smoodly 0.0.16 → 0.0.17

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.
Files changed (133) hide show
  1. package/README.md +156 -21
  2. package/dist/admin/client-ops.js +1 -1
  3. package/dist/admin/custom-section.d.ts +10 -0
  4. package/dist/admin/custom-section.js +10 -0
  5. package/dist/admin/editor/CanvasOverlay.d.ts +4 -4
  6. package/dist/admin/editor/CanvasOverlay.js +5 -5
  7. package/dist/admin/editor/EditorView.js +140 -108
  8. package/dist/admin/editor/GlobalInspector.d.ts +24 -0
  9. package/dist/admin/editor/{SharedInspector.js → GlobalInspector.js} +4 -4
  10. package/dist/admin/editor/InsertPicker.d.ts +6 -5
  11. package/dist/admin/editor/InsertPicker.js +6 -4
  12. package/dist/admin/editor/InspectorFields.d.ts +21 -0
  13. package/dist/admin/editor/InspectorFields.js +22 -0
  14. package/dist/admin/editor/Outline.d.ts +0 -1
  15. package/dist/admin/editor/Outline.js +3 -3
  16. package/dist/admin/editor/{shared-edit.d.ts → global-edit.d.ts} +10 -10
  17. package/dist/admin/editor/{shared-edit.js → global-edit.js} +4 -4
  18. package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
  19. package/dist/admin/editor/{useSharedEdit.js → useGlobalEdit.js} +4 -4
  20. package/dist/admin/forms/FieldWidget.d.ts +14 -1
  21. package/dist/admin/forms/FieldWidget.js +38 -11
  22. package/dist/admin/forms/ListField.d.ts +9 -1
  23. package/dist/admin/forms/ListField.js +16 -8
  24. package/dist/admin/forms/RichtextElementCard.js +11 -5
  25. package/dist/admin/forms/RowsWidget.d.ts +13 -0
  26. package/dist/admin/forms/RowsWidget.js +26 -0
  27. package/dist/admin/forms/SpanBar.d.ts +5 -0
  28. package/dist/admin/forms/SpanBar.js +37 -0
  29. package/dist/admin/forms/richtext-insert.d.ts +2 -2
  30. package/dist/admin/forms/richtext-insert.js +3 -3
  31. package/dist/admin/forms/richtext-nodes.js +1 -1
  32. package/dist/admin/forms/tabs.d.ts +15 -9
  33. package/dist/admin/forms/tabs.js +19 -23
  34. package/dist/admin/forms/useRichtextEditor.js +8 -1
  35. package/dist/admin/global-items.d.ts +9 -0
  36. package/dist/admin/{shared-items.js → global-items.js} +17 -16
  37. package/dist/admin/index.d.ts +3 -3
  38. package/dist/admin/index.js +1 -1
  39. package/dist/admin/ops-impl.js +65 -54
  40. package/dist/admin/ops.d.ts +6 -6
  41. package/dist/admin/serialize.d.ts +18 -6
  42. package/dist/admin/serialize.js +27 -4
  43. package/dist/admin/shell/AdminApp.js +6 -6
  44. package/dist/admin/shell/GlobalFields.d.ts +24 -0
  45. package/dist/admin/shell/GlobalFields.js +19 -0
  46. package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
  47. package/dist/admin/shell/{SharedForm.js → GlobalForm.js} +36 -26
  48. package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
  49. package/dist/admin/shell/{SharedList.js → GlobalList.js} +9 -9
  50. package/dist/admin/shell/IconRail.js +13 -6
  51. package/dist/admin/shell/PagesList.js +1 -6
  52. package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
  53. package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
  54. package/dist/admin/tree-ops.d.ts +10 -8
  55. package/dist/admin/tree-ops.js +28 -16
  56. package/dist/admin/ui/HistoryModal.js +1 -1
  57. package/dist/admin/ui/primitives.d.ts +3 -1
  58. package/dist/admin/ui/primitives.js +2 -2
  59. package/dist/admin/ui/theme.js +54 -14
  60. package/dist/admin/validate.d.ts +27 -2
  61. package/dist/admin/validate.js +47 -10
  62. package/dist/collections.d.ts +3 -3
  63. package/dist/collections.js +1 -1
  64. package/dist/conditions.d.ts +33 -0
  65. package/dist/conditions.js +79 -0
  66. package/dist/config.d.ts +26 -19
  67. package/dist/config.js +26 -25
  68. package/dist/css.d.ts +7 -0
  69. package/dist/css.js +57 -0
  70. package/dist/custom/CustomSection.d.ts +26 -0
  71. package/dist/custom/CustomSection.js +20 -0
  72. package/dist/custom/css.d.ts +3 -0
  73. package/dist/custom/css.js +28 -0
  74. package/dist/custom/index.d.ts +26 -0
  75. package/dist/custom/index.js +16 -0
  76. package/dist/custom/rows.d.ts +28 -0
  77. package/dist/custom/rows.js +129 -0
  78. package/dist/custom/schema.d.ts +32 -0
  79. package/dist/custom/schema.js +39 -0
  80. package/dist/entry-store.d.ts +4 -4
  81. package/dist/entry-store.js +17 -17
  82. package/dist/fields.d.ts +5 -2
  83. package/dist/fields.js +4 -2
  84. package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
  85. package/dist/{shared-id.js → global-id.js} +4 -4
  86. package/dist/global.d.ts +57 -0
  87. package/dist/global.js +51 -0
  88. package/dist/index.d.ts +28 -18
  89. package/dist/index.js +10 -5
  90. package/dist/media.js +1 -1
  91. package/dist/next/index.d.ts +1 -1
  92. package/dist/next/page-renderer.d.ts +4 -2
  93. package/dist/next/page-renderer.js +9 -9
  94. package/dist/next/queries.d.ts +6 -6
  95. package/dist/next/queries.js +7 -7
  96. package/dist/page.js +2 -2
  97. package/dist/pairing.d.ts +13 -7
  98. package/dist/pairing.js +4 -4
  99. package/dist/refs.d.ts +2 -2
  100. package/dist/refs.js +4 -4
  101. package/dist/render.d.ts +13 -1
  102. package/dist/render.js +10 -5
  103. package/dist/resolve.d.ts +11 -6
  104. package/dist/resolve.js +22 -12
  105. package/dist/revalidate.d.ts +4 -4
  106. package/dist/revalidate.js +4 -4
  107. package/dist/richtext-render.js +2 -2
  108. package/dist/richtext-walk.d.ts +5 -5
  109. package/dist/richtext-walk.js +4 -4
  110. package/dist/schema.d.ts +15 -10
  111. package/dist/schema.js +19 -7
  112. package/dist/section-wrapper.d.ts +18 -6
  113. package/dist/section-wrapper.js +24 -17
  114. package/dist/site.d.ts +15 -5
  115. package/dist/site.js +24 -10
  116. package/dist/sql-space.js +1 -1
  117. package/dist/sql.js +2 -2
  118. package/dist/store.d.ts +7 -0
  119. package/dist/store.js +6 -0
  120. package/dist/styles.d.ts +27 -0
  121. package/dist/styles.js +60 -0
  122. package/dist/supabase-entry-store.js +14 -14
  123. package/dist/supabase-store.js +2 -1
  124. package/dist/zones.d.ts +9 -7
  125. package/dist/zones.js +5 -5
  126. package/package.json +1 -1
  127. package/dist/admin/editor/SharedInspector.d.ts +0 -23
  128. package/dist/admin/editor/useSharedEdit.d.ts +0 -6
  129. package/dist/admin/shared-items.d.ts +0 -9
  130. package/dist/admin/shell/SharedFields.d.ts +0 -21
  131. package/dist/admin/shell/SharedFields.js +0 -15
  132. package/dist/shared.d.ts +0 -53
  133. package/dist/shared.js +0 -45
