@aceshooting/lyra-ui 1.0.1 → 1.2.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 (150) hide show
  1. package/README.md +9 -10
  2. package/custom-elements.json +15118 -8342
  3. package/dist/components/chart/chart.d.ts +57 -1
  4. package/dist/components/chart/chart.js +202 -12
  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 +83 -0
  13. package/dist/components/chart/lite-chart.js +424 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +14 -0
  15. package/dist/components/chart/lite-chart.stories.js +114 -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 +76 -13
  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 +69 -3
  50. package/dist/components/flag/flag.js +107 -30
  51. package/dist/components/flag/flag.stories.d.ts +1 -0
  52. package/dist/components/flag/flag.stories.js +14 -0
  53. package/dist/components/flag/flag.styles.js +1 -1
  54. package/dist/components/flag/language-map.d.ts +11 -0
  55. package/dist/components/flag/language-map.js +12 -1
  56. package/dist/components/gauge/gauge.d.ts +2 -0
  57. package/dist/components/gauge/gauge.js +19 -5
  58. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  59. package/dist/components/gauge/gauge.stories.js +6 -0
  60. package/dist/components/graph/graph-loader.d.ts +28 -0
  61. package/dist/components/graph/graph-loader.js +44 -0
  62. package/dist/components/graph/graph.d.ts +41 -15
  63. package/dist/components/graph/graph.js +146 -71
  64. package/dist/components/graph/graph.stories.d.ts +3 -0
  65. package/dist/components/graph/graph.stories.js +38 -0
  66. package/dist/components/graph/graph.styles.js +6 -1
  67. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  68. package/dist/components/heatmap/calendar-grid.js +27 -10
  69. package/dist/components/heatmap/heatmap.d.ts +149 -12
  70. package/dist/components/heatmap/heatmap.js +445 -28
  71. package/dist/components/heatmap/heatmap.stories.d.ts +22 -0
  72. package/dist/components/heatmap/heatmap.stories.js +155 -0
  73. package/dist/components/heatmap/heatmap.styles.js +60 -2
  74. package/dist/components/map/map.d.ts +17 -1
  75. package/dist/components/map/map.js +68 -21
  76. package/dist/components/map/map.stories.d.ts +10 -0
  77. package/dist/components/map/map.stories.js +107 -12
  78. package/dist/components/map/map.styles.js +2 -2
  79. package/dist/components/playback/playback.d.ts +6 -2
  80. package/dist/components/playback/playback.js +36 -12
  81. package/dist/components/playback/playback.stories.d.ts +2 -0
  82. package/dist/components/playback/playback.stories.js +6 -0
  83. package/dist/components/playback/playback.styles.js +10 -2
  84. package/dist/components/select/select.d.ts +125 -0
  85. package/dist/components/select/select.js +480 -0
  86. package/dist/components/select/select.stories.d.ts +21 -0
  87. package/dist/components/select/select.stories.js +91 -0
  88. package/dist/components/select/select.styles.d.ts +1 -0
  89. package/dist/components/select/select.styles.js +212 -0
  90. package/dist/components/skeleton/skeleton.js +4 -7
  91. package/dist/components/sparkline/sparkline.d.ts +1 -1
  92. package/dist/components/sparkline/sparkline.js +40 -13
  93. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  94. package/dist/components/sparkline/sparkline.stories.js +22 -0
  95. package/dist/components/split/split.d.ts +24 -3
  96. package/dist/components/split/split.js +155 -31
  97. package/dist/components/split/split.stories.d.ts +2 -0
  98. package/dist/components/split/split.stories.js +20 -0
  99. package/dist/components/stat/stat.d.ts +43 -1
  100. package/dist/components/stat/stat.js +97 -4
  101. package/dist/components/stat/stat.stories.d.ts +4 -0
  102. package/dist/components/stat/stat.stories.js +66 -1
  103. package/dist/components/stat/stat.styles.js +64 -0
  104. package/dist/components/table/table.d.ts +64 -2
  105. package/dist/components/table/table.js +213 -17
  106. package/dist/components/table/table.stories.d.ts +6 -0
  107. package/dist/components/table/table.stories.js +45 -0
  108. package/dist/components/table/table.styles.js +51 -4
  109. package/dist/components/time-range/time-range.d.ts +34 -5
  110. package/dist/components/time-range/time-range.js +217 -36
  111. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  112. package/dist/components/time-range/time-range.stories.js +13 -0
  113. package/dist/components/time-range/time-range.styles.js +44 -2
  114. package/dist/components/toast/toast-item.d.ts +14 -2
  115. package/dist/components/toast/toast-item.js +134 -17
  116. package/dist/components/toast/toast-item.styles.js +28 -2
  117. package/dist/components/toast/toast.stories.d.ts +3 -0
  118. package/dist/components/toast/toast.stories.js +41 -1
  119. package/dist/components/toast/toast.styles.js +11 -11
  120. package/dist/components/toast/toaster.d.ts +1 -1
  121. package/dist/components/toast/toaster.js +13 -6
  122. package/dist/components/tree/tree-node.d.ts +13 -7
  123. package/dist/components/tree/tree-node.js +24 -10
  124. package/dist/components/tree/tree.d.ts +64 -15
  125. package/dist/components/tree/tree.js +113 -22
  126. package/dist/components/tree/tree.stories.d.ts +4 -0
  127. package/dist/components/tree/tree.stories.js +31 -1
  128. package/dist/components/tree/update-cascade.d.ts +13 -0
  129. package/dist/components/tree/update-cascade.js +13 -0
  130. package/dist/components/widget/widget.d.ts +25 -1
  131. package/dist/components/widget/widget.js +164 -6
  132. package/dist/components/widget/widget.stories.d.ts +3 -0
  133. package/dist/components/widget/widget.stories.js +43 -0
  134. package/dist/components/widget/widget.styles.js +20 -3
  135. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  136. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  137. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  138. package/dist/components/word-cloud/word-cloud.js +255 -0
  139. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  140. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  141. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  142. package/dist/components/word-cloud/word-cloud.styles.js +55 -0
  143. package/dist/internal/a11y.js +2 -1
  144. package/dist/internal/rtl.d.ts +7 -0
  145. package/dist/internal/rtl.js +9 -0
  146. package/dist/lyra.d.ts +9 -1
  147. package/dist/lyra.js +6 -0
  148. package/llms-full.txt +962 -276
  149. package/llms.txt +12 -9
  150. package/package.json +6 -4
