smoodly 0.0.16 → 0.0.17

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 (133) hide show
  1. package/README.md +156 -21
  2. package/dist/admin/client-ops.js +1 -1
  3. package/dist/admin/custom-section.d.ts +10 -0
  4. package/dist/admin/custom-section.js +10 -0
  5. package/dist/admin/editor/CanvasOverlay.d.ts +4 -4
  6. package/dist/admin/editor/CanvasOverlay.js +5 -5
  7. package/dist/admin/editor/EditorView.js +140 -108
  8. package/dist/admin/editor/GlobalInspector.d.ts +24 -0
  9. package/dist/admin/editor/{SharedInspector.js → GlobalInspector.js} +4 -4
  10. package/dist/admin/editor/InsertPicker.d.ts +6 -5
  11. package/dist/admin/editor/InsertPicker.js +6 -4
  12. package/dist/admin/editor/InspectorFields.d.ts +21 -0
  13. package/dist/admin/editor/InspectorFields.js +22 -0
  14. package/dist/admin/editor/Outline.d.ts +0 -1
  15. package/dist/admin/editor/Outline.js +3 -3
  16. package/dist/admin/editor/{shared-edit.d.ts → global-edit.d.ts} +10 -10
  17. package/dist/admin/editor/{shared-edit.js → global-edit.js} +4 -4
  18. package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
  19. package/dist/admin/editor/{useSharedEdit.js → useGlobalEdit.js} +4 -4
  20. package/dist/admin/forms/FieldWidget.d.ts +14 -1
  21. package/dist/admin/forms/FieldWidget.js +38 -11
  22. package/dist/admin/forms/ListField.d.ts +9 -1
  23. package/dist/admin/forms/ListField.js +16 -8
  24. package/dist/admin/forms/RichtextElementCard.js +11 -5
  25. package/dist/admin/forms/RowsWidget.d.ts +13 -0
  26. package/dist/admin/forms/RowsWidget.js +26 -0
  27. package/dist/admin/forms/SpanBar.d.ts +5 -0
  28. package/dist/admin/forms/SpanBar.js +37 -0
  29. package/dist/admin/forms/richtext-insert.d.ts +2 -2
  30. package/dist/admin/forms/richtext-insert.js +3 -3
  31. package/dist/admin/forms/richtext-nodes.js +1 -1
  32. package/dist/admin/forms/tabs.d.ts +15 -9
  33. package/dist/admin/forms/tabs.js +19 -23
  34. package/dist/admin/forms/useRichtextEditor.js +8 -1
  35. package/dist/admin/global-items.d.ts +9 -0
  36. package/dist/admin/{shared-items.js → global-items.js} +17 -16
  37. package/dist/admin/index.d.ts +3 -3
  38. package/dist/admin/index.js +1 -1
  39. package/dist/admin/ops-impl.js +65 -54
  40. package/dist/admin/ops.d.ts +6 -6
  41. package/dist/admin/serialize.d.ts +18 -6
  42. package/dist/admin/serialize.js +27 -4
  43. package/dist/admin/shell/AdminApp.js +6 -6
  44. package/dist/admin/shell/GlobalFields.d.ts +24 -0
  45. package/dist/admin/shell/GlobalFields.js +19 -0
  46. package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
  47. package/dist/admin/shell/{SharedForm.js → GlobalForm.js} +36 -26
  48. package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
  49. package/dist/admin/shell/{SharedList.js → GlobalList.js} +9 -9
  50. package/dist/admin/shell/IconRail.js +13 -6
  51. package/dist/admin/shell/PagesList.js +1 -6
  52. package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
  53. package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
  54. package/dist/admin/tree-ops.d.ts +10 -8
  55. package/dist/admin/tree-ops.js +28 -16
  56. package/dist/admin/ui/HistoryModal.js +1 -1
  57. package/dist/admin/ui/primitives.d.ts +3 -1
  58. package/dist/admin/ui/primitives.js +2 -2
  59. package/dist/admin/ui/theme.js +54 -14
  60. package/dist/admin/validate.d.ts +27 -2
  61. package/dist/admin/validate.js +47 -10
  62. package/dist/collections.d.ts +3 -3
  63. package/dist/collections.js +1 -1
  64. package/dist/conditions.d.ts +33 -0
  65. package/dist/conditions.js +79 -0
  66. package/dist/config.d.ts +26 -19
  67. package/dist/config.js +26 -25
  68. package/dist/css.d.ts +7 -0
  69. package/dist/css.js +57 -0
  70. package/dist/custom/CustomSection.d.ts +26 -0
  71. package/dist/custom/CustomSection.js +20 -0
  72. package/dist/custom/css.d.ts +3 -0
  73. package/dist/custom/css.js +28 -0
  74. package/dist/custom/index.d.ts +26 -0
  75. package/dist/custom/index.js +16 -0
  76. package/dist/custom/rows.d.ts +28 -0
  77. package/dist/custom/rows.js +129 -0
  78. package/dist/custom/schema.d.ts +32 -0
  79. package/dist/custom/schema.js +39 -0
  80. package/dist/entry-store.d.ts +4 -4
  81. package/dist/entry-store.js +17 -17
  82. package/dist/fields.d.ts +5 -2
  83. package/dist/fields.js +4 -2
  84. package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
  85. package/dist/{shared-id.js → global-id.js} +4 -4
  86. package/dist/global.d.ts +57 -0
  87. package/dist/global.js +51 -0
  88. package/dist/index.d.ts +28 -18
  89. package/dist/index.js +10 -5
  90. package/dist/media.js +1 -1
  91. package/dist/next/index.d.ts +1 -1
  92. package/dist/next/page-renderer.d.ts +4 -2
  93. package/dist/next/page-renderer.js +9 -9
  94. package/dist/next/queries.d.ts +6 -6
  95. package/dist/next/queries.js +7 -7
  96. package/dist/page.js +2 -2
  97. package/dist/pairing.d.ts +13 -7
  98. package/dist/pairing.js +4 -4
  99. package/dist/refs.d.ts +2 -2
  100. package/dist/refs.js +4 -4
  101. package/dist/render.d.ts +13 -1
  102. package/dist/render.js +10 -5
  103. package/dist/resolve.d.ts +11 -6
  104. package/dist/resolve.js +22 -12
  105. package/dist/revalidate.d.ts +4 -4
  106. package/dist/revalidate.js +4 -4
  107. package/dist/richtext-render.js +2 -2
  108. package/dist/richtext-walk.d.ts +5 -5
  109. package/dist/richtext-walk.js +4 -4
  110. package/dist/schema.d.ts +15 -10
  111. package/dist/schema.js +19 -7
  112. package/dist/section-wrapper.d.ts +18 -6
  113. package/dist/section-wrapper.js +24 -17
  114. package/dist/site.d.ts +15 -5
  115. package/dist/site.js +24 -10
  116. package/dist/sql-space.js +1 -1
  117. package/dist/sql.js +2 -2
  118. package/dist/store.d.ts +7 -0
  119. package/dist/store.js +6 -0
  120. package/dist/styles.d.ts +27 -0
  121. package/dist/styles.js +60 -0
  122. package/dist/supabase-entry-store.js +14 -14
  123. package/dist/supabase-store.js +2 -1
  124. package/dist/zones.d.ts +9 -7
  125. package/dist/zones.js +5 -5
  126. package/package.json +1 -1
  127. package/dist/admin/editor/SharedInspector.d.ts +0 -23
  128. package/dist/admin/editor/useSharedEdit.d.ts +0 -6
  129. package/dist/admin/shared-items.d.ts +0 -9
  130. package/dist/admin/shell/SharedFields.d.ts +0 -21
  131. package/dist/admin/shell/SharedFields.js +0 -15
  132. package/dist/shared.d.ts +0 -53
  133. package/dist/shared.js +0 -45
