smoodly 0.0.16 → 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.
Files changed (133) hide show
  1. package/README.md +156 -21
  2. package/dist/admin/client-ops.js +1 -1
  3. package/dist/admin/custom-section.d.ts +10 -0
  4. package/dist/admin/custom-section.js +10 -0
  5. package/dist/admin/editor/CanvasOverlay.d.ts +4 -4
  6. package/dist/admin/editor/CanvasOverlay.js +5 -5
  7. package/dist/admin/editor/EditorView.js +140 -108
  8. package/dist/admin/editor/GlobalInspector.d.ts +24 -0
  9. package/dist/admin/editor/{SharedInspector.js → GlobalInspector.js} +4 -4
  10. package/dist/admin/editor/InsertPicker.d.ts +6 -5
  11. package/dist/admin/editor/InsertPicker.js +6 -4
  12. package/dist/admin/editor/InspectorFields.d.ts +21 -0
  13. package/dist/admin/editor/InspectorFields.js +22 -0
  14. package/dist/admin/editor/Outline.d.ts +0 -1
  15. package/dist/admin/editor/Outline.js +3 -3
  16. package/dist/admin/editor/{shared-edit.d.ts → global-edit.d.ts} +10 -10
  17. package/dist/admin/editor/{shared-edit.js → global-edit.js} +4 -4
  18. package/dist/admin/editor/useGlobalEdit.d.ts +6 -0
  19. package/dist/admin/editor/{useSharedEdit.js → useGlobalEdit.js} +4 -4
  20. package/dist/admin/forms/FieldWidget.d.ts +14 -1
  21. package/dist/admin/forms/FieldWidget.js +38 -11
  22. package/dist/admin/forms/ListField.d.ts +9 -1
  23. package/dist/admin/forms/ListField.js +16 -8
  24. package/dist/admin/forms/RichtextElementCard.js +11 -5
  25. package/dist/admin/forms/RowsWidget.d.ts +13 -0
  26. package/dist/admin/forms/RowsWidget.js +26 -0
  27. package/dist/admin/forms/SpanBar.d.ts +5 -0
  28. package/dist/admin/forms/SpanBar.js +37 -0
  29. package/dist/admin/forms/richtext-insert.d.ts +2 -2
  30. package/dist/admin/forms/richtext-insert.js +3 -3
  31. package/dist/admin/forms/richtext-nodes.js +1 -1
  32. package/dist/admin/forms/tabs.d.ts +15 -9
  33. package/dist/admin/forms/tabs.js +19 -23
  34. package/dist/admin/forms/useRichtextEditor.js +8 -1
  35. package/dist/admin/global-items.d.ts +9 -0
  36. package/dist/admin/{shared-items.js → global-items.js} +17 -16
  37. package/dist/admin/index.d.ts +3 -3
  38. package/dist/admin/index.js +1 -1
  39. package/dist/admin/ops-impl.js +65 -54
  40. package/dist/admin/ops.d.ts +6 -6
  41. package/dist/admin/serialize.d.ts +18 -6
  42. package/dist/admin/serialize.js +27 -4
  43. package/dist/admin/shell/AdminApp.js +6 -6
  44. package/dist/admin/shell/GlobalFields.d.ts +24 -0
  45. package/dist/admin/shell/GlobalFields.js +19 -0
  46. package/dist/admin/shell/{SharedForm.d.ts → GlobalForm.d.ts} +1 -1
  47. package/dist/admin/shell/{SharedForm.js → GlobalForm.js} +36 -26
  48. package/dist/admin/shell/{SharedList.d.ts → GlobalList.d.ts} +1 -1
  49. package/dist/admin/shell/{SharedList.js → GlobalList.js} +9 -9
  50. package/dist/admin/shell/IconRail.js +13 -6
  51. package/dist/admin/shell/PagesList.js +1 -6
  52. package/dist/admin/shell/{shared-list.d.ts → global-list.d.ts} +3 -3
  53. package/dist/admin/shell/{shared-list.js → global-list.js} +2 -2
  54. package/dist/admin/tree-ops.d.ts +10 -8
  55. package/dist/admin/tree-ops.js +28 -16
  56. package/dist/admin/ui/HistoryModal.js +1 -1
  57. package/dist/admin/ui/primitives.d.ts +3 -1
  58. package/dist/admin/ui/primitives.js +2 -2
  59. package/dist/admin/ui/theme.js +54 -14
  60. package/dist/admin/validate.d.ts +27 -2
  61. package/dist/admin/validate.js +47 -10
  62. package/dist/collections.d.ts +3 -3
  63. package/dist/collections.js +1 -1
  64. package/dist/conditions.d.ts +33 -0
  65. package/dist/conditions.js +79 -0
  66. package/dist/config.d.ts +26 -19
  67. package/dist/config.js +26 -25
  68. package/dist/css.d.ts +7 -0
  69. package/dist/css.js +57 -0
  70. package/dist/custom/CustomSection.d.ts +26 -0
  71. package/dist/custom/CustomSection.js +20 -0
  72. package/dist/custom/css.d.ts +3 -0
  73. package/dist/custom/css.js +28 -0
  74. package/dist/custom/index.d.ts +26 -0
  75. package/dist/custom/index.js +16 -0
  76. package/dist/custom/rows.d.ts +28 -0
  77. package/dist/custom/rows.js +129 -0
  78. package/dist/custom/schema.d.ts +32 -0
  79. package/dist/custom/schema.js +39 -0
  80. package/dist/entry-store.d.ts +4 -4
  81. package/dist/entry-store.js +17 -17
  82. package/dist/fields.d.ts +5 -2
  83. package/dist/fields.js +4 -2
  84. package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
  85. package/dist/{shared-id.js → global-id.js} +4 -4
  86. package/dist/global.d.ts +57 -0
  87. package/dist/global.js +51 -0
  88. package/dist/index.d.ts +28 -18
  89. package/dist/index.js +10 -5
  90. package/dist/media.js +1 -1
  91. package/dist/next/index.d.ts +1 -1
  92. package/dist/next/page-renderer.d.ts +4 -2
  93. package/dist/next/page-renderer.js +9 -9
  94. package/dist/next/queries.d.ts +6 -6
  95. package/dist/next/queries.js +7 -7
  96. package/dist/page.js +2 -2
  97. package/dist/pairing.d.ts +13 -7
  98. package/dist/pairing.js +4 -4
  99. package/dist/refs.d.ts +2 -2
  100. package/dist/refs.js +4 -4
  101. package/dist/render.d.ts +13 -1
  102. package/dist/render.js +10 -5
  103. package/dist/resolve.d.ts +11 -6
  104. package/dist/resolve.js +22 -12
  105. package/dist/revalidate.d.ts +4 -4
  106. package/dist/revalidate.js +4 -4
  107. package/dist/richtext-render.js +2 -2
  108. package/dist/richtext-walk.d.ts +5 -5
  109. package/dist/richtext-walk.js +4 -4
  110. package/dist/schema.d.ts +15 -10
  111. package/dist/schema.js +19 -7
  112. package/dist/section-wrapper.d.ts +18 -6
  113. package/dist/section-wrapper.js +24 -17
  114. package/dist/site.d.ts +15 -5
  115. package/dist/site.js +24 -10
  116. package/dist/sql-space.js +1 -1
  117. package/dist/sql.js +2 -2
  118. package/dist/store.d.ts +7 -0
  119. package/dist/store.js +6 -0
  120. package/dist/styles.d.ts +27 -0
  121. package/dist/styles.js +60 -0
  122. package/dist/supabase-entry-store.js +14 -14
  123. package/dist/supabase-store.js +2 -1
  124. package/dist/zones.d.ts +9 -7
  125. package/dist/zones.js +5 -5
  126. package/package.json +1 -1
  127. package/dist/admin/editor/SharedInspector.d.ts +0 -23
  128. package/dist/admin/editor/useSharedEdit.d.ts +0 -6
  129. package/dist/admin/shared-items.d.ts +0 -9
  130. package/dist/admin/shell/SharedFields.d.ts +0 -21
  131. package/dist/admin/shell/SharedFields.js +0 -15
  132. package/dist/shared.d.ts +0 -53
  133. package/dist/shared.js +0 -45
