@glw907/cairn-cms 0.5.1 → 0.6.0-rc.1

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 (234) hide show
  1. package/dist/auth/crypto.d.ts +13 -0
  2. package/dist/auth/crypto.d.ts.map +1 -0
  3. package/dist/auth/crypto.js +31 -0
  4. package/dist/auth/store.d.ts +41 -0
  5. package/dist/auth/store.d.ts.map +1 -0
  6. package/dist/auth/store.js +115 -0
  7. package/dist/auth/types.d.ts +25 -0
  8. package/dist/auth/types.d.ts.map +1 -0
  9. package/dist/auth/types.js +1 -0
  10. package/dist/components/AdminLayout.svelte +58 -164
  11. package/dist/components/AdminLayout.svelte.d.ts +14 -18
  12. package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
  13. package/dist/components/ComponentPalette.svelte +36 -20
  14. package/dist/components/ComponentPalette.svelte.d.ts +11 -4
  15. package/dist/components/ComponentPalette.svelte.d.ts.map +1 -1
  16. package/dist/components/ConceptList.svelte +81 -0
  17. package/dist/components/ConceptList.svelte.d.ts +13 -0
  18. package/dist/components/ConceptList.svelte.d.ts.map +1 -0
  19. package/dist/components/ConfirmPage.svelte +23 -20
  20. package/dist/components/ConfirmPage.svelte.d.ts +6 -0
  21. package/dist/components/ConfirmPage.svelte.d.ts.map +1 -1
  22. package/dist/components/EditPage.svelte +155 -136
  23. package/dist/components/EditPage.svelte.d.ts +16 -8
  24. package/dist/components/EditPage.svelte.d.ts.map +1 -1
  25. package/dist/components/LoginPage.svelte +42 -52
  26. package/dist/components/LoginPage.svelte.d.ts +12 -0
  27. package/dist/components/LoginPage.svelte.d.ts.map +1 -1
  28. package/dist/components/ManageEditors.svelte +81 -0
  29. package/dist/components/ManageEditors.svelte.d.ts +23 -0
  30. package/dist/components/ManageEditors.svelte.d.ts.map +1 -0
  31. package/dist/components/MarkdownEditor.svelte +81 -0
  32. package/dist/components/MarkdownEditor.svelte.d.ts +20 -0
  33. package/dist/components/MarkdownEditor.svelte.d.ts.map +1 -0
  34. package/dist/components/NavTree.svelte +73 -63
  35. package/dist/components/NavTree.svelte.d.ts +13 -4
  36. package/dist/components/NavTree.svelte.d.ts.map +1 -1
  37. package/dist/components/cairn-admin.css +42 -0
  38. package/dist/components/index.d.ts +3 -2
  39. package/dist/components/index.d.ts.map +1 -1
  40. package/dist/components/index.js +5 -4
  41. package/dist/content/compose.d.ts +7 -0
  42. package/dist/content/compose.d.ts.map +1 -0
  43. package/dist/content/compose.js +32 -0
  44. package/dist/content/concepts.d.ts +17 -0
  45. package/dist/content/concepts.d.ts.map +1 -0
  46. package/dist/content/concepts.js +41 -0
  47. package/dist/content/frontmatter.d.ts +18 -0
  48. package/dist/content/frontmatter.d.ts.map +1 -0
  49. package/dist/content/frontmatter.js +58 -0
  50. package/dist/content/ids.d.ts +17 -0
  51. package/dist/content/ids.d.ts.map +1 -0
  52. package/dist/content/ids.js +33 -0
  53. package/dist/content/types.d.ts +210 -0
  54. package/dist/content/types.d.ts.map +1 -0
  55. package/dist/content/types.js +1 -0
  56. package/dist/content/validate.d.ts +13 -0
  57. package/dist/content/validate.d.ts.map +1 -0
  58. package/dist/content/validate.js +45 -0
  59. package/dist/email.d.ts +25 -12
  60. package/dist/email.d.ts.map +1 -1
  61. package/dist/email.js +24 -24
  62. package/dist/env.d.ts +24 -0
  63. package/dist/env.d.ts.map +1 -0
  64. package/dist/env.js +29 -0
  65. package/dist/github/credentials.d.ts +12 -0
  66. package/dist/github/credentials.d.ts.map +1 -0
  67. package/dist/github/credentials.js +11 -0
  68. package/dist/github/repo.d.ts +49 -0
  69. package/dist/github/repo.d.ts.map +1 -0
  70. package/dist/github/repo.js +123 -0
  71. package/dist/github/signing.d.ts +17 -0
  72. package/dist/github/signing.d.ts.map +1 -0
  73. package/dist/github/signing.js +79 -0
  74. package/dist/github/types.d.ts +35 -0
  75. package/dist/github/types.d.ts.map +1 -0
  76. package/dist/github/types.js +19 -0
  77. package/dist/index.d.ts +27 -8
  78. package/dist/index.d.ts.map +1 -1
  79. package/dist/index.js +21 -10
  80. package/dist/{nav.d.ts → nav/site-config.d.ts} +16 -24
  81. package/dist/nav/site-config.d.ts.map +1 -0
  82. package/dist/{nav.js → nav/site-config.js} +27 -13
  83. package/dist/render/glyph.d.ts +1 -1
  84. package/dist/render/glyph.d.ts.map +1 -1
  85. package/dist/render/index.d.ts +5 -5
  86. package/dist/render/index.d.ts.map +1 -1
  87. package/dist/render/index.js +6 -6
  88. package/dist/render/pipeline.d.ts +7 -6
  89. package/dist/render/pipeline.d.ts.map +1 -1
  90. package/dist/render/pipeline.js +5 -5
  91. package/dist/render/registry.d.ts +10 -6
  92. package/dist/render/registry.d.ts.map +1 -1
  93. package/dist/render/registry.js +8 -6
  94. package/dist/render/rehype-dispatch.d.ts +8 -7
  95. package/dist/render/rehype-dispatch.d.ts.map +1 -1
  96. package/dist/render/rehype-dispatch.js +16 -14
  97. package/dist/render/remark-directives.d.ts +1 -1
  98. package/dist/render/remark-directives.d.ts.map +1 -1
  99. package/dist/render/sanitize.d.ts +8 -0
  100. package/dist/render/sanitize.d.ts.map +1 -0
  101. package/dist/render/sanitize.js +26 -0
  102. package/dist/sveltekit/auth-routes.d.ts +23 -0
  103. package/dist/sveltekit/auth-routes.d.ts.map +1 -0
  104. package/dist/sveltekit/auth-routes.js +85 -0
  105. package/dist/sveltekit/content-routes.d.ts +80 -0
  106. package/dist/sveltekit/content-routes.d.ts.map +1 -0
  107. package/dist/sveltekit/content-routes.js +183 -0
  108. package/dist/sveltekit/editors-routes.d.ts +24 -0
  109. package/dist/sveltekit/editors-routes.d.ts.map +1 -0
  110. package/dist/sveltekit/editors-routes.js +73 -0
  111. package/dist/sveltekit/guard.d.ts +9 -0
  112. package/dist/sveltekit/guard.d.ts.map +1 -0
  113. package/dist/sveltekit/guard.js +43 -0
  114. package/dist/sveltekit/health.d.ts +19 -0
  115. package/dist/sveltekit/health.d.ts.map +1 -0
  116. package/dist/sveltekit/health.js +12 -0
  117. package/dist/sveltekit/index.d.ts +9 -173
  118. package/dist/sveltekit/index.d.ts.map +1 -1
  119. package/dist/sveltekit/index.js +8 -348
  120. package/dist/sveltekit/nav-routes.d.ts +30 -0
  121. package/dist/sveltekit/nav-routes.d.ts.map +1 -0
  122. package/dist/sveltekit/nav-routes.js +103 -0
  123. package/dist/sveltekit/types.d.ts +32 -0
  124. package/dist/sveltekit/types.d.ts.map +1 -0
  125. package/dist/sveltekit/types.js +1 -0
  126. package/package.json +33 -57
  127. package/src/lib/auth/crypto.ts +37 -0
  128. package/src/lib/auth/store.ts +158 -0
  129. package/src/lib/auth/types.ts +27 -0
  130. package/src/lib/components/AdminLayout.svelte +58 -164
  131. package/src/lib/components/ComponentPalette.svelte +36 -20
  132. package/src/lib/components/ConceptList.svelte +81 -0
  133. package/src/lib/components/ConfirmPage.svelte +23 -20
  134. package/src/lib/components/EditPage.svelte +155 -136
  135. package/src/lib/components/LoginPage.svelte +42 -52
  136. package/src/lib/components/ManageEditors.svelte +81 -0
  137. package/src/lib/components/MarkdownEditor.svelte +81 -0
  138. package/src/lib/components/NavTree.svelte +73 -63
  139. package/src/lib/components/cairn-admin.css +42 -0
  140. package/src/lib/components/index.ts +5 -4
  141. package/src/lib/content/compose.ts +39 -0
  142. package/src/lib/content/concepts.ts +57 -0
  143. package/src/lib/content/frontmatter.ts +71 -0
  144. package/src/lib/content/ids.ts +38 -0
  145. package/src/lib/content/types.ts +235 -0
  146. package/src/lib/content/validate.ts +51 -0
  147. package/src/lib/email.ts +52 -38
  148. package/src/lib/env.ts +32 -0
  149. package/src/lib/github/credentials.ts +27 -0
  150. package/src/lib/github/repo.ts +138 -0
  151. package/src/lib/github/signing.ts +97 -0
  152. package/src/lib/github/types.ts +46 -0
  153. package/src/lib/index.ts +86 -10
  154. package/src/lib/{nav.ts → nav/site-config.ts} +31 -24
  155. package/src/lib/render/glyph.ts +6 -6
  156. package/src/lib/render/index.ts +6 -6
  157. package/src/lib/render/pipeline.ts +23 -22
  158. package/src/lib/render/registry.ts +35 -26
  159. package/src/lib/render/rehype-dispatch.ts +58 -56
  160. package/src/lib/render/remark-directives.ts +46 -46
  161. package/src/lib/render/sanitize.ts +27 -0
  162. package/src/lib/sveltekit/auth-routes.ts +107 -0
  163. package/src/lib/sveltekit/content-routes.ts +261 -0
  164. package/src/lib/sveltekit/editors-routes.ts +82 -0
  165. package/src/lib/sveltekit/guard.ts +47 -0
  166. package/src/lib/sveltekit/health.ts +24 -0
  167. package/src/lib/sveltekit/index.ts +19 -512
  168. package/src/lib/sveltekit/nav-routes.ts +139 -0
  169. package/src/lib/sveltekit/types.ts +33 -0
  170. package/dist/adapter.d.ts +0 -93
  171. package/dist/adapter.d.ts.map +0 -1
  172. package/dist/adapter.js +0 -30
  173. package/dist/auth/admins.d.ts +0 -33
  174. package/dist/auth/admins.d.ts.map +0 -1
  175. package/dist/auth/admins.js +0 -90
  176. package/dist/auth/capabilities.d.ts +0 -7
  177. package/dist/auth/capabilities.d.ts.map +0 -1
  178. package/dist/auth/capabilities.js +0 -26
  179. package/dist/auth/config.d.ts +0 -2097
  180. package/dist/auth/config.d.ts.map +0 -1
  181. package/dist/auth/config.js +0 -78
  182. package/dist/auth/guard.d.ts +0 -34
  183. package/dist/auth/guard.d.ts.map +0 -1
  184. package/dist/auth/guard.js +0 -47
  185. package/dist/auth/index.d.ts +0 -5
  186. package/dist/auth/index.d.ts.map +0 -1
  187. package/dist/auth/index.js +0 -7
  188. package/dist/auth/schema.d.ts +0 -750
  189. package/dist/auth/schema.d.ts.map +0 -1
  190. package/dist/auth/schema.js +0 -93
  191. package/dist/carta.d.ts +0 -39
  192. package/dist/carta.d.ts.map +0 -1
  193. package/dist/carta.js +0 -30
  194. package/dist/components/CollectionList.svelte +0 -96
  195. package/dist/components/CollectionList.svelte.d.ts +0 -8
  196. package/dist/components/CollectionList.svelte.d.ts.map +0 -1
  197. package/dist/components/ManageAdmins.svelte +0 -84
  198. package/dist/components/ManageAdmins.svelte.d.ts +0 -10
  199. package/dist/components/ManageAdmins.svelte.d.ts.map +0 -1
  200. package/dist/content.d.ts +0 -3
  201. package/dist/content.d.ts.map +0 -1
  202. package/dist/content.js +0 -10
  203. package/dist/editor.d.ts +0 -25
  204. package/dist/editor.d.ts.map +0 -1
  205. package/dist/editor.js +0 -20
  206. package/dist/frontmatter.d.ts +0 -3
  207. package/dist/frontmatter.d.ts.map +0 -1
  208. package/dist/frontmatter.js +0 -16
  209. package/dist/github.d.ts +0 -72
  210. package/dist/github.d.ts.map +0 -1
  211. package/dist/github.js +0 -171
  212. package/dist/nav.d.ts.map +0 -1
  213. package/dist/slug.d.ts +0 -7
  214. package/dist/slug.d.ts.map +0 -1
  215. package/dist/slug.js +0 -15
  216. package/dist/utils.d.ts +0 -3
  217. package/dist/utils.d.ts.map +0 -1
  218. package/dist/utils.js +0 -11
  219. package/src/lib/adapter.ts +0 -144
  220. package/src/lib/auth/admins.ts +0 -106
  221. package/src/lib/auth/capabilities.ts +0 -35
  222. package/src/lib/auth/config.ts +0 -108
  223. package/src/lib/auth/guard.ts +0 -60
  224. package/src/lib/auth/index.ts +0 -7
  225. package/src/lib/auth/schema.ts +0 -112
  226. package/src/lib/carta.ts +0 -59
  227. package/src/lib/components/CollectionList.svelte +0 -96
  228. package/src/lib/components/ManageAdmins.svelte +0 -84
  229. package/src/lib/content.ts +0 -11
  230. package/src/lib/editor.ts +0 -38
  231. package/src/lib/frontmatter.ts +0 -17
  232. package/src/lib/github.ts +0 -220
  233. package/src/lib/slug.ts +0 -16
  234. package/src/lib/utils.ts +0 -12
