@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
@@ -0,0 +1,298 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Edit-mode display transforms — extracted from studio.js (Phase 4i). Pure stateless functions that
4
+ * convert document trees for visual editing (template expressions, $map, $switch, empty
5
+ * placeholders).
6
+ */
7
+
8
+ import type { JxMutableNode } from "@jxsuite/schema/types";
9
+
10
+ const mediaTags = new Set(["img", "video", "source", "iframe", "audio"]);
11
+ const TRANSPARENT_PX =
12
+ "data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
13
+
14
+ /**
15
+ * Convert a template string to a displayable expression for edit mode. Replaces ${expr} with ❮ expr
16
+ * ❯ so the runtime renders it as literal text.
17
+ *
18
+ * @param {string} str
19
+ */
20
+ export function templateToEditDisplay(str: string) {
21
+ return str.replace(/\$\{([^}]+)\}/g, "\u276A $1 \u276B");
22
+ }
23
+
24
+ /**
25
+ * Reverse templateToEditDisplay: walk all text nodes in `el` and replace ❪ expr ❫ back to ${expr}
26
+ * so the user edits raw template syntax.
27
+ *
28
+ * @param {HTMLElement} el
29
+ */
30
+ export function restoreTemplateExpressions(el: HTMLElement) {
31
+ const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
32
+ while (walker.nextNode()) {
33
+ const node = walker.currentNode as Text;
34
+ if (node.data.includes("\u276A")) {
35
+ node.data = node.data.replace(/\u276A\s*(.*?)\s*\u276B/g, "${$1}");
36
+ }
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Prepare a document for edit-mode rendering. Replaces template strings with readable literal text,
42
+ * $prototype:Array with placeholders, and $ref bindings with display labels. Preserves state so the
43
+ * runtime can still initialise scope.
44
+ *
45
+ * @param {JxMutableNode} node
46
+ * @returns {JxMutableNode}
47
+ */
48
+ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
49
+ if (!node || typeof node !== "object") return node;
50
+ if (Array.isArray(node)) return node.map(prepareForEditMode);
51
+
52
+ const /** @type {Record<string, unknown>} */ obj = node as Record<string, unknown>;
53
+
54
+ const out: Record<string, unknown> = {};
55
+ let needsMediaPlaceholder = false;
56
+ const isMediaElement = mediaTags.has((obj.tagName as string) || "");
57
+
58
+ // Check if this media element lacks a resolvable src (top-level or in attributes)
59
+ if (isMediaElement) {
60
+ const attrs = obj.attributes as Record<string, unknown> | undefined;
61
+ const topSrc = obj.src;
62
+ const attrSrc = attrs?.src;
63
+ const topPoster = obj.poster;
64
+ const attrPoster = attrs?.poster;
65
+ const hasSrc = (topSrc && topSrc !== "") || (attrSrc && attrSrc !== "");
66
+ const hasPoster = (topPoster && topPoster !== "") || (attrPoster && attrPoster !== "");
67
+ if (!hasSrc && !hasPoster) {
68
+ needsMediaPlaceholder = true;
69
+ }
70
+ }
71
+
72
+ for (const [k, v] of Object.entries(obj)) {
73
+ if (k === "state" || k === "$media" || k === "$elements") {
74
+ out[k] = v; // preserve as-is for runtime resolution
75
+ } else if (k === "$props" && v && typeof v === "object") {
76
+ // Process $props values: convert template strings to display format
77
+ const propsOut: Record<string, unknown> = {};
78
+ for (const [pk, pv] of Object.entries(v)) {
79
+ if (typeof pv === "string" && pv.includes("${")) {
80
+ const isUrlAttr = pk === "src" || pk === "href" || pk === "poster" || pk === "action";
81
+ propsOut[pk] = isUrlAttr ? "" : templateToEditDisplay(pv);
82
+ } else if (pv && typeof pv === "object" && (pv as Record<string, unknown>).$ref) {
83
+ const ref = (pv as Record<string, unknown>).$ref as string;
84
+ const label = ref.startsWith("#/state/") ? ref.slice(8) : ref;
85
+ propsOut[pk] = `{${label}}`;
86
+ } else {
87
+ propsOut[pk] = pv;
88
+ }
89
+ }
90
+ out[k] = propsOut;
91
+ } else if (k === "children") {
92
+ if (Array.isArray(v)) {
93
+ out.children = v.map(prepareForEditMode);
94
+ } else if (
95
+ v &&
96
+ typeof v === "object" &&
97
+ (v as Record<string, unknown>).$prototype === "Array"
98
+ ) {
99
+ // Wrap the map template in a visual repeater perimeter
100
+ const vObj = v as Record<string, unknown>;
101
+ const template = vObj.map;
102
+ if (template && typeof template === "object") {
103
+ out.children = [
104
+ {
105
+ tagName: "div",
106
+ className: "repeater-perimeter",
107
+ children: [prepareForEditMode(template as JxMutableNode)],
108
+ },
109
+ ];
110
+ } else {
111
+ out.children = [];
112
+ }
113
+ } else {
114
+ out.children = prepareForEditMode(v as JxMutableNode);
115
+ }
116
+ } else if (k === "cases" && obj.$switch && v && typeof v === "object") {
117
+ // Replace $switch cases with a placeholder showing the first case or a label
118
+ const caseKeys = Object.keys(v);
119
+ if (caseKeys.length > 0) {
120
+ const firstCase = (v as Record<string, unknown>)[caseKeys[0]];
121
+ if (
122
+ firstCase &&
123
+ typeof firstCase === "object" &&
124
+ !(firstCase as Record<string, unknown>).$ref
125
+ ) {
126
+ out.children = [prepareForEditMode(firstCase)];
127
+ } else {
128
+ out.children = [
129
+ {
130
+ tagName: "div",
131
+ textContent: `[$switch: ${caseKeys.join(" | ")}]`,
132
+ style: {
133
+ fontFamily: "'SF Mono', 'Fira Code', monospace",
134
+ fontSize: "11px",
135
+ padding: "6px 10px",
136
+ background: "color-mix(in srgb, var(--danger) 8%, transparent)",
137
+ border: "1px dashed color-mix(in srgb, var(--danger) 40%, transparent)",
138
+ borderRadius: "4px",
139
+ color: "var(--danger)",
140
+ fontStyle: "italic",
141
+ },
142
+ },
143
+ ];
144
+ }
145
+ }
146
+ } else if (k === "attributes" && isMediaElement && v && typeof v === "object") {
147
+ // Process attributes for media elements: replace src/poster with transparent pixel
148
+ const attrs = v as Record<string, unknown>;
149
+ const processed: Record<string, unknown> = {};
150
+ for (const [ak, av] of Object.entries(attrs)) {
151
+ if (ak === "src" || ak === "poster") {
152
+ if (typeof av === "string" && av !== "" && !av.includes("${")) {
153
+ processed[ak] = av;
154
+ } else {
155
+ needsMediaPlaceholder = true;
156
+ processed[ak] = TRANSPARENT_PX;
157
+ }
158
+ } else if (typeof av === "string" && av.includes("${")) {
159
+ const isUrlAttr = ak === "href" || ak === "action";
160
+ processed[ak] = isUrlAttr ? "" : templateToEditDisplay(av);
161
+ } else if (av && typeof av === "object" && (av as Record<string, unknown>).$ref) {
162
+ const ref = (av as Record<string, unknown>).$ref as string;
163
+ const label = ref.startsWith("#/state/") ? ref.slice(8) : ref;
164
+ processed[ak] = `{${label}}`;
165
+ } else {
166
+ processed[ak] = av;
167
+ }
168
+ }
169
+ out.attributes = processed;
170
+ } else if (k === "style") {
171
+ // Replace template strings in style values with empty strings
172
+ if (v && typeof v === "object") {
173
+ const s: Record<string, unknown> = {};
174
+ for (const [sk, sv] of Object.entries(v)) {
175
+ s[sk] = typeof sv === "string" && sv.includes("${") ? "" : sv;
176
+ }
177
+ out.style = s;
178
+ } else {
179
+ out.style = v;
180
+ }
181
+ } else if (typeof v === "string" && v.includes("${")) {
182
+ // Template string in a display property → show raw expression
183
+ const isMediaSrc =
184
+ (k === "src" || k === "poster") && mediaTags.has((obj.tagName as string) || "");
185
+ if (isMediaSrc) {
186
+ needsMediaPlaceholder = true;
187
+ out[k] = TRANSPARENT_PX;
188
+ } else {
189
+ const isUrlAttr = k === "src" || k === "href" || k === "poster" || k === "action";
190
+ out[k] = isUrlAttr ? "" : templateToEditDisplay(v);
191
+ }
192
+ } else if (v && typeof v === "object" && (v as Record<string, unknown>).$ref) {
193
+ // $ref binding → show ref path as literal text
194
+ const ref = (v as Record<string, unknown>).$ref as string;
195
+ const label = ref.startsWith("#/state/") ? ref.slice(8) : ref;
196
+ const isMediaSrc =
197
+ (k === "src" || k === "poster") && mediaTags.has((obj.tagName as string) || "");
198
+ if (isMediaSrc) {
199
+ needsMediaPlaceholder = true;
200
+ out[k] = TRANSPARENT_PX;
201
+ } else {
202
+ out[k] = `{${label}}`;
203
+ }
204
+ } else {
205
+ // Empty src/poster on media elements → use transparent pixel placeholder
206
+ if (
207
+ (k === "src" || k === "poster") &&
208
+ v === "" &&
209
+ mediaTags.has((obj.tagName as string) || "")
210
+ ) {
211
+ needsMediaPlaceholder = true;
212
+ out[k] = TRANSPARENT_PX;
213
+ } else {
214
+ out[k] = prepareForEditMode(v as JxMutableNode);
215
+ }
216
+ }
217
+ }
218
+
219
+ // Mark empty elements with placeholder classes for design-mode visibility
220
+ if (out.tagName && !out.textContent && !out.innerHTML) {
221
+ const hasChildren = Array.isArray(out.children) && out.children.length > 0;
222
+ if (!hasChildren) {
223
+ const tag = out.tagName as string;
224
+ const textTags = new Set([
225
+ "p",
226
+ "h1",
227
+ "h2",
228
+ "h3",
229
+ "h4",
230
+ "h5",
231
+ "h6",
232
+ "blockquote",
233
+ "li",
234
+ "dt",
235
+ "dd",
236
+ "th",
237
+ "td",
238
+ "span",
239
+ "strong",
240
+ "em",
241
+ "small",
242
+ "mark",
243
+ "code",
244
+ "abbr",
245
+ "q",
246
+ "sub",
247
+ "sup",
248
+ "time",
249
+ "a",
250
+ "button",
251
+ "label",
252
+ "legend",
253
+ "caption",
254
+ "summary",
255
+ "pre",
256
+ "option",
257
+ ]);
258
+ const containerTags = new Set([
259
+ "div",
260
+ "section",
261
+ "article",
262
+ "aside",
263
+ "header",
264
+ "footer",
265
+ "main",
266
+ "nav",
267
+ "figure",
268
+ "figcaption",
269
+ "details",
270
+ "fieldset",
271
+ "form",
272
+ "ul",
273
+ "ol",
274
+ "dl",
275
+ "table",
276
+ ]);
277
+ if (textTags.has(tag)) {
278
+ out.className = out.className
279
+ ? out.className + " empty-text-placeholder"
280
+ : "empty-text-placeholder";
281
+ } else if (containerTags.has(tag)) {
282
+ out.className = out.className
283
+ ? out.className + " empty-container-placeholder"
284
+ : "empty-container-placeholder";
285
+ }
286
+ }
287
+ }
288
+
289
+ // Media elements with missing/dynamic src get a placeholder class
290
+ if (needsMediaPlaceholder) {
291
+ const cls = (out.className as string) || "";
292
+ if (!cls.includes("empty-media-placeholder")) {
293
+ out.className = cls ? cls + " empty-media-placeholder" : "empty-media-placeholder";
294
+ }
295
+ }
296
+
297
+ return out;
298
+ }
@@ -1,5 +1,7 @@
1
1
  /** Google Fonts helpers — shared between head-panel (per-page) and head-editor (project-level). */
2
2
 
3
+ import type { JxHeadEntry } from "@jxsuite/schema/types";
4
+
3
5
  export const GFONTS_CSS_PREFIX = "https://fonts.googleapis.com/css2?";
4
6
  export const GFONTS_PRECONNECT_ORIGINS = [
5
7
  "https://fonts.googleapis.com",
@@ -12,12 +14,12 @@ export const GFONTS_PRECONNECT_ORIGINS = [
12
14
  * @param {JxHeadEntry} entry
13
15
  * @returns {boolean}
14
16
  */
15
- export function isGoogleFontEntry(entry) {
17
+ export function isGoogleFontEntry(entry: JxHeadEntry) {
16
18
  return (
17
19
  entry?.tagName === "link" &&
18
20
  entry?.attributes?.rel === "stylesheet" &&
19
21
  typeof entry?.attributes?.href === "string" &&
20
- /** @type {string} */ (entry.attributes.href).startsWith(GFONTS_CSS_PREFIX)
22
+ (entry.attributes.href as string).startsWith(GFONTS_CSS_PREFIX)
21
23
  );
22
24
  }
23
25
 
@@ -27,11 +29,11 @@ export function isGoogleFontEntry(entry) {
27
29
  * @param {JxHeadEntry} entry
28
30
  * @returns {boolean}
29
31
  */
30
- export function isGoogleFontPreconnect(entry) {
32
+ export function isGoogleFontPreconnect(entry: JxHeadEntry) {
31
33
  return (
32
34
  entry?.tagName === "link" &&
33
35
  entry?.attributes?.rel === "preconnect" &&
34
- GFONTS_PRECONNECT_ORIGINS.includes(/** @type {string} */ (entry?.attributes?.href))
36
+ GFONTS_PRECONNECT_ORIGINS.includes(entry?.attributes?.href as string)
35
37
  );
36
38
  }
37
39
 
@@ -41,7 +43,7 @@ export function isGoogleFontPreconnect(entry) {
41
43
  * @param {string} href
42
44
  * @returns {string}
43
45
  */
44
- export function extractFontFamily(href) {
46
+ export function extractFontFamily(href: string) {
45
47
  const match = href.match(/family=([^&:]+)/);
46
48
  if (!match) return "";
47
49
  return decodeURIComponent(match[1].replace(/\+/g, " "));
@@ -53,7 +55,7 @@ export function extractFontFamily(href) {
53
55
  * @param {string} family
54
56
  * @returns {string}
55
57
  */
56
- export function buildGoogleFontUrl(family) {
58
+ export function buildGoogleFontUrl(family: string) {
57
59
  return `${GFONTS_CSS_PREFIX}family=${encodeURIComponent(family).replace(/%20/g, "+")}&display=swap`;
58
60
  }
59
61
 
@@ -62,17 +64,16 @@ export function buildGoogleFontUrl(family) {
62
64
  *
63
65
  * @param {JxHeadEntry[]} head
64
66
  */
65
- export function ensureGoogleFontPreconnects(head) {
67
+ export function ensureGoogleFontPreconnects(head: JxHeadEntry[]) {
66
68
  for (const origin of GFONTS_PRECONNECT_ORIGINS) {
67
69
  const exists = head.some(
68
- (/** @type {JxHeadEntry} */ e) =>
70
+ (e: JxHeadEntry) =>
69
71
  e?.tagName === "link" &&
70
72
  e?.attributes?.rel === "preconnect" &&
71
73
  e?.attributes?.href === origin,
72
74
  );
73
75
  if (!exists) {
74
- /** @type {Record<string, string | boolean>} */
75
- const attrs = { rel: "preconnect", href: origin };
76
+ const attrs: Record<string, string | boolean> = { rel: "preconnect", href: origin };
76
77
  if (origin === "https://fonts.gstatic.com") attrs.crossorigin = "";
77
78
  head.push({ tagName: "link", attributes: attrs });
78
79
  }
@@ -85,10 +86,10 @@ export function ensureGoogleFontPreconnects(head) {
85
86
  * @param {JxHeadEntry[]} head
86
87
  * @returns {JxHeadEntry[]}
87
88
  */
88
- export function cleanupGoogleFontPreconnects(head) {
89
- const hasFont = head.some((/** @type {JxHeadEntry} */ e) => isGoogleFontEntry(e));
89
+ export function cleanupGoogleFontPreconnects(head: JxHeadEntry[]) {
90
+ const hasFont = head.some((e: JxHeadEntry) => isGoogleFontEntry(e));
90
91
  if (!hasFont) {
91
- return head.filter((/** @type {JxHeadEntry} */ e) => !isGoogleFontPreconnect(e));
92
+ return head.filter((e: JxHeadEntry) => !isGoogleFontPreconnect(e));
92
93
  }
93
94
  return head;
94
95
  }
@@ -6,6 +6,8 @@
6
6
  * on the current tab.
7
7
  */
8
8
 
9
+ import type { JxStyle } from "@jxsuite/schema/types";
10
+
9
11
  /**
10
12
  * Compute the inherited style object for a given breakpoint tab.
11
13
  *
@@ -16,35 +18,38 @@
16
18
  * @param {string | null} activeSelector — current nested selector, or null
17
19
  * @returns {Record<string, string | number>} Inherited style map (prop → value)
18
20
  */
19
- export function computeInheritedStyle(style, mediaNames, activeTab, activeSelector = null) {
21
+ export function computeInheritedStyle(
22
+ style: JxStyle,
23
+ mediaNames: string[],
24
+ activeTab: string | null,
25
+ activeSelector: string | null = null,
26
+ ) {
20
27
  if (activeTab === null || mediaNames.length === 0) return {};
21
28
 
22
- /** @type {Record<string, string | number>} */
23
- let inherited = {};
29
+ let inherited: Record<string, string | number> = {};
24
30
 
25
31
  if (!activeSelector) {
26
32
  // Start with base flat props
27
33
  for (const [p, v] of Object.entries(style)) {
28
- if (typeof v !== "object") inherited[p] = v ?? "";
34
+ if (typeof v !== "object") inherited[p] = (v as string | number) ?? "";
29
35
  }
30
36
  // Layer each media block in order until current tab
31
37
  for (const name of mediaNames) {
32
38
  if (name === activeTab) break;
33
- const block = style[`@${name}`] || {};
39
+ const block = (style[`@${name}`] || {}) as JxStyle;
34
40
  for (const [p, v] of Object.entries(block)) {
35
- if (typeof v !== "object") inherited[p] = v ?? "";
41
+ if (typeof v !== "object") inherited[p] = (v as string | number) ?? "";
36
42
  }
37
43
  }
38
44
  } else {
39
45
  // Selector inheritance: base selector → each media's selector block in order
40
- const baseSel = style[activeSelector] || {};
46
+ const baseSel = (style[activeSelector] || {}) as JxStyle;
41
47
  for (const [p, v] of Object.entries(baseSel)) {
42
- if (typeof v !== "object") inherited[p] = v ?? "";
48
+ if (typeof v !== "object") inherited[p] = (v as string | number) ?? "";
43
49
  }
44
50
  for (const name of mediaNames) {
45
51
  if (name === activeTab) break;
46
- const selBlock =
47
- /** @type {Record<string, unknown>} */ (style[`@${name}`] || {})[activeSelector] || {};
52
+ const selBlock = ((style[`@${name}`] || {}) as Record<string, unknown>)[activeSelector] || {};
48
53
  for (const [p, v] of Object.entries(selBlock)) {
49
54
  if (typeof v !== "object") inherited[p] = v ?? "";
50
55
  }
@@ -4,14 +4,16 @@
4
4
  * These are all side-effect-free functions used by style/properties/events panels.
5
5
  */
6
6
 
7
+ import type { ProjectConfig, ContentTypeDef } from "@jxsuite/schema/types";
8
+
7
9
  /**
8
10
  * CamelCase → kebab-case for inline style attributes
9
11
  *
10
12
  * @param {string} str
11
13
  * @returns {string}
12
14
  */
13
- export function camelToKebab(str) {
14
- return str.replace(/[A-Z]/g, (/** @type {string} */ c) => "-" + c.toLowerCase());
15
+ export function camelToKebab(str: string) {
16
+ return str.replace(/[A-Z]/g, (c: string) => "-" + c.toLowerCase());
15
17
  }
16
18
 
17
19
  /**
@@ -21,10 +23,14 @@ export function camelToKebab(str) {
21
23
  * @param {string} prop
22
24
  * @returns {string}
23
25
  */
24
- export function camelToLabel(prop) {
25
- return prop
26
- .replace(/([A-Z])/g, " $1")
27
- .replace(/^./, (/** @type {string} */ c) => c.toUpperCase());
26
+ export function camelToLabel(prop: string) {
27
+ return prop.replace(/([A-Z])/g, " $1").replace(/^./, (c: string) => c.toUpperCase());
28
+ }
29
+
30
+ export function toCamelCase(str: string): string {
31
+ return str
32
+ .replace(/[^a-zA-Z0-9]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
33
+ .replace(/^[A-Z]/, (c) => c.toLowerCase());
28
34
  }
29
35
 
30
36
  /**
@@ -34,11 +40,10 @@ export function camelToLabel(prop) {
34
40
  * @param {string} val
35
41
  * @returns {string}
36
42
  */
37
- export function kebabToLabel(val) {
43
+ export function kebabToLabel(val: string) {
38
44
  return val.replace(
39
45
  /(^|-)(\w)/g,
40
- (/** @type {string} */ _, /** @type {string} */ sep, /** @type {string} */ c) =>
41
- (sep ? " " : "") + c.toUpperCase(),
46
+ (_: string, sep: string, c: string) => (sep ? " " : "") + c.toUpperCase(),
42
47
  );
43
48
  }
44
49
 
@@ -49,7 +54,10 @@ export function kebabToLabel(val) {
49
54
  * @param {string} prop
50
55
  * @returns {string}
51
56
  */
52
- export function propLabel(entry, prop) {
57
+ export function propLabel(
58
+ entry: { $label?: string; [key: string]: unknown } | null | undefined,
59
+ prop: string,
60
+ ) {
53
61
  return entry?.$label || camelToLabel(prop);
54
62
  }
55
63
 
@@ -60,13 +68,15 @@ export function propLabel(entry, prop) {
60
68
  * @param {string} attr
61
69
  * @returns {string}
62
70
  */
63
- export function attrLabel(entry, attr) {
71
+ export function attrLabel(
72
+ entry: { $label?: string; [key: string]: unknown } | null | undefined,
73
+ attr: string,
74
+ ) {
64
75
  if (entry?.$label) return entry.$label;
65
76
  if (attr.includes("-"))
66
77
  return attr.replace(
67
78
  /(^|-)(\w)/g,
68
- (/** @type {string} */ _, /** @type {string} */ sep, /** @type {string} */ c) =>
69
- (sep ? " " : "") + c.toUpperCase(),
79
+ (_: string, sep: string, c: string) => (sep ? " " : "") + c.toUpperCase(),
70
80
  );
71
81
  return camelToLabel(attr);
72
82
  }
@@ -77,9 +87,8 @@ export function attrLabel(entry, attr) {
77
87
  * @param {string} val
78
88
  * @returns {string}
79
89
  */
80
- export function abbreviateValue(val) {
81
- /** @type {Record<string, string>} */
82
- const map = {
90
+ export function abbreviateValue(val: string) {
91
+ const map: Record<string, string> = {
83
92
  inline: "inl",
84
93
  "inline-block": "i-blk",
85
94
  "inline-flex": "i-flx",
@@ -109,7 +118,7 @@ export function abbreviateValue(val) {
109
118
  * @param {Record<string, unknown>} entry
110
119
  * @returns {string}
111
120
  */
112
- export function inferInputType(entry) {
121
+ export function inferInputType(entry: Record<string, unknown>) {
113
122
  if (entry.$shorthand === true) return "shorthand";
114
123
  if (entry.$input === "button-group") return "button-group";
115
124
  if (entry.$input === "media") return "media";
@@ -130,10 +139,13 @@ export function inferInputType(entry) {
130
139
  * @param {ProjectConfig | null | undefined} projectConfig — parsed project.json
131
140
  * @returns {{ name: string; schema: ContentTypeSchema } | null}
132
141
  */
133
- export function findContentTypeSchema(documentPath, projectConfig) {
142
+ export function findContentTypeSchema(
143
+ documentPath: string | null,
144
+ projectConfig: ProjectConfig | null | undefined,
145
+ ) {
134
146
  if (!documentPath || !projectConfig?.contentTypes) return null;
135
147
  for (const [name, def] of Object.entries(
136
- /** @type {Record<string, ContentTypeDef>} */ (projectConfig.contentTypes),
148
+ projectConfig.contentTypes as Record<string, ContentTypeDef>,
137
149
  )) {
138
150
  if (!def.source || !def.schema) continue;
139
151
  const src = def.source.replace(/^\.\//, "").replace(/\/$/, "");
@@ -160,7 +172,7 @@ export function findContentTypeSchema(documentPath, projectConfig) {
160
172
  * @param {string} prefix - E.g. "--font-"
161
173
  * @returns {string}
162
174
  */
163
- export function friendlyNameToVar(name, prefix) {
175
+ export function friendlyNameToVar(name: string, prefix: string) {
164
176
  const slug = name
165
177
  .trim()
166
178
  .toLowerCase()
@@ -180,13 +192,13 @@ export function friendlyNameToVar(name, prefix) {
180
192
  * @param {string} prefix
181
193
  * @returns {string}
182
194
  */
183
- export function varDisplayName(varName, prefix) {
195
+ export function varDisplayName(varName: string, prefix: string) {
184
196
  return (
185
197
  varName
186
198
  .replace(new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`), "")
187
199
  .replace(/^--/, "")
188
200
  .replace(/-/g, " ")
189
- .replace(/\b\w/g, (/** @type {string} */ c) => c.toUpperCase()) || varName
201
+ .replace(/\b\w/g, (c: string) => c.toUpperCase()) || varName
190
202
  );
191
203
  }
192
204
 
@@ -199,7 +211,7 @@ export function varDisplayName(varName, prefix) {
199
211
  * | { kind: "number" }
200
212
  * | { kind: "text" }}
201
213
  */
202
- export function parseCemType(typeText) {
214
+ export function parseCemType(typeText: string | undefined | null) {
203
215
  if (!typeText) return { kind: "text" };
204
216
  const t = typeText
205
217
  .trim()