@aceshooting/lyra-ui 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/custom-elements.json +9289 -8457
  3. package/dist/components/chart/chart.d.ts +11 -0
  4. package/dist/components/chart/chart.js +54 -2
  5. package/dist/components/chart/lite-chart.d.ts +9 -0
  6. package/dist/components/chart/lite-chart.js +41 -1
  7. package/dist/components/chart/lite-chart.stories.d.ts +2 -0
  8. package/dist/components/chart/lite-chart.stories.js +16 -0
  9. package/dist/components/combobox/combobox.js +1 -1
  10. package/dist/components/flag/flag.d.ts +26 -5
  11. package/dist/components/flag/flag.js +44 -3
  12. package/dist/components/flag/flag.stories.d.ts +1 -0
  13. package/dist/components/flag/flag.stories.js +14 -0
  14. package/dist/components/heatmap/heatmap.d.ts +15 -3
  15. package/dist/components/heatmap/heatmap.js +24 -7
  16. package/dist/components/heatmap/heatmap.stories.d.ts +5 -0
  17. package/dist/components/heatmap/heatmap.stories.js +25 -0
  18. package/dist/components/select/select.d.ts +3 -0
  19. package/dist/components/select/select.js +6 -42
  20. package/dist/components/select/select.stories.d.ts +2 -0
  21. package/dist/components/select/select.stories.js +16 -0
  22. package/dist/components/select/select.styles.js +26 -2
  23. package/dist/components/stat/stat.d.ts +16 -0
  24. package/dist/components/stat/stat.js +41 -1
  25. package/dist/components/stat/stat.stories.d.ts +1 -0
  26. package/dist/components/stat/stat.stories.js +17 -0
  27. package/dist/components/stat/stat.styles.js +20 -0
  28. package/dist/components/widget/widget.d.ts +10 -0
  29. package/dist/components/widget/widget.js +24 -1
  30. package/dist/components/widget/widget.stories.d.ts +2 -0
  31. package/dist/components/widget/widget.stories.js +33 -0
  32. package/dist/components/widget/widget.styles.js +9 -2
  33. package/dist/components/word-cloud/word-cloud.styles.js +1 -0
  34. package/llms-full.txt +90 -20
  35. package/package.json +3 -3
@@ -77,6 +77,9 @@ export declare class LyraChart extends LyraElement {
77
77
  /** True until the lazy-loaded `chart.js` peer dependency has settled (success or failure). */
78
78
  private loading;
79
79
  private zoomed;
80
+ private visible;
81
+ private intersectionObserver?;
82
+ private lastSignature;
80
83
  private canvasEl?;
81
84
  private chart?;
82
85
  private chartJsModule?;
@@ -95,6 +98,14 @@ export declare class LyraChart extends LyraElement {
95
98
  * is called fresh from `buildConfig()` on every draw rather than cached.
96
99
  */
97
100
  private themeColors;
101
+ /**
102
+ * A content-affecting-properties fingerprint used by `updated()` to skip a
103
+ * redundant `draw()` when neither visibility nor any of these properties
104
+ * actually changed since the last draw (e.g. an unrelated property/state
105
+ * update, or `requestUpdate()` with nothing changed). Mirrors
106
+ * `lite-chart.ts`'s `computeSignature()`.
107
+ */
108
+ private computeSignature;
98
109
  /**
99
110
  * Builds `options.scales` for the effective chart type: no scale at all for
100
111
  * pie/doughnut (a proportional-area chart has no axis), the single radial
@@ -99,6 +99,8 @@ export class LyraChart extends LyraElement {
99
99
  /** True until the lazy-loaded `chart.js` peer dependency has settled (success or failure). */
100
100
  this.loading = true;
101
101
  this.zoomed = false;
102
+ this.visible = true;
103
+ this.lastSignature = '';
102
104
  // `chartjs-plugin-zoom`'s own `resetZoom()` synchronously re-invokes the
103
105
  // `onZoomComplete` callback below as part of its reset, which would emit a
104
106
  // stale `{zoomed: true}` right before `resetZoom()` emits the real
@@ -116,11 +118,21 @@ export class LyraChart extends LyraElement {
116
118
  this.chartJsModule = mod;
117
119
  this.draw();
118
120
  });
121
+ if (typeof IntersectionObserver !== 'undefined') {
122
+ this.intersectionObserver = new IntersectionObserver((entries) => {
123
+ const wasVisible = this.visible;
124
+ this.visible = entries[0]?.isIntersecting ?? true;
125
+ if (this.visible && !wasVisible)
126
+ this.draw();
127
+ });
128
+ this.intersectionObserver.observe(this);
129
+ }
119
130
  }
