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
@@ -0,0 +1,115 @@
1
+ import type { EntryOrder } from "./collections.ts";
2
+ import { type Where } from "./entry-store.ts";
3
+ import type { SiteEntry, SitePage } from "./site.ts";
4
+ /** A page's delivery row: a published page, or a folder present in the locale (document null, hasPage false). */
5
+ export type PublishedPageRow = {
6
+ id: string;
7
+ locale: string;
8
+ parentId: string | null;
9
+ sort: number | null;
10
+ /** The served address; null when the tree hides the node (no path, or a code address wins). */
11
+ path: string | null;
12
+ title: string;
13
+ hasPage: boolean;
14
+ /** The page fields with defaults, raw (no asset resolution): what a tree item and a link show. */
15
+ linkFields: Record<string, unknown>;
16
+ document: SitePage | null;
17
+ tags: string[];
18
+ /** The document stamp that built the row (document-stamp.ts); the column name predates the name. */
19
+ model: string;
20
+ stale: boolean;
21
+ publishedAt: number;
22
+ };
23
+ /** An entry's or a global's delivery row. A global's `document` is the value `getGlobal` returns; its collection is the item's name. */
24
+ export type PublishedEntryRow = {
25
+ id: string;
26
+ locale: string;
27
+ collection: string;
28
+ parentId: string | null;
29
+ sort: number | null;
30
+ slug: string | null;
31
+ path: string | null;
32
+ createdAt: number;
33
+ updatedAt: number;
34
+ /** The published snapshot, raw, slug included: what `where` and `order` read. */
35
+ fields: Record<string, unknown>;
36
+ document: SiteEntry | unknown;
37
+ tags: string[];
38
+ /** The document stamp that built the row (document-stamp.ts); the column name predates the name. */
39
+ model: string;
40
+ stale: boolean;
41
+ publishedAt: number;
42
+ };
43
+ /** `globals`: present when the lookup asked for global items by name — the locale's rows of those collections,
44
+ * as `entries` returns them (document, model, stale; the raw fields may be empty), so a page is one call. */
45
+ export type PublishedRoute = ({
46
+ kind: "page";
47
+ row: PublishedPageRow;
48
+ } | {
49
+ kind: "entry";
50
+ row: PublishedEntryRow;
51
+ }) & {
52
+ globals?: PublishedEntryRow[];
53
+ };
54
+ /** `model` and `stale` ride along so a tree read can rebuild what is behind before answering (spec 2026-10-07 §4). */
55
+ export type PublishedTreeRow = Pick<PublishedPageRow, "id" | "parentId" | "sort" | "path" | "title" | "hasPage" | "linkFields" | "model" | "stale">;
56
+ export type PublishedQuery = {
57
+ locale: string;
58
+ where?: Where;
59
+ order?: EntryOrder;
60
+ limit?: number;
61
+ parent?: string | null;
62
+ ids?: string[];
63
+ };
64
+ export type PublishedKey = {
65
+ kind: "page" | "entry";
66
+ id: string;
67
+ locale?: string;
68
+ };
69
+ export interface PublishedStore {
70
+ /** Upsert by (id, locale). */
71
+ putPages(rows: PublishedPageRow[]): Promise<void>;
72
+ putEntries(rows: PublishedEntryRow[]): Promise<void>;
73
+ /** Every locale of the id when `locale` is absent. */
74
+ remove(keys: PublishedKey[]): Promise<void>;
75
+ /** The one-call lookup: a reachable page or entry at the address, else null. With `globals` (collection names of
76
+ * global items), the locale's rows of those collections come along on the route (schema v9), still one call. */
77
+ byAddress(locale: string, path: string, globals?: readonly string[]): Promise<PublishedRoute | null>;
78
+ page(id: string, locale: string): Promise<PublishedPageRow | null>;
79
+ entry(id: string, locale: string): Promise<PublishedEntryRow | null>;
80
+ /** Filtered, ordered and limited over `fields`, in the store (SQL or the memory matcher). */
81
+ entries(collection: string, q: PublishedQuery): Promise<PublishedEntryRow[]>;
82
+ /** The light columns of every row in the locale, with the model and stale flag, in tree order (sort asc nulls last, published, id). */
83
+ tree(locale: string): Promise<PublishedTreeRow[]>;
84
+ markStale(keys: PublishedKey[]): Promise<void>;
85
+ /** The document stamp this space recorded as current (DESIGN §3): what `rebuild` last wrote, or the first
86
+ * writer's; null until then. Only a reader at this stamp rewrites rows. */
87
+ recordedModel(): Promise<string | null>;
88
+ /** Record `model` as the space's current stamp (upsert by space). */
89
+ recordModel(model: string): Promise<void>;
90
+ /** Rows of both tables, and how many of them carry a stamp other than `model`: what `migrate --check` and Content health show. */
91
+ countBehind(model: string): Promise<{
92
+ behind: number;
93
+ total: number;
94
+ }>;
95
+ }
96
+ export declare class MemoryPublishedStore implements PublishedStore {
97
+ private pages;
98
+ private entryRows;
99
+ private recorded;
100
+ recordedModel(): Promise<string | null>;
101
+ recordModel(model: string): Promise<void>;
102
+ countBehind(model: string): Promise<{
103
+ behind: number;
104
+ total: number;
105
+ }>;
106
+ putPages(rows: PublishedPageRow[]): Promise<void>;
107
+ putEntries(rows: PublishedEntryRow[]): Promise<void>;
108
+ remove(keys: PublishedKey[]): Promise<void>;
109
+ byAddress(locale: string, path: string, globals?: readonly string[]): Promise<PublishedRoute | null>;
110
+ page(id: string, locale: string): Promise<PublishedPageRow | null>;
111
+ entry(id: string, locale: string): Promise<PublishedEntryRow | null>;
112
+ entries(collection: string, q: PublishedQuery): Promise<PublishedEntryRow[]>;
113
+ tree(locale: string): Promise<PublishedTreeRow[]>;
114
+ markStale(keys: PublishedKey[]): Promise<void>;
115
+ }
@@ -0,0 +1,136 @@
1
+ // The delivery model's store (spec 2026-10-07 delivery model §1, §5): the
2
+ // finished documents Publish writes, one row per page, entry or global per
3
+ // locale, read in one lookup. Derived from the editing tables and
4
+ // rebuildable; the memory adapter is two maps with the matcher in code, the
5
+ // Supabase one (supabase-published-store.ts) reads published_pages,
6
+ // published_entries, the published_routes view and smoodly_list_published.
7
+ import { matchesWhere } from "./entry-store.js";
8
+ const keyOf = (id, locale) => `${id}:${locale}`;
9
+ const asc = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
10
+ /** smoodly_list_published's ORDER BY: manual by sort, else the built-in time or
11
+ * the field, absent, null and empty last in both directions; ties by creation, then id. */
12
+ function compareRows(order) {
13
+ return (a, b) => {
14
+ if (order === "manual") {
15
+ const d = (a.sort ?? Infinity) - (b.sort ?? Infinity);
16
+ if (d !== 0 && !Number.isNaN(d))
17
+ return d;
18
+ }
19
+ else if (order) {
20
+ const value = (r) => {
21
+ const v = order.by === "createdAt" ? r.createdAt : order.by === "updatedAt" ? r.updatedAt : r.fields[order.by];
22
+ return v === "" ? null : v;
23
+ };
24
+ const va = value(a), vb = value(b);
25
+ if (va == null && vb != null)
26
+ return 1;
27
+ if (vb == null && va != null)
28
+ return -1;
29
+ if (va != null && vb != null) {
30
+ const d = typeof va === "number" && typeof vb === "number" ? va - vb : String(va).localeCompare(String(vb));
31
+ if (d !== 0)
32
+ return order.direction === "asc" ? d : -d;
33
+ }
34
+ }
35
+ return asc(a.createdAt, b.createdAt) || asc(a.id, b.id);
36
+ };
37
+ }
38
+ /** An address holds one row per table, as the SQL's unique (space, locale, path) index says: a row of another id
39
+ * left at an incoming address (a stale row a capped rename did not reach) goes before the write. */
40
+ function vacate(map, rows) {
41
+ const incoming = new Map(rows.filter((r) => r.path !== null).map((r) => [`${r.locale}\u0000${r.path}`, r.id]));
42
+ for (const [k, row] of map) {
43
+ const holder = row.path === null ? undefined : incoming.get(`${row.locale}\u0000${row.path}`);
44
+ if (holder !== undefined && holder !== row.id)
45
+ map.delete(k);
46
+ }
47
+ }
48
+ export class MemoryPublishedStore {
49
+ constructor() {
50
+ this.pages = new Map();
51
+ this.entryRows = new Map();
52
+ this.recorded = null;
53
+ }
54
+ async recordedModel() {
55
+ return this.recorded;
56
+ }
57
+ async recordModel(model) {
58
+ this.recorded = model;
59
+ }
60
+ async countBehind(model) {
61
+ const rows = [...this.pages.values(), ...this.entryRows.values()];
62
+ return { behind: rows.filter((r) => r.model !== model).length, total: rows.length };
63
+ }
64
+ async putPages(rows) {
65
+ vacate(this.pages, rows);
66
+ for (const row of rows)
67
+ this.pages.set(keyOf(row.id, row.locale), structuredClone(row));
68
+ }
69
+ async putEntries(rows) {
70
+ vacate(this.entryRows, rows);
71
+ for (const row of rows)
72
+ this.entryRows.set(keyOf(row.id, row.locale), structuredClone(row));
73
+ }
74
+ async remove(keys) {
75
+ for (const key of keys) {
76
+ const map = key.kind === "page" ? this.pages : this.entryRows;
77
+ for (const [k, row] of map)
78
+ if (row.id === key.id && (key.locale === undefined || row.locale === key.locale))
79
+ map.delete(k);
80
+ }
81
+ }
82
+ async byAddress(locale, path, globals) {
83
+ const found = () => {
84
+ for (const row of this.pages.values()) {
85
+ if (row.locale === locale && row.path === path && row.hasPage)
86
+ return { kind: "page", row: structuredClone(row) };
87
+ }
88
+ for (const row of this.entryRows.values()) {
89
+ if (row.locale === locale && row.path === path)
90
+ return { kind: "entry", row: structuredClone(row) };
91
+ }
92
+ return null;
93
+ };
94
+ const route = found();
95
+ if (!route || !globals)
96
+ return route;
97
+ const wanted = new Set(globals);
98
+ route.globals = structuredClone([...this.entryRows.values()].filter((r) => r.locale === locale && wanted.has(r.collection)));
99
+ return route;
100
+ }
101
+ async page(id, locale) {
102
+ const row = this.pages.get(keyOf(id, locale));
103
+ return row ? structuredClone(row) : null;
104
+ }
105
+ async entry(id, locale) {
106
+ const row = this.entryRows.get(keyOf(id, locale));
107
+ return row ? structuredClone(row) : null;
108
+ }
109
+ async entries(collection, q) {
110
+ const wanted = q.ids ? new Set(q.ids) : null;
111
+ const rows = [...this.entryRows.values()].filter((r) => r.collection === collection &&
112
+ r.locale === q.locale &&
113
+ (q.parent === undefined || r.parentId === q.parent) &&
114
+ (!wanted || wanted.has(r.id)) &&
115
+ (!q.where || matchesWhere(r.fields, q.where)));
116
+ rows.sort(compareRows(q.order));
117
+ return structuredClone(typeof q.limit === "number" ? rows.slice(0, q.limit) : rows);
118
+ }
119
+ async tree(locale) {
120
+ return [...this.pages.values()]
121
+ .filter((r) => r.locale === locale)
122
+ .sort((a, b) => {
123
+ const d = (a.sort ?? Infinity) - (b.sort ?? Infinity);
124
+ return (d !== 0 && !Number.isNaN(d) ? d : 0) || asc(a.publishedAt, b.publishedAt) || asc(a.id, b.id);
125
+ })
126
+ .map(({ id, parentId, sort, path, title, hasPage, linkFields, model, stale }) => structuredClone({ id, parentId, sort, path, title, hasPage, linkFields, model, stale }));
127
+ }
128
+ async markStale(keys) {
129
+ for (const key of keys) {
130
+ const map = key.kind === "page" ? this.pages : this.entryRows;
131
+ for (const row of map.values())
132
+ if (row.id === key.id && (key.locale === undefined || row.locale === key.locale))
133
+ row.stale = true;
134
+ }
135
+ }
136
+ }
@@ -0,0 +1,61 @@
1
+ import { element, section } from "./pairing.tsx";
2
+ import { page } from "./render.tsx";
3
+ import { entryPage } from "./entry-page.tsx";
4
+ import { Section } from "./section-wrapper.tsx";
5
+ export * from "./index.ts";
6
+ export { element, entryPage, page, section, Section };
7
+ export type { PairedComponent } from "./pairing.tsx";
8
+ export { Zone, renderPage } from "./render.tsx";
9
+ export type { PairedPage, PageViewProps, Registry } from "./render.tsx";
10
+ export type { PairedEntryPage } from "./entry-page.tsx";
11
+ export { RichText, defaultRichTextImage } from "./richtext-render.tsx";
12
+ export type { RichTextProps, RichTextElement, RichTextImageProps } from "./richtext-render.tsx";
13
+ export { defineViews } from "./views.ts";
14
+ export type { Views, ViewsInput } from "./views.ts";
15
+ export { createCustomSection } from "./custom/CustomSection.tsx";
16
+ export type { CustomOptions, CustomSectionProps, CustomSectionComponent } from "./custom/CustomSection.tsx";
17
+ /** The root's namespace plus the React pairing: `smoodly.section(schema, View)`, `<smoodly.Section>`. */
18
+ export declare const smoodly: {
19
+ section: typeof section;
20
+ element: typeof element;
21
+ page: typeof page;
22
+ entryPage: typeof entryPage;
23
+ Section: typeof Section;
24
+ schema: {
25
+ page: typeof import("./page.ts").pageSchema;
26
+ section<F extends import("./schema.ts").BuilderMap = {}, S extends import("./schema.ts").BuilderMap = {}>(input: {
27
+ name: string;
28
+ title: string;
29
+ icon?: string;
30
+ description?: string;
31
+ fields?: F | undefined;
32
+ settings?: S | undefined;
33
+ meta?: never;
34
+ styles?: false | {
35
+ omit: string[];
36
+ };
37
+ css?: false;
38
+ sample?: Partial<import("./schema.ts").StoredValues<F> & import("./schema.ts").StoredValues<S>> | undefined;
39
+ }): import("./schema.ts").SectionSchema<F, S>;
40
+ element<F extends import("./schema.ts").BuilderMap = {}, S extends import("./schema.ts").BuilderMap = {}>(input: {
41
+ name: string;
42
+ title: string;
43
+ icon?: string;
44
+ description?: string;
45
+ fields?: F | undefined;
46
+ settings?: S | undefined;
47
+ meta?: never;
48
+ styles?: false | {
49
+ omit: string[];
50
+ };
51
+ css?: false;
52
+ sample?: Partial<import("./schema.ts").StoredValues<F> & import("./schema.ts").StoredValues<S>> | undefined;
53
+ } & {
54
+ contexts: ("richtext" | "column")[];
55
+ }): import("./schema.ts").ElementSchema<F, S>;
56
+ };
57
+ collection: typeof import("./collections.ts").collection;
58
+ toolset: typeof import("./toolset.ts").toolset;
59
+ global: typeof import("./global.ts").globalItem;
60
+ marks: typeof import("./marks.ts").marks;
61
+ };
package/dist/react.js ADDED
@@ -0,0 +1,17 @@
1
+ // smoodly/react: the React half of the package (spec 2026-10-02 astro cloud fit §8; plan 2026-10-06 astro
2
+ // adapter 3, decision 1) — the pairing, the render layer and the view registry. A Next project's components
3
+ // import from here; a `.schema.ts` file and the config import from "smoodly", which loads no React, so an
4
+ // Astro project has none. Everything of the root is re-exported, so one import serves a component.
5
+ import { smoodly as core } from "./index.js";
6
+ import { element, section } from "./pairing.js";
7
+ import { page } from "./render.js";
8
+ import { entryPage } from "./entry-page.js";
9
+ import { Section } from "./section-wrapper.js";
10
+ export * from "./index.js";
11
+ export { element, entryPage, page, section, Section };
12
+ export { Zone, renderPage } from "./render.js";
13
+ export { RichText, defaultRichTextImage } from "./richtext-render.js";
14
+ export { defineViews } from "./views.js";
15
+ export { createCustomSection } from "./custom/CustomSection.js";
16
+ /** The root's namespace plus the React pairing: `smoodly.section(schema, View)`, `<smoodly.Section>`. */
17
+ export const smoodly = { ...core, section, element, page, entryPage, Section };
@@ -1,7 +1,7 @@
1
1
  import type { CollectionSchema } from "./collections.ts";
