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.
- package/README.md +238 -36
- package/dist/admin/auth.d.ts +7 -4
- package/dist/admin/auth.js +1 -1
- package/dist/admin/client-ops.js +3 -3
- package/dist/admin/custom-section.d.ts +10 -0
- package/dist/admin/custom-section.js +10 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +6 -6
- package/dist/admin/editor/EditorView.d.ts +2 -0
- package/dist/admin/editor/EditorView.js +322 -62
- package/dist/admin/editor/GlobalInspector.d.ts +24 -0
- package/dist/admin/editor/GlobalInspector.js +13 -0
- package/dist/admin/editor/InsertPicker.d.ts +6 -5
- package/dist/admin/editor/InsertPicker.js +6 -4
- package/dist/admin/editor/InspectorFields.d.ts +21 -0
- package/dist/admin/editor/InspectorFields.js +22 -0
- package/dist/admin/editor/Outline.d.ts +0 -1
- package/dist/admin/editor/Outline.js +3 -3
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/global-edit.d.ts +39 -0
- package/dist/admin/editor/global-edit.js +124 -0
- package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
- package/dist/admin/editor/useGlobalEdit.js +18 -0
- package/dist/admin/forms/FieldWidget.d.ts +14 -1
- package/dist/admin/forms/FieldWidget.js +38 -11
- package/dist/admin/forms/ListField.d.ts +9 -1
- package/dist/admin/forms/ListField.js +16 -8
- package/dist/admin/forms/RichtextElementCard.js +11 -5
- package/dist/admin/forms/RowsWidget.d.ts +13 -0
- package/dist/admin/forms/RowsWidget.js +26 -0
- package/dist/admin/forms/SpanBar.d.ts +5 -0
- package/dist/admin/forms/SpanBar.js +37 -0
- package/dist/admin/forms/richtext-insert.d.ts +2 -2
- package/dist/admin/forms/richtext-insert.js +3 -3
- package/dist/admin/forms/richtext-nodes.js +1 -1
- package/dist/admin/forms/tabs.d.ts +20 -8
- package/dist/admin/forms/tabs.js +19 -23
- package/dist/admin/forms/useRichtextEditor.js +8 -1
- package/dist/admin/global-items.d.ts +9 -0
- package/dist/admin/{shared-items.js → global-items.js} +29 -14
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/next/op-handler.js +4 -3
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +242 -56
- package/dist/admin/ops.d.ts +38 -6
- package/dist/admin/serialize.d.ts +18 -7
- package/dist/admin/serialize.js +27 -5
- package/dist/admin/shell/AdminApp.js +7 -7
- package/dist/admin/shell/EntriesList.js +19 -3
- package/dist/admin/shell/EntryForm.js +183 -77
- package/dist/admin/shell/GlobalFields.d.ts +24 -0
- package/dist/admin/shell/GlobalFields.js +19 -0
- package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
- package/dist/admin/shell/GlobalForm.js +258 -0
- package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
- package/dist/admin/shell/{SharedList.js → GlobalList.js} +12 -11
- package/dist/admin/shell/IconRail.js +14 -7
- package/dist/admin/shell/PagesList.js +4 -8
- package/dist/admin/shell/entries-list.d.ts +2 -2
- package/dist/admin/shell/entries-list.js +3 -3
- package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
- package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +10 -8
- package/dist/admin/tree-ops.js +28 -16
- package/dist/admin/ui/ActionMenu.d.ts +11 -0
- package/dist/admin/ui/ActionMenu.js +25 -0
- package/dist/admin/ui/HistoryModal.d.ts +23 -0
- package/dist/admin/ui/HistoryModal.js +40 -0
- package/dist/admin/ui/Modal.d.ts +5 -1
- package/dist/admin/ui/Modal.js +3 -3
- package/dist/admin/ui/PublishControl.d.ts +31 -0
- package/dist/admin/ui/PublishControl.js +24 -0
- package/dist/admin/ui/format.d.ts +4 -0
- package/dist/admin/ui/format.js +9 -0
- package/dist/admin/ui/primitives.d.ts +16 -1
- package/dist/admin/ui/primitives.js +20 -4
- package/dist/admin/ui/status-label.d.ts +10 -0
- package/dist/admin/ui/status-label.js +14 -0
- package/dist/admin/ui/theme.js +93 -13
- package/dist/admin/validate.d.ts +27 -2
- package/dist/admin/validate.js +47 -10
- package/dist/collections.d.ts +3 -9
- package/dist/collections.js +1 -2
- package/dist/conditions.d.ts +33 -0
- package/dist/conditions.js +79 -0
- package/dist/config.d.ts +26 -19
- package/dist/config.js +26 -25
- package/dist/css.d.ts +7 -0
- package/dist/css.js +57 -0
- package/dist/custom/CustomSection.d.ts +26 -0
- package/dist/custom/CustomSection.js +20 -0
- package/dist/custom/css.d.ts +3 -0
- package/dist/custom/css.js +28 -0
- package/dist/custom/index.d.ts +26 -0
- package/dist/custom/index.js +16 -0
- package/dist/custom/rows.d.ts +28 -0
- package/dist/custom/rows.js +129 -0
- package/dist/custom/schema.d.ts +32 -0
- package/dist/custom/schema.js +39 -0
- package/dist/entry-store.d.ts +42 -17
- package/dist/entry-store.js +107 -40
- package/dist/fields.d.ts +5 -2
- package/dist/fields.js +4 -2
- package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
- package/dist/{shared-id.js → global-id.js} +4 -4
- package/dist/global.d.ts +57 -0
- package/dist/global.js +51 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.js +10 -5
- package/dist/media.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/page-renderer.d.ts +4 -2
- package/dist/next/page-renderer.js +17 -13
- package/dist/next/queries.d.ts +6 -6
- package/dist/next/queries.js +7 -7
- package/dist/page.js +2 -2
- package/dist/pairing.d.ts +13 -7
- package/dist/pairing.js +4 -4
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +4 -4
- package/dist/render.d.ts +13 -1
- package/dist/render.js +10 -5
- package/dist/resolve.d.ts +11 -6
- package/dist/resolve.js +22 -12
- package/dist/revalidate.d.ts +4 -4
- package/dist/revalidate.js +4 -4
- package/dist/richtext-render.js +2 -2
- package/dist/richtext-walk.d.ts +5 -5
- package/dist/richtext-walk.js +4 -4
- package/dist/schema.d.ts +15 -10
- package/dist/schema.js +19 -7
- package/dist/section-wrapper.d.ts +18 -6
- package/dist/section-wrapper.js +24 -17
- package/dist/site.d.ts +17 -5
- package/dist/site.js +36 -12
- package/dist/sql-space.js +1 -1
- package/dist/sql.js +19 -16
- package/dist/store.d.ts +45 -13
- package/dist/store.js +88 -12
- package/dist/styles.d.ts +27 -0
- package/dist/styles.js +60 -0
- package/dist/supabase-entry-store.d.ts +11 -6
- package/dist/supabase-entry-store.js +115 -39
- package/dist/supabase-store.d.ts +13 -6
- package/dist/supabase-store.js +90 -20
- package/dist/zones.d.ts +9 -7
- package/dist/zones.js +5 -5
- package/package.json +1 -1
- package/dist/admin/shared-items.d.ts +0 -9
- package/dist/admin/shell/SharedForm.js +0 -134
- package/dist/shared.d.ts +0 -46
- 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
|
|
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
|
|
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 (
|
|
4
|
-
// UI 2026-09-09, the route
|
|
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.
|
|
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 {
|
|
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
|
|
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.
|
|
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 ?
|
|
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: "
|
|
54
|
-
], empty: items ? "No
|
|
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}/
|
|
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
|
|
12
|
-
// the route
|
|
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 {
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
//
|
|
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: "
|
|
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
|
|
13
|
-
*
|
|
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
|
|
3
|
-
*
|
|
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) ??
|
|
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
|
|
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
|
|
11
|
+
export declare function globalRows(items: {
|
|
12
12
|
name: string;
|
|
13
13
|
record: EntryRecord;
|
|
14
|
-
}[], registry: AdminRegistry, locale: string):
|
|
14
|
+
}[], registry: AdminRegistry, locale: string): GlobalRow[];
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { localeStatusOf } from "./pages-tree.js";
|
|
2
|
-
export function
|
|
2
|
+
export function globalRows(items, registry, locale) {
|
|
3
3
|
const byName = new Map(items.map((i) => [i.name, i.record]));
|
|
4
|
-
return registry.
|
|
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);
|
package/dist/admin/tree-ops.d.ts
CHANGED
|
@@ -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,
|
|
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" | "
|
|
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
|
|
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:
|
|
25
|
-
/** Also fills an element node (an `AdminElement` is a section shape plus contexts). */
|
|
26
|
-
export declare function nodeFromSample(section: Pick<AdminSection, "name" | "fields" | "
|
|
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
|
|
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[],
|
|
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;
|