smoodly 0.0.61 → 0.0.63
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/dist/admin/client-ops.js +1 -1
- package/dist/admin/drift.js +3 -1
- package/dist/admin/editor/CanvasOverlay.d.ts +4 -1
- package/dist/admin/editor/CanvasOverlay.js +3 -3
- package/dist/admin/editor/EditorView.js +65 -9
- package/dist/admin/editor/GlobalCanvas.d.ts +27 -0
- package/dist/admin/editor/GlobalCanvas.js +159 -0
- package/dist/admin/editor/InsertPicker.d.ts +8 -27
- package/dist/admin/editor/InsertPicker.js +56 -47
- package/dist/admin/editor/SamplePreview.d.ts +7 -0
- package/dist/admin/editor/SamplePreview.js +86 -0
- package/dist/admin/editor/SectionIcon.d.ts +4 -0
- package/dist/admin/editor/SectionIcon.js +8 -0
- package/dist/admin/editor/StructureView.d.ts +2 -2
- package/dist/admin/editor/StructureView.js +2 -1
- package/dist/admin/editor/canvas-address.d.ts +2 -0
- package/dist/admin/editor/canvas-address.js +3 -1
- package/dist/admin/editor/picker-rows.d.ts +33 -0
- package/dist/admin/editor/picker-rows.js +38 -0
- package/dist/admin/editor/sample-frame.d.ts +36 -0
- package/dist/admin/editor/sample-frame.js +58 -0
- package/dist/admin/editor/sample-picks.d.ts +34 -0
- package/dist/admin/editor/sample-picks.js +49 -0
- package/dist/admin/editor/view-memory.d.ts +5 -0
- package/dist/admin/editor/view-memory.js +19 -0
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/forms/MediaWidget.d.ts +1 -1
- package/dist/admin/forms/MediaWidget.js +10 -2
- package/dist/admin/ops.d.ts +5 -0
- package/dist/admin/serialize.d.ts +2 -0
- package/dist/admin/serialize.js +1 -0
- package/dist/admin/shell/AdminApp.js +5 -5
- package/dist/admin/shell/GlobalForm.js +80 -7
- package/dist/admin/shell/GlobalList.js +1 -1
- package/dist/admin/shell/{IconRail.d.ts → NavBar.d.ts} +3 -3
- package/dist/admin/shell/NavBar.js +120 -0
- package/dist/admin/shell/PagesList.js +4 -4
- package/dist/admin/shell/console-links.js +1 -1
- package/dist/admin/shell/global-canvas.d.ts +40 -0
- package/dist/admin/shell/global-canvas.js +60 -0
- package/dist/admin/shell/health-list.js +1 -1
- package/dist/admin/tree-ops.d.ts +11 -4
- package/dist/admin/tree-ops.js +28 -17
- package/dist/admin/ui/primitives.js +1 -1
- package/dist/admin/ui/theme.js +128 -101
- package/dist/admin/validate.js +3 -2
- package/dist/api-client.js +12 -2
- package/dist/astro/middleware.js +2 -0
- package/dist/astro/shell.d.ts +3 -0
- package/dist/astro/shell.js +8 -1
- package/dist/canvas-marker.d.ts +5 -0
- package/dist/canvas-marker.js +18 -6
- package/dist/conditions.js +5 -2
- package/dist/media.d.ts +15 -2
- package/dist/media.js +68 -10
- package/dist/next/page-renderer.js +12 -5
- package/dist/resolve.d.ts +6 -1
- package/dist/resolve.js +7 -7
- package/dist/sample-fill.d.ts +40 -0
- package/dist/sample-fill.js +145 -0
- package/dist/sample-tree.d.ts +30 -0
- package/dist/sample-tree.js +76 -0
- package/dist/site.d.ts +13 -0
- package/dist/site.js +10 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/collections/defining.md +2 -2
- package/docs/content-modeling/sections.md +19 -3
- package/docs/content-modeling/zones.md +3 -3
- package/docs/getting-started/deploying.md +1 -1
- package/docs/getting-started/working-together.md +1 -1
- package/docs/globals/defining.md +1 -1
- package/docs/globals/header-and-footer.md +2 -2
- package/docs/media/library.md +1 -1
- package/package.json +1 -1
- package/dist/admin/shell/IconRail.js +0 -109
package/dist/admin/client-ops.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const GROUPS = {
|
|
2
2
|
pages: ["list", "get", "create", "saveDraft", "publish", "unpublish", "discardDraft", "delete", "rename", "move", "setTemplate", "addLocale", "removeLocale", "listVersions", "restore"],
|
|
3
|
-
entries: ["list", "page", "distinct", "get", "create", "save", "setStatus", "discardDraft", "delete", "move", "addLocale", "removeLocale", "listVersions", "getVersion", "restore"],
|
|
3
|
+
entries: ["list", "page", "distinct", "samplePicks", "get", "create", "save", "setStatus", "discardDraft", "delete", "move", "addLocale", "removeLocale", "listVersions", "getVersion", "restore"],
|
|
4
4
|
globals: ["list", "get", "save", "setStatus", "discardDraft", "listVersions", "getVersion", "restore"],
|
|
5
5
|
assets: ["list", "get", "update", "delete", "usage"],
|
|
6
6
|
members: ["list", "me"],
|
package/dist/admin/drift.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { optionValues } from "../fields.js";
|
|
2
|
+
import { isImagePlaceholder } from "../media.js";
|
|
2
3
|
import { isHidden } from "../conditions.js";
|
|
3
4
|
import { allowedOf } from "../toolset.js";
|
|
4
5
|
import { CUSTOM_NODE_TYPE } from "../custom/schema.js";
|
|
@@ -38,7 +39,8 @@ export function fits(d, value) {
|
|
|
38
39
|
const allowed = optionValues(d.options);
|
|
39
40
|
return Array.isArray(value) && value.every((v) => typeof v === "string" && allowed.includes(v));
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
+
// A fresh node's image placeholder is a value of an image field (spec 2026-11-14 filled samples), of no other.
|
|
43
|
+
case "image": return isObject(value) && (typeof value.asset === "string" || isImagePlaceholder(value));
|
|
42
44
|
case "video":
|
|
43
45
|
case "file": return isObject(value) && typeof value.asset === "string";
|
|
44
46
|
case "ref": return typeof value === "string";
|
|
@@ -10,7 +10,7 @@ export type OverlaySelection = {
|
|
|
10
10
|
/** Rendered by the site's layout, not placed in a zone: no tree gestures (spec 2026-09-13 §2). */
|
|
11
11
|
layout?: boolean;
|
|
12
12
|
};
|
|
13
|
-
export declare function CanvasOverlay({ emptyZones, hoverRect, hoverGlobal, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onCopy, onRemove, }: {
|
|
13
|
+
export declare function CanvasOverlay({ emptyZones, hoverRect, hoverGlobal, selection, plusButtons, fieldHover, editing, dimOutside, onPlus, onMove, onDuplicate, onCopy, onRemove, }: {
|
|
14
14
|
emptyZones: ZoneRect[];
|
|
15
15
|
hoverRect: BridgeRect | undefined;
|
|
16
16
|
/** The hovered block is a global item: the ring takes the global colour. */
|
|
@@ -21,6 +21,9 @@ export declare function CanvasOverlay({ emptyZones, hoverRect, hoverGlobal, sele
|
|
|
21
21
|
fieldHover?: FieldRect;
|
|
22
22
|
/** The field with an active inline session. */
|
|
23
23
|
editing?: FieldRect;
|
|
24
|
+
/** The page above and below the selection is dimmed: the selection is the one subject (a global's own canvas,
|
|
25
|
+
* spec 2026-11-14). A section is a full-width band, so two bands cover the rest. */
|
|
26
|
+
dimOutside?: boolean;
|
|
24
27
|
/** A "+" was clicked, at these viewport coordinates. */
|
|
25
28
|
onPlus(button: PlusButton, clientX: number, clientY: number): void;
|
|
26
29
|
onMove(id: string, dir: -1 | 1): void;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
// The overlay layer above the canvas iframe — every pixel of chrome is
|
|
4
4
|
// ours: empty-zone placeholders, the hover and selection outlines, the
|
|
5
5
|
// selected block's tag and toolbar, and a "+" per insertable boundary.
|
|
@@ -9,9 +9,9 @@ import { useState } from "react";
|
|
|
9
9
|
import { cx, IconButton } from "../ui/primitives.js";
|
|
10
10
|
/** Pixels above a block the riding pill needs (half its height); less and it moves inside. */
|
|
11
11
|
const TAG_CLEARANCE = 14;
|
|
12
|
-
export function CanvasOverlay({ emptyZones, hoverRect, hoverGlobal, selection, plusButtons, fieldHover, editing, onPlus, onMove, onDuplicate, onCopy, onRemove, }) {
|
|
12
|
+
export function CanvasOverlay({ emptyZones, hoverRect, hoverGlobal, selection, plusButtons, fieldHover, editing, dimOutside, onPlus, onMove, onDuplicate, onCopy, onRemove, }) {
|
|
13
13
|
const [hoverPlus, setHoverPlus] = useState(null);
|
|
14
|
-
return (_jsxs("div", { style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: [emptyZones.map((z) => (_jsxs("div", { className: "sm-dashed", style: {
|
|
14
|
+
return (_jsxs("div", { style: { position: "absolute", inset: 0, pointerEvents: "none" }, children: [dimOutside && selection && (_jsxs(_Fragment, { children: [_jsx("div", { "data-dim": "above", className: "sm-dim", style: { position: "absolute", left: 0, right: 0, top: 0, height: Math.max(0, selection.rect.y) } }), _jsx("div", { "data-dim": "below", className: "sm-dim", style: { position: "absolute", left: 0, right: 0, top: selection.rect.y + selection.rect.h, bottom: 0 } })] })), emptyZones.map((z) => (_jsxs("div", { className: "sm-dashed", style: {
|
|
15
15
|
position: "absolute", left: z.x + 12, top: z.y, width: Math.max(0, z.w - 24),
|
|
16
16
|
height: Math.max(56, z.h),
|
|
17
17
|
}, children: [z.zone, " \u00B7 empty"] }, `empty-${z.zone}`))), hoverRect && (_jsx("div", { style: {
|
|
@@ -44,13 +44,14 @@ import { enableDraft } from "./preview-grant.js";
|
|
|
44
44
|
import { CanvasOverlay } from "./CanvasOverlay.js";
|
|
45
45
|
import { InlineRichtext } from "./InlineRichtext.js";
|
|
46
46
|
import { InsertPicker } from "./InsertPicker.js";
|
|
47
|
+
import { insertStands, loadPicks, picksCache } from "./sample-picks.js";
|
|
47
48
|
import { customNode, customNodeLabel, customSection } from "../custom-section.js";
|
|
48
49
|
import { CUSTOM_NODE_TYPE } from "../../custom/index.js";
|
|
49
50
|
import { NoticePane } from "./NoticePane.js";
|
|
50
51
|
import { Outline } from "./Outline.js";
|
|
51
52
|
import { StructureView } from "./StructureView.js";
|
|
52
53
|
import { cardPreview } from "./card-preview.js";
|
|
53
|
-
import { rememberView, rememberedView } from "./view-memory.js";
|
|
54
|
+
import { rememberFilled, rememberView, rememberedFilled, rememberedView } from "./view-memory.js";
|
|
54
55
|
import { createHistory } from "./history.js";
|
|
55
56
|
import { takesUndoKeys, undoKeyOf } from "./undo-keys.js";
|
|
56
57
|
import { canPaste, clipOf, clipboardSpace, pastedNode, readClip, subscribeClip, writeClip } from "./section-clipboard.js";
|
|
@@ -123,6 +124,7 @@ export function EditorView({ ops, registry, id }) {
|
|
|
123
124
|
const inspectorRef = useRef(null);
|
|
124
125
|
// The session hook is created after `mutate`; the autosave and the bridge reach it through this ref.
|
|
125
126
|
const inlineRef = useRef(null);
|
|
127
|
+
/** The open insert picker: where the pick lands. */
|
|
126
128
|
const [picker, setPicker] = useState(null);
|
|
127
129
|
const [tab, setTab] = useState("Content");
|
|
128
130
|
/** Canvas or Structure (spec 2026-10-19 editor at launch §1): one tree, two renderers; remembered per browser. */
|
|
@@ -162,6 +164,17 @@ export function EditorView({ ops, registry, id }) {
|
|
|
162
164
|
const scrollerRef = useRef(null);
|
|
163
165
|
const treeRef = useRef(null);
|
|
164
166
|
const registryRef = useRef(registry);
|
|
167
|
+
// A fresh node's reference picks and whether the service takes an image placeholder (spec 2026-11-14 filled
|
|
168
|
+
// samples): asked once per locale for this editor's life, on the first picker open, insert or locked-zone fill.
|
|
169
|
+
const picksRef = useRef(null);
|
|
170
|
+
// Counts the draft's loads and the editor's leaving: an insert that waited across one is dropped.
|
|
171
|
+
const loadCountRef = useRef(0);
|
|
172
|
+
useEffect(() => () => { loadCountRef.current++; }, []);
|
|
173
|
+
const picksFor = useCallback((forLocale) => {
|
|
174
|
+
if (picksRef.current?.ops !== ops)
|
|
175
|
+
picksRef.current = { ops, cache: picksCache() };
|
|
176
|
+
return picksRef.current.cache.get(forLocale, () => loadPicks(ops, registryRef.current.sections, forLocale));
|
|
177
|
+
}, [ops]);
|
|
165
178
|
registryRef.current = registry;
|
|
166
179
|
// The tree in memory belongs to the locale it was loaded for; a switch
|
|
167
180
|
// flushes first (switchLocale), so the ref and the tree always agree.
|
|
@@ -253,6 +266,7 @@ export function EditorView({ ops, registry, id }) {
|
|
|
253
266
|
let cancelled = false;
|
|
254
267
|
(async () => {
|
|
255
268
|
// A new locale (or a reload) is a new canvas: nothing from the previous one may survive.
|
|
269
|
+
loadCountRef.current++;
|
|
256
270
|
setReady(false);
|
|
257
271
|
setTree(null);
|
|
258
272
|
treeRef.current = null;
|
|
@@ -318,7 +332,16 @@ export function EditorView({ ops, registry, id }) {
|
|
|
318
332
|
// effect: a load runs for a new page, locale or reload, nothing else.
|
|
319
333
|
const reg = registryRef.current;
|
|
320
334
|
const template = reg.pages.find((p) => p.name === result.data.record.template);
|
|
321
|
-
|
|
335
|
+
// A zone that will be filled waits for the picks, so its references and its placeholder are the picker's; any
|
|
336
|
+
// other load asks nothing.
|
|
337
|
+
const stored = result.data.draft;
|
|
338
|
+
let draft = stored;
|
|
339
|
+
if (fillLockedZones(stored, template?.zones ?? {}, reg.sections) !== stored) {
|
|
340
|
+
const fresh = await picksFor(locale);
|
|
341
|
+
if (cancelled)
|
|
342
|
+
return;
|
|
343
|
+
draft = fillLockedZones(stored, template?.zones ?? {}, reg.sections, {}, fresh);
|
|
344
|
+
}
|
|
322
345
|
treeLocaleRef.current = locale;
|
|
323
346
|
setTree(draft);
|
|
324
347
|
treeRef.current = draft;
|
|
@@ -655,7 +678,8 @@ export function EditorView({ ops, registry, id }) {
|
|
|
655
678
|
if (!tree || !globalIds || !record)
|
|
656
679
|
return;
|
|
657
680
|
const template = registryRef.current.pages.find((p) => p.name === record.template);
|
|
658
|
-
|
|
681
|
+
// The answer in hand, if any: the load already waited for it where a section fills a zone.
|
|
682
|
+
const filled = fillLockedZones(tree, template?.zones ?? {}, registryRef.current.sections, globalIds, picksRef.current?.cache.known(locale));
|
|
659
683
|
// Saved like the fill on load, and like it not an edit of the editor's (editedRef).
|
|
660
684
|
if (filled !== tree) {
|
|
661
685
|
setTree(filled);
|
|
@@ -684,8 +708,40 @@ export function EditorView({ ops, registry, id }) {
|
|
|
684
708
|
const fs = findingsAt.get(nodeId);
|
|
685
709
|
return !fs ? undefined : fs.some(isBroken) ? "broken" : "orphan";
|
|
686
710
|
};
|
|
687
|
-
const
|
|
688
|
-
|
|
711
|
+
const pickerOpen = picker !== null;
|
|
712
|
+
useEffect(() => { if (pickerOpen)
|
|
713
|
+
void picksFor(locale); }, [pickerOpen, locale, picksFor]);
|
|
714
|
+
// An insert waits for the picks (asked when the picker opened, so they are usually there). One at a time: a second
|
|
715
|
+
// click while the first waits inserts nothing, and an answer that lands on another locale's canvas is dropped.
|
|
716
|
+
const insertingRef = useRef(false);
|
|
717
|
+
const pickerRef = useRef(picker);
|
|
718
|
+
pickerRef.current = picker;
|
|
719
|
+
const idRef = useRef(id);
|
|
720
|
+
idRef.current = id;
|
|
721
|
+
/** Where the editor stands now, as an insert's answer is checked against (sample-picks.ts insertStands). */
|
|
722
|
+
const standing = (fromPicker) => ({ page: idRef.current, locale: treeLocaleRef.current, load: loadCountRef.current, picker: fromPicker ? pickerRef.current : null });
|
|
723
|
+
const [filled, setFilled] = useState(() => rememberedFilled());
|
|
724
|
+
const filledRef = useRef(filled);
|
|
725
|
+
filledRef.current = filled;
|
|
726
|
+
const changeFilled = (next) => { setFilled(next); rememberFilled(next); };
|
|
727
|
+
const doInsert = async (zone, index, section, fromPicker = true) => {
|
|
728
|
+
if (insertingRef.current)
|
|
729
|
+
return;
|
|
730
|
+
insertingRef.current = true;
|
|
731
|
+
const asked = standing(fromPicker);
|
|
732
|
+
let fresh;
|
|
733
|
+
try {
|
|
734
|
+
fresh = await picksFor(asked.locale);
|
|
735
|
+
}
|
|
736
|
+
finally {
|
|
737
|
+
insertingRef.current = false;
|
|
738
|
+
}
|
|
739
|
+
// The editor moved on while the answer was on its way: nothing is inserted behind its back.
|
|
740
|
+
if (!treeRef.current || !insertStands(asked, standing(fromPicker)))
|
|
741
|
+
return;
|
|
742
|
+
// The switch is read when the node is built: off, neither the sample nor generated values (spec 2026-11-14 filled
|
|
743
|
+
// samples). It is the picker's: a gesture with no modal (the outline's "add first") always inserts filled.
|
|
744
|
+
const node = nodeFromSample(section, newNodeId(section.name), { picks: fresh.picks, images: fresh.images, empty: fromPicker && !filledRef.current });
|
|
689
745
|
mutate((t) => insertNode(t, zone, index, node), { focus: node.id });
|
|
690
746
|
setPicker(null);
|
|
691
747
|
select(node.id);
|
|
@@ -1068,7 +1124,7 @@ export function EditorView({ ops, registry, id }) {
|
|
|
1068
1124
|
return {
|
|
1069
1125
|
...n,
|
|
1070
1126
|
label: global ? global.title : n.label,
|
|
1071
|
-
icon: section
|
|
1127
|
+
...(section?.icon ? { icon: section.icon } : global ? { icon: "⛓" } : {}),
|
|
1072
1128
|
preview: global ? { text: null, image: null } : cardPreview(n.node, section),
|
|
1073
1129
|
global: global !== null,
|
|
1074
1130
|
};
|
|
@@ -1093,7 +1149,7 @@ export function EditorView({ ops, registry, id }) {
|
|
|
1093
1149
|
] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error" || saveState === "refused") && _jsx(SaveIndicator, { state: saveState, pending: dirty }), ready && tree && (_jsxs(_Fragment, { children: [_jsx(IconButton, { label: "Undo", disabled: !canUndo || !!stale, onClick: doUndo, children: _jsx(Undo2, { size: 16, strokeWidth: 1.5, "aria-hidden": "true" }) }), _jsx(IconButton, { label: "Redo", disabled: !canRedo || !!stale, onClick: doRedo, children: _jsx(Redo2, { size: 16, strokeWidth: 1.5, "aria-hidden": "true" }) })] })), ready && tree && (_jsx(Segmented, { options: ["canvas", "structure"], value: view, onChange: switchView, className: "sm-seg--view", label: (o) => o === "canvas"
|
|
1094
1150
|
? _jsx(AppWindow, { size: 14, strokeWidth: 1.5, "aria-hidden": "true" })
|
|
1095
1151
|
: _jsx(Rows3, { size: 14, strokeWidth: 1.5, "aria-hidden": "true" }), option: (o) => ({ title: o === "canvas" ? "Canvas" : "Structure", "aria-label": o === "canvas" ? "Canvas" : "Structure" }) })), ready && (_jsx(LinkButton, { icon: true, href: siteUrl, target: "_blank", rel: "noreferrer", "aria-label": "View live", title: "View live \u2014 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: _jsx(Eye, { size: 16, strokeWidth: 1.5, "aria-hidden": "true" }) })), _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(); } })] }) }), target && _jsx(PresenceLine, { names: editorsOf(others, target), noun: "page" }), stale && _jsx(StaleBanner, { info: stale, onReload: loadTheirs }), _jsx(DriftNotice, { draft: findings, live: liveFindings }), 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: versionAddress(siteUrl, versionId) }) })), _jsxs("div", { className: "sm-editor", inert: stale ? true : undefined, children: [_jsxs("div", { className: "sm-canvas", children: [(banner || diagnosis || crashedLines.length > 0) && (_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 ?? [] })] })), diagnosis && _jsx(CanvasWarning, { cause: diagnosis.cause, path: diagnosis.path }), crashedLines.length > 0 && (_jsx(BannerBox, { tone: "warn", children: crashedLines.map((line, i) => _jsx("p", { children: line }, i)) }))] })), _jsxs("div", { className: cx("sm-canvas__scroll", view === "structure" && "sm-canvas__scroll--structure"), ref: scrollerRef, onClick: (e) => { if (e.target === e.currentTarget)
|
|
1096
|
-
setSelectedId(null); }, children: [view === "structure" && !(ready && tree) && _jsx("div", { className: "sm-canvas__loading", children: "Loading canvas\u2026" }), view === "structure" && ready && tree && (_jsx(StructureView, { zones: structureZones, selectedId: selectedId, onSelect: select, onMoveTo: doMoveTo, onRemove: doRemove, onClearZone: clearZoneAsked, cardActions: cardActions, zoneActions: zoneActions, onAdd: (zone, index
|
|
1152
|
+
setSelectedId(null); }, children: [view === "structure" && !(ready && tree) && _jsx("div", { className: "sm-canvas__loading", children: "Loading canvas\u2026" }), view === "structure" && ready && tree && (_jsx(StructureView, { zones: structureZones, selectedId: selectedId, onSelect: select, onMoveTo: doMoveTo, onRemove: doRemove, onClearZone: clearZoneAsked, cardActions: cardActions, zoneActions: zoneActions, onAdd: (zone, index) => setPicker({ zone, index }) })), _jsxs("div", { className: cx("sm-page", view === "structure" && "sm-canvas__hidden"), style: { overflow: "hidden" }, children: [iframeSrc ? (_jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Page canvas", onLoad: () => { onFrameLoad(); setFrameLoads((n) => n + 1); }, 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) => setPicker({ zone: p.zone, index: p.index }), onMove: doMove, onDuplicate: doDuplicate, onCopy: doCopy, onRemove: doRemove }), inline.richtext && (() => {
|
|
1097
1153
|
const { session } = inline.richtext;
|
|
1098
1154
|
const item = globalItemOf(session.node);
|
|
1099
1155
|
const node = !item && tree ? findNode(tree, session.node) : null;
|
|
@@ -1135,8 +1191,8 @@ export function EditorView({ ops, registry, id }) {
|
|
|
1135
1191
|
}, onPublish: () => { void doPublishGlobal(selectedItem.name); }, onDiscard: () => { void doDiscardGlobal(selectedItem.name); }, onUnpublish: () => { void doUnpublishGlobal(selectedItem.name); } }))] }))] }), view === "canvas" && (_jsx("div", { className: "sm-card", children: _jsx(Outline, { selectedId: selectedId, onSelect: select, onMoveTo: doMoveTo, onRemove: doRemove, zones: outlineZones, onClearZone: clearZoneAsked, cardActions: cardActions, onAddFirst: (zone) => {
|
|
1136
1192
|
const first = allowedSections(policyOf(zone), registry.sections)[0];
|
|
1137
1193
|
if (first)
|
|
1138
|
-
doInsert(zone, 0, first);
|
|
1139
|
-
} }) }))] })] }), picker && (_jsx(InsertPicker, {
|
|
1194
|
+
void doInsert(zone, 0, first, false);
|
|
1195
|
+
} }) }))] })] }), picker && (_jsx(InsertPicker, { zone: picker.zone, siteUrl: siteUrl, policy: policyOf(picker.zone), registry: registry, globalIds: globalIds ?? {}, paste: clip ? { title: clip.title, check: pasteCheck(picker.zone), onPaste: () => doPaste(picker.zone, picker.index) } : undefined, filled: filled, onFilledChange: changeFilled, onInsert: (section) => { void doInsert(picker.zone, picker.index, section); }, onInsertGlobal: (item) => doInsertGlobal(picker.zone, picker.index, item), onInsertCustom: () => doInsertCustom(picker.zone, picker.index), onClose: () => setPicker(null) }))] }));
|
|
1140
1196
|
}
|
|
1141
1197
|
/** The selected node's ref field (if any) resolves to its target
|
|
1142
1198
|
* collection's loaded options — the EntryForm shape, section-scoped. */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { type ReactElement, type Ref } from "react";
|
|
2
|
+
import type { AdminOps } from "../ops.ts";
|
|
3
|
+
import type { AdminGlobal } from "../serialize.ts";
|
|
4
|
+
export type GlobalCanvasHandle = {
|
|
5
|
+
/** A save of the item landed: the frame renders again (after an open inline session ends, as in the page editor). */
|
|
6
|
+
refresh(): void;
|
|
7
|
+
/** End an open inline session (the stale freeze, a reload of the item). */
|
|
8
|
+
endInline(): void;
|
|
9
|
+
/** A text value typed in the inspector, written into its marked element at once (the refresh follows the save). */
|
|
10
|
+
writeText(path: string, value: string): void;
|
|
11
|
+
};
|
|
12
|
+
export declare function GlobalCanvas({ ops, item, pageId, locale, fields, onPatch, onMissing, onInlinePath, ref }: {
|
|
13
|
+
ops: Pick<AdminOps, "pages" | "preview">;
|
|
14
|
+
/** A visual item. */
|
|
15
|
+
item: AdminGlobal;
|
|
16
|
+
pageId: string;
|
|
17
|
+
locale: string;
|
|
18
|
+
/** The form's draft fields: the canvas richtext editor reads its value here. */
|
|
19
|
+
fields: Record<string, unknown>;
|
|
20
|
+
/** A value typed in the frame, by dotted path inside the item's fields. */
|
|
21
|
+
onPatch(path: string, value: unknown): void;
|
|
22
|
+
/** The frame settled without drawing the item, or the page could not be loaded. Once per mount. */
|
|
23
|
+
onMissing(): void;
|
|
24
|
+
/** The content field with an open inline session, or null. */
|
|
25
|
+
onInlinePath?(path: string | null): void;
|
|
26
|
+
ref?: Ref<GlobalCanvasHandle>;
|
|
27
|
+
}): ReactElement;
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The canvas of one global item (spec 2026-11-14 sections and the global canvas §2): a real page of the site
|
|
4
|
+
// that draws the item, in the page editor's frame, with the item selected for good and the rest of the page
|
|
5
|
+
// dimmed. The site has no address that draws a global alone, and the admin holds none of the site's
|
|
6
|
+
// components, so the picture is always a page. Composed from the page editor's pieces — the bridge, the
|
|
7
|
+
// overlay, the inline session — and it owns nothing of the page: it reads the page's draft tree to find a
|
|
8
|
+
// placement and never writes a page. The item's values, its save and its publish stay with the form
|
|
9
|
+
// (shell/GlobalForm.tsx); this reports what was typed in the frame and is told when a save landed.
|
|
10
|
+
import { useEffect, useImperativeHandle, useRef, useState } from "react";
|
|
11
|
+
import { descriptorAt, getAtPath } from "../field-paths.js";
|
|
12
|
+
import { MISS_QUIET_MS, globalNodeIn, missing, settled } from "../shell/global-canvas.js";
|
|
13
|
+
import { canvasAddress, previewUrl } from "./canvas-address.js";
|
|
14
|
+
import { CanvasOverlay } from "./CanvasOverlay.js";
|
|
15
|
+
import { CanvasWarning } from "./CanvasWarning.js";
|
|
16
|
+
import { InlineRichtext } from "./InlineRichtext.js";
|
|
17
|
+
import { enableDraft } from "./preview-grant.js";
|
|
18
|
+
import { useCanvasBridge } from "./useCanvasBridge.js";
|
|
19
|
+
import { useCanvasDiagnosis } from "./useCanvasDiagnosis.js";
|
|
20
|
+
import { useInlineSession } from "./useInlineSession.js";
|
|
21
|
+
/** The gap kept above the item when the frame first scrolls to it. */
|
|
22
|
+
const REVEAL_GAP = 24;
|
|
23
|
+
const noop = () => { };
|
|
24
|
+
export function GlobalCanvas({ ops, item, pageId, locale, fields, onPatch, onMissing, onInlinePath, ref }) {
|
|
25
|
+
const iframeRef = useRef(null);
|
|
26
|
+
const scrollerRef = useRef(null);
|
|
27
|
+
const [page, setPage] = useState(null);
|
|
28
|
+
const [hoverField, setHoverField] = useState(null);
|
|
29
|
+
const [seen, setSeen] = useState({ rendered: false, rectsAfterRendered: false });
|
|
30
|
+
const inlineRef = useRef(null);
|
|
31
|
+
const nodeRef = useRef(null);
|
|
32
|
+
const missed = useRef(false);
|
|
33
|
+
const followRef = useRef(true);
|
|
34
|
+
const latest = useRef({ onMissing, onPatch, onInlinePath });
|
|
35
|
+
latest.current = { onMissing, onPatch, onInlinePath };
|
|
36
|
+
const miss = () => { if (!missed.current) {
|
|
37
|
+
missed.current = true;
|
|
38
|
+
latest.current.onMissing();
|
|
39
|
+
} };
|
|
40
|
+
// Draft mode FIRST, then the page; only then the frame — a frame loaded before the cookie exists renders the
|
|
41
|
+
// published page (EditorView's order). A refusal, a folder or a page with no address here is "missing".
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
let live = true;
|
|
44
|
+
(async () => {
|
|
45
|
+
const draft = await enableDraft(() => ops.preview.enable(), previewUrl());
|
|
46
|
+
if (!live)
|
|
47
|
+
return;
|
|
48
|
+
if (!draft.ok) {
|
|
49
|
+
miss();
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
let got;
|
|
53
|
+
try {
|
|
54
|
+
got = await ops.pages.get(pageId, locale);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
got = null;
|
|
58
|
+
}
|
|
59
|
+
if (!live)
|
|
60
|
+
return;
|
|
61
|
+
const url = got?.ok ? got.data.urls[locale] : undefined;
|
|
62
|
+
if (!got?.ok || !got.data.draft || !url) {
|
|
63
|
+
miss();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
setPage({ src: canvasAddress(url), tree: got.data.draft });
|
|
67
|
+
})();
|
|
68
|
+
return () => { live = false; };
|
|
69
|
+
// The form keys this component by page and locale: one mount, one page.
|
|
70
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
71
|
+
}, []);
|
|
72
|
+
const iframeSrc = page?.src ?? null;
|
|
73
|
+
const { rects, fields: fieldRects, docH, geometry, postToCanvas, onFrameLoad } = useCanvasBridge({
|
|
74
|
+
iframeRef, iframeSrc,
|
|
75
|
+
// Only the item answers a click: with a marked field under it, the inline session; anywhere else, nothing.
|
|
76
|
+
onSelect: (id, path, x, y) => {
|
|
77
|
+
if (id && id === nodeRef.current)
|
|
78
|
+
followRef.current = false;
|
|
79
|
+
if (!id || id !== nodeRef.current || !path)
|
|
80
|
+
return;
|
|
81
|
+
const descriptor = descriptorAt(item, path);
|
|
82
|
+
const kind = descriptor?.type === "text" ? "text" : descriptor?.type === "richtext" ? "richtext" : null;
|
|
83
|
+
const inline = inlineRef.current;
|
|
84
|
+
if (!kind || !inline)
|
|
85
|
+
return;
|
|
86
|
+
if (inline.session && inline.session.path === path)
|
|
87
|
+
return;
|
|
88
|
+
inline.start({ node: id, path, kind }, { x, y });
|
|
89
|
+
},
|
|
90
|
+
onHover: (id, path) => setHoverField(id && id === nodeRef.current ? path : null),
|
|
91
|
+
onRendered: () => {
|
|
92
|
+
inlineRef.current?.onRendered();
|
|
93
|
+
setSeen((s) => (s.rendered ? s : { rendered: true, rectsAfterRendered: false }));
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
// A geometry report after the first committed render: only then may "no node" be believed (global-canvas `settled`).
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
setSeen((s) => (s.rendered && !s.rectsAfterRendered ? { rendered: true, rectsAfterRendered: true } : s));
|
|
99
|
+
}, [rects]);
|
|
100
|
+
const diagnosis = useCanvasDiagnosis(iframeSrc, geometry);
|
|
101
|
+
const nodeId = page ? globalNodeIn(item.name, rects, page.tree) : null;
|
|
102
|
+
nodeRef.current = nodeId;
|
|
103
|
+
const rect = nodeId ? rects.find((r) => r.id === nodeId) : undefined;
|
|
104
|
+
// "This page does not draw the item" is believed only once the frame's reports have gone quiet without it
|
|
105
|
+
// (global-canvas `missing`): every report restarts the wait, and the item arriving cancels it.
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
if (!settled(seen) || nodeId !== null)
|
|
108
|
+
return;
|
|
109
|
+
const timer = setTimeout(() => { if (missing(seen, nodeRef.current, MISS_QUIET_MS))
|
|
110
|
+
miss(); }, MISS_QUIET_MS);
|
|
111
|
+
return () => clearTimeout(timer);
|
|
112
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
113
|
+
}, [seen, nodeId, rects]);
|
|
114
|
+
// The frame opens on the item and stays on it while the page settles: a footer's place moves as the images
|
|
115
|
+
// above it load and the document grows. The first scroll that is not this one — the editor's — ends it, and so
|
|
116
|
+
// does a click on the item: from then on the place under the editor is theirs.
|
|
117
|
+
const follow = followRef;
|
|
118
|
+
const lastTop = useRef(null);
|
|
119
|
+
const rectY = rect?.y;
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const scroller = scrollerRef.current;
|
|
122
|
+
if (!follow.current || rectY === undefined || !scroller)
|
|
123
|
+
return;
|
|
124
|
+
scroller.scrollTop = Math.max(0, rectY - REVEAL_GAP);
|
|
125
|
+
lastTop.current = scroller.scrollTop;
|
|
126
|
+
}, [rectY, docH]);
|
|
127
|
+
const onScroll = () => {
|
|
128
|
+
const scroller = scrollerRef.current;
|
|
129
|
+
if (scroller && lastTop.current !== null && Math.abs(scroller.scrollTop - lastTop.current) > 2)
|
|
130
|
+
follow.current = false;
|
|
131
|
+
};
|
|
132
|
+
const inline = useInlineSession({
|
|
133
|
+
iframeRef, postToCanvas,
|
|
134
|
+
patch: (node, path, value) => {
|
|
135
|
+
if (node !== nodeRef.current)
|
|
136
|
+
return;
|
|
137
|
+
latest.current.onPatch(path, value);
|
|
138
|
+
// A second element marked with the same field follows the one being typed in.
|
|
139
|
+
if (typeof value === "string")
|
|
140
|
+
inlineRef.current?.writeText(node, path, value);
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
inlineRef.current = inline;
|
|
144
|
+
const sessionPath = inline.session?.path ?? null;
|
|
145
|
+
useEffect(() => { latest.current.onInlinePath?.(sessionPath); }, [sessionPath]);
|
|
146
|
+
useImperativeHandle(ref, () => ({
|
|
147
|
+
refresh: () => inlineRef.current?.onSaved(),
|
|
148
|
+
endInline: () => inlineRef.current?.end(),
|
|
149
|
+
writeText: (path, value) => { if (nodeRef.current)
|
|
150
|
+
inlineRef.current?.writeText(nodeRef.current, path, value); },
|
|
151
|
+
}), []);
|
|
152
|
+
const fieldRectOf = (path) => (path && nodeId ? fieldRects.find((r) => r.node === nodeId && r.path === path) : undefined);
|
|
153
|
+
const selection = nodeId && rect
|
|
154
|
+
// `layout`: no tree gestures, whichever way the page draws the item — nothing here moves or removes a page's node.
|
|
155
|
+
? { node: { id: nodeId, type: item.section ?? item.name, fields: {} }, rect, title: item.title, global: true, locked: false, layout: true }
|
|
156
|
+
: null;
|
|
157
|
+
const pageWidth = rects.reduce((w, r) => Math.max(w, r.x + r.w), 320);
|
|
158
|
+
return (_jsxs("div", { className: "sm-canvas", children: [diagnosis && (_jsx("div", { style: { display: "flex", flexDirection: "column", gap: 8, padding: "0 20px 10px" }, children: _jsx(CanvasWarning, { cause: diagnosis.cause, path: diagnosis.path }) })), _jsx("div", { className: "sm-canvas__scroll", ref: scrollerRef, onScroll: onScroll, children: _jsxs("div", { className: "sm-page", style: { overflow: "hidden" }, children: [iframeSrc ? (_jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Global canvas", onLoad: onFrameLoad, style: { width: "100%", height: docH, border: 0, display: "block" } })) : (_jsx("div", { className: "sm-canvas__loading", children: "Loading canvas\u2026" })), _jsx(CanvasOverlay, { emptyZones: [], hoverRect: undefined, selection: selection, plusButtons: [], dimOutside: true, fieldHover: fieldRectOf(hoverField), editing: fieldRectOf(sessionPath), onPlus: noop, onMove: noop, onDuplicate: noop, onCopy: noop, onRemove: noop }), inline.richtext && (_jsx(InlineRichtext, { mount: inline.richtext, pageWidth: pageWidth, value: getAtPath(fields, inline.richtext.session.path), toolset: descriptorAt(item, inline.richtext.session.path)?.toolset, onChange: (doc) => { inline.markChanged(); onPatch(inline.richtext.session.path, doc); }, onEscape: inline.end }))] }) })] }));
|
|
159
|
+
}
|
|
@@ -1,38 +1,19 @@
|
|
|
1
1
|
import type { ZonePolicy } from "../../zones.ts";
|
|
2
2
|
import type { AdminRegistry, AdminSection, AdminGlobal } from "../serialize.ts";
|
|
3
|
-
import type
|
|
4
|
-
|
|
5
|
-
export type PickerAt = {
|
|
3
|
+
import { type PickerPaste } from "./picker-rows.ts";
|
|
4
|
+
export declare function InsertPicker({ zone, siteUrl, policy, registry, globalIds, paste, filled, onFilledChange, onInsert, onInsertGlobal, onInsertCustom, onClose }: {
|
|
6
5
|
zone: string;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
y: number;
|
|
10
|
-
};
|
|
11
|
-
export type PickerPlace = {
|
|
12
|
-
left: number;
|
|
13
|
-
top?: number;
|
|
14
|
-
bottom?: number;
|
|
15
|
-
maxHeight?: number;
|
|
16
|
-
};
|
|
17
|
-
/** Where the panel sits, given its natural height and the viewport: under the
|
|
18
|
-
* anchor when it fits there, above it when that side has more room, and never
|
|
19
|
-
* taller than the room on its side (the panel scrolls). Pure. */
|
|
20
|
-
export declare function placePicker(at: PickerAt, height: number, viewport: {
|
|
21
|
-
width: number;
|
|
22
|
-
height: number;
|
|
23
|
-
}): PickerPlace;
|
|
24
|
-
export declare function InsertPicker({ at, policy, registry, globalIds, paste, onInsert, onInsertGlobal, onInsertCustom, onClose }: {
|
|
25
|
-
at: PickerAt;
|
|
6
|
+
/** The page's address in the edited locale, as the canvas takes it: the samples frame is that page. */
|
|
7
|
+
siteUrl: string;
|
|
26
8
|
/** The section clipboard's entry, first in the list (spec 2026-10-19 editor at launch §3): disabled with the reason when the zone refuses. */
|
|
27
|
-
paste?:
|
|
28
|
-
title: string;
|
|
29
|
-
check: PasteCheck;
|
|
30
|
-
onPaste(): void;
|
|
31
|
-
};
|
|
9
|
+
paste?: PickerPaste;
|
|
32
10
|
policy: ZonePolicy | undefined;
|
|
33
11
|
registry: AdminRegistry;
|
|
34
12
|
/** Global item name → row id; an item without one cannot be placed. */
|
|
35
13
|
globalIds: Record<string, string>;
|
|
14
|
+
/** The switch's state, held by the editor (remembered per browser); the editor reads it when a section is inserted. */
|
|
15
|
+
filled: boolean;
|
|
16
|
+
onFilledChange(filled: boolean): void;
|
|
36
17
|
onInsert(section: AdminSection): void;
|
|
37
18
|
onInsertGlobal(item: AdminGlobal): void;
|
|
38
19
|
onInsertCustom(): void;
|
|
@@ -1,50 +1,59 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import {
|
|
3
|
-
// The insert picker a "+" opens:
|
|
4
|
-
//
|
|
5
|
-
// then the global items that can be placed there
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
// Measure the panel's full height (scrollHeight ignores a max-height already set), then place it.
|
|
43
|
-
useLayoutEffect(() => {
|
|
44
|
-
const el = panel.current;
|
|
45
|
-
if (!el)
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
// The insert picker a "+" opens (spec 2026-11-14 add section previews): a modal with the zone's rows on the left —
|
|
4
|
+
// the clipboard's Paste, the sections the zone's policy allows, the Custom section where the zone allows it (spec
|
|
5
|
+
// 2026-09-14 §6), then the global items that can be placed there — and on the right the current row drawn by the
|
|
6
|
+
// site itself from its sample (SamplePreview). A click on a row inserts, as it always did; hovering a row, or
|
|
7
|
+
// stepping to it with the arrow keys, only moves the picture. A zone that takes nothing says so and has no frame.
|
|
8
|
+
// Under the rows the "Insert filled" switch (spec 2026-11-14 filled samples): off, a section arrives with neither
|
|
9
|
+
// its sample nor generated values. The picture never follows it: it always shows the filled section.
|
|
10
|
+
import { useRef, useState } from "react";
|
|
11
|
+
import { Modal } from "../ui/Modal.js";
|
|
12
|
+
import { cx, SectionLabel, Switch } from "../ui/primitives.js";
|
|
13
|
+
import { samplesAddress } from "./canvas-address.js";
|
|
14
|
+
import { firstRow, pickerRows, stepRow } from "./picker-rows.js";
|
|
15
|
+
import { SamplePreview } from "./SamplePreview.js";
|
|
16
|
+
import { SectionIcon } from "./SectionIcon.js";
|
|
17
|
+
/** Below this window width the picture would be too small to read: the modal is the list alone, and no frame loads. */
|
|
18
|
+
const PANE_MIN_WINDOW = 820;
|
|
19
|
+
export function InsertPicker({ zone, siteUrl, policy, registry, globalIds, paste, filled, onFilledChange, onInsert, onInsertGlobal, onInsertCustom, onClose }) {
|
|
20
|
+
const rows = pickerRows({ policy, registry, globalIds, paste });
|
|
21
|
+
const [current, setCurrent] = useState(() => firstRow(rows));
|
|
22
|
+
const [wide] = useState(() => typeof window === "undefined" || window.innerWidth >= PANE_MIN_WINDOW);
|
|
23
|
+
const list = useRef(null);
|
|
24
|
+
const row = rows[current];
|
|
25
|
+
// Something to insert: a zone that takes nothing (a refused paste alone included) gets no picture.
|
|
26
|
+
const insertable = rows.some((r) => !r.disabled);
|
|
27
|
+
const pane = wide && insertable;
|
|
28
|
+
const pick = (r) => {
|
|
29
|
+
if (r.disabled)
|
|
30
|
+
return;
|
|
31
|
+
if (r.kind === "paste")
|
|
32
|
+
paste?.onPaste();
|
|
33
|
+
else if (r.kind === "section")
|
|
34
|
+
onInsert(r.section);
|
|
35
|
+
else if (r.kind === "custom")
|
|
36
|
+
onInsertCustom();
|
|
37
|
+
else
|
|
38
|
+
onInsertGlobal(r.item);
|
|
39
|
+
};
|
|
40
|
+
const onKeyDown = (e) => {
|
|
41
|
+
if (e.key !== "ArrowDown" && e.key !== "ArrowUp")
|
|
46
42
|
return;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
const next = stepRow(rows, current, e.key === "ArrowDown" ? 1 : -1);
|
|
45
|
+
setCurrent(next);
|
|
46
|
+
list.current?.querySelectorAll(".sm-picker__item")[next]?.focus();
|
|
47
|
+
};
|
|
48
|
+
const firstGlobal = rows.findIndex((r) => r.kind === "global");
|
|
49
|
+
const empty = !rows.some((r) => r.kind !== "paste");
|
|
50
|
+
// The switch speaks for the section rows and the Custom section; Paste and a global item carry their own content.
|
|
51
|
+
const switchable = rows.some((r) => !r.disabled && (r.kind === "section" || r.kind === "custom"));
|
|
52
|
+
return (_jsx(Modal, { title: `Add to ${zone}`, width: pane ? 980 : 380, height: pane ? "min(640px, 85vh)" : undefined, flush: true, onClose: onClose, children: _jsxs("div", { className: cx("sm-insert", pane && "sm-insert--pane"), onKeyDown: onKeyDown, children: [_jsxs("div", { className: "sm-insert__side", children: [_jsxs("div", { ref: list, className: "sm-picker sm-insert__list", children: [rows.map((r, i) => (_jsxs("div", { style: { display: "contents" }, children: [i === firstGlobal && _jsx(SectionLabel, { caps: true, style: { padding: "8px 8px 4px" }, children: "Global" }), _jsxs("button", { type: "button", autoFocus: i === current, "data-kind": r.kind, className: cx("sm-picker__item", r.kind === "paste" && "sm-picker__item--paste", i === current && "sm-picker__item--active"), disabled: r.disabled, title: r.disabled ? r.description : undefined,
|
|
53
|
+
// The pointer takes the focus with it, as in a menu: the arrow keys then step from the row under it.
|
|
54
|
+
onMouseEnter: (e) => { if (!r.disabled) {
|
|
55
|
+
setCurrent(i);
|
|
56
|
+
e.currentTarget.focus({ preventScroll: true });
|
|
57
|
+
} }, onFocus: () => { if (!r.disabled)
|
|
58
|
+
setCurrent(i); }, onClick: () => pick(r), children: [_jsx("span", { className: "sm-picker__icon", children: _jsx(SectionIcon, { icon: r.icon }) }), _jsxs("span", { style: { minWidth: 0 }, children: [_jsx("span", { style: { display: "block", fontWeight: 600 }, children: r.title }), r.description && (_jsx("span", { style: { display: "block", fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: r.description }))] })] })] }, r.key))), empty && _jsx("p", { style: { color: "var(--text-muted)", padding: 10 }, children: "This zone doesn't accept insertions." })] }), switchable && (_jsxs("label", { className: "sm-insert__foot", title: "Off: a section is inserted empty, without its sample content", children: [_jsx(Switch, { checked: filled, onChange: onFilledChange, label: "Insert filled" }), _jsx("span", { "aria-hidden": "true", children: "Insert filled" })] }))] }), pane && row && (_jsxs("div", { className: "sm-insert__pane", children: [_jsx(SamplePreview, { src: samplesAddress(siteUrl, zone), nodeId: row.nodeId, fallback: _jsx("span", { className: "sm-insert__icon", children: _jsx(SectionIcon, { icon: row.icon, size: 32 }) }) }), _jsxs("div", { className: "sm-insert__caption", children: [_jsx("span", { style: { display: "block", fontWeight: 600 }, children: row.title }), row.description && _jsx("span", { style: { display: "block", fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: row.description })] })] }))] }) }));
|
|
50
59
|
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export declare function SamplePreview({ src, nodeId, fallback }: {
|
|
3
|
+
src: string;
|
|
4
|
+
/** The current row's sample node; null for a row that has none (Paste). */
|
|
5
|
+
nodeId: string | null;
|
|
6
|
+
fallback: ReactNode;
|
|
7
|
+
}): import("react").JSX.Element;
|