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/README.md CHANGED
@@ -2,35 +2,36 @@
2
2
 
3
3
  A code-first CMS for Astro and Next.js. Define a component once in code and it
4
4
  becomes a typed, editable building block your editors compose, within
5
- the rails you set. Content lives in Supabase; the admin mounts into
6
- your own site at `/admin`.
5
+ the rails you set. Content and media live in Smoodly's service, read over
6
+ its API; the admin mounts into your own site at `/admin`.
7
7
 
8
8
  ## Install
9
9
 
10
10
  npx create-smoodly-app my-site
11
11
 
12
- This creates an Astro or Next.js site with the admin mounted, the database schema
13
- in place and a first editor who can sign in.
12
+ This creates an Astro or Next.js site with the admin mounted, its content model
13
+ pushed to your space and the starter content seeded.
14
14
 
15
15
  ## Environment
16
16
 
17
- A self-hosted app needs three variables:
17
+ The installer writes them from the project at smoodly.io:
18
18
 
19
19
  | Variable | What it is |
20
20
  | --- | --- |
21
- | `SMOODLY_URL` | Your Supabase project's URL |
22
- | `SMOODLY_SECRET_KEY` | The project's secret key. Server-only. |
23
- | `SMOODLY_PUBLISHABLE_KEY` | The project's publishable key |
21
+ | `SMOODLY_SPACE_ID` | The space your content lives in (`sp_…`) |
22
+ | `SMOODLY_SECRET_KEY` | The space's secret key (`sk_…`). Server-only. |
23
+ | `SMOODLY_PUBLISHABLE_KEY` | The space's publishable key (`pk_…`) |
24
+ | `SITE_URL` | Your site's own address |
24
25
 
25
26
  ## Upgrading
26
27
 
27
- A new version can need a newer database schema. After updating the
28
- package, `npx smoodly migrate` writes the migration files; the
29
- Upgrading guide has the rest.
28
+ Update the package and deploy: a space's schema lives in Smoodly's
29
+ service, which brings it forward itself. The Upgrading guide names each
30
+ version's code changes.
30
31
 
31
32
  ## Docs and source
32
33
 
33
- Guides: https://github.com/laniakea-studio/smoodly-cms/tree/main/guides
34
- Source: https://github.com/laniakea-studio/smoodly-cms
34
+ Guides: https://github.com/smoodly-io/smoodly/tree/main/guides
35
+ Source: https://github.com/smoodly-io/smoodly
35
36
 
36
37
  MIT licensed.
@@ -1,6 +1,7 @@
1
- /** What the browser needs to sign in — public values only (admin/shell/session.ts reads it; the handler and the page data carry it). */
2
- export type AuthConfig = {
3
- url: string;
4
- key: string;
5
- providers?: ("google" | "github")[];
1
+ /** Where the admin sends a person to sign in, and the dashboard its account menu and Members page link to (spec
2
+ * 2026-10-16 sign-in and the dashboard §1, §6): `GET /v1/auth` answers it from the Worker's env; the handler and the
3
+ * page data carry it. Public values only. */
4
+ export type SignInConfig = {
5
+ signInUrl: string;
6
+ consoleUrl: string;
6
7
  };
@@ -1,18 +1,10 @@
1
- import { type MemberRole } from "../permissions.ts";
2
- import type { AdminOpCall, OpResult } from "./ops.ts";
3
- /** What the auth project vouches for: the Auth user id and the email the locale rows record as `updated_by` (spec 2026-09-13 history §2). */
1
+ import type { MemberRole } from "../permissions.ts";
2
+ /** Who the pass names: the account id and the email the locale rows record as `updated_by` (spec 2026-09-13 history §2). */
4
3
  export type VerifiedUser = {
5
4
  userId: string;
6
5
  email: string | null;
7
6
  };
8
- /** The verified member: the user plus the normalized role from `members`. */
7
+ /** The admitted member: the account plus the normalized role from `members`. */
9
8
  export type AdminSession = VerifiedUser & {
10
9
  role: MemberRole;
11
10
  };
12
- export type AdminAuth = {
13
- /** The user behind a session token, or null when the auth project rejects it. */
14
- verify(token: string): Promise<VerifiedUser | null>;
15
- /** The stored role of this user in `members` for the current space, or null without a row. */
16
- memberOf(userId: string): Promise<string | null>;
17
- };
18
- export declare function gateAdminOp(auth: AdminAuth, call: AdminOpCall, run: (call: AdminOpCall, session: AdminSession) => Promise<OpResult<unknown>>): Promise<OpResult<unknown>>;
@@ -1,30 +1 @@
1
- // Admission to the admin (spec 2026-09-04 §5; roles 2026-09-25 members
2
- // management §2): a session token from the auth project, then a row in
3
- // `members` read with the write key, whose role rides on the session.
4
- // One gate around the ops seam — individual ops ask can(). Framework-
5
- // free; Supabase-backed implementation in supabase-auth.ts.
6
- import { normalizeRole } from "../permissions.js";
7
- const unauthorized = (message) => ({ ok: false, code: "unauthorized", message });
8
- export async function gateAdminOp(auth, call, run) {
9
- const token = typeof call === "object" && call !== null && typeof call.token === "string"
10
- ? call.token
11
- : null;
12
- if (!token)
13
- return unauthorized("smoodly: sign in to use the admin.");
14
- let user;
15
- let stored;
16
- try {
17
- user = await auth.verify(token);
18
- // Marked, so the browser signs itself out instead of showing this on every screen (2026-10-02).
19
- if (!user)
20
- return { ok: false, code: "unauthorized", message: "smoodly: your session has expired — sign in again.", expired: true };
21
- stored = await auth.memberOf(user.userId);
22
- }
23
- catch (e) {
24
- console.error("smoodly: auth check failed", e);
25
- return { ok: false, code: "internal", message: "smoodly: could not check your session." };
26
- }
27
- if (stored === null)
28
- return unauthorized("smoodly: this account is not an editor of this site.");
29
- return run(call, { ...user, role: normalizeRole(stored) });
30
- }
1
+ export {};
@@ -11,7 +11,7 @@ export function wireArgs(args) {
11
11
  }
12
12
  return out;
13
13
  }
