smoodly 0.0.16 → 0.0.18

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 (142) hide show
  1. package/README.md +176 -26
  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 +8 -6
  7. package/dist/admin/editor/EditorView.js +140 -108
  8. package/dist/admin/editor/GlobalInspector.d.ts +27 -0
  9. package/dist/admin/editor/GlobalInspector.js +13 -0
  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/ActionMenu.d.ts +3 -1
  57. package/dist/admin/ui/ActionMenu.js +4 -3
  58. package/dist/admin/ui/HistoryModal.js +1 -1
  59. package/dist/admin/ui/PublishControl.d.ts +3 -1
  60. package/dist/admin/ui/PublishControl.js +2 -2
  61. package/dist/admin/ui/primitives.d.ts +4 -2
  62. package/dist/admin/ui/primitives.js +3 -3
  63. package/dist/admin/ui/theme.js +61 -18
  64. package/dist/admin/validate.d.ts +27 -2
  65. package/dist/admin/validate.js +47 -10
  66. package/dist/collections.d.ts +3 -3
  67. package/dist/collections.js +1 -1
  68. package/dist/conditions.d.ts +33 -0
  69. package/dist/conditions.js +79 -0
  70. package/dist/config.d.ts +26 -19
  71. package/dist/config.js +26 -25
  72. package/dist/css.d.ts +7 -0
  73. package/dist/css.js +57 -0
  74. package/dist/custom/CustomSection.d.ts +26 -0
  75. package/dist/custom/CustomSection.js +20 -0
  76. package/dist/custom/css.d.ts +3 -0
  77. package/dist/custom/css.js +28 -0
  78. package/dist/custom/index.d.ts +26 -0
  79. package/dist/custom/index.js +16 -0
  80. package/dist/custom/rows.d.ts +28 -0
  81. package/dist/custom/rows.js +129 -0
  82. package/dist/custom/schema.d.ts +32 -0
  83. package/dist/custom/schema.js +39 -0
  84. package/dist/entry-page.d.ts +5 -0
  85. package/dist/entry-page.js +1 -1
  86. package/dist/entry-store.d.ts +4 -4
  87. package/dist/entry-store.js +17 -17
  88. package/dist/fields.d.ts +5 -2
  89. package/dist/fields.js +4 -2
  90. package/dist/{shared-id.d.ts → global-id.d.ts} +1 -1
  91. package/dist/{shared-id.js → global-id.js} +4 -4
  92. package/dist/global.d.ts +57 -0
  93. package/dist/global.js +51 -0
  94. package/dist/index.d.ts +31 -21
  95. package/dist/index.js +11 -6
  96. package/dist/media.js +1 -1
  97. package/dist/next/index.d.ts +1 -1
  98. package/dist/next/page-renderer.d.ts +4 -2
  99. package/dist/next/page-renderer.js +16 -12
  100. package/dist/next/queries.d.ts +6 -6
  101. package/dist/next/queries.js +7 -7
  102. package/dist/page.js +2 -2
  103. package/dist/pairing.d.ts +13 -7
  104. package/dist/pairing.js +4 -4
  105. package/dist/paths.d.ts +21 -0
  106. package/dist/paths.js +25 -0
  107. package/dist/refs.d.ts +2 -2
  108. package/dist/refs.js +4 -4
  109. package/dist/render.d.ts +23 -1
  110. package/dist/render.js +12 -6
  111. package/dist/resolve.d.ts +11 -6
  112. package/dist/resolve.js +22 -12
  113. package/dist/revalidate.d.ts +4 -4
  114. package/dist/revalidate.js +4 -4
  115. package/dist/richtext-render.js +2 -2
  116. package/dist/richtext-walk.d.ts +5 -5
  117. package/dist/richtext-walk.js +4 -4
  118. package/dist/schema.d.ts +15 -10
  119. package/dist/schema.js +19 -7
  120. package/dist/section-wrapper.d.ts +18 -6
  121. package/dist/section-wrapper.js +24 -17
  122. package/dist/site.d.ts +29 -14
  123. package/dist/site.js +55 -28
  124. package/dist/sql-space.js +1 -1
  125. package/dist/sql.js +2 -2
  126. package/dist/store.d.ts +7 -0
  127. package/dist/store.js +6 -0
  128. package/dist/styles.d.ts +27 -0
  129. package/dist/styles.js +60 -0
  130. package/dist/supabase-entry-store.js +14 -14
  131. package/dist/supabase-store.js +2 -1
  132. package/dist/zones.d.ts +9 -7
  133. package/dist/zones.js +5 -5
  134. package/package.json +1 -1
  135. package/dist/admin/editor/SharedInspector.d.ts +0 -23
  136. package/dist/admin/editor/SharedInspector.js +0 -13
  137. package/dist/admin/editor/useSharedEdit.d.ts +0 -6
  138. package/dist/admin/shared-items.d.ts +0 -9
  139. package/dist/admin/shell/SharedFields.d.ts +0 -21
  140. package/dist/admin/shell/SharedFields.js +0 -15
  141. package/dist/shared.d.ts +0 -53
  142. package/dist/shared.js +0 -45
