@glw907/cairn-cms 0.4.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 (114) hide show
  1. package/README.md +4 -4
  2. package/dist/adapter.d.ts +34 -1
  3. package/dist/adapter.d.ts.map +1 -1
  4. package/dist/auth/capabilities.d.ts +7 -0
  5. package/dist/auth/capabilities.d.ts.map +1 -0
  6. package/dist/auth/capabilities.js +26 -0
  7. package/dist/auth/config.d.ts +9 -9
  8. package/dist/auth/config.d.ts.map +1 -1
  9. package/dist/auth/config.js +5 -5
  10. package/dist/auth/guard.d.ts +1 -1
  11. package/dist/auth/guard.d.ts.map +1 -1
  12. package/dist/auth/guard.js +2 -2
  13. package/dist/auth/index.d.ts +1 -0
  14. package/dist/auth/index.d.ts.map +1 -1
  15. package/dist/auth/index.js +1 -0
  16. package/dist/carta.d.ts +1 -1
  17. package/dist/carta.d.ts.map +1 -1
  18. package/dist/components/AdminLayout.svelte +74 -18
  19. package/dist/components/AdminLayout.svelte.d.ts +9 -0
  20. package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
  21. package/dist/components/CollectionList.svelte +96 -0
  22. package/dist/components/CollectionList.svelte.d.ts +8 -0
  23. package/dist/components/CollectionList.svelte.d.ts.map +1 -0
  24. package/dist/components/ComponentPalette.svelte +34 -0
  25. package/dist/components/ComponentPalette.svelte.d.ts +9 -0
  26. package/dist/components/ComponentPalette.svelte.d.ts.map +1 -0
  27. package/dist/components/ConfirmPage.svelte +2 -2
  28. package/dist/components/EditPage.svelte +69 -31
  29. package/dist/components/EditPage.svelte.d.ts +2 -0
  30. package/dist/components/EditPage.svelte.d.ts.map +1 -1
  31. package/dist/components/LoginPage.svelte +5 -5
  32. package/dist/components/NavTree.svelte +128 -0
  33. package/dist/components/NavTree.svelte.d.ts +8 -0
  34. package/dist/components/NavTree.svelte.d.ts.map +1 -0
  35. package/dist/components/index.d.ts +3 -1
  36. package/dist/components/index.d.ts.map +1 -1
  37. package/dist/components/index.js +3 -1
  38. package/dist/editor.d.ts +25 -0
  39. package/dist/editor.d.ts.map +1 -0
  40. package/dist/editor.js +20 -0
  41. package/dist/email.js +4 -4
  42. package/dist/frontmatter.d.ts +3 -0
  43. package/dist/frontmatter.d.ts.map +1 -0
  44. package/dist/frontmatter.js +16 -0
  45. package/dist/github.d.ts +22 -2
  46. package/dist/github.d.ts.map +1 -1
  47. package/dist/github.js +40 -5
  48. package/dist/index.d.ts +3 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +3 -0
  51. package/dist/nav.d.ts +58 -0
  52. package/dist/nav.d.ts.map +1 -0
  53. package/dist/nav.js +86 -0
  54. package/dist/render/glyph.d.ts +6 -0
  55. package/dist/render/glyph.d.ts.map +1 -0
  56. package/dist/render/glyph.js +5 -0
  57. package/dist/render/index.d.ts +6 -0
  58. package/dist/render/index.d.ts.map +1 -0
  59. package/dist/render/index.js +8 -0
  60. package/dist/render/pipeline.d.ts +16 -0
  61. package/dist/render/pipeline.d.ts.map +1 -0
  62. package/dist/render/pipeline.js +29 -0
  63. package/dist/render/registry.d.ts +28 -0
  64. package/dist/render/registry.d.ts.map +1 -0
  65. package/dist/render/registry.js +11 -0
  66. package/dist/render/rehype-dispatch.d.ts +24 -0
  67. package/dist/render/rehype-dispatch.d.ts.map +1 -0
  68. package/dist/render/rehype-dispatch.js +86 -0
  69. package/dist/render/remark-directives.d.ts +4 -0
  70. package/dist/render/remark-directives.d.ts.map +1 -0
  71. package/dist/render/remark-directives.js +74 -0
  72. package/dist/slug.d.ts +7 -0
  73. package/dist/slug.d.ts.map +1 -0
  74. package/dist/slug.js +15 -0
  75. package/dist/sveltekit/index.d.ts +118 -13
  76. package/dist/sveltekit/index.d.ts.map +1 -1
  77. package/dist/sveltekit/index.js +250 -24
  78. package/dist/utils.d.ts +1 -1
  79. package/dist/utils.d.ts.map +1 -1
  80. package/dist/utils.js +2 -2
  81. package/package.json +20 -3
  82. package/src/lib/adapter.ts +37 -3
  83. package/src/lib/auth/capabilities.ts +35 -0
  84. package/src/lib/auth/config.ts +6 -6
  85. package/src/lib/auth/guard.ts +3 -3
  86. package/src/lib/auth/index.ts +1 -0
  87. package/src/lib/carta.ts +2 -2
  88. package/src/lib/components/AdminLayout.svelte +74 -18
  89. package/src/lib/components/CollectionList.svelte +96 -0
  90. package/src/lib/components/ComponentPalette.svelte +34 -0
  91. package/src/lib/components/ConfirmPage.svelte +2 -2
  92. package/src/lib/components/EditPage.svelte +69 -31
  93. package/src/lib/components/LoginPage.svelte +5 -5
  94. package/src/lib/components/NavTree.svelte +128 -0
  95. package/src/lib/components/index.ts +3 -1
  96. package/src/lib/editor.ts +38 -0
  97. package/src/lib/email.ts +4 -4
  98. package/src/lib/frontmatter.ts +17 -0
  99. package/src/lib/github.ts +38 -6
  100. package/src/lib/index.ts +3 -0
  101. package/src/lib/nav.ts +117 -0
  102. package/src/lib/render/glyph.ts +14 -0
  103. package/src/lib/render/index.ts +8 -0
  104. package/src/lib/render/pipeline.ts +37 -0
  105. package/src/lib/render/registry.ts +36 -0
  106. package/src/lib/render/rehype-dispatch.ts +97 -0
  107. package/src/lib/render/remark-directives.ts +71 -0
  108. package/src/lib/slug.ts +16 -0
  109. package/src/lib/sveltekit/index.ts +355 -37
  110. package/src/lib/utils.ts +2 -2
  111. package/dist/components/AdminList.svelte +0 -33
  112. package/dist/components/AdminList.svelte.d.ts +0 -10
  113. package/dist/components/AdminList.svelte.d.ts.map +0 -1
  114. package/src/lib/components/AdminList.svelte +0 -33
