@aceshooting/lyra-ui 2.0.0 → 2.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 +87 -14
- package/custom-elements.json +11555 -5539
- package/dist/components/app-rail/app-rail-item.d.ts +30 -0
- package/dist/components/app-rail/app-rail-item.js +68 -0
- package/dist/components/app-rail/app-rail-item.styles.d.ts +1 -0
- package/dist/components/app-rail/app-rail-item.styles.js +65 -0
- package/dist/components/app-rail/app-rail.d.ts +21 -24
- package/dist/components/app-rail/app-rail.js +68 -132
- package/dist/components/app-rail/app-rail.styles.js +5 -2
- package/dist/components/attachment-chip/attachment-chip.d.ts +38 -11
- package/dist/components/attachment-chip/attachment-chip.js +73 -29
- package/dist/components/attachment-trigger/attachment-trigger.d.ts +13 -0
- package/dist/components/attachment-trigger/attachment-trigger.js +13 -1
- package/dist/components/attachment-trigger/attachment-trigger.styles.js +28 -6
- package/dist/components/chart/bar-chart.js +2 -18
- package/dist/components/chart/box-plot.d.ts +25 -1
- package/dist/components/chart/box-plot.js +136 -6
- package/dist/components/chart/box-plot.styles.js +13 -0
- package/dist/components/chart/bubble-chart.js +2 -18
- package/dist/components/chart/chart.d.ts +54 -10
- package/dist/components/chart/chart.js +182 -39
- package/dist/components/chart/doughnut-chart.js +2 -18
- package/dist/components/chart/histogram-bin.d.ts +5 -2
- package/dist/components/chart/histogram-bin.js +12 -4
- package/dist/components/chart/histogram.js +8 -16
- package/dist/components/chart/line-chart.js +2 -18
- package/dist/components/chart/lite-chart.d.ts +80 -3
- package/dist/components/chart/lite-chart.js +250 -53
- package/dist/components/chart/pie-chart.js +2 -18
- package/dist/components/chart/polar-area-chart.js +2 -18
- package/dist/components/chart/radar-chart.js +2 -18
- package/dist/components/chart/scatter-chart.js +2 -18
- package/dist/components/chat-composer/chat-composer.d.ts +42 -1
- package/dist/components/chat-composer/chat-composer.js +84 -3
- package/dist/components/chat-composer/chat-composer.styles.js +11 -1
- package/dist/components/chat-message/chat-message.d.ts +32 -4
- package/dist/components/chat-message/chat-message.js +57 -17
- package/dist/components/checkbox/checkbox.d.ts +66 -8
- package/dist/components/checkbox/checkbox.js +173 -50
- package/dist/components/checkbox/checkbox.styles.js +9 -2
- package/dist/components/chip/chip-group.js +8 -2
- package/dist/components/chip/chip.js +9 -2
- package/dist/components/citation-badge/citation-badge.js +21 -3
- package/dist/components/code-block/code-block.d.ts +38 -0
- package/dist/components/code-block/code-block.js +72 -7
- package/dist/components/code-block/code-loader.d.ts +44 -1
- package/dist/components/code-block/code-loader.js +63 -0
- package/dist/components/combobox/combobox.d.ts +46 -5
- package/dist/components/combobox/combobox.js +175 -35
- package/dist/components/combobox/combobox.styles.js +3 -3
- package/dist/components/context-meter/context-meter.js +17 -8
- package/dist/components/context-meter/context-meter.styles.js +0 -6
- package/dist/components/conversation-item/conversation-item.d.ts +13 -16
- package/dist/components/conversation-item/conversation-item.js +29 -20
- package/dist/components/date-picker/calendar-core.d.ts +24 -0
- package/dist/components/date-picker/calendar-core.js +51 -4
- package/dist/components/date-picker/date-input.d.ts +77 -10
- package/dist/components/date-picker/date-input.js +278 -73
- package/dist/components/date-picker/date-picker.d.ts +28 -4
- package/dist/components/date-picker/date-picker.js +95 -37
- package/dist/components/dialog/confirm.js +3 -6
- package/dist/components/dialog/dialog.d.ts +75 -31
- package/dist/components/dialog/dialog.js +147 -131
- package/dist/components/dialog/dialog.styles.js +46 -2
- package/dist/components/dock-panel/dock-panel.d.ts +8 -0
- package/dist/components/dock-panel/dock-panel.js +15 -1
- package/dist/components/dock-panel/dock-panel.styles.js +15 -1
- package/dist/components/document-preview/document-preview.d.ts +15 -7
- package/dist/components/document-preview/document-preview.js +29 -13
- package/dist/components/empty/empty.d.ts +1 -2
- package/dist/components/empty/empty.js +18 -12
- package/dist/components/empty/empty.styles.js +0 -3
- package/dist/components/export-button/export-button.d.ts +12 -0
- package/dist/components/export-button/export-button.js +33 -5
- package/dist/components/export-button/export-button.styles.js +5 -1
- package/dist/components/file-input/accept.js +2 -0
- package/dist/components/file-input/file-input.d.ts +10 -1
- package/dist/components/file-input/file-input.js +36 -5
- package/dist/components/flag/flag.d.ts +1 -1
- package/dist/components/flag/flag.js +15 -1
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +4 -1
- package/dist/components/flag/language-map.js +6 -3
- package/dist/components/gauge/gauge.d.ts +10 -1
- package/dist/components/gauge/gauge.js +28 -9
- package/dist/components/generation-status/generation-status.d.ts +13 -5
- package/dist/components/generation-status/generation-status.js +59 -5
- package/dist/components/graph/graph.d.ts +37 -3
- package/dist/components/graph/graph.js +132 -20
- package/dist/components/heatmap/calendar-grid.d.ts +15 -6
- package/dist/components/heatmap/calendar-grid.js +17 -6
- package/dist/components/heatmap/heatmap-scale.d.ts +5 -1
- package/dist/components/heatmap/heatmap-scale.js +8 -2
- package/dist/components/heatmap/heatmap.d.ts +166 -25
- package/dist/components/heatmap/heatmap.js +372 -89
- package/dist/components/json-viewer/json-viewer.d.ts +22 -5
- package/dist/components/json-viewer/json-viewer.js +115 -37
- package/dist/components/json-viewer/json-viewer.styles.js +2 -1
- package/dist/components/kbd/kbd.js +24 -7
- package/dist/components/live-region/live-region.d.ts +10 -1
- package/dist/components/live-region/live-region.js +42 -6
- package/dist/components/map/map.d.ts +24 -1
- package/dist/components/map/map.js +89 -24
- package/dist/components/markdown/markdown-loader.d.ts +24 -0
- package/dist/components/markdown/markdown-loader.js +35 -1
- package/dist/components/markdown/markdown.d.ts +46 -8
- package/dist/components/markdown/markdown.js +136 -31
- package/dist/components/markdown/markdown.styles.js +7 -1
- package/dist/components/media-card/media-card.d.ts +25 -17
- package/dist/components/media-card/media-card.js +43 -65
- package/dist/components/mention-popover/mention-popover.d.ts +4 -1
- package/dist/components/mention-popover/mention-popover.js +23 -5
- package/dist/components/menu/menu-item.d.ts +32 -3
- package/dist/components/menu/menu-item.js +94 -7
- package/dist/components/menu/menu-item.styles.js +8 -0
- package/dist/components/menu/menu.d.ts +25 -1
- package/dist/components/menu/menu.js +99 -3
- package/dist/components/menu/menu.styles.js +3 -2
- package/dist/components/model-select/model-select.d.ts +48 -3
- package/dist/components/model-select/model-select.js +120 -22
- package/dist/components/model-select/model-select.styles.js +17 -3
- package/dist/components/model-settings-panel/model-settings-panel.d.ts +25 -1
- package/dist/components/model-settings-panel/model-settings-panel.js +65 -1
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +72 -29
- package/dist/components/playback/playback.styles.js +6 -1
- package/dist/components/responsive-panel/responsive-panel.d.ts +29 -16
- package/dist/components/responsive-panel/responsive-panel.js +130 -132
- package/dist/components/responsive-panel/responsive-panel.styles.js +1 -1
- package/dist/components/result-card/result-card.d.ts +8 -2
- package/dist/components/result-card/result-card.js +9 -3
- package/dist/components/result-card/result-card.styles.js +0 -1
- package/dist/components/select/select.d.ts +59 -20
- package/dist/components/select/select.js +125 -43
- package/dist/components/skeleton/skeleton.d.ts +3 -0
- package/dist/components/skeleton/skeleton.js +7 -1
- package/dist/components/skeleton/skeleton.styles.js +5 -4
- package/dist/components/slider/slider.d.ts +57 -5
- package/dist/components/slider/slider.js +195 -80
- package/dist/components/source-card/source-card.d.ts +17 -5
- package/dist/components/source-card/source-card.js +55 -8
- package/dist/components/source-card/source-card.styles.js +1 -1
- package/dist/components/source-list/source-list.js +9 -1
- package/dist/components/sparkline/sparkline.js +34 -10
- package/dist/components/split/split.d.ts +141 -14
- package/dist/components/split/split.js +357 -52
- package/dist/components/split/split.styles.js +18 -1
- package/dist/components/stat/stat.d.ts +10 -4
- package/dist/components/stat/stat.js +46 -11
- package/dist/components/stream-status/stream-status.d.ts +27 -11
- package/dist/components/stream-status/stream-status.js +95 -13
- package/dist/components/streaming-text/streaming-text.d.ts +9 -3
- package/dist/components/streaming-text/streaming-text.js +25 -10
- package/dist/components/streaming-text/streaming-text.styles.js +3 -2
- package/dist/components/switch/switch.d.ts +52 -7
- package/dist/components/switch/switch.js +111 -40
- package/dist/components/switch/switch.styles.js +1 -1
- package/dist/components/table/table.d.ts +18 -3
- package/dist/components/table/table.js +88 -20
- package/dist/components/tabs/tabs.d.ts +23 -0
- package/dist/components/tabs/tabs.js +76 -12
- package/dist/components/tabs/tabs.styles.js +11 -0
- package/dist/components/thinking-panel/thinking-panel.d.ts +3 -1
- package/dist/components/thinking-panel/thinking-panel.js +34 -9
- package/dist/components/thinking-panel/thinking-panel.styles.js +6 -0
- package/dist/components/time-range/time-range.d.ts +59 -2
- package/dist/components/time-range/time-range.js +163 -67
- package/dist/components/toast/toast-item.d.ts +12 -3
- package/dist/components/toast/toast-item.js +90 -53
- package/dist/components/toast/toast-item.styles.js +59 -28
- package/dist/components/toast/toast.js +17 -5
- package/dist/components/toast/toaster.js +10 -1
- package/dist/components/tool-approval-dialog/tool-approval-dialog.d.ts +11 -10
- package/dist/components/tool-approval-dialog/tool-approval-dialog.js +50 -124
- package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.js +1 -1
- package/dist/components/tool-call-chip/tool-call-chip.d.ts +18 -2
- package/dist/components/tool-call-chip/tool-call-chip.js +86 -25
- package/dist/components/tool-param-form/tool-param-form.d.ts +89 -28
- package/dist/components/tool-param-form/tool-param-form.js +350 -106
- package/dist/components/tool-result-dialog/tool-result-dialog.d.ts +27 -7
- package/dist/components/tool-result-dialog/tool-result-dialog.js +80 -115
- package/dist/components/tool-result-dialog/tool-result-dialog.styles.js +1 -1
- package/dist/components/tool-result-view/registry.d.ts +0 -8
- package/dist/components/tool-result-view/registry.js +7 -2
- package/dist/components/tool-result-view/tool-result-view.d.ts +6 -3
- package/dist/components/tool-result-view/tool-result-view.js +18 -4
- package/dist/components/tool-select-dialog/tool-select-dialog.d.ts +11 -11
- package/dist/components/tool-select-dialog/tool-select-dialog.js +44 -133
- package/dist/components/tool-select-dialog/tool-select-dialog.styles.js +13 -4
- package/dist/components/tree/tree-node.d.ts +4 -1
- package/dist/components/tree/tree-node.js +4 -1
- package/dist/components/tree/tree.d.ts +17 -8
- package/dist/components/tree/tree.js +21 -20
- package/dist/components/typing-indicator/typing-indicator.d.ts +12 -2
- package/dist/components/typing-indicator/typing-indicator.js +16 -4
- package/dist/components/typing-indicator/typing-indicator.styles.js +2 -2
- package/dist/components/virtual-list/virtual-list.d.ts +56 -19
- package/dist/components/virtual-list/virtual-list.js +184 -38
- package/dist/components/virtual-list/virtual-list.styles.js +12 -0
- package/dist/components/widget/widget.d.ts +5 -4
- package/dist/components/widget/widget.js +54 -120
- package/dist/components/widget/widget.styles.js +8 -2
- package/dist/components/word-cloud/word-cloud-layout.d.ts +7 -0
- package/dist/components/word-cloud/word-cloud-layout.js +20 -6
- package/dist/components/word-cloud/word-cloud.d.ts +22 -2
- package/dist/components/word-cloud/word-cloud.js +45 -6
- package/dist/components/word-cloud/word-cloud.styles.js +13 -1
- package/dist/internal/anchored-validity.d.ts +1 -0
- package/dist/internal/anchored-validity.js +86 -0
- package/dist/internal/form-associated.d.ts +7 -0
- package/dist/internal/form-associated.js +43 -4
- package/dist/internal/numbers.d.ts +12 -0
- package/dist/internal/numbers.js +26 -0
- package/dist/internal/overlay-manager.d.ts +51 -0
- package/dist/internal/overlay-manager.js +523 -0
- package/dist/internal/positioner.js +27 -3
- package/dist/internal/safe-url.d.ts +12 -0
- package/dist/internal/safe-url.js +41 -0
- package/dist/internal/tokens.styles.js +17 -11
- package/dist/lyra.d.ts +5 -2
- package/dist/lyra.js +3 -1
- package/llms-full.txt +970 -426
- package/llms.txt +5 -4
- package/package.json +19 -2
|
@@ -9,6 +9,7 @@ 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 { srOnly } from '../../internal/a11y.js';
|
|
12
|
+
import { safeFetchUrl, safeLinkHref, safeMediaSrc } from '../../internal/safe-url.js';
|
|
12
13
|
import { styles } from './document-preview.styles.js';
|
|
13
14
|
const IDLE_TEXT_FETCH = { kind: 'idle' };
|
|
14
15
|
function classifyFormat(mimeType) {
|
|
@@ -110,6 +111,13 @@ function icon(paths) {
|
|
|
110
111
|
* that a *rendering* failure and a host's own state machine are different
|
|
111
112
|
* concerns.
|
|
112
113
|
*
|
|
114
|
+
* Every `src` is validated for the DOM/API sink that consumes it. Text
|
|
115
|
+
* fetches and image sources allow relative URLs plus `http:`, `https:`,
|
|
116
|
+
* `blob:`, and `data:`. Download links deliberately exclude `data:` because
|
|
117
|
+
* following a `data:text/html` URL can create an active document. Unsafe or
|
|
118
|
+
* malformed URLs never reach `fetch()`, an image `src`, or an anchor `href`;
|
|
119
|
+
* they render a non-interactive fallback/error instead.
|
|
120
|
+
*
|
|
113
121
|
* Accessibility: the `"converting"` state (no numeric `progress`) is a
|
|
114
122
|
* `role="status"` region wrapping a visually-hidden "Converting document…"
|
|
115
123
|
* string. This is a *plain* static region, not routed through
|
|
@@ -131,10 +139,10 @@ function icon(paths) {
|
|
|
131
139
|
* component doesn't natively support (i.e. whenever format dispatch would
|
|
132
140
|
* otherwise fall through to "generic"). Ignored while `mime-type` resolves
|
|
133
141
|
* to `text`/`image` dispatch, or while `status` is `"converting"`/`"error"`.
|
|
134
|
-
* @event lyra-download - `detail: { src, filename }` — fired when the
|
|
135
|
-
* generic-download fallback
|
|
136
|
-
*
|
|
137
|
-
*
|
|
142
|
+
* @event lyra-download - `detail: { src, filename }` — fired when the safe
|
|
143
|
+
* generic-download fallback link is activated. The browser download itself
|
|
144
|
+
* needs no JS (a plain `<a download>` handles it); this is purely for a host
|
|
145
|
+
* that wants to observe/log the download.
|
|
138
146
|
* @event lyra-render-error - `detail: { error }` — fired when this
|
|
139
147
|
* component's own `text/*`/`application/json` `fetch(src)` fails. Distinct
|
|
140
148
|
* from `status="error"`, which is entirely host-driven (see the class
|
|
@@ -145,14 +153,15 @@ function icon(paths) {
|
|
|
145
153
|
* @csspart body - The wrapper around whichever content is currently showing (text/image preview, the generic fallback, the spinner, or the error message).
|
|
146
154
|
* @csspart spinner - The converting/loading indicator — indeterminate (`role="status"`) or, once numeric progress is known, a determinate `role="progressbar"`. Used both for `status="converting"` and for this component's own in-flight text fetch.
|
|
147
155
|
* @csspart error - The error message region (`role="alert"`) — used both for `status="error"` and for a failed text fetch.
|
|
148
|
-
* @csspart download-link - The `<a download>` affordance in the generic fallback. Only rendered when `src` is set.
|
|
156
|
+
* @csspart download-link - The `<a download>` affordance in the generic fallback. Only rendered when `src` is set and safe for link navigation.
|
|
149
157
|
*/
|
|
150
158
|
export class LyraDocumentPreview extends LyraElement {
|
|
151
159
|
constructor() {
|
|
152
160
|
super(...arguments);
|
|
153
161
|
/** URL to fetch (for `text`/`application/json`) or display (`image`, or as
|
|
154
|
-
* the generic fallback's download `href`).
|
|
155
|
-
*
|
|
162
|
+
* the generic fallback's download `href`). The value is validated against
|
|
163
|
+
* a sink-specific scheme allowlist before use. Optional — gracefully
|
|
164
|
+
* absent while, e.g., a conversion is still in progress. */
|
|
156
165
|
this.src = '';
|
|
157
166
|
/** Drives format dispatch — see the class doc. */
|
|
158
167
|
this.mimeType = '';
|
|
@@ -189,8 +198,9 @@ export class LyraDocumentPreview extends LyraElement {
|
|
|
189
198
|
if (!this.hasUpdated) {
|
|
190
199
|
this.hasUnsupportedSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'unsupported');
|
|
191
200
|
}
|
|
201
|
+
const safeTextSrc = safeFetchUrl(this.src);
|
|
192
202
|
const shouldFetchText = classifyFormat(this.mimeType) === 'text' &&
|
|
193
|
-
|
|
203
|
+
safeTextSrc !== null &&
|
|
194
204
|
this.status !== 'converting' &&
|
|
195
205
|
this.status !== 'error';
|
|
196
206
|
if (shouldFetchText) {
|
|
@@ -201,7 +211,7 @@ export class LyraDocumentPreview extends LyraElement {
|
|
|
201
211
|
changed.has('src') ||
|
|
202
212
|
changed.has('mimeType') ||
|
|
203
213
|
(changed.has('status') && this.textFetch.kind === 'idle')) {
|
|
204
|
-
void this.fetchText(
|
|
214
|
+
void this.fetchText(safeTextSrc);
|
|
205
215
|
}
|
|
206
216
|
}
|
|
207
217
|
else if (this.textFetch.kind !== 'idle') {
|
|
@@ -269,7 +279,9 @@ export class LyraDocumentPreview extends LyraElement {
|
|
|
269
279
|
}
|
|
270
280
|
renderTextPreview() {
|
|
271
281
|
if (this.src === '')
|
|
272
|
-
return html `<
|
|
282
|
+
return html `<p class="empty-note">No document to display.</p>`;
|
|
283
|
+
if (safeFetchUrl(this.src) === null)
|
|
284
|
+
return this.renderError('Document URL is not allowed.');
|
|
273
285
|
switch (this.textFetch.kind) {
|
|
274
286
|
case 'loaded':
|
|
275
287
|
return html `<pre class="text">${this.textFetch.text}</pre>`;
|
|
@@ -284,19 +296,23 @@ export class LyraDocumentPreview extends LyraElement {
|
|
|
284
296
|
renderImagePreview() {
|
|
285
297
|
if (this.src === '')
|
|
286
298
|
return html `<p class="empty-note">No image to display.</p>`;
|
|
287
|
-
|
|
299
|
+
const src = safeMediaSrc(this.src);
|
|
300
|
+
if (src === null)
|
|
301
|
+
return this.renderDownloadFallback();
|
|
302
|
+
return html `<img src=${src} alt=${this.filename || 'Document preview'} />`;
|
|
288
303
|
}
|
|
289
304
|
renderDownloadFallback() {
|
|
290
305
|
const label = this.filename || 'this file';
|
|
306
|
+
const href = safeLinkHref(this.src);
|
|
291
307
|
return html `
|
|
292
308
|
<div class="fallback">
|
|
293
309
|
<span class="fallback-icon" aria-hidden="true">${fileGlyph()}</span>
|
|
294
310
|
<p class="fallback-text">Preview not available for ${label}.</p>
|
|
295
|
-
${
|
|
311
|
+
${href !== null
|
|
296
312
|
? html `
|
|
297
313
|
<a
|
|
298
314
|
part="download-link"
|
|
299
|
-
href=${
|
|
315
|
+
href=${href}
|
|
300
316
|
download=${this.filename || ''}
|
|
301
317
|
@click=${this.onDownloadClick}
|
|
302
318
|
>
|
|
@@ -30,8 +30,7 @@ export declare class LyraEmpty extends LyraElement {
|
|
|
30
30
|
private hasActions;
|
|
31
31
|
protected willUpdate(): void;
|
|
32
32
|
firstUpdated(): void;
|
|
33
|
-
private
|
|
34
|
-
private checkActionsSlot;
|
|
33
|
+
private reconcileSlotHidden;
|
|
35
34
|
private onIconSlotChange;
|
|
36
35
|
private onActionsSlotChange;
|
|
37
36
|
render(): TemplateResult;
|
|
@@ -42,10 +42,12 @@ export class LyraEmpty extends LyraElement {
|
|
|
42
42
|
this.hasIcon = false;
|
|
43
43
|
this.hasActions = false;
|
|
44
44
|
this.onIconSlotChange = (e) => {
|
|
45
|
-
|
|
45
|
+
const slot = e.target;
|
|
46
|
+
this.hasIcon = slot.assignedElements({ flatten: true }).length > 0;
|
|
46
47
|
};
|
|
47
48
|
this.onActionsSlotChange = (e) => {
|
|
48
|
-
|
|
49
|
+
const slot = e.target;
|
|
50
|
+
this.hasActions = slot.assignedElements({ flatten: true }).length > 0;
|
|
49
51
|
};
|
|
50
52
|
}
|
|
51
53
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
@@ -64,17 +66,21 @@ export class LyraEmpty extends LyraElement {
|
|
|
64
66
|
}
|
|
65
67
|
firstUpdated() {
|
|
66
68
|
// Fallback reconciliation against the fully-resolved slot assignment
|
|
67
|
-
// (handles slot-forwarding
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
// (handles slot-forwarding — where `this.children` in `willUpdate` above
|
|
70
|
+
// are forwarding `<slot>` elements rather than the real projected
|
|
71
|
+
// content, e.g. a wrapper component's own default slot re-slotted into
|
|
72
|
+
// ours — and any browser where `slotchange` doesn't fire for content
|
|
73
|
+
// present at parse/upgrade time). This corrects the `hidden` attribute
|
|
74
|
+
// on the wrapper elements directly rather than through
|
|
75
|
+
// `hasIcon`/`hasActions`, so this one-shot correction doesn't need to
|
|
76
|
+
// schedule and wait out a second Lit render pass; the reactive state is
|
|
77
|
+
// left as `willUpdate` set it and continues to drive only the ongoing
|
|
78
|
+
// `slotchange` path below.
|
|
79
|
+
this.reconcileSlotHidden(this.shadowRoot.querySelector('slot:not([name])'), this.shadowRoot.querySelector('[part="icon"]'));
|
|
80
|
+
this.reconcileSlotHidden(this.shadowRoot.querySelector('slot[name="actions"]'), this.shadowRoot.querySelector('[part="actions"]'));
|
|
72
81
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
checkActionsSlot(slot) {
|
|
77
|
-
this.hasActions = slot.assignedElements({ flatten: true }).length > 0;
|
|
82
|
+
reconcileSlotHidden(slot, wrapper) {
|
|
83
|
+
wrapper.toggleAttribute('hidden', slot.assignedElements({ flatten: true }).length === 0);
|
|
78
84
|
}
|
|
79
85
|
render() {
|
|
80
86
|
return html `
|
|
@@ -18,10 +18,16 @@ export type ExportFormat = 'csv' | 'json';
|
|
|
18
18
|
export declare class LyraExportButton extends LyraElement {
|
|
19
19
|
static styles: import("lit").CSSResultGroup[];
|
|
20
20
|
rows: Record<string, unknown>[];
|
|
21
|
+
/** Column allow-list (and CSV header labels) for both export formats. Left
|
|
22
|
+
* at its default empty array, both formats fall back to the union of the
|
|
23
|
+
* rows' own keys instead (see `effectiveColumns()`), rather than CSV
|
|
24
|
+
* degrading to a blank file while only JSON had a fallback. */
|
|
21
25
|
columns: CsvColumn[];
|
|
22
26
|
filename: string;
|
|
23
27
|
formats: ExportFormat[];
|
|
24
28
|
disabled: boolean;
|
|
29
|
+
/** Trigger button text, also feeds the format menu's `aria-label` (as
|
|
30
|
+
* "`${label} format`") so assistive tech gets an accessible name for it. */
|
|
25
31
|
label: string;
|
|
26
32
|
open: boolean;
|
|
27
33
|
private triggerEl?;
|
|
@@ -42,6 +48,12 @@ export declare class LyraExportButton extends LyraElement {
|
|
|
42
48
|
private onKeyDown;
|
|
43
49
|
protected firstUpdated(): void;
|
|
44
50
|
protected updated(changed: PropertyValues): void;
|
|
51
|
+
/** Falls back to the union of the rows' own keys when `columns` is left at
|
|
52
|
+
* its default empty array, so an unconfigured export still produces a
|
|
53
|
+
* proper header + data file instead of blank lines. Both `rowsForExport()`
|
|
54
|
+
* and the CSV branch of `doExport()` share this same fallback, rather than
|
|
55
|
+
* only the JSON path having one. */
|
|
56
|
+
private effectiveColumns;
|
|
45
57
|
/** Applies the same `columns` allow-list CSV exports use, so JSON can't leak fields CSV hides. */
|
|
46
58
|
private rowsForExport;
|
|
47
59
|
private doExport;
|
|
@@ -29,10 +29,16 @@ export class LyraExportButton extends LyraElement {
|
|
|
29
29
|
constructor() {
|
|
30
30
|
super(...arguments);
|
|
31
31
|
this.rows = [];
|
|
32
|
+
/** Column allow-list (and CSV header labels) for both export formats. Left
|
|
33
|
+
* at its default empty array, both formats fall back to the union of the
|
|
34
|
+
* rows' own keys instead (see `effectiveColumns()`), rather than CSV
|
|
35
|
+
* degrading to a blank file while only JSON had a fallback. */
|
|
32
36
|
this.columns = [];
|
|
33
37
|
this.filename = 'export';
|
|
34
38
|
this.formats = ['csv'];
|
|
35
39
|
this.disabled = false;
|
|
40
|
+
/** Trigger button text, also feeds the format menu's `aria-label` (as
|
|
41
|
+
* "`${label} format`") so assistive tech gets an accessible name for it. */
|
|
36
42
|
this.label = 'Export';
|
|
37
43
|
this.open = false;
|
|
38
44
|
this.menuId = nextId('export-menu');
|
|
@@ -76,6 +82,12 @@ export class LyraExportButton extends LyraElement {
|
|
|
76
82
|
this.focusMenuItem(items.length - 1);
|
|
77
83
|
}
|
|
78
84
|
break;
|
|
85
|
+
case 'Tab':
|
|
86
|
+
// No preventDefault -- native Tab navigation proceeds untouched, only
|
|
87
|
+
// the now-stale open menu closes (mirrors lyra-menu's identical
|
|
88
|
+
// Tab handling).
|
|
89
|
+
this.closeMenu();
|
|
90
|
+
break;
|
|
79
91
|
}
|
|
80
92
|
};
|
|
81
93
|
}
|
|
@@ -143,11 +155,24 @@ export class LyraExportButton extends LyraElement {
|
|
|
143
155
|
}
|
|
144
156
|
}
|
|
145
157
|
}
|
|
158
|
+
/** Falls back to the union of the rows' own keys when `columns` is left at
|
|
159
|
+
* its default empty array, so an unconfigured export still produces a
|
|
160
|
+
* proper header + data file instead of blank lines. Both `rowsForExport()`
|
|
161
|
+
* and the CSV branch of `doExport()` share this same fallback, rather than
|
|
162
|
+
* only the JSON path having one. */
|
|
163
|
+
effectiveColumns() {
|
|
164
|
+
if (this.columns.length > 0)
|
|
165
|
+
return this.columns;
|
|
166
|
+
const keys = new Set();
|
|
167
|
+
for (const row of this.rows) {
|
|
168
|
+
for (const key of Object.keys(row))
|
|
169
|
+
keys.add(key);
|
|
170
|
+
}
|
|
171
|
+
return Array.from(keys, (key) => ({ key, label: key }));
|
|
172
|
+
}
|
|
146
173
|
/** Applies the same `columns` allow-list CSV exports use, so JSON can't leak fields CSV hides. */
|
|
147
174
|
rowsForExport() {
|
|
148
|
-
|
|
149
|
-
return this.rows;
|
|
150
|
-
const keys = this.columns.map((c) => c.key);
|
|
175
|
+
const keys = this.effectiveColumns().map((c) => c.key);
|
|
151
176
|
return this.rows.map((row) => {
|
|
152
177
|
const picked = {};
|
|
153
178
|
for (const key of keys)
|
|
@@ -156,13 +181,15 @@ export class LyraExportButton extends LyraElement {
|
|
|
156
181
|
});
|
|
157
182
|
}
|
|
158
183
|
doExport(format) {
|
|
184
|
+
if (this.disabled)
|
|
185
|
+
return;
|
|
159
186
|
this.closeMenu();
|
|
160
187
|
this.triggerEl?.focus();
|
|
161
188
|
const ev = this.emit('lyra-export', { format });
|
|
162
189
|
if (ev.defaultPrevented)
|
|
163
190
|
return;
|
|
164
191
|
if (format === 'csv') {
|
|
165
|
-
downloadBlob(buildCsv(this.rows, this.
|
|
192
|
+
downloadBlob(buildCsv(this.rows, this.effectiveColumns()), `${this.filename}.csv`, 'text/csv;charset=utf-8;');
|
|
166
193
|
}
|
|
167
194
|
else {
|
|
168
195
|
downloadBlob(JSON.stringify(this.rowsForExport(), null, 2), `${this.filename}.json`, 'application/json');
|
|
@@ -191,11 +218,12 @@ export class LyraExportButton extends LyraElement {
|
|
|
191
218
|
${this.label}
|
|
192
219
|
</button>
|
|
193
220
|
${this.formats.length > 1
|
|
194
|
-
? html `<div id=${this.menuId} part="menu" role="menu">
|
|
221
|
+
? html `<div id=${this.menuId} part="menu" role="menu" aria-label="${this.label} format">
|
|
195
222
|
${this.formats.map((f) => html `<button
|
|
196
223
|
part="menu-item"
|
|
197
224
|
role="menuitem"
|
|
198
225
|
type="button"
|
|
226
|
+
?disabled=${this.disabled}
|
|
199
227
|
@click=${() => this.doExport(f)}
|
|
200
228
|
>
|
|
201
229
|
${f.toUpperCase()}
|
|
@@ -77,11 +77,15 @@ export const styles = css `
|
|
|
77
77
|
cursor: pointer;
|
|
78
78
|
border-radius: var(--lyra-radius);
|
|
79
79
|
}
|
|
80
|
-
[part='menu-item']:hover {
|
|
80
|
+
[part='menu-item']:hover:not(:disabled) {
|
|
81
81
|
background: var(--lyra-color-brand-quiet);
|
|
82
82
|
}
|
|
83
83
|
[part='menu-item']:focus-visible {
|
|
84
84
|
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
85
85
|
outline-offset: var(--lyra-focus-ring-offset);
|
|
86
86
|
}
|
|
87
|
+
[part='menu-item']:disabled {
|
|
88
|
+
opacity: var(--lyra-opacity-disabled);
|
|
89
|
+
cursor: not-allowed;
|
|
90
|
+
}
|
|
87
91
|
`;
|
|
@@ -28,6 +28,8 @@ export function matchesAccept(file, accept, assumeExtensionMatch = false) {
|
|
|
28
28
|
return parts.some((p) => {
|
|
29
29
|
if (p.startsWith('.'))
|
|
30
30
|
return name === undefined ? assumeExtensionMatch : name.endsWith(p);
|
|
31
|
+
if (p === '*/*')
|
|
32
|
+
return true;
|
|
31
33
|
if (p.endsWith('/*'))
|
|
32
34
|
return type.startsWith(p.slice(0, -1));
|
|
33
35
|
return type === p;
|
|
@@ -14,7 +14,9 @@ export interface RejectedFile {
|
|
|
14
14
|
* accessible name always comes from `label`, so icon-only slot content
|
|
15
15
|
* remains announced correctly.
|
|
16
16
|
* @event lyra-files - `detail: { files, rejected }`, fired on drop and manual selection.
|
|
17
|
-
* @csspart base
|
|
17
|
+
* @csspart base - The dropzone's root, clickable/focusable container.
|
|
18
|
+
* @csspart input - The visually-hidden native `<input type="file">`.
|
|
19
|
+
* @csspart status - The visually-hidden live region announcing drag accept/reject state.
|
|
18
20
|
*/
|
|
19
21
|
export declare class LyraFileInput extends LyraElement {
|
|
20
22
|
static styles: import("lit").CSSResultGroup[];
|
|
@@ -25,7 +27,14 @@ export declare class LyraFileInput extends LyraElement {
|
|
|
25
27
|
forbiddenMimeTypes: string[];
|
|
26
28
|
maxFileSize: number;
|
|
27
29
|
label: string;
|
|
30
|
+
/** Message announced after an accepted selection; `{count}` is replaced by
|
|
31
|
+
* the number of accepted files. */
|
|
32
|
+
acceptedMessage: string;
|
|
33
|
+
/** Message announced after rejected files; `{count}` is replaced by the
|
|
34
|
+
* number of rejected files. */
|
|
35
|
+
rejectedMessage: string;
|
|
28
36
|
private dragState;
|
|
37
|
+
private resultStatus;
|
|
29
38
|
private inputEl?;
|
|
30
39
|
private dragCounter;
|
|
31
40
|
private isAllowed;
|
|
@@ -21,7 +21,9 @@ import { matchesAccept } from './accept.js';
|
|
|
21
21
|
* accessible name always comes from `label`, so icon-only slot content
|
|
22
22
|
* remains announced correctly.
|
|
23
23
|
* @event lyra-files - `detail: { files, rejected }`, fired on drop and manual selection.
|
|
24
|
-
* @csspart base
|
|
24
|
+
* @csspart base - The dropzone's root, clickable/focusable container.
|
|
25
|
+
* @csspart input - The visually-hidden native `<input type="file">`.
|
|
26
|
+
* @csspart status - The visually-hidden live region announcing drag accept/reject state.
|
|
25
27
|
*/
|
|
26
28
|
export class LyraFileInput extends LyraElement {
|
|
27
29
|
constructor() {
|
|
@@ -33,20 +35,30 @@ export class LyraFileInput extends LyraElement {
|
|
|
33
35
|
this.forbiddenMimeTypes = [];
|
|
34
36
|
this.maxFileSize = 0;
|
|
35
37
|
this.label = 'Drop files here or click to browse';
|
|
38
|
+
/** Message announced after an accepted selection; `{count}` is replaced by
|
|
39
|
+
* the number of accepted files. */
|
|
40
|
+
this.acceptedMessage = '{count} file(s) added.';
|
|
41
|
+
/** Message announced after rejected files; `{count}` is replaced by the
|
|
42
|
+
* number of rejected files. */
|
|
43
|
+
this.rejectedMessage = '{count} file(s) rejected.';
|
|
36
44
|
this.dragState = 'default';
|
|
45
|
+
this.resultStatus = '';
|
|
37
46
|
this.dragCounter = 0;
|
|
38
47
|
this.onDragEnter = (e) => {
|
|
48
|
+
e.preventDefault();
|
|
39
49
|
if (this.disabled)
|
|
40
50
|
return;
|
|
41
|
-
e.preventDefault();
|
|
42
51
|
this.dragCounter++;
|
|
43
52
|
const items = e.dataTransfer ? [...e.dataTransfer.items].filter((i) => i.kind === 'file') : [];
|
|
44
53
|
this.dragState = items.length ? this.previewState(items) : 'default';
|
|
45
54
|
};
|
|
46
55
|
this.onDragOver = (e) => {
|
|
56
|
+
// Always suppress the browser's default drop action (e.g. navigating the
|
|
57
|
+
// whole page to the dropped file), even while disabled — only the
|
|
58
|
+
// subsequent classification/emit logic is gated on `disabled`.
|
|
59
|
+
e.preventDefault();
|
|
47
60
|
if (this.disabled)
|
|
48
61
|
return;
|
|
49
|
-
e.preventDefault();
|
|
50
62
|
};
|
|
51
63
|
this.onDragLeave = (e) => {
|
|
52
64
|
if (this.disabled)
|
|
@@ -57,9 +69,11 @@ export class LyraFileInput extends LyraElement {
|
|
|
57
69
|
this.dragState = 'default';
|
|
58
70
|
};
|
|
59
71
|
this.onDrop = (e) => {
|
|
72
|
+
// Same rationale as `onDragOver`: prevent the browser's default drop
|
|
73
|
+
// action unconditionally, before the `disabled` gate.
|
|
74
|
+
e.preventDefault();
|
|
60
75
|
if (this.disabled)
|
|
61
76
|
return;
|
|
62
|
-
e.preventDefault();
|
|
63
77
|
this.dragCounter = 0;
|
|
64
78
|
this.dragState = 'default';
|
|
65
79
|
const files = [...(e.dataTransfer?.files ?? [])];
|
|
@@ -118,10 +132,18 @@ export class LyraFileInput extends LyraElement {
|
|
|
118
132
|
}
|
|
119
133
|
emitFiles(fileList) {
|
|
120
134
|
const { files, rejected } = this.classify(fileList);
|
|
135
|
+
const messages = [];
|
|
136
|
+
if (files.length)
|
|
137
|
+
messages.push(this.acceptedMessage.replace('{count}', String(files.length)));
|
|
138
|
+
if (rejected.length)
|
|
139
|
+
messages.push(this.rejectedMessage.replace('{count}', String(rejected.length)));
|
|
140
|
+
this.resultStatus = messages.join(' ');
|
|
121
141
|
this.emit('lyra-files', { files, rejected });
|
|
122
142
|
}
|
|
123
143
|
/** Programmatically open the native file picker. */
|
|
124
144
|
openPicker() {
|
|
145
|
+
if (this.disabled)
|
|
146
|
+
return;
|
|
125
147
|
this.inputEl?.click();
|
|
126
148
|
}
|
|
127
149
|
previewState(fileList) {
|
|
@@ -133,7 +155,7 @@ export class LyraFileInput extends LyraElement {
|
|
|
133
155
|
return 'Release to add the file.';
|
|
134
156
|
if (this.dragState === 'reject')
|
|
135
157
|
return 'This file type is not accepted.';
|
|
136
|
-
return
|
|
158
|
+
return this.resultStatus;
|
|
137
159
|
}
|
|
138
160
|
render() {
|
|
139
161
|
return html `
|
|
@@ -188,9 +210,18 @@ __decorate([
|
|
|
188
210
|
__decorate([
|
|
189
211
|
property()
|
|
190
212
|
], LyraFileInput.prototype, "label", void 0);
|
|
213
|
+
__decorate([
|
|
214
|
+
property({ attribute: 'accepted-message' })
|
|
215
|
+
], LyraFileInput.prototype, "acceptedMessage", void 0);
|
|
216
|
+
__decorate([
|
|
217
|
+
property({ attribute: 'rejected-message' })
|
|
218
|
+
], LyraFileInput.prototype, "rejectedMessage", void 0);
|
|
191
219
|
__decorate([
|
|
192
220
|
state()
|
|
193
221
|
], LyraFileInput.prototype, "dragState", void 0);
|
|
222
|
+
__decorate([
|
|
223
|
+
state()
|
|
224
|
+
], LyraFileInput.prototype, "resultStatus", void 0);
|
|
194
225
|
__decorate([
|
|
195
226
|
query('input[type="file"]')
|
|
196
227
|
], LyraFileInput.prototype, "inputEl", void 0);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
2
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
3
|
import '../skeleton/skeleton.js';
|
|
4
|
-
type FlagVariant = 'compact' | 'standard' | 'detailed';
|
|
4
|
+
export type FlagVariant = 'compact' | 'standard' | 'detailed';
|
|
5
5
|
type FlagUrlResolver = (code: string, options?: {
|
|
6
6
|
variant?: FlagVariant;
|
|
7
7
|
}) => Promise<string | undefined>;
|
|
@@ -28,6 +28,14 @@ export async function loadFlagUrl(importFlags = () => import('@aceshooting/lyra-
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* One `Intl.DisplayNames` instance per locale, shared across every `<lyra-flag>` on the page.
|
|
33
|
+
* Constructing one is an ICU locale-data lookup, and `displayNameFor()` runs on every `render()`
|
|
34
|
+
* pass for a flag without an explicit `label` (e.g. toggling `round`), not just on country/language
|
|
35
|
+
* change -- multi-flag lists (language pickers, country lists) would otherwise pay this cost once
|
|
36
|
+
* per element per render. Mirrors the module-level caching `flagUrlResolver` already gets above.
|
|
37
|
+
*/
|
|
38
|
+
const displayNamesCache = new Map();
|
|
31
39
|
/**
|
|
32
40
|
* Resolves an ISO 3166-1 alpha-2 region code to a human-readable, localized
|
|
33
41
|
* display name (e.g. `'FR'` -> `'France'`) via `Intl.DisplayNames`, for use as
|
|
@@ -38,7 +46,13 @@ export async function loadFlagUrl(importFlags = () => import('@aceshooting/lyra-
|
|
|
38
46
|
*/
|
|
39
47
|
function displayNameFor(code) {
|
|
40
48
|
try {
|
|
41
|
-
|
|
49
|
+
const locale = navigator.language;
|
|
50
|
+
let dn = displayNamesCache.get(locale);
|
|
51
|
+
if (!dn) {
|
|
52
|
+
dn = new Intl.DisplayNames([locale], { type: 'region' });
|
|
53
|
+
displayNamesCache.set(locale, dn);
|
|
54
|
+
}
|
|
55
|
+
return dn.of(code.toUpperCase()) ?? code.toUpperCase();
|
|
42
56
|
}
|
|
43
57
|
catch {
|
|
44
58
|
return code.toUpperCase();
|
|
@@ -11,7 +11,7 @@ export const styles = css `
|
|
|
11
11
|
display: block;
|
|
12
12
|
block-size: 100%;
|
|
13
13
|
inline-size: 100%;
|
|
14
|
-
border-radius: var(--lyra-flag-radius,
|
|
14
|
+
border-radius: var(--lyra-flag-radius, calc(var(--lyra-radius) * 0.33));
|
|
15
15
|
box-shadow: 0 0 0 1px var(--lyra-color-border) inset;
|
|
16
16
|
object-fit: cover;
|
|
17
17
|
}
|
|
@@ -17,6 +17,9 @@ export declare const ALPHA2_RE: RegExp;
|
|
|
17
17
|
export declare const LANGUAGE_TO_COUNTRY: Record<string, string>;
|
|
18
18
|
/**
|
|
19
19
|
* Resolve a BCP-47-ish language tag to a flag country code.
|
|
20
|
-
* A region subtag wins (`en-US` → `us`); otherwise the base language is mapped.
|
|
20
|
+
* A region subtag wins (`en-US` → `us`); otherwise the base language is mapped. The region subtag
|
|
21
|
+
* isn't always in the second position -- a script subtag (e.g. `zh-Hant-TW`, ISO 15924, always 4
|
|
22
|
+
* letters) can sit between the base language and the region, so every subtag after the base is
|
|
23
|
+
* scanned for the first 2-letter alpha match rather than assuming it's always `parts[1]`.
|
|
21
24
|
*/
|
|
22
25
|
export declare function languageToCountry(language: string): string | undefined;
|
|
@@ -61,13 +61,16 @@ export const LANGUAGE_TO_COUNTRY = {
|
|
|
61
61
|
};
|
|
62
62
|
/**
|
|
63
63
|
* Resolve a BCP-47-ish language tag to a flag country code.
|
|
64
|
-
* A region subtag wins (`en-US` → `us`); otherwise the base language is mapped.
|
|
64
|
+
* A region subtag wins (`en-US` → `us`); otherwise the base language is mapped. The region subtag
|
|
65
|
+
* isn't always in the second position -- a script subtag (e.g. `zh-Hant-TW`, ISO 15924, always 4
|
|
66
|
+
* letters) can sit between the base language and the region, so every subtag after the base is
|
|
67
|
+
* scanned for the first 2-letter alpha match rather than assuming it's always `parts[1]`.
|
|
65
68
|
*/
|
|
66
69
|
export function languageToCountry(language) {
|
|
67
70
|
const parts = language.toLowerCase().split(/[-_]/);
|
|
68
71
|
const base = parts[0];
|
|
69
|
-
const region = parts
|
|
70
|
-
if (region
|
|
72
|
+
const region = parts.slice(1).find((part) => ALPHA2_RE.test(part));
|
|
73
|
+
if (region)
|
|
71
74
|
return region;
|
|
72
75
|
return LANGUAGE_TO_COUNTRY[base];
|
|
73
76
|
}
|
|
@@ -6,7 +6,11 @@ export type GaugeType = 'radial' | 'linear';
|
|
|
6
6
|
* generic gauge widget exists in Web Awesome.
|
|
7
7
|
*
|
|
8
8
|
* @customElement lyra-gauge
|
|
9
|
-
* @csspart base
|
|
9
|
+
* @csspart base - The root `<svg>`.
|
|
10
|
+
* @csspart track - The background track arc/line.
|
|
11
|
+
* @csspart fill - The animated fill arc/line.
|
|
12
|
+
* @csspart value - The value text.
|
|
13
|
+
* @csspart label - The label text.
|
|
10
14
|
*/
|
|
11
15
|
export declare class LyraGauge extends LyraElement {
|
|
12
16
|
static styles: import("lit").CSSResultGroup[];
|
|
@@ -18,7 +22,12 @@ export declare class LyraGauge extends LyraElement {
|
|
|
18
22
|
/** Imperative override for the displayed/announced value text, e.g. `'72°F'` for a raw `value` of `72`.
|
|
19
23
|
* An empty string is treated the same as unset and falls back to the numeric `value`. */
|
|
20
24
|
valueLabel?: string;
|
|
25
|
+
private get domain();
|
|
21
26
|
private get ratio();
|
|
27
|
+
/** The text rendered/announced for the current value: `valueLabel` when set,
|
|
28
|
+
* else the numeric `value`, blanked (like the ARIA attributes and fill) when
|
|
29
|
+
* `value` is non-finite (NaN/undefined/Infinity/-Infinity). */
|
|
30
|
+
private get displayText();
|
|
22
31
|
protected willUpdate(): void;
|
|
23
32
|
private renderRadial;
|
|
24
33
|
private renderLinear;
|
|
@@ -8,6 +8,7 @@ import { html, svg } from 'lit';
|
|
|
8
8
|
import { property } from 'lit/decorators.js';
|
|
9
9
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
10
|
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { finiteNumber } from '../../internal/numbers.js';
|
|
11
12
|
import { styles } from './gauge.styles.js';
|
|
12
13
|
// Radial gauge sweeps 270° (like a speedometer), leaving a 90° gap at the bottom.
|
|
13
14
|
const SWEEP_DEG = 270;
|
|
@@ -44,7 +45,11 @@ const RADIAL_ARC_LENGTH = (SWEEP_DEG / 360) * 2 * Math.PI * RADIUS;
|
|
|
44
45
|
* generic gauge widget exists in Web Awesome.
|
|
45
46
|
*
|
|
46
47
|
* @customElement lyra-gauge
|
|
47
|
-
* @csspart base
|
|
48
|
+
* @csspart base - The root `<svg>`.
|
|
49
|
+
* @csspart track - The background track arc/line.
|
|
50
|
+
* @csspart fill - The animated fill arc/line.
|
|
51
|
+
* @csspart value - The value text.
|
|
52
|
+
* @csspart label - The label text.
|
|
48
53
|
*/
|
|
49
54
|
export class LyraGauge extends LyraElement {
|
|
50
55
|
constructor() {
|
|
@@ -56,23 +61,37 @@ export class LyraGauge extends LyraElement {
|
|
|
56
61
|
this.label = '';
|
|
57
62
|
}
|
|
58
63
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
64
|
+
// Normalizes a reversed min > max domain by swapping lo/hi, but only once
|
|
65
|
+
// both bounds are finite -- shared by `ratio` (fill geometry) and
|
|
66
|
+
// `willUpdate` (aria-value* trio) so the two can never silently diverge.
|
|
67
|
+
get domain() {
|
|
68
|
+
const min = finiteNumber(this.min, 0);
|
|
69
|
+
const max = finiteNumber(this.max, 100);
|
|
70
|
+
return { lo: Math.min(min, max), hi: Math.max(min, max) };
|
|
71
|
+
}
|
|
59
72
|
get ratio() {
|
|
60
|
-
if (!Number.isFinite(this.value)
|
|
73
|
+
if (!Number.isFinite(this.value))
|
|
74
|
+
return 0;
|
|
75
|
+
const { lo, hi } = this.domain;
|
|
76
|
+
if (!Number.isFinite(lo) || !Number.isFinite(hi))
|
|
61
77
|
return 0;
|
|
62
|
-
const lo = Math.min(this.min, this.max);
|
|
63
|
-
const hi = Math.max(this.min, this.max);
|
|
64
78
|
const span = hi - lo || 1;
|
|
65
79
|
return Math.min(1, Math.max(0, (this.value - lo) / span));
|
|
66
80
|
}
|
|
81
|
+
/** The text rendered/announced for the current value: `valueLabel` when set,
|
|
82
|
+
* else the numeric `value`, blanked (like the ARIA attributes and fill) when
|
|
83
|
+
* `value` is non-finite (NaN/undefined/Infinity/-Infinity). */
|
|
84
|
+
get displayText() {
|
|
85
|
+
return this.valueLabel || (!Number.isFinite(this.value) ? '' : String(this.value));
|
|
86
|
+
}
|
|
67
87
|
willUpdate() {
|
|
68
88
|
this.setAttribute('role', 'meter');
|
|
69
|
-
const finiteTrio = Number.isFinite(this.value) && Number.isFinite(this.min) && Number.isFinite(this.max);
|
|
70
89
|
// Normalize a reversed min > max domain the same way `ratio` does, so the
|
|
71
90
|
// announced aria-value* trio always agrees with the visual fill instead
|
|
72
91
|
// of aria-valuenow pinning to one bound regardless of `value` (and
|
|
73
92
|
// aria-valuemin/valuemax reporting an inverted, invalid ARIA range).
|
|
74
|
-
const lo
|
|
75
|
-
const
|
|
93
|
+
const { lo, hi } = this.domain;
|
|
94
|
+
const finiteTrio = Number.isFinite(this.value) && Number.isFinite(lo) && Number.isFinite(hi);
|
|
76
95
|
if (finiteTrio) {
|
|
77
96
|
const clamped = Math.min(hi, Math.max(lo, this.value));
|
|
78
97
|
this.setAttribute('aria-valuenow', String(clamped));
|
|
@@ -98,7 +117,7 @@ export class LyraGauge extends LyraElement {
|
|
|
98
117
|
this.removeAttribute('aria-valuetext');
|
|
99
118
|
}
|
|
100
119
|
renderRadial() {
|
|
101
|
-
const text = this.
|
|
120
|
+
const text = this.displayText;
|
|
102
121
|
// Dashoffset counts down from the full arc length (nothing revealed) to 0
|
|
103
122
|
// (whole sweep revealed) as ratio goes 0 -> 1 — the classic "draw an SVG
|
|
104
123
|
// path" technique, which transitions smoothly via plain CSS.
|
|
@@ -117,7 +136,7 @@ export class LyraGauge extends LyraElement {
|
|
|
117
136
|
</svg>`;
|
|
118
137
|
}
|
|
119
138
|
renderLinear() {
|
|
120
|
-
const text = this.
|
|
139
|
+
const text = this.displayText;
|
|
121
140
|
const dashoffset = LINEAR_LENGTH * (1 - this.ratio);
|
|
122
141
|
return html `<svg part="base" viewBox="0 0 100 20" preserveAspectRatio="none">
|
|
123
142
|
<line part="track" x1="0" y1=${LINEAR_BAR_Y} x2="100" y2=${LINEAR_BAR_Y} stroke-width=${LINEAR_STROKE}></line>
|