@aceshooting/lyra-ui 18.0.0 → 18.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 (42) hide show
  1. package/CHANGELOG.md +229 -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 +134 -5
  17. package/dist/components/media/map/map.class.js +48 -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 +68 -6
  34. package/llms/components/lr-option.md +8 -4
  35. package/llms/components/lr-select.md +30 -5
  36. package/llms/shared.md +41 -0
  37. package/llms/tokens.md +7 -4
  38. package/llms-full.txt +203 -25
  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,47 @@ 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-theme-icon-button-size` wins wherever both ancestor inputs are set. `--lr-icon-button-size`
1233
+ is **element-scoped on purpose** and is not a wrapper knob: every component re-declares it on its
1234
+ own host so the touch-target floor can apply per element, so a value set on a wrapper is replaced
1235
+ at the first component in between and never reaches anything nested inside one. That behaviour is
1236
+ unchanged — use `--lr-icon-button-size-scope` for the wrapper case.
1237
+
1238
+ ```css
1239
+ /* A dense toolbar, without touching the rest of the app. */
1240
+ .message-toolbar {
1241
+ --lr-icon-button-size-scope: 1.75rem;
1242
+ }
1243
+ ```
1244
+
1245
+ The coarse-pointer floor still applies to both ancestor inputs: on a touch device (`hover: none`
1246
+ or `pointer: coarse`) a resolved value below `2.75rem` is raised back to it, so the new subtree
1247
+ knob is not a way around WCAG 2.2 SC 2.5.8 — a deliberately dense desktop toolbar still becomes
1248
+ tappable on a phone.
1249
+ - **`--lr-color-surface-overlay` follows `--lr-theme-color-surface-default` in both modes.** It is
1250
+ the panel colour behind every floating surface — dropdowns, listboxes, menus, toasts, popovers,
1251
+ dialogs, and the `lr-app-rail` mobile drawer. In light mode it resolves straight to
1252
+ `--lr-color-surface`, so a re-skinned page surface carries them all with it. Dark mode cannot
1253
+ resolve to the page surface — panel and page would be the same near-black, and an open dialog
1254
+ would read as a scrim with text floating on it and no panel at all — so it is **derived** from
1255
+ the page surface instead: `color-mix(in srgb, var(--lr-color-surface) 85%, #8bade2)`, which lifts
1256
+ the panel a fixed amount above whatever the base happens to be. One
1257
+ `--lr-theme-color-surface-default` override therefore re-skins every floating surface in dark
1258
+ mode too, and the elevation delta survives the re-skin. At the built-in dark base the pair still
1259
+ resolves to the same panel colour it always has, so no existing dark theme moves.
1260
+ `--lr-theme-color-surface-overlay` still wins outright when you set it — reach for it only when
1261
+ you want a panel colour unrelated to the page surface.
1221
1262
  - **Aligning your own content next to a checkbox or radio.** `--lr-checkbox-label-indent` /
1222
1263
  `--lr-radio-label-indent` publish the label offset, but custom properties inherit _down_, not
1223
1264
  sideways, so a sibling node in your tree cannot read them off the control. Compute the same
@@ -4934,10 +4975,14 @@ An async `source` row can carry the same two fields (`start`, `end`) alongside i
4934
4975
  - `sync?: PlaceSync` (reflected) — copies the trigger's width, height, or both onto the listbox,
4935
4976
  spelled the same as on `lr-dropdown`/`lr-popup`/`lr-popover`. Unset (the default), the listbox
4936
4977
  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
4978
+ before. `sync="width"` drops that content-based clamp so a full-width trigger with short option
4979
+ labels gets a listbox that aligns to its own edges instead of floating narrower in the middle.
4980
+ A synced listbox is capped on `--lr-positioner-available-inline-size` **alone** the space the
4981
+ positioner actually measured beside the anchor, which still keeps an over-wide trigger from
4982
+ pushing it off-screen. `--lr-popover-viewport-clamp` does **not** apply to a synced listbox — its
4983
+ 92vw default used to shorten the listbox against its own trigger in exactly the full-width case
4984
+ `sync` exists for, and `lr-popup` never applied it either. It still applies with `sync` unset.
4985
+ Like `placement`, a change takes effect the next time the listbox opens
4941
4986
  - `clearable: boolean = false` (reflected) — displays the clear button while there is something to
4942
4987
  clear on **either** axis this control owns: a committed selection, or _visible_ filter text. See
4943
4988
  "the clear button covers two axes" below
@@ -5522,7 +5567,11 @@ exactly like the multi-option case, until the trigger is actually activated.
5522
5567
 
5523
5568
  **Properties:**
5524
5569
 
5525
- - `placeholder: string = ''`
5570
+ - `placeholder: string = ''` — text shown on the trigger while nothing is selected, and the
5571
+ trigger's accessible name when neither a host `aria-label` nor a `label` supplies one. One
5572
+ exception to "an empty selection always shows this": while `loading` is `true` the trigger shows
5573
+ the localized `loading` text instead — see `loading` below. The accessible name is unaffected
5574
+ either way
5526
5575
  - `disabled: boolean = false` (reflected)
5527
5576
  - `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
5528
5577
  - `name: string = ''`
@@ -5561,6 +5610,17 @@ exactly like the multi-option case, until the trigger is actually activated.
5561
5610
  authored, the listbox is actually placed with the cascading `--lr-positioning-strategy` custom
5562
5611
  property honored ahead of that default — see the listbox's own **Themeable custom properties**
5563
5612
  below
5613
+ - `sync?: PlaceSync` (reflected) — copies the trigger's width, height, or both onto the listbox,
5614
+ spelled and typed the same as on `lr-popup`/`lr-popover`/`lr-dropdown`/`lr-combobox`
5615
+ (`'width' | 'height' | 'both'`). Unset (the default), the listbox sizes to its own content,
5616
+ clamped between `--lr-size-12rem` and `min(--lr-popover-viewport-clamp, --lr-size-28rem)`,
5617
+ exactly as before. `sync="width"` drops that content-based clamp so a full-width trigger with
5618
+ short option labels gets a listbox that aligns to its own edges instead of floating narrower in
5619
+ the middle. A synced listbox is capped on `--lr-positioner-available-inline-size` **alone** — the
5620
+ space the positioner actually measured beside the trigger, which still keeps an over-wide trigger
5621
+ from pushing it off-screen; `--lr-popover-viewport-clamp` does **not** apply to a synced listbox,
5622
+ and still does with `sync` unset. Assignment while open repositions in place without closing;
5623
+ unsetting it releases the inline width the positioner wrote
5564
5624
  - `showUnknownOption: boolean = false` (attribute `show-unknown-option`, reflected) — appends every
5565
5625
  committed value that no `<lr-option>` claims to the end of the listbox as a synthetic, badged,
5566
5626
  keyboard-reachable, re-selectable row. Off by default
@@ -5575,9 +5635,16 @@ exactly like the multi-option case, until the trigger is actually activated.
5575
5635
  `loading` placeholder in the trigger label or the relevant `multiple` tag instead of the raw
5576
5636
  value, with no `notInCatalog`/`[part='unknown-value']` badge and no synthetic
5577
5637
  `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
5638
+ missing". It covers an **empty** selection too: with nothing selected a create form whose
5639
+ catalogue is still being fetched, or an edit form whose saved selection is legitimately empty
5640
+ the trigger renders that same localized text in place of `placeholder`, from the same `loading`
5641
+ message key, so both halves of a pending state read the same words and one
5642
+ `registerLyraLocale()` translation reaches both. Nothing to re-localize in the consuming app.
5643
+ With `loading` false an empty selection renders `placeholder` exactly as before. The trigger's
5644
+ accessible name never changes for this: a host `aria-label` wins, then `label`, then
5645
+ `placeholder`, then the localized `select` fallback. A value already matching a live option is
5646
+ unaffected. Never mutates `value`/`selectedOptions` itself, and does not itself disable the
5647
+ trigger — pair it with `disabled` when the control should also be non-interactive while pending
5581
5648
  - `filled: boolean = false` (reflected) — Shoelace alias for the filled trigger treatment
5582
5649
  - `autofocus: boolean = false` / `title: string = ''` — forwarded to the internal trigger
5583
5650
  - `multiple: boolean = false` (reflected) — several options selectable at once; see "Multi-select"
@@ -5636,7 +5703,10 @@ a still-unmatched value then renders the localized `loading` placeholder instead
5636
5703
  with no `unknown-value` badge and no synthetic `showUnknownOption` row, since it is not yet known
5637
5704
  to be missing. Once the matching option mounts, the real label renders on the next render with no
5638
5705
  `value`/`selectedOptions` re-assignment needed, whether or not `loading` is also flipped back to
5639
- `false`.
5706
+ `false`. The same flag covers the other half of that state: with **nothing** selected the trigger
5707
+ renders the same localized `loading` text in place of `placeholder`, so a consumer never has to
5708
+ hand-write a conditional placeholder bound to the same flag and re-localize, in its own catalogue,
5709
+ the string this control already owns.
5640
5710
 
5641
5711
  **Methods:** `focus(options?)`, `blur()`, and `click()` forward to the internal trigger button.
5642
5712
  `show()` and `hide()` return `Promise<void>` and resolve after `lr-after-show`/`lr-after-hide` once
@@ -7082,15 +7152,23 @@ above 24px — see `llms/shared.md`. **`--lr-icon-button-size` is element-scoped
7082
7152
  token in this section:** the shared token layer re-declares it on every `lr-*` host's own `:host`,
7083
7153
  so a rule that sets `--lr-icon-button-size` on an ancestor wrapper is reset the moment it crosses
7084
7154
  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
7155
+ (e.g. one slotted through `<lr-popover>`). The three levers that actually reach it are: setting
7086
7156
  `--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
7157
+ sits between the rule and the property; setting `--lr-icon-button-size-scope` on any ancestor to
7158
+ resize one subtree; or setting `--lr-theme-icon-button-size` on an ancestor to resize the whole
7159
+ application. The shared layer reads the latter two through `var()` at every level, so both reach a
7089
7160
  `<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
7161
+ `<lr-message-actions>` -- see `llms/shared.md` for the full scope table.
7162
+
7163
+ Lowering the floor for a dense action row below 24px is safe **only through the two ancestor
7164
+ levers**. The coarse-pointer/no-hover media rule reads those, and floors the RENDERED hit area back
7165
+ at 2.75rem/44px however far the override lowered it, so the control stays comfortably tappable the
7166
+ moment the pointer reaching it is a finger rather than a mouse. It does **not** rescue
7167
+ `--lr-icon-button-size` set directly on the element: that declaration comes from the outer tree and
7168
+ outranks the shadow tree's own `:host` rule, so the media rule never wins and the rendered hit area
7169
+ is exactly the value set -- `--lr-icon-button-size: 1rem` really does render a 16px target under a
7170
+ coarse pointer, which fails WCAG 2.2 SC 2.5.8. Prefer `--lr-icon-button-size-scope` when the intent
7171
+ is a denser row; reach for the element-scoped name only as a deliberate, localized trade-off. `--lr-icon-button-radius` (default
7094
7172
  `--lr-radius`) is not re-declared anywhere in the shared layer, so — like every other token below —
7095
7173
  it inherits normally from an ancestor even through an intervening component; it is the
7096
7174
  `[part='button']` corner radius, retunable without a `::part(button)` rule — the same
@@ -20518,8 +20596,21 @@ until something else registers it, the same trade `icon-button-register.js` docu
20518
20596
  - `disabled: boolean = false` (reflected) — disables every filter control and reset action.
20519
20597
  - `loading: boolean = false` (reflected) — shows the status spinner and disables reset while leaving
20520
20598
  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`.
20599
+ - `hasActiveFilters: boolean` (read-only) — whether any configured filter currently has a value,
20600
+ including one sitting at its own declared `defaultValue`. Drives the reset button's own disabled
20601
+ state in every `activeFiltersDisplay` mode except `'changed'`; the getter itself is unaffected by
20602
+ `activeFiltersDisplay`.
20603
+ - `hasChangedFilters: boolean` (read-only) — whether any filter's value differs from its own
20604
+ declared `defaultValue`, using the same equality `activeFiltersDisplay: 'changed'` filters its
20605
+ chip row on: a `readonly string[]` default compares positionally, everything else compares with
20606
+ `Object.is`. This is the counterpart to `hasActiveFilters`, not a synonym — a bar whose filters
20607
+ were all declared with non-empty defaults and never touched reads `hasActiveFilters === true` and
20608
+ `hasChangedFilters === false`, because a bar whose defaults narrow the view on load has not been
20609
+ narrowed by the user. A filter with no declared `defaultValue` counts as changed the moment it
20610
+ holds any value at all (there is nothing for it to still equal), and clearing a filter that *does*
20611
+ declare one counts as changed too, since `reset()` would restore it — which is the one case where
20612
+ this getter and the `'changed'` chip row differ, the row's entries being non-empty by
20613
+ construction. Always live, never cached.
20523
20614
  - `invalidFilterIds: readonly string[]` (read-only) — immutable ids of required filters whose
20524
20615
  values are unset.
20525
20616
  - `activeFiltersDisplay: 'all' | 'changed' | 'hidden' = 'all'` (reflected, attribute
@@ -20533,6 +20624,10 @@ until something else registers it, the same trade `icon-button-register.js` docu
20533
20624
  at each index), matching this component's only other array-equality precedent (a custom adapter's
20534
20625
  own `clearValue` comparison); a `'date-range'` value is a single composed string, so it compares
20535
20626
  like any other string. Removing a chip always clears that filter, unaffected by this property.
20627
+ `'changed'` additionally gates the reset button on `hasChangedFilters` instead of
20628
+ `hasActiveFilters`, so an untouched defaults-only bar — which renders no chip in this mode — no
20629
+ longer offers an enabled reset that would change nothing. Enablement under `'all'` and `'hidden'`
20630
+ is unchanged, `disabled`/`loading` still win in every mode, and `reset()` itself is untouched.
20536
20631
 
20537
20632
  The composed reset action uses `lr-button`'s default `m` size tier, matching the default rendered
20538
20633
  height of adjacent select, combobox, input, and date fields instead of introducing a shorter action
@@ -20667,8 +20762,8 @@ control at all (see **Chip-only filters** below): its value belongs to a widget
20667
20762
  page, so the bar renders only its active-filter chip and gives it no toolbar cell.
20668
20763
 
20669
20764
  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
20765
+ `icon: unknown` and `labelVisibility: 'visible' | 'hidden' | 'auto'` fields, and every one whose
20766
+ composed control ships a clear action also accepts `clearable: boolean`. They are forwarded verbatim to that
20672
20767
  control's own same-named property — `icon` into its `start` slot exactly like a choice option's own
20673
20768
  `icon`, rendered inert and `aria-hidden`; `clearable` reaching `<lr-date-input>` under its own
20674
20769
  `with-clear` spelling, since that control has no `clearable`. `'text'` also accepts
@@ -20687,6 +20782,20 @@ so a compact toolbar row still names every field for assistive technology; visua
20687
20782
  `::part(filter-control-label)` in CSS, the only previous option, removed the accessible name along
20688
20783
  with the text.
20689
20784
 
20785
+ `labelVisibility: 'auto'` is the width-dependent middle between the two. It renders exactly what
20786
+ `'visible'` renders — the same stacked label element, the same accessible name computed from it, no
20787
+ `aria-label` and no placeholder fallback — and the bar's own stylesheet visually clips that label
20788
+ once the bar's allocation drops below `30rem`. The threshold is a container query on the host, so it
20789
+ reads the bar's own allocated width, not the viewport's: the same definitions render labelled across
20790
+ a dashboard and unlabelled in a 320px side panel, dialog or split pane, with no host-side breakpoint
20791
+ logic. The label element is never removed at any width, so the field's accessible name is identical
20792
+ in both states, and `'auto'` deliberately does not route the name onto the control the way
20793
+ `'hidden'` does — doing so would name a wide-allocation field twice. A `'checkbox-menu'` filter
20794
+ participates through its own trigger label run, the same one `'hidden'` already clips there. The
20795
+ threshold is fixed rather than themeable: a CSS container query's prelude cannot read a custom
20796
+ property (`var()` is not substituted in an at-rule prelude), so a `--lr-*` hook for it would parse
20797
+ and silently never apply.
20798
+
20690
20799
  `'combobox'` also accepts the same `debounce?: number` (ms) `'text'` already had: it coalesces a
20691
20800
  burst of rapid selection changes (picks, a multi-select toggle, an
20692
20801
  `allowCustomValue`/`allowCreate` commit, or the clear action) into one delayed commit. Unlike
@@ -20716,6 +20825,8 @@ makes that text visually hidden — never removed — so the button keeps its ac
20716
20825
  one case where the hidden label would be the *only* thing the button says (hidden routing, no
20717
20826
  declared `placeholder`, nothing selected) the label routes to the visible summary instead of being
20718
20827
  emitted twice, so the trigger's accessible name stays "Teams", never "Teams Teams".
20828
+ `labelVisibility: 'auto'` clips that same trigger label run, and only below the `30rem` threshold —
20829
+ the label run is always emitted under `'auto'`, since nothing is routed to the summary there.
20719
20830
 
20720
20831
  Because its trigger is a button rather than a field, a `required` `'checkbox-menu'` deliberately
20721
20832
  renders **no** required asterisk and sets **no** `aria-invalid`: the shared required marker has no
@@ -27222,6 +27333,21 @@ LyraMapLegendPattern }`, where `LyraMapLegendPattern` is `'solid' | 'diagonal' |
27222
27333
  and their point-icon defaults, and the same validation applies (path data only, at most 8,192
27223
27334
  characters, positive `viewBox` dimensions). An unusable record is dropped and that row keeps
27224
27335
  rendering its colour swatch, exactly as a row that supplies no `icon` does.
27336
+ A row may finally carry a `group`: the section it belongs to, for a key that describes two
27337
+ layers at once and otherwise could not say which rows belong to which. The rule is pinned
27338
+ rather than inferred: **consecutive** entries sharing an identical `group` render as one section
27339
+ — a visible heading plus a `role="group"` that heading names — an entry with **no** `group`
27340
+ keeps its **declared** position rather than being hoisted above or sunk below a section, and a
27341
+ `group` that reappears after an interruption opens a *second* section rather than reordering
27342
+ rows to merge them. Declaration order is the one thing the legend never rewrites, because the
27343
+ order is itself information about the map. `group` is caller-supplied **data**: it renders
27344
+ verbatim and is never resolved through the locale catalogue. It is trimmed and bounded to 256
27345
+ characters (ellipsized, since it is rendered prose rather than a key matched against
27346
+ `point.field`); a non-string, empty or whitespace-only value leaves no `group` property on the
27347
+ frozen row at all, so an empty string means "ungrouped" instead of an empty heading. Like the
27348
+ row-level `value`, it does not count toward the aggregate label budget — the rendered total is
27349
+ already finite and stated: at most one heading per rendered row, so at most 100 of them. A
27350
+ section is not a row: the 100-row cap and the `legend-limit` summary count rows, never sections.
27225
27351
  - readonly `legendProjection: LyraMapLegendProjection` — frozen `{ inputCount, renderedCount,
27226
27352
  omittedCount, truncatedLabelCount, truncated }` result for the latest assignment. A truncated
27227
27353
  projection renders a localized visible `1–N of M items` summary rather than silently claiming
@@ -27234,6 +27360,21 @@ omittedCount, truncatedLabelCount, truncated }` result for the latest assignment
27234
27360
  roving tabindex: a 100-row interactive legend contributes 100 tab stops, exactly as a 100-series
27235
27361
  `lr-chart` legend does. Each interactive row also grows to the shared `--lr-icon-button-size`
27236
27362
  hit-area floor (WCAG 2.5.8), which the panel's existing `max-block-size` and scrolling contain.
27363
+ - `legendCollapsible: boolean = false` (attribute `legend-collapsible`, reflected) — opt-in: renders
27364
+ a `legend-disclosure` `button` inside the panel that collapses the key down to its header, so a
27365
+ large legend stops permanently covering part of the map. Unset, the panel renders exactly what it
27366
+ rendered before — no button, no `id` minted on the row list, and no `hidden` attribute anywhere.
27367
+ Collapsing hides the gradient bar, the rows, the `legend-limit` summary and the trailing `legend`
27368
+ slot; the `legend-start` slot and the disclosure itself stay visible, so a slotted header survives
27369
+ the collapse and the control that restores the key is never what the collapse hides.
27370
+ - `legendOpen: boolean = true` (attribute `legend-open`, reflected) — whether a `legendCollapsible`
27371
+ panel is expanded. It defaults **open**, so adding only `legendCollapsible` never hides an existing
27372
+ key, and it does nothing at all while `legendCollapsible` is unset. Because it is a
27373
+ `true`-defaulting boolean it uses a custom attribute converter, so `legend-open="false"` parses —
27374
+ the bare presence-based boolean form cannot express `false` at all — and the reflection follows the
27375
+ same converter: open (the default) reflects as an **absent** attribute and collapsed reflects as
27376
+ `legend-open="false"`. It is controlled public state and survives a disconnect/reconnect. Assigning
27377
+ it programmatically reconciles the rendered panel and emits nothing.
27237
27378
  - `hiddenCategories: readonly string[] = []` (attribute: false) — the complete controlled set of
27238
27379
  muted category keys, mirroring `lr-chart`'s `hiddenDatasets`. Clone-owned and frozen; non-string,
27239
27380
  empty, whitespace-only and duplicate entries are dropped (first occurrence wins, matching
@@ -27522,8 +27663,8 @@ payload beside the map.
27522
27663
  `LyraMapHeatmapZoomValue`, `LyraMapLineOptions`, `LyraMapPointOptions`, `LyraMapPointRadiusOptions`,
27523
27664
  `LyraMapPointRadiusInterpolation`, `LyraMapPointIcon`, `LyraMapPointIconMode`,
27524
27665
  `LyraMapPointIconLineCap`, `LyraMapPointIconLineJoin`, `LyraMapMarker`, `LyraMapMarkerActivationDetail`,
27525
- `LyraMapMarkerActivationSource`, `LyraMapLegendToggleDetail`, `LyraMapStyleSpecification`, and
27526
- `LyraMapInstance`.
27666
+ `LyraMapMarkerActivationSource`, `LyraMapLegendToggleDetail`, `LyraMapLegendPanelToggleDetail`,
27667
+ `LyraMapStyleSpecification`, and `LyraMapInstance`.
27527
27668
  The former `LegendEntry`, `ChoroplethLayer`, `GeoJsonDataLayer`, and `MapMarker` names are removed
27528
27669
  in v9 rather than retained as aliases.
27529
27670
 
@@ -27549,6 +27690,13 @@ the set and assign its own value instead. There is deliberately no second, confi
27549
27690
  committed state is `hiddenCategories`, which the host already observes, so a paired before/after
27550
27691
  vocabulary would be permanent public surface nobody asked for. The event is a DOM-interaction
27551
27692
  proposal only, so a programmatic `hiddenCategories` assignment reconciles without emitting it.
27693
+ Also `lr-map-legend-panel-toggle` (**cancelable**; frozen `LyraMapLegendPanelToggleDetail { open }` —
27694
+ the proposed `legendOpen` value), fired once when the `legendCollapsible` disclosure is activated by
27695
+ pointer or by Enter/Space. It is the *panel's* disclosure, not a *category's* visibility, so it
27696
+ deliberately does not reuse `lr-map-legend-toggle`. `preventDefault()` is the same genuine veto:
27697
+ `legendOpen` is not written, the rendered rows and the disclosure's `aria-expanded` do not change,
27698
+ so a host can own the open state and assign its own value from `event.detail.open`. A programmatic
27699
+ `legendOpen` assignment reconciles without emitting it, so a controlled host cannot loop.
27552
27700
  Also `lr-map-marker-activate` (non-cancelable; frozen `LyraMapMarkerActivationDetail { id, lngLat,
27553
27701
  marker, source }`; `id` is the trimmed explicit identity or `undefined`, `marker` is the accepted
27554
27702
  declarative snapshot, and `source` is `'pointer' | 'keyboard'`), and `lr-map-click`
@@ -27571,9 +27719,14 @@ markup as described above.
27571
27719
 
27572
27720
  **Slots:** `legend` — custom legend content, rendered inside the legend panel's own layout so it
27573
27721
  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`,
27722
+ when `legend` and `legendGradient` are both empty. `legend-start` — the same extension point at the
27723
+ **top** of the panel: it renders ahead of the gradient bar and every projected row, where `legend`
27724
+ renders after them, so a host-authored panel header is no longer forced to be a footer. Content in
27725
+ it alone opens the panel too, and neither slot is ever made interactive by `legendInteractive`,
27726
+ which only reaches rows projected from `legend`.
27727
+
27728
+ **CSS parts:** `base`, `container`, `legend`, `legend-disclosure`, `legend-disclosure-icon`,
27729
+ `legend-group`, `legend-group-heading`, `legend-swatch`, `legend-toggle`,
27577
27730
  `legend-toggle-hidden`, `legend-gradient`, `legend-lo`,
27578
27731
  `legend-hi`, `legend-limit`, `marker`, `popup`,
27579
27732
  `popup-content`, `popup-close-button`, `attribution`, `attribution-toggle`, `navigation`,
@@ -27611,6 +27764,26 @@ quiet text token, so the label keeps AA contrast rather than fading with the who
27611
27764
  forced colors the hidden row falls back to `line-through`, which survives a system-color collapse.
27612
27765
  Each activation is announced through the shared light-DOM polite live region, using the same
27613
27766
  `legendTypeShown`/`legendTypeHidden` strings `lr-graph-legend` announces with.
27767
+ Under `legendCollapsible`, `legend-disclosure` is a native `button` whose **visible localized text
27768
+ is its accessible name** and whose `aria-expanded` renders the literal `"true"`/`"false"` — never
27769
+ omitted — with `aria-controls` naming the row list in the same shadow root (idrefs do not cross
27770
+ shadow boundaries, so this is deliberately not an idref into the light DOM). It carries the shared
27771
+ `--lr-icon-button-size` hit-area floor, and `legend-disclosure-icon` is the decorative chevron it
27772
+ rotates: the shared icon set ships one right-pointing glyph and asks callers to rotate the wrapping
27773
+ part, so collapsed points along the reading direction and expanded points down in both directions.
27774
+ The rotation runs on `--lr-transition-fast`, which the token layer already flattens under
27775
+ `prefers-reduced-motion`. A collapsed panel hides its gradient, rows, `legend-limit` summary and
27776
+ trailing `legend` slot with the plain `hidden` attribute, so they leave layout, the accessibility
27777
+ tree and the tab order together.
27778
+ When at least one entry carries a `group`, each consecutive run renders inside `legend-group`
27779
+ (`role="group"`, named by its own `legend-group-heading` through `aria-labelledby`) and **each run
27780
+ gets its own `role="list"`**, because a `list` may only own `listitem`s — a `group` sitting directly
27781
+ inside the outer list is an `aria-required-children`/`aria-required-parent` violation. The outer
27782
+ container therefore drops its own `role` in that case; a legend with no groups keeps the single
27783
+ `role="list"` it has always had. Row `aria-posinset`/`aria-setsize` stay whole-key values inside a
27784
+ section: `aria-setsize` already reports the *input* count so a bounded key stays honest, and a
27785
+ dropped row carries no attributable group, so a section adds a labelled sub-region without
27786
+ renumbering the key.
27614
27787
  `legend-limit` is the localized bounded-projection summary. The five peer-chrome parts project
27615
27788
  stable Lyra names onto MapLibre-generated DOM without erasing peer-supplied part tokens;
27616
27789
  `marker` retains a 24px minimum target in both axes even when a peer/custom marker has no intrinsic
@@ -30352,6 +30525,7 @@ These named interfaces and helper signatures are available to typed integrations
30352
30525
  readonly label: string;
30353
30526
  readonly pattern: LyraMapLegendPattern;
30354
30527
  readonly icon?: LyraMapPointIcon | Omit<LyraMapPointIcon, 'value'>;
30528
+ readonly group?: string;
30355
30529
  }`
30356
30530
  Import: `@aceshooting/lyra-ui/components/media/map/map.class.js`.
30357
30531
  `LyraMapLegendProjection {
@@ -30383,6 +30557,10 @@ These named interfaces and helper signatures are available to typed integrations
30383
30557
  readonly hiddenCategories: readonly string[];
30384
30558
  }`
30385
30559
  Import: `@aceshooting/lyra-ui/components/media/map/map.class.js`.
30560
+ `LyraMapLegendPanelToggleDetail {
30561
+ readonly open: boolean;
30562
+ }`
30563
+ Import: `@aceshooting/lyra-ui/components/media/map/map.class.js`.
30386
30564
  `LyraMapStyleSpecification {
30387
30565
  readonly version: 8;
30388
30566
  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.1.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",