@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
@@ -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
- import ShortcutsGrid from './ShortcutsGrid.svelte';
10
-
11
- let dialog = $state<HTMLDialogElement | null>(null);
12
-
13
- /** Open the cheat sheet. The trigger lives in the host (the edit page's editor footer). */
14
- export function open() {
15
- dialog?.showModal();
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
- import { untrack } from 'svelte';
22
- import { proposedNameFor } from './client-ingest.js';
23
-
24
- /** The record the card emits to its host on insert. */
25
- interface CaptureRecord {
26
- /** The image bytes the author is placing. */
27
- file: File;
28
- /** The editable display name, the proposed stem or the author's edit. */
29
- displayName: string;
30
- /** The alt text. Empty for a decorative image or for an author who proceeded without alt; the
31
- * host commits an empty alt as the needs-alt signal in the `![](media:...)` reference. */
32
- alt: string;
33
- /** True when the author marked the image decorative, distinguishing a deliberate empty alt from
34
- * a left-blank one. The committed alt is empty either way. */
35
- decorative: boolean;
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
- import { untrack } from 'svelte';
21
- import type { FigureRole } from './markdown-format.js';
22
-
23
- /** The roles the segmented control offers, with Measure standing for the null (measure-default)
24
- * role. Declared as a const tuple so the segment loop and the keyboard handler share one source. */
25
- const ROLE_OPTIONS: { value: FigureRole | null; label: string }[] = [
26
- { value: null, label: 'Measure' },
27
- { value: 'center', label: 'Center' },
28
- { value: 'wide', label: 'Wide' },
29
- { value: 'full', label: 'Full' },
30
- ];
31
-
32
- interface Props {
33
- /** The initial caption; the field seeds from it and the author edits a local copy. */
34
- caption?: string;
35
- /** The initial placement role, or null for the measure default. */
36
- role?: FigureRole | null;
37
- /** `wrap` for a bare image (the primary action wraps it), `edit` for an existing figure (the
38
- * primary action updates it and a ghost action unwraps it). */
39
- mode?: 'wrap' | 'edit';
40
- /** Whether the image's alt is empty or marked decorative; the host derives it. Drives the
41
- * alt-status row and the decorative-plus-caption warning. */
42
- decorative?: boolean;
43
- /** Emit the chosen caption and role: the host wraps (wrap mode) or updates (edit mode). */
44
- onapply: (choice: { caption: string; role: FigureRole | null }) => void;
45
- /** Emit the unwrap action (edit mode only); the host replaces the figure with its bare image. */
46
- onunwrap?: () => void;
47
- }
48
-
49
- let { caption = '', role = null, mode = 'wrap', decorative = false, onapply, onunwrap }: Props =
50
- $props();
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}>