@aceshooting/lyra-ui 0.1.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (86) hide show
  1. package/README.md +17 -8
  2. package/custom-elements.json +4460 -2940
  3. package/dist/components/chart/bar-chart.stories.d.ts +5 -0
  4. package/dist/components/chart/bar-chart.stories.js +20 -0
  5. package/dist/components/chart/box-plot.stories.d.ts +5 -0
  6. package/dist/components/chart/box-plot.stories.js +29 -0
  7. package/dist/components/chart/bubble-chart.stories.d.ts +5 -0
  8. package/dist/components/chart/bubble-chart.stories.js +26 -0
  9. package/dist/components/chart/doughnut-chart.stories.d.ts +5 -0
  10. package/dist/components/chart/doughnut-chart.stories.js +26 -0
  11. package/dist/components/chart/histogram.stories.d.ts +5 -0
  12. package/dist/components/chart/histogram.stories.js +17 -0
  13. package/dist/components/chart/line-chart.stories.d.ts +5 -0
  14. package/dist/components/chart/line-chart.stories.js +24 -0
  15. package/dist/components/chart/pie-chart.stories.d.ts +5 -0
  16. package/dist/components/chart/pie-chart.stories.js +26 -0
  17. package/dist/components/chart/polar-area-chart.stories.d.ts +5 -0
  18. package/dist/components/chart/polar-area-chart.stories.js +20 -0
  19. package/dist/components/chart/radar-chart.stories.d.ts +5 -0
  20. package/dist/components/chart/radar-chart.stories.js +20 -0
  21. package/dist/components/chart/scatter-chart.stories.d.ts +5 -0
  22. package/dist/components/chart/scatter-chart.stories.js +31 -0
  23. package/dist/components/combobox/combobox.d.ts +26 -1
  24. package/dist/components/combobox/combobox.js +147 -31
  25. package/dist/components/combobox/combobox.styles.js +21 -0
  26. package/dist/components/combobox/option.d.ts +4 -0
  27. package/dist/components/combobox/option.js +10 -0
  28. package/dist/components/date-picker/calendar-core.d.ts +6 -2
  29. package/dist/components/date-picker/calendar-core.js +23 -4
  30. package/dist/components/date-picker/date-input.d.ts +1 -0
  31. package/dist/components/date-picker/date-input.js +19 -2
  32. package/dist/components/date-picker/date-picker.js +4 -2
  33. package/dist/components/date-picker/date-picker.styles.js +5 -1
  34. package/dist/components/empty/empty.stories.d.ts +5 -0
  35. package/dist/components/empty/empty.stories.js +14 -0
  36. package/dist/components/export-button/export-button.js +7 -2
  37. package/dist/components/export-button/export-button.stories.d.ts +6 -0
  38. package/dist/components/export-button/export-button.stories.js +32 -0
  39. package/dist/components/file-input/file-input.stories.d.ts +5 -0
  40. package/dist/components/file-input/file-input.stories.js +10 -0
  41. package/dist/components/flag/flag.js +12 -2
  42. package/dist/components/gauge/gauge.js +8 -4
  43. package/dist/components/gauge/gauge.stories.d.ts +6 -0
  44. package/dist/components/gauge/gauge.stories.js +13 -0
  45. package/dist/components/graph/graph.stories.d.ts +5 -0
  46. package/dist/components/graph/graph.stories.js +30 -0
  47. package/dist/components/heatmap/heatmap.stories.d.ts +5 -0
  48. package/dist/components/heatmap/heatmap.stories.js +24 -0
  49. package/dist/components/map/map.d.ts +6 -0
  50. package/dist/components/map/map.js +69 -9
  51. package/dist/components/map/map.stories.d.ts +5 -0
  52. package/dist/components/map/map.stories.js +40 -0
  53. package/dist/components/playback/playback.js +6 -0
  54. package/dist/components/playback/playback.stories.d.ts +5 -0
  55. package/dist/components/playback/playback.stories.js +10 -0
  56. package/dist/components/skeleton/skeleton.stories.d.ts +5 -0
  57. package/dist/components/skeleton/skeleton.stories.js +16 -0
  58. package/dist/components/split/split.d.ts +1 -0
  59. package/dist/components/split/split.js +25 -4
  60. package/dist/components/split/split.stories.d.ts +5 -0
  61. package/dist/components/split/split.stories.js +16 -0
  62. package/dist/components/stat/stat.stories.d.ts +5 -0
  63. package/dist/components/stat/stat.stories.js +16 -0
  64. package/dist/components/table/table.stories.d.ts +6 -0
  65. package/dist/components/table/table.stories.js +22 -0
  66. package/dist/components/time-range/time-range.stories.d.ts +6 -0
  67. package/dist/components/time-range/time-range.stories.js +13 -0
  68. package/dist/components/toast/toast-item.d.ts +1 -0
  69. package/dist/components/toast/toast-item.js +16 -1
  70. package/dist/components/tree/tree.stories.d.ts +5 -0
  71. package/dist/components/tree/tree.stories.js +22 -0
  72. package/dist/components/widget/widget.d.ts +40 -0
  73. package/dist/components/widget/widget.js +159 -0
  74. package/dist/components/widget/widget.stories.d.ts +6 -0
  75. package/dist/components/widget/widget.stories.js +31 -0
  76. package/dist/components/widget/widget.styles.d.ts +1 -0
  77. package/dist/components/widget/widget.styles.js +90 -0
  78. package/dist/internal/icons.d.ts +2 -0
  79. package/dist/internal/icons.js +9 -0
  80. package/dist/internal/scroll-lock.d.ts +7 -0
  81. package/dist/internal/scroll-lock.js +25 -0
  82. package/dist/internal/tokens.styles.js +5 -0
  83. package/dist/lyra.d.ts +5 -0
  84. package/dist/lyra.js +2 -0
  85. package/llms.txt +3 -0
  86. package/package.json +3 -3
