@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.
- package/CHANGELOG.md +111 -0
- package/dist/components/AdminLayout.svelte +372 -44
- package/dist/components/AdminLayout.svelte.d.ts +5 -4
- 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/ConceptList.svelte +240 -45
- package/dist/components/ConceptList.svelte.d.ts +12 -2
- package/dist/components/ConfirmPage.svelte +20 -3
- package/dist/components/EditPage.svelte +12 -7
- package/dist/components/LoginPage.svelte +27 -5
- package/dist/components/ManageEditors.svelte +8 -5
- package/dist/components/NavTree.svelte +2 -2
- 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/index.d.ts +0 -2
- package/dist/index.js +4 -1
- package/dist/render/authoring.d.ts +3 -0
- package/dist/render/authoring.js +5 -0
- package/dist/render/registry.d.ts +2 -0
- package/dist/render/registry.js +15 -0
- package/dist/render/rehype-dispatch.d.ts +9 -6
- package/dist/render/rehype-dispatch.js +12 -6
- package/dist/render/remark-directives.js +1 -1
- package/dist/sveltekit/content-routes.d.ts +12 -1
- package/dist/sveltekit/content-routes.js +37 -13
- package/dist/sveltekit/guard.js +32 -0
- package/dist/sveltekit/https-required-page.d.ts +5 -0
- package/dist/sveltekit/https-required-page.js +216 -0
- 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/index.ts +4 -2
- package/src/lib/render/authoring.ts +7 -0
- 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/sveltekit/content-routes.ts +51 -14
- package/src/lib/sveltekit/guard.ts +36 -0
- package/src/lib/sveltekit/https-required-page.ts +220 -0
|
@@ -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.
|
|
Binary file
|
|
Binary file
|
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
|
-
|
|
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,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
|
package/dist/render/registry.js
CHANGED
|
@@ -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]<
|
|
12
|
-
* Pass the title's inline children
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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]<
|
|
23
|
-
* Pass the title's inline children
|
|
24
|
-
*
|
|
25
|
-
*
|
|
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(
|
|
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 =
|
|
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,
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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/dist/sveltekit/guard.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import { redirect, error } from '@sveltejs/kit';
|
|
5
5
|
import { resolveSession } from '../auth/store.js';
|
|
6
6
|
import { sessionCookieName } from '../auth/crypto.js';
|
|
7
|
+
import { httpsRequiredPage } from './https-required-page.js';
|
|
7
8
|
/** The login page and the auth endpoints are public; everything else under /admin is gated. */
|
|
8
9
|
function isPublicAdminPath(pathname) {
|
|
9
10
|
return pathname === '/admin/login' || pathname.startsWith('/admin/auth/');
|
|
@@ -11,6 +12,19 @@ function isPublicAdminPath(pathname) {
|
|
|
11
12
|
function isAdminPath(pathname) {
|
|
12
13
|
return pathname === '/admin' || pathname.startsWith('/admin/');
|
|
13
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Local development (`wrangler dev`) legitimately speaks http; a deployed host does not. The hostname
|
|
17
|
+
* comes from the client `Host` header, so this is UX only: it decides whether to show the help page,
|
|
18
|
+
* never whether to grant access. The session gate below runs regardless. Do not make it an auth check.
|
|
19
|
+
*/
|
|
20
|
+
function isLocalHost(hostname) {
|
|
21
|
+
return (hostname === 'localhost' ||
|
|
22
|
+
hostname === '127.0.0.1' ||
|
|
23
|
+
hostname === '0.0.0.0' ||
|
|
24
|
+
hostname === '::1' ||
|
|
25
|
+
hostname === '[::1]' ||
|
|
26
|
+
hostname.endsWith('.localhost'));
|
|
27
|
+
}
|
|
14
28
|
/**
|
|
15
29
|
* Attach the baseline security headers to an admin response. No full CSP; see the auth-hardening
|
|
16
30
|
* design. frame-ancestors is the modern clickjacking control and the one CSP directive included.
|
|
@@ -23,12 +37,30 @@ function applySecurityHeaders(headers) {
|
|
|
23
37
|
headers.set('Strict-Transport-Security', 'max-age=63072000; includeSubDomains');
|
|
24
38
|
headers.set('Permissions-Policy', 'camera=(), microphone=(), geolocation=()');
|
|
25
39
|
}
|
|
40
|
+
/** The hardened 400 help page for a deployed admin request that arrived over http. */
|
|
41
|
+
function httpsRequiredResponse(url) {
|
|
42
|
+
const httpsUrl = new URL(url);
|
|
43
|
+
httpsUrl.protocol = 'https:';
|
|
44
|
+
const headers = new Headers({
|
|
45
|
+
'Content-Type': 'text/html; charset=utf-8',
|
|
46
|
+
'Cache-Control': 'no-store',
|
|
47
|
+
});
|
|
48
|
+
applySecurityHeaders(headers);
|
|
49
|
+
return new Response(httpsRequiredPage(httpsUrl.toString()), { status: 400, headers });
|
|
50
|
+
}
|
|
26
51
|
/** The SvelteKit `Handle` that guards `/admin/**` and hardens admin responses. */
|
|
27
52
|
export function createAuthGuard() {
|
|
28
53
|
return async function handle({ event, resolve }) {
|
|
29
54
|
const { pathname } = event.url;
|
|
30
55
|
if (!isAdminPath(pathname))
|
|
31
56
|
return resolve(event);
|
|
57
|
+
// A deployed admin request over http never works: the magic-link form POST would fail the
|
|
58
|
+
// framework's CSRF guard with an opaque 403. Serve the help page instead, before resolve()
|
|
59
|
+
// runs that check. This covers the public login/auth paths too, since that is where the form
|
|
60
|
+
// posts. Local http (wrangler dev) is exempt.
|
|
61
|
+
if (event.url.protocol === 'http:' && !isLocalHost(event.url.hostname)) {
|
|
62
|
+
return httpsRequiredResponse(event.url);
|
|
63
|
+
}
|
|
32
64
|
if (!isPublicAdminPath(pathname)) {
|
|
33
65
|
const env = event.platform?.env ?? {};
|
|
34
66
|
const id = event.cookies.get(sessionCookieName(event.url.protocol === 'https:'));
|