smoodly 0.0.16 → 0.0.18

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 (142) hide show
  1. package/README.md +176 -26
  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 +8 -6
  7. package/dist/admin/editor/EditorView.js +140 -108
  8. package/dist/admin/editor/GlobalInspector.d.ts +27 -0
  9. package/dist/admin/editor/GlobalInspector.js +13 -0
  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/ActionMenu.d.ts +3 -1
  57. package/dist/admin/ui/ActionMenu.js +4 -3
  58. package/dist/admin/ui/HistoryModal.js +1 -1
  59. package/dist/admin/ui/PublishControl.d.ts +3 -1
  60. package/dist/admin/ui/PublishControl.js +2 -2
  61. package/dist/admin/ui/primitives.d.ts +4 -2
  62. package/dist/admin/ui/primitives.js +3 -3
  63. package/dist/admin/ui/theme.js +61 -18
  64. package/dist/admin/validate.d.ts +27 -2
  65. package/dist/admin/validate.js +47 -10
  66. package/dist/collections.d.ts +3 -3
  67. package/dist/collections.js +1 -1
  68. package/dist/conditions.d.ts +33 -0
  69. package/dist/conditions.js +79 -0
  70. package/dist/config.d.ts +26 -19
  71. package/dist/config.js +26 -25
  72. package/dist/css.d.ts +7 -0
  73. package/dist/css.js +57 -0
  74. package/dist/custom/CustomSection.d.ts +26 -0
  75. package/dist/custom/CustomSection.js +20 -0
  76. package/dist/custom/css.d.ts +3 -0
  77. package/dist/custom/css.js +28 -0
  78. package/dist/custom/index.d.ts +26 -0
  79. package/dist/custom/index.js +16 -0
  80. package/dist/custom/rows.d.ts +28 -0
  81. package/dist/custom/rows.js +129 -0
  82. package/dist/custom/schema.d.ts +32 -0
  83. package/dist/custom/schema.js +39 -0
  84. package/dist/entry-page.d.ts +5 -0
  85. package/dist/entry-page.js +1 -1
  86. package/dist/entry-store.d.ts +4 -4
  87. package/dist/entry-store.js +17 -17
  88. package/dist/fields.d.ts +5 -2
  89. package/dist/fields.js +4 -2
  90. package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
  91. package/dist/{shared-id.js → global-id.js} +4 -4
  92. package/dist/global.d.ts +57 -0
  93. package/dist/global.js +51 -0
  94. package/dist/index.d.ts +31 -21
  95. package/dist/index.js +11 -6
  96. package/dist/media.js +1 -1
  97. package/dist/next/index.d.ts +1 -1
  98. package/dist/next/page-renderer.d.ts +4 -2
  99. package/dist/next/page-renderer.js +16 -12
  100. package/dist/next/queries.d.ts +6 -6
  101. package/dist/next/queries.js +7 -7
  102. package/dist/page.js +2 -2
  103. package/dist/pairing.d.ts +13 -7
  104. package/dist/pairing.js +4 -4
  105. package/dist/paths.d.ts +21 -0
  106. package/dist/paths.js +25 -0
  107. package/dist/refs.d.ts +2 -2
  108. package/dist/refs.js +4 -4
  109. package/dist/render.d.ts +23 -1
  110. package/dist/render.js +12 -6
  111. package/dist/resolve.d.ts +11 -6
  112. package/dist/resolve.js +22 -12
  113. package/dist/revalidate.d.ts +4 -4
  114. package/dist/revalidate.js +4 -4
  115. package/dist/richtext-render.js +2 -2
  116. package/dist/richtext-walk.d.ts +5 -5
  117. package/dist/richtext-walk.js +4 -4
  118. package/dist/schema.d.ts +15 -10
  119. package/dist/schema.js +19 -7
  120. package/dist/section-wrapper.d.ts +18 -6
  121. package/dist/section-wrapper.js +24 -17
  122. package/dist/site.d.ts +29 -14
  123. package/dist/site.js +55 -28
  124. package/dist/sql-space.js +1 -1
  125. package/dist/sql.js +2 -2
  126. package/dist/store.d.ts +7 -0
  127. package/dist/store.js +6 -0
  128. package/dist/styles.d.ts +27 -0
  129. package/dist/styles.js +60 -0
  130. package/dist/supabase-entry-store.js +14 -14
  131. package/dist/supabase-store.js +2 -1
  132. package/dist/zones.d.ts +9 -7
  133. package/dist/zones.js +5 -5
  134. package/package.json +1 -1
  135. package/dist/admin/editor/SharedInspector.d.ts +0 -23
  136. package/dist/admin/editor/SharedInspector.js +0 -13
  137. package/dist/admin/editor/useSharedEdit.d.ts +0 -6
  138. package/dist/admin/shared-items.d.ts +0 -9
  139. package/dist/admin/shell/SharedFields.d.ts +0 -21
  140. package/dist/admin/shell/SharedFields.js +0 -15
  141. package/dist/shared.d.ts +0 -53
  142. package/dist/shared.js +0 -45
@@ -1,6 +1,8 @@
1
1
  import { fixedSlug } from "../page.js";
2
2
  import { collectionDepth, collectionOrder } from "../collections.js";
3
3
  import { perLocaleSegments } from "../paths.js";
