@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
@@ -133,9 +133,21 @@ default). An inherited or direct public value remains authoritative in every tie
133
133
  Text field + calendar popover, **form-associated** via the shared `FormAssociated` mixin (`name`,
134
134
  `value`, `disabled`, `required` all inherited).
135
135
 
136
- **Properties (42):**
136
+ **Properties (44):**
137
137
 
138
138
  - `appearance: 'filled'|'outlined'|'filled-outlined' = 'outlined'` (reflected)
139
+ - `appliedPreset: LyraDateRangePreset | undefined` (read-only, new in 11.3.0) — the `presets` entry
140
+ whose button produced the current `value`, or `undefined` when the value was picked on the
141
+ calendar, typed into the field, cleared, or reset. Read it inside your own `change`/`input`
142
+ handler; it is updated before those events are relayed, so a handler observes the preset that
143
+ caused the very commit it is handling, and it is `undefined` while the popover has never been
144
+ opened. Mirrors the nested `lr-date-picker`'s identically-named property across this component's
145
+ shadow boundary, which is where the readback is actually needed: the compact
146
+ text-field-plus-popover shape is the one a dashboard filter uses, the nested picker instance is
147
+ unreachable from outside (a CSS part cannot yield it), and the fact is not recoverable from
148
+ `value` — re-deriving it by string-matching is the mapping table `presets` exists to delete and is
149
+ ambiguous anyway. A property rather than an event detail because `input`/`change` here are
150
+ **native** events that cannot carry one
139
151
  - `assumeInteractionOn: string[] = ['input']` (JS only)
140
152
  - `autocomplete: string = ''`
141
153
  - `dayContent?: LyraDatePickerDayContent` (JS only)
