@aceshooting/lyra-ui 18.0.0 → 18.2.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 (42) hide show
  1. package/CHANGELOG.md +274 -0
  2. package/custom-elements.json +1 -1
  3. package/design-tokens.json +1 -1
  4. package/dist/cli/migration-contract.json +1 -1
  5. package/dist/components/conversation/message-actions/message-actions.class.d.ts +2 -1
  6. package/dist/components/forms/combobox/combobox.styles.js +1 -1
  7. package/dist/components/forms/icon-button/icon-button.class.d.ts +18 -9
  8. package/dist/components/forms/select/select.class.d.ts +34 -5
  9. package/dist/components/forms/select/select.class.js +4 -4
  10. package/dist/components/forms/select/select.styles.js +1 -1
  11. package/dist/components/layout/filter-bar/filter-bar.class.d.ts +80 -8
  12. package/dist/components/layout/filter-bar/filter-bar.class.js +11 -3
  13. package/dist/components/layout/filter-bar/filter-bar.styles.js +1 -1
  14. package/dist/components/layout/virtual-list/virtual-list.class.d.ts +47 -16
  15. package/dist/components/layout/virtual-list/virtual-list.class.js +1 -1
  16. package/dist/components/media/map/map.class.d.ts +161 -5
  17. package/dist/components/media/map/map.class.js +50 -21
  18. package/dist/components/media/map/map.styles.js +1 -1
  19. package/dist/components/utility/copy-button/copy-button.class.d.ts +4 -3
  20. package/dist/custom-elements-jsx.d.ts +1 -1
  21. package/dist/events.d.ts +7 -1
  22. package/dist/internal/package-metadata.d.ts +1 -1
  23. package/dist/internal/package-metadata.js +1 -1
  24. package/dist/internal/tokens.styles.js +1 -1
  25. package/dist/lyra.d.ts +1 -1
  26. package/dist/styles/tokens-root.css +1 -1
  27. package/dist/svelte.d.ts +1 -1
  28. package/dist/testing/lyra-tag-event-map.js +1 -1
  29. package/dist/vue.d.ts +1 -1
  30. package/llms/components/lr-combobox.md +8 -4
  31. package/llms/components/lr-filter-bar.md +37 -4
  32. package/llms/components/lr-icon-button.md +15 -7
  33. package/llms/components/lr-map.md +87 -10
  34. package/llms/components/lr-option.md +8 -4
  35. package/llms/components/lr-select.md +30 -5
  36. package/llms/shared.md +46 -0
  37. package/llms/tokens.md +8 -3
  38. package/llms-full.txt +227 -29
  39. package/package.json +1 -1
  40. package/vscode-css-data.json +1 -1
  41. package/vscode-html-data.json +1 -1
  42. package/web-types.json +1 -1
package/llms-full.txt CHANGED
@@ -1218,6 +1218,52 @@ code. See each control's own reference page for its exact pair.
1218
1218
  `lr-select`. It is a _floor_, not a fixed size. Keep the resolved value **at or above 24px**
1219
1219
  (WCAG 2.2 SC 2.5.8 target size); the default leaves headroom. Lowering it below that shrinks
1220
1220
  every affordance in the library at once.
