@jxsuite/studio 1.0.0 → 1.1.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.
Files changed (86) hide show
  1. package/dist/iframe-entry.js +622 -49
  2. package/dist/iframe-entry.js.map +15 -12
  3. package/dist/studio.css +1333 -0
  4. package/dist/studio.js +53069 -31746
  5. package/dist/studio.js.map +109 -74
  6. package/package.json +11 -9
  7. package/src/canvas/canvas-live-render.ts +21 -2
  8. package/src/canvas/canvas-render.ts +35 -16
  9. package/src/canvas/canvas-utils.ts +118 -72
  10. package/src/canvas/iframe-entry.ts +20 -0
  11. package/src/canvas/iframe-eval.ts +155 -0
  12. package/src/canvas/iframe-host.ts +131 -5
  13. package/src/canvas/iframe-inline-edit.ts +107 -1
  14. package/src/canvas/iframe-protocol.ts +43 -3
  15. package/src/canvas/iframe-render.ts +18 -0
  16. package/src/collab/collab-session.ts +23 -8
  17. package/src/collab/collab-state.ts +6 -3
  18. package/src/component-props.ts +104 -0
  19. package/src/editor/inline-edit-apply.ts +36 -1
  20. package/src/editor/inline-edit.ts +58 -1
  21. package/src/editor/shortcuts.ts +41 -12
  22. package/src/files/file-ops.ts +14 -0
  23. package/src/files/files.ts +53 -1
  24. package/src/grid/cell-editors.ts +288 -0
  25. package/src/grid/cell-popovers.ts +108 -0
  26. package/src/grid/csv-codec.ts +122 -0
  27. package/src/grid/edit-buffer.ts +490 -0
  28. package/src/grid/grid-controller.ts +417 -0
  29. package/src/grid/grid-layout.ts +83 -0
  30. package/src/grid/grid-open.ts +167 -0
  31. package/src/grid/grid-panel.ts +302 -0
  32. package/src/grid/grid-source.ts +210 -0
  33. package/src/grid/grid-view.ts +367 -0
  34. package/src/grid/schema-columns.ts +261 -0
  35. package/src/grid/sources/connector-source.ts +217 -0
  36. package/src/grid/sources/content-source.ts +542 -0
  37. package/src/grid/sources/csv-file-source.ts +247 -0
  38. package/src/grid/tabulator-tables.d.ts +120 -0
  39. package/src/panels/block-action-bar.ts +8 -3
  40. package/src/panels/chat-panel.ts +98 -0
  41. package/src/panels/data-grid.ts +9 -387
  42. package/src/panels/editors.ts +43 -17
  43. package/src/panels/events-panel.ts +137 -44
  44. package/src/panels/formula-workspace.ts +379 -0
  45. package/src/panels/frontmatter-fields.ts +231 -0
  46. package/src/panels/frontmatter-panel.ts +132 -0
  47. package/src/panels/git-panel.ts +1 -1
  48. package/src/panels/head-panel.ts +11 -175
  49. package/src/panels/properties-panel.ts +154 -144
  50. package/src/panels/right-panel.ts +9 -47
  51. package/src/panels/signals-panel.ts +115 -23
  52. package/src/panels/statement-editor.ts +710 -0
  53. package/src/panels/style-panel.ts +25 -1
  54. package/src/panels/stylebook-panel.ts +0 -2
  55. package/src/panels/tab-bar.ts +175 -6
  56. package/src/panels/tab-strip.ts +62 -6
  57. package/src/panels/toolbar.ts +37 -3
  58. package/src/services/ai-project-tools.ts +541 -0
  59. package/src/services/ai-session-store.ts +28 -0
  60. package/src/services/ai-system-prompt.ts +194 -24
  61. package/src/services/ai-tools.ts +88 -21
  62. package/src/services/automation.ts +16 -0
  63. package/src/services/document-assistant.ts +81 -11
  64. package/src/services/gated-registry.ts +72 -0
  65. package/src/services/live-preview.ts +0 -0
  66. package/src/services/preview-eval.ts +68 -0
  67. package/src/services/project-adoption.ts +31 -0
  68. package/src/site-context.ts +2 -0
  69. package/src/store.ts +4 -0
  70. package/src/studio.ts +83 -52
  71. package/src/tabs/patch-ops.ts +25 -0
  72. package/src/tabs/tab.ts +39 -1
  73. package/src/tabs/transact.ts +60 -13
  74. package/src/types.ts +12 -0
  75. package/src/ui/dynamic-slot.ts +272 -0
  76. package/src/ui/expression-editor.ts +423 -125
  77. package/src/ui/field-row.ts +5 -0
  78. package/src/ui/formula-catalog.ts +557 -0
  79. package/src/ui/formula-chips.ts +216 -0
  80. package/src/ui/formula-palette.ts +211 -0
  81. package/src/ui/layers.ts +40 -0
  82. package/src/ui/media-picker.ts +1 -1
  83. package/src/ui/panel-resize.ts +15 -1
  84. package/src/utils/preview-format.ts +26 -0
  85. package/src/view.ts +4 -4
  86. package/src/workspace/workspace.ts +14 -0
