@fabrika/canvas-kernel 0.1.44

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 (68) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +74 -0
  3. package/annotate.ts +94 -0
  4. package/daemon.ts +46 -0
  5. package/index.ts +84 -0
  6. package/package.json +74 -0
  7. package/src/client/AnnotationDraftFooter.tsx +76 -0
  8. package/src/client/AnnotationEditor.tsx +320 -0
  9. package/src/client/AnnotationList.tsx +190 -0
  10. package/src/client/AnnotationProvider.tsx +294 -0
  11. package/src/client/AnnotationSidebar.tsx +374 -0
  12. package/src/client/FileIcon.tsx +76 -0
  13. package/src/client/PlanRenderer.tsx +587 -0
  14. package/src/client/Popover.tsx +361 -0
  15. package/src/client/RenderErrorContext.ts +11 -0
  16. package/src/client/ResponsePreview.tsx +186 -0
  17. package/src/client/SessionContext.tsx +3 -0
  18. package/src/client/annotationContext.ts +130 -0
  19. package/src/client/annotationDom.ts +115 -0
  20. package/src/client/annotationDraft.ts +130 -0
  21. package/src/client/annotationMarkdown.ts +102 -0
  22. package/src/client/annotationTarget.ts +73 -0
  23. package/src/client/appContext.tsx +61 -0
  24. package/src/client/blockTarget.ts +127 -0
  25. package/src/client/components/Callout.tsx +27 -0
  26. package/src/client/components/Checklist.tsx +37 -0
  27. package/src/client/components/Choice.tsx +178 -0
  28. package/src/client/components/CodeBlock.tsx +35 -0
  29. package/src/client/components/Diff.tsx +36 -0
  30. package/src/client/components/FilePreview.tsx +88 -0
  31. package/src/client/components/Image.tsx +48 -0
  32. package/src/client/components/Markdown.tsx +44 -0
  33. package/src/client/components/Mermaid.tsx +576 -0
  34. package/src/client/components/Note.tsx +14 -0
  35. package/src/client/components/Priority.tsx +19 -0
  36. package/src/client/components/ResponseNote.tsx +19 -0
  37. package/src/client/components/Section.tsx +33 -0
  38. package/src/client/components/Table.tsx +35 -0
  39. package/src/client/components/Task.tsx +76 -0
  40. package/src/client/components/UserInput.tsx +176 -0
  41. package/src/client/components/index.ts +18 -0
  42. package/src/client/components/renderMarkdownChildren.tsx +100 -0
  43. package/src/client/diffLines.ts +50 -0
  44. package/src/client/generateMarkdown.ts +250 -0
  45. package/src/client/highlightRange.ts +262 -0
  46. package/src/client/hostContext.tsx +55 -0
  47. package/src/client/index.ts +128 -0
  48. package/src/client/popoverPosition.ts +46 -0
  49. package/src/client/preact-all.ts +4 -0
  50. package/src/client/regionTarget.ts +180 -0
  51. package/src/client/runtime.ts +149 -0
  52. package/src/client/styles.css +492 -0
  53. package/src/client/testing/dom.ts +58 -0
  54. package/src/client/textTarget.ts +31 -0
  55. package/src/client/theme.css +66 -0
  56. package/src/client/useRegionAnnotation.tsx +188 -0
  57. package/src/client/useTextAnnotation.tsx +194 -0
  58. package/src/client/utils.ts +33 -0
  59. package/src/engine/channel.ts +278 -0
  60. package/src/engine/compiler.ts +415 -0
  61. package/src/engine/http-utils.ts +14 -0
  62. package/src/engine/lifecycle.ts +302 -0
  63. package/src/engine/paths.ts +63 -0
  64. package/src/engine/router.ts +16 -0
  65. package/src/engine/session.ts +678 -0
  66. package/src/engine/waiter.ts +188 -0
  67. package/src/engine/watcher.ts +84 -0
  68. package/src/engine/websocket.ts +159 -0
