@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
@@ -5,7 +5,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
5
5
  return c > 3 && r && Object.defineProperty(target, key, r), r;
6
6
  };
7
7
  import { html } from 'lit';
8
- import { property } from 'lit/decorators.js';
8
+ import { property, state } from 'lit/decorators.js';
9
9
  import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { defineElement } from '../../internal/prefix.js';
11
11
  import { closeIcon } from '../../internal/icons.js';
@@ -18,6 +18,16 @@ import { styles } from './toast-item.styles.js';
18
18
  // parsing that back out in JS for a single setTimeout would be more fragile
19
19
  // than this documented literal.
20
20
  const ANIM_MS = 180;
21
+ function prefersReducedMotion() {
22
+ return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
23
+ }
24
+ // toast-item.styles.ts already collapses the CSS transition itself to ~0 under
25
+ // reduced motion; this keeps the lyra-after-show/lyra-after-hide events (and
26
+ // the DOM removal in hide()) from still waiting out the full duration of an
27
+ // animation nothing is visibly playing.
28
+ function animMs() {
29
+ return prefersReducedMotion() ? 0 : ANIM_MS;
30
+ }
21
31
  /**
22
32
  * `<lyra-toast-item>` — a single toast notification.
23
33
  * Mirrors the Web Awesome `<wa-toast-item>` API under the `lyra-` prefix.
@@ -46,8 +56,12 @@ export class LyraToastItem extends LyraElement {
46
56
  this.variant = 'neutral';
47
57
  /** Show the icon slot. */
48
58
  this.withIcon = false;
49
- this.remaining = 0;
59
+ this.elapsedMs = 0;
50
60
  this.startedAt = 0;
61
+ this.timerStarted = false;
62
+ this.hovering = false;
63
+ this.focused = false;
64
+ this.hiding = false;
51
65
  this.resumeTimer = () => {
52
66
  // Guard against an interleaved pointer+focus pause/resume sequence
53
67
  // calling resumeTimer() twice without a pauseTimer() in between --
@@ -59,28 +73,75 @@ export class LyraToastItem extends LyraElement {
59
73
  clearTimeout(this.timer);
60
74
  this.timer = undefined;
61
75
  }
62
- if (!isFinite(this.duration) || this.duration <= 0 || this.remaining <= 0)
76
+ if (!isFinite(this.duration) || this.duration <= 0)
77
+ return;
78
+ const remaining = this.duration - this.elapsedMs;
79
+ if (remaining <= 0)
63
80
  return;
64
81
  this.startedAt = performance.now();
65
- this.timer = window.setTimeout(() => this.hide(), this.remaining);
82
+ this.timer = window.setTimeout(() => this.hide(), remaining);
66
83
  };
67
84
  this.pauseTimer = () => {
68
85
  if (this.timer !== undefined) {
69
86
  clearTimeout(this.timer);
70
87
  this.timer = undefined;
71
- this.remaining -= performance.now() - this.startedAt;
88
+ this.elapsedMs += performance.now() - this.startedAt;
72
89
  }
73
90
  };
91
+ // Hover and focus are tracked as independent pause reasons so that
92
+ // releasing one (e.g. Shift-Tabbing focus away) only resumes the timer
93
+ // once *neither* modality still holds the toast paused -- otherwise a
94
+ // pointer resting on the toast would see it auto-dismiss out from under
95
+ // it the moment focus alone moved away, or vice versa.
96
+ this.onPointerEnter = () => {
97
+ this.hovering = true;
98
+ this.pauseTimer();
99
+ };
100
+ this.onPointerLeave = () => {
101
+ this.hovering = false;
102
+ if (!this.focused)
103
+ this.resumeTimer();
104
+ };
105
+ this.onFocusIn = () => {
106
+ this.focused = true;
107
+ this.pauseTimer();
108
+ };
109
+ this.onFocusOut = () => {
110
+ this.focused = false;
111
+ if (!this.hovering)
112
+ this.resumeTimer();
113
+ };
74
114
  }
