@aceshooting/lyra-ui 11.2.0 → 12.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 (222) hide show
  1. package/CHANGELOG.md +407 -0
  2. package/README.md +11 -8
  3. package/custom-elements.json +1 -1
  4. package/dist/all.d.ts +1 -0
  5. package/dist/all.js +1 -1
  6. package/dist/autoloader.js +1 -1
  7. package/dist/components/agent-tools/tool-param-form/tool-param-form.class.d.ts +0 -1
  8. package/dist/components/agent-tools/tool-param-form/tool-param-form.class.js +1 -1
  9. package/dist/components/charts/chart/chart.class.d.ts +13 -1
  10. package/dist/components/charts/chart/histogram-bin.d.ts +0 -1
  11. package/dist/components/charts/chart/histogram-bin.js +1 -1
  12. package/dist/components/charts/chart/lite-chart.class.d.ts +22 -9
  13. package/dist/components/charts/chart/lite-chart.class.js +6 -6
  14. package/dist/components/conversation/code-block/code-block-shared.d.ts +4 -54
  15. package/dist/components/conversation/code-block/code-block-shared.js +1 -1
  16. package/dist/components/conversation/code-block/shiki-types.d.ts +2 -1
  17. package/dist/components/conversation/generation-metrics/generation-metrics.class.js +1 -1
  18. package/dist/components/conversation/model-select/model-select.class.d.ts +0 -3
  19. package/dist/components/conversation/model-select/model-select.class.js +1 -1
  20. package/dist/components/conversation/selection-toolbar/selection-toolbar.class.js +1 -1
  21. package/dist/components/conversation/voice-picker/voice-picker.class.d.ts +0 -1
  22. package/dist/components/conversation/voice-picker/voice-picker.class.js +1 -1
  23. package/dist/components/data/data-grid/data-grid.class.js +1 -1
  24. package/dist/components/data/funnel/funnel.class.d.ts +92 -0
  25. package/dist/components/data/funnel/funnel.class.js +25 -0
  26. package/dist/components/data/funnel/funnel.d.ts +1 -0
  27. package/dist/components/data/funnel/funnel.js +1 -0
  28. package/dist/components/data/funnel/funnel.styles.d.ts +1 -0
  29. package/dist/components/data/funnel/funnel.styles.js +127 -0
  30. package/dist/components/data/graph-query-builder/graph-query-builder.class.d.ts +0 -1
  31. package/dist/components/data/graph-query-builder/graph-query-builder.class.js +1 -1
  32. package/dist/components/data/heatmap/heatmap.class.d.ts +199 -14
  33. package/dist/components/data/heatmap/heatmap.class.js +30 -22
  34. package/dist/components/data/heatmap/heatmap.styles.js +36 -0
  35. package/dist/components/data/index.d.ts +1 -0
  36. package/dist/components/data/index.js +1 -1
  37. package/dist/components/data/table/table.class.d.ts +18 -4
  38. package/dist/components/data/table/table.class.js +2 -2
  39. package/dist/components/data/table/table.styles.js +2 -1
  40. package/dist/components/data/timeline/timeline-cluster.d.ts +1 -0
  41. package/dist/components/data/timeline/timeline-cluster.js +1 -0
  42. package/dist/components/data/timeline/timeline-item.class.d.ts +17 -2
  43. package/dist/components/data/timeline/timeline-item.class.js +13 -2
  44. package/dist/components/data/timeline/timeline-item.styles.js +65 -0
  45. package/dist/components/data/timeline/timeline.class.d.ts +83 -27
  46. package/dist/components/data/timeline/timeline.class.js +3 -3
  47. package/dist/components/data/timeline/timeline.d.ts +1 -0
  48. package/dist/components/data/timeline/timeline.js +1 -1
  49. package/dist/components/data/timeline/timeline.styles.js +14 -0
  50. package/dist/components/forms/button/button.class.js +1 -1
  51. package/dist/components/forms/checkbox/checkbox.class.d.ts +0 -1
  52. package/dist/components/forms/checkbox/checkbox.class.js +1 -1
  53. package/dist/components/forms/checkbox-group/checkbox-group.class.d.ts +0 -1
  54. package/dist/components/forms/checkbox-group/checkbox-group.class.js +1 -1
  55. package/dist/components/forms/code-editor/code-editor.class.js +1 -1
  56. package/dist/components/forms/color-picker/color-picker.class.js +1 -1
  57. package/dist/components/forms/combobox/combobox.class.d.ts +7 -1
  58. package/dist/components/forms/combobox/combobox.class.js +3 -2
  59. package/dist/components/forms/combobox/combobox.styles.js +1 -1
  60. package/dist/components/forms/date-picker/date-input.class.d.ts +23 -0
  61. package/dist/components/forms/date-picker/date-input.class.js +1 -1
  62. package/dist/components/forms/emoji-picker/emoji-data-loader.d.ts +0 -1
  63. package/dist/components/forms/input/native-time-input.class.js +1 -1
  64. package/dist/components/forms/input/time-input-shared.d.ts +2 -1
  65. package/dist/components/forms/input/time-input.class.js +1 -1
  66. package/dist/components/forms/locale-picker/locale-picker.class.d.ts +0 -1
  67. package/dist/components/forms/locale-picker/locale-picker.class.js +1 -1
  68. package/dist/components/forms/radio/radio-button.styles.d.ts +0 -1
  69. package/dist/components/forms/radio/radio-button.styles.js +1 -1
  70. package/dist/components/forms/radio/radio-group.class.d.ts +0 -1
  71. package/dist/components/forms/radio/radio-group.class.js +1 -1
  72. package/dist/components/forms/radio/radio.class.d.ts +0 -1
  73. package/dist/components/forms/radio/radio.class.js +1 -1
  74. package/dist/components/forms/rubric-form/rubric-form.class.d.ts +0 -1
  75. package/dist/components/forms/rubric-form/rubric-form.class.js +1 -1
  76. package/dist/components/forms/select/select.class.d.ts +0 -1
  77. package/dist/components/forms/select/select.class.js +1 -1
  78. package/dist/components/forms/slider/slider.class.d.ts +0 -1
  79. package/dist/components/forms/slider/slider.class.js +1 -1
  80. package/dist/components/forms/switch/switch.class.d.ts +0 -1
  81. package/dist/components/forms/switch/switch.class.js +1 -1
  82. package/dist/components/forms/time-range/time-range.class.d.ts +0 -1
  83. package/dist/components/forms/time-range/time-range.class.js +1 -1
  84. package/dist/components/forms/token-input/token-input.class.d.ts +0 -1
  85. package/dist/components/forms/token-input/token-input.class.js +1 -1
  86. package/dist/components/layout/app-rail/app-rail-item.class.d.ts +16 -0
  87. package/dist/components/layout/app-rail/app-rail-item.class.js +4 -4
  88. package/dist/components/layout/dashboard-grid/layout-internal.d.ts +0 -1
  89. package/dist/components/layout/dashboard-grid/layout-internal.js +1 -1
  90. package/dist/components/layout/details/accordion-owner.d.ts +1 -1
  91. package/dist/components/layout/filter-bar/filter-bar.class.d.ts +32 -3
  92. package/dist/components/layout/filter-bar/filter-bar.class.js +2 -1
  93. package/dist/components/layout/menu/menu-item.class.js +1 -1
  94. package/dist/components/layout/virtual-list/virtual-list.class.js +1 -1
  95. package/dist/components/layout/widget/widget.class.d.ts +16 -0
  96. package/dist/components/layout/widget/widget.class.js +2 -2
  97. package/dist/components/lr-funnel.d.ts +1 -0
  98. package/dist/components/lr-funnel.js +1 -0
  99. package/dist/components/media/av-player/av-metadata.d.ts +0 -2
  100. package/dist/components/media/av-player/av-metadata.js +1 -1
  101. package/dist/components/media/file-input/file-input.class.d.ts +0 -1
  102. package/dist/components/media/file-input/file-input.class.js +1 -1
  103. package/dist/components/media/flag/flag-peer-bulk-standard.d.ts +25 -0
  104. package/dist/components/media/flag/flag-peer-bulk-standard.js +1 -0
  105. package/dist/components/media/flag/flag.class.d.ts +8 -0
  106. package/dist/components/media/flag/flag.class.js +1 -1
  107. package/dist/components/media/map/map-loader.d.ts +4 -1
  108. package/dist/components/media/map/map.class.d.ts +242 -23
  109. package/dist/components/media/map/map.class.js +2 -2
  110. package/dist/components/overlays/chip/chip-group.class.d.ts +16 -0
  111. package/dist/components/overlays/chip/chip-group.class.js +10 -3
  112. package/dist/components/overlays/overlay/popover.class.d.ts +7 -1
  113. package/dist/components/overlays/overlay/popover.class.js +1 -1
  114. package/dist/components/overlays/overlay/tooltip.class.d.ts +1 -0
  115. package/dist/components/overlays/overlay/tooltip.class.js +1 -1
  116. package/dist/components/overlays/progress/progress-shared.d.ts +0 -2
  117. package/dist/components/overlays/progress/progress-shared.js +1 -1
  118. package/dist/components/overlays/rating/rating.class.js +1 -1
  119. package/dist/components/retrieval/graph/graph-canvas.d.ts +10 -9
  120. package/dist/components/retrieval/mind-map/mind-map-layout.d.ts +0 -4
  121. package/dist/components/retrieval/mind-map/mind-map-layout.js +1 -1
  122. package/dist/components/utility/format/format-options.d.ts +0 -2
  123. package/dist/components/utility/format/format-options.js +1 -1
  124. package/dist/components/utility/icon/icon-resource.d.ts +0 -2
  125. package/dist/components/utility/icon/icon-resource.js +1 -1
  126. package/dist/components/viewers/archive-viewer/zip-resource-guard.d.ts +2 -1
  127. package/dist/components/viewers/docx-viewer/docx-resource-guard.d.ts +0 -3
  128. package/dist/components/viewers/docx-viewer/docx-resource-guard.js +1 -1
  129. package/dist/components/viewers/ebook-viewer/ebook-loader.d.ts +2 -1
  130. package/dist/components/viewers/ebook-viewer/epub-resource-guard.d.ts +0 -3
  131. package/dist/components/viewers/ebook-viewer/epub-resource-guard.js +1 -1
  132. package/dist/components/viewers/pdf-viewer/pdf-loader.d.ts +2 -2
  133. package/dist/components/viewers/pdf-viewer/pdf-viewer.class.js +2 -2
  134. package/dist/components/viewers/pptx-viewer/pptx-resource-guard.d.ts +0 -2
  135. package/dist/components/viewers/pptx-viewer/pptx-resource-guard.js +1 -1
  136. package/dist/components/viewers/spreadsheet-viewer/xlsx-resource-guard.d.ts +0 -5
  137. package/dist/components/viewers/spreadsheet-viewer/xlsx-resource-guard.js +1 -1
  138. package/dist/custom-elements-jsx.d.ts +16 -8
  139. package/dist/events.d.ts +15 -0
  140. package/dist/internal/accessibility-visibility.d.ts +1 -1
  141. package/dist/internal/anchor-target.d.ts +1 -3
  142. package/dist/internal/anchor-target.js +2 -2
  143. package/dist/internal/anchored-overlay-runtime.d.ts +7 -0
  144. package/dist/internal/anchored-overlay-runtime.js +1 -0
  145. package/dist/internal/announcement-text.d.ts +1 -1
  146. package/dist/internal/ansi.d.ts +0 -4
  147. package/dist/internal/ansi.js +1 -1
  148. package/dist/internal/aria-ownership.d.ts +2 -1
  149. package/dist/internal/autoloader-manifest.js +1 -1
  150. package/dist/internal/autoloader-tags.d.ts +1 -1
  151. package/dist/internal/autoloader-tags.js +1 -1
  152. package/dist/internal/default-strings.generated.d.ts +1 -0
  153. package/dist/internal/default-strings.generated.js +1 -1
  154. package/dist/internal/focus-navigation.d.ts +3 -2
  155. package/dist/internal/form-associated.d.ts +0 -1
  156. package/dist/internal/form-associated.js +1 -1
  157. package/dist/internal/form-control-labels.d.ts +0 -3
  158. package/dist/internal/form-control-labels.js +1 -1
  159. package/dist/internal/localization-runtime.js +1 -1
  160. package/dist/internal/localization-types.d.ts +1 -1
  161. package/dist/internal/localization.js +1 -1
  162. package/dist/internal/lyra-element.d.ts +7 -14
  163. package/dist/internal/lyra-element.js +1 -1
  164. package/dist/internal/nonmodal-overlay-manager.d.ts +9 -0
  165. package/dist/internal/nonmodal-overlay-manager.js +1 -0
  166. package/dist/internal/overlay-manager.d.ts +4 -20
  167. package/dist/internal/overlay-manager.js +1 -1
  168. package/dist/internal/overlay-stack.d.ts +66 -0
  169. package/dist/internal/overlay-stack.js +1 -0
  170. package/dist/internal/package-metadata.d.ts +1 -1
  171. package/dist/internal/package-metadata.js +1 -1
  172. package/dist/internal/positioner.js +1 -1
  173. package/dist/internal/root-registration-allowlist.d.ts +1 -1
  174. package/dist/internal/root-registration-allowlist.js +1 -1
  175. package/dist/internal/text-quote.d.ts +1 -1
  176. package/dist/lyra.d.ts +5 -4
  177. package/dist/lyra.js +1 -1
  178. package/dist/ssr/all.d.ts +1 -0
  179. package/dist/ssr/all.js +1 -1
  180. package/dist/ssr.d.ts +4 -4
  181. package/dist/styles/tokens-root.css +488 -0
  182. package/dist/svelte.d.ts +17 -8
  183. package/dist/translations/ar.js +1 -1
  184. package/dist/translations/de.js +1 -1
  185. package/dist/translations/es.js +1 -1
  186. package/dist/translations/fa.js +1 -1
  187. package/dist/translations/fr.js +1 -1
  188. package/dist/translations/he.js +1 -1
  189. package/dist/translations/ja.js +1 -1
  190. package/dist/translations/pt-BR.js +1 -1
  191. package/dist/translations/ru.js +1 -1
  192. package/dist/translations/zh-CN.js +1 -1
  193. package/dist/utilities/localization.js +1 -1
  194. package/dist/vue.d.ts +16 -8
  195. package/llms/components/lr-app-rail-item.md +8 -1
  196. package/llms/components/lr-app-rail.md +6 -0
  197. package/llms/components/lr-chip-group.md +7 -0
  198. package/llms/components/lr-chip.md +7 -0
  199. package/llms/components/lr-combobox.md +42 -0
  200. package/llms/components/lr-date-input.md +19 -1
  201. package/llms/components/lr-date-picker.md +19 -1
  202. package/llms/components/lr-filter-bar.md +50 -1
  203. package/llms/components/lr-flag.md +14 -0
  204. package/llms/components/lr-funnel.md +122 -0
  205. package/llms/components/lr-heatmap.md +55 -8
  206. package/llms/components/lr-lite-chart.md +20 -8
  207. package/llms/components/lr-map.md +109 -22
  208. package/llms/components/lr-option.md +42 -0
  209. package/llms/components/lr-popover.md +39 -0
  210. package/llms/components/lr-table.md +7 -3
  211. package/llms/components/lr-timeline-item.md +49 -24
  212. package/llms/components/lr-timeline.md +49 -24
  213. package/llms/components/lr-widget.md +7 -1
  214. package/llms/index.md +3 -2
  215. package/llms/peers.md +1 -1
  216. package/llms/shared.md +110 -1
  217. package/llms-full.txt +681 -67
  218. package/llms.txt +1 -1
  219. package/package.json +26 -8
  220. package/vscode-css-data.json +63 -0
  221. package/vscode-html-data.json +108 -23
  222. package/web-types.json +32738 -310