120
131
  disconnectedCallback() {
121
132
  super.disconnectedCallback();
122
133
  this.chart?.destroy();
123
134
  this.chart = undefined;
135
+ this.intersectionObserver?.disconnect();
124
136
  }
125
137
  updated(changed) {
126
138
  if (this.loading)
@@ -135,9 +147,23 @@ export class LyraChart extends LyraElement {
135
147
  if (changed.has('height')) {
136
148
  this.style.setProperty('--lyra-chart-height', this.height);
137
149
  }
150
+ // While `chart.js` is still loading, `draw()` would no-op anyway (no
151
+ // `chartJsModule`/`canvasEl` yet) — bail before touching `lastSignature`
152
+ // so that phantom "no-op" update doesn't get cached as the baseline and
153
+ // silently swallow the real first draw once loading finishes with no
154
+ // other property having changed in the meantime.
155
+ if (this.loading)
156
+ return;
138
157
  const onlyZoomChanged = changed.size === 1 && changed.has('zoomed');
139
- if (!onlyZoomChanged)
140
- this.draw();
158
+ if (onlyZoomChanged)
159
+ return;
160
+ if (!this.visible)
161
+ return; // becoming visible again triggers its own draw() via the observer above
162
+ const signature = this.computeSignature();
163
+ if (signature === this.lastSignature)
164
+ return;
165
+ this.lastSignature = signature;
166
+ this.draw();
141
167
  }
142
168
  seriesToDataset(s) {
143
169
  const colors = Array.isArray(s.color) ? s.color : s.color ? [s.color] : undefined;
@@ -174,6 +200,29 @@ export class LyraChart extends LyraElement {
174
200
  tooltipText: cs.getPropertyValue('--lyra-chart-tooltip-text').trim() || FALLBACK_TOOLTIP_TEXT,
175
201
  };
176
202
  }
203
+ /**
204
+ * A content-affecting-properties fingerprint used by `updated()` to skip a
205
+ * redundant `draw()` when neither visibility nor any of these properties
206
+ * actually changed since the last draw (e.g. an unrelated property/state
207
+ * update, or `requestUpdate()` with nothing changed). Mirrors
208
+ * `lite-chart.ts`'s `computeSignature()`.
209
+ */
210
+ computeSignature() {
211
+ return JSON.stringify([
212
+ this.type,
213
+ this.labels,
214
+ this.datasets,
215
+ this.legend,
216
+ this.area,
217
+ this.xLabel,
218
+ this.yLabel,
219
+ this.y2Label,
220
+ this.beginAtZero,
221
+ this.horizontal,
222
+ this.stacked,
223
+ this.config,
224
+ ]);
225
+ }
177
226
  /**
178
227
  * Builds `options.scales` for the effective chart type: no scale at all for
179
228
  * pie/doughnut (a proportional-area chart has no axis), the single radial
@@ -428,6 +477,9 @@ __decorate([
428
477
  __decorate([
429
478
  state()
430
479
  ], LyraChart.prototype, "zoomed", void 0);
480
+ __decorate([
481
+ state()
482
+ ], LyraChart.prototype, "visible", void 0);
431
483
  __decorate([
432
484
  query('canvas')
433
485
  ], LyraChart.prototype, "canvasEl", void 0);
@@ -50,22 +50,31 @@ export declare class LyraLiteChart extends LyraElement {
50
50
  beginAtZero: boolean;
51
51
  /** Stacks each category's bars into one segmented bar. Ignored for `type="line"`. */
52
52
  stacked: boolean;
53
+ /** Formats a y-axis tick value for display (e.g. `(v) => \`$${v.toFixed(2)}\``). Falls back to the
54
+ * built-in nice-number formatter when unset. */
55
+ tickFormat?: (value: number) => string;
53
56
  private plotWidth;
54
57
  private plotHeight;
58
+ private visible;
55
59
  private svgEl?;
56
60
  private resizeObserver?;
61
+ private intersectionObserver?;
62
+ private lastSignature;
63
+ private lastResult?;
57
64
  connectedCallback(): void;
58
65
  disconnectedCallback(): void;
59
66
  protected firstUpdated(): void;
60
67
  protected updated(changed: PropertyValues): void;
61
68
  private colorFor;
62
69
  private domain;
70
+ private computeSignature;
63
71
  private emitPoint;
64
72
  private onPointKeyDown;
65
73
  private renderGrid;
66
74
  private renderBars;
67
75
  private renderLines;
68
76
  render(): TemplateResult;
77
+ private renderChart;
69
78
  }
