smoodly 0.0.45 → 0.0.47

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 (245) hide show
  1. package/README.md +3 -3
  2. package/dist/addresses.d.ts +6 -7
  3. package/dist/addresses.js +6 -4
  4. package/dist/admin/PublicSans-LICENSE.txt +93 -0
  5. package/dist/admin/auth-config.d.ts +6 -0
  6. package/dist/admin/auth-config.js +1 -0
  7. package/dist/admin/browser/fetch-op.d.ts +10 -0
  8. package/dist/admin/browser/fetch-op.js +44 -0
  9. package/dist/admin/browser/main.d.ts +1 -0
  10. package/dist/admin/browser/main.js +21 -0
  11. package/dist/admin/custom-section.d.ts +1 -1
  12. package/dist/admin/drift.d.ts +1 -1
  13. package/dist/admin/editor/EditorView.js +14 -14
  14. package/dist/admin/editor/autosave.js +1 -1
  15. package/dist/admin/editor/canvas-address.d.ts +2 -0
  16. package/dist/admin/editor/canvas-address.js +8 -0
  17. package/dist/admin/editor/overlay-geometry.d.ts +1 -1
  18. package/dist/admin/editor/protocol.d.ts +18 -4
  19. package/dist/admin/editor/protocol.js +15 -1
  20. package/dist/admin/editor/useCanvasBridge.d.ts +1 -1
  21. package/dist/admin/editor/useCanvasBridge.js +5 -6
  22. package/dist/admin/editor/useInlineSession.js +4 -2
  23. package/dist/admin/files.d.ts +11 -0
  24. package/dist/admin/files.generated.js +2 -0
  25. package/dist/admin/files.js +8 -0
  26. package/dist/admin/forms/useRefOptions.d.ts +2 -2
  27. package/dist/admin/forms/useRefOptions.js +2 -2
  28. package/dist/admin/forms/useRichtextEditor.js +4 -4
  29. package/dist/admin/handler.d.ts +40 -0
  30. package/dist/admin/handler.js +151 -0
  31. package/dist/admin/health.d.ts +4 -0
  32. package/dist/admin/index.d.ts +1 -1
  33. package/dist/admin/ops-impl.d.ts +7 -1
  34. package/dist/admin/ops-impl.js +112 -35
  35. package/dist/admin/ops.d.ts +3 -2
  36. package/dist/admin/page-data.d.ts +21 -0
  37. package/dist/admin/page-data.js +4 -0
  38. package/dist/admin/page.d.ts +6 -0
  39. package/dist/admin/page.js +35 -0
  40. package/dist/admin/serialize.js +7 -7
  41. package/dist/admin/shell/AdminApp.js +2 -2
  42. package/dist/admin/shell/ContentHealthPage.js +3 -1
  43. package/dist/admin/shell/EntryForm.js +1 -1
  44. package/dist/admin/shell/router.d.ts +3 -16
  45. package/dist/admin/shell/router.js +3 -39
  46. package/dist/admin/shell/session.d.ts +2 -5
  47. package/dist/admin/supabase-auth.js +1 -1
  48. package/dist/admin/tree-ops.d.ts +1 -1
  49. package/dist/admin/ui/fonts.d.ts +12 -0
  50. package/dist/admin/ui/fonts.js +22 -0
  51. package/dist/admin/ui/theme.d.ts +13 -4
  52. package/dist/admin/ui/theme.js +28 -157
  53. package/dist/assemble.d.ts +60 -0
  54. package/dist/assemble.js +359 -0
  55. package/dist/astro/cache.d.ts +34 -0
  56. package/dist/astro/cache.js +69 -0
  57. package/dist/astro/components/Custom.astro +21 -0
  58. package/dist/astro/components/Guarded.astro +24 -0
  59. package/dist/astro/components/Head.astro +26 -0
  60. package/dist/astro/components/Image.astro +20 -0
  61. package/dist/astro/components/RichText.astro +23 -0
  62. package/dist/astro/components/Section.astro +20 -0
  63. package/dist/astro/components/Video.astro +5 -0
  64. package/dist/astro/components/Zone.astro +19 -0
  65. package/dist/astro/components.d.ts +6 -0
  66. package/dist/astro/components.js +7 -0
  67. package/dist/astro/content-loaders.d.ts +40 -0
  68. package/dist/astro/content-loaders.js +100 -0
  69. package/dist/astro/content.d.ts +9 -0
  70. package/dist/astro/content.js +17 -0
  71. package/dist/astro/custom.d.ts +1 -0
  72. package/dist/astro/custom.js +3 -0
  73. package/dist/astro/defaults.d.ts +20 -0
  74. package/dist/astro/defaults.js +21 -0
  75. package/dist/astro/env-files.d.ts +1 -0
  76. package/dist/astro/env-files.js +21 -0
  77. package/dist/astro/head.d.ts +13 -0
  78. package/dist/astro/head.js +19 -0
  79. package/dist/astro/image.d.ts +27 -0
  80. package/dist/astro/image.js +29 -0
  81. package/dist/astro/index.d.ts +43 -0
  82. package/dist/astro/index.js +111 -0
  83. package/dist/astro/locale-paths.d.ts +30 -0
  84. package/dist/astro/locale-paths.js +38 -0
  85. package/dist/astro/middleware.d.ts +1 -0
  86. package/dist/astro/middleware.js +83 -0
  87. package/dist/astro/preview-document.d.ts +1 -0
  88. package/dist/astro/preview-document.js +18 -0
  89. package/dist/astro/preview.d.ts +10 -0
  90. package/dist/astro/preview.js +72 -0
  91. package/dist/astro/richtext.d.ts +15 -0
  92. package/dist/astro/richtext.js +78 -0
  93. package/dist/astro/routes/admin.d.ts +3 -0
  94. package/dist/astro/routes/admin.js +41 -0
  95. package/dist/astro/routes/page.astro +17 -0
  96. package/dist/astro/routes/preview.astro +11 -0
  97. package/dist/astro/runtime.d.ts +32 -0
  98. package/dist/astro/runtime.js +46 -0
  99. package/dist/astro/scope.d.ts +45 -0
  100. package/dist/astro/scope.js +103 -0
  101. package/dist/astro/server.d.ts +14 -0
  102. package/dist/astro/server.js +45 -0
  103. package/dist/astro/shell.d.ts +67 -0
  104. package/dist/astro/shell.js +63 -0
  105. package/dist/astro/style.d.ts +1 -0
  106. package/dist/astro/style.js +11 -0
  107. package/dist/astro/views-check.d.ts +3 -0
  108. package/dist/astro/views-check.js +19 -0
  109. package/dist/astro/zone.d.ts +39 -0
  110. package/dist/astro/zone.js +33 -0
  111. package/dist/bind.d.ts +19 -0
  112. package/dist/bind.js +34 -0
  113. package/dist/bridge/bridge.d.ts +9 -0
  114. package/dist/bridge/bridge.js +107 -0
  115. package/dist/bridge/entry.d.ts +1 -0
  116. package/dist/bridge/entry.js +5 -0
  117. package/dist/bridge/preview-pill.d.ts +18 -0
  118. package/dist/bridge/preview-pill.js +115 -0
  119. package/dist/cli/import.d.ts +3 -0
  120. package/dist/cli/import.js +10 -1
  121. package/dist/cli/load-collections.js +4 -3
  122. package/dist/cli/load-config.d.ts +4 -0
  123. package/dist/cli/load-config.js +26 -0
  124. package/dist/cli/main.d.ts +5 -1
  125. package/dist/cli/main.js +24 -6
  126. package/dist/cli/migrate.d.ts +13 -0
  127. package/dist/cli/migrate.js +30 -2
  128. package/dist/cli/rebuild.d.ts +23 -0
  129. package/dist/cli/rebuild.js +22 -0
  130. package/dist/cli/space-io.d.ts +15 -1
  131. package/dist/cli/space-io.js +14 -0
  132. package/dist/client.js +5 -0
  133. package/dist/config.d.ts +28 -20
  134. package/dist/config.js +71 -75
  135. package/dist/content/content-api.d.ts +1 -1
  136. package/dist/content/node.d.ts +2 -3
  137. package/dist/content/node.js +2 -1
  138. package/dist/crash-message.d.ts +1 -0
  139. package/dist/crash-message.js +8 -0
  140. package/dist/css.d.ts +4 -0
  141. package/dist/css.js +5 -2
  142. package/dist/custom/CustomSection.d.ts +1 -1
  143. package/dist/custom/CustomSection.js +4 -8
  144. package/dist/custom/css.js +2 -2
  145. package/dist/custom/index.d.ts +6 -10
  146. package/dist/custom/index.js +1 -2
  147. package/dist/custom/layout.d.ts +5 -0
  148. package/dist/custom/layout.js +14 -0
  149. package/dist/document-stamp.d.ts +7 -0
  150. package/dist/document-stamp.js +48 -0
  151. package/dist/entry-page.d.ts +3 -18
  152. package/dist/env.js +1 -1
  153. package/dist/global-id.js +2 -2
  154. package/dist/global.d.ts +5 -2
  155. package/dist/global.js +8 -5
  156. package/dist/image/next-config.d.ts +3 -0
  157. package/dist/image/next-config.js +12 -7
  158. package/dist/image/remote-pattern.d.ts +1 -0
  159. package/dist/image/remote-pattern.js +5 -0
  160. package/dist/index.d.ts +15 -17
  161. package/dist/index.js +10 -19
  162. package/dist/marks.d.ts +7 -0
  163. package/dist/marks.js +7 -0
  164. package/dist/next/admin-route.d.ts +7 -0
  165. package/dist/next/admin-route.js +28 -0
  166. package/dist/next/bridge.d.ts +1 -0
  167. package/dist/next/bridge.js +26 -0
  168. package/dist/next/canvas-boundary.d.ts +1 -1
  169. package/dist/next/canvas-slot.d.ts +1 -0
  170. package/dist/next/canvas-slot.js +4 -3
  171. package/dist/next/index.d.ts +2 -0
  172. package/dist/next/index.js +1 -0
  173. package/dist/next/page-renderer.d.ts +5 -1
  174. package/dist/next/page-renderer.js +19 -33
  175. package/dist/next/preview-pill-define.d.ts +1 -0
  176. package/dist/next/preview-pill-define.js +10 -0
  177. package/dist/next/preview-pill.d.ts +0 -3
  178. package/dist/next/preview-pill.js +7 -50
  179. package/dist/next/queries.d.ts +3 -28
  180. package/dist/next/queries.js +3 -90
  181. package/dist/next/reads.d.ts +2 -0
  182. package/dist/next/reads.js +18 -0
  183. package/dist/page-context.d.ts +89 -0
  184. package/dist/page-context.js +8 -0
  185. package/dist/pairing.d.ts +2 -22
  186. package/dist/project.d.ts +85 -0
  187. package/dist/project.js +228 -0
  188. package/dist/published.d.ts +115 -0
  189. package/dist/published.js +136 -0
  190. package/dist/react.d.ts +61 -0
  191. package/dist/react.js +17 -0
  192. package/dist/read-shapes.d.ts +1 -1
  193. package/dist/reads.d.ts +80 -0
  194. package/dist/reads.js +142 -0
  195. package/dist/refs.d.ts +1 -1
  196. package/dist/registration.d.ts +8 -0
  197. package/dist/registration.js +17 -0
  198. package/dist/render.d.ts +5 -83
  199. package/dist/render.js +5 -10
  200. package/dist/resolve.d.ts +1 -1
  201. package/dist/richtext/index.d.ts +6 -2
  202. package/dist/richtext/index.js +3 -2
  203. package/dist/richtext-href.d.ts +4 -0
  204. package/dist/richtext-href.js +17 -0
  205. package/dist/richtext-render.js +1 -14
  206. package/dist/schema-version.d.ts +1 -1
  207. package/dist/schema-version.js +1 -1
  208. package/dist/section-crash.d.ts +2 -2
  209. package/dist/section-crash.js +2 -7
  210. package/dist/section-wrapper.d.ts +3 -10
  211. package/dist/section-wrapper.js +3 -2
  212. package/dist/site.d.ts +40 -14
  213. package/dist/site.js +199 -284
  214. package/dist/smd-key.d.ts +9 -0
  215. package/dist/smd-key.js +1 -0
  216. package/dist/sql-space.d.ts +5 -0
  217. package/dist/sql-space.js +11 -5
  218. package/dist/sql.d.ts +2 -0
  219. package/dist/sql.js +127 -1
  220. package/dist/store.d.ts +1 -1
  221. package/dist/supabase-published-store.d.ts +45 -0
  222. package/dist/supabase-published-store.js +247 -0
  223. package/dist/supabase-store.d.ts +1 -1
  224. package/dist/toolset.d.ts +2 -4
  225. package/dist/toolset.js +6 -4
  226. package/dist/version.d.ts +1 -1
  227. package/dist/version.js +1 -1
  228. package/dist/view-props.d.ts +40 -0
  229. package/dist/view-props.js +1 -0
  230. package/dist/views.d.ts +36 -0
  231. package/dist/views.js +52 -0
  232. package/dist/zones.d.ts +7 -7
  233. package/dist/zones.js +7 -6
  234. package/migrations/20261007120000_smoodly_v8.sql +154 -0
  235. package/migrations/20261007200000_smoodly_v9.sql +30 -0
  236. package/migrations/20261008120000_smoodly_v10.sql +47 -0
  237. package/package.json +46 -19
  238. package/dist/admin/next/bridge.d.ts +0 -2
  239. package/dist/admin/next/bridge.js +0 -129
  240. package/dist/admin/next/create-admin.d.ts +0 -19
  241. package/dist/admin/next/create-admin.js +0 -44
  242. package/dist/admin/next/index.d.ts +0 -8
  243. package/dist/admin/next/index.js +0 -8
  244. package/dist/admin/next/op-handler.d.ts +0 -18
  245. package/dist/admin/next/op-handler.js +0 -31
