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
@@ -1,22 +1,30 @@
1
- import { type Session, type SupabaseClient } from "@supabase/supabase-js";
2
- import type { AuthConfig } from "../auth-config.ts";
3
- export type { AuthConfig };
4
- export type Arrival = "invite" | "recovery";
5
- /** What an auth email link says it is: GoTrue's implicit flow puts `type=` in the fragment; a query form is accepted too. */
6
- export declare function arrivalOf(hash: string, search: string): Arrival | null;
1
+ import type { SignInConfig } from "../auth-config.ts";
2
+ import { type StoredSession } from "../session-flow.ts";
3
+ export type SessionUser = StoredSession["user"];
7
4
  export type SessionState = {
8
5
  status: "loading";
9
6
  } | {
10
7
  status: "signed_out";
8
+ reason: "signed_out" | "refused";
9
+ message?: string;
11
10
  } | {
12
11
  status: "signed_in";
13
- session: Session;
12
+ user: SessionUser;
14
13
  };
15
- export declare function useAdminSession(auth: AuthConfig): {
16
- client: SupabaseClient;
14
+ export declare function useSpaceSession({ apiUrl, spaceId, signIn }: {
15
+ apiUrl: string;
16
+ spaceId: string;
17
+ signIn: SignInConfig;
18
+ }): {
17
19
  state: SessionState;
20
+ /** The current pass, read per call. */
18
21
  token: () => string | null;
19
- /** The email link this visit arrived on, until the Set password screen is done. */
20
- arrival: Arrival | null;
21
- clearArrival: () => void;
22
+ /** Renews now (once at a time); true when a fresh pass is in hand. A refused refresh sends the person to sign in. */
23
+ renew: () => Promise<boolean>;
24
+ /** Ends this site's session; the signed-out page follows. */
25
+ signOut: () => Promise<void>;
26
+ /** Sends the person to sign in. */
27
+ signInAgain: () => Promise<void>;
28
+ /** Hears every new pass (a renewal here or in another tab). */
29
+ onRenew: (listener: (pass: string) => void) => () => void;
22
30
  };
@@ -1,54 +1,120 @@
1
1
  "use client";
2
- // The editor's session, browser side: one Supabase client for the AUTH
3
- // project (smoodly.io's in the cloud, the customer's own in self-host —
4
- // the app passes the URL and publishable key). The token getter reads a
5
- // ref so the ops facade, created once, always sends the current token.
6
- // An auth email link (invite, reset) says what it is in the URL before
7
- // the client consumes the fragment; the shell shows Set password for it
8
- // (spec 2026-09-25 members management §6).
9
- import { useEffect, useRef, useState } from "react";
10
- import { createClient } from "@supabase/supabase-js";
11
- /** What an auth email link says it is: GoTrue's implicit flow puts `type=` in the fragment; a query form is accepted too. */
12
- export function arrivalOf(hash, search) {
13
- const fromHash = new URLSearchParams(hash.replace(/^#/, "")).get("type");
14
- const type = fromHash ?? new URLSearchParams(search).get("type");
15
- return type === "invite" || type === "recovery" ? type : null;
16
- }
17
- export function useAdminSession(auth) {
18
- const browser = typeof window !== "undefined";
19
- // Read BEFORE the client exists: it consumes and strips the fragment as soon as it is constructed.
20
- const [arrival, setArrival] = useState(() => browser ? arrivalOf(window.location.hash, window.location.search) : null);
21
- const [client] = useState(() => createClient(auth.url, auth.key, {
22
- // Own storage key: a self-hoster's own supabase-js client in the same
23
- // browser must never share the admin's session slot (DESIGN.md §1).
24
- auth: { autoRefreshToken: browser, persistSession: browser, detectSessionInUrl: browser, storageKey: "smoodly-auth" },
25
- }));
2
+ // The editor's session for this site's space, browser side (spec 2026-10-16 sign-in and the dashboard §2, §6): no
3
+ // credentials are typed here. On load a one-time code in the fragment is exchanged and stripped; a stored session is
4
+ // used, renewed five minutes before its pass expires; with neither, the person is sent to sign in (smoodly.io, or the
5
+ // local Worker's dev page) and comes back with a code. A refused pass renews once, then sends them to sign in again;
6
+ // Sign out ends this site's session and shows the signed-out page rather than redirecting, which would sign them
7
+ // straight back in while their smoodly.io session lives. Another tab's renewal or sign out is adopted from the
8
+ // `storage` event. The pure steps are admin/session-flow.ts.
9
+ import { useCallback, useEffect, useRef, useState } from "react";
10
+ import { codeFrom, endSession, exchange, readSession, renew, RENEW_BEFORE_MS, signInAddress, STORAGE_KEY } from "../session-flow.js";
11
+ /** A renewal's timer fires up to this much later, so two tabs rarely post the same refresh at once. */
12
+ const JITTER_MS = 30000;
13
+ export function useSpaceSession({ apiUrl, spaceId, signIn }) {
26
14
  const [state, setState] = useState({ status: "loading" });
27
- const current = useRef(null);
15
+ const [expiresAt, setExpiresAt] = useState(null);
16
+ const held = useRef(null);
17
+ const listeners = useRef(new Set());
18
+ const inflight = useRef(null);
19
+ const stores = () => ({ local: window.localStorage, tab: window.sessionStorage });
20
+ const adopt = useCallback((s) => {
21
+ held.current = s;
22
+ setExpiresAt(s.expiresAt);
23
+ setState({ status: "signed_in", user: s.user });
24
+ for (const l of listeners.current)
25
+ l(s.pass);
26
+ }, []);
27
+ const signInAgain = useCallback(async () => {
28
+ const back = window.location.origin + window.location.pathname + window.location.search;
29
+ window.location.assign(await signInAddress(signIn, spaceId, back, stores()));
30
+ }, [signIn, spaceId]);
31
+ const renewNow = useCallback(() => {
32
+ // A tab with no session (signed out here or in another tab) renews nothing and never redirects by itself.
33
+ if (!held.current)
34
+ return Promise.resolve(false);
35
+ inflight.current ?? (inflight.current = (async () => {
36
+ try {
37
+ const s = await renew(apiUrl, spaceId, stores(), held.current?.refresh ?? null);
38
+ if (s) {
39
+ adopt(s);
40
+ return true;
41
+ }
42
+ held.current = null;
43
+ await signInAgain();
44
+ return false;
45
+ }
46
+ catch {
47
+ // Offline or the service down: the session stays; the next call or timer tries again.
48
+ return false;
49
+ }
50
+ finally {
51
+ inflight.current = null;
52
+ }
53
+ })());
54
+ return inflight.current;
55
+ }, [apiUrl, spaceId, adopt, signInAgain]);
28
56
  useEffect(() => {
29
- let active = true;
30
- const apply = (session) => {
31
- current.current = session;
32
- setState(session ? { status: "signed_in", session } : { status: "signed_out" });
33
- };
34
- client.auth.getSession().then(({ data }) => {
35
- if (!active)
57
+ let live = true;
58
+ void (async () => {
59
+ const code = codeFrom(window.location.hash);
60
+ if (code) {
61
+ // The code is good once: out of the address bar and the history entry before anything else.
62
+ window.history.replaceState(window.history.state, "", window.location.pathname + window.location.search);
63
+ const got = await exchange(apiUrl, spaceId, code, stores());
64
+ if (!live)
65
+ return;
66
+ if ("refused" in got)
67
+ setState({ status: "signed_out", reason: "refused", message: got.refused });
68
+ else
69
+ adopt(got);
36
70
  return;
37
- apply(data.session);
38
- }).catch(() => {
39
- if (!active)
71
+ }
72
+ const stored = readSession(window.localStorage, spaceId);
73
+ if (!stored) {
74
+ await signInAgain();
40
75
  return;
41
- apply(null);
42
- });
43
- const { data } = client.auth.onAuthStateChange((event, session) => {
44
- if (event === "PASSWORD_RECOVERY")
45
- setArrival("recovery");
46
- apply(session);
47
- });
48
- return () => {
49
- active = false;
50
- data.subscription.unsubscribe();
76
+ }
77
+ held.current = stored;
78
+ if (stored.expiresAt - Date.now() > RENEW_BEFORE_MS)
79
+ adopt(stored);
80
+ else
81
+ await renewNow();
82
+ })();
83
+ const onStorage = (e) => {
84
+ if (e.key !== STORAGE_KEY(spaceId))
85
+ return;
86
+ const s = readSession(window.localStorage, spaceId);
87
+ if (s) {
88
+ if (s.refresh !== held.current?.refresh)
89
+ adopt(s);
90
+ }
91
+ else if (held.current) {
92
+ held.current = null;
93
+ setState({ status: "signed_out", reason: "signed_out" });
94
+ }
51
95
  };
52
- }, [client]);
53
- return { client, state, token: () => current.current?.access_token ?? null, arrival, clearArrival: () => setArrival(null) };
96
+ window.addEventListener("storage", onStorage);
97
+ return () => { live = false; window.removeEventListener("storage", onStorage); };
98
+ }, []); // eslint-disable-line react-hooks/exhaustive-deps
99
+ // Renew five minutes before the pass expires, a little later per tab.
100
+ useEffect(() => {
101
+ if (expiresAt === null)
102
+ return;
103
+ const wait = Math.max(0, expiresAt - RENEW_BEFORE_MS - Date.now()) + Math.floor(Math.random() * JITTER_MS);
104
+ const timer = setTimeout(() => { void renewNow(); }, wait);
105
+ return () => clearTimeout(timer);
106
+ }, [expiresAt, renewNow]);
107
+ return {
108
+ state,
109
+ token: () => held.current?.pass ?? null,
110
+ renew: renewNow,
111
+ signOut: async () => {
112
+ held.current = null;
113
+ setExpiresAt(null);
114
+ await endSession(apiUrl, spaceId, stores());
115
+ setState({ status: "signed_out", reason: "signed_out" });
116
+ },
117
+ signInAgain,
118
+ onRenew: (listener) => { listeners.current.add(listener); return () => { listeners.current.delete(listener); }; },
119
+ };
54
120
  }
@@ -1,8 +1,4 @@
1
1
  import type { AdminRegistry } from "../serialize.ts";
2
- /** Above this many supported locales the switch is a dropdown. */
3
- export declare const SEGMENTED_MAX = 3;
4
- /** "fi" → "Fi": the locale code as the pill reads it. */
5
- export declare const localeLabel: (locale: string) => string;
6
2
  export declare function LocaleSwitcher({ registry, present, parentLocales, value, adding, onSwitch, onAdd }: {
7
3
  registry: AdminRegistry;
8
4
  /** The locales the record has; `[value]` while nothing is loaded. */
@@ -2,9 +2,9 @@
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { Segmented } from "./primitives.js";
4
4
  /** Above this many supported locales the switch is a dropdown. */
5
- export const SEGMENTED_MAX = 3;
5
+ const SEGMENTED_MAX = 3;
6
6
  /** "fi" → "Fi": the locale code as the pill reads it. */
7
- export const localeLabel = (locale) => locale.charAt(0).toUpperCase() + locale.slice(1);
7
+ const localeLabel = (locale) => locale.charAt(0).toUpperCase() + locale.slice(1);
8
8
  export function LocaleSwitcher({ registry, present, parentLocales, value, adding, onSwitch, onAdd }) {
9
9
  const supported = registry.locales.supported;
10
10
  const missing = supported.filter((l) => !present.includes(l) && l !== value);
@@ -105,17 +105,6 @@ export declare function Segmented<T extends string>({ options, value, onChange,
105
105
  className?: string;
106
106
  } | undefined;
107
107
  }): import("react").JSX.Element;
108
- /** A pill row (the f.radio look, spec 2026-09-14 §5): no track, the chosen pill filled in the
109
- * soft accent. For a small choice inside a table row, where a segmented control reads as tabs. */
110
- export declare function Pills<T extends string>({ options, value, onChange, disabled, title, label }: {
111
- options: readonly T[];
112
- value: T;
113
- onChange(v: T): void;
114
- disabled?: boolean;
115
- /** Why the row is off, on every pill. */
116
- title?: string;
117
- label?(o: T): ReactNode;
118
- }): import("react").JSX.Element;
119
108
  export declare function StatusBadge({ status }: {
120
109
  status: string;
121
110
  }): import("react").JSX.Element;
@@ -133,9 +122,6 @@ export declare function SectionLabel({ children, action, caps, style }: {
133
122
  caps?: boolean;
134
123
  style?: CSSProperties;
135
124
  }): import("react").JSX.Element;
136
- export declare function Divider({ bleed }: {
137
- bleed?: number;
138
- }): import("react").JSX.Element;
139
125
  /** The lines under a publish-validation banner: one per field, each focusing its widget
140
126
  * when the screen can reveal it (spec 2026-09-14 §2). */
141
127
  export declare function FieldErrorList({ fields, onReveal }: {
@@ -151,6 +137,10 @@ export declare function Banner({ tone, children, onDismiss, revealKey }: {
151
137
  * list shrinks as fields are fixed, so typing never scrolls. */
152
138
  revealKey?: string;
153
139
  }): import("react").JSX.Element;
140
+ /** An upload's progress (spec 2026-10-18 media on R2 and Images §9): a bar and the percentage of the bytes sent. */
141
+ export declare function UploadProgress({ fraction }: {
142
+ fraction: number;
143
+ }): import("react").JSX.Element;
154
144
  export declare function cx(...parts: (string | false | null | undefined)[]): string;
155
145
  /** One line of a form's info block: a muted label, the value right-aligned and clipped. */
156
146
  export declare function InfoRow({ label, value, mono }: {
@@ -105,11 +105,6 @@ export function Segmented({ options, value, onChange, stretch, disabled, label,
105
105
  return (_jsx("button", { type: "button", className: cx("sm-seg__opt", extra?.className), "aria-pressed": o === value, disabled: disabled || extra?.disabled, title: extra?.title, onClick: () => onChange(o), children: label ? label(o) : o }, o));
106
106
  }) }));
107
107
  }
108
- /** A pill row (the f.radio look, spec 2026-09-14 §5): no track, the chosen pill filled in the
109
- * soft accent. For a small choice inside a table row, where a segmented control reads as tabs. */
110
- export function Pills({ options, value, onChange, disabled, title, label }) {
111
- return (_jsx("div", { className: cx("sm-pills", disabled && "sm-pills--disabled"), role: "radiogroup", children: options.map((o) => (_jsx("button", { type: "button", className: "sm-pill", "aria-pressed": o === value, disabled: disabled, title: title, onClick: () => onChange(o), children: label ? label(o) : o }, o))) }));
112
- }
113
108
  /* ── data ────────────────────────────────────────────────────────────── */
114
109
  // Keyed by the product labels of ui/status-label.ts (lowercased by StatusBadge).
115
110
  const STATUS_TONE = {
@@ -141,9 +136,6 @@ export function SaveIndicator({ state, pending = false }) {
141
136
  export function SectionLabel({ children, action, caps, style }) {
142
137
  return (_jsxs("div", { className: cx("sm-sectionlabel", caps && "sm-sectionlabel--caps"), style: style, children: [_jsx("span", { className: "sm-label", children: children }), action && _jsx("span", { style: { color: "var(--accent)", fontWeight: 600, fontSize: "var(--text-sm)" }, children: action })] }));
143
138
  }
144
- export function Divider({ bleed = 0 }) {
145
- return _jsx("div", { className: "sm-divider", style: { margin: `0 -${bleed}px` } });
146
- }
147
139
  /** The lines under a publish-validation banner: one per field, each focusing its widget
148
140
  * when the screen can reveal it (spec 2026-09-14 §2). */
149
141
  export function FieldErrorList({ fields, onReveal }) {
@@ -161,6 +153,11 @@ export function Banner({ tone, children, onDismiss, revealKey }) {
161
153
  }, [revealKey]);
162
154
  return (_jsxs("div", { ref: ref, className: `sm-banner sm-banner--${tone}`, role: tone === "error" ? "alert" : undefined, children: [_jsx("div", { style: { flex: 1, minWidth: 0 }, children: children }), onDismiss && (_jsx("button", { type: "button", onClick: onDismiss, "aria-label": "Dismiss", style: { background: "transparent", border: 0, color: "inherit", cursor: "pointer", font: "inherit", padding: 0 }, children: "\u00D7" }))] }));
163
155
  }
156
+ /** An upload's progress (spec 2026-10-18 media on R2 and Images §9): a bar and the percentage of the bytes sent. */
157
+ export function UploadProgress({ fraction }) {
158
+ const pct = Math.round(Math.min(1, Math.max(0, fraction)) * 100);
159
+ return (_jsxs("div", { className: "sm-upload", role: "progressbar", "aria-label": "Uploading", "aria-valuemin": 0, "aria-valuemax": 100, "aria-valuenow": pct, children: [_jsx("div", { className: "sm-upload__track", children: _jsx("div", { className: "sm-upload__fill", style: { width: `${pct}%` } }) }), _jsxs("span", { className: "sm-upload__pct", children: ["Uploading\u2026 ", pct, "%"] })] }));
160
+ }
164
161
  export function cx(...parts) {
165
162
  return parts.filter(Boolean).join(" ");
166
163
  }
@@ -230,6 +230,8 @@ body:has(.sm-admin){margin:0}
230
230
  lit tile a build in flight, the warning tile a build that has not landed or a site that did not answer. */
231
231
  .sm-rail__deploy{margin-top:auto}
232
232
  .sm-rail__deploy+.sm-rail__foot{margin-top:0}
233
+ /* A publish on its way to the site (plan 2026-10-14 publish through the queue): a quiet line above the Deploy tile
234
+ and the avatar, the warning colour when a delivery landed late or with an error. */
233
235
  .sm-rail__badge{position:absolute;top:-4px;right:-6px;min-width:16px;height:16px;padding:0 4px;border-radius:var(--radius-round);background:var(--accent);color:#fff;font-size:10px;font-weight:600;line-height:16px;text-align:center}
234
236
  .sm-rail__tile--lit{background:var(--accent-soft);color:var(--accent);animation:sm-rail-pulse 1.6s ease-in-out infinite}
235
237
  .sm-rail__tile--warn{background:var(--warn-soft);color:var(--warn)}
@@ -681,7 +683,6 @@ body:has(.sm-admin){margin:0}
681
683
  .sm-counter--over{color:var(--warn)}
682
684
  /* The page glyph beside the page title in the empty-selection inspector head. */
683
685
  .sm-pageglyph{width:10px;height:13px;border:1.5px solid var(--text-muted);border-radius:var(--radius-xs);flex:none}
684
- .sm-divider{height:1px;background:var(--line);flex:none}
685
686
  .sm-sectionlabel{display:flex;align-items:center;justify-content:space-between;gap:8px}
686
687
  /* The overline: a small uppercase tracked heading for a group — the
687
688
  inspector's Spacing & layout / Advanced, the picker's Add to <zone> and
@@ -970,6 +971,11 @@ body:has(.sm-admin){margin:0}
970
971
  }
971
972
  /* The picker's drop bar: tall enough to aim a drag at (2026-09-25). */
972
973
  .sm-dropzone--bar{padding:28px 10px;font-size:var(--text-sm)}
974
+ .sm-upload{display:flex;flex-direction:column;align-items:stretch;gap:6px;width:100%;max-width:280px;font-size:var(--text-sm);color:var(--text-muted)}
975
+ .sm-upload__track{height:6px;border-radius:999px;background:var(--surface-inset);overflow:hidden}
976
+ .sm-upload__fill{height:100%;background:var(--accent);border-radius:999px;transition:width 120ms linear}
977
+ .sm-upload__pct{font-variant-numeric:tabular-nums}
978
+ .sm-mediascreen__upload{padding:8px 20px 0}
973
979
  .sm-mediagrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
974
980
  .sm-mediacard{
975
981
  display:flex;flex-direction:column;gap:4px;padding:6px;background:transparent;border:1.5px solid transparent;
@@ -999,7 +1005,7 @@ body:has(.sm-admin){margin:0}
999
1005
  .sm-media__actions{display:flex;gap:6px}
1000
1006
  .sm-media__chip{display:flex;align-items:center;gap:8px;padding:6px 12px;background:var(--surface-inset);border-radius:var(--radius-btn)}
1001
1007
  .sm-crop{display:grid;grid-template-columns:1fr 240px;gap:14px;min-height:0}
1002
- .sm-crop__stage{position:relative;user-select:none;touch-action:none;background:var(--surface-inset);border-radius:var(--radius-md);overflow:hidden}
1008
+ .sm-crop__stage{position:relative;align-self:start;user-select:none;touch-action:none;background:var(--surface-inset);border-radius:var(--radius-md);overflow:hidden}
1003
1009
  .sm-crop__stage>img{display:block;width:100%;height:auto;pointer-events:none}
1004
1010
  .sm-crop__shade{position:absolute;inset:0;background:rgba(0,0,0,.45);pointer-events:none}
1005
1011
  .sm-crop__rect{position:absolute;outline:1.5px solid #fff;box-shadow:0 0 0 1px rgba(0,0,0,.4);cursor:move;background:transparent}
@@ -1010,6 +1016,11 @@ body:has(.sm-admin){margin:0}
1010
1016
  .sm-crop__side{display:flex;flex-direction:column;gap:10px}
1011
1017
  .sm-crop__preview{position:relative;overflow:hidden;border-radius:var(--radius-md);background:var(--surface-inset)}
1012
1018
  .sm-crop__preview>img{position:absolute;max-width:none;display:block}
1019
+ .sm-crop__ghost{position:absolute;outline:1.5px dashed rgba(255,255,255,.9);box-shadow:0 0 0 1px rgba(0,0,0,.35);pointer-events:none}
1020
+ .sm-crop__follows{margin-left:4px;color:var(--accent)}
1021
+ .sm-crop__previews{display:flex;gap:8px;align-items:flex-start}
1022
+ .sm-crop__figure{flex:1;min-width:0;margin:0;display:flex;flex-direction:column;gap:4px}
1023
+ .sm-crop__figure>figcaption{font-size:11px;color:var(--text-muted)}
1013
1024
  .sm-mediascreen{display:grid;grid-template-columns:1fr 300px;column-gap:var(--gutter);flex:1;min-height:0}
1014
1025
  .sm-mediascreen__grid{padding:var(--table-pad);overflow:auto;background:var(--surface-form);border-radius:var(--radius-card-lg)}
1015
1026
  .sm-mediascreen__inspector{
@@ -14,6 +14,7 @@ import { rowsProblems } from "../custom/rows.js";
14
14
  import { isCalendarDay, isInstant } from "./forms/date-picker.js";
15
15
  import { fits, misfitMessage } from "./drift.js";
16
16
  import { isSegment } from "../paths.js";
17
+ import { isCrop } from "../media.js";
17
18
  /** A slug outside the path grammar, at publish (spec 2026-09-28 ASCII slugs §3). */
18
19
  export const SLUG_GRAMMAR_MESSAGE = "Use lowercase letters, digits and single hyphens, like my-post.";
19
20
  const subfields = (d) => d.fields && typeof d.fields === "object" ? d.fields : {};
@@ -81,7 +82,15 @@ function validateValue(path, value, d, errors, elements, scope) {
81
82
  errors.push({ field: path, message: "Not a date and time." });
82
83
  return;
83
84
  }
84
- if (d.type === "image" || d.type === "video" || d.type === "file")
85
+ if (d.type === "image") {
86
+ // The crop window writes fractions inside the image (spec 2026-10-18 media on R2 and Images §6); the server cuts them.
87
+ const v = value;
88
+ const crops = v.crops === undefined ? [] : v.crops && typeof v.crops === "object" && !Array.isArray(v.crops) ? Object.values(v.crops) : [null];
89
+ if ((v.crop !== undefined && !isCrop(v.crop)) || crops.some((c) => !isCrop(c)))
90
+ errors.push({ field: path, message: "A crop lies outside the image." });
91
+ return;
92
+ }
93
+ if (d.type === "video" || d.type === "file")
85
94
  return;
86
95
  if (d.type === "list") {
87
96
  if (!Array.isArray(value))
@@ -0,0 +1,26 @@
1
+ import type { ResolvedConfig } from "./config.ts";
2
+ import { type PushBody } from "./push.ts";
3
+ import { type ContentReads, type DeliveryReads } from "./site.ts";
4
+ export type SmoodlyApiOptions = {
5
+ /** Without a trailing slash: `https://api.smoodly.io`, locally `http://localhost:8787`. */
6
+ apiUrl: string;
7
+ spaceId: string;
8
+ /** The secret key on a server, the publishable key where only published content is read. */
9
+ key: string;
10
+ config: ResolvedConfig;
11
+ /** Test seam; default the global fetch. */
12
+ fetch?: typeof fetch;
13
+ /** The schema push before the first call (spec 2026-10-16 the control plane §4): default, under NODE_ENV=development,
14
+ * the config and the site's address as `live`; `false` sends none. A config change reloads the glue module in both
15
+ * dev servers, which makes a new client and so a new push; an unchanged registry costs the service nothing. */
16
+ push?: false | PushBody;
17
+ };
18
+ /** The content API refused a read: its status, its code (`unauthorized`, `not_found`, `validation`, `internal`) and its message. */
19
+ export declare class SmoodlyApiError extends Error {
20
+ readonly status: number;
21
+ readonly code: string;
22
+ constructor(status: number, code: string, message: string);
23
+ }
24
+ /** A site's reads over the content API: the same face as the service's own site, so the read layer and the shells run
25
+ * unchanged. Every call counts as one read for the adapters' counters. */
26
+ export declare function createSmoodlyApi(options: SmoodlyApiOptions): ContentReads & DeliveryReads;
@@ -0,0 +1,107 @@
1
+ // The site's reads over the content API (spec 2026-10-14 §1): one HTTP call per read, the key in the Authorization
2
+ // header, the envelope `{ ok, data, tags }`. The statics (cacheKey, styles, custom, code pages) are computed from the
3
+ // config exactly as the service's site computes them (site.ts siteStatics), so a cache key never disagrees across
4
+ // the wire. A document's tags ride on the hit (SiteHit.tags), where the read layer takes them.
5
+ import { countRead } from "./read-counter.js";
6
+ import { pushBody, pushSchema } from "./push.js";
7
+ import { siteOptions, siteStatics, } from "./site.js";
8
+ /** The content API refused a read: its status, its code (`unauthorized`, `not_found`, `validation`, `internal`) and its message. */
9
+ export class SmoodlyApiError extends Error {
10
+ constructor(status, code, message) {
11
+ super(message);
12
+ this.status = status;
13
+ this.code = code;
14
+ this.name = "SmoodlyApiError";
15
+ }
16
+ }
17
+ /** A site's reads over the content API: the same face as the service's own site, so the read layer and the shells run
18
+ * unchanged. Every call counts as one read for the adapters' counters. */
19
+ export function createSmoodlyApi(options) {
20
+ const apiUrl = options.apiUrl.replace(/\/+$/, "");
21
+ const base = `${apiUrl}/v1/spaces/${encodeURIComponent(options.spaceId)}`;
22
+ const doFetch = options.fetch ?? fetch;
23
+ const statics = siteStatics(siteOptions(options.config));
24
+ const localeOf = (o) => o?.locale ?? statics.locales.default;
25
+ const env = typeof process === "undefined" ? {} : process.env;
26
+ const push = options.push ?? (env.NODE_ENV === "development" ? pushBody(options.config, env, "live") : false);
27
+ /** One push per client, awaited by every call until it settles; its failure is logged, never thrown. */
28
+ let pushed = null;
29
+ const pushOnce = (body) => (pushed ?? (pushed = pushSchema({ apiUrl, spaceId: options.spaceId, key: options.key, fetch: doFetch }, body).then(() => undefined, (e) => console.error(`smoodly: the schema push failed — ${e instanceof Error ? e.message : String(e)}; the site runs against the schema the space has.`))));
30
+ async function call(path, params) {
31
+ if (push)
32
+ await pushOnce(push);
33
+ countRead();
34
+ const url = new URL(`${base}${path}`);
35
+ for (const [k, v] of Object.entries(params))
36
+ if (v !== undefined && v !== "")
37
+ url.searchParams.set(k, v);
38
+ let res;
39
+ try {
40
+ res = await doFetch(url.toString(), { headers: { authorization: `Bearer ${options.key}`, accept: "application/json" } });
41
+ }
42
+ catch (e) {
43
+ throw new TypeError(`smoodly: can't reach SMOODLY_API_URL (${apiUrl}): ${e instanceof Error ? e.message : String(e)}`);
44
+ }
45
+ const body = (await res.json().catch(() => null));
46
+ if (!body || !body.ok) {
47
+ throw new SmoodlyApiError(res.status, body?.code ?? "internal", body?.message ?? `smoodly: SMOODLY_API_URL (${apiUrl}) answered ${res.status} with no message.`);
48
+ }
49
+ return { data: body.data, tags: body.tags };
50
+ }
51
+ const draftParam = (o) => ({ draft: o?.draft ? "1" : undefined });
52
+ /** The response's tags on a published hit (the read layer takes them from there); a draft carries none. */
53
+ const withTags = (hit, tags, draft) => hit && hit.kind !== "redirect" && !draft ? { ...hit, tags: hit.tags ?? tags } : hit;
54
+ return {
55
+ locales: statics.locales,
56
+ homePageSlug: statics.homePageSlug,
57
+ cacheKey: statics.cacheKey,
58
+ styles: statics.styles,
59
+ custom: statics.custom,
60
+ codePage: statics.codePage,
61
+ async locate(locale, path) {
62
+ return (await call("/locate", { locale, path })).data;
63
+ },
64
+ async targetPaths(target) {
65
+ return (await call("/target-paths", { kind: target.kind, id: target.id })).data;
66
+ },
67
+ async load(target, locale, path, o) {
68
+ const draft = !!o?.draft || o?.version !== undefined;
69
+ const r = await call("/load", { kind: target.kind, id: target.id, locale, path, draft: o?.draft ? "1" : undefined, version: o?.version });
70
+ return withTags(r.data, r.tags, draft);
71
+ },
72
+ async lookup(locale, path) {
73
+ const r = await call("/lookup", { locale, path });
74
+ return withTags(r.data.hit, r.tags, false);
75
+ },
76
+ async lookupWithGlobals(locale, path, items) {
77
+ const r = await call("/lookup", { locale, path, globals: items.map((i) => i.name).join(",") });
78
+ return { hit: withTags(r.data.hit, r.tags, false), globals: new Map(Object.entries(r.data.globals)) };
79
+ },
80
+ async getPagesTree(o) {
81
+ return (await call("/pages", { locale: localeOf(o), ...draftParam(o) })).data;
82
+ },
83
+ async getPage(target, o) {
84
+ return (await call("/page", { locale: localeOf(o), target, ...draftParam(o) })).data;
85
+ },
86
+ async getEntries(collection, o = {}) {
87
+ const { locale: _l, draft: _d, ...query } = o;
88
+ const q = Object.keys(query).length ? JSON.stringify(query) : undefined;
89
+ return (await call(`/entries/${encodeURIComponent(collection)}`, { locale: localeOf(o), query: q, ...draftParam(o) })).data;
90
+ },
91
+ async getEntry(collection, target, o) {
92
+ return (await call(`/entry/${encodeURIComponent(collection)}`, { locale: localeOf(o), target, ...draftParam(o) })).data;
93
+ },
94
+ async getEntriesTree(collection, o) {
95
+ return (await call(`/entries-tree/${encodeURIComponent(collection)}`, { locale: localeOf(o), ...draftParam(o) })).data;
96
+ },
97
+ async getGlobal(item, o) {
98
+ return (await call(`/global/${encodeURIComponent(item.name)}`, { locale: localeOf(o), ...draftParam(o) })).data;
99
+ },
100
+ async addresses(locale) {
101
+ return (await call("/addresses", { locale })).data;
102
+ },
103
+ async newestPublishedAt() {
104
+ return (await call("/status", {})).data.newestPublishedAt;
105
+ },
106
+ };
107
+ }
@@ -1,15 +1,10 @@
1
- import { type EntryRecord } from "./entry-store.ts";
2
1
  import type { CollectionSchema } from "./collections.ts";
3
- import { type PagePathOptions } from "./paths.ts";
4
- import { type GlobalSchema } from "./global.ts";
5
- import type { PageRecord } from "./store.ts";
2
+ import type { PagePathOptions } from "./paths.ts";
6
3
  import type { Descriptor } from "./fields.ts";
7
- import { type Anchors } from "./anchors.ts";
8
- import { type EntryItem } from "./read-shapes.ts";
9
- import type { PublishedPageRow } from "./published.ts";
10
- import type { EntryQuery, PageTreeItem, SiteEntry, SitePage, SiteOptions } from "./site.ts";
4
+ import type { Anchors } from "./anchors.ts";
5
+ import type { EntryQuery, SiteOptions } from "./site.ts";
11
6
  /** What the site computes from its options before any read: the registered shapes, the code-owned addresses and the resolved styles. */
12
- export declare function siteShape(options: SiteOptions): {
7
+ export declare function siteShape(options: Omit<SiteOptions, "stores">): {
13
8
  locales: import("./paths.ts").LocaleSet;
14
9
  pageSchemas: import("./site.ts").SitePageSchema[];
15
10
  homePageSlug: string;
@@ -26,35 +21,3 @@ export type SiteShape = ReturnType<typeof siteShape>;
26
21
  export declare function pageFieldsOf(shape: SiteShape, template: string | null): Record<string, Descriptor>;
27
22
  /** The checks `getEntries` makes before any read, in draft mode and outside it: one function both paths call. */
28
23
  export declare function checkEntryQuery(schema: CollectionSchema<any>, collection: string, o: EntryQuery): void;
29
- /** One read row: the public item, and the record it was projected from (a tree and a bare slug need `parentId`). */
30
- export type ReadRow = {
31
- record: EntryRecord;
32
- item: EntryItem;
33
- };
34
- export type Assembler = {
35
- /** A page of the locale, draft or published, with a stored version in draft mode. */
36
- page(id: string, locale: string, path: string, opts: {
37
- draft: boolean;
38
- version?: string;
39
- }): Promise<SitePage | null>;
40
- entry(id: string, locale: string, path: string, opts: {
41
- draft: boolean;
42
- }): Promise<SiteEntry | null>;
43
- global<V>(item: GlobalSchema<V>, locale: string, opts: {
44
- draft: boolean;
45
- }): Promise<V | null>;
46
- /** The pages tree over the editing tables. */
47
- tree(locale: string, opts: {
48
- draft: boolean;
49
- }): Promise<PageTreeItem[]>;
50
- /** Rows with the record and the projected item. */
51
- entries(collection: string, q: EntryQuery): Promise<ReadRow[]>;
52
- /** The light tree row of a node as the delivery table stores it (path null when hidden), for a locale. */
53
- treeRow(record: PageRecord, all: PageRecord[], locale: string): Promise<Omit<PublishedPageRow, "document" | "tags" | "model" | "stale" | "publishedAt">>;
54
- /** A memoizing copy for one projection call: listPages, getMany, pathsIn, fieldsOf and assets.getMany answer once per argument set.
55
- * `pages`: the page list the caller already read, so the copy's listPages answers with it and never reads again. */
56
- batch(primed?: {
57
- pages?: PageRecord[];
58
- }): Assembler;
59
- };
60
- export declare function createAssembler(options: SiteOptions, shape: SiteShape): Assembler;