@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
@@ -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
+ `;
@@ -0,0 +1,68 @@
1
+ import { type TemplateResult, type PropertyValues } from 'lit';
2
+ import { LyraElement } from '../../internal/lyra-element.js';
3
+ import '../skeleton/skeleton.js';
4
+ export interface GraphNode {
5
+ id: string;
6
+ label?: string;
7
+ radius?: number;
8
+ color?: string;
9
+ }
10
+ export interface GraphLink {
11
+ source: string;
12
+ target: string;
13
+ width?: number;
14
+ }
15
+ /**
16
+ * `<lyra-graph>` — a force-directed node-link diagram with pan/zoom/drag.
17
+ * Requires the optional peer deps `d3-force`/`d3-drag`/`d3-zoom`/`d3-selection`
18
+ * (lazy-loaded; a consumer who never uses this component pays zero d3 cost).
19
+ *
20
+ * @customElement lyra-graph
21
+ * @event lyra-node-click - `detail: { id }`.
22
+ * @event lyra-link-click - `detail: { source, target }`.
23
+ * @csspart base, svg, node, link, label
24
+ */
25
+ export declare class LyraGraph extends LyraElement {
26
+ static styles: import("lit").CSSResultGroup[];
27
+ nodes: GraphNode[];
28
+ links: GraphLink[];
29
+ width: number;
30
+ height: number;
31
+ chargeStrength: number;
32
+ linkDistance: number;
33
+ /** True until the lazy-loaded d3 peer dependencies have settled (success or failure). */
34
+ private loading;
35
+ private simNodes;
36
+ private simLinks;
37
+ /** Current pan/zoom transform (SVG `transform` attribute syntax), driven by d3-zoom. */
38
+ private transform;
39
+ private simulation?;
40
+ private d3?;
41
+ /** The `<svg>` currently wired up with d3-zoom (guards a one-time bind). */
42
+ private zoomedEl?;
43
+ /** Node `<circle>`s already wired up with d3-drag; cleared on every simulation rebuild
44
+ * so DOM elements Lit reuses across a rebuild get rebound to their fresh datum. */
45
+ private boundNodeEls;
46
+ connectedCallback(): void;
47
+ disconnectedCallback(): void;
48
+ private nodeRadius;
49
+ protected updated(changed: PropertyValues): void;
50
+ /**
51
+ * Imperatively wires up d3-zoom (pan/zoom on the `<svg>`) and d3-drag
52
+ * (per-node drag) against the just-rendered DOM. Runs after every render:
53
+ * binding the `<svg>` is a one-time no-op guard (`zoomedEl`), and node
54
+ * `<circle>`s are skipped once bound (`boundNodeEls`) — a WeakSet reset on
55
+ * every `rebuildSimulation()` so DOM nodes Lit reuses across a rebuild get
56
+ * rebound against their new datum instead of a stale one.
57
+ */
58
+ private applyInteractions;
59
+ private rebuildSimulation;
60
+ private onNodeClick;
61
+ private onLinkClick;
62
+ render(): TemplateResult;
63
+ }
64
+ declare global {
65
+ interface HTMLElementTagNameMap {
66
+ 'lyra-graph': LyraGraph;
67
+ }
68
+ }