@@ -0,0 +1,492 @@
1
+ @import url('https://fonts.googleapis.com/css2?family=Instrument+Serif&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');
2
+
3
+ @import "tailwindcss";
4
+ @import "./theme.css";
5
+ @source "./**/*.{tsx,ts,jsx,js}";
6
+
7
+ /* ── Non-theme CSS variables (not used by Tailwind utilities, just custom CSS) ── */
8
+ :root {
9
+ --color-shadow: rgba(0, 0, 0, 0.25);
10
+ --color-scrollbar: rgba(255, 248, 240, 0.06);
11
+ --color-scrollbar-hover: rgba(255, 248, 240, 0.12);
12
+ --color-folder-fill: rgba(196, 154, 58, 0.15);
13
+ --color-folder-fill-active: rgba(196, 154, 58, 0.25);
14
+ --color-folder-stroke: rgba(196, 154, 58, 0.5);
15
+ --color-folder-stroke-active: #c49a3a;
16
+ --color-hljs-base: #c4beb7;
17
+ --color-hljs-keyword: #d2a6ff;
18
+ --color-hljs-string: #a8cc8c;
19
+ --color-hljs-comment: #847d75;
20
+ --color-hljs-number: #e8a87c;
21
+ --color-hljs-variable: #7ec8e3;
22
+ --color-hljs-function: #dbc074;
23
+ --color-hljs-punctuation: #a09a92;
24
+ }
25
+
26
+ [data-theme="light"] {
27
+ /* Override Tailwind theme vars */
28
+ --color-bg-base: #faf5e6;
29
+ --color-bg-surface: #f3edd8;
30
+ --color-bg-elevated: #ebe4ce;
31
+ --color-bg-code: #f0ebd5;
32
+ --color-bg-input: rgba(80, 60, 10, 0.05);
33
+ --color-bg-elevated-half: rgba(235, 228, 206, 0.5);
34
+
35
+ --color-text-primary: #2a2520;
36
+ --color-text-secondary: #5a5550;
37
+ --color-text-tertiary: #9a9390;
38
+ --color-text-code: #4a4540;
39
+ --color-text-disabled: #c5c0b8;
40
+ --color-text-inverse: #faf6ef;
41
+
42
+ --color-border-subtle: rgba(80, 60, 30, 0.08);
43
+ --color-border-medium: rgba(80, 60, 30, 0.10);
44
+ --color-border-hover: rgba(80, 60, 30, 0.15);
45
+ --color-border-strong: rgba(80, 60, 30, 0.20);
46
+
47
+ --color-accent-green: #3a8a5a;
48
+ --color-accent-green-muted: rgba(58, 138, 90, 0.10);
49
+ --color-accent-amber: #a07820;
50
+ --color-accent-amber-muted: rgba(160, 120, 32, 0.10);
51
+ --color-accent-red: #b84a4a;
52
+ --color-accent-red-muted: rgba(184, 74, 74, 0.08);
53
+ --color-accent-blue: #4a7eb4;
54
+ --color-accent-blue-muted: rgba(74, 126, 180, 0.08);
55
+
56
+ --color-highlight-bg: rgba(200, 160, 30, 0.15);
57
+ --color-highlight-border: rgba(200, 160, 30, 0.40);
58
+ --color-highlight-annotation: rgba(200, 160, 30, 0.18);
59
+ --color-highlight-active: rgba(200, 160, 30, 0.35);
60
+ --color-highlight-selected: rgba(200, 160, 30, 0.10);
61
+
62
+ --color-badge-bg: rgba(160, 120, 32, 0.15);
63
+ --color-btn-primary: #2a2520;
64
+ --color-btn-primary-text: #faf6ef;
65
+
66
+ /* Override non-theme vars */
67
+ --color-shadow: rgba(80, 60, 30, 0.10);
68
+ --color-scrollbar: rgba(80, 60, 30, 0.08);
69
+ --color-scrollbar-hover: rgba(80, 60, 30, 0.15);
70
+ --color-folder-fill: rgba(160, 120, 32, 0.12);
71
+ --color-folder-fill-active: rgba(160, 120, 32, 0.22);
72
+ --color-folder-stroke: rgba(160, 120, 32, 0.4);
73
+ --color-folder-stroke-active: #a07820;
74
+ --color-hljs-base: #4a4540;
75
+ --color-hljs-keyword: #7c3aed;
76
+ --color-hljs-string: #2e7d32;
77
+ --color-hljs-comment: #9a9390;
78
+ --color-hljs-number: #c45a20;
79
+ --color-hljs-variable: #1976d2;
80
+ --color-hljs-function: #8a6d00;
81
+ --color-hljs-punctuation: #6b6560;
82
+ }
83
+
84
+ /* ── Base styles ── */
85
+ @layer base {
86
+ body {
87
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
88
+ background-color: var(--color-bg-base);
89
+ color: var(--color-text-primary);
90
+ -webkit-font-smoothing: antialiased;
91
+ -moz-osx-font-smoothing: grayscale;
92
+ }
93
+ }
94
+
95
+ code, pre, .font-mono {
96
+ font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
97
+ }
98
+
99
+ /* Annotation inline marks */
100
+ mark[data-annotation-id] {
101
+ background: var(--color-highlight-annotation);
102
+ border-bottom: 1.5px solid var(--color-highlight-bg);
103
+ border-radius: 2px;
104
+ cursor: pointer;
105
+ transition: background 150ms ease, box-shadow 150ms ease;
106
+ color: inherit;
107
+ padding: 1px 0;
108
+ }
109
+
110
+ mark[data-annotation-id].mark-active {
111
+ background: var(--color-highlight-active);
112
+ border-bottom: 2px solid var(--color-highlight-border);
113
+ box-shadow: 0 0 0 1px var(--color-highlight-border);
114
+ }
115
+
116
+ @keyframes ann-flash {
117
+ 0%, 100% { background: var(--color-highlight-annotation); }
118
+ 25%, 75% { background: var(--color-highlight-active); }
119
+ }
120
+
121
+ .ann-flash {
122
+ animation: ann-flash 0.6s ease 2;
123
+ }
124
+
125
+ /* Region annotations — a normalized box drawn over an image block. The host
126
+ positions the boxes, so every host that marks one gets them placed right. */
127
+ [data-annotation-image] {
128
+ position: relative;
129
+ cursor: crosshair;
130
+ touch-action: none;
131
+ }
132
+
133
+ .ann-region {
134
+ position: absolute;
135
+ background: var(--color-highlight-annotation);
136
+ border: 2px solid var(--color-highlight-bg);
137
+ border-radius: 2px;
138
+ cursor: pointer;
139
+ transition: background 150ms ease, box-shadow 150ms ease;
140
+ }
141
+
142
+ .ann-region[data-annotation-region="ellipse"] {
143
+ border-radius: 50%;
144
+ }
145
+
146
+ .ann-region.mark-active {
147
+ background: var(--color-highlight-active);
148
+ border-color: var(--color-highlight-border);
149
+ box-shadow: 0 0 0 1px var(--color-highlight-border);
150
+ }
151
+
152
+ /* The box being dragged out: not yet an annotation, and never a hit target. */
153
+ .ann-region-draft {
154
+ pointer-events: none;
155
+ border-style: dashed;
156
+ border-color: var(--color-highlight-border);
157
+ }
158
+
159
+ /* Annotation highlight (legacy) */
160
+ .annotation-highlight {
161
+ background: var(--color-highlight-bg);
162
+ border-bottom: 2px solid var(--color-highlight-border);
163
+ border-radius: 2px;
164
+ cursor: pointer;
165
+ transition: background 150ms ease;
166
+ }
167
+
168
+ .annotation-highlight:hover,
169
+ .annotation-highlight.active {
170
+ background: var(--color-highlight-active);
171
+ }
172
+
173
+ /* Selection color */
174
+ ::selection {
175
+ background-color: var(--color-highlight-active);
176
+ color: var(--color-text-primary);
177
+ }
178
+
179
+ /* Focus ring */
180
+ *:focus-visible {
181
+ outline: 2px solid var(--color-accent-blue);
182
+ outline-offset: 2px;
183
+ border-radius: 4px;
184
+ }
185
+
186
+ textarea:focus-visible {
187
+ outline: none;
188
+ }
189
+
190
+ /* Transitions */
191
+ button, a, [role="button"] {
192
+ transition: all 150ms ease;
193
+ }
194
+
195
+ /* Scrollbar */
196
+ ::-webkit-scrollbar {
197
+ width: 6px;
198
+ height: 6px;
199
+ }
200
+
201
+ ::-webkit-scrollbar-track {
202
+ background: transparent;
203
+ }
204
+
205
+ ::-webkit-scrollbar-thumb {
206
+ background: var(--color-scrollbar);
207
+ border-radius: 3px;
208
+ }
209
+
210
+ ::-webkit-scrollbar-thumb:hover {
211
+ background: var(--color-scrollbar-hover);
212
+ }
213
+
214
+ /* Diff highlights for compare view */
215
+ .diff-added {
216
+ background-color: color-mix(in srgb, var(--color-accent-green) 25%, transparent);
217
+ border-radius: 2px;
218
+ padding-inline: 1px;
219
+ padding-block: 1px;
220
+ }
221
+ .diff-removed {
222
+ background-color: color-mix(in srgb, var(--color-accent-red) 25%, transparent);
223
+ border-radius: 2px;
224
+ padding-inline: 1px;
225
+ padding-block: 1px;
226
+ text-decoration: line-through;
227
+ text-decoration-color: var(--color-accent-red);
228
+ }
229
+
230
+ /* Block-level diff highlights for unified compare view */
231
+ .diff-block-removed {
232
+ background: color-mix(in srgb, var(--color-accent-red) 8%, transparent);
233
+ border-left: 3px solid var(--color-accent-red);
234
+ opacity: 0.7;
235
+ }
236
+ .diff-block-added {
237
+ background: color-mix(in srgb, var(--color-accent-green) 8%, transparent);
238
+ border-left: 3px solid var(--color-accent-green);
239
+ }
240
+ .diff-row-removed td {
241
+ background: color-mix(in srgb, var(--color-accent-red) 10%, transparent);
242
+ text-decoration: line-through;
243
+ text-decoration-color: var(--color-accent-red);
244
+ opacity: 0.6;
245
+ }
246
+ .diff-row-added td {
247
+ background: color-mix(in srgb, var(--color-accent-green) 10%, transparent);
248
+ }
249
+ .diff-item-removed {
250
+ background: color-mix(in srgb, var(--color-accent-red) 10%, transparent);
251
+ text-decoration: line-through;
252
+ text-decoration-color: var(--color-accent-red);
253
+ opacity: 0.6;
254
+ }
255
+ .diff-item-added {
256
+ background: color-mix(in srgb, var(--color-accent-green) 10%, transparent);
257
+ }
258
+ .diff-block-label {
259
+ position: absolute;
260
+ top: 4px;
261
+ right: 8px;
262
+ font-size: 10px;
263
+ font-weight: 500;
264
+ text-transform: uppercase;
265
+ padding: 1px 6px;
266
+ border-radius: 3px;
267
+ pointer-events: none;
268
+ }
269
+
270
+ /* Plan update flash */
271
+ @keyframes plan-refresh {
272
+ 0% { opacity: 0.6; }
273
+ 100% { opacity: 1; }
274
+ }
275
+
276
+ .plan-updated {
277
+ animation: plan-refresh 300ms ease;
278
+ }
279
+
280
+ /* Range input styling */
281
+ input[type="range"] {
282
+ height: 20px;
283
+ -webkit-appearance: none;
284
+ appearance: none;
285
+ background: transparent;
286
+ }
287
+
288
+ input[type="range"]::-webkit-slider-thumb {
289
+ -webkit-appearance: none;
290
+ appearance: none;
291
+ width: 14px;
292
+ height: 14px;
293
+ border-radius: 50%;
294
+ background: var(--color-accent-amber);
295
+ cursor: pointer;
296
+ border: 2px solid var(--color-bg-base);
297
+ margin-top: -5px;
298
+ }
299
+
300
+ input[type="range"]::-moz-range-thumb {
301
+ width: 14px;
302
+ height: 14px;
303
+ border-radius: 50%;
304
+ background: var(--color-accent-amber);
305
+ cursor: pointer;
306
+ border: 2px solid var(--color-bg-base);
307
+ }
308
+
309
+ input[type="range"]::-webkit-slider-runnable-track {
310
+ height: 4px;
311
+ border-radius: 2px;
312
+ background: transparent;
313
+ }
314
+
315
+ input[type="range"]::-moz-range-track {
316
+ height: 4px;
317
+ border-radius: 2px;
318
+ background: transparent;
319
+ }
320
+
321
+ /* Prose (Markdown component) */
322
+ .prose-canvas {
323
+ color: var(--color-text-secondary);
324
+ font-size: 0.875rem;
325
+ line-height: 1.7;
326
+ }
327
+
328
+ .prose-canvas h1, .prose-canvas h2, .prose-canvas h3,
329
+ .prose-canvas h4, .prose-canvas h5, .prose-canvas h6 {
330
+ color: var(--color-text-primary);
331
+ font-weight: 600;
332
+ margin-top: 1.5em;
333
+ margin-bottom: 0.5em;
334
+ line-height: 1.3;
335
+ }
336
+
337
+ .prose-canvas h1 { font-size: 1.5rem; }
338
+ .prose-canvas h2 { font-size: 1.25rem; }
339
+ .prose-canvas h3 { font-size: 1.1rem; }
340
+
341
+ .prose-canvas h1:first-child, .prose-canvas h2:first-child,
342
+ .prose-canvas h3:first-child {
343
+ margin-top: 0;
344
+ }
345
+
346
+ .prose-canvas p {
347
+ margin-top: 0.75em;
348
+ margin-bottom: 0.75em;
349
+ }
350
+
351
+ .prose-canvas p:first-child {
352
+ margin-top: 0;
353
+ }
354
+
355
+ .prose-canvas a {
356
+ color: var(--color-accent-blue);
357
+ text-decoration: underline;
358
+ text-decoration-color: color-mix(in srgb, var(--color-accent-blue) 40%, transparent);
359
+ text-underline-offset: 2px;
360
+ }
361
+
362
+ .prose-canvas a:hover {
363
+ text-decoration-color: var(--color-accent-blue);
364
+ }
365
+
366
+ .prose-canvas strong {
367
+ color: var(--color-text-primary);
368
+ font-weight: 600;
369
+ }
370
+
371
+ .prose-canvas code {
372
+ font-size: 0.8em;
373
+ background: var(--color-bg-code);
374
+ color: var(--color-text-code);
375
+ padding: 0.15em 0.4em;
376
+ border-radius: 4px;
377
+ }
378
+
379
+ .prose-canvas pre {
380
+ background: var(--color-bg-code);
381
+ border-radius: 8px;
382
+ padding: 1em;
383
+ overflow-x: auto;
384
+ margin-top: 0.75em;
385
+ margin-bottom: 0.75em;
386
+ }
387
+
388
+ .prose-canvas pre code {
389
+ background: transparent;
390
+ padding: 0;
391
+ font-size: 0.8rem;
392
+ }
393
+
394
+ .prose-canvas ul, .prose-canvas ol {
395
+ padding-left: 1.5em;
396
+ margin-top: 0.5em;
397
+ margin-bottom: 0.5em;
398
+ }
399
+
400
+ .prose-canvas ul { list-style-type: disc; }
401
+ .prose-canvas ol { list-style-type: decimal; }
402
+ .prose-canvas li { margin-top: 0.25em; }
403
+ .prose-canvas li > ul, .prose-canvas li > ol { margin-top: 0.25em; margin-bottom: 0; }
404
+
405
+ .prose-canvas blockquote {
406
+ border-left: 3px solid var(--color-border-strong);
407
+ padding-left: 1em;
408
+ margin-left: 0;
409
+ margin-top: 0.75em;
410
+ margin-bottom: 0.75em;
411
+ color: var(--color-text-tertiary);
412
+ }
413
+
414
+ .prose-canvas hr {
415
+ border: none;
416
+ border-top: 1px solid var(--color-border-subtle);
417
+ margin: 1.5em 0;
418
+ }
419
+
420
+ .prose-canvas table {
421
+ width: 100%;
422
+ border-collapse: collapse;
423
+ margin-top: 0.75em;
424
+ margin-bottom: 0.75em;
425
+ font-size: 0.8rem;
426
+ }
427
+
428
+ .prose-canvas th, .prose-canvas td {
429
+ border: 1px solid var(--color-border-subtle);
430
+ padding: 0.5em 0.75em;
431
+ text-align: left;
432
+ }
433
+
434
+ .prose-canvas th {
435
+ background: var(--color-bg-elevated);
436
+ color: var(--color-text-primary);
437
+ font-weight: 600;
438
+ }
439
+
440
+ .prose-canvas img {
441
+ max-width: 100%;
442
+ border-radius: 8px;
443
+ margin-top: 0.75em;
444
+ margin-bottom: 0.75em;
445
+ }
446
+
447
+ /* highlight.js overrides */
448
+ .hljs {
449
+ background: transparent !important;
450
+ color: var(--color-hljs-base) !important;
451
+ }
452
+
453
+ pre code.hljs {
454
+ padding: 0 !important;
455
+ }
456
+
457
+ .hljs-keyword, .hljs-selector-tag, .hljs-built_in, .hljs-name, .hljs-tag {
458
+ color: var(--color-hljs-keyword) !important;
459
+ }
460
+
461
+ .hljs-string, .hljs-title, .hljs-section, .hljs-attribute, .hljs-literal,
462
+ .hljs-template-tag, .hljs-template-variable, .hljs-type {
463
+ color: var(--color-hljs-string) !important;
464
+ }
465
+
466
+ .hljs-comment, .hljs-quote, .hljs-deletion, .hljs-meta {
467
+ color: var(--color-hljs-comment) !important;
468
+ }
469
+
470
+ .hljs-number, .hljs-regexp, .hljs-addition {
471
+ color: var(--color-hljs-number) !important;
472
+ }
473
+
474
+ .hljs-variable, .hljs-attr {
475
+ color: var(--color-hljs-variable) !important;
476
+ }
477
+
478
+ .hljs-function .hljs-title, .hljs-title.function_ {
479
+ color: var(--color-hljs-function) !important;
480
+ }
481
+
482
+ .hljs-params {
483
+ color: var(--color-hljs-base) !important;
484
+ }
485
+
486
+ .hljs-property {
487
+ color: var(--color-hljs-variable) !important;
488
+ }
489
+
490
+ .hljs-punctuation {
491
+ color: var(--color-hljs-punctuation) !important;
492
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * DOM harness for the client tests.
3
+ *
4
+ * Import this **before** the module under test. ESM evaluates imports in order,
5
+ * so putting it first is what guarantees the globals exist by the time anything
6
+ * that reads `document` is evaluated.
7
+ *
8
+ * happy-dom is not a browser: it runs no layout. Everything geometric —
9
+ * `getBoundingClientRect`, `scrollHeight`, `offsetWidth` — reads zero unless the
10
+ * test stubs it, and `scrollIntoView` and CSS animations do nothing. Assert on
11
+ * structure and on the values a function computes, never on real measurements.
12
+ */
13
+ import { GlobalRegistrator } from "@happy-dom/global-registrator";
14
+
15
+ // Bun runs every test file in one process, so the second file to import this
16
+ // must not register over the first.
17
+ if (!globalThis.document) {
18
+ GlobalRegistrator.register();
19
+ }
20
+
21
+ /**
22
+ * Replace the document body with `html` inside a fresh container and return it.
23
+ * Call it at the start of each test — the document is shared across every test
24
+ * in the process, so whatever the previous one left behind is still there.
25
+ */
26
+ export function mountContainer(html: string): HTMLElement {
27
+ document.body.innerHTML = `<div data-testid="container">${html}</div>`;
28
+ const container = document.body.firstElementChild;
29
+ if (!(container instanceof HTMLElement)) throw new Error("container did not mount");
30
+ return container;
31
+ }
32
+
33
+ /**
34
+ * A Range over the first occurrence of `text` within a single text node of
35
+ * `root`, which is how a reader's selection arrives in the annotation code.
36
+ * Throws rather than returning null so a typo in the fixture fails the test at
37
+ * its cause instead of somewhere downstream.
38
+ */
39
+ export function selectText(root: HTMLElement, text: string): Range {
40
+ const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
41
+ let node: Node | null;
42
+ while ((node = walker.nextNode())) {
43
+ const content = node.textContent ?? "";
44
+ const index = content.indexOf(text);
45
+ if (index === -1) continue;
46
+ const range = document.createRange();
47
+ range.setStart(node, index);
48
+ range.setEnd(node, index + text.length);
49
+ return range;
50
+ }
51
+ throw new Error(`no text node in the fixture contains ${JSON.stringify(text)}`);
52
+ }
53
+
54
+ /** Ids of the marks currently wrapping annotated text, in document order. */
55
+ export function markIds(root: HTMLElement): string[] {
56
+ return [...root.querySelectorAll("[data-annotation-id]")]
57
+ .map((el) => el.getAttribute("data-annotation-id") ?? "");
58
+ }
@@ -0,0 +1,31 @@
1
+ import type { AnnotationTarget } from "./annotationTarget";
2
+ import { restoreMarks } from "./highlightRange";
3
+
4
+ /** A run of text, identified by the text itself. */
5
+ export interface TextLocator {
6
+ text: string;
7
+ }
8
+
9
+ /**
10
+ * Selected text, wrapped in inline `<mark>` elements.
11
+ *
12
+ * The fallback target: any snippet at all is text, so this one claims
13
+ * everything the others turned down and must stay last in the registry.
14
+ */
15
+ export const textTarget: AnnotationTarget<TextLocator> = {
16
+ kind: "text",
17
+
18
+ parse: (snippet) => (snippet ? { text: snippet } : null),
19
+
20
+ format: (locator) => locator.text,
21
+
22
+ // A run of text has no element of its own until a mark wraps it, and the mark
23
+ // is found by annotation id, not by the text. Guessing the element back from
24
+ // page text would resolve an annotation onto whatever happens to read the
25
+ // same today, so a missing mark stays a miss.
26
+ find: () => null,
27
+
28
+ restore: (_locator, root, ann) => {
29
+ restoreMarks(root, [ann]);
30
+ },
31
+ };
@@ -0,0 +1,66 @@
1
+ @theme {
2
+ /* Colors — initial values, overridden by [data-theme] in styles.css */
3
+ --color-bg-base: #1a1915;
4
+ --color-bg-surface: #1f1e18;
5
+ --color-bg-elevated: #25241d;
6
+ --color-bg-code: #1c1b16;
7
+ --color-bg-input: rgba(255, 248, 220, 0.03);
8
+ --color-bg-elevated-half: rgba(37, 36, 29, 0.5);
9
+
10
+ --color-text-primary: #e8e4df;
11
+ --color-text-secondary: #a09a92;
12
+ --color-text-tertiary: #847d75;
13
+ --color-text-code: #c4beb7;
14
+ --color-text-disabled: #3a3530;
15
+ --color-text-inverse: #1a1a1a;
16
+
17
+ --color-border-subtle: rgba(255, 248, 240, 0.06);
18
+ --color-border-medium: rgba(255, 248, 240, 0.08);
19
+ --color-border-hover: rgba(255, 248, 240, 0.12);
20
+ --color-border-strong: rgba(255, 248, 240, 0.15);
21
+
22
+ --color-accent-green: #4a9e6d;
23
+ --color-accent-green-muted: rgba(74, 158, 109, 0.12);
24
+ --color-accent-amber: #c49a3a;
25
+ --color-accent-amber-muted: rgba(196, 154, 58, 0.12);
26
+ --color-accent-red: #c45a5a;
27
+ --color-accent-red-muted: rgba(196, 90, 90, 0.12);
28
+ --color-accent-blue: #5a8ec4;
29
+ --color-accent-blue-muted: rgba(90, 142, 196, 0.10);
30
+
31
+ --color-highlight-bg: rgba(255, 220, 100, 0.15);
32
+ --color-highlight-border: rgba(255, 220, 100, 0.35);
33
+ --color-highlight-annotation: rgba(255, 220, 100, 0.18);
34
+ --color-highlight-active: rgba(255, 220, 100, 0.35);
35
+ --color-highlight-selected: rgba(255, 220, 100, 0.10);
36
+
37
+ --color-badge-bg: rgba(196, 154, 58, 0.20);
38
+ --color-btn-primary: #e8e4df;
39
+ --color-btn-primary-text: #1a1a1a;
40
+
41
+ /* Fonts */
42
+ --font-heading: 'Instrument Serif', Georgia, 'Times New Roman', serif;
43
+ --font-body: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
44
+ --font-mono: 'JetBrains Mono', 'Fira Code', Consolas, monospace;
45
+
46
+ /* Font sizes */
47
+ --text-section: 1.75rem;
48
+ --text-section--line-height: 1.25;
49
+ --text-section--letter-spacing: -0.01em;
50
+ --text-task-label: 0.9375rem;
51
+ --text-task-label--line-height: 1.4;
52
+ --text-body: 0.875rem;
53
+ --text-body--line-height: 1.65;
54
+ --text-code: 0.8125rem;
55
+ --text-code--line-height: 1.6;
56
+ --text-meta: 0.75rem;
57
+ --text-meta--line-height: 1.4;
58
+ --text-meta--letter-spacing: 0.02em;
59
+ --text-tiny: 0.6875rem;
60
+ --text-tiny--line-height: 1.4;
61
+
62
+ /* Shadows */
63
+ --shadow-sm: 0 1px 2px var(--color-shadow);
64
+ --shadow-md: 0 4px 12px var(--color-shadow);
65
+ --shadow-lg: 0 8px 24px var(--color-shadow);
66
+ }