@aceshooting/lyra-ui 0.1.5 → 1.0.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 (83) hide show
  1. package/README.md +17 -8
  2. package/custom-elements.json +4869 -3389
  3. package/dist/components/chart/bar-chart.stories.d.ts +5 -0
  4. package/dist/components/chart/bar-chart.stories.js +20 -0
  5. package/dist/components/chart/box-plot.stories.d.ts +5 -0
  6. package/dist/components/chart/box-plot.stories.js +29 -0
  7. package/dist/components/chart/bubble-chart.stories.d.ts +5 -0
  8. package/dist/components/chart/bubble-chart.stories.js +26 -0
  9. package/dist/components/chart/doughnut-chart.stories.d.ts +5 -0
  10. package/dist/components/chart/doughnut-chart.stories.js +26 -0
  11. package/dist/components/chart/histogram.stories.d.ts +5 -0
  12. package/dist/components/chart/histogram.stories.js +17 -0
  13. package/dist/components/chart/line-chart.stories.d.ts +5 -0
  14. package/dist/components/chart/line-chart.stories.js +24 -0
  15. package/dist/components/chart/pie-chart.stories.d.ts +5 -0
  16. package/dist/components/chart/pie-chart.stories.js +26 -0
  17. package/dist/components/chart/polar-area-chart.stories.d.ts +5 -0
  18. package/dist/components/chart/polar-area-chart.stories.js +20 -0
  19. package/dist/components/chart/radar-chart.stories.d.ts +5 -0
  20. package/dist/components/chart/radar-chart.stories.js +20 -0
  21. package/dist/components/chart/scatter-chart.stories.d.ts +5 -0
  22. package/dist/components/chart/scatter-chart.stories.js +31 -0
  23. package/dist/components/combobox/combobox.d.ts +26 -1
  24. package/dist/components/combobox/combobox.js +147 -31
  25. package/dist/components/combobox/combobox.styles.js +21 -0
  26. package/dist/components/combobox/option.d.ts +4 -0
  27. package/dist/components/combobox/option.js +10 -0
  28. package/dist/components/date-picker/calendar-core.d.ts +6 -2
  29. package/dist/components/date-picker/calendar-core.js +23 -4
  30. package/dist/components/date-picker/date-input.d.ts +1 -0
  31. package/dist/components/date-picker/date-input.js +19 -2
  32. package/dist/components/date-picker/date-picker.js +4 -2
  33. package/dist/components/date-picker/date-picker.styles.js +5 -1
  34. package/dist/components/empty/empty.stories.d.ts +5 -0
  35. package/dist/components/empty/empty.stories.js +14 -0
  36. package/dist/components/export-button/export-button.js +7 -2
  37. package/dist/components/export-button/export-button.stories.d.ts +6 -0
  38. package/dist/components/export-button/export-button.stories.js +32 -0
  39. package/dist/components/file-input/file-input.stories.d.ts +5 -0
  40. package/dist/components/file-input/file-input.stories.js +10 -0
  41. package/dist/components/flag/flag.js +12 -2
  42. package/dist/components/gauge/gauge.js +8 -4
  43. package/dist/components/gauge/gauge.stories.d.ts +6 -0
  44. package/dist/components/gauge/gauge.stories.js +13 -0
  45. package/dist/components/graph/graph.stories.d.ts +5 -0
  46. package/dist/components/graph/graph.stories.js +30 -0
  47. package/dist/components/heatmap/heatmap.stories.d.ts +5 -0
  48. package/dist/components/heatmap/heatmap.stories.js +24 -0
  49. package/dist/components/map/map.d.ts +6 -0
  50. package/dist/components/map/map.js +69 -9
  51. package/dist/components/map/map.stories.d.ts +5 -0
  52. package/dist/components/map/map.stories.js +40 -0
  53. package/dist/components/playback/playback.stories.d.ts +5 -0
  54. package/dist/components/playback/playback.stories.js +10 -0
  55. package/dist/components/skeleton/skeleton.stories.d.ts +5 -0
  56. package/dist/components/skeleton/skeleton.stories.js +16 -0
  57. package/dist/components/split/split.d.ts +1 -0
  58. package/dist/components/split/split.js +25 -4
  59. package/dist/components/split/split.stories.d.ts +5 -0
  60. package/dist/components/split/split.stories.js +16 -0
  61. package/dist/components/table/table.stories.d.ts +6 -0
  62. package/dist/components/table/table.stories.js +22 -0
  63. package/dist/components/time-range/time-range.stories.d.ts +6 -0
  64. package/dist/components/time-range/time-range.stories.js +13 -0
  65. package/dist/components/toast/toast-item.d.ts +1 -0
  66. package/dist/components/toast/toast-item.js +16 -1
  67. package/dist/components/tree/tree.stories.d.ts +5 -0
  68. package/dist/components/tree/tree.stories.js +22 -0
  69. package/dist/components/widget/widget.d.ts +40 -0
  70. package/dist/components/widget/widget.js +159 -0
  71. package/dist/components/widget/widget.stories.d.ts +6 -0
  72. package/dist/components/widget/widget.stories.js +31 -0
  73. package/dist/components/widget/widget.styles.d.ts +1 -0
  74. package/dist/components/widget/widget.styles.js +90 -0
  75. package/dist/internal/icons.d.ts +2 -0
  76. package/dist/internal/icons.js +9 -0
  77. package/dist/internal/scroll-lock.d.ts +7 -0
  78. package/dist/internal/scroll-lock.js +25 -0
  79. package/dist/internal/tokens.styles.js +5 -0
  80. package/dist/lyra.d.ts +5 -0
  81. package/dist/lyra.js +2 -0
  82. package/llms.txt +3 -0
  83. package/package.json +3 -3
