@glw907/cairn-cms 0.29.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.
Files changed (59) hide show
  1. package/CHANGELOG.md +94 -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/package.json +15 -2
  37. package/src/lib/components/AdminLayout.svelte +372 -44
  38. package/src/lib/components/CairnLogo.svelte +28 -0
  39. package/src/lib/components/ComponentForm.svelte +1 -1
  40. package/src/lib/components/ConceptList.svelte +240 -45
  41. package/src/lib/components/ConfirmPage.svelte +20 -3
  42. package/src/lib/components/EditPage.svelte +12 -7
  43. package/src/lib/components/LoginPage.svelte +27 -5
  44. package/src/lib/components/ManageEditors.svelte +8 -5
  45. package/src/lib/components/NavTree.svelte +2 -2
  46. package/src/lib/components/admin-icons.ts +15 -0
  47. package/src/lib/components/cairn-admin.css +162 -7
  48. package/src/lib/components/cairn-favicon.ts +9 -0
  49. package/src/lib/components/chrome-guard.ts +62 -0
  50. package/src/lib/components/fonts/BricolageGrotesque-OFL.txt +93 -0
  51. package/src/lib/components/fonts/Figtree-OFL.txt +93 -0
  52. package/src/lib/components/fonts/bricolage-grotesque.woff2 +0 -0
  53. package/src/lib/components/fonts/figtree.woff2 +0 -0
  54. package/src/lib/index.ts +4 -2
  55. package/src/lib/render/authoring.ts +7 -0
  56. package/src/lib/render/registry.ts +20 -0
  57. package/src/lib/render/rehype-dispatch.ts +13 -6
  58. package/src/lib/render/remark-directives.ts +1 -1
  59. package/src/lib/sveltekit/content-routes.ts +51 -14
@@ -0,0 +1,2 @@
1
+ /** The cairn mark as a `data:image/svg+xml` URL, for a `<link rel="icon">` on the admin pages. */
2
+ export declare const cairnFaviconHref: string;
@@ -0,0 +1,7 @@
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 = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="#7c3aed">' +
5
+ '<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>';
6
+ /** The cairn mark as a `data:image/svg+xml` URL, for a `<link rel="icon">` on the admin pages. */
7
+ export const cairnFaviconHref = `data:image/svg+xml,${encodeURIComponent(svg)}`;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Inspect the admin root's ancestor chain for host chrome. Returns a diagnostic when a
3
+ * width-constraining ancestor sits between the root and <body>, else null. Pure over the DOM so a
4
+ * test can build either shape. The sibling signal (host elements outside the admin subtree) is folded
5
+ * into the message as context rather than raised on its own, because it is the noisier of the two.
6
+ */
7
+ export declare function detectChromeWrap(root: HTMLElement): string | null;
8
+ /** Run the check in dev and log one error when host chrome is detected. A no-op in production. */
9
+ export declare function warnIfChromeWrapped(root: HTMLElement): void;
@@ -0,0 +1,55 @@
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
+ const DOC = 'docs/admin-route-structure.md';
9
+ // max-width values that do not actually constrain the admin below the viewport. A host that sets a
10
+ // defensive `max-width: 100%` or `100vw` on a wrapper is not chrome, so skip those to avoid a spurious
11
+ // dev error. A real constraining container uses an absolute length (`64rem`, `1280px`) or a sub-100
12
+ // percentage, both of which still trip the guard.
13
+ const NON_CONSTRAINING = new Set(['none', '100%', '100vw']);
14
+ function describe(el) {
15
+ const tag = el.tagName.toLowerCase();
16
+ const cls = el.getAttribute('class');
17
+ return cls ? `<${tag} class="${cls}">` : `<${tag}>`;
18
+ }
19
+ /**
20
+ * Inspect the admin root's ancestor chain for host chrome. Returns a diagnostic when a
21
+ * width-constraining ancestor sits between the root and <body>, else null. Pure over the DOM so a
22
+ * test can build either shape. The sibling signal (host elements outside the admin subtree) is folded
23
+ * into the message as context rather than raised on its own, because it is the noisier of the two.
24
+ */
25
+ export function detectChromeWrap(root) {
26
+ const body = root.ownerDocument.body;
27
+ let constrainer = null;
28
+ let maxWidth = '';
29
+ for (let el = root.parentElement; el && el !== body; el = el.parentElement) {
30
+ const elMaxWidth = getComputedStyle(el).maxWidth;
31
+ if (elMaxWidth && !NON_CONSTRAINING.has(elMaxWidth)) {
32
+ constrainer = el;
33
+ maxWidth = elMaxWidth;
34
+ break;
35
+ }
36
+ }
37
+ if (!constrainer)
38
+ return null;
39
+ const siblings = [...body.children].filter((el) => !el.contains(root) && !root.contains(el) && el !== root);
40
+ const siblingNote = siblings.length
41
+ ? ` Host elements also sit beside the admin in <body> (${siblings.map(describe).join(', ')}).`
42
+ : '';
43
+ return (`[cairn-cms] The admin is rendering inside host chrome. A width-constraining ancestor ` +
44
+ `${describe(constrainer)} (max-width: ${maxWidth}) sits between the admin root and <body>, so the ` +
45
+ `admin shell cannot fill the viewport.${siblingNote} Keep the host root layout chrome-free and move ` +
46
+ `your nav, footer, and app.css into a (site) route group. See ${DOC}.`);
47
+ }
48
+ /** Run the check in dev and log one error when host chrome is detected. A no-op in production. */
49
+ export function warnIfChromeWrapped(root) {
50
+ if (!import.meta.env.DEV)
51
+ return;
52
+ const problem = detectChromeWrap(root);
53
+ if (problem)
54
+ console.error(problem);
55
+ }
@@ -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/dist/index.d.ts CHANGED
@@ -27,8 +27,6 @@ export type { ReferenceOptions } from './render/component-reference.js';
27
27
  export { glyph } from './render/glyph.js';