14
- const noResult = (status) => ({ ok: false, code: "internal", message: `smoodly: the admin's endpoint answered ${status} without a result — see the server log.` });
14
+ const noResult = (status) => ({ ok: false, code: "internal", message: `smoodly: the admin API answered ${status} without a result — see the server log.` });
15
15
  /** `fetcher` is the test seam. A network failure rejects; every answer resolves to a typed result.
16
16
  * One operation at a time, in call order: Next queued server actions per browser, and the admin's reads
17
17
  * and writes rely on it (a read sent after a save must see that save's token; a save typed during a
@@ -20,7 +20,8 @@ export function fetchOp(url, fetcher = fetch) {
20
20
  const send = async (call) => {
21
21
  const res = await fetcher(url, {
22
22
  method: "POST",
23
- credentials: "same-origin",
23
+ // The endpoint is the service's origin; the session rides the header, no cookie crosses.
24
+ credentials: "omit",
24
25
  headers: { "content-type": "application/json", ...(call.token ? { authorization: `Bearer ${call.token}` } : {}) },
25
26
  body: JSON.stringify({ path: call.path, args: wireArgs(call.args) }),
26
27
  });
@@ -4,15 +4,19 @@ import { jsx as _jsx } from "react/jsx-runtime";
4
4
  // stylesheet into the document and mounts the shell. Bundled by scripts/build-admin.ts; no app compiles it.
5
5
  import { createRoot } from "react-dom/client";
6
6
  import { ADMIN_DATA_ID, ADMIN_ROOT_ID } from "../page-data.js";
7
- import { setPreviewBase } from "../editor/canvas-address.js";
7
+ import { setCanvasMarks, setPreviewBase, setPreviewUrl } from "../editor/canvas-address.js";
8
8
  import { setDelivery } from "../delivery-mode.js";
9
9
  import { AdminApp } from "../shell/AdminApp.js";
10
+ import { setConsole } from "../shell/console-links.js";
10
11
  import { fontFaceCss } from "../ui/fonts.js";
11
12
  import { ADMIN_CSS } from "../ui/theme.js";
12
13
  import { fetchOp } from "./fetch-op.js";
13
14
  const data = JSON.parse(document.getElementById(ADMIN_DATA_ID).textContent ?? "");
14
15
  setPreviewBase(data.previewBase ?? "");
16
+ setCanvasMarks(data.canvasMarks ?? "query");
17
+ setPreviewUrl(data.previewUrl ?? "");
15
18
  setDelivery(data.delivery ?? "live");
19
+ setConsole(data.signIn.consoleUrl, data.spaceId);
16
20
  // One <style>, fonts first: the canvas copies this element's text into the site's frame (editor/useInlineSession.ts).
17
21
  const style = document.createElement("style");
18
22
  style.setAttribute("data-smoodly-admin-styles", "");
@@ -20,4 +24,4 @@ style.textContent = `${fontFaceCss(data.filesBase)}\n${ADMIN_CSS}`;
20
24
  document.head.appendChild(style);
21
25
  // The page's own background rules did their job (admin/page.ts): the stylesheet owns the look now.
22
26
  document.querySelector("style[data-smoodly-boot]")?.remove();
23
- createRoot(document.getElementById(ADMIN_ROOT_ID)).render(_jsx(AdminApp, { registry: data.registry, op: fetchOp(data.opUrl), auth: data.auth, presenceConfig: data.presence ?? undefined }));
27
+ createRoot(document.getElementById(ADMIN_ROOT_ID)).render(_jsx(AdminApp, { registry: data.registry, op: fetchOp(data.opUrl), signIn: data.signIn, api: { apiUrl: data.apiUrl, spaceId: data.spaceId }, presenceConfig: data.presence ?? undefined }));
@@ -1,5 +1,6 @@
1
1
  import type { AdminOpAction, AdminOps } from "./ops.ts";
2
- /** `onExpired` runs when the server refuses the token outright (gateAdminOp's `expired`): the browser
3
- * still holds a session the auth project no longer knows — after a database reset, a deleted user —
4
- * and only a local sign-out brings the sign-in form back (2026-10-02). */
5
- export declare function makeClientOps(action: AdminOpAction, token?: () => string | null, onExpired?: () => void): AdminOps;
2
+ /** `onExpired` runs when the service refuses the pass outright (`expired`: it expired, or its session ended). When it
3
+ * answers true (the session renewed), the call is sent once more with the fresh pass; otherwise the refusal is
4
+ * returned and the session's own path (a redirect to sign in) takes over (spec 2026-10-16 sign-in and the dashboard
5
+ * §2). */
6
+ export declare function makeClientOps(action: AdminOpAction, token?: () => string | null, onExpired?: () => Promise<boolean> | void): AdminOps;
@@ -2,29 +2,32 @@ const GROUPS = {
2
2
  pages: ["list", "get", "create", "saveDraft", "publish", "unpublish", "discardDraft", "delete", "rename", "move", "setTemplate", "addLocale", "removeLocale", "listVersions", "restore"],
3
3
  entries: ["list", "distinct", "get", "create", "save", "setStatus", "discardDraft", "delete", "move", "addLocale", "removeLocale", "listVersions", "getVersion", "restore"],
4
4
  globals: ["list", "get", "save", "setStatus", "discardDraft", "listVersions", "getVersion", "restore"],
5
- assets: ["list", "get", "beginUpload", "finishUpload", "update", "delete", "usage"],
5
+ assets: ["list", "get", "update", "delete", "usage"],
6
6
  members: ["list", "me", "invite", "setRole", "remove"],
7
7
  redirects: ["list", "targets", "add", "remove"],
8
8
  health: ["scan"],
9
- schema: ["status"],
10
9
  preview: ["enable", "disable"],
11
10
  deploy: ["status", "trigger"],
12
11
  };
