@aceshooting/lyra-ui 11.2.0 → 12.0.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 (74) hide show
  1. package/CHANGELOG.md +388 -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/components/charts/chart/chart.class.d.ts +13 -1
  7. package/dist/components/data/funnel/funnel.class.d.ts +92 -0
  8. package/dist/components/data/funnel/funnel.class.js +25 -0
  9. package/dist/components/data/funnel/funnel.d.ts +1 -0
  10. package/dist/components/data/funnel/funnel.js +1 -0
  11. package/dist/components/data/funnel/funnel.styles.d.ts +1 -0
  12. package/dist/components/data/funnel/funnel.styles.js +127 -0
  13. package/dist/components/data/heatmap/heatmap.class.d.ts +195 -13
  14. package/dist/components/data/heatmap/heatmap.class.js +30 -22
  15. package/dist/components/data/heatmap/heatmap.styles.js +36 -0
  16. package/dist/components/data/index.d.ts +1 -0
  17. package/dist/components/data/index.js +1 -1
  18. package/dist/components/forms/date-picker/date-input.class.d.ts +23 -0
  19. package/dist/components/forms/date-picker/date-input.class.js +1 -1
  20. package/dist/components/layout/app-rail/app-rail-item.class.d.ts +16 -0
  21. package/dist/components/layout/app-rail/app-rail-item.class.js +4 -4
  22. package/dist/components/layout/filter-bar/filter-bar.class.d.ts +32 -3
  23. package/dist/components/layout/filter-bar/filter-bar.class.js +2 -1
  24. package/dist/components/layout/widget/widget.class.d.ts +16 -0
  25. package/dist/components/layout/widget/widget.class.js +2 -2
  26. package/dist/components/lr-funnel.d.ts +1 -0
  27. package/dist/components/lr-funnel.js +1 -0
  28. package/dist/components/media/flag/flag-peer-bulk-standard.d.ts +25 -0
  29. package/dist/components/media/flag/flag-peer-bulk-standard.js +1 -0
  30. package/dist/components/media/flag/flag.class.d.ts +8 -0
  31. package/dist/components/media/flag/flag.class.js +1 -1
  32. package/dist/components/media/map/map.class.d.ts +196 -15
  33. package/dist/components/media/map/map.class.js +1 -1
  34. package/dist/components/overlays/chip/chip-group.class.d.ts +16 -0
  35. package/dist/components/overlays/chip/chip-group.class.js +10 -3
  36. package/dist/custom-elements-jsx.d.ts +12 -4
  37. package/dist/internal/autoloader-manifest.js +1 -1
  38. package/dist/internal/autoloader-tags.d.ts +1 -1
  39. package/dist/internal/autoloader-tags.js +1 -1
  40. package/dist/internal/lyra-element.d.ts +7 -1
  41. package/dist/internal/lyra-element.js +1 -1
  42. package/dist/internal/package-metadata.d.ts +1 -1
  43. package/dist/internal/package-metadata.js +1 -1
  44. package/dist/internal/root-registration-allowlist.d.ts +1 -1
  45. package/dist/internal/root-registration-allowlist.js +1 -1
  46. package/dist/lyra.d.ts +4 -2
  47. package/dist/lyra.js +1 -1
  48. package/dist/ssr/all.d.ts +1 -0
  49. package/dist/ssr/all.js +1 -1
  50. package/dist/ssr.d.ts +4 -4
  51. package/dist/styles/tokens-root.css +488 -0
  52. package/dist/svelte.d.ts +13 -4
  53. package/dist/vue.d.ts +12 -4
  54. package/llms/components/lr-app-rail-item.md +8 -1
  55. package/llms/components/lr-app-rail.md +6 -0
  56. package/llms/components/lr-chip-group.md +7 -0
  57. package/llms/components/lr-chip.md +7 -0
  58. package/llms/components/lr-date-input.md +19 -1
  59. package/llms/components/lr-date-picker.md +19 -1
  60. package/llms/components/lr-filter-bar.md +50 -1
  61. package/llms/components/lr-flag.md +14 -0
  62. package/llms/components/lr-funnel.md +122 -0
  63. package/llms/components/lr-heatmap.md +51 -7
  64. package/llms/components/lr-map.md +65 -8
  65. package/llms/components/lr-widget.md +7 -1
  66. package/llms/index.md +3 -2
  67. package/llms/peers.md +1 -1
  68. package/llms/shared.md +110 -1
  69. package/llms-full.txt +468 -18
  70. package/llms.txt +1 -1
  71. package/package.json +25 -6
  72. package/vscode-css-data.json +42 -0
  73. package/vscode-html-data.json +96 -17
  74. package/web-types.json +32714 -317
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;
@@ -4960,9 +5069,21 @@ default). An inherited or direct public value remains authoritative in every tie
4960
5069
  Text field + calendar popover, **form-associated** via the shared `FormAssociated` mixin (`name`,
4961
5070
  `value`, `disabled`, `required` all inherited).
4962
5071
 
4963
- **Properties (42):**
5072
+ **Properties (44):**
4964
5073
 
4965
5074
  - `appearance: 'filled'|'outlined'|'filled-outlined' = 'outlined'` (reflected)