@@ -11,6 +11,15 @@ import { defineElement } from '../../internal/prefix.js';
11
11
  import { styles } from './flag.styles.js';
12
12
  import { languageToCountry } from './language-map.js';
13
13
  import '../skeleton/skeleton.js';
14
+ /**
15
+ * ISO 3166-1 alpha-2 country code: exactly two letters, case-insensitive.
16
+ * The peer package `@aceshooting/lyra-flags` naively interpolates its `code`
17
+ * argument into a `new URL('./flags/${code}.svg', ...)` with no validation
18
+ * of its own — an unvalidated `country` containing `../` segments (e.g.
19
+ * `../../etc`) can escape the intended flags/ directory. Anything that
20
+ * doesn't match this shape is treated the same as an unknown/missing flag.
21
+ */
22
+ const COUNTRY_CODE_RE = /^[a-z]{2}$/i;
14
23
  let flagUrlResolver;
15
24
  /**
16
25
  * Lazily loads the optional peer dependency '@aceshooting/lyra-flags' once per
@@ -51,8 +60,9 @@ export class LyraFlag extends LyraElement {
51
60
  }
52
61
  static { this.styles = [LyraElement.styles, styles]; }
53
62
  get code() {
54
- if (this.country)
55
- return this.country.toLowerCase();
63
+ if (this.country) {
64
+ return COUNTRY_CODE_RE.test(this.country) ? this.country.toLowerCase() : undefined;
65
+ }
56
66
  if (this.language)
57
67
  return languageToCountry(this.language);
58
68
  return undefined;
@@ -69,6 +69,10 @@ export class LyraGauge extends LyraElement {
69
69
  this.setAttribute('aria-label', this.label);
70
70
  else
71
71
  this.removeAttribute('aria-label');
72
+ if (this.valueLabel)
73
+ this.setAttribute('aria-valuetext', this.valueLabel);
74
+ else
75
+ this.removeAttribute('aria-valuetext');
72
76
  }
73
77
  renderRadial() {
74
78
  const text = this.valueLabel ?? String(this.value);
@@ -85,8 +89,8 @@ export class LyraGauge extends LyraElement {
85
89
  stroke-dasharray=${RADIAL_ARC_LENGTH}
86
90
  stroke-dashoffset=${dashoffset}
87
91
  ></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>` : ''}
92
+ <text part="value" x="50" y="52" aria-hidden="true">${text}</text>
93
+ ${this.label ? svg `<text part="label" x="50" y="68" aria-hidden="true">${this.label}</text>` : ''}
90
94
  </svg>`;
91
95
  }
92
96
  renderLinear() {
@@ -104,8 +108,8 @@ export class LyraGauge extends LyraElement {
104
108
  stroke-dasharray=${LINEAR_LENGTH}
105
109
  stroke-dashoffset=${dashoffset}
106
110
  ></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>
111
+ ${this.label ? svg `<text part="label" x="0" y=${LINEAR_TEXT_Y} aria-hidden="true">${this.label}</text>` : ''}
112
+ <text part="value" x="100" y=${LINEAR_TEXT_Y} aria-hidden="true">${text}</text>
109
113
  </svg>`;
110
114
  }
111
115
  render() {
@@ -0,0 +1,6 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Radial: Story;
6
+ export declare const Linear: Story;
@@ -0,0 +1,13 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Gauge',
4
+ component: 'lyra-gauge',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Radial = {
9
+ render: () => html `<lyra-gauge value="72" max="100" label="CPU"></lyra-gauge>`,
10
+ };
11
+ export const Linear = {
12
+ render: () => html `<lyra-gauge type="linear" value="40" max="100" label="Disk"></lyra-gauge>`,
13
+ };
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
@@ -0,0 +1,30 @@
1
+ import { html } from 'lit';
2
+ const nodes = [
3
+ { id: 'a', label: 'A' },
4
+ { id: 'b', label: 'B' },
5
+ { id: 'c', label: 'C' },
6
+ { id: 'd', label: 'D' },
7
+ ];
8
+ const links = [
9
+ { source: 'a', target: 'b' },
10
+ { source: 'a', target: 'c' },
11
+ { source: 'b', target: 'd' },
12
+ { source: 'c', target: 'd' },
13
+ ];
14
+ const meta = {
15
+ title: 'Graph',
16
+ component: 'lyra-graph',
17
+ tags: ['autodocs'],
18
+ };
19
+ export default meta;
20
+ export const Default = {
21
+ render: () => html `
22
+ <lyra-graph
23
+ width="480"
24
+ height="320"
25
+ style="height: 20rem"
26
+ .nodes=${nodes}
27
+ .links=${links}
28
+ ></lyra-graph>
29
+ `,
30
+ };
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
@@ -0,0 +1,24 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Heatmap',
4
+ component: 'lyra-heatmap',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Default = {
9
+ render: () => html `
10
+ <lyra-heatmap
11
+ cell-size="24"
12
+ value-label="events"
13
+ .rowLabels=${['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}
14
+ .colLabels=${['0h', '6h', '12h', '18h']}
15
+ .values=${[
16
+ [1, 4, 9, 2],
17
+ [0, 2, 6, 3],
18
+ [5, 8, 3, 1],
19
+ [-1, 1, 4, 7],
20
+ [2, 3, 5, 6],
21
+ ]}
22
+ ></lyra-heatmap>
23
+ `,
24
+ };
@@ -28,6 +28,11 @@ export interface MapMarker {
28
28
  * @event lyra-map-load - Fired once the underlying maplibregl.Map loads.
29
29
  * @event lyra-map-click - `detail: { lngLat, feature? }`.
30
30
  * @csspart base, container, legend, legend-swatch
31
+ *
32
+ * ⚠️ The default `mapStyle` (when unset) uses OpenStreetMap's demo tile
33
+ * server, which is not suitable for production traffic — see the
34
+ * `DEFAULT_STYLE` doc comment in `map.ts`. Always pass an explicit
35
+ * `mapStyle` in production.
31
36
  */