1221
+ - **`--lr-icon-button-size-scope`** resizes icon buttons for **one subtree** instead of the whole
1222
+ application. Set it on any wrapper; it inherits past intervening components and reaches every
1223
+ icon-only control below it. Three names, three scopes — pick by how far you want the change to
1224
+ reach:
1225
+
1226
+ | Property | Scope | Set it on |
1227
+ | --- | --- | --- |
1228
+ | `--lr-theme-icon-button-size` | application-wide | `:root`, or any ancestor |
1229
+ | `--lr-icon-button-size-scope` | one subtree | the wrapper you want affected |
1230
+ | `--lr-icon-button-size` | one element | the icon-only control itself |
1231
+
1232
+ `--lr-icon-button-size-scope` wins wherever both ancestor inputs are set — the narrower scope
1233
+ takes precedence, and it has to: the shipped `design-tokens.css` declares
1234
+ `--lr-theme-icon-button-size` on `:root`, and a `var()` chain only falls through for a property
1235
+ that is unset *everywhere*, not merely shadowed nearer the element. Reading the theme tier first
1236
+ therefore made this knob inert for anyone loading that stylesheet (a real 18.1.0 defect, fixed in
1237
+ 18.2.0). `--lr-icon-button-size`
1238
+ is **element-scoped on purpose** and is not a wrapper knob: every component re-declares it on its
1239
+ own host so the touch-target floor can apply per element, so a value set on a wrapper is replaced
1240
+ at the first component in between and never reaches anything nested inside one. That behaviour is
1241
+ unchanged — use `--lr-icon-button-size-scope` for the wrapper case.
1242
+
1243
+ ```css
1244
+ /* A dense toolbar, without touching the rest of the app. */
1245
+ .message-toolbar {
1246
+ --lr-icon-button-size-scope: 1.75rem;
1247
+ }
1248
+ ```
1249
+
1250
+ The coarse-pointer floor still applies to both ancestor inputs: on a touch device (`hover: none`
1251
+ or `pointer: coarse`) a resolved value below `2.75rem` is raised back to it, so the new subtree
1252
+ knob is not a way around WCAG 2.2 SC 2.5.8 — a deliberately dense desktop toolbar still becomes
1253
+ tappable on a phone.
1254
+ - **`--lr-color-surface-overlay` follows `--lr-theme-color-surface-default` in both modes.** It is
1255
+ the panel colour behind every floating surface — dropdowns, listboxes, menus, toasts, popovers,
1256
+ dialogs, and the `lr-app-rail` mobile drawer. In light mode it resolves straight to
1257
+ `--lr-color-surface`, so a re-skinned page surface carries them all with it. Dark mode cannot
1258
+ resolve to the page surface — panel and page would be the same near-black, and an open dialog
1259
+ would read as a scrim with text floating on it and no panel at all — so it is **derived** from
1260
+ the page surface instead: `color-mix(in srgb, var(--lr-color-surface) 85%, #8bade2)`, which lifts
1261
+ the panel a fixed amount above whatever the base happens to be. One
1262
+ `--lr-theme-color-surface-default` override therefore re-skins every floating surface in dark
1263
+ mode too, and the elevation delta survives the re-skin. At the built-in dark base the pair still
1264
+ resolves to the same panel colour it always has, so no existing dark theme moves.
1265
+ `--lr-theme-color-surface-overlay` still wins outright when you set it — reach for it only when
1266
+ you want a panel colour unrelated to the page surface.
1221
1267
  - **Aligning your own content next to a checkbox or radio.** `--lr-checkbox-label-indent` /
1222
1268
  `--lr-radio-label-indent` publish the label offset, but custom properties inherit _down_, not
1223
1269
  sideways, so a sibling node in your tree cannot read them off the control. Compute the same
@@ -4934,10 +4980,14 @@ An async `source` row can carry the same two fields (`start`, `end`) alongside i
4934
4980
  - `sync?: PlaceSync` (reflected) — copies the trigger's width, height, or both onto the listbox,
4935
4981
  spelled the same as on `lr-dropdown`/`lr-popup`/`lr-popover`. Unset (the default), the listbox
4936
4982
  sizes to its own content, clamped between `--lr-size-12rem` and `--lr-size-28rem`, exactly as
4937
- before. `sync="width"` drops that content-based clamp (keeping only the outer viewport/
4938
- available-space ceiling) so a full-width trigger with short option labels gets a listbox that
4939
- aligns to its own edges instead of floating narrower in the middle. Like `placement`, a change
4940
- takes effect the next time the listbox opens
4983
+ before. `sync="width"` drops that content-based clamp so a full-width trigger with short option
4984
+ labels gets a listbox that aligns to its own edges instead of floating narrower in the middle.
4985
+ A synced listbox is capped on `--lr-positioner-available-inline-size` **alone** the space the
4986
+ positioner actually measured beside the anchor, which still keeps an over-wide trigger from
4987
+ pushing it off-screen. `--lr-popover-viewport-clamp` does **not** apply to a synced listbox — its
4988
+ 92vw default used to shorten the listbox against its own trigger in exactly the full-width case
4989
+ `sync` exists for, and `lr-popup` never applied it either. It still applies with `sync` unset.
4990
+ Like `placement`, a change takes effect the next time the listbox opens
4941
4991
  - `clearable: boolean = false` (reflected) — displays the clear button while there is something to
4942
4992
  clear on **either** axis this control owns: a committed selection, or _visible_ filter text. See
4943
4993
  "the clear button covers two axes" below
@@ -5522,7 +5572,11 @@ exactly like the multi-option case, until the trigger is actually activated.
5522
5572
 
5523
5573
  **Properties:**
5524
5574
 
5525
- - `placeholder: string = ''`
5575
+ - `placeholder: string = ''` — text shown on the trigger while nothing is selected, and the
5576
+ trigger's accessible name when neither a host `aria-label` nor a `label` supplies one. One
5577
+ exception to "an empty selection always shows this": while `loading` is `true` the trigger shows
5578
+ the localized `loading` text instead — see `loading` below. The accessible name is unaffected
5579
+ either way
5526
5580
  - `disabled: boolean = false` (reflected)