@@ -1,6 +1,7 @@
1
1
  "use client";
2
2
  import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
- import { allowedSections, allowedShared } from "../tree-ops.js";
3
+ import { allowedSections, allowedGlobals } from "../tree-ops.js";
4
+ import { allowsCustom } from "../custom-section.js";
4
5
  import { SectionLabel } from "../ui/primitives.js";
5
6
  /** Keep the panel on screen; a static render (no window) leaves it where it was asked. */
6
7
  function clamp(at) {
@@ -11,8 +12,9 @@ function clamp(at) {
11
12
  top: Math.min(at.y, Math.max(12, window.innerHeight - 240)),
12
13
  };
13
14
  }
14
- export function InsertPicker({ at, policy, registry, sharedIds, onInsert, onInsertShared, onClose }) {
15
+ export function InsertPicker({ at, policy, registry, globalIds, onInsert, onInsertGlobal, onInsertCustom, onClose }) {
15
16
  const sections = allowedSections(policy, registry.sections);
16
- const shared = allowedShared(policy, registry.shared, sharedIds);
17
- return (_jsx("div", { style: { position: "fixed", inset: 0, zIndex: 60 }, onClick: onClose, children: _jsxs("div", { className: "sm-picker", onClick: (e) => e.stopPropagation(), style: { position: "absolute", ...clamp(at) }, children: [_jsxs(SectionLabel, { mono: true, style: { padding: "6px 8px 8px" }, children: ["Add to ", at.zone] }), sections.map((section) => (_jsxs("button", { type: "button", className: "sm-picker__item", onClick: () => onInsert(section), children: [_jsx("span", { className: "sm-picker__icon", children: section.icon ?? "▤" }), _jsxs("span", { style: { minWidth: 0 }, children: [_jsx("span", { style: { display: "block", fontWeight: 600 }, children: section.title }), section.description && (_jsx("span", { style: { display: "block", fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: section.description }))] })] }, section.name))), shared.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { mono: true, style: { padding: "8px 8px 4px" }, children: "Shared" }), shared.map((item) => (_jsxs("button", { type: "button", className: "sm-picker__item", onClick: () => onInsertShared(item), children: [_jsx("span", { className: "sm-picker__icon", children: "\u26D3" }), _jsxs("span", { style: { minWidth: 0 }, children: [_jsx("span", { style: { display: "block", fontWeight: 600 }, children: item.title }), _jsx("span", { style: { display: "block", fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: "Shared \u00B7 edited in one place" })] })] }, `shared-${item.name}`)))] })), sections.length === 0 && shared.length === 0 && (_jsx("p", { style: { color: "var(--text-muted)", padding: 10 }, children: "This zone doesn't accept insertions." }))] }) }));
17
+ const globals = allowedGlobals(policy, registry.globals, globalIds);
18
+ const custom = allowsCustom(policy);
19
+ return (_jsx("div", { style: { position: "fixed", inset: 0, zIndex: 60 }, onClick: onClose, children: _jsxs("div", { className: "sm-picker", onClick: (e) => e.stopPropagation(), style: { position: "absolute", ...clamp(at) }, children: [_jsxs(SectionLabel, { mono: true, style: { padding: "6px 8px 8px" }, children: ["Add to ", at.zone] }), sections.map((section) => (_jsxs("button", { type: "button", className: "sm-picker__item", onClick: () => onInsert(section), children: [_jsx("span", { className: "sm-picker__icon", children: section.icon ?? "▤" }), _jsxs("span", { style: { minWidth: 0 }, children: [_jsx("span", { style: { display: "block", fontWeight: 600 }, children: section.title }), section.description && (_jsx("span", { style: { display: "block", fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: section.description }))] })] }, section.name))), custom && (_jsxs("button", { type: "button", className: "sm-picker__item", onClick: onInsertCustom, children: [_jsx("span", { className: "sm-picker__icon", children: "\u25A6" }), _jsxs("span", { style: { minWidth: 0 }, children: [_jsx("span", { style: { display: "block", fontWeight: 600 }, children: registry.custom.title }), _jsx("span", { style: { display: "block", fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: registry.custom.description })] })] })), globals.length > 0 && (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { mono: true, style: { padding: "8px 8px 4px" }, children: "Global" }), globals.map((item) => (_jsxs("button", { type: "button", className: "sm-picker__item", onClick: () => onInsertGlobal(item), children: [_jsx("span", { className: "sm-picker__icon", children: "\u26D3" }), _jsxs("span", { style: { minWidth: 0 }, children: [_jsx("span", { style: { display: "block", fontWeight: 600 }, children: item.title }), _jsx("span", { style: { display: "block", fontSize: "var(--text-sm)", color: "var(--text-muted)" }, children: "Global \u00B7 edited in one place" })] })] }, `global-${item.name}`)))] })), sections.length === 0 && globals.length === 0 && !custom && (_jsx("p", { style: { color: "var(--text-muted)", padding: 10 }, children: "This zone doesn't accept insertions." }))] }) }));
18
20
  }
