@aceshooting/lyra-ui 0.1.0 → 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 (125) 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 +32 -0
  46. package/dist/components/empty/empty.js +90 -0
  47. package/dist/components/empty/empty.styles.d.ts +1 -0
  48. package/dist/components/empty/empty.styles.js +42 -0
  49. package/dist/components/export-button/csv.d.ts +10 -0
  50. package/dist/components/export-button/csv.js +28 -0
  51. package/dist/components/export-button/export-button.d.ts +41 -0
  52. package/dist/components/export-button/export-button.js +151 -0
  53. package/dist/components/export-button/export-button.styles.d.ts +1 -0
  54. package/dist/components/export-button/export-button.styles.js +79 -0
  55. package/dist/components/file-input/accept.d.ts +14 -0
  56. package/dist/components/file-input/accept.js +30 -0
  57. package/dist/components/file-input/file-input.d.ts +47 -0
  58. package/dist/components/file-input/file-input.js +182 -0
  59. package/dist/components/file-input/file-input.styles.d.ts +1 -0
  60. package/dist/components/file-input/file-input.styles.js +44 -0
  61. package/dist/components/flag/flag.d.ts +4 -0
  62. package/dist/components/flag/flag.js +24 -1
  63. package/dist/components/flag/flag.styles.js +1 -1
  64. package/dist/components/gauge/gauge.d.ts +29 -0
  65. package/dist/components/gauge/gauge.js +133 -0
  66. package/dist/components/gauge/gauge.styles.d.ts +1 -0
  67. package/dist/components/gauge/gauge.styles.js +60 -0
  68. package/dist/components/graph/graph.d.ts +68 -0
  69. package/dist/components/graph/graph.js +276 -0
  70. package/dist/components/graph/graph.styles.d.ts +1 -0
  71. package/dist/components/graph/graph.styles.js +34 -0
  72. package/dist/components/heatmap/heatmap-scale.d.ts +8 -0
  73. package/dist/components/heatmap/heatmap-scale.js +17 -0
  74. package/dist/components/heatmap/heatmap.d.ts +66 -0
  75. package/dist/components/heatmap/heatmap.js +252 -0
  76. package/dist/components/heatmap/heatmap.styles.d.ts +1 -0
  77. package/dist/components/heatmap/heatmap.styles.js +39 -0
  78. package/dist/components/map/map-loader.d.ts +8 -0
  79. package/dist/components/map/map-loader.js +17 -0
  80. package/dist/components/map/map.d.ts +49 -0
  81. package/dist/components/map/map.js +189 -0
  82. package/dist/components/map/map.styles.d.ts +1 -0
  83. package/dist/components/map/map.styles.js +48 -0
  84. package/dist/components/playback/playback.d.ts +51 -0
  85. package/dist/components/playback/playback.js +146 -0
  86. package/dist/components/playback/playback.styles.d.ts +1 -0
  87. package/dist/components/playback/playback.styles.js +40 -0
  88. package/dist/components/skeleton/skeleton.d.ts +26 -0
  89. package/dist/components/skeleton/skeleton.js +65 -0
  90. package/dist/components/skeleton/skeleton.styles.d.ts +1 -0
  91. package/dist/components/skeleton/skeleton.styles.js +57 -0
  92. package/dist/components/split/split.d.ts +41 -0
  93. package/dist/components/split/split.js +171 -0
  94. package/dist/components/split/split.styles.d.ts +1 -0
  95. package/dist/components/split/split.styles.js +51 -0
  96. package/dist/components/stat/stat.d.ts +36 -0
  97. package/dist/components/stat/stat.js +117 -0
  98. package/dist/components/stat/stat.styles.d.ts +1 -0
  99. package/dist/components/stat/stat.styles.js +86 -0
  100. package/dist/components/table/table.d.ts +54 -0
  101. package/dist/components/table/table.js +201 -0
  102. package/dist/components/table/table.styles.d.ts +1 -0
  103. package/dist/components/table/table.styles.js +98 -0
  104. package/dist/components/time-range/time-range.d.ts +40 -0
  105. package/dist/components/time-range/time-range.js +219 -0
  106. package/dist/components/time-range/time-range.styles.d.ts +1 -0
  107. package/dist/components/time-range/time-range.styles.js +78 -0
  108. package/dist/components/toast/toast-item.js +10 -2
  109. package/dist/components/toast/toast-item.styles.js +13 -4
  110. package/dist/components/toast/toast.styles.js +0 -1
  111. package/dist/components/tree/tree-node.d.ts +50 -0
  112. package/dist/components/tree/tree-node.js +196 -0
  113. package/dist/components/tree/tree.d.ts +65 -0
  114. package/dist/components/tree/tree.js +227 -0
  115. package/dist/components/tree/tree.styles.d.ts +1 -0
  116. package/dist/components/tree/tree.styles.js +13 -0
  117. package/dist/internal/form-associated.js +49 -1
  118. package/dist/internal/icons.d.ts +11 -0
  119. package/dist/internal/icons.js +64 -0
  120. package/dist/internal/tokens.styles.js +41 -1
  121. package/dist/lyra.d.ts +67 -0
  122. package/dist/lyra.js +55 -0
  123. package/llms-full.txt +1425 -0
  124. package/llms.txt +47 -0
  125. package/package.json +60 -11
