@glw907/cairn-cms 0.26.0 → 0.33.0

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/CHANGELOG.md +143 -0
  2. package/dist/auth/crypto.d.ts +0 -1
  3. package/dist/auth/store.d.ts +0 -1
  4. package/dist/auth/types.d.ts +0 -1
  5. package/dist/components/AdminLayout.svelte +372 -44
  6. package/dist/components/AdminLayout.svelte.d.ts +5 -5
  7. package/dist/components/CairnLogo.svelte +28 -0
  8. package/dist/components/CairnLogo.svelte.d.ts +15 -0
  9. package/dist/components/ComponentForm.svelte +1 -1
  10. package/dist/components/ComponentForm.svelte.d.ts +0 -1
  11. package/dist/components/ComponentInsertDialog.svelte.d.ts +0 -1
  12. package/dist/components/ConceptList.svelte +240 -45
  13. package/dist/components/ConceptList.svelte.d.ts +12 -3
  14. package/dist/components/ConfirmPage.svelte +20 -3
  15. package/dist/components/ConfirmPage.svelte.d.ts +0 -1
  16. package/dist/components/DeleteDialog.svelte.d.ts +0 -1
  17. package/dist/components/EditPage.svelte +12 -7
  18. package/dist/components/EditPage.svelte.d.ts +0 -1
  19. package/dist/components/EditorToolbar.svelte.d.ts +0 -1
  20. package/dist/components/IconPicker.svelte.d.ts +0 -1
  21. package/dist/components/LinkPicker.svelte.d.ts +0 -1
  22. package/dist/components/LoginPage.svelte +27 -5
  23. package/dist/components/LoginPage.svelte.d.ts +0 -1
  24. package/dist/components/ManageEditors.svelte +8 -5
  25. package/dist/components/ManageEditors.svelte.d.ts +0 -1
  26. package/dist/components/MarkdownEditor.svelte.d.ts +0 -1
  27. package/dist/components/NavTree.svelte +2 -2
  28. package/dist/components/NavTree.svelte.d.ts +0 -1
  29. package/dist/components/RenameDialog.svelte.d.ts +0 -1
  30. package/dist/components/admin-icons.d.ts +13 -0
  31. package/dist/components/admin-icons.js +15 -0
  32. package/dist/components/cairn-admin.css +5516 -37
  33. package/dist/components/cairn-favicon.d.ts +2 -0
  34. package/dist/components/cairn-favicon.js +7 -0
  35. package/dist/components/chrome-guard.d.ts +9 -0
  36. package/dist/components/chrome-guard.js +55 -0
  37. package/dist/components/fonts/BricolageGrotesque-OFL.txt +93 -0
  38. package/dist/components/fonts/Figtree-OFL.txt +93 -0
  39. package/dist/components/fonts/bricolage-grotesque.woff2 +0 -0
  40. package/dist/components/fonts/figtree.woff2 +0 -0
  41. package/dist/components/index.d.ts +0 -1
  42. package/dist/components/link-completion.d.ts +0 -1
  43. package/dist/components/markdown-format.d.ts +0 -1
  44. package/dist/content/adapter.d.ts +0 -1
  45. package/dist/content/compose.d.ts +1 -2
  46. package/dist/content/compose.js +2 -3
  47. package/dist/content/concepts.d.ts +7 -1
  48. package/dist/content/concepts.js +49 -1
  49. package/dist/content/frontmatter.d.ts +0 -1
  50. package/dist/content/identity.d.ts +23 -0
  51. package/dist/content/identity.js +43 -0
  52. package/dist/content/ids.d.ts +0 -1
  53. package/dist/content/links.d.ts +0 -1
  54. package/dist/content/manifest.d.ts +3 -2
  55. package/dist/content/manifest.js +6 -26
  56. package/dist/content/permalink.d.ts +0 -1
  57. package/dist/content/schema.d.ts +0 -1
  58. package/dist/content/types.d.ts +0 -1
  59. package/dist/content/validate.d.ts +0 -1
  60. package/dist/delivery/CairnHead.svelte.d.ts +0 -1
  61. package/dist/delivery/content-index.d.ts +0 -1
  62. package/dist/delivery/content-index.js +8 -25
  63. package/dist/delivery/data.d.ts +0 -1
  64. package/dist/delivery/excerpt.d.ts +0 -1
  65. package/dist/delivery/feeds.d.ts +0 -1
  66. package/dist/delivery/head.d.ts +0 -1
  67. package/dist/delivery/index.d.ts +0 -1
  68. package/dist/delivery/json-ld.d.ts +0 -1
  69. package/dist/delivery/manifest.d.ts +0 -1
  70. package/dist/delivery/paginate.d.ts +0 -1
  71. package/dist/delivery/responses.d.ts +0 -1
  72. package/dist/delivery/robots.d.ts +0 -1
  73. package/dist/delivery/seo-fields.d.ts +0 -1
  74. package/dist/delivery/seo.d.ts +0 -1
  75. package/dist/delivery/site-descriptors.d.ts +0 -1
  76. package/dist/delivery/site-descriptors.js +5 -6
  77. package/dist/delivery/site-index.d.ts +0 -1
  78. package/dist/delivery/site-indexes.d.ts +0 -1
  79. package/dist/delivery/sitemap.d.ts +0 -1
  80. package/dist/email.d.ts +0 -1
  81. package/dist/env.d.ts +0 -1
  82. package/dist/github/credentials.d.ts +0 -1
  83. package/dist/github/repo.d.ts +0 -1
  84. package/dist/github/signing.d.ts +0 -1
  85. package/dist/github/types.d.ts +0 -1
  86. package/dist/index.d.ts +0 -29
  87. package/dist/index.js +4 -23
  88. package/dist/nav/site-config.d.ts +0 -1
  89. package/dist/render/authoring.d.ts +3 -0
  90. package/dist/render/authoring.js +5 -0
  91. package/dist/render/component-grammar.d.ts +0 -1
  92. package/dist/render/component-insert.d.ts +0 -1
  93. package/dist/render/component-reference.d.ts +0 -1
  94. package/dist/render/component-validate.d.ts +0 -1
  95. package/dist/render/glyph.d.ts +0 -1
  96. package/dist/render/index.d.ts +0 -1
  97. package/dist/render/pipeline.d.ts +0 -1
  98. package/dist/render/pipeline.js +5 -1
  99. package/dist/render/registry.d.ts +2 -1
  100. package/dist/render/registry.js +15 -0
  101. package/dist/render/rehype-dispatch.d.ts +9 -7
  102. package/dist/render/rehype-dispatch.js +12 -6
  103. package/dist/render/remark-directives.d.ts +0 -1
  104. package/dist/render/remark-directives.js +1 -1
  105. package/dist/render/resolve-links.d.ts +0 -1
  106. package/dist/render/sanitize-schema.d.ts +14 -1
  107. package/dist/render/sanitize-schema.js +96 -0
  108. package/dist/sveltekit/auth-routes.d.ts +0 -1
  109. package/dist/sveltekit/content-routes.d.ts +12 -2
  110. package/dist/sveltekit/content-routes.js +37 -13
  111. package/dist/sveltekit/editors-routes.d.ts +0 -1
  112. package/dist/sveltekit/guard.d.ts +0 -1
  113. package/dist/sveltekit/health.d.ts +0 -1
  114. package/dist/sveltekit/index.d.ts +1 -3
  115. package/dist/sveltekit/index.js +0 -1
  116. package/dist/sveltekit/nav-routes.d.ts +0 -1
  117. package/dist/sveltekit/public-routes.d.ts +0 -1
  118. package/dist/sveltekit/types.d.ts +0 -1
  119. package/dist/vite/bin.d.ts +0 -1
  120. package/dist/vite/index.d.ts +0 -1
  121. package/package.json +16 -2
  122. package/src/lib/components/AdminLayout.svelte +372 -44
  123. package/src/lib/components/CairnLogo.svelte +28 -0
  124. package/src/lib/components/ComponentForm.svelte +1 -1
  125. package/src/lib/components/ConceptList.svelte +240 -45
  126. package/src/lib/components/ConfirmPage.svelte +20 -3
  127. package/src/lib/components/EditPage.svelte +12 -7
  128. package/src/lib/components/LoginPage.svelte +27 -5
  129. package/src/lib/components/ManageEditors.svelte +8 -5
  130. package/src/lib/components/NavTree.svelte +2 -2
  131. package/src/lib/components/admin-icons.ts +15 -0
  132. package/src/lib/components/cairn-admin.css +162 -7
  133. package/src/lib/components/cairn-favicon.ts +9 -0
  134. package/src/lib/components/chrome-guard.ts +62 -0
  135. package/src/lib/components/fonts/BricolageGrotesque-OFL.txt +93 -0
  136. package/src/lib/components/fonts/Figtree-OFL.txt +93 -0
  137. package/src/lib/components/fonts/bricolage-grotesque.woff2 +0 -0
  138. package/src/lib/components/fonts/figtree.woff2 +0 -0
  139. package/src/lib/content/compose.ts +3 -3
  140. package/src/lib/content/concepts.ts +61 -1
  141. package/src/lib/content/identity.ts +60 -0
  142. package/src/lib/content/manifest.ts +6 -27
  143. package/src/lib/delivery/content-index.ts +8 -27
  144. package/src/lib/delivery/site-descriptors.ts +5 -6
  145. package/src/lib/index.ts +4 -57
  146. package/src/lib/render/authoring.ts +7 -0
  147. package/src/lib/render/pipeline.ts +4 -1
  148. package/src/lib/render/registry.ts +20 -0
  149. package/src/lib/render/rehype-dispatch.ts +13 -6
  150. package/src/lib/render/remark-directives.ts +1 -1
  151. package/src/lib/render/sanitize-schema.ts +97 -0
  152. package/src/lib/sveltekit/content-routes.ts +51 -14
  153. package/src/lib/sveltekit/index.ts +2 -8
  154. package/dist/auth/crypto.d.ts.map +0 -1
  155. package/dist/auth/store.d.ts.map +0 -1
  156. package/dist/auth/types.d.ts.map +0 -1
  157. package/dist/components/AdminLayout.svelte.d.ts.map +0 -1
  158. package/dist/components/ComponentForm.svelte.d.ts.map +0 -1
  159. package/dist/components/ComponentInsertDialog.svelte.d.ts.map +0 -1
  160. package/dist/components/ConceptList.svelte.d.ts.map +0 -1
  161. package/dist/components/ConfirmPage.svelte.d.ts.map +0 -1
  162. package/dist/components/DeleteDialog.svelte.d.ts.map +0 -1
  163. package/dist/components/EditPage.svelte.d.ts.map +0 -1
  164. package/dist/components/EditorToolbar.svelte.d.ts.map +0 -1
  165. package/dist/components/IconPicker.svelte.d.ts.map +0 -1
  166. package/dist/components/LinkPicker.svelte.d.ts.map +0 -1
  167. package/dist/components/LoginPage.svelte.d.ts.map +0 -1
  168. package/dist/components/ManageEditors.svelte.d.ts.map +0 -1
  169. package/dist/components/MarkdownEditor.svelte.d.ts.map +0 -1
  170. package/dist/components/NavTree.svelte.d.ts.map +0 -1
  171. package/dist/components/RenameDialog.svelte.d.ts.map +0 -1
  172. package/dist/components/index.d.ts.map +0 -1
  173. package/dist/components/link-completion.d.ts.map +0 -1
  174. package/dist/components/markdown-format.d.ts.map +0 -1
  175. package/dist/content/adapter.d.ts.map +0 -1
  176. package/dist/content/compose.d.ts.map +0 -1
  177. package/dist/content/concepts.d.ts.map +0 -1
  178. package/dist/content/frontmatter.d.ts.map +0 -1
  179. package/dist/content/ids.d.ts.map +0 -1
  180. package/dist/content/links.d.ts.map +0 -1
  181. package/dist/content/manifest.d.ts.map +0 -1
  182. package/dist/content/permalink.d.ts.map +0 -1
  183. package/dist/content/schema.d.ts.map +0 -1
  184. package/dist/content/types.d.ts.map +0 -1
  185. package/dist/content/validate.d.ts.map +0 -1
  186. package/dist/delivery/CairnHead.svelte.d.ts.map +0 -1
  187. package/dist/delivery/content-index.d.ts.map +0 -1
  188. package/dist/delivery/data.d.ts.map +0 -1
  189. package/dist/delivery/excerpt.d.ts.map +0 -1
  190. package/dist/delivery/feeds.d.ts.map +0 -1
  191. package/dist/delivery/head.d.ts.map +0 -1
  192. package/dist/delivery/index.d.ts.map +0 -1
  193. package/dist/delivery/json-ld.d.ts.map +0 -1
  194. package/dist/delivery/manifest.d.ts.map +0 -1
  195. package/dist/delivery/paginate.d.ts.map +0 -1
  196. package/dist/delivery/responses.d.ts.map +0 -1
  197. package/dist/delivery/robots.d.ts.map +0 -1
  198. package/dist/delivery/seo-fields.d.ts.map +0 -1
  199. package/dist/delivery/seo.d.ts.map +0 -1
  200. package/dist/delivery/site-descriptors.d.ts.map +0 -1
  201. package/dist/delivery/site-index.d.ts.map +0 -1
  202. package/dist/delivery/site-indexes.d.ts.map +0 -1
  203. package/dist/delivery/sitemap.d.ts.map +0 -1
  204. package/dist/email.d.ts.map +0 -1
  205. package/dist/env.d.ts.map +0 -1
  206. package/dist/github/credentials.d.ts.map +0 -1
  207. package/dist/github/repo.d.ts.map +0 -1
  208. package/dist/github/signing.d.ts.map +0 -1
  209. package/dist/github/types.d.ts.map +0 -1
  210. package/dist/index.d.ts.map +0 -1
  211. package/dist/nav/site-config.d.ts.map +0 -1
  212. package/dist/render/component-grammar.d.ts.map +0 -1
  213. package/dist/render/component-insert.d.ts.map +0 -1
  214. package/dist/render/component-reference.d.ts.map +0 -1
  215. package/dist/render/component-validate.d.ts.map +0 -1
  216. package/dist/render/glyph.d.ts.map +0 -1
  217. package/dist/render/index.d.ts.map +0 -1
  218. package/dist/render/pipeline.d.ts.map +0 -1
  219. package/dist/render/registry.d.ts.map +0 -1
  220. package/dist/render/rehype-dispatch.d.ts.map +0 -1
  221. package/dist/render/remark-directives.d.ts.map +0 -1
  222. package/dist/render/resolve-links.d.ts.map +0 -1
  223. package/dist/render/sanitize-schema.d.ts.map +0 -1
  224. package/dist/sveltekit/auth-routes.d.ts.map +0 -1
  225. package/dist/sveltekit/content-routes.d.ts.map +0 -1
  226. package/dist/sveltekit/editors-routes.d.ts.map +0 -1
  227. package/dist/sveltekit/guard.d.ts.map +0 -1
  228. package/dist/sveltekit/health.d.ts.map +0 -1
  229. package/dist/sveltekit/index.d.ts.map +0 -1
  230. package/dist/sveltekit/nav-routes.d.ts.map +0 -1
  231. package/dist/sveltekit/public-routes.d.ts.map +0 -1
  232. package/dist/sveltekit/types.d.ts.map +0 -1
  233. package/dist/vite/bin.d.ts.map +0 -1
  234. package/dist/vite/index.d.ts.map +0 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,149 @@
