@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.
Files changed (148) hide show
  1. package/README.md +8 -9
  2. package/custom-elements.json +10463 -4519
  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
@@ -4,23 +4,47 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
4
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
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
- import { html } from 'lit';
7
+ import { html, nothing } from 'lit';
8
8
  import { property } 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 './time-range.styles.js';
12
13
  function isArrowKey(key) {
13
14
  return key === 'ArrowRight' || key === 'ArrowUp' || key === 'ArrowLeft' || key === 'ArrowDown';
14
15
  }
16
+ /** Keys that onKeyDown acts on and onKeyUp commits after — arrow keys plus
17
+ * the WAI-ARIA APG slider pattern's Home/End/PageUp/PageDown shortcuts. */
18
+ function isSliderKey(key) {
19
+ return isArrowKey(key) || key === 'Home' || key === 'End' || key === 'PageUp' || key === 'PageDown';
20
+ }
21
+ /** PageUp/PageDown move by a larger increment than a single ArrowUp/Down
22
+ * step, matching the WAI-ARIA APG slider pattern's expected keyboard
23
+ * interactions (and native `<input type=range>`). */
24
+ const PAGE_STEP_MULTIPLIER = 10;
25
+ /** Number of decimal digits in `n`'s shortest string representation, e.g.
26
+ * `0.1` -> 1, `5` -> 0. Used to round a stepped value back to the precision
27
+ * the caller's `step` itself implies, instead of leaving it at whatever
28
+ * binary floating-point noise `value / step` happened to produce. */
29
+ function decimalPlaces(n) {
30
+ const str = n.toString();
31
+ const dot = str.indexOf('.');
32
+ return dot === -1 ? 0 : str.length - dot - 1;
33
+ }
15
34
  /**
16
35
  * `<lyra-time-range>` — a two-handle brush/scrubber over a numeric domain.
17
36
  * Callers map their own time axis to `[min, max]`; no date logic lives here
18
37
  * (matches the no-date-library constraint used elsewhere in this library).
19
38
  *
39
+ * Optionally paired with a row of discrete presets (`presets`) — e.g. "Last
40
+ * 7 days" / "Last 30 days" — rendered above the track; picking one is just a
41
+ * shortcut that sets both handles at once, the continuous brush underneath
42
+ * is unaffected and both interaction modes coexist.
43
+ *
20
44
  * @customElement lyra-time-range
21
- * @event lyra-input - Fired continuously while dragging. `detail: { start, end }`.
22
- * @event lyra-change - Fired on release / keyup-commit. `detail: { start, end }`.
23
- * @csspart base, track, range, handle-start, handle-end
45
+ * @event lyra-input - Fired continuously while dragging or on each arrow-key press. `detail: { start, end }`.
46
+ * @event lyra-change - Fired on release / keyup-commit, or when a preset button is clicked. `detail: { start, end }`.
47
+ * @csspart base, track, range, handle-start, handle-end, presets, preset-button
24
48
  */
