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