70
79
  declare global {
71
80
  interface HTMLElementTagNameMap {
@@ -106,6 +106,8 @@ export class LyraLiteChart extends LyraElement {
106
106
  this.stacked = false;
107
107
  this.plotWidth = 0;
108
108
  this.plotHeight = 0;
109
+ this.visible = true;
110
+ this.lastSignature = '';
109
111
  }
110
112
  static { this.styles = [LyraElement.styles, styles]; }
111
113
  connectedCallback() {
@@ -124,10 +126,17 @@ export class LyraLiteChart extends LyraElement {
124
126
  }
125
127
  }
126
128
  });
129
+ if (typeof IntersectionObserver !== 'undefined') {
130
+ this.intersectionObserver = new IntersectionObserver((entries) => {
131
+ this.visible = entries[0]?.isIntersecting ?? true;
132
+ });
133
+ this.intersectionObserver.observe(this);
134
+ }
127
135
  }
128
136
  disconnectedCallback() {
129
137
  super.disconnectedCallback();
130
138
  this.resizeObserver?.disconnect();
139
+ this.intersectionObserver?.disconnect();
131
140
  }
132
141
  firstUpdated() {
133
142
  if (this.svgEl)
@@ -179,6 +188,20 @@ export class LyraLiteChart extends LyraElement {
179
188
  }
180
189
  return niceDomain(lo, hi, this.beginAtZero, TICK_COUNT);
181
190
  }
191
+ computeSignature() {
192
+ return JSON.stringify([
193
+ this.type,
194
+ this.labels,
195
+ this.datasets,
196
+ this.legend,
197
+ this.xLabel,
198
+ this.yLabel,
199
+ this.beginAtZero,
200
+ this.stacked,
201
+ this.plotWidth,
202
+ this.plotHeight,
203
+ ]);
204
+ }
182
205
  emitPoint(datasetIndex, index) {
183
206
  const label = this.labels[index];
184
207
  const value = this.datasets[datasetIndex]?.data[index] ?? null;
@@ -196,7 +219,7 @@ export class LyraLiteChart extends LyraElement {
196
219
  const y = plotY + plotH - ((t - lo) / span) * plotH;
197
220
  return svg `
198
221
  <line part="grid-line" x1=${plotX} y1=${y} x2=${plotX + plotW} y2=${y}></line>
199
- <text part="axis-label" x=${plotX - 6} y=${y} text-anchor="end" dominant-baseline="middle">${formatTick(t)}</text>
222
+ <text part="axis-label" x=${plotX - 6} y=${y} text-anchor="end" dominant-baseline="middle">${this.tickFormat ? this.tickFormat(t) : formatTick(t)}</text>
200
223
  `;
201
224
  });
202
225
  }
@@ -294,6 +317,17 @@ export class LyraLiteChart extends LyraElement {
294
317
  });
295
318
  }
