@aceshooting/lyra-ui 1.0.1 → 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.
Files changed (148) hide show
  1. package/README.md +8 -9
  2. package/custom-elements.json +12479 -6535
  3. package/dist/components/chart/chart.d.ts +46 -1
  4. package/dist/components/chart/chart.js +148 -10
  5. package/dist/components/chart/chart.stories.d.ts +32 -0
  6. package/dist/components/chart/chart.stories.js +141 -0
  7. package/dist/components/chart/chart.styles.js +16 -0
  8. package/dist/components/chart/histogram.d.ts +2 -0
  9. package/dist/components/chart/histogram.js +18 -2
  10. package/dist/components/chart/line-chart.stories.d.ts +17 -0
  11. package/dist/components/chart/line-chart.stories.js +75 -0
  12. package/dist/components/chart/lite-chart.d.ts +74 -0
  13. package/dist/components/chart/lite-chart.js +384 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +12 -0
  15. package/dist/components/chart/lite-chart.stories.js +98 -0
  16. package/dist/components/chart/lite-chart.styles.d.ts +1 -0
  17. package/dist/components/chart/lite-chart.styles.js +83 -0
  18. package/dist/components/combobox/combobox.d.ts +9 -0
  19. package/dist/components/combobox/combobox.js +75 -12
  20. package/dist/components/combobox/combobox.stories.d.ts +18 -0
  21. package/dist/components/combobox/combobox.stories.js +75 -0
  22. package/dist/components/date-picker/calendar-core.js +13 -2
  23. package/dist/components/date-picker/date-input.d.ts +9 -1
  24. package/dist/components/date-picker/date-input.js +55 -7
  25. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  26. package/dist/components/date-picker/date-input.stories.js +36 -0
  27. package/dist/components/date-picker/date-picker.d.ts +11 -3
  28. package/dist/components/date-picker/date-picker.js +87 -37
  29. package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
  30. package/dist/components/date-picker/date-picker.stories.js +12 -0
  31. package/dist/components/date-picker/date-picker.styles.js +1 -1
  32. package/dist/components/empty/empty.d.ts +12 -1
  33. package/dist/components/empty/empty.js +20 -3
  34. package/dist/components/empty/empty.stories.d.ts +2 -0
  35. package/dist/components/empty/empty.stories.js +20 -0
  36. package/dist/components/empty/empty.styles.js +11 -0
  37. package/dist/components/export-button/csv.js +6 -2
  38. package/dist/components/export-button/export-button.d.ts +15 -1
  39. package/dist/components/export-button/export-button.js +89 -12
  40. package/dist/components/export-button/export-button.stories.d.ts +1 -0
  41. package/dist/components/export-button/export-button.stories.js +5 -0
  42. package/dist/components/export-button/export-button.styles.js +11 -3
  43. package/dist/components/file-input/accept.d.ts +8 -2
  44. package/dist/components/file-input/accept.js +9 -3
  45. package/dist/components/file-input/file-input.d.ts +5 -2
  46. package/dist/components/file-input/file-input.js +23 -8
  47. package/dist/components/file-input/file-input.stories.d.ts +2 -0
  48. package/dist/components/file-input/file-input.stories.js +9 -0
  49. package/dist/components/flag/flag.d.ts +48 -3
  50. package/dist/components/flag/flag.js +65 -29
  51. package/dist/components/flag/flag.styles.js +1 -1
  52. package/dist/components/flag/language-map.d.ts +11 -0
  53. package/dist/components/flag/language-map.js +12 -1
  54. package/dist/components/gauge/gauge.d.ts +2 -0
  55. package/dist/components/gauge/gauge.js +19 -5
  56. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  57. package/dist/components/gauge/gauge.stories.js +6 -0
  58. package/dist/components/graph/graph-loader.d.ts +28 -0
  59. package/dist/components/graph/graph-loader.js +44 -0
  60. package/dist/components/graph/graph.d.ts +41 -15
  61. package/dist/components/graph/graph.js +146 -71
  62. package/dist/components/graph/graph.stories.d.ts +3 -0
  63. package/dist/components/graph/graph.stories.js +38 -0
  64. package/dist/components/graph/graph.styles.js +6 -1
  65. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  66. package/dist/components/heatmap/calendar-grid.js +27 -10
  67. package/dist/components/heatmap/heatmap.d.ts +137 -12
  68. package/dist/components/heatmap/heatmap.js +427 -27
  69. package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
  70. package/dist/components/heatmap/heatmap.stories.js +130 -0
  71. package/dist/components/heatmap/heatmap.styles.js +60 -2
  72. package/dist/components/map/map.d.ts +17 -1
  73. package/dist/components/map/map.js +68 -21
  74. package/dist/components/map/map.stories.d.ts +10 -0
  75. package/dist/components/map/map.stories.js +107 -12
  76. package/dist/components/map/map.styles.js +2 -2
  77. package/dist/components/playback/playback.d.ts +6 -2
  78. package/dist/components/playback/playback.js +36 -12
  79. package/dist/components/playback/playback.stories.d.ts +2 -0
  80. package/dist/components/playback/playback.stories.js +6 -0
  81. package/dist/components/playback/playback.styles.js +10 -2
  82. package/dist/components/select/select.d.ts +122 -0
  83. package/dist/components/select/select.js +516 -0
  84. package/dist/components/select/select.stories.d.ts +19 -0
  85. package/dist/components/select/select.stories.js +75 -0
  86. package/dist/components/select/select.styles.d.ts +1 -0
  87. package/dist/components/select/select.styles.js +188 -0
  88. package/dist/components/skeleton/skeleton.js +4 -7
  89. package/dist/components/sparkline/sparkline.d.ts +1 -1
  90. package/dist/components/sparkline/sparkline.js +40 -13
  91. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  92. package/dist/components/sparkline/sparkline.stories.js +22 -0
  93. package/dist/components/split/split.d.ts +24 -3
  94. package/dist/components/split/split.js +155 -31
  95. package/dist/components/split/split.stories.d.ts +2 -0
  96. package/dist/components/split/split.stories.js +20 -0
  97. package/dist/components/stat/stat.d.ts +27 -1
  98. package/dist/components/stat/stat.js +56 -3
  99. package/dist/components/stat/stat.stories.d.ts +3 -0
  100. package/dist/components/stat/stat.stories.js +49 -1
  101. package/dist/components/stat/stat.styles.js +44 -0
  102. package/dist/components/table/table.d.ts +64 -2
  103. package/dist/components/table/table.js +213 -17
  104. package/dist/components/table/table.stories.d.ts +6 -0
  105. package/dist/components/table/table.stories.js +45 -0
  106. package/dist/components/table/table.styles.js +51 -4
  107. package/dist/components/time-range/time-range.d.ts +34 -5
  108. package/dist/components/time-range/time-range.js +217 -36
  109. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  110. package/dist/components/time-range/time-range.stories.js +13 -0
  111. package/dist/components/time-range/time-range.styles.js +44 -2
  112. package/dist/components/toast/toast-item.d.ts +14 -2
  113. package/dist/components/toast/toast-item.js +134 -17
  114. package/dist/components/toast/toast-item.styles.js +28 -2
  115. package/dist/components/toast/toast.stories.d.ts +3 -0
  116. package/dist/components/toast/toast.stories.js +41 -1
  117. package/dist/components/toast/toast.styles.js +11 -11
  118. package/dist/components/toast/toaster.d.ts +1 -1
  119. package/dist/components/toast/toaster.js +13 -6
  120. package/dist/components/tree/tree-node.d.ts +13 -7
  121. package/dist/components/tree/tree-node.js +24 -10
  122. package/dist/components/tree/tree.d.ts +64 -15
  123. package/dist/components/tree/tree.js +113 -22
  124. package/dist/components/tree/tree.stories.d.ts +4 -0
  125. package/dist/components/tree/tree.stories.js +31 -1
  126. package/dist/components/tree/update-cascade.d.ts +13 -0
  127. package/dist/components/tree/update-cascade.js +13 -0
  128. package/dist/components/widget/widget.d.ts +15 -1
  129. package/dist/components/widget/widget.js +140 -5
  130. package/dist/components/widget/widget.stories.d.ts +1 -0
  131. package/dist/components/widget/widget.stories.js +10 -0
  132. package/dist/components/widget/widget.styles.js +11 -1
  133. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  134. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  135. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  136. package/dist/components/word-cloud/word-cloud.js +255 -0
  137. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  138. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  139. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  140. package/dist/components/word-cloud/word-cloud.styles.js +54 -0
  141. package/dist/internal/a11y.js +2 -1
  142. package/dist/internal/rtl.d.ts +7 -0
  143. package/dist/internal/rtl.js +9 -0
  144. package/dist/lyra.d.ts +9 -1
  145. package/dist/lyra.js +6 -0
  146. package/llms-full.txt +892 -276
  147. package/llms.txt +12 -9
  148. 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