4
+ import { styleDescriptors } from "../styles.js";
5
+ import { CUSTOM_NODE_TYPE, customFields, defaultRow } from "../custom/index.js";
4
6
  function serializeDescriptor(d) {
5
7
  const out = { type: d.type };
6
8
  for (const [key, value] of Object.entries(d)) {
@@ -22,6 +24,14 @@ function serializeDescriptor(d) {
22
24
  function serializeMap(map) {
23
25
  return Object.fromEntries(Object.entries(map ?? {}).map(([k, d]) => [k, serializeDescriptor(d)]));
24
26
  }
27
+ /** A registered section's settings descriptors — what a visual global's form shows (spec 2026-09-14 §3). */
28
+ export function sectionSettings(config, name) {
29
+ return (config.registry.sections ?? []).find((s) => s.schema.name === name)?.schema.settings ?? {};
30
+ }
31
+ /** Whether a section gets the custom-CSS box: the project flag, unless the schema opts out (spec 2026-09-14 §4). */
32
+ function sectionCss(config, name) {
33
+ return config.css && (config.registry.sections ?? []).find((s) => s.schema.name === name)?.schema.css !== false;
34
+ }
25
35
  export function serializeAdminConfig(config) {
26
36
  const perLocale = (v) => perLocaleSegments(v, config.locales.supported);
27
37
  return {
@@ -33,8 +43,9 @@ export function serializeAdminConfig(config) {
33
43
  icon: s.icon,
34
44
  description: s.description,
35
45
  fields: serializeMap(s.fields),
46
+ settings: serializeMap(s.settings),
36
47
  styles: serializeMap(config.resolvedStyles(s.name)),
37
- meta: serializeMap(s.meta),
48
+ css: sectionCss(config, s.name),
38
49
  sample: s.sample,
39
50
  };
40
51
  }),
@@ -47,8 +58,7 @@ export function serializeAdminConfig(config) {
47
58
  description: e.description,
48
59
  contexts: [...(e.contexts ?? [])],
49
60
  fields: serializeMap(e.fields),
50
- styles: serializeMap(e.styles),
51
- meta: serializeMap(e.meta),
61
+ settings: serializeMap(e.settings),
52
62
  sample: e.sample,
53
63
  };
54
64
  }),
@@ -73,13 +83,26 @@ export function serializeAdminConfig(config) {
73
83
  seo: p.meta?.seo === true,
74
84
  };
75
85
  }),