2
2
  import type { EntryRecord } from "./entry-store.ts";
3
3
  import type { LocaleAlternate, PageAlternate } from "./paths.ts";
4
- import type { PageLink } from "./render.tsx";
4
+ import type { PageLink } from "./page-context.ts";
5
5
  import type { ResolvedImage } from "./media.ts";
6
6
  import type { SitePage } from "./site.ts";
7
7
  import type { StyleProps } from "./styles.ts";
@@ -0,0 +1,80 @@
1
+ import type { CollectionSchema } from "./collections.ts";
2
+ import type { GlobalSchema } from "./global.ts";
3
+ import type { PathTarget } from "./paths.ts";
4
+ import { type EntryItem, type PageItem } from "./read-shapes.ts";
5
+ import type { Materialize } from "./schema.ts";
6
+ import type { AddressWithGlobals, EntryQuery, PageTreeItem, SiteHit, SmoodlySite } from "./site.ts";
7
+ /** One content read, as the framework's cache needs it. */
8
+ export type ReadUnit<T> = {
9
+ /** What the published read holds: two calls with one key may share a cached `read(false)`. A draft read
10
+ * has no key of its own — the draft and a stored version read under the same one — so it is never
11
+ * cached by it. The site's model key rides in it. */
12
+ key: string[];
13
+ /** The tags the admin's operations expire it by (revalidate.ts). */
14
+ tags: string[];
15
+ /** Tags only the value knows (a read by address learns the id from the document it returns). An adapter
16
+ * that tags a response after the read adds them; Next's cache needs its tags before the read and ignores this. */
17
+ tagsOf?(value: T): string[];
18
+ /** The read: the live draft, or the published value. */
19
+ read(draft: boolean): Promise<T>;
20
+ };
21
+ /** What a framework supplies. Every read of content passes through `run`, so an adapter that caches whole
22
+ * responses hears the tags of everything a response rendered. A path lookup passes no unit: `locate`, a
23
+ * redirect, and a path that leads nowhere (a null from `getPage` or `getEntry`, a 404) reach no `run`. */
24
+ export type ReadAdapter = {
25
+ /** Resolves when there is a request to read for. Next: `connection()`, so a build prerenders nothing
26
+ * against the database. Absent where a build may read (Astro's static mode). */
27
+ gate?(): Promise<void>;
28
+ /** Runs a unit: the adapter decides draft or published, where the value is cached, and who hears its tags. */
29
+ run<T>(unit: ReadUnit<T>): Promise<T>;
30
+ };
31
+ export type QueryOptions = {
32
+ locale?: string;
33
+ };
34
+ export type EntriesQuery = Omit<EntryQuery, "locale" | "draft" | "ids"> & QueryOptions;
35
+ type ByName<T, N> = Extract<T, {
36
+ name: N;
37
+ }>;
38
+ /** Fields typed from the schema, references materialized as `EntryRef`s; a widened `CollectionSchema[]` array (names are `string`) falls back to the untyped record instead of `never`. */
39
+ type EntryFields<C extends readonly CollectionSchema<any, string>[], N> = string extends C[number]["name"] ? Record<string, unknown> : {
40
+ [K in keyof NonNullable<ByName<C[number], N>["__entry"]>]: Materialize<NonNullable<ByName<C[number], N>["__entry"]>[K]>;
41
+ };
42
+ /** A read item whose `fields` are the collection's typed values. */
43
+ export type TypedEntryItem<C extends readonly CollectionSchema<any, string>[], N> = EntryItem<EntryFields<C, N>>;
44
+ export type GlobalValue<S extends readonly GlobalSchema<any, string>[], N> = string extends S[number]["name"] ? Record<string, unknown> : NonNullable<ByName<S[number], N>["__value"]>;
45
+ export type SmoodlyQueries<C extends readonly CollectionSchema<any, string>[], S extends readonly GlobalSchema<any, string>[]> = {
46
+ /** The published page tree in a locale, every item with its page fields. */
47
+ getPageTree(options?: QueryOptions): Promise<PageTreeItem[]>;
48
+ /** A page by locale-relative path ("/company/about-us") or id. */
49
+ getPage(pathOrId: string, options?: QueryOptions): Promise<PageItem | null>;
50
+ /** Published entries of a collection: `where` (shared fields), `order`, `limit`, `parent` run in the store. */
51
+ getEntries<N extends C[number]["name"]>(collection: N, options?: EntriesQuery): Promise<TypedEntryItem<C, N>[]>;
52
+ getEntry<N extends C[number]["name"]>(collection: N, idOrPath: string, options?: QueryOptions): Promise<TypedEntryItem<C, N> | null>;
53
+ getGlobal<N extends S[number]["name"]>(item: N, options?: QueryOptions): Promise<GlobalValue<S, N> | null>;
54
+ };
55
+ /** The route's read of a located target (locate is never a unit: the id it returns IS the tag). Keyed by
56
+ * what the unit holds, not only where: an address that changes owner — a deploy handing it to a code
57
+ * record, which fires no tag — can never serve the previous occupant (spec 2026-09-29 pre-install fixes
58
+ * §5). `version` is History's stored version and counts inside draft mode only. */
59
+ export declare function hitUnit(site: SmoodlySite, target: PathTarget, locale: string, path: string, version?: string): ReadUnit<SiteHit | null>;
60
+ /** Astro's route read of an address: one lookup outside draft mode — the tags come with the document, so no
61
+ * locate comes first; in draft mode, locate then load as the editing tables need (spec 2026-10-07 delivery
62
+ * model §3). A redirect row answers as a redirect; a code-owned page answers where no record lives. */
63
+ export declare function addressUnit(site: SmoodlySite, locale: string, path: string, version?: string): ReadUnit<SiteHit | {
64
+ kind: "redirect";
65
+ target: PathTarget;
66
+ } | null>;
67
+ /** Astro's route read with the layout's global items riding along (schema v9): the published read is one
68
+ * `lookupWithGlobals`, and the shell seeds each value into the request scope under `globalUnit`'s key, so the
69
+ * layout's loaders cost no call. The tags are the hit's; a global's tag joins when the layout reads it. A draft
70
+ * read brings no globals: a preview reads live. */
71
+ export declare function addressWithGlobalsUnit(site: SmoodlySite, locale: string, path: string, items: readonly GlobalSchema<any>[], version?: string): ReadUnit<AddressWithGlobals>;
72
+ /** A global item's unit, by schema: the one `q.getGlobal` runs for the same item and locale. */
73
+ export declare function globalUnit<V>(site: SmoodlySite, item: GlobalSchema<V>, locale: string): ReadUnit<V | null>;
74
+ /** The typed queries over a site, run by the framework's adapter. */
75
+ export declare function createQueries<const C extends readonly CollectionSchema<any, string>[], const S extends readonly GlobalSchema<any, string>[] = []>(options: {
76
+ site: SmoodlySite;
77
+ collections: C;
78
+ globals?: S;
79
+ }, adapter: ReadAdapter): SmoodlyQueries<C, S>;
80
+ export {};
package/dist/reads.js ADDED
@@ -0,0 +1,142 @@
1
+ import { referencedCollections } from "./read-shapes.js";
2
+ import { collectionTag, entryTag, globalTag, pageTag, pagesTreeTag } from "./revalidate.js";
3
+ /** JSON with object keys sorted at every level and `undefined` dropped, so two spellings of one query share a unit. */
4
+ function stable(value) {
5
+ if (Array.isArray(value))
6
+ return value.map(stable);
7
+ if (value !== null && typeof value === "object") {
8
+ return Object.fromEntries(Object.entries(value)
9
+ .filter(([, v]) => v !== undefined)
10
+ .sort(([a], [b]) => (a < b ? -1 : 1))
11
+ .map(([k, v]) => [k, stable(v)]));
12
+ }
13
+ return value;
14
+ }
15
+ /** A stable key for an entries query: only what was given, normalized. */
16
+ function entriesKey(o) {
17
+ const { locale: _locale, ...rest } = o;
18
+ return JSON.stringify(stable(rest));
19
+ }
20
+ /** The route's read of a located target (locate is never a unit: the id it returns IS the tag). Keyed by
21
+ * what the unit holds, not only where: an address that changes owner — a deploy handing it to a code
22
+ * record, which fires no tag — can never serve the previous occupant (spec 2026-09-29 pre-install fixes
23
+ * §5). `version` is History's stored version and counts inside draft mode only. */
24
+ export function hitUnit(site, target, locale, path, version) {
25
+ return {
26
+ key: ["smoodly-site", site.cacheKey, locale, path, target.kind, target.id],
27
+ tags: [target.kind === "page" ? pageTag(target.id) : entryTag(target.id)],
28
+ read: (draft) => draft ? site.load(target, locale, path, { draft: true, ...(version !== undefined ? { version } : {}) }) : site.load(target, locale, path),
29
+ };
30
+ }
31
+ /** Astro's route read of an address: one lookup outside draft mode — the tags come with the document, so no
32
+ * locate comes first; in draft mode, locate then load as the editing tables need (spec 2026-10-07 delivery
33
+ * model §3). A redirect row answers as a redirect; a code-owned page answers where no record lives. */
34
+ export function addressUnit(site, locale, path, version) {
35
+ return {
36
+ key: ["smoodly-site", site.cacheKey, locale, path],
37
+ tags: [],
38
+ tagsOf: tagsOfHit,
39
+ read: (draft) => (draft ? draftAddress(site, locale, path, version) : site.lookup(locale, path)),
40
+ };
41
+ }
42
+ /** The tags an address's value brings: the page's or the entry's; none for a miss, a redirect or a code-owned page. */
43
+ function tagsOfHit(value) {
44
+ if (!value || value.kind === "redirect")
45
+ return [];
46
+ if (value.kind === "entry")
47
+ return [entryTag(value.entry.id)];
48
+ // A code-owned page has no row to expire it by.
49
+ return value.record.id.startsWith("code:") ? [] : [pageTag(value.record.id)];
50
+ }
51
+ /** The draft read of an address: locate, then load the draft, as the editing tables need. */
52
+ async function draftAddress(site, locale, path, version) {
53
+ const target = await site.locate(locale, path);
54
+ if (!target)
55
+ return site.codePage(locale, path);
56
+ if (target.redirect)
57
+ return { kind: "redirect", target: { kind: target.kind, id: target.id } };
58
+ return site.load(target, locale, path, { draft: true, ...(version !== undefined ? { version } : {}) });
59
+ }
60
+ /** Astro's route read with the layout's global items riding along (schema v9): the published read is one
61
+ * `lookupWithGlobals`, and the shell seeds each value into the request scope under `globalUnit`'s key, so the
62
+ * layout's loaders cost no call. The tags are the hit's; a global's tag joins when the layout reads it. A draft
63
+ * read brings no globals: a preview reads live. */
64
+ export function addressWithGlobalsUnit(site, locale, path, items, version) {
65
+ return {
66
+ key: ["smoodly-site", site.cacheKey, locale, path, "+globals"],
67
+ tags: [],
68
+ tagsOf: (value) => tagsOfHit(value.hit),
69
+ read: async (draft) => (draft ? { hit: await draftAddress(site, locale, path, version), globals: new Map() } : site.lookupWithGlobals(locale, path, items)),
70
+ };
71
+ }
72
+ /** A global item's unit, by schema: the one `q.getGlobal` runs for the same item and locale. */
73
+ export function globalUnit(site, item, locale) {
74
+ return {
75
+ key: ["smoodly-q", site.cacheKey, "global", item.name, locale],
76
+ tags: [globalTag(item.name)],
77
+ read: (draft) => site.getGlobal(item, { locale, draft }),
78
+ };
79
+ }
80
+ /** The typed queries over a site, run by the framework's adapter. */
81
+ export function createQueries(options, adapter) {
82
+ const { site } = options;
83
+ const collectionByName = new Map(options.collections.map((c) => [c.name, c]));
84
+ const globalByName = new Map((options.globals ?? []).map((s) => [s.name, s]));
85
+ const localeOf = (o) => o?.locale ?? site.locales.default;
86
+ const gate = async () => { await adapter.gate?.(); };
87
+ const unit = (key, tags, read) => ({ key: ["smoodly-q", site.cacheKey, ...key], tags, read });
88
+ /** The tag needs the id: a path is located first, outside any unit, as the route does. */
89
+ async function idOf(kind, locale, pathOrId) {
90
+ if (!pathOrId.startsWith("/"))
91
+ return pathOrId;
92
+ const target = await site.locate(locale, pathOrId);
93
+ return target?.kind === kind ? target.id : null;
94
+ }
95
+ return {
96
+ async getPageTree(o) {
97
+ await gate();
98
+ const locale = localeOf(o);
99
+ return adapter.run(unit(["pageTree", locale], [pagesTreeTag()], (draft) => site.getPagesTree({ locale, draft })));
100
+ },
101
+ async getPage(pathOrId, o) {
102
+ await gate();
103
+ const locale = localeOf(o);
104
+ const id = await idOf("page", locale, pathOrId);
105
+ if (!id)
106
+ return null;
107
+ return adapter.run(unit(["page", locale, pathOrId], [pageTag(id)], (draft) => site.getPage(pathOrId, { locale, draft })));
108
+ },
109
+ async getEntries(collection, o = {}) {
110
+ await gate();
111
+ const locale = localeOf(o);
112
+ const { locale: _locale, ...query } = o;
113
+ // The unit also depends on every collection its references resolve into (spec 2026-09-29 content API contract §6).
114
+ const schema = collectionByName.get(collection);
115
+ const tags = [collection, ...(schema ? referencedCollections(schema, [...options.collections]) : [])];
116
+ return adapter.run(unit(["entries", collection, locale, entriesKey(o)], [...new Set(tags)].map(collectionTag), (draft) => site.getEntries(collection, { ...query, locale, draft })));
117
+ },
118
+ async getEntry(collection, idOrPath, o) {
119
+ await gate();
120
+ const locale = localeOf(o);
121
+ const id = await idOf("entry", locale, idOrPath);
122
+ if (!id)
123
+ return null;
124
+ return adapter.run(unit(["entry", collection, locale, idOrPath], [entryTag(id)], async (draft) => {
125
+ const hit = await site.getEntry(collection, idOrPath, { locale, draft });
126
+ // The site layer also answers a bare root slug, but the unit would sit under
127
+ // a tag no op ever fires — so a slug is refused here rather than cached stale.
128
+ if (hit && !idOrPath.startsWith("/") && hit.id !== idOrPath) {
129
+ throw new Error(`smoodly: getEntry("${collection}", "${idOrPath}") takes an id or a path — for a slug, pass the entry's path.`);
130
+ }
131
+ return hit;
132
+ }));
133
+ },
134
+ async getGlobal(name, o) {
135
+ const item = globalByName.get(name);
136
+ if (!item)
137
+ throw new Error(`smoodly: no global item named "${name}".`);
138
+ await gate();
139
+ return adapter.run(globalUnit(site, item, localeOf(o)));
140
+ },
141
+ };
142
+ }
package/dist/refs.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import type { Descriptor } from "./fields.ts";
2
2
  import type { CollectionSchema } from "./collections.ts";