75
115
  static { this.styles = [LyraElement.styles, styles]; }
116
+ updated(changed) {
117
+ if (changed.has('variant')) {
118
+ // Assertive for actionable severities, polite otherwise. Re-evaluated
119
+ // on every `variant` change (not just the first render) so a toast
120
+ // reassigned to `danger`/`warning` after creation is announced as an
121
+ // interruption instead of keeping its original, now-stale role.
122
+ this.setAttribute('role', this.variant === 'danger' || this.variant === 'warning' ? 'alert' : 'status');
123
+ }
124
+ // `elapsedMs`/`duration` are re-read fresh every time the timer is
125
+ // (re)scheduled, so a `duration` change while paused (hovering/focused)
126
+ // or before the timer has ever started needs no action here -- the next
127
+ // resumeTimer()/startTimer() call already picks up the new value. Only a
128
+ // timer that's actively counting down right now is running against a
129
+ // setTimeout scheduled for the *old* duration, so that's the one case
130
+ // that needs an explicit reschedule.
131
+ if (changed.has('duration') && this.timerStarted && this.timer !== undefined) {
132
+ this.pauseTimer();
133
+ this.resumeTimer();
134
+ }
135
+ }
76
136
  firstUpdated() {
77
- // Assertive for actionable severities, polite otherwise.
78
- this.setAttribute('role', this.variant === 'danger' || this.variant === 'warning' ? 'alert' : 'status');
79
137
  this.showRafId = requestAnimationFrame(() => {
80
138
  this.showRafId = undefined;
81
139
  this.setAttribute('data-visible', '');
82
140
  this.emit('lyra-show');
83
- window.setTimeout(() => this.emit('lyra-after-show'), ANIM_MS);
141
+ this.showAnimTimer = window.setTimeout(() => {
142
+ this.showAnimTimer = undefined;
143
+ this.emit('lyra-after-show');
144
+ }, animMs());
84
145
  this.startTimer();
85
146
  });
86
147
  }
@@ -90,12 +151,19 @@ export class LyraToastItem extends LyraElement {
90
151
  cancelAnimationFrame(this.showRafId);
91
152
  this.showRafId = undefined;
92
153
  }
154
+ if (this.showAnimTimer !== undefined) {
155
+ clearTimeout(this.showAnimTimer);
156
+ this.showAnimTimer = undefined;
157
+ }
158
+ if (this.hideAnimTimer !== undefined) {
159
+ clearTimeout(this.hideAnimTimer);
160
+ this.hideAnimTimer = undefined;
161
+ }
93
162
  this.clearTimer();
94
163
  }
95
164
  startTimer() {
96
- if (!isFinite(this.duration) || this.duration <= 0)
97
- return;
98
- this.remaining = this.duration;
165
+ this.timerStarted = true;
166
+ this.elapsedMs = 0;
99
167
  this.resumeTimer();
100
168
  }
