@aceshooting/lyra-ui 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -9
- package/custom-elements.json +10463 -4519
- package/dist/components/chart/chart.d.ts +46 -1
- package/dist/components/chart/chart.js +148 -10
- package/dist/components/chart/chart.stories.d.ts +32 -0
- package/dist/components/chart/chart.stories.js +141 -0
- package/dist/components/chart/chart.styles.js +16 -0
- package/dist/components/chart/histogram.d.ts +2 -0
- package/dist/components/chart/histogram.js +18 -2
- package/dist/components/chart/line-chart.stories.d.ts +17 -0
- package/dist/components/chart/line-chart.stories.js +75 -0
- package/dist/components/chart/lite-chart.d.ts +74 -0
- package/dist/components/chart/lite-chart.js +384 -0
- package/dist/components/chart/lite-chart.stories.d.ts +12 -0
- package/dist/components/chart/lite-chart.stories.js +98 -0
- package/dist/components/chart/lite-chart.styles.d.ts +1 -0
- package/dist/components/chart/lite-chart.styles.js +83 -0
- package/dist/components/combobox/combobox.d.ts +9 -0
- package/dist/components/combobox/combobox.js +75 -12
- package/dist/components/combobox/combobox.stories.d.ts +18 -0
- package/dist/components/combobox/combobox.stories.js +75 -0
- package/dist/components/date-picker/calendar-core.js +13 -2
- package/dist/components/date-picker/date-input.d.ts +9 -1
- package/dist/components/date-picker/date-input.js +55 -7
- package/dist/components/date-picker/date-input.stories.d.ts +5 -0
- package/dist/components/date-picker/date-input.stories.js +36 -0
- package/dist/components/date-picker/date-picker.d.ts +11 -3
- package/dist/components/date-picker/date-picker.js +87 -37
- package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
- package/dist/components/date-picker/date-picker.stories.js +12 -0
- package/dist/components/date-picker/date-picker.styles.js +1 -1
- package/dist/components/empty/empty.d.ts +12 -1
- package/dist/components/empty/empty.js +20 -3
- package/dist/components/empty/empty.stories.d.ts +2 -0
- package/dist/components/empty/empty.stories.js +20 -0
- package/dist/components/empty/empty.styles.js +11 -0
- package/dist/components/export-button/csv.js +6 -2
- package/dist/components/export-button/export-button.d.ts +15 -1
- package/dist/components/export-button/export-button.js +89 -12
- package/dist/components/export-button/export-button.stories.d.ts +1 -0
- package/dist/components/export-button/export-button.stories.js +5 -0
- package/dist/components/export-button/export-button.styles.js +11 -3
- package/dist/components/file-input/accept.d.ts +8 -2
- package/dist/components/file-input/accept.js +9 -3
- package/dist/components/file-input/file-input.d.ts +5 -2
- package/dist/components/file-input/file-input.js +23 -8
- package/dist/components/file-input/file-input.stories.d.ts +2 -0
- package/dist/components/file-input/file-input.stories.js +9 -0
- package/dist/components/flag/flag.d.ts +48 -3
- package/dist/components/flag/flag.js +65 -29
- package/dist/components/flag/flag.styles.js +1 -1
- package/dist/components/flag/language-map.d.ts +11 -0
- package/dist/components/flag/language-map.js +12 -1
- package/dist/components/gauge/gauge.d.ts +2 -0
- package/dist/components/gauge/gauge.js +19 -5
- package/dist/components/gauge/gauge.stories.d.ts +2 -0
- package/dist/components/gauge/gauge.stories.js +6 -0
- package/dist/components/graph/graph-loader.d.ts +28 -0
- package/dist/components/graph/graph-loader.js +44 -0
- package/dist/components/graph/graph.d.ts +41 -15
- package/dist/components/graph/graph.js +146 -71
- package/dist/components/graph/graph.stories.d.ts +3 -0
- package/dist/components/graph/graph.stories.js +38 -0
- package/dist/components/graph/graph.styles.js +6 -1
- package/dist/components/heatmap/calendar-grid.d.ts +7 -0
- package/dist/components/heatmap/calendar-grid.js +27 -10
- package/dist/components/heatmap/heatmap.d.ts +137 -12
- package/dist/components/heatmap/heatmap.js +427 -27
- package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
- package/dist/components/heatmap/heatmap.stories.js +130 -0
- package/dist/components/heatmap/heatmap.styles.js +60 -2
- package/dist/components/map/map.d.ts +17 -1
- package/dist/components/map/map.js +68 -21
- package/dist/components/map/map.stories.d.ts +10 -0
- package/dist/components/map/map.stories.js +107 -12
- package/dist/components/map/map.styles.js +2 -2
- package/dist/components/playback/playback.d.ts +6 -2
- package/dist/components/playback/playback.js +36 -12
- package/dist/components/playback/playback.stories.d.ts +2 -0
- package/dist/components/playback/playback.stories.js +6 -0
- package/dist/components/playback/playback.styles.js +10 -2
- package/dist/components/select/select.d.ts +122 -0
- package/dist/components/select/select.js +516 -0
- package/dist/components/select/select.stories.d.ts +19 -0
- package/dist/components/select/select.stories.js +75 -0
- package/dist/components/select/select.styles.d.ts +1 -0
- package/dist/components/select/select.styles.js +188 -0
- package/dist/components/skeleton/skeleton.js +4 -7
- package/dist/components/sparkline/sparkline.d.ts +1 -1
- package/dist/components/sparkline/sparkline.js +40 -13
- package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
- package/dist/components/sparkline/sparkline.stories.js +22 -0
- package/dist/components/split/split.d.ts +24 -3
- package/dist/components/split/split.js +155 -31
- package/dist/components/split/split.stories.d.ts +2 -0
- package/dist/components/split/split.stories.js +20 -0
- package/dist/components/stat/stat.d.ts +27 -1
- package/dist/components/stat/stat.js +56 -3
- package/dist/components/stat/stat.stories.d.ts +3 -0
- package/dist/components/stat/stat.stories.js +49 -1
- package/dist/components/stat/stat.styles.js +44 -0
- package/dist/components/table/table.d.ts +64 -2
- package/dist/components/table/table.js +213 -17
- package/dist/components/table/table.stories.d.ts +6 -0
- package/dist/components/table/table.stories.js +45 -0
- package/dist/components/table/table.styles.js +51 -4
- package/dist/components/time-range/time-range.d.ts +34 -5
- package/dist/components/time-range/time-range.js +217 -36
- package/dist/components/time-range/time-range.stories.d.ts +2 -0
- package/dist/components/time-range/time-range.stories.js +13 -0
- package/dist/components/time-range/time-range.styles.js +44 -2
- package/dist/components/toast/toast-item.d.ts +14 -2
- package/dist/components/toast/toast-item.js +134 -17
- package/dist/components/toast/toast-item.styles.js +28 -2
- package/dist/components/toast/toast.stories.d.ts +3 -0
- package/dist/components/toast/toast.stories.js +41 -1
- package/dist/components/toast/toast.styles.js +11 -11
- package/dist/components/toast/toaster.d.ts +1 -1
- package/dist/components/toast/toaster.js +13 -6
- package/dist/components/tree/tree-node.d.ts +13 -7
- package/dist/components/tree/tree-node.js +24 -10
- package/dist/components/tree/tree.d.ts +64 -15
- package/dist/components/tree/tree.js +113 -22
- package/dist/components/tree/tree.stories.d.ts +4 -0
- package/dist/components/tree/tree.stories.js +31 -1
- package/dist/components/tree/update-cascade.d.ts +13 -0
- package/dist/components/tree/update-cascade.js +13 -0
- package/dist/components/widget/widget.d.ts +15 -1
- package/dist/components/widget/widget.js +140 -5
- package/dist/components/widget/widget.stories.d.ts +1 -0
- package/dist/components/widget/widget.stories.js +10 -0
- package/dist/components/widget/widget.styles.js +11 -1
- package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
- package/dist/components/word-cloud/word-cloud-layout.js +145 -0
- package/dist/components/word-cloud/word-cloud.d.ts +65 -0
- package/dist/components/word-cloud/word-cloud.js +255 -0
- package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
- package/dist/components/word-cloud/word-cloud.stories.js +55 -0
- package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
- package/dist/components/word-cloud/word-cloud.styles.js +54 -0
- package/dist/internal/a11y.js +2 -1
- package/dist/internal/rtl.d.ts +7 -0
- package/dist/internal/rtl.js +9 -0
- package/dist/lyra.d.ts +9 -1
- package/dist/lyra.js +6 -0
- package/llms-full.txt +892 -276
- package/llms.txt +12 -9
- package/package.json +6 -4
|
@@ -8,8 +8,14 @@ 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 { isRtl } from '../../internal/rtl.js';
|
|
11
12
|
import { styles } from './split.styles.js';
|
|
12
13
|
const KEYBOARD_STEP = 2;
|
|
14
|
+
// Sentinel fallbacks so a one-sided constraint (only `minPx` or only
|
|
15
|
+
// `maxPx`) can still be expressed as a 3-argument CSS `clamp()` in the
|
|
16
|
+
// static layout, instead of needing two different flex-basis shapes.
|
|
17
|
+
const NO_MIN_PX = 0;
|
|
18
|
+
const NO_MAX_PX = 1_000_000;
|
|
13
19
|
/**
|
|
14
20
|
* `<lyra-split>` — resizable panels for dashboard layouts. Direct light-DOM
|
|
15
21
|
* children are the panels; a divider is auto-inserted between each pair.
|
|
@@ -17,6 +23,7 @@ const KEYBOARD_STEP = 2;
|
|
|
17
23
|
* @customElement lyra-split
|
|
18
24
|
* @event lyra-resize - `detail: { sizes }`, fired on every drag step/release
|
|
19
25
|
* and every keyboard step.
|
|
26
|
+
* @slot - Panels to arrange side by side (or stacked, when `orientation="vertical"`); each direct child becomes one resizable panel.
|
|
20
27
|
* @csspart base, divider
|
|
21
28
|
*/
|
|
22
29
|
export class LyraSplit extends LyraElement {
|
|
@@ -25,41 +32,77 @@ export class LyraSplit extends LyraElement {
|
|
|
25
32
|
this.sizes = [];
|
|
26
33
|
this.min = 10;
|
|
27
34
|
this.orientation = 'horizontal';
|
|
35
|
+
/** Optional fixed-pixel min/max per panel, index-aligned with `sizes`. A
|
|
36
|
+
* `null`/missing entry leaves that panel purely percent-based (the
|
|
37
|
+
* existing `min`-only behavior). `sizes`, the `lyra-resize` payload, and
|
|
38
|
+
* localStorage persistence stay percent-based regardless — only the
|
|
39
|
+
* effective clamp bounds change for a constrained panel. */
|
|
40
|
+
this.panelConstraints = [];
|
|
28
41
|
this.panelCount = 0;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
this.
|
|
42
|
+
// Keyed by pointerId so an interrupted or concurrent (multi-touch) drag on
|
|
43
|
+
// one divider never reads or clobbers another pointer's drag state.
|
|
44
|
+
this.drags = new Map();
|
|
32
45
|
this.onSlotChange = () => {
|
|
33
46
|
this.panelCount = this.children.length;
|
|
34
47
|
this.ensureSizes();
|
|
35
48
|
};
|
|
36
49
|
this.onPointerDown = (e, index) => {
|
|
37
|
-
this.
|
|
38
|
-
this.
|
|
39
|
-
|
|
50
|
+
const base = this.renderRoot.querySelector('[part="base"]');
|
|
51
|
+
this.drags.set(e.pointerId, {
|
|
52
|
+
index,
|
|
53
|
+
startPos: this.orientation === 'vertical' ? e.clientY : e.clientX,
|
|
54
|
+
startSizes: [...this.sizes],
|
|
55
|
+
base,
|
|
56
|
+
});
|
|
40
57
|
e.target.setPointerCapture(e.pointerId);
|
|
41
58
|
window.addEventListener('pointermove', this.onPointerMove);
|
|
42
59
|
window.addEventListener('pointerup', this.onPointerUp);
|
|
60
|
+
// A drag can end without a pointerup: a system gesture / palm rejection
|
|
61
|
+
// can fire `pointercancel`, and losing capture (e.g. element removed)
|
|
62
|
+
// fires `lostpointercapture` — both need the same teardown as pointerup
|
|
63
|
+
// or the divider keeps "resizing" in response to unrelated movement.
|
|
64
|
+
window.addEventListener('pointercancel', this.onPointerUp);
|
|
65
|
+
window.addEventListener('lostpointercapture', this.onPointerUp);
|
|
43
66
|
};
|
|
44
67
|
this.onPointerMove = (e) => {
|
|
45
|
-
|
|
68
|
+
const drag = this.drags.get(e.pointerId);
|
|
69
|
+
if (!drag)
|
|
46
70
|
return;
|
|
47
|
-
const
|
|
48
|
-
const total = this.orientation === 'vertical' ? base.clientHeight : base.clientWidth;
|
|
71
|
+
const total = this.orientation === 'vertical' ? drag.base.clientHeight : drag.base.clientWidth;
|
|
49
72
|
const pos = this.orientation === 'vertical' ? e.clientY : e.clientX;
|
|
50
|
-
|
|
51
|
-
|
|
73
|
+
let deltaPercent = ((pos - drag.startPos) / total) * 100;
|
|
74
|
+
// Panels are ordered along the inline axis via CSS `order`, so under RTL
|
|
75
|
+
// `flex-direction: row` already renders panel[i] to the *right* of
|
|
76
|
+
// panel[i+1] — a physically-rightward drag has to shrink index instead
|
|
77
|
+
// of growing it to keep matching the visible panel under the pointer.
|
|
78
|
+
if (this.orientation === 'horizontal' && isRtl(this))
|
|
79
|
+
deltaPercent = -deltaPercent;
|
|
80
|
+
const paired = this.clampPair(drag.startSizes, drag.index, deltaPercent, total);
|
|
81
|
+
// Merge only this drag's pair into the live sizes so a concurrent drag
|
|
82
|
+
// on another divider (different pointerId) isn't clobbered.
|
|
83
|
+
const next = [...this.sizes];
|
|
84
|
+
next[drag.index] = paired[drag.index];
|
|
85
|
+
next[drag.index + 1] = paired[drag.index + 1];
|
|
86
|
+
this.sizes = next;
|
|
52
87
|
this.emit('lyra-resize', { sizes: [...this.sizes] });
|
|
53
88
|
};
|
|
54
|
-
this.onPointerUp = () => {
|
|
55
|
-
this.
|
|
89
|
+
this.onPointerUp = (e) => {
|
|
90
|
+
if (!this.drags.has(e.pointerId))
|
|
91
|
+
return;
|
|
92
|
+
this.drags.delete(e.pointerId);
|
|
56
93
|
this.persist();
|
|
57
|
-
|
|
58
|
-
|
|
94
|
+
if (this.drags.size === 0) {
|
|
95
|
+
window.removeEventListener('pointermove', this.onPointerMove);
|
|
96
|
+
window.removeEventListener('pointerup', this.onPointerUp);
|
|
97
|
+
window.removeEventListener('pointercancel', this.onPointerUp);
|
|
98
|
+
window.removeEventListener('lostpointercapture', this.onPointerUp);
|
|
99
|
+
}
|
|
59
100
|
};
|
|
60
101
|
this.onDividerKeyDown = (e, index) => {
|
|
61
|
-
|
|
62
|
-
const
|
|
102
|
+
// Mirror the same swap as onPointerMove for horizontal+RTL.
|
|
103
|
+
const rtl = this.orientation === 'horizontal' && isRtl(this);
|
|
104
|
+
const forwardKey = this.orientation === 'vertical' ? 'ArrowDown' : rtl ? 'ArrowLeft' : 'ArrowRight';
|
|
105
|
+
const backwardKey = this.orientation === 'vertical' ? 'ArrowUp' : rtl ? 'ArrowRight' : 'ArrowLeft';
|
|
63
106
|
if (e.key === forwardKey) {
|
|
64
107
|
e.preventDefault();
|
|
65
108
|
this.applyDelta(index, KEYBOARD_STEP, true);
|
|
@@ -79,9 +122,12 @@ export class LyraSplit extends LyraElement {
|
|
|
79
122
|
}
|
|
80
123
|
disconnectedCallback() {
|
|
81
124
|
super.disconnectedCallback();
|
|
82
|
-
// Clean up any remaining event listeners
|
|
125
|
+
// Clean up any remaining event listeners from an in-flight drag.
|
|
126
|
+
this.drags.clear();
|
|
83
127
|
window.removeEventListener('pointermove', this.onPointerMove);
|
|
84
128
|
window.removeEventListener('pointerup', this.onPointerUp);
|
|
129
|
+
window.removeEventListener('pointercancel', this.onPointerUp);
|
|
130
|
+
window.removeEventListener('lostpointercapture', this.onPointerUp);
|
|
85
131
|
}
|
|
86
132
|
get storageFullKey() {
|
|
87
133
|
return this.storageKey ? `lyra-split:${this.storageKey}:${this.panelCount}` : undefined;
|
|
@@ -102,7 +148,12 @@ export class LyraSplit extends LyraElement {
|
|
|
102
148
|
return;
|
|
103
149
|
try {
|
|
104
150
|
const parsed = JSON.parse(raw);
|
|
105
|
-
|
|
151
|
+
// A persisted layout can predate a since-raised `min`; reject it rather
|
|
152
|
+
// than restoring sizes that are already stuck below the current floor.
|
|
153
|
+
const isValid = Array.isArray(parsed) &&
|
|
154
|
+
parsed.length === this.panelCount &&
|
|
155
|
+
parsed.every((n) => typeof n === 'number' && Number.isFinite(n) && n >= this.min);
|
|
156
|
+
if (isValid)
|
|
106
157
|
this.sizes = parsed;
|
|
107
158
|
}
|
|
108
159
|
catch {
|
|
@@ -121,23 +172,84 @@ export class LyraSplit extends LyraElement {
|
|
|
121
172
|
}
|
|
122
173
|
}
|
|
123
174
|
ensureSizes() {
|
|
124
|
-
if (this.
|
|
175
|
+
if (this.panelCount === this.sizes.length)
|
|
125
176
|
return;
|
|
126
|
-
|
|
127
|
-
|
|
177
|
+
if (this.panelCount <= 0) {
|
|
178
|
+
this.sizes = [];
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
if (this.sizes.length === 0) {
|
|
182
|
+
const equal = 100 / this.panelCount;
|
|
183
|
+
this.sizes = Array.from({ length: this.panelCount }, () => equal);
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// A panel was added or removed: rebalance the existing sizes
|
|
187
|
+
// proportionally instead of discarding every panel's customized size,
|
|
188
|
+
// so an unrelated panel-count change doesn't wipe the whole layout.
|
|
189
|
+
const diff = this.panelCount - this.sizes.length;
|
|
190
|
+
if (diff > 0) {
|
|
191
|
+
const newShare = 100 / this.panelCount;
|
|
192
|
+
const scale = (100 - newShare * diff) / 100;
|
|
193
|
+
this.sizes = [
|
|
194
|
+
...this.sizes.map((s) => s * scale),
|
|
195
|
+
...Array.from({ length: diff }, () => newShare),
|
|
196
|
+
];
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const kept = this.sizes.slice(0, this.panelCount);
|
|
200
|
+
const removedTotal = this.sizes.slice(this.panelCount).reduce((sum, s) => sum + s, 0);
|
|
201
|
+
const keptTotal = kept.reduce((sum, s) => sum + s, 0) || 1;
|
|
202
|
+
this.sizes = kept.map((s) => s + (s / keptTotal) * removedTotal);
|
|
203
|
+
}
|
|
128
204
|
}
|
|
129
|
-
|
|
205
|
+
/** The container extent (px) along the resize axis, read live so a
|
|
206
|
+
* container resize between calls is always picked up — same live read
|
|
207
|
+
* `onPointerMove` already does via `drag.base.clientWidth/clientHeight`. */
|
|
208
|
+
getContainerSize() {
|
|
209
|
+
const base = this.renderRoot.querySelector('[part="base"]');
|
|
210
|
+
if (!base)
|
|
211
|
+
return 0;
|
|
212
|
+
return this.orientation === 'vertical' ? base.clientHeight : base.clientWidth;
|
|
213
|
+
}
|
|
214
|
+
/** Resolves panel `index`'s effective percent min/max for this clamp pass:
|
|
215
|
+
* a `panelConstraints` entry's px bound converted against the live
|
|
216
|
+
* `containerSize`, or the component's plain percent `min` (no upper
|
|
217
|
+
* bound) when the panel carries no constraint for that side. */
|
|
218
|
+
percentBounds(index, containerSize) {
|
|
219
|
+
const constraint = this.panelConstraints[index];
|
|
220
|
+
let min = this.min;
|
|
221
|
+
let max = Infinity;
|
|
222
|
+
if (constraint && containerSize > 0) {
|
|
223
|
+
if (constraint.minPx != null)
|
|
224
|
+
min = (constraint.minPx / containerSize) * 100;
|
|
225
|
+
if (constraint.maxPx != null)
|
|
226
|
+
max = (constraint.maxPx / containerSize) * 100;
|
|
227
|
+
}
|
|
228
|
+
return { min, max };
|
|
229
|
+
}
|
|
230
|
+
clampPair(sizes, i, delta, containerSize = 0) {
|
|
130
231
|
const next = [...sizes];
|
|
131
|
-
const
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
232
|
+
const pairTotal = next[i] + next[i + 1];
|
|
233
|
+
const a = this.percentBounds(i, containerSize);
|
|
234
|
+
const b = this.percentBounds(i + 1, containerSize);
|
|
235
|
+
// Panel i's own bounds, further narrowed by panel i+1's bounds (its
|
|
236
|
+
// partner's min/max caps how much i can grow/shrink within the pair).
|
|
237
|
+
const loRaw = Math.max(a.min, pairTotal - b.max);
|
|
238
|
+
const hiRaw = Math.min(a.max, pairTotal - b.min);
|
|
239
|
+
// Clamp *toward* the bound instead of rejecting the whole move when the
|
|
240
|
+
// result is still outside it — otherwise a pair that starts under its
|
|
241
|
+
// combined min (e.g. an equal split across many panels) can never be
|
|
242
|
+
// moved at all, since every step recomputes the same rejected delta from
|
|
243
|
+
// the same untouched starting sizes.
|
|
244
|
+
const lo = Math.min(loRaw, pairTotal);
|
|
245
|
+
const hi = Math.max(hiRaw, lo);
|
|
246
|
+
const clampedA = Math.min(Math.max(next[i] + delta, lo), hi);
|
|
247
|
+
next[i] = clampedA;
|
|
248
|
+
next[i + 1] = pairTotal - clampedA;
|
|
137
249
|
return next;
|
|
138
250
|
}
|
|
139
251
|
applyDelta(index, delta, commit) {
|
|
140
|
-
this.sizes = this.clampPair(this.sizes, index, delta);
|
|
252
|
+
this.sizes = this.clampPair(this.sizes, index, delta, this.getContainerSize());
|
|
141
253
|
this.emit('lyra-resize', { sizes: [...this.sizes] });
|
|
142
254
|
if (commit)
|
|
143
255
|
this.persist();
|
|
@@ -147,7 +259,15 @@ export class LyraSplit extends LyraElement {
|
|
|
147
259
|
this.ensureSizes();
|
|
148
260
|
const panels = [...this.children];
|
|
149
261
|
panels.forEach((panel, i) => {
|
|
150
|
-
|
|
262
|
+
const percent = this.sizes[i] ?? 0;
|
|
263
|
+
const constraint = this.panelConstraints[i];
|
|
264
|
+
// clamp() mixes units natively, so a constrained panel stays pinned
|
|
265
|
+
// between its px bounds across container resizes with no extra
|
|
266
|
+
// ResizeObserver — the browser re-evaluates it on every layout pass.
|
|
267
|
+
panel.style.flex =
|
|
268
|
+
constraint && (constraint.minPx != null || constraint.maxPx != null)
|
|
269
|
+
? `0 0 clamp(${constraint.minPx ?? NO_MIN_PX}px, ${percent}%, ${constraint.maxPx ?? NO_MAX_PX}px)`
|
|
270
|
+
: `0 0 ${percent}%`;
|
|
151
271
|
panel.style.order = String(i * 2);
|
|
152
272
|
});
|
|
153
273
|
}
|
|
@@ -161,6 +281,7 @@ export class LyraSplit extends LyraElement {
|
|
|
161
281
|
dividers.push(html `<div
|
|
162
282
|
part="divider"
|
|
163
283
|
role="separator"
|
|
284
|
+
aria-label="Resize divider between panel ${i + 1} and panel ${i + 2}"
|
|
164
285
|
aria-orientation=${this.orientation === 'vertical' ? 'horizontal' : 'vertical'}
|
|
165
286
|
aria-valuenow=${Math.round(this.sizes[i] ?? 0)}
|
|
166
287
|
aria-valuemin=${this.min}
|
|
@@ -186,6 +307,9 @@ __decorate([
|
|
|
186
307
|
__decorate([
|
|
187
308
|
property({ attribute: 'storage-key' })
|
|
188
309
|
], LyraSplit.prototype, "storageKey", void 0);
|
|
310
|
+
__decorate([
|
|
311
|
+
property({ attribute: false })
|
|
312
|
+
], LyraSplit.prototype, "panelConstraints", void 0);
|
|
189
313
|
__decorate([
|
|
190
314
|
state()
|
|
191
315
|
], LyraSplit.prototype, "panelCount", void 0);
|
|
@@ -14,3 +14,23 @@ export const Default = {
|
|
|
14
14
|
</lyra-split>
|
|
15
15
|
`,
|
|
16
16
|
};
|
|
17
|
+
export const Vertical = {
|
|
18
|
+
render: () => html `
|
|
19
|
+
<lyra-split orientation="vertical" style="height: 16rem; border: 1px solid #ddd">
|
|
20
|
+
<div style="padding: 0.5rem">Panel A</div>
|
|
21
|
+
<div style="padding: 0.5rem">Panel B</div>
|
|
22
|
+
<div style="padding: 0.5rem">Panel C</div>
|
|
23
|
+
</lyra-split>
|
|
24
|
+
`,
|
|
25
|
+
};
|
|
26
|
+
export const FixedPixelRangePanel = {
|
|
27
|
+
render: () => html `
|
|
28
|
+
<lyra-split
|
|
29
|
+
style="height: 8rem; border: 1px solid #ddd"
|
|
30
|
+
.panelConstraints=${[{ minPx: 160, maxPx: 320 }, null]}
|
|
31
|
+
>
|
|
32
|
+
<div style="padding: 0.5rem">Sidebar — pinned between 160px and 320px regardless of the split's own percent-based sizing or a container resize</div>
|
|
33
|
+
<div style="padding: 0.5rem">Main content — fills the rest, percent-based as usual</div>
|
|
34
|
+
</lyra-split>
|
|
35
|
+
`,
|
|
36
|
+
};
|
|
@@ -2,6 +2,10 @@ import { type TemplateResult } from 'lit';
|
|
|
2
2
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
3
3
|
export type StatVariant = 'neutral' | 'success' | 'warning' | 'danger';
|
|
4
4
|
export type StatGoodDirection = 'up' | 'down';
|
|
5
|
+
export interface StatRow {
|
|
6
|
+
label: string;
|
|
7
|
+
value: string;
|
|
8
|
+
}
|
|
5
9
|
/**
|
|
6
10
|
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
7
11
|
* "metric row" / "KPI card" pattern common to dashboard UIs.
|
|
@@ -9,7 +13,21 @@ export type StatGoodDirection = 'up' | 'down';
|
|
|
9
13
|
* @customElement lyra-stat
|
|
10
14
|
* @slot - Leading icon.
|
|
11
15
|
* @slot caption - Rich caption content (overrides the `caption` attribute).
|
|
12
|
-
* @
|
|
16
|
+
* @slot spark - A sparkline (e.g. `<lyra-sparkline>`) or other compact trend
|
|
17
|
+
* visual. `lyra-stat` only reserves the slot; it doesn't render one itself.
|
|
18
|
+
* @csspart base - The component's root wrapper.
|
|
19
|
+
* @csspart icon - Container for the leading icon slot.
|
|
20
|
+
* @csspart label - The label text.
|
|
21
|
+
* @csspart value-row - Wrapper around the value and unit.
|
|
22
|
+
* @csspart value - The value text.
|
|
23
|
+
* @csspart unit - The unit text.
|
|
24
|
+
* @csspart trend - The trend pill.
|
|
25
|
+
* @csspart spark - Container for the `spark` slot.
|
|
26
|
+
* @csspart caption - Container for the caption attribute/slot.
|
|
27
|
+
* @csspart rows - Container for the `rows` breakdown list.
|
|
28
|
+
* @csspart row - A single breakdown row (one per `rows` entry).
|
|
29
|
+
* @csspart row-label - The label text of a breakdown row.
|
|
30
|
+
* @csspart row-value - The value text of a breakdown row.
|
|
13
31
|
*/
|
|
14
32
|
export declare class LyraStat extends LyraElement {
|
|
15
33
|
static styles: import("lit").CSSResultGroup[];
|
|
@@ -22,11 +40,19 @@ export declare class LyraStat extends LyraElement {
|
|
|
22
40
|
/** Which trend direction counts as "good" — inverts arrow/color polarity for
|
|
23
41
|
* cost/latency/error-rate-style metrics where a decrease is the win. */
|
|
24
42
|
goodDirection: StatGoodDirection;
|
|
43
|
+
/** Breakdown rows rendered as a simple label/value list beneath the caption. */
|
|
44
|
+
rows: StatRow[];
|
|
45
|
+
/** Visual emphasis (e.g. for a "headline" stat in a group) — orthogonal to
|
|
46
|
+
* the status `variant`; see the `[part='value']` selector below for how
|
|
47
|
+
* the two combine. */
|
|
48
|
+
emphasis: boolean;
|
|
25
49
|
private hasIcon;
|
|
26
50
|
private hasCaptionSlot;
|
|
51
|
+
private hasSparkSlot;
|
|
27
52
|
protected willUpdate(): void;
|
|
28
53
|
private onIconSlotChange;
|
|
29
54
|
private onCaptionSlotChange;
|
|
55
|
+
private onSparkSlotChange;
|
|
30
56
|
render(): TemplateResult;
|
|
31
57
|
}
|
|
32
58
|
declare global {
|
|
@@ -9,6 +9,7 @@ import { property, state } from 'lit/decorators.js';
|
|
|
9
9
|
import { LyraElement } from '../../internal/lyra-element.js';
|
|
10
10
|
import { defineElement } from '../../internal/prefix.js';
|
|
11
11
|
import { chevronIcon } from '../../internal/icons.js';
|
|
12
|
+
import { srOnly } from '../../internal/a11y.js';
|
|
12
13
|
import { styles } from './stat.styles.js';
|
|
13
14
|
/**
|
|
14
15
|
* `<lyra-stat>` — a KPI/stat card. First-party invention consolidating the
|
|
@@ -17,7 +18,21 @@ import { styles } from './stat.styles.js';
|
|
|
17
18
|
* @customElement lyra-stat
|
|
18
19
|
* @slot - Leading icon.
|
|
19
20
|
* @slot caption - Rich caption content (overrides the `caption` attribute).
|
|
20
|
-
* @
|
|
21
|
+
* @slot spark - A sparkline (e.g. `<lyra-sparkline>`) or other compact trend
|
|
22
|
+
* visual. `lyra-stat` only reserves the slot; it doesn't render one itself.
|
|
23
|
+
* @csspart base - The component's root wrapper.
|
|
24
|
+
* @csspart icon - Container for the leading icon slot.
|
|
25
|
+
* @csspart label - The label text.
|
|
26
|
+
* @csspart value-row - Wrapper around the value and unit.
|
|
27
|
+
* @csspart value - The value text.
|
|
28
|
+
* @csspart unit - The unit text.
|
|
29
|
+
* @csspart trend - The trend pill.
|
|
30
|
+
* @csspart spark - Container for the `spark` slot.
|
|
31
|
+
* @csspart caption - Container for the caption attribute/slot.
|
|
32
|
+
* @csspart rows - Container for the `rows` breakdown list.
|
|
33
|
+
* @csspart row - A single breakdown row (one per `rows` entry).
|
|
34
|
+
* @csspart row-label - The label text of a breakdown row.
|
|
35
|
+
* @csspart row-value - The value text of a breakdown row.
|
|
21
36
|
*/
|
|
22
37
|
export class LyraStat extends LyraElement {
|
|
23
38
|
constructor() {
|
|
@@ -31,23 +46,34 @@ export class LyraStat extends LyraElement {
|
|
|
31
46
|
/** Which trend direction counts as "good" — inverts arrow/color polarity for
|
|
32
47
|
* cost/latency/error-rate-style metrics where a decrease is the win. */
|
|
33
48
|
this.goodDirection = 'up';
|
|
49
|
+
/** Breakdown rows rendered as a simple label/value list beneath the caption. */
|
|
50
|
+
this.rows = [];
|
|
51
|
+
/** Visual emphasis (e.g. for a "headline" stat in a group) — orthogonal to
|
|
52
|
+
* the status `variant`; see the `[part='value']` selector below for how
|
|
53
|
+
* the two combine. */
|
|
54
|
+
this.emphasis = false;
|
|
34
55
|
// Same fix `lyra-empty` already established: `[part]:empty` never matches
|
|
35
56
|
// because the part always contains a literal `<slot>` child. Track real
|
|
36
57
|
// slot assignment in JS instead.
|
|
37
58
|
this.hasIcon = false;
|
|
38
59
|
this.hasCaptionSlot = false;
|
|
60
|
+
this.hasSparkSlot = false;
|
|
39
61
|
this.onIconSlotChange = (e) => {
|
|
40
62
|
this.hasIcon = e.target.assignedElements({ flatten: true }).length > 0;
|
|
41
63
|
};
|
|
42
64
|
this.onCaptionSlotChange = (e) => {
|
|
43
65
|
this.hasCaptionSlot = e.target.assignedElements({ flatten: true }).length > 0;
|
|
44
66
|
};
|
|
67
|
+
this.onSparkSlotChange = (e) => {
|
|
68
|
+
this.hasSparkSlot = e.target.assignedElements({ flatten: true }).length > 0;
|
|
69
|
+
};
|
|
45
70
|
}
|
|
46
|
-
static { this.styles = [LyraElement.styles, styles]; }
|
|
71
|
+
static { this.styles = [LyraElement.styles, styles, srOnly]; }
|
|
47
72
|
willUpdate() {
|
|
48
73
|
if (!this.hasUpdated) {
|
|
49
74
|
this.hasIcon = Array.from(this.children).some((el) => !el.hasAttribute('slot'));
|
|
50
75
|
this.hasCaptionSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'caption');
|
|
76
|
+
this.hasSparkSlot = Array.from(this.children).some((el) => el.getAttribute('slot') === 'spark');
|
|
51
77
|
}
|
|
52
78
|
}
|
|
53
79
|
render() {
|
|
@@ -60,6 +86,12 @@ export class LyraStat extends LyraElement {
|
|
|
60
86
|
// [part='trend'].
|
|
61
87
|
const arrow = rawDirection === 'flat' ? '–' : chevronIcon();
|
|
62
88
|
const hasCaption = this.hasCaptionSlot || this.caption.length > 0;
|
|
89
|
+
// The visible pill only ever shows the icon rotation + color to convey
|
|
90
|
+
// direction and good/bad polarity; both are invisible to screen readers,
|
|
91
|
+
// so mirror them into a plain-language sr-only announcement.
|
|
92
|
+
const trendAnnouncement = rawDirection === 'flat'
|
|
93
|
+
? 'unchanged'
|
|
94
|
+
: `${rawDirection === 'up' ? 'increased' : 'decreased'} ${Math.abs(this.trend)}%${isGood == null ? '' : isGood ? ', good' : ', bad'}`;
|
|
63
95
|
return html `
|
|
64
96
|
<div part="base">
|
|
65
97
|
<span part="icon" ?hidden=${!this.hasIcon}
|
|
@@ -76,12 +108,24 @@ export class LyraStat extends LyraElement {
|
|
|
76
108
|
data-direction=${rawDirection}
|
|
77
109
|
data-polarity=${isGood == null ? nothing : isGood ? 'good' : 'bad'}
|
|
78
110
|
>
|
|
79
|
-
<span aria-hidden="true">${arrow}
|
|
111
|
+
<span aria-hidden="true">${arrow} ${Math.abs(this.trend)}%</span>
|
|
112
|
+
<span class="sr-only">${trendAnnouncement}</span>
|
|
80
113
|
</span>`
|
|
81
114
|
: nothing}
|
|
115
|
+
<div part="spark" ?hidden=${!this.hasSparkSlot}>
|
|
116
|
+
<slot name="spark" @slotchange=${this.onSparkSlotChange}></slot>
|
|
117
|
+
</div>
|
|
82
118
|
<div part="caption" ?hidden=${!hasCaption}>
|
|
83
119
|
<slot name="caption" @slotchange=${this.onCaptionSlotChange}>${this.caption}</slot>
|
|
84
120
|
</div>
|
|
121
|
+
<div part="rows" ?hidden=${this.rows.length === 0}>
|
|
122
|
+
${this.rows.map((row) => html `
|
|
123
|
+
<div part="row">
|
|
124
|
+
<span part="row-label">${row.label}</span>
|
|
125
|
+
<span part="row-value">${row.value}</span>
|
|
126
|
+
</div>
|
|
127
|
+
`)}
|
|
128
|
+
</div>
|
|
85
129
|
</div>
|
|
86
130
|
`;
|
|
87
131
|
}
|
|
@@ -107,10 +151,19 @@ __decorate([
|
|
|
107
151
|
__decorate([
|
|
108
152
|
property({ attribute: 'good-direction' })
|
|
109
153
|
], LyraStat.prototype, "goodDirection", void 0);
|
|
154
|
+
__decorate([
|
|
155
|
+
property({ attribute: false })
|
|
156
|
+
], LyraStat.prototype, "rows", void 0);
|
|
157
|
+
__decorate([
|
|
158
|
+
property({ type: Boolean, reflect: true })
|
|
159
|
+
], LyraStat.prototype, "emphasis", void 0);
|
|
110
160
|
__decorate([
|
|
111
161
|
state()
|
|
112
162
|
], LyraStat.prototype, "hasIcon", void 0);
|
|
113
163
|
__decorate([
|
|
114
164
|
state()
|
|
115
165
|
], LyraStat.prototype, "hasCaptionSlot", void 0);
|
|
166
|
+
__decorate([
|
|
167
|
+
state()
|
|
168
|
+
], LyraStat.prototype, "hasSparkSlot", void 0);
|
|
116
169
|
defineElement('stat', LyraStat);
|
|
@@ -7,10 +7,58 @@ const meta = {
|
|
|
7
7
|
export default meta;
|
|
8
8
|
export const Gallery = {
|
|
9
9
|
render: () => html `
|
|
10
|
-
<div
|
|
10
|
+
<div class="flex flex-wrap gap-4">
|
|
11
11
|
<lyra-stat label="Revenue" value="12.4" unit="k€" trend="3.2" variant="success"></lyra-stat>
|
|
12
12
|
<lyra-stat label="Errors" value="128" trend="-5.1" variant="danger"></lyra-stat>
|
|
13
|
+
<lyra-stat label="Pending Reviews" value="42" trend="8.6" variant="warning"></lyra-stat>
|
|
13
14
|
<lyra-stat label="Sessions" value="9,204"></lyra-stat>
|
|
15
|
+
<lyra-stat label="Uptime" value="99.98" unit="%" caption="Last 30 days">
|
|
16
|
+
<svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" stroke-width="2">
|
|
17
|
+
<circle cx="12" cy="12" r="9"></circle>
|
|
18
|
+
</svg>
|
|
19
|
+
</lyra-stat>
|
|
20
|
+
<lyra-stat label="Latency" value="182" unit="ms" trend="14" good-direction="down">
|
|
21
|
+
<span slot="caption">Median over <strong>1,000</strong> requests</span>
|
|
22
|
+
</lyra-stat>
|
|
23
|
+
</div>
|
|
24
|
+
`,
|
|
25
|
+
};
|
|
26
|
+
export const SparkAndBreakdown = {
|
|
27
|
+
render: () => html `
|
|
28
|
+
<div class="flex flex-wrap gap-4">
|
|
29
|
+
<lyra-stat label="Revenue" value="12.4" unit="k€" trend="3.2" variant="success" caption="Last 30 days">
|
|
30
|
+
<lyra-sparkline
|
|
31
|
+
slot="spark"
|
|
32
|
+
type="line"
|
|
33
|
+
.values=${[4, 6, 5, 8, 7, 9, 12, 10, 13, 12.4]}
|
|
34
|
+
></lyra-sparkline>
|
|
35
|
+
</lyra-stat>
|
|
36
|
+
<lyra-stat label="Sessions" value="9,204" caption="By channel">
|
|
37
|
+
<lyra-sparkline slot="spark" type="bar" .values=${[3, 5, 4, 6, 8, 7, 9]}></lyra-sparkline>
|
|
38
|
+
</lyra-stat>
|
|
39
|
+
</div>
|
|
40
|
+
`,
|
|
41
|
+
};
|
|
42
|
+
export const BreakdownRows = {
|
|
43
|
+
render: () => html `
|
|
44
|
+
<div class="flex flex-wrap gap-4">
|
|
45
|
+
<lyra-stat label="Sessions" value="9,204" caption="By channel"></lyra-stat>
|
|
46
|
+
</div>
|
|
47
|
+
`,
|
|
48
|
+
play: async ({ canvasElement }) => {
|
|
49
|
+
const el = canvasElement.querySelector('lyra-stat');
|
|
50
|
+
el.rows = [
|
|
51
|
+
{ label: 'Direct', value: '64%' },
|
|
52
|
+
{ label: 'Referral', value: '21%' },
|
|
53
|
+
{ label: 'Other', value: '15%' },
|
|
54
|
+
];
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
export const Emphasis = {
|
|
58
|
+
render: () => html `
|
|
59
|
+
<div class="flex flex-wrap gap-4">
|
|
60
|
+
<lyra-stat label="Revenue" value="12.4" unit="k€" trend="3.2" emphasis></lyra-stat>
|
|
61
|
+
<lyra-stat label="Errors" value="128" trend="-5.1" variant="danger" emphasis></lyra-stat>
|
|
14
62
|
</div>
|
|
15
63
|
`,
|
|
16
64
|
};
|
|
@@ -76,6 +76,14 @@ export const styles = css `
|
|
|
76
76
|
:host([variant='danger']) [part='value'] {
|
|
77
77
|
color: var(--lyra-color-danger);
|
|
78
78
|
}
|
|
79
|
+
[part='spark'] {
|
|
80
|
+
/* Consumers compose their own <lyra-sparkline slot="spark">; this part
|
|
81
|
+
only needs to reserve width for it. */
|
|
82
|
+
display: block;
|
|
83
|
+
}
|
|
84
|
+
[part='spark'][hidden] {
|
|
85
|
+
display: none;
|
|
86
|
+
}
|
|
79
87
|
[part='caption'] {
|
|
80
88
|
font-size: 0.8125rem;
|
|
81
89
|
color: var(--lyra-color-text-quiet);
|
|
@@ -83,4 +91,40 @@ export const styles = css `
|
|
|
83
91
|
[part='caption'][hidden] {
|
|
84
92
|
display: none;
|
|
85
93
|
}
|
|
94
|
+
[part='rows'] {
|
|
95
|
+
display: flex;
|
|
96
|
+
flex-direction: column;
|
|
97
|
+
/* Slightly tighter than the card's own --lyra-space-xs gap so the
|
|
98
|
+
breakdown list reads as one nested group rather than siblings of
|
|
99
|
+
equal weight with label/value/caption. */
|
|
100
|
+
gap: 0.125rem;
|
|
101
|
+
}
|
|
102
|
+
[part='rows'][hidden] {
|
|
103
|
+
display: none;
|
|
104
|
+
}
|
|
105
|
+
[part='row'] {
|
|
106
|
+
display: flex;
|
|
107
|
+
align-items: baseline;
|
|
108
|
+
justify-content: space-between;
|
|
109
|
+
gap: var(--lyra-space-xs);
|
|
110
|
+
font-size: 0.8125rem;
|
|
111
|
+
}
|
|
112
|
+
[part='row-label'] {
|
|
113
|
+
color: var(--lyra-color-text-quiet);
|
|
114
|
+
}
|
|
115
|
+
[part='row-value'] {
|
|
116
|
+
font-family: ui-monospace, SFMono-Regular, monospace;
|
|
117
|
+
font-weight: 600;
|
|
118
|
+
}
|
|
119
|
+
/* Orthogonal to the status variant: an accent edge that marks a stat as
|
|
120
|
+
visually emphasized (e.g. the "headline" stat in a group) regardless of
|
|
121
|
+
status color. */
|
|
122
|
+
:host([emphasis]) [part='base'] {
|
|
123
|
+
border-inline-start: 3px solid var(--lyra-color-brand);
|
|
124
|
+
}
|
|
125
|
+
/* Status semantics win over visual emphasis: only tint the value with the
|
|
126
|
+
brand color when there's no status variant already claiming it. */
|
|
127
|
+
:host([emphasis][variant='neutral']) [part='value'] {
|
|
128
|
+
color: var(--lyra-color-brand);
|
|
129
|
+
}
|
|
86
130
|
`;
|