smoodly 0.0.15 → 0.0.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +238 -36
- package/dist/admin/auth.d.ts +7 -4
- package/dist/admin/auth.js +1 -1
- package/dist/admin/client-ops.js +3 -3
- package/dist/admin/custom-section.d.ts +10 -0
- package/dist/admin/custom-section.js +10 -0
- package/dist/admin/editor/CanvasOverlay.d.ts +6 -2
- package/dist/admin/editor/CanvasOverlay.js +6 -6
- package/dist/admin/editor/EditorView.d.ts +2 -0
- package/dist/admin/editor/EditorView.js +322 -62
- package/dist/admin/editor/GlobalInspector.d.ts +24 -0
- package/dist/admin/editor/GlobalInspector.js +13 -0
- package/dist/admin/editor/InsertPicker.d.ts +6 -5
- package/dist/admin/editor/InsertPicker.js +6 -4
- package/dist/admin/editor/InspectorFields.d.ts +21 -0
- package/dist/admin/editor/InspectorFields.js +22 -0
- package/dist/admin/editor/Outline.d.ts +0 -1
- package/dist/admin/editor/Outline.js +3 -3
- package/dist/admin/editor/PageSettings.js +3 -2
- package/dist/admin/editor/global-edit.d.ts +39 -0
- package/dist/admin/editor/global-edit.js +124 -0
- package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
- package/dist/admin/editor/useGlobalEdit.js +18 -0
- package/dist/admin/forms/FieldWidget.d.ts +14 -1
- package/dist/admin/forms/FieldWidget.js +38 -11
- package/dist/admin/forms/ListField.d.ts +9 -1
- package/dist/admin/forms/ListField.js +16 -8
- package/dist/admin/forms/RichtextElementCard.js +11 -5
- package/dist/admin/forms/RowsWidget.d.ts +13 -0
- package/dist/admin/forms/RowsWidget.js +26 -0
- package/dist/admin/forms/SpanBar.d.ts +5 -0
- package/dist/admin/forms/SpanBar.js +37 -0
- package/dist/admin/forms/richtext-insert.d.ts +2 -2
- package/dist/admin/forms/richtext-insert.js +3 -3
- package/dist/admin/forms/richtext-nodes.js +1 -1
- package/dist/admin/forms/tabs.d.ts +20 -8
- package/dist/admin/forms/tabs.js +19 -23
- package/dist/admin/forms/useRichtextEditor.js +8 -1
- package/dist/admin/global-items.d.ts +9 -0
- package/dist/admin/{shared-items.js → global-items.js} +29 -14
- package/dist/admin/index.d.ts +3 -3
- package/dist/admin/index.js +1 -1
- package/dist/admin/next/op-handler.js +4 -3
- package/dist/admin/ops-impl.d.ts +3 -0
- package/dist/admin/ops-impl.js +242 -56
- package/dist/admin/ops.d.ts +38 -6
- package/dist/admin/serialize.d.ts +18 -7
- package/dist/admin/serialize.js +27 -5
- package/dist/admin/shell/AdminApp.js +7 -7
- package/dist/admin/shell/EntriesList.js +19 -3
- package/dist/admin/shell/EntryForm.js +183 -77
- package/dist/admin/shell/GlobalFields.d.ts +24 -0
- package/dist/admin/shell/GlobalFields.js +19 -0
- package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
- package/dist/admin/shell/GlobalForm.js +258 -0
- package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
- package/dist/admin/shell/{SharedList.js → GlobalList.js} +12 -11
- package/dist/admin/shell/IconRail.js +14 -7
- package/dist/admin/shell/PagesList.js +4 -8
- package/dist/admin/shell/entries-list.d.ts +2 -2
- package/dist/admin/shell/entries-list.js +3 -3
- package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
- package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
- package/dist/admin/supabase-auth.js +1 -1
- package/dist/admin/tree-ops.d.ts +10 -8
- package/dist/admin/tree-ops.js +28 -16
- package/dist/admin/ui/ActionMenu.d.ts +11 -0
- package/dist/admin/ui/ActionMenu.js +25 -0
- package/dist/admin/ui/HistoryModal.d.ts +23 -0
- package/dist/admin/ui/HistoryModal.js +40 -0
- package/dist/admin/ui/Modal.d.ts +5 -1
- package/dist/admin/ui/Modal.js +3 -3
- package/dist/admin/ui/PublishControl.d.ts +31 -0
- package/dist/admin/ui/PublishControl.js +24 -0
- package/dist/admin/ui/format.d.ts +4 -0
- package/dist/admin/ui/format.js +9 -0
- package/dist/admin/ui/primitives.d.ts +16 -1
- package/dist/admin/ui/primitives.js +20 -4
- package/dist/admin/ui/status-label.d.ts +10 -0
- package/dist/admin/ui/status-label.js +14 -0
- package/dist/admin/ui/theme.js +93 -13
- package/dist/admin/validate.d.ts +27 -2
- package/dist/admin/validate.js +47 -10
- package/dist/collections.d.ts +3 -9
- package/dist/collections.js +1 -2
- package/dist/conditions.d.ts +33 -0
- package/dist/conditions.js +79 -0
- package/dist/config.d.ts +26 -19
- package/dist/config.js +26 -25
- package/dist/css.d.ts +7 -0
- package/dist/css.js +57 -0
- package/dist/custom/CustomSection.d.ts +26 -0
- package/dist/custom/CustomSection.js +20 -0
- package/dist/custom/css.d.ts +3 -0
- package/dist/custom/css.js +28 -0
- package/dist/custom/index.d.ts +26 -0
- package/dist/custom/index.js +16 -0
- package/dist/custom/rows.d.ts +28 -0
- package/dist/custom/rows.js +129 -0
- package/dist/custom/schema.d.ts +32 -0
- package/dist/custom/schema.js +39 -0
- package/dist/entry-store.d.ts +42 -17
- package/dist/entry-store.js +107 -40
- package/dist/fields.d.ts +5 -2
- package/dist/fields.js +4 -2
- package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
- package/dist/{shared-id.js → global-id.js} +4 -4
- package/dist/global.d.ts +57 -0
- package/dist/global.js +51 -0
- package/dist/index.d.ts +28 -18
- package/dist/index.js +10 -5
- package/dist/media.js +1 -1
- package/dist/next/index.d.ts +1 -1
- package/dist/next/page-renderer.d.ts +4 -2
- package/dist/next/page-renderer.js +17 -13
- package/dist/next/queries.d.ts +6 -6
- package/dist/next/queries.js +7 -7
- package/dist/page.js +2 -2
- package/dist/pairing.d.ts +13 -7
- package/dist/pairing.js +4 -4
- package/dist/refs.d.ts +2 -2
- package/dist/refs.js +4 -4
- package/dist/render.d.ts +13 -1
- package/dist/render.js +10 -5
- package/dist/resolve.d.ts +11 -6
- package/dist/resolve.js +22 -12
- package/dist/revalidate.d.ts +4 -4
- package/dist/revalidate.js +4 -4
- package/dist/richtext-render.js +2 -2
- package/dist/richtext-walk.d.ts +5 -5
- package/dist/richtext-walk.js +4 -4
- package/dist/schema.d.ts +15 -10
- package/dist/schema.js +19 -7
- package/dist/section-wrapper.d.ts +18 -6
- package/dist/section-wrapper.js +24 -17
- package/dist/site.d.ts +17 -5
- package/dist/site.js +36 -12
- package/dist/sql-space.js +1 -1
- package/dist/sql.js +19 -16
- package/dist/store.d.ts +45 -13
- package/dist/store.js +88 -12
- package/dist/styles.d.ts +27 -0
- package/dist/styles.js +60 -0
- package/dist/supabase-entry-store.d.ts +11 -6
- package/dist/supabase-entry-store.js +115 -39
- package/dist/supabase-store.d.ts +13 -6
- package/dist/supabase-store.js +90 -20
- package/dist/zones.d.ts +9 -7
- package/dist/zones.js +5 -5
- package/package.json +1 -1
- package/dist/admin/shared-items.d.ts +0 -9
- package/dist/admin/shell/SharedForm.js +0 -134
- package/dist/shared.d.ts +0 -46
- package/dist/shared.js +0 -36
package/dist/resolve.js
CHANGED
|
@@ -6,9 +6,10 @@
|
|
|
6
6
|
// Cycles are cut per path — a ref back to an entry already on its own
|
|
7
7
|
// chain gets a shallow copy (refs as ids), so traversal terminates by
|
|
8
8
|
// construction and the output stays JSON-serializable.
|
|
9
|
-
import {
|
|
9
|
+
import { globalLink, GLOBAL_NODE_TYPE, SETTINGS_KEY, STYLES_KEY, CSS_KEY } from "./global.js";
|
|
10
10
|
import { applyAssets, collectAssetIds } from "./media.js";
|
|
11
11
|
import { elementSchemas } from "./richtext-walk.js";
|
|
12
|
+
import { CUSTOM_NODE_TYPE, customSchema, customToolset } from "./custom/index.js";
|
|
12
13
|
/** The built-in SEO set's one media value (spec 2026-09-10 page fields §2). */
|
|
13
14
|
const META_DESCRIPTORS = { ogImage: { type: "image" } };
|
|
14
15
|
function targetOf(d) {
|
|
@@ -31,8 +32,10 @@ export async function resolveTree(tree, options) {
|
|
|
31
32
|
const schema = "schema" in s ? s.schema : s;
|
|
32
33
|
return [schema.name, schema];
|
|
33
34
|
}));
|
|
35
|
+
// The custom section (spec 2026-09-14 §6): its columns' images resolve like any section's richtext.
|
|
36
|
+
sectionSchemas.set(CUSTOM_NODE_TYPE, customSchema(customToolset(options.custom?.toolset)));
|
|
34
37
|
const resolved = structuredClone(tree);
|
|
35
|
-
await rewritePlacements(resolved, options.
|
|
38
|
+
await rewritePlacements(resolved, options.globals ?? [], options.fetch);
|
|
36
39
|
const tasks = [];
|
|
37
40
|
for (const nodes of Object.values(resolved.zones ?? {})) {
|
|
38
41
|
for (const node of nodes) {
|
|
@@ -52,7 +55,7 @@ export async function resolveTree(tree, options) {
|
|
|
52
55
|
if (!schema)
|
|
53
56
|
continue;
|
|
54
57
|
sets.push({ values: node.fields, descriptors: schema.fields });
|
|
55
|
-
sets.push({ values: node.
|
|
58
|
+
sets.push({ values: node.settings, descriptors: schema.settings ?? {} });
|
|
56
59
|
}
|
|
57
60
|
if (options.page && resolved.fields)
|
|
58
61
|
sets.push({ values: resolved.fields, descriptors: options.page.fields });
|
|
@@ -78,7 +81,7 @@ async function fetchAssets(fetch, ids) {
|
|
|
78
81
|
out.set(id, a);
|
|
79
82
|
return out;
|
|
80
83
|
}
|
|
81
|
-
/** One value set's media, resolved in place. Exported for the site layer's
|
|
84
|
+
/** One value set's media, resolved in place. Exported for the site layer's global-item settings. */
|
|
82
85
|
export async function resolveMedia(values, descriptors, fetch, elements = {}) {
|
|
83
86
|
await resolveMediaSets([{ values, descriptors }], fetch, elements);
|
|
84
87
|
}
|
|
@@ -99,12 +102,12 @@ export async function resolveMediaSets(sets, fetch, elements = {}) {
|
|
|
99
102
|
* site cannot render: an unpublished or dangling row, an object item
|
|
100
103
|
* (no view), an unknown item. The normal pass then hydrates the
|
|
101
104
|
* rewritten nodes' own refs. */
|
|
102
|
-
async function rewritePlacements(tree,
|
|
103
|
-
const byName = new Map(
|
|
105
|
+
async function rewritePlacements(tree, globals, fetch) {
|
|
106
|
+
const byName = new Map(globals.map((s) => [s.name, s]));
|
|
104
107
|
const wanted = new Map();
|
|
105
108
|
for (const nodes of Object.values(tree.zones ?? {})) {
|
|
106
109
|
for (const node of nodes) {
|
|
107
|
-
const link =
|
|
110
|
+
const link = globalLink(node);
|
|
108
111
|
if (!link || !byName.get(link.item)?.section)
|
|
109
112
|
continue;
|
|
110
113
|
const ids = wanted.get(link.item) ?? new Set();
|
|
@@ -116,16 +119,23 @@ async function rewritePlacements(tree, shared, fetch) {
|
|
|
116
119
|
await Promise.all([...wanted].map(async ([item, ids]) => fetched.set(item, await fetch(item, [...ids]))));
|
|
117
120
|
for (const [zone, nodes] of Object.entries(tree.zones ?? {})) {
|
|
118
121
|
tree.zones[zone] = nodes.flatMap((node) => {
|
|
119
|
-
const link =
|
|
122
|
+
const link = globalLink(node);
|
|
120
123
|
if (!link)
|
|
121
|
-
return node.type ===
|
|
124
|
+
return node.type === GLOBAL_NODE_TYPE ? [] : [node];
|
|
122
125
|
const schema = byName.get(link.item);
|
|
123
126
|
const row = fetched.get(link.item)?.[link.ref];
|
|
124
127
|
if (!schema?.section || !row)
|
|
125
128
|
return [];
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
+
// The row's reserved keys become the node's namespaces (spec 2026-09-14 §8): the
|
|
130
|
+
// rewritten node is then a section node like any other, styles inline and css scoped.
|
|
131
|
+
const { [SETTINGS_KEY]: settings, [STYLES_KEY]: styles, [CSS_KEY]: css, ...fields } = structuredClone(row);
|
|
132
|
+
const ns = (v) => (v !== null && typeof v === "object" ? v : undefined);
|
|
133
|
+
return [{
|
|
134
|
+
id: node.id, type: schema.section, fields,
|
|
135
|
+
...(ns(settings) ? { settings: ns(settings) } : {}),
|
|
136
|
+
...(ns(styles) ? { styles: ns(styles) } : {}),
|
|
137
|
+
...(typeof css === "string" && css !== "" ? { css } : {}),
|
|
138
|
+
}];
|
|
129
139
|
});
|
|
130
140
|
}
|
|
131
141
|
}
|
package/dist/revalidate.d.ts
CHANGED
|
@@ -8,10 +8,10 @@ export type AffectedTargets = {
|
|
|
8
8
|
};
|
|
9
9
|
export declare const pageTag: (id: string) => string;
|
|
10
10
|
export declare const entryTag: (id: string) => string;
|
|
11
|
-
/** A
|
|
12
|
-
* locale, and code owns the name): the Next glue's
|
|
13
|
-
* caches under it; ops.
|
|
14
|
-
export declare const
|
|
11
|
+
/** A global item's cache unit, by NAME (a singleton has one row per
|
|
12
|
+
* locale, and code owns the name): the Next glue's getSmoodlyGlobal
|
|
13
|
+
* caches under it; ops.globals expires it. */
|
|
14
|
+
export declare const globalTag: (name: string) => string;
|
|
15
15
|
/** The page-tree listing's cache unit (spec 2026-09-11 §2): one tree-wide
|
|
16
16
|
* tag, fired by every page op that changes what the listing shows. */
|
|
17
17
|
export declare const pagesTreeTag: () => string;
|
package/dist/revalidate.js
CHANGED
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
// change (spec 2026-09-05 §5).
|
|
8
8
|
export const pageTag = (id) => `page:${id}`;
|
|
9
9
|
export const entryTag = (id) => `entry:${id}`;
|
|
10
|
-
/** A
|
|
11
|
-
* locale, and code owns the name): the Next glue's
|
|
12
|
-
* caches under it; ops.
|
|
13
|
-
export const
|
|
10
|
+
/** A global item's cache unit, by NAME (a singleton has one row per
|
|
11
|
+
* locale, and code owns the name): the Next glue's getSmoodlyGlobal
|
|
12
|
+
* caches under it; ops.globals expires it. */
|
|
13
|
+
export const globalTag = (name) => `global:${name}`;
|
|
14
14
|
/** The page-tree listing's cache unit (spec 2026-09-11 §2): one tree-wide
|
|
15
15
|
* tag, fired by every page op that changes what the listing shows. */
|
|
16
16
|
export const pagesTreeTag = () => "pages:tree";
|
package/dist/richtext-render.js
CHANGED
|
@@ -67,8 +67,8 @@ function renderNode(node, r) {
|
|
|
67
67
|
if (!Component)
|
|
68
68
|
return null;
|
|
69
69
|
const fields = a.fields && typeof a.fields === "object" ? a.fields : {};
|
|
70
|
-
const
|
|
71
|
-
return _jsx(Component, { ...fields,
|
|
70
|
+
const settings = a.settings && typeof a.settings === "object" ? a.settings : {};
|
|
71
|
+
return _jsx(Component, { ...fields, settings: settings });
|
|
72
72
|
}
|
|
73
73
|
default: return children;
|
|
74
74
|
}
|
package/dist/richtext-walk.d.ts
CHANGED
|
@@ -8,7 +8,7 @@ export type ImageNodeAttrs = {
|
|
|
8
8
|
export type ElementNodeAttrs = {
|
|
9
9
|
name?: unknown;
|
|
10
10
|
fields?: unknown;
|
|
11
|
-
|
|
11
|
+
settings?: unknown;
|
|
12
12
|
};
|
|
13
13
|
/** A block node as stored — `attrs` is the live object, so a walk may resolve in place. */
|
|
14
14
|
export type BlockNode = {
|
|
@@ -20,23 +20,23 @@ export declare function richtextBlocks(doc: unknown): BlockNode[];
|
|
|
20
20
|
/** An element's descriptors by name — what the walks read an element node against. */
|
|
21
21
|
export type ElementSchemas = Record<string, {
|
|
22
22
|
fields: Record<string, Descriptor>;
|
|
23
|
-
|
|
23
|
+
settings: Record<string, Descriptor>;
|
|
24
24
|
}>;
|
|
25
25
|
type ElementLike = {
|
|
26
26
|
name: string;
|
|
27
27
|
fields: Record<string, Descriptor>;
|
|
28
|
-
|
|
28
|
+
settings?: Record<string, Descriptor>;
|
|
29
29
|
} | {
|
|
30
30
|
schema: {
|
|
31
31
|
name: string;
|
|
32
32
|
fields: Record<string, Descriptor>;
|
|
33
|
-
|
|
33
|
+
settings?: Record<string, Descriptor>;
|
|
34
34
|
};
|
|
35
35
|
};
|
|
36
36
|
export declare function elementSchemas(list: ElementLike[] | undefined): ElementSchemas;
|
|
37
37
|
/** The element schema an element node names, or null for an unknown one. */
|
|
38
38
|
export declare function elementOf(node: BlockNode, elements: ElementSchemas): ElementSchemas[string] | null;
|
|
39
|
-
/** An element node's field and
|
|
39
|
+
/** An element node's field and settings value sets, for a walk. */
|
|
40
40
|
export declare function elementSets(node: BlockNode, schema: ElementSchemas[string]): {
|
|
41
41
|
values: Record<string, unknown>;
|
|
42
42
|
descriptors: Record<string, Descriptor>;
|
package/dist/richtext-walk.js
CHANGED
|
@@ -24,7 +24,7 @@ export function elementSchemas(list) {
|
|
|
24
24
|
for (const e of list ?? []) {
|
|
25
25
|
const s = "schema" in e ? e.schema : e;
|
|
26
26
|
if (!(s.name in out))
|
|
27
|
-
out[s.name] = { fields: s.fields,
|
|
27
|
+
out[s.name] = { fields: s.fields, settings: s.settings ?? {} };
|
|
28
28
|
}
|
|
29
29
|
return out;
|
|
30
30
|
}
|
|
@@ -34,13 +34,13 @@ export function elementOf(node, elements) {
|
|
|
34
34
|
return typeof name === "string" ? elements[name] ?? null : null;
|
|
35
35
|
}
|
|
36
36
|
const asRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v) ? v : undefined;
|
|
37
|
-
/** An element node's field and
|
|
37
|
+
/** An element node's field and settings value sets, for a walk. */
|
|
38
38
|
export function elementSets(node, schema) {
|
|
39
39
|
const a = node.attrs;
|
|
40
40
|
const fields = asRecord(a.fields);
|
|
41
|
-
const
|
|
41
|
+
const settings = asRecord(a.settings);
|
|
42
42
|
return [
|
|
43
43
|
...(fields ? [{ values: fields, descriptors: schema.fields }] : []),
|
|
44
|
-
...(
|
|
44
|
+
...(settings ? [{ values: settings, descriptors: schema.settings }] : []),
|
|
45
45
|
];
|
|
46
46
|
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -8,12 +8,15 @@ export type SectionSchema<F extends BuilderMap = BuilderMap, S extends BuilderMa
|
|
|
8
8
|
icon?: string;
|
|
9
9
|
description?: string;
|
|
10
10
|
fields: Record<string, Descriptor>;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/** The developer's knobs (spec 2026-09-14 §3): the view alone interprets them. */
|
|
12
|
+
settings: Record<string, Descriptor>;
|
|
13
13
|
sample?: Record<string, unknown>;
|
|
14
|
-
|
|
14
|
+
/** Smoodly's styles set (spec 2026-09-14 §2): absent = the whole set; `false` = none; `omit` drops keys. */
|
|
15
|
+
styles?: false | {
|
|
15
16
|
omit: string[];
|
|
16
17
|
};
|
|
18
|
+
/** The custom-CSS box (spec 2026-09-14 §4): `false` opts this section out. */
|
|
19
|
+
css?: false;
|
|
17
20
|
/** phantom: builder types for Props inference */
|
|
18
21
|
readonly __f?: F;
|
|
19
22
|
readonly __s?: S;
|
|
@@ -34,23 +37,25 @@ export type NamespaceValues<M extends BuilderMap> = {
|
|
|
34
37
|
} & {
|
|
35
38
|
[K in OptionalKeys<M>]?: ValueOf<M[K]>;
|
|
36
39
|
};
|
|
37
|
-
type SectionInput<F extends BuilderMap, S extends BuilderMap
|
|
40
|
+
type SectionInput<F extends BuilderMap, S extends BuilderMap> = {
|
|
38
41
|
name: string;
|
|
39
42
|
title: string;
|
|
40
43
|
icon?: string;
|
|
41
44
|
description?: string;
|
|
42
45
|
fields?: F;
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
settings?: S;
|
|
47
|
+
/** Gone (spec 2026-09-14 §1): an id is a setting the view passes to the wrapper. */
|
|
48
|
+
meta?: never;
|
|
49
|
+
styles?: false | {
|
|
47
50
|
omit: string[];
|
|
48
51
|
};
|
|
52
|
+
css?: false;
|
|
53
|
+
sample?: Partial<NamespaceValues<F> & NamespaceValues<S>>;
|
|
49
54
|
};
|
|
50
55
|
export declare const schema: {
|
|
51
56
|
page: typeof pageSchema;
|
|
52
|
-
section<F extends BuilderMap = {}, S extends BuilderMap = {}
|
|
53
|
-
element<F extends BuilderMap = {}, S extends BuilderMap = {}
|
|
57
|
+
section<F extends BuilderMap = {}, S extends BuilderMap = {}>(input: SectionInput<F, S>): SectionSchema<F, S>;
|
|
58
|
+
element<F extends BuilderMap = {}, S extends BuilderMap = {}>(input: SectionInput<F, S> & {
|
|
54
59
|
contexts: ("richtext" | "column")[];
|
|
55
60
|
}): ElementSchema<F, S>;
|
|
56
61
|
};
|
package/dist/schema.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { describePath, findMatch } from "./fields.js";
|
|
2
2
|
import { pageSchema } from "./page.js";
|
|
3
|
+
import { assertConditions } from "./conditions.js";
|
|
4
|
+
import { STYLE_KEYS } from "./styles.js";
|
|
3
5
|
function toDescriptors(map) {
|
|
4
6
|
return Object.fromEntries(Object.entries(map ?? {}).map(([k, b]) => [k, b.descriptor]));
|
|
5
7
|
}
|
|
@@ -13,21 +15,31 @@ function assertNotLocalized(ns, map) {
|
|
|
13
15
|
function baseSchema(input) {
|
|
14
16
|
if (!input?.name)
|
|
15
17
|
throw new Error("smoodly: a schema requires a name.");
|
|
18
|
+
if ("meta" in input)
|
|
19
|
+
throw new Error('smoodly: "meta" is gone — put an id in settings and pass it as <smoodly.Section id={settings.id}>.');
|
|
20
|
+
if (input.styles !== undefined && input.styles !== false) {
|
|
21
|
+
if (typeof input.styles !== "object" || !Array.isArray(input.styles.omit)) {
|
|
22
|
+
throw new Error(`smoodly: section "${input.name}" declares styles — the developer's selects are "settings" since 2026-09-14; Smoodly's styles set is only toggled: false | { omit }.`);
|
|
23
|
+
}
|
|
24
|
+
for (const k of input.styles.omit) {
|
|
25
|
+
if (!STYLE_KEYS.includes(k))
|
|
26
|
+
throw new Error(`smoodly: section "${input.name}": styles.omit names "${k}", which is not a style key.`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
16
29
|
const fields = toDescriptors(input.fields);
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
assertNotLocalized("meta", meta);
|
|
30
|
+
const settings = toDescriptors(input.settings);
|
|
31
|
+
assertNotLocalized("settings", settings);
|
|
32
|
+
assertConditions(input.name, { fields, settings });
|
|
21
33
|
return {
|
|
22
34
|
name: input.name,
|
|
23
35
|
title: input.title,
|
|
24
36
|
icon: input.icon,
|
|
25
37
|
description: input.description,
|
|
26
38
|
fields,
|
|
27
|
-
|
|
28
|
-
meta,
|
|
39
|
+
settings,
|
|
29
40
|
sample: input.sample,
|
|
30
|
-
|
|
41
|
+
...(input.styles !== undefined ? { styles: input.styles } : {}),
|
|
42
|
+
...(input.css !== undefined ? { css: input.css } : {}),
|
|
31
43
|
};
|
|
32
44
|
}
|
|
33
45
|
export const schema = {
|
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
import type { CSSProperties, ReactNode } from "react";
|
|
2
|
-
|
|
2
|
+
import { type StyleProps, type StyleValues } from "./styles.ts";
|
|
3
|
+
/** The reserved key `<Zone>` and `getGlobal` ride on the styles object. */
|
|
4
|
+
export declare const SMD_KEY = "__smd";
|
|
5
|
+
export type SmdInternal = {
|
|
6
|
+
node?: string;
|
|
7
|
+
css?: string;
|
|
8
|
+
values?: StyleValues;
|
|
9
|
+
/** Pre-scoped layout CSS (the custom section, spec 2026-09-14 §6), emitted before the editor's css so the box wins. */
|
|
10
|
+
layoutCss?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function Section({ as, styles, className, style, children, ...rest }: {
|
|
3
13
|
as?: keyof React.JSX.IntrinsicElements;
|
|
4
|
-
styles:
|
|
14
|
+
styles: StyleProps & {
|
|
15
|
+
[SMD_KEY]?: SmdInternal;
|
|
16
|
+
};
|
|
5
17
|
className?: string;
|
|
6
18
|
style?: CSSProperties;
|
|
7
19
|
children?: ReactNode;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
}
|
|
20
|
+
[attr: `data-${string}`]: string | number | boolean | undefined;
|
|
21
|
+
[attr: `aria-${string}`]: string | number | boolean | undefined;
|
|
22
|
+
id?: string;
|
|
23
|
+
}): import("react").DOMElement<Record<string, unknown>, Element>;
|
package/dist/section-wrapper.js
CHANGED
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
import { createElement } from "react";
|
|
2
2
|
import { NODE_ATTR } from "./marks.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
19
|
-
|
|
3
|
+
import { DEFAULT_STYLE_VALUES, inlineStyles } from "./styles.js";
|
|
4
|
+
import { safeCss, scopedCss } from "./css.js";
|
|
5
|
+
// The root wrapper every section renders through (spec 2026-09-14 §2, §4).
|
|
6
|
+
// Owns the real root element, no extra div: element choice via `as`,
|
|
7
|
+
// Smoodly's styles set rendered INLINE (an editor's step beats the
|
|
8
|
+
// section's own class rule with no ordering and no !important), the
|
|
9
|
+
// node id as data-smd-node always (the custom-CSS scope needs it on the
|
|
10
|
+
// live site), the scoped <style> child, and every other prop spread onto
|
|
11
|
+
// the root — settings never become attributes; the view writes its own.
|
|
12
|
+
/** The reserved key `<Zone>` and `getGlobal` ride on the styles object. */
|
|
13
|
+
export const SMD_KEY = "__smd";
|
|
14
|
+
export function Section({ as = "section", styles, className, style, children, ...rest }) {
|
|
15
|
+
const { [SMD_KEY]: smd, ...steps } = (styles ?? {});
|
|
16
|
+
const inline = inlineStyles(steps, smd?.values ?? DEFAULT_STYLE_VALUES);
|
|
17
|
+
const merged = { ...(style ?? {}), ...inline };
|
|
18
|
+
const attrs = { ...rest };
|
|
19
|
+
if (smd?.node)
|
|
20
|
+
attrs[NODE_ATTR] = smd.node;
|
|
21
|
+
if (className !== undefined)
|
|
22
|
+
attrs.className = className;
|
|
23
|
+
if (Object.keys(merged).length > 0)
|
|
24
|
+
attrs.style = merged;
|
|
25
|
+
const css = smd?.node ? safeCss(smd.css) : "";
|
|
26
|
+
return createElement(as, attrs, smd?.layoutCss ? createElement("style", { key: "smd-layout" }, smd.layoutCss) : null, css ? createElement("style", { key: "smd-css" }, scopedCss(smd.node, css)) : null, children);
|
|
20
27
|
}
|
package/dist/site.d.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { type EntryRecord, type EntryStore, type Where } from "./entry-store.ts"
|
|
|
5
5
|
import { type LocaleSet, type PathTarget } from "./paths.ts";
|
|
6
6
|
import type { SectionLike } from "./refs.ts";
|
|
7
7
|
import type { PageContext, PageLink, PageTree } from "./render.tsx";
|
|
8
|
-
import { type
|
|
8
|
+
import { type GlobalSchema } from "./global.ts";
|
|
9
|
+
import { type StyleValues, type StyleValuesInput } from "./styles.ts";
|
|
10
|
+
import { type CustomConfigInput, type ResolvedCustom } from "./custom/index.ts";
|
|
9
11
|
import type { PageRecord, PageStore } from "./store.ts";
|
|
10
12
|
export type SiteOptions = {
|
|
11
13
|
stores: () => {
|
|
@@ -17,9 +19,13 @@ export type SiteOptions = {
|
|
|
17
19
|
sections: SectionLike[];
|
|
18
20
|
/** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
|
|
19
21
|
elements?: SectionLike[];
|
|
20
|
-
|
|
22
|
+
globals?: GlobalSchema[];
|
|
21
23
|
locales: LocaleSet;
|
|
22
24
|
homePageSlug?: string;
|
|
25
|
+
/** The CSS values behind Smoodly's styles set (spec 2026-09-14 §2); the package fallbacks fill the rest. */
|
|
26
|
+
styles?: StyleValuesInput;
|
|
27
|
+
/** The custom section's options (spec 2026-09-14 §6); the defaults when absent. */
|
|
28
|
+
custom?: CustomConfigInput;
|
|
23
29
|
/** Page shapes (entry pages are ignored): the source of each template's page-field descriptors (spec 2026-09-10 page fields §3). */
|
|
24
30
|
pages?: ReadonlyArray<SitePageSchema>;
|
|
25
31
|
};
|
|
@@ -82,12 +88,18 @@ export type EntryQuery = {
|
|
|
82
88
|
limit?: number;
|
|
83
89
|
draft?: boolean;
|
|
84
90
|
};
|
|
91
|
+
/** `version`: a stored page version to render instead of the draft — History (spec 2026-09-13 §4), draft mode only. */
|
|
85
92
|
export type LoadOptions = {
|
|
86
93
|
draft?: boolean;
|
|
94
|
+
version?: string;
|
|
87
95
|
};
|
|
88
96
|
export interface SmoodlySite {
|
|
89
97
|
readonly locales: LocaleSet;
|
|
90
98
|
readonly homePageSlug: string;
|
|
99
|
+
/** The resolved styles values the renderer and `getGlobal` hand the wrapper. */
|
|
100
|
+
readonly styles: StyleValues;
|
|
101
|
+
/** The custom section's resolved options the renderer hands renderPage. */
|
|
102
|
+
readonly custom: ResolvedCustom;
|
|
91
103
|
/** One index hit: what lives at (locale, path). Uncached by design — the id it returns is the cache tag. */
|
|
92
104
|
locate(locale: string, path: string): Promise<PathTarget | null>;
|
|
93
105
|
/** Everything the renderer needs for one target: null when it is a folder, unpublished (outside draft), or gone. */
|
|
@@ -110,11 +122,11 @@ export interface SmoodlySite {
|
|
|
110
122
|
locale?: string;
|
|
111
123
|
draft?: boolean;
|
|
112
124
|
}): Promise<EntryTreeItem[]>;
|
|
113
|
-
/** A
|
|
125
|
+
/** A global item's content in a locale (spec 2026-09-07 §4): an object
|
|
114
126
|
* item's merged fields, or a visual item's section props (fields plus
|
|
115
|
-
* `
|
|
127
|
+
* `settings`), refs hydrated; null when the item has no row there or is
|
|
116
128
|
* unpublished (outside draft mode). */
|
|
117
|
-
|
|
129
|
+
getGlobal<V>(item: GlobalSchema<V>, options?: {
|
|
118
130
|
locale?: string;
|
|
119
131
|
draft?: boolean;
|
|
120
132
|
}): Promise<V | null>;
|
package/dist/site.js
CHANGED
|
@@ -2,7 +2,10 @@ import { ARRAY_FIELD_TYPES, entryFieldsIn, localizedKeys, schemaOf } from "./ent
|
|
|
2
2
|
import { buildPageTree, chainOf, entryPathRows, entrySegment, pagePath, } from "./paths.js";
|
|
3
3
|
import { resolveFields, resolveMedia, resolveMediaSets, resolveTree } from "./resolve.js";
|
|
4
4
|
import { elementSchemas } from "./richtext-walk.js";
|
|
5
|
-
import { STYLES_KEY } from "./
|
|
5
|
+
import { globalNodeId, SETTINGS_KEY, STYLES_KEY, CSS_KEY } from "./global.js";
|
|
6
|
+
import { SMD_KEY } from "./section-wrapper.js";
|
|
7
|
+
import { resolveStyleValues } from "./styles.js";
|
|
8
|
+
import { resolveCustom } from "./custom/index.js";
|
|
6
9
|
import { withDefaults } from "./field-defaults.js";
|
|
7
10
|
/** The `page` prop for a loaded page. `toUrl` is the app's `href` seam
|
|
8
11
|
* bound to the render locale; the default keeps CMS paths as URLs. */
|
|
@@ -21,6 +24,8 @@ export function pageContextOf(hit, toUrl = (p) => p) {
|
|
|
21
24
|
export function createSmoodlySite(options) {
|
|
22
25
|
const locales = options.locales;
|
|
23
26
|
const homePageSlug = options.homePageSlug ?? "home";
|
|
27
|
+
const styles = resolveStyleValues(options.styles);
|
|
28
|
+
const custom = resolveCustom(options.custom);
|
|
24
29
|
const elements = elementSchemas(options.elements);
|
|
25
30
|
const pathOpts = { locales, homePageSlug };
|
|
26
31
|
const mounted = options.collections.filter((c) => c.path !== undefined);
|
|
@@ -33,9 +38,9 @@ export function createSmoodlySite(options) {
|
|
|
33
38
|
const assets = options.stores().assets;
|
|
34
39
|
return assets ? (ids) => assets.getMany(ids) : undefined;
|
|
35
40
|
};
|
|
36
|
-
const
|
|
41
|
+
const sectionSettings = (name) => {
|
|
37
42
|
const s = options.sections.find((x) => ("schema" in x ? x.schema : x).name === name);
|
|
38
|
-
return (s && ("schema" in s ? s.schema : s).
|
|
43
|
+
return (s && ("schema" in s ? s.schema : s).settings) ?? {};
|
|
39
44
|
};
|
|
40
45
|
/** Visible to the public in a locale: present there, and a folder or a
|
|
41
46
|
* published page. Draft mode sees every present node. Presence is the
|
|
@@ -86,7 +91,7 @@ export function createSmoodlySite(options) {
|
|
|
86
91
|
hasPage: hasPage(node, locale, draft),
|
|
87
92
|
fields: fields.get(node.id) ?? {},
|
|
88
93
|
});
|
|
89
|
-
async function loadPage(id, locale, path, draft) {
|
|
94
|
+
async function loadPage(id, locale, path, draft, version) {
|
|
90
95
|
const { pages } = options.stores();
|
|
91
96
|
const record = await pages.getPage(id);
|
|
92
97
|
if (!record || record.template === null)
|
|
@@ -94,17 +99,26 @@ export function createSmoodlySite(options) {
|
|
|
94
99
|
const row = record.locales[locale];
|
|
95
100
|
if (!row)
|
|
96
101
|
return null;
|
|
97
|
-
|
|
102
|
+
let raw;
|
|
103
|
+
if (version !== undefined) {
|
|
104
|
+
// History: a stored version of THIS page in THIS locale, and only inside draft mode.
|
|
105
|
+
const stored = draft ? await pages.getVersion(version) : null;
|
|
106
|
+
raw = stored && stored.pageId === id && stored.locale === locale ? stored.tree : null;
|
|
107
|
+
}
|
|
108
|
+
else {
|
|
109
|
+
raw = draft ? await pages.getDraftTree(id, locale) : row.status === "published" ? await pages.getPublishedTree(id, locale) : null;
|
|
110
|
+
}
|
|
98
111
|
if (!raw)
|
|
99
112
|
return null;
|
|
100
113
|
const tree = await resolveTree(raw, {
|
|
101
114
|
sections: options.sections,
|
|
102
115
|
collections: options.collections,
|
|
103
|
-
|
|
116
|
+
globals: options.globals,
|
|
104
117
|
elements: options.elements,
|
|
105
118
|
fetch: fetcher(draft, locale),
|
|
106
119
|
assets: assetFetcher(),
|
|
107
120
|
page: { fields: pageFields(record.template) },
|
|
121
|
+
custom,
|
|
108
122
|
});
|
|
109
123
|
const all = await pages.listPages();
|
|
110
124
|
const index = new Map(all.map((n) => [n.id, n]));
|
|
@@ -259,7 +273,7 @@ export function createSmoodlySite(options) {
|
|
|
259
273
|
const build = (parentId) => items.filter((i) => i.entry.parentId === parentId).map((i) => ({ ...i, children: build(i.entry.id) }));
|
|
260
274
|
return build(null);
|
|
261
275
|
},
|
|
262
|
-
async
|
|
276
|
+
async getGlobal(item, o) {
|
|
263
277
|
const locale = localeOf(o);
|
|
264
278
|
const draft = o?.draft ?? false;
|
|
265
279
|
const { entries } = options.stores();
|
|
@@ -277,20 +291,30 @@ export function createSmoodlySite(options) {
|
|
|
277
291
|
const fields = await resolveFields(raw, item.fields, { collections: options.collections, elements: options.elements, fetch: fetcher(draft, locale), assets: assetFetcher() });
|
|
278
292
|
if (!item.section)
|
|
279
293
|
return fields;
|
|
280
|
-
const { [STYLES_KEY]:
|
|
294
|
+
const { [SETTINGS_KEY]: settings, [STYLES_KEY]: steps, [CSS_KEY]: css, ...rest } = fields;
|
|
281
295
|
const fetch = assetFetcher();
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
296
|
+
const own = settings !== null && typeof settings === "object" ? settings : {};
|
|
297
|
+
if (fetch)
|
|
298
|
+
await resolveMedia(own, sectionSettings(item.section), fetch, elements);
|
|
299
|
+
// The layout node id, the css and the values ride the styles under the reserved key (spec 2026-09-13 §1, 2026-09-14 §2, §4):
|
|
300
|
+
// the wrapper strips it and emits data-smd-node, so the canvas selects the item and the css reaches it.
|
|
301
|
+
const stepsObject = steps !== null && typeof steps === "object" ? steps : {};
|
|
302
|
+
return {
|
|
303
|
+
...rest,
|
|
304
|
+
settings: own,
|
|
305
|
+
styles: { ...stepsObject, [SMD_KEY]: { node: globalNodeId(item.name), css: typeof css === "string" ? css : undefined, values: styles } },
|
|
306
|
+
};
|
|
285
307
|
},
|
|
286
308
|
};
|
|
287
309
|
const site = {
|
|
288
310
|
locales,
|
|
289
311
|
homePageSlug,
|
|
312
|
+
styles,
|
|
313
|
+
custom,
|
|
290
314
|
locate: (locale, path) => options.stores().pages.resolvePath(locale, path),
|
|
291
315
|
load(target, locale, path, opts = {}) {
|
|
292
316
|
const draft = opts.draft ?? false;
|
|
293
|
-
return target.kind === "page" ? loadPage(target.id, locale, path, draft) : loadEntry(target.id, locale, path, draft);
|
|
317
|
+
return target.kind === "page" ? loadPage(target.id, locale, path, draft, opts.version) : loadEntry(target.id, locale, path, draft);
|
|
294
318
|
},
|
|
295
319
|
async resolve(locale, path, opts) {
|
|
296
320
|
const target = await site.locate(locale, path);
|
package/dist/sql-space.js
CHANGED
|
@@ -31,7 +31,7 @@ $$;`.trim();
|
|
|
31
31
|
}
|
|
32
32
|
/** Which rows a read key (or the anon key, which has no claim) may see.
|
|
33
33
|
* null = write keys only — refs is editor tooling; editors is admission
|
|
34
|
-
* data; nothing a browser needs.
|
|
34
|
+
* data; nothing a browser needs. Global content (spec 2026-09-07) rides
|
|
35
35
|
* the three entry predicates: it is stored as entries. */
|
|
36
36
|
const PUBLISHED_READ = {
|
|
37
37
|
pages: `exists (select 1 from "page_locales" l where l."page_id" = "pages"."id" and l."status" = 'published')`,
|