@aceshooting/lyra-ui 1.0.0 → 1.1.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 (148) hide show
  1. package/README.md +8 -9
  2. package/custom-elements.json +10463 -4519
  3. package/dist/components/chart/chart.d.ts +46 -1
  4. package/dist/components/chart/chart.js +148 -10
  5. package/dist/components/chart/chart.stories.d.ts +32 -0
  6. package/dist/components/chart/chart.stories.js +141 -0
  7. package/dist/components/chart/chart.styles.js +16 -0
  8. package/dist/components/chart/histogram.d.ts +2 -0
  9. package/dist/components/chart/histogram.js +18 -2
  10. package/dist/components/chart/line-chart.stories.d.ts +17 -0
  11. package/dist/components/chart/line-chart.stories.js +75 -0
  12. package/dist/components/chart/lite-chart.d.ts +74 -0
  13. package/dist/components/chart/lite-chart.js +384 -0
  14. package/dist/components/chart/lite-chart.stories.d.ts +12 -0
  15. package/dist/components/chart/lite-chart.stories.js +98 -0
  16. package/dist/components/chart/lite-chart.styles.d.ts +1 -0
  17. package/dist/components/chart/lite-chart.styles.js +83 -0
  18. package/dist/components/combobox/combobox.d.ts +9 -0
  19. package/dist/components/combobox/combobox.js +75 -12
  20. package/dist/components/combobox/combobox.stories.d.ts +18 -0
  21. package/dist/components/combobox/combobox.stories.js +75 -0
  22. package/dist/components/date-picker/calendar-core.js +13 -2
  23. package/dist/components/date-picker/date-input.d.ts +9 -1
  24. package/dist/components/date-picker/date-input.js +55 -7
  25. package/dist/components/date-picker/date-input.stories.d.ts +5 -0
  26. package/dist/components/date-picker/date-input.stories.js +36 -0
  27. package/dist/components/date-picker/date-picker.d.ts +11 -3
  28. package/dist/components/date-picker/date-picker.js +87 -37
  29. package/dist/components/date-picker/date-picker.stories.d.ts +4 -0
  30. package/dist/components/date-picker/date-picker.stories.js +12 -0
  31. package/dist/components/date-picker/date-picker.styles.js +1 -1
  32. package/dist/components/empty/empty.d.ts +12 -1
  33. package/dist/components/empty/empty.js +20 -3
  34. package/dist/components/empty/empty.stories.d.ts +2 -0
  35. package/dist/components/empty/empty.stories.js +20 -0
  36. package/dist/components/empty/empty.styles.js +11 -0
  37. package/dist/components/export-button/csv.js +6 -2
  38. package/dist/components/export-button/export-button.d.ts +15 -1
  39. package/dist/components/export-button/export-button.js +89 -12
  40. package/dist/components/export-button/export-button.stories.d.ts +1 -0
  41. package/dist/components/export-button/export-button.stories.js +5 -0
  42. package/dist/components/export-button/export-button.styles.js +11 -3
  43. package/dist/components/file-input/accept.d.ts +8 -2
  44. package/dist/components/file-input/accept.js +9 -3
  45. package/dist/components/file-input/file-input.d.ts +5 -2
  46. package/dist/components/file-input/file-input.js +23 -8
  47. package/dist/components/file-input/file-input.stories.d.ts +2 -0
  48. package/dist/components/file-input/file-input.stories.js +9 -0
  49. package/dist/components/flag/flag.d.ts +48 -3
  50. package/dist/components/flag/flag.js +65 -29
  51. package/dist/components/flag/flag.styles.js +1 -1
  52. package/dist/components/flag/language-map.d.ts +11 -0
  53. package/dist/components/flag/language-map.js +12 -1
  54. package/dist/components/gauge/gauge.d.ts +2 -0
  55. package/dist/components/gauge/gauge.js +19 -5
  56. package/dist/components/gauge/gauge.stories.d.ts +2 -0
  57. package/dist/components/gauge/gauge.stories.js +6 -0
  58. package/dist/components/graph/graph-loader.d.ts +28 -0
  59. package/dist/components/graph/graph-loader.js +44 -0
  60. package/dist/components/graph/graph.d.ts +41 -15
  61. package/dist/components/graph/graph.js +146 -71
  62. package/dist/components/graph/graph.stories.d.ts +3 -0
  63. package/dist/components/graph/graph.stories.js +38 -0
  64. package/dist/components/graph/graph.styles.js +6 -1
  65. package/dist/components/heatmap/calendar-grid.d.ts +7 -0
  66. package/dist/components/heatmap/calendar-grid.js +27 -10
  67. package/dist/components/heatmap/heatmap.d.ts +137 -12
  68. package/dist/components/heatmap/heatmap.js +427 -27
  69. package/dist/components/heatmap/heatmap.stories.d.ts +17 -0
  70. package/dist/components/heatmap/heatmap.stories.js +130 -0
  71. package/dist/components/heatmap/heatmap.styles.js +60 -2
  72. package/dist/components/map/map.d.ts +17 -1
  73. package/dist/components/map/map.js +68 -21
  74. package/dist/components/map/map.stories.d.ts +10 -0
  75. package/dist/components/map/map.stories.js +107 -12
  76. package/dist/components/map/map.styles.js +2 -2
  77. package/dist/components/playback/playback.d.ts +6 -2
  78. package/dist/components/playback/playback.js +36 -12
  79. package/dist/components/playback/playback.stories.d.ts +2 -0
  80. package/dist/components/playback/playback.stories.js +6 -0
  81. package/dist/components/playback/playback.styles.js +10 -2
  82. package/dist/components/select/select.d.ts +122 -0
  83. package/dist/components/select/select.js +516 -0
  84. package/dist/components/select/select.stories.d.ts +19 -0
  85. package/dist/components/select/select.stories.js +75 -0
  86. package/dist/components/select/select.styles.d.ts +1 -0
  87. package/dist/components/select/select.styles.js +188 -0
  88. package/dist/components/skeleton/skeleton.js +4 -7
  89. package/dist/components/sparkline/sparkline.d.ts +1 -1
  90. package/dist/components/sparkline/sparkline.js +40 -13
  91. package/dist/components/sparkline/sparkline.stories.d.ts +4 -0
  92. package/dist/components/sparkline/sparkline.stories.js +22 -0
  93. package/dist/components/split/split.d.ts +24 -3
  94. package/dist/components/split/split.js +155 -31
  95. package/dist/components/split/split.stories.d.ts +2 -0
  96. package/dist/components/split/split.stories.js +20 -0
  97. package/dist/components/stat/stat.d.ts +27 -1
  98. package/dist/components/stat/stat.js +56 -3
  99. package/dist/components/stat/stat.stories.d.ts +3 -0
  100. package/dist/components/stat/stat.stories.js +49 -1
  101. package/dist/components/stat/stat.styles.js +44 -0
  102. package/dist/components/table/table.d.ts +64 -2
  103. package/dist/components/table/table.js +213 -17
  104. package/dist/components/table/table.stories.d.ts +6 -0
  105. package/dist/components/table/table.stories.js +45 -0
  106. package/dist/components/table/table.styles.js +51 -4
  107. package/dist/components/time-range/time-range.d.ts +34 -5
  108. package/dist/components/time-range/time-range.js +217 -36
  109. package/dist/components/time-range/time-range.stories.d.ts +2 -0
  110. package/dist/components/time-range/time-range.stories.js +13 -0
  111. package/dist/components/time-range/time-range.styles.js +44 -2
  112. package/dist/components/toast/toast-item.d.ts +14 -2
  113. package/dist/components/toast/toast-item.js +134 -17
  114. package/dist/components/toast/toast-item.styles.js +28 -2
  115. package/dist/components/toast/toast.stories.d.ts +3 -0
  116. package/dist/components/toast/toast.stories.js +41 -1
  117. package/dist/components/toast/toast.styles.js +11 -11
  118. package/dist/components/toast/toaster.d.ts +1 -1
  119. package/dist/components/toast/toaster.js +13 -6
  120. package/dist/components/tree/tree-node.d.ts +13 -7
  121. package/dist/components/tree/tree-node.js +24 -10
  122. package/dist/components/tree/tree.d.ts +64 -15
  123. package/dist/components/tree/tree.js +113 -22
  124. package/dist/components/tree/tree.stories.d.ts +4 -0
  125. package/dist/components/tree/tree.stories.js +31 -1
  126. package/dist/components/tree/update-cascade.d.ts +13 -0
  127. package/dist/components/tree/update-cascade.js +13 -0
  128. package/dist/components/widget/widget.d.ts +15 -1
  129. package/dist/components/widget/widget.js +140 -5
  130. package/dist/components/widget/widget.stories.d.ts +1 -0
  131. package/dist/components/widget/widget.stories.js +10 -0
  132. package/dist/components/widget/widget.styles.js +11 -1
  133. package/dist/components/word-cloud/word-cloud-layout.d.ts +63 -0
  134. package/dist/components/word-cloud/word-cloud-layout.js +145 -0
  135. package/dist/components/word-cloud/word-cloud.d.ts +65 -0
  136. package/dist/components/word-cloud/word-cloud.js +255 -0
  137. package/dist/components/word-cloud/word-cloud.stories.d.ts +10 -0
  138. package/dist/components/word-cloud/word-cloud.stories.js +55 -0
  139. package/dist/components/word-cloud/word-cloud.styles.d.ts +1 -0
  140. package/dist/components/word-cloud/word-cloud.styles.js +54 -0
  141. package/dist/internal/a11y.js +2 -1
  142. package/dist/internal/rtl.d.ts +7 -0
  143. package/dist/internal/rtl.js +9 -0
  144. package/dist/lyra.d.ts +9 -1
  145. package/dist/lyra.js +6 -0
  146. package/llms-full.txt +892 -276
  147. package/llms.txt +12 -9
  148. package/package.json +6 -4
