@aceshooting/lyra-ui 0.1.1 → 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 +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
|
@@ -0,0 +1,171 @@
|
|
|
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 './split.styles.js';
|
|
12
|
+
const KEYBOARD_STEP = 2;
|
|
13
|
+
/**
|
|
14
|
+
* `<lyra-split>` — resizable panels for dashboard layouts. Direct light-DOM
|
|
15
|
+
* children are the panels; a divider is auto-inserted between each pair.
|
|
16
|
+
*
|
|
17
|
+
* @customElement lyra-split
|
|
18
|
+
* @event lyra-resize - `detail: { sizes }`, fired on every drag step/release
|
|
19
|
+
* and every keyboard step.
|
|
20
|
+
* @csspart base, divider
|
|
21
|
+
*/
|
|
22
|
+
export class LyraSplit extends LyraElement {
|
|
23
|
+
constructor() {
|
|
24
|
+
super(...arguments);
|
|
25
|
+
this.sizes = [];
|
|
26
|
+
this.min = 10;
|
|
27
|
+
this.orientation = 'horizontal';
|
|
28
|
+
this.panelCount = 0;
|
|
29
|
+
this.dragIndex = -1;
|
|
30
|
+
this.dragStartPos = 0;
|
|
31
|
+
this.dragStartSizes = [];
|
|
32
|
+
this.onPointerDown = (e, index) => {
|
|
33
|
+
this.dragIndex = index;
|
|
34
|
+
this.dragStartPos = this.orientation === 'vertical' ? e.clientY : e.clientX;
|
|
35
|
+
this.dragStartSizes = [...this.sizes];
|
|
36
|
+
e.target.setPointerCapture(e.pointerId);
|
|
37
|
+
window.addEventListener('pointermove', this.onPointerMove);
|
|
38
|
+
window.addEventListener('pointerup', this.onPointerUp);
|
|
39
|
+
};
|
|
40
|
+
this.onPointerMove = (e) => {
|
|
41
|
+
if (this.dragIndex < 0)
|
|
42
|
+
return;
|
|
43
|
+
const base = this.renderRoot.querySelector('[part="base"]');
|
|
44
|
+
const total = this.orientation === 'vertical' ? base.clientHeight : base.clientWidth;
|
|
45
|
+
const pos = this.orientation === 'vertical' ? e.clientY : e.clientX;
|
|
46
|
+
const deltaPercent = ((pos - this.dragStartPos) / total) * 100;
|
|
47
|
+
this.sizes = this.clampPair(this.dragStartSizes, this.dragIndex, deltaPercent);
|
|
48
|
+
this.emit('lyra-resize', { sizes: [...this.sizes] });
|
|
49
|
+
};
|
|
50
|
+
this.onPointerUp = () => {
|
|
51
|
+
this.dragIndex = -1;
|
|
52
|
+
this.persist();
|
|
53
|
+
window.removeEventListener('pointermove', this.onPointerMove);
|
|
54
|
+
window.removeEventListener('pointerup', this.onPointerUp);
|
|
55
|
+
};
|
|
56
|
+
this.onDividerKeyDown = (e, index) => {
|
|
57
|
+
const forwardKey = this.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
|
|
58
|
+
const backwardKey = this.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
|
|
59
|
+
if (e.key === forwardKey) {
|
|
60
|
+
e.preventDefault();
|
|
61
|
+
this.applyDelta(index, KEYBOARD_STEP, true);
|
|
62
|
+
}
|
|
63
|
+
else if (e.key === backwardKey) {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
this.applyDelta(index, -KEYBOARD_STEP, true);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
static { this.styles = [LyraElement.styles, styles]; }
|
|
70
|
+
connectedCallback() {
|
|
71
|
+
super.connectedCallback();
|
|
72
|
+
this.panelCount = this.children.length;
|
|
73
|
+
this.ensureSizes();
|
|
74
|
+
this.loadPersisted();
|
|
75
|
+
}
|
|
76
|
+
disconnectedCallback() {
|
|
77
|
+
super.disconnectedCallback();
|
|
78
|
+
// Clean up any remaining event listeners
|
|
79
|
+
window.removeEventListener('pointermove', this.onPointerMove);
|
|
80
|
+
window.removeEventListener('pointerup', this.onPointerUp);
|
|
81
|
+
}
|
|
82
|
+
get storageFullKey() {
|
|
83
|
+
return this.storageKey ? `lyra-split:${this.storageKey}:${this.panelCount}` : undefined;
|
|
84
|
+
}
|
|
85
|
+
loadPersisted() {
|
|
86
|
+
const key = this.storageFullKey;
|
|
87
|
+
if (!key)
|
|
88
|
+
return;
|
|
89
|
+
const raw = localStorage.getItem(key);
|
|
90
|
+
if (!raw)
|
|
91
|
+
return;
|
|
92
|
+
try {
|
|
93
|
+
const parsed = JSON.parse(raw);
|
|
94
|
+
if (Array.isArray(parsed) && parsed.length === this.panelCount)
|
|
95
|
+
this.sizes = parsed;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
/* ignore malformed persisted state */
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
persist() {
|
|
102
|
+
const key = this.storageFullKey;
|
|
103
|
+
if (key)
|
|
104
|
+
localStorage.setItem(key, JSON.stringify(this.sizes));
|
|
105
|
+
}
|
|
106
|
+
ensureSizes() {
|
|
107
|
+
if (this.sizes.length === this.panelCount && this.panelCount > 0)
|
|
108
|
+
return;
|
|
109
|
+
const equal = 100 / Math.max(1, this.panelCount);
|
|
110
|
+
this.sizes = Array.from({ length: this.panelCount }, () => equal);
|
|
111
|
+
}
|
|
112
|
+
clampPair(sizes, i, delta) {
|
|
113
|
+
const next = [...sizes];
|
|
114
|
+
const a = next[i] + delta;
|
|
115
|
+
const b = next[i + 1] - delta;
|
|
116
|
+
if (a < this.min || b < this.min)
|
|
117
|
+
return sizes;
|
|
118
|
+
next[i] = a;
|
|
119
|
+
next[i + 1] = b;
|
|
120
|
+
return next;
|
|
121
|
+
}
|
|
122
|
+
applyDelta(index, delta, commit) {
|
|
123
|
+
this.sizes = this.clampPair(this.sizes, index, delta);
|
|
124
|
+
this.emit('lyra-resize', { sizes: [...this.sizes] });
|
|
125
|
+
if (commit)
|
|
126
|
+
this.persist();
|
|
127
|
+
}
|
|
128
|
+
updated(changed) {
|
|
129
|
+
if (changed.has('sizes') || this.sizes.length === 0)
|
|
130
|
+
this.ensureSizes();
|
|
131
|
+
const panels = [...this.children];
|
|
132
|
+
panels.forEach((panel, i) => {
|
|
133
|
+
panel.style.flex = `0 0 ${this.sizes[i] ?? 0}%`;
|
|
134
|
+
panel.style.order = String(i * 2);
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
render() {
|
|
138
|
+
const dividers = [];
|
|
139
|
+
for (let i = 0; i < this.panelCount - 1; i++) {
|
|
140
|
+
dividers.push(html `<div
|
|
141
|
+
part="divider"
|
|
142
|
+
role="separator"
|
|
143
|
+
aria-orientation=${this.orientation === 'vertical' ? 'horizontal' : 'vertical'}
|
|
144
|
+
aria-valuenow=${Math.round(this.sizes[i] ?? 0)}
|
|
145
|
+
aria-valuemin=${this.min}
|
|
146
|
+
aria-valuemax=${100 - this.min}
|
|
147
|
+
tabindex="0"
|
|
148
|
+
style=${`order:${i * 2 + 1}`}
|
|
149
|
+
@pointerdown=${(e) => this.onPointerDown(e, i)}
|
|
150
|
+
@keydown=${(e) => this.onDividerKeyDown(e, i)}
|
|
151
|
+
></div>`);
|
|
152
|
+
}
|
|
153
|
+
return html `<div part="base"><slot></slot>${dividers}</div>`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
__decorate([
|
|
157
|
+
property({ attribute: false })
|
|
158
|
+
], LyraSplit.prototype, "sizes", void 0);
|
|
159
|
+
__decorate([
|
|
160
|
+
property({ type: Number })
|
|
161
|
+
], LyraSplit.prototype, "min", void 0);
|
|
162
|
+
__decorate([
|
|
163
|
+
property({ reflect: true })
|
|
164
|
+
], LyraSplit.prototype, "orientation", void 0);
|
|
165
|
+
__decorate([
|
|
166
|
+
property({ attribute: 'storage-key' })
|
|
167
|
+
], LyraSplit.prototype, "storageKey", void 0);
|
|
168
|
+
__decorate([
|
|
169
|
+
state()
|
|
170
|
+
], LyraSplit.prototype, "panelCount", void 0);
|
|
171
|
+
defineElement('split', LyraSplit);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const styles: import("lit").CSSResult;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { css } from 'lit';
|
|
2
|
+
export const styles = css `
|
|
3
|
+
:host {
|
|
4
|
+
display: block;
|
|
5
|
+
inline-size: 100%;
|
|
6
|
+
block-size: 100%;
|
|
7
|
+
}
|
|
8
|
+
[part='base'] {
|
|
9
|
+
display: flex;
|
|
10
|
+
inline-size: 100%;
|
|
11
|
+
block-size: 100%;
|
|
12
|
+
}
|
|
13
|
+
:host([orientation='vertical']) [part='base'] {
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
}
|
|
16
|
+
[part='divider'] {
|
|
17
|
+
position: relative;
|
|
18
|
+
flex: 0 0 auto;
|
|
19
|
+
inline-size: 3px;
|
|
20
|
+
block-size: auto;
|
|
21
|
+
background: var(--lyra-color-border);
|
|
22
|
+
cursor: col-resize;
|
|
23
|
+
touch-action: none;
|
|
24
|
+
}
|
|
25
|
+
/* Transparent hit-slop: widens the draggable/tappable box along the resize
|
|
26
|
+
axis only, without changing the divider's visible 3px width. Generated
|
|
27
|
+
content is part of the originating element's hit-test box, so pointer
|
|
28
|
+
events here still resolve e.target to [part="divider"] itself. */
|
|
29
|
+
[part='divider']::before {
|
|
30
|
+
content: '';
|
|
31
|
+
position: absolute;
|
|
32
|
+
inset-block: 0;
|
|
33
|
+
inset-inline: -6px;
|
|
34
|
+
}
|
|
35
|
+
:host([orientation='vertical']) [part='divider'] {
|
|
36
|
+
inline-size: auto;
|
|
37
|
+
block-size: 3px;
|
|
38
|
+
cursor: row-resize;
|
|
39
|
+
}
|
|
40
|
+
:host([orientation='vertical']) [part='divider']::before {
|
|
41
|
+
inset-block: -6px;
|
|
42
|
+
inset-inline: 0;
|
|
43
|
+
}
|
|
44
|
+
[part='divider']:hover {
|
|
45
|
+
background: var(--lyra-color-brand);
|
|
46
|
+
}
|
|
47
|
+
[part='divider']:focus-visible {
|
|
48
|
+
outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
|
|
49
|
+
outline-offset: var(--lyra-focus-ring-offset);
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { type TemplateResult } from 'lit';
|
|
2
2
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
3
|
export type StatVariant = 'neutral' | 'success' | 'warning' | 'danger';
|
|
4
|
+
export type StatGoodDirection = 'up' | 'down';
|
|
4
5
|
/**
|
|
5
6
|
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
6
7
|
* "metric row" / "KPI card" pattern reinvented in every surveyed repo.
|
|
@@ -18,6 +19,14 @@ export declare class LyraStat extends LyraElement {
|
|
|
18
19
|
variant: StatVariant;
|
|
19
20
|
trend: number;
|
|
20
21
|
caption: string;
|
|
22
|
+
/** Which trend direction counts as "good" — inverts arrow/color polarity for
|
|
23
|
+
* cost/latency/error-rate-style metrics where a decrease is the win. */
|
|
24
|
+
goodDirection: StatGoodDirection;
|
|
25
|
+
private hasIcon;
|
|
26
|
+
private hasCaptionSlot;
|
|
27
|
+
protected willUpdate(): void;
|
|
28
|
+
private onIconSlotChange;
|
|
29
|
+
private onCaptionSlotChange;
|
|
21
30
|
render(): TemplateResult;
|
|
22
31
|
}
|
|
23
32
|
declare global {
|
|
@@ -5,9 +5,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
5
5
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
6
6
|
};
|
|
7
7
|
import { html, nothing } from 'lit';
|
|
8
|
-
import { property } from 'lit/decorators.js';
|
|
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 './stat.styles.js';
|
|
12
13
|
/**
|
|
13
14
|
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
@@ -27,26 +28,61 @@ export class LyraStat extends LyraElement {
|
|
|
27
28
|
this.variant = 'neutral';
|
|
28
29
|
this.trend = NaN;
|
|
29
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
|
+
};
|
|
30
46
|
}
|
|
31
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
|
+
}
|
|
32
54
|
render() {
|
|
33
55
|
const hasTrend = !isNaN(this.trend);
|
|
34
|
-
const
|
|
35
|
-
const
|
|
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;
|
|
36
64
|
return html `
|
|
37
65
|
<div part="base">
|
|
38
|
-
<span part="icon"
|
|
66
|
+
<span part="icon" ?hidden=${!this.hasIcon}
|
|
67
|
+
><slot @slotchange=${this.onIconSlotChange}></slot
|
|
68
|
+
></span>
|
|
39
69
|
<span part="label">${this.label}</span>
|
|
40
70
|
<div part="value-row">
|
|
41
71
|
<span part="value">${this.value}</span>
|
|
42
72
|
<span part="unit">${this.unit}</span>
|
|
43
73
|
</div>
|
|
44
74
|
${hasTrend
|
|
45
|
-
? html `<span
|
|
46
|
-
|
|
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)}%
|
|
47
81
|
</span>`
|
|
48
82
|
: nothing}
|
|
49
|
-
<div part="caption"
|
|
83
|
+
<div part="caption" ?hidden=${!hasCaption}>
|
|
84
|
+
<slot name="caption" @slotchange=${this.onCaptionSlotChange}>${this.caption}</slot>
|
|
85
|
+
</div>
|
|
50
86
|
</div>
|
|
51
87
|
`;
|
|
52
88
|
}
|
|
@@ -69,4 +105,13 @@ __decorate([
|
|
|
69
105
|
__decorate([
|
|
70
106
|
property()
|
|
71
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);
|
|
72
117
|
defineElement('stat', LyraStat);
|
|
@@ -15,7 +15,7 @@ export const styles = css `
|
|
|
15
15
|
[part='icon'] {
|
|
16
16
|
color: var(--lyra-color-text-quiet);
|
|
17
17
|
}
|
|
18
|
-
[part='icon']
|
|
18
|
+
[part='icon'][hidden] {
|
|
19
19
|
display: none;
|
|
20
20
|
}
|
|
21
21
|
[part='label'] {
|
|
@@ -42,18 +42,28 @@ export const styles = css `
|
|
|
42
42
|
[part='trend'] {
|
|
43
43
|
display: inline-flex;
|
|
44
44
|
align-items: center;
|
|
45
|
-
gap:
|
|
45
|
+
gap: var(--lyra-space-xs);
|
|
46
46
|
font-size: 0.8125rem;
|
|
47
47
|
font-weight: 600;
|
|
48
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. */
|
|
49
53
|
padding: 0.05rem 0.4rem;
|
|
50
54
|
align-self: flex-start;
|
|
51
55
|
}
|
|
52
|
-
[part='trend'][data-direction='up'] {
|
|
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'] {
|
|
53
63
|
color: var(--lyra-color-success);
|
|
54
64
|
background: color-mix(in srgb, var(--lyra-color-success) 8%, transparent);
|
|
55
65
|
}
|
|
56
|
-
[part='trend'][data-
|
|
66
|
+
[part='trend'][data-polarity='bad'] {
|
|
57
67
|
color: var(--lyra-color-danger);
|
|
58
68
|
background: color-mix(in srgb, var(--lyra-color-danger) 8%, transparent);
|
|
59
69
|
}
|
|
@@ -70,7 +80,7 @@ export const styles = css `
|
|
|
70
80
|
font-size: 0.8125rem;
|
|
71
81
|
color: var(--lyra-color-text-quiet);
|
|
72
82
|
}
|
|
73
|
-
[part='caption']
|
|
83
|
+
[part='caption'][hidden] {
|
|
74
84
|
display: none;
|
|
75
85
|
}
|
|
76
86
|
`;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type TemplateResult } from 'lit';
|
|
1
|
+
import { type TemplateResult, type PropertyValues } from 'lit';
|
|
2
2
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
3
|
import '../empty/empty.js';
|
|
4
4
|
export interface TableColumn<T> {
|
|
@@ -9,15 +9,19 @@ export interface TableColumn<T> {
|
|
|
9
9
|
cell: (row: T) => unknown;
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* `<lyra-table>` — a presentational, sort/select-aware data table.
|
|
13
|
-
*
|
|
14
|
-
*
|
|
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).
|
|
15
19
|
*
|
|
16
20
|
* @customElement lyra-table
|
|
17
21
|
* @event lyra-sort - A sortable header was activated. `detail: { key }`.
|
|
18
22
|
* @event lyra-row-click - A row was activated. `detail: { row }`.
|
|
19
23
|
* @event lyra-load-more - The "load more" control was activated.
|
|
20
|
-
* @csspart base, table, head, header-cell, row, cell, more-button
|
|
24
|
+
* @csspart base, table, head, header-cell, row, cell, more-button, sort-icon
|
|
21
25
|
*/
|
|
22
26
|
export declare class LyraTable<T = unknown> extends LyraElement {
|
|
23
27
|
static styles: import("lit").CSSResultGroup[];
|
|
@@ -31,8 +35,16 @@ export declare class LyraTable<T = unknown> extends LyraElement {
|
|
|
31
35
|
moreLabel: string;
|
|
32
36
|
emptyHeading: string;
|
|
33
37
|
emptyDescription: string;
|
|
38
|
+
noColumnsHeading: string;
|
|
39
|
+
noColumnsDescription: string;
|
|
40
|
+
private rowsByKey;
|
|
41
|
+
private columnsByKey;
|
|
34
42
|
private keyOf;
|
|
35
|
-
|
|
43
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
44
|
+
private activateColumn;
|
|
45
|
+
private activateRow;
|
|
46
|
+
private onTableClick;
|
|
47
|
+
private onTableKeyDown;
|
|
36
48
|
render(): TemplateResult;
|
|
37
49
|
}
|
|
38
50
|
declare global {
|
|
@@ -10,17 +10,22 @@ import { repeat } from 'lit/directives/repeat.js';
|
|
|
10
10
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
11
11
|
import { defineElement } from '../../internal/prefix.js';
|
|
12
12
|
import { styles } from './table.styles.js';
|
|
13
|
+
import { chevronIcon } from '../../internal/icons.js';
|
|
13
14
|
import '../empty/empty.js';
|
|
14
15
|
/**
|
|
15
|
-
* `<lyra-table>` — a presentational, sort/select-aware data table.
|
|
16
|
-
*
|
|
17
|
-
*
|
|
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).
|
|
18
23
|
*
|
|
19
24
|
* @customElement lyra-table
|
|
20
25
|
* @event lyra-sort - A sortable header was activated. `detail: { key }`.
|
|
21
26
|
* @event lyra-row-click - A row was activated. `detail: { row }`.
|
|
22
27
|
* @event lyra-load-more - The "load more" control was activated.
|
|
23
|
-
* @csspart base, table, head, header-cell, row, cell, more-button
|
|
28
|
+
* @csspart base, table, head, header-cell, row, cell, more-button, sort-icon
|
|
24
29
|
*/
|
|
25
30
|
export class LyraTable extends LyraElement {
|
|
26
31
|
constructor() {
|
|
@@ -34,17 +39,64 @@ export class LyraTable extends LyraElement {
|
|
|
34
39
|
this.moreLabel = 'Load more';
|
|
35
40
|
this.emptyHeading = 'No data';
|
|
36
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
|
+
};
|
|
37
70
|
}
|
|
38
71
|
static { this.styles = [LyraElement.styles, styles]; }
|
|
39
72
|
keyOf(row, index) {
|
|
40
73
|
return this.rowKey ? this.rowKey(row) : index;
|
|
41
74
|
}
|
|
42
|
-
|
|
43
|
-
if (
|
|
44
|
-
|
|
45
|
-
|
|
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 });
|
|
46
92
|
}
|
|
47
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
|
+
}
|
|
48
100
|
if (this.rows.length === 0) {
|
|
49
101
|
return html `<lyra-empty
|
|
50
102
|
heading=${this.emptyHeading}
|
|
@@ -53,28 +105,28 @@ export class LyraTable extends LyraElement {
|
|
|
53
105
|
}
|
|
54
106
|
return html `
|
|
55
107
|
<div part="base">
|
|
56
|
-
<table part="table" role="grid">
|
|
108
|
+
<table part="table" role="grid" @click=${this.onTableClick} @keydown=${this.onTableKeyDown}>
|
|
57
109
|
<thead part="head">
|
|
58
110
|
<tr role="row">
|
|
59
111
|
${this.columns.map((col) => {
|
|
60
|
-
const
|
|
112
|
+
const active = Boolean(col.sortable) && this.sortKey === col.key;
|
|
113
|
+
const ariaSort = active ? (this.sortDir === 'asc' ? 'ascending' : 'descending') : 'none';
|
|
61
114
|
return html `<th
|
|
62
115
|
part="header-cell"
|
|
63
116
|
role="columnheader"
|
|
64
117
|
scope="col"
|
|
118
|
+
data-col-key=${col.key}
|
|
65
119
|
data-align=${col.align ?? 'start'}
|
|
66
120
|
?data-sortable=${col.sortable}
|
|
67
121
|
aria-sort=${col.sortable ? ariaSort : nothing}
|
|
68
122
|
tabindex=${col.sortable ? '0' : '-1'}
|
|
69
|
-
@click=${() => this.onHeaderClick(col)}
|
|
70
|
-
@keydown=${(e) => {
|
|
71
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
72
|
-
e.preventDefault();
|
|
73
|
-
this.onHeaderClick(col);
|
|
74
|
-
}
|
|
75
|
-
}}
|
|
76
123
|
>
|
|
77
124
|
${col.label}
|
|
125
|
+
${active
|
|
126
|
+
? html `<span part="sort-icon" data-dir=${this.sortDir} aria-hidden="true"
|
|
127
|
+
>${chevronIcon()}</span
|
|
128
|
+
>`
|
|
129
|
+
: nothing}
|
|
78
130
|
</th>`;
|
|
79
131
|
})}
|
|
80
132
|
</tr>
|
|
@@ -86,15 +138,9 @@ export class LyraTable extends LyraElement {
|
|
|
86
138
|
return html `<tr
|
|
87
139
|
part="row"
|
|
88
140
|
role="row"
|
|
141
|
+
data-row-key=${String(key)}
|
|
89
142
|
aria-selected=${selected ? 'true' : 'false'}
|
|
90
143
|
tabindex="0"
|
|
91
|
-
@click=${() => this.emit('lyra-row-click', { row })}
|
|
92
|
-
@keydown=${(e) => {
|
|
93
|
-
if (e.key === 'Enter' || e.key === ' ') {
|
|
94
|
-
e.preventDefault();
|
|
95
|
-
this.emit('lyra-row-click', { row });
|
|
96
|
-
}
|
|
97
|
-
}}
|
|
98
144
|
>
|
|
99
145
|
${this.columns.map((col) => html `<td part="cell" role="gridcell" data-align=${col.align ?? 'start'}>
|
|
100
146
|
${col.cell(row)}
|
|
@@ -146,4 +192,10 @@ __decorate([
|
|
|
146
192
|
__decorate([
|
|
147
193
|
property({ attribute: 'empty-description' })
|
|
148
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);
|
|
149
201
|
defineElement('table', LyraTable);
|
|
@@ -33,15 +33,43 @@ export const styles = css `
|
|
|
33
33
|
[part='header-cell'][data-sortable]:hover {
|
|
34
34
|
background: var(--lyra-color-brand-quiet);
|
|
35
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
|
+
}
|
|
36
40
|
[part='header-cell'][data-align='end'] {
|
|
37
41
|
text-align: end;
|
|
38
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
|
+
}
|
|
39
63
|
[part='row']:hover {
|
|
40
64
|
background: var(--lyra-color-brand-quiet);
|
|
41
65
|
}
|
|
42
66
|
[part='row'][aria-selected='true'] {
|
|
43
67
|
background: var(--lyra-color-brand-quiet);
|
|
44
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
|
+
}
|
|
45
73
|
[part='cell'] {
|
|
46
74
|
padding: var(--lyra-space-s);
|
|
47
75
|
border-block-end: 1px solid var(--lyra-color-border);
|
|
@@ -63,4 +91,8 @@ export const styles = css `
|
|
|
63
91
|
[part='more-button']:hover {
|
|
64
92
|
background: var(--lyra-color-brand-quiet);
|
|
65
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
|
+
}
|
|
66
98
|
`;
|