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.
@@ -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 { Button, Grip, IconButton, SectionLabel, cx } from "../ui/primitives.js";
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(IconButton, { label: "Remove", size: 22, onClick: (e) => { e.stopPropagation(); onRemove(node.id); }, children: "\u00D7" })] }, node.id))) }))] }));
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
- // The record changes identity on every rename; re-seed the inputs from it.
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
- }, [record, locale]); // eslint-disable-line react-hooks/exhaustive-deps
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 { Handlers, ZoneRows } from "./Outline.tsx";
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 a boundary, or the empty zone's first slot: opens the picker there. */
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 card's ⋯ entries beyond Remove; plan (c) adds Copy and the Paste entries here. */
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
- /** The card's menu: the given entries, then Remove (danger; disabled in a locked zone). */
19
- export function cardMenu({ locked, onRemove, actions }) {
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 && 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) => (_jsxs("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) ?? [] }) }) })] }), canTake && add(i + 1)] }, node.id))) }))] }));
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
- const item = items[name];
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]));