@aceshooting/lyra-ui 0.1.0 → 0.1.2
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 +32 -0
- package/dist/components/empty/empty.js +90 -0
- package/dist/components/empty/empty.styles.d.ts +1 -0
- package/dist/components/empty/empty.styles.js +42 -0
- 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/skeleton/skeleton.d.ts +26 -0
- package/dist/components/skeleton/skeleton.js +65 -0
- package/dist/components/skeleton/skeleton.styles.d.ts +1 -0
- package/dist/components/skeleton/skeleton.styles.js +57 -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 +36 -0
- package/dist/components/stat/stat.js +117 -0
- package/dist/components/stat/stat.styles.d.ts +1 -0
- package/dist/components/stat/stat.styles.js +86 -0
- package/dist/components/table/table.d.ts +54 -0
- package/dist/components/table/table.js +201 -0
- package/dist/components/table/table.styles.d.ts +1 -0
- package/dist/components/table/table.styles.js +98 -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 +60 -11
|
@@ -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
|
`;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type TemplateResult } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
/**
|
|
4
|
+
* `<lyra-empty>` — a generic empty/no-data state. First-party invention (no
|
|
5
|
+
* Web Awesome equivalent); fills the gap identified across ~30 surveyed repos.
|
|
6
|
+
*
|
|
7
|
+
* @customElement lyra-empty
|
|
8
|
+
* @slot - Custom icon or illustration (defaults to none).
|
|
9
|
+
* @slot actions - Buttons/links shown below the description.
|
|
10
|
+
* @csspart base, icon, heading, description, actions
|
|
11
|
+
*/
|
|
12
|
+
export declare class LyraEmpty extends LyraElement {
|
|
13
|
+
static styles: import("lit").CSSResultGroup[];
|
|
14
|
+
/** Short heading, e.g. "No results". */
|
|
15
|
+
heading: string;
|
|
16
|
+
/** Supporting copy, e.g. "Try a different search." */
|
|
17
|
+
description: string;
|
|
18
|
+
private hasIcon;
|
|
19
|
+
private hasActions;
|
|
20
|
+
protected willUpdate(): void;
|
|
21
|
+
firstUpdated(): void;
|
|
22
|
+
private checkIconSlot;
|
|
23
|
+
private checkActionsSlot;
|
|
24
|
+
private onIconSlotChange;
|
|
25
|
+
private onActionsSlotChange;
|
|
26
|
+
render(): TemplateResult;
|
|
27
|
+
}
|
|
28
|
+
declare global {
|
|
29
|
+
interface HTMLElementTagNameMap {
|
|
30
|
+
'lyra-empty': LyraEmpty;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
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 } from 'lit';
|
|
8
|
+
import { property, state } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { styles } from './empty.styles.js';
|
|
12
|
+
/**
|
|
13
|
+
* `<lyra-empty>` — a generic empty/no-data state. First-party invention (no
|
|
14
|
+
* Web Awesome equivalent); fills the gap identified across ~30 surveyed repos.
|
|
15
|
+
*
|
|
16
|
+
* @customElement lyra-empty
|
|
17
|
+
* @slot - Custom icon or illustration (defaults to none).
|
|
18
|
+
* @slot actions - Buttons/links shown below the description.
|
|
19
|
+
* @csspart base, icon, heading, description, actions
|
|
20
|
+
*/
|
|
21
|
+
export class LyraEmpty extends LyraElement {
|
|
22
|
+
constructor() {
|
|
23
|
+
super(...arguments);
|
|
24
|
+
/** Short heading, e.g. "No results". */
|
|
25
|
+
this.heading = '';
|
|
26
|
+
/** Supporting copy, e.g. "Try a different search." */
|
|
27
|
+
this.description = '';
|
|
28
|
+
// `[part='icon']:empty` never matches because the part always contains a
|
|
29
|
+
// `<slot>` element (CSS `:empty` only ignores text/comment nodes). Track
|
|
30
|
+
// real slot assignment in JS instead and key the CSS off these instead.
|
|
31
|
+
this.hasIcon = false;
|
|
32
|
+
this.hasActions = false;
|
|
33
|
+
this.onIconSlotChange = (e) => {
|
|
34
|
+
this.checkIconSlot(e.target);
|
|
35
|
+
};
|
|
36
|
+
this.onActionsSlotChange = (e) => {
|
|
37
|
+
this.checkActionsSlot(e.target);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
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
|
+
}
|
|
51
|
+
firstUpdated() {
|
|
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.
|
|
56
|
+
this.checkIconSlot(this.shadowRoot.querySelector('slot:not([name])'));
|
|
57
|
+
this.checkActionsSlot(this.shadowRoot.querySelector('slot[name="actions"]'));
|
|
58
|
+
}
|
|
59
|
+
checkIconSlot(slot) {
|
|
60
|
+
this.hasIcon = slot.assignedElements({ flatten: true }).length > 0;
|
|
61
|
+
}
|
|
62
|
+
checkActionsSlot(slot) {
|
|
63
|
+
this.hasActions = slot.assignedElements({ flatten: true }).length > 0;
|
|
64
|
+
}
|
|
65
|
+
render() {
|
|
66
|
+
return html `
|
|
67
|
+
<div part="base">
|
|
68
|
+
<div part="icon" ?hidden=${!this.hasIcon}><slot @slotchange=${this.onIconSlotChange}></slot></div>
|
|
69
|
+
<p part="heading">${this.heading}</p>
|
|
70
|
+
<p part="description">${this.description}</p>
|
|
71
|
+
<div part="actions" ?hidden=${!this.hasActions}>
|
|
72
|
+
<slot name="actions" @slotchange=${this.onActionsSlotChange}></slot>
|
|
73
|
+
</div>
|
|
74
|
+
</div>
|
|
75
|
+
`;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
__decorate([
|
|
79
|
+
property()
|
|
80
|
+
], LyraEmpty.prototype, "heading", void 0);
|
|
81
|
+
__decorate([
|
|
82
|
+
property()
|
|
83
|
+
], LyraEmpty.prototype, "description", void 0);
|
|
84
|
+
__decorate([
|
|
85
|
+
state()
|
|
86
|
+
], LyraEmpty.prototype, "hasIcon", void 0);
|
|
87
|
+
__decorate([
|
|
88
|
+
state()
|
|
89
|
+
], LyraEmpty.prototype, "hasActions", void 0);
|
|
90
|
+
defineElement('empty', LyraEmpty);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: flex;
|
|
5
|
+
}
|
|
6
|
+
[part='base'] {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
align-items: center;
|
|
10
|
+
text-align: center;
|
|
11
|
+
gap: var(--lyra-space-s);
|
|
12
|
+
padding: var(--lyra-space-l);
|
|
13
|
+
color: var(--lyra-color-text-quiet);
|
|
14
|
+
inline-size: 100%;
|
|
15
|
+
}
|
|
16
|
+
[part='icon'] {
|
|
17
|
+
font-size: 2rem;
|
|
18
|
+
line-height: 1;
|
|
19
|
+
color: var(--lyra-color-border);
|
|
20
|
+
}
|
|
21
|
+
[part='icon'][hidden] {
|
|
22
|
+
display: none;
|
|
23
|
+
}
|
|
24
|
+
[part='heading'] {
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
color: var(--lyra-color-text);
|
|
27
|
+
margin: 0;
|
|
28
|
+
}
|
|
29
|
+
[part='description'] {
|
|
30
|
+
font-size: 0.875rem;
|
|
31
|
+
margin: 0;
|
|
32
|
+
}
|
|
33
|
+
[part='description']:empty {
|
|
34
|
+
display: none;
|
|
35
|
+
}
|
|
36
|
+
[part='actions'] {
|
|
37
|
+
margin-block-start: var(--lyra-space-s);
|
|
38
|
+
}
|
|
39
|
+
[part='actions'][hidden] {
|
|
40
|
+
display: none;
|
|
41
|
+
}
|
|
42
|
+
`;
|
|
@@ -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;
|