@@ -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
+ }
@@ -0,0 +1,182 @@
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, state, query } from 'lit/decorators.js';
9
+ import { LyraElement } from '../../internal/lyra-element.js';
10
+ import { defineElement } from '../../internal/prefix.js';
11
+ import { styles } from './file-input.styles.js';
12
+ import { matchesAccept } from './accept.js';
13
+ /**
14
+ * `<lyra-file-input>` — a drag-drop + click-to-browse file dropzone. Emits
15
+ * raw `File[]`; parsing (CSV/XLSX/etc.) is left to the host, matching every
16
+ * surveyed upload seed (all of which upload raw files to a backend).
17
+ *
18
+ * @customElement lyra-file-input
19
+ * @slot - Custom drop-zone content, overrides the `label` attribute.
20
+ * @event lyra-files - `detail: { files, rejected }`, fired on drop and manual selection.
21
+ * @csspart base, input
22
+ */
23
+ export class LyraFileInput extends LyraElement {
24
+ constructor() {
25
+ super(...arguments);
26
+ this.multiple = false;
27
+ this.disabled = false;
28
+ this.accept = '';
29
+ this.allowedMimeTypes = [];
30
+ this.forbiddenMimeTypes = [];
31
+ this.maxFileSize = 0;
32
+ this.label = 'Drop files here or click to browse';
33
+ this.dragState = 'default';
34
+ this.dragCounter = 0;
35
+ this.onDragEnter = (e) => {
36
+ if (this.disabled)
37
+ return;
38
+ e.preventDefault();
39
+ this.dragCounter++;
40
+ const items = e.dataTransfer ? [...e.dataTransfer.items].filter((i) => i.kind === 'file') : [];
41
+ this.dragState = items.length ? this.previewState(items) : 'default';
42
+ };
43
+ this.onDragOver = (e) => {
44
+ if (this.disabled)
45
+ return;
46
+ e.preventDefault();
47
+ };
48
+ this.onDragLeave = (e) => {
49
+ if (this.disabled)
50
+ return;
51
+ e.preventDefault();
52
+ this.dragCounter = Math.max(0, this.dragCounter - 1);
53
+ if (this.dragCounter === 0)
54
+ this.dragState = 'default';
55
+ };
56
+ this.onDrop = (e) => {
57
+ if (this.disabled)
58
+ return;
59
+ e.preventDefault();
60
+ this.dragCounter = 0;
61
+ this.dragState = 'default';
62
+ const files = [...(e.dataTransfer?.files ?? [])];
63
+ if (files.length)
64
+ this.emitFiles(files);
65
+ };
66
+ this.onInputChange = (e) => {
67
+ const files = [...(e.target.files ?? [])];
68
+ e.target.value = '';
69
+ if (files.length)
70
+ this.emitFiles(files);
71
+ };
72
+ this.onKeyDown = (e) => {
73
+ if (this.disabled)
74
+ return;
75
+ if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
76
+ // Prevent Space from scrolling the page, matching the `table.ts`
77
+ // sortable-header/row convention for role-based clickable elements.
78
+ e.preventDefault();
79
+ this.openPicker();
80
+ }
81
+ };
82
+ }
83
+ static { this.styles = [LyraElement.styles, styles]; }
84
+ isAllowed(file) {
85
+ if (this.forbiddenMimeTypes.includes(file.type))
86
+ return 'type';
87
+ if (this.allowedMimeTypes.length > 0 && !this.allowedMimeTypes.includes(file.type))
88
+ return 'type';
89
+ if (this.accept && !matchesAccept(file, this.accept))
90
+ return 'type';
91
+ // `file.size` is `undefined` on the synthetic `DataTransferItem`-cast objects
92
+ // used during dragenter preview (real sizes aren't available until drop),
93
+ // so this naturally only takes effect for the real `classify()` call at drop time.
94
+ if (this.maxFileSize > 0 && file.size > this.maxFileSize)
95
+ return 'size';
96
+ return 'ok';
97
+ }
98
+ classify(fileList) {
99
+ if (!this.multiple && fileList.length > 1) {
100
+ return { files: [], rejected: fileList.map((file) => ({ file, reason: 'count' })) };
101
+ }
102
+ const files = [];
103
+ const rejected = [];
104
+ for (const f of fileList) {
105
+ const reason = this.isAllowed(f);
106
+ if (reason === 'ok')
107
+ files.push(f);
108
+ else
109
+ rejected.push({ file: f, reason });
110
+ }
111
+ return { files, rejected };
112
+ }
113
+ emitFiles(fileList) {
114
+ const { files, rejected } = this.classify(fileList);
115
+ this.emit('lyra-files', { files, rejected });
116
+ }
117
+ /** Programmatically open the native file picker. */
118
+ openPicker() {
119
+ this.inputEl?.click();
120
+ }
121
+ previewState(fileList) {
122
+ const { rejected } = this.classify(fileList);
123
+ return rejected.length > 0 ? 'reject' : 'accept';
124
+ }
125
+ render() {
126
+ return html `
127
+ <div
128
+ part="base"
129
+ role="button"
130
+ tabindex=${this.disabled ? '-1' : '0'}
131
+ aria-disabled=${this.disabled ? 'true' : nothing}
132
+ data-drag-state=${this.dragState}
133
+ @dragenter=${this.onDragEnter}
134
+ @dragover=${this.onDragOver}
135
+ @dragleave=${this.onDragLeave}
136
+ @drop=${this.onDrop}
137
+ @click=${() => !this.disabled && this.openPicker()}
138
+ @keydown=${this.onKeyDown}
139
+ >
140
+ <slot>${this.label}</slot>
141
+ </div>
142
+ <input
143
+ part="input"
144
+ type="file"
145
+ tabindex="-1"
146
+ aria-hidden="true"
147
+ accept=${this.accept}
148
+ ?multiple=${this.multiple}
149
+ ?disabled=${this.disabled}
150
+ @change=${this.onInputChange}
151
+ />
152
+ `;
153
+ }
154
+ }
155
+ __decorate([
156
+ property({ type: Boolean, reflect: true })
157
+ ], LyraFileInput.prototype, "multiple", void 0);
158
+ __decorate([
159
+ property({ type: Boolean, reflect: true })
160
+ ], LyraFileInput.prototype, "disabled", void 0);
161
+ __decorate([
162
+ property()
163
+ ], LyraFileInput.prototype, "accept", void 0);
164
+ __decorate([
165
+ property({ attribute: false })
166
+ ], LyraFileInput.prototype, "allowedMimeTypes", void 0);
167
+ __decorate([
168
+ property({ attribute: false })
169
+ ], LyraFileInput.prototype, "forbiddenMimeTypes", void 0);
170
+ __decorate([
171
+ property({ type: Number, attribute: 'max-file-size' })
172
+ ], LyraFileInput.prototype, "maxFileSize", void 0);
173
+ __decorate([
174
+ property()
175
+ ], LyraFileInput.prototype, "label", void 0);
176
+ __decorate([
177
+ state()
178
+ ], LyraFileInput.prototype, "dragState", void 0);
179
+ __decorate([
180
+ query('input[type="file"]')
181
+ ], LyraFileInput.prototype, "inputEl", void 0);
182
+ defineElement('file-input', LyraFileInput);
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;
@@ -0,0 +1,44 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `
3
+ :host {
4
+ display: block;
5
+ }
6
+ [part='base'] {
7
+ display: flex;
8
+ flex-direction: column;
9
+ align-items: center;
10
+ justify-content: center;
11
+ gap: var(--lyra-space-xs);
12
+ padding: var(--lyra-space-l);
13
+ border: 2px dashed var(--lyra-color-border);
14
+ border-radius: var(--lyra-radius);
15
+ background: var(--lyra-color-surface);
16
+ color: var(--lyra-color-text-quiet);
17
+ text-align: center;
18
+ cursor: pointer;
19
+ font-size: 0.875rem;
20
+ }
21
+ [part='base'][data-drag-state='accept'] {
22
+ border-color: var(--lyra-color-success);
23
+ background: color-mix(in srgb, var(--lyra-color-success) 8%, transparent);
24
+ }
25
+ [part='base'][data-drag-state='reject'] {
26
+ border-color: var(--lyra-color-danger);
27
+ background: color-mix(in srgb, var(--lyra-color-danger) 8%, transparent);
28
+ }
29
+ [part='base']:focus-visible {
30
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-focus-ring-color);
31
+ outline-offset: var(--lyra-focus-ring-offset);
32
+ }
33
+ :host([disabled]) [part='base'] {
34
+ opacity: var(--lyra-opacity-disabled);
35
+ cursor: not-allowed;
36
+ }
37
+ [part='input'] {
38
+ position: absolute;
39
+ inline-size: 1px;
40
+ block-size: 1px;
41
+ opacity: 0;
42
+ overflow: hidden;
43
+ }
44
+ `;
@@ -1,5 +1,6 @@
1
1
  import { type TemplateResult, type PropertyValues } from 'lit';
