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