@@ -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,6 +1,6 @@
1
1
  <script lang="ts">
2
- // The scanner-safe confirm surface (C2). A GET renders this static page nothing is consumed.
3
- // The token rides in a hidden field; only the explicit form POST (the route's default action
2
+ // The scanner-safe confirm surface (C2). A GET renders this static page and consumes nothing.
3
+ // The token rides in a hidden field; only the explicit form POST (the route's default action,
4
4
  // confirmSignIn) verifies it. Mail scanners GET URLs but don't submit forms, so prefetch can't
5
5
  // burn the link. JS-free by design.
6
6
  interface Props {
@@ -1,33 +1,55 @@
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.
16
- // svelte-ignore state_referenced_locally seeding from the initial load is intended.
28
+ // svelte-ignore state_referenced_locally (seeding from the initial load is intended)
17
29
  let body = $state(data.body);
18
30
 
19
- // svelte-ignore state_referenced_locally the preview plugin set is fixed for the load.
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
- // 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
- // svelte-ignore state_referenced_locally form defaults from the initial load.
52
+ // svelte-ignore state_referenced_locally (form defaults from the initial load)
31
53
  const fm = data.frontmatter as Record<string, unknown>;
32
54
 
33
55
  function fmString(key: string): string {
@@ -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"}
@@ -1,13 +1,13 @@
1
1
  <script lang="ts">
2
2
  // The magic-link sign-in page. Requests a link via the better-auth client (client-side, same
3
- // origin). To avoid enumeration the UI shows the SAME neutral copy whether or not the email is
4
- // on the allowlist the server only emails actual editors (see auth/config.ts send gate).
3
+ // origin). To avoid enumeration the UI shows the same neutral copy whether or not the email is
4
+ // on the allowlist. The server only emails actual editors (see auth/config.ts send gate).
5
5
  import { createAuthClient } from 'better-auth/svelte';
6
6
  import { magicLinkClient } from 'better-auth/client/plugins';
7
7
 
8
8
  // The browser client lives in the one component that needs it (requesting a link). Sign-out
9
- // and editor management go through server endpoints, so no shared client module is needed
10
- // and a component-local const keeps better-auth's deep client types out of the packaged .d.ts.
9
+ // and editor management go through server endpoints, so no shared client module is needed.
10
+ // A component-local const keeps better-auth's deep client types out of the packaged .d.ts.
11
11
  const authClient = createAuthClient({ plugins: [magicLinkClient()] });
12
12
 
13
13
  interface Props {
@@ -23,7 +23,7 @@
23
23
  event.preventDefault();
24
24
  busy = true;
25
25
  // The magic-link email points at our /admin/auth/confirm page (built in config.ts), not a
26
- // GET-verify URL so the result is the same regardless of allowlist membership.
26
+ // GET-verify URL, so the result is the same regardless of allowlist membership.
27
27
  await authClient.signIn.magicLink({ email });
28
28
  busy = false;
29
29
  requested = true;
@@ -0,0 +1,128 @@
1
+ <script lang="ts">
2
+ // The navigation tree editor (Pass L). Edits a local copy of the menu tree and posts the whole
3
+ // tree as JSON to the `save` action. DaisyUI primitives under the Warm Stone admin theme. Drag a
4
+ // row up or down to reorder within its level; use Indent/Outdent to nest under the previous
5
+ // sibling or promote a level (capped at the menu's maxDepth). The engine validates on save.
6
+ import { untrack } from 'svelte';
7
+ import type { NavLoadData } from '../sveltekit';
8
+ import type { NavNode } from '../nav';
9
+
10
+ let { data }: { data: NavLoadData } = $props();
11
+
12
+ // A flat, ordered working model is far simpler to drag-edit than a recursive one: each row
13
+ // carries an explicit depth, and the tree is rebuilt from (order + depth) only at submit time.
14
+ interface Row {
15
+ id: number;
16
+ depth: number;
17
+ label: string;
18
+ url: string;
19
+ }
20
+
21
+ let nextId = 1;
22
+ function flatten(nodes: NavNode[], depth: number, out: Row[]): Row[] {
23
+ for (const n of nodes) {
24
+ out.push({ id: nextId++, depth, label: n.label, url: n.url ?? '' });
25
+ if (n.children?.length) flatten(n.children, depth + 1, out);
26
+ }
27
+ return out;
28
+ }
29
+
30
+ let rows = $state<Row[]>(untrack(() => flatten(data.tree, 0, [])));
31
+ const maxDepthIndex = $derived(data.menu.maxDepth - 1); // depth is 0-based here
32
+
33
+ // Rebuild the nested tree from the flat rows by depth, then serialize for the hidden field.
34
+ function toTree(list: Row[]): NavNode[] {
35
+ const root: NavNode[] = [];
36
+ const stack: { depth: number; node: NavNode }[] = [];
37
+ for (const r of list) {
38
+ const node: NavNode = { label: r.label.trim() };
39
+ if (r.url.trim()) node.url = r.url.trim();
40
+ while (stack.length && stack[stack.length - 1].depth >= r.depth) stack.pop();
41
+ if (stack.length) (stack[stack.length - 1].node.children ??= []).push(node);
42
+ else root.push(node);
43
+ stack.push({ depth: r.depth, node });
44
+ }
45
+ return root;
46
+ }
47
+
48
+ const treeJson = $derived(JSON.stringify(toTree(rows)));
49
+
50
+ function addRow() {
51
+ rows = [...rows, { id: nextId++, depth: 0, label: 'New item', url: '' }];
52
+ }
53
+ function removeRow(id: number) {
54
+ rows = rows.filter((r) => r.id !== id);
55
+ }
56
+ function indent(i: number) {
57
+ // A row may nest at most one level deeper than the row above it, and never past the cap.
58
+ if (i === 0) return;
59
+ const ceiling = Math.min(rows[i - 1].depth + 1, maxDepthIndex);
60
+ if (rows[i].depth < ceiling) rows[i].depth += 1;
61
+ }
62
+ function outdent(i: number) {
63
+ if (rows[i].depth > 0) rows[i].depth -= 1;
64
+ }
65
+
66
+ let dragFrom = $state<number | null>(null);
67
+ function onDrop(to: number) {
68
+ if (dragFrom === null || dragFrom === to) return;
69
+ const next = [...rows];
70
+ const [moved] = next.splice(dragFrom, 1);
71
+ next.splice(to, 0, moved);
72
+ rows = next;
73
+ dragFrom = null;
74
+ }
75
+ </script>
76
+
77
+ <div class="cairn-admin">
78
+ <div class="flex items-center justify-between">
79
+ <h1 class="text-xl font-semibold">{data.menu.label}</h1>
80
+ <button type="button" class="btn btn-sm" onclick={addRow}>Add item</button>
81
+ </div>
82
+
83
+ {#if data.saved}
84
+ <div class="alert alert-success mt-3">Navigation saved.</div>
85
+ {/if}
86
+ {#if data.error}
87
+ <div class="alert alert-error mt-3">{data.error}</div>
88
+ {/if}
89
+
90
+ <form method="POST" action="?/save" class="mt-4">
91
+ <input type="hidden" name="tree" value={treeJson} />
92
+ <ul class="menu w-full gap-1">
93
+ {#each rows as row, i (row.id)}
94
+ <li
95
+ draggable="true"
96
+ ondragstart={() => (dragFrom = i)}
97
+ ondragover={(e) => e.preventDefault()}
98
+ ondrop={() => onDrop(i)}
99
+ style={`margin-left:${row.depth * 1.5}rem`}
100
+ >
101
+ <div class="flex items-center gap-2 p-2">
102
+ <span class="cursor-grab opacity-40" aria-hidden="true">&#x283F;</span>
103
+ <input class="input input-sm input-bordered flex-1" placeholder="Label" bind:value={row.label} />
104
+ <input
105
+ class="input input-sm input-bordered flex-1"
106
+ placeholder="/path or https://…"
107
+ list="cairn-nav-pages"
108
+ bind:value={row.url}
109
+ />
110
+ <button type="button" class="btn btn-xs btn-ghost" onclick={() => outdent(i)} aria-label="Outdent">&larr;</button>
111
+ <button type="button" class="btn btn-xs btn-ghost" onclick={() => indent(i)} aria-label="Indent">&rarr;</button>
112
+ <button type="button" class="btn btn-xs btn-ghost text-error" onclick={() => removeRow(row.id)} aria-label="Remove">&times;</button>
113
+ </div>
114
+ </li>
115
+ {/each}
116
+ </ul>
117
+
118
+ <datalist id="cairn-nav-pages">
119
+ {#each data.pages as p (p.url)}
120
+ <option value={p.url}>{p.label}</option>
121
+ {/each}
122
+ </datalist>
123
+
124
+ <div class="mt-4">
125
+ <button type="submit" class="btn btn-primary btn-sm">Save navigation</button>
126
+ </div>
127
+ </form>
128
+ </div>
@@ -0,0 +1,8 @@
1
+ import type { NavLoadData } from '../sveltekit';
2
+ type $$ComponentProps = {
3
+ data: NavLoadData;
4
+ };
5
+ declare const NavTree: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type NavTree = ReturnType<typeof NavTree>;
7
+ export default NavTree;
8
+ //# sourceMappingURL=NavTree.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"NavTree.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/NavTree.svelte.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAY/C,KAAK,gBAAgB,GAAI;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,CAAC;AAiHhD,QAAA,MAAM,OAAO,sDAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
@@ -1,7 +1,9 @@
1
1
  export { default as AdminLayout } from './AdminLayout.svelte';
2
- export { default as AdminList } from './AdminList.svelte';
2
+ export { default as CollectionList } from './CollectionList.svelte';
3
3
  export { default as LoginPage } from './LoginPage.svelte';
4
4
  export { default as ConfirmPage } from './ConfirmPage.svelte';
5
5
  export { default as EditPage } from './EditPage.svelte';
6
6
  export { default as ManageAdmins } from './ManageAdmins.svelte';
7
+ export { default as ComponentPalette } from './ComponentPalette.svelte';
8
+ export { default as NavTree } from './NavTree.svelte';
7
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/components/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/components/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAC1D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC"}
@@ -1,8 +1,10 @@
1
1
  // cairn-cms admin UI shell. Consumers import from 'cairn-cms/components'; each site's
2
2
  // admin route `.svelte` files are one-line shims around these.
3
3
  export { default as AdminLayout } from './AdminLayout.svelte';
4
- export { default as AdminList } from './AdminList.svelte';
4
+ export { default as CollectionList } from './CollectionList.svelte';
5
5
  export { default as LoginPage } from './LoginPage.svelte';
6
6
  export { default as ConfirmPage } from './ConfirmPage.svelte';
7
7
  export { default as EditPage } from './EditPage.svelte';
8
8
  export { default as ManageAdmins } from './ManageAdmins.svelte';
9
+ export { default as ComponentPalette } from './ComponentPalette.svelte';
10
+ export { default as NavTree } from './NavTree.svelte';
@@ -0,0 +1,25 @@
1
+ interface CartaInput {
2
+ getSelection(): {
3
+ start: number;
4
+ end: number;
5
+ direction: string;
6
+ slice: string;
7
+ };
8
+ insertAt(position: number, text: string): void;
9
+ }
10
+ interface CartaLike {
11
+ input?: CartaInput;
12
+ }
13
+ /** The programmatic editing surface the admin relies on. */
14
+ export interface MarkdownEditor {
15
+ /** Insert a component or template at the current cursor position. */
16
+ insertComponent(template: string): void;
17
+ }
18
+ /**
19
+ * Wrap a Carta instance as a MarkdownEditor. Takes a getter (not the instance) because the
20
+ * EditPage component creates the Carta instance once and `carta.input` is only populated after
21
+ * the editor mounts; reading it lazily at call time avoids capturing an undefined `input`.
22
+ */
23
+ export declare function cartaEditor(getCarta: () => CartaLike): MarkdownEditor;
24
+ export {};
25
+ //# sourceMappingURL=editor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"editor.d.ts","sourceRoot":"","sources":["../src/lib/editor.ts"],"names":[],"mappings":"AAQA,UAAU,UAAU;IAClB,YAAY,IAAI;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,GAAG,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACjF,QAAQ,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;CAChD;AAED,UAAU,SAAS;IACjB,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED,4DAA4D;AAC5D,MAAM,WAAW,cAAc;IAC7B,qEAAqE;IACrE,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzC;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,SAAS,GAAG,cAAc,CASrE"}
package/dist/editor.js ADDED
@@ -0,0 +1,20 @@
1
+ // cairn-core: the editor cursor seam (decision P3). The component palette and any later insert
2
+ // control talk to MarkdownEditor, never to Carta directly, so a swap to a different editing
3
+ // engine is contained to this file. Verified against carta-md@4.11: `input.getSelection()` and
4
+ // `input.insertAt(pos, text)` are public on the InputEnhancer.
5
+ /**
6
+ * Wrap a Carta instance as a MarkdownEditor. Takes a getter (not the instance) because the
7
+ * EditPage component creates the Carta instance once and `carta.input` is only populated after
8
+ * the editor mounts; reading it lazily at call time avoids capturing an undefined `input`.
9
+ */
10
+ export function cartaEditor(getCarta) {
11
+ return {
12
+ insertComponent(template) {
13
+ const input = getCarta().input;
14
+ if (!input)
15
+ return; // editor not mounted yet; nothing to insert into
16
+ const { start } = input.getSelection();
17
+ input.insertAt(start, template);
18
+ },
19
+ };
20
+ }
package/dist/email.js CHANGED
@@ -1,9 +1,9 @@
1
1
  // cairn-core: pluggable magic-link email sender.
2
2
  //
3
- // Default adapter is Cloudflare Email Service Email Sending (transactional, arbitrary
4
- // recipients) distinct from Email Routing's recipient-restricted `EmailMessage` flow.
5
- // It is reached through the same `send_email` binding (configured without a
6
- // destination_address) but a different call shape: `binding.send({ to, from, ... })`.
3
+ // The default adapter targets Cloudflare Email Service (Email Sending, transactional,
4
+ // arbitrary recipients), distinct from Email Routing's recipient-restricted `EmailMessage`
5
+ // flow. Both share the same `send_email` binding (configured without a destination_address)
6
+ // but use a different call shape: `binding.send({ to, from, ... })`.
7
7
  // Resend can slot in behind the same `sendMagicLink` signature if needed.
8
8
  export async function sendMagicLink(sender, to, link, siteName, from) {
9
9
  const expiry = "This link expires in 10 minutes and works only once. If you didn't request it, ignore this email.";
@@ -0,0 +1,3 @@
1
+ /** A frontmatter date value (Date or string) to the `YYYY-MM-DD` an `<input type="date">` expects. */
2
+ export declare function dateInputValue(value: unknown): string;
3
+ //# sourceMappingURL=frontmatter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../src/lib/frontmatter.ts"],"names":[],"mappings":"AAMA,sGAAsG;AACtG,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CASrD"}
@@ -0,0 +1,16 @@
1
+ // cairn-core: coerce a frontmatter value to the YYYY-MM-DD string an <input type="date"> wants.
2
+ // gray-matter parses an unquoted YAML date (date: 2026-05-14) into a JS Date, so a string-only
3
+ // read leaves the date input empty and drops the date on save. This normalizes a Date or an
4
+ // ISO-ish string to YYYY-MM-DD. A parsed YAML date is UTC midnight, so slicing the ISO string
5
+ // avoids a local-timezone shift. Internal (not re-exported from the barrel), like utils.ts.
6
+ /** A frontmatter date value (Date or string) to the `YYYY-MM-DD` an `<input type="date">` expects. */
7
+ export function dateInputValue(value) {
8
+ if (value instanceof Date) {
9
+ return Number.isNaN(value.getTime()) ? '' : value.toISOString().slice(0, 10);
10
+ }
11
+ if (typeof value === 'string') {
12
+ const match = value.match(/^\d{4}-\d{2}-\d{2}/);
13
+ return match ? match[0] : '';
14
+ }
15
+ return '';
16
+ }
package/dist/github.d.ts CHANGED
@@ -27,7 +27,7 @@ export declare function appJwt(appId: string, privateKeyPem: string): Promise<st
27
27
  export interface AppCredentials {
28
28
  appId: string;
29
29
  installationId: string;
30
- /** The stored GITHUB_APP_PRIVATE_KEY_B64 base64 of the PEM, single line. */
30
+ /** The stored GITHUB_APP_PRIVATE_KEY_B64: base64 of the PEM, single line. */
31
31
  privateKeyB64: string;
32
32
  }
33
33
  /** Exchange the App JWT for a short-lived installation access token. */
@@ -38,15 +38,35 @@ export interface CommitAuthor {
38
38
  name: string;
39
39
  email: string;
40
40
  }
41
+ /**
42
+ * A concurrent edit lost the SHA race (C3): the file changed between the read and the PUT,
43
+ * from another editor or the site's own CI. Thrown so callers can fail safe (re-fetch and ask
44
+ * the editor to reapply) instead of surfacing a raw 409. Defined and caught inside the package
45
+ * so `instanceof` is reliable (no peer-boundary identity split, unlike kit's `redirect`/`error`).
46
+ */
47
+ export declare class CommitConflictError extends Error {
48
+ readonly path: string;
49
+ constructor(path: string);
50
+ }
41
51
  /**
42
52
  * Commit `content` to `path` on the configured branch via the contents API. Author is the
43
53
  * editor; committer is omitted so GitHub attributes it to the App (cairn-cms[bot]). Updates
44
54
  * the file in place when it exists (passing its sha), creates it otherwise. Returns the
45
- * commit sha.
55
+ * commit sha. A stale-sha 409 (someone committed in between) becomes a `CommitConflictError`.
46
56
  */
47
57
  export declare function commitFile(repo: RepoRef, path: string, content: string, opts: {
48
58
  message: string;
49
59
  author: CommitAuthor;
50
60
  }, token: string): Promise<string>;
61
+ /**
62
+ * Deploy-time self-test for the GitHub App signer (M2): sign a dummy JWT with the configured
63
+ * private key. Exercises the brittle PKCS#1→PKCS#8 conversion + Web Crypto import/sign without
64
+ * any network call or secret in the result, so `/admin/healthz` catches a bad/rotated key
65
+ * before an editor's save fails. Returns `{ ok: false, detail }` rather than throwing.
66
+ */
67
+ export declare function signingSelfTest(appId: string, privateKeyB64: string): Promise<{
68
+ ok: boolean;
69
+ detail?: string;
70
+ }>;
51
71
  export {};
52
72
  //# sourceMappingURL=github.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../src/lib/github.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAcD,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,QAAQ,EAAE,CAKlE;AAED,yDAAyD;AACzD,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAIlG;AAED,iEAAiE;AACjE,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKjG;AAqCD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,8EAA8E;IAC9E,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ9E;AAOD,+EAA+E;AAC/E,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKhG;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAgBjB"}
1
+ {"version":3,"file":"github.d.ts","sourceRoot":"","sources":["../src/lib/github.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,MAAM,WAAW,QAAQ;IACvB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAcD,mFAAmF;AACnF,wBAAgB,WAAW,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAG/D;AAED,UAAU,aAAa;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAED,uFAAuF;AACvF,wBAAgB,aAAa,CAAC,OAAO,EAAE,aAAa,EAAE,GAAG,QAAQ,EAAE,CAKlE;AAED,yDAAyD;AACzD,wBAAsB,YAAY,CAAC,IAAI,EAAE,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,EAAE,CAAC,CAIlG;AAED,iEAAiE;AACjE,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKjG;AAqCD,wFAAwF;AACxF,wBAAsB,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAclF;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,6EAA6E;IAC7E,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,wEAAwE;AACxE,wBAAsB,iBAAiB,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,CAAC,CAQ9E;AAOD,+EAA+E;AAC/E,wBAAsB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAKhG;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;;;;GAKG;AACH,qBAAa,mBAAoB,SAAQ,KAAK;aAChB,IAAI,EAAE,MAAM;gBAAZ,IAAI,EAAE,MAAM;CAIzC;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,OAAO,EACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,EACf,IAAI,EAAE;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,YAAY,CAAA;CAAE,EAC/C,KAAK,EAAE,MAAM,GACZ,OAAO,CAAC,MAAM,CAAC,CAmBjB;AAED;;;;;GAKG;AACH,wBAAsB,eAAe,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAQrH"}