smoodly 0.0.44 → 0.0.46

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 (241) 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/index.d.ts +1 -1
  32. package/dist/admin/ops-impl.d.ts +7 -1
  33. package/dist/admin/ops-impl.js +110 -33
  34. package/dist/admin/ops.d.ts +3 -2
  35. package/dist/admin/page-data.d.ts +21 -0
  36. package/dist/admin/page-data.js +4 -0
  37. package/dist/admin/page.d.ts +6 -0
  38. package/dist/admin/page.js +35 -0
  39. package/dist/admin/serialize.js +7 -7
  40. package/dist/admin/shell/AdminApp.js +2 -2
  41. package/dist/admin/shell/EntryForm.js +1 -1
  42. package/dist/admin/shell/router.d.ts +3 -16
  43. package/dist/admin/shell/router.js +3 -39
  44. package/dist/admin/shell/session.d.ts +2 -5
  45. package/dist/admin/supabase-auth.js +1 -1
  46. package/dist/admin/tree-ops.d.ts +1 -1
  47. package/dist/admin/ui/fonts.d.ts +12 -0
  48. package/dist/admin/ui/fonts.js +22 -0
  49. package/dist/admin/ui/theme.d.ts +13 -4
  50. package/dist/admin/ui/theme.js +28 -157
  51. package/dist/assemble.d.ts +60 -0
  52. package/dist/assemble.js +359 -0
  53. package/dist/astro/cache.d.ts +34 -0
  54. package/dist/astro/cache.js +69 -0
  55. package/dist/astro/components/Custom.astro +21 -0
  56. package/dist/astro/components/Guarded.astro +24 -0
  57. package/dist/astro/components/Head.astro +26 -0
  58. package/dist/astro/components/Image.astro +20 -0
  59. package/dist/astro/components/RichText.astro +23 -0
  60. package/dist/astro/components/Section.astro +20 -0
  61. package/dist/astro/components/Video.astro +5 -0
  62. package/dist/astro/components/Zone.astro +19 -0
  63. package/dist/astro/components.d.ts +6 -0
  64. package/dist/astro/components.js +7 -0
  65. package/dist/astro/content-loaders.d.ts +40 -0
  66. package/dist/astro/content-loaders.js +100 -0
  67. package/dist/astro/content.d.ts +9 -0
  68. package/dist/astro/content.js +17 -0
  69. package/dist/astro/custom.d.ts +1 -0
  70. package/dist/astro/custom.js +3 -0
  71. package/dist/astro/defaults.d.ts +20 -0
  72. package/dist/astro/defaults.js +21 -0
  73. package/dist/astro/env-files.d.ts +1 -0
  74. package/dist/astro/env-files.js +21 -0
  75. package/dist/astro/head.d.ts +13 -0
  76. package/dist/astro/head.js +19 -0
  77. package/dist/astro/image.d.ts +27 -0
  78. package/dist/astro/image.js +29 -0
  79. package/dist/astro/index.d.ts +43 -0
  80. package/dist/astro/index.js +111 -0
  81. package/dist/astro/locale-paths.d.ts +30 -0
  82. package/dist/astro/locale-paths.js +38 -0
  83. package/dist/astro/middleware.d.ts +1 -0
  84. package/dist/astro/middleware.js +83 -0
  85. package/dist/astro/preview-document.d.ts +1 -0
  86. package/dist/astro/preview-document.js +18 -0
  87. package/dist/astro/preview.d.ts +10 -0
  88. package/dist/astro/preview.js +72 -0
  89. package/dist/astro/richtext.d.ts +15 -0
  90. package/dist/astro/richtext.js +78 -0
  91. package/dist/astro/routes/admin.d.ts +3 -0
  92. package/dist/astro/routes/admin.js +41 -0
  93. package/dist/astro/routes/page.astro +17 -0
  94. package/dist/astro/routes/preview.astro +11 -0
  95. package/dist/astro/runtime.d.ts +32 -0
  96. package/dist/astro/runtime.js +46 -0
  97. package/dist/astro/scope.d.ts +45 -0
  98. package/dist/astro/scope.js +103 -0
  99. package/dist/astro/server.d.ts +14 -0
  100. package/dist/astro/server.js +45 -0
  101. package/dist/astro/shell.d.ts +67 -0
  102. package/dist/astro/shell.js +63 -0
  103. package/dist/astro/style.d.ts +1 -0
  104. package/dist/astro/style.js +11 -0
  105. package/dist/astro/views-check.d.ts +3 -0
  106. package/dist/astro/views-check.js +19 -0
  107. package/dist/astro/zone.d.ts +39 -0
  108. package/dist/astro/zone.js +33 -0
  109. package/dist/bind.d.ts +19 -0
  110. package/dist/bind.js +34 -0
  111. package/dist/bridge/bridge.d.ts +9 -0
  112. package/dist/bridge/bridge.js +107 -0
  113. package/dist/bridge/entry.d.ts +1 -0
  114. package/dist/bridge/entry.js +5 -0
  115. package/dist/bridge/preview-pill.d.ts +18 -0
  116. package/dist/bridge/preview-pill.js +115 -0
  117. package/dist/cli/import.d.ts +3 -0
  118. package/dist/cli/import.js +10 -1
  119. package/dist/cli/load-collections.js +4 -3
  120. package/dist/cli/load-config.d.ts +4 -0
  121. package/dist/cli/load-config.js +26 -0
  122. package/dist/cli/main.d.ts +7 -1
  123. package/dist/cli/main.js +32 -7
  124. package/dist/cli/migrate.d.ts +3 -0
  125. package/dist/cli/migrate.js +8 -1
  126. package/dist/cli/rebuild.d.ts +23 -0
  127. package/dist/cli/rebuild.js +22 -0
  128. package/dist/cli/space-io.d.ts +15 -1
  129. package/dist/cli/space-io.js +14 -0
  130. package/dist/client.js +5 -0
  131. package/dist/config.d.ts +28 -20
  132. package/dist/config.js +71 -75
  133. package/dist/content/content-api.d.ts +1 -1
  134. package/dist/content/node.d.ts +2 -3
  135. package/dist/content/node.js +2 -1
  136. package/dist/crash-message.d.ts +1 -0
  137. package/dist/crash-message.js +8 -0
  138. package/dist/css.d.ts +4 -0
  139. package/dist/css.js +5 -2
  140. package/dist/custom/CustomSection.d.ts +1 -1
  141. package/dist/custom/CustomSection.js +4 -8
  142. package/dist/custom/css.js +2 -2
  143. package/dist/custom/index.d.ts +6 -10
  144. package/dist/custom/index.js +1 -2
  145. package/dist/custom/layout.d.ts +5 -0
  146. package/dist/custom/layout.js +14 -0
  147. package/dist/entry-page.d.ts +3 -18
  148. package/dist/env.js +1 -1
  149. package/dist/fields.js +2 -1
  150. package/dist/global-id.js +2 -2
  151. package/dist/global.d.ts +5 -2
  152. package/dist/global.js +8 -5
  153. package/dist/image/next-config.d.ts +3 -0
  154. package/dist/image/next-config.js +12 -7
  155. package/dist/image/remote-pattern.d.ts +1 -0
  156. package/dist/image/remote-pattern.js +5 -0
  157. package/dist/index.d.ts +15 -17
  158. package/dist/index.js +10 -19
  159. package/dist/marks.d.ts +7 -0
  160. package/dist/marks.js +7 -0
  161. package/dist/next/admin-route.d.ts +7 -0
  162. package/dist/next/admin-route.js +28 -0
  163. package/dist/next/bridge.d.ts +1 -0
  164. package/dist/next/bridge.js +26 -0
  165. package/dist/next/canvas-boundary.d.ts +1 -1
  166. package/dist/next/canvas-slot.d.ts +1 -0
  167. package/dist/next/canvas-slot.js +4 -3
  168. package/dist/next/index.d.ts +2 -0
  169. package/dist/next/index.js +1 -0
  170. package/dist/next/page-renderer.d.ts +5 -1
  171. package/dist/next/page-renderer.js +19 -33
  172. package/dist/next/preview-pill-define.d.ts +1 -0
  173. package/dist/next/preview-pill-define.js +10 -0
  174. package/dist/next/preview-pill.d.ts +0 -3
  175. package/dist/next/preview-pill.js +7 -50
  176. package/dist/next/queries.d.ts +3 -28
  177. package/dist/next/queries.js +3 -90
  178. package/dist/next/reads.d.ts +2 -0
  179. package/dist/next/reads.js +18 -0
  180. package/dist/page-context.d.ts +89 -0
  181. package/dist/page-context.js +8 -0
  182. package/dist/pairing.d.ts +2 -22
  183. package/dist/project.d.ts +69 -0
  184. package/dist/project.js +210 -0
  185. package/dist/published.d.ts +96 -0
  186. package/dist/published.js +125 -0
  187. package/dist/react.d.ts +61 -0
  188. package/dist/react.js +17 -0
  189. package/dist/read-shapes.d.ts +1 -1
  190. package/dist/reads.d.ts +80 -0
  191. package/dist/reads.js +142 -0
  192. package/dist/refs.d.ts +1 -1
  193. package/dist/registration.d.ts +8 -0
  194. package/dist/registration.js +17 -0
  195. package/dist/render.d.ts +5 -83
  196. package/dist/render.js +5 -10
  197. package/dist/resolve.d.ts +1 -1
  198. package/dist/richtext/index.d.ts +6 -2
  199. package/dist/richtext/index.js +3 -2
  200. package/dist/richtext-href.d.ts +4 -0
  201. package/dist/richtext-href.js +17 -0
  202. package/dist/richtext-render.js +1 -14
  203. package/dist/schema-version.d.ts +1 -1
  204. package/dist/schema-version.js +1 -1
  205. package/dist/section-crash.d.ts +2 -2
  206. package/dist/section-crash.js +2 -7
  207. package/dist/section-wrapper.d.ts +3 -10
  208. package/dist/section-wrapper.js +3 -2
  209. package/dist/site.d.ts +38 -14
  210. package/dist/site.js +159 -286
  211. package/dist/smd-key.d.ts +9 -0
  212. package/dist/smd-key.js +1 -0
  213. package/dist/sql-space.d.ts +4 -0
  214. package/dist/sql-space.js +10 -5
  215. package/dist/sql.d.ts +2 -0
  216. package/dist/sql.js +117 -0
  217. package/dist/store.d.ts +1 -1
  218. package/dist/supabase-published-store.d.ts +35 -0
  219. package/dist/supabase-published-store.js +215 -0
  220. package/dist/supabase-store.d.ts +1 -1
  221. package/dist/toolset.d.ts +2 -4
  222. package/dist/toolset.js +6 -4
  223. package/dist/version.d.ts +1 -1
  224. package/dist/version.js +1 -1
  225. package/dist/view-props.d.ts +40 -0
  226. package/dist/view-props.js +1 -0
  227. package/dist/views.d.ts +36 -0
  228. package/dist/views.js +52 -0
  229. package/dist/zones.d.ts +7 -7
  230. package/dist/zones.js +7 -6
  231. package/migrations/20261007120000_smoodly_v8.sql +154 -0
  232. package/migrations/20261007200000_smoodly_v9.sql +30 -0
  233. package/package.json +46 -19
  234. package/dist/admin/next/bridge.d.ts +0 -2
  235. package/dist/admin/next/bridge.js +0 -129
  236. package/dist/admin/next/create-admin.d.ts +0 -19
  237. package/dist/admin/next/create-admin.js +0 -44
  238. package/dist/admin/next/index.d.ts +0 -8
  239. package/dist/admin/next/index.js +0 -8
  240. package/dist/admin/next/op-handler.d.ts +0 -18
  241. package/dist/admin/next/op-handler.js +0 -31
