@jxsuite/studio 0.1.0 → 0.5.1
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 +50941 -34749
- package/dist/studio.js.map +461 -345
- package/package.json +46 -35
- 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 +133 -0
- package/src/panels/right-panel.js +130 -0
- package/src/panels/shared.js +41 -0
- package/src/panels/signals-panel.js +95 -94
- package/src/panels/statusbar.js +15 -1
- package/src/panels/toolbar.js +223 -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 +112 -41
- package/src/studio.js +1551 -1565
- 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/canvas-media.js +151 -0
- package/src/utils/inherited-style.js +54 -0
- package/src/utils/studio-utils.js +32 -0
- package/src/view.js +68 -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,151 @@
|
|
|
1
|
+
/** Canvas media/breakpoint utilities — pure functions extracted for testability. */
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Classify $media entries into size breakpoints (get a canvas each) and feature queries (rendered
|
|
5
|
+
* as toolbar toggles).
|
|
6
|
+
*
|
|
7
|
+
* @param {Record<string, string> | null | undefined} mediaDef
|
|
8
|
+
* @returns {{
|
|
9
|
+
* sizeBreakpoints: { name: string; query: string; width: number; type: string }[];
|
|
10
|
+
* featureQueries: { name: string; query: string }[];
|
|
11
|
+
* baseWidth: number;
|
|
12
|
+
* }}
|
|
13
|
+
*/
|
|
14
|
+
export function parseMediaEntries(mediaDef) {
|
|
15
|
+
if (!mediaDef) return { sizeBreakpoints: [], featureQueries: [], baseWidth: 320 };
|
|
16
|
+
const sizes = [],
|
|
17
|
+
features = [];
|
|
18
|
+
let baseWidth = 320;
|
|
19
|
+
for (const [name, query] of Object.entries(mediaDef)) {
|
|
20
|
+
if (name === "--") {
|
|
21
|
+
const wm = String(query).match(/^(\d+)\s*px$/);
|
|
22
|
+
baseWidth = wm ? parseFloat(wm[1]) : 320;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const minMatch = query.match(/min-width:\s*([\d.]+)px/);
|
|
26
|
+
const maxMatch = query.match(/max-width:\s*([\d.]+)px/);
|
|
27
|
+
if (minMatch) sizes.push({ name, query, width: parseFloat(minMatch[1]), type: "min" });
|
|
28
|
+
else if (maxMatch) sizes.push({ name, query, width: parseFloat(maxMatch[1]), type: "max" });
|
|
29
|
+
else features.push({ name, query });
|
|
30
|
+
}
|
|
31
|
+
sizes.sort((a, b) => (a.type === "min" ? a.width - b.width : b.width - a.width));
|
|
32
|
+
return { sizeBreakpoints: sizes, featureQueries: features, baseWidth };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Compute which named breakpoints are active at a given canvas width.
|
|
37
|
+
*
|
|
38
|
+
* @param {{ name: string; width: number; type: string }[]} sizeBreakpoints
|
|
39
|
+
* @param {number} canvasWidth
|
|
40
|
+
* @returns {Set<string>}
|
|
41
|
+
*/
|
|
42
|
+
export function activeBreakpointsForWidth(sizeBreakpoints, canvasWidth) {
|
|
43
|
+
const active = new Set();
|
|
44
|
+
for (const bp of sizeBreakpoints) {
|
|
45
|
+
if (bp.type === "min" && canvasWidth >= bp.width) active.add(bp.name);
|
|
46
|
+
else if (bp.type === "max" && canvasWidth <= bp.width) active.add(bp.name);
|
|
47
|
+
}
|
|
48
|
+
return active;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Apply styles to a canvas element, including active media overrides. Base (flat) styles applied
|
|
53
|
+
* first, then matching media overrides in source order.
|
|
54
|
+
*
|
|
55
|
+
* @param {HTMLElement} el
|
|
56
|
+
* @param {Record<string, any>} styleDef
|
|
57
|
+
* @param {Set<string>} activeBreakpoints
|
|
58
|
+
* @param {Record<string, boolean>} featureToggles
|
|
59
|
+
*/
|
|
60
|
+
export function applyCanvasStyle(el, styleDef, activeBreakpoints, featureToggles) {
|
|
61
|
+
if (!styleDef || typeof styleDef !== "object") return;
|
|
62
|
+
for (const [prop, val] of Object.entries(styleDef)) {
|
|
63
|
+
if (typeof val === "string" || typeof val === "number") {
|
|
64
|
+
try {
|
|
65
|
+
if (prop.startsWith("--")) el.style.setProperty(prop, String(val));
|
|
66
|
+
else /** @type {any} */ (el.style)[prop] = val;
|
|
67
|
+
} catch {}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
for (const [key, val] of Object.entries(styleDef)) {
|
|
71
|
+
if (!key.startsWith("@") || typeof val !== "object") continue;
|
|
72
|
+
const mediaName = key.slice(1);
|
|
73
|
+
if (mediaName === "--") continue;
|
|
74
|
+
if (activeBreakpoints.has(mediaName) || featureToggles[mediaName]) {
|
|
75
|
+
for (const [prop, v] of Object.entries(/** @type {any} */ (val))) {
|
|
76
|
+
if (typeof v === "string" || typeof v === "number") {
|
|
77
|
+
try {
|
|
78
|
+
if (prop.startsWith("--")) el.style.setProperty(prop, String(v));
|
|
79
|
+
else /** @type {any} */ (el.style)[prop] = v;
|
|
80
|
+
} catch {}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Scan stylesheets for @media rules matching active breakpoints, collecting the CSS declarations
|
|
89
|
+
* that should be applied as inline overrides per data-jx element.
|
|
90
|
+
*
|
|
91
|
+
* Returns a Map of data-jx uid → Map of CSS property → value.
|
|
92
|
+
*
|
|
93
|
+
* @param {Iterable<CSSStyleSheet>} styleSheets
|
|
94
|
+
* @param {Set<string>} activeBreakpoints
|
|
95
|
+
* @returns {Map<string, Map<string, string>>}
|
|
96
|
+
*/
|
|
97
|
+
export function collectMediaOverrides(styleSheets, activeBreakpoints) {
|
|
98
|
+
/** @type {Map<string, Map<string, string>>} */
|
|
99
|
+
const overrides = new Map();
|
|
100
|
+
if (!activeBreakpoints.size) return overrides;
|
|
101
|
+
|
|
102
|
+
for (const sheet of styleSheets) {
|
|
103
|
+
/** @type {CSSRuleList | null} */
|
|
104
|
+
let rules;
|
|
105
|
+
try {
|
|
106
|
+
rules = sheet.cssRules;
|
|
107
|
+
} catch {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!rules) continue;
|
|
111
|
+
for (let ri = 0; ri < rules.length; ri++) {
|
|
112
|
+
const rule = rules[ri];
|
|
113
|
+
if (!(rule instanceof CSSMediaRule)) continue;
|
|
114
|
+
if (!activeBreakpoints.has(rule.conditionText)) continue;
|
|
115
|
+
for (let mi = 0; mi < rule.cssRules.length; mi++) {
|
|
116
|
+
const mediaRule = rule.cssRules[mi];
|
|
117
|
+
if (!(mediaRule instanceof CSSStyleRule)) continue;
|
|
118
|
+
const selector = mediaRule.selectorText;
|
|
119
|
+
const jxMatch = selector.match(/\[data-jx="([^"]+)"\]/);
|
|
120
|
+
if (!jxMatch) continue;
|
|
121
|
+
const uid = jxMatch[1];
|
|
122
|
+
if (!overrides.has(uid)) overrides.set(uid, new Map());
|
|
123
|
+
const props = /** @type {Map<string, string>} */ (overrides.get(uid));
|
|
124
|
+
for (let i = 0; i < mediaRule.style.length; i++) {
|
|
125
|
+
const prop = mediaRule.style[i];
|
|
126
|
+
props.set(prop, mediaRule.style.getPropertyValue(prop));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return overrides;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Apply collected media overrides to elements within a canvas.
|
|
136
|
+
*
|
|
137
|
+
* @param {Element} canvasEl
|
|
138
|
+
* @param {Map<string, Map<string, string>>} overrides
|
|
139
|
+
*/
|
|
140
|
+
export function applyOverridesToCanvas(canvasEl, overrides) {
|
|
141
|
+
for (const [uid, props] of overrides) {
|
|
142
|
+
const els = canvasEl.querySelectorAll(`[data-jx="${uid}"]`);
|
|
143
|
+
for (const el of els) {
|
|
144
|
+
for (const [prop, val] of props) {
|
|
145
|
+
try {
|
|
146
|
+
/** @type {HTMLElement} */ (el).style.setProperty(prop, val);
|
|
147
|
+
} catch {}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|