3
- import type { PageTree } from "./render.tsx";
3
+ import type { PageTree } from "./page-context.ts";
4
4
  export type RefEdge = {
5
5
  field: string;
6
6
  targetCollection: string;
@@ -0,0 +1,8 @@
1
+ export type SchemaOr<S> = S | {
2
+ schema: S;
3
+ };
4
+ export declare function schemaOf<S extends object>(x: SchemaOr<S>): S;
5
+ /** The schema a public builder was given, refusing a paired component (plan 2026-10-02 registration split). */
6
+ export declare function schemaOnly<S extends {
7
+ name: string;
8
+ }>(x: S): S;
@@ -0,0 +1,17 @@
1
+ export function schemaOf(x) {
2
+ const carried = x.schema;
3
+ return carried !== null && typeof carried === "object" ? carried : x;
4
+ }
5
+ /** The schema a public builder was given, refusing a paired component (plan 2026-10-02 registration split). */
6
+ export function schemaOnly(x) {
7
+ const carried = x.schema;
8
+ if (carried !== null && typeof carried === "object") {
9
+ if (carried.kind === "entryPage") {
10
+ const c = String(carried.collection);
11
+ throw new Error(`smoodly: an entry page is a view — list smoodly.entryPage(${c}, View) in defineViews({ pages }), not in registry.pages.`);
12
+ }
13
+ const name = String(carried.name);
14
+ throw new Error(`smoodly: pass the schema, not the component — "${name}" is registered by its schema (${name}Schema), and its component goes to defineViews.`);
15
+ }
16
+ return x;
17
+ }