@jxsuite/studio 0.23.2 → 0.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/dist/studio.js +2311 -718
  2. package/dist/studio.js.map +121 -104
  3. package/package.json +8 -7
  4. package/src/browse/{browse-modal.js → browse-modal.ts} +9 -10
  5. package/src/browse/{browse.js → browse.ts} +109 -84
  6. package/src/canvas/{canvas-diff.js → canvas-diff.ts} +42 -35
  7. package/src/canvas/{canvas-helpers.js → canvas-helpers.ts} +25 -19
  8. package/src/canvas/{canvas-live-render.js → canvas-live-render.ts} +53 -63
  9. package/src/canvas/{canvas-render.js → canvas-render.ts} +110 -121
  10. package/src/canvas/{canvas-utils.js → canvas-utils.ts} +48 -44
  11. package/src/canvas/{nested-site-style.js → nested-site-style.ts} +3 -3
  12. package/src/editor/{component-inline-edit.js → component-inline-edit.ts} +45 -33
  13. package/src/editor/{content-inline-edit.js → content-inline-edit.ts} +27 -45
  14. package/src/editor/{context-menu.js → context-menu.ts} +38 -36
  15. package/src/editor/{convert-targets.js → convert-targets.ts} +15 -12
  16. package/src/editor/{convert-to-component.js → convert-to-component.ts} +25 -26
  17. package/src/editor/{convert-to-repeater.js → convert-to-repeater.ts} +60 -40
  18. package/src/editor/{inline-edit.js → inline-edit.ts} +91 -55
  19. package/src/editor/{inline-format.js → inline-format.ts} +46 -51
  20. package/src/editor/{insertion-helper.js → insertion-helper.ts} +47 -63
  21. package/src/editor/{shortcuts.js → shortcuts.ts} +45 -31
  22. package/src/editor/{slash-menu.js → slash-menu.ts} +34 -35
  23. package/src/files/{components.js → components.ts} +12 -15
  24. package/src/files/{file-ops.js → file-ops.ts} +36 -29
  25. package/src/files/{files.js → files.ts} +102 -84
  26. package/src/github/{github-auth.js → github-auth.ts} +3 -3
  27. package/src/github/{github-publish.js → github-publish.ts} +20 -18
  28. package/src/markdown/{md-allowlist.js → md-allowlist.ts} +5 -2
  29. package/src/markdown/{md-convert.js → md-convert.ts} +146 -169
  30. package/src/new-project/{new-project-modal.js → new-project-modal.ts} +16 -18
  31. package/src/panels/{activity-bar.js → activity-bar.ts} +26 -34
  32. package/src/panels/{ai-panel.js → ai-panel.ts} +30 -30
  33. package/src/panels/{block-action-bar.js → block-action-bar.ts} +64 -73
  34. package/src/panels/{canvas-dnd.js → canvas-dnd.ts} +44 -49
  35. package/src/panels/{data-explorer.js → data-explorer.ts} +21 -15
  36. package/src/panels/dnd.ts +417 -0
  37. package/src/panels/{editors.js → editors.ts} +43 -50
  38. package/src/panels/{elements-panel.js → elements-panel.ts} +30 -27
  39. package/src/panels/{events-panel.js → events-panel.ts} +33 -25
  40. package/src/panels/{git-panel.js → git-panel.ts} +97 -83
  41. package/src/panels/{head-panel.js → head-panel.ts} +117 -139
  42. package/src/panels/{imports-panel.js → imports-panel.ts} +69 -89
  43. package/src/panels/{layers-panel.js → layers-panel.ts} +61 -57
  44. package/src/panels/{left-panel.js → left-panel.ts} +56 -63
  45. package/src/panels/{overlays.js → overlays.ts} +28 -36
  46. package/src/panels/{panel-events.js → panel-events.ts} +53 -51
  47. package/src/panels/{preview-render.js → preview-render.ts} +20 -15
  48. package/src/panels/{properties-panel.js → properties-panel.ts} +212 -246
  49. package/src/panels/{pseudo-preview.js → pseudo-preview.ts} +9 -8
  50. package/src/panels/{quick-search.js → quick-search.ts} +16 -17
  51. package/src/panels/{right-panel.js → right-panel.ts} +41 -59
  52. package/src/panels/{shared.js → shared.ts} +8 -6
  53. package/src/panels/{signals-panel.js → signals-panel.ts} +322 -362
  54. package/src/panels/{statusbar.js → statusbar.ts} +14 -14
  55. package/src/panels/{style-inputs.js → style-inputs.ts} +66 -48
  56. package/src/panels/{style-panel.js → style-panel.ts} +235 -290
  57. package/src/panels/{style-utils.js → style-utils.ts} +32 -31
  58. package/src/panels/{stylebook-layers-panel.js → stylebook-layers-panel.ts} +22 -14
  59. package/src/panels/{stylebook-panel.js → stylebook-panel.ts} +127 -140
  60. package/src/panels/{tab-strip.js → tab-strip.ts} +12 -14
  61. package/src/panels/{toolbar.js → toolbar.ts} +68 -70
  62. package/src/panels/{welcome-screen.js → welcome-screen.ts} +14 -16
  63. package/src/{platform.js → platform.ts} +5 -3
  64. package/src/platforms/{devserver.js → devserver.ts} +53 -44
  65. package/src/{recent-projects.js → recent-projects.ts} +17 -12
  66. package/src/{resize-edges.js → resize-edges.ts} +18 -13
  67. package/src/services/{cem-export.js → cem-export.ts} +14 -7
  68. package/src/services/{code-services.js → code-services.ts} +29 -20
  69. package/src/services/{monaco-setup.js → monaco-setup.ts} +2 -3
  70. package/src/settings/{content-types-editor.js → content-types-editor.ts} +88 -67
  71. package/src/settings/{css-vars-editor.js → css-vars-editor.ts} +78 -51
  72. package/src/settings/{defs-editor.js → defs-editor.ts} +70 -56
  73. package/src/settings/{general-settings.js → general-settings.ts} +29 -28
  74. package/src/settings/{head-editor.js → head-editor.ts} +37 -35
  75. package/src/settings/{schema-field-ui.js → schema-field-ui.ts} +86 -76
  76. package/src/settings/{settings-modal.js → settings-modal.ts} +14 -16
  77. package/src/{site-context.js → site-context.ts} +35 -32
  78. package/src/{state.js → state.ts} +95 -78
  79. package/src/{store.js → store.ts} +60 -63
  80. package/src/{studio.js → studio.ts} +117 -119
  81. package/src/tabs/tab.ts +196 -0
  82. package/src/tabs/{transact.js → transact.ts} +107 -60
  83. package/src/types.ts +185 -0
  84. package/src/ui/{button-group.js → button-group.ts} +21 -21
  85. package/src/ui/{color-selector.js → color-selector.ts} +44 -37
  86. package/src/ui/{expression-editor.js → expression-editor.ts} +65 -40
  87. package/src/ui/{field-row.js → field-row.ts} +19 -2
  88. package/src/ui/{icons.js → icons.ts} +2 -2
  89. package/src/ui/{layers.js → layers.ts} +33 -24
  90. package/src/ui/{media-picker.js → media-picker.ts} +13 -9
  91. package/src/ui/{panel-resize.js → panel-resize.ts} +9 -4
  92. package/src/ui/{spectrum.js → spectrum.ts} +4 -4
  93. package/src/ui/{unit-selector.js → unit-selector.ts} +16 -19
  94. package/src/ui/{value-selector.js → value-selector.ts} +28 -28
  95. package/src/ui/{widgets.js → widgets.ts} +62 -15
  96. package/src/utils/{canvas-media.js → canvas-media.ts} +29 -14
  97. package/src/utils/edit-display.ts +298 -0
  98. package/src/utils/{google-fonts.js → google-fonts.ts} +14 -13
  99. package/src/utils/{inherited-style.js → inherited-style.ts} +15 -10
  100. package/src/utils/{studio-utils.js → studio-utils.ts} +35 -23
  101. package/src/{view.js → view.ts} +53 -55
  102. package/src/workspace/{workspace.js → workspace.ts} +44 -33
  103. package/src/panels/dnd.js +0 -426
  104. package/src/tabs/tab.js +0 -192
  105. package/src/utils/edit-display.js +0 -203
  106. /package/src/{reactivity.js → reactivity.ts} +0 -0
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * Studio.js — Jx Studio main application
3
4
  *
