@glw907/cairn-cms 0.5.0 → 0.6.0-rc.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.
- package/dist/auth/crypto.d.ts +13 -0
- package/dist/auth/crypto.d.ts.map +1 -0
- package/dist/auth/crypto.js +31 -0
- package/dist/auth/store.d.ts +41 -0
- package/dist/auth/store.d.ts.map +1 -0
- package/dist/auth/store.js +115 -0
- package/dist/auth/types.d.ts +25 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +1 -0
- package/dist/components/AdminLayout.svelte +58 -108
- package/dist/components/AdminLayout.svelte.d.ts +14 -9
- package/dist/components/AdminLayout.svelte.d.ts.map +1 -1
- package/dist/components/ComponentPalette.svelte +50 -0
- package/dist/components/ComponentPalette.svelte.d.ts +16 -0
- package/dist/components/ComponentPalette.svelte.d.ts.map +1 -0
- package/dist/components/ConceptList.svelte +81 -0
- package/dist/components/ConceptList.svelte.d.ts +13 -0
- package/dist/components/ConceptList.svelte.d.ts.map +1 -0
- package/dist/components/ConfirmPage.svelte +23 -20
- package/dist/components/ConfirmPage.svelte.d.ts +6 -0
- package/dist/components/ConfirmPage.svelte.d.ts.map +1 -1
- package/dist/components/EditPage.svelte +160 -103
- package/dist/components/EditPage.svelte.d.ts +17 -7
- package/dist/components/EditPage.svelte.d.ts.map +1 -1
- package/dist/components/LoginPage.svelte +42 -52
- package/dist/components/LoginPage.svelte.d.ts +12 -0
- package/dist/components/LoginPage.svelte.d.ts.map +1 -1
- package/dist/components/ManageEditors.svelte +81 -0
- package/dist/components/ManageEditors.svelte.d.ts +24 -0
- package/dist/components/ManageEditors.svelte.d.ts.map +1 -0
- package/dist/components/MarkdownEditor.svelte +81 -0
- package/dist/components/MarkdownEditor.svelte.d.ts +20 -0
- package/dist/components/MarkdownEditor.svelte.d.ts.map +1 -0
- package/dist/components/NavTree.svelte +138 -0
- package/dist/components/NavTree.svelte.d.ts +17 -0
- package/dist/components/NavTree.svelte.d.ts.map +1 -0
- package/dist/components/cairn-admin.css +42 -0
- package/dist/components/index.d.ts +5 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +7 -4
- package/dist/content/compose.d.ts +7 -0
- package/dist/content/compose.d.ts.map +1 -0
- package/dist/content/compose.js +32 -0
- package/dist/content/concepts.d.ts +17 -0
- package/dist/content/concepts.d.ts.map +1 -0
- package/dist/content/concepts.js +41 -0
- package/dist/content/frontmatter.d.ts +18 -0
- package/dist/content/frontmatter.d.ts.map +1 -0
- package/dist/content/frontmatter.js +58 -0
- package/dist/content/ids.d.ts +17 -0
- package/dist/content/ids.d.ts.map +1 -0
- package/dist/content/ids.js +33 -0
- package/dist/content/types.d.ts +210 -0
- package/dist/content/types.d.ts.map +1 -0
- package/dist/content/types.js +1 -0
- package/dist/content/validate.d.ts +13 -0
- package/dist/content/validate.d.ts.map +1 -0
- package/dist/content/validate.js +45 -0
- package/dist/email.d.ts +25 -12
- package/dist/email.d.ts.map +1 -1
- package/dist/email.js +24 -24
- package/dist/env.d.ts +24 -0
- package/dist/env.d.ts.map +1 -0
- package/dist/env.js +29 -0
- package/dist/github/credentials.d.ts +12 -0
- package/dist/github/credentials.d.ts.map +1 -0
- package/dist/github/credentials.js +11 -0
- package/dist/github/repo.d.ts +49 -0
- package/dist/github/repo.d.ts.map +1 -0
- package/dist/github/repo.js +123 -0
- package/dist/github/signing.d.ts +17 -0
- package/dist/github/signing.d.ts.map +1 -0
- package/dist/github/signing.js +79 -0
- package/dist/github/types.d.ts +35 -0
- package/dist/github/types.d.ts.map +1 -0
- package/dist/github/types.js +19 -0
- package/dist/index.d.ts +27 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +21 -8
- package/dist/nav/site-config.d.ts +50 -0
- package/dist/nav/site-config.d.ts.map +1 -0
- package/dist/nav/site-config.js +100 -0
- package/dist/render/glyph.d.ts +1 -1
- package/dist/render/glyph.d.ts.map +1 -1
- package/dist/render/index.d.ts +5 -5
- package/dist/render/index.d.ts.map +1 -1
- package/dist/render/index.js +6 -6
- package/dist/render/pipeline.d.ts +3 -3
- package/dist/render/pipeline.d.ts.map +1 -1
- package/dist/render/pipeline.js +4 -4
- package/dist/render/registry.d.ts +6 -4
- package/dist/render/registry.d.ts.map +1 -1
- package/dist/render/registry.js +8 -6
- package/dist/render/rehype-dispatch.d.ts +1 -1
- package/dist/render/rehype-dispatch.d.ts.map +1 -1
- package/dist/render/remark-directives.d.ts +1 -1
- package/dist/render/remark-directives.d.ts.map +1 -1
- package/dist/render/sanitize.d.ts +8 -0
- package/dist/render/sanitize.d.ts.map +1 -0
- package/dist/render/sanitize.js +26 -0
- package/dist/sveltekit/auth-routes.d.ts +23 -0
- package/dist/sveltekit/auth-routes.d.ts.map +1 -0
- package/dist/sveltekit/auth-routes.js +85 -0
- package/dist/sveltekit/content-routes.d.ts +80 -0
- package/dist/sveltekit/content-routes.d.ts.map +1 -0
- package/dist/sveltekit/content-routes.js +183 -0
- package/dist/sveltekit/editors-routes.d.ts +24 -0
- package/dist/sveltekit/editors-routes.d.ts.map +1 -0
- package/dist/sveltekit/editors-routes.js +73 -0
- package/dist/sveltekit/guard.d.ts +9 -0
- package/dist/sveltekit/guard.d.ts.map +1 -0
- package/dist/sveltekit/guard.js +43 -0
- package/dist/sveltekit/health.d.ts +19 -0
- package/dist/sveltekit/health.d.ts.map +1 -0
- package/dist/sveltekit/health.js +12 -0
- package/dist/sveltekit/index.d.ts +9 -83
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +8 -149
- package/dist/sveltekit/nav-routes.d.ts +30 -0
- package/dist/sveltekit/nav-routes.d.ts.map +1 -0
- package/dist/sveltekit/nav-routes.js +103 -0
- package/dist/sveltekit/types.d.ts +32 -0
- package/dist/sveltekit/types.d.ts.map +1 -0
- package/dist/sveltekit/types.js +1 -0
- package/package.json +38 -58
- package/src/lib/auth/crypto.ts +37 -0
- package/src/lib/auth/store.ts +158 -0
- package/src/lib/auth/types.ts +27 -0
- package/src/lib/components/AdminLayout.svelte +58 -108
- package/src/lib/components/ComponentPalette.svelte +50 -0
- package/src/lib/components/ConceptList.svelte +81 -0
- package/src/lib/components/ConfirmPage.svelte +23 -20
- package/src/lib/components/EditPage.svelte +160 -103
- package/src/lib/components/LoginPage.svelte +42 -52
- package/src/lib/components/ManageEditors.svelte +81 -0
- package/src/lib/components/MarkdownEditor.svelte +81 -0
- package/src/lib/components/NavTree.svelte +138 -0
- package/src/lib/components/cairn-admin.css +42 -0
- package/src/lib/components/index.ts +7 -4
- package/src/lib/content/compose.ts +39 -0
- package/src/lib/content/concepts.ts +57 -0
- package/src/lib/content/frontmatter.ts +71 -0
- package/src/lib/content/ids.ts +38 -0
- package/src/lib/content/types.ts +235 -0
- package/src/lib/content/validate.ts +51 -0
- package/src/lib/email.ts +52 -38
- package/src/lib/env.ts +32 -0
- package/src/lib/github/credentials.ts +27 -0
- package/src/lib/github/repo.ts +138 -0
- package/src/lib/github/signing.ts +97 -0
- package/src/lib/github/types.ts +46 -0
- package/src/lib/index.ts +86 -8
- package/src/lib/nav/site-config.ts +124 -0
- package/src/lib/render/glyph.ts +6 -6
- package/src/lib/render/index.ts +6 -6
- package/src/lib/render/pipeline.ts +22 -22
- package/src/lib/render/registry.ts +33 -26
- package/src/lib/render/rehype-dispatch.ts +47 -47
- package/src/lib/render/remark-directives.ts +46 -46
- package/src/lib/render/sanitize.ts +27 -0
- package/src/lib/sveltekit/auth-routes.ts +107 -0
- package/src/lib/sveltekit/content-routes.ts +261 -0
- package/src/lib/sveltekit/editors-routes.ts +82 -0
- package/src/lib/sveltekit/guard.ts +47 -0
- package/src/lib/sveltekit/health.ts +24 -0
- package/src/lib/sveltekit/index.ts +19 -235
- package/src/lib/sveltekit/nav-routes.ts +139 -0
- package/src/lib/sveltekit/types.ts +33 -0
- package/dist/adapter.d.ts +0 -69
- package/dist/adapter.d.ts.map +0 -1
- package/dist/adapter.js +0 -30
- package/dist/auth/admins.d.ts +0 -33
- package/dist/auth/admins.d.ts.map +0 -1
- package/dist/auth/admins.js +0 -90
- package/dist/auth/config.d.ts +0 -2097
- package/dist/auth/config.d.ts.map +0 -1
- package/dist/auth/config.js +0 -78
- package/dist/auth/guard.d.ts +0 -34
- package/dist/auth/guard.d.ts.map +0 -1
- package/dist/auth/guard.js +0 -47
- package/dist/auth/index.d.ts +0 -4
- package/dist/auth/index.d.ts.map +0 -1
- package/dist/auth/index.js +0 -6
- package/dist/auth/schema.d.ts +0 -750
- package/dist/auth/schema.d.ts.map +0 -1
- package/dist/auth/schema.js +0 -93
- package/dist/carta.d.ts +0 -39
- package/dist/carta.d.ts.map +0 -1
- package/dist/carta.js +0 -30
- package/dist/components/AdminList.svelte +0 -33
- package/dist/components/AdminList.svelte.d.ts +0 -10
- package/dist/components/AdminList.svelte.d.ts.map +0 -1
- package/dist/components/ManageAdmins.svelte +0 -84
- package/dist/components/ManageAdmins.svelte.d.ts +0 -10
- package/dist/components/ManageAdmins.svelte.d.ts.map +0 -1
- package/dist/content.d.ts +0 -3
- package/dist/content.d.ts.map +0 -1
- package/dist/content.js +0 -10
- package/dist/github.d.ts +0 -72
- package/dist/github.d.ts.map +0 -1
- package/dist/github.js +0 -171
- package/dist/utils.d.ts +0 -3
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -11
- package/src/lib/adapter.ts +0 -119
- package/src/lib/auth/admins.ts +0 -106
- package/src/lib/auth/config.ts +0 -108
- package/src/lib/auth/guard.ts +0 -60
- package/src/lib/auth/index.ts +0 -6
- package/src/lib/auth/schema.ts +0 -112
- package/src/lib/carta.ts +0 -59
- package/src/lib/components/AdminList.svelte +0 -33
- package/src/lib/components/ManageAdmins.svelte +0 -84
- package/src/lib/content.ts +0 -11
- package/src/lib/github.ts +0 -220
- package/src/lib/utils.ts +0 -12
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Editor } from '../auth/types.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The editors load's data, plus the site name. */
|
|
4
|
+
data: {
|
|
5
|
+
editors: Editor[];
|
|
6
|
+
self: string;
|
|
7
|
+
siteName: string;
|
|
8
|
+
};
|
|
9
|
+
/** The last action's result (an error message when it failed). */
|
|
10
|
+
form: {
|
|
11
|
+
error?: string;
|
|
12
|
+
ok?: boolean;
|
|
13
|
+
} | null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The owner-gated editor management surface: a table of editors with role-flip and remove actions,
|
|
17
|
+
* and an add-editor form. The acting owner's own row disables its destructive controls; the
|
|
18
|
+
* last-owner anti-lockout rule itself is enforced server-side (editors-routes). Actions post to the
|
|
19
|
+
* named `?/setRole`, `?/remove`, and `?/add` actions.
|
|
20
|
+
*/
|
|
21
|
+
declare const ManageEditors: import("svelte").Component<Props, {}, "">;
|
|
22
|
+
type ManageEditors = ReturnType<typeof ManageEditors>;
|
|
23
|
+
export default ManageEditors;
|
|
24
|
+
//# sourceMappingURL=ManageEditors.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ManageEditors.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/ManageEditors.svelte.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG7C,UAAU,KAAK;IACb,mDAAmD;IACnD,IAAI,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5D,kEAAkE;IAClE,IAAI,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/C;AAyEH;;;;;GAKG;AACH,QAAA,MAAM,aAAa,2CAAwC,CAAC;AAC5D,KAAK,aAAa,GAAG,UAAU,CAAC,OAAO,aAAa,CAAC,CAAC;AACtD,eAAe,aAAa,CAAC"}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over Carta exposing a bindable value
|
|
4
|
+
and a cursor-insert callback. Carta and Shiki are client-only, so the editor mounts after the
|
|
5
|
+
component does; until then the hidden field still carries the value so the form submits correctly.
|
|
6
|
+
Swapping Carta for a bare CodeMirror editor stays a one-file change.
|
|
7
|
+
-->
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { onMount } from 'svelte';
|
|
10
|
+
|
|
11
|
+
interface Props {
|
|
12
|
+
/** The markdown source; bindable so the parent reads edits back. */
|
|
13
|
+
value: string;
|
|
14
|
+
/** The hidden field name the value is mirrored to for form submit. */
|
|
15
|
+
name: string;
|
|
16
|
+
/** Carta extensions from the adapter, for the design-accurate preview. */
|
|
17
|
+
plugins?: unknown[];
|
|
18
|
+
/** Receives a `(text) => void` that inserts at the cursor; the palette calls it. */
|
|
19
|
+
registerInsert?: (insert: (text: string) => void) => void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { value = $bindable(), name, plugins = [], registerInsert }: Props = $props();
|
|
23
|
+
|
|
24
|
+
// Local structural type for the Carta editing surface this seam uses. carta-md re-exports its
|
|
25
|
+
// Svelte components from the package entry, so its `Carta` class is not reachable as a named
|
|
26
|
+
// export under NodeNext; a structural type stays compatible without naming it (the shape
|
|
27
|
+
// legacy/src/lib/editor.ts relied on, verified against carta-md@4.11).
|
|
28
|
+
interface CartaInput {
|
|
29
|
+
getSelection(): { start: number };
|
|
30
|
+
insertAt(position: number, text: string): void;
|
|
31
|
+
update(): boolean;
|
|
32
|
+
}
|
|
33
|
+
interface CartaLike {
|
|
34
|
+
input?: CartaInput;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let mounted = $state(false);
|
|
38
|
+
// Carta and the MarkdownEditor component load only in the browser, after mount, so the server
|
|
39
|
+
// bundle never pulls in Carta or Shiki (guarded by the carta-boundary test). The component keeps
|
|
40
|
+
// its real type, so `value` stays bindable; the Carta constructor is reached through a cast
|
|
41
|
+
// because the package entry does not surface the class by name.
|
|
42
|
+
let Editor = $state<(typeof import('carta-md'))['MarkdownEditor'] | null>(null);
|
|
43
|
+
let carta = $state<CartaLike | null>(null);
|
|
44
|
+
|
|
45
|
+
onMount(async () => {
|
|
46
|
+
const mod = await import('carta-md');
|
|
47
|
+
const CartaCtor = (
|
|
48
|
+
mod as unknown as { Carta: new (options: { extensions?: unknown[]; sanitizer: false }) => CartaLike }
|
|
49
|
+
).Carta;
|
|
50
|
+
const instance = new CartaCtor({
|
|
51
|
+
extensions: plugins,
|
|
52
|
+
// Sanitization is the site adapter's concern; the seam passes raw markdown through.
|
|
53
|
+
sanitizer: false,
|
|
54
|
+
});
|
|
55
|
+
carta = instance;
|
|
56
|
+
Editor = mod.MarkdownEditor;
|
|
57
|
+
// Insert at the current cursor through carta.input once the editor is mounted; fall back to
|
|
58
|
+
// appending while input is not yet populated (the pre-mount textarea phase).
|
|
59
|
+
registerInsert?.((text: string) => {
|
|
60
|
+
const inp = instance.input;
|
|
61
|
+
if (inp) {
|
|
62
|
+
const pos = inp.getSelection().start;
|
|
63
|
+
const prefix = pos > 0 ? '\n\n' : '';
|
|
64
|
+
inp.insertAt(pos, `${prefix}${text}`);
|
|
65
|
+
inp.update();
|
|
66
|
+
} else {
|
|
67
|
+
value = value ? `${value}\n\n${text}` : text;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
mounted = true;
|
|
71
|
+
});
|
|
72
|
+
</script>
|
|
73
|
+
|
|
74
|
+
<input type="hidden" {name} value={value} />
|
|
75
|
+
|
|
76
|
+
{#if mounted && Editor && carta}
|
|
77
|
+
{@const EditorComponent = Editor}
|
|
78
|
+
<EditorComponent carta={carta as never} bind:value theme="default" mode="tabs" />
|
|
79
|
+
{:else}
|
|
80
|
+
<textarea class="textarea min-h-64 w-full font-mono text-sm" bind:value aria-label="Markdown source"></textarea>
|
|
81
|
+
{/if}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
/** The markdown source; bindable so the parent reads edits back. */
|
|
3
|
+
value: string;
|
|
4
|
+
/** The hidden field name the value is mirrored to for form submit. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Carta extensions from the adapter, for the design-accurate preview. */
|
|
7
|
+
plugins?: unknown[];
|
|
8
|
+
/** Receives a `(text) => void` that inserts at the cursor; the palette calls it. */
|
|
9
|
+
registerInsert?: (insert: (text: string) => void) => void;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over Carta exposing a bindable value
|
|
13
|
+
* and a cursor-insert callback. Carta and Shiki are client-only, so the editor mounts after the
|
|
14
|
+
* component does; until then the hidden field still carries the value so the form submits correctly.
|
|
15
|
+
* Swapping Carta for a bare CodeMirror editor stays a one-file change.
|
|
16
|
+
*/
|
|
17
|
+
declare const MarkdownEditor: import("svelte").Component<Props, {}, "value">;
|
|
18
|
+
type MarkdownEditor = ReturnType<typeof MarkdownEditor>;
|
|
19
|
+
export default MarkdownEditor;
|
|
20
|
+
//# sourceMappingURL=MarkdownEditor.svelte.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MarkdownEditor.svelte.d.ts","sourceRoot":"","sources":["../../src/lib/components/MarkdownEditor.svelte.ts"],"names":[],"mappings":"AAME,UAAU,KAAK;IACb,oEAAoE;IACpE,KAAK,EAAE,MAAM,CAAC;IACd,sEAAsE;IACtE,IAAI,EAAE,MAAM,CAAC;IACb,0EAA0E;IAC1E,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC;IACpB,oFAAoF;IACpF,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,KAAK,IAAI,CAAC;CAC3D;AAuEH;;;;;GAKG;AACH,QAAA,MAAM,cAAc,gDAAwC,CAAC;AAC7D,KAAK,cAAc,GAAG,UAAU,CAAC,OAAO,cAAc,CAAC,CAAC;AACxD,eAAe,cAAc,CAAC"}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component
|
|
3
|
+
The navigation tree editor. It edits a flat working copy of the menu (each row carries an
|
|
4
|
+
explicit depth) and posts the whole tree as JSON to the save action. Vertical order comes from
|
|
5
|
+
svelte-sortable-list (mouse, and keyboard with Space to lift, arrows to move, Space to drop);
|
|
6
|
+
depth comes from the Indent and Outdent buttons, capped at the menu's maxDepth. The engine
|
|
7
|
+
validates on save.
|
|
8
|
+
-->
|
|
9
|
+
<script lang="ts">
|
|
10
|
+
import { untrack } from 'svelte';
|
|
11
|
+
import { SortableList, sortItems } from '@rodrigodagostino/svelte-sortable-list';
|
|
12
|
+
import type { SortableList as SortableListNS } from '@rodrigodagostino/svelte-sortable-list';
|
|
13
|
+
import '@rodrigodagostino/svelte-sortable-list/styles.css';
|
|
14
|
+
import type { NavLoadData } from '../sveltekit/nav-routes.js';
|
|
15
|
+
import type { NavNode } from '../nav/site-config.js';
|
|
16
|
+
|
|
17
|
+
interface Props {
|
|
18
|
+
/** The nav load's data: the menu meta, the current tree, page options, and flags. */
|
|
19
|
+
data: NavLoadData;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let { data }: Props = $props();
|
|
23
|
+
|
|
24
|
+
// A flat, ordered working model is simpler to reorder than a recursive one: each row carries an
|
|
25
|
+
// explicit depth, and the nested tree is rebuilt from order plus depth only at submit time.
|
|
26
|
+
interface Row {
|
|
27
|
+
id: string;
|
|
28
|
+
depth: number;
|
|
29
|
+
label: string;
|
|
30
|
+
url: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let nextId = 1;
|
|
34
|
+
function flatten(nodes: NavNode[], depth: number, out: Row[]): Row[] {
|
|
35
|
+
for (const n of nodes) {
|
|
36
|
+
out.push({ id: `row-${nextId++}`, depth, label: n.label, url: n.url ?? '' });
|
|
37
|
+
if (n.children?.length) flatten(n.children, depth + 1, out);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// untrack here is not for runtime behavior -- $state runs its initializer once regardless.
|
|
43
|
+
// It suppresses the Svelte compiler warning that `data` (a prop) is referenced outside a
|
|
44
|
+
// reactive context. The component is always remounted on save/error (both redirect), so
|
|
45
|
+
// a one-time snapshot of the initial tree is correct.
|
|
46
|
+
let rows = $state<Row[]>(untrack(() => flatten(data.tree, 0, [])));
|
|
47
|
+
// depth is 0-based internally; maxDepth in the config is 1-based (1 = flat, 2 = one nesting level)
|
|
48
|
+
const maxDepthIndex = $derived(data.menu.maxDepth - 1);
|
|
49
|
+
|
|
50
|
+
// Rebuild the nested tree from the flat rows by depth, then serialize for the hidden field.
|
|
51
|
+
function toTree(list: Row[]): NavNode[] {
|
|
52
|
+
const root: NavNode[] = [];
|
|
53
|
+
const stack: { depth: number; node: NavNode }[] = [];
|
|
54
|
+
for (const r of list) {
|
|
55
|
+
const node: NavNode = { label: r.label.trim() };
|
|
56
|
+
if (r.url.trim()) node.url = r.url.trim();
|
|
57
|
+
while (stack.length && stack[stack.length - 1].depth >= r.depth) stack.pop();
|
|
58
|
+
if (stack.length) (stack[stack.length - 1].node.children ??= []).push(node);
|
|
59
|
+
else root.push(node);
|
|
60
|
+
stack.push({ depth: r.depth, node });
|
|
61
|
+
}
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const treeJson = $derived(JSON.stringify(toTree(rows)));
|
|
66
|
+
|
|
67
|
+
function addRow() {
|
|
68
|
+
rows = [...rows, { id: `row-${nextId++}`, depth: 0, label: 'New item', url: '' }];
|
|
69
|
+
}
|
|
70
|
+
function removeRow(id: string) {
|
|
71
|
+
rows = rows.filter((r) => r.id !== id);
|
|
72
|
+
}
|
|
73
|
+
function indent(i: number) {
|
|
74
|
+
// A row may nest at most one level deeper than the row above it, and never past the cap.
|
|
75
|
+
if (i === 0) return;
|
|
76
|
+
const ceiling = Math.min(rows[i - 1].depth + 1, maxDepthIndex);
|
|
77
|
+
if (rows[i].depth < ceiling) rows[i].depth += 1;
|
|
78
|
+
}
|
|
79
|
+
function outdent(i: number) {
|
|
80
|
+
if (rows[i].depth > 0) rows[i].depth -= 1;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function handleDragEnd(e: SortableListNS.RootEvents['ondragend']) {
|
|
84
|
+
const { draggedItemIndex, targetItemIndex, isCanceled } = e;
|
|
85
|
+
if (!isCanceled && typeof targetItemIndex === 'number' && draggedItemIndex !== targetItemIndex) {
|
|
86
|
+
rows = sortItems(rows, draggedItemIndex, targetItemIndex);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
</script>
|
|
90
|
+
|
|
91
|
+
<h1 class="mb-4 text-xl font-semibold">{data.menu.label}</h1>
|
|
92
|
+
|
|
93
|
+
{#if data.saved}
|
|
94
|
+
<div role="status" class="alert alert-success mb-4 text-sm">Navigation saved.</div>
|
|
95
|
+
{/if}
|
|
96
|
+
{#if data.error}
|
|
97
|
+
<div role="alert" class="alert alert-error mb-4 text-sm">{data.error}</div>
|
|
98
|
+
{/if}
|
|
99
|
+
|
|
100
|
+
<form method="POST" action="?/save">
|
|
101
|
+
<input type="hidden" name="tree" value={treeJson} />
|
|
102
|
+
|
|
103
|
+
<div class="mb-2">
|
|
104
|
+
<button type="button" class="btn btn-sm" onclick={addRow}>Add item</button>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
<div class="sortable-list-area" style="min-height:2.5rem">
|
|
108
|
+
<SortableList.Root ondragend={handleDragEnd} aria-label="Navigation items">
|
|
109
|
+
{#each rows as row, index (row.id)}
|
|
110
|
+
<SortableList.Item id={row.id} {index} aria-label={`${row.label || 'Untitled'}, level ${row.depth + 1}`}>
|
|
111
|
+
<div class="flex items-center gap-2 p-2" style={`margin-left:${row.depth * 1.5}rem`}>
|
|
112
|
+
<input class="input input-sm flex-1" placeholder="Label" aria-label="Label" bind:value={row.label} />
|
|
113
|
+
<input
|
|
114
|
+
class="input input-sm flex-1"
|
|
115
|
+
placeholder="/path or https://example.com"
|
|
116
|
+
list="cairn-nav-pages"
|
|
117
|
+
aria-label="URL"
|
|
118
|
+
bind:value={row.url}
|
|
119
|
+
/>
|
|
120
|
+
<button type="button" class="btn btn-xs btn-ghost" onclick={() => outdent(index)} aria-label="Outdent">←</button>
|
|
121
|
+
<button type="button" class="btn btn-xs btn-ghost" onclick={() => indent(index)} aria-label="Indent">→</button>
|
|
122
|
+
<button type="button" class="btn btn-xs btn-ghost text-error" onclick={() => removeRow(row.id)} aria-label={`Remove ${row.label}`}>×</button>
|
|
123
|
+
</div>
|
|
124
|
+
</SortableList.Item>
|
|
125
|
+
{/each}
|
|
126
|
+
</SortableList.Root>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<datalist id="cairn-nav-pages">
|
|
130
|
+
{#each data.pages as p (p.url)}
|
|
131
|
+
<option value={p.url}>{p.label}</option>
|
|
132
|
+
{/each}
|
|
133
|
+
</datalist>
|
|
134
|
+
|
|
135
|
+
<div class="mt-4">
|
|
136
|
+
<button type="submit" class="btn btn-primary btn-sm">Save navigation</button>
|
|
137
|
+
</div>
|
|
138
|
+
</form>
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import '@rodrigodagostino/svelte-sortable-list/styles.css';
|
|
2
|
+
import type { NavLoadData } from '../sveltekit/nav-routes.js';
|
|
3
|
+
interface Props {
|
|
4
|
+
/** The nav load's data: the menu meta, the current tree, page options, and flags. */
|
|
5
|
+
data: NavLoadData;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* The navigation tree editor. It edits a flat working copy of the menu (each row carries an
|
|
9
|
+
* explicit depth) and posts the whole tree as JSON to the save action. Vertical order comes from
|
|
10
|
+
* svelte-sortable-list (mouse, and keyboard with Space to lift, arrows to move, Space to drop);
|
|
11
|
+
* depth comes from the Indent and Outdent buttons, capped at the menu's maxDepth. The engine
|
|
12
|
+
* validates on save.
|
|
13
|
+
*/
|
|
14
|
+
declare const NavTree: import("svelte").Component<Props, {}, "">;
|
|
15
|
+
type NavTree = ReturnType<typeof NavTree>;
|
|
16
|
+
export default NavTree;
|
|
17
|
+
//# 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":"AAMA,OAAO,mDAAmD,CAAC;AAC3D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAI5D,UAAU,KAAK;IACb,qFAAqF;IACrF,IAAI,EAAE,WAAW,CAAC;CACnB;AA8HH;;;;;;GAMG;AACH,QAAA,MAAM,OAAO,2CAAwC,CAAC;AACtD,KAAK,OAAO,GAAG,UAAU,CAAC,OAAO,OAAO,CAAC,CAAC;AAC1C,eAAe,OAAO,CAAC"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/* Warm Stone: the cairn admin theme. Self-contained, since DaisyUI v5 reads these vars at point
|
|
2
|
+
of use, so this fully overrides the host's theme with no @plugin and no host build step. */
|
|
3
|
+
[data-theme='cairn-admin'] {
|
|
4
|
+
color-scheme: light;
|
|
5
|
+
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
6
|
+
|
|
7
|
+
--color-base-100: oklch(98.5% 0.004 75);
|
|
8
|
+
--color-base-200: oklch(96% 0.005 75);
|
|
9
|
+
--color-base-300: oklch(92% 0.008 75);
|
|
10
|
+
--color-base-content: oklch(28% 0.012 75);
|
|
11
|
+
|
|
12
|
+
--color-primary: oklch(52% 0.2 293);
|
|
13
|
+
--color-primary-content: oklch(98% 0.012 293);
|
|
14
|
+
--color-secondary: oklch(45% 0.02 75);
|
|
15
|
+
--color-secondary-content: oklch(98% 0.004 75);
|
|
16
|
+
--color-accent: oklch(58% 0.16 300);
|
|
17
|
+
--color-accent-content: oklch(98% 0.012 300);
|
|
18
|
+
--color-neutral: oklch(32% 0.012 75);
|
|
19
|
+
--color-neutral-content: oklch(96% 0.004 75);
|
|
20
|
+
|
|
21
|
+
--color-info: oklch(60% 0.12 240);
|
|
22
|
+
--color-info-content: oklch(98% 0.012 240);
|
|
23
|
+
--color-success: oklch(58% 0.12 150);
|
|
24
|
+
--color-success-content: oklch(98% 0.012 150);
|
|
25
|
+
--color-warning: oklch(75% 0.15 70);
|
|
26
|
+
--color-warning-content: oklch(26% 0.05 70);
|
|
27
|
+
--color-error: oklch(58% 0.2 25);
|
|
28
|
+
--color-error-content: oklch(98% 0.012 25);
|
|
29
|
+
|
|
30
|
+
/* Accessible muted text tones: >= 4.5:1 contrast on base-100/base-200. */
|
|
31
|
+
--color-muted: oklch(48% 0.01 75);
|
|
32
|
+
--color-subtle: oklch(42% 0.01 75);
|
|
33
|
+
|
|
34
|
+
--radius-selector: 0.5rem;
|
|
35
|
+
--radius-field: 0.5rem;
|
|
36
|
+
--radius-box: 0.75rem;
|
|
37
|
+
--size-selector: 0.25rem;
|
|
38
|
+
--size-field: 0.25rem;
|
|
39
|
+
--border: 1px;
|
|
40
|
+
--depth: 1;
|
|
41
|
+
--noise: 0;
|
|
42
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
export { default as AdminLayout } from './AdminLayout.svelte';
|
|
2
|
-
export { default as AdminList } from './AdminList.svelte';
|
|
3
2
|
export { default as LoginPage } from './LoginPage.svelte';
|
|
4
3
|
export { default as ConfirmPage } from './ConfirmPage.svelte';
|
|
4
|
+
export { default as ConceptList } from './ConceptList.svelte';
|
|
5
5
|
export { default as EditPage } from './EditPage.svelte';
|
|
6
|
-
export { default as
|
|
6
|
+
export { default as ManageEditors } from './ManageEditors.svelte';
|
|
7
|
+
export { default as MarkdownEditor } from './MarkdownEditor.svelte';
|
|
8
|
+
export { default as ComponentPalette } from './ComponentPalette.svelte';
|
|
9
|
+
export { default as NavTree } from './NavTree.svelte';
|
|
7
10
|
//# 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,
|
|
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,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,OAAO,IAAI,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AACxD,OAAO,EAAE,OAAO,IAAI,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AACpE,OAAO,EAAE,OAAO,IAAI,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACxE,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/components/index.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Admin Svelte components (Plan 05). The Warm Stone theme ships as a CSS side effect imported
|
|
2
|
+
// by the components that set `data-theme="cairn-admin"`.
|
|
3
3
|
export { default as AdminLayout } from './AdminLayout.svelte';
|
|
4
|
-
export { default as AdminList } from './AdminList.svelte';
|
|
5
4
|
export { default as LoginPage } from './LoginPage.svelte';
|
|
6
5
|
export { default as ConfirmPage } from './ConfirmPage.svelte';
|
|
6
|
+
export { default as ConceptList } from './ConceptList.svelte';
|
|
7
7
|
export { default as EditPage } from './EditPage.svelte';
|
|
8
|
-
export { default as
|
|
8
|
+
export { default as ManageEditors } from './ManageEditors.svelte';
|
|
9
|
+
export { default as MarkdownEditor } from './MarkdownEditor.svelte';
|
|
10
|
+
export { default as ComponentPalette } from './ComponentPalette.svelte';
|
|
11
|
+
export { default as NavTree } from './NavTree.svelte';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CairnAdapter, CairnExtension, CairnRuntime } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fold an adapter and any extensions into the composed runtime (seam 2). Extension concepts
|
|
4
|
+
* merge after the adapter's. The asset slot (seam 4) passes through untouched.
|
|
5
|
+
*/
|
|
6
|
+
export declare function composeRuntime(adapter: CairnAdapter, extensions?: CairnExtension[]): CairnRuntime;
|
|
7
|
+
//# sourceMappingURL=compose.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose.d.ts","sourceRoot":"","sources":["../../src/lib/content/compose.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAc,YAAY,EAAE,cAAc,EAAE,YAAY,EAA+B,MAAM,YAAY,CAAC;AAGtH;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,OAAO,EAAE,YAAY,EACrB,UAAU,GAAE,cAAc,EAAO,GAChC,YAAY,CAuBd"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { normalizeConcepts } from './concepts.js';
|
|
2
|
+
/**
|
|
3
|
+
* Fold an adapter and any extensions into the composed runtime (seam 2). Extension concepts
|
|
4
|
+
* merge after the adapter's. The asset slot (seam 4) passes through untouched.
|
|
5
|
+
*/
|
|
6
|
+
export function composeRuntime(adapter, extensions = []) {
|
|
7
|
+
const content = { ...adapter.content };
|
|
8
|
+
const adminPanels = [];
|
|
9
|
+
const fieldTypes = [];
|
|
10
|
+
for (const extension of extensions) {
|
|
11
|
+
// An extension adds concepts; a key that collides with the adapter is last-write-wins.
|
|
12
|
+
// Reserved seam, unused today, so the collision policy is deliberately left simple.
|
|
13
|
+
if (extension.content)
|
|
14
|
+
Object.assign(content, extension.content);
|
|
15
|
+
if (extension.adminPanels)
|
|
16
|
+
adminPanels.push(...extension.adminPanels);
|
|
17
|
+
if (extension.fieldTypes)
|
|
18
|
+
fieldTypes.push(...extension.fieldTypes);
|
|
19
|
+
}
|
|
20
|
+
return {
|
|
21
|
+
siteName: adapter.siteName,
|
|
22
|
+
concepts: normalizeConcepts(content),
|
|
23
|
+
backend: adapter.backend,
|
|
24
|
+
sender: adapter.sender,
|
|
25
|
+
renderPreview: adapter.renderPreview,
|
|
26
|
+
registry: adapter.registry,
|
|
27
|
+
navMenu: adapter.navMenu,
|
|
28
|
+
assets: adapter.assets,
|
|
29
|
+
adminPanels,
|
|
30
|
+
fieldTypes,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ConceptConfig, ConceptDescriptor, RoutingRule } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
4
|
+
* pages are plain navigable structure. Not in adapter config. A future Fragments adds one
|
|
5
|
+
* entry here and one key under `content`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const CONCEPT_ROUTING: Readonly<Record<string, RoutingRule>>;
|
|
8
|
+
/**
|
|
9
|
+
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each declared
|
|
10
|
+
* key under `content` becomes one descriptor; an undeclared (`undefined`) concept is
|
|
11
|
+
* skipped. `routing` is injectable so a contract test can prove a new concept attaches
|
|
12
|
+
* additively; production passes the default `CONCEPT_ROUTING`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeConcepts(content: Record<string, ConceptConfig | undefined>, routing?: Readonly<Record<string, RoutingRule>>): ConceptDescriptor[];
|
|
15
|
+
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
16
|
+
export declare function findConcept(concepts: ConceptDescriptor[], id: string): ConceptDescriptor | undefined;
|
|
17
|
+
//# sourceMappingURL=concepts.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"concepts.d.ts","sourceRoot":"","sources":["../../src/lib/content/concepts.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEhF;;;;GAIG;AACH,eAAO,MAAM,eAAe,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAGjE,CAAC;AAUF;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,GAAG,SAAS,CAAC,EAClD,OAAO,GAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAmB,GAC/D,iBAAiB,EAAE,CAcrB;AAED,yFAAyF;AACzF,wBAAgB,WAAW,CACzB,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,EAAE,EAAE,MAAM,GACT,iBAAiB,GAAG,SAAS,CAE/B"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
3
|
+
* pages are plain navigable structure. Not in adapter config. A future Fragments adds one
|
|
4
|
+
* entry here and one key under `content`.
|
|
5
|
+
*/
|
|
6
|
+
export const CONCEPT_ROUTING = {
|
|
7
|
+
posts: { routable: true, dated: true, inFeeds: true },
|
|
8
|
+
pages: { routable: true, dated: false, inFeeds: false },
|
|
9
|
+
};
|
|
10
|
+
/** Routing for a concept with no table entry: a plain, non-feed, routable page. */
|
|
11
|
+
const DEFAULT_ROUTING = { routable: true, dated: false, inFeeds: false };
|
|
12
|
+
/** Title-case a concept id for the default sidebar label, e.g. "posts" to "Posts". */
|
|
13
|
+
function defaultLabel(id) {
|
|
14
|
+
return id.charAt(0).toUpperCase() + id.slice(1);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). Each declared
|
|
18
|
+
* key under `content` becomes one descriptor; an undeclared (`undefined`) concept is
|
|
19
|
+
* skipped. `routing` is injectable so a contract test can prove a new concept attaches
|
|
20
|
+
* additively; production passes the default `CONCEPT_ROUTING`.
|
|
21
|
+
*/
|
|
22
|
+
export function normalizeConcepts(content, routing = CONCEPT_ROUTING) {
|
|
23
|
+
const descriptors = [];
|
|
24
|
+
for (const [id, config] of Object.entries(content)) {
|
|
25
|
+
if (!config)
|
|
26
|
+
continue;
|
|
27
|
+
descriptors.push({
|
|
28
|
+
id,
|
|
29
|
+
label: config.label ?? defaultLabel(id),
|
|
30
|
+
dir: config.dir,
|
|
31
|
+
routing: routing[id] ?? DEFAULT_ROUTING,
|
|
32
|
+
fields: config.fields,
|
|
33
|
+
validate: config.validate,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
return descriptors;
|
|
37
|
+
}
|
|
38
|
+
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
39
|
+
export function findConcept(concepts, id) {
|
|
40
|
+
return concepts.find((concept) => concept.id === id);
|
|
41
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { FrontmatterField } from './types.js';
|
|
2
|
+
/** Decode submitted form data into raw frontmatter, one rule per field type. */
|
|
3
|
+
export declare function frontmatterFromForm(fields: FrontmatterField[], form: FormData): Record<string, unknown>;
|
|
4
|
+
/**
|
|
5
|
+
* Coerce a frontmatter date value to the `YYYY-MM-DD` an `<input type="date">` wants.
|
|
6
|
+
* gray-matter parses an unquoted YAML date into a JS Date, so a string-only read would
|
|
7
|
+
* leave the input empty and drop the date on save. A parsed YAML date is UTC midnight, so
|
|
8
|
+
* slicing the ISO string avoids a local-timezone shift.
|
|
9
|
+
*/
|
|
10
|
+
export declare function dateInputValue(value: unknown): string;
|
|
11
|
+
/** Reassemble a markdown file from frontmatter and body for committing. */
|
|
12
|
+
export declare function serializeMarkdown(frontmatter: object, body: string): string;
|
|
13
|
+
/** Parse a markdown file into its frontmatter and body: the read-side inverse of serialize. */
|
|
14
|
+
export declare function parseMarkdown(source: string): {
|
|
15
|
+
frontmatter: Record<string, unknown>;
|
|
16
|
+
body: string;
|
|
17
|
+
};
|
|
18
|
+
//# sourceMappingURL=frontmatter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"frontmatter.d.ts","sourceRoot":"","sources":["../../src/lib/content/frontmatter.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEnD,gFAAgF;AAChF,wBAAgB,mBAAmB,CACjC,MAAM,EAAE,gBAAgB,EAAE,EAC1B,IAAI,EAAE,QAAQ,GACb,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA4BzB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CASrD;AAED,2EAA2E;AAC3E,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE3E;AAED,+FAA+F;AAC/F,wBAAgB,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG;IAC7C,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,IAAI,EAAE,MAAM,CAAC;CACd,CAGA"}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// cairn-cms: frontmatter form decoding and on-disk serialization. `frontmatterFromForm`
|
|
2
|
+
// is the form-to-data half of the edit loop; `serializeMarkdown`/`parseMarkdown` are the
|
|
3
|
+
// on-disk write/read pair. Kept as one seam so a site owns its serialization contract
|
|
4
|
+
// (quoting, key order) without the save endpoint reaching for gray-matter directly.
|
|
5
|
+
import matter from 'gray-matter';
|
|
6
|
+
/** Decode submitted form data into raw frontmatter, one rule per field type. */
|
|
7
|
+
export function frontmatterFromForm(fields, form) {
|
|
8
|
+
const data = {};
|
|
9
|
+
for (const field of fields) {
|
|
10
|
+
switch (field.type) {
|
|
11
|
+
case 'boolean':
|
|
12
|
+
data[field.name] = form.get(field.name) === 'on';
|
|
13
|
+
break;
|
|
14
|
+
case 'tags':
|
|
15
|
+
data[field.name] = form.getAll(field.name).map(String);
|
|
16
|
+
break;
|
|
17
|
+
case 'freetags':
|
|
18
|
+
// One comma-separated input to trimmed, de-duplicated, non-empty tags.
|
|
19
|
+
data[field.name] = [
|
|
20
|
+
...new Set(String(form.get(field.name) ?? '')
|
|
21
|
+
.split(',')
|
|
22
|
+
.map((tag) => tag.trim())
|
|
23
|
+
.filter(Boolean)),
|
|
24
|
+
];
|
|
25
|
+
break;
|
|
26
|
+
default:
|
|
27
|
+
// FormData.get returns null for an absent field; normalize to an empty string so
|
|
28
|
+
// a caller reading a text value never gets null.
|
|
29
|
+
data[field.name] = form.get(field.name) ?? '';
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Coerce a frontmatter date value to the `YYYY-MM-DD` an `<input type="date">` wants.
|
|
36
|
+
* gray-matter parses an unquoted YAML date into a JS Date, so a string-only read would
|
|
37
|
+
* leave the input empty and drop the date on save. A parsed YAML date is UTC midnight, so
|
|
38
|
+
* slicing the ISO string avoids a local-timezone shift.
|
|
39
|
+
*/
|
|
40
|
+
export function dateInputValue(value) {
|
|
41
|
+
if (value instanceof Date) {
|
|
42
|
+
return Number.isNaN(value.getTime()) ? '' : value.toISOString().slice(0, 10);
|
|
43
|
+
}
|
|
44
|
+
if (typeof value === 'string') {
|
|
45
|
+
const match = value.match(/^\d{4}-\d{2}-\d{2}/);
|
|
46
|
+
return match ? match[0] : '';
|
|
47
|
+
}
|
|
48
|
+
return '';
|
|
49
|
+
}
|
|
50
|
+
/** Reassemble a markdown file from frontmatter and body for committing. */
|
|
51
|
+
export function serializeMarkdown(frontmatter, body) {
|
|
52
|
+
return matter.stringify(body, frontmatter);
|
|
53
|
+
}
|
|
54
|
+
/** Parse a markdown file into its frontmatter and body: the read-side inverse of serialize. */
|
|
55
|
+
export function parseMarkdown(source) {
|
|
56
|
+
const parsed = matter(source);
|
|
57
|
+
return { frontmatter: parsed.data, body: parsed.content };
|
|
58
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** True when `id` is a valid filename stem: lowercase, no slashes, no leading or trailing hyphen. */
|
|
2
|
+
export declare function isValidId(id: string): boolean;
|
|
3
|
+
/**
|
|
4
|
+
* A content entry's id from its filename: the basename without the `.md` suffix. Pass a
|
|
5
|
+
* basename, not a path; the caller strips any directory prefix first (Plan 03's Git Trees
|
|
6
|
+
* listing yields basenames directly).
|
|
7
|
+
*/
|
|
8
|
+
export declare function idFromFilename(filename: string): string;
|
|
9
|
+
/** The on-disk filename for an id: the id plus `.md`. */
|
|
10
|
+
export declare function filenameFromId(id: string): string;
|
|
11
|
+
/**
|
|
12
|
+
* Lowercase a title into a filename-safe slug stem. Apostrophes are dropped so "Geoff's"
|
|
13
|
+
* becomes "geoffs" (no spurious hyphen). All other non-alphanumeric runs collapse to a
|
|
14
|
+
* single hyphen; leading and trailing hyphens are trimmed.
|
|
15
|
+
*/
|
|
16
|
+
export declare function slugify(title: string): string;
|
|
17
|
+
//# sourceMappingURL=ids.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ids.d.ts","sourceRoot":"","sources":["../../src/lib/content/ids.ts"],"names":[],"mappings":"AAOA,qGAAqG;AACrG,wBAAgB,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAEvD;AAED,yDAAyD;AACzD,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED;;;;GAIG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAM7C"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// cairn-cms: filename-based content ids (spec §7.2). An entry's id is its markdown filename
|
|
2
|
+
// without `.md`, so there is no slug codec. `slugify` derives a filename-safe stem from a
|
|
3
|
+
// title for the create-entry form.
|
|
4
|
+
/** Lowercase alphanumerics with single internal hyphens: the on-disk filename stem rule. */
|
|
5
|
+
const ID_RE = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
6
|
+
/** True when `id` is a valid filename stem: lowercase, no slashes, no leading or trailing hyphen. */
|
|
7
|
+
export function isValidId(id) {
|
|
8
|
+
return ID_RE.test(id);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* A content entry's id from its filename: the basename without the `.md` suffix. Pass a
|
|
12
|
+
* basename, not a path; the caller strips any directory prefix first (Plan 03's Git Trees
|
|
13
|
+
* listing yields basenames directly).
|
|
14
|
+
*/
|
|
15
|
+
export function idFromFilename(filename) {
|
|
16
|
+
return filename.replace(/\.md$/, '');
|
|
17
|
+
}
|
|
18
|
+
/** The on-disk filename for an id: the id plus `.md`. */
|
|
19
|
+
export function filenameFromId(id) {
|
|
20
|
+
return `${id}.md`;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Lowercase a title into a filename-safe slug stem. Apostrophes are dropped so "Geoff's"
|
|
24
|
+
* becomes "geoffs" (no spurious hyphen). All other non-alphanumeric runs collapse to a
|
|
25
|
+
* single hyphen; leading and trailing hyphens are trimmed.
|
|
26
|
+
*/
|
|
27
|
+
export function slugify(title) {
|
|
28
|
+
return title
|
|
29
|
+
.toLowerCase()
|
|
30
|
+
.replace(/'/g, '')
|
|
31
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
32
|
+
.replace(/^-+|-+$/g, '');
|
|
33
|
+
}
|