@jxsuite/studio 0.19.0 → 0.21.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 (99) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +13476 -12330
  3. package/dist/studio.js.map +152 -148
  4. package/dist/workers/editor.worker.js +79 -79
  5. package/dist/workers/json.worker.js +109 -109
  6. package/dist/workers/ts.worker.js +82 -82
  7. package/package.json +19 -7
  8. package/src/browse/browse-modal.js +16 -13
  9. package/src/browse/browse.js +44 -35
  10. package/src/canvas/canvas-diff.js +24 -16
  11. package/src/canvas/canvas-helpers.js +24 -19
  12. package/src/canvas/canvas-live-render.js +113 -39
  13. package/src/canvas/canvas-render.js +119 -73
  14. package/src/canvas/canvas-utils.js +51 -21
  15. package/src/canvas/nested-site-style.js +50 -0
  16. package/src/editor/component-inline-edit.js +14 -13
  17. package/src/editor/content-inline-edit.js +22 -10
  18. package/src/editor/context-menu.js +28 -14
  19. package/src/editor/convert-targets.js +60 -0
  20. package/src/editor/convert-to-component.js +20 -19
  21. package/src/editor/convert-to-repeater.js +226 -0
  22. package/src/editor/inline-edit.js +69 -28
  23. package/src/editor/insertion-helper.js +16 -12
  24. package/src/editor/shortcuts.js +14 -4
  25. package/src/editor/slash-menu.js +40 -19
  26. package/src/files/components.js +1 -1
  27. package/src/files/file-ops.js +37 -16
  28. package/src/files/files.js +322 -68
  29. package/src/github/github-auth.js +122 -0
  30. package/src/github/github-publish.js +126 -0
  31. package/src/markdown/md-convert.js +172 -107
  32. package/src/new-project/new-project-modal.js +204 -0
  33. package/src/panels/activity-bar.js +27 -25
  34. package/src/panels/ai-panel.js +406 -0
  35. package/src/panels/block-action-bar.js +77 -28
  36. package/src/panels/canvas-dnd.js +8 -2
  37. package/src/panels/data-explorer.js +19 -13
  38. package/src/panels/dnd.js +144 -92
  39. package/src/panels/editors.js +55 -35
  40. package/src/panels/elements-panel.js +33 -19
  41. package/src/panels/events-panel.js +15 -10
  42. package/src/panels/git-panel.js +475 -99
  43. package/src/panels/head-panel.js +121 -81
  44. package/src/panels/imports-panel.js +94 -65
  45. package/src/panels/layers-panel.js +64 -43
  46. package/src/panels/left-panel.js +105 -48
  47. package/src/panels/overlays.js +1 -0
  48. package/src/panels/panel-events.js +45 -30
  49. package/src/panels/preview-render.js +23 -18
  50. package/src/panels/properties-panel.js +378 -257
  51. package/src/panels/pseudo-preview.js +2 -2
  52. package/src/panels/quick-search.js +11 -9
  53. package/src/panels/right-panel.js +42 -16
  54. package/src/panels/shared.js +4 -4
  55. package/src/panels/signals-panel.js +415 -166
  56. package/src/panels/statusbar.js +10 -6
  57. package/src/panels/style-inputs.js +67 -35
  58. package/src/panels/style-panel.js +239 -134
  59. package/src/panels/style-utils.js +34 -16
  60. package/src/panels/stylebook-layers-panel.js +12 -10
  61. package/src/panels/stylebook-panel.js +134 -66
  62. package/src/panels/tab-strip.js +5 -2
  63. package/src/panels/toolbar.js +333 -109
  64. package/src/panels/welcome-screen.js +121 -0
  65. package/src/platform.js +2 -4
  66. package/src/platforms/devserver.js +113 -7
  67. package/src/resize-edges.js +98 -0
  68. package/src/services/cem-export.js +12 -7
  69. package/src/services/code-services.js +30 -12
  70. package/src/settings/content-types-editor.js +8 -8
  71. package/src/settings/css-vars-editor.js +30 -24
  72. package/src/settings/defs-editor.js +12 -7
  73. package/src/settings/general-settings.js +85 -3
  74. package/src/settings/head-editor.js +30 -24
  75. package/src/settings/schema-field-ui.js +58 -39
  76. package/src/settings/settings-modal.js +12 -9
  77. package/src/site-context.js +41 -31
  78. package/src/state.js +70 -29
  79. package/src/store.js +36 -35
  80. package/src/studio.js +120 -90
  81. package/src/tabs/tab.js +67 -43
  82. package/src/tabs/transact.js +23 -13
  83. package/src/ui/button-group.js +24 -24
  84. package/src/ui/color-selector.js +28 -23
  85. package/src/ui/field-row.js +3 -3
  86. package/src/ui/icons.js +2 -2
  87. package/src/ui/layers.js +31 -1
  88. package/src/ui/media-picker.js +7 -7
  89. package/src/ui/spectrum.js +5 -1
  90. package/src/ui/unit-selector.js +18 -16
  91. package/src/ui/value-selector.js +21 -15
  92. package/src/ui/widgets.js +22 -19
  93. package/src/utils/canvas-media.js +5 -4
  94. package/src/utils/edit-display.js +32 -26
  95. package/src/utils/google-fonts.js +11 -11
  96. package/src/utils/inherited-style.js +9 -8
  97. package/src/utils/studio-utils.js +18 -18
  98. package/src/view.js +58 -1
  99. package/src/workspace/workspace.js +29 -7
