@aceshooting/lyra-ui 0.1.3 → 0.1.5

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 (82) hide show
  1. package/README.md +2 -8
  2. package/custom-elements.json +4046 -3214
  3. package/dist/components/chart/bar-chart.js +17 -4
  4. package/dist/components/chart/box-plot.d.ts +8 -1
  5. package/dist/components/chart/box-plot.js +40 -4
  6. package/dist/components/chart/box-plot.styles.js +4 -0
  7. package/dist/components/chart/bubble-chart.d.ts +1 -2
  8. package/dist/components/chart/bubble-chart.js +17 -4
  9. package/dist/components/chart/chart-loader.d.ts +21 -4
  10. package/dist/components/chart/chart-loader.js +39 -12
  11. package/dist/components/chart/chart.d.ts +9 -3
  12. package/dist/components/chart/chart.js +42 -21
  13. package/dist/components/chart/doughnut-chart.d.ts +1 -2
  14. package/dist/components/chart/doughnut-chart.js +17 -4
  15. package/dist/components/chart/histogram-bin.js +12 -2
  16. package/dist/components/chart/histogram.d.ts +2 -2
  17. package/dist/components/chart/histogram.js +16 -2
  18. package/dist/components/chart/line-chart.js +17 -4
  19. package/dist/components/chart/pie-chart.d.ts +1 -2
  20. package/dist/components/chart/pie-chart.js +17 -4
  21. package/dist/components/chart/polar-area-chart.d.ts +1 -2
  22. package/dist/components/chart/polar-area-chart.js +17 -4
  23. package/dist/components/chart/radar-chart.d.ts +1 -2
  24. package/dist/components/chart/radar-chart.js +17 -4
  25. package/dist/components/chart/scatter-chart.js +17 -4
  26. package/dist/components/combobox/combobox.js +3 -4
  27. package/dist/components/combobox/combobox.stories.d.ts +6 -0
  28. package/dist/components/combobox/combobox.stories.js +27 -0
  29. package/dist/components/combobox/combobox.styles.js +2 -2
  30. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  31. package/dist/components/date-picker/date-input.stories.js +12 -0
  32. package/dist/components/date-picker/date-input.styles.js +2 -2
  33. package/dist/components/date-picker/date-picker.stories.d.ts +6 -0
  34. package/dist/components/date-picker/date-picker.stories.js +13 -0
  35. package/dist/components/empty/empty.d.ts +1 -1
  36. package/dist/components/empty/empty.js +1 -1
  37. package/dist/components/export-button/export-button.d.ts +2 -2
  38. package/dist/components/export-button/export-button.js +2 -2
  39. package/dist/components/file-input/accept.d.ts +1 -2
  40. package/dist/components/file-input/accept.js +1 -2
  41. package/dist/components/file-input/file-input.d.ts +2 -2
  42. package/dist/components/file-input/file-input.js +2 -2
  43. package/dist/components/flag/flag.stories.d.ts +5 -0
  44. package/dist/components/flag/flag.stories.js +17 -0
  45. package/dist/components/flag/flag.styles.js +1 -1
  46. package/dist/components/gauge/gauge.d.ts +1 -1
  47. package/dist/components/gauge/gauge.js +1 -1
  48. package/dist/components/graph/graph.d.ts +33 -4
  49. package/dist/components/graph/graph.js +146 -30
  50. package/dist/components/heatmap/calendar-grid.d.ts +31 -0
  51. package/dist/components/heatmap/calendar-grid.js +52 -0
  52. package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
  53. package/dist/components/heatmap/heatmap-scale.js +21 -0
  54. package/dist/components/heatmap/heatmap.d.ts +19 -0
  55. package/dist/components/heatmap/heatmap.js +133 -20
  56. package/dist/components/heatmap/heatmap.styles.js +4 -0
  57. package/dist/components/map/map.d.ts +15 -0
  58. package/dist/components/map/map.js +89 -7
  59. package/dist/components/playback/playback.d.ts +2 -2
  60. package/dist/components/playback/playback.js +8 -2
  61. package/dist/components/skeleton/skeleton.d.ts +2 -2
  62. package/dist/components/skeleton/skeleton.js +2 -2
  63. package/dist/components/sparkline/sparkline.stories.d.ts +7 -0
  64. package/dist/components/sparkline/sparkline.stories.js +17 -0
  65. package/dist/components/stat/stat.d.ts +1 -1
  66. package/dist/components/stat/stat.js +6 -7
  67. package/dist/components/stat/stat.stories.d.ts +5 -0
  68. package/dist/components/stat/stat.stories.js +16 -0
  69. package/dist/components/table/table.d.ts +1 -2
  70. package/dist/components/table/table.js +1 -2
  71. package/dist/components/table/table.styles.js +2 -3
  72. package/dist/components/time-range/time-range.js +4 -4
  73. package/dist/components/toast/toast.stories.d.ts +5 -0
  74. package/dist/components/toast/toast.stories.js +32 -0
  75. package/dist/components/tree/tree-node.d.ts +2 -2
  76. package/dist/components/tree/tree-node.js +2 -2
  77. package/dist/internal/form-associated.js +1 -2
  78. package/dist/internal/icons.js +1 -1
  79. package/dist/internal/tokens.styles.js +3 -3
  80. package/llms-full.txt +59 -83
  81. package/llms.txt +1 -2
  82. package/package.json +4 -1
@@ -147,10 +147,10 @@ export class LyraTimeRange extends LyraElement {
147
147
  this.end = Math.min(hi, Math.max(lo, this.end));
148
148
  }
