@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
|
@@ -46,6 +46,20 @@ interface Props {
|
|
|
46
46
|
registerImagePlaceholders?: (api: import('./editor-placeholder.js').ImagePlaceholderApi) => void;
|
|
47
47
|
/** Receives a `() => string` returning the selected text; the web link dialog reads it. */
|
|
48
48
|
registerGetSelection?: (get: () => string) => void;
|
|
49
|
+
/** Receives a `() => { from, to } | null` returning the selection's document offsets, or null when
|
|
50
|
+
* the selection is empty (a bare caret). The tidy host reads it so a selection tidy maps onto the
|
|
51
|
+
* exact selected span, never an identical-looking passage earlier in the document. */
|
|
52
|
+
registerGetSelectionRange?: (get: () => {
|
|
53
|
+
from: number;
|
|
54
|
+
to: number;
|
|
55
|
+
} | null) => void;
|
|
56
|
+
/** Receives the tidy apply api (spec 2.5): the review surface drives the in-buffer decorations and
|
|
57
|
+
* the accept/reject state machine through it. The author's original stays in the buffer until an
|
|
58
|
+
* accept writes; a reject or reject-all leaves it byte-identical. */
|
|
59
|
+
registerTidy?: (api: import('./editor-tidy.js').TidyApi) => void;
|
|
60
|
+
/** Receives a `() => void` that undoes the last editor transaction; the "Undo tidy" chip calls it
|
|
61
|
+
* to take the whole applied tidy back in one move (the apply lands as one history entry). */
|
|
62
|
+
registerUndo?: (undo: () => void) => void;
|
|
49
63
|
/** Receives a `(kind) => void` that transforms the current selection; the host's toolbar calls it. */
|
|
50
64
|
registerFormat?: (format: (kind: FormatKind) => void) => void;
|
|
51
65
|
/** Reports the directive container at the caret (or null when outside any container) whenever
|
|
@@ -72,6 +86,36 @@ interface Props {
|
|
|
72
86
|
/** The surface posture. Prose is the writing instrument (72ch measure, larger type, looser
|
|
73
87
|
* leading); markup is the working surface (fills the card, denser). Prose by default. */
|
|
74
88
|
surface?: 'prose' | 'markup';
|
|
89
|
+
/** Spellcheck and the objective-error layer: the markdown-aware lint underlines. On by default;
|
|
90
|
+
* when off the lint compartment reconfigures to empty (the underlines vanish, the Worker stays
|
|
91
|
+
* idle). The footer toggle drives this. */
|
|
92
|
+
spellcheck?: boolean;
|
|
93
|
+
/** The dialect-resolved dictionary filename, e.g. "dictionary-en-us.txt", from EditData. The
|
|
94
|
+
* source resolves it to a real asset URL and hands it to the spellcheck Worker's init. Defaults to
|
|
95
|
+
* US English. */
|
|
96
|
+
spellcheckDictionary?: string;
|
|
97
|
+
/** The committed personal-dictionary words (spec 1.6), from EditData.siteDictionary. The lint
|
|
98
|
+
* source seeds the spellcheck Worker's personal layer with these at init, so a word another editor
|
|
99
|
+
* committed answers correct from the first lint. Empty by default (dialect-only). */
|
|
100
|
+
siteDictionary?: ReadonlyArray<string>;
|
|
101
|
+
/** The caller-owned pending personal-dictionary additions. When an author chooses "Add to
|
|
102
|
+
* dictionary" the lint source adds the lowercased word here (the underline clears at once); the
|
|
103
|
+
* host (EditPage) commits this set through the addDictionaryWord action at save time and reconciles
|
|
104
|
+
* it against the merged response. A fresh set by default. */
|
|
105
|
+
pendingAdditions?: Set<string>;
|
|
106
|
+
/** Test-only seam for the spellcheck Worker. The real wasm and dictionary assets are resolved with
|
|
107
|
+
* `import.meta.url` and do not load under the vitest browser dev server, so the component test
|
|
108
|
+
* injects a deterministic fake Worker factory and asks the lint source to skip the `ready` wait.
|
|
109
|
+
* When this is absent the production path is untouched: the real `new Worker(...)` and the real
|
|
110
|
+
* asset resolution. Never set this outside a test. */
|
|
111
|
+
spellcheckTest?: {
|
|
112
|
+
createWorker?: () => import('./spellcheck.js').SpellWorker;
|
|
113
|
+
assumeReady?: boolean;
|
|
114
|
+
};
|
|
115
|
+
/** Tidy mode: while a tidy review is open the surface is read-only the way Preview disables the
|
|
116
|
+
* toolbar, so the author cannot edit underneath a pending review. The host sets this when it opens
|
|
117
|
+
* the review and clears it on apply or cancel. Off by default. */
|
|
118
|
+
tidyMode?: boolean;
|
|
75
119
|
}
|
|
76
120
|
/**
|
|
77
121
|
* The `MarkdownEditor` seam (spec §6, seam 5): a thin wrapper over CodeMirror 6 exposing a bindable
|
|
@@ -5,18 +5,14 @@ each piece of syntax with what it makes, and a closing note explains the ::: lay
|
|
|
5
5
|
Built on a native <dialog>, the DeleteDialog recipe; the host drives it through the exported
|
|
6
6
|
open(), so the component renders no trigger of its own.
|
|
7
7
|
-->
|
|
8
|
-
<script lang="ts">
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
function close() {
|
|
18
|
-
dialog?.close();
|
|
19
|
-
}
|
|
8
|
+
<script lang="ts">import ShortcutsGrid from "./ShortcutsGrid.svelte";
|
|
9
|
+
let dialog = $state(null);
|
|
10
|
+
export function open() {
|
|
11
|
+
dialog?.showModal();
|
|
12
|
+
}
|
|
13
|
+
function close() {
|
|
14
|
+
dialog?.close();
|
|
15
|
+
}
|
|
20
16
|
</script>
|
|
21
17
|
|
|
22
18
|
<dialog class="modal" aria-labelledby="cairn-markdown-help-title" bind:this={dialog}>
|
|
@@ -17,63 +17,24 @@ also resolves alt to the empty string. The committed reference keys off an empty
|
|
|
17
17
|
needs-alt signal, so the emitted record uses an empty alt string for both the decorative and the
|
|
18
18
|
left-blank cases, and a separate decorative flag distinguishes them for the host.
|
|
19
19
|
-->
|
|
20
|
-
<script lang="ts">
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
interface Props {
|
|
39
|
-
/** The image to capture; the card previews it from a local object URL. */
|
|
40
|
-
file: File;
|
|
41
|
-
/** Emit the captured record to the host on insert. */
|
|
42
|
-
oncapture: (record: CaptureRecord) => void;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
let { file, oncapture }: Props = $props();
|
|
46
|
-
|
|
47
|
-
// The proposed display name for this file, computed once. A real stem pre-fills the field and shows
|
|
48
|
-
// the Suggested tag; a generic stem yields null, leaving the field empty, required, and untagged.
|
|
49
|
-
const proposed = $derived(proposedNameFor(file.name));
|
|
50
|
-
// Seeded once from the file the card opened with; untrack marks it a deliberate one-time read, not
|
|
51
|
-
// a reactive miss. The card lives for one file, so a later file swap is out of scope.
|
|
52
|
-
let displayName = $state(untrack(() => proposedNameFor(file.name) ?? ''));
|
|
53
|
-
|
|
54
|
-
// The alt mode: unset until the author picks one, then 'describe' or 'decorative'. Unset emits an
|
|
55
|
-
// empty alt (needs-alt debt); insert never blocks on it.
|
|
56
|
-
let altMode = $state<'describe' | 'decorative' | null>(null);
|
|
57
|
-
let altText = $state('');
|
|
58
|
-
|
|
59
|
-
// A local object URL for the preview. Allocation and revoke live in ONE $effect keyed on the file,
|
|
60
|
-
// never in a $derived: a derivation that calls createObjectURL allocates a resource as a side
|
|
61
|
-
// effect, which can desync from the revoke (a re-derive leaks the prior URL). The matched effect
|
|
62
|
-
// revokes on teardown and on any file change, so the blob never leaks.
|
|
63
|
-
let previewUrl = $state('');
|
|
64
|
-
$effect(() => {
|
|
65
|
-
const url = URL.createObjectURL(file);
|
|
66
|
-
previewUrl = url;
|
|
67
|
-
return () => URL.revokeObjectURL(url);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
function submit(e: SubmitEvent) {
|
|
71
|
-
e.preventDefault();
|
|
72
|
-
// Decorative and write-but-blank both commit an empty alt; the decorative flag distinguishes
|
|
73
|
-
// them for the host. A described image carries the trimmed alt text.
|
|
74
|
-
const alt = altMode === 'describe' ? altText.trim() : '';
|
|
75
|
-
oncapture({ file, displayName: displayName.trim(), alt, decorative: altMode === 'decorative' });
|
|
76
|
-
}
|
|
20
|
+
<script lang="ts">import { untrack } from "svelte";
|
|
21
|
+
import { proposedNameFor } from "./client-ingest.js";
|
|
22
|
+
let { file, oncapture } = $props();
|
|
23
|
+
const proposed = $derived(proposedNameFor(file.name));
|
|
24
|
+
let displayName = $state(untrack(() => proposedNameFor(file.name) ?? ""));
|
|
25
|
+
let altMode = $state(null);
|
|
26
|
+
let altText = $state("");
|
|
27
|
+
let previewUrl = $state("");
|
|
28
|
+
$effect(() => {
|
|
29
|
+
const url = URL.createObjectURL(file);
|
|
30
|
+
previewUrl = url;
|
|
31
|
+
return () => URL.revokeObjectURL(url);
|
|
32
|
+
});
|
|
33
|
+
function submit(e) {
|
|
34
|
+
e.preventDefault();
|
|
35
|
+
const alt = altMode === "describe" ? altText.trim() : "";
|
|
36
|
+
oncapture({ file, displayName: displayName.trim(), alt, decorative: altMode === "decorative" });
|
|
37
|
+
}
|
|
77
38
|
</script>
|
|
78
39
|
|
|
79
40
|
<form class="flex flex-col gap-4" onsubmit={submit}>
|
|
@@ -16,77 +16,38 @@ active segment tinted with a check glyph (the non-color state cue, WCAG 1.4.1),
|
|
|
16
16
|
select. Measure maps to the null role (the measure default, no role brace); the others map to their
|
|
17
17
|
own name.
|
|
18
18
|
-->
|
|
19
|
-
<script lang="ts">
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// The author's working copies, seeded once from the props the control opened with. untrack marks
|
|
53
|
-
// the read a deliberate one-time seed (the control mounts fresh per image), not a reactive miss.
|
|
54
|
-
let captionValue = $state(untrack(() => caption));
|
|
55
|
-
let roleValue = $state<FigureRole | null>(untrack(() => role));
|
|
56
|
-
|
|
57
|
-
// The index of the active role in ROLE_OPTIONS, the roving-tabindex focus target.
|
|
58
|
-
const activeIndex = $derived(ROLE_OPTIONS.findIndex((o) => o.value === roleValue));
|
|
59
|
-
|
|
60
|
-
// The decorative-plus-caption contradiction: a decorative image is hidden from screen readers, so a
|
|
61
|
-
// visible caption on it is a state to flag (never blocked, surfaced for the author to resolve).
|
|
62
|
-
const decorativeWithCaption = $derived(decorative && captionValue.trim() !== '');
|
|
63
|
-
|
|
64
|
-
// The segment refs, so arrow-key navigation can move focus to the newly selected segment.
|
|
65
|
-
let segmentEls = $state<HTMLButtonElement[]>([]);
|
|
66
|
-
|
|
67
|
-
function pickRole(value: FigureRole | null) {
|
|
68
|
-
roleValue = value;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
// Arrow keys move and select within the radiogroup (the roving-tabindex pattern); Home/End jump to
|
|
72
|
-
// the ends. Selection follows focus, the standard radiogroup behavior.
|
|
73
|
-
function onSegmentKeydown(e: KeyboardEvent, index: number) {
|
|
74
|
-
let next = index;
|
|
75
|
-
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') next = (index + 1) % ROLE_OPTIONS.length;
|
|
76
|
-
else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp')
|
|
77
|
-
next = (index - 1 + ROLE_OPTIONS.length) % ROLE_OPTIONS.length;
|
|
78
|
-
else if (e.key === 'Home') next = 0;
|
|
79
|
-
else if (e.key === 'End') next = ROLE_OPTIONS.length - 1;
|
|
80
|
-
else return;
|
|
81
|
-
e.preventDefault();
|
|
82
|
-
pickRole(ROLE_OPTIONS[next].value);
|
|
83
|
-
segmentEls[next]?.focus();
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function submit(e: SubmitEvent) {
|
|
87
|
-
e.preventDefault();
|
|
88
|
-
onapply({ caption: captionValue.trim(), role: roleValue });
|
|
89
|
-
}
|
|
19
|
+
<script lang="ts">import { untrack } from "svelte";
|
|
20
|
+
const ROLE_OPTIONS = [
|
|
21
|
+
{ value: null, label: "Measure" },
|
|
22
|
+
{ value: "center", label: "Center" },
|
|
23
|
+
{ value: "wide", label: "Wide" },
|
|
24
|
+
{ value: "full", label: "Full" }
|
|
25
|
+
];
|
|
26
|
+
let { caption = "", role = null, mode = "wrap", decorative = false, onapply, onunwrap } = $props();
|
|
27
|
+
let captionValue = $state(untrack(() => caption));
|
|
28
|
+
let roleValue = $state(untrack(() => role));
|
|
29
|
+
const activeIndex = $derived(ROLE_OPTIONS.findIndex((o) => o.value === roleValue));
|
|
30
|
+
const decorativeWithCaption = $derived(decorative && captionValue.trim() !== "");
|
|
31
|
+
let segmentEls = $state([]);
|
|
32
|
+
function pickRole(value) {
|
|
33
|
+
roleValue = value;
|
|
34
|
+
}
|
|
35
|
+
function onSegmentKeydown(e, index) {
|
|
36
|
+
let next = index;
|
|
37
|
+
if (e.key === "ArrowRight" || e.key === "ArrowDown") next = (index + 1) % ROLE_OPTIONS.length;
|
|
38
|
+
else if (e.key === "ArrowLeft" || e.key === "ArrowUp")
|
|
39
|
+
next = (index - 1 + ROLE_OPTIONS.length) % ROLE_OPTIONS.length;
|
|
40
|
+
else if (e.key === "Home") next = 0;
|
|
41
|
+
else if (e.key === "End") next = ROLE_OPTIONS.length - 1;
|
|
42
|
+
else return;
|
|
43
|
+
e.preventDefault();
|
|
44
|
+
pickRole(ROLE_OPTIONS[next].value);
|
|
45
|
+
segmentEls[next]?.focus();
|
|
46
|
+
}
|
|
47
|
+
function submit(e) {
|
|
48
|
+
e.preventDefault();
|
|
49
|
+
onapply({ caption: captionValue.trim(), role: roleValue });
|
|
50
|
+
}
|
|
90
51
|
</script>
|
|
91
52
|
|
|
92
53
|
<form class="flex flex-col gap-4" onsubmit={submit}>
|