@@ -0,0 +1,154 @@
1
+ -- Smoodly core schema, version 8 (docs/superpowers/specs/2026-10-07-delivery-model-design.md).
2
+ -- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
3
+ -- Never edit a published file: the next change is the next file.
4
+ -- The delta over version 7: the delivery model's two derived tables,
5
+ -- published_pages and published_entries, with the space trigger and the
6
+ -- write-key-only policies every content table has (no row cap: they are
7
+ -- derived), smoodly_list_published over the raw published snapshot, and
8
+ -- the published_routes view, one lookup for any address.
9
+
10
+ set client_min_messages = warning;
11
+
12
+ -- The delivery model (spec 2026-10-07): Publish writes the finished document of a page, an entry
13
+ -- or a global here; a live read is one lookup. Derived and rebuildable from the editing tables.
14
+ create table if not exists "published_pages" (
15
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
16
+ "page_id" uuid not null references "pages" ("id") on delete cascade,
17
+ "locale" text not null,
18
+ "parent_id" uuid,
19
+ "sort" numeric,
20
+ "path" text,
21
+ "title" text not null,
22
+ "has_page" boolean not null,
23
+ "link_fields" jsonb not null default '{}'::jsonb,
24
+ "document" jsonb,
25
+ "tags" text[] not null default '{}',
26
+ "model" text not null,
27
+ "stale" boolean not null default false,
28
+ "published_at" timestamptz not null default now(),
29
+ primary key ("page_id", "locale")
30
+ );
31
+ create unique index if not exists "published_pages_address_idx"
32
+ on "published_pages" ("space_id", "locale", "path") where "path" is not null;
33
+ create index if not exists "published_pages_tree_idx"
34
+ on "published_pages" ("space_id", "locale", "parent_id", "sort");
35
+
36
+ create table if not exists "published_entries" (
37
+ "space_id" uuid not null default '00000000-0000-0000-0000-000000000000',
38
+ "entry_id" uuid not null references "entries" ("id") on delete cascade,
39
+ "locale" text not null,
40
+ "collection" text not null,
41
+ "parent_id" uuid,
42
+ "sort" numeric,
43
+ "slug" text,
44
+ "path" text,
45
+ "created_at" timestamptz not null,
46
+ "updated_at" timestamptz not null,
47
+ "fields" jsonb not null,
48
+ "document" jsonb not null,
49
+ "tags" text[] not null default '{}',
50
+ "model" text not null,
51
+ "stale" boolean not null default false,
52
+ "published_at" timestamptz not null default now(),
53
+ primary key ("entry_id", "locale")
54
+ );
55
+ create index if not exists "published_entries_list_idx"
56
+ on "published_entries" ("space_id", "collection", "locale");
57
+ create unique index if not exists "published_entries_address_idx"
58
+ on "published_entries" ("space_id", "locale", "path") where "path" is not null;
59
+ create index if not exists "published_entries_fields_idx"
60
+ on "published_entries" using gin ("fields");
61
+
62
+ alter table "published_pages" enable row level security;
63
+ alter table "published_entries" enable row level security;
64
+
65
+ -- smoodly_list_entries without the draft branch, over the delivery table (spec 2026-10-07 §3):
66
+ -- `where` and `order` read the raw published snapshot, the row answers with its document.
67
+ -- Invoker rights, so RLS decides what a key sees.
68
+ create or replace function "smoodly_list_published"(
69
+ p_collection text, p_locale text, p_where jsonb, p_order jsonb,
70
+ p_limit integer, p_parent uuid, p_has_parent boolean, p_ids uuid[]
71
+ ) returns table ("id" uuid, "document" jsonb, "model" text, "stale" boolean) language sql stable set search_path = public as $$
72
+ with shown as (
73
+ select p."entry_id" as "id", p."sort", p."created_at", p."updated_at", p."fields", p."document", p."model", p."stale"
74
+ from "published_entries" p
75
+ where p."collection" = p_collection and p."locale" = p_locale
76
+ and (not p_has_parent or p."parent_id" is not distinct from p_parent)
77
+ and (p_ids is null or p."entry_id" = any(p_ids))
78
+ )
79
+ select s."id", s."document", s."model", s."stale" from shown s
80
+ where not exists (
81
+ select 1 from jsonb_each(coalesce(p_where, '{}'::jsonb)) w
82
+ where not (case when w.value ? 'in'
83
+ then (s."fields" ->> w.key) = any (select jsonb_array_elements_text(w.value -> 'in'))
84
+ else s."fields" @> jsonb_build_object(w.key, w.value -> 'contains') end))
85
+ order by
86
+ case when p_order ->> 'by' = 'manual' then s."sort" end asc nulls last,
87
+ case when p_order ->> 'direction' = 'asc' then
88
+ case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
89
+ when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end asc nulls last,
90
+ case when p_order ->> 'direction' = 'desc' then
91
+ case p_order ->> 'by' when 'createdAt' then to_jsonb(s."created_at") when 'updatedAt' then to_jsonb(s."updated_at")
92
+ when 'manual' then null else nullif(nullif(s."fields" -> (p_order ->> 'by'), 'null'::jsonb), '""'::jsonb) end end desc nulls last,
93
+ s."created_at" asc, s."id" asc
94
+ limit p_limit;
95
+ $$;
96
+
97
+ -- One lookup for any address (spec 2026-10-07 §3): a page or an entry, under the caller's own policies.
98
+ create or replace view "published_routes" with (security_invoker = true) as
99
+ select "space_id", "locale", "path", 'page' as "kind", "page_id" as "id", "document", "model", "stale", "tags", null::text as "collection"
100
+ from "published_pages" where "path" is not null and "has_page"
101
+ union all
102
+ select "space_id", "locale", "path", 'entry' as "kind", "entry_id" as "id", "document", "model", "stale", "tags", "collection"
103
+ from "published_entries" where "path" is not null;
104
+
105
+ -- Stamped with the caller's space on insert, as every content table.
106
+ drop trigger if exists "smoodly_a_set_space" on "published_pages";
107
+ create trigger "smoodly_a_set_space" before insert on "published_pages"
108
+ for each row execute function "smoodly_set_space"();
109
+
110
+ drop trigger if exists "smoodly_a_set_space" on "published_entries";
111
+ create trigger "smoodly_a_set_space" before insert on "published_entries"
112
+ for each row execute function "smoodly_set_space"();
113
+
114
+ -- A write key reads and writes its own space; no other key reads (spec 2026-09-29 content API contract §8).
115
+ drop policy if exists "smoodly_write" on "published_pages";
116
+
117
+ drop policy if exists "smoodly_insert" on "published_pages";
118
+ create policy "smoodly_insert" on "published_pages" for insert
119
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
120
+
121
+ drop policy if exists "smoodly_update" on "published_pages";
122
+ create policy "smoodly_update" on "published_pages" for update
123
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
124
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
125
+
126
+ drop policy if exists "smoodly_delete" on "published_pages";
127
+ create policy "smoodly_delete" on "published_pages" for delete
128
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
129
+
130
+ drop policy if exists "smoodly_read" on "published_pages";
131
+ create policy "smoodly_read" on "published_pages" for select
132
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
133
+
134
+ drop policy if exists "smoodly_write" on "published_entries";
135
+
136
+ drop policy if exists "smoodly_insert" on "published_entries";
137
+ create policy "smoodly_insert" on "published_entries" for insert
138
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
139
+
140
+ drop policy if exists "smoodly_update" on "published_entries";
141
+ create policy "smoodly_update" on "published_entries" for update
142
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write')
143
+ with check ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
144
+
145
+ drop policy if exists "smoodly_delete" on "published_entries";
146
+ create policy "smoodly_delete" on "published_entries" for delete
147
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
148
+
149
+ drop policy if exists "smoodly_read" on "published_entries";
150
+ create policy "smoodly_read" on "published_entries" for select
151
+ using ("space_id" = (select "smoodly_current_space"()) and not (select "smoodly_space_key_revoked"()) and (select "smoodly_key_kind"()) = 'write');
152
+
153
+ insert into "smoodly_meta" ("key", "value") values ('schema_version', '8')
154
+ on conflict ("key") do update set "value" = excluded."value";
@@ -0,0 +1,30 @@
1
+ -- Smoodly core schema, version 9 (packages/smoodly/MAINTAINERS.md, register 2026-10-07: what the delivery model left).
2
+ -- Shipped in the smoodly package; `npx smoodly migrate` copies it into supabase/migrations/.
3
+ -- Never edit a published file: the next change is the next file.
4
+ -- The delta over version 8: smoodly_lookup_address, the route row and the
5
+ -- locale's global items in one call, so a live page is one database call
6
+ -- instead of the page then its layout's globals.
7
+
8
+ set client_min_messages = warning;
9
+
10
+ -- The address and the locale's global items in one call (schema v9; register 2026-10-07: a miss was two
11
+ -- rounds, the page then the layout's globals). The route row first, kind 'page' or 'entry'; then one row per
12
+ -- named global, kind 'global', the collection its name. A miss answers nothing: the globals ride with a page
13
+ -- only. Invoker rights, so RLS decides what a key sees.
14
+ create or replace function "smoodly_lookup_address"(p_locale text, p_path text, p_globals text[])
15
+ returns table ("kind" text, "id" uuid, "collection" text, "document" jsonb, "model" text, "stale" boolean, "tags" text[])
16
+ language sql stable set search_path = public as $$
17
+ with route as (
18
+ select r."kind", r."id", r."collection", r."document", r."model", r."stale", r."tags"
19
+ from "published_routes" r where r."locale" = p_locale and r."path" = p_path limit 1
20
+ )
21
+ select * from route
22
+ union all
23
+ select 'global' as "kind", e."entry_id", e."collection", e."document", e."model", e."stale", e."tags"
24
+ from "published_entries" e
25
+ where e."locale" = p_locale and e."collection" = any(coalesce(p_globals, '{}'::text[]))
26
+ and exists (select 1 from route);
27
+ $$;
28
+
29
+ insert into "smoodly_meta" ("key", "value") values ('schema_version', '9')
30
+ on conflict ("key") do update set "value" = excluded."value";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "smoodly",
3
- "version": "0.0.44",
4
- "description": "Open-source, code-first CMS for Next.js on Supabase: define a component once, it becomes a typed, editable building block.",
3
+ "version": "0.0.46",
4
+ "description": "Open-source, code-first CMS for Astro and Next.js on Supabase: define a component once, it becomes a typed, editable building block.",
5
5
  "license": "MIT",
