smoodly 0.0.44 → 0.0.46

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (241) hide show
  1. package/README.md +3 -3
  2. package/dist/addresses.d.ts +6 -7
  3. package/dist/addresses.js +6 -4
  4. package/dist/admin/PublicSans-LICENSE.txt +93 -0
  5. package/dist/admin/auth-config.d.ts +6 -0
  6. package/dist/admin/auth-config.js +1 -0
  7. package/dist/admin/browser/fetch-op.d.ts +10 -0
  8. package/dist/admin/browser/fetch-op.js +44 -0
  9. package/dist/admin/browser/main.d.ts +1 -0
  10. package/dist/admin/browser/main.js +21 -0
  11. package/dist/admin/custom-section.d.ts +1 -1
  12. package/dist/admin/drift.d.ts +1 -1
  13. package/dist/admin/editor/EditorView.js +14 -14
  14. package/dist/admin/editor/autosave.js +1 -1
  15. package/dist/admin/editor/canvas-address.d.ts +2 -0
  16. package/dist/admin/editor/canvas-address.js +8 -0
  17. package/dist/admin/editor/overlay-geometry.d.ts +1 -1
  18. package/dist/admin/editor/protocol.d.ts +18 -4
  19. package/dist/admin/editor/protocol.js +15 -1
  20. package/dist/admin/editor/useCanvasBridge.d.ts +1 -1
  21. package/dist/admin/editor/useCanvasBridge.js +5 -6
  22. package/dist/admin/editor/useInlineSession.js +4 -2
  23. package/dist/admin/files.d.ts +11 -0
  24. package/dist/admin/files.generated.js +2 -0
  25. package/dist/admin/files.js +8 -0
  26. package/dist/admin/forms/useRefOptions.d.ts +2 -2
  27. package/dist/admin/forms/useRefOptions.js +2 -2
  28. package/dist/admin/forms/useRichtextEditor.js +4 -4
  29. package/dist/admin/handler.d.ts +40 -0
  30. package/dist/admin/handler.js +151 -0
  31. package/dist/admin/index.d.ts +1 -1
  32. package/dist/admin/ops-impl.d.ts +7 -1
  33. package/dist/admin/ops-impl.js +110 -33
  34. package/dist/admin/ops.d.ts +3 -2
  35. package/dist/admin/page-data.d.ts +21 -0
  36. package/dist/admin/page-data.js +4 -0
  37. package/dist/admin/page.d.ts +6 -0
  38. package/dist/admin/page.js +35 -0
  39. package/dist/admin/serialize.js +7 -7
  40. package/dist/admin/shell/AdminApp.js +2 -2
  41. package/dist/admin/shell/EntryForm.js +1 -1
  42. package/dist/admin/shell/router.d.ts +3 -16
  43. package/dist/admin/shell/router.js +3 -39
  44. package/dist/admin/shell/session.d.ts +2 -5
  45. package/dist/admin/supabase-auth.js +1 -1
  46. package/dist/admin/tree-ops.d.ts +1 -1
  47. package/dist/admin/ui/fonts.d.ts +12 -0
  48. package/dist/admin/ui/fonts.js +22 -0
  49. package/dist/admin/ui/theme.d.ts +13 -4
  50. package/dist/admin/ui/theme.js +28 -157
  51. package/dist/assemble.d.ts +60 -0
  52. package/dist/assemble.js +359 -0
  53. package/dist/astro/cache.d.ts +34 -0
  54. package/dist/astro/cache.js +69 -0
  55. package/dist/astro/components/Custom.astro +21 -0
  56. package/dist/astro/components/Guarded.astro +24 -0
  57. package/dist/astro/components/Head.astro +26 -0
  58. package/dist/astro/components/Image.astro +20 -0
  59. package/dist/astro/components/RichText.astro +23 -0
  60. package/dist/astro/components/Section.astro +20 -0
  61. package/dist/astro/components/Video.astro +5 -0
  62. package/dist/astro/components/Zone.astro +19 -0
  63. package/dist/astro/components.d.ts +6 -0
  64. package/dist/astro/components.js +7 -0
  65. package/dist/astro/content-loaders.d.ts +40 -0
  66. package/dist/astro/content-loaders.js +100 -0
  67. package/dist/astro/content.d.ts +9 -0
  68. package/dist/astro/content.js +17 -0
  69. package/dist/astro/custom.d.ts +1 -0
  70. package/dist/astro/custom.js +3 -0
  71. package/dist/astro/defaults.d.ts +20 -0
  72. package/dist/astro/defaults.js +21 -0
  73. package/dist/astro/env-files.d.ts +1 -0
  74. package/dist/astro/env-files.js +21 -0
  75. package/dist/astro/head.d.ts +13 -0
  76. package/dist/astro/head.js +19 -0
  77. package/dist/astro/image.d.ts +27 -0
  78. package/dist/astro/image.js +29 -0
  79. package/dist/astro/index.d.ts +43 -0
  80. package/dist/astro/index.js +111 -0
  81. package/dist/astro/locale-paths.d.ts +30 -0
  82. package/dist/astro/locale-paths.js +38 -0
  83. package/dist/astro/middleware.d.ts +1 -0
  84. package/dist/astro/middleware.js +83 -0
  85. package/dist/astro/preview-document.d.ts +1 -0
  86. package/dist/astro/preview-document.js +18 -0
  87. package/dist/astro/preview.d.ts +10 -0
  88. package/dist/astro/preview.js +72 -0
  89. package/dist/astro/richtext.d.ts +15 -0
  90. package/dist/astro/richtext.js +78 -0
  91. package/dist/astro/routes/admin.d.ts +3 -0
  92. package/dist/astro/routes/admin.js +41 -0
  93. package/dist/astro/routes/page.astro +17 -0
  94. package/dist/astro/routes/preview.astro +11 -0
  95. package/dist/astro/runtime.d.ts +32 -0
  96. package/dist/astro/runtime.js +46 -0
  97. package/dist/astro/scope.d.ts +45 -0
  98. package/dist/astro/scope.js +103 -0
  99. package/dist/astro/server.d.ts +14 -0
  100. package/dist/astro/server.js +45 -0
  101. package/dist/astro/shell.d.ts +67 -0
  102. package/dist/astro/shell.js +63 -0
  103. package/dist/astro/style.d.ts +1 -0
  104. package/dist/astro/style.js +11 -0
  105. package/dist/astro/views-check.d.ts +3 -0
  106. package/dist/astro/views-check.js +19 -0
  107. package/dist/astro/zone.d.ts +39 -0
  108. package/dist/astro/zone.js +33 -0
  109. package/dist/bind.d.ts +19 -0
  110. package/dist/bind.js +34 -0
  111. package/dist/bridge/bridge.d.ts +9 -0
  112. package/dist/bridge/bridge.js +107 -0
  113. package/dist/bridge/entry.d.ts +1 -0
  114. package/dist/bridge/entry.js +5 -0
  115. package/dist/bridge/preview-pill.d.ts +18 -0
  116. package/dist/bridge/preview-pill.js +115 -0
  117. package/dist/cli/import.d.ts +3 -0
  118. package/dist/cli/import.js +10 -1
  119. package/dist/cli/load-collections.js +4 -3
  120. package/dist/cli/load-config.d.ts +4 -0
  121. package/dist/cli/load-config.js +26 -0
  122. package/dist/cli/main.d.ts +7 -1
  123. package/dist/cli/main.js +32 -7
  124. package/dist/cli/migrate.d.ts +3 -0
  125. package/dist/cli/migrate.js +8 -1
  126. package/dist/cli/rebuild.d.ts +23 -0
  127. package/dist/cli/rebuild.js +22 -0
  128. package/dist/cli/space-io.d.ts +15 -1
  129. package/dist/cli/space-io.js +14 -0
  130. package/dist/client.js +5 -0
  131. package/dist/config.d.ts +28 -20
  132. package/dist/config.js +71 -75
  133. package/dist/content/content-api.d.ts +1 -1
  134. package/dist/content/node.d.ts +2 -3
  135. package/dist/content/node.js +2 -1
  136. package/dist/crash-message.d.ts +1 -0
  137. package/dist/crash-message.js +8 -0
  138. package/dist/css.d.ts +4 -0
  139. package/dist/css.js +5 -2
  140. package/dist/custom/CustomSection.d.ts +1 -1
  141. package/dist/custom/CustomSection.js +4 -8
  142. package/dist/custom/css.js +2 -2
  143. package/dist/custom/index.d.ts +6 -10
  144. package/dist/custom/index.js +1 -2
  145. package/dist/custom/layout.d.ts +5 -0
  146. package/dist/custom/layout.js +14 -0
  147. package/dist/entry-page.d.ts +3 -18
  148. package/dist/env.js +1 -1
  149. package/dist/fields.js +2 -1
  150. package/dist/global-id.js +2 -2
  151. package/dist/global.d.ts +5 -2
  152. package/dist/global.js +8 -5
  153. package/dist/image/next-config.d.ts +3 -0
  154. package/dist/image/next-config.js +12 -7
  155. package/dist/image/remote-pattern.d.ts +1 -0
  156. package/dist/image/remote-pattern.js +5 -0
  157. package/dist/index.d.ts +15 -17
  158. package/dist/index.js +10 -19
  159. package/dist/marks.d.ts +7 -0
  160. package/dist/marks.js +7 -0
  161. package/dist/next/admin-route.d.ts +7 -0
  162. package/dist/next/admin-route.js +28 -0
  163. package/dist/next/bridge.d.ts +1 -0
  164. package/dist/next/bridge.js +26 -0
  165. package/dist/next/canvas-boundary.d.ts +1 -1
  166. package/dist/next/canvas-slot.d.ts +1 -0
  167. package/dist/next/canvas-slot.js +4 -3
  168. package/dist/next/index.d.ts +2 -0
  169. package/dist/next/index.js +1 -0
  170. package/dist/next/page-renderer.d.ts +5 -1
  171. package/dist/next/page-renderer.js +19 -33
  172. package/dist/next/preview-pill-define.d.ts +1 -0
  173. package/dist/next/preview-pill-define.js +10 -0
  174. package/dist/next/preview-pill.d.ts +0 -3
  175. package/dist/next/preview-pill.js +7 -50
  176. package/dist/next/queries.d.ts +3 -28
  177. package/dist/next/queries.js +3 -90
  178. package/dist/next/reads.d.ts +2 -0
  179. package/dist/next/reads.js +18 -0
  180. package/dist/page-context.d.ts +89 -0
  181. package/dist/page-context.js +8 -0
  182. package/dist/pairing.d.ts +2 -22
  183. package/dist/project.d.ts +69 -0
  184. package/dist/project.js +210 -0
  185. package/dist/published.d.ts +96 -0
  186. package/dist/published.js +125 -0
  187. package/dist/react.d.ts +61 -0
  188. package/dist/react.js +17 -0
  189. package/dist/read-shapes.d.ts +1 -1
  190. package/dist/reads.d.ts +80 -0
  191. package/dist/reads.js +142 -0
  192. package/dist/refs.d.ts +1 -1
  193. package/dist/registration.d.ts +8 -0
  194. package/dist/registration.js +17 -0
  195. package/dist/render.d.ts +5 -83
  196. package/dist/render.js +5 -10
  197. package/dist/resolve.d.ts +1 -1
  198. package/dist/richtext/index.d.ts +6 -2
  199. package/dist/richtext/index.js +3 -2
  200. package/dist/richtext-href.d.ts +4 -0
  201. package/dist/richtext-href.js +17 -0
  202. package/dist/richtext-render.js +1 -14
  203. package/dist/schema-version.d.ts +1 -1
  204. package/dist/schema-version.js +1 -1
  205. package/dist/section-crash.d.ts +2 -2
  206. package/dist/section-crash.js +2 -7
  207. package/dist/section-wrapper.d.ts +3 -10
  208. package/dist/section-wrapper.js +3 -2
  209. package/dist/site.d.ts +38 -14
  210. package/dist/site.js +159 -286
  211. package/dist/smd-key.d.ts +9 -0
  212. package/dist/smd-key.js +1 -0
  213. package/dist/sql-space.d.ts +4 -0
  214. package/dist/sql-space.js +10 -5
  215. package/dist/sql.d.ts +2 -0
  216. package/dist/sql.js +117 -0
  217. package/dist/store.d.ts +1 -1
  218. package/dist/supabase-published-store.d.ts +35 -0
  219. package/dist/supabase-published-store.js +215 -0
  220. package/dist/supabase-store.d.ts +1 -1
  221. package/dist/toolset.d.ts +2 -4
  222. package/dist/toolset.js +6 -4
  223. package/dist/version.d.ts +1 -1
  224. package/dist/version.js +1 -1
  225. package/dist/view-props.d.ts +40 -0
  226. package/dist/view-props.js +1 -0
  227. package/dist/views.d.ts +36 -0
  228. package/dist/views.js +52 -0
  229. package/dist/zones.d.ts +7 -7
  230. package/dist/zones.js +7 -6
  231. package/migrations/20261007120000_smoodly_v8.sql +154 -0
  232. package/migrations/20261007200000_smoodly_v9.sql +30 -0
  233. package/package.json +46 -19
  234. package/dist/admin/next/bridge.d.ts +0 -2
  235. package/dist/admin/next/bridge.js +0 -129
  236. package/dist/admin/next/create-admin.d.ts +0 -19
  237. package/dist/admin/next/create-admin.js +0 -44
  238. package/dist/admin/next/index.d.ts +0 -8
  239. package/dist/admin/next/index.js +0 -8
  240. package/dist/admin/next/op-handler.d.ts +0 -18
  241. package/dist/admin/next/op-handler.js +0 -31
