smoodly 0.0.16 → 0.0.18
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 +176 -26
- package/dist/admin/client-ops.js +1 -1
- package/dist/admin/custom-section.d.ts +10 -0
- package/dist/admin/custom-section.js +10 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +4 -4
- package/dist/admin/editor/CanvasOverlay.js +8 -6
- package/dist/admin/editor/EditorView.js +140 -108
- package/dist/admin/editor/GlobalInspector.d.ts +27 -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/{shared-edit.d.ts → global-edit.d.ts} +10 -10
- package/dist/admin/editor/{shared-edit.js → global-edit.js} +4 -4
- package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
- package/dist/admin/editor/{useSharedEdit.js → useGlobalEdit.js} +4 -4
- 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 +15 -9
- 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} +17 -16
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/ops-impl.js +65 -54
- package/dist/admin/ops.d.ts +6 -6
- package/dist/admin/serialize.d.ts +18 -6
- package/dist/admin/serialize.js +27 -4
- package/dist/admin/shell/AdminApp.js +6 -6
- 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/{SharedForm.js → GlobalForm.js} +36 -26
- package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
- package/dist/admin/shell/{SharedList.js → GlobalList.js} +9 -9
- package/dist/admin/shell/IconRail.js +13 -6
- package/dist/admin/shell/PagesList.js +1 -6
- 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/tree-ops.d.ts +10 -8
- package/dist/admin/tree-ops.js +28 -16
- package/dist/admin/ui/ActionMenu.d.ts +3 -1
- package/dist/admin/ui/ActionMenu.js +4 -3
- package/dist/admin/ui/HistoryModal.js +1 -1
- package/dist/admin/ui/PublishControl.d.ts +3 -1
- package/dist/admin/ui/PublishControl.js +2 -2
- package/dist/admin/ui/primitives.d.ts +4 -2
- package/dist/admin/ui/primitives.js +3 -3
- package/dist/admin/ui/theme.js +61 -18
- package/dist/admin/validate.d.ts +27 -2
- package/dist/admin/validate.js +47 -10
- package/dist/collections.d.ts +3 -3
- package/dist/collections.js +1 -1
- 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-page.d.ts +5 -0
- package/dist/entry-page.js +1 -1
- package/dist/entry-store.d.ts +4 -4
- package/dist/entry-store.js +17 -17
- 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 +31 -21
- package/dist/index.js +11 -6
- 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 +16 -12
- 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/paths.d.ts +21 -0
- package/dist/paths.js +25 -0
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +4 -4
- package/dist/render.d.ts +23 -1
- package/dist/render.js +12 -6
- 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 +29 -14
- package/dist/site.js +55 -28
- package/dist/sql-space.js +1 -1
- package/dist/sql.js +2 -2
- package/dist/store.d.ts +7 -0
- package/dist/store.js +6 -0
- package/dist/styles.d.ts +27 -0
- package/dist/styles.js +60 -0
- package/dist/supabase-entry-store.js +14 -14
- package/dist/supabase-store.js +2 -1
- package/dist/zones.d.ts +9 -7
- package/dist/zones.js +5 -5
- package/package.json +1 -1
- package/dist/admin/editor/SharedInspector.d.ts +0 -23
- package/dist/admin/editor/SharedInspector.js +0 -13
- package/dist/admin/editor/useSharedEdit.d.ts +0 -6
- package/dist/admin/shared-items.d.ts +0 -9
- package/dist/admin/shell/SharedFields.d.ts +0 -21
- package/dist/admin/shell/SharedFields.js +0 -15
- package/dist/shared.d.ts +0 -53
- package/dist/shared.js +0 -45
package/dist/admin/tree-ops.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { GLOBAL_NODE_TYPE } from "../global.js";
|
|
2
2
|
import { setAtPath } from "./field-paths.js";
|
|
3
3
|
export function zoneOf(tree, id) {
|
|
4
4
|
for (const [zone, nodes] of Object.entries(tree.zones)) {
|
|
@@ -64,6 +64,18 @@ export function patchNode(tree, id, ns, key, value) {
|
|
|
64
64
|
return tree;
|
|
65
65
|
return withZone(tree, zone, tree.zones[zone].map((n) => (n.id === id ? { ...n, [ns]: { ...(n[ns] ?? {}), [key]: value } } : n)));
|
|
66
66
|
}
|
|
67
|
+
/** The node's custom CSS (spec 2026-09-14 §4); undefined or "" removes the key. */
|
|
68
|
+
export function patchNodeCss(tree, id, css) {
|
|
69
|
+
const zone = zoneOf(tree, id);
|
|
70
|
+
if (!zone)
|
|
71
|
+
return tree;
|
|
72
|
+
return withZone(tree, zone, tree.zones[zone].map((n) => {
|
|
73
|
+
if (n.id !== id)
|
|
74
|
+
return n;
|
|
75
|
+
const { css: _old, ...rest } = n;
|
|
76
|
+
return css === undefined || css === "" ? rest : { ...rest, css };
|
|
77
|
+
}));
|
|
78
|
+
}
|
|
67
79
|
/** patchNode for a dotted path inside `fields` (spec 2026-09-10 §1):
|
|
68
80
|
* `points.2.heading` sets one item's heading and copies only the spine. */
|
|
69
81
|
export function patchNodePath(tree, id, path, value) {
|
|
@@ -89,11 +101,11 @@ export function allowedSections(policy, all) {
|
|
|
89
101
|
}
|
|
90
102
|
/** The visual items the picker offers in a zone: materialized (an id is
|
|
91
103
|
* known) and rendering a section the zone's policy allows. */
|
|
92
|
-
export function
|
|
104
|
+
export function allowedGlobals(policy, globals, globalIds) {
|
|
93
105
|
if (!policy || policy.kind === "locked")
|
|
94
106
|
return [];
|
|
95
|
-
return
|
|
96
|
-
if (s.section === undefined ||
|
|
107
|
+
return globals.filter((s) => {
|
|
108
|
+
if (s.section === undefined || globalIds[s.name] === undefined)
|
|
97
109
|
return false;
|
|
98
110
|
return policy.allow === "*" || policy.allow.includes(s.section);
|
|
99
111
|
});
|
|
@@ -101,7 +113,7 @@ export function allowedShared(policy, shared, sharedIds) {
|
|
|
101
113
|
/** A page places a visual item as this node (spec 2026-09-07 §6): `item`
|
|
102
114
|
* names the registration, `ref` is the item's row id. */
|
|
103
115
|
export function placementNode(item, ref, id) {
|
|
104
|
-
return { id, type:
|
|
116
|
+
return { id, type: GLOBAL_NODE_TYPE, fields: { item: item.name, ref } };
|
|
105
117
|
}
|
|
106
118
|
/** The empty value a fresh node's field starts at, by descriptor type.
|
|
107
119
|
* Shape matters: a list-valued field seeded with "" is a type lie that
|
|
@@ -111,10 +123,10 @@ const EMPTY_BY_TYPE = { refList: [], object: {} };
|
|
|
111
123
|
* broken-image icon or a stray zero, worse than nothing at all. With no
|
|
112
124
|
* sample, leave the key out of `fields` entirely rather than seed a lie. */
|
|
113
125
|
const ABSENT_WHEN_UNSAMPLED = new Set(["image", "video", "file", "number"]);
|
|
114
|
-
/** Also fills an element node (an `AdminElement` is a section shape plus contexts). */
|
|
126
|
+
/** Also fills an element node (an `AdminElement` is a section shape plus contexts). Never seeds `styles` or `css`: Default stores nothing (spec 2026-09-14 §2). */
|
|
115
127
|
export function nodeFromSample(section, id) {
|
|
116
128
|
const fields = {};
|
|
117
|
-
const
|
|
129
|
+
const settings = {};
|
|
118
130
|
for (const [key, d] of Object.entries(section.fields)) {
|
|
119
131
|
const sampled = section.sample?.[key] ?? d.default ?? (d.type === "boolean" ? false : undefined);
|
|
120
132
|
if (sampled !== undefined)
|
|
@@ -124,20 +136,20 @@ export function nodeFromSample(section, id) {
|
|
|
124
136
|
else
|
|
125
137
|
fields[key] = EMPTY_BY_TYPE[d.type] ?? "";
|
|
126
138
|
}
|
|
127
|
-
for (const [key, d] of Object.entries(section.
|
|
139
|
+
for (const [key, d] of Object.entries(section.settings)) {
|
|
128
140
|
const v = section.sample?.[key] ?? d.default;
|
|
129
141
|
if (v !== undefined)
|
|
130
|
-
|
|
142
|
+
settings[key] = v;
|
|
131
143
|
}
|
|
132
|
-
return { id, type: section.name, fields,
|
|
144
|
+
return { id, type: section.name, fields, settings };
|
|
133
145
|
}
|
|
134
146
|
/** A locked zone holds exactly one node the editor can never insert or
|
|
135
147
|
* remove — so someone has to put it there. This does, from the component's
|
|
136
|
-
* sample (or, for a zone bound to a
|
|
148
|
+
* sample (or, for a zone bound to a global item, a placement), for every
|
|
137
149
|
* locked zone that is empty or missing. Runs when the editor loads a draft,
|
|
138
150
|
* which also covers pages that predate a locked zone being added to their
|
|
139
151
|
* schema. Returns the same tree when nothing changed. */
|
|
140
|
-
export function fillLockedZones(tree, policies, sections,
|
|
152
|
+
export function fillLockedZones(tree, policies, sections, globalIds = {}) {
|
|
141
153
|
let next = null;
|
|
142
154
|
for (const [zone, policy] of Object.entries(policies)) {
|
|
143
155
|
if (policy.kind !== "locked")
|
|
@@ -145,14 +157,14 @@ export function fillLockedZones(tree, policies, sections, sharedIds = {}) {
|
|
|
145
157
|
if ((tree.zones[zone] ?? []).length > 0)
|
|
146
158
|
continue;
|
|
147
159
|
let node = null;
|
|
148
|
-
if (policy.
|
|
149
|
-
// Bound to a
|
|
160
|
+
if (policy.global !== undefined) {
|
|
161
|
+
// Bound to a global item (spec 2026-09-07 §4): a placement, once
|
|
150
162
|
// the editor has loaded the item's row id; until then leave the
|
|
151
163
|
// zone empty rather than seed a plain section there.
|
|
152
|
-
const ref =
|
|
164
|
+
const ref = globalIds[policy.global];
|
|
153
165
|
if (ref === undefined)
|
|
154
166
|
continue;
|
|
155
|
-
node = placementNode({ name: policy.
|
|
167
|
+
node = placementNode({ name: policy.global }, ref, newNodeId(GLOBAL_NODE_TYPE));
|
|
156
168
|
}
|
|
157
169
|
else {
|
|
158
170
|
const section = sections.find((s) => s.name === policy.component);
|
|
@@ -4,8 +4,10 @@ export type ActionMenuItem = {
|
|
|
4
4
|
disabled?: boolean;
|
|
5
5
|
danger?: boolean;
|
|
6
6
|
};
|
|
7
|
-
export declare function ActionMenu({ label, items, disabled }: {
|
|
7
|
+
export declare function ActionMenu({ label, items, disabled, small }: {
|
|
8
8
|
label: string;
|
|
9
9
|
items: ActionMenuItem[];
|
|
10
10
|
disabled?: boolean;
|
|
11
|
+
/** The smaller round button, next to a small Publish. */
|
|
12
|
+
small?: boolean;
|
|
11
13
|
}): import("react").JSX.Element;
|
|
@@ -4,10 +4,11 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
4
4
|
// publish flow §2). Items are always rendered and disabled when they do
|
|
5
5
|
// not apply — the layout never shifts and the editor learns what exists.
|
|
6
6
|
// The popover is the Pages list's row menu shape (`.sm-picker`), anchored
|
|
7
|
-
// under the button instead of at the pointer
|
|
7
|
+
// under the button instead of at the pointer, hanging from the button's
|
|
8
|
+
// right edge (every control sits at its bar's or card's right).
|
|
8
9
|
import { useEffect, useRef, useState } from "react";
|
|
9
10
|
import { cx } from "./primitives.js";
|
|
10
|
-
export function ActionMenu({ label, items, disabled }) {
|
|
11
|
+
export function ActionMenu({ label, items, disabled, small = false }) {
|
|
11
12
|
const [open, setOpen] = useState(false);
|
|
12
13
|
const ref = useRef(null);
|
|
13
14
|
useEffect(() => {
|
|
@@ -21,5 +22,5 @@ export function ActionMenu({ label, items, disabled }) {
|
|
|
21
22
|
document.addEventListener("keydown", onKey);
|
|
22
23
|
return () => { document.removeEventListener("pointerdown", onDown); document.removeEventListener("keydown", onKey); };
|
|
23
24
|
}, [open]);
|
|
24
|
-
return (_jsxs("div", { ref: ref, style: { position: "relative", flex: "none" }, children: [_jsx("button", { type: "button", className: "sm-dotmenu", "aria-label": label, title: label, "aria-haspopup": "menu", "aria-expanded": open, disabled: disabled, onClick: () => setOpen((o) => !o), children: "\u00B7\u00B7\u00B7" }), open && (_jsx("div", { role: "menu", className: "sm-picker sm-picker--menu", children: items.map((item) => (_jsx("button", { type: "button", role: "menuitem", className: cx("sm-picker__item", item.danger && "sm-picker__item--danger"), disabled: item.disabled, onClick: () => { setOpen(false); item.run(); }, children: item.label }, item.label))) }))] }));
|
|
25
|
+
return (_jsxs("div", { ref: ref, style: { position: "relative", flex: "none" }, children: [_jsx("button", { type: "button", className: cx("sm-dotmenu", small && "sm-dotmenu--sm"), "aria-label": label, title: label, "aria-haspopup": "menu", "aria-expanded": open, disabled: disabled, onClick: () => setOpen((o) => !o), children: "\u00B7\u00B7\u00B7" }), open && (_jsx("div", { role: "menu", className: "sm-picker sm-picker--menu", children: items.map((item) => (_jsx("button", { type: "button", role: "menuitem", className: cx("sm-picker__item", item.danger && "sm-picker__item--danger"), disabled: item.disabled, onClick: () => { setOpen(false); item.run(); }, children: item.label }, item.label))) }))] }));
|
|
25
26
|
}
|
|
@@ -4,7 +4,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
4
4
|
// the left — newest first, the unpublished draft tagged Draft and the row
|
|
5
5
|
// the site serves Live — and the selected version on the right, rendered
|
|
6
6
|
// by the screen that opened it (an iframe of the site's draft route for a
|
|
7
|
-
// page, a disabled form for an entry or a
|
|
7
|
+
// page, a disabled form for an entry or a global item). Restore confirms,
|
|
8
8
|
// hands the id to the screen, and closes once the restore landed.
|
|
9
9
|
import { useEffect, useState } from "react";
|
|
10
10
|
import { versionTime } from "./format.js";
|
|
@@ -13,7 +13,7 @@ export declare function publishMenu(opts: {
|
|
|
13
13
|
onUnpublish(): void;
|
|
14
14
|
onHistory?(): void;
|
|
15
15
|
}): ActionMenuItem[];
|
|
16
|
-
export declare function PublishControl({ status, pending, busy, flash, disabled, onPublish, onDiscard, onUnpublish, onHistory, }: {
|
|
16
|
+
export declare function PublishControl({ status, pending, busy, flash, disabled, compact, onPublish, onDiscard, onUnpublish, onHistory, }: {
|
|
17
17
|
/** The locale's stored state; null while the document loads or before it exists. */
|
|
18
18
|
status: LocaleStatus | null;
|
|
19
19
|
/** The editor holds a change the store has not confirmed (dirty, or a save pending / in flight / failed). */
|
|
@@ -28,4 +28,6 @@ export declare function PublishControl({ status, pending, busy, flash, disabled,
|
|
|
28
28
|
onUnpublish(): void;
|
|
29
29
|
/** Opens History (spec 2026-09-13 history §5). Absent = no History item (the canvas card). */
|
|
30
30
|
onHistory?(): void;
|
|
31
|
+
/** The small button and menu — the global inspector's control, so it never reads as the page's Publish. */
|
|
32
|
+
compact?: boolean;
|
|
31
33
|
}): import("react").JSX.Element;
|
|
@@ -16,9 +16,9 @@ export function publishMenu(opts) {
|
|
|
16
16
|
{ label: "Unpublish", disabled: !opts.can.unpublish, run: opts.onUnpublish },
|
|
17
17
|
];
|
|
18
18
|
}
|
|
19
|
-
export function PublishControl({ status, pending = false, busy = null, flash = false, disabled = false, onPublish, onDiscard, onUnpublish, onHistory, }) {
|
|
19
|
+
export function PublishControl({ status, pending = false, busy = null, flash = false, disabled = false, compact = false, onPublish, onDiscard, onUnpublish, onHistory, }) {
|
|
20
20
|
const can = publishActions(status, pending);
|
|
21
21
|
const inert = disabled || busy !== null;
|
|
22
22
|
const word = busy ? BUSY_WORD[busy] : flash ? "Published ✓" : "Publish";
|
|
23
|
-
return (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [status && _jsx(StatusBadge, { status: statusLabel(status) }), _jsx(Button, { variant: flash && !busy ? "ok" : "primary", disabled: inert || !can.publish, onClick: onPublish, children: word }), _jsx(ActionMenu, { label: "More actions", disabled: inert || status === null, items: publishMenu({ can, onDiscard, onUnpublish, onHistory }) })] }));
|
|
23
|
+
return (_jsxs("span", { style: { display: "inline-flex", alignItems: "center", gap: 8 }, children: [status && _jsx(StatusBadge, { status: statusLabel(status) }), _jsx(Button, { variant: flash && !busy ? "ok" : "primary", size: compact ? "sm" : "md", disabled: inert || !can.publish, onClick: onPublish, children: word }), _jsx(ActionMenu, { label: "More actions", disabled: inert || status === null, small: compact, items: publishMenu({ can, onDiscard, onUnpublish, onHistory }) })] }));
|
|
24
24
|
}
|
|
@@ -14,7 +14,7 @@ export declare function Logo({ height }: {
|
|
|
14
14
|
export declare function ThemeToggle(): import("react").JSX.Element;
|
|
15
15
|
type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
|
16
16
|
variant?: "primary" | "secondary" | "ghost" | "danger" | "ok";
|
|
17
|
-
size?: "md" | "lg";
|
|
17
|
+
size?: "sm" | "md" | "lg";
|
|
18
18
|
};
|
|
19
19
|
export declare function Button({ variant, size, className, ...rest }: ButtonProps): import("react").JSX.Element;
|
|
20
20
|
/** The same shape as Button, for navigations that must stay real links. */
|
|
@@ -44,11 +44,13 @@ export declare function Grip(props: ButtonHTMLAttributes<HTMLButtonElement>): im
|
|
|
44
44
|
/** Label + optional counter above a control — the handoff's Input/Textarea head.
|
|
45
45
|
* `asDiv` for groups holding more than one control: an implicit <label>
|
|
46
46
|
* activates its first labelable descendant, so wrapping several is a trap. */
|
|
47
|
-
export declare function Field({ label, mark, counter, hint, error, asDiv, children, style, className, path }: {
|
|
47
|
+
export declare function Field({ label, mark, counter, action, hint, error, asDiv, children, style, className, path }: {
|
|
48
48
|
label?: ReactNode;
|
|
49
49
|
/** A small glyph after the label — the field's "same in every language" mark. */
|
|
50
50
|
mark?: ReactNode;
|
|
51
51
|
counter?: ReactNode;
|
|
52
|
+
/** A small text action at the head's right end, after the counter (a list's Collapse all). */
|
|
53
|
+
action?: ReactNode;
|
|
52
54
|
hint?: ReactNode;
|
|
53
55
|
error?: ReactNode;
|
|
54
56
|
asDiv?: boolean;
|
|
@@ -42,7 +42,7 @@ export function ThemeToggle() {
|
|
|
42
42
|
return (_jsxs("button", { type: "button", role: "menuitem", className: "sm-rail__menu-item", onClick: toggle, children: [_jsxs("span", { className: "sm-when-light", children: [_jsx(Moon, { ...ICON }), "Dark mode"] }), _jsxs("span", { className: "sm-when-dark", children: [_jsx(Sun, { ...ICON }), "Light mode"] })] }));
|
|
43
43
|
}
|
|
44
44
|
export function Button({ variant = "primary", size = "md", className, ...rest }) {
|
|
45
|
-
return (_jsx("button", { type: "button", className: cx("sm-btn", `sm-btn--${variant}`, size === "lg" && "sm-btn--lg", className), ...rest }));
|
|
45
|
+
return (_jsx("button", { type: "button", className: cx("sm-btn", `sm-btn--${variant}`, size === "lg" && "sm-btn--lg", size === "sm" && "sm-btn--sm", className), ...rest }));
|
|
46
46
|
}
|
|
47
47
|
/** The same shape as Button, for navigations that must stay real links. */
|
|
48
48
|
export function LinkButton({ variant = "secondary", children, ...rest }) {
|
|
@@ -66,9 +66,9 @@ export function Grip(props) {
|
|
|
66
66
|
/** Label + optional counter above a control — the handoff's Input/Textarea head.
|
|
67
67
|
* `asDiv` for groups holding more than one control: an implicit <label>
|
|
68
68
|
* activates its first labelable descendant, so wrapping several is a trap. */
|
|
69
|
-
export function Field({ label, mark, counter, hint, error, asDiv, children, style, className, path }) {
|
|
69
|
+
export function Field({ label, mark, counter, action, hint, error, asDiv, children, style, className, path }) {
|
|
70
70
|
const Tag = asDiv ? "div" : "label";
|
|
71
|
-
return (_jsxs(Tag, { className: cx("sm-field", className), style: style, "data-sm-path": path, children: [(label || counter) && (_jsxs("span", { className: "sm-field__head", children: [_jsxs("span", { className: "sm-label", children: [label, mark] }), counter && _jsx("span", { className: "sm-counter", children: counter })] })), children, hint && _jsx("span", { className: "sm-hint", children: hint }), error && _jsx("span", { className: "sm-error", children: error })] }));
|
|
71
|
+
return (_jsxs(Tag, { className: cx("sm-field", className), style: style, "data-sm-path": path, children: [(label || counter || action) && (_jsxs("span", { className: "sm-field__head", children: [_jsxs("span", { className: "sm-label", children: [label, mark] }), (counter || action) && (_jsxs("span", { className: "sm-field__aside", children: [counter && _jsx("span", { className: "sm-counter", children: counter }), action] }))] })), children, hint && _jsx("span", { className: "sm-hint", children: hint }), error && _jsx("span", { className: "sm-error", children: error })] }));
|
|
72
72
|
}
|
|
73
73
|
/** The mark beside a field that is NOT `.localized()`: a globe-off, so a
|
|
74
74
|
* multilingual form needs no "shared across languages" grouping. */
|
package/dist/admin/ui/theme.js
CHANGED
|
@@ -25,7 +25,7 @@ const TOKENS_LIGHT = `
|
|
|
25
25
|
--line:#E8E3DA; --line-dashed:#D8D1C4; --line-soft:#EFEBE3;
|
|
26
26
|
--text:#1F1D1A; --text-secondary:#4E4942; --text-muted:#7A746A; --text-subtle:#A39C90; --text-disabled:#C4BDB1;
|
|
27
27
|
--selection:var(--accent); --nav-active-bg:var(--accent-soft); --nav-active-fg:var(--accent);
|
|
28
|
-
--selection-
|
|
28
|
+
--selection-global:#C4620C;
|
|
29
29
|
--hover:rgba(31,29,26,.05); --press:rgba(31,29,26,.09); --control-hover:#F8F6F1;
|
|
30
30
|
--btn-secondary-bg:var(--surface); --btn-secondary-hover:#F8F6F1; --tile-hover:var(--surface);
|
|
31
31
|
--bar-bg:transparent; --bar-border:0; --menu-border:0; --card-divider:0;
|
|
@@ -63,7 +63,7 @@ const TOKENS_LIGHT = `
|
|
|
63
63
|
`;
|
|
64
64
|
const TOKENS_DARK = `
|
|
65
65
|
--accent:#6CC4A6; --accent-hover:#82D2B8; --accent-soft:rgba(108,196,166,.18); --accent-line:#6CC4A6; --on-accent:#0B1F18;
|
|
66
|
-
--selection-
|
|
66
|
+
--selection-global:#F09A5C;
|
|
67
67
|
--bg:#1C1A17; --canvas:#141210; --surface:#27241F; --surface-form:#27241F; --surface-inset:#1C1A17;
|
|
68
68
|
--input-bg:#211E1A; --input-hover-bg:#211E1A; --input-focus-bg:#211E1A;
|
|
69
69
|
--line:#3A3630; --line-dashed:#4A453D; --line-soft:#3A3630;
|
|
@@ -96,13 +96,13 @@ const SCALE = `
|
|
|
96
96
|
--title-font:var(--font-sans); --title-size:18px; --title-weight:600; --title-tracking:-0.01em;
|
|
97
97
|
--doc-title-size:30px;
|
|
98
98
|
--blocktag-font:var(--font-sans); --blocktag-size:11px; --blocktag-transform:none; --blocktag-tracking:0;
|
|
99
|
-
--blocktag-radius:var(--radius-round); --blocktag-shift:translate(16px,-50%);
|
|
99
|
+
--blocktag-radius:var(--radius-round); --blocktag-shift:translate(16px,-50%); --blocktag-shift-inside:translate(16px,8px);
|
|
100
100
|
--badge-h:24px; --badge-dot:inline-block;
|
|
101
101
|
|
|
102
102
|
--radius-xs:2px; --radius-sm:8px; --radius-md:10px; --radius-lg:12px;
|
|
103
103
|
--radius-card:14px; --radius-card-lg:16px; --radius-btn:999px; --radius-round:999px;
|
|
104
104
|
--page-radius:4px 4px 0 0; --selection-radius:4px;
|
|
105
|
-
--control-h:40px; --control-h-sm:34px; --control-h-lg:48px; --button-h:36px; --row-h:48px; --topbar-h:64px;
|
|
105
|
+
--control-h:40px; --control-h-sm:34px; --control-h-lg:48px; --button-h:36px; --button-h-sm:28px; --row-h:48px; --topbar-h:64px;
|
|
106
106
|
--outline-row-h:36px; --seg-pad-y:7px; --tabs-pad:3px;
|
|
107
107
|
--plus-h:32px; --plus-pad:0 14px 0 6px; --plus-label:inline;
|
|
108
108
|
--nav-w:72px; --inspector-w:360px; --gutter:20px; --frame-pad:24px;
|
|
@@ -110,7 +110,7 @@ const SCALE = `
|
|
|
110
110
|
--table-pad:8px 12px; --table-top:80px; --canvas-pad:24px 40px 0; --card-pad:16px 18px; --card-gap:12px; --form-pad:32px 40px;
|
|
111
111
|
--border:1px solid var(--line);
|
|
112
112
|
--outline-selection:2px solid var(--selection);
|
|
113
|
-
--outline-selection-
|
|
113
|
+
--outline-selection-global:2px solid var(--selection-global);
|
|
114
114
|
`;
|
|
115
115
|
/* ── TEMPORARY: the original look, kept for side-by-side comparison while
|
|
116
116
|
the warm restyle lands. Frozen copies of the three strings above as they
|
|
@@ -133,7 +133,7 @@ const TECHNICAL_LIGHT = `
|
|
|
133
133
|
--text:var(--gray-900); --text-muted:var(--gray-600); --text-subtle:var(--gray-500);
|
|
134
134
|
--accent:var(--brand-600); --on-accent:#FFFFFF; --accent-soft:var(--brand-50);
|
|
135
135
|
--selection:var(--brand-600); --nav-active-bg:var(--brand-100); --nav-active-fg:var(--brand-600);
|
|
136
|
-
--selection-
|
|
136
|
+
--selection-global:#D2610A;
|
|
137
137
|
--hover:rgba(15,20,32,.045); --press:rgba(15,20,32,.08);
|
|
138
138
|
--control-hover:var(--gray-100);
|
|
139
139
|
|
|
@@ -162,7 +162,7 @@ const TECHNICAL_DARK = `
|
|
|
162
162
|
--line:#2A2F3B; --line-dashed:#2A2F3B;
|
|
163
163
|
--text:#EEF0F5; --text-muted:#8C94A8; --text-subtle:#6B7386;
|
|
164
164
|
--accent:var(--brand-300); --on-accent:#06122F; --accent-soft:rgba(110,150,255,.16);
|
|
165
|
-
--selection-
|
|
165
|
+
--selection-global:#F2A35F;
|
|
166
166
|
--nav-active-bg:rgba(110,150,255,.14); --nav-active-fg:var(--brand-200);
|
|
167
167
|
--hover:rgba(238,240,245,.05); --press:rgba(238,240,245,.09);
|
|
168
168
|
--control-hover:var(--input-bg);
|
|
@@ -190,7 +190,7 @@ const TECHNICAL_SCALE = `
|
|
|
190
190
|
--nav-w:72px; --inspector-w:380px;
|
|
191
191
|
--border:1px solid var(--line);
|
|
192
192
|
--outline-selection:1.5px solid var(--selection);
|
|
193
|
-
--outline-selection-
|
|
193
|
+
--outline-selection-global:1.5px solid var(--selection-global);
|
|
194
194
|
`;
|
|
195
195
|
/* The tokens the warm restyle added, with the values the technical look
|
|
196
196
|
implied, so the frozen block above renders the old shapes too. */
|
|
@@ -202,11 +202,11 @@ const TECHNICAL_SHAPE = `
|
|
|
202
202
|
--title-font:var(--font-sans); --title-size:var(--text-xl); --title-weight:600; --title-tracking:0;
|
|
203
203
|
--doc-title-size:var(--text-2xl);
|
|
204
204
|
--blocktag-font:var(--font-mono); --blocktag-size:var(--text-2xs); --blocktag-transform:uppercase; --blocktag-tracking:var(--tracking-tag);
|
|
205
|
-
--blocktag-radius:var(--radius-sm) 0 var(--radius-sm) 0; --blocktag-shift:none;
|
|
205
|
+
--blocktag-radius:var(--radius-sm) 0 var(--radius-sm) 0; --blocktag-shift:none; --blocktag-shift-inside:none;
|
|
206
206
|
--badge-h:20px; --badge-dot:none;
|
|
207
207
|
--radius-lg:var(--radius-md); --radius-card:var(--radius-md); --radius-card-lg:var(--radius-md); --radius-btn:var(--radius-md);
|
|
208
208
|
--page-radius:var(--radius-md); --selection-radius:var(--radius-sm);
|
|
209
|
-
--control-h-sm:24px; --button-h:var(--control-h);
|
|
209
|
+
--control-h-sm:24px; --button-h:var(--control-h); --button-h-sm:24px;
|
|
210
210
|
--outline-row-h:var(--control-h); --seg-pad-y:3px; --tabs-pad:0 14px;
|
|
211
211
|
--plus-h:24px; --plus-pad:0; --plus-label:none;
|
|
212
212
|
--gutter:0px; --frame-pad:0px;
|
|
@@ -304,8 +304,6 @@ body:has(.sm-admin){margin:0}
|
|
|
304
304
|
.sm-rail__btn[aria-current="page"]:hover,.sm-rail__btn[aria-expanded="true"]:hover{color:var(--nav-active-fg)}
|
|
305
305
|
.sm-rail__btn[aria-current="page"] .sm-rail__tile,.sm-rail__btn[aria-expanded="true"] .sm-rail__tile{background:var(--nav-active-bg)}
|
|
306
306
|
.sm-rail__tile svg{width:18px;height:18px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round}
|
|
307
|
-
.sm-rail__chev{position:absolute;right:4px;bottom:3px;width:8px;height:8px}
|
|
308
|
-
.sm-rail__chev svg{width:8px;height:8px;stroke-width:3}
|
|
309
307
|
.sm-rail__label{letter-spacing:.01em;white-space:nowrap}
|
|
310
308
|
/* Not greyed: the reference renders every inactive item at --text and marks
|
|
311
309
|
the unavailable ones only by being inert (no pointer, no hover). */
|
|
@@ -325,6 +323,11 @@ body:has(.sm-admin){margin:0}
|
|
|
325
323
|
}
|
|
326
324
|
.sm-rail__menu[data-open="true"]{display:flex}
|
|
327
325
|
.sm-rail__menu--up{top:auto;bottom:0}
|
|
326
|
+
/* The Collections flyout opens on hover, so it is roomier (the names at --text-lg) and an invisible
|
|
327
|
+
bridge spans the 6px gap so the pointer can cross from the rail without closing it. */
|
|
328
|
+
.sm-rail__menu--collections{width:208px;padding:8px;gap:2px;font-size:var(--text-lg)}
|
|
329
|
+
.sm-rail__menu--collections::before{content:"";position:absolute;left:-8px;top:0;bottom:0;width:8px}
|
|
330
|
+
.sm-rail__menu--collections .sm-rail__menu-item{padding:9px 12px}
|
|
328
331
|
.sm-rail__menu-title{font-size:9px;font-weight:600;letter-spacing:var(--tracking-label);text-transform:uppercase;color:var(--text-muted);padding:6px 8px 4px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
329
332
|
.sm-rail__menu-item{
|
|
330
333
|
appearance:none;background:none;border:0;width:100%;display:flex;align-items:center;justify-content:space-between;
|
|
@@ -374,6 +377,7 @@ body:has(.sm-admin){margin:0}
|
|
|
374
377
|
text-decoration:none;
|
|
375
378
|
}
|
|
376
379
|
.sm-btn--lg{height:var(--control-h-lg);padding:0 20px}
|
|
380
|
+
.sm-btn--sm,.sm-btn--sm.sm-btn--primary,.sm-btn--sm.sm-btn--ok{height:var(--button-h-sm);padding:0 12px;font-size:var(--text-sm)}
|
|
377
381
|
.sm-btn--primary{background:var(--accent);color:var(--on-accent);font-weight:600;padding:0 18px}
|
|
378
382
|
.sm-btn--primary:hover{background:var(--accent-hover);color:var(--on-accent)}
|
|
379
383
|
.sm-btn--secondary{background:var(--btn-secondary-bg);color:var(--text)}
|
|
@@ -385,7 +389,7 @@ body:has(.sm-admin){margin:0}
|
|
|
385
389
|
.sm-btn--ok{background:var(--status-published-bg);color:var(--status-published-fg);font-weight:600;padding:0 18px}
|
|
386
390
|
.sm-btn:disabled,.sm-btn[aria-disabled="true"]{opacity:.5;cursor:default;pointer-events:none}
|
|
387
391
|
.sm-btn:focus-visible,.sm-iconbtn:focus-visible,.sm-nav-item:focus-visible,
|
|
388
|
-
.sm-tab:focus-visible,.sm-seg__opt:focus-visible,.sm-row:focus-visible{
|
|
392
|
+
.sm-tab:focus-visible,.sm-seg__opt:focus-visible,.sm-pill:focus-visible,.sm-row:focus-visible{
|
|
389
393
|
outline:2px solid var(--accent);outline-offset:1px
|
|
390
394
|
}
|
|
391
395
|
|
|
@@ -413,6 +417,10 @@ body:has(.sm-admin){margin:0}
|
|
|
413
417
|
.sm-label__mark{display:inline-flex;color:var(--text-subtle);cursor:help}
|
|
414
418
|
.sm-label__mark svg{width:13px;height:13px}
|
|
415
419
|
.sm-counter{font-size:var(--text-sm);color:var(--text-subtle)}
|
|
420
|
+
.sm-field__aside{display:flex;align-items:baseline;gap:10px;flex:none}
|
|
421
|
+
/* A head's text action (a list's Collapse all / Expand all): a small accent link. */
|
|
422
|
+
.sm-field__action{background:none;border:0;padding:0;font:inherit;font-size:var(--text-sm);font-weight:600;color:var(--accent);cursor:pointer}
|
|
423
|
+
.sm-field__action:hover{color:var(--accent-hover)}
|
|
416
424
|
.sm-hint{font-size:var(--text-sm);color:var(--text-muted)}
|
|
417
425
|
.sm-input,.sm-textarea,.sm-select{
|
|
418
426
|
width:100%;min-width:0;border:1px solid var(--input-border);border-radius:var(--radius-md);
|
|
@@ -504,7 +512,8 @@ body:has(.sm-admin){margin:0}
|
|
|
504
512
|
.sm-rtcard__icon{width:18px;height:18px;flex:none;border-radius:var(--radius-xs);background:var(--accent-soft);color:var(--accent);display:grid;place-items:center;font-size:11px}
|
|
505
513
|
.sm-rtcard__title{flex:1;min-width:0;font-weight:600;font-size:var(--text-sm);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
506
514
|
.sm-rtcard__body{display:flex;flex-direction:column;gap:10px;padding:10px 12px 12px;border-top:1px solid var(--line)}
|
|
507
|
-
.
|
|
515
|
+
/* The whole image, letterboxed on the inset tone, never cropped: the card is a preview, not a crop. */
|
|
516
|
+
.sm-rtcard__thumb{display:block;width:100%;height:160px;object-fit:contain;border-radius:var(--radius-sm);background:var(--surface-inset)}
|
|
508
517
|
.sm-rtcard__file{font-size:var(--text-sm);color:var(--text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|
509
518
|
.sm-richtext__content blockquote{padding-left:12px;border-left:2px solid var(--line);color:var(--text-muted)}
|
|
510
519
|
.sm-richtext__content a{color:var(--accent);text-decoration:underline;text-underline-offset:3px;text-decoration-color:var(--accent-line)}
|
|
@@ -533,6 +542,23 @@ body:has(.sm-admin){margin:0}
|
|
|
533
542
|
display:flex;align-items:center;justify-content:space-between;gap:8px;min-height:var(--control-h);padding:0 6px 0 14px;
|
|
534
543
|
border-radius:var(--radius-md);background:var(--surface-inset);color:var(--text-muted);font-size:var(--text-sm);
|
|
535
544
|
}
|
|
545
|
+
/* The custom section's row cards and the width bar (spec 2026-09-14 §6). */
|
|
546
|
+
.sm-rowcards{display:flex;flex-direction:column;gap:12px;align-items:stretch}
|
|
547
|
+
.sm-rowcard{display:flex;flex-direction:column;gap:12px;padding:12px;border:1px solid var(--line);border-radius:var(--radius-md);background:var(--surface);min-width:0}
|
|
548
|
+
.sm-rowcard__head{display:flex;align-items:center;gap:4px}
|
|
549
|
+
.sm-rowcard__title{flex:1;font-weight:600}
|
|
550
|
+
.sm-rowcard__presets{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
551
|
+
/* Secondary buttons sit on the white card: an outline so they read as buttons. */
|
|
552
|
+
.sm-rowcard__presets .sm-btn{height:var(--control-h-sm);padding:0 10px;font-size:var(--text-sm);border:1px solid var(--line)}
|
|
553
|
+
.sm-rowcards > .sm-btn{align-self:flex-start;border:1px solid var(--line)}
|
|
554
|
+
.sm-stepper{display:flex;align-items:center;gap:8px}
|
|
555
|
+
.sm-stepper__value{min-width:20px;text-align:center;font-weight:600}
|
|
556
|
+
.sm-spanbar{display:flex;gap:2px;height:32px;background:var(--seg-bg);border-radius:var(--radius-md);padding:3px;user-select:none}
|
|
557
|
+
.sm-spanbar__col{position:relative;display:flex;align-items:center;justify-content:center;min-width:0;background:var(--surface);border-radius:6px;font-size:var(--text-sm);color:var(--text-muted)}
|
|
558
|
+
.sm-spanbar__handle{position:absolute;right:-6px;top:0;bottom:0;width:10px;padding:0;border:0;background:transparent;cursor:col-resize;touch-action:none;z-index:1}
|
|
559
|
+
.sm-spanbar__handle::after{content:"";position:absolute;left:4px;top:8px;bottom:8px;width:2px;border-radius:1px;background:var(--text-muted)}
|
|
560
|
+
.sm-spanbar__handle:focus-visible{outline:none}
|
|
561
|
+
.sm-spanbar__handle:focus-visible::after,.sm-spanbar__handle:hover::after{background:var(--accent)}
|
|
536
562
|
/* A field revealed from a canvas click: one pulse of the accent, then nothing. */
|
|
537
563
|
.sm-field--reveal{animation:sm-reveal .9s ease-out;border-radius:var(--radius-sm)}
|
|
538
564
|
@keyframes sm-reveal{0%{outline:2px solid var(--selection);outline-offset:4px}100%{outline:2px solid transparent;outline-offset:4px}}
|
|
@@ -610,6 +636,7 @@ body:has(.sm-admin){margin:0}
|
|
|
610
636
|
.sm-dotmenu:hover{background:var(--btn-secondary-hover);color:var(--text)}
|
|
611
637
|
.sm-dotmenu:disabled{opacity:.4;cursor:default}
|
|
612
638
|
.sm-dotmenu:disabled:hover{background:var(--btn-secondary-bg);color:var(--text-muted)}
|
|
639
|
+
.sm-dotmenu--sm{width:var(--button-h-sm);height:var(--button-h-sm);font-size:var(--text-sm)}
|
|
613
640
|
.sm-picker--menu{position:absolute;top:calc(100% + 6px);right:0;width:220px;z-index:60}
|
|
614
641
|
|
|
615
642
|
/* Status pill: a dot before the word in the warm look (none in the technical). */
|
|
@@ -656,6 +683,18 @@ body:has(.sm-admin){margin:0}
|
|
|
656
683
|
.sm-seg__opt[aria-pressed="true"]{background:var(--surface);color:var(--text);font-weight:600;box-shadow:var(--shadow-seg)}
|
|
657
684
|
/* A locale the record does not have yet: subtle, still a button (it opens the add pane). */
|
|
658
685
|
.sm-seg__opt--absent{color:var(--text-subtle)}
|
|
686
|
+
/* f.radio as a pill row (spec 2026-09-14 §5): small chips a step below the tabs and the
|
|
687
|
+
segmented control — no track, the chosen pill filled in the soft accent, the rest bare.
|
|
688
|
+
Wraps when the labels need it. */
|
|
689
|
+
.sm-pills{display:flex;flex-wrap:wrap;gap:4px;padding:2px 0}
|
|
690
|
+
.sm-pill{
|
|
691
|
+
height:var(--control-h-sm);padding:0 10px;border:0;border-radius:var(--radius-btn);background:transparent;
|
|
692
|
+
color:var(--text-muted);font:inherit;font-size:var(--text-sm);font-weight:500;line-height:1;cursor:pointer;white-space:nowrap;
|
|
693
|
+
}
|
|
694
|
+
.sm-pill:hover{color:var(--text);background:var(--surface-inset)}
|
|
695
|
+
.sm-pill[aria-pressed="true"]{background:var(--accent-soft);color:var(--accent);font-weight:600}
|
|
696
|
+
/* The custom-CSS box: code, no wrapping, no spellcheck. */
|
|
697
|
+
.sm-css{font-family:var(--font-mono);font-size:12px;line-height:1.5;white-space:pre;overflow-x:auto}
|
|
659
698
|
/* Switch (f.boolean, the SEO noindex toggle): a pill track, knob slides right when on. */
|
|
660
699
|
.sm-switch{width:36px;height:20px;padding:2px;border:0;border-radius:var(--radius-round);background:var(--seg-bg);cursor:pointer;display:inline-flex;align-items:center;flex:none}
|
|
661
700
|
.sm-switch[aria-checked="true"]{background:var(--accent)}
|
|
@@ -687,7 +726,7 @@ body:has(.sm-admin){margin:0}
|
|
|
687
726
|
.sm-fielderr__field{background:none;border:0;padding:0;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
|
|
688
727
|
|
|
689
728
|
/* ── cards ─────────────────────────────────────────────────────────── */
|
|
690
|
-
/* The inspector column (editor, entry and
|
|
729
|
+
/* The inspector column (editor, entry and global forms) is a stack of
|
|
691
730
|
cards; the technical look draws them flush with a hairline between. */
|
|
692
731
|
.sm-inspector{
|
|
693
732
|
display:flex;flex-direction:column;gap:var(--card-gap);min-height:0;overflow:auto;
|
|
@@ -704,7 +743,10 @@ body:has(.sm-admin){margin:0}
|
|
|
704
743
|
}
|
|
705
744
|
.sm-inspector__head{display:flex;align-items:center;justify-content:space-between;gap:8px;flex:none}
|
|
706
745
|
.sm-inspector__body{display:flex;flex-direction:column;gap:14px}
|
|
707
|
-
/*
|
|
746
|
+
/* A tab's groups (spec 2026-09-14 §5): the section's own settings untitled, then Spacing & layout and Advanced under a hairline. */
|
|
747
|
+
.sm-inspector__group{display:flex;flex-direction:column;gap:12px}
|
|
748
|
+
.sm-inspector__group+.sm-inspector__group{margin-top:8px;padding-top:12px;border-top:1px solid var(--line)}
|
|
749
|
+
/* The form card: the main column of the entry and global forms. Every
|
|
708
750
|
field (and the banner) is a centred 680px column — a reading width —
|
|
709
751
|
while the card itself still fills the main area. */
|
|
710
752
|
.sm-form{
|
|
@@ -753,13 +795,14 @@ body:has(.sm-admin){margin:0}
|
|
|
753
795
|
letter-spacing:var(--blocktag-tracking);text-transform:var(--blocktag-transform);padding:3px 10px;
|
|
754
796
|
border-radius:var(--blocktag-radius);white-space:nowrap;
|
|
755
797
|
}
|
|
756
|
-
.sm-blocktag--
|
|
798
|
+
.sm-blocktag--global{background:var(--selection-global)}
|
|
799
|
+
.sm-blocktag--inside{transform:var(--blocktag-shift-inside)}
|
|
757
800
|
.sm-blocktoolbar{
|
|
758
801
|
display:flex;gap:2px;padding:3px;background:var(--toolbar-bg);border-radius:var(--radius-btn);
|
|
759
802
|
box-shadow:var(--shadow-float);
|
|
760
803
|
}
|
|
761
804
|
.sm-blocktoolbar__note{font-family:var(--font-mono);font-size:var(--text-xs);color:var(--toolbar-fg);opacity:.7;padding:4px 8px}
|
|
762
|
-
/* The insert affordance between
|
|
805
|
+
/* The insert affordance between sections: a labelled pill (the label is
|
|
763
806
|
hidden in the technical look, leaving the 24px circle). */
|
|
764
807
|
.sm-plus{
|
|
765
808
|
display:inline-flex;align-items:center;gap:8px;height:var(--plus-h);min-width:var(--plus-h);padding:var(--plus-pad);
|
package/dist/admin/validate.d.ts
CHANGED
|
@@ -10,11 +10,36 @@ type Desc = {
|
|
|
10
10
|
min?: unknown;
|
|
11
11
|
fields?: unknown;
|
|
12
12
|
item?: unknown;
|
|
13
|
+
hidden?: unknown;
|
|
14
|
+
};
|
|
15
|
+
/** What conditions read beyond the current level: the node's two namespaces. */
|
|
16
|
+
export type ValidationScope = {
|
|
17
|
+
fields?: Record<string, unknown>;
|
|
18
|
+
settings?: Record<string, unknown>;
|
|
13
19
|
};
|
|
14
20
|
/** The element schemas an element node is validated against (spec 2026-09-08 block nodes §2). */
|
|
15
21
|
export type ElementDescs = Record<string, {
|
|
16
22
|
fields: Record<string, Desc>;
|
|
17
|
-
|
|
23
|
+
settings?: Record<string, Desc>;
|
|
18
24
|
}>;
|
|
19
|
-
|
|
25
|
+
/** Validate one value map. Without `scope.fields` the values ARE the fields (an entry, a page, an object global). */
|
|
26
|
+
export declare function validateFields(values: Record<string, unknown>, descriptors: Record<string, Desc>, elements?: ElementDescs, scope?: ValidationScope): FieldError[];
|
|
27
|
+
/** A tree node (or a visual global, in node shape) against its schema: fields, settings (prefixed), css (spec 2026-09-14 §3, §4). */
|
|
28
|
+
export declare function validateNode(node: {
|
|
29
|
+
fields?: Record<string, unknown>;
|
|
30
|
+
settings?: Record<string, unknown>;
|
|
31
|
+
css?: unknown;
|
|
32
|
+
[other: string]: unknown;
|
|
33
|
+
}, schema: {
|
|
34
|
+
fields: Record<string, Desc>;
|
|
35
|
+
settings?: Record<string, Desc>;
|
|
36
|
+
}, elements?: ElementDescs): FieldError[];
|
|
37
|
+
/** A custom section node (spec 2026-09-14 §6): the columns' richtext (element nodes included), the row structure, the css.
|
|
38
|
+
* The generic walk's own words on `rows.<i>.spans` are dropped (`spans` is no descriptor type it knows) so rowsProblems
|
|
39
|
+
* is the one voice on structure; the columns' richtext errors keep their paths. */
|
|
40
|
+
export declare function validateCustomNode(node: {
|
|
41
|
+
fields?: Record<string, unknown>;
|
|
42
|
+
css?: unknown;
|
|
43
|
+
[other: string]: unknown;
|
|
44
|
+
}, fields: Record<string, Desc>, elements?: ElementDescs): FieldError[];
|
|
20
45
|
export {};
|
package/dist/admin/validate.js
CHANGED
|
@@ -2,9 +2,15 @@
|
|
|
2
2
|
// lives admin/server-side). Entry saves validate; page drafts do NOT
|
|
3
3
|
// (drafts must save freely) — publish validates every node. Objects and
|
|
4
4
|
// lists recurse; error paths are dotted (`cta.href`, `points.2.heading`).
|
|
5
|
+
// A field whose `.hidden()` condition holds is skipped entirely — a hidden
|
|
6
|
+
// required field is not required (spec 2026-09-14 §12); the same isHidden
|
|
7
|
+
// the form uses decides, over the same scope.
|
|
5
8
|
import { optionValues } from "../fields.js";
|
|
9
|
+
import { isHidden } from "../conditions.js";
|
|
10
|
+
import { cssProblem } from "../css.js";
|
|
6
11
|
import { hasContent } from "./richtext-doc.js";
|
|
7
12
|
import { ELEMENT_NODE, richtextBlocks } from "../richtext-walk.js";
|
|
13
|
+
import { rowsProblems } from "../custom/rows.js";
|
|
8
14
|
const subfields = (d) => d.fields && typeof d.fields === "object" ? d.fields : {};
|
|
9
15
|
const itemOf = (d) => d.item && typeof d.item === "object" ? d.item : undefined;
|
|
10
16
|
const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
@@ -12,7 +18,7 @@ const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArr
|
|
|
12
18
|
* may be absent — may be absent altogether. */
|
|
13
19
|
const mayBeAbsent = (d) => d.optional === true || d.type === "boolean" || (d.type === "object" && Object.values(subfields(d)).every(mayBeAbsent));
|
|
14
20
|
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
15
|
-
function validateValue(path, value, d, errors, elements) {
|
|
21
|
+
function validateValue(path, value, d, errors, elements, scope) {
|
|
16
22
|
if (isEmpty(value)) {
|
|
17
23
|
if (!mayBeAbsent(d))
|
|
18
24
|
errors.push({ field: path, message: "Required." });
|
|
@@ -34,8 +40,9 @@ function validateValue(path, value, d, errors, elements) {
|
|
|
34
40
|
const el = typeof a.name === "string" ? elements[a.name] : undefined;
|
|
35
41
|
if (el) {
|
|
36
42
|
const fields = (a.fields && typeof a.fields === "object" ? a.fields : {});
|
|
37
|
-
|
|
38
|
-
|
|
43
|
+
const settings = (a.settings && typeof a.settings === "object" ? a.settings : {});
|
|
44
|
+
validateMap(`${path}.elements.${i}`, fields, el.fields, errors, elements, { self: fields, fields, settings });
|
|
45
|
+
validateMap(`${path}.elements.${i}.settings`, settings, el.settings ?? {}, errors, elements, { self: settings, fields, settings });
|
|
39
46
|
}
|
|
40
47
|
i++;
|
|
41
48
|
}
|
|
@@ -57,8 +64,9 @@ function validateValue(path, value, d, errors, elements) {
|
|
|
57
64
|
if (typeof d.max === "number" && value.length > d.max)
|
|
58
65
|
errors.push({ field: path, message: `At most ${plural(d.max, "item")}.` });
|
|
59
66
|
const item = itemOf(d);
|
|
60
|
-
|
|
61
|
-
|
|
67
|
+
// A scalar item's condition reads the list's own level; an object item reads itself (the object branch).
|
|
68
|
+
if (item && !isHidden(item, scope))
|
|
69
|
+
value.forEach((v, i) => validateValue(`${path}.${i}`, v, item, errors, elements, scope));
|
|
62
70
|
return;
|
|
63
71
|
}
|
|
64
72
|
if (d.type === "object") {
|
|
@@ -66,8 +74,7 @@ function validateValue(path, value, d, errors, elements) {
|
|
|
66
74
|
errors.push({ field: path, message: "Must be an object." });
|
|
67
75
|
return;
|
|
68
76
|
}
|
|
69
|
-
|
|
70
|
-
validateValue(`${path}.${k}`, value[k], sub, errors, elements);
|
|
77
|
+
validateMap(path, value, subfields(d), errors, elements, { ...scope, self: value });
|
|
71
78
|
return;
|
|
72
79
|
}
|
|
73
80
|
if (d.type === "boolean") {
|
|
@@ -97,9 +104,39 @@ function validateValue(path, value, d, errors, elements) {
|
|
|
97
104
|
errors.push({ field: path, message: `Must be at most ${d.max} characters.` });
|
|
98
105
|
}
|
|
99
106
|
}
|
|
100
|
-
|
|
107
|
+
/** One descriptor map at one level: hidden entries are skipped. `prefix` is the dotted path above it ("" at the top). */
|
|
108
|
+
function validateMap(prefix, values, descriptors, errors, elements, scope) {
|
|
109
|
+
for (const [key, d] of Object.entries(descriptors)) {
|
|
110
|
+
if (isHidden(d, scope))
|
|
111
|
+
continue;
|
|
112
|
+
validateValue(prefix === "" ? key : `${prefix}.${key}`, values[key], d, errors, elements, scope);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Validate one value map. Without `scope.fields` the values ARE the fields (an entry, a page, an object global). */
|
|
116
|
+
export function validateFields(values, descriptors, elements = {}, scope = {}) {
|
|
101
117
|
const errors = [];
|
|
102
|
-
|
|
103
|
-
|
|
118
|
+
validateMap("", values, descriptors, errors, elements, { self: values, fields: scope.fields ?? values, settings: scope.settings });
|
|
119
|
+
return errors;
|
|
120
|
+
}
|
|
121
|
+
/** A tree node (or a visual global, in node shape) against its schema: fields, settings (prefixed), css (spec 2026-09-14 §3, §4). */
|
|
122
|
+
export function validateNode(node, schema, elements = {}) {
|
|
123
|
+
const fields = node.fields ?? {};
|
|
124
|
+
const settings = node.settings ?? {};
|
|
125
|
+
const scope = { fields, settings };
|
|
126
|
+
const errors = validateFields(fields, schema.fields, elements, scope);
|
|
127
|
+
for (const e of validateFields(settings, schema.settings ?? {}, elements, scope))
|
|
128
|
+
errors.push({ field: `settings.${e.field}`, message: e.message });
|
|
129
|
+
if (typeof node.css === "string" && node.css !== "") {
|
|
130
|
+
const problem = cssProblem(node.css);
|
|
131
|
+
if (problem)
|
|
132
|
+
errors.push({ field: "css", message: problem });
|
|
133
|
+
}
|
|
104
134
|
return errors;
|
|
105
135
|
}
|
|
136
|
+
/** A custom section node (spec 2026-09-14 §6): the columns' richtext (element nodes included), the row structure, the css.
|
|
137
|
+
* The generic walk's own words on `rows.<i>.spans` are dropped (`spans` is no descriptor type it knows) so rowsProblems
|
|
138
|
+
* is the one voice on structure; the columns' richtext errors keep their paths. */
|
|
139
|
+
export function validateCustomNode(node, fields, elements = {}) {
|
|
140
|
+
const errors = validateNode(node, { fields }, elements).filter((e) => !e.field.startsWith("rows.") || e.field.includes(".columns."));
|
|
141
|
+
return [...rowsProblems(node.fields?.rows), ...errors];
|
|
142
|
+
}
|
package/dist/collections.d.ts
CHANGED
|
@@ -9,10 +9,10 @@ export type EntryOrder = "manual" | {
|
|
|
9
9
|
export declare const DEFAULT_ORDER: EntryOrder;
|
|
10
10
|
export declare const BUILT_IN_ORDER_FIELDS: readonly ["createdAt", "updatedAt"];
|
|
11
11
|
export type CollectionSchema<F extends BuilderMap = BuilderMap, N extends string = string> = {
|
|
12
|
-
/** "
|
|
13
|
-
* 4): a
|
|
12
|
+
/** "global" marks a code-declared singleton (spec 2026-09-07 decision
|
|
13
|
+
* 4): a global item IS a collection with one entry, so both entry
|
|
14
14
|
* stores accept it in the same list and branch on this. */
|
|
15
|
-
kind: "collection" | "
|
|
15
|
+
kind: "collection" | "global";
|
|
16
16
|
/** Literal when declared inline (spec 2026-09-11 §6), so a queries object can check names. */
|
|
17
17
|
name: N;
|
|
18
18
|
title: string;
|
package/dist/collections.js
CHANGED
|
@@ -5,7 +5,7 @@ export const collectionDepth = (c) => c.tree?.depth ?? 1;
|
|
|
5
5
|
export function collection(input) {
|
|
6
6
|
if (!input?.name)
|
|
7
7
|
throw new Error("smoodly: a collection requires a name.");
|
|
8
|
-
if (["
|
|
8
|
+
if (["global", "asset", "media"].includes(input.name))
|
|
9
9
|
throw new Error(`smoodly: "${input.name}" is a reserved name.`);
|
|
10
10
|
if (input.tree !== undefined && (!Number.isInteger(input.tree.depth) || input.tree.depth < 1)) {
|
|
11
11
|
throw new Error(`smoodly: collection "${input.name}" declares an invalid tree depth.`);
|