25
49
  export class LyraTimeRange extends LyraElement {
26
50
  constructor() {
@@ -31,7 +55,17 @@ export class LyraTimeRange extends LyraElement {
31
55
  this.end = 100;
32
56
  this.step = 1;
33
57
  this.disabled = false;
34
- this.dragging = null;
58
+ /**
59
+ * Optional discrete presets rendered as a `[part="presets"]` button row
60
+ * above the track. Purely additive: leaving this empty (the default)
61
+ * renders nothing extra and leaves the continuous brush untouched.
62
+ */
63
+ this.presets = [];
64
+ // Keyed by pointerId rather than a single scalar so two concurrent drags
65
+ // (e.g. a two-finger touch, one per handle) each keep tracking their own
66
+ // handle instead of the second pointerdown hijacking which handle the
67
+ // first pointer's subsequent moves apply to.
68
+ this.drags = new Map();
35
69
  this.onKeyDown = (handle, e) => {
36
70
  // A handle that already has focus when the component becomes disabled
37
71
  // must not still respond to arrow keys (new pointerdowns are already
@@ -41,33 +75,64 @@ export class LyraTimeRange extends LyraElement {
41
75
  if (this.disabled)
42
76
  return;
43
77
  const current = handle === 'start' ? this.start : this.end;
44
- if (e.key === 'ArrowRight' || e.key === 'ArrowUp') {
78
+ // Mirror the same left/right swap as onPointerMove: under RTL, physical
79
+ // ArrowRight moves toward inset-inline-start, i.e. a lower value.
80
+ const rtl = isRtl(this);
81
+ const forwardKey = rtl ? 'ArrowLeft' : 'ArrowRight';
82
+ const backwardKey = rtl ? 'ArrowRight' : 'ArrowLeft';
83
+ if (e.key === forwardKey || e.key === 'ArrowUp') {
45
84
  e.preventDefault();
46
85
  this.setValue(handle, current + this.step, false);
47
86
  }
48
- else if (e.key === 'ArrowLeft' || e.key === 'ArrowDown') {
87
+ else if (e.key === backwardKey || e.key === 'ArrowDown') {
49
88
  e.preventDefault();
50
89
  this.setValue(handle, current - this.step, false);
51
90
  }
91
+ else if (e.key === 'PageUp') {
92
+ e.preventDefault();
93
+ this.setValue(handle, current + this.step * PAGE_STEP_MULTIPLIER, false);
94
+ }
95
+ else if (e.key === 'PageDown') {
96
+ e.preventDefault();
97
+ this.setValue(handle, current - this.step * PAGE_STEP_MULTIPLIER, false);
98
+ }
99
+ else if (e.key === 'Home') {
100
+ e.preventDefault();
101
+ this.setValue(handle, this.reachableBounds(handle).min, false);
102
+ }
103
+ else if (e.key === 'End') {
104
+ e.preventDefault();
105
+ this.setValue(handle, this.reachableBounds(handle).max, false);
106
+ }
52
107
  };
53
108
  this.onKeyUp = (e) => {
54
- // Only commit on release of the arrow keys that onKeyDown acts on —
55
- // releasing an unrelated key (Tab, Shift, ...) while a handle happens
56
- // to be focused must not emit a spurious lyra-change.
57
- if (this.disabled || !isArrowKey(e.key))
109
+ // Only commit on release of the keys that onKeyDown acts on — releasing
110
+ // an unrelated key (Tab, Shift, ...) while a handle happens to be
111
+ // focused must not emit a spurious lyra-change.
112
+ if (this.disabled || !isSliderKey(e.key))
58
113
  return;
59
114
  this.emit('lyra-change', { start: this.start, end: this.end });
60
115
  };
61
116
  this.onPointerDown = (handle, e) => {
62
117
  if (this.disabled)
63
118
  return;
64
- this.dragging = handle;
119
+ const base = this.renderRoot.querySelector('[part="base"]');
120
+ this.drags.set(e.pointerId, { handle, base });
65
121
  e.target.setPointerCapture(e.pointerId);
66
122
  window.addEventListener('pointermove', this.onPointerMove);
67
123
  window.addEventListener('pointerup', this.onPointerUp);
124
+ // A drag can end without a pointerup: a system gesture / palm rejection
125
+ // can fire `pointercancel`, and losing capture (e.g. element removed)
126
+ // fires `lostpointercapture` — both need the same teardown as pointerup
127
+ // or `this.drags` keeps a permanently-stale entry and these window
128
+ // listeners (and the closure keeping this instance alive) never get
129
+ // removed. Mirrors lyra-split's identical fix.
130
+ window.addEventListener('pointercancel', this.onPointerUp);
131
+ window.addEventListener('lostpointercapture', this.onPointerUp);
68
132
  };
69
133
  this.onPointerMove = (e) => {
70
- if (!this.dragging)
134
+ const drag = this.drags.get(e.pointerId);
135
+ if (drag === undefined)
71
136
  return;
72
137
  if (this.disabled) {
73
138
  // These are window-level listeners driven by setPointerCapture, so
@@ -78,17 +143,20 @@ export class LyraTimeRange extends LyraElement {
78
143
  // would otherwise keep mutating start/end (and emitting lyra-input)
79
144
  // after `disabled` flips true mid-drag. Abort the drag instead of
80
145
  // continuing to process it.
81
- this.endDrag(false);
146
+ this.endDrag(e.pointerId, false);
82
147
  return;
83
148
  }
84
- const base = this.renderRoot.querySelector('[part="base"]');
85
- const rect = base.getBoundingClientRect();
86
- const ratio = Math.min(1, Math.max(0, (e.clientX - rect.left) / rect.width));
149
+ const rect = drag.base.getBoundingClientRect();
150
+ const raw = (e.clientX - rect.left) / rect.width;
151
+ // The track is positioned with inset-inline-start (0% at the visual
152
+ // right edge under RTL), so the pointer ratio has to mirror that or a
153
+ // rightward drag would move the handle the wrong way.
154
+ const ratio = Math.min(1, Math.max(0, isRtl(this) ? 1 - raw : raw));
87
155
  const value = this.min + ratio * (this.max - this.min);
88
- this.setValue(this.dragging, value, false);
156
+ this.setValue(drag.handle, value, false);
89
157
  };
90
- this.onPointerUp = () => {
91
- this.endDrag(true);
158
+ this.onPointerUp = (e) => {
159
+ this.endDrag(e.pointerId, true);
92
160
  };
93
161
  }
94
162
  static { this.styles = [LyraElement.styles, styles]; }
@@ -98,16 +166,76 @@ export class LyraTimeRange extends LyraElement {
98
166
  // pointercancel/alt-tab means `pointerup` never reaches `window`), these
99
167
  // window-level listeners — and the closure keeping this instance alive —
100
168
  // would otherwise leak indefinitely.
169
+ this.drags.clear();
101
170
  window.removeEventListener('pointermove', this.onPointerMove);
102
171
  window.removeEventListener('pointerup', this.onPointerUp);
172
+ window.removeEventListener('pointercancel', this.onPointerUp);
173
+ window.removeEventListener('lostpointercapture', this.onPointerUp);
174
+ }
175
+ // Both percentOf() and clamp() key off this same lo/hi pair — willUpdate()
176
+ // already normalizes a caller-supplied `min > max` domain this way, so
177
+ // rendering and interactive dragging must agree with it instead of
178
+ // indexing off this.min/this.max directly (which inverts the visuals and
179
+ // locks dragging to a fixed value when min > max).
180
+ domain() {
181
+ // A caller-supplied min/max that fails Number attribute conversion (e.g.
182
+ // `min="not-a-number"`) arrives here as NaN; Math.min/Math.max would
183
+ // otherwise propagate that NaN into `lo`/`hi` and poison every
184
+ // percentOf()/clamp() caller. Mirror lyra-gauge's `ratio` getter, which
185
+ // short-circuits on isNaN(...) instead, by falling back to this
186
+ // property's own default.
187
+ const min = isNaN(this.min) ? 0 : this.min;
188
+ const max = isNaN(this.max) ? 100 : this.max;
189
+ return { lo: Math.min(min, max), hi: Math.max(min, max) };
103
190
  }
104
191
  percentOf(value) {
105
- const span = this.max - this.min || 1;
106
- return ((value - this.min) / span) * 100;
192
+ // A NaN `start`/`end` (e.g. an invalid `start` attribute) must not reach
193
+ // the rendered `inset-inline-start:NaN%` that's an invalid CSS value
194
+ // the browser silently drops, leaving the handle stuck with no visible
195
+ // recovery. 0% is at least a stable, well-defined position.
196
+ if (isNaN(value))
197
+ return 0;
198
+ const { lo, hi } = this.domain();
199
+ const span = hi - lo || 1;
200
+ return ((value - lo) / span) * 100;
201
+ }
202
+ /** Each handle's actual reachable sub-range, bounded by its sibling
203
+ * handle's current value the same way clamp() already enforces it — used
204
+ * both for aria-valuemin/aria-valuemax and for Home/End's jump targets,
205
+ * instead of reporting/jumping to the component's full [min, max] domain
206
+ * which the sibling handle may make partially unreachable. */
207
+ reachableBounds(handle) {
208
+ const { lo, hi } = this.domain();
209
+ // A NaN sibling value (e.g. an invalid `end` attribute) must not leak
210
+ // into the *other* handle's aria-valuemin/aria-valuemax as a literal
211
+ // "NaN" — fall back to the domain bound it would otherwise report.
212
+ const start = isNaN(this.start) ? lo : this.start;
213
+ const end = isNaN(this.end) ? hi : this.end;
214
+ return handle === 'start' ? { min: lo, max: end } : { min: start, max: hi };
107
215
  }
108
216
  clamp(handle, value) {
109
- const stepped = Math.round(value / this.step) * this.step;
110
- const bounded = Math.min(this.max, Math.max(this.min, stepped));
217
+ const { lo, hi } = this.domain();
218
+ // A non-positive or non-finite step (e.g. a transient `step={0}` while a
219
+ // caller derives it as `(max-min)/tickCount`) would otherwise divide by
220
+ // zero/NaN below and permanently poison start/end with NaN, since the
221
+ // other handle's clamp cross-references this one's (already-NaN) value.
222
+ // Treat it as "unstepped" instead of propagating NaN.
223
+ const hasStep = Number.isFinite(this.step) && this.step > 0;
224
+ // Anchor the step grid at the domain's own `lo` (matching native
225
+ // `<input type=range>`) instead of absolute 0 — otherwise a `min` that
226
+ // isn't itself a multiple of `step` makes the very first nudge off `min`
227
+ // jump to the nearest multiple-of-step-from-zero instead of moving by
228
+ // one `step`. Round the result back to `step`'s own decimal precision
229
+ // (rather than leaving raw `value / step` binary-float noise in place)
230
+ // so repeated steps land on exact values like 20.1 instead of
231
+ // 20.200000000000003.
232
+ let stepped = value;
233
+ if (hasStep) {
234
+ const stepsFromLo = Math.round((value - lo) / this.step);
235
+ const factor = 10 ** decimalPlaces(this.step);
236
+ stepped = Math.round((lo + stepsFromLo * this.step) * factor) / factor;
237
+ }
238
+ const bounded = Math.min(hi, Math.max(lo, stepped));
111
239
  if (handle === 'start')
112
240
  return Math.min(bounded, this.end);
113
241
  return Math.max(bounded, this.start);
@@ -122,13 +250,42 @@ export class LyraTimeRange extends LyraElement {
122
250
  if (commit)
123
251
  this.emit('lyra-change', { start: this.start, end: this.end });
124
252
  }
125
- /** Stop an in-progress drag, optionally committing a final lyra-change. */
126
- endDrag(commit) {
127
- if (this.dragging && commit)
253
+ /**
254
+ * Apply a discrete preset: sets both handles and emits the same
255
+ * lyra-input/lyra-change pair a committed drag or keyboard step would.
256
+ */
257
+ applyPreset(preset) {
258
+ if (this.disabled)
259
+ return;
260
+ // clamp('start', v) pins to <= this.end and clamp('end', v) pins to >=
261
+ // this.start, cross-referencing the *other* handle's current value. If
262
+ // the preset shifts the whole range past the old end (or before the old
263
+ // start), applying setValue() in either order straight away would let
264
+ // that cross-reference clip the first handle against the stale sibling
265
+ // before the second call updates it. Widen the working range first —
266
+ // never narrows anything, just guarantees both setValue() calls below
267
+ // land on the preset's exact values regardless of direction.
268
+ this.start = Math.min(this.start, preset.start);
269
+ this.end = Math.max(this.end, preset.end);
270
+ this.setValue('start', preset.start, false);
271
+ this.setValue('end', preset.end, true);
272
+ }
273
+ /** Stop the drag owned by `pointerId`, optionally committing a final lyra-change. */
274
+ endDrag(pointerId, commit) {
275
+ if (!this.drags.has(pointerId))
276
+ return;
277
+ this.drags.delete(pointerId);
278
+ if (commit)
128
279
  this.emit('lyra-change', { start: this.start, end: this.end });
129
- this.dragging = null;
130
- window.removeEventListener('pointermove', this.onPointerMove);
131
- window.removeEventListener('pointerup', this.onPointerUp);
280
+ // Only the last concurrent drag to end tears down the shared window
281
+ // listeners — another pointer (e.g. the other finger of a two-finger
282
+ // drag) may still be down.
283
+ if (this.drags.size === 0) {
284
+ window.removeEventListener('pointermove', this.onPointerMove);
285
+ window.removeEventListener('pointerup', this.onPointerUp);
286
+ window.removeEventListener('pointercancel', this.onPointerUp);
287
+ window.removeEventListener('lostpointercapture', this.onPointerUp);
288
+ }
132
289
  }
133
290
  willUpdate(changed) {
134
291
  if (changed.has('start') ||
@@ -161,7 +318,26 @@ export class LyraTimeRange extends LyraElement {
161
318
  render() {
162
319
  const startPct = this.percentOf(this.start);
163
320
  const endPct = this.percentOf(this.end);
321
+ const startBounds = this.reachableBounds('start');
322
+ const endBounds = this.reachableBounds('end');
164
323
  return html `
324
+ ${this.presets.length > 0
325
+ ? html `<div part="presets">
326
+ ${this.presets.map((preset) => {
327
+ const active = preset.start === this.start && preset.end === this.end;
328
+ return html `<button
329
+ part="preset-button"
330
+ type="button"
331
+ ?disabled=${this.disabled}
332
+ aria-pressed=${active ? 'true' : 'false'}
333
+ ?data-active=${active}
334
+ @click=${() => this.applyPreset(preset)}
335
+ >
336
+ ${preset.label}
337
+ </button>`;
338
+ })}
339
+ </div>`
340
+ : nothing}
165
341
  <div part="base">
166
342
  <div part="track"></div>
167
343
  <div
@@ -173,9 +349,10 @@ export class LyraTimeRange extends LyraElement {
173
349
  role="slider"
174
350
  tabindex=${this.disabled ? '-1' : '0'}
175
351
  aria-label="Range start"
176
- aria-valuemin=${this.min}
177
- aria-valuemax=${this.max}
178
- aria-valuenow=${this.start}
352
+ aria-disabled=${this.disabled ? 'true' : nothing}
353
+ aria-valuemin=${startBounds.min}
354
+ aria-valuemax=${startBounds.max}
355
+ aria-valuenow=${isNaN(this.start) ? nothing : this.start}
179
356
  style=${`inset-inline-start:${startPct}%`}
180
357
  @pointerdown=${(e) => this.onPointerDown('start', e)}
181
358
  @keydown=${(e) => this.onKeyDown('start', e)}
@@ -186,9 +363,10 @@ export class LyraTimeRange extends LyraElement {
186
363
  role="slider"
187
364
  tabindex=${this.disabled ? '-1' : '0'}
188
365
  aria-label="Range end"
189
- aria-valuemin=${this.min}
190
- aria-valuemax=${this.max}
191
- aria-valuenow=${this.end}
366
+ aria-disabled=${this.disabled ? 'true' : nothing}
367
+ aria-valuemin=${endBounds.min}
368
+ aria-valuemax=${endBounds.max}
369
+ aria-valuenow=${isNaN(this.end) ? nothing : this.end}
192
370
  style=${`inset-inline-start:${endPct}%`}
193
371
  @pointerdown=${(e) => this.onPointerDown('end', e)}
194
372
  @keydown=${(e) => this.onKeyDown('end', e)}
@@ -216,4 +394,7 @@ __decorate([
216
394
  __decorate([
217
395
  property({ type: Boolean, reflect: true })
218
396
  ], LyraTimeRange.prototype, "disabled", void 0);
397
+ __decorate([
398
+ property({ attribute: false })
399
+ ], LyraTimeRange.prototype, "presets", void 0);
219
400
  defineElement('time-range', LyraTimeRange);
@@ -4,3 +4,5 @@ export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Default: Story;
6
6
  export declare const Disabled: Story;
7
+ export declare const CoarseStep: Story;
8
+ export declare const DiscretePresets: Story;
@@ -1,4 +1,9 @@
1
1
  import { html } from 'lit';
2
+ const presets = [
3
+ { label: 'Last 7 days', start: 0, end: 7 },
4
+ { label: 'Last 30 days', start: 0, end: 30 },
5
+ { label: 'Last 90 days', start: 0, end: 90 },
6
+ ];
2
7
  const meta = {
3
8
  title: 'TimeRange',
4
9
  component: 'lyra-time-range',
@@ -11,3 +16,11 @@ export const Default = {
11
16
  export const Disabled = {
12
17
  render: () => html `<lyra-time-range min="0" max="100" start="20" end="80" disabled></lyra-time-range>`,
13
18
  };
19
+ export const CoarseStep = {
20
+ render: () => html `<lyra-time-range min="0" max="100" start="20" end="80" step="10"></lyra-time-range>`,
21
+ };
22
+ export const DiscretePresets = {
23
+ render: () => html `
24
+ <lyra-time-range min="0" max="90" start="0" end="30" .presets=${presets}></lyra-time-range>
25
+ `,
26
+ };
@@ -3,12 +3,54 @@ export const styles = css `
3
3
  :host {
4
4
  display: block;
5
5
  inline-size: 100%;
6
- block-size: 1.5rem;
6
+ /* No fixed block-size here (unlike before presets existed): [part="base"]
7
+ now carries its own 1.5rem below and the host's block box is just the
8
+ natural stack height of its children. With presets empty this still
9
+ computes to exactly 1.5rem (the one [part="base"] child), so the
10
+ brush-only case renders byte-for-byte the same as before. */
11
+ }
12
+ [part='presets'] {
13
+ display: flex;
14
+ flex-wrap: wrap;
15
+ gap: var(--lyra-space-xs);
16
+ margin-block-end: var(--lyra-space-s);
17
+ }
18
+ [part='preset-button'] {
19
+ display: inline-flex;
20
+ align-items: center;
21
+ padding: var(--lyra-space-xs) var(--lyra-space-s);
22
+ border: 1px solid var(--lyra-color-border);
23
+ border-radius: var(--lyra-radius);
24
+ background: var(--lyra-color-surface);
25
+ color: var(--lyra-color-text);
26
+ font: inherit;
27
+ font-size: 0.8125rem;
28
+ cursor: pointer;
29
+ transition: var(--lyra-transition-fast);
30
+ }
31
+ [part='preset-button']:hover:not(:disabled) {
32
+ border-color: var(--lyra-color-brand);
33
+ }
34
+ [part='preset-button']:focus-visible {
35
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
36
+ outline-offset: var(--lyra-focus-ring-offset);
37
+ }
38
+ [part='preset-button'][data-active] {
39
+ background: var(--lyra-color-brand);
40
+ border-color: var(--lyra-color-brand);
41
+ color: var(--lyra-color-on-brand);
42
+ }
43
+ [part='preset-button']:disabled {
44
+ /* Dimming already comes from :host([disabled])'s opacity below (applies
45
+ to the whole host, presets row included) — stacking a second opacity
46
+ here would compound multiplicatively and over-dim relative to the
47
+ handles, which only restate the cursor for the same reason. */
48
+ cursor: not-allowed;
7
49
  }
8
50
  [part='base'] {
9
51
  position: relative;
10
52
  inline-size: 100%;
11
- block-size: 100%;
53
+ block-size: 1.5rem;
12
54
  display: flex;
13
55
  align-items: center;
14
56
  }
@@ -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
  export type ToastVariant = 'brand' | 'success' | 'warning' | 'danger' | 'neutral';
4
4
  export type ToastSize = 'xs' | 's' | 'm' | 'l' | 'xl';
@@ -30,15 +30,27 @@ export declare class LyraToastItem extends LyraElement {
30
30
  /** Show the icon slot. */
31
31
  withIcon: boolean;
32
32
  private timer?;
33
- private remaining;
33
+ private elapsedMs;
34
34
  private startedAt;
35
+ private timerStarted;
35
36
  private showRafId?;
37
+ private showAnimTimer?;
38
+ private hideAnimTimer?;
39
+ private hovering;
40
+ private focused;
41
+ private hiding;
42
+ protected updated(changed: PropertyValues): void;
36
43
  firstUpdated(): void;
37
44
  disconnectedCallback(): void;
38
45
  private startTimer;
39
46
  private resumeTimer;
40
47
  private pauseTimer;
41
48
  private clearTimer;
49
+ private onPointerEnter;
50
+ private onPointerLeave;
51
+ private onFocusIn;
52
+ private onFocusOut;
53
+ private get closeLabel();
42
54
  /** Hide with animation, then remove from the DOM. */
43
55
  hide(): Promise<void>;
44
56
  render(): TemplateResult;