13
- /** `onExpired` runs when the server refuses the token outright (gateAdminOp's `expired`): the browser
14
- * still holds a session the auth project no longer knows — after a database reset, a deleted user —
15
- * and only a local sign-out brings the sign-in form back (2026-10-02). */
12
+ /** `onExpired` runs when the service refuses the pass outright (`expired`: it expired, or its session ended). When it
13
+ * answers true (the session renewed), the call is sent once more with the fresh pass; otherwise the refusal is
14
+ * returned and the session's own path (a redirect to sign in) takes over (spec 2026-10-16 sign-in and the dashboard
15
+ * §2). */
16
16
  export function makeClientOps(action, token, onExpired) {
17
17
  const out = {};
18
+ const send = (path, args) => {
19
+ const t = token?.() ?? null;
20
+ return action({ path, args, ...(t ? { token: t } : {}) });
21
+ };
18
22
  for (const [group, methods] of Object.entries(GROUPS)) {
19
23
  out[group] = {};
20
24
  for (const method of methods) {
21
- out[group][method] = (...args) => {
22
- const t = token?.() ?? null;
23
- return action({ path: `${group}.${method}`, args, ...(t ? { token: t } : {}) }).then((r) => {
24
- if (!r.ok && r.expired)
25
- onExpired?.();
25
+ out[group][method] = async (...args) => {
26
+ const path = `${group}.${method}`;
27
+ const r = await send(path, args);
28
+ if (r.ok || !r.expired)
26
29
  return r;
27
- });
30
+ return (await onExpired?.()) === true ? send(path, args) : r;
28
31
  };
29
32
  }
30
33
  }
@@ -1,5 +1,5 @@
1
1
  export declare function setDelivery(value: "live" | "static"): void;
2
2
  export declare const delivery: () => "live" | "static";
3
- /** A publish succeeded somewhere in the admin: the shell re-reads deploy.status. */
3
+ /** A publish (or an unpublish) succeeded somewhere in the admin. */
4
4
  export declare function notePublished(): void;
5
5
  export declare function onPublished(listener: () => void): () => void;
@@ -2,14 +2,14 @@
2
2
  // Which delivery mode the site runs (spec 2026-10-09 static mode §6): the server names it once, in the
3
3
  // page's data, and the shell shows the Deploy tile for a static site. Module state, set by the browser
4
4
  // entry before the shell mounts, like the preview base (editor/canvas-address.ts). The publish controls
5
- // tell the shell when a publish landed, so the tile's count refreshes at once.
5
+ // tell the shell when a publish succeeded, so the Deploy tile re-reads.
6
6
  let mode = "live";
7
7
  const listeners = new Set();
8
8
  export function setDelivery(value) {
9
9
  mode = value;
10
10
  }
11
11
  export const delivery = () => mode;
12
- /** A publish succeeded somewhere in the admin: the shell re-reads deploy.status. */
12
+ /** A publish (or an unpublish) succeeded somewhere in the admin. */
13
13
  export function notePublished() {
14
14
  for (const listener of listeners)
15
15
  listener();
@@ -27,8 +27,6 @@ export type Finding = {
27
27
  };
28
28
  };
29
29
  export declare const isBroken: (f: Finding) => boolean;
30
- /** The validator's empty: an empty value is `required`'s question, never drift's. */
31
- export declare const isEmptyValue: (v: unknown) => boolean;
32
30
  /** Whether a stored value is the shape its field kind stores (§2). Empty always fits; a kind
33
31
  * this list does not know fits (the custom section's `spans`, whose structure is rowsProblems'). */
34
32
  export declare function fits(d: {
@@ -12,7 +12,7 @@ export const SEVERITY = {
12
12
  };
13
13
  export const isBroken = (f) => SEVERITY[f.kind] === "broken";
14
14
  /** The validator's empty: an empty value is `required`'s question, never drift's. */
15
- export const isEmptyValue = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
15
+ const isEmptyValue = (v) => v === undefined || v === null || v === "" || (Array.isArray(v) && v.length === 0);
16
16
  const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
17
17
  const isStrings = (v) => Array.isArray(v) && v.every((x) => typeof x === "string");
18
18
  /** Whether a stored value is the shape its field kind stores (§2). Empty always fits; a kind
@@ -1,8 +1,5 @@
1
1
  import type { AdminOps } from "../ops.ts";
2
2
  import type { AdminRegistry } from "../serialize.ts";
3
- export declare const withEditorParam: (url: string) => string;
4
- /** The site URL of a stored version — History's iframe (spec 2026-09-13 history §5); no editor parameter, so no bridge. */
5
- export declare const withVersionParam: (url: string, versionId: string) => string;
6
3
  export declare function EditorView({ ops, registry, id }: {
7
4
  ops: AdminOps;
8
5
  registry: AdminRegistry;
@@ -39,7 +39,8 @@ import { tabOf, tabsFor } from "../forms/tabs.js";
39
39
  import { InspectorFields, valueOf } from "./InspectorFields.js";
40
40
  import { allowedSections, canInsert, fillLockedZones, findNode, insertNode, moveNode, moveNodeTo, newNodeId, nodeFromSample, patchNode, patchNodeCss, patchNodePath, patchPage, placementNode, removeNode, zoneOf, clearZone, unsetNodePath, } from "../tree-ops.js";
41
41
  import { useDraftAutosave } from "./autosave.js";
42
- import { draftAddress } from "./canvas-address.js";
42
+ import { canvasAddress, previewUrl, versionAddress } from "./canvas-address.js";
43
+ import { enableDraft } from "./preview-grant.js";
43
44
  import { CanvasOverlay } from "./CanvasOverlay.js";
44
45
  import { InlineRichtext } from "./InlineRichtext.js";
45
46
  import { InsertPicker } from "./InsertPicker.js";
@@ -78,9 +79,6 @@ import { usePresence, usePresenceState, useSavedElsewhere, useTrackPresence } fr
78
79
  import { editorsOf } from "../shell/presence.js";
79
80
  import { PresenceLine } from "../ui/PresenceLine.js";
80
81
  import { Banner as BannerBox, Breadcrumb, Button, FieldErrorList, IconButton, LinkButton, SaveIndicator, Tabs, TopBar, } from "../ui/primitives.js";
81
- export const withEditorParam = (url) => (url.includes("?") ? `${url}&smoodly-editor=1` : `${url}?smoodly-editor=1`);
82
- /** The site URL of a stored version — History's iframe (spec 2026-09-13 history §5); no editor parameter, so no bridge. */
83
- export const withVersionParam = (url, versionId) => `${url}${url.includes("?") ? "&" : "?"}smoodly-version=${encodeURIComponent(versionId)}`;
84
82
  export function EditorView({ ops, registry, id }) {
85
83
  const adminHref = useAdminHref();
86
84
  const [record, setRecord] = useState(null);
@@ -200,7 +198,7 @@ export function EditorView({ ops, registry, id }) {
200
198
  const path = paths[locale] ?? "/";
201
199
  const label = path;
202
200
  const siteUrl = urls[locale] ?? path;
203
- const iframeSrc = ready ? withEditorParam(draftAddress(siteUrl)) : null;
201
+ const iframeSrc = ready ? canvasAddress(siteUrl) : null;
204
202
  // Canvas-originated selection: the node is under the cursor already, so
205
203
  // this path deliberately skips the scroll every other selection does.
206
204
  // (`onCanvasSelect` is declared further down; the handler runs at event time.)
@@ -240,16 +238,9 @@ export function EditorView({ ops, registry, id }) {
240
238
  setTouched(false);
241
239
  setGlobalDirty(false);
242
240
  setGlobalTouched(false);
243
- // ops is a fetch to the admin's endpoint — it can reject outright (offline,
244
- // aborted), not just resolve to a typed error. Either failure mode
245
- // must land on the same full-pane error, never a hung load.
246
- let previewResult;
247
- try {
248
- previewResult = await ops.preview.enable();
249
- }
250
- catch {
251
- previewResult = { ok: false, code: "internal", message: "Couldn't reach the server." };
252
- }
241
+ // The service signs a grant, the site's preview route sets the cookie with it (preview-grant.ts).
242
+ // A rejected call and a refusal at either end land on the same full-pane error, never a hung load.
243
+ const previewResult = await enableDraft(() => ops.preview.enable(), previewUrl());
253
244
  if (cancelled)
254
245
  return;
255
246
  if (!previewResult.ok) {
@@ -781,6 +772,8 @@ export function EditorView({ ops, registry, id }) {
781
772
  setGlobalBusy("publish");
782
773
  const r = await globalEdit.setStatus(name, "published");
783
774
  setGlobalBusy(null);
775
+ if (r.ok)
776
+ notePublished();
784
777
  if (!r.ok && !isStale(r))
785
778
  setBanner({ text: r.message, fields: r.fields }); // a stale refusal arrives through globalEdit.stale
786
779
  };
@@ -789,6 +782,8 @@ export function EditorView({ ops, registry, id }) {
789
782
  setGlobalBusy("unpublish");
790
783
  const r = await globalEdit.setStatus(name, "draft");
791
784
  setGlobalBusy(null);
785
+ if (r.ok)
786
+ notePublished();
792
787
  if (!r.ok)
793
788
  setBanner({ text: r.message, fields: r.fields });
794
789
  };
@@ -901,7 +896,7 @@ export function EditorView({ ops, registry, id }) {
901
896
  trailing: registry.locales.supported.length > 1 && (_jsx(LocaleSwitcher, { registry: registry, present: record ? registry.locales.supported.filter((l) => record.locales[l]) : [locale], parentLocales: parentLocales, value: locale, adding: adding, onSwitch: (l) => { void switchLocale(l); }, onAdd: (l) => { void doAddLocale(l); } })),
902
897
  },
903
898
  { label },
904
- ] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error" || saveState === "refused") && _jsx(SaveIndicator, { state: saveState, pending: dirty }), ready && (_jsx(LinkButton, { icon: true, href: siteUrl, target: "_blank", rel: "noreferrer", "aria-label": "View live", title: "View live \u2014 opens the site route. While the admin's draft-mode cookie is on, this shows the DRAFT \u2014 open it in a private window to see what visitors see.", children: _jsx(Eye, { size: 16, strokeWidth: 1.5, "aria-hidden": "true" }) })), _jsx(PublishControl, { status: record?.locales[locale] ? localeStatusOf(record.locales[locale]) : null, pending: dirty || saveState !== "saved", busy: pageBusy, flash: published, onPublish: () => { void doPublish(); }, onDiscard: () => { void doDiscard(); }, onUnpublish: () => { void doUnpublish(); }, onHistory: () => { void doHistory(); } })] }) }), target && _jsx(PresenceLine, { names: editorsOf(others, target), noun: "page" }), stale && _jsx(StaleBanner, { info: stale, onReload: loadTheirs }), _jsx(DriftNotice, { draft: findings, live: liveFindings }), history && record?.locales[locale] && (_jsx(HistoryModal, { rows: history, draftId: record.locales[locale].draftVersionId, onClose: () => setHistory(null), onRestore: doRestore, render: (versionId) => _jsx("iframe", { className: "sm-history__frame", title: "Version preview", src: withVersionParam(draftAddress(siteUrl), versionId) }) })), _jsxs("div", { className: "sm-editor", inert: stale ? true : undefined, children: [_jsxs("div", { className: "sm-canvas", children: [(banner || diagnosis || crashedLines.length > 0) && (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, padding: "0 20px 10px" }, children: [banner && (_jsxs(BannerBox, { tone: banner.fields && banner.fields.length > 0 ? "warn" : "error", onDismiss: () => setBanner(null), children: [_jsx("p", { children: banner.text }), _jsx(FieldErrorList, { fields: banner.fields ?? [] })] })), diagnosis && _jsx(CanvasWarning, { cause: diagnosis.cause, path: diagnosis.path }), crashedLines.length > 0 && (_jsx(BannerBox, { tone: "warn", children: crashedLines.map((line, i) => _jsx("p", { children: line }, i)) }))] })), _jsx("div", { className: "sm-canvas__scroll", ref: scrollerRef, onClick: (e) => { if (e.target === e.currentTarget)
899
+ ] }) }), right: _jsxs(_Fragment, { children: [(touched || saveState === "error" || saveState === "refused") && _jsx(SaveIndicator, { state: saveState, pending: dirty }), ready && (_jsx(LinkButton, { icon: true, href: siteUrl, target: "_blank", rel: "noreferrer", "aria-label": "View live", title: "View live \u2014 opens the site route. While the admin's draft-mode cookie is on, this shows the DRAFT \u2014 open it in a private window to see what visitors see.", children: _jsx(Eye, { size: 16, strokeWidth: 1.5, "aria-hidden": "true" }) })), _jsx(PublishControl, { status: record?.locales[locale] ? localeStatusOf(record.locales[locale]) : null, pending: dirty || saveState !== "saved", busy: pageBusy, flash: published, onPublish: () => { void doPublish(); }, onDiscard: () => { void doDiscard(); }, onUnpublish: () => { void doUnpublish(); }, onHistory: () => { void doHistory(); } })] }) }), target && _jsx(PresenceLine, { names: editorsOf(others, target), noun: "page" }), stale && _jsx(StaleBanner, { info: stale, onReload: loadTheirs }), _jsx(DriftNotice, { draft: findings, live: liveFindings }), history && record?.locales[locale] && (_jsx(HistoryModal, { rows: history, draftId: record.locales[locale].draftVersionId, onClose: () => setHistory(null), onRestore: doRestore, render: (versionId) => _jsx("iframe", { className: "sm-history__frame", title: "Version preview", src: versionAddress(siteUrl, versionId) }) })), _jsxs("div", { className: "sm-editor", inert: stale ? true : undefined, children: [_jsxs("div", { className: "sm-canvas", children: [(banner || diagnosis || crashedLines.length > 0) && (_jsxs("div", { style: { display: "flex", flexDirection: "column", gap: 8, padding: "0 20px 10px" }, children: [banner && (_jsxs(BannerBox, { tone: banner.fields && banner.fields.length > 0 ? "warn" : "error", onDismiss: () => setBanner(null), children: [_jsx("p", { children: banner.text }), _jsx(FieldErrorList, { fields: banner.fields ?? [] })] })), diagnosis && _jsx(CanvasWarning, { cause: diagnosis.cause, path: diagnosis.path }), crashedLines.length > 0 && (_jsx(BannerBox, { tone: "warn", children: crashedLines.map((line, i) => _jsx("p", { children: line }, i)) }))] })), _jsx("div", { className: "sm-canvas__scroll", ref: scrollerRef, onClick: (e) => { if (e.target === e.currentTarget)
905
900
  setSelectedId(null); }, children: _jsxs("div", { className: "sm-page", style: { overflow: "hidden" }, children: [iframeSrc ? (_jsx("iframe", { ref: iframeRef, src: iframeSrc, title: "Page canvas", onLoad: onFrameLoad, style: { width: "100%", height: docH, border: 0, display: "block" } })) : (_jsx("div", { className: "sm-canvas__loading", children: "Loading canvas\u2026" })), _jsx(CanvasOverlay, { emptyZones: emptyZones, hoverRect: hoverRect, hoverGlobal: hoverGlobal, selection: selection, fieldHover: fieldHover, editing: editing, plusButtons: plusButtons, onPlus: (p, x, y) => setPicker({ zone: p.zone, index: p.index, x: x - 150, y: y + 12 }), onMove: doMove, onDuplicate: doDuplicate, onRemove: doRemove }), inline.richtext && (() => {
906
901
  const { session } = inline.richtext;
907
902
  const item = globalItemOf(session.node);
@@ -1,7 +1,6 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { Allowed } from "../../toolset.ts";
3
3
  import { type ActiveState, type ToolbarCommand } from "../forms/RichtextToolbar.tsx";
4
- export declare const FLOATBAR_W = 340;
5
4
  export declare function FloatingToolbar({ x, top, bottom, pageWidth, allowed, active, canInsert, onCommand, popover }: {
6
5
  /** The caret's left, top and bottom in overlay coordinates. */
7
6
  x: number;
@@ -1,7 +1,7 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { RichtextToolbar } from "../forms/RichtextToolbar.js";
4
- export const FLOATBAR_W = 340;
4
+ const FLOATBAR_W = 340;
5
5
  const GAP = 10;
6
6
  const FLIP_AT = 56;
7
7
  export function FloatingToolbar({ x, top, bottom, pageWidth, allowed, active, canInsert, onCommand, popover }) {
@@ -1,5 +1,3 @@
1
- export declare const SEO_TITLE_MAX = 60;
2
- export declare const SEO_DESCRIPTION_MAX = 160;
3
1
  export type SeoKey = "title" | "description" | "ogImage" | "noindex";
4
2
  export declare function SeoSettings({ meta, onPatch, titleHint, descriptionHint, imageHint }: {
5
3
  meta: Record<string, unknown>;
@@ -2,8 +2,8 @@
2
2
  import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
3
3
  import { MediaWidget } from "../forms/MediaWidget.js";
4
4
  import { Field, Switch } from "../ui/primitives.js";
5
- export const SEO_TITLE_MAX = 60;
6
- export const SEO_DESCRIPTION_MAX = 160;
5
+ const SEO_TITLE_MAX = 60;
6
+ const SEO_DESCRIPTION_MAX = 160;
7
7
  const str = (v) => (typeof v === "string" ? v : "");
8
8
  function Counter({ n, max }) {
9
9
  return _jsxs("span", { className: n > max ? "sm-counter--over" : undefined, children: [n, "/", max] });
@@ -1,2 +1,10 @@
1
1
  export declare function setPreviewBase(value: string): void;
2
2
  export declare const draftAddress: (siteUrl: string) => string;
3
+ export declare function setCanvasMarks(value: "path" | "query"): void;
4
+ /** The canvas: the draft with the editor bridge. */
5
+ export declare const canvasAddress: (siteUrl: string) => string;
6
+ /** History's frame (spec 2026-09-13 history §5): a stored version, no bridge. */
7
+ export declare const versionAddress: (siteUrl: string, versionId: string) => string;
8
+ /** The site's preview route, from the page data: where the grant goes. */
9
+ export declare function setPreviewUrl(value: string): void;
10
+ export declare function previewUrl(): string;
@@ -1,8 +1,24 @@
1
1
  // Where a page's draft is loaded (DESIGN.md Vision, preview per framework): the server names a base once,
2
- // in the page's data, and the canvas and the History frame put it before the page's address. Module
3
- // state, set by the browser entry before the shell mounts: the admin is one script on one page.
2
+ // in the page's data, and the canvas and the History frame put it before the page's address, and says how
3
+ // they mark what they want: in the query, or in the path's last segment where the host strips the query
4
+ // (Next on Vercel; canvas-marker.ts). Module state, set by the browser entry before the shell mounts: the
5
+ // admin is one script on one page.
6
+ import { markCanvasPath } from "../../canvas-marker.js";
4
7
  let base = "";
8
+ let marks = "query";
5
9
  export function setPreviewBase(value) {
6
10
  base = value;
7
11
  }
8
12
  export const draftAddress = (siteUrl) => base + siteUrl;
13
+ export function setCanvasMarks(value) {
14
+ marks = value;
15
+ }
16
+ const withQuery = (url, key, value) => `${url}${url.includes("?") ? "&" : "?"}${key}=${encodeURIComponent(value)}`;
17
+ /** The canvas: the draft with the editor bridge. */
18
+ export const canvasAddress = (siteUrl) => marks === "path" ? markCanvasPath(draftAddress(siteUrl), { editor: true }) : withQuery(draftAddress(siteUrl), "smoodly-editor", "1");
19
+ /** History's frame (spec 2026-09-13 history §5): a stored version, no bridge. */
20
+ export const versionAddress = (siteUrl, versionId) => marks === "path" ? markCanvasPath(draftAddress(siteUrl), { version: versionId }) : withQuery(draftAddress(siteUrl), "smoodly-version", versionId);
21
+ let url = "";
22
+ /** The site's preview route, from the page data: where the grant goes. */
23
+ export function setPreviewUrl(value) { url = value; }
24
+ export function previewUrl() { return url; }
@@ -42,6 +42,8 @@ export type CanvasCause = {
42
42
  export declare function probeOf(requested: string, res: Response): Promise<CanvasProbe>;
43
43
  /** The first matching cause, in the spec's order (§4). */
44
44
  export declare function diagnoseCanvas(probe: CanvasProbe): CanvasCause;
45
+ /** The page's address from the canvas's: a path-marked canvas (canvas-marker.ts) without its last segment. */
46
+ export declare const pagePathOf: (pathname: string) => string;
45
47
  /** The warning's two lines for a cause (§5, verbatim). */
46
48
  export declare function canvasMessage(cause: CanvasCause, path: string): {
47
49
  editor: string;
@@ -37,10 +37,13 @@ export function diagnoseCanvas(probe) {
37
37
  return { kind: "framing", ...framing };
38
38
  return { kind: "script" };
39
39
  }
40
+ /** The page's address from the canvas's: a path-marked canvas (canvas-marker.ts) without its last segment. */
41
+ export const pagePathOf = (pathname) => pathname.replace(/\/(~|%7[Ee])editor$/, "") || "/";
40
42
  /** The redirect's end as a visitor would read it: path + search without the editor parameter, the full URL off-origin. */
41
43
  function redirectTarget(url, requested) {
42
44
  const u = new URL(url);
43
45
  u.searchParams.delete("smoodly-editor");
46
+ u.pathname = pagePathOf(u.pathname);
44
47
  return u.origin === new URL(requested).origin ? u.pathname + u.search : u.href;
45
48
  }
46
49
  /** The header that keeps the canvas iframe out, if any: X-Frame-Options DENY, or a frame-ancestors without the site's own origin. */
@@ -18,10 +18,6 @@ export declare const isPlainTextHost: (el: {
18
18
  * refresh reconciles into it; else as new text content. False when the
19
19
  * element is not a plain text host. */
20
20
  export declare function writeTextInto(el: TextHost, value: string): boolean;
21
- /** The container's layout the richtext editor root copies (spec §5), so a
22
- * flex-column or grid container keeps its block spacing while its
23
- * children are hidden behind the editor. Kebab-cased, for setProperty. */
24
- export declare const LAYOUT_PROPS: readonly ["display", "flex-direction", "gap", "row-gap", "column-gap", "grid-template-columns", "grid-auto-flow", "align-items", "justify-content"];
25
21
  export declare function layoutToCopy(computed: {
26
22
  getPropertyValue(name: string): string;
27
23
  }): Record<string, string>;
@@ -25,7 +25,7 @@ export function writeTextInto(el, value) {
25
25
  /** The container's layout the richtext editor root copies (spec §5), so a
26
26
  * flex-column or grid container keeps its block spacing while its
27
27
  * children are hidden behind the editor. Kebab-cased, for setProperty. */
28
- export const LAYOUT_PROPS = [
28
+ const LAYOUT_PROPS = [
29
29
  "display", "flex-direction", "gap", "row-gap", "column-gap",
30
30
  "grid-template-columns", "grid-auto-flow", "align-items", "justify-content",
31
31
  ];
@@ -0,0 +1,6 @@
1
+ import type { OpResult, PreviewGrant } from "../ops.ts";
2
+ export declare function postPreviewGrant(url: string, action: "enable" | "disable", grant: PreviewGrant, fetcher?: typeof fetch): Promise<OpResult<null>>;
3
+ /** The editor's load: the operation, then the grant to the site. The operation is a fetch to the service
4
+ * and can reject outright (offline, aborted), not just resolve to a typed error; both land on one typed
5
+ * result. A null grant means the server set the cookie itself: nothing to post. */
6
+ export declare function enableDraft(enable: () => Promise<OpResult<PreviewGrant | null>>, url: string, fetcher?: typeof fetch): Promise<OpResult<null>>;
@@ -0,0 +1,25 @@
1
+ export async function postPreviewGrant(url, action, grant, fetcher = fetch) {
2
+ try {
3
+ const res = await fetcher(url, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: JSON.stringify({ action, grant }) });
4
+ const body = (await res.json());
5
+ return typeof body === "object" && body !== null && typeof body.ok === "boolean" ? body : { ok: false, code: "internal", message: `smoodly: the preview route answered ${res.status} without a result.` };
6
+ }
7
+ catch {
8
+ return { ok: false, code: "internal", message: "Couldn't reach the site's preview route." };
9
+ }
10
+ }
11
+ /** The editor's load: the operation, then the grant to the site. The operation is a fetch to the service
12
+ * and can reject outright (offline, aborted), not just resolve to a typed error; both land on one typed
13
+ * result. A null grant means the server set the cookie itself: nothing to post. */
14
+ export async function enableDraft(enable, url, fetcher = fetch) {
15
+ let result;
16
+ try {
17
+ result = await enable();
18
+ }
19
+ catch {
20
+ return { ok: false, code: "internal", message: "Couldn't reach the server." };
21
+ }
22
+ if (!result.ok)
23
+ return result;
24
+ return result.data ? postPreviewGrant(url, "enable", result.data, fetcher) : { ok: true, data: null };
25
+ }
@@ -4,7 +4,7 @@
4
4
  // along and the server renders what the frame got — and name the cause.
5
5
  // A new src, or the bridge speaking after all, drops the answer.
6
6
  import { useEffect, useState } from "react";
7
- import { diagnoseCanvas, probeOf } from "./canvas-diagnosis.js";
7
+ import { diagnoseCanvas, pagePathOf, probeOf } from "./canvas-diagnosis.js";
8
8
  export function useCanvasDiagnosis(iframeSrc, geometry) {
9
9
  const [diagnosis, setDiagnosis] = useState(null);
10
10
  useEffect(() => {
@@ -17,7 +17,7 @@ export function useCanvasDiagnosis(iframeSrc, geometry) {
17
17
  .then((res) => probeOf(requested.href, res))
18
18
  .catch(() => ({ kind: "failed" }))
19
19
  .then((probe) => { if (live)
20
- setDiagnosis({ cause: diagnoseCanvas(probe), path: requested.pathname }); });
20
+ setDiagnosis({ cause: diagnoseCanvas(probe), path: pagePathOf(requested.pathname) }); });
21
21
  return () => { live = false; };
22
22
  }, [iframeSrc, geometry]);
23
23
  return diagnosis;