@aceshooting/lyra-ui 2.0.0 → 2.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 (224) hide show
  1. package/README.md +87 -14
  2. package/custom-elements.json +11555 -5539
  3. package/dist/components/app-rail/app-rail-item.d.ts +30 -0
  4. package/dist/components/app-rail/app-rail-item.js +68 -0
  5. package/dist/components/app-rail/app-rail-item.styles.d.ts +1 -0
  6. package/dist/components/app-rail/app-rail-item.styles.js +65 -0
  7. package/dist/components/app-rail/app-rail.d.ts +21 -24
  8. package/dist/components/app-rail/app-rail.js +68 -132
  9. package/dist/components/app-rail/app-rail.styles.js +5 -2
  10. package/dist/components/attachment-chip/attachment-chip.d.ts +38 -11
  11. package/dist/components/attachment-chip/attachment-chip.js +73 -29
  12. package/dist/components/attachment-trigger/attachment-trigger.d.ts +13 -0
  13. package/dist/components/attachment-trigger/attachment-trigger.js +13 -1
  14. package/dist/components/attachment-trigger/attachment-trigger.styles.js +28 -6
  15. package/dist/components/chart/bar-chart.js +2 -18
  16. package/dist/components/chart/box-plot.d.ts +25 -1
  17. package/dist/components/chart/box-plot.js +136 -6
  18. package/dist/components/chart/box-plot.styles.js +13 -0
  19. package/dist/components/chart/bubble-chart.js +2 -18
  20. package/dist/components/chart/chart.d.ts +54 -10
  21. package/dist/components/chart/chart.js +182 -39
  22. package/dist/components/chart/doughnut-chart.js +2 -18
  23. package/dist/components/chart/histogram-bin.d.ts +5 -2
  24. package/dist/components/chart/histogram-bin.js +12 -4
  25. package/dist/components/chart/histogram.js +8 -16
  26. package/dist/components/chart/line-chart.js +2 -18
  27. package/dist/components/chart/lite-chart.d.ts +80 -3
  28. package/dist/components/chart/lite-chart.js +250 -53
  29. package/dist/components/chart/pie-chart.js +2 -18
  30. package/dist/components/chart/polar-area-chart.js +2 -18
  31. package/dist/components/chart/radar-chart.js +2 -18
  32. package/dist/components/chart/scatter-chart.js +2 -18
  33. package/dist/components/chat-composer/chat-composer.d.ts +42 -1
  34. package/dist/components/chat-composer/chat-composer.js +84 -3
  35. package/dist/components/chat-composer/chat-composer.styles.js +11 -1
  36. package/dist/components/chat-message/chat-message.d.ts +32 -4
  37. package/dist/components/chat-message/chat-message.js +57 -17
  38. package/dist/components/checkbox/checkbox.d.ts +66 -8
  39. package/dist/components/checkbox/checkbox.js +173 -50
  40. package/dist/components/checkbox/checkbox.styles.js +9 -2
  41. package/dist/components/chip/chip-group.js +8 -2
  42. package/dist/components/chip/chip.js +9 -2
  43. package/dist/components/citation-badge/citation-badge.js +21 -3
  44. package/dist/components/code-block/code-block.d.ts +38 -0
  45. package/dist/components/code-block/code-block.js +72 -7
  46. package/dist/components/code-block/code-loader.d.ts +44 -1
  47. package/dist/components/code-block/code-loader.js +63 -0
  48. package/dist/components/combobox/combobox.d.ts +46 -5
  49. package/dist/components/combobox/combobox.js +175 -35
  50. package/dist/components/combobox/combobox.styles.js +3 -3
  51. package/dist/components/context-meter/context-meter.js +17 -8
  52. package/dist/components/context-meter/context-meter.styles.js +0 -6
  53. package/dist/components/conversation-item/conversation-item.d.ts +13 -16
  54. package/dist/components/conversation-item/conversation-item.js +29 -20
  55. package/dist/components/date-picker/calendar-core.d.ts +24 -0
  56. package/dist/components/date-picker/calendar-core.js +51 -4
  57. package/dist/components/date-picker/date-input.d.ts +77 -10
  58. package/dist/components/date-picker/date-input.js +278 -73
  59. package/dist/components/date-picker/date-picker.d.ts +28 -4
  60. package/dist/components/date-picker/date-picker.js +95 -37
  61. package/dist/components/dialog/confirm.js +3 -6
  62. package/dist/components/dialog/dialog.d.ts +75 -31
  63. package/dist/components/dialog/dialog.js +147 -131
  64. package/dist/components/dialog/dialog.styles.js +46 -2
  65. package/dist/components/dock-panel/dock-panel.d.ts +8 -0
  66. package/dist/components/dock-panel/dock-panel.js +15 -1
  67. package/dist/components/dock-panel/dock-panel.styles.js +15 -1
  68. package/dist/components/document-preview/document-preview.d.ts +15 -7
  69. package/dist/components/document-preview/document-preview.js +29 -13
  70. package/dist/components/empty/empty.d.ts +1 -2
  71. package/dist/components/empty/empty.js +18 -12
  72. package/dist/components/empty/empty.styles.js +0 -3
  73. package/dist/components/export-button/export-button.d.ts +12 -0
  74. package/dist/components/export-button/export-button.js +33 -5
  75. package/dist/components/export-button/export-button.styles.js +5 -1
  76. package/dist/components/file-input/accept.js +2 -0
  77. package/dist/components/file-input/file-input.d.ts +10 -1
  78. package/dist/components/file-input/file-input.js +36 -5
  79. package/dist/components/flag/flag.d.ts +1 -1
  80. package/dist/components/flag/flag.js +15 -1
  81. package/dist/components/flag/flag.styles.js +1 -1
  82. package/dist/components/flag/language-map.d.ts +4 -1
  83. package/dist/components/flag/language-map.js +6 -3
  84. package/dist/components/gauge/gauge.d.ts +10 -1
  85. package/dist/components/gauge/gauge.js +28 -9
  86. package/dist/components/generation-status/generation-status.d.ts +13 -5
  87. package/dist/components/generation-status/generation-status.js +59 -5
  88. package/dist/components/graph/graph.d.ts +37 -3
  89. package/dist/components/graph/graph.js +132 -20
  90. package/dist/components/heatmap/calendar-grid.d.ts +15 -6
  91. package/dist/components/heatmap/calendar-grid.js +17 -6
  92. package/dist/components/heatmap/heatmap-scale.d.ts +5 -1
  93. package/dist/components/heatmap/heatmap-scale.js +8 -2
  94. package/dist/components/heatmap/heatmap.d.ts +166 -25
  95. package/dist/components/heatmap/heatmap.js +372 -89
  96. package/dist/components/json-viewer/json-viewer.d.ts +22 -5
  97. package/dist/components/json-viewer/json-viewer.js +115 -37
  98. package/dist/components/json-viewer/json-viewer.styles.js +2 -1
  99. package/dist/components/kbd/kbd.js +24 -7
  100. package/dist/components/live-region/live-region.d.ts +10 -1
  101. package/dist/components/live-region/live-region.js +42 -6
  102. package/dist/components/map/map.d.ts +24 -1
  103. package/dist/components/map/map.js +89 -24
  104. package/dist/components/markdown/markdown-loader.d.ts +24 -0
  105. package/dist/components/markdown/markdown-loader.js +35 -1
  106. package/dist/components/markdown/markdown.d.ts +46 -8
  107. package/dist/components/markdown/markdown.js +136 -31
  108. package/dist/components/markdown/markdown.styles.js +7 -1
  109. package/dist/components/media-card/media-card.d.ts +25 -17
  110. package/dist/components/media-card/media-card.js +43 -65
  111. package/dist/components/mention-popover/mention-popover.d.ts +4 -1
  112. package/dist/components/mention-popover/mention-popover.js +23 -5
  113. package/dist/components/menu/menu-item.d.ts +32 -3
  114. package/dist/components/menu/menu-item.js +94 -7
  115. package/dist/components/menu/menu-item.styles.js +8 -0
  116. package/dist/components/menu/menu.d.ts +25 -1
  117. package/dist/components/menu/menu.js +99 -3
  118. package/dist/components/menu/menu.styles.js +3 -2
  119. package/dist/components/model-select/model-select.d.ts +48 -3
  120. package/dist/components/model-select/model-select.js +120 -22
  121. package/dist/components/model-select/model-select.styles.js +17 -3
  122. package/dist/components/model-settings-panel/model-settings-panel.d.ts +25 -1
  123. package/dist/components/model-settings-panel/model-settings-panel.js +65 -1
  124. package/dist/components/playback/playback.d.ts +6 -2
  125. package/dist/components/playback/playback.js +72 -29
  126. package/dist/components/playback/playback.styles.js +6 -1
  127. package/dist/components/responsive-panel/responsive-panel.d.ts +29 -16
  128. package/dist/components/responsive-panel/responsive-panel.js +130 -132
  129. package/dist/components/responsive-panel/responsive-panel.styles.js +1 -1
  130. package/dist/components/result-card/result-card.d.ts +8 -2
  131. package/dist/components/result-card/result-card.js +9 -3
  132. package/dist/components/result-card/result-card.styles.js +0 -1
  133. package/dist/components/select/select.d.ts +59 -20
  134. package/dist/components/select/select.js +125 -43
  135. package/dist/components/skeleton/skeleton.d.ts +3 -0
  136. package/dist/components/skeleton/skeleton.js +7 -1
  137. package/dist/components/skeleton/skeleton.styles.js +5 -4
  138. package/dist/components/slider/slider.d.ts +57 -5
  139. package/dist/components/slider/slider.js +195 -80
  140. package/dist/components/source-card/source-card.d.ts +17 -5
  141. package/dist/components/source-card/source-card.js +55 -8
  142. package/dist/components/source-card/source-card.styles.js +1 -1
  143. package/dist/components/source-list/source-list.js +9 -1
  144. package/dist/components/sparkline/sparkline.js +34 -10
  145. package/dist/components/split/split.d.ts +141 -14
  146. package/dist/components/split/split.js +357 -52
  147. package/dist/components/split/split.styles.js +18 -1
  148. package/dist/components/stat/stat.d.ts +10 -4
  149. package/dist/components/stat/stat.js +46 -11
  150. package/dist/components/stream-status/stream-status.d.ts +27 -11
  151. package/dist/components/stream-status/stream-status.js +95 -13
  152. package/dist/components/streaming-text/streaming-text.d.ts +9 -3
  153. package/dist/components/streaming-text/streaming-text.js +25 -10
  154. package/dist/components/streaming-text/streaming-text.styles.js +3 -2
  155. package/dist/components/switch/switch.d.ts +52 -7
  156. package/dist/components/switch/switch.js +111 -40
  157. package/dist/components/switch/switch.styles.js +1 -1
  158. package/dist/components/table/table.d.ts +18 -3
  159. package/dist/components/table/table.js +88 -20
  160. package/dist/components/tabs/tabs.d.ts +23 -0
  161. package/dist/components/tabs/tabs.js +76 -12
  162. package/dist/components/tabs/tabs.styles.js +11 -0
  163. package/dist/components/thinking-panel/thinking-panel.d.ts +3 -1
  164. package/dist/components/thinking-panel/thinking-panel.js +34 -9
  165. package/dist/components/thinking-panel/thinking-panel.styles.js +6 -0
  166. package/dist/components/time-range/time-range.d.ts +59 -2
  167. package/dist/components/time-range/time-range.js +163 -67
  168. package/dist/components/toast/toast-item.d.ts +12 -3
  169. package/dist/components/toast/toast-item.js +90 -53
  170. package/dist/components/toast/toast-item.styles.js +59 -28
  171. package/dist/components/toast/toast.js +17 -5
  172. package/dist/components/toast/toaster.js +10 -1
  173. package/dist/components/tool-approval-dialog/tool-approval-dialog.d.ts +11 -10
  174. package/dist/components/tool-approval-dialog/tool-approval-dialog.js +50 -124
  175. package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.js +1 -1
  176. package/dist/components/tool-call-chip/tool-call-chip.d.ts +18 -2
  177. package/dist/components/tool-call-chip/tool-call-chip.js +86 -25
  178. package/dist/components/tool-param-form/tool-param-form.d.ts +89 -28
  179. package/dist/components/tool-param-form/tool-param-form.js +350 -106
  180. package/dist/components/tool-result-dialog/tool-result-dialog.d.ts +27 -7
  181. package/dist/components/tool-result-dialog/tool-result-dialog.js +80 -115
  182. package/dist/components/tool-result-dialog/tool-result-dialog.styles.js +1 -1
  183. package/dist/components/tool-result-view/registry.d.ts +0 -8
  184. package/dist/components/tool-result-view/registry.js +7 -2
  185. package/dist/components/tool-result-view/tool-result-view.d.ts +6 -3
  186. package/dist/components/tool-result-view/tool-result-view.js +18 -4
  187. package/dist/components/tool-select-dialog/tool-select-dialog.d.ts +11 -11
  188. package/dist/components/tool-select-dialog/tool-select-dialog.js +44 -133
  189. package/dist/components/tool-select-dialog/tool-select-dialog.styles.js +13 -4
  190. package/dist/components/tree/tree-node.d.ts +4 -1
  191. package/dist/components/tree/tree-node.js +4 -1
  192. package/dist/components/tree/tree.d.ts +17 -8
  193. package/dist/components/tree/tree.js +21 -20
  194. package/dist/components/typing-indicator/typing-indicator.d.ts +12 -2
  195. package/dist/components/typing-indicator/typing-indicator.js +16 -4
  196. package/dist/components/typing-indicator/typing-indicator.styles.js +2 -2
  197. package/dist/components/virtual-list/virtual-list.d.ts +56 -19
  198. package/dist/components/virtual-list/virtual-list.js +184 -38
  199. package/dist/components/virtual-list/virtual-list.styles.js +12 -0
  200. package/dist/components/widget/widget.d.ts +5 -4
  201. package/dist/components/widget/widget.js +54 -120
  202. package/dist/components/widget/widget.styles.js +8 -2
  203. package/dist/components/word-cloud/word-cloud-layout.d.ts +7 -0
  204. package/dist/components/word-cloud/word-cloud-layout.js +20 -6
  205. package/dist/components/word-cloud/word-cloud.d.ts +22 -2
  206. package/dist/components/word-cloud/word-cloud.js +45 -6
  207. package/dist/components/word-cloud/word-cloud.styles.js +13 -1
  208. package/dist/internal/anchored-validity.d.ts +1 -0
  209. package/dist/internal/anchored-validity.js +86 -0
  210. package/dist/internal/form-associated.d.ts +7 -0
  211. package/dist/internal/form-associated.js +43 -4
  212. package/dist/internal/numbers.d.ts +12 -0
  213. package/dist/internal/numbers.js +26 -0
  214. package/dist/internal/overlay-manager.d.ts +51 -0
  215. package/dist/internal/overlay-manager.js +523 -0
  216. package/dist/internal/positioner.js +27 -3
  217. package/dist/internal/safe-url.d.ts +12 -0
  218. package/dist/internal/safe-url.js +41 -0
  219. package/dist/internal/tokens.styles.js +17 -11
  220. package/dist/lyra.d.ts +5 -2
  221. package/dist/lyra.js +3 -1
  222. package/llms-full.txt +970 -426
  223. package/llms.txt +5 -4
  224. package/package.json +19 -2