2
2
 
3
3
  All notable changes to this project are recorded here, most recent first.
4
4
 
5
+ ## 0.33.0
6
+
7
+ The admin isolates itself from host chrome. A dev-only guard in the admin and login roots walks the
8
+ ancestor chain on mount and logs one `console.error` when a width-constraining ancestor sits between the
9
+ admin root and `<body>`, the sign that a site's root layout is wrapping the admin in its own nav, footer,
10
+ or container. The guard compiles out of production and changes no rendering. The canonical route pattern
11
+ is documented and demonstrated: a chrome-free root layout plus a URL-transparent `(site)` group that
12
+ holds the public chrome and `app.css`, so the host chrome never wraps `/admin`. The showcase gains a
13
+ `(site)` group with plain-CSS chrome, which proves the admin renders fully styled on a site that uses
14
+ neither Tailwind nor DaisyUI.
15
+
16
+ This closes the global at-rule note carried since the self-styling foundation. The compiled admin sheet
17
+ holds DaisyUI `@keyframes` and Tailwind `@property` rules that are document-global by CSS spec, but the
18
+ sheet is code-split to the admin roots that import it, so it loads only on `/admin`, and the route pattern
19
+ keeps the host's CSS off `/admin` from the other side. A boundary test pins that the admin sheet is
20
+ imported only by the admin roots.
21
+
22
+ Consumers must: keep the host root layout chrome-free and move the public chrome plus `app.css` into a
23
+ `(site)` route group, so the host chrome never wraps `/admin`. A site already on this structure needs no
24
+ change. The dev guard names the problem in the console if a root layout still wraps the admin.
25
+
26
+ ## 0.32.0
27
+
28
+ The admin gets a real CMS UX. The concept list is now a searchable, sortable data-table with status
29
+ badges, formatted dates, per-row delete, and pagination. The sidebar carries an icon per nav item and
30
+ a user menu with sign-out. The topbar is sticky and shows breadcrumbs. The admin has a dark mode, with
31
+ a topbar toggle that persists through a cookie and follows the OS preference on a first visit. The admin
32
+ icons are Lucide, added as a runtime dependency.
33
+
34
+ This release also fixes the self-styled admin so its drawer sidebar renders: the stylesheet build now
35
+ flattens CSS nesting before scoping (so DaisyUI's `lg:drawer-open` reveal is not severed from its
36
+ parent), and the admin layout carries `data-theme` on a wrapper so the drawer's own classes are scoped
37
+ descendants. The build gained `lightningcss` as a build-only devDependency for the flatten step; this
38
+ does not affect a consumer's runtime.
39
+
40
+ A frontend-design polish pass then refined the look. The Warm Stone light and dark palettes gained
41
+ clearer surface layering and crisper borders, the sidebar an active state in a soft primary tint, and
42
+ the list table refined column labels, row hover, and cleaner entry-title links. The list now defaults
43
+ to newest-first. A reduced-motion preference is honored inside the admin. A scoped anchor reset
44
+ restores the no-underline, inherit-color default the omitted Preflight used to provide.
45
+
46
+ A design-identity pass then gave the admin its own look. Cairn has a wordmark set in Bricolage
47
+ Grotesque over a body face of Figtree, both self-hosted as variable woff2 under the SIL Open Font
48
+ License, so the admin makes no webfont network call. An app-icon brand tile sits at the top of the
49
+ sidebar with the Cairn cairn-stack mark, a CC0 public-domain glyph, beside a CMS chip. The surfaces
50
+ moved to softer radii and floating cards over a calm warm-neutral ground, with a soft violet lift on
51
+ the primary button. The sidebar and the topbar share one flat header strip, so their intersection
52
+ reads as a single plane.
53
+
54
+ The nav now groups its entries. The core Cairn functions live in one collapsible group, and a
55
+ developer's own admin extensions sit in their own custom-named groups at the same level. Each group's
56
+ open or collapsed state persists through a `cairn-admin-nav-collapsed` cookie that the layout load
57
+ reads for a no-flash first paint, the way the theme cookie already works. A command palette opens with
58
+ Cmd/Ctrl+K or the topbar search box, jumps to any admin destination, and runs a couple of actions like
59
+ the theme toggle. The login and confirm screens carry the same wordmark, voice, and favicon.
60
+
61
+ Two more rendering fixes landed in this window. The login and confirm screens centered on a wrapper
62
+ rather than the themed element, so they now fill the viewport like the rest of the admin. The command
63
+ palette closed its dialog from a result link's own click handler, and closing a native dialog mid-click
64
+ cancelled the navigation, so a destination did nothing; a destination now navigates and the palette
65
+ closes once the new route lands.
66
+
67
+ This is additive for a consumer that mounts the admin through the documented routes. The engine now
68
+ depends on `@lucide/svelte`, which installs transitively, so no consumer action is required. A new
69
+ `listDeleteAction` is available on the content routes for wiring per-row delete on the list page; the
70
+ showcase wires it as the list `?/delete` action.
71
+
72
+ ## 0.31.0
73
+
74
+ The admin now ships its own stylesheet. The engine compiles the admin's Tailwind utilities and
75
+ DaisyUI component classes, scoped under the admin `data-theme`, and the admin styles itself on any
76
+ host with no Tailwind or DaisyUI of its own. The compiled sheet leaks no global rule, so it never
77
+ touches the host's pages.
78
+
79
+ Consumers may: remove any Tailwind `@source` entry that existed only to generate the admin's classes;
80
+ the admin no longer depends on the host's Tailwind or DaisyUI build. A host that already provides
81
+ DaisyUI globally keeps working, since the engine's scoped rules are low-specificity (`:where`) and
82
+ the class names match; a later pass moves the admin out of the host's chrome entirely.
83
+
84
+ ## 0.30.0
85
+
86
+ Carved a `@glw907/cairn-cms/render` authoring subpath for the component-authoring toolkit. `iconSpan`,
87
+ `cardShell`, `headRow`, the re-homed `isElement`, and the new `strAttr` now live there, so the root barrel
88
+ stays lean and a component `build()` imports its helpers from one obvious place. Added `strAttr(ctx, key)`,
89
+ a string-attribute reader, a configurable `headRow` heading level that defaults to 2, a
90
+ `registry.iconField(name)` accessor, and a `defineRegistry` guard that fails a component declaring
91
+ `defaultIconByRole` with no `type:'icon'` attribute. Dropped `rehypeDispatch` from the public surface, so
92
+ `createRenderer` is the one public render pipeline.
93
+
94
+ Consumers must: import `iconSpan`, `cardShell`, `headRow`, `isElement`, and `strAttr` from
95
+ `@glw907/cairn-cms/render` instead of the package root, and replace any direct `rehypeDispatch` use with
96
+ `createRenderer`. A component that sets `defaultIconByRole` with no `type:'icon'` attribute now fails
97
+ `defineRegistry`; give it an icon attribute or drop `defaultIconByRole`.
98
+
99
+ ## 0.29.0
100
+
101
+ Consolidated the URL-identity model. A content entry's id, slug, date, and permalink are now derived in
102
+ one place (`entryIdentity`), so the content index and the manifest cannot drift on an entry's URL, and a
103
+ site's concept descriptors are resolved through one path shared by the admin runtime and the delivery
104
+ layer. No public surface changed.
105
+
106
+ The YAML URL policy is now validated at build. A permalink pattern must be root-relative and use only the
107
+ tokens `:slug`, `:year`, `:month`, and `:day`, a date token is valid only on a dated concept, a
108
+ `datePrefix` must be `year`, `month`, or `day`, and a policy keyed to an undeclared concept fails the
109
+ build.
110
+
111
+ Behavior note: a site whose `content:` URL policy was malformed and silently defaulted will now fail the
112
+ build with a named error. A valid policy is unaffected.
113
+
114
+ ## 0.28.0
115
+
116
+ ### Security
117
+ Closed the render attribute-sink residual by construction. A new post-dispatch guard runs last in
118
+ `createRenderer` and neutralizes the sinks a component `build()` could route a raw author attribute
119
+ value into, including the unsafe URL schemes `javascript:`, `data:`, and `vbscript:` in `href`,
120
+ `src`, `srcset`, `xlink:href`, `poster`, `formaction`, `action`, `object`'s `data`, and
121
+ `background`, the inline `on*` event handlers, and inline `style`, which is stripped wholesale. Safe
122
+ schemes, relative URLs, anchors, and the `cairn:` token are preserved. The guard is gated by the
123
+ existing `unsafeDisableSanitize` switch.
124
+
125
+ Behavior note: a site whose component `build()` emits a non-standard URL scheme, an `on*` handler,
126
+ or inline `style` will see that output neutralized. Route dynamic styling through a class or an
127
+ inert `data-*` attribute instead.
128
+
129
+ ## 0.27.0
130
+
131
+ ### Changed (breaking)
132
+ Narrowed the public export surface so each symbol has one canonical home. The `.` root and
133
+ `/sveltekit` no longer re-export another subpath's symbols, and the internal GitHub, signing, and
134
+ hast helpers left the public API. No symbol changed behavior; only where it exports from.
135
+
136
+ - Consumers must: import the delivery read helpers (`createContentIndex`, `createSiteIndexes`, the
137
+ feed, sitemap, robots, SEO, and pagination builders, `permalink`) from `@glw907/cairn-cms/delivery/data`
138
+ instead of the `.` root.
139
+ - Consumers must: import the public route loaders and the `*Response` helpers (`createPublicRoutes`,
140
+ `rssResponse`, `jsonFeedResponse`, `sitemapResponse`, `robotsResponse`) and the public route types
141
+ (`PublicRoutesDeps`, the public `ListData`, `TagData`, `TagIndexData`, `EntryData`) from
142
+ `@glw907/cairn-cms/delivery` instead of the `.` root or `/sveltekit`.
143
+ - Consumers must: stop importing the internal helpers that left the public API (`appJwt`,
144
+ `installationToken`, `signingSelfTest`, `appCredentials`, `treeUrl`, `contentsUrl`, `readRaw`,
145
+ `fileSha`, `listMarkdown`, `markdownFilesIn`, `commitFile`, `isElement`, `strProp`, `markFirstList`);
146
+ the engine wires GitHub token minting and the render pipeline internally, so no consumer needs them.
147
+
5
148
  ## 0.26.0
