@jxsuite/studio 0.28.2 → 0.28.4
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 +86144 -83971
- package/dist/studio.js.map +229 -219
- package/package.json +5 -5
- package/src/browse/browse-modal.ts +9 -3
- package/src/browse/browse.ts +139 -82
- package/src/canvas/canvas-diff.ts +35 -18
- package/src/canvas/canvas-helpers.ts +55 -21
- package/src/canvas/canvas-live-render.ts +140 -89
- package/src/canvas/canvas-render.ts +127 -82
- package/src/canvas/canvas-utils.ts +68 -42
- package/src/canvas/nested-site-style.ts +15 -7
- package/src/editor/component-inline-edit.ts +90 -51
- package/src/editor/content-inline-edit.ts +75 -72
- package/src/editor/context-menu.ts +125 -71
- package/src/editor/convert-targets.ts +17 -13
- package/src/editor/convert-to-component.ts +51 -28
- package/src/editor/convert-to-repeater.ts +44 -19
- package/src/editor/inline-edit.ts +107 -52
- package/src/editor/inline-format.ts +128 -60
- package/src/editor/insertion-helper.ts +26 -14
- package/src/editor/shortcuts.ts +89 -43
- package/src/editor/slash-menu.ts +41 -26
- package/src/files/components.ts +9 -7
- package/src/files/file-ops.ts +53 -33
- package/src/files/files.ts +246 -143
- package/src/format/constraints.ts +25 -15
- package/src/format/format-host.ts +33 -10
- package/src/github/github-auth.ts +24 -14
- package/src/github/github-publish.ts +20 -14
- package/src/new-project/new-project-modal.ts +27 -18
- package/src/panels/activity-bar.ts +27 -26
- package/src/panels/ai-panel.ts +100 -44
- package/src/panels/block-action-bar.ts +84 -52
- package/src/panels/canvas-dnd.ts +47 -28
- package/src/panels/data-explorer.ts +24 -11
- package/src/panels/dnd.ts +148 -100
- package/src/panels/editors.ts +54 -37
- package/src/panels/elements-panel.ts +23 -13
- package/src/panels/events-panel.ts +48 -38
- package/src/panels/git-panel.ts +95 -60
- package/src/panels/head-panel.ts +138 -84
- package/src/panels/imports-panel.ts +66 -29
- package/src/panels/layers-panel.ts +80 -36
- package/src/panels/left-panel.ts +86 -53
- package/src/panels/overlays.ts +36 -19
- package/src/panels/panel-events.ts +48 -27
- package/src/panels/panel-scheduler.ts +41 -23
- package/src/panels/preview-render.ts +26 -26
- package/src/panels/properties-panel.ts +155 -118
- package/src/panels/pseudo-preview.ts +31 -15
- package/src/panels/quick-search.ts +28 -13
- package/src/panels/right-panel.ts +35 -24
- package/src/panels/shared.ts +46 -29
- package/src/panels/signals-panel.ts +303 -186
- package/src/panels/statusbar.ts +27 -12
- package/src/panels/style-inputs.ts +30 -24
- package/src/panels/style-panel.ts +130 -90
- package/src/panels/style-utils.ts +63 -23
- package/src/panels/stylebook-layers-panel.ts +27 -30
- package/src/panels/stylebook-panel.ts +140 -72
- package/src/panels/tab-strip.ts +21 -10
- package/src/panels/toolbar.ts +52 -40
- package/src/panels/welcome-screen.ts +1 -1
- package/src/platform.ts +2 -1
- package/src/platforms/devserver.ts +221 -105
- package/src/recent-projects.ts +15 -7
- package/src/resize-edges.ts +7 -5
- package/src/services/cem-export.ts +73 -57
- package/src/services/code-services.ts +52 -25
- package/src/services/monaco-setup.ts +5 -5
- package/src/settings/content-types-editor.ts +133 -62
- package/src/settings/css-vars-editor.ts +31 -14
- package/src/settings/defs-editor.ts +125 -63
- package/src/settings/general-settings.ts +11 -6
- package/src/settings/head-editor.ts +30 -15
- package/src/settings/schema-field-ui.ts +75 -35
- package/src/settings/settings-modal.ts +35 -17
- package/src/site-context.ts +80 -47
- package/src/state.ts +98 -65
- package/src/store.ts +42 -23
- package/src/studio.ts +165 -144
- package/src/tabs/tab.ts +51 -45
- package/src/tabs/transact.ts +230 -109
- package/src/types.ts +60 -51
- package/src/ui/button-group.ts +3 -2
- package/src/ui/color-selector.ts +33 -15
- package/src/ui/expression-editor.ts +139 -86
- package/src/ui/field-input.ts +35 -18
- package/src/ui/field-row.ts +1 -1
- package/src/ui/icons.ts +5 -6
- package/src/ui/layers.ts +33 -26
- package/src/ui/media-picker.ts +13 -7
- package/src/ui/panel-resize.ts +39 -20
- package/src/ui/spectrum.ts +4 -2
- package/src/ui/unit-selector.ts +20 -11
- package/src/ui/value-selector.ts +13 -7
- package/src/ui/widgets.ts +34 -18
- package/src/utils/canvas-media.ts +66 -33
- package/src/utils/edit-display.ts +43 -44
- package/src/utils/google-fonts.ts +10 -6
- package/src/utils/inherited-style.ts +22 -8
- package/src/utils/studio-utils.ts +88 -45
- package/src/view.ts +9 -5
- package/src/workspace/workspace.ts +49 -29
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
+
/// <reference lib="dom.iterable" />
|
|
2
3
|
/** Canvas media/breakpoint utilities — pure functions extracted for testability. */
|
|
3
4
|
|
|
4
5
|
import type { JxStyle } from "@jxsuite/schema/types";
|
|
@@ -14,25 +15,31 @@ import type { JxStyle } from "@jxsuite/schema/types";
|
|
|
14
15
|
* baseWidth: number;
|
|
15
16
|
* }}
|
|
16
17
|
*/
|
|
17
|
-
export function parseMediaEntries(mediaDef
|
|
18
|
-
if (!mediaDef)
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
export function parseMediaEntries(mediaDef?: Record<string, string> | null) {
|
|
19
|
+
if (!mediaDef) {
|
|
20
|
+
return { baseWidth: 320, featureQueries: [], sizeBreakpoints: [] };
|
|
21
|
+
}
|
|
22
|
+
const features = [];
|
|
23
|
+
const sizes = [];
|
|
21
24
|
let baseWidth = 320;
|
|
22
25
|
for (const [name, query] of Object.entries(mediaDef)) {
|
|
23
26
|
if (name === "--") {
|
|
24
27
|
const wm = String(query).match(/^(\d+)\s*px$/);
|
|
25
|
-
baseWidth = wm ? parseFloat(wm[1]) : 320;
|
|
28
|
+
baseWidth = wm ? Number.parseFloat(wm[1]) : 320;
|
|
26
29
|
continue;
|
|
27
30
|
}
|
|
28
31
|
const minMatch = query.match(/min-width:\s*([\d.]+)px/);
|
|
29
32
|
const maxMatch = query.match(/max-width:\s*([\d.]+)px/);
|
|
30
|
-
if (minMatch)
|
|
31
|
-
|
|
32
|
-
else
|
|
33
|
+
if (minMatch) {
|
|
34
|
+
sizes.push({ name, query, type: "min", width: Number.parseFloat(minMatch[1]) });
|
|
35
|
+
} else if (maxMatch) {
|
|
36
|
+
sizes.push({ name, query, type: "max", width: Number.parseFloat(maxMatch[1]) });
|
|
37
|
+
} else {
|
|
38
|
+
features.push({ name, query });
|
|
39
|
+
}
|
|
33
40
|
}
|
|
34
41
|
sizes.sort((a, b) => (a.type === "min" ? a.width - b.width : b.width - a.width));
|
|
35
|
-
return {
|
|
42
|
+
return { baseWidth, featureQueries: features, sizeBreakpoints: sizes };
|
|
36
43
|
}
|
|
37
44
|
|
|
38
45
|
/**
|
|
@@ -48,8 +55,11 @@ export function activeBreakpointsForWidth(
|
|
|
48
55
|
) {
|
|
49
56
|
const active = new Set<string>();
|
|
50
57
|
for (const bp of sizeBreakpoints) {
|
|
51
|
-
if (bp.type === "min" && canvasWidth >= bp.width)
|
|
52
|
-
|
|
58
|
+
if (bp.type === "min" && canvasWidth >= bp.width) {
|
|
59
|
+
active.add(bp.name);
|
|
60
|
+
} else if (bp.type === "max" && canvasWidth <= bp.width) {
|
|
61
|
+
active.add(bp.name);
|
|
62
|
+
}
|
|
53
63
|
}
|
|
54
64
|
return active;
|
|
55
65
|
}
|
|
@@ -69,25 +79,37 @@ export function applyCanvasStyle(
|
|
|
69
79
|
activeBreakpoints: Set<string>,
|
|
70
80
|
featureToggles: Record<string, boolean>,
|
|
71
81
|
) {
|
|
72
|
-
if (!styleDef || typeof styleDef !== "object")
|
|
82
|
+
if (!styleDef || typeof styleDef !== "object") {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
73
85
|
for (const [prop, val] of Object.entries(styleDef)) {
|
|
74
86
|
if (typeof val === "string" || typeof val === "number") {
|
|
75
87
|
try {
|
|
76
|
-
if (prop.startsWith("--"))
|
|
77
|
-
|
|
88
|
+
if (prop.startsWith("--")) {
|
|
89
|
+
el.style.setProperty(prop, String(val));
|
|
90
|
+
} else {
|
|
91
|
+
(el.style as unknown as Record<string, unknown>)[prop] = val;
|
|
92
|
+
}
|
|
78
93
|
} catch {}
|
|
79
94
|
}
|
|
80
95
|
}
|
|
81
96
|
for (const [key, val] of Object.entries(styleDef)) {
|
|
82
|
-
if (!key.startsWith("@") || typeof val !== "object" || val === null)
|
|
97
|
+
if (!key.startsWith("@") || typeof val !== "object" || val === null) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
83
100
|
const mediaName = key.slice(1);
|
|
84
|
-
if (mediaName === "--")
|
|
101
|
+
if (mediaName === "--") {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
85
104
|
if (activeBreakpoints.has(mediaName) || featureToggles[mediaName]) {
|
|
86
105
|
for (const [prop, v] of Object.entries(/** @type {Record<string, unknown>} */ val)) {
|
|
87
106
|
if (typeof v === "string" || typeof v === "number") {
|
|
88
107
|
try {
|
|
89
|
-
if (prop.startsWith("--"))
|
|
90
|
-
|
|
108
|
+
if (prop.startsWith("--")) {
|
|
109
|
+
el.style.setProperty(prop, String(v));
|
|
110
|
+
} else {
|
|
111
|
+
(el.style as unknown as Record<string, unknown>)[prop] = v;
|
|
112
|
+
}
|
|
91
113
|
} catch {}
|
|
92
114
|
}
|
|
93
115
|
}
|
|
@@ -109,8 +131,10 @@ export function collectMediaOverrides(
|
|
|
109
131
|
styleSheets: Iterable<CSSStyleSheet>,
|
|
110
132
|
activeBreakpoints: Set<string>,
|
|
111
133
|
) {
|
|
112
|
-
const overrides
|
|
113
|
-
if (
|
|
134
|
+
const overrides = new Map<string, Map<string, string>>();
|
|
135
|
+
if (activeBreakpoints.size === 0) {
|
|
136
|
+
return overrides;
|
|
137
|
+
}
|
|
114
138
|
|
|
115
139
|
for (const sheet of styleSheets) {
|
|
116
140
|
/** @type {CSSRuleList | null} */
|
|
@@ -120,22 +144,31 @@ export function collectMediaOverrides(
|
|
|
120
144
|
} catch {
|
|
121
145
|
continue;
|
|
122
146
|
}
|
|
123
|
-
if (!rules)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (!
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
if (!rules) {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const rule of rules) {
|
|
151
|
+
if (!(rule instanceof CSSMediaRule)) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!activeBreakpoints.has(rule.conditionText)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
for (const mediaRule of rule.cssRules) {
|
|
158
|
+
if (!(mediaRule instanceof CSSStyleRule)) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
131
161
|
const selector = mediaRule.selectorText;
|
|
132
162
|
const jxMatch = selector.match(/\[data-jx="([^"]+)"\]/);
|
|
133
|
-
if (!jxMatch)
|
|
134
|
-
|
|
135
|
-
|
|
163
|
+
if (!jxMatch) {
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const [, uid] = jxMatch;
|
|
167
|
+
if (!overrides.has(uid)) {
|
|
168
|
+
overrides.set(uid, new Map());
|
|
169
|
+
}
|
|
136
170
|
const props = overrides.get(uid) as Map<string, string>;
|
|
137
|
-
for (
|
|
138
|
-
const prop = mediaRule.style[i];
|
|
171
|
+
for (const prop of mediaRule.style) {
|
|
139
172
|
props.set(prop, mediaRule.style.getPropertyValue(prop));
|
|
140
173
|
}
|
|
141
174
|
}
|
|
@@ -18,7 +18,7 @@ const TRANSPARENT_PX =
|
|
|
18
18
|
* @param {string} str
|
|
19
19
|
*/
|
|
20
20
|
export function templateToEditDisplay(str: string) {
|
|
21
|
-
return str.
|
|
21
|
+
return str.replaceAll(/\$\{([^}]+)\}/g, "\u276A $1 \u276B");
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
/**
|
|
@@ -32,7 +32,7 @@ export function restoreTemplateExpressions(el: HTMLElement) {
|
|
|
32
32
|
while (walker.nextNode()) {
|
|
33
33
|
const node = walker.currentNode as Text;
|
|
34
34
|
if (node.data.includes("\u276A")) {
|
|
35
|
-
node.data = node.data.
|
|
35
|
+
node.data = node.data.replaceAll(/\u276A\s*(.*?)\s*\u276B/g, "${$1}");
|
|
36
36
|
}
|
|
37
37
|
}
|
|
38
38
|
}
|
|
@@ -46,8 +46,13 @@ export function restoreTemplateExpressions(el: HTMLElement) {
|
|
|
46
46
|
* @returns {JxMutableNode}
|
|
47
47
|
*/
|
|
48
48
|
export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
49
|
-
if (!node || typeof node !== "object")
|
|
50
|
-
|
|
49
|
+
if (!node || typeof node !== "object") {
|
|
50
|
+
return node;
|
|
51
|
+
}
|
|
52
|
+
if (Array.isArray(node)) {
|
|
53
|
+
// Arrays of nodes round-trip element-wise; the array itself is not a node.
|
|
54
|
+
return node.map((n) => prepareForEditMode(n)) as unknown as JxMutableNode;
|
|
55
|
+
}
|
|
51
56
|
|
|
52
57
|
const /** @type {Record<string, unknown>} */ obj = node as Record<string, unknown>;
|
|
53
58
|
|
|
@@ -71,7 +76,7 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
|
71
76
|
|
|
72
77
|
for (const [k, v] of Object.entries(obj)) {
|
|
73
78
|
if (k === "state" || k === "$media" || k === "$elements") {
|
|
74
|
-
out[k] = v; //
|
|
79
|
+
out[k] = v; // Preserve as-is for runtime resolution
|
|
75
80
|
} else if (k === "$props" && v && typeof v === "object") {
|
|
76
81
|
// Process $props values: convert template strings to display format
|
|
77
82
|
const propsOut: Record<string, unknown> = {};
|
|
@@ -90,7 +95,7 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
|
90
95
|
out[k] = propsOut;
|
|
91
96
|
} else if (k === "children") {
|
|
92
97
|
if (Array.isArray(v)) {
|
|
93
|
-
out.children = v.map(prepareForEditMode);
|
|
98
|
+
out.children = v.map((c) => prepareForEditMode(c));
|
|
94
99
|
} else if (
|
|
95
100
|
v &&
|
|
96
101
|
typeof v === "object" &&
|
|
@@ -99,17 +104,16 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
|
99
104
|
// Wrap the map template in a visual repeater perimeter
|
|
100
105
|
const vObj = v as Record<string, unknown>;
|
|
101
106
|
const template = vObj.map;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
}
|
|
107
|
+
out.children =
|
|
108
|
+
template && typeof template === "object"
|
|
109
|
+
? [
|
|
110
|
+
{
|
|
111
|
+
children: [prepareForEditMode(template as JxMutableNode)],
|
|
112
|
+
className: "repeater-perimeter",
|
|
113
|
+
tagName: "div",
|
|
114
|
+
},
|
|
115
|
+
]
|
|
116
|
+
: [];
|
|
113
117
|
} else {
|
|
114
118
|
out.children = prepareForEditMode(v as JxMutableNode);
|
|
115
119
|
}
|
|
@@ -118,30 +122,25 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
|
118
122
|
const caseKeys = Object.keys(v);
|
|
119
123
|
if (caseKeys.length > 0) {
|
|
120
124
|
const firstCase = (v as Record<string, unknown>)[caseKeys[0]];
|
|
121
|
-
|
|
122
|
-
firstCase &&
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
fontStyle: "italic",
|
|
141
|
-
},
|
|
142
|
-
},
|
|
143
|
-
];
|
|
144
|
-
}
|
|
125
|
+
out.children =
|
|
126
|
+
firstCase && typeof firstCase === "object" && !(firstCase as Record<string, unknown>).$ref
|
|
127
|
+
? [prepareForEditMode(firstCase as JxMutableNode)]
|
|
128
|
+
: [
|
|
129
|
+
{
|
|
130
|
+
style: {
|
|
131
|
+
background: "color-mix(in srgb, var(--danger) 8%, transparent)",
|
|
132
|
+
border: "1px dashed color-mix(in srgb, var(--danger) 40%, transparent)",
|
|
133
|
+
borderRadius: "4px",
|
|
134
|
+
color: "var(--danger)",
|
|
135
|
+
fontFamily: "'SF Mono', 'Fira Code', monospace",
|
|
136
|
+
fontSize: "11px",
|
|
137
|
+
fontStyle: "italic",
|
|
138
|
+
padding: "6px 10px",
|
|
139
|
+
},
|
|
140
|
+
tagName: "div",
|
|
141
|
+
textContent: `[$switch: ${caseKeys.join(" | ")}]`,
|
|
142
|
+
},
|
|
143
|
+
];
|
|
145
144
|
}
|
|
146
145
|
} else if (k === "attributes" && isMediaElement && v && typeof v === "object") {
|
|
147
146
|
// Process attributes for media elements: replace src/poster with transparent pixel
|
|
@@ -276,11 +275,11 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
|
276
275
|
]);
|
|
277
276
|
if (textTags.has(tag)) {
|
|
278
277
|
out.className = out.className
|
|
279
|
-
? out.className
|
|
278
|
+
? `${out.className} empty-text-placeholder`
|
|
280
279
|
: "empty-text-placeholder";
|
|
281
280
|
} else if (containerTags.has(tag)) {
|
|
282
281
|
out.className = out.className
|
|
283
|
-
? out.className
|
|
282
|
+
? `${out.className} empty-container-placeholder`
|
|
284
283
|
: "empty-container-placeholder";
|
|
285
284
|
}
|
|
286
285
|
}
|
|
@@ -290,7 +289,7 @@ export function prepareForEditMode(node: JxMutableNode): JxMutableNode {
|
|
|
290
289
|
if (needsMediaPlaceholder) {
|
|
291
290
|
const cls = (out.className as string) || "";
|
|
292
291
|
if (!cls.includes("empty-media-placeholder")) {
|
|
293
|
-
out.className = cls ? cls
|
|
292
|
+
out.className = cls ? `${cls} empty-media-placeholder` : "empty-media-placeholder";
|
|
294
293
|
}
|
|
295
294
|
}
|
|
296
295
|
|
|
@@ -45,8 +45,10 @@ export function isGoogleFontPreconnect(entry: JxHeadEntry) {
|
|
|
45
45
|
*/
|
|
46
46
|
export function extractFontFamily(href: string) {
|
|
47
47
|
const match = href.match(/family=([^&:]+)/);
|
|
48
|
-
if (!match)
|
|
49
|
-
|
|
48
|
+
if (!match) {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
return decodeURIComponent(match[1].replaceAll("+", " "));
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/**
|
|
@@ -56,7 +58,7 @@ export function extractFontFamily(href: string) {
|
|
|
56
58
|
* @returns {string}
|
|
57
59
|
*/
|
|
58
60
|
export function buildGoogleFontUrl(family: string) {
|
|
59
|
-
return `${GFONTS_CSS_PREFIX}family=${encodeURIComponent(family).
|
|
61
|
+
return `${GFONTS_CSS_PREFIX}family=${encodeURIComponent(family).replaceAll("%20", "+")}&display=swap`;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
/**
|
|
@@ -74,11 +76,13 @@ export function ensureGoogleFontPreconnects(head: JxHeadEntry[]) {
|
|
|
74
76
|
);
|
|
75
77
|
if (!exists) {
|
|
76
78
|
const attrs: Record<string, string | boolean> = {
|
|
77
|
-
rel: "preconnect",
|
|
78
79
|
href: origin,
|
|
80
|
+
rel: "preconnect",
|
|
79
81
|
};
|
|
80
|
-
if (origin === "https://fonts.gstatic.com")
|
|
81
|
-
|
|
82
|
+
if (origin === "https://fonts.gstatic.com") {
|
|
83
|
+
attrs.crossorigin = "";
|
|
84
|
+
}
|
|
85
|
+
head.push({ attributes: attrs, tagName: "link" });
|
|
82
86
|
}
|
|
83
87
|
}
|
|
84
88
|
}
|
|
@@ -24,34 +24,48 @@ export function computeInheritedStyle(
|
|
|
24
24
|
activeTab: string | null,
|
|
25
25
|
activeSelector: string | null = null,
|
|
26
26
|
) {
|
|
27
|
-
if (activeTab === null || mediaNames.length === 0)
|
|
27
|
+
if (activeTab === null || mediaNames.length === 0) {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
const inherited: Record<string, string | number> = {};
|
|
30
32
|
|
|
31
33
|
if (!activeSelector) {
|
|
32
34
|
// Start with base flat props
|
|
33
35
|
for (const [p, v] of Object.entries(style)) {
|
|
34
|
-
if (typeof v !== "object")
|
|
36
|
+
if (typeof v !== "object") {
|
|
37
|
+
inherited[p] = (v as string | number) ?? "";
|
|
38
|
+
}
|
|
35
39
|
}
|
|
36
40
|
// Layer each media block in order until current tab
|
|
37
41
|
for (const name of mediaNames) {
|
|
38
|
-
if (name === activeTab)
|
|
42
|
+
if (name === activeTab) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
39
45
|
const block = (style[`@${name}`] || {}) as JxStyle;
|
|
40
46
|
for (const [p, v] of Object.entries(block)) {
|
|
41
|
-
if (typeof v !== "object")
|
|
47
|
+
if (typeof v !== "object") {
|
|
48
|
+
inherited[p] = (v as string | number) ?? "";
|
|
49
|
+
}
|
|
42
50
|
}
|
|
43
51
|
}
|
|
44
52
|
} else {
|
|
45
53
|
// Selector inheritance: base selector → each media's selector block in order
|
|
46
54
|
const baseSel = (style[activeSelector] || {}) as JxStyle;
|
|
47
55
|
for (const [p, v] of Object.entries(baseSel)) {
|
|
48
|
-
if (typeof v !== "object")
|
|
56
|
+
if (typeof v !== "object") {
|
|
57
|
+
inherited[p] = (v as string | number) ?? "";
|
|
58
|
+
}
|
|
49
59
|
}
|
|
50
60
|
for (const name of mediaNames) {
|
|
51
|
-
if (name === activeTab)
|
|
61
|
+
if (name === activeTab) {
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
52
64
|
const selBlock = ((style[`@${name}`] || {}) as Record<string, unknown>)[activeSelector] || {};
|
|
53
65
|
for (const [p, v] of Object.entries(selBlock)) {
|
|
54
|
-
if (typeof v !== "object")
|
|
66
|
+
if (typeof v !== "object") {
|
|
67
|
+
inherited[p] = v ?? "";
|
|
68
|
+
}
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
71
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
* These are all side-effect-free functions used by style/properties/events panels.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
import type {
|
|
7
|
+
import { defaultContentFormat, formatByName } from "../format/format-host";
|
|
8
|
+
import type { ContentTypeDef, ProjectConfig } from "@jxsuite/schema/types";
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* CamelCase → kebab-case for inline style attributes
|
|
@@ -14,7 +14,7 @@ import type { ProjectConfig, ContentTypeDef } from "@jxsuite/schema/types";
|
|
|
14
14
|
* @returns {string}
|
|
15
15
|
*/
|
|
16
16
|
export function camelToKebab(str: string) {
|
|
17
|
-
return str.
|
|
17
|
+
return str.replaceAll(/[A-Z]/g, (c: string) => `-${c.toLowerCase()}`);
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -25,12 +25,12 @@ export function camelToKebab(str: string) {
|
|
|
25
25
|
* @returns {string}
|
|
26
26
|
*/
|
|
27
27
|
export function camelToLabel(prop: string) {
|
|
28
|
-
return prop.
|
|
28
|
+
return prop.replaceAll(/([A-Z])/g, " $1").replace(/^./, (c: string) => c.toUpperCase());
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
export function toCamelCase(str: string): string {
|
|
32
32
|
return str
|
|
33
|
-
.
|
|
33
|
+
.replaceAll(/[^a-zA-Z0-9]+(.)?/g, (_, c) => (c ? c.toUpperCase() : ""))
|
|
34
34
|
.replace(/^[A-Z]/, (c) => c.toLowerCase());
|
|
35
35
|
}
|
|
36
36
|
|
|
@@ -42,7 +42,7 @@ export function toCamelCase(str: string): string {
|
|
|
42
42
|
* @returns {string}
|
|
43
43
|
*/
|
|
44
44
|
export function kebabToLabel(val: string) {
|
|
45
|
-
return val.
|
|
45
|
+
return val.replaceAll(
|
|
46
46
|
/(^|-)(\w)/g,
|
|
47
47
|
(_: string, sep: string, c: string) => (sep ? " " : "") + c.toUpperCase(),
|
|
48
48
|
);
|
|
@@ -73,12 +73,15 @@ export function attrLabel(
|
|
|
73
73
|
entry: { $label?: string; [key: string]: unknown } | null | undefined,
|
|
74
74
|
attr: string,
|
|
75
75
|
) {
|
|
76
|
-
if (entry?.$label)
|
|
77
|
-
|
|
78
|
-
|
|
76
|
+
if (entry?.$label) {
|
|
77
|
+
return entry.$label;
|
|
78
|
+
}
|
|
79
|
+
if (attr.includes("-")) {
|
|
80
|
+
return attr.replaceAll(
|
|
79
81
|
/(^|-)(\w)/g,
|
|
80
82
|
(_: string, sep: string, c: string) => (sep ? " " : "") + c.toUpperCase(),
|
|
81
83
|
);
|
|
84
|
+
}
|
|
82
85
|
return camelToLabel(attr);
|
|
83
86
|
}
|
|
84
87
|
|
|
@@ -90,25 +93,25 @@ export function attrLabel(
|
|
|
90
93
|
*/
|
|
91
94
|
export function abbreviateValue(val: string) {
|
|
92
95
|
const map: Record<string, string> = {
|
|
96
|
+
baseline: "base",
|
|
97
|
+
column: "col",
|
|
98
|
+
"column-reverse": "col-r",
|
|
99
|
+
contents: "cnt",
|
|
100
|
+
"flex-end": "end",
|
|
101
|
+
"flex-start": "start",
|
|
102
|
+
"flow-root": "flow",
|
|
93
103
|
inline: "inl",
|
|
94
104
|
"inline-block": "i-blk",
|
|
95
105
|
"inline-flex": "i-flx",
|
|
96
106
|
"inline-grid": "i-grd",
|
|
97
|
-
|
|
98
|
-
"flow-root": "flow",
|
|
107
|
+
normal: "norm",
|
|
99
108
|
nowrap: "no-wr",
|
|
100
|
-
"
|
|
101
|
-
"flex-start": "start",
|
|
102
|
-
"flex-end": "end",
|
|
103
|
-
"space-between": "betw",
|
|
109
|
+
"row-reverse": "row-r",
|
|
104
110
|
"space-around": "arnd",
|
|
111
|
+
"space-between": "betw",
|
|
105
112
|
"space-evenly": "even",
|
|
106
113
|
stretch: "str",
|
|
107
|
-
|
|
108
|
-
normal: "norm",
|
|
109
|
-
"row-reverse": "row-r",
|
|
110
|
-
"column-reverse": "col-r",
|
|
111
|
-
column: "col",
|
|
114
|
+
"wrap-reverse": "wr-rev",
|
|
112
115
|
};
|
|
113
116
|
return map[val] || val;
|
|
114
117
|
}
|
|
@@ -120,15 +123,33 @@ export function abbreviateValue(val: string) {
|
|
|
120
123
|
* @returns {string}
|
|
121
124
|
*/
|
|
122
125
|
export function inferInputType(entry: Record<string, unknown>) {
|
|
123
|
-
if (entry.$shorthand === true)
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if (entry
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (entry
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
if (entry.$shorthand === true) {
|
|
127
|
+
return "shorthand";
|
|
128
|
+
}
|
|
129
|
+
if (entry.$input === "button-group") {
|
|
130
|
+
return "button-group";
|
|
131
|
+
}
|
|
132
|
+
if (entry.$input === "media") {
|
|
133
|
+
return "media";
|
|
134
|
+
}
|
|
135
|
+
if (entry.format === "color") {
|
|
136
|
+
return "color";
|
|
137
|
+
}
|
|
138
|
+
if (entry.format === "uri-reference") {
|
|
139
|
+
return "media";
|
|
140
|
+
}
|
|
141
|
+
if (entry.$units !== undefined) {
|
|
142
|
+
return "number-unit";
|
|
143
|
+
}
|
|
144
|
+
if (entry.type === "number") {
|
|
145
|
+
return "number";
|
|
146
|
+
}
|
|
147
|
+
if (Array.isArray(entry.enum)) {
|
|
148
|
+
return "select";
|
|
149
|
+
}
|
|
150
|
+
if (Array.isArray(entry.examples) || Array.isArray(entry.presets)) {
|
|
151
|
+
return "combobox";
|
|
152
|
+
}
|
|
132
153
|
return "text";
|
|
133
154
|
}
|
|
134
155
|
|
|
@@ -144,15 +165,19 @@ export function findContentTypeSchema(
|
|
|
144
165
|
documentPath: string | null,
|
|
145
166
|
projectConfig: ProjectConfig | null | undefined,
|
|
146
167
|
) {
|
|
147
|
-
if (!documentPath || !projectConfig?.contentTypes)
|
|
168
|
+
if (!documentPath || !projectConfig?.contentTypes) {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
148
171
|
for (const [name, def] of Object.entries(
|
|
149
172
|
projectConfig.contentTypes as Record<string, ContentTypeDef>,
|
|
150
173
|
)) {
|
|
151
|
-
if (!def.source || !def.schema)
|
|
174
|
+
if (!def.source || !def.schema) {
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
152
177
|
const src = def.source.replace(/^\.\//, "").replace(/\/$/, "");
|
|
153
178
|
const hasExt = src.includes(".") && !src.endsWith("/");
|
|
154
179
|
if (hasExt) {
|
|
155
|
-
if (documentPath === src || documentPath.endsWith(
|
|
180
|
+
if (documentPath === src || documentPath.endsWith(`/${src}`)) {
|
|
156
181
|
return { name, schema: def.schema };
|
|
157
182
|
}
|
|
158
183
|
} else {
|
|
@@ -162,7 +187,7 @@ export function findContentTypeSchema(
|
|
|
162
187
|
: (formatByName(def.format)?.extensions[0] ??
|
|
163
188
|
defaultContentFormat()?.extensions[0] ??
|
|
164
189
|
".json");
|
|
165
|
-
if (documentPath.startsWith(src
|
|
190
|
+
if (documentPath.startsWith(`${src}/`) && documentPath.endsWith(ext)) {
|
|
166
191
|
return { name, schema: def.schema };
|
|
167
192
|
}
|
|
168
193
|
}
|
|
@@ -182,11 +207,13 @@ export function friendlyNameToVar(name: string, prefix: string) {
|
|
|
182
207
|
const slug = name
|
|
183
208
|
.trim()
|
|
184
209
|
.toLowerCase()
|
|
185
|
-
.
|
|
186
|
-
.
|
|
187
|
-
.
|
|
188
|
-
.
|
|
189
|
-
if (!slug)
|
|
210
|
+
.replaceAll(/[^a-z0-9\s-]/g, "")
|
|
211
|
+
.replaceAll(/\s+/g, "-")
|
|
212
|
+
.replaceAll(/-+/g, "-")
|
|
213
|
+
.replaceAll(/^-|-$/g, "");
|
|
214
|
+
if (!slug) {
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
190
217
|
return `${prefix}${slug}`;
|
|
191
218
|
}
|
|
192
219
|
|
|
@@ -201,10 +228,10 @@ export function friendlyNameToVar(name: string, prefix: string) {
|
|
|
201
228
|
export function varDisplayName(varName: string, prefix: string) {
|
|
202
229
|
return (
|
|
203
230
|
varName
|
|
204
|
-
.replace(new RegExp(`^${prefix.
|
|
231
|
+
.replace(new RegExp(`^${prefix.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw`\$&`)}`), "")
|
|
205
232
|
.replace(/^--/, "")
|
|
206
|
-
.
|
|
207
|
-
.
|
|
233
|
+
.replaceAll("-", " ")
|
|
234
|
+
.replaceAll(/\b\w/g, (c: string) => c.toUpperCase()) || varName
|
|
208
235
|
);
|
|
209
236
|
}
|
|
210
237
|
|
|
@@ -218,13 +245,19 @@ export function varDisplayName(varName: string, prefix: string) {
|
|
|
218
245
|
* | { kind: "text" }}
|
|
219
246
|
*/
|
|
220
247
|
export function parseCemType(typeText: string | undefined | null) {
|
|
221
|
-
if (!typeText)
|
|
248
|
+
if (!typeText) {
|
|
249
|
+
return { kind: "text" };
|
|
250
|
+
}
|
|
222
251
|
const t = typeText
|
|
223
252
|
.trim()
|
|
224
|
-
.
|
|
253
|
+
.replaceAll(/\s*\|\s*undefined\b/g, "")
|
|
225
254
|
.trim();
|
|
226
|
-
if (t === "boolean")
|
|
227
|
-
|
|
255
|
+
if (t === "boolean") {
|
|
256
|
+
return { kind: "boolean" };
|
|
257
|
+
}
|
|
258
|
+
if (t === "number") {
|
|
259
|
+
return { kind: "number" };
|
|
260
|
+
}
|
|
228
261
|
// Detect enum: "'a' | 'b' | 'c'" — pipe-separated quoted literals
|
|
229
262
|
const enumMatch = t.match(/^'[^']*'(\s*\|\s*'[^']*')+$/);
|
|
230
263
|
if (enumMatch) {
|
|
@@ -233,3 +266,13 @@ export function parseCemType(typeText: string | undefined | null) {
|
|
|
233
266
|
}
|
|
234
267
|
return { kind: "text" };
|
|
235
268
|
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Clone a (possibly reactive) document tree via JSON round-trip. Unlike structuredClone, this works
|
|
272
|
+
* on Vue reactive proxies and JSON-normalizes the tree (drops undefined values and functions) —
|
|
273
|
+
* both required for document snapshots and clipboard payloads.
|
|
274
|
+
*/
|
|
275
|
+
export function jsonClone<T>(value: T): T {
|
|
276
|
+
// oxlint-disable-next-line unicorn/prefer-structured-clone -- structuredClone throws on reactive proxies; JSON normalization is the point
|
|
277
|
+
return JSON.parse(JSON.stringify(value)) as T;
|
|
278
|
+
}
|