5527
5581
  - `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
5528
5582
  - `name: string = ''`
@@ -5561,6 +5615,17 @@ exactly like the multi-option case, until the trigger is actually activated.
5561
5615
  authored, the listbox is actually placed with the cascading `--lr-positioning-strategy` custom
5562
5616
  property honored ahead of that default — see the listbox's own **Themeable custom properties**
5563
5617
  below
5618
+ - `sync?: PlaceSync` (reflected) — copies the trigger's width, height, or both onto the listbox,
5619
+ spelled and typed the same as on `lr-popup`/`lr-popover`/`lr-dropdown`/`lr-combobox`
5620
+ (`'width' | 'height' | 'both'`). Unset (the default), the listbox sizes to its own content,
5621
+ clamped between `--lr-size-12rem` and `min(--lr-popover-viewport-clamp, --lr-size-28rem)`,
5622
+ exactly as before. `sync="width"` drops that content-based clamp so a full-width trigger with
5623
+ short option labels gets a listbox that aligns to its own edges instead of floating narrower in
5624
+ the middle. A synced listbox is capped on `--lr-positioner-available-inline-size` **alone** — the
5625
+ space the positioner actually measured beside the trigger, which still keeps an over-wide trigger
5626
+ from pushing it off-screen; `--lr-popover-viewport-clamp` does **not** apply to a synced listbox,
5627
+ and still does with `sync` unset. Assignment while open repositions in place without closing;
5628
+ unsetting it releases the inline width the positioner wrote
5564
5629
  - `showUnknownOption: boolean = false` (attribute `show-unknown-option`, reflected) — appends every
5565
5630
  committed value that no `<lr-option>` claims to the end of the listbox as a synthetic, badged,
5566
5631
  keyboard-reachable, re-selectable row. Off by default
@@ -5575,9 +5640,16 @@ exactly like the multi-option case, until the trigger is actually activated.
5575
5640
  `loading` placeholder in the trigger label or the relevant `multiple` tag instead of the raw
5576
5641
  value, with no `notInCatalog`/`[part='unknown-value']` badge and no synthetic
5577
5642
  `showUnknownOption` listbox row — "not yet resolved" is a different state from "known to be
5578
- missing". A value already matching a live option is unaffected. Never mutates
5579
- `value`/`selectedOptions` itself, and does not itself disable the trigger pair it with
5580
- `disabled` when the control should also be non-interactive while pending
5643
+ missing". It covers an **empty** selection too: with nothing selected a create form whose
5644
+ catalogue is still being fetched, or an edit form whose saved selection is legitimately empty
5645
+ the trigger renders that same localized text in place of `placeholder`, from the same `loading`
5646
+ message key, so both halves of a pending state read the same words and one
5647
+ `registerLyraLocale()` translation reaches both. Nothing to re-localize in the consuming app.
5648
+ With `loading` false an empty selection renders `placeholder` exactly as before. The trigger's
5649
+ accessible name never changes for this: a host `aria-label` wins, then `label`, then
5650
+ `placeholder`, then the localized `select` fallback. A value already matching a live option is
5651
+ unaffected. Never mutates `value`/`selectedOptions` itself, and does not itself disable the
5652
+ trigger — pair it with `disabled` when the control should also be non-interactive while pending
5581
5653
  - `filled: boolean = false` (reflected) — Shoelace alias for the filled trigger treatment
5582
5654
  - `autofocus: boolean = false` / `title: string = ''` — forwarded to the internal trigger
5583
5655
  - `multiple: boolean = false` (reflected) — several options selectable at once; see "Multi-select"
@@ -5636,7 +5708,10 @@ a still-unmatched value then renders the localized `loading` placeholder instead
5636
5708
  with no `unknown-value` badge and no synthetic `showUnknownOption` row, since it is not yet known
5637
5709
  to be missing. Once the matching option mounts, the real label renders on the next render with no
5638
5710
  `value`/`selectedOptions` re-assignment needed, whether or not `loading` is also flipped back to
5639
- `false`.
5711
+ `false`. The same flag covers the other half of that state: with **nothing** selected the trigger
5712
+ renders the same localized `loading` text in place of `placeholder`, so a consumer never has to
5713
+ hand-write a conditional placeholder bound to the same flag and re-localize, in its own catalogue,
5714
+ the string this control already owns.
5640
5715
 
5641
5716
  **Methods:** `focus(options?)`, `blur()`, and `click()` forward to the internal trigger button.
5642
5717
  `show()` and `hide()` return `Promise<void>` and resolve after `lr-after-show`/`lr-after-hide` once
@@ -7082,15 +7157,23 @@ above 24px — see `llms/shared.md`. **`--lr-icon-button-size` is element-scoped
7082
7157
  token in this section:** the shared token layer re-declares it on every `lr-*` host's own `:host`,
7083
7158
  so a rule that sets `--lr-icon-button-size` on an ancestor wrapper is reset the moment it crosses
7084
7159
  into any intervening `lr-*` component and never reaches a `<lr-icon-button>` composed inside it
7085
- (e.g. one slotted through `<lr-popover>`). The only two levers that actually reach it are: setting
7160
+ (e.g. one slotted through `<lr-popover>`). The three levers that actually reach it are: setting
7086
7161
  `--lr-icon-button-size` directly on the icon button element itself, where no intervening component
7087
- sits between the rule and the property; or setting `--lr-theme-icon-button-size` on an ancestor,
7088
- which the shared layer reads through `var()` at every level and which therefore reaches a
7162
+ sits between the rule and the property; setting `--lr-icon-button-size-scope` on any ancestor to
7163
+ resize one subtree; or setting `--lr-theme-icon-button-size` on an ancestor to resize the whole
7164
+ application. The shared layer reads the latter two through `var()` at every level, so both reach a
7089
7165
  `<lr-icon-button>` composed inside another component, e.g. `<lr-copy-button>`/
7090
- `<lr-message-actions>`. Lowering the floor for a dense action row through either lever is safe even
7091
- below 24px: a coarse-pointer/no-hover media rule floors the RENDERED hit area back at 2.75rem/44px
7092
- regardless of how far the override lowered it, so the control stays comfortably tappable the moment
7093
- the pointer reaching it is a finger rather than a mouse. `--lr-icon-button-radius` (default
7166
+ `<lr-message-actions>` -- see `llms/shared.md` for the full scope table.
7167
+
7168
+ Lowering the floor for a dense action row below 24px is safe **only through the two ancestor
7169
+ levers**. The coarse-pointer/no-hover media rule reads those, and floors the RENDERED hit area back
7170
+ at 2.75rem/44px however far the override lowered it, so the control stays comfortably tappable the
7171
+ moment the pointer reaching it is a finger rather than a mouse. It does **not** rescue
7172
+ `--lr-icon-button-size` set directly on the element: that declaration comes from the outer tree and
7173
+ outranks the shadow tree's own `:host` rule, so the media rule never wins and the rendered hit area
7174
+ is exactly the value set -- `--lr-icon-button-size: 1rem` really does render a 16px target under a
7175
+ coarse pointer, which fails WCAG 2.2 SC 2.5.8. Prefer `--lr-icon-button-size-scope` when the intent
7176
+ is a denser row; reach for the element-scoped name only as a deliberate, localized trade-off. `--lr-icon-button-radius` (default
7094
7177
  `--lr-radius`) is not re-declared anywhere in the shared layer, so — like every other token below —
7095
7178
  it inherits normally from an ancestor even through an intervening component; it is the
7096
7179
  `[part='button']` corner radius, retunable without a `::part(button)` rule — the same
@@ -20518,8 +20601,21 @@ until something else registers it, the same trade `icon-button-register.js` docu
20518
20601
  - `disabled: boolean = false` (reflected) — disables every filter control and reset action.
20519
20602
  - `loading: boolean = false` (reflected) — shows the status spinner and disables reset while leaving
20520
20603
  filters editable.
20521
- - `hasActiveFilters: boolean` (read-only) — whether any configured filter currently has a value.
20522
- Drives the reset button's own disabled state; unaffected by `activeFiltersDisplay`.
20604
+ - `hasActiveFilters: boolean` (read-only) — whether any configured filter currently has a value,
20605
+ including one sitting at its own declared `defaultValue`. Drives the reset button's own disabled
20606
+ state in every `activeFiltersDisplay` mode except `'changed'`; the getter itself is unaffected by
20607
+ `activeFiltersDisplay`.
20608
+ - `hasChangedFilters: boolean` (read-only) — whether any filter's value differs from its own
20609
+ declared `defaultValue`, using the same equality `activeFiltersDisplay: 'changed'` filters its
20610
+ chip row on: a `readonly string[]` default compares positionally, everything else compares with
20611
+ `Object.is`. This is the counterpart to `hasActiveFilters`, not a synonym — a bar whose filters
20612
+ were all declared with non-empty defaults and never touched reads `hasActiveFilters === true` and
20613
+ `hasChangedFilters === false`, because a bar whose defaults narrow the view on load has not been
20614
+ narrowed by the user. A filter with no declared `defaultValue` counts as changed the moment it
20615
+ holds any value at all (there is nothing for it to still equal), and clearing a filter that *does*
20616
+ declare one counts as changed too, since `reset()` would restore it — which is the one case where
20617
+ this getter and the `'changed'` chip row differ, the row's entries being non-empty by
20618
+ construction. Always live, never cached.
20523
20619
  - `invalidFilterIds: readonly string[]` (read-only) — immutable ids of required filters whose
20524
20620
  values are unset.
20525
20621
  - `activeFiltersDisplay: 'all' | 'changed' | 'hidden' = 'all'` (reflected, attribute
@@ -20533,6 +20629,10 @@ until something else registers it, the same trade `icon-button-register.js` docu
20533
20629
  at each index), matching this component's only other array-equality precedent (a custom adapter's
20534
20630
  own `clearValue` comparison); a `'date-range'` value is a single composed string, so it compares
20535
20631
  like any other string. Removing a chip always clears that filter, unaffected by this property.
20632
+ `'changed'` additionally gates the reset button on `hasChangedFilters` instead of
20633
+ `hasActiveFilters`, so an untouched defaults-only bar — which renders no chip in this mode — no
20634
+ longer offers an enabled reset that would change nothing. Enablement under `'all'` and `'hidden'`
20635
+ is unchanged, `disabled`/`loading` still win in every mode, and `reset()` itself is untouched.
20536
20636
 
20537
20637
  The composed reset action uses `lr-button`'s default `m` size tier, matching the default rendered
20538
20638
  height of adjacent select, combobox, input, and date fields instead of introducing a shorter action
@@ -20667,8 +20767,8 @@ control at all (see **Chip-only filters** below): its value belongs to a widget
20667
20767
  page, so the bar renders only its active-filter chip and gives it no toolbar cell.
20668
20768
 
20669
20769
  Every built-in (non-`'custom'`) filter definition additionally accepts optional `size: LyraSize`,
20670
- `icon: unknown` and `labelVisibility: 'visible' | 'hidden'` fields, and every one whose composed
20671
- control ships a clear action also accepts `clearable: boolean`. They are forwarded verbatim to that
20770
+ `icon: unknown` and `labelVisibility: 'visible' | 'hidden' | 'auto'` fields, and every one whose
20771
+ composed control ships a clear action also accepts `clearable: boolean`. They are forwarded verbatim to that
20672
20772
  control's own same-named property — `icon` into its `start` slot exactly like a choice option's own
20673
20773
  `icon`, rendered inert and `aria-hidden`; `clearable` reaching `<lr-date-input>` under its own
20674
20774
  `with-clear` spelling, since that control has no `clearable`. `'text'` also accepts
@@ -20687,6 +20787,20 @@ so a compact toolbar row still names every field for assistive technology; visua
20687
20787
  `::part(filter-control-label)` in CSS, the only previous option, removed the accessible name along
20688
20788
  with the text.
20689
20789
 
20790
+ `labelVisibility: 'auto'` is the width-dependent middle between the two. It renders exactly what
20791
+ `'visible'` renders — the same stacked label element, the same accessible name computed from it, no
20792
+ `aria-label` and no placeholder fallback — and the bar's own stylesheet visually clips that label
20793
+ once the bar's allocation drops below `30rem`. The threshold is a container query on the host, so it
20794
+ reads the bar's own allocated width, not the viewport's: the same definitions render labelled across
20795
+ a dashboard and unlabelled in a 320px side panel, dialog or split pane, with no host-side breakpoint
20796
+ logic. The label element is never removed at any width, so the field's accessible name is identical
20797
+ in both states, and `'auto'` deliberately does not route the name onto the control the way
20798
+ `'hidden'` does — doing so would name a wide-allocation field twice. A `'checkbox-menu'` filter
20799
+ participates through its own trigger label run, the same one `'hidden'` already clips there. The
20800
+ threshold is fixed rather than themeable: a CSS container query's prelude cannot read a custom
20801
+ property (`var()` is not substituted in an at-rule prelude), so a `--lr-*` hook for it would parse
20802
+ and silently never apply.
20803
+
20690
20804
  `'combobox'` also accepts the same `debounce?: number` (ms) `'text'` already had: it coalesces a
20691
20805
  burst of rapid selection changes (picks, a multi-select toggle, an
20692
20806
  `allowCustomValue`/`allowCreate` commit, or the clear action) into one delayed commit. Unlike
@@ -20716,6 +20830,8 @@ makes that text visually hidden — never removed — so the button keeps its ac
20716
20830
  one case where the hidden label would be the *only* thing the button says (hidden routing, no
20717
20831
  declared `placeholder`, nothing selected) the label routes to the visible summary instead of being
20718
20832
  emitted twice, so the trigger's accessible name stays "Teams", never "Teams Teams".
20833
+ `labelVisibility: 'auto'` clips that same trigger label run, and only below the `30rem` threshold —
20834
+ the label run is always emitted under `'auto'`, since nothing is routed to the summary there.
20719
20835
 
20720
20836
  Because its trigger is a button rather than a field, a `required` `'checkbox-menu'` deliberately
20721
20837
  renders **no** required asterisk and sets **no** `aria-invalid`: the shared required marker has no
@@ -27222,6 +27338,21 @@ LyraMapLegendPattern }`, where `LyraMapLegendPattern` is `'solid' | 'diagonal' |
27222
27338
  and their point-icon defaults, and the same validation applies (path data only, at most 8,192
27223
27339
  characters, positive `viewBox` dimensions). An unusable record is dropped and that row keeps
27224
27340
  rendering its colour swatch, exactly as a row that supplies no `icon` does.
27341
+ A row may finally carry a `group`: the section it belongs to, for a key that describes two
27342
+ layers at once and otherwise could not say which rows belong to which. The rule is pinned
27343
+ rather than inferred: **consecutive** entries sharing an identical `group` render as one section
27344
+ — a visible heading plus a `role="group"` that heading names — an entry with **no** `group`
27345
+ keeps its **declared** position rather than being hoisted above or sunk below a section, and a
27346
+ `group` that reappears after an interruption opens a *second* section rather than reordering
27347
+ rows to merge them. Declaration order is the one thing the legend never rewrites, because the
27348
+ order is itself information about the map. `group` is caller-supplied **data**: it renders
27349
+ verbatim and is never resolved through the locale catalogue. It is trimmed and bounded to 256
27350
+ characters (ellipsized, since it is rendered prose rather than a key matched against
27351
+ `point.field`); a non-string, empty or whitespace-only value leaves no `group` property on the
27352
+ frozen row at all, so an empty string means "ungrouped" instead of an empty heading. Like the
27353
+ row-level `value`, it does not count toward the aggregate label budget — the rendered total is
27354
+ already finite and stated: at most one heading per rendered row, so at most 100 of them. A
27355
+ section is not a row: the 100-row cap and the `legend-limit` summary count rows, never sections.
27225
27356
  - readonly `legendProjection: LyraMapLegendProjection` — frozen `{ inputCount, renderedCount,
27226
27357
  omittedCount, truncatedLabelCount, truncated }` result for the latest assignment. A truncated
27227
27358
  projection renders a localized visible `1–N of M items` summary rather than silently claiming
@@ -27234,6 +27365,33 @@ omittedCount, truncatedLabelCount, truncated }` result for the latest assignment
27234
27365
  roving tabindex: a 100-row interactive legend contributes 100 tab stops, exactly as a 100-series
