@aceshooting/lyra-ui 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +77 -6
- package/custom-elements.json +12015 -0
- package/dist/components/chart/bar-chart.d.ts +14 -0
- package/dist/components/chart/bar-chart.js +14 -0
- package/dist/components/chart/box-plot.d.ts +46 -0
- package/dist/components/chart/box-plot.js +145 -0
- package/dist/components/chart/box-plot.styles.d.ts +1 -0
- package/dist/components/chart/box-plot.styles.js +22 -0
- package/dist/components/chart/bubble-chart.d.ts +19 -0
- package/dist/components/chart/bubble-chart.js +18 -0
- package/dist/components/chart/chart-loader.d.ts +8 -0
- package/dist/components/chart/chart-loader.js +26 -0
- package/dist/components/chart/chart.d.ts +91 -0
- package/dist/components/chart/chart.js +275 -0
- package/dist/components/chart/chart.styles.d.ts +1 -0
- package/dist/components/chart/chart.styles.js +33 -0
- package/dist/components/chart/doughnut-chart.d.ts +15 -0
- package/dist/components/chart/doughnut-chart.js +14 -0
- package/dist/components/chart/histogram-bin.d.ts +6 -0
- package/dist/components/chart/histogram-bin.js +19 -0
- package/dist/components/chart/histogram.d.ts +20 -0
- package/dist/components/chart/histogram.js +70 -0
- package/dist/components/chart/histogram.styles.d.ts +1 -0
- package/dist/components/chart/histogram.styles.js +1 -0
- package/dist/components/chart/line-chart.d.ts +14 -0
- package/dist/components/chart/line-chart.js +14 -0
- package/dist/components/chart/pie-chart.d.ts +16 -0
- package/dist/components/chart/pie-chart.js +15 -0
- package/dist/components/chart/polar-area-chart.d.ts +15 -0
- package/dist/components/chart/polar-area-chart.js +14 -0
- package/dist/components/chart/radar-chart.d.ts +15 -0
- package/dist/components/chart/radar-chart.js +14 -0
- package/dist/components/chart/scatter-chart.d.ts +15 -0
- package/dist/components/chart/scatter-chart.js +15 -0
- package/dist/components/combobox/combobox.d.ts +16 -0
- package/dist/components/combobox/combobox.js +113 -6
- package/dist/components/combobox/combobox.styles.js +68 -7
- package/dist/components/combobox/option.d.ts +2 -2
- package/dist/components/combobox/option.js +11 -7
- package/dist/components/date-picker/date-input.d.ts +12 -1
- package/dist/components/date-picker/date-input.js +68 -6
- package/dist/components/date-picker/date-input.styles.js +58 -6
- package/dist/components/date-picker/date-picker.js +5 -2
- package/dist/components/date-picker/date-picker.styles.js +9 -2
- package/dist/components/empty/empty.d.ts +1 -0
- package/dist/components/empty/empty.js +14 -2
- package/dist/components/export-button/csv.d.ts +10 -0
- package/dist/components/export-button/csv.js +28 -0
- package/dist/components/export-button/export-button.d.ts +41 -0
- package/dist/components/export-button/export-button.js +151 -0
- package/dist/components/export-button/export-button.styles.d.ts +1 -0
- package/dist/components/export-button/export-button.styles.js +79 -0
- package/dist/components/file-input/accept.d.ts +14 -0
- package/dist/components/file-input/accept.js +30 -0
- package/dist/components/file-input/file-input.d.ts +47 -0
- package/dist/components/file-input/file-input.js +182 -0
- package/dist/components/file-input/file-input.styles.d.ts +1 -0
- package/dist/components/file-input/file-input.styles.js +44 -0
- package/dist/components/flag/flag.d.ts +4 -0
- package/dist/components/flag/flag.js +24 -1
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/gauge/gauge.d.ts +29 -0
- package/dist/components/gauge/gauge.js +133 -0
- package/dist/components/gauge/gauge.styles.d.ts +1 -0
- package/dist/components/gauge/gauge.styles.js +60 -0
- package/dist/components/graph/graph.d.ts +68 -0
- package/dist/components/graph/graph.js +276 -0
- package/dist/components/graph/graph.styles.d.ts +1 -0
- package/dist/components/graph/graph.styles.js +34 -0
- package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
- package/dist/components/heatmap/heatmap-scale.js +17 -0
- package/dist/components/heatmap/heatmap.d.ts +66 -0
- package/dist/components/heatmap/heatmap.js +252 -0
- package/dist/components/heatmap/heatmap.styles.d.ts +1 -0
- package/dist/components/heatmap/heatmap.styles.js +39 -0
- package/dist/components/map/map-loader.d.ts +8 -0
- package/dist/components/map/map-loader.js +17 -0
- package/dist/components/map/map.d.ts +49 -0
- package/dist/components/map/map.js +189 -0
- package/dist/components/map/map.styles.d.ts +1 -0
- package/dist/components/map/map.styles.js +48 -0
- package/dist/components/playback/playback.d.ts +51 -0
- package/dist/components/playback/playback.js +146 -0
- package/dist/components/playback/playback.styles.d.ts +1 -0
- package/dist/components/playback/playback.styles.js +40 -0
- package/dist/components/split/split.d.ts +41 -0
- package/dist/components/split/split.js +171 -0
- package/dist/components/split/split.styles.d.ts +1 -0
- package/dist/components/split/split.styles.js +51 -0
- package/dist/components/stat/stat.d.ts +9 -0
- package/dist/components/stat/stat.js +52 -7
- package/dist/components/stat/stat.styles.js +15 -5
- package/dist/components/table/table.d.ts +18 -6
- package/dist/components/table/table.js +76 -24
- package/dist/components/table/table.styles.js +32 -0
- package/dist/components/time-range/time-range.d.ts +40 -0
- package/dist/components/time-range/time-range.js +219 -0
- package/dist/components/time-range/time-range.styles.d.ts +1 -0
- package/dist/components/time-range/time-range.styles.js +78 -0
- package/dist/components/toast/toast-item.js +10 -2
- package/dist/components/toast/toast-item.styles.js +13 -4
- package/dist/components/toast/toast.styles.js +0 -1
- package/dist/components/tree/tree-node.d.ts +50 -0
- package/dist/components/tree/tree-node.js +196 -0
- package/dist/components/tree/tree.d.ts +65 -0
- package/dist/components/tree/tree.js +227 -0
- package/dist/components/tree/tree.styles.d.ts +1 -0
- package/dist/components/tree/tree.styles.js +13 -0
- package/dist/internal/form-associated.js +49 -1
- package/dist/internal/icons.d.ts +11 -0
- package/dist/internal/icons.js +64 -0
- package/dist/internal/tokens.styles.js +41 -1
- package/dist/lyra.d.ts +67 -0
- package/dist/lyra.js +55 -0
- package/llms-full.txt +1425 -0
- package/llms.txt +47 -0
- package/package.json +53 -4
|
@@ -9,9 +9,19 @@ export const styles = css `
|
|
|
9
9
|
font-size: 0.875rem;
|
|
10
10
|
font-weight: 600;
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
/* :empty never matches here -- the part always contains a literal slot
|
|
13
|
+
child element regardless of assigned/text content -- so real emptiness
|
|
14
|
+
is tracked in JS (hasLabelSlot) and reflected via the hidden attribute
|
|
15
|
+
instead (same fix as [part='hint']/[part='error'] below). Without this,
|
|
16
|
+
the required-asterisk ::after below (which attaches to this box)
|
|
17
|
+
renders a stray ' *' with nothing before it whenever label is unset. */
|
|
18
|
+
[part='form-control-label'][hidden] {
|
|
13
19
|
display: none;
|
|
14
20
|
}
|
|
21
|
+
:host([required]) [part='form-control-label']::after {
|
|
22
|
+
content: ' *';
|
|
23
|
+
color: var(--lyra-color-danger);
|
|
24
|
+
}
|
|
15
25
|
[part='input-wrapper'] {
|
|
16
26
|
display: flex;
|
|
17
27
|
align-items: center;
|
|
@@ -26,7 +36,8 @@ export const styles = css `
|
|
|
26
36
|
border-color: var(--lyra-color-brand);
|
|
27
37
|
}
|
|
28
38
|
:host([disabled]) [part='input-wrapper'] {
|
|
29
|
-
opacity:
|
|
39
|
+
opacity: var(--lyra-opacity-disabled);
|
|
40
|
+
cursor: not-allowed;
|
|
30
41
|
}
|
|
31
42
|
[part='input'] {
|
|
32
43
|
flex: 1 1 auto;
|
|
@@ -40,28 +51,69 @@ export const styles = css `
|
|
|
40
51
|
[part='clear-button'],
|
|
41
52
|
[part='expand-button'] {
|
|
42
53
|
flex: 0 0 auto;
|
|
54
|
+
display: inline-flex;
|
|
55
|
+
align-items: center;
|
|
56
|
+
justify-content: center;
|
|
43
57
|
border: none;
|
|
44
58
|
background: none;
|
|
45
59
|
cursor: pointer;
|
|
46
60
|
color: var(--lyra-color-text-quiet);
|
|
47
|
-
padding:
|
|
61
|
+
padding: var(--lyra-space-xs);
|
|
62
|
+
/* Real touch target in *both* dimensions (WCAG 2.2 SC 2.5.8 needs
|
|
63
|
+
24x24 CSS px, not just height — a design-review fix that only set
|
|
64
|
+
min-block-size left these buttons 24px tall but narrower than that).
|
|
65
|
+
The row has no explicit min-block-size of its own (unlike combobox's
|
|
66
|
+
[part=combobox]), so it can grow to fit the full touch target. */
|
|
67
|
+
min-inline-size: var(--lyra-icon-button-size);
|
|
68
|
+
min-block-size: var(--lyra-icon-button-size);
|
|
48
69
|
line-height: 1;
|
|
49
70
|
font-size: 1rem;
|
|
50
71
|
}
|
|
72
|
+
[part='clear-button']:focus-visible,
|
|
73
|
+
[part='expand-button']:focus-visible {
|
|
74
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
75
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
76
|
+
}
|
|
51
77
|
[part='popup'] {
|
|
52
|
-
display: none;
|
|
53
78
|
position: fixed;
|
|
54
79
|
z-index: 900;
|
|
80
|
+
max-inline-size: min(92vw, 28rem);
|
|
81
|
+
visibility: hidden;
|
|
82
|
+
opacity: 0;
|
|
83
|
+
transform: translateY(-0.25rem);
|
|
84
|
+
transition:
|
|
85
|
+
opacity var(--lyra-transition-fast),
|
|
86
|
+
transform var(--lyra-transition-fast),
|
|
87
|
+
visibility var(--lyra-transition-fast);
|
|
55
88
|
}
|
|
56
89
|
:host([open]) [part='popup'] {
|
|
57
|
-
|
|
90
|
+
visibility: visible;
|
|
91
|
+
opacity: 1;
|
|
92
|
+
transform: translateY(0);
|
|
93
|
+
}
|
|
94
|
+
@media (prefers-reduced-motion: reduce) {
|
|
95
|
+
[part='popup'] {
|
|
96
|
+
transition: none !important;
|
|
97
|
+
}
|
|
58
98
|
}
|
|
59
99
|
[part='hint'] {
|
|
60
100
|
margin-block-start: var(--lyra-space-xs);
|
|
61
101
|
font-size: 0.8125rem;
|
|
62
102
|
color: var(--lyra-color-text-quiet);
|
|
63
103
|
}
|
|
64
|
-
|
|
104
|
+
/* :empty never matches here -- the part always contains a literal
|
|
105
|
+
slot child element regardless of assigned/text content -- so real
|
|
106
|
+
emptiness is tracked in JS (hasHintSlot/hasErrorSlot) and reflected via
|
|
107
|
+
the hidden attribute instead (same fix as lyra-stat's icon/caption). */
|
|
108
|
+
[part='hint'][hidden] {
|
|
109
|
+
display: none;
|
|
110
|
+
}
|
|
111
|
+
[part='error'] {
|
|
112
|
+
margin-block-start: var(--lyra-space-xs);
|
|
113
|
+
font-size: 0.8125rem;
|
|
114
|
+
color: var(--lyra-color-danger);
|
|
115
|
+
}
|
|
116
|
+
[part='error'][hidden] {
|
|
65
117
|
display: none;
|
|
66
118
|
}
|
|
67
119
|
`;
|
|
@@ -8,6 +8,7 @@ import { html } from 'lit';
|
|
|
8
8
|
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
|
+
import { chevronIcon } from '../../internal/icons.js';
|
|
11
12
|
import { styles } from './date-picker.styles.js';
|
|
12
13
|
import { monthMatrix, weekdayLabels, monthTitle, formatISO, parseISO, isSameDay, addMonths, resolveFirstDayOfWeek, } from './calendar-core.js';
|
|
13
14
|
/**
|
|
@@ -241,12 +242,14 @@ export class LyraDatePicker extends LyraElement {
|
|
|
241
242
|
<div part="header">
|
|
242
243
|
${isFirst
|
|
243
244
|
? html `<button part="previous" type="button" aria-label="Previous month" @click=${() => this.nav(-1)}>
|
|
244
|
-
|
|
245
|
+
${chevronIcon()}
|
|
245
246
|
</button>`
|
|
246
247
|
: html `<span></span>`}
|
|
247
248
|
<div part="title">${monthTitle(year, month, this.locale)}</div>
|
|
248
249
|
${isLast
|
|
249
|
-
? html `<button part="next" type="button" aria-label="Next month" @click=${() => this.nav(1)}
|
|
250
|
+
? html `<button part="next" type="button" aria-label="Next month" @click=${() => this.nav(1)}>
|
|
251
|
+
${chevronIcon()}
|
|
252
|
+
</button>`
|
|
250
253
|
: html `<span></span>`}
|
|
251
254
|
</div>
|
|
252
255
|
<div part="weekdays">${labels.map((l) => html `<span part="weekday">${l}</span>`)}</div>
|
|
@@ -38,6 +38,9 @@ export const styles = css `
|
|
|
38
38
|
[part='next']:hover {
|
|
39
39
|
background: var(--lyra-color-brand-quiet);
|
|
40
40
|
}
|
|
41
|
+
[part='previous'] svg {
|
|
42
|
+
transform: rotate(180deg);
|
|
43
|
+
}
|
|
41
44
|
[part='weekdays'] {
|
|
42
45
|
display: grid;
|
|
43
46
|
grid-template-columns: repeat(7, var(--lyra-cell-size));
|
|
@@ -71,6 +74,10 @@ export const styles = css `
|
|
|
71
74
|
[part~='day-outside'] {
|
|
72
75
|
color: var(--lyra-color-text-quiet);
|
|
73
76
|
}
|
|
77
|
+
[part~='day-outside'][part~='day-range-inner'],
|
|
78
|
+
[part~='day-outside'][part~='day-range-inner']:hover {
|
|
79
|
+
color: var(--lyra-color-text);
|
|
80
|
+
}
|
|
74
81
|
[part='day-placeholder'] {
|
|
75
82
|
inline-size: var(--lyra-cell-size);
|
|
76
83
|
block-size: var(--lyra-cell-size);
|
|
@@ -95,7 +102,7 @@ export const styles = css `
|
|
|
95
102
|
cursor: not-allowed;
|
|
96
103
|
}
|
|
97
104
|
[part~='day']:focus-visible {
|
|
98
|
-
outline:
|
|
99
|
-
outline-offset:
|
|
105
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
106
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
100
107
|
}
|
|
101
108
|
`;
|
|
@@ -38,9 +38,21 @@ export class LyraEmpty extends LyraElement {
|
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
41
|
+
willUpdate() {
|
|
42
|
+
// Set from light-DOM children before the first render so the initial
|
|
43
|
+
// paint is already correct — setting `hasIcon`/`hasActions` from
|
|
44
|
+
// `firstUpdated` (after the update completes) would schedule a second,
|
|
45
|
+
// wasted update (Lit's dev-mode "change-in-update" warning).
|
|
46
|
+
if (!this.hasUpdated) {
|
|
47
|
+
this.hasIcon = Array.from(this.children).some((el) => !el.hasAttribute('slot'));
|
|
48
|
+
this.hasActions = Array.from(this.children).some((el) => el.getAttribute('slot') === 'actions');
|
|
49
|
+
}
|
|
50
|
+
}
|
|
41
51
|
firstUpdated() {
|
|
42
|
-
//
|
|
43
|
-
//
|
|
52
|
+
// Fallback reconciliation against the fully-resolved slot assignment
|
|
53
|
+
// (handles slot-forwarding and any browser where `slotchange` doesn't
|
|
54
|
+
// fire for content present at parse/upgrade time). A no-op in the
|
|
55
|
+
// common case since `willUpdate` already set the correct value above.
|
|
44
56
|
this.checkIconSlot(this.shadowRoot.querySelector('slot:not([name])'));
|
|
45
57
|
this.checkActionsSlot(this.shadowRoot.querySelector('slot[name="actions"]'));
|
|
46
58
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Escapes a CSV field: quotes as needed, guards against formula injection. */
|
|
2
|
+
export declare function escapeCsvField(value: unknown): string;
|
|
3
|
+
export interface CsvColumn {
|
|
4
|
+
key: string;
|
|
5
|
+
label: string;
|
|
6
|
+
}
|
|
7
|
+
/** Builds a CRLF-joined CSV string with a header row. */
|
|
8
|
+
export declare function buildCsv(rows: Record<string, unknown>[], columns: CsvColumn[]): string;
|
|
9
|
+
/** Triggers a browser download of `content` as `filename`. */
|
|
10
|
+
export declare function downloadBlob(content: string, filename: string, mime: string): void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
const UNSAFE_LEADING = /^[=+\-@\t\r]/;
|
|
2
|
+
const NEEDS_QUOTING = /[",\n]/;
|
|
3
|
+
/** Escapes a CSV field: quotes as needed, guards against formula injection. */
|
|
4
|
+
export function escapeCsvField(value) {
|
|
5
|
+
let s = value == null ? '' : String(value);
|
|
6
|
+
if (UNSAFE_LEADING.test(s))
|
|
7
|
+
s = `'${s}`;
|
|
8
|
+
if (NEEDS_QUOTING.test(s))
|
|
9
|
+
s = `"${s.replace(/"/g, '""')}"`;
|
|
10
|
+
return s;
|
|
11
|
+
}
|
|
12
|
+
/** Builds a CRLF-joined CSV string with a header row. */
|
|
13
|
+
export function buildCsv(rows, columns) {
|
|
14
|
+
const header = columns.map((c) => escapeCsvField(c.label)).join(',');
|
|
15
|
+
const body = rows.map((row) => columns.map((c) => escapeCsvField(row[c.key])).join(','));
|
|
16
|
+
return [header, ...body].join('\r\n');
|
|
17
|
+
}
|
|
18
|
+
/** Triggers a browser download of `content` as `filename`. */
|
|
19
|
+
export function downloadBlob(content, filename, mime) {
|
|
20
|
+
const blob = new Blob([content], { type: mime });
|
|
21
|
+
const url = URL.createObjectURL(blob);
|
|
22
|
+
const a = document.createElement('a');
|
|
23
|
+
a.href = url;
|
|
24
|
+
a.download = filename;
|
|
25
|
+
a.click();
|
|
26
|
+
// Deferred revoke: Safari can cancel the download if the URL is revoked immediately.
|
|
27
|
+
setTimeout(() => URL.revokeObjectURL(url), 5000);
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
import { type CsvColumn } from './csv.js';
|
|
4
|
+
export type ExportFormat = 'csv' | 'json';
|
|
5
|
+
/**
|
|
6
|
+
* `<lyra-export-button>` — a CSV/JSON download button, single-format or a
|
|
7
|
+
* format-choice menu. First-party invention; consolidates ad-hoc "export CSV"
|
|
8
|
+
* buttons duplicated across surveyed repos.
|
|
9
|
+
*
|
|
10
|
+
* @customElement lyra-export-button
|
|
11
|
+
* @event lyra-export - `detail: { format }`, cancelable — call `preventDefault()`
|
|
12
|
+
* to substitute the built-in client-side download with a server-generated one.
|
|
13
|
+
* @event lyra-export-complete - Fired after a non-cancelled download completes.
|
|
14
|
+
* @csspart trigger, menu, menu-item
|
|
15
|
+
*/
|
|
16
|
+
export declare class LyraExportButton extends LyraElement {
|
|
17
|
+
static styles: import("lit").CSSResultGroup[];
|
|
18
|
+
rows: Record<string, unknown>[];
|
|
19
|
+
columns: CsvColumn[];
|
|
20
|
+
filename: string;
|
|
21
|
+
formats: ExportFormat[];
|
|
22
|
+
disabled: boolean;
|
|
23
|
+
label: string;
|
|
24
|
+
open: boolean;
|
|
25
|
+
private cleanup?;
|
|
26
|
+
disconnectedCallback(): void;
|
|
27
|
+
private onDocPointer;
|
|
28
|
+
private openMenu;
|
|
29
|
+
private closeMenu;
|
|
30
|
+
private onKeyDown;
|
|
31
|
+
protected firstUpdated(): void;
|
|
32
|
+
protected updated(changed: PropertyValues): void;
|
|
33
|
+
private doExport;
|
|
34
|
+
private onTriggerClick;
|
|
35
|
+
render(): TemplateResult;
|
|
36
|
+
}
|
|
37
|
+
declare global {
|
|
38
|
+
interface HTMLElementTagNameMap {
|
|
39
|
+
'lyra-export-button': LyraExportButton;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
2
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
3
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
4
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
5
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
|
+
};
|
|
7
|
+
import { html, nothing } from 'lit';
|
|
8
|
+
import { property } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { place } from '../../internal/positioner.js';
|
|
12
|
+
import { buildCsv, downloadBlob } from './csv.js';
|
|
13
|
+
import { styles } from './export-button.styles.js';
|
|
14
|
+
/**
|
|
15
|
+
* `<lyra-export-button>` — a CSV/JSON download button, single-format or a
|
|
16
|
+
* format-choice menu. First-party invention; consolidates ad-hoc "export CSV"
|
|
17
|
+
* buttons duplicated across surveyed repos.
|
|
18
|
+
*
|
|
19
|
+
* @customElement lyra-export-button
|
|
20
|
+
* @event lyra-export - `detail: { format }`, cancelable — call `preventDefault()`
|
|
21
|
+
* to substitute the built-in client-side download with a server-generated one.
|
|
22
|
+
* @event lyra-export-complete - Fired after a non-cancelled download completes.
|
|
23
|
+
* @csspart trigger, menu, menu-item
|
|
24
|
+
*/
|
|
25
|
+
export class LyraExportButton extends LyraElement {
|
|
26
|
+
constructor() {
|
|
27
|
+
super(...arguments);
|
|
28
|
+
this.rows = [];
|
|
29
|
+
this.columns = [];
|
|
30
|
+
this.filename = 'export';
|
|
31
|
+
this.formats = ['csv'];
|
|
32
|
+
this.disabled = false;
|
|
33
|
+
this.label = 'Export';
|
|
34
|
+
this.open = false;
|
|
35
|
+
this.onDocPointer = (e) => {
|
|
36
|
+
if (!e.composedPath().includes(this))
|
|
37
|
+
this.closeMenu();
|
|
38
|
+
};
|
|
39
|
+
this.onKeyDown = (e) => {
|
|
40
|
+
if (e.key === 'Escape' && this.open) {
|
|
41
|
+
e.preventDefault();
|
|
42
|
+
this.closeMenu();
|
|
43
|
+
this.renderRoot.querySelector('[part="trigger"]')?.focus();
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
48
|
+
disconnectedCallback() {
|
|
49
|
+
super.disconnectedCallback();
|
|
50
|
+
this.cleanup?.();
|
|
51
|
+
document.removeEventListener('pointerdown', this.onDocPointer);
|
|
52
|
+
}
|
|
53
|
+
openMenu() {
|
|
54
|
+
if (this.open)
|
|
55
|
+
return;
|
|
56
|
+
this.open = true;
|
|
57
|
+
document.addEventListener('pointerdown', this.onDocPointer);
|
|
58
|
+
}
|
|
59
|
+
closeMenu() {
|
|
60
|
+
if (!this.open)
|
|
61
|
+
return;
|
|
62
|
+
this.open = false;
|
|
63
|
+
document.removeEventListener('pointerdown', this.onDocPointer);
|
|
64
|
+
}
|
|
65
|
+
firstUpdated() {
|
|
66
|
+
// Single delegated listener catches Escape from the trigger button or
|
|
67
|
+
// any menu-item inside this shadow root.
|
|
68
|
+
this.renderRoot.addEventListener('keydown', this.onKeyDown);
|
|
69
|
+
}
|
|
70
|
+
updated(changed) {
|
|
71
|
+
if (changed.has('open')) {
|
|
72
|
+
this.cleanup?.();
|
|
73
|
+
this.cleanup = undefined;
|
|
74
|
+
if (this.open) {
|
|
75
|
+
const anchor = this.renderRoot.querySelector('[part="trigger"]');
|
|
76
|
+
const menu = this.renderRoot.querySelector('[part="menu"]');
|
|
77
|
+
if (anchor && menu)
|
|
78
|
+
this.cleanup = place(anchor, menu);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
doExport(format) {
|
|
83
|
+
this.closeMenu();
|
|
84
|
+
const ev = this.emit('lyra-export', { format });
|
|
85
|
+
if (ev.defaultPrevented)
|
|
86
|
+
return;
|
|
87
|
+
if (format === 'csv') {
|
|
88
|
+
downloadBlob(buildCsv(this.rows, this.columns), `${this.filename}.csv`, 'text/csv;charset=utf-8;');
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
downloadBlob(JSON.stringify(this.rows, null, 2), `${this.filename}.json`, 'application/json');
|
|
92
|
+
}
|
|
93
|
+
this.emit('lyra-export-complete', { format });
|
|
94
|
+
}
|
|
95
|
+
onTriggerClick() {
|
|
96
|
+
if (this.disabled)
|
|
97
|
+
return;
|
|
98
|
+
if (this.formats.length <= 1)
|
|
99
|
+
this.doExport(this.formats[0] ?? 'csv');
|
|
100
|
+
else
|
|
101
|
+
this.open ? this.closeMenu() : this.openMenu();
|
|
102
|
+
}
|
|
103
|
+
render() {
|
|
104
|
+
return html `
|
|
105
|
+
<button
|
|
106
|
+
part="trigger"
|
|
107
|
+
type="button"
|
|
108
|
+
?disabled=${this.disabled}
|
|
109
|
+
aria-haspopup=${this.formats.length > 1 ? 'menu' : nothing}
|
|
110
|
+
aria-expanded=${this.formats.length > 1 ? (this.open ? 'true' : 'false') : nothing}
|
|
111
|
+
@click=${() => this.onTriggerClick()}
|
|
112
|
+
>
|
|
113
|
+
${this.label}
|
|
114
|
+
</button>
|
|
115
|
+
${this.formats.length > 1
|
|
116
|
+
? html `<div part="menu" role="menu">
|
|
117
|
+
${this.formats.map((f) => html `<button
|
|
118
|
+
part="menu-item"
|
|
119
|
+
role="menuitem"
|
|
120
|
+
type="button"
|
|
121
|
+
@click=${() => this.doExport(f)}
|
|
122
|
+
>
|
|
123
|
+
${f.toUpperCase()}
|
|
124
|
+
</button>`)}
|
|
125
|
+
</div>`
|
|
126
|
+
: nothing}
|
|
127
|
+
`;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
__decorate([
|
|
131
|
+
property({ attribute: false })
|
|
132
|
+
], LyraExportButton.prototype, "rows", void 0);
|
|
133
|
+
__decorate([
|
|
134
|
+
property({ attribute: false })
|
|
135
|
+
], LyraExportButton.prototype, "columns", void 0);
|
|
136
|
+
__decorate([
|
|
137
|
+
property()
|
|
138
|
+
], LyraExportButton.prototype, "filename", void 0);
|
|
139
|
+
__decorate([
|
|
140
|
+
property({ attribute: false })
|
|
141
|
+
], LyraExportButton.prototype, "formats", void 0);
|
|
142
|
+
__decorate([
|
|
143
|
+
property({ type: Boolean, reflect: true })
|
|
144
|
+
], LyraExportButton.prototype, "disabled", void 0);
|
|
145
|
+
__decorate([
|
|
146
|
+
property()
|
|
147
|
+
], LyraExportButton.prototype, "label", void 0);
|
|
148
|
+
__decorate([
|
|
149
|
+
property({ type: Boolean, reflect: true })
|
|
150
|
+
], LyraExportButton.prototype, "open", void 0);
|
|
151
|
+
defineElement('export-button', LyraExportButton);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: inline-block;
|
|
5
|
+
position: relative;
|
|
6
|
+
}
|
|
7
|
+
[part='trigger'] {
|
|
8
|
+
display: inline-flex;
|
|
9
|
+
align-items: center;
|
|
10
|
+
gap: var(--lyra-space-xs);
|
|
11
|
+
padding: var(--lyra-space-xs) var(--lyra-space-m);
|
|
12
|
+
border: 1px solid var(--lyra-color-border);
|
|
13
|
+
border-radius: var(--lyra-radius);
|
|
14
|
+
background: var(--lyra-color-surface);
|
|
15
|
+
color: var(--lyra-color-text);
|
|
16
|
+
font: inherit;
|
|
17
|
+
cursor: pointer;
|
|
18
|
+
}
|
|
19
|
+
[part='trigger']:hover:not(:disabled) {
|
|
20
|
+
border-color: var(--lyra-color-brand);
|
|
21
|
+
}
|
|
22
|
+
[part='trigger']:disabled {
|
|
23
|
+
opacity: var(--lyra-opacity-disabled);
|
|
24
|
+
cursor: not-allowed;
|
|
25
|
+
}
|
|
26
|
+
[part='trigger']:focus-visible {
|
|
27
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
28
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
29
|
+
}
|
|
30
|
+
[part='menu'] {
|
|
31
|
+
/* Closed state: invisible + slightly raised. visibility (not
|
|
32
|
+
display:none) so opacity/transform can actually transition; hit-testing
|
|
33
|
+
and a11y exposure stay off since this part is already position:fixed. */
|
|
34
|
+
visibility: hidden;
|
|
35
|
+
position: fixed;
|
|
36
|
+
z-index: 900;
|
|
37
|
+
min-inline-size: 8rem;
|
|
38
|
+
padding: var(--lyra-space-xs);
|
|
39
|
+
background: var(--lyra-color-surface);
|
|
40
|
+
border: 1px solid var(--lyra-color-border);
|
|
41
|
+
border-radius: var(--lyra-radius);
|
|
42
|
+
box-shadow: var(--lyra-shadow);
|
|
43
|
+
opacity: 0;
|
|
44
|
+
transform: translateY(-4px);
|
|
45
|
+
transition:
|
|
46
|
+
opacity var(--lyra-transition-fast),
|
|
47
|
+
transform var(--lyra-transition-fast),
|
|
48
|
+
visibility var(--lyra-transition-fast);
|
|
49
|
+
}
|
|
50
|
+
:host([open]) [part='menu'] {
|
|
51
|
+
visibility: visible;
|
|
52
|
+
opacity: 1;
|
|
53
|
+
transform: none;
|
|
54
|
+
}
|
|
55
|
+
@media (prefers-reduced-motion: reduce) {
|
|
56
|
+
[part='menu'] {
|
|
57
|
+
transition: none;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
[part='menu-item'] {
|
|
61
|
+
display: block;
|
|
62
|
+
inline-size: 100%;
|
|
63
|
+
text-align: start;
|
|
64
|
+
padding: var(--lyra-space-xs) var(--lyra-space-s);
|
|
65
|
+
border: none;
|
|
66
|
+
background: none;
|
|
67
|
+
color: inherit;
|
|
68
|
+
font: inherit;
|
|
69
|
+
cursor: pointer;
|
|
70
|
+
border-radius: var(--lyra-radius);
|
|
71
|
+
}
|
|
72
|
+
[part='menu-item']:hover {
|
|
73
|
+
background: var(--lyra-color-brand-quiet);
|
|
74
|
+
}
|
|
75
|
+
[part='menu-item']:focus-visible {
|
|
76
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
77
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
78
|
+
}
|
|
79
|
+
`;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a native-`accept`-style string (`".csv,.xlsx"`, `"text/csv"`,
|
|
3
|
+
* `"image/*"`, or any mix, comma-separated) and reports whether `file`
|
|
4
|
+
* matches it — the same three forms the browser's own file picker accepts,
|
|
5
|
+
* now also enforced on the drag-drop path (2026-07-10 audit,
|
|
6
|
+
* "map-file-input" §lyra-file-input, High: previously `accept` only
|
|
7
|
+
* constrained the native picker dialog and had no effect on drop).
|
|
8
|
+
*
|
|
9
|
+
* `file` may be a `DataTransferItem` cast as `File` (the dragenter-preview
|
|
10
|
+
* call site, before the drop payload has real `File` objects) - unlike
|
|
11
|
+
* `File`, `DataTransferItem` has no `.name`, only `.type`, so extension
|
|
12
|
+
* patterns (`.csv`) can't be evaluated pre-drop and never match.
|
|
13
|
+
*/
|
|
14
|
+
export declare function matchesAccept(file: File, accept: string): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parses a native-`accept`-style string (`".csv,.xlsx"`, `"text/csv"`,
|
|
3
|
+
* `"image/*"`, or any mix, comma-separated) and reports whether `file`
|
|
4
|
+
* matches it — the same three forms the browser's own file picker accepts,
|
|
5
|
+
* now also enforced on the drag-drop path (2026-07-10 audit,
|
|
6
|
+
* "map-file-input" §lyra-file-input, High: previously `accept` only
|
|
7
|
+
* constrained the native picker dialog and had no effect on drop).
|
|
8
|
+
*
|
|
9
|
+
* `file` may be a `DataTransferItem` cast as `File` (the dragenter-preview
|
|
10
|
+
* call site, before the drop payload has real `File` objects) - unlike
|
|
11
|
+
* `File`, `DataTransferItem` has no `.name`, only `.type`, so extension
|
|
12
|
+
* patterns (`.csv`) can't be evaluated pre-drop and never match.
|
|
13
|
+
*/
|
|
14
|
+
export function matchesAccept(file, accept) {
|
|
15
|
+
const parts = accept
|
|
16
|
+
.split(',')
|
|
17
|
+
.map((s) => s.trim().toLowerCase())
|
|
18
|
+
.filter(Boolean);
|
|
19
|
+
if (parts.length === 0)
|
|
20
|
+
return true;
|
|
21
|
+
const name = file.name?.toLowerCase();
|
|
22
|
+
const type = (file.type || '').toLowerCase();
|
|
23
|
+
return parts.some((p) => {
|
|
24
|
+
if (p.startsWith('.'))
|
|
25
|
+
return name !== undefined && name.endsWith(p);
|
|
26
|
+
if (p.endsWith('/*'))
|
|
27
|
+
return type.startsWith(p.slice(0, -1));
|
|
28
|
+
return type === p;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { type TemplateResult } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
export interface RejectedFile {
|
|
4
|
+
file: File;
|
|
5
|
+
reason: 'type' | 'count' | 'size';
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* `<lyra-file-input>` — a drag-drop + click-to-browse file dropzone. Emits
|
|
9
|
+
* raw `File[]`; parsing (CSV/XLSX/etc.) is left to the host, matching every
|
|
10
|
+
* surveyed upload seed (all of which upload raw files to a backend).
|
|
11
|
+
*
|
|
12
|
+
* @customElement lyra-file-input
|
|
13
|
+
* @slot - Custom drop-zone content, overrides the `label` attribute.
|
|
14
|
+
* @event lyra-files - `detail: { files, rejected }`, fired on drop and manual selection.
|
|
15
|
+
* @csspart base, input
|
|
16
|
+
*/
|
|
17
|
+
export declare class LyraFileInput extends LyraElement {
|
|
18
|
+
static styles: import("lit").CSSResultGroup[];
|
|
19
|
+
multiple: boolean;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
accept: string;
|
|
22
|
+
allowedMimeTypes: string[];
|
|
23
|
+
forbiddenMimeTypes: string[];
|
|
24
|
+
maxFileSize: number;
|
|
25
|
+
label: string;
|
|
26
|
+
private dragState;
|
|
27
|
+
private inputEl?;
|
|
28
|
+
private dragCounter;
|
|
29
|
+
private isAllowed;
|
|
30
|
+
private classify;
|
|
31
|
+
private emitFiles;
|
|
32
|
+
/** Programmatically open the native file picker. */
|
|
33
|
+
openPicker(): void;
|
|
34
|
+
private previewState;
|
|
35
|
+
private onDragEnter;
|
|
36
|
+
private onDragOver;
|
|
37
|
+
private onDragLeave;
|
|
38
|
+
private onDrop;
|
|
39
|
+
private onInputChange;
|
|
40
|
+
private onKeyDown;
|
|
41
|
+
render(): TemplateResult;
|
|
42
|
+
}
|
|
43
|
+
declare global {
|
|
44
|
+
interface HTMLElementTagNameMap {
|
|
45
|
+
'lyra-file-input': LyraFileInput;
|
|
46
|
+
}
|
|
47
|
+
}
|