149
149
  // Keep start <= end regardless of which side changed (a controlled
150
- // caller may set only `end`, e.g. two-way-binding an external store
151
- // see finding in 2026-07-09 tier2 review). The handle that just moved
152
- // wins; the other side is pulled to meet it, mirroring the direction
153
- // `clamp()` already uses during interactive dragging.
150
+ // caller may set only `end`, e.g. two-way-binding an external store).
151
+ // The handle that just moved wins; the other side is pulled to meet it,
152
+ // mirroring the direction `clamp()` already uses during interactive
153
+ // dragging.
154
154
  if (changed.has('start') || changed.has('min') || changed.has('max')) {
155
155
  this.start = Math.min(this.start, this.end);
156
156
  }
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Triggers: Story;
@@ -0,0 +1,32 @@
1
+ import { html } from 'lit';
2
+ import { toast } from '../../lyra.js';
3
+ const meta = {
4
+ title: 'Toast',
5
+ tags: ['autodocs'],
6
+ parameters: {
7
+ docs: {
8
+ description: {
9
+ component: 'Click a button to fire a toast via the `toast()` helper — the ergonomic entry point that lazily mounts a single `<lyra-toast>` region on `document.body`.',
10
+ },
11
+ },
12
+ },
13
+ };
14
+ export default meta;
15
+ export const Triggers = {
16
+ render: () => html `
17
+ <div style="display:flex; gap:1rem;">
18
+ <button @click=${() => toast('Just so you know')}>Neutral</button>
19
+ <button @click=${() => toast({ message: 'Saved!', variant: 'success' })}>Success</button>
20
+ <button
21
+ @click=${() => toast({
22
+ message: 'Item deleted',
23
+ variant: 'danger',
24
+ duration: 0,
25
+ action: { label: 'Undo', onClick: (item) => item.hide() },
26
+ })}
27
+ >
28
+ Danger + action
29
+ </button>
30
+ </div>
31
+ `,
32
+ };
@@ -9,8 +9,8 @@ import type { TreeItem } from './tree.js';
9
9
  * the *host* element, not the internal `[part="row"]` div — that makes this
10
10
  * node's own nested children (rendered in *its own* shadow root as further
11
11
  * `role="group"` content) genuine DOM descendants of the treeitem, which is
12
- * what the WAI-ARIA treeitem pattern requires (2026-07-10 audit,
13
- * "temporal-graph" §lyra-tree, Medium — previously a shadow-root sibling).
12
+ * what the WAI-ARIA treeitem pattern requires (previously a shadow-root
13
+ * sibling).
14
14
  *
15
15
  * @customElement lyra-tree-node
16
16
  * @csspart row, toggle, label, badge
@@ -81,8 +81,8 @@ const styles = css `
81
81
  * the *host* element, not the internal `[part="row"]` div — that makes this
82
82
  * node's own nested children (rendered in *its own* shadow root as further
83
83
  * `role="group"` content) genuine DOM descendants of the treeitem, which is
84
- * what the WAI-ARIA treeitem pattern requires (2026-07-10 audit,
85
- * "temporal-graph" §lyra-tree, Medium — previously a shadow-root sibling).
84
+ * what the WAI-ARIA treeitem pattern requires (previously a shadow-root
85
+ * sibling).
86
86
  *
87
87
  * @customElement lyra-tree-node
88
88
  * @csspart row, toggle, label, badge
@@ -62,8 +62,7 @@ export function FormAssociated(Base) {
62
62
  /**
63
63
  * Recomputes `ElementInternals`'s validity state. Without this,
64
64
  * `internals` defaults to permanently "valid" and `required` never
65
- * blocks form submission — the single highest-leverage correctness gap
66
- * the 2026-07-10 cross-repo audit found (forms-core §internal/, High).
65
+ * blocks form submission.
67
66
  */
68
67
  updateValidity() {
69
68
  if (this.required && this._value === '') {
@@ -5,7 +5,7 @@ import { svg } from 'lit';
5
5
  * date-input calendar toggle) as literal Unicode/HTML-entity/emoji text
6
6
  * glyphs — font-dependent, non-recolorable beyond plain text color, and
7
7
  * inconsistent in weight/size across the OS/browser font stack a host page
8
- * happens to ship (2026-07-10 design review). These replace all of them.
8
+ * happens to ship. These replace all of them.
9
9
  *
10
10
  * Every icon shares one 24x24 viewBox and one stroke-width so the whole set
11
11
  * reads as one visual language; each renders at `1em` so it inherits the
@@ -22,7 +22,7 @@ export const tokens = css `
22
22
 
23
23
  /* Motion — every component that animates (popovers, gauge fill, toast)
24
24
  reads from these two instead of hand-rolling its own duration/easing,
25
- so the library has one consistent rhythm (2026-07-10 design review). */
25
+ so the library has one consistent rhythm. */
26
26
  --lyra-transition-fast: var(--wa-transition-fast, 120ms ease-out);
27
27
  --lyra-transition-base: var(--wa-transition-normal, 180ms ease-out);
28
28
 
