@glw907/cairn-cms 0.60.0 → 0.62.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 (281) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/components/AdminLayout.svelte +152 -229
  3. package/dist/components/CairnAdmin.svelte +13 -42
  4. package/dist/components/CairnLogo.svelte +1 -6
  5. package/dist/components/CairnMediaLibrary.svelte +821 -1210
  6. package/dist/components/CairnTidySettings.svelte +194 -261
  7. package/dist/components/CairnTidySettings.svelte.d.ts +1 -1
  8. package/dist/components/ComponentForm.svelte +110 -185
  9. package/dist/components/ComponentInsertDialog.svelte +163 -283
  10. package/dist/components/ConceptList.svelte +111 -191
  11. package/dist/components/ConfirmPage.svelte +5 -12
  12. package/dist/components/CsrfField.svelte +5 -11
  13. package/dist/components/DeleteDialog.svelte +15 -42
  14. package/dist/components/EditPage.svelte +781 -1205
  15. package/dist/components/EditorToolbar.svelte +108 -170
  16. package/dist/components/HelpHome.svelte +824 -0
  17. package/dist/components/HelpHome.svelte.d.ts +22 -0
  18. package/dist/components/IconPicker.svelte +23 -53
  19. package/dist/components/LinkPicker.svelte +34 -58
  20. package/dist/components/LoginPage.svelte +14 -27
  21. package/dist/components/ManageEditors.svelte +3 -15
  22. package/dist/components/MarkdownEditor.svelte +689 -957
  23. package/dist/components/MarkdownHelpDialog.svelte +12 -27
  24. package/dist/components/MediaCaptureCard.svelte +18 -57
  25. package/dist/components/MediaFigureControl.svelte +32 -71
  26. package/dist/components/MediaHeroField.svelte +210 -329
  27. package/dist/components/MediaInsertPopover.svelte +156 -283
  28. package/dist/components/MediaPicker.svelte +67 -131
  29. package/dist/components/NavTree.svelte +46 -78
  30. package/dist/components/RenameDialog.svelte +16 -43
  31. package/dist/components/ShortcutsDialog.svelte +9 -13
  32. package/dist/components/ShortcutsGrid.svelte +1 -2
  33. package/dist/components/TidyReview.svelte +140 -248
  34. package/dist/components/WebLinkDialog.svelte +19 -40
  35. package/dist/components/cairn-admin.css +4 -0
  36. package/dist/components/client-ingest.d.ts +16 -8
  37. package/dist/components/client-ingest.js +12 -6
  38. package/dist/components/editor-media.js +16 -8
  39. package/dist/components/editor-placeholder.d.ts +4 -2
  40. package/dist/components/editor-tidy.d.ts +24 -12
  41. package/dist/components/editor-tidy.js +8 -4
  42. package/dist/components/index.d.ts +1 -0
  43. package/dist/components/index.js +1 -0
  44. package/dist/components/link-completion.d.ts +12 -6
  45. package/dist/components/link-completion.js +12 -6
  46. package/dist/components/markdown-directives.d.ts +9 -6
  47. package/dist/components/markdown-directives.js +9 -6
  48. package/dist/components/markdown-format.d.ts +7 -2
  49. package/dist/components/markdown-format.js +59 -28
  50. package/dist/components/markdown-reference.d.ts +8 -0
  51. package/dist/components/markdown-reference.js +22 -0
  52. package/dist/components/media-upload-outcome.d.ts +12 -6
  53. package/dist/components/objective-errors.d.ts +8 -4
  54. package/dist/components/objective-errors.js +8 -4
  55. package/dist/components/preview-doc.d.ts +4 -2
  56. package/dist/components/preview-doc.js +4 -2
  57. package/dist/components/spellcheck.d.ts +57 -29
  58. package/dist/components/spellcheck.js +50 -20
  59. package/dist/components/tidy-categorize.d.ts +20 -10
  60. package/dist/components/tidy-categorize.js +16 -8
  61. package/dist/components/tidy-validate.d.ts +12 -6
  62. package/dist/components/tidy-validate.js +20 -10
  63. package/dist/components/topbar-context.d.ts +4 -2
  64. package/dist/content/advisories.d.ts +51 -0
  65. package/dist/content/advisories.js +79 -0
  66. package/dist/content/compose.d.ts +4 -2
  67. package/dist/content/compose.js +1 -0
  68. package/dist/content/excerpt.js +4 -2
  69. package/dist/content/getting-started.d.ts +18 -0
  70. package/dist/content/getting-started.js +12 -0
  71. package/dist/content/links.d.ts +16 -8
  72. package/dist/content/links.js +12 -6
  73. package/dist/content/manifest.d.ts +36 -18
  74. package/dist/content/manifest.js +32 -16
  75. package/dist/content/media-refs.d.ts +4 -2
  76. package/dist/content/media-refs.js +4 -2
  77. package/dist/content/media-rewrite.d.ts +8 -4
  78. package/dist/content/media-rewrite.js +76 -38
  79. package/dist/content/schema.d.ts +20 -10
  80. package/dist/content/site-dictionary.d.ts +4 -2
  81. package/dist/content/site-dictionary.js +8 -4
  82. package/dist/content/types.d.ts +97 -42
  83. package/dist/delivery/CairnHead.svelte +8 -11
  84. package/dist/delivery/content-index.d.ts +16 -8
  85. package/dist/delivery/feeds.js +4 -2
  86. package/dist/delivery/json-ld.d.ts +3 -0
  87. package/dist/delivery/json-ld.js +3 -0
  88. package/dist/delivery/manifest.d.ts +4 -2
  89. package/dist/delivery/manifest.js +4 -2
  90. package/dist/delivery/public-routes.d.ts +12 -6
  91. package/dist/delivery/public-routes.js +4 -2
  92. package/dist/delivery/seo-fields.d.ts +12 -6
  93. package/dist/delivery/seo-fields.js +8 -4
  94. package/dist/delivery/site-indexes.d.ts +4 -2
  95. package/dist/delivery/site-resolver.d.ts +4 -2
  96. package/dist/delivery/site-resolver.js +4 -2
  97. package/dist/doctor/cloudflare-api.d.ts +6 -0
  98. package/dist/doctor/cloudflare-api.js +6 -0
  99. package/dist/doctor/index.d.ts +12 -6
  100. package/dist/doctor/report.d.ts +3 -0
  101. package/dist/doctor/report.js +3 -0
  102. package/dist/doctor/run.d.ts +3 -0
  103. package/dist/doctor/run.js +3 -0
  104. package/dist/doctor/types.d.ts +10 -2
  105. package/dist/doctor/types.js +6 -0
  106. package/dist/doctor/wrangler-config.d.ts +7 -2
  107. package/dist/doctor/wrangler-config.js +3 -0
  108. package/dist/email.d.ts +4 -2
  109. package/dist/env.d.ts +0 -3
  110. package/dist/env.js +0 -3
  111. package/dist/github/branches.d.ts +4 -2
  112. package/dist/github/branches.js +4 -2
  113. package/dist/github/signing.d.ts +1 -1
  114. package/dist/github/signing.js +2 -2
  115. package/dist/log/events.d.ts +1 -1
  116. package/dist/media/bulk-delete-plan.d.ts +8 -4
  117. package/dist/media/config.d.ts +12 -6
  118. package/dist/media/config.js +16 -8
  119. package/dist/media/delivery-bucket.d.ts +4 -2
  120. package/dist/media/library-entry.d.ts +4 -2
  121. package/dist/media/library-entry.js +4 -2
  122. package/dist/media/manifest.d.ts +29 -15
  123. package/dist/media/manifest.js +29 -16
  124. package/dist/media/naming.d.ts +12 -6
  125. package/dist/media/naming.js +24 -12
  126. package/dist/media/orphan-scan.d.ts +4 -2
  127. package/dist/media/reconcile.d.ts +21 -11
  128. package/dist/media/reconcile.js +12 -6
  129. package/dist/media/reference.d.ts +8 -4
  130. package/dist/media/reference.js +12 -6
  131. package/dist/media/rewrite-plan.d.ts +12 -6
  132. package/dist/media/sniff.d.ts +4 -2
  133. package/dist/media/sniff.js +28 -14
  134. package/dist/media/store.d.ts +16 -8
  135. package/dist/media/store.js +4 -2
  136. package/dist/media/transform-url.d.ts +12 -6
  137. package/dist/media/transform-url.js +8 -4
  138. package/dist/media/usage.d.ts +8 -4
  139. package/dist/nav/site-config.d.ts +16 -8
  140. package/dist/render/component-grammar.d.ts +23 -10
  141. package/dist/render/component-grammar.js +19 -8
  142. package/dist/render/component-insert.d.ts +8 -4
  143. package/dist/render/component-insert.js +4 -2
  144. package/dist/render/component-reference.d.ts +4 -2
  145. package/dist/render/component-reference.js +4 -2
  146. package/dist/render/component-validate.d.ts +3 -0
  147. package/dist/render/component-validate.js +3 -0
  148. package/dist/render/glyph.d.ts +4 -2
  149. package/dist/render/glyph.js +4 -2
  150. package/dist/render/pipeline.d.ts +20 -10
  151. package/dist/render/pipeline.js +4 -2
  152. package/dist/render/registry.d.ts +40 -20
  153. package/dist/render/registry.js +16 -8
  154. package/dist/render/rehype-dispatch.d.ts +22 -8
  155. package/dist/render/rehype-dispatch.js +22 -8
  156. package/dist/render/remark-directives.d.ts +3 -0
  157. package/dist/render/remark-directives.js +3 -0
  158. package/dist/render/remark-figure.d.ts +4 -2
  159. package/dist/render/remark-figure.js +4 -2
  160. package/dist/render/resolve-links.d.ts +4 -2
  161. package/dist/render/resolve-links.js +4 -2
  162. package/dist/render/resolve-media.d.ts +16 -8
  163. package/dist/render/resolve-media.js +12 -6
  164. package/dist/sveltekit/admin-dispatch.d.ts +2 -0
  165. package/dist/sveltekit/admin-dispatch.js +9 -3
  166. package/dist/sveltekit/auth-routes.d.ts +3 -0
  167. package/dist/sveltekit/auth-routes.js +3 -0
  168. package/dist/sveltekit/cairn-admin.d.ts +16 -5
  169. package/dist/sveltekit/cairn-admin.js +26 -10
  170. package/dist/sveltekit/content-routes.d.ts +191 -86
  171. package/dist/sveltekit/content-routes.js +295 -107
  172. package/dist/sveltekit/editors-routes.d.ts +3 -0
  173. package/dist/sveltekit/editors-routes.js +3 -0
  174. package/dist/sveltekit/guard.d.ts +4 -2
  175. package/dist/sveltekit/guard.js +4 -2
  176. package/dist/sveltekit/https-required-page.d.ts +1 -1
  177. package/dist/sveltekit/https-required-page.js +1 -1
  178. package/dist/sveltekit/index.d.ts +1 -1
  179. package/dist/sveltekit/media-route.d.ts +1 -2
  180. package/dist/sveltekit/media-route.js +13 -8
  181. package/dist/sveltekit/nav-routes.d.ts +7 -2
  182. package/dist/sveltekit/nav-routes.js +3 -0
  183. package/dist/sveltekit/types.d.ts +4 -2
  184. package/dist/vite/index.d.ts +32 -16
  185. package/dist/vite/index.js +52 -26
  186. package/dist/vite/resolve-root.d.ts +8 -4
  187. package/dist/vite/resolve-root.js +4 -2
  188. package/package.json +8 -2
  189. package/src/lib/components/AdminLayout.svelte +22 -0
  190. package/src/lib/components/CairnAdmin.svelte +3 -0
  191. package/src/lib/components/CairnTidySettings.svelte +2 -2
  192. package/src/lib/components/ComponentForm.svelte +0 -1
  193. package/src/lib/components/EditPage.svelte +133 -41
  194. package/src/lib/components/HelpHome.svelte +850 -0
  195. package/src/lib/components/MarkdownHelpDialog.svelte +4 -15
  196. package/src/lib/components/client-ingest.ts +20 -10
  197. package/src/lib/components/editor-media.ts +20 -10
  198. package/src/lib/components/editor-placeholder.ts +12 -6
  199. package/src/lib/components/editor-tidy.ts +28 -14
  200. package/src/lib/components/index.ts +1 -0
  201. package/src/lib/components/link-completion.ts +12 -6
  202. package/src/lib/components/markdown-directives.ts +13 -8
  203. package/src/lib/components/markdown-format.ts +63 -30
  204. package/src/lib/components/markdown-reference.ts +30 -0
  205. package/src/lib/components/media-upload-outcome.ts +12 -6
  206. package/src/lib/components/objective-errors.ts +16 -8
  207. package/src/lib/components/preview-doc.ts +4 -2
  208. package/src/lib/components/spellcheck.ts +92 -40
  209. package/src/lib/components/tidy-categorize.ts +28 -14
  210. package/src/lib/components/tidy-validate.ts +28 -14
  211. package/src/lib/components/topbar-context.ts +4 -2
  212. package/src/lib/content/advisories.ts +141 -0
  213. package/src/lib/content/compose.ts +5 -2
  214. package/src/lib/content/excerpt.ts +4 -2
  215. package/src/lib/content/getting-started.ts +31 -0
  216. package/src/lib/content/links.ts +16 -8
  217. package/src/lib/content/manifest.ts +36 -18
  218. package/src/lib/content/media-refs.ts +4 -2
  219. package/src/lib/content/media-rewrite.ts +100 -50
  220. package/src/lib/content/schema.ts +20 -10
  221. package/src/lib/content/site-dictionary.ts +8 -4
  222. package/src/lib/content/types.ts +97 -42
  223. package/src/lib/delivery/content-index.ts +16 -8
  224. package/src/lib/delivery/feeds.ts +4 -2
  225. package/src/lib/delivery/json-ld.ts +3 -0
  226. package/src/lib/delivery/manifest.ts +4 -2
  227. package/src/lib/delivery/public-routes.ts +16 -8
  228. package/src/lib/delivery/seo-fields.ts +12 -6
  229. package/src/lib/delivery/site-indexes.ts +4 -2
  230. package/src/lib/delivery/site-resolver.ts +4 -2
  231. package/src/lib/doctor/cloudflare-api.ts +6 -0
  232. package/src/lib/doctor/index.ts +12 -6
  233. package/src/lib/doctor/report.ts +3 -0
  234. package/src/lib/doctor/run.ts +3 -0
  235. package/src/lib/doctor/types.ts +10 -2
  236. package/src/lib/doctor/wrangler-config.ts +7 -2
  237. package/src/lib/email.ts +4 -2
  238. package/src/lib/env.ts +0 -3
  239. package/src/lib/github/branches.ts +4 -2
  240. package/src/lib/github/signing.ts +2 -2
  241. package/src/lib/log/events.ts +1 -0
  242. package/src/lib/media/bulk-delete-plan.ts +8 -4
  243. package/src/lib/media/config.ts +24 -12
  244. package/src/lib/media/delivery-bucket.ts +4 -2
  245. package/src/lib/media/library-entry.ts +4 -2
  246. package/src/lib/media/manifest.ts +33 -18
  247. package/src/lib/media/naming.ts +24 -12
  248. package/src/lib/media/orphan-scan.ts +4 -2
  249. package/src/lib/media/reconcile.ts +21 -11
  250. package/src/lib/media/reference.ts +12 -6
  251. package/src/lib/media/rewrite-plan.ts +12 -6
  252. package/src/lib/media/sniff.ts +28 -14
  253. package/src/lib/media/store.ts +16 -8
  254. package/src/lib/media/transform-url.ts +12 -6
  255. package/src/lib/media/usage.ts +8 -4
  256. package/src/lib/nav/site-config.ts +16 -8
  257. package/src/lib/render/component-grammar.ts +23 -10
  258. package/src/lib/render/component-insert.ts +8 -4
  259. package/src/lib/render/component-reference.ts +4 -2
  260. package/src/lib/render/component-validate.ts +3 -0
  261. package/src/lib/render/glyph.ts +4 -2
  262. package/src/lib/render/pipeline.ts +20 -10
  263. package/src/lib/render/registry.ts +44 -22
  264. package/src/lib/render/rehype-dispatch.ts +22 -8
  265. package/src/lib/render/remark-directives.ts +3 -0
  266. package/src/lib/render/remark-figure.ts +4 -2
  267. package/src/lib/render/resolve-links.ts +4 -2
  268. package/src/lib/render/resolve-media.ts +16 -8
  269. package/src/lib/sveltekit/admin-dispatch.ts +10 -4
  270. package/src/lib/sveltekit/auth-routes.ts +3 -0
  271. package/src/lib/sveltekit/cairn-admin.ts +37 -15
  272. package/src/lib/sveltekit/content-routes.ts +492 -197
  273. package/src/lib/sveltekit/editors-routes.ts +3 -0
  274. package/src/lib/sveltekit/guard.ts +4 -2
  275. package/src/lib/sveltekit/https-required-page.ts +1 -1
  276. package/src/lib/sveltekit/index.ts +3 -0
  277. package/src/lib/sveltekit/media-route.ts +13 -8
  278. package/src/lib/sveltekit/nav-routes.ts +7 -2
  279. package/src/lib/sveltekit/types.ts +4 -2
  280. package/src/lib/vite/index.ts +60 -30
  281. package/src/lib/vite/resolve-root.ts +8 -4