package/dist/index.d.ts CHANGED
@@ -4,16 +4,21 @@ import { toolset } from "./toolset.ts";
4
4
  import { Section } from "./section-wrapper.tsx";
5
5
  import { page } from "./render.tsx";
6
6
  import { entryPage } from "./entry-page.tsx";
7
- import { shared } from "./shared.ts";
7
+ import { globalItem } from "./global.ts";
8
8
  import { marks } from "./marks.ts";
9
9
  export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.tsx";
10
- export type { PageTree, TreeNode, ZoneContent, PairedPage, Registry, PageContext, PageContextLink, PageLink, PageViewProps } from "./render.tsx";
10
+ export type { PageTree, TreeNode, ZoneContent, PairedPage, Registry, PageContext, PageContextLink, PageContextAlternate, PageLink, PageViewProps } from "./render.tsx";
11
11
  export { RichText, defaultRichTextImage } from "./richtext-render.tsx";
12
12
  export type { RichTextProps, RichTextElement, RichTextImageProps } from "./richtext-render.tsx";
13
13
  export type { PageSchema } from "./page.ts";
14
14
  export { fixedSlug } from "./page.ts";
15
15
  export type { EntryPageSchema, PairedEntryPage, EntryPageProps } from "./entry-page.tsx";
16
16
  export { f, normalizeOptions, optionsOf, optionValues } from "./fields.ts";
17
+ export { when, evaluate, isHidden, assertConditions, splitConditionPath } from "./conditions.ts";
18
+ export { DEFAULT_STYLE_VALUES, STYLE_KEYS, SPACING_NONE, resolveStyleValues, styleDescriptors, inlineStyles } from "./styles.ts";
19
+ export type { StyleValues, StyleValuesInput, StyleProps, StyleKey } from "./styles.ts";
20
+ export * from "./custom/index.ts";
21
+ export type { Condition, ConditionOp, ConditionScope } from "./conditions.ts";
17
22
  export { defaultsOf, withDefaults } from "./field-defaults.ts";
18
23
  export { filterPageTree } from "./page-tree.ts";
19
24
  export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT, parseAspect, cropAspect, clamp01, isMediaValue, collectAssetIds, applyAssets, } from "./media.ts";
@@ -39,12 +44,12 @@ export { SupabaseEntryStore } from "./supabase-entry-store.ts";
39
44
  export { SupabasePathIndex } from "./supabase-path-index.ts";
40
45
  export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, } from "./entry-store.ts";
41
46
  export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.ts";
42
- export { affectedTargets, pageTag, entryTag, sharedTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
47
+ export { affectedTargets, pageTag, entryTag, globalTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
43
48
  export type { AffectedTargets } from "./revalidate.ts";
44
49
  export { resolveTree, resolveFields, resolveMedia } from "./resolve.ts";
45
50
  export type { EntryFetcher, ResolveFieldsOptions, AssetFetcher } from "./resolve.ts";
46
- export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, } from "./paths.ts";
47
- export type { LocaleSet, PathTarget, PathRow, PageLike, EntryLike, PageTreeNode, PagePathOptions } from "./paths.ts";
51
+ export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, pageAlternates, entryAlternates, } from "./paths.ts";
52
+ export type { LocaleSet, PathTarget, PathRow, PageLike, EntryLike, PageTreeNode, PagePathOptions, LocaleAlternate, PageAlternate } from "./paths.ts";
48
53
  export { fixedPathFor } from "./localize.ts";
