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
package/dist/marks.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** A section's root, from `<smoodly.Section>` in editor mode. */
|
|
2
|
+
export const NODE_ATTR = "data-smd-node";
|
|
3
|
+
/** An empty zone's placeholder box, from `<Zone>` in editor mode. */
|
|
4
|
+
export const ZONE_ATTR = "data-smd-zone";
|
|
5
|
+
export const EMPTY_ATTR = "data-smd-empty";
|
|
6
|
+
/** The element that renders a field's value verbatim. */
|
|
7
|
+
export const FIELD_ATTR = "data-smd-field";
|
|
8
|
+
/** A list item's or object field's root: scopes the field names inside it. */
|
|
9
|
+
export const ITEM_ATTR = "data-smd-item";
|
|
10
|
+
/** The root of an inline editing session inside the canvas; the bridge
|
|
11
|
+
* forwards nothing from inside it, and reports no marks under it. */
|
|
12
|
+
export const EDITOR_ATTR = "data-smd-editor";
|
|
13
|
+
/** The section node and dotted field path of a marked element: its own
|
|
14
|
+
* field name, prefixed by every item scope from the element itself up to
|
|
15
|
+
* the section root. Null when the element carries no field mark or sits
|
|
16
|
+
* outside a section node. */
|
|
17
|
+
export function resolveMarkPath(el) {
|
|
18
|
+
const field = el.getAttribute(FIELD_ATTR);
|
|
19
|
+
if (!field)
|
|
20
|
+
return null;
|
|
21
|
+
const segments = [field];
|
|
22
|
+
for (let cur = el; cur; cur = cur.parentElement) {
|
|
23
|
+
const node = cur.getAttribute(NODE_ATTR);
|
|
24
|
+
if (node)
|
|
25
|
+
return { node, path: segments.join(".") };
|
|
26
|
+
const item = cur.getAttribute(ITEM_ATTR);
|
|
27
|
+
if (item)
|
|
28
|
+
segments.unshift(item);
|
|
29
|
+
}
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
function level() {
|
|
33
|
+
return {
|
|
34
|
+
field: (key) => ({ [FIELD_ATTR]: key }),
|
|
35
|
+
item: (key, index) => ({ ...level(), attrs: { [ITEM_ATTR]: `${key}.${index}` } }),
|
|
36
|
+
object: (key) => ({ ...level(), attrs: { [ITEM_ATTR]: key } }),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
/** Typed marks for a section or element schema: `const m = smoodly.marks(heroSchema)`,
|
|
40
|
+
* then `<h1 {...m.field("heading")}>`. The raw attributes stay the contract. */
|
|
41
|
+
export function marks(_schema) {
|
|
42
|
+
return level();
|
|
43
|
+
}
|
package/dist/media.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Descriptor } from "./fields.ts";
|
|
2
|
+
import { type ElementSchemas } from "./richtext-walk.ts";
|
|
2
3
|
export type Crop = {
|
|
3
4
|
left: number;
|
|
4
5
|
top: number;
|
|
@@ -102,7 +103,7 @@ export declare const isMediaValue: (v: unknown) => v is {
|
|
|
102
103
|
};
|
|
103
104
|
/** Every asset id a value set references, walking lists and objects —
|
|
104
105
|
* unlike refs, media may sit inside a container (spec §8). */
|
|
105
|
-
export declare function collectAssetIds(values: Record<string, unknown> | undefined, descriptors: Record<string, Descriptor>, out: Set<string
|
|
106
|
+
export declare function collectAssetIds(values: Record<string, unknown> | undefined, descriptors: Record<string, Descriptor>, out: Set<string>, elements?: ElementSchemas): void;
|
|
106
107
|
/** Replace every media value in place with its resolved object — or delete
|
|
107
108
|
* it when the asset is missing or pending, so `image?.url` guards hold. */
|
|
108
|
-
export declare function applyAssets(values: Record<string, unknown> | undefined, descriptors: Record<string, Descriptor>, assets: Map<string, AssetRecord
|
|
109
|
+
export declare function applyAssets(values: Record<string, unknown> | undefined, descriptors: Record<string, Descriptor>, assets: Map<string, AssetRecord>, elements?: ElementSchemas): void;
|
package/dist/media.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ELEMENT_NODE, IMAGE_NODE, elementOf, elementSets, richtextBlocks } from "./richtext-walk.js";
|
|
1
2
|
/** The refs target collection of every asset edge — a reserved registered name. */
|
|
2
3
|
export const ASSET_COLLECTION = "asset";
|
|
3
4
|
export const MEDIA_TYPES = new Set(["image", "video", "file"]);
|
|
@@ -30,7 +31,7 @@ export const cropAspect = (crop, imageAspect) => (crop.width * imageAspect) / cr
|
|
|
30
31
|
export const isMediaValue = (v) => typeof v === "object" && v !== null && !Array.isArray(v) && typeof v.asset === "string";
|
|
31
32
|
/** Every asset id a value set references, walking lists and objects —
|
|
32
33
|
* unlike refs, media may sit inside a container (spec §8). */
|
|
33
|
-
export function collectAssetIds(values, descriptors, out) {
|
|
34
|
+
export function collectAssetIds(values, descriptors, out, elements = {}) {
|
|
34
35
|
if (!values)
|
|
35
36
|
return;
|
|
36
37
|
for (const [key, d] of Object.entries(descriptors)) {
|
|
@@ -41,8 +42,24 @@ export function collectAssetIds(values, descriptors, out) {
|
|
|
41
42
|
if (isMediaValue(value))
|
|
42
43
|
out.add(value.asset);
|
|
43
44
|
}
|
|
45
|
+
else if (d.type === "richtext") {
|
|
46
|
+
// Block nodes (spec 2026-09-08 block nodes §2): image nodes, and media inside a known element's fields and styles.
|
|
47
|
+
for (const node of richtextBlocks(value)) {
|
|
48
|
+
if (node.type === IMAGE_NODE) {
|
|
49
|
+
const image = node.attrs.image;
|
|
50
|
+
if (isMediaValue(image))
|
|
51
|
+
out.add(image.asset);
|
|
52
|
+
}
|
|
53
|
+
else if (node.type === ELEMENT_NODE) {
|
|
54
|
+
const schema = elementOf(node, elements);
|
|
55
|
+
if (schema)
|
|
56
|
+
for (const set of elementSets(node, schema))
|
|
57
|
+
collectAssetIds(set.values, set.descriptors, out, elements);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
44
61
|
else if (d.type === "object" && typeof value === "object" && !Array.isArray(value)) {
|
|
45
|
-
collectAssetIds(value, d.fields, out);
|
|
62
|
+
collectAssetIds(value, d.fields, out, elements);
|
|
46
63
|
}
|
|
47
64
|
else if (d.type === "list" && Array.isArray(value)) {
|
|
48
65
|
const item = d.item;
|
|
@@ -52,7 +69,10 @@ export function collectAssetIds(values, descriptors, out) {
|
|
|
52
69
|
out.add(v.asset);
|
|
53
70
|
}
|
|
54
71
|
else if (item.type === "object" && v && typeof v === "object") {
|
|
55
|
-
collectAssetIds(v, item.fields, out);
|
|
72
|
+
collectAssetIds(v, item.fields, out, elements);
|
|
73
|
+
}
|
|
74
|
+
else if (item.type === "richtext") {
|
|
75
|
+
collectAssetIds({ v }, { v: item }, out, elements);
|
|
56
76
|
}
|
|
57
77
|
}
|
|
58
78
|
}
|
|
@@ -97,7 +117,7 @@ function resolveOne(value, d, assets) {
|
|
|
97
117
|
}
|
|
98
118
|
/** Replace every media value in place with its resolved object — or delete
|
|
99
119
|
* it when the asset is missing or pending, so `image?.url` guards hold. */
|
|
100
|
-
export function applyAssets(values, descriptors, assets) {
|
|
120
|
+
export function applyAssets(values, descriptors, assets, elements = {}) {
|
|
101
121
|
if (!values)
|
|
102
122
|
return;
|
|
103
123
|
for (const [key, d] of Object.entries(descriptors)) {
|
|
@@ -111,8 +131,25 @@ export function applyAssets(values, descriptors, assets) {
|
|
|
111
131
|
else
|
|
112
132
|
values[key] = resolved;
|
|
113
133
|
}
|
|
134
|
+
else if (d.type === "richtext") {
|
|
135
|
+
for (const node of richtextBlocks(value)) {
|
|
136
|
+
if (node.type === IMAGE_NODE) {
|
|
137
|
+
const resolved = resolveOne(node.attrs.image, { type: "image" }, assets);
|
|
138
|
+
if (resolved === undefined)
|
|
139
|
+
delete node.attrs.image;
|
|
140
|
+
else
|
|
141
|
+
node.attrs.image = resolved;
|
|
142
|
+
}
|
|
143
|
+
else if (node.type === ELEMENT_NODE) {
|
|
144
|
+
const schema = elementOf(node, elements);
|
|
145
|
+
if (schema)
|
|
146
|
+
for (const set of elementSets(node, schema))
|
|
147
|
+
applyAssets(set.values, set.descriptors, assets, elements);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
114
151
|
else if (d.type === "object" && typeof value === "object" && !Array.isArray(value)) {
|
|
115
|
-
applyAssets(value, d.fields, assets);
|
|
152
|
+
applyAssets(value, d.fields, assets, elements);
|
|
116
153
|
}
|
|
117
154
|
else if (d.type === "list" && Array.isArray(value)) {
|
|
118
155
|
const item = d.item;
|
|
@@ -122,7 +159,12 @@ export function applyAssets(values, descriptors, assets) {
|
|
|
122
159
|
else if (item.type === "object") {
|
|
123
160
|
for (const v of value)
|
|
124
161
|
if (v && typeof v === "object")
|
|
125
|
-
applyAssets(v, item.fields, assets);
|
|
162
|
+
applyAssets(v, item.fields, assets, elements);
|
|
163
|
+
}
|
|
164
|
+
else if (item.type === "richtext") {
|
|
165
|
+
// Resolves in place inside the document object `v`; the wrapper is throwaway.
|
|
166
|
+
for (const v of value)
|
|
167
|
+
applyAssets({ v }, { v: item }, assets, elements);
|
|
126
168
|
}
|
|
127
169
|
}
|
|
128
170
|
}
|
package/dist/refs.d.ts
CHANGED
|
@@ -6,7 +6,7 @@ export type RefEdge = {
|
|
|
6
6
|
targetCollection: string;
|
|
7
7
|
targetId: string;
|
|
8
8
|
};
|
|
9
|
-
export declare function collectEntryRefs(fields: Record<string, unknown>, schema: CollectionSchema<any
|
|
9
|
+
export declare function collectEntryRefs(fields: Record<string, unknown>, schema: CollectionSchema<any>, elements?: SectionLike[]): RefEdge[];
|
|
10
10
|
/** A section schema, or anything carrying one (a paired component). */
|
|
11
11
|
export type SectionLike = {
|
|
12
12
|
name: string;
|
|
@@ -19,4 +19,4 @@ export type SectionLike = {
|
|
|
19
19
|
styles?: Record<string, Descriptor>;
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
|
-
export declare function collectTreeRefs(tree: PageTree, sections: SectionLike[]): RefEdge[];
|
|
22
|
+
export declare function collectTreeRefs(tree: PageTree, sections: SectionLike[], elements?: SectionLike[]): RefEdge[];
|
package/dist/refs.js
CHANGED
|
@@ -6,32 +6,52 @@
|
|
|
6
6
|
// this?) — forward resolution stays resolveTree's job.
|
|
7
7
|
import { sharedLink } from "./shared.js";
|
|
8
8
|
import { ASSET_COLLECTION, isMediaValue, MEDIA_TYPES } from "./media.js";
|
|
9
|
+
import { ELEMENT_NODE, IMAGE_NODE, elementOf, elementSchemas, elementSets, richtextBlocks } from "./richtext-walk.js";
|
|
9
10
|
function targetOf(d) {
|
|
10
11
|
const t = d.target?.();
|
|
11
12
|
if (!t?.name)
|
|
12
13
|
throw new Error("smoodly: ref descriptor has no resolvable target.");
|
|
13
14
|
return t.name;
|
|
14
15
|
}
|
|
15
|
-
/** Media edges walk into containers (spec 2026-09-08 media §8)
|
|
16
|
-
* stays the top-level key
|
|
17
|
-
|
|
16
|
+
/** Media edges walk into containers (spec 2026-09-08 media §8) and into
|
|
17
|
+
* richtext documents (block nodes §2); `field` stays the top-level key
|
|
18
|
+
* so the edge is addressable like a ref. */
|
|
19
|
+
function collectMedia(field, value, d, out, elements) {
|
|
18
20
|
if (value == null)
|
|
19
21
|
return;
|
|
20
22
|
if (MEDIA_TYPES.has(d.type)) {
|
|
21
23
|
if (isMediaValue(value))
|
|
22
24
|
out.push({ field, targetCollection: ASSET_COLLECTION, targetId: value.asset });
|
|
23
25
|
}
|
|
26
|
+
else if (d.type === "richtext") {
|
|
27
|
+
for (const node of richtextBlocks(value)) {
|
|
28
|
+
if (node.type === IMAGE_NODE) {
|
|
29
|
+
const image = node.attrs.image;
|
|
30
|
+
if (isMediaValue(image))
|
|
31
|
+
out.push({ field, targetCollection: ASSET_COLLECTION, targetId: image.asset });
|
|
32
|
+
}
|
|
33
|
+
else if (node.type === ELEMENT_NODE) {
|
|
34
|
+
const schema = elementOf(node, elements);
|
|
35
|
+
if (!schema)
|
|
36
|
+
continue;
|
|
37
|
+
for (const set of elementSets(node, schema)) {
|
|
38
|
+
for (const [k, sub] of Object.entries(set.descriptors))
|
|
39
|
+
collectMedia(field, set.values[k], sub, out, elements);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
24
44
|
else if (d.type === "object" && typeof value === "object" && !Array.isArray(value)) {
|
|
25
45
|
for (const [k, sub] of Object.entries(d.fields)) {
|
|
26
|
-
collectMedia(field, value[k], sub, out);
|
|
46
|
+
collectMedia(field, value[k], sub, out, elements);
|
|
27
47
|
}
|
|
28
48
|
}
|
|
29
49
|
else if (d.type === "list" && Array.isArray(value)) {
|
|
30
50
|
for (const v of value)
|
|
31
|
-
collectMedia(field, v, d.item, out);
|
|
51
|
+
collectMedia(field, v, d.item, out, elements);
|
|
32
52
|
}
|
|
33
53
|
}
|
|
34
|
-
function collectFrom(values, descriptors, out) {
|
|
54
|
+
function collectFrom(values, descriptors, out, elements) {
|
|
35
55
|
for (const [field, d] of Object.entries(descriptors)) {
|
|
36
56
|
const value = values[field];
|
|
37
57
|
if (value == null)
|
|
@@ -47,16 +67,17 @@ function collectFrom(values, descriptors, out) {
|
|
|
47
67
|
}
|
|
48
68
|
else {
|
|
49
69
|
// Refs are top-level only (f.list/f.object refuse them); media is not.
|
|
50
|
-
collectMedia(field, value, d, out);
|
|
70
|
+
collectMedia(field, value, d, out, elements);
|
|
51
71
|
}
|
|
52
72
|
}
|
|
53
73
|
}
|
|
54
|
-
export function collectEntryRefs(fields, schema) {
|
|
74
|
+
export function collectEntryRefs(fields, schema, elements) {
|
|
55
75
|
const out = [];
|
|
56
|
-
collectFrom(fields, schema.fields, out);
|
|
76
|
+
collectFrom(fields, schema.fields, out, elementSchemas(elements));
|
|
57
77
|
return out;
|
|
58
78
|
}
|
|
59
|
-
export function collectTreeRefs(tree, sections) {
|
|
79
|
+
export function collectTreeRefs(tree, sections, elements) {
|
|
80
|
+
const els = elementSchemas(elements);
|
|
60
81
|
const byName = new Map(sections.map((s) => {
|
|
61
82
|
const schema = "schema" in s ? s.schema : s;
|
|
62
83
|
return [schema.name, schema];
|
|
@@ -74,9 +95,9 @@ export function collectTreeRefs(tree, sections) {
|
|
|
74
95
|
const schema = byName.get(node.type);
|
|
75
96
|
if (!schema)
|
|
76
97
|
continue; // fail-soft, same as renderPage
|
|
77
|
-
collectFrom(node.fields, schema.fields, out);
|
|
98
|
+
collectFrom(node.fields, schema.fields, out, els);
|
|
78
99
|
if (schema.styles && node.styles)
|
|
79
|
-
collectFrom(node.styles, schema.styles, out);
|
|
100
|
+
collectFrom(node.styles, schema.styles, out, els);
|
|
80
101
|
}
|
|
81
102
|
}
|
|
82
103
|
return out;
|
package/dist/render.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { EMPTY_ATTR, ZONE_ATTR } from "./marks.js";
|
|
2
3
|
/** The `page` prop of a view rendered outside the site (a test, a preview
|
|
3
4
|
* of a shape): a real object, so a view never guards an optional prop, but
|
|
4
5
|
* no position in the tree. Frozen: one singleton shared by every such
|
|
@@ -13,7 +14,7 @@ export function Zone({ of: content }) {
|
|
|
13
14
|
if (!nodes.length) {
|
|
14
15
|
// In editor mode an empty zone still needs a measurable box for the
|
|
15
16
|
// overlay's "+ add" affordance; in production it renders nothing.
|
|
16
|
-
return content?.editor ? (_jsx("div", {
|
|
17
|
+
return content?.editor ? (_jsx("div", { [ZONE_ATTR]: content.name, [EMPTY_ATTR]: "", style: { minHeight: 48 } })) : null;
|
|
17
18
|
}
|
|
18
19
|
return (_jsx(_Fragment, { children: nodes.map((node) => {
|
|
19
20
|
const Component = content.components.get(node.type);
|
package/dist/resolve.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { CollectionSchema } from "./collections.ts";
|
|
|
3
3
|
import type { PageTree } from "./render.tsx";
|
|
4
4
|
import { type SharedSchema } from "./shared.ts";
|
|
5
5
|
import { type AssetRecord } from "./media.ts";
|
|
6
|
+
import { type ElementSchemas } from "./richtext-walk.ts";
|
|
7
|
+
import type { SectionLike } from "./refs.ts";
|
|
6
8
|
export type EntryFetcher = (collection: string, ids: string[]) => Promise<Record<string, Record<string, unknown>>>;
|
|
7
9
|
export type AssetFetcher = (ids: string[]) => Promise<Record<string, AssetRecord>>;
|
|
8
10
|
type SchemaLike = {
|
|
@@ -26,20 +28,24 @@ type ResolveOptions = {
|
|
|
26
28
|
assets?: AssetFetcher;
|
|
27
29
|
/** The registered shared items a placement node may name (spec 2026-09-07 §4). */
|
|
28
30
|
shared?: SharedSchema[];
|
|
31
|
+
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
32
|
+
elements?: SectionLike[];
|
|
29
33
|
/** Optional safety valve on chain length; unlimited by default. */
|
|
30
34
|
maxDepth?: number;
|
|
31
35
|
};
|
|
32
36
|
export declare function resolveTree(tree: PageTree, options: ResolveOptions): Promise<PageTree>;
|
|
33
37
|
/** One value set's media, resolved in place. Exported for the site layer's shared-item styles. */
|
|
34
|
-
export declare function resolveMedia(values: Record<string, unknown>, descriptors: Record<string, Descriptor>, fetch: AssetFetcher): Promise<void>;
|
|
38
|
+
export declare function resolveMedia(values: Record<string, unknown>, descriptors: Record<string, Descriptor>, fetch: AssetFetcher, elements?: ElementSchemas): Promise<void>;
|
|
35
39
|
/** Many value sets, one asset round (plus one for posters): the tree's
|
|
36
40
|
* nodes and every entry hydrated through a ref, or a list page's items.
|
|
37
41
|
* Exported for the site layer's content API. */
|
|
38
|
-
export declare function resolveMediaSets(sets: MediaSet[], fetch: AssetFetcher): Promise<void>;
|
|
42
|
+
export declare function resolveMediaSets(sets: MediaSet[], fetch: AssetFetcher, elements?: ElementSchemas): Promise<void>;
|
|
39
43
|
export type ResolveFieldsOptions = {
|
|
40
44
|
collections: CollectionSchema<any>[];
|
|
41
45
|
fetch: EntryFetcher;
|
|
42
46
|
assets?: AssetFetcher;
|
|
47
|
+
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
48
|
+
elements?: SectionLike[];
|
|
43
49
|
maxDepth?: number;
|
|
44
50
|
};
|
|
45
51
|
/** One entry's fields, refs hydrated — the entry page's counterpart of
|
package/dist/resolve.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// construction and the output stays JSON-serializable.
|
|
9
9
|
import { sharedLink, SHARED_NODE_TYPE, STYLES_KEY } from "./shared.js";
|
|
10
10
|
import { applyAssets, collectAssetIds } from "./media.js";
|
|
11
|
+
import { elementSchemas } from "./richtext-walk.js";
|
|
11
12
|
function targetOf(d) {
|
|
12
13
|
const t = d.target?.();
|
|
13
14
|
if (!t?.name)
|
|
@@ -49,7 +50,7 @@ export async function resolveTree(tree, options) {
|
|
|
49
50
|
sets.push({ values: node.fields, descriptors: schema.fields });
|
|
50
51
|
sets.push({ values: node.styles, descriptors: schema.styles ?? {} });
|
|
51
52
|
}
|
|
52
|
-
await resolveMediaSets([...sets, ...hydrated], options.assets);
|
|
53
|
+
await resolveMediaSets([...sets, ...hydrated], options.assets, elementSchemas(options.elements));
|
|
53
54
|
}
|
|
54
55
|
return resolved;
|
|
55
56
|
}
|
|
@@ -70,19 +71,19 @@ async function fetchAssets(fetch, ids) {
|
|
|
70
71
|
return out;
|
|
71
72
|
}
|
|
72
73
|
/** One value set's media, resolved in place. Exported for the site layer's shared-item styles. */
|
|
73
|
-
export async function resolveMedia(values, descriptors, fetch) {
|
|
74
|
-
await resolveMediaSets([{ values, descriptors }], fetch);
|
|
74
|
+
export async function resolveMedia(values, descriptors, fetch, elements = {}) {
|
|
75
|
+
await resolveMediaSets([{ values, descriptors }], fetch, elements);
|
|
75
76
|
}
|
|
76
77
|
/** Many value sets, one asset round (plus one for posters): the tree's
|
|
77
78
|
* nodes and every entry hydrated through a ref, or a list page's items.
|
|
78
79
|
* Exported for the site layer's content API. */
|
|
79
|
-
export async function resolveMediaSets(sets, fetch) {
|
|
80
|
+
export async function resolveMediaSets(sets, fetch, elements = {}) {
|
|
80
81
|
const ids = new Set();
|
|
81
82
|
for (const { values, descriptors } of sets)
|
|
82
|
-
collectAssetIds(values, descriptors, ids);
|
|
83
|
+
collectAssetIds(values, descriptors, ids, elements);
|
|
83
84
|
const assets = await fetchAssets(fetch, ids);
|
|
84
85
|
for (const { values, descriptors } of sets)
|
|
85
|
-
applyAssets(values, descriptors, assets);
|
|
86
|
+
applyAssets(values, descriptors, assets, elements);
|
|
86
87
|
}
|
|
87
88
|
/** Placements first (spec 2026-09-07 §4): fetch every linked item in one
|
|
88
89
|
* call per item, rewrite each node into the item's section node — the
|
|
@@ -128,7 +129,7 @@ export async function resolveFields(fields, descriptors, options) {
|
|
|
128
129
|
collectTasks(resolved, descriptors, tasks);
|
|
129
130
|
const hydrated = await runResolution(tasks, { ...options, maxDepth: options.maxDepth ?? Infinity });
|
|
130
131
|
if (options.assets)
|
|
131
|
-
await resolveMediaSets([{ values: resolved, descriptors }, ...hydrated], options.assets);
|
|
132
|
+
await resolveMediaSets([{ values: resolved, descriptors }, ...hydrated], options.assets, elementSchemas(options.elements));
|
|
132
133
|
return resolved;
|
|
133
134
|
}
|
|
134
135
|
/** Hydrates every task's refs in place and returns each hydrated entry
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ReactElement } from "react";
|
|
2
|
+
import { type RichTextElement } from "../richtext-render.tsx";
|
|
3
|
+
export type CreateRichTextOptions = {
|
|
4
|
+
/** The registered Elements a body may embed. */
|
|
5
|
+
elements?: RichTextElement[];
|
|
6
|
+
/** next/image `sizes` for body images; the site's text column width. */
|
|
7
|
+
sizes?: string;
|
|
8
|
+
};
|
|
9
|
+
export declare function createRichText({ elements, sizes }?: CreateRichTextOptions): (props: {
|
|
10
|
+
doc: unknown;
|
|
11
|
+
}) => ReactElement;
|
|
12
|
+
export type { RichTextElement, RichTextImageProps, RichTextProps } from "../richtext-render.tsx";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { RichText } from "../richtext-render.js";
|
|
3
|
+
import { SmoodlyImage } from "../image/SmoodlyImage.js";
|
|
4
|
+
export function createRichText({ elements = [], sizes } = {}) {
|
|
5
|
+
const image = ({ image, caption }) => (_jsxs("figure", { children: [_jsx(SmoodlyImage, { image: image, sizes: sizes }), caption !== "" && _jsx("figcaption", { children: caption })] }));
|
|
6
|
+
return function BoundRichText({ doc }) {
|
|
7
|
+
return _jsx(RichText, { doc: doc, elements: elements, image: image });
|
|
8
|
+
};
|
|
9
|
+
}
|
|
@@ -1,3 +1,23 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ReactElement, type ReactNode } from "react";
|
|
2
|
+
import type { ResolvedImage } from "./media.ts";
|
|
3
|
+
export type RichTextImageProps = {
|
|
4
|
+
image: ResolvedImage;
|
|
5
|
+
caption: string;
|
|
6
|
+
};
|
|
7
|
+
/** A paired element component: `smoodly.element(schema, View)`. */
|
|
8
|
+
export type RichTextElement = {
|
|
9
|
+
schema: {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
} & ((props: any) => ReactElement);
|
|
13
|
+
export type RichTextProps = {
|
|
2
14
|
doc: unknown;
|
|
3
|
-
|
|
15
|
+
/** The registered Elements a document may embed, rendered by name. */
|
|
16
|
+
elements?: RichTextElement[];
|
|
17
|
+
/** How an image node renders; the default is a figure over a plain <img>. */
|
|
18
|
+
image?: (props: RichTextImageProps) => ReactNode;
|
|
19
|
+
};
|
|
20
|
+
/** Lazy by default: body images sit below the fold, and a lazy <img> is
|
|
21
|
+
* the one React does not hoist a preload hint for on the server. */
|
|
22
|
+
export declare function defaultRichTextImage({ image, caption }: RichTextImageProps): ReactNode;
|
|
23
|
+
export declare function RichText({ doc, elements, image }: RichTextProps): import("react").JSX.Element | null;
|
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/site.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ export type SiteOptions = {
|
|
|
14
14
|
};
|
|
15
15
|
collections: CollectionSchema<any>[];
|
|
16
16
|
sections: SectionLike[];
|
|
17
|
+
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
18
|
+
elements?: SectionLike[];
|
|
17
19
|
shared?: SharedSchema[];
|
|
18
20
|
locales: LocaleSet;
|
|
19
21
|
homePageSlug?: string;
|