@@ -18,28 +19,28 @@ import {
18
19
  requireProjectState,
19
20
  updateUi,
20
21
  initShellRefs,
21
- } from "./store.js";
22
+ } from "./store";
22
23
 
23
- import { activeTab, openTab, closeAllTabs } from "./workspace/workspace.js";
24
- import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "./tabs/transact.js";
25
- import { effect } from "./reactivity.js";
24
+ import { activeTab, openTab, closeAllTabs } from "./workspace/workspace";
25
+ import { transactDoc, mutateUpdateDef, mutateUpdateProperty } from "./tabs/transact";
26
+ import { effect } from "./reactivity";
26
27
 
27
- import { view } from "./view.js";
28
+ import { view } from "./view";
28
29
 
29
- import { isEditing, isEditableBlock } from "./editor/inline-edit.js";
30
- import { initComponentInlineEdit } from "./editor/component-inline-edit.js";
31
- import { enterInlineEdit } from "./editor/content-inline-edit.js";
32
- import { initCanvasUtils, applyTransform, positionZoomIndicator } from "./canvas/canvas-utils.js";
33
- import { initCanvasHelpers, getActivePanel, findCanvasElement } from "./canvas/canvas-helpers.js";
34
- import { initCanvasRender, renderCanvas } from "./canvas/canvas-render.js";
35
- import { initCanvasLiveRender } from "./canvas/canvas-live-render.js";
30
+ import { isEditing, isEditableBlock } from "./editor/inline-edit";
31
+ import { initComponentInlineEdit } from "./editor/component-inline-edit";
32
+ import { enterInlineEdit } from "./editor/content-inline-edit";
33
+ import { initCanvasUtils, applyTransform, positionZoomIndicator } from "./canvas/canvas-utils";
34
+ import { initCanvasHelpers, getActivePanel, findCanvasElement } from "./canvas/canvas-helpers";
35
+ import { initCanvasRender, renderCanvas } from "./canvas/canvas-render";
36
+ import { initCanvasLiveRender } from "./canvas/canvas-live-render";
36
37
  import {
37
38
  renderStatusbar,
38
39
  statusMessage,
39
40
  setStatusbarRenderer,
40
41
  mountStatusbar,
41
- } from "./panels/statusbar.js";
42
- import { loadMarkdown, saveFile, exportFile, serializeDocument } from "./files/file-ops.js";
42
+ } from "./panels/statusbar";
43
+ import { loadMarkdown, saveFile, exportFile, serializeDocument } from "./files/file-ops";
43
44
  import {
44
45
  loadProject as _loadProject,
45
46
  openProject as _openProject,
@@ -49,51 +50,55 @@ import {
49
50
  setupTreeKeyboard,
50
51
  registerFileTreeDnD,
51
52
  loadDirectory,
52
- } from "./files/files.js";
53
- import { renderImportsTemplate } from "./panels/imports-panel.js";
54
- import { renderHeadTemplate } from "./panels/head-panel.js";
55
- import { exportCemManifest as _exportCemManifest } from "./services/cem-export.js";
56
-
57
- import { registerPlatform, getPlatform, hasPlatform } from "./platform.js";
58
- import { parseMediaEntries } from "./utils/canvas-media.js";
59
- import { createDevServerPlatform } from "./platforms/devserver.js";
60
- import { mountResizeEdges } from "./resize-edges.js";
61
- import { codeService } from "./services/code-services.js";
62
- import { defCategory, defBadgeLabel, renderSignalsTemplate } from "./panels/signals-panel.js";
63
- import { loadComponentRegistry } from "./files/components.js";
53
+ } from "./files/files";
54
+ import { renderImportsTemplate } from "./panels/imports-panel";
55
+ import { renderHeadTemplate } from "./panels/head-panel";
56
+ import { exportCemManifest as _exportCemManifest } from "./services/cem-export";
57
+
58
+ import { registerPlatform, getPlatform, hasPlatform } from "./platform";
59
+ import { parseMediaEntries } from "./utils/canvas-media";
60
+ import { createDevServerPlatform } from "./platforms/devserver";
61
+ import { mountResizeEdges } from "./resize-edges";
62
+ import { codeService } from "./services/code-services";
63
+ import { defCategory, defBadgeLabel, renderSignalsTemplate } from "./panels/signals-panel";
64
+ import { loadComponentRegistry } from "./files/components";
64
65
 