2
2
  import { LyraElement } from '../../internal/lyra-element.js';
3
+ import '../skeleton/skeleton.js';
3
4
  /**
4
5
  * `<lyra-flag>` — a country/language flag.
5
6
  *
@@ -24,8 +25,11 @@ export declare class LyraFlag extends LyraElement {
24
25
  /** Render as a circular flag. */
25
26
  round: boolean;
26
27
  private src?;
28
+ /** True while the lazy-loaded `@aceshooting/lyra-flags` peer resolver is in flight. */
29
+ private loading;
27
30
  private get code();
28
31
  protected willUpdate(changed: PropertyValues<this>): void;
32
+ protected updated(): void;
29
33
  render(): TemplateResult;
30
34
  }
31
35
  declare global {
@@ -10,6 +10,7 @@ import { LyraElement } from '../../internal/lyra-element.js';
10
10
  import { defineElement } from '../../internal/prefix.js';
11
11
  import { styles } from './flag.styles.js';
12
12
  import { languageToCountry } from './language-map.js';
13
+ import '../skeleton/skeleton.js';
13
14
  let flagUrlResolver;
14
15
  /**
15
16
  * Lazily loads the optional peer dependency '@aceshooting/lyra-flags' once per
@@ -45,6 +46,8 @@ export class LyraFlag extends LyraElement {
45
46
  super(...arguments);
46
47
  /** Render as a circular flag. */
47
48
  this.round = false;
49
+ /** True while the lazy-loaded `@aceshooting/lyra-flags` peer resolver is in flight. */
50
+ this.loading = true;
48
51
  }
