@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.
- package/dist/studio.js +2311 -718
- package/dist/studio.js.map +121 -104
- package/package.json +8 -7
- package/src/browse/{browse-modal.js → browse-modal.ts} +9 -10
- package/src/browse/{browse.js → browse.ts} +109 -84
- package/src/canvas/{canvas-diff.js → canvas-diff.ts} +42 -35
- package/src/canvas/{canvas-helpers.js → canvas-helpers.ts} +25 -19
- package/src/canvas/{canvas-live-render.js → canvas-live-render.ts} +53 -63
- package/src/canvas/{canvas-render.js → canvas-render.ts} +110 -121
- package/src/canvas/{canvas-utils.js → canvas-utils.ts} +48 -44
- package/src/canvas/{nested-site-style.js → nested-site-style.ts} +3 -3
- package/src/editor/{component-inline-edit.js → component-inline-edit.ts} +45 -33
- package/src/editor/{content-inline-edit.js → content-inline-edit.ts} +27 -45
- package/src/editor/{context-menu.js → context-menu.ts} +38 -36
- package/src/editor/{convert-targets.js → convert-targets.ts} +15 -12
- package/src/editor/{convert-to-component.js → convert-to-component.ts} +25 -26
- package/src/editor/{convert-to-repeater.js → convert-to-repeater.ts} +60 -40
- package/src/editor/{inline-edit.js → inline-edit.ts} +91 -55
- package/src/editor/{inline-format.js → inline-format.ts} +46 -51
- package/src/editor/{insertion-helper.js → insertion-helper.ts} +47 -63
- package/src/editor/{shortcuts.js → shortcuts.ts} +45 -31
- package/src/editor/{slash-menu.js → slash-menu.ts} +34 -35
- package/src/files/{components.js → components.ts} +12 -15
- package/src/files/{file-ops.js → file-ops.ts} +36 -29
- package/src/files/{files.js → files.ts} +102 -84
- package/src/github/{github-auth.js → github-auth.ts} +3 -3
- package/src/github/{github-publish.js → github-publish.ts} +20 -18
- package/src/markdown/{md-allowlist.js → md-allowlist.ts} +5 -2
- package/src/markdown/{md-convert.js → md-convert.ts} +146 -169
- package/src/new-project/{new-project-modal.js → new-project-modal.ts} +16 -18
- package/src/panels/{activity-bar.js → activity-bar.ts} +26 -34
- package/src/panels/{ai-panel.js → ai-panel.ts} +30 -30
- package/src/panels/{block-action-bar.js → block-action-bar.ts} +64 -73
- package/src/panels/{canvas-dnd.js → canvas-dnd.ts} +44 -49
- package/src/panels/{data-explorer.js → data-explorer.ts} +21 -15
- package/src/panels/dnd.ts +417 -0
- package/src/panels/{editors.js → editors.ts} +43 -50
- package/src/panels/{elements-panel.js → elements-panel.ts} +30 -27
- package/src/panels/{events-panel.js → events-panel.ts} +33 -25
- package/src/panels/{git-panel.js → git-panel.ts} +97 -83
- package/src/panels/{head-panel.js → head-panel.ts} +117 -139
- package/src/panels/{imports-panel.js → imports-panel.ts} +69 -89
- package/src/panels/{layers-panel.js → layers-panel.ts} +61 -57
- package/src/panels/{left-panel.js → left-panel.ts} +56 -63
- package/src/panels/{overlays.js → overlays.ts} +28 -36
- package/src/panels/{panel-events.js → panel-events.ts} +53 -51
- package/src/panels/{preview-render.js → preview-render.ts} +20 -15
- package/src/panels/{properties-panel.js → properties-panel.ts} +212 -246
- package/src/panels/{pseudo-preview.js → pseudo-preview.ts} +9 -8
- package/src/panels/{quick-search.js → quick-search.ts} +16 -17
- package/src/panels/{right-panel.js → right-panel.ts} +41 -59
- package/src/panels/{shared.js → shared.ts} +8 -6
- package/src/panels/{signals-panel.js → signals-panel.ts} +322 -362
- package/src/panels/{statusbar.js → statusbar.ts} +14 -14
- package/src/panels/{style-inputs.js → style-inputs.ts} +66 -48
- package/src/panels/{style-panel.js → style-panel.ts} +235 -290
- package/src/panels/{style-utils.js → style-utils.ts} +32 -31
- package/src/panels/{stylebook-layers-panel.js → stylebook-layers-panel.ts} +22 -14
- package/src/panels/{stylebook-panel.js → stylebook-panel.ts} +127 -140
- package/src/panels/{tab-strip.js → tab-strip.ts} +12 -14
- package/src/panels/{toolbar.js → toolbar.ts} +68 -70
- package/src/panels/{welcome-screen.js → welcome-screen.ts} +14 -16
- package/src/{platform.js → platform.ts} +5 -3
- package/src/platforms/{devserver.js → devserver.ts} +53 -44
- package/src/{recent-projects.js → recent-projects.ts} +17 -12
- package/src/{resize-edges.js → resize-edges.ts} +18 -13
- package/src/services/{cem-export.js → cem-export.ts} +14 -7
- package/src/services/{code-services.js → code-services.ts} +29 -20
- package/src/services/{monaco-setup.js → monaco-setup.ts} +2 -3
- package/src/settings/{content-types-editor.js → content-types-editor.ts} +88 -67
- package/src/settings/{css-vars-editor.js → css-vars-editor.ts} +78 -51
- package/src/settings/{defs-editor.js → defs-editor.ts} +70 -56
- package/src/settings/{general-settings.js → general-settings.ts} +29 -28
- package/src/settings/{head-editor.js → head-editor.ts} +37 -35
- package/src/settings/{schema-field-ui.js → schema-field-ui.ts} +86 -76
- package/src/settings/{settings-modal.js → settings-modal.ts} +14 -16
- package/src/{site-context.js → site-context.ts} +35 -32
- package/src/{state.js → state.ts} +95 -78
- package/src/{store.js → store.ts} +60 -63
- package/src/{studio.js → studio.ts} +117 -119
- package/src/tabs/tab.ts +196 -0
- package/src/tabs/{transact.js → transact.ts} +107 -60
- package/src/types.ts +185 -0
- package/src/ui/{button-group.js → button-group.ts} +21 -21
- package/src/ui/{color-selector.js → color-selector.ts} +44 -37
- package/src/ui/{expression-editor.js → expression-editor.ts} +65 -40
- package/src/ui/{field-row.js → field-row.ts} +19 -2
- package/src/ui/{icons.js → icons.ts} +2 -2
- package/src/ui/{layers.js → layers.ts} +33 -24
- package/src/ui/{media-picker.js → media-picker.ts} +13 -9
- package/src/ui/{panel-resize.js → panel-resize.ts} +9 -4
- package/src/ui/{spectrum.js → spectrum.ts} +4 -4
- package/src/ui/{unit-selector.js → unit-selector.ts} +16 -19
- package/src/ui/{value-selector.js → value-selector.ts} +28 -28
- package/src/ui/{widgets.js → widgets.ts} +62 -15
- package/src/utils/{canvas-media.js → canvas-media.ts} +29 -14
- package/src/utils/edit-display.ts +298 -0
- package/src/utils/{google-fonts.js → google-fonts.ts} +14 -13
- package/src/utils/{inherited-style.js → inherited-style.ts} +15 -10
- package/src/utils/{studio-utils.js → studio-utils.ts} +35 -23
- package/src/{view.js → view.ts} +53 -55
- package/src/workspace/{workspace.js → workspace.ts} +44 -33
- package/src/panels/dnd.js +0 -426
- package/src/tabs/tab.js +0 -192
- package/src/utils/edit-display.js +0 -203
- /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
|
-
|
|
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(
|
|
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
|
-
(
|
|
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
|
-
|
|
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((
|
|
89
|
+
export function cleanupGoogleFontPreconnects(head: JxHeadEntry[]) {
|
|
90
|
+
const hasFont = head.some((e: JxHeadEntry) => isGoogleFontEntry(e));
|
|
90
91
|
if (!hasFont) {
|
|
91
|
-
return head.filter((
|
|
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(
|
|
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
|
-
|
|
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, (
|
|
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
|
-
|
|
27
|
-
|
|
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
|
-
(
|
|
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(
|
|
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(
|
|
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
|
-
(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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, (
|
|
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()
|