49
54
  export type { PageStore, PageRecord, PageLocale, PageVersion, PageStoreOptions, CreatePageInput, AddLocaleOptions } from "./store.ts";
50
55
  export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.ts";
@@ -56,21 +61,28 @@ export type { CollectionSchema, EntryOrder } from "./collections.ts";
56
61
  export { DEFAULT_TOOLSET, type Toolset, type ToolsetMark, type ToolsetNode } from "./toolset.ts";
57
62
  export { collectionOrder, collectionDepth, DEFAULT_ORDER } from "./collections.ts";
58
63
  export type { ZonePolicy } from "./zones.ts";
59
- export { SHARED_NODE_TYPE, STYLES_KEY, sharedLink } from "./shared.ts";
60
- export { sharedEntryId } from "./shared-id.ts";
64
+ export { GLOBAL_NODE_TYPE, SETTINGS_KEY, STYLES_KEY, CSS_KEY, globalLink } from "./global.ts";
65
+ export { SMD_KEY } from "./section-wrapper.tsx";
66
+ export type { SmdInternal } from "./section-wrapper.tsx";
67
+ export { CSS_MAX, cssProblem, safeCss, scopedCss } from "./css.ts";
68
+ export { globalEntryId } from "./global-id.ts";
61
69
  export { DEFAULT_SPACE_ID } from "./sql-space.ts";