@@ -160,6 +172,12 @@ Text field + calendar popover, **form-associated** via the shared `FormAssociate
160
172
  - `pill: boolean = false` (reflected)
161
173
  - `placement: LyraDateInputPlacement = 'bottom-start'` (reflected; all 12 side/alignment
162
174
  placements are accepted)
175
+ - `presets: LyraDateRangePreset[] = []` (JS only, new in 11.1.0) — forwarded verbatim to the nested
176
+ `lr-date-picker`, whose own `presets` documents the semantics (range mode only, open bounds
177
+ resolving to `min`/`max`, unset renders nothing). Forwarded rather than reimplemented because the
178
+ picker lives in this component's shadow root, so a consumer has no route to it. The row's parts
179
+ are re-exported as `presets`/`preset-button`, and `appliedPreset` above reports which entry
180
+ produced the current value
163
181
  - `readonly: boolean = false` and `required: boolean = false` (reflected)
164
182
  - `size: LyraSize = 'm'` (reflected; `2xs`–`xl` and aliases)
165
183
  - `today: string = ''` (reflected ISO override)
@@ -52,7 +52,8 @@ value therefore stays inside a 320px LTR or RTL bar, with the chip's own label e
52
52
  overflow ownership rather than widening the page.
53
53
 
54
54
  Each edit exposes one filter-bar `lr-input` carrying a detached, deeply frozen snapshot of the
55
- complete value object. A built-in or
55
+ complete value object, plus the `filterId` that changed and (new in 11.3.0) `appliedPreset` — see
56
+ "Date-range quick ranges" below. A built-in or
56
57
  custom control's own `lr-input`/`lr-change` aliases stay inside the wrapper so their incompatible
57
58
  detail shapes cannot escape as duplicate bar events; native-style `input`/`change` events from the
58
59
  composed controls continue bubbling normally.
@@ -102,6 +103,54 @@ to `<lr-input>` for an open-ended free-text query rather than a closed choice se
102
103
  filter's value is the raw query string, verbatim, and its chip shows exactly that string — the same
103
104
  text the user typed, not a truncated or normalized form.
104
105
 
106
+ ### Date-range quick ranges
107
+
108
+ A `'date-range'` definition also accepts `presets?: readonly LyraDateRangePreset[]` (new in 11.3.0),
109
+ forwarded to its composed `<lr-date-input>` exactly like `min`/`max`, so the quick-range row
110
+ ("Today", "Last 7 days", "All time") renders inside that filter's own calendar popover. Entries are
111
+ `LyraDateRangePreset { label, start?, end? }` with ISO `YYYY-MM-DD` bounds; an omitted bound is open
112
+ and resolves to the filter's `min`/`max`, and an open bound with no corresponding limit renders that
113
+ button disabled. `presets` is deliberately **not** accepted on a single `'date'` filter: a preset
114
+ names two dates, so `lr-date-picker` ignores the list outside range mode, and a list passed there is
115
+ dropped rather than rendering a row that cannot do anything.
116
+
117
+ The `lr-input` emitted by such a commit carries `appliedPreset`, the definition entry whose button
118
+ produced it — the bar's own frozen snapshot, so it compares identical to `filters[i].presets[j]`. It
119
+ is `undefined` for every other filter type and for a range picked or typed by hand. A filter bar
120
+ whose values round-trip through a query string needs it because `value` holds only the frozen ISO
121
+ range: persisting "Last 7 days" as a preset id keeps it meaning the last 7 days after the next
122
+ reload, and re-deriving it by string-matching `value` is both the mapping table `presets` exists to
123
+ delete and ambiguous (Today and This month coincide on the 1st). It rides the event rather than
124
+ `value` because it is metadata about one edit, not a filter value — `value` stays the plain,
125
+ JSON-serializable record it has always been.
126
+
127
+ ```ts
128
+ const filters: LyraFilterBarFilterDefinition[] = [
129
+ {
130
+ filterId: "period",
131
+ label: "Reporting period",
132
+ type: "date-range",
133
+ min: "2020-01-01",
134
+ max: "2030-12-31",
135
+ presets: [
136
+ { label: "Last 7 days", start: "2026-08-13", end: "2026-08-19" },
137
+ { label: "This month", start: "2026-08-01", end: "2026-08-31" },
138
+ { label: "All time" },
139
+ ],
140
+ },
141
+ ];
142
+
143
+ bar.addEventListener("lr-input", (event) => {
144
+ const { value, filterId, appliedPreset } = event.detail;
145
+ persist({ ...value, periodPreset: appliedPreset?.label });
146
+ });
147
+ ```
148
+
149
+ Before this, the only way to give a filter-bar date range a quick-range row was `type: 'custom'`,
150
+ which means hand-rendering an `lr-date-input` plus a full adapter (`clearValue`, `isEmpty`,
151
+ `formatValue`) to set one property, and forfeits the built-in date-range chip localization described
152
+ next.
153
+
105
154
  Date chips localize exactly one round-trip-valid ISO `YYYY-MM-DD` segment; date-range chips require
106
155
  exactly two slash-separated segments. Four-digit
107
156
  years `0000`–`0099` retain those literal years rather than inheriting JavaScript's 1900 offset;
@@ -219,6 +219,20 @@ import "@aceshooting/lyra-ui/components/media/flag/flag-peer.js";
219
219
  unneeded 249-entry fetch. Only the standard tier is bulk-fetched this way;
220
220
  `fidelity="compact"/"detailed"` on individual elements still resolves through its own lazy
221
221
  per-code loader.
222
+ - Rendering many flags at once **and** leaving every `<lr-flag>` on the default
223
+ `fidelity="standard"`: import
224
+ `@aceshooting/lyra-ui/components/media/flag/flag-peer-bulk-standard.js` instead of
225
+ `flag-peer-bulk.js` (never more than one of the three peer entries — each
226
+ `setFlagUrlResolver()` call replaces the previous resolver). It registers
227
+ `@aceshooting/lyra-flags/standard`'s `createFlagUrlResolver()`, which is backed by the same
228
+ standard-tier-only eager map as the root's, without statically importing the detailed and
229
+ compact loader maps the root entry needs for its per-call `variant`. Those maps are what
230
+ `flag-peer-bulk.js` pays for its batching: on a real production build with a 156-country flag
231
+ column, routing bulk resolution through the package root emitted +65 detailed SVGs and +31
232
+ compact WebPs — +15.8MB of assets no route rendered — swamping the chunk-count win the bulk path
233
+ exists for. The tradeoff is the tier commitment: `fidelity="compact"/"detailed"` on an individual
234
+ element resolves to that code's standard asset instead (a silent no-op, not an error), so use
235
+ `flag-peer-bulk.js` when per-instance fidelity must actually be honoured.
222
236
  - 65 of `@aceshooting/lyra-flags`' 249 flags (any whose design includes a detailed coat of
223
237
  arms/seal/emblem, e.g. `es`, `pt`, `sv`) ship **three** fidelity tiers, selected via the
224
238
  `fidelity` property (`flagUrl(code, { variant: fidelity })` under the hood): `"compact"` — a tiny WebP raster for
@@ -0,0 +1,122 @@
1
+ <!-- GENERATED by scripts/build-llms.mjs from llms/data.md — do not edit this file. -->
2
+
3
+ # `lr-funnel`
4
+
5
+ - **Import** `import '@aceshooting/lyra-ui/components/lr-funnel.js';` (stable tag alias; registers the tag)
6
+ - **Class** `LyraFunnel`, also available unregistered from `@aceshooting/lyra-ui/components/data/funnel/funnel.class.js`
7
+ - **Family** `components/data/` — see `llms/index.md` for its siblings
8
+ - **Status** `experimental` since `12.0.0` — see the maturity and deprecation policy in `llms/shared.md`
9
+ - **Deprecations** none
10
+ - **Optional peers** none
11
+ - **Themeable via** 14 parts, 4 custom properties — see this component's own `@csspart`/`@cssprop` list below
12
+ - **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`
13
+
14
+ ---
15
+
16
+ ## `lr-funnel`
17
+
18
+ Dependency-free conversion funnel (no charting library): an ordered set of stages, each drawn as a
19
+ bar whose length is that stage's share of the **first** stage, read top-to-bottom as progressive
20
+ drop-off. It sits beside `lr-gauge` and `lr-heatmap` as an analytics primitive rather than a general
21
+ chart type.
22
+
23
+ It is deliberately not a sorted bar chart: it normalizes to the first stage rather than to the data
24
+ maximum, draws no value axis, and reads as stage-to-stage retention rather than category comparison.
25
+ Reach for `lr-bar-chart`/`lr-lite-chart` when you actually want a value axis and category comparison.
26
+
27
+ The whole chart is plain HTML — stage names, absolute values, shares and drop-off percentages are
28
+ real text inside an `<ol>`, so there is no sighted-only drawing needing a separate transcript. The
29
+ list carries the accessible name; a host `aria-label` overrides `label`.
30
+
31
+ **Properties:**
32
+
33
+ - `stages: readonly LyraFunnelStage[] = []` (property only; every share is measured against
34
+ `stages[0]`)
35
+ - `comparison: readonly LyraFunnelStage[] = []` (property only — an optional baseline/peer cohort
36
+ drawn behind each bar as a dashed outline, normalized to **its own** first stage so a cohort's
37
+ funnel *shape* stays comparable against a baseline whose absolute volumes are not)
38
+ - `comparisonLabel: string = ''` (attribute `comparison-label`; falls back to a localized generic
39
+ label)
40
+ - `label: string = ''` (accessible name for the stage list)
41
+ - `dropoff: boolean = true` (reflected; `dropoff="false"` in markup really does turn it off —
42
+ the property uses the explicit `false`-parsing converter, not attribute presence)
43
+ - `sharePrecision: number = 0` (attribute `share-precision`; fraction digits for every share and
44
+ drop-off percentage, clamped to `0`–`20`)
45
+
46
+ ```ts
47
+ interface LyraFunnelStage {
48
+ readonly label: string;
49
+ readonly value: number;
50
+ readonly color?: string; // CSS color; unparseable values fall back to --lr-funnel-bar-color
51
+ }
52
+ ```
53
+
54
+ **Events:** none.
55
+
56
+ **Slots:** none.
57
+
58
+ **CSS parts:** `base`, `stages` (the `<ol>`), `stage`, `dropoff`, `stage-header`, `stage-label`,
59
+ `stage-value`, `stage-share`, `comparison-value`, `track`, `bar`, `bar-overflow` (a second token on
60
+ `bar` when the stage exceeds the first stage), `comparison-bar`, `empty`
61
+
62
+ **Themeable custom properties:** `--lr-funnel-bar-color` (default `var(--lr-color-brand)`),
63
+ `--lr-funnel-comparison-color` (default `var(--lr-color-border-strong)`), `--lr-funnel-track-color`
64
+ (default `var(--lr-color-surface-raised)`), `--lr-funnel-bar-size` (track thickness, default
65
+ `var(--lr-size-1-5rem)`).
66
+
67
+ **Optional peer deps:** none.
68
+
69
+ ```html
70
+ <script type="module">
71
+ import '@aceshooting/lyra-ui/components/data/funnel/funnel.js';
72
+ </script>
73
+
74
+ <lr-funnel id="signup" label="Self-serve signup" share-precision="1"></lr-funnel>
75
+
76
+ <script type="module">
77
+ const funnel = document.querySelector('#signup');
78
+ funnel.stages = [
79
+ { label: 'Visited pricing', value: 12480 },
80
+ { label: 'Started trial', value: 4310 },
81
+ { label: 'Converted to paid', value: 512 },
82
+ ];
83
+ // Normalized to ITS OWN first stage, so a 380-visitor account still compares against a
84
+ // 12,480-visitor peer group.
85
+ funnel.comparison = [
86
+ { label: 'Visited pricing', value: 380 },
87
+ { label: 'Started trial', value: 141 },
88
+ { label: 'Converted to paid', value: 12 },
89
+ ];
90
+ funnel.comparisonLabel = 'Acme Corp';
91
+ </script>
92
+ ```
93
+
94
+ **Known gotchas:**
95
+
96
+ - **Shares are of the first stage, not of the previous one.** `stage-share` answers "how much of the
97
+ top of the funnel is left here?"; the separate `dropoff` row answers "what changed since the
98
+ previous stage?". Both are rendered because the percentage is usually the interesting number and
99
+ the absolute count is the credibility check.
100
+ - **A zero or negative first stage cannot define a share.** Every `stage-share` is omitted, every bar
101
+ is zero-length, and only the absolute values render. Nothing is silently divided by zero and no
102
+ `NaN` reaches the geometry.
103
+ - **A stage larger than its predecessor is legal** (funnel re-entry). Its share is reported
104
+ truthfully above 100% in text, while the bar clamps to the track and gains the `bar-overflow` part
105
+ token so you can style it. Drop-off for that stage reads as an increase.
106
+ - **Drop-off is omitted, not zeroed, when the previous stage is non-positive** — a change relative to
107
+ zero is undefined.
108
+ - **A comparison series of a different length pairs by index.** Extra comparison entries are ignored;
109
+ stages past its end simply get no comparison bar. A comparison series whose own first value is
110
+ zero or negative draws nothing.
111
+ - **Non-finite `value`s are treated as `0`** rather than blanking the stage, so one bad row cannot
112
+ take the chart with it.
113
+ - Values, shares and drop-off percentages all format through the component's effective locale
114
+ (`locale` attribute, or an inherited one) — never a hardcoded `en`.
115
+ - Bars grow from the inline-start edge and every dimension is a logical property, so `dir="rtl"`
116
+ needs no extra work. The component never sets its own `dir`.
117
+ - Layout responds to the **container** (`container-type: inline-size` on `base`), not the viewport:
118
+ below roughly `18rem` of allocated width the stage name moves to its own line so the value and
119
+ share stay together. Bar motion is a token-driven transition that stops under
120
+ `prefers-reduced-motion`.
121
+ - No events, no interaction model, no zoom/pan. If you need a clickable funnel, wrap the element and
122
+ handle clicks yourself.
@@ -8,7 +8,7 @@
8
8
  - **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