5075
+ - `appliedPreset: LyraDateRangePreset | undefined` (read-only, new in 11.3.0) — the `presets` entry
5076
+ whose button produced the current `value`, or `undefined` when the value was picked on the
5077
+ calendar, typed into the field, cleared, or reset. Read it inside your own `change`/`input`
5078
+ handler; it is updated before those events are relayed, so a handler observes the preset that
5079
+ caused the very commit it is handling, and it is `undefined` while the popover has never been
5080
+ opened. Mirrors the nested `lr-date-picker`'s identically-named property across this component's
5081
+ shadow boundary, which is where the readback is actually needed: the compact
5082
+ text-field-plus-popover shape is the one a dashboard filter uses, the nested picker instance is
5083
+ unreachable from outside (a CSS part cannot yield it), and the fact is not recoverable from
5084
+ `value` — re-deriving it by string-matching is the mapping table `presets` exists to delete and is
5085
+ ambiguous anyway. A property rather than an event detail because `input`/`change` here are
5086
+ **native** events that cannot carry one
4966
5087
  - `assumeInteractionOn: string[] = ['input']` (JS only)
4967
5088
  - `autocomplete: string = ''`
4968
5089
  - `dayContent?: LyraDatePickerDayContent` (JS only)
@@ -4987,6 +5108,12 @@ Text field + calendar popover, **form-associated** via the shared `FormAssociate
4987
5108
  - `pill: boolean = false` (reflected)
4988
5109
  - `placement: LyraDateInputPlacement = 'bottom-start'` (reflected; all 12 side/alignment
4989
5110
  placements are accepted)
5111
+ - `presets: LyraDateRangePreset[] = []` (JS only, new in 11.1.0) — forwarded verbatim to the nested
5112
+ `lr-date-picker`, whose own `presets` documents the semantics (range mode only, open bounds
5113
+ resolving to `min`/`max`, unset renders nothing). Forwarded rather than reimplemented because the
5114
+ picker lives in this component's shadow root, so a consumer has no route to it. The row's parts
5115
+ are re-exported as `presets`/`preset-button`, and `appliedPreset` above reports which entry
5116
+ produced the current value
4990
5117
  - `readonly: boolean = false` and `required: boolean = false` (reflected)
4991
5118
  - `size: LyraSize = 'm'` (reflected; `2xs`–`xl` and aliases)
4992
5119
  - `today: string = ''` (reflected ISO override)
@@ -10675,6 +10802,114 @@ Dependency-free SVG radial, full-circle ring, or linear meter (no charting libra
10675
10802
 
10676
10803
  ---
10677
10804
 
10805
+ ## `lr-funnel`
10806
+
10807
+ Dependency-free conversion funnel (no charting library): an ordered set of stages, each drawn as a
10808
+ bar whose length is that stage's share of the **first** stage, read top-to-bottom as progressive
10809
+ drop-off. It sits beside `lr-gauge` and `lr-heatmap` as an analytics primitive rather than a general
10810
+ chart type.
10811
+
10812
+ It is deliberately not a sorted bar chart: it normalizes to the first stage rather than to the data
10813
+ maximum, draws no value axis, and reads as stage-to-stage retention rather than category comparison.
10814
+ Reach for `lr-bar-chart`/`lr-lite-chart` when you actually want a value axis and category comparison.
10815
+
10816
+ The whole chart is plain HTML — stage names, absolute values, shares and drop-off percentages are
10817
+ real text inside an `<ol>`, so there is no sighted-only drawing needing a separate transcript. The
10818
+ list carries the accessible name; a host `aria-label` overrides `label`.
10819
+
10820
+ **Properties:**
10821
+
10822
+ - `stages: readonly LyraFunnelStage[] = []` (property only; every share is measured against
10823
+ `stages[0]`)
10824
+ - `comparison: readonly LyraFunnelStage[] = []` (property only — an optional baseline/peer cohort
10825
+ drawn behind each bar as a dashed outline, normalized to **its own** first stage so a cohort's
10826
+ funnel *shape* stays comparable against a baseline whose absolute volumes are not)
10827
+ - `comparisonLabel: string = ''` (attribute `comparison-label`; falls back to a localized generic
10828
+ label)
10829
+ - `label: string = ''` (accessible name for the stage list)
10830
+ - `dropoff: boolean = true` (reflected; `dropoff="false"` in markup really does turn it off —
10831
+ the property uses the explicit `false`-parsing converter, not attribute presence)
10832
+ - `sharePrecision: number = 0` (attribute `share-precision`; fraction digits for every share and
10833
+ drop-off percentage, clamped to `0`–`20`)
10834
+
10835
+ ```ts
10836
+ interface LyraFunnelStage {
10837
+ readonly label: string;
10838
+ readonly value: number;
10839
+ readonly color?: string; // CSS color; unparseable values fall back to --lr-funnel-bar-color
10840
+ }
10841
+ ```
10842
+
10843
+ **Events:** none.
10844
+
10845
+ **Slots:** none.
10846
+
10847
+ **CSS parts:** `base`, `stages` (the `<ol>`), `stage`, `dropoff`, `stage-header`, `stage-label`,
10848
+ `stage-value`, `stage-share`, `comparison-value`, `track`, `bar`, `bar-overflow` (a second token on
10849
+ `bar` when the stage exceeds the first stage), `comparison-bar`, `empty`
10850
+
10851
+ **Themeable custom properties:** `--lr-funnel-bar-color` (default `var(--lr-color-brand)`),
10852
+ `--lr-funnel-comparison-color` (default `var(--lr-color-border-strong)`), `--lr-funnel-track-color`
10853
+ (default `var(--lr-color-surface-raised)`), `--lr-funnel-bar-size` (track thickness, default
10854
+ `var(--lr-size-1-5rem)`).
10855
+
10856
+ **Optional peer deps:** none.
10857
+
10858
+ ```html
10859
+ <script type="module">
10860
+ import '@aceshooting/lyra-ui/components/data/funnel/funnel.js';
10861
+ </script>
10862
+
10863
+ <lr-funnel id="signup" label="Self-serve signup" share-precision="1"></lr-funnel>
10864
+
10865
+ <script type="module">
10866
+ const funnel = document.querySelector('#signup');
10867
+ funnel.stages = [
10868
+ { label: 'Visited pricing', value: 12480 },
10869
+ { label: 'Started trial', value: 4310 },
10870
+ { label: 'Converted to paid', value: 512 },
10871
+ ];
10872
+ // Normalized to ITS OWN first stage, so a 380-visitor account still compares against a
10873
+ // 12,480-visitor peer group.
10874
+ funnel.comparison = [
10875
+ { label: 'Visited pricing', value: 380 },
10876
+ { label: 'Started trial', value: 141 },
10877
+ { label: 'Converted to paid', value: 12 },
10878
+ ];
10879
+ funnel.comparisonLabel = 'Acme Corp';
10880
+ </script>
10881
+ ```
10882
+
10883
+ **Known gotchas:**
10884
+
10885
+ - **Shares are of the first stage, not of the previous one.** `stage-share` answers "how much of the
10886
+ top of the funnel is left here?"; the separate `dropoff` row answers "what changed since the
10887
+ previous stage?". Both are rendered because the percentage is usually the interesting number and
10888
+ the absolute count is the credibility check.
10889
+ - **A zero or negative first stage cannot define a share.** Every `stage-share` is omitted, every bar
10890
+ is zero-length, and only the absolute values render. Nothing is silently divided by zero and no
10891
+ `NaN` reaches the geometry.
10892
+ - **A stage larger than its predecessor is legal** (funnel re-entry). Its share is reported
10893
+ truthfully above 100% in text, while the bar clamps to the track and gains the `bar-overflow` part
10894
+ token so you can style it. Drop-off for that stage reads as an increase.
10895
+ - **Drop-off is omitted, not zeroed, when the previous stage is non-positive** — a change relative to
10896
+ zero is undefined.
10897
+ - **A comparison series of a different length pairs by index.** Extra comparison entries are ignored;
10898
+ stages past its end simply get no comparison bar. A comparison series whose own first value is
10899
+ zero or negative draws nothing.
10900
+ - **Non-finite `value`s are treated as `0`** rather than blanking the stage, so one bad row cannot
10901
+ take the chart with it.
10902
+ - Values, shares and drop-off percentages all format through the component's effective locale
10903
+ (`locale` attribute, or an inherited one) — never a hardcoded `en`.
10904
+ - Bars grow from the inline-start edge and every dimension is a logical property, so `dir="rtl"`
10905
+ needs no extra work. The component never sets its own `dir`.
10906
+ - Layout responds to the **container** (`container-type: inline-size` on `base`), not the viewport:
10907
+ below roughly `18rem` of allocated width the stage name moves to its own line so the value and
10908
+ share stay together. Bar motion is a token-driven transition that stops under
10909
+ `prefers-reduced-motion`.
10910
+ - No events, no interaction model, no zoom/pan. If you need a clickable funnel, wrap the element and
10911
+ handle clicks yourself.
10912
+
10678
10913
  ## `lr-word-cloud`
10679
10914
 
10680
10915
  Dependency-free SVG word/tag cloud. First-party invention (no Web Awesome equivalent). Lays words
@@ -10890,6 +11125,38 @@ weekdayLabelText?: (jsWeekday:number)=>string|undefined; monthLabelText?:
10890
11125
  `colLabelHeight="auto"` to have the band size itself to the rotated extent. **Not mirrored under
10891
11126
  `dir="rtl"`** — both grid modes deliberately retain physical LTR geometry, so leaning one axis'
10892
11127
  labels the other way would be incoherent
11128
+ - `stickyLabels: 'none' | 'rows' | 'cols' | 'both' = 'none'` (attribute `sticky-labels`, reflected)
11129
+ — freezes a **matrix** label band against the grid's own scrolling. `'rows'` pins the row-label
11130
+ gutter so it survives horizontal scrolling, `'cols'` pins the column-label band so it survives
11131
+ vertical scrolling, `'both'` pins both. Labels and cells otherwise share one bitmap, so neither
11132
+ band can be `position: sticky` on its own and a tall matrix scrolls its column header out of
11133
+ view; the only workaround was a light-DOM mirror row that had to hardcode the gutter width and
11134
+ cell size, which made it mutually exclusive with `row-label-width="auto"`. A frozen band is
11135
+ repainted into its own layer in the same draw pass, from the same resolved `matrixGeometry` the
11136
+ cells were painted with, so it tracks a `row-label-width`/`col-label-height` `"auto"`
11137
+ re-resolution, a resize, and a DPR change without drifting a pixel. Freezing needs something to
11138
+ scroll, so the frozen modes wrap the grid in a `[part="grid"]` scrollport: it is bounded inline by
11139
+ the host's own allocation — a matrix wider than a 320px host scrolls inside the component instead
11140
+ of overflowing it — and unbounded in block until you set `--lr-heatmap-grid-max-block-size`, which
11141
+ a frozen column band needs in order to have vertical scrolling to stay behind. The bands are
11142
+ `aria-hidden` duplicates of pixels the canvas already painted, so the accessible representation is
11143
+ unchanged, and the `accessibleCells` overlay moves inside the scrollport so the cell buttons
11144
+ scroll with the canvas they cover. The grid keeps this component's physical LTR geometry under
11145
+ `dir="rtl"`, so the frozen gutter stays on the same physical side as the labels the canvas paints.
11146
+ Matrix mode only, like `matrixGeometry`: the property is read in calendar mode but has no effect
11147
+ there, since a calendar's axes are a different geometry (fixed weekday gutter, month band, and the
11148
+ optional `columnX`/`rowY` overrides). Unset (`'none'`, the default) renders exactly what it always
11149
+ did — one canvas, no scrollport, no extra elements — and an unsupported value normalizes to it.
11150
+ The union is re-exported from the package root as `LyraHeatmapStickyLabels`, so a typed consumer
11151
+ can annotate a variable or a framework prop with it. Everything positioned in canvas coordinates
11152
+ moves into the scrollport with the cells: `[part="tooltip"]` renders inside it, so it stays on
11153
+ the cell it describes through a scroll instead of drifting by the scroll offset, and because
11154
+ `overflow: auto` clips whatever leaves the scrollport, the tooltip is kept inside the visible
11155
+ window too — clamped along the inline axis and flipped to below its cell when a frozen band
11156
+ leaves no room above it. Arrow-key navigation scrolls the focused cell into that window, clear of
11157
+ the frozen bands, which the frozen modes need in their own right: the canvas is the roving tab
11158
+ stop, its focus ring is painted into the bitmap rather than carried by a focusable element the
11159
+ browser would scroll to, and it calls `preventDefault()` on the arrows
10893
11160
  - `maxCellSize?: number` (attribute `max-cell-size`) — ceiling, in CSS px, on the cell size
10894
11161
  `fitToWidth` derives from the host width, in **both** modes. Exists because `fitToWidth` divides
10895
11162
  the _whole_ host width across the grid, so a 5-week calendar or a 3-column matrix in a wide pane
@@ -11029,9 +11296,12 @@ color?: string; label?: string; partOfRamp?: boolean }`: a discrete legend key r
11029
11296
  color-scheme change; called automatically on theme changes, exposed for a consumer that needs to