@@ -11,7 +11,9 @@ export type ExportFormat = 'csv' | 'json';
11
11
  * @event lyra-export - `detail: { format }`, cancelable — call `preventDefault()`
12
12
  * to substitute the built-in client-side download with a server-generated one.
13
13
  * @event lyra-export-complete - Fired after a non-cancelled download completes.
14
- * @csspart trigger, menu, menu-item
14
+ * @csspart trigger - The button that triggers the export (or opens the format menu).
15
+ * @csspart menu - The format-choice menu, shown when more than one format is configured.
16
+ * @csspart menu-item - A single format option inside the menu.
15
17
  */
16
18
  export declare class LyraExportButton extends LyraElement {
17
19
  static styles: import("lit").CSSResultGroup[];
@@ -22,14 +24,26 @@ export declare class LyraExportButton extends LyraElement {
22
24
  disabled: boolean;
23
25
  label: string;
24
26
  open: boolean;
27
+ private triggerEl?;
28
+ private menuEl?;
29
+ private readonly menuId;
25
30
  private cleanup?;
31
+ /** Which menu item to focus the next time `open` flips true; reset after use. */
32
+ private pendingMenuFocusIndex;
26
33
  disconnectedCallback(): void;
27
34
  private onDocPointer;
28
35
  private openMenu;
29
36
  private closeMenu;
37
+ private menuItemEls;
38
+ /** Focuses the menu item at `index` (clamped), if the menu is already open and rendered. */
39
+ private focusMenuItem;
40
+ /** Opens the menu (if closed) and focuses `index`, or moves focus there directly if already open. */
41
+ private focusMenuItemOnOpen;
30
42
  private onKeyDown;
31
43
  protected firstUpdated(): void;
32
44
  protected updated(changed: PropertyValues): void;
45
+ /** Applies the same `columns` allow-list CSV exports use, so JSON can't leak fields CSV hides. */
46
+ private rowsForExport;
33
47
  private doExport;
34
48
  private onTriggerClick;
35
49
  render(): TemplateResult;
@@ -5,10 +5,11 @@ 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, nothing } from 'lit';
8
- import { property } from 'lit/decorators.js';
8
+ import { property, query } from 'lit/decorators.js';
9
9
  import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { defineElement } from '../../internal/prefix.js';
11
11
  import { place } from '../../internal/positioner.js';
12
+ import { nextId } from '../../internal/a11y.js';
12
13
  import { buildCsv, downloadBlob } from './csv.js';
13
14
  import { styles } from './export-button.styles.js';
14
15
  /**
@@ -20,7 +21,9 @@ import { styles } from './export-button.styles.js';
20
21
  * @event lyra-export - `detail: { format }`, cancelable — call `preventDefault()`
21
22
  * to substitute the built-in client-side download with a server-generated one.
22
23
  * @event lyra-export-complete - Fired after a non-cancelled download completes.
23
- * @csspart trigger, menu, menu-item
24
+ * @csspart trigger - The button that triggers the export (or opens the format menu).
25
+ * @csspart menu - The format-choice menu, shown when more than one format is configured.
26
+ * @csspart menu-item - A single format option inside the menu.
24
27
  */
25
28
  export class LyraExportButton extends LyraElement {
26
29
  constructor() {
@@ -32,15 +35,47 @@ export class LyraExportButton extends LyraElement {
32
35
  this.disabled = false;
33
36
  this.label = 'Export';
34
37
  this.open = false;
38
+ this.menuId = nextId('export-menu');
39
+ /** Which menu item to focus the next time `open` flips true; reset after use. */
40
+ this.pendingMenuFocusIndex = 0;
35
41
  this.onDocPointer = (e) => {
36
42
  if (!e.composedPath().includes(this))
37
43
  this.closeMenu();
38
44
  };
39
45
  this.onKeyDown = (e) => {
40
- if (e.key === 'Escape' && this.open) {
41
- e.preventDefault();
42
- this.closeMenu();
43
- this.renderRoot.querySelector('[part="trigger"]')?.focus();
46
+ if (e.key === 'Escape') {
47
+ if (this.open) {
48
+ e.preventDefault();
49
+ this.closeMenu();
50
+ this.triggerEl?.focus();
51
+ }
52
+ return;
53
+ }
54
+ if (this.formats.length <= 1 || this.disabled)
55
+ return;
56
+ const items = this.menuItemEls();
57
+ const currentIndex = items.indexOf(e.target);
58
+ switch (e.key) {
59
+ case 'ArrowDown':
60
+ e.preventDefault();
61
+ this.focusMenuItemOnOpen(currentIndex === -1 ? 0 : currentIndex + 1);
62
+ break;
63
+ case 'ArrowUp':
64
+ e.preventDefault();
65
+ this.focusMenuItemOnOpen(currentIndex === -1 ? items.length - 1 : currentIndex - 1);
66
+ break;
67
+ case 'Home':
68
+ if (this.open) {
69
+ e.preventDefault();
70
+ this.focusMenuItem(0);
71
+ }
72
+ break;
73
+ case 'End':
74
+ if (this.open) {
75
+ e.preventDefault();
76
+ this.focusMenuItem(items.length - 1);
77
+ }
78
+ break;
44
79
  }
45
80
  };
46
81
  }
@@ -60,9 +95,29 @@ export class LyraExportButton extends LyraElement {
60
95
  return;
61
96
  this.open = false;
62
97
  }
98
+ menuItemEls() {
99
+ return Array.from(this.renderRoot.querySelectorAll('[part="menu-item"]'));
100
+ }
101
+ /** Focuses the menu item at `index` (clamped), if the menu is already open and rendered. */
102
+ focusMenuItem(index) {
103
+ const items = this.menuItemEls();
104
+ if (items.length === 0)
105
+ return;
106
+ items[Math.max(0, Math.min(items.length - 1, index))]?.focus();
107
+ }
108
+ /** Opens the menu (if closed) and focuses `index`, or moves focus there directly if already open. */
109
+ focusMenuItemOnOpen(index) {
110
+ if (this.open) {
111
+ this.focusMenuItem(index);
112
+ }
113
+ else {
114
+ this.pendingMenuFocusIndex = index;
115
+ this.openMenu();
116
+ }
117
+ }
63
118
  firstUpdated() {
64
- // Single delegated listener catches Escape from the trigger button or
65
- // any menu-item inside this shadow root.
119
+ // Single delegated listener catches Escape/Arrow/Home/End from the
120
+ // trigger button or any menu-item inside this shadow root.
66
121
  this.renderRoot.addEventListener('keydown', this.onKeyDown);
67
122
  }
68
123
  updated(changed) {
@@ -76,16 +131,31 @@ export class LyraExportButton extends LyraElement {
76
131
  // property), which bypasses openMenu() entirely.
77
132
  document.removeEventListener('pointerdown', this.onDocPointer);
78
133
  if (this.open) {
79
- const anchor = this.renderRoot.querySelector('[part="trigger"]');
80
- const menu = this.renderRoot.querySelector('[part="menu"]');
134
+ const anchor = this.triggerEl;
135
+ const menu = this.menuEl;
81
136
  if (anchor && menu)
82
137
  this.cleanup = place(anchor, menu);
83
138
  document.addEventListener('pointerdown', this.onDocPointer);
139
+ this.focusMenuItem(this.pendingMenuFocusIndex);
140
+ this.pendingMenuFocusIndex = 0;
84
141
  }
85
142
  }
86
143
  }
144
+ /** Applies the same `columns` allow-list CSV exports use, so JSON can't leak fields CSV hides. */
145
+ rowsForExport() {
146
+ if (this.columns.length === 0)
147
+ return this.rows;
148
+ const keys = this.columns.map((c) => c.key);
149
+ return this.rows.map((row) => {
150
+ const picked = {};
151
+ for (const key of keys)
152
+ picked[key] = row[key];
153
+ return picked;
154
+ });
155
+ }
87
156
  doExport(format) {
88
157
  this.closeMenu();
158
+ this.triggerEl?.focus();
89
159
  const ev = this.emit('lyra-export', { format });
90
160
  if (ev.defaultPrevented)
91
161
  return;
@@ -93,7 +163,7 @@ export class LyraExportButton extends LyraElement {
93
163
  downloadBlob(buildCsv(this.rows, this.columns), `${this.filename}.csv`, 'text/csv;charset=utf-8;');
94
164
  }
95
165
  else {
96
- downloadBlob(JSON.stringify(this.rows, null, 2), `${this.filename}.json`, 'application/json');
166
+ downloadBlob(JSON.stringify(this.rowsForExport(), null, 2), `${this.filename}.json`, 'application/json');
97
167
  }
98
168
  this.emit('lyra-export-complete', { format });
99
169
  }
@@ -113,12 +183,13 @@ export class LyraExportButton extends LyraElement {
113
183
  ?disabled=${this.disabled}
114
184
  aria-haspopup=${this.formats.length > 1 ? 'menu' : nothing}
115
185
  aria-expanded=${this.formats.length > 1 ? (this.open ? 'true' : 'false') : nothing}
186
+ aria-controls=${this.formats.length > 1 ? this.menuId : nothing}
116
187
  @click=${() => this.onTriggerClick()}
117
188
  >
118
189
  ${this.label}
119
190
  </button>
120
191
  ${this.formats.length > 1
121
- ? html `<div part="menu" role="menu">
192
+ ? html `<div id=${this.menuId} part="menu" role="menu">
122
193
  ${this.formats.map((f) => html `<button
123
194
  part="menu-item"
124
195
  role="menuitem"
@@ -153,4 +224,10 @@ __decorate([
153
224
  __decorate([
154
225
  property({ type: Boolean, reflect: true })
155
226
  ], LyraExportButton.prototype, "open", void 0);
227
+ __decorate([
228
+ query('[part="trigger"]')
229
+ ], LyraExportButton.prototype, "triggerEl", void 0);
230
+ __decorate([
231
+ query('[part="menu"]')
232
+ ], LyraExportButton.prototype, "menuEl", void 0);
156
233
  defineElement('export-button', LyraExportButton);
@@ -4,3 +4,4 @@ export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const SingleFormat: Story;
6
6
  export declare const MultiFormatMenu: Story;
7
+ export declare const Disabled: Story;
@@ -30,3 +30,8 @@ export const MultiFormatMenu = {
30
30
  ></lyra-export-button>
31
31
  `,
32
32
  };
33
+ export const Disabled = {
34
+ render: () => html `
35
+ <lyra-export-button filename="demo" .rows=${rows} .columns=${columns} disabled></lyra-export-button>
36
+ `,
37
+ };
@@ -30,7 +30,16 @@ export const styles = css `
30
30
  [part='menu'] {
31
31
  /* Closed state: invisible + slightly raised. visibility (not
32
32
  display:none) so opacity/transform can actually transition; hit-testing
33
- and a11y exposure stay off since this part is already position:fixed. */
33
+ and a11y exposure stay off since this part is already position:fixed.
34
+ visibility deliberately isn't in the transition list below: a
35
+ transitioned property's computed value only settles to its new
36
+ target after the UA has run an actual style-change/rendering pass,
37
+ which lags behind a same-tick attribute write (e.g. updated()
38
+ synchronously focusing the first menu item right after flipping
39
+ open) -- that item would still resolve as visibility: hidden (and
40
+ so silently fail to focus) at the moment .focus() is called. Leaving
41
+ visibility untransitioned makes it apply immediately, in the same
42
+ synchronous style pass as the open attribute write. */
34
43
  visibility: hidden;
35
44
  position: fixed;
36
45
  z-index: 900;
@@ -44,8 +53,7 @@ export const styles = css `
44
53
  transform: translateY(-4px);
45
54
  transition:
46
55
  opacity var(--lyra-transition-fast),
47
- transform var(--lyra-transition-fast),
48
- visibility var(--lyra-transition-fast);
56
+ transform var(--lyra-transition-fast);
49
57
  }
50
58
  :host([open]) [part='menu'] {
51
59
  visibility: visible;
@@ -8,6 +8,12 @@
8
8
  * `file` may be a `DataTransferItem` cast as `File` (the dragenter-preview
9
9
  * call site, before the drop payload has real `File` objects) - unlike
10
10
  * `File`, `DataTransferItem` has no `.name`, only `.type`, so extension
11
- * patterns (`.csv`) can't be evaluated pre-drop and never match.
11
+ * patterns (`.csv`) can't be evaluated pre-drop. By default an unresolved
12
+ * extension pattern counts as no match, since that's the correct behavior
13
+ * once real `File` objects are available at drop time. Pass
14
+ * `assumeExtensionMatch: true` to instead treat an unresolved extension
15
+ * pattern as a possible match — this is what dragenter preview uses so it
16
+ * doesn't render a false "reject" state for accept lists that are
17
+ * extension-only, when the file may well be accepted once dropped.
12
18
  */
13
- export declare function matchesAccept(file: File, accept: string): boolean;
19
+ export declare function matchesAccept(file: File, accept: string, assumeExtensionMatch?: boolean): boolean;
@@ -8,9 +8,15 @@
8
8
  * `file` may be a `DataTransferItem` cast as `File` (the dragenter-preview
9
9
  * call site, before the drop payload has real `File` objects) - unlike
10
10
  * `File`, `DataTransferItem` has no `.name`, only `.type`, so extension
11
- * patterns (`.csv`) can't be evaluated pre-drop and never match.
11
+ * patterns (`.csv`) can't be evaluated pre-drop. By default an unresolved
12
+ * extension pattern counts as no match, since that's the correct behavior
13
+ * once real `File` objects are available at drop time. Pass
14
+ * `assumeExtensionMatch: true` to instead treat an unresolved extension
15
+ * pattern as a possible match — this is what dragenter preview uses so it
16
+ * doesn't render a false "reject" state for accept lists that are
17
+ * extension-only, when the file may well be accepted once dropped.
12
18
  */
13
- export function matchesAccept(file, accept) {
19
+ export function matchesAccept(file, accept, assumeExtensionMatch = false) {
14
20
  const parts = accept
15
21
  .split(',')
16
22
  .map((s) => s.trim().toLowerCase())
@@ -21,7 +27,7 @@ export function matchesAccept(file, accept) {
21
27
  const type = (file.type || '').toLowerCase();
22
28
  return parts.some((p) => {
23
29
  if (p.startsWith('.'))
24
- return name !== undefined && name.endsWith(p);
30
+ return name === undefined ? assumeExtensionMatch : name.endsWith(p);
25
31
  if (p.endsWith('/*'))
26
32
  return type.startsWith(p.slice(0, -1));
27
33
  return type === p;
@@ -10,9 +10,11 @@ export interface RejectedFile {
10
10
  * where files ultimately get uploaded and processed anyway.
11
11
  *
12
12
  * @customElement lyra-file-input
13
- * @slot - Custom drop-zone content, overrides the `label` attribute.
13
+ * @slot - Custom drop-zone content, overrides the `label` attribute. The
14
+ * accessible name always comes from `label`, so icon-only slot content
15
+ * remains announced correctly.
14
16
  * @event lyra-files - `detail: { files, rejected }`, fired on drop and manual selection.
15
- * @csspart base, input
17
+ * @csspart base, input, status
16
18
  */
17
19
  export declare class LyraFileInput extends LyraElement {
18
20
  static styles: import("lit").CSSResultGroup[];
@@ -38,6 +40,7 @@ export declare class LyraFileInput extends LyraElement {
38
40
  private onDrop;
39
41
  private onInputChange;
40
42
  private onKeyDown;
43
+ private statusText;
41
44
  render(): TemplateResult;
42
45
  }
43
46
  declare global {
@@ -8,6 +8,7 @@ import { html, nothing } from 'lit';
8
8
  import { property, state, query } from 'lit/decorators.js';
9
9
  import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { defineElement } from '../../internal/prefix.js';
11
+ import { srOnly } from '../../internal/a11y.js';
11
12
  import { styles } from './file-input.styles.js';
12
13
  import { matchesAccept } from './accept.js';
13
14
  /**
@@ -16,9 +17,11 @@ import { matchesAccept } from './accept.js';
16
17
  * where files ultimately get uploaded and processed anyway.
17
18
  *
18
19
  * @customElement lyra-file-input
19
- * @slot - Custom drop-zone content, overrides the `label` attribute.
20
+ * @slot - Custom drop-zone content, overrides the `label` attribute. The
21
+ * accessible name always comes from `label`, so icon-only slot content
22
+ * remains announced correctly.
20
23
  * @event lyra-files - `detail: { files, rejected }`, fired on drop and manual selection.
21
- * @csspart base, input
24
+ * @csspart base, input, status
22
25
  */
23
26
  export class LyraFileInput extends LyraElement {
24
27
  constructor() {
@@ -80,13 +83,16 @@ export class LyraFileInput extends LyraElement {
80
83
  }
81
84
  };
82
85
  }
83
- static { this.styles = [LyraElement.styles, styles]; }
84
- isAllowed(file) {
86
+ static { this.styles = [LyraElement.styles, styles, srOnly]; }
87
+ isAllowed(file, isPreview = false) {
85
88
  if (this.forbiddenMimeTypes.includes(file.type))
86
89
  return 'type';
87
90
  if (this.allowedMimeTypes.length > 0 && !this.allowedMimeTypes.includes(file.type))
88
91
  return 'type';
89
- if (this.accept && !matchesAccept(file, this.accept))
92
+ // During dragenter preview, `accept` extension patterns can't be evaluated (no
93
+ // `.name` yet) — treat them as a possible match rather than a guaranteed reject,
94
+ // so the preview doesn't flag a file that will in fact be accepted on drop.
95
+ if (this.accept && !matchesAccept(file, this.accept, isPreview))
90
96
  return 'type';
91
97
  // `file.size` is `undefined` on the synthetic `DataTransferItem`-cast objects
92
98
  // used during dragenter preview (real sizes aren't available until drop),
@@ -95,14 +101,14 @@ export class LyraFileInput extends LyraElement {
95
101
  return 'size';
96
102
  return 'ok';
97
103
  }
98
- classify(fileList) {
104
+ classify(fileList, isPreview = false) {
99
105
  if (!this.multiple && fileList.length > 1) {
100
106
  return { files: [], rejected: fileList.map((file) => ({ file, reason: 'count' })) };
101
107
  }
102
108
  const files = [];
103
109
  const rejected = [];
104
110
  for (const f of fileList) {
105
- const reason = this.isAllowed(f);
111
+ const reason = this.isAllowed(f, isPreview);
106
112
  if (reason === 'ok')
107
113
  files.push(f);
108
114
  else
@@ -119,9 +125,16 @@ export class LyraFileInput extends LyraElement {
119
125
  this.inputEl?.click();
120
126
  }
121
127
  previewState(fileList) {
122
- const { rejected } = this.classify(fileList);
128
+ const { rejected } = this.classify(fileList, true);
123
129
  return rejected.length > 0 ? 'reject' : 'accept';
124
130
  }
131
+ statusText() {
132
+ if (this.dragState === 'accept')
133
+ return 'Release to add the file.';
134
+ if (this.dragState === 'reject')
135
+ return 'This file type is not accepted.';
136
+ return '';
137
+ }
125
138
  render() {
126
139
  return html `
127
140
  <div
@@ -129,6 +142,7 @@ export class LyraFileInput extends LyraElement {
129
142
  role="button"
130
143
  tabindex=${this.disabled ? '-1' : '0'}
131
144
  aria-disabled=${this.disabled ? 'true' : nothing}
145
+ aria-label=${this.label}
132
146
  data-drag-state=${this.dragState}
133
147
  @dragenter=${this.onDragEnter}
134
148
  @dragover=${this.onDragOver}
@@ -139,6 +153,7 @@ export class LyraFileInput extends LyraElement {
139
153
  >
140
154
  <slot>${this.label}</slot>
141
155
  </div>
156
+ <div part="status" class="sr-only" role="status" aria-live="polite">${this.statusText()}</div>
142
157
  <input
143
158
  part="input"
144
159
  type="file"
@@ -3,3 +3,5 @@ declare const meta: Meta;
3
3
  export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Default: Story;
6
+ export declare const CustomSlotContent: Story;
7
+ export declare const Disabled: Story;
@@ -8,3 +8,12 @@ export default meta;
8
8
  export const Default = {
9
9
  render: () => html `<lyra-file-input multiple accept=".csv,.xlsx"></lyra-file-input>`,
10
10
  };
11
+ export const CustomSlotContent = {
12
+ render: () => html `<lyra-file-input multiple accept=".csv,.xlsx" label="Upload spreadsheets">
13
+ <strong>Drag spreadsheets here</strong>
14
+ <span>or click to browse (.csv, .xlsx)</span>
15
+ </lyra-file-input>`,
16
+ };
17
+ export const Disabled = {
18
+ render: () => html `<lyra-file-input disabled accept=".csv,.xlsx"></lyra-file-input>`,
19
+ };
@@ -1,17 +1,48 @@
1
1
  import { type TemplateResult, type PropertyValues } from 'lit';
2
2
  import { LyraElement } from '../../internal/lyra-element.js';
3
3
  import '../skeleton/skeleton.js';
4
+ type FlagUrlResolver = (code: string, options?: {
5
+ variant?: 'detailed';
6
+ }) => Promise<string | undefined>;
7
+ /**
8
+ * Resolves the optional peer dependency `@aceshooting/lyra-flags`'s `flagUrl`
9
+ * via the given importer (a real dynamic import by default). Uncached and
10
+ * dependency-injectable — unlike `loadFlagUrlResolver()` below — so the
11
+ * caught-error warning path is directly testable without needing to
12
+ * actually uninstall the package.
13
+ */
14
+ export declare function loadFlagUrl(importFlags?: () => Promise<{
15
+ flagUrl: FlagUrlResolver;
16
+ }>): Promise<FlagUrlResolver | null>;
4
17
  /**
5
18
  * `<lyra-flag>` — a country/language flag.
6
19
  *
7
20
  * Flag images are shipped by the optional peer package `@aceshooting/lyra-flags`,
8
21
  * not bundled into lyra-ui itself, so importing the core library pulls zero flag
9
22
  * weight. Give it a `country` (ISO 3166-1 alpha-2) or a `language` tag (mapped to
10
- * a representative country).
23
+ * a representative country). While that peer package's `flagUrl()` resolves (or
24
+ * if it isn't installed), the host carries `aria-busy="true"` and a skeleton
25
+ * placeholder renders in its place.
26
+ *
27
+ * **Bundle-size note:** `country`/`language` resolve through the peer package's
28
+ * `flagUrl(code)`, which is itself genuinely code-split per flag (see that
29
+ * package's docs) — using `<lyra-flag country="fr">` anywhere in an app ships
30
+ * only the flags actually requested at runtime, not all 249. If you already
31
+ * have a flag's URL at build time (e.g. from your own literal
32
+ * `import frUrl from '@aceshooting/lyra-flags/flags/fr.svg?url'`), pass it as
33
+ * `src` instead to skip the peer-package round trip (and its loading-skeleton
34
+ * flash) entirely.
35
+ *
36
+ * A handful of flags (e.g. `es`, `pt` — any whose design includes a detailed coat of
37
+ * arms/seal/emblem) ship a second, full-detail source SVG alongside the default icon-optimized
38
+ * one; set `detailed` to request it (e.g. for a hero-scale display where the extra illustrative
39
+ * detail is actually visible). A no-op for every other code — see `detailed`'s own doc.
11
40
  *
12
41
  * @customElement lyra-flag
13
42
  * @example <lyra-flag country="fr"></lyra-flag>
14
43
  * @example <lyra-flag language="en" label="English"></lyra-flag>
44
+ * @example <lyra-flag src=${frUrl} label="French"></lyra-flag>
45
+ * @example <lyra-flag country="es" detailed></lyra-flag>
15
46
  * @csspart image - The underlying <img>.
16
47
  */
17
48
  export declare class LyraFlag extends LyraElement {
@@ -20,13 +51,47 @@ export declare class LyraFlag extends LyraElement {
20
51
  country?: string;
21
52
  /** BCP-47-ish language tag (e.g. `en`, `en-US`) resolved to a country flag. */
22
53
  language?: string;
23
- /** Accessible label / `alt` text. Defaults to the uppercase code. */
54
+ /**
55
+ * A pre-resolved flag image URL — takes precedence over `country`/`language`
56
+ * and skips the `@aceshooting/lyra-flags` peer-package lookup (and its
57
+ * loading-skeleton round trip) entirely. See the class doc: mainly useful to
58
+ * avoid even the small per-flag async hop when you already have the URL at
59
+ * build time. `label` is effectively required alongside `src` — there's no
60
+ * `country`/`language` to derive a fallback `alt` from.
61
+ */
62
+ src?: string;
63
+ /**
64
+ * Accessible label / `alt` text. Defaults to a localized, human-readable
65
+ * region name derived from the *resolved country code* via
66
+ * `Intl.DisplayNames` (e.g. `"United Kingdom"`) — for a `language`-only
67
+ * element (e.g. `language="en"`) that's the mapped country's display name,
68
+ * not the language tag itself. Falls back to the bare uppercase code if
69
+ * `Intl.DisplayNames` can't resolve it. Has no default when only `src` is
70
+ * given (no country/language to derive one from).
71
+ */
24
72
  label?: string;
25
73
  /** Render as a circular flag. */
26
74
  round: boolean;
27
- private src?;
75
+ /**
76
+ * Requests the pristine, full-detail source SVG instead of the default icon-optimized one —
77
+ * only meaningful for the minority of `country`/`language` codes whose source art was large
78
+ * enough to need optimizing (e.g. `es`, `pt`, a national coat of arms/seal/emblem); for every
79
+ * other code this is a safe no-op (the default and detailed variants are the same file). Has no
80
+ * effect when `src` is set — a pre-resolved URL is used as-is regardless. Intended for rendering
81
+ * a flag larger than icon scale (e.g. a hero display) where the extra illustrative detail is
82
+ * actually visible.
83
+ */
84
+ detailed: boolean;
85
+ private resolvedSrc?;
28
86
  /** True while the lazy-loaded `@aceshooting/lyra-flags` peer resolver is in flight. */
29
87
  private loading;
88
+ /**
89
+ * Bumped on every `willUpdate` pass; captured by each in-flight resolver
90
+ * `.then()` so a resolution for a `country`/`language` that's since changed
91
+ * (or been cleared) can recognize itself as stale and no-op instead of
92
+ * overwriting newer state.
93
+ */
94
+ private resolveToken;
30
95
  private get code();
31
96
  protected willUpdate(changed: PropertyValues<this>): void;
32
97
  protected updated(): void;
@@ -37,3 +102,4 @@ declare global {
37
102
  'lyra-flag': LyraFlag;
38
103
  }
39
104
  }
105
+ export {};