@glw907/cairn-cms 0.5.0 → 0.5.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 (64) hide show
  1. package/dist/adapter.d.ts +24 -0
  2. package/dist/adapter.d.ts.map +1 -1
  3. package/dist/auth/capabilities.d.ts +7 -0
  4. package/dist/auth/capabilities.d.ts.map +1 -0
  5. package/dist/auth/capabilities.js +26 -0
  6. package/dist/auth/index.d.ts +1 -0
  7. package/dist/auth/index.d.ts.map +1 -1
  8. package/dist/auth/index.js +1 -0
  9. package/dist/components/AdminLayout.svelte +72 -16
  10. package/dist/components/AdminLayout.svelte.d.ts +9 -0
  11. package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
  12. package/dist/components/CollectionList.svelte +96 -0
  13. package/dist/components/CollectionList.svelte.d.ts +8 -0
  14. package/dist/components/CollectionList.svelte.d.ts.map +1 -0
  15. package/dist/components/ComponentPalette.svelte +34 -0
  16. package/dist/components/ComponentPalette.svelte.d.ts +9 -0
  17. package/dist/components/ComponentPalette.svelte.d.ts.map +1 -0
  18. package/dist/components/EditPage.svelte +66 -28
  19. package/dist/components/EditPage.svelte.d.ts +2 -0
  20. package/dist/components/EditPage.svelte.d.ts.map +1 -1
  21. package/dist/components/NavTree.svelte +128 -0
  22. package/dist/components/NavTree.svelte.d.ts +8 -0
  23. package/dist/components/NavTree.svelte.d.ts.map +1 -0
  24. package/dist/components/index.d.ts +3 -1
  25. package/dist/components/index.d.ts.map +1 -1
  26. package/dist/components/index.js +3 -1
  27. package/dist/editor.d.ts +25 -0
  28. package/dist/editor.d.ts.map +1 -0
  29. package/dist/editor.js +20 -0
  30. package/dist/frontmatter.d.ts +3 -0
  31. package/dist/frontmatter.d.ts.map +1 -0
  32. package/dist/frontmatter.js +16 -0
  33. package/dist/index.d.ts +2 -0
  34. package/dist/index.d.ts.map +1 -1
  35. package/dist/index.js +2 -0
  36. package/dist/nav.d.ts +58 -0
  37. package/dist/nav.d.ts.map +1 -0
  38. package/dist/nav.js +86 -0
  39. package/dist/slug.d.ts +7 -0
  40. package/dist/slug.d.ts.map +1 -0
  41. package/dist/slug.js +15 -0
  42. package/dist/sveltekit/index.d.ts +102 -12
  43. package/dist/sveltekit/index.d.ts.map +1 -1
  44. package/dist/sveltekit/index.js +219 -20
  45. package/package.json +7 -2
  46. package/src/lib/adapter.ts +25 -0
  47. package/src/lib/auth/capabilities.ts +35 -0
  48. package/src/lib/auth/index.ts +1 -0
  49. package/src/lib/components/AdminLayout.svelte +72 -16
  50. package/src/lib/components/CollectionList.svelte +96 -0
  51. package/src/lib/components/ComponentPalette.svelte +34 -0
  52. package/src/lib/components/EditPage.svelte +66 -28
  53. package/src/lib/components/NavTree.svelte +128 -0
  54. package/src/lib/components/index.ts +3 -1
  55. package/src/lib/editor.ts +38 -0
  56. package/src/lib/frontmatter.ts +17 -0
  57. package/src/lib/index.ts +2 -0
  58. package/src/lib/nav.ts +117 -0
  59. package/src/lib/slug.ts +16 -0
  60. package/src/lib/sveltekit/index.ts +303 -26
  61. package/dist/components/AdminList.svelte +0 -33
  62. package/dist/components/AdminList.svelte.d.ts +0 -10
  63. package/dist/components/AdminList.svelte.d.ts.map +0 -1
  64. package/src/lib/components/AdminList.svelte +0 -33