@@ -28,26 +28,29 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
28
28
  // Core code: no next/* imports live here — the bridge is the only piece
29
29
  // that knows about Next, and it ships to the site, not the admin.
30
30
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
31
- import { SHARED_NODE_TYPE, STYLES_KEY, sharedNameOf } from "../../shared.js";
31
+ import { CSS_KEY, GLOBAL_NODE_TYPE, SETTINGS_KEY, STYLES_KEY, globalNameOf } from "../../global.js";
32
32
  import { descriptorAt, getAtPath } from "../field-paths.js";
33
33
  import { FieldWidget } from "../forms/FieldWidget.js";
34
34
  import { revealField } from "../forms/reveal.js";
35
35
  import { tabOf, tabsFor } from "../forms/tabs.js";
36
- import { allowedSections, canInsert, fillLockedZones, findNode, insertNode, moveNode, moveNodeTo, newNodeId, nodeFromSample, patchNode, patchNodePath, patchPage, placementNode, removeNode, zoneOf, } from "../tree-ops.js";
36
+ import { InspectorFields, valueOf } from "./InspectorFields.js";
37
+ import { allowedSections, canInsert, fillLockedZones, findNode, insertNode, moveNode, moveNodeTo, newNodeId, nodeFromSample, patchNode, patchNodeCss, patchNodePath, patchPage, placementNode, removeNode, zoneOf, } from "../tree-ops.js";
37
38
  import { useDraftAutosave } from "./autosave.js";
38
39
  import { CanvasOverlay } from "./CanvasOverlay.js";
39
40
  import { InlineRichtext } from "./InlineRichtext.js";
40
41
  import { InsertPicker } from "./InsertPicker.js";
42
+ import { customNode, customNodeLabel, customSection } from "../custom-section.js";
43
+ import { CUSTOM_NODE_TYPE } from "../../custom/index.js";
41
44
  import { NoticePane } from "./NoticePane.js";
42
45
  import { Outline } from "./Outline.js";
43
46
  import { PageInspector } from "./PageInspector.js";
44
- import { SharedInspector } from "./SharedInspector.js";
47
+ import { GlobalInspector } from "./GlobalInspector.js";
45
48
  import { localeFromUrl } from "./locale-url.js";
46
49
  import { plusButtonsFor } from "./overlay-geometry.js";
47
50
  import { useCanvasBridge } from "./useCanvasBridge.js";
48
51
  import { useInlineSession } from "./useInlineSession.js";
49
52
  import { usePageLocale } from "./usePageLocale.js";
50
- import { useSharedEdit } from "./useSharedEdit.js";
53
+ import { useGlobalEdit } from "./useGlobalEdit.js";
51
54
  import { BASE } from "../shell/base.js";
52
55
  import { isFixedNode, localeStatusOf, sourceLocaleOf } from "../shell/pages-tree.js";
53
56
  import { entryTitle } from "../shell/entries-list.js";
@@ -89,14 +92,14 @@ export function EditorView({ ops, registry, id }) {
89
92
  const [dirty, setDirty] = useState(false);
90
93
  /** Any edit on this canvas yet: the autosave indicator stays hidden until the first one (§6). */
91
94
  const [touched, setTouched] = useState(false);
92
- const [sharedBusy, setSharedBusy] = useState(null);
93
- /** A shared edit since the item's last save ack; and any shared edit on this canvas yet. */
94
- const [sharedDirty, setSharedDirty] = useState(false);
95
- const [sharedTouched, setSharedTouched] = useState(false);
95
+ const [globalBusy, setGlobalBusy] = useState(null);
96
+ /** A global edit since the item's last save ack; and any global edit on this canvas yet. */
97
+ const [globalDirty, setGlobalDirty] = useState(false);
98
+ const [globalTouched, setGlobalTouched] = useState(false);
96
99
  const [refOptions, setRefOptions] = useState({});
97
- // Shared item name → row id, loaded once per editor mount: the picker's
98
- // Shared group and a bound locked zone need the id to place a node.
99
- const [sharedIds, setSharedIds] = useState(null);
100
+ // Global item name → row id, loaded once per editor mount: the picker's
101
+ // Global group and a bound locked zone need the id to place a node.
102
+ const [globalIds, setGlobalIds] = useState(null);
100
103
  const iframeRef = useRef(null);
101
104
  const scrollerRef = useRef(null);
102
105
  const treeRef = useRef(null);
@@ -124,17 +127,17 @@ export function EditorView({ ops, registry, id }) {
124
127
  },
125
128
  onSaved: () => inlineRef.current?.onSaved(),
126
129
  });
127
- // Edits to a shared block go to the shared entry through their own
130
+ // Edits to a global block go to the global entry through their own
128
131
  // autosave (spec 2026-09-13 §4); the save ack refreshes the canvas
129
132
  // through the same inline-session deferral as the page's.
