@aceshooting/lyra-ui 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/custom-elements.json +15118 -8342
- package/dist/components/chart/chart.d.ts +57 -1
- package/dist/components/chart/chart.js +202 -12
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +83 -0
- package/dist/components/chart/lite-chart.js +424 -0
- package/dist/components/chart/lite-chart.stories.d.ts +14 -0
- package/dist/components/chart/lite-chart.stories.js +114 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +76 -13
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +69 -3
- package/dist/components/flag/flag.js +107 -30
- package/dist/components/flag/flag.stories.d.ts +1 -0
- package/dist/components/flag/flag.stories.js +14 -0
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +149 -12
- package/dist/components/heatmap/heatmap.js +445 -28
- package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
- package/dist/components/heatmap/heatmap.stories.js +155 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +125 -0
- package/dist/components/select/select.js +480 -0
- package/dist/components/select/select.stories.d.ts +21 -0
- package/dist/components/select/select.stories.js +91 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +212 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +43 -1
- package/dist/components/stat/stat.js +97 -4
- package/dist/components/stat/stat.stories.d.ts +4 -0
- package/dist/components/stat/stat.stories.js +66 -1
- package/dist/components/stat/stat.styles.js +64 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +25 -1
- package/dist/components/widget/widget.js +164 -6
- package/dist/components/widget/widget.stories.d.ts +3 -0
- package/dist/components/widget/widget.stories.js +43 -0
- package/dist/components/widget/widget.styles.js +20 -3
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +55 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +962 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
package/llms-full.txt
CHANGED
|
@@ -17,10 +17,11 @@ here.
|
|
|
17
17
|
## Contents
|
|
18
18
|
|
|
19
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`
|
|
20
|
+
- **v1 core** — `lyra-combobox`/`lyra-option`, `lyra-select`, `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`, `lyra-widget`, `lyra-word-cloud`
|
|
22
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
|
|
23
|
+
- **Chart family** — `lyra-chart` (core), typed subclasses, `lyra-histogram`, `lyra-box-plot`,
|
|
24
|
+
`lyra-lite-chart` (dependency-free bar/line alternative, no peer deps)
|
|
24
25
|
- **Map & file-input** — `lyra-map`, `lyra-file-input`
|
|
25
26
|
- **Cross-cutting notes** — packaging and adoption caveats that apply across the whole library
|
|
26
27
|
|
|
@@ -41,47 +42,66 @@ Not a custom element itself — infrastructure every `lyra-*` component in the l
|
|
|
41
42
|
properties `name: string`, `value: string` (hand-written accessor via Lit's `noAccessor` so
|
|
42
43
|
`internals.setFormValue()` fires synchronously on assignment, not on the async update cycle),
|
|
43
44
|
`disabled: boolean` (reflected), `required: boolean` (reflected). Provides
|
|
44
|
-
`checkValidity()`/`reportValidity()` (delegate to `internals`), `
|
|
45
|
-
|
|
46
|
-
`
|
|
45
|
+
`checkValidity()`/`reportValidity()` (delegate to `internals`), `formDisabledCallback(disabled)`,
|
|
46
|
+
and a real `updateValidity()` that calls `internals.setValidity({ valueMissing: true }, ...)`
|
|
47
|
+
whenever `required` is set and `value === ''` (re-run on every `value`/`required` change and once
|
|
48
|
+
from `connectedCallback()`) — so `checkValidity()`/`reportValidity()`/`:invalid`/`:user-invalid`
|
|
49
|
+
now reflect real constraint state instead of always reporting valid. `formResetCallback()`
|
|
50
|
+
restores the value captured from the element's original `value` *content attribute* (native
|
|
51
|
+
`defaultValue` semantics), not a hardcoded blank — only a later `setAttribute('value', ...)` or
|
|
52
|
+
declarative markup updates that captured default; assigning the `.value` IDL property never does.
|
|
53
|
+
Only `lyra-date-input` uses this mixin; `lyra-combobox` and `lyra-select` each hand-roll an
|
|
54
|
+
equivalent independently instead (same `setValidity`/default-capture behavior — `lyra-combobox`
|
|
55
|
+
needs its own copy because its value can be an array in `multiple` mode, `lyra-select` because its
|
|
56
|
+
default is captured from a declared-`selected` `<lyra-option>` rather than a `value` content
|
|
57
|
+
attribute — see each component's own gotchas for where its copy diverges from the mixin).
|
|
47
58
|
- **`positioner.ts` → `place(anchor, popup, opts?): () => void`** — thin wrapper over
|
|
48
59
|
`@floating-ui/dom`'s `computePosition` + `autoUpdate`. Forces `strategy: 'fixed'` (to match the
|
|
49
60
|
popup's own `position:fixed` CSS — otherwise it lands offset by the page scroll position),
|
|
50
61
|
middleware `offset(opts.offset ?? 4)`, `flip()`, `shift({ padding: 8 })`, default
|
|
51
62
|
`placement: 'bottom-start'`. Returns a cleanup function that stops the `autoUpdate` loop — call it
|
|
52
|
-
in `disconnectedCallback()`. Used by `lyra-combobox`, `lyra-
|
|
63
|
+
in `disconnectedCallback()`. Used by `lyra-combobox`, `lyra-select`, `lyra-date-input`,
|
|
64
|
+
`lyra-export-button`.
|
|
53
65
|
- **`tokens.styles.ts` → `tokens`** (a `css` template prepended to every component's stylesheet) —
|
|
54
66
|
the whole design-token layer. Every value chains through a `--wa-*` Web Awesome token first, with
|
|
55
67
|
a hard-coded fallback, e.g. `--lyra-color-brand: var(--wa-color-brand-fill-loud, #0969da)`. Full
|
|
56
68
|
set: `--lyra-color-surface / -text / -text-quiet / -border / -brand / -brand-quiet / -success /
|
|
57
|
-
-warning / -danger`, `--lyra-
|
|
58
|
-
|
|
59
|
-
|
|
69
|
+
-warning / -danger`, `--lyra-color-on-brand` (text/icon color for content painted on a solid
|
|
70
|
+
brand-fill background, e.g. selected calendar days — not itself light/dark-swapped, since it's
|
|
71
|
+
white on both fills), `--lyra-space-xs / -s / -m / -l`, `--lyra-radius`, `--lyra-shadow`,
|
|
72
|
+
`--lyra-font`, `--lyra-transition-fast` / `-base` (motion timing every animating component should
|
|
73
|
+
share), `--lyra-opacity-disabled` (one shared disabled-state opacity), `--lyra-focus-ring-width` /
|
|
74
|
+
`-color` / `-offset` (every `:focus-visible` rule should reference these), `--lyra-icon-button-size`
|
|
75
|
+
(minimum tappable box for icon-only buttons; the glyph itself doesn't grow). Also sets
|
|
76
|
+
`:host([hidden]) { display: none !important; }`, an inherited `box-sizing: border-box` reset, and
|
|
77
|
+
a `prefers-color-scheme: dark` fallback that re-points the color tokens' hard-coded defaults at a
|
|
78
|
+
dark palette (only takes effect when no real `--wa-*` theme value is present).
|
|
60
79
|
- **`prefix.ts`** — `LYRA_PREFIX = 'lyra'`; `tag(name)` → `` `lyra-${name}` ``; `defineElement(name,
|
|
61
80
|
ctor)` — idempotent `customElements.define`, safe if a module is evaluated twice.
|
|
62
81
|
- **`a11y.ts`** — `nextId(scope)`: monotonic id generator (e.g. `nextId('combobox-list')` →
|
|
63
82
|
`"lyra-combobox-list-3"`); `srOnly`: a visually-hidden-but-AT-visible CSS class (used by
|
|
64
|
-
`lyra-skeleton`'s "Loading…" text).
|
|
83
|
+
`lyra-skeleton`'s "Loading…" text and `lyra-stat`'s trend announcement).
|
|
84
|
+
- **`icons.ts`** — the shared inline-SVG icon set (`chevronIcon`, `closeIcon`, `playIcon`,
|
|
85
|
+
`pauseIcon`, `calendarIcon`, `expandIcon`), replacing the literal Unicode/emoji glyphs components
|
|
86
|
+
used previously. One 24×24 viewBox/stroke-width per icon, rendered at `1em` so each inherits the
|
|
87
|
+
caller's font size; none bake in a direction — callers rotate the wrapping `part` element via CSS
|
|
88
|
+
for "up"/"open"/etc.
|
|
89
|
+
- **`scroll-lock.ts` → `lockScroll(): () => void`** — ref-counted `document.documentElement`
|
|
90
|
+
scroll lock (used by `lyra-widget`'s fullscreen mode); safe to acquire/release more than once
|
|
91
|
+
concurrently, restores the original `overflow` only once the last outstanding lock releases.
|
|
92
|
+
- **`rtl.ts` → `isRtl(el): boolean`** — reads `getComputedStyle(el).direction === 'rtl'`, for the
|
|
93
|
+
pointer/keyboard math (drag ratios, arrow-key direction) in `lyra-split` and `lyra-time-range`
|
|
94
|
+
that reasons about physical left/right and can't rely on logical CSS properties alone.
|
|
65
95
|
|
|
66
96
|
**Known gotchas:**
|
|
67
|
-
- `
|
|
68
|
-
`
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
- `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
|
-
- 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
|
-
- 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.
|
|
97
|
+
- `formResetCallback()`'s restore-to-constructed-default behavior (both here and in
|
|
98
|
+
`lyra-combobox`'s hand-rolled copy) only captures the default from the `value` *content
|
|
99
|
+
attribute*/declarative markup — a plain property assignment (`el.value = 'x'`) never redefines
|
|
100
|
+
what `form.reset()` restores to, matching native `defaultValue`/`defaultSelected` semantics but
|
|
101
|
+
worth knowing if you expected "whatever was last set" to survive a reset.
|
|
102
|
+
- no shared label/input association helper as such, but `lyra-combobox` and `lyra-date-input` each
|
|
103
|
+
now pair their own `<label part="form-control-label" for=...>` with a matching `id` on the input,
|
|
104
|
+
so clicking the visible label text does focus the field.
|
|
85
105
|
|
|
86
106
|
---
|
|
87
107
|
|
|
@@ -99,27 +119,47 @@ shared `FormAssociated` mixin — see gotchas).
|
|
|
99
119
|
- `multiple: boolean = false` (reflected)
|
|
100
120
|
- `placeholder: string = ''`
|
|
101
121
|
- `disabled: boolean = false` (reflected)
|
|
102
|
-
- `required: boolean = false` (reflected —
|
|
122
|
+
- `required: boolean = false` (reflected — enforced via `internals.setValidity()`; also reflected as
|
|
123
|
+
`aria-required` on `<input part="combobox-input">` immediately, and `aria-invalid` once the field
|
|
124
|
+
has been touched, see gotchas)
|
|
103
125
|
- `name: string = ''`
|
|
104
126
|
- `label: string = ''`
|
|
105
127
|
- `hint: string = ''`
|
|
128
|
+
- `errorText: string = ''` (attribute `error-text` — static error copy shown below the hint;
|
|
129
|
+
overridden by slotted `error` content when provided)
|
|
106
130
|
- `open: boolean = false` (reflected)
|
|
107
131
|
- `withClear: boolean = false` (attribute **`with-clear`** — not `clearable`, see gotchas)
|
|
108
132
|
- `maxOptionsVisible: number = 3` (attribute `max-options-visible` — caps how many selected tags
|
|
109
133
|
show before collapsing to `+N`)
|
|
110
134
|
- `emptyText: string = 'No results'` (attribute `empty-text`)
|
|
111
135
|
- `filter: OptionFilter | null = null` (attribute: false — `(option, query) => boolean`; default
|
|
112
|
-
matches `label`/`searchText` case-insensitively)
|
|
136
|
+
matches `label`/`searchText` case-insensitively; ignored while `source` is set)
|
|
137
|
+
- `source: ComboboxSource | null = null` (attribute: false — `(query: string) =>
|
|
138
|
+
Promise<ComboboxSourceRow[]>`; when set, replaces the light-DOM `<lyra-option>` list with an async
|
|
139
|
+
lookup, debounced ~200ms after each keystroke and re-run on clear/pick. A "Loading…" row is shown
|
|
140
|
+
while a call is in flight; a stale in-flight call that resolves after a newer one (or after
|
|
141
|
+
disconnect) is dropped via a monotonic token)
|
|
142
|
+
- `maxRender: number = 200` (attribute `max-render` — caps how many rows render at once, always
|
|
143
|
+
keeping the current selection visible even if it's outside the cap; the excess shows as an
|
|
144
|
+
"+N more — refine your search" row instead of being dropped silently)
|
|
113
145
|
- `value: string | string[]` — a getter/setter: plain `string` in single mode, `string[]` in
|
|
114
146
|
`multiple` mode
|
|
115
147
|
|
|
148
|
+
`ComboboxSourceRow = { value: string; label: string; sub?: string; dotColor?: string; group?:
|
|
149
|
+
string; disabled?: boolean }` — the row shape both the async `source` and the light-DOM
|
|
150
|
+
`<lyra-option>` path normalize to internally.
|
|
151
|
+
|
|
116
152
|
**Events:** `change` (native-style — selection changed), `input` (native-style — typed or selection
|
|
117
153
|
changed), `lyra-show`, `lyra-hide`, `lyra-clear`
|
|
118
154
|
|
|
119
|
-
**Slots:** default (`<lyra-option>` children), `label`, `hint`
|
|
155
|
+
**Slots:** default (`<lyra-option>` children), `label`, `hint`, `error` (overrides the `errorText`
|
|
156
|
+
attribute when provided)
|
|
120
157
|
|
|
121
158
|
**CSS parts:** `form-control`, `form-control-label`, `combobox`, `tags`, `tag`,
|
|
122
|
-
`tag__remove-button`, `combobox-input`, `clear-button`, `expand-icon`, `listbox`, `option`,
|
|
159
|
+
`tag__remove-button`, `combobox-input`, `clear-button`, `expand-icon`, `listbox`, `option`,
|
|
160
|
+
`option-dot` (the leading status dot, when a row's `dotColor` is set), `option-label`, `option-sub`
|
|
161
|
+
(a row's secondary line, when `sub` is set), `option-overflow` (the "+N more" indicator from
|
|
162
|
+
`maxRender`), `error`, `hint`
|
|
123
163
|
|
|
124
164
|
**Themeable custom properties:** shared tokens only.
|
|
125
165
|
|
|
@@ -130,10 +170,14 @@ changed), `lyra-show`, `lyra-hide`, `lyra-clear`
|
|
|
130
170
|
**Properties:**
|
|
131
171
|
- `value: string = ''`
|
|
132
172
|
- `disabled: boolean = false`
|
|
133
|
-
- `selected: boolean = false` (reflected — set by the parent combobox
|
|
173
|
+
- `selected: boolean = false` (reflected — set by the parent combobox, but also **read** on initial
|
|
174
|
+
mount, see below)
|
|
134
175
|
- `group: string = ''` (section header)
|
|
135
176
|
- `searchText: string = ''` (attribute `search-text` — extra text the filter matches beyond the
|
|
136
177
|
visible label)
|
|
178
|
+
- `sub: string = ''` (optional secondary line rendered under the label, e.g. a status/date summary)
|
|
179
|
+
- `dotColor: string = ''` (attribute `dot-color` — optional CSS color for a small leading status
|
|
180
|
+
dot, any valid CSS color)
|
|
137
181
|
- `label` is a **read-only getter**: explicit `label` attribute wins, else trimmed `textContent`.
|
|
138
182
|
|
|
139
183
|
```html
|
|
@@ -146,24 +190,117 @@ changed), `lyra-show`, `lyra-hide`, `lyra-clear`
|
|
|
146
190
|
</script>
|
|
147
191
|
```
|
|
148
192
|
|
|
193
|
+
```html
|
|
194
|
+
<!-- Async data source instead of light-DOM <lyra-option> children: -->
|
|
195
|
+
<lyra-combobox id="cb2" label="Fruit (async)" with-clear></lyra-combobox>
|
|
196
|
+
<script type="module">
|
|
197
|
+
document.getElementById('cb2').source = async (query) => {
|
|
198
|
+
const rows = await fetchFruit(query); // your own lookup
|
|
199
|
+
return rows.map((r) => ({ value: r.id, label: r.name }));
|
|
200
|
+
};
|
|
201
|
+
</script>
|
|
202
|
+
```
|
|
203
|
+
|
|
149
204
|
Multi-select submits as **repeated `FormData` entries** (not a joined string) —
|
|
150
|
-
`new FormData(form).getAll(name)` behaves like a native multi-value control.
|
|
205
|
+
`new FormData(form).getAll(name)` behaves like a native multi-value control. An unnamed multi-select
|
|
206
|
+
(`multiple` with no `name`) contributes nothing to the form at all, matching a nameless native
|
|
207
|
+
`<select multiple>`, rather than falling back to a shared literal key that could collide with
|
|
208
|
+
another unnamed combobox in the same form.
|
|
151
209
|
|
|
152
210
|
**Known gotchas:**
|
|
153
|
-
- `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
|
-
- 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
211
|
- naming drift from Web Awesome parity: the shipped attribute is **`with-clear`**, not
|
|
162
212
|
`clearable` (which is what `<wa-combobox>` uses) — don't copy-paste WA docs assuming a 1:1
|
|
163
213
|
attribute name here.
|
|
214
|
+
- a host-level `aria-label` attribute on `<lyra-combobox>` now takes priority over `label`/
|
|
215
|
+
`placeholder`/`"Combobox"` when resolving the accessible name on `[part="combobox-input"]` —
|
|
216
|
+
previously it was silently ignored. Matches the same fallback on `<lyra-select>`.
|
|
164
217
|
- no `size` variant (small/medium/large) unlike sibling `lyra-toast-item`.
|
|
218
|
+
- `aria-required` reflects `required` immediately; `aria-invalid`, by contrast, only reflects
|
|
219
|
+
**after the field has been touched** (first `blur`) — a `required` field with a validity error
|
|
220
|
+
doesn't look invalid to assistive tech before that, by design (avoids flashing invalid styling on
|
|
221
|
+
first render). Blurring the input (Tab away) now also closes an open listbox, the same as a
|
|
222
|
+
native `<select>`'s popup, not just a click outside or Escape.
|
|
223
|
+
- `dotColor`/`sub`/`group` are read from light-DOM `<lyra-option>` children as before, but are also
|
|
224
|
+
first-class fields on `ComboboxSourceRow` for the async `source` path — an async lookup can drive
|
|
225
|
+
the same grouped/dot/sub-text rendering a static option list can.
|
|
165
226
|
- 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.
|
|
227
|
+
kept on the input) is implemented correctly — a genuine strength, safe to build on. Declaratively-
|
|
228
|
+
selected options (`<lyra-option value="b" selected>`) are seeded into the selection (mirroring
|
|
229
|
+
native `<select><option selected>`) both the first time options are collected **and** for any
|
|
230
|
+
later batch slotted in afterward (e.g. a lazily-populated list appended post-connect) — only the
|
|
231
|
+
`form.reset()` default itself is captured exclusively from that very first pass; a later pick, or
|
|
232
|
+
a later batch of newly-`selected` options, never redefines what a reset restores to.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## `lyra-select`
|
|
237
|
+
|
|
238
|
+
A plain closed-list dropdown — a direct `<lyra-*>` counterpart to `<wa-select>`/`<wa-option>`.
|
|
239
|
+
**Form-associated** (hand-rolled internals, not the shared `FormAssociated` mixin — same reasoning
|
|
240
|
+
as `lyra-combobox`, see the shared-foundation notes above). The trigger is a `<button>`, not a text
|
|
241
|
+
input: click/Enter/Space/ArrowDown opens it, and there's no typing-to-filter. Options are
|
|
242
|
+
`<lyra-option value>` children — the same element `<lyra-combobox>` uses — reconciled the same way
|
|
243
|
+
combobox does, and the popup reuses `internal/positioner.ts` for placement.
|
|
244
|
+
|
|
245
|
+
Single-select only, with no `filter`/`source`/`with-clear`/`max-options-visible`/`empty-text`/
|
|
246
|
+
`max-render`/`multiple` surface — reach for `<lyra-combobox>` instead whenever any of those apply.
|
|
247
|
+
|
|
248
|
+
**Properties:**
|
|
249
|
+
- `placeholder: string = ''`
|
|
250
|
+
- `disabled: boolean = false` (reflected)
|
|
251
|
+
- `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
|
|
252
|
+
- `name: string = ''`
|
|
253
|
+
- `label: string = ''`
|
|
254
|
+
- `hint: string = ''`
|
|
255
|
+
- `errorText: string = ''` (attribute `error-text` — static error copy shown below the hint;
|
|
256
|
+
overridden by slotted `error` content when provided)
|
|
257
|
+
- `open: boolean = false` (reflected)
|
|
258
|
+
- `size: 'xs'|'s'|'m'|'l'|'xl' = 'm'` (reflected — same scale as `lyra-toast-item`'s `size`, for
|
|
259
|
+
compact toolbar placements that don't fit the default trigger height)
|
|
260
|
+
- `value: string` — a getter/setter; always a single string (no `multiple` mode)
|
|
261
|
+
|
|
262
|
+
**Events:** `change` (native-style — selection changed), `input` (fired alongside `change` on every
|
|
263
|
+
selection change — a native `<select>` doesn't meaningfully distinguish the two either),
|
|
264
|
+
`lyra-show`, `lyra-hide`
|
|
265
|
+
|
|
266
|
+
**Slots:** default (`<lyra-option>` children), `label`, `hint`, `error` (overrides the `errorText`
|
|
267
|
+
attribute when provided)
|
|
268
|
+
|
|
269
|
+
**CSS parts:** `form-control`, `form-control-label`, `trigger`, `listbox`, `option`, `option-dot`
|
|
270
|
+
(the leading status dot, when a row's `dotColor` is set), `option-label`, `option-sub` (a row's
|
|
271
|
+
secondary line, when `sub` is set), `expand-icon`, `error`, `hint`
|
|
272
|
+
|
|
273
|
+
**Themeable custom properties:** shared tokens only.
|
|
274
|
+
|
|
275
|
+
**Optional peer deps:** none.
|
|
276
|
+
|
|
277
|
+
```html
|
|
278
|
+
<lyra-select id="sel" label="Fruit" placeholder="Pick one…">
|
|
279
|
+
<lyra-option value="a">Apple</lyra-option>
|
|
280
|
+
<lyra-option value="b" selected>Banana</lyra-option>
|
|
281
|
+
</lyra-select>
|
|
282
|
+
<script type="module">
|
|
283
|
+
document.getElementById('sel').addEventListener('change', (e) => console.log(e.target.value));
|
|
284
|
+
</script>
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
**Known gotchas:**
|
|
288
|
+
- The trigger keeps real DOM focus throughout — the listbox's "active" row is conveyed via
|
|
289
|
+
`aria-activedescendant`, never actual focus, matching the WAI-ARIA "select-only combobox" pattern
|
|
290
|
+
(as opposed to `lyra-combobox`'s editable-input pattern).
|
|
291
|
+
- No typing-to-filter, but a printable keypress still jumps to (while open) or directly selects
|
|
292
|
+
(while closed) the next non-disabled option whose label starts with what's been typed, matching a
|
|
293
|
+
native `<select>`'s own type-ahead; the buffer resets ~500ms after the last keystroke.
|
|
294
|
+
- Declaratively-selected options (`<lyra-option value="b" selected>`) seed the initial selection
|
|
295
|
+
(mirroring native `<select><option selected>`) both the first time options are collected and for
|
|
296
|
+
any later-slotted batch — only that very first pass' declared selection becomes the
|
|
297
|
+
`form.reset()` default, the same rule `lyra-combobox` follows.
|
|
298
|
+
- `aria-required` on the trigger reflects `required` immediately; `aria-invalid` only reflects once
|
|
299
|
+
the trigger has been blurred (touched) at least once, mirroring `lyra-combobox`'s own input.
|
|
300
|
+
Blurring the trigger (Tab away) closes an open listbox, the same as a native `<select>`'s popup.
|
|
301
|
+
- The trigger's accessible name now checks a host-level `aria-label` attribute first, before falling
|
|
302
|
+
back to `label`/`placeholder`/`"Select"` — a plain `aria-label` on `<lyra-select>` is no longer
|
|
303
|
+
silently ignored.
|
|
167
304
|
|
|
168
305
|
---
|
|
169
306
|
|
|
@@ -195,7 +332,8 @@ Inline month-grid calendar, not form-associated (used standalone or embedded ins
|
|
|
195
332
|
**Getters:** `selection: { from: Date|null; to: Date|null }`, `valueAsDate: Date | null` (single
|
|
196
333
|
mode only)
|
|
197
334
|
|
|
198
|
-
**Methods:** `clear()`, `goToToday()`, `goToDate(date: string | Date)`
|
|
335
|
+
**Methods:** `clear()`, `goToToday()`, `goToDate(date: string | Date)` (clamped to `min`/`max` before
|
|
336
|
+
navigating/focusing)
|
|
199
337
|
|
|
200
338
|
**Events:** `input` (during interaction — for range mode, fires after the first click of a pair),
|
|
201
339
|
`change` (committed value)
|
|
@@ -230,15 +368,22 @@ Text field + calendar popover, **form-associated** via the shared `FormAssociate
|
|
|
230
368
|
- `months: 1|2 = 1`
|
|
231
369
|
- `firstDayOfWeek: string = 'auto'`
|
|
232
370
|
- `weekdayFormat: 'narrow'|'short'|'long' = 'short'`
|
|
371
|
+
- `disablePast: boolean = false` (attribute `disable-past`)
|
|
372
|
+
- `disableFuture: boolean = false` (attribute `disable-future`)
|
|
373
|
+
- `withOutsideDays: boolean = false` (attribute `with-outside-days`)
|
|
374
|
+
|
|
375
|
+
**Properties (own, continued):**
|
|
376
|
+
- `errorText: string = ''` (attribute `error-text` — static error copy; overridden by slotted
|
|
377
|
+
`error` content when provided)
|
|
233
378
|
|
|
234
379
|
**Methods:** `show()`, `hide()`, `clear()`
|
|
235
380
|
|
|
236
381
|
**Events:** `input`, `change`, `lyra-show`, `lyra-hide`, `lyra-clear`
|
|
237
382
|
|
|
238
|
-
**Slots:** `label`, `hint`
|
|
383
|
+
**Slots:** `label`, `error` (overrides `errorText`), `hint`
|
|
239
384
|
|
|
240
385
|
**CSS parts:** `form-control`, `form-control-label`, `input-wrapper`, `input`, `clear-button`,
|
|
241
|
-
`expand-button`, `expand-icon`, `popup`, `date-picker`, `hint`
|
|
386
|
+
`expand-button`, `expand-icon`, `popup`, `date-picker`, `error`, `hint`
|
|
242
387
|
|
|
243
388
|
**Themeable custom properties:** shared tokens only.
|
|
244
389
|
|
|
@@ -257,30 +402,30 @@ Text field + calendar popover, **form-associated** via the shared `FormAssociate
|
|
|
257
402
|
from the package root**, but worth knowing when reasoning about behavior): `parseISO`, `formatISO`,
|
|
258
403
|
`isSameDay`, `addMonths`, `clampDate`, `monthMatrix(year, month, firstDayOfWeek)`,
|
|
259
404
|
`weekdayLabels(firstDayOfWeek, format, locale)`, `monthTitle(year, month, locale)`,
|
|
260
|
-
`resolveFirstDayOfWeek(value)`.
|
|
405
|
+
`resolveFirstDayOfWeek(value, locale?)`.
|
|
261
406
|
|
|
262
407
|
**Known gotchas:**
|
|
263
|
-
- `first-day-of-week="auto"`
|
|
264
|
-
`
|
|
265
|
-
|
|
266
|
-
a
|
|
267
|
-
an
|
|
268
|
-
|
|
269
|
-
- (
|
|
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.
|
|
408
|
+
- `first-day-of-week="auto"` now derives from `locale` when the runtime's `Intl.Locale` exposes
|
|
409
|
+
week-info (`weekInfo`/`getWeekInfo()`, still shifting between engines) — `resolveFirstDayOfWeek()`
|
|
410
|
+
only hardcodes Sunday as the fallback when that isn't available or `locale` is unset. A
|
|
411
|
+
French/German-locale user with a supporting runtime now gets the Monday-first grid their OS would
|
|
412
|
+
show; on an older runtime it still falls back to Sunday-first silently. **If you need a guaranteed
|
|
413
|
+
locale-correct week start regardless of runtime**, pass an explicit
|
|
414
|
+
`first-day-of-week="mon"` (accepted values: `sun`/`mon`/`tue`/`wed`/`thu`/`fri`/`sat`).
|
|
273
415
|
- (date-input only) — opening the calendar doesn't move focus into it (`show()` only flips
|
|
274
416
|
`open`) — a keyboard user opening via `Alt+ArrowDown` lands on a visible popover with focus still
|
|
275
|
-
on the text input.
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
`
|
|
417
|
+
on the text input. Escape *does* close it from anywhere inside the form control (including the
|
|
418
|
+
nested picker's own day/nav buttons), returning focus to the expand button.
|
|
419
|
+
- (date-input only) — typing an unparseable string and blurring/committing sets `badInput` via
|
|
420
|
+
`internals.setValidity()` and reverts the displayed text to the last valid commit, but does not
|
|
421
|
+
reflect `aria-invalid` onto the visible `<input part="input">` itself (unlike `lyra-combobox`,
|
|
422
|
+
which does reflect `aria-invalid`/`aria-required` onto its own input) — rely on `data-invalid`
|
|
423
|
+
(a host attribute, toggled the same way) or `reportValidity()` rather than assuming the input's own
|
|
424
|
+
ARIA state.
|
|
282
425
|
- The grid keyboard pattern (Arrow/PageUp/PageDown/Home/End navigation with correct focus
|
|
283
|
-
sequencing) is implemented correctly and safe to rely on
|
|
426
|
+
sequencing) is implemented correctly and safe to rely on, as is the selected/range-day text color
|
|
427
|
+
(`--lyra-color-on-brand`, not a hardcoded literal — safe to override `--lyra-color-brand` without
|
|
428
|
+
losing contrast on selected-day text).
|
|
284
429
|
|
|
285
430
|
---
|
|
286
431
|
|
|
@@ -317,7 +462,8 @@ A single notification.
|
|
|
317
462
|
|
|
318
463
|
**Properties:**
|
|
319
464
|
- `duration: number = 5000` (ms; `Infinity` or `<= 0` disables auto-dismiss)
|
|
320
|
-
- `size: 'xs'|'s'|'m'|'l'|'xl' = 'm'` (reflected
|
|
465
|
+
- `size: 'xs'|'s'|'m'|'l'|'xl' = 'm'` (reflected — drives both `--padding` and the toast's own
|
|
466
|
+
font-size via `:host([size=...])`, from a compact `xs` up to a roomier `xl`)
|
|
321
467
|
- `variant: 'brand'|'success'|'warning'|'danger'|'neutral' = 'neutral'` (reflected)
|
|
322
468
|
- `withIcon: boolean = false` (attribute `with-icon`)
|
|
323
469
|
|
|
@@ -330,15 +476,24 @@ DOM.
|
|
|
330
476
|
|
|
331
477
|
**CSS parts:** `toast-item`, `accent`, `icon`, `content`, `close-button`
|
|
332
478
|
|
|
333
|
-
**Themeable custom properties:** `--accent-width` (4px), `--show-duration`/`--hide-duration`
|
|
334
|
-
|
|
479
|
+
**Themeable custom properties:** `--accent-width` (4px), `--show-duration`/`--hide-duration`
|
|
480
|
+
(`var(--lyra-transition-base, 180ms ease-out)` — 180ms by default, matching the JS-side show/hide
|
|
481
|
+
animation timer, which is not itself reading this token), `--padding` (`var(--lyra-space-m)`),
|
|
482
|
+
`--font-size` (`1rem`) — both `--padding` and `--font-size` are auto-swapped per `size`, from a
|
|
483
|
+
compact `xs` up to a roomier `xl` — `--accent-color` (defaults to `--lyra-color-border`,
|
|
335
484
|
auto-swapped per `variant` to `--lyra-color-brand/-success/-warning/-danger`).
|
|
336
485
|
|
|
337
486
|
**Optional peer deps:** none.
|
|
338
487
|
|
|
339
488
|
`role` is chosen automatically per `variant`: `"alert"` for `danger`/`warning`, `"status"`
|
|
340
|
-
otherwise
|
|
341
|
-
`
|
|
489
|
+
otherwise — re-evaluated on every `variant` change, not just at first render, so reassigning
|
|
490
|
+
`variant` to `danger`/`warning` after creation is announced as an interruption instead of keeping
|
|
491
|
+
its original, now-stale role. Auto-dismiss timer **pauses** on `pointerenter`/`focusin`, **resumes**
|
|
492
|
+
on `pointerleave`/`focusout`, with real elapsed-time bookkeeping (WCAG 2.2.1 timing-adjustable) —
|
|
493
|
+
hover and focus are tracked as independent pause reasons, so releasing only one (e.g. the pointer
|
|
494
|
+
leaves while focus remains, or vice versa) keeps the timer paused until *neither* holds it anymore.
|
|
495
|
+
A `duration` change while the timer is actively counting down reschedules it immediately against
|
|
496
|
+
the new value instead of waiting for the next pause/resume cycle.
|
|
342
497
|
|
|
343
498
|
### `toast()`
|
|
344
499
|
|
|
@@ -355,7 +510,9 @@ toast({ message: 'Deleted', variant: 'danger', action: { label: 'Undo', onClick:
|
|
|
355
510
|
`toast(input: ToastOptions | string): ToastHandle` where
|
|
356
511
|
`ToastOptions = ToastCreateOptions & { message: string; placement?: ToastPlacement; action?: { label: string; onClick: (item: LyraToastItem) => void } }`,
|
|
357
512
|
and `ToastHandle = { item: Promise<LyraToastItem>; dismiss: () => void }`. Lazily mounts (and
|
|
358
|
-
re-mounts if removed)
|
|
513
|
+
re-mounts if removed) **one singleton `<lyra-toast>` region per distinct `placement`** on
|
|
514
|
+
`document.body` — a `toast()` call targeting one placement never relocates toasts already showing
|
|
515
|
+
at another, since `placement` is a per-call option rather than a single global region's setting.
|
|
359
516
|
|
|
360
517
|
```html
|
|
361
518
|
<script type="module">
|
|
@@ -369,9 +526,15 @@ re-mounts if removed) a single `<lyra-toast>` region on `document.body`.
|
|
|
369
526
|
`role="status" aria-live="polite"` **and** each `lyra-toast-item` independently self-assigns its
|
|
370
527
|
own `role` (`status`/`alert`) — nesting live regions can cause some screen readers to announce a
|
|
371
528
|
new toast twice. Plausible from the code, unverified against real AT.
|
|
372
|
-
-
|
|
373
|
-
|
|
374
|
-
|
|
529
|
+
- the close button's accessible name is derived from the toast's own message text (`"Close: <first
|
|
530
|
+
40 chars>…"`, falling back to bare `"Close"` only when the toast has no text content) rather than
|
|
531
|
+
a bare `"Close"` on every instance — useful when several toasts are stacked and a screen-reader or
|
|
532
|
+
switch-access user needs to tell their close buttons apart without activating one first.
|
|
533
|
+
- pause/resume-on-hover/focus (the component's main accessibility differentiator), including the
|
|
534
|
+
independent-hover-vs-focus pause reasons above, now has regression test coverage.
|
|
535
|
+
- `hide()` is idempotent (a second call while already hiding is a no-op) and `[part="close-button"]`
|
|
536
|
+
gets `aria-disabled="true"` once hiding starts, so a stray extra click/Enter during the hide
|
|
537
|
+
animation can't re-enter it.
|
|
375
538
|
- Prefer the `toast()` helper over manually creating `<lyra-toast>`/`<lyra-toast-item>` — it already
|
|
376
539
|
handles the singleton-region and remount-if-removed logic.
|
|
377
540
|
|
|
@@ -409,8 +572,13 @@ the reviewed family (pure CSS cascade, no JS/`getComputedStyle` bridging needed)
|
|
|
409
572
|
- Auto-sets `role="img"` + a descriptive `aria-label` ("Trend of N values, last X" / "No data" when
|
|
410
573
|
empty) — good default, but still just a single summary sentence with no tabular fallback; fine for
|
|
411
574
|
a glance-value sparkline, worth reconsidering if it's ever the *sole* representation of a metric.
|
|
412
|
-
-
|
|
413
|
-
|
|
575
|
+
- flat data (every value equal, so the auto-computed range spans zero) now renders a centered
|
|
576
|
+
midline/mid-height bars instead of collapsing every point to the bottom edge, and a single-value
|
|
577
|
+
series renders a visible flat line (a zero-length path was previously invisible). A `type="bar"`
|
|
578
|
+
series past 500 points is decimated to at most 500 rendered `<rect>`s (evenly sampled, not
|
|
579
|
+
aggregated/averaged) so an unbounded `values` array can't turn directly into that many shadow-DOM
|
|
580
|
+
nodes; `line`/`area` types have no such cap (they already collapse to one `<path>` regardless of
|
|
581
|
+
point count).
|
|
414
582
|
|
|
415
583
|
---
|
|
416
584
|
|
|
@@ -423,9 +591,17 @@ Country/language flag image. Flag artwork ships in a **separate, optional peer p
|
|
|
423
591
|
- `country?: string` (ISO 3166-1 alpha-2, e.g. `"fr"` — takes precedence over `language`)
|
|
424
592
|
- `language?: string` (BCP-47-ish tag, e.g. `"en"`/`"en-US"`, resolved to a representative country
|
|
425
593
|
via `languageToCountry()`)
|
|
426
|
-
- `
|
|
427
|
-
|
|
594
|
+
- `src?: string` (a pre-resolved flag image URL — takes precedence over `country`/`language` and
|
|
595
|
+
skips the peer-package lookup/loading-skeleton round trip entirely; mainly useful to avoid even
|
|
596
|
+
the small per-flag async hop when you already have the URL at build time, e.g. from
|
|
597
|
+
`import frUrl from '@aceshooting/lyra-flags/flags/fr.svg?url'`. `label` is effectively required
|
|
598
|
+
alongside `src` since there's no `country`/`language` to derive a fallback `alt` from.)
|
|
599
|
+
- `label?: string` (accessible name / `alt` text — **defaults to a localized, human-readable region
|
|
600
|
+
name derived from the *resolved country* code via `Intl.DisplayNames` if omitted**, see gotchas)
|
|
428
601
|
- `round: boolean = false` (reflected — circular crop)
|
|
602
|
+
- `detailed: boolean = false` (reflected — requests the pristine, pre-optimization source SVG
|
|
603
|
+
instead of the default icon-optimized one; a no-op for the majority of codes whose source art was
|
|
604
|
+
never large enough to need optimizing. Has no effect when `src` is set. See gotchas.)
|
|
429
605
|
|
|
430
606
|
**Events:** none.
|
|
431
607
|
|
|
@@ -433,7 +609,8 @@ Country/language flag image. Flag artwork ships in a **separate, optional peer p
|
|
|
433
609
|
|
|
434
610
|
**CSS parts:** `image`
|
|
435
611
|
|
|
436
|
-
**Themeable custom properties:** `--lyra-flag-radius` (default `2px`, non-`round` corner radius)
|
|
612
|
+
**Themeable custom properties:** `--lyra-flag-radius` (default `2px`, non-`round` corner radius);
|
|
613
|
+
also consumes `--lyra-color-border` (the inset ring around the flag image).
|
|
437
614
|
|
|
438
615
|
**Optional peer deps:** `@aceshooting/lyra-flags` — required for the component to actually render an
|
|
439
616
|
image; without it, renders an empty template (see gotchas).
|
|
@@ -454,15 +631,41 @@ pnpm add @aceshooting/lyra-flags # required peer — without it, <lyra-flag> r
|
|
|
454
631
|
|
|
455
632
|
**Known gotchas:**
|
|
456
633
|
- Requires the optional peer `@aceshooting/lyra-flags` to actually render an image; without it the
|
|
457
|
-
component
|
|
458
|
-
|
|
634
|
+
component still shows a `<lyra-skeleton variant="rect">` placeholder (with `aria-busy="true"` on
|
|
635
|
+
the host) while resolving, then settles into an **empty template** plus a one-time `console.warn`
|
|
636
|
+
once the resolver rejects (lazy `import()`, cached module-wide so the warning fires only once per
|
|
637
|
+
page even with many `<lyra-flag>` instances).
|
|
459
638
|
- Rendering is async even when the peer *is* installed: `src` resolves after an `import()` +
|
|
460
|
-
resolver call, so there's a brief
|
|
639
|
+
resolver call, so there's a brief loading-skeleton window on first paint/attribute change — don't
|
|
461
640
|
assume the `<img>` exists synchronously right after setting `country`/`language`.
|
|
462
|
-
- if `label` is omitted, the accessible name (`alt`) falls back to
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
641
|
+
- if `label` is omitted, the accessible name (`alt`) falls back to a localized region name via
|
|
642
|
+
`Intl.DisplayNames([navigator.language], { type: 'region' })` (e.g. `"United Kingdom"`) instead of
|
|
643
|
+
a bare code — for `language="en"` that's derived from `"GB"` (the mapped country), not `"EN"` (the
|
|
644
|
+
language tag itself). Falls back further to the bare uppercase code if `Intl.DisplayNames` throws
|
|
645
|
+
for an unrecognized region or isn't available. You can still pass an explicit `label` (e.g.
|
|
646
|
+
`"France"`) to override the derived name.
|
|
647
|
+
- an invalid `country` (anything not matching the ISO 3166-1 alpha-2 shape, e.g. containing `../`)
|
|
648
|
+
is rejected before it ever reaches the peer's `flagUrl()` resolver — treated the same as an
|
|
649
|
+
unknown/missing flag rather than being passed through. `languageToCountry()`'s region-subtag path
|
|
650
|
+
is validated against that same alpha-2 shape too (not just a bare length-2 check), so a malformed
|
|
651
|
+
region such as `"en-01"` correctly falls through to the `LANGUAGE_TO_COUNTRY[base]` lookup instead
|
|
652
|
+
of being accepted verbatim as a country code.
|
|
653
|
+
- rapidly reassigning `country`/`language`/`src` no longer risks a stale resolution overwriting a
|
|
654
|
+
newer one: each resolver call is tagged with a token captured at the time it was kicked off, and a
|
|
655
|
+
`.then()` that fires after a later change (or after the field was cleared) is discarded instead of
|
|
656
|
+
clobbering the rendered image/`loading`.
|
|
657
|
+
- `country`/`language` resolve through `@aceshooting/lyra-flags`'s `flagUrl(code)`, which is itself
|
|
658
|
+
genuinely code-split per flag: using `<lyra-flag country="fr">` anywhere in an app fetches only
|
|
659
|
+
the flags actually requested at runtime (verified with a real Vite build — referencing 2 codes
|
|
660
|
+
shipped ~28 KB total, not all 249). If you already have a flag's URL at build time, `src` skips
|
|
661
|
+
the peer-package round trip (and its loading-skeleton flash) entirely.
|
|
662
|
+
- 65 of `@aceshooting/lyra-flags`' 249 flags (any whose design includes a detailed coat of
|
|
663
|
+
arms/seal/emblem, e.g. `es`, `pt`, `sv`) ship a second, pristine full-detail source SVG alongside
|
|
664
|
+
the default icon-optimized one (`flagUrl(code, { variant: 'detailed' })`, or `detailed` on
|
|
665
|
+
`<lyra-flag>`) — the default variant is the one `country`/`language` resolve to unless `detailed`
|
|
666
|
+
is set, and is what almost every consumer wants (an icon-scale rendering, ~65% smaller on average
|
|
667
|
+
for the 65 affected codes, no visible fidelity loss at that scale). `detailed` is a safe no-op for
|
|
668
|
+
the other 184 codes — same file either way.
|
|
466
669
|
|
|
467
670
|
---
|
|
468
671
|
|
|
@@ -475,6 +678,9 @@ First-party "no data" state (no Web Awesome equivalent).
|
|
|
475
678
|
**Properties:**
|
|
476
679
|
- `heading: string = ''`
|
|
477
680
|
- `description: string = ''`
|
|
681
|
+
- `compact: boolean = false` (reflected) — tighter, left-aligned rendering (less padding, a lighter
|
|
682
|
+
heading weight) for use inside a constrained space like a widget body or table cell, instead of
|
|
683
|
+
the centered/spacious full-page default
|
|
478
684
|
|
|
479
685
|
**Events:** none.
|
|
480
686
|
|
|
@@ -482,7 +688,7 @@ First-party "no data" state (no Web Awesome equivalent).
|
|
|
482
688
|
|
|
483
689
|
**CSS parts:** `base`, `icon`, `heading`, `description`, `actions`
|
|
484
690
|
|
|
485
|
-
**Themeable custom properties:** shared tokens only (`--lyra-space-s/-l`,
|
|
691
|
+
**Themeable custom properties:** shared tokens only (`--lyra-space-xs/-s/-l`,
|
|
486
692
|
`--lyra-color-text-quiet/-border/-text`).
|
|
487
693
|
|
|
488
694
|
**Optional peer deps:** none.
|
|
@@ -495,10 +701,8 @@ First-party "no data" state (no Web Awesome equivalent).
|
|
|
495
701
|
```
|
|
496
702
|
|
|
497
703
|
**Known gotchas:**
|
|
498
|
-
-
|
|
499
|
-
|
|
500
|
-
- 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.
|
|
704
|
+
- `[part="base"]` is `role="status" aria-live="polite"`, so a list/table transitioning to empty
|
|
705
|
+
does announce to screen readers — no extra wiring needed on the host's part.
|
|
502
706
|
- Note: correctly works around the classic `:empty`-pseudo-class trap (a wrapper with a `<slot>`
|
|
503
707
|
inside can never match `:empty`) by tracking real slot assignment in JS (`hasIcon`/`hasActions`) —
|
|
504
708
|
`lyra-table` reuses this component for its own empty-rows state, and `lyra-stat` (below) now uses
|
|
@@ -560,16 +764,38 @@ KPI/stat card — value + unit + label + optional icon/trend/caption.
|
|
|
560
764
|
- `goodDirection: 'up'|'down' = 'up'` (attribute `good-direction`) — which trend direction counts
|
|
561
765
|
as "good"; inverts arrow/color polarity for cost/latency/error-rate-style metrics where a
|
|
562
766
|
*decrease* is the win.
|
|
767
|
+
- `rows: StatRow[] = []` (attribute: false) — `StatRow { label: string; value: string }`; rendered as
|
|
768
|
+
a simple label/value breakdown list (`[part="rows"]`/`[part="row"]`/`[part="row-label"]`/
|
|
769
|
+
`[part="row-value"]`) beneath the caption, hidden entirely when empty
|
|
770
|
+
- `emphasis: boolean = false` (reflected) — visual emphasis (e.g. for a "headline" stat in a group):
|
|
771
|
+
a brand-colored accent edge, orthogonal to the status `variant`; status semantics still win over
|
|
772
|
+
it — `emphasis` only additionally tints `[part="value"]` brand-colored when `variant` is still
|
|
773
|
+
`'neutral'`, never overriding an actual `success`/`warning`/`danger` value color
|
|
774
|
+
- `exactValue: string = ''` (attribute `exact-value`) — rendered as a `title` attribute on
|
|
775
|
+
`[part="value"]` for a hover tooltip (e.g. `value="$1.2K" exact-value="$1,204.37"`); also gives
|
|
776
|
+
`[part="value"]` `tabindex="0"` (only when set) so the tooltip is reachable by keyboard focus, not
|
|
777
|
+
just hover
|
|
778
|
+
- `sub: string = ''` — a secondary line distinct from `caption`, e.g. a comparison-period label,
|
|
779
|
+
rendered as `[part="sub"]` between the trend pill and the caption; hidden entirely when unset
|
|
780
|
+
- `prose: boolean = false` (reflected) — CSS-only variant that shrinks/lightens `[part="value"]` and
|
|
781
|
+
hides `[part="unit"]`, for rendering a loading/status message in place of a numeric value
|
|
782
|
+
- `compact: boolean = false` (reflected) — tighter card padding; same convention as `lyra-empty`'s and
|
|
783
|
+
`lyra-widget`'s `compact`
|
|
563
784
|
|
|
564
785
|
**Events:** none.
|
|
565
786
|
|
|
566
787
|
**Slots:** default (leading icon), `caption` (rich caption content — overrides the `caption`
|
|
567
|
-
attribute when slotted content is provided)
|
|
788
|
+
attribute when slotted content is provided), `spark` (a sparkline, e.g. `<lyra-sparkline
|
|
789
|
+
slot="spark">`, or other compact trend visual — `lyra-stat` only reserves the slot and doesn't
|
|
790
|
+
render one itself), `sub` (rich sub-line content — overrides the `sub` attribute when slotted content
|
|
791
|
+
is provided)
|
|
568
792
|
|
|
569
|
-
**CSS parts:** `base`, `icon`, `label`, `value-row`, `value`, `unit`, `trend`, `
|
|
793
|
+
**CSS parts:** `base`, `icon`, `label`, `value-row`, `value`, `unit`, `trend`, `sub`, `spark`,
|
|
794
|
+
`caption`, `rows`, `row`, `row-label`, `row-value`
|
|
570
795
|
|
|
571
796
|
**Themeable custom properties:** shared tokens only (`--lyra-color-success/-warning/-danger` drive
|
|
572
|
-
the `variant`-colored value text and up/down trend pill
|
|
797
|
+
the `variant`-colored value text and up/down trend pill; `--lyra-color-brand` drives `emphasis`'s
|
|
798
|
+
accent edge and value tint).
|
|
573
799
|
|
|
574
800
|
**Optional peer deps:** none.
|
|
575
801
|
|
|
@@ -580,8 +806,12 @@ the `variant`-colored value text and up/down trend pill).
|
|
|
580
806
|
```
|
|
581
807
|
|
|
582
808
|
**Known gotchas:**
|
|
583
|
-
- no `aria-live`
|
|
584
|
-
|
|
809
|
+
- no `aria-live` region wraps `value`/`trend` — an in-place update after first render still isn't
|
|
810
|
+
proactively announced to screen readers. The trend pill's direction/polarity is no longer
|
|
811
|
+
conveyed by icon rotation/color alone, though: a visually-hidden span now spells it out in plain
|
|
812
|
+
language (e.g. "increased 4.2%, good" / "decreased 2%, bad" / "unchanged"), so a screen reader
|
|
813
|
+
landing on the pill (rather than being live-notified of a change) gets the full meaning, not just
|
|
814
|
+
an `aria-hidden` arrow glyph.
|
|
585
815
|
|
|
586
816
|
---
|
|
587
817
|
|
|
@@ -592,7 +822,13 @@ of `rows` — this component only renders and emits intents.
|
|
|
592
822
|
|
|
593
823
|
**Properties:**
|
|
594
824
|
- `columns: TableColumn<T>[] = []` (attribute: false) — `{ key, label, sortable?, align?:
|
|
595
|
-
'start'|'end', cell: (row) => unknown }`
|
|
825
|
+
'start'|'end', priority?: 'medium'|'low', sticky?: boolean, cell: (row) => unknown }` —
|
|
826
|
+
`priority` progressively hides that column via a `@container` query as `[part='base']` narrows
|
|
827
|
+
(`'low'` hides first, under a ~900px container width; `'medium'` next, under ~640px; both
|
|
828
|
+
breakpoints are fixed in `table.styles.ts`, not themeable tokens), reversible via
|
|
829
|
+
`[part='reveal-columns-button']` (rendered whenever any column sets a `priority`); `sticky` pins
|
|
830
|
+
that column's header cell/cells to the inline-start edge (`position: sticky`) while the table
|
|
831
|
+
scrolls horizontally
|
|
596
832
|
- `rows: T[] = []` (attribute: false)
|
|
597
833
|
- `sortKey: string = ''` (attribute `sort-key`)
|
|
598
834
|
- `sortDir: 'asc'|'desc' = 'asc'` (attribute `sort-dir`)
|
|
@@ -604,13 +840,19 @@ of `rows` — this component only renders and emits intents.
|
|
|
604
840
|
- `emptyDescription: string = ''` (attribute `empty-description`)
|
|
605
841
|
- `noColumnsHeading: string = 'No columns configured'` (attribute `no-columns-heading`)
|
|
606
842
|
- `noColumnsDescription: string = ''` (attribute `no-columns-description`)
|
|
843
|
+
- `revealColumnsLabel: string = 'Show all columns'` (attribute `reveal-columns-label` — the
|
|
844
|
+
reveal-button's label while `priority`-hidden columns are hidden)
|
|
845
|
+
- `hideColumnsLabel: string = 'Show fewer columns'` (attribute `hide-columns-label` — the same
|
|
846
|
+
button's label once they've been revealed)
|
|
607
847
|
|
|
608
848
|
**Events:** `lyra-sort` (`detail: { key }`, fired on sortable-header activation), `lyra-row-click`
|
|
609
849
|
(`detail: { row }`), `lyra-load-more` (fired on the "load more" button)
|
|
610
850
|
|
|
611
851
|
**Slots:** none — content comes entirely from `columns`/`rows`.
|
|
612
852
|
|
|
613
|
-
**CSS parts:** `base`, `table`, `head`, `header-cell`, `row`, `cell`, `more-button`
|
|
853
|
+
**CSS parts:** `base`, `table`, `head`, `header-cell`, `row`, `cell`, `more-button`, `sort-icon` (a
|
|
854
|
+
chevron indicator shown on the active sortable header, rotated per `sortDir`), `reveal-columns-button`
|
|
855
|
+
(shown only when at least one column sets `priority`)
|
|
614
856
|
|
|
615
857
|
**Themeable custom properties:** `--lyra-table-max-height` (default `none`; controls the scrollable
|
|
616
858
|
body's `max-block-size`).
|
|
@@ -638,8 +880,16 @@ body's `max-block-size`).
|
|
|
638
880
|
a `cell()` callback if you need multi-select.
|
|
639
881
|
- no `caption`/`aria-label` property, so the rendered `role="grid"` has no accessible name
|
|
640
882
|
distinguishing it from any other grid on the page.
|
|
641
|
-
-
|
|
642
|
-
|
|
883
|
+
- Full roving-tabindex grid keyboard pattern (one `tabindex="0"` stop among header cells, one among
|
|
884
|
+
body rows) — Left/Right/Home/End move within the header row, Up/Down/Home/End move within the
|
|
885
|
+
body, Down from the header enters the body's roving stop and Up from the body's first row returns
|
|
886
|
+
to the header, Enter/Space still only sort/activate — a genuine strength versus most siblings in
|
|
887
|
+
this family. A `priority`-hidden header/cell is skipped when computing the visible header stops,
|
|
888
|
+
so arrow-key navigation never strands the roving stop on a hidden column.
|
|
889
|
+
- a `cell()` template can render its own interactive content (a button, link, input, etc.) without
|
|
890
|
+
it being swallowed by row/column activation: clicks and Enter/Space landing on (or bubbling
|
|
891
|
+
through) anything matching `button, a[href], input, select, textarea, [role="button"]` are left
|
|
892
|
+
alone by the table's own delegated `click`/`keydown` handlers.
|
|
643
893
|
|
|
644
894
|
---
|
|
645
895
|
|
|
@@ -653,7 +903,8 @@ Dependency-free SVG radial or linear meter (270°-sweep arc math, no charting li
|
|
|
653
903
|
- `max: number = 100`
|
|
654
904
|
- `type: 'radial'|'linear' = 'radial'` (reflected)
|
|
655
905
|
- `label: string = ''`
|
|
656
|
-
- `valueLabel?: string` (attribute: false — overrides only the *visible* text, see gotchas
|
|
906
|
+
- `valueLabel?: string` (attribute: false — overrides only the *visible* text, see gotchas; an empty
|
|
907
|
+
string is treated the same as unset and falls back to the numeric `value`)
|
|
657
908
|
|
|
658
909
|
**Events:** none.
|
|
659
910
|
|
|
@@ -675,12 +926,10 @@ gauge-specific custom property exists).
|
|
|
675
926
|
```
|
|
676
927
|
|
|
677
928
|
**Known gotchas:**
|
|
678
|
-
-
|
|
679
|
-
text
|
|
680
|
-
`
|
|
681
|
-
|
|
682
|
-
`role="meter"` host that already carries `aria-valuenow`/`aria-label` — risks double/mismatched
|
|
683
|
-
announcements when `valueLabel` differs from the raw `value`.
|
|
929
|
+
- setting `valueLabel` (e.g. `"72°F"`) now also sets `aria-valuetext` on the host (in addition to
|
|
930
|
+
changing the visible SVG text), so a screen reader announces your formatted string instead of the
|
|
931
|
+
raw `aria-valuenow` number; the SVG `<text part="value">`/`<text part="label">` elements are
|
|
932
|
+
`aria-hidden="true"` so they're no longer separately exposed inside the same `role="meter"` host.
|
|
684
933
|
- no color-threshold/variant support: a single fixed `--lyra-color-brand` fill regardless
|
|
685
934
|
of value (no amber/red-near-capacity idiom built in).
|
|
686
935
|
- no documented component-specific sizing custom property; host size is fixed em values
|
|
@@ -688,6 +937,11 @@ gauge-specific custom property exists).
|
|
|
688
937
|
element instead.
|
|
689
938
|
- Divide-by-zero guarded (`max - min || 1`), and radial/linear share one component via the `type`
|
|
690
939
|
attribute.
|
|
940
|
+
- a non-finite `value`/`min`/`max` (e.g. left `NaN` mid-binding) no longer leaks a literal `"NaN"`
|
|
941
|
+
into `aria-valuenow`/`aria-valuemax`/the rendered value text — the affected ARIA attribute is
|
|
942
|
+
omitted entirely instead, and a finite `value` is clamped into `[min, max]` before being written
|
|
943
|
+
to `aria-valuenow` (the SVG arc/bar fill was already clamped via `ratio`; the ARIA number now
|
|
944
|
+
matches what's drawn).
|
|
691
945
|
|
|
692
946
|
---
|
|
693
947
|
|
|
@@ -698,7 +952,10 @@ opens a small menu).
|
|
|
698
952
|
|
|
699
953
|
**Properties:**
|
|
700
954
|
- `rows: Record<string, unknown>[] = []` (attribute: false)
|
|
701
|
-
- `columns: CsvColumn[] = []` (attribute: false) — `{ key, label }
|
|
955
|
+
- `columns: CsvColumn[] = []` (attribute: false) — `{ key, label }`; also acts as a field allow-list
|
|
956
|
+
for **both** export formats when non-empty (CSV always projected through `columns`; JSON now picks
|
|
957
|
+
the same `columns[].key` set rather than dumping every field on `rows`, so it can't leak a field
|
|
958
|
+
CSV would have hidden) — leave `columns` empty to export every field of `rows` as-is in JSON
|
|
702
959
|
- `filename: string = 'export'`
|
|
703
960
|
- `formats: ExportFormat[] = ['csv']` (attribute: false; `ExportFormat = 'csv' | 'json'` **only** —
|
|
704
961
|
no XLSX despite the family sometimes being described as covering it)
|
|
@@ -732,7 +989,7 @@ substitute your own server-generated download instead of the built-in client-sid
|
|
|
732
989
|
Package-level CSV utilities (used internally, also exported for standalone use — `import {
|
|
733
990
|
escapeCsvField, buildCsv, downloadBlob } from '@aceshooting/lyra-ui'`):
|
|
734
991
|
```ts
|
|
735
|
-
escapeCsvField(value: unknown): string // quotes/escapes; neutralizes formula-injection (
|
|
992
|
+
escapeCsvField(value: unknown): string // quotes/escapes; neutralizes formula-injection (=,+,@,tab,CR) with a leading apostrophe — a bare leading '-' is deliberately left alone (OWASP guidance: it's not itself formula syntax, and guarding it would mangle ordinary negative numbers)
|
|
736
993
|
buildCsv(rows: Record<string, unknown>[], columns: CsvColumn[]): string // CRLF-joined, header row included
|
|
737
994
|
downloadBlob(content: string, filename: string, mime: string): void // triggers a browser download
|
|
738
995
|
```
|
|
@@ -742,6 +999,11 @@ downloadBlob(content: string, filename: string, mime: string): void // trig
|
|
|
742
999
|
`'xlsx'` expecting it to work.
|
|
743
1000
|
- CSV formula-injection guarding and the deferred (5s) `URL.revokeObjectURL` (works around Safari
|
|
744
1001
|
cancelling in-flight downloads on immediate revoke) are genuine, safe-to-rely-on strengths.
|
|
1002
|
+
- the multi-format menu (`role="menu"`) supports full arrow-key navigation — ArrowUp/ArrowDown move
|
|
1003
|
+
between items (opening the menu and seeding the right one focused, if it was closed), Home/End
|
|
1004
|
+
jump to the first/last item once open, Escape closes it and returns focus to the trigger button,
|
|
1005
|
+
and a completed export also returns focus to the trigger — not just Escape/click-outside as
|
|
1006
|
+
before.
|
|
745
1007
|
|
|
746
1008
|
---
|
|
747
1009
|
|
|
@@ -758,6 +1020,13 @@ auto-inserted between each adjacent pair.
|
|
|
758
1020
|
- `storageKey?: string` (attribute `storage-key` — persists sizes to `localStorage` under
|
|
759
1021
|
`` `lyra-split:${key}:${panelCount}` ``, scoped by panel count so a stale layout for a different
|
|
760
1022
|
panel count is ignored)
|
|
1023
|
+
- `panelConstraints: (PanelConstraint | null)[] = []` (attribute: false) — `PanelConstraint { minPx?:
|
|
1024
|
+
number; maxPx?: number }`, index-aligned with `sizes`; a `null`/missing entry (or an omitted bound
|
|
1025
|
+
within an entry) leaves that side of that panel purely percent-based (the existing `min`-only
|
|
1026
|
+
behavior). `sizes`, the `lyra-resize` payload, and localStorage persistence stay percent-based
|
|
1027
|
+
regardless — only the effective drag/keyboard clamp bounds (and the rendered `flex-basis`, via a
|
|
1028
|
+
native CSS `clamp()` so a constrained panel stays pinned between its px bounds across container
|
|
1029
|
+
resizes with no extra `ResizeObserver`) change for a constrained panel.
|
|
761
1030
|
|
|
762
1031
|
**Events:** `lyra-resize` (`detail: { sizes }`, fired on every drag step/release **and** every
|
|
763
1032
|
keyboard step)
|
|
@@ -779,29 +1048,168 @@ keyboard step)
|
|
|
779
1048
|
```
|
|
780
1049
|
|
|
781
1050
|
Keyboard: focus a divider (`Tab`), then `ArrowRight`/`ArrowLeft` (horizontal) or
|
|
782
|
-
`ArrowDown`/`ArrowUp` (vertical) to resize by a fixed 2% step
|
|
1051
|
+
`ArrowDown`/`ArrowUp` (vertical) to resize by a fixed 2% step — RTL-aware for horizontal layouts
|
|
1052
|
+
(under `direction: rtl`, the forward/backward keys and drag-delta sign both swap so they still track
|
|
1053
|
+
the visually-adjacent panel).
|
|
783
1054
|
|
|
784
1055
|
**Known gotchas:**
|
|
785
|
-
- `panelCount`
|
|
786
|
-
`
|
|
787
|
-
|
|
788
|
-
`
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
`
|
|
794
|
-
|
|
1056
|
+
- `panelCount` now reacts to `slotchange` (not just the initial `connectedCallback()` read), and
|
|
1057
|
+
`ensureSizes()` rebalances existing sizes proportionally when a panel is added or removed after
|
|
1058
|
+
mount instead of discarding the whole layout — a conditionally-shown side panel no longer leaves
|
|
1059
|
+
`panelCount`/`sizes`/divider count stale.
|
|
1060
|
+
- divider `aria-valuemax` is now computed per adjacent pair (`sizes[i] + sizes[i+1] - min`) rather
|
|
1061
|
+
than a blanket `100 - min`, so it's accurate for 3+-panel layouts too, not just exactly two panels
|
|
1062
|
+
— this formula still only accounts for the plain percent `min`, though: with `panelConstraints`
|
|
1063
|
+
set, a panel's real achievable range can be narrower (or expressed in px) than what
|
|
1064
|
+
`aria-valuemin`/`aria-valuemax` report. Each divider also now has its own `aria-label` ("Resize
|
|
1065
|
+
divider between panel N and panel N+1") distinguishing it from any other divider in a
|
|
1066
|
+
multi-divider layout.
|
|
795
1067
|
- no feasibility check on `min` vs. panel count (e.g. 3 panels with `min=40` is
|
|
796
1068
|
unsatisfiable) — result is a silently frozen splitter with no warning.
|
|
797
|
-
- `
|
|
798
|
-
|
|
799
|
-
`
|
|
1069
|
+
- concurrent drags are tracked per `pointerId` (not a single scalar), so a multi-touch drag on two
|
|
1070
|
+
different dividers moves both independently instead of the second pointer clobbering the first's
|
|
1071
|
+
drag state; `pointercancel`/`lostpointercapture` (not just `pointerup`) both end a drag.
|
|
1072
|
+
- `localStorage.getItem`/`setItem` calls are now both wrapped in their own `try`/`catch` (in addition
|
|
1073
|
+
to the `JSON.parse` result already being caught), so a blocked or quota-exceeded store fails
|
|
1074
|
+
silently instead of throwing from inside a `pointerup`/`keydown` handler. A persisted layout whose
|
|
1075
|
+
panel count no longer matches, or whose sizes are already below the current `min` floor, is
|
|
1076
|
+
rejected rather than restored.
|
|
800
1077
|
- Pointer-drag lifecycle (pointer capture, window-listener cleanup on both drag-end and
|
|
801
1078
|
`disconnectedCallback`) is solid and safe to rely on.
|
|
802
1079
|
|
|
803
1080
|
---
|
|
804
1081
|
|
|
1082
|
+
## `lyra-widget`
|
|
1083
|
+
|
|
1084
|
+
A titled panel shell with an optional collapse toggle and an optional fullscreen-expand toggle.
|
|
1085
|
+
First-party invention (no Web Awesome equivalent). Fullscreen promotes the same host element in
|
|
1086
|
+
place (a CSS state, not a clone/portal), so slotted content (a chart, a running simulation, scroll
|
|
1087
|
+
position) survives the transition.
|
|
1088
|
+
|
|
1089
|
+
**Properties:**
|
|
1090
|
+
- `label: string = ''`
|
|
1091
|
+
- `sublabel: string = ''`
|
|
1092
|
+
- `collapsible: boolean = false` (reflected — shows the collapse/expand chevron button)
|
|
1093
|
+
- `collapsed: boolean = false` (reflected)
|
|
1094
|
+
- `expandable: boolean = false` (reflected — shows the fullscreen toggle button)
|
|
1095
|
+
- `fullscreen: boolean = false` (reflected)
|
|
1096
|
+
- `fullscreenInset: string = ''` (attribute `fullscreen-inset`) — raw CSS `inset` shorthand applied to
|
|
1097
|
+
`[part="base"]` and `[part="backdrop"]` while fullscreen instead of the default
|
|
1098
|
+
`var(--lyra-space-l)` on every side, e.g. `"0 0 0 240px"` to leave a 240px persistent
|
|
1099
|
+
sidebar/toolbar visible during fullscreen
|
|
1100
|
+
- `compact: boolean = false` (reflected) — tighter header/body padding, same convention as
|
|
1101
|
+
`lyra-empty`'s `compact`
|
|
1102
|
+
|
|
1103
|
+
**Events:** `lyra-collapse-change` (`detail: boolean`, the new `collapsed` state),
|
|
1104
|
+
`lyra-fullscreen-change` (`detail: boolean`, the new `fullscreen` state — also fired when fullscreen
|
|
1105
|
+
is exited via Escape or a backdrop click, not just the toggle button)
|
|
1106
|
+
|
|
1107
|
+
**Slots:** default (the panel body), `actions` (header action controls, rendered before the
|
|
1108
|
+
collapse/expand buttons)
|
|
1109
|
+
|
|
1110
|
+
**CSS parts:** `base`, `header`, `title`, `label`, `sublabel`, `actions`, `collapse-button`,
|
|
1111
|
+
`fullscreen-button`, `body`, `backdrop`
|
|
1112
|
+
|
|
1113
|
+
**Themeable custom properties:** `--lyra-widget-overlay-color` (default `rgb(0 0 0 / 0.5)` — the
|
|
1114
|
+
fullscreen backdrop scrim color; component-specific since no shared `--wa-*`/`--lyra-*` overlay token
|
|
1115
|
+
exists), plus shared tokens (`--lyra-space-*`, `--lyra-color-border/-surface/-text-quiet`,
|
|
1116
|
+
`--lyra-radius`, `--lyra-shadow`, `--lyra-icon-button-size`, `--lyra-focus-ring-*`).
|
|
1117
|
+
|
|
1118
|
+
**Optional peer deps:** none.
|
|
1119
|
+
|
|
1120
|
+
```html
|
|
1121
|
+
<lyra-widget label="Load profile" sublabel="Last 7 days" collapsible expandable>
|
|
1122
|
+
<span slot="actions"><button>Refresh</button></span>
|
|
1123
|
+
<div>Panel body content — a chart, a table, anything.</div>
|
|
1124
|
+
</lyra-widget>
|
|
1125
|
+
```
|
|
1126
|
+
|
|
1127
|
+
While `fullscreen`, `[part="base"]` (not the host itself) takes `role="dialog"` + `aria-modal="true"`
|
|
1128
|
+
(with `aria-label` from `label`, falling back to `"Fullscreen panel"`), document scroll is locked
|
|
1129
|
+
(ref-counted, safe with multiple simultaneously-fullscreen widgets), and Tab/Shift+Tab are bounded
|
|
1130
|
+
to the panel's own focusable content (`actions` slot → collapse/fullscreen buttons → body slot,
|
|
1131
|
+
matching visual tab order — resolved shadow-piercingly, so a slotted custom element's real
|
|
1132
|
+
focusable target inside its own shadow root is found too) so keyboard focus can't escape to page
|
|
1133
|
+
content hidden behind the backdrop. Escape or clicking the backdrop exits fullscreen and returns
|
|
1134
|
+
focus to whichever button triggered it. Set `fullscreen-inset` (e.g. `"0 0 0 240px"`) to reserve
|
|
1135
|
+
space for a persistent sidebar/toolbar that should stay visible instead of being covered by the
|
|
1136
|
+
fullscreen panel/backdrop — it overrides the default `var(--lyra-space-l)` inset on every side for
|
|
1137
|
+
both `[part="base"]` and `[part="backdrop"]`. Set `compact` for tighter header/body padding.
|
|
1138
|
+
|
|
1139
|
+
**Known gotchas:**
|
|
1140
|
+
- a reconnect that preserves the same element instance (e.g. a drag-and-drop reparent) restores the
|
|
1141
|
+
scroll lock and the Tab-trap `keydown` listener if `fullscreen` was still `true` across the
|
|
1142
|
+
move — `disconnectedCallback`/`connectedCallback` fire back-to-back with no update in between, so
|
|
1143
|
+
`willUpdate()` alone wouldn't otherwise notice.
|
|
1144
|
+
- `collapsed` hides the body via `hidden` rather than an animated height transition — collapsing is
|
|
1145
|
+
instant, not a slide.
|
|
1146
|
+
|
|
1147
|
+
---
|
|
1148
|
+
|
|
1149
|
+
## `lyra-word-cloud`
|
|
1150
|
+
|
|
1151
|
+
Dependency-free SVG word/tag cloud. First-party invention (no Web Awesome equivalent). Lays words
|
|
1152
|
+
out via an outward Archimedean-spiral search — heaviest word placed first, each word spiraling from
|
|
1153
|
+
the center until it clears every word already placed. Unlike sibling `lyra-sparkline`/`lyra-heatmap`
|
|
1154
|
+
(one `role="img"` glyph standing in for an aggregate value), each word here is its own focusable
|
|
1155
|
+
`role="button"`, since the individual words are the meaningful interactive content.
|
|
1156
|
+
|
|
1157
|
+
**Properties:**
|
|
1158
|
+
- `words: WordCloudWord[] = []` (attribute: false) — `{ text: string, weight: number, color?:
|
|
1159
|
+
string, group?: string }`; `weight` drives font size, `color` overrides the palette for that word,
|
|
1160
|
+
`group` shares one palette color across every word with the same `group` value
|
|
1161
|
+
- `minFontSize: number = 12` (attribute `min-font-size`) — px, applied to the lowest-weight word
|
|
1162
|
+
- `maxFontSize: number = 48` (attribute `max-font-size`) — px, applied to the highest-weight word
|
|
1163
|
+
- `scale: 'linear'|'sqrt' = 'linear'` — `sqrt` compresses the weight→font-size mapping so one heavy
|
|
1164
|
+
word doesn't dwarf the rest, matching `lyra-heatmap`'s `scale` property
|
|
1165
|
+
- `orientations: 'horizontal'|'mixed' = 'horizontal'` — `mixed` lets ~25% of words render rotated
|
|
1166
|
+
90° for denser packing
|
|
1167
|
+
- `palette?: string[]` (attribute: false) — custom categorical colors, cycled by word index (or by
|
|
1168
|
+
`group`); defaults to the `--lyra-word-cloud-color-1..8` tokens
|
|
1169
|
+
|
|
1170
|
+
**Events:** `lyra-word-click` (`detail: { text, weight, group }`, fires on click, or Enter/Space on
|
|
1171
|
+
a focused word)
|
|
1172
|
+
|
|
1173
|
+
**Slots:** none.
|
|
1174
|
+
|
|
1175
|
+
**CSS parts:** `base`, `svg`, `word` (each `<text>`), `empty` (the no-data placeholder)
|
|
1176
|
+
|
|
1177
|
+
**Themeable custom properties:** `--lyra-word-cloud-color-1` through `-8` (the default categorical
|
|
1178
|
+
palette, a data-driven literal exception like `lyra-heatmap`'s scale-ramp endpoints — exposed as
|
|
1179
|
+
retheme-able custom properties instead of hardcoded), plus shared tokens (`--lyra-font`,
|
|
1180
|
+
`--lyra-focus-ring-*`, `--lyra-transition-fast`, `--lyra-color-text-quiet`).
|
|
1181
|
+
|
|
1182
|
+
**Optional peer deps:** none.
|
|
1183
|
+
|
|
1184
|
+
```html
|
|
1185
|
+
<lyra-word-cloud id="cloud" style="height: 20rem"></lyra-word-cloud>
|
|
1186
|
+
<script type="module">
|
|
1187
|
+
document.getElementById('cloud').words = [
|
|
1188
|
+
{ text: 'JavaScript', weight: 90 },
|
|
1189
|
+
{ text: 'TypeScript', weight: 75 },
|
|
1190
|
+
{ text: 'Lit', weight: 60, group: 'framework' },
|
|
1191
|
+
];
|
|
1192
|
+
document.getElementById('cloud').addEventListener('lyra-word-click', (e) => console.log(e.detail));
|
|
1193
|
+
</script>
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
**Known gotchas:**
|
|
1197
|
+
- capped at 150 words (`MAX_WORDS` in `word-cloud-layout.ts`, mirroring `lyra-sparkline`'s
|
|
1198
|
+
`MAX_BARS` DOM-node-count guard) — anything beyond that is silently dropped from layout (not
|
|
1199
|
+
rendered, not an error); a pathological input (e.g. one huge word repeated many times) can also
|
|
1200
|
+
exhaust the spiral search's radius bound and get dropped the same way.
|
|
1201
|
+
- every word is its own tab stop (`tabindex="0"` `role="button"`), not a single roving-tabindex
|
|
1202
|
+
container like `lyra-heatmap` — fine for a modest word count, but tabbing through a cloud near the
|
|
1203
|
+
150-word cap is a lot of stops; there's no non-visual "get the whole word+weight list at once"
|
|
1204
|
+
alternative yet.
|
|
1205
|
+
- text width is measured via a detached `<canvas>` 2D context (`ctx.measureText`), not the actual
|
|
1206
|
+
rendered SVG glyph metrics — close enough for collision-avoidance spacing, but not pixel-exact.
|
|
1207
|
+
- rotation (`orientations="mixed"`) is genuinely random per layout (`Math.random()`, not seeded), so
|
|
1208
|
+
which words render rotated changes on every re-layout (any `words`/`minFontSize`/`maxFontSize`/
|
|
1209
|
+
`scale`/`orientations` change) — don't rely on rotation being stable across renders.
|
|
1210
|
+
|
|
1211
|
+
---
|
|
1212
|
+
|
|
805
1213
|
# Temporal & graph
|
|
806
1214
|
|
|
807
1215
|
## `lyra-time-range`
|
|
@@ -816,17 +1224,24 @@ onto `[min, max]`).
|
|
|
816
1224
|
- `end: number = 100`
|
|
817
1225
|
- `step: number = 1`
|
|
818
1226
|
- `disabled: boolean = false` (reflected)
|
|
1227
|
+
- `presets: TimeRangePreset[] = []` (attribute: false) — `TimeRangePreset { label: string; start:
|
|
1228
|
+
number; end: number }`; optional discrete presets (e.g. "Last 7 days") rendered as a
|
|
1229
|
+
`[part="presets"]` button row above the track — purely additive, the continuous brush is
|
|
1230
|
+
unaffected and both interaction modes coexist; picking one sets both handles and emits the same
|
|
1231
|
+
`lyra-input`/`lyra-change` pair a committed drag or keyboard step would
|
|
819
1232
|
|
|
820
|
-
**Events:** `lyra-input` (fired continuously while dragging or on arrow
|
|
821
|
-
start, end }`), `lyra-change` (fired on pointer release /
|
|
822
|
-
end }`)
|
|
1233
|
+
**Events:** `lyra-input` (fired continuously while dragging or on each arrow/Home/End/PageUp/
|
|
1234
|
+
PageDown key press, `detail: { start, end }`), `lyra-change` (fired on pointer release /
|
|
1235
|
+
key-up-commit, or when a preset button is clicked, `detail: { start, end }`)
|
|
823
1236
|
|
|
824
1237
|
**Slots:** none.
|
|
825
1238
|
|
|
826
|
-
**CSS parts:** `base`, `track`, `range`, `handle-start`, `handle-end`
|
|
1239
|
+
**CSS parts:** `base`, `track`, `range`, `handle-start`, `handle-end`, `presets`, `preset-button`
|
|
827
1240
|
|
|
828
1241
|
**Themeable custom properties:** shared tokens only — `--lyra-color-border`, `--lyra-color-brand`,
|
|
829
|
-
`--lyra-color-surface`, `--lyra-shadow
|
|
1242
|
+
`--lyra-color-surface`, `--lyra-shadow` (track/handles), plus (for `presets`) `--lyra-color-text`,
|
|
1243
|
+
`--lyra-color-on-brand` (the active preset button's text), `--lyra-radius`, `--lyra-space-xs/-s`,
|
|
1244
|
+
`--lyra-transition-fast`, `--lyra-focus-ring-*`. No component-local tokens.
|
|
830
1245
|
|
|
831
1246
|
**Optional peer deps:** none.
|
|
832
1247
|
|
|
@@ -839,15 +1254,27 @@ end }`)
|
|
|
839
1254
|
```
|
|
840
1255
|
|
|
841
1256
|
**Known gotchas:**
|
|
842
|
-
- Keyboard support
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
-
|
|
850
|
-
|
|
1257
|
+
- Keyboard support now matches the full WAI-ARIA APG slider pattern: ArrowUp/Right and ArrowDown/Left
|
|
1258
|
+
move by `step` (RTL-aware — under `direction: rtl` the forward/backward keys swap so they still
|
|
1259
|
+
track the visually-adjacent direction), PageUp/PageDown move by `step * 10`, and Home/End jump to
|
|
1260
|
+
that handle's actual *reachable* bound — clamped by the sibling handle's current value, not the
|
|
1261
|
+
component's full `[min, max]` domain, so Home/End on the `end` handle can't jump past `start` (and
|
|
1262
|
+
vice versa). Pointer-drag is RTL-aware the same way (mirrors the drag ratio under `direction:
|
|
1263
|
+
rtl`).
|
|
1264
|
+
- A disabled handle now gets `aria-disabled="true"` in addition to losing `tabindex` — a
|
|
1265
|
+
screen-reader user exploring by virtual cursor no longer hears it announced as a live, adjustable
|
|
1266
|
+
slider.
|
|
1267
|
+
- `aria-valuemin`/`aria-valuemax` on each handle report that handle's reachable sub-range (bounded by
|
|
1268
|
+
its sibling), not the full domain — matching what Home/End actually jump to.
|
|
1269
|
+
- No `aria-valuetext`: only raw numeric `aria-valuenow` is exposed (omitted entirely, rather than a
|
|
1270
|
+
literal `"NaN"`, if `start`/`end` is non-finite), no hook for a human-readable (e.g. formatted
|
|
1271
|
+
date/time) equivalent of the mapped domain.
|
|
1272
|
+
- Handles a `min > max` domain, a non-positive/non-finite `step`, and disabled-mid-drag/
|
|
1273
|
+
disconnect-mid-drag correctly (tested) — safe to rely on those edge cases. Concurrent drags are
|
|
1274
|
+
tracked per `pointerId` (not a single scalar), so a two-finger touch — one finger per handle —
|
|
1275
|
+
moves both independently instead of the second pointer hijacking which handle the first pointer's
|
|
1276
|
+
moves apply to; `pointercancel`/`lostpointercapture` (not just `pointerup`) both end a drag, same
|
|
1277
|
+
fix as `lyra-split`.
|
|
851
1278
|
|
|
852
1279
|
---
|
|
853
1280
|
|
|
@@ -875,7 +1302,8 @@ tick and manual step)
|
|
|
875
1302
|
**CSS parts:** `base`, `play-button`, `slider`
|
|
876
1303
|
|
|
877
1304
|
**Themeable custom properties:** shared tokens only — `--lyra-space-s`, `--lyra-color-border`,
|
|
878
|
-
`--lyra-color-surface`, `--lyra-color-text`, `--lyra-color-brand
|
|
1305
|
+
`--lyra-color-surface`, `--lyra-color-text`, `--lyra-color-brand`, `--lyra-icon-button-size` (the
|
|
1306
|
+
play button's box).
|
|
879
1307
|
|
|
880
1308
|
**Optional peer deps:** none.
|
|
881
1309
|
|
|
@@ -888,9 +1316,15 @@ tick and manual step)
|
|
|
888
1316
|
```
|
|
889
1317
|
|
|
890
1318
|
**Known gotchas:**
|
|
891
|
-
- `index` is
|
|
892
|
-
|
|
893
|
-
|
|
1319
|
+
- `index` is now re-clamped into `[0, length)` as soon as `length` shrinks (in `willUpdate()`, not
|
|
1320
|
+
waiting for the next `tick()`/`goTo()`/`next()`/`previous()` call) — setting `el.length = 2` while
|
|
1321
|
+
`el.index = 7` immediately pulls `index` back to `1`, and playback auto-pauses if `length` drops
|
|
1322
|
+
to `<= 1` while playing (the play button and slider would otherwise both become disabled with no
|
|
1323
|
+
way to stop it — both are `?disabled` whenever `length <= 1`, not just the button).
|
|
1324
|
+
- `intervalMs` is live-reactive mid-playback: ticking is a self-rescheduling `setTimeout` (not one
|
|
1325
|
+
long-lived `setInterval`), so `intervalMs` is re-read fresh before every tick — changing
|
|
1326
|
+
`interval-ms` while `playing` takes effect on the very next step instead of only after a
|
|
1327
|
+
pause/play cycle.
|
|
894
1328
|
- No `aria-valuetext`/visible "N of M" position label on the range input.
|
|
895
1329
|
- Calling `play()`/`pause()` programmatically (not via the button) gives no `aria-live`
|
|
896
1330
|
announcement of the Play/Pause state change.
|
|
@@ -899,31 +1333,70 @@ tick and manual step)
|
|
|
899
1333
|
|
|
900
1334
|
## `lyra-heatmap`
|
|
901
1335
|
|
|
902
|
-
A Canvas-rendered
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1336
|
+
A Canvas-rendered heatmap with a DPR-aware, resize-aware redraw loop, in one of two `mode`s:
|
|
1337
|
+
`"matrix"` (default — a `rowLabels` × `colLabels` grid of `values`) or `"calendar"` (a
|
|
1338
|
+
GitHub-style Sunday–Saturday × week grid built from `days`, colored by quartile bucket rather than
|
|
1339
|
+
the matrix mode's continuous ramp). Every cell is independently addressable despite being
|
|
1340
|
+
canvas-drawn (no per-cell DOM node): a `pointermove` hit-test over the canvas shows `[part="tooltip"]`
|
|
1341
|
+
with that cell's label + value; the canvas is `tabindex="0"` with arrow-key roving focus (a stroked
|
|
1342
|
+
ring redrawn over the focused cell on every draw, plus `[part="live-region"]` announcing it); and a
|
|
1343
|
+
click, or Enter/Space on the focused cell, fires `lyra-cell-click`.
|
|
906
1344
|
|
|
907
1345
|
**Properties:**
|
|
908
|
-
- `rowLabels: string[] = []` (attribute: false)
|
|
909
|
-
- `colLabels: string[] = []` (attribute: false)
|
|
910
|
-
- `values: number[][] = []` (attribute: false) — `-1`
|
|
911
|
-
are safe (`?? -1`)
|
|
912
|
-
- `cellSize: number = 22` (attribute `cell-size`
|
|
1346
|
+
- `rowLabels: string[] = []` (attribute: false — matrix mode only)
|
|
1347
|
+
- `colLabels: string[] = []` (attribute: false — matrix mode only)
|
|
1348
|
+
- `values: number[][] = []` (attribute: false — matrix mode only) — `-1` or any non-finite value is
|
|
1349
|
+
the "no data" sentinel; ragged/sparse rows are safe (`?? -1`)
|
|
1350
|
+
- `cellSize: number = 22` (attribute `cell-size` — matrix mode only, ignored when `fitToWidth` is
|
|
1351
|
+
set)
|
|
1352
|
+
- `fitToWidth: boolean = false` (attribute `fit-to-width` — matrix mode only; derives `cellSize` from
|
|
1353
|
+
the host's measured `clientWidth` on every draw/resize instead of the fixed `cell-size`, so the
|
|
1354
|
+
grid actually fills the available width — see gotchas for the default, non-`fit-to-width` behavior)
|
|
913
1355
|
- `valueLabel: string = 'value'` (attribute `value-label`)
|
|
914
|
-
- `scale: 'linear' | 'sqrt' = 'linear'`
|
|
915
|
-
|
|
916
|
-
|
|
1356
|
+
- `scale: 'linear' | 'sqrt' = 'linear'` (matrix mode only — calendar mode always buckets by quartile)
|
|
1357
|
+
- `mode: 'matrix' | 'calendar' = 'matrix'`
|
|
1358
|
+
- `days: CalendarDay[] = []` (attribute: false — calendar mode only) — `CalendarDay { date:
|
|
1359
|
+
string /* ISO yyyy-mm-dd */; value: number }`; need not be sorted or contiguous, and an entry whose
|
|
1360
|
+
`date` doesn't parse is dropped rather than poisoning the whole grid
|
|
1361
|
+
- `bucketCount: number = 5` (attribute `bucket-count` — calendar mode only; non-finite values fall
|
|
1362
|
+
back to 5 with a one-time console warning, and the count is floored to an integer >= 2)
|
|
1363
|
+
- `annotations: HeatmapAnnotation[] = []` (attribute: false) — `HeatmapAnnotation { row?: number;
|
|
1364
|
+
col?: number; date?: string; label?: string }`: matrix mode matches by `row`/`col`, calendar mode
|
|
1365
|
+
by `date` (whichever pair matches the active `mode`; the other fields are ignored). Draws a
|
|
1366
|
+
stroked ring over the matching cell; an annotation with a `label` also gets its own
|
|
1367
|
+
`[part="legend-annotation"]` entry in the legend.
|
|
1368
|
+
- `cellText?: (pos: MatrixCellPos | CalendarCellPos, value: number) => string` (attribute: false) —
|
|
1369
|
+
formats the per-cell hover tooltip and keyboard live-region announcement text; receives the cell
|
|
1370
|
+
position (`{ row, col }` in matrix mode, `{ week, weekday }` in calendar mode) and its value.
|
|
1371
|
+
Unset (the default) falls back to the built-in English "Row X, Col Y: value" / "Mon DD: value"
|
|
1372
|
+
template — additive, not breaking.
|
|
1373
|
+
|
|
1374
|
+
**Events:** `lyra-cell-click` (fired on click, or Enter/Space on the keyboard-focused cell —
|
|
1375
|
+
`detail: { row, col, value }` in matrix mode, `detail: { date, value }` in calendar mode)
|
|
917
1376
|
|
|
918
1377
|
**Slots:** none.
|
|
919
1378
|
|
|
920
|
-
**CSS parts:** `base`, `canvas`, `
|
|
1379
|
+
**CSS parts:** `base`, `canvas`, `tooltip` (hover tooltip, positioned over the hovered cell),
|
|
1380
|
+
`live-region` (visually-hidden `role="status" aria-live="polite"` element announcing the
|
|
1381
|
+
keyboard-focused cell), `legend`, `legend-lo`, `legend-hi`, `legend-annotation` (one per labeled
|
|
1382
|
+
`annotations` entry)
|
|
921
1383
|
|
|
922
1384
|
**Themeable custom properties:** `--lyra-heatmap-scale-lo` (default `#cde2fb`),
|
|
923
|
-
`--lyra-heatmap-scale-hi` (default `#0969da`) — the sequential color-ramp endpoints
|
|
924
|
-
`getComputedStyle` each draw (any valid
|
|
925
|
-
resolved through a scratch canvas).
|
|
926
|
-
`--lyra-
|
|
1385
|
+
`--lyra-heatmap-scale-hi` (default `#0969da`) — the sequential color-ramp endpoints (matrix mode) or
|
|
1386
|
+
quartile-bucket ramp endpoints (calendar mode), resolved via `getComputedStyle` each draw (any valid
|
|
1387
|
+
CSS color syntax — hex/rgb/hsl/oklch/named — works, resolved through a scratch canvas).
|
|
1388
|
+
`--lyra-heatmap-no-data-fill` (default `rgba(128,128,128,0.25)` — the no-data cell fill, same
|
|
1389
|
+
resolve-via-`getComputedStyle` pattern), `--lyra-heatmap-label-font` (default `10px sans-serif` — the
|
|
1390
|
+
canvas-drawn axis/month/weekday label font), `--lyra-heatmap-focus-ring-color` (default
|
|
1391
|
+
`var(--lyra-focus-ring-color)` — the canvas-drawn ring stroked around the keyboard-focused cell;
|
|
1392
|
+
also reused by `[part="canvas"]`'s own `:focus-visible` outline so the two stay visually in sync),
|
|
1393
|
+
`--lyra-heatmap-annotation-color` (default `var(--lyra-color-danger)` — the canvas-drawn ring
|
|
1394
|
+
stroked around an annotated cell, deliberately not one of the sequential ramp colors so it stays
|
|
1395
|
+
visible regardless of what it's drawn over). `--lyra-heatmap-tooltip-bg` (default
|
|
1396
|
+
`var(--lyra-color-surface)`) and `--lyra-heatmap-tooltip-text` (default `var(--lyra-color-text)`) —
|
|
1397
|
+
unlike the canvas-drawn tokens above, `[part="tooltip"]` is a real DOM element and consumes these
|
|
1398
|
+
directly, no `getComputedStyle` bridging needed. Also consumes `--lyra-color-text-quiet` (axis label
|
|
1399
|
+
color) and `--lyra-space-xs`.
|
|
927
1400
|
|
|
928
1401
|
**Optional peer deps:** none.
|
|
929
1402
|
|
|
@@ -937,20 +1410,39 @@ resolved through a scratch canvas). Also consumes `--lyra-color-text-quiet` (axi
|
|
|
937
1410
|
</script>
|
|
938
1411
|
```
|
|
939
1412
|
|
|
1413
|
+
```html
|
|
1414
|
+
<!-- Calendar mode: a GitHub-contributions-style day grid -->
|
|
1415
|
+
<lyra-heatmap mode="calendar" value-label="commits"></lyra-heatmap>
|
|
1416
|
+
<script>
|
|
1417
|
+
document.querySelector('lyra-heatmap').days = [
|
|
1418
|
+
{ date: '2026-01-01', value: 3 },
|
|
1419
|
+
{ date: '2026-01-02', value: 0 },
|
|
1420
|
+
// ...
|
|
1421
|
+
];
|
|
1422
|
+
</script>
|
|
1423
|
+
```
|
|
1424
|
+
|
|
940
1425
|
**Known gotchas:**
|
|
941
|
-
- the `ResizeObserver`
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
style
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1426
|
+
- the `ResizeObserver` only actually resizes the drawn grid in matrix mode **when
|
|
1427
|
+
`fit-to-width` is set**. Without it (the default), `draw()` sizes the canvas as
|
|
1428
|
+
`PAD_LEFT + cols * cellSize`, never from the host's measured width, so a container-resize redraw is
|
|
1429
|
+
a geometric no-op; the stylesheet's `canvas { inline-size: 100% }` is also dead code in that case,
|
|
1430
|
+
since `draw()` unconditionally sets an inline `canvas.style.width/height` that wins over it.
|
|
1431
|
+
Calendar mode ignores `fit-to-width` entirely — its canvas is always sized from the computed
|
|
1432
|
+
`weekCount`.
|
|
1433
|
+
- the host is `role="group"` (not `role="img"`) with a dimensions+range summary `aria-label`
|
|
1434
|
+
(calendar mode: a day-count + range summary instead) — `[part="canvas"]` inside it is a real
|
|
1435
|
+
focusable, keyboard-operable, per-cell-interactive control (roving arrow-key focus,
|
|
1436
|
+
`[part="live-region"]` announcements, `lyra-cell-click`), and `role="img"` is documented (ARIA) to
|
|
1437
|
+
flatten its subtree to a single image for some assistive tech, which conflicted with that
|
|
1438
|
+
focusable descendant — fixed, matching `lyra-lite-chart`/`lyra-word-cloud`'s existing `role="group"`
|
|
1439
|
+
pattern.
|
|
1440
|
+
- `NaN`/non-finite cell values in matrix mode are correctly treated as no-data now (alongside `-1`),
|
|
1441
|
+
and repeated DPR crossings (moving the window across displays with different pixel ratios) no
|
|
1442
|
+
longer leak a `MediaQueryList` listener per crossing — both previously-known issues are fixed.
|
|
1443
|
+
- calendar mode's date labels (used by the default `cellText` template and the tooltip/live-region
|
|
1444
|
+
text) now format via the runtime locale (`toLocaleString(undefined, ...)`) instead of a hardcoded
|
|
1445
|
+
`'en'` — fixed.
|
|
954
1446
|
|
|
955
1447
|
---
|
|
956
1448
|
|
|
@@ -966,8 +1458,14 @@ A force-directed node-link diagram with pan/zoom/drag, built on `d3-force`.
|
|
|
966
1458
|
dropped)
|
|
967
1459
|
- `width: number = 800`
|
|
968
1460
|
- `height: number = 600`
|
|
969
|
-
- `chargeStrength: number = -300` (attribute `charge-strength`)
|
|
970
|
-
- `linkDistance: number = 100` (attribute `link-distance`)
|
|
1461
|
+
- `chargeStrength: number = -300` (attribute `charge-strength` — live-reactive, see gotchas)
|
|
1462
|
+
- `linkDistance: number = 100` (attribute `link-distance` — live-reactive, see gotchas)
|
|
1463
|
+
- `minZoom: number = 0.1` (attribute `min-zoom`)
|
|
1464
|
+
- `maxZoom: number = 8` (attribute `max-zoom`)
|
|
1465
|
+
- `seed?: number` — when set, seeds each node's initial x/y deterministically
|
|
1466
|
+
(keyed by node **id**, not array index/order) instead of `forceSimulation()`'s own random start,
|
|
1467
|
+
and settles the simulation synchronously instead of animating the settle (same effect
|
|
1468
|
+
`prefers-reduced-motion` has, see gotchas)
|
|
971
1469
|
|
|
972
1470
|
**Events:** `lyra-node-click` (`detail: { id }`), `lyra-link-click` (`detail: { source, target }`)
|
|
973
1471
|
|
|
@@ -994,17 +1492,36 @@ install with `pnpm add d3-force d3-drag d3-zoom d3-selection`).
|
|
|
994
1492
|
|
|
995
1493
|
**Known gotchas:**
|
|
996
1494
|
- per-tick full re-render is expensive: every d3-force tick (up to ~300 by default,
|
|
997
|
-
continuously while dragging via `alphaTarget(0.3)`)
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
-
|
|
1003
|
-
|
|
1004
|
-
`
|
|
1005
|
-
|
|
1006
|
-
-
|
|
1007
|
-
|
|
1495
|
+
continuously while dragging via `alphaTarget(0.3)`) writes node/link positions straight onto the
|
|
1496
|
+
already-rendered DOM via `setAttribute()` rather than reassigning `simNodes`/`simLinks` (that
|
|
1497
|
+
reassignment — and the Lit re-render/`applyInteractions()` re-scan it used to force on every tick —
|
|
1498
|
+
now only happens once per structural `nodes`/`links` change). Still a noticeable cost building up
|
|
1499
|
+
the initial layout or while a node is actively being dragged, just no longer once per tick on an
|
|
1500
|
+
otherwise-settled graph.
|
|
1501
|
+
- `chargeStrength`/`linkDistance` **are** live-reactive post-mount now (retuned on the existing
|
|
1502
|
+
force objects and the simulation nudged via `alpha(0.3).restart()`) — no need to also touch
|
|
1503
|
+
`nodes`/`links` to see the effect.
|
|
1504
|
+
- zoom is bounded via `minZoom`/`maxZoom` (`d3-zoom`'s `.scaleExtent(...)`, live-reactive); pan/
|
|
1505
|
+
zoom/drag are still pointer-only with no keyboard equivalent. Links (`<line part="link">`) are now
|
|
1506
|
+
keyboard-operable too (`tabindex="0"`, `role="button"`, `aria-label`, Enter/Space), matching nodes.
|
|
1507
|
+
- while the `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection` peers are resolving, the host shows a
|
|
1508
|
+
`<lyra-skeleton>` sized to `width`/`height` with `aria-busy="true"` — but if they fail to load
|
|
1509
|
+
(not installed), it still settles into a permanently empty `<svg>` (0 nodes/0 links) plus a
|
|
1510
|
+
one-time console warning, same as before; the skeleton only covers the loading window itself.
|
|
1511
|
+
- `GraphNode.color` is sanitized (rejects `;`/`{`/`}`) before being written into the
|
|
1512
|
+
`--lyra-node-fill` inline custom property, so an untrusted color string can't break out of that
|
|
1513
|
+
CSS declaration.
|
|
1514
|
+
- a structural `nodes`/`links` change now carries over each already-settled node's position (and any
|
|
1515
|
+
in-progress drag) by id when rebuilding the simulation, instead of discarding every node's (x, y)
|
|
1516
|
+
and re-running the whole ~300-tick random-start settle from scratch — only genuinely new ids get a
|
|
1517
|
+
fresh start. Handy for a streaming/incrementally-updated graph, whose existing layout no longer
|
|
1518
|
+
jumps every time a node/link is appended.
|
|
1519
|
+
- under `prefers-reduced-motion: reduce`, or whenever `seed` is set, the simulation converges
|
|
1520
|
+
synchronously (ticked in a loop down to `alphaMin` before first paint) instead of animating over
|
|
1521
|
+
~300 rendered frames; user-initiated motion (dragging a node) is unaffected either way.
|
|
1522
|
+
- the `<svg part="svg">` now carries `role="group"` and an `aria-label` summarizing the node/link
|
|
1523
|
+
counts (e.g. "Node-link diagram with 5 nodes and 4 links"), and node `<text part="label">`s are
|
|
1524
|
+
`aria-hidden="true"` (their content is already covered by each node's own `aria-label`).
|
|
1008
1525
|
|
|
1009
1526
|
---
|
|
1010
1527
|
|
|
@@ -1016,14 +1533,30 @@ recursively renders itself and its own nested children.
|
|
|
1016
1533
|
|
|
1017
1534
|
### `lyra-tree`
|
|
1018
1535
|
|
|
1536
|
+
Implements the full WAI-ARIA treeitem keyboard pattern: a single roving `tabindex` (tracked as
|
|
1537
|
+
`activeId`, pushed down to every `<lyra-tree-node>` including nested ones) and
|
|
1538
|
+
ArrowUp/Down/Right/Left/Home/End/Enter/Space handled by one delegated `keydown` listener (native
|
|
1539
|
+
`KeyboardEvent`s are `composed: true` and bubble across shadow-DOM boundaries, so a press inside a
|
|
1540
|
+
deeply-nested node's own shadow root still reaches it).
|
|
1541
|
+
|
|
1019
1542
|
**Properties:**
|
|
1020
1543
|
- `data: TreeItem[] = []` (attribute: false) — `TreeItem { id: string; label: string; children?:
|
|
1021
1544
|
TreeItem[]; badge?: string | number }`
|
|
1545
|
+
- `label: string = ''` — accessible name for the tree; `role="tree"` lives on an internal
|
|
1546
|
+
`[part="base"]` element, not the host, so this is the only way to label it (`aria-label`/
|
|
1547
|
+
`aria-labelledby` set directly on `<lyra-tree>` wouldn't reach it)
|
|
1548
|
+
|
|
1549
|
+
**Keyboard:** ArrowDown/ArrowUp move the roving focus to the next/previous *visible* node.
|
|
1550
|
+
ArrowRight expands a collapsed node (focus stays put; a second ArrowRight then steps into the first
|
|
1551
|
+
child) or moves into an already-expanded node's first child. ArrowLeft collapses an expanded node, or
|
|
1552
|
+
moves focus to its parent. Home/End jump to the first/last visible node. Enter/Space activate
|
|
1553
|
+
`select()` on the focused node.
|
|
1022
1554
|
|
|
1023
1555
|
**Methods:** `expandAll()`, `collapseAll()` (both recursive, properly sequenced around Lit's render
|
|
1024
1556
|
cycle).
|
|
1025
1557
|
|
|
1026
|
-
**Events:** none dispatched directly (see `lyra-tree-node` below — they bubble up
|
|
1558
|
+
**Events:** none dispatched directly (see `lyra-tree-node` below — they bubble up and are also
|
|
1559
|
+
observed internally to keep the roving `activeId` in sync with clicks).
|
|
1027
1560
|
|
|
1028
1561
|
**Slots:** default (holds the `<lyra-tree-node>` elements it manages).
|
|
1029
1562
|
|
|
@@ -1036,15 +1569,29 @@ cycle).
|
|
|
1036
1569
|
|
|
1037
1570
|
### `lyra-tree-node`
|
|
1038
1571
|
|
|
1039
|
-
Normally set internally by `lyra-tree`, but a public element.
|
|
1572
|
+
Normally set internally by `lyra-tree`, but a public element. `role="treeitem"` (plus
|
|
1573
|
+
`aria-expanded`/`aria-level`/`aria-setsize`/`aria-posinset` and the roving `tabindex`, driven by
|
|
1574
|
+
`<lyra-tree>`) live on the *host* element itself, not an internal row `<div>` — so this node's own
|
|
1575
|
+
nested children (rendered in its own shadow root as further `role="group"` content) are genuine DOM
|
|
1576
|
+
descendants of the treeitem, matching the WAI-ARIA treeitem pattern's containment expectation.
|
|
1040
1577
|
|
|
1041
1578
|
**Properties:**
|
|
1042
1579
|
- `item: TreeItem` (required, attribute: false)
|
|
1043
1580
|
- `depth: number = 0`
|
|
1044
1581
|
- `expanded: boolean = false` (reflected)
|
|
1582
|
+
- `activeId: string | null = null` (attribute: false) — the id of the tree's roving-tabindex-focused
|
|
1583
|
+
item, pushed down from `<lyra-tree>`; normally set internally, not by consumers
|
|
1584
|
+
- `setSize: number = 1`, `posInSet: number = 1` (attribute: false) — this node's `aria-setsize`/
|
|
1585
|
+
`aria-posinset` values among its siblings, pushed down from `<lyra-tree>`; normally set internally,
|
|
1586
|
+
not by consumers
|
|
1045
1587
|
|
|
1046
|
-
**
|
|
1047
|
-
|
|
1588
|
+
**Methods:** `expand()`, `collapse()` (each a no-op if already in that state, or a leaf), `select()`
|
|
1589
|
+
(fires `lyra-node-select`).
|
|
1590
|
+
|
|
1591
|
+
**Events:** `lyra-node-toggle` (`detail: { id, expanded }`, fired by `expand()`/`collapse()` — via
|
|
1592
|
+
the toggle button or ArrowRight/ArrowLeft), `lyra-node-select` (`detail: { id }`, fired by `select()`
|
|
1593
|
+
— via clicking the label or Enter/Space) — dispatched from `lyra-tree-node`, bubble/compose up
|
|
1594
|
+
through `lyra-tree`'s light DOM.
|
|
1048
1595
|
|
|
1049
1596
|
**Slots:** none.
|
|
1050
1597
|
|
|
@@ -1065,20 +1612,17 @@ indentation), plus the shared tokens listed above.
|
|
|
1065
1612
|
```
|
|
1066
1613
|
|
|
1067
1614
|
**Known gotchas:**
|
|
1068
|
-
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
`repeat()`, so reordering/inserting into a nested array can silently attach the wrong node's
|
|
1080
|
-
`expanded` state.
|
|
1081
|
-
- `role="tree"` has no accessible name (`aria-label`/`aria-labelledby`).
|
|
1615
|
+
- all four previously-known ARIA gaps in this pair are fixed: the treeitem row is now genuinely
|
|
1616
|
+
keyboard-operable with a roving tabindex and full arrow-key navigation (not just the expand/collapse
|
|
1617
|
+
button); the expanded-children `role="group"` is now a real DOM descendant of its `role="treeitem"`
|
|
1618
|
+
host rather than a shadow-DOM sibling; by-id reconciliation (preserving `expanded` state across
|
|
1619
|
+
data reassignment) now applies at every depth via a keyed `repeat()`, not just depth 0; and
|
|
1620
|
+
`role="tree"` now has an accessible name via the new `label` property.
|
|
1621
|
+
- `lyra-tree`'s `getUpdateComplete()` cascades into every currently-known descendant
|
|
1622
|
+
`<lyra-tree-node>`'s own `updateComplete` (see `update-cascade.ts`) so that code awaiting the
|
|
1623
|
+
tree's `updateComplete` (e.g. after `focusNode()`) doesn't run before an arbitrarily-nested node has
|
|
1624
|
+
actually finished rendering its pushed-down `activeId`/`tabIndex` — one more pending update per
|
|
1625
|
+
depth level, otherwise.
|
|
1082
1626
|
|
|
1083
1627
|
---
|
|
1084
1628
|
|
|
@@ -1091,9 +1635,9 @@ Chart.js wrapper every other `lyra-*-chart` tag subclasses; supports both a simp
|
|
|
1091
1635
|
`wa-chart` `config` property).
|
|
1092
1636
|
|
|
1093
1637
|
**Properties:**
|
|
1094
|
-
- `type: LyraChartType = 'line'` — `LyraChartType = 'line' | 'bar' | 'scatter'
|
|
1095
|
-
|
|
1096
|
-
|
|
1638
|
+
- `type: LyraChartType = 'line'` — `LyraChartType = 'line' | 'bar' | 'scatter' | 'pie' | 'doughnut' |
|
|
1639
|
+
'radar' | 'polarArea' | 'bubble'` — every type string the typed subclasses below lock `type` to is
|
|
1640
|
+
already a first-class member, so `<lyra-chart type="pie">` needs no subclass or cast to work
|
|
1097
1641
|
- `labels: string[] = []` (attribute: false)
|
|
1098
1642
|
- `datasets: Series[] = []` (attribute: false) — `Series { label: string; data?: (number|null)[];
|
|
1099
1643
|
points?: {x,y,label?}[]; color?: string|string[]; fill?: boolean; width?: number; dash?: boolean;
|
|
@@ -1107,13 +1651,23 @@ Chart.js wrapper every other `lyra-*-chart` tag subclasses; supports both a simp
|
|
|
1107
1651
|
- `yLabel: string = ''` (attribute `y-label`)
|
|
1108
1652
|
- `y2Label: string = ''` (attribute `y2-label`)
|
|
1109
1653
|
- `beginAtZero: boolean = true` (attribute `begin-at-zero`)
|
|
1654
|
+
- `horizontal: boolean = false` — sets `options.indexAxis = 'y'`, Chart.js's own mechanism for
|
|
1655
|
+
horizontal bars (also flips `line`/`area` types onto a horizontal category axis)
|
|
1656
|
+
- `stacked: boolean = false` — stacks the `x`/`y`(/`y2`) scale entries `buildScales()` returns; only
|
|
1657
|
+
meaningful for `bar`/`line` types (scatter/bubble's linear `x` scale and the radial `r` scale used
|
|
1658
|
+
by radar/polar-area are out of scope)
|
|
1110
1659
|
- `config?: Partial<ChartConfiguration>` (attribute: false) — deep-merged over the generated
|
|
1111
1660
|
config; any nested key wins without clobbering sibling generated keys
|
|
1112
1661
|
|
|
1113
|
-
**Methods:** `resetZoom()`
|
|
1662
|
+
**Methods:** `resetZoom()` (reset any active zoom/pan to the original view), `refreshTheme()`
|
|
1663
|
+
(forces a redraw so the `--lyra-chart-*` tokens below are re-read from the current computed style —
|
|
1664
|
+
the escape hatch for a consumer's own theme-toggle handler to call when it flips something, e.g. a
|
|
1665
|
+
`data-theme` attribute, that doesn't otherwise change any `lyra-chart` property)
|
|
1114
1666
|
|
|
1115
1667
|
**Events:** `lyra-zoom` (`detail: { zoomed: boolean }`, fired on zoom-complete and on
|
|
1116
|
-
`resetZoom()`)
|
|
1668
|
+
`resetZoom()`), `lyra-point-click` (fired when a click lands on, or nearest to — intersect-only —
|
|
1669
|
+
a data point/segment, for any chart type, not just bar; `detail: { datasetIndex: number, index:
|
|
1670
|
+
number, label: string | undefined, value: unknown }`)
|
|
1117
1671
|
|
|
1118
1672
|
**Slots:** none.
|
|
1119
1673
|
|
|
@@ -1121,8 +1675,13 @@ Chart.js wrapper every other `lyra-*-chart` tag subclasses; supports both a simp
|
|
|
1121
1675
|
|
|
1122
1676
|
**Themeable custom properties:** `--lyra-chart-height` (set programmatically on the host from the
|
|
1123
1677
|
`height` property — must be read from the host, not a shadow-tree descendant, since custom
|
|
1124
|
-
properties only cascade downward);
|
|
1125
|
-
`--lyra-color-
|
|
1678
|
+
properties only cascade downward); `--lyra-chart-grid-color` (default `var(--lyra-color-border)`),
|
|
1679
|
+
`--lyra-chart-tick-color` (default `var(--lyra-color-text-quiet)`), `--lyra-chart-legend-color`
|
|
1680
|
+
(default `var(--lyra-color-text)`), `--lyra-chart-tooltip-bg` (default `var(--lyra-color-surface)`),
|
|
1681
|
+
`--lyra-chart-tooltip-text` (default `var(--lyra-color-text)`) — each resolved fresh via
|
|
1682
|
+
`getComputedStyle` on every draw (Chart.js renders to canvas, not the DOM, so it can't consume CSS
|
|
1683
|
+
`var()` directly), driving the grid lines, tick labels, legend text, and tooltip background/text
|
|
1684
|
+
respectively; plus shared `--lyra-space-xs`.
|
|
1126
1685
|
|
|
1127
1686
|
**Optional peer deps:** `chart.js`, `chartjs-plugin-zoom` (lazy-loaded once per page via
|
|
1128
1687
|
`chart-loader.ts`; registers only the tree-shaken controller/element/scale subset used).
|
|
@@ -1137,28 +1696,124 @@ properties only cascade downward); shared tokens `--lyra-space-xs`, `--lyra-colo
|
|
|
1137
1696
|
```
|
|
1138
1697
|
|
|
1139
1698
|
**Known gotchas:**
|
|
1140
|
-
-
|
|
1141
|
-
`<lyra-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
-
|
|
1150
|
-
`lyra-polar-area-chart
|
|
1151
|
-
`
|
|
1152
|
-
|
|
1699
|
+
- `type` at the `lyra-chart` (core) level is completely unchecked — `buildConfig()` passes it
|
|
1700
|
+
straight to Chart.js as-is, so `<lyra-chart type="pie">` already works today with no subclass
|
|
1701
|
+
needed. Each typed subclass below locks its *own* `type` via a real prototype accessor (see the
|
|
1702
|
+
typed-subclasses section) — a genuine runtime lock, not just a compile-time default.
|
|
1703
|
+
- no global theme-broadcast event exists to auto-retheme an already-drawn chart on a theme switch —
|
|
1704
|
+
a consumer flipping something upstream (e.g. a `data-theme` attribute) that doesn't otherwise
|
|
1705
|
+
change any `lyra-chart` property must call `refreshTheme()` itself to make Chart.js re-read the
|
|
1706
|
+
`--lyra-chart-*` tokens above.
|
|
1707
|
+
- generated `scales` are type-appropriate: no scale at all for `lyra-pie-chart`/
|
|
1708
|
+
`lyra-doughnut-chart`, and a single radial `r` scale (respecting `beginAtZero`) for
|
|
1709
|
+
`lyra-radar-chart`/`lyra-polar-area-chart`, instead of always generating the cartesian `x`/`y`/`y2`
|
|
1710
|
+
block. `xLabel`/`yLabel`/`y2Label` are still silently inert for all four of those subclasses (a
|
|
1711
|
+
radial scale and "no scale" both have nowhere to put an axis title) — reach a titled radial scale
|
|
1712
|
+
only via raw `config`.
|
|
1153
1713
|
- No `chartjs-plugin-annotation` is registered by default — reachable only by importing it
|
|
1154
1714
|
separately and using the raw `config` passthrough (Chart.js's registry is a global singleton).
|
|
1715
|
+
- while the `chart.js` peer is resolving, the host shows a `<lyra-skeleton variant="rect">` with
|
|
1716
|
+
`aria-busy="true"` (same lazy-load pattern as `lyra-graph`/`lyra-map`/`lyra-flag`); Chart.js's own
|
|
1717
|
+
~1s draw-in animation is skipped under `prefers-reduced-motion: reduce`. The raw `config`
|
|
1718
|
+
passthrough is deep-merged with `__proto__`/`constructor`/`prototype` keys skipped unconditionally,
|
|
1719
|
+
so a JSON-sourced `config` (e.g. parsed from an API response) can't reach up and pollute
|
|
1720
|
+
`Object.prototype` through the merge.
|
|
1721
|
+
- lazy-redraw + memoization: an `IntersectionObserver` gates `draw()` — while the host is scrolled
|
|
1722
|
+
off-screen, property changes that would otherwise trigger a Chart.js redraw are skipped (and a
|
|
1723
|
+
single redraw fires once it re-enters the viewport). Independently, a content-signature check
|
|
1724
|
+
(`type`, `labels`, `datasets`, `legend`, `area`, `xLabel`, `yLabel`, `y2Label`, `beginAtZero`,
|
|
1725
|
+
`horizontal`, `stacked`, `config`) skips calling into Chart.js when none of those have actually
|
|
1726
|
+
changed since the last draw (e.g. an unrelated property/state update, or a bare `requestUpdate()`).
|
|
1727
|
+
`refreshTheme()` calls `draw()` directly and is unaffected by either gate — it always redraws, since
|
|
1728
|
+
a theme change isn't reflected in any tracked property.
|
|
1729
|
+
|
|
1730
|
+
---
|
|
1731
|
+
|
|
1732
|
+
## `lyra-lite-chart`
|
|
1733
|
+
|
|
1734
|
+
A dependency-free bar/line chart — plain SVG/DOM rendering, zero peer dependencies (unlike
|
|
1735
|
+
`lyra-chart`, which wraps `chart.js`). For a project whose architecture forbids a charting
|
|
1736
|
+
dependency outright: covers grouped/stacked bars, multi-series lines, per-point click, and hover
|
|
1737
|
+
tooltips (native SVG `<title>`, no positioning JS) — not a full `lyra-chart` replacement (no
|
|
1738
|
+
zoom/pan, no pie/doughnut/radar/scatter/bubble types, no horizontal/dual-y-axis, no raw-config
|
|
1739
|
+
passthrough). Not a subclass of `LyraChart`.
|
|
1740
|
+
|
|
1741
|
+
**Properties:**
|
|
1742
|
+
- `type: LyraLiteChartType = 'bar'` — `'bar' | 'line'`
|
|
1743
|
+
- `labels: string[] = []` (attribute: false)
|
|
1744
|
+
- `datasets: LiteSeries[] = []` (attribute: false) — `LiteSeries { label: string; data:
|
|
1745
|
+
(number|null)[]; color?: string }`
|
|
1746
|
+
- `legend: boolean = false`
|
|
1747
|
+
- `height: string = '280px'`
|
|
1748
|
+
- `xLabel: string = ''` (attribute `x-label`)
|
|
1749
|
+
- `yLabel: string = ''` (attribute `y-label`)
|
|
1750
|
+
- `beginAtZero: boolean = true` (attribute `begin-at-zero`)
|
|
1751
|
+
- `stacked: boolean = false` — sums each category's bars into one segmented bar instead of grouping
|
|
1752
|
+
them side by side; ignored for `type="line"`
|
|
1753
|
+
- `tickFormat?: (value: number) => string` (attribute: false) — formats a y-axis tick value for
|
|
1754
|
+
display (e.g. `(v) => \`$${v.toFixed(2)}\`` for currency, or a duration formatter for `"42s"`).
|
|
1755
|
+
Falls back to the built-in "nice numbers" formatter when unset.
|
|
1756
|
+
|
|
1757
|
+
**Events:** `lyra-point-click` — fired when a bar/point is activated (click, or Enter/Space while
|
|
1758
|
+
focused). `detail: { datasetIndex: number, index: number, label: string | undefined, value: number
|
|
1759
|
+
| null }` — same shape as `lyra-chart`'s `lyra-point-click`.
|
|
1760
|
+
|
|
1761
|
+
**Performance:** rendering is `IntersectionObserver`-gated and content-signature-memoized — `render()`
|
|
1762
|
+
skips recomputing the grid/marks (and reuses the previous `TemplateResult`) while the host is scrolled
|
|
1763
|
+
off-screen, or when none of the content-affecting properties (`type`, `labels`, `datasets`, `legend`,
|
|
1764
|
+
`xLabel`, `yLabel`, `beginAtZero`, `stacked`, or the measured plot size) have changed since the last
|
|
1765
|
+
render. A `tickFormat` identity change alone (with everything else unchanged) does not force a redraw —
|
|
1766
|
+
in practice `tickFormat` is stable across a component's lifetime, and any data change that would make a
|
|
1767
|
+
different `tickFormat` output visible already changes `datasets`/`labels` too.
|
|
1768
|
+
|
|
1769
|
+
**Slots:** none.
|
|
1770
|
+
|
|
1771
|
+
**CSS parts:** `base`, `grid-line`, `axis-label`, `axis-title`, `bar`, `line`, `point`, `legend`,
|
|
1772
|
+
`legend-item`, `legend-swatch`.
|
|
1773
|
+
|
|
1774
|
+
**Themeable custom properties:** `--lyra-chart-height` (same host-level property as `lyra-chart`);
|
|
1775
|
+
`--lyra-chart-grid-color`, `--lyra-chart-tick-color`, `--lyra-chart-legend-color` — same token
|
|
1776
|
+
*names* as `lyra-chart`, so a host already theming `lyra-chart` themes this for free. Unlike
|
|
1777
|
+
`lyra-chart` (canvas-rendered, needs `getComputedStyle`-based re-theming on every draw), this is
|
|
1778
|
+
plain SVG/DOM and reads these via native CSS `var()` — no JS-side resolution step, and no
|
|
1779
|
+
`refreshTheme()` method needed (there's nothing to go stale).
|
|
1780
|
+
|
|
1781
|
+
**Optional peer deps:** none. This is the point of the component.
|
|
1782
|
+
|
|
1783
|
+
```html
|
|
1784
|
+
<lyra-lite-chart type="bar" stacked legend x-label="Week" y-label="Commits"></lyra-lite-chart>
|
|
1785
|
+
<script>
|
|
1786
|
+
const c = document.querySelector('lyra-lite-chart');
|
|
1787
|
+
c.labels = ['W1', 'W2', 'W3', 'W4'];
|
|
1788
|
+
c.datasets = [
|
|
1789
|
+
{ label: 'Docs', data: [4, 6, 3, 8] },
|
|
1790
|
+
{ label: 'Bugs', data: [3, 2, 5, 4] },
|
|
1791
|
+
];
|
|
1792
|
+
</script>
|
|
1793
|
+
```
|
|
1794
|
+
|
|
1795
|
+
**Known gotchas:**
|
|
1796
|
+
- No `horizontal` mode (unlike `lyra-chart`) — deliberately cut from scope, not a stub: bars are
|
|
1797
|
+
always vertical.
|
|
1798
|
+
- No dual y-axis (`Series.axis: 'y2'`) — every series shares one y-axis/domain.
|
|
1799
|
+
- Series colors default to a fixed built-in 8-color categorical palette (round-robin by dataset
|
|
1800
|
+
index) when `color` is unset — not configurable beyond passing `color` per series.
|
|
1801
|
+
- Bar/point elements are real focusable DOM nodes (`tabindex="0" role="button"`, each with its own
|
|
1802
|
+
`aria-label`), so the `<svg>` itself uses `role="group"`, not `role="img"` — an "img" role would
|
|
1803
|
+
conflict with genuinely interactive descendants (axe's `nested-interactive` rule).
|
|
1804
|
+
- Tick values use a standard "nice numbers" (1/2/5 × 10ⁿ) rounding step, not exact data min/max —
|
|
1805
|
+
intentional (readable axis labels), matches how most charting libraries pick tick steps.
|
|
1155
1806
|
|
|
1156
1807
|
---
|
|
1157
1808
|
|
|
1158
1809
|
## 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`
|
|
1159
1810
|
|
|
1160
|
-
Each is `LyraChart` with `type`
|
|
1161
|
-
|
|
1811
|
+
Each is `LyraChart` with `type` locked to a fixed value via a real `get`/`set` accessor installed on
|
|
1812
|
+
its own prototype (`declare type: '…'` narrows the TS type at compile time; the runtime lock is the
|
|
1813
|
+
`Object.defineProperty` pair alongside it — the same pattern `lyra-histogram`'s own locked `type`
|
|
1814
|
+
below uses), not merely a class-field default a later assignment could still override. All other
|
|
1815
|
+
properties/events/parts/tokens are identical to `lyra-chart` above. Same optional peer deps
|
|
1816
|
+
(`chart.js`, `chartjs-plugin-zoom`).
|
|
1162
1817
|
|
|
1163
1818
|
```html
|
|
1164
1819
|
<lyra-bar-chart legend></lyra-bar-chart>
|
|
@@ -1173,12 +1828,12 @@ are identical to `lyra-chart` above. Same optional peer deps (`chart.js`, `chart
|
|
|
1173
1828
|
```
|
|
1174
1829
|
|
|
1175
1830
|
**Known gotchas (in addition to the core `lyra-chart` list above):**
|
|
1831
|
+
- `type` truly is locked per subclass: `<lyra-pie-chart type="bar">` or `el.type = 'bar'` at runtime
|
|
1832
|
+
is a genuine no-op (the accessor's setter silently ignores the write), not a footgun like a plain
|
|
1833
|
+
overridden class-field default would be.
|
|
1176
1834
|
- `lyra-bubble-chart` needs `Series.points` entries with an `x`/`y`/`r` triple, but `Series.points`
|
|
1177
|
-
is typed as `{x, y, label?}[]` with no `r` field — cast through `unknown
|
|
1178
|
-
`BubblePoint` type) when constructing bubble data.
|
|
1179
|
-
- Reading `chart.type` back in TypeScript for the 5 non-`line`/`bar`/`scatter` subclasses yields a
|
|
1180
|
-
value that, by `LyraChartType`'s own declaration, is never a legal member of that type (each is
|
|
1181
|
-
set via `as unknown as LyraChartType`).
|
|
1835
|
+
is typed as `{x, y, label?}[]` with no `r` field — cast the array through `as unknown as
|
|
1836
|
+
Series['points']` (or a local `BubblePoint` type) when constructing bubble data.
|
|
1182
1837
|
|
|
1183
1838
|
---
|
|
1184
1839
|
|
|
@@ -1191,8 +1846,11 @@ Bins `values` into `bins` equal-width buckets and renders as a bar chart (extend
|
|
|
1191
1846
|
- `bins: number = 10`
|
|
1192
1847
|
- `values: number[] = []` (attribute: false)
|
|
1193
1848
|
- `label: string = 'Frequency'`
|
|
1194
|
-
- `labels`/`datasets` are **derived, read-only** (installed as getter/setter pairs on the
|
|
1195
|
-
prototype; direct writes are silently ignored) — computed from
|
|
1849
|
+
- `labels`/`datasets`/`type` are **derived, read-only** (installed as getter/setter pairs on the
|
|
1850
|
+
prototype; direct writes are silently ignored) — `labels`/`datasets` are computed from
|
|
1851
|
+
`values`/`bins`, and `type` always reads back `'bar'` regardless of any assignment. Unlike the
|
|
1852
|
+
typed `lyra-*-chart` subclasses above (whose "locked" `type` is enforced only by convention),
|
|
1853
|
+
`lyra-histogram`'s lock is a real getter/setter — `el.type = 'line'` is a genuine no-op here.
|
|
1196
1854
|
- All other `LyraChart` properties (`legend`, `area`, `zoom`, `config`, `height`, etc.) are
|
|
1197
1855
|
inherited and usable.
|
|
1198
1856
|
|
|
@@ -1206,10 +1864,8 @@ Bins `values` into `bins` equal-width buckets and renders as a bar chart (extend
|
|
|
1206
1864
|
```
|
|
1207
1865
|
|
|
1208
1866
|
**Known gotchas:**
|
|
1209
|
-
-
|
|
1210
|
-
`
|
|
1211
|
-
`RangeError` even earlier (`Array.from({length: -1})`). No guard exists anywhere in the chain —
|
|
1212
|
-
**always pass `bins >= 1`.**
|
|
1867
|
+
- `bins <= 0` no longer crashes: `binValues()` now returns an empty bucket array for `binCount <= 0`
|
|
1868
|
+
(or empty `values`), so the histogram just renders with no bars instead of throwing.
|
|
1213
1869
|
|
|
1214
1870
|
---
|
|
1215
1871
|
|
|
@@ -1273,7 +1929,16 @@ raw `map` escape hatch for anything unexposed.
|
|
|
1273
1929
|
(discrete swatch rows only, no continuous gradient bar)
|
|
1274
1930
|
- `choropleth?: ChoroplethLayer` (attribute: false) — `ChoroplethLayer { sourceId: string; geojson:
|
|
1275
1931
|
GeoJSON.FeatureCollection; field: string; stops: [number, string][] }` (interpolated
|
|
1276
|
-
fill-color expression from `field`'s value against `stops`
|
|
1932
|
+
fill-color expression from `field`'s value against `stops`; `stops` must contain at least one
|
|
1933
|
+
`[value, color]` pair — an empty array is ignored, leaving whatever fill layer already exists, if
|
|
1934
|
+
any, untouched, rather than being applied)
|
|
1935
|
+
- `markers: MapMarker[] = []` (attribute: false) — `MapMarker { id?: string; lngLat: [number,
|
|
1936
|
+
number]; color?: string; label?: string; html?: string }`; reconciled by `id` (falling back to a
|
|
1937
|
+
`lng,lat` key when `id` is omitted) so an unchanged marker isn't torn down and recreated on every
|
|
1938
|
+
`markers` reassignment — its `lngLat` **and** its popup content (`html`/`label`, in that
|
|
1939
|
+
precedence) are both updated in place, and the popup is removed if a later update sets neither.
|
|
1940
|
+
`html` is rendered via `Popup.setHTML()` — **raw markup, inline event handlers included** — only
|
|
1941
|
+
pass trusted content, sanitize anything derived from user input first.
|
|
1277
1942
|
|
|
1278
1943
|
**Getters:** `map` → the raw `maplibregl.Map` instance.
|
|
1279
1944
|
|
|
@@ -1303,26 +1968,32 @@ exists and was hit)
|
|
|
1303
1968
|
stops: [[0, '#cde2fb'], [100, '#0969da']],
|
|
1304
1969
|
};
|
|
1305
1970
|
m.legend = [{ color: '#cde2fb', label: 'Low' }, { color: '#0969da', label: 'High' }];
|
|
1971
|
+
m.markers = [{ lngLat: [2.29, 48.86], label: 'Eiffel Tower' }];
|
|
1306
1972
|
m.addEventListener('lyra-map-click', (e) => console.log(e.detail.feature?.properties));
|
|
1307
1973
|
</script>
|
|
1308
1974
|
```
|
|
1309
1975
|
|
|
1310
1976
|
**Known gotchas:**
|
|
1311
|
-
- clearing or swapping the choropleth
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
- `mapStyle`
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
- No built-in popup rendering (despite sometimes being described as covering "legend/popup/
|
|
1322
|
-
choropleth") — only the raw `lyra-map-click` event; hand-roll your own `maplibregl.Popup` on
|
|
1323
|
-
click.
|
|
1977
|
+
- clearing or swapping the choropleth no longer leaks the old layer: setting `choropleth =
|
|
1978
|
+
undefined`, or changing `choropleth.sourceId` to a different value, now calls `removeLayer`/
|
|
1979
|
+
`removeSource` on whatever was previously applied before adding the new one (or nothing, if
|
|
1980
|
+
cleared).
|
|
1981
|
+
- `mapStyle` changes after construction now call `setStyle()` (in addition to `center`/`zoom`
|
|
1982
|
+
already calling `setCenter`/`setZoom`) — the choropleth is automatically re-applied once the new
|
|
1983
|
+
style's own `'style.load'` fires, since a style change wipes every layer/source maplibre-gl knows
|
|
1984
|
+
about.
|
|
1985
|
+
- Point markers now have a declarative API (`markers`, above) with popup support — the `.map` escape
|
|
1986
|
+
hatch and manual `new maplibregl.Marker()` are no longer the only way to place pins.
|
|
1324
1987
|
- No click-select visual highlight on choropleth features (no `setFeatureState` call) — clicking
|
|
1325
|
-
only fires the event, no built-in visual feedback.
|
|
1988
|
+
only fires the event, no built-in visual feedback. Popups are still only reachable declaratively
|
|
1989
|
+
through `markers`' `html`/`label` — a choropleth-feature click still has no built-in popup, only the
|
|
1990
|
+
raw `lyra-map-click` event.
|
|
1991
|
+
- `LegendEntry.color` is validated against a strict CSS-color-syntax allowlist before being applied
|
|
1992
|
+
to the legend swatch's `background`, rejecting anything that isn't recognizable color syntax
|
|
1993
|
+
(notably `url(...)`, which `background` also accepts and would otherwise fetch as soon as the
|
|
1994
|
+
swatch renders).
|
|
1995
|
+
- while the `maplibre-gl` peer is resolving, the host shows a `<lyra-skeleton variant="rect">` with
|
|
1996
|
+
`aria-busy="true"` in place of the map container.
|
|
1326
1997
|
|
|
1327
1998
|
---
|
|
1328
1999
|
|
|
@@ -1334,21 +2005,29 @@ parsing (that's left entirely to the host).
|
|
|
1334
2005
|
**Properties:**
|
|
1335
2006
|
- `multiple: boolean = false` (reflected)
|
|
1336
2007
|
- `disabled: boolean = false` (reflected)
|
|
1337
|
-
- `accept: string = ''` —
|
|
2008
|
+
- `accept: string = ''` — a native-`accept`-style string (`.csv,.xlsx`, `text/csv`, `image/*`, or any
|
|
2009
|
+
comma-separated mix); now enforced on **both** the native picker dialog and the drag-drop path, see
|
|
2010
|
+
gotchas
|
|
1338
2011
|
- `allowedMimeTypes: string[] = []` (attribute: false) — exact MIME-string allowlist
|
|
1339
2012
|
- `forbiddenMimeTypes: string[] = []` (attribute: false) — exact MIME-string denylist, checked
|
|
1340
2013
|
**before** (and takes precedence over) `allowedMimeTypes`
|
|
2014
|
+
- `maxFileSize: number = 0` (attribute `max-file-size` — bytes; `0` disables the check)
|
|
1341
2015
|
- `label: string = 'Drop files here or click to browse'`
|
|
1342
2016
|
|
|
1343
2017
|
**Methods:** `openPicker()` — programmatically opens the native file dialog.
|
|
1344
2018
|
|
|
1345
|
-
**Events:** `lyra-files` (`detail: { files: File[], rejected:
|
|
1346
|
-
manual file-picker selection — `
|
|
2019
|
+
**Events:** `lyra-files` (`detail: { files: File[], rejected: RejectedFile[] }`, fired on both drop
|
|
2020
|
+
and manual file-picker selection) — `RejectedFile = { file: File; reason: 'type' | 'count' | 'size'
|
|
2021
|
+
}`: `'type'` from `accept`/`allowedMimeTypes`/`forbiddenMimeTypes`, `'count'` when a single-file
|
|
2022
|
+
input (`multiple` unset) receives more than one file (in which case *all* files are rejected, none
|
|
2023
|
+
accepted), `'size'` from `maxFileSize`.
|
|
1347
2024
|
|
|
1348
2025
|
**Slots:** default slot — custom dropzone content, overrides the `label` attribute text when
|
|
1349
|
-
provided.
|
|
2026
|
+
provided. The accessible name always comes from `label` regardless, so icon-only slot content still
|
|
2027
|
+
announces correctly.
|
|
1350
2028
|
|
|
1351
|
-
**CSS parts:** `base`, `input`
|
|
2029
|
+
**CSS parts:** `base`, `input`, `status` (a visually-hidden `role="status" aria-live="polite"`
|
|
2030
|
+
element carrying the drag accept/reject announcement)
|
|
1352
2031
|
|
|
1353
2032
|
**Themeable custom properties:** shared tokens only — `--lyra-space-xs`, `--lyra-space-l`,
|
|
1354
2033
|
`--lyra-color-border`, `--lyra-radius`, `--lyra-color-surface`, `--lyra-color-text-quiet`,
|
|
@@ -1360,7 +2039,7 @@ provided.
|
|
|
1360
2039
|
<lyra-file-input multiple accept=".csv,.xlsx" allowed-mime-types='["text/csv"]'></lyra-file-input>
|
|
1361
2040
|
<script>
|
|
1362
2041
|
document.querySelector('lyra-file-input').addEventListener('lyra-files', (e) => {
|
|
1363
|
-
console.log('accepted:', e.detail.files, 'rejected:', e.detail.rejected);
|
|
2042
|
+
console.log('accepted:', e.detail.files, 'rejected:', e.detail.rejected); // rejected[i].reason
|
|
1364
2043
|
});
|
|
1365
2044
|
</script>
|
|
1366
2045
|
```
|
|
@@ -1369,25 +2048,24 @@ Note: `allowedMimeTypes`/`forbiddenMimeTypes` are complex properties (`attribute
|
|
|
1369
2048
|
them via JS (`el.allowedMimeTypes = [...]`), not as a JSON string attribute; the snippet above is
|
|
1370
2049
|
illustrative of intent only.
|
|
1371
2050
|
|
|
2051
|
+
`accept.ts` exports `matchesAccept(file, accept, assumeExtensionMatch?)` (internal — not
|
|
2052
|
+
re-exported from the package root) — parses the same three `accept` forms the browser's native
|
|
2053
|
+
picker accepts (extension, exact MIME, `type/*` wildcard) and reports whether a `File` matches. Used
|
|
2054
|
+
both for the drop path's real rejection and for the dragenter preview, where a `DataTransferItem` (no
|
|
2055
|
+
`.name`, only `.type`) can't evaluate an extension pattern yet — `assumeExtensionMatch: true` treats
|
|
2056
|
+
that as a possible match during preview so the drag-over UI doesn't flash a false "reject" state for
|
|
2057
|
+
an extension-only `accept` list.
|
|
2058
|
+
|
|
1372
2059
|
**Known gotchas:**
|
|
1373
|
-
- **`accept` only constrains the native file-picker dialog; it is silently ignored on
|
|
1374
|
-
the drag-drop path.** `isAllowed()` checks only `allowedMimeTypes`/`forbiddenMimeTypes` (exact
|
|
1375
|
-
MIME-string match — no extension or `image/*`-wildcard support like native `accept` has);
|
|
1376
|
-
`this.accept` itself is never read anywhere else in the component. Concretely:
|
|
1377
|
-
`<lyra-file-input accept=".csv,.xlsx">` with no `allowedMimeTypes` set will silently **accept** a
|
|
1378
|
-
dropped `.png` (fires `lyra-files` with the png in `files`, not `rejected`) even though the UI
|
|
1379
|
-
visually implies only CSV/XLSX are allowed. **To actually enforce type restriction on drop, you
|
|
1380
|
-
must also set `allowedMimeTypes` (and/or `forbiddenMimeTypes`) — `accept` alone is not
|
|
1381
|
-
sufficient.**
|
|
1382
|
-
- No max-file-size validation (no `maxFileSize` property) — a host wanting a byte-limit must
|
|
1383
|
-
intercept `lyra-files` and re-filter itself.
|
|
1384
2060
|
- No paste-from-clipboard support (no `paste`-event handling of `e.clipboardData.files`).
|
|
1385
|
-
- No `aria-live` announcement of the drag accept/reject state (`data-drag-state` attribute drives
|
|
1386
|
-
border/background color only, invisible to screen readers mid-drag) — the click/keyboard-to-
|
|
1387
|
-
browse path is unaffected and correctly operable (`role="button"`, `tabindex` 0/-1 by disabled,
|
|
1388
|
-
`aria-disabled`, Enter/Space handling).
|
|
1389
2061
|
- Dragged folders aren't specially detected (no `webkitGetAsEntry()` check) — likely surfaces as a
|
|
1390
2062
|
phantom zero-byte `File` with an empty MIME type rather than a clear rejection.
|
|
2063
|
+
- `maxFileSize`/`accept` extension patterns can't be evaluated during the dragenter preview (no real
|
|
2064
|
+
`File.size`/`.name` available yet from a `DataTransferItem`) — the live preview state (border/
|
|
2065
|
+
background color, `status` announcement) is therefore only a best-effort hint; the authoritative
|
|
2066
|
+
accept/reject decision (and `rejected[].reason`) is always the one made at actual drop time.
|
|
2067
|
+
- the click/keyboard-to-browse path is correctly operable (`role="button"`, `tabindex` 0/-1 by
|
|
2068
|
+
disabled, `aria-disabled`, Enter/Space handling).
|
|
1391
2069
|
|
|
1392
2070
|
---
|
|
1393
2071
|
|
|
@@ -1395,7 +2073,15 @@ illustrative of intent only.
|
|
|
1395
2073
|
|
|
1396
2074
|
These apply across the whole library rather than to any one component:
|
|
1397
2075
|
|
|
1398
|
-
- **Packaging
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
2076
|
+
- **Packaging.** `custom-elements.json` is now in `package.json`'s `files` allowlist and
|
|
2077
|
+
regenerated by a `prepack` script (`pnpm run manifest`) before publish, so a manifest-driven IDE/
|
|
2078
|
+
tooling integration (e.g. custom-elements-manifest-based autocomplete) does see an up-to-date copy
|
|
2079
|
+
in the published tarball.
|
|
2080
|
+
- **Lazy-loaded optional peers share one pattern.** `lyra-chart`/`lyra-histogram`/`lyra-box-plot`,
|
|
2081
|
+
`lyra-graph`, `lyra-map`, and `lyra-flag` each show a `<lyra-skeleton>` placeholder (with
|
|
2082
|
+
`aria-busy="true"` on the host) while their respective optional peer dependency resolves, then
|
|
2083
|
+
settle into their normal render — or, if the peer genuinely isn't installed, an empty/fallback
|
|
2084
|
+
render plus a one-time `console.warn` (deduped module-wide, not per-instance).
|
|
2085
|
+
- **RTL.** Components generally rely on CSS logical properties to flip layout for free; `lyra-split`
|
|
2086
|
+
and `lyra-time-range` additionally consult `internal/rtl.ts`'s `isRtl()` to mirror the physical
|
|
2087
|
+
drag-direction/arrow-key math that logical properties alone don't cover.
|