@aceshooting/lyra-ui 0.1.1 → 0.1.2

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 (117) hide show
  1. package/README.md +77 -6
  2. package/custom-elements.json +12015 -0
  3. package/dist/components/chart/bar-chart.d.ts +14 -0
  4. package/dist/components/chart/bar-chart.js +14 -0
  5. package/dist/components/chart/box-plot.d.ts +46 -0
  6. package/dist/components/chart/box-plot.js +145 -0
  7. package/dist/components/chart/box-plot.styles.d.ts +1 -0
  8. package/dist/components/chart/box-plot.styles.js +22 -0
  9. package/dist/components/chart/bubble-chart.d.ts +19 -0
  10. package/dist/components/chart/bubble-chart.js +18 -0
  11. package/dist/components/chart/chart-loader.d.ts +8 -0
  12. package/dist/components/chart/chart-loader.js +26 -0
  13. package/dist/components/chart/chart.d.ts +91 -0
  14. package/dist/components/chart/chart.js +275 -0
  15. package/dist/components/chart/chart.styles.d.ts +1 -0
  16. package/dist/components/chart/chart.styles.js +33 -0
  17. package/dist/components/chart/doughnut-chart.d.ts +15 -0
  18. package/dist/components/chart/doughnut-chart.js +14 -0
  19. package/dist/components/chart/histogram-bin.d.ts +6 -0
  20. package/dist/components/chart/histogram-bin.js +19 -0
  21. package/dist/components/chart/histogram.d.ts +20 -0
  22. package/dist/components/chart/histogram.js +70 -0
  23. package/dist/components/chart/histogram.styles.d.ts +1 -0
  24. package/dist/components/chart/histogram.styles.js +1 -0
  25. package/dist/components/chart/line-chart.d.ts +14 -0
  26. package/dist/components/chart/line-chart.js +14 -0
  27. package/dist/components/chart/pie-chart.d.ts +16 -0
  28. package/dist/components/chart/pie-chart.js +15 -0
  29. package/dist/components/chart/polar-area-chart.d.ts +15 -0
  30. package/dist/components/chart/polar-area-chart.js +14 -0
  31. package/dist/components/chart/radar-chart.d.ts +15 -0
  32. package/dist/components/chart/radar-chart.js +14 -0
  33. package/dist/components/chart/scatter-chart.d.ts +15 -0
  34. package/dist/components/chart/scatter-chart.js +15 -0
  35. package/dist/components/combobox/combobox.d.ts +16 -0
  36. package/dist/components/combobox/combobox.js +113 -6
  37. package/dist/components/combobox/combobox.styles.js +68 -7
  38. package/dist/components/combobox/option.d.ts +2 -2
  39. package/dist/components/combobox/option.js +11 -7
  40. package/dist/components/date-picker/date-input.d.ts +12 -1
  41. package/dist/components/date-picker/date-input.js +68 -6
  42. package/dist/components/date-picker/date-input.styles.js +58 -6
  43. package/dist/components/date-picker/date-picker.js +5 -2
  44. package/dist/components/date-picker/date-picker.styles.js +9 -2
  45. package/dist/components/empty/empty.d.ts +1 -0
  46. package/dist/components/empty/empty.js +14 -2
  47. package/dist/components/export-button/csv.d.ts +10 -0
  48. package/dist/components/export-button/csv.js +28 -0
  49. package/dist/components/export-button/export-button.d.ts +41 -0
  50. package/dist/components/export-button/export-button.js +151 -0
  51. package/dist/components/export-button/export-button.styles.d.ts +1 -0
  52. package/dist/components/export-button/export-button.styles.js +79 -0
  53. package/dist/components/file-input/accept.d.ts +14 -0
  54. package/dist/components/file-input/accept.js +30 -0
  55. package/dist/components/file-input/file-input.d.ts +47 -0
  56. package/dist/components/file-input/file-input.js +182 -0
  57. package/dist/components/file-input/file-input.styles.d.ts +1 -0
  58. package/dist/components/file-input/file-input.styles.js +44 -0
  59. package/dist/components/flag/flag.d.ts +4 -0
  60. package/dist/components/flag/flag.js +24 -1
  61. package/dist/components/flag/flag.styles.js +1 -1
  62. package/dist/components/gauge/gauge.d.ts +29 -0
  63. package/dist/components/gauge/gauge.js +133 -0
  64. package/dist/components/gauge/gauge.styles.d.ts +1 -0
  65. package/dist/components/gauge/gauge.styles.js +60 -0
  66. package/dist/components/graph/graph.d.ts +68 -0
  67. package/dist/components/graph/graph.js +276 -0
  68. package/dist/components/graph/graph.styles.d.ts +1 -0
  69. package/dist/components/graph/graph.styles.js +34 -0
  70. package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
  71. package/dist/components/heatmap/heatmap-scale.js +17 -0
  72. package/dist/components/heatmap/heatmap.d.ts +66 -0
  73. package/dist/components/heatmap/heatmap.js +252 -0
  74. package/dist/components/heatmap/heatmap.styles.d.ts +1 -0
  75. package/dist/components/heatmap/heatmap.styles.js +39 -0
  76. package/dist/components/map/map-loader.d.ts +8 -0
  77. package/dist/components/map/map-loader.js +17 -0
  78. package/dist/components/map/map.d.ts +49 -0
  79. package/dist/components/map/map.js +189 -0
  80. package/dist/components/map/map.styles.d.ts +1 -0
  81. package/dist/components/map/map.styles.js +48 -0
  82. package/dist/components/playback/playback.d.ts +51 -0
  83. package/dist/components/playback/playback.js +146 -0
  84. package/dist/components/playback/playback.styles.d.ts +1 -0
  85. package/dist/components/playback/playback.styles.js +40 -0
  86. package/dist/components/split/split.d.ts +41 -0
  87. package/dist/components/split/split.js +171 -0
  88. package/dist/components/split/split.styles.d.ts +1 -0
  89. package/dist/components/split/split.styles.js +51 -0
  90. package/dist/components/stat/stat.d.ts +9 -0
  91. package/dist/components/stat/stat.js +52 -7
  92. package/dist/components/stat/stat.styles.js +15 -5
  93. package/dist/components/table/table.d.ts +18 -6
  94. package/dist/components/table/table.js +76 -24
  95. package/dist/components/table/table.styles.js +32 -0
  96. package/dist/components/time-range/time-range.d.ts +40 -0
  97. package/dist/components/time-range/time-range.js +219 -0
  98. package/dist/components/time-range/time-range.styles.d.ts +1 -0
  99. package/dist/components/time-range/time-range.styles.js +78 -0
  100. package/dist/components/toast/toast-item.js +10 -2
  101. package/dist/components/toast/toast-item.styles.js +13 -4
  102. package/dist/components/toast/toast.styles.js +0 -1
  103. package/dist/components/tree/tree-node.d.ts +50 -0
  104. package/dist/components/tree/tree-node.js +196 -0
  105. package/dist/components/tree/tree.d.ts +65 -0
  106. package/dist/components/tree/tree.js +227 -0
  107. package/dist/components/tree/tree.styles.d.ts +1 -0
  108. package/dist/components/tree/tree.styles.js +13 -0
  109. package/dist/internal/form-associated.js +49 -1
  110. package/dist/internal/icons.d.ts +11 -0
  111. package/dist/internal/icons.js +64 -0
  112. package/dist/internal/tokens.styles.js +41 -1
  113. package/dist/lyra.d.ts +67 -0
  114. package/dist/lyra.js +55 -0
  115. package/llms-full.txt +1425 -0
  116. package/llms.txt +47 -0
  117. package/package.json +53 -4
