@glw907/cairn-cms 0.59.0 → 0.60.1
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 +60 -0
- package/dist/components/AdminLayout.svelte +130 -229
- package/dist/components/CairnAdmin.svelte +12 -41
- package/dist/components/CairnLogo.svelte +1 -6
- package/dist/components/CairnMediaLibrary.svelte +821 -1210
- package/dist/components/CairnTidySettings.svelte +486 -0
- package/dist/components/CairnTidySettings.svelte.d.ts +32 -0
- package/dist/components/ComponentForm.svelte +110 -185
- package/dist/components/ComponentInsertDialog.svelte +163 -283
- package/dist/components/ConceptList.svelte +111 -191
- package/dist/components/ConfirmPage.svelte +5 -12
- package/dist/components/CsrfField.svelte +5 -11
- package/dist/components/DeleteDialog.svelte +15 -42
- package/dist/components/EditPage.svelte +786 -918
- package/dist/components/EditorToolbar.svelte +108 -170
- package/dist/components/IconPicker.svelte +23 -53
- package/dist/components/LinkPicker.svelte +34 -58
- package/dist/components/LoginPage.svelte +14 -27
- package/dist/components/ManageEditors.svelte +3 -15
- package/dist/components/MarkdownEditor.svelte +688 -789
- package/dist/components/MarkdownEditor.svelte.d.ts +44 -0
- package/dist/components/MarkdownHelpDialog.svelte +8 -12
- package/dist/components/MediaCaptureCard.svelte +18 -57
- package/dist/components/MediaFigureControl.svelte +32 -71
- package/dist/components/MediaHeroField.svelte +210 -329
- package/dist/components/MediaInsertPopover.svelte +156 -283
- package/dist/components/MediaPicker.svelte +67 -131
- package/dist/components/NavTree.svelte +46 -78
- package/dist/components/RenameDialog.svelte +16 -43
- package/dist/components/ShortcutsDialog.svelte +9 -13
- package/dist/components/ShortcutsGrid.svelte +1 -2
- package/dist/components/TidyReview.svelte +355 -0
- package/dist/components/TidyReview.svelte.d.ts +47 -0
- package/dist/components/WebLinkDialog.svelte +19 -40
- package/dist/components/cairn-admin.css +768 -0
- package/dist/components/editor-tidy.d.ts +31 -0
- package/dist/components/editor-tidy.js +199 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-directives.d.ts +16 -0
- package/dist/components/markdown-directives.js +34 -0
- package/dist/components/objective-errors.d.ts +30 -0
- package/dist/components/objective-errors.js +113 -0
- package/dist/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
- package/dist/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
- package/dist/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
- package/dist/components/spellcheck-worker.d.ts +80 -0
- package/dist/components/spellcheck-worker.js +161 -0
- package/dist/components/spellcheck.d.ts +148 -0
- package/dist/components/spellcheck.js +553 -0
- package/dist/components/tidy-categorize.d.ts +67 -0
- package/dist/components/tidy-categorize.js +392 -0
- package/dist/components/tidy-diff.d.ts +60 -0
- package/dist/components/tidy-diff.js +147 -0
- package/dist/components/tidy-validate.d.ts +37 -0
- package/dist/components/tidy-validate.js +174 -0
- package/dist/content/compose.d.ts +1 -1
- package/dist/content/compose.js +11 -0
- package/dist/content/site-dictionary.d.ts +31 -0
- package/dist/content/site-dictionary.js +82 -0
- package/dist/content/types.d.ts +25 -0
- package/dist/delivery/CairnHead.svelte +8 -11
- package/dist/doctor/checks-local.d.ts +1 -0
- package/dist/doctor/checks-local.js +55 -6
- package/dist/doctor/index.js +2 -1
- package/dist/log/events.d.ts +1 -1
- package/dist/nav/site-config.d.ts +98 -0
- package/dist/nav/site-config.js +132 -0
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +6 -2
- package/dist/sveltekit/cairn-admin.d.ts +13 -1
- package/dist/sveltekit/cairn-admin.js +22 -3
- package/dist/sveltekit/content-routes.d.ts +135 -1
- package/dist/sveltekit/content-routes.js +351 -3
- package/dist/sveltekit/tidy-prompt.d.ts +11 -0
- package/dist/sveltekit/tidy-prompt.js +118 -0
- package/package.json +11 -2
- package/src/lib/components/CairnAdmin.svelte +3 -0
- package/src/lib/components/CairnTidySettings.svelte +553 -0
- package/src/lib/components/EditPage.svelte +371 -2
- package/src/lib/components/MarkdownEditor.svelte +168 -1
- package/src/lib/components/TidyReview.svelte +463 -0
- package/src/lib/components/cairn-admin.css +25 -0
- package/src/lib/components/editor-tidy.ts +241 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-directives.ts +35 -0
- package/src/lib/components/objective-errors.ts +155 -0
- package/src/lib/components/spellcheck-assets/dictionary-en-us.txt +104743 -0
- package/src/lib/components/spellcheck-assets/spellchecker-wasm-LICENSE.txt +21 -0
- package/src/lib/components/spellcheck-assets/spellchecker-wasm.wasm +0 -0
- package/src/lib/components/spellcheck-worker.ts +279 -0
- package/src/lib/components/spellcheck.ts +693 -0
- package/src/lib/components/tidy-categorize.ts +460 -0
- package/src/lib/components/tidy-diff.ts +196 -0
- package/src/lib/components/tidy-validate.ts +202 -0
- package/src/lib/content/compose.ts +11 -1
- package/src/lib/content/site-dictionary.ts +84 -0
- package/src/lib/content/types.ts +25 -0
- package/src/lib/doctor/checks-local.ts +59 -5
- package/src/lib/doctor/index.ts +2 -0
- package/src/lib/log/events.ts +7 -1
- package/src/lib/nav/site-config.ts +197 -0
- package/src/lib/sveltekit/admin-dispatch.ts +7 -3
- package/src/lib/sveltekit/cairn-admin.ts +32 -4
- package/src/lib/sveltekit/content-routes.ts +504 -4
- package/src/lib/sveltekit/tidy-prompt.ts +153 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project are recorded here, most recent first.
|
|
4
4
|
|
|
5
|
+
## 0.60.1
|
|
6
|
+
|
|
7
|
+
A packaging fix so the library bundles cleanly in a Vite 8 consumer. It supersedes `0.60.0`, whose
|
|
8
|
+
consumer build failed on Vite 8 / Rolldown. `svelte-package` ships `.svelte` with `<script lang="ts">`
|
|
9
|
+
and the TypeScript intact, and Rolldown parses that `<script>` as JavaScript before the Svelte plugin
|
|
10
|
+
compiles the file, failing on a TypeScript optional parameter (`registry?: T` loses its type but keeps
|
|
11
|
+
the `?`). The shipped `.svelte` now carry a plain-JavaScript `<script>` body. The `lang="ts"` tag
|
|
12
|
+
stays, because the component markup still uses TypeScript that the Svelte compiler reads (typed
|
|
13
|
+
`{#snippet}` parameters and `{@const x = y as T}` casts).
|
|
14
|
+
|
|
15
|
+
No consumer action is required. The change is to the published `dist` only; the public API and the
|
|
16
|
+
types are unchanged.
|
|
17
|
+
|
|
18
|
+
## 0.60.0
|
|
19
|
+
|
|
20
|
+
<!-- release-size: minor -->
|
|
21
|
+
|
|
22
|
+
The editor learns to copy-edit. Two features land together on the markdown source: a spellcheck that
|
|
23
|
+
runs as you write, and an opt-in tidy that reads a draft once with a language model and proposes a
|
|
24
|
+
light copy-edit you review before any of it lands.
|
|
25
|
+
|
|
26
|
+
Spellcheck is on by default. Misspelled words pick up a quiet amber underline, and the correction
|
|
27
|
+
popover offers ranked suggestions, an add-to-dictionary action, and an ignore-for-this-session
|
|
28
|
+
action, all keyboard-reachable. It runs locally on a Web Worker, so no text leaves the browser, and
|
|
29
|
+
it reads the markdown structure: code, links, frontmatter, layout-block machinery, and `media:`
|
|
30
|
+
tokens are never flagged. A second quiet layer catches the objective slips spellcheck misses: a
|
|
31
|
+
doubled word, a double space inside a line, a stray run of punctuation. The dialect is declared once
|
|
32
|
+
per site under `spellcheck.dialect` (default `en-us`), so a British site loads the British word list
|
|
33
|
+
and "colour" reads as correct. The personal dictionary is a git-committed file at
|
|
34
|
+
`src/content/.cairn/dictionary.txt`, so a word one editor adds is shared with the rest through the
|
|
35
|
+
same commit pipeline the content uses.
|
|
36
|
+
|
|
37
|
+
Tidy is opt-in and off until a developer enables it. When on, an editor runs it over the whole
|
|
38
|
+
document or a selection, and cairn reads the draft once through the Anthropic API and computes the
|
|
39
|
+
diff locally. The review is a step-in diff dialog: insertions show in green, deletions struck through
|
|
40
|
+
in red, and the author's original stays in the buffer until they apply. Objective fixes come pre-kept;
|
|
41
|
+
a judgment edit (a configured style normalization, a grammar reword) carries a review-this treatment
|
|
42
|
+
and a plain-language reason, and it is not swept by Accept fixes until confirmed. The prompt is built
|
|
43
|
+
from the site's own convention config and never harmonizes to the author's habits or guesses an
|
|
44
|
+
undeclared style, so an author's voice is preserved. Output is validated as a proofread, not a
|
|
45
|
+
restructure: a result that changes the heading structure, the frontmatter, a `media:` token, a code
|
|
46
|
+
block, or more than a bounded fraction of the wording is discarded with an honest message and the
|
|
47
|
+
document is left untouched. Conventions are edited in a two-tier settings screen and stored in the
|
|
48
|
+
committed site config under `tidy.conventions`.
|
|
49
|
+
|
|
50
|
+
New dependencies: `@codemirror/lint` (the surfacing layer for both spellcheck and the objective-error
|
|
51
|
+
underlines), `@anthropic-ai/sdk` (the Worker-side tidy model call, guarded off the client), and
|
|
52
|
+
`spellchecker-wasm` plus its bundled English dictionary asset (the spellcheck engine, delivered from
|
|
53
|
+
the packaged `dist` so the Worker and the word list reach a consumer build).
|
|
54
|
+
|
|
55
|
+
No consumer action is required for an existing site. Both features are additive. Spellcheck replaces
|
|
56
|
+
the browser's native spell checking with cairn's own, so an upgrading editor sees the new amber
|
|
57
|
+
underline and the in-editor correction popover in place of the browser's right-click menu, with no
|
|
58
|
+
config change needed. Tidy gives a site nothing until a developer turns it on: set `tidy.enabled: true`
|
|
59
|
+
in the site config, add the `ANTHROPIC_API_KEY` Worker secret, and optionally pick a model and
|
|
60
|
+
conventions. `cairn doctor` checks that the key is configured once tidy is enabled. The editor
|
|
61
|
+
walkthrough is in [write in the editor](docs/guides/write-in-the-editor.md), the developer setup is in
|
|
62
|
+
[enable tidy and the editor copy-edit](docs/guides/enable-tidy.md), and the design rationale is in
|
|
63
|
+
[the editor copy-edit](docs/explanation/editor-copyedit.md).
|
|
64
|
+
|
|
5
65
|
## 0.59.0
|
|
6
66
|
|
|
7
67
|
<!-- release-size: minor -->
|
|
@@ -6,239 +6,140 @@ root sets `data-theme` to the resolved light or dark theme (seeded from the SSR'
|
|
|
6
6
|
flipped by the topbar toggle) and imports the self-contained Warm Stone theme, so the admin looks
|
|
7
7
|
identical on every host regardless of the site's own theme.
|
|
8
8
|
-->
|
|
9
|
-
<script lang="ts">
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const extensionGroups: { name: string; items: NavItem[] }[] = [
|
|
71
|
-
{ name: 'Marketing', items: [
|
|
72
|
-
{ label: 'Campaigns', icon: BlocksIcon },
|
|
73
|
-
{ label: 'Audiences', icon: BlocksIcon },
|
|
74
|
-
] },
|
|
75
|
-
{ name: 'Shop', items: [
|
|
76
|
-
{ label: 'Products', icon: BlocksIcon },
|
|
77
|
-
{ label: 'Orders', icon: BlocksIcon },
|
|
78
|
-
] },
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
// Up to two uppercase initials from the display name, falling back to '?' for an empty name.
|
|
82
|
-
function initialsOf(displayName: string): string {
|
|
83
|
-
const letters = displayName
|
|
84
|
-
.split(/\s+/)
|
|
85
|
-
.filter(Boolean)
|
|
86
|
-
.slice(0, 2)
|
|
87
|
-
.map((word) => word[0]?.toUpperCase() ?? '')
|
|
88
|
-
.join('');
|
|
89
|
-
return letters || '?';
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const initials = $derived(initialsOf(data.user.displayName));
|
|
93
|
-
|
|
94
|
-
function isActive(href: string): boolean {
|
|
95
|
-
return data.pathname === href || data.pathname.startsWith(`${href}/`);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// Which nav groups are collapsed. Seeded once from the SSR'd cookie (so a collapsed group renders
|
|
99
|
-
// collapsed with no flash), then owned by the toggle below, which mirrors each change to the cookie.
|
|
100
|
-
let collapsed = $state(new Set(untrack(() => data.collapsedNav)));
|
|
101
|
-
|
|
102
|
-
function onToggleSection(label: string, open: boolean) {
|
|
103
|
-
const next = new Set(collapsed);
|
|
104
|
-
if (open) next.delete(label);
|
|
105
|
-
else next.add(label);
|
|
106
|
-
collapsed = next;
|
|
107
|
-
const value = [...next].map((entry) => encodeURIComponent(entry)).join(',');
|
|
108
|
-
writeAdminCookie('cairn-admin-nav-collapsed', value);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
let drawerOpen = $state(false);
|
|
112
|
-
|
|
113
|
-
function onKeydown(e: KeyboardEvent) {
|
|
114
|
-
if (e.key.toLowerCase() === 'b' && (e.metaKey || e.ctrlKey)) {
|
|
115
|
-
e.preventDefault();
|
|
116
|
-
drawerOpen = !drawerOpen;
|
|
117
|
-
}
|
|
118
|
-
if (e.key.toLowerCase() === 'k' && (e.metaKey || e.ctrlKey)) {
|
|
119
|
-
e.preventDefault();
|
|
120
|
-
openPalette();
|
|
121
|
-
}
|
|
9
|
+
<script lang="ts">import { onMount, setContext, untrack } from "svelte";
|
|
10
|
+
import CsrfField from "./CsrfField.svelte";
|
|
11
|
+
import { CSRF_CONTEXT_KEY } from "./csrf-context.js";
|
|
12
|
+
import { provideTopbar } from "./topbar-context.js";
|
|
13
|
+
import { MenuIcon, LogOutIcon, SunIcon, MoonIcon, ChevronRightIcon, SearchIcon } from "./admin-icons.js";
|
|
14
|
+
import CairnLogo from "./CairnLogo.svelte";
|
|
15
|
+
import { cairnFaviconHref } from "./cairn-favicon.js";
|
|
16
|
+
import { warnIfChromeWrapped } from "./chrome-guard.js";
|
|
17
|
+
import FileTextIcon from "@lucide/svelte/icons/file-text";
|
|
18
|
+
import SignpostIcon from "@lucide/svelte/icons/signpost";
|
|
19
|
+
import SettingsIcon from "@lucide/svelte/icons/settings";
|
|
20
|
+
import UsersIcon from "@lucide/svelte/icons/users";
|
|
21
|
+
import ImageIcon from "@lucide/svelte/icons/image";
|
|
22
|
+
import BlocksIcon from "@lucide/svelte/icons/blocks";
|
|
23
|
+
import ExternalLinkIcon from "@lucide/svelte/icons/external-link";
|
|
24
|
+
import "./cairn-admin.css";
|
|
25
|
+
let { data, children } = $props();
|
|
26
|
+
setContext(CSRF_CONTEXT_KEY, () => data.csrf);
|
|
27
|
+
function writeAdminCookie(name, value) {
|
|
28
|
+
document.cookie = `${name}=${value}; path=/admin; max-age=31536000; samesite=lax`;
|
|
29
|
+
}
|
|
30
|
+
const coreItems = $derived([
|
|
31
|
+
...data.concepts.map((c) => ({ label: c.label, icon: FileTextIcon, href: `/admin/${c.id}` })),
|
|
32
|
+
// Media is a content peer, immediately after the concepts.
|
|
33
|
+
{ label: "Media", icon: ImageIcon, href: "/admin/media" },
|
|
34
|
+
...data.navLabel ? [{ label: data.navLabel, icon: SignpostIcon, href: "/admin/nav" }] : [],
|
|
35
|
+
{ label: "Settings", icon: SettingsIcon, href: "/admin/settings" },
|
|
36
|
+
...data.canManageEditors ? [{ label: "Editors", icon: UsersIcon, href: "/admin/editors" }] : []
|
|
37
|
+
]);
|
|
38
|
+
const extensionGroups = [
|
|
39
|
+
{ name: "Marketing", items: [
|
|
40
|
+
{ label: "Campaigns", icon: BlocksIcon },
|
|
41
|
+
{ label: "Audiences", icon: BlocksIcon }
|
|
42
|
+
] },
|
|
43
|
+
{ name: "Shop", items: [
|
|
44
|
+
{ label: "Products", icon: BlocksIcon },
|
|
45
|
+
{ label: "Orders", icon: BlocksIcon }
|
|
46
|
+
] }
|
|
47
|
+
];
|
|
48
|
+
function initialsOf(displayName) {
|
|
49
|
+
const letters = displayName.split(/\s+/).filter(Boolean).slice(0, 2).map((word) => word[0]?.toUpperCase() ?? "").join("");
|
|
50
|
+
return letters || "?";
|
|
51
|
+
}
|
|
52
|
+
const initials = $derived(initialsOf(data.user.displayName));
|
|
53
|
+
function isActive(href) {
|
|
54
|
+
return data.pathname === href || data.pathname.startsWith(`${href}/`);
|
|
55
|
+
}
|
|
56
|
+
let collapsed = $state(new Set(untrack(() => data.collapsedNav)));
|
|
57
|
+
function onToggleSection(label, open) {
|
|
58
|
+
const next = new Set(collapsed);
|
|
59
|
+
if (open) next.delete(label);
|
|
60
|
+
else next.add(label);
|
|
61
|
+
collapsed = next;
|
|
62
|
+
const value = [...next].map((entry) => encodeURIComponent(entry)).join(",");
|
|
63
|
+
writeAdminCookie("cairn-admin-nav-collapsed", value);
|
|
64
|
+
}
|
|
65
|
+
let drawerOpen = $state(false);
|
|
66
|
+
function onKeydown(e) {
|
|
67
|
+
if (e.key.toLowerCase() === "b" && (e.metaKey || e.ctrlKey)) {
|
|
68
|
+
e.preventDefault();
|
|
69
|
+
drawerOpen = !drawerOpen;
|
|
122
70
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
// close() against a result link's own navigation, which would cancel it.
|
|
127
|
-
$effect(() => {
|
|
128
|
-
data.pathname;
|
|
129
|
-
drawerOpen = false;
|
|
130
|
-
paletteDialog?.close();
|
|
131
|
-
publishAllDialog?.close();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
// Seed from the SSR'd theme once. The live theme is owned by this state and the toggle, so the
|
|
135
|
-
// initial read of data.theme is intentional and untracked to keep it out of any reactive graph.
|
|
136
|
-
let theme = $state<'cairn-admin' | 'cairn-admin-dark'>(untrack(() => data.theme));
|
|
137
|
-
|
|
138
|
-
// First mount with no persisted choice follows the OS preference. A returning user's cookie was
|
|
139
|
-
// already honored by the layout load (data.theme), so this only fires on a first-ever visit.
|
|
140
|
-
$effect(() => {
|
|
141
|
-
const hasCookie = document.cookie.split('; ').some((c) => c.startsWith('cairn-admin-theme='));
|
|
142
|
-
if (!hasCookie && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
|
|
143
|
-
theme = 'cairn-admin-dark';
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
function toggleTheme() {
|
|
148
|
-
theme = theme === 'cairn-admin' ? 'cairn-admin-dark' : 'cairn-admin';
|
|
149
|
-
writeAdminCookie('cairn-admin-theme', theme);
|
|
71
|
+
if (e.key.toLowerCase() === "k" && (e.metaKey || e.ctrlKey)) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
openPalette();
|
|
150
74
|
}
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
75
|
+
}
|
|
76
|
+
$effect(() => {
|
|
77
|
+
data.pathname;
|
|
78
|
+
drawerOpen = false;
|
|
79
|
+
paletteDialog?.close();
|
|
80
|
+
publishAllDialog?.close();
|
|
81
|
+
});
|
|
82
|
+
let theme = $state(untrack(() => data.theme));
|
|
83
|
+
$effect(() => {
|
|
84
|
+
const hasCookie = document.cookie.split("; ").some((c) => c.startsWith("cairn-admin-theme="));
|
|
85
|
+
if (!hasCookie && window.matchMedia?.("(prefers-color-scheme: dark)").matches) {
|
|
86
|
+
theme = "cairn-admin-dark";
|
|
160
87
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
groups.set(label, [...(groups.get(label) ?? []), entry.id]);
|
|
177
|
-
}
|
|
178
|
-
return [...groups.entries()].map(([label, ids]) => ({ label, ids }));
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
// The bare data-theme wrapper is the admin root the dev chrome-guard measures from.
|
|
182
|
-
let rootEl = $state<HTMLElement>();
|
|
183
|
-
onMount(() => {
|
|
184
|
-
if (rootEl) warnIfChromeWrapped(rootEl);
|
|
185
|
-
});
|
|
186
|
-
|
|
187
|
-
const paletteCommands = $derived<Command[]>([
|
|
188
|
-
...coreItems.map((item) => ({ label: item.label, icon: item.icon, href: item.href })),
|
|
189
|
-
{ label: 'View the live site', icon: ExternalLinkIcon, href: '/', external: true },
|
|
190
|
-
theme === 'cairn-admin'
|
|
191
|
-
? { label: 'Switch to dark mode', icon: MoonIcon, action: toggleTheme }
|
|
192
|
-
: { label: 'Switch to light mode', icon: SunIcon, action: toggleTheme },
|
|
193
|
-
]);
|
|
194
|
-
const paletteResults = $derived(
|
|
195
|
-
paletteCommands.filter((c) => c.label.toLowerCase().includes(paletteQuery.trim().toLowerCase())),
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
function openPalette() {
|
|
199
|
-
if (paletteDialog?.open) return; // showModal throws on an already-open dialog
|
|
200
|
-
paletteQuery = '';
|
|
201
|
-
paletteDialog?.showModal();
|
|
202
|
-
}
|
|
203
|
-
// An action command (theme toggle). Link commands are real <a> elements that navigate on click, so
|
|
204
|
-
// the Enter shortcut clicks the first result element and both paths share the one navigation.
|
|
205
|
-
function runCommand(cmd: Command) {
|
|
206
|
-
paletteDialog?.close();
|
|
207
|
-
cmd.action?.();
|
|
208
|
-
}
|
|
209
|
-
function submitPalette() {
|
|
210
|
-
(paletteList?.querySelector('a, button') as HTMLElement | null)?.click();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
interface Crumb {
|
|
214
|
-
label: string;
|
|
215
|
-
href?: string;
|
|
88
|
+
});
|
|
89
|
+
function toggleTheme() {
|
|
90
|
+
theme = theme === "cairn-admin" ? "cairn-admin-dark" : "cairn-admin";
|
|
91
|
+
writeAdminCookie("cairn-admin-theme", theme);
|
|
92
|
+
}
|
|
93
|
+
let paletteDialog = $state();
|
|
94
|
+
let paletteList = $state();
|
|
95
|
+
let paletteQuery = $state("");
|
|
96
|
+
let publishAllDialog = $state();
|
|
97
|
+
const pendingCount = $derived(data.pendingEntries?.length ?? 0);
|
|
98
|
+
const pendingGroups = $derived.by(() => {
|
|
99
|
+
const groups = /* @__PURE__ */ new Map();
|
|
100
|
+
for (const entry of data.pendingEntries ?? []) {
|
|
101
|
+
const label = data.concepts.find((c) => c.id === entry.concept)?.label ?? entry.concept;
|
|
102
|
+
groups.set(label, [...groups.get(label) ?? [], entry.id]);
|
|
216
103
|
}
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
104
|
+
return [...groups.entries()].map(([label, ids]) => ({ label, ids }));
|
|
105
|
+
});
|
|
106
|
+
let rootEl = $state();
|
|
107
|
+
onMount(() => {
|
|
108
|
+
if (rootEl) warnIfChromeWrapped(rootEl);
|
|
109
|
+
});
|
|
110
|
+
const paletteCommands = $derived([
|
|
111
|
+
...coreItems.map((item) => ({ label: item.label, icon: item.icon, href: item.href })),
|
|
112
|
+
{ label: "View the live site", icon: ExternalLinkIcon, href: "/", external: true },
|
|
113
|
+
theme === "cairn-admin" ? { label: "Switch to dark mode", icon: MoonIcon, action: toggleTheme } : { label: "Switch to light mode", icon: SunIcon, action: toggleTheme }
|
|
114
|
+
]);
|
|
115
|
+
const paletteResults = $derived(
|
|
116
|
+
paletteCommands.filter((c) => c.label.toLowerCase().includes(paletteQuery.trim().toLowerCase()))
|
|
117
|
+
);
|
|
118
|
+
function openPalette() {
|
|
119
|
+
if (paletteDialog?.open) return;
|
|
120
|
+
paletteQuery = "";
|
|
121
|
+
paletteDialog?.showModal();
|
|
122
|
+
}
|
|
123
|
+
function runCommand(cmd) {
|
|
124
|
+
paletteDialog?.close();
|
|
125
|
+
cmd.action?.();
|
|
126
|
+
}
|
|
127
|
+
function submitPalette() {
|
|
128
|
+
paletteList?.querySelector("a, button")?.click();
|
|
129
|
+
}
|
|
130
|
+
const crumbs = $derived.by(() => {
|
|
131
|
+
const segs = data.pathname.split("/").filter(Boolean);
|
|
132
|
+
if (segs.length < 2 || segs[0] !== "admin") return [];
|
|
133
|
+
const conceptId = segs[1];
|
|
134
|
+
const concept = data.concepts.find((c) => c.id === conceptId);
|
|
135
|
+
const out = [{ label: concept?.label ?? conceptId, href: `/admin/${conceptId}` }];
|
|
136
|
+
if (segs[2]) out.push({ label: decodeURIComponent(segs[2]) });
|
|
137
|
+
return out;
|
|
138
|
+
});
|
|
139
|
+
const pageTitle = $derived(crumbs.length ? crumbs[crumbs.length - 1].label : "Admin");
|
|
140
|
+
const isDeskRoute = $derived(data.pathname.split("/").filter(Boolean).length > 2);
|
|
141
|
+
let topbar = $state({ desk: null, zen: false });
|
|
142
|
+
provideTopbar(topbar);
|
|
242
143
|
</script>
|
|
243
144
|
|
|
244
145
|
<svelte:head>
|
|
@@ -5,47 +5,16 @@ component for every admin view, feeding it the discriminated `AdminData` from `c
|
|
|
5
5
|
load. It is a pure switcher on `data.view`: the public auth pages mount bare, and the authed views
|
|
6
6
|
mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
import type { ContentFormFailure } from '../sveltekit/content-routes.js';
|
|
19
|
-
import type { ComponentRegistry } from '../render/registry.js';
|
|
20
|
-
import type { IconSet } from '../render/glyph.js';
|
|
21
|
-
import type { LinkResolve } from '../content/links.js';
|
|
22
|
-
import type { MediaResolve } from '../render/resolve-media.js';
|
|
23
|
-
|
|
24
|
-
interface Props {
|
|
25
|
-
/** The discriminated view data from `createCairnAdmin`'s load. */
|
|
26
|
-
data: AdminData;
|
|
27
|
-
/** The last action's result, forwarded to whichever view rendered: the shared content-action
|
|
28
|
-
* failure family (every failure carries `error`), merged with the auth and editors results,
|
|
29
|
-
* so the route's one `form` export covers every view. */
|
|
30
|
-
form?:
|
|
31
|
-
| (ContentFormFailure & {
|
|
32
|
-
sent?: boolean;
|
|
33
|
-
status?: 'sent' | 'send_error' | 'throttled';
|
|
34
|
-
ok?: boolean;
|
|
35
|
-
})
|
|
36
|
-
| null;
|
|
37
|
-
/** The site's design-accurate render pipeline, for the edit view's preview pane. */
|
|
38
|
-
render?: (
|
|
39
|
-
md: string,
|
|
40
|
-
opts?: { stagger?: boolean; resolve?: LinkResolve; resolveMedia?: MediaResolve },
|
|
41
|
-
) => string | Promise<string>;
|
|
42
|
-
/** The site's component registry, for the edit view's insert palette. */
|
|
43
|
-
registry?: ComponentRegistry;
|
|
44
|
-
/** The site's icon set, for the edit view's guided form fields. */
|
|
45
|
-
icons?: IconSet;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
let { data, form = null, render, registry, icons }: Props = $props();
|
|
8
|
+
<script lang="ts">import AdminLayout from "./AdminLayout.svelte";
|
|
9
|
+
import LoginPage from "./LoginPage.svelte";
|
|
10
|
+
import ConfirmPage from "./ConfirmPage.svelte";
|
|
11
|
+
import ConceptList from "./ConceptList.svelte";
|
|
12
|
+
import EditPage from "./EditPage.svelte";
|
|
13
|
+
import ManageEditors from "./ManageEditors.svelte";
|
|
14
|
+
import NavTree from "./NavTree.svelte";
|
|
15
|
+
import CairnMediaLibrary from "./CairnMediaLibrary.svelte";
|
|
16
|
+
import CairnTidySettings from "./CairnTidySettings.svelte";
|
|
17
|
+
let { data, form = null, render, registry, icons } = $props();
|
|
49
18
|
</script>
|
|
50
19
|
|
|
51
20
|
{#if data.view === 'login'}
|
|
@@ -69,6 +38,8 @@ mount inside `AdminLayout`. No styling or wrapper elements of its own.
|
|
|
69
38
|
<NavTree data={data.page} />
|
|
70
39
|
{:else if data.view === 'media'}
|
|
71
40
|
<CairnMediaLibrary data={data.page} {form} />
|
|
41
|
+
{:else if data.view === 'settings'}
|
|
42
|
+
<CairnTidySettings data={data.page} />
|
|
72
43
|
{/if}
|
|
73
44
|
</AdminLayout>
|
|
74
45
|
{/if}
|
|
@@ -7,12 +7,7 @@ accessible name.
|
|
|
7
7
|
Artwork: the "cairn" icon from the Temaki icon set (https://github.com/ideditor/temaki), released into
|
|
8
8
|
the public domain under CC0 1.0 (no attribution required). Recorded here as provenance.
|
|
9
9
|
-->
|
|
10
|
-
<script lang="ts">
|
|
11
|
-
interface Props {
|
|
12
|
-
/** Utility classes for sizing and color, e.g. `h-7 w-7 text-primary`. */
|
|
13
|
-
class?: string;
|
|
14
|
-
}
|
|
15
|
-
let { class: className = '' }: Props = $props();
|
|
10
|
+
<script lang="ts">let { class: className = "" } = $props();
|
|
16
11
|
</script>
|
|
17
12
|
|
|
18
13
|
<svg
|