@aceshooting/lyra-ui 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -9
- package/custom-elements.json +12479 -6535
- package/dist/components/chart/chart.d.ts +46 -1
- package/dist/components/chart/chart.js +148 -10
- 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 +74 -0
- package/dist/components/chart/lite-chart.js +384 -0
- package/dist/components/chart/lite-chart.stories.d.ts +12 -0
- package/dist/components/chart/lite-chart.stories.js +98 -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 +75 -12
- 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 +48 -3
- package/dist/components/flag/flag.js +65 -29
- 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 +137 -12
- package/dist/components/heatmap/heatmap.js +427 -27
- package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
- package/dist/components/heatmap/heatmap.stories.js +130 -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 +122 -0
- package/dist/components/select/select.js +516 -0
- package/dist/components/select/select.stories.d.ts +19 -0
- package/dist/components/select/select.stories.js +75 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +188 -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 +27 -1
- package/dist/components/stat/stat.js +56 -3
- package/dist/components/stat/stat.stories.d.ts +3 -0
- package/dist/components/stat/stat.stories.js +49 -1
- package/dist/components/stat/stat.styles.js +44 -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 +15 -1
- package/dist/components/widget/widget.js +140 -5
- package/dist/components/widget/widget.stories.d.ts +1 -0
- package/dist/components/widget/widget.stories.js +10 -0
- package/dist/components/widget/widget.styles.js +11 -1
- 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 +54 -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 +892 -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,109 @@ 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.
|
|
164
214
|
- no `size` variant (small/medium/large) unlike sibling `lyra-toast-item`.
|
|
215
|
+
- `aria-required` reflects `required` immediately; `aria-invalid`, by contrast, only reflects
|
|
216
|
+
**after the field has been touched** (first `blur`) — a `required` field with a validity error
|
|
217
|
+
doesn't look invalid to assistive tech before that, by design (avoids flashing invalid styling on
|
|
218
|
+
first render). Blurring the input (Tab away) now also closes an open listbox, the same as a
|
|
219
|
+
native `<select>`'s popup, not just a click outside or Escape.
|
|
220
|
+
- `dotColor`/`sub`/`group` are read from light-DOM `<lyra-option>` children as before, but are also
|
|
221
|
+
first-class fields on `ComboboxSourceRow` for the async `source` path — an async lookup can drive
|
|
222
|
+
the same grouped/dot/sub-text rendering a static option list can.
|
|
165
223
|
- 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.
|
|
224
|
+
kept on the input) is implemented correctly — a genuine strength, safe to build on. Declaratively-
|
|
225
|
+
selected options (`<lyra-option value="b" selected>`) are seeded into the selection (mirroring
|
|
226
|
+
native `<select><option selected>`) both the first time options are collected **and** for any
|
|
227
|
+
later batch slotted in afterward (e.g. a lazily-populated list appended post-connect) — only the
|
|
228
|
+
`form.reset()` default itself is captured exclusively from that very first pass; a later pick, or
|
|
229
|
+
a later batch of newly-`selected` options, never redefines what a reset restores to.
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## `lyra-select`
|
|
234
|
+
|
|
235
|
+
A plain closed-list dropdown — a direct `<lyra-*>` counterpart to `<wa-select>`/`<wa-option>`.
|
|
236
|
+
**Form-associated** (hand-rolled internals, not the shared `FormAssociated` mixin — same reasoning
|
|
237
|
+
as `lyra-combobox`, see the shared-foundation notes above). The trigger is a `<button>`, not a text
|
|
238
|
+
input: click/Enter/Space/ArrowDown opens it, and there's no typing-to-filter. Options are
|
|
239
|
+
`<lyra-option value>` children — the same element `<lyra-combobox>` uses — reconciled the same way
|
|
240
|
+
combobox does, and the popup reuses `internal/positioner.ts` for placement.
|
|
241
|
+
|
|
242
|
+
Single-select only, with no `filter`/`source`/`with-clear`/`max-options-visible`/`empty-text`/
|
|
243
|
+
`max-render`/`multiple` surface — reach for `<lyra-combobox>` instead whenever any of those apply.
|
|
244
|
+
|
|
245
|
+
**Properties:**
|
|
246
|
+
- `placeholder: string = ''`
|
|
247
|
+
- `disabled: boolean = false` (reflected)
|
|
248
|
+
- `required: boolean = false` (reflected — enforced via `internals.setValidity()`)
|
|
249
|
+
- `name: string = ''`
|
|
250
|
+
- `label: string = ''`
|
|
251
|
+
- `hint: string = ''`
|
|
252
|
+
- `errorText: string = ''` (attribute `error-text` — static error copy shown below the hint;
|
|
253
|
+
overridden by slotted `error` content when provided)
|
|
254
|
+
- `open: boolean = false` (reflected)
|
|
255
|
+
- `value: string` — a getter/setter; always a single string (no `multiple` mode)
|
|
256
|
+
|
|
257
|
+
**Events:** `change` (native-style — selection changed), `input` (fired alongside `change` on every
|
|
258
|
+
selection change — a native `<select>` doesn't meaningfully distinguish the two either),
|
|
259
|
+
`lyra-show`, `lyra-hide`
|
|
260
|
+
|
|
261
|
+
**Slots:** default (`<lyra-option>` children), `label`, `hint`, `error` (overrides the `errorText`
|
|
262
|
+
attribute when provided)
|
|
263
|
+
|
|
264
|
+
**CSS parts:** `form-control`, `form-control-label`, `trigger`, `listbox`, `option`, `option-dot`
|
|
265
|
+
(the leading status dot, when a row's `dotColor` is set), `option-label`, `option-sub` (a row's
|
|
266
|
+
secondary line, when `sub` is set), `expand-icon`, `error`, `hint`
|
|
267
|
+
|
|
268
|
+
**Themeable custom properties:** shared tokens only.
|
|
269
|
+
|
|
270
|
+
**Optional peer deps:** none.
|
|
271
|
+
|
|
272
|
+
```html
|
|
273
|
+
<lyra-select id="sel" label="Fruit" placeholder="Pick one…">
|
|
274
|
+
<lyra-option value="a">Apple</lyra-option>
|
|
275
|
+
<lyra-option value="b" selected>Banana</lyra-option>
|
|
276
|
+
</lyra-select>
|
|
277
|
+
<script type="module">
|
|
278
|
+
document.getElementById('sel').addEventListener('change', (e) => console.log(e.target.value));
|
|
279
|
+
</script>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
**Known gotchas:**
|
|
283
|
+
- The trigger keeps real DOM focus throughout — the listbox's "active" row is conveyed via
|
|
284
|
+
`aria-activedescendant`, never actual focus, matching the WAI-ARIA "select-only combobox" pattern
|
|
285
|
+
(as opposed to `lyra-combobox`'s editable-input pattern).
|
|
286
|
+
- No typing-to-filter, but a printable keypress still jumps to (while open) or directly selects
|
|
287
|
+
(while closed) the next non-disabled option whose label starts with what's been typed, matching a
|
|
288
|
+
native `<select>`'s own type-ahead; the buffer resets ~500ms after the last keystroke.
|
|
289
|
+
- Declaratively-selected options (`<lyra-option value="b" selected>`) seed the initial selection
|
|
290
|
+
(mirroring native `<select><option selected>`) both the first time options are collected and for
|
|
291
|
+
any later-slotted batch — only that very first pass' declared selection becomes the
|
|
292
|
+
`form.reset()` default, the same rule `lyra-combobox` follows.
|
|
293
|
+
- `aria-required` on the trigger reflects `required` immediately; `aria-invalid` only reflects once
|
|
294
|
+
the trigger has been blurred (touched) at least once, mirroring `lyra-combobox`'s own input.
|
|
295
|
+
Blurring the trigger (Tab away) closes an open listbox, the same as a native `<select>`'s popup.
|
|
167
296
|
|
|
168
297
|
---
|
|
169
298
|
|
|
@@ -195,7 +324,8 @@ Inline month-grid calendar, not form-associated (used standalone or embedded ins
|
|
|
195
324
|
**Getters:** `selection: { from: Date|null; to: Date|null }`, `valueAsDate: Date | null` (single
|
|
196
325
|
mode only)
|
|
197
326
|
|
|
198
|
-
**Methods:** `clear()`, `goToToday()`, `goToDate(date: string | Date)`
|
|
327
|
+
**Methods:** `clear()`, `goToToday()`, `goToDate(date: string | Date)` (clamped to `min`/`max` before
|
|
328
|
+
navigating/focusing)
|
|
199
329
|
|
|
200
330
|
**Events:** `input` (during interaction — for range mode, fires after the first click of a pair),
|
|
201
331
|
`change` (committed value)
|
|
@@ -230,15 +360,22 @@ Text field + calendar popover, **form-associated** via the shared `FormAssociate
|
|
|
230
360
|
- `months: 1|2 = 1`
|
|
231
361
|
- `firstDayOfWeek: string = 'auto'`
|
|
232
362
|
- `weekdayFormat: 'narrow'|'short'|'long' = 'short'`
|
|
363
|
+
- `disablePast: boolean = false` (attribute `disable-past`)
|
|
364
|
+
- `disableFuture: boolean = false` (attribute `disable-future`)
|
|
365
|
+
- `withOutsideDays: boolean = false` (attribute `with-outside-days`)
|
|
366
|
+
|
|
367
|
+
**Properties (own, continued):**
|
|
368
|
+
- `errorText: string = ''` (attribute `error-text` — static error copy; overridden by slotted
|
|
369
|
+
`error` content when provided)
|
|
233
370
|
|
|
234
371
|
**Methods:** `show()`, `hide()`, `clear()`
|
|
235
372
|
|
|
236
373
|
**Events:** `input`, `change`, `lyra-show`, `lyra-hide`, `lyra-clear`
|
|
237
374
|
|
|
238
|
-
**Slots:** `label`, `hint`
|
|
375
|
+
**Slots:** `label`, `error` (overrides `errorText`), `hint`
|
|
239
376
|
|
|
240
377
|
**CSS parts:** `form-control`, `form-control-label`, `input-wrapper`, `input`, `clear-button`,
|
|
241
|
-
`expand-button`, `expand-icon`, `popup`, `date-picker`, `hint`
|
|
378
|
+
`expand-button`, `expand-icon`, `popup`, `date-picker`, `error`, `hint`
|
|
242
379
|
|
|
243
380
|
**Themeable custom properties:** shared tokens only.
|
|
244
381
|
|
|
@@ -257,30 +394,30 @@ Text field + calendar popover, **form-associated** via the shared `FormAssociate
|
|
|
257
394
|
from the package root**, but worth knowing when reasoning about behavior): `parseISO`, `formatISO`,
|
|
258
395
|
`isSameDay`, `addMonths`, `clampDate`, `monthMatrix(year, month, firstDayOfWeek)`,
|
|
259
396
|
`weekdayLabels(firstDayOfWeek, format, locale)`, `monthTitle(year, month, locale)`,
|
|
260
|
-
`resolveFirstDayOfWeek(value)`.
|
|
397
|
+
`resolveFirstDayOfWeek(value, locale?)`.
|
|
261
398
|
|
|
262
399
|
**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.
|
|
400
|
+
- `first-day-of-week="auto"` now derives from `locale` when the runtime's `Intl.Locale` exposes
|
|
401
|
+
week-info (`weekInfo`/`getWeekInfo()`, still shifting between engines) — `resolveFirstDayOfWeek()`
|
|
402
|
+
only hardcodes Sunday as the fallback when that isn't available or `locale` is unset. A
|
|
403
|
+
French/German-locale user with a supporting runtime now gets the Monday-first grid their OS would
|
|
404
|
+
show; on an older runtime it still falls back to Sunday-first silently. **If you need a guaranteed
|
|
405
|
+
locale-correct week start regardless of runtime**, pass an explicit
|
|
406
|
+
`first-day-of-week="mon"` (accepted values: `sun`/`mon`/`tue`/`wed`/`thu`/`fri`/`sat`).
|
|
273
407
|
- (date-input only) — opening the calendar doesn't move focus into it (`show()` only flips
|
|
274
408
|
`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
|
-
`
|
|
409
|
+
on the text input. Escape *does* close it from anywhere inside the form control (including the
|
|
410
|
+
nested picker's own day/nav buttons), returning focus to the expand button.
|
|
411
|
+
- (date-input only) — typing an unparseable string and blurring/committing sets `badInput` via
|
|
412
|
+
`internals.setValidity()` and reverts the displayed text to the last valid commit, but does not
|
|
413
|
+
reflect `aria-invalid` onto the visible `<input part="input">` itself (unlike `lyra-combobox`,
|
|
414
|
+
which does reflect `aria-invalid`/`aria-required` onto its own input) — rely on `data-invalid`
|
|
415
|
+
(a host attribute, toggled the same way) or `reportValidity()` rather than assuming the input's own
|
|
416
|
+
ARIA state.
|
|
282
417
|
- The grid keyboard pattern (Arrow/PageUp/PageDown/Home/End navigation with correct focus
|
|
283
|
-
sequencing) is implemented correctly and safe to rely on
|
|
418
|
+
sequencing) is implemented correctly and safe to rely on, as is the selected/range-day text color
|
|
419
|
+
(`--lyra-color-on-brand`, not a hardcoded literal — safe to override `--lyra-color-brand` without
|
|
420
|
+
losing contrast on selected-day text).
|
|
284
421
|
|
|
285
422
|
---
|
|
286
423
|
|
|
@@ -317,7 +454,8 @@ A single notification.
|
|
|
317
454
|
|
|
318
455
|
**Properties:**
|
|
319
456
|
- `duration: number = 5000` (ms; `Infinity` or `<= 0` disables auto-dismiss)
|
|
320
|
-
- `size: 'xs'|'s'|'m'|'l'|'xl' = 'm'` (reflected
|
|
457
|
+
- `size: 'xs'|'s'|'m'|'l'|'xl' = 'm'` (reflected — drives both `--padding` and the toast's own
|
|
458
|
+
font-size via `:host([size=...])`, from a compact `xs` up to a roomier `xl`)
|
|
321
459
|
- `variant: 'brand'|'success'|'warning'|'danger'|'neutral' = 'neutral'` (reflected)
|
|
322
460
|
- `withIcon: boolean = false` (attribute `with-icon`)
|
|
323
461
|
|
|
@@ -330,15 +468,24 @@ DOM.
|
|
|
330
468
|
|
|
331
469
|
**CSS parts:** `toast-item`, `accent`, `icon`, `content`, `close-button`
|
|
332
470
|
|
|
333
|
-
**Themeable custom properties:** `--accent-width` (4px), `--show-duration`/`--hide-duration`
|
|
334
|
-
|
|
471
|
+
**Themeable custom properties:** `--accent-width` (4px), `--show-duration`/`--hide-duration`
|
|
472
|
+
(`var(--lyra-transition-base, 180ms ease-out)` — 180ms by default, matching the JS-side show/hide
|
|
473
|
+
animation timer, which is not itself reading this token), `--padding` (`var(--lyra-space-m)`),
|
|
474
|
+
`--font-size` (`1rem`) — both `--padding` and `--font-size` are auto-swapped per `size`, from a
|
|
475
|
+
compact `xs` up to a roomier `xl` — `--accent-color` (defaults to `--lyra-color-border`,
|
|
335
476
|
auto-swapped per `variant` to `--lyra-color-brand/-success/-warning/-danger`).
|
|
336
477
|
|
|
337
478
|
**Optional peer deps:** none.
|
|
338
479
|
|
|
339
480
|
`role` is chosen automatically per `variant`: `"alert"` for `danger`/`warning`, `"status"`
|
|
340
|
-
otherwise
|
|
341
|
-
`
|
|
481
|
+
otherwise — re-evaluated on every `variant` change, not just at first render, so reassigning
|
|
482
|
+
`variant` to `danger`/`warning` after creation is announced as an interruption instead of keeping
|
|
483
|
+
its original, now-stale role. Auto-dismiss timer **pauses** on `pointerenter`/`focusin`, **resumes**
|
|
484
|
+
on `pointerleave`/`focusout`, with real elapsed-time bookkeeping (WCAG 2.2.1 timing-adjustable) —
|
|
485
|
+
hover and focus are tracked as independent pause reasons, so releasing only one (e.g. the pointer
|
|
486
|
+
leaves while focus remains, or vice versa) keeps the timer paused until *neither* holds it anymore.
|
|
487
|
+
A `duration` change while the timer is actively counting down reschedules it immediately against
|
|
488
|
+
the new value instead of waiting for the next pause/resume cycle.
|
|
342
489
|
|
|
343
490
|
### `toast()`
|
|
344
491
|
|
|
@@ -355,7 +502,9 @@ toast({ message: 'Deleted', variant: 'danger', action: { label: 'Undo', onClick:
|
|
|
355
502
|
`toast(input: ToastOptions | string): ToastHandle` where
|
|
356
503
|
`ToastOptions = ToastCreateOptions & { message: string; placement?: ToastPlacement; action?: { label: string; onClick: (item: LyraToastItem) => void } }`,
|
|
357
504
|
and `ToastHandle = { item: Promise<LyraToastItem>; dismiss: () => void }`. Lazily mounts (and
|
|
358
|
-
re-mounts if removed)
|
|
505
|
+
re-mounts if removed) **one singleton `<lyra-toast>` region per distinct `placement`** on
|
|
506
|
+
`document.body` — a `toast()` call targeting one placement never relocates toasts already showing
|
|
507
|
+
at another, since `placement` is a per-call option rather than a single global region's setting.
|
|
359
508
|
|
|
360
509
|
```html
|
|
361
510
|
<script type="module">
|
|
@@ -369,9 +518,15 @@ re-mounts if removed) a single `<lyra-toast>` region on `document.body`.
|
|
|
369
518
|
`role="status" aria-live="polite"` **and** each `lyra-toast-item` independently self-assigns its
|
|
370
519
|
own `role` (`status`/`alert`) — nesting live regions can cause some screen readers to announce a
|
|
371
520
|
new toast twice. Plausible from the code, unverified against real AT.
|
|
372
|
-
-
|
|
373
|
-
|
|
374
|
-
|
|
521
|
+
- the close button's accessible name is derived from the toast's own message text (`"Close: <first
|
|
522
|
+
40 chars>…"`, falling back to bare `"Close"` only when the toast has no text content) rather than
|
|
523
|
+
a bare `"Close"` on every instance — useful when several toasts are stacked and a screen-reader or
|
|
524
|
+
switch-access user needs to tell their close buttons apart without activating one first.
|
|
525
|
+
- pause/resume-on-hover/focus (the component's main accessibility differentiator), including the
|
|
526
|
+
independent-hover-vs-focus pause reasons above, now has regression test coverage.
|
|
527
|
+
- `hide()` is idempotent (a second call while already hiding is a no-op) and `[part="close-button"]`
|
|
528
|
+
gets `aria-disabled="true"` once hiding starts, so a stray extra click/Enter during the hide
|
|
529
|
+
animation can't re-enter it.
|
|
375
530
|
- Prefer the `toast()` helper over manually creating `<lyra-toast>`/`<lyra-toast-item>` — it already
|
|
376
531
|
handles the singleton-region and remount-if-removed logic.
|
|
377
532
|
|
|
@@ -409,8 +564,13 @@ the reviewed family (pure CSS cascade, no JS/`getComputedStyle` bridging needed)
|
|
|
409
564
|
- Auto-sets `role="img"` + a descriptive `aria-label` ("Trend of N values, last X" / "No data" when
|
|
410
565
|
empty) — good default, but still just a single summary sentence with no tabular fallback; fine for
|
|
411
566
|
a glance-value sparkline, worth reconsidering if it's ever the *sole* representation of a metric.
|
|
412
|
-
-
|
|
413
|
-
|
|
567
|
+
- flat data (every value equal, so the auto-computed range spans zero) now renders a centered
|
|
568
|
+
midline/mid-height bars instead of collapsing every point to the bottom edge, and a single-value
|
|
569
|
+
series renders a visible flat line (a zero-length path was previously invisible). A `type="bar"`
|
|
570
|
+
series past 500 points is decimated to at most 500 rendered `<rect>`s (evenly sampled, not
|
|
571
|
+
aggregated/averaged) so an unbounded `values` array can't turn directly into that many shadow-DOM
|
|
572
|
+
nodes; `line`/`area` types have no such cap (they already collapse to one `<path>` regardless of
|
|
573
|
+
point count).
|
|
414
574
|
|
|
415
575
|
---
|
|
416
576
|
|
|
@@ -423,8 +583,13 @@ Country/language flag image. Flag artwork ships in a **separate, optional peer p
|
|
|
423
583
|
- `country?: string` (ISO 3166-1 alpha-2, e.g. `"fr"` — takes precedence over `language`)
|
|
424
584
|
- `language?: string` (BCP-47-ish tag, e.g. `"en"`/`"en-US"`, resolved to a representative country
|
|
425
585
|
via `languageToCountry()`)
|
|
426
|
-
- `
|
|
427
|
-
|
|
586
|
+
- `src?: string` (a pre-resolved flag image URL — takes precedence over `country`/`language` and
|
|
587
|
+
skips the peer-package lookup/loading-skeleton round trip entirely; mainly useful to avoid even
|
|
588
|
+
the small per-flag async hop when you already have the URL at build time, e.g. from
|
|
589
|
+
`import frUrl from '@aceshooting/lyra-flags/flags/fr.svg?url'`. `label` is effectively required
|
|
590
|
+
alongside `src` since there's no `country`/`language` to derive a fallback `alt` from.)
|
|
591
|
+
- `label?: string` (accessible name / `alt` text — **defaults to the bare uppercase *resolved
|
|
592
|
+
country* code if omitted**, see gotchas)
|
|
428
593
|
- `round: boolean = false` (reflected — circular crop)
|
|
429
594
|
|
|
430
595
|
**Events:** none.
|
|
@@ -433,7 +598,8 @@ Country/language flag image. Flag artwork ships in a **separate, optional peer p
|
|
|
433
598
|
|
|
434
599
|
**CSS parts:** `image`
|
|
435
600
|
|
|
436
|
-
**Themeable custom properties:** `--lyra-flag-radius` (default `2px`, non-`round` corner radius)
|
|
601
|
+
**Themeable custom properties:** `--lyra-flag-radius` (default `2px`, non-`round` corner radius);
|
|
602
|
+
also consumes `--lyra-color-border` (the inset ring around the flag image).
|
|
437
603
|
|
|
438
604
|
**Optional peer deps:** `@aceshooting/lyra-flags` — required for the component to actually render an
|
|
439
605
|
image; without it, renders an empty template (see gotchas).
|
|
@@ -454,15 +620,33 @@ pnpm add @aceshooting/lyra-flags # required peer — without it, <lyra-flag> r
|
|
|
454
620
|
|
|
455
621
|
**Known gotchas:**
|
|
456
622
|
- Requires the optional peer `@aceshooting/lyra-flags` to actually render an image; without it the
|
|
457
|
-
component
|
|
458
|
-
|
|
623
|
+
component still shows a `<lyra-skeleton variant="rect">` placeholder (with `aria-busy="true"` on
|
|
624
|
+
the host) while resolving, then settles into an **empty template** plus a one-time `console.warn`
|
|
625
|
+
once the resolver rejects (lazy `import()`, cached module-wide so the warning fires only once per
|
|
626
|
+
page even with many `<lyra-flag>` instances).
|
|
459
627
|
- Rendering is async even when the peer *is* installed: `src` resolves after an `import()` +
|
|
460
|
-
resolver call, so there's a brief
|
|
628
|
+
resolver call, so there's a brief loading-skeleton window on first paint/attribute change — don't
|
|
461
629
|
assume the `<img>` exists synchronously right after setting `country`/`language`.
|
|
462
|
-
- if `label` is omitted, the accessible name (`alt`) falls back to the **bare uppercase
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
630
|
+
- if `label` is omitted, the accessible name (`alt`) falls back to the **bare uppercase resolved
|
|
631
|
+
country code** — for `language="en"` that's `"GB"` (the mapped country), not `"EN"` (the language
|
|
632
|
+
tag itself). A screen reader will typically spell this out letter-by-letter rather than announce a
|
|
633
|
+
country name. Always pass an explicit `label` (e.g. `"France"`) if the flag conveys real
|
|
634
|
+
information rather than being purely decorative.
|
|
635
|
+
- an invalid `country` (anything not matching the ISO 3166-1 alpha-2 shape, e.g. containing `../`)
|
|
636
|
+
is rejected before it ever reaches the peer's `flagUrl()` resolver — treated the same as an
|
|
637
|
+
unknown/missing flag rather than being passed through. `languageToCountry()`'s region-subtag path
|
|
638
|
+
is validated against that same alpha-2 shape too (not just a bare length-2 check), so a malformed
|
|
639
|
+
region such as `"en-01"` correctly falls through to the `LANGUAGE_TO_COUNTRY[base]` lookup instead
|
|
640
|
+
of being accepted verbatim as a country code.
|
|
641
|
+
- rapidly reassigning `country`/`language`/`src` no longer risks a stale resolution overwriting a
|
|
642
|
+
newer one: each resolver call is tagged with a token captured at the time it was kicked off, and a
|
|
643
|
+
`.then()` that fires after a later change (or after the field was cleared) is discarded instead of
|
|
644
|
+
clobbering the rendered image/`loading`.
|
|
645
|
+
- `country`/`language` resolve through `@aceshooting/lyra-flags`'s `flagUrl(code)`, which is itself
|
|
646
|
+
genuinely code-split per flag: using `<lyra-flag country="fr">` anywhere in an app fetches only
|
|
647
|
+
the flags actually requested at runtime (verified with a real Vite build — referencing 2 codes
|
|
648
|
+
shipped ~28 KB total, not all 249). If you already have a flag's URL at build time, `src` skips
|
|
649
|
+
the peer-package round trip (and its loading-skeleton flash) entirely.
|
|
466
650
|
|
|
467
651
|
---
|
|
468
652
|
|
|
@@ -475,6 +659,9 @@ First-party "no data" state (no Web Awesome equivalent).
|
|
|
475
659
|
**Properties:**
|
|
476
660
|
- `heading: string = ''`
|
|
477
661
|
- `description: string = ''`
|
|
662
|
+
- `compact: boolean = false` (reflected) — tighter, left-aligned rendering (less padding, a lighter
|
|
663
|
+
heading weight) for use inside a constrained space like a widget body or table cell, instead of
|
|
664
|
+
the centered/spacious full-page default
|
|
478
665
|
|
|
479
666
|
**Events:** none.
|
|
480
667
|
|
|
@@ -482,7 +669,7 @@ First-party "no data" state (no Web Awesome equivalent).
|
|
|
482
669
|
|
|
483
670
|
**CSS parts:** `base`, `icon`, `heading`, `description`, `actions`
|
|
484
671
|
|
|
485
|
-
**Themeable custom properties:** shared tokens only (`--lyra-space-s/-l`,
|
|
672
|
+
**Themeable custom properties:** shared tokens only (`--lyra-space-xs/-s/-l`,
|
|
486
673
|
`--lyra-color-text-quiet/-border/-text`).
|
|
487
674
|
|
|
488
675
|
**Optional peer deps:** none.
|
|
@@ -495,10 +682,8 @@ First-party "no data" state (no Web Awesome equivalent).
|
|
|
495
682
|
```
|
|
496
683
|
|
|
497
684
|
**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.
|
|
685
|
+
- `[part="base"]` is `role="status" aria-live="polite"`, so a list/table transitioning to empty
|
|
686
|
+
does announce to screen readers — no extra wiring needed on the host's part.
|
|
502
687
|
- Note: correctly works around the classic `:empty`-pseudo-class trap (a wrapper with a `<slot>`
|
|
503
688
|
inside can never match `:empty`) by tracking real slot assignment in JS (`hasIcon`/`hasActions`) —
|
|
504
689
|
`lyra-table` reuses this component for its own empty-rows state, and `lyra-stat` (below) now uses
|
|
@@ -560,16 +745,27 @@ KPI/stat card — value + unit + label + optional icon/trend/caption.
|
|
|
560
745
|
- `goodDirection: 'up'|'down' = 'up'` (attribute `good-direction`) — which trend direction counts
|
|
561
746
|
as "good"; inverts arrow/color polarity for cost/latency/error-rate-style metrics where a
|
|
562
747
|
*decrease* is the win.
|
|
748
|
+
- `rows: StatRow[] = []` (attribute: false) — `StatRow { label: string; value: string }`; rendered as
|
|
749
|
+
a simple label/value breakdown list (`[part="rows"]`/`[part="row"]`/`[part="row-label"]`/
|
|
750
|
+
`[part="row-value"]`) beneath the caption, hidden entirely when empty
|
|
751
|
+
- `emphasis: boolean = false` (reflected) — visual emphasis (e.g. for a "headline" stat in a group):
|
|
752
|
+
a brand-colored accent edge, orthogonal to the status `variant`; status semantics still win over
|
|
753
|
+
it — `emphasis` only additionally tints `[part="value"]` brand-colored when `variant` is still
|
|
754
|
+
`'neutral'`, never overriding an actual `success`/`warning`/`danger` value color
|
|
563
755
|
|
|
564
756
|
**Events:** none.
|
|
565
757
|
|
|
566
758
|
**Slots:** default (leading icon), `caption` (rich caption content — overrides the `caption`
|
|
567
|
-
attribute when slotted content is provided)
|
|
759
|
+
attribute when slotted content is provided), `spark` (a sparkline, e.g. `<lyra-sparkline
|
|
760
|
+
slot="spark">`, or other compact trend visual — `lyra-stat` only reserves the slot and doesn't
|
|
761
|
+
render one itself)
|
|
568
762
|
|
|
569
|
-
**CSS parts:** `base`, `icon`, `label`, `value-row`, `value`, `unit`, `trend`, `caption
|
|
763
|
+
**CSS parts:** `base`, `icon`, `label`, `value-row`, `value`, `unit`, `trend`, `spark`, `caption`,
|
|
764
|
+
`rows`, `row`, `row-label`, `row-value`
|
|
570
765
|
|
|
571
766
|
**Themeable custom properties:** shared tokens only (`--lyra-color-success/-warning/-danger` drive
|
|
572
|
-
the `variant`-colored value text and up/down trend pill
|
|
767
|
+
the `variant`-colored value text and up/down trend pill; `--lyra-color-brand` drives `emphasis`'s
|
|
768
|
+
accent edge and value tint).
|
|
573
769
|
|
|
574
770
|
**Optional peer deps:** none.
|
|
575
771
|
|
|
@@ -580,8 +776,12 @@ the `variant`-colored value text and up/down trend pill).
|
|
|
580
776
|
```
|
|
581
777
|
|
|
582
778
|
**Known gotchas:**
|
|
583
|
-
- no `aria-live`
|
|
584
|
-
|
|
779
|
+
- no `aria-live` region wraps `value`/`trend` — an in-place update after first render still isn't
|
|
780
|
+
proactively announced to screen readers. The trend pill's direction/polarity is no longer
|
|
781
|
+
conveyed by icon rotation/color alone, though: a visually-hidden span now spells it out in plain
|
|
782
|
+
language (e.g. "increased 4.2%, good" / "decreased 2%, bad" / "unchanged"), so a screen reader
|
|
783
|
+
landing on the pill (rather than being live-notified of a change) gets the full meaning, not just
|
|
784
|
+
an `aria-hidden` arrow glyph.
|
|
585
785
|
|
|
586
786
|
---
|
|
587
787
|
|
|
@@ -592,7 +792,13 @@ of `rows` — this component only renders and emits intents.
|
|
|
592
792
|
|
|
593
793
|
**Properties:**
|
|
594
794
|
- `columns: TableColumn<T>[] = []` (attribute: false) — `{ key, label, sortable?, align?:
|
|
595
|
-
'start'|'end', cell: (row) => unknown }`
|
|
795
|
+
'start'|'end', priority?: 'medium'|'low', sticky?: boolean, cell: (row) => unknown }` —
|
|
796
|
+
`priority` progressively hides that column via a `@container` query as `[part='base']` narrows
|
|
797
|
+
(`'low'` hides first, under a ~900px container width; `'medium'` next, under ~640px; both
|
|
798
|
+
breakpoints are fixed in `table.styles.ts`, not themeable tokens), reversible via
|
|
799
|
+
`[part='reveal-columns-button']` (rendered whenever any column sets a `priority`); `sticky` pins
|
|
800
|
+
that column's header cell/cells to the inline-start edge (`position: sticky`) while the table
|
|
801
|
+
scrolls horizontally
|
|
596
802
|
- `rows: T[] = []` (attribute: false)
|
|
597
803
|
- `sortKey: string = ''` (attribute `sort-key`)
|
|
598
804
|
- `sortDir: 'asc'|'desc' = 'asc'` (attribute `sort-dir`)
|
|
@@ -604,13 +810,19 @@ of `rows` — this component only renders and emits intents.
|
|
|
604
810
|
- `emptyDescription: string = ''` (attribute `empty-description`)
|
|
605
811
|
- `noColumnsHeading: string = 'No columns configured'` (attribute `no-columns-heading`)
|
|
606
812
|
- `noColumnsDescription: string = ''` (attribute `no-columns-description`)
|
|
813
|
+
- `revealColumnsLabel: string = 'Show all columns'` (attribute `reveal-columns-label` — the
|
|
814
|
+
reveal-button's label while `priority`-hidden columns are hidden)
|
|
815
|
+
- `hideColumnsLabel: string = 'Show fewer columns'` (attribute `hide-columns-label` — the same
|
|
816
|
+
button's label once they've been revealed)
|
|
607
817
|
|
|
608
818
|
**Events:** `lyra-sort` (`detail: { key }`, fired on sortable-header activation), `lyra-row-click`
|
|
609
819
|
(`detail: { row }`), `lyra-load-more` (fired on the "load more" button)
|
|
610
820
|
|
|
611
821
|
**Slots:** none — content comes entirely from `columns`/`rows`.
|
|
612
822
|
|
|
613
|
-
**CSS parts:** `base`, `table`, `head`, `header-cell`, `row`, `cell`, `more-button`
|
|
823
|
+
**CSS parts:** `base`, `table`, `head`, `header-cell`, `row`, `cell`, `more-button`, `sort-icon` (a
|
|
824
|
+
chevron indicator shown on the active sortable header, rotated per `sortDir`), `reveal-columns-button`
|
|
825
|
+
(shown only when at least one column sets `priority`)
|
|
614
826
|
|
|
615
827
|
**Themeable custom properties:** `--lyra-table-max-height` (default `none`; controls the scrollable
|
|
616
828
|
body's `max-block-size`).
|
|
@@ -638,8 +850,16 @@ body's `max-block-size`).
|
|
|
638
850
|
a `cell()` callback if you need multi-select.
|
|
639
851
|
- no `caption`/`aria-label` property, so the rendered `role="grid"` has no accessible name
|
|
640
852
|
distinguishing it from any other grid on the page.
|
|
641
|
-
-
|
|
642
|
-
|
|
853
|
+
- Full roving-tabindex grid keyboard pattern (one `tabindex="0"` stop among header cells, one among
|
|
854
|
+
body rows) — Left/Right/Home/End move within the header row, Up/Down/Home/End move within the
|
|
855
|
+
body, Down from the header enters the body's roving stop and Up from the body's first row returns
|
|
856
|
+
to the header, Enter/Space still only sort/activate — a genuine strength versus most siblings in
|
|
857
|
+
this family. A `priority`-hidden header/cell is skipped when computing the visible header stops,
|
|
858
|
+
so arrow-key navigation never strands the roving stop on a hidden column.
|
|
859
|
+
- a `cell()` template can render its own interactive content (a button, link, input, etc.) without
|
|
860
|
+
it being swallowed by row/column activation: clicks and Enter/Space landing on (or bubbling
|
|
861
|
+
through) anything matching `button, a[href], input, select, textarea, [role="button"]` are left
|
|
862
|
+
alone by the table's own delegated `click`/`keydown` handlers.
|
|
643
863
|
|
|
644
864
|
---
|
|
645
865
|
|
|
@@ -653,7 +873,8 @@ Dependency-free SVG radial or linear meter (270°-sweep arc math, no charting li
|
|
|
653
873
|
- `max: number = 100`
|
|
654
874
|
- `type: 'radial'|'linear' = 'radial'` (reflected)
|
|
655
875
|
- `label: string = ''`
|
|
656
|
-
- `valueLabel?: string` (attribute: false — overrides only the *visible* text, see gotchas
|
|
876
|
+
- `valueLabel?: string` (attribute: false — overrides only the *visible* text, see gotchas; an empty
|
|
877
|
+
string is treated the same as unset and falls back to the numeric `value`)
|
|
657
878
|
|
|
658
879
|
**Events:** none.
|
|
659
880
|
|
|
@@ -675,12 +896,10 @@ gauge-specific custom property exists).
|
|
|
675
896
|
```
|
|
676
897
|
|
|
677
898
|
**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`.
|
|
899
|
+
- setting `valueLabel` (e.g. `"72°F"`) now also sets `aria-valuetext` on the host (in addition to
|
|
900
|
+
changing the visible SVG text), so a screen reader announces your formatted string instead of the
|
|
901
|
+
raw `aria-valuenow` number; the SVG `<text part="value">`/`<text part="label">` elements are
|
|
902
|
+
`aria-hidden="true"` so they're no longer separately exposed inside the same `role="meter"` host.
|
|
684
903
|
- no color-threshold/variant support: a single fixed `--lyra-color-brand` fill regardless
|
|
685
904
|
of value (no amber/red-near-capacity idiom built in).
|
|
686
905
|
- no documented component-specific sizing custom property; host size is fixed em values
|
|
@@ -688,6 +907,11 @@ gauge-specific custom property exists).
|
|
|
688
907
|
element instead.
|
|
689
908
|
- Divide-by-zero guarded (`max - min || 1`), and radial/linear share one component via the `type`
|
|
690
909
|
attribute.
|
|
910
|
+
- a non-finite `value`/`min`/`max` (e.g. left `NaN` mid-binding) no longer leaks a literal `"NaN"`
|
|
911
|
+
into `aria-valuenow`/`aria-valuemax`/the rendered value text — the affected ARIA attribute is
|
|
912
|
+
omitted entirely instead, and a finite `value` is clamped into `[min, max]` before being written
|
|
913
|
+
to `aria-valuenow` (the SVG arc/bar fill was already clamped via `ratio`; the ARIA number now
|
|
914
|
+
matches what's drawn).
|
|
691
915
|
|
|
692
916
|
---
|
|
693
917
|
|
|
@@ -698,7 +922,10 @@ opens a small menu).
|
|
|
698
922
|
|
|
699
923
|
**Properties:**
|
|
700
924
|
- `rows: Record<string, unknown>[] = []` (attribute: false)
|
|
701
|
-
- `columns: CsvColumn[] = []` (attribute: false) — `{ key, label }
|
|
925
|
+
- `columns: CsvColumn[] = []` (attribute: false) — `{ key, label }`; also acts as a field allow-list
|
|
926
|
+
for **both** export formats when non-empty (CSV always projected through `columns`; JSON now picks
|
|
927
|
+
the same `columns[].key` set rather than dumping every field on `rows`, so it can't leak a field
|
|
928
|
+
CSV would have hidden) — leave `columns` empty to export every field of `rows` as-is in JSON
|
|
702
929
|
- `filename: string = 'export'`
|
|
703
930
|
- `formats: ExportFormat[] = ['csv']` (attribute: false; `ExportFormat = 'csv' | 'json'` **only** —
|
|
704
931
|
no XLSX despite the family sometimes being described as covering it)
|
|
@@ -732,7 +959,7 @@ substitute your own server-generated download instead of the built-in client-sid
|
|
|
732
959
|
Package-level CSV utilities (used internally, also exported for standalone use — `import {
|
|
733
960
|
escapeCsvField, buildCsv, downloadBlob } from '@aceshooting/lyra-ui'`):
|
|
734
961
|
```ts
|
|
735
|
-
escapeCsvField(value: unknown): string // quotes/escapes; neutralizes formula-injection (
|
|
962
|
+
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
963
|
buildCsv(rows: Record<string, unknown>[], columns: CsvColumn[]): string // CRLF-joined, header row included
|
|
737
964
|
downloadBlob(content: string, filename: string, mime: string): void // triggers a browser download
|
|
738
965
|
```
|
|
@@ -742,6 +969,11 @@ downloadBlob(content: string, filename: string, mime: string): void // trig
|
|
|
742
969
|
`'xlsx'` expecting it to work.
|
|
743
970
|
- CSV formula-injection guarding and the deferred (5s) `URL.revokeObjectURL` (works around Safari
|
|
744
971
|
cancelling in-flight downloads on immediate revoke) are genuine, safe-to-rely-on strengths.
|
|
972
|
+
- the multi-format menu (`role="menu"`) supports full arrow-key navigation — ArrowUp/ArrowDown move
|
|
973
|
+
between items (opening the menu and seeding the right one focused, if it was closed), Home/End
|
|
974
|
+
jump to the first/last item once open, Escape closes it and returns focus to the trigger button,
|
|
975
|
+
and a completed export also returns focus to the trigger — not just Escape/click-outside as
|
|
976
|
+
before.
|
|
745
977
|
|
|
746
978
|
---
|
|
747
979
|
|
|
@@ -758,6 +990,13 @@ auto-inserted between each adjacent pair.
|
|
|
758
990
|
- `storageKey?: string` (attribute `storage-key` — persists sizes to `localStorage` under
|
|
759
991
|
`` `lyra-split:${key}:${panelCount}` ``, scoped by panel count so a stale layout for a different
|
|
760
992
|
panel count is ignored)
|
|
993
|
+
- `panelConstraints: (PanelConstraint | null)[] = []` (attribute: false) — `PanelConstraint { minPx?:
|
|
994
|
+
number; maxPx?: number }`, index-aligned with `sizes`; a `null`/missing entry (or an omitted bound
|
|
995
|
+
within an entry) leaves that side of that panel purely percent-based (the existing `min`-only
|
|
996
|
+
behavior). `sizes`, the `lyra-resize` payload, and localStorage persistence stay percent-based
|
|
997
|
+
regardless — only the effective drag/keyboard clamp bounds (and the rendered `flex-basis`, via a
|
|
998
|
+
native CSS `clamp()` so a constrained panel stays pinned between its px bounds across container
|
|
999
|
+
resizes with no extra `ResizeObserver`) change for a constrained panel.
|
|
761
1000
|
|
|
762
1001
|
**Events:** `lyra-resize` (`detail: { sizes }`, fired on every drag step/release **and** every
|
|
763
1002
|
keyboard step)
|
|
@@ -779,29 +1018,160 @@ keyboard step)
|
|
|
779
1018
|
```
|
|
780
1019
|
|
|
781
1020
|
Keyboard: focus a divider (`Tab`), then `ArrowRight`/`ArrowLeft` (horizontal) or
|
|
782
|
-
`ArrowDown`/`ArrowUp` (vertical) to resize by a fixed 2% step
|
|
1021
|
+
`ArrowDown`/`ArrowUp` (vertical) to resize by a fixed 2% step — RTL-aware for horizontal layouts
|
|
1022
|
+
(under `direction: rtl`, the forward/backward keys and drag-delta sign both swap so they still track
|
|
1023
|
+
the visually-adjacent panel).
|
|
783
1024
|
|
|
784
1025
|
**Known gotchas:**
|
|
785
|
-
- `panelCount`
|
|
786
|
-
`
|
|
787
|
-
|
|
788
|
-
`
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
`
|
|
794
|
-
|
|
1026
|
+
- `panelCount` now reacts to `slotchange` (not just the initial `connectedCallback()` read), and
|
|
1027
|
+
`ensureSizes()` rebalances existing sizes proportionally when a panel is added or removed after
|
|
1028
|
+
mount instead of discarding the whole layout — a conditionally-shown side panel no longer leaves
|
|
1029
|
+
`panelCount`/`sizes`/divider count stale.
|
|
1030
|
+
- divider `aria-valuemax` is now computed per adjacent pair (`sizes[i] + sizes[i+1] - min`) rather
|
|
1031
|
+
than a blanket `100 - min`, so it's accurate for 3+-panel layouts too, not just exactly two panels
|
|
1032
|
+
— this formula still only accounts for the plain percent `min`, though: with `panelConstraints`
|
|
1033
|
+
set, a panel's real achievable range can be narrower (or expressed in px) than what
|
|
1034
|
+
`aria-valuemin`/`aria-valuemax` report. Each divider also now has its own `aria-label` ("Resize
|
|
1035
|
+
divider between panel N and panel N+1") distinguishing it from any other divider in a
|
|
1036
|
+
multi-divider layout.
|
|
795
1037
|
- no feasibility check on `min` vs. panel count (e.g. 3 panels with `min=40` is
|
|
796
1038
|
unsatisfiable) — result is a silently frozen splitter with no warning.
|
|
797
|
-
- `
|
|
798
|
-
|
|
799
|
-
`
|
|
1039
|
+
- concurrent drags are tracked per `pointerId` (not a single scalar), so a multi-touch drag on two
|
|
1040
|
+
different dividers moves both independently instead of the second pointer clobbering the first's
|
|
1041
|
+
drag state; `pointercancel`/`lostpointercapture` (not just `pointerup`) both end a drag.
|
|
1042
|
+
- `localStorage.getItem`/`setItem` calls are now both wrapped in their own `try`/`catch` (in addition
|
|
1043
|
+
to the `JSON.parse` result already being caught), so a blocked or quota-exceeded store fails
|
|
1044
|
+
silently instead of throwing from inside a `pointerup`/`keydown` handler. A persisted layout whose
|
|
1045
|
+
panel count no longer matches, or whose sizes are already below the current `min` floor, is
|
|
1046
|
+
rejected rather than restored.
|
|
800
1047
|
- Pointer-drag lifecycle (pointer capture, window-listener cleanup on both drag-end and
|
|
801
1048
|
`disconnectedCallback`) is solid and safe to rely on.
|
|
802
1049
|
|
|
803
1050
|
---
|
|
804
1051
|
|
|
1052
|
+
## `lyra-widget`
|
|
1053
|
+
|
|
1054
|
+
A titled panel shell with an optional collapse toggle and an optional fullscreen-expand toggle.
|
|
1055
|
+
First-party invention (no Web Awesome equivalent). Fullscreen promotes the same host element in
|
|
1056
|
+
place (a CSS state, not a clone/portal), so slotted content (a chart, a running simulation, scroll
|
|
1057
|
+
position) survives the transition.
|
|
1058
|
+
|
|
1059
|
+
**Properties:**
|
|
1060
|
+
- `label: string = ''`
|
|
1061
|
+
- `sublabel: string = ''`
|
|
1062
|
+
- `collapsible: boolean = false` (reflected — shows the collapse/expand chevron button)
|
|
1063
|
+
- `collapsed: boolean = false` (reflected)
|
|
1064
|
+
- `expandable: boolean = false` (reflected — shows the fullscreen toggle button)
|
|
1065
|
+
- `fullscreen: boolean = false` (reflected)
|
|
1066
|
+
|
|
1067
|
+
**Events:** `lyra-collapse-change` (`detail: boolean`, the new `collapsed` state),
|
|
1068
|
+
`lyra-fullscreen-change` (`detail: boolean`, the new `fullscreen` state — also fired when fullscreen
|
|
1069
|
+
is exited via Escape or a backdrop click, not just the toggle button)
|
|
1070
|
+
|
|
1071
|
+
**Slots:** default (the panel body), `actions` (header action controls, rendered before the
|
|
1072
|
+
collapse/expand buttons)
|
|
1073
|
+
|
|
1074
|
+
**CSS parts:** `base`, `header`, `title`, `label`, `sublabel`, `actions`, `collapse-button`,
|
|
1075
|
+
`fullscreen-button`, `body`, `backdrop`
|
|
1076
|
+
|
|
1077
|
+
**Themeable custom properties:** `--lyra-widget-overlay-color` (default `rgb(0 0 0 / 0.5)` — the
|
|
1078
|
+
fullscreen backdrop scrim color; component-specific since no shared `--wa-*`/`--lyra-*` overlay token
|
|
1079
|
+
exists), plus shared tokens (`--lyra-space-*`, `--lyra-color-border/-surface/-text-quiet`,
|
|
1080
|
+
`--lyra-radius`, `--lyra-shadow`, `--lyra-icon-button-size`, `--lyra-focus-ring-*`).
|
|
1081
|
+
|
|
1082
|
+
**Optional peer deps:** none.
|
|
1083
|
+
|
|
1084
|
+
```html
|
|
1085
|
+
<lyra-widget label="Load profile" sublabel="Last 7 days" collapsible expandable>
|
|
1086
|
+
<span slot="actions"><button>Refresh</button></span>
|
|
1087
|
+
<div>Panel body content — a chart, a table, anything.</div>
|
|
1088
|
+
</lyra-widget>
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
While `fullscreen`, `[part="base"]` (not the host itself) takes `role="dialog"` + `aria-modal="true"`
|
|
1092
|
+
(with `aria-label` from `label`, falling back to `"Fullscreen panel"`), document scroll is locked
|
|
1093
|
+
(ref-counted, safe with multiple simultaneously-fullscreen widgets), and Tab/Shift+Tab are bounded
|
|
1094
|
+
to the panel's own focusable content (`actions` slot → collapse/fullscreen buttons → body slot,
|
|
1095
|
+
matching visual tab order — resolved shadow-piercingly, so a slotted custom element's real
|
|
1096
|
+
focusable target inside its own shadow root is found too) so keyboard focus can't escape to page
|
|
1097
|
+
content hidden behind the backdrop. Escape or clicking the backdrop exits fullscreen and returns
|
|
1098
|
+
focus to whichever button triggered it.
|
|
1099
|
+
|
|
1100
|
+
**Known gotchas:**
|
|
1101
|
+
- no size/density variant — header and body padding are fixed.
|
|
1102
|
+
- a reconnect that preserves the same element instance (e.g. a drag-and-drop reparent) restores the
|
|
1103
|
+
scroll lock and the Tab-trap `keydown` listener if `fullscreen` was still `true` across the
|
|
1104
|
+
move — `disconnectedCallback`/`connectedCallback` fire back-to-back with no update in between, so
|
|
1105
|
+
`willUpdate()` alone wouldn't otherwise notice.
|
|
1106
|
+
- `collapsed` hides the body via `hidden` rather than an animated height transition — collapsing is
|
|
1107
|
+
instant, not a slide.
|
|
1108
|
+
|
|
1109
|
+
---
|
|
1110
|
+
|
|
1111
|
+
## `lyra-word-cloud`
|
|
1112
|
+
|
|
1113
|
+
Dependency-free SVG word/tag cloud. First-party invention (no Web Awesome equivalent). Lays words
|
|
1114
|
+
out via an outward Archimedean-spiral search — heaviest word placed first, each word spiraling from
|
|
1115
|
+
the center until it clears every word already placed. Unlike sibling `lyra-sparkline`/`lyra-heatmap`
|
|
1116
|
+
(one `role="img"` glyph standing in for an aggregate value), each word here is its own focusable
|
|
1117
|
+
`role="button"`, since the individual words are the meaningful interactive content.
|
|
1118
|
+
|
|
1119
|
+
**Properties:**
|
|
1120
|
+
- `words: WordCloudWord[] = []` (attribute: false) — `{ text: string, weight: number, color?:
|
|
1121
|
+
string, group?: string }`; `weight` drives font size, `color` overrides the palette for that word,
|
|
1122
|
+
`group` shares one palette color across every word with the same `group` value
|
|
1123
|
+
- `minFontSize: number = 12` (attribute `min-font-size`) — px, applied to the lowest-weight word
|
|
1124
|
+
- `maxFontSize: number = 48` (attribute `max-font-size`) — px, applied to the highest-weight word
|
|
1125
|
+
- `scale: 'linear'|'sqrt' = 'linear'` — `sqrt` compresses the weight→font-size mapping so one heavy
|
|
1126
|
+
word doesn't dwarf the rest, matching `lyra-heatmap`'s `scale` property
|
|
1127
|
+
- `orientations: 'horizontal'|'mixed' = 'horizontal'` — `mixed` lets ~25% of words render rotated
|
|
1128
|
+
90° for denser packing
|
|
1129
|
+
- `palette?: string[]` (attribute: false) — custom categorical colors, cycled by word index (or by
|
|
1130
|
+
`group`); defaults to the `--lyra-word-cloud-color-1..8` tokens
|
|
1131
|
+
|
|
1132
|
+
**Events:** `lyra-word-click` (`detail: { text, weight, group }`, fires on click, or Enter/Space on
|
|
1133
|
+
a focused word)
|
|
1134
|
+
|
|
1135
|
+
**Slots:** none.
|
|
1136
|
+
|
|
1137
|
+
**CSS parts:** `base`, `svg`, `word` (each `<text>`), `empty` (the no-data placeholder)
|
|
1138
|
+
|
|
1139
|
+
**Themeable custom properties:** `--lyra-word-cloud-color-1` through `-8` (the default categorical
|
|
1140
|
+
palette, a data-driven literal exception like `lyra-heatmap`'s scale-ramp endpoints — exposed as
|
|
1141
|
+
retheme-able custom properties instead of hardcoded), plus shared tokens (`--lyra-font`,
|
|
1142
|
+
`--lyra-focus-ring-*`, `--lyra-transition-fast`, `--lyra-color-text-quiet`).
|
|
1143
|
+
|
|
1144
|
+
**Optional peer deps:** none.
|
|
1145
|
+
|
|
1146
|
+
```html
|
|
1147
|
+
<lyra-word-cloud id="cloud" style="height: 20rem"></lyra-word-cloud>
|
|
1148
|
+
<script type="module">
|
|
1149
|
+
document.getElementById('cloud').words = [
|
|
1150
|
+
{ text: 'JavaScript', weight: 90 },
|
|
1151
|
+
{ text: 'TypeScript', weight: 75 },
|
|
1152
|
+
{ text: 'Lit', weight: 60, group: 'framework' },
|
|
1153
|
+
];
|
|
1154
|
+
document.getElementById('cloud').addEventListener('lyra-word-click', (e) => console.log(e.detail));
|
|
1155
|
+
</script>
|
|
1156
|
+
```
|
|
1157
|
+
|
|
1158
|
+
**Known gotchas:**
|
|
1159
|
+
- capped at 150 words (`MAX_WORDS` in `word-cloud-layout.ts`, mirroring `lyra-sparkline`'s
|
|
1160
|
+
`MAX_BARS` DOM-node-count guard) — anything beyond that is silently dropped from layout (not
|
|
1161
|
+
rendered, not an error); a pathological input (e.g. one huge word repeated many times) can also
|
|
1162
|
+
exhaust the spiral search's radius bound and get dropped the same way.
|
|
1163
|
+
- every word is its own tab stop (`tabindex="0"` `role="button"`), not a single roving-tabindex
|
|
1164
|
+
container like `lyra-heatmap` — fine for a modest word count, but tabbing through a cloud near the
|
|
1165
|
+
150-word cap is a lot of stops; there's no non-visual "get the whole word+weight list at once"
|
|
1166
|
+
alternative yet.
|
|
1167
|
+
- text width is measured via a detached `<canvas>` 2D context (`ctx.measureText`), not the actual
|
|
1168
|
+
rendered SVG glyph metrics — close enough for collision-avoidance spacing, but not pixel-exact.
|
|
1169
|
+
- rotation (`orientations="mixed"`) is genuinely random per layout (`Math.random()`, not seeded), so
|
|
1170
|
+
which words render rotated changes on every re-layout (any `words`/`minFontSize`/`maxFontSize`/
|
|
1171
|
+
`scale`/`orientations` change) — don't rely on rotation being stable across renders.
|
|
1172
|
+
|
|
1173
|
+
---
|
|
1174
|
+
|
|
805
1175
|
# Temporal & graph
|
|
806
1176
|
|
|
807
1177
|
## `lyra-time-range`
|
|
@@ -816,17 +1186,24 @@ onto `[min, max]`).
|
|
|
816
1186
|
- `end: number = 100`
|
|
817
1187
|
- `step: number = 1`
|
|
818
1188
|
- `disabled: boolean = false` (reflected)
|
|
1189
|
+
- `presets: TimeRangePreset[] = []` (attribute: false) — `TimeRangePreset { label: string; start:
|
|
1190
|
+
number; end: number }`; optional discrete presets (e.g. "Last 7 days") rendered as a
|
|
1191
|
+
`[part="presets"]` button row above the track — purely additive, the continuous brush is
|
|
1192
|
+
unaffected and both interaction modes coexist; picking one sets both handles and emits the same
|
|
1193
|
+
`lyra-input`/`lyra-change` pair a committed drag or keyboard step would
|
|
819
1194
|
|
|
820
|
-
**Events:** `lyra-input` (fired continuously while dragging or on arrow
|
|
821
|
-
start, end }`), `lyra-change` (fired on pointer release /
|
|
822
|
-
end }`)
|
|
1195
|
+
**Events:** `lyra-input` (fired continuously while dragging or on each arrow/Home/End/PageUp/
|
|
1196
|
+
PageDown key press, `detail: { start, end }`), `lyra-change` (fired on pointer release /
|
|
1197
|
+
key-up-commit, or when a preset button is clicked, `detail: { start, end }`)
|
|
823
1198
|
|
|
824
1199
|
**Slots:** none.
|
|
825
1200
|
|
|
826
|
-
**CSS parts:** `base`, `track`, `range`, `handle-start`, `handle-end`
|
|
1201
|
+
**CSS parts:** `base`, `track`, `range`, `handle-start`, `handle-end`, `presets`, `preset-button`
|
|
827
1202
|
|
|
828
1203
|
**Themeable custom properties:** shared tokens only — `--lyra-color-border`, `--lyra-color-brand`,
|
|
829
|
-
`--lyra-color-surface`, `--lyra-shadow
|
|
1204
|
+
`--lyra-color-surface`, `--lyra-shadow` (track/handles), plus (for `presets`) `--lyra-color-text`,
|
|
1205
|
+
`--lyra-color-on-brand` (the active preset button's text), `--lyra-radius`, `--lyra-space-xs/-s`,
|
|
1206
|
+
`--lyra-transition-fast`, `--lyra-focus-ring-*`. No component-local tokens.
|
|
830
1207
|
|
|
831
1208
|
**Optional peer deps:** none.
|
|
832
1209
|
|
|
@@ -839,15 +1216,27 @@ end }`)
|
|
|
839
1216
|
```
|
|
840
1217
|
|
|
841
1218
|
**Known gotchas:**
|
|
842
|
-
- Keyboard support
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
-
|
|
850
|
-
|
|
1219
|
+
- Keyboard support now matches the full WAI-ARIA APG slider pattern: ArrowUp/Right and ArrowDown/Left
|
|
1220
|
+
move by `step` (RTL-aware — under `direction: rtl` the forward/backward keys swap so they still
|
|
1221
|
+
track the visually-adjacent direction), PageUp/PageDown move by `step * 10`, and Home/End jump to
|
|
1222
|
+
that handle's actual *reachable* bound — clamped by the sibling handle's current value, not the
|
|
1223
|
+
component's full `[min, max]` domain, so Home/End on the `end` handle can't jump past `start` (and
|
|
1224
|
+
vice versa). Pointer-drag is RTL-aware the same way (mirrors the drag ratio under `direction:
|
|
1225
|
+
rtl`).
|
|
1226
|
+
- A disabled handle now gets `aria-disabled="true"` in addition to losing `tabindex` — a
|
|
1227
|
+
screen-reader user exploring by virtual cursor no longer hears it announced as a live, adjustable
|
|
1228
|
+
slider.
|
|
1229
|
+
- `aria-valuemin`/`aria-valuemax` on each handle report that handle's reachable sub-range (bounded by
|
|
1230
|
+
its sibling), not the full domain — matching what Home/End actually jump to.
|
|
1231
|
+
- No `aria-valuetext`: only raw numeric `aria-valuenow` is exposed (omitted entirely, rather than a
|
|
1232
|
+
literal `"NaN"`, if `start`/`end` is non-finite), no hook for a human-readable (e.g. formatted
|
|
1233
|
+
date/time) equivalent of the mapped domain.
|
|
1234
|
+
- Handles a `min > max` domain, a non-positive/non-finite `step`, and disabled-mid-drag/
|
|
1235
|
+
disconnect-mid-drag correctly (tested) — safe to rely on those edge cases. Concurrent drags are
|
|
1236
|
+
tracked per `pointerId` (not a single scalar), so a two-finger touch — one finger per handle —
|
|
1237
|
+
moves both independently instead of the second pointer hijacking which handle the first pointer's
|
|
1238
|
+
moves apply to; `pointercancel`/`lostpointercapture` (not just `pointerup`) both end a drag, same
|
|
1239
|
+
fix as `lyra-split`.
|
|
851
1240
|
|
|
852
1241
|
---
|
|
853
1242
|
|
|
@@ -875,7 +1264,8 @@ tick and manual step)
|
|
|
875
1264
|
**CSS parts:** `base`, `play-button`, `slider`
|
|
876
1265
|
|
|
877
1266
|
**Themeable custom properties:** shared tokens only — `--lyra-space-s`, `--lyra-color-border`,
|
|
878
|
-
`--lyra-color-surface`, `--lyra-color-text`, `--lyra-color-brand
|
|
1267
|
+
`--lyra-color-surface`, `--lyra-color-text`, `--lyra-color-brand`, `--lyra-icon-button-size` (the
|
|
1268
|
+
play button's box).
|
|
879
1269
|
|
|
880
1270
|
**Optional peer deps:** none.
|
|
881
1271
|
|
|
@@ -888,9 +1278,15 @@ tick and manual step)
|
|
|
888
1278
|
```
|
|
889
1279
|
|
|
890
1280
|
**Known gotchas:**
|
|
891
|
-
- `index` is
|
|
892
|
-
|
|
893
|
-
|
|
1281
|
+
- `index` is now re-clamped into `[0, length)` as soon as `length` shrinks (in `willUpdate()`, not
|
|
1282
|
+
waiting for the next `tick()`/`goTo()`/`next()`/`previous()` call) — setting `el.length = 2` while
|
|
1283
|
+
`el.index = 7` immediately pulls `index` back to `1`, and playback auto-pauses if `length` drops
|
|
1284
|
+
to `<= 1` while playing (the play button and slider would otherwise both become disabled with no
|
|
1285
|
+
way to stop it — both are `?disabled` whenever `length <= 1`, not just the button).
|
|
1286
|
+
- `intervalMs` is live-reactive mid-playback: ticking is a self-rescheduling `setTimeout` (not one
|
|
1287
|
+
long-lived `setInterval`), so `intervalMs` is re-read fresh before every tick — changing
|
|
1288
|
+
`interval-ms` while `playing` takes effect on the very next step instead of only after a
|
|
1289
|
+
pause/play cycle.
|
|
894
1290
|
- No `aria-valuetext`/visible "N of M" position label on the range input.
|
|
895
1291
|
- Calling `play()`/`pause()` programmatically (not via the button) gives no `aria-live`
|
|
896
1292
|
announcement of the Play/Pause state change.
|
|
@@ -899,31 +1295,65 @@ tick and manual step)
|
|
|
899
1295
|
|
|
900
1296
|
## `lyra-heatmap`
|
|
901
1297
|
|
|
902
|
-
A Canvas-rendered
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1298
|
+
A Canvas-rendered heatmap with a DPR-aware, resize-aware redraw loop, in one of two `mode`s:
|
|
1299
|
+
`"matrix"` (default — a `rowLabels` × `colLabels` grid of `values`) or `"calendar"` (a
|
|
1300
|
+
GitHub-style Sunday–Saturday × week grid built from `days`, colored by quartile bucket rather than
|
|
1301
|
+
the matrix mode's continuous ramp). Every cell is independently addressable despite being
|
|
1302
|
+
canvas-drawn (no per-cell DOM node): a `pointermove` hit-test over the canvas shows `[part="tooltip"]`
|
|
1303
|
+
with that cell's label + value; the canvas is `tabindex="0"` with arrow-key roving focus (a stroked
|
|
1304
|
+
ring redrawn over the focused cell on every draw, plus `[part="live-region"]` announcing it); and a
|
|
1305
|
+
click, or Enter/Space on the focused cell, fires `lyra-cell-click`.
|
|
906
1306
|
|
|
907
1307
|
**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`
|
|
1308
|
+
- `rowLabels: string[] = []` (attribute: false — matrix mode only)
|
|
1309
|
+
- `colLabels: string[] = []` (attribute: false — matrix mode only)
|
|
1310
|
+
- `values: number[][] = []` (attribute: false — matrix mode only) — `-1` or any non-finite value is
|
|
1311
|
+
the "no data" sentinel; ragged/sparse rows are safe (`?? -1`)
|
|
1312
|
+
- `cellSize: number = 22` (attribute `cell-size` — matrix mode only, ignored when `fitToWidth` is
|
|
1313
|
+
set)
|
|
1314
|
+
- `fitToWidth: boolean = false` (attribute `fit-to-width` — matrix mode only; derives `cellSize` from
|
|
1315
|
+
the host's measured `clientWidth` on every draw/resize instead of the fixed `cell-size`, so the
|
|
1316
|
+
grid actually fills the available width — see gotchas for the default, non-`fit-to-width` behavior)
|
|
913
1317
|
- `valueLabel: string = 'value'` (attribute `value-label`)
|
|
914
|
-
- `scale: 'linear' | 'sqrt' = 'linear'`
|
|
915
|
-
|
|
916
|
-
|
|
1318
|
+
- `scale: 'linear' | 'sqrt' = 'linear'` (matrix mode only — calendar mode always buckets by quartile)
|
|
1319
|
+
- `mode: 'matrix' | 'calendar' = 'matrix'`
|
|
1320
|
+
- `days: CalendarDay[] = []` (attribute: false — calendar mode only) — `CalendarDay { date:
|
|
1321
|
+
string /* ISO yyyy-mm-dd */; value: number }`; need not be sorted or contiguous, and an entry whose
|
|
1322
|
+
`date` doesn't parse is dropped rather than poisoning the whole grid
|
|
1323
|
+
- `bucketCount: number = 5` (attribute `bucket-count` — calendar mode only; non-finite values fall
|
|
1324
|
+
back to 5 with a one-time console warning, and the count is floored to an integer >= 2)
|
|
1325
|
+
- `annotations: HeatmapAnnotation[] = []` (attribute: false) — `HeatmapAnnotation { row?: number;
|
|
1326
|
+
col?: number; date?: string; label?: string }`: matrix mode matches by `row`/`col`, calendar mode
|
|
1327
|
+
by `date` (whichever pair matches the active `mode`; the other fields are ignored). Draws a
|
|
1328
|
+
stroked ring over the matching cell; an annotation with a `label` also gets its own
|
|
1329
|
+
`[part="legend-annotation"]` entry in the legend.
|
|
1330
|
+
|
|
1331
|
+
**Events:** `lyra-cell-click` (fired on click, or Enter/Space on the keyboard-focused cell —
|
|
1332
|
+
`detail: { row, col, value }` in matrix mode, `detail: { date, value }` in calendar mode)
|
|
917
1333
|
|
|
918
1334
|
**Slots:** none.
|
|
919
1335
|
|
|
920
|
-
**CSS parts:** `base`, `canvas`, `
|
|
1336
|
+
**CSS parts:** `base`, `canvas`, `tooltip` (hover tooltip, positioned over the hovered cell),
|
|
1337
|
+
`live-region` (visually-hidden `role="status" aria-live="polite"` element announcing the
|
|
1338
|
+
keyboard-focused cell), `legend`, `legend-lo`, `legend-hi`, `legend-annotation` (one per labeled
|
|
1339
|
+
`annotations` entry)
|
|
921
1340
|
|
|
922
1341
|
**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-
|
|
1342
|
+
`--lyra-heatmap-scale-hi` (default `#0969da`) — the sequential color-ramp endpoints (matrix mode) or
|
|
1343
|
+
quartile-bucket ramp endpoints (calendar mode), resolved via `getComputedStyle` each draw (any valid
|
|
1344
|
+
CSS color syntax — hex/rgb/hsl/oklch/named — works, resolved through a scratch canvas).
|
|
1345
|
+
`--lyra-heatmap-no-data-fill` (default `rgba(128,128,128,0.25)` — the no-data cell fill, same
|
|
1346
|
+
resolve-via-`getComputedStyle` pattern), `--lyra-heatmap-label-font` (default `10px sans-serif` — the
|
|
1347
|
+
canvas-drawn axis/month/weekday label font), `--lyra-heatmap-focus-ring-color` (default
|
|
1348
|
+
`var(--lyra-focus-ring-color)` — the canvas-drawn ring stroked around the keyboard-focused cell;
|
|
1349
|
+
also reused by `[part="canvas"]`'s own `:focus-visible` outline so the two stay visually in sync),
|
|
1350
|
+
`--lyra-heatmap-annotation-color` (default `var(--lyra-color-danger)` — the canvas-drawn ring
|
|
1351
|
+
stroked around an annotated cell, deliberately not one of the sequential ramp colors so it stays
|
|
1352
|
+
visible regardless of what it's drawn over). `--lyra-heatmap-tooltip-bg` (default
|
|
1353
|
+
`var(--lyra-color-surface)`) and `--lyra-heatmap-tooltip-text` (default `var(--lyra-color-text)`) —
|
|
1354
|
+
unlike the canvas-drawn tokens above, `[part="tooltip"]` is a real DOM element and consumes these
|
|
1355
|
+
directly, no `getComputedStyle` bridging needed. Also consumes `--lyra-color-text-quiet` (axis label
|
|
1356
|
+
color) and `--lyra-space-xs`.
|
|
927
1357
|
|
|
928
1358
|
**Optional peer deps:** none.
|
|
929
1359
|
|
|
@@ -937,20 +1367,36 @@ resolved through a scratch canvas). Also consumes `--lyra-color-text-quiet` (axi
|
|
|
937
1367
|
</script>
|
|
938
1368
|
```
|
|
939
1369
|
|
|
1370
|
+
```html
|
|
1371
|
+
<!-- Calendar mode: a GitHub-contributions-style day grid -->
|
|
1372
|
+
<lyra-heatmap mode="calendar" value-label="commits"></lyra-heatmap>
|
|
1373
|
+
<script>
|
|
1374
|
+
document.querySelector('lyra-heatmap').days = [
|
|
1375
|
+
{ date: '2026-01-01', value: 3 },
|
|
1376
|
+
{ date: '2026-01-02', value: 0 },
|
|
1377
|
+
// ...
|
|
1378
|
+
];
|
|
1379
|
+
</script>
|
|
1380
|
+
```
|
|
1381
|
+
|
|
940
1382
|
**Known gotchas:**
|
|
941
|
-
- the `ResizeObserver`
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
style
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
1383
|
+
- the `ResizeObserver` only actually resizes the drawn grid in matrix mode **when
|
|
1384
|
+
`fit-to-width` is set**. Without it (the default), `draw()` sizes the canvas as
|
|
1385
|
+
`PAD_LEFT + cols * cellSize`, never from the host's measured width, so a container-resize redraw is
|
|
1386
|
+
a geometric no-op; the stylesheet's `canvas { inline-size: 100% }` is also dead code in that case,
|
|
1387
|
+
since `draw()` unconditionally sets an inline `canvas.style.width/height` that wins over it.
|
|
1388
|
+
Calendar mode ignores `fit-to-width` entirely — its canvas is always sized from the computed
|
|
1389
|
+
`weekCount`.
|
|
1390
|
+
- the host itself is unconditionally `role="img"` with a dimensions+range summary `aria-label`
|
|
1391
|
+
(calendar mode: a day-count + range summary instead), same as before — but `[part="canvas"]`
|
|
1392
|
+
inside it is now a real focusable, keyboard-operable, per-cell-interactive control (roving
|
|
1393
|
+
arrow-key focus, `[part="live-region"]` announcements, `lyra-cell-click`). `role="img"` is
|
|
1394
|
+
documented (ARIA) to flatten its subtree to a single image for some assistive tech, which is in
|
|
1395
|
+
tension with a focusable descendant inside it — plausible from the code, unverified against real
|
|
1396
|
+
AT.
|
|
1397
|
+
- `NaN`/non-finite cell values in matrix mode are correctly treated as no-data now (alongside `-1`),
|
|
1398
|
+
and repeated DPR crossings (moving the window across displays with different pixel ratios) no
|
|
1399
|
+
longer leak a `MediaQueryList` listener per crossing — both previously-known issues are fixed.
|
|
954
1400
|
|
|
955
1401
|
---
|
|
956
1402
|
|
|
@@ -966,8 +1412,14 @@ A force-directed node-link diagram with pan/zoom/drag, built on `d3-force`.
|
|
|
966
1412
|
dropped)
|
|
967
1413
|
- `width: number = 800`
|
|
968
1414
|
- `height: number = 600`
|
|
969
|
-
- `chargeStrength: number = -300` (attribute `charge-strength`)
|
|
970
|
-
- `linkDistance: number = 100` (attribute `link-distance`)
|
|
1415
|
+
- `chargeStrength: number = -300` (attribute `charge-strength` — live-reactive, see gotchas)
|
|
1416
|
+
- `linkDistance: number = 100` (attribute `link-distance` — live-reactive, see gotchas)
|
|
1417
|
+
- `minZoom: number = 0.1` (attribute `min-zoom`)
|
|
1418
|
+
- `maxZoom: number = 8` (attribute `max-zoom`)
|
|
1419
|
+
- `seed?: number` (attribute: false) — when set, seeds each node's initial x/y deterministically
|
|
1420
|
+
(keyed by node **id**, not array index/order) instead of `forceSimulation()`'s own random start,
|
|
1421
|
+
and settles the simulation synchronously instead of animating the settle (same effect
|
|
1422
|
+
`prefers-reduced-motion` has, see gotchas)
|
|
971
1423
|
|
|
972
1424
|
**Events:** `lyra-node-click` (`detail: { id }`), `lyra-link-click` (`detail: { source, target }`)
|
|
973
1425
|
|
|
@@ -994,17 +1446,36 @@ install with `pnpm add d3-force d3-drag d3-zoom d3-selection`).
|
|
|
994
1446
|
|
|
995
1447
|
**Known gotchas:**
|
|
996
1448
|
- 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
|
-
|
|
1449
|
+
continuously while dragging via `alphaTarget(0.3)`) writes node/link positions straight onto the
|
|
1450
|
+
already-rendered DOM via `setAttribute()` rather than reassigning `simNodes`/`simLinks` (that
|
|
1451
|
+
reassignment — and the Lit re-render/`applyInteractions()` re-scan it used to force on every tick —
|
|
1452
|
+
now only happens once per structural `nodes`/`links` change). Still a noticeable cost building up
|
|
1453
|
+
the initial layout or while a node is actively being dragged, just no longer once per tick on an
|
|
1454
|
+
otherwise-settled graph.
|
|
1455
|
+
- `chargeStrength`/`linkDistance` **are** live-reactive post-mount now (retuned on the existing
|
|
1456
|
+
force objects and the simulation nudged via `alpha(0.3).restart()`) — no need to also touch
|
|
1457
|
+
`nodes`/`links` to see the effect.
|
|
1458
|
+
- zoom is bounded via `minZoom`/`maxZoom` (`d3-zoom`'s `.scaleExtent(...)`, live-reactive); pan/
|
|
1459
|
+
zoom/drag are still pointer-only with no keyboard equivalent. Links (`<line part="link">`) are now
|
|
1460
|
+
keyboard-operable too (`tabindex="0"`, `role="button"`, `aria-label`, Enter/Space), matching nodes.
|
|
1461
|
+
- while the `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection` peers are resolving, the host shows a
|
|
1462
|
+
`<lyra-skeleton>` sized to `width`/`height` with `aria-busy="true"` — but if they fail to load
|
|
1463
|
+
(not installed), it still settles into a permanently empty `<svg>` (0 nodes/0 links) plus a
|
|
1464
|
+
one-time console warning, same as before; the skeleton only covers the loading window itself.
|
|
1465
|
+
- `GraphNode.color` is sanitized (rejects `;`/`{`/`}`) before being written into the
|
|
1466
|
+
`--lyra-node-fill` inline custom property, so an untrusted color string can't break out of that
|
|
1467
|
+
CSS declaration.
|
|
1468
|
+
- a structural `nodes`/`links` change now carries over each already-settled node's position (and any
|
|
1469
|
+
in-progress drag) by id when rebuilding the simulation, instead of discarding every node's (x, y)
|
|
1470
|
+
and re-running the whole ~300-tick random-start settle from scratch — only genuinely new ids get a
|
|
1471
|
+
fresh start. Handy for a streaming/incrementally-updated graph, whose existing layout no longer
|
|
1472
|
+
jumps every time a node/link is appended.
|
|
1473
|
+
- under `prefers-reduced-motion: reduce`, or whenever `seed` is set, the simulation converges
|
|
1474
|
+
synchronously (ticked in a loop down to `alphaMin` before first paint) instead of animating over
|
|
1475
|
+
~300 rendered frames; user-initiated motion (dragging a node) is unaffected either way.
|
|
1476
|
+
- the `<svg part="svg">` now carries `role="group"` and an `aria-label` summarizing the node/link
|
|
1477
|
+
counts (e.g. "Node-link diagram with 5 nodes and 4 links"), and node `<text part="label">`s are
|
|
1478
|
+
`aria-hidden="true"` (their content is already covered by each node's own `aria-label`).
|
|
1008
1479
|
|
|
1009
1480
|
---
|
|
1010
1481
|
|
|
@@ -1016,14 +1487,30 @@ recursively renders itself and its own nested children.
|
|
|
1016
1487
|
|
|
1017
1488
|
### `lyra-tree`
|
|
1018
1489
|
|
|
1490
|
+
Implements the full WAI-ARIA treeitem keyboard pattern: a single roving `tabindex` (tracked as
|
|
1491
|
+
`activeId`, pushed down to every `<lyra-tree-node>` including nested ones) and
|
|
1492
|
+
ArrowUp/Down/Right/Left/Home/End/Enter/Space handled by one delegated `keydown` listener (native
|
|
1493
|
+
`KeyboardEvent`s are `composed: true` and bubble across shadow-DOM boundaries, so a press inside a
|
|
1494
|
+
deeply-nested node's own shadow root still reaches it).
|
|
1495
|
+
|
|
1019
1496
|
**Properties:**
|
|
1020
1497
|
- `data: TreeItem[] = []` (attribute: false) — `TreeItem { id: string; label: string; children?:
|
|
1021
1498
|
TreeItem[]; badge?: string | number }`
|
|
1499
|
+
- `label: string = ''` — accessible name for the tree; `role="tree"` lives on an internal
|
|
1500
|
+
`[part="base"]` element, not the host, so this is the only way to label it (`aria-label`/
|
|
1501
|
+
`aria-labelledby` set directly on `<lyra-tree>` wouldn't reach it)
|
|
1502
|
+
|
|
1503
|
+
**Keyboard:** ArrowDown/ArrowUp move the roving focus to the next/previous *visible* node.
|
|
1504
|
+
ArrowRight expands a collapsed node (focus stays put; a second ArrowRight then steps into the first
|
|
1505
|
+
child) or moves into an already-expanded node's first child. ArrowLeft collapses an expanded node, or
|
|
1506
|
+
moves focus to its parent. Home/End jump to the first/last visible node. Enter/Space activate
|
|
1507
|
+
`select()` on the focused node.
|
|
1022
1508
|
|
|
1023
1509
|
**Methods:** `expandAll()`, `collapseAll()` (both recursive, properly sequenced around Lit's render
|
|
1024
1510
|
cycle).
|
|
1025
1511
|
|
|
1026
|
-
**Events:** none dispatched directly (see `lyra-tree-node` below — they bubble up
|
|
1512
|
+
**Events:** none dispatched directly (see `lyra-tree-node` below — they bubble up and are also
|
|
1513
|
+
observed internally to keep the roving `activeId` in sync with clicks).
|
|
1027
1514
|
|
|
1028
1515
|
**Slots:** default (holds the `<lyra-tree-node>` elements it manages).
|
|
1029
1516
|
|
|
@@ -1036,15 +1523,24 @@ cycle).
|
|
|
1036
1523
|
|
|
1037
1524
|
### `lyra-tree-node`
|
|
1038
1525
|
|
|
1039
|
-
Normally set internally by `lyra-tree`, but a public element.
|
|
1526
|
+
Normally set internally by `lyra-tree`, but a public element. `role="treeitem"` (plus
|
|
1527
|
+
`aria-expanded`/`aria-level`/`aria-setsize`/`aria-posinset` and the roving `tabindex`, driven by
|
|
1528
|
+
`<lyra-tree>`) live on the *host* element itself, not an internal row `<div>` — so this node's own
|
|
1529
|
+
nested children (rendered in its own shadow root as further `role="group"` content) are genuine DOM
|
|
1530
|
+
descendants of the treeitem, matching the WAI-ARIA treeitem pattern's containment expectation.
|
|
1040
1531
|
|
|
1041
1532
|
**Properties:**
|
|
1042
1533
|
- `item: TreeItem` (required, attribute: false)
|
|
1043
1534
|
- `depth: number = 0`
|
|
1044
1535
|
- `expanded: boolean = false` (reflected)
|
|
1045
1536
|
|
|
1046
|
-
**
|
|
1047
|
-
|
|
1537
|
+
**Methods:** `expand()`, `collapse()` (each a no-op if already in that state, or a leaf), `select()`
|
|
1538
|
+
(fires `lyra-node-select`).
|
|
1539
|
+
|
|
1540
|
+
**Events:** `lyra-node-toggle` (`detail: { id, expanded }`, fired by `expand()`/`collapse()` — via
|
|
1541
|
+
the toggle button or ArrowRight/ArrowLeft), `lyra-node-select` (`detail: { id }`, fired by `select()`
|
|
1542
|
+
— via clicking the label or Enter/Space) — dispatched from `lyra-tree-node`, bubble/compose up
|
|
1543
|
+
through `lyra-tree`'s light DOM.
|
|
1048
1544
|
|
|
1049
1545
|
**Slots:** none.
|
|
1050
1546
|
|
|
@@ -1065,20 +1561,17 @@ indentation), plus the shared tokens listed above.
|
|
|
1065
1561
|
```
|
|
1066
1562
|
|
|
1067
1563
|
**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`).
|
|
1564
|
+
- all four previously-known ARIA gaps in this pair are fixed: the treeitem row is now genuinely
|
|
1565
|
+
keyboard-operable with a roving tabindex and full arrow-key navigation (not just the expand/collapse
|
|
1566
|
+
button); the expanded-children `role="group"` is now a real DOM descendant of its `role="treeitem"`
|
|
1567
|
+
host rather than a shadow-DOM sibling; by-id reconciliation (preserving `expanded` state across
|
|
1568
|
+
data reassignment) now applies at every depth via a keyed `repeat()`, not just depth 0; and
|
|
1569
|
+
`role="tree"` now has an accessible name via the new `label` property.
|
|
1570
|
+
- `lyra-tree`'s `getUpdateComplete()` cascades into every currently-known descendant
|
|
1571
|
+
`<lyra-tree-node>`'s own `updateComplete` (see `update-cascade.ts`) so that code awaiting the
|
|
1572
|
+
tree's `updateComplete` (e.g. after `focusNode()`) doesn't run before an arbitrarily-nested node has
|
|
1573
|
+
actually finished rendering its pushed-down `activeId`/`tabIndex` — one more pending update per
|
|
1574
|
+
depth level, otherwise.
|
|
1082
1575
|
|
|
1083
1576
|
---
|
|
1084
1577
|
|
|
@@ -1091,9 +1584,9 @@ Chart.js wrapper every other `lyra-*-chart` tag subclasses; supports both a simp
|
|
|
1091
1584
|
`wa-chart` `config` property).
|
|
1092
1585
|
|
|
1093
1586
|
**Properties:**
|
|
1094
|
-
- `type: LyraChartType = 'line'` — `LyraChartType = 'line' | 'bar' | 'scatter'
|
|
1095
|
-
|
|
1096
|
-
|
|
1587
|
+
- `type: LyraChartType = 'line'` — `LyraChartType = 'line' | 'bar' | 'scatter' | 'pie' | 'doughnut' |
|
|
1588
|
+
'radar' | 'polarArea' | 'bubble'` — every type string the typed subclasses below lock `type` to is
|
|
1589
|
+
already a first-class member, so `<lyra-chart type="pie">` needs no subclass or cast to work
|
|
1097
1590
|
- `labels: string[] = []` (attribute: false)
|
|
1098
1591
|
- `datasets: Series[] = []` (attribute: false) — `Series { label: string; data?: (number|null)[];
|
|
1099
1592
|
points?: {x,y,label?}[]; color?: string|string[]; fill?: boolean; width?: number; dash?: boolean;
|
|
@@ -1107,13 +1600,23 @@ Chart.js wrapper every other `lyra-*-chart` tag subclasses; supports both a simp
|
|
|
1107
1600
|
- `yLabel: string = ''` (attribute `y-label`)
|
|
1108
1601
|
- `y2Label: string = ''` (attribute `y2-label`)
|
|
1109
1602
|
- `beginAtZero: boolean = true` (attribute `begin-at-zero`)
|
|
1603
|
+
- `horizontal: boolean = false` — sets `options.indexAxis = 'y'`, Chart.js's own mechanism for
|
|
1604
|
+
horizontal bars (also flips `line`/`area` types onto a horizontal category axis)
|
|
1605
|
+
- `stacked: boolean = false` — stacks the `x`/`y`(/`y2`) scale entries `buildScales()` returns; only
|
|
1606
|
+
meaningful for `bar`/`line` types (scatter/bubble's linear `x` scale and the radial `r` scale used
|
|
1607
|
+
by radar/polar-area are out of scope)
|
|
1110
1608
|
- `config?: Partial<ChartConfiguration>` (attribute: false) — deep-merged over the generated
|
|
1111
1609
|
config; any nested key wins without clobbering sibling generated keys
|
|
1112
1610
|
|
|
1113
|
-
**Methods:** `resetZoom()`
|
|
1611
|
+
**Methods:** `resetZoom()` (reset any active zoom/pan to the original view), `refreshTheme()`
|
|
1612
|
+
(forces a redraw so the `--lyra-chart-*` tokens below are re-read from the current computed style —
|
|
1613
|
+
the escape hatch for a consumer's own theme-toggle handler to call when it flips something, e.g. a
|
|
1614
|
+
`data-theme` attribute, that doesn't otherwise change any `lyra-chart` property)
|
|
1114
1615
|
|
|
1115
1616
|
**Events:** `lyra-zoom` (`detail: { zoomed: boolean }`, fired on zoom-complete and on
|
|
1116
|
-
`resetZoom()`)
|
|
1617
|
+
`resetZoom()`), `lyra-point-click` (fired when a click lands on, or nearest to — intersect-only —
|
|
1618
|
+
a data point/segment, for any chart type, not just bar; `detail: { datasetIndex: number, index:
|
|
1619
|
+
number, label: string | undefined, value: unknown }`)
|
|
1117
1620
|
|
|
1118
1621
|
**Slots:** none.
|
|
1119
1622
|
|
|
@@ -1121,8 +1624,13 @@ Chart.js wrapper every other `lyra-*-chart` tag subclasses; supports both a simp
|
|
|
1121
1624
|
|
|
1122
1625
|
**Themeable custom properties:** `--lyra-chart-height` (set programmatically on the host from the
|
|
1123
1626
|
`height` property — must be read from the host, not a shadow-tree descendant, since custom
|
|
1124
|
-
properties only cascade downward);
|
|
1125
|
-
`--lyra-color-
|
|
1627
|
+
properties only cascade downward); `--lyra-chart-grid-color` (default `var(--lyra-color-border)`),
|
|
1628
|
+
`--lyra-chart-tick-color` (default `var(--lyra-color-text-quiet)`), `--lyra-chart-legend-color`
|
|
1629
|
+
(default `var(--lyra-color-text)`), `--lyra-chart-tooltip-bg` (default `var(--lyra-color-surface)`),
|
|
1630
|
+
`--lyra-chart-tooltip-text` (default `var(--lyra-color-text)`) — each resolved fresh via
|
|
1631
|
+
`getComputedStyle` on every draw (Chart.js renders to canvas, not the DOM, so it can't consume CSS
|
|
1632
|
+
`var()` directly), driving the grid lines, tick labels, legend text, and tooltip background/text
|
|
1633
|
+
respectively; plus shared `--lyra-space-xs`.
|
|
1126
1634
|
|
|
1127
1635
|
**Optional peer deps:** `chart.js`, `chartjs-plugin-zoom` (lazy-loaded once per page via
|
|
1128
1636
|
`chart-loader.ts`; registers only the tree-shaken controller/element/scale subset used).
|
|
@@ -1137,28 +1645,105 @@ properties only cascade downward); shared tokens `--lyra-space-xs`, `--lyra-colo
|
|
|
1137
1645
|
```
|
|
1138
1646
|
|
|
1139
1647
|
**Known gotchas:**
|
|
1140
|
-
-
|
|
1141
|
-
`<lyra-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
-
|
|
1150
|
-
`lyra-polar-area-chart
|
|
1151
|
-
`
|
|
1152
|
-
|
|
1648
|
+
- `type` at the `lyra-chart` (core) level is completely unchecked — `buildConfig()` passes it
|
|
1649
|
+
straight to Chart.js as-is, so `<lyra-chart type="pie">` already works today with no subclass
|
|
1650
|
+
needed. Each typed subclass below locks its *own* `type` via a real prototype accessor (see the
|
|
1651
|
+
typed-subclasses section) — a genuine runtime lock, not just a compile-time default.
|
|
1652
|
+
- no global theme-broadcast event exists to auto-retheme an already-drawn chart on a theme switch —
|
|
1653
|
+
a consumer flipping something upstream (e.g. a `data-theme` attribute) that doesn't otherwise
|
|
1654
|
+
change any `lyra-chart` property must call `refreshTheme()` itself to make Chart.js re-read the
|
|
1655
|
+
`--lyra-chart-*` tokens above.
|
|
1656
|
+
- generated `scales` are type-appropriate: no scale at all for `lyra-pie-chart`/
|
|
1657
|
+
`lyra-doughnut-chart`, and a single radial `r` scale (respecting `beginAtZero`) for
|
|
1658
|
+
`lyra-radar-chart`/`lyra-polar-area-chart`, instead of always generating the cartesian `x`/`y`/`y2`
|
|
1659
|
+
block. `xLabel`/`yLabel`/`y2Label` are still silently inert for all four of those subclasses (a
|
|
1660
|
+
radial scale and "no scale" both have nowhere to put an axis title) — reach a titled radial scale
|
|
1661
|
+
only via raw `config`.
|
|
1153
1662
|
- No `chartjs-plugin-annotation` is registered by default — reachable only by importing it
|
|
1154
1663
|
separately and using the raw `config` passthrough (Chart.js's registry is a global singleton).
|
|
1664
|
+
- while the `chart.js` peer is resolving, the host shows a `<lyra-skeleton variant="rect">` with
|
|
1665
|
+
`aria-busy="true"` (same lazy-load pattern as `lyra-graph`/`lyra-map`/`lyra-flag`); Chart.js's own
|
|
1666
|
+
~1s draw-in animation is skipped under `prefers-reduced-motion: reduce`. The raw `config`
|
|
1667
|
+
passthrough is deep-merged with `__proto__`/`constructor`/`prototype` keys skipped unconditionally,
|
|
1668
|
+
so a JSON-sourced `config` (e.g. parsed from an API response) can't reach up and pollute
|
|
1669
|
+
`Object.prototype` through the merge.
|
|
1670
|
+
|
|
1671
|
+
---
|
|
1672
|
+
|
|
1673
|
+
## `lyra-lite-chart`
|
|
1674
|
+
|
|
1675
|
+
A dependency-free bar/line chart — plain SVG/DOM rendering, zero peer dependencies (unlike
|
|
1676
|
+
`lyra-chart`, which wraps `chart.js`). For a project whose architecture forbids a charting
|
|
1677
|
+
dependency outright: covers grouped/stacked bars, multi-series lines, per-point click, and hover
|
|
1678
|
+
tooltips (native SVG `<title>`, no positioning JS) — not a full `lyra-chart` replacement (no
|
|
1679
|
+
zoom/pan, no pie/doughnut/radar/scatter/bubble types, no horizontal/dual-y-axis, no raw-config
|
|
1680
|
+
passthrough). Not a subclass of `LyraChart`.
|
|
1681
|
+
|
|
1682
|
+
**Properties:**
|
|
1683
|
+
- `type: LyraLiteChartType = 'bar'` — `'bar' | 'line'`
|
|
1684
|
+
- `labels: string[] = []` (attribute: false)
|
|
1685
|
+
- `datasets: LiteSeries[] = []` (attribute: false) — `LiteSeries { label: string; data:
|
|
1686
|
+
(number|null)[]; color?: string }`
|
|
1687
|
+
- `legend: boolean = false`
|
|
1688
|
+
- `height: string = '280px'`
|
|
1689
|
+
- `xLabel: string = ''` (attribute `x-label`)
|
|
1690
|
+
- `yLabel: string = ''` (attribute `y-label`)
|
|
1691
|
+
- `beginAtZero: boolean = true` (attribute `begin-at-zero`)
|
|
1692
|
+
- `stacked: boolean = false` — sums each category's bars into one segmented bar instead of grouping
|
|
1693
|
+
them side by side; ignored for `type="line"`
|
|
1694
|
+
|
|
1695
|
+
**Events:** `lyra-point-click` — fired when a bar/point is activated (click, or Enter/Space while
|
|
1696
|
+
focused). `detail: { datasetIndex: number, index: number, label: string | undefined, value: number
|
|
1697
|
+
| null }` — same shape as `lyra-chart`'s `lyra-point-click`.
|
|
1698
|
+
|
|
1699
|
+
**Slots:** none.
|
|
1700
|
+
|
|
1701
|
+
**CSS parts:** `base`, `grid-line`, `axis-label`, `axis-title`, `bar`, `line`, `point`, `legend`,
|
|
1702
|
+
`legend-item`, `legend-swatch`.
|
|
1703
|
+
|
|
1704
|
+
**Themeable custom properties:** `--lyra-chart-height` (same host-level property as `lyra-chart`);
|
|
1705
|
+
`--lyra-chart-grid-color`, `--lyra-chart-tick-color`, `--lyra-chart-legend-color` — same token
|
|
1706
|
+
*names* as `lyra-chart`, so a host already theming `lyra-chart` themes this for free. Unlike
|
|
1707
|
+
`lyra-chart` (canvas-rendered, needs `getComputedStyle`-based re-theming on every draw), this is
|
|
1708
|
+
plain SVG/DOM and reads these via native CSS `var()` — no JS-side resolution step, and no
|
|
1709
|
+
`refreshTheme()` method needed (there's nothing to go stale).
|
|
1710
|
+
|
|
1711
|
+
**Optional peer deps:** none. This is the point of the component.
|
|
1712
|
+
|
|
1713
|
+
```html
|
|
1714
|
+
<lyra-lite-chart type="bar" stacked legend x-label="Week" y-label="Commits"></lyra-lite-chart>
|
|
1715
|
+
<script>
|
|
1716
|
+
const c = document.querySelector('lyra-lite-chart');
|
|
1717
|
+
c.labels = ['W1', 'W2', 'W3', 'W4'];
|
|
1718
|
+
c.datasets = [
|
|
1719
|
+
{ label: 'Docs', data: [4, 6, 3, 8] },
|
|
1720
|
+
{ label: 'Bugs', data: [3, 2, 5, 4] },
|
|
1721
|
+
];
|
|
1722
|
+
</script>
|
|
1723
|
+
```
|
|
1724
|
+
|
|
1725
|
+
**Known gotchas:**
|
|
1726
|
+
- No `horizontal` mode (unlike `lyra-chart`) — deliberately cut from scope, not a stub: bars are
|
|
1727
|
+
always vertical.
|
|
1728
|
+
- No dual y-axis (`Series.axis: 'y2'`) — every series shares one y-axis/domain.
|
|
1729
|
+
- Series colors default to a fixed built-in 8-color categorical palette (round-robin by dataset
|
|
1730
|
+
index) when `color` is unset — not configurable beyond passing `color` per series.
|
|
1731
|
+
- Bar/point elements are real focusable DOM nodes (`tabindex="0" role="button"`, each with its own
|
|
1732
|
+
`aria-label`), so the `<svg>` itself uses `role="group"`, not `role="img"` — an "img" role would
|
|
1733
|
+
conflict with genuinely interactive descendants (axe's `nested-interactive` rule).
|
|
1734
|
+
- Tick values use a standard "nice numbers" (1/2/5 × 10ⁿ) rounding step, not exact data min/max —
|
|
1735
|
+
intentional (readable axis labels), matches how most charting libraries pick tick steps.
|
|
1155
1736
|
|
|
1156
1737
|
---
|
|
1157
1738
|
|
|
1158
1739
|
## 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
1740
|
|
|
1160
|
-
Each is `LyraChart` with `type`
|
|
1161
|
-
|
|
1741
|
+
Each is `LyraChart` with `type` locked to a fixed value via a real `get`/`set` accessor installed on
|
|
1742
|
+
its own prototype (`declare type: '…'` narrows the TS type at compile time; the runtime lock is the
|
|
1743
|
+
`Object.defineProperty` pair alongside it — the same pattern `lyra-histogram`'s own locked `type`
|
|
1744
|
+
below uses), not merely a class-field default a later assignment could still override. All other
|
|
1745
|
+
properties/events/parts/tokens are identical to `lyra-chart` above. Same optional peer deps
|
|
1746
|
+
(`chart.js`, `chartjs-plugin-zoom`).
|
|
1162
1747
|
|
|
1163
1748
|
```html
|
|
1164
1749
|
<lyra-bar-chart legend></lyra-bar-chart>
|
|
@@ -1173,12 +1758,12 @@ are identical to `lyra-chart` above. Same optional peer deps (`chart.js`, `chart
|
|
|
1173
1758
|
```
|
|
1174
1759
|
|
|
1175
1760
|
**Known gotchas (in addition to the core `lyra-chart` list above):**
|
|
1761
|
+
- `type` truly is locked per subclass: `<lyra-pie-chart type="bar">` or `el.type = 'bar'` at runtime
|
|
1762
|
+
is a genuine no-op (the accessor's setter silently ignores the write), not a footgun like a plain
|
|
1763
|
+
overridden class-field default would be.
|
|
1176
1764
|
- `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`).
|
|
1765
|
+
is typed as `{x, y, label?}[]` with no `r` field — cast the array through `as unknown as
|
|
1766
|
+
Series['points']` (or a local `BubblePoint` type) when constructing bubble data.
|
|
1182
1767
|
|
|
1183
1768
|
---
|
|
1184
1769
|
|
|
@@ -1191,8 +1776,11 @@ Bins `values` into `bins` equal-width buckets and renders as a bar chart (extend
|
|
|
1191
1776
|
- `bins: number = 10`
|
|
1192
1777
|
- `values: number[] = []` (attribute: false)
|
|
1193
1778
|
- `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
|
|
1779
|
+
- `labels`/`datasets`/`type` are **derived, read-only** (installed as getter/setter pairs on the
|
|
1780
|
+
prototype; direct writes are silently ignored) — `labels`/`datasets` are computed from
|
|
1781
|
+
`values`/`bins`, and `type` always reads back `'bar'` regardless of any assignment. Unlike the
|
|
1782
|
+
typed `lyra-*-chart` subclasses above (whose "locked" `type` is enforced only by convention),
|
|
1783
|
+
`lyra-histogram`'s lock is a real getter/setter — `el.type = 'line'` is a genuine no-op here.
|
|
1196
1784
|
- All other `LyraChart` properties (`legend`, `area`, `zoom`, `config`, `height`, etc.) are
|
|
1197
1785
|
inherited and usable.
|
|
1198
1786
|
|
|
@@ -1206,10 +1794,8 @@ Bins `values` into `bins` equal-width buckets and renders as a bar chart (extend
|
|
|
1206
1794
|
```
|
|
1207
1795
|
|
|
1208
1796
|
**Known gotchas:**
|
|
1209
|
-
-
|
|
1210
|
-
`
|
|
1211
|
-
`RangeError` even earlier (`Array.from({length: -1})`). No guard exists anywhere in the chain —
|
|
1212
|
-
**always pass `bins >= 1`.**
|
|
1797
|
+
- `bins <= 0` no longer crashes: `binValues()` now returns an empty bucket array for `binCount <= 0`
|
|
1798
|
+
(or empty `values`), so the histogram just renders with no bars instead of throwing.
|
|
1213
1799
|
|
|
1214
1800
|
---
|
|
1215
1801
|
|
|
@@ -1273,7 +1859,16 @@ raw `map` escape hatch for anything unexposed.
|
|
|
1273
1859
|
(discrete swatch rows only, no continuous gradient bar)
|
|
1274
1860
|
- `choropleth?: ChoroplethLayer` (attribute: false) — `ChoroplethLayer { sourceId: string; geojson:
|
|
1275
1861
|
GeoJSON.FeatureCollection; field: string; stops: [number, string][] }` (interpolated
|
|
1276
|
-
fill-color expression from `field`'s value against `stops`
|
|
1862
|
+
fill-color expression from `field`'s value against `stops`; `stops` must contain at least one
|
|
1863
|
+
`[value, color]` pair — an empty array is ignored, leaving whatever fill layer already exists, if
|
|
1864
|
+
any, untouched, rather than being applied)
|
|
1865
|
+
- `markers: MapMarker[] = []` (attribute: false) — `MapMarker { id?: string; lngLat: [number,
|
|
1866
|
+
number]; color?: string; label?: string; html?: string }`; reconciled by `id` (falling back to a
|
|
1867
|
+
`lng,lat` key when `id` is omitted) so an unchanged marker isn't torn down and recreated on every
|
|
1868
|
+
`markers` reassignment — its `lngLat` **and** its popup content (`html`/`label`, in that
|
|
1869
|
+
precedence) are both updated in place, and the popup is removed if a later update sets neither.
|
|
1870
|
+
`html` is rendered via `Popup.setHTML()` — **raw markup, inline event handlers included** — only
|
|
1871
|
+
pass trusted content, sanitize anything derived from user input first.
|
|
1277
1872
|
|
|
1278
1873
|
**Getters:** `map` → the raw `maplibregl.Map` instance.
|
|
1279
1874
|
|
|
@@ -1303,26 +1898,32 @@ exists and was hit)
|
|
|
1303
1898
|
stops: [[0, '#cde2fb'], [100, '#0969da']],
|
|
1304
1899
|
};
|
|
1305
1900
|
m.legend = [{ color: '#cde2fb', label: 'Low' }, { color: '#0969da', label: 'High' }];
|
|
1901
|
+
m.markers = [{ lngLat: [2.29, 48.86], label: 'Eiffel Tower' }];
|
|
1306
1902
|
m.addEventListener('lyra-map-click', (e) => console.log(e.detail.feature?.properties));
|
|
1307
1903
|
</script>
|
|
1308
1904
|
```
|
|
1309
1905
|
|
|
1310
1906
|
**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.
|
|
1907
|
+
- clearing or swapping the choropleth no longer leaks the old layer: setting `choropleth =
|
|
1908
|
+
undefined`, or changing `choropleth.sourceId` to a different value, now calls `removeLayer`/
|
|
1909
|
+
`removeSource` on whatever was previously applied before adding the new one (or nothing, if
|
|
1910
|
+
cleared).
|
|
1911
|
+
- `mapStyle` changes after construction now call `setStyle()` (in addition to `center`/`zoom`
|
|
1912
|
+
already calling `setCenter`/`setZoom`) — the choropleth is automatically re-applied once the new
|
|
1913
|
+
style's own `'style.load'` fires, since a style change wipes every layer/source maplibre-gl knows
|
|
1914
|
+
about.
|
|
1915
|
+
- Point markers now have a declarative API (`markers`, above) with popup support — the `.map` escape
|
|
1916
|
+
hatch and manual `new maplibregl.Marker()` are no longer the only way to place pins.
|
|
1324
1917
|
- No click-select visual highlight on choropleth features (no `setFeatureState` call) — clicking
|
|
1325
|
-
only fires the event, no built-in visual feedback.
|
|
1918
|
+
only fires the event, no built-in visual feedback. Popups are still only reachable declaratively
|
|
1919
|
+
through `markers`' `html`/`label` — a choropleth-feature click still has no built-in popup, only the
|
|
1920
|
+
raw `lyra-map-click` event.
|
|
1921
|
+
- `LegendEntry.color` is validated against a strict CSS-color-syntax allowlist before being applied
|
|
1922
|
+
to the legend swatch's `background`, rejecting anything that isn't recognizable color syntax
|
|
1923
|
+
(notably `url(...)`, which `background` also accepts and would otherwise fetch as soon as the
|
|
1924
|
+
swatch renders).
|
|
1925
|
+
- while the `maplibre-gl` peer is resolving, the host shows a `<lyra-skeleton variant="rect">` with
|
|
1926
|
+
`aria-busy="true"` in place of the map container.
|
|
1326
1927
|
|
|
1327
1928
|
---
|
|
1328
1929
|
|
|
@@ -1334,21 +1935,29 @@ parsing (that's left entirely to the host).
|
|
|
1334
1935
|
**Properties:**
|
|
1335
1936
|
- `multiple: boolean = false` (reflected)
|
|
1336
1937
|
- `disabled: boolean = false` (reflected)
|
|
1337
|
-
- `accept: string = ''` —
|
|
1938
|
+
- `accept: string = ''` — a native-`accept`-style string (`.csv,.xlsx`, `text/csv`, `image/*`, or any
|
|
1939
|
+
comma-separated mix); now enforced on **both** the native picker dialog and the drag-drop path, see
|
|
1940
|
+
gotchas
|
|
1338
1941
|
- `allowedMimeTypes: string[] = []` (attribute: false) — exact MIME-string allowlist
|
|
1339
1942
|
- `forbiddenMimeTypes: string[] = []` (attribute: false) — exact MIME-string denylist, checked
|
|
1340
1943
|
**before** (and takes precedence over) `allowedMimeTypes`
|
|
1944
|
+
- `maxFileSize: number = 0` (attribute `max-file-size` — bytes; `0` disables the check)
|
|
1341
1945
|
- `label: string = 'Drop files here or click to browse'`
|
|
1342
1946
|
|
|
1343
1947
|
**Methods:** `openPicker()` — programmatically opens the native file dialog.
|
|
1344
1948
|
|
|
1345
|
-
**Events:** `lyra-files` (`detail: { files: File[], rejected:
|
|
1346
|
-
manual file-picker selection — `
|
|
1949
|
+
**Events:** `lyra-files` (`detail: { files: File[], rejected: RejectedFile[] }`, fired on both drop
|
|
1950
|
+
and manual file-picker selection) — `RejectedFile = { file: File; reason: 'type' | 'count' | 'size'
|
|
1951
|
+
}`: `'type'` from `accept`/`allowedMimeTypes`/`forbiddenMimeTypes`, `'count'` when a single-file
|
|
1952
|
+
input (`multiple` unset) receives more than one file (in which case *all* files are rejected, none
|
|
1953
|
+
accepted), `'size'` from `maxFileSize`.
|
|
1347
1954
|
|
|
1348
1955
|
**Slots:** default slot — custom dropzone content, overrides the `label` attribute text when
|
|
1349
|
-
provided.
|
|
1956
|
+
provided. The accessible name always comes from `label` regardless, so icon-only slot content still
|
|
1957
|
+
announces correctly.
|
|
1350
1958
|
|
|
1351
|
-
**CSS parts:** `base`, `input`
|
|
1959
|
+
**CSS parts:** `base`, `input`, `status` (a visually-hidden `role="status" aria-live="polite"`
|
|
1960
|
+
element carrying the drag accept/reject announcement)
|
|
1352
1961
|
|
|
1353
1962
|
**Themeable custom properties:** shared tokens only — `--lyra-space-xs`, `--lyra-space-l`,
|
|
1354
1963
|
`--lyra-color-border`, `--lyra-radius`, `--lyra-color-surface`, `--lyra-color-text-quiet`,
|
|
@@ -1360,7 +1969,7 @@ provided.
|
|
|
1360
1969
|
<lyra-file-input multiple accept=".csv,.xlsx" allowed-mime-types='["text/csv"]'></lyra-file-input>
|
|
1361
1970
|
<script>
|
|
1362
1971
|
document.querySelector('lyra-file-input').addEventListener('lyra-files', (e) => {
|
|
1363
|
-
console.log('accepted:', e.detail.files, 'rejected:', e.detail.rejected);
|
|
1972
|
+
console.log('accepted:', e.detail.files, 'rejected:', e.detail.rejected); // rejected[i].reason
|
|
1364
1973
|
});
|
|
1365
1974
|
</script>
|
|
1366
1975
|
```
|
|
@@ -1369,25 +1978,24 @@ Note: `allowedMimeTypes`/`forbiddenMimeTypes` are complex properties (`attribute
|
|
|
1369
1978
|
them via JS (`el.allowedMimeTypes = [...]`), not as a JSON string attribute; the snippet above is
|
|
1370
1979
|
illustrative of intent only.
|
|
1371
1980
|
|
|
1981
|
+
`accept.ts` exports `matchesAccept(file, accept, assumeExtensionMatch?)` (internal — not
|
|
1982
|
+
re-exported from the package root) — parses the same three `accept` forms the browser's native
|
|
1983
|
+
picker accepts (extension, exact MIME, `type/*` wildcard) and reports whether a `File` matches. Used
|
|
1984
|
+
both for the drop path's real rejection and for the dragenter preview, where a `DataTransferItem` (no
|
|
1985
|
+
`.name`, only `.type`) can't evaluate an extension pattern yet — `assumeExtensionMatch: true` treats
|
|
1986
|
+
that as a possible match during preview so the drag-over UI doesn't flash a false "reject" state for
|
|
1987
|
+
an extension-only `accept` list.
|
|
1988
|
+
|
|
1372
1989
|
**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
1990
|
- 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
1991
|
- Dragged folders aren't specially detected (no `webkitGetAsEntry()` check) — likely surfaces as a
|
|
1390
1992
|
phantom zero-byte `File` with an empty MIME type rather than a clear rejection.
|
|
1993
|
+
- `maxFileSize`/`accept` extension patterns can't be evaluated during the dragenter preview (no real
|
|
1994
|
+
`File.size`/`.name` available yet from a `DataTransferItem`) — the live preview state (border/
|
|
1995
|
+
background color, `status` announcement) is therefore only a best-effort hint; the authoritative
|
|
1996
|
+
accept/reject decision (and `rejected[].reason`) is always the one made at actual drop time.
|
|
1997
|
+
- the click/keyboard-to-browse path is correctly operable (`role="button"`, `tabindex` 0/-1 by
|
|
1998
|
+
disabled, `aria-disabled`, Enter/Space handling).
|
|
1391
1999
|
|
|
1392
2000
|
---
|
|
1393
2001
|
|
|
@@ -1395,7 +2003,15 @@ illustrative of intent only.
|
|
|
1395
2003
|
|
|
1396
2004
|
These apply across the whole library rather than to any one component:
|
|
1397
2005
|
|
|
1398
|
-
- **Packaging
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
2006
|
+
- **Packaging.** `custom-elements.json` is now in `package.json`'s `files` allowlist and
|
|
2007
|
+
regenerated by a `prepack` script (`pnpm run manifest`) before publish, so a manifest-driven IDE/
|
|
2008
|
+
tooling integration (e.g. custom-elements-manifest-based autocomplete) does see an up-to-date copy
|
|
2009
|
+
in the published tarball.
|
|
2010
|
+
- **Lazy-loaded optional peers share one pattern.** `lyra-chart`/`lyra-histogram`/`lyra-box-plot`,
|
|
2011
|
+
`lyra-graph`, `lyra-map`, and `lyra-flag` each show a `<lyra-skeleton>` placeholder (with
|
|
2012
|
+
`aria-busy="true"` on the host) while their respective optional peer dependency resolves, then
|
|
2013
|
+
settle into their normal render — or, if the peer genuinely isn't installed, an empty/fallback
|
|
2014
|
+
render plus a one-time `console.warn` (deduped module-wide, not per-instance).
|
|
2015
|
+
- **RTL.** Components generally rely on CSS logical properties to flip layout for free; `lyra-split`
|
|
2016
|
+
and `lyra-time-range` additionally consult `internal/rtl.ts`'s `isRtl()` to mirror the physical
|
|
2017
|
+
drag-direction/arrow-key math that logical properties alone don't cover.
|