76
- shared: (config.registry.shared ?? []).map((s) => ({
86
+ globals: (config.registry.globals ?? []).map((s) => ({
77
87
  name: s.name,
78
88
  title: s.title,
79
89
  ...(s.section !== undefined ? { section: s.section } : {}),
80
90
  fields: serializeMap(s.fields),
91
+ settings: serializeMap(s.section !== undefined ? sectionSettings(config, s.section) : {}),
81
92
  styles: serializeMap(s.section !== undefined ? config.resolvedStyles(s.section) : {}),
93
+ css: s.section !== undefined ? sectionCss(config, s.section) : false,
82
94
  })),
95
+ custom: {
96
+ name: CUSTOM_NODE_TYPE,
97
+ title: "Custom section",
98
+ description: "Your own rows of columns",
99
+ fields: serializeMap(customFields(config.custom.toolset)),
100
+ settings: {},
101
+ styles: serializeMap(styleDescriptors(config.styles)),
102
+ css: config.css,
103
+ sample: { rows: [defaultRow()] },
104
+ },
105
+ css: config.css,
83
106
  locales: config.locales,
84
107
  homePageSlug: config.homePageSlug,
85
108
  pageDepth: config.pageDepth,
@@ -17,8 +17,8 @@ import { makeClientOps } from "../client-ops.js";
17
17
  import { PagesList } from "./PagesList.js";
18
18
  import { EntriesList } from "./EntriesList.js";
19
19
  import { EntryForm } from "./EntryForm.js";
20
- import { SharedList } from "./SharedList.js";
21
- import { SharedForm } from "./SharedForm.js";
20
+ import { GlobalList } from "./GlobalList.js";
21
+ import { GlobalForm } from "./GlobalForm.js";
22
22
  import { EditorView } from "../editor/EditorView.js";
23
23
  import { LoginView } from "./LoginView.js";
24
24
  import { useAdminSession } from "./session.js";
@@ -49,10 +49,10 @@ export function AdminApp({ registry, op, auth }) {
49
49
  main = _jsx(EntriesList, { ops: ops, registry: registry, collection: head });
50
50
  else if (isCollection && tail)
51
51
  main = _jsx(EntryForm, { ops: ops, registry: registry, collection: head, id: decodeURIComponent(tail) });
52
- else if (head === "shared" && !tail)
53
- main = _jsx(SharedList, { ops: ops, registry: registry });
54
- else if (head === "shared" && tail)
55
- main = _jsx(SharedForm, { ops: ops, registry: registry, name: decodeURIComponent(tail) });
52
+ else if (head === "globals" && !tail)
53
+ main = _jsx(GlobalList, { ops: ops, registry: registry });
54
+ else if (head === "globals" && tail)
55
+ main = _jsx(GlobalForm, { ops: ops, registry: registry, name: decodeURIComponent(tail) });
56
56
  else if (head === "media")
57
57
  main = _jsx(MediaScreen, {});
58
58
  else
@@ -0,0 +1,24 @@
1
+ import type { AdminGlobal } from "../serialize.ts";
2
+ import type { Group } from "../forms/tabs.ts";
3
+ import { type NodeValues } from "../editor/InspectorFields.tsx";
4
+ export type RefOption = {
5
+ id: string;
6
+ title: string;
7
+ status: string;
8
+ };
9
+ /** The item's value map as the inspector reads it: the reserved keys pulled out into their namespaces. */
10
+ export declare function globalValues(fields: Record<string, unknown>): NodeValues;
11
+ export declare function GlobalFields({ item, groups, fields, errors, refOptions, multilingual, inlinePath, onEditHere, onChange }: {
12
+ item: AdminGlobal;
13
+ groups: Group[];
14
+ /** The merged draft fields; a visual item's settings, styles and css under their reserved keys. */
15
+ fields: Record<string, unknown>;
16
+ errors?: Record<string, string>;
17
+ /** Loaded ref options by target collection. */
18
+ refOptions: Record<string, RefOption[]>;
19
+ multilingual: boolean;
20
+ /** The content field with an active inline session: its richtext widget yields to a note. */
21
+ inlinePath?: string;
22
+ onEditHere?(): void;
23
+ onChange(ns: "fields" | "settings" | "styles" | "css", key: string, value: unknown): void;
24
+ }): import("react").JSX.Element;
@@ -0,0 +1,19 @@
1
+ "use client";
2
+ import { jsx as _jsx } from "react/jsx-runtime";
3
+ import { CSS_KEY, SETTINGS_KEY, STYLES_KEY } from "../../global.js";
4
+ import { FieldWidget } from "../forms/FieldWidget.js";
5
+ import { InspectorFields, valueOf } from "../editor/InspectorFields.js";
6
+ import { UnlocalizedMark } from "../ui/primitives.js";
7
+ /** The item's value map as the inspector reads it: the reserved keys pulled out into their namespaces. */
8
+ export function globalValues(fields) {
9
+ const ns = (key) => (fields[key] && typeof fields[key] === "object" ? fields[key] : {});
10
+ return { fields, settings: ns(SETTINGS_KEY), styles: ns(STYLES_KEY), css: typeof fields[CSS_KEY] === "string" ? fields[CSS_KEY] : undefined };
11
+ }
12
+ export function GlobalFields({ item, groups, fields, errors, refOptions, multilingual, inlinePath, onEditHere, onChange }) {
13
+ const values = globalValues(fields);
14
+ return (_jsx(InspectorFields, { groups: groups, values: values, render: ({ key, ns, descriptor }, { emptyLabel, scope }) => {
15
+ const target = (descriptor.type === "ref" || descriptor.type === "refList") && typeof descriptor.target === "string" ? descriptor.target : null;
16
+ const localized = ns === "fields" && item.fields[key]?.localized === true;
17
+ return (_jsx(FieldWidget, { fieldKey: key, descriptor: descriptor, value: valueOf(values, { key, ns, descriptor }), emptyLabel: emptyLabel, scope: scope, errors: errors, refOptions: target ? refOptions[target] ?? [] : undefined, mark: ns === "fields" && multilingual && !localized ? _jsx(UnlocalizedMark, {}) : undefined, inlinePath: ns === "fields" ? inlinePath : undefined, onEditHere: onEditHere, onChange: (value) => onChange(ns, key, value) }, `${ns}.${key}`));
18
+ } }));
19
+ }
@@ -1,6 +1,6 @@
1
1
  import type { AdminOps } from "../ops.ts";
2
2
  import type { AdminRegistry } from "../serialize.ts";
3
- export declare function SharedForm({ ops, registry, name }: {
3
+ export declare function GlobalForm({ ops, registry, name }: {
4
4
  ops: AdminOps;
5
5
  registry: AdminRegistry;
6
6
  name: string;
@@ -1,17 +1,19 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
- // One Globals item (shared content, spec 2026-09-07 §6), one locale: the
3
+ // One Globals item (global content, spec 2026-09-07 §6), one locale: the
4
4
  // entry form's shape without what a singleton never has — no slug, no
5
5
  // delete, no add/remove locale (every supported locale exists by
6
6
  // construction), no site URL. The fields in schema order, a globe-off
7
7
  // mark after the name of each that is not .localized(), and for a visual
8
- // item a Styles group edited under the reserved `$styles` key. Since
8
+ // item the Settings tab's groups stacked after the content, edited under
9
+ // the reserved `$settings` / `$styles` / `$css` keys (spec 2026-09-14 §5). Since
9
10
  // 2026-09-14 it autosaves like the entry form: no Save button, the top
10
11
  // bar carries the indicator and the one publish control, publish
11
12
  // validates the draft and lists the fields in an amber banner. "Used on
12
13
  // N pages" is the refs index's answer.
13
14
  import { useEffect, useMemo, useRef, useState } from "react";
14
- import { STYLES_KEY } from "../../shared.js";
15
+ import { CSS_KEY, SETTINGS_KEY, STYLES_KEY } from "../../global.js";
16
+ import { tabsFor } from "../forms/tabs.js";
15
17
  import { revealField } from "../forms/reveal.js";
16
18
  import { useDraftAutosave } from "../editor/autosave.js";
17
19
  import { updatedLine } from "../ui/format.js";
@@ -23,11 +25,11 @@ import { localeStatusOf } from "./pages-tree.js";
23
25
  import { ResizeHandle } from "../ui/ResizeHandle.js";
24
26
  import { entryTitle } from "./entries-list.js";
25
27
  import { BASE } from "./base.js";
26
- import { SharedFields } from "./SharedFields.js";
28
+ import { GlobalFields } from "./GlobalFields.js";
27
29
  import { localeFromUrl, writeLocaleToUrl } from "../editor/locale-url.js";
28
30
  import { useLeaveGuard } from "./router.js";
29
- export function SharedForm({ ops, registry, name }) {
30
- const item = registry.shared.find((s) => s.name === name);
31
+ export function GlobalForm({ ops, registry, name }) {
32
+ const item = registry.globals.find((s) => s.name === name);
31
33
  const multilingual = registry.locales.supported.length > 1;
32
34
  const [locale, setLocale] = useState(() => localeFromUrl(registry));
33
35
  const [record, setRecord] = useState(null);
@@ -49,7 +51,7 @@ export function SharedForm({ ops, registry, name }) {
49
51
  const { saveState, mark, flush } = useDraftAutosave({
50
52
  read: () => fieldsRef.current,
51
53
  persist: async (value) => {
52
- const r = await ops.shared.save(name, locale, value);
54
+ const r = await ops.globals.save(name, locale, value);
53
55
  if (r.ok)
54
56
  setRecord(r.data);
55
57
  return r;
@@ -71,7 +73,7 @@ export function SharedForm({ ops, registry, name }) {
71
73
  setFieldErrors({});
72
74
  setDirty(false);
73
75
  setTouched(false);
74
- ops.shared.get(name, locale).then((r) => {
76
+ ops.globals.get(name, locale).then((r) => {
75
77
  if (cancelled)
76
78
  return;
77
79
  if (r.ok) {
@@ -102,7 +104,7 @@ export function SharedForm({ ops, registry, name }) {
102
104
  return () => { cancelled = true; };
103
105
  }, [ops, refTargets, registry, locale]);
104
106
  if (!item)
105
- return _jsx("p", { style: { padding: 32 }, children: "No such shared item." });
107
+ return _jsx("p", { style: { padding: 32 }, children: "No such global item." });
106
108
  const row = record?.locales[locale];
107
109
  const status = row ? localeStatusOf(row) : null;
108
110
  const fail = (r) => {
@@ -123,7 +125,7 @@ export function SharedForm({ ops, registry, name }) {
123
125
  }
124
126
  let r;
125
127
  try {
126
- r = await ops.shared.setStatus(name, locale, "published");
128
+ r = await ops.globals.setStatus(name, locale, "published");
127
129
  }
128
130
  catch {
129
131
  r = { ok: false, code: "internal", message: "Couldn't reach the server." };
@@ -143,7 +145,7 @@ export function SharedForm({ ops, registry, name }) {
143
145
  setBusy("unpublish");
144
146
  let r;
145
147
  try {
146
- r = await ops.shared.setStatus(name, locale, "draft");
148
+ r = await ops.globals.setStatus(name, locale, "draft");
147
149
  }
148
150
  catch {
149
151
  r = { ok: false, code: "internal", message: "Couldn't reach the server." };
@@ -166,7 +168,7 @@ export function SharedForm({ ops, registry, name }) {
166
168
  }
167
169
  let r;
168
170
  try {
169
- r = await ops.shared.discardDraft(name, locale);
171
+ r = await ops.globals.discardDraft(name, locale);
170
172
  }
171
173
  catch {
172
174
  r = { ok: false, code: "internal", message: "Couldn't reach the server." };
@@ -187,7 +189,7 @@ export function SharedForm({ ops, registry, name }) {
187
189
  setNotice({ tone: "error", text: "Couldn't save your latest changes — history not opened." });
188
190
  return;
189
191
  }
190
- const r = await ops.shared.listVersions(name, locale);
192
+ const r = await ops.globals.listVersions(name, locale);
191
193
  if (!r.ok) {
192
194
  fail(r);
193
195
  return;
@@ -196,14 +198,14 @@ export function SharedForm({ ops, registry, name }) {
196
198
  setHistory(historyRows(r.data, row));
197
199
  };
198
200
  const loadSnapshot = async (versionId) => {
199
- const r = await ops.shared.getVersion(name, versionId);
201
+ const r = await ops.globals.getVersion(name, versionId);
200
202
  if (r.ok)
201
203
  setSnapshot({ id: r.data.id, fields: r.data.fields });
202
204
  else
203
205
  fail(r);
204
206
  };
205
207
  const restore = async (versionId) => {
206
- const r = await ops.shared.restore(name, locale, versionId);
208
+ const r = await ops.globals.restore(name, locale, versionId);
207
209
  if (!r.ok) {
208
210
  fail(r);
209
211
  return false;
@@ -225,24 +227,32 @@ export function SharedForm({ ops, registry, name }) {
225
227
  setLocale(next);
226
228
  };
227
229
  const pages = new Set(usage.filter((u) => u.sourceKind === "page").map((u) => u.sourceId));
228
- const fieldEntries = Object.entries(item.fields).map(([key, descriptor]) => ({ key, ns: "fields", descriptor }));
229
- const styleEntries = item.section !== undefined
230
- ? Object.entries(item.styles).map(([key, descriptor]) => ({ key, ns: "styles", descriptor }))
231
- : [];
230
+ // The Globals page has no tab row: Content's groups, then Settings' under one title.
231
+ const tabs = tabsFor(item);
232
+ const contentGroups = tabs.find((t) => t.tab === "Content")?.groups ?? [];
233
+ const settingGroups = tabs.find((t) => t.tab === "Settings")?.groups ?? [];
232
234
  const onChange = (ns, key, value) => {
233
- setFields((f) => ns === "fields"
234
- ? { ...(f ?? {}), [key]: value }
235
- : { ...(f ?? {}), [STYLES_KEY]: { ...(f?.[STYLES_KEY] ?? {}), [key]: value } });
235
+ setFields((f) => {
236
+ const cur = f ?? {};
237
+ if (ns === "fields")
238
+ return { ...cur, [key]: value };
239
+ if (ns === "css") {
240
+ const { [CSS_KEY]: _old, ...rest } = cur;
241
+ return value === undefined || value === "" ? rest : { ...rest, [CSS_KEY]: value };
242
+ }
243
+ const nsKey = ns === "settings" ? SETTINGS_KEY : STYLES_KEY;
244
+ return { ...cur, [nsKey]: { ...(cur[nsKey] ?? {}), [key]: value } };
245
+ });
236
246
  setDirty(true);
237
247
  setTouched(true);
238
248
  mark();
239
249
  };
240
- return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx(_Fragment, { children: _jsx(Breadcrumb, { backHref: `${BASE}/shared`, items: [
250
+ return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx(_Fragment, { children: _jsx(Breadcrumb, { backHref: `${BASE}/globals`, items: [
241
251
  {
242
- label: "Globals", href: `${BASE}/shared`,
252
+ label: "Globals", href: `${BASE}/globals`,
243
253
  trailing: multilingual && (_jsx(LocaleSwitcher, { registry: registry, present: registry.locales.supported, parentLocales: [], value: locale, adding: false, onSwitch: (l) => { void switchLocale(l); }, onAdd: () => undefined })),
244
254
  },
245
255
  { label: item.title },
246
- ] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error") && _jsx(SaveIndicator, { state: saveState }), _jsx(PublishControl, { status: status, pending: dirty || saveState !== "saved", busy: busy, flash: published, disabled: !row, onPublish: () => { void publish(); }, onUnpublish: () => { void unpublish(); }, onDiscard: () => { void discard(); }, onHistory: () => { void openHistory(); } })] }) }), history && row && (_jsx(HistoryModal, { rows: history, draftId: row.draftVersionId, onSelect: (v) => { void loadSnapshot(v); }, onClose: () => setHistory(null), onRestore: restore, render: (versionId) => snapshot?.id === versionId ? (_jsxs("fieldset", { disabled: true, className: "sm-history__form", children: [_jsx(SharedFields, { item: item, entries: fieldEntries, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } }), styleEntries.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Styles" }), _jsx(SharedFields, { item: item, entries: styleEntries, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } })] }))] })) : _jsx("p", { className: "sm-hint", style: { padding: 16 }, children: "Loading\u2026" }) })), _jsxs("div", { className: "sm-editor", children: [_jsxs("div", { className: "sm-form", ref: formRef, children: [notice && (_jsxs(Banner, { tone: notice.tone, onDismiss: () => setNotice(null), children: [_jsx("p", { style: { margin: 0 }, children: notice.text }), _jsx(FieldErrorList, { fields: notice.fields ?? [], onReveal: (path) => { if (formRef.current)
247
- revealField(formRef.current, path); } })] })), record && (_jsxs("p", { className: "sm-hint", children: [pages.size === 0 ? "Not placed on any page yet." : `Used on ${pages.size} page${pages.size === 1 ? "" : "s"}: `, [...pages].map((id, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx("a", { href: `${BASE}/pages/${encodeURIComponent(id)}?locale=${encodeURIComponent(locale)}`, children: id })] }, id)))] })), fields !== null ? (_jsxs(_Fragment, { children: [_jsx(SharedFields, { item: item, entries: fieldEntries, fields: fields, errors: fieldErrors, refOptions: refOptions, multilingual: multilingual, onChange: onChange }), styleEntries.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Styles" }), _jsx(SharedFields, { item: item, entries: styleEntries, fields: fields, errors: fieldErrors, refOptions: refOptions, multilingual: multilingual, onChange: onChange })] }))] })) : !notice && _jsx("p", { style: { color: "var(--text-muted)" }, children: "Loading\u2026" })] }), _jsx(ResizeHandle, {}), _jsx("aside", { className: "sm-inspector", children: record && row && (_jsxs("div", { className: "sm-card", children: [_jsx(SectionLabel, { children: "Document info" }), _jsx(InfoRow, { label: "Kind", value: item.section !== undefined ? `Visual · ${item.section}` : "Fields" }), _jsx(InfoRow, { label: "Updated", value: updatedLine(row.updatedAt, row.updatedBy) }), _jsx(InfoRow, { label: "ID", value: record.id, mono: true })] })) })] })] }));
256
+ ] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error") && _jsx(SaveIndicator, { state: saveState }), _jsx(PublishControl, { status: status, pending: dirty || saveState !== "saved", busy: busy, flash: published, disabled: !row, onPublish: () => { void publish(); }, onUnpublish: () => { void unpublish(); }, onDiscard: () => { void discard(); }, onHistory: () => { void openHistory(); } })] }) }), history && row && (_jsx(HistoryModal, { rows: history, draftId: row.draftVersionId, onSelect: (v) => { void loadSnapshot(v); }, onClose: () => setHistory(null), onRestore: restore, render: (versionId) => snapshot?.id === versionId ? (_jsxs("fieldset", { disabled: true, className: "sm-history__form", children: [_jsx(GlobalFields, { item: item, groups: contentGroups, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } }), settingGroups.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Settings" }), _jsx(GlobalFields, { item: item, groups: settingGroups, fields: snapshot.fields, refOptions: refOptions, multilingual: multilingual, onChange: () => { } })] }))] })) : _jsx("p", { className: "sm-hint", style: { padding: 16 }, children: "Loading\u2026" }) })), _jsxs("div", { className: "sm-editor", children: [_jsxs("div", { className: "sm-form", ref: formRef, children: [notice && (_jsxs(Banner, { tone: notice.tone, onDismiss: () => setNotice(null), children: [_jsx("p", { style: { margin: 0 }, children: notice.text }), _jsx(FieldErrorList, { fields: notice.fields ?? [], onReveal: (path) => { if (formRef.current)
257
+ revealField(formRef.current, path); } })] })), record && (_jsxs("p", { className: "sm-hint", children: [pages.size === 0 ? "Not placed on any page yet." : `Used on ${pages.size} page${pages.size === 1 ? "" : "s"}: `, [...pages].map((id, i) => (_jsxs("span", { children: [i > 0 && ", ", _jsx("a", { href: `${BASE}/pages/${encodeURIComponent(id)}?locale=${encodeURIComponent(locale)}`, children: id })] }, id)))] })), fields !== null ? (_jsxs(_Fragment, { children: [_jsx(GlobalFields, { item: item, groups: contentGroups, fields: fields, errors: fieldErrors, refOptions: refOptions, multilingual: multilingual, onChange: onChange }), settingGroups.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Settings" }), _jsx(GlobalFields, { item: item, groups: settingGroups, fields: fields, errors: fieldErrors, refOptions: refOptions, multilingual: multilingual, onChange: onChange })] }))] })) : !notice && _jsx("p", { style: { color: "var(--text-muted)" }, children: "Loading\u2026" })] }), _jsx(ResizeHandle, {}), _jsx("aside", { className: "sm-inspector", children: record && row && (_jsxs("div", { className: "sm-card", children: [_jsx(SectionLabel, { children: "Document info" }), _jsx(InfoRow, { label: "Kind", value: item.section !== undefined ? `Visual · ${item.section}` : "Fields" }), _jsx(InfoRow, { label: "Updated", value: updatedLine(row.updatedAt, row.updatedBy) }), _jsx(InfoRow, { label: "ID", value: record.id, mono: true })] })) })] })] }));
248
258
  }
@@ -1,6 +1,6 @@
1
1
  import type { AdminOps } from "../ops.ts";
2
2
  import type { AdminRegistry } from "../serialize.ts";
3
- export declare function SharedList({ ops, registry }: {
3
+ export declare function GlobalList({ ops, registry }: {
4
4
  ops: AdminOps;
5
5
  registry: AdminRegistry;
6
6
  }): import("react").JSX.Element;
@@ -1,13 +1,13 @@
1
1
  "use client";
2
2
  import { jsx as _jsx } from "react/jsx-runtime";
3
- // The Globals list (shared content, spec 2026-09-07 §6; renamed in the
4
- // UI 2026-09-09, the route stays /shared): every registered item is
3
+ // The Globals list (global content, spec 2026-09-07 §6; renamed in the
4
+ // UI 2026-09-09, the route is /globals): every registered item is
5
5
  // a row, edited on its own page. Nothing here is created or deleted by
6
- // editors — the registry declares the items and ops.shared.list
6
+ // editors — the registry declares the items and ops.globals.list
7
7
  // materializes them — so there is no "+ New".
8
8
  import { useCallback, useEffect, useState } from "react";
9
9
  import { ListView } from "./ListView.js";
10
- import { sharedRows } from "./shared-list.js";
10
+ import { globalRows } from "./global-list.js";
11
11
  import { statusLabel } from "../ui/status-label.js";
12
12
  import { StatusBadge } from "../ui/primitives.js";
13
13
  import { LocalePicker } from "../ui/LocaleSwitcher.js";
@@ -26,7 +26,7 @@ function rememberedLocale(registry) {
26
26
  }
27
27
  return registry.locales.default;
28
28
  }
29
- export function SharedList({ ops, registry }) {
29
+ export function GlobalList({ ops, registry }) {
30
30
  const [items, setItems] = useState(null);
31
31
  const [error, setError] = useState(null);
32
32
  const [locale, setLocale] = useState(() => rememberedLocale(registry));
@@ -39,22 +39,22 @@ export function SharedList({ ops, registry }) {
39
39
  catch { /* see rememberedLocale */ }
40
40
  };
41
41
  const load = useCallback(() => {
42
- ops.shared.list()
42
+ ops.globals.list()
43
43
  .then((r) => (r.ok ? setItems(r.data) : setError(r.message)))
44
44
  .catch(() => setError("Couldn't reach the server."));
45
45
  }, [ops]);
46
46
  useEffect(load, [load]);
47
47
  if (error)
48
48
  return _jsx(ErrorPane, { message: error });
49
- const rows = items ? sharedRows(items, registry, locale) : [];
49
+ const rows = items ? globalRows(items, registry, locale) : [];
50
50
  return (_jsx(ListView, { title: "Globals", titleTrailing: multilingual && _jsx(LocalePicker, { locales: registry.locales.supported, value: locale, onChange: pickLocale }), columns: [
51
51
  { label: "Title", width: "minmax(160px,1fr)" },
52
52
  { label: "Kind", width: "90px" },
53
53
  { label: "Updated", width: "minmax(110px,160px)" },
54
54
  { label: "Status", width: "170px" },
55
- ], empty: items ? "No shared content is registered." : "Loading…", rows: rows.map((row) => ({
55
+ ], empty: items ? "No globals are registered." : "Loading…", rows: rows.map((row) => ({
56
56
  key: row.name,
57
- href: `${BASE}/shared/${encodeURIComponent(row.name)}?locale=${encodeURIComponent(locale)}`,
57
+ href: `${BASE}/globals/${encodeURIComponent(row.name)}?locale=${encodeURIComponent(locale)}`,
58
58
  cells: [
59
59
  _jsx("span", { className: "sm-cell sm-cell--title", children: row.title }, "t"),
60
60
  _jsx("span", { className: "sm-cell sm-cell--muted", children: row.flavour }, "k"),
@@ -8,8 +8,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
8
8
  //
9
9
  // Items are registry-derived, never separately configured: Pages, then
10
10
  // Collections — one rail item whose flyout lists every collection — then
11
- // Media, Globals (the shared content items, when the registry has any;
12
- // the route stays /shared), and Settings, shown but inert until it
11
+ // Media, Globals (the global content items, when the registry has any;
12
+ // the route is /globals), and Settings, shown but inert until it
13
13
  // exists. The user avatar is pinned to the bottom and opens the same
14
14
  // flyout with the theme toggle and Sign out, which the old nav's footer
15
15
  // held.
@@ -18,7 +18,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
18
18
  // ↑/↓ move between entries, a pointer down outside closes. A child being
19
19
  // active highlights its parent tile and the entry inside the menu.
20
20
  import { useEffect, useId, useRef, useState } from "react";
21
- import { ChevronDown, Globe, Image, LibraryBig, PanelTop, Settings } from "lucide-react";
21
+ import { Globe, Image, LibraryBig, PanelTop, Settings } from "lucide-react";
22
22
  import { ThemeToggle } from "../ui/primitives.js";
23
23
  import { BASE } from "./base.js";
24
24
  import { RouteLink } from "./RouteLink.js";
@@ -33,14 +33,20 @@ export function IconRail({ registry, active, email, onSignOut }) {
33
33
  const root = useRef(null);
34
34
  const uid = useId();
35
35
  const [open, setOpen] = useState(null);
36
+ // Collections opens on hover as well (spec 2026-09-15 rail tweaks): a hover
37
+ // open closes when the pointer leaves and never steals focus; a click open
38
+ // is sticky and moves focus into the menu for the keyboard.
39
+ const [hovered, setHovered] = useState(false);
36
40
  const menuId = (key) => `sm-rail-menu${uid}${key}`;
37
41
  const toggle = (key) => setOpen((cur) => (cur === key ? null : key));
38
42
  const close = () => setOpen(null);
39
- // Opening moves focus into the menu; a pointer down outside closes it.
43
+ const hoverLeave = () => setHovered(false);
44
+ // A click open moves focus into the menu; a pointer down outside closes it.
40
45
  useEffect(() => {
41
46
  if (!open || !root.current)
42
47
  return;
43
- root.current.querySelector(`#${CSS.escape(menuId(open))} [role=menuitem]`)?.focus();
48
+ if (!hovered)
49
+ root.current.querySelector(`#${CSS.escape(menuId(open))} [role=menuitem]`)?.focus();
44
50
  const onPointerDown = (e) => {
45
51
  if (e.target instanceof Node && !root.current?.contains(e.target))
46
52
  close();
@@ -68,7 +74,8 @@ export function IconRail({ registry, active, email, onSignOut }) {
68
74
  const link = (key, label, icon, href) => (_jsx("li", { className: "sm-rail__item", children: href ? (_jsxs(RouteLink, { className: "sm-rail__btn", href: href, "aria-current": key === active ? "page" : undefined, onClick: close, children: [_jsx("span", { className: "sm-rail__tile", children: icon }), _jsx("span", { className: "sm-rail__label", children: label })] })) : (_jsxs("a", { className: "sm-rail__btn", "aria-disabled": true, children: [_jsx("span", { className: "sm-rail__tile", children: icon }), _jsx("span", { className: "sm-rail__label", children: label })] })) }));
69
75
  const collections = registry.collections;
70
76
  const collectionActive = collections.some((c) => c.name === active);
77
+ const collectionsOpen = open === "collections" || (hovered && open === null);
71
78
  const name = email?.split("@")[0] ?? "Editor";
72
79
  const initials = name.slice(0, 2).toUpperCase();
73
- return (_jsxs("nav", { className: "sm-rail", "aria-label": "Primary", ref: root, onKeyDown: onKeyDown, children: [_jsx(AppIcon, {}), _jsxs("ul", { className: "sm-rail__list", children: [link("pages", "Pages", _jsx(PanelTop, { ...ICON }), `${BASE}/pages`), collections.length > 0 && (_jsxs("li", { className: "sm-rail__item", children: [_jsxs("button", { type: "button", className: "sm-rail__btn", "aria-current": collectionActive ? "page" : undefined, "aria-haspopup": "menu", "aria-expanded": open === "collections", "aria-controls": menuId("collections"), onClick: () => toggle("collections"), children: [_jsxs("span", { className: "sm-rail__tile", children: [_jsx(LibraryBig, { ...ICON }), _jsx("span", { className: "sm-rail__chev", children: _jsx(ChevronDown, { ...ICON, size: 8, strokeWidth: 3 }) })] }), _jsx("span", { className: "sm-rail__label", children: "Collections" })] }), _jsxs("ul", { className: "sm-rail__menu", role: "menu", id: menuId("collections"), "data-open": open === "collections", children: [_jsx("li", { className: "sm-rail__menu-title", role: "presentation", children: "Collections" }), collections.map((c) => (_jsx("li", { role: "none", children: _jsx(RouteLink, { role: "menuitem", className: "sm-rail__menu-item", href: `${BASE}/${c.name}`, "aria-current": c.name === active ? "page" : undefined, onClick: close, children: _jsx("span", { children: c.title }) }) }, c.name)))] })] })), link("media", "Media", _jsx(Image, { ...ICON }), `${BASE}/media`), registry.shared.length > 0 && link("shared", "Globals", _jsx(Globe, { ...ICON }), `${BASE}/shared`), link("settings", "Settings", _jsx(Settings, { ...ICON }), null)] }), _jsxs("div", { className: "sm-rail__item sm-rail__foot", children: [_jsx("button", { type: "button", className: "sm-rail__user", title: name, "aria-haspopup": "menu", "aria-expanded": open === "user", "aria-controls": menuId("user"), onClick: () => toggle("user"), children: initials }), _jsxs("ul", { className: "sm-rail__menu sm-rail__menu--up", role: "menu", id: menuId("user"), "data-open": open === "user", children: [_jsx("li", { className: "sm-rail__menu-title", role: "presentation", children: name }), _jsx("li", { role: "none", children: _jsx(ThemeToggle, {}) }), _jsx("li", { role: "none", children: _jsx("button", { type: "button", role: "menuitem", className: "sm-rail__menu-item", onClick: () => { close(); onSignOut(); }, children: "Sign out" }) })] })] })] }));
80
+ return (_jsxs("nav", { className: "sm-rail", "aria-label": "Primary", ref: root, onKeyDown: onKeyDown, children: [_jsx(AppIcon, {}), _jsxs("ul", { className: "sm-rail__list", children: [link("pages", "Pages", _jsx(PanelTop, { ...ICON }), `${BASE}/pages`), collections.length > 0 && (_jsxs("li", { className: "sm-rail__item", onMouseEnter: () => setHovered(true), onMouseLeave: hoverLeave, children: [_jsxs("button", { type: "button", className: "sm-rail__btn", "aria-current": collectionActive ? "page" : undefined, "aria-haspopup": "menu", "aria-expanded": collectionsOpen, "aria-controls": menuId("collections"), onClick: () => toggle("collections"), children: [_jsx("span", { className: "sm-rail__tile", children: _jsx(LibraryBig, { ...ICON }) }), _jsx("span", { className: "sm-rail__label", children: "Collections" })] }), _jsx("ul", { className: "sm-rail__menu sm-rail__menu--collections", role: "menu", id: menuId("collections"), "data-open": collectionsOpen, children: collections.map((c) => (_jsx("li", { role: "none", children: _jsx(RouteLink, { role: "menuitem", className: "sm-rail__menu-item", href: `${BASE}/${c.name}`, "aria-current": c.name === active ? "page" : undefined, onClick: () => { close(); hoverLeave(); }, children: _jsx("span", { children: c.title }) }) }, c.name))) })] })), link("media", "Media", _jsx(Image, { ...ICON }), `${BASE}/media`), registry.globals.length > 0 && link("globals", "Globals", _jsx(Globe, { ...ICON }), `${BASE}/globals`), link("settings", "Settings", _jsx(Settings, { ...ICON }), null)] }), _jsxs("div", { className: "sm-rail__item sm-rail__foot", children: [_jsx("button", { type: "button", className: "sm-rail__user", title: name, "aria-haspopup": "menu", "aria-expanded": open === "user", "aria-controls": menuId("user"), onClick: () => toggle("user"), children: initials }), _jsxs("ul", { className: "sm-rail__menu sm-rail__menu--up", role: "menu", id: menuId("user"), "data-open": open === "user", children: [_jsx("li", { className: "sm-rail__menu-title", role: "presentation", children: name }), _jsx("li", { role: "none", children: _jsx(ThemeToggle, {}) }), _jsx("li", { role: "none", children: _jsx("button", { type: "button", role: "menuitem", className: "sm-rail__menu-item", onClick: () => { close(); onSignOut(); }, children: "Sign out" }) })] })] })] }));
74
81
  }
@@ -154,7 +154,6 @@ export function PagesList({ ops, registry }) {
154
154
  return;
155
155
  void run(() => ops.pages.rename(row.id, { locale, title, slug }));
156
156
  };
157
- const move = (row, dir) => void run(() => ops.pages.move(row.id, { parentId: row.parentId, index: row.index + dir }));
158
157
  const destroy = (row) => {
159
158
  const scope = row.locales.length > 1 ? ` in every language (${row.locales.join(", ")})` : "";
160
159
  if (!window.confirm(`Delete "${row.title}"${scope}?`))
@@ -175,11 +174,7 @@ export function PagesList({ ops, registry }) {
175
174
  items.push({ label: "Add page here", run: () => addPageHere(row) });
176
175
  if (!row.fixed)
177
176
  items.push({ label: "Rename…", run: () => rename(row) });
178
- // Every row reorders among its siblings, fixed nodes and the home page included (root order is nothing code owns).
179
- if (row.index > 0)
180
- items.push({ label: "Move up", run: () => move(row, -1) });
181
- if (row.index < row.siblingCount - 1)
182
- items.push({ label: "Move down", run: () => move(row, 1) });
177
+ // Reordering is the grip's (drag, or its arrow keys) — no Move up / Move down here.
183
178
  if (multilingual && !row.fixed && !row.isHome && row.locales.length > 1) {
184
179
  items.push({ label: `Remove from ${locale}`, run: () => removeLocale(row) });
185
180
  }
@@ -1,14 +1,14 @@
1
1
  import type { EntryRecord } from "../../entry-store.ts";
2
2
  import type { AdminRegistry } from "../serialize.ts";
3
3
  import { type LocaleStatus } from "./pages-tree.ts";
4
- export type SharedRow = {
4
+ export type GlobalRow = {
5
5
  name: string;
6
6
  title: string;
7
7
  flavour: "Visual" | "Fields";
8
8
  status: LocaleStatus;
9
9
  updatedAt: number;
10
10
  };
11
- export declare function sharedRows(items: {
11
+ export declare function globalRows(items: {
12
12
  name: string;
13
13
  record: EntryRecord;
14
- }[], registry: AdminRegistry, locale: string): SharedRow[];
14
+ }[], registry: AdminRegistry, locale: string): GlobalRow[];
@@ -1,7 +1,7 @@
1
1
  import { localeStatusOf } from "./pages-tree.js";
2
- export function sharedRows(items, registry, locale) {
2
+ export function globalRows(items, registry, locale) {
3
3
  const byName = new Map(items.map((i) => [i.name, i.record]));
4
- return registry.shared.flatMap((item) => {
4
+ return registry.globals.flatMap((item) => {
5
5
  const record = byName.get(item.name);
6
6
  const row = record?.locales[locale];
7
7
  if (!record || !row)
@@ -1,6 +1,6 @@
1
1
  import type { PageTree, TreeNode } from "../render.tsx";
2
2
  import type { ZonePolicy } from "../zones.ts";
3
- import type { AdminSection, AdminShared } from "./serialize.ts";
3
+ import type { AdminSection, AdminGlobal } from "./serialize.ts";
4
4
  export declare function zoneOf(tree: PageTree, id: string): string | null;
5
5
  export declare function findNode(tree: PageTree, id: string): TreeNode | null;
6
6
  export declare function insertNode(tree: PageTree, zone: string, index: number, node: TreeNode): PageTree;
@@ -10,7 +10,9 @@ export declare function moveNode(tree: PageTree, id: string, dir: -1 | 1): PageT
10
10
  * then spliced in — moveItem's convention); clamped, and the same tree
11
11
  * when nothing changes. The drop path; `moveNode` stays for the arrows. */
12
12
  export declare function moveNodeTo(tree: PageTree, id: string, index: number): PageTree;
13
- export declare function patchNode(tree: PageTree, id: string, ns: "fields" | "styles" | "meta", key: string, value: unknown): PageTree;
13
+ export declare function patchNode(tree: PageTree, id: string, ns: "fields" | "settings" | "styles", key: string, value: unknown): PageTree;
14
+ /** The node's custom CSS (spec 2026-09-14 §4); undefined or "" removes the key. */
15
+ export declare function patchNodeCss(tree: PageTree, id: string, css: string | undefined): PageTree;
14
16
  /** patchNode for a dotted path inside `fields` (spec 2026-09-10 §1):
15
17
  * `points.2.heading` sets one item's heading and copies only the spine. */
16
18
  export declare function patchNodePath(tree: PageTree, id: string, path: string, value: unknown): PageTree;
@@ -18,19 +20,19 @@ export declare function canInsert(policy: ZonePolicy | undefined, count: number)
18
20
  export declare function allowedSections(policy: ZonePolicy | undefined, all: AdminSection[]): AdminSection[];
19
21
  /** The visual items the picker offers in a zone: materialized (an id is
20
22
  * known) and rendering a section the zone's policy allows. */
21
- export declare function allowedShared(policy: ZonePolicy | undefined, shared: AdminShared[], sharedIds: Record<string, string>): AdminShared[];
23
+ export declare function allowedGlobals(policy: ZonePolicy | undefined, globals: AdminGlobal[], globalIds: Record<string, string>): AdminGlobal[];
22
24
  /** A page places a visual item as this node (spec 2026-09-07 §6): `item`
23
25
  * names the registration, `ref` is the item's row id. */
24
- export declare function placementNode(item: AdminShared, ref: string, id: string): TreeNode;
25
- /** Also fills an element node (an `AdminElement` is a section shape plus contexts). */
26
- export declare function nodeFromSample(section: Pick<AdminSection, "name" | "fields" | "styles" | "sample">, id: string): TreeNode;
26
+ export declare function placementNode(item: AdminGlobal, ref: string, id: string): TreeNode;
27
+ /** Also fills an element node (an `AdminElement` is a section shape plus contexts). Never seeds `styles` or `css`: Default stores nothing (spec 2026-09-14 §2). */
28
+ export declare function nodeFromSample(section: Pick<AdminSection, "name" | "fields" | "settings" | "sample">, id: string): TreeNode;
27
29
  /** A locked zone holds exactly one node the editor can never insert or
28
30
  * remove — so someone has to put it there. This does, from the component's
29
- * sample (or, for a zone bound to a shared item, a placement), for every
31
+ * sample (or, for a zone bound to a global item, a placement), for every
30
32
  * locked zone that is empty or missing. Runs when the editor loads a draft,
31
33
  * which also covers pages that predate a locked zone being added to their
32
34
  * schema. Returns the same tree when nothing changed. */
33
- export declare function fillLockedZones(tree: PageTree, policies: Record<string, ZonePolicy>, sections: AdminSection[], sharedIds?: Record<string, string>): PageTree;
35
+ export declare function fillLockedZones(tree: PageTree, policies: Record<string, ZonePolicy>, sections: AdminSection[], globalIds?: Record<string, string>): PageTree;
34
36
  export declare function newNodeId(type: string): string;
35
37
  /** A page-level value — the template's page fields or the SEO meta (spec 2026-09-10 page fields §5). */
36
38
  export declare function patchPage(tree: PageTree, ns: "fields" | "meta", key: string, value: unknown): PageTree;