@@ -22,9 +22,22 @@ const FALLBACK_SCALE_HI = '#0969da';
22
22
  const FALLBACK_LABEL_FONT = '10px sans-serif';
23
23
  const CAL_PAD_LEFT = 28;
24
24
  const CAL_LABEL_H = 16;
25
+ /** Calendar mode's original fixed cell size — now also the effective fallback
26
+ * the `cellSize` accessor uses in calendar mode when left unset (see its
27
+ * doc comment), so a consumer who never touches `cellSize` sees no change. */
25
28
  const CAL_CELL = 11;
26
29
  const CAL_GAP = 2;
30
+ /** Matrix mode's original fixed cell size — the effective fallback the
31
+ * `cellSize` accessor uses in matrix mode when left unset. */
32
+ const DEFAULT_MATRIX_CELL_SIZE = 22;
27
33
  const DEFAULT_BUCKET_COUNT = 5;
34
+ /**
35
+ * A linear RGB ramp cannot contain more than 256 visually distinct integer
36
+ * steps on any channel. Keeping the bucket count within that bound avoids an
37
+ * untrusted attribute/property value turning the ramp into an unbounded
38
+ * allocation without discarding any useful color resolution.
39
+ */
40
+ export const MAX_BUCKET_COUNT = 256;
28
41
  /** Ring stroke width for both the annotation overlay and the keyboard focus ring. */
