smoodly 0.0.15 → 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 (154) hide show
  1. package/README.md +238 -36
  2. package/dist/admin/auth.d.ts +7 -4
  3. package/dist/admin/auth.js +1 -1
  4. package/dist/admin/client-ops.js +3 -3
  5. package/dist/admin/custom-section.d.ts +10 -0
  6. package/dist/admin/custom-section.js +10 -0
  7. package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
  8. package/dist/admin/editor/CanvasOverlay.js +6 -6
  9. package/dist/admin/editor/EditorView.d.ts +2 -0
  10. package/dist/admin/editor/EditorView.js +322 -62
  11. package/dist/admin/editor/GlobalInspector.d.ts +24 -0
  12. package/dist/admin/editor/GlobalInspector.js +13 -0
  13. package/dist/admin/editor/InsertPicker.d.ts +6 -5
  14. package/dist/admin/editor/InsertPicker.js +6 -4
  15. package/dist/admin/editor/InspectorFields.d.ts +21 -0
  16. package/dist/admin/editor/InspectorFields.js +22 -0
  17. package/dist/admin/editor/Outline.d.ts +0 -1
  18. package/dist/admin/editor/Outline.js +3 -3
  19. package/dist/admin/editor/PageSettings.js +3 -2
  20. package/dist/admin/editor/global-edit.d.ts +39 -0
  21. package/dist/admin/editor/global-edit.js +124 -0
  22. package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
  23. package/dist/admin/editor/useGlobalEdit.js +18 -0
  24. package/dist/admin/forms/FieldWidget.d.ts +14 -1
  25. package/dist/admin/forms/FieldWidget.js +38 -11
  26. package/dist/admin/forms/ListField.d.ts +9 -1
  27. package/dist/admin/forms/ListField.js +16 -8
  28. package/dist/admin/forms/RichtextElementCard.js +11 -5
  29. package/dist/admin/forms/RowsWidget.d.ts +13 -0
  30. package/dist/admin/forms/RowsWidget.js +26 -0
  31. package/dist/admin/forms/SpanBar.d.ts +5 -0
  32. package/dist/admin/forms/SpanBar.js +37 -0
  33. package/dist/admin/forms/richtext-insert.d.ts +2 -2
  34. package/dist/admin/forms/richtext-insert.js +3 -3
  35. package/dist/admin/forms/richtext-nodes.js +1 -1
  36. package/dist/admin/forms/tabs.d.ts +20 -8
  37. package/dist/admin/forms/tabs.js +19 -23
  38. package/dist/admin/forms/useRichtextEditor.js +8 -1
  39. package/dist/admin/global-items.d.ts +9 -0
  40. package/dist/admin/{shared-items.js → global-items.js} +29 -14
  41. package/dist/admin/index.d.ts +3 -3
  42. package/dist/admin/index.js +1 -1
  43. package/dist/admin/next/op-handler.js +4 -3
  44. package/dist/admin/ops-impl.d.ts +3 -0
  45. package/dist/admin/ops-impl.js +242 -56
  46. package/dist/admin/ops.d.ts +38 -6
  47. package/dist/admin/serialize.d.ts +18 -7
  48. package/dist/admin/serialize.js +27 -5
  49. package/dist/admin/shell/AdminApp.js +7 -7
  50. package/dist/admin/shell/EntriesList.js +19 -3
  51. package/dist/admin/shell/EntryForm.js +183 -77
  52. package/dist/admin/shell/GlobalFields.d.ts +24 -0
  53. package/dist/admin/shell/GlobalFields.js +19 -0
  54. package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
  55. package/dist/admin/shell/GlobalForm.js +258 -0
  56. package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
  57. package/dist/admin/shell/{SharedList.js → GlobalList.js} +12 -11
  58. package/dist/admin/shell/IconRail.js +14 -7
  59. package/dist/admin/shell/PagesList.js +4 -8
  60. package/dist/admin/shell/entries-list.d.ts +2 -2
  61. package/dist/admin/shell/entries-list.js +3 -3
  62. package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
  63. package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
  64. package/dist/admin/supabase-auth.js +1 -1
  65. package/dist/admin/tree-ops.d.ts +10 -8
  66. package/dist/admin/tree-ops.js +28 -16
  67. package/dist/admin/ui/ActionMenu.d.ts +11 -0
  68. package/dist/admin/ui/ActionMenu.js +25 -0
  69. package/dist/admin/ui/HistoryModal.d.ts +23 -0
  70. package/dist/admin/ui/HistoryModal.js +40 -0
  71. package/dist/admin/ui/Modal.d.ts +5 -1
  72. package/dist/admin/ui/Modal.js +3 -3
  73. package/dist/admin/ui/PublishControl.d.ts +31 -0
  74. package/dist/admin/ui/PublishControl.js +24 -0
  75. package/dist/admin/ui/format.d.ts +4 -0
  76. package/dist/admin/ui/format.js +9 -0
  77. package/dist/admin/ui/primitives.d.ts +16 -1
  78. package/dist/admin/ui/primitives.js +20 -4
  79. package/dist/admin/ui/status-label.d.ts +10 -0
  80. package/dist/admin/ui/status-label.js +14 -0
  81. package/dist/admin/ui/theme.js +93 -13
  82. package/dist/admin/validate.d.ts +27 -2
  83. package/dist/admin/validate.js +47 -10
  84. package/dist/collections.d.ts +3 -9
  85. package/dist/collections.js +1 -2
  86. package/dist/conditions.d.ts +33 -0
  87. package/dist/conditions.js +79 -0
  88. package/dist/config.d.ts +26 -19
  89. package/dist/config.js +26 -25
  90. package/dist/css.d.ts +7 -0
  91. package/dist/css.js +57 -0
  92. package/dist/custom/CustomSection.d.ts +26 -0
  93. package/dist/custom/CustomSection.js +20 -0
  94. package/dist/custom/css.d.ts +3 -0
  95. package/dist/custom/css.js +28 -0
  96. package/dist/custom/index.d.ts +26 -0
  97. package/dist/custom/index.js +16 -0
  98. package/dist/custom/rows.d.ts +28 -0
  99. package/dist/custom/rows.js +129 -0
  100. package/dist/custom/schema.d.ts +32 -0
  101. package/dist/custom/schema.js +39 -0
  102. package/dist/entry-store.d.ts +42 -17
  103. package/dist/entry-store.js +107 -40
  104. package/dist/fields.d.ts +5 -2
  105. package/dist/fields.js +4 -2
  106. package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
  107. package/dist/{shared-id.js → global-id.js} +4 -4
  108. package/dist/global.d.ts +57 -0
  109. package/dist/global.js +51 -0
  110. package/dist/index.d.ts +28 -18
  111. package/dist/index.js +10 -5
  112. package/dist/media.js +1 -1
  113. package/dist/next/index.d.ts +1 -1
  114. package/dist/next/page-renderer.d.ts +4 -2
  115. package/dist/next/page-renderer.js +17 -13
  116. package/dist/next/queries.d.ts +6 -6
  117. package/dist/next/queries.js +7 -7
  118. package/dist/page.js +2 -2
  119. package/dist/pairing.d.ts +13 -7
  120. package/dist/pairing.js +4 -4
  121. package/dist/refs.d.ts +2 -2
  122. package/dist/refs.js +4 -4
  123. package/dist/render.d.ts +13 -1
  124. package/dist/render.js +10 -5
  125. package/dist/resolve.d.ts +11 -6
  126. package/dist/resolve.js +22 -12
  127. package/dist/revalidate.d.ts +4 -4
  128. package/dist/revalidate.js +4 -4
  129. package/dist/richtext-render.js +2 -2
  130. package/dist/richtext-walk.d.ts +5 -5
  131. package/dist/richtext-walk.js +4 -4
  132. package/dist/schema.d.ts +15 -10
  133. package/dist/schema.js +19 -7
  134. package/dist/section-wrapper.d.ts +18 -6
  135. package/dist/section-wrapper.js +24 -17
  136. package/dist/site.d.ts +17 -5
  137. package/dist/site.js +36 -12
  138. package/dist/sql-space.js +1 -1
  139. package/dist/sql.js +19 -16
  140. package/dist/store.d.ts +45 -13
  141. package/dist/store.js +88 -12
  142. package/dist/styles.d.ts +27 -0
  143. package/dist/styles.js +60 -0
  144. package/dist/supabase-entry-store.d.ts +11 -6
  145. package/dist/supabase-entry-store.js +115 -39
  146. package/dist/supabase-store.d.ts +13 -6
  147. package/dist/supabase-store.js +90 -20
  148. package/dist/zones.d.ts +9 -7
  149. package/dist/zones.js +5 -5
  150. package/package.json +1 -1
  151. package/dist/admin/shared-items.d.ts +0 -9
  152. package/dist/admin/shell/SharedForm.js +0 -134
  153. package/dist/shared.d.ts +0 -46
  154. package/dist/shared.js +0 -36
