@glw907/cairn-cms 0.4.0 → 0.5.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/README.md +4 -4
- package/dist/adapter.d.ts +10 -1
- package/dist/adapter.d.ts.map +1 -1
- package/dist/auth/config.d.ts +9 -9
- package/dist/auth/config.d.ts.map +1 -1
- package/dist/auth/config.js +5 -5
- package/dist/auth/guard.d.ts +1 -1
- package/dist/auth/guard.d.ts.map +1 -1
- package/dist/auth/guard.js +2 -2
- package/dist/carta.d.ts +1 -1
- package/dist/carta.d.ts.map +1 -1
- package/dist/components/AdminLayout.svelte +3 -3
- package/dist/components/AdminList.svelte +1 -1
- package/dist/components/ConfirmPage.svelte +2 -2
- package/dist/components/EditPage.svelte +5 -5
- package/dist/components/LoginPage.svelte +5 -5
- package/dist/email.js +4 -4
- package/dist/github.d.ts +22 -2
- package/dist/github.d.ts.map +1 -1
- package/dist/github.js +40 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/render/glyph.d.ts +6 -0
- package/dist/render/glyph.d.ts.map +1 -0
- package/dist/render/glyph.js +5 -0
- package/dist/render/index.d.ts +6 -0
- package/dist/render/index.d.ts.map +1 -0
- package/dist/render/index.js +8 -0
- package/dist/render/pipeline.d.ts +16 -0
- package/dist/render/pipeline.d.ts.map +1 -0
- package/dist/render/pipeline.js +29 -0
- package/dist/render/registry.d.ts +28 -0
- package/dist/render/registry.d.ts.map +1 -0
- package/dist/render/registry.js +11 -0
- package/dist/render/rehype-dispatch.d.ts +24 -0
- package/dist/render/rehype-dispatch.d.ts.map +1 -0
- package/dist/render/rehype-dispatch.js +86 -0
- package/dist/render/remark-directives.d.ts +4 -0
- package/dist/render/remark-directives.d.ts.map +1 -0
- package/dist/render/remark-directives.js +74 -0
- package/dist/sveltekit/index.d.ts +17 -2
- package/dist/sveltekit/index.d.ts.map +1 -1
- package/dist/sveltekit/index.js +33 -6
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +2 -2
- package/package.json +15 -3
- package/src/lib/adapter.ts +12 -3
- package/src/lib/auth/config.ts +6 -6
- package/src/lib/auth/guard.ts +3 -3
- package/src/lib/carta.ts +2 -2
- package/src/lib/components/AdminLayout.svelte +3 -3
- package/src/lib/components/AdminList.svelte +1 -1
- package/src/lib/components/ConfirmPage.svelte +2 -2
- package/src/lib/components/EditPage.svelte +5 -5
- package/src/lib/components/LoginPage.svelte +5 -5
- package/src/lib/email.ts +4 -4
- package/src/lib/github.ts +38 -6
- package/src/lib/index.ts +1 -0
- package/src/lib/render/glyph.ts +14 -0
- package/src/lib/render/index.ts +8 -0
- package/src/lib/render/pipeline.ts +37 -0
- package/src/lib/render/registry.ts +36 -0
- package/src/lib/render/rehype-dispatch.ts +97 -0
- package/src/lib/render/remark-directives.ts +71 -0
- package/src/lib/sveltekit/index.ts +54 -13
- package/src/lib/utils.ts +2 -2
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Element } from 'hast';
|
|
2
|
+
/** A site component: how it inserts (editor) and how it renders (rehype). */
|
|
3
|
+
export interface ComponentDef {
|
|
4
|
+
/** Directive name, e.g. 'card' (matches `:::card`). */
|
|
5
|
+
name: string;
|
|
6
|
+
/** Palette label. */
|
|
7
|
+
label: string;
|
|
8
|
+
/** Palette description. */
|
|
9
|
+
description: string;
|
|
10
|
+
/** Markdown scaffold inserted at the cursor by the editor palette. */
|
|
11
|
+
insertTemplate: string;
|
|
12
|
+
/** Build the final hast element from the stamped directive element. */
|
|
13
|
+
build: (node: Element, rise?: string) => Element;
|
|
14
|
+
/** Optional role→default-icon (e.g. `{ caution: 'warning' }`). */
|
|
15
|
+
defaultIconByRole?: Record<string, string>;
|
|
16
|
+
}
|
|
17
|
+
export interface ComponentRegistry {
|
|
18
|
+
defs: ComponentDef[];
|
|
19
|
+
names: string[];
|
|
20
|
+
get(name: string): ComponentDef | undefined;
|
|
21
|
+
defaultIcon(name: string, role?: string): string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** Build a registry from a site's component definitions. The single source the
|
|
24
|
+
* render pipeline (directive stamp + rehype dispatch) and the editor palette read. */
|
|
25
|
+
export declare function defineRegistry(input: {
|
|
26
|
+
components: ComponentDef[];
|
|
27
|
+
}): ComponentRegistry;
|
|
28
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/lib/render/registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AAEpC,6EAA6E;AAC7E,MAAM,WAAW,YAAY;IAC5B,uDAAuD;IACvD,IAAI,EAAE,MAAM,CAAC;IACb,qBAAqB;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,2BAA2B;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,sEAAsE;IACtE,cAAc,EAAE,MAAM,CAAC;IACvB,uEAAuE;IACvE,KAAK,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;IACjD,kEAAkE;IAClE,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,iBAAiB;IACjC,IAAI,EAAE,YAAY,EAAE,CAAC;IACrB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,GAAG,CAAC,IAAI,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;IAC5C,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;CAC7D;AAED;uFACuF;AACvF,wBAAgB,cAAc,CAAC,KAAK,EAAE;IAAE,UAAU,EAAE,YAAY,EAAE,CAAA;CAAE,GAAG,iBAAiB,CAQvF"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** Build a registry from a site's component definitions. The single source the
|
|
2
|
+
* render pipeline (directive stamp + rehype dispatch) and the editor palette read. */
|
|
3
|
+
export function defineRegistry(input) {
|
|
4
|
+
const byName = new Map(input.components.map((c) => [c.name, c]));
|
|
5
|
+
return {
|
|
6
|
+
defs: input.components,
|
|
7
|
+
names: input.components.map((c) => c.name),
|
|
8
|
+
get: (name) => byName.get(name),
|
|
9
|
+
defaultIcon: (name, role) => (role ? byName.get(name)?.defaultIconByRole?.[role] : undefined),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { Root, Element, ElementContent } from 'hast';
|
|
2
|
+
import type { ComponentRegistry } from './registry';
|
|
3
|
+
export declare function isElement(node: ElementContent | undefined): node is Element;
|
|
4
|
+
export declare function strProp(node: Element, name: string): string | undefined;
|
|
5
|
+
/** Wrap a pre-built glyph in an ec-icon span; secondary role adds the modifier. */
|
|
6
|
+
export declare function iconSpan(glyphEl: Element, role?: string): Element;
|
|
7
|
+
/** A site's icon factory: turn a stamped icon name + role into a hast element. */
|
|
8
|
+
export type MakeIcon = (name: string, role?: string) => Element;
|
|
9
|
+
export declare function splitHead(node: Element, makeIcon?: MakeIcon): {
|
|
10
|
+
head: Element;
|
|
11
|
+
rest: ElementContent[];
|
|
12
|
+
};
|
|
13
|
+
/** Section wrapper: `<section class=…><div class="card-body">…</div></section>`,
|
|
14
|
+
* with an optional inline rise style. */
|
|
15
|
+
export declare function cardShell(classes: string[], rise: string | undefined, body: ElementContent[]): Element;
|
|
16
|
+
/** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
|
|
17
|
+
* text nodes so the bare list serializes without newlines. Returns that <ul>. */
|
|
18
|
+
export declare function markFirstList(children: ElementContent[]): Element | undefined;
|
|
19
|
+
/** Rehype transformer: dispatch each stamped element through its registry `build`
|
|
20
|
+
* fn. Top-level primitives get a document-order rise stagger when `rise` is
|
|
21
|
+
* supplied (a site's per-index motion formula); nested ones don't. Non-primitive
|
|
22
|
+
* content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
|
|
23
|
+
export declare function rehypeDispatch(registry: ComponentRegistry, rise?: (idx: number) => string): (tree: Root) => void;
|
|
24
|
+
//# sourceMappingURL=rehype-dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rehype-dispatch.d.ts","sourceRoot":"","sources":["../../src/lib/render/rehype-dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,cAAc,EAAc,MAAM,MAAM,CAAC;AAEtE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEpD,wBAAgB,SAAS,CAAC,IAAI,EAAE,cAAc,GAAG,SAAS,GAAG,IAAI,IAAI,OAAO,CAE3E;AAKD,wBAAgB,OAAO,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAGvE;AAED,mFAAmF;AACnF,wBAAgB,QAAQ,CAAC,OAAO,EAAE,OAAO,EAAE,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAGjE;AAED,kFAAkF;AAClF,MAAM,MAAM,QAAQ,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC;AAMhE,wBAAgB,SAAS,CAAC,IAAI,EAAE,OAAO,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,cAAc,EAAE,CAAA;CAAE,CAYvG;AAED;0CAC0C;AAC1C,wBAAgB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,EAAE,IAAI,EAAE,cAAc,EAAE,GAAG,OAAO,CAItG;AAED;kFACkF;AAClF,wBAAgB,aAAa,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,OAAO,GAAG,SAAS,CAS7E;AAmBD;;;mFAGmF;AACnF,wBAAgB,cAAc,CAAC,QAAQ,EAAE,iBAAiB,EAAE,IAAI,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,MAAM,IACjF,MAAM,IAAI,UAUlB"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { h } from 'hastscript';
|
|
2
|
+
export function isElement(node) {
|
|
3
|
+
return !!node && node.type === 'element';
|
|
4
|
+
}
|
|
5
|
+
// hast Properties values are PropertyValue (string | number | boolean | array | null).
|
|
6
|
+
// Directive markers (dataIcon/dataRole/dataPrimitive) are always stamped as strings;
|
|
7
|
+
// this reads them back with that guarantee instead of casting at each call site.
|
|
8
|
+
export function strProp(node, name) {
|
|
9
|
+
const value = node.properties?.[name];
|
|
10
|
+
return typeof value === 'string' ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
/** Wrap a pre-built glyph in an ec-icon span; secondary role adds the modifier. */
|
|
13
|
+
export function iconSpan(glyphEl, role) {
|
|
14
|
+
const className = role === 'secondary' ? ['ec-icon', 'ec-icon-secondary'] : ['ec-icon'];
|
|
15
|
+
return h('span', { className }, [glyphEl]);
|
|
16
|
+
}
|
|
17
|
+
// Pull the section's <h2> out, retag it .card-title, and build the .ec-head row
|
|
18
|
+
// (optional icon + heading). Returns the head plus the remaining body children.
|
|
19
|
+
// `makeIcon` (site-supplied) turns the stamped data-icon into an element; omit it
|
|
20
|
+
// for a head with no icon.
|
|
21
|
+
export function splitHead(node, makeIcon) {
|
|
22
|
+
const children = node.children;
|
|
23
|
+
const i = children.findIndex((c) => isElement(c) && c.tagName === 'h2');
|
|
24
|
+
const h2 = children[i];
|
|
25
|
+
h2.properties = { ...h2.properties, className: ['card-title'] };
|
|
26
|
+
const rest = children.filter((_, j) => j !== i);
|
|
27
|
+
const icon = strProp(node, 'dataIcon');
|
|
28
|
+
const role = strProp(node, 'dataRole');
|
|
29
|
+
const headKids = [];
|
|
30
|
+
if (makeIcon && icon)
|
|
31
|
+
headKids.push(makeIcon(icon, role));
|
|
32
|
+
headKids.push(h2);
|
|
33
|
+
return { head: h('div', { className: ['ec-head'] }, headKids), rest };
|
|
34
|
+
}
|
|
35
|
+
/** Section wrapper: `<section class=…><div class="card-body">…</div></section>`,
|
|
36
|
+
* with an optional inline rise style. */
|
|
37
|
+
export function cardShell(classes, rise, body) {
|
|
38
|
+
const properties = { className: classes };
|
|
39
|
+
if (rise)
|
|
40
|
+
properties.style = rise;
|
|
41
|
+
return h('section', properties, [h('div', { className: ['card-body'] }, body)]);
|
|
42
|
+
}
|
|
43
|
+
/** Tag the first <ul> among children with `ec-grid` and strip its whitespace-only
|
|
44
|
+
* text nodes so the bare list serializes without newlines. Returns that <ul>. */
|
|
45
|
+
export function markFirstList(children) {
|
|
46
|
+
const ul = children.find((c) => isElement(c) && c.tagName === 'ul');
|
|
47
|
+
if (ul) {
|
|
48
|
+
ul.properties = { ...ul.properties, className: ['ec-grid'] };
|
|
49
|
+
ul.children = ul.children.filter((c) => !(c.type === 'text' && /^\s*$/.test(c.value)));
|
|
50
|
+
}
|
|
51
|
+
return ul;
|
|
52
|
+
}
|
|
53
|
+
// Recurse into a node's children, transforming any nested primitive sections
|
|
54
|
+
// (a grid inside a card, panels inside a split) WITHOUT a rise stagger.
|
|
55
|
+
function transformChildren(children, registry) {
|
|
56
|
+
return children.map((c) => {
|
|
57
|
+
if (isElement(c) && c.properties?.dataPrimitive)
|
|
58
|
+
return transformNode(c, registry);
|
|
59
|
+
if (isElement(c))
|
|
60
|
+
c.children = transformChildren(c.children, registry);
|
|
61
|
+
return c;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
function transformNode(node, registry, rise) {
|
|
65
|
+
node.children = transformChildren(node.children, registry);
|
|
66
|
+
const name = strProp(node, 'dataPrimitive');
|
|
67
|
+
const def = name ? registry.get(name) : undefined;
|
|
68
|
+
return def ? def.build(node, rise) : node;
|
|
69
|
+
}
|
|
70
|
+
/** Rehype transformer: dispatch each stamped element through its registry `build`
|
|
71
|
+
* fn. Top-level primitives get a document-order rise stagger when `rise` is
|
|
72
|
+
* supplied (a site's per-index motion formula); nested ones don't. Non-primitive
|
|
73
|
+
* content (lede, intro paragraphs, the page-toc nav) passes through untouched. */
|
|
74
|
+
export function rehypeDispatch(registry, rise) {
|
|
75
|
+
return (tree) => {
|
|
76
|
+
let idx = 0;
|
|
77
|
+
tree.children = tree.children.map((child) => {
|
|
78
|
+
if (isElement(child) && child.properties?.dataPrimitive) {
|
|
79
|
+
return transformNode(child, registry, rise ? rise(idx++) : undefined);
|
|
80
|
+
}
|
|
81
|
+
if (isElement(child))
|
|
82
|
+
child.children = transformChildren(child.children, registry);
|
|
83
|
+
return child;
|
|
84
|
+
});
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remark-directives.d.ts","sourceRoot":"","sources":["../../src/lib/render/remark-directives.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA8B,IAAI,EAAQ,MAAM,OAAO,CAAC;AAGpE,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAmCpD,wBAAgB,oBAAoB,CAAC,QAAQ,EAAE,iBAAiB,IAEvD,MAAM,IAAI,UA8BlB"}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { visit } from 'unist-util-visit';
|
|
2
|
+
// Reconstruct a directive's authored attribute block (`{#id .class key="value"}`).
|
|
3
|
+
// Accidental prose directives carry none, so this is almost always empty.
|
|
4
|
+
function serializeAttributes(attributes) {
|
|
5
|
+
if (!attributes)
|
|
6
|
+
return '';
|
|
7
|
+
const tokens = [];
|
|
8
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
9
|
+
if (value == null)
|
|
10
|
+
tokens.push(key);
|
|
11
|
+
else if (key === 'id')
|
|
12
|
+
tokens.push(`#${value}`);
|
|
13
|
+
else if (key === 'class')
|
|
14
|
+
for (const c of value.split(/\s+/).filter(Boolean))
|
|
15
|
+
tokens.push(`.${c}`);
|
|
16
|
+
else
|
|
17
|
+
tokens.push(`${key}="${value}"`);
|
|
18
|
+
}
|
|
19
|
+
return tokens.length ? `{${tokens.join(' ')}}` : '';
|
|
20
|
+
}
|
|
21
|
+
// The vocabulary is container-only (`:::name`). A text directive (`:name`) or
|
|
22
|
+
// leaf directive (`::name`) is therefore always an accidental colon in prose
|
|
23
|
+
// ("4:00", "9:30", "ratio 16:9") that micromark tokenized as a directive.
|
|
24
|
+
// Restore it to its literal source text so prose renders verbatim.
|
|
25
|
+
function restoreLiteral(node) {
|
|
26
|
+
const marker = node.type === 'leafDirective' ? '::' : ':';
|
|
27
|
+
const attrs = serializeAttributes(node.attributes);
|
|
28
|
+
if (node.children.length === 0) {
|
|
29
|
+
return [{ type: 'text', value: marker + node.name + attrs }];
|
|
30
|
+
}
|
|
31
|
+
const open = { type: 'text', value: `${marker}${node.name}[` };
|
|
32
|
+
const close = { type: 'text', value: `]${attrs}` };
|
|
33
|
+
return [open, ...node.children, close];
|
|
34
|
+
}
|
|
35
|
+
// Stamp each registered container directive with data-* markers carrying its
|
|
36
|
+
// component name, icon, and role. No structure is built here; the rehype
|
|
37
|
+
// dispatcher rewrites the marked elements once their children are hast.
|
|
38
|
+
// Text and leaf directives are restored to literal text (accidental prose colons).
|
|
39
|
+
export function remarkDirectiveStamp(registry) {
|
|
40
|
+
const known = new Set(registry.names);
|
|
41
|
+
return (tree) => {
|
|
42
|
+
visit(tree, 'containerDirective', (node) => {
|
|
43
|
+
if (!known.has(node.name))
|
|
44
|
+
return;
|
|
45
|
+
const attrs = node.attributes ?? {};
|
|
46
|
+
const role = attrs.role || undefined;
|
|
47
|
+
let icon = attrs.icon || undefined;
|
|
48
|
+
if (!icon && role)
|
|
49
|
+
icon = registry.defaultIcon(node.name, role);
|
|
50
|
+
const properties = { dataPrimitive: node.name };
|
|
51
|
+
if (icon)
|
|
52
|
+
properties.dataIcon = icon;
|
|
53
|
+
if (role)
|
|
54
|
+
properties.dataRole = role;
|
|
55
|
+
const data = node.data ?? (node.data = {});
|
|
56
|
+
data.hName = 'div';
|
|
57
|
+
data.hProperties = properties;
|
|
58
|
+
});
|
|
59
|
+
visit(tree, ['textDirective', 'leafDirective'], (node, index, parent) => {
|
|
60
|
+
if (!parent || index == null)
|
|
61
|
+
return;
|
|
62
|
+
const literal = restoreLiteral(node);
|
|
63
|
+
if (node.type === 'leafDirective') {
|
|
64
|
+
// Leaf directives sit at block level; wrap the restored text in a paragraph.
|
|
65
|
+
const paragraph = { type: 'paragraph', children: literal };
|
|
66
|
+
parent.children.splice(index, 1, paragraph);
|
|
67
|
+
}
|
|
68
|
+
else {
|
|
69
|
+
parent.children.splice(index, 1, ...literal);
|
|
70
|
+
}
|
|
71
|
+
return index;
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { CairnUser } from '../auth/guard';
|
|
2
2
|
import { type RepoFile } from '../github';
|
|
3
3
|
import { type CairnAdapter, type CairnField } from '../adapter';
|
|
4
|
-
/** The `platform.env` bindings the content routes read. All optional
|
|
4
|
+
/** The `platform.env` bindings the content routes read. All optional; the handlers guard. */
|
|
5
5
|
export interface AdminEnv {
|
|
6
6
|
GITHUB_APP_ID?: string;
|
|
7
7
|
GITHUB_APP_INSTALLATION_ID?: string;
|
|
@@ -19,7 +19,7 @@ export interface AdminLayoutData {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Branding + session for every admin page. `siteName` flows from the adapter without pulling
|
|
22
|
-
* its plugin graph into client bundles
|
|
22
|
+
* its plugin graph into client bundles; the import stays server-side in the layout load.
|
|
23
23
|
* `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
|
|
24
24
|
* (those kit virtual modules have no types outside a kit app, so they can't live in the
|
|
25
25
|
* package); reading `event.url` here also opts the layout load into rerunning on navigation.
|
|
@@ -65,5 +65,20 @@ export declare function saveCommit(event: PlatformEvent & {
|
|
|
65
65
|
user: CairnUser | null;
|
|
66
66
|
};
|
|
67
67
|
}, adapter: CairnAdapter): Promise<never>;
|
|
68
|
+
export interface HealthData {
|
|
69
|
+
ok: boolean;
|
|
70
|
+
checks: {
|
|
71
|
+
githubAppSigning: {
|
|
72
|
+
ok: boolean;
|
|
73
|
+
detail?: string;
|
|
74
|
+
};
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Deploy-time health check (M2): signs a dummy App JWT to prove the GitHub App key loads and
|
|
79
|
+
* the PKCS#1→PKCS#8 conversion still works, before an editor hits it on save. Behind the
|
|
80
|
+
* `/admin` guard (signed-in editors only); returns ok/fail with no secret in the body.
|
|
81
|
+
*/
|
|
82
|
+
export declare function healthLoad(event: PlatformEvent): Promise<HealthData>;
|
|
68
83
|
export {};
|
|
69
84
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/lib/sveltekit/index.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,eAAe,CAAC;AAC/C,OAAO,EAOL,KAAK,QAAQ,EACd,MAAM,WAAW,CAAC;AAEnB,OAAO,EAAuC,KAAK,YAAY,EAAE,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAErG,6FAA6F;AAC7F,MAAM,WAAW,QAAQ;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,0BAA0B,CAAC,EAAE,MAAM,CAAC;IACpC,0BAA0B,CAAC,EAAE,MAAM,CAAC;CACrC;AAED,UAAU,aAAa;IACrB,QAAQ,CAAC,EAAE;QAAE,GAAG,CAAC,EAAE,QAAQ,CAAA;KAAE,CAAC;CAC/B;AA2BD,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EACvD,OAAO,EAAE,YAAY,GACpB,eAAe,CAEjB;AAID,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,4FAA4F;AAC5F,wBAAsB,aAAa,CACjC,KAAK,EAAE,aAAa,EACpB,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC;IAAE,WAAW,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC,CAajD;AAID,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,wBAAsB,QAAQ,CAC5B,KAAK,EAAE,aAAa,GAAG;IAAE,MAAM,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,EAAE,EAAE,MAAM,CAAA;KAAE,CAAC;IAAC,GAAG,EAAE,GAAG,CAAA;CAAE,EACzE,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,QAAQ,CAAC,CAyBnB;AAID,wBAAsB,UAAU,CAC9B,KAAK,EAAE,aAAa,GAAG;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE;QAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAA;KAAE,CAAA;CAAE,EAC/E,OAAO,EAAE,YAAY,GACpB,OAAO,CAAC,KAAK,CAAC,CAoDhB;AAID,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE;QAAE,gBAAgB,EAAE;YAAE,EAAE,EAAE,OAAO,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,CAAC;CAChE;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAAC,KAAK,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAS1E"}
|
package/dist/sveltekit/index.js
CHANGED
|
@@ -2,22 +2,22 @@
|
|
|
2
2
|
// route files are thin shims (`export const load = (event) => editLoad(event, cairn)`).
|
|
3
3
|
//
|
|
4
4
|
// SvelteKit's filesystem routing requires the route *files* to live in each site's
|
|
5
|
-
// `src/routes/`, but their bodies are identical across sites
|
|
5
|
+
// `src/routes/`, but their bodies are identical across sites. Only the adapter differs.
|
|
6
6
|
// These functions take the SvelteKit event (typed structurally, to avoid depending on the
|
|
7
7
|
// site-generated `App.*` ambient types) plus the site `CairnAdapter`, and throw
|
|
8
8
|
// `redirect`/`error` from `@sveltejs/kit` (a peer dependency, so the thrown objects share
|
|
9
|
-
// class identity with the host's runtime
|
|
9
|
+
// class identity with the host's runtime; otherwise the redirect 500s). Auth/session/manage-editors
|
|
10
10
|
// logic lives under `@glw907/cairn-cms/auth`; this module is content-only (list/edit/save).
|
|
11
11
|
import { redirect, error } from '@sveltejs/kit';
|
|
12
12
|
import matter from 'gray-matter';
|
|
13
|
-
import { listMarkdown, readRaw, commitFile, installationToken } from '../github';
|
|
13
|
+
import { listMarkdown, readRaw, commitFile, installationToken, signingSelfTest, CommitConflictError, } from '../github';
|
|
14
14
|
import { serializeMarkdown } from '../content';
|
|
15
15
|
import { findCollection, frontmatterFromForm } from '../adapter';
|
|
16
16
|
/**
|
|
17
17
|
* Mint a GitHub App installation token for *reads* when the App is configured, else undefined
|
|
18
18
|
* (reads then fall back to anonymous). Authenticated reads get the 5000/hr limit; anonymous
|
|
19
19
|
* reads share GitHub's 60/hr-per-IP budget across Cloudflare's egress IPs, so they 403 in prod.
|
|
20
|
-
* A mint failure degrades gracefully to anonymous rather than 500ing
|
|
20
|
+
* A mint failure degrades gracefully to anonymous rather than 500ing. Unlike the commit path,
|
|
21
21
|
* where a missing App is fatal, a read can still succeed unauthenticated.
|
|
22
22
|
*/
|
|
23
23
|
async function readToken(env) {
|
|
@@ -38,7 +38,7 @@ async function readToken(env) {
|
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Branding + session for every admin page. `siteName` flows from the adapter without pulling
|
|
41
|
-
* its plugin graph into client bundles
|
|
41
|
+
* its plugin graph into client bundles; the import stays server-side in the layout load.
|
|
42
42
|
* `pathname` lets the shared shell highlight the active nav item without a `$app/*` import
|
|
43
43
|
* (those kit virtual modules have no types outside a kit app, so they can't live in the
|
|
44
44
|
* package); reading `event.url` here also opts the layout load into rerunning on navigation.
|
|
@@ -117,6 +117,33 @@ export async function saveCommit(event, adapter) {
|
|
|
117
117
|
installationId: env.GITHUB_APP_INSTALLATION_ID,
|
|
118
118
|
privateKeyB64: env.GITHUB_APP_PRIVATE_KEY_B64,
|
|
119
119
|
});
|
|
120
|
-
|
|
120
|
+
try {
|
|
121
|
+
await commitFile(adapter.backend, `${collection.dir}/${id}.md`, markdown, { message: `Update ${collection.label.toLowerCase()}: ${id}`, author: { name: user.name, email: user.email } }, token);
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
// Concurrent-edit 409 (C3): fail safe. Bounce back with a reload prompt; the editor reloads
|
|
125
|
+
// the current version and reapplies. Any other error is unexpected, so rethrow.
|
|
126
|
+
if (err instanceof CommitConflictError) {
|
|
127
|
+
const message = 'This file changed since you opened it. Reload and reapply your edits.';
|
|
128
|
+
throw redirect(303, `/admin/edit/${type}/${id}?error=${encodeURIComponent(message)}`);
|
|
129
|
+
}
|
|
130
|
+
throw err;
|
|
131
|
+
}
|
|
121
132
|
throw redirect(303, `/admin/edit/${type}/${id}?saved=1`);
|
|
122
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Deploy-time health check (M2): signs a dummy App JWT to prove the GitHub App key loads and
|
|
136
|
+
* the PKCS#1→PKCS#8 conversion still works, before an editor hits it on save. Behind the
|
|
137
|
+
* `/admin` guard (signed-in editors only); returns ok/fail with no secret in the body.
|
|
138
|
+
*/
|
|
139
|
+
export async function healthLoad(event) {
|
|
140
|
+
const env = event.platform?.env;
|
|
141
|
+
let githubAppSigning;
|
|
142
|
+
if (env?.GITHUB_APP_ID && env.GITHUB_APP_PRIVATE_KEY_B64) {
|
|
143
|
+
githubAppSigning = await signingSelfTest(env.GITHUB_APP_ID, env.GITHUB_APP_PRIVATE_KEY_B64);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
githubAppSigning = { ok: false, detail: 'GitHub App not configured' };
|
|
147
|
+
}
|
|
148
|
+
return { ok: githubAppSigning.ok, checks: { githubAppSigning } };
|
|
149
|
+
}
|
package/dist/utils.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
/** Encode bytes as unpadded base64url (RFC 4648 §5)
|
|
1
|
+
/** Encode bytes as unpadded base64url (RFC 4648 §5), the JWT/token wire format. */
|
|
2
2
|
export declare function bytesToB64url(bytes: Uint8Array): string;
|
|
3
3
|
//# sourceMappingURL=utils.d.ts.map
|
package/dist/utils.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.ts"],"names":[],"mappings":"AAOA,
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/lib/utils.ts"],"names":[],"mappings":"AAOA,mFAAmF;AACnF,wBAAgB,aAAa,CAAC,KAAK,EAAE,UAAU,GAAG,MAAM,CAGvD"}
|
package/dist/utils.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// cairn-core: internal encoding helpers shared across modules.
|
|
2
2
|
//
|
|
3
|
-
// Deliberately NOT re-exported from index.ts
|
|
3
|
+
// Deliberately NOT re-exported from index.ts. These are implementation details of the
|
|
4
4
|
// auth/github crypto, not part of the public API (auth.ts signs tokens, github.ts builds
|
|
5
5
|
// the App JWT; both need base64url). Keeping them here stops bytesToB64url leaking through
|
|
6
6
|
// the `export *` barrel.
|
|
7
|
-
/** Encode bytes as unpadded base64url (RFC 4648 §5)
|
|
7
|
+
/** Encode bytes as unpadded base64url (RFC 4648 §5), the JWT/token wire format. */
|
|
8
8
|
export function bytesToB64url(bytes) {
|
|
9
9
|
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join('');
|
|
10
10
|
return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glw907/cairn-cms",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -86,7 +86,20 @@
|
|
|
86
86
|
"svelte": "^5.0.0"
|
|
87
87
|
},
|
|
88
88
|
"dependencies": {
|
|
89
|
-
"
|
|
89
|
+
"@types/hast": "^3.0.4",
|
|
90
|
+
"@types/mdast": "^4.0.4",
|
|
91
|
+
"gray-matter": "^4",
|
|
92
|
+
"hastscript": "^9.0.1",
|
|
93
|
+
"mdast-util-directive": "^3.1.0",
|
|
94
|
+
"rehype-raw": "^7.0.0",
|
|
95
|
+
"rehype-slug": "^6.0.0",
|
|
96
|
+
"rehype-stringify": "^10.0.1",
|
|
97
|
+
"remark-directive": "^4.0.0",
|
|
98
|
+
"remark-gfm": "^4",
|
|
99
|
+
"remark-parse": "^11.0.0",
|
|
100
|
+
"remark-rehype": "^11.1.2",
|
|
101
|
+
"unified": "^11.0.5",
|
|
102
|
+
"unist-util-visit": "^5.1.0"
|
|
90
103
|
},
|
|
91
104
|
"devDependencies": {
|
|
92
105
|
"@better-auth/cli": "^1.4.21",
|
|
@@ -103,7 +116,6 @@
|
|
|
103
116
|
"svelte": "^5",
|
|
104
117
|
"svelte-check": "^4",
|
|
105
118
|
"typescript": "^6.0.3",
|
|
106
|
-
"unified": "^11.0.5",
|
|
107
119
|
"vitest": "^4.1.6"
|
|
108
120
|
}
|
|
109
121
|
}
|
package/src/lib/adapter.ts
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
// This is the single seam that lets one admin surface serve different designs. A site
|
|
4
4
|
// supplies a `CairnAdapter` (see `src/lib/cairn.config.ts`) describing its backend repo,
|
|
5
5
|
// its editable collections (folder + form fields + frontmatter validator), and its preview
|
|
6
|
-
// plugin set. cairn-core never hard-codes a collection, tag, or directive
|
|
6
|
+
// plugin set. cairn-core never hard-codes a collection, tag, or directive; it reads them
|
|
7
7
|
// from the adapter. Field descriptors are plain data so a load function can hand them to
|
|
8
|
-
// the editor form across the server
|
|
8
|
+
// the editor form across the server-to-client boundary.
|
|
9
9
|
import type { PreviewPlugins } from './carta';
|
|
10
10
|
import type { RepoRef } from './github';
|
|
11
|
+
import type { ComponentRegistry } from './render';
|
|
11
12
|
|
|
12
13
|
interface FieldBase {
|
|
13
14
|
/** Frontmatter key and form input name. */
|
|
@@ -63,13 +64,21 @@ export interface CairnCollection {
|
|
|
63
64
|
export interface CairnAdapter {
|
|
64
65
|
/** Branding + magic-link email copy. */
|
|
65
66
|
siteName: string;
|
|
66
|
-
/** From: address for magic-link email
|
|
67
|
+
/** From: address for magic-link email (must be a domain-authenticated sender). */
|
|
67
68
|
sender: string;
|
|
68
69
|
/** The repository the admin reads content from and commits to. */
|
|
69
70
|
backend: RepoRef;
|
|
70
71
|
/** Site plugin set for the Carta preview (parity with the live render). */
|
|
71
72
|
preview: PreviewPlugins;
|
|
72
73
|
collections: CairnCollection[];
|
|
74
|
+
/**
|
|
75
|
+
* The site's component registry: the single declaration of its directive
|
|
76
|
+
* components (R10a). Rendering parity already flows through `preview`; this
|
|
77
|
+
* exposes the same registry so the editor's insert-component palette can read
|
|
78
|
+
* `registry.defs`. Optional: a site with no rich components (e.g. 907.life) may
|
|
79
|
+
* omit it or supply an empty registry.
|
|
80
|
+
*/
|
|
81
|
+
registry?: ComponentRegistry;
|
|
73
82
|
}
|
|
74
83
|
|
|
75
84
|
/** Look up a collection by its route segment, or undefined if the segment is unknown. */
|
package/src/lib/auth/config.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// cairn-core: the better-auth instance. Auth is engine code (engine-fat rule), so the whole
|
|
2
|
-
// config
|
|
2
|
+
// config lives here: Drizzle/D1 adapter, magic-link (POST-confirm-shaped send), admin roles.
|
|
3
3
|
// Instantiated PER REQUEST in hooks.server.ts (the D1 binding is request-scoped); the factory
|
|
4
4
|
// is cheap (no I/O at construction).
|
|
5
5
|
import { betterAuth } from 'better-auth';
|
|
@@ -14,7 +14,7 @@ import * as schema from './schema';
|
|
|
14
14
|
|
|
15
15
|
// Two-tier roles on the admin plugin's access-control system: `owner` holds every admin
|
|
16
16
|
// statement (manage editors, revoke sessions); `editor` holds none (content-only). `adminRoles`
|
|
17
|
-
// must name a role defined here, so owner
|
|
17
|
+
// must name a role defined here, so owner (not the plugin's built-in `admin`) is the gate.
|
|
18
18
|
const ac = createAccessControl(defaultStatements);
|
|
19
19
|
const owner = ac.newRole(defaultStatements);
|
|
20
20
|
const editor = ac.newRole({});
|
|
@@ -37,17 +37,17 @@ export interface AuthBranding {
|
|
|
37
37
|
sender: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** The drizzle adapter result `betterAuth` consumes
|
|
40
|
+
/** The drizzle adapter result `betterAuth` consumes (same provider/schema everywhere). */
|
|
41
41
|
type DrizzleDb = Parameters<typeof drizzleAdapter>[0];
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* The shared better-auth config. Kept separate from `createAuth` so the test harness can run
|
|
45
45
|
* the EXACT plugin set (allowlist semantics, expiry, POST-confirm send) over an in-memory
|
|
46
|
-
* SQLite instead of D1. `disableSignUp:true` makes the `user` table the editor allowlist
|
|
46
|
+
* SQLite instead of D1. `disableSignUp:true` makes the `user` table the editor allowlist:
|
|
47
47
|
* magic-link never auto-creates, so the only way in is the owner-gated admin `createUser`
|
|
48
48
|
* (see auth/admins.ts). `adminRoles:['owner']` lets owners (not the default `admin` role)
|
|
49
49
|
* drive the admin API. Tokens are stored hashed and consumed atomically on first verify
|
|
50
|
-
* (better-auth GHSA-hc7v-rggr-4hvx)
|
|
50
|
+
* (better-auth GHSA-hc7v-rggr-4hvx), single-use by construction (C1).
|
|
51
51
|
*/
|
|
52
52
|
export function buildAuth(opts: {
|
|
53
53
|
database: DrizzleDb;
|
|
@@ -70,7 +70,7 @@ export function buildAuth(opts: {
|
|
|
70
70
|
sendMagicLink: async ({ email, token }, ctx) => {
|
|
71
71
|
// Allowlist gate: better-auth always fires this callback (even for unknown emails, to
|
|
72
72
|
// avoid enumeration) and only blocks user creation at verify. So gate the actual send
|
|
73
|
-
// here
|
|
73
|
+
// here. Never email a non-editor. The login UI shows neutral copy either way, so this
|
|
74
74
|
// leaks nothing; it just stops strangers receiving a dead link.
|
|
75
75
|
const existing = await ctx?.context.internalAdapter.findUserByEmail(email);
|
|
76
76
|
if (!existing?.user) return;
|
package/src/lib/auth/guard.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// cairn-core: server-side auth helpers the site route shims delegate to. Each takes the
|
|
2
|
-
// SvelteKit event
|
|
3
|
-
// `App.*` ambient types
|
|
2
|
+
// SvelteKit event, typed structurally so the package never depends on a site's generated
|
|
3
|
+
// `App.*` ambient types, plus the per-request `Auth` from `locals`.
|
|
4
4
|
import { redirect } from '@sveltejs/kit';
|
|
5
5
|
import type { Auth } from './config';
|
|
6
6
|
|
|
7
|
-
/** The session shape the whole admin reads
|
|
7
|
+
/** The session shape the whole admin reads: layout, guards, content fns, manage-editors. */
|
|
8
8
|
export interface CairnUser {
|
|
9
9
|
id: string;
|
|
10
10
|
email: string;
|
package/src/lib/carta.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// cairn-core: pure Carta options/transformer wiring for render-only preview.
|
|
2
2
|
//
|
|
3
|
-
// Plugins are passed in
|
|
3
|
+
// Plugins are passed in rather than imported; that seam is what the Pass D adapter formalises.
|
|
4
4
|
// No `carta-md` import: its index re-exports Svelte components that the node test env
|
|
5
5
|
// can't load. The Svelte component calls `new Carta(previewCartaOptions(...))` directly.
|
|
6
6
|
import type { Pluggable, Processor } from 'unified';
|
|
@@ -37,7 +37,7 @@ export function previewTransformers({ remarkPlugins, rehypePlugins }: PreviewPlu
|
|
|
37
37
|
return [...phase(remarkPlugins, 'remark'), ...phase(rehypePlugins, 'rehype')];
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
/** Minimal Options subset we populate
|
|
40
|
+
/** Minimal Options subset we populate (avoids importing carta-md, which re-exports Svelte components). */
|
|
41
41
|
interface PreviewCartaOptions {
|
|
42
42
|
sanitizer: false;
|
|
43
43
|
rehypeOptions: { allowDangerousHtml: boolean };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// Neutral admin chrome, shared across sites so the tool looks identical everywhere (only the
|
|
3
3
|
// adapter's siteName varies). When signed in it's a responsive DaisyUI drawer+navbar shell
|
|
4
|
-
// (`drawer lg:drawer-open
|
|
4
|
+
// (`drawer lg:drawer-open`, sidebar pinned on desktop, slide-over + hamburger on mobile),
|
|
5
5
|
// patterned on scosman/CMSaasStarter's `(admin)/(menu)` layout. The nav is data-driven and
|
|
6
6
|
// role-gated, so a new surface is one entry in `nav` (plus its route + component). Signed out
|
|
7
7
|
// (the login page lives under this layout) it falls back to a minimal centered shell.
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
label: string;
|
|
23
23
|
icon: Snippet;
|
|
24
24
|
active: boolean;
|
|
25
|
-
/** Owner-only surface
|
|
25
|
+
/** Owner-only surface; hidden from regular editors. */
|
|
26
26
|
owner?: boolean;
|
|
27
27
|
}
|
|
28
28
|
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
<input id="admin-drawer" type="checkbox" class="drawer-toggle" />
|
|
74
74
|
|
|
75
75
|
<div class="drawer-content">
|
|
76
|
-
<!-- Mobile top bar
|
|
76
|
+
<!-- Mobile top bar; the desktop sidebar replaces this at lg. -->
|
|
77
77
|
<div class="navbar bg-base-100 lg:hidden">
|
|
78
78
|
<div class="flex-1">
|
|
79
79
|
<span class="px-2 text-xl font-bold">{data.siteName} CMS</span>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
// The /admin content list: every collection's files, linking into the editor. Data comes
|
|
3
3
|
// from `adminListLoad` (collections) merged with `adminLayoutLoad` (siteName). The shell
|
|
4
|
-
// (AdminLayout) owns the chrome
|
|
4
|
+
// (AdminLayout) owns the chrome (site title, signed-in identity, nav, sign out), so this
|
|
5
5
|
// page renders only the content body.
|
|
6
6
|
import type { AdminCollectionList } from '../sveltekit';
|
|
7
7
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
-
// The scanner-safe confirm surface (C2). A GET renders this static page
|
|
3
|
-
// The token rides in a hidden field; only the explicit form POST (the route's default action
|
|
2
|
+
// The scanner-safe confirm surface (C2). A GET renders this static page and consumes nothing.
|
|
3
|
+
// The token rides in a hidden field; only the explicit form POST (the route's default action,
|
|
4
4
|
// confirmSignIn) verifies it. Mail scanners GET URLs but don't submit forms, so prefetch can't
|
|
5
5
|
// burn the link. JS-free by design.
|
|
6
6
|
interface Props {
|
|
@@ -13,21 +13,21 @@
|
|
|
13
13
|
|
|
14
14
|
// Body is editable state; the Carta editor's preview runs the exact site plugin set, so it
|
|
15
15
|
// matches the live page. A hidden input carries the current value into the form.
|
|
16
|
-
// svelte-ignore state_referenced_locally
|
|
16
|
+
// svelte-ignore state_referenced_locally (seeding from the initial load is intended)
|
|
17
17
|
let body = $state(data.body);
|
|
18
18
|
|
|
19
|
-
// svelte-ignore state_referenced_locally
|
|
19
|
+
// svelte-ignore state_referenced_locally (the preview plugin set is fixed for the load)
|
|
20
20
|
const carta = new Carta(previewCartaOptions(preview));
|
|
21
21
|
|
|
22
22
|
// Carta's MarkdownEditor must not render on the worker (it pulls Shiki). onMount fires only
|
|
23
|
-
// in the browser, so SSR renders the plain textarea and the client swaps in the editor
|
|
24
|
-
// the kit-free equivalent of the per-site route's `$app/environment` `browser` guard.
|
|
23
|
+
// in the browser, so SSR renders the plain textarea and the client swaps in the editor.
|
|
24
|
+
// This is the kit-free equivalent of the per-site route's `$app/environment` `browser` guard.
|
|
25
25
|
let mounted = $state(false);
|
|
26
26
|
onMount(() => {
|
|
27
27
|
mounted = true;
|
|
28
28
|
});
|
|
29
29
|
|
|
30
|
-
// svelte-ignore state_referenced_locally
|
|
30
|
+
// svelte-ignore state_referenced_locally (form defaults from the initial load)
|
|
31
31
|
const fm = data.frontmatter as Record<string, unknown>;
|
|
32
32
|
|
|
33
33
|
function fmString(key: string): string {
|