@jxsuite/studio 0.6.2 → 0.7.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.
@@ -0,0 +1,270 @@
1
+ /**
2
+ * Layers panel — document tree view showing element hierarchy with collapse, selection, move
3
+ * actions, and drag-and-drop reordering.
4
+ */
5
+
6
+ import { html, nothing } from "lit-html";
7
+ import {
8
+ getState,
9
+ update,
10
+ flattenTree,
11
+ getNodeAtPath,
12
+ pathKey,
13
+ pathsEqual,
14
+ parentElementPath,
15
+ childIndex,
16
+ nodeLabel,
17
+ selectNode,
18
+ moveNode,
19
+ removeNode,
20
+ VOID_ELEMENTS,
21
+ } from "../store.js";
22
+ import { view } from "../view.js";
23
+ import { isInlineElement } from "../editor/inline-edit.js";
24
+ import { showContextMenu } from "../editor/context-menu.js";
25
+
26
+ /**
27
+ * @param {{ navigateToComponent: any; rerender: () => void }} ctx
28
+ * @returns {import("lit-html").TemplateResult}
29
+ */
30
+ export function renderLayersTemplate(ctx) {
31
+ const S = getState();
32
+
33
+ for (const fn of view.dndCleanups) fn();
34
+ view.dndCleanups = [];
35
+
36
+ const rows = flattenTree(S.document);
37
+ const collapsed = S._collapsed || (S._collapsed = new Set());
38
+
39
+ /** @type {any[]} */
40
+ const layerRows = [];
41
+ for (const { node, path, depth, nodeType } of rows) {
42
+ let hidden = false;
43
+ for (let d = 1; d <= path.length; d++) {
44
+ const sub = path.slice(0, d);
45
+ if (d < path.length && collapsed.has(pathKey(sub))) {
46
+ hidden = true;
47
+ break;
48
+ }
49
+ }
50
+ if (hidden) continue;
51
+
52
+ if (S.mode === "content" && path.length === 0) continue;
53
+
54
+ if (nodeType === "text") {
55
+ const textPreview = String(node).length > 40 ? String(node).slice(0, 40) + "…" : String(node);
56
+ layerRows.push(html`
57
+ <div
58
+ class="layer-row"
59
+ style="padding-left:${depth * 16 + 8}px; opacity: 0.6; font-style: italic;"
60
+ >
61
+ <span class="layer-tag" style="background: #64748b; font-size: 0.65rem;">text</span>
62
+ <span class="layer-label">${textPreview}</span>
63
+ </div>
64
+ `);
65
+ continue;
66
+ }
67
+
68
+ if (path.length >= 2 && nodeType === "element") {
69
+ const pPath = parentElementPath(path);
70
+ const parentNode = pPath ? getNodeAtPath(S.document, pPath) : null;
71
+ if (parentNode && isInlineElement(node, parentNode)) continue;
72
+ }
73
+
74
+ const key = pathKey(path);
75
+ const isSelected = pathsEqual(path, S.selection);
76
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0;
77
+ const hasMapChildren =
78
+ node.children && typeof node.children === "object" && node.children.$prototype === "Array";
79
+ const hasCases =
80
+ node.$switch &&
81
+ node.cases &&
82
+ typeof node.cases === "object" &&
83
+ Object.keys(node.cases).length > 0;
84
+ const isExpandable =
85
+ hasChildren || hasMapChildren || hasCases || (nodeType === "map" && node.map);
86
+ const isVoidEl = VOID_ELEMENTS.has((node.tagName || "div").toLowerCase());
87
+
88
+ /** @type {any} */
89
+ let badgeClass, badgeText, badgeTitle;
90
+ if (nodeType === "map") {
91
+ badgeClass = "layer-tag map-tag";
92
+ badgeText = "↻";
93
+ badgeTitle = "Repeater (mapped array)";
94
+ } else if (nodeType === "case" || nodeType === "case-ref") {
95
+ badgeClass = "layer-tag case-tag";
96
+ badgeText = path[path.length - 1];
97
+ badgeTitle = `$switch case: ${path[path.length - 1]}`;
98
+ } else if (node.$switch) {
99
+ badgeClass = "layer-tag switch-tag";
100
+ badgeText = "⇄";
101
+ badgeTitle = "$switch";
102
+ } else {
103
+ badgeClass = "layer-tag";
104
+ badgeText = node.tagName || "div";
105
+ badgeTitle = undefined;
106
+ }
107
+
108
+ /** @type {any} */
109
+ let labelText, labelItalic;
110
+ if (nodeType === "case-ref") {
111
+ labelText = node.$ref || "external";
112
+ labelItalic = true;
113
+ } else {
114
+ labelText = nodeLabel(node);
115
+ labelItalic = false;
116
+ }
117
+
118
+ const isElement = nodeType === "element";
119
+ const isRoot = S.mode === "content" ? path.length === 0 : path.length < 2;
120
+ const idx = isElement ? /** @type {number} */ (childIndex(path)) : 0;
121
+ const parentPath = isElement && !isRoot ? /** @type {any} */ (parentElementPath(path)) : null;
122
+ const parentNode = parentPath ? getNodeAtPath(S.document, parentPath) : null;
123
+ const siblingCount = parentNode?.children?.length || 0;
124
+ const canMoveUp = isElement && !isRoot && idx > 0;
125
+ const canMoveDown = isElement && !isRoot && idx < siblingCount - 1;
126
+ const prevSibling = canMoveUp && parentNode ? parentNode.children[idx - 1] : null;
127
+ const canMoveIn =
128
+ isElement &&
129
+ !isRoot &&
130
+ prevSibling &&
131
+ !VOID_ELEMENTS.has((prevSibling.tagName || "div").toLowerCase());
132
+ const grandparentPath =
133
+ isElement && parentPath && parentPath.length >= 2
134
+ ? /** @type {any} */ (parentElementPath(parentPath))
135
+ : null;
136
+ const canMoveOut = isElement && !isRoot && !!grandparentPath;
137
+
138
+ layerRows.push(html`
139
+ <div
140
+ class="layer-row${isSelected ? " selected" : ""}"
141
+ data-path=${key}
142
+ data-dnd-row=${isElement ? key : nothing}
143
+ data-dnd-depth=${isElement ? depth : nothing}
144
+ data-dnd-void=${isElement && isVoidEl ? "" : nothing}
145
+ @click=${() => update(selectNode(getState(), path))}
146
+ @contextmenu=${isElement
147
+ ? (/** @type {any} */ e) =>
148
+ showContextMenu(e, path, getState(), {
149
+ onEditComponent: ctx.navigateToComponent,
150
+ })
151
+ : nothing}
152
+ >
153
+ <span class="layer-indent" style="width:${depth * 16}px"></span>
154
+ <span class="layer-toggle"
155
+ >${isExpandable
156
+ ? html`
157
+ ${collapsed.has(key)
158
+ ? html`<sp-icon-chevron-right></sp-icon-chevron-right>`
159
+ : html`<sp-icon-chevron-down></sp-icon-chevron-down>`}
160
+ `
161
+ : nothing}</span
162
+ >
163
+ <span class=${badgeClass} title=${badgeTitle ?? nothing}>${badgeText}</span>
164
+ <span class="layer-label" style=${labelItalic ? "font-style:italic" : nothing}
165
+ >${labelText}</span
166
+ >
167
+ ${isElement && !isRoot
168
+ ? html`
169
+ <span class="layer-actions">
170
+ ${canMoveUp
171
+ ? html`<sp-action-button
172
+ quiet
173
+ size="xs"
174
+ title="Move up"
175
+ @click=${(/** @type {any} */ e) => {
176
+ e.stopPropagation();
177
+ /** @type {HTMLElement} */ (e.currentTarget).blur();
178
+ update(moveNode(getState(), path, parentPath, idx - 1));
179
+ }}
180
+ >
181
+ <sp-icon-arrow-up slot="icon"></sp-icon-arrow-up>
182
+ </sp-action-button>`
183
+ : nothing}
184
+ ${canMoveDown
185
+ ? html`<sp-action-button
186
+ quiet
187
+ size="xs"
188
+ title="Move down"
189
+ @click=${(/** @type {any} */ e) => {
190
+ e.stopPropagation();
191
+ /** @type {HTMLElement} */ (e.currentTarget).blur();
192
+ update(moveNode(getState(), path, parentPath, idx + 2));
193
+ }}
194
+ >
195
+ <sp-icon-arrow-down slot="icon"></sp-icon-arrow-down>
196
+ </sp-action-button>`
197
+ : nothing}
198
+ ${canMoveIn
199
+ ? html`<sp-action-button
200
+ quiet
201
+ size="xs"
202
+ title="Move into previous sibling"
203
+ @click=${(/** @type {any} */ e) => {
204
+ e.stopPropagation();
205
+ /** @type {HTMLElement} */ (e.currentTarget).blur();
206
+ const prevPath = [...parentPath, idx - 1];
207
+ const prev = getNodeAtPath(getState().document, prevPath);
208
+ const len = prev?.children?.length || 0;
209
+ update(moveNode(getState(), path, prevPath, len));
210
+ }}
211
+ >
212
+ <sp-icon-arrow-right slot="icon"></sp-icon-arrow-right>
213
+ </sp-action-button>`
214
+ : nothing}
215
+ ${canMoveOut
216
+ ? html`<sp-action-button
217
+ quiet
218
+ size="xs"
219
+ title="Move out of parent"
220
+ @click=${(/** @type {any} */ e) => {
221
+ e.stopPropagation();
222
+ /** @type {HTMLElement} */ (e.currentTarget).blur();
223
+ const parentIdx = /** @type {number} */ (childIndex(parentPath));
224
+ update(moveNode(getState(), path, grandparentPath, parentIdx + 1));
225
+ }}
226
+ >
227
+ <sp-icon-arrow-left slot="icon"></sp-icon-arrow-left>
228
+ </sp-action-button>`
229
+ : nothing}
230
+ <sp-action-button
231
+ quiet
232
+ size="xs"
233
+ class="layer-delete"
234
+ title="Delete"
235
+ @click=${(/** @type {any} */ e) => {
236
+ e.stopPropagation();
237
+ update(removeNode(getState(), path));
238
+ }}
239
+ >
240
+ <sp-icon-close slot="icon"></sp-icon-close>
241
+ </sp-action-button>
242
+ </span>
243
+ `
244
+ : nothing}
245
+ </div>
246
+ `);
247
+ }
248
+
249
+ return html`
250
+ <div class="layers-container" style="position:relative">
251
+ <div
252
+ class="layers-tree"
253
+ @click=${(/** @type {any} */ e) => {
254
+ const toggle = e.target.closest(".layer-toggle");
255
+ if (!toggle) return;
256
+ e.stopPropagation();
257
+ const row = toggle.closest(".layer-row");
258
+ if (!row) return;
259
+ const key = row.dataset.path;
260
+ if (!key) return;
261
+ if (collapsed.has(key)) collapsed.delete(key);
262
+ else collapsed.add(key);
263
+ ctx.rerender();
264
+ }}
265
+ >
266
+ ${layerRows}
267
+ </div>
268
+ </div>
269
+ `;
270
+ }
@@ -0,0 +1,141 @@
1
+ /**
2
+ * Left panel — orchestrator that delegates to per-tab render functions.
3
+ *
4
+ * Each sub-panel exports a render function that takes its dependencies as arguments and returns a
5
+ * TemplateResult — the same pattern as imports-panel, signals-panel, etc. Only this orchestrator
6
+ * uses mount/render/unmount because it owns the DOM root and error boundary.
7
+ */
8
+
9
+ import { html, render as litRender, nothing } from "lit-html";
10
+ import {
11
+ getState,
12
+ leftPanel,
13
+ updateSession,
14
+ update,
15
+ applyMutation,
16
+ updateFrontmatter,
17
+ } from "../store.js";
18
+ import { view } from "../view.js";
19
+ import { ensureLitState } from "./shared.js";
20
+ import { renderLayersTemplate } from "./layers-panel.js";
21
+ import { renderStylebookLayersTemplate } from "./stylebook-layers-panel.js";
22
+ import { renderElementsTemplate } from "./elements-panel.js";
23
+
24
+ /** @type {any} */
25
+ let _ctx = null;
26
+
27
+ /**
28
+ * Mount the left panel orchestrator.
29
+ *
30
+ * @param {any} ctx — callbacks and references that avoid circular dependencies
31
+ */
32
+ export function mount(ctx) {
33
+ _ctx = ctx;
34
+ }
35
+
36
+ export function unmount() {
37
+ _ctx = null;
38
+ }
39
+
40
+ export function render() {
41
+ if (!_ctx) return;
42
+ try {
43
+ ensureLitState(leftPanel);
44
+ _render();
45
+ } catch (e) {
46
+ console.error("left-panel render error:", e);
47
+ try {
48
+ leftPanel.textContent = "";
49
+ // @ts-ignore — clear Lit's internal state to recover from marker corruption
50
+ delete leftPanel["_$litPart$"];
51
+ _render();
52
+ } catch (e2) {
53
+ console.error("left-panel retry failed:", e2);
54
+ }
55
+ }
56
+ }
57
+
58
+ function _render() {
59
+ const S = getState();
60
+ const tab = S.ui.leftTab;
61
+
62
+ /** @type {any} */
63
+ let content;
64
+ if (tab === "layers")
65
+ content =
66
+ _ctx.getCanvasMode() === "settings"
67
+ ? renderStylebookLayersTemplate({
68
+ selectStylebookTag: _ctx.selectStylebookTag,
69
+ stylebookMeta: _ctx.stylebookMeta,
70
+ })
71
+ : renderLayersTemplate({
72
+ navigateToComponent: _ctx.navigateToComponent,
73
+ rerender: render,
74
+ });
75
+ else if (tab === "imports")
76
+ content = _ctx.renderImportsTemplate({
77
+ renderLeftPanel: render,
78
+ documentPath: S.documentPath,
79
+ documentElements: S.document.$elements || [],
80
+ applyMutation: (/** @type {any} */ fn) => {
81
+ update(applyMutation(getState(), fn));
82
+ },
83
+ });
84
+ else if (tab === "files") content = _ctx.renderFilesTemplate();
85
+ else if (tab === "blocks")
86
+ content = renderElementsTemplate({
87
+ webdata: _ctx.webdata,
88
+ defaultDef: _ctx.defaultDef,
89
+ rerender: render,
90
+ });
91
+ else if (tab === "state")
92
+ content = _ctx.renderSignalsTemplate(S, {
93
+ renderLeftPanel: render,
94
+ renderCanvas: _ctx.renderCanvas,
95
+ updateSession,
96
+ });
97
+ else if (tab === "data")
98
+ content = _ctx.renderDataExplorerTemplate(S.document.state, view.liveScope, {
99
+ renderCanvas: _ctx.renderCanvas,
100
+ renderLeftPanel: render,
101
+ defCategory: _ctx.defCategory,
102
+ defBadgeLabel: _ctx.defBadgeLabel,
103
+ });
104
+ else if (tab === "head") {
105
+ const isContent = S.mode === "content";
106
+ const fm = S.content?.frontmatter ?? {};
107
+ const headDoc = isContent ? { ...S.document, title: fm.title, $head: fm.$head } : S.document;
108
+ content = _ctx.renderHeadTemplate({
109
+ document: headDoc,
110
+ applyMutation: isContent
111
+ ? (/** @type {any} */ fn) => {
112
+ const tmp = { title: fm.title, $head: fm.$head ? [...fm.$head] : undefined };
113
+ fn(tmp);
114
+ let s = getState();
115
+ if (tmp.title !== fm.title) s = updateFrontmatter(s, "title", tmp.title);
116
+ const newHead = tmp.$head && tmp.$head.length > 0 ? tmp.$head : undefined;
117
+ s = updateFrontmatter(s, "$head", newHead);
118
+ update(s);
119
+ }
120
+ : (/** @type {any} */ fn) => {
121
+ update(applyMutation(getState(), fn));
122
+ },
123
+ renderLeftPanel: render,
124
+ });
125
+ } else if (tab === "git") content = _ctx.renderGitPanel(S);
126
+ else content = nothing;
127
+
128
+ litRender(html`<div class="panel-body">${content}</div>`, /** @type {any} */ (leftPanel));
129
+
130
+ // Post-render side effects
131
+ if (tab === "layers" && _ctx.getCanvasMode() !== "settings") _ctx.registerLayersDnD();
132
+ else if (tab === "imports") {
133
+ /* no post-render DnD needed */
134
+ } else if (tab === "blocks") {
135
+ _ctx.registerElementsDnD();
136
+ _ctx.registerComponentsDnD();
137
+ } else if (tab === "files") {
138
+ const tree = /** @type {any} */ (leftPanel)?.querySelector(".file-tree");
139
+ if (tree) _ctx.setupTreeKeyboard(tree);
140
+ }
141
+ }
@@ -11,6 +11,7 @@ import { eventsSidebarTemplate } from "./events-panel.js";
11
11
  import { isCustomElementDoc } from "./signals-panel.js";