130
- const sharedEdit = useSharedEdit({ ops, onSaved: () => { setSharedDirty(false); inlineRef.current?.onSaved(); } });
131
- const patchShared = (name, path, value) => {
132
- sharedEdit.patch(name, path, value);
133
- setSharedDirty(true);
134
- setSharedTouched(true);
133
+ const globalEdit = useGlobalEdit({ ops, onSaved: () => { setGlobalDirty(false); inlineRef.current?.onSaved(); } });
134
+ const patchGlobal = (name, path, value) => {
135
+ globalEdit.patch(name, path, value);
136
+ setGlobalDirty(true);
137
+ setGlobalTouched(true);
135
138
  };
136
- /** Both targets, page first: a locale switch must leave nothing pending on either. Page Publish keeps `flush()` — it never touches a shared item. */
137
- const flushAll = useCallback(async () => (await flush()) && (await sharedEdit.flush()), [flush, sharedEdit.flush]);
139
+ /** Both targets, page first: a locale switch must leave nothing pending on either. Page Publish keeps `flush()` — it never touches a global item. */
140
+ const flushAll = useCallback(async () => (await flush()) && (await globalEdit.flush()), [flush, globalEdit.flush]);
138
141
  const { locale, adding, switchLocale, doAddLocale, doRemoveLocale } = usePageLocale({
139
142
  registry, ops, id, record, setRecord, flush: flushAll,
140
143
  fail: (text) => setBanner({ text }),
@@ -173,11 +176,11 @@ export function EditorView({ ops, registry, id }) {
173
176
  setSelectedId(null);
174
177
  setHoverId(null);
175
178
  setFolder(false);
176
- sharedEdit.reset(); // a new locale or a reload is a new canvas; the switch flushed first
179
+ globalEdit.reset(); // a new locale or a reload is a new canvas; the switch flushed first
177
180
  setDirty(false);
178
181
  setTouched(false);
179
- setSharedDirty(false);
180
- setSharedTouched(false);
182
+ setGlobalDirty(false);
183
+ setGlobalTouched(false);
181
184
  // ops is a server action — it can reject outright (offline, 500,
182
185
  // aborted), not just resolve to a typed error. Either failure mode
183
186
  // must land on the same full-pane error, never a hung load.
@@ -269,27 +272,28 @@ export function EditorView({ ops, registry, id }) {
269
272
  const template = useMemo(() => (record ? registry.pages.find((p) => p.name === record.template) ?? null : null), [record, registry]);
270
273
  const zoneOrder = useMemo(() => Object.keys(template?.zones ?? {}), [template]);
271
274
  const policyOf = useCallback((zone) => (zone ? template?.zones[zone] : undefined), [template]);
272
- const sectionOf = useCallback((type) => registry.sections.find((s) => s.name === type) ?? null, [registry]);
273
- /** The shared item a placement node places, or null for any other node. */
274
- const sharedOf = useCallback((node) => {
275
- if (node.type !== SHARED_NODE_TYPE)
275
+ // The custom section (spec 2026-09-14 §6) is the registry's one synthetic section.
276
+ const sectionOf = useCallback((type) => (type === CUSTOM_NODE_TYPE ? customSection(registry) : registry.sections.find((s) => s.name === type) ?? null), [registry]);
277
+ /** The global item a placement node places, or null for any other node. */
278
+ const globalOf = useCallback((node) => {
279
+ if (node.type !== GLOBAL_NODE_TYPE)
276
280
  return null;
277
- return registry.shared.find((s) => s.name === node.fields?.item) ?? null;
281
+ return registry.globals.find((s) => s.name === node.fields?.item) ?? null;
278
282
  }, [registry]);
279
- /** The shared item behind a node id: a placement's, by the tree, or a layout-rendered
280
- * item's, by its `shared:` id (spec 2026-09-13 §1). Reads the refs — safe at event time. */
281
- const sharedItemOf = useCallback((id) => {
283
+ /** The global item behind a node id: a placement's, by the tree, or a layout-rendered
284
+ * item's, by its `global:` id (spec 2026-09-13 §1). Reads the refs — safe at event time. */
285
+ const globalItemOf = useCallback((id) => {
282
286
  const node = treeRef.current ? findNode(treeRef.current, id) : null;
283
287
  if (node)
284
- return sharedOf(node);
285
- const name = sharedNameOf(id);
286
- return name ? registryRef.current.shared.find((s) => s.name === name) ?? null : null;
287
- }, [sharedOf]);
288
+ return globalOf(node);
289
+ const name = globalNameOf(id);
290
+ return name ? registryRef.current.globals.find((s) => s.name === name) ?? null : null;
291
+ }, [globalOf]);
288
292
  // ── ref fields inside sections: same option loading as EntryForm ──────
289
293
  const refTargets = useMemo(() => {
290
294
  const out = {};
291
295
  for (const section of registry.sections) {
292
- for (const ns of ["fields", "styles", "meta"]) {
296
+ for (const ns of ["fields", "settings"]) {
293
297
  for (const [key, d] of Object.entries(section[ns] ?? {})) {
294
298
  if ((d.type === "ref" || d.type === "refList") && typeof d.target === "string") {
295
299
  out[`${section.name}.${ns}.${key}`] = d.target;
@@ -325,36 +329,36 @@ export function EditorView({ ops, registry, id }) {
325
329
  return () => { cancelled = true; };
326
330
  }, [ops, refTargets, registry, locale]);
327
331
  useEffect(() => {
328
- if (registryRef.current.shared.length === 0) {
329
- setSharedIds({});
332
+ if (registryRef.current.globals.length === 0) {
333
+ setGlobalIds({});
330
334
  return;
331
335
  }
332
336
  let cancelled = false;
333
- // A failed load leaves the map empty: the picker's Shared group and
337
+ // A failed load leaves the map empty: the picker's Global group and
334
338
  // any zone bound to an item degrade to nothing to place, not a stuck
335
339
  // editor — so say why in the console, like the ref options above.
336
- ops.shared.list()
340
+ ops.globals.list()
337
341
  .then((r) => {
338
342
  if (cancelled)
339
343
  return;
340
344
  if (!r.ok)
341
345
  console.error(r.message);
342
- setSharedIds(r.ok ? Object.fromEntries(r.data.map((i) => [i.name, i.record.id])) : {});
346
+ setGlobalIds(r.ok ? Object.fromEntries(r.data.map((i) => [i.name, i.record.id])) : {});
343
347
  })
344
348
  .catch((e) => { console.error(e); if (!cancelled)
345
- setSharedIds({}); });
349
+ setGlobalIds({}); });
346
350
  return () => { cancelled = true; };
347
351
  }, [ops]);
348
- // A selected shared block loads its item for the canvas locale, once (spec 2026-09-13 §3).
349
- const selectedItemName = selectedId ? sharedItemOf(selectedId)?.name ?? null : null;
352
+ // A selected global block loads its item for the canvas locale, once (spec 2026-09-13 §3).
353
+ const selectedItemName = selectedId ? globalItemOf(selectedId)?.name ?? null : null;
350
354
  useEffect(() => {
351
355
  if (selectedItemName)
352
- void sharedEdit.load(selectedItemName, locale);
356
+ void globalEdit.load(selectedItemName, locale);
353
357
  }, [selectedItemName, locale]); // eslint-disable-line react-hooks/exhaustive-deps
354
358
  useEffect(() => {
355
- if (sharedEdit.loadError)
356
- setBanner({ text: sharedEdit.loadError });
357
- }, [sharedEdit.loadError]);
359
+ if (globalEdit.loadError)
360
+ setBanner({ text: globalEdit.loadError });
361
+ }, [globalEdit.loadError]);
358
362
  // Selecting from the outline must reveal the block. The canvas column is
359
363
  // the scroller now (the iframe has no scrollbar of its own — it is sized to
360
364
  // the full document), so scroll it to the rect the bridge reported.
@@ -381,9 +385,9 @@ export function EditorView({ ops, registry, id }) {
381
385
  const inline = useInlineSession({
382
386
  iframeRef, postToCanvas,
383
387
  patch: (node, path, value) => {
384
- const item = sharedItemOf(node);
388
+ const item = globalItemOf(node);
385
389
  if (item)
386
- patchShared(item.name, path, value);
390
+ patchGlobal(item.name, path, value);
387
391
  else
388
392
  mutate((t) => patchNodePath(t, node, path, value));
389
393
  // A second element marked with the same field follows the one being typed in.
@@ -428,8 +432,8 @@ export function EditorView({ ops, registry, id }) {
428
432
  };
429
433
  const onFieldClick = (id, path, x, y) => {
430
434
  const node = tree ? findNode(tree, id) : null;
431
- // A shared block's fields are the item's — the section's own (spec 2026-09-13 §5); any other block's are its section's.
432
- const item = sharedItemOf(id);
435
+ // A global block's fields are the item's — the section's own (spec 2026-09-13 §5); any other block's are its section's.
436
+ const item = globalItemOf(id);
433
437
  const schema = item ?? (node ? sectionOf(node.type) : null);
434
438
  if (!schema)
435
439
  return;
@@ -442,25 +446,28 @@ export function EditorView({ ops, registry, id }) {
442
446
  }
443
447
  if (inline.session && inline.session.node === id && inline.session.path === path)
444
448
  return;
445
- const kind = descriptor.type === "text" ? "text" : descriptor.type === "richtext" ? "richtext" : null;
449
+ // A custom section's columns edit in the inspector for now (register 2026-09-15): the click reveals
450
+ // the column's editor rather than opening an inline session on the canvas.
451
+ const kind = node?.type === CUSTOM_NODE_TYPE ? null
452
+ : descriptor.type === "text" ? "text" : descriptor.type === "richtext" ? "richtext" : null;
446
453
  if (kind && inline.start({ node: id, path, kind }, { x, y }))
447
454
  return;
448
455
  // Any other field, or a text mark on a container: the inspector reveals its widget.
449
456
  reveal(path, schema);
450
457
  };
451
- // A zone bound to a shared item needs the item's row id (ops.shared.list),
458
+ // A zone bound to a global item needs the item's row id (ops.globals.list),
452
459
  // which can land after the draft did. Fill then; nothing else changes.
453
460
  useEffect(() => {
454
- if (!tree || !sharedIds || !record)
461
+ if (!tree || !globalIds || !record)
455
462
  return;
456
463
  const template = registryRef.current.pages.find((p) => p.name === record.template);
457
- const filled = fillLockedZones(tree, template?.zones ?? {}, registryRef.current.sections, sharedIds);
464
+ const filled = fillLockedZones(tree, template?.zones ?? {}, registryRef.current.sections, globalIds);
458
465
  if (filled !== tree)
459
466
  mutate(() => filled);
460
467
  // `tree` is deliberately not a dependency: this runs when the ids land
461
468
  // or a new draft loads, not on every keystroke.
462
469
  // eslint-disable-next-line react-hooks/exhaustive-deps
463
- }, [sharedIds, record, mutate, tree === null]);
470
+ }, [globalIds, record, mutate, tree === null]);
464
471
  // ── tree gestures ────────────────────────────────────────────────────
465
472
  const nodesIn = useCallback((zone) => tree?.zones[zone] ?? [], [tree]);
466
473
  const zoneCanTake = useCallback((zone) => canInsert(policyOf(zone), nodesIn(zone).length), [policyOf, nodesIn]);
@@ -470,11 +477,17 @@ export function EditorView({ ops, registry, id }) {
470
477
  setPicker(null);
471
478
  select(node.id);
472
479
  };
473
- const doInsertShared = (zone, index, item) => {
474
- const ref = sharedIds?.[item.name];
480
+ const doInsertCustom = (zone, index) => {
481
+ const node = customNode(registry, newNodeId(CUSTOM_NODE_TYPE));
482
+ mutate((t) => insertNode(t, zone, index, node));
483
+ setPicker(null);
484
+ select(node.id);
485
+ };
486
+ const doInsertGlobal = (zone, index, item) => {
487
+ const ref = globalIds?.[item.name];
475
488
  if (!ref)
476
489
  return;
477
- const node = placementNode(item, ref, newNodeId(SHARED_NODE_TYPE));
490
+ const node = placementNode(item, ref, newNodeId(GLOBAL_NODE_TYPE));
478
491
  mutate((t) => insertNode(t, zone, index, node));
479
492
  setPicker(null);
480
493
  select(node.id);
@@ -486,13 +499,16 @@ export function EditorView({ ops, registry, id }) {
486
499
  if (!zone || !zoneCanTake(zone))
487
500
  return;
488
501
  const index = nodesIn(zone).findIndex((n) => n.id === node.id) + 1;
502
+ // Deep copies: a shallow one would share every list (a custom section's rows) between the original and the copy.
489
503
  const copy = {
490
504
  ...node,
491
505
  id: newNodeId(node.type),
492
- fields: { ...node.fields },
493
- styles: node.styles ? { ...node.styles } : undefined,
494
- meta: node.meta ? { ...node.meta } : undefined,
506
+ fields: structuredClone(node.fields),
507
+ settings: node.settings ? structuredClone(node.settings) : undefined,
508
+ styles: node.styles ? structuredClone(node.styles) : undefined,
495
509
  };
510
+ if (node.css === undefined)
511
+ delete copy.css;
496
512
  mutate((t) => insertNode(t, zone, index, copy));
497
513
  select(copy.id);
498
514
  };
@@ -509,6 +525,7 @@ export function EditorView({ ops, registry, id }) {
509
525
  const doMove = (id, dir) => mutate((t) => moveNode(t, id, dir));
510
526
  const doMoveTo = (id, index) => mutate((t) => moveNodeTo(t, id, index));
511
527
  const doPatch = (id, ns, key, value) => mutate((t) => patchNode(t, id, ns, key, value));
528
+ const doPatchCss = (id, css) => mutate((t) => patchNodeCss(t, id, css));
512
529
  /** A page-level value (page fields, SEO meta) — same dirty/debounce path as a node's (spec 2026-09-10 page fields §5). */
513
530
  const doPatchPage = (ns, key, value) => mutate((t) => patchPage(t, ns, key, value));
514
531
  // ── publish ──────────────────────────────────────────────────────────
@@ -628,49 +645,49 @@ export function EditorView({ ops, registry, id }) {
628
645
  setReloadKey((k) => k + 1);
629
646
  return true;
630
647
  };
631
- /** The shared card's own actions (spec 2026-09-13 §5, publish flow §2): the item in the canvas locale, never the page. */
632
- const doPublishShared = async (name) => {
648
+ /** The global card's own actions (spec 2026-09-13 §5, publish flow §2): the item in the canvas locale, never the page. */
649
+ const doPublishGlobal = async (name) => {
633
650
  setBanner(null);
634
- setSharedBusy("publish");
635
- const r = await sharedEdit.setStatus(name, "published");
636
- setSharedBusy(null);
651
+ setGlobalBusy("publish");
652
+ const r = await globalEdit.setStatus(name, "published");
653
+ setGlobalBusy(null);
637
654
  if (!r.ok)
638
655
  setBanner({ text: r.message, fields: r.fields });
639
656
  };
640
- const doUnpublishShared = async (name) => {
657
+ const doUnpublishGlobal = async (name) => {
641
658
  setBanner(null);
642
- setSharedBusy("unpublish");
643
- const r = await sharedEdit.setStatus(name, "draft");
644
- setSharedBusy(null);
659
+ setGlobalBusy("unpublish");
660
+ const r = await globalEdit.setStatus(name, "draft");
661
+ setGlobalBusy(null);
645
662
  if (!r.ok)
646
663
  setBanner({ text: r.message, fields: r.fields });
647
664
  };
648
- const doDiscardShared = async (name) => {
665
+ const doDiscardGlobal = async (name) => {
649
666
  if (!window.confirm("Discard your changes and restore the published version?"))
650
667
  return;
651
668
  setBanner(null);
652
- setSharedBusy("discard");
653
- if (!(await sharedEdit.flush())) {
654
- setSharedBusy(null);
669
+ setGlobalBusy("discard");
670
+ if (!(await globalEdit.flush())) {
671
+ setGlobalBusy(null);
655
672
  setBanner({ text: "Couldn't save your latest changes — discard cancelled." });
656
673
  return;
657
674
  }
658
675
  let r;
659
676
  try {
660
- r = await ops.shared.discardDraft(name, locale);
677
+ r = await ops.globals.discardDraft(name, locale);
661
678
  }
662
679
  catch {
663
680
  r = { ok: false, code: "internal", message: "Couldn't reach the server." };
664
681
  }
665
- setSharedBusy(null);
682
+ setGlobalBusy(null);
666
683
  if (!r.ok) {
667
684
  setBanner({ text: r.message });
668
685
  return;
669
686
  }
670
687
  // The item reloads from the store; the canvas refreshes through the same path as a save ack.
671
- sharedEdit.reset();
672
- setSharedDirty(false);
673
- void sharedEdit.load(name, locale);
688
+ globalEdit.reset();
689
+ setGlobalDirty(false);
690
+ void globalEdit.load(name, locale);
674
691
  inlineRef.current?.onSaved();
675
692
  };
676
693
  // ── keyboard ─────────────────────────────────────────────────────────
@@ -696,35 +713,37 @@ export function EditorView({ ops, registry, id }) {
696
713
  }
697
714
  const selected = tree && selectedId ? findNode(tree, selectedId) : null;
698
715
  const selectedSection = selected ? sectionOf(selected.type) : null;
699
- const selectedShared = selected ? sharedOf(selected) : null;
700
- // A layout-rendered item: selected by its `shared:` id, in no zone (spec 2026-09-13 §1).
701
- const layoutItem = selectedId && !selected ? sharedItemOf(selectedId) : null;
702
- const selectedItem = selectedShared ?? layoutItem;
716
+ const selectedGlobal = selected ? globalOf(selected) : null;
717
+ // A layout-rendered item: selected by its `global:` id, in no zone (spec 2026-09-13 §1).
718
+ const layoutItem = selectedId && !selected ? globalItemOf(selectedId) : null;
719
+ const selectedItem = selectedGlobal ?? layoutItem;
703
720
  const selZone = tree && selected ? zoneOf(tree, selected.id) : null;
704
721
  const selLocked = policyOf(selZone)?.kind === "locked";
705
722
  const rectById = new Map(rects.map((r) => [r.id, r]));
706
723
  const selRect = selectedId ? rectById.get(selectedId) : undefined;
707
724
  const hoverRect = hoverId && hoverId !== selectedId ? rectById.get(hoverId) : undefined;
708
- const hoverShared = hoverId ? sharedItemOf(hoverId) !== null : false;
725
+ const hoverGlobal = hoverId ? globalItemOf(hoverId) !== null : false;
709
726
  const fieldRectOf = (f) => f ? fieldRects.find((r) => r.node === f.node && r.path === f.path) : undefined;
710
727
  const fieldHover = hoverField && hoverField.node === selectedId ? fieldRectOf(hoverField) : undefined;
711
728
  const editing = fieldRectOf(inline.session);
712
729
  const pageWidth = rects.reduce((w, r) => Math.max(w, r.x + r.w), 320);
713
- const selection = selected && selRect && (selectedSection || selectedShared)
730
+ const selection = selected && selRect && (selectedSection || selectedGlobal)
714
731
  ? {
715
732
  node: selected, rect: selRect,
716
- title: selectedSection?.title ?? selectedShared?.title ?? selected.type,
717
- shared: Boolean(selectedShared), locked: selLocked,
733
+ title: selectedSection?.title ?? selectedGlobal?.title ?? selected.type,
734
+ global: Boolean(selectedGlobal), locked: selLocked,
718
735
  }
719
736
  : layoutItem && selectedId && selRect
720
737
  ? {
721
- node: { id: selectedId, type: SHARED_NODE_TYPE, fields: { item: layoutItem.name } },
722
- rect: selRect, title: layoutItem.title, shared: true, locked: false, layout: true,
738
+ node: { id: selectedId, type: GLOBAL_NODE_TYPE, fields: { item: layoutItem.name } },
739
+ rect: selRect, title: layoutItem.title, global: true, locked: false, layout: true,
723
740
  }
724
741
  : null;
725
742
  const labelOf = (node) => {
726
- const shared = sharedOf(node);
727
- return sectionOf(node.type)?.title ?? (shared ? `${shared.title} · shared` : node.type);
743
+ if (node.type === CUSTOM_NODE_TYPE)
744
+ return customNodeLabel(node);
745
+ const global = globalOf(node);
746
+ return sectionOf(node.type)?.title ?? (global ? `${global.title} · global` : node.type);
728
747
  };
729
748
  // Reveal is defined here, where the rect map exists; select() calls it
730
749
  // through the ref so its own identity stays stable.
@@ -739,56 +758,69 @@ export function EditorView({ ops, registry, id }) {
739
758
  const plusButtons = plusButtonsFor({ zoneOrder, canTake: zoneCanTake, nodesIn, rects: rectById, emptyZones });
740
759
  const tabs = selectedSection ? tabsFor(selectedSection) : selectedItem ? tabsFor(selectedItem) : [];
741
760
  const activeTab = tabs.some((t) => t.tab === tab) ? tab : tabs[0]?.tab;
742
- const entries = tabs.find((t) => t.tab === activeTab)?.entries ?? [];
761
+ const activeGroups = tabs.find((t) => t.tab === activeTab)?.groups ?? [];
743
762
  return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx(_Fragment, { children: _jsx(Breadcrumb, { backHref: `${BASE}/pages`, items: [
744
763
  {
745
764
  label: "Pages", href: `${BASE}/pages`,
746
765
  trailing: registry.locales.supported.length > 1 && (_jsx(LocaleSwitcher, { registry: registry, present: record ? registry.locales.supported.filter((l) => record.locales[l]) : [locale], parentLocales: parentLocales, value: locale, adding: adding, onSwitch: (l) => { void switchLocale(l); }, onAdd: (l) => { void doAddLocale(l); } })),
747
766
  },
748
767
  { label },
749
- ] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error") && _jsx(SaveIndicator, { state: saveState }), ready && (_jsx(LinkButton, { href: siteUrl, target: "_blank", rel: "noreferrer", title: "Opens the site route. While the admin's draft-mode cookie is on, this shows the DRAFT \u2014 open it in a private window to see what visitors see.", children: "View live" })), _jsx(PublishControl, { status: record?.locales[locale] ? localeStatusOf(record.locales[locale]) : null, pending: dirty || saveState !== "saved", busy: pageBusy, flash: published, onPublish: () => { void doPublish(); }, onDiscard: () => { void doDiscard(); }, onUnpublish: () => { void doUnpublish(); }, onHistory: () => { void doHistory(); } })] }) }), history && record?.locales[locale] && (_jsx(HistoryModal, { rows: history, draftId: record.locales[locale].draftVersionId, onClose: () => setHistory(null), onRestore: doRestore, render: (versionId) => _jsx("iframe", { className: "sm-history__frame", title: "Version preview", src: withVersionParam(siteUrl, versionId) }) })), _jsxs("div", { className: "sm-editor", children: [_jsxs("div", { className: "sm-canvas", children: [(banner || geometry === "missing") && (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, padding: "0 20px 10px" }, children: [banner && (_jsxs(BannerBox, { tone: banner.fields && banner.fields.length > 0 ? "warn" : "error", onDismiss: () => setBanner(null), children: [_jsx("p", { children: banner.text }), _jsx(FieldErrorList, { fields: banner.fields ?? [] })] })), geometry === "missing" && (_jsx(BannerBox, { tone: "warn", children: "Selection unavailable \u2014 the page didn't report geometry. The outline still works." }))] })), _jsx("div", { className: "sm-canvas__scroll", ref: scrollerRef, children: _jsxs("div", { className: "sm-page", style: { overflow: "hidden" }, children: [iframeSrc ? (_jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Page canvas", style: { width: "100%", height: docH, border: 0, display: "block" } })) : (_jsx("div", { className: "sm-canvas__loading", children: "Loading canvas\u2026" })), _jsx(CanvasOverlay, { emptyZones: emptyZones, hoverRect: hoverRect, hoverShared: hoverShared, selection: selection, fieldHover: fieldHover, editing: editing, plusButtons: plusButtons, onPlus: (p, x, y) => setPicker({ zone: p.zone, index: p.index, x: x - 150, y: y + 12 }), onMove: doMove, onDuplicate: doDuplicate, onRemove: doRemove }), inline.richtext && (() => {
768
+ ] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error") && _jsx(SaveIndicator, { state: saveState }), ready && (_jsx(LinkButton, { href: siteUrl, target: "_blank", rel: "noreferrer", title: "Opens the site route. While the admin's draft-mode cookie is on, this shows the DRAFT \u2014 open it in a private window to see what visitors see.", children: "View live" })), _jsx(PublishControl, { status: record?.locales[locale] ? localeStatusOf(record.locales[locale]) : null, pending: dirty || saveState !== "saved", busy: pageBusy, flash: published, onPublish: () => { void doPublish(); }, onDiscard: () => { void doDiscard(); }, onUnpublish: () => { void doUnpublish(); }, onHistory: () => { void doHistory(); } })] }) }), history && record?.locales[locale] && (_jsx(HistoryModal, { rows: history, draftId: record.locales[locale].draftVersionId, onClose: () => setHistory(null), onRestore: doRestore, render: (versionId) => _jsx("iframe", { className: "sm-history__frame", title: "Version preview", src: withVersionParam(siteUrl, versionId) }) })), _jsxs("div", { className: "sm-editor", children: [_jsxs("div", { className: "sm-canvas", children: [(banner || geometry === "missing") && (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, padding: "0 20px 10px" }, children: [banner && (_jsxs(BannerBox, { tone: banner.fields && banner.fields.length > 0 ? "warn" : "error", onDismiss: () => setBanner(null), children: [_jsx("p", { children: banner.text }), _jsx(FieldErrorList, { fields: banner.fields ?? [] })] })), geometry === "missing" && (_jsx(BannerBox, { tone: "warn", children: "Selection unavailable \u2014 the page didn't report geometry. The outline still works." }))] })), _jsx("div", { className: "sm-canvas__scroll", ref: scrollerRef, children: _jsxs("div", { className: "sm-page", style: { overflow: "hidden" }, children: [iframeSrc ? (_jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Page canvas", style: { width: "100%", height: docH, border: 0, display: "block" } })) : (_jsx("div", { className: "sm-canvas__loading", children: "Loading canvas\u2026" })), _jsx(CanvasOverlay, { emptyZones: emptyZones, hoverRect: hoverRect, hoverGlobal: hoverGlobal, selection: selection, fieldHover: fieldHover, editing: editing, plusButtons: plusButtons, onPlus: (p, x, y) => setPicker({ zone: p.zone, index: p.index, x: x - 150, y: y + 12 }), onMove: doMove, onDuplicate: doDuplicate, onRemove: doRemove }), inline.richtext && (() => {
750
769
  const { session } = inline.richtext;
751
- const item = sharedItemOf(session.node);
770
+ const item = globalItemOf(session.node);
752
771
  const node = !item && tree ? findNode(tree, session.node) : null;
753
772
  const schema = item ?? (node ? sectionOf(node.type) : null);
754
- const fields = item ? sharedEdit.items[item.name]?.fields : node?.fields;
773
+ const fields = item ? globalEdit.items[item.name]?.fields : node?.fields;
755
774
  if (!schema || !fields)
756
775
  return null;
757
776
  return (_jsx(InlineRichtext, { mount: inline.richtext, pageWidth: pageWidth, value: getAtPath(fields, session.path), toolset: descriptorAt(schema, session.path)?.toolset, onChange: (doc) => {
758
777
  inline.markChanged();
759
778
  if (item)
760
- patchShared(item.name, session.path, doc);
779
+ patchGlobal(item.name, session.path, doc);
761
780
  else
762
781
  mutate((t) => patchNodePath(t, session.node, session.path, doc));
763
782
  }, onEscape: inline.end }));
764
- })()] }) })] }), _jsx(ResizeHandle, {}), _jsxs("aside", { className: "sm-inspector", ref: inspectorRef, children: [_jsxs("div", { className: "sm-card", children: [selectedSection || selectedItem ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sm-inspector__head", children: [_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 }, children: [_jsx("span", { className: "sm-blockglyph" }), _jsx("span", { className: "sm-title", style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: selectedSection?.title ?? selectedItem?.title }), selectedItem && (sharedTouched || sharedEdit.saveState === "error") && _jsx(SaveIndicator, { state: sharedEdit.saveState })] }), _jsx(IconButton, { label: "Clear selection", onClick: () => setSelectedId(null), children: "\u00D7" })] }), tabs.length > 0 && (_jsx(Tabs, { tabs: tabs.map((t) => t.tab), value: activeTab, onChange: (t) => setTab(t) }))] })) : record ? (_jsx(PageInspector, { record: record, locale: locale, registry: registry, tree: tree, path: paths[locale], fixed: isFixedNode(record, registry), onRename: onRename, onPatch: doPatchPage, onRemoveLocale: Object.keys(record.locales).length > 1 &&
783
+ })()] }) })] }), _jsx(ResizeHandle, {}), _jsxs("aside", { className: "sm-inspector", ref: inspectorRef, children: [_jsxs("div", { className: "sm-card", children: [selectedSection || selectedItem ? (_jsxs(_Fragment, { children: [_jsxs("div", { className: "sm-inspector__head", children: [_jsxs("span", { style: { display: "flex", alignItems: "center", gap: 8, minWidth: 0 }, children: [_jsx("span", { className: "sm-blockglyph" }), _jsx("span", { className: "sm-title", style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }, children: selectedSection?.title ?? (selectedItem ? `${selectedItem.title} · global` : null) }), selectedItem && (globalTouched || globalEdit.saveState === "error") && _jsx(SaveIndicator, { state: globalEdit.saveState })] }), _jsx(IconButton, { label: "Clear selection", onClick: () => setSelectedId(null), children: "\u00D7" })] }), tabs.length > 0 && (_jsx(Tabs, { tabs: tabs.map((t) => t.tab), value: activeTab, onChange: (t) => setTab(t) }))] })) : record ? (_jsx(PageInspector, { record: record, locale: locale, registry: registry, tree: tree, path: paths[locale], fixed: isFixedNode(record, registry), onRename: onRename, onPatch: doPatchPage, onRemoveLocale: Object.keys(record.locales).length > 1 &&
765
784
  !isFixedNode(record, registry) &&
766
785
  !(record.parentId === null && record.locales[registry.locales.default]?.slug === registry.homePageSlug)
767
786
  ? () => { void doRemoveLocale(); }
768
- : undefined })) : null, (selectedSection || selectedItem) && (_jsxs("div", { className: "sm-inspector__body", children: [selected && selectedSection && entries.map(({ key, ns, descriptor }) => (_jsx(FieldWidget, { fieldKey: key, descriptor: descriptor, value: selected[ns]?.[key], refOptions: refOptionsFor(refOptions, refTargets, selectedSection, ns, key, descriptor), inlinePath: ns === "fields" && inline.session?.node === selected.id ? inline.session.path : undefined, onEditHere: () => { const p = inline.session?.path; inline.end(); if (p)
769
- reveal(p, selectedSection); }, onChange: (value) => {
770
- doPatch(selected.id, ns, key, value);
771
- if (ns === "fields")
772
- syncTexts(selected.id, selectedSection, { ...selected.fields, [key]: value });
773
- } }, `${selected.id}.${ns}.${key}`))), selectedItem && selectedId && (_jsx(SharedInspector, { item: selectedItem, edit: sharedEdit.items[selectedItem.name] ?? null, locale: locale, entries: entries, multilingual: registry.locales.supported.length > 1, refOptions: refOptions, pending: sharedDirty || sharedEdit.saveState !== "saved", busy: sharedBusy, inlinePath: inline.session?.node === selectedId ? inline.session.path : undefined, onEditHere: () => { const p = inline.session?.path; inline.end(); if (p)
787
+ : undefined })) : null, (selectedSection || selectedItem) && (_jsxs("div", { className: "sm-inspector__body", children: [selected && selectedSection && (_jsx(InspectorFields, { groups: activeGroups, values: selected, render: ({ key, ns, descriptor }, { emptyLabel, scope }) => (_jsx(FieldWidget, { fieldKey: key, descriptor: descriptor, value: valueOf(selected, { key, ns, descriptor }), emptyLabel: emptyLabel, scope: scope, refOptions: ns === "fields" || ns === "settings" ? refOptionsFor(refOptions, refTargets, selectedSection, ns, key, descriptor) : undefined, inlinePath: ns === "fields" && inline.session?.node === selected.id ? inline.session.path : undefined, onEditHere: () => { const p = inline.session?.path; inline.end(); if (p)
788
+ reveal(p, selectedSection); }, onChange: (value) => {
789
+ if (ns === "css")
790
+ doPatchCss(selected.id, value);
791
+ else
792
+ doPatch(selected.id, ns, key, value);
793
+ if (ns === "fields")
794
+ syncTexts(selected.id, selectedSection, { ...selected.fields, [key]: value });
795
+ } }, `${selected.id}.${ns}.${key}`)) })), selectedItem && selectedId && (_jsx(GlobalInspector, { item: selectedItem, edit: globalEdit.items[selectedItem.name] ?? null, locale: locale, groups: activeGroups, multilingual: registry.locales.supported.length > 1, refOptions: refOptions, pending: globalDirty || globalEdit.saveState !== "saved", busy: globalBusy, inlinePath: inline.session?.node === selectedId ? inline.session.path : undefined, onEditHere: () => { const p = inline.session?.path; inline.end(); if (p)
774
796
  reveal(p, selectedItem); }, onChange: (ns, key, value) => {
775
- patchShared(selectedItem.name, ns === "fields" ? key : `${STYLES_KEY}.${key}`, value);
797
+ patchGlobal(selectedItem.name, globalPath(ns, key), value);
776
798
  if (ns === "fields") {
777
- const current = sharedEdit.items[selectedItem.name];
799
+ const current = globalEdit.items[selectedItem.name];
778
800
  if (current)
779
801
  syncTexts(selectedId, selectedItem, { ...current.fields, [key]: value });
780
802
  }
781
- }, onPublish: () => { void doPublishShared(selectedItem.name); }, onDiscard: () => { void doDiscardShared(selectedItem.name); }, onUnpublish: () => { void doUnpublishShared(selectedItem.name); } }))] }))] }), _jsx("div", { className: "sm-card", children: _jsx(Outline, { selectedId: selectedId, onSelect: select, onMove: doMove, onMoveTo: doMoveTo, onRemove: doRemove, zones: zoneOrder.map((zone) => ({
803
+ }, onPublish: () => { void doPublishGlobal(selectedItem.name); }, onDiscard: () => { void doDiscardGlobal(selectedItem.name); }, onUnpublish: () => { void doUnpublishGlobal(selectedItem.name); } }))] }))] }), _jsx("div", { className: "sm-card", children: _jsx(Outline, { selectedId: selectedId, onSelect: select, onMoveTo: doMoveTo, onRemove: doRemove, zones: zoneOrder.map((zone) => ({
782
804
  zone, locked: policyOf(zone)?.kind === "locked", canTake: zoneCanTake(zone),
783
805
  nodes: nodesIn(zone).map((node) => ({ node, label: labelOf(node) })),
784
806
  })), onAddFirst: (zone) => {
785
807
  const first = allowedSections(policyOf(zone), registry.sections)[0];
786
808
  if (first)
787
809
  doInsert(zone, 0, first);
788
- } }) })] })] }), picker && (_jsx(InsertPicker, { at: picker, policy: policyOf(picker.zone), registry: registry, sharedIds: sharedIds ?? {}, onInsert: (section) => doInsert(picker.zone, picker.index, section), onInsertShared: (item) => doInsertShared(picker.zone, picker.index, item), onClose: () => setPicker(null) }))] }));
810
+ } }) })] })] }), picker && (_jsx(InsertPicker, { at: picker, policy: policyOf(picker.zone), registry: registry, globalIds: globalIds ?? {}, onInsert: (section) => doInsert(picker.zone, picker.index, section), onInsertGlobal: (item) => doInsertGlobal(picker.zone, picker.index, item), onInsertCustom: () => doInsertCustom(picker.zone, picker.index), onClose: () => setPicker(null) }))] }));
789
811
  }