27235
27366
  `lr-chart` legend does. Each interactive row also grows to the shared `--lr-icon-button-size`
27236
27367
  hit-area floor (WCAG 2.5.8), which the panel's existing `max-block-size` and scrolling contain.
27368
+ - `legendControlRole: 'button' | 'checkbox' = 'button'` (attribute `legend-control-role`,
27369
+ reflected) — how each `legendInteractive` row's toggle presents itself to assistive tech; inert
27370
+ while `legendInteractive` is unset, exactly like the toggle itself. `'button'` is byte-identical
27371
+ to every legend rendered before this property existed: `<button aria-pressed>`. `'checkbox'`
27372
+ renders the SAME `<button>` element with its implicit role overridden to `role="checkbox"` and
27373
+ `aria-checked` in place of `aria-pressed` — the swatch, the label, the click handler and the
27374
+ platform's own Enter/Space activation are unchanged, so `hiddenCategories`, the cancelable
27375
+ `lr-map-legend-toggle` veto, `group` sections and `legendCollapsible` all compose with either
27376
+ role. `aria-checked` tracks the same visibility flag `aria-pressed` does, inverted from
27377
+ `hiddenCategories`: a hidden category renders `aria-checked="false"`. Prefer `'checkbox'` when the
27378
+ legend reads as a checklist of independent show/hide toggles; prefer the `'button'` default when
27379
+ it reads as a set of filtering actions. An unsupported value normalizes back to `'button'`.
27380
+ - `legendCollapsible: boolean = false` (attribute `legend-collapsible`, reflected) — opt-in: renders
27381
+ a `legend-disclosure` `button` inside the panel that collapses the key down to its header, so a
27382
+ large legend stops permanently covering part of the map. Unset, the panel renders exactly what it
27383
+ rendered before — no button, no `id` minted on the row list, and no `hidden` attribute anywhere.
27384
+ Collapsing hides the gradient bar, the rows, the `legend-limit` summary and the trailing `legend`
27385
+ slot; the `legend-start` slot and the disclosure itself stay visible, so a slotted header survives
27386
+ the collapse and the control that restores the key is never what the collapse hides.
27387
+ - `legendOpen: boolean = true` (attribute `legend-open`, reflected) — whether a `legendCollapsible`
27388
+ panel is expanded. It defaults **open**, so adding only `legendCollapsible` never hides an existing
27389
+ key, and it does nothing at all while `legendCollapsible` is unset. Because it is a
27390
+ `true`-defaulting boolean it uses a custom attribute converter, so `legend-open="false"` parses —
27391
+ the bare presence-based boolean form cannot express `false` at all — and the reflection follows the
27392
+ same converter: open (the default) reflects as an **absent** attribute and collapsed reflects as
27393
+ `legend-open="false"`. It is controlled public state and survives a disconnect/reconnect. Assigning
27394
+ it programmatically reconciles the rendered panel and emits nothing.
27237
27395
  - `hiddenCategories: readonly string[] = []` (attribute: false) — the complete controlled set of
