@glw907/cairn-cms 0.58.0 → 0.60.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +84 -0
- package/dist/components/CairnAdmin.svelte +3 -0
- package/dist/components/CairnMediaLibrary.svelte +1101 -27
- package/dist/components/CairnMediaLibrary.svelte.d.ts +10 -2
- package/dist/components/CairnTidySettings.svelte +553 -0
- package/dist/components/CairnTidySettings.svelte.d.ts +32 -0
- package/dist/components/EditPage.svelte +371 -2
- package/dist/components/MarkdownEditor.svelte +168 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +44 -0
- package/dist/components/TidyReview.svelte +463 -0
- package/dist/components/TidyReview.svelte.d.ts +47 -0
- package/dist/components/admin-icons.d.ts +1 -0
- package/dist/components/admin-icons.js +1 -0
- package/dist/components/cairn-admin.css +913 -2
- 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 +146 -0
- package/dist/components/spellcheck.js +541 -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/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/media/bulk-delete-plan.d.ts +24 -0
- package/dist/media/bulk-delete-plan.js +25 -0
- package/dist/media/orphan-scan.d.ts +37 -0
- package/dist/media/orphan-scan.js +42 -0
- package/dist/media/reconcile.d.ts +3 -0
- package/dist/media/reconcile.js +3 -2
- 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 +16 -1
- package/dist/sveltekit/cairn-admin.js +28 -3
- package/dist/sveltekit/content-routes.d.ts +171 -4
- package/dist/sveltekit/content-routes.js +597 -3
- package/dist/sveltekit/index.d.ts +1 -1
- package/dist/sveltekit/tidy-prompt.d.ts +11 -0
- package/dist/sveltekit/tidy-prompt.js +118 -0
- package/package.json +10 -1
- package/src/lib/components/CairnAdmin.svelte +3 -0
- package/src/lib/components/CairnMediaLibrary.svelte +1101 -27
- 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/admin-icons.ts +1 -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 +679 -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 +9 -1
- package/src/lib/media/bulk-delete-plan.ts +54 -0
- package/src/lib/media/orphan-scan.ts +74 -0
- package/src/lib/media/reconcile.ts +3 -2
- 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 +38 -4
- package/src/lib/sveltekit/content-routes.ts +795 -7
- package/src/lib/sveltekit/index.ts +1 -0
- package/src/lib/sveltekit/tidy-prompt.ts +153 -0
|
@@ -34,8 +34,96 @@ export interface SiteConfig {
|
|
|
34
34
|
menus?: Record<string, unknown>;
|
|
35
35
|
/** Per-concept URL policy: the permalink pattern and date-prefix granularity, keyed by concept id. */
|
|
36
36
|
content?: Record<string, ConceptUrlPolicy>;
|
|
37
|
+
/** The editor spellcheck settings. The dialect is declared once per site (spec 1.2), so a British
|
|
38
|
+
* site loads the British word list and "colour" reads as correct. Today only US English ships, so an
|
|
39
|
+
* unset or unknown dialect resolves to it. */
|
|
40
|
+
spellcheck?: {
|
|
41
|
+
dialect?: string;
|
|
42
|
+
};
|
|
43
|
+
/** The editor tidy (LLM copy-edit) settings. Opt-in at the site level (spec 2.8): tidy is a remote,
|
|
44
|
+
* costly model call, so the whole block is optional and `enabled` defaults false. The model is a
|
|
45
|
+
* developer-tier fact; the `conventions` block is the editor-tier per-convention config that builds
|
|
46
|
+
* the prompt's CONVENTIONS section. The Anthropic API key is a Worker secret, never config. */
|
|
47
|
+
tidy?: TidyConfig;
|
|
37
48
|
[key: string]: unknown;
|
|
38
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The tidy block on the site config. Every field is optional so the YAML can carry as little as
|
|
52
|
+
* `tidy: { enabled: true }` and the defaults fill the rest.
|
|
53
|
+
*/
|
|
54
|
+
export interface TidyConfig {
|
|
55
|
+
/** Master switch. Default false; tidy is opt-in (spec 2.8, decision 1). */
|
|
56
|
+
enabled?: boolean;
|
|
57
|
+
/** The model id. Default `claude-sonnet-4-6`; the alternative is `claude-haiku-4-5` (spec 2.2). */
|
|
58
|
+
model?: string;
|
|
59
|
+
/** The per-convention toggles that build the prompt's CONVENTIONS section. */
|
|
60
|
+
conventions?: Partial<TidyConventions>;
|
|
61
|
+
}
|
|
62
|
+
/** The default tidy model when a site sets none: Sonnet, the judgment floor for a light copy-edit. */
|
|
63
|
+
export declare const DEFAULT_TIDY_MODEL = "claude-sonnet-4-6";
|
|
64
|
+
/**
|
|
65
|
+
* The corrected convention set (spec "The corrected convention set"), the resolved shape the prompt
|
|
66
|
+
* builder consumes. Every field carries a concrete value; `resolveTidyConventions` fills the defaults
|
|
67
|
+
* from a partial config. The Fixes group is the objective fixes (default on, governed by the always-on
|
|
68
|
+
* core); the style tier defaults off (a falsy variant means off); the advanced tier defaults off.
|
|
69
|
+
* Sentence spacing is dropped on purpose and regional spelling is `spellcheck.dialect`, not a toggle.
|
|
70
|
+
*/
|
|
71
|
+
export interface TidyConventions {
|
|
72
|
+
/** The objective Fixes group (spelling, grammar, doubled words, whitespace, capitals, terminal
|
|
73
|
+
* punctuation). Default on. The always-on core governs it; this toggle lets the screen turn the
|
|
74
|
+
* group off. */
|
|
75
|
+
fixes: boolean;
|
|
76
|
+
/** Oxford comma position. Off when undefined; `always` | `complex-only` (AP) | `never`. */
|
|
77
|
+
oxfordComma?: 'always' | 'complex-only' | 'never';
|
|
78
|
+
/** Number style threshold. Off when undefined; the always-numeral exception sets (ages, dates,
|
|
79
|
+
* measurements, percentages) apply at any threshold. */
|
|
80
|
+
numberStyle?: 'under-ten' | 'under-hundred' | 'always-numerals';
|
|
81
|
+
/** Measurement notation only (never the system, never the number). Off when undefined. */
|
|
82
|
+
measurements?: 'abbreviate' | 'spell-out';
|
|
83
|
+
/** Percent rendering. Off when undefined; `sign` is "%", `word` is "percent". */
|
|
84
|
+
percent?: 'sign' | 'word';
|
|
85
|
+
/** Em-dash spacing. Off when undefined. */
|
|
86
|
+
emDash?: 'spaced' | 'closed';
|
|
87
|
+
/** Turn a hyphen between two numbers into an en dash. Default off. */
|
|
88
|
+
enDashRanges: boolean;
|
|
89
|
+
/** Ellipsis rendering. Off when undefined. */
|
|
90
|
+
ellipsis?: 'single-char' | 'three-dots';
|
|
91
|
+
/** Time format. Off when undefined. */
|
|
92
|
+
timeFormat?: '5 PM' | '5pm' | '5 p.m.';
|
|
93
|
+
/** Advanced: convert straight quotes to curly with the full apostrophe rule set. Default off. */
|
|
94
|
+
smartQuotes: boolean;
|
|
95
|
+
/** Advanced: correct brand and proper-noun capitalization on a curated list only. Default off. */
|
|
96
|
+
brandCaps: boolean;
|
|
97
|
+
}
|
|
98
|
+
/** The resting tidy convention set: Fixes on, every style and advanced toggle off. */
|
|
99
|
+
export declare function defaultTidyConventions(): TidyConventions;
|
|
100
|
+
/**
|
|
101
|
+
* Resolve a partial conventions config (from the YAML) into the concrete TidyConventions the prompt
|
|
102
|
+
* builder consumes. An absent field falls to its default: Fixes on, the style and advanced toggles
|
|
103
|
+
* off. A multi-position toggle stays undefined (off) unless the config names a variant.
|
|
104
|
+
*/
|
|
105
|
+
export declare function resolveTidyConventions(partial: Partial<TidyConventions> | undefined): TidyConventions;
|
|
106
|
+
export declare class TidyConventionsError extends Error {
|
|
107
|
+
/** A malformed settings payload maps to the same diagnostic as a malformed config. */
|
|
108
|
+
readonly conditionId = "config.site-config-invalid";
|
|
109
|
+
constructor(message: string);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Validate and normalize an untrusted conventions object (from the settings form) into a concrete
|
|
113
|
+
* TidyConventions. This input is committed to the repo, so every field is bounded to its known set:
|
|
114
|
+
* a boolean toggle must be a boolean, and a multi-position toggle must be one of its listed variants
|
|
115
|
+
* or absent (off). An unknown key is dropped rather than carried, so the committed block can never
|
|
116
|
+
* grow a junk key. Throws TidyConventionsError on a value outside its allowed set.
|
|
117
|
+
*/
|
|
118
|
+
export declare function validateTidyConventions(value: unknown): TidyConventions;
|
|
119
|
+
/** The dialect string when a site sets none: US English, the only dictionary that ships today. */
|
|
120
|
+
export declare const DEFAULT_DIALECT = "en-US";
|
|
121
|
+
/**
|
|
122
|
+
* The dictionary asset file for a site's configured dialect, defaulting to US English. The main thread
|
|
123
|
+
* resolves this filename to a real URL (the spike's out-of-bundle asset) and hands it to the Worker in
|
|
124
|
+
* the `init` message; the Worker never reads config. An unknown dialect falls back to the default file.
|
|
125
|
+
*/
|
|
126
|
+
export declare function dictionaryFileForDialect(dialect: string | undefined): string;
|
|
39
127
|
export declare class SiteConfigError extends Error {
|
|
40
128
|
/** The registered diagnostic condition a malformed site config maps to (mirrors CairnError). */
|
|
41
129
|
readonly conditionId = "config.site-config-invalid";
|
|
@@ -54,3 +142,13 @@ export declare function urlPolicyFrom(config: SiteConfig): Record<string, Concep
|
|
|
54
142
|
* serializes without `url`/`children` keys.
|
|
55
143
|
*/
|
|
56
144
|
export declare function setMenu(raw: string, name: string, tree: NavNode[]): string;
|
|
145
|
+
/**
|
|
146
|
+
* Write the editor-tier tidy conventions into the YAML site-config text and reserialize, preserving
|
|
147
|
+
* every other top-level key and the file's comments and key order (parseDocument round-trips both,
|
|
148
|
+
* the same machinery setMenu uses). Only the `tidy.conventions` block is touched: the developer-tier
|
|
149
|
+
* `tidy.enabled` and `tidy.model` are read-only in the screen, so this leaves them as they are and a
|
|
150
|
+
* save can never silently flip the deploy-time facts. A convention whose value is undefined (a
|
|
151
|
+
* collapsed multi-position toggle, off) is dropped, so the committed block carries only the on
|
|
152
|
+
* toggles, the same shape `resolveTidyConventions` fills the defaults back from on read.
|
|
153
|
+
*/
|
|
154
|
+
export declare function setTidy(raw: string, conventions: Partial<TidyConventions>): string;
|
package/dist/nav/site-config.js
CHANGED
|
@@ -59,6 +59,114 @@ export function validateNavTree(value, maxDepth) {
|
|
|
59
59
|
}
|
|
60
60
|
return walk(value, 1);
|
|
61
61
|
}
|
|
62
|
+
/** The default tidy model when a site sets none: Sonnet, the judgment floor for a light copy-edit. */
|
|
63
|
+
export const DEFAULT_TIDY_MODEL = 'claude-sonnet-4-6';
|
|
64
|
+
/** The resting tidy convention set: Fixes on, every style and advanced toggle off. */
|
|
65
|
+
export function defaultTidyConventions() {
|
|
66
|
+
return { fixes: true, enDashRanges: false, smartQuotes: false, brandCaps: false };
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a partial conventions config (from the YAML) into the concrete TidyConventions the prompt
|
|
70
|
+
* builder consumes. An absent field falls to its default: Fixes on, the style and advanced toggles
|
|
71
|
+
* off. A multi-position toggle stays undefined (off) unless the config names a variant.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveTidyConventions(partial) {
|
|
74
|
+
const base = defaultTidyConventions();
|
|
75
|
+
if (partial === undefined)
|
|
76
|
+
return base;
|
|
77
|
+
return {
|
|
78
|
+
fixes: partial.fixes ?? base.fixes,
|
|
79
|
+
oxfordComma: partial.oxfordComma,
|
|
80
|
+
numberStyle: partial.numberStyle,
|
|
81
|
+
measurements: partial.measurements,
|
|
82
|
+
percent: partial.percent,
|
|
83
|
+
emDash: partial.emDash,
|
|
84
|
+
enDashRanges: partial.enDashRanges ?? base.enDashRanges,
|
|
85
|
+
ellipsis: partial.ellipsis,
|
|
86
|
+
timeFormat: partial.timeFormat,
|
|
87
|
+
smartQuotes: partial.smartQuotes ?? base.smartQuotes,
|
|
88
|
+
brandCaps: partial.brandCaps ?? base.brandCaps,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
// The allowed values for each multi-position style toggle, the single source the validator checks an
|
|
92
|
+
// untrusted settings payload against. A value outside its list is rejected, so the committed YAML can
|
|
93
|
+
// only ever carry a known variant the prompt builder understands.
|
|
94
|
+
const OXFORD_COMMA_VALUES = ['always', 'complex-only', 'never'];
|
|
95
|
+
const NUMBER_STYLE_VALUES = ['under-ten', 'under-hundred', 'always-numerals'];
|
|
96
|
+
const MEASUREMENTS_VALUES = ['abbreviate', 'spell-out'];
|
|
97
|
+
const PERCENT_VALUES = ['sign', 'word'];
|
|
98
|
+
const EM_DASH_VALUES = ['spaced', 'closed'];
|
|
99
|
+
const ELLIPSIS_VALUES = ['single-char', 'three-dots'];
|
|
100
|
+
const TIME_FORMAT_VALUES = ['5 PM', '5pm', '5 p.m.'];
|
|
101
|
+
export class TidyConventionsError extends Error {
|
|
102
|
+
/** A malformed settings payload maps to the same diagnostic as a malformed config. */
|
|
103
|
+
conditionId = 'config.site-config-invalid';
|
|
104
|
+
constructor(message) {
|
|
105
|
+
super(message);
|
|
106
|
+
this.name = 'TidyConventionsError';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Validate and normalize an untrusted conventions object (from the settings form) into a concrete
|
|
111
|
+
* TidyConventions. This input is committed to the repo, so every field is bounded to its known set:
|
|
112
|
+
* a boolean toggle must be a boolean, and a multi-position toggle must be one of its listed variants
|
|
113
|
+
* or absent (off). An unknown key is dropped rather than carried, so the committed block can never
|
|
114
|
+
* grow a junk key. Throws TidyConventionsError on a value outside its allowed set.
|
|
115
|
+
*/
|
|
116
|
+
export function validateTidyConventions(value) {
|
|
117
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
118
|
+
throw new TidyConventionsError('Tidy conventions must be an object');
|
|
119
|
+
}
|
|
120
|
+
const input = value;
|
|
121
|
+
function bool(key, fallback) {
|
|
122
|
+
const v = input[key];
|
|
123
|
+
if (v === undefined)
|
|
124
|
+
return fallback;
|
|
125
|
+
if (typeof v !== 'boolean')
|
|
126
|
+
throw new TidyConventionsError(`${key} must be true or false`);
|
|
127
|
+
return v;
|
|
128
|
+
}
|
|
129
|
+
function variant(key, allowed) {
|
|
130
|
+
const v = input[key];
|
|
131
|
+
if (v === undefined || v === null || v === false)
|
|
132
|
+
return undefined;
|
|
133
|
+
if (typeof v !== 'string' || !allowed.includes(v)) {
|
|
134
|
+
throw new TidyConventionsError(`${key} must be one of ${allowed.join(', ')}`);
|
|
135
|
+
}
|
|
136
|
+
return v;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
fixes: bool('fixes', true),
|
|
140
|
+
oxfordComma: variant('oxfordComma', OXFORD_COMMA_VALUES),
|
|
141
|
+
numberStyle: variant('numberStyle', NUMBER_STYLE_VALUES),
|
|
142
|
+
measurements: variant('measurements', MEASUREMENTS_VALUES),
|
|
143
|
+
percent: variant('percent', PERCENT_VALUES),
|
|
144
|
+
emDash: variant('emDash', EM_DASH_VALUES),
|
|
145
|
+
enDashRanges: bool('enDashRanges', false),
|
|
146
|
+
ellipsis: variant('ellipsis', ELLIPSIS_VALUES),
|
|
147
|
+
timeFormat: variant('timeFormat', TIME_FORMAT_VALUES),
|
|
148
|
+
smartQuotes: bool('smartQuotes', false),
|
|
149
|
+
brandCaps: bool('brandCaps', false),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
/** The dialect string when a site sets none: US English, the only dictionary that ships today. */
|
|
153
|
+
export const DEFAULT_DIALECT = 'en-US';
|
|
154
|
+
// The dialect-to-dictionary map. Only US English ships now; a new locale adds one entry here and one
|
|
155
|
+
// committed dictionary file under spellcheck-assets, and the rest of the chain (the main-thread URL
|
|
156
|
+
// resolution, the worker fetch) needs no change. An unknown or unset dialect falls back to the default
|
|
157
|
+
// rather than throwing, so a typo or a future-locale config never breaks the editor.
|
|
158
|
+
const DICTIONARY_BY_DIALECT = {
|
|
159
|
+
'en-US': 'dictionary-en-us.txt',
|
|
160
|
+
};
|
|
161
|
+
/**
|
|
162
|
+
* The dictionary asset file for a site's configured dialect, defaulting to US English. The main thread
|
|
163
|
+
* resolves this filename to a real URL (the spike's out-of-bundle asset) and hands it to the Worker in
|
|
164
|
+
* the `init` message; the Worker never reads config. An unknown dialect falls back to the default file.
|
|
165
|
+
*/
|
|
166
|
+
export function dictionaryFileForDialect(dialect) {
|
|
167
|
+
const key = dialect ?? DEFAULT_DIALECT;
|
|
168
|
+
return DICTIONARY_BY_DIALECT[key] ?? DICTIONARY_BY_DIALECT[DEFAULT_DIALECT];
|
|
169
|
+
}
|
|
62
170
|
export class SiteConfigError extends Error {
|
|
63
171
|
/** The registered diagnostic condition a malformed site config maps to (mirrors CairnError). */
|
|
64
172
|
conditionId = 'config.site-config-invalid';
|
|
@@ -104,3 +212,27 @@ export function setMenu(raw, name, tree) {
|
|
|
104
212
|
doc.setIn(['menus', name], tree);
|
|
105
213
|
return doc.toString();
|
|
106
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Write the editor-tier tidy conventions into the YAML site-config text and reserialize, preserving
|
|
217
|
+
* every other top-level key and the file's comments and key order (parseDocument round-trips both,
|
|
218
|
+
* the same machinery setMenu uses). Only the `tidy.conventions` block is touched: the developer-tier
|
|
219
|
+
* `tidy.enabled` and `tidy.model` are read-only in the screen, so this leaves them as they are and a
|
|
220
|
+
* save can never silently flip the deploy-time facts. A convention whose value is undefined (a
|
|
221
|
+
* collapsed multi-position toggle, off) is dropped, so the committed block carries only the on
|
|
222
|
+
* toggles, the same shape `resolveTidyConventions` fills the defaults back from on read.
|
|
223
|
+
*/
|
|
224
|
+
export function setTidy(raw, conventions) {
|
|
225
|
+
const doc = parseDocument(raw);
|
|
226
|
+
if (doc.get('siteName') === undefined) {
|
|
227
|
+
throw new SiteConfigError('Site config must be a mapping with a siteName');
|
|
228
|
+
}
|
|
229
|
+
// Drop undefined-valued keys (a collapsed multi-position toggle) so the committed YAML carries only
|
|
230
|
+
// the enabled conventions; the resolver fills the rest back from defaults on read.
|
|
231
|
+
const block = {};
|
|
232
|
+
for (const [key, value] of Object.entries(conventions)) {
|
|
233
|
+
if (value !== undefined)
|
|
234
|
+
block[key] = value;
|
|
235
|
+
}
|
|
236
|
+
doc.setIn(['tidy', 'conventions'], block);
|
|
237
|
+
return doc.toString();
|
|
238
|
+
}
|
|
@@ -3,8 +3,8 @@ import { isValidId } from '../content/ids.js';
|
|
|
3
3
|
/**
|
|
4
4
|
* Fixed first segments that never resolve as concepts. The engine only allows posts and pages
|
|
5
5
|
* today, so no collision is possible, but the parser does not depend on that: a reserved
|
|
6
|
-
* segment wins before concept lookup. `settings`
|
|
7
|
-
*
|
|
6
|
+
* segment wins before concept lookup. `settings`, `nav`, and `media` are decided as views below,
|
|
7
|
+
* so they are not in this no-view set.
|
|
8
8
|
*/
|
|
9
9
|
const RESERVED_SEGMENTS = new Set(['login', 'auth', 'editors', 'nav', 'settings']);
|
|
10
10
|
/**
|
|
@@ -47,6 +47,10 @@ export function parseAdminPath(pathname, concepts) {
|
|
|
47
47
|
// concept), which is the correct shape.
|
|
48
48
|
if (head === 'media')
|
|
49
49
|
return { view: 'media' };
|
|
50
|
+
// settings is its own view, a peer of editors and nav. /admin/settings/<anything> 404s naturally
|
|
51
|
+
// (the two-segment branch never matches settings), which is the correct shape.
|
|
52
|
+
if (head === 'settings')
|
|
53
|
+
return { view: 'settings' };
|
|
50
54
|
if (RESERVED_SEGMENTS.has(head))
|
|
51
55
|
return null;
|
|
52
56
|
const concept = findConcept(concepts, head);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ContentRoutesDeps, type LayoutData, type ListData, type EditData, type MediaLibraryData } from './content-routes.js';
|
|
1
|
+
import { type ContentRoutesDeps, type LayoutData, type ListData, type EditData, type MediaLibraryData, type SettingsData } from './content-routes.js';
|
|
2
2
|
import { type NavLoadData } from './nav-routes.js';
|
|
3
3
|
import type { AuthBranding, SendMagicLink } from '../email.js';
|
|
4
4
|
import type { AuthEnv, Editor } from '../auth/types.js';
|
|
@@ -21,6 +21,11 @@ export interface CairnAdminDeps {
|
|
|
21
21
|
branding?: AuthBranding;
|
|
22
22
|
send?: SendMagicLink;
|
|
23
23
|
mintToken?: ContentRoutesDeps['mintToken'];
|
|
24
|
+
/** Build the Anthropic client for the tidy action. Forwarded to the content routes; a site that
|
|
25
|
+
* enables tidy injects a stub here to avoid a real network call. Defaults to the real SDK client. */
|
|
26
|
+
anthropic?: ContentRoutesDeps['anthropic'];
|
|
27
|
+
/** The tidy action's own request deadline in milliseconds. Forwarded to the content routes. */
|
|
28
|
+
tidyTimeoutMs?: ContentRoutesDeps['tidyTimeoutMs'];
|
|
24
29
|
}
|
|
25
30
|
/**
|
|
26
31
|
* One admin view's data, discriminated for the admin page component's switch. The public
|
|
@@ -65,6 +70,10 @@ export type AdminData = {
|
|
|
65
70
|
view: 'media';
|
|
66
71
|
layout: LayoutData;
|
|
67
72
|
page: MediaLibraryData;
|
|
73
|
+
} | {
|
|
74
|
+
view: 'settings';
|
|
75
|
+
layout: LayoutData;
|
|
76
|
+
page: SettingsData;
|
|
68
77
|
};
|
|
69
78
|
export declare function createCairnAdmin(runtime: CairnRuntime, deps?: CairnAdminDeps): {
|
|
70
79
|
load: (event: AdminEvent) => Promise<AdminData>;
|
|
@@ -74,10 +83,13 @@ export declare function createCairnAdmin(runtime: CairnRuntime, deps?: CairnAdmi
|
|
|
74
83
|
logout: (event: AdminEvent) => Promise<never>;
|
|
75
84
|
create: (event: AdminEvent) => Promise<never>;
|
|
76
85
|
save: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
86
|
+
saveSettings: (event: AdminEvent) => Promise<never>;
|
|
77
87
|
upload: (event: AdminEvent) => Promise<import("./content-routes.js").UploadResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
78
88
|
publish: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
79
89
|
discard: (event: AdminEvent) => Promise<never>;
|
|
80
90
|
rename: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
91
|
+
addDictionaryWord: (event: AdminEvent) => Promise<import("./content-routes.js").DictionaryAddResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
92
|
+
tidy: (event: AdminEvent) => Promise<import("./content-routes.js").TidyResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
81
93
|
delete: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
82
94
|
mediaDelete: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
83
95
|
mediaUpdate: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
@@ -86,6 +98,9 @@ export declare function createCairnAdmin(runtime: CairnRuntime, deps?: CairnAdmi
|
|
|
86
98
|
mediaReplace: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
87
99
|
mediaAltPreview: (event: AdminEvent) => Promise<import("./content-routes.js").MediaAltPreviewPlan | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
88
100
|
mediaAltPropagate: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
101
|
+
mediaBulkDelete: (event: AdminEvent) => Promise<import("./content-routes.js").MediaBulkDeleteResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
102
|
+
mediaOrphanScan: (event: AdminEvent) => Promise<import("../media/orphan-scan.js").OrphanScan | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
103
|
+
mediaPurge: (event: AdminEvent) => Promise<import("./content-routes.js").MediaOrphanPurgeResult | import("@sveltejs/kit").ActionFailure<unknown>>;
|
|
89
104
|
publishAll: (event: AdminEvent) => Promise<never>;
|
|
90
105
|
addEditor: (event: AdminEvent) => Promise<import("@sveltejs/kit").ActionFailure<{
|
|
91
106
|
error: string;
|
|
@@ -19,7 +19,11 @@ export function createCairnAdmin(runtime, deps = {}) {
|
|
|
19
19
|
replyTo: runtime.sender.replyTo,
|
|
20
20
|
};
|
|
21
21
|
const auth = createAuthRoutes({ branding, send: deps.send });
|
|
22
|
-
const content = createContentRoutes(runtime, {
|
|
22
|
+
const content = createContentRoutes(runtime, {
|
|
23
|
+
mintToken: deps.mintToken,
|
|
24
|
+
anthropic: deps.anthropic,
|
|
25
|
+
tidyTimeoutMs: deps.tidyTimeoutMs,
|
|
26
|
+
});
|
|
23
27
|
const editors = createEditorRoutes();
|
|
24
28
|
// The nav surface exists only when the site configures a menu; without one its view is a 404.
|
|
25
29
|
const nav = runtime.navMenu ? createNavRoutes(runtime, { mintToken: deps.mintToken }) : null;
|
|
@@ -82,6 +86,11 @@ export function createCairnAdmin(runtime, deps = {}) {
|
|
|
82
86
|
const [layout, page] = await Promise.all([content.layoutLoad(delegated), content.mediaLibraryLoad(delegated)]);
|
|
83
87
|
return { view: 'media', layout, page };
|
|
84
88
|
}
|
|
89
|
+
case 'settings': {
|
|
90
|
+
const delegated = contentEvent(event, {});
|
|
91
|
+
const [layout, page] = await Promise.all([content.layoutLoad(delegated), content.settingsLoad(delegated)]);
|
|
92
|
+
return { view: 'settings', layout, page };
|
|
93
|
+
}
|
|
85
94
|
}
|
|
86
95
|
}
|
|
87
96
|
/** Wrap a delegate in the parse-and-check every action shares: parse the pathname exactly
|
|
@@ -97,9 +106,9 @@ export function createCairnAdmin(runtime, deps = {}) {
|
|
|
97
106
|
};
|
|
98
107
|
}
|
|
99
108
|
// The topbar posts publishAll from every authed admin page; login and confirm may not.
|
|
100
|
-
const authedViews = ['list', 'edit', 'editors', 'nav', 'media'];
|
|
109
|
+
const authedViews = ['list', 'edit', 'editors', 'nav', 'media', 'settings'];
|
|
101
110
|
// An editor signs out from wherever they are, so logout accepts any parsed view.
|
|
102
|
-
const anyView = ['index', 'login', 'confirm', 'list', 'edit', 'editors', 'nav', 'media'];
|
|
111
|
+
const anyView = ['index', 'login', 'confirm', 'list', 'edit', 'editors', 'nav', 'media', 'settings'];
|
|
103
112
|
/** The full admin action vocabulary, one named async function per action, so a site's
|
|
104
113
|
* catch-all route exports `admin.actions` directly. Each wrapper stays thin: parse,
|
|
105
114
|
* validate the view, synthesize the params the wrapped action reads, delegate. The
|
|
@@ -116,10 +125,20 @@ export function createCairnAdmin(runtime, deps = {}) {
|
|
|
116
125
|
throw error(404, 'Not found');
|
|
117
126
|
return nav.navSave(contentEvent(event, {}));
|
|
118
127
|
}),
|
|
128
|
+
// The tidy settings save (spec 2.8, Task 15): the editor commits the per-convention block to the
|
|
129
|
+
// committed YAML. Gated to the settings view, so it 404s elsewhere; the action itself 404s again
|
|
130
|
+
// when tidy is off, the server half of the truthful visibility gate.
|
|
131
|
+
saveSettings: viewAction(['settings'], (event) => content.settingsSave(contentEvent(event, {}))),
|
|
119
132
|
upload: viewAction(['edit'], (event, view) => content.uploadAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
120
133
|
publish: viewAction(['edit'], (event, view) => content.publishAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
121
134
|
discard: viewAction(['edit'], (event, view) => content.discardAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
122
135
|
rename: viewAction(['edit'], (event, view) => content.renameAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
136
|
+
// The personal-dictionary add (spec 1.6): the editor commits its pending add-to-dictionary words at
|
|
137
|
+
// save time. Gated to the edit view, where the spellcheck surface lives, so it 404s elsewhere.
|
|
138
|
+
addDictionaryWord: viewAction(['edit'], (event, view) => content.addDictionaryWord(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
139
|
+
// Tidy (spec 2.1): the editor posts the buffer to `?/tidy` for a light LLM copy-edit. Gated to the
|
|
140
|
+
// edit view, where the review surface lives, so it 404s elsewhere.
|
|
141
|
+
tidy: viewAction(['edit'], (event, view) => content.tidyAction(contentEvent(event, { concept: view.concept.id, id: view.id }))),
|
|
123
142
|
delete: viewAction(['edit', 'list'], (event, view) => view.view === 'edit'
|
|
124
143
|
? content.deleteAction(contentEvent(event, { concept: view.concept.id, id: view.id }))
|
|
125
144
|
: content.listDeleteAction(contentEvent(event, { concept: view.concept.id }))),
|
|
@@ -134,6 +153,12 @@ export function createCairnAdmin(runtime, deps = {}) {
|
|
|
134
153
|
mediaReplace: viewAction(['media'], (event) => content.mediaReplaceApply(contentEvent(event, {}))),
|
|
135
154
|
mediaAltPreview: viewAction(['media'], (event) => content.mediaAltPreview(contentEvent(event, {}))),
|
|
136
155
|
mediaAltPropagate: viewAction(['media'], (event) => content.mediaAltApply(contentEvent(event, {}))),
|
|
156
|
+
// Pass C library actions: a multi-select bulk delete, the on-demand orphan scan, and the
|
|
157
|
+
// irreversible byte purge. The component posts to `?/mediaBulkDelete`, `?/mediaOrphanScan`, and
|
|
158
|
+
// `?/mediaPurge` (the purge key is short of its content method name). All gate on the media view.
|
|
159
|
+
mediaBulkDelete: viewAction(['media'], (event) => content.mediaBulkDelete(contentEvent(event, {}))),
|
|
160
|
+
mediaOrphanScan: viewAction(['media'], (event) => content.mediaOrphanScan(contentEvent(event, {}))),
|
|
161
|
+
mediaPurge: viewAction(['media'], (event) => content.mediaPurgeOrphans(contentEvent(event, {}))),
|
|
137
162
|
publishAll: viewAction(authedViews, (event) => content.publishAllAction(contentEvent(event, {}))),
|
|
138
163
|
addEditor: viewAction(['editors'], (event) => editors.addEditorAction(event)),
|
|
139
164
|
removeEditor: viewAction(['editors'], (event) => editors.removeEditorAction(event)),
|