@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.
- package/dist/studio.css +98 -98
- package/dist/studio.js +12577 -12183
- package/dist/studio.js.map +146 -144
- package/dist/workers/editor.worker.js +79 -79
- package/dist/workers/json.worker.js +109 -109
- package/dist/workers/ts.worker.js +82 -82
- package/package.json +17 -6
- package/src/browse/browse.js +25 -19
- package/src/canvas/canvas-diff.js +24 -16
- package/src/canvas/canvas-helpers.js +24 -19
- package/src/canvas/canvas-live-render.js +94 -39
- package/src/canvas/canvas-render.js +119 -73
- package/src/canvas/canvas-utils.js +38 -15
- package/src/canvas/nested-site-style.js +50 -0
- package/src/editor/component-inline-edit.js +14 -13
- package/src/editor/content-inline-edit.js +22 -10
- package/src/editor/context-menu.js +1 -1
- package/src/editor/convert-to-component.js +12 -10
- package/src/editor/inline-edit.js +66 -28
- package/src/editor/insertion-helper.js +16 -12
- package/src/editor/shortcuts.js +4 -2
- package/src/editor/slash-menu.js +6 -4
- package/src/files/components.js +1 -1
- package/src/files/file-ops.js +37 -16
- package/src/files/files.js +78 -52
- package/src/github/github-auth.js +122 -0
- package/src/github/github-publish.js +115 -0
- package/src/markdown/md-convert.js +172 -107
- package/src/new-project/new-project-modal.js +204 -0
- package/src/panels/activity-bar.js +22 -20
- package/src/panels/ai-panel.js +399 -0
- package/src/panels/block-action-bar.js +5 -5
- package/src/panels/canvas-dnd.js +8 -2
- package/src/panels/data-explorer.js +19 -13
- package/src/panels/dnd.js +144 -92
- package/src/panels/editors.js +55 -35
- package/src/panels/elements-panel.js +33 -19
- package/src/panels/events-panel.js +15 -10
- package/src/panels/git-panel.js +469 -98
- package/src/panels/head-panel.js +121 -81
- package/src/panels/imports-panel.js +88 -64
- package/src/panels/layers-panel.js +64 -43
- package/src/panels/left-panel.js +85 -47
- package/src/panels/overlays.js +1 -0
- package/src/panels/panel-events.js +45 -30
- package/src/panels/preview-render.js +23 -18
- package/src/panels/properties-panel.js +365 -249
- package/src/panels/pseudo-preview.js +2 -2
- package/src/panels/quick-search.js +7 -5
- package/src/panels/right-panel.js +26 -2
- package/src/panels/shared.js +4 -4
- package/src/panels/signals-panel.js +295 -166
- package/src/panels/statusbar.js +4 -4
- package/src/panels/style-inputs.js +67 -35
- package/src/panels/style-panel.js +233 -132
- package/src/panels/style-utils.js +34 -16
- package/src/panels/stylebook-layers-panel.js +12 -10
- package/src/panels/stylebook-panel.js +134 -66
- package/src/panels/toolbar.js +333 -109
- package/src/panels/welcome-screen.js +121 -0
- package/src/platform.js +2 -4
- package/src/platforms/devserver.js +113 -7
- package/src/resize-edges.js +98 -0
- package/src/services/cem-export.js +12 -7
- package/src/services/code-services.js +30 -12
- package/src/settings/content-types-editor.js +7 -7
- package/src/settings/css-vars-editor.js +30 -24
- package/src/settings/defs-editor.js +12 -7
- package/src/settings/general-settings.js +85 -3
- package/src/settings/head-editor.js +30 -24
- package/src/settings/schema-field-ui.js +58 -39
- package/src/site-context.js +41 -31
- package/src/state.js +70 -29
- package/src/store.js +21 -26
- package/src/studio.js +103 -81
- package/src/tabs/tab.js +67 -43
- package/src/tabs/transact.js +23 -13
- package/src/ui/button-group.js +24 -24
- package/src/ui/color-selector.js +28 -23
- package/src/ui/field-row.js +3 -3
- package/src/ui/icons.js +2 -2
- package/src/ui/media-picker.js +7 -7
- package/src/ui/spectrum.js +5 -1
- package/src/ui/unit-selector.js +18 -16
- package/src/ui/value-selector.js +21 -15
- package/src/ui/widgets.js +22 -19
- package/src/utils/canvas-media.js +5 -4
- package/src/utils/edit-display.js +31 -20
- package/src/utils/google-fonts.js +11 -11
- package/src/utils/inherited-style.js +9 -8
- package/src/utils/studio-utils.js +7 -7
- package/src/view.js +58 -1
- 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 {
|
|
26
|
+
* @param {string | number | undefined} value
|
|
27
27
|
* @param {(val: string) => void} onChange
|
|
28
28
|
* @param {string} [placeholder]
|
|
29
|
-
* @returns {
|
|
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 {
|
|
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 {
|
|
47
|
+
* @param {Record<string, unknown>} entry
|
|
48
48
|
* @param {string} prop
|
|
49
|
-
* @param {
|
|
50
|
-
* @param {(val:
|
|
51
|
-
* @
|
|
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(
|
|
61
|
-
max=${ifDefined(
|
|
62
|
-
step=${ifDefined(
|
|
63
|
-
@change=${debouncedStyleCommit(`num:${prop}`, 400, (/** @type {
|
|
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 {
|
|
79
|
+
* @param {Record<string, unknown>} entry — css-meta or schema entry
|
|
77
80
|
* @param {string} prop — property key
|
|
78
|
-
* @param {
|
|
79
|
-
* @param {(val:
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
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 {
|
|
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 {
|
|
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 {
|
|
27
|
-
if (node.
|
|
28
|
-
node.
|
|
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 {
|
|
39
|
-
* @returns {
|
|
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,
|
|
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(
|
|
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 (
|
|
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
|
|
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" &&
|
|
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 (
|
|
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,
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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,
|
|
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 {
|
|
86
|
-
* @returns {
|
|
85
|
+
* @param {JxHeadEntry[]} head
|
|
86
|
+
* @returns {JxHeadEntry[]}
|
|
87
87
|
*/
|
|
88
88
|
export function cleanupGoogleFontPreconnects(head) {
|
|
89
|
-
const hasFont = head.some((/** @type {
|
|
89
|
+
const hasFont = head.some((/** @type {JxHeadEntry} */ e) => isGoogleFontEntry(e));
|
|
90
90
|
if (!hasFont) {
|
|
91
|
-
return head.filter((/** @type {
|
|
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 {
|
|
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,
|
|
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,
|
|
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 =
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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 {
|
|
131
|
-
* @returns {{ name: string; schema:
|
|
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,
|
|
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 {
|
|
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
|
-
/**
|
|
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 {
|
|
19
|
+
/** @type {JxMutableNode | null} */
|
|
20
20
|
clipboard: null,
|
|
21
|
-
/** @type {
|
|
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 {
|
|
45
|
-
|
|
46
|
-
|
|
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,
|
|
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,
|
|
108
|
+
* document: Record<string, unknown>;
|
|
106
109
|
* frontmatter?: Record<string, unknown>;
|
|
107
110
|
* sourceFormat?: string | null;
|
|
108
111
|
* }} newTabOpts
|