@@ -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;
@@ -0,0 +1,258 @@
1
+ "use client";
2
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // One Globals item (global content, spec 2026-09-07 §6), one locale: the
4
+ // entry form's shape without what a singleton never has — no slug, no
5
+ // delete, no add/remove locale (every supported locale exists by
6
+ // construction), no site URL. The fields in schema order, a globe-off
7
+ // mark after the name of each that is not .localized(), and for a visual
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
10
+ // 2026-09-14 it autosaves like the entry form: no Save button, the top
11
+ // bar carries the indicator and the one publish control, publish
12
+ // validates the draft and lists the fields in an amber banner. "Used on
13
+ // N pages" is the refs index's answer.
14
+ import { useEffect, useMemo, useRef, useState } from "react";
15
+ import { CSS_KEY, SETTINGS_KEY, STYLES_KEY } from "../../global.js";
16
+ import { tabsFor } from "../forms/tabs.js";
17
+ import { revealField } from "../forms/reveal.js";
18
+ import { useDraftAutosave } from "../editor/autosave.js";
19
+ import { updatedLine } from "../ui/format.js";
20
+ import { LocaleSwitcher } from "../ui/LocaleSwitcher.js";
21
+ import { Banner, Breadcrumb, FieldErrorList, InfoRow, SaveIndicator, SectionLabel, TopBar } from "../ui/primitives.js";
22
+ import { PublishControl } from "../ui/PublishControl.js";
23
+ import { HistoryModal, historyRows } from "../ui/HistoryModal.js";
24
+ import { localeStatusOf } from "./pages-tree.js";
25
+ import { ResizeHandle } from "../ui/ResizeHandle.js";
26
+ import { entryTitle } from "./entries-list.js";
27
+ import { BASE } from "./base.js";
28
+ import { GlobalFields } from "./GlobalFields.js";
29
+ import { localeFromUrl, writeLocaleToUrl } from "../editor/locale-url.js";
30
+ import { useLeaveGuard } from "./router.js";
31
+ export function GlobalForm({ ops, registry, name }) {
32
+ const item = registry.globals.find((s) => s.name === name);
33
+ const multilingual = registry.locales.supported.length > 1;
34
+ const [locale, setLocale] = useState(() => localeFromUrl(registry));
35
+ const [record, setRecord] = useState(null);
36
+ const [fields, setFields] = useState(null);
37
+ const [usage, setUsage] = useState([]);
38
+ const [refOptions, setRefOptions] = useState({});
39
+ const [notice, setNotice] = useState(null);
40
+ const [fieldErrors, setFieldErrors] = useState({});
41
+ const [dirty, setDirty] = useState(false);
42
+ const [touched, setTouched] = useState(false);
43
+ const [busy, setBusy] = useState(null);
44
+ const [published, setPublished] = useState(false);
45
+ const [reloadKey, setReloadKey] = useState(0);
46
+ const [history, setHistory] = useState(null);
47
+ const [snapshot, setSnapshot] = useState(null);
48
+ const formRef = useRef(null);
49
+ const fieldsRef = useRef(fields);
50
+ fieldsRef.current = fields;
51
+ const { saveState, mark, flush } = useDraftAutosave({
52
+ read: () => fieldsRef.current,
53
+ persist: async (value) => {
54
+ const r = await ops.globals.save(name, locale, value);
55
+ if (r.ok)
56
+ setRecord(r.data);
57
+ return r;
58
+ },
59
+ onSaved: () => setDirty(false),
60
+ });
61
+ useLeaveGuard(dirty || saveState !== "saved");
62
+ const refTargets = useMemo(() => {
63
+ const out = {};
64
+ for (const [key, d] of Object.entries(item?.fields ?? {})) {
65
+ if ((d.type === "ref" || d.type === "refList") && typeof d.target === "string")
66
+ out[key] = d.target;
67
+ }
68
+ return out;
69
+ }, [item]);
70
+ useEffect(() => {
71
+ let cancelled = false;
72
+ setFields(null);
73
+ setFieldErrors({});
74
+ setDirty(false);
75
+ setTouched(false);
76
+ ops.globals.get(name, locale).then((r) => {
77
+ if (cancelled)
78
+ return;
79
+ if (r.ok) {
80
+ setRecord(r.data.record);
81
+ setFields(r.data.fields);
82
+ setUsage(r.data.usage);
83
+ }
84
+ else
85
+ setNotice({ tone: "error", text: r.message });
86
+ }).catch(() => { if (!cancelled)
87
+ setNotice({ tone: "error", text: "Couldn't reach the server." }); });
88
+ return () => { cancelled = true; };
89
+ }, [ops, name, locale, reloadKey]);
90
+ // Ref options in the locale being edited — the entry form's rule.
91
+ useEffect(() => {
92
+ let cancelled = false;
93
+ for (const target of new Set(Object.values(refTargets))) {
94
+ const targetMeta = registry.collections.find((c) => c.name === target);
95
+ ops.entries.list(target).then((r) => {
96
+ if (cancelled || !r.ok)
97
+ return;
98
+ const options = r.data
99
+ .filter((e) => e.locales[locale])
100
+ .map((e) => ({ id: e.id, title: targetMeta ? entryTitle(e, targetMeta, registry, locale) : e.id, status: e.locales[locale].status }));
101
+ setRefOptions((prev) => ({ ...prev, [target]: options }));
102
+ }).catch(console.error);
103
+ }
104
+ return () => { cancelled = true; };
105
+ }, [ops, refTargets, registry, locale]);
106
+ if (!item)
107
+ return _jsx("p", { style: { padding: 32 }, children: "No such global item." });
108
+ const row = record?.locales[locale];
109
+ const status = row ? localeStatusOf(row) : null;
110
+ const fail = (r) => {
111
+ if (r.code === "validation" && r.fields && r.fields.length > 0) {
112
+ setFieldErrors(Object.fromEntries(r.fields.map((f) => [f.field, f.message])));
113
+ setNotice({ tone: "warn", text: r.message, fields: r.fields });
114
+ }
115
+ else
116
+ setNotice({ tone: "error", text: r.message });
117
+ };
118
+ const publish = async () => {
119
+ setNotice(null);
120
+ setBusy("publish");
121
+ if (!(await flush())) {
122
+ setBusy(null);
123
+ setNotice({ tone: "error", text: "Couldn't save your latest changes — publish cancelled." });
124
+ return;
125
+ }
126
+ let r;
127
+ try {
128
+ r = await ops.globals.setStatus(name, locale, "published");
129
+ }
130
+ catch {
131
+ r = { ok: false, code: "internal", message: "Couldn't reach the server." };
132
+ }
133
+ setBusy(null);
134
+ if (!r.ok) {
135
+ fail(r);
136
+ return;
137
+ }
138
+ setRecord(r.data);
139
+ setFieldErrors({});
140
+ setPublished(true);
141
+ setTimeout(() => setPublished(false), 1600);
142
+ };
143
+ const unpublish = async () => {
144
+ setNotice(null);
145
+ setBusy("unpublish");
146
+ let r;
147
+ try {
148
+ r = await ops.globals.setStatus(name, locale, "draft");
149
+ }
150
+ catch {
151
+ r = { ok: false, code: "internal", message: "Couldn't reach the server." };
152
+ }
153
+ setBusy(null);
154
+ if (r.ok)
155
+ setRecord(r.data);
156
+ else
157
+ fail(r);
158
+ };
159
+ const discard = async () => {
160
+ if (!window.confirm("Discard your changes and restore the published version?"))
161
+ return;
162
+ setNotice(null);
163
+ setBusy("discard");
164
+ if (!(await flush())) {
165
+ setBusy(null);
166
+ setNotice({ tone: "error", text: "Couldn't save your latest changes — discard cancelled." });
167
+ return;
168
+ }
169
+ let r;
170
+ try {
171
+ r = await ops.globals.discardDraft(name, locale);
172
+ }
173
+ catch {
174
+ r = { ok: false, code: "internal", message: "Couldn't reach the server." };
175
+ }
176
+ setBusy(null);
177
+ if (!r.ok) {
178
+ fail(r);
179
+ return;
180
+ }
181
+ setRecord(r.data);
182
+ setReloadKey((k) => k + 1);
183
+ setNotice({ tone: "ok", text: "Changes discarded." });
184
+ };
185
+ const openHistory = async () => {
186
+ if (!row)
187
+ return;
188
+ if (!(await flush())) {
189
+ setNotice({ tone: "error", text: "Couldn't save your latest changes — history not opened." });
190
+ return;
191
+ }
192
+ const r = await ops.globals.listVersions(name, locale);
193
+ if (!r.ok) {
194
+ fail(r);
195
+ return;
196
+ }
197
+ setSnapshot(null);
198
+ setHistory(historyRows(r.data, row));
199
+ };
200
+ const loadSnapshot = async (versionId) => {
201
+ const r = await ops.globals.getVersion(name, versionId);
202
+ if (r.ok)
203
+ setSnapshot({ id: r.data.id, fields: r.data.fields });
204
+ else
205
+ fail(r);
206
+ };
207
+ const restore = async (versionId) => {
208
+ const r = await ops.globals.restore(name, locale, versionId);
209
+ if (!r.ok) {
210
+ fail(r);
211
+ return false;
212
+ }
213
+ setRecord(r.data);
214
+ setReloadKey((k) => k + 1);
215
+ setNotice({ tone: "ok", text: "Version restored." });
216
+ return true;
217
+ };
218
+ const switchLocale = async (next) => {
219
+ if (next === locale)
220
+ return;
221
+ if (!(await flush())) {
222
+ setNotice({ tone: "error", text: "Couldn't save your latest changes — stayed on this locale." });
223
+ return;
224
+ }
225
+ writeLocaleToUrl(next);
226
+ setNotice(null);
227
+ setLocale(next);
228
+ };
229
+ const pages = new Set(usage.filter((u) => u.sourceKind === "page").map((u) => u.sourceId));
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 ?? [];
234
+ const onChange = (ns, 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
+ });
246
+ setDirty(true);
247
+ setTouched(true);
248
+ mark();
249
+ };
250
+ return (_jsxs(_Fragment, { children: [_jsx(TopBar, { left: _jsx(_Fragment, { children: _jsx(Breadcrumb, { backHref: `${BASE}/globals`, items: [
251
+ {
252
+ label: "Globals", href: `${BASE}/globals`,
253
+ trailing: multilingual && (_jsx(LocaleSwitcher, { registry: registry, present: registry.locales.supported, parentLocales: [], value: locale, adding: false, onSwitch: (l) => { void switchLocale(l); }, onAdd: () => undefined })),
254
+ },
255
+ { label: item.title },
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 })] })) })] })] }));
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,14 @@
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
+ import { statusLabel } from "../ui/status-label.js";
11
12
  import { StatusBadge } from "../ui/primitives.js";
