@glw907/cairn-cms 0.29.0 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/dist/components/AdminLayout.svelte +372 -44
  3. package/dist/components/AdminLayout.svelte.d.ts +5 -4
  4. package/dist/components/CairnLogo.svelte +28 -0
  5. package/dist/components/CairnLogo.svelte.d.ts +15 -0
  6. package/dist/components/ComponentForm.svelte +1 -1
  7. package/dist/components/ConceptList.svelte +240 -45
  8. package/dist/components/ConceptList.svelte.d.ts +12 -2
  9. package/dist/components/ConfirmPage.svelte +20 -3
  10. package/dist/components/EditPage.svelte +12 -7
  11. package/dist/components/LoginPage.svelte +27 -5
  12. package/dist/components/ManageEditors.svelte +8 -5
  13. package/dist/components/NavTree.svelte +2 -2
  14. package/dist/components/admin-icons.d.ts +13 -0
  15. package/dist/components/admin-icons.js +15 -0
  16. package/dist/components/cairn-admin.css +5516 -37
  17. package/dist/components/cairn-favicon.d.ts +2 -0
  18. package/dist/components/cairn-favicon.js +7 -0
  19. package/dist/components/chrome-guard.d.ts +9 -0
  20. package/dist/components/chrome-guard.js +55 -0
  21. package/dist/components/fonts/BricolageGrotesque-OFL.txt +93 -0
  22. package/dist/components/fonts/Figtree-OFL.txt +93 -0
  23. package/dist/components/fonts/bricolage-grotesque.woff2 +0 -0
  24. package/dist/components/fonts/figtree.woff2 +0 -0
  25. package/dist/index.d.ts +0 -2
  26. package/dist/index.js +4 -1
  27. package/dist/render/authoring.d.ts +3 -0
  28. package/dist/render/authoring.js +5 -0
  29. package/dist/render/registry.d.ts +2 -0
  30. package/dist/render/registry.js +15 -0
  31. package/dist/render/rehype-dispatch.d.ts +9 -6
  32. package/dist/render/rehype-dispatch.js +12 -6
  33. package/dist/render/remark-directives.js +1 -1
  34. package/dist/sveltekit/content-routes.d.ts +12 -1
  35. package/dist/sveltekit/content-routes.js +37 -13
  36. package/dist/sveltekit/guard.js +32 -0
  37. package/dist/sveltekit/https-required-page.d.ts +5 -0
  38. package/dist/sveltekit/https-required-page.js +216 -0
  39. package/package.json +16 -2
  40. package/src/lib/components/AdminLayout.svelte +372 -44
  41. package/src/lib/components/CairnLogo.svelte +28 -0
  42. package/src/lib/components/ComponentForm.svelte +1 -1
  43. package/src/lib/components/ConceptList.svelte +240 -45
  44. package/src/lib/components/ConfirmPage.svelte +20 -3
  45. package/src/lib/components/EditPage.svelte +12 -7
  46. package/src/lib/components/LoginPage.svelte +27 -5
  47. package/src/lib/components/ManageEditors.svelte +8 -5
  48. package/src/lib/components/NavTree.svelte +2 -2
  49. package/src/lib/components/admin-icons.ts +15 -0
  50. package/src/lib/components/cairn-admin.css +162 -7
  51. package/src/lib/components/cairn-favicon.ts +9 -0
  52. package/src/lib/components/chrome-guard.ts +62 -0
  53. package/src/lib/components/fonts/BricolageGrotesque-OFL.txt +93 -0
  54. package/src/lib/components/fonts/Figtree-OFL.txt +93 -0
  55. package/src/lib/components/fonts/bricolage-grotesque.woff2 +0 -0
  56. package/src/lib/components/fonts/figtree.woff2 +0 -0
  57. package/src/lib/index.ts +4 -2
  58. package/src/lib/render/authoring.ts +7 -0
  59. package/src/lib/render/registry.ts +20 -0
  60. package/src/lib/render/rehype-dispatch.ts +13 -6
  61. package/src/lib/render/remark-directives.ts +1 -1
  62. package/src/lib/sveltekit/content-routes.ts +51 -14
  63. package/src/lib/sveltekit/guard.ts +36 -0
  64. package/src/lib/sveltekit/https-required-page.ts +220 -0
@@ -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-family: system-ui, -apple-system, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
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(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);
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.5rem;
36
- --radius-box: 0.75rem;
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.
package/src/lib/index.ts CHANGED
@@ -88,8 +88,10 @@ export type { ReferenceOptions } from './render/component-reference.js';
88
88
  export { glyph } from './render/glyph.js';
89
89
  export type { IconSet } from './render/glyph.js';
90
90
  export { remarkDirectiveStamp } from './render/remark-directives.js';
91
- export { rehypeDispatch, iconSpan, cardShell, headRow } from './render/rehype-dispatch.js';
92
- export type { MakeIcon } from './render/rehype-dispatch.js';
91
+ // The component-authoring helpers (iconSpan, cardShell, headRow, isElement, strAttr) live on the
92
+ // @glw907/cairn-cms/render subpath, not the root barrel. rehypeDispatch is deliberately not public:
93
+ // createRenderer is the one public render pipeline, so the safe plugin ordering is the only public
94
+ // path. See docs/superpowers/specs/2026-06-05-cairn-render-authoring-surface-design.md.
93
95
  export { createRenderer } from './render/pipeline.js';
94
96
  export type { RendererOptions } from './render/pipeline.js';
95
97
 
