@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
|
@@ -81,9 +81,182 @@ export interface SiteConfig {
|
|
|
81
81
|
menus?: Record<string, unknown>;
|
|
82
82
|
/** Per-concept URL policy: the permalink pattern and date-prefix granularity, keyed by concept id. */
|
|
83
83
|
content?: Record<string, ConceptUrlPolicy>;
|
|
84
|
+
/** The editor spellcheck settings. The dialect is declared once per site (spec 1.2), so a British
|
|
85
|
+
* site loads the British word list and "colour" reads as correct. Today only US English ships, so an
|
|
86
|
+
* unset or unknown dialect resolves to it. */
|
|
87
|
+
spellcheck?: { dialect?: string };
|
|
88
|
+
/** The editor tidy (LLM copy-edit) settings. Opt-in at the site level (spec 2.8): tidy is a remote,
|
|
89
|
+
* costly model call, so the whole block is optional and `enabled` defaults false. The model is a
|
|
90
|
+
* developer-tier fact; the `conventions` block is the editor-tier per-convention config that builds
|
|
91
|
+
* the prompt's CONVENTIONS section. The Anthropic API key is a Worker secret, never config. */
|
|
92
|
+
tidy?: TidyConfig;
|
|
84
93
|
[key: string]: unknown;
|
|
85
94
|
}
|
|
86
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The tidy block on the site config. Every field is optional so the YAML can carry as little as
|
|
98
|
+
* `tidy: { enabled: true }` and the defaults fill the rest.
|
|
99
|
+
*/
|
|
100
|
+
export interface TidyConfig {
|
|
101
|
+
/** Master switch. Default false; tidy is opt-in (spec 2.8, decision 1). */
|
|
102
|
+
enabled?: boolean;
|
|
103
|
+
/** The model id. Default `claude-sonnet-4-6`; the alternative is `claude-haiku-4-5` (spec 2.2). */
|
|
104
|
+
model?: string;
|
|
105
|
+
/** The per-convention toggles that build the prompt's CONVENTIONS section. */
|
|
106
|
+
conventions?: Partial<TidyConventions>;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The default tidy model when a site sets none: Sonnet, the judgment floor for a light copy-edit. */
|
|
110
|
+
export const DEFAULT_TIDY_MODEL = 'claude-sonnet-4-6';
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The corrected convention set (spec "The corrected convention set"), the resolved shape the prompt
|
|
114
|
+
* builder consumes. Every field carries a concrete value; `resolveTidyConventions` fills the defaults
|
|
115
|
+
* from a partial config. The Fixes group is the objective fixes (default on, governed by the always-on
|
|
116
|
+
* core); the style tier defaults off (a falsy variant means off); the advanced tier defaults off.
|
|
117
|
+
* Sentence spacing is dropped on purpose and regional spelling is `spellcheck.dialect`, not a toggle.
|
|
118
|
+
*/
|
|
119
|
+
export interface TidyConventions {
|
|
120
|
+
/** The objective Fixes group (spelling, grammar, doubled words, whitespace, capitals, terminal
|
|
121
|
+
* punctuation). Default on. The always-on core governs it; this toggle lets the screen turn the
|
|
122
|
+
* group off. */
|
|
123
|
+
fixes: boolean;
|
|
124
|
+
/** Oxford comma position. Off when undefined; `always` | `complex-only` (AP) | `never`. */
|
|
125
|
+
oxfordComma?: 'always' | 'complex-only' | 'never';
|
|
126
|
+
/** Number style threshold. Off when undefined; the always-numeral exception sets (ages, dates,
|
|
127
|
+
* measurements, percentages) apply at any threshold. */
|
|
128
|
+
numberStyle?: 'under-ten' | 'under-hundred' | 'always-numerals';
|
|
129
|
+
/** Measurement notation only (never the system, never the number). Off when undefined. */
|
|
130
|
+
measurements?: 'abbreviate' | 'spell-out';
|
|
131
|
+
/** Percent rendering. Off when undefined; `sign` is "%", `word` is "percent". */
|
|
132
|
+
percent?: 'sign' | 'word';
|
|
133
|
+
/** Em-dash spacing. Off when undefined. */
|
|
134
|
+
emDash?: 'spaced' | 'closed';
|
|
135
|
+
/** Turn a hyphen between two numbers into an en dash. Default off. */
|
|
136
|
+
enDashRanges: boolean;
|
|
137
|
+
/** Ellipsis rendering. Off when undefined. */
|
|
138
|
+
ellipsis?: 'single-char' | 'three-dots';
|
|
139
|
+
/** Time format. Off when undefined. */
|
|
140
|
+
timeFormat?: '5 PM' | '5pm' | '5 p.m.';
|
|
141
|
+
/** Advanced: convert straight quotes to curly with the full apostrophe rule set. Default off. */
|
|
142
|
+
smartQuotes: boolean;
|
|
143
|
+
/** Advanced: correct brand and proper-noun capitalization on a curated list only. Default off. */
|
|
144
|
+
brandCaps: boolean;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** The resting tidy convention set: Fixes on, every style and advanced toggle off. */
|
|
148
|
+
export function defaultTidyConventions(): TidyConventions {
|
|
149
|
+
return { fixes: true, enDashRanges: false, smartQuotes: false, brandCaps: false };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Resolve a partial conventions config (from the YAML) into the concrete TidyConventions the prompt
|
|
154
|
+
* builder consumes. An absent field falls to its default: Fixes on, the style and advanced toggles
|
|
155
|
+
* off. A multi-position toggle stays undefined (off) unless the config names a variant.
|
|
156
|
+
*/
|
|
157
|
+
export function resolveTidyConventions(partial: Partial<TidyConventions> | undefined): TidyConventions {
|
|
158
|
+
const base = defaultTidyConventions();
|
|
159
|
+
if (partial === undefined) return base;
|
|
160
|
+
return {
|
|
161
|
+
fixes: partial.fixes ?? base.fixes,
|
|
162
|
+
oxfordComma: partial.oxfordComma,
|
|
163
|
+
numberStyle: partial.numberStyle,
|
|
164
|
+
measurements: partial.measurements,
|
|
165
|
+
percent: partial.percent,
|
|
166
|
+
emDash: partial.emDash,
|
|
167
|
+
enDashRanges: partial.enDashRanges ?? base.enDashRanges,
|
|
168
|
+
ellipsis: partial.ellipsis,
|
|
169
|
+
timeFormat: partial.timeFormat,
|
|
170
|
+
smartQuotes: partial.smartQuotes ?? base.smartQuotes,
|
|
171
|
+
brandCaps: partial.brandCaps ?? base.brandCaps,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// The allowed values for each multi-position style toggle, the single source the validator checks an
|
|
176
|
+
// untrusted settings payload against. A value outside its list is rejected, so the committed YAML can
|
|
177
|
+
// only ever carry a known variant the prompt builder understands.
|
|
178
|
+
const OXFORD_COMMA_VALUES = ['always', 'complex-only', 'never'] as const;
|
|
179
|
+
const NUMBER_STYLE_VALUES = ['under-ten', 'under-hundred', 'always-numerals'] as const;
|
|
180
|
+
const MEASUREMENTS_VALUES = ['abbreviate', 'spell-out'] as const;
|
|
181
|
+
const PERCENT_VALUES = ['sign', 'word'] as const;
|
|
182
|
+
const EM_DASH_VALUES = ['spaced', 'closed'] as const;
|
|
183
|
+
const ELLIPSIS_VALUES = ['single-char', 'three-dots'] as const;
|
|
184
|
+
const TIME_FORMAT_VALUES = ['5 PM', '5pm', '5 p.m.'] as const;
|
|
185
|
+
|
|
186
|
+
export class TidyConventionsError extends Error {
|
|
187
|
+
/** A malformed settings payload maps to the same diagnostic as a malformed config. */
|
|
188
|
+
readonly conditionId = 'config.site-config-invalid';
|
|
189
|
+
|
|
190
|
+
constructor(message: string) {
|
|
191
|
+
super(message);
|
|
192
|
+
this.name = 'TidyConventionsError';
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Validate and normalize an untrusted conventions object (from the settings form) into a concrete
|
|
198
|
+
* TidyConventions. This input is committed to the repo, so every field is bounded to its known set:
|
|
199
|
+
* a boolean toggle must be a boolean, and a multi-position toggle must be one of its listed variants
|
|
200
|
+
* or absent (off). An unknown key is dropped rather than carried, so the committed block can never
|
|
201
|
+
* grow a junk key. Throws TidyConventionsError on a value outside its allowed set.
|
|
202
|
+
*/
|
|
203
|
+
export function validateTidyConventions(value: unknown): TidyConventions {
|
|
204
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
205
|
+
throw new TidyConventionsError('Tidy conventions must be an object');
|
|
206
|
+
}
|
|
207
|
+
const input = value as Record<string, unknown>;
|
|
208
|
+
|
|
209
|
+
function bool(key: string, fallback: boolean): boolean {
|
|
210
|
+
const v = input[key];
|
|
211
|
+
if (v === undefined) return fallback;
|
|
212
|
+
if (typeof v !== 'boolean') throw new TidyConventionsError(`${key} must be true or false`);
|
|
213
|
+
return v;
|
|
214
|
+
}
|
|
215
|
+
function variant<T extends string>(key: string, allowed: readonly T[]): T | undefined {
|
|
216
|
+
const v = input[key];
|
|
217
|
+
if (v === undefined || v === null || v === false) return undefined;
|
|
218
|
+
if (typeof v !== 'string' || !(allowed as readonly string[]).includes(v)) {
|
|
219
|
+
throw new TidyConventionsError(`${key} must be one of ${allowed.join(', ')}`);
|
|
220
|
+
}
|
|
221
|
+
return v as T;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
fixes: bool('fixes', true),
|
|
226
|
+
oxfordComma: variant('oxfordComma', OXFORD_COMMA_VALUES),
|
|
227
|
+
numberStyle: variant('numberStyle', NUMBER_STYLE_VALUES),
|
|
228
|
+
measurements: variant('measurements', MEASUREMENTS_VALUES),
|
|
229
|
+
percent: variant('percent', PERCENT_VALUES),
|
|
230
|
+
emDash: variant('emDash', EM_DASH_VALUES),
|
|
231
|
+
enDashRanges: bool('enDashRanges', false),
|
|
232
|
+
ellipsis: variant('ellipsis', ELLIPSIS_VALUES),
|
|
233
|
+
timeFormat: variant('timeFormat', TIME_FORMAT_VALUES),
|
|
234
|
+
smartQuotes: bool('smartQuotes', false),
|
|
235
|
+
brandCaps: bool('brandCaps', false),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/** The dialect string when a site sets none: US English, the only dictionary that ships today. */
|
|
240
|
+
export const DEFAULT_DIALECT = 'en-US';
|
|
241
|
+
|
|
242
|
+
// The dialect-to-dictionary map. Only US English ships now; a new locale adds one entry here and one
|
|
243
|
+
// committed dictionary file under spellcheck-assets, and the rest of the chain (the main-thread URL
|
|
244
|
+
// resolution, the worker fetch) needs no change. An unknown or unset dialect falls back to the default
|
|
245
|
+
// rather than throwing, so a typo or a future-locale config never breaks the editor.
|
|
246
|
+
const DICTIONARY_BY_DIALECT: Record<string, string> = {
|
|
247
|
+
'en-US': 'dictionary-en-us.txt',
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* The dictionary asset file for a site's configured dialect, defaulting to US English. The main thread
|
|
252
|
+
* resolves this filename to a real URL (the spike's out-of-bundle asset) and hands it to the Worker in
|
|
253
|
+
* the `init` message; the Worker never reads config. An unknown dialect falls back to the default file.
|
|
254
|
+
*/
|
|
255
|
+
export function dictionaryFileForDialect(dialect: string | undefined): string {
|
|
256
|
+
const key = dialect ?? DEFAULT_DIALECT;
|
|
257
|
+
return DICTIONARY_BY_DIALECT[key] ?? DICTIONARY_BY_DIALECT[DEFAULT_DIALECT]!;
|
|
258
|
+
}
|
|
259
|
+
|
|
87
260
|
export class SiteConfigError extends Error {
|
|
88
261
|
/** The registered diagnostic condition a malformed site config maps to (mirrors CairnError). */
|
|
89
262
|
readonly conditionId = 'config.site-config-invalid';
|
|
@@ -133,3 +306,27 @@ export function setMenu(raw: string, name: string, tree: NavNode[]): string {
|
|
|
133
306
|
doc.setIn(['menus', name], tree);
|
|
134
307
|
return doc.toString();
|
|
135
308
|
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Write the editor-tier tidy conventions into the YAML site-config text and reserialize, preserving
|
|
312
|
+
* every other top-level key and the file's comments and key order (parseDocument round-trips both,
|
|
313
|
+
* the same machinery setMenu uses). Only the `tidy.conventions` block is touched: the developer-tier
|
|
314
|
+
* `tidy.enabled` and `tidy.model` are read-only in the screen, so this leaves them as they are and a
|
|
315
|
+
* save can never silently flip the deploy-time facts. A convention whose value is undefined (a
|
|
316
|
+
* collapsed multi-position toggle, off) is dropped, so the committed block carries only the on
|
|
317
|
+
* toggles, the same shape `resolveTidyConventions` fills the defaults back from on read.
|
|
318
|
+
*/
|
|
319
|
+
export function setTidy(raw: string, conventions: Partial<TidyConventions>): string {
|
|
320
|
+
const doc = parseDocument(raw);
|
|
321
|
+
if (doc.get('siteName') === undefined) {
|
|
322
|
+
throw new SiteConfigError('Site config must be a mapping with a siteName');
|
|
323
|
+
}
|
|
324
|
+
// Drop undefined-valued keys (a collapsed multi-position toggle) so the committed YAML carries only
|
|
325
|
+
// the enabled conventions; the resolver fills the rest back from defaults on read.
|
|
326
|
+
const block: Record<string, unknown> = {};
|
|
327
|
+
for (const [key, value] of Object.entries(conventions)) {
|
|
328
|
+
if (value !== undefined) block[key] = value;
|
|
329
|
+
}
|
|
330
|
+
doc.setIn(['tidy', 'conventions'], block);
|
|
331
|
+
return doc.toString();
|
|
332
|
+
}
|
|
@@ -16,13 +16,14 @@ export type AdminView =
|
|
|
16
16
|
| { view: 'edit'; concept: ConceptDescriptor; id: string }
|
|
17
17
|
| { view: 'editors' }
|
|
18
18
|
| { view: 'nav' }
|
|
19
|
-
| { view: 'media' }
|
|
19
|
+
| { view: 'media' }
|
|
20
|
+
| { view: 'settings' };
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Fixed first segments that never resolve as concepts. The engine only allows posts and pages
|
|
23
24
|
* today, so no collision is possible, but the parser does not depend on that: a reserved
|
|
24
|
-
* segment wins before concept lookup. `settings`
|
|
25
|
-
*
|
|
25
|
+
* segment wins before concept lookup. `settings`, `nav`, and `media` are decided as views below,
|
|
26
|
+
* so they are not in this no-view set.
|
|
26
27
|
*/
|
|
27
28
|
const RESERVED_SEGMENTS = new Set(['login', 'auth', 'editors', 'nav', 'settings']);
|
|
28
29
|
|
|
@@ -62,6 +63,9 @@ export function parseAdminPath(
|
|
|
62
63
|
// reserved-no-view set. /admin/media/<anything> 404s naturally (media is not a configured
|
|
63
64
|
// concept), which is the correct shape.
|
|
64
65
|
if (head === 'media') return { view: 'media' };
|
|
66
|
+
// settings is its own view, a peer of editors and nav. /admin/settings/<anything> 404s naturally
|
|
67
|
+
// (the two-segment branch never matches settings), which is the correct shape.
|
|
68
|
+
if (head === 'settings') return { view: 'settings' };
|
|
65
69
|
if (RESERVED_SEGMENTS.has(head)) return null;
|
|
66
70
|
const concept = findConcept(concepts, head);
|
|
67
71
|
return concept ? { view: 'list', concept } : null;
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type ListData,
|
|
16
16
|
type EditData,
|
|
17
17
|
type MediaLibraryData,
|
|
18
|
+
type SettingsData,
|
|
18
19
|
} from './content-routes.js';
|
|
19
20
|
import { createEditorRoutes } from './editors-routes.js';
|
|
20
21
|
import { createNavRoutes, type NavLoadData } from './nav-routes.js';
|
|
@@ -41,6 +42,11 @@ export interface CairnAdminDeps {
|
|
|
41
42
|
branding?: AuthBranding;
|
|
42
43
|
send?: SendMagicLink;
|
|
43
44
|
mintToken?: ContentRoutesDeps['mintToken'];
|
|
45
|
+
/** Build the Anthropic client for the tidy action. Forwarded to the content routes; a site that
|
|
46
|
+
* enables tidy injects a stub here to avoid a real network call. Defaults to the real SDK client. */
|
|
47
|
+
anthropic?: ContentRoutesDeps['anthropic'];
|
|
48
|
+
/** The tidy action's own request deadline in milliseconds. Forwarded to the content routes. */
|
|
49
|
+
tidyTimeoutMs?: ContentRoutesDeps['tidyTimeoutMs'];
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
/**
|
|
@@ -55,7 +61,8 @@ export type AdminData =
|
|
|
55
61
|
| { view: 'edit'; layout: LayoutData; page: EditData }
|
|
56
62
|
| { view: 'editors'; layout: LayoutData; page: { editors: Editor[]; self: string } }
|
|
57
63
|
| { view: 'nav'; layout: LayoutData; page: NavLoadData }
|
|
58
|
-
| { view: 'media'; layout: LayoutData; page: MediaLibraryData }
|
|
64
|
+
| { view: 'media'; layout: LayoutData; page: MediaLibraryData }
|
|
65
|
+
| { view: 'settings'; layout: LayoutData; page: SettingsData };
|
|
59
66
|
|
|
60
67
|
export function createCairnAdmin(runtime: CairnRuntime, deps: CairnAdminDeps = {}) {
|
|
61
68
|
// The runtime already composes the site name and the sender identity, so the magic-link
|
|
@@ -66,7 +73,11 @@ export function createCairnAdmin(runtime: CairnRuntime, deps: CairnAdminDeps = {
|
|
|
66
73
|
replyTo: runtime.sender.replyTo,
|
|
67
74
|
};
|
|
68
75
|
const auth = createAuthRoutes({ branding, send: deps.send });
|
|
69
|
-
const content = createContentRoutes(runtime, {
|
|
76
|
+
const content = createContentRoutes(runtime, {
|
|
77
|
+
mintToken: deps.mintToken,
|
|
78
|
+
anthropic: deps.anthropic,
|
|
79
|
+
tidyTimeoutMs: deps.tidyTimeoutMs,
|
|
80
|
+
});
|
|
70
81
|
const editors = createEditorRoutes();
|
|
71
82
|
// The nav surface exists only when the site configures a menu; without one its view is a 404.
|
|
72
83
|
const nav = runtime.navMenu ? createNavRoutes(runtime, { mintToken: deps.mintToken }) : null;
|
|
@@ -129,6 +140,11 @@ export function createCairnAdmin(runtime: CairnRuntime, deps: CairnAdminDeps = {
|
|
|
129
140
|
const [layout, page] = await Promise.all([content.layoutLoad(delegated), content.mediaLibraryLoad(delegated)]);
|
|
130
141
|
return { view: 'media', layout, page };
|
|
131
142
|
}
|
|
143
|
+
case 'settings': {
|
|
144
|
+
const delegated = contentEvent(event, {});
|
|
145
|
+
const [layout, page] = await Promise.all([content.layoutLoad(delegated), content.settingsLoad(delegated)]);
|
|
146
|
+
return { view: 'settings', layout, page };
|
|
147
|
+
}
|
|
132
148
|
}
|
|
133
149
|
}
|
|
134
150
|
|
|
@@ -148,9 +164,9 @@ export function createCairnAdmin(runtime: CairnRuntime, deps: CairnAdminDeps = {
|
|
|
148
164
|
}
|
|
149
165
|
|
|
150
166
|
// The topbar posts publishAll from every authed admin page; login and confirm may not.
|
|
151
|
-
const authedViews = ['list', 'edit', 'editors', 'nav', 'media'] as const;
|
|
167
|
+
const authedViews = ['list', 'edit', 'editors', 'nav', 'media', 'settings'] as const;
|
|
152
168
|
// An editor signs out from wherever they are, so logout accepts any parsed view.
|
|
153
|
-
const anyView = ['index', 'login', 'confirm', 'list', 'edit', 'editors', 'nav', 'media'] as const;
|
|
169
|
+
const anyView = ['index', 'login', 'confirm', 'list', 'edit', 'editors', 'nav', 'media', 'settings'] as const;
|
|
154
170
|
|
|
155
171
|
/** The full admin action vocabulary, one named async function per action, so a site's
|
|
156
172
|
* catch-all route exports `admin.actions` directly. Each wrapper stays thin: parse,
|
|
@@ -166,10 +182,22 @@ export function createCairnAdmin(runtime: CairnRuntime, deps: CairnAdminDeps = {
|
|
|
166
182
|
if (!nav) throw error(404, 'Not found');
|
|
167
183
|
return nav.navSave(contentEvent(event, {}));
|
|
168
184
|
}),
|
|
185
|
+
// The tidy settings save (spec 2.8, Task 15): the editor commits the per-convention block to the
|
|
186
|
+
// committed YAML. Gated to the settings view, so it 404s elsewhere; the action itself 404s again
|
|
187
|
+
// when tidy is off, the server half of the truthful visibility gate.
|
|
188
|
+
saveSettings: viewAction(['settings'], (event) => content.settingsSave(contentEvent(event, {}))),
|
|
169
189
|
upload: viewAction(['edit'], (event, view) => content.uploadAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
170
190
|
publish: viewAction(['edit'], (event, view) => content.publishAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
171
191
|
discard: viewAction(['edit'], (event, view) => content.discardAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
172
192
|
rename: viewAction(['edit'], (event, view) => content.renameAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
193
|
+
// The personal-dictionary add (spec 1.6): the editor commits its pending add-to-dictionary words at
|
|
194
|
+
// save time. Gated to the edit view, where the spellcheck surface lives, so it 404s elsewhere.
|
|
195
|
+
addDictionaryWord: viewAction(['edit'], (event, view) =>
|
|
196
|
+
content.addDictionaryWord(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
197
|
+
// Tidy (spec 2.1): the editor posts the buffer to `?/tidy` for a light LLM copy-edit. Gated to the
|
|
198
|
+
// edit view, where the review surface lives, so it 404s elsewhere.
|
|
199
|
+
tidy: viewAction(['edit'], (event, view) =>
|
|
200
|
+
content.tidyAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
173
201
|
delete: viewAction(['edit', 'list'], (event, view) =>
|
|
174
202
|
view.view === 'edit'
|
|
175
203
|
? content.deleteAction(contentEvent(event, { concept: view.concept.id, id: view.id }))
|