@@ -49,8 +49,8 @@ export const tokens = css `
49
49
  /* Standalone (no Web Awesome theme) dark-mode fallback. A real --wa-* value
50
50
  from a consumer's theme always wins — this only changes what a bare
51
51
  lyra-ui component renders when dropped, unstyled, onto a dark host page
52
- (2026-07-10 design review: previously zero dark-mode adaptation existed
53
- in the pure-fallback case). */
52
+ (previously zero dark-mode adaptation existed in the pure-fallback
53
+ case). */
54
54
  @media (prefers-color-scheme: dark) {
55
55
  :host {
56
56
  --lyra-color-surface: var(--wa-color-surface-default, #1a1a1a);
package/llms-full.txt CHANGED
@@ -64,7 +64,7 @@ Not a custom element itself — infrastructure every `lyra-*` component in the l
64
64
  `lyra-skeleton`'s "Loading…" text).
65
65
 
66
66
  **Known gotchas:**
67
- - **High** — `required` is currently inert for real validation. `FormAssociated` never calls
67
+ - `required` is currently inert for real validation. `FormAssociated` never calls
68
68
  `internals.setValidity(...)`; `ElementInternals` defaults to "valid" until that's called.
69
69
  `checkValidity()`/`reportValidity()` always return `true`. Concretely:
70
70
  `<form><lyra-date-input name="d" required></lyra-date-input></form>`; `form.reportValidity()`
@@ -72,14 +72,14 @@ Not a custom element itself — infrastructure every `lyra-*` component in the l
72
72
  `:invalid`/`:user-invalid` match, no `aria-invalid`. This affects both `lyra-date-input` (via the
73
73
  mixin) and `lyra-combobox` (via its own hand-rolled copy). **Do not rely on `required` for
74
74
  constraint validation yet** — validate in application code until this is fixed.
75
- - **Medium** — `formResetCallback()` always resets to blank (`''` / `[]`), not the field's
75
+ - `formResetCallback()` always resets to blank (`''` / `[]`), not the field's
76
76
  originally-constructed value. `form.reset()` on `<lyra-date-input value="2026-07-15">` clears it
77
77
  to `''` instead of restoring `"2026-07-15"` the way a native input's `defaultValue` would.
78
- - **Medium** — only `lyra-date-input` actually uses the shared mixin; `lyra-combobox` independently
78
+ - only `lyra-date-input` actually uses the shared mixin; `lyra-combobox` independently
79
79
  hand-rolls the same internals/validity/reset logic (needed because its value can be an array in
80
80
  `multiple` mode) — the two copies can silently diverge, and the `setValidity` gap above has to be
81
81
  fixed in both places.
82
- - **Low** — no shared label/input association helper: `<label part="form-control-label">` in both
82
+ - no shared label/input association helper: `<label part="form-control-label">` in both
83
83
  `lyra-combobox` and `lyra-date-input` is a DOM sibling of the input with no `id`/`for` pairing, so
84
84
  clicking the visible label text doesn't focus the field the way a native `<label for>` would.
85
85
 
@@ -150,18 +150,18 @@ Multi-select submits as **repeated `FormData` entries** (not a joined string)
150
150
  `new FormData(form).getAll(name)` behaves like a native multi-value control.
151
151
 
152
152
  **Known gotchas:**
153
- - **High** — `required` is declared but completely inert: never read for validity (see the shared
153
+ - `required` is declared but completely inert: never read for validity (see the shared
154
154
  `internal/` gotchas above) **and** never reflected as `aria-required` onto the actual
155
155
  `<input part="combobox-input">` — a screen-reader user gets zero indication the field is
156
156
  mandatory even before the validity fix lands.
157
- - **Medium** — declaratively-selected options are **ignored on initial render**:
157
+ - declaratively-selected options are **ignored on initial render**:
158
158
  `<lyra-option value="b" selected>` in markup never seeds the combobox's selection — only the
159
159
  combobox's own state pushes down to options, never the reverse. Set `.value` programmatically
160
160
  after mount instead of relying on the `selected` attribute in markup.
161
- - **Low** — naming drift from Web Awesome parity: the shipped attribute is **`with-clear`**, not
161
+ - naming drift from Web Awesome parity: the shipped attribute is **`with-clear`**, not
162
162
  `clearable` (which is what `<wa-combobox>` uses) — don't copy-paste WA docs assuming a 1:1
163
163
  attribute name here.
164
- - **Low** — no `size` variant (small/medium/large) unlike sibling `lyra-toast-item`.
164
+ - no `size` variant (small/medium/large) unlike sibling `lyra-toast-item`.
165
165
  - Full ARIA 1.2 combobox pattern (`role=combobox`, roving `aria-activedescendant`, real DOM focus
166
166
  kept on the input) is implemented correctly — a genuine strength, safe to build on.
167
167
 
@@ -260,24 +260,24 @@ from the package root**, but worth knowing when reasoning about behavior): `pars
260
260
  `resolveFirstDayOfWeek(value)`.
261
261
 
262
262
  **Known gotchas:**
263
- - **Medium** — `first-day-of-week="auto"` does **not** derive from `locale` despite the name:
263
+ - `first-day-of-week="auto"` does **not** derive from `locale` despite the name:
264
264
  `resolveFirstDayOfWeek()` hardcodes `0` (Sunday) whenever the value is `'auto'`, even though
265
265
  `locale` is already used elsewhere (weekday labels, month title). A French/German-locale user gets
266
266
  a Sunday-first grid instead of the Monday-first layout their OS would show. **Workaround:** pass
267
267
  an explicit `first-day-of-week="mon"` (accepted values: `sun`/`mon`/`tue`/`wed`/`thu`/`fri`/`sat`)
268
268
  if you need locale-correct week starts.