@@ -0,0 +1,21 @@
1
+ import type { ReactElement, ReactNode } from "react";
2
+ import type { Group, TabEntry } from "../forms/tabs.ts";
3
+ import type { WidgetScope } from "../forms/FieldWidget.tsx";
4
+ export type NodeValues = {
5
+ fields: Record<string, unknown>;
6
+ settings?: Record<string, unknown>;
7
+ styles?: Record<string, unknown>;
8
+ css?: string;
9
+ };
10
+ /** The entries whose condition does not hold: a bare path reads the entry's own namespace. */
11
+ export declare function visibleEntries(entries: TabEntry[], values: NodeValues): TabEntry[];
12
+ export declare function valueOf(values: NodeValues, entry: TabEntry): unknown;
13
+ export declare function InspectorFields({ groups, values, render }: {
14
+ groups: Group[];
15
+ values: NodeValues;
16
+ /** One widget for one entry. */
17
+ render(entry: TabEntry, opts: {
18
+ emptyLabel?: string;
19
+ scope: WidgetScope;
20
+ }): ReactNode;
21
+ }): ReactElement;
@@ -0,0 +1,22 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { isHidden } from "../../conditions.js";
4
+ import { SectionLabel } from "../ui/primitives.js";
5
+ /** The entries whose condition does not hold: a bare path reads the entry's own namespace. */
6
+ export function visibleEntries(entries, values) {
7
+ return entries.filter(({ ns, descriptor }) => ns === "css" || !isHidden(descriptor, { self: values[ns], fields: values.fields, settings: values.settings }));
8
+ }
9
+ export function valueOf(values, entry) {
10
+ if (entry.ns === "css")
11
+ return values.css;
12
+ return values[entry.ns]?.[entry.key];
13
+ }
14
+ export function InspectorFields({ groups, values, render }) {
15
+ const scope = { fields: values.fields, settings: values.settings };
16
+ return (_jsx(_Fragment, { children: groups.map((g, i) => {
17
+ const entries = visibleEntries(g.entries, values);
18
+ if (entries.length === 0)
19
+ return null;
20
+ return (_jsxs("div", { className: "sm-inspector__group", children: [g.title && _jsx(SectionLabel, { mono: true, children: g.title }), entries.map((e) => render(e, { emptyLabel: e.ns === "styles" ? "Default" : undefined, scope }))] }, g.title ?? i));
21
+ }) }));
22
+ }
@@ -13,7 +13,6 @@ type Handlers = {
13
13
  onSelect(id: string): void;
14
14
  /** The empty row of an insertable zone was clicked. */
15
15
  onAddFirst(zone: string): void;
16
- onMove(id: string, dir: -1 | 1): void;
17
16
  /** The drop and the grip's arrow keys: `index` in the resulting order. */
18
17
  onMoveTo(id: string, index: number): void;
19
18
  onRemove(id: string): void;
@@ -3,13 +3,13 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
3
3
  import { Grip, IconButton, SectionLabel, cx } from "../ui/primitives.js";
4
4
  import { useSortable } from "../ui/useSortable.js";
5
5
  export function Outline({ zones, ...handlers }) {
6
- return (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Outline" }), zones.map((z) => _jsx(OutlineZone, { ...z, ...handlers }, z.zone))] }));
6
+ return (_jsxs(_Fragment, { children: [_jsx(SectionLabel, { children: "Page Outline" }), zones.map((z) => _jsx(OutlineZone, { ...z, ...handlers }, z.zone))] }));
7
7
  }
8
- function OutlineZone({ zone, locked, canTake, nodes, selectedId, onSelect, onAddFirst, onMove, onMoveTo, onRemove }) {
8
+ function OutlineZone({ zone, locked, canTake, nodes, selectedId, onSelect, onAddFirst, onMoveTo, onRemove }) {
9
9
  const sortable = useSortable({
10
10
  count: locked ? 0 : nodes.length,
11
11
  onDrop: (from, to) => onMoveTo(nodes[from].node.id, to),
12
12
  });
13
13
  return (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 4 }, children: [_jsxs(SectionLabel, { mono: true, children: [zone, locked && " · locked"] }), nodes.length === 0 && (_jsxs("div", { className: cx("sm-outline-row", "sm-outline-row--empty", canTake && "sm-outline-row--clickable"), onClick: () => { if (canTake)
14
- 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 }) => (_jsxs("div", { className: "sm-outline-row sm-outline-row--locked sm-outline-row--clickable", 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 }, i) => (_jsxs("div", { onClick: () => onSelect(node.id), style: sortable.rowStyle(i), className: cx("sm-outline-row", "sm-outline-row--clickable", "sm-outline-row--grip", node.id === selectedId && "sm-outline-row--selected", sortable.rowClass(i)), children: [_jsx(Grip, { ...sortable.handleProps(i) }), _jsx("span", { className: "sm-outline-row__label", children: label }), _jsxs("span", { style: { display: "flex", gap: 2, flex: "none" }, children: [_jsx(IconButton, { label: "Move up", size: 22, disabled: i === 0, onClick: (e) => { e.stopPropagation(); onMove(node.id, -1); }, children: "\u2191" }), _jsx(IconButton, { label: "Move down", size: 22, disabled: i === nodes.length - 1, onClick: (e) => { e.stopPropagation(); onMove(node.id, 1); }, children: "\u2193" }), _jsx(IconButton, { label: "Remove", size: 22, onClick: (e) => { e.stopPropagation(); onRemove(node.id); }, children: "\u00D7" })] })] }, node.id))) }))] }));
14
+ 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 }) => (_jsxs("div", { className: "sm-outline-row sm-outline-row--locked sm-outline-row--clickable", 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 }, i) => (_jsxs("div", { onClick: () => onSelect(node.id), style: sortable.rowStyle(i), className: cx("sm-outline-row", "sm-outline-row--clickable", "sm-outline-row--grip", node.id === selectedId && "sm-outline-row--selected", 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))) }))] }));
15
15
  }
@@ -1,27 +1,27 @@
1
1
  import type { EntryRecord, EntryUsage } from "../../entry-store.ts";
2
2
  import type { AdminOps, OpResult } from "../ops.ts";
3
3
  import { type SaveState } from "./autosave.ts";