package/llms-full.txt CHANGED
@@ -34,7 +34,7 @@ family and entry point.
34
34
 
35
35
  - **Foundation** — base class, form association, events, i18n, theming, TypeScript, frameworks, SSR, packaging (`llms/shared.md`)
36
36
  - **Form controls & inputs** — 29 tags in `components/forms/`
37
- - **Data display, dashboards & flow canvas** — 25 tags in `components/data/`
37
+ - **Data display, dashboards & flow canvas** — 26 tags in `components/data/`
38
38
  - **Layout, navigation & structure** — 35 tags in `components/layout/`
39
39
  - **Overlays, status & feedback** — 21 tags in `components/overlays/`
40
40
  - **Utilities, formatting & observers** — 20 tags in `components/utility/`
@@ -202,6 +202,8 @@ The entry points, then:
202
202
  a pre-resolved `src`) additionally needs
203
203
  `import '@aceshooting/lyra-ui/components/media/flag/flag-peer.js';` once.
204
204
  - **Other subpaths.** `@aceshooting/lyra-ui/theme.css` (ready-made light/dark theme),
205
+ `@aceshooting/lyra-ui/tokens-root.css` (opt-in: the curated resolved `--lr-*` tokens at `:root`,
206
+ so your own components can read them),
205
207
  `@aceshooting/lyra-ui/native.css` (opt-in native-element styles inside `.lr-native`),
206
208
  `@aceshooting/lyra-ui/utilities.css` (opt-in light-DOM layout/text utilities),
207
209
  `@aceshooting/lyra-ui/theme.js` (the zero-dependency mode/accent runtime),
@@ -696,10 +698,110 @@ reaches plain application CSS, and it never reaches your own custom elements, si
696
698
  descendant of an `lr-*` shadow root — `body { color: var(--lr-color-text) }` in application CSS
697
699
  resolves to nothing, silently, not an error. Retheme through layer 1 (`--lr-theme-*`), which
698
700
  `theme.css` supplies at document scope and which inherits normally into every nested shadow root.