@@ -22,298 +22,171 @@ editor.focusEditor() (the selection is intact, since opening only blurred the ed
22
22
  narrow breakpoint it falls back to a full-height bottom sheet (the admin design system's modal-sizing
23
23
  rule). The CSRF token is read from the admin context.
24
24
  -->
25
- <script lang="ts">
26
- import { getContext, tick } from 'svelte';
27
- import { CSRF_CONTEXT_KEY } from './csrf-context.js';
28
- import MediaCaptureCard from './MediaCaptureCard.svelte';
29
- import MediaPicker, { type MediaLibraryEntry, type MediaSelection } from './MediaPicker.svelte';
30
- import {
31
- ingestFile,
32
- buildUploadRequest,
33
- sendUpload,
34
- ingestFailureKind,
35
- failureCard,
36
- type IngestFailureCard,
37
- } from './client-ingest.js';
38
- import { deserialize } from '$app/forms';
39
- import { uploadOutcome, type UploadEnvelope, type UploadFailureKind } from './media-upload-outcome.js';
40
- import type { MediaEntry } from '../media/manifest.js';
41
-
42
- // The placeholder api type is referenced inline (import('...').Type), never a static
43
- // `import type ... from`, so no static edge to the dynamically-imported editor-placeholder module
44
- // sits in this client component (the editor-boundary test bars that edge by a textual `from` scan).
45
- type ImagePlaceholderApi = import('./editor-placeholder.js').ImagePlaceholderApi;
46
-
47
- /** The record the capture card emits, the same shape MediaCaptureCard.oncapture carries. */
48
- interface CaptureRecord {
49
- file: File;
50
- displayName: string;
51
- alt: string;
52
- decorative: boolean;
25
+ <script lang="ts">import { getContext, tick } from "svelte";
26
+ import { CSRF_CONTEXT_KEY } from "./csrf-context.js";
27
+ import MediaCaptureCard from "./MediaCaptureCard.svelte";
28
+ import MediaPicker, {} from "./MediaPicker.svelte";
29
+ import {
30
+ ingestFile,
31
+ buildUploadRequest,
32
+ sendUpload,
33
+ ingestFailureKind,
34
+ failureCard
35
+ } from "./client-ingest.js";
36
+ import { deserialize } from "$app/forms";
37
+ import { uploadOutcome } from "./media-upload-outcome.js";
38
+ let { conceptId, id, library, editor, onuploaded, trigger = false } = $props();
39
+ const csrf = getContext(CSRF_CONTEXT_KEY);
40
+ let view = $state(null);
41
+ let captureFile = $state(null);
42
+ let status = $state({ kind: "idle" });
43
+ let anchor = $state(null);
44
+ let panel = $state(null);
45
+ let fileInput = $state(null);
46
+ let retryButton = $state(null);
47
+ let expiredCloseButton = $state(null);
48
+ let reusedDoneButton = $state(null);
49
+ $effect(() => {
50
+ const kind = status.kind;
51
+ if (kind !== "failed" && kind !== "expired" && kind !== "reused") return;
52
+ void tick().then(() => {
53
+ if (kind === "failed") retryButton?.focus();
54
+ else if (kind === "expired") expiredCloseButton?.focus();
55
+ else reusedDoneButton?.focus();
56
+ });
57
+ });
58
+ export function open(signal, file) {
59
+ anchor = editor.caretCoords();
60
+ status = { kind: "idle" };
61
+ if (signal === "capture" && file) {
62
+ captureFile = file;
63
+ view = "capture";
64
+ } else {
65
+ captureFile = null;
66
+ view = "chooser";
53
67
  }
54
-
55
- interface Props {
56
- /** The concept the entry belongs to (the upload action's route param). */
57
- conceptId: string;
58
- /** The entry id (the upload action's route param). */
59
- id: string;
60
- /** The merged committed-plus-uploaded media library, keyed by content hash, for the picker. */
61
- library: Record<string, MediaLibraryEntry>;
62
- /** The editor seams the popover drives: caret anchoring, focus restore, the placeholder api, and
63
- * the direct-insert path for a picked image (no upload). */
64
- editor: {
65
- caretCoords: () => { left: number; right: number; top: number; bottom: number } | null;
66
- focusEditor: () => void;
67
- placeholders: ImagePlaceholderApi;
68
- insertImage: (alt: string, ref: string) => void;
69
- };
70
- /** Called with the server-owned record on a successful upload; the host appends it to its records
71
- * state and merges it into the library so the source decoration resolves the new reference. */
72
- onuploaded: (record: MediaEntry) => void;
73
- /** Render the built-in trigger button. False (the default) mounts headless; the host opens the
74
- * popover through the exported open(). */
75
- trigger?: boolean;
68
+ void tick().then(() => panel?.focus());
69
+ }
70
+ function close() {
71
+ view = null;
72
+ captureFile = null;
73
+ status = { kind: "idle" };
74
+ editor.focusEditor();
75
+ }
76
+ function onKeydown(e) {
77
+ if (e.key === "Escape") {
78
+ e.preventDefault();
79
+ close();
80
+ return;
76
81
  }
77
-
78
- let { conceptId, id, library, editor, onuploaded, trigger = false }: Props = $props();
79
-
80
- // The CSRF token getter from the admin context (AdminLayout provides it). Undefined outside the
81
- // shell, where the empty token fails the guard's check, the intended fail-closed signal.
82
- const csrf = getContext<(() => string) | undefined>(CSRF_CONTEXT_KEY);
83
-
84
- // The view the popover shows. 'capture' is the one-step card with bytes in hand; 'chooser' leads
85
- // with upload and the picker; null is closed.
86
- type View = 'capture' | 'chooser';
87
- let view = $state<View | null>(null);
88
- let captureFile = $state<File | null>(null);
89
-
90
- // The card a failed loop surfaces: an ingest-taxonomy card (its own message) or the envelope-only
91
- // generic card. The two share the failed-card shape, so one alias names both.
92
- type FailureCard = IngestFailureCard | { status: 'failed'; kind: UploadFailureKind; message: string };
93
-
94
- // The transient status of the in-flight or failed loop, surfaced under the active view. 'reused'
95
- // briefly notes a dedup collapse; the failure and expired states carry a message and a retry.
96
- type Status =
97
- | { kind: 'idle' }
98
- | { kind: 'reused' }
99
- | { kind: 'failed'; card: FailureCard; retry: () => void }
100
- | { kind: 'expired' };
101
- let status = $state<Status>({ kind: 'idle' });
102
-
103
- // The anchor coordinates captured on open, so the popover positions at the caret even after focus
104
- // leaves the editor. Null falls back to a centered position.
105
- let anchor = $state<{ left: number; right: number; top: number; bottom: number } | null>(null);
106
- let panel = $state<HTMLDivElement | null>(null);
107
- let fileInput = $state<HTMLInputElement | null>(null);
108
- // The primary control of each terminal status block, bound so focus lands on the action when the
109
- // block renders (the focused capture-card submit unmounts when the loop starts, so focus would
110
- // otherwise drop to <body>).
111
- let retryButton = $state<HTMLButtonElement | null>(null);
112
- let expiredCloseButton = $state<HTMLButtonElement | null>(null);
113
- let reusedDoneButton = $state<HTMLButtonElement | null>(null);
114
-
115
- // When the loop settles into a terminal state, move focus to that state's primary control (Retry,
116
- // Close, or Done) so the keyboard/screen-reader user lands on the action and the Tab trap plus
117
- // Escape stay in play. The message blocks carry role="alert"/role="status" so the transition is
118
- // also announced (WCAG 4.1.3, 2.4.3). Keyed on status.kind, after the block renders.
119
- $effect(() => {
120
- const kind = status.kind;
121
- if (kind !== 'failed' && kind !== 'expired' && kind !== 'reused') return;
122
- void tick().then(() => {
123
- if (kind === 'failed') retryButton?.focus();
124
- else if (kind === 'expired') expiredCloseButton?.focus();
125
- else reusedDoneButton?.focus();
126
- });
82
+ if (e.key !== "Tab" || !panel) return;
83
+ const focusable = panel.querySelectorAll(
84
+ 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])'
85
+ );
86
+ if (focusable.length === 0) return;
87
+ const first = focusable[0];
88
+ const last = focusable[focusable.length - 1];
89
+ const activeEl = document.activeElement;
90
+ if (e.shiftKey && (activeEl === first || activeEl === panel)) {
91
+ e.preventDefault();
92
+ last.focus();
93
+ } else if (!e.shiftKey && activeEl === last) {
94
+ e.preventDefault();
95
+ first.focus();
96
+ }
97
+ }
98
+ function onPick(sel) {
99
+ editor.insertImage(sel.alt, sel.ref);
100
+ close();
101
+ }
102
+ function onChosenFile(e) {
103
+ const input = e.currentTarget;
104
+ const file = input.files?.[0];
105
+ if (file) {
106
+ captureFile = file;
107
+ view = "capture";
108
+ status = { kind: "idle" };
109
+ }
110
+ }
111
+ async function runUpload(record) {
112
+ const objectUrl = URL.createObjectURL(record.file);
113
+ const pid = editor.placeholders.begin(objectUrl);
114
+ view = null;
115
+ captureFile = null;
116
+ const discardPlaceholder = () => {
117
+ editor.placeholders.cancel(pid);
118
+ URL.revokeObjectURL(objectUrl);
119
+ };
120
+ const fail = (card) => {
121
+ discardPlaceholder();
122
+ status = { kind: "failed", card, retry: () => void runUpload(record) };
123
+ };
124
+ const expire = () => {
125
+ discardPlaceholder();
126
+ status = { kind: "expired" };
127
+ };
128
+ const genericCard = () => fail({ status: "failed", kind: "generic", message: GENERIC_FAILURE_MESSAGE });
129
+ editor.placeholders.progress(pid, 0.4);
130
+ let ingested;
131
+ try {
132
+ ingested = await ingestFile(record.file);
133
+ } catch (err) {
134
+ fail(failureCard(ingestFailureKind(err)));
135
+ return;
136
+ }
137
+ editor.placeholders.progress(pid, 0.85);
138
+ const { url, init } = buildUploadRequest({
139
+ conceptId,
140
+ id,
141
+ bytes: ingested.blob,
142
+ contentType: ingested.contentType,
143
+ csrf: csrf?.() ?? "",
144
+ filename: record.file.name,
145
+ alt: record.alt,
146
+ displayName: record.displayName,
147
+ width: ingested.width,
148
+ height: ingested.height
127
149
  });
128
-
129
- /**
130
- * Open the popover. 'capture' with a file goes straight to the capture card (paste/drag); 'chooser'
131
- * leads with the upload zone and the picker (the toolbar button). Anchors to the caret and moves
132
- * focus in.
133
- */
134
- export function open(signal: 'chooser' | 'capture', file?: File): void {
135
- anchor = editor.caretCoords();
136
- status = { kind: 'idle' };
137
- if (signal === 'capture' && file) {
138
- captureFile = file;
139
- view = 'capture';
140
- } else {
141
- captureFile = null;
142
- view = 'chooser';
143
- }
144
- // Move focus into the panel once it renders.
145
- void tick().then(() => panel?.focus());
150
+ let res;
151
+ try {
152
+ res = await sendUpload(url, init);
153
+ } catch (err) {
154
+ fail(failureCard(ingestFailureKind(err)));
155
+ return;
146
156
  }
147
-
148
- function close() {
149
- view = null;
150
- captureFile = null;
151
- status = { kind: 'idle' };
152
- editor.focusEditor();
157
+ if (res.type === "opaqueredirect" || res.status === 0) {
158
+ expire();
159
+ return;
153
160
  }
154
-
155
- // Trap Tab within the panel and close on Escape, restoring focus to the editor.
156
- function onKeydown(e: KeyboardEvent) {
157
- if (e.key === 'Escape') {
158
- e.preventDefault();
159
- close();
160
- return;
161
- }
162
- if (e.key !== 'Tab' || !panel) return;
163
- const focusable = panel.querySelectorAll<HTMLElement>(
164
- 'a[href], button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])',
165
- );
166
- if (focusable.length === 0) return;
167
- const first = focusable[0];
168
- const last = focusable[focusable.length - 1];
169
- const activeEl = document.activeElement;
170
- if (e.shiftKey && (activeEl === first || activeEl === panel)) {
171
- e.preventDefault();
172
- last.focus();
173
- } else if (!e.shiftKey && activeEl === last) {
174
- e.preventDefault();
175
- first.focus();
176
- }
161
+ let outcome;
162
+ try {
163
+ outcome = uploadOutcome(deserialize(await res.text()));
164
+ } catch {
165
+ genericCard();
166
+ return;
177
167
  }
178
-
179
- // The picker path: a picked asset inserts its reference directly at the caret with no upload, then
180
- // the popover closes. This is the reuse-an-existing path.
181
- function onPick(sel: MediaSelection) {
182
- editor.insertImage(sel.alt, sel.ref);
183
- close();
168
+ if (outcome.kind === "session-expired") {
169
+ expire();
170
+ return;
184
171
  }
185
-
186
- // A chosen file (the choose-file fallback in the chooser) routes to the capture card, the same one
187
- // a paste or drag opens, so every byte path runs the one capture-then-upload flow.
188
- function onChosenFile(e: Event) {
189
- const input = e.currentTarget as HTMLInputElement;
190
- const file = input.files?.[0];
191
- if (file) {
192
- captureFile = file;
193
- view = 'capture';
194
- status = { kind: 'idle' };
195
- }
172
+ if (outcome.kind === "failed") {
173
+ if (outcome.failure === "generic") genericCard();
174
+ else fail(failureCard(outcome.failure));
175
+ return;
196
176
  }
197
-
198
- // The optimistic upload loop, on a capture-card record. It lands a placeholder, runs the ingest and
199
- // upload, and resolves the placeholder to the committed reference or cancels it on any failure, so
200
- // the source is never left with a half-written token.
201
- async function runUpload(record: CaptureRecord) {
202
- const objectUrl = URL.createObjectURL(record.file);
203
- const pid = editor.placeholders.begin(objectUrl);
204
- // Close the byte-capture view now; the placeholder carries the progress in the source.
205
- view = null;
206
- captureFile = null;
207
-
208
- // Drop the in-flight placeholder, leaving the source exactly as it was, and free the object URL.
209
- // Both unsuccessful paths (a typed failure, an expired session) end here before setting status.
210
- const discardPlaceholder = () => {
211
- editor.placeholders.cancel(pid);
212
- URL.revokeObjectURL(objectUrl);
213
- };
214
- const fail = (card: FailureCard) => {
215
- discardPlaceholder();
216
- status = { kind: 'failed', card, retry: () => void runUpload(record) };
217
- };
218
- const expire = () => {
219
- discardPlaceholder();
220
- status = { kind: 'expired' };
221
- };
222
- // The author-facing card for an envelope-only generic refusal (a deserialize throw or an
223
- // operational reason with no author-actionable specifics).
224
- const genericCard = () => fail({ status: 'failed', kind: 'generic', message: GENERIC_FAILURE_MESSAGE });
225
-
226
- // Stage progress, not real byte counts: fetch cannot report upload bytes, so the bar reads the
227
- // ingest/upload LIFECYCLE (begin ~0.1 set by the field, ingesting ~0.4, uploading ~0.85, resolve
228
- // clears it). Honest stage progress, never a fabricated timer.
229
- editor.placeholders.progress(pid, 0.4);
230
- let ingested: Awaited<ReturnType<typeof ingestFile>>;
231
- try {
232
- ingested = await ingestFile(record.file);
233
- } catch (err) {
234
- fail(failureCard(ingestFailureKind(err)));
235
- return;
236
- }
237
-
238
- editor.placeholders.progress(pid, 0.85);
239
- const { url, init } = buildUploadRequest({
240
- conceptId,
241
- id,
242
- bytes: ingested.blob,
243
- contentType: ingested.contentType,
244
- csrf: csrf?.() ?? '',
245
- filename: record.file.name,
246
- alt: record.alt,
247
- displayName: record.displayName,
248
- width: ingested.width,
249
- height: ingested.height,
250
- });
251
-
252
- let res: Response;
253
- try {
254
- res = await sendUpload(url, init);
255
- } catch (err) {
256
- fail(failureCard(ingestFailureKind(err)));
257
- return;
258
- }
259
-
260
- // The guard's expired-session 303 under redirect: 'manual' surfaces as an opaque, status-0
261
- // response; treat it as session-expired before parsing a body that is not there.
262
- if (res.type === 'opaqueredirect' || res.status === 0) {
263
- expire();
264
- return;
265
- }
266
-
267
- // deserialize returns the generic ActionResult; the upload action's success data is an
268
- // UploadResult and its failure data carries an error string, so the result matches UploadEnvelope.
269
- // The cast names that known shape for the pure mapper (the redirect/status-0 case is handled above).
270
- // An unexpected server response (a 500/502/504 from a worker crash, OOM, or an edge timeout) is an
271
- // HTML error page, not a devalue-encoded result, so deserialize throws. Catch it and route the
272
- // throw through fail() with the generic card, so the placeholder cancels and a Retry is offered.
273
- let outcome: ReturnType<typeof uploadOutcome>;
274
- try {
275
- outcome = uploadOutcome(deserialize(await res.text()) as UploadEnvelope);
276
- } catch {
277
- genericCard();
278
- return;
279
- }
280
- if (outcome.kind === 'session-expired') {
281
- expire();
282
- return;
283
- }
284
- if (outcome.kind === 'failed') {
285
- // An ingest-taxonomy kind reuses failureCard's own message; the envelope-only `generic` kind
286
- // carries its own plain message. Either way the card shows the message with a Retry.
287
- if (outcome.failure === 'generic') genericCard();
288
- else fail(failureCard(outcome.failure));
289
- return;
290
- }
291
-
292
- // Success: swap the placeholder for the committed reference in one transaction, hand the record
293
- // up, and close. A dedup reuse still inserts the existing reference (the decision) and briefly
294
- // notes it.
295
- editor.placeholders.resolveTo(pid, record.alt, outcome.reference);
296
- onuploaded(outcome.record);
297
- URL.revokeObjectURL(objectUrl);
298
- if (outcome.reused) {
299
- status = { kind: 'reused' };
300
- } else {
301
- close();
302
- }
177
+ editor.placeholders.resolveTo(pid, record.alt, outcome.reference);
178
+ onuploaded(outcome.record);
179
+ URL.revokeObjectURL(objectUrl);
180
+ if (outcome.reused) {
181
+ status = { kind: "reused" };
182
+ } else {
183
+ close();
303
184
  }
304
-
305
- // The author-facing message for an envelope-only generic refusal (a binding-missing, a csrf, a
306
- // length-required: operational refusals with no author-actionable specifics). The ingest-taxonomy
307
- // kinds carry their own messages through failureCard.
308
- const GENERIC_FAILURE_MESSAGE = 'The upload could not be completed. Please try again.';
309
-
310
- // The popover's anchored position: just below the caret line, clamped into the viewport. A null
311
- // anchor centers it. The full-height sheet at the narrow breakpoint is the CSS fallback.
312
- const positionStyle = $derived(
313
- anchor
314
- ? `left: ${Math.max(8, Math.min(anchor.left, (typeof window !== 'undefined' ? window.innerWidth : 1024) - 360))}px; top: ${anchor.bottom + 6}px;`
315
- : 'left: 50%; top: 4rem; transform: translateX(-50%);',
316
- );
185
+ }
186
+ const GENERIC_FAILURE_MESSAGE = "The upload could not be completed. Please try again.";
187
+ const positionStyle = $derived(
188
+ anchor ? `left: ${Math.max(8, Math.min(anchor.left, (typeof window !== "undefined" ? window.innerWidth : 1024) - 360))}px; top: ${anchor.bottom + 6}px;` : "left: 50%; top: 4rem; transform: translateX(-50%);"
189
+ );
317
190
  </script>
318
191
 
319
192
  {#if trigger}