@@ -62,6 +62,8 @@ export class LyraCombobox extends LyraElement {
62
62
  this.maxOptionsVisible = 3;
63
63
  this.emptyText = 'No results';
64
64
  this.filter = null;
65
+ this.source = null;
66
+ this.maxRender = 200;
65
67
  this.query = '';
66
68
  this.activeIndex = -1;
67
69
  this.options = [];
@@ -78,7 +80,17 @@ export class LyraCombobox extends LyraElement {
78
80
  this.hasHintSlot = false;
79
81
  this.hasErrorSlot = false;
80
82
  this.hasLabelSlot = false;
83
+ this.loading = false;
84
+ this.asyncRows = [];
85
+ this.sourceToken = 0;
86
+ this._selectedLabelCache = new Map();
87
+ // Rebuilt once per render (in render(), before renderRows()) from the
88
+ // currently-visible row set -- backs the delegated listbox click/mousedown
89
+ // handlers' data-value lookup below, instead of each option row closing
90
+ // over its own row object.
91
+ this._rowsByValue = new Map();
81
92
  this.listId = nextId('combobox-list');
93
+ this.inputId = nextId('combobox-input');
82
94
  this._selected = [];
83
95
  // What `form.reset()` restores to. Captured exactly once, from whatever
84
96
  // `<lyra-option selected>` markup was present the first time slotted
@@ -121,6 +133,8 @@ export class LyraCombobox extends LyraElement {
121
133
  this.query = e.target.value;
122
134
  this.activeIndex = -1;
123
135
  this.show();
136
+ if (this.source)
137
+ this.runSource(this.query);
124
138
  this.emit('input');
125
139
  };
126
140
  this.onInputBlur = () => {
@@ -136,22 +150,24 @@ export class LyraCombobox extends LyraElement {
136
150
  this.hasLabelSlot = e.target.assignedElements({ flatten: true }).length > 0;
137
151
  };
138
152
  this.onKeyDown = (e) => {
139
- const opts = this.filtered.filter((o) => !o.disabled);
153
+ const navigable = this.renderedRows.rows.filter((r) => !r.disabled);
140
154
  switch (e.key) {
141
155
  case 'ArrowDown':
142
156
  e.preventDefault();
143
157
  if (!this.open)
144
158
  return this.show();
145
- this.activeIndex = Math.min(opts.length - 1, this.activeIndex + 1);
159
+ this.activeIndex = Math.min(navigable.length - 1, this.activeIndex + 1);
146
160
  break;
147
161
  case 'ArrowUp':
148
162
  e.preventDefault();
163
+ if (!this.open)
164
+ return this.show();
149
165
  this.activeIndex = Math.max(0, this.activeIndex - 1);
150
166
  break;
151
167
  case 'Enter':
152
- if (this.open && this.activeIndex >= 0 && opts[this.activeIndex]) {
168
+ if (this.open && this.activeIndex >= 0 && navigable[this.activeIndex]) {
153
169
  e.preventDefault();
154
- this.pick(opts[this.activeIndex]);
170
+ this.pickRow(navigable[this.activeIndex]);
155
171
  }
156
172
  break;
157
173
  case 'Escape':
@@ -169,7 +185,7 @@ export class LyraCombobox extends LyraElement {
169
185
  case 'End':
170
186
  if (this.open) {
171
187
  e.preventDefault();
172
- this.activeIndex = opts.length - 1;
188
+ this.activeIndex = navigable.length - 1;
173
189
  }
174
190
  break;
175
191
  case 'Backspace':
@@ -188,6 +204,22 @@ export class LyraCombobox extends LyraElement {
188
204
  this.renderRoot.querySelector('[part="combobox-input"]')?.focus();
189
205
  this.show();
190
206
  };
207
+ // Delegated onto [part="listbox"] (see render()) rather than one closure
208
+ // pair allocated per option per render -- resolves the target row via
209
+ // closest('[part="option"]') + a data-value lookup into `_rowsByValue`.
210
+ this.onListboxMouseDown = (e) => {
211
+ if (e.target.closest('[part="option"]'))
212
+ e.preventDefault();
213
+ };
214
+ this.onListboxClick = (e) => {
215
+ const optionEl = e.target.closest('[part="option"]');
216
+ const value = optionEl?.dataset.value;
217
+ if (value === undefined)
218
+ return;
219
+ const row = this._rowsByValue.get(value);
220
+ if (row)
221
+ this.pickRow(row);
222
+ };
191
223
  this.internals = this.attachInternals();
192
224
  }
193
225
  connectedCallback() {
@@ -257,7 +289,36 @@ export class LyraCombobox extends LyraElement {
257
289
  o.selected = sel.has(o.value);
258
290
  }
259
291
  labelFor(value) {
260
- return this.options.find((o) => o.value === value)?.label ?? value;
292
+ return this._selectedLabelCache.get(value) ?? this.options.find((o) => o.value === value)?.label ?? value;
293
+ }
294
+ /** The current row set in a source-agnostic shape, before capping. */
295
+ get effectiveRows() {
296
+ if (this.source)
297
+ return this.asyncRows;
298
+ return this.filtered.map((o) => ({
299
+ value: o.value,
300
+ label: o.label,
301
+ sub: o.sub || undefined,
302
+ dotColor: o.dotColor || undefined,
303
+ group: o.group || undefined,
304
+ disabled: o.disabled,
305
+ }));
306
+ }
307
+ /** `effectiveRows` capped to `maxRender`, always keeping the current selection visible. */
308
+ get renderedRows() {
309
+ const all = this.effectiveRows;
310
+ if (all.length <= this.maxRender)
311
+ return { rows: all, overflow: 0 };
312
+ const capped = all.slice(0, this.maxRender);
313
+ const cappedValues = new Set(capped.map((r) => r.value));
314
+ for (const v of this._selected) {
315
+ if (!cappedValues.has(v)) {
316
+ const selectedRow = all.find((r) => r.value === v);
317
+ if (selectedRow)
318
+ capped.push(selectedRow);
319
+ }
320
+ }
321
+ return { rows: capped, overflow: all.length - capped.length };
261
322
  }
262
323
  get filtered() {
263
324
  const q = this.query.trim().toLowerCase();
@@ -298,6 +359,12 @@ export class LyraCombobox extends LyraElement {
298
359
  const listbox = this.renderRoot.querySelector('[part="listbox"]');
299
360
  if (anchor && listbox)
300
361
  this.cleanup = place(anchor, listbox);
362
+ // Reacting to the `open` property itself (not just inside show())
363
+ // means this fires however `open` became true — via show()'s own
364
+ // user-interaction paths, or a consumer/test setting `el.open = true`
365
+ // directly, which bypasses show() entirely.
366
+ if (this.source && this.asyncRows.length === 0)
367
+ this.runSource(this.query);
301
368
  }
302
369
  }
303
370
  if (changed.has('required'))
@@ -306,17 +373,18 @@ export class LyraCombobox extends LyraElement {
306
373
  this.toggleAttribute('data-invalid', this.touched && !this.internals.validity.valid);
307
374
  }
308
375
  }
309
- pick(option) {
310
- if (option.disabled)
376
+ pickRow(row) {
377
+ if (row.disabled)
311
378
  return;
379
+ this._selectedLabelCache.set(row.value, row.label);
312
380
  if (this.multiple) {
313
381
  const set = new Set(this._selected);
314
- set.has(option.value) ? set.delete(option.value) : set.add(option.value);
382
+ set.has(row.value) ? set.delete(row.value) : set.add(row.value);
315
383
  this.value = [...set];
316
384
  this.query = '';
317
385
  }
318
386
  else {
319
- this.value = option.value;
387
+ this.value = row.value;
320
388
  this.query = '';
321
389
  this.hide();
322
390
  }
@@ -333,38 +401,61 @@ export class LyraCombobox extends LyraElement {
333
401
  this.emit('lyra-clear');
334
402
  this.emit('change');
335
403
  }
336
- renderRows(filtered, activeId) {
337
- const rows = [];
404
+ runSource(query) {
405
+ if (!this.source)
406
+ return;
407
+ clearTimeout(this.sourceTimer);
408
+ this.sourceTimer = setTimeout(() => {
409
+ const token = ++this.sourceToken;
410
+ this.loading = true;
411
+ this.source(query)
412
+ .then((rows) => {
413
+ if (token !== this.sourceToken || !this.isConnected)
414
+ return;
415
+ this.asyncRows = rows;
416
+ })
417
+ .finally(() => {
418
+ if (token === this.sourceToken)
419
+ this.loading = false;
420
+ });
421
+ }, 200);
422
+ }
423
+ renderRows(rows, activeId) {
424
+ const out = [];
338
425
  let currentGroup;
339
- let optIndex = 0;
340
- filtered.forEach((o) => {
426
+ const selectedSet = new Set(this._selected);
427
+ rows.forEach((o, i) => {
341
428
  if (o.group !== currentGroup) {
342
429
  currentGroup = o.group;
343
430
  if (currentGroup)
344
- rows.push(html `<div class="group-label">${currentGroup}</div>`);
431
+ out.push(html `<div class="group-label">${currentGroup}</div>`);
345
432
  }
346
- const id = `${this.listId}-opt-${optIndex++}`;
347
- const selected = this._selected.includes(o.value);
348
- rows.push(html `<div
433
+ const id = `${this.listId}-opt-${i}`;
434
+ const selected = selectedSet.has(o.value);
435
+ out.push(html `<div
349
436
  part="option"
350
437
  id=${id}
351
438
  role="option"
439
+ data-value=${o.value}
352
440
  aria-selected=${selected ? 'true' : 'false'}
353
441
  aria-disabled=${o.disabled ? 'true' : 'false'}
354
442
  ?data-active=${id === activeId}
355
- @mousedown=${(e) => e.preventDefault()}
356
- @click=${() => this.pick(o)}
357
443
  >
358
- <span>${o.label}</span>
444
+ ${o.dotColor ? html `<span part="option-dot" style=${`background:${o.dotColor}`}></span>` : ''}
445
+ <span part="option-label">
446
+ <span>${o.label}</span>
447
+ ${o.sub ? html `<span part="option-sub">${o.sub}</span>` : ''}
448
+ </span>
359
449
  </div>`);
360
450
  });
361
- return rows;
451
+ return out;
362
452
  }
363
453
  render() {
364
- const filtered = this.filtered;
365
- const activeOpts = filtered.filter((o) => !o.disabled);
366
- const active = this.activeIndex >= 0 ? activeOpts[this.activeIndex] : undefined;
367
- const activeId = active ? `${this.listId}-opt-${filtered.indexOf(active)}` : '';
454
+ const { rows, overflow } = this.renderedRows;
455
+ this._rowsByValue = new Map(rows.map((r) => [r.value, r]));
456
+ const navigable = rows.filter((r) => !r.disabled);
457
+ const active = this.activeIndex >= 0 ? navigable[this.activeIndex] : undefined;
458
+ const activeId = active ? `${this.listId}-opt-${rows.indexOf(active)}` : '';
368
459
  const shownTags = this.multiple ? this._selected.slice(0, this.maxOptionsVisible) : [];
369
460
  const extra = this.multiple ? this._selected.length - shownTags.length : 0;
370
461
  const hasValue = this._selected.length > 0;
@@ -373,7 +464,7 @@ export class LyraCombobox extends LyraElement {
373
464
  const hasLabel = this.hasLabelSlot || this.label.length > 0;
374
465
  return html `
375
466
  <div part="form-control">
376
- <label part="form-control-label" ?hidden=${!hasLabel}>
467
+ <label part="form-control-label" for=${this.inputId} ?hidden=${!hasLabel}>
377
468
  ${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
378
469
  </label>
379
470
  <div part="combobox" @mousedown=${this.onComboMouseDown}>
@@ -394,6 +485,7 @@ export class LyraCombobox extends LyraElement {
394
485
  ${extra > 0 ? html `<span part="tag">+${extra}</span>` : ''}
395
486
  </div>
396
487
  <input
488
+ id=${this.inputId}
397
489
  part="combobox-input"
398
490
  role="combobox"
399
491
  aria-label=${this.label || this.placeholder || 'Combobox'}
@@ -425,10 +517,22 @@ export class LyraCombobox extends LyraElement {
425
517
  : ''}
426
518
  <span part="expand-icon" aria-hidden="true">${chevronIcon()}</span>
427
519
  </div>
428
- <div part="listbox" id=${this.listId} role="listbox" aria-multiselectable=${this.multiple ? 'true' : 'false'}>
429
- ${filtered.length === 0
430
- ? html `<div class="empty">${this.emptyText}</div>`
431
- : this.renderRows(filtered, activeId)}
520
+ <div
521
+ part="listbox"
522
+ id=${this.listId}
523
+ role="listbox"
524
+ aria-multiselectable=${this.multiple ? 'true' : 'false'}
525
+ @mousedown=${this.onListboxMouseDown}
526
+ @click=${this.onListboxClick}
527
+ >
528
+ ${this.loading
529
+ ? html `<div class="loading">Loading…</div>`
530
+ : rows.length === 0
531
+ ? html `<div class="empty">${this.emptyText}</div>`
532
+ : html `${this.renderRows(rows, activeId)}
533
+ ${overflow > 0
534
+ ? html `<div part="option-overflow">+${overflow} more — refine your search</div>`
535
+ : ''}`}
432
536
  </div>
433
537
  <div part="error" ?hidden=${!hasError}>
434
538
  ${this.errorText}<slot name="error" @slotchange=${this.onErrorSlotChange}></slot>
@@ -480,6 +584,12 @@ __decorate([
480
584
  __decorate([
481
585
  property({ attribute: false })
482
586
  ], LyraCombobox.prototype, "filter", void 0);
587
+ __decorate([
588
+ property({ attribute: false })
589
+ ], LyraCombobox.prototype, "source", void 0);
590
+ __decorate([
591
+ property({ attribute: 'max-render', type: Number })
592
+ ], LyraCombobox.prototype, "maxRender", void 0);
483
593
  __decorate([
484
594
  state()
485
595
  ], LyraCombobox.prototype, "query", void 0);
@@ -501,4 +611,10 @@ __decorate([
501
611
  __decorate([
502
612
  state()
503
613
  ], LyraCombobox.prototype, "hasLabelSlot", void 0);
614
+ __decorate([
615
+ state()
616
+ ], LyraCombobox.prototype, "loading", void 0);
617
+ __decorate([
618
+ state()
619
+ ], LyraCombobox.prototype, "asyncRows", void 0);
504
620
  defineElement('combobox', LyraCombobox);
@@ -172,6 +172,27 @@ export const styles = css `
172
172
  opacity: var(--lyra-opacity-disabled);
173
173
  cursor: not-allowed;
174
174
  }
175
+ [part='option-dot'] {
176
+ inline-size: 0.5rem;
177
+ block-size: 0.5rem;
178
+ border-radius: 50%;
179
+ flex: 0 0 auto;
180
+ }
181
+ [part='option-label'] {
182
+ display: flex;
183
+ flex-direction: column;
184
+ min-inline-size: 0;
185
+ }
186
+ [part='option-sub'] {
187
+ font-size: 0.75rem;
188
+ color: var(--lyra-color-text-quiet);
189
+ }
190
+ [part='option-overflow'],
191
+ .loading {
192
+ padding: var(--lyra-space-s) var(--lyra-space-m);
193
+ font-size: 0.8125rem;
194
+ color: var(--lyra-color-text-quiet);
195
+ }
175
196
 
176
197
  .group-label {
177
198
  padding: var(--lyra-space-xs) var(--lyra-space-s) 0;
@@ -21,6 +21,10 @@ export declare class LyraOption extends LyraElement {
21
21
  group: string;
22
22
  /** Extra text the filter should match beyond the label. */
23
23
  searchText: string;
24
+ /** Optional secondary line rendered under the label (e.g. a status/date summary). */
25
+ sub: string;
26
+ /** Optional color for a small leading status dot (any valid CSS color). */
27
+ dotColor: string;
24
28
  /** Resolved label: explicit non-empty `label` attribute wins, else trimmed text content. */
25
29
  get label(): string;
26
30
  render(): TemplateResult;
@@ -30,6 +30,10 @@ export class LyraOption extends LyraElement {
30
30
  this.group = '';
31
31
  /** Extra text the filter should match beyond the label. */
32
32
  this.searchText = '';
33
+ /** Optional secondary line rendered under the label (e.g. a status/date summary). */
34
+ this.sub = '';
35
+ /** Optional color for a small leading status dot (any valid CSS color). */
36
+ this.dotColor = '';
33
37
  }
34
38
  static { this.styles = [
35
39
  LyraElement.styles,
@@ -63,4 +67,10 @@ __decorate([
63
67
  __decorate([
64
68
  property({ attribute: 'search-text' })
65
69
  ], LyraOption.prototype, "searchText", void 0);
70
+ __decorate([
71
+ property()
72
+ ], LyraOption.prototype, "sub", void 0);
73
+ __decorate([
74
+ property({ attribute: 'dot-color' })
75
+ ], LyraOption.prototype, "dotColor", void 0);
66
76
  defineElement('option', LyraOption);
@@ -16,5 +16,9 @@ export declare function monthMatrix(year: number, month: number, firstDayOfWeek?
16
16
  export declare function weekdayLabels(firstDayOfWeek?: number, format?: WeekdayFormat, locale?: string): string[];
17
17
  /** Localized "Month YYYY" title. */
18
18
  export declare function monthTitle(year: number, month: number, locale?: string): string;
19
- /** Resolve a `first-day-of-week` attribute (name or 'auto') to a 0–6 index. */
20
- export declare function resolveFirstDayOfWeek(value: string): number;
19
+ /**
20
+ * Resolve a `first-day-of-week` attribute (name or 'auto') to a 0–6 index.
21
+ * `'auto'` derives the answer from `locale` via `Intl.Locale`'s week-info
22
+ * when the runtime supports it, falling back to Sunday otherwise.
23
+ */
24
+ export declare function resolveFirstDayOfWeek(value: string, locale?: string): number;
@@ -63,9 +63,28 @@ export function monthTitle(year, month, locale) {
63
63
  return new Intl.DateTimeFormat(locale || undefined, { month: 'long', year: 'numeric' }).format(new Date(year, month, 1));
64
64
  }
65
65
  const FDOW = { sun: 0, mon: 1, tue: 2, wed: 3, thu: 4, fri: 5, sat: 6 };
66
- /** Resolve a `first-day-of-week` attribute (name or 'auto') to a 0–6 index. */
67
- export function resolveFirstDayOfWeek(value) {
68
- if (!value || value === 'auto')
69
- return 0;
66
+ /** Locale-derived first day of week (0=Sunday 6=Saturday), or null if unsupported. */
67
+ function localeFirstDayOfWeek(locale) {
68
+ try {
69
+ const l = new Intl.Locale(locale);
70
+ const firstDay = l.weekInfo?.firstDay ?? l.getWeekInfo?.().firstDay;
71
+ if (!firstDay)
72
+ return null;
73
+ return firstDay % 7; // ISO 1-7 (Mon-Sun) -> 0-6 (Sun-Sat)
74
+ }
75
+ catch {
76
+ return null;
77
+ }
78
+ }
79
+ /**
80
+ * Resolve a `first-day-of-week` attribute (name or 'auto') to a 0–6 index.
81
+ * `'auto'` derives the answer from `locale` via `Intl.Locale`'s week-info
82
+ * when the runtime supports it, falling back to Sunday otherwise.
83
+ */
84
+ export function resolveFirstDayOfWeek(value, locale) {
85
+ if (!value || value === 'auto') {
86
+ const derived = locale ? localeFirstDayOfWeek(locale) : null;
87
+ return derived ?? 0;
88
+ }
70
89
  return FDOW[value.toLowerCase()] ?? 0;
71
90
  }
@@ -36,6 +36,7 @@ export declare class LyraDateInput extends LyraDateInput_base {
36
36
  firstDayOfWeek: string;
37
37
  weekdayFormat: WeekdayFormat;
38
38
  private cleanupFn?;
39
+ private inputId;
39
40
  private touched;
40
41
  private hasHintSlot;
41
42
  private hasErrorSlot;
@@ -10,6 +10,7 @@ import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { FormAssociated } from '../../internal/form-associated.js';
11
11
  import { defineElement } from '../../internal/prefix.js';
12
12
  import { place } from '../../internal/positioner.js';
13
+ import { nextId } from '../../internal/a11y.js';
13
14
  import { closeIcon, calendarIcon } from '../../internal/icons.js';
14
15
  import { parseISO, formatISO } from './calendar-core.js';
15
16
  import { styles } from './date-input.styles.js';
@@ -48,6 +49,7 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
48
49
  this.months = 1;
49
50
  this.firstDayOfWeek = 'auto';
50
51
  this.weekdayFormat = 'short';
52
+ this.inputId = nextId('date-input');
51
53
  // Set on the date input's first `blur`; gates the `data-invalid`
52
54
  // reflection below so validity styling never flashes on first render.
53
55
  this.touched = false;
@@ -66,8 +68,11 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
66
68
  this.hide();
67
69
  };
68
70
  this.onInputChange = (e) => {
69
- const raw = e.target.value.trim();
71
+ const target = e.target;
72
+ const raw = target.value.trim();
70
73
  if (!raw) {
74
+ // The mixin's value setter recomputes validity (updateValidity()),
75
+ // which clears any stale badInput state along the way.
71
76
  this.value = '';
72
77
  this.emit('input');
73
78
  this.emit('change');
@@ -79,6 +84,15 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
79
84
  this.emit('input');
80
85
  this.emit('change');
81
86
  }
87
+ else {
88
+ // Unparseable text: don't silently keep the committed value while the
89
+ // field still shows garbage -- revert the displayed text back to the
90
+ // last valid commit and flag the constraint-validation state (mirrors
91
+ // how `required` is already wired via internals.setValidity(), see
92
+ // form-associated.ts).
93
+ target.value = this.displayText;
94
+ this.internals.setValidity({ badInput: true }, 'Enter a valid date');
95
+ }
82
96
  };
83
97
  this.onInputKey = (e) => {
84
98
  if (e.altKey && e.key === 'ArrowDown') {
@@ -176,11 +190,12 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
176
190
  const hasLabel = this.hasLabelSlot || this.label.length > 0;
177
191
  return html `
178
192
  <div part="form-control">
179
- <label part="form-control-label" ?hidden=${!hasLabel}>
193
+ <label part="form-control-label" for=${this.inputId} ?hidden=${!hasLabel}>
180
194
  ${this.label}<slot name="label" @slotchange=${this.onLabelSlotChange}></slot>
181
195
  </label>
182
196
  <div part="input-wrapper">
183
197
  <input
198
+ id=${this.inputId}
184
199
  part="input"
185
200
  type="text"
186
201
  aria-label=${this.label || this.placeholder || 'Date'}
@@ -223,6 +238,8 @@ export class LyraDateInput extends FormAssociated(LyraElement) {
223
238
  .max=${this.max}
224
239
  .months=${this.months}
225
240
  .locale=${this.locale}
241
+ .disabled=${this.disabled}
242
+ .readonly=${this.readonly}
226
243
  first-day-of-week=${this.firstDayOfWeek}
227
244
  weekday-format=${this.weekdayFormat}
228
245
  @change=${this.onPickerChange}
@@ -102,9 +102,11 @@ export class LyraDatePicker extends LyraElement {
102
102
  }
103
103
  }
104
104
  get fdow() {
105
- return resolveFirstDayOfWeek(this.firstDayOfWeek);
105
+ return resolveFirstDayOfWeek(this.firstDayOfWeek, this.locale);
106
106
  }
107
107
  isDisabled(d) {
108
+ if (this.disabled || this.readonly)
109
+ return true;
108
110
  const min = parseISO(this.min);
109
111
  const max = parseISO(this.max);
110
112
  if (min && d < min)
@@ -135,7 +137,7 @@ export class LyraDatePicker extends LyraElement {
135
137
  this.emit('change');
136
138
  }
137
139
  selectDate(date) {
138
- if (this.disabled || this.readonly || this.isDisabled(date))
140
+ if (this.isDisabled(date))
139
141
  return;
140
142
  this.focusedDate = date;
141
143
  if (this.mode === 'range') {
@@ -4,6 +4,10 @@ export const styles = css `
4
4
  display: inline-block;
5
5
  --lyra-cell-size: 2.25rem;
6
6
  }
7
+ :host([disabled]) {
8
+ opacity: var(--lyra-opacity-disabled);
9
+ pointer-events: none;
10
+ }
7
11
  [part='base'] {
8
12
  display: flex;
9
13
  gap: var(--lyra-space-l);
@@ -94,7 +98,7 @@ export const styles = css `
94
98
  [part~='day-range-start'],
95
99
  [part~='day-range-end'] {
96
100
  background: var(--lyra-color-brand);
97
- color: #fff;
101
+ color: var(--lyra-color-on-brand);
98
102
  }
99
103
  [part~='day']:disabled {
100
104
  color: var(--lyra-color-text-quiet);
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
@@ -0,0 +1,14 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Empty',
4
+ component: 'lyra-empty',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Default = {
9
+ render: () => html `
10
+ <lyra-empty heading="No results" description="Try a different search.">
11
+ <span slot="actions"><button>Reset</button></span>
12
+ </lyra-empty>
13
+ `,
14
+ };
@@ -54,13 +54,11 @@ export class LyraExportButton extends LyraElement {
54
54
  if (this.open)
55
55
  return;
56
56
  this.open = true;
57
- document.addEventListener('pointerdown', this.onDocPointer);
58
57
  }
59
58
  closeMenu() {
60
59
  if (!this.open)
61
60
  return;
62
61
  this.open = false;
63
- document.removeEventListener('pointerdown', this.onDocPointer);
64
62
  }
65
63
  firstUpdated() {
66
64
  // Single delegated listener catches Escape from the trigger button or
@@ -71,11 +69,18 @@ export class LyraExportButton extends LyraElement {
71
69
  if (changed.has('open')) {
72
70
  this.cleanup?.();
73
71
  this.cleanup = undefined;
72
+ // Reacting to the `open` property itself (not just inside
73
+ // openMenu()) means this fires however `open` became true -- via
74
+ // openMenu()'s own click path, or a consumer/test setting
75
+ // `el.open = true` directly (valid API on a `reflect: true`
76
+ // property), which bypasses openMenu() entirely.
77
+ document.removeEventListener('pointerdown', this.onDocPointer);
74
78
  if (this.open) {
75
79
  const anchor = this.renderRoot.querySelector('[part="trigger"]');
76
80
  const menu = this.renderRoot.querySelector('[part="menu"]');
77
81
  if (anchor && menu)
78
82
  this.cleanup = place(anchor, menu);
83
+ document.addEventListener('pointerdown', this.onDocPointer);
79
84
  }
80
85
  }
81
86
  }
@@ -0,0 +1,6 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const SingleFormat: Story;
6
+ export declare const MultiFormatMenu: Story;
@@ -0,0 +1,32 @@
1
+ import { html } from 'lit';
2
+ const rows = [
3
+ { id: 'a', name: 'Alpha', score: 92 },
4
+ { id: 'b', name: 'Beta', score: 81 },
5
+ { id: 'c', name: 'Gamma', score: 76 },
6
+ ];
7
+ const columns = [
8
+ { key: 'id', label: 'ID' },
9
+ { key: 'name', label: 'Name' },
10
+ { key: 'score', label: 'Score' },
11
+ ];
12
+ const meta = {
13
+ title: 'ExportButton',
14
+ component: 'lyra-export-button',
15
+ tags: ['autodocs'],
16
+ };
17
+ export default meta;
18
+ export const SingleFormat = {
19
+ render: () => html `
20
+ <lyra-export-button filename="demo" .rows=${rows} .columns=${columns}></lyra-export-button>
21
+ `,
22
+ };
23
+ export const MultiFormatMenu = {
24
+ render: () => html `
25
+ <lyra-export-button
26
+ filename="demo"
27
+ .rows=${rows}
28
+ .columns=${columns}
29
+ .formats=${['csv', 'json']}
30
+ ></lyra-export-button>
31
+ `,
32
+ };
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
@@ -0,0 +1,10 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'FileInput',
4
+ component: 'lyra-file-input',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Default = {
9
+ render: () => html `<lyra-file-input multiple accept=".csv,.xlsx"></lyra-file-input>`,
10
+ };