@glw907/cairn-cms 0.56.2 → 0.57.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 +134 -0
- package/dist/components/AdminLayout.svelte +3 -0
- package/dist/components/CairnAdmin.svelte +8 -1
- package/dist/components/CairnAdmin.svelte.d.ts +2 -0
- package/dist/components/CairnMediaLibrary.svelte +949 -0
- package/dist/components/CairnMediaLibrary.svelte.d.ts +37 -0
- package/dist/components/EditPage.svelte +348 -7
- package/dist/components/EditPage.svelte.d.ts +2 -0
- package/dist/components/MarkdownEditor.svelte +283 -1
- package/dist/components/MarkdownEditor.svelte.d.ts +37 -1
- package/dist/components/MediaCaptureCard.svelte +135 -0
- package/dist/components/MediaCaptureCard.svelte.d.ts +40 -0
- package/dist/components/MediaFigureControl.svelte +247 -0
- package/dist/components/MediaFigureControl.svelte.d.ts +40 -0
- package/dist/components/MediaHeroField.svelte +578 -0
- package/dist/components/MediaHeroField.svelte.d.ts +75 -0
- package/dist/components/MediaInsertPopover.svelte +449 -0
- package/dist/components/MediaInsertPopover.svelte.d.ts +58 -0
- package/dist/components/MediaPicker.svelte +257 -0
- package/dist/components/MediaPicker.svelte.d.ts +41 -0
- package/dist/components/admin-icons.d.ts +12 -0
- package/dist/components/admin-icons.js +12 -0
- package/dist/components/cairn-admin.css +901 -9
- package/dist/components/client-ingest.d.ts +142 -0
- package/dist/components/client-ingest.js +297 -0
- package/dist/components/editor-media.d.ts +11 -0
- package/dist/components/editor-media.js +206 -0
- package/dist/components/editor-placeholder.d.ts +26 -0
- package/dist/components/editor-placeholder.js +166 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/markdown-directives.d.ts +12 -0
- package/dist/components/markdown-directives.js +42 -0
- package/dist/components/markdown-format.d.ts +89 -0
- package/dist/components/markdown-format.js +255 -0
- package/dist/components/media-upload-outcome.d.ts +52 -0
- package/dist/components/media-upload-outcome.js +48 -0
- package/dist/content/compose.js +3 -0
- package/dist/content/frontmatter.js +22 -0
- package/dist/content/manifest.d.ts +4 -0
- package/dist/content/manifest.js +41 -1
- package/dist/content/media-refs.d.ts +7 -0
- package/dist/content/media-refs.js +52 -0
- package/dist/content/schema.d.ts +5 -2
- package/dist/content/schema.js +17 -0
- package/dist/content/types.d.ts +64 -11
- package/dist/content/validate.js +31 -0
- package/dist/delivery/public-routes.d.ts +16 -0
- package/dist/delivery/public-routes.js +46 -3
- package/dist/delivery/seo-fields.js +7 -1
- package/dist/delivery/seo.d.ts +2 -0
- package/dist/delivery/seo.js +3 -0
- package/dist/doctor/checks-local.d.ts +1 -0
- package/dist/doctor/checks-local.js +21 -0
- package/dist/doctor/index.d.ts +3 -1
- package/dist/doctor/index.js +11 -2
- package/dist/doctor/types.d.ts +3 -0
- package/dist/doctor/wrangler-config.d.ts +3 -0
- package/dist/doctor/wrangler-config.js +20 -0
- package/dist/env.d.ts +19 -0
- package/dist/env.js +26 -0
- package/dist/index.d.ts +1 -1
- package/dist/log/events.d.ts +1 -1
- package/dist/media/config.d.ts +24 -0
- package/dist/media/config.js +69 -0
- package/dist/media/delivery-bucket.d.ts +34 -0
- package/dist/media/delivery-bucket.js +10 -0
- package/dist/media/index.d.ts +6 -0
- package/dist/media/index.js +13 -0
- package/dist/media/library-entry.d.ts +30 -0
- package/dist/media/library-entry.js +17 -0
- package/dist/media/manifest.d.ts +44 -0
- package/dist/media/manifest.js +105 -0
- package/dist/media/naming.d.ts +18 -0
- package/dist/media/naming.js +112 -0
- package/dist/media/reconcile.d.ts +36 -0
- package/dist/media/reconcile.js +45 -0
- package/dist/media/reference.d.ts +12 -0
- package/dist/media/reference.js +33 -0
- package/dist/media/sniff.d.ts +18 -0
- package/dist/media/sniff.js +106 -0
- package/dist/media/store.d.ts +25 -0
- package/dist/media/store.js +16 -0
- package/dist/media/transform-url.d.ts +26 -0
- package/dist/media/transform-url.js +38 -0
- package/dist/media/usage.d.ts +48 -0
- package/dist/media/usage.js +90 -0
- package/dist/render/pipeline.d.ts +2 -0
- package/dist/render/pipeline.js +13 -2
- package/dist/render/registry.js +3 -0
- package/dist/render/remark-figure.d.ts +4 -0
- package/dist/render/remark-figure.js +103 -0
- package/dist/render/resolve-media.d.ts +34 -0
- package/dist/render/resolve-media.js +78 -0
- package/dist/render/sanitize-schema.d.ts +4 -2
- package/dist/render/sanitize-schema.js +5 -3
- package/dist/sveltekit/admin-dispatch.d.ts +2 -0
- package/dist/sveltekit/admin-dispatch.js +5 -0
- package/dist/sveltekit/cairn-admin.d.ts +8 -1
- package/dist/sveltekit/cairn-admin.js +10 -2
- package/dist/sveltekit/content-routes.d.ts +77 -2
- package/dist/sveltekit/content-routes.js +470 -10
- package/dist/sveltekit/csrf.d.ts +16 -0
- package/dist/sveltekit/csrf.js +18 -0
- package/dist/sveltekit/guard.js +10 -3
- package/dist/sveltekit/index.d.ts +2 -1
- package/dist/sveltekit/index.js +1 -0
- package/dist/sveltekit/media-route.d.ts +12 -0
- package/dist/sveltekit/media-route.js +137 -0
- package/dist/vite/index.d.ts +3 -0
- package/dist/vite/index.js +7 -2
- package/package.json +7 -1
- package/src/lib/components/AdminLayout.svelte +3 -0
- package/src/lib/components/CairnAdmin.svelte +8 -1
- package/src/lib/components/CairnMediaLibrary.svelte +949 -0
- package/src/lib/components/EditPage.svelte +348 -7
- package/src/lib/components/MarkdownEditor.svelte +283 -1
- package/src/lib/components/MediaCaptureCard.svelte +135 -0
- package/src/lib/components/MediaFigureControl.svelte +247 -0
- package/src/lib/components/MediaHeroField.svelte +578 -0
- package/src/lib/components/MediaInsertPopover.svelte +449 -0
- package/src/lib/components/MediaPicker.svelte +257 -0
- package/src/lib/components/admin-icons.ts +12 -0
- package/src/lib/components/cairn-admin.css +37 -0
- package/src/lib/components/client-ingest.ts +380 -0
- package/src/lib/components/editor-media.ts +248 -0
- package/src/lib/components/editor-placeholder.ts +213 -0
- package/src/lib/components/index.ts +1 -0
- package/src/lib/components/markdown-directives.ts +46 -0
- package/src/lib/components/markdown-format.ts +307 -1
- package/src/lib/components/media-upload-outcome.ts +83 -0
- package/src/lib/content/compose.ts +3 -0
- package/src/lib/content/frontmatter.ts +20 -1
- package/src/lib/content/manifest.ts +44 -1
- package/src/lib/content/media-refs.ts +58 -0
- package/src/lib/content/schema.ts +31 -7
- package/src/lib/content/types.ts +80 -13
- package/src/lib/content/validate.ts +29 -1
- package/src/lib/delivery/public-routes.ts +52 -3
- package/src/lib/delivery/seo-fields.ts +6 -1
- package/src/lib/delivery/seo.ts +5 -0
- package/src/lib/doctor/checks-local.ts +22 -0
- package/src/lib/doctor/index.ts +21 -3
- package/src/lib/doctor/types.ts +3 -0
- package/src/lib/doctor/wrangler-config.ts +23 -0
- package/src/lib/env.ts +28 -0
- package/src/lib/index.ts +2 -0
- package/src/lib/log/events.ts +8 -1
- package/src/lib/media/config.ts +103 -0
- package/src/lib/media/delivery-bucket.ts +41 -0
- package/src/lib/media/index.ts +22 -0
- package/src/lib/media/library-entry.ts +58 -0
- package/src/lib/media/manifest.ts +122 -0
- package/src/lib/media/naming.ts +130 -0
- package/src/lib/media/reconcile.ts +79 -0
- package/src/lib/media/reference.ts +40 -0
- package/src/lib/media/sniff.ts +114 -0
- package/src/lib/media/store.ts +57 -0
- package/src/lib/media/transform-url.ts +58 -0
- package/src/lib/media/usage.ts +152 -0
- package/src/lib/render/pipeline.ts +17 -3
- package/src/lib/render/registry.ts +5 -0
- package/src/lib/render/remark-figure.ts +132 -0
- package/src/lib/render/resolve-media.ts +96 -0
- package/src/lib/render/sanitize-schema.ts +5 -3
- package/src/lib/sveltekit/admin-dispatch.ts +6 -1
- package/src/lib/sveltekit/cairn-admin.ts +13 -3
- package/src/lib/sveltekit/content-routes.ts +589 -12
- package/src/lib/sveltekit/csrf.ts +18 -0
- package/src/lib/sveltekit/guard.ts +12 -3
- package/src/lib/sveltekit/index.ts +6 -0
- package/src/lib/sveltekit/media-route.ts +158 -0
- package/src/lib/vite/index.ts +9 -2
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { MediaLibraryData, ContentFormFailure } from '../sveltekit/content-routes.js';
|
|
2
|
+
interface Props {
|
|
3
|
+
/** The media library load's data: the unioned assets, the per-hash usage overlay, and a
|
|
4
|
+
* degraded-load error. */
|
|
5
|
+
data: MediaLibraryData;
|
|
6
|
+
/** The last media action's result. A `?/mediaDelete` refusal carries the fresh breaking list
|
|
7
|
+
* the in-use face re-opens on; a `?/mediaUpdate` failure carries the error the slide-over
|
|
8
|
+
* surfaces. The route exports one `form`, so this is the merged `ContentFormFailure`. */
|
|
9
|
+
form?: ContentFormFailure | null;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The admin Media Library screen, a peer of Posts and Pages. It browses every committed media asset,
|
|
13
|
+
* shows where each one is used, edits its name and default alt, and deletes it safely. The resting
|
|
14
|
+
* surface is a visual contact-sheet grid (a roving-tabindex listbox of tiles), with a list-density
|
|
15
|
+
* toggle that flips to an enriched sortable table. One toolbar row carries search, a pick-one triage
|
|
16
|
+
* radiogroup (All, Needs alt, Unused), and the density toggle. Filtering, sorting, and a growing
|
|
17
|
+
* client window all run over the full loaded set in component state.
|
|
18
|
+
*
|
|
19
|
+
* Activating a tile or row opens a NON-MODAL detail slide-over from the right (the established
|
|
20
|
+
* details-slide-over recipe): no scrim, the library stays live and in the a11y tree behind it, Escape
|
|
21
|
+
* closes it, focus moves in on open and returns to the originating tile or row on close. It is a
|
|
22
|
+
* labelled region, not a dialog, so it never traps focus or inerts the list. It holds the large
|
|
23
|
+
* preview, the name and the `media:` reference with a copy button, the alt editor (a describe or
|
|
24
|
+
* decorative radiogroup plus the alt field, posting to `?/mediaUpdate` together with the display name
|
|
25
|
+
* and slug), the where-used list grouped published-then-branch, the metadata grid, and the actions.
|
|
26
|
+
*
|
|
27
|
+
* Delete opens a two-faced safe-delete alertdialog: a native modal `<dialog>` with no light dismiss.
|
|
28
|
+
* The in-use face names the breaking entries and gates Delete behind a typed-slug confirmation; the
|
|
29
|
+
* orphan face is a calm confirm. Both post to `?/mediaDelete`. A `form` carrying a fresh
|
|
30
|
+
* `MediaDeleteRefusal` re-opens the in-use face on its fresh breaking list.
|
|
31
|
+
*
|
|
32
|
+
* It is node-safe by construction: it types assets with MediaLibraryEntry from the shared node-safe
|
|
33
|
+
* projection and pulls in no editor module (the editor-boundary test bars a @codemirror leak).
|
|
34
|
+
*/
|
|
35
|
+
declare const CairnMediaLibrary: import("svelte").Component<Props, {}, "">;
|
|
36
|
+
type CairnMediaLibrary = ReturnType<typeof CairnMediaLibrary>;
|
|
37
|
+
export default CairnMediaLibrary;
|
|
@@ -25,6 +25,7 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
25
25
|
import LinkIcon from '@lucide/svelte/icons/link';
|
|
26
26
|
import FileSymlinkIcon from '@lucide/svelte/icons/file-symlink';
|
|
27
27
|
import PanelRightIcon from '@lucide/svelte/icons/panel-right';
|
|
28
|
+
import ImageIcon from '@lucide/svelte/icons/image';
|
|
28
29
|
import { useTopbar } from './topbar-context.js';
|
|
29
30
|
import CsrfField from './CsrfField.svelte';
|
|
30
31
|
import MarkdownEditor from './MarkdownEditor.svelte';
|
|
@@ -32,21 +33,37 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
32
33
|
import ComponentInsertDialog, { insertableDefs, hasSchema } from './ComponentInsertDialog.svelte';
|
|
33
34
|
import LinkPicker from './LinkPicker.svelte';
|
|
34
35
|
import WebLinkDialog from './WebLinkDialog.svelte';
|
|
36
|
+
import MediaInsertPopover from './MediaInsertPopover.svelte';
|
|
37
|
+
import MediaHeroField from './MediaHeroField.svelte';
|
|
38
|
+
import MediaFigureControl from './MediaFigureControl.svelte';
|
|
35
39
|
import DeleteDialog from './DeleteDialog.svelte';
|
|
36
40
|
import RenameDialog from './RenameDialog.svelte';
|
|
37
41
|
import MarkdownHelpDialog from './MarkdownHelpDialog.svelte';
|
|
38
42
|
import ShortcutsDialog from './ShortcutsDialog.svelte';
|
|
39
43
|
import { cairnLinkCompletionSource } from './link-completion.js';
|
|
40
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
findMediaImagesNeedingAlt,
|
|
46
|
+
unwrapCairnLink,
|
|
47
|
+
unwrapFigure,
|
|
48
|
+
updateFigure,
|
|
49
|
+
wrapImageInFigure,
|
|
50
|
+
type FigureAtImage,
|
|
51
|
+
type FigureRole,
|
|
52
|
+
type FormatKind,
|
|
53
|
+
} from './markdown-format.js';
|
|
41
54
|
import { buildPreviewDoc, deviceLabel, previewDevice, previewDevices, type PreviewDeviceId } from './preview-doc.js';
|
|
42
55
|
import { directiveLineKind, findInlineDirectives } from './markdown-directives.js';
|
|
43
56
|
import type { ComponentRegistry, ComponentDef } from '../render/registry.js';
|
|
44
57
|
import { parseComponent, componentRoundTripSafety } from '../render/component-grammar.js';
|
|
45
58
|
import type { IconSet } from '../render/glyph.js';
|
|
46
59
|
import type { ContentFormFailure, EditData } from '../sveltekit/content-routes.js';
|
|
47
|
-
import type { TextareaField, TagsField, FreeTagsField } from '../content/types.js';
|
|
60
|
+
import type { TextareaField, TagsField, FreeTagsField, ImageValue } from '../content/types.js';
|
|
48
61
|
import type { LinkResolve } from '../content/links.js';
|
|
49
62
|
import { manifestLinkResolver } from '../content/manifest.js';
|
|
63
|
+
import type { MediaResolve } from '../render/resolve-media.js';
|
|
64
|
+
import { manifestMediaResolver } from '../render/resolve-media.js';
|
|
65
|
+
import type { MediaEntry } from '../media/manifest.js';
|
|
66
|
+
import { mediaLibraryEntry } from '../media/library-entry.js';
|
|
50
67
|
|
|
51
68
|
interface Props {
|
|
52
69
|
/** The edit load's data, plus the site name for the heading. */
|
|
@@ -54,7 +71,10 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
54
71
|
/** The site's component registry, for the insert palette. */
|
|
55
72
|
registry?: ComponentRegistry;
|
|
56
73
|
/** The site's design-accurate render pipeline; the preview pane renders its output, which the floored pipeline already sanitized. */
|
|
57
|
-
render?: (
|
|
74
|
+
render?: (
|
|
75
|
+
md: string,
|
|
76
|
+
opts?: { stagger?: boolean; resolve?: LinkResolve; resolveMedia?: MediaResolve },
|
|
77
|
+
) => string | Promise<string>;
|
|
58
78
|
/** The site's icon set, for the guided form's icon fields. */
|
|
59
79
|
icons?: IconSet;
|
|
60
80
|
/** The last content action's failure: the save guard's broken links, the delete guard's
|
|
@@ -126,6 +146,12 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
126
146
|
if (target?.closest('dialog, #cairn-pane-write')) return;
|
|
127
147
|
fieldsDirty = true;
|
|
128
148
|
}
|
|
149
|
+
// Mark the details fields dirty without a form input event. The hero field writes its value to
|
|
150
|
+
// hidden inputs, whose programmatic value changes do not fire the form's oninput, so it signals
|
|
151
|
+
// dirty through this helper instead.
|
|
152
|
+
function markFieldsDirty() {
|
|
153
|
+
fieldsDirty = true;
|
|
154
|
+
}
|
|
129
155
|
|
|
130
156
|
// The edit form element, for the Ctrl/Cmd+S shortcut's requestSubmit.
|
|
131
157
|
let editForm = $state<HTMLFormElement | null>(null);
|
|
@@ -362,12 +388,62 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
362
388
|
// The editor's range-replace seam, registered by MarkdownEditor on mount; the dialog's Update
|
|
363
389
|
// routes through it to overwrite an edited block's source span. A no-op until then.
|
|
364
390
|
let replaceRange = $state.raw<(from: number, to: number, text: string) => void>(() => {});
|
|
391
|
+
// The editor's select-range seam, registered by MarkdownEditor on mount; the needs-alt notice's
|
|
392
|
+
// jump control routes through it to land the author on an image that lacks alt. A no-op until then.
|
|
393
|
+
let selectRange = $state.raw<(from: number, to: number) => void>(() => {});
|
|
365
394
|
let insertLink = $state.raw<(href: string, title: string) => void>(() => {});
|
|
366
395
|
// The editor's current selection, registered by MarkdownEditor on mount; the web link dialog
|
|
367
396
|
// reads it for the Text field's default.
|
|
368
397
|
let getSelection = $state.raw<() => string>(() => '');
|
|
369
398
|
// The editor's selection transform, registered by MarkdownEditor on mount; a no-op until then.
|
|
370
399
|
let format = $state.raw<(kind: FormatKind) => void>(() => {});
|
|
400
|
+
|
|
401
|
+
// The media insert seams, registered by MarkdownEditor on mount, mirroring the range holders
|
|
402
|
+
// above. The popover drives the optimistic upload loop through them: the caret anchor, the focus
|
|
403
|
+
// restore, the placeholder api, and the direct-insert path for a picked image. The placeholder
|
|
404
|
+
// api type is referenced inline (import('...').Type), never a static `import type ... from`, so
|
|
405
|
+
// no static edge to the dynamically-imported editor-placeholder module sits in this component
|
|
406
|
+
// (the editor-boundary test bars that edge by a textual `from` scan).
|
|
407
|
+
let caretCoords = $state.raw<() => { left: number; right: number; top: number; bottom: number } | null>(
|
|
408
|
+
() => null,
|
|
409
|
+
);
|
|
410
|
+
let focusEditor = $state.raw<() => void>(() => {});
|
|
411
|
+
let placeholders = $state.raw<import('./editor-placeholder.js').ImagePlaceholderApi | null>(null);
|
|
412
|
+
let insertImageFn = $state.raw<(alt: string, ref: string) => void>(() => {});
|
|
413
|
+
|
|
414
|
+
// A no-op placeholder api so the editor object handed to the popover is never null before the
|
|
415
|
+
// editor registers its real one on mount.
|
|
416
|
+
const noopPlaceholders: import('./editor-placeholder.js').ImagePlaceholderApi = {
|
|
417
|
+
begin: () => 0,
|
|
418
|
+
progress: () => {},
|
|
419
|
+
resolveTo: () => {},
|
|
420
|
+
cancel: () => {},
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
// The editor object the popover drives, delegating through the holders so the latest registered
|
|
424
|
+
// function is always used (the holders start as no-ops and are replaced on mount).
|
|
425
|
+
const editorApi = $derived({
|
|
426
|
+
caretCoords: () => caretCoords(),
|
|
427
|
+
focusEditor: () => focusEditor(),
|
|
428
|
+
placeholders: placeholders ?? noopPlaceholders,
|
|
429
|
+
insertImage: (alt: string, ref: string) => insertImageFn(alt, ref),
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
// The headless media insert popover, opened from the toolbar control, paste, or drop.
|
|
433
|
+
let mediaPopover = $state<MediaInsertPopover | null>(null);
|
|
434
|
+
|
|
435
|
+
// The rendered hero fields' refs (for the needs-alt notice's "Add alt text" action, which focuses
|
|
436
|
+
// the field's own alt input) and their reported needs-alt signals, keyed by field name. A hero is
|
|
437
|
+
// a frontmatter value with no body offset, so its needs-alt signal comes from the field, not the
|
|
438
|
+
// body scanner (findMediaImagesNeedingAlt), and its remediation focuses the alt input, never a
|
|
439
|
+
// source range (selectRange). The records are keyed by field name; `data.fields` is static for the
|
|
440
|
+
// page's lifetime, so a key never goes stale (no per-key cleanup on unmount is needed).
|
|
441
|
+
let heroFieldRefs = $state<Record<string, MediaHeroField>>({});
|
|
442
|
+
let heroNeedsAlt = $state<Record<string, boolean>>({});
|
|
443
|
+
|
|
444
|
+
// The server-owned records from each successful upload this session. They ride the save form as
|
|
445
|
+
// the hidden `media` field, so the save action merges them into media.json.
|
|
446
|
+
let uploadedRecords = $state<MediaEntry[]>([]);
|
|
371
447
|
// A headless dialog instance, typed structurally over its exported open() (the linkPicker idiom).
|
|
372
448
|
type DialogHandle = { open: () => void };
|
|
373
449
|
// The toolbar's insert dialogs. Each holds its own <form>, so they mount outside the edit form
|
|
@@ -397,6 +473,35 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
397
473
|
// matching interface is not exported.
|
|
398
474
|
type CaretComponent = { name: string | null; markdown: string; from: number; to: number };
|
|
399
475
|
let caretComponent = $state<CaretComponent | null>(null);
|
|
476
|
+
|
|
477
|
+
// The media image at the editor caret, reported by MarkdownEditor whenever it changes (null off any
|
|
478
|
+
// media image). The Figure control reads it to wrap a bare image or edit an existing figure; it
|
|
479
|
+
// writes source through the replaceRange seam. The figure dialog is mounted headless below.
|
|
480
|
+
let mediaAtCaret = $state<FigureAtImage | null>(null);
|
|
481
|
+
// The figure control's host <dialog>, opened by the toolbar control. Mounted outside the edit form
|
|
482
|
+
// (a form nested in a form is invalid HTML), the Edit-block dialog pattern.
|
|
483
|
+
let figureDialog = $state<HTMLDialogElement | null>(null);
|
|
484
|
+
// Whether the Figure control is available: a media image sits at the caret and Preview is not
|
|
485
|
+
// showing (the insert controls disable together with the Write surface). The control is always
|
|
486
|
+
// rendered (it never mounts on caret move); only its enabled state changes.
|
|
487
|
+
const figureAvailable = $derived(mediaAtCaret != null && !insertDisabled);
|
|
488
|
+
const figureLabel = $derived(
|
|
489
|
+
figureAvailable
|
|
490
|
+
? mediaAtCaret?.figure
|
|
491
|
+
? 'Edit the figure at the cursor'
|
|
492
|
+
: 'Wrap the image at the cursor in a figure'
|
|
493
|
+
: 'Place the cursor on an image to add a figure',
|
|
494
|
+
);
|
|
495
|
+
// Whether the image at the caret is decorative (empty or whitespace-only alt). The token came from
|
|
496
|
+
// a parsed image node, so the alt is the source between `![` and the closing `]` before `](`. An
|
|
497
|
+
// empty alt is the needs-alt signal; the figure control surfaces it and the decorative-plus-caption
|
|
498
|
+
// warning. Derived from the reported token so it tracks the caret.
|
|
499
|
+
const figureDecorative = $derived.by(() => {
|
|
500
|
+
if (!mediaAtCaret) return false;
|
|
501
|
+
const token = body.slice(mediaAtCaret.imageFrom, mediaAtCaret.imageTo);
|
|
502
|
+
const match = /^!\[([\s\S]*?)\]\(/.exec(token);
|
|
503
|
+
return (match?.[1] ?? '').trim() === '';
|
|
504
|
+
});
|
|
400
505
|
// A def is actionable for guided edit when it has a schema (the same notion the insert catalog
|
|
401
506
|
// lists by): a template-only def has no form to re-open into. Reuses the dialog's exported
|
|
402
507
|
// hasSchema so the two surfaces can never drift on what counts as editable.
|
|
@@ -476,6 +581,68 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
476
581
|
insertDialog?.editComponent(editable.def, values, editable.range);
|
|
477
582
|
}
|
|
478
583
|
|
|
584
|
+
// The figure dialog's pre-fill, snapshotted when the control opens so the form never mixes a newer
|
|
585
|
+
// caret with the values it opened on. Captured from mediaAtCaret at open time: edit mode with the
|
|
586
|
+
// figure's caption/role when a figure wraps the image, else wrap mode with empty caption and the
|
|
587
|
+
// measure default. decorative rides the snapshot too. Null while the dialog is closed.
|
|
588
|
+
let figurePrefill = $state<{
|
|
589
|
+
mode: 'wrap' | 'edit';
|
|
590
|
+
caption: string;
|
|
591
|
+
role: FigureRole | null;
|
|
592
|
+
decorative: boolean;
|
|
593
|
+
image: { from: number; to: number };
|
|
594
|
+
figureRange: { from: number; to: number } | null;
|
|
595
|
+
} | null>(null);
|
|
596
|
+
|
|
597
|
+
// Open the figure control over the media image at the caret. Inert unless a media image sits there
|
|
598
|
+
// and the Write surface is up, the same gate the toolbar control shows. The snapshot is the source
|
|
599
|
+
// of truth for the apply handlers, so a caret move while the dialog is open never re-targets it.
|
|
600
|
+
function openFigure() {
|
|
601
|
+
if (!figureAvailable || !mediaAtCaret) return;
|
|
602
|
+
const at = mediaAtCaret;
|
|
603
|
+
figurePrefill = {
|
|
604
|
+
mode: at.figure ? 'edit' : 'wrap',
|
|
605
|
+
caption: at.figure?.caption ?? '',
|
|
606
|
+
role: at.figure?.role ?? null,
|
|
607
|
+
decorative: figureDecorative,
|
|
608
|
+
image: { from: at.imageFrom, to: at.imageTo },
|
|
609
|
+
figureRange: at.figure ? { from: at.figure.from, to: at.figure.to } : null,
|
|
610
|
+
};
|
|
611
|
+
figureDialog?.showModal();
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Apply the control's choice through the replaceRange seam, then close. Wrap a bare image or update
|
|
615
|
+
// an existing figure, off the snapshot the dialog opened on. The pure transform owns the source
|
|
616
|
+
// shape and keeps the media token byte-intact; the preview stays read-only.
|
|
617
|
+
function applyFigure(choice: { caption: string; role: FigureRole | null }) {
|
|
618
|
+
const pre = figurePrefill;
|
|
619
|
+
if (!pre) return;
|
|
620
|
+
const result =
|
|
621
|
+
pre.mode === 'edit' && pre.figureRange
|
|
622
|
+
? updateFigure(body, pre.figureRange, choice.caption, choice.role)
|
|
623
|
+
: wrapImageInFigure(body, pre.image.from, pre.image.to, choice.caption, choice.role);
|
|
624
|
+
writeFigureResult(result);
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
// Unwrap the figure back to its bare image, then close. Edit mode only (the snapshot carries the
|
|
628
|
+
// figure range). The bare image token is restored verbatim by the pure transform.
|
|
629
|
+
function unwrapFigureAction() {
|
|
630
|
+
const pre = figurePrefill;
|
|
631
|
+
if (!pre || !pre.figureRange) return;
|
|
632
|
+
writeFigureResult(unwrapFigure(body, pre.figureRange));
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// Write a figure transform's result back to the editor: overwrite the whole doc through the
|
|
636
|
+
// replaceRange seam, then place the selection the transform chose (the seam alone drops the caret
|
|
637
|
+
// at the end). replaceRange dispatches the doc change and focuses the surface; selectRange then
|
|
638
|
+
// dispatches a selection-only transaction, which CodeMirror's history does not record as its own
|
|
639
|
+
// undoable event, so one undo reverts the whole figure write. Close the dialog last.
|
|
640
|
+
function writeFigureResult(result: { doc: string; from: number; to: number }) {
|
|
641
|
+
replaceRange(0, body.length, result.doc);
|
|
642
|
+
selectRange(result.from, result.to);
|
|
643
|
+
figureDialog?.close();
|
|
644
|
+
}
|
|
645
|
+
|
|
479
646
|
// The header's status badge, in ConceptList's vocabulary: a pending entry reads Edited (or New
|
|
480
647
|
// when it has never been published); otherwise the live site matches and it reads Published.
|
|
481
648
|
const status = $derived.by(() => {
|
|
@@ -541,6 +708,21 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
541
708
|
}
|
|
542
709
|
}
|
|
543
710
|
|
|
711
|
+
// The media images in the live body that carry no alt text, recomputed as the author types. Alt is
|
|
712
|
+
// accessibility debt, never a render or publish failure, so this drives a non-blocking warning the
|
|
713
|
+
// author can act on or leave; the count drops and the notice clears as each alt is filled.
|
|
714
|
+
const needsAlt = $derived(findMediaImagesNeedingAlt(body));
|
|
715
|
+
|
|
716
|
+
// The declared image (hero) fields, for labelling the needs-alt notice's frontmatter rows.
|
|
717
|
+
const imageFields = $derived(
|
|
718
|
+
data.fields.filter((f) => f.type === 'image').map((f) => ({ name: f.name, label: f.label })),
|
|
719
|
+
);
|
|
720
|
+
// The frontmatter-hero needs-alt rows: each image field whose hero reports a needs-alt signal. The
|
|
721
|
+
// row's action focuses the field's alt input (the body scanner and its source-range jump cannot
|
|
722
|
+
// reach a frontmatter value). The headline count sums these with the body scanner's hits.
|
|
723
|
+
const heroRows = $derived(imageFields.filter((f) => heroNeedsAlt[f.name]));
|
|
724
|
+
const needsAltCount = $derived(needsAlt.length + heroRows.length);
|
|
725
|
+
|
|
544
726
|
// The delete guard's inbound linkers, from a refused delete (fail 409). Empty when the delete was
|
|
545
727
|
// not refused. When set, a delete was blocked by a link that appeared since the page loaded.
|
|
546
728
|
const deleteRefusedLinks = $derived(form?.inboundLinks ?? []);
|
|
@@ -653,6 +835,28 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
653
835
|
// returns undefined for a missing target so the render step marks it cairn-broken-link.
|
|
654
836
|
const resolveLink = $derived(manifestLinkResolver(data.linkTargets));
|
|
655
837
|
|
|
838
|
+
// The media analog: it turns a media: reference into its /media delivery path in the preview, and
|
|
839
|
+
// returns undefined for a missing target so the render step marks it cairn-broken-media. The
|
|
840
|
+
// committed mediaTargets projection is merged with this session's uploaded records (the same
|
|
841
|
+
// override the picker's library does), so a just-uploaded image renders its thumbnail in the live
|
|
842
|
+
// preview before the next save commits it, rather than reading as a broken reference.
|
|
843
|
+
const resolveMediaTargets = $derived({
|
|
844
|
+
...data.mediaTargets,
|
|
845
|
+
...Object.fromEntries(
|
|
846
|
+
uploadedRecords.map((r) => [r.hash, { slug: r.slug, ext: r.ext, contentType: r.contentType }]),
|
|
847
|
+
),
|
|
848
|
+
});
|
|
849
|
+
const resolveMedia = $derived(manifestMediaResolver(resolveMediaTargets));
|
|
850
|
+
|
|
851
|
+
// The picker's library, the committed projection merged with this session's uploaded records,
|
|
852
|
+
// keyed by content hash. An uploaded record overrides a committed entry on a hash match (the same
|
|
853
|
+
// hash is the same bytes, so the override is harmless). This is what the editor decorates with, so
|
|
854
|
+
// a just-uploaded image carries its source chip before the next save commits it.
|
|
855
|
+
const mediaLibrary = $derived({
|
|
856
|
+
...data.mediaLibrary,
|
|
857
|
+
...Object.fromEntries(uploadedRecords.map((r) => [r.hash, mediaLibraryEntry(r)])),
|
|
858
|
+
});
|
|
859
|
+
|
|
656
860
|
// The [[ autocomplete source over the same link targets, handed to the editor's generic seam.
|
|
657
861
|
const completionSources = $derived([cairnLinkCompletionSource(data.linkTargets)]);
|
|
658
862
|
|
|
@@ -719,10 +923,11 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
719
923
|
if (mode !== 'preview' || !render) return;
|
|
720
924
|
const md = body;
|
|
721
925
|
const resolve = resolveLink; // tracked read in the effect body
|
|
926
|
+
const resolveMediaRef = resolveMedia; // tracked read in the effect body
|
|
722
927
|
const run = ++previewRun;
|
|
723
928
|
const handle = setTimeout(async () => {
|
|
724
929
|
try {
|
|
725
|
-
const html = await render(md, { resolve });
|
|
930
|
+
const html = await render(md, { resolve, resolveMedia: resolveMediaRef });
|
|
726
931
|
if (run === previewRun) {
|
|
727
932
|
previewHtml = html;
|
|
728
933
|
previewFailed = false;
|
|
@@ -921,6 +1126,45 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
921
1126
|
</ul>
|
|
922
1127
|
</div>
|
|
923
1128
|
{/if}
|
|
1129
|
+
<!-- The publish-time needs-alt notice: a non-blocking warning, never a block. Alt text is
|
|
1130
|
+
accessibility debt, so the author can add it now or save without it; the count drops live as
|
|
1131
|
+
each alt is filled and the notice clears at zero. The leading glyph carries the state alongside
|
|
1132
|
+
the count, so the caution reads without relying on hue. Each row's jump control selects the
|
|
1133
|
+
image in the source through the editor's select-range seam, landing the author on it to type
|
|
1134
|
+
the alt. The role="status" live region renders unconditionally (present and empty at load), so
|
|
1135
|
+
when the first debt appears its count change announces; a region conditionally mounted with its
|
|
1136
|
+
first content may not be observed by assistive tech (WCAG 4.1.3). The visible alert chrome and
|
|
1137
|
+
content gate on the count, so an empty region shows nothing. A plain wrapper (not display:contents)
|
|
1138
|
+
carries the role, since some assistive tech drops a role off a display:contents box. -->
|
|
1139
|
+
<div role="status">
|
|
1140
|
+
{#if needsAltCount}
|
|
1141
|
+
<div class="alert alert-warning mb-4 flex-col items-start text-sm">
|
|
1142
|
+
<p class="flex items-center gap-2 font-medium">
|
|
1143
|
+
<svg class="h-4 w-4 shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
1144
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v4m0 4h.01M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z" />
|
|
1145
|
+
</svg>
|
|
1146
|
+
<span>{needsAltCount} {needsAltCount === 1 ? 'image needs' : 'images need'} alt text</span>
|
|
1147
|
+
</p>
|
|
1148
|
+
<p>Alt text describes an image for readers who cannot see it. Add it now, or save and come back to it.</p>
|
|
1149
|
+
<ul class="mt-1 w-full">
|
|
1150
|
+
{#each needsAlt as item (item.from)}
|
|
1151
|
+
<li class="flex items-center justify-between gap-2">
|
|
1152
|
+
<code class="text-xs">{item.ref}</code>
|
|
1153
|
+
<button type="button" class="btn btn-xs" onclick={() => selectRange(item.from, item.to)}>Add alt text</button>
|
|
1154
|
+
</li>
|
|
1155
|
+
{/each}
|
|
1156
|
+
<!-- The frontmatter-hero rows: a hero has no body offset, so its action focuses the field's
|
|
1157
|
+
own alt input rather than a source range. -->
|
|
1158
|
+
{#each heroRows as hero (hero.name)}
|
|
1159
|
+
<li class="flex items-center justify-between gap-2">
|
|
1160
|
+
<span class="text-xs font-medium">{hero.label}</span>
|
|
1161
|
+
<button type="button" class="btn btn-xs" onclick={() => heroFieldRefs[hero.name]?.focusAlt()}>Add alt text</button>
|
|
1162
|
+
</li>
|
|
1163
|
+
{/each}
|
|
1164
|
+
</ul>
|
|
1165
|
+
</div>
|
|
1166
|
+
{/if}
|
|
1167
|
+
</div>
|
|
924
1168
|
{#if draftWarning}
|
|
925
1169
|
<div class="alert alert-warning mb-4 text-sm">
|
|
926
1170
|
Saved. Note: this page links to unpublished {draftWarning.includes(',') ? 'pages' : 'a page'} ({draftWarning}), which will 404 until published.
|
|
@@ -1040,9 +1284,10 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
1040
1284
|
<button
|
|
1041
1285
|
type="button"
|
|
1042
1286
|
class="btn btn-ghost btn-sm btn-square"
|
|
1043
|
-
disabled
|
|
1044
|
-
aria-label="
|
|
1045
|
-
title="
|
|
1287
|
+
disabled={insertDisabled}
|
|
1288
|
+
aria-label="Insert image"
|
|
1289
|
+
title="Insert image"
|
|
1290
|
+
onclick={() => mediaPopover?.open('chooser')}
|
|
1046
1291
|
>
|
|
1047
1292
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true">
|
|
1048
1293
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
|
|
@@ -1050,6 +1295,26 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
1050
1295
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
|
1051
1296
|
</svg>
|
|
1052
1297
|
</button>
|
|
1298
|
+
<!-- The Figure control: always rendered, enabled only when the caret sits on a media image
|
|
1299
|
+
(and the Write surface is up). It never mounts or unmounts on caret movement; only its
|
|
1300
|
+
enabled state changes (the Edit-block pattern). The unavailable state uses aria-disabled,
|
|
1301
|
+
not the native disabled attribute, so the control stays focusable and its reason reaches
|
|
1302
|
+
assistive technology; openFigure() early-returns so the dead click is inert. The dimming
|
|
1303
|
+
uses opacity and cursor utilities, never .btn-disabled, because that sets
|
|
1304
|
+
pointer-events: none and would suppress the title tooltip a mouse user reads for the why. -->
|
|
1305
|
+
<button
|
|
1306
|
+
type="button"
|
|
1307
|
+
class="btn btn-sm btn-ghost btn-square"
|
|
1308
|
+
class:opacity-50={!figureAvailable}
|
|
1309
|
+
class:cursor-not-allowed={!figureAvailable}
|
|
1310
|
+
aria-haspopup="dialog"
|
|
1311
|
+
aria-label={figureLabel}
|
|
1312
|
+
title={figureLabel}
|
|
1313
|
+
aria-disabled={!figureAvailable}
|
|
1314
|
+
onclick={openFigure}
|
|
1315
|
+
>
|
|
1316
|
+
<ImageIcon class="h-4 w-4" aria-hidden="true" />
|
|
1317
|
+
</button>
|
|
1053
1318
|
{/snippet}
|
|
1054
1319
|
</EditorToolbar>
|
|
1055
1320
|
{/if}
|
|
@@ -1062,14 +1327,25 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
1062
1327
|
{surface}
|
|
1063
1328
|
registerInsert={(fn) => (insert = fn)}
|
|
1064
1329
|
onComponentAtCaret={(info) => (caretComponent = info)}
|
|
1330
|
+
onMediaImageAtCaret={(info) => (mediaAtCaret = info)}
|
|
1065
1331
|
registerReplaceRange={(fn) => (replaceRange = fn)}
|
|
1332
|
+
registerSelectRange={(fn) => (selectRange = fn)}
|
|
1066
1333
|
registerInsertLink={(fn) => (insertLink = fn)}
|
|
1067
1334
|
registerGetSelection={(fn) => (getSelection = fn)}
|
|
1068
1335
|
registerFormat={(fn) => (format = fn)}
|
|
1336
|
+
registerCaretCoords={(fn) => (caretCoords = fn)}
|
|
1337
|
+
registerFocusEditor={(fn) => (focusEditor = fn)}
|
|
1338
|
+
registerImagePlaceholders={(api) => (placeholders = api)}
|
|
1339
|
+
registerInsertImage={(fn) => (insertImageFn = fn)}
|
|
1340
|
+
onImageIngest={(file) => mediaPopover?.open('capture', file)}
|
|
1069
1341
|
{completionSources}
|
|
1342
|
+
{mediaLibrary}
|
|
1070
1343
|
{focusMode}
|
|
1071
1344
|
{typewriter}
|
|
1072
1345
|
/>
|
|
1346
|
+
<!-- The accumulated uploaded records ride the save form alongside the body. The save action
|
|
1347
|
+
reads `media` and merges these records into media.json (publish submits the same form). -->
|
|
1348
|
+
<input type="hidden" name="media" value={JSON.stringify(uploadedRecords)} />
|
|
1073
1349
|
</div>
|
|
1074
1350
|
{#if mode === 'preview'}
|
|
1075
1351
|
<!-- The preview ground: recessed under the floating frame card so the page reads as a
|
|
@@ -1290,6 +1566,20 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
1290
1566
|
value={tagValue}
|
|
1291
1567
|
/>
|
|
1292
1568
|
</label>
|
|
1569
|
+
{:else if field.type === 'image'}
|
|
1570
|
+
{@const heroValue = data.frontmatter[field.name] as ImageValue | undefined}
|
|
1571
|
+
<MediaHeroField
|
|
1572
|
+
bind:this={heroFieldRefs[field.name]}
|
|
1573
|
+
field={{ name: field.name, label: field.label }}
|
|
1574
|
+
value={heroValue}
|
|
1575
|
+
decorative={heroValue?.decorative ?? false}
|
|
1576
|
+
mediaLibrary={mediaLibrary}
|
|
1577
|
+
conceptId={data.conceptId}
|
|
1578
|
+
id={data.id}
|
|
1579
|
+
onuploaded={(record) => (uploadedRecords = [...uploadedRecords, record])}
|
|
1580
|
+
ondirty={markFieldsDirty}
|
|
1581
|
+
onneedsaltchange={(n) => (heroNeedsAlt = { ...heroNeedsAlt, [field.name]: n })}
|
|
1582
|
+
/>
|
|
1293
1583
|
{:else}
|
|
1294
1584
|
<label class="flex flex-col gap-1">
|
|
1295
1585
|
<span class="text-sm font-medium">{field.label}</span>
|
|
@@ -1365,6 +1655,57 @@ count, the Prose/Markup posture pair, the focus and typewriter toggles, and the
|
|
|
1365
1655
|
<WebLinkDialog bind:this={webLinkDialog} trigger={false} insert={insertLink} selection={getSelection} />
|
|
1366
1656
|
<LinkPicker bind:this={linkPicker} trigger={false} linkTargets={data.linkTargets} insert={insertLink} />
|
|
1367
1657
|
|
|
1658
|
+
<!-- The media insert popover, mounted headless: the toolbar control, a paste, or a drop drives it
|
|
1659
|
+
through its exported open(). On a successful upload it hands the server-owned record up; the
|
|
1660
|
+
record joins uploadedRecords (the hidden save field) and the merged library (the source chip). -->
|
|
1661
|
+
<MediaInsertPopover
|
|
1662
|
+
bind:this={mediaPopover}
|
|
1663
|
+
trigger={false}
|
|
1664
|
+
conceptId={data.conceptId}
|
|
1665
|
+
id={data.id}
|
|
1666
|
+
library={mediaLibrary}
|
|
1667
|
+
editor={editorApi}
|
|
1668
|
+
onuploaded={(record) => (uploadedRecords = [...uploadedRecords, record])}
|
|
1669
|
+
/>
|
|
1670
|
+
|
|
1671
|
+
<!-- The figure control's host dialog, mounted headless outside the edit form (the control holds its
|
|
1672
|
+
own <form>). The toolbar Figure control opens it through openFigure(), pre-filled from the caret
|
|
1673
|
+
snapshot. The control is keyed on figurePrefill so it remounts fresh per open, seeding its fields
|
|
1674
|
+
from the new caption/role. The native <dialog> gives the focus trap and Escape for free, and the
|
|
1675
|
+
close event (the X, the backdrop, Escape, and the apply path all fire it) clears the snapshot so
|
|
1676
|
+
the host state matches the closed dialog. -->
|
|
1677
|
+
<dialog
|
|
1678
|
+
class="modal"
|
|
1679
|
+
aria-labelledby="cairn-figure-dialog-title"
|
|
1680
|
+
bind:this={figureDialog}
|
|
1681
|
+
onclose={() => (figurePrefill = null)}
|
|
1682
|
+
>
|
|
1683
|
+
<div class="modal-box max-w-sm">
|
|
1684
|
+
<div class="mb-3 flex items-center justify-between">
|
|
1685
|
+
<h2 id="cairn-figure-dialog-title" class="flex items-center gap-2 text-base font-semibold">
|
|
1686
|
+
<ImageIcon class="h-4 w-4 text-[var(--color-accent)]" aria-hidden="true" />
|
|
1687
|
+
{figurePrefill?.mode === 'edit' ? 'Edit figure' : 'Wrap in a figure'}
|
|
1688
|
+
</h2>
|
|
1689
|
+
<button type="button" class="btn btn-ghost btn-sm" aria-label="Close" onclick={() => figureDialog?.close()}>✕</button>
|
|
1690
|
+
</div>
|
|
1691
|
+
{#if figurePrefill}
|
|
1692
|
+
{#key figurePrefill}
|
|
1693
|
+
<MediaFigureControl
|
|
1694
|
+
caption={figurePrefill.caption}
|
|
1695
|
+
role={figurePrefill.role}
|
|
1696
|
+
mode={figurePrefill.mode}
|
|
1697
|
+
decorative={figurePrefill.decorative}
|
|
1698
|
+
onapply={applyFigure}
|
|
1699
|
+
onunwrap={unwrapFigureAction}
|
|
1700
|
+
/>
|
|
1701
|
+
{/key}
|
|
1702
|
+
{/if}
|
|
1703
|
+
</div>
|
|
1704
|
+
<form method="dialog" class="modal-backdrop">
|
|
1705
|
+
<button tabindex="-1" aria-label="Close">close</button>
|
|
1706
|
+
</form>
|
|
1707
|
+
</dialog>
|
|
1708
|
+
|
|
1368
1709
|
<!-- The lifecycle dialogs, mounted headless: the header's overflow menu drives them through their
|
|
1369
1710
|
exported open(). Their POST forms flip the leaving flag so the leave guard stands down. -->
|
|
1370
1711
|
<RenameDialog
|
|
@@ -2,6 +2,7 @@ import type { ComponentRegistry } from '../render/registry.js';
|
|
|
2
2
|
import type { IconSet } from '../render/glyph.js';
|
|
3
3
|
import type { ContentFormFailure, EditData } from '../sveltekit/content-routes.js';
|
|
4
4
|
import type { LinkResolve } from '../content/links.js';
|
|
5
|
+
import type { MediaResolve } from '../render/resolve-media.js';
|
|
5
6
|
interface Props {
|
|
6
7
|
/** The edit load's data, plus the site name for the heading. */
|
|
7
8
|
data: EditData & {
|
|
@@ -13,6 +14,7 @@ interface Props {
|
|
|
13
14
|
render?: (md: string, opts?: {
|
|
14
15
|
stagger?: boolean;
|
|
15
16
|
resolve?: LinkResolve;
|
|
17
|
+
resolveMedia?: MediaResolve;
|
|
16
18
|
}) => string | Promise<string>;
|
|
17
19
|
/** The site's icon set, for the guided form's icon fields. */
|
|
18
20
|
icons?: IconSet;
|