28
28
  export type { IconSet } from './render/glyph.js';
29
29
  export { remarkDirectiveStamp } from './render/remark-directives.js';
30
- export { rehypeDispatch, iconSpan, cardShell, headRow } from './render/rehype-dispatch.js';
31
- export type { MakeIcon } from './render/rehype-dispatch.js';
32
30
  export { createRenderer } from './render/pipeline.js';
33
31
  export type { RendererOptions } from './render/pipeline.js';
34
32
  export type { RepoRef, RepoFile, CommitAuthor, AppCredentials } from './github/types.js';
package/dist/index.js CHANGED
@@ -21,7 +21,10 @@ export { buildComponentInsert } from './render/component-insert.js';
21
21
  export { generateComponentReference } from './render/component-reference.js';
22
22
  export { glyph } from './render/glyph.js';
23
23
  export { remarkDirectiveStamp } from './render/remark-directives.js';
24
- export { rehypeDispatch, iconSpan, cardShell, headRow } from './render/rehype-dispatch.js';
24
+ // The component-authoring helpers (iconSpan, cardShell, headRow, isElement, strAttr) live on the
25
+ // @glw907/cairn-cms/render subpath, not the root barrel. rehypeDispatch is deliberately not public:
26
+ // createRenderer is the one public render pipeline, so the safe plugin ordering is the only public
27
+ // path. See docs/superpowers/specs/2026-06-05-cairn-render-authoring-surface-design.md.
25
28
  export { createRenderer } from './render/pipeline.js';
26
29
  export { CommitConflictError } from './github/types.js';
27
30
  // Nav tree and site-config helpers (Plan 06).
@@ -0,0 +1,3 @@
1
+ export { iconSpan, cardShell, headRow, isElement, strAttr } from './rehype-dispatch.js';
2
+ export type { MakeIcon } from './rehype-dispatch.js';
3
+ export type { ComponentContext } from './registry.js';
@@ -0,0 +1,5 @@
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';
@@ -70,6 +70,8 @@ export interface ComponentRegistry {
70
70
  names: string[];
71
71
  get(name: string): ComponentDef | undefined;
72
72
  defaultIcon(name: string, role?: string): string | undefined;
73
+ /** The component's first `type:'icon'` attribute, or undefined when it declares none. */
74
+ iconField(name: string): AttributeField | undefined;
73
75
  }
