smoodly 0.0.51 → 0.0.52

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 (285) hide show
  1. package/README.md +14 -13
  2. package/dist/admin/auth-config.d.ts +6 -5
  3. package/dist/admin/auth.d.ts +3 -11
  4. package/dist/admin/auth.js +1 -30
  5. package/dist/admin/browser/fetch-op.js +3 -2
  6. package/dist/admin/browser/main.js +6 -2
  7. package/dist/admin/client-ops.d.ts +5 -4
  8. package/dist/admin/client-ops.js +14 -11
  9. package/dist/admin/delivery-mode.d.ts +1 -1
  10. package/dist/admin/delivery-mode.js +2 -2
  11. package/dist/admin/drift.d.ts +0 -2
  12. package/dist/admin/drift.js +1 -1
  13. package/dist/admin/editor/EditorView.d.ts +0 -3
  14. package/dist/admin/editor/EditorView.js +11 -16
  15. package/dist/admin/editor/FloatingToolbar.d.ts +0 -1
  16. package/dist/admin/editor/FloatingToolbar.js +1 -1
  17. package/dist/admin/editor/SeoSettings.d.ts +0 -2
  18. package/dist/admin/editor/SeoSettings.js +2 -2
  19. package/dist/admin/editor/canvas-address.d.ts +8 -0
  20. package/dist/admin/editor/canvas-address.js +18 -2
  21. package/dist/admin/editor/canvas-diagnosis.d.ts +2 -0
  22. package/dist/admin/editor/canvas-diagnosis.js +3 -0
  23. package/dist/admin/editor/inline-dom.d.ts +0 -4
  24. package/dist/admin/editor/inline-dom.js +1 -1
  25. package/dist/admin/editor/preview-grant.d.ts +6 -0
  26. package/dist/admin/editor/preview-grant.js +25 -0
  27. package/dist/admin/editor/useCanvasDiagnosis.js +2 -2
  28. package/dist/admin/files.generated.js +1 -1
  29. package/dist/admin/fixed-nodes.d.ts +0 -6
  30. package/dist/admin/fixed-nodes.js +1 -81
  31. package/dist/admin/forms/CropEditor.d.ts +4 -2
  32. package/dist/admin/forms/CropEditor.js +58 -5
  33. package/dist/admin/forms/ImagePreview.d.ts +3 -1
  34. package/dist/admin/forms/ImagePreview.js +9 -3
  35. package/dist/admin/forms/MediaGrid.js +2 -1
  36. package/dist/admin/forms/MediaPicker.js +9 -6
  37. package/dist/admin/forms/MediaWidget.js +11 -9
  38. package/dist/admin/forms/assets-context.d.ts +7 -3
  39. package/dist/admin/forms/assets-context.js +5 -3
  40. package/dist/admin/forms/crop-editor.d.ts +24 -2
  41. package/dist/admin/forms/crop-editor.js +36 -2
  42. package/dist/admin/forms/date-picker.d.ts +0 -1
  43. package/dist/admin/forms/date-picker.js +1 -1
  44. package/dist/admin/forms/tabs.d.ts +0 -1
  45. package/dist/admin/forms/tabs.js +1 -1
  46. package/dist/admin/forms/upload.d.ts +18 -11
  47. package/dist/admin/forms/upload.js +62 -24
  48. package/dist/admin/global-items.d.ts +0 -3
  49. package/dist/admin/global-items.js +0 -54
  50. package/dist/admin/handler.d.ts +22 -28
  51. package/dist/admin/handler.js +146 -47
  52. package/dist/admin/health.d.ts +0 -4
  53. package/dist/admin/index.d.ts +5 -8
  54. package/dist/admin/index.js +3 -6
  55. package/dist/admin/ops.d.ts +36 -22
  56. package/dist/admin/page-data.d.ts +14 -6
  57. package/dist/admin/page-data.js +1 -1
  58. package/dist/admin/session-flow.d.ts +43 -0
  59. package/dist/admin/session-flow.js +94 -0
  60. package/dist/admin/shell/AdminApp.d.ts +8 -3
  61. package/dist/admin/shell/AdminApp.js +37 -58
  62. package/dist/admin/shell/ContentHealthPage.js +1 -3
  63. package/dist/admin/shell/GlobalFields.d.ts +0 -3
  64. package/dist/admin/shell/GlobalFields.js +1 -1
  65. package/dist/admin/shell/IconRail.js +5 -4
  66. package/dist/admin/shell/MediaScreen.js +9 -6
  67. package/dist/admin/shell/MembersPage.d.ts +1 -2
  68. package/dist/admin/shell/MembersPage.js +19 -90
  69. package/dist/admin/shell/SettingsView.d.ts +1 -2
  70. package/dist/admin/shell/SettingsView.js +2 -2
  71. package/dist/admin/shell/SignedOutView.d.ts +5 -0
  72. package/dist/admin/shell/SignedOutView.js +9 -0
  73. package/dist/admin/shell/console-links.d.ts +5 -0
  74. package/dist/admin/shell/console-links.js +11 -0
  75. package/dist/admin/shell/members-list.d.ts +1 -14
  76. package/dist/admin/shell/members-list.js +2 -23
  77. package/dist/admin/shell/pages-tree.d.ts +0 -2
  78. package/dist/admin/shell/pages-tree.js +1 -1
  79. package/dist/admin/shell/presence-socket.d.ts +10 -0
  80. package/dist/admin/shell/presence-socket.js +191 -0
  81. package/dist/admin/shell/presence.d.ts +6 -18
  82. package/dist/admin/shell/presence.js +4 -11
  83. package/dist/admin/shell/session.d.ts +20 -12
  84. package/dist/admin/shell/session.js +113 -47
  85. package/dist/admin/ui/LocaleSwitcher.d.ts +0 -4
  86. package/dist/admin/ui/LocaleSwitcher.js +2 -2
  87. package/dist/admin/ui/primitives.d.ts +4 -14
  88. package/dist/admin/ui/primitives.js +5 -8
  89. package/dist/admin/ui/theme.js +13 -2
  90. package/dist/admin/validate.js +10 -1
  91. package/dist/api-client.d.ts +26 -0
  92. package/dist/api-client.js +107 -0
  93. package/dist/assemble.d.ts +4 -41
  94. package/dist/assemble.js +4 -302
  95. package/dist/asset-store.d.ts +28 -30
  96. package/dist/asset-store.js +26 -121
  97. package/dist/astro/components/Head.astro +4 -1
  98. package/dist/astro/components/Image.astro +12 -17
  99. package/dist/astro/components/Video.astro +5 -2
  100. package/dist/astro/head.d.ts +3 -0
  101. package/dist/astro/head.js +24 -0
  102. package/dist/astro/index.js +54 -25
  103. package/dist/astro/locale-paths.d.ts +5 -2
  104. package/dist/astro/locale-paths.js +10 -2
  105. package/dist/astro/routes/admin.js +9 -7
  106. package/dist/astro/runtime.d.ts +2 -1
  107. package/dist/astro/runtime.js +4 -3
  108. package/dist/astro/server.d.ts +7 -11
  109. package/dist/astro/server.js +22 -28
  110. package/dist/astro/shell.d.ts +2 -2
  111. package/dist/astro/static-media-node.d.ts +4 -0
  112. package/dist/astro/static-media-node.js +25 -0
  113. package/dist/astro/static-media.d.ts +17 -0
  114. package/dist/astro/static-media.js +55 -0
  115. package/dist/astro/static.d.ts +5 -10
  116. package/dist/astro/static.js +13 -29
  117. package/dist/canvas-marker.d.ts +14 -0
  118. package/dist/canvas-marker.js +36 -0
  119. package/dist/cli/api.d.ts +14 -0
  120. package/dist/cli/api.js +48 -0
  121. package/dist/cli/app-env.d.ts +2 -0
  122. package/dist/cli/app-env.js +20 -0
  123. package/dist/cli/export.d.ts +16 -2
  124. package/dist/cli/export.js +52 -35
  125. package/dist/cli/import.d.ts +7 -11
  126. package/dist/cli/import.js +50 -54
  127. package/dist/cli/load-config.d.ts +0 -1
  128. package/dist/cli/load-config.js +1 -1
  129. package/dist/cli/main.d.ts +4 -12
  130. package/dist/cli/main.js +26 -72
  131. package/dist/cli/push.d.ts +6 -0
  132. package/dist/cli/push.js +31 -0
  133. package/dist/collections.d.ts +0 -1
  134. package/dist/collections.js +1 -1
  135. package/dist/config.d.ts +20 -5
  136. package/dist/config.js +18 -1
  137. package/dist/content/content-api.d.ts +22 -23
  138. package/dist/content/content-api.js +46 -56
  139. package/dist/content/mime.d.ts +1 -0
  140. package/dist/content/mime.js +6 -0
  141. package/dist/deploy-store.d.ts +19 -0
  142. package/dist/deploy-store.js +1 -0
  143. package/dist/entry-store.d.ts +3 -89
  144. package/dist/entry-store.js +2 -550
  145. package/dist/env.d.ts +27 -20
  146. package/dist/env.js +38 -50
  147. package/dist/fields.d.ts +11 -3
  148. package/dist/fields.js +33 -2
  149. package/dist/global-id.d.ts +2 -0
  150. package/dist/global-id.js +4 -2
  151. package/dist/image/SmoodlyImage.d.ts +9 -5
  152. package/dist/image/SmoodlyImage.js +29 -15
  153. package/dist/image/SmoodlyVideo.d.ts +1 -1
  154. package/dist/image/SmoodlyVideo.js +3 -2
  155. package/dist/image/index.d.ts +2 -3
  156. package/dist/image/index.js +0 -2
  157. package/dist/image/markup.d.ts +53 -0
  158. package/dist/image/markup.js +111 -0
  159. package/dist/index.d.ts +12 -22
  160. package/dist/index.js +7 -20
  161. package/dist/media-cut.d.ts +44 -0
  162. package/dist/media-cut.js +70 -0
  163. package/dist/media.d.ts +57 -3
  164. package/dist/media.js +125 -15
  165. package/dist/member-store.d.ts +3 -22
  166. package/dist/member-store.js +1 -53
  167. package/dist/next/admin-route.d.ts +1 -1
  168. package/dist/next/admin-route.js +5 -3
  169. package/dist/next/meta.js +2 -3
  170. package/dist/next/page-renderer.d.ts +2 -2
  171. package/dist/next/page-renderer.js +22 -11
  172. package/dist/next/queries.d.ts +2 -2
  173. package/dist/path-index.d.ts +0 -24
  174. package/dist/path-index.js +0 -98
  175. package/dist/push.d.ts +30 -0
  176. package/dist/push.js +52 -0
  177. package/dist/read-counter.d.ts +2 -0
  178. package/dist/read-counter.js +6 -0
  179. package/dist/reads.d.ts +7 -7
  180. package/dist/reads.js +3 -0
  181. package/dist/registry-json.d.ts +11 -0
  182. package/dist/registry-json.js +81 -0
  183. package/dist/render.d.ts +0 -1
  184. package/dist/render.js +1 -1
  185. package/dist/resolve.d.ts +12 -6
  186. package/dist/resolve.js +11 -8
  187. package/dist/revalidate.d.ts +8 -1
  188. package/dist/revalidate.js +8 -1
  189. package/dist/richtext/index.d.ts +1 -1
  190. package/dist/section-wrapper.d.ts +1 -1
  191. package/dist/section-wrapper.js +0 -9
  192. package/dist/signed-message.d.ts +26 -0
  193. package/dist/signed-message.js +103 -0
  194. package/dist/site.d.ts +59 -18
  195. package/dist/site.js +21 -252
  196. package/dist/space-transfer.d.ts +5 -2
  197. package/dist/space-transfer.js +17 -5
  198. package/dist/store.d.ts +2 -80
  199. package/dist/store.js +3 -489
  200. package/dist/toolset.d.ts +2 -1
  201. package/dist/toolset.js +1 -1
  202. package/dist/version.d.ts +1 -1
  203. package/dist/version.js +1 -1
  204. package/package.json +4 -15
  205. package/dist/admin/ops-impl.d.ts +0 -51
  206. package/dist/admin/ops-impl.js +0 -1403
  207. package/dist/admin/shell/InviteModal.d.ts +0 -6
  208. package/dist/admin/shell/InviteModal.js +0 -28
  209. package/dist/admin/shell/LoginView.d.ts +0 -5
  210. package/dist/admin/shell/LoginView.js +0 -80
  211. package/dist/admin/shell/SchemaNotice.d.ts +0 -4
  212. package/dist/admin/shell/SchemaNotice.js +0 -4
  213. package/dist/admin/shell/SetPasswordView.d.ts +0 -7
  214. package/dist/admin/shell/SetPasswordView.js +0 -41
  215. package/dist/admin/shell/presence-realtime.d.ts +0 -3
  216. package/dist/admin/shell/presence-realtime.js +0 -32
  217. package/dist/admin/supabase-auth.d.ts +0 -7
  218. package/dist/admin/supabase-auth.js +0 -30
  219. package/dist/astro/deploy-deps.d.ts +0 -10
  220. package/dist/astro/deploy-deps.js +0 -8
  221. package/dist/astro/image.d.ts +0 -27
  222. package/dist/astro/image.js +0 -29
  223. package/dist/bind.d.ts +0 -19
  224. package/dist/bind.js +0 -34
  225. package/dist/cli/add-admin.d.ts +0 -11
  226. package/dist/cli/add-admin.js +0 -25
  227. package/dist/cli/load-collections.d.ts +0 -2
  228. package/dist/cli/load-collections.js +0 -27
  229. package/dist/cli/migrate.d.ts +0 -43
  230. package/dist/cli/migrate.js +0 -138
  231. package/dist/cli/rebuild.d.ts +0 -23
  232. package/dist/cli/rebuild.js +0 -22
  233. package/dist/cli/space-io.d.ts +0 -41
  234. package/dist/cli/space-io.js +0 -84
  235. package/dist/client.d.ts +0 -11
  236. package/dist/client.js +0 -54
  237. package/dist/current-space.d.ts +0 -4
  238. package/dist/current-space.js +0 -9
  239. package/dist/document-stamp.d.ts +0 -7
  240. package/dist/document-stamp.js +0 -48
  241. package/dist/image/loaders.d.ts +0 -15
  242. package/dist/image/loaders.js +0 -5
  243. package/dist/image/next-config.d.ts +0 -18
  244. package/dist/image/next-config.js +0 -32
  245. package/dist/image/remote-pattern.d.ts +0 -1
  246. package/dist/image/remote-pattern.js +0 -5
  247. package/dist/migrations.d.ts +0 -37
  248. package/dist/migrations.js +0 -73
  249. package/dist/project.d.ts +0 -88
  250. package/dist/project.js +0 -231
  251. package/dist/published.d.ts +0 -142
  252. package/dist/published.js +0 -164
  253. package/dist/ref-index.d.ts +0 -19
  254. package/dist/ref-index.js +0 -0
  255. package/dist/schema-read.d.ts +0 -23
  256. package/dist/schema-read.js +0 -50
  257. package/dist/schema-version.d.ts +0 -19
  258. package/dist/schema-version.js +0 -34
  259. package/dist/sql-space.d.ts +0 -44
  260. package/dist/sql-space.js +0 -289
  261. package/dist/sql.d.ts +0 -15
  262. package/dist/sql.js +0 -556
  263. package/dist/supabase-asset-store.d.ts +0 -21
  264. package/dist/supabase-asset-store.js +0 -170
  265. package/dist/supabase-entry-store.d.ts +0 -92
  266. package/dist/supabase-entry-store.js +0 -824
  267. package/dist/supabase-member-store.d.ts +0 -21
  268. package/dist/supabase-member-store.js +0 -96
  269. package/dist/supabase-path-index.d.ts +0 -30
  270. package/dist/supabase-path-index.js +0 -138
  271. package/dist/supabase-published-store.d.ts +0 -56
  272. package/dist/supabase-published-store.js +0 -314
  273. package/dist/supabase-store.d.ts +0 -103
  274. package/dist/supabase-store.js +0 -678
  275. package/migrations/20260926120000_smoodly_v1.sql +0 -535
  276. package/migrations/20260926130000_smoodly_v2.sql +0 -26
  277. package/migrations/20260926140000_smoodly_v3.sql +0 -38
  278. package/migrations/20260926150000_smoodly_v4.sql +0 -41
  279. package/migrations/20260929120000_smoodly_v5.sql +0 -56
  280. package/migrations/20260929135523_smoodly_v6.sql +0 -200
  281. package/migrations/20260930040956_smoodly_v7.sql +0 -84
  282. package/migrations/20261007120000_smoodly_v8.sql +0 -154
  283. package/migrations/20261007200000_smoodly_v9.sql +0 -30
  284. package/migrations/20261008120000_smoodly_v10.sql +0 -47
  285. package/migrations/20261009120000_smoodly_v11.sql +0 -49