12
12
  import { ensureLitState } from "./shared.js";
13
13
  import { isColorPopoverOpen } from "../ui/color-selector.js";
14
+ import { renderStylePanelTemplate } from "./style-panel.js";
14
15
 
15
16
  /** @type {any} */
16
17
  let _ctx = null;
@@ -21,7 +22,7 @@ let _unsub = null;
21
22
  /**
22
23
  * Mount the right panel.
23
24
  *
24
- * @param {any} ctx — { propertiesSidebarTemplate, renderStylePanelTemplate, renderCanvas,
25
+ * @param {any} ctx — { propertiesSidebarTemplate, getCanvasMode, renderCanvas,
25
26
  * updateForcedPseudoPreview }
26
27
  */
27
28
  export function mount(ctx) {
@@ -117,7 +118,7 @@ function rightPanelTemplate() {
117
118
  });
118
119
  } else if (tab === "style") {
119
120
  try {
120
- bodyT = _ctx.renderStylePanelTemplate();
121
+ bodyT = renderStylePanelTemplate({ getCanvasMode: _ctx.getCanvasMode });
121
122
  } catch (/** @type {any} */ e) {
122
123
  console.error("[renderStylePanelTemplate]", e);
123
124
  }
@@ -0,0 +1,176 @@
1
+ /** Style input widgets — keyword, select, combobox, and font renderers for the style panel. */
2
+
3
+ import { html } from "lit-html";
4
+ import { live } from "lit-html/directives/live.js";
5
+ import { getState, update, updateStyle, debouncedStyleCommit } from "../store.js";
6
+ import { widgetForType as _widgetForType } from "../ui/widgets.js";
7
+ import { kebabToLabel, friendlyNameToVar, varDisplayName } from "../utils/studio-utils.js";
8
+ import {
9
+ TYPO_PREVIEW_PROPS,
10
+ currentFontFamily,
11
+ getFontVars,
12
+ getCssInitialMap,
13
+ camelToKebab,
14
+ } from "./style-utils.js";
15
+
16
+ /**
17
+ * Dual-mode keyword input — shared by select (enum) and combobox (examples) widgets.
18
+ *
19
+ * @param {any} options @param {any} prop @param {any} value @param {any} onChange
20
+ */
21
+ export function renderKeywordInput(options, prop, value, onChange) {
22
+ const cssInitialMap = getCssInitialMap();
23
+ const isTypoPreview = TYPO_PREVIEW_PROPS.has(prop) || prop === "fontWeight";
24
+ const font = isTypoPreview ? currentFontFamily() : "";
25
+ const cssProp = isTypoPreview ? camelToKebab(prop) : "";
26
+
27
+ const comboOptions = options.map((/** @type {any} */ v) => {
28
+ const label = v.includes("-")
29
+ ? kebabToLabel(v)
30
+ : v.replace(/^./, (/** @type {any} */ c) => c.toUpperCase());
31
+ const style = isTypoPreview ? `${cssProp}: ${v};${font ? ` font-family: ${font}` : ""}` : "";
32
+ return { value: v, label, style };
33
+ });
34
+
35
+ return html`<jx-value-selector
36
+ size="s"
37
+ .value=${value || ""}
38
+ placeholder=${cssInitialMap.get(prop) || ""}
39
+ .options=${comboOptions}
40
+ @change=${(/** @type {any} */ e) => onChange(e.target.value)}
41
+ @input=${debouncedStyleCommit(`kw:${prop}`, 400, (/** @type {any} */ e) =>
42
+ onChange(e.target.value),
43
+ )}
44
+ ></jx-value-selector>`;
45
+ }
46
+
47
+ /** @param {any} entry @param {any} prop @param {any} value @param {any} onChange */
48
+ export function renderSelectInput(entry, prop, value, onChange) {
49
+ return renderKeywordInput(entry.enum || [], prop, value, onChange);
50
+ }
51
+
52
+ /** @param {any} preset @param {any} onChange */
53
+ function handleFontPresetSelection(preset, onChange) {
54
+ const S = getState();
55
+ const varName = friendlyNameToVar(preset.title, "--font-");
56
+ if (!S.document?.style?.[varName]) {
57
+ update(updateStyle(S, [], varName, preset.value));
58
+ }
59
+ onChange(`var(${varName})`);
60
+ }
61
+
62
+ /** @param {any} val @param {any} presets @param {any} onChange */
63
+ function handleFontSelection(val, presets, onChange) {
64
+ if (!val) return;
65
+ if (val.startsWith("__preset__:")) {
66
+ const title = val.slice("__preset__:".length);
67
+ const preset = presets.find((/** @type {any} */ p) => p.title === title);
68
+ if (preset) handleFontPresetSelection(preset, onChange);
69
+ return;
70
+ }
71
+ if (val.startsWith("--")) {
72
+ onChange(`var(${val})`);
73
+ return;
74
+ }
75
+ const preset = presets.find((/** @type {any} */ p) => p.title === val);
76
+ if (preset) {
77
+ handleFontPresetSelection(preset, onChange);
78
+ return;
79
+ }
80
+ const fontVars = getFontVars();
81
+ const matchedVar = fontVars.find(
82
+ (/** @type {any} */ fv) => varDisplayName(fv.name, "--font-") === val,
83
+ );
84
+ if (matchedVar) {
85
+ onChange(`var(${matchedVar.name})`);
86
+ return;
87
+ }
88
+ onChange(val);
89
+ }
90
+
91
+ /**
92
+ * Build font options array for jx-value-selector.
93
+ *
94
+ * @param {any[]} fontVars @param {any[]} presets
95
+ * @returns {{ value: string; label: string; style: string }[] | { divider: true }[]}
96
+ */
97
+ export function buildFontOptions(fontVars, presets) {
98
+ /** @type {any[]} */
99
+ const opts = fontVars.map((/** @type {any} */ fv) => ({
100
+ value: fv.name,
101
+ label: varDisplayName(fv.name, "--font-"),
102
+ style: `font-family: ${fv.value}`,
103
+ }));
104
+ const unadded = presets.filter(
105
+ (/** @type {any} */ p) =>
106
+ !fontVars.some((/** @type {any} */ fv) => fv.name === friendlyNameToVar(p.title, "--font-")),
107
+ );
108
+ if (unadded.length > 0 && opts.length > 0) opts.push({ divider: true });
109
+ for (const p of unadded) {
110
+ opts.push({
111
+ value: "__preset__:" + p.title,
112
+ label: p.title,
113
+ style: `font-family: ${p.value}`,
114
+ });
115
+ }
116
+ return opts;
117
+ }
118
+
119
+ /** @param {any} entry @param {any} prop @param {any} value @param {any} onChange */
120
+ export function renderComboboxInput(entry, prop, value, onChange) {
121
+ const cssInitialMap = getCssInitialMap();
122
+ const fontVars = prop === "fontFamily" ? getFontVars() : [];
123
+ const presets = entry.presets || [];
124
+ const examples = entry.examples || [];
125
+
126
+ if (prop === "fontFamily") {
127
+ const varMatch = typeof value === "string" && value.match(/^var\((--[^)]+)\)$/);
128
+ const comboValue = varMatch ? varMatch[1] : value || "";
129
+ const fontOptions = buildFontOptions(fontVars, presets);
130
+ return html`<jx-value-selector
131
+ size="s"
132
+ .value=${comboValue}
133
+ placeholder=${cssInitialMap.get("fontFamily") || ""}
134
+ .options=${fontOptions}
135
+ @change=${(/** @type {any} */ e) => handleFontSelection(e.target.value, presets, onChange)}
136
+ @input=${debouncedStyleCommit("combo:fontFamily", 400, (/** @type {any} */ e) =>
137
+ onChange(e.target.value),
138
+ )}
139
+ ></jx-value-selector>`;
140
+ }
141
+
142
+ if (examples.length > 0) {
143
+ return renderKeywordInput(examples, prop, value, onChange);
144
+ }
145
+
146
+ return html`
147
+ <sp-textfield
148
+ size="s"
149
+ placeholder=${cssInitialMap.get(prop) || ""}
150
+ .value=${live(value || "")}
151
+ @input=${debouncedStyleCommit(`combo:${prop}`, 400, (/** @type {any} */ e) =>
152
+ onChange(e.target.value),
153
+ )}
154
+ ></sp-textfield>
155
+ `;
156
+ }
157
+
158
+ /**
159
+ * Style-aware widgetForType — wraps the generic widget renderer with style-specific select/combobox
160
+ * inputs and CSS initial-value placeholders.
161
+ */
162
+ export function widgetForType(
163
+ /** @type {any} */ type,
164
+ /** @type {any} */ entry,
165
+ /** @type {any} */ prop,
166
+ /** @type {any} */ value,
167
+ /** @type {any} */ onCommit,
168
+ /** @type {any} */ opts = {},
169
+ ) {
170
+ const cssInitialMap = getCssInitialMap();
171
+ return _widgetForType(type, entry, prop, value, onCommit, {
172
+ placeholder: opts.placeholder || cssInitialMap.get(prop) || "",
173
+ renderSelect: renderSelectInput,
174
+ renderCombobox: renderComboboxInput,
175
+ });
176
+ }