29
42
  const RING_LINE_WIDTH = 2;
30
43
  const FALLBACK_FOCUS_RING_COLOR = '#0969da';
@@ -66,30 +79,26 @@ function warnInvalidColor(color) {
66
79
  console.warn(`<lyra-heatmap> could not parse "${color}" (set via --lyra-heatmap-scale-lo/-hi) as a CSS ` +
67
80
  'color; falling back to the default ramp endpoint.');
68
81
  }
69
- const warnedInvalidBucketCounts = new Set();
70
- function warnInvalidBucketCount(value) {
71
- if (warnedInvalidBucketCounts.has(value))
72
- return;
73
- warnedInvalidBucketCounts.add(value);
74
- console.warn(`<lyra-heatmap> received a non-finite bucket-count (${value}); falling back to ${DEFAULT_BUCKET_COUNT}.`);
75
- }
76
82
  /**
77
- * Normalizes `bucketCount` to a finite integer >= 2. `Math.max(2, bucketCount)`
78
- * alone stays `NaN` for a non-numeric `bucket-count` attribute (`Math.max`
79
- * propagates `NaN`), which silently zeroes out the ramp array and makes every
80
- * cell keep whatever `fillStyle` the previous draw left. A fractional count
81
- * is floored rather than passed through as-is, so the ramp array's length
82
- * (built via `Array.from({ length: buckets }, ...)`, which truncates a
83
- * fractional `length`) always agrees with the index `quartileBucket` computes
84
- * from the same `buckets` value.
83
+ * Normalizes a bucket count to the safe, renderable range. Non-finite values
84
+ * restore the public default; finite values are floored and clamped to
85
+ * `[2, MAX_BUCKET_COUNT]`. Flooring keeps the ramp array's length in exact
86
+ * agreement with the count used by `quartileBucket()`.
85
87
  */