701
+ To _read_ (not retheme) the resolved values from your own components, import the opt-in
702
+ [`tokens-root.css`](#reading-the-resolved-tokens-from-your-own-components--tokens-rootcss), which
703
+ declares a curated subset of layer 2 at `:root`.
699
704
  See [Where an override actually reaches](#where-an-override-actually-reaches) below for the full
700
705
  inheritance rules, including the one documented exception (per-component `--lr-<component>-*`
701
706
  hooks, layer 3, which do inherit through wrappers).
702
707
 
708
+ ### Reading the resolved tokens from your own components — `tokens-root.css`
709
+
710
+ The paragraph above is a real problem for any application that has custom elements of its own: they
711
+ are not descendants of an `lr-*` shadow root either, so `var(--lr-color-border)` inside **your**
712
+ component resolves to nothing, and `var(--lr-space-m, 0.5rem)` quietly runs on its literal fallback
713
+ forever. Both failures are invisible without reading computed styles in a browser.
714
+
715
+ Import one optional stylesheet and the curated part of layer 2 exists at document scope:
716
+
717
+ ```css
718
+ @import "@aceshooting/lyra-ui/theme.css"; /* the --lr-theme-* input layer */
719
+ @import "@aceshooting/lyra-ui/tokens-root.css"; /* the resolved --lr-* layer, at :root */
720
+ ```
721
+
722
+ ```css
723
+ /* Now valid in your own component's stylesheet, in plain application CSS, anywhere. */
724
+ .app-panel {
725
+ padding: var(--lr-space-m);
726
+ border: var(--lr-border-width-thin) solid var(--lr-color-border);
727
+ border-radius: var(--lr-radius);
728
+ background: var(--lr-color-surface-raised);
729
+ color: var(--lr-color-text);
730
+ font-family: var(--lr-font);
731
+ }
732
+ .app-panel:focus-visible {
733
+ outline: var(--lr-focus-ring);
734
+ outline-offset: var(--lr-focus-ring-offset);
735
+ }
736
+ ```
737
+
738
+ **It is opt-in, and it is a curated subset — not all of layer 2.** `--lr-*` is internal precisely so
739
+ it can change without a major version; publishing all of it at `:root` would freeze several hundred
740
+ internal decisions as permanent API. What ships is what an application's own component needs to sit
741
+ inside a Lyra UI without looking foreign:
742
+
743
+ | Family | Names |
744
+ | --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
745
+ | Ambient colour | `--lr-color-surface`, `--lr-color-surface-raised`, `--lr-color-surface-overlay`, `--lr-color-overlay`, `--lr-color-text`, `--lr-color-text-quiet`, `--lr-color-border`, `--lr-color-border-strong` |
746
+ | The semantic grid | all 45 `--lr-color-{brand,success,warning,danger,neutral}-{fill,border,on}-{quiet,normal,loud}` slots |
747
+ | Flat colour aliases | `--lr-color-{brand,success,warning,danger,neutral}`, `--lr-color-{brand,success,warning,danger}-quiet`, `--lr-color-on-{brand,success,warning,danger,neutral}` |
748
+ | Spacing | `--lr-space-2xs`, `--lr-space-xs`, `--lr-space-s`, `--lr-space-m`, `--lr-space-l`, `--lr-space-2xl` |
749
+ | Geometry | `--lr-radius-xs`, `--lr-radius`, `--lr-radius-pill`, `--lr-border-width-thin`, `--lr-border-width-medium`, `--lr-border-width-thick` |
750
+ | Elevation | `--lr-shadow-color`, `--lr-shadow-xs`, `--lr-shadow-s`, `--lr-shadow-m`, `--lr-shadow-l`, `--lr-shadow-xl`, `--lr-shadow` |
751
+ | Typography | `--lr-font`, `--lr-font-mono`, the ten `--lr-font-size-*` steps, the four `--lr-font-weight-*` steps |
752
+ | State and motion | `--lr-focus-ring`, `--lr-focus-ring-color`, `--lr-focus-ring-width`, `--lr-focus-ring-offset`, `--lr-opacity-disabled`, `--lr-opacity-muted`, `--lr-duration-fast`, `--lr-duration-base`, `--lr-easing-standard`, `--lr-easing-emphasized`, `--lr-transition-fast`, `--lr-transition-base` |
753
+
754
+ The grid ships whole because its contrast guarantee is **per tier** — a `fill-quiet` background is
755
+ only guaranteed legible under the matching `on-quiet` foreground — so shipping the flat aliases
756
+ alone would hand you a pairing with nothing behind it.
757
+
758
+ **Deliberately not published**, and each for a reason that makes reading it a bug rather than a
759
+ convenience: `--lr-ramp-*` (a step encodes a light-mode choice and has no theme hook),
760
+ `--lr-size-*` (value-named geometry constants, frozen internals), the chart, graph and terminal
761
+ palettes (generated ramps that move with the palette tooling), `--lr-layer-*` (stacking order is
762
+ your decision), `--lr-color-mix-*` and `--lr-hover-brightness` (inputs to the library's own
763
+ interaction recipe), `--lr-line-height-*`, the per-control internals (`--lr-icon-button-size`,
764
+ `--lr-otp-input-segment-size`, `--lr-scroll-fade-size`, `--lr-popover-viewport-clamp`,
765
+ `--lr-safe-area-*`, `--lr-mask-opaque`, `--lr-color-no-data`), and the nine variant-following slots
766
+ (`--lr-color-fill-loud` and friends), which mean "the variant _this_ element is set to" and are
767
+ meaningless on `:root`. If you need one of these, ask for it to be added rather than reading it out
768
+ of a component's shadow root.
769
+
770
+ **Stability promise.** Every name in the table is public API from the release that introduced it: it
771
+ will not be renamed or removed outside a major version, and its meaning will not change. Its _value_
772
+ may change in a minor exactly as it may inside a component — a palette retune moves your elements
773
+ and the kit's together, which is the point. Names absent from the file stay internal and may change
774
+ in any release.
775
+
776
+ **Modes work the way the components' do.** Light on `:root` and on `.lr-light` /
777
+ `[data-lr-theme="light"]`, dark under `prefers-color-scheme: dark` (unless an explicit light scope
778
+ opts out) and on `.lr-dark` / `[data-lr-theme="dark"]`, plus the same `forced-colors` and
779
+ `prefers-reduced-motion` overrides the components apply — and those two reach you on **every** one
780
+ of those routes, including a dark OS with no explicit scope, because they repeat the compound
781
+ `:root:not(.lr-light):not([data-lr-theme="light"])` selector the dark route uses rather than a bare
782
+ `:root` that the dark route would out-specify. Every declaration keeps its `--lr-theme-*`
783
+ input in front of a resolved fallback, so `theme.css` and your own overrides still win here exactly
784
+ as they do inside a component.
785
+
786
+ **One caveat, and it is the same shape as layer 2's rule everywhere else.** A `--lr-theme-*` input
787
+ set on a mid-tree element retunes every `lr-*` component below it, because each component re-derives
788
+ the resolved layer on its own `:host`. The document-scope copy cannot: it is substituted where it is
789
+ declared, and what inherits past that point is the finished value. So if an application element
790
+ carries a subtree override and expects its **own** descendants to follow, give that element a mode
791
+ scope too — `class="lr-light"`, `class="lr-dark"`, or `data-lr-theme` — which is what the file's
792
+ mode rules key on, and the whole subset resolves again there:
793
+
794
+ ```html
795
+ <!-- Both the lr-* components and the app's own elements below follow the override. -->
796
+ <section class="lr-light" style="--lr-theme-color-brand-fill-loud: #7c3aed">…</section>
797
+ ```
798
+
799
+ Everything sits in the `lr-theme` cascade layer, like `theme.css`, so any unlayered application rule
800
+ beats it regardless of load order, and the file declares custom properties only — notably not
801
+ `color-scheme` — so importing it paints nothing by itself. `--lr-focus-ring` and its three parts are
802
+ also declared at document scope by `theme.css`; both spell the same chain, so importing both is a
803
+ no-op either way round.
804
+
703
805
  ### The colour ramp and the semantic grid
704
806
 
705
807
  Colour has two layers beneath the `--lr-*` tokens you normally read.
@@ -1246,7 +1348,10 @@ stay in light DOM: they do not pierce a component's shadow root.
1246
1348
  `utilities.css` places exact, zero-specificity `:where(.lr-*)` classes in `lr-utilities`. It never
1247
1349
  uses a substring class selector, so a class such as `app-lr-flex-preview` does not opt in. Both
1248
1350
  assets repeat `@layer lr-base, lr-theme, lr-utilities, lr-overrides`; an ordinary unlayered
1249
- application rule therefore beats them regardless of load order.
1351
+ application rule therefore beats them regardless of load order. A third opt-in asset,
1352
+ [`tokens-root.css`](#reading-the-resolved-tokens-from-your-own-components--tokens-rootcss), is not a
1353
+ style sheet in the same sense — it declares custom properties only, and exists so your own
1354
+ components can read the resolved `--lr-*` tokens these two are written against.
1250
1355
 
1251
1356
  ### Utility class inventory
1252
1357
 
@@ -2757,6 +2862,7 @@ These named interfaces and helper signatures are available to typed integrations
2757
2862
  "lr-format-bytes": unknown;
2758
2863
  "lr-format-date": unknown;
2759
2864
  "lr-format-number": unknown;
2865
+ "lr-funnel": unknown;
2760
2866
  "lr-gauge": unknown;
2761
2867
  "lr-generation-metrics": unknown;
2762
2868
  "lr-geojson-view": unknown;
@@ -3142,6 +3248,7 @@ These named interfaces and helper signatures are available to typed integrations
3142
3248
  "lr-format-bytes": unknown;
3143
3249
  "lr-format-date": unknown;
3144
3250
  "lr-format-number": unknown;
3251
+ "lr-funnel": unknown;
3145
3252
  "lr-gauge": unknown;
3146
3253
  "lr-generation-metrics": unknown;
3147
3254
  "lr-geojson-view": unknown;
@@ -3428,6 +3535,7 @@ These named interfaces and helper signatures are available to typed integrations
3428
3535
  "lr-format-bytes": unknown;
3429
3536
  "lr-format-date": unknown;
3430
3537
  "lr-format-number": unknown;
3538
+ "lr-funnel": unknown;
3431
3539
  "lr-gauge": unknown;
3432
3540
  "lr-generation-metrics": unknown;
3433
3541
  "lr-geojson-view": unknown;
@@ -3754,6 +3862,7 @@ These named interfaces and helper signatures are available to typed integrations
3754
3862
  "lr-format-bytes": unknown;
3755
3863
  "lr-format-date": unknown;
3756
3864
  "lr-format-number": unknown;
3865
+ "lr-funnel": unknown;
3757
3866
  "lr-gauge": unknown;
3758
3867
  "lr-generation-metrics": unknown;
3759
3868
  "lr-geojson-view": unknown;
@@ -4056,6 +4165,48 @@ Filterable single/multi-select combining a text input with a listbox. Mirrors th
4056
4165
  `<wa-combobox>` API under the `lr-` prefix. **Form-associated** (hand-rolled internals, not the
4057
4166
  shared `FormAssociated` mixin — see gotchas).
4058
4167
 
4168
+ **First-interaction registration.** Where initial-route weight is stricter than a static combobox
4169
+ registration allows, keep a labelled native `<input list>` as the working pre-JavaScript control
4170
+ and import only the granular combobox registration on its first focus. Copy the native value after
4171
+ the import resolves so typing that happens while the chunk is in flight is not lost, then transfer
4172
+ focus explicitly — the browser does not replay the focus event after custom-element upgrade:
4173
+
4174
+ ```html
4175
+ <div id="country-fallback">
4176
+ <label for="country-native">Country</label>
4177
+ <input id="country-native" name="country" list="country-options">
4178
+ <datalist id="country-options"><option value="France"></option></datalist>
4179
+ </div>
4180
+ <lr-combobox id="country-enhanced" name="country" label="Country" hidden>
4181
+ <lr-option value="France">France</lr-option>
4182
+ </lr-combobox>
4183
+ <script type="module">
4184
+ const fallback = document.querySelector("#country-fallback");
4185
+ const input = document.querySelector("#country-native");
4186
+ const combobox = document.querySelector("#country-enhanced");
4187
+ let registration;
4188
+ input.addEventListener("focus", async () => {
4189
+ if (!combobox.hidden) return;
4190
+ registration ??= import(
4191
+ "@aceshooting/lyra-ui/components/forms/combobox/combobox.js"
4192
+ ).catch((error) => {
4193
+ registration = undefined; // let a later interaction retry
4194
+ throw error;
4195
+ });
4196
+ await registration;
4197
+ await customElements.whenDefined("lr-combobox");
4198
+ combobox.value = input.value;
4199
+ fallback.hidden = true;
4200
+ combobox.hidden = false;
4201
+ combobox.focus();
4202
+ });
4203
+ </script>
4204
+ ```
4205
+
4206
+ Leave the native control in place if registration fails. This pattern preserves the initial shell;
4207
+ the full form-label, option, overlay, and first-open positioning contracts arrive in deferred
4208
+ chunks instead of being weakened in a separate partial combobox implementation.
4209
+
4059
4210
  An `lr-option` row remains bounded by its owning listbox: the default label ellipsizes and each
4060
4211
  `start`/`end` (or `prefix`/`suffix`) adornment is capped at 40% of the row. Unbroken metadata
4061
4212
  therefore cannot widen a 320px LTR or RTL picker.
@@ -4960,9 +5111,21 @@ default). An inherited or direct public value remains authoritative in every tie
4960
5111
  Text field + calendar popover, **form-associated** via the shared `FormAssociated` mixin (`name`,
4961
5112
  `value`, `disabled`, `required` all inherited).
4962
5113
 
4963
- **Properties (42):**
5114
+ **Properties (44):**
4964
5115
 
4965
5116
  - `appearance: 'filled'|'outlined'|'filled-outlined' = 'outlined'` (reflected)
5117
+ - `appliedPreset: LyraDateRangePreset | undefined` (read-only, new in 11.3.0) — the `presets` entry
5118
+ whose button produced the current `value`, or `undefined` when the value was picked on the
5119
+ calendar, typed into the field, cleared, or reset. Read it inside your own `change`/`input`
5120
+ handler; it is updated before those events are relayed, so a handler observes the preset that
5121
+ caused the very commit it is handling, and it is `undefined` while the popover has never been
5122
+ opened. Mirrors the nested `lr-date-picker`'s identically-named property across this component's
5123
+ shadow boundary, which is where the readback is actually needed: the compact
5124
+ text-field-plus-popover shape is the one a dashboard filter uses, the nested picker instance is
5125
+ unreachable from outside (a CSS part cannot yield it), and the fact is not recoverable from
5126
+ `value` — re-deriving it by string-matching is the mapping table `presets` exists to delete and is
5127
+ ambiguous anyway. A property rather than an event detail because `input`/`change` here are
5128
+ **native** events that cannot carry one
4966
5129
  - `assumeInteractionOn: string[] = ['input']` (JS only)
4967
5130
  - `autocomplete: string = ''`
4968
5131
  - `dayContent?: LyraDatePickerDayContent` (JS only)
@@ -4987,6 +5150,12 @@ Text field + calendar popover, **form-associated** via the shared `FormAssociate
4987
5150
  - `pill: boolean = false` (reflected)
4988
5151
  - `placement: LyraDateInputPlacement = 'bottom-start'` (reflected; all 12 side/alignment
4989
5152
  placements are accepted)
5153
+ - `presets: LyraDateRangePreset[] = []` (JS only, new in 11.1.0) — forwarded verbatim to the nested
5154
+ `lr-date-picker`, whose own `presets` documents the semantics (range mode only, open bounds
5155
+ resolving to `min`/`max`, unset renders nothing). Forwarded rather than reimplemented because the
5156
+ picker lives in this component's shadow root, so a consumer has no route to it. The row's parts
5157
+ are re-exported as `presets`/`preset-button`, and `appliedPreset` above reports which entry
5158
+ produced the current value
4990
5159
  - `readonly: boolean = false` and `required: boolean = false` (reflected)
4991
5160
  - `size: LyraSize = 'm'` (reflected; `2xs`–`xl` and aliases)
4992
5161
  - `today: string = ''` (reflected ISO override)
@@ -10182,7 +10351,7 @@ no-rows states return the empty element as the shadow root's own root, with no `
10182
10351
  wrapper around it — `::part(base)` does not apply in those two states, only in the filtered-to-zero
10183
10352
  one — and that `empty` disappears entirely once the `empty` slot is filled.
10184
10353
 
10185
- - `scrollMode: 'self' | 'page' = 'self'` (attribute `scroll-mode`, reflected) — which element
10354
+ - `scrollMode: 'self' | 'page' | 'auto' = 'self'` (attribute `scroll-mode`, reflected) — which element
10186
10355
  scrolls when the table overflows. `'self'` makes `[part="base"]` the scroll container, which is
10187
10356
  what pairs with `--lr-table-max-height` and makes the sticky header pin inside the table's own
10188
10357
  viewport. `'page'` hands scrolling back to the document. Needed because a scroll container clips
@@ -10190,8 +10359,12 @@ one — and that `empty` disappears entirely once the `empty` slot is filled.
10190
10359
  still `overflow: auto` becomes a sticky containing block that never scrolls, and its header
10191
10360
  scrolls away with the page. With `'page'` the header's nearest scrollport is the page, so it pins
10192
10361
  there; the cost is that a table wider than its host overflows the page instead of scrolling
10193
- inside itself. Named `scrollMode` rather than `scroll` because a `scroll` property would shadow
10194
- `Element.prototype.scroll()`
10362
+ inside itself. The opt-in `'auto'` mode resolves between those two behaviors from the rendered
10363
+ allocation: while content fits it uses page flow, and only while content actually overflows
10364
+ horizontally does `[part="base"]` become the contained scrollport. It re-evaluates when either
10365
+ the allocated width or the rendered table's intrinsic width changes, so the same table can flow
10366
+ with a desktop page and contain itself in a 320px panel. The default remains `'self'`. Named
10367
+ `scrollMode` rather than `scroll` because a `scroll` property would shadow `Element.prototype.scroll()`
10195
10368
 
10196
10369
  **Themeable custom properties:** `--lr-table-cell-color` (default `inherit`),
10197
10370
  `--lr-table-cell-link-color` (default `var(--lr-color-brand)`) and
@@ -10675,6 +10848,114 @@ Dependency-free SVG radial, full-circle ring, or linear meter (no charting libra
10675
10848
 
10676
10849
  ---
10677
10850
 
10851
+ ## `lr-funnel`
10852
+
10853
+ Dependency-free conversion funnel (no charting library): an ordered set of stages, each drawn as a
10854
+ bar whose length is that stage's share of the **first** stage, read top-to-bottom as progressive
10855
+ drop-off. It sits beside `lr-gauge` and `lr-heatmap` as an analytics primitive rather than a general
10856
+ chart type.
10857
+
10858
+ It is deliberately not a sorted bar chart: it normalizes to the first stage rather than to the data
10859
+ maximum, draws no value axis, and reads as stage-to-stage retention rather than category comparison.
10860
+ Reach for `lr-bar-chart`/`lr-lite-chart` when you actually want a value axis and category comparison.
10861
+
10862
+ The whole chart is plain HTML — stage names, absolute values, shares and drop-off percentages are
10863
+ real text inside an `<ol>`, so there is no sighted-only drawing needing a separate transcript. The
10864
+ list carries the accessible name; a host `aria-label` overrides `label`.
10865
+
10866
+ **Properties:**
10867
+
10868
+ - `stages: readonly LyraFunnelStage[] = []` (property only; every share is measured against
10869
+ `stages[0]`)
10870
+ - `comparison: readonly LyraFunnelStage[] = []` (property only — an optional baseline/peer cohort
10871
+ drawn behind each bar as a dashed outline, normalized to **its own** first stage so a cohort's
10872
+ funnel *shape* stays comparable against a baseline whose absolute volumes are not)
10873
+ - `comparisonLabel: string = ''` (attribute `comparison-label`; falls back to a localized generic
10874
+ label)
10875
+ - `label: string = ''` (accessible name for the stage list)
10876
+ - `dropoff: boolean = true` (reflected; `dropoff="false"` in markup really does turn it off —
10877
+ the property uses the explicit `false`-parsing converter, not attribute presence)
10878
+ - `sharePrecision: number = 0` (attribute `share-precision`; fraction digits for every share and
10879
+ drop-off percentage, clamped to `0`–`20`)
10880
+
10881
+ ```ts
10882
+ interface LyraFunnelStage {
10883
+ readonly label: string;
10884
+ readonly value: number;
10885
+ readonly color?: string; // CSS color; unparseable values fall back to --lr-funnel-bar-color
10886
+ }
10887
+ ```
10888
+
10889
+ **Events:** none.
10890
+
10891
+ **Slots:** none.
10892
+
10893
+ **CSS parts:** `base`, `stages` (the `<ol>`), `stage`, `dropoff`, `stage-header`, `stage-label`,
10894
+ `stage-value`, `stage-share`, `comparison-value`, `track`, `bar`, `bar-overflow` (a second token on
10895
+ `bar` when the stage exceeds the first stage), `comparison-bar`, `empty`
10896
+
10897
+ **Themeable custom properties:** `--lr-funnel-bar-color` (default `var(--lr-color-brand)`),
10898
+ `--lr-funnel-comparison-color` (default `var(--lr-color-border-strong)`), `--lr-funnel-track-color`
10899
+ (default `var(--lr-color-surface-raised)`), `--lr-funnel-bar-size` (track thickness, default
10900
+ `var(--lr-size-1-5rem)`).
10901
+
10902
+ **Optional peer deps:** none.
10903
+
10904
+ ```html
10905
+ <script type="module">
10906
+ import '@aceshooting/lyra-ui/components/data/funnel/funnel.js';
10907
+ </script>
10908
+
10909
+ <lr-funnel id="signup" label="Self-serve signup" share-precision="1"></lr-funnel>
10910
+
10911
+ <script type="module">
10912
+ const funnel = document.querySelector('#signup');
10913
+ funnel.stages = [
10914
+ { label: 'Visited pricing', value: 12480 },
10915
+ { label: 'Started trial', value: 4310 },
10916
+ { label: 'Converted to paid', value: 512 },
10917
+ ];
10918
+ // Normalized to ITS OWN first stage, so a 380-visitor account still compares against a
10919
+ // 12,480-visitor peer group.
10920
+ funnel.comparison = [
10921
+ { label: 'Visited pricing', value: 380 },
10922
+ { label: 'Started trial', value: 141 },
10923
+ { label: 'Converted to paid', value: 12 },
10924
+ ];
10925
+ funnel.comparisonLabel = 'Acme Corp';
10926
+ </script>
10927
+ ```
10928
+
10929
+ **Known gotchas:**
10930
+
10931
+ - **Shares are of the first stage, not of the previous one.** `stage-share` answers "how much of the
10932
+ top of the funnel is left here?"; the separate `dropoff` row answers "what changed since the
10933
+ previous stage?". Both are rendered because the percentage is usually the interesting number and
10934
+ the absolute count is the credibility check.
10935
+ - **A zero or negative first stage cannot define a share.** Every `stage-share` is omitted, every bar
10936
+ is zero-length, and only the absolute values render. Nothing is silently divided by zero and no
10937
+ `NaN` reaches the geometry.
10938
+ - **A stage larger than its predecessor is legal** (funnel re-entry). Its share is reported
10939
+ truthfully above 100% in text, while the bar clamps to the track and gains the `bar-overflow` part
10940
+ token so you can style it. Drop-off for that stage reads as an increase.
10941
+ - **Drop-off is omitted, not zeroed, when the previous stage is non-positive** — a change relative to
10942
+ zero is undefined.
10943
+ - **A comparison series of a different length pairs by index.** Extra comparison entries are ignored;
10944
+ stages past its end simply get no comparison bar. A comparison series whose own first value is
10945
+ zero or negative draws nothing.
10946
+ - **Non-finite `value`s are treated as `0`** rather than blanking the stage, so one bad row cannot
10947
+ take the chart with it.
10948
+ - Values, shares and drop-off percentages all format through the component's effective locale
10949
+ (`locale` attribute, or an inherited one) — never a hardcoded `en`.
10950
+ - Bars grow from the inline-start edge and every dimension is a logical property, so `dir="rtl"`
10951
+ needs no extra work. The component never sets its own `dir`.
10952
+ - Layout responds to the **container** (`container-type: inline-size` on `base`), not the viewport:
10953
+ below roughly `18rem` of allocated width the stage name moves to its own line so the value and
10954
+ share stay together. Bar motion is a token-driven transition that stops under
10955
+ `prefers-reduced-motion`.
10956
+ - No events, no interaction model, no zoom/pan. If you need a clickable funnel, wrap the element and
10957
+ handle clicks yourself.
10958
+
10678
10959
  ## `lr-word-cloud`
10679
10960
 
10680
10961
  Dependency-free SVG word/tag cloud. First-party invention (no Web Awesome equivalent). Lays words
@@ -10890,6 +11171,38 @@ weekdayLabelText?: (jsWeekday:number)=>string|undefined; monthLabelText?:
10890
11171
  `colLabelHeight="auto"` to have the band size itself to the rotated extent. **Not mirrored under
10891
11172
  `dir="rtl"`** — both grid modes deliberately retain physical LTR geometry, so leaning one axis'
10892
11173
  labels the other way would be incoherent
11174
+ - `stickyLabels: 'none' | 'rows' | 'cols' | 'both' = 'none'` (attribute `sticky-labels`, reflected)
11175
+ — freezes a **matrix** label band against the grid's own scrolling. `'rows'` pins the row-label
11176
+ gutter so it survives horizontal scrolling, `'cols'` pins the column-label band so it survives
11177
+ vertical scrolling, `'both'` pins both. Labels and cells otherwise share one bitmap, so neither
11178
+ band can be `position: sticky` on its own and a tall matrix scrolls its column header out of
11179
+ view; the only workaround was a light-DOM mirror row that had to hardcode the gutter width and
11180
+ cell size, which made it mutually exclusive with `row-label-width="auto"`. A frozen band is
11181
+ repainted into its own layer in the same draw pass, from the same resolved `matrixGeometry` the
11182
+ cells were painted with, so it tracks a `row-label-width`/`col-label-height` `"auto"`
11183
+ re-resolution, a resize, and a DPR change without drifting a pixel. Freezing needs something to
11184
+ scroll, so the frozen modes wrap the grid in a `[part="grid"]` scrollport: it is bounded inline by
11185
+ the host's own allocation — a matrix wider than a 320px host scrolls inside the component instead
11186
+ of overflowing it — and unbounded in block until you set `--lr-heatmap-grid-max-block-size`, which
11187
+ a frozen column band needs in order to have vertical scrolling to stay behind. The bands are
11188
+ `aria-hidden` duplicates of pixels the canvas already painted, so the accessible representation is
11189
+ unchanged, and the `accessibleCells` overlay moves inside the scrollport so the cell buttons
11190
+ scroll with the canvas they cover. The grid keeps this component's physical LTR geometry under
11191
+ `dir="rtl"`, so the frozen gutter stays on the same physical side as the labels the canvas paints.
11192
+ Matrix mode only, like `matrixGeometry`: the property is read in calendar mode but has no effect
11193
+ there, since a calendar's axes are a different geometry (fixed weekday gutter, month band, and the
11194
+ optional `columnX`/`rowY` overrides). Unset (`'none'`, the default) renders exactly what it always
11195
+ did — one canvas, no scrollport, no extra elements — and an unsupported value normalizes to it.
11196
+ The union is re-exported from the package root as `LyraHeatmapStickyLabels`, so a typed consumer
11197
+ can annotate a variable or a framework prop with it. Everything positioned in canvas coordinates
11198
+ moves into the scrollport with the cells: `[part="tooltip"]` renders inside it, so it stays on
11199
+ the cell it describes through a scroll instead of drifting by the scroll offset, and because
11200
+ `overflow: auto` clips whatever leaves the scrollport, the tooltip is kept inside the visible
11201
+ window too — clamped along the inline axis and flipped to below its cell when a frozen band
11202
+ leaves no room above it. Arrow-key navigation scrolls the focused cell into that window, clear of
11203
+ the frozen bands, which the frozen modes need in their own right: the canvas is the roving tab
11204
+ stop, its focus ring is painted into the bitmap rather than carried by a focusable element the
11205
+ browser would scroll to, and it calls `preventDefault()` on the arrows
10893
11206
  - `maxCellSize?: number` (attribute `max-cell-size`) — ceiling, in CSS px, on the cell size
10894
11207
  `fitToWidth` derives from the host width, in **both** modes. Exists because `fitToWidth` divides
10895
11208
  the _whole_ host width across the grid, so a 5-week calendar or a 3-column matrix in a wide pane
@@ -11019,7 +11332,10 @@ color?: string; label?: string; partOfRamp?: boolean }`: a discrete legend key r
11019
11332
  are never consulted by the color ramp, the bucket math, the tooltip, or the generated accessible
11020
11333
  name, so adding them changes nothing a cell renders. Labeled `annotations` still render their
11021
11334
  `[part="legend-annotation"]` entries after the stops. Unset (the default) or an empty array
11022
- reproduces the exact gradient legend, unchanged. A dev-mode-only warning fires when a stop's
11335
+ reproduces the exact gradient legend, unchanged. Reassigning stops whose `value`/`color`/`label`/
11336
+ `partOfRamp` fields are unchanged does not schedule a canvas redraw. The assignment still takes
11337
+ a fresh frozen ownership snapshot, so mutating and reassigning the same caller array is detected
11338
+ while mutation without reassignment remains isolated. A dev-mode-only warning fires when a stop's
11023
11339
  `color` doesn't match the corresponding `colorSteps` entry — set `partOfRamp: false` on a stop
11024
11340
  that is a real, distinctly-colored swatch deliberately outside the sequential ramp (e.g. a fixed
11025
11341
  "no data" color next to an N-step ramp) to exclude just that stop from the comparison; defaults to
@@ -11029,9 +11345,12 @@ color?: string; label?: string; partOfRamp?: boolean }`: a discrete legend key r
11029
11345
  color-scheme change; called automatically on theme changes, exposed for a consumer that needs to
11030
11346
  force a redraw manually. `matrixGeometry: Readonly<{ padLeft: number; padTop: number; cellSize:
11031
11347
  number }> | undefined` — the gutter/cell geometry the last matrix-mode draw actually painted with,
11032
- in CSS pixels; `undefined` in calendar mode. Lets a light-DOM consumer (e.g. a sticky header mirror
11033
- for a tall matrix) line up with the canvas without hardcoding the same numbers `row-label-width`/
11034
- `col-label-height`'s `"auto"` resolution would otherwise keep private.
11348
+ in CSS pixels; `undefined` in calendar mode. Lets a light-DOM consumer line up with the canvas
11349
+ without hardcoding the same numbers `row-label-width`/`col-label-height`'s `"auto"` resolution would
11350
+ otherwise keep private. For the case that motivated it — a frozen header or gutter on a tall or wide
11351
+ matrix — prefer `stickyLabels`, which freezes the band inside the component and needs no mirror at
11352
+ all; the getter remains the way to align a *separate* element (a sibling chart, a custom overlay)
11353
+ with the grid.
11035
11354
 
11036
11355
  **Events:** `lr-cell-click` (fired on click, or Enter/Space on the keyboard-focused cell —
11037
11356
  `detail: { row, col, value }` in matrix mode, `detail: { date, value }` in calendar mode),
@@ -11042,8 +11361,12 @@ in calendar mode)
11042
11361
 