@@ -22,3 +22,133 @@ export const Default = {
22
22
  ></lyra-heatmap>
23
23
  `,
24
24
  };
25
+ export const SqrtScaleFitToWidth = {
26
+ render: () => html `
27
+ <lyra-heatmap
28
+ scale="sqrt"
29
+ fit-to-width
30
+ value-label="requests"
31
+ .rowLabels=${['Mon', 'Tue', 'Wed']}
32
+ .colLabels=${['0h', '6h', '12h', '18h']}
33
+ .values=${[
34
+ [1, 8, 400, 20],
35
+ [0, 2, 6, 3],
36
+ [-1, 1, 4, 7],
37
+ ]}
38
+ ></lyra-heatmap>
39
+ `,
40
+ };
41
+ export const CalendarMode = {
42
+ render: () => {
43
+ const days = [];
44
+ const start = Date.UTC(2026, 0, 1);
45
+ for (let i = 0; i < 120; i++) {
46
+ const date = new Date(start + i * 86_400_000).toISOString().slice(0, 10);
47
+ const value = Math.round(Math.abs(Math.sin(i / 6)) * 10);
48
+ days.push({ date, value });
49
+ }
50
+ return html `
51
+ <lyra-heatmap mode="calendar" value-label="commits" bucket-count="5" .days=${days}></lyra-heatmap>
52
+ `;
53
+ },
54
+ };
55
+ export const CustomTheme = {
56
+ render: () => html `
57
+ <lyra-heatmap
58
+ style="
59
+ --lyra-heatmap-scale-lo: #fde7e9;
60
+ --lyra-heatmap-scale-hi: #b3122f;
61
+ --lyra-heatmap-no-data-fill: rgba(0, 0, 0, 0.08);
62
+ --lyra-heatmap-label-font: 11px monospace;
63
+ "
64
+ cell-size="24"
65
+ value-label="events"
66
+ .rowLabels=${['Mon', 'Tue', 'Wed']}
67
+ .colLabels=${['0h', '6h', '12h']}
68
+ .values=${[
69
+ [1, 4, 9],
70
+ [0, 2, 6],
71
+ [-1, 1, 4],
72
+ ]}
73
+ ></lyra-heatmap>
74
+ `,
75
+ };
76
+ /**
77
+ * Hover a cell for its tooltip, tab to the grid and use the arrow keys to
78
+ * move the focus ring (a visually-hidden live region announces each move),
79
+ * and click or press Enter/Space on a cell to see its `lyra-cell-click`
80
+ * detail logged below.
81
+ */
82
+ export const HoverFocusClick = {
83
+ render: () => {
84
+ const onCellClick = (e) => {
85
+ const detail = e.detail;
86
+ const out = document.getElementById('cell-click-out');
87
+ if (out)
88
+ out.textContent = JSON.stringify(detail);
89
+ };
90
+ return html `
91
+ <lyra-heatmap
92
+ cell-size="28"
93
+ value-label="events"
94
+ .rowLabels=${['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}
95
+ .colLabels=${['0h', '6h', '12h', '18h']}
96
+ .values=${[
97
+ [1, 4, 9, 2],
98
+ [0, 2, 6, 3],
99
+ [5, 8, 3, 1],
100
+ [-1, 1, 4, 7],
101
+ [2, 3, 5, 6],
102
+ ]}
103
+ @lyra-cell-click=${onCellClick}
104
+ ></lyra-heatmap>
105
+ <p>Last <code>lyra-cell-click</code> detail: <code id="cell-click-out">(none yet)</code></p>
106
+ `;
107
+ },
108
+ };
109
+ /**
110
+ * Cells flagged via `annotations` get a stroked ring; ones with a `label`
111
+ * also surface a swatch + text entry in the legend.
112
+ */
113
+ export const Annotations = {
114
+ render: () => html `
115
+ <lyra-heatmap
116
+ cell-size="24"
117
+ value-label="events"
118
+ .rowLabels=${['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}
119
+ .colLabels=${['0h', '6h', '12h', '18h']}
120
+ .values=${[
121
+ [1, 4, 9, 2],
122
+ [0, 2, 6, 3],
123
+ [5, 8, 3, 1],
124
+ [-1, 1, 4, 7],
125
+ [2, 3, 5, 6],
126
+ ]}
127
+ .annotations=${[
128
+ { row: 0, col: 2, label: 'Peak load' },
129
+ { row: 2, col: 1, label: 'Incident' },
130
+ ]}
131
+ ></lyra-heatmap>
132
+ `,
133
+ };
134
+ /** `annotations` in calendar mode match by ISO `date` instead of `row`/`col`. */
135
+ export const CalendarAnnotations = {
136
+ render: () => {
137
+ const days = [];
138
+ const start = Date.UTC(2026, 0, 1);
139
+ for (let i = 0; i < 120; i++) {
140
+ const date = new Date(start + i * 86_400_000).toISOString().slice(0, 10);
141
+ const value = Math.round(Math.abs(Math.sin(i / 6)) * 10);
142
+ days.push({ date, value });
143
+ }
144
+ return html `
145
+ <lyra-heatmap
146
+ mode="calendar"
147
+ value-label="commits"
148
+ bucket-count="5"
149
+ .days=${days}
150
+ .annotations=${[{ date: '2026-02-14', label: 'Release day' }]}
151
+ ></lyra-heatmap>
152
+ `;
153
+ },
154
+ };
@@ -4,8 +4,8 @@ export const styles = css `
4
4
  display: block;
5
5
  /* Sequential ramp endpoints for the data-driven cell colors (canvas can't
6
6
  consume var() directly, so heatmap.ts resolves these via
7
- getComputedStyle and interpolates between them). Componentspecific
8
- business color per Global Constraints — exposed so hosts can retheme
7
+ getComputedStyle and interpolates between them). Component-specific
8
+ business color — exposed so hosts can retheme
9
9
  the ramp without raw hex leaking into the public API. */
10
10
  --lyra-heatmap-scale-lo: #cde2fb;
11
11
  --lyra-heatmap-scale-hi: #0969da;
@@ -13,8 +13,34 @@ export const styles = css `
13
13
  getComputedStyle pattern as the ramp endpoints above, so hosts can
14
14
  retheme it instead of it being a hardcoded literal in heatmap.ts. */
15
15
  --lyra-heatmap-no-data-fill: rgba(128, 128, 128, 0.25);
16
+ /* Canvas-drawn axis/month/weekday label font — same resolve-via-
17
+ getComputedStyle pattern as the ramp endpoints above (canvas can't
18
+ consume var() directly). */
19
+ --lyra-heatmap-label-font: 10px sans-serif;
20
+ /* [part="tooltip"] is a real DOM element (not canvas-drawn), so unlike
21
+ the tokens above it consumes these var()s directly — no getComputedStyle
22
+ resolution needed. Own tokens (not the bare --lyra-color-surface/-text)
23
+ so a host can retheme just the heatmap tooltip, same rationale as
24
+ chart.ts's --lyra-chart-tooltip-bg/-text. */
25
+ --lyra-heatmap-tooltip-bg: var(--lyra-color-surface);
26
+ --lyra-heatmap-tooltip-text: var(--lyra-color-text);
27
+ /* Canvas-drawn focus-ring stroke around the keyboard-focused cell — same
28
+ resolve-via-getComputedStyle pattern as the ramp endpoints (canvas
29
+ can't consume var() directly). A dedicated token (rather than reusing
30
+ --lyra-focus-ring-color straight from tokens.styles.ts) so a host can
31
+ retheme the in-canvas ring independently of every other :focus-visible
32
+ outline in the library, while defaulting to that same brand color —
33
+ also reused by the [part="canvas"]:focus-visible outline below so the
34
+ two stay visually in sync. */
35
+ --lyra-heatmap-focus-ring-color: var(--lyra-focus-ring-color);
36
+ /* Canvas-drawn ring around annotated cells — same resolve-via-
37
+ getComputedStyle pattern. Defaults to --lyra-color-danger (a loud,
38
+ attention-grabbing color distinct from the sequential data ramp) so an
39
+ annotation reads clearly against any point on that ramp. */
40
+ --lyra-heatmap-annotation-color: var(--lyra-color-danger);
16
41
  }
17
42
  [part='base'] {
43
+ position: relative;
18
44
  display: flex;
19
45
  flex-direction: column;
20
46
  gap: var(--lyra-space-xs);
@@ -23,6 +49,27 @@ export const styles = css `
23
49
  display: block;
24
50
  inline-size: 100%;
25
51
  }
52
+ [part='canvas']:focus-visible {
53
+ outline: var(--lyra-focus-ring-width) solid var(--lyra-heatmap-focus-ring-color);
54
+ outline-offset: var(--lyra-focus-ring-offset);
55
+ }
56
+ [part='tooltip'] {
57
+ position: absolute;
58
+ transform: translate(-50%, -100%);
59
+ margin-block-start: -6px;
60
+ padding: 2px 6px;
61
+ border-radius: var(--lyra-radius);
62
+ background: var(--lyra-heatmap-tooltip-bg);
63
+ color: var(--lyra-heatmap-tooltip-text);
64
+ font-size: 0.75rem;
65
+ white-space: nowrap;
66
+ box-shadow: var(--lyra-shadow);
67
+ pointer-events: none;
68
+ z-index: 1;
69
+ }
70
+ [part='tooltip'][hidden] {
71
+ display: none;
72
+ }
26
73
  [part='legend'] {
27
74
  display: flex;
28
75
  align-items: center;
@@ -40,4 +87,15 @@ export const styles = css `
40
87
  var(--lyra-heatmap-scale-hi)
41
88
  );
42
89
  }
90
+ [part='legend-annotation'] {
91
+ display: inline-flex;
92
+ align-items: center;
93
+ gap: 3px;
94
+ }
95
+ [part='legend-annotation'] .ring-swatch {
96
+ inline-size: 0.6rem;
97
+ block-size: 0.6rem;
98
+ border-radius: 50%;
99
+ border: 2px solid var(--lyra-heatmap-annotation-color);
100
+ }
43
101
  `;
@@ -6,10 +6,20 @@ export interface LegendEntry {
6
6
  color: string;
7
7
  label: string;
8
8
  }
9
+ /**
10
+ * Declarative GeoJSON fill layer, colored by interpolating `field`'s value
11
+ * across `stops`.
12
+ */
9
13
  export interface ChoroplethLayer {
10
14
  sourceId: string;
11
15
  geojson: GeoJSON.FeatureCollection;
12
16
  field: string;
17
+ /**
18
+ * `[value, color]` pairs, ascending by `value`, fed to a maplibre-gl
19
+ * `interpolate` expression. Must contain at least one pair -- an empty
20
+ * array can't build a valid expression, so it's ignored (the existing fill
21
+ * layer, if any, is left as-is) rather than applied.
22
+ */
13
23
  stops: [number, string][];
14
24
  }
15
25
  export interface MapMarker {
@@ -17,6 +27,12 @@ export interface MapMarker {
17
27
  lngLat: [number, number];
18
28
  color?: string;
19
29
  label?: string;
30
+ /**
31
+ * Rendered as the marker's popup content via maplibre-gl's
32
+ * `Popup.setHTML()` -- parsed as raw markup, inline event handlers
33
+ * included. Only ever pass trusted content; sanitize anything derived
34
+ * from user input before assigning it here.
35
+ */
20
36
  html?: string;
21
37
  }
22
38
  /**
@@ -49,7 +65,7 @@ export declare class LyraMap extends LyraElement {
49
65
  private _styleLoaded;
50
66
  private _appliedChoroplethSourceId?;
51
67
  private _appliedFillLayerId?;
52
- private _maplibreModule;
68
+ private _maplibreModule?;
53
69
  private _markerInstances;
54
70
  private _connectGeneration;
55
71
  /** The raw `maplibregl.Map` instance — escape hatch for anything this wrapper doesn't expose. */
@@ -38,17 +38,27 @@ const DEFAULT_STYLE = {
38
38
  layers: [{ id: 'lyra-osm', type: 'raster', source: 'lyra-osm' }],
39
39
  };
40
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
41
+ * Matches only actual CSS color syntax: hex (`#fff`/`#ffffff`/`#ffffffff`),
42
+ * bare keywords (named colors, `transparent`, `currentColor`), the standard
43
+ * color functions, and `var(--custom-property)` references. Anything else --
44
+ * notably `url(...)`, which is otherwise valid `background` syntax -- is
45
+ * rejected.
46
+ */
47
+ const SAFE_SWATCH_COLOR = /^(?:#[0-9a-fA-F]{3,8}|[a-zA-Z]+|(?:rgb|rgba|hsl|hsla|hwb|lab|lch|oklab|oklch)\([-+0-9.%,/\s]+\)|var\(--[\w-]+\))$/;
48
+ /**
49
+ * Rejects a `LegendEntry.color` that isn't recognizable CSS color syntax --
50
+ * both to stop it breaking out of the single `background` declaration it's
51
+ * assigned to (e.g. `;`, `{`, `}`, which terminate/reopen a declaration) and
52
+ * to stop non-color values such as `url(...)` from being accepted, which
53
+ * `background` also parses and would fetch as soon as the swatch renders.
54
+ * This matters even though the swatch's color is set via Lit's `styleMap`
55
+ * directive (not raw string interpolation): `styleMap`'s first commit for a
56
+ * given attribute part serializes the whole `style` value as a single string
47
57
  * (only later updates go through the safe `CSSStyleDeclaration.setProperty()`
48
58
  * path), so an unsanitized value could still inject on that first render.
49
59
  */
50
60
  function sanitizeSwatchColor(color) {
51
- return /[;{}]/.test(color) ? undefined : color;
61
+ return SAFE_SWATCH_COLOR.test(color.trim()) ? color : undefined;
52
62
  }
53
63
  /**
54
64
  * `<lyra-map>` — a maplibre-gl wrapper with a declarative legend and
@@ -207,19 +217,34 @@ export class LyraMap extends LyraElement {
207
217
  if (this._appliedChoroplethSourceId && this._appliedChoroplethSourceId !== sourceId) {
208
218
  this.removeChoropleth();
209
219
  }
210
- const colorExpr = ['interpolate', ['linear'], ['get', field]];
211
- for (const [value, color] of stops)
212
- colorExpr.push(value, color);
213
220
  const existingSource = this._map.getSource(sourceId);
214
221
  if (existingSource) {
215
- // Re-apply both the data and the color expression: `field`/`stops` may have
216
- // changed even though `sourceId` didn't (e.g. switching which metric colors
217
- // the same GeoJSON), so the paint property must be refreshed too.
222
+ // Re-apply the data even if the color expression below ends up skipped:
223
+ // `geojson` may have changed even though `sourceId`/`stops` didn't.
218
224
  existingSource.setData(geojson);
219
- this._map.setPaintProperty(fillLayerId, 'fill-color', colorExpr);
220
225
  }
221
226
  else {
222
227
  this._map.addSource(sourceId, { type: 'geojson', data: geojson, promoteId: 'id' });
228
+ }
229
+ this._appliedChoroplethSourceId = sourceId;
230
+ // An `interpolate` expression needs at least one [value, color] stop pair
231
+ // -- maplibre-gl's own expression parser requires it and otherwise fires a
232
+ // silently-ignored ErrorEvent instead of throwing (`addLayer`/
233
+ // `setPaintProperty` just no-op), so an empty `stops` would otherwise
234
+ // "succeed" here without ever creating/updating the fill layer, and every
235
+ // later update (valid `stops`, same `sourceId`) would find `existingSource`
236
+ // and wrongly assume `setPaintProperty` has a real layer to target. Bail
237
+ // before touching the layer at all and leave whatever fill layer already
238
+ // exists (if any) untouched until `stops` is non-empty again.
239
+ if (stops.length === 0)
240
+ return;
241
+ const colorExpr = ['interpolate', ['linear'], ['get', field]];
242
+ for (const [value, color] of stops)
243
+ colorExpr.push(value, color);
244
+ if (this._map.getLayer(fillLayerId)) {
245
+ this._map.setPaintProperty(fillLayerId, 'fill-color', colorExpr);
246
+ }
247
+ else {
223
248
  this._map.addLayer({
224
249
  id: fillLayerId,
225
250
  type: 'fill',
@@ -227,7 +252,6 @@ export class LyraMap extends LyraElement {
227
252
  paint: { 'fill-color': colorExpr, 'fill-opacity': 0.75 },
228
253
  });
229
254
  }
230
- this._appliedChoroplethSourceId = sourceId;
231
255
  this._appliedFillLayerId = fillLayerId;
232
256
  }
233
257
  /** Removes whatever choropleth layer/source is currently applied, if any. */
@@ -243,11 +267,18 @@ export class LyraMap extends LyraElement {
243
267
  this._appliedChoroplethSourceId = undefined;
244
268
  this._appliedFillLayerId = undefined;
245
269
  }
246
- async applyMarkers() {
247
- if (!this._map)
248
- return;
249
- const mod = await loadMaplibre();
250
- if (!mod)
270
+ // Deliberately synchronous (no `await loadMaplibre()`): `_maplibreModule` is
271
+ // already cached by the time `_map` exists (both are set in the same
272
+ // connectedCallback closure, see above), and every caller here is
273
+ // fire-and-forget. Re-awaiting the loader would open a window between the
274
+ // `_map` check and the marker mutations below where a disconnect (which
275
+ // synchronously clears `_map`) could resume into a torn-down map -- staying
276
+ // synchronous closes that window by construction instead of re-checking
277
+ // after the fact.
278
+ applyMarkers() {
279
+ const map = this._map;
280
+ const mod = this._maplibreModule;
281
+ if (!map || !mod)
251
282
  return;
252
283
  const visible = new Set();
253
284
  for (const m of this.markers) {
@@ -264,11 +295,27 @@ export class LyraMap extends LyraElement {
264
295
  popup.setText(m.label);
265
296
  marker.setPopup(popup);
266
297
  }
267
- marker.addTo(this._map);
298
+ marker.addTo(map);
268
299
  this._markerInstances.set(key, marker);
269
300
  }
270
301
  else {
271
302
  existing.setLngLat(m.lngLat);
303
+ const popup = existing.getPopup();
304
+ if (m.html) {
305
+ if (popup)
306
+ popup.setHTML(m.html);
307
+ else
308
+ existing.setPopup(new mod.Popup({ offset: 12 }).setHTML(m.html));
309
+ }
310
+ else if (m.label) {
311
+ if (popup)
312
+ popup.setText(m.label);
313
+ else
314
+ existing.setPopup(new mod.Popup({ offset: 12 }).setText(m.label));
315
+ }
316
+ else if (popup) {
317
+ existing.setPopup(undefined);
318
+ }
272
319
  }
273
320
  }
274
321
  for (const [key, marker] of this._markerInstances) {
@@ -3,3 +3,13 @@ declare const meta: Meta;
3
3
  export default meta;
4
4
  type Story = StoryObj;
5
5
  export declare const Default: Story;
6
+ /**
7
+ * `choropleth` adds a GeoJSON fill layer, colored by interpolating `field`'s
8
+ * value across `stops` -- pair it with `legend` to label the color ramp.
9
+ */
10
+ export declare const Choropleth: Story;
11
+ /**
12
+ * `markers` renders a pin per entry, each with an optional colored tint and
13
+ * an openable popup built from `label` (plain text) or `html` (raw markup).
14
+ */
15
+ export declare const Markers: Story;
@@ -3,6 +3,18 @@ const legend = [
3
3
  { color: '#5b8def', label: 'Low' },
4
4
  { color: '#e5484d', label: 'High' },
5
5
  ];
6
+ const RASTER_STYLE = {
7
+ version: 8,
8
+ sources: {
9
+ demo: {
10
+ type: 'raster',
11
+ tiles: ['https://tile.openstreetmap.org/{z}/{x}/{y}.png'],
12
+ tileSize: 256,
13
+ attribution: '© OpenStreetMap contributors',
14
+ },
15
+ },
16
+ layers: [{ id: 'demo', type: 'raster', source: 'demo' }],
17
+ };
6
18
  const meta = {
7
19
  title: 'Map',
8
20
  component: 'lyra-map',
@@ -23,18 +35,101 @@ export const Default = {
23
35
  center="[2.3522, 48.8566]"
24
36
  zoom="4"
25
37
  .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
+ .mapStyle=${RASTER_STYLE}
38
39
  ></lyra-map>
39
40
  `,
40
41
  };
42
+ /**
43
+ * `choropleth` adds a GeoJSON fill layer, colored by interpolating `field`'s
44
+ * value across `stops` -- pair it with `legend` to label the color ramp.
45
+ */
46
+ export const Choropleth = {
47
+ render: () => {
48
+ const choropleth = {
49
+ sourceId: 'regions',
50
+ field: 'value',
51
+ stops: [
52
+ [0, '#5b8def'],
53
+ [50, '#f2c94c'],
54
+ [100, '#e5484d'],
55
+ ],
56
+ geojson: {
57
+ type: 'FeatureCollection',
58
+ features: [
59
+ {
60
+ type: 'Feature',
61
+ properties: { value: 20 },
62
+ geometry: {
63
+ type: 'Polygon',
64
+ coordinates: [
65
+ [
66
+ [2.25, 48.9],
67
+ [2.35, 48.9],
68
+ [2.35, 48.85],
69
+ [2.25, 48.85],
70
+ [2.25, 48.9],
71
+ ],
72
+ ],
73
+ },
74
+ },
75
+ {
76
+ type: 'Feature',
77
+ properties: { value: 80 },
78
+ geometry: {
79
+ type: 'Polygon',
80
+ coordinates: [
81
+ [
82
+ [2.35, 48.9],
83
+ [2.45, 48.9],
84
+ [2.45, 48.85],
85
+ [2.35, 48.85],
86
+ [2.35, 48.9],
87
+ ],
88
+ ],
89
+ },
90
+ },
91
+ ],
92
+ },
93
+ };
94
+ return html `
95
+ <lyra-map
96
+ style="height: 20rem"
97
+ center="[2.3522, 48.8566]"
98
+ zoom="10"
99
+ .legend=${[
100
+ { color: '#5b8def', label: 'Low' },
101
+ { color: '#f2c94c', label: 'Medium' },
102
+ { color: '#e5484d', label: 'High' },
103
+ ]}
104
+ .choropleth=${choropleth}
105
+ .mapStyle=${RASTER_STYLE}
106
+ ></lyra-map>
107
+ `;
108
+ },
109
+ };
110
+ /**
111
+ * `markers` renders a pin per entry, each with an optional colored tint and
112
+ * an openable popup built from `label` (plain text) or `html` (raw markup).
113
+ */
114
+ export const Markers = {
115
+ render: () => {
116
+ const markers = [
117
+ { id: 'eiffel', lngLat: [2.2945, 48.8584], label: 'Eiffel Tower' },
118
+ {
119
+ id: 'louvre',
120
+ lngLat: [2.3364, 48.8606],
121
+ color: '#e5484d',
122
+ html: '<strong>Louvre</strong><br>Museum',
123
+ },
124
+ ];
125
+ return html `
126
+ <lyra-map
127
+ style="height: 20rem"
128
+ center="[2.3522, 48.8566]"
129
+ zoom="12"
130
+ .markers=${markers}
131
+ .mapStyle=${RASTER_STYLE}
132
+ ></lyra-map>
133
+ `;
134
+ },
135
+ };
@@ -26,7 +26,7 @@ export const styles = css `
26
26
  z-index: 1;
27
27
  display: flex;
28
28
  flex-direction: column;
29
- gap: 0.2rem;
29
+ gap: var(--lyra-space-xs);
30
30
  padding: var(--lyra-space-xs) var(--lyra-space-s);
31
31
  background: var(--lyra-color-surface);
32
32
  border: 1px solid var(--lyra-color-border);
@@ -37,7 +37,7 @@ export const styles = css `
37
37
  .legend-row {
38
38
  display: flex;
39
39
  align-items: center;
40
- gap: 0.35rem;
40
+ gap: var(--lyra-space-xs);
41
41
  }
42
42
  [part='legend-swatch'] {
43
43
  inline-size: 0.75rem;
@@ -6,8 +6,8 @@ import { LyraElement } from '../../internal/lyra-element.js';
6
6
  * play-timers in time-series dashboards.
7
7
  *
8
8
  * @customElement lyra-playback
9
- * @event lyra-play
10
- * @event lyra-pause
9
+ * @event lyra-play - Fired when playback starts.
10
+ * @event lyra-pause - Fired when playback stops (including auto-pause).
11
11
  * @event lyra-step - `detail: { index }`, fired on every tick and manual step.
12
12
  * @csspart base, play-button, slider
13
13
  */
@@ -27,13 +27,17 @@ export declare class LyraPlayback extends LyraElement {
27
27
  */
28
28
  hidden: boolean;
29
29
  private timer?;
30
+ /** Largest index reachable at the current `length` (never negative). */
31
+ private get maxIndex();
30
32
  disconnectedCallback(): void;
31
33
  protected willUpdate(changed: PropertyValues): void;
32
34
  /** Start playback; no-op if there's nothing to advance through. */
33
35
  play(): void;
34
36
  /** Stop playback. */
35
37
  pause(): void;
38
+ /** Toggle between playing and paused. */
36
39
  toggle(): void;
40
+ private scheduleTick;
37
41
  private tick;
38
42
  private setIndex;
39
43
  /** Advance one step without starting playback. */