@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.
- package/CHANGELOG.md +13 -0
- package/dist/components/AdminLayout.svelte +130 -229
- package/dist/components/CairnAdmin.svelte +10 -42
- package/dist/components/CairnLogo.svelte +1 -6
- package/dist/components/CairnMediaLibrary.svelte +821 -1210
- package/dist/components/CairnTidySettings.svelte +192 -259
- 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 +665 -1166
- 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 +689 -957
- 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 +140 -248
- package/dist/components/WebLinkDialog.svelte +19 -40
- package/dist/components/cairn-admin.css +4 -0
- package/dist/components/spellcheck.d.ts +3 -1
- package/dist/components/spellcheck.js +14 -2
- package/dist/delivery/CairnHead.svelte +8 -11
- package/package.json +2 -2
- package/src/lib/components/spellcheck.ts +16 -2
|
@@ -17,256 +17,148 @@ the model made and never a count of the author's own usage. A normalization name
|
|
|
17
17
|
setting that authorized it; counting the author's own habit is the harmonize-to-author judgment cairn
|
|
18
18
|
must never make, so no such count exists.
|
|
19
19
|
-->
|
|
20
|
-
<script lang="ts">
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
const keptCount = $derived(hunks.filter((h) => dispositions[h.index] === 'kept').length);
|
|
155
|
-
const reviewCount = $derived(hunks.filter((h) => dispositions[h.index] === 'undecided').length);
|
|
156
|
-
const skipCount = $derived(hunks.filter((h) => dispositions[h.index] === 'rejected').length);
|
|
157
|
-
|
|
158
|
-
let dialog = $state<HTMLDialogElement | null>(null);
|
|
159
|
-
|
|
160
|
-
$effect(() => {
|
|
161
|
-
// Open the dialog once on mount; showModal supplies the focus trap and Escape.
|
|
162
|
-
dialog?.showModal();
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
function setDisposition(index: number, next: Disposition) {
|
|
166
|
-
overrides = { ...overrides, [index]: next };
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// Narrate one hunk in the polite region. The verb says what just happened to it ("Kept", "Skipped",
|
|
170
|
-
// or "Focused" as the cursor lands on it). The sentence carries the kind and the before/after text,
|
|
171
|
-
// and for a normalization appends the config-named rationale (never a usage count). The trailing
|
|
172
|
-
// nonce keeps a repeated identical action audible.
|
|
173
|
-
function narrate(h: Hunk, verb: string) {
|
|
174
|
-
const where = `Hunk ${hunks.indexOf(h) + 1} of ${hunks.length}`;
|
|
175
|
-
const what = h.delRun.mid && h.addRun.mid ? `${h.delRun.mid.trim()} becomes ${h.addRun.mid.trim()}` : h.label;
|
|
176
|
-
const why = h.because ? `, your ${h.because.label} setting is ${h.because.variant}` : '';
|
|
177
|
-
actionMessage = `${where}. ${h.label}. ${what}${why}. ${verb}.${nonce()}`;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function acceptHunk(h: Hunk) {
|
|
181
|
-
setDisposition(h.index, 'kept');
|
|
182
|
-
narrate(h, 'Kept');
|
|
183
|
-
}
|
|
184
|
-
function rejectHunk(h: Hunk) {
|
|
185
|
-
setDisposition(h.index, 'rejected');
|
|
186
|
-
narrate(h, 'Skipped');
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Accept fixes (the bulk action): mark EVERY OBJECTIVE hunk kept and nothing else. A judgment hunk is
|
|
190
|
-
// never touched here, so it stays undecided and is never swept. The tally region announces the result.
|
|
191
|
-
function acceptFixes() {
|
|
192
|
-
const next = { ...overrides };
|
|
193
|
-
for (const h of hunks) if (h.objective) next[h.index] = 'kept';
|
|
194
|
-
overrides = next;
|
|
195
|
-
const n = hunks.filter((h) => h.objective).length;
|
|
196
|
-
const stillReview = hunks.filter((h) => effectiveDisposition(h, next) === 'undecided').length;
|
|
197
|
-
tallyMessage = `${n} fixes kept. ${stillReview} still to review.${nonce()}`;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Reject all: mark every hunk rejected; no text is written. The tally region announces it.
|
|
201
|
-
function rejectAll() {
|
|
202
|
-
overrides = Object.fromEntries(hunks.map((h) => [h.index, 'rejected'] as const));
|
|
203
|
-
tallyMessage = `All ${hunks.length} changes skipping.${nonce()}`;
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
// Apply: write the kept hunks in ONE batched transaction through the apply seam, then close. The
|
|
207
|
-
// seam's acceptMany dispatches a single view.dispatch({ changes }), so the whole tidy is one undoable
|
|
208
|
-
// step. ONLY the kept indexes are passed, so an undecided judgment hunk is never written.
|
|
209
|
-
function apply() {
|
|
210
|
-
const keptIndexes = hunks.filter((h) => dispositions[h.index] === 'kept').map((h) => h.index);
|
|
211
|
-
api.acceptMany(keptIndexes);
|
|
212
|
-
api.exit();
|
|
213
|
-
dialog?.close();
|
|
214
|
-
onclose(true);
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
// Cancel: write nothing, clear the decorations, leave the document byte-identical.
|
|
218
|
-
function cancel() {
|
|
219
|
-
api.exit();
|
|
220
|
-
dialog?.close();
|
|
221
|
-
onclose(false);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function showInText(h: Hunk) {
|
|
225
|
-
const c = changes.find((ch) => ch.index === h.index);
|
|
226
|
-
if (c) onshow(c.from, c.to);
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// Move the step-through cursor and announce the hunk it lands on. A screen-reader user pressing j/k
|
|
230
|
-
// hears the newly-focused hunk (kind plus before/after text, plus the because-line for a judgment
|
|
231
|
-
// hunk), the same spec invariant the per-hunk action narration holds. Without this a move was silent.
|
|
232
|
-
function moveFocus(next: number) {
|
|
233
|
-
focusedPos = next;
|
|
234
|
-
const h = hunks[focusedPos];
|
|
235
|
-
if (h) narrate(h, 'Focused');
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
// Keyboard step-through on the hunk list (graft 3): j/k or n/p move; a/r accept/reject the focused
|
|
239
|
-
// hunk; A accepts all objective; Escape cancels (the native dialog supplies Escape, handled below).
|
|
240
|
-
function onListKeydown(e: KeyboardEvent) {
|
|
241
|
-
const h = hunks[focusedPos];
|
|
242
|
-
if (e.key === 'j' || e.key === 'n') {
|
|
243
|
-
moveFocus(Math.min(focusedPos + 1, hunks.length - 1));
|
|
244
|
-
e.preventDefault();
|
|
245
|
-
} else if (e.key === 'k' || e.key === 'p') {
|
|
246
|
-
moveFocus(Math.max(focusedPos - 1, 0));
|
|
247
|
-
e.preventDefault();
|
|
248
|
-
} else if (e.key === 'a' && !e.shiftKey) {
|
|
249
|
-
if (h) acceptHunk(h);
|
|
250
|
-
e.preventDefault();
|
|
251
|
-
} else if (e.key === 'r' && !e.shiftKey) {
|
|
252
|
-
if (h) rejectHunk(h);
|
|
253
|
-
e.preventDefault();
|
|
254
|
-
} else if (e.key === 'A' || (e.key === 'a' && e.shiftKey)) {
|
|
255
|
-
acceptFixes();
|
|
256
|
-
e.preventDefault();
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
// The native dialog raises a cancel event on Escape; map it to the surface's cancel so the buffer is
|
|
261
|
-
// left untouched and the host clears tidy mode.
|
|
262
|
-
function onDialogCancel(e: Event) {
|
|
20
|
+
<script lang="ts">import SparklesIcon from "@lucide/svelte/icons/sparkles";
|
|
21
|
+
import CheckIcon from "@lucide/svelte/icons/check";
|
|
22
|
+
import XIcon from "@lucide/svelte/icons/x";
|
|
23
|
+
import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert";
|
|
24
|
+
import LightbulbIcon from "@lucide/svelte/icons/lightbulb";
|
|
25
|
+
import EyeIcon from "@lucide/svelte/icons/eye";
|
|
26
|
+
import { lineLabel } from "./tidy-diff.js";
|
|
27
|
+
import {
|
|
28
|
+
categorize,
|
|
29
|
+
isObjective,
|
|
30
|
+
buildBecause,
|
|
31
|
+
categoryLabel
|
|
32
|
+
} from "./tidy-categorize.js";
|
|
33
|
+
let { changes, original, conventions, model, title, api, onclose, onshow } = $props();
|
|
34
|
+
const hunks = $derived(changes.map((c) => {
|
|
35
|
+
const category = categorize(c, original, conventions);
|
|
36
|
+
const objective = isObjective(category);
|
|
37
|
+
const removed = original.slice(c.from, c.to);
|
|
38
|
+
const added = c.replacement;
|
|
39
|
+
const line = lineLabel(original, c.from);
|
|
40
|
+
const lines = original.split("\n");
|
|
41
|
+
const contextBefore = line >= 2 ? lines[line - 2] ?? "" : "";
|
|
42
|
+
const contextAfter = line < lines.length ? lines[line] ?? "" : "";
|
|
43
|
+
const lineStart = original.lastIndexOf("\n", c.from - 1) + 1;
|
|
44
|
+
const nextNewline = original.indexOf("\n", c.from);
|
|
45
|
+
const lineEnd = nextNewline === -1 ? original.length : nextNewline;
|
|
46
|
+
const fullLine = original.slice(lineStart, lineEnd);
|
|
47
|
+
const pre = original.slice(lineStart, c.from);
|
|
48
|
+
const post = original.slice(c.to, lineEnd);
|
|
49
|
+
const because = category.kind === "normalization" ? buildBecause(category.convention, conventions) : null;
|
|
50
|
+
return {
|
|
51
|
+
index: c.index,
|
|
52
|
+
category,
|
|
53
|
+
objective,
|
|
54
|
+
line,
|
|
55
|
+
contextBefore,
|
|
56
|
+
contextAfter,
|
|
57
|
+
delText: fullLine,
|
|
58
|
+
addText: pre + added + post,
|
|
59
|
+
delRun: { pre, mid: removed, post },
|
|
60
|
+
addRun: { pre, mid: added, post },
|
|
61
|
+
because,
|
|
62
|
+
label: categoryLabel(category)
|
|
63
|
+
};
|
|
64
|
+
}));
|
|
65
|
+
let overrides = $state({});
|
|
66
|
+
function effectiveDisposition(h, map) {
|
|
67
|
+
return map[h.index] ?? (h.objective ? "kept" : "undecided");
|
|
68
|
+
}
|
|
69
|
+
const dispositions = $derived(
|
|
70
|
+
Object.fromEntries(hunks.map((h) => [h.index, effectiveDisposition(h, overrides)]))
|
|
71
|
+
);
|
|
72
|
+
let focusedPos = $state(0);
|
|
73
|
+
let tallyMessage = $state("");
|
|
74
|
+
let actionMessage = $state("");
|
|
75
|
+
let announceNonce = 0;
|
|
76
|
+
function nonce() {
|
|
77
|
+
return announceNonce++ % 2 === 0 ? "" : "";
|
|
78
|
+
}
|
|
79
|
+
const keptCount = $derived(hunks.filter((h) => dispositions[h.index] === "kept").length);
|
|
80
|
+
const reviewCount = $derived(hunks.filter((h) => dispositions[h.index] === "undecided").length);
|
|
81
|
+
const skipCount = $derived(hunks.filter((h) => dispositions[h.index] === "rejected").length);
|
|
82
|
+
let dialog = $state(null);
|
|
83
|
+
$effect(() => {
|
|
84
|
+
dialog?.showModal();
|
|
85
|
+
});
|
|
86
|
+
function setDisposition(index, next) {
|
|
87
|
+
overrides = { ...overrides, [index]: next };
|
|
88
|
+
}
|
|
89
|
+
function narrate(h, verb) {
|
|
90
|
+
const where = `Hunk ${hunks.indexOf(h) + 1} of ${hunks.length}`;
|
|
91
|
+
const what = h.delRun.mid && h.addRun.mid ? `${h.delRun.mid.trim()} becomes ${h.addRun.mid.trim()}` : h.label;
|
|
92
|
+
const why = h.because ? `, your ${h.because.label} setting is ${h.because.variant}` : "";
|
|
93
|
+
actionMessage = `${where}. ${h.label}. ${what}${why}. ${verb}.${nonce()}`;
|
|
94
|
+
}
|
|
95
|
+
function acceptHunk(h) {
|
|
96
|
+
setDisposition(h.index, "kept");
|
|
97
|
+
narrate(h, "Kept");
|
|
98
|
+
}
|
|
99
|
+
function rejectHunk(h) {
|
|
100
|
+
setDisposition(h.index, "rejected");
|
|
101
|
+
narrate(h, "Skipped");
|
|
102
|
+
}
|
|
103
|
+
function acceptFixes() {
|
|
104
|
+
const next = { ...overrides };
|
|
105
|
+
for (const h of hunks) if (h.objective) next[h.index] = "kept";
|
|
106
|
+
overrides = next;
|
|
107
|
+
const n = hunks.filter((h) => h.objective).length;
|
|
108
|
+
const stillReview = hunks.filter((h) => effectiveDisposition(h, next) === "undecided").length;
|
|
109
|
+
tallyMessage = `${n} fixes kept. ${stillReview} still to review.${nonce()}`;
|
|
110
|
+
}
|
|
111
|
+
function rejectAll() {
|
|
112
|
+
overrides = Object.fromEntries(hunks.map((h) => [h.index, "rejected"]));
|
|
113
|
+
tallyMessage = `All ${hunks.length} changes skipping.${nonce()}`;
|
|
114
|
+
}
|
|
115
|
+
function apply() {
|
|
116
|
+
const keptIndexes = hunks.filter((h) => dispositions[h.index] === "kept").map((h) => h.index);
|
|
117
|
+
api.acceptMany(keptIndexes);
|
|
118
|
+
api.exit();
|
|
119
|
+
dialog?.close();
|
|
120
|
+
onclose(true);
|
|
121
|
+
}
|
|
122
|
+
function cancel() {
|
|
123
|
+
api.exit();
|
|
124
|
+
dialog?.close();
|
|
125
|
+
onclose(false);
|
|
126
|
+
}
|
|
127
|
+
function showInText(h) {
|
|
128
|
+
const c = changes.find((ch) => ch.index === h.index);
|
|
129
|
+
if (c) onshow(c.from, c.to);
|
|
130
|
+
}
|
|
131
|
+
function moveFocus(next) {
|
|
132
|
+
focusedPos = next;
|
|
133
|
+
const h = hunks[focusedPos];
|
|
134
|
+
if (h) narrate(h, "Focused");
|
|
135
|
+
}
|
|
136
|
+
function onListKeydown(e) {
|
|
137
|
+
const h = hunks[focusedPos];
|
|
138
|
+
if (e.key === "j" || e.key === "n") {
|
|
139
|
+
moveFocus(Math.min(focusedPos + 1, hunks.length - 1));
|
|
140
|
+
e.preventDefault();
|
|
141
|
+
} else if (e.key === "k" || e.key === "p") {
|
|
142
|
+
moveFocus(Math.max(focusedPos - 1, 0));
|
|
143
|
+
e.preventDefault();
|
|
144
|
+
} else if (e.key === "a" && !e.shiftKey) {
|
|
145
|
+
if (h) acceptHunk(h);
|
|
146
|
+
e.preventDefault();
|
|
147
|
+
} else if (e.key === "r" && !e.shiftKey) {
|
|
148
|
+
if (h) rejectHunk(h);
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
} else if (e.key === "A" || e.key === "a" && e.shiftKey) {
|
|
151
|
+
acceptFixes();
|
|
263
152
|
e.preventDefault();
|
|
264
|
-
cancel();
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
function actsLabel(h: Hunk): string {
|
|
268
|
-
return h.objective ? 'Accept or reject this fix' : 'Accept or reject this change';
|
|
269
153
|
}
|
|
154
|
+
}
|
|
155
|
+
function onDialogCancel(e) {
|
|
156
|
+
e.preventDefault();
|
|
157
|
+
cancel();
|
|
158
|
+
}
|
|
159
|
+
function actsLabel(h) {
|
|
160
|
+
return h.objective ? "Accept or reject this fix" : "Accept or reject this change";
|
|
161
|
+
}
|
|
270
162
|
</script>
|
|
271
163
|
|
|
272
164
|
<dialog
|
|
@@ -6,46 +6,25 @@ text; when the editor holds a selection it arrives as the default text, and the
|
|
|
6
6
|
that selection either way. Built on a native <dialog>, following the LinkPicker a11y conventions,
|
|
7
7
|
and opened by the host's Ctrl/Cmd+K shortcut through the exported open().
|
|
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
|
-
/** Open the dialog with fresh fields; the edit page's Ctrl/Cmd+K shortcut calls it too. */
|
|
30
|
-
export function open() {
|
|
31
|
-
href = '';
|
|
32
|
-
text = selection?.() ?? '';
|
|
33
|
-
dialog?.showModal();
|
|
34
|
-
// showModal() lands focus on the first focusable element (the header Close button), so move
|
|
35
|
-
// it to the address input the dialog exists for (WCAG 2.4.3). A microtask defers past the
|
|
36
|
-
// dialog's own focus handling, the RenameDialog recipe.
|
|
37
|
-
queueMicrotask(() => hrefInput?.focus());
|
|
38
|
-
}
|
|
39
|
-
function close() {
|
|
40
|
-
dialog?.close();
|
|
41
|
-
}
|
|
42
|
-
function submit(e: SubmitEvent) {
|
|
43
|
-
e.preventDefault();
|
|
44
|
-
// With no text and no selection the address itself becomes the display text, so the link
|
|
45
|
-
// never renders as an invisible pair of brackets.
|
|
46
|
-
insert(href, text.trim() || href);
|
|
47
|
-
close();
|
|
48
|
-
}
|
|
9
|
+
<script lang="ts">let { insert, selection, disabled = false, trigger = true } = $props();
|
|
10
|
+
let dialog = $state(null);
|
|
11
|
+
let hrefInput = $state(null);
|
|
12
|
+
let href = $state("");
|
|
13
|
+
let text = $state("");
|
|
14
|
+
export function open() {
|
|
15
|
+
href = "";
|
|
16
|
+
text = selection?.() ?? "";
|
|
17
|
+
dialog?.showModal();
|
|
18
|
+
queueMicrotask(() => hrefInput?.focus());
|
|
19
|
+
}
|
|
20
|
+
function close() {
|
|
21
|
+
dialog?.close();
|
|
22
|
+
}
|
|
23
|
+
function submit(e) {
|
|
24
|
+
e.preventDefault();
|
|
25
|
+
insert(href, text.trim() || href);
|
|
26
|
+
close();
|
|
27
|
+
}
|
|
49
28
|
</script>
|
|
50
29
|
|
|
51
30
|
{#if trigger}
|
|
@@ -7135,6 +7135,10 @@
|
|
|
7135
7135
|
user-select: none;
|
|
7136
7136
|
}
|
|
7137
7137
|
|
|
7138
|
+
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .\[builtin\:vite-dynamic-import-vars\] {
|
|
7139
|
+
builtin: vite-dynamic-import-vars;
|
|
7140
|
+
}
|
|
7141
|
+
|
|
7138
7142
|
@media (hover: hover) {
|
|
7139
7143
|
:where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .group-hover\/sec\:opacity-90:is(:where(.group\/sec):hover *) {
|
|
7140
7144
|
opacity: .9;
|
|
@@ -87,7 +87,9 @@ export declare function createSpellWorker(): SpellWorker;
|
|
|
87
87
|
/** The real wasm asset URL, resolved module-relative the same way the worker is. */
|
|
88
88
|
export declare function resolveWasmUrl(): string;
|
|
89
89
|
/** The real dictionary asset URL for a dictionary filename, resolved module-relative. The caller
|
|
90
|
-
* passes the dialect-resolved filename (default `dictionary-en-us.txt`).
|
|
90
|
+
* passes the dialect-resolved filename (default `dictionary-en-us.txt`). `dictionaryFileForDialect`
|
|
91
|
+
* already collapses an unknown dialect to the default, so an unmapped name falls back the same way
|
|
92
|
+
* rather than pointing at an asset that does not ship. */
|
|
91
93
|
export declare function resolveDictionaryUrl(dictionaryFile: string): string;
|
|
92
94
|
/** Options for {@link cairnSpellcheck}, so the unit and component layers can inject a fake Worker
|
|
93
95
|
* factory in place of the real `new Worker(...)`. */
|
|
@@ -290,10 +290,22 @@ export function createSpellWorker() {
|
|
|
290
290
|
export function resolveWasmUrl() {
|
|
291
291
|
return new URL('./spellcheck-assets/spellchecker-wasm.wasm', import.meta.url).href;
|
|
292
292
|
}
|
|
293
|
+
/** Each shipped dictionary, mapped to a resolver that builds its asset URL with a LITERAL
|
|
294
|
+
* `new URL(..., import.meta.url)`. The literal path is load-bearing. A templated `new URL` makes Vite
|
|
295
|
+
* and rolldown treat the directory as a glob and parse every sibling module to build it, including the
|
|
296
|
+
* `.svelte` components that still carry `lang="ts"` in `dist`, and the glob parser chokes on the TS
|
|
297
|
+
* syntax and breaks the consumer build. This set mirrors the dialect map in `nav/site-config.ts`; add
|
|
298
|
+
* one line per new shipped dialect dictionary. */
|
|
299
|
+
const DICTIONARY_URLS = {
|
|
300
|
+
'dictionary-en-us.txt': () => new URL('./spellcheck-assets/dictionary-en-us.txt', import.meta.url).href,
|
|
301
|
+
};
|
|
293
302
|
/** The real dictionary asset URL for a dictionary filename, resolved module-relative. The caller
|
|
294
|
-
* passes the dialect-resolved filename (default `dictionary-en-us.txt`).
|
|
303
|
+
* passes the dialect-resolved filename (default `dictionary-en-us.txt`). `dictionaryFileForDialect`
|
|
304
|
+
* already collapses an unknown dialect to the default, so an unmapped name falls back the same way
|
|
305
|
+
* rather than pointing at an asset that does not ship. */
|
|
295
306
|
export function resolveDictionaryUrl(dictionaryFile) {
|
|
296
|
-
|
|
307
|
+
const resolve = DICTIONARY_URLS[dictionaryFile] ?? DICTIONARY_URLS['dictionary-en-us.txt'];
|
|
308
|
+
return resolve();
|
|
297
309
|
}
|
|
298
310
|
/** How far past the visible viewport to lint, so a small scroll does not re-lint from scratch. */
|
|
299
311
|
const VIEWPORT_MARGIN = 1000;
|
|
@@ -5,17 +5,14 @@ tags, and one escaped JSON-LD script. The title renders from seo.title by defaul
|
|
|
5
5
|
lets the site own the <title>, and a string overrides it. It carries no CSS, so it pulls in no
|
|
6
6
|
admin styles.
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
title,
|
|
17
|
-
}: { seo: SeoMeta; title?: string | false } = $props();
|
|
18
|
-
const titleText = $derived(title === undefined ? seo.title : title);
|
|
8
|
+
<script lang="ts">import { jsonLdScript } from "./json-ld.js";
|
|
9
|
+
let {
|
|
10
|
+
/** The plain-data head to render. */
|
|
11
|
+
seo,
|
|
12
|
+
/** Title override: a string replaces seo.title, false lets the site own <title>. */
|
|
13
|
+
title
|
|
14
|
+
} = $props();
|
|
15
|
+
const titleText = $derived(title === void 0 ? seo.title : title);
|
|
19
16
|
</script>
|
|
20
17
|
|
|
21
18
|
<svelte:head>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@glw907/cairn-cms",
|
|
3
|
-
"version": "0.60.
|
|
3
|
+
"version": "0.60.1",
|
|
4
4
|
"description": "Embedded, magic-link, GitHub-committing CMS for SvelteKit/Cloudflare sites.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": [
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"markdown"
|
|
27
27
|
],
|
|
28
28
|
"scripts": {
|
|
29
|
-
"package": "svelte-package && node scripts/build-admin-css.mjs && chmod +x dist/vite/bin.js dist/doctor/bin.js",
|
|
29
|
+
"package": "svelte-package && node scripts/build-admin-css.mjs && node scripts/transpile-dist-svelte.mjs && chmod +x dist/vite/bin.js dist/doctor/bin.js",
|
|
30
30
|
"check:package": "npm run package && publint --strict && attw --pack . --ignore-rules no-resolution cjs-resolves-to-esm internal-resolution-error",
|
|
31
31
|
"check:reference": "npm run package && node scripts/reference-coverage.mjs",
|
|
32
32
|
"check:reference:signatures": "npm run package && node scripts/check-reference-signatures.mjs",
|
|
@@ -368,10 +368,24 @@ export function resolveWasmUrl(): string {
|
|
|
368
368
|
return new URL('./spellcheck-assets/spellchecker-wasm.wasm', import.meta.url).href;
|
|
369
369
|
}
|
|
370
370
|
|
|
371
|
+
/** Each shipped dictionary, mapped to a resolver that builds its asset URL with a LITERAL
|
|
372
|
+
* `new URL(..., import.meta.url)`. The literal path is load-bearing. A templated `new URL` makes Vite
|
|
373
|
+
* and rolldown treat the directory as a glob and parse every sibling module to build it, including the
|
|
374
|
+
* `.svelte` components that still carry `lang="ts"` in `dist`, and the glob parser chokes on the TS
|
|
375
|
+
* syntax and breaks the consumer build. This set mirrors the dialect map in `nav/site-config.ts`; add
|
|
376
|
+
* one line per new shipped dialect dictionary. */
|
|
377
|
+
const DICTIONARY_URLS: Record<string, () => string> = {
|
|
378
|
+
'dictionary-en-us.txt': () =>
|
|
379
|
+
new URL('./spellcheck-assets/dictionary-en-us.txt', import.meta.url).href,
|
|
380
|
+
};
|
|
381
|
+
|
|
371
382
|
/** The real dictionary asset URL for a dictionary filename, resolved module-relative. The caller
|
|
372
|
-
* passes the dialect-resolved filename (default `dictionary-en-us.txt`).
|
|
383
|
+
* passes the dialect-resolved filename (default `dictionary-en-us.txt`). `dictionaryFileForDialect`
|
|
384
|
+
* already collapses an unknown dialect to the default, so an unmapped name falls back the same way
|
|
385
|
+
* rather than pointing at an asset that does not ship. */
|
|
373
386
|
export function resolveDictionaryUrl(dictionaryFile: string): string {
|
|
374
|
-
|
|
387
|
+
const resolve = DICTIONARY_URLS[dictionaryFile] ?? DICTIONARY_URLS['dictionary-en-us.txt'];
|
|
388
|
+
return resolve();
|
|
375
389
|
}
|
|
376
390
|
|
|
377
391
|
/** How far past the visible viewport to lint, so a small scroll does not re-lint from scratch. */
|