9
9
  - **Deprecations** none
10
10
  - **Optional peers** none
11
- - **Themeable via** 15 parts, 10 custom properties — see this component's own `@csspart`/`@cssprop` list below
11
+ - **Themeable via** 18 parts, 12 custom properties — see this component's own `@csspart`/`@cssprop` list below
12
12
  - **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`
13
13
 
14
14
  ---
@@ -98,6 +98,38 @@ weekdayLabelText?: (jsWeekday:number)=>string|undefined; monthLabelText?:
98
98
  `colLabelHeight="auto"` to have the band size itself to the rotated extent. **Not mirrored under
99
99
  `dir="rtl"`** — both grid modes deliberately retain physical LTR geometry, so leaning one axis'
100
100
  labels the other way would be incoherent
101
+ - `stickyLabels: 'none' | 'rows' | 'cols' | 'both' = 'none'` (attribute `sticky-labels`, reflected)
102
+ — freezes a **matrix** label band against the grid's own scrolling. `'rows'` pins the row-label
103
+ gutter so it survives horizontal scrolling, `'cols'` pins the column-label band so it survives
104
+ vertical scrolling, `'both'` pins both. Labels and cells otherwise share one bitmap, so neither
105
+ band can be `position: sticky` on its own and a tall matrix scrolls its column header out of
106
+ view; the only workaround was a light-DOM mirror row that had to hardcode the gutter width and
107
+ cell size, which made it mutually exclusive with `row-label-width="auto"`. A frozen band is
108
+ repainted into its own layer in the same draw pass, from the same resolved `matrixGeometry` the
109
+ cells were painted with, so it tracks a `row-label-width`/`col-label-height` `"auto"`
110
+ re-resolution, a resize, and a DPR change without drifting a pixel. Freezing needs something to
111
+ scroll, so the frozen modes wrap the grid in a `[part="grid"]` scrollport: it is bounded inline by
112
+ the host's own allocation — a matrix wider than a 320px host scrolls inside the component instead
113
+ of overflowing it — and unbounded in block until you set `--lr-heatmap-grid-max-block-size`, which
114
+ a frozen column band needs in order to have vertical scrolling to stay behind. The bands are
115
+ `aria-hidden` duplicates of pixels the canvas already painted, so the accessible representation is
116
+ unchanged, and the `accessibleCells` overlay moves inside the scrollport so the cell buttons
117
+ scroll with the canvas they cover. The grid keeps this component's physical LTR geometry under
118
+ `dir="rtl"`, so the frozen gutter stays on the same physical side as the labels the canvas paints.
119
+ Matrix mode only, like `matrixGeometry`: the property is read in calendar mode but has no effect
120
+ there, since a calendar's axes are a different geometry (fixed weekday gutter, month band, and the
121
+ optional `columnX`/`rowY` overrides). Unset (`'none'`, the default) renders exactly what it always
122
+ did — one canvas, no scrollport, no extra elements — and an unsupported value normalizes to it.
123
+ The union is re-exported from the package root as `LyraHeatmapStickyLabels`, so a typed consumer
124
+ can annotate a variable or a framework prop with it. Everything positioned in canvas coordinates
125
+ moves into the scrollport with the cells: `[part="tooltip"]` renders inside it, so it stays on
126
+ the cell it describes through a scroll instead of drifting by the scroll offset, and because
127
+ `overflow: auto` clips whatever leaves the scrollport, the tooltip is kept inside the visible
128
+ window too — clamped along the inline axis and flipped to below its cell when a frozen band
129
+ leaves no room above it. Arrow-key navigation scrolls the focused cell into that window, clear of
130
+ the frozen bands, which the frozen modes need in their own right: the canvas is the roving tab
131
+ stop, its focus ring is painted into the bitmap rather than carried by a focusable element the
132
+ browser would scroll to, and it calls `preventDefault()` on the arrows
101
133
  - `maxCellSize?: number` (attribute `max-cell-size`) — ceiling, in CSS px, on the cell size
102
134
  `fitToWidth` derives from the host width, in **both** modes. Exists because `fitToWidth` divides
103
135
  the _whole_ host width across the grid, so a 5-week calendar or a 3-column matrix in a wide pane
@@ -227,7 +259,10 @@ color?: string; label?: string; partOfRamp?: boolean }`: a discrete legend key r
227
259
  are never consulted by the color ramp, the bucket math, the tooltip, or the generated accessible
