@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.
Files changed (104) hide show
  1. package/dist/studio.js +86144 -83971
  2. package/dist/studio.js.map +229 -219
  3. package/package.json +5 -5
  4. package/src/browse/browse-modal.ts +9 -3
  5. package/src/browse/browse.ts +139 -82
  6. package/src/canvas/canvas-diff.ts +35 -18
  7. package/src/canvas/canvas-helpers.ts +55 -21
  8. package/src/canvas/canvas-live-render.ts +140 -89
  9. package/src/canvas/canvas-render.ts +127 -82
  10. package/src/canvas/canvas-utils.ts +68 -42
  11. package/src/canvas/nested-site-style.ts +15 -7
  12. package/src/editor/component-inline-edit.ts +90 -51
  13. package/src/editor/content-inline-edit.ts +75 -72
  14. package/src/editor/context-menu.ts +125 -71
  15. package/src/editor/convert-targets.ts +17 -13
  16. package/src/editor/convert-to-component.ts +51 -28
  17. package/src/editor/convert-to-repeater.ts +44 -19
  18. package/src/editor/inline-edit.ts +107 -52
  19. package/src/editor/inline-format.ts +128 -60
  20. package/src/editor/insertion-helper.ts +26 -14
  21. package/src/editor/shortcuts.ts +89 -43
  22. package/src/editor/slash-menu.ts +41 -26
  23. package/src/files/components.ts +9 -7
  24. package/src/files/file-ops.ts +53 -33
  25. package/src/files/files.ts +246 -143
  26. package/src/format/constraints.ts +25 -15
  27. package/src/format/format-host.ts +33 -10
  28. package/src/github/github-auth.ts +24 -14
  29. package/src/github/github-publish.ts +20 -14
  30. package/src/new-project/new-project-modal.ts +27 -18
  31. package/src/panels/activity-bar.ts +27 -26
  32. package/src/panels/ai-panel.ts +100 -44
  33. package/src/panels/block-action-bar.ts +84 -52
  34. package/src/panels/canvas-dnd.ts +47 -28
  35. package/src/panels/data-explorer.ts +24 -11
  36. package/src/panels/dnd.ts +148 -100
  37. package/src/panels/editors.ts +54 -37
  38. package/src/panels/elements-panel.ts +23 -13
  39. package/src/panels/events-panel.ts +48 -38
  40. package/src/panels/git-panel.ts +95 -60
  41. package/src/panels/head-panel.ts +138 -84
  42. package/src/panels/imports-panel.ts +66 -29
  43. package/src/panels/layers-panel.ts +80 -36
  44. package/src/panels/left-panel.ts +86 -53
  45. package/src/panels/overlays.ts +36 -19
  46. package/src/panels/panel-events.ts +48 -27
  47. package/src/panels/panel-scheduler.ts +41 -23
  48. package/src/panels/preview-render.ts +26 -26
  49. package/src/panels/properties-panel.ts +155 -118
  50. package/src/panels/pseudo-preview.ts +31 -15
  51. package/src/panels/quick-search.ts +28 -13
  52. package/src/panels/right-panel.ts +35 -24
  53. package/src/panels/shared.ts +46 -29
  54. package/src/panels/signals-panel.ts +303 -186
  55. package/src/panels/statusbar.ts +27 -12
  56. package/src/panels/style-inputs.ts +30 -24
  57. package/src/panels/style-panel.ts +130 -90
  58. package/src/panels/style-utils.ts +63 -23
  59. package/src/panels/stylebook-layers-panel.ts +27 -30
  60. package/src/panels/stylebook-panel.ts +140 -72
  61. package/src/panels/tab-strip.ts +21 -10
  62. package/src/panels/toolbar.ts +52 -40
  63. package/src/panels/welcome-screen.ts +1 -1
  64. package/src/platform.ts +2 -1
  65. package/src/platforms/devserver.ts +221 -105
  66. package/src/recent-projects.ts +15 -7
  67. package/src/resize-edges.ts +7 -5
  68. package/src/services/cem-export.ts +73 -57
  69. package/src/services/code-services.ts +52 -25
  70. package/src/services/monaco-setup.ts +5 -5
  71. package/src/settings/content-types-editor.ts +133 -62
  72. package/src/settings/css-vars-editor.ts +31 -14
  73. package/src/settings/defs-editor.ts +125 -63
  74. package/src/settings/general-settings.ts +11 -6
  75. package/src/settings/head-editor.ts +30 -15
  76. package/src/settings/schema-field-ui.ts +75 -35
  77. package/src/settings/settings-modal.ts +35 -17
  78. package/src/site-context.ts +80 -47
  79. package/src/state.ts +98 -65
  80. package/src/store.ts +42 -23
  81. package/src/studio.ts +165 -144
  82. package/src/tabs/tab.ts +51 -45
  83. package/src/tabs/transact.ts +230 -109
  84. package/src/types.ts +60 -51
  85. package/src/ui/button-group.ts +3 -2
  86. package/src/ui/color-selector.ts +33 -15
  87. package/src/ui/expression-editor.ts +139 -86
  88. package/src/ui/field-input.ts +35 -18
  89. package/src/ui/field-row.ts +1 -1
  90. package/src/ui/icons.ts +5 -6
  91. package/src/ui/layers.ts +33 -26
  92. package/src/ui/media-picker.ts +13 -7
  93. package/src/ui/panel-resize.ts +39 -20
  94. package/src/ui/spectrum.ts +4 -2
  95. package/src/ui/unit-selector.ts +20 -11
  96. package/src/ui/value-selector.ts +13 -7
  97. package/src/ui/widgets.ts +34 -18
  98. package/src/utils/canvas-media.ts +66 -33
  99. package/src/utils/edit-display.ts +43 -44
  100. package/src/utils/google-fonts.ts +10 -6
  101. package/src/utils/inherited-style.ts +22 -8
  102. package/src/utils/studio-utils.ts +88 -45
  103. package/src/view.ts +9 -5
  104. package/src/workspace/workspace.ts +49 -29