65
66
  import { html, render as litRender } from "lit-html";
66
67
 
67
68
  import webdata from "../data/webdata.json";
68
- import { renderDataExplorerTemplate } from "./panels/data-explorer.js";
69
- import { renderGitPanel } from "./panels/git-panel.js";
69
+ import { renderDataExplorerTemplate } from "./panels/data-explorer";
70
+ import { renderGitPanel } from "./panels/git-panel";
70
71
 
71
72
  // ─── Spectrum Web Components ──────────────────────────────────────────────────
72
73
  // Explicit class imports + registration — bare side-effect imports are tree-shaken
73
74
  // by Bun's bundler despite sideEffects declarations in Spectrum's package.json.
74
- import { components as _swc } from "./ui/spectrum.js"; // eslint-disable-line no-unused-vars
75
+ import { components as _swc } from "./ui/spectrum";
76
+ void _swc;
75
77
  import "./ui/panel-resize.js";
76
- import { initLayers } from "./ui/layers.js";
77
- import { initShortcuts } from "./editor/shortcuts.js";
78
- import { renderActivityBar, mount as mountActivityBar } from "./panels/activity-bar.js";
79
- import * as toolbarPanel from "./panels/toolbar.js";
80
- import * as overlaysPanel from "./panels/overlays.js";
81
- import * as rightPanelMod from "./panels/right-panel.js";
82
- import * as leftPanelMod from "./panels/left-panel.js";
83
- import * as tabStrip from "./panels/tab-strip.js";
84
- import { renderStylebookOverlays } from "./panels/stylebook-panel.js";
85
- import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from "./panels/dnd.js";
86
- import { defaultDef } from "./panels/shared.js";
87
- import { registerFunctionCompletions } from "./panels/editors.js";
88
- import { renderBlockActionBar, initBlockActionBar } from "./panels/block-action-bar.js";
89
- import { initCssData } from "./panels/style-utils.js";
90
- import { updateForcedPseudoPreview } from "./panels/pseudo-preview.js";
91
- import { initPanelEvents } from "./panels/panel-events.js";
92
- import { initQuickSearch } from "./panels/quick-search.js";
93
- import { addRecentProject } from "./recent-projects.js";
94
- import { initWelcome } from "./panels/welcome-screen.js";
95
- import { openNewProjectModal } from "./new-project/new-project-modal.js";
96
- import { cloneRepository } from "./panels/git-panel.js";
78
+ import { initLayers } from "./ui/layers";
79
+ import { initShortcuts } from "./editor/shortcuts";
80
+ import { renderActivityBar, mount as mountActivityBar } from "./panels/activity-bar";
81
+ import * as toolbarPanel from "./panels/toolbar";
82
+ import * as overlaysPanel from "./panels/overlays";
83
+ import * as rightPanelMod from "./panels/right-panel";
84
+ import * as leftPanelMod from "./panels/left-panel";
85
+ import * as tabStrip from "./panels/tab-strip";
86
+ import { renderStylebookOverlays } from "./panels/stylebook-panel";
87
+ import { registerLayersDnD, registerComponentsDnD, registerElementsDnD } from "./panels/dnd";
88
+ import { defaultDef } from "./panels/shared";
89
+ import { registerFunctionCompletions } from "./panels/editors";
90
+ import { renderBlockActionBar, initBlockActionBar } from "./panels/block-action-bar";
91
+ import { initCssData } from "./panels/style-utils";
92
+ import { updateForcedPseudoPreview } from "./panels/pseudo-preview";
93
+ import { initPanelEvents } from "./panels/panel-events";
94
+ import { initQuickSearch } from "./panels/quick-search";
95
+ import { addRecentProject } from "./recent-projects";
96
+ import { initWelcome } from "./panels/welcome-screen";
97
+ import { openNewProjectModal } from "./new-project/new-project-modal";
98
+ import { cloneRepository } from "./panels/git-panel";
99
+ import type { DocumentStackEntry, GitDiffState } from "./types";
100
+ import type { JxPath } from "./state";
101
+ import type { JxMutableNode } from "@jxsuite/schema/types";
97
102
 