228
260
  name, so adding them changes nothing a cell renders. Labeled `annotations` still render their
229
261
  `[part="legend-annotation"]` entries after the stops. Unset (the default) or an empty array
230
- reproduces the exact gradient legend, unchanged. A dev-mode-only warning fires when a stop's
262
+ reproduces the exact gradient legend, unchanged. Reassigning stops whose `value`/`color`/`label`/
263
+ `partOfRamp` fields are unchanged does not schedule a canvas redraw. The assignment still takes
264
+ a fresh frozen ownership snapshot, so mutating and reassigning the same caller array is detected
265
+ while mutation without reassignment remains isolated. A dev-mode-only warning fires when a stop's
231
266
  `color` doesn't match the corresponding `colorSteps` entry — set `partOfRamp: false` on a stop
232
267
  that is a real, distinctly-colored swatch deliberately outside the sequential ramp (e.g. a fixed
233
268
  "no data" color next to an N-step ramp) to exclude just that stop from the comparison; defaults to
@@ -237,9 +272,12 @@ color?: string; label?: string; partOfRamp?: boolean }`: a discrete legend key r
237
272
  color-scheme change; called automatically on theme changes, exposed for a consumer that needs to
238
273
  force a redraw manually. `matrixGeometry: Readonly<{ padLeft: number; padTop: number; cellSize:
239
274
  number }> | undefined` — the gutter/cell geometry the last matrix-mode draw actually painted with,
240
- in CSS pixels; `undefined` in calendar mode. Lets a light-DOM consumer (e.g. a sticky header mirror
241
- for a tall matrix) line up with the canvas without hardcoding the same numbers `row-label-width`/
242
- `col-label-height`'s `"auto"` resolution would otherwise keep private.
275
+ in CSS pixels; `undefined` in calendar mode. Lets a light-DOM consumer line up with the canvas
276
+ without hardcoding the same numbers `row-label-width`/`col-label-height`'s `"auto"` resolution would
277
+ otherwise keep private. For the case that motivated it — a frozen header or gutter on a tall or wide
278
+ matrix — prefer `stickyLabels`, which freezes the band inside the component and needs no mirror at
279
+ all; the getter remains the way to align a *separate* element (a sibling chart, a custom overlay)
280
+ with the grid.
243
281
 
244
282
  **Events:** `lr-cell-click` (fired on click, or Enter/Space on the keyboard-focused cell —
245
283
  `detail: { row, col, value }` in matrix mode, `detail: { date, value }` in calendar mode),
@@ -250,8 +288,12 @@ in calendar mode)
250
288
 
251
289
  **Slots:** none.
252
290
 
253
- **CSS parts:** `base`, `canvas`, `cells` (opt-in per-cell overlay), `cell` (one opt-in native cell
254
- button), `tooltip` (hover tooltip, positioned over the hovered cell),
291
+ **CSS parts:** `base`, `canvas`, `grid` (the scrollport wrapping the canvas while `stickyLabels`
292
+ freezes an axis — absent entirely otherwise), `row-labels`/`col-labels` (the frozen label bands,
293
+ rendered only for the axis `stickyLabels` names), `cells` (opt-in per-cell overlay), `cell` (one opt-in native cell
294
+ button), `tooltip` (hover tooltip, positioned over the hovered cell — inside `[part="grid"]` while
295
+ `stickyLabels` freezes an axis, so it scrolls with the cells, and a `[part="base"]` child
296
+ otherwise),
255
297
  `live-region` (visually-hidden, `aria-hidden` mirror of the keyboard-focused cell; the actual
256
298
  announcement uses the shared light-DOM polite sink), `projection-limit` (localized assistive
257
299
  disclosure when bounded canonicalization truncates input), `legend`, `legend-lo`, `legend-hi` (both omitted, along with the gradient
@@ -287,7 +329,12 @@ dedicated token distinct from both the focus ring and the annotation ring so a h
287
329
  independently). `--lr-heatmap-tooltip-bg` (default
288
330
  `var(--lr-color-surface)`) and `--lr-heatmap-tooltip-text` (default `var(--lr-color-text)`) —
289
331
  unlike the canvas-drawn tokens above, `[part="tooltip"]` is a real DOM element and consumes these
290
- directly, no `getComputedStyle` bridging needed. Also consumes `--lr-color-text-quiet` (axis label
332
+ directly, no `getComputedStyle` bridging needed. `--lr-heatmap-sticky-label-bg` (default
333
+ `var(--lr-color-surface)` — the backdrop painted under a frozen `stickyLabels` band, resolved via
334
+ `getComputedStyle` like the other canvas-drawn tokens; it must stay **opaque**, since it covers the
335
+ same labels the scrolling canvas painted underneath it) and `--lr-heatmap-grid-max-block-size`
336
+ (default `none` — the block-size ceiling of the `[part="grid"]` scrollport, consumed directly by
337
+ 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
291
338
  color), `--lr-space-xs`, `--lr-radius`/`--lr-shadow` (tooltip box), and `--lr-focus-ring-width`/
292
339
  `--lr-focus-ring-offset` (the real `[part="canvas"]:focus-visible` DOM outline, stroked in the
293
340
  same color as `--lr-heatmap-focus-ring-color`).
@@ -87,13 +87,16 @@ passthrough). Not a subclass of `LyraChart`.
87
87
  - `barWidth: number = 32` (attribute `bar-width`, px) — each bar's fixed width in `layout="scroll"`
88
88
  mode; ignored in the default `'fit'` mode. An excessive value is reduced as needed by the
89
89
  1,000,000px scroll-content ceiling.
90
- - `maxLabels?: number` (attribute `max-labels`, type Number) — decimates which category axis labels
91
- actually render *text* when `labels.length > maxLabels`: always shows the first and last label,
92
- and roughly evenly distributes
93
- the rest between them. Works in either `layout` mode. Unset (the default) renders every label.
94
- Each rendered category label is allocation-aware: narrow/long text is ellipsized before paint,
95
- with the complete caller label retained as its accessible name. Independently, the global
96
- 1,000-record safety sampler may bound both marks and labels for very large category×series input.
90
+ - `maxLabels?: number | 'auto'` (attribute `max-labels`) — decimates which category axis labels
91
+ actually render *text*: always shows the first and last label and roughly evenly distributes the
92
+ rest between them. A number is authoritative. `'auto'` derives the cap after each resize from the
93
+ resolved plot width and widest rendered caller label, using the same deterministic 7px-per-
94
+ character estimate as label ellipsis plus 10px of lane breathing room. It therefore responds to
95
+ either `layout` mode without DOM text measurement or browser-specific font metrics. Unset (the
96
+ default) renders every label, unchanged. Each rendered category label is allocation-aware:
97
+ narrow/long text is ellipsized before paint, with the complete caller label retained as its
98
+ accessible name. Independently, the global 1,000-record safety sampler may bound both marks and
99
+ labels for very large category×series input.
97
100
  - `barX?: (index: number) => number` (attribute: false, bar type only) — overrides the internal
98
101
  per-category x-origin formula (`plotX + i * slot`) used by both bars and their axis labels, so a
99
102
  consumer can pixel-align this chart's bars with a sibling `<lr-heatmap>` calendar's week columns
@@ -115,7 +118,16 @@ passthrough). Not a subclass of `LyraChart`.
115
118
  - `skipZero: boolean = false` (attribute `skip-zero`, bar type only) — omits a bar entirely (no
116
119
  mark/tabindex/tooltip) for a value that is exactly `0`; `null`/non-finite values are always
117
120
  skipped regardless.
118
- - `valueAxisGutter?: number` (attribute `value-axis-gutter`) — value-axis gutter width.
121
+ - `valueAxisGutter?: number | 'auto'` (attribute `value-axis-gutter`) — value-axis gutter width in
122
+ CSS px. A finite number is authoritative (clamped to 0…1,000,000px as before). `'auto'` sizes the
123
+ gutter from the exact value-tick strings rendered in that pass — `formatter` output first, then
124
+ `tickFormat`, then the component's `effectiveLocale` number formatting — using the deterministic
125
+ 7px-per-character estimate plus 14px for the tick offset and font-width variance. Automatic
126
+ sizing never shrinks below the legacy 36px default. In `layout="fit"` it is bounded to the smaller
127
+ of 240px or 40% of the measured SVG width, so a pathological formatter result cannot consume the
128
+ plot. In `layout="scroll"` the cap is 240px: that SVG has an explicit content width, so deriving a
129
+ percentage cap from its own ResizeObserver result would create a shrinking feedback loop. The
130
+ gutter remains at logical start under RTL. Unset keeps exactly 36px.
119
131
  - `barGapRatio?: number` (attribute `bar-gap-ratio`) — overrides the internal 0.2 `BAR_GROUP_GAP`
120
132
  fraction of a category slot left as a gap between categories. Unset keeps the fixed 0.2.
121
133
  - `scale: 'linear' | 'sqrt' | 'logarithmic' = 'linear'` — `'sqrt'` (**bar type only**) maps a bar's
@@ -16,13 +16,19 @@
16
16
  ## `lr-map`
17
17
 
18
18
  A `maplibre-gl` wrapper with a declarative legend, a single choropleth GeoJSON fill layer, markers,
19
- and additive plain-GeoJSON `dataLayers`, plus a peer-neutral `map` getter for common imperative
20
- operations. Its runtime value is the underlying MapLibre map.
19
+ and additive `dataLayers` — plain GeoJSON, natively clustered points, or a heatmap density surface
20
+ plus a peer-neutral `map` getter for common imperative operations. Its runtime value is the
21
+ underlying MapLibre map. The component observes its own map-container allocation and calls the
22
+ peer's `resize()` when that allocation changes.
21
23
 
22
24
  **Properties:**
23
25
 
24
26
  - `center: [number, number] = [0, 0]`
25
27
  - `zoom: number = 2`
28
+ - `renderWorldCopies?: boolean` (attribute: false) — forwarded to MapLibre when its map is
29
+ constructed. Leave it unset to preserve MapLibre's own current default; set `false` before
30
+ construction to stop repeating the world horizontally. This is a construction-time option, so a
31
+ later change takes effect after the component is disconnected and reconnected.
26
32
  - `mapStyle?: LyraMapStyleSpecification | string` (attribute: false) — required before a map is
27
33
  constructed. Object assignments are detached and recursively frozen; create and reassign a new
28
34
  style to update it. `LyraMapStyleSpecification` is the peer-neutral structural subset accepted from
@@ -97,10 +103,15 @@ string; geojson: GeoJSON.FeatureCollection; field: string; stops: [number, strin
97
103
  gotchas. Entries with non-finite coordinates or latitude outside `[-90, 90]` are skipped without
98
104
  aborting valid siblings. `color` is used only when the browser accepts it as CSS `color`;
99
105
  declaration breaks and `url()` paint servers fall back to MapLibre's default marker color.
106
+ Every retained marker is a named `role="button"` tab stop, including one without a popup. Click,
107
+ Enter, and Space emit `lr-map-marker-activate`; Space suppresses its page-scroll default while
108
+ preserving MapLibre's popup toggle. A popup-bearing marker additionally exposes
109
+ `aria-haspopup="dialog"`, `aria-controls`, and explicit `aria-expanded`.
100
110
  - `dataLayers: LyraMapGeoJsonDataLayer[] = []` (attribute: false) —
101
111
  `LyraMapGeoJsonDataLayer { sourceId: string; geojson: GeoJSON.Feature |
102
112
  GeoJSON.FeatureCollection; tone?: 'accent' | 'success' | 'warning' |
103
- 'danger' | 'neutral'; color?: string; strokeColor?: string }`. `sourceId` is trimmed and must be nonempty; the first layer for a
113
+ 'danger' | 'neutral'; color?: string; strokeColor?: string; kind?: LyraMapDataLayerKind;
114
+ heatmap?: LyraMapHeatmapOptions; cluster?: LyraMapClusterOptions }`. `sourceId` is trimmed and must be nonempty; the first layer for a
104
115
  `sourceId` wins and blank or later duplicate records are ignored. Each retained entry adds one
