smoodly 0.0.12 → 0.0.13
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 +219 -25
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +9 -11
- package/dist/admin/editor/EditorView.js +130 -25
- package/dist/admin/editor/FloatingToolbar.d.ts +16 -0
- package/dist/admin/editor/FloatingToolbar.js +14 -0
- package/dist/admin/editor/InlineRichtext.d.ts +9 -0
- package/dist/admin/editor/InlineRichtext.js +50 -0
- package/dist/admin/editor/Outline.d.ts +16 -10
- package/dist/admin/editor/Outline.js +13 -10
- package/dist/admin/editor/inline-dom.d.ts +27 -0
- package/dist/admin/editor/inline-dom.js +43 -0
- package/dist/admin/editor/inline-session.d.ts +30 -0
- package/dist/admin/editor/inline-session.js +19 -0
- package/dist/admin/editor/protocol.d.ts +22 -1
- package/dist/admin/editor/useCanvasBridge.d.ts +8 -4
- package/dist/admin/editor/useCanvasBridge.js +12 -7
- package/dist/admin/editor/useInlineSession.d.ts +46 -0
- package/dist/admin/editor/useInlineSession.js +244 -0
- package/dist/admin/field-paths.d.ts +13 -0
- package/dist/admin/field-paths.js +49 -0
- package/dist/admin/forms/FieldWidget.d.ts +7 -2
- package/dist/admin/forms/FieldWidget.js +12 -40
- package/dist/admin/forms/ListField.d.ts +10 -0
- package/dist/admin/forms/ListField.js +42 -0
- package/dist/admin/forms/MediaWidget.js +10 -3
- package/dist/admin/forms/RichtextElementCard.d.ts +3 -0
- package/dist/admin/forms/RichtextElementCard.js +37 -0
- package/dist/admin/forms/RichtextImageCard.d.ts +3 -0
- package/dist/admin/forms/RichtextImageCard.js +42 -0
- package/dist/admin/forms/RichtextInsertMenu.d.ts +6 -0
- package/dist/admin/forms/RichtextInsertMenu.js +6 -0
- package/dist/admin/forms/RichtextPopovers.d.ts +7 -0
- package/dist/admin/forms/RichtextPopovers.js +25 -0
- package/dist/admin/forms/RichtextToolbar.d.ts +4 -2
- package/dist/admin/forms/RichtextToolbar.js +4 -2
- package/dist/admin/forms/RichtextWidget.d.ts +4 -1
- package/dist/admin/forms/RichtextWidget.js +23 -120
- package/dist/admin/forms/elements-context.d.ts +7 -0
- package/dist/admin/forms/elements-context.js +13 -0
- package/dist/admin/forms/reveal.d.ts +5 -0
- package/dist/admin/forms/reveal.js +47 -0
- package/dist/admin/forms/richtext-extensions.d.ts +8 -0
- package/dist/admin/forms/richtext-extensions.js +11 -0
- package/dist/admin/forms/richtext-form-context.d.ts +16 -0
- package/dist/admin/forms/richtext-form-context.js +18 -0
- package/dist/admin/forms/richtext-insert.d.ts +38 -0
- package/dist/admin/forms/richtext-insert.js +37 -0
- package/dist/admin/forms/richtext-nodes.d.ts +37 -0
- package/dist/admin/forms/richtext-nodes.js +85 -0
- package/dist/admin/forms/richtext-paste.js +5 -0
- package/dist/admin/forms/tabs.d.ts +3 -0
- package/dist/admin/forms/tabs.js +6 -0
- package/dist/admin/forms/useRichtextEditor.d.ts +51 -0
- package/dist/admin/forms/useRichtextEditor.js +281 -0
- package/dist/admin/next/bridge.js +51 -20
- package/dist/admin/ops-impl.js +13 -8
- package/dist/admin/richtext-doc.d.ts +3 -0
- package/dist/admin/richtext-doc.js +12 -0
- package/dist/admin/serialize.d.ts +7 -0
- package/dist/admin/serialize.js +16 -1
- package/dist/admin/shell/AdminApp.js +2 -1
- package/dist/admin/shell/EntriesList.js +34 -12
- package/dist/admin/shell/EntryForm.js +22 -15
- package/dist/admin/shell/IconRail.js +6 -5
- package/dist/admin/shell/ListView.d.ts +9 -2
- package/dist/admin/shell/ListView.js +7 -6
- package/dist/admin/shell/LoginView.js +2 -2
- package/dist/admin/shell/PagesList.js +53 -24
- package/dist/admin/shell/SharedForm.js +17 -11
- package/dist/admin/shell/SharedList.js +5 -3
- package/dist/admin/shell/entries-list.d.ts +4 -0
- package/dist/admin/shell/entries-list.js +6 -0
- package/dist/admin/shell/pages-tree.d.ts +24 -0
- package/dist/admin/shell/pages-tree.js +59 -1
- package/dist/admin/tree-ops.d.ts +9 -1
- package/dist/admin/tree-ops.js +27 -0
- package/dist/admin/ui/LocaleSwitcher.d.ts +10 -0
- package/dist/admin/ui/LocaleSwitcher.js +28 -8
- package/dist/admin/ui/Modal.js +14 -2
- package/dist/admin/ui/ResizeHandle.d.ts +1 -0
- package/dist/admin/ui/ResizeHandle.js +127 -0
- package/dist/admin/ui/primitives.d.ts +29 -4
- package/dist/admin/ui/primitives.js +29 -10
- package/dist/admin/ui/resize.d.ts +16 -0
- package/dist/admin/ui/resize.js +23 -0
- package/dist/admin/ui/sortable.d.ts +23 -0
- package/dist/admin/ui/sortable.js +57 -0
- package/dist/admin/ui/theme.d.ts +3 -1
- package/dist/admin/ui/theme.js +512 -165
- package/dist/admin/ui/useSortable.d.ts +35 -0
- package/dist/admin/ui/useSortable.js +220 -0
- package/dist/admin/validate.d.ts +6 -1
- package/dist/admin/validate.js +28 -9
- package/dist/config.d.ts +1 -0
- package/dist/config.js +36 -0
- package/dist/entry-store.d.ts +5 -2
- package/dist/entry-store.js +4 -3
- package/dist/fields.d.ts +4 -0
- package/dist/fields.js +2 -2
- package/dist/image/SmoodlyImage.d.ts +4 -1
- package/dist/image/SmoodlyImage.js +2 -2
- package/dist/index.d.ts +8 -1
- package/dist/index.js +5 -1
- package/dist/marks.d.ts +53 -0
- package/dist/marks.js +43 -0
- package/dist/media.d.ts +3 -2
- package/dist/media.js +48 -6
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +33 -12
- package/dist/render.js +2 -1
- package/dist/resolve.d.ts +8 -2
- package/dist/resolve.js +8 -7
- package/dist/richtext/index.d.ts +12 -0
- package/dist/richtext/index.js +9 -0
- package/dist/richtext-render.d.ts +22 -2
- package/dist/richtext-render.js +40 -13
- package/dist/richtext-walk.d.ts +44 -0
- package/dist/richtext-walk.js +46 -0
- package/dist/schema.d.ts +1 -1
- package/dist/schema.js +13 -1
- package/dist/section-wrapper.js +2 -1
- package/dist/site.d.ts +2 -0
- package/dist/site.js +7 -4
- package/dist/store.d.ts +3 -1
- package/dist/store.js +4 -4
- package/dist/supabase-entry-store.d.ts +1 -0
- package/dist/supabase-entry-store.js +2 -1
- package/dist/supabase-store.js +1 -1
- package/dist/toolset.d.ts +7 -1
- package/dist/toolset.js +13 -4
- package/package.json +6 -1
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type CSSProperties, type KeyboardEvent, type MouseEvent, type PointerEvent as ReactPointerEvent, type RefObject } from "react";
|
|
2
|
+
/** A tree surface's reading of a slot; `index` undefined = append (last child). */
|
|
3
|
+
export type Projection = {
|
|
4
|
+
depth: number;
|
|
5
|
+
parentId: string | null;
|
|
6
|
+
index: number | undefined;
|
|
7
|
+
};
|
|
8
|
+
export type SortableOptions = {
|
|
9
|
+
/** How many rows the container's first children are. */
|
|
10
|
+
count: number;
|
|
11
|
+
/** `to` is the index in the RESULTING order. Return a promise to hold the rows in place until it settles. */
|
|
12
|
+
onDrop(from: number, to: number, projection: Projection | null): void | Promise<unknown>;
|
|
13
|
+
/** Tree surfaces: refine the slot from the pointer's x drift (px); null = invalid slot. */
|
|
14
|
+
project?(slot: number, from: number, xDrift: number): Projection | null;
|
|
15
|
+
/** Fires once when the threshold is crossed, BEFORE the rows are measured. */
|
|
16
|
+
onDragStart?(from: number): void;
|
|
17
|
+
};
|
|
18
|
+
export type SortableActive = {
|
|
19
|
+
from: number;
|
|
20
|
+
to: number;
|
|
21
|
+
projection: Projection | null;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
};
|
|
24
|
+
export type Sortable = {
|
|
25
|
+
containerRef: RefObject<HTMLDivElement | null>;
|
|
26
|
+
handleProps(index: number): {
|
|
27
|
+
onPointerDown(e: ReactPointerEvent<HTMLElement>): void;
|
|
28
|
+
onKeyDown(e: KeyboardEvent<HTMLElement>): void;
|
|
29
|
+
onClick(e: MouseEvent<HTMLElement>): void;
|
|
30
|
+
};
|
|
31
|
+
rowStyle(index: number): CSSProperties;
|
|
32
|
+
rowClass(index: number): string | undefined;
|
|
33
|
+
active: SortableActive | null;
|
|
34
|
+
};
|
|
35
|
+
export declare function useSortable(options: SortableOptions): Sortable;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
// One drag gesture for every ordered row list in the admin (spec
|
|
3
|
+
// 2026-09-09 §2–§3, §5): the maths lives in ./sortable.ts; this hook
|
|
4
|
+
// owns the pointer, the one-time row measurement, autoscroll, the
|
|
5
|
+
// keyboard step and the pending hold. Rows are never reordered in React
|
|
6
|
+
// state during a drag — only `onDrop` changes data.
|
|
7
|
+
import { useEffect, useLayoutEffect, useRef, useState, } from "react";
|
|
8
|
+
import { clampOffset, gapTransforms, restingOffset, slotAt, stepIndex } from "./sortable.js";
|
|
9
|
+
const THRESHOLD = 4;
|
|
10
|
+
const EDGE = 32;
|
|
11
|
+
const MAX_SPEED = 14;
|
|
12
|
+
const TRANSITION = "transform 120ms ease";
|
|
13
|
+
const scrollParent = (el) => {
|
|
14
|
+
for (let node = el; node; node = node.parentElement) {
|
|
15
|
+
const overflow = window.getComputedStyle(node).overflowY;
|
|
16
|
+
if (overflow === "auto" || overflow === "scroll")
|
|
17
|
+
return node;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
};
|
|
21
|
+
const sameProjection = (a, b) => a === b || (a !== null && b !== null && a.depth === b.depth && a.parentId === b.parentId && a.index === b.index);
|
|
22
|
+
/** The click that follows a real drag lands on the row (or its table) and
|
|
23
|
+
* would open it — eat exactly one click, and give up if none comes. */
|
|
24
|
+
const suppressNextClick = () => {
|
|
25
|
+
const eat = (e) => { e.stopPropagation(); e.preventDefault(); };
|
|
26
|
+
window.addEventListener("click", eat, { capture: true, once: true });
|
|
27
|
+
window.setTimeout(() => window.removeEventListener("click", eat, { capture: true }), 0);
|
|
28
|
+
};
|
|
29
|
+
const measureRows = (container, count) => {
|
|
30
|
+
const top = container.getBoundingClientRect().top;
|
|
31
|
+
return Array.from(container.children).slice(0, count).map((child) => {
|
|
32
|
+
const r = child.getBoundingClientRect();
|
|
33
|
+
return { top: r.top - top, height: r.height };
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
export function useSortable(options) {
|
|
37
|
+
const containerRef = useRef(null);
|
|
38
|
+
const latest = useRef(options);
|
|
39
|
+
latest.current = options;
|
|
40
|
+
const pointer = useRef(null);
|
|
41
|
+
const [active, setActive] = useState(null);
|
|
42
|
+
const [hold, setHold] = useState(null);
|
|
43
|
+
const holdRef = useRef(null);
|
|
44
|
+
holdRef.current = hold;
|
|
45
|
+
// Measure once the threshold render has committed: onDragStart may have
|
|
46
|
+
// collapsed rows (the pages tree), and those must be gone first.
|
|
47
|
+
useLayoutEffect(() => {
|
|
48
|
+
const p = pointer.current;
|
|
49
|
+
const container = containerRef.current;
|
|
50
|
+
if (!active || !p || p.rects || !container)
|
|
51
|
+
return;
|
|
52
|
+
p.rects = measureRows(container, latest.current.count);
|
|
53
|
+
p.scroller = scrollParent(container);
|
|
54
|
+
}, [active !== null]);
|
|
55
|
+
useEffect(() => () => pointer.current?.detach(), []);
|
|
56
|
+
const commit = (from, to, projection, rects) => {
|
|
57
|
+
const result = latest.current.onDrop(from, to, projection);
|
|
58
|
+
if (result && typeof result.then === "function") {
|
|
59
|
+
const next = { from, to, rects };
|
|
60
|
+
setHold(next);
|
|
61
|
+
const clear = () => setHold((h) => (h === next ? null : h));
|
|
62
|
+
result.then(clear, clear);
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
const update = (clientX, clientY) => {
|
|
66
|
+
const p = pointer.current;
|
|
67
|
+
const container = containerRef.current;
|
|
68
|
+
if (!p || !p.started || !p.rects || !container)
|
|
69
|
+
return;
|
|
70
|
+
const { project } = latest.current;
|
|
71
|
+
const raw = clientY - container.getBoundingClientRect().top - p.startCY;
|
|
72
|
+
const own = p.rects[p.from];
|
|
73
|
+
// The slot follows the pointer; the row's transform is held inside the
|
|
74
|
+
// rows' extent, since past it the scroller grows and the edge
|
|
75
|
+
// autoscroll never reaches an end.
|
|
76
|
+
const to = slotAt(p.rects, p.from, own.top + raw + own.height / 2);
|
|
77
|
+
const dy = clampOffset(p.rects, p.from, raw);
|
|
78
|
+
const projection = project ? project(to, p.from, clientX - p.startX) : null;
|
|
79
|
+
const valid = !project || projection !== null;
|
|
80
|
+
p.dy = dy;
|
|
81
|
+
const row = container.children[p.from];
|
|
82
|
+
if (row)
|
|
83
|
+
row.style.transform = `translateY(${dy}px)`;
|
|
84
|
+
if (to !== p.to || valid !== p.valid || !sameProjection(projection, p.projection)) {
|
|
85
|
+
p.to = to;
|
|
86
|
+
p.valid = valid;
|
|
87
|
+
p.projection = projection;
|
|
88
|
+
setActive({ from: p.from, to, projection, valid });
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const tick = () => {
|
|
92
|
+
const p = pointer.current;
|
|
93
|
+
if (!p || !p.started)
|
|
94
|
+
return;
|
|
95
|
+
const s = p.scroller;
|
|
96
|
+
if (s) {
|
|
97
|
+
const r = s.getBoundingClientRect();
|
|
98
|
+
let delta = 0;
|
|
99
|
+
if (p.lastY < r.top + EDGE)
|
|
100
|
+
delta = -Math.min(MAX_SPEED, (r.top + EDGE - p.lastY) / 2);
|
|
101
|
+
else if (p.lastY > r.bottom - EDGE)
|
|
102
|
+
delta = Math.min(MAX_SPEED, (p.lastY - (r.bottom - EDGE)) / 2);
|
|
103
|
+
if (delta !== 0) {
|
|
104
|
+
s.scrollTop += delta;
|
|
105
|
+
update(p.lastX, p.lastY);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
p.raf = window.requestAnimationFrame(tick);
|
|
109
|
+
};
|
|
110
|
+
const finish = (drop) => {
|
|
111
|
+
const p = pointer.current;
|
|
112
|
+
if (!p)
|
|
113
|
+
return;
|
|
114
|
+
p.detach();
|
|
115
|
+
window.cancelAnimationFrame(p.raf);
|
|
116
|
+
document.body.style.userSelect = "";
|
|
117
|
+
pointer.current = null;
|
|
118
|
+
if (!p.started)
|
|
119
|
+
return;
|
|
120
|
+
suppressNextClick();
|
|
121
|
+
const changed = p.to !== p.from || (p.projection !== null);
|
|
122
|
+
if (drop && p.valid && p.rects && changed)
|
|
123
|
+
commit(p.from, p.to, p.projection, p.rects);
|
|
124
|
+
setActive(null);
|
|
125
|
+
};
|
|
126
|
+
const onPointerDown = (index) => (e) => {
|
|
127
|
+
if (e.button !== 0 || pointer.current || holdRef.current)
|
|
128
|
+
return;
|
|
129
|
+
const container = containerRef.current;
|
|
130
|
+
if (!container)
|
|
131
|
+
return;
|
|
132
|
+
e.preventDefault();
|
|
133
|
+
const move = (ev) => {
|
|
134
|
+
const p = pointer.current;
|
|
135
|
+
if (!p)
|
|
136
|
+
return;
|
|
137
|
+
p.lastX = ev.clientX;
|
|
138
|
+
p.lastY = ev.clientY;
|
|
139
|
+
if (!p.started) {
|
|
140
|
+
if (Math.abs(ev.clientX - p.startX) + Math.abs(ev.clientY - p.startY) < THRESHOLD)
|
|
141
|
+
return;
|
|
142
|
+
p.started = true;
|
|
143
|
+
document.body.style.userSelect = "none";
|
|
144
|
+
latest.current.onDragStart?.(p.from);
|
|
145
|
+
setActive({ from: p.from, to: p.from, projection: null, valid: true });
|
|
146
|
+
p.raf = window.requestAnimationFrame(tick);
|
|
147
|
+
return; // measured in the layout effect; the next move positions the row
|
|
148
|
+
}
|
|
149
|
+
ev.preventDefault();
|
|
150
|
+
update(ev.clientX, ev.clientY);
|
|
151
|
+
};
|
|
152
|
+
const up = () => finish(true);
|
|
153
|
+
const cancel = () => finish(false);
|
|
154
|
+
const key = (ev) => { if (ev.key === "Escape")
|
|
155
|
+
finish(false); };
|
|
156
|
+
pointer.current = {
|
|
157
|
+
from: index, startX: e.clientX, startY: e.clientY,
|
|
158
|
+
startCY: e.clientY - container.getBoundingClientRect().top,
|
|
159
|
+
lastX: e.clientX, lastY: e.clientY, started: false,
|
|
160
|
+
rects: null, scroller: null, raf: 0,
|
|
161
|
+
to: index, dy: 0, projection: null, valid: true,
|
|
162
|
+
detach() {
|
|
163
|
+
window.removeEventListener("pointermove", move);
|
|
164
|
+
window.removeEventListener("pointerup", up);
|
|
165
|
+
window.removeEventListener("pointercancel", cancel);
|
|
166
|
+
window.removeEventListener("keydown", key);
|
|
167
|
+
},
|
|
168
|
+
};
|
|
169
|
+
window.addEventListener("pointermove", move);
|
|
170
|
+
window.addEventListener("pointerup", up);
|
|
171
|
+
window.addEventListener("pointercancel", cancel);
|
|
172
|
+
window.addEventListener("keydown", key);
|
|
173
|
+
};
|
|
174
|
+
const onKeyDown = (index) => (e) => {
|
|
175
|
+
e.stopPropagation(); // the row's own Enter/Space handler must not see the grip's keys
|
|
176
|
+
if (e.key !== "ArrowUp" && e.key !== "ArrowDown")
|
|
177
|
+
return;
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
if (pointer.current || holdRef.current)
|
|
180
|
+
return;
|
|
181
|
+
const container = containerRef.current;
|
|
182
|
+
const { count, project } = latest.current;
|
|
183
|
+
const to = stepIndex(count, index, e.key === "ArrowUp" ? -1 : 1);
|
|
184
|
+
if (to === null || !container)
|
|
185
|
+
return;
|
|
186
|
+
const projection = project ? project(to, index, 0) : null;
|
|
187
|
+
if (project && projection === null)
|
|
188
|
+
return;
|
|
189
|
+
commit(index, to, projection, measureRows(container, count));
|
|
190
|
+
};
|
|
191
|
+
const onClick = (e) => { e.stopPropagation(); e.preventDefault(); };
|
|
192
|
+
const rowStyle = (index) => {
|
|
193
|
+
const p = pointer.current;
|
|
194
|
+
if (active && p?.rects) {
|
|
195
|
+
if (index === active.from)
|
|
196
|
+
return { transform: `translateY(${p.dy}px)`, transition: "none" };
|
|
197
|
+
const shift = gapTransforms(p.rects, active.from, active.to)[index] ?? 0;
|
|
198
|
+
return { transform: `translateY(${shift}px)`, transition: TRANSITION };
|
|
199
|
+
}
|
|
200
|
+
if (hold) {
|
|
201
|
+
const y = index === hold.from
|
|
202
|
+
? restingOffset(hold.rects, hold.from, hold.to)
|
|
203
|
+
: gapTransforms(hold.rects, hold.from, hold.to)[index] ?? 0;
|
|
204
|
+
return { transform: `translateY(${y}px)`, transition: TRANSITION };
|
|
205
|
+
}
|
|
206
|
+
return {};
|
|
207
|
+
};
|
|
208
|
+
const rowClass = (index) => {
|
|
209
|
+
if (!active || index !== active.from)
|
|
210
|
+
return undefined;
|
|
211
|
+
return active.valid ? "sm-drag--lifted" : "sm-drag--lifted sm-drag--invalid";
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
containerRef,
|
|
215
|
+
handleProps: (index) => ({ onPointerDown: onPointerDown(index), onKeyDown: onKeyDown(index), onClick }),
|
|
216
|
+
rowStyle,
|
|
217
|
+
rowClass,
|
|
218
|
+
active,
|
|
219
|
+
};
|
|
220
|
+
}
|
package/dist/admin/validate.d.ts
CHANGED
|
@@ -11,5 +11,10 @@ type Desc = {
|
|
|
11
11
|
fields?: unknown;
|
|
12
12
|
item?: unknown;
|
|
13
13
|
};
|
|
14
|
-
|
|
14
|
+
/** The element schemas an element node is validated against (spec 2026-09-08 block nodes §2). */
|
|
15
|
+
export type ElementDescs = Record<string, {
|
|
16
|
+
fields: Record<string, Desc>;
|
|
17
|
+
styles?: Record<string, Desc>;
|
|
18
|
+
}>;
|
|
19
|
+
export declare function validateFields(values: Record<string, unknown>, descriptors: Record<string, Desc>, elements?: ElementDescs): FieldError[];
|
|
15
20
|
export {};
|
package/dist/admin/validate.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
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
|
-
import {
|
|
5
|
+
import { hasContent } from "./richtext-doc.js";
|
|
6
|
+
import { ELEMENT_NODE, richtextBlocks } from "../richtext-walk.js";
|
|
6
7
|
const subfields = (d) => d.fields && typeof d.fields === "object" ? d.fields : {};
|
|
7
8
|
const itemOf = (d) => d.item && typeof d.item === "object" ? d.item : undefined;
|
|
8
9
|
const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
|
|
@@ -10,15 +11,33 @@ const isEmpty = (v) => v === undefined || v === null || v === "" || (Array.isArr
|
|
|
10
11
|
* may be absent — may be absent altogether. */
|
|
11
12
|
const mayBeAbsent = (d) => d.optional === true || (d.type === "object" && Object.values(subfields(d)).every(mayBeAbsent));
|
|
12
13
|
const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
|
|
13
|
-
function validateValue(path, value, d, errors) {
|
|
14
|
+
function validateValue(path, value, d, errors, elements) {
|
|
14
15
|
if (isEmpty(value)) {
|
|
15
16
|
if (!mayBeAbsent(d))
|
|
16
17
|
errors.push({ field: path, message: "Required." });
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
19
|
-
if (d.type === "richtext"
|
|
20
|
-
if (!
|
|
21
|
-
|
|
20
|
+
if (d.type === "richtext") {
|
|
21
|
+
if (!hasContent(value)) {
|
|
22
|
+
if (!mayBeAbsent(d))
|
|
23
|
+
errors.push({ field: path, message: "Required." });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
// Element nodes (spec 2026-09-08 block nodes §2): fields validated against
|
|
27
|
+
// the element's descriptors, indexed among the document's element nodes.
|
|
28
|
+
let i = 0;
|
|
29
|
+
for (const node of richtextBlocks(value)) {
|
|
30
|
+
if (node.type !== ELEMENT_NODE)
|
|
31
|
+
continue;
|
|
32
|
+
const a = node.attrs;
|
|
33
|
+
const el = typeof a.name === "string" ? elements[a.name] : undefined;
|
|
34
|
+
if (el) {
|
|
35
|
+
const fields = (a.fields && typeof a.fields === "object" ? a.fields : {});
|
|
36
|
+
for (const [k, sub] of Object.entries(el.fields))
|
|
37
|
+
validateValue(`${path}.elements.${i}.${k}`, fields[k], sub, errors, elements);
|
|
38
|
+
}
|
|
39
|
+
i++;
|
|
40
|
+
}
|
|
22
41
|
return;
|
|
23
42
|
}
|
|
24
43
|
if (d.type === "image" || d.type === "video" || d.type === "file") {
|
|
@@ -38,7 +57,7 @@ function validateValue(path, value, d, errors) {
|
|
|
38
57
|
errors.push({ field: path, message: `At most ${plural(d.max, "item")}.` });
|
|
39
58
|
const item = itemOf(d);
|
|
40
59
|
if (item)
|
|
41
|
-
value.forEach((v, i) => validateValue(`${path}.${i}`, v, item, errors));
|
|
60
|
+
value.forEach((v, i) => validateValue(`${path}.${i}`, v, item, errors, elements));
|
|
42
61
|
return;
|
|
43
62
|
}
|
|
44
63
|
if (d.type === "object") {
|
|
@@ -47,7 +66,7 @@ function validateValue(path, value, d, errors) {
|
|
|
47
66
|
return;
|
|
48
67
|
}
|
|
49
68
|
for (const [k, sub] of Object.entries(subfields(d)))
|
|
50
|
-
validateValue(`${path}.${k}`, value[k], sub, errors);
|
|
69
|
+
validateValue(`${path}.${k}`, value[k], sub, errors, elements);
|
|
51
70
|
return;
|
|
52
71
|
}
|
|
53
72
|
if (d.type === "select" && Array.isArray(d.options) && !d.options.includes(value)) {
|
|
@@ -57,9 +76,9 @@ function validateValue(path, value, d, errors) {
|
|
|
57
76
|
errors.push({ field: path, message: `Must be at most ${d.max} characters.` });
|
|
58
77
|
}
|
|
59
78
|
}
|
|
60
|
-
export function validateFields(values, descriptors) {
|
|
79
|
+
export function validateFields(values, descriptors, elements = {}) {
|
|
61
80
|
const errors = [];
|
|
62
81
|
for (const [field, d] of Object.entries(descriptors))
|
|
63
|
-
validateValue(field, values[field], d, errors);
|
|
82
|
+
validateValue(field, values[field], d, errors, elements);
|
|
64
83
|
return errors;
|
|
65
84
|
}
|
package/dist/config.d.ts
CHANGED
package/dist/config.js
CHANGED
|
@@ -125,10 +125,46 @@ function assertLocales(config) {
|
|
|
125
125
|
if (!supported.includes(def))
|
|
126
126
|
throw new Error(`smoodly: the default locale "${def}" is not in the supported list.`);
|
|
127
127
|
}
|
|
128
|
+
/** Every element name a richtext toolset embeds, walking objects and lists. */
|
|
129
|
+
function toolsetElementNames(descriptors, out) {
|
|
130
|
+
for (const d of Object.values(descriptors ?? {})) {
|
|
131
|
+
if (d.type === "richtext") {
|
|
132
|
+
const t = d.toolset;
|
|
133
|
+
if (Array.isArray(t?.elements))
|
|
134
|
+
for (const n of t.elements)
|
|
135
|
+
if (typeof n === "string")
|
|
136
|
+
out.add(n);
|
|
137
|
+
}
|
|
138
|
+
else if (d.type === "object") {
|
|
139
|
+
toolsetElementNames(d.fields, out);
|
|
140
|
+
}
|
|
141
|
+
else if (d.type === "list") {
|
|
142
|
+
toolsetElementNames({ item: d.item }, out);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** A field can never offer an element the admin cannot serialize (spec 2026-09-08 block nodes §1). */
|
|
147
|
+
function assertToolsetElements(config) {
|
|
148
|
+
const registered = new Set((config.registry.elements ?? []).map((e) => e.schema.name));
|
|
149
|
+
const wanted = new Set();
|
|
150
|
+
for (const s of config.registry.sections ?? [])
|
|
151
|
+
toolsetElementNames(s.schema.fields, wanted);
|
|
152
|
+
for (const e of config.registry.elements ?? [])
|
|
153
|
+
toolsetElementNames(e.schema.fields, wanted);
|
|
154
|
+
for (const c of config.registry.collections ?? [])
|
|
155
|
+
toolsetElementNames(c.fields, wanted);
|
|
156
|
+
for (const s of config.registry.shared ?? [])
|
|
157
|
+
toolsetElementNames(s.fields, wanted);
|
|
158
|
+
for (const name of wanted) {
|
|
159
|
+
if (!registered.has(name))
|
|
160
|
+
throw new Error(`smoodly: a toolset embeds element "${name}", which is not in registry.elements.`);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
128
163
|
export function defineConfig(config) {
|
|
129
164
|
assertLocales(config);
|
|
130
165
|
assertUniqueNames(config);
|
|
131
166
|
assertSharedSections(config);
|
|
167
|
+
assertToolsetElements(config);
|
|
132
168
|
assertRootSegments(config);
|
|
133
169
|
const depth = config.pages?.tree?.depth ?? 1;
|
|
134
170
|
if (!Number.isInteger(depth) || depth < 1)
|
package/dist/entry-store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CollectionSchema, EntryOrder } from "./collections.ts";
|
|
2
2
|
import type { SharedSchema } from "./shared.ts";
|
|
3
|
-
import { type RefEdge } from "./refs.ts";
|
|
3
|
+
import { type RefEdge, type SectionLike } from "./refs.ts";
|
|
4
4
|
import { MemoryRefIndex } from "./ref-index.ts";
|
|
5
5
|
import { type PathIndex } from "./path-index.ts";
|
|
6
6
|
import { type LocaleSet, type PathTarget } from "./paths.ts";
|
|
@@ -63,6 +63,8 @@ export type EntryStoreOptions = {
|
|
|
63
63
|
paths?: PathIndex;
|
|
64
64
|
locales?: LocaleSet;
|
|
65
65
|
shared?: SharedSchema[];
|
|
66
|
+
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
67
|
+
elements?: SectionLike[];
|
|
66
68
|
};
|
|
67
69
|
export interface EntryStore {
|
|
68
70
|
/** Creates the node and ONE locale row from the merged `fields`: shared
|
|
@@ -166,7 +168,7 @@ export declare function entryFieldsIn(record: EntryRecord, locale: string): Reco
|
|
|
166
168
|
* moved past. Safe-delete must hold while ANY of them shows the target,
|
|
167
169
|
* since the published site still renders it (DESIGN.md §3 "Page edges
|
|
168
170
|
* in refs", the same stance). */
|
|
169
|
-
export declare function entryRefEdges(fieldSets: Record<string, unknown>[], schema: CollectionSchema<any
|
|
171
|
+
export declare function entryRefEdges(fieldSets: Record<string, unknown>[], schema: CollectionSchema<any>, elements?: SectionLike[]): RefEdge[];
|
|
170
172
|
/** What a message calls an entry: its title field in that locale, else its id. */
|
|
171
173
|
export declare function entryTitleIn(record: EntryRecord, locale: string, schema: CollectionSchema<any>): string;
|
|
172
174
|
/**
|
|
@@ -192,6 +194,7 @@ export declare class MemoryEntryStore implements EntryStore {
|
|
|
192
194
|
private paths;
|
|
193
195
|
private locales;
|
|
194
196
|
private schemas;
|
|
197
|
+
private elements;
|
|
195
198
|
constructor(collections: CollectionSchema<any>[], refs?: MemoryRefIndex, options?: EntryStoreOptions);
|
|
196
199
|
private schema;
|
|
197
200
|
private row;
|
package/dist/entry-store.js
CHANGED
|
@@ -92,10 +92,10 @@ export function entryFieldsIn(record, locale) {
|
|
|
92
92
|
* moved past. Safe-delete must hold while ANY of them shows the target,
|
|
93
93
|
* since the published site still renders it (DESIGN.md §3 "Page edges
|
|
94
94
|
* in refs", the same stance). */
|
|
95
|
-
export function entryRefEdges(fieldSets, schema) {
|
|
95
|
+
export function entryRefEdges(fieldSets, schema, elements) {
|
|
96
96
|
const edges = [];
|
|
97
97
|
for (const fields of fieldSets)
|
|
98
|
-
edges.push(...collectEntryRefs(fields, schema));
|
|
98
|
+
edges.push(...collectEntryRefs(fields, schema, elements));
|
|
99
99
|
return edges;
|
|
100
100
|
}
|
|
101
101
|
/** What a message calls an entry: its title field in that locale, else its id. */
|
|
@@ -148,6 +148,7 @@ export class MemoryEntryStore {
|
|
|
148
148
|
this.paths = options.paths ?? new MemoryPathIndex();
|
|
149
149
|
this.locales = options.locales ?? DEFAULT_ENTRY_LOCALES;
|
|
150
150
|
this.schemas = [...collections, ...(options.shared ?? [])];
|
|
151
|
+
this.elements = options.elements;
|
|
151
152
|
}
|
|
152
153
|
schema(collection) {
|
|
153
154
|
return schemaOf(this.schemas, collection);
|
|
@@ -223,7 +224,7 @@ export class MemoryEntryStore {
|
|
|
223
224
|
await this.paths.rewrite(nodes.map((n) => ({ kind: "entry", id: n.id })), entryPathRows(nodes, this.byId, this.schema(collection), this.locales));
|
|
224
225
|
}
|
|
225
226
|
writeRefs(entry) {
|
|
226
|
-
this.refs.rewrite({ sourceKind: "entry", sourceId: entry.id }, entryRefEdges(this.liveFieldSets(entry), this.schema(entry.collection)));
|
|
227
|
+
this.refs.rewrite({ sourceKind: "entry", sourceId: entry.id }, entryRefEdges(this.liveFieldSets(entry), this.schema(entry.collection), this.elements));
|
|
227
228
|
}
|
|
228
229
|
siblings(collection, parentId, except) {
|
|
229
230
|
return [...this.entries.values()]
|
package/dist/fields.d.ts
CHANGED
|
@@ -42,6 +42,10 @@ type NotLocalized<B> = B extends {
|
|
|
42
42
|
} ? Fl extends {
|
|
43
43
|
localized: true;
|
|
44
44
|
} ? never : B : B;
|
|
45
|
+
export declare const describePath: (path: string) => string;
|
|
46
|
+
/** First path at which `matches` is true of the descriptor, walking into
|
|
47
|
+
* object subfields and a list's item, or null. */
|
|
48
|
+
export declare function findMatch(d: Descriptor, matches: (d: Descriptor) => boolean, path?: string): string | null;
|
|
45
49
|
export declare const f: {
|
|
46
50
|
text: () => FieldBuilder<string, {}>;
|
|
47
51
|
link: () => FieldBuilder<string, {}>;
|
package/dist/fields.js
CHANGED
|
@@ -22,10 +22,10 @@ function builder(descriptor) {
|
|
|
22
22
|
tab: (tab) => chain({ tab }),
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
-
const describePath = (path) => (path === "" ? "the item" : `"${path}"`);
|
|
25
|
+
export const describePath = (path) => (path === "" ? "the item" : `"${path}"`);
|
|
26
26
|
/** First path at which `matches` is true of the descriptor, walking into
|
|
27
27
|
* object subfields and a list's item, or null. */
|
|
28
|
-
function findMatch(d, matches, path = "") {
|
|
28
|
+
export function findMatch(d, matches, path = "") {
|
|
29
29
|
if (matches(d))
|
|
30
30
|
return path;
|
|
31
31
|
if (d.type === "object") {
|
|
@@ -14,5 +14,8 @@ export type SmoodlyImageProps = {
|
|
|
14
14
|
alt?: string;
|
|
15
15
|
loader?: ImageLoader;
|
|
16
16
|
unoptimized?: boolean;
|
|
17
|
+
} & {
|
|
18
|
+
/** Data attributes land on the wrapper box — a section's field mark (spec 2026-09-10 §1). */
|
|
19
|
+
[attr: `data-${string}`]: string | undefined;
|
|
17
20
|
};
|
|
18
|
-
export declare function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized }: SmoodlyImageProps): ReactElement;
|
|
21
|
+
export declare function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized, ...data }: SmoodlyImageProps): ReactElement;
|
|
@@ -5,11 +5,11 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
5
5
|
// site imports from the media layer. Server-safe — no "use client".
|
|
6
6
|
import Image from "next/image";
|
|
7
7
|
import { boxAspectOf, frameFor } from "./crop-math.js";
|
|
8
|
-
export function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized }) {
|
|
8
|
+
export function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized, ...data }) {
|
|
9
9
|
const box = boxAspectOf(image, aspect);
|
|
10
10
|
const frame = loader?.handlesCrop ? { left: 0, top: 0, width: 100, height: 100 } : frameFor(image, box);
|
|
11
11
|
const svg = image.mime === "image/svg+xml";
|
|
12
|
-
return (_jsx("span", { className: className, style: { display: "block", position: "relative", overflow: "hidden", aspectRatio: String(box), ...style }, children: _jsx(Image, { src: image.url, alt: alt ?? image.alt, width: image.width, height: image.height, sizes: sizes, priority: priority, loading: loading, unoptimized: unoptimized ?? svg, ...(loader ? { loader: ({ width, quality }) => loader.url(image, { width, quality }) } : {}), style: {
|
|
12
|
+
return (_jsx("span", { className: className, ...data, style: { display: "block", position: "relative", overflow: "hidden", aspectRatio: String(box), ...style }, children: _jsx(Image, { src: image.url, alt: alt ?? image.alt, width: image.width, height: image.height, sizes: sizes, priority: priority, loading: loading, unoptimized: unoptimized ?? svg, ...(loader ? { loader: ({ width, quality }) => loader.url(image, { width, quality }) } : {}), style: {
|
|
13
13
|
position: "absolute", left: `${frame.left}%`, top: `${frame.top}%`,
|
|
14
14
|
width: `${frame.width}%`, height: `${frame.height}%`, maxWidth: "none",
|
|
15
15
|
} }) }));
|
package/dist/index.d.ts
CHANGED
|
@@ -5,9 +5,11 @@ import { Section } from "./section-wrapper.tsx";
|
|
|
5
5
|
import { page } from "./render.tsx";
|
|
6
6
|
import { entryPage } from "./entry-page.tsx";
|
|
7
7
|
import { shared } from "./shared.ts";
|
|
8
|
+
import { marks } from "./marks.ts";
|
|
8
9
|
export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.tsx";
|
|
9
10
|
export type { PageTree, TreeNode, ZoneContent, PairedPage, Registry, PageContext, PageContextLink, PageLink, PageViewProps } from "./render.tsx";
|
|
10
|
-
export { RichText } from "./richtext-render.tsx";
|
|
11
|
+
export { RichText, defaultRichTextImage } from "./richtext-render.tsx";
|
|
12
|
+
export type { RichTextProps, RichTextElement, RichTextImageProps } from "./richtext-render.tsx";
|
|
11
13
|
export type { PageSchema } from "./page.ts";
|
|
12
14
|
export { fixedSlug } from "./page.ts";
|
|
13
15
|
export type { EntryPageSchema, PairedEntryPage, EntryPageProps } from "./entry-page.tsx";
|
|
@@ -25,6 +27,8 @@ export { SupabasePageStore } from "./supabase-store.ts";
|
|
|
25
27
|
export { SupabaseAssetStore } from "./supabase-asset-store.ts";
|
|
26
28
|
export { collectEntryRefs, collectTreeRefs } from "./refs.ts";
|
|
27
29
|
export type { RefEdge, SectionLike } from "./refs.ts";
|
|
30
|
+
export { richtextBlocks, elementSchemas, IMAGE_NODE, ELEMENT_NODE } from "./richtext-walk.ts";
|
|
31
|
+
export type { BlockNode, ElementSchemas, ImageNodeAttrs, ElementNodeAttrs } from "./richtext-walk.ts";
|
|
28
32
|
export { MemoryRefIndex } from "./ref-index.ts";
|
|
29
33
|
export { MemoryPathIndex } from "./path-index.ts";
|
|
30
34
|
export type { PathIndex } from "./path-index.ts";
|
|
@@ -126,7 +130,10 @@ export declare const smoodly: {
|
|
|
126
130
|
toolset: typeof toolset;
|
|
127
131
|
shared: typeof shared;
|
|
128
132
|
Section: typeof Section;
|
|
133
|
+
marks: typeof marks;
|
|
129
134
|
};
|
|
135
|
+
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
|
|
136
|
+
export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
|
|
130
137
|
export { resolveSmoodlyEnv, smoodlyEnv } from "./env.ts";
|
|
131
138
|
export type { SmoodlyEnv } from "./env.ts";
|
|
132
139
|
export { clientOptions, createSmoodlyClient } from "./client.ts";
|
package/dist/index.js
CHANGED
|
@@ -6,8 +6,9 @@ import { Section } from "./section-wrapper.js";
|
|
|
6
6
|
import { page } from "./render.js";
|
|
7
7
|
import { entryPage } from "./entry-page.js";
|
|
8
8
|
import { shared } from "./shared.js";
|
|
9
|
+
import { marks } from "./marks.js";
|
|
9
10
|
export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.js";
|
|
10
|
-
export { RichText } from "./richtext-render.js";
|
|
11
|
+
export { RichText, defaultRichTextImage } from "./richtext-render.js";
|
|
11
12
|
export { fixedSlug } from "./page.js";
|
|
12
13
|
export { f } from "./fields.js";
|
|
13
14
|
export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT, parseAspect, cropAspect, clamp01, isMediaValue, collectAssetIds, applyAssets, } from "./media.js";
|
|
@@ -20,6 +21,7 @@ export { imageDimensions } from "./image-dimensions.js";
|
|
|
20
21
|
export { SupabasePageStore } from "./supabase-store.js";
|
|
21
22
|
export { SupabaseAssetStore } from "./supabase-asset-store.js";
|
|
22
23
|
export { collectEntryRefs, collectTreeRefs } from "./refs.js";
|
|
24
|
+
export { richtextBlocks, elementSchemas, IMAGE_NODE, ELEMENT_NODE } from "./richtext-walk.js";
|
|
23
25
|
export { MemoryRefIndex } from "./ref-index.js";
|
|
24
26
|
export { MemoryPathIndex } from "./path-index.js";
|
|
25
27
|
export { MemoryEntryStore } from "./entry-store.js";
|
|
@@ -46,7 +48,9 @@ export const smoodly = {
|
|
|
46
48
|
toolset,
|
|
47
49
|
shared,
|
|
48
50
|
Section,
|
|
51
|
+
marks,
|
|
49
52
|
};
|
|
53
|
+
export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.js";
|
|
50
54
|
export { resolveSmoodlyEnv, smoodlyEnv } from "./env.js";
|
|
51
55
|
export { clientOptions, createSmoodlyClient } from "./client.js";
|
|
52
56
|
export { createSmoodlySite, pageContextOf } from "./site.js";
|
package/dist/marks.d.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { BuilderMap, NamespaceValues } from "./schema.ts";
|
|
2
|
+
/** A section's root, from `<smoodly.Section>` in editor mode. */
|
|
3
|
+
export declare const NODE_ATTR = "data-smd-node";
|
|
4
|
+
/** An empty zone's placeholder box, from `<Zone>` in editor mode. */
|
|
5
|
+
export declare const ZONE_ATTR = "data-smd-zone";
|
|
6
|
+
export declare const EMPTY_ATTR = "data-smd-empty";
|
|
7
|
+
/** The element that renders a field's value verbatim. */
|
|
8
|
+
export declare const FIELD_ATTR = "data-smd-field";
|
|
9
|
+
/** A list item's or object field's root: scopes the field names inside it. */
|
|
10
|
+
export declare const ITEM_ATTR = "data-smd-item";
|
|
11
|
+
/** The root of an inline editing session inside the canvas; the bridge
|
|
12
|
+
* forwards nothing from inside it, and reports no marks under it. */
|
|
13
|
+
export declare const EDITOR_ATTR = "data-smd-editor";
|
|
14
|
+
/** What the resolver needs from an element — a DOM Element satisfies it. */
|
|
15
|
+
export type MarkNode = {
|
|
16
|
+
getAttribute(name: string): string | null;
|
|
17
|
+
parentElement: MarkNode | null;
|
|
18
|
+
};
|
|
19
|
+
export type MarkPath = {
|
|
20
|
+
node: string;
|
|
21
|
+
path: string;
|
|
22
|
+
};
|
|
23
|
+
/** The section node and dotted field path of a marked element: its own
|
|
24
|
+
* field name, prefixed by every item scope from the element itself up to
|
|
25
|
+
* the section root. Null when the element carries no field mark or sits
|
|
26
|
+
* outside a section node. */
|
|
27
|
+
export declare function resolveMarkPath(el: MarkNode): MarkPath | null;
|
|
28
|
+
type Keys<T> = Extract<keyof T, string>;
|
|
29
|
+
type ElementOf<T> = NonNullable<T> extends readonly (infer E)[] ? E : never;
|
|
30
|
+
/** The helpers for one level of a section's values: the section itself,
|
|
31
|
+
* a list item, or an object field. Item attributes are relative to the
|
|
32
|
+
* parent scope — the resolver joins them — so the helpers carry no path. */
|
|
33
|
+
export type Marks<T> = {
|
|
34
|
+
/** Spread on the element that renders `key` verbatim. */
|
|
35
|
+
field<K extends Keys<T>>(key: K): {
|
|
36
|
+
"data-smd-field": K;
|
|
37
|
+
};
|
|
38
|
+
/** List item `index` of `key`: spread `.attrs` on the item's root and mark the fields inside with the returned helpers. */
|
|
39
|
+
item<K extends Keys<T>>(key: K, index: number): Scope<ElementOf<T[K]>>;
|
|
40
|
+
/** The object field `key`: spread `.attrs` on its root and mark the fields inside. */
|
|
41
|
+
object<K extends Keys<T>>(key: K): Scope<NonNullable<T[K]>>;
|
|
42
|
+
};
|
|
43
|
+
export type Scope<T> = Marks<T> & {
|
|
44
|
+
attrs: {
|
|
45
|
+
"data-smd-item": string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
/** Typed marks for a section or element schema: `const m = smoodly.marks(heroSchema)`,
|
|
49
|
+
* then `<h1 {...m.field("heading")}>`. The raw attributes stay the contract. */
|
|
50
|
+
export declare function marks<S extends {
|
|
51
|
+
__f?: BuilderMap;
|
|
52
|
+
}>(_schema: S): Marks<NamespaceValues<NonNullable<S["__f"]>>>;
|
|
53
|
+
export {};
|