86
- function resolveBucketCount(bucketCount) {
88
+ export function normalizeBucketCount(bucketCount) {
87
89
  if (!Number.isFinite(bucketCount)) {
88
- warnInvalidBucketCount(bucketCount);
89
90
  return DEFAULT_BUCKET_COUNT;
90
91
  }
91
- return Math.max(2, Math.floor(bucketCount));
92
+ return Math.min(MAX_BUCKET_COUNT, Math.max(2, Math.floor(bucketCount)));
92
93
  }
94
+ const bucketCountConverter = {
95
+ fromAttribute(value) {
96
+ return value === null ? DEFAULT_BUCKET_COUNT : normalizeBucketCount(Number(value));
97
+ },
98
+ toAttribute(value) {
99
+ return String(normalizeBucketCount(value));
100
+ },
101
+ };
93
102
  /**
94
103
  * Resolves any syntactically valid CSS `<color>` — hex, `rgb()`, `hsl()`,
95
104
  * `oklch()`, a named color, etc. — to an `[r, g, b]` triple.
@@ -142,12 +151,16 @@ export function mixColor(fromColor, toColor, t) {
142
151
  * - `"matrix"` (default): a `rowLabels` x `colLabels` grid of `values`. `-1`
143
152
  * (or any non-finite value) is treated as "no data". `scale="sqrt"`
144
153
  * compresses the ramp via `sqrtStep()` so one heavy cell doesn't wash out
145
- * the rest; the default `"linear"` scale maps linearly instead. `scale`
146
- * only affects matrix mode — calendar mode always buckets by quartile.
147
- * - `"calendar"`: a GitHub-style Sunday-Saturday x week grid built from
148
- * `days`, colored by `quartileBucket()` into `bucketCount` buckets. As in
149
- * matrix mode, a cell whose `value` is negative or non-finite is treated
150
- * as "no data" rather than being bucketed.
154
+ * the rest; the default `"linear"` scale maps linearly instead.
155
+ * - `"calendar"`: a GitHub-style Sunday-Saturday (or `firstDayOfWeek`-anchored
156
+ * see below) x week grid built from `days`. `scale` governs its
157
+ * bucketing too: the default `"linear"` buckets by `quartileBucket()`
158
+ * (today's original behavior, unchanged); `"sqrt"` instead compresses via
159
+ * the same `sqrtStep()` magnitude compression matrix mode uses, so one
160
+ * heavy day doesn't wash out the rest. As in matrix mode, a cell whose
161
+ * `value` is negative or non-finite is treated as "no data" rather than
162
+ * being bucketed — as is a grid position with no matching entry in `days`
163
+ * at all (a gap in a sparse calendar).
151
164
  *
152
165
  * The sequential color ramp's endpoints are read from the
153
166
  * `--lyra-heatmap-scale-lo`/`-hi` custom properties (declared in
@@ -171,7 +184,21 @@ export function mixColor(fromColor, toColor, t) {
171
184
  * week column — drawing, hit-testing, the focus ring, and month-label
172
185
  * positioning all consult it consistently, so a consumer can pixel-align a
173
186
  * calendar's week columns with a sibling chart's coordinate system. Unset
174
- * (the default) keeps the original evenly-spaced formula.
187
+ * (the default) keeps the original evenly-spaced formula. `rowY` is its
188
+ * calendar-mode vertical analogue — overrides the y-origin computed for each
189
+ * weekday row, consulted consistently by drawing, hit-testing, and the focus
190
+ * ring via the private `rowYFor()` helper (mirroring `columnXFor()` exactly).
191
+ *
192
+ * `firstDayOfWeek` (calendar mode only, default `0`/Sunday, no-op in matrix
193
+ * mode) anchors the calendar grid at a different weekday — `0`-`6`, same
194
+ * numbering as `CalendarCellPos.weekday` (`0` Sunday .. `6` Saturday) —
195
+ * threaded into `buildCalendarGrid()`.
196
+ *
197
+ * `cellSize`/`fitToWidth` (previously matrix-mode only) also drive calendar
198
+ * mode's per-cell size: unset, calendar mode keeps today's original 11px
199
+ * cell size unchanged; explicitly set, the same fixed size (or, with
200
+ * `fitToWidth`, the same host-width-derived size matrix mode already
201
+ * supports) governs calendar mode's grid too.
175
202
  *
176
203
  * @customElement lyra-heatmap
177
204
  * @event lyra-cell-click - Fired on click, or Enter/Space on the
@@ -179,7 +206,14 @@ export function mixColor(fromColor, toColor, t) {
179
206
  * `detail: { date, value }` in calendar mode. `cellText` overrides the
180
207
  * built-in English "Row X, Col Y: value" / "Mon DD: value" template used for
181
208
  * both the hover tooltip and the keyboard live-region announcement.
182
- * @csspart base, canvas, tooltip, live-region, legend, legend-lo, legend-hi, legend-annotation
209
+ * @csspart base - The heatmap wrapper.
210
+ * @csspart canvas - The heatmap canvas.
211
+ * @csspart tooltip - The hover tooltip.
212
+ * @csspart live-region - The visually hidden keyboard announcement region.
213
+ * @csspart legend - The color legend.
214
+ * @csspart legend-lo - The low legend endpoint.
215
+ * @csspart legend-hi - The high legend endpoint.
216
+ * @csspart legend-annotation - An annotation label.
183
217
  */
184
218
  export class LyraHeatmap extends LyraElement {
185
219
  constructor() {
@@ -187,25 +221,57 @@ export class LyraHeatmap extends LyraElement {
187
221
  this.rowLabels = [];
188
222
  this.colLabels = [];
189
223
  this.values = [];
190
- this.cellSize = 22;
191
224
  this.valueLabel = 'value';
225
+ /**
226
+ * `"linear"` (default) maps values linearly to the color ramp in matrix
227
+ * mode, and buckets calendar-mode values via `quartileBucket()` — both
228
+ * unchanged from before this property governed calendar mode too.
229
+ * `"sqrt"` compresses via `sqrtStep()`'s square-root magnitude compression
230
+ * instead, in *both* modes, so one heavy cell/day doesn't wash out the
231
+ * rest of a skewed dataset.
232
+ */
192
233
  this.scale = 'linear';
193
234
  /**
194
235
  * When set, `cellSize` is derived from the host's measured `clientWidth`
195
236
  * on every draw (including ResizeObserver-triggered redraws) instead of
196
237
  * the fixed `cell-size` attribute, so the grid actually fills the
197
238
  * available width. Without this, canvas dimensions are computed purely
198
- * from `PAD_LEFT + cols * cellSize`, so a resize-triggered redraw is a
199
- * geometric no-op.
239
+ * from `PAD_LEFT + cols * cellSize` (matrix mode) or
240
+ * `CAL_PAD_LEFT + weekCount * cellSize` (calendar mode), so a
241
+ * resize-triggered redraw is a geometric no-op. Originally matrix-mode
242
+ * only; now applies to calendar mode too.
200
243
  */
201
244
  this.fitToWidth = false;
202
245
  this.mode = 'matrix';
203
246
  this.days = [];
204
- this.bucketCount = DEFAULT_BUCKET_COUNT;
247
+ /**
248
+ * Calendar mode only (no-op in matrix mode): anchors the calendar grid at
249
+ * a different weekday instead of always Sunday — `0`-`6`, same numbering
250
+ * as `CalendarCellPos.weekday` (`0` Sunday .. `6` Saturday, matching
251
+ * `CalendarCell.weekday`'s existing convention). Threaded into
252
+ * `buildCalendarGrid()`. Defaults to `0` (Sunday), unchanged from before
253
+ * this property existed.
254
+ */
255
+ this.firstDayOfWeek = 0;
256
+ this._bucketCount = DEFAULT_BUCKET_COUNT;
205
257
  /** Cells to ring-highlight — `row`/`col` in matrix mode, `date` in calendar mode. See `HeatmapAnnotation`. */
206
258
  this.annotations = [];
259
+ this.themeRefreshQueued = false;
207
260
  /** The current value range, refreshed once per update cycle by `willUpdate()`. See `computeValueRange()`. */
208
261
  this.cachedValueRange = null;
262
+ /**
263
+ * The calendar-mode grid layout, refreshed by `willUpdate()` only when
264
+ * `days` actually changes (see `computeValueRange()`'s twin comment above)
265
+ * — `buildCalendarGrid()` parses every date, filters invalid ones, and does
266
+ * a full `.slice().sort()` for month labels, so recomputing it from
267
+ * `drawCalendar()`, `hitTestCalendar()`, `calendarCellAt()`, and
268
+ * `onCalendarKeyDown()` independently would redo that work 2-4x for a
269
+ * single hover/click/keydown.
270
+ */
271
+ this.cachedCalendarGrid = buildCalendarGrid(this.days);
272
+ this.cachedCalendarSortedValues = [];
273
+ this.cachedCalendarCellsByPos = new Map();
274
+ this.cachedRamp = null;
209
275
  /** The cell currently under the pointer (`null` when not hovering one) — drives `[part="tooltip"]`. */
210
276
  this.hoverCell = null;
211
277
  /** The roving keyboard-focus cell cursor, moved by arrow keys — drives the
@@ -230,6 +296,28 @@ export class LyraHeatmap extends LyraElement {
230
296
  this.watchDpr();
231
297
  this.draw();
232
298
  };
299
+ this.onColorSchemeChange = () => {
300
+ this.refreshTheme();
301
+ };
302
+ this.queueThemeRefresh = () => {
303
+ if (this.themeRefreshQueued)
304
+ return;
305
+ this.themeRefreshQueued = true;
306
+ queueMicrotask(() => {
307
+ this.themeRefreshQueued = false;
308
+ if (this.isConnected)
309
+ this.refreshTheme();
310
+ });
311
+ };
312
+ this.scheduleDraw = () => {
313
+ if (this.drawRafId !== undefined)
314
+ return;
315
+ this.drawRafId = requestAnimationFrame(() => {
316
+ this.drawRafId = undefined;
317
+ if (this.isConnected)
318
+ this.draw();
319
+ });
320
+ };
233
321
  this.onPointerMove = (e) => {
234
322
  const next = this.hitTest(e.offsetX, e.offsetY);
235
323
  if (!this.samePos(this.hoverCell, next))
@@ -258,16 +346,49 @@ export class LyraHeatmap extends LyraElement {
258
346
  };
259
347
  }
260
348
  static { this.styles = [LyraElement.styles, styles, srOnly]; }
349
+ /**
350
+ * Effective per-cell size (CSS px). Originally matrix-mode only; now also
351
+ * governs calendar mode's cell size (replacing the previously hardcoded
352
+ * 11px constant there) once explicitly set. Left unset, each mode keeps
353
+ * its own original default — `DEFAULT_MATRIX_CELL_SIZE` (22) in matrix
354
+ * mode, `CAL_CELL` (11) in calendar mode — so an existing consumer who
355
+ * never touches `cellSize` sees no change in either mode. Set explicitly
356
+ * (attribute or property), the same value governs both modes alike.
357
+ */
358
+ get cellSize() {
359
+ return this._cellSize ?? (this.mode === 'calendar' ? CAL_CELL : DEFAULT_MATRIX_CELL_SIZE);
360
+ }
361
+ set cellSize(value) {
362
+ const oldValue = this._cellSize;
363
+ this._cellSize = value;
364
+ this.requestUpdate('cellSize', oldValue);
365
+ }
366
+ get bucketCount() {
367
+ return this._bucketCount;
368
+ }
369
+ set bucketCount(value) {
370
+ const oldValue = this._bucketCount;
371
+ this._bucketCount = normalizeBucketCount(value);
372
+ this.requestUpdate('bucketCount', oldValue);
373
+ }
261
374
  connectedCallback() {
262
375
  super.connectedCallback();
263
- this.resizeObserver = new ResizeObserver(() => this.draw());
376
+ this.resizeObserver = new ResizeObserver(this.scheduleDraw);
264
377
  this.resizeObserver.observe(this);
265
378
  this.watchDpr();
379
+ this.watchTheme();
266
380
  }
267
381
  disconnectedCallback() {
268
382
  super.disconnectedCallback();
269
383
  this.resizeObserver?.disconnect();
270
384
  this.dprQuery?.removeEventListener('change', this.onDprChange);
385
+ this.colorSchemeQuery?.removeEventListener('change', this.onColorSchemeChange);
386
+ this.themeObserver?.disconnect();
387
+ this.themeObserver = undefined;
388
+ if (this.drawRafId !== undefined) {
389
+ cancelAnimationFrame(this.drawRafId);
390
+ this.drawRafId = undefined;
391
+ }
271
392
  }
272
393
  watchDpr() {
273
394
  // A MediaQueryList's `matches` is fixed at creation time, so crossing the
@@ -278,24 +399,69 @@ export class LyraHeatmap extends LyraElement {
278
399
  this.dprQuery = matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
279
400
  this.dprQuery.addEventListener('change', this.onDprChange);
280
401
  }
402
+ watchTheme() {
403
+ const view = this.ownerDocument.defaultView;
404
+ if (!view)
405
+ return;
406
+ this.colorSchemeQuery = view.matchMedia?.('(prefers-color-scheme: dark)');
407
+ this.colorSchemeQuery?.addEventListener('change', this.onColorSchemeChange);
408
+ if (typeof MutationObserver === 'undefined')
409
+ return;
410
+ const targets = [this];
411
+ let parent = this.parentElement;
412
+ while (parent) {
413
+ targets.push(parent);
414
+ parent = parent.parentElement;
415
+ }
416
+ this.themeObserver = new MutationObserver(this.queueThemeRefresh);
417
+ for (const target of targets) {
418
+ this.themeObserver.observe(target, {
419
+ attributes: true,
420
+ attributeFilter: ['class', 'style', 'data-theme', 'data-color-scheme'],
421
+ });
422
+ }
423
+ }
281
424
  willUpdate(changed) {
282
- if (changed.has('mode') || changed.has('rowLabels') || changed.has('colLabels') || changed.has('days')) {
425
+ if (changed.has('mode') ||
426
+ changed.has('rowLabels') ||
427
+ changed.has('colLabels') ||
428
+ changed.has('days') ||
429
+ changed.has('firstDayOfWeek')) {
283
430
  // The previous focus/hover cursor may no longer address a real cell
284
431
  // once the grid's shape (or mode) changes out from under it —
285
432
  // stroking a focus ring at a stale (row, col)/(week, weekday), or
286
433
  // leaving the live region announcing a stale value, would be actively
287
434
  // misleading, so drop both instead. Reactive-property writes belong in
288
435
  // willUpdate() (not updated()), which folds them into this same
289
- // render rather than scheduling a whole extra update pass.
436
+ // render rather than scheduling a whole extra update pass. A
437
+ // `firstDayOfWeek` change reshuffles every cell's week/weekday the same
438
+ // way a `days` change does, so it resets the cursor too.
290
439
  this.focusedCell = null;
291
440
  this.hoverCell = null;
292
441
  this.liveText = '';
293
442
  }
443
+ // Calendar-mode grid layout depends only on `days`/`firstDayOfWeek` (not
444
+ // `mode`) — rebuilt here, once per cycle, instead of independently by
445
+ // every call site (see `cachedCalendarGrid`'s doc comment).
446
+ if (changed.has('days') || changed.has('firstDayOfWeek') || !this.hasUpdated) {
447
+ this.cachedCalendarGrid = buildCalendarGrid(this.days, this.firstDayOfWeek);
448
+ this.cachedCalendarSortedValues = this.cachedCalendarGrid.cells
449
+ .map((cell) => cell.value)
450
+ .filter((value) => Number.isFinite(value) && value >= 0)
451
+ .sort((a, b) => a - b);
452
+ this.cachedCalendarCellsByPos = new Map(this.cachedCalendarGrid.cells.map((cell) => [`${cell.week}:${cell.weekday}`, cell]));
453
+ }
294
454
  if (!this.hasUpdated) {
295
455
  this.authorSuppliedRole = this.hasAttribute('role');
296
456
  this.authorSuppliedAriaLabel = this.hasAttribute('aria-label');
297
457
  }
298
- this.cachedValueRange = this.computeValueRange();
458
+ // A hover/keyboard-focus @state() change (hoverCell/focusedCell/liveText)
459
+ // triggers this same willUpdate() but never touches values/days/mode, so
460
+ // gating the O(rows*cols) rescan the same way the focus/hover reset above
461
+ // is gated avoids redoing it on every pointer move.
462
+ if (changed.has('values') || changed.has('days') || changed.has('mode') || !this.hasUpdated) {
463
+ this.cachedValueRange = this.computeValueRange();
464
+ }
299
465
  const bounds = this.cachedValueRange;
300
466
  const range = bounds ? `${bounds[0]}–${bounds[1]}` : 'no data';
301
467
  // Only default role/aria-label when the author hasn't already supplied
@@ -331,7 +497,29 @@ export class LyraHeatmap extends LyraElement {
331
497
  const source = this.mode === 'calendar' ? this.days.map((d) => d.value) : this.values.flat();
332
498
  return minMax(source.filter((v) => Number.isFinite(v) && v >= 0));
333
499
  }
334
- updated() {
500
+ updated(changed) {
501
+ if ([
502
+ 'values',
503
+ 'rowLabels',
504
+ 'colLabels',
505
+ 'cellSize',
506
+ 'valueLabel',
507
+ 'scale',
508
+ 'fitToWidth',
509
+ 'mode',
510
+ 'days',
511
+ 'bucketCount',
512
+ 'annotations',
513
+ 'columnX',
514
+ 'rowY',
515
+ 'firstDayOfWeek',
516
+ 'focusedCell',
517
+ ].some((name) => changed.has(name))) {
518
+ this.draw();
519
+ }
520
+ }
521
+ /** Redraws canvas content after an upstream token or theme change. */
522
+ refreshTheme() {
335
523
  this.draw();
336
524
  }
337
525
  /** Reads the customizable ramp endpoints off the host's computed style. */
@@ -367,17 +555,63 @@ export class LyraHeatmap extends LyraElement {
367
555
  else
368
556
  this.drawMatrix();
369
557
  }
558
+ colorRamp(bucketCount) {
559
+ const [scaleLo, scaleHi] = this.scaleEndpoints();
560
+ const normalizedBucketCount = normalizeBucketCount(bucketCount);
561
+ const key = `${scaleLo}\u0000${scaleHi}\u0000${normalizedBucketCount}`;
562
+ if (this.cachedRamp?.key === key)
563
+ return this.cachedRamp;
564
+ const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
565
+ const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
566
+ const colors = Array.from({ length: normalizedBucketCount }, (_, i) => mixRgb(loRgb, hiRgb, i / (normalizedBucketCount - 1)));
567
+ this.cachedRamp = { key, colors, loRgb, hiRgb };
568
+ return this.cachedRamp;
569
+ }
570
+ /**
571
+ * Effective per-cell size in calendar mode — mirrors `matrixCellSize()`
572
+ * exactly: `fitToWidth` derives it from the host's measured width,
573
+ * otherwise it's the (possibly explicitly-set) `cellSize` property, which
574
+ * itself falls back to today's original 11px calendar default when left
575
+ * unset (see `cellSize`'s accessor doc comment). Shared by `columnXFor()`,
576
+ * `rowYFor()`, `drawCalendar()`, and the hit-testing below (`weekAtX()`,
577
+ * `weekdayAtY()`, `cellRect()`) so they always agree on exactly the same
578
+ * geometry as what's actually painted.
579
+ */
580
+ calendarCellSize() {
581
+ const { weekCount } = this.cachedCalendarGrid;
582
+ if (this.fitToWidth && weekCount > 0) {
583
+ // Unlike matrix mode's contiguous cells, calendar columns are spaced
584
+ // `cellSize + CAL_GAP` apart (see `columnXFor()`), so the gap has to be
585
+ // subtracted back out here — otherwise the painted width would overshoot
586
+ // `hostWidth` by `weekCount * CAL_GAP` (every column's gap, once each).
587
+ const hostWidth = this.clientWidth || CAL_PAD_LEFT + weekCount * (this.cellSize + CAL_GAP);
588
+ return Math.max(4, (hostWidth - CAL_PAD_LEFT) / weekCount - CAL_GAP);
589
+ }
590
+ return this.cellSize;
591
+ }
370
592
  /**
371
593
  * Calendar-mode week-column x-origin (canvas-local CSS px) — `columnX(week)`
372
- * when set, otherwise the original evenly-spaced formula. Shared by every
373
- * calendar-mode drawing and hit-testing call site (`drawCalendar()`,
594
+ * when set, otherwise the original evenly-spaced formula (now derived from
595
+ * `calendarCellSize()` rather than the fixed `CAL_CELL` constant). Shared
596
+ * by every calendar-mode drawing and hit-testing call site (`drawCalendar()`,
374
597
  * `hitTestCalendar()`/`weekAtX()`, `cellRect()`) so painted geometry and
375
598
  * interactive hit-testing never disagree — mirrors the existing
376
599
  * `matrixCellSize()` invariant for matrix mode's
377
600
  * `drawMatrix()`/`hitTestMatrix()`/`cellRect()`.
378
601
  */
379
602
  columnXFor(week) {
380
- return this.columnX ? this.columnX(week) : CAL_PAD_LEFT + week * (CAL_CELL + CAL_GAP);
603
+ return this.columnX ? this.columnX(week) : CAL_PAD_LEFT + week * (this.calendarCellSize() + CAL_GAP);
604
+ }
605
+ /**
606
+ * Calendar-mode weekday-row y-origin (canvas-local CSS px) — `rowY(weekday)`
607
+ * when set, otherwise the original evenly-spaced formula (now derived from
608
+ * `calendarCellSize()` rather than the fixed `CAL_CELL` constant). The
609
+ * vertical analogue of `columnXFor()`, mirroring its exact
610
+ * dispatch-with-computed-fallback shape and shared by the same
611
+ * drawing/hit-testing/focus-ring call sites.
612
+ */
613
+ rowYFor(weekday) {
614
+ return this.rowY ? this.rowY(weekday) : CAL_LABEL_H + weekday * (this.calendarCellSize() + CAL_GAP);
381
615
  }
382
616
  /**
383
617
  * Inverse of `columnXFor()`: resolves an x position (canvas-local CSS px)
@@ -391,28 +625,51 @@ export class LyraHeatmap extends LyraElement {
391
625
  */
392
626
  weekAtX(x, weekCount) {
393
627
  if (!this.columnX) {
394
- const week = Math.floor((x - CAL_PAD_LEFT) / (CAL_CELL + CAL_GAP));
628
+ const cellSize = this.calendarCellSize();
629
+ const week = Math.floor((x - CAL_PAD_LEFT) / (cellSize + CAL_GAP));
395
630
  return week >= 0 && week < weekCount ? week : null;
396
631
  }
397
632
  for (let week = 0; week < weekCount; week++) {
398
633
  const start = this.columnXFor(week);
399
- const end = week + 1 < weekCount ? this.columnXFor(week + 1) : start + (CAL_CELL + CAL_GAP);
634
+ const end = week + 1 < weekCount ? this.columnXFor(week + 1) : start + (this.calendarCellSize() + CAL_GAP);
400
635
  if (x >= start && x < end)
401
636
  return week;
402
637
  }
403
638
  return null;
404
639
  }
405
640
  /**
406
- * Locale-derived weekday-axis labels for the Sunday..Saturday rows drawn
407
- * down the left of the calendar grid. Only indices 1 (Mon), 3 (Wed), and 5
408
- * (Fri) are filled in matching today's sparse every-other-day label
409
- * density the rest stay blank. `firstWeekStart` is always a UTC Sunday
410
- * by construction (see `buildCalendarGrid()`), so weekday index 0..6 is a
411
- * fixed Sun..Sat positional mapping; only the label *text* for indices
412
- * 1/3/5 is derived here, via `Intl.DateTimeFormat` on a real UTC date that
413
- * actually falls on that weekday, so it follows the runtime locale instead
414
- * of a hardcoded English array (see `calendarCellText()`'s tooltip text
415
- * for the same pattern).
641
+ * Inverse of `rowYFor()`: resolves a y position (canvas-local CSS px) to
642
+ * the weekday row it falls in (0-6), or `null` if it's outside every row.
643
+ * Mirrors `weekAtX()`'s closed-form-vs-scan split for the same reason: the
644
+ * default spacing has a closed-form inverse; an arbitrary `rowY` override
645
+ * doesn't.
646
+ */
647
+ weekdayAtY(y) {
648
+ if (!this.rowY) {
649
+ const cellSize = this.calendarCellSize();
650
+ const weekday = Math.floor((y - CAL_LABEL_H) / (cellSize + CAL_GAP));
651
+ return weekday >= 0 && weekday < 7 ? weekday : null;
652
+ }
653
+ for (let weekday = 0; weekday < 7; weekday++) {
654
+ const start = this.rowYFor(weekday);
655
+ const end = weekday + 1 < 7 ? this.rowYFor(weekday + 1) : start + (this.calendarCellSize() + CAL_GAP);
656
+ if (y >= start && y < end)
657
+ return weekday;
658
+ }
659
+ return null;
660
+ }
661
+ /**
662
+ * Locale-derived weekday-axis labels for the 7 rows drawn down the left of
663
+ * the calendar grid. Only indices 1, 3, and 5 (relative to `firstWeekStart`)
664
+ * are filled in — matching today's sparse every-other-day label density —
665
+ * the rest stay blank. `firstWeekStart` is always the UTC
666
+ * `firstDayOfWeek`-weekday by construction (see `buildCalendarGrid()`), so
667
+ * weekday index 0..6 is a fixed positional mapping relative to it (with the
668
+ * default `firstDayOfWeek` of 0, that's the original Sun..Sat mapping,
669
+ * unchanged); only the label *text* for indices 1/3/5 is derived here, via
670
+ * `Intl.DateTimeFormat` on a real UTC date that actually falls on that
671
+ * weekday, so it follows the runtime locale instead of a hardcoded English
672
+ * array (see `calendarCellText()`'s tooltip text for the same pattern).
416
673
  */
417
674
  weekdayLabels(firstWeekStart) {
418
675
  const formatter = new Intl.DateTimeFormat(undefined, { weekday: 'short', timeZone: 'UTC' });
@@ -425,9 +682,14 @@ export class LyraHeatmap extends LyraElement {
425
682
  drawCalendar() {
426
683
  if (!this.canvas)
427
684
  return;
428
- const { cells, weekCount, firstWeekStart, monthLabels } = buildCalendarGrid(this.days);
685
+ const { cells, weekCount, firstWeekStart, monthLabels } = this.cachedCalendarGrid;
686
+ const cellSize = this.calendarCellSize();
429
687
  const w = this.columnXFor(Math.max(1, weekCount));
430
- const h = CAL_LABEL_H + 7 * (CAL_CELL + CAL_GAP);
688
+ // Derived from `rowYFor(7)` (one past the last of the 7 weekday rows), the
689
+ // vertical mirror of `w`'s `columnXFor(weekCount)` — so a custom `rowY`
690
+ // that spaces rows out further than the default formula still gets a
691
+ // canvas tall enough to paint every row, instead of clipping them.
692
+ const h = this.rowYFor(7);
431
693
  const dpr = window.devicePixelRatio || 1;
432
694
  this.canvas.width = w * dpr;
433
695
  this.canvas.height = h * dpr;
@@ -438,24 +700,39 @@ export class LyraHeatmap extends LyraElement {
438
700
  return;
439
701
  ctx.scale(dpr, dpr);
440
702
  ctx.clearRect(0, 0, w, h);
441
- const sortedValues = cells
442
- .map((c) => c.value)
443
- .filter((v) => Number.isFinite(v) && v >= 0)
444
- .sort((a, b) => a - b);
445
- const [scaleLo, scaleHi] = this.scaleEndpoints();
446
- const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
447
- const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
448
- const buckets = resolveBucketCount(this.bucketCount);
449
- const ramp = Array.from({ length: buckets }, (_, i) => mixRgb(loRgb, hiRgb, i / (buckets - 1)));
703
+ // Normalize at the allocation boundary as a final guard even though the
704
+ // public accessor and attribute converter already normalize their inputs.
705
+ const buckets = normalizeBucketCount(this.bucketCount);
706
+ const ramp = this.colorRamp(buckets).colors;
450
707
  const noDataFill = this.noDataFill();
451
- for (const cell of cells) {
452
- const x = this.columnXFor(cell.week);
453
- const y = CAL_LABEL_H + cell.weekday * (CAL_CELL + CAL_GAP);
454
- ctx.fillStyle =
455
- cell.value < 0 || !Number.isFinite(cell.value)
456
- ? noDataFill
457
- : ramp[quartileBucket(cell.value, sortedValues, buckets)];
458
- ctx.fillRect(x, y, CAL_CELL, CAL_CELL);
708
+ // Only consulted when `scale === 'sqrt'` — mirrors `drawMatrix()`'s own
709
+ // `hi` bound, reusing the already-cached value range instead of
710
+ // rescanning `sortedValues` for its max.
711
+ const bounds = this.cachedValueRange;
712
+ const hi = bounds ? bounds[1] : 1;
713
+ // Indexed once per draw so every (week, weekday) grid position can be
714
+ // painted below, not just positions with a matching entry in `cells` — a
715
+ // day missing from `days` entirely (a gap in a sparse calendar) still
716
+ // gets a real grid cell (see `calendarCellAt()`'s doc comment for the
717
+ // same `-1` no-data sentinel used here) instead of being left
718
+ // transparent.
719
+ for (let week = 0; week < weekCount; week++) {
720
+ for (let weekday = 0; weekday < 7; weekday++) {
721
+ const value = this.cachedCalendarCellsByPos.get(`${week}:${weekday}`)?.value ?? -1;
722
+ const x = this.columnXFor(week);
723
+ const y = this.rowYFor(weekday);
724
+ if (value < 0 || !Number.isFinite(value)) {
725
+ ctx.fillStyle = noDataFill;
726
+ }
727
+ else if (this.scale === 'sqrt') {
728
+ const step = sqrtStep(value, hi, buckets);
729
+ ctx.fillStyle = step < 0 ? noDataFill : ramp[step];
730
+ }
731
+ else {
732
+ ctx.fillStyle = ramp[quartileBucket(value, this.cachedCalendarSortedValues, buckets)];
733
+ }
734
+ ctx.fillRect(x, y, cellSize, cellSize);
735
+ }
459
736
  }
460
737
  // Annotation ring overlay, stroked after the fill pass so it reads
461
738
  // clearly over the data-driven cell color — see drawMatrix()'s twin of
@@ -470,8 +747,8 @@ export class LyraHeatmap extends LyraElement {
470
747
  if (!match)
471
748
  continue;
472
749
  const x = this.columnXFor(match.week);
473
- const y = CAL_LABEL_H + match.weekday * (CAL_CELL + CAL_GAP);
474
- ctx.strokeRect(x + 0.5, y + 0.5, CAL_CELL - 1, CAL_CELL - 1);
750
+ const y = this.rowYFor(match.weekday);
751
+ ctx.strokeRect(x + 0.5, y + 0.5, cellSize - 1, cellSize - 1);
475
752
  }
476
753
  }
477
754
  // Keyboard focus ring, redrawn on top of the fill pass (and any
@@ -482,8 +759,8 @@ export class LyraHeatmap extends LyraElement {
482
759
  ctx.lineWidth = RING_LINE_WIDTH;
483
760
  ctx.strokeStyle = this.focusRingColor();
484
761
  const x = this.columnXFor(week);
485
- const y = CAL_LABEL_H + weekday * (CAL_CELL + CAL_GAP);
486
- ctx.strokeRect(x + 0.5, y + 0.5, CAL_CELL - 1, CAL_CELL - 1);
762
+ const y = this.rowYFor(weekday);
763
+ ctx.strokeRect(x + 0.5, y + 0.5, cellSize - 1, cellSize - 1);
487
764
  }
488
765
  }
489
766
  ctx.fillStyle = this.labelColor();
@@ -494,7 +771,7 @@ export class LyraHeatmap extends LyraElement {
494
771
  const WEEKDAY_LABELS = this.weekdayLabels(firstWeekStart);
495
772
  WEEKDAY_LABELS.forEach((label, weekday) => {
496
773
  if (label)
497
- ctx.fillText(label, 2, CAL_LABEL_H + weekday * (CAL_CELL + CAL_GAP) + CAL_CELL - 1);
774
+ ctx.fillText(label, 2, this.rowYFor(weekday) + cellSize - 1);
498
775
  });
499
776
  }
500
777
  /**
@@ -532,10 +809,9 @@ export class LyraHeatmap extends LyraElement {
532
809
  const bounds = this.cachedValueRange;
533
810
  const lo = bounds ? bounds[0] : 0;
534
811
  const hi = bounds ? bounds[1] : 1;
535
- const [scaleLo, scaleHi] = this.scaleEndpoints();
536
- const loRgb = resolveRgb(scaleLo, FALLBACK_SCALE_LO);
537
- const hiRgb = resolveRgb(scaleHi, FALLBACK_SCALE_HI);
538
- const ramp = Array.from({ length: RAMP_STEPS }, (_, i) => mixRgb(loRgb, hiRgb, i / (RAMP_STEPS - 1)));
812
+ const rampData = this.colorRamp(RAMP_STEPS);
813
+ const ramp = rampData.colors;
814
+ const { loRgb, hiRgb } = rampData;
539
815
  const noDataFill = this.noDataFill();
540
816
  for (let r = 0; r < rows; r++) {
541
817
  for (let c = 0; c < cols; c++) {
@@ -627,12 +903,12 @@ export class LyraHeatmap extends LyraElement {
627
903
  return { row, col };
628
904
  }
629
905
  hitTestCalendar(x, y) {
630
- const { weekCount } = buildCalendarGrid(this.days);
906
+ const { weekCount } = this.cachedCalendarGrid;
631
907
  if (weekCount === 0)
632
908
  return null;
633
909
  const week = this.weekAtX(x, weekCount);
634
- const weekday = Math.floor((y - CAL_LABEL_H) / (CAL_CELL + CAL_GAP));
635
- if (week === null || weekday < 0 || weekday > 6)
910
+ const weekday = this.weekdayAtY(y);
911
+ if (week === null || weekday === null)
636
912
  return null;
637
913
  return { week, weekday };
638
914
  }
@@ -645,7 +921,7 @@ export class LyraHeatmap extends LyraElement {
645
921
  * sentinel uses in matrix mode), instead of being unresolvable.
646
922
  */
647
923
  calendarCellAt(pos) {
648
- const { cells, firstWeekStart } = buildCalendarGrid(this.days);
924
+ const { cells, firstWeekStart } = this.cachedCalendarGrid;
649
925
  const match = cells.find((c) => c.week === pos.week && c.weekday === pos.weekday);
650
926
  if (match)
651
927
  return { date: match.date, value: match.value };
@@ -659,11 +935,12 @@ export class LyraHeatmap extends LyraElement {
659
935
  */
660
936
  cellRect(pos) {
661
937
  if ('week' in pos) {
938
+ const cellSize = this.calendarCellSize();
662
939
  return {
663
940
  x: this.columnXFor(pos.week),
664
- y: CAL_LABEL_H + pos.weekday * (CAL_CELL + CAL_GAP),
665
- w: CAL_CELL,
666
- h: CAL_CELL,
941
+ y: this.rowYFor(pos.weekday),
942
+ w: cellSize,
943
+ h: cellSize,
667
944
  };
668
945
  }
669
946
  const cellSize = this.matrixCellSize(this.colLabels.length);
@@ -777,7 +1054,7 @@ export class LyraHeatmap extends LyraElement {
777
1054
  this.announce(next);
778
1055
  }
779
1056
  onCalendarKeyDown(e) {
780
- const { weekCount } = buildCalendarGrid(this.days);
1057
+ const { weekCount } = this.cachedCalendarGrid;
781
1058
  if (weekCount === 0)
782
1059
  return;
783
1060
  if (e.key === 'Enter' || e.key === ' ') {
@@ -851,7 +1128,7 @@ __decorate([
851
1128
  ], LyraHeatmap.prototype, "values", void 0);
852
1129
  __decorate([
853
1130
  property({ type: Number, attribute: 'cell-size' })
854
- ], LyraHeatmap.prototype, "cellSize", void 0);
1131
+ ], LyraHeatmap.prototype, "cellSize", null);
855
1132
  __decorate([
856
1133
  property({ attribute: 'value-label' })
857
1134
  ], LyraHeatmap.prototype, "valueLabel", void 0);
@@ -868,8 +1145,11 @@ __decorate([
868
1145
  property({ attribute: false })
869
1146
  ], LyraHeatmap.prototype, "days", void 0);
870
1147
  __decorate([
871
- property({ attribute: 'bucket-count', type: Number })
872
- ], LyraHeatmap.prototype, "bucketCount", void 0);
1148
+ property({ type: Number, attribute: 'first-day-of-week' })
1149
+ ], LyraHeatmap.prototype, "firstDayOfWeek", void 0);
1150
+ __decorate([
1151
+ property({ attribute: 'bucket-count', converter: bucketCountConverter })
1152
+ ], LyraHeatmap.prototype, "bucketCount", null);
873
1153
  __decorate([
874
1154
  property({ attribute: false })
875
1155
  ], LyraHeatmap.prototype, "annotations", void 0);
@@ -879,6 +1159,9 @@ __decorate([
879
1159
  __decorate([
880
1160
  property({ attribute: false })
881
1161
  ], LyraHeatmap.prototype, "columnX", void 0);
1162
+ __decorate([
1163
+ property({ attribute: false })
1164
+ ], LyraHeatmap.prototype, "rowY", void 0);
882
1165
  __decorate([
883
1166
  query('canvas')
884
1167
  ], LyraHeatmap.prototype, "canvas", void 0);