smoodly 0.0.12 → 0.0.14
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 +294 -40
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +9 -11
- package/dist/admin/editor/EditorView.js +135 -28
- 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/PageInspector.d.ts +21 -0
- package/dist/admin/editor/PageInspector.js +24 -0
- package/dist/admin/editor/PageSettings.d.ts +1 -3
- package/dist/admin/editor/PageSettings.js +7 -6
- package/dist/admin/editor/SeoSettings.d.ts +7 -0
- package/dist/admin/editor/SeoSettings.js +15 -0
- 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 +35 -43
- 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 +85 -19
- package/dist/admin/richtext-doc.d.ts +3 -0
- package/dist/admin/richtext-doc.js +12 -0
- package/dist/admin/serialize.d.ts +11 -0
- package/dist/admin/serialize.js +18 -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 +11 -1
- package/dist/admin/tree-ops.js +32 -1
- 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 +38 -4
- package/dist/admin/ui/primitives.js +35 -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 +527 -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 +52 -12
- package/dist/collections.d.ts +6 -5
- package/dist/config.d.ts +1 -0
- package/dist/config.js +36 -0
- package/dist/entry-store.d.ts +23 -3
- package/dist/entry-store.js +31 -5
- package/dist/field-defaults.d.ts +8 -0
- package/dist/field-defaults.js +18 -0
- package/dist/fields.d.ts +30 -1
- package/dist/fields.js +33 -3
- package/dist/image/SmoodlyImage.d.ts +4 -1
- package/dist/image/SmoodlyImage.js +2 -2
- package/dist/index.d.ts +16 -7
- package/dist/index.js +10 -4
- 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/next/index.d.ts +2 -0
- package/dist/next/index.js +1 -0
- package/dist/next/meta.d.ts +7 -3
- package/dist/next/meta.js +18 -9
- package/dist/next/page-renderer.js +1 -1
- package/dist/next/queries.d.ts +36 -0
- package/dist/next/queries.js +87 -0
- package/dist/page-tree.d.ts +3 -0
- package/dist/page-tree.js +6 -0
- package/dist/page.d.ts +11 -3
- package/dist/page.js +26 -0
- package/dist/pairing.d.ts +2 -1
- package/dist/pairing.js +2 -9
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +33 -12
- package/dist/render.d.ts +14 -7
- package/dist/render.js +4 -3
- package/dist/resolve.d.ts +12 -2
- package/dist/resolve.js +16 -7
- package/dist/revalidate.d.ts +6 -0
- package/dist/revalidate.js +6 -0
- 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/shared.d.ts +7 -7
- package/dist/site.d.ts +25 -1
- package/dist/site.js +89 -22
- package/dist/store.d.ts +7 -1
- package/dist/store.js +13 -4
- package/dist/supabase-entry-store.d.ts +1 -0
- package/dist/supabase-entry-store.js +34 -11
- package/dist/supabase-store.d.ts +1 -0
- package/dist/supabase-store.js +14 -1
- package/dist/toolset.d.ts +7 -1
- package/dist/toolset.js +13 -4
- package/package.json +6 -1
package/dist/richtext-render.js
CHANGED
|
@@ -1,11 +1,15 @@
|
|
|
1
|
-
import {
|
|
2
|
-
// The site-side richtext renderer (spec 2026-09-07
|
|
3
|
-
// TipTap JSON → React, a fragment with no wrapper
|
|
4
|
-
// the surrounding element and its class. Pure
|
|
5
|
-
// it works in shared, server and
|
|
6
|
-
// render their children — a
|
|
7
|
-
//
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
// The site-side richtext renderer (spec 2026-09-07 list field §5; block
|
|
3
|
+
// nodes spec 2026-09-08): TipTap JSON → React, a fragment with no wrapper
|
|
4
|
+
// so the developer owns the surrounding element and its class. Pure
|
|
5
|
+
// React, no Node or Next imports, so it works in shared, server and
|
|
6
|
+
// client components alike. Unknown nodes render their children — a
|
|
7
|
+
// document from a later, richer editor degrades instead of crashing.
|
|
8
|
+
// Block nodes: an `image` node renders through the `image` render prop
|
|
9
|
+
// (default: a figure over a plain <img>); an `element` node renders the
|
|
10
|
+
// paired component named in `elements`, nothing when unknown.
|
|
8
11
|
import { Fragment, createElement } from "react";
|
|
12
|
+
import { ELEMENT_NODE, IMAGE_NODE } from "./richtext-walk.js";
|
|
9
13
|
/** Only schemes an editor's link may carry reach the page: http(s), mailto, tel,
|
|
10
14
|
* and site-relative targets. Anything else (javascript:, data:, an empty href,
|
|
11
15
|
* a protocol-relative `//host` or a backslash form `/\host` that a browser
|
|
@@ -18,19 +22,27 @@ function safeHref(raw) {
|
|
|
18
22
|
const href = raw.replace(/[\t\n\r]/g, "").trim();
|
|
19
23
|
return href !== "" && SAFE_HREF.test(href) ? href : null;
|
|
20
24
|
}
|
|
21
|
-
|
|
25
|
+
/** Lazy by default: body images sit below the fold, and a lazy <img> is
|
|
26
|
+
* the one React does not hoist a preload hint for on the server. */
|
|
27
|
+
export function defaultRichTextImage({ image, caption }) {
|
|
28
|
+
return (_jsxs("figure", { children: [_jsx("img", { src: image.url, width: image.width, height: image.height, alt: image.alt, loading: "lazy" }), caption !== "" && _jsx("figcaption", { children: caption })] }));
|
|
29
|
+
}
|
|
30
|
+
const isResolvedImage = (v) => typeof v === "object" && v !== null && typeof v.url === "string"
|
|
31
|
+
&& typeof v.width === "number" && typeof v.height === "number";
|
|
32
|
+
export function RichText({ doc, elements = [], image = defaultRichTextImage }) {
|
|
22
33
|
const d = doc;
|
|
23
34
|
if (!d || typeof d !== "object" || d.type !== "doc" || !Array.isArray(d.content))
|
|
24
35
|
return null;
|
|
25
|
-
|
|
36
|
+
const r = { elements: new Map(elements.map((e) => [e.schema.name, e])), image };
|
|
37
|
+
return _jsx(_Fragment, { children: renderNodes(d.content, r) });
|
|
26
38
|
}
|
|
27
|
-
function renderNodes(nodes) {
|
|
28
|
-
return nodes.map((n, i) => _jsx(Fragment, { children: renderNode(n) }, i));
|
|
39
|
+
function renderNodes(nodes, r) {
|
|
40
|
+
return nodes.map((n, i) => _jsx(Fragment, { children: renderNode(n, r) }, i));
|
|
29
41
|
}
|
|
30
|
-
function renderNode(node) {
|
|
42
|
+
function renderNode(node, r) {
|
|
31
43
|
if (!node || typeof node !== "object")
|
|
32
44
|
return null;
|
|
33
|
-
const children = Array.isArray(node.content) ? renderNodes(node.content) : null;
|
|
45
|
+
const children = Array.isArray(node.content) ? renderNodes(node.content, r) : null;
|
|
34
46
|
switch (node.type) {
|
|
35
47
|
case "text": return withMarks(node.text ?? "", node.marks ?? []);
|
|
36
48
|
case "paragraph": return _jsx("p", { children: children });
|
|
@@ -43,6 +55,21 @@ function renderNode(node) {
|
|
|
43
55
|
case "listItem": return _jsx("li", { children: children });
|
|
44
56
|
case "blockquote": return _jsx("blockquote", { children: children });
|
|
45
57
|
case "hardBreak": return _jsx("br", {});
|
|
58
|
+
case IMAGE_NODE: {
|
|
59
|
+
const a = (node.attrs ?? {});
|
|
60
|
+
if (!isResolvedImage(a.image))
|
|
61
|
+
return null;
|
|
62
|
+
return r.image({ image: a.image, caption: typeof a.caption === "string" ? a.caption : "" });
|
|
63
|
+
}
|
|
64
|
+
case ELEMENT_NODE: {
|
|
65
|
+
const a = (node.attrs ?? {});
|
|
66
|
+
const Component = typeof a.name === "string" ? r.elements.get(a.name) : undefined;
|
|
67
|
+
if (!Component)
|
|
68
|
+
return null;
|
|
69
|
+
const fields = a.fields && typeof a.fields === "object" ? a.fields : {};
|
|
70
|
+
const styles = a.styles && typeof a.styles === "object" ? a.styles : {};
|
|
71
|
+
return _jsx(Component, { ...fields, styles: styles });
|
|
72
|
+
}
|
|
46
73
|
default: return children;
|
|
47
74
|
}
|
|
48
75
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Descriptor } from "./fields.ts";
|
|
2
|
+
export declare const IMAGE_NODE = "image";
|
|
3
|
+
export declare const ELEMENT_NODE = "element";
|
|
4
|
+
export type ImageNodeAttrs = {
|
|
5
|
+
image?: unknown;
|
|
6
|
+
caption?: unknown;
|
|
7
|
+
};
|
|
8
|
+
export type ElementNodeAttrs = {
|
|
9
|
+
name?: unknown;
|
|
10
|
+
fields?: unknown;
|
|
11
|
+
styles?: unknown;
|
|
12
|
+
};
|
|
13
|
+
/** A block node as stored — `attrs` is the live object, so a walk may resolve in place. */
|
|
14
|
+
export type BlockNode = {
|
|
15
|
+
type: string;
|
|
16
|
+
attrs: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
/** Every image or element node in document order, at any depth. */
|
|
19
|
+
export declare function richtextBlocks(doc: unknown): BlockNode[];
|
|
20
|
+
/** An element's descriptors by name — what the walks read an element node against. */
|
|
21
|
+
export type ElementSchemas = Record<string, {
|
|
22
|
+
fields: Record<string, Descriptor>;
|
|
23
|
+
styles: Record<string, Descriptor>;
|
|
24
|
+
}>;
|
|
25
|
+
type ElementLike = {
|
|
26
|
+
name: string;
|
|
27
|
+
fields: Record<string, Descriptor>;
|
|
28
|
+
styles?: Record<string, Descriptor>;
|
|
29
|
+
} | {
|
|
30
|
+
schema: {
|
|
31
|
+
name: string;
|
|
32
|
+
fields: Record<string, Descriptor>;
|
|
33
|
+
styles?: Record<string, Descriptor>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export declare function elementSchemas(list: ElementLike[] | undefined): ElementSchemas;
|
|
37
|
+
/** The element schema an element node names, or null for an unknown one. */
|
|
38
|
+
export declare function elementOf(node: BlockNode, elements: ElementSchemas): ElementSchemas[string] | null;
|
|
39
|
+
/** An element node's field and style value sets, for a walk. */
|
|
40
|
+
export declare function elementSets(node: BlockNode, schema: ElementSchemas[string]): {
|
|
41
|
+
values: Record<string, unknown>;
|
|
42
|
+
descriptors: Record<string, Descriptor>;
|
|
43
|
+
}[];
|
|
44
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const IMAGE_NODE = "image";
|
|
2
|
+
export const ELEMENT_NODE = "element";
|
|
3
|
+
const isNode = (v) => typeof v === "object" && v !== null && typeof v.type === "string";
|
|
4
|
+
/** Every image or element node in document order, at any depth. */
|
|
5
|
+
export function richtextBlocks(doc) {
|
|
6
|
+
const out = [];
|
|
7
|
+
const walk = (n) => {
|
|
8
|
+
if (!isNode(n))
|
|
9
|
+
return;
|
|
10
|
+
if (n.type === IMAGE_NODE || n.type === ELEMENT_NODE) {
|
|
11
|
+
if (typeof n.attrs !== "object" || n.attrs === null)
|
|
12
|
+
n.attrs = {};
|
|
13
|
+
out.push(n);
|
|
14
|
+
}
|
|
15
|
+
if (Array.isArray(n.content))
|
|
16
|
+
for (const c of n.content)
|
|
17
|
+
walk(c);
|
|
18
|
+
};
|
|
19
|
+
walk(doc);
|
|
20
|
+
return out;
|
|
21
|
+
}
|
|
22
|
+
export function elementSchemas(list) {
|
|
23
|
+
const out = {};
|
|
24
|
+
for (const e of list ?? []) {
|
|
25
|
+
const s = "schema" in e ? e.schema : e;
|
|
26
|
+
if (!(s.name in out))
|
|
27
|
+
out[s.name] = { fields: s.fields, styles: s.styles ?? {} };
|
|
28
|
+
}
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/** The element schema an element node names, or null for an unknown one. */
|
|
32
|
+
export function elementOf(node, elements) {
|
|
33
|
+
const name = node.attrs.name;
|
|
34
|
+
return typeof name === "string" ? elements[name] ?? null : null;
|
|
35
|
+
}
|
|
36
|
+
const asRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
|
|
37
|
+
/** An element node's field and style value sets, for a walk. */
|
|
38
|
+
export function elementSets(node, schema) {
|
|
39
|
+
const a = node.attrs;
|
|
40
|
+
const fields = asRecord(a.fields);
|
|
41
|
+
const styles = asRecord(a.styles);
|
|
42
|
+
return [
|
|
43
|
+
...(fields ? [{ values: fields, descriptors: schema.fields }] : []),
|
|
44
|
+
...(styles ? [{ values: styles, descriptors: schema.styles }] : []),
|
|
45
|
+
];
|
|
46
|
+
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Descriptor, type FieldBuilder, type ValueOf } from "./fields.ts";
|
|
2
2
|
import { pageSchema } from "./page.ts";
|
|
3
3
|
export type BuilderMap = Record<string, FieldBuilder<any, any>>;
|
|
4
4
|
export type SectionSchema<F extends BuilderMap = BuilderMap, S extends BuilderMap = BuilderMap> = {
|
package/dist/schema.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { describePath, findMatch } from "./fields.js";
|
|
1
2
|
import { pageSchema } from "./page.js";
|
|
2
3
|
function toDescriptors(map) {
|
|
3
4
|
return Object.fromEntries(Object.entries(map ?? {}).map(([k, b]) => [k, b.descriptor]));
|
|
@@ -35,6 +36,17 @@ export const schema = {
|
|
|
35
36
|
return { kind: "section", ...baseSchema(input) };
|
|
36
37
|
},
|
|
37
38
|
element(input) {
|
|
38
|
-
|
|
39
|
+
const base = baseSchema(input);
|
|
40
|
+
// An element inside a document is unreachable to the refs index, which
|
|
41
|
+
// addresses refs by top-level key (spec 2026-09-08 block nodes §1).
|
|
42
|
+
if (input.contexts.includes("richtext")) {
|
|
43
|
+
for (const [key, d] of Object.entries(base.fields)) {
|
|
44
|
+
const hit = findMatch(d, (sub) => sub.type === "ref" || sub.type === "refList", key);
|
|
45
|
+
if (hit !== null) {
|
|
46
|
+
throw new Error(`smoodly: element "${base.name}" cannot hold a ref (at ${describePath(hit)}) — refs cannot live inside richtext.`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return { kind: "element", contexts: input.contexts, ...base };
|
|
39
51
|
},
|
|
40
52
|
};
|
package/dist/section-wrapper.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createElement } from "react";
|
|
2
|
+
import { NODE_ATTR } from "./marks.js";
|
|
2
3
|
// The root wrapper every section renders through. Owns the mechanical DOM
|
|
3
4
|
// work on the real root element (no extra div): element choice via `as`,
|
|
4
5
|
// string-valued styles emitted as kebab-cased data attributes. Built-in
|
|
@@ -10,7 +11,7 @@ export function Section({ as = "section", styles, className, style, children, })
|
|
|
10
11
|
const { __smoodlyNode, ...rest } = (styles ?? {});
|
|
11
12
|
const dataAttrs = {};
|
|
12
13
|
if (typeof __smoodlyNode === "string")
|
|
13
|
-
dataAttrs[
|
|
14
|
+
dataAttrs[NODE_ATTR] = __smoodlyNode;
|
|
14
15
|
for (const [key, value] of Object.entries(rest)) {
|
|
15
16
|
if (typeof value === "string")
|
|
16
17
|
dataAttrs[`data-${kebab(key)}`] = value;
|
package/dist/shared.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { BuilderMap, NamespaceValues, SectionSchema } from "./schema.ts";
|
|
|
6
6
|
export declare const SHARED_NODE_TYPE = "shared";
|
|
7
7
|
/** The reserved key a visual item's styles live under in the node's shared JSONB. */
|
|
8
8
|
export declare const STYLES_KEY = "$styles";
|
|
9
|
-
export type SharedSchema<V = Record<string, unknown
|
|
9
|
+
export type SharedSchema<V = Record<string, unknown>, N extends string = string> = CollectionSchema<any, N> & {
|
|
10
10
|
kind: "shared";
|
|
11
11
|
/** The registered section a visual item renders through; absent = an object item. */
|
|
12
12
|
section?: string;
|
|
@@ -28,19 +28,19 @@ export declare function sharedLink(node: {
|
|
|
28
28
|
type: string;
|
|
29
29
|
fields?: Record<string, unknown>;
|
|
30
30
|
}): SharedLink | null;
|
|
31
|
-
type SharedInput = {
|
|
32
|
-
name:
|
|
31
|
+
type SharedInput<N extends string = string> = {
|
|
32
|
+
name: N;
|
|
33
33
|
title: string;
|
|
34
34
|
versions?: boolean;
|
|
35
35
|
};
|
|
36
|
-
export declare function shared<F extends BuilderMap>(input: SharedInput & {
|
|
36
|
+
export declare function shared<F extends BuilderMap, const N extends string = string>(input: SharedInput<N> & {
|
|
37
37
|
fields: F;
|
|
38
38
|
section?: undefined;
|
|
39
|
-
}): SharedSchema<NamespaceValues<F
|
|
40
|
-
export declare function shared<F extends BuilderMap, St extends BuilderMap>(input: SharedInput & {
|
|
39
|
+
}): SharedSchema<NamespaceValues<F>, N>;
|
|
40
|
+
export declare function shared<F extends BuilderMap, St extends BuilderMap, const N extends string = string>(input: SharedInput<N> & {
|
|
41
41
|
section: PairedComponent<SectionSchema<F, St>, F, St>;
|
|
42
42
|
fields?: undefined;
|
|
43
43
|
}): SharedSchema<NamespaceValues<F> & {
|
|
44
44
|
styles: NamespaceValues<St>;
|
|
45
|
-
}>;
|
|
45
|
+
}, N>;
|
|
46
46
|
export {};
|
package/dist/site.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { AssetStore } from "./asset-store.ts";
|
|
2
2
|
import type { CollectionSchema, EntryOrder } from "./collections.ts";
|
|
3
|
-
import
|
|
3
|
+
import type { Descriptor } from "./fields.ts";
|
|
4
|
+
import { type EntryRecord, type EntryStore, type Where } from "./entry-store.ts";
|
|
4
5
|
import { type LocaleSet, type PathTarget } from "./paths.ts";
|
|
5
6
|
import type { SectionLike } from "./refs.ts";
|
|
6
7
|
import type { PageContext, PageLink, PageTree } from "./render.tsx";
|
|
@@ -14,9 +15,21 @@ export type SiteOptions = {
|
|
|
14
15
|
};
|
|
15
16
|
collections: CollectionSchema<any>[];
|
|
16
17
|
sections: SectionLike[];
|
|
18
|
+
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
19
|
+
elements?: SectionLike[];
|
|
17
20
|
shared?: SharedSchema[];
|
|
18
21
|
locales: LocaleSet;
|
|
19
22
|
homePageSlug?: string;
|
|
23
|
+
/** Page shapes (entry pages are ignored): the source of each template's page-field descriptors (spec 2026-09-10 page fields §3). */
|
|
24
|
+
pages?: ReadonlyArray<SitePageSchema>;
|
|
25
|
+
};
|
|
26
|
+
/** A page registration as the site needs it: the template's page fields. */
|
|
27
|
+
export type SitePageSchema = {
|
|
28
|
+
schema: {
|
|
29
|
+
kind: string;
|
|
30
|
+
name: string;
|
|
31
|
+
fields?: Record<string, Descriptor>;
|
|
32
|
+
};
|
|
20
33
|
};
|
|
21
34
|
export type SitePage = {
|
|
22
35
|
kind: "page";
|
|
@@ -25,6 +38,8 @@ export type SitePage = {
|
|
|
25
38
|
tree: PageTree;
|
|
26
39
|
title: string;
|
|
27
40
|
path: string;
|
|
41
|
+
/** The page's own fields (spec 2026-09-10 page fields §3): resolved, defaults applied. */
|
|
42
|
+
fields: Record<string, unknown>;
|
|
28
43
|
ancestors: PageLink[];
|
|
29
44
|
children: PageLink[];
|
|
30
45
|
/** locale → path, for metadata alternates: the locales the page is
|
|
@@ -48,6 +63,7 @@ export type PageTreeItem = {
|
|
|
48
63
|
title: string;
|
|
49
64
|
path: string;
|
|
50
65
|
hasPage: boolean;
|
|
66
|
+
fields: Record<string, unknown>;
|
|
51
67
|
children: PageTreeItem[];
|
|
52
68
|
};
|
|
53
69
|
export type SiteEntryItem = {
|
|
@@ -57,10 +73,14 @@ export type SiteEntryItem = {
|
|
|
57
73
|
export type EntryTreeItem = SiteEntryItem & {
|
|
58
74
|
children: EntryTreeItem[];
|
|
59
75
|
};
|
|
76
|
+
/** `where`, `limit`, `parent` and `order` run in the store (spec 2026-09-11 §3); `draft` reads live rows inside the editor preview. */
|
|
60
77
|
export type EntryQuery = {
|
|
61
78
|
locale?: string;
|
|
62
79
|
parent?: string | null;
|
|
63
80
|
order?: EntryOrder;
|
|
81
|
+
where?: Where;
|
|
82
|
+
limit?: number;
|
|
83
|
+
draft?: boolean;
|
|
64
84
|
};
|
|
65
85
|
export type LoadOptions = {
|
|
66
86
|
draft?: boolean;
|
|
@@ -75,16 +95,20 @@ export interface SmoodlySite {
|
|
|
75
95
|
resolve(locale: string, path: string, options?: LoadOptions): Promise<SiteHit | null>;
|
|
76
96
|
getPagesTree(options?: {
|
|
77
97
|
locale?: string;
|
|
98
|
+
draft?: boolean;
|
|
78
99
|
}): Promise<PageTreeItem[]>;
|
|
79
100
|
getPage(pathOrId: string, options?: {
|
|
80
101
|
locale?: string;
|
|
102
|
+
draft?: boolean;
|
|
81
103
|
}): Promise<SitePage | null>;
|
|
82
104
|
getEntries(collection: string, options?: EntryQuery): Promise<SiteEntryItem[]>;
|
|
83
105
|
getEntry(collection: string, idOrPath: string, options?: {
|
|
84
106
|
locale?: string;
|
|
107
|
+
draft?: boolean;
|
|
85
108
|
}): Promise<SiteEntryItem | null>;
|
|
86
109
|
getEntriesTree(collection: string, options?: {
|
|
87
110
|
locale?: string;
|
|
111
|
+
draft?: boolean;
|
|
88
112
|
}): Promise<EntryTreeItem[]>;
|
|
89
113
|
/** A shared item's content in a locale (spec 2026-09-07 §4): an object
|
|
90
114
|
* item's merged fields, or a visual item's section props (fields plus
|
package/dist/site.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
-
import { entryFieldsIn, schemaOf } from "./entry-store.js";
|
|
1
|
+
import { ARRAY_FIELD_TYPES, entryFieldsIn, localizedKeys, schemaOf } from "./entry-store.js";
|
|
2
2
|
import { buildPageTree, chainOf, entryPathRows, entrySegment, pagePath, } from "./paths.js";
|
|
3
3
|
import { resolveFields, resolveMedia, resolveMediaSets, resolveTree } from "./resolve.js";
|
|
4
|
+
import { elementSchemas } from "./richtext-walk.js";
|
|
4
5
|
import { STYLES_KEY } from "./shared.js";
|
|
6
|
+
import { withDefaults } from "./field-defaults.js";
|
|
5
7
|
/** The `page` prop for a loaded page. `toUrl` is the app's `href` seam
|
|
6
8
|
* bound to the render locale; the default keeps CMS paths as URLs. */
|
|
7
9
|
export function pageContextOf(hit, toUrl = (p) => p) {
|
|
@@ -11,6 +13,7 @@ export function pageContextOf(hit, toUrl = (p) => p) {
|
|
|
11
13
|
title: hit.title,
|
|
12
14
|
path: hit.path,
|
|
13
15
|
url: toUrl(hit.path),
|
|
16
|
+
fields: hit.fields,
|
|
14
17
|
ancestors: hit.ancestors.map(withUrl),
|
|
15
18
|
children: hit.children.map(withUrl),
|
|
16
19
|
};
|
|
@@ -18,6 +21,7 @@ export function pageContextOf(hit, toUrl = (p) => p) {
|
|
|
18
21
|
export function createSmoodlySite(options) {
|
|
19
22
|
const locales = options.locales;
|
|
20
23
|
const homePageSlug = options.homePageSlug ?? "home";
|
|
24
|
+
const elements = elementSchemas(options.elements);
|
|
21
25
|
const pathOpts = { locales, homePageSlug };
|
|
22
26
|
const mounted = options.collections.filter((c) => c.path !== undefined);
|
|
23
27
|
/** The resolver's fetcher, bound to a locale: the merged fields of that
|
|
@@ -44,11 +48,43 @@ export function createSmoodlySite(options) {
|
|
|
44
48
|
const row = node.locales[locale];
|
|
45
49
|
return node.template !== null && row !== undefined && (draft || row.status === "published");
|
|
46
50
|
};
|
|
51
|
+
const pageFields = (template) => {
|
|
52
|
+
if (template === null)
|
|
53
|
+
return {};
|
|
54
|
+
const p = (options.pages ?? []).find((x) => x.schema.kind === "page" && x.schema.name === template);
|
|
55
|
+
return p?.schema.fields ?? {};
|
|
56
|
+
};
|
|
57
|
+
/** The version whose fields a link shows: the draft's in draft mode, else the published one. */
|
|
58
|
+
const versionFor = (node, locale, draft) => {
|
|
59
|
+
const row = node.locales[locale];
|
|
60
|
+
if (!row || node.template === null)
|
|
61
|
+
return null;
|
|
62
|
+
return draft ? row.draftVersionId : row.publishedVersionId;
|
|
63
|
+
};
|
|
64
|
+
/** Raw page fields (no asset resolution) of many nodes in ONE store read
|
|
65
|
+
* (spec 2026-09-10 page fields §3), defaults applied; `{}` for a folder. */
|
|
66
|
+
async function linkFields(nodes, locale, draft) {
|
|
67
|
+
const { pages } = options.stores();
|
|
68
|
+
const wanted = new Map(); // node id → version id
|
|
69
|
+
for (const n of nodes) {
|
|
70
|
+
const v = versionFor(n, locale, draft);
|
|
71
|
+
if (v)
|
|
72
|
+
wanted.set(n.id, v);
|
|
73
|
+
}
|
|
74
|
+
const byVersion = wanted.size > 0 ? await pages.fieldsOf([...new Set(wanted.values())]) : {};
|
|
75
|
+
const out = new Map();
|
|
76
|
+
for (const n of nodes) {
|
|
77
|
+
const v = wanted.get(n.id);
|
|
78
|
+
out.set(n.id, n.template === null ? {} : withDefaults(v ? byVersion[v] : undefined, pageFields(n.template)));
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
47
82
|
/** Only ever called for a node PRESENT in the locale (visible() or an ancestor of one). */
|
|
48
|
-
const link = (node, byId, locale, draft) => ({
|
|
83
|
+
const link = (node, byId, locale, draft, fields) => ({
|
|
49
84
|
title: node.locales[locale].title,
|
|
50
85
|
path: pagePath(node, byId, locale, pathOpts),
|
|
51
86
|
hasPage: hasPage(node, locale, draft),
|
|
87
|
+
fields: fields.get(node.id) ?? {},
|
|
52
88
|
});
|
|
53
89
|
async function loadPage(id, locale, path, draft) {
|
|
54
90
|
const { pages } = options.stores();
|
|
@@ -65,17 +101,19 @@ export function createSmoodlySite(options) {
|
|
|
65
101
|
sections: options.sections,
|
|
66
102
|
collections: options.collections,
|
|
67
103
|
shared: options.shared,
|
|
104
|
+
elements: options.elements,
|
|
68
105
|
fetch: fetcher(draft, locale),
|
|
69
106
|
assets: assetFetcher(),
|
|
107
|
+
page: { fields: pageFields(record.template) },
|
|
70
108
|
});
|
|
71
109
|
const all = await pages.listPages();
|
|
72
110
|
const index = new Map(all.map((n) => [n.id, n]));
|
|
73
111
|
const byId = (x) => index.get(x);
|
|
74
112
|
const chain = chainOf(record, byId);
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
|
|
78
|
-
|
|
113
|
+
const kids = all.filter((n) => n.parentId === id && visible(n, locale, draft));
|
|
114
|
+
const fields = await linkFields([...chain.slice(0, -1), ...kids], locale, draft);
|
|
115
|
+
const ancestors = chain.slice(0, -1).map((n) => link(n, byId, locale, draft, fields));
|
|
116
|
+
const children = kids.map((n) => link(n, byId, locale, draft, fields));
|
|
79
117
|
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
|
|
80
118
|
const paths = Object.fromEntries(Object.entries(await pages.pathsOf(id)).filter(([l]) => draft || record.locales[l]?.status === "published"));
|
|
81
119
|
return {
|
|
@@ -85,6 +123,7 @@ export function createSmoodlySite(options) {
|
|
|
85
123
|
tree,
|
|
86
124
|
title: row.title,
|
|
87
125
|
path,
|
|
126
|
+
fields: withDefaults(tree.fields, pageFields(record.template)),
|
|
88
127
|
ancestors,
|
|
89
128
|
children,
|
|
90
129
|
paths,
|
|
@@ -107,7 +146,7 @@ export function createSmoodlySite(options) {
|
|
|
107
146
|
: (await entries.getMany(schema.name, [id], locale, { status: "published" }))[id];
|
|
108
147
|
if (!raw)
|
|
109
148
|
return null;
|
|
110
|
-
const fields = await resolveFields(raw, schema.fields, { collections: options.collections, fetch: fetcher(draft, locale), assets: assetFetcher() });
|
|
149
|
+
const fields = await resolveFields(raw, schema.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), assets: assetFetcher() });
|
|
111
150
|
// Alternates advertise PUBLISHED locales only; the editor (draft) sees every present one.
|
|
112
151
|
const paths = Object.fromEntries(Object.entries(await entries.pathsOf(schema.name, id)).filter(([l]) => draft || record.locales[l]?.status === "published"));
|
|
113
152
|
return { kind: "entry", collection: schema.name, entry: { ...record, fields }, path, paths };
|
|
@@ -128,28 +167,55 @@ export function createSmoodlySite(options) {
|
|
|
128
167
|
path: entryPathRows([record], byId, schema, locales).find((r) => r.locale === locale)?.path ?? null,
|
|
129
168
|
}));
|
|
130
169
|
}
|
|
131
|
-
/** Public in a locale: its row exists there and is published. */
|
|
132
|
-
const publicEntry = (e, locale) => e.locales[locale]?.status === "published";
|
|
133
170
|
async function getEntries(collection, o = {}) {
|
|
134
171
|
const schema = schemaOf(options.collections, collection);
|
|
135
172
|
const locale = localeOf(o);
|
|
173
|
+
const draft = o.draft ?? false;
|
|
136
174
|
const { entries } = options.stores();
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
175
|
+
// A filter that silently matched nothing would be worse than an error (spec 2026-09-11 §3).
|
|
176
|
+
for (const key of Object.keys(o.where ?? {})) {
|
|
177
|
+
if (key === "slug" || localizedKeys(schema).includes(key)) {
|
|
178
|
+
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is localized — where filters shared fields only (spec 2026-09-11 §3).`);
|
|
179
|
+
}
|
|
180
|
+
if (!(key in schema.fields))
|
|
181
|
+
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is not a field of the collection.`);
|
|
182
|
+
// Any-of reads the text form of the whole field (PostgREST's `->>`), which for
|
|
183
|
+
// an array is its JSON — `["a"]` never matches "a" — so refuse rather than match nothing.
|
|
184
|
+
if (Array.isArray(o.where[key]) && ARRAY_FIELD_TYPES.has(schema.fields[key].type ?? "")) {
|
|
185
|
+
throw new Error(`smoodly: getEntries("${collection}"): "${key}" is array-valued — any-of is for scalar fields; pass one value to match entries containing it.`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (o.limit !== undefined && (!Number.isInteger(o.limit) || o.limit < 0)) {
|
|
189
|
+
throw new Error(`smoodly: getEntries("${collection}"): limit must be a non-negative integer.`);
|
|
190
|
+
}
|
|
191
|
+
// The store filters, orders and limits (spec 2026-09-11 §3): the locale
|
|
192
|
+
// gate — presence in draft mode, published otherwise — is part of the
|
|
193
|
+
// query, so `limit` counts rows that are really there.
|
|
194
|
+
const inLocale = await entries.list(collection, {
|
|
195
|
+
order: o.order, where: o.where, limit: o.limit,
|
|
196
|
+
...(o.parent !== undefined ? { parent: o.parent } : {}),
|
|
197
|
+
...(draft ? { locale } : { status: "published", locale }),
|
|
198
|
+
});
|
|
199
|
+
// Paths need every ancestor: a tree collection reads the whole collection for that, a flat one does not.
|
|
200
|
+
const all = schema.tree ? await entries.list(collection) : inLocale;
|
|
201
|
+
const fieldsById = draft
|
|
202
|
+
? Object.fromEntries(inLocale.map((e) => [e.id, entryFieldsIn(e, locale)]))
|
|
203
|
+
: await entries.getMany(collection, inLocale.map((e) => e.id), locale, { status: "published" });
|
|
140
204
|
// Refs stay ids on the content API; media resolves (one asset round for
|
|
141
205
|
// the whole list), because a list page's cards need their image URLs.
|
|
142
206
|
const fetchAssets = assetFetcher();
|
|
143
207
|
if (fetchAssets)
|
|
144
|
-
await resolveMediaSets(Object.values(
|
|
145
|
-
return entryItems(schema,
|
|
208
|
+
await resolveMediaSets(Object.values(fieldsById).map((values) => ({ values, descriptors: schema.fields })), fetchAssets, elements);
|
|
209
|
+
return entryItems(schema, inLocale, all, locale, fieldsById);
|
|
146
210
|
}
|
|
147
211
|
const api = {
|
|
148
212
|
async getPagesTree(o) {
|
|
149
213
|
const locale = localeOf(o);
|
|
214
|
+
const draft = o?.draft ?? false;
|
|
150
215
|
const all = await options.stores().pages.listPages();
|
|
151
|
-
const shown = all.filter((n) => visible(n, locale,
|
|
216
|
+
const shown = all.filter((n) => visible(n, locale, draft));
|
|
152
217
|
const index = new Map(shown.map((n) => [n.id, n]));
|
|
218
|
+
const fields = await linkFields(shown, locale, draft);
|
|
153
219
|
const toItem = (node) => ({
|
|
154
220
|
id: node.record.id,
|
|
155
221
|
title: node.record.locales[locale].title,
|
|
@@ -157,7 +223,8 @@ export function createSmoodlySite(options) {
|
|
|
157
223
|
// buildPageTree's nullable path is never null here; pagePath is
|
|
158
224
|
// the same derivation and keeps the type honest.
|
|
159
225
|
path: node.path ?? pagePath(node.record, (x) => index.get(x), locale, pathOpts),
|
|
160
|
-
hasPage: hasPage(node.record, locale,
|
|
226
|
+
hasPage: hasPage(node.record, locale, draft),
|
|
227
|
+
fields: fields.get(node.record.id) ?? {},
|
|
161
228
|
children: node.children.map(toItem),
|
|
162
229
|
});
|
|
163
230
|
return buildPageTree(shown, locale, pathOpts).map(toItem);
|
|
@@ -167,7 +234,7 @@ export function createSmoodlySite(options) {
|
|
|
167
234
|
const path = pathOrId.startsWith("/") ? pathOrId : (await options.stores().pages.pathsOf(pathOrId))[locale];
|
|
168
235
|
if (!path)
|
|
169
236
|
return null;
|
|
170
|
-
const hit = await site.resolve(locale, path);
|
|
237
|
+
const hit = await site.resolve(locale, path, { draft: o?.draft ?? false });
|
|
171
238
|
return hit?.kind === "page" ? hit : null;
|
|
172
239
|
},
|
|
173
240
|
getEntries,
|
|
@@ -175,10 +242,10 @@ export function createSmoodlySite(options) {
|
|
|
175
242
|
schemaOf(options.collections, collection); // an unknown collection is a wiring error on every branch
|
|
176
243
|
const locale = localeOf(o);
|
|
177
244
|
if (idOrPath.startsWith("/")) {
|
|
178
|
-
const hit = await site.resolve(locale, idOrPath);
|
|
245
|
+
const hit = await site.resolve(locale, idOrPath, { draft: o?.draft ?? false });
|
|
179
246
|
return hit?.kind === "entry" && hit.collection === collection ? { entry: hit.entry, path: hit.path } : null;
|
|
180
247
|
}
|
|
181
|
-
const items = await getEntries(collection, { locale });
|
|
248
|
+
const items = await getEntries(collection, { locale, draft: o?.draft ?? false });
|
|
182
249
|
const byId = items.find((i) => i.entry.id === idOrPath);
|
|
183
250
|
if (byId)
|
|
184
251
|
return byId;
|
|
@@ -188,7 +255,7 @@ export function createSmoodlySite(options) {
|
|
|
188
255
|
return items.find((i) => i.entry.parentId === null && entrySegment(i.entry, locale) === idOrPath) ?? null;
|
|
189
256
|
},
|
|
190
257
|
async getEntriesTree(collection, o) {
|
|
191
|
-
const items = await getEntries(collection, { locale: localeOf(o) });
|
|
258
|
+
const items = await getEntries(collection, { locale: localeOf(o), draft: o?.draft ?? false });
|
|
192
259
|
const build = (parentId) => items.filter((i) => i.entry.parentId === parentId).map((i) => ({ ...i, children: build(i.entry.id) }));
|
|
193
260
|
return build(null);
|
|
194
261
|
},
|
|
@@ -207,13 +274,13 @@ export function createSmoodlySite(options) {
|
|
|
207
274
|
: (await entries.getMany(item.name, [record.id], locale, { status: "published" }))[record.id];
|
|
208
275
|
if (!raw)
|
|
209
276
|
return null;
|
|
210
|
-
const fields = await resolveFields(raw, item.fields, { collections: options.collections, fetch: fetcher(draft, locale), assets: assetFetcher() });
|
|
277
|
+
const fields = await resolveFields(raw, item.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), assets: assetFetcher() });
|
|
211
278
|
if (!item.section)
|
|
212
279
|
return fields;
|
|
213
280
|
const { [STYLES_KEY]: styles, ...rest } = fields;
|
|
214
281
|
const fetch = assetFetcher();
|
|
215
282
|
if (fetch && styles && typeof styles === "object")
|
|
216
|
-
await resolveMedia(styles, sectionStyles(item.section), fetch);
|
|
283
|
+
await resolveMedia(styles, sectionStyles(item.section), fetch, elements);
|
|
217
284
|
return { ...rest, styles: styles !== null && typeof styles === "object" ? styles : {} };
|
|
218
285
|
},
|
|
219
286
|
};
|
package/dist/store.d.ts
CHANGED
|
@@ -78,6 +78,9 @@ export interface PageStore {
|
|
|
78
78
|
publish(id: string, locale: string): Promise<PageRecord>;
|
|
79
79
|
getDraftTree(id: string, locale: string): Promise<PageTree | null>;
|
|
80
80
|
getPublishedTree(id: string, locale: string): Promise<PageTree | null>;
|
|
81
|
+
/** The page fields of many versions in ONE read (spec 2026-09-10 page fields §3):
|
|
82
|
+
* versionId → `tree.fields ?? {}`; ids that do not exist are absent. */
|
|
83
|
+
fieldsOf(versionIds: string[]): Promise<Record<string, Record<string, unknown>>>;
|
|
81
84
|
listVersions(id: string, locale: string): Promise<PageVersion[]>;
|
|
82
85
|
deletePage(id: string): Promise<void>;
|
|
83
86
|
}
|
|
@@ -86,6 +89,8 @@ export interface PageStore {
|
|
|
86
89
|
* written; without `entries` publishing is not integrity-checked. */
|
|
87
90
|
export type PageStoreOptions = {
|
|
88
91
|
sections?: SectionLike[];
|
|
92
|
+
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
93
|
+
elements?: SectionLike[];
|
|
89
94
|
entries?: EntryStore;
|
|
90
95
|
/** The shared URL index (one instance for pages AND entries). */
|
|
91
96
|
paths?: PathIndex;
|
|
@@ -123,7 +128,7 @@ export declare function localeRow(page: PageRecord, locale: string): PageLocale;
|
|
|
123
128
|
/** A page's refs rows cover EVERY live tree — each locale's draft and
|
|
124
129
|
* published trees — so an entry stays delete-guarded while any of them
|
|
125
130
|
* shows it. Callers dedupe on write (the SQL PK does it for real). */
|
|
126
|
-
export declare function pageRefEdges(trees: PageTree[], sections: SectionLike[]): RefEdge[];
|
|
131
|
+
export declare function pageRefEdges(trees: PageTree[], sections: SectionLike[], elements?: SectionLike[]): RefEdge[];
|
|
127
132
|
/** The publish-integrity gate (DESIGN.md §3), per locale: a page must not
|
|
128
133
|
* go live in a locale showing entries the published site cannot resolve
|
|
129
134
|
* THERE — every referenced entry must exist, have a row in that locale,
|
|
@@ -177,6 +182,7 @@ export declare class MemoryPageStore implements PageStore {
|
|
|
177
182
|
saveDraft(id: string, locale: string, tree: PageTree): Promise<PageVersion>;
|
|
178
183
|
publish(id: string, locale: string): Promise<PageRecord>;
|
|
179
184
|
getDraftTree(id: string, locale: string): Promise<PageTree | null>;
|
|
185
|
+
fieldsOf(versionIds: string[]): Promise<Record<string, Record<string, unknown>>>;
|
|
180
186
|
getPublishedTree(id: string, locale: string): Promise<PageTree | null>;
|
|
181
187
|
listVersions(id: string, locale: string): Promise<PageVersion[]>;
|
|
182
188
|
deletePage(id: string): Promise<void>;
|
package/dist/store.js
CHANGED
|
@@ -50,10 +50,10 @@ export function localeRow(page, locale) {
|
|
|
50
50
|
/** A page's refs rows cover EVERY live tree — each locale's draft and
|
|
51
51
|
* published trees — so an entry stays delete-guarded while any of them
|
|
52
52
|
* shows it. Callers dedupe on write (the SQL PK does it for real). */
|
|
53
|
-
export function pageRefEdges(trees, sections) {
|
|
53
|
+
export function pageRefEdges(trees, sections, elements) {
|
|
54
54
|
const edges = [];
|
|
55
55
|
for (const tree of trees)
|
|
56
|
-
edges.push(...collectTreeRefs(tree, sections));
|
|
56
|
+
edges.push(...collectTreeRefs(tree, sections, elements));
|
|
57
57
|
return edges;
|
|
58
58
|
}
|
|
59
59
|
/** The publish-integrity gate (DESIGN.md §3), per locale: a page must not
|
|
@@ -159,10 +159,10 @@ export class MemoryPageStore {
|
|
|
159
159
|
return trees;
|
|
160
160
|
}
|
|
161
161
|
writeRefs(page) {
|
|
162
|
-
const { sections, refs } = this.options;
|
|
162
|
+
const { sections, elements, refs } = this.options;
|
|
163
163
|
if (!sections || !refs)
|
|
164
164
|
return;
|
|
165
|
-
refs.rewrite({ sourceKind: "page", sourceId: page.id }, pageRefEdges(this.liveTrees(page), sections));
|
|
165
|
+
refs.rewrite({ sourceKind: "page", sourceId: page.id }, pageRefEdges(this.liveTrees(page), sections, elements));
|
|
166
166
|
}
|
|
167
167
|
newVersion(pageId, locale, tree) {
|
|
168
168
|
const version = { id: uid(), pageId, locale, tree: structuredClone(tree), createdAt: Date.now() };
|
|
@@ -410,6 +410,15 @@ export class MemoryPageStore {
|
|
|
410
410
|
return null;
|
|
411
411
|
return structuredClone(this.versions.get(row.draftVersionId)?.tree ?? null);
|
|
412
412
|
}
|
|
413
|
+
async fieldsOf(versionIds) {
|
|
414
|
+
const out = {};
|
|
415
|
+
for (const id of versionIds) {
|
|
416
|
+
const v = this.versions.get(id);
|
|
417
|
+
if (v)
|
|
418
|
+
out[id] = structuredClone(v.tree.fields ?? {});
|
|
419
|
+
}
|
|
420
|
+
return out;
|
|
421
|
+
}
|
|
413
422
|
async getPublishedTree(id, locale) {
|
|
414
423
|
const row = this.pages.get(id)?.locales[locale];
|
|
415
424
|
if (!row?.publishedVersionId)
|