269
- - **Medium** (date-input only) — the calendar popover has **no Escape-to-close**: only
269
+ - (date-input only) — the calendar popover has **no Escape-to-close**: only
270
270
  `Alt+ArrowDown` opens it; there's no keydown handling to `hide()` on Escape, unlike `lyra-combobox`
271
271
  in the same package. A keyboard user must Tab all the way through the grid/nav/clear buttons or
272
272
  click elsewhere to dismiss it.
273
- - **Low** (date-input only) — opening the calendar doesn't move focus into it (`show()` only flips
273
+ - (date-input only) — opening the calendar doesn't move focus into it (`show()` only flips
274
274
  `open`) — a keyboard user opening via `Alt+ArrowDown` lands on a visible popover with focus still
275
275
  on the text input.
276
- - **Low** — one hardcoded, non-token color: selected/range-start/range-end day text is
276
+ - one hardcoded, non-token color: selected/range-start/range-end day text is
277
277
  `color: #fff` in `date-picker.styles.ts` — if you override
278
278
  `--lyra-color-brand`/`--wa-color-brand-fill-loud` to something light, contrast on selected-day
279
279
  text isn't guaranteed.
280
- - **Low** (date-input) — shares the mixin's reset-to-blank defect: `form.reset()` always clears to
280
+ - (date-input only) — shares the mixin's reset-to-blank defect: `form.reset()` always clears to
281
281
  `''` rather than restoring the originally-constructed `value`.
282
282
  - The grid keyboard pattern (Arrow/PageUp/PageDown/Home/End navigation with correct focus
283
283
  sequencing) is implemented correctly and safe to rely on.
@@ -365,11 +365,11 @@ re-mounts if removed) a single `<lyra-toast>` region on `document.body`.
365
365
  ```
366
366
 
367
367
  **Known gotchas:**
368
- - **Low** — possible nested live-region double-announcement: the stack region is
368
+ - possible nested live-region double-announcement: the stack region is
369
369
  `role="status" aria-live="polite"` **and** each `lyra-toast-item` independently self-assigns its
370
370
  own `role` (`status`/`alert`) — nesting live regions can cause some screen readers to announce a
371
371
  new toast twice. Plausible from the code, unverified against real AT.
372
- - **Low** — pause/resume-on-hover/focus (the component's main accessibility differentiator) has no
372
+ - pause/resume-on-hover/focus (the component's main accessibility differentiator) has no
373
373
  regression test coverage — don't assume a future refactor won't silently break it; test it
374
374
  yourself if it's load-bearing for your app.
375
375
  - Prefer the `toast()` helper over manually creating `<lyra-toast>`/`<lyra-toast-item>` — it already
@@ -459,7 +459,7 @@ pnpm add @aceshooting/lyra-flags # required peer — without it, <lyra-flag> r
459
459
  - Rendering is async even when the peer *is* installed: `src` resolves after an `import()` +
460
460
  resolver call, so there's a brief empty-render window on first paint/attribute change — don't
461
461
  assume the `<img>` exists synchronously right after setting `country`/`language`.
462
- - **Low** — if `label` is omitted, the accessible name (`alt`) falls back to the **bare uppercase
462
+ - if `label` is omitted, the accessible name (`alt`) falls back to the **bare uppercase
463
463
  ISO code** (e.g. `"FR"`, `"DE"`) — a screen reader will typically spell this out letter-by-letter
464
464
  rather than announce a country name. Always pass an explicit `label` (e.g. `"France"`) if the flag
465
465
  conveys real information rather than being purely decorative.
@@ -495,14 +495,14 @@ First-party "no data" state (no Web Awesome equivalent).
495
495
  ```
496
496
 
497
497
  **Known gotchas:**