6
149
 
7
150
  ### Added
@@ -16,4 +16,3 @@ export declare function generateToken(): string;
16
16
  export declare function generateSessionId(): string;
17
17
  /** The lowercase hex SHA-256 of a token, for storage and lookup. */
18
18
  export declare function hashToken(token: string): Promise<string>;
19
- //# sourceMappingURL=crypto.d.ts.map
@@ -40,4 +40,3 @@ export declare function setEditorRole(db: D1Database, email: string, role: Role)
40
40
  * `removeOwnerIfNotLast`). Returns false (and writes nothing) when this is the last owner.
41
41
  */
42
42
  export declare function demoteOwnerIfNotLast(db: D1Database, email: string): Promise<boolean>;
43
- //# sourceMappingURL=store.d.ts.map
@@ -22,4 +22,3 @@ export interface AuthEnv {
22
22
  }): Promise<void>;
23
23
  };
24
24
  }
25
- //# sourceMappingURL=types.d.ts.map
@@ -2,12 +2,23 @@
2
2
  @component
3
3
  The admin shell: a DaisyUI drawer-and-navbar that wraps every authed admin page. The nav is
4
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.
5
+ root sets `data-theme` to the resolved light or dark theme (seeded from the SSR'd cookie choice,
6
+ flipped by the topbar toggle) and imports the self-contained Warm Stone theme, so the admin looks
7
+ identical on every host regardless of the site's own theme.
7
8
  -->
