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.