@aceshooting/lyra-ui 0.1.0 → 0.1.2
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.
- package/README.md +77 -6
- package/custom-elements.json +12015 -0
- package/dist/components/chart/bar-chart.d.ts +14 -0
- package/dist/components/chart/bar-chart.js +14 -0
- package/dist/components/chart/box-plot.d.ts +46 -0
- package/dist/components/chart/box-plot.js +145 -0
- package/dist/components/chart/box-plot.styles.d.ts +1 -0
- package/dist/components/chart/box-plot.styles.js +22 -0
- package/dist/components/chart/bubble-chart.d.ts +19 -0
- package/dist/components/chart/bubble-chart.js +18 -0
- package/dist/components/chart/chart-loader.d.ts +8 -0
- package/dist/components/chart/chart-loader.js +26 -0
- package/dist/components/chart/chart.d.ts +91 -0
- package/dist/components/chart/chart.js +275 -0
- package/dist/components/chart/chart.styles.d.ts +1 -0
- package/dist/components/chart/chart.styles.js +33 -0
- package/dist/components/chart/doughnut-chart.d.ts +15 -0
- package/dist/components/chart/doughnut-chart.js +14 -0
- package/dist/components/chart/histogram-bin.d.ts +6 -0
- package/dist/components/chart/histogram-bin.js +19 -0
- package/dist/components/chart/histogram.d.ts +20 -0
- package/dist/components/chart/histogram.js +70 -0
- package/dist/components/chart/histogram.styles.d.ts +1 -0
- package/dist/components/chart/histogram.styles.js +1 -0
- package/dist/components/chart/line-chart.d.ts +14 -0
- package/dist/components/chart/line-chart.js +14 -0
- package/dist/components/chart/pie-chart.d.ts +16 -0
- package/dist/components/chart/pie-chart.js +15 -0
- package/dist/components/chart/polar-area-chart.d.ts +15 -0
- package/dist/components/chart/polar-area-chart.js +14 -0
- package/dist/components/chart/radar-chart.d.ts +15 -0
- package/dist/components/chart/radar-chart.js +14 -0
- package/dist/components/chart/scatter-chart.d.ts +15 -0
- package/dist/components/chart/scatter-chart.js +15 -0
- package/dist/components/combobox/combobox.d.ts +16 -0
- package/dist/components/combobox/combobox.js +113 -6
- package/dist/components/combobox/combobox.styles.js +68 -7
- package/dist/components/combobox/option.d.ts +2 -2
- package/dist/components/combobox/option.js +11 -7
- package/dist/components/date-picker/date-input.d.ts +12 -1
- package/dist/components/date-picker/date-input.js +68 -6
- package/dist/components/date-picker/date-input.styles.js +58 -6
- package/dist/components/date-picker/date-picker.js +5 -2
- package/dist/components/date-picker/date-picker.styles.js +9 -2
- package/dist/components/empty/empty.d.ts +32 -0
- package/dist/components/empty/empty.js +90 -0
- package/dist/components/empty/empty.styles.d.ts +1 -0
- package/dist/components/empty/empty.styles.js +42 -0
- package/dist/components/export-button/csv.d.ts +10 -0
- package/dist/components/export-button/csv.js +28 -0
- package/dist/components/export-button/export-button.d.ts +41 -0
- package/dist/components/export-button/export-button.js +151 -0
- package/dist/components/export-button/export-button.styles.d.ts +1 -0
- package/dist/components/export-button/export-button.styles.js +79 -0
- package/dist/components/file-input/accept.d.ts +14 -0
- package/dist/components/file-input/accept.js +30 -0
- package/dist/components/file-input/file-input.d.ts +47 -0
- package/dist/components/file-input/file-input.js +182 -0
- package/dist/components/file-input/file-input.styles.d.ts +1 -0
- package/dist/components/file-input/file-input.styles.js +44 -0
- package/dist/components/flag/flag.d.ts +4 -0
- package/dist/components/flag/flag.js +24 -1
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/gauge/gauge.d.ts +29 -0
- package/dist/components/gauge/gauge.js +133 -0
- package/dist/components/gauge/gauge.styles.d.ts +1 -0
- package/dist/components/gauge/gauge.styles.js +60 -0
- package/dist/components/graph/graph.d.ts +68 -0
- package/dist/components/graph/graph.js +276 -0
- package/dist/components/graph/graph.styles.d.ts +1 -0
- package/dist/components/graph/graph.styles.js +34 -0
- package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
- package/dist/components/heatmap/heatmap-scale.js +17 -0
- package/dist/components/heatmap/heatmap.d.ts +66 -0
- package/dist/components/heatmap/heatmap.js +252 -0
- package/dist/components/heatmap/heatmap.styles.d.ts +1 -0
- package/dist/components/heatmap/heatmap.styles.js +39 -0
- package/dist/components/map/map-loader.d.ts +8 -0
- package/dist/components/map/map-loader.js +17 -0
- package/dist/components/map/map.d.ts +49 -0
- package/dist/components/map/map.js +189 -0
- package/dist/components/map/map.styles.d.ts +1 -0
- package/dist/components/map/map.styles.js +48 -0
- package/dist/components/playback/playback.d.ts +51 -0
- package/dist/components/playback/playback.js +146 -0
- package/dist/components/playback/playback.styles.d.ts +1 -0
- package/dist/components/playback/playback.styles.js +40 -0
- package/dist/components/skeleton/skeleton.d.ts +26 -0
- package/dist/components/skeleton/skeleton.js +65 -0
- package/dist/components/skeleton/skeleton.styles.d.ts +1 -0
- package/dist/components/skeleton/skeleton.styles.js +57 -0
- package/dist/components/split/split.d.ts +41 -0
- package/dist/components/split/split.js +171 -0
- package/dist/components/split/split.styles.d.ts +1 -0
- package/dist/components/split/split.styles.js +51 -0
- package/dist/components/stat/stat.d.ts +36 -0
- package/dist/components/stat/stat.js +117 -0
- package/dist/components/stat/stat.styles.d.ts +1 -0
- package/dist/components/stat/stat.styles.js +86 -0
- package/dist/components/table/table.d.ts +54 -0
- package/dist/components/table/table.js +201 -0
- package/dist/components/table/table.styles.d.ts +1 -0
- package/dist/components/table/table.styles.js +98 -0
- package/dist/components/time-range/time-range.d.ts +40 -0
- package/dist/components/time-range/time-range.js +219 -0
- package/dist/components/time-range/time-range.styles.d.ts +1 -0
- package/dist/components/time-range/time-range.styles.js +78 -0
- package/dist/components/toast/toast-item.js +10 -2
- package/dist/components/toast/toast-item.styles.js +13 -4
- package/dist/components/toast/toast.styles.js +0 -1
- package/dist/components/tree/tree-node.d.ts +50 -0
- package/dist/components/tree/tree-node.js +196 -0
- package/dist/components/tree/tree.d.ts +65 -0
- package/dist/components/tree/tree.js +227 -0
- package/dist/components/tree/tree.styles.d.ts +1 -0
- package/dist/components/tree/tree.styles.js +13 -0
- package/dist/internal/form-associated.js +49 -1
- package/dist/internal/icons.d.ts +11 -0
- package/dist/internal/icons.js +64 -0
- package/dist/internal/tokens.styles.js +41 -1
- package/dist/lyra.d.ts +67 -0
- package/dist/lyra.js +55 -0
- package/llms-full.txt +1425 -0
- package/llms.txt +47 -0
- package/package.json +60 -11
package/llms-full.txt
ADDED
|
@@ -0,0 +1,1425 @@
|
|
|
1
|
+
# @aceshooting/lyra-ui — LLM API Reference
|
|
2
|
+
|
|
3
|
+
`@aceshooting/lyra-ui` is a library of free, clean-room [Lit](https://lit.dev) web components — a
|
|
4
|
+
companion to [Web Awesome](https://webawesome.com) that reimplements several Web Awesome **Pro**
|
|
5
|
+
components as open-source equivalents (same tag shape, attributes, slots, events, and CSS parts,
|
|
6
|
+
under a `lyra-` prefix instead of `wa-`), plus a number of extras that have no Web Awesome
|
|
7
|
+
counterpart at all (dashboard atoms, temporal/graph widgets, chart wrappers, a map component, a
|
|
8
|
+
file dropzone). It is **token-compatible with Web Awesome**: every component's stylesheet resolves
|
|
9
|
+
through Web Awesome's `--wa-*` design tokens first, falling back to its own `--lyra-*` tokens, so
|
|
10
|
+
components read as native inside a Web Awesome-themed app and are equally usable standalone with no
|
|
11
|
+
theme at all. This document is the complete, component-by-component API reference intended for
|
|
12
|
+
coding assistants integrating the library; for install instructions, the full tag/tier table, and
|
|
13
|
+
the `<wa-*>` → `<lyra-*>` migration notes, see the package [README](./README.md) — or the shorter
|
|
14
|
+
curated index at [`llms.txt`](./llms.txt) if you just need the shape of the library before diving in
|
|
15
|
+
here.
|
|
16
|
+
|
|
17
|
+
## Contents
|
|
18
|
+
|
|
19
|
+
- **Shared foundation** — `internal/` (base class, form-association mixin, positioning helper, design tokens)
|
|
20
|
+
- **v1 core** — `lyra-combobox`/`lyra-option`, `lyra-date-picker`/`lyra-date-input`, `lyra-toast` family, `lyra-sparkline`, `lyra-flag`
|
|
21
|
+
- **Dashboard atoms** — `lyra-empty`, `lyra-skeleton`, `lyra-stat`, `lyra-table`, `lyra-gauge`, `lyra-export-button`, `lyra-split`
|
|
22
|
+
- **Temporal & graph** — `lyra-time-range`, `lyra-playback`, `lyra-heatmap`, `lyra-graph`, `lyra-tree`/`lyra-tree-node`
|
|
23
|
+
- **Chart family** — `lyra-chart` (core), typed subclasses, `lyra-histogram`, `lyra-box-plot`
|
|
24
|
+
- **Map & file-input** — `lyra-map`, `lyra-file-input`
|
|
25
|
+
- **Cross-cutting notes** — packaging and adoption caveats that apply across the whole library
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## Shared foundation: `internal/`
|
|
30
|
+
|
|
31
|
+
Not a custom element itself — infrastructure every `lyra-*` component in the library composes.
|
|
32
|
+
|
|
33
|
+
- **`LyraElement`** (`internal/lyra-element.ts`) — base class every `lyra-*` component `extends`.
|
|
34
|
+
`static styles = [tokens]` (the design-token layer below); subclasses prepend `LyraElement.styles`
|
|
35
|
+
to their own `static styles` array to inherit it. Supplies
|
|
36
|
+
`protected emit<T>(name: string, detail?: T): CustomEvent<T>` — dispatches a
|
|
37
|
+
`{ bubbles: true, composed: true, cancelable: true }` `CustomEvent` and returns it, so callers can
|
|
38
|
+
do `const ev = this.emit('lyra-export', {...}); if (ev.defaultPrevented) return;`.
|
|
39
|
+
- **`FormAssociated(Base)`** mixin (`internal/form-associated.ts`) — makes a `LitElement`
|
|
40
|
+
form-associated: `static formAssociated = true`, `attachInternals()` in the constructor. Adds
|
|
41
|
+
properties `name: string`, `value: string` (hand-written accessor via Lit's `noAccessor` so
|
|
42
|
+
`internals.setFormValue()` fires synchronously on assignment, not on the async update cycle),
|
|
43
|
+
`disabled: boolean` (reflected), `required: boolean` (reflected). Provides
|
|
44
|
+
`checkValidity()`/`reportValidity()` (delegate to `internals`), `formResetCallback()` (sets
|
|
45
|
+
`value = ''`), `formDisabledCallback(disabled)`. Only `lyra-date-input` uses this mixin;
|
|
46
|
+
`lyra-combobox` hand-rolls an equivalent independently (see its gotchas).
|
|
47
|
+
- **`positioner.ts` → `place(anchor, popup, opts?): () => void`** — thin wrapper over
|
|
48
|
+
`@floating-ui/dom`'s `computePosition` + `autoUpdate`. Forces `strategy: 'fixed'` (to match the
|
|
49
|
+
popup's own `position:fixed` CSS — otherwise it lands offset by the page scroll position),
|
|
50
|
+
middleware `offset(opts.offset ?? 4)`, `flip()`, `shift({ padding: 8 })`, default
|
|
51
|
+
`placement: 'bottom-start'`. Returns a cleanup function that stops the `autoUpdate` loop — call it
|
|
52
|
+
in `disconnectedCallback()`. Used by `lyra-combobox`, `lyra-date-input`, `lyra-export-button`.
|
|
53
|
+
- **`tokens.styles.ts` → `tokens`** (a `css` template prepended to every component's stylesheet) —
|
|
54
|
+
the whole design-token layer. Every value chains through a `--wa-*` Web Awesome token first, with
|
|
55
|
+
a hard-coded fallback, e.g. `--lyra-color-brand: var(--wa-color-brand-fill-loud, #0969da)`. Full
|
|
56
|
+
set: `--lyra-color-surface / -text / -text-quiet / -border / -brand / -brand-quiet / -success /
|
|
57
|
+
-warning / -danger`, `--lyra-space-xs / -s / -m / -l`, `--lyra-radius`, `--lyra-shadow`,
|
|
58
|
+
`--lyra-font`. Also sets `:host([hidden]) { display: none !important; }` and an inherited
|
|
59
|
+
`box-sizing: border-box` reset.
|
|
60
|
+
- **`prefix.ts`** — `LYRA_PREFIX = 'lyra'`; `tag(name)` → `` `lyra-${name}` ``; `defineElement(name,
|
|
61
|
+
ctor)` — idempotent `customElements.define`, safe if a module is evaluated twice.
|
|
62
|
+
- **`a11y.ts`** — `nextId(scope)`: monotonic id generator (e.g. `nextId('combobox-list')` →
|
|
63
|
+
`"lyra-combobox-list-3"`); `srOnly`: a visually-hidden-but-AT-visible CSS class (used by
|
|
64
|
+
`lyra-skeleton`'s "Loading…" text).
|
|
65
|
+
|
|
66
|
+
**Known gotchas:**
|
|
67
|
+
- **High** — `required` is currently inert for real validation. `FormAssociated` never calls
|
|
68
|
+
`internals.setValidity(...)`; `ElementInternals` defaults to "valid" until that's called.
|
|
69
|
+
`checkValidity()`/`reportValidity()` always return `true`. Concretely:
|
|
70
|
+
`<form><lyra-date-input name="d" required></lyra-date-input></form>`; `form.reportValidity()`
|
|
71
|
+
returns `true` and the form submits with the field empty — no native bubble, no
|
|
72
|
+
`:invalid`/`:user-invalid` match, no `aria-invalid`. This affects both `lyra-date-input` (via the
|
|
73
|
+
mixin) and `lyra-combobox` (via its own hand-rolled copy). **Do not rely on `required` for
|
|
74
|
+
constraint validation yet** — validate in application code until this is fixed.
|
|
75
|
+
- **Medium** — `formResetCallback()` always resets to blank (`''` / `[]`), not the field's
|
|
76
|
+
originally-constructed value. `form.reset()` on `<lyra-date-input value="2026-07-15">` clears it
|
|
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
|
|
79
|
+
hand-rolls the same internals/validity/reset logic (needed because its value can be an array in
|
|
80
|
+
`multiple` mode) — the two copies can silently diverge, and the `setValidity` gap above has to be
|
|
81
|
+
fixed in both places.
|
|
82
|
+
- **Low** — no shared label/input association helper: `<label part="form-control-label">` in both
|
|
83
|
+
`lyra-combobox` and `lyra-date-input` is a DOM sibling of the input with no `id`/`for` pairing, so
|
|
84
|
+
clicking the visible label text doesn't focus the field the way a native `<label for>` would.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
# v1 core: forms, toasts, sparkline, flag
|
|
89
|
+
|
|
90
|
+
## `lyra-combobox` / `lyra-option`
|
|
91
|
+
|
|
92
|
+
Filterable single/multi-select combining a text input with a listbox. Mirrors the core
|
|
93
|
+
`<wa-combobox>` API under the `lyra-` prefix. **Form-associated** (hand-rolled internals, not the
|
|
94
|
+
shared `FormAssociated` mixin — see gotchas).
|
|
95
|
+
|
|
96
|
+
### `lyra-combobox`
|
|
97
|
+
|
|
98
|
+
**Properties:**
|
|
99
|
+
- `multiple: boolean = false` (reflected)
|
|
100
|
+
- `placeholder: string = ''`
|
|
101
|
+
- `disabled: boolean = false` (reflected)
|
|
102
|
+
- `required: boolean = false` (reflected — **inert**, see gotchas)
|
|
103
|
+
- `name: string = ''`
|
|
104
|
+
- `label: string = ''`
|
|
105
|
+
- `hint: string = ''`
|
|
106
|
+
- `open: boolean = false` (reflected)
|
|
107
|
+
- `withClear: boolean = false` (attribute **`with-clear`** — not `clearable`, see gotchas)
|
|
108
|
+
- `maxOptionsVisible: number = 3` (attribute `max-options-visible` — caps how many selected tags
|
|
109
|
+
show before collapsing to `+N`)
|
|
110
|
+
- `emptyText: string = 'No results'` (attribute `empty-text`)
|
|
111
|
+
- `filter: OptionFilter | null = null` (attribute: false — `(option, query) => boolean`; default
|
|
112
|
+
matches `label`/`searchText` case-insensitively)
|
|
113
|
+
- `value: string | string[]` — a getter/setter: plain `string` in single mode, `string[]` in
|
|
114
|
+
`multiple` mode
|
|
115
|
+
|
|
116
|
+
**Events:** `change` (native-style — selection changed), `input` (native-style — typed or selection
|
|
117
|
+
changed), `lyra-show`, `lyra-hide`, `lyra-clear`
|
|
118
|
+
|
|
119
|
+
**Slots:** default (`<lyra-option>` children), `label`, `hint`
|
|
120
|
+
|
|
121
|
+
**CSS parts:** `form-control`, `form-control-label`, `combobox`, `tags`, `tag`,
|
|
122
|
+
`tag__remove-button`, `combobox-input`, `clear-button`, `expand-icon`, `listbox`, `option`, `hint`
|
|
123
|
+
|
|
124
|
+
**Themeable custom properties:** shared tokens only.
|
|
125
|
+
|
|
126
|
+
**Optional peer deps:** none.
|
|
127
|
+
|
|
128
|
+
### `lyra-option`
|
|
129
|
+
|
|
130
|
+
**Properties:**
|
|
131
|
+
- `value: string = ''`
|
|
132
|
+
- `disabled: boolean = false`
|
|
133
|
+
- `selected: boolean = false` (reflected — set by the parent combobox)
|
|
134
|
+
- `group: string = ''` (section header)
|
|
135
|
+
- `searchText: string = ''` (attribute `search-text` — extra text the filter matches beyond the
|
|
136
|
+
visible label)
|
|
137
|
+
- `label` is a **read-only getter**: explicit `label` attribute wins, else trimmed `textContent`.
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<lyra-combobox id="cb" label="Country" placeholder="Search…" with-clear>
|
|
141
|
+
<lyra-option value="fr">France</lyra-option>
|
|
142
|
+
<lyra-option value="de" search-text="deutschland">Germany</lyra-option>
|
|
143
|
+
</lyra-combobox>
|
|
144
|
+
<script type="module">
|
|
145
|
+
document.getElementById('cb').addEventListener('change', (e) => console.log(e.target.value));
|
|
146
|
+
</script>
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Multi-select submits as **repeated `FormData` entries** (not a joined string) —
|
|
150
|
+
`new FormData(form).getAll(name)` behaves like a native multi-value control.
|
|
151
|
+
|
|
152
|
+
**Known gotchas:**
|
|
153
|
+
- **High** — `required` is declared but completely inert: never read for validity (see the shared
|
|
154
|
+
`internal/` gotchas above) **and** never reflected as `aria-required` onto the actual
|
|
155
|
+
`<input part="combobox-input">` — a screen-reader user gets zero indication the field is
|
|
156
|
+
mandatory even before the validity fix lands.
|
|
157
|
+
- **Medium** — declaratively-selected options are **ignored on initial render**:
|
|
158
|
+
`<lyra-option value="b" selected>` in markup never seeds the combobox's selection — only the
|
|
159
|
+
combobox's own state pushes down to options, never the reverse. Set `.value` programmatically
|
|
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
|
|
162
|
+
`clearable` (which is what `<wa-combobox>` uses) — don't copy-paste WA docs assuming a 1:1
|
|
163
|
+
attribute name here.
|
|
164
|
+
- **Low** — no `size` variant (small/medium/large) unlike sibling `lyra-toast-item`.
|
|
165
|
+
- Full ARIA 1.2 combobox pattern (`role=combobox`, roving `aria-activedescendant`, real DOM focus
|
|
166
|
+
kept on the input) is implemented correctly — a genuine strength, safe to build on.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## `lyra-date-picker` / `lyra-date-input` (+ `calendar-core.ts`)
|
|
171
|
+
|
|
172
|
+
Mirrors the core `<wa-date-picker>`/`<wa-date-input>` API under `lyra-`. **Value is always ISO
|
|
173
|
+
8601**: `YYYY-MM-DD` (single) or `YYYY-MM-DD/YYYY-MM-DD` (range).
|
|
174
|
+
|
|
175
|
+
### `lyra-date-picker`
|
|
176
|
+
|
|
177
|
+
Inline month-grid calendar, not form-associated (used standalone or embedded inside
|
|
178
|
+
`lyra-date-input`'s popover).
|
|
179
|
+
|
|
180
|
+
**Properties:**
|
|
181
|
+
- `value: string = ''`
|
|
182
|
+
- `mode: 'single'|'range' = 'single'`
|
|
183
|
+
- `min: string = ''`
|
|
184
|
+
- `max: string = ''`
|
|
185
|
+
- `disabled: boolean = false` (reflected)
|
|
186
|
+
- `readonly: boolean = false` (reflected)
|
|
187
|
+
- `months: 1|2 = 1`
|
|
188
|
+
- `locale: string = ''`
|
|
189
|
+
- `firstDayOfWeek: string = 'auto'` (attribute `first-day-of-week` — see gotchas)
|
|
190
|
+
- `weekdayFormat: 'narrow'|'short'|'long' = 'short'` (attribute `weekday-format`)
|
|
191
|
+
- `disablePast: boolean = false` (attribute `disable-past`)
|
|
192
|
+
- `disableFuture: boolean = false` (attribute `disable-future`)
|
|
193
|
+
- `withOutsideDays: boolean = false` (attribute `with-outside-days`)
|
|
194
|
+
|
|
195
|
+
**Getters:** `selection: { from: Date|null; to: Date|null }`, `valueAsDate: Date | null` (single
|
|
196
|
+
mode only)
|
|
197
|
+
|
|
198
|
+
**Methods:** `clear()`, `goToToday()`, `goToDate(date: string | Date)`
|
|
199
|
+
|
|
200
|
+
**Events:** `input` (during interaction — for range mode, fires after the first click of a pair),
|
|
201
|
+
`change` (committed value)
|
|
202
|
+
|
|
203
|
+
**Slots:** none.
|
|
204
|
+
|
|
205
|
+
**CSS parts:** `base`, `month`, `header`, `title`, `previous`, `next`, `weekdays`, `weekday`,
|
|
206
|
+
`grid`, `week`, `day`, `day-outside`, `day-today`, `day-selected`, `day-range-start`,
|
|
207
|
+
`day-range-end`, `day-range-inner`, `day-placeholder`
|
|
208
|
+
|
|
209
|
+
**Themeable custom properties:** `--lyra-cell-size` (default `2.25rem`, controls day-cell/grid-column
|
|
210
|
+
size).
|
|
211
|
+
|
|
212
|
+
**Optional peer deps:** none.
|
|
213
|
+
|
|
214
|
+
### `lyra-date-input`
|
|
215
|
+
|
|
216
|
+
Text field + calendar popover, **form-associated** via the shared `FormAssociated` mixin (`name`,
|
|
217
|
+
`value`, `disabled`, `required` all inherited).
|
|
218
|
+
|
|
219
|
+
**Properties (own):**
|
|
220
|
+
- `mode: 'single'|'range' = 'single'`
|
|
221
|
+
- `min: string = ''`
|
|
222
|
+
- `max: string = ''`
|
|
223
|
+
- `readonly: boolean = false` (reflected)
|
|
224
|
+
- `open: boolean = false` (reflected)
|
|
225
|
+
- `withClear: boolean = false` (attribute `with-clear`)
|
|
226
|
+
- `label: string = ''`
|
|
227
|
+
- `hint: string = ''`
|
|
228
|
+
- `placeholder: string = ''`
|
|
229
|
+
- `locale: string = ''`
|
|
230
|
+
- `months: 1|2 = 1`
|
|
231
|
+
- `firstDayOfWeek: string = 'auto'`
|
|
232
|
+
- `weekdayFormat: 'narrow'|'short'|'long' = 'short'`
|
|
233
|
+
|
|
234
|
+
**Methods:** `show()`, `hide()`, `clear()`
|
|
235
|
+
|
|
236
|
+
**Events:** `input`, `change`, `lyra-show`, `lyra-hide`, `lyra-clear`
|
|
237
|
+
|
|
238
|
+
**Slots:** `label`, `hint`
|
|
239
|
+
|
|
240
|
+
**CSS parts:** `form-control`, `form-control-label`, `input-wrapper`, `input`, `clear-button`,
|
|
241
|
+
`expand-button`, `expand-icon`, `popup`, `date-picker`, `hint`
|
|
242
|
+
|
|
243
|
+
**Themeable custom properties:** shared tokens only.
|
|
244
|
+
|
|
245
|
+
**Optional peer deps:** none.
|
|
246
|
+
|
|
247
|
+
```html
|
|
248
|
+
<lyra-date-input id="di" label="Start date" with-clear name="start"></lyra-date-input>
|
|
249
|
+
<script type="module">
|
|
250
|
+
const di = document.getElementById('di');
|
|
251
|
+
di.value = '2026-07-10';
|
|
252
|
+
di.addEventListener('change', () => console.log(di.value)); // ISO string
|
|
253
|
+
</script>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`calendar-core.ts` exports the pure date math both components share (**internal — not re-exported
|
|
257
|
+
from the package root**, but worth knowing when reasoning about behavior): `parseISO`, `formatISO`,
|
|
258
|
+
`isSameDay`, `addMonths`, `clampDate`, `monthMatrix(year, month, firstDayOfWeek)`,
|
|
259
|
+
`weekdayLabels(firstDayOfWeek, format, locale)`, `monthTitle(year, month, locale)`,
|
|
260
|
+
`resolveFirstDayOfWeek(value)`.
|
|
261
|
+
|
|
262
|
+
**Known gotchas:**
|
|
263
|
+
- **Medium** — `first-day-of-week="auto"` does **not** derive from `locale` despite the name:
|
|
264
|
+
`resolveFirstDayOfWeek()` hardcodes `0` (Sunday) whenever the value is `'auto'`, even though
|
|
265
|
+
`locale` is already used elsewhere (weekday labels, month title). A French/German-locale user gets
|
|
266
|
+
a Sunday-first grid instead of the Monday-first layout their OS would show. **Workaround:** pass
|
|
267
|
+
an explicit `first-day-of-week="mon"` (accepted values: `sun`/`mon`/`tue`/`wed`/`thu`/`fri`/`sat`)
|
|
268
|
+
if you need locale-correct week starts.
|
|
269
|
+
- **Medium** (date-input only) — the calendar popover has **no Escape-to-close**: only
|
|
270
|
+
`Alt+ArrowDown` opens it; there's no keydown handling to `hide()` on Escape, unlike `lyra-combobox`
|
|
271
|
+
in the same package. A keyboard user must Tab all the way through the grid/nav/clear buttons or
|
|
272
|
+
click elsewhere to dismiss it.
|
|
273
|
+
- **Low** (date-input only) — opening the calendar doesn't move focus into it (`show()` only flips
|
|
274
|
+
`open`) — a keyboard user opening via `Alt+ArrowDown` lands on a visible popover with focus still
|
|
275
|
+
on the text input.
|
|
276
|
+
- **Low** — one hardcoded, non-token color: selected/range-start/range-end day text is
|
|
277
|
+
`color: #fff` in `date-picker.styles.ts` — if you override
|
|
278
|
+
`--lyra-color-brand`/`--wa-color-brand-fill-loud` to something light, contrast on selected-day
|
|
279
|
+
text isn't guaranteed.
|
|
280
|
+
- **Low** (date-input) — shares the mixin's reset-to-blank defect: `form.reset()` always clears to
|
|
281
|
+
`''` rather than restoring the originally-constructed `value`.
|
|
282
|
+
- The grid keyboard pattern (Arrow/PageUp/PageDown/Home/End navigation with correct focus
|
|
283
|
+
sequencing) is implemented correctly and safe to rely on.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## `lyra-toast` / `lyra-toast-item` / `toast()`
|
|
288
|
+
|
|
289
|
+
Stacking toast/notification region. Mirrors `<wa-toast>`/`<wa-toast-item>` under `lyra-`.
|
|
290
|
+
|
|
291
|
+
### `lyra-toast`
|
|
292
|
+
|
|
293
|
+
One per page recommended — the region.
|
|
294
|
+
|
|
295
|
+
**Properties:**
|
|
296
|
+
- `placement: ToastPlacement = 'top-end'` (reflected) — one of `'top-start'|'top-center'|'top-end'|
|
|
297
|
+
'bottom-start'|'bottom-center'|'bottom-end'`
|
|
298
|
+
|
|
299
|
+
**Methods:** `async create(message: string, options?: ToastCreateOptions): Promise<LyraToastItem>` —
|
|
300
|
+
`ToastCreateOptions = { variant?, duration?, size?, withIcon? }`
|
|
301
|
+
|
|
302
|
+
**Events:** none.
|
|
303
|
+
|
|
304
|
+
**Slots:** default (`<lyra-toast-item>` children)
|
|
305
|
+
|
|
306
|
+
**CSS parts:** `stack`
|
|
307
|
+
|
|
308
|
+
**Themeable custom properties:** `--gap` (default `var(--lyra-space-s)`), `--width` (default
|
|
309
|
+
`28rem`) — set directly on the `<lyra-toast>` element (not `--lyra-`-prefixed, but overridable
|
|
310
|
+
per-instance).
|
|
311
|
+
|
|
312
|
+
**Optional peer deps:** none.
|
|
313
|
+
|
|
314
|
+
### `lyra-toast-item`
|
|
315
|
+
|
|
316
|
+
A single notification.
|
|
317
|
+
|
|
318
|
+
**Properties:**
|
|
319
|
+
- `duration: number = 5000` (ms; `Infinity` or `<= 0` disables auto-dismiss)
|
|
320
|
+
- `size: 'xs'|'s'|'m'|'l'|'xl' = 'm'` (reflected)
|
|
321
|
+
- `variant: 'brand'|'success'|'warning'|'danger'|'neutral' = 'neutral'` (reflected)
|
|
322
|
+
- `withIcon: boolean = false` (attribute `with-icon`)
|
|
323
|
+
|
|
324
|
+
**Methods:** `async hide(): Promise<void>` — plays the hide animation, then removes itself from the
|
|
325
|
+
DOM.
|
|
326
|
+
|
|
327
|
+
**Events:** `lyra-show`, `lyra-after-show`, `lyra-hide`, `lyra-after-hide`
|
|
328
|
+
|
|
329
|
+
**Slots:** default (message), `icon`
|
|
330
|
+
|
|
331
|
+
**CSS parts:** `toast-item`, `accent`, `icon`, `content`, `close-button`
|
|
332
|
+
|
|
333
|
+
**Themeable custom properties:** `--accent-width` (4px), `--show-duration`/`--hide-duration` (250ms
|
|
334
|
+
each), `--padding` (`var(--lyra-space-m)`), `--accent-color` (defaults to `--lyra-color-border`,
|
|
335
|
+
auto-swapped per `variant` to `--lyra-color-brand/-success/-warning/-danger`).
|
|
336
|
+
|
|
337
|
+
**Optional peer deps:** none.
|
|
338
|
+
|
|
339
|
+
`role` is chosen automatically per `variant`: `"alert"` for `danger`/`warning`, `"status"`
|
|
340
|
+
otherwise. Auto-dismiss timer **pauses** on `pointerenter`/`focusin`, **resumes** on
|
|
341
|
+
`pointerleave`/`focusout`, with real elapsed-time bookkeeping (WCAG 2.2.1 timing-adjustable).
|
|
342
|
+
|
|
343
|
+
### `toast()`
|
|
344
|
+
|
|
345
|
+
From the `toaster` controller — the ergonomic entry point, no manual `<lyra-toast>` mounting
|
|
346
|
+
needed:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
import { toast } from '@aceshooting/lyra-ui';
|
|
350
|
+
|
|
351
|
+
toast('Saved');
|
|
352
|
+
toast({ message: 'Deleted', variant: 'danger', action: { label: 'Undo', onClick: (item) => {/*...*/} } });
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
`toast(input: ToastOptions | string): ToastHandle` where
|
|
356
|
+
`ToastOptions = ToastCreateOptions & { message: string; placement?: ToastPlacement; action?: { label: string; onClick: (item: LyraToastItem) => void } }`,
|
|
357
|
+
and `ToastHandle = { item: Promise<LyraToastItem>; dismiss: () => void }`. Lazily mounts (and
|
|
358
|
+
re-mounts if removed) a single `<lyra-toast>` region on `document.body`.
|
|
359
|
+
|
|
360
|
+
```html
|
|
361
|
+
<script type="module">
|
|
362
|
+
import { toast } from '@aceshooting/lyra-ui';
|
|
363
|
+
document.getElementById('save-btn').addEventListener('click', () => toast('Saved!'));
|
|
364
|
+
</script>
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
**Known gotchas:**
|
|
368
|
+
- **Low** — possible nested live-region double-announcement: the stack region is
|
|
369
|
+
`role="status" aria-live="polite"` **and** each `lyra-toast-item` independently self-assigns its
|
|
370
|
+
own `role` (`status`/`alert`) — nesting live regions can cause some screen readers to announce a
|
|
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
|
|
373
|
+
regression test coverage — don't assume a future refactor won't silently break it; test it
|
|
374
|
+
yourself if it's load-bearing for your app.
|
|
375
|
+
- Prefer the `toast()` helper over manually creating `<lyra-toast>`/`<lyra-toast-item>` — it already
|
|
376
|
+
handles the singleton-region and remount-if-removed logic.
|
|
377
|
+
|
|
378
|
+
---
|
|
379
|
+
|
|
380
|
+
## `lyra-sparkline`
|
|
381
|
+
|
|
382
|
+
Zero-dependency inline SVG trend chart (mirrors `<wa-sparkline>`).
|
|
383
|
+
|
|
384
|
+
**Properties:**
|
|
385
|
+
- `values: number[] = []`
|
|
386
|
+
- `type: 'line' | 'bar' | 'area' = 'line'`
|
|
387
|
+
- `min?: number` (defaults to data minimum)
|
|
388
|
+
- `max?: number` (defaults to data maximum)
|
|
389
|
+
|
|
390
|
+
**Events:** none.
|
|
391
|
+
|
|
392
|
+
**Slots:** none.
|
|
393
|
+
|
|
394
|
+
**CSS parts:** `line`, `area`, `bar`
|
|
395
|
+
|
|
396
|
+
**Themeable custom properties:** `--lyra-color-brand` only — the single cleanest theming story in
|
|
397
|
+
the reviewed family (pure CSS cascade, no JS/`getComputedStyle` bridging needed).
|
|
398
|
+
|
|
399
|
+
**Optional peer deps:** none.
|
|
400
|
+
|
|
401
|
+
```html
|
|
402
|
+
<lyra-sparkline type="area"></lyra-sparkline>
|
|
403
|
+
<script>
|
|
404
|
+
document.querySelector('lyra-sparkline').values = [3, 5, 4, 8, 6, 9, 7];
|
|
405
|
+
</script>
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Known gotchas:**
|
|
409
|
+
- Auto-sets `role="img"` + a descriptive `aria-label` ("Trend of N values, last X" / "No data" when
|
|
410
|
+
empty) — good default, but still just a single summary sentence with no tabular fallback; fine for
|
|
411
|
+
a glance-value sparkline, worth reconsidering if it's ever the *sole* representation of a metric.
|
|
412
|
+
- `min`/`max` overrides and the `bar`/`area` render types are lightly tested — no correctness
|
|
413
|
+
concerns found, just be aware coverage is thin there.
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## `lyra-flag`
|
|
418
|
+
|
|
419
|
+
Country/language flag image. Flag artwork ships in a **separate, optional peer package**
|
|
420
|
+
(`@aceshooting/lyra-flags`) — importing `lyra-ui` core never pulls in flag image weight.
|
|
421
|
+
|
|
422
|
+
**Properties:**
|
|
423
|
+
- `country?: string` (ISO 3166-1 alpha-2, e.g. `"fr"` — takes precedence over `language`)
|
|
424
|
+
- `language?: string` (BCP-47-ish tag, e.g. `"en"`/`"en-US"`, resolved to a representative country
|
|
425
|
+
via `languageToCountry()`)
|
|
426
|
+
- `label?: string` (accessible name / `alt` text — **defaults to the bare uppercase code if
|
|
427
|
+
omitted**, see gotchas)
|
|
428
|
+
- `round: boolean = false` (reflected — circular crop)
|
|
429
|
+
|
|
430
|
+
**Events:** none.
|
|
431
|
+
|
|
432
|
+
**Slots:** none.
|
|
433
|
+
|
|
434
|
+
**CSS parts:** `image`
|
|
435
|
+
|
|
436
|
+
**Themeable custom properties:** `--lyra-flag-radius` (default `2px`, non-`round` corner radius).
|
|
437
|
+
|
|
438
|
+
**Optional peer deps:** `@aceshooting/lyra-flags` — required for the component to actually render an
|
|
439
|
+
image; without it, renders an empty template (see gotchas).
|
|
440
|
+
|
|
441
|
+
Also exported from the package root:
|
|
442
|
+
`languageToCountry(language: string): string | undefined` and the `LANGUAGE_TO_COUNTRY` lookup
|
|
443
|
+
table (region subtag wins, e.g. `en-US` → `us`; plain `en` → `gb`; override the table per-app if you
|
|
444
|
+
need different defaults).
|
|
445
|
+
|
|
446
|
+
```html
|
|
447
|
+
<lyra-flag country="fr" label="France"></lyra-flag>
|
|
448
|
+
<lyra-flag language="en-US" round></lyra-flag>
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
```bash
|
|
452
|
+
pnpm add @aceshooting/lyra-flags # required peer — without it, <lyra-flag> renders nothing
|
|
453
|
+
```
|
|
454
|
+
|
|
455
|
+
**Known gotchas:**
|
|
456
|
+
- Requires the optional peer `@aceshooting/lyra-flags` to actually render an image; without it the
|
|
457
|
+
component renders an **empty template** plus a one-time `console.warn` (lazy `import()`, cached
|
|
458
|
+
module-wide so the warning fires only once per page even with many `<lyra-flag>` instances).
|
|
459
|
+
- Rendering is async even when the peer *is* installed: `src` resolves after an `import()` +
|
|
460
|
+
resolver call, so there's a brief empty-render window on first paint/attribute change — don't
|
|
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
|
|
463
|
+
ISO code** (e.g. `"FR"`, `"DE"`) — a screen reader will typically spell this out letter-by-letter
|
|
464
|
+
rather than announce a country name. Always pass an explicit `label` (e.g. `"France"`) if the flag
|
|
465
|
+
conveys real information rather than being purely decorative.
|
|
466
|
+
|
|
467
|
+
---
|
|
468
|
+
|
|
469
|
+
# Dashboard atoms
|
|
470
|
+
|
|
471
|
+
## `lyra-empty`
|
|
472
|
+
|
|
473
|
+
First-party "no data" state (no Web Awesome equivalent).
|
|
474
|
+
|
|
475
|
+
**Properties:**
|
|
476
|
+
- `heading: string = ''`
|
|
477
|
+
- `description: string = ''`
|
|
478
|
+
|
|
479
|
+
**Events:** none.
|
|
480
|
+
|
|
481
|
+
**Slots:** default (icon/illustration), `actions` (buttons/links below the description)
|
|
482
|
+
|
|
483
|
+
**CSS parts:** `base`, `icon`, `heading`, `description`, `actions`
|
|
484
|
+
|
|
485
|
+
**Themeable custom properties:** shared tokens only (`--lyra-space-s/-l`,
|
|
486
|
+
`--lyra-color-text-quiet/-border/-text`).
|
|
487
|
+
|
|
488
|
+
**Optional peer deps:** none.
|
|
489
|
+
|
|
490
|
+
```html
|
|
491
|
+
<lyra-empty heading="No results" description="Try a different search.">
|
|
492
|
+
<svg slot="" ...></svg> <!-- default slot: any icon/illustration -->
|
|
493
|
+
<div slot="actions"><button>Clear filters</button></div>
|
|
494
|
+
</lyra-empty>
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
**Known gotchas:**
|
|
498
|
+
- **Medium** — no live-region semantics: a plain role-less `<div>`, so a list/table transitioning to
|
|
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
|
|
501
|
+
are tuned for full-page use, awkward embedded in a card or narrow container.
|
|
502
|
+
- Note: correctly works around the classic `:empty`-pseudo-class trap (a wrapper with a `<slot>`
|
|
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.
|
|
506
|
+
|
|
507
|
+
---
|
|
508
|
+
|
|
509
|
+
## `lyra-skeleton`
|
|
510
|
+
|
|
511
|
+
Loading placeholder (`text`/`circle`/`rect` shapes, `pulse`/`sheen` effects).
|
|
512
|
+
|
|
513
|
+
**Properties:**
|
|
514
|
+
- `variant: 'text'|'circle'|'rect' = 'text'` (reflected)
|
|
515
|
+
- `effect: 'pulse'|'sheen' = 'pulse'` (reflected)
|
|
516
|
+
- `width?: string`
|
|
517
|
+
- `height?: string`
|
|
518
|
+
|
|
519
|
+
**Events:** none.
|
|
520
|
+
|
|
521
|
+
**Slots:** none.
|
|
522
|
+
|
|
523
|
+
**CSS parts:** `base`
|
|
524
|
+
|
|
525
|
+
**Themeable custom properties:** `--lyra-skeleton-w`, `--lyra-skeleton-h` (set/cleared by the
|
|
526
|
+
`width`/`height` properties; default `100%` / `1em`).
|
|
527
|
+
|
|
528
|
+
**Optional peer deps:** none.
|
|
529
|
+
|
|
530
|
+
```html
|
|
531
|
+
<lyra-skeleton variant="circle" width="3rem" height="3rem"></lyra-skeleton>
|
|
532
|
+
<lyra-skeleton variant="text"></lyra-skeleton>
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
**Known gotchas:**
|
|
536
|
+
- **Medium** — every instance is an independent, simultaneously-firing `role="status"` live region
|
|
537
|
+
(via `willUpdate` setting `role="status"` plus visually-hidden "Loading…" text). A realistic
|
|
538
|
+
loading state (e.g. a 10×5 skeleton table = 50 instances) mounts 50 separate live regions at
|
|
539
|
+
nearly the same tick — expect screen readers to announce "Loading…" dozens of times, not once.
|
|
540
|
+
There's no grouping primitive to dedupe this; if it matters, wrap your own single `role="status"`
|
|
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
|
|
543
|
+
yourself.
|
|
544
|
+
- Respects `prefers-reduced-motion` (both effects) — safe to leave as-is for that concern.
|
|
545
|
+
|
|
546
|
+
---
|
|
547
|
+
|
|
548
|
+
## `lyra-stat`
|
|
549
|
+
|
|
550
|
+
KPI/stat card — value + unit + label + optional icon/trend/caption.
|
|
551
|
+
|
|
552
|
+
**Properties:**
|
|
553
|
+
- `label: string = ''`
|
|
554
|
+
- `value: string = ''`
|
|
555
|
+
- `unit: string = ''`
|
|
556
|
+
- `variant: 'neutral'|'success'|'warning'|'danger' = 'neutral'` (reflected)
|
|
557
|
+
- `trend: number = NaN` (a `NaN` sentinel hides the trend pill entirely — set an actual number to
|
|
558
|
+
show it)
|
|
559
|
+
- `caption: string = ''`
|
|
560
|
+
|
|
561
|
+
**Events:** none.
|
|
562
|
+
|
|
563
|
+
**Slots:** default (leading icon), `caption` (rich caption content — **see gotchas, it does not
|
|
564
|
+
override the attribute**)
|
|
565
|
+
|
|
566
|
+
**CSS parts:** `base`, `icon`, `label`, `value-row`, `value`, `unit`, `trend`, `caption`
|
|
567
|
+
|
|
568
|
+
**Themeable custom properties:** shared tokens only (`--lyra-color-success/-warning/-danger` drive
|
|
569
|
+
the `variant`-colored value text and up/down trend pill).
|
|
570
|
+
|
|
571
|
+
**Optional peer deps:** none.
|
|
572
|
+
|
|
573
|
+
```html
|
|
574
|
+
<lyra-stat label="Active users" value="1,204" trend="4.2" variant="success">
|
|
575
|
+
<svg slot="">...</svg>
|
|
576
|
+
</lyra-stat>
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**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.
|
|
597
|
+
|
|
598
|
+
---
|
|
599
|
+
|
|
600
|
+
## `lyra-table`
|
|
601
|
+
|
|
602
|
+
Presentational, sort/select-aware data table. **The host owns actual sorting/filtering/pagination**
|
|
603
|
+
of `rows` — this component only renders and emits intents.
|
|
604
|
+
|
|
605
|
+
**Properties:**
|
|
606
|
+
- `columns: TableColumn<T>[] = []` (attribute: false) — `{ key, label, sortable?, align?:
|
|
607
|
+
'start'|'end', cell: (row) => unknown }`
|
|
608
|
+
- `rows: T[] = []` (attribute: false)
|
|
609
|
+
- `sortKey: string = ''` (attribute `sort-key`)
|
|
610
|
+
- `sortDir: 'asc'|'desc' = 'asc'` (attribute `sort-dir`)
|
|
611
|
+
- `rowKey?: (row: T) => string | number` (attribute: false)
|
|
612
|
+
- `selectedKey: string | number | null = null` (attribute: false) — **single**-selection only
|
|
613
|
+
- `hasMore: boolean = false` (attribute `has-more`, reflected)
|
|
614
|
+
- `moreLabel: string = 'Load more'` (attribute `more-label`)
|
|
615
|
+
- `emptyHeading: string = 'No data'` (attribute `empty-heading`)
|
|
616
|
+
- `emptyDescription: string = ''` (attribute `empty-description`)
|
|
617
|
+
|
|
618
|
+
**Events:** `lyra-sort` (`detail: { key }`, fired on sortable-header activation), `lyra-row-click`
|
|
619
|
+
(`detail: { row }`), `lyra-load-more` (fired on the "load more" button)
|
|
620
|
+
|
|
621
|
+
**Slots:** none — content comes entirely from `columns`/`rows`.
|
|
622
|
+
|
|
623
|
+
**CSS parts:** `base`, `table`, `head`, `header-cell`, `row`, `cell`, `more-button`
|
|
624
|
+
|
|
625
|
+
**Themeable custom properties:** `--lyra-table-max-height` (default `none`; controls the scrollable
|
|
626
|
+
body's `max-block-size`).
|
|
627
|
+
|
|
628
|
+
**Optional peer deps:** none.
|
|
629
|
+
|
|
630
|
+
```html
|
|
631
|
+
<lyra-table id="t" sort-key="name" sort-dir="asc"></lyra-table>
|
|
632
|
+
<script type="module">
|
|
633
|
+
const t = document.getElementById('t');
|
|
634
|
+
t.columns = [
|
|
635
|
+
{ key: 'name', label: 'Name', sortable: true, cell: (r) => r.name },
|
|
636
|
+
{ key: 'value', label: 'Value', align: 'end', cell: (r) => r.value },
|
|
637
|
+
];
|
|
638
|
+
t.rows = [{ name: 'Alpha', value: 1 }, { name: 'Beta', value: 2 }];
|
|
639
|
+
t.rowKey = (r) => r.name;
|
|
640
|
+
t.addEventListener('lyra-sort', (e) => console.log('sort by', e.detail.key));
|
|
641
|
+
t.addEventListener('lyra-row-click', (e) => console.log('clicked', e.detail.row));
|
|
642
|
+
</script>
|
|
643
|
+
```
|
|
644
|
+
|
|
645
|
+
**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`);
|
|
654
|
+
there's no bulk-select/checkbox-column API — you must hand-roll a checkbox column entirely inside
|
|
655
|
+
a `cell()` callback if you need multi-select.
|
|
656
|
+
- **Low** — no `caption`/`aria-label` property, so the rendered `role="grid"` has no accessible name
|
|
657
|
+
distinguishing it from any other grid on the page.
|
|
658
|
+
- Keyboard equivalents (Enter/Space) for both header-sort and row-activate are implemented and
|
|
659
|
+
solid — a genuine strength versus most siblings in this family.
|
|
660
|
+
|
|
661
|
+
---
|
|
662
|
+
|
|
663
|
+
## `lyra-gauge`
|
|
664
|
+
|
|
665
|
+
Dependency-free SVG radial or linear meter (270°-sweep arc math, no charting library).
|
|
666
|
+
|
|
667
|
+
**Properties:**
|
|
668
|
+
- `value: number = 0`
|
|
669
|
+
- `min: number = 0`
|
|
670
|
+
- `max: number = 100`
|
|
671
|
+
- `type: 'radial'|'linear' = 'radial'` (reflected)
|
|
672
|
+
- `label: string = ''`
|
|
673
|
+
- `valueLabel?: string` (attribute: false — overrides only the *visible* text, see gotchas)
|
|
674
|
+
|
|
675
|
+
**Events:** none.
|
|
676
|
+
|
|
677
|
+
**Slots:** none.
|
|
678
|
+
|
|
679
|
+
**CSS parts:** `base` (the `<svg>`), `track`, `fill`, `value`, `label`
|
|
680
|
+
|
|
681
|
+
**Themeable custom properties:** `--lyra-color-brand` (fill color, shared token — no per-instance
|
|
682
|
+
gauge-specific custom property exists).
|
|
683
|
+
|
|
684
|
+
**Optional peer deps:** none.
|
|
685
|
+
|
|
686
|
+
```html
|
|
687
|
+
<lyra-gauge value="72" min="0" max="100" label="CPU"></lyra-gauge>
|
|
688
|
+
<lyra-gauge type="linear" value="0.4" max="1"></lyra-gauge>
|
|
689
|
+
<script type="module">
|
|
690
|
+
document.querySelector('lyra-gauge').valueLabel = '72°F'; // changes displayed text only, see gotchas
|
|
691
|
+
</script>
|
|
692
|
+
```
|
|
693
|
+
|
|
694
|
+
**Known gotchas:**
|
|
695
|
+
- **Medium** — no `aria-valuetext`: setting `valueLabel` (e.g. `"72°F"`) changes the visible SVG
|
|
696
|
+
text but never reaches the accessibility tree — `aria-valuenow` always carries the bare numeric
|
|
697
|
+
`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
|
|
699
|
+
`role="meter"` host that already carries `aria-valuenow`/`aria-label` — risks double/mismatched
|
|
700
|
+
announcements when `valueLabel` differs from the raw `value`.
|
|
701
|
+
- **Low** — no color-threshold/variant support: a single fixed `--lyra-color-brand` fill regardless
|
|
702
|
+
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
|
|
704
|
+
(`8em`/`12em` radial, `12em`/`1.5em` linear) — resize via plain CSS `width`/`height` on the
|
|
705
|
+
element instead.
|
|
706
|
+
- Divide-by-zero guarded (`max - min || 1`), and radial/linear share one component via the `type`
|
|
707
|
+
attribute.
|
|
708
|
+
|
|
709
|
+
---
|
|
710
|
+
|
|
711
|
+
## `lyra-export-button`
|
|
712
|
+
|
|
713
|
+
CSV/JSON download button — either single-format (click exports immediately) or multi-format (click
|
|
714
|
+
opens a small menu).
|
|
715
|
+
|
|
716
|
+
**Properties:**
|
|
717
|
+
- `rows: Record<string, unknown>[] = []` (attribute: false)
|
|
718
|
+
- `columns: CsvColumn[] = []` (attribute: false) — `{ key, label }`
|
|
719
|
+
- `filename: string = 'export'`
|
|
720
|
+
- `formats: ExportFormat[] = ['csv']` (attribute: false; `ExportFormat = 'csv' | 'json'` **only** —
|
|
721
|
+
no XLSX despite the family sometimes being described as covering it)
|
|
722
|
+
- `disabled: boolean = false` (reflected)
|
|
723
|
+
- `label: string = 'Export'`
|
|
724
|
+
- `open: boolean = false` (reflected)
|
|
725
|
+
|
|
726
|
+
**Events:** `lyra-export` (`detail: { format }`, **cancelable** — call `preventDefault()` to
|
|
727
|
+
substitute your own server-generated download instead of the built-in client-side one),
|
|
728
|
+
`lyra-export-complete` (`detail: { format }`, fires after a non-cancelled download completes)
|
|
729
|
+
|
|
730
|
+
**Slots:** none.
|
|
731
|
+
|
|
732
|
+
**CSS parts:** `trigger`, `menu`, `menu-item`
|
|
733
|
+
|
|
734
|
+
**Themeable custom properties:** shared tokens only.
|
|
735
|
+
|
|
736
|
+
**Optional peer deps:** none.
|
|
737
|
+
|
|
738
|
+
```html
|
|
739
|
+
<lyra-export-button id="exp" filename="report" label="Export"></lyra-export-button>
|
|
740
|
+
<script type="module">
|
|
741
|
+
const exp = document.getElementById('exp');
|
|
742
|
+
exp.rows = [{ name: 'Alpha', value: 1 }];
|
|
743
|
+
exp.columns = [{ key: 'name', label: 'Name' }, { key: 'value', label: 'Value' }];
|
|
744
|
+
exp.formats = ['csv', 'json']; // shows a format-choice menu instead of exporting immediately
|
|
745
|
+
exp.addEventListener('lyra-export', (e) => console.log('exporting', e.detail.format));
|
|
746
|
+
</script>
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
Package-level CSV utilities (used internally, also exported for standalone use — `import {
|
|
750
|
+
escapeCsvField, buildCsv, downloadBlob } from '@aceshooting/lyra-ui'`):
|
|
751
|
+
```ts
|
|
752
|
+
escapeCsvField(value: unknown): string // quotes/escapes; neutralizes formula-injection (=,+,-,@,tab,CR) with a leading apostrophe
|
|
753
|
+
buildCsv(rows: Record<string, unknown>[], columns: CsvColumn[]): string // CRLF-joined, header row included
|
|
754
|
+
downloadBlob(content: string, filename: string, mime: string): void // triggers a browser download
|
|
755
|
+
```
|
|
756
|
+
|
|
757
|
+
**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
|
|
764
|
+
`'xlsx'` expecting it to work.
|
|
765
|
+
- CSV formula-injection guarding and the deferred (5s) `URL.revokeObjectURL` (works around Safari
|
|
766
|
+
cancelling in-flight downloads on immediate revoke) are genuine, safe-to-rely-on strengths.
|
|
767
|
+
|
|
768
|
+
---
|
|
769
|
+
|
|
770
|
+
## `lyra-split`
|
|
771
|
+
|
|
772
|
+
Resizable panels for dashboard layouts. Direct **light-DOM children are the panels**; a divider is
|
|
773
|
+
auto-inserted between each adjacent pair.
|
|
774
|
+
|
|
775
|
+
**Properties:**
|
|
776
|
+
- `sizes: number[] = []` (attribute: false — percentages per panel, auto-computed equally if
|
|
777
|
+
omitted/mismatched)
|
|
778
|
+
- `min: number = 10` (min % per panel)
|
|
779
|
+
- `orientation: 'horizontal'|'vertical' = 'horizontal'` (reflected)
|
|
780
|
+
- `storageKey?: string` (attribute `storage-key` — persists sizes to `localStorage` under
|
|
781
|
+
`` `lyra-split:${key}:${panelCount}` ``, scoped by panel count so a stale layout for a different
|
|
782
|
+
panel count is ignored)
|
|
783
|
+
|
|
784
|
+
**Events:** `lyra-resize` (`detail: { sizes }`, fired on every drag step/release **and** every
|
|
785
|
+
keyboard step)
|
|
786
|
+
|
|
787
|
+
**Slots:** default (each direct child element is one panel).
|
|
788
|
+
|
|
789
|
+
**CSS parts:** `base`, `divider`
|
|
790
|
+
|
|
791
|
+
**Themeable custom properties:** shared tokens only.
|
|
792
|
+
|
|
793
|
+
**Optional peer deps:** none.
|
|
794
|
+
|
|
795
|
+
```html
|
|
796
|
+
<lyra-split storage-key="dashboard-main" min="15">
|
|
797
|
+
<div>Panel A</div>
|
|
798
|
+
<div>Panel B</div>
|
|
799
|
+
<div>Panel C</div>
|
|
800
|
+
</lyra-split>
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Keyboard: focus a divider (`Tab`), then `ArrowRight`/`ArrowLeft` (horizontal) or
|
|
804
|
+
`ArrowDown`/`ArrowUp` (vertical) to resize by a fixed 2% step.
|
|
805
|
+
|
|
806
|
+
**Known gotchas:**
|
|
807
|
+
- **Medium** — `panelCount` is computed **once**, in `connectedCallback()`, from
|
|
808
|
+
`this.children.length`. Adding/removing a panel after mount (e.g. conditionally showing a side
|
|
809
|
+
panel) leaves `panelCount`/`sizes`/divider count silently stale — there's no
|
|
810
|
+
`slotchange`/`MutationObserver` to react to it. If your panel set can change, re-create the
|
|
811
|
+
`<lyra-split>` rather than mutating its children live.
|
|
812
|
+
- **Medium** — divider `aria-valuemax` (reported as `100 - min`) is only correct for exactly **two**
|
|
813
|
+
panels. With 3+ panels the real achievable max for divider `i` is `sizes[i] + sizes[i+1] - min`
|
|
814
|
+
(resizing only trades size between the adjacent pair) — e.g. three equal 33.3% panels with
|
|
815
|
+
`min=10` all report `aria-valuemax="90"` though the true achievable value is ~56.6. Don't trust
|
|
816
|
+
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
|
|
818
|
+
unsatisfiable) — result is a silently frozen splitter with no warning.
|
|
819
|
+
- **Low** — `localStorage.getItem`/`setItem` calls themselves are unguarded (only the `JSON.parse`
|
|
820
|
+
result is try/caught) — a blocked or quota-exceeded store can throw synchronously from inside a
|
|
821
|
+
`pointerup`/`keydown` handler.
|
|
822
|
+
- Pointer-drag lifecycle (pointer capture, window-listener cleanup on both drag-end and
|
|
823
|
+
`disconnectedCallback`) is solid and safe to rely on.
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
# Temporal & graph
|
|
828
|
+
|
|
829
|
+
## `lyra-time-range`
|
|
830
|
+
|
|
831
|
+
A two-handle brush/scrubber over a numeric domain (no date logic — callers map their own time axis
|
|
832
|
+
onto `[min, max]`).
|
|
833
|
+
|
|
834
|
+
**Properties:**
|
|
835
|
+
- `min: number = 0`
|
|
836
|
+
- `max: number = 100`
|
|
837
|
+
- `start: number = 0`
|
|
838
|
+
- `end: number = 100`
|
|
839
|
+
- `step: number = 1`
|
|
840
|
+
- `disabled: boolean = false` (reflected)
|
|
841
|
+
|
|
842
|
+
**Events:** `lyra-input` (fired continuously while dragging or on arrow-key press, `detail: {
|
|
843
|
+
start, end }`), `lyra-change` (fired on pointer release / arrow-key-up — commit, `detail: { start,
|
|
844
|
+
end }`)
|
|
845
|
+
|
|
846
|
+
**Slots:** none.
|
|
847
|
+
|
|
848
|
+
**CSS parts:** `base`, `track`, `range`, `handle-start`, `handle-end`
|
|
849
|
+
|
|
850
|
+
**Themeable custom properties:** shared tokens only — `--lyra-color-border`, `--lyra-color-brand`,
|
|
851
|
+
`--lyra-color-surface`, `--lyra-shadow`. No component-local tokens.
|
|
852
|
+
|
|
853
|
+
**Optional peer deps:** none.
|
|
854
|
+
|
|
855
|
+
```html
|
|
856
|
+
<lyra-time-range min="0" max="1440" start="480" end="1020" step="15"></lyra-time-range>
|
|
857
|
+
<script>
|
|
858
|
+
document.querySelector('lyra-time-range')
|
|
859
|
+
.addEventListener('lyra-change', (e) => console.log(e.detail.start, e.detail.end));
|
|
860
|
+
</script>
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
**Known gotchas:**
|
|
864
|
+
- Keyboard support is ArrowUp/Right and ArrowDown/Left only (moves by `step`) — no
|
|
865
|
+
Home/End/PageUp/PageDown, unlike native `<input type=range>` or the WAI-ARIA slider pattern it
|
|
866
|
+
otherwise follows.
|
|
867
|
+
- A disabled handle loses `tabindex` but never gets `aria-disabled` — a screen-reader user
|
|
868
|
+
exploring by virtual cursor still hears it announced as a live, adjustable slider.
|
|
869
|
+
- No `aria-valuetext`: only raw numeric `aria-valuenow` is exposed, no hook for a human-readable
|
|
870
|
+
(e.g. formatted date/time) equivalent of the mapped domain.
|
|
871
|
+
- Handles a `min > max` domain and disabled-mid-drag/disconnect-mid-drag correctly (tested) — safe
|
|
872
|
+
to rely on those edge cases.
|
|
873
|
+
|
|
874
|
+
---
|
|
875
|
+
|
|
876
|
+
## `lyra-playback`
|
|
877
|
+
|
|
878
|
+
Steps an index through `[0, length)` on a fixed interval — play/pause for time-series scrubbing.
|
|
879
|
+
|
|
880
|
+
**Properties:**
|
|
881
|
+
- `length: number = 0`
|
|
882
|
+
- `index: number = 0`
|
|
883
|
+
- `intervalMs: number = 900` (attribute `interval-ms`)
|
|
884
|
+
- `playing: boolean = false` (reflected)
|
|
885
|
+
- `loop: boolean = true`
|
|
886
|
+
- `hidden: boolean = false` (reflected; re-declared over the native IDL property so Lit's
|
|
887
|
+
change-tracking sees it and auto-pauses on `hidden = true`)
|
|
888
|
+
|
|
889
|
+
**Methods:** `play()`, `pause()`, `toggle()`, `next()`, `previous()`, `goTo(index: number)` — all
|
|
890
|
+
idempotent/clamped; `length <= 1` is a no-op degenerate case.
|
|
891
|
+
|
|
892
|
+
**Events:** `lyra-play`, `lyra-pause` (no detail), `lyra-step` (`detail: { index }`, fired on every
|
|
893
|
+
tick and manual step)
|
|
894
|
+
|
|
895
|
+
**Slots:** none.
|
|
896
|
+
|
|
897
|
+
**CSS parts:** `base`, `play-button`, `slider`
|
|
898
|
+
|
|
899
|
+
**Themeable custom properties:** shared tokens only — `--lyra-space-s`, `--lyra-color-border`,
|
|
900
|
+
`--lyra-color-surface`, `--lyra-color-text`, `--lyra-color-brand`.
|
|
901
|
+
|
|
902
|
+
**Optional peer deps:** none.
|
|
903
|
+
|
|
904
|
+
```html
|
|
905
|
+
<lyra-playback length="24" interval-ms="500"></lyra-playback>
|
|
906
|
+
<script>
|
|
907
|
+
const pb = document.querySelector('lyra-playback');
|
|
908
|
+
pb.addEventListener('lyra-step', (e) => renderFrame(e.detail.index));
|
|
909
|
+
</script>
|
|
910
|
+
```
|
|
911
|
+
|
|
912
|
+
**Known gotchas:**
|
|
913
|
+
- `index` is never re-clamped when `length` shrinks below it — e.g. setting `el.length = 2` while
|
|
914
|
+
`el.index = 7` leaves `index` out of range until the next `tick()`/`goTo()`/`next()`/`previous()`
|
|
915
|
+
call happens to correct it.
|
|
916
|
+
- No `aria-valuetext`/visible "N of M" position label on the range input.
|
|
917
|
+
- Calling `play()`/`pause()` programmatically (not via the button) gives no `aria-live`
|
|
918
|
+
announcement of the Play/Pause state change.
|
|
919
|
+
|
|
920
|
+
---
|
|
921
|
+
|
|
922
|
+
## `lyra-heatmap`
|
|
923
|
+
|
|
924
|
+
A Canvas-rendered **matrix** heatmap (rows × cols grid) with a DPR-aware, resize-aware redraw loop.
|
|
925
|
+
**Matrix layout only — despite the family sometimes being described as covering "matrix + calendar
|
|
926
|
+
heatmap," no calendar/day-grid layout exists in the shipped source.** Do not assume a
|
|
927
|
+
calendar-heatmap mode is available; it would need to be built as a new variant.
|
|
928
|
+
|
|
929
|
+
**Properties:**
|
|
930
|
+
- `rowLabels: string[] = []` (attribute: false)
|
|
931
|
+
- `colLabels: string[] = []` (attribute: false)
|
|
932
|
+
- `values: number[][] = []` (attribute: false) — `-1` is the "no data" sentinel; ragged/sparse rows
|
|
933
|
+
are safe (`?? -1`)
|
|
934
|
+
- `cellSize: number = 22` (attribute `cell-size`)
|
|
935
|
+
- `valueLabel: string = 'value'` (attribute `value-label`)
|
|
936
|
+
- `scale: 'linear' | 'sqrt' = 'linear'`
|
|
937
|
+
|
|
938
|
+
**Events:** none dispatched.
|
|
939
|
+
|
|
940
|
+
**Slots:** none.
|
|
941
|
+
|
|
942
|
+
**CSS parts:** `base`, `canvas`, `legend`
|
|
943
|
+
|
|
944
|
+
**Themeable custom properties:** `--lyra-heatmap-scale-lo` (default `#cde2fb`),
|
|
945
|
+
`--lyra-heatmap-scale-hi` (default `#0969da`) — the sequential color-ramp endpoints, resolved via
|
|
946
|
+
`getComputedStyle` each draw (any valid CSS color syntax — hex/rgb/hsl/oklch/named — works,
|
|
947
|
+
resolved through a scratch canvas). Also consumes `--lyra-color-text-quiet` (axis label color) and
|
|
948
|
+
`--lyra-space-xs`.
|
|
949
|
+
|
|
950
|
+
**Optional peer deps:** none.
|
|
951
|
+
|
|
952
|
+
```html
|
|
953
|
+
<lyra-heatmap value-label="requests"></lyra-heatmap>
|
|
954
|
+
<script>
|
|
955
|
+
const hm = document.querySelector('lyra-heatmap');
|
|
956
|
+
hm.rowLabels = ['Mon', 'Tue', 'Wed'];
|
|
957
|
+
hm.colLabels = ['00h', '06h', '12h', '18h'];
|
|
958
|
+
hm.values = [[3, 8, 12, 4], [1, 2, 9, 5], [0, 4, 6, 2]];
|
|
959
|
+
</script>
|
|
960
|
+
```
|
|
961
|
+
|
|
962
|
+
**Known gotchas:**
|
|
963
|
+
- **High** — the `ResizeObserver` is wired but effectively inert: `draw()` always sizes the canvas
|
|
964
|
+
as `PAD_LEFT + cols * cellSize`, never from the host's measured width, so a container resize
|
|
965
|
+
redraws identical dimensions. The stylesheet's `canvas { inline-size: 100% }` is also dead code —
|
|
966
|
+
`draw()` unconditionally sets an inline `canvas.style.width/height` on every redraw, and inline
|
|
967
|
+
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`
|
|
969
|
+
is `false`, so it falls into the normal color-ramp branch, produces an invalid `fillStyle`, and
|
|
970
|
+
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
|
|
972
|
+
leaks one stale `MediaQueryList` listener per DPR crossing (each `onDprChange` creates a new
|
|
973
|
+
listener without removing the previous one).
|
|
974
|
+
- Per-cell values are not individually inspectable by assistive tech — the whole grid is one
|
|
975
|
+
`role="img"` with only a dimensions+range summary `aria-label`.
|
|
976
|
+
|
|
977
|
+
---
|
|
978
|
+
|
|
979
|
+
## `lyra-graph`
|
|
980
|
+
|
|
981
|
+
A force-directed node-link diagram with pan/zoom/drag, built on `d3-force`.
|
|
982
|
+
|
|
983
|
+
**Properties:**
|
|
984
|
+
- `nodes: GraphNode[] = []` (attribute: false) — `GraphNode { id: string; label?: string; radius?:
|
|
985
|
+
number; color?: string }`
|
|
986
|
+
- `links: GraphLink[] = []` (attribute: false) — `GraphLink { source: string; target: string;
|
|
987
|
+
width?: number }` (source/target are node ids; links whose endpoints don't resolve are silently
|
|
988
|
+
dropped)
|
|
989
|
+
- `width: number = 800`
|
|
990
|
+
- `height: number = 600`
|
|
991
|
+
- `chargeStrength: number = -300` (attribute `charge-strength`)
|
|
992
|
+
- `linkDistance: number = 100` (attribute `link-distance`)
|
|
993
|
+
|
|
994
|
+
**Events:** `lyra-node-click` (`detail: { id }`), `lyra-link-click` (`detail: { source, target }`)
|
|
995
|
+
|
|
996
|
+
**Slots:** none.
|
|
997
|
+
|
|
998
|
+
**CSS parts:** `base`, `svg`, `node`, `link`, `label`
|
|
999
|
+
|
|
1000
|
+
**Themeable custom properties:** `--lyra-node-fill` (set inline per-node from `GraphNode.color`,
|
|
1001
|
+
falls back to `--lyra-color-brand`); also uses `--lyra-color-border`, `--lyra-color-text`.
|
|
1002
|
+
|
|
1003
|
+
**Optional peer deps:** `d3-force`, `d3-drag`, `d3-zoom`, `d3-selection` (all four required
|
|
1004
|
+
together; lazy-`import()`ed once per page, `console.warn` once and renders empty if missing —
|
|
1005
|
+
install with `pnpm add d3-force d3-drag d3-zoom d3-selection`).
|
|
1006
|
+
|
|
1007
|
+
```html
|
|
1008
|
+
<lyra-graph style="display:block;height:500px"></lyra-graph>
|
|
1009
|
+
<script>
|
|
1010
|
+
const g = document.querySelector('lyra-graph');
|
|
1011
|
+
g.nodes = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }];
|
|
1012
|
+
g.links = [{ source: 'a', target: 'b' }];
|
|
1013
|
+
g.addEventListener('lyra-node-click', (e) => console.log(e.detail.id));
|
|
1014
|
+
</script>
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
**Known gotchas:**
|
|
1018
|
+
- **High** — per-tick full re-render is expensive: every d3-force tick (up to ~300 by default,
|
|
1019
|
+
continuously while dragging via `alphaTarget(0.3)`) replaces `simNodes`/`simLinks` with fresh
|
|
1020
|
+
array references, forcing Lit to re-diff the entire node/link template and re-run
|
|
1021
|
+
`applyInteractions()`. Noticeable cost for graphs of a few hundred+ nodes.
|
|
1022
|
+
- **Medium** — `chargeStrength`/`linkDistance` are **not** live-reactive: changing them post-mount
|
|
1023
|
+
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
|
|
1025
|
+
with no keyboard equivalent. Links (`<line part="link">`) are click-only — no
|
|
1026
|
+
`role`/`tabindex`/`aria-label`, unlike nodes which are properly keyboard-operable
|
|
1027
|
+
(`tabindex="0"`, `role="button"`, Enter/Space).
|
|
1028
|
+
- No visible fallback if the d3 peers fail to load — just a permanently empty `<svg>` plus a
|
|
1029
|
+
one-time console warning.
|
|
1030
|
+
|
|
1031
|
+
---
|
|
1032
|
+
|
|
1033
|
+
## `lyra-tree` / `lyra-tree-node`
|
|
1034
|
+
|
|
1035
|
+
An expand/collapse hierarchy (document/graph navigation tree). `lyra-tree` owns the data and
|
|
1036
|
+
imperatively creates/reconciles light-DOM `<lyra-tree-node>` children by `id`; `lyra-tree-node`
|
|
1037
|
+
recursively renders itself and its own nested children.
|
|
1038
|
+
|
|
1039
|
+
### `lyra-tree`
|
|
1040
|
+
|
|
1041
|
+
**Properties:**
|
|
1042
|
+
- `data: TreeItem[] = []` (attribute: false) — `TreeItem { id: string; label: string; children?:
|
|
1043
|
+
TreeItem[]; badge?: string | number }`
|
|
1044
|
+
|
|
1045
|
+
**Methods:** `expandAll()`, `collapseAll()` (both recursive, properly sequenced around Lit's render
|
|
1046
|
+
cycle).
|
|
1047
|
+
|
|
1048
|
+
**Events:** none dispatched directly (see `lyra-tree-node` below — they bubble up).
|
|
1049
|
+
|
|
1050
|
+
**Slots:** default (holds the `<lyra-tree-node>` elements it manages).
|
|
1051
|
+
|
|
1052
|
+
**CSS parts:** `base`
|
|
1053
|
+
|
|
1054
|
+
**Themeable custom properties:** shared tokens `--lyra-space-xs`/`-s`, `--lyra-color-brand-quiet`,
|
|
1055
|
+
`--lyra-color-text-quiet`, `--lyra-color-border`, `--lyra-radius`.
|
|
1056
|
+
|
|
1057
|
+
**Optional peer deps:** none.
|
|
1058
|
+
|
|
1059
|
+
### `lyra-tree-node`
|
|
1060
|
+
|
|
1061
|
+
Normally set internally by `lyra-tree`, but a public element.
|
|
1062
|
+
|
|
1063
|
+
**Properties:**
|
|
1064
|
+
- `item: TreeItem` (required, attribute: false)
|
|
1065
|
+
- `depth: number = 0`
|
|
1066
|
+
- `expanded: boolean = false` (reflected)
|
|
1067
|
+
|
|
1068
|
+
**Events:** `lyra-node-toggle` (`detail: { id, expanded }`), `lyra-node-select` (`detail: { id }`) —
|
|
1069
|
+
dispatched from `lyra-tree-node`, bubble/compose up through `lyra-tree`'s light DOM.
|
|
1070
|
+
|
|
1071
|
+
**Slots:** none.
|
|
1072
|
+
|
|
1073
|
+
**CSS parts:** `row`, `toggle`, `label`, `badge`
|
|
1074
|
+
|
|
1075
|
+
**Themeable custom properties:** `--lyra-tree-depth` (internal, set inline per row for
|
|
1076
|
+
indentation), plus the shared tokens listed above.
|
|
1077
|
+
|
|
1078
|
+
**Optional peer deps:** none.
|
|
1079
|
+
|
|
1080
|
+
```html
|
|
1081
|
+
<lyra-tree></lyra-tree>
|
|
1082
|
+
<script>
|
|
1083
|
+
document.querySelector('lyra-tree').data = [
|
|
1084
|
+
{ id: '1', label: 'Root', children: [{ id: '1a', label: 'Child A' }, { id: '1b', label: 'Child B', badge: 3 }] },
|
|
1085
|
+
];
|
|
1086
|
+
</script>
|
|
1087
|
+
```
|
|
1088
|
+
|
|
1089
|
+
**Known gotchas:**
|
|
1090
|
+
- **High** — the treeitem row has **zero keyboard interaction**: `role="treeitem"` has no
|
|
1091
|
+
`tabindex` and no keydown handler, so it never enters the Tab order. The only keyboard-operable
|
|
1092
|
+
control at all is the expand/collapse `<button>` — and that's removed (`tabindex="-1"`,
|
|
1093
|
+
`aria-hidden`) for leaf nodes. `lyra-node-select` fires only from the label's `@click`, with no
|
|
1094
|
+
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
|
|
1096
|
+
descendant, of its `role="treeitem"` row — violates the WAI-ARIA treeitem pattern's
|
|
1097
|
+
DOM-containment expectation. (The library's own axe test does not catch this — axe's structural
|
|
1098
|
+
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)
|
|
1100
|
+
only applies at depth 0. Nested `children` arrays are rendered via a plain `.map()` with no keyed
|
|
1101
|
+
`repeat()`, so reordering/inserting into a nested array can silently attach the wrong node's
|
|
1102
|
+
`expanded` state.
|
|
1103
|
+
- `role="tree"` has no accessible name (`aria-label`/`aria-labelledby`).
|
|
1104
|
+
|
|
1105
|
+
---
|
|
1106
|
+
|
|
1107
|
+
# Chart family (`chart/`)
|
|
1108
|
+
|
|
1109
|
+
## `lyra-chart` (core)
|
|
1110
|
+
|
|
1111
|
+
Chart.js wrapper every other `lyra-*-chart` tag subclasses; supports both a simplified
|
|
1112
|
+
`Series`-based attribute surface and a raw Chart.js `config` passthrough (mirrors Web Awesome's
|
|
1113
|
+
`wa-chart` `config` property).
|
|
1114
|
+
|
|
1115
|
+
**Properties:**
|
|
1116
|
+
- `type: LyraChartType = 'line'` — `LyraChartType = 'line' | 'bar' | 'scatter'` (narrow TS union;
|
|
1117
|
+
other Chart.js type strings — `'pie'`, `'doughnut'`, etc. — work at runtime unchecked, which is
|
|
1118
|
+
exactly how the typed subclasses below achieve their locked type)
|
|
1119
|
+
- `labels: string[] = []` (attribute: false)
|
|
1120
|
+
- `datasets: Series[] = []` (attribute: false) — `Series { label: string; data?: (number|null)[];
|
|
1121
|
+
points?: {x,y,label?}[]; color?: string|string[]; fill?: boolean; width?: number; dash?: boolean;
|
|
1122
|
+
noTooltip?: boolean; axis?: 'y'|'y2'; pointColors?: string[]; pointRadius?: number; type?:
|
|
1123
|
+
'line'|'bar' }`
|
|
1124
|
+
- `legend: boolean = false`
|
|
1125
|
+
- `area: boolean = false`
|
|
1126
|
+
- `zoom: boolean = false`
|
|
1127
|
+
- `height: string = '280px'`
|
|
1128
|
+
- `xLabel: string = ''` (attribute `x-label`)
|
|
1129
|
+
- `yLabel: string = ''` (attribute `y-label`)
|
|
1130
|
+
- `y2Label: string = ''` (attribute `y2-label`)
|
|
1131
|
+
- `beginAtZero: boolean = true` (attribute `begin-at-zero`)
|
|
1132
|
+
- `config?: Partial<ChartConfiguration>` (attribute: false) — deep-merged over the generated
|
|
1133
|
+
config; any nested key wins without clobbering sibling generated keys
|
|
1134
|
+
|
|
1135
|
+
**Methods:** `resetZoom()`
|
|
1136
|
+
|
|
1137
|
+
**Events:** `lyra-zoom` (`detail: { zoomed: boolean }`, fired on zoom-complete and on
|
|
1138
|
+
`resetZoom()`)
|
|
1139
|
+
|
|
1140
|
+
**Slots:** none.
|
|
1141
|
+
|
|
1142
|
+
**CSS parts:** `base`, `canvas`, `reset-zoom-button`
|
|
1143
|
+
|
|
1144
|
+
**Themeable custom properties:** `--lyra-chart-height` (set programmatically on the host from the
|
|
1145
|
+
`height` property — must be read from the host, not a shadow-tree descendant, since custom
|
|
1146
|
+
properties only cascade downward); shared tokens `--lyra-space-xs`, `--lyra-color-border`,
|
|
1147
|
+
`--lyra-color-surface`, `--lyra-color-text`.
|
|
1148
|
+
|
|
1149
|
+
**Optional peer deps:** `chart.js`, `chartjs-plugin-zoom` (lazy-loaded once per page via
|
|
1150
|
+
`chart-loader.ts`; registers only the tree-shaken controller/element/scale subset used).
|
|
1151
|
+
|
|
1152
|
+
```html
|
|
1153
|
+
<lyra-chart type="line" x-label="Day" y-label="kWh" legend></lyra-chart>
|
|
1154
|
+
<script>
|
|
1155
|
+
const c = document.querySelector('lyra-chart');
|
|
1156
|
+
c.labels = ['Mon', 'Tue', 'Wed'];
|
|
1157
|
+
c.datasets = [{ label: 'Production', data: [12, 19, 7], color: '#2563eb' }];
|
|
1158
|
+
</script>
|
|
1159
|
+
```
|
|
1160
|
+
|
|
1161
|
+
**Known gotchas:**
|
|
1162
|
+
- **Medium** — "locked" `type` in the typed subclasses (below) is **not actually enforced**:
|
|
1163
|
+
`<lyra-pie-chart type="bar">` or `el.type = 'bar'` at runtime silently turns it into a bar chart.
|
|
1164
|
+
Conversely, `<lyra-chart type="pie">` already works today with no subclass needed at all —
|
|
1165
|
+
`buildConfig()` passes `type` straight through unchecked.
|
|
1166
|
+
- **Medium** — the `lyra-theme` window-event listener (meant for re-theming on theme switch) is
|
|
1167
|
+
dead code: nothing in the library ever dispatches that event, and even if it were dispatched, no
|
|
1168
|
+
`--lyra-*`/`getComputedStyle` color resolution happens anywhere in this component — Chart.js
|
|
1169
|
+
always renders grid/tick/legend/tooltip text with its own hardcoded palette, which can read as
|
|
1170
|
+
low-contrast in dark mode.
|
|
1171
|
+
- **Medium** — generated `scales` config is cartesian (`x`/`y`/`y2`) only: for `lyra-radar-chart`/
|
|
1172
|
+
`lyra-polar-area-chart` (which need a single radial `r` scale) and for `lyra-pie-chart`/
|
|
1173
|
+
`lyra-doughnut-chart` (no scale at all), the inherited `xLabel`/`yLabel`/`beginAtZero`/`y2Label`
|
|
1174
|
+
properties are silently inert with no warning. Reach the radial scale only via raw `config`.
|
|
1175
|
+
- No `chartjs-plugin-annotation` is registered by default — reachable only by importing it
|
|
1176
|
+
separately and using the raw `config` passthrough (Chart.js's registry is a global singleton).
|
|
1177
|
+
|
|
1178
|
+
---
|
|
1179
|
+
|
|
1180
|
+
## Typed subclasses: `lyra-line-chart`, `lyra-bar-chart`, `lyra-pie-chart`, `lyra-doughnut-chart`, `lyra-radar-chart`, `lyra-polar-area-chart`, `lyra-bubble-chart`, `lyra-scatter-chart`
|
|
1181
|
+
|
|
1182
|
+
Each is `LyraChart` with `type` overridden to a fixed value; all other properties/events/parts/tokens
|
|
1183
|
+
are identical to `lyra-chart` above. Same optional peer deps (`chart.js`, `chartjs-plugin-zoom`).
|
|
1184
|
+
|
|
1185
|
+
```html
|
|
1186
|
+
<lyra-bar-chart legend></lyra-bar-chart>
|
|
1187
|
+
<lyra-pie-chart></lyra-pie-chart>
|
|
1188
|
+
<script>
|
|
1189
|
+
document.querySelector('lyra-bar-chart').labels = ['A', 'B'];
|
|
1190
|
+
document.querySelector('lyra-bar-chart').datasets = [{ label: 'Count', data: [4, 9] }];
|
|
1191
|
+
|
|
1192
|
+
document.querySelector('lyra-pie-chart').labels = ['A', 'B', 'C'];
|
|
1193
|
+
document.querySelector('lyra-pie-chart').datasets = [{ label: 'Share', data: [30, 45, 25], color: ['#2563eb', '#16a34a', '#dc2626'] }];
|
|
1194
|
+
</script>
|
|
1195
|
+
```
|
|
1196
|
+
|
|
1197
|
+
**Known gotchas (in addition to the core `lyra-chart` list above):**
|
|
1198
|
+
- `lyra-bubble-chart` needs `Series.points` entries with an `x`/`y`/`r` triple, but `Series.points`
|
|
1199
|
+
is typed as `{x, y, label?}[]` with no `r` field — cast through `unknown` (or a local
|
|
1200
|
+
`BubblePoint` type) when constructing bubble data.
|
|
1201
|
+
- Reading `chart.type` back in TypeScript for the 5 non-`line`/`bar`/`scatter` subclasses yields a
|
|
1202
|
+
value that, by `LyraChartType`'s own declaration, is never a legal member of that type (each is
|
|
1203
|
+
set via `as unknown as LyraChartType`).
|
|
1204
|
+
|
|
1205
|
+
---
|
|
1206
|
+
|
|
1207
|
+
## `lyra-histogram`
|
|
1208
|
+
|
|
1209
|
+
Bins `values` into `bins` equal-width buckets and renders as a bar chart (extends `LyraChart`,
|
|
1210
|
+
`type` fixed to `'bar'`).
|
|
1211
|
+
|
|
1212
|
+
**Properties:**
|
|
1213
|
+
- `bins: number = 10`
|
|
1214
|
+
- `values: number[] = []` (attribute: false)
|
|
1215
|
+
- `label: string = 'Frequency'`
|
|
1216
|
+
- `labels`/`datasets` are **derived, read-only** (installed as getter/setter pairs on the
|
|
1217
|
+
prototype; direct writes are silently ignored) — computed from `values`/`bins`.
|
|
1218
|
+
- All other `LyraChart` properties (`legend`, `area`, `zoom`, `config`, `height`, etc.) are
|
|
1219
|
+
inherited and usable.
|
|
1220
|
+
|
|
1221
|
+
**Events/parts/tokens/peers:** identical to `lyra-chart`.
|
|
1222
|
+
|
|
1223
|
+
```html
|
|
1224
|
+
<lyra-histogram bins="12"></lyra-histogram>
|
|
1225
|
+
<script>
|
|
1226
|
+
document.querySelector('lyra-histogram').values = [1, 2, 2, 3, 5, 5, 5, 8, 13, 13];
|
|
1227
|
+
</script>
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
**Known gotchas:**
|
|
1231
|
+
- **High** — **`bins <= 0` crashes** on non-empty `values`: `bins=0` throws `TypeError` inside
|
|
1232
|
+
`binValues()` (bucket index resolves to `-1`, `buckets[-1].count++` fails); negative `bins` throws
|
|
1233
|
+
`RangeError` even earlier (`Array.from({length: -1})`). No guard exists anywhere in the chain —
|
|
1234
|
+
**always pass `bins >= 1`.**
|
|
1235
|
+
|
|
1236
|
+
---
|
|
1237
|
+
|
|
1238
|
+
## `lyra-box-plot`
|
|
1239
|
+
|
|
1240
|
+
Box-and-whisker chart from a precomputed five-number summary (no raw sample data sent to the
|
|
1241
|
+
browser). Does **not** extend `LyraChart` — a deliberately bespoke API.
|
|
1242
|
+
|
|
1243
|
+
**Properties:**
|
|
1244
|
+
- `labels: string[] = []` (attribute: false)
|
|
1245
|
+
- `boxes: BoxPlotSeries[] = []` (attribute: false) — `BoxPlotSeries { label: string; data:
|
|
1246
|
+
BoxPlotPoint[]; color?: string }`, `BoxPlotPoint { min, q1, median, q3, max }`
|
|
1247
|
+
- `legend: boolean = false`
|
|
1248
|
+
- `height: string = '280px'`
|
|
1249
|
+
- `yLabel: string = ''` (attribute `y-label`)
|
|
1250
|
+
- `beginAtZero: boolean = true` (attribute `begin-at-zero`)
|
|
1251
|
+
|
|
1252
|
+
**Events:** none.
|
|
1253
|
+
|
|
1254
|
+
**Slots:** none.
|
|
1255
|
+
|
|
1256
|
+
**CSS parts:** `base`, `canvas`
|
|
1257
|
+
|
|
1258
|
+
**Themeable custom properties:** `--lyra-chart-height` (same mechanism as `lyra-chart`; styles file
|
|
1259
|
+
is a re-export of `chart.styles.ts`).
|
|
1260
|
+
|
|
1261
|
+
**Optional peer deps:** `@sgratzl/chartjs-chart-boxplot` (plus `chart.js` transitively, loaded
|
|
1262
|
+
independently of the base `chart-loader.ts`).
|
|
1263
|
+
|
|
1264
|
+
```html
|
|
1265
|
+
<lyra-box-plot y-label="Latency (ms)"></lyra-box-plot>
|
|
1266
|
+
<script>
|
|
1267
|
+
const bp = document.querySelector('lyra-box-plot');
|
|
1268
|
+
bp.labels = ['Run A', 'Run B'];
|
|
1269
|
+
bp.boxes = [{ label: 'p50–p99', data: [{ min: 10, q1: 20, median: 30, q3: 45, max: 90 }, { min: 12, q1: 18, median: 25, q3: 35, max: 60 }] }];
|
|
1270
|
+
</script>
|
|
1271
|
+
```
|
|
1272
|
+
|
|
1273
|
+
**Known gotchas:**
|
|
1274
|
+
- **Medium** — no raw `config` passthrough (unlike every other chart tag) — limited to
|
|
1275
|
+
`labels`/`boxes`/`legend`/`height`/`yLabel`/`beginAtZero`; can't reach the underlying
|
|
1276
|
+
controller's own options (`itemRadius`, `outlierRadius`, `coef`).
|
|
1277
|
+
- The peer-dependency-missing failure path (`.catch()` → warn + resolve `null`) is untested —
|
|
1278
|
+
verify it manually if shipping without the peer installed.
|
|
1279
|
+
|
|
1280
|
+
---
|
|
1281
|
+
|
|
1282
|
+
# Map & file-input
|
|
1283
|
+
|
|
1284
|
+
## `lyra-map`
|
|
1285
|
+
|
|
1286
|
+
A `maplibre-gl` wrapper with a declarative legend and a single choropleth GeoJSON fill layer, plus a
|
|
1287
|
+
raw `map` escape hatch for anything unexposed.
|
|
1288
|
+
|
|
1289
|
+
**Properties:**
|
|
1290
|
+
- `center: [number, number] = [0, 0]`
|
|
1291
|
+
- `zoom: number = 2`
|
|
1292
|
+
- `mapStyle: StyleSpecification | string = DEFAULT_STYLE` (attribute: false) — default is a basic
|
|
1293
|
+
OSM raster tile style
|
|
1294
|
+
- `legend: LegendEntry[] = []` (attribute: false) — `LegendEntry { color: string; label: string }`
|
|
1295
|
+
(discrete swatch rows only, no continuous gradient bar)
|
|
1296
|
+
- `choropleth?: ChoroplethLayer` (attribute: false) — `ChoroplethLayer { sourceId: string; geojson:
|
|
1297
|
+
GeoJSON.FeatureCollection; field: string; stops: [number, string][] }` (interpolated
|
|
1298
|
+
fill-color expression from `field`'s value against `stops`)
|
|
1299
|
+
|
|
1300
|
+
**Getters:** `map` → the raw `maplibregl.Map` instance.
|
|
1301
|
+
|
|
1302
|
+
**Events:** `lyra-map-load` (fired once, after the underlying map's own `'load'`), `lyra-map-click`
|
|
1303
|
+
(`detail: { lngLat: [lng, lat], feature? }` — feature only populated if a choropleth fill layer
|
|
1304
|
+
exists and was hit)
|
|
1305
|
+
|
|
1306
|
+
**Slots:** none.
|
|
1307
|
+
|
|
1308
|
+
**CSS parts:** `base`, `container`, `legend`, `legend-swatch`
|
|
1309
|
+
|
|
1310
|
+
**Themeable custom properties:** shared tokens only — `--lyra-space-s`, `--lyra-color-surface`,
|
|
1311
|
+
`--lyra-color-border`, `--lyra-shadow`, `--lyra-radius`.
|
|
1312
|
+
|
|
1313
|
+
**Optional peer deps:** `maplibre-gl` (lazy-loaded; consumer must **separately** `import
|
|
1314
|
+
'maplibre-gl/dist/maplibre-gl.css'` once — the component does not do this for you).
|
|
1315
|
+
|
|
1316
|
+
```html
|
|
1317
|
+
<lyra-map center="[2.35, 48.85]" zoom="10"></lyra-map>
|
|
1318
|
+
<script>
|
|
1319
|
+
import 'maplibre-gl/dist/maplibre-gl.css';
|
|
1320
|
+
const m = document.querySelector('lyra-map');
|
|
1321
|
+
m.choropleth = {
|
|
1322
|
+
sourceId: 'regions',
|
|
1323
|
+
geojson: myGeoJson,
|
|
1324
|
+
field: 'value',
|
|
1325
|
+
stops: [[0, '#cde2fb'], [100, '#0969da']],
|
|
1326
|
+
};
|
|
1327
|
+
m.legend = [{ color: '#cde2fb', label: 'Low' }, { color: '#0969da', label: 'High' }];
|
|
1328
|
+
m.addEventListener('lyra-map-click', (e) => console.log(e.detail.feature?.properties));
|
|
1329
|
+
</script>
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
**Known gotchas:**
|
|
1333
|
+
- **Medium** — clearing or swapping the choropleth **leaks the old layer**: `applyChoropleth()`
|
|
1334
|
+
only ever adds or updates; setting `choropleth = undefined`, or changing `choropleth.sourceId` to
|
|
1335
|
+
a different value, is a silent no-op — the previously added GeoJSON source and fill layer keep
|
|
1336
|
+
rendering forever (there is no `removeLayer`/`removeSource` call anywhere).
|
|
1337
|
+
- **Medium** — `mapStyle` is reactive but changing it after construction is a **silent no-op** —
|
|
1338
|
+
unlike `center`/`zoom`, which do call `setCenter`/`setZoom` on change, there's no `setStyle()`
|
|
1339
|
+
call wired up.
|
|
1340
|
+
- No declarative point-marker API — only the single choropleth polygon-fill layer exists; placing
|
|
1341
|
+
pins/points requires dropping to the `.map` escape hatch and calling `new maplibregl.Marker()`
|
|
1342
|
+
manually.
|
|
1343
|
+
- No built-in popup rendering (despite sometimes being described as covering "legend/popup/
|
|
1344
|
+
choropleth") — only the raw `lyra-map-click` event; hand-roll your own `maplibregl.Popup` on
|
|
1345
|
+
click.
|
|
1346
|
+
- No click-select visual highlight on choropleth features (no `setFeatureState` call) — clicking
|
|
1347
|
+
only fires the event, no built-in visual feedback.
|
|
1348
|
+
|
|
1349
|
+
---
|
|
1350
|
+
|
|
1351
|
+
## `lyra-file-input`
|
|
1352
|
+
|
|
1353
|
+
A drag-drop + click-to-browse file dropzone. Emits raw `File[]` only — no client-side CSV/XLSX/etc.
|
|
1354
|
+
parsing (that's left entirely to the host).
|
|
1355
|
+
|
|
1356
|
+
**Properties:**
|
|
1357
|
+
- `multiple: boolean = false` (reflected)
|
|
1358
|
+
- `disabled: boolean = false` (reflected)
|
|
1359
|
+
- `accept: string = ''` — **see gotchas: decorative on the drop path**
|
|
1360
|
+
- `allowedMimeTypes: string[] = []` (attribute: false) — exact MIME-string allowlist
|
|
1361
|
+
- `forbiddenMimeTypes: string[] = []` (attribute: false) — exact MIME-string denylist, checked
|
|
1362
|
+
**before** (and takes precedence over) `allowedMimeTypes`
|
|
1363
|
+
- `label: string = 'Drop files here or click to browse'`
|
|
1364
|
+
|
|
1365
|
+
**Methods:** `openPicker()` — programmatically opens the native file dialog.
|
|
1366
|
+
|
|
1367
|
+
**Events:** `lyra-files` (`detail: { files: File[], rejected: File[] }`, fired on both drop and
|
|
1368
|
+
manual file-picker selection — `rejected` carries no reason code, just the raw rejected `File[]`)
|
|
1369
|
+
|
|
1370
|
+
**Slots:** default slot — custom dropzone content, overrides the `label` attribute text when
|
|
1371
|
+
provided.
|
|
1372
|
+
|
|
1373
|
+
**CSS parts:** `base`, `input`
|
|
1374
|
+
|
|
1375
|
+
**Themeable custom properties:** shared tokens only — `--lyra-space-xs`, `--lyra-space-l`,
|
|
1376
|
+
`--lyra-color-border`, `--lyra-radius`, `--lyra-color-surface`, `--lyra-color-text-quiet`,
|
|
1377
|
+
`--lyra-color-success` (drag-accept state), `--lyra-color-danger` (drag-reject state).
|
|
1378
|
+
|
|
1379
|
+
**Optional peer deps:** none.
|
|
1380
|
+
|
|
1381
|
+
```html
|
|
1382
|
+
<lyra-file-input multiple accept=".csv,.xlsx" allowed-mime-types='["text/csv"]'></lyra-file-input>
|
|
1383
|
+
<script>
|
|
1384
|
+
document.querySelector('lyra-file-input').addEventListener('lyra-files', (e) => {
|
|
1385
|
+
console.log('accepted:', e.detail.files, 'rejected:', e.detail.rejected);
|
|
1386
|
+
});
|
|
1387
|
+
</script>
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
Note: `allowedMimeTypes`/`forbiddenMimeTypes` are complex properties (`attribute: false`) — set
|
|
1391
|
+
them via JS (`el.allowedMimeTypes = [...]`), not as a JSON string attribute; the snippet above is
|
|
1392
|
+
illustrative of intent only.
|
|
1393
|
+
|
|
1394
|
+
**Known gotchas:**
|
|
1395
|
+
- **High** — **`accept` only constrains the native file-picker dialog; it is silently ignored on
|
|
1396
|
+
the drag-drop path.** `isAllowed()` checks only `allowedMimeTypes`/`forbiddenMimeTypes` (exact
|
|
1397
|
+
MIME-string match — no extension or `image/*`-wildcard support like native `accept` has);
|
|
1398
|
+
`this.accept` itself is never read anywhere else in the component. Concretely:
|
|
1399
|
+
`<lyra-file-input accept=".csv,.xlsx">` with no `allowedMimeTypes` set will silently **accept** a
|
|
1400
|
+
dropped `.png` (fires `lyra-files` with the png in `files`, not `rejected`) even though the UI
|
|
1401
|
+
visually implies only CSV/XLSX are allowed. **To actually enforce type restriction on drop, you
|
|
1402
|
+
must also set `allowedMimeTypes` (and/or `forbiddenMimeTypes`) — `accept` alone is not
|
|
1403
|
+
sufficient.**
|
|
1404
|
+
- No max-file-size validation (no `maxFileSize` property) — a host wanting a byte-limit must
|
|
1405
|
+
intercept `lyra-files` and re-filter itself.
|
|
1406
|
+
- No paste-from-clipboard support (no `paste`-event handling of `e.clipboardData.files`).
|
|
1407
|
+
- No `aria-live` announcement of the drag accept/reject state (`data-drag-state` attribute drives
|
|
1408
|
+
border/background color only, invisible to screen readers mid-drag) — the click/keyboard-to-
|
|
1409
|
+
browse path is unaffected and correctly operable (`role="button"`, `tabindex` 0/-1 by disabled,
|
|
1410
|
+
`aria-disabled`, Enter/Space handling).
|
|
1411
|
+
- Dragged folders aren't specially detected (no `webkitGetAsEntry()` check) — likely surfaces as a
|
|
1412
|
+
phantom zero-byte `File` with an empty MIME type rather than a clear rejection.
|
|
1413
|
+
|
|
1414
|
+
---
|
|
1415
|
+
|
|
1416
|
+
## Cross-cutting notes
|
|
1417
|
+
|
|
1418
|
+
These apply across the whole library rather than to any one component:
|
|
1419
|
+
|
|
1420
|
+
- **Packaging gap.** `custom-elements.json` is currently gitignored and missing from
|
|
1421
|
+
`package.json`'s `files` allowlist, with no `prepack`/publish CI step regenerating it — so a
|
|
1422
|
+
manifest-driven IDE/tooling integration (e.g. custom-elements-manifest-based autocomplete) may
|
|
1423
|
+
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.
|