smoodly 0.0.15 → 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.
- package/README.md +238 -36
- package/dist/admin/auth.d.ts +7 -4
- package/dist/admin/auth.js +1 -1
- package/dist/admin/client-ops.js +3 -3
- package/dist/admin/custom-section.d.ts +10 -0
- package/dist/admin/custom-section.js +10 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +6 -6
- package/dist/admin/editor/EditorView.d.ts +2 -0
- package/dist/admin/editor/EditorView.js +322 -62
- package/dist/admin/editor/GlobalInspector.d.ts +24 -0
- package/dist/admin/editor/GlobalInspector.js +13 -0
- package/dist/admin/editor/InsertPicker.d.ts +6 -5
- package/dist/admin/editor/InsertPicker.js +6 -4
- package/dist/admin/editor/InspectorFields.d.ts +21 -0
- package/dist/admin/editor/InspectorFields.js +22 -0
- package/dist/admin/editor/Outline.d.ts +0 -1
- package/dist/admin/editor/Outline.js +3 -3
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/global-edit.d.ts +39 -0
- package/dist/admin/editor/global-edit.js +124 -0
- package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
- package/dist/admin/editor/useGlobalEdit.js +18 -0
- package/dist/admin/forms/FieldWidget.d.ts +14 -1
- package/dist/admin/forms/FieldWidget.js +38 -11
- package/dist/admin/forms/ListField.d.ts +9 -1
- package/dist/admin/forms/ListField.js +16 -8
- package/dist/admin/forms/RichtextElementCard.js +11 -5
- package/dist/admin/forms/RowsWidget.d.ts +13 -0
- package/dist/admin/forms/RowsWidget.js +26 -0
- package/dist/admin/forms/SpanBar.d.ts +5 -0
- package/dist/admin/forms/SpanBar.js +37 -0
- package/dist/admin/forms/richtext-insert.d.ts +2 -2
- package/dist/admin/forms/richtext-insert.js +3 -3
- package/dist/admin/forms/richtext-nodes.js +1 -1
- package/dist/admin/forms/tabs.d.ts +20 -8
- package/dist/admin/forms/tabs.js +19 -23
- package/dist/admin/forms/useRichtextEditor.js +8 -1
- package/dist/admin/global-items.d.ts +9 -0
- package/dist/admin/{shared-items.js → global-items.js} +29 -14
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/next/op-handler.js +4 -3
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +242 -56
- package/dist/admin/ops.d.ts +38 -6
- package/dist/admin/serialize.d.ts +18 -7
- package/dist/admin/serialize.js +27 -5
- package/dist/admin/shell/AdminApp.js +7 -7
- package/dist/admin/shell/EntriesList.js +19 -3
- package/dist/admin/shell/EntryForm.js +183 -77
- package/dist/admin/shell/GlobalFields.d.ts +24 -0
- package/dist/admin/shell/GlobalFields.js +19 -0
- package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
- package/dist/admin/shell/GlobalForm.js +258 -0
- package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
- package/dist/admin/shell/{SharedList.js → GlobalList.js} +12 -11
- package/dist/admin/shell/IconRail.js +14 -7
- package/dist/admin/shell/PagesList.js +4 -8
- package/dist/admin/shell/entries-list.d.ts +2 -2
- package/dist/admin/shell/entries-list.js +3 -3
- package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
- package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +10 -8
- package/dist/admin/tree-ops.js +28 -16
- package/dist/admin/ui/ActionMenu.d.ts +11 -0
- package/dist/admin/ui/ActionMenu.js +25 -0
- package/dist/admin/ui/HistoryModal.d.ts +23 -0
- package/dist/admin/ui/HistoryModal.js +40 -0
- package/dist/admin/ui/Modal.d.ts +5 -1
- package/dist/admin/ui/Modal.js +3 -3
- package/dist/admin/ui/PublishControl.d.ts +31 -0
- package/dist/admin/ui/PublishControl.js +24 -0
- package/dist/admin/ui/format.d.ts +4 -0
- package/dist/admin/ui/format.js +9 -0
- package/dist/admin/ui/primitives.d.ts +16 -1
- package/dist/admin/ui/primitives.js +20 -4
- package/dist/admin/ui/status-label.d.ts +10 -0
- package/dist/admin/ui/status-label.js +14 -0
- package/dist/admin/ui/theme.js +93 -13
- package/dist/admin/validate.d.ts +27 -2
- package/dist/admin/validate.js +47 -10
- package/dist/collections.d.ts +3 -9
- package/dist/collections.js +1 -2
- package/dist/conditions.d.ts +33 -0
- package/dist/conditions.js +79 -0
- package/dist/config.d.ts +26 -19
- package/dist/config.js +26 -25
- package/dist/css.d.ts +7 -0
- package/dist/css.js +57 -0
- package/dist/custom/CustomSection.d.ts +26 -0
- package/dist/custom/CustomSection.js +20 -0
- package/dist/custom/css.d.ts +3 -0
- package/dist/custom/css.js +28 -0
- package/dist/custom/index.d.ts +26 -0
- package/dist/custom/index.js +16 -0
- package/dist/custom/rows.d.ts +28 -0
- package/dist/custom/rows.js +129 -0
- package/dist/custom/schema.d.ts +32 -0
- package/dist/custom/schema.js +39 -0
- package/dist/entry-store.d.ts +42 -17
- package/dist/entry-store.js +107 -40
- package/dist/fields.d.ts +5 -2
- package/dist/fields.js +4 -2
- package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
- package/dist/{shared-id.js → global-id.js} +4 -4
- package/dist/global.d.ts +57 -0
- package/dist/global.js +51 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.js +10 -5
- package/dist/media.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/page-renderer.d.ts +4 -2
- package/dist/next/page-renderer.js +17 -13
- package/dist/next/queries.d.ts +6 -6
- package/dist/next/queries.js +7 -7
- package/dist/page.js +2 -2
- package/dist/pairing.d.ts +13 -7
- package/dist/pairing.js +4 -4
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +4 -4
- package/dist/render.d.ts +13 -1
- package/dist/render.js +10 -5
- package/dist/resolve.d.ts +11 -6
- package/dist/resolve.js +22 -12
- package/dist/revalidate.d.ts +4 -4
- package/dist/revalidate.js +4 -4
- package/dist/richtext-render.js +2 -2
- package/dist/richtext-walk.d.ts +5 -5
- package/dist/richtext-walk.js +4 -4
- package/dist/schema.d.ts +15 -10
- package/dist/schema.js +19 -7
- package/dist/section-wrapper.d.ts +18 -6
- package/dist/section-wrapper.js +24 -17
- package/dist/site.d.ts +17 -5
- package/dist/site.js +36 -12
- package/dist/sql-space.js +1 -1
- package/dist/sql.js +19 -16
- package/dist/store.d.ts +45 -13
- package/dist/store.js +88 -12
- package/dist/styles.d.ts +27 -0
- package/dist/styles.js +60 -0
- package/dist/supabase-entry-store.d.ts +11 -6
- package/dist/supabase-entry-store.js +115 -39
- package/dist/supabase-store.d.ts +13 -6
- package/dist/supabase-store.js +90 -20
- package/dist/zones.d.ts +9 -7
- package/dist/zones.js +5 -5
- package/package.json +1 -1
- package/dist/admin/shared-items.d.ts +0 -9
- package/dist/admin/shell/SharedForm.js +0 -134
- package/dist/shared.d.ts +0 -46
- package/dist/shared.js +0 -36
|
@@ -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
|
|
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.
|
|
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 {
|
|
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
|
|
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
|
-
|
|
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,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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
21
|
-
return { type: "element", attrs: { name: element.name, fields,
|
|
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", {}),
|
|
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)]; },
|
|
@@ -1,14 +1,26 @@
|
|
|
1
|
-
import type { AdminField
|
|
2
|
-
export type TabName = "Content" | "
|
|
1
|
+
import type { AdminField } from "../serialize.ts";
|
|
2
|
+
export type TabName = "Content" | "Settings";
|
|
3
|
+
export type GroupTitle = "Spacing & layout" | "Advanced";
|
|
3
4
|
export type TabEntry = {
|
|
4
5
|
key: string;
|
|
5
|
-
ns: "fields" | "styles" | "
|
|
6
|
+
ns: "fields" | "settings" | "styles" | "css";
|
|
6
7
|
descriptor: AdminField;
|
|
7
8
|
};
|
|
8
|
-
export
|
|
9
|
-
|
|
9
|
+
export type Group = {
|
|
10
|
+
title?: GroupTitle;
|
|
10
11
|
entries: TabEntry[];
|
|
12
|
+
};
|
|
13
|
+
/** A section, an element (no styles, no css) or a global item. */
|
|
14
|
+
export type FieldsHolder = {
|
|
15
|
+
fields: Record<string, AdminField>;
|
|
16
|
+
settings?: Record<string, AdminField>;
|
|
17
|
+
styles?: Record<string, AdminField>;
|
|
18
|
+
css?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export declare const CSS_DESCRIPTOR: AdminField;
|
|
21
|
+
export declare function tabsFor(holder: FieldsHolder): {
|
|
22
|
+
tab: TabName;
|
|
23
|
+
groups: Group[];
|
|
11
24
|
}[];
|
|
12
|
-
/** The tab
|
|
13
|
-
|
|
14
|
-
export declare function tabOf(section: AdminSection, path: string): TabName | undefined;
|
|
25
|
+
/** The tab a dotted path belongs to, by its top-level key — a canvas click reveals it there. */
|
|
26
|
+
export declare function tabOf(holder: FieldsHolder, path: string): TabName | undefined;
|
package/dist/admin/forms/tabs.js
CHANGED
|
@@ -1,26 +1,22 @@
|
|
|
1
|
-
const
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
};
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const tab = override ?? (MEDIA_TYPES.has(descriptor.type) ? "Media" : TAB_BY_NS[ns]);
|
|
14
|
-
buckets[tab].push({ key, ns, descriptor });
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
return ["Content", "Styles", "Media", "Meta"]
|
|
18
|
-
.filter((tab) => buckets[tab].length > 0)
|
|
19
|
-
.map((tab) => ({ tab, entries: buckets[tab] }));
|
|
1
|
+
export const CSS_DESCRIPTOR = { type: "css" };
|
|
2
|
+
const entries = (ns, map) => Object.entries(map ?? {}).map(([key, descriptor]) => ({ key, ns, descriptor }));
|
|
3
|
+
export function tabsFor(holder) {
|
|
4
|
+
const content = [{ entries: entries("fields", holder.fields) }];
|
|
5
|
+
const settings = [
|
|
6
|
+
{ entries: entries("settings", holder.settings) },
|
|
7
|
+
{ title: "Spacing & layout", entries: entries("styles", holder.styles) },
|
|
8
|
+
{ title: "Advanced", entries: holder.css === true ? [{ key: "css", ns: "css", descriptor: CSS_DESCRIPTOR }] : [] },
|
|
9
|
+
];
|
|
10
|
+
return [["Content", content], ["Settings", settings]]
|
|
11
|
+
.map(([tab, groups]) => ({ tab, groups: groups.filter((g) => g.entries.length > 0) }))
|
|
12
|
+
.filter((t) => t.groups.length > 0);
|
|
20
13
|
}
|
|
21
|
-
/** The tab
|
|
22
|
-
|
|
23
|
-
export function tabOf(section, path) {
|
|
14
|
+
/** The tab a dotted path belongs to, by its top-level key — a canvas click reveals it there. */
|
|
15
|
+
export function tabOf(holder, path) {
|
|
24
16
|
const key = path.split(".")[0];
|
|
25
|
-
|
|
17
|
+
if (key in holder.fields)
|
|
18
|
+
return "Content";
|
|
19
|
+
if (key in (holder.settings ?? {}) || key in (holder.styles ?? {}) || (key === "css" && holder.css === true))
|
|
20
|
+
return "Settings";
|
|
21
|
+
return undefined;
|
|
26
22
|
}
|
|
@@ -135,7 +135,14 @@ export function useRichtextEditor({ value, toolset, onChange, contentClass, onEs
|
|
|
135
135
|
if (!editor || value === emitted.current)
|
|
136
136
|
return;
|
|
137
137
|
emitted.current = value;
|
|
138
|
-
|
|
138
|
+
// TipTap's React node views (the image and element cards) flush React synchronously inside
|
|
139
|
+
// setContent, which React forbids from a lifecycle effect ("flushSync was called from inside a
|
|
140
|
+
// lifecycle method"). One microtask later React is idle; a widget unmounted meanwhile is skipped.
|
|
141
|
+
const next = isDoc(value) ? value : EMPTY;
|
|
142
|
+
queueMicrotask(() => {
|
|
143
|
+
if (!editor.isDestroyed)
|
|
144
|
+
editor.commands.setContent(next, { emitUpdate: false });
|
|
145
|
+
});
|
|
139
146
|
}, [editor, value]);
|
|
140
147
|
const active = useEditorState({
|
|
141
148
|
editor,
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ResolvedConfig } from "../config.ts";
|
|
2
|
+
import type { Descriptor } from "../fields.ts";
|
|
3
|
+
import type { EntryRecord, EntryStore } from "../entry-store.ts";
|
|
4
|
+
import { type GlobalSchema } from "../global.ts";
|
|
5
|
+
/** The merged fields a fresh item starts from: sampled field values, and
|
|
6
|
+
* for a visual item `$settings` = the section's settings defaults with the
|
|
7
|
+
* sample's settings values on top. Unsampled fields stay absent. */
|
|
8
|
+
export declare function initialGlobalFields(item: GlobalSchema, settingsDescriptors: Record<string, Descriptor>): Record<string, unknown>;
|
|
9
|
+
export declare function ensureGlobalItems(entries: EntryStore, config: ResolvedConfig): Promise<Record<string, EntryRecord>>;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SETTINGS_KEY } from "../global.js";
|
|
2
|
+
import { sectionSettings } from "./serialize.js";
|
|
2
3
|
/** The merged fields a fresh item starts from: sampled field values, and
|
|
3
|
-
* for a visual item `$
|
|
4
|
-
* sample's
|
|
5
|
-
export function
|
|
4
|
+
* for a visual item `$settings` = the section's settings defaults with the
|
|
5
|
+
* sample's settings values on top. Unsampled fields stay absent. */
|
|
6
|
+
export function initialGlobalFields(item, settingsDescriptors) {
|
|
6
7
|
const out = {};
|
|
7
8
|
for (const key of Object.keys(item.fields)) {
|
|
8
9
|
const sampled = item.sample?.[key];
|
|
@@ -10,25 +11,25 @@ export function initialSharedFields(item, resolvedStyles) {
|
|
|
10
11
|
out[key] = sampled;
|
|
11
12
|
}
|
|
12
13
|
if (item.section !== undefined) {
|
|
13
|
-
const
|
|
14
|
-
for (const [key, d] of Object.entries(
|
|
14
|
+
const settings = {};
|
|
15
|
+
for (const [key, d] of Object.entries(settingsDescriptors)) {
|
|
15
16
|
const value = item.sample?.[key] ?? d.default;
|
|
16
17
|
if (value !== undefined)
|
|
17
|
-
|
|
18
|
+
settings[key] = value;
|
|
18
19
|
}
|
|
19
|
-
out[
|
|
20
|
+
out[SETTINGS_KEY] = settings;
|
|
20
21
|
}
|
|
21
22
|
return out;
|
|
22
23
|
}
|
|
23
|
-
export async function
|
|
24
|
+
export async function ensureGlobalItems(entries, config) {
|
|
24
25
|
const { default: def, supported } = config.locales;
|
|
25
26
|
const out = {};
|
|
26
|
-
for (const item of config.registry.
|
|
27
|
+
for (const item of config.registry.globals ?? []) {
|
|
27
28
|
let [record] = await entries.list(item.name);
|
|
28
29
|
if (!record) {
|
|
29
30
|
try {
|
|
30
|
-
const
|
|
31
|
-
record = await entries.create(item.name,
|
|
31
|
+
const settings = item.section !== undefined ? sectionSettings(config, item.section) : {};
|
|
32
|
+
record = await entries.create(item.name, initialGlobalFields(item, settings), { locale: def });
|
|
32
33
|
}
|
|
33
34
|
catch (e) {
|
|
34
35
|
const message = e instanceof Error ? e.message : String(e);
|
|
@@ -37,7 +38,7 @@ export async function ensureSharedItems(entries, config) {
|
|
|
37
38
|
if (message.includes("it is a singleton"))
|
|
38
39
|
[record] = await entries.list(item.name);
|
|
39
40
|
else
|
|
40
|
-
console.warn(`smoodly: could not materialize
|
|
41
|
+
console.warn(`smoodly: could not materialize global item "${item.name}": ${message}`);
|
|
41
42
|
if (!record)
|
|
42
43
|
continue;
|
|
43
44
|
}
|
|
@@ -51,10 +52,24 @@ export async function ensureSharedItems(entries, config) {
|
|
|
51
52
|
catch (e) {
|
|
52
53
|
const message = e instanceof Error ? e.message : String(e);
|
|
53
54
|
if (!message.includes("already exists in locale")) {
|
|
54
|
-
console.warn(`smoodly: could not materialize
|
|
55
|
+
console.warn(`smoodly: could not materialize global item "${item.name}" in "${locale}": ${message}`);
|
|
55
56
|
}
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
// Every global item is versioned (decided 2026-09-13). A row published before that has
|
|
60
|
+
// no pointers, so its next save would go live: publish it once more here, which snapshots
|
|
61
|
+
// what is live and points both pointers at it. Idempotent — pointed rows are skipped.
|
|
62
|
+
for (const locale of supported) {
|
|
63
|
+
const row = record.locales[locale];
|
|
64
|
+
if (!row || row.status !== "published" || row.publishedVersionId !== null)
|
|
65
|
+
continue;
|
|
66
|
+
try {
|
|
67
|
+
record = await entries.setStatus(item.name, record.id, locale, "published");
|
|
68
|
+
}
|
|
69
|
+
catch (e) {
|
|
70
|
+
console.warn(`smoodly: could not version global item "${item.name}" in "${locale}": ${e instanceof Error ? e.message : String(e)}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
58
73
|
out[item.name] = record;
|
|
59
74
|
}
|
|
60
75
|
return out;
|
package/dist/admin/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export type { AdminOps, AdminOpCall, AdminOpAction, OpResult, OpError, PageCreateInput, PageGetResult,
|
|
1
|
+
export type { AdminOps, AdminOpCall, AdminOpAction, OpResult, OpError, PageCreateInput, PageGetResult, GlobalListItem, GlobalGetResult, AssetUsageItem } from "./ops.ts";
|
|
2
2
|
export { createAdminOps, runAdminOp, type AdminEffects } from "./ops-impl.ts";
|
|
3
3
|
export { fixedNodeSpecs, fixedNodeOf, ensureFixedNodes, type FixedNode } from "./fixed-nodes.ts";
|
|
4
|
-
export {
|
|
4
|
+
export { ensureGlobalItems, initialGlobalFields } from "./global-items.ts";
|
|
5
5
|
export { makeClientOps } from "./client-ops.ts";
|
|
6
6
|
export { gateAdminOp, type AdminAuth } from "./auth.ts";
|
|
7
7
|
export { supabaseAdminAuth } from "./supabase-auth.ts";
|
|
8
8
|
export { serializeAdminConfig } from "./serialize.ts";
|
|
9
|
-
export type { AdminRegistry, AdminSection, AdminCollection, AdminPageTemplate,
|
|
9
|
+
export type { AdminRegistry, AdminSection, AdminCollection, AdminPageTemplate, AdminGlobal, AdminField } from "./serialize.ts";
|
|
10
10
|
export * from "./tree-ops.ts";
|
|
11
11
|
export { validateFields, type FieldError } from "./validate.ts";
|
package/dist/admin/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { createAdminOps, runAdminOp } from "./ops-impl.js";
|
|
2
2
|
export { fixedNodeSpecs, fixedNodeOf, ensureFixedNodes } from "./fixed-nodes.js";
|
|
3
|
-
export {
|
|
3
|
+
export { ensureGlobalItems, initialGlobalFields } from "./global-items.js";
|
|
4
4
|
export { makeClientOps } from "./client-ops.js";
|
|
5
5
|
export { gateAdminOp } from "./auth.js";
|
|
6
6
|
export { supabaseAdminAuth } from "./supabase-auth.js";
|
|
@@ -7,7 +7,7 @@ import { draftMode } from "next/headers";
|
|
|
7
7
|
import { gateAdminOp } from "../auth.js";
|
|
8
8
|
import { createAdminOps, runAdminOp } from "../ops-impl.js";
|
|
9
9
|
export function createAdminOpHandler(deps) {
|
|
10
|
-
const
|
|
10
|
+
const base = {
|
|
11
11
|
config: deps.config,
|
|
12
12
|
pages: deps.pages,
|
|
13
13
|
entries: deps.entries,
|
|
@@ -24,6 +24,7 @@ export function createAdminOpHandler(deps) {
|
|
|
24
24
|
disable: async () => (await draftMode()).disable(),
|
|
25
25
|
},
|
|
26
26
|
},
|
|
27
|
-
}
|
|
28
|
-
|
|
27
|
+
};
|
|
28
|
+
// Ops are built per call so the verified session rides into every store write as `by` (spec 2026-09-13 history §2).
|
|
29
|
+
return (call) => gateAdminOp(deps.auth, call, (c, session) => runAdminOp(createAdminOps({ ...base, actor: session }), c));
|
|
29
30
|
}
|
package/dist/admin/ops-impl.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { EntryStore } from "../entry-store.ts";
|
|
|
3
3
|
import type { AssetStore } from "../asset-store.ts";
|
|
4
4
|
import type { PageStore } from "../store.ts";
|
|
5
5
|
import type { AdminOpCall, AdminOps, OpResult } from "./ops.ts";
|
|
6
|
+
import type { AdminSession } from "./auth.ts";
|
|
6
7
|
export type AdminEffects = {
|
|
7
8
|
/** Expire one cache tag (`pageTag(id)` / `entryTag(id)`). */
|
|
8
9
|
revalidate?(tag: string): void;
|
|
@@ -20,6 +21,8 @@ export declare function createAdminOps(deps: {
|
|
|
20
21
|
href?: (path: string, locale: string) => string;
|
|
21
22
|
/** The media library; absent = every assets op answers "no media store". */
|
|
22
23
|
assets?: AssetStore;
|
|
24
|
+
/** The verified editor; their email is every write's `by`. Absent for the seed and tests (spec 2026-09-13 history §2). */
|
|
25
|
+
actor?: AdminSession;
|
|
23
26
|
}): AdminOps;
|
|
24
27
|
/** Path-dispatch for the one-server-action seam. Own-property checks only —
|
|
25
28
|
* "constructor.constructor" and prototype paths must not resolve. */
|