package/dist/adapter.d.ts CHANGED
@@ -35,6 +35,13 @@ export interface CairnCollection {
35
35
  /** Route `[type]` segment and list key, e.g. `posts`. */
36
36
  type: string;
37
37
  label: string;
38
+ /**
39
+ * Editing shape. `story` (the default when absent) is a dated feed entry; `page` is a
40
+ * navigation-placed entry with a path-like slug and no date emphasis. Drives the create
41
+ * form and the editor header. Never gates editing capability: the palette and toolbar are
42
+ * available to both. (Pass K, R4.)
43
+ */
44
+ kind?: 'page' | 'story';
38
45
  /** Repo-relative folder holding the collection's markdown files. */
39
46
  dir: string;
40
47
  /** Editor form fields, rendered in order. */
@@ -42,6 +49,17 @@ export interface CairnCollection {
42
49
  /** Validate raw frontmatter (from the form) into the on-disk object, throwing on error. */
43
50
  validate(data: Record<string, unknown>, source: string): object;
44
51
  }
52
+ /** A managed navigation menu, read from and committed to the site's YAML config file. */
53
+ export interface NavMenuConfig {
54
+ /** Repo-relative path to the site-config YAML, e.g. 'src/lib/site.config.yaml'. */
55
+ configPath: string;
56
+ /** Key within the file's `menus` map, e.g. 'primary'. */
57
+ menuName: string;
58
+ /** Sidebar/admin label for the menu. */
59
+ label: string;
60
+ /** Max nesting depth allowed in the editor (1 = flat). Defaults to 2. */
61
+ maxDepth?: number;
62
+ }
45
63
  export interface CairnAdapter {
46
64
  /** Branding + magic-link email copy. */
47
65
  siteName: string;
@@ -60,6 +78,12 @@ export interface CairnAdapter {
60
78
  * omit it or supply an empty registry.
61
79
  */
62
80
  registry?: ComponentRegistry;
81
+ /**
82
+ * The navigation menu this site manages from `/admin/nav` (R3/Pass L2). The menu lives in the
83
+ * site's git-committed YAML config (read at build time by the layout, committed back by the
84
+ * editor). Omit to hide the nav surface, the same opt-in shape as `registry`.
85
+ */
86
+ navMenu?: NavMenuConfig;
63
87
  }
64
88
  /** Look up a collection by its route segment, or undefined if the segment is unknown. */
65
89
  export declare function findCollection(adapter: CairnAdapter, type: string): CairnCollection | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/lib/adapter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,UAAU,SAAS;IACjB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,IAAI,EAAE,SAAS,CAAC;CACjB;AACD,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AACD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,SAAS,GACT,aAAa,GACb,YAAY,GACZ,SAAS,GACT,aAAa,CAAC;AAElB,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,6CAA6C;IAC7C,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACjE;AAED,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,CAAC;CAC9B;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE/F;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,eAAe,EAC3B,IAAI,EAAE,QAAQ,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB"}
1
+ {"version":3,"file":"adapter.d.ts","sourceRoot":"","sources":["../src/lib/adapter.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,UAAU,CAAC;AAElD,UAAU,SAAS;IACjB,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;CACd;AACD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AACD,MAAM,WAAW,YAAa,SAAQ,SAAS;IAC7C,IAAI,EAAE,SAAS,CAAC;CACjB;AACD,MAAM,WAAW,SAAU,SAAQ,SAAS;IAC1C,IAAI,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;CAC5B;AACD,MAAM,WAAW,aAAc,SAAQ,SAAS;IAC9C,IAAI,EAAE,UAAU,CAAC;IACjB,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,MAAM,UAAU,GAClB,SAAS,GACT,SAAS,GACT,aAAa,GACb,YAAY,GACZ,SAAS,GACT,aAAa,CAAC;AAElB,MAAM,WAAW,eAAe;IAC9B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IACxB,oEAAoE;IACpE,GAAG,EAAE,MAAM,CAAC;IACZ,6CAA6C;IAC7C,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,2FAA2F;IAC3F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;CACjE;AAED,yFAAyF;AACzF,MAAM,WAAW,aAAa;IAC5B,mFAAmF;IACnF,UAAU,EAAE,MAAM,CAAC;IACnB,yDAAyD;IACzD,QAAQ,EAAE,MAAM,CAAC;IACjB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,yEAAyE;IACzE,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,wCAAwC;IACxC,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,OAAO,EAAE,OAAO,CAAC;IACjB,2EAA2E;IAC3E,OAAO,EAAE,cAAc,CAAC;IACxB,WAAW,EAAE,eAAe,EAAE,CAAC;IAC/B;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,iBAAiB,CAAC;IAC7B;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;CACzB;AAED,yFAAyF;AACzF,wBAAgB,cAAc,CAAC,OAAO,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,GAAG,eAAe,GAAG,SAAS,CAE/F;AAED,0FAA0F;AAC1F,wBAAgB,mBAAmB,CACjC,UAAU,EAAE,eAAe,EAC3B,IAAI,EAAE,QAAQ,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA0BzB"}
@@ -0,0 +1,7 @@
1
+ import type { CairnUser } from './guard';
2
+ export type Capability = 'story:create' | 'story:edit' | 'page:edit' | 'page:create' | 'nav:manage' | 'user:manage';
3
+ /** Does this user hold the capability? A signed-out (null) user holds nothing. */
4
+ export declare function can(user: CairnUser | null, cap: Capability): boolean;
5
+ /** Assert the capability for a route load/action: 401 when signed out, 403 when under-privileged. */
6
+ export declare function requireCapability(user: CairnUser | null, cap: Capability): CairnUser;
7
+ //# sourceMappingURL=capabilities.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"capabilities.d.ts","sourceRoot":"","sources":["../../src/lib/auth/capabilities.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEzC,MAAM,MAAM,UAAU,GAClB,cAAc,GACd,YAAY,GACZ,WAAW,GACX,aAAa,GACb,YAAY,GACZ,aAAa,CAAC;AASlB,kFAAkF;AAClF,wBAAgB,GAAG,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,EAAE,GAAG,EAAE,UAAU,GAAG,OAAO,CAIpE;AAED,qGAAqG;AACrG,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,IAAI,EAAE,GAAG,EAAE,UAAU,GAAG,SAAS,CAIpF"}
@@ -0,0 +1,26 @@
1
+ // cairn-core: capability checks. Management surfaces gate on a capability, not on a role name,
2
+ // so the two-tier owner/editor model can grow finer capabilities (and a future role) additively.
3
+ // Creating a page and changing the nav are structural acts, so they sit with owner; editing a
4
+ // page's content and running the story feed are everyday editor work.
5
+ import { error } from '@sveltejs/kit';
6
+ // One source of truth. `'all'` means every capability; otherwise the explicit grant list. A future
7
+ // `manager` role is one more row here, no call-site changes.
8
+ const CAPS_BY_ROLE = {
9
+ owner: 'all',
10
+ editor: ['story:create', 'story:edit', 'page:edit'],
11
+ };
12
+ /** Does this user hold the capability? A signed-out (null) user holds nothing. */
13
+ export function can(user, cap) {
14
+ if (!user)
15
+ return false;
16
+ const grants = CAPS_BY_ROLE[user.role];
17
+ return grants === 'all' || grants.includes(cap);
18
+ }
19
+ /** Assert the capability for a route load/action: 401 when signed out, 403 when under-privileged. */
20
+ export function requireCapability(user, cap) {
21
+ if (!user)
22
+ throw error(401, 'Not signed in');
23
+ if (!can(user, cap))
24
+ throw error(403, 'You do not have permission to do that');
25
+ return user;
26
+ }
@@ -1,4 +1,5 @@
1
1
  export { createAuth, type Auth, type AuthEnv, type AuthBranding } from './config';
2
2
  export { loadSession, requireSession, confirmSignIn, signOut, type CairnUser } from './guard';
3
3
  export { adminsLoad, addAdmin, removeAdmin, setAdminRole, requireOwner, type AdminsData } from './admins';
4
+ export { can, requireCapability, type Capability } from './capabilities';
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAC9F,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/auth/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,UAAU,EAAE,KAAK,IAAI,EAAE,KAAK,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,UAAU,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,aAAa,EAAE,OAAO,EAAE,KAAK,SAAS,EAAE,MAAM,SAAS,CAAC;AAC9F,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,YAAY,EAAE,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAC1G,OAAO,EAAE,GAAG,EAAE,iBAAiB,EAAE,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC"}
@@ -4,3 +4,4 @@
4
4
  export { createAuth } from './config';
5
5
  export { loadSession, requireSession, confirmSignIn, signOut } from './guard';
6
6
  export { adminsLoad, addAdmin, removeAdmin, setAdminRole, requireOwner } from './admins';
7
+ export { can, requireCapability } from './capabilities';
@@ -1,11 +1,7 @@
1
1
  <script lang="ts">
2
- // Neutral admin chrome, shared across sites so the tool looks identical everywhere (only the
3
- // adapter's siteName varies). When signed in it's a responsive DaisyUI drawer+navbar shell
4
- // (`drawer lg:drawer-open`, sidebar pinned on desktop, slide-over + hamburger on mobile),
5
- // patterned on scosman/CMSaasStarter's `(admin)/(menu)` layout. The nav is data-driven and
6
- // role-gated, so a new surface is one entry in `nav` (plus its route + component). Signed out
7
- // (the login page lives under this layout) it falls back to a minimal centered shell.
8
- // Each site's `admin/+layout.svelte` is a one-line shim that forwards `data` + `children`.
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.
9
5
  import type { Snippet } from 'svelte';
10
6
  import type { CairnUser } from '../auth';
11
7
 
@@ -13,7 +9,14 @@
13
9
  data,
14
10
  children,
15
11
  }: {
16
- data: { siteName: string; user: CairnUser | null; pathname: string };
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
+ };
17
20
  children: Snippet;
18
21
  } = $props();
19
22
 
@@ -27,12 +30,17 @@
27
30
  }