@@ -0,0 +1,231 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Frontmatter-fields.ts — Shared schema-driven frontmatter field collection and renderers.
4
+ *
5
+ * Used by both frontmatter editing surfaces: the Document tab (head-panel) and the above-canvas
6
+ * Properties panel (frontmatter-panel). Fields come from the content-collection schema
7
+ * (`findContentTypeSchema`) plus any extra keys already present in the frontmatter; each renders as
8
+ * a typed widget committing through `mutateUpdateFrontmatter`.
9
+ */
10
+
11
+ import { html } from "lit-html";
12
+ import { live } from "lit-html/directives/live.js";
13
+ import { renderFieldRow } from "../ui/field-row";
14
+ import { spNumberField, spTextField } from "../ui/field-input";
15
+ import { renderMediaPicker } from "../ui/media-picker";
16
+ import { activeTab } from "../workspace/workspace";
17
+ import { mutateUpdateFrontmatter, transactDoc } from "../tabs/transact";
18
+ import { findContentTypeSchema } from "../utils/studio-utils";
19
+
20
+ import type { JsonValue } from "../types";
21
+ import type { Tab } from "../tabs/tab";
22
+ import type { ProjectConfig } from "@jxsuite/schema/types";
23
+
24
+ export interface FmSchemaEntry {
25
+ type?: string;
26
+ enum?: string[];
27
+ format?: string;
28
+ properties?: Record<string, unknown>;
29
+ }
30
+
31
+ export interface FmField {
32
+ field: string;
33
+ entry: FmSchemaEntry;
34
+ value: JsonValue;
35
+ }
36
+
37
+ export interface FmFieldSet {
38
+ /** Matched content collection, or null when the doc isn't part of one. */
39
+ collection: { name: string; schema: unknown } | null;
40
+ /** Schema-declared fields (schema order) followed by extra frontmatter keys. */
41
+ fields: FmField[];
42
+ requiredFields: Set<string>;
43
+ /** True when the matched collection declares `schema.properties`. */
44
+ hasSchema: boolean;
45
+ }
46
+
47
+ /**
48
+ * Collect the frontmatter fields to display for a tab: schema-declared fields first (in schema
49
+ * order), then any extra frontmatter keys not in the schema with types inferred from their values.
50
+ * `$`-prefixed keys and `reserved` keys are skipped.
51
+ *
52
+ * @param {Tab} tab
53
+ * @param {ProjectConfig | null | undefined} projectConfig
54
+ * @param {Set<string>} reserved — keys managed by dedicated controls elsewhere
55
+ * @returns {FmFieldSet}
56
+ */
57
+ export function collectFmFields(
58
+ tab: Tab,
59
+ projectConfig: ProjectConfig | null | undefined,
60
+ reserved: Set<string>,
61
+ ): FmFieldSet {
62
+ const fm = tab.doc.content?.frontmatter || {};
63
+ const collection = findContentTypeSchema(tab.documentPath, projectConfig);
64
+ const schema = collection?.schema as
65
+ | { properties?: Record<string, FmSchemaEntry>; required?: string[] }
66
+ | undefined;
67
+ const schemaProps = schema?.properties;
68
+ const requiredFields = new Set(schema?.required || []);
69
+
70
+ const fields: FmField[] = [];
71
+ if (schemaProps) {
72
+ for (const [field, fieldSchema] of Object.entries(schemaProps)) {
73
+ if (reserved.has(field)) {
74
+ continue;
75
+ }
76
+ fields.push({ entry: fieldSchema, field, value: fm[field] as JsonValue });
77
+ }
78
+ for (const [field, value] of Object.entries(fm)) {
79
+ if (schemaProps[field] || field.startsWith("$") || reserved.has(field)) {
80
+ continue;
81
+ }
82
+ fields.push({
83
+ entry: { type: typeof value === "boolean" ? "boolean" : "string" },
84
+ field,
85
+ value: value as JsonValue,
86
+ });
87
+ }
88
+ } else {
89
+ for (const [field, value] of Object.entries(fm)) {
90
+ if (field.startsWith("$") || reserved.has(field)) {
91
+ continue;
92
+ }
93
+ fields.push({
94
+ entry: { type: typeof value === "boolean" ? "boolean" : "string" },
95
+ field,
96
+ value: value as JsonValue,
97
+ });
98
+ }
99
+ }
100
+
101
+ return { collection, fields, hasSchema: Boolean(schemaProps), requiredFields };
102
+ }
103
+
104
+ /**
105
+ * Render one frontmatter field as a typed widget row. Commits through `transactDoc` +
106
+ * `mutateUpdateFrontmatter` on the active tab.
107
+ *
108
+ * @param {string} field
109
+ * @param {FmSchemaEntry} entry
110
+ * @param {JsonValue} value
111
+ * @param {Set<string>} requiredFields
112
+ * @returns {import("lit-html").TemplateResult}
113
+ */
114
+ export function renderFmField(
115
+ field: string,
116
+ entry: FmSchemaEntry,
117
+ value: JsonValue,
118
+ requiredFields: Set<string>,
119
+ ) {
120
+ const isRequired = requiredFields.has(field);
121
+ const label = field.replaceAll(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
122
+ const displayLabel = label + (isRequired ? " *" : "");
123
+ const hasVal = value !== undefined && value !== "" && value !== false;
124
+ const onClear = () => transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field));
125
+
126
+ if (entry.type === "boolean") {
127
+ return renderFieldRow({
128
+ hasValue: hasVal,
129
+ label: displayLabel,
130
+ onClear,
131
+ prop: field,
132
+ widget: html`
133
+ <sp-checkbox
134
+ size="s"
135
+ .checked=${live(Boolean(value))}
136
+ @change=${(e: Event) =>
137
+ transactDoc(activeTab.value, (t) =>
138
+ mutateUpdateFrontmatter(
139
+ t,
140
+ field,
141
+ (e.target as HTMLInputElement).checked || undefined,
142
+ ),
143
+ )}
144
+ ></sp-checkbox>
145
+ `,
146
+ });
147
+ }
148
+
149
+ if (entry.type === "array") {
150
+ const display = Array.isArray(value) ? value.join(", ") : (value as string) || "";
151
+ return renderFieldRow({
152
+ hasValue: hasVal,
153
+ label: displayLabel,
154
+ onClear,
155
+ prop: field,
156
+ widget: spTextField(
157
+ `fm:${field}`,
158
+ display,
159
+ (v: string) => {
160
+ const arr = v
161
+ ? v
162
+ .split(",")
163
+ .map((s: string) => s.trim())
164
+ .filter(Boolean)
165
+ : undefined;
166
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, arr));
167
+ },
168
+ { placeholder: "comma, separated" },
169
+ ),
170
+ });
171
+ }
172
+
173
+ if (Array.isArray(entry.enum)) {
174
+ return renderFieldRow({
175
+ hasValue: hasVal,
176
+ label: displayLabel,
177
+ onClear,
178
+ prop: field,
179
+ widget: html`
180
+ <sp-picker
181
+ size="s"
182
+ .value=${live(value || "")}
183
+ @change=${(e: Event) =>
184
+ transactDoc(activeTab.value, (t) =>
185
+ mutateUpdateFrontmatter(t, field, (e.target as HTMLInputElement).value || undefined),
186
+ )}
187
+ >
188
+ ${entry.enum.map((opt: string) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`)}
189
+ </sp-picker>
190
+ `,
191
+ });
192
+ }
193
+
194
+ if (entry.format === "image") {
195
+ return renderFieldRow({
196
+ hasValue: hasVal,
197
+ label: displayLabel,
198
+ onClear,
199
+ prop: field,
200
+ widget: renderMediaPicker(field, value as string, (v: string) =>
201
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
202
+ ),
203
+ });
204
+ }
205
+
206
+ if (entry.type === "number") {
207
+ return renderFieldRow({
208
+ hasValue: hasVal,
209
+ label: displayLabel,
210
+ onClear,
211
+ prop: field,
212
+ widget: spNumberField(value !== undefined ? Number(value) : undefined, (n) =>
213
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, n)),
214
+ ),
215
+ });
216
+ }
217
+
218
+ return renderFieldRow({
219
+ hasValue: hasVal,
220
+ label: displayLabel,
221
+ onClear,
222
+ prop: field,
223
+ widget: spTextField(
224
+ `fm:${field}`,
225
+ (value as string) || "",
226
+ (v: string) =>
227
+ transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
228
+ { placeholder: entry.format === "date" ? "YYYY-MM-DD" : "" },
229
+ ),
230
+ });
231
+ }
@@ -0,0 +1,132 @@
1
+ /// <reference lib="dom" />
2
+ /**
3
+ * Frontmatter-panel.ts — Obsidian-style "Properties" panel above the canvas.
4
+ *
5
+ * Shown only when the active tab is a content-collection document (matched via
6
+ * `findContentTypeSchema`) in content mode with the effective canvas mode "edit". The fields sit
7
+ * inside a single collapsible accordion item so the panel can be dismissed to a slim header bar;
8
+ * the expanded/collapsed state persists per tab (`tab.session.ui.frontmatterOpen`).
9
+ *
10
+ * Unlike the Document-tab section (head-panel), no keys are reserved here — `title` renders as a
11
+ * regular property, matching Obsidian.
12
+ */
13
+
14
+ import { html, nothing, render as litRender } from "lit-html";
15
+ import { frontmatterPanelEl, projectState } from "../store";
16
+ import { activeTab } from "../workspace/workspace";
17
+ import { effect, effectScope } from "../reactivity";
18
+ import { createPanelScheduler } from "./panel-scheduler";
19
+ import { collectFmFields, renderFmField } from "./frontmatter-fields";
20
+
21
+ import type { PanelScheduler } from "./panel-scheduler";
22
+
23
+ interface FrontmatterPanelCtx {
24
+ /** Effective canvas mode (composes the base mode with the preview toggle). */
25
+ getCanvasMode: () => string;
26
+ }
27
+
28
+ const NO_RESERVED_KEYS = new Set<string>();
29
+
30
+ let _ctx: FrontmatterPanelCtx | null = null;
31
+ let _scheduler: PanelScheduler | null = null;
32
+ let _scope: { stop: () => void; run: <T>(fn: () => T) => T | undefined } | null = null;
33
+
34
+ /**
35
+ * Mount the frontmatter panel: bind the focus-aware scheduler to the shell host and re-render
36
+ * reactively on tab/mode/frontmatter changes.
37
+ *
38
+ * @param {FrontmatterPanelCtx} ctx
39
+ */
40
+ export function mount(ctx: FrontmatterPanelCtx) {
41
+ if (!frontmatterPanelEl) {
42
+ return;
43
+ }
44
+ _ctx = ctx;
45
+ _scheduler = createPanelScheduler({ render: _doRender, root: frontmatterPanelEl });
46
+ _scheduler.bindFocus();
47
+ _scope = effectScope();
48
+ _scope.run(() => {
49
+ effect(() => {
50
+ const tab = activeTab.value;
51
+ if (tab) {
52
+ // Track everything the panel reads: mode/ui flags plus the frontmatter object itself AND
53
+ // Its entries — field commits mutate keys in place while the source-mode round-trip swaps
54
+ // The whole object; both must re-fire this effect.
55
+ void tab.doc.mode;
56
+ void tab.session.ui.canvasMode;
57
+ void tab.session.ui.preview;
58
+ void tab.session.ui.frontmatterOpen;
59
+ const fm = tab.doc.content?.frontmatter;
60
+ if (fm) {
61
+ for (const key of Object.keys(fm)) {
62
+ void fm[key];
63
+ }
64
+ }
65
+ }
66
+ render();
67
+ });
68
+ });
69
+ }
70
+
71
+ export function unmount() {
72
+ _scope?.stop();
73
+ _scope = null;
74
+ _ctx = null;
75
+ _scheduler?.unbind();
76
+ _scheduler = null;
77
+ }
78
+
79
+ /**
80
+ * Request a render. Coalesced and deferred while a text input in the panel is focused, so
81
+ * re-renders never clobber a field mid-edit.
82
+ */
83
+ export function render() {
84
+ _scheduler?.schedule();
85
+ }
86
+
87
+ function _doRender() {
88
+ if (!_ctx || !frontmatterPanelEl) {
89
+ return;
90
+ }
91
+
92
+ const tab = activeTab.value;
93
+ const eligible = Boolean(
94
+ tab && tab.doc.mode === "content" && _ctx.getCanvasMode() === "edit" && tab.documentPath,
95
+ );
96
+ const fieldSet = eligible
97
+ ? collectFmFields(tab!, projectState?.projectConfig, NO_RESERVED_KEYS)
98
+ : null;
99
+
100
+ if (!fieldSet?.collection) {
101
+ // Lit leaves comment markers, so the host is hidden explicitly rather than relying on :empty.
102
+ litRender(nothing, frontmatterPanelEl);
103
+ frontmatterPanelEl.hidden = true;
104
+ return;
105
+ }
106
+
107
+ const { collection, fields, requiredFields } = fieldSet;
108
+ const open = tab!.session.ui.frontmatterOpen !== false;
109
+
110
+ litRender(
111
+ html`
112
+ <sp-accordion size="s">
113
+ <sp-accordion-item
114
+ label="Properties · ${collection.name}"
115
+ ?open=${open}
116
+ @sp-accordion-item-toggle=${(e: Event) => {
117
+ const t = activeTab.value;
118
+ if (t) {
119
+ t.session.ui.frontmatterOpen = (e.target as HTMLElement & { open: boolean }).open;
120
+ }
121
+ }}
122
+ >
123
+ <div class="fm-panel-body">
124
+ ${fields.map((f) => renderFmField(f.field, f.entry, f.value, requiredFields))}
125
+ </div>
126
+ </sp-accordion-item>
127
+ </sp-accordion>
128
+ `,
129
+ frontmatterPanelEl,
130
+ );
131
+ frontmatterPanelEl.hidden = false;
132
+ }
@@ -471,7 +471,7 @@ export function renderGitPanel(
471
471
  .value=${live(S.ui.gitCommitMessage || "")}
472
472
  @input=${(e: Event) => updateUi("gitCommitMessage", (e.target as HTMLInputElement).value)}
473
473
  @keydown=${(e: KeyboardEvent) => {
474
- if (e.ctrlKey && e.key === "Enter") {
474
+ if ((e.ctrlKey || e.metaKey) && e.key === "Enter") {
475
475
  e.preventDefault();
476
476
  void doCommit();
477
477
  }
@@ -7,15 +7,13 @@
7
7
  */
8
8
 
9
9
  import { html, nothing } from "lit-html";
10
- import { live } from "lit-html/directives/live.js";
11
10
  import { renderFieldRow } from "../ui/field-row";
12
- import { spNumberField, spTextArea, spTextField } from "../ui/field-input";
11
+ import { spTextArea, spTextField } from "../ui/field-input";
13
12
  import { renderMediaPicker } from "../ui/media-picker";
14
13
  import { projectState, renderOnly } from "../store";
15
- import type { DirEntry, JsonValue } from "../types";
14
+ import type { DirEntry } from "../types";
16
15
  import { activeTab } from "../workspace/workspace";
17
- import { mutateUpdateFrontmatter, transactDoc } from "../tabs/transact";
18
- import { findContentTypeSchema } from "../utils/studio-utils";
16
+ import { collectFmFields, renderFmField } from "./frontmatter-fields";
19
17
  import { isGoogleFontEntry, isGoogleFontPreconnect } from "../utils/google-fonts";
20
18
  import { invalidateLayoutCache } from "../site-context";
21
19
  import { getPlatform } from "../platform";
@@ -31,13 +29,6 @@ interface MetaField {
31
29
  media?: boolean;
32
30
  }
33
31
 
34
- interface FmSchemaEntry {
35
- type?: string;
36
- enum?: string[];
37
- format?: string;
38
- properties?: Record<string, unknown>;
39
- }
40
-
41
32
  // ─── Layout picker ──────────────────────────────────────────────────────────
42
33
 
43
34
  /** @type {{ name: string; path: string }[] | null} */
@@ -564,49 +555,13 @@ function renderFrontmatterSection() {
564
555
  return nothing;
565
556
  }
566
557
 
567
- const fm = tab.doc.content?.frontmatter || {};
568
- const col = findContentTypeSchema(tab.documentPath, projectState?.projectConfig);
569
- const schema = col?.schema as
570
- | { properties?: Record<string, FmSchemaEntry>; required?: string[] }
571
- | undefined;
572
- const schemaProps = schema?.properties;
573
- const requiredFields = new Set(schema?.required || []);
574
-
575
- /** @type {{ field: string; entry: FmSchemaEntry; value: JsonValue }[]} */
576
- const fields = [];
577
- if (schemaProps) {
578
- for (const [field, fieldSchema] of Object.entries(
579
- /** @type {Record<string, FmSchemaEntry>} */ schemaProps,
580
- )) {
581
- if (RESERVED_FM_KEYS.has(field)) {
582
- continue;
583
- }
584
- fields.push({ entry: fieldSchema, field, value: fm[field] as JsonValue });
585
- }
586
- for (const [field, value] of Object.entries(fm)) {
587
- if (schemaProps[field] || field.startsWith("$") || RESERVED_FM_KEYS.has(field)) {
588
- continue;
589
- }
590
- fields.push({
591
- entry: { type: typeof value === "boolean" ? "boolean" : "string" },
592
- field,
593
- value: /** @type {JsonValue} */ value,
594
- });
595
- }
596
- } else {
597
- for (const [field, value] of Object.entries(fm)) {
598
- if (field.startsWith("$") || RESERVED_FM_KEYS.has(field)) {
599
- continue;
600
- }
601
- fields.push({
602
- entry: { type: typeof value === "boolean" ? "boolean" : "string" },
603
- field,
604
- value: /** @type {JsonValue} */ value,
605
- });
606
- }
607
- }
558
+ const { collection, fields, hasSchema, requiredFields } = collectFmFields(
559
+ tab,
560
+ projectState?.projectConfig,
561
+ RESERVED_FM_KEYS,
562
+ );
608
563
 
609
- if (fields.length === 0 && !schemaProps) {
564
+ if (fields.length === 0 && !hasSchema) {
610
565
  return nothing;
611
566
  }
612
567
 
@@ -614,131 +569,12 @@ function renderFrontmatterSection() {
614
569
  <div class="imports-section">
615
570
  <div class="imports-section-header">
616
571
  <span class="imports-section-title"
617
- >${col ? `Frontmatter (${col.name})` : "Frontmatter"}</span
572
+ >${collection ? `Frontmatter (${collection.name})` : "Frontmatter"}</span
618
573
  >
619
574
  </div>
620
575
  <div class="head-section-body">
621
- ${fields.map((f) => renderFmField(f.field, f.entry, f.value as JsonValue, requiredFields))}
576
+ ${fields.map((f) => renderFmField(f.field, f.entry, f.value, requiredFields))}
622
577
  </div>
623
578
  </div>
624
579
  `;
625
580
  }