6
6
  "repository": {
7
7
  "type": "git",
@@ -19,10 +19,30 @@
19
19
  "types": "./dist/index.d.ts",
20
20
  "default": "./dist/index.js"
21
21
  },
22
+ "./react": {
23
+ "types": "./dist/react.d.ts",
24
+ "default": "./dist/react.js"
25
+ },
22
26
  "./next": {
23
27
  "types": "./dist/next/index.d.ts",
24
28
  "default": "./dist/next/index.js"
25
29
  },
30
+ "./astro": {
31
+ "types": "./dist/astro/index.d.ts",
32
+ "default": "./dist/astro/index.js"
33
+ },
34
+ "./astro/runtime": {
35
+ "types": "./dist/astro/runtime.d.ts",
36
+ "default": "./dist/astro/runtime.js"
37
+ },
38
+ "./astro/components": {
39
+ "types": "./dist/astro/components.d.ts",
40
+ "default": "./dist/astro/components.js"
41
+ },
42
+ "./astro/content": {
43
+ "types": "./dist/astro/content.d.ts",
44
+ "default": "./dist/astro/content.js"
45
+ },
26
46
  "./image": {
27
47
  "types": "./dist/image/index.d.ts",
28
48
  "default": "./dist/image/index.js"
@@ -39,10 +59,6 @@
39
59
  "types": "./dist/admin/index.d.ts",
40
60
  "default": "./dist/admin/index.js"
41
61
  },
