@ingcreators/annot-core 0.1.0

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 (65) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +201 -0
  3. package/README.md +56 -0
  4. package/dist/auto-capture-options.d.ts +78 -0
  5. package/dist/editor/arrow-markers.d.ts +142 -0
  6. package/dist/editor/bake-translate.d.ts +192 -0
  7. package/dist/editor/font-registry.d.ts +58 -0
  8. package/dist/editor/gradient-utils.d.ts +23 -0
  9. package/dist/editor/history-core.d.ts +48 -0
  10. package/dist/editor/icons/brand-icons.d.ts +53 -0
  11. package/dist/editor/icons/material-symbols.d.ts +105 -0
  12. package/dist/editor/icons/registry.d.ts +179 -0
  13. package/dist/editor/icons/render.d.ts +22 -0
  14. package/dist/editor/icons/sanitize.d.ts +60 -0
  15. package/dist/editor/index.d.ts +13 -0
  16. package/dist/editor/path-utils.d.ts +32 -0
  17. package/dist/editor/property-schema.d.ts +263 -0
  18. package/dist/editor/rich-text-mapper.d.ts +8 -0
  19. package/dist/editor/selection-geometry.d.ts +66 -0
  20. package/dist/editor/shape-utils.d.ts +27 -0
  21. package/dist/editor/svg-format.d.ts +68 -0
  22. package/dist/editor/svg-id-utils.d.ts +37 -0
  23. package/dist/editor/svg-to-annotation-shapes.d.ts +34 -0
  24. package/dist/editor/text-utils.d.ts +212 -0
  25. package/dist/editor/tool-lifecycle.d.ts +56 -0
  26. package/dist/editor/tool-options.d.ts +126 -0
  27. package/dist/editor/tool-panel-adapter.d.ts +105 -0
  28. package/dist/editor/tool-preset-serde.d.ts +43 -0
  29. package/dist/editor/tool-registry.d.ts +320 -0
  30. package/dist/editor/tool-style-reader.d.ts +36 -0
  31. package/dist/editor/tool-style-writer.d.ts +33 -0
  32. package/dist/editor/toolbar-icons.d.ts +84 -0
  33. package/dist/editor/transform-utils.d.ts +127 -0
  34. package/dist/editor/viewport-math.d.ts +69 -0
  35. package/dist/encode/index.d.ts +4 -0
  36. package/dist/encode/options.d.ts +79 -0
  37. package/dist/encode/png8.d.ts +10 -0
  38. package/dist/headless.d.ts +29 -0
  39. package/dist/icons/index.d.ts +15 -0
  40. package/dist/icons/types.d.ts +108 -0
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +3164 -0
  43. package/dist/storage/errors.d.ts +59 -0
  44. package/dist/storage/index.d.ts +6 -0
  45. package/dist/storage/metadata-cache.d.ts +231 -0
  46. package/dist/storage/path.d.ts +49 -0
  47. package/dist/storage/thumbnail-cache.d.ts +110 -0
  48. package/dist/storage/thumbnail.d.ts +31 -0
  49. package/dist/storage/types.d.ts +677 -0
  50. package/dist/utils/assert.d.ts +31 -0
  51. package/dist/utils/constants.d.ts +10 -0
  52. package/dist/utils/dash-utils.d.ts +6 -0
  53. package/dist/utils/desktop-bridge.d.ts +349 -0
  54. package/dist/utils/filename.d.ts +48 -0
  55. package/dist/utils/id.d.ts +21 -0
  56. package/dist/utils/index.d.ts +5 -0
  57. package/dist/utils/types.d.ts +20 -0
  58. package/dist/xmp/xmp-browser.d.ts +39 -0
  59. package/dist/zip/zip-builder.d.ts +8 -0
  60. package/dist/zip/zip-bytes.d.ts +22 -0
  61. package/package.json +58 -0
  62. package/styles/editor.css +1912 -0
  63. package/styles/fonts.css +46 -0
  64. package/styles/property-panel.css +779 -0
  65. package/styles/toolbar.css +673 -0