101
169
  clearTimer() {
@@ -103,12 +171,43 @@ export class LyraToastItem extends LyraElement {
103
171
  clearTimeout(this.timer);
104
172
  this.timer = undefined;
105
173
  }
174
+ // A stack of several simultaneously-open toasts otherwise gives every
175
+ // close button the same bare "Close" label, so screen-reader/switch-access
176
+ // users can't tell which toast a given button dismisses without first
177
+ // activating it. Deriving the label from the toast's own message content
178
+ // mirrors combobox's per-item "Remove X" labeling for the same reason.
179
+ //
180
+ // Only direct Text-node children are considered -- the message is always
181
+ // assigned as plain text (`create()` sets `textContent`), whereas anything
182
+ // else appended to the host (an action `<button>`, a `slot="icon"` element
183
+ // and its own descendant text) is a sibling *element*, not a top-level text
184
+ // node, so it's excluded without needing to know about those features here.
185
+ get closeLabel() {
186
+ const text = Array.from(this.childNodes)
187
+ .filter((node) => node.nodeType === Node.TEXT_NODE)
188
+ .map((node) => node.textContent ?? '')
189
+ .join('')
190
+ .trim()
191
+ .replace(/\s+/g, ' ');
192
+ if (!text)
193
+ return 'Close';
194
+ const snippet = text.length > 40 ? `${text.slice(0, 40)}…` : text;
195
+ return `Close: ${snippet}`;
196
+ }
106
197
  /** Hide with animation, then remove from the DOM. */
107
198
  async hide() {
199
+ if (this.hiding)
200
+ return;
201
+ this.hiding = true;
108
202
  this.clearTimer();
109
203
  this.emit('lyra-hide');
110
204
  this.removeAttribute('data-visible');
111
- await new Promise((r) => window.setTimeout(r, ANIM_MS));
205
+ await new Promise((resolve) => {
206
+ this.hideAnimTimer = window.setTimeout(() => {
207
+ this.hideAnimTimer = undefined;
208
+ resolve();
209
+ }, animMs());
210
+ });
112
211
  this.emit('lyra-after-hide');
113
212
  this.remove();
114
213
  }
@@ -116,15 +215,30 @@ export class LyraToastItem extends LyraElement {
116
215
  return html `
117
216
  <div
118
217
  part="toast-item"
119
- @pointerenter=${this.pauseTimer}
120
- @pointerleave=${this.resumeTimer}
121
- @focusin=${this.pauseTimer}
122
- @focusout=${this.resumeTimer}
218
+ @pointerenter=${this.onPointerEnter}
219
+ @pointerleave=${this.onPointerLeave}
220
+ @focusin=${this.onFocusIn}
221
+ @focusout=${this.onFocusOut}
123
222
  >
124
223
  <span part="accent" aria-hidden="true"></span>
125
224
  ${this.withIcon ? html `<span part="icon"><slot name="icon"></slot></span>` : ''}
126
225
  <div part="content"><slot></slot></div>
127
- <button part="close-button" type="button" aria-label="Close" @click=${() => this.hide()}>
226
+ <button
227
+ part="close-button"
228
+ type="button"
229
+ aria-label=${this.closeLabel}
230
+ aria-disabled=${this.hiding ? 'true' : 'false'}
231
+ @click=${(e) => {
232
+ // Reflect the disabled state on the DOM node synchronously, not
233
+ // just via the reactive binding above -- Lit's re-render from
234
+ // `this.hiding = true` (inside hide()) lands on the next
235
+ // microtask, which is too late for a screen reader (or a second
236
+ // rapid click) that inspects the attribute right after this
237
+ // handler returns.
238
+ e.currentTarget.setAttribute('aria-disabled', 'true');
239
+ void this.hide();
240
+ }}
241
+ >
128
242
  ${closeIcon()}
129
243
  </button>
130
244
  </div>
@@ -143,4 +257,7 @@ __decorate([
143
257
  __decorate([
144
258
  property({ type: Boolean, attribute: 'with-icon' })
145
259
  ], LyraToastItem.prototype, "withIcon", void 0);
260
+ __decorate([
261
+ state()
262
+ ], LyraToastItem.prototype, "hiding", void 0);
146
263
  defineElement('toast-item', LyraToastItem);
@@ -6,6 +6,7 @@ export const styles = css `
6
6
  --show-duration: var(--lyra-transition-base, 180ms ease-out);
7
7
  --hide-duration: var(--lyra-transition-base, 180ms ease-out);
8
8
  --padding: var(--lyra-space-m);
9
+ --font-size: 1rem;
9
10
  --accent-color: var(--lyra-color-border);
10
11
  }
11
12
  :host([variant='brand']) {
@@ -20,6 +21,26 @@ export const styles = css `
20
21
  :host([variant='danger']) {
21
22
  --accent-color: var(--lyra-color-danger);
22
23
  }
24
+ :host([size='xs']) {
25
+ --padding: var(--lyra-space-xs);
26
+ --font-size: 0.75rem;
27
+ }
28
+ :host([size='s']) {
29
+ --padding: var(--lyra-space-s);
30
+ --font-size: 0.875rem;
31
+ }
32
+ :host([size='m']) {
33
+ --padding: var(--lyra-space-m);
34
+ --font-size: 1rem;
35
+ }
36
+ :host([size='l']) {
37
+ --padding: var(--lyra-space-l);
38
+ --font-size: 1.125rem;
39
+ }
40
+ :host([size='xl']) {
41
+ --padding: calc(var(--lyra-space-l) * 1.5);
42
+ --font-size: 1.25rem;
43
+ }
23
44
 
24
45
  [part='toast-item'] {
25
46
  position: relative;
@@ -29,6 +50,7 @@ export const styles = css `
29
50
  inline-size: 100%;
30
51
  padding: var(--padding);
31
52
  padding-inline-start: calc(var(--padding) + var(--accent-width));
53
+ font-size: var(--font-size);
32
54
  background: var(--lyra-color-surface);
33
55
  color: var(--lyra-color-text);
34
56
  border: 1px solid var(--lyra-color-border);
@@ -75,14 +97,18 @@ export const styles = css `
75
97
  border: none;
76
98
  cursor: pointer;
77
99
  color: var(--lyra-color-text-quiet);
78
- font-size: 1rem;
100
+ font-size: 1em;
79
101
  line-height: 1;
80
102
  padding: var(--lyra-space-xs);
81
103
  border-radius: var(--lyra-radius);
82
104
  }
83
- [part='close-button']:hover {
105
+ [part='close-button']:hover:not([aria-disabled='true']) {
84
106
  color: var(--lyra-color-text);
85
107
  }
108
+ [part='close-button'][aria-disabled='true'] {
109
+ opacity: var(--lyra-opacity-disabled);
110
+ cursor: not-allowed;
111
+ }
86
112
  [part='close-button']:focus-visible {
87
113
  outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
88
114
  outline-offset: var(--lyra-focus-ring-offset);
@@ -3,3 +3,6 @@ declare const meta: Meta;
3
3
  export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Triggers: Story;
6
+ export declare const Sizes: Story;
7
+ export declare const WithIcon: Story;
8
+ export declare const Placements: Story;
@@ -2,11 +2,12 @@ import { html } from 'lit';
2
2
  import { toast } from '../../lyra.js';
3
3
  const meta = {
4
4
  title: 'Toast',
5
+ component: 'lyra-toast',
5
6
  tags: ['autodocs'],
6
7
  parameters: {
7
8
  docs: {
8
9
  description: {
9
- component: 'Click a button to fire a toast via the `toast()` helper — the ergonomic entry point that lazily mounts a single `<lyra-toast>` region on `document.body`.',
10
+ component: 'Click a button to fire a toast via the `toast()` helper — the ergonomic entry point that lazily mounts one `<lyra-toast>` region per placement on `document.body`.',
10
11
  },
11
12
  },
12
13
  },
@@ -30,3 +31,42 @@ export const Triggers = {
30
31
  </div>
31
32
  `,
32
33
  };
34
+ const sizes = ['xs', 's', 'm', 'l', 'xl'];
35
+ export const Sizes = {
36
+ render: () => html `
37
+ <div style="display:flex; gap:1rem;">
38
+ ${sizes.map((size) => html `<button @click=${() => toast({ message: `Size "${size}"`, size })}>${size}</button>`)}
39
+ </div>
40
+ `,
41
+ };
42
+ export const WithIcon = {
43
+ render: () => html `
44
+ <div style="display:flex; gap:1rem;">
45
+ <button
46
+ @click=${() => toast({ message: 'Upload complete', variant: 'success', withIcon: true }).item.then((item) => {
47
+ const icon = document.createElement('span');
48
+ icon.slot = 'icon';
49
+ icon.textContent = '✓';
50
+ item.appendChild(icon);
51
+ })}
52
+ >
53
+ Success + icon
54
+ </button>
55
+ </div>
56
+ `,
57
+ };
58
+ const placements = [
59
+ 'top-start',
60
+ 'top-center',
61
+ 'top-end',
62
+ 'bottom-start',
63
+ 'bottom-center',
64
+ 'bottom-end',
65
+ ];
66
+ export const Placements = {
67
+ render: () => html `
68
+ <div style="display:flex; flex-wrap:wrap; gap:1rem;">
69
+ ${placements.map((placement) => html `<button @click=${() => toast({ message: placement, placement })}>${placement}</button>`)}
70
+ </div>
71
+ `,
72
+ };
@@ -13,7 +13,7 @@ export const styles = css `
13
13
  flex-direction: column;
14
14
  gap: var(--gap);
15
15
  inline-size: var(--width);
16
- max-inline-size: calc(100vw - 2rem);
16
+ max-inline-size: calc(100vw - (var(--lyra-space-l) * 2));
17
17
  }
18
18
  ::slotted(*) {
19
19
  pointer-events: auto;
@@ -23,28 +23,28 @@ export const styles = css `
23
23
  flex-direction: column-reverse;
24
24
  }