42
- "./admin/next": {
43
- "types": "./dist/admin/next/index.d.ts",
44
- "default": "./dist/admin/next/index.js"
45
- },
46
62
  "./migrations": {
47
63
  "types": "./dist/migrations.d.ts",
48
64
  "default": "./dist/migrations.js"
@@ -54,35 +70,33 @@
54
70
  "bin"
55
71
  ],
56
72
  "scripts": {
57
- "build": "node scripts/write-version.mjs && rm -rf dist && tsc -p tsconfig.build.json",
73
+ "build:admin": "tsx scripts/build-admin.ts",
74
+ "build": "node scripts/write-version.mjs && rm -rf dist && tsc -p tsconfig.build.json && node scripts/copy-astro.mjs && npm run build:admin",
58
75
  "prepublishOnly": "npm run build",
59
- "pretest": "node scripts/write-version.mjs",
76
+ "pretest": "node scripts/write-version.mjs && npm run -s build:admin",
60
77
  "test": "vitest run",
61
78
  "test:watch": "vitest",
62
79
  "typecheck": "tsc --noEmit",
63
80
  "test:supabase": "bash scripts/test-supabase.sh"
64
81
  },
65
82
  "peerDependencies": {
83
+ "astro": "^7.3.5",
66
84
  "next": ">=16",
67
85
  "react": "^18 || ^19"
68
86
  },