@@ -0,0 +1,13 @@
1
+ /** The session cookie name. */
2
+ export declare const COOKIE_NAME = "cairn_session";
3
+ /** Magic-link tokens live 10 minutes. */
4
+ export declare const TOKEN_TTL_MS: number;
5
+ /** Sessions live 30 days. */
6
+ export declare const SESSION_TTL_MS: number;
7
+ /** A fresh 256-bit magic-link token, url-safe. */
8
+ export declare function generateToken(): string;
9
+ /** A fresh 256-bit session id, url-safe. */
10
+ export declare function generateSessionId(): string;
11
+ /** The lowercase hex SHA-256 of a token, for storage and lookup. */
12
+ export declare function hashToken(token: string): Promise<string>;
13
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../../src/lib/auth/crypto.ts"],"names":[],"mappings":"AAIA,+BAA+B;AAC/B,eAAO,MAAM,WAAW,kBAAkB,CAAC;AAE3C,yCAAyC;AACzC,eAAO,MAAM,YAAY,QAAiB,CAAC;AAE3C,6BAA6B;AAC7B,eAAO,MAAM,cAAc,QAA2B,CAAC;AAUvD,kDAAkD;AAClD,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED,4CAA4C;AAC5C,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,oEAAoE;AACpE,wBAAsB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9D"}
@@ -0,0 +1,31 @@
1
+ // Token and session-id generation plus SHA-256 token hashing, on Web Crypto so the
2
+ // code runs unchanged in workerd. The store keeps only the hash of a token, never the
3
+ // token itself (spec 7.1).
4
+ /** The session cookie name. */
5
+ export const COOKIE_NAME = 'cairn_session';
6
+ /** Magic-link tokens live 10 minutes. */
7
+ export const TOKEN_TTL_MS = 10 * 60 * 1000;
8
+ /** Sessions live 30 days. */
9
+ export const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
10
+ function randomBase64Url(byteLength = 32) {
11
+ const bytes = new Uint8Array(byteLength);
12
+ crypto.getRandomValues(bytes);
13
+ let binary = '';
14
+ for (const b of bytes)
15
+ binary += String.fromCharCode(b);
16
+ return btoa(binary).replaceAll('+', '-').replaceAll('/', '_').replaceAll('=', '');
17
+ }
18
+ /** A fresh 256-bit magic-link token, url-safe. */
19
+ export function generateToken() {
20
+ return randomBase64Url(32);
21
+ }
22
+ /** A fresh 256-bit session id, url-safe. */
23
+ export function generateSessionId() {
24
+ return randomBase64Url(32);
25
+ }
26
+ /** The lowercase hex SHA-256 of a token, for storage and lookup. */
27
+ export async function hashToken(token) {
28
+ const data = new TextEncoder().encode(token);
29
+ const digest = await crypto.subtle.digest('SHA-256', data);
30
+ return [...new Uint8Array(digest)].map((b) => b.toString(16).padStart(2, '0')).join('');
31
+ }
@@ -0,0 +1,41 @@
1
+ import type { D1Database } from '@cloudflare/workers-types';
2
+ import type { Editor, Role } from './types.js';
3
+ /** Look an email up in the allowlist. */
4
+ export declare function findEditor(db: D1Database, email: string): Promise<Editor | null>;
5
+ /** Replace any prior token for this email with a fresh one, atomically. */
6
+ export declare function issueToken(db: D1Database, email: string, tokenHash: string, expiresAt: number, now: number): Promise<void>;
7
+ /**
8
+ * Consume a token in one atomic statement. A returned email means the token was present and
9
+ * unexpired and is now gone, so the link is single-use by construction on strongly-consistent D1.
10
+ */
11
+ export declare function consumeToken(db: D1Database, tokenHash: string, now: number): Promise<string | null>;
12
+ /** Create a session row. */
13
+ export declare function createSession(db: D1Database, id: string, email: string, expiresAt: number, now: number): Promise<void>;
14
+ /**
15
+ * Resolve a session to its editor, joining `editor` so the role is read live. An expired
16
+ * session or a removed editor resolves to null, which revokes access on the next request.
17
+ */
18
+ export declare function resolveSession(db: D1Database, id: string, now: number): Promise<Editor | null>;
19
+ /** Delete a session (logout). */
20
+ export declare function deleteSession(db: D1Database, id: string): Promise<void>;
21
+ /** The full allowlist, sorted by email. */
22
+ export declare function listEditors(db: D1Database): Promise<Editor[]>;
23
+ /** Add an editor to the allowlist. */
24
+ export declare function insertEditor(db: D1Database, email: string, displayName: string, role: Role, now: number): Promise<void>;
25
+ /** Remove an editor and cut their live access (sessions and any pending token go too). */
26
+ export declare function deleteEditor(db: D1Database, email: string): Promise<void>;
27
+ /**
28
+ * Remove an owner only if another owner remains. The count is part of the DELETE, so two
29
+ * concurrent removals cannot both pass a separate check and strand the allowlist at zero
30
+ * owners. Returns false (and writes nothing) when this is the last owner. On success the
31
+ * editor's sessions and pending token go too.
32
+ */
33
+ export declare function removeOwnerIfNotLast(db: D1Database, email: string): Promise<boolean>;
34
+ /** Change an editor's role. The guard reads the new role on the next request. */
35
+ export declare function setEditorRole(db: D1Database, email: string, role: Role): Promise<void>;
36
+ /**
37
+ * Demote an owner to editor only if another owner remains, in one atomic statement (see
38
+ * `removeOwnerIfNotLast`). Returns false (and writes nothing) when this is the last owner.
39
+ */
40
+ export declare function demoteOwnerIfNotLast(db: D1Database, email: string): Promise<boolean>;
41
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/lib/auth/store.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAC5D,OAAO,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAQ/C,yCAAyC;AACzC,wBAAsB,UAAU,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMtF;AAED,2EAA2E;AAC3E,wBAAsB,UAAU,CAC9B,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAOf;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAMzG;AAED,4BAA4B;AAC5B,wBAAsB,aAAa,CACjC,EAAE,EAAE,UAAU,EACd,EAAE,EAAE,MAAM,EACV,KAAK,EAAE,MAAM,EACb,SAAS,EAAE,MAAM,EACjB,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAKf;AAED;;;GAGG;AACH,wBAAsB,cAAc,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAUpG;AAED,iCAAiC;AACjC,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAE7E;AAED,2CAA2C;AAC3C,wBAAsB,WAAW,CAAC,EAAE,EAAE,UAAU,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAKnE;AAED,sCAAsC;AACtC,wBAAsB,YAAY,CAChC,EAAE,EAAE,UAAU,EACd,KAAK,EAAE,MAAM,EACb,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,IAAI,EACV,GAAG,EAAE,MAAM,GACV,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,0FAA0F;AAC1F,wBAAsB,YAAY,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAM/E;AAED;;;;;GAKG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAe1F;AAED,iFAAiF;AACjF,wBAAsB,aAAa,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAE5F;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAU1F"}
@@ -0,0 +1,115 @@
1
+ function toEditor(row) {
2
+ return { email: row.email, displayName: row.display_name, role: row.role };
3
+ }
4
+ /** Look an email up in the allowlist. */
5
+ export async function findEditor(db, email) {
6
+ const row = await db
7
+ .prepare('SELECT email, display_name, role FROM editor WHERE email = ?')
8
+ .bind(email)
9
+ .first();
10
+ return row ? toEditor(row) : null;
11
+ }
12
+ /** Replace any prior token for this email with a fresh one, atomically. */
13
+ export async function issueToken(db, email, tokenHash, expiresAt, now) {
14
+ await db.batch([
15
+ db.prepare('DELETE FROM magic_token WHERE email = ?').bind(email),
16
+ db
17
+ .prepare('INSERT INTO magic_token (token_hash, email, expires_at, created_at) VALUES (?, ?, ?, ?)')
18
+ .bind(tokenHash, email, expiresAt, now),
19
+ ]);
20
+ }
21
+ /**
22
+ * Consume a token in one atomic statement. A returned email means the token was present and
23
+ * unexpired and is now gone, so the link is single-use by construction on strongly-consistent D1.
24
+ */
25
+ export async function consumeToken(db, tokenHash, now) {
26
+ const row = await db
27
+ .prepare('DELETE FROM magic_token WHERE token_hash = ? AND expires_at > ? RETURNING email')
28
+ .bind(tokenHash, now)
29
+ .first();
30
+ return row?.email ?? null;
31
+ }
32
+ /** Create a session row. */
33
+ export async function createSession(db, id, email, expiresAt, now) {
34
+ await db
35
+ .prepare('INSERT INTO session (id, email, expires_at, created_at) VALUES (?, ?, ?, ?)')
36
+ .bind(id, email, expiresAt, now)
37
+ .run();
38
+ }
39
+ /**
40
+ * Resolve a session to its editor, joining `editor` so the role is read live. An expired
41
+ * session or a removed editor resolves to null, which revokes access on the next request.
42
+ */
43
+ export async function resolveSession(db, id, now) {
44
+ const row = await db
45
+ .prepare(`SELECT e.email AS email, e.display_name AS display_name, e.role AS role
46
+ FROM session s JOIN editor e ON e.email = s.email
47
+ WHERE s.id = ? AND s.expires_at > ?`)
48
+ .bind(id, now)
49
+ .first();
50
+ return row ? toEditor(row) : null;
51
+ }
52
+ /** Delete a session (logout). */
53
+ export async function deleteSession(db, id) {
54
+ await db.prepare('DELETE FROM session WHERE id = ?').bind(id).run();
55
+ }
56
+ /** The full allowlist, sorted by email. */
57
+ export async function listEditors(db) {
58
+ const { results } = await db
59
+ .prepare('SELECT email, display_name, role FROM editor ORDER BY email')
60
+ .all();
61
+ return results.map(toEditor);
62
+ }
63
+ /** Add an editor to the allowlist. */
64
+ export async function insertEditor(db, email, displayName, role, now) {
65
+ await db
66
+ .prepare('INSERT INTO editor (email, display_name, role, created_at) VALUES (?, ?, ?, ?)')
67
+ .bind(email, displayName, role, now)
68
+ .run();
69
+ }
70
+ /** Remove an editor and cut their live access (sessions and any pending token go too). */
71
+ export async function deleteEditor(db, email) {
72
+ await db.batch([
73
+ db.prepare('DELETE FROM session WHERE email = ?').bind(email),
74
+ db.prepare('DELETE FROM magic_token WHERE email = ?').bind(email),
75
+ db.prepare('DELETE FROM editor WHERE email = ?').bind(email),
76
+ ]);
77
+ }
78
+ /**
79
+ * Remove an owner only if another owner remains. The count is part of the DELETE, so two
80
+ * concurrent removals cannot both pass a separate check and strand the allowlist at zero
81
+ * owners. Returns false (and writes nothing) when this is the last owner. On success the
82
+ * editor's sessions and pending token go too.
83
+ */
84
+ export async function removeOwnerIfNotLast(db, email) {
85
+ const res = await db
86
+ .prepare(`DELETE FROM editor
87
+ WHERE email = ? AND role = 'owner'
88
+ AND (SELECT COUNT(*) FROM editor WHERE role = 'owner') > 1`)
89
+ .bind(email)
90
+ .run();
91
+ if (res.meta.changes !== 1)
92
+ return false;
93
+ await db.batch([
94
+ db.prepare('DELETE FROM session WHERE email = ?').bind(email),
95
+ db.prepare('DELETE FROM magic_token WHERE email = ?').bind(email),
96
+ ]);
97
+ return true;
98
+ }
99
+ /** Change an editor's role. The guard reads the new role on the next request. */
100
+ export async function setEditorRole(db, email, role) {
101
+ await db.prepare('UPDATE editor SET role = ? WHERE email = ?').bind(role, email).run();
102
+ }
103
+ /**
104
+ * Demote an owner to editor only if another owner remains, in one atomic statement (see
105
+ * `removeOwnerIfNotLast`). Returns false (and writes nothing) when this is the last owner.
106
+ */
107
+ export async function demoteOwnerIfNotLast(db, email) {
108
+ const res = await db
109
+ .prepare(`UPDATE editor SET role = 'editor'
110
+ WHERE email = ? AND role = 'owner'
111
+ AND (SELECT COUNT(*) FROM editor WHERE role = 'owner') > 1`)
112
+ .bind(email)
113
+ .run();
114
+ return res.meta.changes === 1;
115
+ }
@@ -0,0 +1,25 @@
1
+ import type { D1Database } from '@cloudflare/workers-types';
2
+ export type Role = 'owner' | 'editor';
3
+ /** The session shape the whole admin reads: guard, loads, content fns, manage-editors. */
4
+ export interface Editor {
5
+ email: string;
6
+ displayName: string;
7
+ role: Role;
8
+ }
9
+ /** Worker bindings and vars the auth layer reads; a structural subset of `Platform.env`. */
10
+ export interface AuthEnv {
11
+ AUTH_DB?: D1Database;
12
+ /** Canonical origin for confirmation links, never read from a request header (spec 7.1, risk H3). */
13
+ PUBLIC_ORIGIN?: string;
14
+ /** Cloudflare Email Sending binding. */
15
+ EMAIL?: {
16
+ send(message: {
17
+ to: string;
18
+ from: string;
19
+ subject: string;
20
+ html: string;
21
+ text: string;
22
+ }): Promise<void>;
23
+ };
24
+ }
25
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/lib/auth/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,2BAA2B,CAAC;AAE5D,MAAM,MAAM,IAAI,GAAG,OAAO,GAAG,QAAQ,CAAC;AAEtC,0FAA0F;AAC1F,MAAM,WAAW,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,4FAA4F;AAC5F,MAAM,WAAW,OAAO;IACtB,OAAO,CAAC,EAAE,UAAU,CAAC;IACrB,qGAAqG;IACrG,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wCAAwC;IACxC,KAAK,CAAC,EAAE;QACN,IAAI,CAAC,OAAO,EAAE;YACZ,EAAE,EAAE,MAAM,CAAC;YACX,IAAI,EAAE,MAAM,CAAC;YACb,OAAO,EAAE,MAAM,CAAC;YAChB,IAAI,EAAE,MAAM,CAAC;YACb,IAAI,EAAE,MAAM,CAAC;SACd,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;KACnB,CAAC;CACH"}
@@ -0,0 +1 @@
1
+ export {};
@@ -1,186 +1,80 @@
1
+ <!--
2
+ @component
3
+ The admin shell: a DaisyUI drawer-and-navbar that wraps every authed admin page. The nav is
4
+ data-driven from the enabled concepts and role-gated (owners see the manage-editors entry). The
5
+ root sets `data-theme="cairn-admin"` and imports the self-contained Warm Stone theme, so the
6
+ admin looks identical on every host regardless of the site's own theme.
7
+ -->
1
8
  <script lang="ts">