790
812
  /** The selected node's ref field (if any) resolves to its target
791
813
  * collection's loaded options — the EntryForm shape, section-scoped. */
814
+ /** Where a global item's widget writes (spec 2026-09-13 §3, 2026-09-14 §5): fields at the key, the other namespaces under their reserved keys. */
815
+ function globalPath(ns, key) {
816
+ if (ns === "fields")
817
+ return key;
818
+ if (ns === "settings")
819
+ return `${SETTINGS_KEY}.${key}`;
820
+ if (ns === "styles")
821
+ return `${STYLES_KEY}.${key}`;
822
+ return CSS_KEY;
823
+ }
792
824
  function refOptionsFor(loaded, targets, section, ns, key, descriptor) {
793
825
  if (descriptor.type !== "ref" && descriptor.type !== "refList")
794
826
  return undefined;
@@ -0,0 +1,24 @@
1
+ import type { AdminGlobal } from "../serialize.ts";
2
+ import type { Group } from "../forms/tabs.ts";
3
+ import type { GlobalEditItem } from "./global-edit.ts";
4
+ import { type RefOption } from "../shell/GlobalFields.tsx";
5
+ import { type PublishBusy } from "../ui/PublishControl.tsx";
6
+ export declare function GlobalInspector({ item, edit, locale, groups, multilingual, refOptions, inlinePath, onEditHere, onChange, pending, busy, onPublish, onDiscard, onUnpublish, }: {
7
+ item: AdminGlobal;
8
+ /** null while the item loads. */
9
+ edit: GlobalEditItem | null;
10
+ locale: string;
11
+ /** The active tab's groups (spec 2026-09-14 §5). */
12
+ groups: Group[];
13
+ multilingual: boolean;
14
+ refOptions: Record<string, RefOption[]>;
15
+ inlinePath?: string;
16
+ onEditHere?(): void;
17
+ onChange(ns: "fields" | "settings" | "styles" | "css", key: string, value: unknown): void;
18
+ /** An edit the store has not confirmed (dirty, or the item's autosave not in `saved`). */
19
+ pending: boolean;
20
+ busy: PublishBusy;
21
+ onPublish(): void;
22
+ onDiscard(): void;
23
+ onUnpublish(): void;
24
+ }): import("react").JSX.Element;
@@ -2,12 +2,12 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { BASE } from "../shell/base.js";
4
4
  import { localeStatusOf } from "../shell/pages-tree.js";
5
- import { SharedFields } from "../shell/SharedFields.js";
5
+ import { GlobalFields } from "../shell/GlobalFields.js";
6
6
  import { PublishControl } from "../ui/PublishControl.js";
7
- export function SharedInspector({ item, edit, locale, entries, multilingual, refOptions, inlinePath, onEditHere, onChange, pending, busy, onPublish, onDiscard, onUnpublish, }) {
7
+ export function GlobalInspector({ item, edit, locale, groups, multilingual, refOptions, inlinePath, onEditHere, onChange, pending, busy, onPublish, onDiscard, onUnpublish, }) {
8
8
  const row = edit?.record.locales[locale];
9
- // The same three states the shared list shows; the control decides what applies.
9
+ // The same three states the global list shows; the control decides what applies.
10
10
  const status = row ? localeStatusOf(row) : "draft";
11
11
  const pages = edit ? [...new Set(edit.usage.filter((u) => u.sourceKind === "page").map((u) => u.sourceId))] : [];
12
- return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [_jsx("p", { className: "sm-hint", children: "Shared content \u2014 changes every page it is used on." }), edit && (_jsxs("p", { className: "sm-hint", children: [pages.length === 0 ? "Not placed on any page." : `Used on ${pages.length} page${pages.length === 1 ? "" : "s"}: `, pages.map((id, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx("a", { href: `${BASE}/pages/${encodeURIComponent(id)}?locale=${encodeURIComponent(locale)}`, children: id })] }, id)))] })), _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }, children: _jsx(PublishControl, { status: row ? status : null, pending: pending, busy: busy, disabled: !edit, onPublish: onPublish, onDiscard: onDiscard, onUnpublish: onUnpublish }) }), edit ? (_jsx(SharedFields, { item: item, entries: entries, fields: edit.fields, errors: edit.errors, refOptions: refOptions, multilingual: multilingual, inlinePath: inlinePath, onEditHere: onEditHere, onChange: onChange })) : (_jsx("p", { style: { color: "var(--text-muted)" }, children: "Loading\u2026" }))] }));
12
+ return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 12 }, children: [_jsx("p", { className: "sm-hint", children: "Global content \u2014 changes every page it is used on." }), edit && (_jsxs("p", { className: "sm-hint", children: [pages.length === 0 ? "Not placed on any page." : `Used on ${pages.length} page${pages.length === 1 ? "" : "s"}: `, pages.map((id, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx("a", { href: `${BASE}/pages/${encodeURIComponent(id)}?locale=${encodeURIComponent(locale)}`, children: id })] }, id)))] })), _jsx("div", { style: { display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }, children: _jsx(PublishControl, { status: row ? status : null, pending: pending, busy: busy, disabled: !edit, onPublish: onPublish, onDiscard: onDiscard, onUnpublish: onUnpublish }) }), edit ? (_jsx(GlobalFields, { item: item, groups: groups, fields: edit.fields, errors: edit.errors, refOptions: refOptions, multilingual: multilingual, inlinePath: inlinePath, onEditHere: onEditHere, onChange: onChange })) : (_jsx("p", { style: { color: "var(--text-muted)" }, children: "Loading\u2026" }))] }));
13
13
  }