105
116
  GeoJSON source plus three geometry-filtered layers
106
117
  (fill, line, and circle, so a mixed `FeatureCollection` renders correctly), colored from the
@@ -120,9 +131,65 @@ GeoJSON.FeatureCollection; tone?: 'accent' | 'success' | 'warning' |
120
131
  from overwriting or removing a same-named source supplied by `mapStyle`. Independent of
121
132
  `choropleth` — no `field`/`stops` color-interpolation, just the geometry rendered in a flat tone;
122
133
  use `choropleth` instead when you need a data-driven color ramp. An entry whose `sourceId`
123
- persists across a `dataLayers` reassignment gets its GeoJSON updated in place (`setData()`), one
124
- that's dropped has its private source/layers removed, and a genuinely new `sourceId` gets new
125
- resources nothing leaks on removal, style change, or disconnect.
134
+ persists across a `dataLayers` reassignment gets its GeoJSON updated in place (`updateData()`
135
+ when stable feature IDs make a safe diff possible, otherwise `setData()`), one that's dropped has
136
+ its private source/layers removed, and a genuinely new `sourceId` gets new resources — nothing
137
+ leaks on removal, style change, or disconnect.
138
+
139
+ `cluster` and `kind` (both new in 11.3.0) opt one entry out of that three-layer geometry split.
140
+ **Both are strictly additive: an entry that sets neither renders exactly what it rendered before,
141
+ down to the layer ids and the point layer's filter.**
142
+
143
+ `cluster?: LyraMapClusterOptions { radius?: number; maxZoom?: number; radiusSteps?: [number,
144
+ number][]; colorSteps?: [number, string][]; countFont?: string[] }` turns the entry's source into a
145
+ natively clustered one — `cluster`/`clusterRadius` (default 50)/`clusterMaxZoom` (default 14) on
146
+ the source, plus a `${sourceId}-cluster` circle filtered on `has('point_count')`, a
147
+ `${sourceId}-cluster-count` label, and a `${sourceId}-circle` layer for the points that stayed
148
+ unclustered. `cluster: {}` opts in at every default. This is what a thousands-of-points map needs
149
+ and what `markers` cannot be: `markers` mints one real, individually focusable DOM element per
150
+ entry, which is right for tens of pins and both unreadable and expensive for thousands.
151
+ `radiusSteps` and `colorSteps` are `['step', …]` breaks keyed on `point_count`, in the same
152
+ ascending `[value, output]` vocabulary as `choropleth.stops` — including the same base rule, where
153
+ the first entry's own output covers everything below the first threshold, and the same colour
154
+ resolution, where a `var(--lr-…)` reference in a `colorSteps` entry is resolved against the host
155
+ before it reaches MapLibre (which paints to a WebGL canvas and never sees the CSS cascade), so a
156
+ retheme moves the cluster breaks with everything else. No fill or line layer is created for a
157
+ clustered entry, because MapLibre's clustering keeps point features only. The count
158
+ label needs glyphs: a style that declares none gets the graduated circles without the numbers
159
+ (adding a text layer against a glyph-less style paints nothing and only emits peer errors), and
160
+ `countFont` names the font stack when your style's glyph source lacks MapLibre's spec default.
161
+ Cluster options are baked into the source at creation time by MapLibre and have no setter, so
162
+ changing them (or `kind`) rebuilds that one entry's source and layers; every other update still
163
+ reconciles in place.
164
+
165
+ `kind?: 'auto' | 'heatmap'` (`LyraMapDataLayerKind`, default `'auto'` — today's geometry split)
166
+ renders the source as MapLibre's own first-class `heatmap` layer instead, which the geometry split
167
+ cannot express at all: thousands of overlapping circles read as one opaque blob rather than as
168
+ where the data is concentrated. `heatmap?: LyraMapHeatmapOptions { weightField?: string;
169
+ weightRange?: [number, number]; stops?: [number, string][]; radius?: LyraMapHeatmapZoomValue;
170
+ intensity?: LyraMapHeatmapZoomValue; opacity?: number }`, where `LyraMapHeatmapZoomValue` is a
171
+ scalar number or bounded `[zoom, value][]` stops,
172
+ configures it. `weightField` weights each point by a feature property, and `weightRange` maps that
173
+ property's own units onto the 0–1 weight MapLibre expects — without it the raw value is passed
174
+ through, which saturates the surface for any quantity above ~1; with neither, every point weighs 1.
175
+ `stops` are `[density, color]` pairs with density in `[0, 1]`, **the same `[value, color]`
176
+ vocabulary `choropleth.stops` and `legendGradient` already share**, so a `legendGradient` bar can
177
+ describe the ramp without a second copy of it, and `var(--lr-…)` stops resolve against the host the
178
+ same way `color`/`strokeColor` do. A ramp that doesn't start at density 0 gets a fully transparent
179
+ stop prepended, because a coloured zero tints the entire map — so **a single stop is already a
180
+ complete ramp**, as long as it sits above density 0: `stops: [[1, '#ff0000']]` is exactly
181
+ transparent → red. The one authored ramp that can't be honoured is a lone stop AT density 0, which
182
+ describes a flat colour rather than a gradient; that one — like an unset or wholly unusable
183
+ `stops` — falls back to the built-in ramp, which runs transparent → `--lr-color-brand` →
184
+ `--lr-color-success` → `--lr-color-warning` → `--lr-color-danger`, so a retheme moves the density
185
+ surface with everything else. Scalar `radius` (default 30) and `intensity` (default 1) preserve
186
+ their established behavior. Two or more usable `[zoom, value]` stops emit linear zoom
187
+ interpolation; stops are sorted, duplicate zooms removed, zoom clamped into `[0, 24]`, radius
188
+ into `[1, 200]`, and intensity into `[0, 100]`. One usable stop becomes a scalar and an unusable
189
+ array falls back to the existing default. `opacity` is clamped into `[0, 1]`; omission leaves the
190
+ peer's default untouched on construction, and dropping a previously-authored value restores 1.
191
+ `cluster` is ignored on a heatmap entry: a heatmap already aggregates density, and clustering its
192
+ input would feed it one point per cluster instead of the real distribution.
126
193
  - `maxBounds: LyraMapBounds | null = null` (attribute: false) — box the map may not pan outside,
127
194
  `[[west, south], [east, north]]`. Prefer it over calling `map.setMaxBounds()` through the `.map`
128
195
  escape hatch: constraining the camera can wedge maplibre-gl at a sub-1 fractional zoom in a wide
@@ -134,14 +201,15 @@ GeoJSON.FeatureCollection; tone?: 'accent' | 'success' | 'warning' |
134
201
 
135
202
  **Choropleth and `dataLayers` updates are diffed before they reach the peer.** `setData()`
136
203
  unconditionally re-tiles and repaints an entire source, which is invisible on a static map and
137
- expensive on an animated one. When an update changes only feature *properties* the values driving
138
- the colour ramp the component emits maplibre-gl's incremental `updateData()` instead. The fast
139
- path is deliberately strict, and needs all of: the same feature count, a `string`/`number` `id` on
140
- every feature in the same order, and geometry that is the **same object** as last time (not merely
141
- deep-equal). Structurally comparing polygon rings would cost about what the re-tile costs, and a
142
- false positive would paint stale geometry. Reuse your geometry objects across frames — the
143
- efficient way to build an animation anyway and the fast path applies; otherwise it falls back to
144
- `setData()` with no change in behaviour. Peers predating `updateData()` always take the old path.
204
+ expensive on an animated one. When every feature has a unique `string`/`number` `id` and retained
205
+ features keep semantically unchanged geometry/bbox values, the component emits MapLibre's
206
+ incremental `updateData()` for property changes, additions, removals, and order changes. The exact
207
+ next order is preserved: an unchanged prefix stays in place and only the invalidated suffix is
208
+ removed and re-added. Lyra's immutable ownership boundary detaches GeoJSON assignments, so geometry
209
+ reference identity cannot survive between frames; a bounded, accessor-free comparison verifies the
210
+ JSON geometry graph instead. A missing/duplicate ID, changed geometry, exceeded bound, or uncertain
211
+ comparison falls back to `setData()` with no change in rendered behaviour. Peers without
212
+ `updateData()` always take the old path.
145
213
 
146
214
  **Feature properties are tiled, and therefore bounded in numeric magnitude.** MapLibre GL tiles
147
215
  every GeoJSON source through a worker into a protobuf vector tile, so a property carrying a huge
@@ -160,7 +228,9 @@ payload beside the map.
160
228
  empty canvas name. The non-semantic `[part="base"]` wrapper is not named instead.
161
229
 
162
230
  **Authoring types:** `LyraMapLegendEntry`, `LyraMapLegendPattern`, `LyraMapLegendProjection`, `LyraMapChoroplethLayer`,
163
- `LyraMapGeoJsonDataLayer`, `LyraMapMarker`, `LyraMapStyleSpecification`, and `LyraMapInstance`.
231
+ `LyraMapGeoJsonDataLayer`, `LyraMapDataLayerKind`, `LyraMapClusterOptions`, `LyraMapHeatmapOptions`,
232
+ `LyraMapHeatmapZoomValue`, `LyraMapMarker`, `LyraMapMarkerActivationDetail`,
233
+ `LyraMapMarkerActivationSource`, `LyraMapStyleSpecification`, and `LyraMapInstance`.
164
234
  The former `LegendEntry`, `ChoroplethLayer`, `GeoJsonDataLayer`, and `MapMarker` names are removed
165
235
  in v9 rather than retained as aliases.
166
236
 
@@ -175,14 +245,22 @@ shared `maplibre-gl` import without constructing a map or allocating a WebGL con
175
245
  `false` when the peer is unavailable, allowing an application to choose a fallback before connecting
176
246
  an element.
177
247
 
178
- **Events:** `lr-map-load` (fired once, after the underlying map's own `'load'`), `lr-map-click`
248
+ **Events:** `lr-map-load` (fired once, after the underlying map's own `'load'`),
249
+ `lr-map-marker-activate` (non-cancelable; frozen `LyraMapMarkerActivationDetail { id, lngLat,
250
+ marker, source }`; `id` is the trimmed explicit identity or `undefined`, `marker` is the accepted
251
+ declarative snapshot, and `source` is `'pointer' | 'keyboard'`), and `lr-map-click`
179
252
  (frozen `detail: { readonly lngLat: readonly [lng, lat], readonly feature?, readonly origin?,
180
253
  readonly sourceId? }`; the tuple and any hit GeoJSON feature are detached and recursively frozen).
181
254
  `feature` resolves against the choropleth fill layer **and** every applied `dataLayers`
182
- fill/line/circle layer, topmost first — so a shape painted through `dataLayers` is identifiable
183
- instead of being indistinguishable from empty space. `origin` is `'choropleth'` or `'data-layer'`,
184
- and `sourceId` carries the authored `dataLayers[].sourceId` for a data-layer hit; both are
185
- `undefined` whenever `feature` is
255
+ fill/line/circle/cluster layer, topmost first — so a shape painted through `dataLayers` is
256
+ identifiable instead of being indistinguishable from empty space. `origin` is `'choropleth'`,
257
+ `'data-layer'` or `'cluster'`, and `sourceId` carries the authored `dataLayers[].sourceId` for a
258
+ data-layer or cluster hit; both are `undefined` whenever `feature` is. A cluster hit is reported
259
+ separately because it is a synthetic aggregate rather than one of your features: its useful payload
260
+ is MapLibre's `point_count`/`point_count_abbreviated`/`cluster_id` properties, which is what a
261
+ zoom-to-cluster handler reads. The count label is deliberately not hit-tested (it sits exactly on
262
+ the circle already queried and would only make the label the topmost hit), and a `kind: 'heatmap'`
263
+ layer is never queried at all — MapLibre returns no features for a rendered density surface
186
264
 
187
265
  **Slots:** `legend` — custom legend content, rendered inside the legend panel's own layout so it
188
266
  stays positioned with the map instead of floating beside it. Supplying it opens the panel even
@@ -248,6 +326,10 @@ Vite with v6:
248
326
  { color: "#0969da", label: "High", pattern: "diagonal" },
249
327
  ];
250
328
  m.markers = [{ lngLat: [2.29, 48.86], label: "Eiffel Tower" }];
329
+ m.renderWorldCopies = false;
330
+ m.addEventListener("lr-map-marker-activate", (e) =>
331
+ console.log(e.detail.id, e.detail.lngLat)
332
+ );
251
333
  m.addEventListener("lr-map-click", (e) =>
252
334
  console.log(e.detail.feature?.properties)
253
335
  );
@@ -264,8 +346,9 @@ https://maplibre.org/maplibre-gl-js/docs/#esm.
264
346
  created peer instance, renders only the localized initialization failure, and can retry after a
265
347
  new style or reconnect without an unhandled promise rejection. Capability probes and error
266
348
  constructors come from the current owner document, including after same-origin adoption.
267
- - A marker with a popup handles Space at its focus boundary: one activation toggles the peer popup
268
- and suppresses the page-scroll default. Markers without a popup do not consume Space.
349
+ - Every marker handles Enter and Space as button activation. Space suppresses the page-scroll
350
+ default whether or not a popup exists; when one does, MapLibre still receives its own popup
351
+ toggle and the component emits exactly one `lr-map-marker-activate` notification.
269
352
  - clearing or swapping the choropleth no longer leaks the old layer: setting `choropleth =
270
353
  undefined`, or changing `choropleth.sourceId` to a different value, now calls `removeLayer`/
271
354
  `removeSource` on whatever was previously applied before adding the new one (or nothing, if
@@ -310,5 +393,9 @@ undefined`, or changing `choropleth.sourceId` to a different value, now calls `r
310
393
  `lr-map-load` never fires until the element is actually scrolled into view. Deliberate: caps
311
394
  concurrent WebGL contexts when many `<lr-map>`s sit in one dashboard/grid. Skipped entirely
312
395
  (constructs immediately once the peer loads) when `IntersectionObserver` itself is unavailable.
396
+ - Once constructed, the owner realm's `ResizeObserver` watches the rendered map container and calls
397
+ the current peer's `resize()` on allocation changes. It is replaced on reconnect/adoption and
398
+ disconnected before peer teardown, so a stale delivery cannot resize a removed map. Browsers
399
+ without `ResizeObserver` retain MapLibre's own window-resize behavior.
313
400
 
314
401
  ---