498
- - **Medium** — no live-region semantics: a plain role-less `<div>`, so a list/table transitioning to
498
+ - no live-region semantics: a plain role-less `<div>`, so a list/table transitioning to
499
499
  empty announces nothing to screen readers (unlike sibling `lyra-skeleton`'s `role="status"`).
500
- - **Low** — no compact/density variant; fixed `padding: var(--lyra-space-l)` and 2rem icon sizing
500
+ - no compact/density variant; fixed `padding: var(--lyra-space-l)` and 2rem icon sizing
501
501
  are tuned for full-page use, awkward embedded in a card or narrow container.
502
502
  - Note: correctly works around the classic `:empty`-pseudo-class trap (a wrapper with a `<slot>`
503
503
  inside can never match `:empty`) by tracking real slot assignment in JS (`hasIcon`/`hasActions`) —
504
- `lyra-table` reuses this component for its own empty-rows state, but `lyra-stat` (below)
505
- reintroduces the very bug this component avoids.
504
+ `lyra-table` reuses this component for its own empty-rows state, and `lyra-stat` (below) now uses
505
+ the same JS-tracked-slot-state pattern for its own icon/caption wrappers.
506
506
 
507
507
  ---
508
508
 
@@ -533,13 +533,13 @@ Loading placeholder (`text`/`circle`/`rect` shapes, `pulse`/`sheen` effects).
533
533
  ```
534
534
 
535
535
  **Known gotchas:**
536
- - **Medium** — every instance is an independent, simultaneously-firing `role="status"` live region
536
+ - every instance is an independent, simultaneously-firing `role="status"` live region
537
537
  (via `willUpdate` setting `role="status"` plus visually-hidden "Loading…" text). A realistic
538
538
  loading state (e.g. a 10×5 skeleton table = 50 instances) mounts 50 separate live regions at
539
539
  nearly the same tick — expect screen readers to announce "Loading…" dozens of times, not once.
540
540
  There's no grouping primitive to dedupe this; if it matters, wrap your own single `role="status"`
541
541
  region around a set of skeletons and mark the individual ones `aria-hidden`.
542
- - **Low** — no `lines`/`count` shorthand for "N lines of skeleton text" — stamp out N elements
542
+ - no `lines`/`count` shorthand for "N lines of skeleton text" — stamp out N elements
543
543
  yourself.
544
544
  - Respects `prefers-reduced-motion` (both effects) — safe to leave as-is for that concern.
545
545
 
@@ -557,11 +557,14 @@ KPI/stat card — value + unit + label + optional icon/trend/caption.
557
557
  - `trend: number = NaN` (a `NaN` sentinel hides the trend pill entirely — set an actual number to
558
558
  show it)
559
559
  - `caption: string = ''`
560
+ - `goodDirection: 'up'|'down' = 'up'` (attribute `good-direction`) — which trend direction counts
561
+ as "good"; inverts arrow/color polarity for cost/latency/error-rate-style metrics where a
562
+ *decrease* is the win.
560
563
 
561
564
  **Events:** none.
562
565
 
563
- **Slots:** default (leading icon), `caption` (rich caption content — **see gotchas, it does not
564
- override the attribute**)
566
+ **Slots:** default (leading icon), `caption` (rich caption content — overrides the `caption`
567
+ attribute when slotted content is provided)
565
568
 
566
569
  **CSS parts:** `base`, `icon`, `label`, `value-row`, `value`, `unit`, `trend`, `caption`
567
570
 
@@ -577,23 +580,8 @@ the `variant`-colored value text and up/down trend pill).
577
580
  ```
578
581
 
579
582
  **Known gotchas:**
580
- - **High** icon/caption wrappers can never CSS-collapse: the stylesheet still relies on
581
- `[part='icon']:empty` / `[part='caption']:empty { display: none }`, but both wrappers always
582
- contain a literal `<slot>` child, so `:empty` never matches regardless of whether content is
583
- provided. `<lyra-stat label="x" value="1">` with **no** icon/caption still lays out both empty
584
- wrappers (visible gap/padding). This is the exact bug `lyra-empty` (above) correctly works around
585
- via JS-tracked slot state — that fix has not been ported here.
586
- - **Medium** — the `caption` **slot does not override** the `caption` **attribute** despite the
587
- intent implied by the docs: `render()` emits `${this.caption}<slot name="caption"></slot>`
588
- unconditionally, so setting both concatenates rather than the slot replacing the attribute text.
589
- Pick one or the other, don't set both.
590
- - **Medium** — `trend` polarity is hardcoded "higher is better": `trend > 0` always renders an
591
- up-arrow in the success color, `trend < 0` always down/danger, with no invert flag. For metrics
592
- where a *decrease* is good (cost, latency, error rate, churn), pre-negate the number yourself —
593
- but that also flips the arrow direction misleadingly, so there's no fully correct workaround
594
- today.
595
- - **Low** — the trend glyph (▲/▼/–) has no `aria-hidden`/accessible label, and there's no
596
- `aria-live` for in-place `value`/`trend` updates.
583
+ - no `aria-live` for in-place `value`/`trend` updates a value/trend change after first
584
+ render is not announced to screen readers.
597
585
 
598
586
  ---
599
587
 
@@ -614,6 +602,8 @@ of `rows` — this component only renders and emits intents.
614
602
  - `moreLabel: string = 'Load more'` (attribute `more-label`)
615
603
  - `emptyHeading: string = 'No data'` (attribute `empty-heading`)
616
604
  - `emptyDescription: string = ''` (attribute `empty-description`)
605
+ - `noColumnsHeading: string = 'No columns configured'` (attribute `no-columns-heading`)
606
+ - `noColumnsDescription: string = ''` (attribute `no-columns-description`)
617
607
 
618
608
  **Events:** `lyra-sort` (`detail: { key }`, fired on sortable-header activation), `lyra-row-click`
619
609
  (`detail: { row }`), `lyra-load-more` (fired on the "load more" button)
@@ -643,17 +633,10 @@ body's `max-block-size`).
643
633
  ```
644
634
 
645
635
  **Known gotchas:**
646
- - **Medium** with `columns.length === 0` and non-empty `rows`, every `<tr role="row">` renders
647
- with **zero** `<th>`/`<td>` children — an ARIA-invalid row that's still `tabindex="0"` and
648
- clickable despite no visible/accessible content. Only the zero-**rows** case is handled (falls
649
- back to `<lyra-empty>`); always pass at least one column.
650
- - **Medium** — a fresh `@click`/`@keydown` closure is allocated for every sortable header and every
651
- row on **every render** — real, avoidable GC churn on wide/tall tables that re-render on each
652
- sort.
653
- - **Medium** — only single-row selection is modeled (`selectedKey: string | number | null`);
636
+ - only single-row selection is modeled (`selectedKey: string | number | null`);
654
637
  there's no bulk-select/checkbox-column API — you must hand-roll a checkbox column entirely inside
655
638
  a `cell()` callback if you need multi-select.
656
- - **Low** — no `caption`/`aria-label` property, so the rendered `role="grid"` has no accessible name
639
+ - no `caption`/`aria-label` property, so the rendered `role="grid"` has no accessible name
657
640
  distinguishing it from any other grid on the page.
658
641
  - Keyboard equivalents (Enter/Space) for both header-sort and row-activate are implemented and
659
642
  solid — a genuine strength versus most siblings in this family.
@@ -692,15 +675,15 @@ gauge-specific custom property exists).
692
675
  ```
693
676
 
694
677
  **Known gotchas:**
695
- - **Medium** — no `aria-valuetext`: setting `valueLabel` (e.g. `"72°F"`) changes the visible SVG
678
+ - no `aria-valuetext`: setting `valueLabel` (e.g. `"72°F"`) changes the visible SVG
696
679
  text but never reaches the accessibility tree — `aria-valuenow` always carries the bare numeric
697
680
  `value`. A screen reader announces the raw number, not your formatted string.
698
- - **Medium** — the SVG `<text>` elements aren't `aria-hidden`, so they're exposed inside the same
681
+ - the SVG `<text>` elements aren't `aria-hidden`, so they're exposed inside the same
699
682
  `role="meter"` host that already carries `aria-valuenow`/`aria-label` — risks double/mismatched
700
683
  announcements when `valueLabel` differs from the raw `value`.
701
- - **Low** — no color-threshold/variant support: a single fixed `--lyra-color-brand` fill regardless
684
+ - no color-threshold/variant support: a single fixed `--lyra-color-brand` fill regardless
702
685
  of value (no amber/red-near-capacity idiom built in).
703
- - **Low** — no documented component-specific sizing custom property; host size is fixed em values
686
+ - no documented component-specific sizing custom property; host size is fixed em values
704
687
  (`8em`/`12em` radial, `12em`/`1.5em` linear) — resize via plain CSS `width`/`height` on the
705
688
  element instead.
706
689
  - Divide-by-zero guarded (`max - min || 1`), and radial/linear share one component via the `type`
@@ -755,12 +738,7 @@ downloadBlob(content: string, filename: string, mime: string): void // trig
755
738
  ```
756
739
 
757
740
  **Known gotchas:**
758
- - **High** the format-choice menu breaks this library's own disclosure-menu conventions: unlike
759
- `lyra-combobox`/`lyra-date-input` in the same package, it has **no outside-pointerdown dismissal,
760
- no Escape-to-close**, and `[part="trigger"]` never gets `aria-haspopup`/`aria-expanded`. Clicking
761
- elsewhere on the page does not close an open format menu — handle this yourself (e.g. close on
762
- your own outside-click listener) until fixed upstream.
763
- - **Low** — XLSX isn't implemented (`ExportFormat` really is `'csv' | 'json'`) — don't pass
741
+ - XLSX isn't implemented (`ExportFormat` really is `'csv' | 'json'`) don't pass
764
742
  `'xlsx'` expecting it to work.
765
743
  - CSV formula-injection guarding and the deferred (5s) `URL.revokeObjectURL` (works around Safari
766
744
  cancelling in-flight downloads on immediate revoke) are genuine, safe-to-rely-on strengths.
@@ -804,19 +782,19 @@ Keyboard: focus a divider (`Tab`), then `ArrowRight`/`ArrowLeft` (horizontal) or
804
782
  `ArrowDown`/`ArrowUp` (vertical) to resize by a fixed 2% step.
805
783
 
806
784
  **Known gotchas:**
807
- - **Medium** — `panelCount` is computed **once**, in `connectedCallback()`, from
785
+ - `panelCount` is computed **once**, in `connectedCallback()`, from
808
786
  `this.children.length`. Adding/removing a panel after mount (e.g. conditionally showing a side
809
787
  panel) leaves `panelCount`/`sizes`/divider count silently stale — there's no
810
788
  `slotchange`/`MutationObserver` to react to it. If your panel set can change, re-create the
811
789
  `<lyra-split>` rather than mutating its children live.
812
- - **Medium** — divider `aria-valuemax` (reported as `100 - min`) is only correct for exactly **two**
790
+ - divider `aria-valuemax` (reported as `100 - min`) is only correct for exactly **two**
813
791
  panels. With 3+ panels the real achievable max for divider `i` is `sizes[i] + sizes[i+1] - min`
814
792
  (resizing only trades size between the adjacent pair) — e.g. three equal 33.3% panels with
815
793
  `min=10` all report `aria-valuemax="90"` though the true achievable value is ~56.6. Don't trust
816
794
  the reported max for 3+-panel layouts.
817
- - **Low** — no feasibility check on `min` vs. panel count (e.g. 3 panels with `min=40` is
795
+ - no feasibility check on `min` vs. panel count (e.g. 3 panels with `min=40` is
818
796
  unsatisfiable) — result is a silently frozen splitter with no warning.
819
- - **Low** — `localStorage.getItem`/`setItem` calls themselves are unguarded (only the `JSON.parse`
797
+ - `localStorage.getItem`/`setItem` calls themselves are unguarded (only the `JSON.parse`
820
798
  result is try/caught) — a blocked or quota-exceeded store can throw synchronously from inside a
821
799
  `pointerup`/`keydown` handler.
822
800
  - Pointer-drag lifecycle (pointer capture, window-listener cleanup on both drag-end and
@@ -960,15 +938,15 @@ resolved through a scratch canvas). Also consumes `--lyra-color-text-quiet` (axi
960
938
  ```
961
939
 
962
940
  **Known gotchas:**
963
- - **High** — the `ResizeObserver` is wired but effectively inert: `draw()` always sizes the canvas
941
+ - the `ResizeObserver` is wired but effectively inert: `draw()` always sizes the canvas
964
942
  as `PAD_LEFT + cols * cellSize`, never from the host's measured width, so a container resize
965
943
  redraws identical dimensions. The stylesheet's `canvas { inline-size: 100% }` is also dead code —
966
944
  `draw()` unconditionally sets an inline `canvas.style.width/height` on every redraw, and inline
967
945
  style always wins over that external rule.
968
- - **Medium** — `NaN`/non-finite cell values are **not** treated as no-data (only `-1` is): `NaN < 0`
946
+ - `NaN`/non-finite cell values are **not** treated as no-data (only `-1` is): `NaN < 0`
969
947
  is `false`, so it falls into the normal color-ramp branch, produces an invalid `fillStyle`, and
970
948
  that cell silently keeps whatever color was left over from the previous loop iteration.
971
- - **Medium** — moving the browser window repeatedly across displays with different pixel ratios
949
+ - moving the browser window repeatedly across displays with different pixel ratios
972
950
  leaks one stale `MediaQueryList` listener per DPR crossing (each `onDprChange` creates a new
973
951
  listener without removing the previous one).
974
952
  - Per-cell values are not individually inspectable by assistive tech — the whole grid is one
@@ -1015,13 +993,13 @@ install with `pnpm add d3-force d3-drag d3-zoom d3-selection`).
1015
993
  ```
1016
994
 
1017
995
  **Known gotchas:**
1018
- - **High** — per-tick full re-render is expensive: every d3-force tick (up to ~300 by default,
996
+ - per-tick full re-render is expensive: every d3-force tick (up to ~300 by default,
1019
997
  continuously while dragging via `alphaTarget(0.3)`) replaces `simNodes`/`simLinks` with fresh
1020
998
  array references, forcing Lit to re-diff the entire node/link template and re-run
1021
999
  `applyInteractions()`. Noticeable cost for graphs of a few hundred+ nodes.
1022
- - **Medium** — `chargeStrength`/`linkDistance` are **not** live-reactive: changing them post-mount
1000
+ - `chargeStrength`/`linkDistance` are **not** live-reactive: changing them post-mount
1023
1001
  does nothing until `nodes`/`links` also change (only those two trigger `rebuildSimulation()`).
1024
- - **Medium** — zoom has no `.scaleExtent(...)` (unbounded in/out); pan/zoom/drag are pointer-only
1002
+ - zoom has no `.scaleExtent(...)` (unbounded in/out); pan/zoom/drag are pointer-only
1025
1003
  with no keyboard equivalent. Links (`<line part="link">`) are click-only — no
1026
1004
  `role`/`tabindex`/`aria-label`, unlike nodes which are properly keyboard-operable
1027
1005
  (`tabindex="0"`, `role="button"`, Enter/Space).
@@ -1087,16 +1065,16 @@ indentation), plus the shared tokens listed above.
1087
1065
  ```