8
9
  <script lang="ts">
9
- import type { Snippet } from 'svelte';
10
+ import { onMount, untrack, type Component, type Snippet } from 'svelte';
10
11
  import type { LayoutData } from '../sveltekit/content-routes.js';
12
+ import { MenuIcon, LogOutIcon, SunIcon, MoonIcon, ChevronRightIcon, SearchIcon } from './admin-icons.js';
13
+ import CairnLogo from './CairnLogo.svelte';
14
+ import { cairnFaviconHref } from './cairn-favicon.js';
15
+ import { warnIfChromeWrapped } from './chrome-guard.js';
16
+ import FileTextIcon from '@lucide/svelte/icons/file-text';
17
+ import SignpostIcon from '@lucide/svelte/icons/signpost';
18
+ import SettingsIcon from '@lucide/svelte/icons/settings';
19
+ import UsersIcon from '@lucide/svelte/icons/users';
20
+ import BlocksIcon from '@lucide/svelte/icons/blocks';
21
+ import ExternalLinkIcon from '@lucide/svelte/icons/external-link';
11
22
  import './cairn-admin.css';
12
23
 
13
24
  interface Props {
@@ -19,62 +30,379 @@ admin looks identical on every host regardless of the site's own theme.
19
30
 
20
31
  let { data, children }: Props = $props();
21
32
 
33
+ // Persist an admin preference for a year, path-scoped to /admin so the cookie never reaches the
34
+ // host's own pages.
35
+ function writeAdminCookie(name: string, value: string) {
36
+ document.cookie = `${name}=${value}; path=/admin; max-age=31536000; samesite=lax`;
37
+ }
38
+
39
+ // A nav entry. `href` makes it a link; without one it is an inert stub (a developer-tool slot the
40
+ // extension mechanism has not wired yet).
22
41
  interface NavItem {
23
- href: string;
24
42
  label: string;
25
- owner?: boolean;
43
+ icon: Component;
44
+ href?: string;
26
45
  }
27
46
 
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 },
47
+ // The core Cairn functions, all in one group: the content concepts, the nav-menu editor (when the
48
+ // site configures one; a signpost, kept distinct from the Settings gear), the site Settings, and
49
+ // the owner-only Editors.
50
+ const coreItems: NavItem[] = $derived([
51
+ ...data.concepts.map((c) => ({ label: c.label, icon: FileTextIcon, href: `/admin/${c.id}` })),
52
+ ...(data.navLabel ? [{ label: data.navLabel, icon: SignpostIcon, href: '/admin/nav' }] : []),
53
+ { label: 'Settings', icon: SettingsIcon, href: '/admin/settings' },
54
+ ...(data.canManageEditors ? [{ label: 'Editors', icon: UsersIcon, href: '/admin/editors' }] : []),
32
55
  ]);