2
- // Neutral admin chrome shared across sites. Signed in: DaisyUI drawer+navbar shell (sidebar
3
- // pinned on desktop, slide-over on mobile). Signed out: minimal centered shell. The
4
- // `cairn-admin` class on both roots scopes the "Warm Stone" theme; see the style block.
5
9
  import type { Snippet } from 'svelte';
6
- import type { CairnUser } from '../auth';
10
+ import type { LayoutData } from '../sveltekit/content-routes.js';
11
+ import './cairn-admin.css';
7
12
 
8
- let {
9
- data,
10
- children,
11
- }: {
12
- data: {
13
- siteName: string;
14
- user: CairnUser | null;
15
- pathname: string;
16
- collections: { type: string; label: string }[];
17
- navMenus: { name: string; label: string }[];
18
- canManageNav: boolean;
19
- };
13
+ interface Props {
14
+ /** The layout load's data: site name, user, nav concepts, active path, owner capability. */
15
+ data: LayoutData;
16
+ /** The page body. */
20
17
  children: Snippet;
21
- } = $props();
18
+ }
19
+
20
+ let { data, children }: Props = $props();
22
21
 
23
22
  interface NavItem {
24
23
  href: string;
25
24
  label: string;
26
- icon: Snippet;
27
- active: boolean;
28
- /** Owner-only surface; hidden from regular editors. */
29
25
  owner?: boolean;
30
26
  }
