@glw907/cairn-cms 0.60.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/components/AdminLayout.svelte +130 -229
  3. package/dist/components/CairnAdmin.svelte +10 -42
  4. package/dist/components/CairnLogo.svelte +1 -6
  5. package/dist/components/CairnMediaLibrary.svelte +821 -1210
  6. package/dist/components/CairnTidySettings.svelte +192 -259
  7. package/dist/components/ComponentForm.svelte +110 -185
  8. package/dist/components/ComponentInsertDialog.svelte +163 -283
  9. package/dist/components/ConceptList.svelte +111 -191
  10. package/dist/components/ConfirmPage.svelte +5 -12
  11. package/dist/components/CsrfField.svelte +5 -11
  12. package/dist/components/DeleteDialog.svelte +15 -42
  13. package/dist/components/EditPage.svelte +665 -1166
  14. package/dist/components/EditorToolbar.svelte +108 -170
  15. package/dist/components/IconPicker.svelte +23 -53
  16. package/dist/components/LinkPicker.svelte +34 -58
  17. package/dist/components/LoginPage.svelte +14 -27
  18. package/dist/components/ManageEditors.svelte +3 -15
  19. package/dist/components/MarkdownEditor.svelte +689 -957
  20. package/dist/components/MarkdownHelpDialog.svelte +8 -12
  21. package/dist/components/MediaCaptureCard.svelte +18 -57
  22. package/dist/components/MediaFigureControl.svelte +32 -71
  23. package/dist/components/MediaHeroField.svelte +210 -329
  24. package/dist/components/MediaInsertPopover.svelte +156 -283
  25. package/dist/components/MediaPicker.svelte +67 -131
  26. package/dist/components/NavTree.svelte +46 -78
  27. package/dist/components/RenameDialog.svelte +16 -43
  28. package/dist/components/ShortcutsDialog.svelte +9 -13
  29. package/dist/components/ShortcutsGrid.svelte +1 -2
  30. package/dist/components/TidyReview.svelte +140 -248
  31. package/dist/components/WebLinkDialog.svelte +19 -40
  32. package/dist/components/cairn-admin.css +4 -0
  33. package/dist/components/spellcheck.d.ts +3 -1
  34. package/dist/components/spellcheck.js +14 -2
  35. package/dist/delivery/CairnHead.svelte +8 -11
  36. package/package.json +2 -2
  37. package/src/lib/components/spellcheck.ts +16 -2
@@ -22,267 +22,200 @@ announces the new total; the per-keystroke diff examples are aria-hidden so the
22
22
  The save commits the conventions block to the same committed YAML the nav editor writes (one config
23
23
  home), diffable and shared across editors.
24
24
  -->