33
56
 
34
- const visibleNav = $derived(navItems.filter((item) => !item.owner || data.canManageEditors));
57
+ // The developer-extension groups: each custom-named, with its own items, collapsible like the core
58
+ // group. The CairnExtension seam will supply these; until it lands they are inert example stubs that
59
+ // show the shape, multiple named groups kept visually apart from the core functions.
60
+ const extensionGroups: { name: string; items: NavItem[] }[] = [
61
+ { name: 'Marketing', items: [
62
+ { label: 'Campaigns', icon: BlocksIcon },
63
+ { label: 'Audiences', icon: BlocksIcon },
64
+ ] },
65
+ { name: 'Shop', items: [
66
+ { label: 'Products', icon: BlocksIcon },
67
+ { label: 'Orders', icon: BlocksIcon },
68
+ ] },
69
+ ];
70
+
71
+ // Up to two uppercase initials from the display name, falling back to '?' for an empty name.
72
+ function initialsOf(displayName: string): string {
73
+ const letters = displayName
74
+ .split(/\s+/)
75
+ .filter(Boolean)
76
+ .slice(0, 2)
77
+ .map((word) => word[0]?.toUpperCase() ?? '')
78
+ .join('');
79
+ return letters || '?';
80
+ }
81
+
82
+ const initials = $derived(initialsOf(data.user.displayName));
35
83
 