11043
11362
  **Slots:** none.
11044
11363
 
11045
- **CSS parts:** `base`, `canvas`, `cells` (opt-in per-cell overlay), `cell` (one opt-in native cell
11046
- button), `tooltip` (hover tooltip, positioned over the hovered cell),
11364
+ **CSS parts:** `base`, `canvas`, `grid` (the scrollport wrapping the canvas while `stickyLabels`
11365
+ freezes an axis — absent entirely otherwise), `row-labels`/`col-labels` (the frozen label bands,
11366
+ rendered only for the axis `stickyLabels` names), `cells` (opt-in per-cell overlay), `cell` (one opt-in native cell
11367
+ button), `tooltip` (hover tooltip, positioned over the hovered cell — inside `[part="grid"]` while
11368
+ `stickyLabels` freezes an axis, so it scrolls with the cells, and a `[part="base"]` child
11369
+ otherwise),
11047
11370
  `live-region` (visually-hidden, `aria-hidden` mirror of the keyboard-focused cell; the actual
11048
11371
  announcement uses the shared light-DOM polite sink), `projection-limit` (localized assistive
11049
11372
  disclosure when bounded canonicalization truncates input), `legend`, `legend-lo`, `legend-hi` (both omitted, along with the gradient
@@ -11079,7 +11402,12 @@ dedicated token distinct from both the focus ring and the annotation ring so a h
11079
11402
  independently). `--lr-heatmap-tooltip-bg` (default
11080
11403
  `var(--lr-color-surface)`) and `--lr-heatmap-tooltip-text` (default `var(--lr-color-text)`) —