25
25
  :host([placement='top-start']) {
26
- inset-block-start: 1rem;
27
- inset-inline-start: 1rem;
26
+ inset-block-start: var(--lyra-space-l);
27
+ inset-inline-start: var(--lyra-space-l);
28
28
  }
29
29
  :host([placement='top-end']) {
30
- inset-block-start: 1rem;
31
- inset-inline-end: 1rem;
30
+ inset-block-start: var(--lyra-space-l);
31
+ inset-inline-end: var(--lyra-space-l);
32
32
  }
33
33
  :host([placement='top-center']) {
34
- inset-block-start: 1rem;
34
+ inset-block-start: var(--lyra-space-l);
35
35
  inset-inline: 0;
36
36
  margin-inline: auto;
37
37
  }
38
38
  :host([placement='bottom-start']) {
39
- inset-block-end: 1rem;
40
- inset-inline-start: 1rem;
39
+ inset-block-end: var(--lyra-space-l);
40
+ inset-inline-start: var(--lyra-space-l);
41
41
  }
42
42
  :host([placement='bottom-end']) {
43
- inset-block-end: 1rem;
44
- inset-inline-end: 1rem;
43
+ inset-block-end: var(--lyra-space-l);
44
+ inset-inline-end: var(--lyra-space-l);
45
45
  }