27238
27396
  muted category keys, mirroring `lr-chart`'s `hiddenDatasets`. Clone-owned and frozen; non-string,
27239
27397
  empty, whitespace-only and duplicate entries are dropped (first occurrence wins, matching
@@ -27522,8 +27680,8 @@ payload beside the map.
27522
27680
  `LyraMapHeatmapZoomValue`, `LyraMapLineOptions`, `LyraMapPointOptions`, `LyraMapPointRadiusOptions`,
27523
27681
  `LyraMapPointRadiusInterpolation`, `LyraMapPointIcon`, `LyraMapPointIconMode`,
27524
27682
  `LyraMapPointIconLineCap`, `LyraMapPointIconLineJoin`, `LyraMapMarker`, `LyraMapMarkerActivationDetail`,
27525
- `LyraMapMarkerActivationSource`, `LyraMapLegendToggleDetail`, `LyraMapStyleSpecification`, and
27526
- `LyraMapInstance`.
27683
+ `LyraMapMarkerActivationSource`, `LyraMapLegendToggleDetail`, `LyraMapLegendPanelToggleDetail`,
27684
+ `LyraMapStyleSpecification`, and `LyraMapInstance`.
27527
27685
  The former `LegendEntry`, `ChoroplethLayer`, `GeoJsonDataLayer`, and `MapMarker` names are removed
27528
27686
  in v9 rather than retained as aliases.
27529
27687
 
@@ -27549,6 +27707,13 @@ the set and assign its own value instead. There is deliberately no second, confi
27549
27707
  committed state is `hiddenCategories`, which the host already observes, so a paired before/after
27550
27708
  vocabulary would be permanent public surface nobody asked for. The event is a DOM-interaction
27551
27709
  proposal only, so a programmatic `hiddenCategories` assignment reconciles without emitting it.
27710
+ Also `lr-map-legend-panel-toggle` (**cancelable**; frozen `LyraMapLegendPanelToggleDetail { open }` —
27711
+ the proposed `legendOpen` value), fired once when the `legendCollapsible` disclosure is activated by
27712
+ pointer or by Enter/Space. It is the *panel's* disclosure, not a *category's* visibility, so it
27713
+ deliberately does not reuse `lr-map-legend-toggle`. `preventDefault()` is the same genuine veto:
27714
+ `legendOpen` is not written, the rendered rows and the disclosure's `aria-expanded` do not change,
27715
+ so a host can own the open state and assign its own value from `event.detail.open`. A programmatic
27716
+ `legendOpen` assignment reconciles without emitting it, so a controlled host cannot loop.
27552
27717
  Also `lr-map-marker-activate` (non-cancelable; frozen `LyraMapMarkerActivationDetail { id, lngLat,
27553
27718
  marker, source }`; `id` is the trimmed explicit identity or `undefined`, `marker` is the accepted
27554
27719
  declarative snapshot, and `source` is `'pointer' | 'keyboard'`), and `lr-map-click`
@@ -27571,9 +27736,14 @@ markup as described above.
27571
27736
 
27572
27737
  **Slots:** `legend` — custom legend content, rendered inside the legend panel's own layout so it
27573
27738
  stays positioned with the map instead of floating beside it. Supplying it opens the panel even
27574
- when `legend` and `legendGradient` are both empty.
27575
-
27576
- **CSS parts:** `base`, `container`, `legend`, `legend-swatch`, `legend-toggle`,
27739
+ when `legend` and `legendGradient` are both empty. `legend-start` — the same extension point at the
27740
+ **top** of the panel: it renders ahead of the gradient bar and every projected row, where `legend`
27741
+ renders after them, so a host-authored panel header is no longer forced to be a footer. Content in
27742
+ it alone opens the panel too, and neither slot is ever made interactive by `legendInteractive`,
27743
+ which only reaches rows projected from `legend`.
27744
+
27745
+ **CSS parts:** `base`, `container`, `legend`, `legend-disclosure`, `legend-disclosure-icon`,
27746
+ `legend-group`, `legend-group-heading`, `legend-swatch`, `legend-toggle`,
27577
27747
  `legend-toggle-hidden`, `legend-gradient`, `legend-lo`,
27578
27748
  `legend-hi`, `legend-limit`, `marker`, `popup`,
27579
27749
  `popup-content`, `popup-close-button`, `attribution`, `attribution-toggle`, `navigation`,
@@ -27601,16 +27771,39 @@ container in the tree, so `legend` withholds `aria-controls` rather than leaving
27601
27771
  idref.
27602
27772
  Under `legendInteractive`, a row carrying a `value` wraps its swatch and label in a
27603
27773
  `legend-toggle` `button` with `aria-pressed` rendered as the literal `"true"`/`"false"` — never
27604
- omitted, because a missing attribute reports "not a toggle button" rather than "unpressed". The
27605
- button's accessible name is its own visible label (caller-supplied data, so deliberately not
27606
- localized), scoped by the legend's own localized group name; no `aria-label` restates the state,
27607
- which would make assistive tech announce it twice. A hidden row's button additionally carries the
27774
+ omitted, because a missing attribute reports "not a toggle button" rather than "unpressed". Setting
27775
+ `legendControlRole="checkbox"` renders the SAME `button` with `role="checkbox"` and `aria-checked`
27776
+ (also always the literal `"true"`/`"false"`) in place of `aria-pressed`, and nothing else about the
27777
+ row changes. The button's accessible name is its own visible label (caller-supplied data, so
27778
+ deliberately not localized), scoped by the legend's own localized group name; no `aria-label`
27779
+ restates the state, which would make assistive tech announce it twice. A hidden row's button
27780
+ additionally carries the
27608
27781
  `legend-toggle-hidden` token — state lives in the part name, so `::part(legend-toggle-hidden)` is a