49
52
  static { this.styles = [LyraElement.styles, styles]; }
50
53
  get code() {
@@ -55,18 +58,35 @@ export class LyraFlag extends LyraElement {
55
58
  return undefined;
56
59
  }
57
60
  willUpdate(changed) {
58
- if (!changed.has('country') && !changed.has('language'))
61
+ // `this.hasUpdated` (not just `changed`) forces this body to run once on
62
+ // the very first update even when neither `country` nor `language` is
63
+ // ever set: an unset optional property's first "assignment" is
64
+ // undefined -> undefined (no-op to Lit), so `changed.has('country')`
65
+ // would otherwise never become true for a `<lyra-flag>` that never
66
+ // receives a code, leaving `loading` stuck at its initial `true` forever.
67
+ if (this.hasUpdated && !changed.has('country') && !changed.has('language'))
59
68
  return;
60
69
  const code = this.code;
61
70
  if (!code) {
62
71
  this.src = undefined;
72
+ this.loading = false;
63
73
  return;
64
74
  }
75
+ this.loading = true;
65
76
  void loadFlagUrlResolver().then((resolve) => {
66
77
  this.src = resolve?.(code);
78
+ this.loading = false;
67
79
  });
68
80
  }
81
+ updated() {
82
+ if (this.loading)
83
+ this.setAttribute('aria-busy', 'true');
84
+ else
85
+ this.removeAttribute('aria-busy');
86
+ }
69
87
  render() {
88
+ if (this.loading)
89
+ return html `<lyra-skeleton variant="rect"></lyra-skeleton>`;
70
90
  if (!this.src)
71
91
  return html ``;
72
92
  const alt = this.label ?? (this.code ?? '').toUpperCase();
@@ -88,4 +108,7 @@ __decorate([
88
108
  __decorate([
89
109
  state()
90
110
  ], LyraFlag.prototype, "src", void 0);
111
+ __decorate([
112
+ state()
113
+ ], LyraFlag.prototype, "loading", void 0);
91
114
  defineElement('flag', LyraFlag);
@@ -2,7 +2,7 @@ import { css } from 'lit';
2
2
  export const styles = css `
3
3
  :host {
4
4
  display: inline-block;
5
- inline-size: auto;
5
+ aspect-ratio: 4 / 3;
6
6
  block-size: 1em;
7
7
  line-height: 0;
8
8
  vertical-align: middle;
@@ -0,0 +1,29 @@
1
+ import { type TemplateResult } from 'lit';
2
+ import { LyraElement } from '../../internal/lyra-element.js';
3
+ export type GaugeType = 'radial' | 'linear';
4
+ /**
5
+ * `<lyra-gauge>` — a radial or linear meter. First-party invention; no
6
+ * generic gauge widget was found across the surveyed repos.
7
+ *
8
+ * @customElement lyra-gauge
9
+ * @csspart base, track, fill, value, label
10
+ */
11
+ export declare class LyraGauge extends LyraElement {
12
+ static styles: import("lit").CSSResultGroup[];
13
+ value: number;
14
+ min: number;
15
+ max: number;
16
+ type: GaugeType;
17
+ label: string;
18
+ valueLabel?: string;
19
+ private get ratio();
20
+ protected willUpdate(): void;
21
+ private renderRadial;
22
+ private renderLinear;
23
+ render(): TemplateResult;
24
+ }
25
+ declare global {
26
+ interface HTMLElementTagNameMap {
27
+ 'lyra-gauge': LyraGauge;
28
+ }
29
+ }
@@ -0,0 +1,133 @@
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, svg } 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 { styles } from './gauge.styles.js';
12
+ // Radial gauge sweeps 270° (like a speedometer), leaving a 90° gap at the bottom.
13
+ const SWEEP_DEG = 270;
14
+ const START_DEG = 135; // rotate so the gap is centered at the bottom
15
+ const RADIUS = 40;
16
+ const CENTER = 50;
17
+ const STROKE = 10;
18
+ // Linear gauge: keep the bar thin and low in the 0..20 viewBox so the top
19
+ // ~12 units are free for a value/label text row above it.
20
+ const LINEAR_STROKE = 6;
21
+ const LINEAR_BAR_Y = 15;
22
+ const LINEAR_TEXT_Y = 8;
23
+ // Length of the x1=0 -> x2=100 horizontal fill/track line (Euclidean, y1==y2).
24
+ const LINEAR_LENGTH = 100;
25
+ function polarToCartesian(angleDeg) {
26
+ const rad = (angleDeg * Math.PI) / 180;
27
+ return [CENTER + RADIUS * Math.cos(rad), CENTER + RADIUS * Math.sin(rad)];
28
+ }
29
+ function arcPath(startDeg, endDeg) {
30
+ const [sx, sy] = polarToCartesian(startDeg);
31
+ const [ex, ey] = polarToCartesian(endDeg);
32
+ const largeArc = endDeg - startDeg > 180 ? 1 : 0;
33
+ return `M ${sx} ${sy} A ${RADIUS} ${RADIUS} 0 ${largeArc} 1 ${ex} ${ey}`;
34
+ }
35
+ // The radial fill's `d` is fixed geometry (the full 270° sweep), built once —
36
+ // a `d` rebuilt from `ratio` on every render is not reliably CSS-transitionable
37
+ // across browsers. Animated progress instead rides stroke-dasharray/
38
+ // stroke-dashoffset (always transitionable) over this constant path, the same
39
+ // technique used for the linear variant's fill line below.
40
+ const RADIAL_ARC_D = arcPath(START_DEG, START_DEG + SWEEP_DEG);
41
+ const RADIAL_ARC_LENGTH = (SWEEP_DEG / 360) * 2 * Math.PI * RADIUS;
42
+ /**
43
+ * `<lyra-gauge>` — a radial or linear meter. First-party invention; no
44
+ * generic gauge widget was found across the surveyed repos.
45
+ *
46
+ * @customElement lyra-gauge
47
+ * @csspart base, track, fill, value, label
48
+ */
49
+ export class LyraGauge extends LyraElement {
50
+ constructor() {
51
+ super(...arguments);
52
+ this.value = 0;
53
+ this.min = 0;
54
+ this.max = 100;
55
+ this.type = 'radial';
56
+ this.label = '';
57
+ }
58
+ static { this.styles = [LyraElement.styles, styles]; }
59
+ get ratio() {
60
+ const span = this.max - this.min || 1;
61
+ return Math.min(1, Math.max(0, (this.value - this.min) / span));
62
+ }
63
+ willUpdate() {
64
+ this.setAttribute('role', 'meter');
65
+ this.setAttribute('aria-valuenow', String(this.value));
66
+ this.setAttribute('aria-valuemin', String(this.min));
67
+ this.setAttribute('aria-valuemax', String(this.max));
68
+ if (this.label)
69
+ this.setAttribute('aria-label', this.label);
70
+ else
71
+ this.removeAttribute('aria-label');
72
+ }
73
+ renderRadial() {
74
+ const text = this.valueLabel ?? String(this.value);
75
+ // Dashoffset counts down from the full arc length (nothing revealed) to 0
76
+ // (whole sweep revealed) as ratio goes 0 -> 1 — the classic "draw an SVG
77
+ // path" technique, which transitions smoothly via plain CSS.
78
+ const dashoffset = RADIAL_ARC_LENGTH * (1 - this.ratio);
79
+ return html `<svg part="base" viewBox="0 0 100 100">
80
+ <path part="track" stroke-width=${STROKE} d=${RADIAL_ARC_D}></path>
81
+ <path
82
+ part="fill"
83
+ stroke-width=${STROKE}
84
+ d=${RADIAL_ARC_D}
85
+ stroke-dasharray=${RADIAL_ARC_LENGTH}
86
+ stroke-dashoffset=${dashoffset}
87
+ ></path>
88
+ <text part="value" x="50" y="52">${text}</text>
89
+ ${this.label ? svg `<text part="label" x="50" y="68">${this.label}</text>` : ''}
90
+ </svg>`;
91
+ }
92
+ renderLinear() {
93
+ const text = this.valueLabel ?? String(this.value);
94
+ const dashoffset = LINEAR_LENGTH * (1 - this.ratio);
95
+ return html `<svg part="base" viewBox="0 0 100 20" preserveAspectRatio="none">
96
+ <line part="track" x1="0" y1=${LINEAR_BAR_Y} x2="100" y2=${LINEAR_BAR_Y} stroke-width=${LINEAR_STROKE}></line>
97
+ <line
98
+ part="fill"
99
+ x1="0"
100
+ y1=${LINEAR_BAR_Y}
101
+ x2="100"
102
+ y2=${LINEAR_BAR_Y}
103
+ stroke-width=${LINEAR_STROKE}
104
+ stroke-dasharray=${LINEAR_LENGTH}
105
+ stroke-dashoffset=${dashoffset}
106
+ ></line>
107
+ ${this.label ? svg `<text part="label" x="0" y=${LINEAR_TEXT_Y}>${this.label}</text>` : ''}
108
+ <text part="value" x="100" y=${LINEAR_TEXT_Y}>${text}</text>
109
+ </svg>`;
110
+ }
111
+ render() {
112
+ return this.type === 'linear' ? this.renderLinear() : this.renderRadial();
113
+ }
114
+ }
115
+ __decorate([
116
+ property({ type: Number })
117
+ ], LyraGauge.prototype, "value", void 0);
118
+ __decorate([
119
+ property({ type: Number })
120
+ ], LyraGauge.prototype, "min", void 0);
121
+ __decorate([
122
+ property({ type: Number })
123
+ ], LyraGauge.prototype, "max", void 0);
124
+ __decorate([
125
+ property({ reflect: true })
126
+ ], LyraGauge.prototype, "type", void 0);
127
+ __decorate([
128
+ property()
129
+ ], LyraGauge.prototype, "label", void 0);
130
+ __decorate([
131
+ property({ attribute: false })
132
+ ], LyraGauge.prototype, "valueLabel", void 0);
133
+ defineElement('gauge', LyraGauge);
@@ -0,0 +1 @@
1
+ export declare const styles: import("lit").CSSResult;
@@ -0,0 +1,60 @@
1
+ import { css } from 'lit';
2
+ export const styles = css `
3
+ :host {
4
+ display: inline-block;
5
+ inline-size: 8em;
6
+ block-size: 8em;
7
+ }
8
+ :host([type='linear']) {
9
+ inline-size: 12em;
10
+ block-size: 1.5em;
11
+ }
12
+ svg {
13
+ display: block;
14
+ inline-size: 100%;
15
+ block-size: 100%;
16
+ overflow: visible;
17
+ }
18
+ [part='track'] {
19
+ fill: none;
20
+ stroke: var(--lyra-color-border);
21
+ }
22
+ [part='fill'] {
23
+ fill: none;
24
+ stroke: var(--lyra-color-brand);
25
+ stroke-linecap: round;
26
+ transition: stroke-dashoffset var(--lyra-transition-base);
27
+ }
28
+ [part='value'] {
29
+ font-size: 1rem;
30
+ font-weight: 700;
31
+ text-anchor: middle;
32
+ fill: var(--lyra-color-text);
33
+ font-family: var(--lyra-font);
34
+ }
35
+ [part='label'] {
36
+ font-size: 0.625rem;
37
+ text-anchor: middle;
38
+ fill: var(--lyra-color-text-quiet);
39
+ font-family: var(--lyra-font);
40
+ text-transform: uppercase;
41
+ }
42
+ :host([type='linear']) [part='fill'] {
43
+ stroke-linecap: butt;
44
+ }
45
+ :host([type='linear']) [part='value'],
46
+ :host([type='linear']) [part='label'] {
47
+ font-size: 0.5rem;
48
+ }
49
+ :host([type='linear']) [part='value'] {
50
+ text-anchor: end;
51
+ }
52
+ :host([type='linear']) [part='label'] {
53
+ text-anchor: start;
54
+ }
55
+ @media (prefers-reduced-motion: reduce) {
56
+ [part='fill'] {
57
+ transition: none !important;
58
+ }
59
+ }
60
+ `;