@glw907/cairn-cms 0.26.0 → 0.33.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -0
- package/dist/auth/crypto.d.ts +0 -1
- package/dist/auth/store.d.ts +0 -1
- package/dist/auth/types.d.ts +0 -1
- package/dist/components/AdminLayout.svelte +372 -44
- package/dist/components/AdminLayout.svelte.d.ts +5 -5
- package/dist/components/CairnLogo.svelte +28 -0
- package/dist/components/CairnLogo.svelte.d.ts +15 -0
- package/dist/components/ComponentForm.svelte +1 -1
- package/dist/components/ComponentForm.svelte.d.ts +0 -1
- package/dist/components/ComponentInsertDialog.svelte.d.ts +0 -1
- package/dist/components/ConceptList.svelte +240 -45
- package/dist/components/ConceptList.svelte.d.ts +12 -3
- package/dist/components/ConfirmPage.svelte +20 -3
- package/dist/components/ConfirmPage.svelte.d.ts +0 -1
- package/dist/components/DeleteDialog.svelte.d.ts +0 -1
- package/dist/components/EditPage.svelte +12 -7
- package/dist/components/EditPage.svelte.d.ts +0 -1
- package/dist/components/EditorToolbar.svelte.d.ts +0 -1
- package/dist/components/IconPicker.svelte.d.ts +0 -1
- package/dist/components/LinkPicker.svelte.d.ts +0 -1
- package/dist/components/LoginPage.svelte +27 -5
- package/dist/components/LoginPage.svelte.d.ts +0 -1
- package/dist/components/ManageEditors.svelte +8 -5
- package/dist/components/ManageEditors.svelte.d.ts +0 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +0 -1
- package/dist/components/NavTree.svelte +2 -2
- package/dist/components/NavTree.svelte.d.ts +0 -1
- package/dist/components/RenameDialog.svelte.d.ts +0 -1
- package/dist/components/admin-icons.d.ts +13 -0
- package/dist/components/admin-icons.js +15 -0
- package/dist/components/cairn-admin.css +5516 -37
- package/dist/components/cairn-favicon.d.ts +2 -0
- package/dist/components/cairn-favicon.js +7 -0
- package/dist/components/chrome-guard.d.ts +9 -0
- package/dist/components/chrome-guard.js +55 -0
- package/dist/components/fonts/BricolageGrotesque-OFL.txt +93 -0
- package/dist/components/fonts/Figtree-OFL.txt +93 -0
- package/dist/components/fonts/bricolage-grotesque.woff2 +0 -0
- package/dist/components/fonts/figtree.woff2 +0 -0
- package/dist/components/index.d.ts +0 -1
- package/dist/components/link-completion.d.ts +0 -1
- package/dist/components/markdown-format.d.ts +0 -1
- package/dist/content/adapter.d.ts +0 -1
- package/dist/content/compose.d.ts +1 -2
- package/dist/content/compose.js +2 -3
- package/dist/content/concepts.d.ts +7 -1
- package/dist/content/concepts.js +49 -1
- package/dist/content/frontmatter.d.ts +0 -1
- package/dist/content/identity.d.ts +23 -0
- package/dist/content/identity.js +43 -0
- package/dist/content/ids.d.ts +0 -1
- package/dist/content/links.d.ts +0 -1
- package/dist/content/manifest.d.ts +3 -2
- package/dist/content/manifest.js +6 -26
- package/dist/content/permalink.d.ts +0 -1
- package/dist/content/schema.d.ts +0 -1
- package/dist/content/types.d.ts +0 -1
- package/dist/content/validate.d.ts +0 -1
- package/dist/delivery/CairnHead.svelte.d.ts +0 -1
- package/dist/delivery/content-index.d.ts +0 -1
- package/dist/delivery/content-index.js +8 -25
- package/dist/delivery/data.d.ts +0 -1
- package/dist/delivery/excerpt.d.ts +0 -1
- package/dist/delivery/feeds.d.ts +0 -1
- package/dist/delivery/head.d.ts +0 -1
- package/dist/delivery/index.d.ts +0 -1
- package/dist/delivery/json-ld.d.ts +0 -1
- package/dist/delivery/manifest.d.ts +0 -1
- package/dist/delivery/paginate.d.ts +0 -1
- package/dist/delivery/responses.d.ts +0 -1
- package/dist/delivery/robots.d.ts +0 -1
- package/dist/delivery/seo-fields.d.ts +0 -1
- package/dist/delivery/seo.d.ts +0 -1
- package/dist/delivery/site-descriptors.d.ts +0 -1
- package/dist/delivery/site-descriptors.js +5 -6
- package/dist/delivery/site-index.d.ts +0 -1
- package/dist/delivery/site-indexes.d.ts +0 -1
- package/dist/delivery/sitemap.d.ts +0 -1
- package/dist/email.d.ts +0 -1
- package/dist/env.d.ts +0 -1
- package/dist/github/credentials.d.ts +0 -1
- package/dist/github/repo.d.ts +0 -1
- package/dist/github/signing.d.ts +0 -1
- package/dist/github/types.d.ts +0 -1
- package/dist/index.d.ts +0 -29
- package/dist/index.js +4 -23
- package/dist/nav/site-config.d.ts +0 -1
- package/dist/render/authoring.d.ts +3 -0
- package/dist/render/authoring.js +5 -0
- package/dist/render/component-grammar.d.ts +0 -1
- package/dist/render/component-insert.d.ts +0 -1
- package/dist/render/component-reference.d.ts +0 -1
- package/dist/render/component-validate.d.ts +0 -1
- package/dist/render/glyph.d.ts +0 -1
- package/dist/render/index.d.ts +0 -1
- package/dist/render/pipeline.d.ts +0 -1
- package/dist/render/pipeline.js +5 -1
- package/dist/render/registry.d.ts +2 -1
- package/dist/render/registry.js +15 -0
- package/dist/render/rehype-dispatch.d.ts +9 -7
- package/dist/render/rehype-dispatch.js +12 -6
- package/dist/render/remark-directives.d.ts +0 -1
- package/dist/render/remark-directives.js +1 -1
- package/dist/render/resolve-links.d.ts +0 -1
- package/dist/render/sanitize-schema.d.ts +14 -1
- package/dist/render/sanitize-schema.js +96 -0
- package/dist/sveltekit/auth-routes.d.ts +0 -1
- package/dist/sveltekit/content-routes.d.ts +12 -2
- package/dist/sveltekit/content-routes.js +37 -13
- package/dist/sveltekit/editors-routes.d.ts +0 -1
- package/dist/sveltekit/guard.d.ts +0 -1
- package/dist/sveltekit/health.d.ts +0 -1
- package/dist/sveltekit/index.d.ts +1 -3
- package/dist/sveltekit/index.js +0 -1
- package/dist/sveltekit/nav-routes.d.ts +0 -1
- package/dist/sveltekit/public-routes.d.ts +0 -1
- package/dist/sveltekit/types.d.ts +0 -1
- package/dist/vite/bin.d.ts +0 -1
- package/dist/vite/index.d.ts +0 -1
- package/package.json +16 -2
- package/src/lib/components/AdminLayout.svelte +372 -44
- package/src/lib/components/CairnLogo.svelte +28 -0
- package/src/lib/components/ComponentForm.svelte +1 -1
- package/src/lib/components/ConceptList.svelte +240 -45
- package/src/lib/components/ConfirmPage.svelte +20 -3
- package/src/lib/components/EditPage.svelte +12 -7
- package/src/lib/components/LoginPage.svelte +27 -5
- package/src/lib/components/ManageEditors.svelte +8 -5
- package/src/lib/components/NavTree.svelte +2 -2
- package/src/lib/components/admin-icons.ts +15 -0
- package/src/lib/components/cairn-admin.css +162 -7
- package/src/lib/components/cairn-favicon.ts +9 -0
- package/src/lib/components/chrome-guard.ts +62 -0
- package/src/lib/components/fonts/BricolageGrotesque-OFL.txt +93 -0
- package/src/lib/components/fonts/Figtree-OFL.txt +93 -0
- package/src/lib/components/fonts/bricolage-grotesque.woff2 +0 -0
- package/src/lib/components/fonts/figtree.woff2 +0 -0
- package/src/lib/content/compose.ts +3 -3
- package/src/lib/content/concepts.ts +61 -1
- package/src/lib/content/identity.ts +60 -0
- package/src/lib/content/manifest.ts +6 -27
- package/src/lib/delivery/content-index.ts +8 -27
- package/src/lib/delivery/site-descriptors.ts +5 -6
- package/src/lib/index.ts +4 -57
- package/src/lib/render/authoring.ts +7 -0
- package/src/lib/render/pipeline.ts +4 -1
- package/src/lib/render/registry.ts +20 -0
- package/src/lib/render/rehype-dispatch.ts +13 -6
- package/src/lib/render/remark-directives.ts +1 -1
- package/src/lib/render/sanitize-schema.ts +97 -0
- package/src/lib/sveltekit/content-routes.ts +51 -14
- package/src/lib/sveltekit/index.ts +2 -8
- package/dist/auth/crypto.d.ts.map +0 -1
- package/dist/auth/store.d.ts.map +0 -1
- package/dist/auth/types.d.ts.map +0 -1
- package/dist/components/AdminLayout.svelte.d.ts.map +0 -1
- package/dist/components/ComponentForm.svelte.d.ts.map +0 -1
- package/dist/components/ComponentInsertDialog.svelte.d.ts.map +0 -1
- package/dist/components/ConceptList.svelte.d.ts.map +0 -1
- package/dist/components/ConfirmPage.svelte.d.ts.map +0 -1
- package/dist/components/DeleteDialog.svelte.d.ts.map +0 -1
- package/dist/components/EditPage.svelte.d.ts.map +0 -1
- package/dist/components/EditorToolbar.svelte.d.ts.map +0 -1
- package/dist/components/IconPicker.svelte.d.ts.map +0 -1
- package/dist/components/LinkPicker.svelte.d.ts.map +0 -1
- package/dist/components/LoginPage.svelte.d.ts.map +0 -1
- package/dist/components/ManageEditors.svelte.d.ts.map +0 -1
- package/dist/components/MarkdownEditor.svelte.d.ts.map +0 -1
- package/dist/components/NavTree.svelte.d.ts.map +0 -1
- package/dist/components/RenameDialog.svelte.d.ts.map +0 -1
- package/dist/components/index.d.ts.map +0 -1
- package/dist/components/link-completion.d.ts.map +0 -1
- package/dist/components/markdown-format.d.ts.map +0 -1
- package/dist/content/adapter.d.ts.map +0 -1
- package/dist/content/compose.d.ts.map +0 -1
- package/dist/content/concepts.d.ts.map +0 -1
- package/dist/content/frontmatter.d.ts.map +0 -1
- package/dist/content/ids.d.ts.map +0 -1
- package/dist/content/links.d.ts.map +0 -1
- package/dist/content/manifest.d.ts.map +0 -1
- package/dist/content/permalink.d.ts.map +0 -1
- package/dist/content/schema.d.ts.map +0 -1
- package/dist/content/types.d.ts.map +0 -1
- package/dist/content/validate.d.ts.map +0 -1
- package/dist/delivery/CairnHead.svelte.d.ts.map +0 -1
- package/dist/delivery/content-index.d.ts.map +0 -1
- package/dist/delivery/data.d.ts.map +0 -1
- package/dist/delivery/excerpt.d.ts.map +0 -1
- package/dist/delivery/feeds.d.ts.map +0 -1
- package/dist/delivery/head.d.ts.map +0 -1
- package/dist/delivery/index.d.ts.map +0 -1
- package/dist/delivery/json-ld.d.ts.map +0 -1
- package/dist/delivery/manifest.d.ts.map +0 -1
- package/dist/delivery/paginate.d.ts.map +0 -1
- package/dist/delivery/responses.d.ts.map +0 -1
- package/dist/delivery/robots.d.ts.map +0 -1
- package/dist/delivery/seo-fields.d.ts.map +0 -1
- package/dist/delivery/seo.d.ts.map +0 -1
- package/dist/delivery/site-descriptors.d.ts.map +0 -1
- package/dist/delivery/site-index.d.ts.map +0 -1
- package/dist/delivery/site-indexes.d.ts.map +0 -1
- package/dist/delivery/sitemap.d.ts.map +0 -1
- package/dist/email.d.ts.map +0 -1
- package/dist/env.d.ts.map +0 -1
- package/dist/github/credentials.d.ts.map +0 -1
- package/dist/github/repo.d.ts.map +0 -1
- package/dist/github/signing.d.ts.map +0 -1
- package/dist/github/types.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/nav/site-config.d.ts.map +0 -1
- package/dist/render/component-grammar.d.ts.map +0 -1
- package/dist/render/component-insert.d.ts.map +0 -1
- package/dist/render/component-reference.d.ts.map +0 -1
- package/dist/render/component-validate.d.ts.map +0 -1
- package/dist/render/glyph.d.ts.map +0 -1
- package/dist/render/index.d.ts.map +0 -1
- package/dist/render/pipeline.d.ts.map +0 -1
- package/dist/render/registry.d.ts.map +0 -1
- package/dist/render/rehype-dispatch.d.ts.map +0 -1
- package/dist/render/remark-directives.d.ts.map +0 -1
- package/dist/render/resolve-links.d.ts.map +0 -1
- package/dist/render/sanitize-schema.d.ts.map +0 -1
- package/dist/sveltekit/auth-routes.d.ts.map +0 -1
- package/dist/sveltekit/content-routes.d.ts.map +0 -1
- package/dist/sveltekit/editors-routes.d.ts.map +0 -1
- package/dist/sveltekit/guard.d.ts.map +0 -1
- package/dist/sveltekit/health.d.ts.map +0 -1
- package/dist/sveltekit/index.d.ts.map +0 -1
- package/dist/sveltekit/nav-routes.d.ts.map +0 -1
- package/dist/sveltekit/public-routes.d.ts.map +0 -1
- package/dist/sveltekit/types.d.ts.map +0 -1
- package/dist/vite/bin.d.ts.map +0 -1
- package/dist/vite/index.d.ts.map +0 -1
|
@@ -1,13 +1,24 @@
|
|
|
1
|
+
/* Cairn's own typeface, self-hosted so the admin needs no external font request. Figtree carries the
|
|
2
|
+
body and UI (friendly, highly legible at small sizes); Bricolage Grotesque gives the brand and the
|
|
3
|
+
page headings a distinct voice. Both are variable (one file spans the weight range) and licensed
|
|
4
|
+
under the SIL Open Font License. The `@font-face` rules are added by scripts/build-admin-css.mjs
|
|
5
|
+
after compile (so the woff2 url stays relative to the shipped sheet, not the source tree). */
|
|
6
|
+
|
|
1
7
|
/* Warm Stone: the cairn admin theme. Self-contained, since DaisyUI v5 reads these vars at point
|
|
2
8
|
of use, so this fully overrides the host's theme with no @plugin and no host build step. */
|
|
3
9
|
[data-theme='cairn-admin'] {
|
|
4
10
|
color-scheme: light;
|
|
5
|
-
font-
|
|
11
|
+
--font-body: 'Figtree Variable', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
12
|
+
--font-display: 'Bricolage Grotesque Variable', var(--font-body);
|
|
13
|
+
font-family: var(--font-body);
|
|
14
|
+
/* Crisper, lighter glyph rendering for the variable fonts. */
|
|
15
|
+
-webkit-font-smoothing: antialiased;
|
|
16
|
+
-moz-osx-font-smoothing: grayscale;
|
|
6
17
|
|
|
7
|
-
--color-base-100: oklch(
|
|
8
|
-
--color-base-200: oklch(96% 0.
|
|
9
|
-
--color-base-300: oklch(
|
|
10
|
-
--color-base-content: oklch(
|
|
18
|
+
--color-base-100: oklch(99% 0.004 75);
|
|
19
|
+
--color-base-200: oklch(96.5% 0.006 75);
|
|
20
|
+
--color-base-300: oklch(89% 0.011 75);
|
|
21
|
+
--color-base-content: oklch(26% 0.014 75);
|
|
11
22
|
|
|
12
23
|
--color-primary: oklch(52% 0.2 293);
|
|
13
24
|
--color-primary-content: oklch(98% 0.012 293);
|
|
@@ -32,11 +43,155 @@
|
|
|
32
43
|
--color-subtle: oklch(42% 0.01 75);
|
|
33
44
|
|
|
34
45
|
--radius-selector: 0.5rem;
|
|
35
|
-
--radius-field: 0.
|
|
36
|
-
--radius-box:
|
|
46
|
+
--radius-field: 0.625rem;
|
|
47
|
+
--radius-box: 1rem;
|
|
48
|
+
--size-selector: 0.25rem;
|
|
49
|
+
--size-field: 0.25rem;
|
|
50
|
+
--border: 1px;
|
|
51
|
+
--depth: 1;
|
|
52
|
+
--noise: 0;
|
|
53
|
+
|
|
54
|
+
/* Soft, layered elevation: a close contact shadow plus a wide, diffuse one, warm-tinted. The cards
|
|
55
|
+
float gently instead of sitting in hard boxes, which reads more current than a flat border. */
|
|
56
|
+
--cairn-shadow: 0 1px 2px oklch(28% 0.02 75 / 0.05), 0 8px 24px -6px oklch(28% 0.02 75 / 0.1);
|
|
57
|
+
/* A faint hairline for the floating cards in light, where the shadow carries the definition. */
|
|
58
|
+
--cairn-card-border: oklch(93% 0.008 75);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/* Warm Stone, dark. Mirrors the light palette in dark tones, keeping the same hues (the warm 75 and
|
|
62
|
+
the violet 293 primary) so the admin reads as one theme under either mode. DaisyUI v5 reads these
|
|
63
|
+
at point of use, so the compiled component classes need no change. The muted and subtle tones keep
|
|
64
|
+
>= 4.5:1 contrast on the dark bases. The polish pass tunes these values against the reference. */
|
|
65
|
+
[data-theme='cairn-admin-dark'] {
|
|
66
|
+
color-scheme: dark;
|
|
67
|
+
--font-body: 'Figtree Variable', system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
|
|
68
|
+
--font-display: 'Bricolage Grotesque Variable', var(--font-body);
|
|
69
|
+
font-family: var(--font-body);
|
|
70
|
+
/* Crisper, lighter glyph rendering for the variable fonts. */
|
|
71
|
+
-webkit-font-smoothing: antialiased;
|
|
72
|
+
-moz-osx-font-smoothing: grayscale;
|
|
73
|
+
|
|
74
|
+
/* Dark surface layering: the panels (sidebar, topbar, cards = base-100) lift clearly above the app
|
|
75
|
+
background (base-200, the deepest tone), with base-300 borders lighter still for a visible edge.
|
|
76
|
+
The wider base-100-to-base-200 gap is what reads as depth on dark, since shadows barely show. */
|
|
77
|
+
--color-base-100: oklch(24% 0.01 75);
|
|
78
|
+
--color-base-200: oklch(15.5% 0.009 75);
|
|
79
|
+
--color-base-300: oklch(30% 0.014 75);
|
|
80
|
+
--color-base-content: oklch(93% 0.006 75);
|
|
81
|
+
|
|
82
|
+
--color-primary: oklch(68% 0.18 293);
|
|
83
|
+
--color-primary-content: oklch(20% 0.04 293);
|
|
84
|
+
--color-secondary: oklch(72% 0.02 75);
|
|
85
|
+
--color-secondary-content: oklch(20% 0.008 75);
|
|
86
|
+
--color-accent: oklch(70% 0.14 300);
|
|
87
|
+
--color-accent-content: oklch(20% 0.04 300);
|
|
88
|
+
--color-neutral: oklch(80% 0.01 75);
|
|
89
|
+
--color-neutral-content: oklch(22% 0.008 75);
|
|
90
|
+
|
|
91
|
+
--color-info: oklch(72% 0.12 240);
|
|
92
|
+
--color-info-content: oklch(20% 0.04 240);
|
|
93
|
+
--color-success: oklch(70% 0.12 150);
|
|
94
|
+
--color-success-content: oklch(20% 0.04 150);
|
|
95
|
+
--color-warning: oklch(80% 0.14 70);
|
|
96
|
+
--color-warning-content: oklch(24% 0.05 70);
|
|
97
|
+
--color-error: oklch(70% 0.18 25);
|
|
98
|
+
--color-error-content: oklch(20% 0.04 25);
|
|
99
|
+
|
|
100
|
+
/* Accessible muted text tones on the dark bases. */
|
|
101
|
+
--color-muted: oklch(72% 0.01 75);
|
|
102
|
+
--color-subtle: oklch(80% 0.008 75);
|
|
103
|
+
|
|
104
|
+
--radius-selector: 0.5rem;
|
|
105
|
+
--radius-field: 0.625rem;
|
|
106
|
+
--radius-box: 1rem;
|
|
37
107
|
--size-selector: 0.25rem;
|
|
38
108
|
--size-field: 0.25rem;
|
|
39
109
|
--border: 1px;
|
|
40
110
|
--depth: 1;
|
|
41
111
|
--noise: 0;
|
|
112
|
+
|
|
113
|
+
/* On dark, a soft shadow barely registers, so it is deeper and darker to still lift the cards. */
|
|
114
|
+
--cairn-shadow: 0 1px 2px oklch(0% 0 0 / 0.35), 0 8px 24px -6px oklch(0% 0 0 / 0.5);
|
|
115
|
+
/* On dark the shadow does little, so the card keeps a visible border for definition. */
|
|
116
|
+
--cairn-card-border: oklch(30% 0.014 75);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/* The scoped reset that replaces global Preflight (which the admin sheet omits). It applies only
|
|
120
|
+
inside the admin theme roots, so it never reaches the host's pages. The dark root has no variables
|
|
121
|
+
yet (plan 2 adds them); naming it here is harmless and keeps the reset stable across both themes. */
|
|
122
|
+
[data-theme='cairn-admin'] *,
|
|
123
|
+
[data-theme='cairn-admin'] *::before,
|
|
124
|
+
[data-theme='cairn-admin'] *::after,
|
|
125
|
+
[data-theme='cairn-admin-dark'] *,
|
|
126
|
+
[data-theme='cairn-admin-dark'] *::before,
|
|
127
|
+
[data-theme='cairn-admin-dark'] *::after {
|
|
128
|
+
box-sizing: border-box;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* Anchors inherit their color and drop the underline by default, which the omitted Preflight used to
|
|
132
|
+
do. This lives in the `components` layer, below `utilities`, so a Tailwind utility (a `text-*`
|
|
133
|
+
color, a `hover:underline`) and DaisyUI's own `.link`/`.breadcrumbs` rules (which sit in the
|
|
134
|
+
utilities layer) all override it. Cascade layers resolve before specificity, so an unlayered reset
|
|
135
|
+
here would instead beat every utility and silently kill those hover and link affordances. */
|
|
136
|
+
@layer components {
|
|
137
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) a {
|
|
138
|
+
color: inherit;
|
|
139
|
+
text-decoration: none;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/* Collapsible nav sections use a native <details>. Hide the default disclosure marker and rotate
|
|
143
|
+
the caret to point down when the section is open. Scoped to the admin, components layer so a
|
|
144
|
+
utility still wins. */
|
|
145
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) summary {
|
|
146
|
+
list-style: none;
|
|
147
|
+
}
|
|
148
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) summary::-webkit-details-marker {
|
|
149
|
+
display: none;
|
|
150
|
+
}
|
|
151
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .cairn-caret {
|
|
152
|
+
transition: rotate 150ms ease;
|
|
153
|
+
}
|
|
154
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) details[open] > summary .cairn-caret {
|
|
155
|
+
rotate: 90deg;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* Selected text picks up the brand violet, so highlighting reads as Cairn rather than the browser
|
|
159
|
+
default blue. A light tint keeps the text legible. */
|
|
160
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) ::selection {
|
|
161
|
+
background-color: color-mix(in oklch, var(--color-primary) 24%, transparent);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/* One brand-violet keyboard-focus ring across the admin, so every control shows the same clear
|
|
165
|
+
focus, including the links, nav items, and summaries that the browser would otherwise outline in
|
|
166
|
+
its default color. Only keyboard focus triggers it. */
|
|
167
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) :focus-visible {
|
|
168
|
+
outline: 2px solid var(--color-primary);
|
|
169
|
+
outline-offset: 2px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* The primary action carries a soft violet lift that grows on hover, so the main call to action
|
|
173
|
+
reads as raised and current rather than a flat fill. The mix adapts to the theme's primary. */
|
|
174
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .btn-primary:not(:disabled) {
|
|
175
|
+
box-shadow:
|
|
176
|
+
0 1px 2px color-mix(in oklch, var(--color-primary) 22%, transparent),
|
|
177
|
+
0 6px 16px -5px color-mix(in oklch, var(--color-primary) 38%, transparent);
|
|
178
|
+
}
|
|
179
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .btn-primary:not(:disabled):hover {
|
|
180
|
+
box-shadow:
|
|
181
|
+
0 2px 4px color-mix(in oklch, var(--color-primary) 26%, transparent),
|
|
182
|
+
0 10px 24px -5px color-mix(in oklch, var(--color-primary) 48%, transparent);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/* Respect a reduced-motion preference inside the admin. DaisyUI's modal, drawer, and the admin's
|
|
187
|
+
own hover transitions otherwise animate regardless. Scoped to the admin roots, so it never
|
|
188
|
+
reaches the host's pages. */
|
|
189
|
+
@media (prefers-reduced-motion: reduce) {
|
|
190
|
+
[data-theme='cairn-admin'] *,
|
|
191
|
+
[data-theme='cairn-admin-dark'] * {
|
|
192
|
+
animation-duration: 0.01ms !important;
|
|
193
|
+
animation-iteration-count: 1 !important;
|
|
194
|
+
transition-duration: 0.01ms !important;
|
|
195
|
+
scroll-behavior: auto !important;
|
|
196
|
+
}
|
|
42
197
|
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The cairn mark as an inline SVG data URL, so an admin browser tab carries Cairn's brand. The fill
|
|
2
|
+
// is a fixed violet near the admin primary, since a favicon cannot read a CSS variable. The path is
|
|
3
|
+
// the same public-domain Temaki cairn used by CairnLogo.svelte.
|
|
4
|
+
const svg =
|
|
5
|
+
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="#7c3aed">' +
|
|
6
|
+
'<path d="M6.28 14C5.56 14 1 13.89 1 12.91C1 11.46 2.16 11.07 3.2 10.81C4.36 10.51 13.18 9.77 13.76 10.07C14.46 10.43 13.52 12.49 12.44 12.77C11.28 13.07 10.21 14 8.48 14C7.05 14 9.69 14 6.28 14ZM6.92 4.5C6.67 4.5 5 4.43 5 3.88C5 3.07 5.75 2.51 5.96 2.35C6.36 2.03 6.32 1.62 6.54 1.27C6.84 0.79 7.61 0.5 7.88 0.5C8.1 0.5 8.75 0.9 9.23 1.42C9.45 1.66 10 2.77 10 3.12C10 4.22 9.36 4.5 8.85 4.5C8.33 4.5 8.15 4.5 6.92 4.5ZM3.68 8.22C3 7.73 3.67 6.86 4.57 6.21C5.38 5.63 5.92 5.96 6.79 5.7C8.33 5.24 9.02 5.72 9.02 5.72L10.9 6.82C12.03 7.63 10.99 7.67 10.38 8.56C9.79 9.42 8.18 9.11 7.42 9.33C6.78 9.53 5.75 9.71 4.62 8.9L3.68 8.22Z"/></svg>';
|
|
7
|
+
|
|
8
|
+
/** The cairn mark as a `data:image/svg+xml` URL, for a `<link rel="icon">` on the admin pages. */
|
|
9
|
+
export const cairnFaviconHref = `data:image/svg+xml,${encodeURIComponent(svg)}`;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Dev-only structural check that catches a host mounting the admin inside its own chrome. Every admin
|
|
2
|
+
// rule is scoped and the admin self-styles, but a host whose root layout wraps the admin in a
|
|
3
|
+
// width-constraining container (a `<main class="container">`) or renders its nav and footer around it
|
|
4
|
+
// breaks the full-bleed admin shell. The engine cannot prevent that layout mistake, so it names it.
|
|
5
|
+
// The check walks the ancestor chain once on mount and emits one console.error that points at the
|
|
6
|
+
// route-structure doc. The public entry runs only under import.meta.env.DEV, never throws, and changes
|
|
7
|
+
// no rendering.
|
|
8
|
+
|
|
9
|
+
const DOC = 'docs/admin-route-structure.md';
|
|
10
|
+
|
|
11
|
+
// max-width values that do not actually constrain the admin below the viewport. A host that sets a
|
|
12
|
+
// defensive `max-width: 100%` or `100vw` on a wrapper is not chrome, so skip those to avoid a spurious
|
|
13
|
+
// dev error. A real constraining container uses an absolute length (`64rem`, `1280px`) or a sub-100
|
|
14
|
+
// percentage, both of which still trip the guard.
|
|
15
|
+
const NON_CONSTRAINING = new Set(['none', '100%', '100vw']);
|
|
16
|
+
|
|
17
|
+
function describe(el: Element): string {
|
|
18
|
+
const tag = el.tagName.toLowerCase();
|
|
19
|
+
const cls = el.getAttribute('class');
|
|
20
|
+
return cls ? `<${tag} class="${cls}">` : `<${tag}>`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Inspect the admin root's ancestor chain for host chrome. Returns a diagnostic when a
|
|
25
|
+
* width-constraining ancestor sits between the root and <body>, else null. Pure over the DOM so a
|
|
26
|
+
* test can build either shape. The sibling signal (host elements outside the admin subtree) is folded
|
|
27
|
+
* into the message as context rather than raised on its own, because it is the noisier of the two.
|
|
28
|
+
*/
|
|
29
|
+
export function detectChromeWrap(root: HTMLElement): string | null {
|
|
30
|
+
const body = root.ownerDocument.body;
|
|
31
|
+
let constrainer: HTMLElement | null = null;
|
|
32
|
+
let maxWidth = '';
|
|
33
|
+
for (let el = root.parentElement; el && el !== body; el = el.parentElement) {
|
|
34
|
+
const elMaxWidth = getComputedStyle(el).maxWidth;
|
|
35
|
+
if (elMaxWidth && !NON_CONSTRAINING.has(elMaxWidth)) {
|
|
36
|
+
constrainer = el;
|
|
37
|
+
maxWidth = elMaxWidth;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (!constrainer) return null;
|
|
42
|
+
|
|
43
|
+
const siblings = [...body.children].filter(
|
|
44
|
+
(el) => !el.contains(root) && !root.contains(el) && el !== root,
|
|
45
|
+
);
|
|
46
|
+
const siblingNote = siblings.length
|
|
47
|
+
? ` Host elements also sit beside the admin in <body> (${siblings.map(describe).join(', ')}).`
|
|
48
|
+
: '';
|
|
49
|
+
return (
|
|
50
|
+
`[cairn-cms] The admin is rendering inside host chrome. A width-constraining ancestor ` +
|
|
51
|
+
`${describe(constrainer)} (max-width: ${maxWidth}) sits between the admin root and <body>, so the ` +
|
|
52
|
+
`admin shell cannot fill the viewport.${siblingNote} Keep the host root layout chrome-free and move ` +
|
|
53
|
+
`your nav, footer, and app.css into a (site) route group. See ${DOC}.`
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Run the check in dev and log one error when host chrome is detected. A no-op in production. */
|
|
58
|
+
export function warnIfChromeWrapped(root: HTMLElement): void {
|
|
59
|
+
if (!import.meta.env.DEV) return;
|
|
60
|
+
const problem = detectChromeWrap(root);
|
|
61
|
+
if (problem) console.error(problem);
|
|
62
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2022 The Bricolage Grotesque Project Authors (https://github.com/ateliertriay/bricolage)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
https://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Copyright 2022 The Figtree Project Authors (https://github.com/erikdkennedy/figtree)
|
|
2
|
+
|
|
3
|
+
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
|
4
|
+
This license is copied below, and is also available with a FAQ at:
|
|
5
|
+
http://scripts.sil.org/OFL
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
-----------------------------------------------------------
|
|
9
|
+
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
|
10
|
+
-----------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
PREAMBLE
|
|
13
|
+
The goals of the Open Font License (OFL) are to stimulate worldwide
|
|
14
|
+
development of collaborative font projects, to support the font creation
|
|
15
|
+
efforts of academic and linguistic communities, and to provide a free and
|
|
16
|
+
open framework in which fonts may be shared and improved in partnership
|
|
17
|
+
with others.
|
|
18
|
+
|
|
19
|
+
The OFL allows the licensed fonts to be used, studied, modified and
|
|
20
|
+
redistributed freely as long as they are not sold by themselves. The
|
|
21
|
+
fonts, including any derivative works, can be bundled, embedded,
|
|
22
|
+
redistributed and/or sold with any software provided that any reserved
|
|
23
|
+
names are not used by derivative works. The fonts and derivatives,
|
|
24
|
+
however, cannot be released under any other type of license. The
|
|
25
|
+
requirement for fonts to remain under this license does not apply
|
|
26
|
+
to any document created using the fonts or their derivatives.
|
|
27
|
+
|
|
28
|
+
DEFINITIONS
|
|
29
|
+
"Font Software" refers to the set of files released by the Copyright
|
|
30
|
+
Holder(s) under this license and clearly marked as such. This may
|
|
31
|
+
include source files, build scripts and documentation.
|
|
32
|
+
|
|
33
|
+
"Reserved Font Name" refers to any names specified as such after the
|
|
34
|
+
copyright statement(s).
|
|
35
|
+
|
|
36
|
+
"Original Version" refers to the collection of Font Software components as
|
|
37
|
+
distributed by the Copyright Holder(s).
|
|
38
|
+
|
|
39
|
+
"Modified Version" refers to any derivative made by adding to, deleting,
|
|
40
|
+
or substituting -- in part or in whole -- any of the components of the
|
|
41
|
+
Original Version, by changing formats or by porting the Font Software to a
|
|
42
|
+
new environment.
|
|
43
|
+
|
|
44
|
+
"Author" refers to any designer, engineer, programmer, technical
|
|
45
|
+
writer or other person who contributed to the Font Software.
|
|
46
|
+
|
|
47
|
+
PERMISSION & CONDITIONS
|
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
49
|
+
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
|
50
|
+
redistribute, and sell modified and unmodified copies of the Font
|
|
51
|
+
Software, subject to the following conditions:
|
|
52
|
+
|
|
53
|
+
1) Neither the Font Software nor any of its individual components,
|
|
54
|
+
in Original or Modified Versions, may be sold by itself.
|
|
55
|
+
|
|
56
|
+
2) Original or Modified Versions of the Font Software may be bundled,
|
|
57
|
+
redistributed and/or sold with any software, provided that each copy
|
|
58
|
+
contains the above copyright notice and this license. These can be
|
|
59
|
+
included either as stand-alone text files, human-readable headers or
|
|
60
|
+
in the appropriate machine-readable metadata fields within text or
|
|
61
|
+
binary files as long as those fields can be easily viewed by the user.
|
|
62
|
+
|
|
63
|
+
3) No Modified Version of the Font Software may use the Reserved Font
|
|
64
|
+
Name(s) unless explicit written permission is granted by the corresponding
|
|
65
|
+
Copyright Holder. This restriction only applies to the primary font name as
|
|
66
|
+
presented to the users.
|
|
67
|
+
|
|
68
|
+
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
|
69
|
+
Software shall not be used to promote, endorse or advertise any
|
|
70
|
+
Modified Version, except to acknowledge the contribution(s) of the
|
|
71
|
+
Copyright Holder(s) and the Author(s) or with their explicit written
|
|
72
|
+
permission.
|
|
73
|
+
|
|
74
|
+
5) The Font Software, modified or unmodified, in part or in whole,
|
|
75
|
+
must be distributed entirely under this license, and must not be
|
|
76
|
+
distributed under any other license. The requirement for fonts to
|
|
77
|
+
remain under this license does not apply to any document created
|
|
78
|
+
using the Font Software.
|
|
79
|
+
|
|
80
|
+
TERMINATION
|
|
81
|
+
This license becomes null and void if any of the above conditions are
|
|
82
|
+
not met.
|
|
83
|
+
|
|
84
|
+
DISCLAIMER
|
|
85
|
+
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
86
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
|
87
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
|
88
|
+
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
|
89
|
+
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
90
|
+
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
|
91
|
+
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
92
|
+
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
|
93
|
+
OTHER DEALINGS IN THE FONT SOFTWARE.
|
|
Binary file
|
|
Binary file
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// concepts, components, field types, and save hooks. Shaped now so the extension contract
|
|
5
5
|
// is additive later.
|
|
6
6
|
import type { AdminPanel, CairnAdapter, CairnExtension, CairnRuntime, ConceptConfig, FieldTypeDef } from './types.js';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import { resolveConcepts } from './concepts.js';
|
|
8
|
+
import type { SiteConfig } from '../nav/site-config.js';
|
|
9
9
|
|
|
10
10
|
/** The input to {@link composeRuntime}. `siteConfig` is required so the per-concept URL policy is
|
|
11
11
|
* always derived from one source and can never be silently dropped. `extensions` fold in after the
|
|
@@ -36,7 +36,7 @@ export function composeRuntime({ adapter, siteConfig, extensions = [] }: Compose
|
|
|
36
36
|
}
|
|
37
37
|
return {
|
|
38
38
|
siteName: adapter.siteName,
|
|
39
|
-
concepts:
|
|
39
|
+
concepts: resolveConcepts(content, siteConfig),
|
|
40
40
|
backend: adapter.backend,
|
|
41
41
|
sender: adapter.sender,
|
|
42
42
|
render: adapter.render,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
// future Fragments concept attaches by adding one key under `content` and one routing
|
|
5
5
|
// entry, with no reshape here.
|
|
6
6
|
import type { ConceptConfig, ConceptDescriptor, ConceptUrlPolicy, RoutingRule } from './types.js';
|
|
7
|
+
import { urlPolicyFrom, type SiteConfig } from '../nav/site-config.js';
|
|
7
8
|
|
|
8
9
|
/**
|
|
9
10
|
* Concept-fixed routing, keyed by concept id (spec §7.2). Posts are dated feed entries;
|
|
@@ -28,6 +29,43 @@ function defaultPermalink(id: string): string {
|
|
|
28
29
|
return id === 'pages' ? '/:slug' : `/${id}/:slug`;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
/** Permalink tokens the resolver understands. */
|
|
33
|
+
const KNOWN_TOKENS = new Set(['slug', 'year', 'month', 'day']);
|
|
34
|
+
/** The date-bearing tokens; valid only for a dated concept. */
|
|
35
|
+
const DATE_TOKENS = new Set(['year', 'month', 'day']);
|
|
36
|
+
/** The valid date-prefix granularities. A runtime check, since the YAML is untyped. */
|
|
37
|
+
const DATE_PREFIXES = new Set<string>(['year', 'month', 'day']);
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Validate one concept's URL policy at build, so a misconfigured permalink or datePrefix fails loudly
|
|
41
|
+
* here rather than emitting a wrong or defaulted URL at render. The permalink must be root-relative and
|
|
42
|
+
* use only known tokens, a date token requires a dated concept, and the datePrefix must be in range.
|
|
43
|
+
*/
|
|
44
|
+
function validateUrlPolicy(id: string, policy: ConceptUrlPolicy, dated: boolean): void {
|
|
45
|
+
if (policy.permalink !== undefined) {
|
|
46
|
+
const pattern = policy.permalink;
|
|
47
|
+
if (!pattern.startsWith('/')) {
|
|
48
|
+
throw new Error(`cairn: concept "${id}" permalink "${pattern}" must start with "/"`);
|
|
49
|
+
}
|
|
50
|
+
for (const match of pattern.matchAll(/:(\w+)/g)) {
|
|
51
|
+
const token = match[1];
|
|
52
|
+
if (!KNOWN_TOKENS.has(token)) {
|
|
53
|
+
throw new Error(`cairn: concept "${id}" permalink "${pattern}" uses unknown token ":${token}"`);
|
|
54
|
+
}
|
|
55
|
+
if (DATE_TOKENS.has(token) && !dated) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`cairn: concept "${id}" is not dated, so permalink "${pattern}" cannot use the date token ":${token}"`,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (policy.datePrefix !== undefined && !DATE_PREFIXES.has(policy.datePrefix)) {
|
|
63
|
+
throw new Error(
|
|
64
|
+
`cairn: concept "${id}" datePrefix "${policy.datePrefix}" must be one of year, month, day`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
31
69
|
/**
|
|
32
70
|
* Normalize an adapter's declared concepts into uniform descriptors (seam 1). URL policy
|
|
33
71
|
* (`permalink`, `datePrefix`) comes from the YAML site-config, passed here as `urlPolicy` keyed by
|
|
@@ -41,6 +79,14 @@ export function normalizeConcepts(
|
|
|
41
79
|
routing: Readonly<Record<string, RoutingRule>> = CONCEPT_ROUTING,
|
|
42
80
|
): ConceptDescriptor[] {
|
|
43
81
|
const descriptors: ConceptDescriptor[] = [];
|
|
82
|
+
const declaredConcepts = new Set(
|
|
83
|
+
Object.keys(content).filter((key) => content[key] !== undefined),
|
|
84
|
+
);
|
|
85
|
+
for (const key of Object.keys(urlPolicy)) {
|
|
86
|
+
if (!declaredConcepts.has(key)) {
|
|
87
|
+
throw new Error(`cairn: URL policy names concept "${key}", which is not declared under content`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
44
90
|
for (const [id, config] of Object.entries(content)) {
|
|
45
91
|
if (!config) continue;
|
|
46
92
|
const summaryFields = config.summaryFields ?? [];
|
|
@@ -51,12 +97,14 @@ export function normalizeConcepts(
|
|
|
51
97
|
`cairn: concept "${id}" summaryFields key "${undeclared}" is not a declared field`,
|
|
52
98
|
);
|
|
53
99
|
}
|
|
100
|
+
const conceptRouting = routing[id] ?? DEFAULT_ROUTING;
|
|
54
101
|
const policy = urlPolicy[id] ?? {};
|
|
102
|
+
validateUrlPolicy(id, policy, conceptRouting.dated);
|
|
55
103
|
descriptors.push({
|
|
56
104
|
id,
|
|
57
105
|
label: config.label ?? defaultLabel(id),
|
|
58
106
|
dir: config.dir,
|
|
59
|
-
routing:
|
|
107
|
+
routing: conceptRouting,
|
|
60
108
|
permalink: policy.permalink ?? defaultPermalink(id),
|
|
61
109
|
datePrefix: policy.datePrefix ?? 'day',
|
|
62
110
|
fields: config.schema.fields,
|
|
@@ -67,6 +115,18 @@ export function normalizeConcepts(
|
|
|
67
115
|
return descriptors;
|
|
68
116
|
}
|
|
69
117
|
|
|
118
|
+
/**
|
|
119
|
+
* Resolve a site's concept descriptors from its content map and parsed site config. The admin runtime
|
|
120
|
+
* (composeRuntime) and the delivery layer (siteDescriptors) both call this, so the per-concept URL
|
|
121
|
+
* policy is derived once from the YAML and the runtime and delivery permalinks cannot diverge.
|
|
122
|
+
*/
|
|
123
|
+
export function resolveConcepts(
|
|
124
|
+
content: Record<string, ConceptConfig | undefined>,
|
|
125
|
+
siteConfig: SiteConfig,
|
|
126
|
+
): ConceptDescriptor[] {
|
|
127
|
+
return normalizeConcepts(content, urlPolicyFrom(siteConfig));
|
|
128
|
+
}
|
|
129
|
+
|
|
70
130
|
/** Look up a normalized concept by id, or undefined when the site does not enable it. */
|
|
71
131
|
export function findConcept(
|
|
72
132
|
concepts: ConceptDescriptor[],
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// cairn-cms: a content entry's URL identity in one place (engine-hardening pass 3). The id, the
|
|
2
|
+
// slug, the date, and the permalink are computed here, so the content index and the manifest cannot
|
|
3
|
+
// drift on what an entry's URL is. A cairn: link resolves through the manifest in the admin preview
|
|
4
|
+
// and through the content index in the public build, so the two must agree by construction.
|
|
5
|
+
import { idFromFilename, slugFromId } from './ids.js';
|
|
6
|
+
import { permalink } from './permalink.js';
|
|
7
|
+
import type { ConceptDescriptor } from './types.js';
|
|
8
|
+
|
|
9
|
+
/** A content entry's resolved URL identity. */
|
|
10
|
+
export interface EntryIdentity {
|
|
11
|
+
id: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
date?: string;
|
|
14
|
+
permalink: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The basename of a glob path: the segment after the last slash, or the whole path. */
|
|
18
|
+
function basename(path: string): string {
|
|
19
|
+
const slash = path.lastIndexOf('/');
|
|
20
|
+
return slash >= 0 ? path.slice(slash + 1) : path;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** A present, non-empty string, else undefined. The read-model string coercion. */
|
|
24
|
+
export function asString(value: unknown): string | undefined {
|
|
25
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** A YYYY-MM-DD date. An unquoted YAML date parses as a JS Date; a string is sliced to its date head. */
|
|
29
|
+
export function asDate(value: unknown): string | undefined {
|
|
30
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? undefined : value.toISOString().slice(0, 10);
|
|
31
|
+
if (typeof value === 'string') return value.match(/^\d{4}-\d{2}-\d{2}/)?.[0];
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Tags as an array, empty when the file declares none. */
|
|
36
|
+
export function asTags(value: unknown): string[] {
|
|
37
|
+
return Array.isArray(value) ? value.map(String) : [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** A content entry's id: its filename stem (the date prefix is part of a dated id). */
|
|
41
|
+
export function entryId(path: string): string {
|
|
42
|
+
return idFromFilename(basename(path));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a content entry's URL identity from its concept descriptor, its file path, and its parsed
|
|
47
|
+
* frontmatter. The slug strips the leading date prefix for a dated concept and is the id verbatim for
|
|
48
|
+
* an undated one. The permalink is the one resolver every reader shares. The caller parses the markdown
|
|
49
|
+
* once and passes the frontmatter, so there is no second parse here.
|
|
50
|
+
*/
|
|
51
|
+
export function entryIdentity(
|
|
52
|
+
descriptor: ConceptDescriptor,
|
|
53
|
+
path: string,
|
|
54
|
+
frontmatter: Record<string, unknown>,
|
|
55
|
+
): EntryIdentity {
|
|
56
|
+
const id = entryId(path);
|
|
57
|
+
const slug = slugFromId(id, descriptor.routing.dated ? descriptor.datePrefix : null);
|
|
58
|
+
const date = asDate(frontmatter.date);
|
|
59
|
+
return { id, slug, date, permalink: permalink(descriptor, { id, slug, date }) };
|
|
60
|
+
}
|