74
76
  /** The hast property name carrying one declared attribute from stamp to dispatch, e.g. `tone`
75
77
  * becomes `dataAttrTone`. The directive stamp writes it and the rehype dispatch reads it, so both
@@ -4,17 +4,32 @@
4
4
  export function dataAttrProp(key) {
5
5
  return `dataAttr${key.charAt(0).toUpperCase()}${key.slice(1)}`;
6
6
  }
7
+ /** A component's first `type:'icon'` attribute, or undefined when it declares none. Both the
8
+ * construction-time guard and the registry's `iconField` derive the icon field from this one
9
+ * predicate rather than spelling the `type === 'icon'` find twice. */
10
+ function findIconField(def) {
11
+ return def.attributes?.find((field) => field.type === 'icon');
12
+ }
7
13
  /**
8
14
  * Build a registry from a site's component definitions. The single source the render
9
15
  * pipeline (directive stamp plus rehype dispatch) and the editor palette both read.
10
16
  */
11
17
  export function defineRegistry({ components }) {
18
+ for (const c of components) {
19
+ if (c.defaultIconByRole && Object.keys(c.defaultIconByRole).length > 0 && !findIconField(c)) {
20
+ throw new Error(`cairn: component "${c.name}" sets defaultIconByRole but declares no type:'icon' attribute, so the default icon can never render`);
21
+ }
22
+ }
12
23
  const byName = new Map(components.map((c) => [c.name, c]));
13
24
  return {
14
25
  defs: components,
15
26
  names: components.map((c) => c.name),
16
27
  get: (name) => byName.get(name),
17
28
  defaultIcon: (name, role) => (role ? byName.get(name)?.defaultIconByRole?.[role] : undefined),
29
+ iconField: (name) => {
30
+ const def = byName.get(name);
31
+ return def ? findIconField(def) : undefined;
32
+ },
18
33
  };
19
34
  }
20
35
  /** Seed an empty {@link ComponentValues} from a component's schema: attribute defaults (or '' / false)
@@ -1,6 +1,9 @@
1
1
  import type { Root, Element, ElementContent } from 'hast';
2
- import { type ComponentRegistry } from './registry.js';
2
+ import { type ComponentContext, type ComponentRegistry } from './registry.js';
3
3
  export declare function isElement(node: ElementContent | undefined): node is Element;
4
+ /** Read a declared string attribute off the component context, returning undefined for a boolean or
5
+ * absent value. Replaces the `typeof ctx.attributes[key] === 'string'` narrowing a build repeats. */
6
+ export declare function strAttr(ctx: ComponentContext, key: string): string | undefined;
4
7
  export declare function strProp(node: Element, name: string): string | undefined;
5
8
  /** Wrap a pre-built glyph in an ec-icon span; secondary role adds the modifier. */
6
9
  export declare function iconSpan(glyphEl: Element, role?: string): Element;
@@ -8,11 +11,11 @@ export declare function iconSpan(glyphEl: Element, role?: string): Element;
8
11
  export type MakeIcon = (name: string, role?: string) => Element;
9
12
  /** Section wrapper: `<section class=…><div class="card-body">…</div></section>`. */
10
13
  export declare function cardShell(classes: string[], body: ElementContent[]): Element;
11
- /** Card head row: `<div class="ec-head">[icon]<h2 class="card-title">{title}</h2></div>`.
12
- * Pass the title's inline children and an optional pre-built icon element, the way `cardShell`
13
- * takes already-built body content. This factors the icon-plus-heading head that a titled
14
- * component build would otherwise rebuild by hand (the shape the removed `splitHead` produced). */
15
- export declare function headRow(title: ElementContent[], icon?: Element): Element;
14
+ /** Card head row: `<div class="ec-head">[icon]<hN class="card-title">{title}</hN></div>`.
15
+ * Pass the title's inline children, an optional pre-built icon element, and an optional heading
16
+ * level (default 2). This factors the icon-plus-heading head that a titled component build would
17
+ * otherwise rebuild by hand (the shape the removed `splitHead` produced). */
18
+ export declare function headRow(title: ElementContent[], icon?: Element, level?: number): Element;
16
19
  /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
17
20
  * text nodes so the bare list serializes without newlines. Returns that <ul>. */
18
21
  export declare function markFirstList(children: ElementContent[]): Element | undefined;
@@ -3,6 +3,12 @@ import { dataAttrProp } from './registry.js';
3
3
  export function isElement(node) {
4
4
  return !!node && node.type === 'element';
5
5
  }
6
+ /** Read a declared string attribute off the component context, returning undefined for a boolean or
7
+ * absent value. Replaces the `typeof ctx.attributes[key] === 'string'` narrowing a build repeats. */
8
+ export function strAttr(ctx, key) {
9
+ const value = ctx.attributes[key];
10
+ return typeof value === 'string' ? value : undefined;
11
+ }
6
12
  // hast Properties values are PropertyValue (string | number | boolean | array | null).
