@jxsuite/studio 0.19.0 → 0.20.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 (93) hide show
  1. package/dist/studio.css +98 -98
  2. package/dist/studio.js +12577 -12183
  3. package/dist/studio.js.map +146 -144
  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 +17 -6
  8. package/src/browse/browse.js +25 -19
  9. package/src/canvas/canvas-diff.js +24 -16
  10. package/src/canvas/canvas-helpers.js +24 -19
  11. package/src/canvas/canvas-live-render.js +94 -39
  12. package/src/canvas/canvas-render.js +119 -73
  13. package/src/canvas/canvas-utils.js +38 -15
  14. package/src/canvas/nested-site-style.js +50 -0
  15. package/src/editor/component-inline-edit.js +14 -13
  16. package/src/editor/content-inline-edit.js +22 -10
  17. package/src/editor/context-menu.js +1 -1
  18. package/src/editor/convert-to-component.js +12 -10
  19. package/src/editor/inline-edit.js +66 -28
  20. package/src/editor/insertion-helper.js +16 -12
  21. package/src/editor/shortcuts.js +4 -2
  22. package/src/editor/slash-menu.js +6 -4
  23. package/src/files/components.js +1 -1
  24. package/src/files/file-ops.js +37 -16
  25. package/src/files/files.js +78 -52
  26. package/src/github/github-auth.js +122 -0
  27. package/src/github/github-publish.js +115 -0
  28. package/src/markdown/md-convert.js +172 -107
  29. package/src/new-project/new-project-modal.js +204 -0
  30. package/src/panels/activity-bar.js +22 -20
  31. package/src/panels/ai-panel.js +399 -0
  32. package/src/panels/block-action-bar.js +5 -5
  33. package/src/panels/canvas-dnd.js +8 -2
  34. package/src/panels/data-explorer.js +19 -13
  35. package/src/panels/dnd.js +144 -92
  36. package/src/panels/editors.js +55 -35
  37. package/src/panels/elements-panel.js +33 -19
  38. package/src/panels/events-panel.js +15 -10
  39. package/src/panels/git-panel.js +469 -98
  40. package/src/panels/head-panel.js +121 -81
  41. package/src/panels/imports-panel.js +88 -64
  42. package/src/panels/layers-panel.js +64 -43
  43. package/src/panels/left-panel.js +85 -47
  44. package/src/panels/overlays.js +1 -0
  45. package/src/panels/panel-events.js +45 -30
  46. package/src/panels/preview-render.js +23 -18
  47. package/src/panels/properties-panel.js +365 -249
  48. package/src/panels/pseudo-preview.js +2 -2
  49. package/src/panels/quick-search.js +7 -5
  50. package/src/panels/right-panel.js +26 -2
  51. package/src/panels/shared.js +4 -4
  52. package/src/panels/signals-panel.js +295 -166
  53. package/src/panels/statusbar.js +4 -4
  54. package/src/panels/style-inputs.js +67 -35
  55. package/src/panels/style-panel.js +233 -132
  56. package/src/panels/style-utils.js +34 -16
  57. package/src/panels/stylebook-layers-panel.js +12 -10
  58. package/src/panels/stylebook-panel.js +134 -66
  59. package/src/panels/toolbar.js +333 -109
  60. package/src/panels/welcome-screen.js +121 -0
  61. package/src/platform.js +2 -4
  62. package/src/platforms/devserver.js +113 -7
  63. package/src/resize-edges.js +98 -0
  64. package/src/services/cem-export.js +12 -7
  65. package/src/services/code-services.js +30 -12
  66. package/src/settings/content-types-editor.js +7 -7
  67. package/src/settings/css-vars-editor.js +30 -24
  68. package/src/settings/defs-editor.js +12 -7
  69. package/src/settings/general-settings.js +85 -3
  70. package/src/settings/head-editor.js +30 -24
  71. package/src/settings/schema-field-ui.js +58 -39
  72. package/src/site-context.js +41 -31
  73. package/src/state.js +70 -29
  74. package/src/store.js +21 -26
  75. package/src/studio.js +103 -81
  76. package/src/tabs/tab.js +67 -43
  77. package/src/tabs/transact.js +23 -13
  78. package/src/ui/button-group.js +24 -24
  79. package/src/ui/color-selector.js +28 -23
  80. package/src/ui/field-row.js +3 -3
  81. package/src/ui/icons.js +2 -2
  82. package/src/ui/media-picker.js +7 -7
  83. package/src/ui/spectrum.js +5 -1
  84. package/src/ui/unit-selector.js +18 -16
  85. package/src/ui/value-selector.js +21 -15
  86. package/src/ui/widgets.js +22 -19
  87. package/src/utils/canvas-media.js +5 -4
  88. package/src/utils/edit-display.js +31 -20
  89. package/src/utils/google-fonts.js +11 -11
  90. package/src/utils/inherited-style.js +9 -8
  91. package/src/utils/studio-utils.js +7 -7
  92. package/src/view.js +58 -1
  93. package/src/workspace/workspace.js +10 -7