11081
11404
  unlike the canvas-drawn tokens above, `[part="tooltip"]` is a real DOM element and consumes these
11082
- directly, no `getComputedStyle` bridging needed. Also consumes `--lr-color-text-quiet` (axis label
11405
+ directly, no `getComputedStyle` bridging needed. `--lr-heatmap-sticky-label-bg` (default
11406
+ `var(--lr-color-surface)` — the backdrop painted under a frozen `stickyLabels` band, resolved via
11407
+ `getComputedStyle` like the other canvas-drawn tokens; it must stay **opaque**, since it covers the
11408
+ same labels the scrolling canvas painted underneath it) and `--lr-heatmap-grid-max-block-size`
11409
+ (default `none` — the block-size ceiling of the `[part="grid"]` scrollport, consumed directly by
11410
+ that real DOM element; set it to give a frozen column band vertical scrolling to stay behind). Also consumes `--lr-color-text-quiet` (axis label
11083
11411
  color), `--lr-space-xs`, `--lr-radius`/`--lr-shadow` (tooltip box), and `--lr-focus-ring-width`/
11084
11412
  `--lr-focus-ring-offset` (the real `[part="canvas"]:focus-visible` DOM outline, stroked in the
11085
11413
  same color as `--lr-heatmap-focus-ring-color`).
@@ -12318,20 +12646,21 @@ renders each event as a `<button part="agenda-event">`.
12318
12646
 
12319
12647
  ## `lr-timeline` and `lr-timeline-item`
12320
12648
 
12321
- Read-only chronological sequence. `lr-timeline` is a `role="list"` flex container; each
12649
+ Read-only chronological sequence by default. `lr-timeline` is a `role="list"` flex container; each
12322
12650
  `lr-timeline-item` is a light-DOM child that sets `role="listitem"` on itself and renders its own
12323
12651
  marker plus the trailing rail segment reaching toward the next item's marker. The last item's rail is
12324
12652
  suppressed purely in CSS (`::slotted([role='listitem']:last-child)`) — no JS coordination anywhere.
12325
- Neither element has events, keyboard navigation, or a selection model: a passive record display, by
12326
- design (an item's `title`/`description` routinely hold focusable content, so wrapping the row in
12327
- `role="button"` would trip `nested-interactive`).
12653
+ Items have no keyboard navigation or selection model: a passive record display, by design (an
12654
+ item's `title`/`description` routinely hold focusable content, so wrapping the row in `role="button"`
12655
+ would trip `nested-interactive`). The opt-in clustered time scale adds only native count-marker
12656
+ buttons; it does not make the individual rows interactive.
12328
12657
 
12329
12658
  **`lr-timeline` properties:** `orientation: 'vertical' | 'horizontal' = 'vertical'` — note the
12330
12659
  opposite default from `lr-stepper`; `horizontal` makes `[part='base']` a horizontally scrollable row.
12331
12660
  `accessibleLabel: string = ''` (attribute `aria-label`) overrides the localized `"Timeline"` name
12332
12661
  (the `role="list"` element is in the shadow root and never inherits a host attribute). Read-only
12333
- `itemCount: number` is the live count of direct default-slot `<lr-timeline-item>` children;
12334
- unrelated slotted elements and text nodes are ignored.
12662
+ `itemCount: number` is the live count of default-slot `<lr-timeline-item>` assignments (including
12663
+ flattened forwarding slots); unrelated slotted elements and text nodes are ignored.
12335
12664
  `scale: 'flow' | 'time' = 'flow'` (attribute `scale`, type `LyraTimelineScale`) chooses how items
12336
12665
  are distributed along the main axis. `'flow'` is the default even sequence, where `timestamp` is
12337
12666
  rendered as text but carries no positional meaning. `'time'` positions each item at its true
@@ -12339,23 +12668,34 @@ proportion of the range, so a gap of weeks and a gap of decades stop looking ide
12339
12668
  needs a definite extent to distribute along — `--lr-timeline-time-extent` (default
12340
12669
  `var(--lr-size-20rem)`), applied as `block-size` when vertical and `inline-size` when horizontal —
12341
12670
  because items are absolutely positioned and a percentage against an auto-sized track resolves to
12342
- zero. `collision: 'overlap' | 'stack' = 'overlap'` (attribute `collision`, type `LyraTimelineCollision`)
12343
- chooses what `scale="time"` does with items landing on nearly the same position: `'overlap'` leaves
12344
- them stacked on one another, `'stack'` steps each colliding item one lane along the **cross** axis
12345
- (indent per lane: `--lr-timeline-collision-offset`, default `var(--lr-space-l)`), which is what a
12346
- dense chronology needs items within 1.5% of the axis of each other count as colliding, and an
12347
- isolated item returns to lane 0 rather than inheriting a preceding run's depth. There is
12348
- deliberately no `'cluster'` mode: collapsing coincident items into one expandable marker needs a
12349
- selection model and click events this passive component does not have.
12671
+ zero. `collision: 'overlap' | 'stack' | 'cluster' = 'overlap'` (attribute `collision`, type
12672
+ `LyraTimelineCollision`) chooses what `scale="time"` does with items landing on nearly the same
12673
+ position: `'overlap'` leaves them stacked on one another, `'stack'` steps each colliding item one
12674
+ lane along the **cross** axis (indent per lane: `--lr-timeline-collision-offset`, default
12675
+ `var(--lr-space-l)`), and `'cluster'` replaces every group of at least two colliding items with one
12676
+ count marker. The group's first member in author order becomes its representative and renders the
12677
+ button in that item's existing list/Tab position; the remaining members are hidden, so visual,
12678
+ semantic, and keyboard order do not diverge merely because clustering is enabled. Stack mode and
12679
+ the cluster floor treat items within 1.5% of the axis as colliding.
12680
+ Cluster mode widens that floor to the rendered count action's footprint on the currently allocated
12681
+ axis, then reclusters when the allocation or rendered action size changes, so interactive marker
12682
+ hit areas do not overlap.
12683
+ Cluster windows are bounded from their first sorted position rather than transitively chaining every
12684
+ dense neighbour, so a large history becomes a useful sequence of markers rather than one axis-wide
12685
+ cluster. An isolated item remains an ordinary timeline item. Activating a count marker is a
12686
+ notification only; it never expands or selects items internally.
12350
12687
  `rangeStart` / `rangeEnd` (`Date | string | number`, attribute: false) pin the axis instead of
12351
12688
  deriving it from the earliest/latest item; a reversed or non-finite pair falls back to the derived
12352
12689
  range. An item with no parseable `timestamp` (including one supplied only through the `timestamp`
12353
12690
  slot, which carries no machine-readable instant) keeps document order and is spread evenly, so a
12354
- partially-timestamped list degrades rather than stacking at the origin. Items sharing an instant
12355
- overlap rather than being fanned into lanes: lane assignment, brushing, zooming and per-event
12356
- selection belong to a denser component than this deliberately passive one. Positions are written to
12691
+ partially-timestamped list degrades rather than stacking at the origin. Reassigning an item's
12692
+ `timestamp` reclusters without requiring a slot mutation. Positions are written to
12357
12693
  each child as a private `--_lr-timeline-item-offset` custom property and removed again on a switch
12358
- back to `'flow'`, so the component still never alters its children's content or structure.
12694
+ back to `'flow'`. Cluster mode adds and removes a private visibility marker on non-representative
12695
+ members and temporarily changes only the representative item's shadow presentation; switching
12696
+ mode, shrinking the data, or disconnecting restores every ordinary row. If regrouping would remove
12697
+ the focused row content or count action, focus moves to the replacement cluster action, the first
12698
+ surviving item action, or the timeline list as a programmatic fallback.
12359
12699
 
12360
12700
  **`lr-timeline-item` properties:** `timestamp?: Date | string | number` (attribute: false — `Date`
12361
12701
  isn't attribute-serializable; invalid input normalizes to unset and renders no timestamp UI),
@@ -12365,8 +12705,16 @@ isn't attribute-serializable; invalid input normalizes to unset and renders no t
12365
12705
  optional pulse disabled under `prefers-reduced-motion: reduce`, with explicit `aria-current="true"`
12366
12706
  or `"false"` on the host).
12367
12707
 
12368
- **Events:** none on either element. Read the reactive `itemCount` property after changing direct
12369
- children; the internal slot's non-composed `slotchange` event is not a host-level public signal.
12708
+ **Events:** `lr-timeline` emits `lr-cluster-activate` when a `collision="cluster"` marker is
12709
+ activated by pointer, Enter, or Space. Its non-cancelable, bubbling, composed
12710
+ `detail: LyraTimelineClusterActivateDetail` is `{ items: readonly LyraTimelineItem[] }`: a fresh
12711
+ frozen snapshot of the cluster members in document order, preserving each element's identity.
12712
+ `LyraTimelineClusterActivateDetail {
12713
+ items: unknown;
12714
+ }`
12715
+ Use it to open a consumer-owned popover, dialog, or detail view. `lr-timeline-item` emits no events.
12716
+ Read the reactive `itemCount` property after changing assigned items; the internal slot's
12717
+ non-composed `slotchange` event is not a host-level public signal.
12370
12718
 
12371
12719
  **Slots:** `lr-timeline`'s default slot holds the items, in display order. On an item the **default
12372
12720
  slot is the title** (there is no `title` slot), plus `marker-icon` (marker glyph override; an empty
@@ -12375,7 +12723,9 @@ slot falls back to a color-coded dot), `timestamp` (wins outright over the
12375
12723
  entirely when empty).
12376
12724
 
12377
12725
  **CSS parts:** timeline `base` — the `role="list"` flex container (no separate `list` part). Item:
12378
- `base`, `track` (marker + rail spine, always the opposite axis from `base`), `marker`
12726
+ `cluster` the native count button rendered while that item represents a cluster, with a 40px
12727
+ minimum action surface; `cluster-count` — its painted count pill; `base`, `track`
12728
+ (marker + rail spine, always the opposite axis from `base`), `marker`
12379
12729
  (`aria-hidden="true"`, decorative), `rail` (the connecting segment; `visibility: hidden` rather than
12380
12730
  removed on the last item, so marker alignment stays consistent), `content`, `header` (flex row
12381
12731
  wrapping `title` and `timestamp`; wraps at narrow widths rather than truncating), `title`,
@@ -12383,8 +12733,11 @@ wrapping `title` and `timestamp`; wraps at narrow widths rather than truncating)
12383
12733
 
12384
12734
  **Themeable custom properties:** `--lr-timeline-gap` (default `var(--lr-space-l)`) — declared on
12385
12735
  `lr-timeline` but consumed inside each item via inheritance across the slot boundary; it is both the
12386
- inter-item spacing and the length each rail bridges. `--lr-scroll-fade-size` (default `2rem`) controls each
12387
- horizontal-overflow edge fade; forced-colors mode removes the masks while retaining native
12736
+ inter-item spacing and the length each rail bridges. `--lr-timeline-cluster-size` (default
12737
+ `var(--lr-size-2rem)`) sizes the painted count pill, `--lr-timeline-cluster-bg` (default
12738
+ `var(--lr-color-brand)`) sets its background, and `--lr-timeline-cluster-color` (default
12739
+ `var(--lr-color-on-brand)`) sets its foreground. `--lr-scroll-fade-size` (default `2rem`) controls
12740
+ each horizontal-overflow edge fade; forced-colors mode removes the masks while retaining native
12388
12741
  scrolling. On the item: `--lr-timeline-marker-size`
12389
12742
  (default `var(--lr-size-1-25rem)`, both dimensions so the dot stays circular),
12390
12743
  `--lr-timeline-rail-width` (default `var(--lr-border-width-medium)`), `--lr-timeline-rail-color`
@@ -12975,6 +13328,13 @@ These named interfaces and helper signatures are available to typed integrations
12975
13328
  maxZoom: unknown;