7
13
  // Directive markers (dataPrimitive/dataRole/dataAttr<Key>) are always stamped as strings;
8
14
  // this reads them back with that guarantee instead of casting at each call site.
@@ -19,15 +25,15 @@ export function iconSpan(glyphEl, role) {
19
25
  export function cardShell(classes, body) {
20
26
  return h('section', { className: classes }, [h('div', { className: ['card-body'] }, body)]);
21
27
  }
22
- /** Card head row: `<div class="ec-head">[icon]<h2 class="card-title">{title}</h2></div>`.
23
- * Pass the title's inline children and an optional pre-built icon element, the way `cardShell`
24
- * takes already-built body content. This factors the icon-plus-heading head that a titled
25
- * component build would otherwise rebuild by hand (the shape the removed `splitHead` produced). */
26
- export function headRow(title, icon) {
28
+ /** Card head row: `<div class="ec-head">[icon]<hN class="card-title">{title}</hN></div>`.
29
+ * Pass the title's inline children, an optional pre-built icon element, and an optional heading
30
+ * level (default 2). This factors the icon-plus-heading head that a titled component build would
31
+ * otherwise rebuild by hand (the shape the removed `splitHead` produced). */
32
+ export function headRow(title, icon, level = 2) {
27
33
  const children = [];
28
34
  if (icon)
29
35
  children.push(icon);
30
- children.push(h('h2', { className: ['card-title'] }, title));
36
+ children.push(h(`h${level}`, { className: ['card-title'] }, title));
31
37
  return h('div', { className: ['ec-head'] }, children);
32
38
  }
33
39
  /** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
@@ -60,7 +60,7 @@ export function remarkDirectiveStamp(registry) {
60
60
  const def = registry.get(node.name);
61
61
  const attrs = node.attributes ?? {};
62
62
  const role = attrs.role || undefined;
63
- const iconField = def?.attributes?.find((field) => field.type === 'icon');
63
+ const iconField = registry.iconField(node.name);
64
64
  const iconKey = iconField?.key ?? 'icon';
65
65
  let icon = attrs[iconKey] || undefined;
66
66
  if (!icon && role)
@@ -8,11 +8,12 @@ export interface NavConcept {
8
8
  id: string;
9
9
  label: string;
10
10
  }
11
- /** The admin layout's data: site identity, the signed-in user, the nav, and the active path. */
11
+ /** The admin layout's data: site identity, the signed-in user, the nav, the active path, and theme. */
12
12
  export interface LayoutData {
13
13
  siteName: string;
14
14
  user: {
15
15
  displayName: string;
16
+ email: string;
16
17
  role: Role;
17
18
  };
18
19
  concepts: NavConcept[];
@@ -20,6 +21,11 @@ export interface LayoutData {
20
21
  canManageEditors: boolean;
21
22
  /** The nav menu's label when the site configures one; gates the Navigation nav entry. Null otherwise. */
22
23
  navLabel: string | null;
24
+ /** The admin theme resolved for SSR: the persisted cookie choice, or the light default. */
25
+ theme: 'cairn-admin' | 'cairn-admin-dark';
26
+ /** The nav group labels the user has collapsed, from the persisted cookie. Read at SSR so a
27
+ * collapsed group renders collapsed with no flash. Empty when none are collapsed. */
28
+ collapsedNav: string[];
23
29
  }
24
30
  /** One row in a concept's list view. */
25
31
  export interface EntrySummary {
@@ -72,6 +78,10 @@ export interface ContentEvent {
72
78
  platform?: {
73
79
  env?: GithubKeyEnv;
74
80
  };
81
+ /** SvelteKit's cookie jar; the layout load reads the persisted admin theme. Optional for non-route callers. */
82
+ cookies?: {
83
+ get(name: string): string | undefined;
84
+ };
75
85
  }
76
86
  /** Injectable dependencies; tests stub the token mint to avoid signing a real key. */
77
87
  export interface ContentRoutesDeps {
@@ -86,6 +96,7 @@ export declare function createContentRoutes(runtime: CairnRuntime, deps?: Conten
86
96
  editLoad: (event: ContentEvent) => Promise<EditData>;
87
97
  saveAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
88
98
  deleteAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
99
+ listDeleteAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
89
100
  renameAction: (event: ContentEvent) => Promise<ReturnType<typeof fail> | never>;
90
101
  mintToken: (env: GithubKeyEnv) => Promise<string>;
91
102
  };
@@ -29,16 +29,24 @@ function conceptOf(runtime, params) {
29
29
  }
30
30
  export function createContentRoutes(runtime, deps = {}) {
31
31
  const mintToken = deps.mintToken ?? ((env) => cachedInstallationToken(appCredentials(runtime.backend, env)));
32
- /** Layout load for every admin page: the nav, the user, and the active path. */
32
+ /** Layout load for every admin page: the nav, the user, the active path, and the resolved theme. */
33
33
  function layoutLoad(event) {
34
34
  const editor = sessionOf(event);
35
+ const cookieTheme = event.cookies?.get('cairn-admin-theme');
36
+ const theme = cookieTheme === 'cairn-admin-dark' ? 'cairn-admin-dark' : 'cairn-admin';
37
+ const cookieCollapsed = event.cookies?.get('cairn-admin-nav-collapsed');
38
+ const collapsedNav = cookieCollapsed
39
+ ? cookieCollapsed.split(',').map((part) => decodeURIComponent(part)).filter(Boolean)
40
+ : [];
35
41
  return {
36
42
  siteName: runtime.siteName,
37
- user: { displayName: editor.displayName, role: editor.role },
43
+ user: { displayName: editor.displayName, email: editor.email, role: editor.role },
38
44
  concepts: runtime.concepts.map((c) => ({ id: c.id, label: c.label })),
39
45
  pathname: event.url.pathname,
40
46
  canManageEditors: editor.role === 'owner',
41
47
  navLabel: runtime.navMenu?.label ?? null,
48
+ theme,
49
+ collapsedNav,
42
50
  };
43
51
  }
44
52
  /** Redirect /admin to the first concept's list (spec §7.6: land on the first concept). */
@@ -251,15 +259,12 @@ export function createContentRoutes(runtime, deps = {}) {
251
259
  const savedQuery = draft.length ? `saved=1&drafts=${encodeURIComponent(draft.join(','))}` : 'saved=1';
252
260
  throw redirect(303, `/admin/${concept.id}/${id}?${savedQuery}`);
253
261
  }
254
- /** Delete an entry. Block-until-clean: refuse while inbound links exist (naming them), else commit
255
- * the file removal and the manifest patch in one commit. The inbound recheck here is the
256
- * authoritative gate, closing the load-to-delete race. */
257
- async function deleteAction(event) {
258
- const editor = sessionOf(event);
259
- const concept = conceptOf(runtime, event.params);
260
- const id = event.params.id ?? '';
261
- if (!isValidId(id))
262
- throw error(400, 'Invalid entry id');
262
+ /** The shared delete core. Block-until-clean: refuse while inbound links exist (naming them), else
263
+ * commit the file removal and the manifest patch in one commit. The inbound recheck here is the
264
+ * authoritative gate, closing the load-to-delete race. Both the editor delete (id from params) and
265
+ * the list delete (id from the form body) call this with an already-validated id, so the guard is
266
+ * enforced once. */
267
+ async function deleteEntry(event, concept, id, editor) {
263
268
  const path = `${concept.dir}/${filenameFromId(id)}`;
264
269
  const token = await mintToken(event.platform?.env ?? {});
265
270
  // An absent manifest degrades the inbound gate to "allow": with no manifest there is nothing to
@@ -268,7 +273,7 @@ export function createContentRoutes(runtime, deps = {}) {
268
273
  const manifest = manifestRaw === null ? emptyManifest() : parseManifest(manifestRaw);
269
274
  const inbound = inboundLinks(manifest, concept.id, id);
270
275
  if (inbound.length) {
271
- return fail(409, { inboundLinks: inbound });
276
+ return fail(409, { inboundLinks: inbound, id });
272
277
  }
273
278
  const nextManifest = serializeManifest(removeEntry(manifest, concept.id, id));
274
279
  try {
@@ -286,6 +291,25 @@ export function createContentRoutes(runtime, deps = {}) {
286
291
  }
287
292
  throw redirect(303, `/admin/${concept.id}`);
288
293
  }
294
+ /** Delete an entry from its editor. The id comes from the route param. */
295
+ async function deleteAction(event) {
296
+ const editor = sessionOf(event);
297
+ const concept = conceptOf(runtime, event.params);
298
+ const id = event.params.id ?? '';
299
+ if (!isValidId(id))
300
+ throw error(400, 'Invalid entry id');
301
+ return deleteEntry(event, concept, id, editor);
302
+ }
303
+ /** Delete an entry from the concept list. The id comes from the form body. */
304
+ async function listDeleteAction(event) {
305
+ const editor = sessionOf(event);
306
+ const concept = conceptOf(runtime, event.params);
307
+ const form = await event.request.formData();
308
+ const id = String(form.get('id') ?? '');
309
+ if (!isValidId(id))
310
+ throw error(400, 'Invalid entry id');
311
+ return deleteEntry(event, concept, id, editor);
312
+ }
289
313
  /** Rename an entry: change its slug, move the file, and rewrite every inbound cairn token in one
290
314
  * atomic commit, so no internal link breaks. The collision check and the inbound recompute here
291
315
  * are the authoritative gate. The same last-writer-wins manifest race as save and delete applies,
@@ -364,5 +388,5 @@ export function createContentRoutes(runtime, deps = {}) {
364
388
  }
365
389
  throw redirect(303, `/admin/${concept.id}/${newId}?renamed=1`);
366
390
  }
367
- return { layoutLoad, indexRedirect, listLoad, createAction, editLoad, saveAction, deleteAction, renameAction, mintToken };
391
+ return { layoutLoad, indexRedirect, listLoad, createAction, editLoad, saveAction, deleteAction, listDeleteAction, renameAction, mintToken };
368
392
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@glw907/cairn-cms",
3
- "version": "0.29.0",
3
+ "version": "0.33.0",
4
4
  "description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
5
5
  "type": "module",
6
6
  "sideEffects": [
@@ -26,9 +26,10 @@
26
26
  "markdown"
27
27
  ],
28
28
  "scripts": {
29
- "package": "svelte-package && chmod +x dist/vite/bin.js",
29
+ "package": "svelte-package && node scripts/build-admin-css.mjs && chmod +x dist/vite/bin.js",
30
30
  "check:package": "npm run package && publint --strict && attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm internal-resolution-error",
31
31
  "check:reference": "npm run package && node scripts/reference-coverage.mjs",
32
+ "check:docs": "node scripts/docs-links.mjs",
32
33
  "prepare": "npm run package",
33
34
  "check": "svelte-check --tsconfig ./tsconfig.json",
34
35
  "test": "vitest run",
@@ -53,6 +54,11 @@
53
54
  "svelte": "./dist/components/index.js",
54
55
  "default": "./dist/components/index.js"
55
56
  },
57
+ "./render": {
58
+ "types": "./dist/render/authoring.d.ts",
59
+ "svelte": "./dist/render/authoring.js",
60
+ "default": "./dist/render/authoring.js"
61
+ },
56
62
  "./delivery": {
57
63
  "types": "./dist/delivery/index.d.ts",
58
64
  "svelte": "./dist/delivery/index.js",
@@ -93,6 +99,7 @@
93
99
  "@codemirror/language": "^6.12.3",
94
100
  "@codemirror/state": "^6.6.0",
95
101
  "@codemirror/view": "^6.43.0",
102
+ "@lucide/svelte": "^1.17.0",
96
103
  "@rodrigodagostino/svelte-sortable-list": "^2.1.17",
97
104
  "@types/hast": "^3.0.4",
98
105
  "@types/mdast": "^4.0.4",
@@ -121,13 +128,19 @@
121
128
  "@sveltejs/kit": "^2.61",
122
129
  "@sveltejs/package": "^2",
123
130
  "@sveltejs/vite-plugin-svelte": "^7.1",
131
+ "@tailwindcss/postcss": "^4.3.0",
124
132
  "@types/node": "^22.19.19",
125
133
  "@vitest/browser": "^4.1.7",
126
134
  "@vitest/browser-playwright": "^4.1.7",
135
+ "daisyui": "^5.5.23",
136
+ "lightningcss": "^1.32.0",
127
137
  "playwright": "^1.60.0",
138
+ "postcss": "^8.5.15",
139
+ "postcss-prefix-selector": "^2.1.1",
128
140
  "publint": "^0.3.21",
129
141
  "svelte": "^5.55",
130
142
  "svelte-check": "^4",
143
+ "tailwindcss": "^4.3.0",
131
144
  "typescript": "^6.0.3",
132
145
  "vite": "^8.0",
133
146
  "vitest": "^4.1",