69
87
  "peerDependenciesMeta": {
88
+ "astro": {
89
+ "optional": true
90
+ },
70
91
  "next": {
71
92
  "optional": true
93
+ },
94
+ "react": {
95
+ "optional": true
72
96
  }
73
97
  },
74
98
  "devDependencies": {
75
- "@types/node": "^26.4.1",
76
- "@types/react": "^19",
77
- "@types/react-dom": "^19.2.5",
78
- "next": "^16.3.4",
79
- "react": "^19",
80
- "react-dom": "^19",
81
- "typescript": "^5",
82
- "vitest": "^3"
83
- },
84
- "dependencies": {
85
- "@supabase/supabase-js": "^2.113.0",
99
+ "@fontsource-variable/public-sans": "^5.3.0",
86
100
  "@tiptap/core": "3.31.4",
87
101
  "@tiptap/extension-blockquote": "3.31.4",
88
102
  "@tiptap/extension-bold": "3.31.4",
@@ -96,7 +110,20 @@
96
110
  "@tiptap/react": "3.31.4",
97
111
  "@tiptap/starter-kit": "3.31.4",
98
112
  "@tiptap/suggestion": "3.31.4",
113
+ "@types/node": "^26.4.1",
114
+ "@types/react": "^19",
115
+ "@types/react-dom": "^19.2.5",
116
+ "astro": "^7.3.5",
117
+ "esbuild": "^0.28.2",
99
118
  "lucide-react": "^1.41.0",
119
+ "next": "^16.3.4",
120
+ "react": "^19",
121
+ "react-dom": "^19",
122
+ "typescript": "^5",
123
+ "vitest": "^3"
124
+ },
125
+ "dependencies": {
126
+ "@supabase/supabase-js": "^2.113.0",
100
127
  "tsx": "^4.23.15"
101
128
  }