31
27
 
32
- const nav = $derived<NavItem[]>([
33
- ...data.collections.map((collection) => ({
34
- href: `/admin/${collection.type}`,
35
- label: collection.label,
36
- icon: contentIcon,
37
- active:
38
- data.pathname === `/admin/${collection.type}` ||
39
- data.pathname.startsWith(`/admin/edit/${collection.type}/`),
40
- })),
41
- ...(data.canManageNav && data.navMenus.length
42
- ? [{ href: '/admin/nav', label: 'Navigation', icon: navIcon, active: data.pathname.startsWith('/admin/nav') }]
43
- : []),
44
- {
45
- href: '/admin/admins',
46
- label: 'Editors',
47
- icon: editorsIcon,
48
- owner: true,
49
- active: data.pathname.startsWith('/admin/admins'),
50
- },
28
+ const navItems: NavItem[] = $derived([
29
+ ...data.concepts.map((c) => ({ href: `/admin/${c.id}`, label: c.label })),
30
+ ...(data.navLabel ? [{ href: '/admin/nav', label: data.navLabel }] : []),
31
+ { href: '/admin/editors', label: 'Editors', owner: true },
51
32
  ]);
52
- const visibleNav = $derived(nav.filter((item) => !item.owner || data.user?.role === 'owner'));
53
33
 
54
- // Close the slide-over after a nav tap on mobile.
55
- function closeDrawer(): void {
56
- const toggle = document.getElementById('admin-drawer');
57
- if (toggle instanceof HTMLInputElement) toggle.checked = false;
34
+ const visibleNav = $derived(navItems.filter((item) => !item.owner || data.canManageEditors));
35
+
36
+ function isActive(href: string): boolean {
37
+ return data.pathname === href || data.pathname.startsWith(`${href}/`);
58
38
  }
59
39
  </script>
60
40
 
61
- {#snippet contentIcon()}
62
- <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
63
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
64
- d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
65
- </svg>
66
- {/snippet}
67
-
68
- {#snippet editorsIcon()}
69
- <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
70
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
71
- d="M17 20h5v-2a4 4 0 00-3-3.87M9 20H4v-2a4 4 0 013-3.87m6-1.13a4 4 0 10-4-4 4 4 0 004 4zm6 0a4 4 0 10-3.5-2.1" />
72
- </svg>
73
- {/snippet}
74
-
75
- {#snippet navIcon()}
76
- <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
77
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
78
- d="M4 6h16M4 12h16M4 18h16" />
79
- </svg>
80
- {/snippet}
81
-
82
- <svelte:head>
83
- <meta name="robots" content="noindex, nofollow" />
84
- </svelte:head>
85
-
86
- {#if data.user}
87
- <div class="cairn-admin drawer min-h-screen bg-base-200 lg:drawer-open" data-pagefind-ignore>
88
- <input id="admin-drawer" type="checkbox" class="drawer-toggle" />
89
-
90
- <div class="drawer-content">
91
- <!-- Mobile top bar; the desktop sidebar replaces this at lg. -->
92
- <div class="navbar bg-base-100 lg:hidden">
93
- <div class="flex-1">
94
- <span class="px-2 text-xl font-bold">{data.siteName} CMS</span>
95
- </div>
96
- <div class="flex-none">
97
- <label for="admin-drawer" class="btn btn-square btn-ghost" aria-label="Open menu">
98
- <svg class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
99
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h7" />
100
- </svg>
101
- </label>
102
- </div>
41
+ <div data-theme="cairn-admin" class="drawer lg:drawer-open min-h-screen bg-base-200 text-base-content">
42
+ <input id="cairn-drawer" type="checkbox" class="drawer-toggle" />
43
+
44
+ <div class="drawer-content flex flex-col">
45
+ <div class="navbar bg-base-100 border-b border-base-300">
46
+ <div class="flex-none lg:hidden">
47
+ <label for="cairn-drawer" aria-label="Open menu" class="btn btn-square btn-ghost">
48
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
49
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16" />
50
+ </svg>
51
+ </label>
103
52
  </div>
104
-
105
- <main class="container px-4 py-6 lg:px-8">
106
- {@render children()}
107
- </main>
53
+ <div class="flex-1 px-2 font-semibold">{data.siteName}</div>
54
+ <div class="flex-none px-2 text-sm text-[var(--color-muted)]">{data.user.displayName}</div>
108
55
  </div>
109
56
 
110
- <div class="drawer-side z-10">
111
- <label for="admin-drawer" class="drawer-overlay" aria-label="Close menu"></label>
112
- <div class="flex min-h-full w-80 flex-col bg-base-100 lg:border-r lg:border-base-300">
113
- <ul class="menu menu-lg grow p-4">
114
- <li class="menu-title flex flex-row items-center text-xl font-bold text-base-content">
115
- <span class="grow">{data.siteName} CMS</span>
116
- <label for="admin-drawer" class="ml-3 cursor-pointer lg:hidden" aria-label="Close menu">✕</label>
117
- </li>
118
- {#each visibleNav as item (item.href)}
119
- <li>
120
- <a href={item.href} class={item.active ? 'active' : ''} onclick={closeDrawer}>
121
- {@render item.icon()}
122
- {item.label}
123
- </a>
124
- </li>
125
- {/each}
126
- </ul>
127
-
128
- <div class="border-t border-base-300 p-4">
129
- <p class="text-sm font-medium">{data.user.name}</p>
130
- <p class="text-xs opacity-60">{data.user.email}</p>
131
- <form method="POST" action="/admin/auth/logout" class="mt-3">
132
- <button type="submit" class="btn btn-ghost btn-sm btn-block justify-start">Sign out</button>
133
- </form>
134
- </div>
135
- </div>
136
- </div>
137
- </div>
138
- {:else}
139
- <!-- Signed out (login page): no nav, just a centered surface. -->
140
- <div class="cairn-admin min-h-screen bg-base-200" data-pagefind-ignore>
141
- <div class="mx-auto max-w-3xl px-4 py-8">
57
+ <main class="flex-1 p-4 lg:p-8">
142
58
  {@render children()}
143
- </div>
59
+ </main>
144
60
  </div>
145
- {/if}
146
-
147
- <style>
148
- /* Warm Stone: a neutral, fully self-contained admin theme (R6), light-only. Overriding the
149
- DaisyUI v5 tokens + font on this root re-skins the whole admin subtree by inheritance, so
150
- the tool looks identical on every host regardless of the site's own theme. Values are OKLCH
151
- (no hex/rgb, per the design-system rule). Warm-gray neutrals (hue ~75), violet accent. */
152
- .cairn-admin {
153
- color-scheme: light;
154
- font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
155
-
156
- --color-base-100: oklch(98.5% 0.004 75);
157
- --color-base-200: oklch(96% 0.005 75);
158
- --color-base-300: oklch(92% 0.008 75);
159
- --color-base-content: oklch(28% 0.012 75);
160
61
 
161
- --color-primary: oklch(52% 0.20 293);
162
- --color-primary-content: oklch(98% 0.012 293);
163
- --color-secondary: oklch(45% 0.02 75);
164
- --color-secondary-content: oklch(98% 0.004 75);
165
- --color-accent: oklch(58% 0.16 300);
166
- --color-accent-content: oklch(98% 0.012 300);
167
- --color-neutral: oklch(32% 0.012 75);
168
- --color-neutral-content: oklch(96% 0.004 75);
169
-
170
- --color-info: oklch(60% 0.12 240);
171
- --color-info-content: oklch(98% 0.01 240);
172
- --color-success: oklch(58% 0.12 150);
173
- --color-success-content: oklch(98% 0.01 150);
174
- --color-warning: oklch(75% 0.15 70);
175
- --color-warning-content: oklch(25% 0.02 70);
176
- --color-error: oklch(58% 0.20 25);
177
- --color-error-content: oklch(98% 0.01 25);
178
-
179
- --radius-selector: 0.5rem;
180
- --radius-field: 0.5rem;
181
- --radius-box: 0.75rem;
182
- --size-selector: 0.25rem;
183
- --size-field: 0.25rem;
184
- --border: 1px;
185
- }
186
- </style>
62
+ <div class="drawer-side">
63
+ <label for="cairn-drawer" aria-label="Close menu" class="drawer-overlay"></label>
64
+ <nav class="bg-base-100 min-h-full w-64 border-r border-base-300 p-4" aria-label="Site content">
65
+ <div class="menu-title mb-2 px-2 text-xs uppercase tracking-wide text-[var(--color-muted)]">Content</div>
66
+ <ul class="menu menu-lg w-full">
67
+ {#each visibleNav as item (item.href)}
68
+ <li>
69
+ <a href={item.href} class:menu-active={isActive(item.href)} aria-current={isActive(item.href) ? 'page' : undefined}>
70
+ {item.label}
71
+ </a>
72
+ </li>
73
+ {/each}
74
+ </ul>
75
+ <form method="POST" action="/admin/auth/logout" class="mt-6 px-2">
76
+ <button type="submit" class="btn btn-ghost btn-sm btn-block">Sign out</button>
77
+ </form>
78
+ </nav>
79
+ </div>
80
+ </div>
@@ -1,23 +1,19 @@
1
1
  import type { Snippet } from 'svelte';
2
- import type { CairnUser } from '../auth';
3
- type $$ComponentProps = {
4
- data: {
5
- siteName: string;
6
- user: CairnUser | null;
7
- pathname: string;
8
- collections: {
9
- type: string;
10
- label: string;
11
- }[];
12
- navMenus: {
13
- name: string;
14
- label: string;
15
- }[];
16
- canManageNav: boolean;
17
- };
2
+ import type { LayoutData } from '../sveltekit/content-routes.js';
3
+ import './cairn-admin.css';
4
+ interface Props {
5
+ /** The layout load's data: site name, user, nav concepts, active path, owner capability. */
6
+ data: LayoutData;
7
+ /** The page body. */
18
8
  children: Snippet;
19
- };
20
- declare const AdminLayout: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ }
10
+ /**
11
+ * The admin shell: a DaisyUI drawer-and-navbar that wraps every authed admin page. The nav is
12
+ * data-driven from the enabled concepts and role-gated (owners see the manage-editors entry). The
13
+ * root sets `data-theme="cairn-admin"` and imports the self-contained Warm Stone theme, so the
14
+ * admin looks identical on every host regardless of the site's own theme.
15
+ */
16
+ declare const AdminLayout: import("svelte").Component<Props, {}, "">;
21
17
  type AdminLayout = ReturnType<typeof AdminLayout>;
22
18
  export default AdminLayout;
23
19
  //# sourceMappingURL=AdminLayout.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"AdminLayout.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AdminLayout.svelte.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAWxC,KAAK,gBAAgB,GAAI;IACtB,IAAI,EAAE;QACJ,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;QACvB,QAAQ,EAAE,MAAM,CAAC;QACjB,WAAW,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC/C,QAAQ,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAA;SAAE,EAAE,CAAC;QAC5C,YAAY,EAAE,OAAO,CAAC;KACvB,CAAC;IACF,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAkIJ,QAAA,MAAM,WAAW,sDAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
1
+ {"version":3,"file":"AdminLayout.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AdminLayout.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,gCAAgC,CAAC;AACjE,OAAO,mBAAmB,CAAC;AAGzB,UAAU,KAAK;IACb,4FAA4F;IAC5F,IAAI,EAAE,UAAU,CAAC;IACjB,qBAAqB;IACrB,QAAQ,EAAE,OAAO,CAAC;CACnB;AAyEH;;;;;GAKG;AACH,QAAA,MAAM,WAAW,2CAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -1,31 +1,47 @@
1
+ <!--
2
+ @component
3
+ The insert-component palette: a dropdown listing the site's registered directive components
4
+ (seam 3). Picking one inserts its template at the cursor through the editor's insert callback.
5
+ Renders nothing when the site configures no registry.
6
+ -->
1
7
  <script lang="ts">
2
- // The insert-component palette (R10). Reads the site's component registry (R10a) and inserts a
3
- // scaffolded directive snippet at the cursor via the `insert` callback. DaisyUI dropdown so it
4
- // matches the Warm Stone admin theme. Shown only when the site supplies a non-empty registry; a
5
- // plain-markdown site (e.g. 907.life) passes no registry and this renders nothing.
6
- import type { ComponentRegistry } from '../render';
8
+ import type { ComponentRegistry } from '../render/registry.js';
7
9
 
8
- let { registry, insert }: { registry?: ComponentRegistry; insert: (template: string) => void } =
9
- $props();
10
+ interface Props {
11
+ /** The site's component registry; the palette derives its catalog from it. */
12
+ registry?: ComponentRegistry;
13
+ /** Insert a template at the editor's cursor. */
14
+ insert: (template: string) => void;
15
+ }
16
+
17
+ let { registry, insert }: Props = $props();
10
18
 
11
19
  const defs = $derived(registry?.defs ?? []);
20
+ let open = $state(false);
12
21
  </script>
13
22
 
14
23
  {#if defs.length > 0}
15
- <div class="dropdown">
16
- <button type="button" tabindex="0" class="btn btn-sm btn-ghost">Insert ▾</button>
17
- <ul
18
- class="dropdown-content menu z-10 mt-1 w-72 rounded-box border border-base-300 bg-base-100 p-2 shadow"
19
- >
24
+ <div
25
+ class="dropdown"
26
+ class:dropdown-open={open}
27
+ role="presentation"
28
+ onkeydown={(e) => { if (e.key === 'Escape') open = false; }}
29
+ >
30
+ <button
31
+ type="button"
32
+ class="btn btn-sm btn-ghost"
33
+ aria-haspopup="listbox"
34
+ aria-expanded={open}
35
+ onclick={() => (open = !open)}
36
+ >Insert</button>
37
+ <ul class="dropdown-content menu rounded-box border border-base-300 bg-base-100 z-10 w-56 shadow" role="listbox">
20
38
  {#each defs as def (def.name)}
21
- <li>
22
- <button
23
- type="button"
24
- class="flex flex-col items-start gap-0.5"
25
- onclick={() => insert(def.insertTemplate)}
26
- >
27
- <span class="font-medium">{def.label}</span>
28
- <span class="text-xs opacity-60">{def.description}</span>
39
+ <li role="option" aria-selected={false}>
40
+ <button type="button" onclick={() => { insert(def.insertTemplate); open = false; }}>
41
+ <span class="flex flex-col items-start">
42
+ <span class="font-medium">{def.label}</span>
43
+ <span class="text-xs text-[var(--color-muted)]">{def.description}</span>
44
+ </span>
29
45
  </button>
30
46
  </li>
31
47
  {/each}
@@ -1,9 +1,16 @@
1
- import type { ComponentRegistry } from '../render';
2
- type $$ComponentProps = {
1
+ import type { ComponentRegistry } from '../render/registry.js';
2
+ interface Props {
3
+ /** The site's component registry; the palette derives its catalog from it. */
3
4
  registry?: ComponentRegistry;
5
+ /** Insert a template at the editor's cursor. */
4
6
  insert: (template: string) => void;
5
- };
6
- declare const ComponentPalette: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ }
8
+ /**
9
+ * The insert-component palette: a dropdown listing the site's registered directive components
10
+ * (seam 3). Picking one inserts its template at the cursor through the editor's insert callback.
11
+ * Renders nothing when the site configures no registry.
12
+ */
13
+ declare const ComponentPalette: import("svelte").Component<Props, {}, "">;
7
14
  type ComponentPalette = ReturnType<typeof ComponentPalette>;
8
15
  export default ComponentPalette;
9
16
  //# sourceMappingURL=ComponentPalette.svelte.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ComponentPalette.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ComponentPalette.svelte.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AAElD,KAAK,gBAAgB,GAAI;IAAE,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAAC,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAA;CAAE,CAAC;AAgC/F,QAAA,MAAM,gBAAgB,sDAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}
1
+ {"version":3,"file":"ComponentPalette.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ComponentPalette.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAG7D,UAAU,KAAK;IACb,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B,gDAAgD;IAChD,MAAM,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;CACpC;AAgCH;;;;GAIG;AACH,QAAA,MAAM,gBAAgB,2CAAwC,CAAC;AAC/D,KAAK,gBAAgB,GAAG,UAAU,CAAC,OAAO,gBAAgB,CAAC,CAAC;AAC5D,eAAe,gBAAgB,CAAC"}