@@ -0,0 +1,673 @@
1
+ #toolbar {
2
+ position: absolute;
3
+ top: 0;
4
+ left: 0;
5
+ right: 0;
6
+ height: 48px;
7
+ background: var(--annot-bg-panel);
8
+ border-bottom: 1px solid var(--annot-border-color);
9
+ display: flex;
10
+ align-items: center;
11
+ padding: 0 8px;
12
+ gap: 4px;
13
+ z-index: 10;
14
+ }
15
+
16
+ .toolbar-group {
17
+ display: flex;
18
+ align-items: center;
19
+ gap: 2px;
20
+ }
21
+
22
+ .toolbar-separator {
23
+ width: 1px;
24
+ height: 28px;
25
+ background: var(--annot-border-color);
26
+ margin: 0 6px;
27
+ }
28
+
29
+ .toolbar-btn {
30
+ width: 36px;
31
+ height: 36px;
32
+ border: 1px solid transparent;
33
+ border-radius: 6px;
34
+ background: transparent;
35
+ color: var(--annot-text-primary);
36
+ font-size: 20px;
37
+ cursor: pointer;
38
+ font-variation-settings:
39
+ "FILL" 0,
40
+ "wght" 300,
41
+ "GRAD" 0,
42
+ "opsz" 20;
43
+ display: flex;
44
+ align-items: center;
45
+ justify-content: center;
46
+ transition:
47
+ background 0.1s,
48
+ border-color 0.1s;
49
+ position: relative;
50
+ }
51
+
52
+ .toolbar-btn:hover {
53
+ background: var(--annot-hover-bg);
54
+ border-color: var(--annot-hover-border);
55
+ }
56
+
57
+ .toolbar-btn.active {
58
+ background: var(--annot-active-bg);
59
+ border-color: var(--annot-active-border);
60
+ color: var(--annot-accent);
61
+ }
62
+
63
+ /* Custom CSS tooltip — generic for any element with data-tooltip.
64
+ *
65
+ * Uses `[data-tooltip]` (NOT `[title]`) as the source attribute so
66
+ * the browser doesn't show a DUPLICATE native tooltip on top of our
67
+ * custom one. `title` triggers the OS's built-in yellow bubble
68
+ * (Windows) / gray bubble (macOS), which is styled differently per
69
+ * platform, can't be themed, and appears with a fixed ~500 ms delay.
70
+ * Modern SaaS tools (Figma, Notion, Linear, Miro, VS Code, etc.) all
71
+ * skip `title` and use a custom tooltip like this one.
72
+ *
73
+ * Accessibility: `aria-label` is set in parallel with `data-tooltip`
74
+ * (via the `setTooltip()` helper in utils/tooltip.ts), so screen
75
+ * readers still announce the control's name.
76
+ *
77
+ * Scope: any element with `data-tooltip`, NOT scoped to specific
78
+ * button classes — otherwise every new button or tooltip-bearing
79
+ * element requires adding its class to this selector. A generic
80
+ * rule means `setTooltip(anyEl, "…")` Just Works.
81
+ *
82
+ * `bottom: -40px` — wide enough to clear the extended body of an
83
+ * accessibility-enlarged mouse cursor. Default OS cursors (~16×20 px)
84
+ * don't need this much offset, but Windows' "Large" / "Extra Large"
85
+ * pointer presets (32–64 px) extend down-right from the tip and would
86
+ * obscure a closer tooltip. 40 px clears the "Large" preset. */
87
+
88
+ /* Positioning context for the ::after pseudo. `:where()` keeps the
89
+ * specificity at zero so consuming elements can still override
90
+ * `position` (e.g. a table cell that needs `position: static` for
91
+ * its own layout) without `!important`. Most elements that get
92
+ * data-tooltip are buttons/chips that are fine with relative. */
93
+ :where([data-tooltip]) {
94
+ position: relative;
95
+ }
96
+
97
+ [data-tooltip]::after {
98
+ content: attr(data-tooltip);
99
+ position: absolute;
100
+ bottom: -40px;
101
+ left: 50%;
102
+ transform: translateX(-50%);
103
+ background: var(--annot-bg-panel-deep);
104
+ color: var(--annot-text-primary);
105
+ /* Force a plain system font. Without this, buttons that carry the
106
+ * `material-symbols-outlined` class would make the tooltip's text
107
+ * try to render as Material Symbols ligatures (tooltip text like
108
+ * "Help" isn't a valid ligature, so the browser falls back to
109
+ * whatever glyph coverage the font has — which looks weird /
110
+ * unstyled). */
111
+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
112
+ font-variation-settings: normal;
113
+ font-weight: 400;
114
+ font-size: 11px;
115
+ line-height: 1.2;
116
+ padding: 3px 8px;
117
+ border-radius: 4px;
118
+ border: 1px solid var(--annot-border-color);
119
+ white-space: nowrap;
120
+ pointer-events: none;
121
+ opacity: 0;
122
+ transition: opacity 0.15s;
123
+ z-index: 100;
124
+ /* Subtle shadow so the bubble reads against the canvas background
125
+ * (which may be the screenshot itself, not a theme-panel color). */
126
+ box-shadow: 0 2px 6px var(--annot-shadow, rgba(0, 0, 0, 0.15));
127
+ }
128
+
129
+ [data-tooltip]:hover::after {
130
+ opacity: 1;
131
+ }
132
+
133
+ .toolbar-input {
134
+ height: 28px;
135
+ border: 1px solid var(--annot-input-border);
136
+ border-radius: 4px;
137
+ background: var(--annot-input-bg);
138
+ color: var(--annot-text-primary);
139
+ font-size: 12px;
140
+ padding: 0 6px;
141
+ }
142
+
143
+ .toolbar-color {
144
+ width: 28px;
145
+ height: 28px;
146
+ border: 1px solid var(--annot-input-border);
147
+ border-radius: 4px;
148
+ padding: 0;
149
+ cursor: pointer;
150
+ background: transparent;
151
+ }
152
+
153
+ .toolbar-color::-webkit-color-swatch-wrapper {
154
+ padding: 2px;
155
+ }
156
+ .toolbar-color::-webkit-color-swatch {
157
+ border: none;
158
+ border-radius: 2px;
159
+ }
160
+
161
+ .toolbar-range {
162
+ width: 60px;
163
+ accent-color: var(--annot-accent);
164
+ }
165
+
166
+ .toolbar-label {
167
+ font-size: 11px;
168
+ color: var(--annot-text-secondary);
169
+ margin-right: 2px;
170
+ }
171
+
172
+ .toolbar-spacer {
173
+ flex: 1;
174
+ }
175
+
176
+ /* ---- Vertical variant (left-side tool strip) ----
177
+ *
178
+ * Enabled via the `.toolbar-vertical` class on the Toolbar container.
179
+ * The same DOM structure is used; only layout-direction properties are
180
+ * overridden so tool buttons, groups, separators, and the spacer all
181
+ * rotate 90°.
182
+ */
183
+ .toolbar-vertical {
184
+ /* `<annot-toolbar>` is a custom element with no UA stylesheet, so
185
+ * its default `display` is `inline`. The horizontal toolbar mode
186
+ * worked accidentally because inline-block children flowed
187
+ * horizontally on their own; the vertical mode worked accidentally
188
+ * because `<div class="tool-btn-wrap">` is block-level and stacks
189
+ * naturally. In both modes every flex-related declaration below
190
+ * (`flex-direction`, `gap`, `align-items`, `flex: 1` on the
191
+ * spacer) was silently no-op'd until `display: flex` was set on
192
+ * the host. #428 added `align-items: center` to fix the
193
+ * separator off-centre but it never took effect for that reason —
194
+ * the storybook DOM inspection in the follow-up PR confirmed
195
+ * `parentDisplay: "inline"` on the toolbar. Promoting to flex
196
+ * here turns every previously-silent rule on at once: gap
197
+ * spacing, centre-aligned narrower children, and the spacer
198
+ * actually pushing the history group to the bottom of the
199
+ * strip. */
200
+ display: flex;
201
+ flex-direction: column;
202
+ padding: 6px 0;
203
+ gap: 2px;
204
+ align-items: center;
205
+ }
206
+ .toolbar-vertical .toolbar-group {
207
+ flex-direction: column;
208
+ }
209
+ .toolbar-vertical .toolbar-separator {
210
+ width: 28px;
211
+ height: 1px;
212
+ margin: 6px 0;
213
+ }
214
+ .toolbar-vertical .toolbar-spacer {
215
+ flex: 1; /* flex: 1 already grows the correct axis in column layout */
216
+ }
217
+ /* Vertical mode: suppress the CSS ::after tooltip. It would need to
218
+ * extend to the right of the 48px sidebar, which (a) gets clipped by
219
+ * overflow-x: hidden and (b) pollutes the sidebar's scroll-area
220
+ * calculation causing a spurious horizontal scrollbar. Native browser
221
+ * `title` tooltips still appear on hover for accessibility — we just
222
+ * drop the custom styled bubble in this mode. */
223
+ /* Vertical toolbar: reposition the tooltip to the RIGHT of the
224
+ * button instead of below. Below-positioning makes no sense for a
225
+ * vertical strip — the tooltip would overlap the NEXT tool button
226
+ * stacked directly underneath. Rightward positioning keeps the
227
+ * tooltip in the canvas area beside the sidebar, which is empty
228
+ * space from the sidebar's perspective.
229
+ *
230
+ * Requires `#editor-sidebar { overflow-x: visible }` (see editor.css)
231
+ * so the tooltip can extend beyond the 48px strip. */
232
+ .toolbar-vertical .toolbar-btn[data-tooltip]::after {
233
+ bottom: auto;
234
+ top: 50%;
235
+ left: 100%;
236
+ transform: translateY(-50%);
237
+ margin-left: 12px;
238
+ }
239
+ /* Dropdown arrow — horizontal mode centers it below the icon; in
240
+ * vertical mode, put it on the right edge so it doesn't bleed into
241
+ * the next tool stacked below. */
242
+ /* Tool dropdown menu opens to the RIGHT of the tool in vertical mode
243
+ * instead of below it. */
244
+ .toolbar-vertical .tool-dropdown-menu {
245
+ top: 0;
246
+ left: 100%;
247
+ margin-top: 0;
248
+ margin-left: 6px;
249
+ }
250
+
251
+ /* Copy dropdown */
252
+ .copy-dropdown {
253
+ position: relative;
254
+ display: flex;
255
+ align-items: center;
256
+ }
257
+
258
+ .copy-dropdown-arrow {
259
+ width: 20px;
260
+ font-size: 10px;
261
+ padding: 0;
262
+ margin-left: -2px;
263
+ border-top-left-radius: 0;
264
+ border-bottom-left-radius: 0;
265
+ }
266
+
267
+ .copy-dropdown-menu {
268
+ position: absolute;
269
+ top: 100%;
270
+ right: 0;
271
+ margin-top: 4px;
272
+ background: var(--annot-bg-panel);
273
+ border: 1px solid var(--annot-border-color);
274
+ border-radius: 6px;
275
+ flex-direction: column;
276
+ overflow: hidden;
277
+ box-shadow: 0 4px 12px var(--annot-shadow);
278
+ z-index: 100;
279
+ }
280
+
281
+ .copy-dropdown-item {
282
+ display: block;
283
+ width: 100%;
284
+ padding: 8px 16px;
285
+ border: none;
286
+ background: transparent;
287
+ color: var(--annot-text-primary);
288
+ font-size: 13px;
289
+ text-align: left;
290
+ cursor: pointer;
291
+ white-space: nowrap;
292
+ }
293
+
294
+ .copy-dropdown-item:hover {
295
+ background: var(--annot-choice-hover);
296
+ color: var(--annot-accent);
297
+ }
298
+
299
+ /* Tool button with dropdown */
300
+ .tool-btn-wrap {
301
+ position: relative;
302
+ }
303
+
304
+ /* Note: the previous `.tool-dropdown-arrow` caret was removed when
305
+ * the variant badge took over the flyout-trigger role. Keeping the
306
+ * rules for `.tool-dropdown-menu` below because some other code
307
+ * paths may reuse the class name for non-toolbar popovers.
308
+ *
309
+ * The `.toolbar-vertical .tool-dropdown-menu` rule earlier in the
310
+ * file is a contextual override (different docking for vertical
311
+ * toolbars); the base styling lives here with the rest of the
312
+ * dropdown-menu rules, hence the descending-specificity suppression. */
313
+ /* biome-ignore lint/style/noDescendingSpecificity: base rule deliberately follows contextual .toolbar-vertical descendant */
314
+ .tool-dropdown-menu {
315
+ position: absolute;
316
+ top: 100%;
317
+ left: 0;
318
+ margin-top: 2px;
319
+ background: var(--annot-bg-panel-deep);
320
+ border: 1px solid var(--annot-border-color);
321
+ border-radius: 8px;
322
+ padding: 8px 10px;
323
+ display: flex;
324
+ flex-direction: column;
325
+ gap: 6px;
326
+ z-index: 200;
327
+ box-shadow: 0 4px 16px var(--annot-shadow);
328
+ min-width: 220px;
329
+ white-space: nowrap;
330
+ max-height: calc(100vh - 80px);
331
+ overflow-y: auto;
332
+ }
333
+
334
+ /* --- Tool flyout (variant picker opened from the ▼ arrow) ---
335
+ *
336
+ * Rendered into <body> with position: fixed so the popover escapes the
337
+ * toolbar / editor-header `overflow: hidden`. Placement (right/below)
338
+ * is computed in JS via getBoundingClientRect + viewport clamp.
339
+ */
340
+ .tool-flyout {
341
+ background: var(--annot-bg-panel-deep);
342
+ border: 1px solid var(--annot-border-color);
343
+ border-radius: 8px;
344
+ padding: 6px;
345
+ box-shadow: 0 6px 20px var(--annot-shadow);
346
+ display: flex;
347
+ gap: 4px;
348
+ }
349
+
350
+ /* Compact variant picker — tight row of icon chips. The chips are the
351
+ * primary hit target (parent-button click uses the last picked variant). */
352
+ .tool-flyout-variant .tool-flyout-row {
353
+ display: flex;
354
+ gap: 4px;
355
+ }
356
+
357
+ .tool-flyout-chip {
358
+ width: 32px;
359
+ height: 32px;
360
+ border: 1px solid transparent;
361
+ border-radius: 6px;
362
+ background: transparent;
363
+ color: var(--annot-text-primary);
364
+ font-size: 22px;
365
+ font-variation-settings:
366
+ "FILL" 0,
367
+ "wght" 500,
368
+ "GRAD" 0,
369
+ "opsz" 24;
370
+ cursor: pointer;
371
+ display: inline-flex;
372
+ align-items: center;
373
+ justify-content: center;
374
+ transition:
375
+ background 0.12s,
376
+ border-color 0.12s,
377
+ color 0.12s;
378
+ }
379
+
380
+ .tool-flyout-chip:hover {
381
+ /* Border on hover mirrors `.tool-flyout-color-chip:hover` and gives
382
+ * the user a clear "this row is targetable" signal even when the
383
+ * `--annot-hover-bg` token resolves to a very subtle tint (e.g.
384
+ * VSCode's `--vscode-list-hoverBackground`). Without the border
385
+ * the bg-only delta looked nearly identical to the inactive chip
386
+ * under VSCode's theme map. */
387
+ background: var(--annot-hover-bg);
388
+ border-color: var(--annot-accent);
389
+ color: var(--annot-accent);
390
+ }
391
+
392
+ .tool-flyout-chip.active {
393
+ /* "Filled" active state — solid `--annot-accent` background +
394
+ * contrasting glyph. The earlier "tinted bg + blue glyph" pattern
395
+ * (`--annot-choice-active` 18 % blue tint vs the hover state's
396
+ * 6-10 % tint) left active and hover too close to read apart at a
397
+ * glance, especially in PWA Light where the hover token resolves
398
+ * to `#eef1fa` and the active token to `#dce4ff` — both pale
399
+ * blues. Switching to a solid fill makes active visually dominant
400
+ * across themes:
401
+ *
402
+ * PWA dark — bg `#7c9cff` (light blue) + glyph `#121933` (navy) ≈ 9:1
403
+ * PWA light — bg `#3456c9` (dark blue) + glyph `#ffffff` (white) ≈ 9:1
404
+ * VSCode — bg button-bg + glyph button-foreground (overridden
405
+ * in the webview's inline `<style>` so the paired
406
+ * tokens hold ≥ 4.5:1 even when the workbench theme
407
+ * picks a low-contrast button-background).
408
+ *
409
+ * Hover keeps the outlined-with-blue-glyph look — a clear
410
+ * three-state hierarchy (inactive → outlined hover → filled
411
+ * active). */
412
+ background: var(--annot-accent);
413
+ border-color: var(--annot-accent);
414
+ color: var(--annot-bg-panel);
415
+ }
416
+
417
+ /* Chips whose glyph is an inline SVG (rather than a Material Symbols
418
+ * ligature). The SVG inherits `currentColor` from the chip, so all
419
+ * the color / hover / active rules above Just Work without extra
420
+ * selectors. Size matches the optical weight of the 22px
421
+ * material-symbols glyph next to it so mixed flyouts (e.g. rect + SVG
422
+ * and ellipse + font glyph) read as one row. */
423
+ .tool-flyout-chip-svg {
424
+ /* Same box + interaction rules as .tool-flyout-chip — mirrored here
425
+ * because the chip omits the material-symbols class. */
426
+ width: 32px;
427
+ height: 32px;
428
+ border: 1px solid transparent;
429
+ border-radius: 6px;
430
+ background: transparent;
431
+ color: var(--annot-text-primary);
432
+ cursor: pointer;
433
+ display: inline-flex;
434
+ align-items: center;
435
+ justify-content: center;
436
+ transition:
437
+ background 0.12s,
438
+ border-color 0.12s,
439
+ color 0.12s;
440
+ }
441
+ .tool-flyout-chip-svg:hover {
442
+ /* Match `.tool-flyout-chip:hover` — border for hover affordance
443
+ * even when `--annot-hover-bg` resolves to a very subtle tint. */
444
+ background: var(--annot-hover-bg);
445
+ border-color: var(--annot-accent);
446
+ color: var(--annot-accent);
447
+ }
448
+ .tool-flyout-chip-svg.active {
449
+ /* Match `.tool-flyout-chip.active` — see the comment there. */
450
+ background: var(--annot-accent);
451
+ border-color: var(--annot-accent);
452
+ color: var(--annot-bg-panel);
453
+ }
454
+ .tool-flyout-chip-svg > svg {
455
+ width: 22px;
456
+ height: 22px;
457
+ }
458
+
459
+ /* Highlight color picker — a tight row of 6 colored circle chips.
460
+ * Each chip's background IS the preset color; a ring indicates the
461
+ * currently-selected swatch so the user sees at a glance which color
462
+ * a bare click on the Highlight button will draw with. */
463
+ .tool-flyout-highlight .tool-flyout-color-row {
464
+ display: flex;
465
+ gap: 6px;
466
+ }
467
+
468
+ .tool-flyout-color-chip {
469
+ /* Same 32×32 frame + hover / active vocabulary as .tool-flyout-chip-svg
470
+ * (glyph chips) so Highlight color selection reads as one entry in
471
+ * the same Type-picker family. Color lives on the inner ::before
472
+ * swatch; the frame handles selection affordance via border / bg. */
473
+ width: 32px;
474
+ height: 32px;
475
+ border: 1px solid transparent;
476
+ border-radius: 6px;
477
+ background: transparent;
478
+ cursor: pointer;
479
+ padding: 0;
480
+ display: inline-flex;
481
+ align-items: center;
482
+ justify-content: center;
483
+ transition:
484
+ background 0.12s,
485
+ border-color 0.12s;
486
+ }
487
+
488
+ .tool-flyout-color-chip::before {
489
+ content: "";
490
+ display: block;
491
+ width: 18px;
492
+ height: 18px;
493
+ border-radius: 3px;
494
+ background: var(--swatch-color, transparent);
495
+ box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.12) inset;
496
+ }
497
+
498
+ .tool-flyout-color-chip:hover {
499
+ background: var(--annot-hover-bg);
500
+ border-color: var(--annot-accent);
501
+ }
502
+
503
+ .tool-flyout-color-chip.active {
504
+ /* Match `.tool-flyout-chip.active` — see the comment there. */
505
+ background: var(--annot-choice-active, var(--annot-choice-hover, var(--annot-hover-bg)));
506
+ border-color: var(--annot-accent);
507
+ }
508
+
509
+ /* Variant-bearing tool buttons need positioning context so the
510
+ * bottom-right badge and / or the highlight color bar can anchor
511
+ * themselves. Applied to all toolbar buttons because the cost is
512
+ * zero when no pseudo-element / child badge exists. */
513
+ .toolbar-btn {
514
+ position: relative;
515
+ }
516
+
517
+ /* Variant badge — small icon tucked into the bottom-right corner to
518
+ * indicate which sub-variant the next bare click on the main button
519
+ * will create. Serves DOUBLE DUTY as the flyout trigger: clicking
520
+ * the badge opens the variant picker (see #ensureBadge in
521
+ * toolbar.ts). The MAIN icon (tool identity) stays fixed; the badge
522
+ * alone mutates. Pattern-matched to Affinity / Photoshop's
523
+ * sub-variant indicator, where the in-button indicator IS the flyout
524
+ * affordance — no separate caret needed.
525
+ *
526
+ * Sizing / placement notes:
527
+ * - 16×16 visual bounds (slightly larger than the old 14×14 so
528
+ * the badge is easier to see AND hit).
529
+ * - pointer-events: auto so the badge is clickable. Parent button's
530
+ * text-node (the main icon) stays the click target for everything
531
+ * EXCEPT the badge zone — so main click activates tool, badge
532
+ * click opens flyout.
533
+ * - Hover state (ring + slight zoom) signals interactivity. */
534
+ .tool-btn-badge {
535
+ position: absolute;
536
+ right: -2px;
537
+ bottom: -2px;
538
+ width: 12px;
539
+ height: 12px;
540
+ display: flex;
541
+ align-items: center;
542
+ justify-content: center;
543
+ font-size: 10px;
544
+ line-height: 1;
545
+ color: var(--annot-accent);
546
+ background: var(--annot-bg-panel);
547
+ border-radius: 50%;
548
+ box-shadow: 0 0 0 1.5px var(--annot-bg-panel);
549
+ pointer-events: auto;
550
+ cursor: pointer;
551
+ /* Visual badge stays small so it floats outside the main icon's
552
+ * visual bounds (right/bottom -2px) without occluding it. The
553
+ * `::before` rule below extends the hit area back out to the
554
+ * button corner so clickability matches the larger 16×16 era. */
555
+ padding: 0;
556
+ border: none;
557
+ transition:
558
+ transform 0.12s,
559
+ box-shadow 0.12s;
560
+ font-variation-settings:
561
+ "FILL" 1,
562
+ "wght" 600,
563
+ "GRAD" 0,
564
+ "opsz" 20;
565
+ }
566
+
567
+ .tool-btn-badge:hover {
568
+ transform: scale(1.15);
569
+ box-shadow:
570
+ 0 0 0 1.5px var(--annot-bg-panel),
571
+ 0 0 0 3px var(--annot-accent);
572
+ }
573
+
574
+ /* Expand the effective click zone beyond the visual circle without
575
+ * affecting layout. An invisible ::before covers the bottom-right
576
+ * quadrant of the parent button; the badge remains absolutely
577
+ * positioned within it. Users can click anywhere in that quadrant
578
+ * and hit the badge — forgiving for touch + hover-and-click. */
579
+ .tool-btn-badge::before {
580
+ content: "";
581
+ position: absolute;
582
+ inset: -6px;
583
+ border-radius: 50%;
584
+ }
585
+
586
+ /* When the parent button is active (tool selected), accentuate the
587
+ * badge so it stays visible against the active-state background. */
588
+ /* Active-state badge: blend the badge frame with the active button
589
+ * background so the badge feels integral to the active tool rather
590
+ * than a separate panel-coloured callout. The original token name
591
+ * was `--bg-active` but no such variable existed (typo of
592
+ * `--annot-active-bg`), so until now the rule fell through to
593
+ * `--annot-bg-panel` and the badge stayed panel-coloured under
594
+ * every state. Pointing at the real `--annot-active-bg` token
595
+ * restores the intended "blend" look:
596
+ *
597
+ * PWA dark — rgba(124,156,255,0.18) tint over the active
598
+ * button background; blue glyph reads fine.
599
+ * PWA light — pale-blue active tint; blue glyph reads fine.
600
+ * VSCode — list-activeSelectionBackground (strong selection
601
+ * blue); the `.tool-btn-badge` glyph stays
602
+ * `var(--annot-accent)` (also blue) and would
603
+ * vanish, so the inline VSCode override in
604
+ * `packages/vscode/src/webview/index.html` switches
605
+ * the active-badge foreground to
606
+ * `--vscode-list-activeSelectionForeground`. */
607
+ .toolbar-btn.active .tool-btn-badge {
608
+ background: var(--annot-active-bg, var(--annot-bg-panel));
609
+ box-shadow: 0 0 0 1.5px var(--annot-active-bg, var(--annot-bg-panel));
610
+ }
611
+ .toolbar-btn.active .tool-btn-badge:hover {
612
+ box-shadow:
613
+ 0 0 0 1.5px var(--annot-active-bg, var(--annot-bg-panel)),
614
+ 0 0 0 3px var(--annot-accent);
615
+ }
616
+
617
+ /* SVG-based badge (for rect / rounded variants where Material Symbols
618
+ * don't give enough visual distinction). Shares positioning + chrome
619
+ * with .tool-btn-badge, just swaps ligature font rendering for an
620
+ * inline SVG. Size scales with the badge's 16px bounds so visual
621
+ * weight matches the ligature glyphs next to it. */
622
+ .tool-btn-badge-svg > svg {
623
+ width: 10px;
624
+ height: 10px;
625
+ color: var(--annot-accent);
626
+ stroke: currentColor;
627
+ }
628
+
629
+ /* Highlight tool badge — circular panel frame containing a rounded-
630
+ * square color swatch. The outer circle keeps the badge shape
631
+ * consistent with every other tool's variant badge (shape/arrow/
632
+ * text/marker/redact all get a circle in the bottom-right); the
633
+ * inner square mirrors the Selection-panel / flyout swatch chips so
634
+ * the "color = variant" reading lines up visually across all three
635
+ * surfaces. `--swatch-color` is set inline by #syncToolButtonIcon. */
636
+ .tool-btn-badge-color {
637
+ /* Shares the variant-badge sizing + offset (12×12 floating outside
638
+ * the icon at right/bottom -2px) so the toolbar reads consistently
639
+ * across all tools. Only the inner swatch chip differs. */
640
+ font-size: 0;
641
+ color: transparent;
642
+ /* Inherits background (panel color) + border-radius (50%) from
643
+ * .tool-btn-badge. Nudge the outer ring thicker so light colors
644
+ * stay visible against the panel. */
645
+ box-shadow: 0 0 0 1.5px var(--annot-bg-panel);
646
+ }
647
+
648
+ .tool-btn-badge-color::after {
649
+ /* Inner rounded square showing the current highlight color. Sized
650
+ * to fit the 12×12 frame with a thin ring of badge-bg visible all
651
+ * around. */
652
+ content: "";
653
+ display: block;
654
+ width: 8px;
655
+ height: 8px;
656
+ border-radius: 2px;
657
+ background: var(--swatch-color, transparent);
658
+ box-shadow: 0 0 0 0.5px rgba(0, 0, 0, 0.25);
659
+ }
660
+
661
+ .toolbar-btn.active .tool-btn-badge-color {
662
+ box-shadow: 0 0 0 1.5px var(--annot-active-bg, var(--annot-bg-panel));
663
+ }
664
+
665
+ /* Larger popover used by the Scratchpad button — holds a scrollable
666
+ * thumbnail grid plus a save-selection action. */
667
+ .tool-flyout-scratchpad {
668
+ flex-direction: column;
669
+ gap: 8px;
670
+ width: 264px;
671
+ max-height: min(420px, calc(100vh - 40px));
672
+ padding: 10px;
673
+ }