11030
11297
  force a redraw manually. `matrixGeometry: Readonly<{ padLeft: number; padTop: number; cellSize:
11031
11298
  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.
11299
+ in CSS pixels; `undefined` in calendar mode. Lets a light-DOM consumer line up with the canvas
11300
+ without hardcoding the same numbers `row-label-width`/`col-label-height`'s `"auto"` resolution would
11301
+ otherwise keep private. For the case that motivated it — a frozen header or gutter on a tall or wide
11302
+ matrix — prefer `stickyLabels`, which freezes the band inside the component and needs no mirror at
11303
+ all; the getter remains the way to align a *separate* element (a sibling chart, a custom overlay)
11304
+ with the grid.
11035
11305
 
11036
11306
  **Events:** `lr-cell-click` (fired on click, or Enter/Space on the keyboard-focused cell —
11037
11307
  `detail: { row, col, value }` in matrix mode, `detail: { date, value }` in calendar mode),
@@ -11042,8 +11312,12 @@ in calendar mode)
11042
11312
 
11043
11313
  **Slots:** none.
11044
11314
 
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),
11315
+ **CSS parts:** `base`, `canvas`, `grid` (the scrollport wrapping the canvas while `stickyLabels`
11316
+ freezes an axis — absent entirely otherwise), `row-labels`/`col-labels` (the frozen label bands,
11317
+ rendered only for the axis `stickyLabels` names), `cells` (opt-in per-cell overlay), `cell` (one opt-in native cell
11318
+ button), `tooltip` (hover tooltip, positioned over the hovered cell — inside `[part="grid"]` while
11319
+ `stickyLabels` freezes an axis, so it scrolls with the cells, and a `[part="base"]` child
11320
+ otherwise),
11047
11321
  `live-region` (visually-hidden, `aria-hidden` mirror of the keyboard-focused cell; the actual
11048
11322
  announcement uses the shared light-DOM polite sink), `projection-limit` (localized assistive
11049
11323
  disclosure when bounded canonicalization truncates input), `legend`, `legend-lo`, `legend-hi` (both omitted, along with the gradient
@@ -11079,7 +11353,12 @@ dedicated token distinct from both the focus ring and the annotation ring so a h
11079
11353
  independently). `--lr-heatmap-tooltip-bg` (default
11080
11354
  `var(--lr-color-surface)`) and `--lr-heatmap-tooltip-text` (default `var(--lr-color-text)`) —
11081
11355
  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
11356
+ directly, no `getComputedStyle` bridging needed. `--lr-heatmap-sticky-label-bg` (default
11357
+ `var(--lr-color-surface)` — the backdrop painted under a frozen `stickyLabels` band, resolved via
11358
+ `getComputedStyle` like the other canvas-drawn tokens; it must stay **opaque**, since it covers the
11359
+ same labels the scrolling canvas painted underneath it) and `--lr-heatmap-grid-max-block-size`
11360
+ (default `none` — the block-size ceiling of the `[part="grid"]` scrollport, consumed directly by
11361
+ 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
11362
  color), `--lr-space-xs`, `--lr-radius`/`--lr-shadow` (tooltip box), and `--lr-focus-ring-width`/
11084
11363
  `--lr-focus-ring-offset` (the real `[part="canvas"]:focus-visible` DOM outline, stroked in the
11085
11364
  same color as `--lr-heatmap-focus-ring-color`).
@@ -12975,6 +13254,13 @@ These named interfaces and helper signatures are available to typed integrations
12975
13254
  maxZoom: unknown;
12976
13255
  }`
