@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
@@ -17,256 +17,148 @@ the model made and never a count of the author's own usage. A normalization name
17
17
  setting that authorized it; counting the author's own habit is the harmonize-to-author judgment cairn
18
18
  must never make, so no such count exists.
19
19
  -->
20
- <script lang="ts">
21
- import SparklesIcon from '@lucide/svelte/icons/sparkles';
22
- import CheckIcon from '@lucide/svelte/icons/check';
23
- import XIcon from '@lucide/svelte/icons/x';
24
- import TriangleAlertIcon from '@lucide/svelte/icons/triangle-alert';
25
- import LightbulbIcon from '@lucide/svelte/icons/lightbulb';
26
- import EyeIcon from '@lucide/svelte/icons/eye';
27
- import type { Change } from './tidy-diff.js';
28
- import { lineLabel } from './tidy-diff.js';
29
- import {
30
- categorize,
31
- isObjective,
32
- buildBecause,
33
- categoryLabel,
34
- type TidyCategory,
35
- } from './tidy-categorize.js';
36
- import type { TidyConventions } from '../nav/site-config.js';
37
-
38
- interface Props {
39
- /** The validated change set (Task 13 output), the unit the surface accepts and rejects. */
40
- changes: Change[];
41
- /** The captured original the diff was computed against; the source of every line label and the
42
- * before/after rows. Positions index this string. */
43
- original: string;
44
- /** The resolved tidy conventions, the ONLY data source for a normalization's because-line and the
45
- * category inference. Never the buffer's usage. */
46
- conventions: TidyConventions;
47
- /** The model that produced the result, for the head pill (e.g. "claude-sonnet-4-6"). */
48
- model: string;
49
- /** The document's display title, for the head. */
50
- title: string;
51
- /** The apply seam from MarkdownEditor: the surface drives the in-buffer decorations and the batched
52
- * apply through it. Typed with an inline `import(...)` so no static editor-module edge sits in this
53
- * component (the editor-boundary test bars that edge by a textual scan). */
54
- api: import('./editor-tidy.js').TidyApi;
55
- /** Called when the review closes (apply or cancel), so the host clears tidy mode and re-enables the
56
- * editor. `applied` is true when the author applied changes, false on cancel/reject-all. */
57
- onclose: (applied: boolean) => void;
58
- /** Called to scroll the editor underneath to a hunk's source line; the host drives the editor's
59
- * selectRange seam. */
60
- onshow: (from: number, to: number) => void;
61
- }
62
-
63
- let { changes, original, conventions, model, title, api, onclose, onshow }: Props = $props();
64
-
65
- // One hunk per change, with its locally-inferred category, line label, diff rows, and because-line.
66
- // Computed once from the immutable inputs; the disposition lives in its own reactive array so the
67
- // rows do not recompute on every toggle.
68
- interface Hunk {
69
- index: number;
70
- category: TidyCategory;
71
- objective: boolean;
72
- line: number;
73
- contextBefore: string;
74
- contextAfter: string;
75
- delText: string;
76
- addText: string;
77
- delRun: { pre: string; mid: string; post: string };
78
- addRun: { pre: string; mid: string; post: string };
79
- because: ReturnType<typeof buildBecause>;
80
- label: string;
81
- }
82
-
83
- const hunks: Hunk[] = $derived(changes.map((c) => {
84
- const category = categorize(c, original, conventions);
85
- const objective = isObjective(category);
86
- const removed = original.slice(c.from, c.to);
87
- const added = c.replacement;
88
- // The line containing the change, and the one line of context above and below it (graft 4).
89
- const line = lineLabel(original, c.from);
90
- const lines = original.split('\n');
91
- const contextBefore = line >= 2 ? lines[line - 2] ?? '' : '';
92
- const contextAfter = line < lines.length ? lines[line] ?? '' : '';
93
- // The changed line, split around the changed run so the diff can underline/strike just the run.
94
- const lineStart = original.lastIndexOf('\n', c.from - 1) + 1;
95
- const nextNewline = original.indexOf('\n', c.from);
96
- const lineEnd = nextNewline === -1 ? original.length : nextNewline;
97
- const fullLine = original.slice(lineStart, lineEnd);
98
- const pre = original.slice(lineStart, c.from);
99
- const post = original.slice(c.to, lineEnd);
100
- const because =
101
- category.kind === 'normalization' ? buildBecause(category.convention, conventions) : null;
102
- return {
103
- index: c.index,
104
- category,
105
- objective,
106
- line,
107
- contextBefore,
108
- contextAfter,
109
- delText: fullLine,
110
- addText: pre + added + post,
111
- delRun: { pre, mid: removed, post },
112
- addRun: { pre, mid: added, post },
113
- because,
114
- label: categoryLabel(category),
115
- };
116
- }));
117
-
118
- // The per-hunk disposition. Objective hunks open pre-kept; judgment hunks open undecided. The defaults
119
- // come from the hunks (their safety rank); the author's per-hunk and bulk choices land in `overrides`,
120
- // and `dispositions` is the merged effective map keyed by the stable change index. Splitting the two
121
- // keeps the default reactive to the derived hunks without capturing only their initial value.
122
- type Disposition = 'kept' | 'rejected' | 'undecided';
123
- let overrides = $state<Record<number, Disposition>>({});
124
-
125
- // The disposition a hunk takes under a given override map: the author's choice if present, else the
126
- // safety-rank default (objective hunks pre-kept, judgment hunks undecided). One source for the default.
127
- function effectiveDisposition(h: Hunk, map: Record<number, Disposition>): Disposition {
128
- return map[h.index] ?? (h.objective ? 'kept' : 'undecided');
129
- }
130
-
131
- const dispositions = $derived<Record<number, Disposition>>(
132
- Object.fromEntries(hunks.map((h) => [h.index, effectiveDisposition(h, overrides)] as const)),
133
- );
134
-
135
- // The keyboard step-through cursor: the focused hunk's array position. j/k move; a/r act on it.
136
- let focusedPos = $state(0);
137
-
138
- // The two live regions (the MediaPicker discipline). The tally region (role=status) speaks only on a
139
- // bulk action; the action region (aria-live=polite) narrates the single per-hunk action and each
140
- // cursor move. A live region re-announces only when its text changes, so a deterministic message
141
- // (the same hunk, the same verb) would go silent on a repeat. Each writer appends an invisible
142
- // incrementing nonce so the region text always mutates and the screen reader always speaks it.
143
- let tallyMessage = $state('');
144
- let actionMessage = $state('');
145
- let announceNonce = 0;
146
-
147
- // An invisible suffix that flips on every call, so a repeated identical announcement still changes
148
- // the region text and re-fires the live region. It is a zero-width space, never voiced, so the heard
149
- // sentence is unchanged. Each region keeps its own parity through the shared counter.
150
- function nonce(): string {
151
- return announceNonce++ % 2 === 0 ? '' : '​';
152
- }
153
-
154
- const keptCount = $derived(hunks.filter((h) => dispositions[h.index] === 'kept').length);
155
- const reviewCount = $derived(hunks.filter((h) => dispositions[h.index] === 'undecided').length);
156
- const skipCount = $derived(hunks.filter((h) => dispositions[h.index] === 'rejected').length);
157
-
158
- let dialog = $state<HTMLDialogElement | null>(null);
159
-
160
- $effect(() => {
161
- // Open the dialog once on mount; showModal supplies the focus trap and Escape.
162
- dialog?.showModal();
163
- });
164
-
165
- function setDisposition(index: number, next: Disposition) {
166
- overrides = { ...overrides, [index]: next };
167
- }
168
-
169
- // Narrate one hunk in the polite region. The verb says what just happened to it ("Kept", "Skipped",
170
- // or "Focused" as the cursor lands on it). The sentence carries the kind and the before/after text,
171
- // and for a normalization appends the config-named rationale (never a usage count). The trailing
172
- // nonce keeps a repeated identical action audible.
173
- function narrate(h: Hunk, verb: string) {
174
- const where = `Hunk ${hunks.indexOf(h) + 1} of ${hunks.length}`;
175
- const what = h.delRun.mid && h.addRun.mid ? `${h.delRun.mid.trim()} becomes ${h.addRun.mid.trim()}` : h.label;
176
- const why = h.because ? `, your ${h.because.label} setting is ${h.because.variant}` : '';
177
- actionMessage = `${where}. ${h.label}. ${what}${why}. ${verb}.${nonce()}`;
178
- }
179
-
180
- function acceptHunk(h: Hunk) {
181
- setDisposition(h.index, 'kept');
182
- narrate(h, 'Kept');
183
- }
184
- function rejectHunk(h: Hunk) {
185
- setDisposition(h.index, 'rejected');
186
- narrate(h, 'Skipped');
187
- }
188
-
189
- // Accept fixes (the bulk action): mark EVERY OBJECTIVE hunk kept and nothing else. A judgment hunk is
190
- // never touched here, so it stays undecided and is never swept. The tally region announces the result.
191
- function acceptFixes() {
192
- const next = { ...overrides };
193
- for (const h of hunks) if (h.objective) next[h.index] = 'kept';
194
- overrides = next;
195
- const n = hunks.filter((h) => h.objective).length;
196
- const stillReview = hunks.filter((h) => effectiveDisposition(h, next) === 'undecided').length;
197
- tallyMessage = `${n} fixes kept. ${stillReview} still to review.${nonce()}`;
198
- }
199
-
200
- // Reject all: mark every hunk rejected; no text is written. The tally region announces it.
201
- function rejectAll() {
202
- overrides = Object.fromEntries(hunks.map((h) => [h.index, 'rejected'] as const));
203
- tallyMessage = `All ${hunks.length} changes skipping.${nonce()}`;
204
- }
205
-
206
- // Apply: write the kept hunks in ONE batched transaction through the apply seam, then close. The
207
- // seam's acceptMany dispatches a single view.dispatch({ changes }), so the whole tidy is one undoable
208
- // step. ONLY the kept indexes are passed, so an undecided judgment hunk is never written.
209
- function apply() {
210
- const keptIndexes = hunks.filter((h) => dispositions[h.index] === 'kept').map((h) => h.index);
211
- api.acceptMany(keptIndexes);
212
- api.exit();
213
- dialog?.close();
214
- onclose(true);
215
- }
216
-
217
- // Cancel: write nothing, clear the decorations, leave the document byte-identical.
218
- function cancel() {
219
- api.exit();
220
- dialog?.close();
221
- onclose(false);
222
- }
223
-
224
- function showInText(h: Hunk) {
225
- const c = changes.find((ch) => ch.index === h.index);
226
- if (c) onshow(c.from, c.to);
227
- }
228
-
229
- // Move the step-through cursor and announce the hunk it lands on. A screen-reader user pressing j/k
230
- // hears the newly-focused hunk (kind plus before/after text, plus the because-line for a judgment
231
- // hunk), the same spec invariant the per-hunk action narration holds. Without this a move was silent.
232
- function moveFocus(next: number) {
233
- focusedPos = next;
234
- const h = hunks[focusedPos];
235
- if (h) narrate(h, 'Focused');
236
- }
237
-
238
- // Keyboard step-through on the hunk list (graft 3): j/k or n/p move; a/r accept/reject the focused
239
- // hunk; A accepts all objective; Escape cancels (the native dialog supplies Escape, handled below).
240
- function onListKeydown(e: KeyboardEvent) {
241
- const h = hunks[focusedPos];
242
- if (e.key === 'j' || e.key === 'n') {
243
- moveFocus(Math.min(focusedPos + 1, hunks.length - 1));
244
- e.preventDefault();
245
- } else if (e.key === 'k' || e.key === 'p') {
246
- moveFocus(Math.max(focusedPos - 1, 0));
247
- e.preventDefault();
248
- } else if (e.key === 'a' && !e.shiftKey) {
249
- if (h) acceptHunk(h);
250
- e.preventDefault();
251
- } else if (e.key === 'r' && !e.shiftKey) {
252
- if (h) rejectHunk(h);
253
- e.preventDefault();
254
- } else if (e.key === 'A' || (e.key === 'a' && e.shiftKey)) {
255
- acceptFixes();
256
- e.preventDefault();
257
- }
258
- }
259
-
260
- // The native dialog raises a cancel event on Escape; map it to the surface's cancel so the buffer is
261
- // left untouched and the host clears tidy mode.
262
- function onDialogCancel(e: Event) {
20
+ <script lang="ts">import SparklesIcon from "@lucide/svelte/icons/sparkles";
21
+ import CheckIcon from "@lucide/svelte/icons/check";
22
+ import XIcon from "@lucide/svelte/icons/x";
23
+ import TriangleAlertIcon from "@lucide/svelte/icons/triangle-alert";
24
+ import LightbulbIcon from "@lucide/svelte/icons/lightbulb";
25
+ import EyeIcon from "@lucide/svelte/icons/eye";
26
+ import { lineLabel } from "./tidy-diff.js";
27
+ import {
28
+ categorize,
29
+ isObjective,
30
+ buildBecause,
31
+ categoryLabel
32
+ } from "./tidy-categorize.js";
33
+ let { changes, original, conventions, model, title, api, onclose, onshow } = $props();
34
+ const hunks = $derived(changes.map((c) => {
35
+ const category = categorize(c, original, conventions);
36
+ const objective = isObjective(category);
37
+ const removed = original.slice(c.from, c.to);
38
+ const added = c.replacement;
39
+ const line = lineLabel(original, c.from);
40
+ const lines = original.split("\n");
41
+ const contextBefore = line >= 2 ? lines[line - 2] ?? "" : "";
42
+ const contextAfter = line < lines.length ? lines[line] ?? "" : "";
43
+ const lineStart = original.lastIndexOf("\n", c.from - 1) + 1;
44
+ const nextNewline = original.indexOf("\n", c.from);
45
+ const lineEnd = nextNewline === -1 ? original.length : nextNewline;
46
+ const fullLine = original.slice(lineStart, lineEnd);
47
+ const pre = original.slice(lineStart, c.from);
48
+ const post = original.slice(c.to, lineEnd);
49
+ const because = category.kind === "normalization" ? buildBecause(category.convention, conventions) : null;
50
+ return {
51
+ index: c.index,
52
+ category,
53
+ objective,
54
+ line,
55
+ contextBefore,
56
+ contextAfter,
57
+ delText: fullLine,
58
+ addText: pre + added + post,
59
+ delRun: { pre, mid: removed, post },
60
+ addRun: { pre, mid: added, post },
61
+ because,
62
+ label: categoryLabel(category)
63
+ };
64
+ }));
65
+ let overrides = $state({});
66
+ function effectiveDisposition(h, map) {
67
+ return map[h.index] ?? (h.objective ? "kept" : "undecided");
68
+ }
69
+ const dispositions = $derived(
70
+ Object.fromEntries(hunks.map((h) => [h.index, effectiveDisposition(h, overrides)]))
71
+ );
72
+ let focusedPos = $state(0);
73
+ let tallyMessage = $state("");
74
+ let actionMessage = $state("");
75
+ let announceNonce = 0;
76
+ function nonce() {
77
+ return announceNonce++ % 2 === 0 ? "" : "​";
78
+ }
79
+ const keptCount = $derived(hunks.filter((h) => dispositions[h.index] === "kept").length);
80
+ const reviewCount = $derived(hunks.filter((h) => dispositions[h.index] === "undecided").length);
81
+ const skipCount = $derived(hunks.filter((h) => dispositions[h.index] === "rejected").length);
82
+ let dialog = $state(null);
83
+ $effect(() => {
84
+ dialog?.showModal();
85
+ });
86
+ function setDisposition(index, next) {
87
+ overrides = { ...overrides, [index]: next };
88
+ }
89
+ function narrate(h, verb) {
90
+ const where = `Hunk ${hunks.indexOf(h) + 1} of ${hunks.length}`;
91
+ const what = h.delRun.mid && h.addRun.mid ? `${h.delRun.mid.trim()} becomes ${h.addRun.mid.trim()}` : h.label;
92
+ const why = h.because ? `, your ${h.because.label} setting is ${h.because.variant}` : "";
93
+ actionMessage = `${where}. ${h.label}. ${what}${why}. ${verb}.${nonce()}`;
94
+ }
95
+ function acceptHunk(h) {
96
+ setDisposition(h.index, "kept");
97
+ narrate(h, "Kept");
98
+ }
99
+ function rejectHunk(h) {
100
+ setDisposition(h.index, "rejected");
101
+ narrate(h, "Skipped");
102
+ }
103
+ function acceptFixes() {
104
+ const next = { ...overrides };
105
+ for (const h of hunks) if (h.objective) next[h.index] = "kept";
106
+ overrides = next;
107
+ const n = hunks.filter((h) => h.objective).length;
108
+ const stillReview = hunks.filter((h) => effectiveDisposition(h, next) === "undecided").length;
109
+ tallyMessage = `${n} fixes kept. ${stillReview} still to review.${nonce()}`;
110
+ }
111
+ function rejectAll() {
112
+ overrides = Object.fromEntries(hunks.map((h) => [h.index, "rejected"]));
113
+ tallyMessage = `All ${hunks.length} changes skipping.${nonce()}`;
114
+ }
115
+ function apply() {
116
+ const keptIndexes = hunks.filter((h) => dispositions[h.index] === "kept").map((h) => h.index);
117
+ api.acceptMany(keptIndexes);
118
+ api.exit();
119
+ dialog?.close();
120
+ onclose(true);
121
+ }
122
+ function cancel() {
123
+ api.exit();
124
+ dialog?.close();
125
+ onclose(false);
126
+ }
127
+ function showInText(h) {
128
+ const c = changes.find((ch) => ch.index === h.index);
129
+ if (c) onshow(c.from, c.to);
130
+ }
131
+ function moveFocus(next) {
132
+ focusedPos = next;
133
+ const h = hunks[focusedPos];
134
+ if (h) narrate(h, "Focused");
135
+ }
136
+ function onListKeydown(e) {
137
+ const h = hunks[focusedPos];
138
+ if (e.key === "j" || e.key === "n") {
139
+ moveFocus(Math.min(focusedPos + 1, hunks.length - 1));
140
+ e.preventDefault();
141
+ } else if (e.key === "k" || e.key === "p") {
142
+ moveFocus(Math.max(focusedPos - 1, 0));
143
+ e.preventDefault();
144
+ } else if (e.key === "a" && !e.shiftKey) {
145
+ if (h) acceptHunk(h);
146
+ e.preventDefault();
147
+ } else if (e.key === "r" && !e.shiftKey) {
148
+ if (h) rejectHunk(h);
149
+ e.preventDefault();
150
+ } else if (e.key === "A" || e.key === "a" && e.shiftKey) {
151
+ acceptFixes();
263
152
  e.preventDefault();
264
- cancel();
265
- }
266
-
267
- function actsLabel(h: Hunk): string {
268
- return h.objective ? 'Accept or reject this fix' : 'Accept or reject this change';
269
153
  }
154
+ }
155
+ function onDialogCancel(e) {
156
+ e.preventDefault();
157
+ cancel();
158
+ }
159
+ function actsLabel(h) {
160
+ return h.objective ? "Accept or reject this fix" : "Accept or reject this change";
161
+ }
270
162
  </script>
271
163
 
272
164
  <dialog
@@ -6,46 +6,25 @@ text; when the editor holds a selection it arrives as the default text, and the
6
6
  that selection either way. Built on a native <dialog>, following the LinkPicker a11y conventions,
7
7
  and opened by the host's Ctrl/Cmd+K shortcut through the exported open().
8
8
  -->
9
- <script lang="ts">
10
- interface Props {
11
- /** Insert an inline link at the editor cursor; the editor's registerInsertLink seam. */
12
- insert: (href: string, title: string) => void;
13
- /** Read the editor's current selection, for the Text field's default. */
14
- selection?: () => string;
15
- /** Disable the trigger; the host sets it while Preview shows. */
16
- disabled?: boolean;
17
- /** Render the built-in Web link trigger. False mounts only the dialog, for a host that
18
- * supplies its own trigger and opens the dialog through the exported open(). */
19
- trigger?: boolean;
20
- }
21
-
22
- let { insert, selection, disabled = false, trigger = true }: Props = $props();
23
-
24
- let dialog = $state<HTMLDialogElement | null>(null);
25
- let hrefInput = $state<HTMLInputElement | null>(null);
26
- let href = $state('');
27
- let text = $state('');
28
-
29
- /** Open the dialog with fresh fields; the edit page's Ctrl/Cmd+K shortcut calls it too. */
30
- export function open() {
31
- href = '';
32
- text = selection?.() ?? '';
33
- dialog?.showModal();
34
- // showModal() lands focus on the first focusable element (the header Close button), so move
35
- // it to the address input the dialog exists for (WCAG 2.4.3). A microtask defers past the
36
- // dialog's own focus handling, the RenameDialog recipe.
37
- queueMicrotask(() => hrefInput?.focus());
38
- }
39
- function close() {
40
- dialog?.close();
41
- }
42
- function submit(e: SubmitEvent) {
43
- e.preventDefault();
44
- // With no text and no selection the address itself becomes the display text, so the link
45
- // never renders as an invisible pair of brackets.
46
- insert(href, text.trim() || href);
47
- close();
48
- }
9
+ <script lang="ts">let { insert, selection, disabled = false, trigger = true } = $props();
10
+ let dialog = $state(null);
11
+ let hrefInput = $state(null);
12
+ let href = $state("");
13
+ let text = $state("");
14
+ export function open() {
15
+ href = "";
16
+ text = selection?.() ?? "";
17
+ dialog?.showModal();
18
+ queueMicrotask(() => hrefInput?.focus());
19
+ }
20
+ function close() {
21
+ dialog?.close();
22
+ }
23
+ function submit(e) {
24
+ e.preventDefault();
25
+ insert(href, text.trim() || href);
26
+ close();
27
+ }
49
28
  </script>
50
29
 
51
30
  {#if trigger}
@@ -7135,6 +7135,10 @@
7135
7135
  user-select: none;
7136
7136
  }
7137
7137
 
7138
+ :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .\[builtin\:vite-dynamic-import-vars\] {
7139
+ builtin: vite-dynamic-import-vars;
7140
+ }
7141
+
7138
7142
  @media (hover: hover) {
7139
7143
  :where([data-theme='cairn-admin'], [data-theme='cairn-admin-dark']) .group-hover\/sec\:opacity-90:is(:where(.group\/sec):hover *) {
7140
7144
  opacity: .9;
@@ -44,11 +44,15 @@ export declare function firstImageFile(dt: {
44
44
  files?: ArrayLike<File>;
45
45
  items?: ArrayLike<DataTransferItem>;
46
46
  }): File | null;
47
- /** The conservative canvas area budget, about 16.7M px (4096 x 4096). A source over this is scaled
48
- * down before any `drawImage`, never clipped. */
47
+ /**
48
+ * The conservative canvas area budget, about 16.7M px (4096 x 4096). A source over this is scaled
49
+ * down before any `drawImage`, never clipped.
50
+ */
49
51
  export declare const MAX_AREA = 16777216;
50
- /** The conservative short-side budget. A source whose smaller dimension exceeds this is scaled down so
51
- * the short side lands at the cap, even when its area is within MAX_AREA. */
52
+ /**
53
+ * The conservative short-side budget. A source whose smaller dimension exceeds this is scaled down so
54
+ * the short side lands at the cap, even when its area is within MAX_AREA.
55
+ */
52
56
  export declare const MAX_SHORT_SIDE = 4096;
53
57
  /**
54
58
  * The canvas budget for a source of the given dimensions. When the source area exceeds MAX_AREA or its
@@ -60,9 +64,11 @@ export declare function budgetForDimensions(width: number, height: number): {
60
64
  width: number;
61
65
  height: number;
62
66
  };
63
- /** The ingest failure taxonomy. `decode-unsupported` is a format the browser and the HEIC decoder both
67
+ /**
68
+ * The ingest failure taxonomy. `decode-unsupported` is a format the browser and the HEIC decoder both
64
69
  * refuse; `transcode-failed` is a HEIC decode or a canvas re-encode that threw; `too-large` is a
65
- * source still over budget after a transcode; `network` is the upload fetch rejecting. */
70
+ * source still over budget after a transcode; `network` is the upload fetch rejecting.
71
+ */
66
72
  export type IngestFailureKind = 'decode-unsupported' | 'transcode-failed' | 'too-large' | 'network';
67
73
  /** A failed ingest card: a stable discriminant plus a human message the capture card renders. */
68
74
  export interface IngestFailureCard {
@@ -136,7 +142,9 @@ export declare function ingestFailureKind(error: unknown): IngestFailureKind;
136
142
  * `fetch`.
137
143
  */
138
144
  export declare function sendUpload(url: string, init: RequestInit): Promise<Response>;
139
- /** Guard a drop target: cancel the browser's default open-the-file behavior on `dragover` and `drop`
140
- * so a dropped image stays inside the editor rather than navigating the page to the file. */
145
+ /**
146
+ * Guard a drop target: cancel the browser's default open-the-file behavior on `dragover` and `drop`
147
+ * so a dropped image stays inside the editor rather than navigating the page to the file.
148
+ */
141
149
  export declare function guardDropTarget(event: DragEvent): void;
142
150
  export {};
@@ -109,11 +109,15 @@ export function normalizeDataTransfer(dt) {
109
109
  export function firstImageFile(dt) {
110
110
  return normalizeDataTransfer(dt)[0] ?? null;
111
111
  }
112
- /** The conservative canvas area budget, about 16.7M px (4096 x 4096). A source over this is scaled
113
- * down before any `drawImage`, never clipped. */
112
+ /**
113
+ * The conservative canvas area budget, about 16.7M px (4096 x 4096). A source over this is scaled
114
+ * down before any `drawImage`, never clipped.
115
+ */
114
116
  export const MAX_AREA = 16_777_216;
115
- /** The conservative short-side budget. A source whose smaller dimension exceeds this is scaled down so
116
- * the short side lands at the cap, even when its area is within MAX_AREA. */
117
+ /**
118
+ * The conservative short-side budget. A source whose smaller dimension exceeds this is scaled down so
119
+ * the short side lands at the cap, even when its area is within MAX_AREA.
120
+ */
117
121
  export const MAX_SHORT_SIDE = 4096;
118
122
  /**
119
123
  * The canvas budget for a source of the given dimensions. When the source area exceeds MAX_AREA or its
@@ -290,8 +294,10 @@ export async function sendUpload(url, init) {
290
294
  throw new IngestError('network');
291
295
  }
292
296
  }
293
- /** Guard a drop target: cancel the browser's default open-the-file behavior on `dragover` and `drop`
294
- * so a dropped image stays inside the editor rather than navigating the page to the file. */
297
+ /**
298
+ * Guard a drop target: cancel the browser's default open-the-file behavior on `dragover` and `drop`
299
+ * so a dropped image stays inside the editor rather than navigating the page to the file.
300
+ */
295
301
  export function guardDropTarget(event) {
296
302
  event.preventDefault();
297
303
  }
@@ -103,8 +103,10 @@ class MediaChipWidget extends WidgetType {
103
103
  return false;
104
104
  }
105
105
  }
106
- /** Scan one line's text for media image tokens, mapping each to its document offsets and resolving its
107
- * library entry. lineFrom is the line's document start, so the match offsets become absolute. */
106
+ /**
107
+ * Scan one line's text for media image tokens, mapping each to its document offsets and resolving its
108
+ * library entry. lineFrom is the line's document start, so the match offsets become absolute.
109
+ */
108
110
  function matchesInLine(text, lineFrom, library) {
109
111
  const out = [];
110
112
  MEDIA_IMAGE.lastIndex = 0;
@@ -133,10 +135,12 @@ function matchesInLine(text, lineFrom, library) {
133
135
  }
134
136
  return out;
135
137
  }
136
- /** Every media image match across the editor's visible ranges, in document order, each carrying its
138
+ /**
139
+ * Every media image match across the editor's visible ranges, in document order, each carrying its
137
140
  * enclosing figure role. One {@link fenceScan} over the whole document feeds the cheap per-token
138
141
  * figure detection (no remark parse on the per-rebuild chip path); the visible lines are scanned
139
- * for tokens, then each token's line index drives {@link figureRoleAtLine}. */
142
+ * for tokens, then each token's line index drives {@link figureRoleAtLine}.
143
+ */
140
144
  function visibleMatches(view, library) {
141
145
  const lines = view.state.doc.toString().split('\n');
142
146
  const scan = fenceScan(lines);
@@ -153,8 +157,10 @@ function visibleMatches(view, library) {
153
157
  }
154
158
  return out;
155
159
  }
156
- /** Replace decorations for each visible media image's reference token: the chip widget over the URL
157
- * token, the alt left untouched. The same spans seed the atomic-range set. */
160
+ /**
161
+ * Replace decorations for each visible media image's reference token: the chip widget over the URL
162
+ * token, the alt left untouched. The same spans seed the atomic-range set.
163
+ */
158
164
  function buildMediaDecorations(view, library) {
159
165
  const builder = new RangeSetBuilder();
160
166
  for (const match of visibleMatches(view, library)) {
@@ -162,9 +168,11 @@ function buildMediaDecorations(view, library) {
162
168
  }
163
169
  return builder.finish();
164
170
  }
165
- /** The atomic ranges for the visible media reference tokens: a caret or selection edit treats each
171
+ /**
172
+ * The atomic ranges for the visible media reference tokens: a caret or selection edit treats each
166
173
  * token as one unit, so a stray keystroke replaces the whole reference rather than corrupting a hex
167
- * digit. Built from the same matches the decorations use, so the two never disagree. */
174
+ * digit. Built from the same matches the decorations use, so the two never disagree.
175
+ */
168
176
  function buildAtomicRanges(view, library) {
169
177
  const ranges = [];
170
178
  for (const match of visibleMatches(view, library)) {
@@ -1,8 +1,10 @@
1
1
  import { EditorView } from '@codemirror/view';
2
2
  import { type Extension } from '@codemirror/state';
3
- /** The seam the host drives: begin lands a placeholder and returns its id; progress moves its bar;
3
+ /**
4
+ * The seam the host drives: begin lands a placeholder and returns its id; progress moves its bar;
4
5
  * resolveTo swaps it for the committed image text; cancel removes it leaving the source untouched.
5
- * Mirrors the register-callback idiom MarkdownEditor uses for its other editor ops. */
6
+ * Mirrors the register-callback idiom MarkdownEditor uses for its other editor ops.
7
+ */
6
8
  export interface ImagePlaceholderApi {
7
9
  /** Land an optimistic placeholder at the current caret from a local object URL; returns its id. */
8
10
  begin(objectUrl: string): number;