package/dist/config.js CHANGED
@@ -5,51 +5,49 @@ import { DEFAULT_ADMIN_PATH, assertAdminPath, isAdminSlug } from "./admin-path.j
5
5
  import { resolveStyleValues, styleDescriptors } from "./styles.js";
6
6
  import { resolveCustom } from "./custom/index.js";
7
7
  import { isValidZone } from "./time-zone.js";
8
+ import { schemaOnly } from "./registration.js";
8
9
  /** `global`: the placement node type and an admin route; `asset`: the refs
9
10
  * target of every media edge; `media`: the admin's Media screen route. */
10
11
  export const RESERVED_NAMES = ["global", "asset", "media"];
11
12
  const isReserved = (name) => RESERVED_NAMES.includes(name);
12
- function assertUniqueNames(config) {
13
- const seen = new Set();
14
- const sectionNameList = (config.registry.sections ?? []).map((s) => s.schema.name);
15
- const sectionNames = new Set(sectionNameList);
16
- const all = [
17
- ...sectionNameList,
18
- ...(config.registry.elements ?? []).map((e) => e.schema.name),
19
- ...(config.registry.collections ?? []).map((c) => c.name),
20
- ...(config.registry.pages ?? []).filter((p) => p.schema.kind !== "entryPage").map((p) => p.schema.name),
21
- ];
22
- for (const name of all) {
23
- if (isReserved(name))
24
- throw new Error(`smoodly: "${name}" is a reserved name.`);
25
- if (seen.has(name))
26
- throw new Error(`smoodly: duplicate registered name "${name}".`);
27
- seen.add(name);
28
- }
29
- // Global items get their own pass, after every other kind is settled: a
30
- // global item's name must be unique among global items, and must not
31
- // collide with any collection/element/page/section name above — EXCEPT a
32
- // visual item may reuse the name of the registered section it renders
33
- // (spec 2026-09-07 §2's example pairs a "footer" section with a "footer"
34
- // item). That exemption is narrow: it only excuses a match against the
35
- // item's own, actually-registered section — never against a collection,
36
- // element, or page that happens to share the string, and never against a
37
- // second global item of the same name.
38
- const globalSeen = new Set();
39
- for (const item of config.registry.globals ?? []) {
40
- const ownSection = item.name === item.section && sectionNames.has(item.name);
41
- if (isReserved(item.name))
42
- throw new Error(`smoodly: "${item.name}" is a reserved name.`);
43
- if (globalSeen.has(item.name) || (seen.has(item.name) && !ownSection)) {
44
- throw new Error(`smoodly: duplicate registered name "${item.name}".`);
13
+ function normalizeRegistry(config) {
14
+ const collections = config.registry.collections ?? [];
15
+ for (const c of collections) {
16
+ if (c.kind === "global") {
17
+ throw new Error(`smoodly: "${c.name}" is a global item — list it under registry.globals, not registry.collections.`);
45
18
  }
46
- globalSeen.add(item.name);
47
19
  }
20
+ return {
21
+ sections: (config.registry.sections ?? []).map((s) => schemaOnly(s)),
22
+ elements: (config.registry.elements ?? []).map((e) => schemaOnly(e)),
23
+ collections,
24
+ globals: config.registry.globals ?? [],
25
+ // Page types only: an entry page is a view, registered with defineViews, and schemaOnly refuses it here.
26
+ pages: (config.registry.pages ?? []).map((p) => schemaOnly(p)),
27
+ };
28
+ }
29
+ /** Names are unique per kind (plan 2026-10-02 registration split): a section and a collection may share one. */
30
+ function assertUniqueNames(registry) {
31
+ const check = (names, what) => {
32
+ const seen = new Set();
33
+ for (const name of names) {
34
+ if (isReserved(name))
35
+ throw new Error(`smoodly: "${name}" is a reserved name.`);
36
+ if (seen.has(name))
37
+ throw new Error(`smoodly: duplicate ${what} name "${name}".`);
38
+ seen.add(name);
39
+ }
40
+ };
41
+ // One component map renders sections and elements, so they share a namespace.
42
+ check([...registry.sections, ...registry.elements].map((s) => s.name), "section or element");
43
+ // A global item is stored as an entry of a collection named after it, so the two share one.
44
+ check([...registry.collections, ...registry.globals].map((c) => c.name), "collection or global item");
45
+ check(registry.pages.map((p) => p.name), "page type");
48
46
  }
49
47
  /** A visual item renders through a registered section. */
50
- function assertGlobalSections(config) {
51
- const sections = new Set((config.registry.sections ?? []).map((s) => s.schema.name));
52
- for (const item of config.registry.globals ?? []) {
48
+ function assertGlobalSections(registry) {
49
+ const sections = new Set(registry.sections.map((s) => s.name));
50
+ for (const item of registry.globals) {
53
51
  if (item.section !== undefined && !sections.has(item.section)) {
54
52
  throw new Error(`smoodly: global item "${item.name}" renders section "${item.section}", which is not registered.`);
55
53
  }
@@ -62,39 +60,35 @@ function assertGlobalSections(config) {
62
60
  * path, which is the collection's index page — and nothing may sit
63
61
  * inside a collection's address. An address starting at the admin's
64
62
  * segment is refused outright (spec 2026-09-17 slug guard §5). */
65
- function assertRootSegments(config, adminPath) {
66
- const { default: def } = config.locales;
63
+ function assertRootSegments(registry, locales, adminPath) {
64
+ const { default: def } = locales;
67
65
  // Fixed slugs and collection paths both fill an omitted locale from the
68
66
  // default one — where the materializer puts the record and the entry
69
67
  // derivation puts the entries — so the checks see every clash at boot.
70
- const pageSegments = (v) => fixedPageSegments(v, config.locales);
71
- const pages = (config.registry.pages ?? []).filter((p) => p.schema.kind === "page" && p.schema.slug !== undefined);
68
+ const pageSegments = (v) => fixedPageSegments(v, locales);
69
+ const pages = registry.pages.filter((p) => p.slug !== undefined);
72
70
  const owners = new Map(); // "<locale> <segment>" → page name
73
71
  for (const p of pages) {
74
- const slug = p.schema.slug;
72
+ const slug = p.slug;
75
73
  if (typeof slug !== "string" && slug[def] === undefined) {
76
- throw new Error(`smoodly: page "${p.schema.name}" declares a per-locale slug without the default locale "${def}".`);
74
+ throw new Error(`smoodly: page "${p.name}" declares a per-locale slug without the default locale "${def}".`);
77
75
  }
78
76
  for (const [locale, segment] of Object.entries(pageSegments(slug))) {
79
- assertAddress(segment, `page "${p.schema.name}" fixed slug`);
77
+ assertAddress(segment, `page "${p.name}" fixed slug`);
80
78
  if (isAdminSlug(segment.split("/")[0], adminPath)) {
81
- throw new Error(`smoodly: page "${p.schema.name}" has the fixed slug "${segment}", where the admin lives — change its slug or admin.path.`);
79
+ throw new Error(`smoodly: page "${p.name}" has the fixed slug "${segment}", where the admin lives — change its slug or admin.path.`);
82
80
  }
83
81
  const owner = owners.get(`${locale} ${segment}`);
84
82
  if (owner) {
85
- throw new Error(`smoodly: page registrations "${owner}" and "${p.schema.name}" both claim the fixed slug "${segment}" (${locale}).`);
83
+ throw new Error(`smoodly: page registrations "${owner}" and "${p.name}" both claim the fixed slug "${segment}" (${locale}).`);
86
84
  }
87
- owners.set(`${locale} ${segment}`, p.schema.name);
85
+ owners.set(`${locale} ${segment}`, p.name);
88
86
  }
89
87
  }
90
- const entryPages = new Set((config.registry.pages ?? []).filter((p) => p.schema.kind === "entryPage").map((p) => p.schema.collection));
91
88
  const collectionOwners = new Map();
92
- for (const c of config.registry.collections ?? []) {
89
+ for (const c of registry.collections) {
93
90
  if (c.path === undefined)
94
91
  continue;
95
- if (!entryPages.has(c.name)) {
96
- throw new Error(`smoodly: collection "${c.name}" declares a path but no entry page — register smoodly.entryPage(${c.name}, View).`);
97
- }
98
92
  if (typeof c.path !== "string" && c.path[def] === undefined) {
99
93
  throw new Error(`smoodly: collection "${c.name}" declares a per-locale path without the default locale "${def}".`);
100
94
  }
@@ -115,8 +109,8 @@ function assertRootSegments(config, adminPath) {
115
109
  const pageName = owners.get(`${locale} ${segment}`);
116
110
  if (!pageName)
117
111
  continue;
118
- const page = pages.find((p) => p.schema.name === pageName);
119
- const everywhere = Object.entries(segments).every(([l, s]) => pageSegments(page.schema.slug)[l] === s);
112
+ const page = pages.find((p) => p.name === pageName);
113
+ const everywhere = Object.entries(segments).every(([l, s]) => pageSegments(page.slug)[l] === s);
120
114
  if (!everywhere) {
121
115
  throw new Error(`smoodly: collection "${c.name}" and page registration "${pageName}" both claim the address "${segment}" (${locale}).`);
122
116
  }
@@ -125,15 +119,15 @@ function assertRootSegments(config, adminPath) {
125
119
  // A collection's entries live below its address: nothing else may sit there (spec 2026-09-29 nested paths §2).
126
120
  const claims = [];
127
121
  for (const p of pages)
128
- for (const [locale, address] of Object.entries(pageSegments(p.schema.slug)))
129
- claims.push({ what: `page "${p.schema.name}"`, locale, address });
130
- for (const c of config.registry.collections ?? []) {
122
+ for (const [locale, address] of Object.entries(pageSegments(p.slug)))
123
+ claims.push({ what: `page "${p.name}"`, locale, address });
124
+ for (const c of registry.collections) {
131
125
  if (c.path === undefined)
132
126
  continue;
133
127
  for (const [locale, address] of Object.entries(pageSegments(c.path)))
134
128
  claims.push({ what: `collection "${c.name}"`, locale, address });
135
129
  }
136
- for (const c of config.registry.collections ?? []) {
130
+ for (const c of registry.collections) {
137
131
  if (c.path === undefined)
138
132
  continue;
139
133
  for (const [locale, base] of Object.entries(pageSegments(c.path))) {
@@ -184,18 +178,18 @@ function toolsetElementNames(descriptors, out) {
184
178
  }
185
179
  }
186
180
  /** A field can never offer an element the admin cannot serialize (spec 2026-09-08 block nodes §1). */
187
- function assertToolsetElements(config) {
188
- const registered = new Set((config.registry.elements ?? []).map((e) => e.schema.name));
181
+ function assertToolsetElements(registry, custom) {
182
+ const registered = new Set(registry.elements.map((e) => e.name));
189
183
  const wanted = new Set();
190
- for (const s of config.registry.sections ?? [])
191
- toolsetElementNames(s.schema.fields, wanted);
192
- for (const e of config.registry.elements ?? [])
193
- toolsetElementNames(e.schema.fields, wanted);
194
- for (const c of config.registry.collections ?? [])
184
+ for (const s of registry.sections)
185
+ toolsetElementNames(s.fields, wanted);
186
+ for (const e of registry.elements)
187
+ toolsetElementNames(e.fields, wanted);
188
+ for (const c of registry.collections)
195
189
  toolsetElementNames(c.fields, wanted);
196
- for (const s of config.registry.globals ?? [])
190
+ for (const s of registry.globals)
197
191
  toolsetElementNames(s.fields, wanted);
198
- for (const n of config.custom?.toolset?.elements ?? [])
192
+ for (const n of custom?.toolset?.elements ?? [])
199
193
  wanted.add(n);
200
194
  for (const name of wanted) {
201
195
  if (!registered.has(name))
@@ -205,12 +199,13 @@ function assertToolsetElements(config) {
205
199
  export function defineConfig(config) {
206
200
  assertLocales(config);
207
201
  assertTimeZone(config);
208
- assertUniqueNames(config);
209
- assertGlobalSections(config);
210
- assertToolsetElements(config);
202
+ const registry = normalizeRegistry(config);
203
+ assertUniqueNames(registry);
204
+ assertGlobalSections(registry);
205
+ assertToolsetElements(registry, config.custom);
211
206
  const adminPath = config.admin?.path ?? DEFAULT_ADMIN_PATH;
212
207
  assertAdminPath(adminPath);
213
- assertRootSegments(config, adminPath);
208
+ assertRootSegments(registry, config.locales, adminPath);
214
209
  const depth = config.pages?.tree?.depth ?? 1;
215
210
  if (!Number.isInteger(depth) || depth < 1)
216
211
  throw new Error("smoodly: pages.tree.depth must be a positive integer.");
@@ -218,7 +213,7 @@ export function defineConfig(config) {
218
213
  // collection's mount: that node is a folder or an index page, never "/".
219
214
  const homePageSlug = config.homePageSlug ?? "home";
220
215
  assertSegment(homePageSlug, "homePageSlug");
221
- for (const c of config.registry.collections ?? []) {
216
+ for (const c of registry.collections) {
222
217
  if (c.path === undefined)
223
218
  continue;
224
219
  const segment = typeof c.path === "string" ? c.path : c.path[config.locales.default];
@@ -229,7 +224,7 @@ export function defineConfig(config) {
229
224
  // A code-owned record keeps its address's last part as its slug, and the
230
225
  // root record slugged homePageSlug IS the home page: a nested address
231
226
  // ending in it would serve "/" (spec 2026-09-29 nested paths §3).
232
- for (const a of codeAddresses({ locales: config.locales, pages: config.registry.pages ?? [], collections: config.registry.collections ?? [] })) {
227
+ for (const a of codeAddresses({ locales: config.locales, pages: registry.pages, collections: registry.collections })) {
233
228
  for (const [locale, address] of Object.entries(a.segments)) {
234
229
  if (address.includes("/") && lastPart(address) === homePageSlug) {
235
230
  throw new Error(`smoodly: the address "${address}" (${locale}) ends in homePageSlug "${homePageSlug}" — its record would be the home page; end it in another segment.`);
@@ -238,7 +233,7 @@ export function defineConfig(config) {
238
233
  }
239
234
  const styles = resolveStyleValues(config.styles);
240
235
  const custom = resolveCustom(config.custom);
241
- const byName = new Map((config.registry.sections ?? []).map((s) => [s.schema.name, s.schema]));
236
+ const byName = new Map(registry.sections.map((s) => [s.name, s]));
242
237
  const media = {
243
238
  maxSize: config.media?.maxSize ?? DEFAULT_MEDIA.maxSize,
244
239
  accept: config.media?.accept ? [...config.media.accept] : [...DEFAULT_MEDIA.accept],
@@ -246,9 +241,10 @@ export function defineConfig(config) {
246
241
  if (!Number.isInteger(media.maxSize) || media.maxSize <= 0) {
247
242
  throw new Error("smoodly: media.maxSize must be a positive integer of bytes.");
248
243
  }
249
- const addresses = addressMap(codeAddresses({ locales: config.locales, pages: config.registry.pages ?? [], collections: config.registry.collections ?? [] }));
244
+ const addresses = addressMap(codeAddresses({ locales: config.locales, pages: registry.pages, collections: registry.collections }));
250
245
  return {
251
246
  ...config,
247
+ registry,
252
248
  homePageSlug,
253
249
  pageDepth: depth,
254
250
  media,
@@ -3,7 +3,7 @@ import type { EntryCreateInput, OpError, PageCreateInput } from "../admin/ops.ts
3
3
  import type { FieldError } from "../admin/validate.ts";
4
4
  import type { EntryRecord } from "../entry-store.ts";
5
5
  import type { PageRecord } from "../store.ts";
6
- import type { PageTree } from "../render.tsx";
6
+ import type { PageTree } from "../page-context.ts";
7
7
  import type { ImageValue } from "../media.ts";
8
8
  import type { MemberStore } from "../member-store.ts";
9
9
  export type UploadMeta = {
@@ -1,7 +1,6 @@
1
1
  import type { BuilderMap, SectionSchema, StoredValues } from "../schema.ts";
2
- import type { PairedComponent } from "../pairing.tsx";
3
- import type { TreeNode } from "../render.tsx";
4
- export declare function node<F extends BuilderMap, St extends BuilderMap>(section: PairedComponent<SectionSchema<F, St>, F, St>, fields: Partial<StoredValues<F>>, options?: {
2
+ import type { TreeNode } from "../page-context.ts";
3
+ export declare function node<F extends BuilderMap, St extends BuilderMap>(section: SectionSchema<F, St>, fields: Partial<StoredValues<F>>, options?: {
5
4
  settings?: Partial<StoredValues<St>>;
6
5
  id?: string;
7
6
  }): TreeNode;
@@ -1,6 +1,7 @@
1
1
  import { newNodeId } from "../admin/tree-ops.js";
2
+ import { schemaOnly } from "../registration.js";
2
3
  export function node(section, fields, options = {}) {
3
- const type = section.schema.name;
4
+ const type = schemaOnly(section).name;
4
5
  return {
5
6
  id: options.id ?? newNodeId(type),
6
7
  type,
@@ -0,0 +1 @@
1
+ export declare function crashMessage(error: unknown): string;
@@ -0,0 +1,8 @@
1
+ // Why a section crashed, for its placeholder (spec 2026-09-27 section guard §3): the message, or the digest
2
+ // when a production build stripped the message. Shared by the React placeholder and Astro's guard.
3
+ export function crashMessage(error) {
4
+ const digest = typeof error === "object" && error !== null ? error.digest : undefined;
5
+ if (typeof digest === "string")
6
+ return `the server log has the error (digest ${digest})`;
7
+ return error instanceof Error ? error.message : String(error);
8
+ }
package/dist/css.d.ts CHANGED
@@ -3,5 +3,9 @@ export declare const CSS_MAX = 4000;
3
3
  export declare function cssProblem(css: string): string | null;
4
4
  /** The snippet when it is a clean string, else "". */
5
5
  export declare function safeCss(css: unknown): string;
6
+ /** A node id as the inside of a CSS attribute-selector string. "<" as its CSS escape: the rule is written inside
7
+ * a <style> element, and an id is whatever a request stored, so it must never be able to close that element
8
+ * (Astro writes the text raw; React escapes it). */
9
+ export declare const escapeNodeId: (id: string) => string;
6
10
  /** Nested under the node's selector — every rule inside reaches the section only. */
7
11
  export declare function scopedCss(nodeId: string, css: string): string;
package/dist/css.js CHANGED
@@ -50,8 +50,11 @@ export function cssProblem(css) {
50
50
  export function safeCss(css) {
51
51
  return typeof css === "string" && css !== "" && cssProblem(css) === null ? css : "";
52
52
  }
53
+ /** A node id as the inside of a CSS attribute-selector string. "<" as its CSS escape: the rule is written inside
54
+ * a <style> element, and an id is whatever a request stored, so it must never be able to close that element
55
+ * (Astro writes the text raw; React escapes it). */
56
+ export const escapeNodeId = (id) => id.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/</g, "\\3c ");
53
57
  /** Nested under the node's selector — every rule inside reaches the section only. */
54
58
  export function scopedCss(nodeId, css) {
55
- const id = nodeId.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
56
- return `[data-smd-node="${id}"]{${css}}`;
59
+ return `[data-smd-node="${escapeNodeId(nodeId)}"]{${css}}`;
57
60
  }
@@ -1,5 +1,5 @@
1
1
  import type { ComponentType, ReactElement } from "react";
2
- import { SMD_KEY, type SmdInternal } from "../section-wrapper.tsx";
2
+ import { SMD_KEY, type SmdInternal } from "../smd-key.ts";
3
3
  import type { StyleProps } from "../styles.ts";
4
4
  import { type RichTextElement } from "../richtext-render.tsx";
5
5
  export type CustomOptions = {
@@ -1,19 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { Section, SMD_KEY } from "../section-wrapper.js";
2
+ import { Section } from "../section-wrapper.js";
3
+ import { SMD_KEY } from "../smd-key.js";
3
4
  import { RichText } from "../richtext-render.js";
4
5
  import { FIELD_ATTR, ITEM_ATTR } from "../marks.js";
5
6
  import { CUSTOM_NODE_TYPE, DEFAULT_BREAKPOINT } from "./schema.js";
6
- import { customCss } from "./css.js";
7
+ import { customLayout } from "./layout.js";
7
8
  import { RENDER_VIEW } from "../section-guard.js";
8
- const isRow = (r) => typeof r === "object" && r !== null && Array.isArray(r.columns);
9
9
  export function createCustomSection({ RichText: Bound, breakpoint = DEFAULT_BREAKPOINT, elements = [] }) {
10
10
  const Body = Bound ?? (({ doc }) => _jsx(RichText, { doc: doc, elements: elements }));
11
11
  function CustomSection({ rows, styles }) {
12
- const list = Array.isArray(rows) ? rows.filter(isRow) : [];
13
- const smd = styles?.[SMD_KEY];
14
- const withLayout = smd?.node && list.length > 0
15
- ? { ...styles, [SMD_KEY]: { ...smd, layoutCss: customCss(smd.node, list, breakpoint) } }
16
- : styles;
12
+ const { list, styles: withLayout } = customLayout(rows, styles ?? {}, breakpoint);
17
13
  return (_jsx(Section, { styles: withLayout, "data-custom": "", children: list.map((row, i) => (_jsx("div", { className: row.reverse ? "smd-row smd-row--reverse" : "smd-row", "data-row": i, [ITEM_ATTR]: `rows.${i}`, children: row.columns.map((doc, j) => (_jsx("div", { className: "smd-col", [FIELD_ATTR]: `columns.${j}`, children: _jsx(Body, { doc: doc }) }, j))) }, i))) }));
18
14
  }
19
15
  CustomSection.schema = { name: CUSTOM_NODE_TYPE, title: "Custom section" };
@@ -4,12 +4,12 @@
4
4
  // reaches it. One rule per row by data-row (nth-child would count the
5
5
  // <style> children), one breakpoint that stacks every row.
6
6
  import { ALIGN_VALUES, GAP_VALUES, SPAN_TOTAL } from "./schema.js";
7
+ import { escapeNodeId } from "../css.js";
7
8
  import { renormalize } from "./rows.js";
8
9
  const LENGTH_RE = /^\d+(?:\.\d+)?(?:px|rem|em)$/;
9
10
  export const isCssLength = (s) => LENGTH_RE.test(s);
10
- const escapeId = (id) => id.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
11
11
  export function customCss(nodeId, rows, breakpoint) {
12
- const s = `[data-smd-node="${escapeId(nodeId)}"]`;
12
+ const s = `[data-smd-node="${escapeNodeId(nodeId)}"]`;
13
13
  let css = `${s} .smd-row{display:grid;max-width:var(--smd-width,var(--container,none));margin-inline:auto;padding-inline:var(--smd-gutter,1rem);box-sizing:border-box}` +
14
14
  `${s} .smd-row+.smd-row{margin-top:2rem}`;
15
15
  rows.forEach((row, i) => {
@@ -1,25 +1,21 @@
1
- import type { ComponentType } from "react";
2
1
  import type { Toolset } from "../toolset.ts";
3
2
  import { customSchema } from "./schema.ts";
4
3
  export * from "./schema.ts";
5
4
  export * from "./rows.ts";
6
5
  export * from "./css.ts";
7
- export { createCustomSection } from "./CustomSection.tsx";
8
- export type { CustomOptions, CustomSectionProps, CustomSectionComponent } from "./CustomSection.tsx";
9
- /** `config.custom` / `site.custom` as written (spec 2026-09-14 §6). */
6
+ /** `config.custom` / `site.custom` as written (spec 2026-09-14 §6). The bound RichText is a view: it goes to defineViews and reaches the renderer as an option. */
10
7
  export type CustomConfigInput = {
11
8
  breakpoint?: string;
12
9
  toolset?: Toolset;
13
- RichText?: ComponentType<{
14
- doc: unknown;
15
- }>;
16
10
  };
11
+ /** The bound RichText as the config sees it: a framework's component over a document (React: `ComponentType<{ doc }>`). */
12
+ export type RichTextView = (props: {
13
+ doc: unknown;
14
+ }) => unknown;
17
15
  export type ResolvedCustom = {
18
16
  breakpoint: string;
19
17
  toolset: Toolset;
20
- RichText?: ComponentType<{
21
- doc: unknown;
22
- }>;
18
+ RichText?: RichTextView;
23
19
  };
24
20
  /** Every section map the walks build gets the custom schema too (spec 2026-09-14 §6), so a column's images resolve and count as usage. */
25
21
  export declare function withCustomSchema<T>(sections: readonly T[], toolset: Toolset): (T | ReturnType<typeof customSchema>)[];
@@ -3,7 +3,6 @@ import { isCssLength } from "./css.js";
3
3
  export * from "./schema.js";
4
4
  export * from "./rows.js";
5
5
  export * from "./css.js";
6
- export { createCustomSection } from "./CustomSection.js";
7
6
  /** Every section map the walks build gets the custom schema too (spec 2026-09-14 §6), so a column's images resolve and count as usage. */
8
7
  export function withCustomSchema(sections, toolset) {
9
8
  return [...sections, customSchema(toolset)];
@@ -12,5 +11,5 @@ export function resolveCustom(input) {
12
11
  const breakpoint = input?.breakpoint ?? DEFAULT_BREAKPOINT;
13
12
  if (!isCssLength(breakpoint))
14
13
  throw new Error(`smoodly: custom.breakpoint "${breakpoint}" must be a length like "48rem" or "768px".`);
15
- return { breakpoint, toolset: customToolset(input?.toolset), ...(input?.RichText ? { RichText: input.RichText } : {}) };
14
+ return { breakpoint, toolset: customToolset(input?.toolset) };
16
15
  }
@@ -0,0 +1,5 @@
1
+ import type { CustomRow } from "./schema.ts";
2
+ export declare function customLayout(rows: unknown, styles: Record<string, unknown>, breakpoint: string): {
3
+ list: CustomRow[];
4
+ styles: Record<string, unknown>;
5
+ };
@@ -0,0 +1,14 @@
1
+ // The custom section's rows and layout css, decided once for both renderers (CustomSection.tsx and
2
+ // astro/custom.ts): the rows a stored value yields (fail-soft: a malformed value is no rows), and the layout
3
+ // CSS — Smoodly's own, computed from the stored numbers — riding the styles' reserved key as `layoutCss`,
4
+ // which the wrapper emits before the editor's own css so the box still wins. No framework import.
5
+ import { SMD_KEY } from "../smd-key.js";
6
+ import { customCss } from "./css.js";
7
+ const isRow = (r) => typeof r === "object" && r !== null && Array.isArray(r.columns);
8
+ export function customLayout(rows, styles, breakpoint) {
9
+ const list = Array.isArray(rows) ? rows.filter(isRow) : [];
10
+ const smd = styles[SMD_KEY];
11
+ if (!smd?.node || list.length === 0)
12
+ return { list, styles };
13
+ return { list, styles: { ...styles, [SMD_KEY]: { ...smd, layoutCss: customCss(smd.node, list, breakpoint) } } };
14
+ }
@@ -0,0 +1,7 @@
1
+ import type { SiteOptions } from "./site.ts";
2
+ /** Bumped by hand when the assembler's output changes shape; the upgrading guide names each bump. */
3
+ export declare const DOCUMENT_FORMAT = 1;
4
+ /** The parts of the registry that reach a document: what `cacheKey` hashes, minus the presentation. */
5
+ export declare function documentModel(options: SiteOptions): unknown;
6
+ /** `<format>:<model hash>` — what a delivery row's `model` column holds. */
7
+ export declare function documentStamp(options: SiteOptions): string;
@@ -0,0 +1,48 @@
1
+ // The stamp a delivery row carries (DESIGN §3, the document stamp, 2026-10-08): which document format and
2
+ // which content model built it. Not the cache key: the package version is in that one, not here, because a
3
+ // release that leaves documents alone must leave them alone.
4
+ import { modelKey } from "./model-key.js";
5
+ import { schemaOf } from "./registration.js";
6
+ /** Bumped by hand when the assembler's output changes shape; the upgrading guide names each bump. */
7
+ export const DOCUMENT_FORMAT = 1;
8
+ /** Schema keys that reach the admin only: a change to one leaves every document as it was. */
9
+ const PRESENTATION = new Set(["title", "description", "sample", "icon", "preview", "group", "admin"]);
10
+ /** A field descriptor without the parts that reach the admin only (a hidden condition decides what the editor shows). */
11
+ function fieldModel(field) {
12
+ if (!field || typeof field !== "object")
13
+ return field;
14
+ const { hidden: _hidden, ...rest } = field;
15
+ return Object.fromEntries(Object.entries(rest).map(([k, v]) => [k, k === "fields" ? fieldsModel(v) : k === "of" ? fieldModel(v) : v]));
16
+ }
17
+ function fieldsModel(fields) {
18
+ if (!fields || typeof fields !== "object")
19
+ return fields;
20
+ return Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, fieldModel(v)]));
21
+ }
22
+ /** A schema without its presentation. */
23
+ function schemaModel(schema) {
24
+ if (!schema || typeof schema !== "object")
25
+ return schema;
26
+ return Object.fromEntries(Object.entries(schema)
27
+ .filter(([k]) => !PRESENTATION.has(k))
28
+ .map(([k, v]) => [k, k === "fields" || k === "settings" ? fieldsModel(v) : v]));
29
+ }
30
+ /** The parts of the registry that reach a document: what `cacheKey` hashes, minus the presentation. */
31
+ export function documentModel(options) {
32
+ return {
33
+ collections: options.collections.map(schemaModel),
34
+ // A global whole, its presentation dropped: its settings' defaults reach a visual item's document.
35
+ globals: (options.globals ?? []).map(schemaModel),
36
+ sections: options.sections.map((x) => schemaModel(schemaOf(x))),
37
+ elements: (options.elements ?? []).map((x) => schemaModel(schemaOf(x))),
38
+ pages: options.pages.map((x) => schemaModel(schemaOf(x))),
39
+ locales: options.locales,
40
+ homePageSlug: options.homePageSlug,
41
+ styles: options.styles,
42
+ custom: options.custom,
43
+ };
44
+ }
45
+ /** `<format>:<model hash>` — what a delivery row's `model` column holds. */
46
+ export function documentStamp(options) {
47
+ return `${DOCUMENT_FORMAT}:${modelKey(documentModel(options))}`;
48
+ }
@@ -1,23 +1,8 @@
1
1
  import type { ComponentType, ReactElement } from "react";
2
2
  import type { CollectionSchema } from "./collections.ts";
3
- import type { EntryItem } from "./read-shapes.ts";
4
- import type { Materialize } from "./schema.ts";
5
- export type EntryPageSchema = {
6
- kind: "entryPage";
7
- name: string;
8
- collection: string;
9
- };
10
- /** What an entry page renders: the entry as a read returns it — its path, locale and
11
- * locale alternates ride on it (spec 2026-09-29 content API contract §3). */
12
- export type EntryPageProps<F = Record<string, unknown>> = {
13
- entry: EntryItem<F>;
14
- };
3
+ import type { EntryFields, EntryPageProps, EntryPageSchema } from "./view-props.ts";
4
+ export type { EntryPageSchema, EntryPageProps };
15
5
  export type PairedEntryPage = ((props: EntryPageProps) => ReactElement) & {
16
6
  schema: EntryPageSchema;
17
7
  };
18
- /** The collection's fields as a read returns them. */
19
- type Materialized<C extends CollectionSchema<any>> = {
20
- [K in keyof NonNullable<C["__entry"]>]: Materialize<NonNullable<C["__entry"]>[K]>;
21
- };
22
- export declare function entryPage<C extends CollectionSchema<any>>(collection: C, View: ComponentType<EntryPageProps<Materialized<C>>>): PairedEntryPage;
23
- export {};
8
+ export declare function entryPage<C extends CollectionSchema<any>>(collection: C, View: ComponentType<EntryPageProps<EntryFields<C>>>): PairedEntryPage;
package/dist/env.js CHANGED
@@ -25,7 +25,7 @@ function jwtRole(v) {
25
25
  export function resolveSmoodlyEnv(env, need = "site") {
26
26
  const missing = (need === "admin" ? ADMIN_NEEDS : SITE_NEEDS).filter((name) => !env[name]);
27
27
  if (missing.length > 0)
28
- throw new Error(`smoodly: set ${missing.join(", ")} in .env.local, or in your host's environment variables (Deploying: ${DEPLOYING}).`);
28
+ throw new Error(`smoodly: set ${missing.join(", ")} in .env (Astro) or .env.local (Next.js), or in your host's environment variables (Deploying: ${DEPLOYING}).`);
29
29
  const secretKey = env.SMOODLY_SECRET_KEY;
30
30
  const publishableKey = env.SMOODLY_PUBLISHABLE_KEY ?? "";
31
31
  if (secretKey.startsWith("sb_publishable_") || jwtRole(secretKey) === "anon") {
package/dist/global-id.js CHANGED
@@ -10,8 +10,8 @@
10
10
  // No imports — Web Crypto (`globalThis.crypto.subtle`) exists in Node
11
11
  // >= 20 and in every browser, so this module carries no node builtin
12
12
  // into the package root's graph. It matters because every scaffolded
13
- // section does `import { f, smoodly } from "smoodly"`, and a "use client"
14
- // section's browser build would fail on `node:crypto`.
13
+ // component does `import { smoodly } from "smoodly/react"`, which re-exports
14
+ // the root, and a "use client" section's browser build would fail on `node:crypto`.
15
15
  const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
16
16
  /** The namespace's 16 raw bytes — RFC 4122 hashes the bytes, not the text. */
17
17
  function uuidBytes(uuid) {
package/dist/global.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { CollectionSchema } from "./collections.ts";
2
2
  import type { Descriptor } from "./fields.ts";
3
- import type { PairedComponent } from "./pairing.tsx";
4
3
  import type { BuilderMap, SectionSchema, Values } from "./schema.ts";
5
4
  import type { GlobalItem, VisualGlobalItem } from "./read-shapes.ts";
6
5
  /** The reserved tree node type of a placement. */
@@ -24,6 +23,9 @@ export type GlobalSchema<V = Record<string, unknown>, N extends string = string>
24
23
  /** The section's settings descriptors (empty for an object item). */
25
24
  settings: Record<string, Descriptor>;
26
25
  sample?: Record<string, unknown>;
26
+ /** Every page renders it (the header, the footer): the page's lookup brings it along, so the layout's read costs
27
+ * no call (decided 2026-10-08). Absent: placed in zones or read by one view, fetched only where it is read. */
28
+ layout?: true;
27
29
  /** phantom: what getGlobal returns */
28
30
  readonly __value?: V;
29
31
  };
@@ -42,13 +44,14 @@ export declare function globalLink(node: {
42
44
  type GlobalInput<N extends string = string> = {
43
45
  name: N;
44
46
  title: string;
47
+ layout?: boolean;
45
48
  };
46
49
  export declare function globalItem<F extends BuilderMap, const N extends string = string>(input: GlobalInput<N> & {
47
50
  fields: F;
48
51
  section?: undefined;
49
52
  }): GlobalSchema<GlobalItem<Values<F>>, N>;
50
53
  export declare function globalItem<F extends BuilderMap, St extends BuilderMap, const N extends string = string>(input: GlobalInput<N> & {
51
- section: PairedComponent<SectionSchema<F, St>, F, St>;
54
+ section: SectionSchema<F, St>;
52
55
  fields?: undefined;
53
56
  }): GlobalSchema<VisualGlobalItem<Values<F>, Values<St>>, N>;
54
57
  export {};
package/dist/global.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { schemaOnly } from "./registration.js";
1
2
  /** The reserved tree node type of a placement. */
2
3
  export const GLOBAL_NODE_TYPE = "global";
3
4
  /** The reserved keys a visual item's settings, styles steps and css live under in its row (spec 2026-09-14 §8). */
@@ -31,8 +32,9 @@ export function globalItem(input) {
31
32
  if ((input.fields === undefined) === (input.section === undefined)) {
32
33
  throw new Error(`smoodly: global item "${input.name}" must declare exactly one of \`section\` or \`fields\`.`);
33
34
  }
34
- const fields = input.section
35
- ? input.section.schema.fields
35
+ const section = input.section ? schemaOnly(input.section) : undefined;
36
+ const fields = section
37
+ ? section.fields
36
38
  : Object.fromEntries(Object.entries(input.fields).map(([k, b]) => [k, b.descriptor]));
37
39
  for (const key of RESERVED_KEYS) {
38
40
  if (Object.prototype.hasOwnProperty.call(fields, key)) {
@@ -44,8 +46,9 @@ export function globalItem(input) {
44
46
  name: input.name,
45
47
  title: input.title,
46
48
  fields,
47
- settings: input.section ? input.section.schema.settings : {},
48
- ...(input.section ? { section: input.section.schema.name } : {}),
49
- ...(input.section?.schema.sample ? { sample: input.section.schema.sample } : {}),
49
+ settings: section ? section.settings : {},
50
+ ...(section ? { section: section.name } : {}),
51
+ ...(section?.sample ? { sample: section.sample } : {}),
52
+ ...(input.layout ? { layout: true } : {}),
50
53
  };
51
54
  }
@@ -12,4 +12,7 @@ export type SmoodlyImagesConfig = {
12
12
  * Supabase stack answers on. A public hostname that happens to resolve
13
13
  * privately (a Docker alias) is not detected; set the flag by hand then. */
14
14
  export declare function isPrivateHost(hostname: string): boolean;
15
+ /** The Supabase host's public media path as a remote pattern. Defined here, not in remote-pattern.ts: this
16
+ * entry loads in plain Node and imports nothing; remote-pattern.ts re-exports it for the Astro side. */
17
+ export declare function mediaRemotePattern(url: string): RemotePattern;
15
18
  export declare function smoodlyImages(url?: string | undefined): SmoodlyImagesConfig;