12977
13256
 
13257
+ - **`components-data-funnel-funnel-contracts`** — Supporting data types and helpers for this component family.
13258
+ `LyraFunnelStage {
13259
+ label: unknown;
13260
+ value: unknown;
13261
+ color: unknown;
13262
+ }`
13263
+
12978
13264
  - **`components-data-graph-query-builder-graph-query-builder-contracts`** — Supporting data types and helpers for this component family.
12979
13265
  `GraphQueryDeleteDetail {
12980
13266
  queryId: unknown;
@@ -13648,6 +13934,12 @@ TemplateResult; ariaLabel?: string }`. Each entry gets a header toggle button
13648
13934
  - `activeViewId: string = ''` (attribute: false) — the currently active view's `viewId`; defaults to the
13649
13935
  first entry of `views` (or `''` when `views` is empty). Settable directly to control the active
13650
13936
  view externally; also updated internally when a view toggle is clicked.
13937
+ - `activeView: string = ''` (attribute: false) — **deprecated alias for `activeViewId`**, which it
13938
+ seeds. `activeView` was this member's original public name and the rename was never announced, so
13939
+ a shipped `.activeView=${…}` binding silently became inert and the widget fell back to its first
13940
+ view. It seeds rather than being read alongside, because the component itself writes
13941
+ `activeViewId` (a toggle click, and the fallback when `views` drops the active id) — so a stale
13942
+ alias must not undo a later interactive change. Prefer `activeViewId` in new code.
13651
13943
  - `accessibleLabel: string | null = null` (attribute `aria-label`) — overrides the label-derived