12976
13329
  }`
12977
13330
 
13331
+ - **`components-data-funnel-funnel-contracts`** — Supporting data types and helpers for this component family.
13332
+ `LyraFunnelStage {
13333
+ label: unknown;
13334
+ value: unknown;
13335
+ color: unknown;
13336
+ }`
13337
+
12978
13338
  - **`components-data-graph-query-builder-graph-query-builder-contracts`** — Supporting data types and helpers for this component family.
12979
13339
  `GraphQueryDeleteDetail {
12980
13340
  queryId: unknown;
@@ -13648,6 +14008,12 @@ TemplateResult; ariaLabel?: string }`. Each entry gets a header toggle button
13648
14008
  - `activeViewId: string = ''` (attribute: false) — the currently active view's `viewId`; defaults to the
13649
14009
  first entry of `views` (or `''` when `views` is empty). Settable directly to control the active
13650
14010
  view externally; also updated internally when a view toggle is clicked.
14011
+ - `activeView: string = ''` (attribute: false) — **deprecated alias for `activeViewId`**, which it
14012
+ seeds. `activeView` was this member's original public name and the rename was never announced, so
14013
+ a shipped `.activeView=${…}` binding silently became inert and the widget fell back to its first
14014
+ view. It seeds rather than being read alongside, because the component itself writes
14015
+ `activeViewId` (a toggle click, and the fallback when `views` drops the active id) — so a stale
14016
+ alias must not undo a later interactive change. Prefer `activeViewId` in new code.
13651
14017
  - `accessibleLabel: string | null = null` (attribute `aria-label`) — overrides the label-derived
13652
14018
  fullscreen dialog name. An explicitly empty value is retained; property, slotted-label, and
13653
14019
  localized fallbacks apply only when it is absent.
@@ -15317,6 +15683,12 @@ removing the label from the accessibility tree.
15317
15683
  reflects `aria-current="page"` on `[part='base']` and drives the current visual treatment. The rail
15318
15684
  has no built-in routing, so the consumer sets this per item (e.g. by comparing `href` against the
15319
15685
  current location).
15686
+ - `active: boolean = false` — **deprecated alias for `current`**, read alongside it: the item is
15687
+ current when either is true. `active` was this member's original public name, in both property and
15688
+ attribute form; it was renamed to `current` without an alias, so shipped consumers writing
15689
+ `.active=${…}` or `<lr-app-rail-item active>` silently lost their current-item indicator and kept a
15690
+ permanent `aria-current="false"`. A Lit property binding on a custom element is untyped, so nothing
15691
+ in a consumer's type check or test suite could catch it. Prefer `current` in new code.
15320
15692
  - `tooltip: boolean = false` (reflected) — opt-in hover/focus flyout (`[part='tooltip']`) showing
15321
15693
  this item's label text while the rail's `icon-only` mode (set externally by the parent
15322
15694
  `<lr-app-rail>` as the viewport narrows) hides it from view. No effect outside icon-only mode,
@@ -16550,7 +16922,8 @@ value therefore stays inside a 320px LTR or RTL bar, with the chip's own label e
16550
16922
  overflow ownership rather than widening the page.
16551
16923
 
16552
16924
  Each edit exposes one filter-bar `lr-input` carrying a detached, deeply frozen snapshot of the
16553
- complete value object. A built-in or
16925
+ complete value object, plus the `filterId` that changed and (new in 11.3.0) `appliedPreset` — see
16926
+ "Date-range quick ranges" below. A built-in or
16554
16927
  custom control's own `lr-input`/`lr-change` aliases stay inside the wrapper so their incompatible
16555
16928
  detail shapes cannot escape as duplicate bar events; native-style `input`/`change` events from the
16556
16929
  composed controls continue bubbling normally.
@@ -16600,6 +16973,54 @@ to `<lr-input>` for an open-ended free-text query rather than a closed choice se
16600
16973
  filter's value is the raw query string, verbatim, and its chip shows exactly that string — the same
16601
16974
  text the user typed, not a truncated or normalized form.
16602
16975
 
16976
+ ### Date-range quick ranges
16977
+
16978
+ A `'date-range'` definition also accepts `presets?: readonly LyraDateRangePreset[]` (new in 11.3.0),
16979
+ forwarded to its composed `<lr-date-input>` exactly like `min`/`max`, so the quick-range row
16980
+ ("Today", "Last 7 days", "All time") renders inside that filter's own calendar popover. Entries are
16981
+ `LyraDateRangePreset { label, start?, end? }` with ISO `YYYY-MM-DD` bounds; an omitted bound is open
16982
+ and resolves to the filter's `min`/`max`, and an open bound with no corresponding limit renders that
16983
+ button disabled. `presets` is deliberately **not** accepted on a single `'date'` filter: a preset
16984
+ names two dates, so `lr-date-picker` ignores the list outside range mode, and a list passed there is
16985
+ dropped rather than rendering a row that cannot do anything.
16986
+
16987
+ The `lr-input` emitted by such a commit carries `appliedPreset`, the definition entry whose button
16988
+ produced it — the bar's own frozen snapshot, so it compares identical to `filters[i].presets[j]`. It
16989
+ is `undefined` for every other filter type and for a range picked or typed by hand. A filter bar
16990
+ whose values round-trip through a query string needs it because `value` holds only the frozen ISO
16991
+ range: persisting "Last 7 days" as a preset id keeps it meaning the last 7 days after the next
16992
+ reload, and re-deriving it by string-matching `value` is both the mapping table `presets` exists to
16993
+ delete and ambiguous (Today and This month coincide on the 1st). It rides the event rather than
16994
+ `value` because it is metadata about one edit, not a filter value — `value` stays the plain,
16995
+ JSON-serializable record it has always been.
16996
+
16997
+ ```ts
16998
+ const filters: LyraFilterBarFilterDefinition[] = [
16999
+ {
17000
+ filterId: "period",
17001
+ label: "Reporting period",
17002
+ type: "date-range",
17003
+ min: "2020-01-01",
17004
+ max: "2030-12-31",
17005
+ presets: [
17006
+ { label: "Last 7 days", start: "2026-08-13", end: "2026-08-19" },
17007
+ { label: "This month", start: "2026-08-01", end: "2026-08-31" },
17008
+ { label: "All time" },
17009
+ ],
17010
+ },
17011
+ ];
17012
+
17013
+ bar.addEventListener("lr-input", (event) => {
17014
+ const { value, filterId, appliedPreset } = event.detail;
17015
+ persist({ ...value, periodPreset: appliedPreset?.label });
17016
+ });
17017
+ ```
17018
+
17019
+ Before this, the only way to give a filter-bar date range a quick-range row was `type: 'custom'`,
17020
+ which means hand-rendering an `lr-date-input` plus a full adapter (`clearValue`, `isEmpty`,
17021
+ `formatValue`) to set one property, and forfeits the built-in date-range chip localization described
17022
+ next.
17023
+
16603
17024
  Date chips localize exactly one round-trip-valid ISO `YYYY-MM-DD` segment; date-range chips require
16604
17025
  exactly two slash-separated segments. Four-digit
16605
17026
  years `0000`–`0099` retain those literal years rather than inheriting JavaScript's 1900 offset;
@@ -17041,6 +17462,7 @@ These named interfaces and helper signatures are available to typed integrations
17041
17462
  }`
17042
17463
  `LyraFilterBarDateRangeDefinition {
17043
17464
  type: unknown;
17465
+ presets: unknown;
17044
17466
  min: unknown;
17045
17467
  max: unknown;
17046
17468
  filterId: unknown;
@@ -17052,6 +17474,7 @@ These named interfaces and helper signatures are available to typed integrations
17052
17474
  `LyraFilterBarInputDetail {
17053
17475
  value: unknown;
17054
17476
  filterId: unknown;
17477
+ appliedPreset: unknown;
17055
17478
  }`
17056
17479
  `LyraFilterBarOption {
17057
17480
  value: unknown;
@@ -18294,6 +18717,13 @@ no `.items` array prop).
18294
18717
 
18295
18718
  **Properties:**
18296
18719
 
18720
+ - `accessibleLabel: string = ''` (attribute `aria-label`) — accessible name for the group,
18721
+ forwarded to `[part='base']`, which carries `role='group'` whenever a name is supplied. Supply
18722
+ one for any chip row that means something as a set (a filter row, a tag list); without it the
18723
+ container stays roleless, because an unnamed group role adds verbosity without information.
18724
+ A host `aria-label` does **not** reach a shadow root on its own — this property is what makes it
18725
+ reach `[part='base']`, the element that owns the role. Mirrors how `lr-radio-group` and
18726
+ `lr-segmented` name their own `radiogroup`.
18297
18727
  - `maxVisible?: number` (attribute `max-visible`) — maximum number of assigned children shown before
18298
18728
  the rest collapse behind a "+N" indicator; flattened slot-forwarded children count the same as
18299
18729
  direct children. Author-hidden or inert children do not consume capacity or inflate the hidden
@@ -18565,6 +18995,45 @@ from the shared positioner and caps its dimensions to the measured available spa
18565
18995
 
18566
18996
  A click-triggered, light-dismiss floating surface positioned with the shared Floating UI positioner.
18567
18997
 
18998
+ **First-interaction registration.** A performance-sensitive navigation shell can keep a native
18999
+ `<details>` disclosure working before JavaScript, then load only the granular popover registration
19000
+ when that fallback first opens. The interaction that starts an async import is not replayed after
19001
+ the custom element upgrades, so finish the handoff explicitly with `show()`:
19002
+
19003
+ ```html
19004
+ <details id="account-fallback">
19005
+ <summary>Account</summary>
19006
+ <nav aria-label="Account"><a href="/profile">Profile</a></nav>
19007
+ </details>
19008
+ <lr-popover id="account-popover" popup-role="none" hidden>
19009
+ <button slot="trigger">Account</button>
19010
+ <nav aria-label="Account"><a href="/profile">Profile</a></nav>
19011
+ </lr-popover>
19012
+ <script type="module">
19013
+ const fallback = document.querySelector("#account-fallback");
19014
+ const popover = document.querySelector("#account-popover");
19015
+ let registration;
19016
+ fallback.addEventListener("toggle", async () => {
19017
+ if (!fallback.open || !popover.hidden) return;
19018
+ registration ??= import(
19019
+ "@aceshooting/lyra-ui/components/overlays/overlay/popover.js"
19020
+ ).catch((error) => {
19021
+ registration = undefined; // let a later interaction retry
19022
+ throw error;
19023
+ });
19024
+ await registration;
19025
+ await customElements.whenDefined("lr-popover");
19026
+ fallback.hidden = true;
19027
+ popover.hidden = false;
19028
+ await popover.show();
19029
+ });
19030
+ </script>
19031
+ ```
19032
+
19033
+ This keeps the fallback's markup and semantics in the initial document. The registration, Lyra
19034
+ base, nonmodal stack, and first-open positioning runtime remain outside that initial module graph.
19035
+ If the import fails, leave the native disclosure visible and usable.
19036
+
18568
19037
  **Properties:**
18569
19038
 
18570
19039
  - `open: boolean = false` (reflected) — assigning it runs the same `lr-show`/`lr-hide` lifecycle as
@@ -21895,6 +22364,20 @@ import "@aceshooting/lyra-ui/components/media/flag/flag-peer.js";
21895
22364
  unneeded 249-entry fetch. Only the standard tier is bulk-fetched this way;
21896
22365
  `fidelity="compact"/"detailed"` on individual elements still resolves through its own lazy
21897
22366
  per-code loader.
22367
+ - Rendering many flags at once **and** leaving every `<lr-flag>` on the default
22368
+ `fidelity="standard"`: import
22369
+ `@aceshooting/lyra-ui/components/media/flag/flag-peer-bulk-standard.js` instead of
22370
+ `flag-peer-bulk.js` (never more than one of the three peer entries — each
22371
+ `setFlagUrlResolver()` call replaces the previous resolver). It registers
22372
+ `@aceshooting/lyra-flags/standard`'s `createFlagUrlResolver()`, which is backed by the same
22373
+ standard-tier-only eager map as the root's, without statically importing the detailed and
22374
+ compact loader maps the root entry needs for its per-call `variant`. Those maps are what
22375
+ `flag-peer-bulk.js` pays for its batching: on a real production build with a 156-country flag
22376
+ column, routing bulk resolution through the package root emitted +65 detailed SVGs and +31
22377
+ compact WebPs — +15.8MB of assets no route rendered — swamping the chunk-count win the bulk path
22378
+ exists for. The tradeoff is the tier commitment: `fidelity="compact"/"detailed"` on an individual
22379
+ element resolves to that code's standard asset instead (a silent no-op, not an error), so use
22380
+ `flag-peer-bulk.js` when per-instance fidelity must actually be honoured.
21898
22381
  - 65 of `@aceshooting/lyra-flags`' 249 flags (any whose design includes a detailed coat of
21899
22382
  arms/seal/emblem, e.g. `es`, `pt`, `sv`) ship **three** fidelity tiers, selected via the