package/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # smoodly
2
2
 
3
- A code-first CMS for Next.js. Define a component once in code and it
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
5
  the rails you set. Content lives in Supabase; the admin mounts into
6
- your own app at `/admin`.
6
+ 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 a Next.js site with the admin mounted, the database schema
12
+ This creates an Astro or Next.js site with the admin mounted, the database schema
13
13
  in place and a first editor who can sign in.
14
14
 
15
15
  ## Environment
@@ -1,4 +1,5 @@
1
1
  import { type LocaleSet } from "./paths.ts";
2
+ import { type SchemaOr } from "./registration.ts";
2
3
  /** owner key → locale → address, without slashes at either end (`company/news`). */
3
4
  export type Addresses = Record<string, Record<string, string>>;
4
5
  export declare const collectionKey: (name: string) => string;
@@ -21,12 +22,10 @@ export type CodeAddress = {
21
22
  collection: string | null;
22
23
  };
23
24
  type PageLike = {
24
- schema: {
25
- kind: string;
26
- name: string;
27
- title?: string;
28
- slug?: string | Record<string, string>;
29
- };
25
+ kind: string;
26
+ name: string;
27
+ title?: string;
28
+ slug?: string | Record<string, string>;
30
29
  };
31
30
  type CollectionLike = {
32
31
  name: string;
@@ -35,7 +34,7 @@ type CollectionLike = {
35
34
  };
36
35
  export declare function codeAddresses(input: {
37
36
  locales: LocaleSet;
38
- pages: ReadonlyArray<PageLike>;
37
+ pages: ReadonlyArray<SchemaOr<PageLike>>;
39
38
  collections: ReadonlyArray<CollectionLike>;
40
39
  }): CodeAddress[];
41
40
  export declare const addressMap: (list: CodeAddress[]) => Addresses;
package/dist/addresses.js CHANGED
@@ -5,6 +5,7 @@
5
5
  // rule, so SEGMENT_RE stays the only gate. Pure: defineConfig, the
6
6
  // materializer, the stores, the site and the admin tree all read it here.
7
7
  import { assertSegment, fixedPageSegments } from "./paths.js";
8
+ import { schemaOf } from "./registration.js";
8
9
  export const collectionKey = (name) => `collection:${name}`;
9
10
  export const pageKey = (name) => `page:${name}`;
10
11
  /** The collection an owner key names, or null for a fixed page. */
@@ -23,11 +24,12 @@ export function codeAddresses(input) {
23
24
  const def = input.locales.default;
24
25
  const byDefault = new Map();
25
26
  const out = [];
26
- for (const p of input.pages) {
27
- if (p.schema.kind !== "page" || p.schema.slug === undefined)
27
+ for (const carrier of input.pages) {
28
+ const p = schemaOf(carrier);
29
+ if (p.kind !== "page" || p.slug === undefined)
28
30
  continue;
29
- const segments = fixedPageSegments(p.schema.slug, input.locales);
30
- const spec = { key: pageKey(p.schema.name), segments, title: p.schema.title ?? p.schema.name, template: p.schema.name, collection: null };
31
+ const segments = fixedPageSegments(p.slug, input.locales);
32
+ const spec = { key: pageKey(p.name), segments, title: p.title ?? p.name, template: p.name, collection: null };
31
33
  byDefault.set(segments[def], spec);
32
34
  out.push(spec);
33
35
  }
@@ -0,0 +1,93 @@
1
+ Copyright 2015 The Public Sans Project Authors (https://github.com/uswds/public-sans) PublicSans-Italic[wght].ttf: Copyright 2015 The Public Sans Project Authors (https://github.com/uswds/public-sans)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -0,0 +1,6 @@
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")[];
6
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { AdminOpAction } from "../ops.ts";
2
+ /** JSON has no `undefined`: in an array it becomes null, which several ops read as a value (`expected:
3
+ * null` is "no draft", `undefined` is "skip the check"). Trailing ones are dropped; one between two
4
+ * arguments cannot be sent and is named here. */
5
+ export declare function wireArgs(args: unknown[]): unknown[];
6
+ /** `fetcher` is the test seam. A network failure rejects; every answer resolves to a typed result.
7
+ * One operation at a time, in call order: Next queued server actions per browser, and the admin's reads
8
+ * and writes rely on it (a read sent after a save must see that save's token; a save typed during a
9
+ * publish must follow the publish). A call that fails does not hold the ones behind it. */
10
+ export declare function fetchOp(url: string, fetcher?: typeof fetch): AdminOpAction;
@@ -0,0 +1,44 @@
1
+ /** JSON has no `undefined`: in an array it becomes null, which several ops read as a value (`expected:
2
+ * null` is "no draft", `undefined` is "skip the check"). Trailing ones are dropped; one between two
3
+ * arguments cannot be sent and is named here. */
4
+ export function wireArgs(args) {
5
+ let end = args.length;
6
+ while (end > 0 && args[end - 1] === undefined)
7
+ end--;
8
+ const out = args.slice(0, end);
9
+ if (out.some((a) => a === undefined)) {
10
+ throw new Error("smoodly: an operation argument is undefined between two others — pass null or a value; JSON cannot carry undefined.");
11
+ }
12
+ return out;
13
+ }
14
+ const noResult = (status) => ({ ok: false, code: "internal", message: `smoodly: the admin's endpoint answered ${status} without a result — see the server log.` });
15
+ /** `fetcher` is the test seam. A network failure rejects; every answer resolves to a typed result.
16
+ * One operation at a time, in call order: Next queued server actions per browser, and the admin's reads
17
+ * and writes rely on it (a read sent after a save must see that save's token; a save typed during a
18
+ * publish must follow the publish). A call that fails does not hold the ones behind it. */
19
+ export function fetchOp(url, fetcher = fetch) {
20
+ const send = async (call) => {
21
+ const res = await fetcher(url, {
22
+ method: "POST",
23
+ credentials: "same-origin",
24
+ headers: { "content-type": "application/json", ...(call.token ? { authorization: `Bearer ${call.token}` } : {}) },
25
+ body: JSON.stringify({ path: call.path, args: wireArgs(call.args) }),
26
+ });
27
+ let body;
28
+ try {
29
+ body = await res.json();
30
+ }
31
+ catch {
32
+ return noResult(res.status);
33
+ }
34
+ if (typeof body === "object" && body !== null && typeof body.ok === "boolean")
35
+ return body;
36
+ return noResult(res.status);
37
+ };
38
+ let last = Promise.resolve();
39
+ return (call) => {
40
+ const run = last.then(() => send(call));
41
+ last = run.catch(() => { });
42
+ return run;
43
+ };
44
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,21 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ // packages/smoodly/src/admin/browser/main.tsx
3
+ // The prebuilt admin's entry (spec 2026-10-02 astro cloud fit §4): reads the page's data, writes the
4
+ // stylesheet into the document and mounts the shell. Bundled by scripts/build-admin.ts; no app compiles it.
5
+ import { createRoot } from "react-dom/client";
6
+ import { ADMIN_DATA_ID, ADMIN_ROOT_ID } from "../page-data.js";
7
+ import { setPreviewBase } from "../editor/canvas-address.js";
8
+ import { AdminApp } from "../shell/AdminApp.js";
9
+ import { fontFaceCss } from "../ui/fonts.js";
10
+ import { ADMIN_CSS } from "../ui/theme.js";
11
+ import { fetchOp } from "./fetch-op.js";
12
+ const data = JSON.parse(document.getElementById(ADMIN_DATA_ID).textContent ?? "");
13
+ setPreviewBase(data.previewBase ?? "");
14
+ // One <style>, fonts first: the canvas copies this element's text into the site's frame (editor/useInlineSession.ts).
15
+ const style = document.createElement("style");
16
+ style.setAttribute("data-smoodly-admin-styles", "");
17
+ style.textContent = `${fontFaceCss(data.filesBase)}\n${ADMIN_CSS}`;
18
+ document.head.appendChild(style);
19
+ // The page's own background rules did their job (admin/page.ts): the stylesheet owns the look now.
20
+ document.querySelector("style[data-smoodly-boot]")?.remove();
21
+ createRoot(document.getElementById(ADMIN_ROOT_ID)).render(_jsx(AdminApp, { registry: data.registry, op: fetchOp(data.opUrl), auth: data.auth, presenceConfig: data.presence ?? undefined }));
@@ -1,5 +1,5 @@
1
1
  import type { AdminRegistry, AdminSection } from "./serialize.ts";
2
- import type { TreeNode } from "../render.tsx";
2
+ import type { TreeNode } from "../page-context.ts";
3
3
  import type { ZonePolicy } from "../zones.ts";
4
4
  export declare const customSection: (registry: Pick<AdminRegistry, "custom">) => AdminSection;
5
5
  /** Only a freeform zone that says so takes a custom section (the developer's per-zone switch). */
@@ -1,5 +1,5 @@
1
1
  import type { AdminField, AdminRegistry } from "./serialize.ts";
2
- import type { PageTree } from "../render.tsx";
2
+ import type { PageTree } from "../page-context.ts";
3
3
  export type DriftKind = "unknown-section" | "unknown-element" | "unknown-shape" | "unknown-collection" | "misfit" | "zone-disallowed" | "zone-over-max" | "orphan-field" | "orphan-zone" | "element-outside-toolset" | "localization-on" | "localization-off";
4
4
  export type Severity = "broken" | "orphan";
5
5
  export declare const SEVERITY: Record<DriftKind, Severity>;
@@ -38,6 +38,7 @@ import { tabOf, tabsFor } from "../forms/tabs.js";
38
38
  import { InspectorFields, valueOf } from "./InspectorFields.js";
39
39
  import { allowedSections, canInsert, fillLockedZones, findNode, insertNode, moveNode, moveNodeTo, newNodeId, nodeFromSample, patchNode, patchNodeCss, patchNodePath, patchPage, placementNode, removeNode, zoneOf, clearZone, unsetNodePath, } from "../tree-ops.js";
40
40
  import { useDraftAutosave } from "./autosave.js";
41
+ import { draftAddress } from "./canvas-address.js";
41
42
  import { CanvasOverlay } from "./CanvasOverlay.js";
42
43
  import { InlineRichtext } from "./InlineRichtext.js";
43
44
  import { InsertPicker } from "./InsertPicker.js";
@@ -198,7 +199,7 @@ export function EditorView({ ops, registry, id }) {
198
199
  const path = paths[locale] ?? "/";
199
200
  const label = path;
200
201
  const siteUrl = urls[locale] ?? path;
201
- const iframeSrc = ready ? withEditorParam(siteUrl) : null;
202
+ const iframeSrc = ready ? withEditorParam(draftAddress(siteUrl)) : null;
202
203
  // Canvas-originated selection: the node is under the cursor already, so
203
204
  // this path deliberately skips the scroll every other selection does.
204
205
  // (`onCanvasSelect` is declared further down; the handler runs at event time.)
@@ -238,7 +239,7 @@ export function EditorView({ ops, registry, id }) {
238
239
  setTouched(false);
239
240
  setGlobalDirty(false);
240
241
  setGlobalTouched(false);
241
- // ops is a server action — it can reject outright (offline, 500,
242
+ // ops is a fetch to the admin's endpoint — it can reject outright (offline,
242
243
  // aborted), not just resolve to a typed error. Either failure mode
243
244
  // must land on the same full-pane error, never a hung load.
244
245
  let previewResult;
@@ -287,10 +288,8 @@ export function EditorView({ ops, registry, id }) {
287
288
  // Locked zones can't be filled by any gesture, so fill the empty
288
289
  // ones here; a change marks the draft dirty so the debounced save
289
290
  // persists it and the canvas shows the same tree as the outline.
290
- // Read through the ref: `registry` is a server-component prop, so
291
- // its identity changes on every RSC refresh — and preview.enable's
292
- // cookie write triggers one. As a dependency it would loop this
293
- // effect forever (same reason AdminApp pins `ops` with useState).
291
+ // Read through the ref, so the registry is not a dependency of this
292
+ // effect: a load runs for a new page, locale or reload, nothing else.
294
293
  const reg = registryRef.current;
295
294
  const template = reg.pages.find((p) => p.name === result.data.record.template);
296
295
  const draft = fillLockedZones(result.data.draft, template?.zones ?? {}, reg.sections);
@@ -435,14 +434,15 @@ export function EditorView({ ops, registry, id }) {
435
434
  useSavedElsewhere(target, (event) => {
436
435
  if (stale)
437
436
  return;
438
- if (!(dirty || globalDirty || saveState !== "saved" || globalEdit.saveState !== "saved")) {
439
- keepSelectionRef.current = true;
440
- setReloadKey((k) => k + 1);
441
- }
442
- else if (editedRef.current)
437
+ const pending = dirty || globalDirty || saveState !== "saved" || globalEdit.saveState !== "saved";
438
+ if (pending && editedRef.current)
443
439
  setStale({ by: event.email, at: Date.now() });
440
+ // Clean, or only the fill is pending. The fill waits in the autosave's debounce, which no flag here shows
441
+ // (it marks without setDirty): always reset, or it would save their reloaded tree and move the token
442
+ // under the editor who just saved (found 2026-10-03, on a production build fast enough to open the page
443
+ // before the first fill landed).
444
444
  else
445
- loadTheirsQuietly(); // only the fill is pending: drop it
445
+ loadTheirsQuietly();
446
446
  });
447
447
  // Selecting from the outline must reveal the block. The canvas column is
448
448
  // the scroller now (the iframe has no scrollbar of its own — it is sized to
@@ -661,7 +661,7 @@ export function EditorView({ ops, registry, id }) {
661
661
  }
662
662
  catch (e) {
663
663
  console.error(e);
664
- // Same transport concern as the autosave persist: the server action can reject
664
+ // Same transport concern as the autosave persist: the fetch can reject
665
665
  // outright instead of resolving to a typed error.
666
666
  result = { ok: false, code: "internal", message: "Publish failed — check your connection and try again." };
667
667
  }
@@ -898,7 +898,7 @@ export function EditorView({ ops, registry, id }) {
898
898
  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); } })),
899
899
  },
900
900
  { label },
901
- ] }) }), 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(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)
901
+ ] }) }), 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)
902
902
  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 && (() => {
903
903
  const { session } = inline.richtext;
904
904
  const item = globalItemOf(session.node);
@@ -45,7 +45,7 @@ export function createDraftAutosave(opts) {
45
45
  }
46
46
  catch (e) {
47
47
  console.error(e);
48
- // persist is a server action — it can reject outright (offline, 500,
48
+ // persist is a fetch to the admin's endpoint — it can reject outright (offline,
49
49
  // aborted), not just resolve to a typed error. Fold that into the
50
50
  // same failure path so it isn't a stuck "saving" state.
51
51
  result = { ok: false, code: "internal", message: "" };
@@ -0,0 +1,2 @@
1
+ export declare function setPreviewBase(value: string): void;
2
+ export declare const draftAddress: (siteUrl: string) => string;
@@ -0,0 +1,8 @@
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.
4
+ let base = "";
5
+ export function setPreviewBase(value) {
6
+ base = value;
7
+ }
8
+ export const draftAddress = (siteUrl) => base + siteUrl;
@@ -1,4 +1,4 @@
1
- import type { TreeNode } from "../../render.tsx";
1
+ import type { TreeNode } from "../../page-context.ts";
2
2
  import type { BridgeRect, ZoneRect } from "./protocol.ts";
3
3
  export type PlusButton = {
4
4
  zone: string;
@@ -50,11 +50,25 @@ export type FromBridge = {
50
50
  source: "smoodly";
51
51
  kind: "rendered";
52
52
  };
53
+ /** The editor asks one thing of the canvas: render again. */
53
54
  export type ToBridge = {
54
55
  source: "smoodly";
55
56
  kind: "refresh";
56
- } | {
57
- source: "smoodly";
58
- kind: "scroll";
59
- id: string;
60
57
  };
58
+ /** What both guards read of a MessageEvent. */
59
+ type Incoming = {
60
+ data: unknown;
61
+ origin: string;
62
+ source: unknown;
63
+ };
64
+ /** The bridge's guard: a message counts only when the window framing this one sent it, from this
65
+ * origin, in Smoodly's envelope. The canvas is same-origin with the admin by design (DESIGN.md §2). */
66
+ export declare function toBridgeMessage(e: Incoming, win: {
67
+ parent: unknown;
68
+ location: {
69
+ origin: string;
70
+ };
71
+ }): ToBridge | null;
72
+ /** The editor's guard: a message counts only when the canvas frame sent it, from the admin's own origin. */
73
+ export declare function fromBridgeMessage(e: Incoming, frame: unknown, origin: string): FromBridge | null;
74
+ export {};
@@ -1 +1,15 @@
1
- export {};
1
+ /** The bridge's guard: a message counts only when the window framing this one sent it, from this
2
+ * origin, in Smoodly's envelope. The canvas is same-origin with the admin by design (DESIGN.md §2). */
3
+ export function toBridgeMessage(e, win) {
4
+ if (e.source !== win.parent || e.origin !== win.location.origin)
5
+ return null;
6
+ const m = e.data;
7
+ return typeof m === "object" && m?.source === "smoodly" && m.kind === "refresh" ? { source: "smoodly", kind: "refresh" } : null;
8
+ }
9
+ /** The editor's guard: a message counts only when the canvas frame sent it, from the admin's own origin. */
10
+ export function fromBridgeMessage(e, frame, origin) {
11
+ if (!frame || e.source !== frame || e.origin !== origin)
12
+ return null;
13
+ const m = e.data;
14
+ return typeof m === "object" && m?.source === "smoodly" ? m : null;
15
+ }
@@ -1,5 +1,5 @@
1
1
  import { type RefObject } from "react";
2
- import type { BridgeRect, FieldRect, ToBridge, ZoneRect } from "./protocol.ts";
2
+ import { type BridgeRect, type FieldRect, type ToBridge, type ZoneRect } from "./protocol.ts";
3
3
  import type { CrashedSection } from "../../marks.ts";
4
4
  export type Geometry = "waiting" | "ok" | "missing";
5
5
  export declare function useCanvasBridge({ iframeRef, iframeSrc, onSelect, onHover, onRendered }: {
@@ -1,11 +1,12 @@
1
1
  // The admin side of the editor⇄canvas protocol: the geometry the bridge
2
2
  // reports (node rects, empty zones, document height), select/hover
3
- // forwarded from the iframe, and `postToCanvas` for refresh/scroll.
3
+ // forwarded from the iframe, and `postToCanvas` for refresh.
4
4
  // Silence is survivable — GEOMETRY_TIMEOUT_MS after the frame's load
5
5
  // with no bridge message, `geometry` turns "missing" and the page editor
6
6
  // diagnoses why (spec 2026-09-27 shadowed-page detector §2); the iframe
7
7
  // and the outline keep working.
8
8
  import { useCallback, useEffect, useRef, useState } from "react";
9
+ import { fromBridgeMessage } from "./protocol.js";
9
10
  const GEOMETRY_TIMEOUT_MS = 4000;
10
11
  export function useCanvasBridge({ iframeRef, iframeSrc, onSelect, onHover, onRendered }) {
11
12
  const [rects, setRects] = useState([]);
@@ -40,14 +41,12 @@ export function useCanvasBridge({ iframeRef, iframeSrc, onSelect, onHover, onRen
40
41
  const postToCanvas = useCallback((m) => {
41
42
  if (m.kind === "refresh" && !heard.current)
42
43
  missedRefresh.current = true;
43
- iframeRef.current?.contentWindow?.postMessage(m, "*");
44
+ iframeRef.current?.contentWindow?.postMessage(m, window.location.origin);
44
45
  }, [iframeRef]);
45
46
  useEffect(() => {
46
47
  const onMessage = (e) => {
47
- const m = e.data;
48
- if (m?.source !== "smoodly")
49
- return;
50
- if (e.source !== iframeRef.current?.contentWindow)
48
+ const m = fromBridgeMessage(e, iframeRef.current?.contentWindow, window.location.origin);
49
+ if (!m)
51
50
  return;
52
51
  setGeometry("ok"); // any bridge message: Smoodly rendered the canvas with its bridge
53
52
  if (!heard.current) {
@@ -216,13 +216,15 @@ export function useInlineSession({ iframeRef, postToCanvas, patch }) {
216
216
  clearTimeout(closeTimer.current); }, []);
217
217
  return { session: state.session, richtext, start, end, writeText, markChanged, onSaved, onRendered, onSelection, onCanvasChange };
218
218
  }
219
- /** The admin stylesheet inside the iframe, once: the cards inside the canvas editor are admin UI. */
219
+ /** The admin stylesheet inside the iframe, once: the cards inside the canvas editor are admin UI. The text is
220
+ * the admin document's own, fonts included (the entry writes it with the files' absolute addresses,
221
+ * browser/main.tsx); the bare rules when no page mounted one (a test). */
220
222
  function ensureStyles(d) {
221
223
  if (d.querySelector("style[data-smoodly-admin-styles]"))
222
224
  return;
223
225
  const style = d.createElement("style");
224
226
  style.setAttribute("data-smoodly-admin-styles", "");
225
- style.textContent = ADMIN_CSS;
227
+ style.textContent = document.querySelector("style[data-smoodly-admin-styles]")?.textContent ?? ADMIN_CSS;
226
228
  d.head.appendChild(style);
227
229
  }
228
230
  /** The caret at a point, for the text session (spec §4). */
@@ -0,0 +1,11 @@
1
+ export type AdminFile = {
2
+ type: string;
3
+ body: string;
4
+ encoding?: "base64";
5
+ };
6
+ /** `hash` names the build: the files are served under it and cached for good. */
7
+ export type AdminFiles = {
8
+ hash: string;
9
+ files: Record<string, AdminFile>;
10
+ };
11
+ export declare function loadAdminFiles(): Promise<AdminFiles>;