package/dist/env.js CHANGED
@@ -1,50 +1,49 @@
1
- // The one config shape (spec 2026-09-04 §4/§6). Self-host sets three
2
- // names (the live site alone needs two); the cloud sets five. The two defaults are what keep a single
3
- // code path: a default for an absent value is not a mode branch.
4
- import { createSmoodlyClient } from "./client.js";
5
- import { readCurrentSpace } from "./current-space.js";
6
- const SITE_NEEDS = ["SMOODLY_URL", "SMOODLY_SECRET_KEY"];
7
- const ADMIN_NEEDS = [...SITE_NEEDS, "SMOODLY_PUBLISHABLE_KEY"];
1
+ // The one config shape (spec 2026-09-04 §4/§6; spec 2026-10-14 §1; spec 2026-10-16 the control plane §5). A site and
2
+ // its admin read over Smoodly's API with three names — the API's address (https://api.smoodly.io unless set), the
3
+ // space, the space's secret key (sk_) — and may hold the space's publishable key (pk_); the admin learns where editors
4
+ // sign in from the service (GET /v1/auth). Media addresses come with the content, so the site names no media host
5
+ // (spec 2026-10-18 media on R2 and Images §7).
6
+ const SITE_NEEDS = ["SMOODLY_SPACE_ID", "SMOODLY_SECRET_KEY"];
7
+ /** Smoodly's hosted API; SMOODLY_API_URL is set only for another address (the repo's loop, a staging service). */
8
+ export const DEFAULT_API_URL = "https://api.smoodly.io";
9
+ /** The admin learns where to sign in from the service, so it needs what the site needs. */
10
+ const ADMIN_NEEDS = SITE_NEEDS;
11
+ const NEEDS = { site: SITE_NEEDS, admin: ADMIN_NEEDS };
12
+ const isHttp = (v) => /^https?:\/\/[^/\s]+/.test(v);
8
13
  const DEPLOYING = "https://smoodly.io/docs/getting-started/deploying";