27609
27782
  reachable hook — and dims only its `aria-hidden` swatch while re-colouring the label through the
27610
27783
  quiet text token, so the label keeps AA contrast rather than fading with the whole button. In
27611
27784
  forced colors the hidden row falls back to `line-through`, which survives a system-color collapse.
27612
27785
  Each activation is announced through the shared light-DOM polite live region, using the same
27613
27786
  `legendTypeShown`/`legendTypeHidden` strings `lr-graph-legend` announces with.
27787
+ Under `legendCollapsible`, `legend-disclosure` is a native `button` whose **visible localized text
27788
+ is its accessible name** and whose `aria-expanded` renders the literal `"true"`/`"false"` — never
27789
+ omitted — with `aria-controls` naming the row list in the same shadow root (idrefs do not cross
27790
+ shadow boundaries, so this is deliberately not an idref into the light DOM). It carries the shared
27791
+ `--lr-icon-button-size` hit-area floor, and `legend-disclosure-icon` is the decorative chevron it
27792
+ rotates: the shared icon set ships one right-pointing glyph and asks callers to rotate the wrapping
27793
+ part, so collapsed points along the reading direction and expanded points down in both directions.
27794
+ The rotation runs on `--lr-transition-fast`, which the token layer already flattens under
27795
+ `prefers-reduced-motion`. A collapsed panel hides its gradient, rows, `legend-limit` summary and
27796
+ trailing `legend` slot with the plain `hidden` attribute, so they leave layout, the accessibility
27797
+ tree and the tab order together.
27798
+ When at least one entry carries a `group`, each consecutive run renders inside `legend-group`
27799
+ (`role="group"`, named by its own `legend-group-heading` through `aria-labelledby`) and **each run
27800
+ gets its own `role="list"`**, because a `list` may only own `listitem`s — a `group` sitting directly
27801
+ inside the outer list is an `aria-required-children`/`aria-required-parent` violation. The outer
27802
+ container therefore drops its own `role` in that case; a legend with no groups keeps the single
27803
+ `role="list"` it has always had. Row `aria-posinset`/`aria-setsize` stay whole-key values inside a
27804
+ section: `aria-setsize` already reports the *input* count so a bounded key stays honest, and a
27805
+ dropped row carries no attributable group, so a section adds a labelled sub-region without
27806
+ renumbering the key.
27614
27807
  `legend-limit` is the localized bounded-projection summary. The five peer-chrome parts project
