@aceshooting/lyra-ui 1.0.1 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -10
- package/custom-elements.json +15118 -8342
- package/dist/components/chart/chart.d.ts +57 -1
- package/dist/components/chart/chart.js +202 -12
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +83 -0
- package/dist/components/chart/lite-chart.js +424 -0
- package/dist/components/chart/lite-chart.stories.d.ts +14 -0
- package/dist/components/chart/lite-chart.stories.js +114 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +76 -13
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +69 -3
- package/dist/components/flag/flag.js +107 -30
- package/dist/components/flag/flag.stories.d.ts +1 -0
- package/dist/components/flag/flag.stories.js +14 -0
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +149 -12
- package/dist/components/heatmap/heatmap.js +445 -28
- package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
- package/dist/components/heatmap/heatmap.stories.js +155 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +125 -0
- package/dist/components/select/select.js +480 -0
- package/dist/components/select/select.stories.d.ts +21 -0
- package/dist/components/select/select.stories.js +91 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +212 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +43 -1
- package/dist/components/stat/stat.js +97 -4
- package/dist/components/stat/stat.stories.d.ts +4 -0
- package/dist/components/stat/stat.stories.js +66 -1
- package/dist/components/stat/stat.styles.js +64 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +25 -1
- package/dist/components/widget/widget.js +164 -6
- package/dist/components/widget/widget.stories.d.ts +3 -0
- package/dist/components/widget/widget.stories.js +43 -0
- package/dist/components/widget/widget.styles.js +20 -3
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +55 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +962 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
|
@@ -9,31 +9,50 @@ import { property, state } from 'lit/decorators.js';
|
|
|
9
9
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
10
|
import { defineElement } from '../../internal/prefix.js';
|
|
11
11
|
import { styles } from './flag.styles.js';
|
|
12
|
-
import { languageToCountry } from './language-map.js';
|
|
12
|
+
import { ALPHA2_RE, languageToCountry } from './language-map.js';
|
|
13
13
|
import '../skeleton/skeleton.js';
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* doesn't match this shape is treated the same as an unknown/missing flag.
|
|
15
|
+
* Resolves the optional peer dependency `@aceshooting/lyra-flags`'s `flagUrl`
|
|
16
|
+
* via the given importer (a real dynamic import by default). Uncached and
|
|
17
|
+
* dependency-injectable — unlike `loadFlagUrlResolver()` below — so the
|
|
18
|
+
* caught-error warning path is directly testable without needing to
|
|
19
|
+
* actually uninstall the package.
|
|
21
20
|
*/
|
|
22
|
-
|
|
21
|
+
export async function loadFlagUrl(importFlags = () => import('@aceshooting/lyra-flags')) {
|
|
22
|
+
try {
|
|
23
|
+
return (await importFlags()).flagUrl;
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
console.warn("<lyra-flag> needs the optional peer dependency '@aceshooting/lyra-flags' to render " +
|
|
27
|
+
'flag images — install it with `pnpm add @aceshooting/lyra-flags`.');
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Resolves an ISO 3166-1 alpha-2 region code to a human-readable, localized
|
|
33
|
+
* display name (e.g. `'FR'` -> `'France'`) via `Intl.DisplayNames`, for use as
|
|
34
|
+
* the default accessible name (`alt`) instead of a bare code read
|
|
35
|
+
* letter-by-letter by most screen readers. Falls back to the uppercase code
|
|
36
|
+
* itself if `Intl.DisplayNames` throws (unrecognized region) or isn't
|
|
37
|
+
* available in the current runtime.
|
|
38
|
+
*/
|
|
39
|
+
function displayNameFor(code) {
|
|
40
|
+
try {
|
|
41
|
+
return new Intl.DisplayNames([navigator.language], { type: 'region' }).of(code.toUpperCase()) ?? code.toUpperCase();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return code.toUpperCase();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
23
47
|
let flagUrlResolver;
|
|
24
48
|
/**
|
|
25
49
|
* Lazily loads the optional peer dependency '@aceshooting/lyra-flags' once per
|
|
26
|
-
* page. Resolves to `null` (with a one-time warning) if
|
|
50
|
+
* page. Resolves to `null` (with a one-time warning, see `loadFlagUrl()`) if
|
|
51
|
+
* it isn't installed.
|
|
27
52
|
*/
|
|
28
53
|
function loadFlagUrlResolver() {
|
|
29
54
|
if (!flagUrlResolver) {
|
|
30
|
-
flagUrlResolver =
|
|
31
|
-
.then((mod) => mod.flagUrl)
|
|
32
|
-
.catch(() => {
|
|
33
|
-
console.warn("<lyra-flag> needs the optional peer dependency '@aceshooting/lyra-flags' to render " +
|
|
34
|
-
'flag images — install it with `pnpm add @aceshooting/lyra-flags`.');
|
|
35
|
-
return null;
|
|
36
|
-
});
|
|
55
|
+
flagUrlResolver = loadFlagUrl();
|
|
37
56
|
}
|
|
38
57
|
return flagUrlResolver;
|
|
39
58
|
}
|
|
@@ -43,11 +62,29 @@ function loadFlagUrlResolver() {
|
|
|
43
62
|
* Flag images are shipped by the optional peer package `@aceshooting/lyra-flags`,
|
|
44
63
|
* not bundled into lyra-ui itself, so importing the core library pulls zero flag
|
|
45
64
|
* weight. Give it a `country` (ISO 3166-1 alpha-2) or a `language` tag (mapped to
|
|
46
|
-
* a representative country).
|
|
65
|
+
* a representative country). While that peer package's `flagUrl()` resolves (or
|
|
66
|
+
* if it isn't installed), the host carries `aria-busy="true"` and a skeleton
|
|
67
|
+
* placeholder renders in its place.
|
|
68
|
+
*
|
|
69
|
+
* **Bundle-size note:** `country`/`language` resolve through the peer package's
|
|
70
|
+
* `flagUrl(code)`, which is itself genuinely code-split per flag (see that
|
|
71
|
+
* package's docs) — using `<lyra-flag country="fr">` anywhere in an app ships
|
|
72
|
+
* only the flags actually requested at runtime, not all 249. If you already
|
|
73
|
+
* have a flag's URL at build time (e.g. from your own literal
|
|
74
|
+
* `import frUrl from '@aceshooting/lyra-flags/flags/fr.svg?url'`), pass it as
|
|
75
|
+
* `src` instead to skip the peer-package round trip (and its loading-skeleton
|
|
76
|
+
* flash) entirely.
|
|
77
|
+
*
|
|
78
|
+
* A handful of flags (e.g. `es`, `pt` — any whose design includes a detailed coat of
|
|
79
|
+
* arms/seal/emblem) ship a second, full-detail source SVG alongside the default icon-optimized
|
|
80
|
+
* one; set `detailed` to request it (e.g. for a hero-scale display where the extra illustrative
|
|
81
|
+
* detail is actually visible). A no-op for every other code — see `detailed`'s own doc.
|
|
47
82
|
*
|
|
48
83
|
* @customElement lyra-flag
|
|
49
84
|
* @example <lyra-flag country="fr"></lyra-flag>
|
|
50
85
|
* @example <lyra-flag language="en" label="English"></lyra-flag>
|
|
86
|
+
* @example <lyra-flag src=${frUrl} label="French"></lyra-flag>
|
|
87
|
+
* @example <lyra-flag country="es" detailed></lyra-flag>
|
|
51
88
|
* @csspart image - The underlying <img>.
|
|
52
89
|
*/
|
|
53
90
|
export class LyraFlag extends LyraElement {
|
|
@@ -55,13 +92,30 @@ export class LyraFlag extends LyraElement {
|
|
|
55
92
|
super(...arguments);
|
|
56
93
|
/** Render as a circular flag. */
|
|
57
94
|
this.round = false;
|
|
95
|
+
/**
|
|
96
|
+
* Requests the pristine, full-detail source SVG instead of the default icon-optimized one —
|
|
97
|
+
* only meaningful for the minority of `country`/`language` codes whose source art was large
|
|
98
|
+
* enough to need optimizing (e.g. `es`, `pt`, a national coat of arms/seal/emblem); for every
|
|
99
|
+
* other code this is a safe no-op (the default and detailed variants are the same file). Has no
|
|
100
|
+
* effect when `src` is set — a pre-resolved URL is used as-is regardless. Intended for rendering
|
|
101
|
+
* a flag larger than icon scale (e.g. a hero display) where the extra illustrative detail is
|
|
102
|
+
* actually visible.
|
|
103
|
+
*/
|
|
104
|
+
this.detailed = false;
|
|
58
105
|
/** True while the lazy-loaded `@aceshooting/lyra-flags` peer resolver is in flight. */
|
|
59
106
|
this.loading = true;
|
|
107
|
+
/**
|
|
108
|
+
* Bumped on every `willUpdate` pass; captured by each in-flight resolver
|
|
109
|
+
* `.then()` so a resolution for a `country`/`language` that's since changed
|
|
110
|
+
* (or been cleared) can recognize itself as stale and no-op instead of
|
|
111
|
+
* overwriting newer state.
|
|
112
|
+
*/
|
|
113
|
+
this.resolveToken = 0;
|
|
60
114
|
}
|
|
61
115
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
62
116
|
get code() {
|
|
63
117
|
if (this.country) {
|
|
64
|
-
return
|
|
118
|
+
return ALPHA2_RE.test(this.country) ? this.country.toLowerCase() : undefined;
|
|
65
119
|
}
|
|
66
120
|
if (this.language)
|
|
67
121
|
return languageToCountry(this.language);
|
|
@@ -69,22 +123,37 @@ export class LyraFlag extends LyraElement {
|
|
|
69
123
|
}
|
|
70
124
|
willUpdate(changed) {
|
|
71
125
|
// `this.hasUpdated` (not just `changed`) forces this body to run once on
|
|
72
|
-
// the very first update even when
|
|
126
|
+
// the very first update even when none of `country`/`language`/`src` is
|
|
73
127
|
// ever set: an unset optional property's first "assignment" is
|
|
74
|
-
// undefined -> undefined (no-op to Lit), so `changed.has(
|
|
75
|
-
// would otherwise
|
|
76
|
-
// receives
|
|
77
|
-
if (this.hasUpdated &&
|
|
128
|
+
// undefined -> undefined (no-op to Lit), so none of `changed.has(...)`
|
|
129
|
+
// would otherwise ever become true for a `<lyra-flag>` that never
|
|
130
|
+
// receives one, leaving `loading` stuck at its initial `true` forever.
|
|
131
|
+
if (this.hasUpdated &&
|
|
132
|
+
!changed.has('country') &&
|
|
133
|
+
!changed.has('language') &&
|
|
134
|
+
!changed.has('src') &&
|
|
135
|
+
!changed.has('detailed')) {
|
|
78
136
|
return;
|
|
137
|
+
}
|
|
138
|
+
const token = ++this.resolveToken; // invalidates any in-flight peer resolution either way
|
|
139
|
+
if (this.src) {
|
|
140
|
+
this.resolvedSrc = undefined;
|
|
141
|
+
this.loading = false;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
79
144
|
const code = this.code;
|
|
80
145
|
if (!code) {
|
|
81
|
-
this.
|
|
146
|
+
this.resolvedSrc = undefined;
|
|
82
147
|
this.loading = false;
|
|
83
148
|
return;
|
|
84
149
|
}
|
|
85
150
|
this.loading = true;
|
|
86
|
-
void loadFlagUrlResolver()
|
|
87
|
-
|
|
151
|
+
void loadFlagUrlResolver()
|
|
152
|
+
.then((resolve) => resolve?.(code, this.detailed ? { variant: 'detailed' } : undefined))
|
|
153
|
+
.then((url) => {
|
|
154
|
+
if (token !== this.resolveToken)
|
|
155
|
+
return; // superseded by a later country/language/src change
|
|
156
|
+
this.resolvedSrc = url;
|
|
88
157
|
this.loading = false;
|
|
89
158
|
});
|
|
90
159
|
}
|
|
@@ -97,10 +166,12 @@ export class LyraFlag extends LyraElement {
|
|
|
97
166
|
render() {
|
|
98
167
|
if (this.loading)
|
|
99
168
|
return html `<lyra-skeleton variant="rect"></lyra-skeleton>`;
|
|
100
|
-
|
|
169
|
+
const url = this.src ?? this.resolvedSrc;
|
|
170
|
+
if (!url)
|
|
101
171
|
return html ``;
|
|
102
|
-
const
|
|
103
|
-
|
|
172
|
+
const code = this.code;
|
|
173
|
+
const alt = this.label ?? (code ? displayNameFor(code) : '');
|
|
174
|
+
return html `<img part="image" src=${url} alt=${alt} loading="lazy" decoding="async" />`;
|
|
104
175
|
}
|
|
105
176
|
}
|
|
106
177
|
__decorate([
|
|
@@ -109,15 +180,21 @@ __decorate([
|
|
|
109
180
|
__decorate([
|
|
110
181
|
property()
|
|
111
182
|
], LyraFlag.prototype, "language", void 0);
|
|
183
|
+
__decorate([
|
|
184
|
+
property()
|
|
185
|
+
], LyraFlag.prototype, "src", void 0);
|
|
112
186
|
__decorate([
|
|
113
187
|
property()
|
|
114
188
|
], LyraFlag.prototype, "label", void 0);
|
|
115
189
|
__decorate([
|
|
116
190
|
property({ type: Boolean, reflect: true })
|
|
117
191
|
], LyraFlag.prototype, "round", void 0);
|
|
192
|
+
__decorate([
|
|
193
|
+
property({ type: Boolean, reflect: true })
|
|
194
|
+
], LyraFlag.prototype, "detailed", void 0);
|
|
118
195
|
__decorate([
|
|
119
196
|
state()
|
|
120
|
-
], LyraFlag.prototype, "
|
|
197
|
+
], LyraFlag.prototype, "resolvedSrc", void 0);
|
|
121
198
|
__decorate([
|
|
122
199
|
state()
|
|
123
200
|
], LyraFlag.prototype, "loading", void 0);
|
|
@@ -15,3 +15,17 @@ export const Gallery = {
|
|
|
15
15
|
</div>
|
|
16
16
|
`,
|
|
17
17
|
};
|
|
18
|
+
export const DetailedVariant = {
|
|
19
|
+
render: () => html `
|
|
20
|
+
<div style="display:flex; gap:2rem; align-items:center;">
|
|
21
|
+
<div style="display:flex; flex-direction:column; align-items:center; gap:0.5rem;">
|
|
22
|
+
<lyra-flag country="es" label="Spain (default, icon-optimized)" style="height: 6rem"></lyra-flag>
|
|
23
|
+
<span>default</span>
|
|
24
|
+
</div>
|
|
25
|
+
<div style="display:flex; flex-direction:column; align-items:center; gap:0.5rem;">
|
|
26
|
+
<lyra-flag country="es" detailed label="Spain (detailed)" style="height: 6rem"></lyra-flag>
|
|
27
|
+
<span>detailed</span>
|
|
28
|
+
</div>
|
|
29
|
+
</div>
|
|
30
|
+
`,
|
|
31
|
+
};
|
|
@@ -12,7 +12,7 @@ export const styles = css `
|
|
|
12
12
|
block-size: 100%;
|
|
13
13
|
inline-size: 100%;
|
|
14
14
|
border-radius: var(--lyra-flag-radius, 2px);
|
|
15
|
-
box-shadow: 0 0 0 1px
|
|
15
|
+
box-shadow: 0 0 0 1px var(--lyra-color-border) inset;
|
|
16
16
|
object-fit: cover;
|
|
17
17
|
}
|
|
18
18
|
:host([round]) {
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO 3166-1 alpha-2 shape: exactly two ASCII letters, case-insensitive.
|
|
3
|
+
* Shared by `flag.ts` (validating `country`) and `languageToCountry` below
|
|
4
|
+
* (validating a language tag's region subtag), because both values end up
|
|
5
|
+
* passed to the peer package `@aceshooting/lyra-flags`' `flagUrl()`, which
|
|
6
|
+
* naively interpolates its `code` argument into a `new URL('./flags/${code}.svg',
|
|
7
|
+
* ...)` with no validation of its own -- an unvalidated value containing `../`
|
|
8
|
+
* segments can escape the intended flags/ directory. Anything that doesn't
|
|
9
|
+
* match this shape is treated the same as an unknown/missing flag.
|
|
10
|
+
*/
|
|
11
|
+
export declare const ALPHA2_RE: RegExp;
|
|
1
12
|
/**
|
|
2
13
|
* Default mapping from a language subtag to a representative country flag
|
|
3
14
|
* (ISO 3166-1 alpha-2). Languages don't map 1:1 to countries; these are the
|
|
@@ -1,3 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ISO 3166-1 alpha-2 shape: exactly two ASCII letters, case-insensitive.
|
|
3
|
+
* Shared by `flag.ts` (validating `country`) and `languageToCountry` below
|
|
4
|
+
* (validating a language tag's region subtag), because both values end up
|
|
5
|
+
* passed to the peer package `@aceshooting/lyra-flags`' `flagUrl()`, which
|
|
6
|
+
* naively interpolates its `code` argument into a `new URL('./flags/${code}.svg',
|
|
7
|
+
* ...)` with no validation of its own -- an unvalidated value containing `../`
|
|
8
|
+
* segments can escape the intended flags/ directory. Anything that doesn't
|
|
9
|
+
* match this shape is treated the same as an unknown/missing flag.
|
|
10
|
+
*/
|
|
11
|
+
export const ALPHA2_RE = /^[a-z]{2}$/i;
|
|
1
12
|
/**
|
|
2
13
|
* Default mapping from a language subtag to a representative country flag
|
|
3
14
|
* (ISO 3166-1 alpha-2). Languages don't map 1:1 to countries; these are the
|
|
@@ -56,7 +67,7 @@ export function languageToCountry(language) {
|
|
|
56
67
|
const parts = language.toLowerCase().split(/[-_]/);
|
|
57
68
|
const base = parts[0];
|
|
58
69
|
const region = parts[1];
|
|
59
|
-
if (region && region
|
|
70
|
+
if (region && ALPHA2_RE.test(region))
|
|
60
71
|
return region;
|
|
61
72
|
return LANGUAGE_TO_COUNTRY[base];
|
|
62
73
|
}
|
|
@@ -15,6 +15,8 @@ export declare class LyraGauge extends LyraElement {
|
|
|
15
15
|
max: number;
|
|
16
16
|
type: GaugeType;
|
|
17
17
|
label: string;
|
|
18
|
+
/** Imperative override for the displayed/announced value text, e.g. `'72°F'` for a raw `value` of `72`.
|
|
19
|
+
* An empty string is treated the same as unset and falls back to the numeric `value`. */
|
|
18
20
|
valueLabel?: string;
|
|
19
21
|
private get ratio();
|
|
20
22
|
protected willUpdate(): void;
|
|
@@ -57,14 +57,28 @@ export class LyraGauge extends LyraElement {
|
|
|
57
57
|
}
|
|
58
58
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
59
59
|
get ratio() {
|
|
60
|
+
if (isNaN(this.value) || isNaN(this.min) || isNaN(this.max))
|
|
61
|
+
return 0;
|
|
60
62
|
const span = this.max - this.min || 1;
|
|
61
63
|
return Math.min(1, Math.max(0, (this.value - this.min) / span));
|
|
62
64
|
}
|
|
63
65
|
willUpdate() {
|
|
64
66
|
this.setAttribute('role', 'meter');
|
|
65
|
-
this.
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
if (!isNaN(this.value) && !isNaN(this.min) && !isNaN(this.max)) {
|
|
68
|
+
const clamped = Math.min(this.max, Math.max(this.min, this.value));
|
|
69
|
+
this.setAttribute('aria-valuenow', String(clamped));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
this.removeAttribute('aria-valuenow');
|
|
73
|
+
}
|
|
74
|
+
if (isNaN(this.min))
|
|
75
|
+
this.removeAttribute('aria-valuemin');
|
|
76
|
+
else
|
|
77
|
+
this.setAttribute('aria-valuemin', String(this.min));
|
|
78
|
+
if (isNaN(this.max))
|
|
79
|
+
this.removeAttribute('aria-valuemax');
|
|
80
|
+
else
|
|
81
|
+
this.setAttribute('aria-valuemax', String(this.max));
|
|
68
82
|
if (this.label)
|
|
69
83
|
this.setAttribute('aria-label', this.label);
|
|
70
84
|
else
|
|
@@ -75,7 +89,7 @@ export class LyraGauge extends LyraElement {
|
|
|
75
89
|
this.removeAttribute('aria-valuetext');
|
|
76
90
|
}
|
|
77
91
|
renderRadial() {
|
|
78
|
-
const text = this.valueLabel
|
|
92
|
+
const text = this.valueLabel || (isNaN(this.value) ? '' : String(this.value));
|
|
79
93
|
// Dashoffset counts down from the full arc length (nothing revealed) to 0
|
|
80
94
|
// (whole sweep revealed) as ratio goes 0 -> 1 — the classic "draw an SVG
|
|
81
95
|
// path" technique, which transitions smoothly via plain CSS.
|
|
@@ -94,7 +108,7 @@ export class LyraGauge extends LyraElement {
|
|
|
94
108
|
</svg>`;
|
|
95
109
|
}
|
|
96
110
|
renderLinear() {
|
|
97
|
-
const text = this.valueLabel
|
|
111
|
+
const text = this.valueLabel || (isNaN(this.value) ? '' : String(this.value));
|
|
98
112
|
const dashoffset = LINEAR_LENGTH * (1 - this.ratio);
|
|
99
113
|
return html `<svg part="base" viewBox="0 0 100 20" preserveAspectRatio="none">
|
|
100
114
|
<line part="track" x1="0" y1=${LINEAR_BAR_Y} x2="100" y2=${LINEAR_BAR_Y} stroke-width=${LINEAR_STROKE}></line>
|
|
@@ -11,3 +11,9 @@ export const Radial = {
|
|
|
11
11
|
export const Linear = {
|
|
12
12
|
render: () => html `<lyra-gauge type="linear" value="40" max="100" label="Disk"></lyra-gauge>`,
|
|
13
13
|
};
|
|
14
|
+
export const WithValueLabel = {
|
|
15
|
+
render: () => html `<lyra-gauge value="72" max="100" label="Temp" .valueLabel=${'72°F'}></lyra-gauge>`,
|
|
16
|
+
};
|
|
17
|
+
export const NonzeroMin = {
|
|
18
|
+
render: () => html `<lyra-gauge value="18" min="-20" max="40" label="Outdoor Temp"></lyra-gauge>`,
|
|
19
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type * as ForceModule from 'd3-force';
|
|
2
|
+
import type * as DragModule from 'd3-drag';
|
|
3
|
+
import type * as ZoomModule from 'd3-zoom';
|
|
4
|
+
import type * as SelectionModule from 'd3-selection';
|
|
5
|
+
export interface D3Modules {
|
|
6
|
+
forceSimulation: typeof ForceModule.forceSimulation;
|
|
7
|
+
forceLink: typeof ForceModule.forceLink;
|
|
8
|
+
forceManyBody: typeof ForceModule.forceManyBody;
|
|
9
|
+
forceCenter: typeof ForceModule.forceCenter;
|
|
10
|
+
forceCollide: typeof ForceModule.forceCollide;
|
|
11
|
+
drag: typeof DragModule.drag;
|
|
12
|
+
zoom: typeof ZoomModule.zoom;
|
|
13
|
+
select: typeof SelectionModule.select;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Resolves the optional peer dependencies `d3-force`/`d3-drag`/`d3-zoom`/
|
|
17
|
+
* `d3-selection` via the given importers (real dynamic imports by default).
|
|
18
|
+
* Uncached and dependency-injectable so both the success path and the
|
|
19
|
+
* caught-error warning path are directly testable without needing to
|
|
20
|
+
* actually uninstall any of the four packages.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadD3Modules(importForce?: () => Promise<typeof ForceModule>, importDrag?: () => Promise<typeof DragModule>, importZoom?: () => Promise<typeof ZoomModule>, importSelection?: () => Promise<typeof SelectionModule>): Promise<D3Modules | null>;
|
|
23
|
+
/**
|
|
24
|
+
* Lazily loads the d3 peer dependencies (see `loadD3Modules()`) once per
|
|
25
|
+
* page. Resolves to `null` if they aren't installed — mirrors
|
|
26
|
+
* `<lyra-flag>`'s peer-dependency pattern.
|
|
27
|
+
*/
|
|
28
|
+
export declare function loadD3(): Promise<D3Modules | null>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the optional peer dependencies `d3-force`/`d3-drag`/`d3-zoom`/
|
|
3
|
+
* `d3-selection` via the given importers (real dynamic imports by default).
|
|
4
|
+
* Uncached and dependency-injectable so both the success path and the
|
|
5
|
+
* caught-error warning path are directly testable without needing to
|
|
6
|
+
* actually uninstall any of the four packages.
|
|
7
|
+
*/
|
|
8
|
+
export async function loadD3Modules(importForce = () => import('d3-force'), importDrag = () => import('d3-drag'), importZoom = () => import('d3-zoom'), importSelection = () => import('d3-selection')) {
|
|
9
|
+
try {
|
|
10
|
+
const [force, dragMod, zoomMod, selectionMod] = await Promise.all([
|
|
11
|
+
importForce(),
|
|
12
|
+
importDrag(),
|
|
13
|
+
importZoom(),
|
|
14
|
+
importSelection(),
|
|
15
|
+
]);
|
|
16
|
+
return {
|
|
17
|
+
forceSimulation: force.forceSimulation,
|
|
18
|
+
forceLink: force.forceLink,
|
|
19
|
+
forceManyBody: force.forceManyBody,
|
|
20
|
+
forceCenter: force.forceCenter,
|
|
21
|
+
forceCollide: force.forceCollide,
|
|
22
|
+
drag: dragMod.drag,
|
|
23
|
+
zoom: zoomMod.zoom,
|
|
24
|
+
select: selectionMod.select,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
catch (err) {
|
|
28
|
+
console.warn('<lyra-graph> needs the optional peer dependencies `d3-force`, `d3-drag`, ' +
|
|
29
|
+
'`d3-zoom`, and `d3-selection` — install them with `pnpm add d3-force d3-drag d3-zoom d3-selection`:', err);
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
let d3Modules;
|
|
34
|
+
/**
|
|
35
|
+
* Lazily loads the d3 peer dependencies (see `loadD3Modules()`) once per
|
|
36
|
+
* page. Resolves to `null` if they aren't installed — mirrors
|
|
37
|
+
* `<lyra-flag>`'s peer-dependency pattern.
|
|
38
|
+
*/
|
|
39
|
+
export function loadD3() {
|
|
40
|
+
if (!d3Modules) {
|
|
41
|
+
d3Modules = loadD3Modules();
|
|
42
|
+
}
|
|
43
|
+
return d3Modules;
|
|
44
|
+
}
|
|
@@ -17,6 +17,10 @@ export interface GraphLink {
|
|
|
17
17
|
* Requires the optional peer deps `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection`
|
|
18
18
|
* (lazy-loaded; a consumer who never uses this component pays zero d3 cost).
|
|
19
19
|
*
|
|
20
|
+
* Set `seed` for a deterministic layout: node initial positions become
|
|
21
|
+
* reproducible (keyed by node id) and the settle happens synchronously
|
|
22
|
+
* instead of animating, like `prefers-reduced-motion`.
|
|
23
|
+
*
|
|
20
24
|
* @customElement lyra-graph
|
|
21
25
|
* @event lyra-node-click - `detail: { id }`.
|
|
22
26
|
* @event lyra-link-click - `detail: { source, target }`.
|
|
@@ -32,12 +36,14 @@ export declare class LyraGraph extends LyraElement {
|
|
|
32
36
|
linkDistance: number;
|
|
33
37
|
minZoom: number;
|
|
34
38
|
maxZoom: number;
|
|
39
|
+
/** When set, seeds each node's initial x/y deterministically (keyed by
|
|
40
|
+
* node id, not array index) instead of forceSimulation()'s own random
|
|
41
|
+
* start, and settles the simulation synchronously — see rebuildSimulation(). */
|
|
42
|
+
seed?: number;
|
|
35
43
|
/** True until the lazy-loaded d3 peer dependencies have settled (success or failure). */
|
|
36
44
|
private loading;
|
|
37
45
|
private simNodes;
|
|
38
46
|
private simLinks;
|
|
39
|
-
/** Current pan/zoom transform (SVG `transform` attribute syntax), driven by d3-zoom. */
|
|
40
|
-
private transform;
|
|
41
47
|
private simulation?;
|
|
42
48
|
/** The live charge/link force objects, kept so chargeStrength/linkDistance
|
|
43
49
|
* changes can retune them in place (see updated()) instead of requiring a
|
|
@@ -47,6 +53,14 @@ export declare class LyraGraph extends LyraElement {
|
|
|
47
53
|
private d3?;
|
|
48
54
|
/** The `<svg>` currently wired up with d3-zoom (guards a one-time bind). */
|
|
49
55
|
private zoomedEl?;
|
|
56
|
+
/** The pan/zoom `<g>`, cached alongside `zoomedEl` so the zoom handler can
|
|
57
|
+
* write the transform straight to the DOM (see applyInteractions()) instead
|
|
58
|
+
* of round-tripping through a Lit reactive property on every pan/zoom event. */
|
|
59
|
+
private gEl?;
|
|
60
|
+
/** The live zoom behavior, kept so minZoom/maxZoom changes can retune its
|
|
61
|
+
* scaleExtent in place (see applyInteractions()) instead of requiring the
|
|
62
|
+
* `<svg>` to be rebound. */
|
|
63
|
+
private zoomBehavior?;
|
|
50
64
|
/** Node `<circle>`s already wired up with d3-drag; cleared on every simulation rebuild
|
|
51
65
|
* so DOM elements Lit reuses across a rebuild get rebound to their fresh datum. */
|
|
52
66
|
private boundNodeEls;
|
|
@@ -64,25 +78,37 @@ export declare class LyraGraph extends LyraElement {
|
|
|
64
78
|
protected updated(changed: PropertyValues): void;
|
|
65
79
|
/**
|
|
66
80
|
* Imperatively wires up d3-zoom (pan/zoom on the `<svg>`) and d3-drag
|
|
67
|
-
* (per-node drag) against the just-rendered DOM. The zoom bind is a
|
|
68
|
-
* one-time
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
* `
|
|
75
|
-
*
|
|
81
|
+
* (per-node drag) against the just-rendered DOM. The zoom bind itself is a
|
|
82
|
+
* one-time guard (`zoomedEl`) — but the bound `zoomBehavior`'s
|
|
83
|
+
* `scaleExtent` is re-read from `minZoom`/`maxZoom` on every call so a
|
|
84
|
+
* post-mount change to either still takes effect. The node-drag bind +
|
|
85
|
+
* node/link/label element caching for `onTick()` only run when
|
|
86
|
+
* `changed` indicates a fresh structural render just happened, not on
|
|
87
|
+
* every call — otherwise this would re-scan the DOM via
|
|
88
|
+
* `querySelectorAll` on every Lit update, which used to include every
|
|
89
|
+
* single simulation tick. `<circle>`s already bound are skipped
|
|
90
|
+
* (`boundNodeEls`) — a WeakSet reset on every `rebuildSimulation()` so DOM
|
|
91
|
+
* nodes Lit reuses across a rebuild get rebound against their new datum
|
|
92
|
+
* instead of a stale one. The zoom handler writes the resulting transform
|
|
93
|
+
* straight to the cached `gEl` (bound once alongside `zoomedEl`, since the
|
|
94
|
+
* outer `<g>` is a static part of the template and never recreated by Lit)
|
|
95
|
+
* instead of assigning a Lit reactive property — panning/zooming fires
|
|
96
|
+
* continuously while dragging, and reassigning a `@state()` there would
|
|
97
|
+
* force a full re-render (recomputing every node/link template) on every
|
|
98
|
+
* single event, the same class of cost `onTick()` already avoids for ticks.
|
|
76
99
|
*/
|
|
77
100
|
private applyInteractions;
|
|
78
101
|
/**
|
|
79
102
|
* Runs on every d3-force simulation tick (up to ~300 while a graph settles
|
|
80
103
|
* on load, continuously while a node is being dragged via
|
|
81
104
|
* `alphaTarget(0.3)`). Writes positions straight to the already-rendered
|
|
82
|
-
* DOM via
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
* `querySelectorAll` scan) on every single frame.
|
|
105
|
+
* DOM via `setAttribute()` instead of reassigning the reactive
|
|
106
|
+
* `simNodes`/`simLinks` properties, which would force a full Lit re-render
|
|
107
|
+
* (and, before the structural-render gate in `applyInteractions()`, an
|
|
108
|
+
* unconditional `querySelectorAll` scan) on every single frame. Writing attributes
|
|
109
|
+
* directly (rather than wrapping each element in a d3 selection just to
|
|
110
|
+
* call `.attr()`) avoids allocating a throwaway Selection per element on
|
|
111
|
+
* this component's highest-frequency code path.
|
|
86
112
|
*/
|
|
87
113
|
private onTick;
|
|
88
114
|
private rebuildSimulation;
|