@@ -0,0 +1,7 @@
1
+ // cairn-cms: the component-authoring toolkit (@glw907/cairn-cms/render). A site authoring components
2
+ // through build(ctx) reaches for these hast builders and the string-attribute reader. Curated on
3
+ // purpose: the internal hast helpers (strProp, markFirstList, dataAttrProp) stay internal, and
4
+ // rehypeDispatch is deliberately omitted (createRenderer is the one public render pipeline).
5
+ export { iconSpan, cardShell, headRow, isElement, strAttr } from './rehype-dispatch.js';
6
+ export type { MakeIcon } from './rehype-dispatch.js';
7
+ export type { ComponentContext } from './registry.js';
@@ -82,6 +82,8 @@ export interface ComponentRegistry {
82
82
  names: string[];
83
83
  get(name: string): ComponentDef | undefined;
84
84
  defaultIcon(name: string, role?: string): string | undefined;
85
+ /** The component's first `type:'icon'` attribute, or undefined when it declares none. */
86
+ iconField(name: string): AttributeField | undefined;
85
87
  }
86
88
 
87
89
  /** The hast property name carrying one declared attribute from stamp to dispatch, e.g. `tone`
@@ -91,17 +93,35 @@ export function dataAttrProp(key: string): string {
91
93
  return `dataAttr${key.charAt(0).toUpperCase()}${key.slice(1)}`;
92
94
  }
93
95
 
96
+ /** A component's first `type:'icon'` attribute, or undefined when it declares none. Both the
97
+ * construction-time guard and the registry's `iconField` derive the icon field from this one
98
+ * predicate rather than spelling the `type === 'icon'` find twice. */
99
+ function findIconField(def: ComponentDef): AttributeField | undefined {
100
+ return def.attributes?.find((field) => field.type === 'icon');
101
+ }
102
+
94
103
  /**
95
104
  * Build a registry from a site's component definitions. The single source the render
96
105
  * pipeline (directive stamp plus rehype dispatch) and the editor palette both read.
97
106
  */
98
107
  export function defineRegistry({ components }: { components: ComponentDef[] }): ComponentRegistry {
108
+ for (const c of components) {
109
+ if (c.defaultIconByRole && Object.keys(c.defaultIconByRole).length > 0 && !findIconField(c)) {
110
+ throw new Error(
111
+ `cairn: component "${c.name}" sets defaultIconByRole but declares no type:'icon' attribute, so the default icon can never render`,
112
+ );
113
+ }
114
+ }
99
115
  const byName = new Map(components.map((c) => [c.name, c]));
100
116
  return {
101
117
  defs: components,
102
118
  names: components.map((c) => c.name),
103
119
  get: (name) => byName.get(name),
104
120
  defaultIcon: (name, role) => (role ? byName.get(name)?.defaultIconByRole?.[role] : undefined),
121
+ iconField: (name) => {
122
+ const def = byName.get(name);
123
+ return def ? findIconField(def) : undefined;
124
+ },
105
125
  };
106
126
  }
107
127
 
@@ -6,6 +6,13 @@ export function isElement(node: ElementContent | undefined): node is Element {
6
6
  return !!node && node.type === 'element';
7
7
  }
8
8
 
9
+ /** Read a declared string attribute off the component context, returning undefined for a boolean or
10
+ * absent value. Replaces the `typeof ctx.attributes[key] === 'string'` narrowing a build repeats. */
11
+ export function strAttr(ctx: ComponentContext, key: string): string | undefined {
12
+ const value = ctx.attributes[key];
13
+ return typeof value === 'string' ? value : undefined;
14
+ }
15
+
9
16
  // hast Properties values are PropertyValue (string | number | boolean | array | null).
10
17
  // Directive markers (dataPrimitive/dataRole/dataAttr<Key>) are always stamped as strings;
11
18
  // this reads them back with that guarantee instead of casting at each call site.
@@ -28,14 +35,14 @@ export function cardShell(classes: string[], body: ElementContent[]): Element {
28
35
  return h('section', { className: classes }, [h('div', { className: ['card-body'] }, body)]);
29
36
  }
30
37
 
31
- /** Card head row: `<div class="ec-head">[icon]<h2 class="card-title">{title}</h2></div>`.
32
- * Pass the title's inline children and an optional pre-built icon element, the way `cardShell`
33
- * takes already-built body content. This factors the icon-plus-heading head that a titled
34
- * component build would otherwise rebuild by hand (the shape the removed `splitHead` produced). */
35
- export function headRow(title: ElementContent[], icon?: Element): Element {
38
+ /** Card head row: `<div class="ec-head">[icon]<hN class="card-title">{title}</hN></div>`.
39
+ * Pass the title's inline children, an optional pre-built icon element, and an optional heading
40
+ * level (default 2). This factors the icon-plus-heading head that a titled component build would
41
+ * otherwise rebuild by hand (the shape the removed `splitHead` produced). */
42
+ export function headRow(title: ElementContent[], icon?: Element, level: number = 2): Element {
36
43
  const children: ElementContent[] = [];
37
44
  if (icon) children.push(icon);
38
- children.push(h('h2', { className: ['card-title'] }, title));
45
+ children.push(h(`h${level}`, { className: ['card-title'] }, title));
39
46
  return h('div', { className: ['ec-head'] }, children);
40
47
  }
41
48
 
@@ -59,7 +59,7 @@ export function remarkDirectiveStamp(registry: ComponentRegistry) {
59
59
  const def = registry.get(node.name);
60
60
  const attrs = node.attributes ?? {};
61
61
  const role = attrs.role || undefined;
62
- const iconField = def?.attributes?.find((field) => field.type === 'icon');
62
+ const iconField = registry.iconField(node.name);
63
63
  const iconKey = iconField?.key ?? 'icon';
64
64
  let icon = attrs[iconKey] || undefined;
65
65
  if (!icon && role) icon = registry.defaultIcon(node.name, role);