@@ -1,18 +1,19 @@
1
1
  import type { ZonePolicy } from "../../zones.ts";
2
- import type { AdminRegistry, AdminSection, AdminShared } from "../serialize.ts";
2
+ import type { AdminRegistry, AdminSection, AdminGlobal } from "../serialize.ts";
3
3
  export type PickerAt = {
4
4
  zone: string;
5
5
  index: number;
6
6
  x: number;
7
7
  y: number;
8
8
  };
9
- export declare function InsertPicker({ at, policy, registry, sharedIds, onInsert, onInsertShared, onClose }: {
9
+ export declare function InsertPicker({ at, policy, registry, globalIds, onInsert, onInsertGlobal, onInsertCustom, onClose }: {
10
10
  at: PickerAt;
11
11
  policy: ZonePolicy | undefined;
12
12
  registry: AdminRegistry;
13
- /** Shared item name → row id; an item without one cannot be placed. */
14
- sharedIds: Record<string, string>;
13
+ /** Global item name → row id; an item without one cannot be placed. */
14
+ globalIds: Record<string, string>;
15
15
  onInsert(section: AdminSection): void;
16
- onInsertShared(item: AdminShared): void;
16
+ onInsertGlobal(item: AdminGlobal): void;
17
+ onInsertCustom(): void;
17
18
  onClose(): void;
18
19
  }): import("react").JSX.Element;