@jxsuite/studio 0.23.1 → 0.24.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 (105) hide show
  1. package/dist/studio.js +2231 -715
  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} +110 -85
  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} +7 -5
  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.js → edit-display.ts} +23 -20
  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} +38 -26
  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/{reactivity.js → reactivity.ts} +0 -0
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * Site context helpers - merge site-level definitions with file-level.
3
4
  *
@@ -5,8 +6,16 @@
5
6
  * definitions merge on top (file wins on conflict).
6
7
  */
7
8
 
8
- import { projectState, setProjectState, requireProjectState } from "./store.js";
9
- import { getPlatform } from "./platform.js";
9
+ import { projectState, setProjectState, requireProjectState } from "./store";
10
+ import { getPlatform } from "./platform";
11
+
12
+ import type {
13
+ JxElement,
14
+ JxMutableNode,
15
+ JxStyle,
16
+ JxHeadEntry,
17
+ ProjectConfig,
18
+ } from "@jxsuite/schema/types";
10
19
 
11
20
  /**
12
21
  * Merge site $media with document $media. Document keys win on conflict.
@@ -15,7 +24,7 @@ import { getPlatform } from "./platform.js";
15
24
  * undefined)
16
25
  * @returns {Record<string, string>}
17
26
  */
18
- export function getEffectiveMedia(docMedia) {
27
+ export function getEffectiveMedia(docMedia: Record<string, string> | undefined) {
19
28
  const siteMedia = projectState?.projectConfig?.$media;
20
29
  if (!siteMedia) return docMedia || {};
21
30
  if (!docMedia) return { ...siteMedia };
@@ -29,7 +38,7 @@ export function getEffectiveMedia(docMedia) {
29
38
  * @param {JxStyle | undefined} docStyle - The current document's style (may be undefined)
30
39
  * @returns {JxStyle}
31
40
  */
32
- export function getEffectiveStyle(docStyle) {
41
+ export function getEffectiveStyle(docStyle: JxStyle | undefined) {
33
42
  const siteStyle = projectState?.projectConfig?.style;
34
43
  if (!siteStyle) return docStyle || {};
35
44
  if (!docStyle) return { ...siteStyle };
@@ -41,7 +50,7 @@ export function getEffectiveStyle(docStyle) {
41
50
  typeof merged[k] === "object" &&
42
51
  merged[k] !== null
43
52
  ) {
44
- merged[k] = { .../** @type {JxStyle} */ (merged[k]), .../** @type {JxStyle} */ (v) };
53
+ merged[k] = { ...(merged[k] as JxStyle), ...(v as JxStyle) };
45
54
  } else {
46
55
  merged[k] = v;
47
56
  }
@@ -56,7 +65,7 @@ export function getEffectiveStyle(docStyle) {
56
65
  * undefined)
57
66
  * @returns {Record<string, string>}
58
67
  */
59
- export function getEffectiveImports(docImports) {
68
+ export function getEffectiveImports(docImports: Record<string, string> | undefined) {
60
69
  const siteImports = projectState?.projectConfig?.imports;
61
70
  if (!siteImports) return docImports || {};
62
71
  if (!docImports) return { ...siteImports };
@@ -70,14 +79,12 @@ export function getEffectiveImports(docImports) {
70
79
  * undefined)
71
80
  * @returns {(JxElement | string)[]}
72
81
  */
73
- export function getEffectiveElements(docElements) {
82
+ export function getEffectiveElements(docElements?: (JxElement | string)[]) {
74
83
  const siteElements = projectState?.projectConfig?.$elements;
75
84
  if (!siteElements?.length) return docElements || [];
76
85
  if (!docElements?.length) return [...siteElements];
77
- /** @type {Set<string>} */
78
- const seen = new Set();
79
- /** @type {(JxElement | string)[]} */
80
- const merged = [];
86
+ const seen: Set<string> = new Set();
87
+ const merged: (JxElement | string)[] = [];
81
88
  for (const entry of [...siteElements, ...docElements]) {
82
89
  const key = typeof entry === "string" ? entry : entry?.$ref;
83
90
  if (key && !seen.has(key)) {
@@ -94,14 +101,12 @@ export function getEffectiveElements(docElements) {
94
101
  * @param {JxHeadEntry[]} [docHead] - The current document's $head (may be undefined)
95
102
  * @returns {JxHeadEntry[]}
96
103
  */
97
- export function getEffectiveHead(docHead) {
104
+ export function getEffectiveHead(docHead?: JxHeadEntry[]) {
98
105
  const siteHead = projectState?.projectConfig?.$head;
99
106
  if (!siteHead?.length) return docHead || [];
100
107
  if (!docHead?.length) return [...siteHead];
101
- /** @type {Set<string>} */
102
- const seen = new Set();
103
- /** @type {JxHeadEntry[]} */
104
- const merged = [];
108
+ const seen: Set<string> = new Set();
109
+ const merged: JxHeadEntry[] = [];
105
110
  for (const entry of [...siteHead, ...docHead]) {
106
111
  const key =
107
112
  String(entry?.attributes?.href || entry?.attributes?.src || "") || JSON.stringify(entry);
@@ -115,8 +120,7 @@ export function getEffectiveHead(docHead) {
115
120
 
116
121
  // ─── Layout resolution ──────────────────────────────────────────────────────
117
122
 
118
- /** @type {Map<string, JxMutableNode>} */
119
- const layoutCache = new Map();
123
+ const layoutCache: Map<string, JxMutableNode> = new Map();
120
124
 
121
125
  export function invalidateLayoutCache() {
122
126
  layoutCache.clear();
@@ -129,7 +133,7 @@ export function invalidateLayoutCache() {
129
133
  * or undefined)
130
134
  * @returns {string | null} The layout path, or null if no layout applies
131
135
  */
132
- export function getEffectiveLayoutPath(docLayout) {
136
+ export function getEffectiveLayoutPath(docLayout: string | false | undefined) {
133
137
  if (docLayout === false) return null;
134
138
  const defaultLayout = projectState?.projectConfig?.defaults?.layout;
135
139
  return docLayout || defaultLayout || null;
@@ -141,10 +145,10 @@ export function getEffectiveLayoutPath(docLayout) {
141
145
  * @param {string} layoutPath - Relative path to the layout file (e.g., "./layouts/base.json")
142
146
  * @returns {Promise<JxMutableNode | null>} The parsed layout document, or null on failure
143
147
  */
144
- export async function resolveLayoutDoc(layoutPath) {
148
+ export async function resolveLayoutDoc(layoutPath: string) {
145
149
  const normalized = layoutPath.replace(/^\.\//, "");
146
150
  if (layoutCache.has(normalized))
147
- return structuredClone(/** @type {JxMutableNode} */ (layoutCache.get(normalized)));
151
+ return structuredClone(layoutCache.get(normalized) as JxMutableNode);
148
152
 
149
153
  try {
150
154
  const platform = getPlatform();
@@ -165,7 +169,7 @@ export async function resolveLayoutDoc(layoutPath) {
165
169
  * @param {JxMutableNode} pageDoc - The page document
166
170
  * @returns {JxMutableNode} The merged document
167
171
  */
168
- export function distributePageIntoLayout(layoutDoc, pageDoc) {
172
+ export function distributePageIntoLayout(layoutDoc: JxMutableNode, pageDoc: JxMutableNode) {
169
173
  const pageChildren = pageDoc.children ?? [];
170
174
  const children = typeof pageChildren === "string" ? [pageChildren] : pageChildren;
171
175
 
@@ -193,20 +197,19 @@ export function distributePageIntoLayout(layoutDoc, pageDoc) {
193
197
  }
194
198
 
195
199
  function fillSlots(
196
- /** @type {JxMutableNode} */ node,
197
- /** @type {Map<string, JxMutableNode[]>} */ named,
198
- /** @type {(JxMutableNode | string)[]} */ defaults,
200
+ node: JxMutableNode,
201
+ named: Map<string, JxMutableNode[]>,
202
+ defaults: (JxMutableNode | string)[],
199
203
  ) {
200
204
  if (!node || typeof node !== "object") return;
201
205
  if (!Array.isArray(node.children)) return;
202
206
 
203
- /** @type {(JxMutableNode | string)[]} */
204
- const newChildren = [];
207
+ const newChildren: (JxMutableNode | string)[] = [];
205
208
  for (const child of node.children) {
206
209
  if (child && typeof child === "object" && child.tagName === "slot") {
207
210
  const slotName = child.attributes?.name;
208
211
  if (slotName && named.has(slotName)) {
209
- newChildren.push(.../** @type {JxMutableNode[]} */ (named.get(slotName)));
212
+ newChildren.push(...(named.get(slotName) as JxMutableNode[]));
210
213
  named.delete(slotName);
211
214
  } else if (!slotName && defaults.length > 0) {
212
215
  newChildren.push(...defaults);
@@ -218,7 +221,7 @@ function fillSlots(
218
221
  newChildren.push(child);
219
222
  }
220
223
  }
221
- node.children = /** @type {JxMutableNode[]} */ (newChildren);
224
+ node.children = newChildren as JxMutableNode[];
222
225
  }
223
226
 
224
227
  /**
@@ -226,12 +229,12 @@ function fillSlots(
226
229
  *
227
230
  * @param {Partial<ProjectConfig>} patch - Fields to merge into the current projectConfig
228
231
  */
229
- export async function updateSiteConfig(patch) {
232
+ export async function updateSiteConfig(patch: Partial<ProjectConfig>) {
230
233
  const platform = getPlatform();
231
- const config = /** @type {ProjectConfig} */ ({
234
+ const config = {
232
235
  ...requireProjectState().projectConfig,
233
236
  ...patch,
234
- });
237
+ } as ProjectConfig;
235
238
  await platform.writeFile("project.json", JSON.stringify(config, null, 2));
236
239
  setProjectState({ ...requireProjectState(), projectConfig: config });
237
240
  }
@@ -1,3 +1,4 @@
1
+ /// <reference lib="dom" />
1
2
  /**
2
3
  * State.js — Builder state model and mutation API
3
4
  *
@@ -8,39 +9,43 @@
8
9
  * = third child of first child
9
10
  */
10
11
 
11
- /**
12
- * @typedef {(string | number)[]} JxPath
13
- *
14
- * @typedef {{ document: JxMutableNode; selection: JxPath | null }} HistorySnapshot
15
- *
16
- * @typedef {{
17
- * document: JxMutableNode;
18
- * selection: JxPath | null;
19
- * hover: JxPath | null;
20
- * history: HistorySnapshot[];
21
- * historyIndex: number;
22
- * dirty: boolean;
23
- * fileHandle: FileSystemFileHandle | null;
24
- * documentPath: string | null;
25
- * documentStack: StudioStackFrame[];
26
- * handlersSource: string | null;
27
- * mode: string;
28
- * content: { frontmatter: Record<string, unknown> };
29
- * ui: Record<string, unknown>;
30
- * canvas: { status: string; scope: Record<string, unknown> | null; error: string | null };
31
- * }} StudioState
32
- *
33
- * @typedef {{
34
- * document: JxMutableNode;
35
- * selection: JxPath | null;
36
- * fileHandle: FileSystemFileHandle | null;
37
- * documentPath: string | null;
38
- * dirty: boolean;
39
- * history: HistorySnapshot[];
40
- * historyIndex: number;
41
- * mode: string;
42
- * }} StudioStackFrame
43
- */
12
+ import type { ProjectState } from "./types";
13
+ import type { JxMutableNode } from "@jxsuite/schema/types";
14
+
15
+ export type JxPath = (string | number)[];
16
+
17
+ interface HistorySnapshot {
18
+ document: JxMutableNode;
19
+ selection: JxPath | null;
20
+ }
21
+
22
+ export interface StudioState {
23
+ document: JxMutableNode;
24
+ selection: JxPath | null;
25
+ hover: JxPath | null;
26
+ history: HistorySnapshot[];
27
+ historyIndex: number;
28
+ dirty: boolean;
29
+ fileHandle: FileSystemFileHandle | null;
30
+ documentPath: string | null;
31
+ documentStack: StudioStackFrame[];
32
+ handlersSource: string | null;
33
+ mode: string;
34
+ content: { frontmatter: Record<string, unknown> };
35
+ ui: Record<string, unknown>;
36
+ canvas: { status: string; scope: Record<string, unknown> | null; error: string | null };
37
+ }
38
+
39
+ interface StudioStackFrame {
40
+ document: JxMutableNode;
41
+ selection: JxPath | null;
42
+ fileHandle: FileSystemFileHandle | null;
43
+ documentPath: string | null;
44
+ dirty: boolean;
45
+ history: HistorySnapshot[];
46
+ historyIndex: number;
47
+ mode: string;
48
+ }
44
49
 
45
50
  // ─── Path utilities ───────────────────────────────────────────────────────────
46
51
 
@@ -51,10 +56,10 @@
51
56
  * @param {JxPath} path
52
57
  * @returns {JxMutableNode}
53
58
  */
54
- export function getNodeAtPath(doc, path) {
59
+ export function getNodeAtPath(doc: JxMutableNode, path: JxPath) {
55
60
  let node = doc;
56
61
  for (const key of path) {
57
- if (node == null) return /** @type {JxMutableNode} */ (/** @type {unknown} */ (undefined));
62
+ if (node == null) return undefined as unknown as JxMutableNode;
58
63
  node = node[key];
59
64
  }
60
65
  return node;
@@ -66,7 +71,7 @@ export function getNodeAtPath(doc, path) {
66
71
  * @param {JxPath} path
67
72
  * @returns {JxPath | null}
68
73
  */
69
- export function parentElementPath(path) {
74
+ export function parentElementPath(path: JxPath) {
70
75
  return path.length >= 2 ? path.slice(0, -2) : null;
71
76
  }
72
77
 
@@ -76,7 +81,7 @@ export function parentElementPath(path) {
76
81
  * @param {JxPath} path
77
82
  * @returns {string | number}
78
83
  */
79
- export function childIndex(path) {
84
+ export function childIndex(path: JxPath) {
80
85
  return path[path.length - 1];
81
86
  }
82
87
 
@@ -86,7 +91,7 @@ export function childIndex(path) {
86
91
  * @param {JxPath} path
87
92
  * @returns {string}
88
93
  */
89
- export function pathKey(path) {
94
+ export function pathKey(path: JxPath) {
90
95
  return path.join("/");
91
96
  }
92
97
 
@@ -97,7 +102,7 @@ export function pathKey(path) {
97
102
  * @param {JxPath | null} b
98
103
  * @returns {boolean}
99
104
  */
100
- export function pathsEqual(a, b) {
105
+ export function pathsEqual(a: JxPath | null, b: JxPath | null) {
101
106
  if (a === b) return true;
102
107
  if (!a || !b || a.length !== b.length) return false;
103
108
  return a.every((v, i) => v === b[i]);
@@ -110,7 +115,7 @@ export function pathsEqual(a, b) {
110
115
  * @param {JxPath} descendant
111
116
  * @returns {boolean}
112
117
  */
113
- export function isAncestor(path, descendant) {
118
+ export function isAncestor(path: JxPath, descendant: JxPath) {
114
119
  if (path.length > descendant.length) return false;
115
120
  return path.every((v, i) => v === descendant[i]);
116
121
  }
@@ -133,21 +138,24 @@ export function isAncestor(path, descendant) {
133
138
  * nodeType: string;
134
139
  * }[]}
135
140
  */
136
- export function flattenTree(doc, path = [], depth = 0) {
141
+ export type FlatRow = {
142
+ node: JxMutableNode | string | number | boolean;
143
+ path: JxPath;
144
+ depth: number;
145
+ nodeType: string;
146
+ };
147
+
148
+ export function flattenTree(
149
+ doc: JxMutableNode | string | number | boolean,
150
+ path: JxPath = [],
151
+ depth: number = 0,
152
+ ): FlatRow[] {
137
153
  // Text node children: bare primitives get a "text" row
138
154
  if (typeof doc === "string" || typeof doc === "number" || typeof doc === "boolean") {
139
155
  return [{ node: doc, path, depth, nodeType: "text" }];
140
156
  }
141
157
 
142
- /**
143
- * @type {{
144
- * node: JxMutableNode | string | number | boolean;
145
- * path: JxPath;
146
- * depth: number;
147
- * nodeType: string;
148
- * }[]}
149
- */
150
- const rows = [{ node: doc, path, depth, nodeType: "element" }];
158
+ const rows: FlatRow[] = [{ node: doc, path, depth, nodeType: "element" }];
151
159
 
152
160
  // Custom component instances without user-authored children are atomic in the layer tree
153
161
  if (doc.$props && (doc.tagName || "").includes("-") && !Array.isArray(doc.children)) {
@@ -164,19 +172,18 @@ export function flattenTree(doc, path = [], depth = 0) {
164
172
  } else if (
165
173
  children &&
166
174
  typeof children === "object" &&
167
- /** @type {JxMutableNode} */ (children).$prototype === "Array"
175
+ (children as JxMutableNode).$prototype === "Array"
168
176
  ) {
169
177
  // $map — emit the map container, then recurse into the template
170
- rows.push({ node: children, path: [...path, "children"], depth: depth + 1, nodeType: "map" });
171
- const mapDef = /** @type {JxMutableNode} */ (children).map;
178
+ rows.push({
179
+ node: children as JxMutableNode,
180
+ path: [...path, "children"],
181
+ depth: depth + 1,
182
+ nodeType: "map",
183
+ });
184
+ const mapDef = (children as JxMutableNode).map;
172
185
  if (mapDef && typeof mapDef === "object") {
173
- rows.push(
174
- ...flattenTree(
175
- /** @type {JxMutableNode} */ (mapDef),
176
- [...path, "children", "map"],
177
- depth + 2,
178
- ),
179
- );
186
+ rows.push(...flattenTree(mapDef as JxMutableNode, [...path, "children", "map"], depth + 2));
180
187
  }
181
188
  }
182
189
 
@@ -184,12 +191,22 @@ export function flattenTree(doc, path = [], depth = 0) {
184
191
  if (doc.$switch && doc.cases && typeof doc.cases === "object") {
185
192
  for (const [caseName, caseDef] of Object.entries(doc.cases)) {
186
193
  const casePath = [...path, "cases", caseName];
187
- if (caseDef && typeof caseDef === "object" && /** @type {JxMutableNode} */ (caseDef).$ref) {
188
- rows.push({ node: caseDef, path: casePath, depth: depth + 1, nodeType: "case-ref" });
194
+ if (caseDef && typeof caseDef === "object" && (caseDef as JxMutableNode).$ref) {
195
+ rows.push({
196
+ node: caseDef as JxMutableNode,
197
+ path: casePath,
198
+ depth: depth + 1,
199
+ nodeType: "case-ref",
200
+ });
189
201
  } else if (caseDef && typeof caseDef === "object") {
190
- rows.push({ node: caseDef, path: casePath, depth: depth + 1, nodeType: "case" });
202
+ rows.push({
203
+ node: caseDef as JxMutableNode,
204
+ path: casePath,
205
+ depth: depth + 1,
206
+ nodeType: "case",
207
+ });
191
208
  // Recurse into case children (skip the case node itself — already emitted)
192
- const caseChildren = flattenTree(caseDef, casePath, depth + 2);
209
+ const caseChildren = flattenTree(caseDef as JxMutableNode, casePath, depth + 2);
193
210
  rows.push(...caseChildren.slice(1));
194
211
  }
195
212
  }
@@ -204,7 +221,7 @@ export function flattenTree(doc, path = [], depth = 0) {
204
221
  * @param {JxMutableNode | null} node
205
222
  * @returns {string}
206
223
  */
207
- export function nodeLabel(node) {
224
+ export function nodeLabel(node: JxMutableNode | null) {
208
225
  if (!node) return "?";
209
226
  // $map container (Repeater)
210
227
  if (node.$prototype === "Array") {
@@ -227,7 +244,7 @@ export function nodeLabel(node) {
227
244
  * @param {JxMutableNode} doc
228
245
  * @returns {StudioState}
229
246
  */
230
- export function createState(doc) {
247
+ export function createState(doc: JxMutableNode): StudioState {
231
248
  const initial = { document: doc, selection: null };
232
249
  return {
233
250
  document: doc,
@@ -264,6 +281,7 @@ export function createState(doc) {
264
281
  gitCommitMessage: "", // commit message input
265
282
  gitLoading: false, // loading indicator during async ops
266
283
  gitError: null, // error message string
284
+ gitDiffState: null,
267
285
  pendingInlineEdit: null, // null | { path, mediaName } — deferred inline edit awaiting canvas readiness
268
286
  },
269
287
  canvas: {
@@ -283,8 +301,8 @@ export function createState(doc) {
283
301
  * @param {Partial<StudioState>} session
284
302
  * @returns {StudioState}
285
303
  */
286
- export function toFlat(doc, session) {
287
- return /** @type {StudioState} */ ({ ...doc, ...session });
304
+ export function toFlat(doc: Partial<StudioState>, session: Partial<StudioState>) {
305
+ return { ...doc, ...session } as StudioState;
288
306
  }
289
307
 
290
308
  /**
@@ -293,7 +311,7 @@ export function toFlat(doc, session) {
293
311
  * @param {StudioState} S
294
312
  * @returns {{ doc: Partial<StudioState>; session: Partial<StudioState> }}
295
313
  */
296
- export function fromFlat(S) {
314
+ export function fromFlat(S: StudioState) {
297
315
  const {
298
316
  document,
299
317
  dirty,
@@ -334,11 +352,10 @@ export function fromFlat(S) {
334
352
  // selectedPath: string|null, searchQuery: string }
335
353
  // DirEntry: { name, path, type: "file"|"directory", size, modified }
336
354
 
337
- /** @type {ProjectState | null} */
338
- export let projectState = null;
355
+ export let projectState: ProjectState | null = null;
339
356
 
340
357
  /** @param {ProjectState | null} ps */
341
- export function setProjectState(ps) {
358
+ export function setProjectState(ps: ProjectState | null) {
342
359
  projectState = ps;
343
360
  }
344
361
 
@@ -349,7 +366,7 @@ export function setProjectState(ps) {
349
366
  * @returns {ProjectState}
350
367
  */
351
368
  export function requireProjectState() {
352
- return /** @type {ProjectState} */ (projectState);
369
+ return projectState as ProjectState;
353
370
  }
354
371
 
355
372
  // ─── Frontmatter mutation ───────────────────────────────────────────────────
@@ -363,7 +380,7 @@ export function requireProjectState() {
363
380
  * @param {unknown} value
364
381
  * @returns {StudioState}
365
382
  */
366
- export function updateFrontmatter(state, field, value) {
383
+ export function updateFrontmatter(state: StudioState, field: string, value: unknown) {
367
384
  const fm = { ...state.content?.frontmatter };
368
385
  if (value === undefined || value === null || value === "") delete fm[field];
369
386
  else fm[field] = value;
@@ -381,7 +398,7 @@ export function updateFrontmatter(state, field, value) {
381
398
  * @param {JxPath | null} path
382
399
  * @returns {StudioState}
383
400
  */
384
- export function selectNode(state, path) {
401
+ export function selectNode(state: StudioState, path: JxPath | null) {
385
402
  return { ...state, selection: path };
386
403
  }
387
404
 
@@ -390,7 +407,7 @@ export function selectNode(state, path) {
390
407
  * @param {JxPath | null} path
391
408
  * @returns {StudioState}
392
409
  */
393
- export function hoverNode(state, path) {
410
+ export function hoverNode(state: StudioState, path: JxPath | null) {
394
411
  return { ...state, hover: path };
395
412
  }
396
413
 
@@ -404,7 +421,7 @@ export function hoverNode(state, path) {
404
421
  * @param {string | null} documentPath
405
422
  * @returns {StudioState}
406
423
  */
407
- export function pushDocument(state, doc, documentPath) {
424
+ export function pushDocument(state: StudioState, doc: JxMutableNode, documentPath: string | null) {
408
425
  const frame = {
409
426
  document: state.document,
410
427
  selection: state.selection,
@@ -428,7 +445,7 @@ export function pushDocument(state, doc, documentPath) {
428
445
  * @param {StudioState} state
429
446
  * @returns {StudioState}
430
447
  */
431
- export function popDocument(state) {
448
+ export function popDocument(state: StudioState) {
432
449
  if (!state.documentStack || state.documentStack.length === 0) return state;
433
450
  const stack = [...state.documentStack];
434
451
  const frame = stack.pop();