@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
|
@@ -0,0 +1,117 @@
|
|
|
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, state } from 'lit/decorators.js';
|
|
9
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
11
|
+
import { chevronIcon } from '../../internal/icons.js';
|
|
12
|
+
import { styles } from './stat.styles.js';
|
|
13
|
+
/**
|
|
14
|
+
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
15
|
+
* "metric row" / "KPI card" pattern reinvented in every surveyed repo.
|
|
16
|
+
*
|
|
17
|
+
* @customElement lyra-stat
|
|
18
|
+
* @slot - Leading icon.
|
|
19
|
+
* @slot caption - Rich caption content (overrides the `caption` attribute).
|
|
20
|
+
* @csspart base, icon, label, value, unit, trend, caption
|
|
21
|
+
*/
|
|
22
|
+
export class LyraStat extends LyraElement {
|
|
23
|
+
constructor() {
|
|
24
|
+
super(...arguments);
|
|
25
|
+
this.label = '';
|
|
26
|
+
this.value = '';
|
|
27
|
+
this.unit = '';
|
|
28
|
+
this.variant = 'neutral';
|
|
29
|
+
this.trend = NaN;
|
|
30
|
+
this.caption = '';
|
|
31
|
+
/** Which trend direction counts as "good" — inverts arrow/color polarity for
|
|
32
|
+
* cost/latency/error-rate-style metrics where a decrease is the win. */
|
|
33
|
+
this.goodDirection = 'up';
|
|
34
|
+
// Same fix `lyra-empty` already established: `[part]:empty` never matches
|
|
35
|
+
// because the part always contains a literal `<slot>` child. Track real
|
|
36
|
+
// slot assignment in JS instead (2026-07-10 audit, "dashboard-atoms"
|
|
37
|
+
// §lyra-stat, High).
|
|
38
|
+
this.hasIcon = false;
|
|
39
|
+
this.hasCaptionSlot = false;
|
|
40
|
+
this.onIconSlotChange = (e) => {
|
|
41
|
+
this.hasIcon = e.target.assignedElements({ flatten: true }).length > 0;
|
|
42
|
+
};
|
|
43
|
+
this.onCaptionSlotChange = (e) => {
|
|
44
|
+
this.hasCaptionSlot = e.target.assignedElements({ flatten: true }).length > 0;
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
48
|
+
willUpdate() {
|
|
49
|
+
if (!this.hasUpdated) {
|
|
50
|
+
this.hasIcon = Array.from(this.children).some((el) => !el.hasAttribute('slot'));
|
|
51
|
+
this.hasCaptionSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'caption');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
render() {
|
|
55
|
+
const hasTrend = !isNaN(this.trend);
|
|
56
|
+
const rawDirection = this.trend > 0 ? 'up' : this.trend < 0 ? 'down' : 'flat';
|
|
57
|
+
const isGood = rawDirection === 'flat' ? null : rawDirection === this.goodDirection;
|
|
58
|
+
// 2026-07-10 design review, "dashboard-atoms" §lyra-stat: the trend
|
|
59
|
+
// pill rendered literal ▲/▼ glyphs — font-dependent and inconsistent
|
|
60
|
+
// with the rest of the icon set — swapped for the shared chevronIcon(),
|
|
61
|
+
// rotated per direction via CSS on the wrapping [part='trend'].
|
|
62
|
+
const arrow = rawDirection === 'flat' ? '–' : chevronIcon();
|
|
63
|
+
const hasCaption = this.hasCaptionSlot || this.caption.length > 0;
|
|
64
|
+
return html `
|
|
65
|
+
<div part="base">
|
|
66
|
+
<span part="icon" ?hidden=${!this.hasIcon}
|
|
67
|
+
><slot @slotchange=${this.onIconSlotChange}></slot
|
|
68
|
+
></span>
|
|
69
|
+
<span part="label">${this.label}</span>
|
|
70
|
+
<div part="value-row">
|
|
71
|
+
<span part="value">${this.value}</span>
|
|
72
|
+
<span part="unit">${this.unit}</span>
|
|
73
|
+
</div>
|
|
74
|
+
${hasTrend
|
|
75
|
+
? html `<span
|
|
76
|
+
part="trend"
|
|
77
|
+
data-direction=${rawDirection}
|
|
78
|
+
data-polarity=${isGood == null ? nothing : isGood ? 'good' : 'bad'}
|
|
79
|
+
>
|
|
80
|
+
<span aria-hidden="true">${arrow}</span> ${Math.abs(this.trend)}%
|
|
81
|
+
</span>`
|
|
82
|
+
: nothing}
|
|
83
|
+
<div part="caption" ?hidden=${!hasCaption}>
|
|
84
|
+
<slot name="caption" @slotchange=${this.onCaptionSlotChange}>${this.caption}</slot>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
__decorate([
|
|
91
|
+
property()
|
|
92
|
+
], LyraStat.prototype, "label", void 0);
|
|
93
|
+
__decorate([
|
|
94
|
+
property()
|
|
95
|
+
], LyraStat.prototype, "value", void 0);
|
|
96
|
+
__decorate([
|
|
97
|
+
property()
|
|
98
|
+
], LyraStat.prototype, "unit", void 0);
|
|
99
|
+
__decorate([
|
|
100
|
+
property({ reflect: true })
|
|
101
|
+
], LyraStat.prototype, "variant", void 0);
|
|
102
|
+
__decorate([
|
|
103
|
+
property({ type: Number })
|
|
104
|
+
], LyraStat.prototype, "trend", void 0);
|
|
105
|
+
__decorate([
|
|
106
|
+
property()
|
|
107
|
+
], LyraStat.prototype, "caption", void 0);
|
|
108
|
+
__decorate([
|
|
109
|
+
property({ attribute: 'good-direction' })
|
|
110
|
+
], LyraStat.prototype, "goodDirection", void 0);
|
|
111
|
+
__decorate([
|
|
112
|
+
state()
|
|
113
|
+
], LyraStat.prototype, "hasIcon", void 0);
|
|
114
|
+
__decorate([
|
|
115
|
+
state()
|
|
116
|
+
], LyraStat.prototype, "hasCaptionSlot", void 0);
|
|
117
|
+
defineElement('stat', LyraStat);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
}
|
|
6
|
+
[part='base'] {
|
|
7
|
+
display: flex;
|
|
8
|
+
flex-direction: column;
|
|
9
|
+
gap: var(--lyra-space-xs);
|
|
10
|
+
padding: var(--lyra-space-m);
|
|
11
|
+
border: 1px solid var(--lyra-color-border);
|
|
12
|
+
border-radius: var(--lyra-radius);
|
|
13
|
+
background: var(--lyra-color-surface);
|
|
14
|
+
}
|
|
15
|
+
[part='icon'] {
|
|
16
|
+
color: var(--lyra-color-text-quiet);
|
|
17
|
+
}
|
|
18
|
+
[part='icon'][hidden] {
|
|
19
|
+
display: none;
|
|
20
|
+
}
|
|
21
|
+
[part='label'] {
|
|
22
|
+
font-size: 0.75rem;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
text-transform: uppercase;
|
|
25
|
+
letter-spacing: 0.04em;
|
|
26
|
+
color: var(--lyra-color-text-quiet);
|
|
27
|
+
}
|
|
28
|
+
[part='value-row'] {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: baseline;
|
|
31
|
+
gap: var(--lyra-space-xs);
|
|
32
|
+
}
|
|
33
|
+
[part='value'] {
|
|
34
|
+
font-size: 1.75rem;
|
|
35
|
+
font-weight: 700;
|
|
36
|
+
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
37
|
+
}
|
|
38
|
+
[part='unit'] {
|
|
39
|
+
font-size: 0.875rem;
|
|
40
|
+
color: var(--lyra-color-text-quiet);
|
|
41
|
+
}
|
|
42
|
+
[part='trend'] {
|
|
43
|
+
display: inline-flex;
|
|
44
|
+
align-items: center;
|
|
45
|
+
gap: var(--lyra-space-xs);
|
|
46
|
+
font-size: 0.8125rem;
|
|
47
|
+
font-weight: 600;
|
|
48
|
+
border-radius: var(--lyra-radius);
|
|
49
|
+
/* 0.05rem/0.4rem don't cleanly map to any --lyra-space-* step (the
|
|
50
|
+
smallest, --lyra-space-xs, is 0.25rem): rounding the vertical value
|
|
51
|
+
up to xs would 5x this chip's padding and blow out the compact
|
|
52
|
+
trend-chip look, so both stay literal here. */
|
|
53
|
+
padding: 0.05rem 0.4rem;
|
|
54
|
+
align-self: flex-start;
|
|
55
|
+
}
|
|
56
|
+
[part='trend'][data-direction='up'] svg {
|
|
57
|
+
transform: rotate(-90deg);
|
|
58
|
+
}
|
|
59
|
+
[part='trend'][data-direction='down'] svg {
|
|
60
|
+
transform: rotate(90deg);
|
|
61
|
+
}
|
|
62
|
+
[part='trend'][data-polarity='good'] {
|
|
63
|
+
color: var(--lyra-color-success);
|
|
64
|
+
background: color-mix(in srgb, var(--lyra-color-success) 8%, transparent);
|
|
65
|
+
}
|
|
66
|
+
[part='trend'][data-polarity='bad'] {
|
|
67
|
+
color: var(--lyra-color-danger);
|
|
68
|
+
background: color-mix(in srgb, var(--lyra-color-danger) 8%, transparent);
|
|
69
|
+
}
|
|
70
|
+
:host([variant='success']) [part='value'] {
|
|
71
|
+
color: var(--lyra-color-success);
|
|
72
|
+
}
|
|
73
|
+
:host([variant='warning']) [part='value'] {
|
|
74
|
+
color: var(--lyra-color-warning);
|
|
75
|
+
}
|
|
76
|
+
:host([variant='danger']) [part='value'] {
|
|
77
|
+
color: var(--lyra-color-danger);
|
|
78
|
+
}
|
|
79
|
+
[part='caption'] {
|
|
80
|
+
font-size: 0.8125rem;
|
|
81
|
+
color: var(--lyra-color-text-quiet);
|
|
82
|
+
}
|
|
83
|
+
[part='caption'][hidden] {
|
|
84
|
+
display: none;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
import '../empty/empty.js';
|
|
4
|
+
export interface TableColumn<T> {
|
|
5
|
+
key: string;
|
|
6
|
+
label: string;
|
|
7
|
+
sortable?: boolean;
|
|
8
|
+
align?: 'start' | 'end';
|
|
9
|
+
cell: (row: T) => unknown;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `<lyra-table>` — a presentational, sort/select-aware data table.
|
|
13
|
+
*
|
|
14
|
+
* Header/row activation is delegated: one `click` and one `keydown`
|
|
15
|
+
* listener on `<table>` resolve the target via `closest('[data-col-key]'
|
|
16
|
+
* | '[data-row-key]')` and a key→object lookup map, instead of allocating
|
|
17
|
+
* fresh per-column/per-row closures on every render (2026-07-10 audit,
|
|
18
|
+
* "dashboard-atoms" §lyra-table, Medium).
|
|
19
|
+
*
|
|
20
|
+
* @customElement lyra-table
|
|
21
|
+
* @event lyra-sort - A sortable header was activated. `detail: { key }`.
|
|
22
|
+
* @event lyra-row-click - A row was activated. `detail: { row }`.
|
|
23
|
+
* @event lyra-load-more - The "load more" control was activated.
|
|
24
|
+
* @csspart base, table, head, header-cell, row, cell, more-button, sort-icon
|
|
25
|
+
*/
|
|
26
|
+
export declare class LyraTable<T = unknown> extends LyraElement {
|
|
27
|
+
static styles: import("lit").CSSResultGroup[];
|
|
28
|
+
columns: TableColumn<T>[];
|
|
29
|
+
rows: T[];
|
|
30
|
+
sortKey: string;
|
|
31
|
+
sortDir: 'asc' | 'desc';
|
|
32
|
+
rowKey?: (row: T) => string | number;
|
|
33
|
+
selectedKey: string | number | null;
|
|
34
|
+
hasMore: boolean;
|
|
35
|
+
moreLabel: string;
|
|
36
|
+
emptyHeading: string;
|
|
37
|
+
emptyDescription: string;
|
|
38
|
+
noColumnsHeading: string;
|
|
39
|
+
noColumnsDescription: string;
|
|
40
|
+
private rowsByKey;
|
|
41
|
+
private columnsByKey;
|
|
42
|
+
private keyOf;
|
|
43
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
44
|
+
private activateColumn;
|
|
45
|
+
private activateRow;
|
|
46
|
+
private onTableClick;
|
|
47
|
+
private onTableKeyDown;
|
|
48
|
+
render(): TemplateResult;
|
|
49
|
+
}
|
|
50
|
+
declare global {
|
|
51
|
+
interface HTMLElementTagNameMap {
|
|
52
|
+
'lyra-table': LyraTable;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,201 @@
|
|
|
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 { repeat } from 'lit/directives/repeat.js';
|
|
10
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
11
|
+
import { defineElement } from '../../internal/prefix.js';
|
|
12
|
+
import { styles } from './table.styles.js';
|
|
13
|
+
import { chevronIcon } from '../../internal/icons.js';
|
|
14
|
+
import '../empty/empty.js';
|
|
15
|
+
/**
|
|
16
|
+
* `<lyra-table>` — a presentational, sort/select-aware data table.
|
|
17
|
+
*
|
|
18
|
+
* Header/row activation is delegated: one `click` and one `keydown`
|
|
19
|
+
* listener on `<table>` resolve the target via `closest('[data-col-key]'
|
|
20
|
+
* | '[data-row-key]')` and a key→object lookup map, instead of allocating
|
|
21
|
+
* fresh per-column/per-row closures on every render (2026-07-10 audit,
|
|
22
|
+
* "dashboard-atoms" §lyra-table, Medium).
|
|
23
|
+
*
|
|
24
|
+
* @customElement lyra-table
|
|
25
|
+
* @event lyra-sort - A sortable header was activated. `detail: { key }`.
|
|
26
|
+
* @event lyra-row-click - A row was activated. `detail: { row }`.
|
|
27
|
+
* @event lyra-load-more - The "load more" control was activated.
|
|
28
|
+
* @csspart base, table, head, header-cell, row, cell, more-button, sort-icon
|
|
29
|
+
*/
|
|
30
|
+
export class LyraTable extends LyraElement {
|
|
31
|
+
constructor() {
|
|
32
|
+
super(...arguments);
|
|
33
|
+
this.columns = [];
|
|
34
|
+
this.rows = [];
|
|
35
|
+
this.sortKey = '';
|
|
36
|
+
this.sortDir = 'asc';
|
|
37
|
+
this.selectedKey = null;
|
|
38
|
+
this.hasMore = false;
|
|
39
|
+
this.moreLabel = 'Load more';
|
|
40
|
+
this.emptyHeading = 'No data';
|
|
41
|
+
this.emptyDescription = '';
|
|
42
|
+
this.noColumnsHeading = 'No columns configured';
|
|
43
|
+
this.noColumnsDescription = '';
|
|
44
|
+
this.rowsByKey = new Map();
|
|
45
|
+
this.columnsByKey = new Map();
|
|
46
|
+
this.onTableClick = (e) => {
|
|
47
|
+
const target = e.target;
|
|
48
|
+
const th = target.closest('[data-col-key]');
|
|
49
|
+
if (th)
|
|
50
|
+
return this.activateColumn(th.dataset.colKey);
|
|
51
|
+
const tr = target.closest('[data-row-key]');
|
|
52
|
+
if (tr)
|
|
53
|
+
this.activateRow(tr.dataset.rowKey);
|
|
54
|
+
};
|
|
55
|
+
this.onTableKeyDown = (e) => {
|
|
56
|
+
if (e.key !== 'Enter' && e.key !== ' ')
|
|
57
|
+
return;
|
|
58
|
+
const target = e.target;
|
|
59
|
+
const th = target.closest('[data-col-key]');
|
|
60
|
+
if (th) {
|
|
61
|
+
e.preventDefault();
|
|
62
|
+
return this.activateColumn(th.dataset.colKey);
|
|
63
|
+
}
|
|
64
|
+
const tr = target.closest('[data-row-key]');
|
|
65
|
+
if (tr) {
|
|
66
|
+
e.preventDefault();
|
|
67
|
+
this.activateRow(tr.dataset.rowKey);
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
72
|
+
keyOf(row, index) {
|
|
73
|
+
return this.rowKey ? this.rowKey(row) : index;
|
|
74
|
+
}
|
|
75
|
+
willUpdate(changed) {
|
|
76
|
+
if (changed.has('rows') || changed.has('rowKey')) {
|
|
77
|
+
this.rowsByKey = new Map(this.rows.map((row, i) => [String(this.keyOf(row, i)), row]));
|
|
78
|
+
}
|
|
79
|
+
if (changed.has('columns')) {
|
|
80
|
+
this.columnsByKey = new Map(this.columns.map((c) => [c.key, c]));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
activateColumn(key) {
|
|
84
|
+
const col = this.columnsByKey.get(key);
|
|
85
|
+
if (col?.sortable)
|
|
86
|
+
this.emit('lyra-sort', { key: col.key });
|
|
87
|
+
}
|
|
88
|
+
activateRow(key) {
|
|
89
|
+
const row = this.rowsByKey.get(key);
|
|
90
|
+
if (row !== undefined)
|
|
91
|
+
this.emit('lyra-row-click', { row });
|
|
92
|
+
}
|
|
93
|
+
render() {
|
|
94
|
+
if (this.columns.length === 0) {
|
|
95
|
+
return html `<lyra-empty
|
|
96
|
+
heading=${this.noColumnsHeading}
|
|
97
|
+
description=${this.noColumnsDescription}
|
|
98
|
+
></lyra-empty>`;
|
|
99
|
+
}
|
|
100
|
+
if (this.rows.length === 0) {
|
|
101
|
+
return html `<lyra-empty
|
|
102
|
+
heading=${this.emptyHeading}
|
|
103
|
+
description=${this.emptyDescription}
|
|
104
|
+
></lyra-empty>`;
|
|
105
|
+
}
|
|
106
|
+
return html `
|
|
107
|
+
<div part="base">
|
|
108
|
+
<table part="table" role="grid" @click=${this.onTableClick} @keydown=${this.onTableKeyDown}>
|
|
109
|
+
<thead part="head">
|
|
110
|
+
<tr role="row">
|
|
111
|
+
${this.columns.map((col) => {
|
|
112
|
+
const active = Boolean(col.sortable) && this.sortKey === col.key;
|
|
113
|
+
const ariaSort = active ? (this.sortDir === 'asc' ? 'ascending' : 'descending') : 'none';
|
|
114
|
+
return html `<th
|
|
115
|
+
part="header-cell"
|
|
116
|
+
role="columnheader"
|
|
117
|
+
scope="col"
|
|
118
|
+
data-col-key=${col.key}
|
|
119
|
+
data-align=${col.align ?? 'start'}
|
|
120
|
+
?data-sortable=${col.sortable}
|
|
121
|
+
aria-sort=${col.sortable ? ariaSort : nothing}
|
|
122
|
+
tabindex=${col.sortable ? '0' : '-1'}
|
|
123
|
+
>
|
|
124
|
+
${col.label}
|
|
125
|
+
${active
|
|
126
|
+
? html `<span part="sort-icon" data-dir=${this.sortDir} aria-hidden="true"
|
|
127
|
+
>${chevronIcon()}</span
|
|
128
|
+
>`
|
|
129
|
+
: nothing}
|
|
130
|
+
</th>`;
|
|
131
|
+
})}
|
|
132
|
+
</tr>
|
|
133
|
+
</thead>
|
|
134
|
+
<tbody>
|
|
135
|
+
${repeat(this.rows, (row, i) => this.keyOf(row, i), (row, i) => {
|
|
136
|
+
const key = this.keyOf(row, i);
|
|
137
|
+
const selected = this.selectedKey !== null && this.selectedKey === key;
|
|
138
|
+
return html `<tr
|
|
139
|
+
part="row"
|
|
140
|
+
role="row"
|
|
141
|
+
data-row-key=${String(key)}
|
|
142
|
+
aria-selected=${selected ? 'true' : 'false'}
|
|
143
|
+
tabindex="0"
|
|
144
|
+
>
|
|
145
|
+
${this.columns.map((col) => html `<td part="cell" role="gridcell" data-align=${col.align ?? 'start'}>
|
|
146
|
+
${col.cell(row)}
|
|
147
|
+
</td>`)}
|
|
148
|
+
</tr>`;
|
|
149
|
+
})}
|
|
150
|
+
</tbody>
|
|
151
|
+
</table>
|
|
152
|
+
${this.hasMore
|
|
153
|
+
? html `<button
|
|
154
|
+
part="more-button"
|
|
155
|
+
type="button"
|
|
156
|
+
@click=${() => this.emit('lyra-load-more')}
|
|
157
|
+
>
|
|
158
|
+
${this.moreLabel}
|
|
159
|
+
</button>`
|
|
160
|
+
: nothing}
|
|
161
|
+
</div>
|
|
162
|
+
`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
__decorate([
|
|
166
|
+
property({ attribute: false })
|
|
167
|
+
], LyraTable.prototype, "columns", void 0);
|
|
168
|
+
__decorate([
|
|
169
|
+
property({ attribute: false })
|
|
170
|
+
], LyraTable.prototype, "rows", void 0);
|
|
171
|
+
__decorate([
|
|
172
|
+
property({ attribute: 'sort-key' })
|
|
173
|
+
], LyraTable.prototype, "sortKey", void 0);
|
|
174
|
+
__decorate([
|
|
175
|
+
property({ attribute: 'sort-dir' })
|
|
176
|
+
], LyraTable.prototype, "sortDir", void 0);
|
|
177
|
+
__decorate([
|
|
178
|
+
property({ attribute: false })
|
|
179
|
+
], LyraTable.prototype, "rowKey", void 0);
|
|
180
|
+
__decorate([
|
|
181
|
+
property({ attribute: false })
|
|
182
|
+
], LyraTable.prototype, "selectedKey", void 0);
|
|
183
|
+
__decorate([
|
|
184
|
+
property({ type: Boolean, attribute: 'has-more', reflect: true })
|
|
185
|
+
], LyraTable.prototype, "hasMore", void 0);
|
|
186
|
+
__decorate([
|
|
187
|
+
property({ attribute: 'more-label' })
|
|
188
|
+
], LyraTable.prototype, "moreLabel", void 0);
|
|
189
|
+
__decorate([
|
|
190
|
+
property({ attribute: 'empty-heading' })
|
|
191
|
+
], LyraTable.prototype, "emptyHeading", void 0);
|
|
192
|
+
__decorate([
|
|
193
|
+
property({ attribute: 'empty-description' })
|
|
194
|
+
], LyraTable.prototype, "emptyDescription", void 0);
|
|
195
|
+
__decorate([
|
|
196
|
+
property({ attribute: 'no-columns-heading' })
|
|
197
|
+
], LyraTable.prototype, "noColumnsHeading", void 0);
|
|
198
|
+
__decorate([
|
|
199
|
+
property({ attribute: 'no-columns-description' })
|
|
200
|
+
], LyraTable.prototype, "noColumnsDescription", void 0);
|
|
201
|
+
defineElement('table', LyraTable);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
inline-size: 100%;
|
|
6
|
+
}
|
|
7
|
+
[part='base'] {
|
|
8
|
+
overflow: auto;
|
|
9
|
+
max-block-size: var(--lyra-table-max-height, none);
|
|
10
|
+
border: 1px solid var(--lyra-color-border);
|
|
11
|
+
border-radius: var(--lyra-radius);
|
|
12
|
+
}
|
|
13
|
+
[part='table'] {
|
|
14
|
+
inline-size: 100%;
|
|
15
|
+
border-collapse: collapse;
|
|
16
|
+
font-size: 0.875rem;
|
|
17
|
+
}
|
|
18
|
+
[part='header-cell'] {
|
|
19
|
+
position: sticky;
|
|
20
|
+
inset-block-start: 0;
|
|
21
|
+
background: var(--lyra-color-surface);
|
|
22
|
+
text-align: start;
|
|
23
|
+
font-weight: 600;
|
|
24
|
+
padding: var(--lyra-space-s);
|
|
25
|
+
border-block-end: 1px solid var(--lyra-color-border);
|
|
26
|
+
cursor: default;
|
|
27
|
+
white-space: nowrap;
|
|
28
|
+
}
|
|
29
|
+
[part='header-cell'][aria-sort]:not([aria-sort='none']),
|
|
30
|
+
[part='header-cell'][data-sortable] {
|
|
31
|
+
cursor: pointer;
|
|
32
|
+
}
|
|
33
|
+
[part='header-cell'][data-sortable]:hover {
|
|
34
|
+
background: var(--lyra-color-brand-quiet);
|
|
35
|
+
}
|
|
36
|
+
[part='header-cell'][data-sortable]:focus-visible {
|
|
37
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
38
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
39
|
+
}
|
|
40
|
+
[part='header-cell'][data-align='end'] {
|
|
41
|
+
text-align: end;
|
|
42
|
+
}
|
|
43
|
+
[part='sort-icon'] {
|
|
44
|
+
display: inline-block;
|
|
45
|
+
margin-inline-start: var(--lyra-space-xs);
|
|
46
|
+
vertical-align: middle;
|
|
47
|
+
transition: transform var(--lyra-transition-fast);
|
|
48
|
+
}
|
|
49
|
+
[part='sort-icon'] svg {
|
|
50
|
+
display: block;
|
|
51
|
+
}
|
|
52
|
+
/* Rotate the wrapping part element, not the svg — internal/icons.ts's
|
|
53
|
+
documented contract ("callers ... rotate the wrapping part element via
|
|
54
|
+
CSS transform: rotate(...), not the svg"). Design-review finding on
|
|
55
|
+
Task 3 (lyra-table sort indicator): this previously rotated the inner
|
|
56
|
+
<svg> directly. */
|
|
57
|
+
[part='sort-icon'][data-dir='asc'] {
|
|
58
|
+
transform: rotate(-90deg);
|
|
59
|
+
}
|
|
60
|
+
[part='sort-icon'][data-dir='desc'] {
|
|
61
|
+
transform: rotate(90deg);
|
|
62
|
+
}
|
|
63
|
+
[part='row']:hover {
|
|
64
|
+
background: var(--lyra-color-brand-quiet);
|
|
65
|
+
}
|
|
66
|
+
[part='row'][aria-selected='true'] {
|
|
67
|
+
background: var(--lyra-color-brand-quiet);
|
|
68
|
+
}
|
|
69
|
+
[part='row']:focus-visible {
|
|
70
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
71
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
72
|
+
}
|
|
73
|
+
[part='cell'] {
|
|
74
|
+
padding: var(--lyra-space-s);
|
|
75
|
+
border-block-end: 1px solid var(--lyra-color-border);
|
|
76
|
+
}
|
|
77
|
+
[part='cell'][data-align='end'] {
|
|
78
|
+
text-align: end;
|
|
79
|
+
}
|
|
80
|
+
[part='more-button'] {
|
|
81
|
+
display: block;
|
|
82
|
+
inline-size: 100%;
|
|
83
|
+
padding: var(--lyra-space-s);
|
|
84
|
+
border: none;
|
|
85
|
+
background: none;
|
|
86
|
+
color: var(--lyra-color-brand);
|
|
87
|
+
font: inherit;
|
|
88
|
+
cursor: pointer;
|
|
89
|
+
border-block-start: 1px solid var(--lyra-color-border);
|
|
90
|
+
}
|
|
91
|
+
[part='more-button']:hover {
|
|
92
|
+
background: var(--lyra-color-brand-quiet);
|
|
93
|
+
}
|
|
94
|
+
[part='more-button']:focus-visible {
|
|
95
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
96
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
97
|
+
}
|
|
98
|
+
`;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
|
+
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
|
+
/**
|
|
4
|
+
* `<lyra-time-range>` — a two-handle brush/scrubber over a numeric domain.
|
|
5
|
+
* Callers map their own time axis to `[min, max]`; no date logic lives here
|
|
6
|
+
* (matches the no-date-library constraint used elsewhere in this library).
|
|
7
|
+
*
|
|
8
|
+
* @customElement lyra-time-range
|
|
9
|
+
* @event lyra-input - Fired continuously while dragging. `detail: { start, end }`.
|
|
10
|
+
* @event lyra-change - Fired on release / keyup-commit. `detail: { start, end }`.
|
|
11
|
+
* @csspart base, track, range, handle-start, handle-end
|
|
12
|
+
*/
|
|
13
|
+
export declare class LyraTimeRange extends LyraElement {
|
|
14
|
+
static styles: import("lit").CSSResultGroup[];
|
|
15
|
+
min: number;
|
|
16
|
+
max: number;
|
|
17
|
+
start: number;
|
|
18
|
+
end: number;
|
|
19
|
+
step: number;
|
|
20
|
+
disabled: boolean;
|
|
21
|
+
private dragging;
|
|
22
|
+
disconnectedCallback(): void;
|
|
23
|
+
private percentOf;
|
|
24
|
+
private clamp;
|
|
25
|
+
private setValue;
|
|
26
|
+
private onKeyDown;
|
|
27
|
+
private onKeyUp;
|
|
28
|
+
private onPointerDown;
|
|
29
|
+
private onPointerMove;
|
|
30
|
+
private onPointerUp;
|
|
31
|
+
/** Stop an in-progress drag, optionally committing a final lyra-change. */
|
|
32
|
+
private endDrag;
|
|
33
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
34
|
+
render(): TemplateResult;
|
|
35
|
+
}
|
|
36
|
+
declare global {
|
|
37
|
+
interface HTMLElementTagNameMap {
|
|
38
|
+
'lyra-time-range': LyraTimeRange;
|
|
39
|
+
}
|
|
40
|
+
}
|