- this.dragIndex = -1;
30
- this.dragStartPos = 0;
31
- this.dragStartSizes = [];
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.dragIndex = index;
38
- this.dragStartPos = this.orientation === 'vertical' ? e.clientY : e.clientX;
39
- this.dragStartSizes = [...this.sizes];
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
- if (this.dragIndex < 0)
68
+ const drag = this.drags.get(e.pointerId);
69
+ if (!drag)
46
70
  return;
47
- const base = this.renderRoot.querySelector('[part="base"]');
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
- const deltaPercent = ((pos - this.dragStartPos) / total) * 100;
51
- this.sizes = this.clampPair(this.dragStartSizes, this.dragIndex, deltaPercent);
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.dragIndex = -1;
89
+ this.onPointerUp = (e) => {
90
+ if (!this.drags.has(e.pointerId))
91
+ return;
92
+ this.drags.delete(e.pointerId);
56
93
  this.persist();
57
- window.removeEventListener('pointermove', this.onPointerMove);
58
- window.removeEventListener('pointerup', this.onPointerUp);
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
- const forwardKey = this.orientation === 'vertical' ? 'ArrowDown' : 'ArrowRight';
62
- const backwardKey = this.orientation === 'vertical' ? 'ArrowUp' : 'ArrowLeft';
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
- if (Array.isArray(parsed) && parsed.length === this.panelCount)
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.sizes.length === this.panelCount && this.panelCount > 0)
175
+ if (this.panelCount === this.sizes.length)
125
176
  return;