1088
1066
 
1089
1067
  **Known gotchas:**
1090
- - **High** — the treeitem row has **zero keyboard interaction**: `role="treeitem"` has no
1068
+ - the treeitem row has **zero keyboard interaction**: `role="treeitem"` has no
1091
1069
  `tabindex` and no keydown handler, so it never enters the Tab order. The only keyboard-operable
1092
1070
  control at all is the expand/collapse `<button>` — and that's removed (`tabindex="-1"`,
1093
1071
  `aria-hidden`) for leaf nodes. `lyra-node-select` fires only from the label's `@click`, with no
1094
1072
  keyboard equivalent, and there is no roving-tabindex/arrow-key navigation model.
1095
- - **Medium** — `role="group"` (the expanded-children wrapper) is a shadow-DOM **sibling**, not a DOM
1073
+ - `role="group"` (the expanded-children wrapper) is a shadow-DOM **sibling**, not a DOM
1096
1074
  descendant, of its `role="treeitem"` row — violates the WAI-ARIA treeitem pattern's
1097
1075
  DOM-containment expectation. (The library's own axe test does not catch this — axe's structural
1098
1076
  rules accept a group as a valid child of `role="tree"` regardless of true ownership.)
1099
- - **Medium** — by-id reconciliation (which preserves `expanded` state across data reassignment)
1077
+ - by-id reconciliation (which preserves `expanded` state across data reassignment)
1100
1078
  only applies at depth 0. Nested `children` arrays are rendered via a plain `.map()` with no keyed
