@jxsuite/studio 0.28.2 → 0.28.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/studio.js +86144 -83971
- package/dist/studio.js.map +229 -219
- package/package.json +5 -5
- package/src/browse/browse-modal.ts +9 -3
- package/src/browse/browse.ts +139 -82
- package/src/canvas/canvas-diff.ts +35 -18
- package/src/canvas/canvas-helpers.ts +55 -21
- package/src/canvas/canvas-live-render.ts +140 -89
- package/src/canvas/canvas-render.ts +127 -82
- package/src/canvas/canvas-utils.ts +68 -42
- package/src/canvas/nested-site-style.ts +15 -7
- package/src/editor/component-inline-edit.ts +90 -51
- package/src/editor/content-inline-edit.ts +75 -72
- package/src/editor/context-menu.ts +125 -71
- package/src/editor/convert-targets.ts +17 -13
- package/src/editor/convert-to-component.ts +51 -28
- package/src/editor/convert-to-repeater.ts +44 -19
- package/src/editor/inline-edit.ts +107 -52
- package/src/editor/inline-format.ts +128 -60
- package/src/editor/insertion-helper.ts +26 -14
- package/src/editor/shortcuts.ts +89 -43
- package/src/editor/slash-menu.ts +41 -26
- package/src/files/components.ts +9 -7
- package/src/files/file-ops.ts +53 -33
- package/src/files/files.ts +246 -143
- package/src/format/constraints.ts +25 -15
- package/src/format/format-host.ts +33 -10
- package/src/github/github-auth.ts +24 -14
- package/src/github/github-publish.ts +20 -14
- package/src/new-project/new-project-modal.ts +27 -18
- package/src/panels/activity-bar.ts +27 -26
- package/src/panels/ai-panel.ts +100 -44
- package/src/panels/block-action-bar.ts +84 -52
- package/src/panels/canvas-dnd.ts +47 -28
- package/src/panels/data-explorer.ts +24 -11
- package/src/panels/dnd.ts +148 -100
- package/src/panels/editors.ts +54 -37
- package/src/panels/elements-panel.ts +23 -13
- package/src/panels/events-panel.ts +48 -38
- package/src/panels/git-panel.ts +95 -60
- package/src/panels/head-panel.ts +138 -84
- package/src/panels/imports-panel.ts +66 -29
- package/src/panels/layers-panel.ts +80 -36
- package/src/panels/left-panel.ts +86 -53
- package/src/panels/overlays.ts +36 -19
- package/src/panels/panel-events.ts +48 -27
- package/src/panels/panel-scheduler.ts +41 -23
- package/src/panels/preview-render.ts +26 -26
- package/src/panels/properties-panel.ts +155 -118
- package/src/panels/pseudo-preview.ts +31 -15
- package/src/panels/quick-search.ts +28 -13
- package/src/panels/right-panel.ts +35 -24
- package/src/panels/shared.ts +46 -29
- package/src/panels/signals-panel.ts +303 -186
- package/src/panels/statusbar.ts +27 -12
- package/src/panels/style-inputs.ts +30 -24
- package/src/panels/style-panel.ts +130 -90
- package/src/panels/style-utils.ts +63 -23
- package/src/panels/stylebook-layers-panel.ts +27 -30
- package/src/panels/stylebook-panel.ts +140 -72
- package/src/panels/tab-strip.ts +21 -10
- package/src/panels/toolbar.ts +52 -40
- package/src/panels/welcome-screen.ts +1 -1
- package/src/platform.ts +2 -1
- package/src/platforms/devserver.ts +221 -105
- package/src/recent-projects.ts +15 -7
- package/src/resize-edges.ts +7 -5
- package/src/services/cem-export.ts +73 -57
- package/src/services/code-services.ts +52 -25
- package/src/services/monaco-setup.ts +5 -5
- package/src/settings/content-types-editor.ts +133 -62
- package/src/settings/css-vars-editor.ts +31 -14
- package/src/settings/defs-editor.ts +125 -63
- package/src/settings/general-settings.ts +11 -6
- package/src/settings/head-editor.ts +30 -15
- package/src/settings/schema-field-ui.ts +75 -35
- package/src/settings/settings-modal.ts +35 -17
- package/src/site-context.ts +80 -47
- package/src/state.ts +98 -65
- package/src/store.ts +42 -23
- package/src/studio.ts +165 -144
- package/src/tabs/tab.ts +51 -45
- package/src/tabs/transact.ts +230 -109
- package/src/types.ts +60 -51
- package/src/ui/button-group.ts +3 -2
- package/src/ui/color-selector.ts +33 -15
- package/src/ui/expression-editor.ts +139 -86
- package/src/ui/field-input.ts +35 -18
- package/src/ui/field-row.ts +1 -1
- package/src/ui/icons.ts +5 -6
- package/src/ui/layers.ts +33 -26
- package/src/ui/media-picker.ts +13 -7
- package/src/ui/panel-resize.ts +39 -20
- package/src/ui/spectrum.ts +4 -2
- package/src/ui/unit-selector.ts +20 -11
- package/src/ui/value-selector.ts +13 -7
- package/src/ui/widgets.ts +34 -18
- package/src/utils/canvas-media.ts +66 -33
- package/src/utils/edit-display.ts +43 -44
- package/src/utils/google-fonts.ts +10 -6
- package/src/utils/inherited-style.ts +22 -8
- package/src/utils/studio-utils.ts +88 -45
- package/src/view.ts +9 -5
- package/src/workspace/workspace.ts +49 -29
package/src/ui/layers.ts
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/// <reference lib="dom" />
|
|
2
|
-
import { render as litRender, nothing
|
|
2
|
+
import { html, render as litRender, nothing } from "lit-html";
|
|
3
|
+
import type { TemplateResult } from "lit-html";
|
|
3
4
|
|
|
4
5
|
let _popoverLayer: HTMLElement;
|
|
5
6
|
let _modalLayer: HTMLElement;
|
|
6
7
|
let _dialogLayer: HTMLElement;
|
|
7
8
|
|
|
8
9
|
export function initLayers() {
|
|
9
|
-
_popoverLayer = document.
|
|
10
|
-
_modalLayer = document.
|
|
11
|
-
_dialogLayer = document.
|
|
10
|
+
_popoverLayer = document.querySelector("#layer-popover") as HTMLElement;
|
|
11
|
+
_modalLayer = document.querySelector("#layer-modal") as HTMLElement;
|
|
12
|
+
_dialogLayer = document.querySelector("#layer-dialog") as HTMLElement;
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -19,15 +20,17 @@ export function initLayers() {
|
|
|
19
20
|
* @returns {Promise<T>}
|
|
20
21
|
*/
|
|
21
22
|
export function showDialog<T>(
|
|
22
|
-
templateFn: (done: (value: T) => void) =>
|
|
23
|
+
templateFn: (done: (value: T) => void) => TemplateResult,
|
|
23
24
|
): Promise<T> {
|
|
24
25
|
return new Promise((resolve) => {
|
|
25
26
|
const slot = document.createElement("div");
|
|
26
27
|
slot.style.pointerEvents = "auto";
|
|
27
|
-
_dialogLayer.
|
|
28
|
+
_dialogLayer.append(slot);
|
|
28
29
|
let resolved = false;
|
|
29
30
|
const done = (value: T) => {
|
|
30
|
-
if (resolved)
|
|
31
|
+
if (resolved) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
31
34
|
resolved = true;
|
|
32
35
|
litRender(nothing, slot);
|
|
33
36
|
slot.remove();
|
|
@@ -47,7 +50,7 @@ export function showDialog<T>(
|
|
|
47
50
|
*/
|
|
48
51
|
export function showConfirmDialog(
|
|
49
52
|
headline: string,
|
|
50
|
-
message: string |
|
|
53
|
+
message: string | TemplateResult,
|
|
51
54
|
opts: {
|
|
52
55
|
confirmLabel?: string;
|
|
53
56
|
cancelLabel?: string;
|
|
@@ -80,21 +83,21 @@ export function showConfirmDialog(
|
|
|
80
83
|
*
|
|
81
84
|
* @param {import("lit-html").TemplateResult} template
|
|
82
85
|
*/
|
|
83
|
-
export function openModal(template:
|
|
86
|
+
export function openModal(template: TemplateResult) {
|
|
84
87
|
const slot = document.createElement("div");
|
|
85
88
|
slot.style.pointerEvents = "auto";
|
|
86
|
-
_modalLayer.
|
|
89
|
+
_modalLayer.append(slot);
|
|
87
90
|
litRender(template, slot);
|
|
88
91
|
return {
|
|
89
|
-
host: slot,
|
|
90
|
-
/** @param {import("lit-html").TemplateResult} tpl */
|
|
91
|
-
update(tpl: import("lit-html").TemplateResult) {
|
|
92
|
-
litRender(tpl, slot);
|
|
93
|
-
},
|
|
94
92
|
close() {
|
|
95
93
|
litRender(nothing, slot);
|
|
96
94
|
slot.remove();
|
|
97
95
|
},
|
|
96
|
+
host: slot,
|
|
97
|
+
/** @param {import("lit-html").TemplateResult} tpl */
|
|
98
|
+
update(tpl: TemplateResult) {
|
|
99
|
+
litRender(tpl, slot);
|
|
100
|
+
},
|
|
98
101
|
};
|
|
99
102
|
}
|
|
100
103
|
|
|
@@ -109,7 +112,7 @@ export function openModal(template: import("lit-html").TemplateResult) {
|
|
|
109
112
|
* }} [opts]
|
|
110
113
|
*/
|
|
111
114
|
export function renderPopover(
|
|
112
|
-
template:
|
|
115
|
+
template: TemplateResult,
|
|
113
116
|
opts: {
|
|
114
117
|
dismissOnOutsideClick?: boolean;
|
|
115
118
|
onDismiss?: () => void;
|
|
@@ -120,7 +123,7 @@ export function renderPopover(
|
|
|
120
123
|
slot.style.pointerEvents = "auto";
|
|
121
124
|
const target =
|
|
122
125
|
opts.layer === "modal" ? _modalLayer : opts.layer === "dialog" ? _dialogLayer : _popoverLayer;
|
|
123
|
-
target.
|
|
126
|
+
target.append(slot);
|
|
124
127
|
litRender(template, slot);
|
|
125
128
|
|
|
126
129
|
let outsideClickHandler: ((e: MouseEvent) => void) | null = null;
|
|
@@ -132,16 +135,13 @@ export function renderPopover(
|
|
|
132
135
|
}
|
|
133
136
|
};
|
|
134
137
|
requestAnimationFrame(() => {
|
|
135
|
-
if (outsideClickHandler)
|
|
138
|
+
if (outsideClickHandler) {
|
|
139
|
+
document.addEventListener("mousedown", outsideClickHandler, true);
|
|
140
|
+
}
|
|
136
141
|
});
|
|
137
142
|
}
|
|
138
143
|
|
|
139
144
|
const handle = {
|
|
140
|
-
host: slot,
|
|
141
|
-
/** @param {import("lit-html").TemplateResult} tpl */
|
|
142
|
-
update(tpl: import("lit-html").TemplateResult) {
|
|
143
|
-
litRender(tpl, slot);
|
|
144
|
-
},
|
|
145
145
|
dismiss() {
|
|
146
146
|
if (outsideClickHandler) {
|
|
147
147
|
document.removeEventListener("mousedown", outsideClickHandler, true);
|
|
@@ -149,11 +149,16 @@ export function renderPopover(
|
|
|
149
149
|
litRender(nothing, slot);
|
|
150
150
|
slot.remove();
|
|
151
151
|
},
|
|
152
|
+
host: slot,
|
|
153
|
+
/** @param {import("lit-html").TemplateResult} tpl */
|
|
154
|
+
update(tpl: TemplateResult) {
|
|
155
|
+
litRender(tpl, slot);
|
|
156
|
+
},
|
|
152
157
|
};
|
|
153
158
|
return handle;
|
|
154
159
|
}
|
|
155
160
|
|
|
156
|
-
const _namedSlots
|
|
161
|
+
const _namedSlots = new Map<string, HTMLElement>();
|
|
157
162
|
|
|
158
163
|
/**
|
|
159
164
|
* Get or create a named slot in a layer. Useful for persistent popovers like zoom indicator.
|
|
@@ -165,14 +170,16 @@ const _namedSlots: Map<string, HTMLElement> = new Map();
|
|
|
165
170
|
export function getLayerSlot(layer: "popover" | "modal" | "dialog", id: string) {
|
|
166
171
|
const key = `${layer}:${id}`;
|
|
167
172
|
let slot = _namedSlots.get(key);
|
|
168
|
-
if (slot && slot.parentElement)
|
|
173
|
+
if (slot && slot.parentElement) {
|
|
174
|
+
return slot;
|
|
175
|
+
}
|
|
169
176
|
|
|
170
177
|
slot = document.createElement("div");
|
|
171
178
|
slot.style.pointerEvents = "auto";
|
|
172
179
|
const target =
|
|
173
180
|
(layer === "popover" ? _popoverLayer : layer === "modal" ? _modalLayer : _dialogLayer) ||
|
|
174
181
|
document.body;
|
|
175
|
-
target.
|
|
182
|
+
target.append(slot);
|
|
176
183
|
_namedSlots.set(key, slot);
|
|
177
184
|
return slot;
|
|
178
185
|
}
|
package/src/ui/media-picker.ts
CHANGED
|
@@ -64,9 +64,9 @@ async function collectMedia(
|
|
|
64
64
|
const ext = dot > 0 ? entry.name.slice(dot).toLowerCase() : "";
|
|
65
65
|
if (MEDIA_EXTENSIONS.has(ext)) {
|
|
66
66
|
results.push({
|
|
67
|
-
path: `/${entry.path}`,
|
|
68
|
-
name: entry.name,
|
|
69
67
|
isImage: IMAGE_EXTENSIONS.has(ext),
|
|
68
|
+
name: entry.name,
|
|
69
|
+
path: `/${entry.path}`,
|
|
70
70
|
});
|
|
71
71
|
}
|
|
72
72
|
}
|
|
@@ -78,7 +78,9 @@ async function collectMedia(
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
async function loadMediaCache() {
|
|
81
|
-
if (mediaCacheLoaded)
|
|
81
|
+
if (mediaCacheLoaded) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
82
84
|
const platform = getPlatform();
|
|
83
85
|
mediaCache = await collectMedia("public", platform);
|
|
84
86
|
// Strip "public/" prefix so paths match production (public/ contents served at root)
|
|
@@ -137,7 +139,9 @@ function onPopoverOutsideClick(e: MouseEvent) {
|
|
|
137
139
|
function renderMediaPickerPopover() {
|
|
138
140
|
const host = getLayerSlot("popover", "media-picker");
|
|
139
141
|
const rect = _popoverAnchorEl?.getBoundingClientRect();
|
|
140
|
-
if (!rect)
|
|
142
|
+
if (!rect) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
141
145
|
|
|
142
146
|
const query = _popoverFilter.toLowerCase();
|
|
143
147
|
const filtered = query
|
|
@@ -148,7 +152,7 @@ function renderMediaPickerPopover() {
|
|
|
148
152
|
const options = filtered.slice(0, 50);
|
|
149
153
|
|
|
150
154
|
// Compute initial position below the anchor
|
|
151
|
-
let left = rect
|
|
155
|
+
let { left } = rect;
|
|
152
156
|
let top = rect.bottom + 4;
|
|
153
157
|
|
|
154
158
|
// Estimate popover dimensions for viewport clamping before first paint
|
|
@@ -195,7 +199,7 @@ function renderMediaPickerPopover() {
|
|
|
195
199
|
dismissMediaPickerPopover();
|
|
196
200
|
}}
|
|
197
201
|
>
|
|
198
|
-
${options.length
|
|
202
|
+
${options.length > 0
|
|
199
203
|
? options.map(
|
|
200
204
|
(m) => html`
|
|
201
205
|
<sp-menu-item value=${m.path}>
|
|
@@ -252,7 +256,9 @@ function renderMediaPickerPopover() {
|
|
|
252
256
|
}
|
|
253
257
|
}
|
|
254
258
|
|
|
255
|
-
if (_popoverFilterEl)
|
|
259
|
+
if (_popoverFilterEl) {
|
|
260
|
+
_popoverFilterEl.focus();
|
|
261
|
+
}
|
|
256
262
|
});
|
|
257
263
|
}
|
|
258
264
|
|
package/src/ui/panel-resize.ts
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
* localStorage so they survive page reloads.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
9
|
+
import { applyPanelCollapse, view } from "../view";
|
|
10
10
|
|
|
11
11
|
const STORAGE_KEY = "jx-studio-panel-widths";
|
|
12
12
|
const MIN_WIDTH = 160;
|
|
13
|
-
const MAX_RATIO = 0.5; //
|
|
13
|
+
const MAX_RATIO = 0.5; // Max 50% of viewport
|
|
14
14
|
const DEFAULT_LEFT = 240;
|
|
15
15
|
const DEFAULT_RIGHT = 280;
|
|
16
16
|
|
|
@@ -20,13 +20,21 @@ const root = document.documentElement;
|
|
|
20
20
|
|
|
21
21
|
try {
|
|
22
22
|
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || "{}");
|
|
23
|
-
if (saved.left)
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (saved.
|
|
23
|
+
if (saved.left) {
|
|
24
|
+
root.style.setProperty("--panel-w-left", `${saved.left}px`);
|
|
25
|
+
}
|
|
26
|
+
if (saved.right) {
|
|
27
|
+
root.style.setProperty("--panel-w-right", `${saved.right}px`);
|
|
28
|
+
}
|
|
29
|
+
if (saved.leftCollapsed) {
|
|
30
|
+
view.leftPanelCollapsed = true;
|
|
31
|
+
}
|
|
32
|
+
if (saved.rightCollapsed) {
|
|
33
|
+
view.rightPanelCollapsed = true;
|
|
34
|
+
}
|
|
27
35
|
applyPanelCollapse();
|
|
28
36
|
} catch {
|
|
29
|
-
//
|
|
37
|
+
// Ignore
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
// ─── Setup handles ───────────────────────────────────────────────────────────
|
|
@@ -50,17 +58,20 @@ function setupHandle(
|
|
|
50
58
|
try {
|
|
51
59
|
handle.setPointerCapture(e.pointerId);
|
|
52
60
|
} catch {
|
|
53
|
-
/*
|
|
61
|
+
/* Synthetic events */
|
|
54
62
|
}
|
|
55
63
|
handle.classList.add("dragging");
|
|
56
64
|
document.body.style.userSelect = "none";
|
|
57
65
|
|
|
58
|
-
const current =
|
|
59
|
-
|
|
66
|
+
const current =
|
|
67
|
+
Number.parseInt(getComputedStyle(root).getPropertyValue(cssVar), 10) || defaultWidth;
|
|
68
|
+
drag = { startWidth: current, startX: e.clientX };
|
|
60
69
|
});
|
|
61
70
|
|
|
62
71
|
handle.addEventListener("pointermove", (e) => {
|
|
63
|
-
if (!drag)
|
|
72
|
+
if (!drag) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
64
75
|
const delta = side === "left" ? e.clientX - drag.startX : drag.startX - e.clientX;
|
|
65
76
|
const maxWidth = window.innerWidth * MAX_RATIO;
|
|
66
77
|
const newWidth = Math.round(Math.min(maxWidth, Math.max(MIN_WIDTH, drag.startWidth + delta)));
|
|
@@ -68,12 +79,14 @@ function setupHandle(
|
|
|
68
79
|
});
|
|
69
80
|
|
|
70
81
|
handle.addEventListener("pointerup", (e) => {
|
|
71
|
-
if (!drag)
|
|
82
|
+
if (!drag) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
72
85
|
drag = null;
|
|
73
86
|
try {
|
|
74
87
|
handle.releasePointerCapture(e.pointerId);
|
|
75
88
|
} catch {
|
|
76
|
-
/*
|
|
89
|
+
/* Synthetic events */
|
|
77
90
|
}
|
|
78
91
|
handle.classList.remove("dragging");
|
|
79
92
|
document.body.style.userSelect = "";
|
|
@@ -87,20 +100,26 @@ function setupHandle(
|
|
|
87
100
|
}
|
|
88
101
|
|
|
89
102
|
function persistWidths() {
|
|
90
|
-
const left =
|
|
103
|
+
const left =
|
|
104
|
+
Number.parseInt(getComputedStyle(root).getPropertyValue("--panel-w-left"), 10) || DEFAULT_LEFT;
|
|
91
105
|
const right =
|
|
92
|
-
parseInt(getComputedStyle(root).getPropertyValue("--panel-w-right")) ||
|
|
106
|
+
Number.parseInt(getComputedStyle(root).getPropertyValue("--panel-w-right"), 10) ||
|
|
107
|
+
DEFAULT_RIGHT;
|
|
93
108
|
try {
|
|
94
109
|
localStorage.setItem(STORAGE_KEY, JSON.stringify({ left, right }));
|
|
95
110
|
} catch {
|
|
96
|
-
//
|
|
111
|
+
// Storage full or unavailable
|
|
97
112
|
}
|
|
98
113
|
}
|
|
99
114
|
|
|
100
115
|
// ─── Initialize ──────────────────────────────────────────────────────────────
|
|
101
116
|
|
|
102
|
-
const resizeLeft = document.
|
|
103
|
-
const resizeRight = document.
|
|
117
|
+
const resizeLeft = document.querySelector<HTMLElement>("#resize-left");
|
|
118
|
+
const resizeRight = document.querySelector<HTMLElement>("#resize-right");
|
|
104
119
|
|
|
105
|
-
if (resizeLeft)
|
|
106
|
-
|
|
120
|
+
if (resizeLeft) {
|
|
121
|
+
setupHandle(resizeLeft, "--panel-w-left", "left", DEFAULT_LEFT);
|
|
122
|
+
}
|
|
123
|
+
if (resizeRight) {
|
|
124
|
+
setupHandle(resizeRight, "--panel-w-right", "right", DEFAULT_RIGHT);
|
|
125
|
+
}
|
package/src/ui/spectrum.ts
CHANGED
|
@@ -148,7 +148,7 @@ import { JxColorPopover } from "./color-selector";
|
|
|
148
148
|
import { IconChevron100 } from "@spectrum-web-components/icons-ui/src/elements/IconChevron100.js";
|
|
149
149
|
|
|
150
150
|
// Register all components. Using defineElement from Spectrum's base package
|
|
151
|
-
//
|
|
151
|
+
// Ensures duplicate registration is handled gracefully.
|
|
152
152
|
import { defineElement } from "@spectrum-web-components/base/src/define-element.js";
|
|
153
153
|
|
|
154
154
|
const components = [
|
|
@@ -283,7 +283,9 @@ const components = [
|
|
|
283
283
|
];
|
|
284
284
|
|
|
285
285
|
for (const [tag, ctor] of components as [string, CustomElementConstructor][]) {
|
|
286
|
-
if (!customElements.get(tag))
|
|
286
|
+
if (!customElements.get(tag)) {
|
|
287
|
+
defineElement(tag, ctor);
|
|
288
|
+
}
|
|
287
289
|
}
|
|
288
290
|
|
|
289
291
|
// Register theme fragments (these are also side-effect-only in the original modules)
|
package/src/ui/unit-selector.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
import { html, nothing } from "lit-html";
|
|
10
10
|
import { live } from "lit-html/directives/live.js";
|
|
11
11
|
import { classMap } from "lit-html/directives/class-map.js";
|
|
12
|
-
import {
|
|
12
|
+
import { cancelStyleDebounce, debouncedStyleCommit } from "../store";
|
|
13
13
|
|
|
14
14
|
export const UNIT_RE = /^(-?[\d.]+)(px|rem|em|%|vw|vh|svw|svh|dvh|ms|s|fr|ch|ex|deg)?$/;
|
|
15
15
|
|
|
@@ -28,7 +28,7 @@ export function renderUnitSelector(
|
|
|
28
28
|
/** @type {string} */ prop: string,
|
|
29
29
|
/** @type {string | number | undefined} */ value: string | number | undefined,
|
|
30
30
|
/** @type {(val: string) => void} */ onChange: (val: string) => void,
|
|
31
|
-
/** @type {string} */ placeholder
|
|
31
|
+
/** @type {string} */ placeholder = "",
|
|
32
32
|
) {
|
|
33
33
|
const units = (entry.$units || []) as string[];
|
|
34
34
|
const keywords = (entry.$keywords || []) as string[];
|
|
@@ -39,12 +39,16 @@ export function renderUnitSelector(
|
|
|
39
39
|
|
|
40
40
|
const currentUnit = isKeyword ? units[0] || "" : match ? match[2] || "" : units[0] || "";
|
|
41
41
|
let displayValue;
|
|
42
|
-
if (isKeyword)
|
|
43
|
-
|
|
44
|
-
else if (
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
if (isKeyword) {
|
|
43
|
+
displayValue = strVal;
|
|
44
|
+
} else if (match) {
|
|
45
|
+
[, displayValue] = match;
|
|
46
|
+
} else if (strVal !== "") {
|
|
47
|
+
const num = Number.parseFloat(strVal);
|
|
48
|
+
displayValue = Number.isNaN(num) ? strVal : String(num);
|
|
49
|
+
} else {
|
|
50
|
+
displayValue = "";
|
|
51
|
+
}
|
|
48
52
|
|
|
49
53
|
// Parse placeholder so inherited values display as "500" not "500px"
|
|
50
54
|
const placeholderMatch = placeholder.match(UNIT_RE);
|
|
@@ -60,8 +64,11 @@ export function renderUnitSelector(
|
|
|
60
64
|
onChange("");
|
|
61
65
|
return;
|
|
62
66
|
}
|
|
63
|
-
if (isNumericVal(val))
|
|
64
|
-
|
|
67
|
+
if (isNumericVal(val)) {
|
|
68
|
+
onChange(units.length > 0 ? val + currentUnit : val);
|
|
69
|
+
} else {
|
|
70
|
+
onChange(val);
|
|
71
|
+
}
|
|
65
72
|
};
|
|
66
73
|
|
|
67
74
|
return html`
|
|
@@ -100,7 +107,9 @@ export function renderUnitSelector(
|
|
|
100
107
|
} else if (units.includes(chosen)) {
|
|
101
108
|
const curMatch = String(value ?? "").match(UNIT_RE);
|
|
102
109
|
const numPart = curMatch ? curMatch[1] : "";
|
|
103
|
-
if (numPart)
|
|
110
|
+
if (numPart) {
|
|
111
|
+
onChange(numPart + chosen);
|
|
112
|
+
}
|
|
104
113
|
}
|
|
105
114
|
}}
|
|
106
115
|
>
|
package/src/ui/value-selector.ts
CHANGED
|
@@ -23,10 +23,10 @@ type ComboOption = { value: string; label: string; style?: string } | { divider:
|
|
|
23
23
|
|
|
24
24
|
export class JxValueSelector extends LitElement {
|
|
25
25
|
static properties = {
|
|
26
|
-
|
|
26
|
+
options: { attribute: false },
|
|
27
27
|
placeholder: { type: String },
|
|
28
28
|
size: { type: String },
|
|
29
|
-
|
|
29
|
+
value: { type: String },
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
declare value: string;
|
|
@@ -41,7 +41,7 @@ export class JxValueSelector extends LitElement {
|
|
|
41
41
|
this.placeholder = "";
|
|
42
42
|
this.size = "s";
|
|
43
43
|
this.options = [];
|
|
44
|
-
this._menuId =
|
|
44
|
+
this._menuId = `jx-combo-${Math.random().toString(36).slice(2, 8)}`;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
/** No shadow DOM — render directly into light DOM */
|
|
@@ -52,14 +52,16 @@ export class JxValueSelector extends LitElement {
|
|
|
52
52
|
/** Check if current value matches a predefined option */
|
|
53
53
|
get _isPicker() {
|
|
54
54
|
return (
|
|
55
|
-
|
|
55
|
+
Boolean(this.value) &&
|
|
56
56
|
this.options.some((o: ComboOption) => !("divider" in o) && o.value === this.value)
|
|
57
57
|
);
|
|
58
58
|
}
|
|
59
59
|
|
|
60
60
|
/** Get the selected option's style string for the picker button preview */
|
|
61
61
|
get _selectedStyle() {
|
|
62
|
-
if (!this._isPicker)
|
|
62
|
+
if (!this._isPicker) {
|
|
63
|
+
return "";
|
|
64
|
+
}
|
|
63
65
|
const opt = this.options.find((o: ComboOption) => !("divider" in o) && o.value === this.value);
|
|
64
66
|
return (opt as { value: string; label: string; style?: string } | undefined)?.style || "";
|
|
65
67
|
}
|
|
@@ -85,7 +87,9 @@ export class JxValueSelector extends LitElement {
|
|
|
85
87
|
/** Combobox mode: sp-menu @change handler */
|
|
86
88
|
_handleMenuChange(e: Event) {
|
|
87
89
|
e.stopPropagation();
|
|
88
|
-
if (!(e.target as HTMLInputElement).value)
|
|
90
|
+
if (!(e.target as HTMLInputElement).value) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
89
93
|
this.value = (e.target as HTMLInputElement).value;
|
|
90
94
|
this.dispatchEvent(new Event("change", { bubbles: true, composed: true }));
|
|
91
95
|
}
|
|
@@ -102,7 +106,9 @@ export class JxValueSelector extends LitElement {
|
|
|
102
106
|
const group = this.querySelector(".jx-combobox-group");
|
|
103
107
|
const w = group ? (group as HTMLElement).offsetWidth : 0;
|
|
104
108
|
const popover = (e.target as HTMLElement).querySelector("sp-popover");
|
|
105
|
-
if (popover && w)
|
|
109
|
+
if (popover && w) {
|
|
110
|
+
(popover as HTMLElement).style.minWidth = `${w}px`;
|
|
111
|
+
}
|
|
106
112
|
}
|
|
107
113
|
|
|
108
114
|
render() {
|
package/src/ui/widgets.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { renderColorSelector } from "./color-selector";
|
|
|
19
19
|
import { renderUnitSelector } from "./unit-selector";
|
|
20
20
|
import { renderButtonGroup } from "./button-group";
|
|
21
21
|
import { renderMediaPicker } from "./media-picker";
|
|
22
|
+
import type { TemplateResult } from "lit-html";
|
|
22
23
|
|
|
23
24
|
/**
|
|
24
25
|
* Render a plain text input widget.
|
|
@@ -33,7 +34,7 @@ export function renderTextInput(
|
|
|
33
34
|
prop: string,
|
|
34
35
|
value: string | number | undefined,
|
|
35
36
|
onChange: (val: string) => void,
|
|
36
|
-
placeholder
|
|
37
|
+
placeholder = "",
|
|
37
38
|
) {
|
|
38
39
|
return html`
|
|
39
40
|
<sp-textfield
|
|
@@ -62,7 +63,7 @@ export function renderNumberInput(
|
|
|
62
63
|
prop: string,
|
|
63
64
|
value: string | number | undefined,
|
|
64
65
|
onChange: (val: string | number) => void,
|
|
65
|
-
placeholder
|
|
66
|
+
placeholder = "",
|
|
66
67
|
) {
|
|
67
68
|
const minimum = entry.minimum as number | undefined;
|
|
68
69
|
const maximum = entry.maximum as number | undefined;
|
|
@@ -77,8 +78,11 @@ export function renderNumberInput(
|
|
|
77
78
|
step=${ifDefined(maximum !== undefined && maximum <= 1 ? 0.1 : undefined)}
|
|
78
79
|
@change=${debouncedStyleCommit(`num:${prop}`, 400, (e: Event) => {
|
|
79
80
|
const v = (e.target as HTMLInputElement & { value: number | undefined }).value;
|
|
80
|
-
if (v === undefined || isNaN(v))
|
|
81
|
-
|
|
81
|
+
if (v === undefined || Number.isNaN(v)) {
|
|
82
|
+
onChange("");
|
|
83
|
+
} else {
|
|
84
|
+
onChange(Number(v));
|
|
85
|
+
}
|
|
82
86
|
})}
|
|
83
87
|
></sp-number-field>
|
|
84
88
|
`;
|
|
@@ -120,37 +124,49 @@ export function widgetForType(
|
|
|
120
124
|
renderSelect?: (
|
|
121
125
|
entry: Record<string, unknown>,
|
|
122
126
|
prop: string,
|
|
123
|
-
value:
|
|
127
|
+
value: string | number | undefined,
|
|
124
128
|
onCommit: (val: string | number) => void,
|
|
125
|
-
) =>
|
|
129
|
+
) => TemplateResult;
|
|
126
130
|
renderCombobox?: (
|
|
127
131
|
entry: Record<string, unknown>,
|
|
128
132
|
prop: string,
|
|
129
|
-
value:
|
|
133
|
+
value: string | number | undefined,
|
|
130
134
|
onCommit: (val: string | number) => void,
|
|
131
|
-
) =>
|
|
135
|
+
) => TemplateResult;
|
|
132
136
|
} = {},
|
|
133
137
|
) {
|
|
134
138
|
switch (type) {
|
|
135
|
-
case "button-group":
|
|
139
|
+
case "button-group": {
|
|
136
140
|
return renderButtonGroup(entry, prop, value, onCommit);
|
|
137
|
-
|
|
141
|
+
}
|
|
142
|
+
case "color": {
|
|
138
143
|
return renderColorSelector(prop, value, onCommit);
|
|
139
|
-
|
|
144
|
+
}
|
|
145
|
+
case "number-unit": {
|
|
140
146
|
return renderUnitSelector(entry, prop, value, onCommit, opts.placeholder);
|
|
141
|
-
|
|
147
|
+
}
|
|
148
|
+
case "number": {
|
|
142
149
|
return renderNumberInput(entry, prop, value, onCommit, opts.placeholder);
|
|
143
|
-
|
|
150
|
+
}
|
|
151
|
+
case "media": {
|
|
144
152
|
return renderMediaPicker(prop, String(value ?? ""), onCommit);
|
|
145
|
-
|
|
153
|
+
}
|
|
154
|
+
case "select": {
|
|
146
155
|
// Allow caller to override select rendering (e.g. for typography preview)
|
|
147
|
-
if (opts.renderSelect)
|
|
156
|
+
if (opts.renderSelect) {
|
|
157
|
+
return opts.renderSelect(entry, prop, value, onCommit);
|
|
158
|
+
}
|
|
148
159
|
return renderTextInput(prop, value, onCommit, opts.placeholder);
|
|
149
|
-
|
|
160
|
+
}
|
|
161
|
+
case "combobox": {
|
|
150
162
|
// Allow caller to override combobox rendering (e.g. for font family)
|
|
151
|
-
if (opts.renderCombobox)
|
|
163
|
+
if (opts.renderCombobox) {
|
|
164
|
+
return opts.renderCombobox(entry, prop, value, onCommit);
|
|
165
|
+
}
|
|
152
166
|
return renderTextInput(prop, value, onCommit, opts.placeholder);
|
|
153
|
-
|
|
167
|
+
}
|
|
168
|
+
default: {
|
|
154
169
|
return renderTextInput(prop, value, onCommit, opts.placeholder);
|
|
170
|
+
}
|
|
155
171
|
}
|
|
156
172
|
}
|