25
- <script lang="ts">
26
- import { untrack } from 'svelte';
27
- import CsrfField from './CsrfField.svelte';
28
- import CheckIcon from '@lucide/svelte/icons/check';
29
- import CircleIcon from '@lucide/svelte/icons/circle';
30
- import SettingsIcon from '@lucide/svelte/icons/settings';
31
- import LockIcon from '@lucide/svelte/icons/lock';
32
- import CodeIcon from '@lucide/svelte/icons/code-xml';
33
- import ListIcon from '@lucide/svelte/icons/list';
34
- import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
35
- import InfoIcon from '@lucide/svelte/icons/info';
36
- import ArrowRightIcon from '@lucide/svelte/icons/arrow-right';
37
- import SparklesIcon from '@lucide/svelte/icons/sparkles';
38
- import type { SettingsData } from '../sveltekit/content-routes.js';
39
- import type { TidyConventions } from '../nav/site-config.js';
40
-
41
- interface Props {
42
- /** The two-tier settings load: the read-only developer facts, the truthful gate flag, and the
43
- * resolved editor-tier conventions. */
44
- data: SettingsData;
45
- }
46
-
47
- let { data }: Props = $props();
48
-
49
- // The working copy of the editor-tier conventions: every control binds to this, and the save posts
50
- // it. Seeded once from the load's resolved conventions, so the resting state IS the committed state.
51
- // A fresh load remounts the screen (the route key), so seeding from the initial prop is correct.
52
- let conv = $state<TidyConventions>(untrack(() => ({ ...data.conventions })));
53
-
54
- // A multi-position style row: its config key, label, the variants (value + short label), and the
55
- // diff example for the chosen variant.
56
- type Variant = { value: string; label: string };
57
- type StyleRow = {
58
- key: keyof TidyConventions;
59
- name: string;
60
- /** The variants when the row is a multi-position toggle; absent for a plain on/off (en-dash). */
61
- variants?: Variant[];
62
- /** The radiogroup's label ("Write times as"). */
63
- variantLabel?: string;
64
- /** The generic "what it does" example, shown when the row is off (a hypothetical). */
65
- egBefore: string;
66
- egAfter: string;
67
- };
68
-
69
- // The style conventions, in mockup order. Each maps to one config field. The multi-position rows
70
- // carry their variants; en-dash is a plain on/off.
71
- const styleRows: StyleRow[] = [
72
- {
73
- key: 'oxfordComma',
74
- name: 'Oxford comma',
75
- variantLabel: 'Use the Oxford comma',
76
- variants: [
77
- { value: 'always', label: 'Always' },
78
- { value: 'complex-only', label: 'Only in complex lists' },
79
- { value: 'never', label: 'Never' },
80
- ],
81
- egBefore: 'wax, skins and poles',
82
- egAfter: 'wax, skins, and poles',
83
- },
84
- {
85
- key: 'emDash',
86
- name: 'Em-dash style',
87
- variantLabel: 'Write em dashes as',
88
- variants: [
89
- { value: 'spaced', label: 'Spaced' },
90
- { value: 'closed', label: 'Closed' },
91
- ],
92
- egBefore: 'grooming--early',
93
- egAfter: 'grooming—early',
94
- },
95
- { key: 'enDashRanges', name: 'En-dash in number ranges', egBefore: '9-11 am', egAfter: '9–11 am' },
96
- {
97
- key: 'ellipsis',
98
- name: 'Ellipsis',
99
- variantLabel: 'Write ellipses as',
100
- variants: [
101
- { value: 'single-char', label: 'One character' },
102
- { value: 'three-dots', label: 'Three dots' },
103
- ],
104
- egBefore: 'later...',
105
- egAfter: 'later…',
106
- },
107
- {
108
- key: 'timeFormat',
109
- name: 'Time format',
110
- variantLabel: 'Write times as',
111
- variants: [
112
- { value: '5 PM', label: '5 PM' },
113
- { value: '5pm', label: '5pm' },
114
- { value: '5 p.m.', label: '5 p.m.' },
115
- ],
116
- egBefore: 'doors at 5pm',
117
- egAfter: 'doors at 5 PM',
118
- },
119
- {
120
- key: 'numberStyle',
121
- name: 'Number style',
122
- variantLabel: 'Write numbers as',
123
- variants: [
124
- { value: 'under-ten', label: 'Spell out under ten' },
125
- { value: 'under-hundred', label: 'Spell out under 100' },
126
- { value: 'always-numerals', label: 'Always numerals' },
127
- ],
128
- egBefore: '7 inches of snow',
129
- egAfter: 'seven inches of snow',
130
- },
131
- {
132
- key: 'measurements',
133
- name: 'Measurements and units',
134
- variantLabel: 'Write units as',
135
- variants: [
136
- { value: 'abbreviate', label: 'Abbreviate' },
137
- { value: 'spell-out', label: 'Spell out' },
138
- ],
139
- egBefore: '15 centimeters',
140
- egAfter: '15 cm',
141
- },
142
- {
143
- key: 'percent',
144
- name: 'Percent',
145
- variantLabel: 'Write percent as',
146
- variants: [
147
- { value: 'sign', label: 'Sign (%)' },
148
- { value: 'word', label: 'Word (percent)' },
149
- ],
150
- egBefore: '30 percent',
151
- egAfter: '30%',
152
- },
153
- ];
154
-
155
- // The advanced (higher-risk) rows: plain on/off booleans behind a disclosure.
156
- const advancedRows: { key: keyof TidyConventions; name: string; egBefore: string; egAfter: string }[] = [
157
- { key: 'smartQuotes', name: 'Curly quotes', egBefore: '"groomed"', egAfter: '“groomed”' },
158
- { key: 'brandCaps', name: 'Brand and proper-noun capitals', egBefore: 'github', egAfter: 'GitHub' },
159
- ];
160
-
161
- // --- whether a row is on, generic over the config shape ---
162
- // A boolean field is on when true; a multi-position field is on when it carries a variant.
163
- function rowOn(key: keyof TidyConventions): boolean {
164
- const v = conv[key];
165
- return typeof v === 'boolean' ? v : v !== undefined;
166
- }
167
-
168
- // The default variant a multi-position toggle takes when turned on: the first listed (the mockup's
169
- // leading position). A plain on/off uses true.
170
- function defaultVariant(row: StyleRow): string | boolean {
171
- return row.variants ? row.variants[0].value : true;
172
- }
173
-
174
- function toggleStyle(row: StyleRow) {
175
- if (rowOn(row.key)) {
176
- // Off: a multi-position field collapses to undefined; a boolean field to false.
177
- (conv[row.key] as unknown) = row.variants ? undefined : false;
178
- } else {
179
- (conv[row.key] as unknown) = defaultVariant(row);
180
- }
181
- }
182
-
183
- function toggleBool(key: keyof TidyConventions) {
184
- (conv[key] as unknown) = !rowOn(key);
185
- }
186
-
187
- function pickVariant(key: keyof TidyConventions, value: string) {
188
- (conv[key] as unknown) = value;
189
- }
190
-
191
- // --- the live counts and the generated summary, in the role="status" regions ---
192
- const styleOnCount = $derived(
193
- styleRows.filter((r) => rowOn(r.key)).length + advancedRows.filter((r) => rowOn(r.key)).length,
194
- );
195
-
196
- // The "fix" clause names the always-on objective set plus any on style convention; the "leaves
197
- // alone" clause names what stays untouched. Both are generated from the live config, so the line is
198
- // always true for any combination.
199
- const summaryFixes = $derived.by(() => {
200
- const parts: string[] = [];
201
- if (conv.fixes) parts.push('spelling', 'grammar', 'doubled words', 'spacing', 'capitals', 'end punctuation');
202
- if (rowOn('oxfordComma')) parts.push('commas');
203
- if (rowOn('timeFormat')) parts.push('time format');
204
- if (rowOn('numberStyle')) parts.push('number style');
205
- if (rowOn('measurements')) parts.push('units');
206
- if (rowOn('percent')) parts.push('percent');
207
- if (rowOn('emDash') || rowOn('enDashRanges')) parts.push('dashes');
208
- if (rowOn('ellipsis')) parts.push('ellipses');
209
- if (rowOn('smartQuotes')) parts.push('quotes');
210
- if (rowOn('brandCaps')) parts.push('brand names');
211
- return parts.length ? joinList(parts) : 'nothing yet';
212
- });
213
- const summaryLeaves = $derived.by(() => {
214
- const parts: string[] = [];
215
- if (!rowOn('oxfordComma')) parts.push('commas');
216
- if (!rowOn('emDash') && !rowOn('enDashRanges')) parts.push('dashes');
217
- if (!rowOn('numberStyle')) parts.push('number style');
218
- if (!rowOn('measurements')) parts.push('units');
219
- if (!rowOn('percent')) parts.push('percent');
220
- if (!rowOn('smartQuotes')) parts.push('quotes');
221
- if (!rowOn('brandCaps')) parts.push('brand names');
222
- return parts.length ? joinList(parts) : 'nothing';
223
- });
224
-
225
- function joinList(parts: string[]): string {
226
- if (parts.length === 1) return parts[0];
227
- if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
228
- return `${parts.slice(0, -1).join(', ')}, and ${parts[parts.length - 1]}`;
229
- }
230
-
231
- // --- section masters and the safe-default reset ---
232
- function styleAllOn() {
233
- for (const row of styleRows) if (!rowOn(row.key)) (conv[row.key] as unknown) = defaultVariant(row);
234
- for (const row of advancedRows) (conv[row.key] as unknown) = true;
235
- }
236
- function styleAllOff() {
237
- for (const row of styleRows) (conv[row.key] as unknown) = row.variants ? undefined : false;
238
- for (const row of advancedRows) (conv[row.key] as unknown) = false;
239
- }
240
- function fixesAllOff() {
241
- conv.fixes = false;
242
- }
243
- function fixesAllOn() {
244
- conv.fixes = true;
245
- }
246
- // Reset to the safe resting default: Fixes on, every style and advanced toggle off, every variant
247
- // collapsed. Never named a house style.
248
- function resetSafeDefault() {
249
- conv = { fixes: true, enDashRanges: false, smartQuotes: false, brandCaps: false };
250
- }
251
-
252
- // --- the radiogroup roving-tabindex handler, the CairnMediaLibrary triage recipe ---
253
- // The selected radio is the only tab stop; Arrow/Home/End move the selection and the focus with
254
- // wraparound. A declared radiogroup owes this keyboard model.
255
- let radioEls = $state<Record<string, HTMLButtonElement[]>>(
256
- Object.fromEntries(styleRows.filter((r) => r.variants).map((r) => [String(r.key), []])),
257
- );
258
- function onRadioKeydown(e: KeyboardEvent, row: StyleRow, i: number) {
259
- if (!row.variants) return;
260
- const n = row.variants.length;
261
- let next = i;
262
- if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (i + 1) % n;
263
- else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') next = (i - 1 + n) % n;
264
- else if (e.key === 'Home') next = 0;
265
- else if (e.key === 'End') next = n - 1;
266
- else return;
267
- e.preventDefault();
268
- pickVariant(row.key, row.variants[next].value);
269
- radioEls[String(row.key)]?.[next]?.focus();
270
- }
271
-
272
- // The conventions payload the save posts: the live working copy as JSON.
273
- const conventionsJson = $derived(JSON.stringify(conv));
274
-
275
- // The shared class for a check-and-tint on/off button (the binary-state idiom, no DaisyUI .toggle).
276
- function onoffClass(on: boolean): string {
277
- return `inline-flex h-[30px] items-center gap-1.5 rounded-lg border px-2.5 text-xs font-semibold ${
278
- on
279
- ? 'border-primary/30 bg-primary/10 text-primary'
280
- : 'border-[var(--cairn-card-border)] bg-base-100 text-[var(--color-muted)] hover:border-primary/35 hover:text-base-content'
281
- }`;
25
+ <script lang="ts">import { untrack } from "svelte";
26
+ import CsrfField from "./CsrfField.svelte";
27
+ import CheckIcon from "@lucide/svelte/icons/check";
28
+ import CircleIcon from "@lucide/svelte/icons/circle";
29
+ import SettingsIcon from "@lucide/svelte/icons/settings";
30
+ import LockIcon from "@lucide/svelte/icons/lock";
31
+ import CodeIcon from "@lucide/svelte/icons/code-xml";
32
+ import ListIcon from "@lucide/svelte/icons/list";
33
+ import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert";
34
+ import InfoIcon from "@lucide/svelte/icons/info";
35
+ import ArrowRightIcon from "@lucide/svelte/icons/arrow-right";
36
+ import SparklesIcon from "@lucide/svelte/icons/sparkles";
37
+ let { data } = $props();
38
+ let conv = $state(untrack(() => ({ ...data.conventions })));
39
+ const styleRows = [
40
+ {
41
+ key: "oxfordComma",
42
+ name: "Oxford comma",
43
+ variantLabel: "Use the Oxford comma",
44
+ variants: [
45
+ { value: "always", label: "Always" },
46
+ { value: "complex-only", label: "Only in complex lists" },
47
+ { value: "never", label: "Never" }
48
+ ],
49
+ egBefore: "wax, skins and poles",
50
+ egAfter: "wax, skins, and poles"
51
+ },
52
+ {
53
+ key: "emDash",
54
+ name: "Em-dash style",
55
+ variantLabel: "Write em dashes as",
56
+ variants: [
57
+ { value: "spaced", label: "Spaced" },
58
+ { value: "closed", label: "Closed" }
59
+ ],
60
+ egBefore: "grooming--early",
61
+ egAfter: "grooming—early"
62
+ },
63
+ { key: "enDashRanges", name: "En-dash in number ranges", egBefore: "9-11 am", egAfter: "9–11 am" },
64
+ {
65
+ key: "ellipsis",
66
+ name: "Ellipsis",
67
+ variantLabel: "Write ellipses as",
68
+ variants: [
69
+ { value: "single-char", label: "One character" },
70
+ { value: "three-dots", label: "Three dots" }
71
+ ],
72
+ egBefore: "later...",
73
+ egAfter: "later…"
74
+ },
75
+ {
76
+ key: "timeFormat",
77
+ name: "Time format",
78
+ variantLabel: "Write times as",
79
+ variants: [
80
+ { value: "5 PM", label: "5 PM" },
81
+ { value: "5pm", label: "5pm" },
82
+ { value: "5 p.m.", label: "5 p.m." }
83
+ ],
84
+ egBefore: "doors at 5pm",
85
+ egAfter: "doors at 5 PM"
86
+ },
87
+ {
88
+ key: "numberStyle",
89
+ name: "Number style",
90
+ variantLabel: "Write numbers as",
91
+ variants: [
92
+ { value: "under-ten", label: "Spell out under ten" },
93
+ { value: "under-hundred", label: "Spell out under 100" },
94
+ { value: "always-numerals", label: "Always numerals" }
95
+ ],
96
+ egBefore: "7 inches of snow",
97
+ egAfter: "seven inches of snow"
98
+ },
99
+ {
100
+ key: "measurements",
101
+ name: "Measurements and units",
102
+ variantLabel: "Write units as",
103
+ variants: [
104
+ { value: "abbreviate", label: "Abbreviate" },
105
+ { value: "spell-out", label: "Spell out" }
106
+ ],
107
+ egBefore: "15 centimeters",
108
+ egAfter: "15 cm"
109
+ },
110
+ {
111
+ key: "percent",
112
+ name: "Percent",
113
+ variantLabel: "Write percent as",
114
+ variants: [
115
+ { value: "sign", label: "Sign (%)" },
116
+ { value: "word", label: "Word (percent)" }
117
+ ],
118
+ egBefore: "30 percent",
119
+ egAfter: "30%"
282
120
  }
283
- function segClass(on: boolean): string {
284
- return `inline-flex items-center gap-1.5 px-3 py-1.5 text-xs ${on ? 'bg-primary/10 text-primary font-medium' : 'text-[var(--color-muted)]'}`;
121
+ ];
122
+ const advancedRows = [
123
+ { key: "smartQuotes", name: "Curly quotes", egBefore: '"groomed"', egAfter: "“groomed”" },
124
+ { key: "brandCaps", name: "Brand and proper-noun capitals", egBefore: "github", egAfter: "GitHub" }
125
+ ];
126
+ function rowOn(key) {
127
+ const v = conv[key];
128
+ return typeof v === "boolean" ? v : v !== void 0;
129
+ }
130
+ function defaultVariant(row) {
131
+ return row.variants ? row.variants[0].value : true;
132
+ }
133
+ function toggleStyle(row) {
134
+ if (rowOn(row.key)) {
135
+ conv[row.key] = row.variants ? void 0 : false;
136
+ } else {
137
+ conv[row.key] = defaultVariant(row);
285
138
  }
139
+ }
140
+ function toggleBool(key) {
141
+ conv[key] = !rowOn(key);
142
+ }
143
+ function pickVariant(key, value) {
144
+ conv[key] = value;
145
+ }
146
+ const styleOnCount = $derived(
147
+ styleRows.filter((r) => rowOn(r.key)).length + advancedRows.filter((r) => rowOn(r.key)).length
148
+ );
149
+ const summaryFixes = $derived.by(() => {
150
+ const parts = [];
151
+ if (conv.fixes) parts.push("spelling", "grammar", "doubled words", "spacing", "capitals", "end punctuation");
152
+ if (rowOn("oxfordComma")) parts.push("commas");
153
+ if (rowOn("timeFormat")) parts.push("time format");
154
+ if (rowOn("numberStyle")) parts.push("number style");
155
+ if (rowOn("measurements")) parts.push("units");
156
+ if (rowOn("percent")) parts.push("percent");
157
+ if (rowOn("emDash") || rowOn("enDashRanges")) parts.push("dashes");
158
+ if (rowOn("ellipsis")) parts.push("ellipses");
159
+ if (rowOn("smartQuotes")) parts.push("quotes");
160
+ if (rowOn("brandCaps")) parts.push("brand names");
161
+ return parts.length ? joinList(parts) : "nothing yet";
162
+ });
163
+ const summaryLeaves = $derived.by(() => {
164
+ const parts = [];
165
+ if (!rowOn("oxfordComma")) parts.push("commas");
166
+ if (!rowOn("emDash") && !rowOn("enDashRanges")) parts.push("dashes");
167
+ if (!rowOn("numberStyle")) parts.push("number style");
168
+ if (!rowOn("measurements")) parts.push("units");
169
+ if (!rowOn("percent")) parts.push("percent");
170
+ if (!rowOn("smartQuotes")) parts.push("quotes");
171
+ if (!rowOn("brandCaps")) parts.push("brand names");
172
+ return parts.length ? joinList(parts) : "nothing";
173
+ });
174
+ function joinList(parts) {
175
+ if (parts.length === 1) return parts[0];
176
+ if (parts.length === 2) return `${parts[0]} and ${parts[1]}`;
177
+ return `${parts.slice(0, -1).join(", ")}, and ${parts[parts.length - 1]}`;
178
+ }
179
+ function styleAllOn() {
180
+ for (const row of styleRows) if (!rowOn(row.key)) conv[row.key] = defaultVariant(row);
181
+ for (const row of advancedRows) conv[row.key] = true;
182
+ }
183
+ function styleAllOff() {
184
+ for (const row of styleRows) conv[row.key] = row.variants ? void 0 : false;
185
+ for (const row of advancedRows) conv[row.key] = false;
186
+ }
187
+ function fixesAllOff() {
188
+ conv.fixes = false;
189
+ }
190
+ function fixesAllOn() {
191
+ conv.fixes = true;
192
+ }
193
+ function resetSafeDefault() {
194
+ conv = { fixes: true, enDashRanges: false, smartQuotes: false, brandCaps: false };
195
+ }
196
+ let radioEls = $state(
197
+ Object.fromEntries(styleRows.filter((r) => r.variants).map((r) => [String(r.key), []]))
198
+ );
199
+ function onRadioKeydown(e, row, i) {
200
+ if (!row.variants) return;
201
+ const n = row.variants.length;
202
+ let next = i;
203
+ if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (i + 1) % n;
204
+ else if (e.key === "ArrowLeft" || e.key === "ArrowUp") next = (i - 1 + n) % n;
205
+ else if (e.key === "Home") next = 0;
206
+ else if (e.key === "End") next = n - 1;
207
+ else return;
208
+ e.preventDefault();
209
+ pickVariant(row.key, row.variants[next].value);
210
+ radioEls[String(row.key)]?.[next]?.focus();
211
+ }
212
+ const conventionsJson = $derived(JSON.stringify(conv));
213
+ function onoffClass(on) {
214
+ return `inline-flex h-[30px] items-center gap-1.5 rounded-lg border px-2.5 text-xs font-semibold ${on ? "border-primary/30 bg-primary/10 text-primary" : "border-[var(--cairn-card-border)] bg-base-100 text-[var(--color-muted)] hover:border-primary/35 hover:text-base-content"}`;
215
+ }
216
+ function segClass(on) {
217
+ return `inline-flex items-center gap-1.5 px-3 py-1.5 text-xs ${on ? "bg-primary/10 text-primary font-medium" : "text-[var(--color-muted)]"}`;
218
+ }
286
219
  </script>
287
220
 
288
221
  <div class="mx-auto max-w-3xl px-2 py-2">