21900
22383
  `fidelity` property (`flagUrl(code, { variant: fidelity })` under the hood): `"compact"` — a tiny WebP raster for
@@ -22006,13 +22489,19 @@ dimming at `itemCount <= 1`), `--lr-focus-ring-*`.
22006
22489
  ## `lr-map`
22007
22490
 
22008
22491
  A `maplibre-gl` wrapper with a declarative legend, a single choropleth GeoJSON fill layer, markers,
22009
- and additive plain-GeoJSON `dataLayers`, plus a peer-neutral `map` getter for common imperative
22010
- operations. Its runtime value is the underlying MapLibre map.
22492
+ and additive `dataLayers` — plain GeoJSON, natively clustered points, or a heatmap density surface
22493
+ plus a peer-neutral `map` getter for common imperative operations. Its runtime value is the
22494
+ underlying MapLibre map. The component observes its own map-container allocation and calls the
22495
+ peer's `resize()` when that allocation changes.
22011
22496
 
22012
22497
  **Properties:**
22013
22498
 
22014
22499
  - `center: [number, number] = [0, 0]`
22015
22500
  - `zoom: number = 2`
22501
+ - `renderWorldCopies?: boolean` (attribute: false) — forwarded to MapLibre when its map is
22502
+ constructed. Leave it unset to preserve MapLibre's own current default; set `false` before
22503
+ construction to stop repeating the world horizontally. This is a construction-time option, so a
22504
+ later change takes effect after the component is disconnected and reconnected.
22016
22505
  - `mapStyle?: LyraMapStyleSpecification | string` (attribute: false) — required before a map is
22017
22506
  constructed. Object assignments are detached and recursively frozen; create and reassign a new
22018
22507
  style to update it. `LyraMapStyleSpecification` is the peer-neutral structural subset accepted from
@@ -22087,10 +22576,15 @@ string; geojson: GeoJSON.FeatureCollection; field: string; stops: [number, strin
22087
22576
  gotchas. Entries with non-finite coordinates or latitude outside `[-90, 90]` are skipped without
22088
22577
  aborting valid siblings. `color` is used only when the browser accepts it as CSS `color`;
22089
22578
  declaration breaks and `url()` paint servers fall back to MapLibre's default marker color.
22579
+ Every retained marker is a named `role="button"` tab stop, including one without a popup. Click,
22580
+ Enter, and Space emit `lr-map-marker-activate`; Space suppresses its page-scroll default while
22581
+ preserving MapLibre's popup toggle. A popup-bearing marker additionally exposes
22582
+ `aria-haspopup="dialog"`, `aria-controls`, and explicit `aria-expanded`.
22090
22583
  - `dataLayers: LyraMapGeoJsonDataLayer[] = []` (attribute: false) —
22091
22584
  `LyraMapGeoJsonDataLayer { sourceId: string; geojson: GeoJSON.Feature |
22092
22585
  GeoJSON.FeatureCollection; tone?: 'accent' | 'success' | 'warning' |
22093
- 'danger' | 'neutral'; color?: string; strokeColor?: string }`. `sourceId` is trimmed and must be nonempty; the first layer for a
22586
+ 'danger' | 'neutral'; color?: string; strokeColor?: string; kind?: LyraMapDataLayerKind;
22587
+ heatmap?: LyraMapHeatmapOptions; cluster?: LyraMapClusterOptions }`. `sourceId` is trimmed and must be nonempty; the first layer for a
22094
22588
  `sourceId` wins and blank or later duplicate records are ignored. Each retained entry adds one
22095
22589
  GeoJSON source plus three geometry-filtered layers
22096
22590
  (fill, line, and circle, so a mixed `FeatureCollection` renders correctly), colored from the
@@ -22110,9 +22604,65 @@ GeoJSON.FeatureCollection; tone?: 'accent' | 'success' | 'warning' |
22110
22604
  from overwriting or removing a same-named source supplied by `mapStyle`. Independent of
22111
22605
  `choropleth` — no `field`/`stops` color-interpolation, just the geometry rendered in a flat tone;
22112
22606
  use `choropleth` instead when you need a data-driven color ramp. An entry whose `sourceId`
22113
- persists across a `dataLayers` reassignment gets its GeoJSON updated in place (`setData()`), one
22114
- that's dropped has its private source/layers removed, and a genuinely new `sourceId` gets new
22115
- resources nothing leaks on removal, style change, or disconnect.
22607
+ persists across a `dataLayers` reassignment gets its GeoJSON updated in place (`updateData()`
22608
+ when stable feature IDs make a safe diff possible, otherwise `setData()`), one that's dropped has
22609
+ its private source/layers removed, and a genuinely new `sourceId` gets new resources — nothing
22610
+ leaks on removal, style change, or disconnect.
22611
+
22612
+ `cluster` and `kind` (both new in 11.3.0) opt one entry out of that three-layer geometry split.
22613
+ **Both are strictly additive: an entry that sets neither renders exactly what it rendered before,
22614
+ down to the layer ids and the point layer's filter.**
22615
+
22616
+ `cluster?: LyraMapClusterOptions { radius?: number; maxZoom?: number; radiusSteps?: [number,
22617
+ number][]; colorSteps?: [number, string][]; countFont?: string[] }` turns the entry's source into a
22618
+ natively clustered one — `cluster`/`clusterRadius` (default 50)/`clusterMaxZoom` (default 14) on
22619
+ the source, plus a `${sourceId}-cluster` circle filtered on `has('point_count')`, a
22620
+ `${sourceId}-cluster-count` label, and a `${sourceId}-circle` layer for the points that stayed
22621
+ unclustered. `cluster: {}` opts in at every default. This is what a thousands-of-points map needs
22622
+ and what `markers` cannot be: `markers` mints one real, individually focusable DOM element per
22623
+ entry, which is right for tens of pins and both unreadable and expensive for thousands.
22624
+ `radiusSteps` and `colorSteps` are `['step', …]` breaks keyed on `point_count`, in the same
22625
+ ascending `[value, output]` vocabulary as `choropleth.stops` — including the same base rule, where
22626
+ the first entry's own output covers everything below the first threshold, and the same colour
22627
+ resolution, where a `var(--lr-…)` reference in a `colorSteps` entry is resolved against the host
22628
+ before it reaches MapLibre (which paints to a WebGL canvas and never sees the CSS cascade), so a
22629
+ retheme moves the cluster breaks with everything else. No fill or line layer is created for a
22630
+ clustered entry, because MapLibre's clustering keeps point features only. The count
22631
+ label needs glyphs: a style that declares none gets the graduated circles without the numbers
22632
+ (adding a text layer against a glyph-less style paints nothing and only emits peer errors), and
22633
+ `countFont` names the font stack when your style's glyph source lacks MapLibre's spec default.
22634
+ Cluster options are baked into the source at creation time by MapLibre and have no setter, so
22635
+ changing them (or `kind`) rebuilds that one entry's source and layers; every other update still
22636
+ reconciles in place.
22637
+
22638
+ `kind?: 'auto' | 'heatmap'` (`LyraMapDataLayerKind`, default `'auto'` — today's geometry split)
22639
+ renders the source as MapLibre's own first-class `heatmap` layer instead, which the geometry split
22640
+ cannot express at all: thousands of overlapping circles read as one opaque blob rather than as
22641
+ where the data is concentrated. `heatmap?: LyraMapHeatmapOptions { weightField?: string;
22642
+ weightRange?: [number, number]; stops?: [number, string][]; radius?: LyraMapHeatmapZoomValue;
22643
+ intensity?: LyraMapHeatmapZoomValue; opacity?: number }`, where `LyraMapHeatmapZoomValue` is a
22644
+ scalar number or bounded `[zoom, value][]` stops,
22645
+ configures it. `weightField` weights each point by a feature property, and `weightRange` maps that
22646
+ property's own units onto the 0–1 weight MapLibre expects — without it the raw value is passed
22647
+ through, which saturates the surface for any quantity above ~1; with neither, every point weighs 1.
22648
+ `stops` are `[density, color]` pairs with density in `[0, 1]`, **the same `[value, color]`
22649
+ vocabulary `choropleth.stops` and `legendGradient` already share**, so a `legendGradient` bar can
22650
+ describe the ramp without a second copy of it, and `var(--lr-…)` stops resolve against the host the
22651
+ same way `color`/`strokeColor` do. A ramp that doesn't start at density 0 gets a fully transparent
22652
+ stop prepended, because a coloured zero tints the entire map — so **a single stop is already a
22653
+ complete ramp**, as long as it sits above density 0: `stops: [[1, '#ff0000']]` is exactly
22654
+ transparent → red. The one authored ramp that can't be honoured is a lone stop AT density 0, which
22655
+ describes a flat colour rather than a gradient; that one — like an unset or wholly unusable
22656
+ `stops` — falls back to the built-in ramp, which runs transparent → `--lr-color-brand` →
22657
+ `--lr-color-success` → `--lr-color-warning` → `--lr-color-danger`, so a retheme moves the density
22658
+ surface with everything else. Scalar `radius` (default 30) and `intensity` (default 1) preserve
22659
+ their established behavior. Two or more usable `[zoom, value]` stops emit linear zoom
22660
+ interpolation; stops are sorted, duplicate zooms removed, zoom clamped into `[0, 24]`, radius
22661
+ into `[1, 200]`, and intensity into `[0, 100]`. One usable stop becomes a scalar and an unusable
22662
+ array falls back to the existing default. `opacity` is clamped into `[0, 1]`; omission leaves the
22663
+ peer's default untouched on construction, and dropping a previously-authored value restores 1.
22664
+ `cluster` is ignored on a heatmap entry: a heatmap already aggregates density, and clustering its
22665
+ input would feed it one point per cluster instead of the real distribution.
22116
22666
  - `maxBounds: LyraMapBounds | null = null` (attribute: false) — box the map may not pan outside,
22117
22667
  `[[west, south], [east, north]]`. Prefer it over calling `map.setMaxBounds()` through the `.map`
22118
22668
  escape hatch: constraining the camera can wedge maplibre-gl at a sub-1 fractional zoom in a wide
@@ -22124,14 +22674,15 @@ GeoJSON.FeatureCollection; tone?: 'accent' | 'success' | 'warning' |
22124
22674
 
22125
22675
  **Choropleth and `dataLayers` updates are diffed before they reach the peer.** `setData()`
22126
22676
  unconditionally re-tiles and repaints an entire source, which is invisible on a static map and
22127
- expensive on an animated one. When an update changes only feature *properties* the values driving
22128
- the colour ramp the component emits maplibre-gl's incremental `updateData()` instead. The fast
22129
- path is deliberately strict, and needs all of: the same feature count, a `string`/`number` `id` on
22130
- every feature in the same order, and geometry that is the **same object** as last time (not merely
22131
- deep-equal). Structurally comparing polygon rings would cost about what the re-tile costs, and a
22132
- false positive would paint stale geometry. Reuse your geometry objects across frames — the
22133
- efficient way to build an animation anyway and the fast path applies; otherwise it falls back to
22134
- `setData()` with no change in behaviour. Peers predating `updateData()` always take the old path.
22677
+ expensive on an animated one. When every feature has a unique `string`/`number` `id` and retained
22678
+ features keep semantically unchanged geometry/bbox values, the component emits MapLibre's
22679
+ incremental `updateData()` for property changes, additions, removals, and order changes. The exact
22680
+ next order is preserved: an unchanged prefix stays in place and only the invalidated suffix is
22681
+ removed and re-added. Lyra's immutable ownership boundary detaches GeoJSON assignments, so geometry
22682
+ reference identity cannot survive between frames; a bounded, accessor-free comparison verifies the
22683
+ JSON geometry graph instead. A missing/duplicate ID, changed geometry, exceeded bound, or uncertain
22684
+ comparison falls back to `setData()` with no change in rendered behaviour. Peers without
22685
+ `updateData()` always take the old path.
22135
22686
 
22136
22687
  **Feature properties are tiled, and therefore bounded in numeric magnitude.** MapLibre GL tiles
22137
22688
  every GeoJSON source through a worker into a protobuf vector tile, so a property carrying a huge
@@ -22150,7 +22701,9 @@ payload beside the map.
22150
22701
  empty canvas name. The non-semantic `[part="base"]` wrapper is not named instead.
22151
22702
 
22152
22703
  **Authoring types:** `LyraMapLegendEntry`, `LyraMapLegendPattern`, `LyraMapLegendProjection`, `LyraMapChoroplethLayer`,
22153
- `LyraMapGeoJsonDataLayer`, `LyraMapMarker`, `LyraMapStyleSpecification`, and `LyraMapInstance`.
22704
+ `LyraMapGeoJsonDataLayer`, `LyraMapDataLayerKind`, `LyraMapClusterOptions`, `LyraMapHeatmapOptions`,
22705
+ `LyraMapHeatmapZoomValue`, `LyraMapMarker`, `LyraMapMarkerActivationDetail`,
22706
+ `LyraMapMarkerActivationSource`, `LyraMapStyleSpecification`, and `LyraMapInstance`.
22154
22707
  The former `LegendEntry`, `ChoroplethLayer`, `GeoJsonDataLayer`, and `MapMarker` names are removed
22155
22708
  in v9 rather than retained as aliases.
22156
22709
 
@@ -22165,14 +22718,22 @@ shared `maplibre-gl` import without constructing a map or allocating a WebGL con
22165
22718
  `false` when the peer is unavailable, allowing an application to choose a fallback before connecting
22166
22719
  an element.
22167
22720
 
22168
- **Events:** `lr-map-load` (fired once, after the underlying map's own `'load'`), `lr-map-click`
22721
+ **Events:** `lr-map-load` (fired once, after the underlying map's own `'load'`),
22722
+ `lr-map-marker-activate` (non-cancelable; frozen `LyraMapMarkerActivationDetail { id, lngLat,
22723
+ marker, source }`; `id` is the trimmed explicit identity or `undefined`, `marker` is the accepted
22724
+ declarative snapshot, and `source` is `'pointer' | 'keyboard'`), and `lr-map-click`
22169
22725
  (frozen `detail: { readonly lngLat: readonly [lng, lat], readonly feature?, readonly origin?,
22170
22726
  readonly sourceId? }`; the tuple and any hit GeoJSON feature are detached and recursively frozen).
22171
22727
  `feature` resolves against the choropleth fill layer **and** every applied `dataLayers`
22172
- fill/line/circle layer, topmost first — so a shape painted through `dataLayers` is identifiable
22173
- instead of being indistinguishable from empty space. `origin` is `'choropleth'` or `'data-layer'`,
22174
- and `sourceId` carries the authored `dataLayers[].sourceId` for a data-layer hit; both are
22175
- `undefined` whenever `feature` is
22728
+ fill/line/circle/cluster layer, topmost first — so a shape painted through `dataLayers` is
22729
+ identifiable instead of being indistinguishable from empty space. `origin` is `'choropleth'`,
22730
+ `'data-layer'` or `'cluster'`, and `sourceId` carries the authored `dataLayers[].sourceId` for a
22731
+ data-layer or cluster hit; both are `undefined` whenever `feature` is. A cluster hit is reported
22732
+ separately because it is a synthetic aggregate rather than one of your features: its useful payload
22733
+ is MapLibre's `point_count`/`point_count_abbreviated`/`cluster_id` properties, which is what a
22734
+ zoom-to-cluster handler reads. The count label is deliberately not hit-tested (it sits exactly on
22735
+ the circle already queried and would only make the label the topmost hit), and a `kind: 'heatmap'`
22736
+ layer is never queried at all — MapLibre returns no features for a rendered density surface
22176
22737
 
22177
22738
  **Slots:** `legend` — custom legend content, rendered inside the legend panel's own layout so it
22178
22739
  stays positioned with the map instead of floating beside it. Supplying it opens the panel even
@@ -22238,6 +22799,10 @@ Vite with v6:
22238
22799
  { color: "#0969da", label: "High", pattern: "diagonal" },
22239
22800
  ];
22240
22801
  m.markers = [{ lngLat: [2.29, 48.86], label: "Eiffel Tower" }];
22802
+ m.renderWorldCopies = false;
22803
+ m.addEventListener("lr-map-marker-activate", (e) =>
22804
+ console.log(e.detail.id, e.detail.lngLat)
22805
+ );
22241
22806
  m.addEventListener("lr-map-click", (e) =>
22242
22807
  console.log(e.detail.feature?.properties)
22243
22808
  );
@@ -22254,8 +22819,9 @@ https://maplibre.org/maplibre-gl-js/docs/#esm.
22254
22819
  created peer instance, renders only the localized initialization failure, and can retry after a
22255
22820
  new style or reconnect without an unhandled promise rejection. Capability probes and error
22256
22821
  constructors come from the current owner document, including after same-origin adoption.
22257
- - A marker with a popup handles Space at its focus boundary: one activation toggles the peer popup
22258
- and suppresses the page-scroll default. Markers without a popup do not consume Space.
22822
+ - Every marker handles Enter and Space as button activation. Space suppresses the page-scroll
22823
+ default whether or not a popup exists; when one does, MapLibre still receives its own popup
22824
+ toggle and the component emits exactly one `lr-map-marker-activate` notification.
22259
22825
  - clearing or swapping the choropleth no longer leaks the old layer: setting `choropleth =