46
46
  :host([placement='bottom-center']) {
47
- inset-block-end: 1rem;
47
+ inset-block-end: var(--lyra-space-l);
48
48
  inset-inline: 0;
49
49
  margin-inline: auto;
50
50
  }
@@ -18,7 +18,7 @@ export interface ToastHandle {
18
18
  }
19
19
  /**
20
20
  * Show a toast. Ergonomic convenience over `<lyra-toast>.create()` that mounts
21
- * and reuses a single page-level region — the drop-in for `react-hot-toast`.
21
+ * and reuses a page-level region per placement — the drop-in for `react-hot-toast`.
22
22
  *
23
23
  * @example toast('Saved');
24
24
  * @example toast({ message: 'Deleted', variant: 'danger', action: { label: 'Undo', onClick: undo } });
@@ -1,19 +1,26 @@
1
1
  import { LyraToast } from './toast.js';
2
2
  import './toast.js';
3
- let region;
4
- /** Get (or lazily mount) the singleton toast region on `document.body`. */
5
- function getRegion(placement) {
3
+ const DEFAULT_PLACEMENT = 'top-end';
4
+ const regions = new Map();
5
+ /**
6
+ * Get (or lazily mount) the singleton toast region for a given placement.
7
+ * Each placement gets its own region element so that a `toast()` call
8
+ * targeting one placement never relocates toasts already showing at another
9
+ * -- `placement` is a per-call option, not a global, retroactive one.
10
+ */
11
+ function getRegion(placement = DEFAULT_PLACEMENT) {
12
+ let region = regions.get(placement);
6
13
  if (!region || !region.isConnected) {
7
14
  region = document.createElement('lyra-toast');
15
+ region.placement = placement;
8
16
  document.body.appendChild(region);
17
+ regions.set(placement, region);
9
18
  }
10
- if (placement)
11
- region.placement = placement;
12
19
  return region;
13
20
  }
14
21
  /**
15
22
  * Show a toast. Ergonomic convenience over `<lyra-toast>.create()` that mounts
16
- * and reuses a single page-level region — the drop-in for `react-hot-toast`.
23
+ * and reuses a page-level region per placement — the drop-in for `react-hot-toast`.
17
24
  *
18
25
  * @example toast('Saved');
19
26
  * @example toast({ message: 'Deleted', variant: 'danger', action: { label: 'Undo', onClick: undo } });
@@ -13,6 +13,8 @@ import type { TreeItem } from './tree.js';
13
13
  * sibling).
14
14
  *
15
15
  * @customElement lyra-tree-node
16
+ * @event lyra-node-toggle - `detail: { id, expanded }`, fired when this node is expanded or collapsed (via `expand()`/`collapse()`, the toggle button, or ArrowRight/ArrowLeft).
17
+ * @event lyra-node-select - `detail: { id }`, fired when this node's primary action is activated (via `select()`, clicking its label, or Enter/Space).
16
18
  * @csspart row, toggle, label, badge
17
19
  */
18
20
  export declare class LyraTreeNode extends LyraElement {
@@ -32,15 +34,19 @@ export declare class LyraTreeNode extends LyraElement {
32
34
  collapse(): void;
33
35
  /** Fire this item's primary "select" action (Enter/Space, or clicking the label). */
34
36
  select(): void;
37
+ /** See `cascadeUpdateComplete` and the matching override on `<lyra-tree>`. */
38
+ protected getUpdateComplete(): Promise<boolean>;
35
39
  /**
36
- * Cascade `updateComplete`: a nested `<lyra-tree-node>` rendered by this
37
- * one (in its own shadow root) only receives a pushed-down `activeId` (and
38
- * the `tabIndex` that comes from it) once *this* node's own render has
39
- * committed. Without this override, awaiting a parent's `updateComplete`
40
- * doesn't guarantee an arbitrarily-nested descendant's own pending update
41
- * has also settled see the matching override on `<lyra-tree>`.
40
+ * A `<button>` is a real focusable click target even with
41
+ * `tabindex="-1" aria-hidden="true"` -- only *sequential* (Tab-key)
42
+ * navigation honors `tabindex="-1"`, a mouse click does not. Left
43
+ * unguarded, clicking this toggle parks real DOM focus on this hidden,
44
+ * non-treeitem button instead of the host, which is both an a11y dead end
45
+ * and invisible to `:host(:focus-visible)`'s ring. `preventDefault()` on
46
+ * `mousedown` blocks that default focus move; focusing the host instead
47
+ * keeps focus on the actual `role="treeitem"` element the row represents.
42
48
  */
43
- protected getUpdateComplete(): Promise<boolean>;
49
+ private onToggleMouseDown;
44
50
  render(): TemplateResult;
45
51
  }
46
52
  declare global {
@@ -11,6 +11,7 @@ import { LyraElement } from '../../internal/lyra-element.js';
11
11
  import { defineElement } from '../../internal/prefix.js';
12
12
  import { chevronIcon } from '../../internal/icons.js';
13
13
  import { css } from 'lit';
14
+ import { cascadeUpdateComplete } from './update-cascade.js';
14
15
  const styles = css `
15
16
  :host {
16
17
  display: block;
@@ -52,7 +53,9 @@ const styles = css `
52
53
  cursor: pointer;
53
54
  flex: 0 0 auto;
54
55
  }
55
- [part='toggle']:empty {
56
+ [part='toggle'][hidden] {
57
+ /* visibility (not display) so the placeholder keeps its layout box --
58
+ a leaf row still lines up with sibling rows that do have a chevron. */
56
59
  visibility: hidden;
57
60
  }
58
61
  :host([expanded]) [part='toggle'] {
@@ -85,6 +88,8 @@ const styles = css `
85
88
  * sibling).
86
89
  *
87
90
  * @customElement lyra-tree-node
91
+ * @event lyra-node-toggle - `detail: { id, expanded }`, fired when this node is expanded or collapsed (via `expand()`/`collapse()`, the toggle button, or ArrowRight/ArrowLeft).
92
+ * @event lyra-node-select - `detail: { id }`, fired when this node's primary action is activated (via `select()`, clicking its label, or Enter/Space).
88
93
  * @csspart row, toggle, label, badge
89
94
  */
90
95
  export class LyraTreeNode extends LyraElement {
@@ -96,6 +101,20 @@ export class LyraTreeNode extends LyraElement {
96
101
  this.activeId = null;
97
102
  this.setSize = 1;
98
103
  this.posInSet = 1;
104
+ /**
105
+ * A `<button>` is a real focusable click target even with
106
+ * `tabindex="-1" aria-hidden="true"` -- only *sequential* (Tab-key)
107
+ * navigation honors `tabindex="-1"`, a mouse click does not. Left
108
+ * unguarded, clicking this toggle parks real DOM focus on this hidden,
109
+ * non-treeitem button instead of the host, which is both an a11y dead end
110
+ * and invisible to `:host(:focus-visible)`'s ring. `preventDefault()` on
111
+ * `mousedown` blocks that default focus move; focusing the host instead
112
+ * keeps focus on the actual `role="treeitem"` element the row represents.
113
+ */
114
+ this.onToggleMouseDown = (e) => {
115
+ e.preventDefault();
116
+ this.focus();
117
+ };
99
118
  }
100
119
  static { this.styles = [LyraElement.styles, styles]; }
101
120
  get hasChildren() {
@@ -130,17 +149,10 @@ export class LyraTreeNode extends LyraElement {
130
149
  select() {
131
150
  this.emit('lyra-node-select', { id: this.item.id });
132
151
  }
133
- /**
134
- * Cascade `updateComplete`: a nested `<lyra-tree-node>` rendered by this
135
- * one (in its own shadow root) only receives a pushed-down `activeId` (and
136
- * the `tabIndex` that comes from it) once *this* node's own render has
137
- * committed. Without this override, awaiting a parent's `updateComplete`
138
- * doesn't guarantee an arbitrarily-nested descendant's own pending update
139
- * has also settled — see the matching override on `<lyra-tree>`.
140
- */
152
+ /** See `cascadeUpdateComplete` and the matching override on `<lyra-tree>`. */
141
153
  async getUpdateComplete() {
142
154
  const result = await super.getUpdateComplete();
143
- await Promise.all([...(this.shadowRoot?.querySelectorAll('lyra-tree-node') ?? [])].map((n) => n.updateComplete));
155
+ await cascadeUpdateComplete(this.shadowRoot?.querySelectorAll('lyra-tree-node') ?? []);
144
156
  return result;
145
157
  }
146
158
  render() {
@@ -151,6 +163,8 @@ export class LyraTreeNode extends LyraElement {
151
163
  type="button"
152
164
  tabindex="-1"
153
165
  aria-hidden="true"
166
+ ?hidden=${!this.hasChildren}
167
+ @mousedown=${this.onToggleMouseDown}
154
168
  @click=${(e) => {
155
169
  e.stopPropagation();
156
170
  this.expanded ? this.collapse() : this.expand();