1101
1079
  `repeat()`, so reordering/inserting into a nested array can silently attach the wrong node's
1102
1080
  `expanded` state.
@@ -1159,16 +1137,16 @@ properties only cascade downward); shared tokens `--lyra-space-xs`, `--lyra-colo
1159
1137
  ```
1160
1138
 
1161
1139
  **Known gotchas:**
1162
- - **Medium** — "locked" `type` in the typed subclasses (below) is **not actually enforced**:
1140
+ - "locked" `type` in the typed subclasses (below) is **not actually enforced**:
1163
1141
  `<lyra-pie-chart type="bar">` or `el.type = 'bar'` at runtime silently turns it into a bar chart.
1164
1142
  Conversely, `<lyra-chart type="pie">` already works today with no subclass needed at all —
1165
1143
  `buildConfig()` passes `type` straight through unchecked.
1166
- - **Medium** — the `lyra-theme` window-event listener (meant for re-theming on theme switch) is
1144
+ - the `lyra-theme` window-event listener (meant for re-theming on theme switch) is
1167
1145
  dead code: nothing in the library ever dispatches that event, and even if it were dispatched, no
1168
1146
  `--lyra-*`/`getComputedStyle` color resolution happens anywhere in this component — Chart.js
1169
1147
  always renders grid/tick/legend/tooltip text with its own hardcoded palette, which can read as
