@jxsuite/studio 0.25.1 → 0.25.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/studio",
3
- "version": "0.25.1",
3
+ "version": "0.25.3",
4
4
  "description": "Jx Studio — visual builder for Jx documents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -31,10 +31,10 @@
31
31
  },
32
32
  "dependencies": {
33
33
  "@atlaskit/pragmatic-drag-and-drop": "^1.8.1",
34
- "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.1.0",
35
- "@jxsuite/parser": "^0.25.1",
36
- "@jxsuite/runtime": "^0.25.1",
37
- "@jxsuite/schema": "^0.25.1",
34
+ "@atlaskit/pragmatic-drag-and-drop-hitbox": "^1.2.0",
35
+ "@jxsuite/parser": "^0.25.3",
36
+ "@jxsuite/runtime": "^0.25.3",
37
+ "@jxsuite/schema": "^0.25.3",
38
38
  "@spectrum-web-components/accordion": "^1.12.1",
39
39
  "@spectrum-web-components/action-bar": "1.12.1",
40
40
  "@spectrum-web-components/action-button": "^1.12.1",
@@ -11,7 +11,7 @@ import * as monaco from "monaco-editor/esm/vs/editor/editor.api.js";
11
11
  import { canvasWrap, canvasPanels, updateCanvas } from "../store";
12
12
  import { activeTab } from "../workspace/workspace";
13
13
  import { view } from "../view";
14
- import { loadMarkdown } from "../files/file-ops";
14
+ import { loadMarkdown, serializeDocument } from "../files/file-ops";
15
15
  import { renderWelcome } from "../panels/welcome-screen";
16
16
  import { projectState } from "../state";
17
17
  import {
@@ -75,6 +75,22 @@ export function initCanvasRender(ctx: CanvasRenderCtx) {
75
75
  _ctx = ctx;
76
76
  }
77
77
 
78
+ /** Monaco language for the source view of a tab's document. */
79
+ function sourceLang(tab: import("../tabs/tab.js").Tab) {
80
+ if (tab.doc.sourceFormat === "md") return "markdown";
81
+ return (tab.documentPath || "").endsWith(".js") ? "javascript" : "json";
82
+ }
83
+
84
+ /**
85
+ * The full source text for the source view. Markdown files serialize to their on-disk form
86
+ * (frontmatter YAML — title, $head, etc. — plus the body), not just the JSON of the body tree.
87
+ */
88
+ function sourceContent(tab: import("../tabs/tab.js").Tab, lang: string) {
89
+ if (lang === "markdown") return serializeDocument(tab);
90
+ if (lang === "javascript") return tab.doc.document?.toString?.() || "";
91
+ return JSON.stringify(tab.doc.document, null, 2);
92
+ }
93
+
78
94
  export function renderCanvas() {
79
95
  const tab = activeTab.value;
80
96
  if (!tab) {
@@ -118,15 +134,15 @@ export function renderCanvas() {
118
134
  view.functionEditor = null;
119
135
  }
120
136
 
121
- // Source mode: update existing Monaco editor without recreating
137
+ // Source mode: update existing Monaco editor without recreating. Don't replace the buffer while
138
+ // the user is actively typing in it — that would reformat under the cursor (the source view is
139
+ // the editing surface here, mirroring the panel draft-state behaviour).
122
140
  if (canvasMode === "source" && view.monacoEditor) {
123
- const filePath = tab.documentPath || "document.json";
124
- const isJs = filePath.endsWith(".js");
125
- const newVal = isJs ? S.document?.toString?.() || "" : JSON.stringify(S.document, null, 2);
126
- const currentVal = view.monacoEditor.getValue();
127
- if (currentVal !== newVal) {
128
- view.monacoEditor._ignoreNextChange = true;
129
- view.monacoEditor.setValue(newVal);
141
+ const editor = view.monacoEditor;
142
+ const newVal = sourceContent(tab, sourceLang(tab));
143
+ if (!editor.hasTextFocus() && editor.getValue() !== newVal) {
144
+ editor._ignoreNextChange = true;
145
+ editor.setValue(newVal);
130
146
  }
131
147
  return;
132
148
  }
@@ -234,9 +250,8 @@ export function renderCanvas() {
234
250
  );
235
251
 
236
252
  const filePath = tab.documentPath || "document.json";
237
- const lang = filePath.endsWith(".js") ? "javascript" : "json";
238
- const content =
239
- lang === "json" ? JSON.stringify(S.document, null, 2) : S.document?.toString?.() || "";
253
+ const lang = sourceLang(tab);
254
+ const content = sourceContent(tab, lang);
240
255
  const modelUri = monaco.Uri.parse("file:///" + filePath);
241
256
  const model = monaco.editor.createModel(content, lang, modelUri);
242
257
  view.monacoEditor = monaco.editor.create(editorContainer as unknown as HTMLElement, {
@@ -262,10 +277,20 @@ export function renderCanvas() {
262
277
  return;
263
278
  }
264
279
  clearTimeout(debounce);
265
- debounce = setTimeout(() => {
280
+ debounce = setTimeout(async () => {
266
281
  const tab = activeTab.value;
267
282
  if (!tab) return;
268
- if (lang === "json") {
283
+ if (lang === "markdown") {
284
+ try {
285
+ // Parse the full markdown source back into body + frontmatter (title, $head, etc.).
286
+ const { document, frontmatter } = await loadMarkdown(editor.getValue());
287
+ tab.doc.document = document as JxMutableNode;
288
+ tab.doc.content.frontmatter = frontmatter;
289
+ tab.doc.dirty = true;
290
+ } catch {
291
+ // Unparseable markdown — don't update state
292
+ }
293
+ } else if (lang === "json") {
269
294
  try {
270
295
  tab.doc.document = JSON.parse(editor.getValue());
271
296
  tab.doc.dirty = true;
@@ -9,8 +9,9 @@
9
9
  import { html, nothing } from "lit-html";
10
10
  import { live } from "lit-html/directives/live.js";
11
11
  import { renderFieldRow } from "../ui/field-row";
12
+ import { spTextField, spTextArea, spNumberField } from "../ui/field-input";
12
13
  import { renderMediaPicker } from "../ui/media-picker";
13
- import { debouncedStyleCommit, renderOnly, projectState } from "../store";
14
+ import { renderOnly, projectState } from "../store";
14
15
  import type { JsonValue, DirEntry } from "../types";
15
16
  import { activeTab } from "../workspace/workspace";
16
17
  import { transactDoc, mutateUpdateFrontmatter } from "../tabs/transact";
@@ -237,32 +238,13 @@ function renderMetaFieldRow(
237
238
  });
238
239
  }
239
240
 
241
+ const commit = (v: string) =>
242
+ applyMutation((d: JxMutableNode) => upsertMeta(d, field.attr, field.key, v.trim()));
243
+ const placeholder =
244
+ field.key === "viewport" ? "width=device-width, initial-scale=1" : `${field.label}…`;
240
245
  const widget = field.multiline
241
- ? html`
242
- <sp-textfield
243
- size="s"
244
- multiline
245
- .value=${live(val)}
246
- placeholder="${field.label}…"
247
- @input=${debouncedStyleCommit(`head:${field.key}`, 400, (e: Event) => {
248
- const content = (e.target as HTMLInputElement).value?.trim() ?? "";
249
- applyMutation((d: JxMutableNode) => upsertMeta(d, field.attr, field.key, content));
250
- })}
251
- ></sp-textfield>
252
- `
253
- : html`
254
- <sp-textfield
255
- size="s"
256
- .value=${live(val)}
257
- placeholder=${field.key === "viewport"
258
- ? "width=device-width, initial-scale=1"
259
- : `${field.label}…`}
260
- @input=${debouncedStyleCommit(`head:${field.key}`, 400, (e: Event) => {
261
- const content = (e.target as HTMLInputElement).value?.trim() ?? "";
262
- applyMutation((d: JxMutableNode) => upsertMeta(d, field.attr, field.key, content));
263
- })}
264
- ></sp-textfield>
265
- `;
246
+ ? spTextArea(`head:${field.key}`, val, commit, { placeholder: `${field.label}…` })
247
+ : spTextField(`head:${field.key}`, val, commit, { placeholder });
266
248
 
267
249
  return renderFieldRow({
268
250
  prop: field.key,
@@ -396,20 +378,17 @@ export function renderHeadTemplate({
396
378
  applyMutation((d: JxMutableNode) => {
397
379
  delete d.title;
398
380
  }),
399
- widget: html`
400
- <sp-textfield
401
- size="s"
402
- .value=${live(title)}
403
- placeholder="Page title…"
404
- @input=${debouncedStyleCommit("head:title", 400, (e: Event) => {
405
- const val = (e.target as HTMLInputElement).value?.trim() ?? "";
406
- applyMutation((d: JxMutableNode) => {
407
- if (val) d.title = val;
408
- else delete d.title;
409
- });
410
- })}
411
- ></sp-textfield>
412
- `,
381
+ widget: spTextField(
382
+ "head:title",
383
+ title,
384
+ (v: string) =>
385
+ applyMutation((d: JxMutableNode) => {
386
+ const val = v.trim();
387
+ if (val) d.title = val;
388
+ else delete d.title;
389
+ }),
390
+ { placeholder: "Page title…" },
391
+ ),
413
392
  })}
414
393
  ${PAGE_FIELDS.map((field) => renderMetaFieldRow(field, head, applyMutation))}
415
394
  ${renderFieldRow({
@@ -620,28 +599,26 @@ function renderFmField(
620
599
  }
621
600
 
622
601
  if (entry.type === "array") {
623
- const display = Array.isArray(value) ? value.join(", ") : value || "";
602
+ const display = Array.isArray(value) ? value.join(", ") : (value as string) || "";
624
603
  return renderFieldRow({
625
604
  prop: field,
626
605
  label: displayLabel,
627
606
  hasValue: hasVal,
628
607
  onClear,
629
- widget: html`
630
- <sp-textfield
631
- size="s"
632
- placeholder="comma, separated"
633
- .value=${live(display)}
634
- @input=${debouncedStyleCommit(`fm:${field}`, 400, (e: Event) => {
635
- const arr = (e.target as HTMLInputElement).value
636
- ? (e.target as HTMLInputElement).value
637
- .split(",")
638
- .map((s: string) => s.trim())
639
- .filter(Boolean)
640
- : undefined;
641
- transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, arr));
642
- })}
643
- ></sp-textfield>
644
- `,
608
+ widget: spTextField(
609
+ `fm:${field}`,
610
+ display,
611
+ (v: string) => {
612
+ const arr = v
613
+ ? v
614
+ .split(",")
615
+ .map((s: string) => s.trim())
616
+ .filter(Boolean)
617
+ : undefined;
618
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, arr));
619
+ },
620
+ { placeholder: "comma, separated" },
621
+ ),
645
622
  });
646
623
  }
647
624
 
@@ -684,19 +661,9 @@ function renderFmField(
684
661
  label: displayLabel,
685
662
  hasValue: hasVal,
686
663
  onClear,
687
- widget: html`
688
- <sp-number-field
689
- size="s"
690
- hide-stepper
691
- .value=${live(value !== undefined ? Number(value) : undefined)}
692
- @change=${debouncedStyleCommit(`fm:${field}`, 400, (e: Event) => {
693
- const v = (e.target as HTMLInputElement).value;
694
- transactDoc(activeTab.value, (t) =>
695
- mutateUpdateFrontmatter(t, field, isNaN(Number(v)) ? undefined : Number(v)),
696
- );
697
- })}
698
- ></sp-number-field>
699
- `,
664
+ widget: spNumberField(value !== undefined ? Number(value) : undefined, (n) =>
665
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, n)),
666
+ ),
700
667
  });
701
668
  }
702
669
 
@@ -705,17 +672,12 @@ function renderFmField(
705
672
  label: displayLabel,
706
673
  hasValue: hasVal,
707
674
  onClear,
708
- widget: html`
709
- <sp-textfield
710
- size="s"
711
- placeholder=${entry.format === "date" ? "YYYY-MM-DD" : ""}
712
- .value=${live(value || "")}
713
- @input=${debouncedStyleCommit(`fm:${field}`, 400, (e: Event) => {
714
- transactDoc(activeTab.value, (t) =>
715
- mutateUpdateFrontmatter(t, field, (e.target as HTMLInputElement).value || undefined),
716
- );
717
- })}
718
- ></sp-textfield>
719
- `,
675
+ widget: spTextField(
676
+ `fm:${field}`,
677
+ (value as string) || "",
678
+ (v: string) =>
679
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
680
+ { placeholder: entry.format === "date" ? "YYYY-MM-DD" : "" },
681
+ ),
720
682
  });
721
683
  }
@@ -11,6 +11,7 @@ import { html, render as litRender, nothing } from "lit-html";
11
11
  import type { TemplateResult } from "lit-html";
12
12
  import { leftPanel, updateSession } from "../store";
13
13
  import { effect, effectScope } from "../reactivity";
14
+ import { createPanelScheduler, type PanelScheduler } from "./panel-scheduler";
14
15
  import { activeTab } from "../workspace/workspace";
15
16
  import { view } from "../view";
16
17
  import { transact, mutateUpdateFrontmatter } from "../tabs/transact";
@@ -49,18 +50,7 @@ let _ctx: LeftPanelCtx | null = null;
49
50
 
50
51
  let _scope: import("@vue/reactivity").EffectScope | null = null;
51
52
 
52
- let _rendering = false;
53
- let _scheduled = false;
54
- let _hasFocus = false;
55
-
56
- function _onFocusIn() {
57
- _hasFocus = true;
58
- }
59
-
60
- function _onFocusOut() {
61
- _hasFocus = false;
62
- render();
63
- }
53
+ let _scheduler: PanelScheduler | null = null;
64
54
 
65
55
  /**
66
56
  * Mount the left panel orchestrator.
@@ -69,8 +59,8 @@ function _onFocusOut() {
69
59
  */
70
60
  export function mount(ctx: LeftPanelCtx) {
71
61
  _ctx = ctx;
72
- leftPanel.addEventListener("focusin", _onFocusIn);
73
- leftPanel.addEventListener("focusout", _onFocusOut);
62
+ _scheduler = createPanelScheduler({ root: leftPanel, render: _doRender });
63
+ _scheduler.bindFocus();
74
64
  _scope = effectScope();
75
65
  _scope.run(() => {
76
66
  effect(() => {
@@ -85,9 +75,7 @@ export function mount(ctx: LeftPanelCtx) {
85
75
  void tab.session.ui.gitLoading;
86
76
  void tab.session.ui.gitError;
87
77
  }
88
- if (!_hasFocus) {
89
- render();
90
- }
78
+ render();
91
79
  });
92
80
  });
93
81
  }
@@ -96,25 +84,21 @@ export function unmount() {
96
84
  _scope?.stop();
97
85
  _scope = null;
98
86
  _ctx = null;
99
- leftPanel.removeEventListener("focusin", _onFocusIn);
100
- leftPanel.removeEventListener("focusout", _onFocusOut);
101
- _hasFocus = false;
87
+ _scheduler?.unbind();
88
+ _scheduler = null;
102
89
  }
103
90
 
91
+ /**
92
+ * Request a render. Coalesced and deferred while a text input in the panel is focused (so explicit
93
+ * callers — renderOnly("leftPanel"), tab switches, etc. — can never clobber a field mid-edit).
94
+ */
104
95
  export function render() {
105
- if (!_ctx) return;
106
- if (_rendering) return;
107
- if (!_scheduled) {
108
- _scheduled = true;
109
- queueMicrotask(_flush);
110
- }
96
+ _scheduler?.schedule();
111
97
  }
112
98
 
113
- function _flush() {
114
- _scheduled = false;
99
+ /** Actual DOM paint, invoked by the scheduler. Includes a Lit-marker-corruption recovery retry. */
100
+ function _doRender() {
115
101
  if (!_ctx) return;
116
- if (_rendering) return;
117
- _rendering = true;
118
102
  try {
119
103
  _render();
120
104
  } catch (e) {
@@ -127,8 +111,6 @@ function _flush() {
127
111
  } catch (e2) {
128
112
  console.error("left-panel retry failed:", e2);
129
113
  }
130
- } finally {
131
- _rendering = false;
132
114
  }
133
115
 
134
116
  if (view.leftTab === "layers") {
@@ -0,0 +1,126 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Panel-scheduler.ts — Shared focus-aware render scheduler for Studio panels.
4
+ *
5
+ * Generalizes the guard that previously lived only in the right panel: render requests are
6
+ * coalesced via requestAnimationFrame and DEFERRED while a text input inside the panel root is
7
+ * focused, then flushed on focusout. Routing every render path (reactive effect, renderOnly(...),
8
+ * explicit calls) through one scheduler means a focused field is never rebuilt mid-edit — which is
9
+ * what previously truncated or dropped characters in the document/head sidebar.
10
+ */
11
+
12
+ /**
13
+ * Is the element (or its shadow-DOM active descendant) a text-entry control whose value would be
14
+ * clobbered by a re-render? Covers native inputs and the Spectrum web components used in panels.
15
+ *
16
+ * @param {Element | null} el
17
+ * @returns {boolean}
18
+ */
19
+ export function isTextInput(el: Element | null): boolean {
20
+ if (!el) return false;
21
+ const tag = el.tagName.toLowerCase();
22
+ if (tag === "input" || tag === "textarea") return true;
23
+ if (tag === "sp-textfield" || tag === "sp-number-field" || tag === "sp-search") return true;
24
+ if (el.shadowRoot?.activeElement) return isTextInput(el.shadowRoot.activeElement);
25
+ return false;
26
+ }
27
+
28
+ export interface PanelScheduler {
29
+ /** Request a render. Coalesced; deferred while a text input in the panel is focused. */
30
+ schedule(): void;
31
+ /** True when a render would currently be deferred (text input focused or blockWhile()). */
32
+ isEditing(): boolean;
33
+ /** Attach focusin/focusout listeners to the panel root. */
34
+ bindFocus(): void;
35
+ /** Detach listeners and cancel any pending frame. */
36
+ unbind(): void;
37
+ /** Render immediately, bypassing the rAF coalescing (still respects the focus guard). */
38
+ flushNow(): void;
39
+ }
40
+
41
+ /**
42
+ * Create a focus-aware render scheduler bound to a panel root.
43
+ *
44
+ * @param {{
45
+ * root: HTMLElement;
46
+ * render: () => void;
47
+ * blockWhile?: () => boolean;
48
+ * }} opts
49
+ * - `render` does the actual DOM paint; `blockWhile` is an extra defer predicate (e.g. a color
50
+ * popover being open).
51
+ * @returns {PanelScheduler}
52
+ */
53
+ export function createPanelScheduler(opts: {
54
+ root: HTMLElement;
55
+ render: () => void;
56
+ blockWhile?: () => boolean;
57
+ }): PanelScheduler {
58
+ const { root, render, blockWhile } = opts;
59
+ let editing = false;
60
+ let scheduled = false;
61
+ let pending = false;
62
+ let rendering = false;
63
+ let rafId = 0;
64
+
65
+ const blocked = () => editing || (blockWhile ? blockWhile() : false);
66
+
67
+ function flush() {
68
+ scheduled = false;
69
+ rafId = 0;
70
+ if (rendering) return;
71
+ // Defer while a field is focused — flushed later by focusout (or the next schedule()).
72
+ if (blocked()) {
73
+ pending = true;
74
+ return;
75
+ }
76
+ pending = false;
77
+ rendering = true;
78
+ try {
79
+ render();
80
+ } finally {
81
+ rendering = false;
82
+ }
83
+ }
84
+
85
+ function schedule() {
86
+ if (scheduled) return;
87
+ scheduled = true;
88
+ rafId = requestAnimationFrame(flush);
89
+ }
90
+
91
+ function onFocusIn(e: FocusEvent) {
92
+ editing = isTextInput(e.target as Element);
93
+ }
94
+
95
+ function onFocusOut() {
96
+ // Clear the editing flag and schedule a flush. If focus is merely moving to another field in
97
+ // the same panel, the synchronous focusin that follows re-sets `editing` before the rAF runs,
98
+ // so the deferred render keeps waiting (no mid-edit clobber).
99
+ editing = false;
100
+ if (pending) schedule();
101
+ }
102
+
103
+ return {
104
+ schedule,
105
+ isEditing: () => blocked(),
106
+ bindFocus() {
107
+ root.addEventListener("focusin", onFocusIn);
108
+ root.addEventListener("focusout", onFocusOut);
109
+ },
110
+ unbind() {
111
+ root.removeEventListener("focusin", onFocusIn);
112
+ root.removeEventListener("focusout", onFocusOut);
113
+ if (rafId) cancelAnimationFrame(rafId);
114
+ scheduled = false;
115
+ pending = false;
116
+ editing = false;
117
+ rendering = false;
118
+ },
119
+ flushNow() {
120
+ if (rafId) cancelAnimationFrame(rafId);
121
+ rafId = 0;
122
+ scheduled = false;
123
+ flush();
124
+ },
125
+ };
126
+ }
@@ -20,6 +20,7 @@ import { view } from "../view";
20
20
  import { componentRegistry } from "../files/components";
21
21
  import { widgetForType } from "./style-inputs";
22
22
  import { renderFieldRow } from "../ui/field-row";
23
+ import { spTextField, spTextArea } from "../ui/field-input";
23
24
  import {
24
25
  attrLabel,
25
26
  inferInputType,
@@ -96,28 +97,17 @@ function bindableFieldRow(
96
97
  (d as JxPrototypeDef)?.$prototype !== "Function",
97
98
  );
98
99
 
99
- /** @type {ReturnType<typeof setTimeout> | undefined} */
100
- let debounce: ReturnType<typeof setTimeout> | undefined;
101
- const onInput = (e: Event) => {
102
- clearTimeout(debounce);
103
- debounce = setTimeout(() => onChange((e.target as HTMLInputElement).value), 400);
104
- };
105
-
106
100
  const staticVal = isBound ? "" : (rawValue ?? "");
101
+ const fieldKey = `prop:${label}`;
107
102
  const staticTpl =
108
103
  type === "textarea"
109
- ? html`<sp-textfield
110
- multiline
111
- size="s"
112
- .value=${live(staticVal)}
113
- @input=${onInput}
114
- ></sp-textfield>`
104
+ ? spTextArea(fieldKey, String(staticVal), (v: string) => onChange(v))
115
105
  : type === "checkbox"
116
106
  ? html`<sp-checkbox
117
107
  ?checked=${!!staticVal}
118
108
  @change=${(e: Event) => onChange((e.target as HTMLInputElement).checked)}
119
109
  ></sp-checkbox>`
120
- : html`<sp-textfield size="s" .value=${live(staticVal)} @input=${onInput}></sp-textfield>`;
110
+ : spTextField(fieldKey, String(staticVal), (v: string) => onChange(v));
121
111
 
122
112
  const boundTpl = html`
123
113
  <sp-picker
@@ -476,15 +466,14 @@ function renderComponentPropsFieldsTemplate(
476
466
  } else if (prop.format === "color") {
477
467
  widgetTpl = renderColorSelector(prop.name, staticVal, onChange);
478
468
  } else if (prop.format === "date") {
479
- widgetTpl = html`<sp-textfield
480
- size="s"
481
- placeholder="YYYY-MM-DD"
482
- .value=${live(staticVal)}
483
- @input=${(e: Event) => {
484
- clearTimeout(debounce);
485
- debounce = setTimeout(() => onChange((e.target as HTMLInputElement).value), 400);
486
- }}
487
- ></sp-textfield>`;
469
+ widgetTpl = spTextField(
470
+ `cprop:${prop.name}`,
471
+ String(staticVal),
472
+ (v: string) => onChange(v),
473
+ {
474
+ placeholder: "YYYY-MM-DD",
475
+ },
476
+ );
488
477
  } else if (parsed.kind === "boolean") {
489
478
  widgetTpl = html`<sp-checkbox
490
479
  size="s"
@@ -511,14 +500,9 @@ function renderComponentPropsFieldsTemplate(
511
500
  onChange(e.detail?.value ?? (e.target as HTMLInputElement).value)}
512
501
  ></jx-value-selector>`;
513
502
  } else {
514
- widgetTpl = html`<sp-textfield
515
- size="s"
516
- .value=${live(staticVal)}
517
- @input=${(e: Event) => {
518
- clearTimeout(debounce);
519
- debounce = setTimeout(() => onChange((e.target as HTMLInputElement).value), 400);
520
- }}
521
- ></sp-textfield>`;
503
+ widgetTpl = spTextField(`cprop:${prop.name}`, String(staticVal), (v: string) =>
504
+ onChange(v),
505
+ );
522
506
  }
523
507
 
524
508
  return html`
@@ -1085,20 +1069,11 @@ export function renderPropertiesPanelTemplate(ctx: {
1085
1069
  : nothing}
1086
1070
  <sp-field-label size="s">ID</sp-field-label>
1087
1071
  </div>
1088
- <sp-textfield
1089
- size="s"
1090
- .value=${live(node.$id || "")}
1091
- @input=${debouncedStyleCommit("prop:$id", 400, (e: Event) => {
1092
- transactDoc(activeTab.value, (t) =>
1093
- mutateUpdateProperty(
1094
- t,
1095
- path,
1096
- "$id",
1097
- (e.target as HTMLInputElement).value || undefined,
1098
- ),
1099
- );
1100
- })}
1101
- ></sp-textfield>
1072
+ ${spTextField("prop:$id", String(node.$id || ""), (v: string) =>
1073
+ transactDoc(activeTab.value, (t) =>
1074
+ mutateUpdateProperty(t, path, "$id", v || undefined),
1075
+ ),
1076
+ )}
1102
1077
  </div>
1103
1078
  <div class="style-row" data-prop="className">
1104
1079
  <div class="style-row-label">
@@ -1116,20 +1091,11 @@ export function renderPropertiesPanelTemplate(ctx: {
1116
1091
  : nothing}
1117
1092
  <sp-field-label size="s">Class</sp-field-label>
1118
1093
  </div>
1119
- <sp-textfield
1120
- size="s"
1121
- .value=${live(node.className || "")}
1122
- @input=${debouncedStyleCommit("prop:className", 400, (e: Event) => {
1123
- transactDoc(activeTab.value, (t) =>
1124
- mutateUpdateProperty(
1125
- t,
1126
- path,
1127
- "className",
1128
- (e.target as HTMLInputElement).value || undefined,
1129
- ),
1130
- );
1131
- })}
1132
- ></sp-textfield>
1094
+ ${spTextField("prop:className", String(node.className || ""), (v: string) =>
1095
+ transactDoc(activeTab.value, (t) =>
1096
+ mutateUpdateProperty(t, path, "className", v || undefined),
1097
+ ),
1098
+ )}
1133
1099
  </div>
1134
1100
  ${!Array.isArray(node.children) || node.children.length === 0
1135
1101
  ? html`
@@ -1149,25 +1115,14 @@ export function renderPropertiesPanelTemplate(ctx: {
1149
1115
  : nothing}
1150
1116
  <sp-field-label size="s">Text Content</sp-field-label>
1151
1117
  </div>
1152
- <sp-textfield
1153
- size="s"
1154
- multiline
1155
- .value=${live(
1156
- typeof node.textContent === "string"
1157
- ? node.textContent
1158
- : (node.textContent ?? ""),
1159
- )}
1160
- @input=${debouncedStyleCommit("prop:textContent", 400, (e: Event) => {
1118
+ ${spTextArea(
1119
+ "prop:textContent",
1120
+ typeof node.textContent === "string" ? node.textContent : "",
1121
+ (v: string) =>
1161
1122
  transactDoc(activeTab.value, (t) =>
1162
- mutateUpdateProperty(
1163
- t,
1164
- path,
1165
- "textContent",
1166
- (e.target as HTMLInputElement).value || undefined,
1167
- ),
1168
- );
1169
- })}
1170
- ></sp-textfield>
1123
+ mutateUpdateProperty(t, path, "textContent", v || undefined),
1124
+ ),
1125
+ )}
1171
1126
  </div>
1172
1127
  `
1173
1128
  : nothing}