@@ -9,9 +9,19 @@ export const styles = css `
9
9
  font-size: 0.875rem;
10
10
  font-weight: 600;
11
11
  }
12
- [part='form-control-label']:empty {
12
+ /* :empty never matches here -- the part always contains a literal slot
13
+ child element regardless of assigned/text content -- so real emptiness
14
+ is tracked in JS (hasLabelSlot) and reflected via the hidden attribute
15
+ instead (same fix as [part='hint']/[part='error'] below). Without this,
16
+ the required-asterisk ::after below (which attaches to this box)
17
+ renders a stray ' *' with nothing before it whenever label is unset. */
18
+ [part='form-control-label'][hidden] {
13
19
  display: none;
14
20
  }
21
+ :host([required]) [part='form-control-label']::after {
22
+ content: ' *';
23
+ color: var(--lyra-color-danger);
24
+ }
15
25
  [part='input-wrapper'] {
16
26
  display: flex;
17
27
  align-items: center;
@@ -26,7 +36,8 @@ export const styles = css `
26
36
  border-color: var(--lyra-color-brand);
27
37
  }
28
38
  :host([disabled]) [part='input-wrapper'] {
29
- opacity: 0.5;
39
+ opacity: var(--lyra-opacity-disabled);
40
+ cursor: not-allowed;
30
41
  }