32
37
  export declare class LyraMap extends LyraElement {
33
38
  static styles: import("lit").CSSResultGroup[];
@@ -46,6 +51,7 @@ export declare class LyraMap extends LyraElement {
46
51
  private _appliedFillLayerId?;
47
52
  private _maplibreModule;
48
53
  private _markerInstances;
54
+ private _connectGeneration;
49
55
  /** The raw `maplibregl.Map` instance — escape hatch for anything this wrapper doesn't expose. */
50
56
  get map(): MaplibreMap | undefined;
51
57
  connectedCallback(): void;
@@ -7,11 +7,24 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
7
7
  /// <reference types="geojson" />
8
8
  import { html, nothing } from 'lit';
9
9
  import { property, query, state } from 'lit/decorators.js';
10
+ import { styleMap } from 'lit/directives/style-map.js';
10
11
  import { LyraElement } from '../../internal/lyra-element.js';
11
12
  import { defineElement } from '../../internal/prefix.js';
12
13
  import { loadMaplibre } from './map-loader.js';
13
14
  import { styles } from './map.styles.js';
14
15
  import '../skeleton/skeleton.js';
16
+ /**
17
+ * Fallback `mapStyle` used only when a consumer never sets one. Points at
18
+ * OpenStreetMap's shared **demo** tile server (`tile.openstreetmap.org`),
19
+ * which is explicitly NOT for production/bulk use: it has no capacity
20
+ * guarantees, requires an identifying `User-Agent`/`Referer`, and will
21
+ * rate-limit or IP-block non-compliant or high-volume clients per the OSM
22
+ * Foundation's tile usage policy
23
+ * (https://operations.osmfoundation.org/policies/tiles/). Convenient for
24
+ * local development and quick prototypes only — production apps MUST supply
25
+ * their own `mapStyle` (a hosted vector/raster style from a tile provider
26
+ * you have a plan with, or your own tile server).
27
+ */
15
28
  const DEFAULT_STYLE = {
16
29
  version: 8,
17
30
  sources: {
@@ -24,6 +37,19 @@ const DEFAULT_STYLE = {
24
37
  },
25
38
  layers: [{ id: 'lyra-osm', type: 'raster', source: 'lyra-osm' }],
26
39
  };
40
+ /**
41
+ * Rejects a `LegendEntry.color` that could break out of the single
42
+ * `background` declaration it's assigned to — `;`, `{`, and `}` are all a
43
+ * value needs to terminate that declaration and start another. This matters
44
+ * even though the swatch's color is set via Lit's `styleMap` directive (not
45
+ * raw string interpolation): `styleMap`'s first commit for a given
46
+ * attribute part serializes the whole `style` value as a single string
47
+ * (only later updates go through the safe `CSSStyleDeclaration.setProperty()`
48
+ * path), so an unsanitized value could still inject on that first render.
49
+ */
50
+ function sanitizeSwatchColor(color) {
51
+ return /[;{}]/.test(color) ? undefined : color;
52
+ }
27
53
  /**
28
54
  * `<lyra-map>` — a maplibre-gl wrapper with a declarative legend and
29
55
  * choropleth GeoJSON layer, plus a raw `map` escape hatch. Requires the
@@ -33,6 +59,11 @@ const DEFAULT_STYLE = {
33
59
  * @event lyra-map-load - Fired once the underlying maplibregl.Map loads.
34
60
  * @event lyra-map-click - `detail: { lngLat, feature? }`.
35
61
  * @csspart base, container, legend, legend-swatch
62
+ *
63
+ * ⚠️ The default `mapStyle` (when unset) uses OpenStreetMap's demo tile
64
+ * server, which is not suitable for production traffic — see the
65
+ * `DEFAULT_STYLE` doc comment in `map.ts`. Always pass an explicit
66
+ * `mapStyle` in production.
36
67
  */
37
68
  export class LyraMap extends LyraElement {
38
69
  constructor() {
@@ -52,6 +83,17 @@ export class LyraMap extends LyraElement {
52
83
  // choropleth updates from ever re-running.
53
84
  this._styleLoaded = false;
54
85
  this._markerInstances = new Map();
86
+ // Bumped on every connectedCallback and captured by value in its
87
+ // loadMaplibre().then() closure below. A disconnect immediately followed
88
+ // by a reconnect (fast remounts, route/tab switches, etc.) before that
89
+ // cached promise settles would otherwise let *every* connect attempt's
90
+ // closure construct its own maplibregl.Map against the same container —
91
+ // only the last one written to `this._map` survives, and the earlier
92
+ // ones leak their WebGL context/canvas/event listeners forever. Each
93
+ // closure compares its captured generation against the current value
94
+ // before doing anything observable, and bails if a newer
95
+ // connectedCallback has since superseded it.
96
+ this._connectGeneration = 0;
55
97
  }
56
98
  static { this.styles = [LyraElement.styles, styles]; }
57
99
  /** The raw `maplibregl.Map` instance — escape hatch for anything this wrapper doesn't expose. */
@@ -60,7 +102,15 @@ export class LyraMap extends LyraElement {
60
102
  }
61
103
  connectedCallback() {
62
104
  super.connectedCallback();
105
+ const generation = ++this._connectGeneration;
63
106
  void loadMaplibre().then(async (mod) => {
107
+ // A newer connectedCallback (disconnect + reconnect) already
108
+ // superseded this attempt while loadMaplibre()'s cached promise was
109
+ // in flight — bail before touching any state, let the newer attempt's
110
+ // own closure (already queued behind this one on the same promise)
111
+ // take over instead.
112
+ if (generation !== this._connectGeneration)
113
+ return;
64
114
  this.loading = false;
65
115
  if (!mod)
66
116
  return;
@@ -68,11 +118,13 @@ export class LyraMap extends LyraElement {
68
118
  // `[part="container"]` only exists once `loading` flips to `false` and
69
119
  // Lit re-renders — wait for that render to land before querying it.
70
120
  await this.updateComplete;
71
- // The element may have been removed from the DOM while the async
72
- // loadMaplibre()/updateComplete window was in flight — don't spin up
73
- // a maplibre Map (WebGL context + event listeners) for a detached
74
- // instance (disconnectedCallback's cleanup already ran).
75
- if (!this.containerEl || !this.isConnected)
121
+ // Re-check after the await: the element may have been removed from
122
+ // the DOM entirely while this loadMaplibre()/updateComplete window
123
+ // was in flight — don't spin up a maplibre Map (WebGL context + event
124
+ // listeners) for a detached instance (disconnectedCallback's cleanup
125
+ // already ran) — or superseded by yet another disconnect+reconnect
126
+ // cycle that happened during the `await` above.
127
+ if (generation !== this._connectGeneration || !this.containerEl || !this.isConnected)
76
128
  return;
77
129
  this._map = new mod.Map({
78
130
  container: this.containerEl,
@@ -122,11 +174,16 @@ export class LyraMap extends LyraElement {
122
174
  this._styleLoaded = false;
123
175
  this._appliedChoroplethSourceId = undefined;
124
176
  this._appliedFillLayerId = undefined;
125
- this._map.setStyle(this.mapStyle);
177
+ // Register the listener *before* calling setStyle(): maplibre-gl's
178
+ // diff-based style update path (small, incremental style changes) can
179
+ // fire 'style.load' synchronously from inside setStyle() itself —
180
+ // registering the listener afterwards would miss that emission and
181
+ // leave the choropleth (and `_styleLoaded`) never re-applied.
126
182
  this._map.once('style.load', () => {
127
183
  this._styleLoaded = true;
128
184
  this.applyChoropleth();
129
185
  });
186
+ this._map.setStyle(this.mapStyle);
130
187
  }
131
188
  else if (changed.has('choropleth') && this._styleLoaded) {
132
189
  this.applyChoropleth();
@@ -234,10 +291,13 @@ export class LyraMap extends LyraElement {
234
291
  : html `<div part="container"></div>`}
235
292
  ${this.legend.length
236
293
  ? html `<div part="legend">
237
- ${this.legend.map((entry) => html `<div class="legend-row">
238
- <span part="legend-swatch" style=${`background:${entry.color}`}></span>
294
+ ${this.legend.map((entry) => {
295
+ const bg = sanitizeSwatchColor(entry.color);
296
+ return html `<div class="legend-row">
297
+ <span part="legend-swatch" style=${styleMap(bg ? { background: bg } : {})}></span>
239
298
  <span>${entry.label}</span>
240
- </div>`)}
299
+ </div>`;
300
+ })}
241
301
  </div>`
242
302
  : nothing}
243
303
  </div>
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
@@ -0,0 +1,40 @@
1
+ import { html } from 'lit';
2
+ const legend = [
3
+ { color: '#5b8def', label: 'Low' },
4
+ { color: '#e5484d', label: 'High' },
5
+ ];
6
+ const meta = {
7
+ title: 'Map',
8
+ component: 'lyra-map',
9
+ tags: ['autodocs'],
10
+ parameters: {
11
+ docs: {
12
+ description: {
13
+ component: 'Demoed with a raster OpenStreetMap tile style since a vector style needs an API key.',
14
+ },
15
+ },
16
+ },
17
+ };
18
+ export default meta;
19
+ export const Default = {
20
+ render: () => html `
21
+ <lyra-map
22
+ style="height: 20rem"
23
+ center="[2.3522, 48.8566]"
24
+ zoom="4"
25
+ .legend=${legend}
26
+ .mapStyle=${{
27
+ version: 8,
28
+ sources: {
29
+ demo: {
30
+ type: 'raster',
31
+ tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
32
+ tileSize: 256,
33
+ attribution: '© OpenStreetMap contributors',
34
+ },
35
+ },
36
+ layers: [{ id: 'demo', type: 'raster', source: 'demo' }],
37
+ }}
38
+ ></lyra-map>
39
+ `,
40
+ };
@@ -46,6 +46,12 @@ export class LyraPlayback extends LyraElement {
46
46
  willUpdate(changed) {
47
47
  if (changed.has('hidden') && this.hidden)
48
48
  this.pause();
49
+ // If length is externally reduced to <= 1 while playing, the timer would
50
+ // otherwise keep firing forever — and the play button (the only control
51
+ // that could stop it) becomes ?disabled at that point, leaving no way to
52
+ // stop it from the rendered UI.
53
+ if (changed.has('length') && this.length <= 1)
54
+ this.pause();
49
55
  }
50
56
  /** Start playback; no-op if there's nothing to advance through. */
51
57
  play() {
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
@@ -0,0 +1,10 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Playback',
4
+ component: 'lyra-playback',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Default = {
9
+ render: () => html `<lyra-playback length="10" interval-ms="500" loop></lyra-playback>`,
10
+ };
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Gallery: Story;
@@ -0,0 +1,16 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Skeleton',
4
+ component: 'lyra-skeleton',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Gallery = {
9
+ render: () => html `
10
+ <div style="display:flex; gap:1rem; align-items:center;">
11
+ <lyra-skeleton width="10rem" height="1rem"></lyra-skeleton>
12
+ <lyra-skeleton variant="circle" width="3rem" height="3rem"></lyra-skeleton>
13
+ <lyra-skeleton variant="rect" effect="sheen" width="6rem" height="3rem"></lyra-skeleton>
14
+ </div>
15
+ `,
16
+ };
@@ -25,6 +25,7 @@ export declare class LyraSplit extends LyraElement {
25
25
  private loadPersisted;
26
26
  private persist;
27
27
  private ensureSizes;
28
+ private onSlotChange;
28
29
  private clampPair;
29
30
  private applyDelta;
30
31
  private onPointerDown;
@@ -29,6 +29,10 @@ export class LyraSplit extends LyraElement {
29
29
  this.dragIndex = -1;
30
30
  this.dragStartPos = 0;
31
31
  this.dragStartSizes = [];
32
+ this.onSlotChange = () => {
33
+ this.panelCount = this.children.length;
34
+ this.ensureSizes();
35
+ };
32
36
  this.onPointerDown = (e, index) => {
33
37
  this.dragIndex = index;
34
38
  this.dragStartPos = this.orientation === 'vertical' ? e.clientY : e.clientX;
@@ -86,7 +90,14 @@ export class LyraSplit extends LyraElement {
86
90
  const key = this.storageFullKey;
87
91
  if (!key)
88
92
  return;
89
- const raw = localStorage.getItem(key);
93
+ let raw;
94
+ try {
95
+ raw = localStorage.getItem(key);
96
+ }
97
+ catch {
98
+ /* localStorage unavailable (private browsing, sandboxed iframe, etc.) */
99
+ return;
100
+ }
90
101
  if (!raw)
91
102
  return;
92
103
  try {
@@ -100,8 +111,14 @@ export class LyraSplit extends LyraElement {
100
111
  }
101
112
  persist() {
102
113
  const key = this.storageFullKey;
103
- if (key)
114
+ if (!key)
115
+ return;
116
+ try {
104
117
  localStorage.setItem(key, JSON.stringify(this.sizes));
118
+ }
119
+ catch {
120
+ /* ignore persistence failures (e.g. quota exceeded, private browsing) */
121
+ }
105
122
  }
106
123
  ensureSizes() {
107
124
  if (this.sizes.length === this.panelCount && this.panelCount > 0)
@@ -137,20 +154,24 @@ export class LyraSplit extends LyraElement {
137
154
  render() {
138
155
  const dividers = [];
139
156
  for (let i = 0; i < this.panelCount - 1; i++) {
157
+ // The achievable max for this divider is bounded by its two adjacent
158
+ // panels, not the whole track — pushing past it would starve a panel
159
+ // further down the line even though this pair still has room.
160
+ const valueMax = (this.sizes[i] ?? 0) + (this.sizes[i + 1] ?? 0) - this.min;
140
161
  dividers.push(html `<div
141
162
  part="divider"
142
163
  role="separator"
143
164
  aria-orientation=${this.orientation === 'vertical' ? 'horizontal' : 'vertical'}
144
165
  aria-valuenow=${Math.round(this.sizes[i] ?? 0)}
145
166
  aria-valuemin=${this.min}
146
- aria-valuemax=${100 - this.min}
167
+ aria-valuemax=${Math.round(valueMax)}
147
168
  tabindex="0"
148
169
  style=${`order:${i * 2 + 1}`}
149
170
  @pointerdown=${(e) => this.onPointerDown(e, i)}
150
171
  @keydown=${(e) => this.onDividerKeyDown(e, i)}
151
172
  ></div>`);
152
173
  }
153
- return html `<div part="base"><slot></slot>${dividers}</div>`;
174
+ return html `<div part="base"><slot @slotchange=${this.onSlotChange}></slot>${dividers}</div>`;
154
175
  }
155
176
  }
156
177
  __decorate([
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
@@ -0,0 +1,16 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Split',
4
+ component: 'lyra-split',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Default = {
9
+ render: () => html `
10
+ <lyra-split style="height: 8rem; border: 1px solid #ddd">
11
+ <div style="padding: 0.5rem">Panel A</div>
12
+ <div style="padding: 0.5rem">Panel B</div>
13
+ <div style="padding: 0.5rem">Panel C</div>
14
+ </lyra-split>
15
+ `,
16
+ };
@@ -0,0 +1,5 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Gallery: Story;
@@ -0,0 +1,16 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'Stat',
4
+ component: 'lyra-stat',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Gallery = {
9
+ render: () => html `
10
+ <div style="display:flex; gap:1rem; flex-wrap:wrap;">
11
+ <lyra-stat label="Revenue" value="12.4" unit="k€" trend="3.2" variant="success"></lyra-stat>
12
+ <lyra-stat label="Errors" value="128" trend="-5.1" variant="danger"></lyra-stat>
13
+ <lyra-stat label="Sessions" value="9,204"></lyra-stat>
14
+ </div>
15
+ `,
16
+ };
@@ -0,0 +1,6 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
6
+ export declare const Empty: Story;
@@ -0,0 +1,22 @@
1
+ import { html } from 'lit';
2
+ const rows = [
3
+ { id: 'a', name: 'Alpha', score: 92 },
4
+ { id: 'b', name: 'Beta', score: 81 },
5
+ { id: 'c', name: 'Gamma', score: 76 },
6
+ ];
7
+ const columns = [
8
+ { key: 'name', label: 'Name', sortable: true, cell: (r) => r.name },
9
+ { key: 'score', label: 'Score', sortable: true, align: 'end', cell: (r) => r.score },
10
+ ];
11
+ const meta = {
12
+ title: 'Table',
13
+ component: 'lyra-table',
14
+ tags: ['autodocs'],
15
+ };
16
+ export default meta;
17
+ export const Default = {
18
+ render: () => html `<lyra-table .columns=${columns} .rows=${rows}></lyra-table>`,
19
+ };
20
+ export const Empty = {
21
+ render: () => html `<lyra-table .columns=${columns} .rows=${[]}></lyra-table>`,
22
+ };
@@ -0,0 +1,6 @@
1
+ import type { Meta, StoryObj } from '@storybook/web-components-vite';
2
+ declare const meta: Meta;
3
+ export default meta;
4
+ type Story = StoryObj;
5
+ export declare const Default: Story;
6
+ export declare const Disabled: Story;
@@ -0,0 +1,13 @@
1
+ import { html } from 'lit';
2
+ const meta = {
3
+ title: 'TimeRange',
4
+ component: 'lyra-time-range',
5
+ tags: ['autodocs'],
6
+ };
7
+ export default meta;
8
+ export const Default = {
9
+ render: () => html `<lyra-time-range min="0" max="100" start="20" end="80"></lyra-time-range>`,
10
+ };
11
+ export const Disabled = {
12
+ render: () => html `<lyra-time-range min="0" max="100" start="20" end="80" disabled></lyra-time-range>`,
13
+ };
@@ -32,6 +32,7 @@ export declare class LyraToastItem extends LyraElement {
32
32
  private timer?;
33
33
  private remaining;
34
34
  private startedAt;
35
+ private showRafId?;
35
36
  firstUpdated(): void;
36
37
  disconnectedCallback(): void;
37
38
  private startTimer;
@@ -49,6 +49,16 @@ export class LyraToastItem extends LyraElement {
49
49
  this.remaining = 0;
50
50
  this.startedAt = 0;
51
51
  this.resumeTimer = () => {
52
+ // Guard against an interleaved pointer+focus pause/resume sequence
53
+ // calling resumeTimer() twice without a pauseTimer() in between --
54
+ // without clearing here, the earlier setTimeout is orphaned (not
55
+ // tracked by `this.timer` anymore) and still fires on its own
56
+ // schedule, auto-dismissing the toast early even after it's paused
57
+ // again.
58
+ if (this.timer !== undefined) {
59
+ clearTimeout(this.timer);
60
+ this.timer = undefined;
61
+ }
52
62
  if (!isFinite(this.duration) || this.duration <= 0 || this.remaining <= 0)
53
63
  return;
54
64
  this.startedAt = performance.now();
@@ -66,7 +76,8 @@ export class LyraToastItem extends LyraElement {
66
76
  firstUpdated() {
67
77
  // Assertive for actionable severities, polite otherwise.
68
78
  this.setAttribute('role', this.variant === 'danger' || this.variant === 'warning' ? 'alert' : 'status');
69
- requestAnimationFrame(() => {
79
+ this.showRafId = requestAnimationFrame(() => {
80
+ this.showRafId = undefined;
70
81
  this.setAttribute('data-visible', '');
71
82
  this.emit('lyra-show');
72
83
  window.setTimeout(() => this.emit('lyra-after-show'), ANIM_MS);
@@ -75,6 +86,10 @@ export class LyraToastItem extends LyraElement {
75
86
  }
76
87
  disconnectedCallback() {
77
88
  super.disconnectedCallback();
89
+ if (this.showRafId !== undefined) {
90
+ cancelAnimationFrame(this.showRafId);
91
+ this.showRafId = undefined;
92
+ }
78
93
  this.clearTimer();
79
94
  }
80
95
  startTimer() {