PageSourceSearch

https://smartcone.ai/src/shell/types/shellgrid/index.js

js smartcone.ai collected 2026-10-05 15:06:15 UTC 10,407 bytes, 259 lines download raw bytes

1// shell/types/shellgrid/index.js
2//
3// SHELL GRID — a container shell that HOSTS existing shells (ER Diagram, App
4// Builder, …) in cells and lets them cooperate via one SharedContext key bus.
5// Shells stay fully standalone; placed in a grid they opt into the bus.
6//
7// LAYOUT MODES (shell_grid_spec.md §1.3, two of the three built here):
8//   - "grid": the cells side-by-side in a CSS grid (the original proof layout).
9//   - "tabs": a tab bar (one tab per cell) shows ONE cell full-width in the
10//     content area; the others are hidden with display:none — NOT unmounted —
11//     so their state AND the shared-context bus persist across tab switches.
12//
13// One toolbar control toggles Grid <-> Tabs LIVE. Toggling re-renders the
14// LAYOUT only: the hosted shell instances and the single SharedContext are
15// untouched — cells are show/hidden/reflowed, never remounted. This keeps the
16// bus and every shell's internal state intact across a toggle.
17//
18// The grid owns ONE SharedContext and passes it into each hosted shell's
19// mount(el, body, { ...cell.opts, sharedContext: ctx, cellId, shell }). That is
20// the entire shell contract (§3.1): a shell that ignores opts.sharedContext is
21// unaffected.
22
23import { ensureShellGridStyles } from "./styles.js";
24import { createSharedContext } from "../../common/sharedContext.js";
25
26// Hardcoded Studio preset for the proof: ER Diagram in cell 1, App Builder in
27// cell 2. (Spec §4 / §5 — a "Studio" is a saved shellgrid doc; for the proof we
28// use the built-in preset rather than reading a config doc.) Reduced to the two
29// cells + the single `schema` wire the proof needs.
30const PROOF_PRESET = {
31  // Default to TABS: addresses the cramped-real-estate complaint by giving each
32  // hosted shell the full content area. The toolbar toggle flips to Grid live.
33  mode: "tabs",
34  cells: [
35    { id: "c1", shell: "erdiagram", title: "Schema", opts: {} },
36    { id: "c2", shell: "appbuilder", title: "Design", opts: {} },
37  ],
38};
39
40// Read the current repo identity straight from the header inputs (same source
41// as appTray.getCurrentRepoIdentity, appTray.js:807-812; not exported, so we
42// read the DOM directly). For the proof `project` is seeded once at mount.
43function readRepoIdentity() {
44  return {
45    project: (document.getElementById("repoProject")?.value || "").trim() || "MainProject",
46    layout:  (document.getElementById("repoLayout")?.value  || "").trim() || "Main",
47    wsDb:     (() => { try { return (localStorage.getItem("repo.wsDb") || "").trim(); } catch { return ""; } })(),
48  };
49}
50
51export function mount(el, body, opts = {}) {
52  ensureShellGridStyles();
53
54  body.style.position = "relative";
55  body.style.overflow = "hidden";
56
57  const config = PROOF_PRESET;
58
59  // activeMode lives on the grid (not in config / not persisted for the proof).
60  // Toggling it re-renders the layout only — never the hosted shells.
61  let activeMode = config.mode === "grid" ? "grid" : "tabs";
62  // activeCell tracks the visible cell in tabs mode. Defaults to the first cell.
63  let activeCellId = config.cells[0]?.id || null;
64
65  // §2.1 — ONE SharedContext owned by this grid instance. Not a module
66  // singleton: two open grids get two buses and never cross-talk.
67  const ctx = createSharedContext();
68
69  // §2.2 / §7 — seed the live `project` key from the repo header so hosted
70  // shells can read it via the bus (full live re-point on header switch is a
71  // follow-on; the proof seeds once).
72  ctx.setContext("project", readRepoIdentity());
73
74  // ── Chrome: a top toolbar (mode toggle + tab bar) above the cell area ──────
75  const toolbar = document.createElement("div");
76  toolbar.className = "shellgrid-toolbar";
77
78  // Mode toggle: a small segmented control flipping Grid <-> Tabs.
79  const modeToggle = document.createElement("div");
80  modeToggle.className = "shellgrid-modetoggle";
81  modeToggle.setAttribute("role", "group");
82  modeToggle.setAttribute("aria-label", "Layout mode");
83
84  const tabsBtn = document.createElement("button");
85  tabsBtn.type = "button";
86  tabsBtn.className = "shellgrid-modebtn";
87  tabsBtn.dataset.mode = "tabs";
88  tabsBtn.textContent = "Tabs";
89  tabsBtn.title = "Tabs layout — one shell full-width";
90
91  const gridBtn = document.createElement("button");
92  gridBtn.type = "button";
93  gridBtn.className = "shellgrid-modebtn";
94  gridBtn.dataset.mode = "grid";
95  gridBtn.textContent = "Grid";
96  gridBtn.title = "Grid layout — shells side-by-side";
97
98  modeToggle.append(tabsBtn, gridBtn);
99
100  // Tab bar: one tab per cell. Only shown/used in tabs mode.
101  const tabBar = document.createElement("div");
102  tabBar.className = "shellgrid-tabbar";
103  tabBar.setAttribute("role", "tablist");
104
105  toolbar.append(modeToggle, tabBar);
106  body.appendChild(toolbar);
107
108  // Grid host: the cell area. In grid mode it lays cells out in equal columns;
109  // in tabs mode every cell is full-area and all-but-active is display:none.
110  const grid = document.createElement("div");
111  grid.className = "shellgrid-host";
112  body.appendChild(grid);
113
114  const cells = new Map(); // id -> { el, body, mounted, teardown, tab }
115
116  function mountCell(cell) {
117    const cellWrap = document.createElement("div");
118    cellWrap.className = "shellgrid-cell";
119    cellWrap.dataset.cellId = cell.id;
120
121    const titleBar = document.createElement("div");
122    titleBar.className = "shellgrid-cell-title";
123    titleBar.textContent = cell.title || cell.shell;
124
125    const cellBody = document.createElement("div");
126    cellBody.className = "shellgrid-cell-body";
127    cellBody.id = `shellgrid-${cell.id}-${Date.now()}`;
128
129    cellWrap.append(titleBar, cellBody);
130    grid.appendChild(cellWrap);
131
132    // A tab in the tab bar for this cell.
133    const tab = document.createElement("button");
134    tab.type = "button";
135    tab.className = "shellgrid-tab";
136    tab.dataset.cellId = cell.id;
137    tab.setAttribute("role", "tab");
138    tab.textContent = cell.title || cell.shell;
139    tab.addEventListener("click", () => setActiveCell(cell.id));
140    tabBar.appendChild(tab);
141
142    const entry = { el: cellWrap, body: cellBody, mounted: false, teardown: null, tab };
143    cells.set(cell.id, entry);
144
145    // Lazy import the hosted shell — same mechanism every shell uses
146    // (appTray.js:874, 838). Any current/future shell is hostable with zero
147    // per-shell registration here.
148    import(`../${cell.shell}/index.js`)
149      .then((mod) => {
150        if (typeof mod.mount !== "function") {
151          cellBody.textContent = `Shell "${cell.shell}" has no mount().`;
152          return;
153        }
154        // §3.1 — pass the shared bus in. A standalone tray window does NOT, so
155        // the same shell stays standalone-safe.
156        const result = mod.mount(cellBody, cellBody, {
157          ...(cell.opts || {}),
158          // Reuse the host shell handle so hosted shells that read opts.shell
159          // (e.g. for versioning) still work.
160          shell: opts.shell,
161          sharedContext: ctx,
162          cellId: cell.id,
163          shell_: cell.shell,
164        });
165        entry.mounted = true;
166        entry.teardown = () => {
167          try { result?.api?.dispose?.(); } catch (_) {}
168          while (cellWrap.firstChild) cellWrap.removeChild(cellWrap.firstChild);
169          entry.mounted = false;
170        };
171      })
172      .catch((err) => {
173        console.error(`Shell Grid: failed to load cell "${cell.id}" (${cell.shell})`, err);
174        cellBody.textContent = `Failed to load "${cell.shell}": ${err?.message || err}`;
175      });
176  }
177
178  config.cells.forEach(mountCell);
179
180  // ── Layout application (the ONLY thing toggling/tab-switching touches) ─────
181  // Never remounts a cell. Just toggles classes / display so every hosted shell
182  // instance and the single SharedContext survive across toggles and switches.
183  function applyLayout() {
184    grid.classList.toggle("is-grid", activeMode === "grid");
185    grid.classList.toggle("is-tabs", activeMode === "tabs");
186
187    if (activeMode === "grid") {
188      grid.style.gridTemplateColumns = `repeat(${config.cells.length}, minmax(0, 1fr))`;
189    } else {
190      grid.style.gridTemplateColumns = "";
191    }
192
193    // Mode-toggle active state.
194    tabsBtn.classList.toggle("is-active", activeMode === "tabs");
195    gridBtn.classList.toggle("is-active", activeMode === "grid");
196    tabsBtn.setAttribute("aria-pressed", String(activeMode === "tabs"));
197    gridBtn.setAttribute("aria-pressed", String(activeMode === "grid"));
198
199    // Tab bar only matters in tabs mode.
200    toolbar.classList.toggle("tabs-active", activeMode === "tabs");
201
202    // Per-cell visibility.
203    cells.forEach((entry, id) => {
204      const isActive = activeMode === "tabs" ? id === activeCellId : true;
205      // display:none, NOT unmount — shell state + bus subscriptions persist.
206      entry.el.style.display = isActive ? "" : "none";
207      // In tabs mode the per-cell title bar is redundant with the tab; hide it.
208      const tBar = entry.el.querySelector(".shellgrid-cell-title");
209      if (tBar) tBar.style.display = activeMode === "tabs" ? "none" : "";
210      // Tab active state.
211      if (entry.tab) {
212        const tabActive = activeMode === "tabs" && id === activeCellId;
213        entry.tab.classList.toggle("is-active", tabActive);
214        entry.tab.setAttribute("aria-selected", String(tabActive));
215      }
216    });
217  }
218
219  function setMode(mode) {
220    const next = mode === "grid" ? "grid" : "tabs";
221    if (next === activeMode) return;
222    activeMode = next;
223    applyLayout();
224  }
225
226  function setActiveCell(id) {
227    if (!cells.has(id)) return;
228    activeCellId = id;
229    // Switching tabs is show/hide only; the shown shell already reflects current
230    // context because it stayed mounted and subscribed (replay-last bus).
231    applyLayout();
232  }
233
234  tabsBtn.addEventListener("click", () => setMode("tabs"));
235  gridBtn.addEventListener("click", () => setMode("grid"));
236
237  applyLayout();
238
239  function dispose() {
240    cells.forEach((entry) => { try { entry.teardown?.(); } catch (_) {} });
241    cells.clear();
242    while (grid.firstChild) grid.removeChild(grid.firstChild);
243    if (toolbar.parentNode) toolbar.parentNode.removeChild(toolbar);
244  }
245
246  return {
247    api: {
248      getContext: ctx.get,
249      sharedContext: ctx,
250      getMode: () => activeMode,
251      setMode,
252      setActiveCell,
253      dispose,
254    },
255  };
256}
257
258export function extendToolbar() { /* no-op */ }
259export function update()        { /* no-op */ }

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.