28
31
 
29
32
  const nav = $derived<NavItem[]>([
30
- {
31
- href: '/admin',
32
- label: 'Content',
33
+ ...data.collections.map((collection) => ({
34
+ href: `/admin/${collection.type}`,
35
+ label: collection.label,
33
36
  icon: contentIcon,
34
- active: data.pathname === '/admin' || data.pathname.startsWith('/admin/edit'),
35
- },
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
+ : []),
36
44
  {
37
45
  href: '/admin/admins',
38
46
  label: 'Editors',
@@ -43,7 +51,7 @@
43
51
  ]);
44
52
  const visibleNav = $derived(nav.filter((item) => !item.owner || data.user?.role === 'owner'));
45
53
 
46
- // Close the slide-over after a nav tap on mobile (no-op on desktop where it's pinned open).
54
+ // Close the slide-over after a nav tap on mobile.
47
55
  function closeDrawer(): void {
48
56
  const toggle = document.getElementById('admin-drawer');
49
57
  if (toggle instanceof HTMLInputElement) toggle.checked = false;
@@ -64,12 +72,19 @@
64
72
  </svg>
65
73
  {/snippet}
66
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
+
67
82
  <svelte:head>
68
83
  <meta name="robots" content="noindex, nofollow" />
69
84
  </svelte:head>
70
85
 
71
86
  {#if data.user}
72
- <div class="drawer min-h-screen bg-base-200 lg:drawer-open" data-pagefind-ignore>
87
+ <div class="cairn-admin drawer min-h-screen bg-base-200 lg:drawer-open" data-pagefind-ignore>
73
88
  <input id="admin-drawer" type="checkbox" class="drawer-toggle" />
74
89
 
75
90
  <div class="drawer-content">
@@ -122,9 +137,50 @@
122
137
  </div>
123
138
  {:else}
124
139
  <!-- Signed out (login page): no nav, just a centered surface. -->
125
- <div class="min-h-screen bg-base-200" data-pagefind-ignore>
140
+ <div class="cairn-admin min-h-screen bg-base-200" data-pagefind-ignore>
126
141
  <div class="mx-auto max-w-3xl px-4 py-8">
127
142
  {@render children()}
128
143
  </div>
129
144
  </div>
130
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
+
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>
@@ -5,6 +5,15 @@ type $$ComponentProps = {
5
5
  siteName: string;
6
6
  user: CairnUser | null;
7
7
  pathname: string;
8
+ collections: {
9
+ type: string;
10
+ label: string;
11
+ }[];
12
+ navMenus: {
13
+ name: string;
14
+ label: string;
15
+ }[];
16
+ canManageNav: boolean;
8
17
  };
9
18
  children: Snippet;
10
19
  };
@@ -1 +1 @@
1
- {"version":3,"file":"AdminLayout.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/AdminLayout.svelte.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAWxC,KAAK,gBAAgB,GAAI;IACtB,IAAI,EAAE;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IACrE,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC;AAyHJ,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":"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"}
@@ -0,0 +1,96 @@
1
+ <script lang="ts">
2
+ // One collection's entries: a table (title, date, draft badge) linking into the editor, plus a
3
+ // collapsible "New entry" form. The author types a title; the slug stem derives from it (R4) and
4
+ // stays editable. A story collection also collects a date, which createEntry forwards so the new
5
+ // entry opens with its date set. Placeholders differ by kind. The shell (AdminLayout) owns the
6
+ // chrome and nav; this renders only the body.
7
+ import type { CollectionListData } from '../sveltekit';
8
+ import { slugify } from '../slug';
9
+
10
+ let { data }: { data: CollectionListData } = $props();
11
+
12
+ let title = $state('');
13
+ let slug = $state('');
14
+ let slugEdited = $state(false);
15
+
16
+ // Keep the slug in sync with the title until the author edits the slug directly.
17
+ function onTitleInput(value: string) {
18
+ title = value;
19
+ if (!slugEdited) slug = slugify(value);
20
+ }
21
+
22
+ const slugPlaceholder = $derived(data.kind === 'page' ? 'about-us' : '2026-05-my-entry');
23
+ </script>
24
+
25
+ <div class="flex items-center justify-between gap-4">
26
+ <h1 class="text-2xl font-bold">{data.label}</h1>
27
+ {#if data.canCreate}
28
+ <details class="dropdown dropdown-end">
29
+ <summary class="btn btn-primary btn-sm">New entry</summary>
30
+ <form
31
+ method="POST"
32
+ action="?/create"
33
+ class="dropdown-content z-10 mt-2 flex w-80 flex-col gap-2 rounded-box border border-base-300 bg-base-100 p-4 shadow"
34
+ >
35
+ <label class="flex flex-col gap-1">
36
+ <span class="text-sm font-medium">Title</span>
37
+ <input
38
+ type="text"
39
+ value={title}
40
+ oninput={(e) => onTitleInput(e.currentTarget.value)}
41
+ placeholder="A human title"
42
+ class="input w-full"
43
+ />
44
+ </label>
45
+
46
+ {#if data.kind === 'story'}
47
+ <label class="flex flex-col gap-1">
48
+ <span class="text-sm font-medium">Date</span>
49
+ <input type="date" name="date" class="input w-full" />
50
+ </label>
51
+ {/if}
52
+
53
+ <label class="flex flex-col gap-1">
54
+ <span class="text-sm font-medium">Slug</span>
55
+ <input
56
+ type="text"
57
+ name="id"
58
+ required
59
+ bind:value={slug}
60
+ oninput={() => (slugEdited = true)}
61
+ placeholder={slugPlaceholder}
62
+ pattern="[a-z0-9]([a-z0-9-]*[a-z0-9])?"
63
+ class="input w-full"
64
+ />
65
+ <span class="text-xs opacity-60">Lowercase letters, numbers, and hyphens. Becomes the filename.</span>
66
+ </label>
67
+
68
+ <button type="submit" class="btn btn-primary btn-sm">Create &amp; edit</button>
69
+ </form>
70
+ </details>
71
+ {/if}
72
+ </div>
73
+
74
+ {#if data.formError}
75
+ <div class="alert alert-error mt-4"><span>{data.formError}</span></div>
76
+ {/if}
77
+
78
+ {#if data.error}
79
+ <div class="alert alert-warning mt-6">Couldn't load {data.label.toLowerCase()}: {data.error}</div>
80
+ {:else if data.entries.length === 0}
81
+ <p class="mt-6 opacity-60">No entries yet.</p>
82
+ {:else}
83
+ <ul class="menu mt-6 rounded-box border border-base-300 bg-base-100 p-2">
84
+ {#each data.entries as entry (entry.path)}
85
+ <li>
86
+ <a href="/admin/edit/{data.type}/{entry.id}" class="flex items-center justify-between gap-3">
87
+ <span class="flex items-center gap-2">
88
+ <span>{entry.title}</span>
89
+ {#if entry.draft}<span class="badge badge-warning badge-sm">Draft</span>{/if}
90
+ </span>
91
+ {#if entry.date}<span class="text-xs opacity-60">{entry.date}</span>{/if}
92
+ </a>
93
+ </li>
94
+ {/each}
95
+ </ul>
96
+ {/if}
@@ -0,0 +1,8 @@
1
+ import type { CollectionListData } from '../sveltekit';
2
+ type $$ComponentProps = {
3
+ data: CollectionListData;
4
+ };
5
+ declare const CollectionList: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type CollectionList = ReturnType<typeof CollectionList>;
7
+ export default CollectionList;
8
+ //# sourceMappingURL=CollectionList.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"CollectionList.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/CollectionList.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAGtD,KAAK,gBAAgB,GAAI;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,CAAC;AAiFvD,QAAA,MAAM,cAAc,sDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
@@ -0,0 +1,34 @@
1
+ <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';
7
+
8
+ let { registry, insert }: { registry?: ComponentRegistry; insert: (template: string) => void } =
9
+ $props();
10
+
11
+ const defs = $derived(registry?.defs ?? []);
12
+ </script>
13
+
14
+ {#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
+ >
20
+ {#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>
29
+ </button>
30
+ </li>
31
+ {/each}
32
+ </ul>
33
+ </div>
34
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { ComponentRegistry } from '../render';
2
+ type $$ComponentProps = {
3
+ registry?: ComponentRegistry;
4
+ insert: (template: string) => void;
5
+ };
6
+ declare const ComponentPalette: import("svelte").Component<$$ComponentProps, {}, "">;
7
+ type ComponentPalette = ReturnType<typeof ComponentPalette>;
8
+ export default ComponentPalette;
9
+ //# sourceMappingURL=ComponentPalette.svelte.d.ts.map
@@ -0,0 +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,15 +1,27 @@
1
1
  <script lang="ts">
2
- // The editor: a per-field frontmatter form (driven by the adapter's `fields`) plus a Carta
3
- // markdown editor whose preview runs the site's plugin set (passed as `preview`). Data comes
4
- // from `editLoad` merged with `adminLayoutLoad` (siteName); `carta-md` is a peer dependency.
2
+ // The editor: a per-field frontmatter form (driven by the adapter's `fields`) beside a Carta
3
+ // markdown editor whose preview runs the site plugin set (`preview`). Content-forward layout:
4
+ // the editor is the prominent column, frontmatter sits in a side column (R4). A cairn control
5
+ // row hosts the insert-component palette (R10) and the preview toggle (R12); basic formatting
6
+ // stays on Carta's built-in toolbar (R11). Data comes from `editLoad` merged with the layout
7
+ // load (siteName); `carta-md` is a peer dependency.
5
8
  import { onMount } from 'svelte';
6
9
  import { Carta, MarkdownEditor } from 'carta-md';
7
10
  import 'carta-md/default.css';
8
11
  import { previewCartaOptions, type PreviewPlugins } from '../carta';
9
12
  import type { CairnField } from '../adapter';
13
+ import type { ComponentRegistry } from '../render';
10
14
  import type { EditData } from '../sveltekit';
15
+ import { cartaEditor } from '../editor';
16
+ import { dateInputValue } from '../frontmatter';
17
+ import ComponentPalette from './ComponentPalette.svelte';
11
18
 
12
- let { data, preview }: { data: EditData & { siteName: string }; preview: PreviewPlugins } = $props();
19
+ let {
20
+ data,
21
+ preview,
22
+ registry,
23
+ }: { data: EditData & { siteName: string }; preview: PreviewPlugins; registry?: ComponentRegistry } =
24
+ $props();
13
25
 
14
26
  // Body is editable state; the Carta editor's preview runs the exact site plugin set, so it
15
27
  // matches the live page. A hidden input carries the current value into the form.
@@ -18,14 +30,24 @@
18
30
 
19
31
  // svelte-ignore state_referenced_locally (the preview plugin set is fixed for the load)
20
32
  const carta = new Carta(previewCartaOptions(preview));
33
+ const editor = cartaEditor(() => carta);
21
34
 
22
- // Carta's MarkdownEditor must not render on the worker (it pulls Shiki). onMount fires only
23
- // in the browser, so SSR renders the plain textarea and the client swaps in the editor.
24
- // This is the kit-free equivalent of the per-site route's `$app/environment` `browser` guard.
35
+ // Carta's MarkdownEditor must not render on the worker (it pulls Shiki). onMount fires only in
36
+ // the browser, so SSR renders the plain textarea and the client swaps in the editor.
25
37
  let mounted = $state(false);
38
+
39
+ // Preview toggle (R12), persisted per user. 'split' shows the live preview beside the editor;
40
+ // 'tabs' foregrounds the editor full width with the preview one click away.
41
+ let mode = $state<'split' | 'tabs'>('split');
26
42
  onMount(() => {
27
43
  mounted = true;
44
+ const saved = localStorage.getItem('cairn-admin:preview');
45
+ if (saved === 'tabs' || saved === 'split') mode = saved;
28
46
  });
47
+ function togglePreview() {
48
+ mode = mode === 'split' ? 'tabs' : 'split';
49
+ localStorage.setItem('cairn-admin:preview', mode);
50
+ }
29
51
 
30
52
  // svelte-ignore state_referenced_locally (form defaults from the initial load)
31
53
  const fm = data.frontmatter as Record<string, unknown>;
@@ -39,31 +61,58 @@
39
61
  function fmFreeTags(key: string): string {
40
62
  return Array.isArray(fm[key]) ? (fm[key] as unknown[]).map(String).join(', ') : '';
41
63
  }
64
+
65
+ // Kind-aware header: a story leads with its date; a page leads with its slug/path.
66
+ const subtitle = $derived(
67
+ data.kind === 'page'
68
+ ? `Page · ${data.path}`
69
+ : `${data.label} · ${dateInputValue(fm['date']) || data.path}`,
70
+ );
42
71
  </script>
43
72
 
44
73
  <svelte:head>
45
- <title>Edit {data.title} · {data.siteName} CMS</title>
74
+ <title>{data.isNew ? `New ${data.label} entry` : `Edit ${data.title}`} · {data.siteName} CMS</title>
46
75
  </svelte:head>
47
76
 
48
77
  <div class="flex items-center justify-between gap-4">
49
78
  <div>
50
- <a href="/admin" class="text-sm opacity-70 hover:underline">← Back</a>
51
- <h1 class="mt-1 text-2xl font-bold">{data.title}</h1>
52
- <p class="text-sm opacity-60">{data.label} · {data.path}</p>
79
+ <a href="/admin/{data.type}" class="text-sm opacity-70 hover:underline">← Back to {data.label}</a>
80
+ <h1 class="mt-1 text-2xl font-bold">{data.isNew ? `New ${data.label} entry` : data.title}</h1>
81
+ <p class="text-sm opacity-60">{subtitle}</p>
53
82
  </div>
54
83
  </div>
55
84
 
56
85
  {#if data.saved}
57
- <div class="alert alert-success mt-6"><span>Saved committed to main; the site will redeploy.</span></div>
86
+ <div class="alert alert-success mt-6"><span>Saved. Committed to main; the site will redeploy.</span></div>
58
87
  {:else if data.error}
59
88
  <div class="alert alert-error mt-6"><span>{data.error}</span></div>
60
89
  {/if}
61
90
 
62
- <form method="POST" action="/admin/save" class="mt-6 flex flex-col gap-5">
91
+ <form method="POST" action="/admin/save" class="mt-6 flex flex-col gap-5 lg:grid lg:grid-cols-[1fr_20rem] lg:items-start">
63
92
  <input type="hidden" name="type" value={data.type} />
64
93
  <input type="hidden" name="id" value={data.id} />
94
+ {#if data.isNew}<input type="hidden" name="new" value="1" />{/if}
95
+
96
+ <!-- Editor column (content-forward: first and widest) -->
97
+ <div class="flex flex-col gap-3 lg:order-1">
98
+ <div class="flex items-center justify-between gap-2">
99
+ <ComponentPalette {registry} insert={(template) => editor.insertComponent(template)} />
100
+ <button type="button" class="btn btn-sm btn-ghost" onclick={togglePreview}>
101
+ {mode === 'split' ? 'Hide preview' : 'Show preview'}
102
+ </button>
103
+ </div>
104
+ <div class="rounded-box border border-base-300 bg-base-100 p-2">
105
+ <input type="hidden" name="body" value={body} />
106
+ {#if mounted}
107
+ <MarkdownEditor {carta} bind:value={body} {mode} />
108
+ {:else}
109
+ <textarea bind:value={body} rows="20" class="textarea w-full font-mono"></textarea>
110
+ {/if}
111
+ </div>
112
+ </div>
65
113
 
66
- <fieldset class="grid gap-4 rounded-box border border-base-300 bg-base-100 p-6">
114
+ <!-- Frontmatter side column -->
115
+ <fieldset class="grid gap-4 rounded-box border border-base-300 bg-base-100 p-6 lg:order-2">
67
116
  {#each data.fields as field (field.name)}
68
117
  {#if field.type === 'text' || field.type === 'date'}
69
118
  <label class="flex flex-col gap-1">
@@ -72,7 +121,7 @@
72
121
  type={field.type === 'date' ? 'date' : 'text'}
73
122
  name={field.name}
74
123
  required={field.required}
75
- value={fmString(field.name)}
124
+ value={field.type === 'date' ? dateInputValue(fm[field.name]) : fmString(field.name)}
76
125
  class="input w-full"
77
126
  />
78
127
  </label>
@@ -108,18 +157,7 @@
108
157
  </label>
109
158
  {/if}
110
159
  {/each}
111
- </fieldset>
112
-
113
- <div class="rounded-box border border-base-300 bg-base-100 p-2">
114
- <input type="hidden" name="body" value={body} />
115
- {#if mounted}
116
- <MarkdownEditor {carta} bind:value={body} mode="tabs" />
117
- {:else}
118
- <textarea bind:value={body} rows="20" class="textarea w-full font-mono"></textarea>
119
- {/if}
120
- </div>
121
160
 
122
- <div class="flex justify-end">
123
- <button type="submit" class="btn btn-primary">Save &amp; commit</button>
124
- </div>
161
+ <button type="submit" class="btn btn-primary mt-2">{data.isNew ? 'Create & commit' : 'Save & commit'}</button>
162
+ </fieldset>
125
163
  </form>
@@ -1,11 +1,13 @@
1
1
  import 'carta-md/default.css';
2
2
  import { type PreviewPlugins } from '../carta';
3
+ import type { ComponentRegistry } from '../render';
3
4
  import type { EditData } from '../sveltekit';
4
5
  type $$ComponentProps = {
5
6
  data: EditData & {
6
7
  siteName: string;
7
8
  };
8
9
  preview: PreviewPlugins;
10
+ registry?: ComponentRegistry;
9
11
  };
10
12
  declare const EditPage: import("svelte").Component<$$ComponentProps, {}, "">;
11
13
  type EditPage = ReturnType<typeof EditPage>;
@@ -1 +1 @@
1
- {"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,sBAAsB,CAAC;AAC9B,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAEpE,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAE5C,KAAK,gBAAgB,GAAI;IAAE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,OAAO,EAAE,cAAc,CAAA;CAAE,CAAC;AAwH7F,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}
1
+ {"version":3,"file":"EditPage.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/EditPage.svelte.ts"],"names":[],"mappings":"AAWA,OAAO,sBAAsB,CAAC;AAC9B,OAAO,EAAuB,KAAK,cAAc,EAAE,MAAM,UAAU,CAAC;AAEpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,WAAW,CAAC;AACnD,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAK5C,KAAK,gBAAgB,GAAI;IAAE,IAAI,EAAE,QAAQ,GAAG;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,OAAO,EAAE,cAAc,CAAC;IAAC,QAAQ,CAAC,EAAE,iBAAiB,CAAA;CAAE,CAAC;AA8J3H,QAAA,MAAM,QAAQ,sDAAwC,CAAC;AACvD,KAAK,QAAQ,GAAG,UAAU,CAAC,OAAO,QAAQ,CAAC,CAAC;AAC5C,eAAe,QAAQ,CAAC"}