@jxsuite/studio 0.1.0 → 0.5.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 +47638 -33445
- package/dist/studio.js.map +449 -344
- package/package.json +45 -34
- package/src/browse/browse.js +414 -0
- package/src/editor/context-menu.js +48 -1
- package/src/editor/convert-to-component.js +208 -0
- package/src/editor/inline-edit.js +33 -6
- package/src/editor/shortcuts.js +6 -1
- package/src/files/components.js +4 -2
- package/src/files/file-ops.js +102 -54
- package/src/files/files.js +22 -8
- package/src/markdown/md-convert.js +309 -11
- package/src/panels/activity-bar.js +3 -0
- package/src/panels/head-panel.js +576 -0
- package/src/panels/overlays.js +125 -0
- package/src/panels/right-panel.js +104 -0
- package/src/panels/shared.js +41 -0
- package/src/panels/signals-panel.js +95 -94
- package/src/panels/toolbar.js +217 -0
- package/src/platforms/devserver.js +58 -16
- package/src/settings/collections-editor.js +428 -0
- package/src/settings/defs-editor.js +418 -0
- package/src/settings/schema-field-ui.js +329 -0
- package/src/state.js +99 -2
- package/src/store.js +77 -41
- package/src/studio.js +1523 -1375
- package/src/ui/button-group.js +91 -0
- package/src/ui/color-selector.js +299 -0
- package/src/ui/field-row.js +47 -0
- package/src/ui/media-picker.js +172 -0
- package/src/ui/panel-resize.js +96 -0
- package/src/ui/spectrum.js +36 -2
- package/src/ui/unit-selector.js +106 -0
- package/src/ui/{jx-styled-combobox.js → value-selector.js} +7 -7
- package/src/ui/widgets.js +106 -0
- package/src/utils/inherited-style.js +54 -0
- package/src/utils/studio-utils.js +32 -0
- package/src/view.js +45 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Panel-resize.js — Draggable resize handles for left and right sidebars.
|
|
3
|
+
*
|
|
4
|
+
* Self-initializing module. Import it and the resize handles become interactive. Persists widths to
|
|
5
|
+
* localStorage so they survive page reloads.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const STORAGE_KEY = "jx-studio-panel-widths";
|
|
9
|
+
const MIN_WIDTH = 160;
|
|
10
|
+
const MAX_RATIO = 0.5; // max 50% of viewport
|
|
11
|
+
const DEFAULT_LEFT = 240;
|
|
12
|
+
const DEFAULT_RIGHT = 280;
|
|
13
|
+
|
|
14
|
+
const root = document.documentElement;
|
|
15
|
+
|
|
16
|
+
// ─── Restore saved widths ────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
|
|
20
|
+
if (saved.left) root.style.setProperty("--panel-w-left", `${saved.left}px`);
|
|
21
|
+
if (saved.right) root.style.setProperty("--panel-w-right", `${saved.right}px`);
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Setup handles ───────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @param {HTMLElement} handle
|
|
30
|
+
* @param {string} cssVar
|
|
31
|
+
* @param {"left" | "right"} side
|
|
32
|
+
* @param {number} defaultWidth
|
|
33
|
+
*/
|
|
34
|
+
function setupHandle(handle, cssVar, side, defaultWidth) {
|
|
35
|
+
/** @type {{ startX: number; startWidth: number } | null} */
|
|
36
|
+
let drag = null;
|
|
37
|
+
|
|
38
|
+
handle.addEventListener("pointerdown", (e) => {
|
|
39
|
+
e.preventDefault();
|
|
40
|
+
try {
|
|
41
|
+
handle.setPointerCapture(e.pointerId);
|
|
42
|
+
} catch {
|
|
43
|
+
/* synthetic events */
|
|
44
|
+
}
|
|
45
|
+
handle.classList.add("dragging");
|
|
46
|
+
document.body.style.userSelect = "none";
|
|
47
|
+
|
|
48
|
+
const current = parseInt(getComputedStyle(root).getPropertyValue(cssVar)) || defaultWidth;
|
|
49
|
+
drag = { startX: e.clientX, startWidth: current };
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
handle.addEventListener("pointermove", (e) => {
|
|
53
|
+
if (!drag) return;
|
|
54
|
+
const delta = side === "left" ? e.clientX - drag.startX : drag.startX - e.clientX;
|
|
55
|
+
const maxWidth = window.innerWidth * MAX_RATIO;
|
|
56
|
+
const newWidth = Math.round(Math.min(maxWidth, Math.max(MIN_WIDTH, drag.startWidth + delta)));
|
|
57
|
+
root.style.setProperty(cssVar, `${newWidth}px`);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
handle.addEventListener("pointerup", (e) => {
|
|
61
|
+
if (!drag) return;
|
|
62
|
+
drag = null;
|
|
63
|
+
try {
|
|
64
|
+
handle.releasePointerCapture(e.pointerId);
|
|
65
|
+
} catch {
|
|
66
|
+
/* synthetic events */
|
|
67
|
+
}
|
|
68
|
+
handle.classList.remove("dragging");
|
|
69
|
+
document.body.style.userSelect = "";
|
|
70
|
+
persistWidths();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
handle.addEventListener("dblclick", () => {
|
|
74
|
+
root.style.setProperty(cssVar, `${defaultWidth}px`);
|
|
75
|
+
persistWidths();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function persistWidths() {
|
|
80
|
+
const left = parseInt(getComputedStyle(root).getPropertyValue("--panel-w-left")) || DEFAULT_LEFT;
|
|
81
|
+
const right =
|
|
82
|
+
parseInt(getComputedStyle(root).getPropertyValue("--panel-w-right")) || DEFAULT_RIGHT;
|
|
83
|
+
try {
|
|
84
|
+
localStorage.setItem(STORAGE_KEY, JSON.stringify({ left, right }));
|
|
85
|
+
} catch {
|
|
86
|
+
// storage full or unavailable
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ─── Initialize ──────────────────────────────────────────────────────────────
|
|
91
|
+
|
|
92
|
+
const resizeLeft = document.getElementById("resize-left");
|
|
93
|
+
const resizeRight = document.getElementById("resize-right");
|
|
94
|
+
|
|
95
|
+
if (resizeLeft) setupHandle(resizeLeft, "--panel-w-left", "left", DEFAULT_LEFT);
|
|
96
|
+
if (resizeRight) setupHandle(resizeRight, "--panel-w-right", "right", DEFAULT_RIGHT);
|
package/src/ui/spectrum.js
CHANGED
|
@@ -37,10 +37,22 @@ import { Switch as SpSwitch } from "@spectrum-web-components/switch/src/Switch.j
|
|
|
37
37
|
import { Divider } from "@spectrum-web-components/divider/src/Divider.js";
|
|
38
38
|
import { Tooltip } from "@spectrum-web-components/tooltip/src/Tooltip.js";
|
|
39
39
|
import { Overlay } from "@spectrum-web-components/overlay/src/Overlay.js";
|
|
40
|
+
import { OverlayTrigger } from "@spectrum-web-components/overlay/src/OverlayTrigger.js";
|
|
41
|
+
import { Dialog } from "@spectrum-web-components/dialog/src/Dialog.js";
|
|
42
|
+
import { DialogWrapper } from "@spectrum-web-components/dialog/src/DialogWrapper.js";
|
|
43
|
+
import { HelpText } from "@spectrum-web-components/help-text/src/HelpText.js";
|
|
44
|
+
import { Button } from "@spectrum-web-components/button/src/Button.js";
|
|
45
|
+
import { Underlay } from "@spectrum-web-components/underlay/src/Underlay.js";
|
|
40
46
|
import { PickerButton } from "@spectrum-web-components/picker-button/src/PickerButton.js";
|
|
41
47
|
import { Accordion } from "@spectrum-web-components/accordion/src/Accordion.js";
|
|
42
48
|
import { AccordionItem } from "@spectrum-web-components/accordion/src/AccordionItem.js";
|
|
43
49
|
import { ActionBar } from "@spectrum-web-components/action-bar/src/ActionBar.js";
|
|
50
|
+
import { Table } from "@spectrum-web-components/table/src/Table.js";
|
|
51
|
+
import { TableHead } from "@spectrum-web-components/table/src/TableHead.js";
|
|
52
|
+
import { TableHeadCell } from "@spectrum-web-components/table/src/TableHeadCell.js";
|
|
53
|
+
import { TableBody } from "@spectrum-web-components/table/src/TableBody.js";
|
|
54
|
+
import { TableRow } from "@spectrum-web-components/table/src/TableRow.js";
|
|
55
|
+
import { TableCell } from "@spectrum-web-components/table/src/TableCell.js";
|
|
44
56
|
|
|
45
57
|
// Icons
|
|
46
58
|
import { IconFolder } from "@spectrum-web-components/icons-workflow/src/elements/IconFolder.js";
|
|
@@ -49,8 +61,10 @@ import { IconDocument } from "@spectrum-web-components/icons-workflow/src/elemen
|
|
|
49
61
|
import { IconFileCode } from "@spectrum-web-components/icons-workflow/src/elements/IconFileCode.js";
|
|
50
62
|
import { IconFileTxt } from "@spectrum-web-components/icons-workflow/src/elements/IconFileTxt.js";
|
|
51
63
|
import { IconImage } from "@spectrum-web-components/icons-workflow/src/elements/IconImage.js";
|
|
64
|
+
import { IconFileSingleWebPage } from "@spectrum-web-components/icons-workflow/src/elements/IconFileSingleWebPage.js";
|
|
52
65
|
import { IconRefresh } from "@spectrum-web-components/icons-workflow/src/elements/IconRefresh.js";
|
|
53
66
|
import { IconAdd } from "@spectrum-web-components/icons-workflow/src/elements/IconAdd.js";
|
|
67
|
+
import { IconUpload } from "@spectrum-web-components/icons-workflow/src/elements/IconUpload.js";
|
|
54
68
|
import { IconLayers } from "@spectrum-web-components/icons-workflow/src/elements/IconLayers.js";
|
|
55
69
|
import { IconViewGrid } from "@spectrum-web-components/icons-workflow/src/elements/IconViewGrid.js";
|
|
56
70
|
import { IconBrackets } from "@spectrum-web-components/icons-workflow/src/elements/IconBrackets.js";
|
|
@@ -69,6 +83,7 @@ import { IconExport } from "@spectrum-web-components/icons-workflow/src/elements
|
|
|
69
83
|
import { IconPreview } from "@spectrum-web-components/icons-workflow/src/elements/IconPreview.js";
|
|
70
84
|
import { IconCode } from "@spectrum-web-components/icons-workflow/src/elements/IconCode.js";
|
|
71
85
|
import { IconBrush } from "@spectrum-web-components/icons-workflow/src/elements/IconBrush.js";
|
|
86
|
+
import { IconGears } from "@spectrum-web-components/icons-workflow/src/elements/IconGears.js";
|
|
72
87
|
import { IconBack } from "@spectrum-web-components/icons-workflow/src/elements/IconBack.js";
|
|
73
88
|
import { IconProperties } from "@spectrum-web-components/icons-workflow/src/elements/IconProperties.js";
|
|
74
89
|
import { IconEvent } from "@spectrum-web-components/icons-workflow/src/elements/IconEvent.js";
|
|
@@ -103,6 +118,7 @@ import { IconBox } from "@spectrum-web-components/icons-workflow/src/elements/Ic
|
|
|
103
118
|
import { IconVisibility } from "@spectrum-web-components/icons-workflow/src/elements/IconVisibility.js";
|
|
104
119
|
import { IconVisibilityOff } from "@spectrum-web-components/icons-workflow/src/elements/IconVisibilityOff.js";
|
|
105
120
|
import { IconArtboard } from "@spectrum-web-components/icons-workflow/src/elements/IconArtboard.js";
|
|
121
|
+
import { IconViewList } from "@spectrum-web-components/icons-workflow/src/elements/IconViewList.js";
|
|
106
122
|
|
|
107
123
|
// Inline formatting icons
|
|
108
124
|
import { IconTextBold } from "@spectrum-web-components/icons-workflow/src/elements/IconTextBold.js";
|
|
@@ -114,7 +130,8 @@ import { IconTextSubscript } from "@spectrum-web-components/icons-workflow/src/e
|
|
|
114
130
|
import { IconLink } from "@spectrum-web-components/icons-workflow/src/elements/IconLink.js";
|
|
115
131
|
|
|
116
132
|
// Custom studio components
|
|
117
|
-
import {
|
|
133
|
+
import { JxValueSelector } from "./value-selector.js";
|
|
134
|
+
import { JxColorPopover } from "./color-selector.js";
|
|
118
135
|
|
|
119
136
|
// UI icons (used internally by Spectrum components like accordion, picker, combobox)
|
|
120
137
|
import { IconChevron100 } from "@spectrum-web-components/icons-ui/src/elements/IconChevron100.js";
|
|
@@ -151,18 +168,32 @@ const components = [
|
|
|
151
168
|
["sp-divider", Divider],
|
|
152
169
|
["sp-tooltip", Tooltip],
|
|
153
170
|
["sp-overlay", Overlay],
|
|
171
|
+
["overlay-trigger", OverlayTrigger],
|
|
172
|
+
["sp-dialog", Dialog],
|
|
173
|
+
["sp-dialog-wrapper", DialogWrapper],
|
|
174
|
+
["sp-help-text", HelpText],
|
|
175
|
+
["sp-button", Button],
|
|
176
|
+
["sp-underlay", Underlay],
|
|
154
177
|
["sp-picker-button", PickerButton],
|
|
155
178
|
["sp-accordion", Accordion],
|
|
156
179
|
["sp-accordion-item", AccordionItem],
|
|
157
180
|
["sp-action-bar", ActionBar],
|
|
181
|
+
["sp-table", Table],
|
|
182
|
+
["sp-table-head", TableHead],
|
|
183
|
+
["sp-table-head-cell", TableHeadCell],
|
|
184
|
+
["sp-table-body", TableBody],
|
|
185
|
+
["sp-table-row", TableRow],
|
|
186
|
+
["sp-table-cell", TableCell],
|
|
158
187
|
["sp-icon-folder", IconFolder],
|
|
159
188
|
["sp-icon-folder-open", IconFolderOpen],
|
|
160
189
|
["sp-icon-document", IconDocument],
|
|
161
190
|
["sp-icon-file-code", IconFileCode],
|
|
162
191
|
["sp-icon-file-txt", IconFileTxt],
|
|
163
192
|
["sp-icon-image", IconImage],
|
|
193
|
+
["sp-icon-file-single-web-page", IconFileSingleWebPage],
|
|
164
194
|
["sp-icon-refresh", IconRefresh],
|
|
165
195
|
["sp-icon-add", IconAdd],
|
|
196
|
+
["sp-icon-upload", IconUpload],
|
|
166
197
|
["sp-icon-layers", IconLayers],
|
|
167
198
|
["sp-icon-view-grid", IconViewGrid],
|
|
168
199
|
["sp-icon-brackets", IconBrackets],
|
|
@@ -181,6 +212,7 @@ const components = [
|
|
|
181
212
|
["sp-icon-preview", IconPreview],
|
|
182
213
|
["sp-icon-code", IconCode],
|
|
183
214
|
["sp-icon-brush", IconBrush],
|
|
215
|
+
["sp-icon-gears", IconGears],
|
|
184
216
|
["sp-icon-back", IconBack],
|
|
185
217
|
["sp-icon-properties", IconProperties],
|
|
186
218
|
["sp-icon-event", IconEvent],
|
|
@@ -213,6 +245,7 @@ const components = [
|
|
|
213
245
|
["sp-icon-visibility", IconVisibility],
|
|
214
246
|
["sp-icon-visibility-off", IconVisibilityOff],
|
|
215
247
|
["sp-icon-artboard", IconArtboard],
|
|
248
|
+
["sp-icon-view-list", IconViewList],
|
|
216
249
|
["sp-icon-text-bold", IconTextBold],
|
|
217
250
|
["sp-icon-text-italic", IconTextItalic],
|
|
218
251
|
["sp-icon-text-underline", IconTextUnderline],
|
|
@@ -223,7 +256,8 @@ const components = [
|
|
|
223
256
|
// UI icons (internal component chrome)
|
|
224
257
|
["sp-icon-chevron100", IconChevron100],
|
|
225
258
|
// Custom studio components
|
|
226
|
-
["jx-
|
|
259
|
+
["jx-value-selector", JxValueSelector],
|
|
260
|
+
["jx-color-popover", JxColorPopover],
|
|
227
261
|
];
|
|
228
262
|
|
|
229
263
|
for (const [tag, ctor] of /** @type {[string, CustomElementConstructor][]} */ (components)) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit-selector.js — Number + unit picker widget.
|
|
3
|
+
*
|
|
4
|
+
* Renders a text field for numeric input paired with a unit picker dropdown. Handles keywords
|
|
5
|
+
* (auto, inherit, etc.) alongside numeric+unit values.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { html, nothing } from "lit-html";
|
|
9
|
+
import { live } from "lit-html/directives/live.js";
|
|
10
|
+
import { classMap } from "lit-html/directives/class-map.js";
|
|
11
|
+
import { debouncedStyleCommit } from "../store.js";
|
|
12
|
+
|
|
13
|
+
export const UNIT_RE = /^(-?[\d.]+)(px|rem|em|%|vw|vh|svw|svh|dvh|ms|s|fr|ch|ex|deg)?$/;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Render a number + unit selector widget.
|
|
17
|
+
*
|
|
18
|
+
* @param {any} entry — css-meta entry with $units and $keywords arrays
|
|
19
|
+
* @param {string} prop — property key (for debounce namespace)
|
|
20
|
+
* @param {any} value — current value (e.g. "12px", "auto", "")
|
|
21
|
+
* @param {(val: string) => void} onChange — commit callback
|
|
22
|
+
* @returns {any}
|
|
23
|
+
*/
|
|
24
|
+
export function renderUnitSelector(
|
|
25
|
+
/** @type {any} */ entry,
|
|
26
|
+
/** @type {any} */ prop,
|
|
27
|
+
/** @type {any} */ value,
|
|
28
|
+
/** @type {any} */ onChange,
|
|
29
|
+
/** @type {string} */ placeholder = "",
|
|
30
|
+
) {
|
|
31
|
+
const units = entry.$units || [];
|
|
32
|
+
const keywords = entry.$keywords || [];
|
|
33
|
+
const strVal = String(value ?? "");
|
|
34
|
+
const match = strVal.match(UNIT_RE);
|
|
35
|
+
const isKeyword = !match && strVal !== "" && keywords.includes(strVal);
|
|
36
|
+
const isNumericVal = (/** @type {any} */ v) => /^-?\d*\.?\d*$/.test(v);
|
|
37
|
+
|
|
38
|
+
const currentUnit = isKeyword ? units[0] || "" : match ? match[2] || "" : units[0] || "";
|
|
39
|
+
let displayValue;
|
|
40
|
+
if (isKeyword) displayValue = strVal;
|
|
41
|
+
else if (match) displayValue = match[1];
|
|
42
|
+
else if (strVal !== "") {
|
|
43
|
+
const num = parseFloat(strVal);
|
|
44
|
+
displayValue = isNaN(num) ? strVal : String(num);
|
|
45
|
+
} else displayValue = "";
|
|
46
|
+
|
|
47
|
+
const isExpression = isKeyword || (displayValue !== "" && !isNumericVal(displayValue));
|
|
48
|
+
const hasUnits = units.length > 0 || keywords.length > 0;
|
|
49
|
+
const btnId = `style-unit-${prop}`;
|
|
50
|
+
|
|
51
|
+
return html`
|
|
52
|
+
<div class="style-input-number-unit">
|
|
53
|
+
<div class=${classMap({ "input-group": true, "is-expression": isExpression })}>
|
|
54
|
+
<sp-textfield
|
|
55
|
+
size="s"
|
|
56
|
+
placeholder=${placeholder || "0"}
|
|
57
|
+
.value=${live(displayValue)}
|
|
58
|
+
@input=${debouncedStyleCommit(`nui:${prop}`, 400, (/** @type {any} */ e) => {
|
|
59
|
+
const val = (e.target.value ?? "").trim();
|
|
60
|
+
if (val === "") {
|
|
61
|
+
onChange("");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (isNumericVal(val)) onChange(units.length > 0 ? val + currentUnit : val);
|
|
65
|
+
else onChange(val);
|
|
66
|
+
})}
|
|
67
|
+
></sp-textfield>
|
|
68
|
+
${hasUnits
|
|
69
|
+
? html`
|
|
70
|
+
<sp-picker-button id=${btnId} size="s">
|
|
71
|
+
<span slot="label">${currentUnit || units[0] || ""}</span>
|
|
72
|
+
</sp-picker-button>
|
|
73
|
+
<sp-overlay trigger="${btnId}@click" placement="bottom-end" offset="4">
|
|
74
|
+
<sp-popover style="min-width: var(--spectrum-component-width-900, 64px)">
|
|
75
|
+
<sp-menu
|
|
76
|
+
label="CSS unit"
|
|
77
|
+
@change=${(/** @type {any} */ e) => {
|
|
78
|
+
const chosen = e.target.value;
|
|
79
|
+
if (keywords.includes(chosen)) {
|
|
80
|
+
onChange(chosen);
|
|
81
|
+
} else if (units.includes(chosen)) {
|
|
82
|
+
const curMatch = String(value ?? "").match(UNIT_RE);
|
|
83
|
+
const numPart = curMatch ? curMatch[1] : "";
|
|
84
|
+
if (numPart) onChange(numPart + chosen);
|
|
85
|
+
}
|
|
86
|
+
}}
|
|
87
|
+
>
|
|
88
|
+
${units.map(
|
|
89
|
+
(/** @type {any} */ u) => html`<sp-menu-item value=${u}>${u}</sp-menu-item>`,
|
|
90
|
+
)}
|
|
91
|
+
${keywords.length > 0 && units.length > 0
|
|
92
|
+
? html`<sp-menu-divider></sp-menu-divider>`
|
|
93
|
+
: nothing}
|
|
94
|
+
${keywords.map(
|
|
95
|
+
(/** @type {any} */ kw) =>
|
|
96
|
+
html`<sp-menu-item value=${kw}>${kw}</sp-menu-item>`,
|
|
97
|
+
)}
|
|
98
|
+
</sp-menu>
|
|
99
|
+
</sp-popover>
|
|
100
|
+
</sp-overlay>
|
|
101
|
+
`
|
|
102
|
+
: nothing}
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
`;
|
|
106
|
+
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Value Selector — Dual-mode styled combobox custom element.
|
|
3
3
|
*
|
|
4
4
|
* Renders as sp-picker when the current value matches a predefined option, or as a textfield +
|
|
5
5
|
* dropdown overlay (manual combobox) when it doesn't. Both modes share identical styled menu items,
|
|
6
6
|
* ensuring visual consistency.
|
|
7
7
|
*
|
|
8
|
-
* Usage: html`<jx-
|
|
8
|
+
* Usage: html`<jx-value-selector size="s" .value=${"italic"} placeholder="normal" .options=${[{
|
|
9
9
|
* value: "italic", label: "Italic", style: "font-style: italic" }]} @change=${handler}
|
|
10
10
|
* @input=${handler}
|
|
11
11
|
*
|
|
12
|
-
* > </jx-
|
|
12
|
+
* > </jx-value-selector>`
|
|
13
13
|
*
|
|
14
14
|
* Options format: { value: string, label: string, style?: string } — menu item { divider: true } —
|
|
15
15
|
* menu divider
|
|
@@ -20,7 +20,7 @@ import { live } from "lit/directives/live.js";
|
|
|
20
20
|
|
|
21
21
|
/** @typedef {{ value: string; label: string; style?: string } | { divider: true }} ComboOption */
|
|
22
22
|
|
|
23
|
-
export class
|
|
23
|
+
export class JxValueSelector extends LitElement {
|
|
24
24
|
static properties = {
|
|
25
25
|
value: { type: String },
|
|
26
26
|
placeholder: { type: String },
|
|
@@ -71,14 +71,14 @@ export class JxStyledCombobox extends LitElement {
|
|
|
71
71
|
|
|
72
72
|
/** Picker mode: sp-picker @change handler */
|
|
73
73
|
_handlePickerChange(/** @type {any} */ e) {
|
|
74
|
-
e.stopPropagation();
|
|
74
|
+
e.stopPropagation();
|
|
75
75
|
this.value = e.target.value;
|
|
76
76
|
this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** Combobox mode: sp-menu @change handler */
|
|
80
80
|
_handleMenuChange(/** @type {any} */ e) {
|
|
81
|
-
e.stopPropagation();
|
|
81
|
+
e.stopPropagation();
|
|
82
82
|
if (!e.target.value) return;
|
|
83
83
|
this.value = e.target.value;
|
|
84
84
|
this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
@@ -86,7 +86,7 @@ export class JxStyledCombobox extends LitElement {
|
|
|
86
86
|
|
|
87
87
|
/** Combobox mode: textfield @input handler */
|
|
88
88
|
_handleInput(/** @type {any} */ e) {
|
|
89
|
-
e.stopPropagation();
|
|
89
|
+
e.stopPropagation();
|
|
90
90
|
this.value = e.target.value;
|
|
91
91
|
this.dispatchEvent(new Event("input", { bubbles: true, composed: true }));
|
|
92
92
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Widgets.js — Widget type dispatcher and simple widget renderers.
|
|
3
|
+
*
|
|
4
|
+
* This module provides `widgetForType()` which dispatches to the appropriate widget based on the
|
|
5
|
+
* inferred type from css-meta/schema entries, plus the simpler widget renderers (text, number,
|
|
6
|
+
* select/combobox).
|
|
7
|
+
*
|
|
8
|
+
* Complex widgets are imported from their dedicated modules: - renderColorSelector →
|
|
9
|
+
* ui/color-selector.js - renderUnitSelector → ui/unit-selector.js - renderButtonGroup →
|
|
10
|
+
* ui/button-group.js
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { html } from "lit-html";
|
|
14
|
+
import { live } from "lit-html/directives/live.js";
|
|
15
|
+
import { ifDefined } from "lit-html/directives/if-defined.js";
|
|
16
|
+
import { debouncedStyleCommit } from "../store.js";
|
|
17
|
+
import { renderColorSelector } from "./color-selector.js";
|
|
18
|
+
import { renderUnitSelector } from "./unit-selector.js";
|
|
19
|
+
import { renderButtonGroup } from "./button-group.js";
|
|
20
|
+
import { renderMediaPicker } from "./media-picker.js";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render a plain text input widget.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} prop
|
|
26
|
+
* @param {any} value
|
|
27
|
+
* @param {(val: string) => void} onChange
|
|
28
|
+
* @param {string} [placeholder]
|
|
29
|
+
* @returns {any}
|
|
30
|
+
*/
|
|
31
|
+
export function renderTextInput(prop, value, onChange, placeholder = "") {
|
|
32
|
+
return html`
|
|
33
|
+
<sp-textfield
|
|
34
|
+
size="s"
|
|
35
|
+
placeholder=${placeholder}
|
|
36
|
+
.value=${live(value || "")}
|
|
37
|
+
@input=${debouncedStyleCommit(`text:${prop}`, 400, (/** @type {any} */ e) =>
|
|
38
|
+
onChange(e.target.value),
|
|
39
|
+
)}
|
|
40
|
+
></sp-textfield>
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render a number input widget (sp-number-field).
|
|
46
|
+
*
|
|
47
|
+
* @param {any} entry
|
|
48
|
+
* @param {string} prop
|
|
49
|
+
* @param {any} value
|
|
50
|
+
* @param {(val: any) => void} onChange
|
|
51
|
+
* @returns {any}
|
|
52
|
+
*/
|
|
53
|
+
export function renderNumberInput(entry, prop, value, onChange, placeholder = "") {
|
|
54
|
+
return html`
|
|
55
|
+
<sp-number-field
|
|
56
|
+
size="s"
|
|
57
|
+
hide-stepper
|
|
58
|
+
.value=${live(value !== undefined && value !== "" ? Number(value) : undefined)}
|
|
59
|
+
placeholder=${placeholder}
|
|
60
|
+
min=${ifDefined(entry.minimum)}
|
|
61
|
+
max=${ifDefined(entry.maximum)}
|
|
62
|
+
step=${ifDefined(entry.maximum !== undefined && entry.maximum <= 1 ? 0.1 : undefined)}
|
|
63
|
+
@change=${debouncedStyleCommit(`num:${prop}`, 400, (/** @type {any} */ e) => {
|
|
64
|
+
const v = e.target.value;
|
|
65
|
+
if (v === undefined || isNaN(v)) onChange("");
|
|
66
|
+
else onChange(Number(v));
|
|
67
|
+
})}
|
|
68
|
+
></sp-number-field>
|
|
69
|
+
`;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Dispatch to the appropriate widget based on inferred type.
|
|
74
|
+
*
|
|
75
|
+
* @param {string} type — one of: button-group, color, number-unit, number, select, combobox, text
|
|
76
|
+
* @param {any} entry — css-meta or schema entry
|
|
77
|
+
* @param {string} prop — property key
|
|
78
|
+
* @param {any} value — current value
|
|
79
|
+
* @param {(val: any) => void} onCommit — commit callback
|
|
80
|
+
* @param {{ placeholder?: string; renderSelect?: Function; renderCombobox?: Function }} [opts]
|
|
81
|
+
* @returns {any}
|
|
82
|
+
*/
|
|
83
|
+
export function widgetForType(type, entry, prop, value, onCommit, opts = {}) {
|
|
84
|
+
switch (type) {
|
|
85
|
+
case "button-group":
|
|
86
|
+
return renderButtonGroup(entry, prop, value, onCommit);
|
|
87
|
+
case "color":
|
|
88
|
+
return renderColorSelector(prop, value, onCommit);
|
|
89
|
+
case "number-unit":
|
|
90
|
+
return renderUnitSelector(entry, prop, value, onCommit, opts.placeholder);
|
|
91
|
+
case "number":
|
|
92
|
+
return renderNumberInput(entry, prop, value, onCommit, opts.placeholder);
|
|
93
|
+
case "media":
|
|
94
|
+
return renderMediaPicker(prop, value, onCommit);
|
|
95
|
+
case "select":
|
|
96
|
+
// Allow caller to override select rendering (e.g. for typography preview)
|
|
97
|
+
if (opts.renderSelect) return opts.renderSelect(entry, prop, value, onCommit);
|
|
98
|
+
return renderTextInput(prop, value, onCommit, opts.placeholder);
|
|
99
|
+
case "combobox":
|
|
100
|
+
// Allow caller to override combobox rendering (e.g. for font family)
|
|
101
|
+
if (opts.renderCombobox) return opts.renderCombobox(entry, prop, value, onCommit);
|
|
102
|
+
return renderTextInput(prop, value, onCommit, opts.placeholder);
|
|
103
|
+
default:
|
|
104
|
+
return renderTextInput(prop, value, onCommit, opts.placeholder);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inherited-style.js — Computes the effective inherited style for a given breakpoint tab.
|
|
3
|
+
*
|
|
4
|
+
* Walks the cascade (base → each media block in order) up to but not including the active
|
|
5
|
+
* breakpoint, producing the set of property values that would apply if no explicit override exists
|
|
6
|
+
* on the current tab.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Compute the inherited style object for a given breakpoint tab.
|
|
11
|
+
*
|
|
12
|
+
* @param {Record<string, any>} style — full style object (flat props + @media blocks + selectors)
|
|
13
|
+
* @param {string[]} mediaNames — ordered breakpoint names (from parseMediaEntries, respects cascade
|
|
14
|
+
* direction)
|
|
15
|
+
* @param {string | null} activeTab — current breakpoint tab name, or null for base
|
|
16
|
+
* @param {string | null} activeSelector — current nested selector, or null
|
|
17
|
+
* @returns {Record<string, any>} Inherited style map (prop → value)
|
|
18
|
+
*/
|
|
19
|
+
export function computeInheritedStyle(style, mediaNames, activeTab, activeSelector = null) {
|
|
20
|
+
if (activeTab === null || mediaNames.length === 0) return {};
|
|
21
|
+
|
|
22
|
+
/** @type {Record<string, any>} */
|
|
23
|
+
let inherited = {};
|
|
24
|
+
|
|
25
|
+
if (!activeSelector) {
|
|
26
|
+
// Start with base flat props
|
|
27
|
+
for (const [p, v] of Object.entries(style)) {
|
|
28
|
+
if (typeof v !== "object") inherited[p] = v;
|
|
29
|
+
}
|
|
30
|
+
// Layer each media block in order until current tab
|
|
31
|
+
for (const name of mediaNames) {
|
|
32
|
+
if (name === activeTab) break;
|
|
33
|
+
const block = style[`@${name}`] || {};
|
|
34
|
+
for (const [p, v] of Object.entries(block)) {
|
|
35
|
+
if (typeof v !== "object") inherited[p] = v;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} else {
|
|
39
|
+
// Selector inheritance: base selector → each media's selector block in order
|
|
40
|
+
const baseSel = style[activeSelector] || {};
|
|
41
|
+
for (const [p, v] of Object.entries(baseSel)) {
|
|
42
|
+
if (typeof v !== "object") inherited[p] = v;
|
|
43
|
+
}
|
|
44
|
+
for (const name of mediaNames) {
|
|
45
|
+
if (name === activeTab) break;
|
|
46
|
+
const selBlock = (style[`@${name}`] || {})[activeSelector] || {};
|
|
47
|
+
for (const [p, v] of Object.entries(selBlock)) {
|
|
48
|
+
if (typeof v !== "object") inherited[p] = v;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return inherited;
|
|
54
|
+
}
|
|
@@ -112,7 +112,9 @@ export function abbreviateValue(val) {
|
|
|
112
112
|
export function inferInputType(entry) {
|
|
113
113
|
if (entry.$shorthand === true) return "shorthand";
|
|
114
114
|
if (entry.$input === "button-group") return "button-group";
|
|
115
|
+
if (entry.$input === "media") return "media";
|
|
115
116
|
if (entry.format === "color") return "color";
|
|
117
|
+
if (entry.format === "uri-reference") return "media";
|
|
116
118
|
if (entry.$units !== undefined) return "number-unit";
|
|
117
119
|
if (entry.type === "number") return "number";
|
|
118
120
|
if (Array.isArray(entry.enum)) return "select";
|
|
@@ -120,6 +122,36 @@ export function inferInputType(entry) {
|
|
|
120
122
|
return "text";
|
|
121
123
|
}
|
|
122
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Match a document path to a content collection and return its schema. Uses simple directory-prefix
|
|
127
|
+
* + extension matching against the collection's `source` glob.
|
|
128
|
+
*
|
|
129
|
+
* @param {string | null} documentPath — project-relative path (e.g. "blog/hello.md")
|
|
130
|
+
* @param {any} projectConfig — parsed project.json
|
|
131
|
+
* @returns {{ name: string; schema: any } | null}
|
|
132
|
+
*/
|
|
133
|
+
export function findCollectionSchema(documentPath, projectConfig) {
|
|
134
|
+
if (!documentPath || !projectConfig?.collections) return null;
|
|
135
|
+
for (const [name, def] of Object.entries(
|
|
136
|
+
/** @type {Record<string, any>} */ (projectConfig.collections),
|
|
137
|
+
)) {
|
|
138
|
+
if (!def.source || !def.schema) continue;
|
|
139
|
+
const src = def.source.replace(/^\.\//, "");
|
|
140
|
+
const dir = src.split("/")[0];
|
|
141
|
+
const ext = src.includes("*.md")
|
|
142
|
+
? ".md"
|
|
143
|
+
: src.includes("*.json")
|
|
144
|
+
? ".json"
|
|
145
|
+
: src.includes("*.csv")
|
|
146
|
+
? ".csv"
|
|
147
|
+
: null;
|
|
148
|
+
if (documentPath.startsWith(dir + "/") && (!ext || documentPath.endsWith(ext))) {
|
|
149
|
+
return { name, schema: def.schema };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
|
|
123
155
|
/**
|
|
124
156
|
* Convert a human-readable name to a CSS variable name. E.g. "Geometric Humanist" →
|
|
125
157
|
* "--font-geometric-humanist"
|
package/src/view.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View.js — Transient view state for Jx Studio
|
|
3
|
+
*
|
|
4
|
+
* Holds DOM references, editor instances, cleanup functions, and other mutable state that is the
|
|
5
|
+
* OUTPUT of renderers (not the input). Separating this from persistent app state (in S via
|
|
6
|
+
* store.js) makes renderer dependencies explicit.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** @type {any} */
|
|
10
|
+
export const view = {
|
|
11
|
+
// Canvas infrastructure
|
|
12
|
+
panzoomWrap: null,
|
|
13
|
+
liveScope: null,
|
|
14
|
+
renderGeneration: 0,
|
|
15
|
+
centerObserver: null,
|
|
16
|
+
needsCenter: true,
|
|
17
|
+
panX: 0,
|
|
18
|
+
panY: 0,
|
|
19
|
+
prevCanvasMode: null,
|
|
20
|
+
|
|
21
|
+
// Editor instances
|
|
22
|
+
monacoEditor: null,
|
|
23
|
+
functionEditor: null,
|
|
24
|
+
|
|
25
|
+
// Inline editing
|
|
26
|
+
componentInlineEdit: null,
|
|
27
|
+
pendingInlineEdit: null,
|
|
28
|
+
inlineEditCleanup: null,
|
|
29
|
+
|
|
30
|
+
// Floating UI containers
|
|
31
|
+
blockActionBarEl: null,
|
|
32
|
+
linkPopoverHost: null,
|
|
33
|
+
|
|
34
|
+
// Selection & drag
|
|
35
|
+
selDragCleanup: null,
|
|
36
|
+
|
|
37
|
+
// Cleanup arrays (reset on each render cycle)
|
|
38
|
+
dndCleanups: [],
|
|
39
|
+
canvasDndCleanups: [],
|
|
40
|
+
canvasEventCleanups: [],
|
|
41
|
+
|
|
42
|
+
// Pseudo-state preview
|
|
43
|
+
forcedStyleTag: null,
|
|
44
|
+
forcedAttrEl: null,
|
|
45
|
+
};
|