102
129
  }
@@ -1,2 +0,0 @@
1
- import { type ReactElement } from "react";
2
- export declare function EditorBridge(): ReactElement;
@@ -1,129 +0,0 @@
1
- "use client";
2
- import { jsx as _jsx } from "react/jsx-runtime";
3
- // Runs INSIDE the draft-route iframe (glue adds it only when draft mode +
4
- // the editor param are on). Reports geometry — section nodes, empty zones
5
- // and, since spec 2026-09-10, every marked field with its resolved path —
6
- // forwards select/hover with the field under the pointer, executes
7
- // refresh/scroll, and says when a render has committed. The admin overlay
8
- // draws all chrome — bar one rule this component renders: a section that
9
- // renders nothing (a fresh custom section, an all-optional section left
10
- // empty) is stamped blank and takes a min-height, so the editor can see
11
- // and select it; the live site never mounts the bridge, so it keeps its
12
- // true height. Rects are in document coordinates: the iframe is sized
13
- // to the full document height by the editor, so client rects + scroll
14
- // offsets map 1:1 onto the overlay.
15
- import { useEffect } from "react";
16
- import { useRouter } from "next/navigation";
17
- import { BLANK_ATTR, CRASHED_ATTR, EDITOR_ATTR, FIELD_ATTR, NODE_ATTR, ZONE_ATTR, crashedOf, resolveMarkPath } from "../../marks.js";
18
- const post = (m) => window.parent.postMessage(m, "*");
19
- /** The height of an md-spaced section with nothing inside (3.5rem + 3.5rem). */
20
- // The editor sizes the iframe to the reported height; the document's own
21
- // viewport must never scroll (2026-09-26): a fractional content height
22
- // (4595.3px in a 4595px frame) left it scrollable by a hair, and a
23
- // trackpad scroll over the canvas showed the frame's overlay scrollbar.
24
- // Hidden overflow drops no layout — programmatic scrollIntoView still
25
- // works, and the live site never mounts this.
26
- const BRIDGE_CSS = `html{overflow:hidden}[${NODE_ATTR}][${BLANK_ATTR}]{min-height:7rem}`;
27
- const MEDIA = "img,picture,video,audio,svg,canvas,iframe,object";
28
- /** No visible text (innerText skips <style> and hidden content) and no media. */
29
- const isBlank = (el) => el.innerText.trim() === "" && el.querySelector(MEDIA) === null;
30
- export function EditorBridge() {
31
- const router = useRouter();
32
- // Every commit of this component — the mount, then each RSC refresh,
33
- // which re-renders the route — tells the editor the new markup is in
34
- // the DOM (spec 2026-09-10 §5: the inline richtext editor lets go of
35
- // its container only then).
36
- useEffect(() => { post({ source: "smoodly", kind: "rendered" }); });
37
- useEffect(() => {
38
- const docRect = (el) => {
39
- const r = el.getBoundingClientRect();
40
- return { x: r.left + window.scrollX, y: r.top + window.scrollY, w: r.width, h: r.height };
41
- };
42
- const all = (selector) => Array.from(document.querySelectorAll(selector));
43
- const inEditor = (el) => Boolean(el?.closest?.(`[${EDITOR_ATTR}]`));
44
- const report = () => {
45
- // Array.from, not spread: the package's lib set is ES2020 + DOM
46
- // (no DOM.Iterable), so a NodeList isn't statically iterable here.
47
- const roots = all(`[${NODE_ATTR}]`);
48
- // Stamp before measuring, so the min-height is in the rects; only on a
49
- // change, so the attribute mutation this causes converges instead of looping.
50
- for (const el of roots) {
51
- const blank = isBlank(el);
52
- if (blank !== el.hasAttribute(BLANK_ATTR)) {
53
- if (blank)
54
- el.setAttribute(BLANK_ATTR, "");
55
- else
56
- el.removeAttribute(BLANK_ATTR);
57
- }
58
- }
59
- const nodes = roots.map((el) => ({ id: el.getAttribute(NODE_ATTR), ...docRect(el) }));
60
- const emptyZones = all(`[${ZONE_ATTR}]`).map((el) => ({ zone: el.getAttribute(ZONE_ATTR), ...docRect(el) }));
61
- const fields = [];
62
- for (const el of all(`[${FIELD_ATTR}]`)) {
63
- // A mark inside the admin's own editor root (a nested element card) is not the page's.
64
- if (inEditor(el))
65
- continue;
66
- const mark = resolveMarkPath(el);
67
- if (mark)
68
- fields.push({ ...mark, ...docRect(el) });
69
- }
70
- // Rounded up from the fractional box, so the frame is never a hair too short.
71
- const docH = Math.ceil(Math.max(document.documentElement.scrollHeight, document.documentElement.getBoundingClientRect().height));
72
- // Crashed sections (spec 2026-09-27 section guard §6): the placeholders the guard and the canvas net left.
73
- const crashed = crashedOf(all(`[${CRASHED_ATTR}]`));
74
- post({ source: "smoodly", kind: "rects", nodes, emptyZones, fields, docH, crashed });
75
- };
76
- const raf = () => requestAnimationFrame(report);
77
- const ro = new ResizeObserver(raf);
78
- ro.observe(document.documentElement);
79
- // characterData too: a text session edits a text node in place, and the outline must follow.
80
- const mo = new MutationObserver(raf);
81
- mo.observe(document.body, { subtree: true, childList: true, attributes: true, characterData: true });
82
- const elementOf = (target) => target;
83
- const nodeIdAt = (target) => elementOf(target)?.closest?.(`[${NODE_ATTR}]`)?.getAttribute(NODE_ATTR) ?? null;
84
- const pathAt = (target) => {
85
- const el = elementOf(target)?.closest?.(`[${FIELD_ATTR}]`);
86
- return el ? resolveMarkPath(el)?.path ?? null : null;
87
- };
88
- const onClick = (e) => {
89
- // The canvas must never navigate away from under the editor.
90
- if (elementOf(e.target)?.closest?.("a"))
91
- e.preventDefault();
92
- // Inside its own editor root the admin owns every click.
93
- if (inEditor(elementOf(e.target)))
94
- return;
95
- post({ source: "smoodly", kind: "select", id: nodeIdAt(e.target), path: pathAt(e.target), x: e.clientX, y: e.clientY });
96
- };
97
- const onMove = (e) => {
98
- if (inEditor(elementOf(e.target)))
99
- return;
100
- post({ source: "smoodly", kind: "hover", id: nodeIdAt(e.target), path: pathAt(e.target) });
101
- };
102
- const onMessage = (e) => {
103
- const m = e.data;
104
- if (m?.source !== "smoodly")
105
- return;
106
- if (m.kind === "refresh")
107
- router.refresh();
108
- if (m.kind === "scroll") {
109
- document
110
- .querySelector(`[${NODE_ATTR}="${CSS.escape(m.id)}"]`)
111
- ?.scrollIntoView({ behavior: "smooth", block: "center" });
112
- }
113
- };
114
- document.addEventListener("click", onClick, true);
115
- document.addEventListener("mousemove", onMove, true);
116
- window.addEventListener("message", onMessage);
117
- report();
118
- const late = setTimeout(report, 500); // fonts/images settle
119
- return () => {
120
- ro.disconnect();
121
- mo.disconnect();
122
- clearTimeout(late);
123
- document.removeEventListener("click", onClick, true);
124
- document.removeEventListener("mousemove", onMove, true);
125
- window.removeEventListener("message", onMessage);
126
- };
127
- }, [router]);
128
- return _jsx("style", { "data-smoodly-bridge": "", children: BRIDGE_CSS });
129
- }
@@ -1,19 +0,0 @@
1
- import type { ReactNode } from "react";
2
- import type { ResolvedConfig } from "../../config.ts";
3
- import type { AdminOpAction } from "../ops.ts";
4
- import type { AuthConfig } from "../shell/session.ts";
5
- import type { PresenceConfig } from "../../env.ts";
6
- /** `presence` (optional) is the content project's public config for editing presence (spec 2026-09-26
7
- * concurrency §5; presence only, never content reads, spec 2026-09-29 content API contract §8), async because its space is asked of the database; absent or failed, presence
8
- * is off and everything else works. */
9
- export declare function createSmoodlyAdmin(input: {
10
- config: ResolvedConfig;
11
- op: AdminOpAction;
12
- auth: () => AuthConfig;
13
- presence?: () => PresenceConfig | Promise<PresenceConfig>;
14
- }): {
15
- AdminPage: () => null;
16
- AdminLayout: ({ children }: {
17
- children: ReactNode;
18
- }) => Promise<import("react").JSX.Element>;
19
- };
@@ -1,44 +0,0 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { serializeAdminConfig } from "../serialize.js";
3
- import { AdminApp } from "../shell/AdminApp.js";
4
- import { AdminStyles } from "../ui/theme.js";
5
- /** `presence` (optional) is the content project's public config for editing presence (spec 2026-09-26
6
- * concurrency §5; presence only, never content reads, spec 2026-09-29 content API contract §8), async because its space is asked of the database; absent or failed, presence
7
- * is off and everything else works. */
8
- export function createSmoodlyAdmin(input) {
9
- const registry = serializeAdminConfig(input.config);
10
- /** The catch-all page only makes every /admin URL exist. The shell is
11
- * mounted by the layout, not here: a server action that revalidates
12
- * (publish, delete, a save of published content) answers with a
13
- * re-rendered tree for the URL it was posted to, and Next remounts a
14
- * page whose dynamic segment changed since the document load, while a
15
- * layout persists (spec 2026-09-08 client routing, §1 amendment). */
16
- function AdminPage() {
17
- return null;
18
- }
19
- /** The admin owns the viewport: the whole UI is a fixed three-column
20
- * shell that scrolls internally, never the host document. The design
21
- * system's tokens ride along in one <style> — the host app imports
22
- * nothing (see ui/theme.tsx). The shell reads its route from the URL
23
- * on mount (shell/router.ts); `children` is the page's nothing.
24
- * suppressHydrationWarning: the theme boot script sets `data-theme`
25
- * on this element before React hydrates, on purpose (no white flash
26
- * for a dark-mode editor), so the attribute differs from the server's. */
27
- async function AdminLayout({ children }) {
28
- // Presence is the one thing that needs it: a failed lookup (an unmigrated project, an
29
- // unreachable database) is logged and turns presence off, so the shell still reaches
30
- // sign-in, the schema notice or the admin-path ErrorPane.
31
- // Auth first: an admin that cannot start (a missing publishable key) fails on its own
32
- // error, not after a presence line that suggests it runs without presence.
33
- const auth = input.auth();
34
- let presenceConfig;
35
- try {
36
- presenceConfig = await input.presence?.();
37
- }
38
- catch (e) {
39
- console.error("smoodly: editing presence is off —", e instanceof Error ? e.message : e);
40
- }
41
- return (_jsxs("div", { className: "sm-admin", suppressHydrationWarning: true, children: [_jsx(AdminStyles, {}), _jsx(AdminApp, { registry: registry, op: input.op, auth: auth, presenceConfig: presenceConfig }), children] }));
42
- }
43
- return { AdminPage, AdminLayout };
44
- }
@@ -1,8 +0,0 @@
1
- export { createAdminOpHandler } from "./op-handler.ts";
2
- export { createSmoodlyAdmin } from "./create-admin.tsx";
3
- export type { AdminOpCall, AdminOpAction, AdminOps, OpResult, MembersListResult } from "../ops.ts";
4
- export type { InviteInput, InviteResult, MemberRecord } from "../../member-store.ts";
5
- export type { MemberRole } from "../../permissions.ts";
6
- export type { AuthConfig } from "../shell/session.ts";
7
- export type { PresenceConfig } from "../../env.ts";
8
- export type { AdminAuth } from "../auth.ts";
@@ -1,8 +0,0 @@
1
- // The admin route group's entry. createSmoodlyAdmin imports the "use
2
- // client" shell, so any module that imports a value from here puts the
3
- // whole admin in its route's JavaScript — whether it renders or not.
4
- // Never import it from code the site's routes reach (smoodly/server.ts
5
- // included); the auth glue lives in `smoodly/admin`, which has no client
6
- // code.
7
- export { createAdminOpHandler } from "./op-handler.js";
8
- export { createSmoodlyAdmin } from "./create-admin.js";
@@ -1,18 +0,0 @@
1
- import type { ResolvedConfig } from "../../config.ts";
2
- import type { EntryStore } from "../../entry-store.ts";
3
- import type { PageStore } from "../../store.ts";
4
- import type { AssetStore } from "../../asset-store.ts";
5
- import type { MemberStore } from "../../member-store.ts";
6
- import { type AdminAuth } from "../auth.ts";
7
- import type { AdminOpAction } from "../ops.ts";
8
- export declare function createAdminOpHandler(deps: {
9
- config: ResolvedConfig;
10
- pages: PageStore;
11
- entries: EntryStore;
12
- auth: AdminAuth;
13
- href?: (path: string, locale: string) => string;
14
- /** The media library; without it the assets ops answer "no media store". */
15
- assets?: AssetStore;
16
- /** The admission table; without it members.list answers "no member store". */
17
- members?: MemberStore;
18
- }): AdminOpAction;
@@ -1,31 +0,0 @@
1
- // packages/smoodly/src/admin/next/op-handler.ts
2
- // The Next face of the ops seam: ONE server-action body, gated by
3
- // AdminAuth. The app's 'use server' file wraps this in its single
4
- // exported async function.
5
- import { updateTag } from "next/cache";
6
- import { draftMode } from "next/headers";
7
- import { gateAdminOp } from "../auth.js";
8
- import { createAdminOps, runAdminOp } from "../ops-impl.js";
9
- export function createAdminOpHandler(deps) {
10
- const base = {
11
- config: deps.config,
12
- pages: deps.pages,
13
- entries: deps.entries,
14
- href: deps.href,
15
- assets: deps.assets,
16
- members: deps.members,
17
- effects: {
18
- // updateTag, not revalidateTag(tag, profile): the profile form is
19
- // stale-while-revalidate, so the first request after publish still
20
- // served the cached null (a 404) and only the second saw the page.
21
- // updateTag expires now — Server Actions only, which this is.
22
- revalidate: (tag) => updateTag(tag),
23
- draft: {
24
- enable: async () => (await draftMode()).enable(),
25
- disable: async () => (await draftMode()).disable(),
26
- },
27
- },
28
- };
29
- // Ops are built per call so the verified session rides into every store write as `by` (spec 2026-09-13 history §2).
30
- return (call) => gateAdminOp(deps.auth, call, (c, session) => runAdminOp(createAdminOps({ ...base, actor: session }), c));
31
- }