4
- export type SharedEditItem = {
4
+ export type GlobalEditItem = {
5
5
  name: string;
6
6
  locale: string;
7
7
  record: EntryRecord;
8
- /** The merged draft fields; a visual item's styles under `$styles`. */
8
+ /** The merged draft fields; a visual item's settings under `$settings`. */
9
9
  fields: Record<string, unknown>;
10
10
  usage: EntryUsage[];
11
11
  /** The last save's field errors by dotted path; cleared by a successful save. */
12
12
  errors: Record<string, string>;
13
13
  };
14
- export type SharedEditState = {
15
- items: Record<string, SharedEditItem>;
14
+ export type GlobalEditState = {
15
+ items: Record<string, GlobalEditItem>;
16
16
  saveState: SaveState;
17
17
  loadError: string | null;
18
18
  };
19
- export type SharedEdit = {
20
- getState(): SharedEditState;
19
+ export type GlobalEdit = {
20
+ getState(): GlobalEditState;
21
21
  subscribe(listener: () => void): () => void;
22
22
  /** Load an item for a locale once; a repeat while loaded or loading is a no-op. */
23
23
  load(name: string, locale: string): Promise<void>;
24
- /** Set the value at a dotted path (`tagline`, `$styles.tone`, `columns.1.title`); a debounced save follows. */
24
+ /** Set the value at a dotted path (`tagline`, `$settings.tone`, `columns.1.title`); a debounced save follows. */
25
25
  patch(name: string, path: string, value: unknown): void;
26
26
  flush(): Promise<boolean>;
27
27
  /** Flush, then set the item's status in its locale; the record updates on success. */
@@ -30,10 +30,10 @@ export type SharedEdit = {
30
30
  reset(): void;
31
31
  dispose(): void;
32
32
  };
33
- export declare function createSharedEdit(opts: {
34
- ops: Pick<AdminOps, "shared">;
33
+ export declare function createGlobalEdit(opts: {
34
+ ops: Pick<AdminOps, "globals">;
35
35
  onSaved?: () => void;
36
36
  debounceMs?: number;
37
37
  retryBaseMs?: number;
38
38
  retryCapMs?: number;
39
- }): SharedEdit;
39
+ }): GlobalEdit;
@@ -1,6 +1,6 @@
1
1
  import { setAtPath } from "../field-paths.js";
2
2
  import { createDraftAutosave } from "./autosave.js";
3
- export function createSharedEdit(opts) {
3
+ export function createGlobalEdit(opts) {
4
4
  let items = {};
5
5
  let loadError = null;
6
6
  let generation = 0;
@@ -20,7 +20,7 @@ export function createSharedEdit(opts) {
20
20
  const at = rev.get(item.name) ?? 0;
21
21
  let r;
22
22
  try {
23
- r = await opts.ops.shared.save(item.name, item.locale, item.fields);
23
+ r = await opts.ops.globals.save(item.name, item.locale, item.fields);
24
24
  }
25
25
  catch {
26
26
  r = { ok: false, code: "internal", message: "Couldn't reach the server." };
@@ -62,7 +62,7 @@ export function createSharedEdit(opts) {
62
62
  const gen = generation;
63
63
  let r;
64
64
  try {
65
- r = await opts.ops.shared.get(name, locale);
65
+ r = await opts.ops.globals.get(name, locale);
66
66
  }
67
67
  catch {
68
68
  r = { ok: false, code: "internal", message: "Couldn't reach the server." };
@@ -98,7 +98,7 @@ export function createSharedEdit(opts) {
98
98
  }
99
99
  let r;
100
100
  try {
101
- r = await opts.ops.shared.setStatus(name, item.locale, status);
101
+ r = await opts.ops.globals.setStatus(name, item.locale, status);
102
102
  }
103
103
  catch {
104
104
  r = { ok: false, code: "internal", message: "Couldn't reach the server." };
@@ -0,0 +1,6 @@
1
+ import type { AdminOps } from "../ops.ts";
2
+ import { type GlobalEdit, type GlobalEditState } from "./global-edit.ts";
3
+ export declare function useGlobalEdit({ ops, onSaved }: {
4
+ ops: Pick<AdminOps, "globals">;
5
+ onSaved?: () => void;
6
+ }): GlobalEditState & Pick<GlobalEdit, "load" | "patch" | "flush" | "setStatus" | "reset">;
@@ -1,13 +1,13 @@
1
- // React glue for `createSharedEdit`: one controller per editor mount, the
1
+ // React glue for `createGlobalEdit`: one controller per editor mount, the
2
2
  // latest `onSaved` through a ref (it closes over the inline session), the
3
3
  // state through useSyncExternalStore. On unmount a pending save is
4
4
  // flushed first — the server call outlives the view — then the timers stop.
5
5
  import { useEffect, useRef, useState, useSyncExternalStore } from "react";
6
- import { createSharedEdit } from "./shared-edit.js";
7
- export function useSharedEdit({ ops, onSaved }) {
6
+ import { createGlobalEdit } from "./global-edit.js";
7
+ export function useGlobalEdit({ ops, onSaved }) {
8
8
  const latest = useRef(onSaved);
9
9
  latest.current = onSaved;
10
- const [controller] = useState(() => createSharedEdit({ ops, onSaved: () => latest.current?.() }));
10
+ const [controller] = useState(() => createGlobalEdit({ ops, onSaved: () => latest.current?.() }));
11
11
  useEffect(() => () => { void controller.flush(); controller.dispose(); }, [controller]);
12
12
  const state = useSyncExternalStore(controller.subscribe, controller.getState, controller.getState);
13
13
  return {
@@ -1,6 +1,11 @@
1
1
  import type { ReactElement, ReactNode } from "react";
2
2
  import type { AdminField } from "../serialize.ts";
3
- export declare function FieldWidget({ descriptor: d, value: valueProp, onChange, errors, path: pathProp, bare, refOptions, fieldKey, prefix, mark, inlinePath, onEditHere, }: {
3
+ /** The two namespaces a `fields.` / `settings.` condition reads (spec 2026-09-14 §12). */
4
+ export type WidgetScope = {
5
+ fields?: Record<string, unknown>;
6
+ settings?: Record<string, unknown>;
7
+ };
8
+ export declare function FieldWidget({ descriptor: d, value: valueProp, onChange, errors, path: pathProp, bare, refOptions, fieldKey, prefix, mark, inlinePath, onEditHere, scope, emptyLabel, }: {
4
9
  descriptor: AdminField;
5
10
  value: unknown;
6
11
  onChange(value: unknown): void;
@@ -21,4 +26,12 @@ export declare function FieldWidget({ descriptor: d, value: valueProp, onChange,
21
26
  /** The path of the field being edited in the canvas: its richtext widget yields to a note. */
22
27
  inlinePath?: string;
23
28
  onEditHere?(): void;
29
+ /** The node's top-level values, for a subfield hidden by `fields.x` / `settings.x`. */
30
+ scope?: WidgetScope;
31
+ /** An optional radio's empty pill; `None` unless the caller says otherwise (`Default` for a styles step). */
32
+ emptyLabel?: string;
24
33
  }): ReactElement;
34
+ /** The richtext field while it is edited in the canvas (spec 2026-09-10 §5). */
35
+ export declare function InlineNote({ onEditHere }: {
36
+ onEditHere?(): void;
37
+ }): import("react").JSX.Element;
@@ -3,16 +3,25 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import { RichtextWidget } from "./RichtextWidget.js";
4
4
  import { MediaWidget } from "./MediaWidget.js";
5
5
  import { ListField } from "./ListField.js";
6
+ import { RowsWidget } from "./RowsWidget.js";
6
7
  import { optionsOf } from "../../fields.js";
8
+ import { isHidden } from "../../conditions.js";
9
+ import { cssProblem } from "../../css.js";
7
10
  import { Button, Field, IconButton, Switch } from "../ui/primitives.js";
8
11
  import { patchObject } from "./list.js";
9
- const label = (key) => key.replace(/([A-Z])/g, " $1").replace(/^./, (c) => c.toUpperCase());
10
- export function FieldWidget({ descriptor: d, value: valueProp, onChange, errors, path: pathProp, bare, refOptions = [], fieldKey, prefix, mark, inlinePath, onEditHere, }) {
12
+ /** A key as a caption, sentence case: `spacingTop` → "Spacing top", `css` → "Custom CSS". */
13
+ const label = (key) => key === "css" ? "Custom CSS" : key.replace(/([A-Z])/g, (c) => ` ${c.toLowerCase()}`).replace(/^./, (c) => c.toUpperCase());
14
+ export function FieldWidget({ descriptor: d, value: valueProp, onChange, errors, path: pathProp, bare, refOptions = [], fieldKey, prefix, mark, inlinePath, onEditHere, scope, emptyLabel, }) {
11
15
  const path = pathProp ?? fieldKey;
12
16
  const message = errors?.[path];
13
17
  // The descriptor's default shows while the value is absent (spec 2026-09-10 page fields §1).
14
18
  const value = valueProp === undefined && d.default !== undefined ? d.default : valueProp;
15
19
  const optionTitle = (o) => o.status === "draft" ? `${o.title} (draft)` : o.title;
20
+ // The custom section's rows (spec 2026-09-14 §6): the row cards instead of the generic list.
21
+ if (d.widget === "rows") {
22
+ const rows = _jsx(RowsWidget, { descriptor: d, value: value, onChange: onChange, errors: errors, path: path, inlinePath: inlinePath, onEditHere: onEditHere });
23
+ return bare ? rows : _jsx(Field, { label: label(fieldKey), error: message, asDiv: true, path: path, children: rows });
24
+ }
16
25
  let control;
17
26
  let counter;
18
27
  let asDiv = d.type === "refList";
@@ -39,8 +48,23 @@ export function FieldWidget({ descriptor: d, value: valueProp, onChange, errors,
39
48
  break;
40
49
  }
41
50
  case "radio": {
42
- asDiv = true; // several inputs: never inside one implicit label
43
- control = (_jsx("div", { className: "sm-choices", role: "radiogroup", children: optionsOf(d.options).map((o) => (_jsxs("label", { className: "sm-choice", children: [_jsx("input", { type: "radio", name: path, value: o.value, checked: value === o.value, onChange: () => onChange(o.value) }), o.label] }, o.value))) }));
51
+ asDiv = true; // a row of buttons: never inside one implicit label
52
+ // Pills (spec 2026-09-14 §5); an optional radio starts with the empty pill, which stores nothing.
53
+ const empty = value === undefined || value === "";
54
+ const pills = [
55
+ ...(d.optional === true ? [{ value: undefined, label: emptyLabel ?? "None" }] : []),
56
+ ...optionsOf(d.options),
57
+ ];
58
+ control = (_jsx("div", { className: "sm-pills", role: "radiogroup", children: pills.map((o) => (_jsx("button", { type: "button", className: "sm-pill", "aria-pressed": o.value === undefined ? empty : value === o.value, onClick: () => onChange(o.value), children: o.label }, o.value ?? ""))) }));
59
+ break;
60
+ }
61
+ case "css": {
62
+ // The custom-CSS box (spec 2026-09-14 §4): the refusal shows as it is typed; publish refuses it too.
63
+ const text = typeof value === "string" ? value : "";
64
+ const problem = text === "" ? null : cssProblem(text);
65
+ control = (_jsx("textarea", { className: "sm-textarea sm-css", rows: 6, spellCheck: false, value: text, onChange: (e) => onChange(e.target.value === "" ? undefined : e.target.value) }));
66
+ if (problem)
67
+ return _jsx(Field, { label: label(fieldKey), error: problem, path: path, children: control });
44
68
  break;
45
69
  }
46
70
  case "checkboxes": {
@@ -77,17 +101,20 @@ export function FieldWidget({ descriptor: d, value: valueProp, onChange, errors,
77
101
  case "object": {
78
102
  asDiv = true;
79
103
  const obj = (value && typeof value === "object" && !Array.isArray(value) ? value : {});
80
- control = (_jsx("div", { className: "sm-group", children: Object.entries(d.fields ?? {}).map(([k, sub]) => (_jsx(FieldWidget, { fieldKey: k, descriptor: sub, value: obj[k], errors: errors, path: `${path}.${k}`, inlinePath: inlinePath, onEditHere: onEditHere, onChange: (v) => onChange(patchObject(obj, k, v, d.optional === true)) }, k))) }));
104
+ control = (_jsx("div", { className: "sm-group", children: Object.entries(d.fields ?? {})
105
+ .filter(([, sub]) => !isHidden(sub, { self: obj, fields: scope?.fields, settings: scope?.settings }))
106
+ .map(([k, sub]) => (_jsx(FieldWidget, { fieldKey: k, descriptor: sub, value: obj[k], errors: errors, path: `${path}.${k}`, inlinePath: inlinePath, onEditHere: onEditHere, scope: scope, onChange: (v) => onChange(patchObject(obj, k, v, d.optional === true)) }, k))) }));
81
107
  break;
82
108
  }
83
109
  case "list": {
84
- asDiv = true;
110
+ // The list renders its own head (label, counter, Collapse all) so the toggle sits beside the label.
85
111
  const items = Array.isArray(value) ? value : [];
86
112
  const item = d.item ?? { type: "text" };
87
- if (typeof d.max === "number")
88
- counter = `${items.length} of ${d.max}`;
89
- control = (_jsx(ListField, { descriptor: d, items: items, onChange: onChange, path: path, renderItem: (it, i) => (_jsx(FieldWidget, { fieldKey: String(i), descriptor: item, value: it, errors: errors, path: `${path}.${i}`, bare: true, inlinePath: inlinePath, onEditHere: onEditHere, onChange: (v) => onChange(items.map((x, j) => (j === i ? v : x))) })) }));
90
- break;
113
+ const count = typeof d.max === "number" ? `${items.length} of ${d.max}` : undefined;
114
+ const list = (_jsx(ListField, { descriptor: d, items: items, onChange: onChange, path: path, head: bare ? undefined : { label: label(fieldKey), mark, counter: count, error: message }, renderItem: (it, i) => (_jsx(FieldWidget, { fieldKey: String(i), descriptor: item, value: it, errors: errors, path: `${path}.${i}`, bare: true, inlinePath: inlinePath, onEditHere: onEditHere, scope: scope, onChange: (v) => onChange(items.map((x, j) => (j === i ? v : x))) })) }));
115
+ if (!bare)
116
+ return list;
117
+ return _jsxs(_Fragment, { children: [list, message && _jsx("span", { className: "sm-error", children: message })] });
91
118
  }
92
119
  default:
93
120
  control = _jsx("textarea", { className: "sm-textarea", disabled: true, value: JSON.stringify(value ?? null, null, 2), rows: 2 });
@@ -101,6 +128,6 @@ export function FieldWidget({ descriptor: d, value: valueProp, onChange, errors,
101
128
  return (_jsx(Field, { label: label(fieldKey), mark: mark, counter: counter, error: message, asDiv: asDiv, path: path, children: control }));
102
129
  }
103
130
  /** The richtext field while it is edited in the canvas (spec 2026-09-10 §5). */
104
- function InlineNote({ onEditHere }) {
131
+ export function InlineNote({ onEditHere }) {
105
132
  return (_jsxs("div", { className: "sm-inline-note", children: [_jsx("span", { children: "Editing in the canvas" }), _jsx(Button, { variant: "secondary", onClick: onEditHere, children: "Edit here" })] }));
106
133
  }
@@ -1,10 +1,18 @@
1
1
  import { type ReactNode } from "react";
2
2
  import type { AdminField } from "../serialize.ts";
3
- export declare function ListField({ descriptor: d, items, onChange, renderItem, path }: {
3
+ export declare function ListField({ descriptor: d, items, onChange, renderItem, path, head }: {
4
4
  descriptor: AdminField;
5
5
  items: unknown[];
6
6
  onChange(value: unknown[]): void;
7
7
  renderItem(item: unknown, index: number): ReactNode;
8
8
  /** The list's dotted path; each row carries `<path>.<i>` for the canvas reveal. */
9
9
  path?: string;
10
+ /** The field head to render around the rows (label, mark, counter, error), so the
11
+ * collapse toggle can sit in it; absent for a bare list (a list item). */
12
+ head?: {
13
+ label: ReactNode;
14
+ mark?: ReactNode;
15
+ counter?: ReactNode;
16
+ error?: ReactNode;
17
+ };
10
18
  }): import("react").JSX.Element;
@@ -1,15 +1,16 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
- // A list field's rows: collapse per row, add and remove, and reordering
4
- // by drag (useSortable), by the arrow keys on the grip, or by the ↑/↓
5
- // buttons — every path through the pure helpers in ./list.ts. The item
6
- // widget is rendered by the caller (FieldWidget), which keeps this file
7
- // free of the recursion.
3
+ // A list field's rows: collapse per row (and all at once from the head's
4
+ // Collapse all / Expand all), add and remove, and reordering by drag
5
+ // (useSortable), by the arrow keys on the grip, or by the ↑/↓ buttons —
6
+ // every path through the pure helpers in ./list.ts. The item widget is
7
+ // rendered by the caller (FieldWidget), which keeps this file free of
8
+ // the recursion.
8
9
  import { useState } from "react";
9
- import { Button, Grip, IconButton } from "../ui/primitives.js";
10
+ import { Button, Field, Grip, IconButton } from "../ui/primitives.js";
10
11
  import { useSortable } from "../ui/useSortable.js";
11
12
  import { addItem, itemSummary, moveIndex, moveItem, removeIndex, removeItem } from "./list.js";
12
- export function ListField({ descriptor: d, items, onChange, renderItem, path }) {
13
+ export function ListField({ descriptor: d, items, onChange, renderItem, path, head }) {
13
14
  // Keyed to the item's index rather than to `key={i}` position, so a move
14
15
  // or remove doesn't hand a row someone else's open/closed state.
15
16
  const [collapsed, setCollapsed] = useState(() => new Set());
@@ -22,7 +23,10 @@ export function ListField({ descriptor: d, items, onChange, renderItem, path })
22
23
  setCollapsed((prev) => moveIndex(prev, from, to));
23
24
  };
24
25
  const sortable = useSortable({ count: items.length, onDrop: move });
25
- return (_jsxs("div", { className: "sm-list", ref: sortable.containerRef, children: [items.map((it, i) => (_jsx(ListRow, { index: i, count: items.length, summary: itemSummary(it, i), open: !collapsed.has(i), path: path === undefined ? undefined : `${path}.${i}`, style: sortable.rowStyle(i), className: sortable.rowClass(i), grip: _jsx(Grip, { ...sortable.handleProps(i) }), onToggle: () => setCollapsed((prev) => {
26
+ // One toggle for the whole list: Collapse all until every row is closed, then Expand all.
27
+ const allCollapsed = items.length > 0 && items.every((_, i) => collapsed.has(i));
28
+ const toggleAll = items.length > 1 && (_jsx("button", { type: "button", className: "sm-field__action", onClick: () => setCollapsed(allCollapsed ? new Set() : new Set(items.map((_, i) => i))), children: allCollapsed ? "Expand all" : "Collapse all" }));
29
+ const rows = (_jsxs("div", { className: "sm-list", ref: sortable.containerRef, children: [items.map((it, i) => (_jsx(ListRow, { index: i, count: items.length, summary: itemSummary(it, i), open: !collapsed.has(i), path: path === undefined ? undefined : `${path}.${i}`, style: sortable.rowStyle(i), className: sortable.rowClass(i), grip: _jsx(Grip, { ...sortable.handleProps(i) }), onToggle: () => setCollapsed((prev) => {
26
30
  const next = new Set(prev);
27
31
  if (next.has(i))
28
32
  next.delete(i);
@@ -33,6 +37,10 @@ export function ListField({ descriptor: d, items, onChange, renderItem, path })
33
37
  onChange(removeItem(items, i));
34
38
  setCollapsed((prev) => removeIndex(prev, i));
35
39
  }, children: renderItem(it, i) }, i))), _jsx(Button, { variant: "secondary", disabled: atMax, onClick: () => onChange(addItem(items, d)), children: "Add" })] }));
40
+ if (!head)
41
+ return rows;
42
+ // A div, not a label: a label would press the first row's button on any click.
43
+ return (_jsx(Field, { label: head.label, mark: head.mark, counter: head.counter, action: toggleAll || undefined, error: head.error, asDiv: true, path: path, children: rows }));
36
44
  }
37
45
  /** One list item: a header with the grip, the summary and the row
38
46
  * controls, the item's widget underneath while expanded. Header is a
@@ -2,16 +2,19 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  // The element node view (spec 2026-09-08 block nodes §3): the node view IS
4
4
  // the form — a header (icon, title, collapse, delete) and the element's
5
- // fields then styles through FieldWidget, reading and writing attrs.
5
+ // fields then settings through FieldWidget, reading and writing attrs.
6
6
  // A richtext field inside mounts a nested RichtextWidget with its own
7
- // toolset. Errors come from the enclosing form under
7
+ // toolset. Settings sit under a divider; a field hidden by a condition
8
+ // leaves the card — the element's own values are the scope (spec
9
+ // 2026-09-14 §12). Errors come from the enclosing form under
8
10
  // `<path>.elements.<i>.<key>`, i being this node's index among the
9
11
  // document's element nodes. Collapse is component state only: a card
10
12
  // starts collapsed (2026-09-09) unless it holds nothing yet — a freshly
11
13
  // inserted element opens, so its fields can be filled straight away.
12
14
  import { useState } from "react";
13
15
  import { NodeViewWrapper } from "@tiptap/react";
14
- import { IconButton } from "../ui/primitives.js";
16
+ import { isHidden } from "../../conditions.js";
17
+ import { IconButton, SectionLabel } from "../ui/primitives.js";
15
18
  import { FieldWidget } from "./FieldWidget.js";
16
19
  import { useElements } from "./elements-context.js";
17
20
  import { cardClass, useRichtextForm } from "./richtext-form-context.js";
@@ -26,12 +29,15 @@ export function RichtextElementCard({ node, editor, getPos, updateAttributes, de
26
29
  const name = String(node.attrs.name ?? "");
27
30
  const element = elements.find((e) => e.name === name);
28
31
  const fields = (node.attrs.fields && typeof node.attrs.fields === "object" ? node.attrs.fields : {});
29
- const styles = (node.attrs.styles && typeof node.attrs.styles === "object" ? node.attrs.styles : {});
32
+ const settings = (node.attrs.settings && typeof node.attrs.settings === "object" ? node.attrs.settings : {});
30
33
  const [open, setOpen] = useState(() => isBlank(fields));
31
34
  const pos = getPos();
32
35
  const index = typeof pos === "number" ? elementIndexAt(editor.state.doc, pos) : 0;
33
36
  const base = `${form.path}.elements.${index}`;
34
37
  const select = () => { if (typeof pos === "number")
35
38
  editor.commands.setNodeSelection(pos); };
36
- return (_jsxs(NodeViewWrapper, { className: cardClass(selected, form.canvas), "data-theme": form.canvas ? "light" : undefined, children: [_jsxs("div", { className: "sm-rtcard__head", "data-drag-handle": true, onClick: select, children: [_jsx("span", { className: "sm-rtcard__icon", "aria-hidden": "true", children: element?.icon ?? "▤" }), _jsx("span", { className: "sm-rtcard__title", children: element?.title ?? `Unknown element "${name}"` }), _jsx(IconButton, { label: open ? "Collapse" : "Expand", onClick: (e) => { e.stopPropagation(); setOpen((o) => !o); }, children: open ? "▾" : "▸" }), _jsx(IconButton, { label: "Delete element", onClick: (e) => { e.stopPropagation(); deleteNode(); }, children: "\u00D7" })] }), open && element && (_jsxs("div", { className: "sm-rtcard__body", contentEditable: false, children: [Object.entries(element.fields).map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: fields[key], errors: form.errors, path: `${base}.${key}`, onChange: (v) => updateAttributes({ fields: { ...fields, [key]: v } }) }, key))), Object.entries(element.styles).map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: styles[key], path: `${base}.styles.${key}`, onChange: (v) => updateAttributes({ styles: { ...styles, [key]: v } }) }, `style-${key}`)))] }))] }));
39
+ const scope = { fields, settings };
40
+ const visibleFields = Object.entries(element?.fields ?? {}).filter(([, d]) => !isHidden(d, { self: fields, ...scope }));
41
+ const visibleSettings = Object.entries(element?.settings ?? {}).filter(([, d]) => !isHidden(d, { self: settings, ...scope }));
42
+ return (_jsxs(NodeViewWrapper, { className: cardClass(selected, form.canvas), "data-theme": form.canvas ? "light" : undefined, children: [_jsxs("div", { className: "sm-rtcard__head", "data-drag-handle": true, onClick: select, children: [_jsx("span", { className: "sm-rtcard__icon", "aria-hidden": "true", children: element?.icon ?? "▤" }), _jsx("span", { className: "sm-rtcard__title", children: element?.title ?? `Unknown element "${name}"` }), _jsx(IconButton, { label: open ? "Collapse" : "Expand", onClick: (e) => { e.stopPropagation(); setOpen((o) => !o); }, children: open ? "▾" : "▸" }), _jsx(IconButton, { label: "Delete element", onClick: (e) => { e.stopPropagation(); deleteNode(); }, children: "\u00D7" })] }), open && element && (_jsxs("div", { className: "sm-rtcard__body", contentEditable: false, children: [visibleFields.map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: fields[key], errors: form.errors, path: `${base}.${key}`, scope: scope, onChange: (v) => updateAttributes({ fields: { ...fields, [key]: v } }) }, key))), visibleSettings.length > 0 && _jsx(SectionLabel, { mono: true, children: "Settings" }), visibleSettings.map(([key, d]) => (_jsx(FieldWidget, { fieldKey: key, descriptor: d, value: settings[key], errors: form.errors, path: `${base}.settings.${key}`, scope: scope, onChange: (v) => updateAttributes({ settings: { ...settings, [key]: v } }) }, `setting-${key}`)))] }))] }));
37
43
  }
@@ -0,0 +1,13 @@
1
+ import type { ReactElement } from "react";
2
+ import type { AdminField } from "../serialize.ts";
3
+ import { type CustomRow } from "../../custom/index.ts";
4
+ export declare function RowsWidget({ descriptor, value, onChange, errors, path, inlinePath, onEditHere }: {
5
+ descriptor: AdminField;
6
+ value: unknown;
7
+ onChange(rows: CustomRow[]): void;
8
+ errors?: Record<string, string>;
9
+ path: string;
10
+ /** The path of the column being edited in the canvas: its widget yields to a note. */
11
+ inlinePath?: string;
12
+ onEditHere?(): void;
13
+ }): ReactElement;
@@ -0,0 +1,26 @@
1
+ "use client";
2
+ import { jsxs as _jsxs, jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
+ import { Button, Field, IconButton, Switch } from "../ui/primitives.js";
4
+ import { FieldWidget, InlineNote } from "./FieldWidget.js";
5
+ import { RichtextWidget } from "./RichtextWidget.js";
6
+ import { SpanBar } from "./SpanBar.js";
7
+ import { moveItem, removeItem } from "./list.js";
8
+ import { MAX_COLUMNS, defaultRow, presetsFor, setColumnCount } from "../../custom/index.js";
9
+ const isRow = (r) => typeof r === "object" && r !== null && Array.isArray(r.columns) && Array.isArray(r.spans);
10
+ export function RowsWidget({ descriptor, value, onChange, errors, path, inlinePath, onEditHere }) {
11
+ const rows = Array.isArray(value) ? value.filter(isRow) : [];
12
+ const item = descriptor.item?.fields ?? {};
13
+ const columnDescriptor = (item.columns?.item ?? { type: "richtext" });
14
+ const patch = (i, row) => onChange(rows.map((r, j) => (j === i ? row : r)));
15
+ return (_jsxs("div", { className: "sm-rowcards", children: [rows.map((row, i) => {
16
+ const n = row.columns.length;
17
+ const base = `${path}.${i}`;
18
+ const presets = presetsFor(n);
19
+ return (_jsxs("div", { className: "sm-rowcard", "data-sm-path": base, children: [_jsxs("div", { className: "sm-rowcard__head", children: [_jsxs("span", { className: "sm-rowcard__title", children: ["Row ", i + 1] }), _jsx(IconButton, { label: "Move up", disabled: i === 0, onClick: () => onChange(moveItem(rows, i, i - 1)), children: "\u2191" }), _jsx(IconButton, { label: "Move down", disabled: i === rows.length - 1, onClick: () => onChange(moveItem(rows, i, i + 1)), children: "\u2193" }), _jsx(IconButton, { label: "Remove row", disabled: rows.length === 1, onClick: () => onChange(removeItem(rows, i)), children: "\u00D7" })] }), _jsx(Field, { label: "Columns", asDiv: true, error: errors?.[`${base}.columns`] ?? errors?.[`${base}.spans`], children: _jsxs("div", { className: "sm-stepper", children: [_jsx(IconButton, { label: "Fewer columns", disabled: n <= 1, onClick: () => patch(i, setColumnCount(row, n - 1)), children: "\u2212" }), _jsx("span", { className: "sm-stepper__value", children: n }), _jsx(IconButton, { label: "More columns", disabled: n >= MAX_COLUMNS, onClick: () => patch(i, setColumnCount(row, n + 1)), children: "+" })] }) }), n > 1 && (_jsxs(_Fragment, { children: [_jsxs(Field, { label: "Widths", asDiv: true, path: `${base}.spans`, children: [_jsx(SpanBar, { spans: row.spans, onChange: (spans) => patch(i, { ...row, spans }) }), presets.length > 0 && (_jsx("div", { className: "sm-rowcard__presets", children: presets.map((p) => (_jsx(Button, { variant: "secondary", onClick: () => patch(i, { ...row, spans: p.spans }), children: p.label }, p.label))) }))] }), _jsx(FieldWidget, { fieldKey: "gap", descriptor: item.gap, value: row.gap, path: `${base}.gap`, onChange: (v) => patch(i, { ...row, gap: v }) }), _jsx(FieldWidget, { fieldKey: "verticalAlign", descriptor: item.align, value: row.align, path: `${base}.align`, onChange: (v) => patch(i, { ...row, align: v }) }), _jsx(Field, { label: "Reverse on mobile", asDiv: true, path: `${base}.reverse`, children: _jsx(Switch, { checked: row.reverse === true, onChange: (v) => patch(i, { ...row, reverse: v }), label: "Reverse on mobile" }) })] })), row.columns.map((doc, j) => {
20
+ const colPath = `${base}.columns.${j}`;
21
+ return (_jsx(Field, { label: `Column ${j + 1}`, asDiv: true, path: colPath, error: errors?.[colPath], children: inlinePath === colPath
22
+ ? _jsx(InlineNote, { onEditHere: onEditHere })
23
+ : _jsx(RichtextWidget, { value: doc, toolset: columnDescriptor.toolset, errors: errors, path: colPath, onChange: (next) => patch(i, { ...row, columns: row.columns.map((c, k) => (k === j ? next : c)) }) }) }, j));
24
+ })] }, i));
25
+ }), _jsx(Button, { variant: "secondary", onClick: () => onChange([...rows, defaultRow()]), children: "+ Add row" })] }));
26
+ }
@@ -0,0 +1,5 @@
1
+ import { type ReactElement } from "react";
2
+ export declare function SpanBar({ spans, onChange }: {
3
+ spans: number[];
4
+ onChange(spans: number[]): void;
5
+ }): ReactElement;
@@ -0,0 +1,37 @@
1
+ "use client";
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ // The width bar (spec 2026-09-14 §6): one box per column sized by its
4
+ // span, a handle on each boundary dragged in twelfths (pointer events on
5
+ // the bar's own width — no library) or nudged with the arrow keys.
6
+ import { useRef } from "react";
7
+ import { setSpan, spanAtX } from "../../custom/index.js";
8
+ export function SpanBar({ spans, onChange }) {
9
+ const bar = useRef(null);
10
+ const drag = (handle) => (e) => {
11
+ const el = bar.current;
12
+ if (!el)
13
+ return;
14
+ e.preventDefault();
15
+ e.currentTarget.setPointerCapture(e.pointerId);
16
+ const rect = el.getBoundingClientRect();
17
+ let current = spans;
18
+ const move = (ev) => {
19
+ const next = setSpan(current, handle, spanAtX(ev.clientX - rect.left, rect.width, current, handle));
20
+ if (next[handle] !== current[handle]) {
21
+ current = next;
22
+ onChange(next);
23
+ }
24
+ };
25
+ const up = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
26
+ window.addEventListener("pointermove", move);
27
+ window.addEventListener("pointerup", up);
28
+ };
29
+ const key = (handle) => (e) => {
30
+ const delta = e.key === "ArrowLeft" ? -1 : e.key === "ArrowRight" ? 1 : 0;
31
+ if (delta === 0)
32
+ return;
33
+ e.preventDefault();
34
+ onChange(setSpan(spans, handle, spans[handle] + delta));
35
+ };
36
+ return (_jsx("div", { className: "sm-spanbar", ref: bar, children: spans.map((s, i) => (_jsxs("div", { className: "sm-spanbar__col", style: { flex: s }, children: [s, i < spans.length - 1 && (_jsx("button", { type: "button", className: "sm-spanbar__handle", role: "slider", "aria-label": `Resize columns ${i + 1} and ${i + 2}`, "aria-valuemin": 1, "aria-valuemax": s + spans[i + 1] - 1, "aria-valuenow": s, onPointerDown: drag(i), onKeyDown: key(i) }))] }, `c${i}`))) }));
37
+ }
@@ -18,7 +18,7 @@ export type ElementNodeJSON = {
18
18
  attrs: {
19
19
  name: string;
20
20
  fields: Record<string, unknown>;
21
- styles: Record<string, unknown>;
21
+ settings: Record<string, unknown>;
22
22
  };
23
23
  };
24
24
  export type ImageNodeJSON = {
@@ -31,7 +31,7 @@ export type ImageNodeJSON = {
31
31
  caption: string;
32
32
  };
33
33
  };
34
- /** A fresh element: sample first, then the empties, styles from their defaults — as a section insertion does. */
34
+ /** A fresh element: sample first, then the empties, settings from their defaults — as a section insertion does. */
35
35
  export declare function elementNode(element: AdminElement): ElementNodeJSON;
36
36
  export declare function imageNode(assetId: string): ImageNodeJSON;
37
37
  /** How many element nodes precede `pos` — the validator's `elements.<i>` index. */
@@ -15,10 +15,10 @@ export function filterItems(items, query) {
15
15
  const q = query.trim().toLowerCase();
16
16
  return q === "" ? items : items.filter((i) => i.title.toLowerCase().includes(q));
17
17
  }
18
- /** A fresh element: sample first, then the empties, styles from their defaults — as a section insertion does. */
18
+ /** A fresh element: sample first, then the empties, settings from their defaults — as a section insertion does. */
19
19
  export function elementNode(element) {
20
- const { fields, styles } = nodeFromSample(element, "");
21
- return { type: "element", attrs: { name: element.name, fields, styles: styles ?? {} } };
20
+ const { fields, settings } = nodeFromSample(element, "");
21
+ return { type: "element", attrs: { name: element.name, fields, settings: settings ?? {} } };
22
22
  }
23
23
  export function imageNode(assetId) {
24
24
  return { type: "image", attrs: { image: { asset: assetId }, caption: "" } };
@@ -36,7 +36,7 @@ export const ElementNode = Node.create({
36
36
  atom: true,
37
37
  draggable: true,
38
38
  addAttributes() {
39
- return { name: { default: "" }, fields: jsonAttr("fields", {}), styles: jsonAttr("styles", {}) };
39
+ return { name: { default: "" }, fields: jsonAttr("fields", {}), settings: jsonAttr("settings", {}) };
40
40
  },
41
41
  parseHTML() { return [{ tag: "div[data-smoodly-element]" }]; },
42
42
  renderHTML({ HTMLAttributes }) { return ["div", mergeAttributes({ "data-smoodly-element": "" }, HTMLAttributes)]; },