27615
27808
  stable Lyra names onto MapLibre-generated DOM without erasing peer-supplied part tokens;
27616
27809
  `marker` retains a 24px minimum target in both axes even when a peer/custom marker has no intrinsic
@@ -30352,6 +30545,7 @@ These named interfaces and helper signatures are available to typed integrations
30352
30545
  readonly label: string;
30353
30546
  readonly pattern: LyraMapLegendPattern;
30354
30547
  readonly icon?: LyraMapPointIcon | Omit<LyraMapPointIcon, 'value'>;
30548
+ readonly group?: string;
30355
30549
  }`
30356
30550
  Import: `@aceshooting/lyra-ui/components/media/map/map.class.js`.
30357
30551
  `LyraMapLegendProjection {
@@ -30383,6 +30577,10 @@ These named interfaces and helper signatures are available to typed integrations
30383
30577
  readonly hiddenCategories: readonly string[];
30384
30578
  }`
30385
30579
  Import: `@aceshooting/lyra-ui/components/media/map/map.class.js`.
30580
+ `LyraMapLegendPanelToggleDetail {
30581
+ readonly open: boolean;
30582
+ }`
30583
+ Import: `@aceshooting/lyra-ui/components/media/map/map.class.js`.
30386
30584
  `LyraMapStyleSpecification {
30387
30585
  readonly version: 8;
30388
30586
  readonly sources: Readonly<Record<string, unknown>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aceshooting/lyra-ui",
3
- "version": "18.0.0",
3
+ "version": "18.2.0",
4
4
  "description": "Free, independent Lit web components: an open-source alternative to Shoelace and Web Awesome with accessible forms, dashboards, charts, and agent UI.",
5
5
  "keywords": [
6
6
  "web-components",