126
- const equal = 100 / Math.max(1, this.panelCount);
127
- this.sizes = Array.from({ length: this.panelCount }, () => equal);
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
- clampPair(sizes, i, delta) {
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 a = next[i] + delta;
132
- const b = next[i + 1] - delta;
133
- if (a < this.min || b < this.min)
134
- return sizes;
135
- next[i] = a;
136
- next[i + 1] = b;
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
- panel.style.flex = `0 0 ${this.sizes[i] ?? 0}%`;
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);
@@ -3,3 +3,5 @@ declare const meta: Meta;
3
3
  export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Default: Story;
6
+ export declare const Vertical: Story;
7
+ export declare const FixedPixelRangePanel: Story;
@@ -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
- * @csspart base, icon, label, value, unit, trend, caption
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
- * @csspart base, icon, label, value, unit, trend, caption
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}</span> ${Math.abs(this.trend)}%
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);
@@ -3,3 +3,6 @@ declare const meta: Meta;
3
3
  export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Gallery: Story;
6
+ export declare const SparkAndBreakdown: Story;
7
+ export declare const BreakdownRows: Story;
8
+ export declare const Emphasis: Story;
@@ -7,10 +7,58 @@ const meta = {
7
7
  export default meta;
8
8
  export const Gallery = {
9
9
  render: () => html `
10
- <div style="display:flex; gap:1rem; flex-wrap:wrap;">
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
  `;