package/src/ui/widgets.js CHANGED
@@ -23,19 +23,19 @@ import { renderMediaPicker } from "./media-picker.js";
23
23
  * Render a plain text input widget.
24
24
  *
25
25
  * @param {string} prop
26
- * @param {any} value
26
+ * @param {string | number | undefined} value
27
27
  * @param {(val: string) => void} onChange
28
28
  * @param {string} [placeholder]
29
- * @returns {any}
29
+ * @returns {import("lit-html").TemplateResult}
30
30
  */
31
31
  export function renderTextInput(prop, value, onChange, placeholder = "") {
32
32
  return html`
33
33
  <sp-textfield
34
34
  size="s"
35
35
  placeholder=${placeholder}
36
- .value=${live(value || "")}
37
- @input=${debouncedStyleCommit(`text:${prop}`, 400, (/** @type {any} */ e) =>
38
- onChange(e.target.value),
36
+ .value=${live(String(value ?? ""))}
37
+ @input=${debouncedStyleCommit(`text:${prop}`, 400, (/** @type {Event} */ e) =>
38
+ onChange(/** @type {HTMLInputElement} */ (e.target).value),
39
39
  )}
40
40
  ></sp-textfield>
41
41
  `;
@@ -44,24 +44,27 @@ export function renderTextInput(prop, value, onChange, placeholder = "") {
44
44
  /**
45
45
  * Render a number input widget (sp-number-field).
46
46
  *
47
- * @param {any} entry
47
+ * @param {Record<string, unknown>} entry
48
48
  * @param {string} prop
49
- * @param {any} value
50
- * @param {(val: any) => void} onChange
51
- * @returns {any}
49
+ * @param {string | number | undefined} value
50
+ * @param {(val: string | number) => void} onChange
51
+ * @param {string} [placeholder]
52
+ * @returns {import("lit-html").TemplateResult}
52
53
  */
53
54
  export function renderNumberInput(entry, prop, value, onChange, placeholder = "") {
55
+ const minimum = /** @type {number | undefined} */ (entry.minimum);
56
+ const maximum = /** @type {number | undefined} */ (entry.maximum);
54
57
  return html`
55
58
  <sp-number-field
56
59
  size="s"
57
60
  hide-stepper
58
61
  .value=${live(value !== undefined && value !== "" ? Number(value) : undefined)}
59
62
  placeholder=${placeholder}
60
- min=${ifDefined(entry.minimum)}
61
- max=${ifDefined(entry.maximum)}
62
- step=${ifDefined(entry.maximum !== undefined && entry.maximum <= 1 ? 0.1 : undefined)}
63
- @change=${debouncedStyleCommit(`num:${prop}`, 400, (/** @type {any} */ e) => {
64
- const v = e.target.value;
63
+ min=${ifDefined(minimum)}
64
+ max=${ifDefined(maximum)}
65
+ step=${ifDefined(maximum !== undefined && maximum <= 1 ? 0.1 : undefined)}
66
+ @change=${debouncedStyleCommit(`num:${prop}`, 400, (/** @type {Event} */ e) => {
67
+ const v = /** @type {HTMLInputElement & { value: number | undefined }} */ (e.target).value;
65
68
  if (v === undefined || isNaN(v)) onChange("");
66
69
  else onChange(Number(v));
67
70
  })}
@@ -73,12 +76,12 @@ export function renderNumberInput(entry, prop, value, onChange, placeholder = ""
73
76
  * Dispatch to the appropriate widget based on inferred type.
74
77
  *
75
78
  * @param {string} type — one of: button-group, color, number-unit, number, select, combobox, text
76
- * @param {any} entry — css-meta or schema entry
79
+ * @param {Record<string, unknown>} entry — css-meta or schema entry
77
80
  * @param {string} prop — property key
78
- * @param {any} value — current value
79
- * @param {(val: any) => void} onCommit — commit callback
81
+ * @param {string | number | undefined} value — current value
82
+ * @param {(val: string | number) => void} onCommit — commit callback
80
83
  * @param {{ placeholder?: string; renderSelect?: Function; renderCombobox?: Function }} [opts]
81
- * @returns {any}
84
+ * @returns {import("lit-html").TemplateResult}
82
85
  */
83
86
  export function widgetForType(type, entry, prop, value, onCommit, opts = {}) {
84
87
  switch (type) {
@@ -91,7 +94,7 @@ export function widgetForType(type, entry, prop, value, onCommit, opts = {}) {
91
94
  case "number":
92
95
  return renderNumberInput(entry, prop, value, onCommit, opts.placeholder);
93
96
  case "media":
94
- return renderMediaPicker(prop, value, onCommit);
97
+ return renderMediaPicker(prop, String(value ?? ""), onCommit);
95
98
  case "select":
96
99
  // Allow caller to override select rendering (e.g. for typography preview)
97
100
  if (opts.renderSelect) return opts.renderSelect(entry, prop, value, onCommit);
@@ -53,7 +53,7 @@ export function activeBreakpointsForWidth(sizeBreakpoints, canvasWidth) {
53
53
  * first, then matching media overrides in source order.
54
54
  *
55
55
  * @param {HTMLElement} el
56
- * @param {Record<string, any>} styleDef
56
+ * @param {JxStyle | null | undefined} styleDef
57
57
  * @param {Set<string>} activeBreakpoints
58
58
  * @param {Record<string, boolean>} featureToggles
59
59
  */
@@ -63,7 +63,7 @@ export function applyCanvasStyle(el, styleDef, activeBreakpoints, featureToggles
63
63
  if (typeof val === "string" || typeof val === "number") {
64
64
  try {
65
65
  if (prop.startsWith("--")) el.style.setProperty(prop, String(val));
66
- else /** @type {any} */ (el.style)[prop] = val;
66
+ else /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (el.style))[prop] = val;
67
67
  } catch {}
68
68
  }
69
69
  }
@@ -72,11 +72,12 @@ export function applyCanvasStyle(el, styleDef, activeBreakpoints, featureToggles
72
72
  const mediaName = key.slice(1);
73
73
  if (mediaName === "--") continue;
74
74
  if (activeBreakpoints.has(mediaName) || featureToggles[mediaName]) {
75
- for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
75
+ for (const [prop, v] of Object.entries(/** @type {Record<string, unknown>} */ (val))) {
76
76
  if (typeof v === "string" || typeof v === "number") {
77
77
  try {
78
78
  if (prop.startsWith("--")) el.style.setProperty(prop, String(v));
79
- else /** @type {any} */ (el.style)[prop] = v;
79
+ else
80
+ /** @type {Record<string, unknown>} */ (/** @type {unknown} */ (el.style))[prop] = v;
80
81
  } catch {}
81
82
  }
82
83
  }
@@ -8,7 +8,7 @@
8
8
  * Convert a template string to a displayable expression for edit mode. Replaces ${expr} with ❮ expr
9
9
  * ❯ so the runtime renders it as literal text.
10
10
  *
11
- * @param {any} str
11
+ * @param {string} str
12
12
  */
13
13
  export function templateToEditDisplay(str) {
14
14
  return str.replace(/\$\{([^}]+)\}/g, "\u276A $1 \u276B");
@@ -18,14 +18,14 @@ export function templateToEditDisplay(str) {
18
18
  * Reverse templateToEditDisplay: walk all text nodes in `el` and replace ❪ expr ❫ back to ${expr}
19
19
  * so the user edits raw template syntax.
20
20
  *
21
- * @param {any} el
21
+ * @param {HTMLElement} el
22
22
  */
23
23
  export function restoreTemplateExpressions(el) {
24
24
  const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
25
25
  while (walker.nextNode()) {
26
- const node = /** @type {any} */ (walker.currentNode);
27
- if (node.textContent.includes("\u276A")) {
28
- node.textContent = node.textContent.replace(/\u276A\s*(.*?)\s*\u276B/g, "${$1}");
26
+ const node = /** @type {Text} */ (walker.currentNode);
27
+ if (node.data.includes("\u276A")) {
28
+ node.data = node.data.replace(/\u276A\s*(.*?)\s*\u276B/g, "${$1}");
29
29
  }
30
30
  }
31
31
  }
@@ -35,24 +35,31 @@ export function restoreTemplateExpressions(el) {
35
35
  * $prototype:Array with placeholders, and $ref bindings with display labels. Preserves state so the
36
36
  * runtime can still initialise scope.
37
37
  *
38
- * @param {any} node
39
- * @returns {any}
38
+ * @param {JxMutableNode} node
39
+ * @returns {JxMutableNode}
40
40
  */
41
41
  export function prepareForEditMode(node) {
42
42
  if (!node || typeof node !== "object") return node;
43
43
  if (Array.isArray(node)) return node.map(prepareForEditMode);
44
44
 
45
- /** @type {Record<string, any>} */
45
+ const /** @type {Record<string, unknown>} */ obj = /** @type {Record<string, unknown>} */ (node);
46
+
47
+ /** @type {Record<string, unknown>} */
46
48
  const out = {};
47
- for (const [k, v] of Object.entries(node)) {
49
+ for (const [k, v] of Object.entries(obj)) {
48
50
  if (k === "state" || k === "$media" || k === "$props" || k === "$elements") {
49
51
  out[k] = v; // preserve as-is for runtime resolution
50
52
  } else if (k === "children") {
51
53
  if (Array.isArray(v)) {
52
54
  out.children = v.map(prepareForEditMode);
53
- } else if (v && typeof v === "object" && v.$prototype === "Array") {
55
+ } else if (
56
+ v &&
57
+ typeof v === "object" &&
58
+ /** @type {Record<string, unknown>} */ (v).$prototype === "Array"
59
+ ) {
54
60
  // Wrap the map template in a visual repeater perimeter
55
- const template = v.map;
61
+ const vObj = /** @type {Record<string, unknown>} */ (v);
62
+ const template = vObj.map;
56
63
  if (template && typeof template === "object") {
57
64
  out.children = [
58
65
  {
@@ -70,14 +77,18 @@ export function prepareForEditMode(node) {
70
77
  out.children = [];
71
78
  }
72
79
  } else {
73
- out.children = prepareForEditMode(v);
80
+ out.children = prepareForEditMode(/** @type {JxMutableNode} */ (v));
74
81
  }
75
- } else if (k === "cases" && node.$switch && v && typeof v === "object") {
82
+ } else if (k === "cases" && obj.$switch && v && typeof v === "object") {
76
83
  // Replace $switch cases with a placeholder showing the first case or a label
77
84
  const caseKeys = Object.keys(v);
78
85
  if (caseKeys.length > 0) {
79
- const firstCase = v[caseKeys[0]];
80
- if (firstCase && typeof firstCase === "object" && !firstCase.$ref) {
86
+ const firstCase = /** @type {Record<string, unknown>} */ (v)[caseKeys[0]];
87
+ if (
88
+ firstCase &&
89
+ typeof firstCase === "object" &&
90
+ !(/** @type {Record<string, unknown>} */ (firstCase).$ref)
91
+ ) {
81
92
  out.children = [prepareForEditMode(firstCase)];
82
93
  } else {
83
94
  out.children = [
@@ -101,7 +112,7 @@ export function prepareForEditMode(node) {
101
112
  } else if (k === "style") {
102
113
  // Replace template strings in style values with empty strings
103
114
  if (v && typeof v === "object") {
104
- /** @type {Record<string, any>} */
115
+ /** @type {Record<string, unknown>} */
105
116
  const s = {};
106
117
  for (const [sk, sv] of Object.entries(v)) {
107
118
  s[sk] = typeof sv === "string" && sv.includes("${") ? "" : sv;
@@ -113,13 +124,13 @@ export function prepareForEditMode(node) {
113
124
  } else if (typeof v === "string" && v.includes("${")) {
114
125
  // Template string in a display property → show raw expression
115
126
  out[k] = templateToEditDisplay(v);
116
- } else if (v && typeof v === "object" && v.$ref) {
127
+ } else if (v && typeof v === "object" && /** @type {Record<string, unknown>} */ (v).$ref) {
117
128
  // $ref binding → show ref path as literal text
118
- const ref = v.$ref;
129
+ const ref = /** @type {string} */ (/** @type {Record<string, unknown>} */ (v).$ref);
119
130
  const label = ref.startsWith("#/state/") ? ref.slice(8) : ref;
120
131
  out[k] = `{${label}}`;
121
132
  } else {
122
- out[k] = prepareForEditMode(v);
133
+ out[k] = prepareForEditMode(/** @type {JxMutableNode} */ (v));
123
134
  }
124
135
  }
125
136
 
@@ -127,7 +138,7 @@ export function prepareForEditMode(node) {
127
138
  if (out.tagName && !out.textContent && !out.innerHTML) {
128
139
  const hasChildren = Array.isArray(out.children) && out.children.length > 0;
129
140
  if (!hasChildren) {
130
- const tag = out.tagName;
141
+ const tag = /** @type {string} */ (out.tagName);
131
142
  const textTags = new Set([
132
143
  "p",
133
144
  "h1",
@@ -9,7 +9,7 @@ export const GFONTS_PRECONNECT_ORIGINS = [
9
9
  /**
10
10
  * Check if a `$head` entry is a Google Fonts stylesheet link.
11
11
  *
12
- * @param {any} entry
12
+ * @param {JxHeadEntry} entry
13
13
  * @returns {boolean}
14
14
  */
15
15
  export function isGoogleFontEntry(entry) {
@@ -17,21 +17,21 @@ export function isGoogleFontEntry(entry) {
17
17
  entry?.tagName === "link" &&
18
18
  entry?.attributes?.rel === "stylesheet" &&
19
19
  typeof entry?.attributes?.href === "string" &&
20
- entry.attributes.href.startsWith(GFONTS_CSS_PREFIX)
20
+ /** @type {string} */ (entry.attributes.href).startsWith(GFONTS_CSS_PREFIX)
21
21
  );
22
22
  }
23
23
 
24
24
  /**
25
25
  * Check if a `$head` entry is a Google Fonts preconnect link.
26
26
  *
27
- * @param {any} entry
27
+ * @param {JxHeadEntry} entry
28
28
  * @returns {boolean}
29
29
  */
30
30
  export function isGoogleFontPreconnect(entry) {
31
31
  return (
32
32
  entry?.tagName === "link" &&
33
33
  entry?.attributes?.rel === "preconnect" &&
34
- GFONTS_PRECONNECT_ORIGINS.includes(entry?.attributes?.href)
34
+ GFONTS_PRECONNECT_ORIGINS.includes(/** @type {string} */ (entry?.attributes?.href))
35
35
  );
36
36
  }
37
37
 
@@ -60,18 +60,18 @@ export function buildGoogleFontUrl(family) {
60
60
  /**
61
61
  * Ensure preconnect links exist in a `$head` array for Google Fonts.
62
62
  *
63
- * @param {any[]} head
63
+ * @param {JxHeadEntry[]} head
64
64
  */
65
65
  export function ensureGoogleFontPreconnects(head) {
66
66
  for (const origin of GFONTS_PRECONNECT_ORIGINS) {
67
67
  const exists = head.some(
68
- (/** @type {any} */ e) =>
68
+ (/** @type {JxHeadEntry} */ e) =>
69
69
  e?.tagName === "link" &&
70
70
  e?.attributes?.rel === "preconnect" &&
71
71
  e?.attributes?.href === origin,
72
72
  );
73
73
  if (!exists) {
74
- /** @type {Record<string, any>} */
74
+ /** @type {Record<string, string | boolean>} */
75
75
  const attrs = { rel: "preconnect", href: origin };
76
76
  if (origin === "https://fonts.gstatic.com") attrs.crossorigin = "";
77
77
  head.push({ tagName: "link", attributes: attrs });
@@ -82,13 +82,13 @@ export function ensureGoogleFontPreconnects(head) {
82
82
  /**
83
83
  * Remove preconnect links if no Google Font stylesheets remain.
84
84
  *
85
- * @param {any[]} head
86
- * @returns {any[]}
85
+ * @param {JxHeadEntry[]} head
86
+ * @returns {JxHeadEntry[]}
87
87
  */
88
88
  export function cleanupGoogleFontPreconnects(head) {
89
- const hasFont = head.some((/** @type {any} */ e) => isGoogleFontEntry(e));
89
+ const hasFont = head.some((/** @type {JxHeadEntry} */ e) => isGoogleFontEntry(e));
90
90
  if (!hasFont) {
91
- return head.filter((/** @type {any} */ e) => !isGoogleFontPreconnect(e));
91
+ return head.filter((/** @type {JxHeadEntry} */ e) => !isGoogleFontPreconnect(e));
92
92
  }
93
93
  return head;
94
94
  }
@@ -9,43 +9,44 @@
9
9
  /**
10
10
  * Compute the inherited style object for a given breakpoint tab.
11
11
  *
12
- * @param {Record<string, any>} style — full style object (flat props + @media blocks + selectors)
12
+ * @param {JxStyle} style — full style object (flat props + @media blocks + selectors)
13
13
  * @param {string[]} mediaNames — ordered breakpoint names (from parseMediaEntries, respects cascade
14
14
  * direction)
15
15
  * @param {string | null} activeTab — current breakpoint tab name, or null for base
16
16
  * @param {string | null} activeSelector — current nested selector, or null
17
- * @returns {Record<string, any>} Inherited style map (prop → value)
17
+ * @returns {Record<string, string | number>} Inherited style map (prop → value)
18
18
  */
19
19
  export function computeInheritedStyle(style, mediaNames, activeTab, activeSelector = null) {
20
20
  if (activeTab === null || mediaNames.length === 0) return {};
21
21
 
22
- /** @type {Record<string, any>} */
22
+ /** @type {Record<string, string | number>} */
23
23
  let inherited = {};
24
24
 
25
25
  if (!activeSelector) {
26
26
  // Start with base flat props
27
27
  for (const [p, v] of Object.entries(style)) {
28
- if (typeof v !== "object") inherited[p] = v;
28
+ if (typeof v !== "object") inherited[p] = v ?? "";
29
29
  }
30
30
  // Layer each media block in order until current tab
31
31
  for (const name of mediaNames) {
32
32
  if (name === activeTab) break;
33
33
  const block = style[`@${name}`] || {};
34
34
  for (const [p, v] of Object.entries(block)) {
35
- if (typeof v !== "object") inherited[p] = v;
35
+ if (typeof v !== "object") inherited[p] = v ?? "";
36
36
  }
37
37
  }
38
38
  } else {
39
39
  // Selector inheritance: base selector → each media's selector block in order
40
40
  const baseSel = style[activeSelector] || {};
41
41
  for (const [p, v] of Object.entries(baseSel)) {
42
- if (typeof v !== "object") inherited[p] = v;
42
+ if (typeof v !== "object") inherited[p] = v ?? "";
43
43
  }
44
44
  for (const name of mediaNames) {
45
45
  if (name === activeTab) break;
46
- const selBlock = (style[`@${name}`] || {})[activeSelector] || {};
46
+ const selBlock =
47
+ /** @type {Record<string, unknown>} */ (style[`@${name}`] || {})[activeSelector] || {};
47
48
  for (const [p, v] of Object.entries(selBlock)) {
48
- if (typeof v !== "object") inherited[p] = v;
49
+ if (typeof v !== "object") inherited[p] = v ?? "";
49
50
  }
50
51
  }
51
52
  }
@@ -45,7 +45,7 @@ export function kebabToLabel(val) {
45
45
  /**
46
46
  * Get display label from metadata entry or prop name
47
47
  *
48
- * @param {any} entry
48
+ * @param {{ $label?: string; [key: string]: unknown } | null | undefined} entry
49
49
  * @param {string} prop
50
50
  * @returns {string}
51
51
  */
@@ -56,7 +56,7 @@ export function propLabel(entry, prop) {
56
56
  /**
57
57
  * Label for HTML attributes — handles kebab-case (aria-label → "Aria Label")
58
58
  *
59
- * @param {any} entry
59
+ * @param {{ $label?: string; [key: string]: unknown } | null | undefined} entry
60
60
  * @param {string} attr
61
61
  * @returns {string}
62
62
  */
@@ -106,7 +106,7 @@ export function abbreviateValue(val) {
106
106
  /**
107
107
  * Determine input widget type from a css-meta entry
108
108
  *
109
- * @param {any} entry
109
+ * @param {Record<string, unknown>} entry
110
110
  * @returns {string}
111
111
  */
112
112
  export function inferInputType(entry) {
@@ -127,13 +127,13 @@ export function inferInputType(entry) {
127
127
  * extension matching against the content type's `source` glob.
128
128
  *
129
129
  * @param {string | null} documentPath — project-relative path (e.g. "blog/hello.md")
130
- * @param {any} projectConfig — parsed project.json
131
- * @returns {{ name: string; schema: any } | null}
130
+ * @param {ProjectConfig | null | undefined} projectConfig — parsed project.json
131
+ * @returns {{ name: string; schema: ContentTypeSchema } | null}
132
132
  */
133
133
  export function findContentTypeSchema(documentPath, projectConfig) {
134
134
  if (!documentPath || !projectConfig?.contentTypes) return null;
135
135
  for (const [name, def] of Object.entries(
136
- /** @type {Record<string, any>} */ (projectConfig.contentTypes),
136
+ /** @type {Record<string, ContentTypeDef>} */ (projectConfig.contentTypes),
137
137
  )) {
138
138
  if (!def.source || !def.schema) continue;
139
139
  const src = def.source.replace(/^\.\//, "");
@@ -186,7 +186,7 @@ export function varDisplayName(varName, prefix) {
186
186
  .replace(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), "")
187
187
  .replace(/^--/, "")
188
188
  .replace(/-/g, " ")
189
- .replace(/\b\w/g, (/** @type {any} */ c) => c.toUpperCase()) || varName
189
+ .replace(/\b\w/g, (/** @type {string} */ c) => c.toUpperCase()) || varName
190
190
  );
191
191
  }
192
192
 
package/src/view.js CHANGED
@@ -6,7 +6,61 @@
6
6
  * store.js) makes renderer dependencies explicit.
7
7
  */
8
8
 
9
- /** @type {any} */
9
+ /**
10
+ * @typedef {{
11
+ * panzoomWrap: HTMLElement | null;
12
+ * renderGeneration: number;
13
+ * centerObserver: ResizeObserver | null;
14
+ * needsCenter: boolean;
15
+ * panX: number;
16
+ * panY: number;
17
+ * prevCanvasMode: string | null;
18
+ * monacoEditor:
19
+ * | (import("monaco-editor").editor.IStandaloneCodeEditor & { _ignoreNextChange?: boolean })
20
+ * | null;
21
+ * functionEditor:
22
+ * | (import("monaco-editor").editor.IStandaloneCodeEditor & {
23
+ * _ignoreNextChange?: boolean;
24
+ * _editingTarget?: string | null;
25
+ * })
26
+ * | null;
27
+ * componentInlineEdit: {
28
+ * el: HTMLElement;
29
+ * path: (string | number)[];
30
+ * originalText: string;
31
+ * mediaName: string | null;
32
+ * _outsideHandler?: ((e: MouseEvent) => void) | null;
33
+ * [k: string]: unknown;
34
+ * } | null;
35
+ * inlineEditCleanup: (() => void) | null;
36
+ * blockActionBarEl: HTMLElement | null;
37
+ * selDragCleanup: (() => void) | null;
38
+ * dndCleanups: (() => void)[];
39
+ * canvasDndCleanups: (() => void)[];
40
+ * canvasEventCleanups: (() => void)[];
41
+ * forcedStyleTag: HTMLStyleElement | null;
42
+ * forcedAttrEl: HTMLElement | null;
43
+ * elementsCollapsed: Set<string>;
44
+ * elementsFilter: string;
45
+ * lastDragInput: { clientX: number; clientY: number; [k: string]: unknown } | null;
46
+ * _currentDropTargetRow: HTMLElement | null;
47
+ * layerDragSourceHeight: number;
48
+ * savedRange: Range | null;
49
+ * _completionRegistered: boolean;
50
+ * stylebookElToTag: WeakMap<Element, string>;
51
+ * showAddBreakpointForm: boolean;
52
+ * addBreakpointPreview: string;
53
+ * layoutSelection: unknown;
54
+ * leftTab: string;
55
+ * leftPanelCollapsed: boolean;
56
+ * rightPanelCollapsed: boolean;
57
+ * autosaveTimer: ReturnType<typeof setTimeout> | null;
58
+ * _layersCollapsed: Set<string> | null;
59
+ * [key: string]: unknown;
60
+ * }} ViewState
61
+ */
62
+
63
+ /** @type {ViewState} */
10
64
  export const view = {
11
65
  // Canvas infrastructure
12
66
  panzoomWrap: null,
@@ -70,6 +124,9 @@ export const view = {
70
124
 
71
125
  // Autosave
72
126
  autosaveTimer: null,
127
+
128
+ // Layers panel collapsed state
129
+ _layersCollapsed: null,
73
130
  };
74
131
 
75
132
  const COLLAPSE_STORAGE_KEY = "jx-studio-panel-widths";
@@ -16,9 +16,9 @@ export const workspace = reactive({
16
16
  projectConfig: null,
17
17
  /** @type {ComponentEntry[]} */
18
18
  componentRegistry: [],
19
- /** @type {import("../state.js").JxNode | null} */
19
+ /** @type {JxMutableNode | null} */
20
20
  clipboard: null,
21
- /** @type {Record<string, any> | null} */
21
+ /** @type {JxStyle | null} */
22
22
  styleClipboard: null,
23
23
  fileTree: {
24
24
  /** @type {Map<string, FileEntry[]>} */
@@ -41,9 +41,11 @@ export const workspace = reactive({
41
41
  });
42
42
 
43
43
  /** @type {import("@vue/reactivity").ComputedRef<Tab>} */
44
- export const activeTab = /** @type {any} */ (
45
- computed(() =>
46
- workspace.activeTabId ? (workspace.tabs.get(workspace.activeTabId) ?? null) : null,
44
+ export const activeTab = /** @type {import("@vue/reactivity").ComputedRef<Tab>} */ (
45
+ /** @type {unknown} */ (
46
+ computed(() =>
47
+ workspace.activeTabId ? (workspace.tabs.get(workspace.activeTabId) ?? null) : null,
48
+ )
47
49
  )
48
50
  );
49
51
 
@@ -54,9 +56,10 @@ export const activeTab = /** @type {any} */ (
54
56
  * id: string;
55
57
  * documentPath?: string | null;
56
58
  * fileHandle?: FileSystemFileHandle | null;
57
- * document: Record<string, any>;
59
+ * document: Record<string, unknown>;
58
60
  * frontmatter?: Record<string, unknown>;
59
61
  * sourceFormat?: string | null;
62
+ * capabilities?: { modes?: string[] };
60
63
  * }} opts
61
64
  * @returns {Tab}
62
65
  */
@@ -102,7 +105,7 @@ export function closeAllTabs() {
102
105
  * @param {{
103
106
  * id: string;
104
107
  * documentPath?: string | null;
105
- * document: Record<string, any>;
108
+ * document: Record<string, unknown>;
106
109
  * frontmatter?: Record<string, unknown>;
107
110
  * sourceFormat?: string | null;
108
111
  * }} newTabOpts