1170
1148
  low-contrast in dark mode.
1171
- - **Medium** — generated `scales` config is cartesian (`x`/`y`/`y2`) only: for `lyra-radar-chart`/
1149
+ - generated `scales` config is cartesian (`x`/`y`/`y2`) only: for `lyra-radar-chart`/
1172
1150
  `lyra-polar-area-chart` (which need a single radial `r` scale) and for `lyra-pie-chart`/
1173
1151
  `lyra-doughnut-chart` (no scale at all), the inherited `xLabel`/`yLabel`/`beginAtZero`/`y2Label`
1174
1152
  properties are silently inert with no warning. Reach the radial scale only via raw `config`.
@@ -1228,7 +1206,7 @@ Bins `values` into `bins` equal-width buckets and renders as a bar chart (extend
1228
1206
  ```
1229
1207
 
1230
1208
  **Known gotchas:**
1231
- - **High** — **`bins <= 0` crashes** on non-empty `values`: `bins=0` throws `TypeError` inside
1209
+ - **`bins <= 0` crashes** on non-empty `values`: `bins=0` throws `TypeError` inside
1232
1210
  `binValues()` (bucket index resolves to `-1`, `buckets[-1].count++` fails); negative `bins` throws
1233
1211
  `RangeError` even earlier (`Array.from({length: -1})`). No guard exists anywhere in the chain —
1234
1212
  **always pass `bins >= 1`.**
@@ -1271,7 +1249,7 @@ independently of the base `chart-loader.ts`).
1271
1249
  ```
1272
1250
 
1273
1251
  **Known gotchas:**
1274
- - **Medium** — no raw `config` passthrough (unlike every other chart tag) — limited to
1252
+ - no raw `config` passthrough (unlike every other chart tag) — limited to
1275
1253
  `labels`/`boxes`/`legend`/`height`/`yLabel`/`beginAtZero`; can't reach the underlying
1276
1254
  controller's own options (`itemRadius`, `outlierRadius`, `coef`).
1277
1255
  - The peer-dependency-missing failure path (`.catch()` → warn + resolve `null`) is untested —
@@ -1330,11 +1308,11 @@ exists and was hit)
1330
1308
  ```
1331
1309
 
1332
1310
  **Known gotchas:**
1333
- - **Medium** — clearing or swapping the choropleth **leaks the old layer**: `applyChoropleth()`
1311
+ - clearing or swapping the choropleth **leaks the old layer**: `applyChoropleth()`
1334
1312
  only ever adds or updates; setting `choropleth = undefined`, or changing `choropleth.sourceId` to
1335
1313
  a different value, is a silent no-op — the previously added GeoJSON source and fill layer keep
1336
1314
  rendering forever (there is no `removeLayer`/`removeSource` call anywhere).
1337
- - **Medium** — `mapStyle` is reactive but changing it after construction is a **silent no-op** —
1315
+ - `mapStyle` is reactive but changing it after construction is a **silent no-op** —
1338
1316
  unlike `center`/`zoom`, which do call `setCenter`/`setZoom` on change, there's no `setStyle()`
1339
1317
  call wired up.
1340
1318
  - No declarative point-marker API — only the single choropleth polygon-fill layer exists; placing
@@ -1392,7 +1370,7 @@ them via JS (`el.allowedMimeTypes = [...]`), not as a JSON string attribute; the
1392
1370
  illustrative of intent only.
1393
1371
 
1394
1372
  **Known gotchas:**
1395
- - **High** — **`accept` only constrains the native file-picker dialog; it is silently ignored on
1373
+ - **`accept` only constrains the native file-picker dialog; it is silently ignored on
1396
1374
  the drag-drop path.** `isAllowed()` checks only `allowedMimeTypes`/`forbiddenMimeTypes` (exact
1397
1375
  MIME-string match — no extension or `image/*`-wildcard support like native `accept` has);
1398
1376
  `this.accept` itself is never read anywhere else in the component. Concretely:
@@ -1421,5 +1399,3 @@ These apply across the whole library rather than to any one component:
1421
1399
  `package.json`'s `files` allowlist, with no `prepack`/publish CI step regenerating it — so a
1422
1400
  manifest-driven IDE/tooling integration (e.g. custom-elements-manifest-based autocomplete) may
1423
1401
  not see it in a published tarball until that packaging gap is fixed.
1424
- - **Adoption status.** Zero real-world adoption of `@aceshooting/lyra-ui` currently exists in any
1425
- consumer repo — treat any "battle-tested in production" framing with that caveat.
package/llms.txt CHANGED
@@ -43,5 +43,4 @@ The library shipped in five incremental batches; `llms-full.txt` is ordered the
43
43
 
44
44
  - Shared internals every tag builds on (`LyraElement` base class, the `FormAssociated` mixin, the
45
45
  floating-ui `positioner`, and the `--lyra-*`/`--wa-*` design-token layer) are documented at the
46
- top of `llms-full.txt`, along with cross-cutting notes on packaging and adoption status that
47
- apply to the whole library.
46
+ top of `llms-full.txt`, along with cross-cutting packaging notes that apply to the whole library.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aceshooting/lyra-ui",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "Free, clean-room Lit web components — a companion to Web Awesome.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -20,6 +20,9 @@
20
20
  "publishConfig": {
21
21
  "access": "public"
22
22
  },
23
+ "engines": {
24
+ "node": ">=20"
25
+ },
23
26
  "files": [
24
27
  "dist",
25
28
  "custom-elements.json",