13652
13944
  fullscreen dialog name. An explicitly empty value is retained; property, slotted-label, and
13653
13945
  localized fallbacks apply only when it is absent.
@@ -15317,6 +15609,12 @@ removing the label from the accessibility tree.
15317
15609
  reflects `aria-current="page"` on `[part='base']` and drives the current visual treatment. The rail
15318
15610
  has no built-in routing, so the consumer sets this per item (e.g. by comparing `href` against the
15319
15611
  current location).
15612
+ - `active: boolean = false` — **deprecated alias for `current`**, read alongside it: the item is
15613
+ current when either is true. `active` was this member's original public name, in both property and
15614
+ attribute form; it was renamed to `current` without an alias, so shipped consumers writing
15615
+ `.active=${…}` or `<lr-app-rail-item active>` silently lost their current-item indicator and kept a
15616
+ permanent `aria-current="false"`. A Lit property binding on a custom element is untyped, so nothing
15617
+ in a consumer's type check or test suite could catch it. Prefer `current` in new code.
15320
15618
  - `tooltip: boolean = false` (reflected) — opt-in hover/focus flyout (`[part='tooltip']`) showing
15321
15619
  this item's label text while the rail's `icon-only` mode (set externally by the parent
15322
15620
  `<lr-app-rail>` as the viewport narrows) hides it from view. No effect outside icon-only mode,
@@ -16550,7 +16848,8 @@ value therefore stays inside a 320px LTR or RTL bar, with the chip's own label e
16550
16848
  overflow ownership rather than widening the page.
16551
16849
 
16552
16850
  Each edit exposes one filter-bar `lr-input` carrying a detached, deeply frozen snapshot of the
16553
- complete value object. A built-in or
16851
+ complete value object, plus the `filterId` that changed and (new in 11.3.0) `appliedPreset` — see
16852
+ "Date-range quick ranges" below. A built-in or
16554
16853
  custom control's own `lr-input`/`lr-change` aliases stay inside the wrapper so their incompatible
16555
16854
  detail shapes cannot escape as duplicate bar events; native-style `input`/`change` events from the
16556
16855
  composed controls continue bubbling normally.
@@ -16600,6 +16899,54 @@ to `<lr-input>` for an open-ended free-text query rather than a closed choice se
16600
16899
  filter's value is the raw query string, verbatim, and its chip shows exactly that string — the same
16601
16900
  text the user typed, not a truncated or normalized form.
16602
16901
 
16902
+ ### Date-range quick ranges
16903
+
16904
+ A `'date-range'` definition also accepts `presets?: readonly LyraDateRangePreset[]` (new in 11.3.0),
16905
+ forwarded to its composed `<lr-date-input>` exactly like `min`/`max`, so the quick-range row
16906
+ ("Today", "Last 7 days", "All time") renders inside that filter's own calendar popover. Entries are
16907
+ `LyraDateRangePreset { label, start?, end? }` with ISO `YYYY-MM-DD` bounds; an omitted bound is open
16908
+ and resolves to the filter's `min`/`max`, and an open bound with no corresponding limit renders that
16909
+ button disabled. `presets` is deliberately **not** accepted on a single `'date'` filter: a preset
16910
+ names two dates, so `lr-date-picker` ignores the list outside range mode, and a list passed there is
16911
+ dropped rather than rendering a row that cannot do anything.
16912
+
16913
+ The `lr-input` emitted by such a commit carries `appliedPreset`, the definition entry whose button
16914
+ produced it — the bar's own frozen snapshot, so it compares identical to `filters[i].presets[j]`. It
16915
+ is `undefined` for every other filter type and for a range picked or typed by hand. A filter bar
16916
+ whose values round-trip through a query string needs it because `value` holds only the frozen ISO
16917
+ range: persisting "Last 7 days" as a preset id keeps it meaning the last 7 days after the next
16918
+ reload, and re-deriving it by string-matching `value` is both the mapping table `presets` exists to
16919
+ delete and ambiguous (Today and This month coincide on the 1st). It rides the event rather than
16920
+ `value` because it is metadata about one edit, not a filter value — `value` stays the plain,
16921
+ JSON-serializable record it has always been.
16922
+
16923
+ ```ts
16924
+ const filters: LyraFilterBarFilterDefinition[] = [
16925
+ {
16926
+ filterId: "period",
16927
+ label: "Reporting period",
16928
+ type: "date-range",
16929
+ min: "2020-01-01",
16930
+ max: "2030-12-31",
16931
+ presets: [
16932
+ { label: "Last 7 days", start: "2026-08-13", end: "2026-08-19" },
16933
+ { label: "This month", start: "2026-08-01", end: "2026-08-31" },
16934
+ { label: "All time" },
16935
+ ],
16936
+ },
16937
+ ];
16938
+
16939
+ bar.addEventListener("lr-input", (event) => {
16940
+ const { value, filterId, appliedPreset } = event.detail;
16941
+ persist({ ...value, periodPreset: appliedPreset?.label });
16942
+ });
16943
+ ```
16944
+
16945
+ Before this, the only way to give a filter-bar date range a quick-range row was `type: 'custom'`,
16946
+ which means hand-rendering an `lr-date-input` plus a full adapter (`clearValue`, `isEmpty`,
16947
+ `formatValue`) to set one property, and forfeits the built-in date-range chip localization described
16948
+ next.
16949
+
16603
16950
  Date chips localize exactly one round-trip-valid ISO `YYYY-MM-DD` segment; date-range chips require
