PageSourceSearch

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

js smartcone.ai collected 2026-10-05 15:05:51 UTC 6,521 bytes, 140 lines download raw bytes

1// /src/shell/types/studio/index.js
2// The Studio, hosted inside an app-shell so it can be a DOCKED TAB on apps.html
3// instead of a page navigation (Jason: "make studio a tab not a new page").
4//
5// ⛔ WHY AN IFRAME, and not the Studio's modules mounted straight into the shell:
6// studio.html's src/app.js owns `document.body` — it builds the canvas, the bottom
7// app-tray, the drop zone, the floating assistant and the welcome splash, all as
8// body-level singletons keyed by fixed element ids. apps.html already has a body of
9// its own (the launcher grids, the fixed dock strip at top:0, its own shells). Two
10// owners of one body is not a layout problem that CSS can settle — it is duplicate
11// ids and duplicate global listeners. A same-origin iframe gives the Studio the
12// whole document it expects, costs one extra JS context, and leaves studio.html
13// working unchanged as a standalone page.
14//
15// Same-origin matters and is deliberate: NO `sandbox` attribute. The Studio needs
16// localStorage (the session token), sessionStorage and its ws-dba WebSocket, and a
17// sandbox without allow-same-origin takes all three away — it would paint a login
18// page inside the tab. This frames OUR OWN page from OUR OWN origin; the untrusted
19// case (a built app's preview) is appviewer/index.js, which IS sandboxed.
20//
21// Tab-switching hides the shell with display:none (shellDock.js). A hidden iframe
22// keeps its document and its socket alive, which is exactly what makes this a tab
23// rather than a bookmark: leave the Studio, come back, the canvas is as you left it.
24
25const STYLE_ID = "zx-studio-shell-styles";
26
27// The one Studio tab's iframe. There is deliberately only ever one Studio open
28// (appsLauncher.js enforces it): a second would be a second full canvas and a
29// second ws-dba session inside one page.
30let activeFrame = null;
31
32function ensureStyles() {
33  if (document.getElementById(STYLE_ID)) return;
34  const st = document.createElement("style");
35  st.id = STYLE_ID;
36  st.textContent = `
37  .zx-studio-wrap  { width:100%; height:100%; position:relative; overflow:hidden; background:#111827; }
38  .zx-studio-frame { display:block; width:100%; height:100%; border:none; background:#fff; }
39  /* An iframe eats the pointer stream, so a drag or resize started on the shell
40     chrome dies the moment the cursor crosses into the Studio. shell.js already
41     marks the gesture on the shell element; this is the half that lets go. */
42  .app-shell.dragging .zx-studio-frame,
43  .app-shell.resizing .zx-studio-frame { pointer-events: none; }
44  .zx-studio-fail {
45    position:absolute; inset:0; display:flex; flex-direction:column; gap:10px;
46    align-items:center; justify-content:center; padding:24px; text-align:center;
47    font:14px/1.5 system-ui; color:#e5e7eb; background:#111827;
48  }
49  .zx-studio-fail a { color:#93c5fd; }
50  `;
51  document.head.appendChild(st);
52}
53
54/**
55 * Resolve the Studio page next to the CURRENT page, not next to this module.
56 * A bare "./studio.html" in an ES module under src/shell/types/studio/ is read
57 * against the document, so it happens to be right today — but only because
58 * apps.html sits in the same directory as studio.html. Deriving it from
59 * location.href says that out loud instead of relying on it.
60 *
61 * @param {{launch?: string, run?: string}} [opts]
62 * @returns {string}
63 */
64function studioUrl(opts = {}) {
65  const url = new URL("studio.html", window.location.href);
66  // studio.html's autoLaunchFromUrl() reads these: ?run= wins over ?launch=.
67  if (opts.run) url.searchParams.set("run", opts.run);
68  else if (opts.launch) url.searchParams.set("launch", opts.launch);
69  return url.toString();
70}
71
72/**
73 * Mount the Studio into a shell.
74 *
75 * @param {HTMLElement} el    shell.contentArea
76 * @param {HTMLElement} body  the wrapper the loader already appended to `el`
77 * @param {object}      ctx
78 * @param {object}      ctx.shell   the shell handle from createShell()
79 * @param {string}      [ctx.launch] appId to deep-link open inside the Studio
80 * @param {string}      [ctx.run]    appbuilder docId to open in the Studio's viewer
81 */
82export function mount(el, body, { shell, launch, run } = {}) {
83  ensureStyles();
84  body.classList.add("zx-studio-wrap");
85
86  const frame = document.createElement("iframe");
87  frame.className = "zx-studio-frame";
88  frame.title = "ZIM-X Design Studio";
89  // The Studio is a full application, not a document: it must be allowed to go
90  // fullscreen and to reach the clipboard for its copy actions.
91  frame.setAttribute("allow", "clipboard-read; clipboard-write; fullscreen");
92  frame.src = studioUrl({ launch, run });
93
94  // A blank tab is indistinguishable from a slow one, and the Studio is a heavy
95  // page. Say so if the load never lands rather than leaving an empty rectangle.
96  frame.addEventListener("error", () => {
97    const fail = document.createElement("div");
98    fail.className = "zx-studio-fail";
99    fail.innerHTML =
100      "<div><strong>The Studio did not load in this tab.</strong></div>" +
101      `<div>Open it as its own page: <a href="${studioUrl({ launch, run })}">studio.html</a></div>`;
102    body.appendChild(fail);
103  });
104
105  body.appendChild(frame);
106
107  // The shell's file chrome (save / open / export / NDJSON) is meaningless here —
108  // the Studio owns its own documents — so shell.js leaves it off for this
109  // appType. Nothing else to wire: the iframe IS the content.
110  if (shell?.contentArea) shell.contentArea.style.overflow = "hidden";
111
112  activeFrame = frame;
113  return { frame };
114}
115
116/**
117 * Ask the LIVE Studio tab to open an app on its canvas.
118 *
119 * ?launch= only covers the first open — it is read once, at boot. Every later tile
120 * press has to reach a Studio that is already running, and re-pointing the iframe
121 * would reload it and lose the canvas. src/app.js listenForHostLaunch() is the
122 * other end; it checks the origin, so the targetOrigin here is not decorative.
123 *
124 * @param {string} appId  a src/appsCatalog.js id
125 * @returns {boolean} false when there is no live Studio frame to talk to
126 */
127export function postStudioLaunch(appId) {
128  if (!appId) return false;
129  // A closed shell leaves a detached iframe behind, and posting into one is a
130  // silent no-op — exactly the failure that looks like "the tile does nothing".
131  if (!activeFrame || !activeFrame.isConnected || !activeFrame.contentWindow) {
132    activeFrame = null;
133    return false;
134  }
135  activeFrame.contentWindow.postMessage(
136    { type: "zimx:studio-launch", appId },
137    window.location.origin,
138  );
139  return true;
140}

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.