22260
22826
  undefined`, or changing `choropleth.sourceId` to a different value, now calls `removeLayer`/
22261
22827
  `removeSource` on whatever was previously applied before adding the new one (or nothing, if
@@ -22300,6 +22866,10 @@ undefined`, or changing `choropleth.sourceId` to a different value, now calls `r
22300
22866
  `lr-map-load` never fires until the element is actually scrolled into view. Deliberate: caps
22301
22867
  concurrent WebGL contexts when many `<lr-map>`s sit in one dashboard/grid. Skipped entirely
22302
22868
  (constructs immediately once the peer loads) when `IntersectionObserver` itself is unavailable.
22869
+ - Once constructed, the owner realm's `ResizeObserver` watches the rendered map container and calls
22870
+ the current peer's `resize()` on allocation changes. It is replaced on reconnect/adoption and
22871
+ disconnected before peer teardown, so a stale delivery cannot resize a removed map. Browsers
22872
+ without `ResizeObserver` retain MapLibre's own window-resize behavior.
22303
22873
 
22304
22874
  ---
22305
22875
 
@@ -24299,6 +24869,12 @@ These named interfaces and helper signatures are available to typed integrations
24299
24869
  reason: unknown;
24300
24870
  }`
24301
24871
 
24872
+ - **`components-media-flag-flag-peer-bulk-standard-contracts`** — Supporting data types and helpers for this component family.
24873
+ `registerLyraFlagStandardBulkPeer(): unknown`
24874
+
24875
+ - **`components-media-flag-flag-peer-bulk-contracts`** — Supporting data types and helpers for this component family.
24876
+ `registerLyraFlagBulkPeer(): unknown`
24877
+
24302
24878
  - **`components-media-flag-flag-peer-contracts`** — Supporting data types and helpers for this component family.
24303
24879
  `registerLyraFlagPeer(): unknown`
24304
24880
 
@@ -24333,6 +24909,8 @@ These named interfaces and helper signatures are available to typed integrations
24333
24909
  - **`components-media-map-map-loader-contracts`** — Supporting data types and helpers for this component family.
24334
24910
  `loadMaplibre(): unknown`
24335
24911
  `MapLibreGeoJsonDiff {
24912
+ remove: unknown;
24913
+ add: unknown;
24336
24914
  update: unknown;
24337
24915
  id: unknown;
24338
24916
  addOrUpdateProperties: unknown;
@@ -24426,6 +25004,13 @@ These named interfaces and helper signatures are available to typed integrations
24426
25004
  stops: unknown;
24427
25005
  interpolation: unknown;
24428
25006
  stepBaseColor: unknown;
25007
+ }`
25008
+ `LyraMapClusterOptions {
25009
+ radius: unknown;
25010
+ maxZoom: unknown;
25011
+ radiusSteps: unknown;
25012
+ colorSteps: unknown;
25013
+ countFont: unknown;
24429
25014
  }`
24430
25015
  `LyraMapGeoJsonDataLayer {
24431
25016
  sourceId: unknown;
@@ -24433,6 +25018,17 @@ These named interfaces and helper signatures are available to typed integrations
24433
25018
  tone: unknown;
24434
25019
  color: unknown;
24435
25020
  strokeColor: unknown;
25021
+ kind: unknown;
25022
+ heatmap: unknown;
25023
+ cluster: unknown;
25024
+ }`
25025
+ `LyraMapHeatmapOptions {
25026
+ weightField: unknown;
25027
+ weightRange: unknown;
25028
+ stops: unknown;
25029
+ radius: unknown;
25030
+ intensity: unknown;
25031
+ opacity: unknown;
24436
25032
  }`
24437
25033
  `LyraMapInstance {
24438
25034
  getCanvas: unknown;
@@ -24466,6 +25062,12 @@ These named interfaces and helper signatures are available to typed integrations
24466
25062
  color: unknown;
24467
25063
  label: unknown;
24468
25064
  unsafeHtml: unknown;
25065
+ }`
25066
+ `LyraMapMarkerActivationDetail {
25067
+ id: unknown;
25068
+ lngLat: unknown;
25069
+ marker: unknown;
25070
+ source: unknown;
24469
25071
  }`
24470
25072
  `LyraMapStyleSpecification {
24471
25073
  version: unknown;
@@ -25057,13 +25659,16 @@ passthrough). Not a subclass of `LyraChart`.
25057
25659
  - `barWidth: number = 32` (attribute `bar-width`, px) — each bar's fixed width in `layout="scroll"`
25058
25660
  mode; ignored in the default `'fit'` mode. An excessive value is reduced as needed by the
25059
25661
  1,000,000px scroll-content ceiling.
25060
- - `maxLabels?: number` (attribute `max-labels`, type Number) — decimates which category axis labels
25061
- actually render *text* when `labels.length > maxLabels`: always shows the first and last label,
25062
- and roughly evenly distributes
25063
- the rest between them. Works in either `layout` mode. Unset (the default) renders every label.
25064
- Each rendered category label is allocation-aware: narrow/long text is ellipsized before paint,
25065
- with the complete caller label retained as its accessible name. Independently, the global
25066
- 1,000-record safety sampler may bound both marks and labels for very large category×series input.
25662
+ - `maxLabels?: number | 'auto'` (attribute `max-labels`) — decimates which category axis labels
25663
+ actually render *text*: always shows the first and last label and roughly evenly distributes the
25664
+ rest between them. A number is authoritative. `'auto'` derives the cap after each resize from the
25665
+ resolved plot width and widest rendered caller label, using the same deterministic 7px-per-
25666
+ character estimate as label ellipsis plus 10px of lane breathing room. It therefore responds to
25667
+ either `layout` mode without DOM text measurement or browser-specific font metrics. Unset (the
25668
+ default) renders every label, unchanged. Each rendered category label is allocation-aware:
25669
+ narrow/long text is ellipsized before paint, with the complete caller label retained as its
25670
+ accessible name. Independently, the global 1,000-record safety sampler may bound both marks and
25671
+ labels for very large category×series input.
25067
25672
  - `barX?: (index: number) => number` (attribute: false, bar type only) — overrides the internal
25068
25673
  per-category x-origin formula (`plotX + i * slot`) used by both bars and their axis labels, so a
25069
25674
  consumer can pixel-align this chart's bars with a sibling `<lr-heatmap>` calendar's week columns
@@ -25085,7 +25690,16 @@ passthrough). Not a subclass of `LyraChart`.
25085
25690
  - `skipZero: boolean = false` (attribute `skip-zero`, bar type only) — omits a bar entirely (no
25086
25691
  mark/tabindex/tooltip) for a value that is exactly `0`; `null`/non-finite values are always
25087
25692
  skipped regardless.
25088
- - `valueAxisGutter?: number` (attribute `value-axis-gutter`) — value-axis gutter width.
25693
+ - `valueAxisGutter?: number | 'auto'` (attribute `value-axis-gutter`) — value-axis gutter width in
25694
+ CSS px. A finite number is authoritative (clamped to 0…1,000,000px as before). `'auto'` sizes the
25695
+ gutter from the exact value-tick strings rendered in that pass — `formatter` output first, then
25696
+ `tickFormat`, then the component's `effectiveLocale` number formatting — using the deterministic
25697
+ 7px-per-character estimate plus 14px for the tick offset and font-width variance. Automatic
25698
+ sizing never shrinks below the legacy 36px default. In `layout="fit"` it is bounded to the smaller
25699
+ of 240px or 40% of the measured SVG width, so a pathological formatter result cannot consume the
25700
+ plot. In `layout="scroll"` the cap is 240px: that SVG has an explicit content width, so deriving a
25701
+ percentage cap from its own ResizeObserver result would create a shrinking feedback loop. The
25702
+ gutter remains at logical start under RTL. Unset keeps exactly 36px.
25089
25703
  - `barGapRatio?: number` (attribute `bar-gap-ratio`) — overrides the internal 0.2 `BAR_GROUP_GAP`
25090
25704
  fraction of a category slot left as a gap between categories. Unset keeps the fixed 0.2.
25091
25705
  - `scale: 'linear' | 'sqrt' | 'logarithmic' = 'linear'` — `'sqrt'` (**bar type only**) maps a bar's