@@ -1,12 +1,12 @@
1
1
  // The in-app read API (spec 2026-09-11): five typed queries over the
2
2
  // framework-free site layer, each draft-aware inside the editor preview
3
- // and cached under a tag outside it — the pattern getSmoodlyShared set.
3
+ // and cached under a tag outside it — the pattern getSmoodlyGlobal set.
4
4
  // Lives in next/ because draftMode and unstable_cache are Next's; an
5
5
  // Astro glue wraps the same site reads over its own cache seam.
6
6
  import { unstable_cache } from "next/cache";
7
7
  import { draftMode } from "next/headers";
8
8
  import { connection } from "next/server";
9
- import { collectionTag, entryTag, pageTag, pagesTreeTag, sharedTag } from "../revalidate.js";
9
+ import { collectionTag, entryTag, pageTag, pagesTreeTag, globalTag } from "../revalidate.js";
10
10
  /** JSON with object keys sorted at every level and `undefined` dropped, so two spellings of one query share a unit. */
11
11
  function stable(value) {
12
12
  if (Array.isArray(value))
@@ -26,7 +26,7 @@ function entriesKey(o) {
26
26
  }
27
27
  export function createSmoodlyQueries(options) {
28
28
  const { site } = options;
29
- const sharedByName = new Map((options.shared ?? []).map((s) => [s.name, s]));
29
+ const globalByName = new Map((options.globals ?? []).map((s) => [s.name, s]));
30
30
  const localeOf = (o) => o?.locale ?? site.locales.default;
31
31
  /** Every query gates on the request (never a build prerender), then reads live in draft mode or cached under `tags`. */
32
32
  async function run(key, tags, read) {
@@ -76,12 +76,12 @@ export function createSmoodlyQueries(options) {
76
76
  return hit;
77
77
  });
78
78
  },
79
- async getShared(name, o) {
79
+ async getGlobal(name, o) {
80
80
  const locale = localeOf(o);
81
- const item = sharedByName.get(name);
81
+ const item = globalByName.get(name);
82
82
  if (!item)
83
- throw new Error(`smoodly: no shared item named "${name}".`);
84
- return run(["shared", name, locale], [sharedTag(name)], (draft) => site.getShared(item, { locale, draft }));
83
+ throw new Error(`smoodly: no global item named "${name}".`);
84
+ return run(["global", name, locale], [globalTag(name)], (draft) => site.getGlobal(item, { locale, draft }));
85
85
  },
86
86
  };
87
87
  }