@@ -13,6 +13,7 @@
13
13
  import { unified } from "unified";
14
14
  import remarkStringify from "remark-stringify";
15
15
  import remarkDirective from "remark-directive";
16
+ import remarkGfm from "remark-gfm";
16
17
  import { MD_ALL } from "./md-allowlist.js";
17
18
  import { htmlToJx } from "@jxsuite/parser/transpile";
18
19
 
@@ -21,10 +22,10 @@ import { htmlToJx } from "@jxsuite/parser/transpile";
21
22
  /**
22
23
  * Mdast node-type → Jx tagName mapping
23
24
  *
24
- * @type {Record<string, (n: any) => string>}
25
+ * @type {Record<string, (n: MdastNode) => string>}
25
26
  */
26
27
  const MDAST_TAG_MAP = {
27
- heading: (/** @type {any} */ n) => `h${n.depth}`,
28
+ heading: (/** @type {MdastNode} */ n) => `h${n.depth}`,
28
29
  paragraph: () => "p",
29
30
  text: () => "span",
30
31
  emphasis: () => "em",
@@ -34,37 +35,39 @@ const MDAST_TAG_MAP = {
34
35
  link: () => "a",
35
36
  image: () => "img",
36
37
  blockquote: () => "blockquote",
37
- list: (/** @type {any} */ n) => (n.ordered ? "ol" : "ul"),
38
+ list: (/** @type {MdastNode} */ n) => (n.ordered ? "ol" : "ul"),
38
39
  listItem: () => "li",
39
40
  code: () => "pre",
40
41
  thematicBreak: () => "hr",
41
42
  table: () => "table",
42
43
  tableRow: () => "tr",
43
- tableCell: (/** @type {any} */ n) => (n.isHeader ? "th" : "td"),
44
+ tableCell: (/** @type {MdastNode} */ n) => (n.isHeader ? "th" : "td"),
44
45
  break: () => "br",
45
46
  };
46
47
 
47
48
  /**
48
49
  * Convert an mdast tree to a Jx element tree.
49
50
  *
50
- * @param {any} mdast - Root mdast node (type: 'root')
51
- * @returns {any} Jx element tree
51
+ * @param {MdastNode} mdast - Root mdast node (type: 'root')
52
+ * @returns {JxElement} Jx element tree
52
53
  */
53
54
  export function mdToJx(mdast) {
54
55
  if (mdast.type === "root") {
55
56
  return {
56
- children: (mdast.children ?? [])
57
- .filter((/** @type {any} */ n) => n.type !== "yaml" && n.type !== "toml")
58
- .flatMap(convertMdastNode)
59
- .filter(Boolean),
57
+ children: /** @type {(JxElement | string)[]} */ (
58
+ (mdast.children ?? [])
59
+ .filter((/** @type {MdastNode} */ n) => n.type !== "yaml" && n.type !== "toml")
60
+ .flatMap(convertMdastNode)
61
+ .filter(Boolean)
62
+ ),
60
63
  };
61
64
  }
62
- return convertMdastNode(mdast);
65
+ return /** @type {JxElement} */ (convertMdastNode(mdast));
63
66
  }
64
67
 
65
68
  /**
66
- * @param {any} node
67
- * @returns {any}
69
+ * @param {MdastNode} node
70
+ * @returns {JxElement | null}
68
71
  */
69
72
  function convertMdastNode(node) {
70
73
  if (!node) return null;
@@ -81,14 +84,16 @@ function convertMdastNode(node) {
81
84
  if (node.type === "html") {
82
85
  if (!node.value) return null;
83
86
  const nodes = htmlToJx(node.value);
84
- return nodes.length === 1 ? nodes[0] : { tagName: "div", children: nodes };
87
+ return nodes.length === 1
88
+ ? /** @type {JxElement} */ (nodes[0])
89
+ : { tagName: "div", children: nodes };
85
90
  }
86
91
 
87
92
  const tagFn = MDAST_TAG_MAP[node.type];
88
93
  if (!tagFn) return null;
89
94
 
90
95
  const tag = tagFn(node);
91
- /** @type {Record<string, any>} */
96
+ /** @type {JxElement} */
92
97
  const el = { tagName: tag };
93
98
 
94
99
  switch (node.type) {
@@ -97,8 +102,10 @@ function convertMdastNode(node) {
97
102
  // If contains only a single text child, flatten to textContent
98
103
  if (node.children?.length === 1 && node.children[0].type === "text") {
99
104
  el.textContent = node.children[0].value;
100
- } else if (node.children?.length > 0) {
101
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
105
+ } else if (node.children?.length) {
106
+ el.children = /** @type {(JxElement | string)[]} */ (
107
+ node.children.flatMap(convertMdastNode).filter(Boolean)
108
+ );
102
109
  }
103
110
  break;
104
111
  }
@@ -112,8 +119,10 @@ function convertMdastNode(node) {
112
119
  case "delete": {
113
120
  if (node.children?.length === 1 && node.children[0].type === "text") {
114
121
  el.textContent = node.children[0].value;
115
- } else if (node.children?.length > 0) {
116
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
122
+ } else if (node.children?.length) {
123
+ el.children = /** @type {(JxElement | string)[]} */ (
124
+ node.children.flatMap(convertMdastNode).filter(Boolean)
125
+ );
117
126
  }
118
127
  break;
119
128
  }
@@ -123,30 +132,36 @@ function convertMdastNode(node) {
123
132
  break;
124
133
 
125
134
  case "link":
126
- el.attributes = { href: node.url };
135
+ el.attributes = { href: node.url ?? "" };
127
136
  if (node.title) el.attributes.title = node.title;
128
137
  if (node.children?.length === 1 && node.children[0].type === "text") {
129
138
  el.textContent = node.children[0].value;
130
- } else if (node.children?.length > 0) {
131
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
139
+ } else if (node.children?.length) {
140
+ el.children = /** @type {(JxElement | string)[]} */ (
141
+ node.children.flatMap(convertMdastNode).filter(Boolean)
142
+ );
132
143
  }
133
144
  break;
134
145
 
135
146
  case "image":
136
- el.attributes = { src: node.url, alt: node.alt ?? "" };
147
+ el.attributes = { src: node.url ?? "", alt: node.alt ?? "" };
137
148
  if (node.title) el.attributes.title = node.title;
138
149
  break;
139
150
 
140
151
  case "blockquote":
141
152
  case "listItem":
142
- if (node.children?.length > 0) {
143
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
153
+ if (node.children?.length) {
154
+ el.children = /** @type {(JxElement | string)[]} */ (
155
+ node.children.flatMap(convertMdastNode).filter(Boolean)
156
+ );
144
157
  }
145
158
  break;
146
159
 
147
160
  case "list":
148
- if (node.children?.length > 0) {
149
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
161
+ if (node.children?.length) {
162
+ el.children = /** @type {(JxElement | string)[]} */ (
163
+ node.children.flatMap(convertMdastNode).filter(Boolean)
164
+ );
150
165
  }
151
166
  if (node.start != null && node.start !== 1) {
152
167
  el.attributes = { start: String(node.start) };
@@ -171,24 +186,36 @@ function convertMdastNode(node) {
171
186
 
172
187
  case "table": {
173
188
  // Mdast tables have rows directly; split into thead/tbody
174
- const rows = (node.children ?? []).flatMap(convertMdastNode).filter(Boolean);
175
- const thead = rows.length > 0 ? { tagName: "thead", children: [rows[0]] } : null;
176
- const tbody = rows.length > 1 ? { tagName: "tbody", children: rows.slice(1) } : null;
177
- el.children = [thead, tbody].filter(Boolean);
189
+ const rows = /** @type {JxElement[]} */ (
190
+ (node.children ?? []).flatMap(convertMdastNode).filter(Boolean)
191
+ );
192
+ const thead =
193
+ rows.length > 0
194
+ ? { tagName: "thead", children: /** @type {(JxElement | string)[]} */ ([rows[0]]) }
195
+ : null;
196
+ const tbody =
197
+ rows.length > 1
198
+ ? { tagName: "tbody", children: /** @type {(JxElement | string)[]} */ (rows.slice(1)) }
199
+ : null;
200
+ el.children = /** @type {(JxElement | string)[]} */ ([thead, tbody].filter(Boolean));
178
201
  break;
179
202
  }
180
203
 
181
204
  case "tableRow":
182
- if (node.children?.length > 0) {
183
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
205
+ if (node.children?.length) {
206
+ el.children = /** @type {(JxElement | string)[]} */ (
207
+ node.children.flatMap(convertMdastNode).filter(Boolean)
208
+ );
184
209
  }
185
210
  break;
186
211
 
187
212
  case "tableCell":
188
213
  if (node.children?.length === 1 && node.children[0].type === "text") {
189
214
  el.textContent = node.children[0].value;
190
- } else if (node.children?.length > 0) {
191
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
215
+ } else if (node.children?.length) {
216
+ el.children = /** @type {(JxElement | string)[]} */ (
217
+ node.children.flatMap(convertMdastNode).filter(Boolean)
218
+ );
192
219
  }
193
220
  break;
194
221
  }
@@ -197,11 +224,11 @@ function convertMdastNode(node) {
197
224
  }
198
225
 
199
226
  /**
200
- * @param {any} node
201
- * @returns {any}
227
+ * @param {MdastNode} node
228
+ * @returns {JxElement | null}
202
229
  */
203
230
  function convertDirective(node) {
204
- /** @type {Record<string, any>} */
231
+ /** @type {JxElement} */
205
232
  const el = { tagName: node.name };
206
233
  if (node.attributes && Object.keys(node.attributes).length > 0) {
207
234
  el.attributes = { ...node.attributes };
@@ -210,11 +237,15 @@ function convertDirective(node) {
210
237
  // Text directives place label as textContent
211
238
  if (node.children?.length === 1 && node.children[0].type === "text") {
212
239
  el.textContent = node.children[0].value;
213
- } else if (node.children?.length > 0) {
214
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
240
+ } else if (node.children?.length) {
241
+ el.children = /** @type {(JxElement | string)[]} */ (
242
+ node.children.flatMap(convertMdastNode).filter(Boolean)
243
+ );
215
244
  }
216
- } else if (node.type === "containerDirective" && node.children?.length > 0) {
217
- el.children = node.children.flatMap(convertMdastNode).filter(Boolean);
245
+ } else if (node.type === "containerDirective" && node.children?.length) {
246
+ el.children = /** @type {(JxElement | string)[]} */ (
247
+ node.children.flatMap(convertMdastNode).filter(Boolean)
248
+ );
218
249
  }
219
250
  return el;
220
251
  }
@@ -274,13 +305,18 @@ const TAG_MDAST_MAP = {
274
305
  /**
275
306
  * Convert a Jx element tree to an mdast tree.
276
307
  *
277
- * @param {any} jx - Jx element tree (root content div)
278
- * @returns {any} Mdast root node
308
+ * @param {JxElement} jx - Jx element tree (root content div)
309
+ * @returns {MdastNode} Mdast root node
279
310
  */
280
311
  export function jxToMd(jx) {
281
- const children = (jx.children ?? [])
282
- .map((/** @type {any} */ child, /** @type {number} */ _i) => convertJxNode(child, true))
283
- .filter(Boolean);
312
+ const childArray = /** @type {(JxElement | string)[]} */ (
313
+ Array.isArray(jx.children) ? jx.children : []
314
+ );
315
+ const children = /** @type {MdastNode[]} */ (
316
+ childArray
317
+ .map((/** @type {JxElement | string} */ child) => convertJxNode(child, true))
318
+ .filter(Boolean)
319
+ );
284
320
 
285
321
  return { type: "root", children };
286
322
  }
@@ -289,7 +325,7 @@ export function jxToMd(jx) {
289
325
  * Check if a Jx element has extra properties beyond the standard mdast-compatible ones. Elements
290
326
  * with style, event handlers, state bindings, etc. need directive syntax.
291
327
  *
292
- * @param {any} el
328
+ * @param {JxElement} el
293
329
  * @returns {boolean}
294
330
  */
295
331
  function hasJxProps(el) {
@@ -310,9 +346,9 @@ function hasJxProps(el) {
310
346
  /**
311
347
  * Convert a single Jx element to an mdast node.
312
348
  *
313
- * @param {any} el - Jx element
349
+ * @param {JxElement | string | number} el - Jx element
314
350
  * @param {boolean} isBlock - Whether this element is in a block context
315
- * @returns {any} Mdast node
351
+ * @returns {MdastNode | null} Mdast node
316
352
  */
317
353
  function convertJxNode(el, isBlock) {
318
354
  // Bare string/number text nodes → mdast text nodes
@@ -362,17 +398,17 @@ function convertJxNode(el, isBlock) {
362
398
  case "link":
363
399
  return {
364
400
  type: "link",
365
- url: el.attributes?.href ?? "",
366
- title: el.attributes?.title ?? null,
401
+ url: /** @type {string} */ (el.attributes?.href) ?? "",
402
+ title: /** @type {string | null} */ (el.attributes?.title) ?? null,
367
403
  children: inlineChildren(el),
368
404
  };
369
405
 
370
406
  case "image":
371
407
  return {
372
408
  type: "image",
373
- url: el.attributes?.src ?? "",
374
- alt: el.attributes?.alt ?? "",
375
- title: el.attributes?.title ?? null,
409
+ url: /** @type {string} */ (el.attributes?.src) ?? "",
410
+ alt: /** @type {string} */ (el.attributes?.alt) ?? "",
411
+ title: /** @type {string | null} */ (el.attributes?.title) ?? null,
376
412
  };
377
413
 
378
414
  case "blockquote":
@@ -385,11 +421,14 @@ function convertJxNode(el, isBlock) {
385
421
  return {
386
422
  type: "list",
387
423
  ordered: tag === "ol",
388
- start: tag === "ol" ? parseInt(el.attributes?.start, 10) || 1 : null,
424
+ start:
425
+ tag === "ol" ? parseInt(/** @type {string} */ (el.attributes?.start), 10) || 1 : null,
389
426
  spread: false,
390
- children: (el.children ?? [])
391
- .map((/** @type {any} */ c) => convertJxNode(c, true))
392
- .filter(Boolean),
427
+ children: /** @type {MdastNode[]} */ (
428
+ /** @type {(JxElement | string)[]} */ (el.children ?? [])
429
+ .map((/** @type {JxElement | string} */ c) => convertJxNode(c, true))
430
+ .filter(Boolean)
431
+ ),
393
432
  };
394
433
 
395
434
  case "listItem":
@@ -401,8 +440,10 @@ function convertJxNode(el, isBlock) {
401
440
 
402
441
  case "code": {
403
442
  // pre > code → fenced code block
404
- const codeChild = el.children?.[0];
405
- const langClass = codeChild?.attributes?.class ?? "";
443
+ const codeChild = /** @type {JxElement | undefined} */ (
444
+ Array.isArray(el.children) ? el.children[0] : undefined
445
+ );
446
+ const langClass = /** @type {string} */ (codeChild?.attributes?.class) ?? "";
406
447
  const lang = langClass.replace("language-", "") || null;
407
448
  return {
408
449
  type: "code",
@@ -419,11 +460,12 @@ function convertJxNode(el, isBlock) {
419
460
 
420
461
  case "table": {
421
462
  // Flatten thead/tbody back to rows
422
- /** @type {any[]} */
463
+ /** @type {MdastNode[]} */
423
464
  const rows = [];
424
- for (const section of el.children ?? []) {
465
+ for (const section of /** @type {(JxElement | string)[]} */ (el.children ?? [])) {
466
+ if (typeof section === "string") continue;
425
467
  if (section.tagName === "thead" || section.tagName === "tbody") {
426
- for (const row of section.children ?? []) {
468
+ for (const row of /** @type {(JxElement | string)[]} */ (section.children ?? [])) {
427
469
  const mdRow = convertJxNode(row, true);
428
470
  if (mdRow) {
429
471
  // Mark header cells
@@ -439,7 +481,6 @@ function convertJxNode(el, isBlock) {
439
481
  }
440
482
  return {
441
483
  type: "table",
442
- align: null,
443
484
  children: rows,
444
485
  };
445
486
  }
@@ -447,9 +488,11 @@ function convertJxNode(el, isBlock) {
447
488
  case "tableRow":
448
489
  return {
449
490
  type: "tableRow",
450
- children: (el.children ?? [])
451
- .map((/** @type {any} */ c) => convertJxNode(c, false))
452
- .filter(Boolean),
491
+ children: /** @type {MdastNode[]} */ (
492
+ /** @type {(JxElement | string)[]} */ (el.children ?? [])
493
+ .map((/** @type {JxElement | string} */ c) => convertJxNode(c, false))
494
+ .filter(Boolean)
495
+ ),
453
496
  };
454
497
 
455
498
  case "tableCell":
@@ -466,28 +509,36 @@ function convertJxNode(el, isBlock) {
466
509
  * Get inline children from a Jx element as mdast nodes. Handles both textContent shorthand and
467
510
  * explicit children array.
468
511
  *
469
- * @param {any} el
470
- * @returns {any[]}
512
+ * @param {JxElement} el
513
+ * @returns {MdastNode[]}
471
514
  */
472
515
  function inlineChildren(el) {
473
516
  if (el.textContent != null) {
474
517
  return [{ type: "text", value: String(el.textContent) }];
475
518
  }
476
- return (el.children ?? []).map((/** @type {any} */ c) => convertJxNode(c, false)).filter(Boolean);
519
+ return /** @type {MdastNode[]} */ (
520
+ /** @type {(JxElement | string)[]} */ (el.children ?? [])
521
+ .map((/** @type {JxElement | string} */ c) => convertJxNode(c, false))
522
+ .filter(Boolean)
523
+ );
477
524
  }
478
525
 
479
526
  /**
480
527
  * Get block children from a Jx element as mdast nodes.
481
528
  *
482
- * @param {any} el
483
- * @returns {any[]}
529
+ * @param {JxElement} el
530
+ * @returns {MdastNode[]}
484
531
  */
485
532
  function blockChildren(el) {
486
533
  if (el.textContent != null) {
487
534
  // Wrap bare text in a paragraph
488
535
  return [{ type: "paragraph", children: [{ type: "text", value: String(el.textContent) }] }];
489
536
  }
490
- return (el.children ?? []).map((/** @type {any} */ c) => convertJxNode(c, true)).filter(Boolean);
537
+ return /** @type {MdastNode[]} */ (
538
+ /** @type {(JxElement | string)[]} */ (el.children ?? [])
539
+ .map((/** @type {JxElement | string} */ c) => convertJxNode(c, true))
540
+ .filter(Boolean)
541
+ );
491
542
  }
492
543
 
493
544
  /**
@@ -495,11 +546,11 @@ function blockChildren(el) {
495
546
  * handlers, etc.) and HTML attributes into a flat dot-path attribute map suitable for
496
547
  * remark-directive.
497
548
  *
498
- * @param {any} el
549
+ * @param {JxElement} el
499
550
  * @returns {Record<string, string>}
500
551
  */
501
552
  function collectDirectiveAttrs(el) {
502
- /** @type {Record<string, any>} */
553
+ /** @type {Record<string, unknown>} */
503
554
  const propsObj = {};
504
555
 
505
556
  for (const [key, value] of Object.entries(el)) {
@@ -516,7 +567,9 @@ function collectDirectiveAttrs(el) {
516
567
 
517
568
  // Merge HTML attributes
518
569
  if (el.attributes) {
519
- for (const [key, value] of Object.entries(el.attributes)) {
570
+ for (const [key, value] of Object.entries(
571
+ /** @type {Record<string, unknown>} */ (el.attributes),
572
+ )) {
520
573
  propsObj[key] = value;
521
574
  }
522
575
  }
@@ -528,12 +581,12 @@ function collectDirectiveAttrs(el) {
528
581
  * Convert a Jx element to a directive node, preserving all Jx-specific properties as collapsed
529
582
  * dot-path directive attributes.
530
583
  *
531
- * @param {any} el
584
+ * @param {JxElement} el
532
585
  * @param {boolean} isBlock
533
- * @returns {any}
586
+ * @returns {MdastNode}
534
587
  */
535
588
  function convertToDirective(el, isBlock) {
536
- const tag = el.tagName ?? "div";
589
+ const tag = /** @type {string} */ (el.tagName) ?? "div";
537
590
  const attrs = collectDirectiveAttrs(el);
538
591
 
539
592
  if (!isBlock) {
@@ -545,14 +598,17 @@ function convertToDirective(el, isBlock) {
545
598
  children:
546
599
  el.textContent != null
547
600
  ? [{ type: "text", value: String(el.textContent) }]
548
- : (el.children ?? [])
549
- .map((/** @type {any} */ c) => convertJxNode(c, false))
550
- .filter(Boolean),
601
+ : /** @type {MdastNode[]} */ (
602
+ /** @type {(JxElement | string)[]} */ (el.children ?? [])
603
+ .map((/** @type {JxElement | string} */ c) => convertJxNode(c, false))
604
+ .filter(Boolean)
605
+ ),
551
606
  };
552
607
  }
553
608
 
554
609
  // Block without children → leafDirective
555
- if (!el.children?.length && el.textContent == null) {
610
+ const childArray = /** @type {(JxElement | string)[] | undefined} */ (el.children);
611
+ if (!childArray?.length && el.textContent == null) {
556
612
  return {
557
613
  type: "leafDirective",
558
614
  name: tag,
@@ -562,7 +618,7 @@ function convertToDirective(el, isBlock) {
562
618
  }
563
619
 
564
620
  // Block with children → containerDirective
565
- /** @type {any[]} */
621
+ /** @type {MdastNode[]} */
566
622
  let directiveChildren;
567
623
  if (el.textContent != null) {
568
624
  directiveChildren = [
@@ -571,15 +627,19 @@ function convertToDirective(el, isBlock) {
571
627
  } else if (INLINE_CONTENT_TAGS.has(tag)) {
572
628
  // Tags with inline content model: wrap all children in a single paragraph
573
629
  // so remark serializes them as one continuous inline flow
574
- const inlineNodes = (el.children ?? [])
575
- .map((/** @type {any} */ c) => convertJxNode(c, false))
576
- .filter(Boolean);
630
+ const inlineNodes = /** @type {MdastNode[]} */ (
631
+ /** @type {(JxElement | string)[]} */ (el.children ?? [])
632
+ .map((/** @type {JxElement | string} */ c) => convertJxNode(c, false))
633
+ .filter(Boolean)
634
+ );
577
635
  directiveChildren =
578
636
  inlineNodes.length > 0 ? [{ type: "paragraph", children: inlineNodes }] : [];
579
637
  } else {
580
- directiveChildren = (el.children ?? [])
581
- .map((/** @type {any} */ c) => convertJxNode(c, true))
582
- .filter(Boolean);
638
+ directiveChildren = /** @type {MdastNode[]} */ (
639
+ /** @type {(JxElement | string)[]} */ (el.children ?? [])
640
+ .map((/** @type {JxElement | string} */ c) => convertJxNode(c, true))
641
+ .filter(Boolean)
642
+ );
583
643
  }
584
644
 
585
645
  return {
@@ -633,7 +693,7 @@ const JX_ANNOTATION_KEYS = new Set(["$title", "$description"]);
633
693
  * top-level props and uses remark-stringify with remark-directive for the body — standard markdown
634
694
  * elements emit as native syntax, Jx-decorated elements emit as directives.
635
695
  *
636
- * @param {any} doc - Jx JSON document
696
+ * @param {JxMutableNode} doc - Jx JSON document
637
697
  * @returns {string} Jx Markdown source
638
698
  */
639
699
  export function jxDocToMd(doc) {
@@ -643,7 +703,7 @@ export function jxDocToMd(doc) {
643
703
  const lines = [];
644
704
 
645
705
  // Emit YAML frontmatter
646
- /** @type {Record<string, any>} */
706
+ /** @type {Record<string, unknown>} */
647
707
  const frontmatter = {};
648
708
  for (const [key, value] of Object.entries(doc)) {
649
709
  if (key === "children") continue;
@@ -659,17 +719,22 @@ export function jxDocToMd(doc) {
659
719
 
660
720
  // Convert children to mdast and stringify with remark
661
721
  if (Array.isArray(doc.children) && doc.children.length > 0) {
662
- const mdastChildren = doc.children
663
- .map((/** @type {any} */ child) => convertJxNode(child, true))
664
- .filter(Boolean);
665
-
666
- const mdast = /** @type {any} */ ({ type: "root", children: mdastChildren });
722
+ const mdastChildren = /** @type {MdastNode[]} */ (
723
+ doc.children
724
+ .map((/** @type {JxMutableNode | string} */ child) =>
725
+ convertJxNode(/** @type {JxElement | string} */ (child), true),
726
+ )
727
+ .filter(Boolean)
728
+ );
729
+
730
+ const mdast = { type: "root", children: mdastChildren };
667
731
  const md = unified()
732
+ .use(remarkGfm)
668
733
  .use(remarkDirective)
669
734
  .use(remarkStringify, { bullet: "-", emphasis: "*", strong: "*" })
670
- .stringify(mdast);
735
+ .stringify(/** @type {import("mdast").Root} */ (/** @type {unknown} */ (mdast)));
671
736
 
672
- lines.push(md);
737
+ lines.push(/** @type {string} */ (md));
673
738
  }
674
739
 
675
740
  return (
@@ -683,9 +748,9 @@ export function jxDocToMd(doc) {
683
748
  /**
684
749
  * Lazy import of yaml stringify — avoids importing at module load.
685
750
  *
686
- * @returns {{ stringify: (v: any) => string }}
751
+ * @returns {{ stringify: (v: unknown) => string }}
687
752
  */
688
- let _yaml = /** @type {any} */ (null);
753
+ let _yaml = /** @type {{ stringify: (v: unknown) => string } | null} */ (null);
689
754
  function yamlImport() {
690
755
  if (!_yaml) {
691
756
  // Dynamic require avoided; use the yaml package already available in studio
@@ -698,7 +763,7 @@ function yamlImport() {
698
763
  * Simple YAML stringifier for frontmatter. Handles the subset of YAML needed for Jx frontmatter
699
764
  * (scalars, arrays, nested objects).
700
765
  *
701
- * @param {any} value
766
+ * @param {unknown} value
702
767
  * @param {number} indent
703
768
  * @returns {string}
704
769
  */
@@ -735,7 +800,7 @@ function yamlStringifySimple(value, indent = 0) {
735
800
  }
736
801
 
737
802
  if (typeof value === "object") {
738
- const entries = Object.entries(value);
803
+ const entries = Object.entries(/** @type {Record<string, unknown>} */ (value));
739
804
  if (entries.length === 0) return "{}";
740
805
  return entries
741
806
  .map(([k, v]) => {
@@ -755,14 +820,14 @@ function yamlStringifySimple(value, indent = 0) {
755
820
  * Collapse a Jx props object to a flat directive attribute map. Applies key mapping: strips `$`
756
821
  * from Jx keywords, `:` from pseudo-classes, `@` from media queries.
757
822
  *
758
- * @param {Record<string, any>} propsObj
823
+ * @param {Record<string, unknown>} propsObj
759
824
  * @returns {Record<string, string>}
760
825
  */
761
826
  function collapsePropsToAttrMap(propsObj) {
762
827
  /** @type {Record<string, string>} */
763
828
  const result = {};
764
829
 
765
- function walk(/** @type {Record<string, any>} */ obj, /** @type {string} */ prefix) {
830
+ function walk(/** @type {Record<string, unknown>} */ obj, /** @type {string} */ prefix) {
766
831
  for (const [key, value] of Object.entries(obj)) {
767
832
  let mdAttrKey = key;
768
833
  // Strip $ prefix for Jx keywords
@@ -785,7 +850,7 @@ function collapsePropsToAttrMap(propsObj) {
785
850
  const fullKey = prefix ? `${prefix}.${mdAttrKey}` : mdAttrKey;
786
851
 
787
852
  if (value && typeof value === "object" && !Array.isArray(value)) {
788
- walk(value, fullKey);
853
+ walk(/** @type {Record<string, unknown>} */ (value), fullKey);
789
854
  } else {
790
855
  result[fullKey] = String(value);
791
856
  }