16604
16951
  exactly two slash-separated segments. Four-digit
16605
16952
  years `0000`–`0099` retain those literal years rather than inheriting JavaScript's 1900 offset;
@@ -17041,6 +17388,7 @@ These named interfaces and helper signatures are available to typed integrations
17041
17388
  }`
17042
17389
  `LyraFilterBarDateRangeDefinition {
17043
17390
  type: unknown;
17391
+ presets: unknown;
17044
17392
  min: unknown;
17045
17393
  max: unknown;
17046
17394
  filterId: unknown;
@@ -17052,6 +17400,7 @@ These named interfaces and helper signatures are available to typed integrations
17052
17400
  `LyraFilterBarInputDetail {
17053
17401
  value: unknown;
17054
17402
  filterId: unknown;
17403
+ appliedPreset: unknown;
17055
17404
  }`
17056
17405
  `LyraFilterBarOption {
17057
17406
  value: unknown;
@@ -18294,6 +18643,13 @@ no `.items` array prop).
18294
18643
 
18295
18644
  **Properties:**
18296
18645
 
18646
+ - `accessibleLabel: string = ''` (attribute `aria-label`) — accessible name for the group,
18647
+ forwarded to `[part='base']`, which carries `role='group'` whenever a name is supplied. Supply
18648
+ one for any chip row that means something as a set (a filter row, a tag list); without it the
18649
+ container stays roleless, because an unnamed group role adds verbosity without information.
18650
+ A host `aria-label` does **not** reach a shadow root on its own — this property is what makes it
18651
+ reach `[part='base']`, the element that owns the role. Mirrors how `lr-radio-group` and
18652
+ `lr-segmented` name their own `radiogroup`.
18297
18653
  - `maxVisible?: number` (attribute `max-visible`) — maximum number of assigned children shown before
18298
18654
  the rest collapse behind a "+N" indicator; flattened slot-forwarded children count the same as
18299
18655
  direct children. Author-hidden or inert children do not consume capacity or inflate the hidden
@@ -21895,6 +22251,20 @@ import "@aceshooting/lyra-ui/components/media/flag/flag-peer.js";
21895
22251
  unneeded 249-entry fetch. Only the standard tier is bulk-fetched this way;
21896
22252
  `fidelity="compact"/"detailed"` on individual elements still resolves through its own lazy
21897
22253
  per-code loader.
22254
+ - Rendering many flags at once **and** leaving every `<lr-flag>` on the default
22255
+ `fidelity="standard"`: import
22256
+ `@aceshooting/lyra-ui/components/media/flag/flag-peer-bulk-standard.js` instead of
22257
+ `flag-peer-bulk.js` (never more than one of the three peer entries — each
22258
+ `setFlagUrlResolver()` call replaces the previous resolver). It registers
22259
+ `@aceshooting/lyra-flags/standard`'s `createFlagUrlResolver()`, which is backed by the same
22260
+ standard-tier-only eager map as the root's, without statically importing the detailed and
22261
+ compact loader maps the root entry needs for its per-call `variant`. Those maps are what
22262
+ `flag-peer-bulk.js` pays for its batching: on a real production build with a 156-country flag
22263
+ column, routing bulk resolution through the package root emitted +65 detailed SVGs and +31
22264
+ compact WebPs — +15.8MB of assets no route rendered — swamping the chunk-count win the bulk path
22265
+ exists for. The tradeoff is the tier commitment: `fidelity="compact"/"detailed"` on an individual
22266
+ element resolves to that code's standard asset instead (a silent no-op, not an error), so use
22267
+ `flag-peer-bulk.js` when per-instance fidelity must actually be honoured.
21898
22268
  - 65 of `@aceshooting/lyra-flags`' 249 flags (any whose design includes a detailed coat of
21899
22269
  arms/seal/emblem, e.g. `es`, `pt`, `sv`) ship **three** fidelity tiers, selected via the
21900
22270
  `fidelity` property (`flagUrl(code, { variant: fidelity })` under the hood): `"compact"` — a tiny WebP raster for
@@ -22006,8 +22376,9 @@ dimming at `itemCount <= 1`), `--lr-focus-ring-*`.
22006
22376
  ## `lr-map`
22007
22377
 
22008
22378
  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.
22379
+ and additive `dataLayers` — plain GeoJSON, natively clustered points, or a heatmap density surface
22380
+ plus a peer-neutral `map` getter for common imperative operations. Its runtime value is the
22381
+ underlying MapLibre map.
22011
22382
 
22012
22383
  **Properties:**
22013
22384
 
@@ -22090,7 +22461,8 @@ string; geojson: GeoJSON.FeatureCollection; field: string; stops: [number, strin
22090
22461
  - `dataLayers: LyraMapGeoJsonDataLayer[] = []` (attribute: false) —
22091
22462
  `LyraMapGeoJsonDataLayer { sourceId: string; geojson: GeoJSON.Feature |
22092
22463
  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
22464
+ 'danger' | 'neutral'; color?: string; strokeColor?: string; kind?: LyraMapDataLayerKind;
22465
+ heatmap?: LyraMapHeatmapOptions; cluster?: LyraMapClusterOptions }`. `sourceId` is trimmed and must be nonempty; the first layer for a
22094
22466
  `sourceId` wins and blank or later duplicate records are ignored. Each retained entry adds one
22095
22467
  GeoJSON source plus three geometry-filtered layers
22096
22468
  (fill, line, and circle, so a mixed `FeatureCollection` renders correctly), colored from the
@@ -22113,6 +22485,55 @@ GeoJSON.FeatureCollection; tone?: 'accent' | 'success' | 'warning' |
22113
22485
  persists across a `dataLayers` reassignment gets its GeoJSON updated in place (`setData()`), one
22114
22486
  that's dropped has its private source/layers removed, and a genuinely new `sourceId` gets new
22115
22487
  resources — nothing leaks on removal, style change, or disconnect.
22488
+
22489
+ `cluster` and `kind` (both new in 11.3.0) opt one entry out of that three-layer geometry split.
22490
+ **Both are strictly additive: an entry that sets neither renders exactly what it rendered before,
22491
+ down to the layer ids and the point layer's filter.**
22492
+
22493
+ `cluster?: LyraMapClusterOptions { radius?: number; maxZoom?: number; radiusSteps?: [number,
22494
+ number][]; colorSteps?: [number, string][]; countFont?: string[] }` turns the entry's source into a
22495
+ natively clustered one — `cluster`/`clusterRadius` (default 50)/`clusterMaxZoom` (default 14) on
22496
+ the source, plus a `${sourceId}-cluster` circle filtered on `has('point_count')`, a
22497
+ `${sourceId}-cluster-count` label, and a `${sourceId}-circle` layer for the points that stayed
22498
+ unclustered. `cluster: {}` opts in at every default. This is what a thousands-of-points map needs
22499
+ and what `markers` cannot be: `markers` mints one real, individually focusable DOM element per
22500
+ entry, which is right for tens of pins and both unreadable and expensive for thousands.
22501
+ `radiusSteps` and `colorSteps` are `['step', …]` breaks keyed on `point_count`, in the same
22502
+ ascending `[value, output]` vocabulary as `choropleth.stops` — including the same base rule, where
22503
+ the first entry's own output covers everything below the first threshold, and the same colour
22504
+ resolution, where a `var(--lr-…)` reference in a `colorSteps` entry is resolved against the host
22505
+ before it reaches MapLibre (which paints to a WebGL canvas and never sees the CSS cascade), so a
22506
+ retheme moves the cluster breaks with everything else. No fill or line layer is created for a
22507
+ clustered entry, because MapLibre's clustering keeps point features only. The count
22508
+ label needs glyphs: a style that declares none gets the graduated circles without the numbers
22509
+ (adding a text layer against a glyph-less style paints nothing and only emits peer errors), and
22510
+ `countFont` names the font stack when your style's glyph source lacks MapLibre's spec default.
22511
+ Cluster options are baked into the source at creation time by MapLibre and have no setter, so
22512
+ changing them (or `kind`) rebuilds that one entry's source and layers; every other update still
22513
+ reconciles in place.
22514
+
22515
+ `kind?: 'auto' | 'heatmap'` (`LyraMapDataLayerKind`, default `'auto'` — today's geometry split)
22516
+ renders the source as MapLibre's own first-class `heatmap` layer instead, which the geometry split
22517
+ cannot express at all: thousands of overlapping circles read as one opaque blob rather than as
22518
+ where the data is concentrated. `heatmap?: LyraMapHeatmapOptions { weightField?: string;
22519
+ weightRange?: [number, number]; stops?: [number, string][]; radius?: number; intensity?: number }`
22520
+ configures it. `weightField` weights each point by a feature property, and `weightRange` maps that
22521
+ property's own units onto the 0–1 weight MapLibre expects — without it the raw value is passed
22522
+ through, which saturates the surface for any quantity above ~1; with neither, every point weighs 1.
22523
+ `stops` are `[density, color]` pairs with density in `[0, 1]`, **the same `[value, color]`
22524
+ vocabulary `choropleth.stops` and `legendGradient` already share**, so a `legendGradient` bar can
22525
+ describe the ramp without a second copy of it, and `var(--lr-…)` stops resolve against the host the
22526
+ same way `color`/`strokeColor` do. A ramp that doesn't start at density 0 gets a fully transparent
22527
+ stop prepended, because a coloured zero tints the entire map — so **a single stop is already a
22528
+ complete ramp**, as long as it sits above density 0: `stops: [[1, '#ff0000']]` is exactly
22529
+ transparent → red. The one authored ramp that can't be honoured is a lone stop AT density 0, which
22530
+ describes a flat colour rather than a gradient; that one — like an unset or wholly unusable
22531
+ `stops` — falls back to the built-in ramp, which runs transparent → `--lr-color-brand` →
22532
+ `--lr-color-success` → `--lr-color-warning` → `--lr-color-danger`, so a retheme moves the density
22533
+ surface with everything else. `radius`
22534
+ (default 30) and `intensity` (default 1) are MapLibre's own. `cluster` is ignored on a heatmap
22535
+ entry: a heatmap already aggregates density, and clustering its input would feed it one point per
22536
+ cluster instead of the real distribution.
22116
22537
  - `maxBounds: LyraMapBounds | null = null` (attribute: false) — box the map may not pan outside,
22117
22538
  `[[west, south], [east, north]]`. Prefer it over calling `map.setMaxBounds()` through the `.map`
22118
22539
  escape hatch: constraining the camera can wedge maplibre-gl at a sub-1 fractional zoom in a wide
@@ -22150,7 +22571,8 @@ payload beside the map.
22150
22571
  empty canvas name. The non-semantic `[part="base"]` wrapper is not named instead.
22151
22572
 
22152
22573
  **Authoring types:** `LyraMapLegendEntry`, `LyraMapLegendPattern`, `LyraMapLegendProjection`, `LyraMapChoroplethLayer`,
22153
- `LyraMapGeoJsonDataLayer`, `LyraMapMarker`, `LyraMapStyleSpecification`, and `LyraMapInstance`.
22574
+ `LyraMapGeoJsonDataLayer`, `LyraMapDataLayerKind`, `LyraMapClusterOptions`, `LyraMapHeatmapOptions`,
22575
+ `LyraMapMarker`, `LyraMapStyleSpecification`, and `LyraMapInstance`.
22154
22576
  The former `LegendEntry`, `ChoroplethLayer`, `GeoJsonDataLayer`, and `MapMarker` names are removed
22155
22577
  in v9 rather than retained as aliases.
22156
22578
 
@@ -22169,10 +22591,15 @@ an element.
22169
22591
  (frozen `detail: { readonly lngLat: readonly [lng, lat], readonly feature?, readonly origin?,
22170
22592
  readonly sourceId? }`; the tuple and any hit GeoJSON feature are detached and recursively frozen).
22171
22593
  `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
22594
+ fill/line/circle/cluster layer, topmost first — so a shape painted through `dataLayers` is
22595
+ identifiable instead of being indistinguishable from empty space. `origin` is `'choropleth'`,
22596
+ `'data-layer'` or `'cluster'`, and `sourceId` carries the authored `dataLayers[].sourceId` for a
22597
+ data-layer or cluster hit; both are `undefined` whenever `feature` is. A cluster hit is reported
22598
+ separately because it is a synthetic aggregate rather than one of your features: its useful payload
22599
+ is MapLibre's `point_count`/`point_count_abbreviated`/`cluster_id` properties, which is what a
22600
+ zoom-to-cluster handler reads. The count label is deliberately not hit-tested (it sits exactly on
22601
+ the circle already queried and would only make the label the topmost hit), and a `kind: 'heatmap'`
22602
+ layer is never queried at all — MapLibre returns no features for a rendered density surface
22176
22603
 
22177
22604
  **Slots:** `legend` — custom legend content, rendered inside the legend panel's own layout so it
22178
22605
  stays positioned with the map instead of floating beside it. Supplying it opens the panel even
@@ -24299,6 +24726,12 @@ These named interfaces and helper signatures are available to typed integrations
24299
24726
  reason: unknown;
24300
24727
  }`
24301
24728
 
24729
+ - **`components-media-flag-flag-peer-bulk-standard-contracts`** — Supporting data types and helpers for this component family.
24730
+ `registerLyraFlagStandardBulkPeer(): unknown`
24731
+
24732
+ - **`components-media-flag-flag-peer-bulk-contracts`** — Supporting data types and helpers for this component family.
24733
+ `registerLyraFlagBulkPeer(): unknown`
24734
+
24302
24735
  - **`components-media-flag-flag-peer-contracts`** — Supporting data types and helpers for this component family.
24303
24736
  `registerLyraFlagPeer(): unknown`
24304
24737
 
@@ -24426,6 +24859,13 @@ These named interfaces and helper signatures are available to typed integrations
24426
24859
  stops: unknown;
24427
24860
  interpolation: unknown;
24428
24861
  stepBaseColor: unknown;
24862
+ }`
24863
+ `LyraMapClusterOptions {
24864
+ radius: unknown;
24865
+ maxZoom: unknown;
24866
+ radiusSteps: unknown;
24867
+ colorSteps: unknown;
24868
+ countFont: unknown;
24429
24869
  }`
24430
24870
  `LyraMapGeoJsonDataLayer {
24431
24871
  sourceId: unknown;
@@ -24433,6 +24873,16 @@ These named interfaces and helper signatures are available to typed integrations
24433
24873
  tone: unknown;
24434
24874
  color: unknown;
24435
24875
  strokeColor: unknown;
24876
+ kind: unknown;
24877
+ heatmap: unknown;
24878
+ cluster: unknown;
24879
+ }`
24880
+ `LyraMapHeatmapOptions {
24881
+ weightField: unknown;
24882
+ weightRange: unknown;
24883
+ stops: unknown;
24884
+ radius: unknown;
24885
+ intensity: unknown;
24436
24886
  }`
24437
24887
  `LyraMapInstance {
24438
24888
  getCanvas: unknown;