@aceshooting/lyra-ui 17.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 (107) hide show
  1. package/CHANGELOG.md +483 -0
  2. package/README.md +2 -2
  3. package/custom-elements.json +1 -1
  4. package/design-tokens.json +1 -1
  5. package/dist/cli/migration-contract.json +1 -1
  6. package/dist/components/conversation/message-actions/message-actions.class.d.ts +2 -1
  7. package/dist/components/conversation/streaming-text/streaming-text-core.class.d.ts +22 -0
  8. package/dist/components/conversation/streaming-text/streaming-text-core.class.js +1 -0
  9. package/dist/components/conversation/streaming-text/streaming-text.class.d.ts +22 -0
  10. package/dist/components/conversation/streaming-text/streaming-text.class.js +1 -0
  11. package/dist/components/conversation/thread-list/thread-list.class.js +1 -1
  12. package/dist/components/data/table/table.styles.js +1 -1
  13. package/dist/components/forms/combobox/combobox.class.d.ts +12 -2
  14. package/dist/components/forms/combobox/combobox.class.js +2 -2
  15. package/dist/components/forms/combobox/combobox.styles.js +1 -1
  16. package/dist/components/forms/icon-button/icon-button.class.d.ts +18 -9
  17. package/dist/components/forms/select/select.class.d.ts +63 -8
  18. package/dist/components/forms/select/select.class.js +4 -4
  19. package/dist/components/forms/select/select.styles.js +1 -1
  20. package/dist/components/layout/filter-bar/filter-bar.class.d.ts +148 -14
  21. package/dist/components/layout/filter-bar/filter-bar.class.js +12 -4
  22. package/dist/components/layout/filter-bar/filter-bar.styles.js +1 -1
  23. package/dist/components/layout/reorder-list/reorder-list.class.js +1 -1
  24. package/dist/components/layout/virtual-list/virtual-list.class.d.ts +154 -18
  25. package/dist/components/layout/virtual-list/virtual-list.class.js +12 -7
  26. package/dist/components/media/map/map.class.d.ts +269 -14
  27. package/dist/components/media/map/map.class.js +62 -30
  28. package/dist/components/media/map/map.styles.js +1 -1
  29. package/dist/components/utility/copy-button/copy-button.class.d.ts +4 -3
  30. package/dist/custom-elements-jsx.d.ts +1 -1
  31. package/dist/events.d.ts +13 -1
  32. package/dist/internal/package-metadata.d.ts +1 -1
  33. package/dist/internal/package-metadata.js +1 -1
  34. package/dist/internal/tokens.styles.js +1 -1
  35. package/dist/lyra.d.ts +1 -1
  36. package/dist/lyra.js +1 -1
  37. package/dist/styles/tokens-root.css +1 -1
  38. package/dist/svelte.d.ts +1 -1
  39. package/dist/testing/lyra-tag-event-map.js +1 -1
  40. package/dist/translations/ar/retrieval.js +1 -1
  41. package/dist/translations/ar/shared.js +1 -1
  42. package/dist/translations/de/retrieval.js +1 -1
  43. package/dist/translations/de/shared.js +1 -1
  44. package/dist/translations/es/retrieval.js +1 -1
  45. package/dist/translations/es/shared.js +1 -1
  46. package/dist/translations/fa/retrieval.js +1 -1
  47. package/dist/translations/fa/shared.js +1 -1
  48. package/dist/translations/fr/retrieval.js +1 -1
  49. package/dist/translations/fr/shared.js +1 -1
  50. package/dist/translations/he/retrieval.js +1 -1
  51. package/dist/translations/he/shared.js +1 -1
  52. package/dist/translations/it/retrieval.js +1 -1
  53. package/dist/translations/it/shared.js +1 -1
  54. package/dist/translations/ja/retrieval.js +1 -1
  55. package/dist/translations/ja/shared.js +1 -1
  56. package/dist/translations/pt-BR/retrieval.js +1 -1
  57. package/dist/translations/pt-BR/shared.js +1 -1
  58. package/dist/translations/ro/agent-tools.d.ts +1 -0
  59. package/dist/translations/ro/agent-tools.js +1 -0
  60. package/dist/translations/ro/charts.d.ts +1 -0
  61. package/dist/translations/ro/charts.js +1 -0
  62. package/dist/translations/ro/conversation.d.ts +1 -0
  63. package/dist/translations/ro/conversation.js +1 -0
  64. package/dist/translations/ro/data.d.ts +1 -0
  65. package/dist/translations/ro/data.js +1 -0
  66. package/dist/translations/ro/forms.d.ts +1 -0
  67. package/dist/translations/ro/forms.js +1 -0
  68. package/dist/translations/ro/layout.d.ts +1 -0
  69. package/dist/translations/ro/layout.js +1 -0
  70. package/dist/translations/ro/media.d.ts +1 -0
  71. package/dist/translations/ro/media.js +1 -0
  72. package/dist/translations/ro/overlays.d.ts +1 -0
  73. package/dist/translations/ro/overlays.js +1 -0
  74. package/dist/translations/ro/retrieval.d.ts +1 -0
  75. package/dist/translations/ro/retrieval.js +1 -0
  76. package/dist/translations/ro/shared.d.ts +1 -0
  77. package/dist/translations/ro/shared.js +1 -0
  78. package/dist/translations/ro/utility.d.ts +1 -0
  79. package/dist/translations/ro/utility.js +1 -0
  80. package/dist/translations/ro/viewers.d.ts +1 -0
  81. package/dist/translations/ro/viewers.js +5 -0
  82. package/dist/translations/ro.d.ts +1 -0
  83. package/dist/translations/ro.js +1 -0
  84. package/dist/translations/ru/retrieval.js +1 -1
  85. package/dist/translations/ru/shared.js +1 -1
  86. package/dist/translations/zh-CN/retrieval.js +1 -1
  87. package/dist/translations/zh-CN/shared.js +1 -1
  88. package/dist/vue.d.ts +1 -1
  89. package/llms/components/lr-button.md +5 -4
  90. package/llms/components/lr-card.md +2 -1
  91. package/llms/components/lr-combobox.md +11 -0
  92. package/llms/components/lr-filter-bar.md +123 -7
  93. package/llms/components/lr-icon-button.md +17 -8
  94. package/llms/components/lr-map.md +127 -8
  95. package/llms/components/lr-option.md +11 -0
  96. package/llms/components/lr-select.md +45 -3
  97. package/llms/components/lr-streaming-text-core.md +7 -3
  98. package/llms/components/lr-streaming-text.md +7 -2
  99. package/llms/components/lr-virtual-list.md +53 -0
  100. package/llms/shared.md +47 -3
  101. package/llms/tokens.md +7 -4
  102. package/llms-full.txt +465 -36
  103. package/package.json +28 -2
  104. package/registrations.json +1 -1
  105. package/vscode-css-data.json +1 -1
  106. package/vscode-html-data.json +1 -1
  107. package/web-types.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,488 @@