296
319
  render() {
320
+ if (!this.visible && this.lastResult)
321
+ return this.lastResult;
322
+ const signature = this.computeSignature();
323
+ if (signature === this.lastSignature && this.lastResult)
324
+ return this.lastResult;
325
+ this.lastSignature = signature;
326
+ const result = this.renderChart();
327
+ this.lastResult = result;
328
+ return result;
329
+ }
330
+ renderChart() {
297
331
  const w = this.plotWidth || 400;
298
332
  const h = this.plotHeight || 200;
299
333
  const padLeft = PAD_LEFT + (this.yLabel ? AXIS_TITLE_SPACE : 0);
@@ -367,12 +401,18 @@ __decorate([
367
401
  __decorate([
368
402
  property({ type: Boolean })
369
403
  ], LyraLiteChart.prototype, "stacked", void 0);
404
+ __decorate([
405
+ property({ attribute: false })
406
+ ], LyraLiteChart.prototype, "tickFormat", void 0);
370
407
  __decorate([
371
408
  state()
372
409
  ], LyraLiteChart.prototype, "plotWidth", void 0);
373
410
  __decorate([
374
411
  state()
375
412
  ], LyraLiteChart.prototype, "plotHeight", void 0);
413
+ __decorate([
414
+ state()
415
+ ], LyraLiteChart.prototype, "visible", void 0);
376
416
  __decorate([
377
417
  query('svg')
378
418
  ], LyraLiteChart.prototype, "svgEl", void 0);
@@ -10,3 +10,5 @@ export declare const StackedBars: Story;
10
10
  export declare const Line: Story;
11
11
  /** Clicking (or Enter/Space on a focused) bar/point fires `lyra-point-click`, same detail shape as `lyra-chart`'s. */
12
12
  export declare const ClickToFilter: Story;
13
+ /** `tickFormat` customizes y-axis tick labels (e.g. currency) instead of the built-in nice-number formatter. */
14
+ export declare const CurrencyTickFormat: Story;
@@ -96,3 +96,19 @@ export const ClickToFilter = {
96
96
  `;
97
97
  },
98
98
  };
99
+ /** `tickFormat` customizes y-axis tick labels (e.g. currency) instead of the built-in nice-number formatter. */
100
+ export const CurrencyTickFormat = {
101
+ render: () => {
102
+ const series = [{ label: 'Revenue', data: [1204.37, 1890.5, 1420.1, 2260.75] }];
103
+ return html `
104
+ <lyra-lite-chart
105
+ type="bar"
106
+ height="16rem"
107
+ style="width: 22rem"
108
+ .labels=${['Q1', 'Q2', 'Q3', 'Q4']}
109
+ .datasets=${series}
110
+ .tickFormat=${(v) => `$${v.toFixed(2)}`}
111
+ ></lyra-lite-chart>
112
+ `;
113
+ },
114
+ };
@@ -549,7 +549,7 @@ export class LyraCombobox extends LyraElement {
549
549
  id=${this.inputId}
550
550
  part="combobox-input"
551
551
  role="combobox"
552
- aria-label=${this.label || this.placeholder || 'Combobox'}
552
+ aria-label=${this.getAttribute('aria-label') || this.label || this.placeholder || 'Combobox'}
553
553
  aria-expanded=${this.open ? 'true' : 'false'}
554
554
  aria-controls=${this.listId}
555
555
  aria-activedescendant=${activeId}
@@ -1,7 +1,9 @@
1
1
  import { type TemplateResult, type PropertyValues } from 'lit';
2
2
  import { LyraElement } from '../../internal/lyra-element.js';
3
3
  import '../skeleton/skeleton.js';
4
- type FlagUrlResolver = (code: string) => Promise<string | undefined>;
4
+ type FlagUrlResolver = (code: string, options?: {
5
+ variant?: 'detailed';
6
+ }) => Promise<string | undefined>;
5
7
  /**
6
8
  * Resolves the optional peer dependency `@aceshooting/lyra-flags`'s `flagUrl`
7
9
  * via the given importer (a real dynamic import by default). Uncached and
@@ -31,10 +33,16 @@ export declare function loadFlagUrl(importFlags?: () => Promise<{
31
33
  * `src` instead to skip the peer-package round trip (and its loading-skeleton
32
34
  * flash) entirely.
33
35
  *
36
+ * A handful of flags (e.g. `es`, `pt` — any whose design includes a detailed coat of
37
+ * arms/seal/emblem) ship a second, full-detail source SVG alongside the default icon-optimized
38
+ * one; set `detailed` to request it (e.g. for a hero-scale display where the extra illustrative
39
+ * detail is actually visible). A no-op for every other code — see `detailed`'s own doc.
40
+ *
34
41
  * @customElement lyra-flag
35
42
  * @example <lyra-flag country="fr"></lyra-flag>
36
43
  * @example <lyra-flag language="en" label="English"></lyra-flag>
37
44
  * @example <lyra-flag src=${frUrl} label="French"></lyra-flag>
45
+ * @example <lyra-flag country="es" detailed></lyra-flag>
38
46
  * @csspart image - The underlying <img>.
39
47
  */
40
48
  export declare class LyraFlag extends LyraElement {
@@ -53,14 +61,27 @@ export declare class LyraFlag extends LyraElement {
53
61
  */
54
62
  src?: string;
55
63
  /**
56
- * Accessible label / `alt` text. Defaults to the uppercase *resolved country
57
- * code* for a `language`-only element (e.g. `language="en"`) that's the
58
- * mapped country (`"GB"`), not the language tag itself (`"EN"`). Has no
59
- * default when only `src` is given (no country/language to derive one from).
64
+ * Accessible label / `alt` text. Defaults to a localized, human-readable
65
+ * region name derived from the *resolved country code* via
66
+ * `Intl.DisplayNames` (e.g. `"United Kingdom"`) for a `language`-only
67
+ * element (e.g. `language="en"`) that's the mapped country's display name,
68
+ * not the language tag itself. Falls back to the bare uppercase code if
69
+ * `Intl.DisplayNames` can't resolve it. Has no default when only `src` is
70
+ * given (no country/language to derive one from).
60
71
  */
61
72
  label?: string;
62
73
  /** Render as a circular flag. */
63
74
  round: boolean;
75
+ /**
76
+ * Requests the pristine, full-detail source SVG instead of the default icon-optimized one —
77
+ * only meaningful for the minority of `country`/`language` codes whose source art was large
78
+ * enough to need optimizing (e.g. `es`, `pt`, a national coat of arms/seal/emblem); for every
79
+ * other code this is a safe no-op (the default and detailed variants are the same file). Has no
80
+ * effect when `src` is set — a pre-resolved URL is used as-is regardless. Intended for rendering
81
+ * a flag larger than icon scale (e.g. a hero display) where the extra illustrative detail is
82
+ * actually visible.
83
+ */
84
+ detailed: boolean;
64
85
  private resolvedSrc?;
65
86
  /** True while the lazy-loaded `@aceshooting/lyra-flags` peer resolver is in flight. */
66
87
  private loading;
@@ -28,6 +28,22 @@ export async function loadFlagUrl(importFlags = () => import('@aceshooting/lyra-
28
28
  return null;
29
29
  }
30
30
  }
31
+ /**
32
+ * Resolves an ISO 3166-1 alpha-2 region code to a human-readable, localized
33
+ * display name (e.g. `'FR'` -> `'France'`) via `Intl.DisplayNames`, for use as
34
+ * the default accessible name (`alt`) instead of a bare code read
35
+ * letter-by-letter by most screen readers. Falls back to the uppercase code
36
+ * itself if `Intl.DisplayNames` throws (unrecognized region) or isn't
37
+ * available in the current runtime.
38
+ */
39
+ function displayNameFor(code) {
40
+ try {
41
+ return new Intl.DisplayNames([navigator.language], { type: 'region' }).of(code.toUpperCase()) ?? code.toUpperCase();
42
+ }
43
+ catch {
44
+ return code.toUpperCase();
45
+ }
46
+ }
31
47
  let flagUrlResolver;
32
48
  /**
33
49
  * Lazily loads the optional peer dependency '@aceshooting/lyra-flags' once per
@@ -59,10 +75,16 @@ function loadFlagUrlResolver() {
59
75
  * `src` instead to skip the peer-package round trip (and its loading-skeleton
60
76
  * flash) entirely.
61
77
  *
78
+ * A handful of flags (e.g. `es`, `pt` — any whose design includes a detailed coat of
79
+ * arms/seal/emblem) ship a second, full-detail source SVG alongside the default icon-optimized
80
+ * one; set `detailed` to request it (e.g. for a hero-scale display where the extra illustrative
81
+ * detail is actually visible). A no-op for every other code — see `detailed`'s own doc.
82
+ *
62
83
  * @customElement lyra-flag
63
84
  * @example <lyra-flag country="fr"></lyra-flag>
64
85
  * @example <lyra-flag language="en" label="English"></lyra-flag>
65
86
  * @example <lyra-flag src=${frUrl} label="French"></lyra-flag>
87
+ * @example <lyra-flag country="es" detailed></lyra-flag>
66
88
  * @csspart image - The underlying <img>.
67
89
  */
68
90
  export class LyraFlag extends LyraElement {
@@ -70,6 +92,16 @@ export class LyraFlag extends LyraElement {
70
92
  super(...arguments);
71
93
  /** Render as a circular flag. */
72
94
  this.round = false;
95
+ /**
96
+ * Requests the pristine, full-detail source SVG instead of the default icon-optimized one —
97
+ * only meaningful for the minority of `country`/`language` codes whose source art was large
98
+ * enough to need optimizing (e.g. `es`, `pt`, a national coat of arms/seal/emblem); for every
99
+ * other code this is a safe no-op (the default and detailed variants are the same file). Has no
100
+ * effect when `src` is set — a pre-resolved URL is used as-is regardless. Intended for rendering
101
+ * a flag larger than icon scale (e.g. a hero display) where the extra illustrative detail is
102
+ * actually visible.
103
+ */
104
+ this.detailed = false;
73
105
  /** True while the lazy-loaded `@aceshooting/lyra-flags` peer resolver is in flight. */
74
106
  this.loading = true;
75
107
  /**
@@ -96,8 +128,13 @@ export class LyraFlag extends LyraElement {
96
128
  // undefined -> undefined (no-op to Lit), so none of `changed.has(...)`
97
129
  // would otherwise ever become true for a `<lyra-flag>` that never
98
130
  // receives one, leaving `loading` stuck at its initial `true` forever.
99
- if (this.hasUpdated && !changed.has('country') && !changed.has('language') && !changed.has('src'))
131
+ if (this.hasUpdated &&
132
+ !changed.has('country') &&
133
+ !changed.has('language') &&
134
+ !changed.has('src') &&
135
+ !changed.has('detailed')) {
100
136
  return;
137
+ }
101
138
  const token = ++this.resolveToken; // invalidates any in-flight peer resolution either way
102
139
  if (this.src) {
103
140
  this.resolvedSrc = undefined;
@@ -112,7 +149,7 @@ export class LyraFlag extends LyraElement {
112
149
  }
113
150
  this.loading = true;
114
151
  void loadFlagUrlResolver()
115
- .then((resolve) => resolve?.(code))
152
+ .then((resolve) => resolve?.(code, this.detailed ? { variant: 'detailed' } : undefined))
116
153
  .then((url) => {
117
154
  if (token !== this.resolveToken)
118
155
  return; // superseded by a later country/language/src change
@@ -132,7 +169,8 @@ export class LyraFlag extends LyraElement {
132
169
  const url = this.src ?? this.resolvedSrc;
133
170
  if (!url)
134
171
  return html ``;
135
- const alt = this.label ?? (this.code ?? '').toUpperCase();
172
+ const code = this.code;
173
+ const alt = this.label ?? (code ? displayNameFor(code) : '');
136
174
  return html `<img part="image" src=${url} alt=${alt} loading="lazy" decoding="async" />`;
137
175
  }
138
176
  }
@@ -151,6 +189,9 @@ __decorate([
151
189
  __decorate([
152
190
  property({ type: Boolean, reflect: true })
153
191
  ], LyraFlag.prototype, "round", void 0);
192
+ __decorate([
193
+ property({ type: Boolean, reflect: true })
194
+ ], LyraFlag.prototype, "detailed", void 0);
154
195
  __decorate([
155
196
  state()
156
197
  ], LyraFlag.prototype, "resolvedSrc", void 0);
@@ -3,3 +3,4 @@ declare const meta: Meta;
3
3
  export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Gallery: Story;
6
+ export declare const DetailedVariant: Story;
@@ -15,3 +15,17 @@ export const Gallery = {
15
15
  </div>
16
16
  `,
17
17
  };
18
+ export const DetailedVariant = {
19
+ render: () => html `
20
+ <div style="display:flex; gap:2rem; align-items:center;">
21
+ <div style="display:flex; flex-direction:column; align-items:center; gap:0.5rem;">
22
+ <lyra-flag country="es" label="Spain (default, icon-optimized)" style="height: 6rem"></lyra-flag>
23
+ <span>default</span>
24
+ </div>
25
+ <div style="display:flex; flex-direction:column; align-items:center; gap:0.5rem;">
26
+ <lyra-flag country="es" detailed label="Spain (detailed)" style="height: 6rem"></lyra-flag>
27
+ <span>detailed</span>
28
+ </div>
29
+ </div>
30
+ `,
31
+ };
@@ -82,7 +82,9 @@ export declare function mixColor(fromColor: string, toColor: string, t: number):
82
82
  * @customElement lyra-heatmap
83
83
  * @event lyra-cell-click - Fired on click, or Enter/Space on the
84
84
  * focused/hovered cell. `detail: { row, col, value }` in matrix mode,
85
- * `detail: { date, value }` in calendar mode.
85
+ * `detail: { date, value }` in calendar mode. `cellText` overrides the
86
+ * built-in English "Row X, Col Y: value" / "Mon DD: value" template used for
87
+ * both the hover tooltip and the keyboard live-region announcement.
86
88
  * @csspart base, canvas, tooltip, live-region, legend, legend-lo, legend-hi, legend-annotation
87
89
  */
88
90
  export declare class LyraHeatmap extends LyraElement {
@@ -107,6 +109,10 @@ export declare class LyraHeatmap extends LyraElement {
107
109
  bucketCount: number;
108
110
  /** Cells to ring-highlight — `row`/`col` in matrix mode, `date` in calendar mode. See `HeatmapAnnotation`. */
109
111
  annotations: HeatmapAnnotation[];
112
+ /** Formats the per-cell tooltip and keyboard live-region text — receives the cell position
113
+ * (`MatrixCellPos` in matrix mode, `CalendarCellPos` in calendar mode) and its value. Falls back to
114
+ * the built-in English "Row X, Col Y: value" / "Mon DD: value" template when unset. */
115
+ cellText?: (pos: MatrixCellPos | CalendarCellPos, value: number) => string;
110
116
  private canvas?;
111
117
  private resizeObserver?;
112
118
  private dprQuery?;
@@ -185,11 +191,17 @@ export declare class LyraHeatmap extends LyraElement {
185
191
  /**
186
192
  * Human-readable "<label>: <value>" text for a cell — shared by the hover
187
193
  * tooltip and the keyboard-focus live-region announcement, so both always
188
- * describe a cell the same way.
194
+ * describe a cell the same way. This is the built-in English fallback used
195
+ * when `cellText` isn't set — see `resolveCellText()`.
189
196
  */
190
- private cellText;
197
+ private defaultCellText;
191
198
  private matrixCellText;
192
199
  private calendarCellText;
200
+ /** The raw numeric value at a cell position, in either mode — shared by `resolveCellText()`
201
+ * so a custom `cellText` formatter receives the same value the built-in template would use. */
202
+ private valueAt;
203
+ /** Dispatches to the host-provided `cellText` formatter when set, otherwise the built-in template. */
204
+ private resolveCellText;
193
205
  /** Refreshes the visually-hidden live-region text for a newly-focused cell. */
194
206
  private announce;
195
207
  private emitCellClick;
@@ -170,7 +170,9 @@ export function mixColor(fromColor, toColor, t) {
170
170
  * @customElement lyra-heatmap
171
171
  * @event lyra-cell-click - Fired on click, or Enter/Space on the
172
172
  * focused/hovered cell. `detail: { row, col, value }` in matrix mode,
173
- * `detail: { date, value }` in calendar mode.
173
+ * `detail: { date, value }` in calendar mode. `cellText` overrides the
174
+ * built-in English "Row X, Col Y: value" / "Mon DD: value" template used for
175
+ * both the hover tooltip and the keyboard live-region announcement.
174
176
  * @csspart base, canvas, tooltip, live-region, legend, legend-lo, legend-hi, legend-annotation
175
177
  */
176
178
  export class LyraHeatmap extends LyraElement {
@@ -270,7 +272,7 @@ export class LyraHeatmap extends LyraElement {
270
272
  this.cachedValueRange = this.computeValueRange();
271
273
  const bounds = this.cachedValueRange;
272
274
  const range = bounds ? `${bounds[0]}–${bounds[1]}` : 'no data';
273
- this.setAttribute('role', 'img');
275
+ this.setAttribute('role', 'group');
274
276
  if (this.mode === 'calendar') {
275
277
  this.setAttribute('aria-label', `Calendar heatmap of ${this.days.length} days, ${this.valueLabel} range ${range}`);
276
278
  }
@@ -579,9 +581,10 @@ export class LyraHeatmap extends LyraElement {
579
581
  /**
580
582
  * Human-readable "<label>: <value>" text for a cell — shared by the hover
581
583
  * tooltip and the keyboard-focus live-region announcement, so both always
582
- * describe a cell the same way.
584
+ * describe a cell the same way. This is the built-in English fallback used
585
+ * when `cellText` isn't set — see `resolveCellText()`.
583
586
  */
584
- cellText(pos) {
587
+ defaultCellText(pos) {
585
588
  return 'week' in pos ? this.calendarCellText(pos) : this.matrixCellText(pos);
586
589
  }
587
590
  matrixCellText(pos) {
@@ -593,13 +596,24 @@ export class LyraHeatmap extends LyraElement {
593
596
  }
594
597
  calendarCellText(pos) {
595
598
  const { date, value } = this.calendarCellAt(pos);
596
- const label = parseIsoDate(date).toLocaleString('en', { month: 'short', day: 'numeric', timeZone: 'UTC' });
599
+ const label = parseIsoDate(date).toLocaleString(undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' });
597
600
  const valueText = value < 0 || !Number.isFinite(value) ? 'no data' : String(value);
598
601
  return `${label}: ${valueText}`;
599
602
  }
603
+ /** The raw numeric value at a cell position, in either mode — shared by `resolveCellText()`
604
+ * so a custom `cellText` formatter receives the same value the built-in template would use. */
605
+ valueAt(pos) {
606
+ if ('week' in pos)
607
+ return this.calendarCellAt(pos).value;
608
+ return this.values[pos.row]?.[pos.col] ?? -1;
609
+ }
610
+ /** Dispatches to the host-provided `cellText` formatter when set, otherwise the built-in template. */
611
+ resolveCellText(pos) {
612
+ return this.cellText ? this.cellText(pos, this.valueAt(pos)) : this.defaultCellText(pos);
613
+ }
600
614
  /** Refreshes the visually-hidden live-region text for a newly-focused cell. */
601
615
  announce(pos) {
602
- this.liveText = this.cellText(pos);
616
+ this.liveText = this.resolveCellText(pos);
603
617
  }
604
618
  emitCellClick(pos) {
605
619
  if ('week' in pos) {
@@ -703,7 +717,7 @@ export class LyraHeatmap extends LyraElement {
703
717
  ?hidden=${!this.hoverCell}
704
718
  style=${styleMap(this.hoverCell ? this.tooltipStyle(this.hoverCell) : {})}
705
719
  >
706
- ${this.hoverCell ? this.cellText(this.hoverCell) : ''}
720
+ ${this.hoverCell ? this.resolveCellText(this.hoverCell) : ''}
707
721
  </div>
708
722
  <div part="live-region" class="sr-only" role="status" aria-live="polite">${this.liveText}</div>
709
723
  <div part="legend">
@@ -750,6 +764,9 @@ __decorate([
750
764
  __decorate([
751
765
  property({ attribute: false })
752
766
  ], LyraHeatmap.prototype, "annotations", void 0);
767
+ __decorate([
768
+ property({ attribute: false })
769
+ ], LyraHeatmap.prototype, "cellText", void 0);
753
770
  __decorate([
754
771
  query('canvas')
755
772
  ], LyraHeatmap.prototype, "canvas", void 0);
@@ -18,5 +18,10 @@ export declare const HoverFocusClick: Story;
18
18
  * also surface a swatch + text entry in the legend.
19
19
  */
20
20
  export declare const Annotations: Story;
21
+ /**
22
+ * `cellText` overrides the built-in English tooltip/live-region template —
23
+ * here with a French translation — for both matrix and calendar modes.
24
+ */
25
+ export declare const CustomCellText: Story;
21
26
  /** `annotations` in calendar mode match by ISO `date` instead of `row`/`col`. */
22
27
  export declare const CalendarAnnotations: Story;
@@ -131,6 +131,31 @@ export const Annotations = {
131
131
  ></lyra-heatmap>
132
132
  `,
133
133
  };
134
+ /**
135
+ * `cellText` overrides the built-in English tooltip/live-region template —
136
+ * here with a French translation — for both matrix and calendar modes.
137
+ */
138
+ export const CustomCellText = {
139
+ render: () => html `
140
+ <lyra-heatmap
141
+ cell-size="28"
142
+ value-label="évènements"
143
+ .rowLabels=${['Lun', 'Mar', 'Mer']}
144
+ .colLabels=${['0h', '6h', '12h']}
145
+ .values=${[
146
+ [1, 4, 9],
147
+ [0, 2, 6],
148
+ [-1, 1, 4],
149
+ ]}
150
+ .cellText=${(pos, value) => {
151
+ const rows = ['Lun', 'Mar', 'Mer'];
152
+ const cols = ['0h', '6h', '12h'];
153
+ const valueText = value < 0 ? 'aucune donnée' : String(value);
154
+ return `Ligne ${rows[pos.row]}, Col ${cols[pos.col]} : ${valueText}`;
155
+ }}
156
+ ></lyra-heatmap>
157
+ `,
158
+ };
134
159
  /** `annotations` in calendar mode match by ISO `date` instead of `row`/`col`. */
135
160
  export const CalendarAnnotations = {
136
161
  render: () => {
@@ -42,6 +42,7 @@ import '../combobox/option.js';
42
42
  * @csspart error - The error message.
43
43
  * @csspart hint - The hint message.
44
44
  */
45
+ export type LyraSelectSize = 'xs' | 's' | 'm' | 'l' | 'xl';
45
46
  export declare class LyraSelect extends LyraElement {
46
47
  static formAssociated: boolean;
47
48
  static styles: import("lit").CSSResultGroup[];
@@ -58,6 +59,8 @@ export declare class LyraSelect extends LyraElement {
58
59
  hint: string;
59
60
  errorText: string;
60
61
  open: boolean;
62
+ /** Visual size — same `xs`–`xl` scale as `lyra-toast-item`'s `size`. */
63
+ size: LyraSelectSize;
61
64
  private activeIndex;
62
65
  private options;
63
66
  private touched;