36
84
  function isActive(href: string): boolean {
37
85
  return data.pathname === href || data.pathname.startsWith(`${href}/`);
38
86
  }
87
+
88
+ // Which nav groups are collapsed. Seeded once from the SSR'd cookie (so a collapsed group renders
89
+ // collapsed with no flash), then owned by the toggle below, which mirrors each change to the cookie.
90
+ let collapsed = $state(new Set(untrack(() => data.collapsedNav)));
91
+
92
+ function onToggleSection(label: string, open: boolean) {
93
+ const next = new Set(collapsed);
94
+ if (open) next.delete(label);
95
+ else next.add(label);
96
+ collapsed = next;
97
+ const value = [...next].map((entry) => encodeURIComponent(entry)).join(',');
98
+ writeAdminCookie('cairn-admin-nav-collapsed', value);
99
+ }
100
+
101
+ let drawerOpen = $state(false);
102
+
103
+ function onKeydown(e: KeyboardEvent) {
104
+ if (e.key.toLowerCase() === 'b' && (e.metaKey || e.ctrlKey)) {
105
+ e.preventDefault();
106
+ drawerOpen = !drawerOpen;
107
+ }
108
+ if (e.key.toLowerCase() === 'k' && (e.metaKey || e.ctrlKey)) {
109
+ e.preventDefault();
110
+ openPalette();
111
+ }
112
+ }
113
+
114
+ // Close the mobile drawer and the command palette whenever the active path changes (a nav click
115
+ // navigated). Closing the palette here, after the navigation lands, avoids racing a synchronous
116
+ // close() against a result link's own navigation, which would cancel it.
117
+ $effect(() => {
118
+ data.pathname;
119
+ drawerOpen = false;
120
+ paletteDialog?.close();
121
+ });
122
+
123
+ // Seed from the SSR'd theme once. The live theme is owned by this state and the toggle, so the
124
+ // initial read of data.theme is intentional and untracked to keep it out of any reactive graph.
125
+ let theme = $state<'cairn-admin' | 'cairn-admin-dark'>(untrack(() => data.theme));
126
+
127
+ // First mount with no persisted choice follows the OS preference. A returning user's cookie was
128
+ // already honored by the layout load (data.theme), so this only fires on a first-ever visit.
129
+ $effect(() => {
130
+ const hasCookie = document.cookie.split('; ').some((c) => c.startsWith('cairn-admin-theme='));
131
+ if (!hasCookie && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
132
+ theme = 'cairn-admin-dark';
133
+ }
134
+ });
135
+
136
+ function toggleTheme() {
137
+ theme = theme === 'cairn-admin' ? 'cairn-admin-dark' : 'cairn-admin';
138
+ writeAdminCookie('cairn-admin-theme', theme);
139
+ }
140
+
141
+ // The command palette: a quick jump-to over the admin's destinations plus a couple of actions, so
142
+ // the topbar carries something productive. Opened by the topbar trigger or Cmd/Ctrl+K.
143
+ interface Command {
144
+ label: string;
145
+ icon: Component;
146
+ href?: string;
147
+ external?: boolean;
148
+ action?: () => void;
149
+ }
150
+
151
+ let paletteDialog = $state<HTMLDialogElement>();
152
+ let paletteList = $state<HTMLUListElement>();
153
+ let paletteQuery = $state('');
154
+
155
+ // The bare data-theme wrapper is the admin root the dev chrome-guard measures from.
156
+ let rootEl = $state<HTMLElement>();
157
+ onMount(() => {
158
+ if (rootEl) warnIfChromeWrapped(rootEl);
159
+ });
160
+
161
+ const paletteCommands = $derived<Command[]>([
162
+ ...coreItems.map((item) => ({ label: item.label, icon: item.icon, href: item.href })),
163
+ { label: 'View the live site', icon: ExternalLinkIcon, href: '/', external: true },
164
+ theme === 'cairn-admin'
165
+ ? { label: 'Switch to dark mode', icon: MoonIcon, action: toggleTheme }
166
+ : { label: 'Switch to light mode', icon: SunIcon, action: toggleTheme },
167
+ ]);
168
+ const paletteResults = $derived(
169
+ paletteCommands.filter((c) => c.label.toLowerCase().includes(paletteQuery.trim().toLowerCase())),
170
+ );
171
+
172
+ function openPalette() {
173
+ if (paletteDialog?.open) return; // showModal throws on an already-open dialog
174
+ paletteQuery = '';
175
+ paletteDialog?.showModal();
176
+ }
177
+ // An action command (theme toggle). Link commands are real <a> elements that navigate on click, so
178
+ // the Enter shortcut clicks the first result element and both paths share the one navigation.
179
+ function runCommand(cmd: Command) {
180
+ paletteDialog?.close();
181
+ cmd.action?.();
182
+ }
183
+ function submitPalette() {
184
+ (paletteList?.querySelector('a, button') as HTMLElement | null)?.click();
185
+ }
186
+
187
+ interface Crumb {
188
+ label: string;
189
+ href?: string;
190
+ }
191
+
192
+ // Path-derived breadcrumbs: the concept label (from the nav) then the entry id segment. Only the
193
+ // /admin/<concept>/<id> depth shows a trail; a bare concept list shows just the concept.
194
+ const crumbs = $derived.by<Crumb[]>(() => {
195
+ const segs = data.pathname.split('/').filter(Boolean); // ['admin', concept, id?]
196
+ if (segs.length < 2 || segs[0] !== 'admin') return [];
197
+ const conceptId = segs[1];
198
+ const concept = data.concepts.find((c) => c.id === conceptId);
199
+ const out: Crumb[] = [{ label: concept?.label ?? conceptId, href: `/admin/${conceptId}` }];
200
+ if (segs[2]) out.push({ label: decodeURIComponent(segs[2]) });
201
+ return out;
202
+ });
203
+
204
+ // The browser-tab title: the deepest breadcrumb (the active concept or entry), then the brand.
205
+ const pageTitle = $derived(crumbs.length ? crumbs[crumbs.length - 1].label : 'Admin');
39
206
  </script>