@@ -9,18 +9,19 @@
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
+ import { spNumberField, spTextArea, spTextField } from "../ui/field-input";
13
13
  import { renderMediaPicker } from "../ui/media-picker";
14
- import { renderOnly, projectState } from "../store";
15
- import type { JsonValue, DirEntry } from "../types";
14
+ import { projectState, renderOnly } from "../store";
15
+ import type { DirEntry, JsonValue } from "../types";
16
16
  import { activeTab } from "../workspace/workspace";
17
- import { transactDoc, mutateUpdateFrontmatter } from "../tabs/transact";
17
+ import { mutateUpdateFrontmatter, transactDoc } from "../tabs/transact";
18
18
  import { findContentTypeSchema } from "../utils/studio-utils";
19
19
  import { isGoogleFontEntry, isGoogleFontPreconnect } from "../utils/google-fonts";
20
20
  import { invalidateLayoutCache } from "../site-context";
21
21
  import { getPlatform } from "../platform";
22
22
 
23
23
  import type { JxHeadEntry, JxMutableNode } from "@jxsuite/schema/types";
24
+ import type { TemplateResult } from "lit-html";
24
25
 
25
26
  interface MetaField {
26
27
  label: string;
@@ -51,8 +52,8 @@ async function loadLayoutEntries() {
51
52
  .map((f: DirEntry) => ({
52
53
  name: f.name
53
54
  .replace(/\.json$/, "")
54
- .replace(/[-_]+/g, " ")
55
- .replace(/\b\w/g, (c) => c.toUpperCase()),
55
+ .replaceAll(/[-_]+/g, " ")
56
+ .replaceAll(/\b\w/g, (c) => c.toUpperCase()),
56
57
  path: `./layouts/${f.name}`,
57
58
  }));
58
59
  } catch {
@@ -68,20 +69,20 @@ export function invalidateLayoutPickerCache() {
68
69
  // ─── Field definitions ───────────────────────────────────────────────────
69
70
 
70
71
  const PAGE_FIELDS: MetaField[] = [
71
- { label: "Description", attr: "name", key: "description" },
72
- { label: "Viewport", attr: "name", key: "viewport" },
72
+ { attr: "name", key: "description", label: "Description" },
73
+ { attr: "name", key: "viewport", label: "Viewport" },
73
74
  ];
74
75
 
75
76
  const OG_FIELDS: MetaField[] = [
76
- { label: "Title", attr: "property", key: "og:title" },
77
+ { attr: "property", key: "og:title", label: "Title" },
77
78
  {
78
- label: "Description",
79
79
  attr: "property",
80
80
  key: "og:description",
81
+ label: "Description",
81
82
  multiline: true,
82
83
  },
83
- { label: "Image", attr: "property", key: "og:image", media: true },
84
- { label: "Type", attr: "property", key: "og:type" },
84
+ { attr: "property", key: "og:image", label: "Image", media: true },
85
+ { attr: "property", key: "og:type", label: "Type" },
85
86
  ];
86
87
 
87
88
  /** Set of `name`/`property` values managed by the structured forms. */
@@ -101,7 +102,9 @@ const RESERVED_FM_KEYS = new Set(["title"]);
101
102
  * @returns {JxHeadEntry | undefined}
102
103
  */
103
104
  function findMetaEntry(head: JxHeadEntry[], attr: "name" | "property", key: string) {
104
- if (!head) return undefined;
105
+ if (!head) {
106
+ return;
107
+ }
105
108
  return head.find((e: JxHeadEntry) => e?.tagName === "meta" && e?.attributes?.[attr] === key);
106
109
  }
107
110
 
@@ -113,7 +116,9 @@ function findMetaEntry(head: JxHeadEntry[], attr: "name" | "property", key: stri
113
116
  * @returns {JxHeadEntry | undefined}
114
117
  */
115
118
  function findLinkEntry(head: JxHeadEntry[], rel: string) {
116
- if (!head) return undefined;
119
+ if (!head) {
120
+ return;
121
+ }
117
122
  return head.find((e: JxHeadEntry) => e?.tagName === "link" && e?.attributes?.rel === rel);
118
123
  }
119
124
 
@@ -124,15 +129,21 @@ function findLinkEntry(head: JxHeadEntry[], rel: string) {
124
129
  * @returns {boolean}
125
130
  */
126
131
  function isManagedEntry(entry: JxHeadEntry) {
127
- if (!entry?.tagName) return false;
132
+ if (!entry?.tagName) {
133
+ return false;
134
+ }
128
135
  // Managed meta tags
129
136
  if (entry.tagName === "meta") {
130
137
  const name = String(entry?.attributes?.name ?? "");
131
138
  const prop = String(entry?.attributes?.property ?? "");
132
- return !!(name && MANAGED_META_KEYS.has(name)) || !!(prop && MANAGED_META_KEYS.has(prop));
139
+ return (
140
+ Boolean(name && MANAGED_META_KEYS.has(name)) || Boolean(prop && MANAGED_META_KEYS.has(prop))
141
+ );
133
142
  }
134
143
  // Managed link: favicon
135
- if (entry.tagName === "link" && entry?.attributes?.rel === "icon") return true;
144
+ if (entry.tagName === "link" && entry?.attributes?.rel === "icon") {
145
+ return true;
146
+ }
136
147
  return false;
137
148
  }
138
149
 
@@ -145,18 +156,20 @@ function isManagedEntry(entry: JxHeadEntry) {
145
156
  * @param {string} content
146
157
  */
147
158
  function upsertMeta(doc: JxMutableNode, attr: "name" | "property", key: string, content: string) {
148
- if (!doc.$head) doc.$head = [];
159
+ if (!doc.$head) {
160
+ doc.$head = [];
161
+ }
149
162
  const idx = doc.$head.findIndex(
150
163
  (e: JxHeadEntry) => e?.tagName === "meta" && e?.attributes?.[attr] === key,
151
164
  );
152
165
  if (content) {
153
- const entry = { tagName: "meta", attributes: { [attr]: key, content } };
154
- if (idx >= 0) {
166
+ const entry = { attributes: { [attr]: key, content }, tagName: "meta" };
167
+ if (idx !== -1) {
155
168
  doc.$head[idx] = entry;
156
169
  } else {
157
170
  doc.$head.push(entry);
158
171
  }
159
- } else if (idx >= 0) {
172
+ } else if (idx !== -1) {
160
173
  doc.$head.splice(idx, 1);
161
174
  }
162
175
  }
@@ -169,18 +182,20 @@ function upsertMeta(doc: JxMutableNode, attr: "name" | "property", key: string,
169
182
  * @param {string} href
170
183
  */
171
184
  function upsertLink(doc: JxMutableNode, rel: string, href: string) {
172
- if (!doc.$head) doc.$head = [];
185
+ if (!doc.$head) {
186
+ doc.$head = [];
187
+ }
173
188
  const idx = doc.$head.findIndex(
174
189
  (e: JxHeadEntry) => e?.tagName === "link" && e?.attributes?.rel === rel,
175
190
  );
176
191
  if (href) {
177
- const entry = { tagName: "link", attributes: { rel, href } };
178
- if (idx >= 0) {
192
+ const entry = { attributes: { href, rel }, tagName: "link" };
193
+ if (idx !== -1) {
179
194
  doc.$head[idx] = entry;
180
195
  } else {
181
196
  doc.$head.push(entry);
182
197
  }
183
- } else if (idx >= 0) {
198
+ } else if (idx !== -1) {
184
199
  doc.$head.splice(idx, 1);
185
200
  }
186
201
  }
@@ -192,13 +207,25 @@ function upsertLink(doc: JxMutableNode, rel: string, href: string) {
192
207
  * @returns {string}
193
208
  */
194
209
  function entryLabel(entry: JxHeadEntry) {
195
- if (!entry?.tagName) return "unknown";
210
+ if (!entry?.tagName) {
211
+ return "unknown";
212
+ }
196
213
  const a = entry.attributes ?? {};
197
- if (a.name) return `<meta name="${String(a.name)}">`;
198
- if (a.property) return `<meta property="${String(a.property)}">`;
199
- if (a.rel && a.href) return `<link rel="${String(a.rel)}">`;
200
- if (a.src) return `<script src="${String(a.src)}">`;
201
- if (a.charset) return `<meta charset="${String(a.charset)}">`;
214
+ if (a.name) {
215
+ return `<meta name="${String(a.name)}">`;
216
+ }
217
+ if (a.property) {
218
+ return `<meta property="${String(a.property)}">`;
219
+ }
220
+ if (a.rel && a.href) {
221
+ return `<link rel="${String(a.rel)}">`;
222
+ }
223
+ if (a.src) {
224
+ return `<script src="${String(a.src)}">`;
225
+ }
226
+ if (a.charset) {
227
+ return `<meta charset="${String(a.charset)}">`;
228
+ }
202
229
  return `<${entry.tagName}>`;
203
230
  }
204
231
 
@@ -233,10 +260,10 @@ function renderMetaFieldRow(
233
260
 
234
261
  if (field.media) {
235
262
  return renderFieldRow({
236
- prop: field.key,
263
+ hasValue: Boolean(val),
237
264
  label: field.label,
238
- hasValue: !!val,
239
265
  onClear: () => applyMutation((d: JxMutableNode) => upsertMeta(d, field.attr, field.key, "")),
266
+ prop: field.key,
240
267
  widget: renderMediaPicker(field.key, val, (v: string) => {
241
268
  applyMutation((d: JxMutableNode) => upsertMeta(d, field.attr, field.key, v || ""));
242
269
  }),
@@ -254,10 +281,10 @@ function renderMetaFieldRow(
254
281
  : spTextField(`head:${field.key}`, val, commit, { placeholder });
255
282
 
256
283
  return renderFieldRow({
257
- prop: field.key,
284
+ hasValue: Boolean(val),
258
285
  label: field.label,
259
- hasValue: !!val,
260
286
  onClear: () => applyMutation((d: JxMutableNode) => upsertMeta(d, field.attr, field.key, "")),
287
+ prop: field.key,
261
288
  widget,
262
289
  });
263
290
  }
@@ -304,21 +331,20 @@ export function renderHeadTemplate({
304
331
  projectState?.isSiteProject &&
305
332
  (tab.documentPath.startsWith("pages/") || tab.documentPath.startsWith("./pages/"));
306
333
 
307
- let layoutSection: import("lit-html").TemplateResult | symbol = nothing;
334
+ let layoutSection: TemplateResult | symbol = nothing;
308
335
  if (isPage) {
309
336
  if (layoutEntries === null) {
310
337
  loadLayoutEntries();
311
338
  } else {
312
339
  const currentLayout = doc.$layout;
313
340
  const defaultLayout = projectState?.projectConfig?.defaults?.layout;
314
- const displayValue =
315
- currentLayout === false ? "__none__" : currentLayout ? currentLayout : "__default__";
341
+ const displayValue = currentLayout === false ? "__none__" : currentLayout || "__default__";
316
342
  const defaultLabel = defaultLayout
317
343
  ? defaultLayout
318
344
  .replace(/^\.\/layouts\//, "")
319
345
  .replace(/\.json$/, "")
320
- .replace(/[-_]+/g, " ")
321
- .replace(/\b\w/g, (c: string) => c.toUpperCase())
346
+ .replaceAll(/[-_]+/g, " ")
347
+ .replaceAll(/\b\w/g, (c: string) => c.toUpperCase())
322
348
  : "";
323
349
 
324
350
  layoutSection = html`
@@ -328,13 +354,13 @@ export function renderHeadTemplate({
328
354
  </div>
329
355
  <div class="head-section-body">
330
356
  ${renderFieldRow({
331
- prop: "layout",
332
- label: "Layout",
333
357
  hasValue: currentLayout !== undefined,
358
+ label: "Layout",
334
359
  onClear: () =>
335
360
  applyMutation((d: JxMutableNode) => {
336
361
  delete d.$layout;
337
362
  }),
363
+ prop: "layout",
338
364
  widget: html`
339
365
  <sp-picker
340
366
  size="s"
@@ -342,9 +368,13 @@ export function renderHeadTemplate({
342
368
  @change=${(e: Event) => {
343
369
  const val = (e.target as HTMLInputElement).value;
344
370
  applyMutation((d: JxMutableNode) => {
345
- if (val === "__default__") delete d.$layout;
346
- else if (val === "__none__") d.$layout = false;
347
- else d.$layout = val;
371
+ if (val === "__default__") {
372
+ delete d.$layout;
373
+ } else if (val === "__none__") {
374
+ d.$layout = false;
375
+ } else {
376
+ d.$layout = val;
377
+ }
348
378
  });
349
379
  invalidateLayoutCache();
350
380
  }}
@@ -378,31 +408,34 @@ export function renderHeadTemplate({
378
408
  </div>
379
409
  <div class="head-section-body">
380
410
  ${renderFieldRow({
381
- prop: "title",
411
+ hasValue: Boolean(title),
382
412
  label: "Title",
383
- hasValue: !!title,
384
413
  onClear: () =>
385
414
  applyMutation((d: JxMutableNode) => {
386
415
  delete d.title;
387
416
  }),
417
+ prop: "title",
388
418
  widget: spTextField(
389
419
  "head:title",
390
420
  title,
391
421
  (v: string) =>
392
422
  applyMutation((d: JxMutableNode) => {
393
423
  const val = v.trim();
394
- if (val) d.title = val;
395
- else delete d.title;
424
+ if (val) {
425
+ d.title = val;
426
+ } else {
427
+ delete d.title;
428
+ }
396
429
  }),
397
430
  { placeholder: "Page title…" },
398
431
  ),
399
432
  })}
400
433
  ${PAGE_FIELDS.map((field) => renderMetaFieldRow(field, head, applyMutation))}
401
434
  ${renderFieldRow({
402
- prop: "icon",
435
+ hasValue: Boolean(iconHref),
403
436
  label: "Icon",
404
- hasValue: !!iconHref,
405
437
  onClear: () => applyMutation((d: JxMutableNode) => upsertLink(d, "icon", "")),
438
+ prop: "icon",
406
439
  widget: renderMediaPicker("icon", iconHref, (v: string) => {
407
440
  applyMutation((d: JxMutableNode) => upsertLink(d, "icon", v || ""));
408
441
  }),
@@ -442,9 +475,13 @@ export function renderHeadTemplate({
442
475
  title="Remove"
443
476
  @click=${() => {
444
477
  applyMutation((d: JxMutableNode) => {
445
- if (!d.$head) return;
478
+ if (!d.$head) {
479
+ return;
480
+ }
446
481
  const idx = d.$head.indexOf(entry);
447
- if (idx >= 0) d.$head.splice(idx, 1);
482
+ if (idx !== -1) {
483
+ d.$head.splice(idx, 1);
484
+ }
448
485
  });
449
486
  renderLeftPanel();
450
487
  }}
@@ -483,21 +520,29 @@ export function renderHeadTemplate({
483
520
  const tagName = tagPicker?.value || "meta";
484
521
  const attrKey = attrField?.value?.trim();
485
522
  const attrVal = valField?.value?.trim();
486
- if (!attrKey || !attrVal) return;
487
- if (attrField) attrField.value = "";
488
- if (valField) valField.value = "";
523
+ if (!attrKey || !attrVal) {
524
+ return;
525
+ }
526
+ if (attrField) {
527
+ attrField.value = "";
528
+ }
529
+ if (valField) {
530
+ valField.value = "";
531
+ }
489
532
 
490
- const entry: JxHeadEntry = { tagName, attributes: {} };
533
+ const entry: JxHeadEntry = { attributes: {}, tagName };
491
534
  if (tagName === "meta") {
492
- entry.attributes = { name: attrKey, content: attrVal };
535
+ entry.attributes = { content: attrVal, name: attrKey };
493
536
  } else if (tagName === "link") {
494
- entry.attributes = { rel: attrKey, href: attrVal };
537
+ entry.attributes = { href: attrVal, rel: attrKey };
495
538
  } else if (tagName === "script") {
496
539
  entry.attributes = { [attrKey]: attrVal };
497
540
  }
498
541
 
499
542
  applyMutation((d: JxMutableNode) => {
500
- if (!d.$head) d.$head = [];
543
+ if (!d.$head) {
544
+ d.$head = [];
545
+ }
501
546
  d.$head.push(entry);
502
547
  });
503
548
  renderLeftPanel();
@@ -515,7 +560,9 @@ export function renderHeadTemplate({
515
560
 
516
561
  function renderFrontmatterSection() {
517
562
  const tab = activeTab.value;
518
- if (!tab) return nothing;
563
+ if (!tab) {
564
+ return nothing;
565
+ }
519
566
 
520
567
  const fm = tab.doc.content?.frontmatter || {};
521
568
  const col = findContentTypeSchema(tab.documentPath, projectState?.projectConfig);
@@ -531,29 +578,37 @@ function renderFrontmatterSection() {
531
578
  for (const [field, fieldSchema] of Object.entries(
532
579
  /** @type {Record<string, FmSchemaEntry>} */ schemaProps,
533
580
  )) {
534
- if (RESERVED_FM_KEYS.has(field)) continue;
535
- fields.push({ field, entry: fieldSchema, value: fm[field] as JsonValue });
581
+ if (RESERVED_FM_KEYS.has(field)) {
582
+ continue;
583
+ }
584
+ fields.push({ entry: fieldSchema, field, value: fm[field] as JsonValue });
536
585
  }
537
586
  for (const [field, value] of Object.entries(fm)) {
538
- if (schemaProps[field] || field.startsWith("$") || RESERVED_FM_KEYS.has(field)) continue;
587
+ if (schemaProps[field] || field.startsWith("$") || RESERVED_FM_KEYS.has(field)) {
588
+ continue;
589
+ }
539
590
  fields.push({
540
- field,
541
591
  entry: { type: typeof value === "boolean" ? "boolean" : "string" },
592
+ field,
542
593
  value: /** @type {JsonValue} */ value,
543
594
  });
544
595
  }
545
596
  } else {
546
597
  for (const [field, value] of Object.entries(fm)) {
547
- if (field.startsWith("$") || RESERVED_FM_KEYS.has(field)) continue;
598
+ if (field.startsWith("$") || RESERVED_FM_KEYS.has(field)) {
599
+ continue;
600
+ }
548
601
  fields.push({
549
- field,
550
602
  entry: { type: typeof value === "boolean" ? "boolean" : "string" },
603
+ field,
551
604
  value: /** @type {JsonValue} */ value,
552
605
  });
553
606
  }
554
607
  }
555
608
 
556
- if (fields.length === 0 && !schemaProps) return nothing;
609
+ if (fields.length === 0 && !schemaProps) {
610
+ return nothing;
611
+ }
557
612
 
558
613
  return html`
559
614
  <div class="imports-section">
@@ -576,22 +631,21 @@ function renderFmField(
576
631
  requiredFields: Set<string>,
577
632
  ) {
578
633
  const isRequired = requiredFields.has(field);
579
- const label = field.replace(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
634
+ const label = field.replaceAll(/([A-Z])/g, " $1").replace(/^./, (s) => s.toUpperCase());
580
635
  const displayLabel = label + (isRequired ? " *" : "");
581
636
  const hasVal = value !== undefined && value !== "" && value !== false;
582
- const onClear = () =>
583
- transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, undefined));
637
+ const onClear = () => transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field));
584
638
 
585
639
  if (entry.type === "boolean") {
586
640
  return renderFieldRow({
587
- prop: field,
588
- label: displayLabel,
589
641
  hasValue: hasVal,
642
+ label: displayLabel,
590
643
  onClear,
644
+ prop: field,
591
645
  widget: html`
592
646
  <sp-checkbox
593
647
  size="s"
594
- .checked=${live(!!value)}
648
+ .checked=${live(Boolean(value))}
595
649
  @change=${(e: Event) =>
596
650
  transactDoc(activeTab.value, (t) =>
597
651
  mutateUpdateFrontmatter(
@@ -608,10 +662,10 @@ function renderFmField(
608
662
  if (entry.type === "array") {
609
663
  const display = Array.isArray(value) ? value.join(", ") : (value as string) || "";
610
664
  return renderFieldRow({
611
- prop: field,
612
- label: displayLabel,
613
665
  hasValue: hasVal,
666
+ label: displayLabel,
614
667
  onClear,
668
+ prop: field,
615
669
  widget: spTextField(
616
670
  `fm:${field}`,
617
671
  display,
@@ -631,10 +685,10 @@ function renderFmField(
631
685
 
632
686
  if (Array.isArray(entry.enum)) {
633
687
  return renderFieldRow({
634
- prop: field,
635
- label: displayLabel,
636
688
  hasValue: hasVal,
689
+ label: displayLabel,
637
690
  onClear,
691
+ prop: field,
638
692
  widget: html`
639
693
  <sp-picker
640
694
  size="s"
@@ -652,10 +706,10 @@ function renderFmField(
652
706
 
653
707
  if (entry.format === "image") {
654
708
  return renderFieldRow({
655
- prop: field,
656
- label: displayLabel,
657
709
  hasValue: hasVal,
710
+ label: displayLabel,
658
711
  onClear,
712
+ prop: field,
659
713
  widget: renderMediaPicker(field, value as string, (v: string) =>
660
714
  transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, v || undefined)),
661
715
  ),
@@ -664,10 +718,10 @@ function renderFmField(
664
718
 
665
719
  if (entry.type === "number") {
666
720
  return renderFieldRow({
667
- prop: field,
668
- label: displayLabel,
669
721
  hasValue: hasVal,
722
+ label: displayLabel,
670
723
  onClear,
724
+ prop: field,
671
725
  widget: spNumberField(value !== undefined ? Number(value) : undefined, (n) =>
672
726
  transactDoc(activeTab.value, (t) => mutateUpdateFrontmatter(t, field, n)),
673
727
  ),
@@ -675,10 +729,10 @@ function renderFmField(
675
729
  }
676
730
 
677
731
  return renderFieldRow({
678
- prop: field,
679
- label: displayLabel,
680
732
  hasValue: hasVal,
733
+ label: displayLabel,
681
734
  onClear,
735
+ prop: field,
682
736
  widget: spTextField(
683
737
  `fm:${field}`,
684
738
  (value as string) || "",