9
- const isJwt = (v) => v.split(".").length === 3;
10
- /** A legacy JWT key's role claim ("anon", "service_role"; "authenticated" for a cloud space key); null when it cannot be read. */
11
- function jwtRole(v) {
12
- if (!isJwt(v))
13
- return null;
14
- try {
15
- const role = JSON.parse(atob(v.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))).role;
16
- return typeof role === "string" ? role : null;
17
- }
18
- catch {
19
- return null;
20
- }
21
- }
22
- /** The live site reads with the URL and the secret key; the admin and presence also need the
23
- * publishable key, so their callers pass "admin" (spec 2026-09-30 tester findings §4). A key
24
- * in the wrong variable or of no known form is named here, not by a failed read later. */
14
+ /** A Smoodly key: its kind, the space it belongs to, the random part and the stamp (spec 2026-10-16 the control plane §1). */
15
+ const SMOODLY_KEY = (kind) => new RegExp(`^${kind}_sp_(eu_)?[a-z2-7]{20}_[a-z2-7]{32}_[a-z2-7]{16}$`);
16
+ /** The live site and its admin read over the API with its address, the space and the secret key (spec 2026-10-16 the
17
+ * control plane §5). A key in the wrong variable or of no known form is named here, not by a failed read later. */
25
18
  export function resolveSmoodlyEnv(env, need = "site") {
26
- const missing = (need === "admin" ? ADMIN_NEEDS : SITE_NEEDS).filter((name) => !env[name]);
19
+ const missing = NEEDS[need].filter((name) => !env[name]);
27
20
  if (missing.length > 0)
28
21
  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
22
  const secretKey = env.SMOODLY_SECRET_KEY;
30
23
  const publishableKey = env.SMOODLY_PUBLISHABLE_KEY ?? "";
31
- if (secretKey.startsWith("sb_publishable_") || jwtRole(secretKey) === "anon") {
32
- throw new Error("smoodly: SMOODLY_SECRET_KEY holds a publishable key — copy the secret (service-role) key from your Supabase project.");
24
+ if (SMOODLY_KEY("pk").test(secretKey)) {
25
+ throw new Error("smoodly: SMOODLY_SECRET_KEY holds the publishable key (pk_…) — copy the secret key (sk_…) from the space page.");
33
26
  }
34
- if (!secretKey.startsWith("sb_secret_") && !isJwt(secretKey)) {
35
- throw new Error("smoodly: SMOODLY_SECRET_KEY is neither an sb_secret_ key nor a JWT — copy the secret (service-role) key from your Supabase project.");
27
+ if (!SMOODLY_KEY("sk").test(secretKey)) {
28
+ throw new Error("smoodly: SMOODLY_SECRET_KEY is not a Smoodly secret key (sk_…) — copy it from the space page.");
36
29
  }
37
- if (publishableKey.startsWith("sb_secret_") || jwtRole(publishableKey) === "service_role") {
38
- throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY holds a secret key — it reaches the browser; copy the publishable (anon) key from your Supabase project.");
30
+ if (SMOODLY_KEY("sk").test(publishableKey)) {
31
+ throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY holds the secret key (sk_…) — it reaches the browser; copy the publishable key (pk_…) from the space page.");
39
32
  }
40
- if (publishableKey && !publishableKey.startsWith("sb_publishable_") && !isJwt(publishableKey)) {
41
- throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY is neither an sb_publishable_ key nor a JWT — copy the publishable (anon) key from your Supabase project.");
33
+ if (publishableKey && !SMOODLY_KEY("pk").test(publishableKey)) {
34
+ throw new Error("smoodly: SMOODLY_PUBLISHABLE_KEY is not a Smoodly publishable key (pk_…) — copy it from the space page.");
42
35
  }
43
- const url = env.SMOODLY_URL;
44
- if (!/^https?:\/\/[^/\s]+/.test(url)) {
45
- throw new Error(`smoodly: SMOODLY_URL "${url}" is not an http(s) address — copy the project URL from your Supabase project.`);
36
+ const apiUrl = (env.SMOODLY_API_URL || DEFAULT_API_URL).replace(/\/+$/, "");
37
+ if (!isHttp(apiUrl)) {
38
+ throw new Error(`smoodly: SMOODLY_API_URL "${apiUrl}" is not an http(s) address — the address of Smoodly's API (locally the service on http://localhost:8787; see Deploying: ${DEPLOYING}).`);
46
39
  }
47
- return { url, secretKey, publishableKey, authUrl: env.SMOODLY_AUTH_URL || url, authKey: env.SMOODLY_AUTH_KEY || publishableKey };
40
+ const spaceId = env.SMOODLY_SPACE_ID;
41
+ return { secretKey, publishableKey, apiUrl, spaceId };
42
+ }
43
+ /** What the API client is built from (createSmoodlyApi): the site's env, the secret key as the key. */
44
+ export function apiEnv(env = process.env) {
45
+ const e = resolveSmoodlyEnv(env, "site");
46
+ return { apiUrl: e.apiUrl, spaceId: e.spaceId, key: e.secretKey };
48
47
  }
49
48
  let siteUrlWarned = false;
50
49
  /** The site's own address for the root layout's `metadataBase`, so canonical, hreflang and social
@@ -68,18 +67,7 @@ export function siteUrl(env = process.env) {
68
67
  /** `resolveSmoodlyEnv(process.env)` — call lazily, never at module load,
69
68
  * so `next build` without env still succeeds. */
70
69
  export const smoodlyEnv = (need = "site") => resolveSmoodlyEnv(process.env, need);
71
- /** One lookup per process per content project and key: the space never
72
- * changes for a key. A failed lookup is not cached, so the next call retries. */
73
- const spaces = new Map();
74
- /** The presence config, with the space the database says the secret key acts in —
75
- * asked, never read out of the key, so it cannot disagree with where rows land. */
76
- export function presenceConfig(env, db) {
77
- const cacheKey = `${env.url}\n${env.secretKey}`;
78
- let space = spaces.get(cacheKey);
79
- if (!space) {
80
- space = readCurrentSpace(db ?? createSmoodlyClient(env));
81
- spaces.set(cacheKey, space);
82
- space.catch(() => spaces.delete(cacheKey));
83
- }
84
- return space.then((spaceId) => ({ url: env.url, key: env.publishableKey, spaceId }));
70
+ /** The presence config from the env: no lookup, the space is the site's own. */
71
+ export function presenceConfig(env) {
72
+ return { apiUrl: env.apiUrl, spaceId: env.spaceId };
85
73
  }
package/dist/fields.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type Toolset } from "./toolset.ts";
2
2
  import { type ResolvedFile, type ResolvedImage, type ResolvedVideo } from "./media.ts";
3
+ import { type ImageQuality } from "./media-cut.ts";
3
4
  import { type Condition, when } from "./conditions.ts";
4
5
  export type Descriptor = {
5
6
  type: string;
@@ -88,10 +89,17 @@ export declare const f: {
88
89
  datetime: () => FieldBuilder<string, {}>;
89
90
  number: () => FieldBuilder<number, {}>;
90
91
  /** `aspect`: one `"w:h"` or several the crop tool may lock to — a per-use
91
- * constraint, so the component knows its box before the image loads. */
92
- image: (opts?: {
92
+ * constraint, so the component knows its box before the image loads.
93
+ * `crops`: named crops for art direction, a ratio each (spec 2026-10-18 media on R2 and Images §6), framed by the
94
+ * editor in one tab each and placed by the view's `media`; the first is the field's main crop. Not with `aspect`.
95
+ * `quality` and `maxWidth`: this kind of image's cut quality and srcset cap, over the project's and under a view's
96
+ * (spec 2026-10-18 image quality and max width §1). */
97
+ image: <const C extends Record<string, string> = Record<string, string>>(opts?: {
93
98
  aspect?: string | string[];
94
- }) => FieldBuilder<ResolvedImage, {}>;
99
+ crops?: C;
100
+ quality?: ImageQuality;
101
+ maxWidth?: number;
102
+ }) => FieldBuilder<ResolvedImage<Extract<keyof C, string>>, {}>;
95
103
  video: () => FieldBuilder<ResolvedVideo, {}>;
96
104
  file: () => FieldBuilder<ResolvedFile, {}>;
97
105
  richtext: (opts?: {
package/dist/fields.js CHANGED
@@ -3,6 +3,7 @@
3
3
  // keyed by descriptor type. Chaining is immutable.
4
4
  import { DEFAULT_TOOLSET } from "./toolset.js";
5
5
  import { parseAspect } from "./media.js";
6
+ import { isImageQuality, isMaxWidth, qualityError } from "./media-cut.js";
6
7
  import { when } from "./conditions.js";
7
8
  /** Normalizes a schema's option list to objects; refuses an empty list,
8
9
  * since a choice with nothing to choose is a schema bug (spec 2026-09-10
@@ -86,9 +87,19 @@ export const f = {
86
87
  datetime: () => builder({ type: "datetime" }),
87
88
  number: () => builder({ type: "number" }),
88
89
  /** `aspect`: one `"w:h"` or several the crop tool may lock to — a per-use
89
- * constraint, so the component knows its box before the image loads. */
90
+ * constraint, so the component knows its box before the image loads.
91
+ * `crops`: named crops for art direction, a ratio each (spec 2026-10-18 media on R2 and Images §6), framed by the
92
+ * editor in one tab each and placed by the view's `media`; the first is the field's main crop. Not with `aspect`.
93
+ * `quality` and `maxWidth`: this kind of image's cut quality and srcset cap, over the project's and under a view's
94
+ * (spec 2026-10-18 image quality and max width §1). */
90
95
  image: (opts) => {
91
96
  const aspect = opts?.aspect;
97
+ const crops = opts?.crops;
98
+ const quality = opts?.quality, maxWidth = opts?.maxWidth;
99
+ if (quality !== undefined && !isImageQuality(quality))
100
+ throw new Error(qualityError(quality, "f.image quality"));
101
+ if (maxWidth !== undefined && !isMaxWidth(maxWidth))
102
+ throw new Error("smoodly: f.image maxWidth must be a positive whole number of pixels.");
92
103
  if (aspect !== undefined) {
93
104
  const list = Array.isArray(aspect) ? aspect : [aspect];
94
105
  if (list.length === 0)
@@ -96,7 +107,27 @@ export const f = {
96
107
  for (const a of list)
97
108
  parseAspect(a);
98
109
  }
99
- return builder({ type: "image", ...(aspect !== undefined ? { aspect } : {}) });
110
+ if (crops !== undefined) {
111
+ if (aspect !== undefined)
112
+ throw new Error("smoodly: f.image takes `crops` or `aspect`, not both — each named crop has its own ratio.");
113
+ const names = Object.keys(crops);
114
+ if (names.length === 0)
115
+ throw new Error('smoodly: f.image `crops` names no crop — name one or more, like { desktop: "16:9" }.');
116
+ for (const name of names) {
117
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(name))
118
+ throw new Error(`smoodly: f.image crop "${name}" is not a name — start with a lowercase letter, then letters and digits, like "mobile".`);
119
+ try {
120
+ parseAspect(crops[name]);
121
+ }
122
+ catch {
123
+ throw new Error(`smoodly: f.image crop "${name}": "${crops[name]}" is not an aspect — use "w:h", like "16:9".`);
124
+ }
125
+ }
126
+ }
127
+ return builder({
128
+ type: "image", ...(aspect !== undefined ? { aspect } : {}), ...(crops !== undefined ? { crops: { ...crops } } : {}),
129
+ ...(quality !== undefined ? { quality } : {}), ...(maxWidth !== undefined ? { maxWidth } : {}),
130
+ });
100
131
  },
101
132
  video: () => builder({ type: "video" }),
102
133
  file: () => builder({ type: "file" }),
@@ -1,3 +1,5 @@
1
+ /** The namespace a global item's id is derived in: fixed, so an export and an import of the same registry agree on the row. */
2
+ export declare const DEFAULT_SPACE_ID = "00000000-0000-0000-0000-000000000000";
1
3
  /** RFC 4122 §4.3 uuid v5: sha1(namespace bytes ‖ name bytes), version 5,
2
4
  * variant 10xx, first 16 bytes of the digest. */
3
5
  export declare function globalEntryId(space: string, name: string): Promise<string>;
package/dist/global-id.js CHANGED
@@ -7,11 +7,13 @@
7
7
  // share one table: a name-only id would make space B's footer collide
8
8
  // with space A's.
9
9
  //
10
- // No imports — Web Crypto (`globalThis.crypto.subtle`) exists in Node
10
+ // No imports — Web Crypto (`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
13
  // component does `import { smoodly } from "smoodly/react"`, which re-exports
14
14
  // the root, and a "use client" section's browser build would fail on `node:crypto`.
15
+ /** The namespace a global item's id is derived in: fixed, so an export and an import of the same registry agree on the row. */
16
+ export const DEFAULT_SPACE_ID = "00000000-0000-0000-0000-000000000000";
15
17
  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
18
  /** The namespace's 16 raw bytes — RFC 4122 hashes the bytes, not the text. */
17
19
  function uuidBytes(uuid) {
@@ -27,7 +29,7 @@ function uuidBytes(uuid) {
27
29
  * variant 10xx, first 16 bytes of the digest. */
28
30
  export async function globalEntryId(space, name) {
29
31
  const input = new Uint8Array([...uuidBytes(space), ...new TextEncoder().encode(name)]);
30
- const digest = new Uint8Array(await globalThis.crypto.subtle.digest("SHA-1", input));
32
+ const digest = new Uint8Array(await crypto.subtle.digest("SHA-1", input));
31
33
  const bytes = digest.subarray(0, 16);
32
34
  bytes[6] = (bytes[6] & 0x0f) | 0x50; // version 5 in the high nibble of octet 6
33
35
  bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 10xx in the top bits of octet 8
@@ -1,10 +1,12 @@
1
1
  import type { CSSProperties, ReactElement } from "react";
2
2
  import type { ResolvedImage } from "../media.ts";
3
- import type { ImageLoader } from "./loaders.ts";
3
+ import type { ImageQuality } from "../media-cut.ts";
4
4
  export type SmoodlyImageProps = {
5
5
  image: ResolvedImage;
6
- /** Overrides the value's aspect for this placement (`"21:9"`). */
6
+ /** Overrides the value's aspect for this placement (`"21:9"`): the cut is framed around the hotspot. */
7
7
  aspect?: string;
8
+ /** Where each named crop applies, in order: `{ mobile: "(max-width: 767px)" }` (spec 2026-10-18 media §6). */
9
+ media?: Record<string, string>;
8
10
  sizes?: string;
9
11
  priority?: boolean;
10
12
  loading?: "eager" | "lazy";
@@ -12,10 +14,12 @@ export type SmoodlyImageProps = {
12
14
  style?: CSSProperties;
13
15
  /** Overrides the resolved alt (rarely right — the editor owns it). */
14
16
  alt?: string;
15
- loader?: ImageLoader;
16
- unoptimized?: boolean;
17
+ /** This placement's cut quality (50, 60, 75, 85, 95 or 100) and srcset cap, over the field's and the project's (spec
18
+ * 2026-10-18 image quality and max width §1). */
19
+ quality?: ImageQuality;
20
+ maxWidth?: number;
17
21
  } & {
18
22
  /** Data attributes land on the wrapper box — a section's field mark (spec 2026-09-10 §1). */
19
23
  [attr: `data-${string}`]: string | undefined;
20
24
  };
21
- export declare function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized, ...data }: SmoodlyImageProps): ReactElement;
25
+ export declare function SmoodlyImage({ image, aspect, media, sizes, priority, loading, className, style, alt, quality, maxWidth, ...data }: SmoodlyImageProps): ReactElement;
@@ -1,16 +1,30 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- // <SmoodlyImage>: a box at the aspect (declared, chosen or natural), the
3
- // image inside positioned by crop and hotspot, dimensions set so nothing
4
- // shifts. Wraps next/image; the only file besides SmoodlyVideo that a
5
- // site imports from the media layer. Server-safe — no "use client".
6
- import Image from "next/image";
7
- import { boxAspectOf, frameFor } from "./crop-math.js";
8
- export function SmoodlyImage({ image, aspect, sizes, priority, loading, className, style, alt, loader, unoptimized, ...data }) {
9
- const box = boxAspectOf(image, aspect);
10
- const frame = loader?.handlesCrop ? { left: 0, top: 0, width: 100, height: 100 } : frameFor(image, box);
11
- const svg = image.mime === "image/svg+xml";
12
- return (_jsx("span", { className: className, ...data, style: { display: "block", position: "relative", overflow: "hidden", aspectRatio: String(box), ...style }, children: _jsx(Image, { src: image.url, alt: alt ?? image.alt, width: image.width, height: image.height, sizes: sizes, priority: priority, loading: loading, unoptimized: unoptimized ?? svg, ...(loader ? { loader: ({ width, quality }) => loader.url(image, { width, quality }) } : {}), style: {
13
- position: "absolute", left: `${frame.left}%`, top: `${frame.top}%`,
14
- width: `${frame.width}%`, height: `${frame.height}%`, maxWidth: "none",
15
- } }) }));
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { preconnect } from "react-dom";
3
+ import { imageMarkup } from "./markup.js";
4
+ /** `a:b;c-d:e` as React's style object, in the same order, so React writes the same string back. */
5
+ function styleObject(css) {
6
+ const out = {};
7
+ for (const decl of css.split(";")) {
8
+ const at = decl.indexOf(":");
9
+ if (at > 0)
10
+ out[decl.slice(0, at).replace(/-([a-z])/g, (_, c) => c.toUpperCase())] = decl.slice(at + 1);
11
+ }
12
+ return out;
13
+ }
14
+ function originOf(url) {
15
+ try {
16
+ return new URL(url).origin;
17
+ }
18
+ catch {
19
+ return null;
20
+ }
21
+ }
22
+ export function SmoodlyImage({ image, aspect, media, sizes, priority, loading, className, style, alt, quality, maxWidth, ...data }) {
23
+ const m = imageMarkup(image, { aspect, media, sizes, priority, loading, alt, quality, maxWidth });
24
+ const origin = originOf(image.cut ?? image.url);
25
+ if (origin)
26
+ preconnect(origin);
27
+ const i = m.img;
28
+ const img = (_jsx("img", { src: i.src, srcSet: i.srcset, sizes: i.sizes, alt: i.alt, width: i.width, height: i.height, loading: i.loading, fetchPriority: i.fetchpriority, decoding: i.decoding, style: styleObject(i.style) }));
29
+ return (_jsx("span", { className: className, ...data, style: { ...styleObject(m.box), ...style }, children: m.sources.length > 0 ? (_jsxs("picture", { children: [m.sources.map((s) => _jsx("source", { media: s.media, srcSet: s.srcset, sizes: s.sizes, width: s.width, height: s.height, type: s.type }, `${s.media}:${s.type}`)), img] })) : img }));
16
30
  }
@@ -3,5 +3,5 @@ import type { ResolvedVideo } from "../media.ts";
3
3
  export type SmoodlyVideoProps = {
4
4
  video: ResolvedVideo;
5
5
  } & Omit<VideoHTMLAttributes<HTMLVideoElement>, "src" | "poster" | "width" | "height">;
6
- /** A <video> over the bucket's CDN with its poster and size. No transcoding (spec §9). */
6
+ /** A <video> from the media host with its poster (a cut where there is one) and size. No transcoding (spec §9). */
7
7
  export declare function SmoodlyVideo({ video, ...rest }: SmoodlyVideoProps): ReactElement;
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- /** A <video> over the bucket's CDN with its poster and size. No transcoding (spec §9). */
2
+ import { posterUrl } from "./markup.js";
3
+ /** A <video> from the media host with its poster (a cut where there is one) and size. No transcoding (spec §9). */
3
4
  export function SmoodlyVideo({ video, ...rest }) {
4
- return (_jsx("video", { src: video.url, poster: video.poster?.url, width: video.width, height: video.height, playsInline: true, ...rest }));
5
+ return (_jsx("video", { src: video.url, poster: posterUrl(video), width: video.width, height: video.height, playsInline: true, ...rest }));
5
6
  }
@@ -1,6 +1,5 @@
1
1
  export { SmoodlyImage, type SmoodlyImageProps } from "./SmoodlyImage.tsx";
2
2
  export { SmoodlyVideo, type SmoodlyVideoProps } from "./SmoodlyVideo.tsx";
3
- export { imageUrl, nextLoader, type ImageLoader, type ImageUrlOptions } from "./loaders.ts";
4
3
  export { frameFor, boxAspectOf, type Frame } from "./crop-math.ts";
5
- export { smoodlyImages, isPrivateHost, type RemotePattern, type SmoodlyImagesConfig } from "./next-config.ts";
6
- export type { ResolvedImage, ResolvedVideo, ResolvedFile, Crop, Hotspot } from "../media.ts";
4
+ export type { ResolvedImage, ResolvedCrop, ResolvedVideo, ResolvedFile, Crop, Hotspot } from "../media.ts";
5
+ export type { ImageQuality } from "../media-cut.ts";
@@ -1,5 +1,3 @@
1
1
  export { SmoodlyImage } from "./SmoodlyImage.js";
2
2
  export { SmoodlyVideo } from "./SmoodlyVideo.js";
3
- export { imageUrl, nextLoader } from "./loaders.js";
4
3
  export { frameFor, boxAspectOf } from "./crop-math.js";
5
- export { smoodlyImages, isPrivateHost } from "./next-config.js";
@@ -0,0 +1,53 @@
1
+ import { type ResolvedImage, type ResolvedVideo } from "../media.ts";
2
+ import { type ImageQuality } from "../media-cut.ts";
3
+ export type ImageMarkupOptions = {
4
+ /** Overrides the value's ratio for this placement: the cut is framed in CSS around the hotspot. */
5
+ aspect?: string;
6
+ sizes?: string;
7
+ /** Where each named crop applies: `{ mobile: "(max-width: 767px)" }`, in order; the first match wins. */
8
+ media?: Record<string, string>;
9
+ priority?: boolean;
10
+ loading?: "eager" | "lazy";
11
+ /** Overrides the resolved alt (rarely right — the editor owns it). */
12
+ alt?: string;
13
+ /** This placement's cut quality and srcset cap, over the image's own (its field's or project's) and the default 75
14
+ * (spec 2026-10-18 image quality and max width §1). */
15
+ quality?: ImageQuality;
16
+ maxWidth?: number;
17
+ };
18
+ /** A <source>: `media` for a placed crop, `type` for a format a static build offers (astro/static-media.ts). */
19
+ export type ImageSource = {
20
+ media?: string;
21
+ srcset: string;
22
+ sizes?: string;
23
+ width: number;
24
+ height: number;
25
+ type?: string;
26
+ };
27
+ export type ImageTag = {
28
+ src: string;
29
+ srcset?: string;
30
+ sizes?: string;
31
+ alt: string;
32
+ width: number;
33
+ height: number;
34
+ loading: "eager" | "lazy";
35
+ fetchpriority?: "high";
36
+ decoding: "async";
37
+ style: string;
38
+ };
39
+ export type ImageMarkup = {
40
+ box: string;
41
+ sources: ImageSource[];
42
+ img: ImageTag;
43
+ };
44
+ export declare function imageMarkup(image: ResolvedImage, options: ImageMarkupOptions): ImageMarkup;
45
+ /** A video's poster: its cut at the src width and its own quality where there is one, else the original. */
46
+ export declare function posterUrl(video: ResolvedVideo): string | undefined;
47
+ /** The markup as HTML, attribute for attribute what React writes for <SmoodlyImage> (Astro's <Image> sets it as is),
48
+ * React's spellings included (`srcSet`: attribute names are case-insensitive in HTML). */
49
+ export declare function imageHtml(m: ImageMarkup, box?: {
50
+ class?: string;
51
+ style?: string;
52
+ data?: Record<string, string | undefined>;
53
+ }): string;
@@ -0,0 +1,111 @@
1
+ // One image markup for both frameworks (spec 2026-10-18 media on R2 and Images §7; plan (c) decision 5): what
2
+ // <SmoodlyImage> (React) and <Image> (Astro) render, decided here so both are only markup and a test holds them equal.
3
+ // Plain HTML, no optimizer and no Smoodly JavaScript: a box, an <img> whose srcset lists the cut's widths, or a
4
+ // <picture> with one <source> per crop the view places. Without a cut (locally, an SVG) the original is framed in CSS
5
+ // by crop and hotspot, today's path (crop-math.ts).
6
+ import { CENTER_HOTSPOT, clamp01, parseAspect } from "../media.js";
7
+ import { cutUrl, cutWidths, DEFAULT_QUALITY, isImageQuality, parseCutSegment, qualityError } from "../media-cut.js";
8
+ import { escapeHtml } from "../richtext-href.js";
9
+ import { boxAspectOf, frameFor } from "./crop-math.js";
10
+ /** The `src` of a cut: the widest kept width up to 1280, a browser with no srcset support's fair middle. */
11
+ const SRC_WIDTH = 1280;
12
+ const pct = (n) => `${Math.round(n * 1000) / 1000}%`;
13
+ const frameStyle = (f) => `position:absolute;left:${pct(f.left)};top:${pct(f.top)};width:${pct(f.width)};height:${pct(f.height)};max-width:none`;
14
+ const FULL = frameStyle({ left: 0, top: 0, width: 100, height: 100 });
15
+ const boxStyle = (aspect) => `display:block;position:relative;overflow:hidden;aspect-ratio:${aspect}`;
16
+ /** The rectangle a cut's address names (`…/<left>.<top>.<width>.<height>.<sig>`), or null for none. */
17
+ function rectOfCut(cut) {
18
+ return cut ? parseCutSegment(cut.slice(cut.lastIndexOf("/") + 1))?.rect ?? null : null;
19
+ }
20
+ function deliveryOf(image, options) {
21
+ const quality = options.quality ?? image.quality ?? DEFAULT_QUALITY;
22
+ if (!isImageQuality(quality))
23
+ throw new Error(qualityError(quality));
24
+ const maxWidth = options.maxWidth ?? image.maxWidth;
25
+ return { quality, ...(maxWidth !== undefined ? { maxWidth } : {}) };
26
+ }
27
+ function srcsetOf(cut, width, d) {
28
+ const widths = cutWidths(width, d.maxWidth);
29
+ const fit = widths.filter((w) => w <= SRC_WIDTH);
30
+ return {
31
+ src: cutUrl(cut, fit.length > 0 ? fit[fit.length - 1] : widths[0], d.quality),
32
+ srcset: widths.map((w) => `${cutUrl(cut, w, d.quality)} ${w}w`).join(", "),
33
+ };
34
+ }
35
+ /** A cut shown in a box of another ratio (the view's `aspect`): framed around the hotspot, as the original was. */
36
+ function frameOverCut(image, rect, box) {
37
+ const spot = image.hotspot ?? CENTER_HOTSPOT;
38
+ const hotspot = { x: clamp01((spot.x * image.width - rect.left) / rect.width), y: clamp01((spot.y * image.height - rect.top) / rect.height) };
39
+ return frameStyle(frameFor({ width: rect.width, height: rect.height, hotspot }, box));
40
+ }
41
+ export function imageMarkup(image, options) {
42
+ const loading = options.priority ? "eager" : options.loading ?? "lazy";
43
+ const tag = { alt: options.alt ?? image.alt, loading, ...(options.priority ? { fetchpriority: "high" } : {}), decoding: "async" };
44
+ const sizes = options.sizes ? { sizes: options.sizes } : {};
45
+ const rect = image.mime === "image/svg+xml" ? null : rectOfCut(image.cut);
46
+ const spot = image.hotspot ?? CENTER_HOTSPOT;
47
+ const delivery = deliveryOf(image, options);
48
+ // Art direction: a <source> per placed crop; each has its own shape, so the box takes it from the chosen source's
49
+ // width and height, and the image covers it (around the hotspot where there is no cut, locally).
50
+ const sources = [];
51
+ for (const [name, media] of Object.entries(options.media ?? {})) {
52
+ const crop = image.crops?.[name];
53
+ if (!crop)
54
+ continue;
55
+ const own = rectOfCut(crop.cut);
56
+ sources.push({ media, srcset: own && crop.cut ? srcsetOf(crop.cut, own.width, delivery).srcset : image.url, ...sizes, width: crop.width, height: crop.height });
57
+ }
58
+ if (sources.length > 0) {
59
+ const main = image.crops ? Object.values(image.crops)[0] : undefined;
60
+ const size = rect ? { width: rect.width, height: rect.height } : main ? { width: main.width, height: main.height } : { width: image.width, height: image.height };
61
+ return {
62
+ box: "display:block",
63
+ sources,
64
+ img: {
65
+ ...(rect && image.cut ? { ...srcsetOf(image.cut, rect.width, delivery), ...sizes } : { src: image.url }),
66
+ ...tag, ...size,
67
+ style: `display:block;width:100%;height:auto;object-fit:cover;object-position:${pct(spot.x * 100)} ${pct(spot.y * 100)}`,
68
+ },
69
+ };
70
+ }
71
+ if (rect && image.cut) {
72
+ const own = rect.width / rect.height;
73
+ const box = options.aspect ? parseAspect(options.aspect) : own;
74
+ return {
75
+ box: boxStyle(box),
76
+ sources,
77
+ img: {
78
+ ...srcsetOf(image.cut, rect.width, delivery), ...sizes, ...tag, width: rect.width, height: rect.height,
79
+ style: Math.abs(box / own - 1) < 0.005 ? FULL : frameOverCut(image, rect, box),
80
+ },
81
+ };
82
+ }
83
+ // No cut: the original, framed in CSS by crop and hotspot (an SVG fills its box as it is).
84
+ const box = boxAspectOf(image, options.aspect);
85
+ return {
86
+ box: boxStyle(box),
87
+ sources,
88
+ img: { src: image.url, ...tag, width: image.width, height: image.height, style: frameStyle(frameFor(image, box)) },
89
+ };
90
+ }
91
+ /** A video's poster: its cut at the src width and its own quality where there is one, else the original. */
92
+ export function posterUrl(video) {
93
+ const poster = video.poster;
94
+ if (!poster)
95
+ return undefined;
96
+ const rect = rectOfCut(poster.cut);
97
+ return rect && poster.cut ? srcsetOf(poster.cut, rect.width, deliveryOf(poster, {})).src : poster.url;
98
+ }
99
+ const attr = (name, value) => (value === undefined ? "" : ` ${name}="${escapeHtml(String(value))}"`);
100
+ /** The markup as HTML, attribute for attribute what React writes for <SmoodlyImage> (Astro's <Image> sets it as is),
101
+ * React's spellings included (`srcSet`: attribute names are case-insensitive in HTML). */
102
+ export function imageHtml(m, box = {}) {
103
+ const style = box.style ? `${m.box};${box.style}` : m.box;
104
+ const data = Object.entries(box.data ?? {}).map(([k, v]) => attr(k, v)).join("");
105
+ const i = m.img;
106
+ const img = `<img${attr("src", i.src)}${attr("srcSet", i.srcset)}${attr("sizes", i.sizes)}${attr("alt", i.alt)}${attr("width", i.width)}${attr("height", i.height)}`
107
+ + `${attr("loading", i.loading)}${attr("fetchPriority", i.fetchpriority)}${attr("decoding", i.decoding)}${attr("style", i.style)}/>`;
108
+ const sources = m.sources.map((s) => `<source${attr("media", s.media)}${attr("srcSet", s.srcset)}${attr("sizes", s.sizes)}${attr("width", s.width)}${attr("height", s.height)}${attr("type", s.type)}/>`).join("");
109
+ const inner = m.sources.length > 0 ? `<picture>${sources}${img}</picture>` : img;
110
+ return `<span${attr("class", box.class)}${data}${attr("style", style)}>${inner}</span>`;
111
+ }
package/dist/index.d.ts CHANGED
@@ -2,7 +2,6 @@ import { collection } from "./collections.ts";
2
2
  import { toolset } from "./toolset.ts";
3
3
  import { globalItem } from "./global.ts";
4
4
  import { marks } from "./marks.ts";
5
- export { createSupabaseStores, siteOptions } from "./bind.ts";
6
5
  export { schemaOf } from "./registration.ts";
7
6
  export type { SchemaOr } from "./registration.ts";
8
7
  export { EMPTY_PAGE_CONTEXT } from "./page-context.ts";
@@ -25,36 +24,24 @@ export { ASSET_COLLECTION, DEFAULT_MEDIA, MEDIA_TYPES, FULL_CROP, CENTER_HOTSPOT
25
24
  export type { Crop, Hotspot, AssetKind, ImageValue, VideoValue, FileValue, AssetMeta, AssetRecord, ResolvedImage, ResolvedVideo, ResolvedFile, } from "./media.ts";
26
25
  export { z } from "./zones.ts";
27
26
  export { defineConfig } from "./config.ts";
28
- export { collectionSQL, coreTablesSQL } from "./sql.ts";
29
- export { SCHEMA_VERSION, SCHEMA_VERSION_KEY, schemaVersionSQL, coreFileVersion, schemaBehind, schemaBehindMessage } from "./schema-version.ts";
30
27
  export { draftToken, sameToken, isDraftToken, assertFresh, StaleDraftError } from "./draft-token.ts";
31
28
  export type { DraftToken, DraftPointerRow } from "./draft-token.ts";
32
- export { MemoryPageStore } from "./store.ts";
33
- export { MemoryAssetStore, assetErrors, kindOf, extensionFor, objectPath, withDownload, PAGE_SIZE as ASSET_PAGE_SIZE } from "./asset-store.ts";
29
+ export { assetErrors, kindOf, extensionFor, objectPath, PAGE_SIZE as ASSET_PAGE_SIZE } from "./asset-store.ts";
34
30
  export type { AssetStore, UploadTicket, FinishInput, AssetQuery, AssetPage, BeginUploadInput, CreateAssetInput } from "./asset-store.ts";
35
31
  export { imageDimensions } from "./image-dimensions.ts";
36
- export { SupabasePageStore } from "./supabase-store.ts";
37
- export { SupabaseAssetStore } from "./supabase-asset-store.ts";
38
- export { MemoryMemberStore, sortMembers, memberErrors } from "./member-store.ts";
32
+ export type { ImageQuality } from "./media-cut.ts";
33
+ export { sortMembers, memberErrors } from "./member-store.ts";
39
34
  export type { MemberStore, MemberRecord, InviteInput, InviteResult } from "./member-store.ts";
40
- export { SupabaseMemberStore } from "./supabase-member-store.ts";
41
35
  export { MEMBER_ROLES, can, normalizeRole, isMemberRole } from "./permissions.ts";
42
36
  export type { MemberRole, Capability } from "./permissions.ts";
43
37
  export { collectEntryRefs, collectTreeRefs } from "./refs.ts";
44
38
  export type { RefEdge, SectionLike } from "./refs.ts";
45
39
  export { richtextBlocks, elementSchemas, IMAGE_NODE, ELEMENT_NODE } from "./richtext-walk.ts";
46
40
  export type { BlockNode, ElementSchemas, ImageNodeAttrs, ElementNodeAttrs } from "./richtext-walk.ts";
47
- export { MemoryRefIndex } from "./ref-index.ts";
48
- export { MemoryPathIndex } from "./path-index.ts";
49
41
  export type { PathIndex } from "./path-index.ts";
50
- export { MemoryEntryStore } from "./entry-store.ts";
51
- export { SupabaseEntryStore } from "./supabase-entry-store.ts";
52
- export { SupabasePathIndex } from "./supabase-path-index.ts";
53
42
  export type { EntryStore, EntryRecord, EntryLocale, EntryVersion, EntryScan, EntryUsage, ListOptions, CreateEntryOptions, AddEntryLocaleOptions, EntryStoreOptions, Where, WhereValue, SnapshotQuery, SnapshotRow, } from "./entry-store.ts";
54
43
  export { entryErrors, compareEntries, DEFAULT_ENTRY_LOCALES, localizedKeys, splitFields, entryLocaleRow, entryFieldsIn, entryRefEdges, entryTitleIn, matchesWhere, } from "./entry-store.ts";
55
- export { MemoryPublishedStore } from "./published.ts";
56
- export { SupabasePublishedStore } from "./supabase-published-store.ts";
57
- export type { PublishedStore, PublishedPageRow, PublishedEntryRow, PublishedRoute, PublishedTreeRow, PublishedQuery, PublishedKey } from "./published.ts";
44
+ export type { DeployStore } from "./deploy-store.ts";
58
45
  export { affectedTargets, pageTag, entryTag, globalTag, pagesTreeTag, collectionTag } from "./revalidate.ts";
59
46
  export type { AffectedTargets } from "./revalidate.ts";
60
47
  export { resolveTree, resolveFields, resolveMedia } from "./resolve.ts";
@@ -83,7 +70,7 @@ export { SMD_KEY } from "./smd-key.ts";
83
70
  export type { SmdInternal } from "./smd-key.ts";
84
71
  export { CSS_MAX, cssProblem, safeCss, scopedCss } from "./css.ts";
85
72
  export { globalEntryId } from "./global-id.ts";
86
- export { DEFAULT_SPACE_ID } from "./sql-space.ts";
73
+ export { DEFAULT_SPACE_ID } from "./global-id.ts";
87
74
  export type { GlobalSchema, GlobalLink } from "./global.ts";
88
75
  /** The registration namespace: schemas and their parts. The React pairing (`smoodly.section`, `<smoodly.Section>`) is smoodly/react's. */
89
76
  export declare const smoodly: {
@@ -127,8 +114,11 @@ export declare const smoodly: {
127
114
  };
128
115
  export { NODE_ATTR, ZONE_ATTR, EMPTY_ATTR, BLANK_ATTR, FIELD_ATTR, ITEM_ATTR, EDITOR_ATTR, resolveMarkPath } from "./marks.ts";
129
116
  export type { MarkNode, MarkPath, Marks, Scope } from "./marks.ts";
130
- export { resolveSmoodlyEnv, smoodlyEnv, presenceConfig, siteUrl } from "./env.ts";
117
+ export { apiEnv, resolveSmoodlyEnv, smoodlyEnv, presenceConfig, siteUrl } from "./env.ts";
118
+ export type { SmoodlyEnvNeed } from "./env.ts";
119
+ export { createSmoodlyApi, SmoodlyApiError } from "./api-client.ts";
120
+ export type { SmoodlyApiOptions } from "./api-client.ts";
131
121
  export type { SmoodlyEnv, PresenceConfig } from "./env.ts";
132
- export { clientOptions, createSmoodlyClient } from "./client.ts";
133
- export { createSmoodlySite, pageContextOf } from "./site.ts";
134
- export type { SiteOptions, SitePageSchema, SmoodlySite, LoadOptions, PageTreeItem, EntryTreeItem, EntryQuery, } from "./site.ts";
122
+ export { MESSAGE_MAX_AGE_MS, signMessage, verifyMessage } from "./signed-message.ts";
123
+ export { pageContextOf, siteStatics } from "./site.ts";
124
+ export type { SiteOptions, SitePageSchema, ContentReads, DeliveryReads, LoadOptions, PageTreeItem, EntryTreeItem, EntryQuery, } from "./site.ts";