smoodly 0.0.54 → 0.0.56
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/editor/EditorView.js +1 -1
- package/dist/admin/editor/Outline.d.ts +9 -0
- package/dist/admin/editor/Outline.js +12 -3
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/StructureView.d.ts +4 -12
- package/dist/admin/editor/StructureView.js +11 -11
- package/dist/admin/editor/global-edit.js +5 -2
- package/dist/admin/files.generated.js +1 -1
- package/dist/admin/forms/FieldWidget.js +4 -1
- package/dist/admin/tree-ops.js +6 -2
- package/dist/admin/ui/theme.js +3 -4
- package/dist/admin/validate.d.ts +4 -0
- package/dist/admin/validate.js +9 -0
- package/dist/astro/components/RichText.astro +2 -1
- package/dist/astro/preview.d.ts +6 -3
- package/dist/astro/preview.js +25 -12
- package/dist/astro/richtext.js +2 -2
- package/dist/astro/routes/admin.js +2 -2
- package/dist/config.js +6 -3
- package/dist/css.js +4 -0
- package/dist/custom/index.js +3 -0
- package/dist/entry-store.d.ts +3 -0
- package/dist/image/markup.js +19 -3
- package/dist/read-shapes.d.ts +1 -0
- package/dist/read-shapes.js +4 -1
- package/dist/registration.d.ts +3 -2
- package/dist/registration.js +7 -2
- package/dist/richtext/index.js +9 -0
- package/dist/richtext-href.d.ts +2 -0
- package/dist/richtext-href.js +2 -0
- package/dist/richtext-render.js +5 -4
- package/dist/store.d.ts +3 -0
- package/dist/store.js +6 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/views.js +27 -0
- package/package.json +5 -1
|
@@ -1132,7 +1132,7 @@ export function EditorView({ ops, registry, id }) {
|
|
|
1132
1132
|
if (current)
|
|
1133
1133
|
syncTexts(selectedId, selectedItem, { ...current.fields, [key]: value });
|
|
1134
1134
|
}
|
|
1135
|
-
}, 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, onAddFirst: (zone) => {
|
|
1135
|
+
}, 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
1136
|
const first = allowedSections(policyOf(zone), registry.sections)[0];
|
|
1137
1137
|
if (first)
|
|
1138
1138
|
doInsert(zone, 0, first);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TreeNode } from "../../render.tsx";
|
|
2
|
+
import { type ActionMenuItem } from "../ui/ActionMenu.tsx";
|
|
2
3
|
export type ZoneRows = {
|
|
3
4
|
zone: string;
|
|
4
5
|
locked: boolean;
|
|
@@ -23,7 +24,15 @@ export type Handlers = {
|
|
|
23
24
|
onRemove(id: string): void;
|
|
24
25
|
/** A removed zone's Clear zone. */
|
|
25
26
|
onClearZone?(zone: string): void;
|
|
27
|
+
/** The row's ⋯ entries before Remove: Copy and the Paste entries (spec 2026-10-19 editor at launch §3). */
|
|
28
|
+
cardActions?(node: TreeNode, zone: string, index: number): ActionMenuItem[];
|
|
26
29
|
};
|
|
30
|
+
/** The row's menu, shared with the structure view's cards: the given entries, then Remove (danger; disabled in a locked zone). */
|
|
31
|
+
export declare function cardMenu({ locked, onRemove, actions }: {
|
|
32
|
+
locked: boolean;
|
|
33
|
+
onRemove(): void;
|
|
34
|
+
actions: ActionMenuItem[];
|
|
35
|
+
}): ActionMenuItem[];
|
|
27
36
|
export declare function Outline({ zones, ...handlers }: {
|
|
28
37
|
zones: ZoneRows[];
|
|
29
38
|
} & Handlers): import("react").JSX.Element;
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
-
import {
|
|
3
|
+
import { ActionMenu } from "../ui/ActionMenu.js";
|
|
4
|
+
import { Button, Grip, SectionLabel, cx } from "../ui/primitives.js";
|
|
4
5
|
import { useSortable } from "../ui/useSortable.js";
|
|
6
|
+
/** The row's menu, shared with the structure view's cards: the given entries, then Remove (danger; disabled in a locked zone). */
|
|
7
|
+
export function cardMenu({ locked, onRemove, actions }) {
|
|
8
|
+
return [...actions, { label: "Remove", run: onRemove, danger: true, disabled: locked }];
|
|
9
|
+
}
|
|
10
|
+
const stop = (e) => e.stopPropagation();
|
|
11
|
+
/** The ⋯'s Enter and Space must not select the row; every other key (the editor's Cmd+Z) bubbles on. */
|
|
12
|
+
const stopActivation = (e) => { if (e.key === "Enter" || e.key === " ")
|
|
13
|
+
e.stopPropagation(); };
|
|
5
14
|
export function Outline({ zones, ...handlers }) {
|
|
6
15
|
return (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Page Outline" }), zones.map((z) => _jsx(OutlineZone, { ...z, ...handlers }, z.zone))] }));
|
|
7
16
|
}
|
|
8
|
-
function OutlineZone({ zone, locked, canTake, nodes, removed, note, selectedId, onSelect, onAddFirst, onMoveTo, onRemove, onClearZone }) {
|
|
17
|
+
function OutlineZone({ zone, locked, canTake, nodes, removed, note, selectedId, onSelect, onAddFirst, onMoveTo, onRemove, onClearZone, cardActions }) {
|
|
9
18
|
const sortable = useSortable({
|
|
10
19
|
count: locked || removed ? 0 : nodes.length,
|
|
11
20
|
onDrop: (from, to) => onMoveTo(nodes[from].node.id, to),
|
|
@@ -14,5 +23,5 @@ function OutlineZone({ zone, locked, canTake, nodes, removed, note, selectedId,
|
|
|
14
23
|
return (_jsx("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, "data-testid": "removed-zone", children: _jsxs("div", { className: "sm-outline-row sm-outline-row--orphan sm-outline-row--removed", children: [_jsxs("span", { className: "sm-outline-row__label", children: ["Removed zone `", zone, "` \u00B7 ", nodes.length, " ", nodes.length === 1 ? "section" : "sections"] }), _jsx(Button, { variant: "danger", size: "sm", onClick: () => onClearZone?.(zone), children: "Clear zone" })] }) }));
|
|
15
24
|
}
|
|
16
25
|
return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [_jsxs(SectionLabel, { caps: true, children: [zone, locked && " · locked"] }), note && _jsx("p", { className: "sm-outline-note", children: note }), nodes.length === 0 && (_jsxs("div", { className: cx("sm-outline-row", "sm-outline-row--empty", canTake && "sm-outline-row--clickable"), onClick: () => { if (canTake)
|
|
17
|
-
onAddFirst(zone); }, children: [_jsx("span", { className: "sm-outline-row__label", children: "Empty" }), canTake && _jsx("span", { className: "sm-outline-row__add", children: "+ Add" })] })), locked && nodes.map(({ node, label, mark }) => (_jsxs("div", { className: cx("sm-outline-row sm-outline-row--locked sm-outline-row--clickable", mark && `sm-outline-row--${mark}`), "data-testid": "outline-row", onClick: () => onSelect(node.id), children: [_jsx("span", { className: "sm-outline-row__label", children: label }), _jsx("span", { className: "sm-outline-row__note", children: "Locked" })] }, node.id))), !locked && nodes.length > 0 && (_jsx("div", { ref: sortable.containerRef, style: { display: "flex", flexDirection: "column", gap: 4 }, children: nodes.map(({ node, label, mark }, i) => (_jsxs("div", { onClick: () => onSelect(node.id), style: sortable.rowStyle(i), "data-testid": "outline-row", className: cx("sm-outline-row", "sm-outline-row--clickable", "sm-outline-row--grip", node.id === selectedId && "sm-outline-row--selected", mark && `sm-outline-row--${mark}`, sortable.rowClass(i)), children: [_jsx(Grip, { ...sortable.handleProps(i) }), _jsx("span", { className: "sm-outline-row__label", children: label }), _jsx(
|
|
26
|
+
onAddFirst(zone); }, children: [_jsx("span", { className: "sm-outline-row__label", children: "Empty" }), canTake && _jsx("span", { className: "sm-outline-row__add", children: "+ Add" })] })), locked && nodes.map(({ node, label, mark }) => (_jsxs("div", { className: cx("sm-outline-row sm-outline-row--locked sm-outline-row--clickable", mark && `sm-outline-row--${mark}`), "data-testid": "outline-row", onClick: () => onSelect(node.id), children: [_jsx("span", { className: "sm-outline-row__label", children: label }), _jsx("span", { className: "sm-outline-row__note", children: "Locked" })] }, node.id))), !locked && nodes.length > 0 && (_jsx("div", { ref: sortable.containerRef, style: { display: "flex", flexDirection: "column", gap: 4 }, children: nodes.map(({ node, label, mark }, i) => (_jsxs("div", { onClick: () => onSelect(node.id), style: sortable.rowStyle(i), "data-testid": "outline-row", className: cx("sm-outline-row", "sm-outline-row--clickable", "sm-outline-row--grip", node.id === selectedId && "sm-outline-row--selected", mark && `sm-outline-row--${mark}`, sortable.rowClass(i)), children: [_jsx(Grip, { ...sortable.handleProps(i) }), _jsx("span", { className: "sm-outline-row__label", children: label }), _jsx("span", { className: "sm-outline-row__menu", onClick: stop, onKeyDown: stopActivation, children: _jsx(ActionMenu, { small: true, label: "Section actions", items: cardMenu({ locked: false, onRemove: () => onRemove(node.id), actions: cardActions?.(node, zone, i) ?? [] }) }) })] }, node.id))) }))] }));
|
|
18
27
|
}
|
|
@@ -23,10 +23,11 @@ export function PageSettings({ record, locale, registry, path, fixed, redirects,
|
|
|
23
23
|
/** One commit at a time: Enter commits, then the confirm's autofocus blurs the input, and that blur must not ask again. */
|
|
24
24
|
const committing = useRef(false);
|
|
25
25
|
const isHome = record.parentId === null && record.locales[registry.locales.default]?.slug === registry.homePageSlug;
|
|
26
|
-
//
|
|
26
|
+
// Re-seed the inputs when the stored title or slug changes (a rename, a locale switch, a restore) — not on the
|
|
27
|
+
// record's identity: every autosave ack replaces the record, and typing in a field meanwhile must survive it.
|
|
27
28
|
useEffect(() => {
|
|
28
29
|
setDraft({ title: row?.title ?? "", slug: row?.slug ?? "" });
|
|
29
|
-
}, [
|
|
30
|
+
}, [row?.title, row?.slug, locale]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
30
31
|
if (!row)
|
|
31
32
|
return null;
|
|
32
33
|
const commit = async (key) => {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
|
-
import type { TreeNode } from "../../page-context.ts";
|
|
3
2
|
import { type ActionMenuItem } from "../ui/ActionMenu.tsx";
|
|
4
3
|
import type { CardPreview } from "./card-preview.ts";
|
|
5
|
-
import type
|
|
4
|
+
import { cardMenu, type Handlers, type ZoneRows } from "./Outline.tsx";
|
|
5
|
+
export { cardMenu };
|
|
6
6
|
/** A card: the outline's row plus the section's icon from the registry (`icon ?? "▤"`), its preview, and whether it is a placed global item. */
|
|
7
7
|
export type StructureNode = ZoneRows["nodes"][number] & {
|
|
8
8
|
icon: string;
|
|
@@ -15,17 +15,9 @@ export type StructureZone = Omit<ZoneRows, "nodes"> & {
|
|
|
15
15
|
export type StructureViewProps = {
|
|
16
16
|
zones: StructureZone[];
|
|
17
17
|
} & Omit<Handlers, "onAddFirst"> & {
|
|
18
|
-
/** "+ Add section" at
|
|
18
|
+
/** "+ Add section" at the zone's end, or the empty zone's first slot: opens the picker there. */
|
|
19
19
|
onAdd(zone: string, index: number, clientX: number, clientY: number): void;
|
|
20
|
-
/** The
|
|
21
|
-
cardActions?(node: TreeNode, zone: string, index: number): ActionMenuItem[];
|
|
22
|
-
/** The zone heading's ⋯ entries; plan (c) adds Paste. Absent or empty: no ⋯ on the heading. */
|
|
20
|
+
/** The zone heading's ⋯ entries (Paste). Absent or empty: no ⋯ on the heading. */
|
|
23
21
|
zoneActions?(zone: string): ActionMenuItem[];
|
|
24
22
|
};
|
|
25
|
-
/** The card's menu: the given entries, then Remove (danger; disabled in a locked zone). */
|
|
26
|
-
export declare function cardMenu({ locked, onRemove, actions }: {
|
|
27
|
-
locked: boolean;
|
|
28
|
-
onRemove(): void;
|
|
29
|
-
actions: ActionMenuItem[];
|
|
30
|
-
}): ActionMenuItem[];
|
|
31
23
|
export declare function StructureView({ zones, ...rest }: StructureViewProps): ReactElement;
|
|
@@ -8,17 +8,17 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
// nothing here touches the tree. A card is a `div[role=button]`, not a
|
|
9
9
|
// <button>: it holds the grip and the menu, and a button inside a button
|
|
10
10
|
// is invalid HTML. The outline's marks carry over: broken red, orphan
|
|
11
|
-
// muted, a removed zone as one row with Clear zone.
|
|
11
|
+
// muted, a removed zone as one row with Clear zone. One "+ Add section"
|
|
12
|
+
// per zone, at its end (revised 2026-10-20: one between every pair of
|
|
13
|
+
// cards read as noise); a place in the middle is a paste or a drag.
|
|
12
14
|
import { useEffect, useState } from "react";
|
|
13
15
|
import { ActionMenu } from "../ui/ActionMenu.js";
|
|
14
16
|
import { Button, Grip, SectionLabel, cx } from "../ui/primitives.js";
|
|
15
17
|
import { useSortable } from "../ui/useSortable.js";
|
|
16
18
|
import { AssetThumb } from "../forms/MediaGrid.js";
|
|
17
19
|
import { useAssets } from "../forms/assets-context.js";
|
|
18
|
-
|
|
19
|
-
export
|
|
20
|
-
return [...actions, { label: "Remove", run: onRemove, danger: true, disabled: locked }];
|
|
21
|
-
}
|
|
20
|
+
import { cardMenu } from "./Outline.js";
|
|
21
|
+
export { cardMenu };
|
|
22
22
|
export function StructureView({ zones, ...rest }) {
|
|
23
23
|
return (_jsx("div", { className: "sm-structure", "data-testid": "structure", children: zones.map((z) => _jsx(StructureZoneView, { ...z, ...rest }, z.zone)) }));
|
|
24
24
|
}
|
|
@@ -36,12 +36,12 @@ function StructureZoneView({ zone, locked, canTake, nodes, removed, note, select
|
|
|
36
36
|
}
|
|
37
37
|
const actions = zoneActions?.(zone) ?? [];
|
|
38
38
|
const add = (index) => (_jsx("span", { className: "sm-structure__slot", children: _jsxs("button", { type: "button", className: "sm-structure__add", "aria-label": `Add to ${zone}`, title: `Add to ${zone}`, onClick: (e) => { e.stopPropagation(); onAdd(zone, index, e.clientX, e.clientY); }, children: [_jsx("span", { className: "sm-structure__add-icon", "aria-hidden": "true", children: "+" }), "Add section"] }) }));
|
|
39
|
-
return (_jsxs("div", { className: "sm-structure__group", children: [_jsxs("div", { className: "sm-structure__zone", children: [_jsxs(SectionLabel, { caps: true, children: [zone, locked && " · locked"] }), actions.length > 0 && _jsx(ActionMenu, { small: true, label: "Zone actions", items: actions })] }), note && _jsx("p", { className: "sm-outline-note", children: note }), nodes.length === 0 && (_jsxs("div", { className: "sm-structure__empty", children: [_jsx("span", { children: "Empty" }), canTake && add(0)] })), nodes.length > 0 &&
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
return (_jsxs("div", { className: "sm-structure__group", children: [_jsxs("div", { className: "sm-structure__zone", children: [_jsxs(SectionLabel, { caps: true, children: [zone, locked && " · locked"] }), actions.length > 0 && _jsx(ActionMenu, { small: true, label: "Zone actions", items: actions })] }), note && _jsx("p", { className: "sm-outline-note", children: note }), nodes.length === 0 && (_jsxs("div", { className: "sm-structure__empty", children: [_jsx("span", { children: "Empty" }), canTake && add(0)] })), nodes.length > 0 && (_jsx("div", { ref: sortable.containerRef, className: "sm-structure__rows", children: nodes.map(({ node, label, mark, icon, preview, global }, i) => (_jsx("div", { className: cx("sm-structure__row", sortable.rowClass(i)), style: sortable.rowStyle(i), children: _jsxs("div", { role: "button", tabIndex: 0, "data-testid": "structure-card", "data-node": node.id, className: cx("sm-structure__card", node.id === selectedId && "sm-structure__card--selected", locked && "sm-structure__card--locked", mark && `sm-structure__card--${mark}`), onClick: () => onSelect(node.id), onKeyDown: (e) => { if (e.key === "Enter" || e.key === " ") {
|
|
40
|
+
e.preventDefault();
|
|
41
|
+
onSelect(node.id);
|
|
42
|
+
} }, children: [locked ? _jsx("span", { className: "sm-structure__gripslot", "aria-hidden": "true" }) : _jsx(Grip, { ...sortable.handleProps(i) }), _jsx("span", { className: "sm-structure__icon", "aria-hidden": "true", children: icon }), _jsxs("span", { className: "sm-structure__text", children: [_jsxs("span", { className: "sm-structure__title", children: [label, global && _jsx("span", { className: "sm-structure__badge", children: "global" })] }), global
|
|
43
|
+
? _jsx("span", { className: "sm-structure__preview", children: "Global \u00B7 edited in one place" })
|
|
44
|
+
: preview.text !== null && _jsx("span", { className: "sm-structure__preview", children: preview.text })] }), !global && preview.image && _jsx(Thumb, { image: preview.image }), _jsx("span", { className: "sm-structure__menu", onClick: stop, onKeyDown: stopActivation, children: _jsx(ActionMenu, { small: true, label: "Section actions", items: cardMenu({ locked, onRemove: () => onRemove(node.id), actions: cardActions?.(node, zone, i) ?? [] }) }) })] }) }, node.id))) })), nodes.length > 0 && canTake && add(nodes.length)] }));
|
|
45
45
|
}
|
|
46
46
|
/** The thumbnail: the asset record fetched through the assets context (cached per shell lifetime), the box drawn at once. */
|
|
47
47
|
function Thumb({ image }) {
|
|
@@ -102,12 +102,15 @@ export function createGlobalEdit(opts) {
|
|
|
102
102
|
},
|
|
103
103
|
flush: autosave.flush,
|
|
104
104
|
async setStatus(name, status) {
|
|
105
|
-
|
|
106
|
-
if (!item)
|
|
105
|
+
if (!items[name])
|
|
107
106
|
return { ok: false, code: "not_found", message: "This item hasn't loaded yet." };
|
|
108
107
|
if (!(await autosave.flush())) {
|
|
109
108
|
return { ok: false, code: "internal", message: "Couldn't save your latest changes — publish cancelled." };
|
|
110
109
|
}
|
|
110
|
+
// Read the item after the flush: a save it landed replaced the record, and with it the token.
|
|
111
|
+
const item = items[name];
|
|
112
|
+
if (!item)
|
|
113
|
+
return { ok: false, code: "not_found", message: "This item hasn't loaded yet." };
|
|
111
114
|
let r;
|
|
112
115
|
try {
|
|
113
116
|
r = await opts.ops.globals.setStatus(name, item.locale, status, draftToken(item.record.locales[item.locale]));
|