98
103
  // ─── Globals ──────────────────────────────────────────────────────────────────
99
104
  // These mutable variables are local to studio.js for now. As sections are extracted
@@ -104,7 +109,7 @@ function getCanvasMode() {
104
109
  }
105
110
 
106
111
  /** @param {string} mode */
107
- function setCanvasMode(mode) {
112
+ function setCanvasMode(mode: string) {
108
113
  if (getCanvasMode() === "git-diff" && mode !== "git-diff") {
109
114
  gitDiffState = null;
110
115
  }
@@ -112,13 +117,12 @@ function setCanvasMode(mode) {
112
117
  if (tab) tab.session.ui.canvasMode = mode;
113
118
  }
114
119
 
115
- /** @type {import("./canvas/canvas-render.js").GitDiffState | null} */
116
- let gitDiffState = null;
120
+ let gitDiffState: GitDiffState | null = null;
117
121
 
118
122
  // ─── Component registry ───────────────────────────────────────────────────────
119
123
 
120
124
  /** @param {string} componentPath */
121
- async function navigateToComponent(componentPath) {
125
+ async function navigateToComponent(componentPath: string) {
122
126
  try {
123
127
  const platform = getPlatform();
124
128
  const content = await platform.readFile(componentPath);
@@ -142,7 +146,7 @@ async function navigateToComponent(componentPath) {
142
146
  // Load the component
143
147
  tab.doc.document = parsed;
144
148
  tab.doc.dirty = false;
145
- tab.doc.mode = /** @type {string} */ (/** @type {unknown} */ (null));
149
+ tab.doc.mode = null as unknown as string;
146
150
  tab.doc.sourceFormat = null;
147
151
  tab.documentPath = componentPath;
148
152
  tab.session.selection = null;
@@ -152,8 +156,8 @@ async function navigateToComponent(componentPath) {
152
156
 
153
157
  render();
154
158
  statusMessage(`Editing component: ${parsed.tagName || componentPath}`);
155
- } catch (/** @type {unknown} */ e) {
156
- const err = /** @type {Error} */ (e);
159
+ } catch (e) {
160
+ const err = e as Error;
157
161
  statusMessage(`Error: ${err.message}`);
158
162
  }
159
163
  }
@@ -165,23 +169,21 @@ async function navigateBack() {
165
169
  try {
166
170
  const platform = getPlatform();
167
171
  await platform.writeFile(tab.documentPath, serializeDocument(tab));
168
- } catch (/** @type {unknown} */ e) {
169
- const err = /** @type {Error} */ (e);
172
+ } catch (e) {
173
+ const err = e as Error;
170
174
  statusMessage(`Save error: ${err.message}`);
171
175
  }
172
176
  }
173
177
 
174
178
  // Pop the stack
175
- const frame = /** @type {Record<string, unknown> | undefined} */ (
176
- tab.session.documentStack.pop()
177
- );
179
+ const frame = tab.session.documentStack.pop() as Record<string, unknown> | undefined;
178
180
  if (!frame) return;
179
- tab.doc.document = /** @type {JxMutableNode} */ (frame.document);
180
- tab.doc.dirty = /** @type {boolean} */ (frame.dirty);
181
- tab.doc.mode = /** @type {string} */ (frame.mode);
182
- tab.doc.sourceFormat = /** @type {string | null} */ (frame.sourceFormat);
183
- tab.documentPath = /** @type {string | null} */ (frame.documentPath);
184
- tab.session.selection = /** @type {JxPath | null} */ (frame.selection);
181
+ tab.doc.document = frame.document as JxMutableNode;
182
+ tab.doc.dirty = frame.dirty as boolean;
183
+ tab.doc.mode = frame.mode as string;
184
+ tab.doc.sourceFormat = frame.sourceFormat as string | null;
185
+ tab.documentPath = frame.documentPath as string | null;
186
+ tab.session.selection = frame.selection as JxPath | null;
185
187
  view.leftTab = "layers";
186
188
 
187
189
  render();
@@ -189,7 +191,7 @@ async function navigateBack() {
189
191
  }
190
192
 
191
193
  /** @param {number} targetIndex */
192
- async function navigateToLevel(targetIndex) {
194
+ async function navigateToLevel(targetIndex: number) {
193
195
  const tab = activeTab.value;
194
196
  const stack = tab?.session.documentStack;
195
197
  if (!stack || targetIndex < 0 || targetIndex >= stack.length) return;
@@ -197,20 +199,20 @@ async function navigateToLevel(targetIndex) {
197
199
  try {
198
200
  const platform = getPlatform();
199
201
  await platform.writeFile(tab.documentPath, serializeDocument(tab));
200
- } catch (/** @type {unknown} */ e) {
201
- const err = /** @type {Error} */ (e);
202
+ } catch (e) {
203
+ const err = e as Error;
202
204
  statusMessage(`Save error: ${err.message}`);
203
205
  }
204
206
  }
205
207
 
206
- const frame = /** @type {DocumentStackEntry} */ (stack[targetIndex]);
208
+ const frame = stack[targetIndex] as DocumentStackEntry;
207
209
  tab.session.documentStack = stack.slice(0, targetIndex);
208
- tab.doc.document = /** @type {JxMutableNode} */ (frame.document);
209
- tab.doc.dirty = /** @type {boolean} */ (frame.dirty);
210
- tab.doc.mode = /** @type {string} */ (frame.mode);
211
- tab.doc.sourceFormat = /** @type {string | null} */ (frame.sourceFormat);
212
- tab.documentPath = /** @type {string | null} */ (frame.documentPath);
213
- tab.session.selection = /** @type {JxPath | null} */ (frame.selection);
210
+ tab.doc.document = frame.document as JxMutableNode;
211
+ tab.doc.dirty = frame.dirty as boolean;
212
+ tab.doc.mode = frame.mode as string;
213
+ tab.doc.sourceFormat = frame.sourceFormat as string | null;
214
+ tab.documentPath = frame.documentPath as string | null;
215
+ tab.session.selection = frame.selection as JxPath | null;
214
216
  view.leftTab = "layers";
215
217
 
216
218
  render();
@@ -229,23 +231,16 @@ async function closeFunctionEditor() {
229
231
  const minResult = await codeService("minify", { code: currentCode });
230
232
  const bodyToStore = minResult?.code ?? currentCode;
231
233
  if (editing.type === "def") {
232
- transactDoc(tab, (t) =>
233
- mutateUpdateDef(t, /** @type {string} */ (editing.defName), { body: bodyToStore }),
234
- );
234
+ transactDoc(tab, (t) => mutateUpdateDef(t, editing.defName as string, { body: bodyToStore }));
235
235
  } else if (editing.type === "event") {
236
- const node = getNodeAtPath(tab.doc.document, /** @type {JxPath} */ (editing.path));
237
- const current = node?.[/** @type {string} */ (editing.eventKey)] || {};
236
+ const node = getNodeAtPath(tab.doc.document, editing.path as JxPath);
237
+ const current = node?.[editing.eventKey as string] || {};
238
238
  transactDoc(tab, (t) =>
239
- mutateUpdateProperty(
240
- t,
241
- /** @type {JxPath} */ (editing.path),
242
- /** @type {string} */ (editing.eventKey),
243
- {
244
- .../** @type {object} */ (current),
245
- $prototype: "Function",
246
- body: bodyToStore,
247
- },
248
- ),
239
+ mutateUpdateProperty(t, editing.path as JxPath, editing.eventKey as string, {
240
+ .../** @type {object} */ (current),
241
+ $prototype: "Function",
242
+ body: bodyToStore,
243
+ }),
249
244
  );
250
245
  }
251
246
  view.functionEditor.dispose();
@@ -262,17 +257,25 @@ document.body.appendChild(datalistHost);
262
257
  litRender(
263
258
  html`
264
259
  <datalist id="tag-names">
265
- ${webdata.allTags.map((/** @type {string} */ tag) => html`<option value=${tag}></option>`)}
266
- </datalist>
267
- <datalist id="css-props">
268
- ${webdata.cssProps.map(
269
- (/** @type {string[]} */ [name]) => html`<option value=${name}></option>`,
270
- )}
260
+ ${webdata.allTags.map((tag: string) => html`<option value=${tag}></option>`)}
271
261
  </datalist>
262
+ <datalist id="css-props"></datalist>
272
263
  `,
273
264
  datalistHost,
274
265
  );
275
266
 
267
+ requestIdleCallback(() => {
268
+ const dl = document.getElementById("css-props");
269
+ if (!dl) return;
270
+ const frag = document.createDocumentFragment();
271
+ for (const [name] of webdata.cssProps) {
272
+ const opt = document.createElement("option");
273
+ opt.value = name;
274
+ frag.appendChild(opt);
275
+ }
276
+ dl.appendChild(frag);
277
+ });
278
+
276
279
  initCssData(webdata);
277
280
 
278
281
  // ─── Module-level UI state (must be before render() call) ─────────────────────
@@ -293,10 +296,10 @@ initShellRefs();
293
296
  // Mount extracted panel modules
294
297
  toolbarPanel.mount(toolbarEl, {
295
298
  navigateBack: () => navigateBack(),
296
- navigateToLevel: (/** @type {number} */ i) => navigateToLevel(i),
299
+ navigateToLevel: (i: number) => navigateToLevel(i),
297
300
  closeFunctionEditor: () => closeFunctionEditor(),
298
301
  openProject: () => openProject(),
299
- openRecentProject: (/** @type {string} */ root) => openRecentProject(root),
302
+ openRecentProject: (root: string) => openRecentProject(root),
300
303
  saveFile: () => saveFile(),
301
304
  parseMediaEntries,
302
305
  getCanvasMode,
@@ -308,7 +311,7 @@ toolbarPanel.mount(toolbarEl, {
308
311
  initLayers();
309
312
  initQuickSearch();
310
313
 
311
- tabStrip.mount(/** @type {HTMLElement} */ (document.querySelector("#tab-strip")));
314
+ tabStrip.mount(document.querySelector("#tab-strip") as HTMLElement);
312
315
 
313
316
  overlaysPanel.mount({
314
317
  getCanvasMode,
@@ -351,16 +354,14 @@ initCanvasRender({
351
354
  get gitDiffState() {
352
355
  return gitDiffState;
353
356
  },
354
- setGitDiffState: (
355
- /** @type {import("./canvas/canvas-render.js").GitDiffState | null} */ state,
356
- ) => {
357
+ setGitDiffState: (state: GitDiffState | null) => {
357
358
  gitDiffState = state;
358
359
  },
359
360
  });
360
361
 
361
362
  initWelcome({
362
363
  openProject: () => openProject(),
363
- openRecentProject: (/** @type {string} */ root) => openRecentProject(root),
364
+ openRecentProject: (root: string) => openRecentProject(root),
364
365
  openNewProject: async () => {
365
366
  const result = await openNewProjectModal();
366
367
  if (result) openRecentProject(result.root);
@@ -426,9 +427,7 @@ leftPanelMod.mount({
426
427
  setupTreeKeyboard,
427
428
  registerFileTreeDnD,
428
429
  cloneRepository: () => cloneRepository({ openRecentProject }),
429
- setGitDiffState: (
430
- /** @type {import("./canvas/canvas-render.js").GitDiffState | null} */ state,
431
- ) => {
430
+ setGitDiffState: (state: GitDiffState | null) => {
432
431
  gitDiffState = state;
433
432
  },
434
433
  });
@@ -444,7 +443,7 @@ mountStatusbar();
444
443
  mountActivityBar();
445
444
 
446
445
  // Clicking on the canvas-wrap background (outside any canvas panel) deselects the current element
447
- canvasWrap.addEventListener("click", (/** @type {MouseEvent} */ e) => {
446
+ canvasWrap.addEventListener("click", (e: MouseEvent) => {
448
447
  if (e.target !== canvasWrap && e.target !== view.panzoomWrap) return;
449
448
  if (!activeTab.value?.session.selection) return;
450
449
  activeTab.value.session.selection = null;
@@ -560,7 +559,7 @@ if (_projectParam) {
560
559
  id: fileRelPath,
561
560
  documentPath: fileRelPath,
562
561
  document: parsedDoc,
563
- frontmatter,
562
+ ...(frontmatter != null && { frontmatter }),
564
563
  sourceFormat: isMd ? "md" : null,
565
564
  });
566
565
 
@@ -572,8 +571,8 @@ if (_projectParam) {
572
571
  render();
573
572
  statusMessage(`Opened ${fileRelPath}`);
574
573
  }
575
- } catch (/** @type {unknown} */ e) {
576
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
574
+ } catch (e) {
575
+ statusMessage(`Error: ${(e as Error).message}`);
577
576
  }
578
577
  })();
579
578
  }
@@ -619,7 +618,7 @@ function openProject() {
619
618
  renderLeftPanel,
620
619
  });
621
620
  }
622
- async function openRecentProject(/** @type {string} */ root) {
621
+ async function openRecentProject(root: string) {
623
622
  try {
624
623
  const platform = getPlatform();
625
624
  platform.projectRoot = root;
@@ -667,14 +666,14 @@ async function openRecentProject(/** @type {string} */ root) {
667
666
  statusMessage(`Opened project: ${requireProjectState().name}`);
668
667
 
669
668
  await openHomePage();
670
- } catch (/** @type {unknown} */ e) {
671
- statusMessage(`Error: ${/** @type {Error} */ (e).message}`);
669
+ } catch (e) {
670
+ statusMessage(`Error: ${(e as Error).message}`);
672
671
  }
673
672
  }
674
673
  function renderFilesTemplate() {
675
674
  return _renderFilesTemplate({ openProject, openFileFromTree, renderLeftPanel });
676
675
  }
677
- function openFileFromTree(/** @type {string} */ path) {
676
+ function openFileFromTree(path: string) {
678
677
  return openFileInTab(path);
679
678
  }
680
679
 
@@ -708,8 +707,7 @@ initShortcuts(() => ({
708
707
 
709
708
  // ─── Autosave (registered as update middleware) ──────────────────────────────
710
709
 
711
- /** @type {number} */
712
- const AUTO_SAVE_DELAY = 2000;
710
+ const AUTO_SAVE_DELAY: number = 2000;
713
711
 
714
712
  function scheduleAutosave() {
715
713
  const tab = activeTab.value;
@@ -0,0 +1,196 @@
1
+ /// <reference lib="dom" />
2
+ import { reactive, effectScope } from "../reactivity";
3
+ import type {
4
+ GitDiffState,
5
+ InlineEditDef,
6
+ FunctionEditDef,
7
+ DocumentStackEntry,
8
+ GitStatusResult,
9
+ GitBranchesResult,
10
+ } from "../types";
11
+ import type { JxMutableNode } from "@jxsuite/schema/types";
12
+
13
+ export interface TabUi {
14
+ rightTab: string;
15
+ canvasMode: string;
16
+ zoom: number;
17
+ activeMedia: string | null;
18
+ activeSelector: string | null;
19
+ editingFunction: FunctionEditDef | null;
20
+ featureToggles: Record<string, boolean>;
21
+ styleSections: Record<string, boolean>;
22
+ inspectorSections: Record<string, boolean>;
23
+ styleShorthands: Record<string, boolean>;
24
+ styleFilter: string;
25
+ styleFilterActive: boolean;
26
+ stylebookSelection: string | null;
27
+ stylebookTab: string;
28
+ stylebookFilter: string;
29
+ stylebookCustomizedOnly: boolean;
30
+ settingsTab: string;
31
+ gitStatus: GitStatusResult | null;
32
+ gitBranches: GitBranchesResult | null;
33
+ gitCommitMessage: string;
34
+ gitLoading: boolean;
35
+ gitError: string | null;
36
+ gitDiffState: GitDiffState | null;
37
+ pendingInlineEdit: InlineEditDef | null;
38
+ }
39
+
40
+ interface HistorySnapshot {
41
+ document: Record<string, unknown>;
42
+ selection: (string | number)[] | null;
43
+ }
44
+
45
+ export interface Tab {
46
+ id: string;
47
+ documentPath: string | null;
48
+ fileHandle: FileSystemFileHandle | null;
49
+ capabilities: { modes: string[] };
50
+ scope: { stop(): void; run<T>(fn: () => T): T | undefined; [k: string]: unknown };
51
+ doc: {
52
+ document: JxMutableNode;
53
+ content: { frontmatter: Record<string, unknown> };
54
+ mode: string;
55
+ sourceFormat: string | null;
56
+ handlersSource: string | null;
57
+ dirty: boolean;
58
+ };
59
+ session: {
60
+ selection: (string | number)[] | null;
61
+ hover: (string | number)[] | null;
62
+ clipboard: JxMutableNode | null;
63
+ documentStack: DocumentStackEntry[];
64
+ ui: TabUi;
65
+ canvas: {
66
+ status: string;
67
+ scope: { stop(): void; [k: string]: unknown } | null;
68
+ error: string | null;
69
+ pendingInlineEdit: InlineEditDef | null;
70
+ };
71
+ };
72
+ history: {
73
+ snapshots: HistorySnapshot[];
74
+ index: number;
75
+ };
76
+ }
77
+
78
+ /** @returns {TabUi} */
79
+ function createDefaultUi() {
80
+ return {
81
+ rightTab: "properties",
82
+ canvasMode: "edit",
83
+ zoom: 1,
84
+ activeMedia: null,
85
+ activeSelector: null,
86
+ editingFunction: null,
87
+ featureToggles: {},
88
+ styleSections: {},
89
+ inspectorSections: {},
90
+ styleShorthands: {},
91
+ styleFilter: "",
92
+ styleFilterActive: false,
93
+ stylebookSelection: null,
94
+ stylebookTab: "elements",
95
+ stylebookFilter: "",
96
+ stylebookCustomizedOnly: false,
97
+ settingsTab: "stylebook",
98
+ gitStatus: null,
99
+ gitBranches: null,
100
+ gitCommitMessage: "",
101
+ gitLoading: false,
102
+ gitError: null,
103
+ gitDiffState: null,
104
+ pendingInlineEdit: null,
105
+ };
106
+ }
107
+
108
+ const ALL_MODES = ["edit", "design", "preview", "source", "stylebook"];
109
+
110
+ /**
111
+ * Create a new tab with reactive doc/session/history trees, owned by an effectScope.
112
+ *
113
+ * @param {{
114
+ * id: string;
115
+ * documentPath?: string | null;
116
+ * fileHandle?: FileSystemFileHandle | null;
117
+ * document: Record<string, unknown>;
118
+ * frontmatter?: Record<string, unknown>;
119
+ * sourceFormat?: string | null;
120
+ * capabilities?: { modes?: string[] };
121
+ * }} opts
122
+ * @returns {Tab}
123
+ */
124
+ export function createTab({
125
+ id,
126
+ documentPath = null,
127
+ fileHandle = null,
128
+ document,
129
+ frontmatter,
130
+ sourceFormat = null,
131
+ capabilities,
132
+ }: {
133
+ id: string;
134
+ documentPath?: string | null;
135
+ fileHandle?: FileSystemFileHandle | null;
136
+ document: Record<string, unknown>;
137
+ frontmatter?: Record<string, unknown>;
138
+ sourceFormat?: string | null;
139
+ capabilities?: { modes?: string[] };
140
+ }) {
141
+ const scope = effectScope();
142
+
143
+ const resolvedModes = capabilities?.modes ?? inferModes(documentPath, sourceFormat);
144
+
145
+ const tab = scope.run(() => ({
146
+ id,
147
+ documentPath,
148
+ fileHandle,
149
+ capabilities: { modes: resolvedModes },
150
+ scope,
151
+ doc: reactive({
152
+ document,
153
+ sourceFormat,
154
+ content: { frontmatter: frontmatter || {} },
155
+ mode:
156
+ sourceFormat === "md" ? "content" : documentPath?.endsWith(".md") ? "content" : "component",
157
+ handlersSource: null,
158
+ dirty: false,
159
+ }),
160
+ session: reactive({
161
+ selection: null,
162
+ hover: null,
163
+ clipboard: null,
164
+ documentStack: [],
165
+ ui: createDefaultUi(),
166
+ canvas: { status: "idle", scope: null, error: null, pendingInlineEdit: null },
167
+ }),
168
+ history: reactive({
169
+ snapshots: [{ document: structuredClone(document), selection: null }],
170
+ index: 0,
171
+ }),
172
+ })) as unknown as Tab;
173
+
174
+ return tab;
175
+ }
176
+
177
+ /**
178
+ * @param {string | null | undefined} documentPath
179
+ * @param {string | null} sourceFormat
180
+ * @returns {string[]}
181
+ */
182
+ function inferModes(documentPath: string | null | undefined, sourceFormat: string | null) {
183
+ if (documentPath === "project.json") return ["stylebook", "source"];
184
+ if (sourceFormat === "md" || documentPath?.endsWith(".md"))
185
+ return ["edit", "design", "preview", "source"];
186
+ return ALL_MODES;
187
+ }
188
+
189
+ /**
190
+ * Dispose a tab — stops its effectScope, killing all effects created within it.
191
+ *
192
+ * @param {Tab} tab
193
+ */
194
+ export function disposeTab(tab: Tab) {
195
+ tab.scope.stop();
196
+ }