40
207
 
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>
208
+ <svelte:head>
209
+ <title>{pageTitle} · {data.siteName}</title>
210
+ <link rel="icon" href={cairnFaviconHref} />
211
+ </svelte:head>
212
+
213
+ <svelte:window onkeydown={onKeydown} />
214
+
215
+ <!-- data-theme sits on a bare wrapper, not on the drawer itself: every admin rule is scoped as a
216
+ descendant of the theme root (`:where([data-theme]) .drawer`), so a class on the theme element
217
+ itself never matches. Keeping the drawer and its base/utility classes one level in lets the
218
+ scoped sheet style them. -->
219
+ <div data-theme={theme} bind:this={rootEl}>
220
+ <div class="drawer lg:drawer-open min-h-screen bg-base-200 text-base-content">
221
+ <input id="cairn-drawer" type="checkbox" class="drawer-toggle" bind:checked={drawerOpen} />
222
+
223
+ <div class="drawer-content flex flex-col">
224
+ <!-- The topbar is a flat, opaque continuation of the sidebar's brand band: same surface and the
225
+ same hairline, no shadow, so the two form one clean header strip across the sidebar seam. -->
226
+ <div class="navbar bg-base-100 border-b border-[var(--cairn-card-border)] sticky top-0 z-30 gap-2 px-4 lg:px-8">
227
+ <div class="flex-none lg:hidden">
228
+ <label for="cairn-drawer" aria-label="Open menu" class="btn btn-square btn-ghost">
229
+ <MenuIcon class="h-5 w-5" />
230
+ </label>
231
+ </div>
232
+ <!-- Context on the left: the breadcrumb trail inside an entry, the site name on a bare list.
233
+ Hidden on small screens to leave room for the palette trigger. -->
234
+ <div class="hidden min-w-0 max-w-[30%] flex-none truncate sm:block">
235
+ {#if crumbs.length > 1}
236
+ <nav aria-label="Breadcrumb" class="breadcrumbs text-sm">
237
+ <ul>
238
+ {#each crumbs as crumb (crumb.href ?? crumb.label)}
239
+ <li>{#if crumb.href}<a href={crumb.href}>{crumb.label}</a>{:else}{crumb.label}{/if}</li>
240
+ {/each}
241
+ </ul>
242
+ </nav>
243
+ {:else}
244
+ <span class="font-semibold tracking-tight">{data.siteName}</span>
245
+ {/if}
246
+ </div>
247
+ <!-- The command-palette trigger fills the center: a quick jump-to over the admin, opened here
248
+ or with Cmd/Ctrl+K. -->
249
+ <div class="flex min-w-0 flex-1 justify-center">
250
+ <button
251
+ type="button"
252
+ onclick={openPalette}
253
+ class="flex w-full max-w-md items-center gap-2 rounded-field border border-[var(--cairn-card-border)] bg-base-200/70 px-3 py-1.5 text-sm text-[var(--color-muted)] transition-colors hover:bg-base-200 hover:text-base-content"
254
+ >
255
+ <SearchIcon class="h-4 w-4 shrink-0" aria-hidden="true" />
256
+ <span class="truncate">Search or jump to&hellip;</span>
257
+ <kbd class="ml-auto hidden rounded border border-[var(--cairn-card-border)] px-1.5 text-[0.6875rem] font-medium sm:inline">&#8984;K</kbd>
258
+ </button>
259
+ </div>
260
+ <div class="flex-none">
261
+ <button type="button" class="btn btn-square btn-ghost" aria-label="Toggle theme" onclick={toggleTheme}>
262
+ {#if theme === 'cairn-admin'}<MoonIcon class="h-5 w-5" />{:else}<SunIcon class="h-5 w-5" />{/if}
263
+ </button>
264
+ </div>
52
265
  </div>
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>
266
+
267
+ <main class="flex-1 p-4 lg:p-8">
268
+ {@render children()}
269
+ </main>
270
+
271
+ <dialog bind:this={paletteDialog} class="modal" aria-label="Search or jump to">
272
+ <div class="modal-box max-w-xl self-start p-0 sm:mt-[12vh]">
273
+ <div class="flex items-center gap-2 border-b border-[var(--cairn-card-border)] px-4">
274
+ <SearchIcon class="h-4 w-4 shrink-0 text-[var(--color-muted)]" aria-hidden="true" />
275
+ <input
276
+ bind:value={paletteQuery}
277
+ type="text"
278
+ aria-label="Search or jump to"
279
+ placeholder="Search or jump to…"
280
+ class="w-full bg-transparent py-3.5 text-sm outline-hidden placeholder:text-[var(--color-muted)]"
281
+ onkeydown={(e) => {
282
+ if (e.key === 'Enter') {
283
+ e.preventDefault();
284
+ submitPalette();
285
+ }
286
+ }}
287
+ />
288
+ </div>
289
+ {#if paletteResults.length}
290
+ <ul bind:this={paletteList} class="menu max-h-[60vh] w-full gap-0.5 overflow-y-auto p-2">
291
+ {#each paletteResults as cmd (cmd.label)}
292
+ <li>
293
+ {#if cmd.href}
294
+ <!-- An internal link navigates and the pathname effect closes the palette once the route lands,
295
+ so it carries no onclick (closing here would cancel the navigation). An external link
296
+ opens a new tab and leaves this page, so it closes the palette itself. -->
297
+ <a
298
+ href={cmd.href}
299
+ target={cmd.external ? '_blank' : undefined}
300
+ rel={cmd.external ? 'noopener' : undefined}
301
+ onclick={cmd.external ? () => paletteDialog?.close() : undefined}
302
+ >
303
+ <cmd.icon class="h-4 w-4 text-[var(--color-muted)]" aria-hidden="true" />
304
+ {cmd.label}
305
+ {#if cmd.external}<ExternalLinkIcon class="ml-auto h-3.5 w-3.5 opacity-50" aria-hidden="true" />{/if}
306
+ </a>
307
+ {:else}
308
+ <button type="button" onclick={() => runCommand(cmd)}>
309
+ <cmd.icon class="h-4 w-4 text-[var(--color-muted)]" aria-hidden="true" />
310
+ {cmd.label}
311
+ </button>
312
+ {/if}
313
+ </li>
314
+ {/each}
315
+ </ul>
316
+ {:else}
317
+ <p class="px-4 py-6 text-center text-sm text-[var(--color-muted)]">No matches for "{paletteQuery}".</p>
318
+ {/if}
319
+ </div>
320
+ <form method="dialog" class="modal-backdrop"><button tabindex="-1" aria-label="Close">close</button></form>
321
+ </dialog>
55
322
  </div>
56
323
 
57
- <main class="flex-1 p-4 lg:p-8">
58
- {@render children()}
59
- </main>
60
- </div>
324
+ <div class="drawer-side">
325
+ <label for="cairn-drawer" aria-label="Close menu" class="drawer-overlay"></label>
326
+ <nav class="bg-base-100 flex min-h-full w-64 flex-col border-r border-[var(--cairn-card-border)]" aria-label="Site content">
327
+ <!-- Brand band, the same height as the topbar. The mark sits in a filled "app-icon" tile, which
328
+ anchors the corner as a deliberate brand object rather than a washed box. The logo and
329
+ wordmark link to the admin home. -->
330
+ <div class="flex h-16 flex-none items-center border-b border-[var(--cairn-card-border)] px-3">
331
+ <a href="/admin" aria-label="Cairn admin home" class="flex items-center gap-2.5 rounded-field px-2 py-1.5 transition-colors hover:bg-base-content/[0.05]">
332
+ <span class="flex h-8 w-8 items-center justify-center rounded-xl bg-primary text-primary-content shadow-sm">
333
+ <CairnLogo class="h-5 w-5" />
334
+ </span>
335
+ <span class="text-xl font-bold tracking-[-0.01em] font-[family-name:var(--font-display)]">Cairn</span>
336
+ <span class="rounded-md border border-base-300 px-1.5 py-px text-[0.625rem] font-semibold uppercase tracking-[0.12em] text-[var(--color-muted)]">CMS</span>
337
+ </a>
338
+ </div>
339
+
340
+ <div class="flex-1 space-y-1 overflow-y-auto py-4">
341
+ {#snippet navSection(label: string, items: NavItem[])}
342
+ <details class="px-2" open={!collapsed.has(label)} ontoggle={(e) => onToggleSection(label, e.currentTarget.open)}>
343
+ <summary class="group/sec flex cursor-pointer select-none items-center gap-2 rounded-field bg-base-content/[0.04] py-2 pl-5 pr-3 text-[0.6875rem] font-semibold uppercase tracking-[0.08em] text-[var(--color-muted)] transition-colors hover:bg-base-content/[0.08] hover:text-base-content">
344
+ <span class="truncate">{label}</span>
345
+ <ChevronRightIcon class="cairn-caret ml-auto h-3 w-3 shrink-0 opacity-50 transition-opacity group-hover/sec:opacity-90" aria-hidden="true" />
346
+ </summary>
347
+ <ul class="menu menu-sm mt-1 w-full gap-0.5 p-0">
348
+ {#each items as item (item.href ?? item.label)}
349
+ <li>
350
+ {#if item.href}
351
+ <a
352
+ href={item.href}
353
+ class={isActive(item.href)
354
+ ? 'bg-primary/10 font-semibold text-primary'
355
+ : 'font-medium text-[var(--color-subtle)]'}
356
+ aria-current={isActive(item.href) ? 'page' : undefined}
357
+ >
358
+ <item.icon class="h-4 w-4" aria-hidden="true" />
359
+ {item.label}
360
+ </a>
361
+ {:else}
362
+ <span
363
+ class="cursor-default font-medium text-[var(--color-muted)] opacity-60"
364
+ aria-disabled="true"
365
+ title="A slot for a site developer's own admin tool. Not wired yet."
366
+ >
367
+ <item.icon class="h-4 w-4" aria-hidden="true" />
368
+ {item.label}
369
+ </span>
370
+ {/if}
371
+ </li>
372
+ {/each}
373
+ </ul>
374
+ </details>
375
+ {/snippet}
61
376
 
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>
377
+ <!-- Core is the built-in Cairn functions; each developer group sits at the same level. All
378
+ are peer collapsible sections. The extension groups are inert stubs until the
379
+ CairnExtension seam supplies them. -->
380
+ {@render navSection('Core', coreItems)}
381
+ {#each extensionGroups as group (group.name)}
382
+ {@render navSection(group.name, group.items)}
383
+ {/each}
384
+ </div>
385
+
386
+ <div class="flex-none border-t border-[var(--cairn-card-border)] px-5 py-4">
387
+ <div class="flex items-center gap-3">
388
+ <div class="avatar avatar-placeholder">
389
+ <div class="bg-neutral text-neutral-content w-9 rounded-full">
390
+ <span class="text-sm">{initials}</span>
391
+ </div>
392
+ </div>
393
+ <div class="min-w-0 flex-1">
394
+ <div class="truncate text-sm font-medium">{data.user.displayName}</div>
395
+ <div class="truncate text-xs text-[var(--color-muted)]">{data.user.email}</div>
396
+ <div class="text-xs capitalize text-[var(--color-subtle)]">{data.user.role}</div>
397
+ </div>
398
+ </div>
399
+ <form method="POST" action="/admin/auth/logout" class="mt-4">
400
+ <button type="submit" class="btn btn-ghost btn-sm btn-block justify-start">
401
+ <LogOutIcon class="h-4 w-4" /> Sign out
402
+ </button>
403
+ </form>
404
+ </div>
405
+ </nav>
406
+ </div>
79
407
  </div>
80
408
  </div>
@@ -1,4 +1,4 @@
1
- import type { Snippet } from 'svelte';
1
+ import { type Component, type Snippet } from 'svelte';
2
2
  import type { LayoutData } from '../sveltekit/content-routes.js';
3
3
  import './cairn-admin.css';
4
4
  interface Props {
@@ -10,10 +10,10 @@ interface Props {
10
10
  /**
11
11
  * The admin shell: a DaisyUI drawer-and-navbar that wraps every authed admin page. The nav is
12
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.
13
+ * root sets `data-theme` to the resolved light or dark theme (seeded from the SSR'd cookie choice,
14
+ * flipped by the topbar toggle) and imports the self-contained Warm Stone theme, so the admin looks
15
+ * identical on every host regardless of the site's own theme.
15
16
  */
16
- declare const AdminLayout: import("svelte").Component<Props, {}, "">;
17
+ declare const AdminLayout: Component<Props, {}, "">;
17
18
  type AdminLayout = ReturnType<typeof AdminLayout>;
18
19
  export default AdminLayout;
19
- //# sourceMappingURL=AdminLayout.svelte.d.ts.map