package/dist/page.js CHANGED
@@ -30,12 +30,12 @@ function normalizePolicy(p) {
30
30
  return out;
31
31
  }
32
32
  case "freeform":
33
- return { kind: "freeform", allow: p.allow, rows: p.rows };
33
+ return { kind: "freeform", allow: p.allow, custom: p.custom === true };
34
34
  case "locked":
35
35
  return {
36
36
  kind: "locked",
37
37
  component: p.component,
38
- ...(typeof p.shared === "string" ? { shared: p.shared } : {}),
38
+ ...(typeof p.global === "string" ? { global: p.global } : {}),
39
39
  };
40
40
  default:
41
41
  throw new Error(`smoodly: unknown zone policy kind "${String(p.kind)}".`);
package/dist/pairing.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  import type { ComponentType, ReactElement } from "react";
2
2
  import type { BuilderMap, ElementSchema, NamespaceValues, SectionSchema } from "./schema.ts";
3
3
  import type { PageContext, ZoneContent } from "./render.tsx";
4
+ import type { StyleProps } from "./styles.ts";
4
5
  /** What the view receives. A page view gets its zones and where it sits
5
- * in the tree; a section/element view gets field values flat and styles
6
- * complete (defaults filled). The page branch keys on `kind` — an
6
+ * in the tree; a section/element view gets field values flat, settings
7
+ * complete (defaults filled) and Smoodly's styles steps as stored (Default
8
+ * stores nothing — spec 2026-09-14 §2). The page branch keys on `kind` — an
7
9
  * optional-only check would match section schemas too, since a type
8
10
  * lacking `__zones` still extends `{ __zones?: X }`. */