12
13
  import { LocalePicker } from "../ui/LocaleSwitcher.js";
13
14
  import { relativeTime } from "../ui/format.js";
@@ -25,7 +26,7 @@ function rememberedLocale(registry) {
25
26
  }
26
27
  return registry.locales.default;
27
28
  }
28
- export function SharedList({ ops, registry }) {
29
+ export function GlobalList({ ops, registry }) {
29
30
  const [items, setItems] = useState(null);
30
31
  const [error, setError] = useState(null);
31
32
  const [locale, setLocale] = useState(() => rememberedLocale(registry));
@@ -38,27 +39,27 @@ export function SharedList({ ops, registry }) {
38
39
  catch { /* see rememberedLocale */ }
39
40
  };
40
41
  const load = useCallback(() => {
41
- ops.shared.list()
42
+ ops.globals.list()
42
43
  .then((r) => (r.ok ? setItems(r.data) : setError(r.message)))
43
44
  .catch(() => setError("Couldn't reach the server."));
44
45
  }, [ops]);
45
46
  useEffect(load, [load]);
46
47
  if (error)
47
48
  return _jsx(ErrorPane, { message: error });
48
- const rows = items ? sharedRows(items, registry, locale) : [];
49
+ const rows = items ? globalRows(items, registry, locale) : [];
49
50
  return (_jsx(ListView, { title: "Globals", titleTrailing: multilingual && _jsx(LocalePicker, { locales: registry.locales.supported, value: locale, onChange: pickLocale }), columns: [
50
51
  { label: "Title", width: "minmax(160px,1fr)" },
51
52
  { label: "Kind", width: "90px" },
52
53
  { label: "Updated", width: "minmax(110px,160px)" },
53
- { label: "Status", width: "120px" },
54
- ], empty: items ? "No shared content is registered." : "Loading…", rows: rows.map((row) => ({
54
+ { label: "Status", width: "170px" },
55
+ ], empty: items ? "No globals are registered." : "Loading…", rows: rows.map((row) => ({
55
56
  key: row.name,
56
- href: `${BASE}/shared/${encodeURIComponent(row.name)}?locale=${encodeURIComponent(locale)}`,
57
+ href: `${BASE}/globals/${encodeURIComponent(row.name)}?locale=${encodeURIComponent(locale)}`,
57
58
  cells: [
58
59
  _jsx("span", { className: "sm-cell sm-cell--title", children: row.title }, "t"),
59
60
  _jsx("span", { className: "sm-cell sm-cell--muted", children: row.flavour }, "k"),
60
61
  _jsx("span", { className: "sm-cell sm-cell--muted", children: relativeTime(row.updatedAt) }, "u"),
61
- _jsx(StatusBadge, { status: row.status }, "s"),
62
+ _jsx(StatusBadge, { status: statusLabel(row.status) }, "s"),
62
63
  ],
63
64
  })) }));
64
65
  }
@@ -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,12 +18,12 @@ 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, Image, LayoutPanelTop, Library, Settings2, Square } 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";
25
25
  /** Lucide at the rail's size and stroke (the tile's CSS restates both). */
26
- const ICON = { size: 18, strokeWidth: 1.75, "aria-hidden": true };
26
+ const ICON = { size: 18, strokeWidth: 2, "aria-hidden": true };
27
27
  /** The app icon, inlined: a package can't ship a file into the host app's
28
28
  * /public. Brand blue on white in both themes, so no --logo-filter. */
29
29
  function AppIcon() {
@@ -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(Square, { ...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(Library, { ...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(LayoutPanelTop, { ...ICON }), `${BASE}/shared`), link("settings", "Settings", _jsx(Settings2, { ...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
  }
@@ -11,6 +11,7 @@ import { useCallback, useEffect, useState } from "react";
11
11
  import { ListView } from "./ListView.js";
12
12
  import { canDragRow, openTemplates, pagesTreeRows, projectDrop, sourceLocaleOf, subtreeHeight } from "./pages-tree.js";
13
13
  import { Button, StatusBadge } from "../ui/primitives.js";
14
+ import { statusLabel } from "../ui/status-label.js";
14
15
  import { LocalePicker } from "../ui/LocaleSwitcher.js";
15
16
  import { useSortable } from "../ui/useSortable.js";
16
17
  import { BASE } from "./base.js";
@@ -153,7 +154,6 @@ export function PagesList({ ops, registry }) {
153
154
  return;
154
155
  void run(() => ops.pages.rename(row.id, { locale, title, slug }));
155
156
  };
156
- const move = (row, dir) => void run(() => ops.pages.move(row.id, { parentId: row.parentId, index: row.index + dir }));
157
157
  const destroy = (row) => {
158
158
  const scope = row.locales.length > 1 ? ` in every language (${row.locales.join(", ")})` : "";
159
159
  if (!window.confirm(`Delete "${row.title}"${scope}?`))
@@ -174,11 +174,7 @@ export function PagesList({ ops, registry }) {
174
174
  items.push({ label: "Add page here", run: () => addPageHere(row) });
175
175
  if (!row.fixed)
176
176
  items.push({ label: "Rename…", run: () => rename(row) });
177
- // Every row reorders among its siblings, fixed nodes and the home page included (root order is nothing code owns).
178
- if (row.index > 0)
179
- items.push({ label: "Move up", run: () => move(row, -1) });
180
- if (row.index < row.siblingCount - 1)
181
- 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.
182
178
  if (multilingual && !row.fixed && !row.isHome && row.locales.length > 1) {
183
179
  items.push({ label: `Remove from ${locale}`, run: () => removeLocale(row) });
184
180
  }
@@ -207,7 +203,7 @@ export function PagesList({ ops, registry }) {
207
203
  { label: "Title", width: "minmax(160px,1fr)" },
208
204
  { label: "Path", width: "minmax(140px,260px)" },
209
205
  { label: "Template", width: "minmax(100px,160px)" },
210
- { label: "Status", width: "110px" },
206
+ { label: "Status", width: "170px" },
211
207
  { label: "", width: "32px" },
212
208
  ], empty: pages ? "No pages yet." : "Loading…", sortable: { hook: sortable, canDrag: (i) => canDragRow(rows[i]) }, rows: rows.map((row, i) => {
213
209
  // An absent node is still a row (structure is shared) but reads as dimmed.
@@ -225,7 +221,7 @@ export function PagesList({ ops, registry }) {
225
221
  : _jsxs("span", { className: "sm-cell sm-cell--muted", children: ["not in ", locale, " \u2014 click to add"] }, "p"),
226
222
  _jsx("span", { className: "sm-cell sm-cell--muted", style: dim, children: row.templateTitle }, "k"),
227
223
  row.status
228
- ? _jsx(StatusBadge, { status: row.status }, "s")
224
+ ? _jsx(StatusBadge, { status: statusLabel(row.status) }, "s")
229
225
  : _jsx("span", { className: "sm-cell sm-cell--muted", children: "\u2014" }, "s"),
230
226
  _jsx("button", { type: "button", className: "sm-rowmenu", "aria-label": "Row menu", onClick: (e) => { e.stopPropagation(); setMenu({ row, x: e.clientX, y: e.clientY }); }, onKeyDown: (e) => e.stopPropagation(), children: "\u00B7\u00B7\u00B7" }, "m"),
231
227
  ],
@@ -9,8 +9,8 @@ export type EntryRow = {
9
9
  /** The locale row's updatedAt when present, else the node's. */
10
10
  updatedAt: number;
11
11
  };
12
- /** The title field's value in a locale: its own, else the default
13
- * locale's, else any locale's, else the id. Display only — a shared
12
+ /** The entry's title in the selected locale, else the default locale's, else any locale's;
13
+ * "Untitled" when every one is empty (spec 2026-09-14 §4). Display only — a shared
14
14
  * title field reads the same from every locale. */
15
15
  export declare function entryTitle(record: EntryRecord, meta: AdminCollection, registry: AdminRegistry, locale: string): string;
16
16
  /** The locale an "Add <locale>" copies from: the default when the record has it, else its first. */
@@ -1,6 +1,6 @@
1
1
  import { localeStatusOf } from "./pages-tree.js";
2
- /** The title field's value in a locale: its own, else the default
3
- * locale's, else any locale's, else the id. Display only — a shared
2
+ /** The entry's title in the selected locale, else the default locale's, else any locale's;
3
+ * "Untitled" when every one is empty (spec 2026-09-14 §4). Display only — a shared
4
4
  * title field reads the same from every locale. */
5
5
  export function entryTitle(record, meta, registry, locale) {
6
6
  const key = meta.titleField;
@@ -11,7 +11,7 @@ export function entryTitle(record, meta, registry, locale) {
11
11
  const value = key in row.fields ? row.fields[key] : record.fields[key];
12
12
  return typeof value === "string" && value.length > 0 ? value : undefined;
13
13
  };
14
- return from(locale) ?? from(registry.locales.default) ?? Object.keys(record.locales).map(from).find((t) => t !== undefined) ?? record.id;
14
+ return from(locale) ?? from(registry.locales.default) ?? Object.keys(record.locales).map(from).find((t) => t !== undefined) ?? "Untitled";
15
15
  }
16
16
  /** The locale an "Add <locale>" copies from: the default when the record has it, else its first. */
17
17
  export function entrySourceLocale(record, registry) {
@@ -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)
@@ -11,7 +11,7 @@ export function supabaseAdminAuth(input) {
11
11
  return {
12
12
  async verify(token) {
13
13
  const res = await auth.auth.getUser(token);
14
- return res.data.user ? { userId: res.data.user.id } : null;
14
+ return res.data.user ? { userId: res.data.user.id, email: res.data.user.email ?? null } : null;
15
15
  },
16
16
  async isEditor(userId) {
17
17
  const res = await input.db.from("editors").select("user_id").eq("user_id", userId).limit(1);
@@ -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;