1
1
  # Changelog
2
2
 
3
+ ## 18.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6137062: `<lr-filter-bar>`: under `activeFiltersDisplay="changed"`, the reset button now keys its enablement
8
+ on `hasChangedFilters` instead of `hasActiveFilters`.
9
+
10
+ `'changed'` exists to say that a filter sitting at its own declared `defaultValue` is not something
11
+ the user applied, and it already suppressed that filter's chip. The reset button next to the empty
12
+ chip row stayed enabled anyway, so a defaults-only bar rendered "nothing is applied" and "press here
13
+ to clear what's applied" side by side, and pressing it was a no-op that still emitted `lr-input` and
14
+ `lr-reset`.
15
+
16
+ Nothing else changes: enablement under `activeFiltersDisplay="all"` (the default) and `"hidden"` is
17
+ byte for byte what it was, `disabled`/`loading` still win in every mode, `hasActiveFilters` keeps its
18
+ meaning, and `reset()` itself is untouched — it still restores every declared `defaultValue`.
19
+ - 6137062: `<lr-filter-bar>`: add a public read-only `hasChangedFilters`.
20
+
21
+ `hasActiveFilters` answers "does any filter hold a value", which is deliberately `true` for a bar
22
+ whose filters were declared with their own non-empty `defaultValue` and have never been touched —
23
+ so a host that wanted to show "3 filters applied", enable an "Apply"/"Save search" action, or badge
24
+ a collapsed filter panel had to re-derive the default comparison itself from `filters` and `value`.
25
+ `hasChangedFilters` exposes it directly, using the exact equality `activeFiltersDisplay: 'changed'`
26
+ already filters its chip row on: a `readonly string[]` default compares positionally, everything
27
+ else compares with `Object.is`, and a filter with no declared `defaultValue` counts as changed the
28
+ moment it holds any value at all. Clearing a filter that *does* declare one also counts as changed,
29
+ since `reset()` would restore it — which is the one case where it differs from the `'changed'` chip
30
+ row, whose entries are non-empty values by construction.
31
+
32
+ `hasActiveFilters` is unchanged, and so is every existing behaviour keyed on it.
33
+ - 6137062: `<lr-filter-bar>`: a filter definition's `labelVisibility` accepts a third value, `'auto'`.
34
+
35
+ `'visible'` and `'hidden'` were an all-or-nothing choice made once, at definition time, for a
36
+ component that is as likely to sit in a 320px side panel as across a full page — so a bar authored
37
+ for a dashboard lost a whole row of vertical space in a drawer, and one authored for the drawer
38
+ shipped unlabelled fields to the dashboard. `'auto'` renders exactly what `'visible'` does (same
39
+ stacked label element, same accessible name computed from it, no `aria-label` and no placeholder
40
+ fallback) and visually clips that label once the bar's **own** allocation — a container query on the
41
+ host, not the viewport — drops below `30rem`. The label element stays in the DOM at every width, so
42
+ the field's accessible name is identical in both states; the previous workaround, visually hiding
43
+ `::part(filter-control-label)` from a consumer stylesheet, removed the name along with the text.
44
+
45
+ `'checkbox-menu'` participates through its own trigger label run, the same one `'hidden'` already
46
+ clips there.
47
+
48
+ `'visible'`, `'hidden'` and an unset `labelVisibility` are unchanged at every allocation. The host
49
+ does become an inline-size query container (with the library's standard
50
+ `contain-intrinsic-inline-size` fallback), which is a no-op for a bar in an ordinary block or flex
51
+ allocation.
52
+ - 3f4be41: `<lr-map>`: add `legendCollapsible`/`legendOpen` and the cancelable `lr-map-legend-panel-toggle`.
53
+
54
+ The legend panel could not be collapsed at all, so a large key permanently covered part of the map.
55
+ `legend-collapsible` renders a native disclosure button inside the panel whose visible localized
56
+ text is its accessible name, whose `aria-expanded` renders the literal `"true"`/`"false"`, and whose
57
+ `aria-controls` names the row list in the same shadow root. `legendOpen` defaults **open**, so adding
58
+ only `legendCollapsible` never hides an existing key; it is a `true`-defaulting boolean, so
59
+ `legend-open="false"` parses and the open default reflects as an absent attribute.
60
+
61
+ Activation emits the cancelable `lr-map-legend-panel-toggle` (`detail: { open }`). `preventDefault()`
62
+ is a real veto — nothing is written and nothing re-renders — so a host can own the open state, and a
63
+ programmatic `legendOpen` assignment reconciles without emitting, so a controlled host cannot loop.
64
+ Collapsing hides the gradient, the rows, the `legend-limit` summary and the trailing `legend` slot;
65
+ the `legend-start` slot and the disclosure stay visible. New `legend-disclosure` and
66
+ `legend-disclosure-icon` parts. With `legendCollapsible` unset the rendered legend is unchanged.
67
+ - 3f4be41: `<lr-map>`: add an optional `group` to `LyraMapLegendEntry`, splitting the legend into sections.
68
+
69
+ `legend` was one flat array rendered as a single list, so a key covering two layers could not say
70
+ which rows belonged to which. Consecutive entries sharing an identical `group` now render as one
71
+ section — a visible `legend-group-heading` plus a `role="group"` that heading names through
72
+ `aria-labelledby`. The grouping rule is pinned rather than inferred: an entry with no `group` keeps
73
+ its **declared** position (never hoisted or sunk), and a `group` that reappears after an interruption
74
+ opens a second section rather than reordering rows to merge them.
75
+
76
+ `group` is caller-supplied data, so it renders verbatim and is never passed through the locale
77
+ catalog; it is trimmed, bounded to 256 characters, and a non-string, empty or whitespace-only value
78
+ means "ungrouped" rather than an empty heading. Sections are not rows: the 100-row cap and the
79
+ `legend-limit` summary still count rows. A grouped legend gives each run its own `role="list"`
80
+ (a `list` may only own `listitem`s), while a legend with no groups renders exactly as before. New
81
+ `legend-group` and `legend-group-heading` parts.
82
+ - 3f4be41: `<lr-map>`: add a `legend-start` slot that renders at the top of the legend panel.
83
+
84
+ The panel's only host extension point was `slot="legend"`, which renders after the gradient bar and
85
+ every projected row — so a host-authored panel header (a title, a source note, a control) could only
86
+ ever be a footer. `legend-start` renders ahead of both. Content in it alone opens the panel, exactly
87
+ as `legend` content alone already did, and it is never made interactive by `legendInteractive`. The
88
+ existing `legend` slot keeps its position, so an unset map's legend markup is unchanged.
89
+ - 42af229: `<lr-select>`: `loading` now covers the whole pending state, not only its committed-value half.
90
+ It already rewrote an unresolved committed value's label to the localized `loading` text and
91
+ suppressed the "not in catalog" badge; with nothing selected at all -- a create form whose option
92
+ catalogue is still being fetched, or an edit form whose saved selection is legitimately empty --
93
+ `labelFor()` was never reached and the trigger fell through to the consumer's own `placeholder`.
94
+ Covering that half meant hand-writing a conditional placeholder bound to the same flag and
95
+ re-localizing, in the consuming app's own catalogue, the exact string this control already owns.
96
+
97
+ The trigger now renders the same localized text in place of `placeholder` while `loading` is
98
+ `true` and the selection is empty -- the existing `loading` message key, so both halves always read
99
+ the same words and a `registerLyraLocale()` translation reaches them both. With `loading` false an
100
+ empty selection renders the consumer's `placeholder` exactly as before, `value` is never touched,
101
+ and the trigger's accessible name is unchanged: a host `aria-label` still wins, then `label`, then
102
+ `placeholder`, then the localized `select` fallback.
103
+ - 42af229: `<lr-select>`: new `sync` property, closing the shared anchored-surface sizing vocabulary
104
+ `<lr-popup>`, `<lr-popover>`, `<lr-dropdown>` and `<lr-combobox>` already spell. It takes the same
105
+ `'width' | 'height' | 'both'` type (`PlaceSync`), the same unset default, and is wired through the
106
+ same `place()` option, so `sync="width"` copies the rendered trigger width onto the listbox: a
107
+ full-width select with short option labels no longer opens a listbox visibly narrower than, and
108
+ floating centred under, its own trigger. Changes reposition an already-open listbox without closing
109
+ it, and unsetting it releases the inline width the positioner wrote.
110
+
111
+ With `sync` unset the listbox renders exactly as before -- `inline-size: max-content` between
112
+ `--lr-size-12rem` and `min(--lr-popover-viewport-clamp, --lr-size-28rem)`. A synced listbox is
113
+ capped on `--lr-positioner-available-inline-size` alone, adopting the corrected clamp rather than
114
+ the viewport-clamp shortfall it would otherwise have inherited, so it matches its trigger at any
115
+ width while the measured available space still keeps it on screen.
116
+ - b22fdbb: Design tokens: add `--lr-icon-button-size-scope`, an ancestor-scoped input for the icon-only
117
+ control size.
118
+
119
+ `--lr-icon-button-size` is element-scoped and has to stay that way: the shared token layer
120
+ re-declares it on every `lr-*` host so the coarse-pointer touch-target floor can apply per element.
121
+ The side effect is that a wrapper setting `--lr-icon-button-size` is replaced at the first
122
+ component in between, so it never reaches an icon button composed inside another component — a
123
+ `<lr-icon-button>` slotted through `<lr-popover>`, or the copy affordance inside `<lr-code-block>`.
124
+ The only ancestor lever was `--lr-theme-icon-button-size`, which is application-wide by design, so
125
+ "make just this toolbar denser" meant reaching for the global theme input and scoping it by hand.
126
+
127
+ `--lr-icon-button-size-scope` is declared nowhere, so it inherits the whole way down and reaches
128
+ every icon-only control below the wrapper that sets it:
129
+
130
+ ```css
131
+ .message-toolbar {
132
+ --lr-icon-button-size-scope: 1.75rem;
133
+ }
134
+ ```
135
+
136
+ Precedence is `--lr-theme-icon-button-size` → `--lr-icon-button-size-scope` → the `2.5rem` default,
137
+ and the coarse-pointer floor still raises a resolved value below `2.75rem` back to it, so the new
138
+ knob is not a route around WCAG 2.2 SC 2.5.8. Purely additive: `--lr-icon-button-size` keeps its
139
+ current element-scoped behaviour exactly, and the resolved default is unchanged.
140
+
141
+ ### Patch Changes
142
+
143
+ - 6137062: Docs: `<lr-filter-bar>`'s consumer reference now covers `hasChangedFilters` (and how it differs from
144
+ `hasActiveFilters` on a defaults-only bar), the `activeFiltersDisplay="changed"` reset-enablement
145
+ coupling, and `labelVisibility: 'auto'` with its `30rem` container-query threshold.
146
+ - 3f4be41: Document `<lr-map>`'s collapsible legend, `legend-start` slot and legend sections in the authored
147
+ `llms/media.md` reference: `legendCollapsible`/`legendOpen` (including the `true` default and the
148
+ `legend-open="false"` parse), the cancelable `lr-map-legend-panel-toggle`, the `group` field and its
149
+ pinned ordering rule, and the four new parts.
150
+ - 42af229: `<lr-combobox>`: a `sync="width"`/`sync="both"` listbox is no longer shortened by
151
+ `--lr-popover-viewport-clamp`. The synced rule capped the listbox at
152
+ `min(var(--lr-popover-viewport-clamp), var(--lr-positioner-available-inline-size, 100vw))`, and the
153
+ clamp's 92vw default resolves below the width of exactly the full-width trigger `sync` exists to
154
+ serve -- a 738px trigger in an 800px viewport got a 736px listbox, and the shortfall grows with the
155
+ trigger. The synced rule now caps on `var(--lr-positioner-available-inline-size, 100vw)` alone,
156
+ matching `<lr-popup>`: a width-synced listbox is anchored to an element already on screen and
157
+ measured against the space actually available beside it, so the extra viewport ceiling had nothing
158
+ left to protect against, while the available-space term still keeps an over-wide anchor from
159
+ pushing the listbox off-screen. An unsynced listbox is unchanged and keeps
160
+ `min(--lr-popover-viewport-clamp, --lr-size-28rem)`.
161
+ - 42af229: Docs: `llms/forms.md` now documents `<lr-select>`'s new `sync` property (same vocabulary note as
162
+ `<lr-combobox>`'s, stating its unset default), records that a width-synced listbox on **both**
163
+ controls is capped on `--lr-positioner-available-inline-size` alone rather than also by
164
+ `--lr-popover-viewport-clamp`, and describes `loading`'s coverage of an empty selection alongside
165
+ the `placeholder` entry it now takes precedence over.
166
+ - b22fdbb: Design tokens: derive the dark-mode overlay surface from the page surface instead of pinning it.
167
+
168
+ `--lr-color-surface-overlay` is the panel colour behind every floating surface — dropdowns,
169
+ listboxes, menus, toasts, popovers, dialogs, and the `lr-app-rail` mobile drawer. In light mode it
170
+ already resolves straight to `--lr-color-surface`, so one `--lr-theme-color-surface-default`
171
+ override carries all of them with it. Dark mode pinned a literal instead, because panel and page
172
+ resolving to the same near-black makes an open dialog read as a scrim with text floating on it and
173
+ no panel at all. The cost was that re-skinning the dark base surface left every floating surface at
174
+ the stock colour — a mismatched panel rather than a themed one.
175
+
176
+ It is now derived, keeping the elevation delta the literal existed to provide:
177
+
178
+ ```css
179
+ --lr-color-surface-overlay: var(
180
+ --lr-theme-color-surface-overlay,
181
+ color-mix(in srgb, var(--lr-color-surface) 85%, #8bade2)
182
+ );
183
+ ```
184
+
185
+ At the built-in dark base the pair resolves to the same panel colour it always has, so no existing
186
+ dark theme moves. Light mode is untouched, and an explicit `--lr-theme-color-surface-overlay` still
187
+ wins outright.
188
+ - b22fdbb: Docs: state the icon-button size scopes and the derived dark overlay surface in `llms/shared.md`.
189
+
190
+ Three properties now resize icon-only controls and they differ only in how far they reach, which is
191
+ not something a consumer can infer from the names: `--lr-theme-icon-button-size` is
192
+ application-wide, `--lr-icon-button-size-scope` covers one subtree, and `--lr-icon-button-size` is
193
+ element-scoped because the shared token layer re-declares it on every host. The reference now says
194
+ so in one table, with the precedence order and the coarse-pointer floor spelled out, instead of
195
+ leaving "my wrapper rule does nothing" to be rediscovered per project.
196
+
197
+ The overlay-surface entry records that `--lr-color-surface-overlay` follows
198
+ `--lr-theme-color-surface-default` in dark mode as well as light, so a single base-surface override
199
+ re-skins every dropdown, listbox, menu, toast and drawer, and explains why dark derives the value
200
+ rather than resolving straight to the page surface the way light does.
201
+ - 90449c6: `<lr-virtual-list>`: `row-height="auto"` driven by an external `scrollElement` no longer raises the
202
+ uncaught window error "ResizeObserver loop completed with undelivered notifications." when the
203
+ scroller jumps to its end and the newly revealed window measures. Nothing was ever dropped -- the
204
+ browser re-delivers on the following frame -- but the notice arrives as an uncaught `ErrorEvent` on
205
+ `window`, so it landed on whatever was running at the time and failed consumers' clean-console e2e
206
+ gates with a message that named no component.
207
+
208
+ The offending write is `[part="spacer"]`'s height, the list's whole virtual extent. `onRowsResized()`
209
+ folded each newly measured row height into the offsets and asked for a render; Lit flushes that
210
+ render on the microtask checkpoint that follows the observer callback, which is still inside the
211
+ browser's resize-observation delivery. Under an external `scrollElement` that write resizes an
212
+ *observed* box, because `[part="base"][data-external-scroll]` takes its own block size from the
213
+ spacer and is watched by the container `ResizeObserver` -- and `[part="base"]` sits shallower in the
214
+ tree than the rows just broadcast, so the browser records the resize as a skipped observation and
215
+ ends the loop with the error. Reproduced deterministically in WebKit and, with a large enough first
216
+ measurement delta, in Chromium too.
217
+
218
+ Reading inside the delivery is fine; folding the result into the offsets is what resizes the box.
219
+ So the measurement callbacks now only stash what they observed, and the offsets rebuild -- with the
220
+ scroll-anchor correction that belongs to it, so neither is ever painted without the other -- runs on
221
+ the animation frame this component already used to defer `observe()` calls out of the same delivery.
222
+ A render that still lands mid-delivery then re-reads offsets nothing has changed and writes the
223
+ extent already in the DOM, which resizes nothing. Renders themselves are never held: this element's
224
+ `updateComplete` keeps settling in the same microtask run as before, which is what every component
225
+ composing it relies on when it reads rendered rows back after its own update. Only the
226
+ external-`scrollElement` case defers at all -- with the list's own viewport scrolling,
227
+ `[part="base"]` takes its block size from `--lr-virtual-list-height` rather than from the spacer, so
228
+ the extent write reaches no observed box and every measurement there stays as immediate as it was,
229
+ fixed numeric `row-height` included. The error message itself is neither suppressed nor filtered
230
+ anywhere.
231
+
232
+ ## 18.0.0
233
+
234
+ ### Major Changes
235
+
236
+ - 555b974: **Major only because several exported TypeScript unions and maps gained members. No runtime
237
+ behaviour changes, no attribute or event was removed or renamed, and no default moved.** If you do
238
+ not perform exhaustive type-level matching over the members listed below, this release is a
239
+ drop-in upgrade and you can stop reading here.
240
+
241
+ The public-API gate classifies widening an exported union as breaking, and it is right to: a
242
+ consumer with an exhaustive `switch` over one of these, or a mapped type keyed by one, stops
243
+ compiling until the new member is handled. Nothing else about them changed.
244
+
245
+ **`lr-filter-bar`**
246
+ - `LyraFilterBarControlType` gains `'chip'`.
247
+ - `LyraFilterBarFilterDefinition` gains `LyraFilterBarChipDefinition`.
248
+ - `LyraFilterBarDefinitionValue<D>` gains an arm resolving a chip filter to the full
249
+ `LyraFilterBarFieldValue`, exactly as `'custom'` already does. Every other `D` resolves as before.
250
+
251
+ An exhaustive `switch (definition.type)` over a filter schema now needs a `'chip'` case, and a
252
+ `Record<LyraFilterBarControlType, T>` needs a `chip` key.
253
+
254
+ **`lr-map`**
255
+ - `LyraMapLegendEntry` gains an optional `value` category key.
256
+ - `LyraMapEventMap` gains `'lr-map-legend-toggle'`.
257
+ - New exported `LyraMapLegendToggleDetail`.
258
+
259
+ A `Record<keyof LyraMapEventMap, T>` needs the new key. `LyraMapLegendEntry` is only widened by an
260
+ optional property, so constructing one is unaffected; only code that enumerates its keys is.
261
+
262
+ **`lr-virtual-list`**
263
+ - New `rowProjection` property and `row-projection` attribute, new `projectedRows` getter, new
264
+ exported `LyraVirtualListRowProjection`, and two exported constants,
265
+ `VIRTUAL_LIST_ROW_ATTRIBUTE` and `VIRTUAL_LIST_STICKY_ATTRIBUTE`.
266
+
267
+ **Generated framework declarations.** `./custom-elements-jsx`, `./vue` and `./svelte` are
268
+ regenerated from the manifest, so the prop types for the components above widen accordingly. This
269
+ is the bulk of the gate's reported diff and needs no action.
270
+
271
+ **Migration:** add the new members to any exhaustive union handling; otherwise upgrade directly.
272
+
273
+ ### Minor Changes
274
+
275
+ - 49eb3ef: `<lr-combobox>` gains `sync` (unset by default), spelled and shaped identically to
276
+ `<lr-dropdown>`/`<lr-popup>`/`<lr-popover>`'s property of the same name. Setting `sync="width"`
277
+ copies the trigger's rendered width onto the listbox and drops the previously fixed 12rem-28rem
278
+ content-based clamp (keeping only the outer viewport/available-space ceiling), so a full-width
279
+ trigger with short option labels gets a listbox that aligns to its own edges instead of floating
280
+ narrower in the middle. Left unset, the listbox continues to size to its own content exactly as
281
+ before.
282
+ - 204aeb4: **`<lr-filter-bar>`: a control-less `type: 'chip'` filter, for a value owned by a widget elsewhere
283
+ on the page.**
284
+
285
+ A calendar heatmap cell, a map selection, a chart brush — the filter is real, but the control that
286
+ sets it is not in the toolbar. Until now the only way to surface such a filter in the bar was a
287
+ `type: 'custom'` definition whose renderer drew something inert just to occupy the cell it was
288
+ forced to claim.
289
+
290
+ `type: 'chip'` renders **no control and no toolbar cell**: no `field` wrapper is emitted for it, so
291
+ `lr-filter-bar::part(field)` and `::part(field-<filterId>)` never match one, and a bar whose filters
292
+ are *all* chip-only paints no empty column — its `controls` row still renders the reset button (the
293
+ "clear all" action such a bar needs), the `end` slot and the loading spinner, exactly like a bar
294
+ declaring no filters at all.
295
+
296
+ Everything else is unchanged from any other filter type. The value lives in `value` under its own
297
+ filter ID, rides every `lr-input`/`lr-reset` detail, counts toward `hasActiveFilters` (so it enables
298
+ the reset button) and toward `invalidFilterIds` when `required`, renders a removable active-filter
299
+ chip subject to `activeFiltersDisplay`, and is cleared both by removing that chip and by `reset()`.
300
+ A `required` chip-only filter is honoured in bookkeeping only — it joins `invalidFilterIds`, fails
301
+ `checkValidity()` and moves `lr-validity-change` — but renders no inline error, because the bar
302
+ renders no element of its own for it; the owning widget keeps its own error affordance. The
303
+ inherited `placeholder` is inert here for the same reason it already is for `type: 'custom'`.
304
+
305
+ **Chip text.** An optional `formatValue(value, locale)` produces it, and `locale` is the bar's
306
+ `effectiveLocale` — the same locale every built-in type's own chip formatting and a custom adapter's
307
+ `formatValue` already receive, so the caller localizes its own data. Omitted, the fallback ladder is
308
+ the one a custom adapter's omitted `formatValue` uses: a string array renders as a localized
309
+ conjunction list, anything else renders verbatim through `String(value)`, and an unset value renders
310
+ empty. Verbatim is exact — a chip-only value never passes through the date branch that localizes a
311
+ `'date'`/`'date-range'` chip, so an ISO day is not silently reformatted and a value containing a
312
+ slash is not mangled.
313
+
314
+ **Clearing.** An optional `clearValue` (default `''`, what every non-multi built-in type writes) is
315
+ what a chip removal writes; declare `[]` for an array-valued chip-only filter. An optional `isEmpty`
316
+ overrides the built-in emptiness rule. A domain sentinel must pair the two: a sentinel clear value
317
+ with no matching `isEmpty` leaves the bar reading the "cleared" value as still set and still
318
+ rendering a chip for it — the identical pairing a custom adapter's own `clearValue`/`isEmpty`
319
+ already documents. With the pair declared, the sentinel is never stored in `value` and an absent key
320
+ reads back as the sentinel for the owning widget.
321
+
322
+ **Additive only.** A schema declaring no `'chip'` filter renders byte-identical shadow DOM: the
323
+ render path keys on `type === 'chip'` exactly, never on "not a known control type", so an
324
+ unrecognized `type` still falls back to `<lr-select>` as before. At the type level, a `'chip'` filter
325
+ keeps the full unconstrained field value in `LyraFilterBarValueFor<Defs>`, like `'custom'`. New
326
+ exported type: `LyraFilterBarChipDefinition`.
327
+ - af5a12e: `<lr-map>` gains an opt-in interactive legend, so a categorical point layer's key can double as its
328
+ layer switch instead of needing a hand-built checkbox group positioned over the canvas.
329
+
330
+ `legendInteractive` (attribute `legend-interactive`, default `false`) turns every legend row that
331
+ carries the new `LyraMapLegendEntry.value` category key into a keyboard-operable toggle button.
332
+ `value` is the same string a `point.colors` / `point.icons` record matches against
333
+ `point.field` / `point.iconField`, so one set of category records can feed both the paint and the
334
+ key and the two cannot drift. It is distinct from — and does not derive from — the `value` on a
335
+ row's `icon` record, which is still dropped from the canonical readback as before. Rows without a
336
+ `value` stay inert even when `legendInteractive` is set.
337
+
338
+ `hiddenCategories: readonly string[]` is the controlled state, mirroring `<lr-chart>`'s
339
+ `hiddenDatasets`, and is honoured on first render rather than only after a user toggle. Activating a
340
+ toggle emits a cancelable `lr-map-legend-toggle` carrying the activated key, its proposed
341
+ visibility, and the complete proposed hidden set; `preventDefault()` is a real veto — nothing is
342
+ written, the row's `aria-pressed` does not change, the map is not repainted, and no announcement is
343
+ made — so a host can own the state itself. A programmatic `hiddenCategories` assignment reconciles
344
+ silently and emits nothing.
345
+
346
+ A hidden category's points, point icons and stroke are muted in the rendered MapLibre layers
347
+ through the new `--lr-map-hidden-category-opacity` (default `0.15`), resolved live from the cascade
348
+ because MapLibre paints to a WebGL canvas that never sees `var()`. The legend row itself dims only
349
+ its decorative swatch (`--lr-map-legend-hidden-swatch-opacity`, default `0.5`) and re-colors its
350
+ label through the quiet-text token, so the label keeps WCAG AA contrast rather than fading with the
351
+ swatch. New parts: `legend-toggle` and `legend-toggle-hidden`.
352
+
353
+ Each toggle is an ordinary tabbable `<button>` with `aria-pressed` rendering both `"true"` and
354
+ `"false"`, matching `<lr-chart>` and `<lr-graph-legend>` rather than introducing a third legend
355
+ vocabulary; a long interactive legend therefore contributes one tab stop per keyed row. Show/hide
356
+ state changes are announced through the shared light-DOM live region.
357
+
358
+ With `legendInteractive` left unset, the legend DOM and every MapLibre paint property are
359
+ byte-identical to before: no button is rendered and `circle-opacity` is never written at all.
360
+ - 3024e74: Added a complete Romanian (`ro`) translation catalog, the twelfth full locale to ship with the
361
+ package. Like every other locale, it is split into twelve side-effect-only per-family slices
362
+ (`@aceshooting/lyra-ui/translations/ro/<family>.js`) plus the back-compat aggregate
363
+ `@aceshooting/lyra-ui/translations/ro.js`, covering all 1,291 keys in `LYRA_DEFAULT_STRINGS`
364
+ (including the 14 pluralized entries). Romanian's CLDR plural category set is `one`/`few`/`other`
365
+ rather than English's `one`/`other` — `few` covers `0` and `2`-`19` (and the `101`-`119`-per-hundred
366
+ band), while `other` is the `de`-requiring form from `20` upward (`"20 de rezultate"` vs.
367
+ `"2 rezultate"`) — so every pluralized message was authored with all three categories rather than
368
+ widening `few` to `other`.
369
+ - 9db9165: `<lr-select>` gains `loading` (default `false`, reflected). A committed value whose catalog hasn't
370
+ arrived yet -- `<lr-option>`s still being fetched/mounted asynchronously -- previously had no way to
371
+ distinguish itself from a genuinely stale value: both rendered the raw value string badged
372
+ dashed/italic `notInCatalog`. Setting `loading` while the matching option is still pending instead
373
+ renders the localized `loading` placeholder, with no `unknown-value` badge and no synthetic
374
+ `showUnknownOption` listbox row, in the trigger label or the relevant `multiple`-mode tag. Once the
375
+ matching `<lr-option>` mounts, the real label renders automatically on the next render, with or
376
+ without also flipping `loading` back to `false`. A value that already matches a live option, and
377
+ every existing `loading`-unset unknown-value/`getUnknownLabel` behavior, is unchanged.
378
+ - 1942512: `<lr-streaming-text>`/`<lr-streaming-text-core>` now forward their composed
379
+ `<lr-markdown>`/`<lr-markdown-core>`'s documented CSS parts (`content`, `heading`, `paragraph`,
380
+ `list`, `code-block`, `inline-code`, `link`, `table`, `blockquote`, `img`, `math`) through
381
+ `exportparts`, reusing each name verbatim since none collides with either wrapper's own `base`/
382
+ `cursor` parts. A host-level `lr-streaming-text::part(link)`/`::part(img)` (or the `-core` variant's
383
+ equivalent) rule now reaches the rendered `<a>`/`<img>` the same way it already does applied
384
+ directly to `<lr-markdown>`/`<lr-markdown-core>` -- previously that styling surface was unreachable
385
+ from outside either wrapper's own shadow boundary.
386
+ - c78f78c: `<lr-virtual-list>` gains `row-projection`, an opt-in light-DOM row projection mode, so an
387
+ application whose list rows are already styled by its own global stylesheet can adopt virtualization
388
+ without rehoming every row rule.
389
+
390
+ Until now `renderItem`'s output was stamped inside the component's shadow root, so document CSS
391
+ could not reach row content: adopting virtualization meant moving a dozen descendant rules per row
392
+ into a new custom element or a growing set of custom properties — a stylesheet refactor, paid
393
+ exactly by the lists long enough to need virtualizing. With `row-projection="light"` the windowed
394
+ rows are rendered into the host's own light DOM and assigned into the shadow viewport through
395
+ internal named slots. The component keeps owning windowing, measurement, spacer sizing,
396
+ `scrollToIndex()`, the external-scroller mode and the ARIA contract; the consumer keeps owning the
397
+ row markup and its cascade, and ordinary document CSS styles a virtualized row exactly as it styles
398
+ the same row unvirtualized.
399
+
400
+ Positioning stays on the shadow-side `[part="row"]` wrapper, which the document cannot select, so
401
+ windowing is not overridable by consumer CSS and the whole existing part vocabulary — `base`,
402
+ `spacer`, `row`, `group`, `sticky-group` — keeps matching in both modes.
403
+
404
+ Costs, stated plainly rather than hidden:
405
+
406
+ - Each projected row sits inside one component-owned wrapper element carrying the reserved
407
+ `data-lr-virtual-list-row` attribute (exported as `VIRTUAL_LIST_ROW_ATTRIBUTE`; the sticky band's
408
+ counterpart is `VIRTUAL_LIST_STICKY_ATTRIBUTE`). Descendant selectors port unchanged; child
409
+ combinators, `:nth-child`, `:first-child` and sibling combinators written against unvirtualized
410
+ markup do not, and `:nth-child` reflects the current window rather than the item index.
411
+ - A delegated listener on the host now sees an un-retargeted `event.target`, so
412
+ `closest('[part="row"]')` no longer resolves — use `closest('[data-lr-virtual-list-row]')`.
413
+ - The document cascade now reaches row content, including resets that previously could not.
414
+ - Per-row light-DOM state does not survive a disconnect/reconnect, because disconnect removes the
415
+ projected rows completely.
416
+ - On a hydrated page the first window is shadow-rendered for one task before it swaps into the
417
+ light DOM, so server markup and the first client render agree.
418
+
419
+ `rowProjection` is `'shadow'` by default. Left unset, every existing list renders byte-identically
420
+ and the host's light DOM stays empty.
421
+
422
+ ### Patch Changes
423
+
424
+ - ce460db: **Documentation: `aria-pressed` stopped reaching a link button (`href` set) in 16.0.0 — recorded
425
+ here because 16.0.0 filed it where nobody would find it.**
426
+
427
+ No behaviour changes in this release. This entry exists because the change itself shipped without a
428
+ discoverable release note, and a consumer pinning the old contract met it as an unexplained test
429
+ failure after a dependency bump.
430
+
431
+ **What changed, and when.** Through 15.0.0, `<lr-button>`, `<lr-icon-button>` and `<lr-card>`
432
+ forwarded a host `aria-pressed` onto the `<a>` they render when `href` is set. Since **16.0.0** they
433
+ do not: `aria-pressed` reaches the `<button>` rendering only. `role="link"` has no pressed state, so
434
+ forwarding it there asserted an ARIA state that does not exist on that role — an ARIA conformance
435
+ failure, not a feature. The behaviour is correct and is not being reverted.
436
+
437
+ This narrowed a documented forwarding contract, so it was a **breaking change** for anyone relying
438
+ on the old behaviour, and it should have been filed as one. 16.0.0 did describe it, but as the third
439
+ sub-heading of a long `<lr-card>` disabled-state entry whose opening sentence never mentions
440
+ `<lr-button>` — so neither scanning the release notes for button changes nor reading a per-component
441
+ digest surfaced it.
442
+
443
+ **Migration.** If the control is a real toggle, drop `href` — the same host `aria-pressed` then
444
+ reaches the `<button>` that replaces the anchor, and the pressed state is exposed again. If it is a
445
+ navigation target, use the global `aria-current` (`page`, `step`, `location`, `date`, `time`,
446
+ `true`, `false`) instead: that one is valid on `role="link"` and does still reach the anchor.
447
+
448
+ The `lr-button`, `lr-icon-button` and `lr-card` reference sections now name 16.0.0 as the version
449
+ the carve-out landed in, so the rule can be dated from the reference alone.
450
+ - 87d02cb: Fixed `<lr-reorder-list>`'s documented Ctrl/Cmd+ArrowUp/ArrowDown shortcut so it fires from real
451
+ keyboard focus on a row's own move button, not only when a `keydown` happens to be dispatched
452
+ synthetically on the button's host element. The move buttons are composed `<lr-icon-button>`s, so
453
+ real DOM focus during ordinary keyboard use lands on that button's own native `<button>` — one
454
+ shadow boundary deeper than `<lr-reorder-item>`'s own shadow root. The exclusion predicate that
455
+ tells the row's own chrome apart from a consumer's nested control only recognized an element whose
456
+ root node was exactly the item's shadow root, so the nested native button fell through to the
457
+ generic `name === 'button'` exclusion and every Ctrl/Cmd+Arrow press starting from real focus on a
458
+ move button was discarded as though it had come from a consumer's own control.
459
+ - 87d02cb: Fixed `<lr-reorder-list>` in `controlled` mode so it no longer steals focus back to the moved row
460
+ when the consumer has deliberately moved focus elsewhere while an async `finalizePendingMove()` /
461
+ host reconciliation is still in flight. Previously the post-move focus restore unconditionally
462
+ focused the moved row's move button once the host's re-render settled, even when the consumer had
463
+ already moved focus to an unrelated control on the page in the meantime — overriding that
464
+ deliberate choice. The restore now only runs when the pre-restore focus was still somewhere inside
465
+ this list, or there was nothing meaningful to preserve (nothing focused, or the previously-focused
466
+ node was disconnected by the host's own reconciliation); a still-connected external focus target
467
+ is left alone.
468
+ - cada7da: `<lr-table>`'s `editType: 'select'` cell editor now shows a themed disclosure chevron instead of
469
+ looking like a plain text field. `appearance: none` on `select[part='cell-editor']` removed the
470
+ native arrow with no replacement; the glyph is repainted on the enclosing `[part='cell']` (a bare
471
+ `<select>` cannot host a decorative child, and a `mask` can't apply to the select itself without
472
+ clipping its own text), using the same mask + `background: currentColor` technique already shipped
473
+ for the map attribution-toggle glyph, positioned at the logical inline-end edge so it mirrors
474
+ correctly under `dir="rtl"` and survives `forced-colors: active`. The `text`/`number` cell editors
475
+ are unaffected.
476
+ - e4402ef: Fixed `<lr-thread-list>` data-mode rows so choosing a `renderActions`-rendered menu action (for
477
+ example a consumer's `<lr-dropdown>` containing an `<lr-menu>`) no longer also fires the row's own
478
+ `lr-select` and selects/opens the conversation. The row's `lr-select` listener previously reacted to
479
+ any `lr-select`-named event that bubbled through the `<lr-conversation-item>`, including one
480
+ coincidentally emitted by a `renderActions`-rendered descendant (`<lr-menu>` fires its own
481
+ `lr-select` on item choice). It now only treats the event as row activation when it was dispatched
482
+ directly on the `<lr-conversation-item>` itself (`e.target === e.currentTarget`), which is how a
483
+ real row click/keyboard activation always dispatches it; a descendant's bubbled event is still
484
+ absorbed at the row boundary (as before) but no longer re-emitted as the row's own selection.
485
+
3
486
  ## 17.0.0
4
487
 
5
488
  ### Major Changes
package/README.md CHANGED
@@ -476,11 +476,11 @@ registerLyraLocale("en", {
476
476
  });
477
477
  ```
478
478
 
479
- Eleven translation catalogs now ship as side-effect-only modules, so a common locale no longer needs a
479
+ Twelve translation catalogs now ship as side-effect-only modules, so a common locale no longer needs a
480
480
  hand-written catalog at all:
481
481
 
482
482
  ```js
483
- import "@aceshooting/lyra-ui/translations/fr.js"; // also ar, de, es, fa, he, it, ja, pt-BR, ru, zh-CN
483
+ import "@aceshooting/lyra-ui/translations/fr.js"; // also ar, de, es, fa, he, it, ja, pt-BR, ro, ru, zh-CN
484
484
  ```
485
485
 
486
486
  Each locale also ships as twelve smaller per-family slices, so an app using only a handful of