62
- export type { SharedSchema, SharedLink } from "./shared.ts";
70
+ export type { GlobalSchema, GlobalLink } from "./global.ts";
63
71
  export declare const smoodly: {
64
72
  schema: {
65
73
  page: typeof import("./page.ts").pageSchema;
66
- section<F extends import("./schema.ts").BuilderMap = {}, S extends import("./schema.ts").BuilderMap = {}, M extends import("./schema.ts").BuilderMap = {}>(input: {
74
+ section<F extends import("./schema.ts").BuilderMap = {}, S extends import("./schema.ts").BuilderMap = {}>(input: {
67
75
  name: string;
68
76
  title: string;
69
77
  icon?: string;
70
78
  description?: string;
71
79
  fields?: F | undefined;
72
- styles?: S | undefined;
73
- meta?: M | undefined;
80
+ settings?: S | undefined;
81
+ meta?: never;
82
+ styles?: false | {
83
+ omit: string[];
84
+ };
85
+ css?: false;
74
86
  sample?: Partial<{ [K_1 in Exclude<keyof F, { [K in keyof F]: F[K] extends {
75
87
  __flags?: infer Fl;
76
88
  } ? Fl extends {
@@ -88,18 +100,19 @@ export declare const smoodly: {
88
100
  } ? Fl extends {
89
101
  optional: true;
90
102
  } ? K_3 : never : never; }[keyof S]]?: import("./fields.ts").ValueOf<S[K_5]> | undefined; }> | undefined;
91
- styleDefaults?: false | {
92
- omit: string[];
93
- };
94
103
  }): import("./schema.ts").SectionSchema<F, S>;
95
- element<F extends import("./schema.ts").BuilderMap = {}, S extends import("./schema.ts").BuilderMap = {}, M extends import("./schema.ts").BuilderMap = {}>(input: {
104
+ element<F extends import("./schema.ts").BuilderMap = {}, S extends import("./schema.ts").BuilderMap = {}>(input: {
96
105
  name: string;
97
106
  title: string;
98
107
  icon?: string;
99
108
  description?: string;
100
109
  fields?: F | undefined;
101
- styles?: S | undefined;
102
- meta?: M | undefined;
110
+ settings?: S | undefined;
111
+ meta?: never;
112
+ styles?: false | {
113
+ omit: string[];
114
+ };
115
+ css?: false;
103
116
  sample?: Partial<{ [K_1 in Exclude<keyof F, { [K in keyof F]: F[K] extends {
104
117
  __flags?: infer Fl;
105
118
  } ? Fl extends {
@@ -117,9 +130,6 @@ export declare const smoodly: {
117
130
  } ? Fl extends {
118
131
  optional: true;
119
132
  } ? K_3 : never : never; }[keyof S]]?: import("./fields.ts").ValueOf<S[K_5]> | undefined; }> | undefined;
120
- styleDefaults?: false | {
121
- omit: string[];
122
- };
123
133
  } & {
124
134
  contexts: ("richtext" | "column")[];
125
135
  }): import("./schema.ts").ElementSchema<F, S>;
@@ -130,7 +140,7 @@ export declare const smoodly: {
130
140
  entryPage: typeof entryPage;
131
141
  collection: typeof collection;
132
142
  toolset: typeof toolset;
133
- shared: typeof shared;
143
+ global: typeof globalItem;
134
144
  Section: typeof Section;
135
145
  marks: typeof marks;
136
146
  };
package/dist/index.js CHANGED
@@ -5,12 +5,15 @@ import { toolset } from "./toolset.js";
5
5
  import { Section } from "./section-wrapper.js";
6
6
  import { page } from "./render.js";
7
7
  import { entryPage } from "./entry-page.js";
8
- import { shared } from "./shared.js";
8
+ import { globalItem } from "./global.js";
9
9
  import { marks } from "./marks.js";
10
10
  export { Zone, renderPage, EMPTY_PAGE_CONTEXT } from "./render.js";
11
11
  export { RichText, defaultRichTextImage } from "./richtext-render.js";
12
12
  export { fixedSlug } from "./page.js";
13
13
  export { f, normalizeOptions, optionsOf, optionValues } from "./fields.js";
14
+ export { when, evaluate, isHidden, assertConditions, splitConditionPath } from "./conditions.js";
15
+ export { DEFAULT_STYLE_VALUES, STYLE_KEYS, SPACING_NONE, resolveStyleValues, styleDescriptors, inlineStyles } from "./styles.js";
16
+ export * from "./custom/index.js";
14
17
  export { defaultsOf, withDefaults } from "./field-defaults.js";
15
18
  export { filterPageTree } from "./page-tree.js";
16
19
  export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT, parseAspect, cropAspect, clamp01, isMediaValue, collectAssetIds, applyAssets, } from "./media.js";
@@ -30,15 +33,17 @@ export { MemoryEntryStore } from "./entry-store.js";
30
33
  export { SupabaseEntryStore } from "./supabase-entry-store.js";
31
34
  export { SupabasePathIndex } from "./supabase-path-index.js";
32
35
  export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.js";
33
- export { affectedTargets, pageTag, entryTag, sharedTag, pagesTreeTag, collectionTag } from "./revalidate.js";
36
+ export { affectedTargets, pageTag, entryTag, globalTag, pagesTreeTag, collectionTag } from "./revalidate.js";
34
37
  export { resolveTree, resolveFields, resolveMedia } from "./resolve.js";
35
- export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, } from "./paths.js";
38
+ export { SEGMENT_RE, isSegment, assertSegment, joinPath, splitPath, segmentFor, isHomeNode, pagePathIn, chainOf, assertDepth, pagePath, pagePathRows, entryPathRows, collectionSegment, buildPageTree, perLocaleSegments, assertSupportedLocale, unsupportedLocale, pageAlternates, entryAlternates, } from "./paths.js";
36
39
  export { fixedPathFor } from "./localize.js";
37
40
  export { pathOptions, pageErrors, localeRow, pageRefEdges, DEFAULT_LOCALES } from "./store.js";
38
41
  export { DEFAULT_TOOLSET } from "./toolset.js";
39
42
  export { collectionOrder, collectionDepth, DEFAULT_ORDER } from "./collections.js";
40
- export { SHARED_NODE_TYPE, STYLES_KEY, sharedLink } from "./shared.js";
41
- export { sharedEntryId } from "./shared-id.js";
43
+ export { GLOBAL_NODE_TYPE, SETTINGS_KEY, STYLES_KEY, CSS_KEY, globalLink } from "./global.js";
44
+ export { SMD_KEY } from "./section-wrapper.js";
45
+ export { CSS_MAX, cssProblem, safeCss, scopedCss } from "./css.js";
46
+ export { globalEntryId } from "./global-id.js";
42
47
  export { DEFAULT_SPACE_ID } from "./sql-space.js";
43
48
  export const smoodly = {
44
49
  schema,
@@ -48,7 +53,7 @@ export const smoodly = {
48
53
  entryPage,
49
54
  collection,
50
55
  toolset,
51
- shared,
56
+ global: globalItem,
52
57
  Section,
53
58
  marks,
54
59
  };
package/dist/media.js CHANGED
@@ -43,7 +43,7 @@ export function collectAssetIds(values, descriptors, out, elements = {}) {
43
43
  out.add(value.asset);
44
44
  }
45
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.
46
+ // Block nodes (spec 2026-09-08 block nodes §2): image nodes, and media inside a known element's fields and settings.
47
47
  for (const node of richtextBlocks(value)) {
48
48
  if (node.type === IMAGE_NODE) {
49
49
  const image = node.attrs.image;
@@ -2,4 +2,4 @@ export { createPageRenderer } from "./page-renderer.tsx";
2
2
  export type { PageRendererOptions, PathRenderOptions, PageRegistration } from "./page-renderer.tsx";
3
3
  export { metadataFrom } from "./meta.ts";
4
4
  export { createSmoodlyQueries } from "./queries.ts";
5
- export type { SmoodlyQueries, QueryOptions, EntriesQuery, TypedEntryItem, SharedValue } from "./queries.ts";
5
+ export type { SmoodlyQueries, QueryOptions, EntriesQuery, TypedEntryItem, GlobalValue } from "./queries.ts";
@@ -2,7 +2,7 @@ import type { ComponentType, ReactElement } from "react";
2
2
  import type { Metadata } from "next";
3
3
  import type { Descriptor } from "../fields.ts";
4
4
  import { type PairedPage } from "../render.tsx";
5
- import type { SharedSchema } from "../shared.ts";
5
+ import type { GlobalSchema } from "../global.ts";
6
6
  import { type SmoodlySite } from "../site.ts";
7
7
  /** A registered section, as renderPage's registry needs it. */
8
8
  type SectionEntry = ComponentType<any> & {
@@ -22,6 +22,8 @@ export type PageRegistration = ((props: any) => ReactElement) & {
22
22
  export type PageRendererOptions = {
23
23
  site: SmoodlySite;
24
24
  sections: SectionEntry[];
25
+ /** Registered Elements: what the custom section's default richtext renderer needs when no RichText is bound (spec 2026-09-14 §6). */
26
+ elements?: SectionEntry[];
25
27
  pages: ReadonlyArray<PageRegistration>;
26
28
  /** Locale-relative CMS path → site URL. Default: the path itself. With
27
29
  * next-intl: `(path, locale) => getPathname({ locale, href: path })`. */
@@ -32,7 +34,7 @@ export type PathRenderOptions = {
32
34
  searchParams?: Promise<Record<string, string | string[] | undefined>>;
33
35
  };
34
36
  export declare function createPageRenderer(options: PageRendererOptions): {
35
- getSmoodlyShared: <V>(item: SharedSchema<V>, opts?: {
37
+ getSmoodlyGlobal: <V>(item: GlobalSchema<V>, opts?: {
36
38
  locale?: string;
37
39
  }) => Promise<V | null>;
38
40
  renderSmoodlyPath: (segments: string[], opts?: PathRenderOptions) => Promise<ReactElement<unknown, string | import("react").JSXElementConstructor<any>>>;
@@ -7,7 +7,7 @@ import { EditorBridge } from "../admin/next/bridge.js";
7
7
  import { fixedPathFor } from "../localize.js";
8
8
  import { joinPath } from "../paths.js";
9
9
  import { renderPage } from "../render.js";
10
- import { entryTag, pageTag, sharedTag } from "../revalidate.js";
10
+ import { entryTag, pageTag, globalTag } from "../revalidate.js";
11
11
  import { pageContextOf } from "../site.js";
12
12
  import { metadataFrom } from "./meta.js";
13
13
  export function createPageRenderer(options) {
@@ -34,20 +34,24 @@ export function createPageRenderer(options) {
34
34
  console.warn(`smoodly: no page registration named "${hit.template}" — the record at ${hit.path} is a 404.`);
35
35
  notFound();
36
36
  }
37
- const page = pageContextOf(hit, (path) => href(path, locale));
38
- return renderPage(Page, hit.tree, { sections: options.sections }, { editor, page });
37
+ const page = pageContextOf(hit, href);
38
+ return renderPage(Page, hit.tree, { sections: options.sections, elements: options.elements, styles: site.styles, custom: site.custom }, { editor, page });
39
39
  }
40
40
  const View = entryPages.get(hit.collection);
41
41
  if (!View) {
42
42
  console.warn(`smoodly: no entry page for collection "${hit.collection}" — the entry at ${hit.path} is a 404.`);
43
43
  notFound();
44
44
  }
45
- return _jsx(View, { entry: hit.entry, path: hit.path });
45
+ return _jsx(View, { entry: hit.entry, path: hit.path, locale: hit.locale, locales: hit.locales });
46
46
  }
47
+ /** hreflang from the SAME map a language switch reads (spec 2026-09-15 §3): the locales with a URL, through href. */
47
48
  function alternates(hit) {
48
49
  if (site.locales.supported.length < 2)
49
50
  return {};
50
- const languages = Object.fromEntries(Object.entries(hit.paths).map(([locale, path]) => [locale, href(path, locale)]));
51
+ const languages = {};
52
+ for (const [locale, alt] of Object.entries(hit.locales))
53
+ if (alt?.path)
54
+ languages[locale] = href(alt.path, locale);
51
55
  return { alternates: { languages } };
52
56
  }
53
57
  async function page(pathFor, opts, accept) {
@@ -86,20 +90,20 @@ export function createPageRenderer(options) {
86
90
  * error surfaces as a rejected promise like every other failure. */
87
91
  const fixedPath = (Page) => (locale) => fixedPathFor(Page.schema, locale, site.locales, site.homePageSlug);
88
92
  const literal = (segments) => () => joinPath(segments);
89
- /** A shared item for a layout: cached per (item, locale) under the
90
- * shared tag outside draft mode, the live draft inside it. */
91
- async function getSmoodlyShared(item, opts) {
93
+ /** A global item for a layout: cached per (item, locale) under the
94
+ * global tag outside draft mode, the live draft inside it. */
95
+ async function getSmoodlyGlobal(item, opts) {
92
96
  await connection();
93
97
  const locale = opts?.locale ?? site.locales.default;
94
98
  const { isEnabled: draft } = await draftMode();
95
99
  if (draft)
96
- return site.getShared(item, { locale, draft: true });
97
- return unstable_cache(() => site.getShared(item, { locale }), ["smoodly-shared", item.name, locale], {
98
- tags: [sharedTag(item.name)],
100
+ return site.getGlobal(item, { locale, draft: true });
101
+ return unstable_cache(() => site.getGlobal(item, { locale }), ["smoodly-global", item.name, locale], {
102
+ tags: [globalTag(item.name)],
99
103
  })();
100
104
  }
101
105
  return {
102
- getSmoodlyShared,
106
+ getSmoodlyGlobal,
103
107
  renderSmoodlyPath: (segments, opts) => page(literal(segments), opts, any),
104
108
  smoodlyMetadata: (segments, opts) => metadata(literal(segments), opts, any),
105
109
  /** A fixed registration rendered from the developer's own route file. */
@@ -1,6 +1,6 @@
1
1
  import type { CollectionSchema } from "../collections.ts";
2
2
  import type { EntryRecord } from "../entry-store.ts";
3
- import type { SharedSchema } from "../shared.ts";
3
+ import type { GlobalSchema } from "../global.ts";
4
4
  import type { EntryQuery, PageTreeItem, SiteEntryItem, SitePage, SmoodlySite } from "../site.ts";
5
5
  export type QueryOptions = {
6
6
  locale?: string;
@@ -17,8 +17,8 @@ export type TypedEntryItem<C extends readonly CollectionSchema<any, string>[], N
17
17
  fields: EntryFields<C, N>;
18
18
  };
19
19
  };
20
- export type SharedValue<S extends readonly SharedSchema<any, string>[], N> = string extends S[number]["name"] ? Record<string, unknown> : NonNullable<ByName<S[number], N>["__value"]>;
21
- export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S extends readonly SharedSchema<any, string>[]> = {
20
+ export type GlobalValue<S extends readonly GlobalSchema<any, string>[], N> = string extends S[number]["name"] ? Record<string, unknown> : NonNullable<ByName<S[number], N>["__value"]>;
21
+ export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S extends readonly GlobalSchema<any, string>[]> = {
22
22
  /** The published page tree in a locale, every item with its page fields. */
23
23
  getPageTree(options?: QueryOptions): Promise<PageTreeItem[]>;
24
24
  /** A page by locale-relative path ("/company/about-us") or id. */
@@ -26,11 +26,11 @@ export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S
26
26
  /** Published entries of a collection: `where` (shared fields), `order`, `limit`, `parent` run in the store. */
27
27
  getEntries<N extends C[number]["name"]>(collection: N, options?: EntriesQuery): Promise<TypedEntryItem<C, N>[]>;
28
28
  getEntry<N extends C[number]["name"]>(collection: N, idOrPath: string, options?: QueryOptions): Promise<TypedEntryItem<C, N> | null>;
29
- getShared<N extends S[number]["name"]>(item: N, options?: QueryOptions): Promise<SharedValue<S, N> | null>;
29
+ getGlobal<N extends S[number]["name"]>(item: N, options?: QueryOptions): Promise<GlobalValue<S, N> | null>;
30
30
  };
31
- export declare function createSmoodlyQueries<const C extends readonly CollectionSchema<any, string>[], const S extends readonly SharedSchema<any, string>[] = []>(options: {
31
+ export declare function createSmoodlyQueries<const C extends readonly CollectionSchema<any, string>[], const S extends readonly GlobalSchema<any, string>[] = []>(options: {
32
32
  site: SmoodlySite;
33
33
  collections: C;
34
- shared?: S;
34
+ globals?: S;
35
35
  }): SmoodlyQueries<C, S>;
36
36
  export {};
@@ -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/paths.d.ts CHANGED
@@ -91,6 +91,27 @@ export declare function entrySegment(entry: EntryLike, locale: string): string |
91
91
  export declare function entryPathRows(targets: EntryLike[], byId: (id: string) => EntryLike | undefined, collection: {
92
92
  path?: string | Record<string, string>;
93
93
  }, set: LocaleSet): PathRow[];
94
+ /** A target's address in one locale (spec 2026-09-15 §1). A page always
95
+ * has both; an entry's slug is null when its row claims no URL, and its
96
+ * path null then or when the collection has no `path`. */
97
+ export type LocaleAlternate = {
98
+ slug: string | null;
99
+ path: string | null;
100
+ };
101
+ export type PageAlternate = {
102
+ slug: string;
103
+ path: string;
104
+ };
105
+ /** The page's address in EVERY supported locale, in config order: null
106
+ * where the node is absent, where an ancestor is, or where `advertised`
107
+ * says no — the site passes "published, or draft mode", so an
108
+ * unpublished translation is never linked on the public site. */
109
+ export declare function pageAlternates(node: PageLike, byId: (id: string) => PageLike | undefined, opts: PagePathOptions, advertised: (locale: string) => boolean): Record<string, PageAlternate | null>;
110
+ /** The entry's address in EVERY supported locale, in config order: null
111
+ * where the row is absent or not advertised. `pathIn` is the caller's
112
+ * path source — the derived rows in a list, the `paths` index for one
113
+ * loaded record — so both entry reads apply ONE rule. */
114
+ export declare function entryAlternates(entry: EntryLike, set: LocaleSet, advertised: (locale: string) => boolean, pathIn: (locale: string) => string | null): Record<string, LocaleAlternate | null>;
94
115
  export type PageTreeNode<T extends PageLike> = {
95
116
  record: T;
96
117
  path: string | null;
package/dist/paths.js CHANGED
@@ -130,6 +130,31 @@ export function entryPathRows(targets, byId, collection, set) {
130
130
  }
131
131
  return rows;
132
132
  }
133
+ /** The page's address in EVERY supported locale, in config order: null
134
+ * where the node is absent, where an ancestor is, or where `advertised`
135
+ * says no — the site passes "published, or draft mode", so an
136
+ * unpublished translation is never linked on the public site. */
137
+ export function pageAlternates(node, byId, opts, advertised) {
138
+ const out = {};
139
+ for (const locale of opts.locales.supported) {
140
+ const own = node.locales[locale];
141
+ const path = own && advertised(locale) ? pagePathIn(node, byId, locale, opts) : null;
142
+ out[locale] = own && path !== null ? { slug: own.slug, path } : null;
143
+ }
144
+ return out;
145
+ }
146
+ /** The entry's address in EVERY supported locale, in config order: null
147
+ * where the row is absent or not advertised. `pathIn` is the caller's
148
+ * path source — the derived rows in a list, the `paths` index for one
149
+ * loaded record — so both entry reads apply ONE rule. */
150
+ export function entryAlternates(entry, set, advertised, pathIn) {
151
+ const out = {};
152
+ for (const locale of set.supported) {
153
+ const own = entry.locales[locale];
154
+ out[locale] = own && advertised(locale) ? { slug: own.slug, path: pathIn(locale) } : null;
155
+ }
156
+ return out;
157
+ }
133
158
  export function buildPageTree(records, locale, opts) {
134
159
  const byId = (id) => records.find((r) => r.id === id);
135
160
  const build = (parentId) => records
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,19 @@
1
1
  import type { ComponentType, ReactElement } from "react";
2
+ import type { PageAlternate } from "./paths.ts";
2
3
  import type { PageSchema } from "./page.ts";
3
4
  import type { BuilderMap, NamespaceValues } from "./schema.ts";
5
+ import { type StyleValues } from "./styles.ts";
6
+ import { type CustomOptions } from "./custom/index.ts";
4
7
  export type TreeNode = {
5
8
  id: string;
6
9
  type: string;
7
10
  fields: Record<string, unknown>;
11
+ /** The developer's knobs (spec 2026-09-14 §3). */
12
+ settings?: Record<string, unknown>;
13
+ /** Smoodly's styles steps (spec 2026-09-14 §2); absent keys are Default. */
8
14
  styles?: Record<string, unknown>;
9
- meta?: Record<string, unknown>;
15
+ /** The custom-CSS snippet (spec 2026-09-14 §4), scoped by the wrapper. */
16
+ css?: string;
10
17
  };
11
18
  /** `meta` is the page's own metadata (SEO title, description, og image).
12
19
  * It rides inside the tree deliberately: trees are already JSONB in
@@ -35,16 +42,25 @@ export type PageLink = {
35
42
  export type PageContextLink = PageLink & {
36
43
  url: string;
37
44
  };
45
+ /** The page's address in one other locale, `url` through the app's href seam (spec 2026-09-15 §3). */
46
+ export type PageContextAlternate = PageAlternate & {
47
+ url: string;
48
+ };
38
49
  export type PageContext<Fields = Record<string, unknown>> = {
39
50
  id: string;
40
51
  title: string;
41
52
  path: string;
42
53
  url: string;
54
+ /** The locale the page was rendered in. */
55
+ locale: string;
43
56
  /** The page's own fields, typed from its schema on the view; resolved, defaults applied. */
44
57
  fields: Fields;
45
58
  /** Root first, parent last. */
46
59
  ancestors: ReadonlyArray<PageContextLink>;
47
60
  children: ReadonlyArray<PageContextLink>;
61
+ /** One key per supported locale: the page's `{ slug, path, url }` there, or null where it is
62
+ * absent or unpublished (draft mode: absent only). What a language switch links to. */
63
+ locales: Readonly<Record<string, PageContextAlternate | null>>;
48
64
  };
49
65
  /** The `page` prop of a view rendered outside the site (a test, a preview
50
66
  * of a shape): a real object, so a view never guards an optional prop, but
@@ -60,6 +76,8 @@ export type ZoneContent = {
60
76
  readonly name: string;
61
77
  readonly nodes: TreeNode[];
62
78
  readonly components: ReadonlyMap<string, AnyPaired>;
79
+ /** The config's CSS values behind the styles set (spec 2026-09-14 §2). */
80
+ readonly styleValues: StyleValues;
63
81
  readonly editor: boolean;
64
82
  };
65
83
  export declare function Zone({ of: content }: {
@@ -76,6 +94,10 @@ export declare function page<Zones extends Record<string, unknown>, F extends Bu
76
94
  export type Registry = {
77
95
  sections?: AnyPaired[];
78
96
  elements?: AnyPaired[];
97
+ /** Resolved styles values; the package fallbacks when absent. */
98
+ styles?: StyleValues;
99
+ /** The custom section's bound RichText and breakpoint (spec 2026-09-14 §6); the defaults when absent. */
100
+ custom?: CustomOptions;
79
101
  };
80
102
  export declare function renderPage<Zones extends Record<string, unknown>>(Page: PairedPage<Zones>, tree: PageTree, registry: Registry, options?: {
81
103
  editor?: boolean;