9
11
  export type Props<S> = S extends {
@@ -17,19 +19,23 @@ export type Props<S> = S extends {
17
19
  __f?: infer F extends BuilderMap;
18
20
  __s?: infer St extends BuilderMap;
19
21
  } ? NamespaceValues<F> & {
20
- styles: NamespaceValues<St>;
22
+ settings: NamespaceValues<St>;
23
+ styles: StyleProps;
21
24
  } : never;
22
- /** What hard-coded JSX may pass: fields flat, styles optional and partial. */
25
+ /** What hard-coded JSX may pass: fields flat, settings optional and partial, styles optional. */
23
26
  type ExternalProps<F extends BuilderMap, St extends BuilderMap> = NamespaceValues<F> & {
24
- styles?: Partial<NamespaceValues<St>>;
27
+ settings?: Partial<NamespaceValues<St>>;
28
+ styles?: StyleProps;
25
29
  };
26
30
  export type PairedComponent<Schema, F extends BuilderMap, St extends BuilderMap> = ((props: ExternalProps<F, St>) => ReactElement) & {
27
31
  schema: Schema;
28
32
  };
29
33
  export declare function section<F extends BuilderMap, St extends BuilderMap>(schema: SectionSchema<F, St>, View: ComponentType<NamespaceValues<F> & {
30
- styles: NamespaceValues<St>;
34
+ settings: NamespaceValues<St>;
35
+ styles: StyleProps;
31
36
  }>): PairedComponent<SectionSchema<F, St>, F, St>;
32
37
  export declare function element<F extends BuilderMap, St extends BuilderMap>(schema: ElementSchema<F, St>, View: ComponentType<NamespaceValues<F> & {
33
- styles: NamespaceValues<St>;
38
+ settings: NamespaceValues<St>;
39
+ styles: StyleProps;
34
40
  }>): PairedComponent<ElementSchema<F, St>, F, St>;
35
41
  export {};
package/dist/pairing.js CHANGED
@@ -1,12 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { defaultsOf } from "./field-defaults.js";
3
3
  function pair(schema, View) {
4
- const defaults = defaultsOf(schema.styles);
4
+ const defaults = defaultsOf(schema.settings);
5
5
  const Component = (props) => {
6
- const { styles: styleOverrides, ...fields } = props ?? {};
7
- const styles = { ...defaults, ...styleOverrides };
6
+ const { settings: overrides, styles, ...fields } = props ?? {};
7
+ const settings = { ...defaults, ...overrides };
8
8
  const V = View;
9
- return _jsx(V, { ...fields, styles: styles });
9
+ return _jsx(V, { ...fields, settings: settings, styles: styles ?? {} });
10
10
  };
11
11
  Component.schema = schema;
12
12
  return Component;
package/dist/refs.d.ts CHANGED
@@ -11,12 +11,12 @@ export declare function collectEntryRefs(fields: Record<string, unknown>, schema
11
11
  export type SectionLike = {
12
12
  name: string;
13
13
  fields: Record<string, Descriptor>;
14
- styles?: Record<string, Descriptor>;
14
+ settings?: Record<string, Descriptor>;
15
15
  } | {
16
16
  schema: {
17
17
  name: string;
18
18
  fields: Record<string, Descriptor>;
19
- styles?: Record<string, Descriptor>;
19
+ settings?: Record<string, Descriptor>;
20
20
  };
21
21
  };
22
22
  export declare function collectTreeRefs(tree: PageTree, sections: SectionLike[], elements?: SectionLike[]): RefEdge[];
package/dist/refs.js CHANGED
@@ -4,7 +4,7 @@
4
4
  // and never the source of truth. Stores rewrite a source's edges on
5
5
  // every save; `refs` answers only the REVERSE question (who depends on
6
6
  // this?) — forward resolution stays resolveTree's job.
7
- import { sharedLink } from "./shared.js";
7
+ import { globalLink } from "./global.js";
8
8
  import { ASSET_COLLECTION, isMediaValue, MEDIA_TYPES } from "./media.js";
9
9
  import { ELEMENT_NODE, IMAGE_NODE, elementOf, elementSchemas, elementSets, richtextBlocks } from "./richtext-walk.js";
10
10
  function targetOf(d) {
@@ -87,7 +87,7 @@ export function collectTreeRefs(tree, sections, elements) {
87
87
  for (const node of nodes) {
88
88
  // A placement (spec 2026-09-07 §5): one edge, keyed by the node id
89
89
  // so two placements of one item on one page stay distinct rows.
90
- const link = sharedLink(node);
90
+ const link = globalLink(node);
91
91
  if (link) {
92
92
  out.push({ field: node.id, targetCollection: link.item, targetId: link.ref });
93
93
  continue;
@@ -96,8 +96,8 @@ export function collectTreeRefs(tree, sections, elements) {
96
96
  if (!schema)
97
97
  continue; // fail-soft, same as renderPage
98
98
  collectFrom(node.fields, schema.fields, out, els);
99
- if (schema.styles && node.styles)
100
- collectFrom(node.styles, schema.styles, out, els);
99
+ if (schema.settings && node.settings)
100
+ collectFrom(node.settings, schema.settings, out, els);
101
101
  }
102
102
  }
103
103
  return out;
package/dist/render.d.ts CHANGED
@@ -1,12 +1,18 @@
1
1
  import type { ComponentType, ReactElement } from "react";
2
2
  import type { PageSchema } from "./page.ts";
3
3
  import type { BuilderMap, NamespaceValues } from "./schema.ts";
4
+ import { type StyleValues } from "./styles.ts";
5
+ import { type CustomOptions } from "./custom/index.ts";
4
6
  export type TreeNode = {
5
7
  id: string;
6
8
  type: string;
7
9
  fields: Record<string, unknown>;
10
+ /** The developer's knobs (spec 2026-09-14 §3). */
11
+ settings?: Record<string, unknown>;
12
+ /** Smoodly's styles steps (spec 2026-09-14 §2); absent keys are Default. */
8
13
  styles?: Record<string, unknown>;
9
- meta?: Record<string, unknown>;
14
+ /** The custom-CSS snippet (spec 2026-09-14 §4), scoped by the wrapper. */
15
+ css?: string;
10
16
  };
11
17
  /** `meta` is the page's own metadata (SEO title, description, og image).
12
18
  * It rides inside the tree deliberately: trees are already JSONB in
@@ -60,6 +66,8 @@ export type ZoneContent = {
60
66
  readonly name: string;
61
67
  readonly nodes: TreeNode[];
62
68
  readonly components: ReadonlyMap<string, AnyPaired>;
69
+ /** The config's CSS values behind the styles set (spec 2026-09-14 §2). */
70
+ readonly styleValues: StyleValues;
63
71
  readonly editor: boolean;
64
72
  };
65
73
  export declare function Zone({ of: content }: {
@@ -76,6 +84,10 @@ export declare function page<Zones extends Record<string, unknown>, F extends Bu
76
84
  export type Registry = {
77
85
  sections?: AnyPaired[];
78
86
  elements?: AnyPaired[];
87
+ /** Resolved styles values; the package fallbacks when absent. */
88
+ styles?: StyleValues;
89
+ /** The custom section's bound RichText and breakpoint (spec 2026-09-14 §6); the defaults when absent. */
90
+ custom?: CustomOptions;
79
91
  };
80
92
  export declare function renderPage<Zones extends Record<string, unknown>>(Page: PairedPage<Zones>, tree: PageTree, registry: Registry, options?: {
81
93
  editor?: boolean;
package/dist/render.js CHANGED
@@ -1,5 +1,8 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  import { EMPTY_ATTR, ZONE_ATTR } from "./marks.js";
3
+ import { SMD_KEY } from "./section-wrapper.js";
4
+ import { DEFAULT_STYLE_VALUES } from "./styles.js";
5
+ import { CUSTOM_NODE_TYPE, createCustomSection } from "./custom/index.js";
3
6
  /** The `page` prop of a view rendered outside the site (a test, a preview
4
7
  * of a shape): a real object, so a view never guards an optional prop, but
5
8
  * no position in the tree. Frozen: one singleton shared by every such
@@ -22,10 +25,9 @@ export function Zone({ of: content }) {
22
25
  console.warn(`smoodly: no registered component for node type "${node.type}" — skipping.`);
23
26
  return null;
24
27
  }
25
- const styles = content.editor
26
- ? { ...(node.styles ?? {}), __smoodlyNode: node.id }
27
- : (node.styles ?? {});
28
- return _jsx(Component, { ...node.fields, styles: styles }, node.id);
28
+ // The node id, the css and the values ride the styles under one reserved key; the wrapper reads and strips it.
29
+ const styles = { ...(node.styles ?? {}), [SMD_KEY]: { node: node.id, css: typeof node.css === "string" ? node.css : undefined, values: content.styleValues } };
30
+ return _jsx(Component, { ...node.fields, settings: node.settings ?? {}, styles: styles }, node.id);
29
31
  }) }));
30
32
  }
31
33
  export function page(schema, View) {
@@ -41,9 +43,12 @@ export function renderPage(Page, tree, registry, options) {
41
43
  for (const c of [...(registry.sections ?? []), ...(registry.elements ?? [])]) {
42
44
  components.set(c.schema.name, c);
43
45
  }
46
+ // The custom section (spec 2026-09-14 §6): Smoodly's own renderer under the reserved node type.
47
+ components.set(CUSTOM_NODE_TYPE, createCustomSection({ ...(registry.custom ?? {}), elements: (registry.elements ?? []) }));
48
+ const styleValues = registry.styles ?? DEFAULT_STYLE_VALUES;
44
49
  const zones = Object.fromEntries(Object.keys(Page.schema.zones).map((zone) => [
45
50
  zone,
46
- { name: zone, nodes: tree.zones?.[zone] ?? [], components, editor: options?.editor ?? false },
51
+ { name: zone, nodes: tree.zones?.[zone] ?? [], components, styleValues, editor: options?.editor ?? false },
47
52
  ]));
48
53
  return _jsx(Page, { zones: zones, page: (options?.page ?? EMPTY_PAGE_CONTEXT) });
49
54
  }
package/dist/resolve.d.ts CHANGED
@@ -1,18 +1,19 @@
1
1
  import type { Descriptor } from "./fields.ts";
2
2
  import type { CollectionSchema } from "./collections.ts";
3
3
  import type { PageTree } from "./render.tsx";
4
- import { type SharedSchema } from "./shared.ts";
4
+ import { type GlobalSchema } from "./global.ts";
5
5
  import { type AssetRecord } from "./media.ts";
6
6
  import { type ElementSchemas } from "./richtext-walk.ts";
7
7
  import type { SectionLike } from "./refs.ts";
8
+ import type { Toolset } from "./toolset.ts";
8
9
  export type EntryFetcher = (collection: string, ids: string[]) => Promise<Record<string, Record<string, unknown>>>;
9
10
  export type AssetFetcher = (ids: string[]) => Promise<Record<string, AssetRecord>>;
10
11
  type SchemaLike = {
11
12
  name: string;
12
13
  fields: Record<string, Descriptor>;
13
- styles?: Record<string, Descriptor>;
14
+ settings?: Record<string, Descriptor>;
14
15
  };
15
- /** One value set whose media resolves in place: a node's fields or styles,
16
+ /** One value set whose media resolves in place: a node's fields or settings,
16
17
  * an entry's fields, or an entry hydrated through a ref. */
17
18
  export type MediaSet = {
18
19
  values: Record<string, unknown> | undefined;
@@ -26,8 +27,8 @@ type ResolveOptions = {
26
27
  fetch: EntryFetcher;
27
28
  /** Media (spec 2026-09-08 §9): one batched asset fetch per page, values become resolved objects. */
28
29
  assets?: AssetFetcher;
29
- /** The registered shared items a placement node may name (spec 2026-09-07 §4). */
30
- shared?: SharedSchema[];
30
+ /** The registered global items a placement node may name (spec 2026-09-07 §4). */
31
+ globals?: GlobalSchema[];
31
32
  /** Registered Elements a richtext document may embed (spec 2026-09-08 block nodes §2). */
32
33
  elements?: SectionLike[];
33
34
  /** Optional safety valve on chain length; unlimited by default. */
@@ -36,9 +37,13 @@ type ResolveOptions = {
36
37
  page?: {
37
38
  fields: Record<string, Descriptor>;
38
39
  };
40
+ /** The custom section's columns toolset (spec 2026-09-14 §6); the default when absent. The schema is always in the map. */
41
+ custom?: {
42
+ toolset?: Toolset;
43
+ };
39
44
  };
40
45
  export declare function resolveTree(tree: PageTree, options: ResolveOptions): Promise<PageTree>;
41
- /** One value set's media, resolved in place. Exported for the site layer's shared-item styles. */
46
+ /** One value set's media, resolved in place. Exported for the site layer's global-item settings. */
42
47
  export declare function resolveMedia(values: Record<string, unknown>, descriptors: Record<string, Descriptor>, fetch: AssetFetcher, elements?: ElementSchemas): Promise<void>;
43
48
  /** Many value sets, one asset round (plus one for posters): the tree's
44
49
  * nodes and every entry hydrated through a ref, or a list page's items.
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 { sharedLink, SHARED_NODE_TYPE, STYLES_KEY } from "./shared.js";
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.shared ?? [], options.fetch);
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.styles, descriptors: schema.styles ?? {} });
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 shared-item styles. */
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, shared, fetch) {
103
- const byName = new Map(shared.map((s) => [s.name, s]));
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 = sharedLink(node);
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 = sharedLink(node);
122
+ const link = globalLink(node);
120
123
  if (!link)
121
- return node.type === SHARED_NODE_TYPE ? [] : [node];
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
- const { [STYLES_KEY]: styles, ...fields } = structuredClone(row);
127
- const isObject = styles !== null && typeof styles === "object";
128
- return [{ id: node.id, type: schema.section, fields, ...(isObject ? { styles: styles } : {}) }];
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
  }
@@ -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 shared item's cache unit, by NAME (a singleton has one row per
12
- * locale, and code owns the name): the Next glue's getSmoodlyShared
13
- * caches under it; ops.shared expires it. */
14
- export declare const sharedTag: (name: string) => string;
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;
@@ -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 shared item's cache unit, by NAME (a singleton has one row per
11
- * locale, and code owns the name): the Next glue's getSmoodlyShared
12
- * caches under it; ops.shared expires it. */
13
- export const sharedTag = (name) => `shared:${name}`;
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";
@@ -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 styles = a.styles && typeof a.styles === "object" ? a.styles : {};
71
- return _jsx(Component, { ...fields, styles: styles });
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
  }
@@ -8,7 +8,7 @@ export type ImageNodeAttrs = {
8
8
  export type ElementNodeAttrs = {
9
9
  name?: unknown;
10
10
  fields?: unknown;
11
- styles?: unknown;
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
- styles: Record<string, Descriptor>;
23
+ settings: Record<string, Descriptor>;
24
24
  }>;
25
25
  type ElementLike = {
26
26
  name: string;
27
27
  fields: Record<string, Descriptor>;
28
- styles?: Record<string, Descriptor>;
28
+ settings?: Record<string, Descriptor>;
29
29
  } | {
30
30
  schema: {
31
31
  name: string;
32
32
  fields: Record<string, Descriptor>;
33
- styles?: Record<string, Descriptor>;
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 style value sets, for a walk. */
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>;
@@ -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, styles: s.styles ?? {} };
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 style value sets, for a walk. */
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 styles = asRecord(a.styles);
41
+ const settings = asRecord(a.settings);
42
42
  return [
43
43
  ...(fields ? [{ values: fields, descriptors: schema.fields }] : []),
44
- ...(styles ? [{ values: styles, descriptors: schema.styles }] : []),
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
- styles: Record<string, Descriptor>;
12
- meta: Record<string, Descriptor>;
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
- styleDefaults?: false | {
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, M 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
- styles?: S;
44
- meta?: M;
45
- sample?: Partial<NamespaceValues<F> & NamespaceValues<S>>;
46
- styleDefaults?: false | {
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 = {}, M extends BuilderMap = {}>(input: SectionInput<F, S, M>): SectionSchema<F, S>;
53
- element<F extends BuilderMap = {}, S extends BuilderMap = {}, M extends BuilderMap = {}>(input: SectionInput<F, S, M> & {
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 styles = toDescriptors(input.styles);
18
- const meta = toDescriptors(input.meta);
19
- assertNotLocalized("styles", styles);
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
- styles,
28
- meta,
39
+ settings,
29
40
  sample: input.sample,
30
- styleDefaults: input.styleDefaults,
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
- export declare function Section({ as, styles, className, style, children, }: {
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: Record<string, unknown>;
14
+ styles: StyleProps & {
15
+ [SMD_KEY]?: SmdInternal;
16
+ };
5
17
  className?: string;
6
18
  style?: CSSProperties;
7
19
  children?: ReactNode;
8
- }): import("react").ReactElement<{
9
- className: string | undefined;
10
- style: CSSProperties | undefined;
11
- }, string | import("react").JSXElementConstructor<any>>;
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>;