626
-
627
- function renderFmField(
628
- field: string,
629
- entry: FmSchemaEntry,
630
- value: JsonValue,
631
- requiredFields: Set<string>,
632
- ) {
633
- const isRequired = requiredFields.has(field);
634
- const label = field.replaceAll(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
635
- const displayLabel = label + (isRequired ? " *" : "");
636
- const hasVal = value !== undefined && value !== "" && value !== false;
637
- const onClear = () => transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field));
638
-
639
- if (entry.type === "boolean") {
640
- return renderFieldRow({
641
- hasValue: hasVal,
642
- label: displayLabel,
643
- onClear,
644
- prop: field,
645
- widget: html`
646
- <sp-checkbox
647
- size="s"
648
- .checked=${live(Boolean(value))}
649
- @change=${(e: Event) =>
650
- transactDoc(activeTab.value, (t) =>
651
- mutateUpdateFrontmatter(
652
- t,
653
- field,
654
- (e.target as HTMLInputElement).checked || undefined,
655
- ),
656
- )}
657
- ></sp-checkbox>
658
- `,
659
- });
660
- }
661
-
662
- if (entry.type === "array") {
663
- const display = Array.isArray(value) ? value.join(", ") : (value as string) || "";
664
- return renderFieldRow({
665
- hasValue: hasVal,
666
- label: displayLabel,
667
- onClear,
668
- prop: field,
669
- widget: spTextField(
670
- `fm:${field}`,
671
- display,
672
- (v: string) => {
673
- const arr = v
674
- ? v
675
- .split(",")
676
- .map((s: string) => s.trim())
677
- .filter(Boolean)
678
- : undefined;
679
- transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, arr));
680
- },
681
- { placeholder: "comma, separated" },
682
- ),
683
- });
684
- }
685
-
686
- if (Array.isArray(entry.enum)) {
687
- return renderFieldRow({
688
- hasValue: hasVal,
689
- label: displayLabel,
690
- onClear,
691
- prop: field,
692
- widget: html`
693
- <sp-picker
694
- size="s"
695
- .value=${live(value || "")}
696
- @change=${(e: Event) =>
697
- transactDoc(activeTab.value, (t) =>
698
- mutateUpdateFrontmatter(t, field, (e.target as HTMLInputElement).value || undefined),
699
- )}
700
- >
701
- ${entry.enum.map((opt: string) => html`<sp-menu-item value=${opt}>${opt}</sp-menu-item>`)}
702
- </sp-picker>
703
- `,
704
- });
705
- }
706
-
707
- if (entry.format === "image") {
708
- return renderFieldRow({
709
- hasValue: hasVal,
710
- label: displayLabel,
711
- onClear,
712
- prop: field,
713
- widget: renderMediaPicker(field, value as string, (v: string) =>
714
- transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
715
- ),
716
- });
717
- }
718
-
719
- if (entry.type === "number") {
720
- return renderFieldRow({
721
- hasValue: hasVal,
722
- label: displayLabel,
723
- onClear,
724
- prop: field,
725
- widget: spNumberField(value !== undefined ? Number(value) : undefined, (n) =>
726
- transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, n)),
727
- ),
728
- });
729
- }
730
-
731
- return renderFieldRow({
732
- hasValue: hasVal,
733
- label: displayLabel,
734
- onClear,
735
- prop: field,
736
- widget: spTextField(
737
- `fm:${field}`,
738
- (value as string) || "",
739
- (v: string) =>
740
- transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
741
- { placeholder: entry.format === "date" ? "YYYY-MM-DD" : "" },
742
- ),
743
- });
744
- }