31
42
  [part='input'] {
32
43
  flex: 1 1 auto;
@@ -40,28 +51,69 @@ export const styles = css `
40
51
  [part='clear-button'],
41
52
  [part='expand-button'] {
42
53
  flex: 0 0 auto;
54
+ display: inline-flex;
55
+ align-items: center;
56
+ justify-content: center;
43
57
  border: none;
44
58
  background: none;
45
59
  cursor: pointer;
46
60
  color: var(--lyra-color-text-quiet);
47
- padding: 0 var(--lyra-space-xs);
61
+ padding: var(--lyra-space-xs);
62
+ /* Real touch target in *both* dimensions (WCAG 2.2 SC 2.5.8 needs
63
+ 24x24 CSS px, not just height — a design-review fix that only set
64
+ min-block-size left these buttons 24px tall but narrower than that).
65
+ The row has no explicit min-block-size of its own (unlike combobox's
66
+ [part=combobox]), so it can grow to fit the full touch target. */
67
+ min-inline-size: var(--lyra-icon-button-size);
68
+ min-block-size: var(--lyra-icon-button-size);
48
69
  line-height: 1;
49
70
  font-size: 1rem;
50
71
  }
72
+ [part='clear-button']:focus-visible,
73
+ [part='expand-button']:focus-visible {
74
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
75
+ outline-offset: var(--lyra-focus-ring-offset);
76
+ }
51
77
  [part='popup'] {
52
- display: none;
53
78
  position: fixed;
54
79
  z-index: 900;
80
+ max-inline-size: min(92vw, 28rem);
81
+ visibility: hidden;
82
+ opacity: 0;
83
+ transform: translateY(-0.25rem);
84
+ transition:
85
+ opacity var(--lyra-transition-fast),
86
+ transform var(--lyra-transition-fast),
87
+ visibility var(--lyra-transition-fast);
55
88
  }
56
89
  :host([open]) [part='popup'] {
57
- display: block;
90
+ visibility: visible;
91
+ opacity: 1;
92
+ transform: translateY(0);
93
+ }
94
+ @media (prefers-reduced-motion: reduce) {
95
+ [part='popup'] {
96
+ transition: none !important;
97
+ }
58
98
  }
59
99
  [part='hint'] {
60
100
  margin-block-start: var(--lyra-space-xs);
61
101
  font-size: 0.8125rem;
62
102
  color: var(--lyra-color-text-quiet);
63
103
  }
64
- [part='hint']:empty {
104
+ /* :empty never matches here -- the part always contains a literal
105
+ slot child element regardless of assigned/text content -- so real
106
+ emptiness is tracked in JS (hasHintSlot/hasErrorSlot) and reflected via
107
+ the hidden attribute instead (same fix as lyra-stat's icon/caption). */
108
+ [part='hint'][hidden] {
109
+ display: none;
110
+ }
111
+ [part='error'] {
112
+ margin-block-start: var(--lyra-space-xs);
113
+ font-size: 0.8125rem;
114
+ color: var(--lyra-color-danger);
115
+ }
116
+ [part='error'][hidden] {
65
117
  display: none;
66
118
  }
67
119
  `;
@@ -8,6 +8,7 @@ import { html } from 'lit';
8
8
  import { property, state } from 'lit/decorators.js';
9
9
  import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { defineElement } from '../../internal/prefix.js';
11
+ import { chevronIcon } from '../../internal/icons.js';
11
12
  import { styles } from './date-picker.styles.js';
12
13
  import { monthMatrix, weekdayLabels, monthTitle, formatISO, parseISO, isSameDay, addMonths, resolveFirstDayOfWeek, } from './calendar-core.js';
13
14
  /**
@@ -241,12 +242,14 @@ export class LyraDatePicker extends LyraElement {
241
242
  <div part="header">
242
243
  ${isFirst
243
244
  ? html `<button part="previous" type="button" aria-label="Previous month" @click=${() => this.nav(-1)}>
244
-
245
+ ${chevronIcon()}
245
246
  </button>`
246
247
  : html `<span></span>`}
247
248
  <div part="title">${monthTitle(year, month, this.locale)}</div>
248
249
  ${isLast
249
- ? html `<button part="next" type="button" aria-label="Next month" @click=${() => this.nav(1)}>›</button>`
250
+ ? html `<button part="next" type="button" aria-label="Next month" @click=${() => this.nav(1)}>
251
+ ${chevronIcon()}
252
+ </button>`
250
253
  : html `<span></span>`}
251
254
  </div>
252
255
  <div part="weekdays">${labels.map((l) => html `<span part="weekday">${l}</span>`)}</div>
@@ -38,6 +38,9 @@ export const styles = css `
38
38
  [part='next']:hover {
39
39
  background: var(--lyra-color-brand-quiet);
40
40
  }
41
+ [part='previous'] svg {
42
+ transform: rotate(180deg);
43
+ }
41
44
  [part='weekdays'] {
42
45
  display: grid;
43
46
  grid-template-columns: repeat(7, var(--lyra-cell-size));
@@ -71,6 +74,10 @@ export const styles = css `
71
74
  [part~='day-outside'] {
72
75
  color: var(--lyra-color-text-quiet);
73
76
  }
77
+ [part~='day-outside'][part~='day-range-inner'],
78
+ [part~='day-outside'][part~='day-range-inner']:hover {
79
+ color: var(--lyra-color-text);
80
+ }
74
81
  [part='day-placeholder'] {
75
82
  inline-size: var(--lyra-cell-size);
76
83
  block-size: var(--lyra-cell-size);
@@ -95,7 +102,7 @@ export const styles = css `
95
102
  cursor: not-allowed;
96
103
  }
97
104
  [part~='day']:focus-visible {
98
- outline: 2px solid var(--lyra-color-brand);
99
- outline-offset: 1px;
105
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
106
+ outline-offset: var(--lyra-focus-ring-offset);
100
107
  }
101
108
  `;
@@ -17,6 +17,7 @@ export declare class LyraEmpty extends LyraElement {
17
17
  description: string;
18
18
  private hasIcon;
19
19
  private hasActions;
20
+ protected willUpdate(): void;
20
21
  firstUpdated(): void;
21
22
  private checkIconSlot;
22
23
  private checkActionsSlot;
@@ -38,9 +38,21 @@ export class LyraEmpty extends LyraElement {
38
38
  };
39
39
  }
40
40
  static { this.styles = [LyraElement.styles, styles]; }
41
+ willUpdate() {
42
+ // Set from light-DOM children before the first render so the initial
43
+ // paint is already correct — setting `hasIcon`/`hasActions` from
44
+ // `firstUpdated` (after the update completes) would schedule a second,
45
+ // wasted update (Lit's dev-mode "change-in-update" warning).
46
+ if (!this.hasUpdated) {
47
+ this.hasIcon = Array.from(this.children).some((el) => !el.hasAttribute('slot'));
48
+ this.hasActions = Array.from(this.children).some((el) => el.getAttribute('slot') === 'actions');
49
+ }
50
+ }
41
51
  firstUpdated() {
42
- // `slotchange` isn't guaranteed to fire for content already present at
43
- // parse/upgrade time in every browser, so do an initial check here too.
52
+ // Fallback reconciliation against the fully-resolved slot assignment
53
+ // (handles slot-forwarding and any browser where `slotchange` doesn't
54
+ // fire for content present at parse/upgrade time). A no-op in the
55
+ // common case since `willUpdate` already set the correct value above.
44
56
  this.checkIconSlot(this.shadowRoot.querySelector('slot:not([name])'));
45
57
  this.checkActionsSlot(this.shadowRoot.querySelector('slot[name="actions"]'));
46
58
  }
@@ -0,0 +1,10 @@
1
+ /** Escapes a CSV field: quotes as needed, guards against formula injection. */
2
+ export declare function escapeCsvField(value: unknown): string;
3
+ export interface CsvColumn {
4
+ key: string;
5
+ label: string;
6
+ }
7
+ /** Builds a CRLF-joined CSV string with a header row. */
8
+ export declare function buildCsv(rows: Record<string, unknown>[], columns: CsvColumn[]): string;
9
+ /** Triggers a browser download of `content` as `filename`. */
10
+ export declare function downloadBlob(content: string, filename: string, mime: string): void;
@@ -0,0 +1,28 @@
1
+ const UNSAFE_LEADING = /^[=+\-@\t\r]/;
2
+ const NEEDS_QUOTING = /[",\n]/;
3
+ /** Escapes a CSV field: quotes as needed, guards against formula injection. */
4
+ export function escapeCsvField(value) {
5
+ let s = value == null ? '' : String(value);
6
+ if (UNSAFE_LEADING.test(s))
7
+ s = `'${s}`;
8
+ if (NEEDS_QUOTING.test(s))
9
+ s = `"${s.replace(/"/g, '""')}"`;
10
+ return s;
11
+ }
12
+ /** Builds a CRLF-joined CSV string with a header row. */
13
+ export function buildCsv(rows, columns) {
14
+ const header = columns.map((c) => escapeCsvField(c.label)).join(',');
15
+ const body = rows.map((row) => columns.map((c) => escapeCsvField(row[c.key])).join(','));
16
+ return [header, ...body].join('\r\n');
17
+ }
18
+ /** Triggers a browser download of `content` as `filename`. */
19
+ export function downloadBlob(content, filename, mime) {
20
+ const blob = new Blob([content], { type: mime });
21
+ const url = URL.createObjectURL(blob);
22
+ const a = document.createElement('a');
23
+ a.href = url;
24
+ a.download = filename;
25
+ a.click();
26
+ // Deferred revoke: Safari can cancel the download if the URL is revoked immediately.
27
+ setTimeout(() => URL.revokeObjectURL(url), 5000);
28
+ }
@@ -0,0 +1,41 @@
1
+ import { type TemplateResult, type PropertyValues } from 'lit';
2
+ import { LyraElement } from '../../internal/lyra-element.js';
3
+ import { type CsvColumn } from './csv.js';
4
+ export type ExportFormat = 'csv' | 'json';
5
+ /**
6
+ * `<lyra-export-button>` — a CSV/JSON download button, single-format or a
7
+ * format-choice menu. First-party invention; consolidates ad-hoc "export CSV"
8
+ * buttons duplicated across surveyed repos.
9
+ *
10
+ * @customElement lyra-export-button
11
+ * @event lyra-export - `detail: { format }`, cancelable — call `preventDefault()`
12
+ * to substitute the built-in client-side download with a server-generated one.
13
+ * @event lyra-export-complete - Fired after a non-cancelled download completes.
14
+ * @csspart trigger, menu, menu-item
15
+ */
16
+ export declare class LyraExportButton extends LyraElement {
17
+ static styles: import("lit").CSSResultGroup[];
18
+ rows: Record<string, unknown>[];
19
+ columns: CsvColumn[];
20
+ filename: string;
21
+ formats: ExportFormat[];
22
+ disabled: boolean;
23
+ label: string;
24
+ open: boolean;
25
+ private cleanup?;
26
+ disconnectedCallback(): void;
27
+ private onDocPointer;
28
+ private openMenu;
29
+ private closeMenu;
30
+ private onKeyDown;
31
+ protected firstUpdated(): void;
32
+ protected updated(changed: PropertyValues): void;
33
+ private doExport;
34
+ private onTriggerClick;
35
+ render(): TemplateResult;
36
+ }
37
+ declare global {
38
+ interface HTMLElementTagNameMap {
39
+ 'lyra-export-button': LyraExportButton;
40
+ }
41
+ }
@@ -0,0 +1,151 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { html, nothing } from 'lit';
8
+ import { property } from 'lit/decorators.js';
9
+ import { LyraElement } from '../../internal/lyra-element.js';
10
+ import { defineElement } from '../../internal/prefix.js';
11
+ import { place } from '../../internal/positioner.js';
12
+ import { buildCsv, downloadBlob } from './csv.js';
13
+ import { styles } from './export-button.styles.js';
14
+ /**
15
+ * `<lyra-export-button>` — a CSV/JSON download button, single-format or a
16
+ * format-choice menu. First-party invention; consolidates ad-hoc "export CSV"
17
+ * buttons duplicated across surveyed repos.
18
+ *
19
+ * @customElement lyra-export-button
20
+ * @event lyra-export - `detail: { format }`, cancelable — call `preventDefault()`
21
+ * to substitute the built-in client-side download with a server-generated one.
22
+ * @event lyra-export-complete - Fired after a non-cancelled download completes.
23
+ * @csspart trigger, menu, menu-item
24
+ */
25
+ export class LyraExportButton extends LyraElement {
26
+ constructor() {
27
+ super(...arguments);
28
+ this.rows = [];
29
+ this.columns = [];
30
+ this.filename = 'export';
31
+ this.formats = ['csv'];
32
+ this.disabled = false;
33
+ this.label = 'Export';
34
+ this.open = false;
35
+ this.onDocPointer = (e) => {
36
+ if (!e.composedPath().includes(this))
37
+ this.closeMenu();
38
+ };
39
+ this.onKeyDown = (e) => {
40
+ if (e.key === 'Escape' && this.open) {
41
+ e.preventDefault();
42
+ this.closeMenu();
43
+ this.renderRoot.querySelector('[part="trigger"]')?.focus();
44
+ }
45
+ };
46
+ }
47
+ static { this.styles = [LyraElement.styles, styles]; }
48
+ disconnectedCallback() {
49
+ super.disconnectedCallback();
50
+ this.cleanup?.();
51
+ document.removeEventListener('pointerdown', this.onDocPointer);
52
+ }
53
+ openMenu() {
54
+ if (this.open)
55
+ return;
56
+ this.open = true;
57
+ document.addEventListener('pointerdown', this.onDocPointer);
58
+ }
59
+ closeMenu() {
60
+ if (!this.open)
61
+ return;
62
+ this.open = false;
63
+ document.removeEventListener('pointerdown', this.onDocPointer);
64
+ }
65
+ firstUpdated() {
66
+ // Single delegated listener catches Escape from the trigger button or
67
+ // any menu-item inside this shadow root.
68
+ this.renderRoot.addEventListener('keydown', this.onKeyDown);
69
+ }
70
+ updated(changed) {
71
+ if (changed.has('open')) {
72
+ this.cleanup?.();
73
+ this.cleanup = undefined;
74
+ if (this.open) {
75
+ const anchor = this.renderRoot.querySelector('[part="trigger"]');
76
+ const menu = this.renderRoot.querySelector('[part="menu"]');
77
+ if (anchor && menu)
78
+ this.cleanup = place(anchor, menu);
79
+ }
80
+ }
81
+ }
82
+ doExport(format) {
83
+ this.closeMenu();
84
+ const ev = this.emit('lyra-export', { format });
85
+ if (ev.defaultPrevented)
86
+ return;
87
+ if (format === 'csv') {
88
+ downloadBlob(buildCsv(this.rows, this.columns), `${this.filename}.csv`, 'text/csv;charset=utf-8;');
89
+ }
90
+ else {
91
+ downloadBlob(JSON.stringify(this.rows, null, 2), `${this.filename}.json`, 'application/json');
92
+ }
93
+ this.emit('lyra-export-complete', { format });
94
+ }
95
+ onTriggerClick() {
96
+ if (this.disabled)
97
+ return;
98
+ if (this.formats.length <= 1)
99
+ this.doExport(this.formats[0] ?? 'csv');
100
+ else
101
+ this.open ? this.closeMenu() : this.openMenu();
102
+ }
103
+ render() {
104
+ return html `
105
+ <button
106
+ part="trigger"
107
+ type="button"
108
+ ?disabled=${this.disabled}
109
+ aria-haspopup=${this.formats.length > 1 ? 'menu' : nothing}
110
+ aria-expanded=${this.formats.length > 1 ? (this.open ? 'true' : 'false') : nothing}
111
+ @click=${() => this.onTriggerClick()}
112
+ >
113
+ ${this.label}
114
+ </button>
115
+ ${this.formats.length > 1
116
+ ? html `<div part="menu" role="menu">
117
+ ${this.formats.map((f) => html `<button
118
+ part="menu-item"
119
+ role="menuitem"
120
+ type="button"
121
+ @click=${() => this.doExport(f)}
122
+ >
123
+ ${f.toUpperCase()}
124
+ </button>`)}
125
+ </div>`
126
+ : nothing}
127
+ `;
128
+ }
129
+ }
130
+ __decorate([
131
+ property({ attribute: false })
132
+ ], LyraExportButton.prototype, "rows", void 0);
133
+ __decorate([
134
+ property({ attribute: false })
135
+ ], LyraExportButton.prototype, "columns", void 0);
136
+ __decorate([
137
+ property()
138
+ ], LyraExportButton.prototype, "filename", void 0);
139
+ __decorate([
140
+ property({ attribute: false })
141
+ ], LyraExportButton.prototype, "formats", void 0);
142
+ __decorate([
143
+ property({ type: Boolean, reflect: true })
144
+ ], LyraExportButton.prototype, "disabled", void 0);
145
+ __decorate([
146
+ property()
147
+ ], LyraExportButton.prototype, "label", void 0);
148
+ __decorate([
149
+ property({ type: Boolean, reflect: true })
150
+ ], LyraExportButton.prototype, "open", void 0);
151
+ defineElement('export-button', LyraExportButton);
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;
@@ -0,0 +1,79 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `
3
+ :host {
4
+ display: inline-block;
5
+ position: relative;
6
+ }
7
+ [part='trigger'] {
8
+ display: inline-flex;
9
+ align-items: center;
10
+ gap: var(--lyra-space-xs);
11
+ padding: var(--lyra-space-xs) var(--lyra-space-m);
12
+ border: 1px solid var(--lyra-color-border);
13
+ border-radius: var(--lyra-radius);
14
+ background: var(--lyra-color-surface);
15
+ color: var(--lyra-color-text);
16
+ font: inherit;
17
+ cursor: pointer;
18
+ }
19
+ [part='trigger']:hover:not(:disabled) {
20
+ border-color: var(--lyra-color-brand);
21
+ }
22
+ [part='trigger']:disabled {
23
+ opacity: var(--lyra-opacity-disabled);
24
+ cursor: not-allowed;
25
+ }
26
+ [part='trigger']:focus-visible {
27
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
28
+ outline-offset: var(--lyra-focus-ring-offset);
29
+ }
30
+ [part='menu'] {
31
+ /* Closed state: invisible + slightly raised. visibility (not
32
+ display:none) so opacity/transform can actually transition; hit-testing
33
+ and a11y exposure stay off since this part is already position:fixed. */
34
+ visibility: hidden;
35
+ position: fixed;
36
+ z-index: 900;
37
+ min-inline-size: 8rem;
38
+ padding: var(--lyra-space-xs);
39
+ background: var(--lyra-color-surface);
40
+ border: 1px solid var(--lyra-color-border);
41
+ border-radius: var(--lyra-radius);
42
+ box-shadow: var(--lyra-shadow);
43
+ opacity: 0;
44
+ transform: translateY(-4px);
45
+ transition:
46
+ opacity var(--lyra-transition-fast),
47
+ transform var(--lyra-transition-fast),
48
+ visibility var(--lyra-transition-fast);
49
+ }
50
+ :host([open]) [part='menu'] {
51
+ visibility: visible;
52
+ opacity: 1;
53
+ transform: none;
54
+ }
55
+ @media (prefers-reduced-motion: reduce) {
56
+ [part='menu'] {
57
+ transition: none;
58
+ }
59
+ }
60
+ [part='menu-item'] {
61
+ display: block;
62
+ inline-size: 100%;
63
+ text-align: start;
64
+ padding: var(--lyra-space-xs) var(--lyra-space-s);
65
+ border: none;
66
+ background: none;
67
+ color: inherit;
68
+ font: inherit;
69
+ cursor: pointer;
70
+ border-radius: var(--lyra-radius);
71
+ }
72
+ [part='menu-item']:hover {
73
+ background: var(--lyra-color-brand-quiet);
74
+ }
75
+ [part='menu-item']:focus-visible {
76
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
77
+ outline-offset: var(--lyra-focus-ring-offset);
78
+ }
79
+ `;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Parses a native-`accept`-style string (`".csv,.xlsx"`, `"text/csv"`,
3
+ * `"image/*"`, or any mix, comma-separated) and reports whether `file`
4
+ * matches it — the same three forms the browser's own file picker accepts,
5
+ * now also enforced on the drag-drop path (2026-07-10 audit,
6
+ * "map-file-input" §lyra-file-input, High: previously `accept` only
7
+ * constrained the native picker dialog and had no effect on drop).
8
+ *
9
+ * `file` may be a `DataTransferItem` cast as `File` (the dragenter-preview
10
+ * call site, before the drop payload has real `File` objects) - unlike
11
+ * `File`, `DataTransferItem` has no `.name`, only `.type`, so extension
12
+ * patterns (`.csv`) can't be evaluated pre-drop and never match.
13
+ */
14
+ export declare function matchesAccept(file: File, accept: string): boolean;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Parses a native-`accept`-style string (`".csv,.xlsx"`, `"text/csv"`,
3
+ * `"image/*"`, or any mix, comma-separated) and reports whether `file`
4
+ * matches it — the same three forms the browser's own file picker accepts,
5
+ * now also enforced on the drag-drop path (2026-07-10 audit,
6
+ * "map-file-input" §lyra-file-input, High: previously `accept` only
7
+ * constrained the native picker dialog and had no effect on drop).
8
+ *
9
+ * `file` may be a `DataTransferItem` cast as `File` (the dragenter-preview
10
+ * call site, before the drop payload has real `File` objects) - unlike
11
+ * `File`, `DataTransferItem` has no `.name`, only `.type`, so extension
12
+ * patterns (`.csv`) can't be evaluated pre-drop and never match.
13
+ */
14
+ export function matchesAccept(file, accept) {
15
+ const parts = accept
16
+ .split(',')
17
+ .map((s) => s.trim().toLowerCase())
18
+ .filter(Boolean);
19
+ if (parts.length === 0)
20
+ return true;
21
+ const name = file.name?.toLowerCase();
22
+ const type = (file.type || '').toLowerCase();
23
+ return parts.some((p) => {
24
+ if (p.startsWith('.'))
25
+ return name !== undefined && name.endsWith(p);
26
+ if (p.endsWith('/*'))
27
+ return type.startsWith(p.slice(0, -1));
28
+ return type === p;
29
+ });
30
+ }
@@ -0,0 +1,47 @@
1
+ import { type TemplateResult } from 'lit';
2
+ import { LyraElement } from '../../internal/lyra-element.js';
3
+ export interface RejectedFile {
4
+ file: File;
5
+ reason: 'type' | 'count' | 'size';
6
+ }
7
+ /**
8
+ * `<lyra-file-input>` — a drag-drop + click-to-browse file dropzone. Emits
9
+ * raw `File[]`; parsing (CSV/XLSX/etc.) is left to the host, matching every
10
+ * surveyed upload seed (all of which upload raw files to a backend).
11
+ *
12
+ * @customElement lyra-file-input
13
+ * @slot - Custom drop-zone content, overrides the `label` attribute.
14
+ * @event lyra-files - `detail: { files, rejected }`, fired on drop and manual selection.
15
+ * @csspart base, input
16
+ */
17
+ export declare class LyraFileInput extends LyraElement {
18
+ static styles: import("lit").CSSResultGroup[];
19
+ multiple: boolean;
20
+ disabled: boolean;
21
+ accept: string;
22
+ allowedMimeTypes: string[];
23
+ forbiddenMimeTypes: string[];
24
+ maxFileSize: number;
25
+ label: string;
26
+ private dragState;
27
+ private inputEl?;
28
+ private dragCounter;
29
+ private isAllowed;
30
+ private classify;
31
+ private emitFiles;
32
+ /** Programmatically open the native file picker. */
33
+ openPicker(): void;
34
+ private previewState;
35
+ private onDragEnter;
36
+ private onDragOver;
37
+ private onDragLeave;
38
+ private onDrop;
39
+ private onInputChange;
40
+ private onKeyDown;
41
+ render(): TemplateResult;
42
+ }
43
+ declare global {
44
+ interface HTMLElementTagNameMap {
45
+ 'lyra-file-input': LyraFileInput;
46
+ }
47
+ }