@aceshooting/lyra-ui 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +12 -1
  2. package/custom-elements.json +21460 -8489
  3. package/dist/components/app-rail/app-rail.d.ts +171 -0
  4. package/dist/components/app-rail/app-rail.js +483 -0
  5. package/dist/components/app-rail/app-rail.styles.d.ts +1 -0
  6. package/dist/components/app-rail/app-rail.styles.js +125 -0
  7. package/dist/components/attachment-chip/attachment-chip.d.ts +114 -0
  8. package/dist/components/attachment-chip/attachment-chip.js +312 -0
  9. package/dist/components/attachment-chip/attachment-chip.styles.d.ts +1 -0
  10. package/dist/components/attachment-chip/attachment-chip.styles.js +184 -0
  11. package/dist/components/attachment-trigger/attachment-trigger.d.ts +83 -0
  12. package/dist/components/attachment-trigger/attachment-trigger.js +258 -0
  13. package/dist/components/attachment-trigger/attachment-trigger.styles.d.ts +1 -0
  14. package/dist/components/attachment-trigger/attachment-trigger.styles.js +68 -0
  15. package/dist/components/chart/box-plot.js +11 -1
  16. package/dist/components/chart/chart-loader.d.ts +44 -13
  17. package/dist/components/chart/chart-loader.js +65 -18
  18. package/dist/components/chart/chart.d.ts +12 -2
  19. package/dist/components/chart/chart.js +121 -14
  20. package/dist/components/chart/histogram-bin.d.ts +4 -1
  21. package/dist/components/chart/histogram-bin.js +24 -14
  22. package/dist/components/chart/lite-chart.d.ts +63 -0
  23. package/dist/components/chart/lite-chart.js +181 -23
  24. package/dist/components/chart/lite-chart.styles.js +9 -0
  25. package/dist/components/chat-composer/chat-composer.d.ts +92 -0
  26. package/dist/components/chat-composer/chat-composer.js +285 -0
  27. package/dist/components/chat-composer/chat-composer.styles.d.ts +1 -0
  28. package/dist/components/chat-composer/chat-composer.styles.js +135 -0
  29. package/dist/components/chat-message/chat-message.d.ts +113 -0
  30. package/dist/components/chat-message/chat-message.js +291 -0
  31. package/dist/components/chat-message/chat-message.styles.d.ts +1 -0
  32. package/dist/components/chat-message/chat-message.styles.js +203 -0
  33. package/dist/components/checkbox/checkbox.d.ts +59 -0
  34. package/dist/components/checkbox/checkbox.js +230 -0
  35. package/dist/components/checkbox/checkbox.styles.d.ts +1 -0
  36. package/dist/components/checkbox/checkbox.styles.js +70 -0
  37. package/dist/components/chip/chip-group.d.ts +56 -0
  38. package/dist/components/chip/chip-group.js +124 -0
  39. package/dist/components/chip/chip-group.styles.d.ts +1 -0
  40. package/dist/components/chip/chip-group.styles.js +57 -0
  41. package/dist/components/chip/chip.d.ts +64 -0
  42. package/dist/components/chip/chip.js +115 -0
  43. package/dist/components/chip/chip.styles.d.ts +1 -0
  44. package/dist/components/chip/chip.styles.js +115 -0
  45. package/dist/components/citation-badge/citation-badge.d.ts +113 -0
  46. package/dist/components/citation-badge/citation-badge.js +300 -0
  47. package/dist/components/citation-badge/citation-badge.styles.d.ts +1 -0
  48. package/dist/components/citation-badge/citation-badge.styles.js +96 -0
  49. package/dist/components/code-block/code-block.d.ts +89 -0
  50. package/dist/components/code-block/code-block.js +317 -0
  51. package/dist/components/code-block/code-block.styles.d.ts +1 -0
  52. package/dist/components/code-block/code-block.styles.js +187 -0
  53. package/dist/components/code-block/code-loader.d.ts +44 -0
  54. package/dist/components/code-block/code-loader.js +79 -0
  55. package/dist/components/combobox/combobox.d.ts +19 -1
  56. package/dist/components/combobox/combobox.js +84 -14
  57. package/dist/components/combobox/option.d.ts +5 -1
  58. package/dist/components/combobox/option.js +35 -0
  59. package/dist/components/context-meter/context-meter.d.ts +53 -0
  60. package/dist/components/context-meter/context-meter.js +150 -0
  61. package/dist/components/context-meter/context-meter.styles.d.ts +1 -0
  62. package/dist/components/context-meter/context-meter.styles.js +120 -0
  63. package/dist/components/conversation-item/conversation-item.d.ts +128 -0
  64. package/dist/components/conversation-item/conversation-item.js +322 -0
  65. package/dist/components/conversation-item/conversation-item.styles.d.ts +1 -0
  66. package/dist/components/conversation-item/conversation-item.styles.js +155 -0
  67. package/dist/components/date-picker/date-input.d.ts +24 -0
  68. package/dist/components/date-picker/date-input.js +134 -22
  69. package/dist/components/date-picker/date-picker.d.ts +7 -0
  70. package/dist/components/date-picker/date-picker.js +85 -16
  71. package/dist/components/dialog/confirm.d.ts +40 -0
  72. package/dist/components/dialog/confirm.js +79 -0
  73. package/dist/components/dialog/dialog.d.ts +85 -0
  74. package/dist/components/dialog/dialog.js +272 -0
  75. package/dist/components/dialog/dialog.styles.d.ts +1 -0
  76. package/dist/components/dialog/dialog.styles.js +52 -0
  77. package/dist/components/dock-panel/dock-panel.d.ts +134 -0
  78. package/dist/components/dock-panel/dock-panel.js +345 -0
  79. package/dist/components/dock-panel/dock-panel.styles.d.ts +1 -0
  80. package/dist/components/dock-panel/dock-panel.styles.js +145 -0
  81. package/dist/components/document-preview/document-preview.d.ts +135 -0
  82. package/dist/components/document-preview/document-preview.js +372 -0
  83. package/dist/components/document-preview/document-preview.styles.d.ts +1 -0
  84. package/dist/components/document-preview/document-preview.styles.js +191 -0
  85. package/dist/components/export-button/csv.js +1 -1
  86. package/dist/components/export-button/export-button.js +2 -0
  87. package/dist/components/flag/flag.d.ts +42 -5
  88. package/dist/components/flag/flag.js +70 -3
  89. package/dist/components/gauge/gauge.js +19 -10
  90. package/dist/components/generation-status/generation-status.d.ts +109 -0
  91. package/dist/components/generation-status/generation-status.js +273 -0
  92. package/dist/components/generation-status/generation-status.styles.d.ts +1 -0
  93. package/dist/components/generation-status/generation-status.styles.js +84 -0
  94. package/dist/components/heatmap/calendar-grid.d.ts +4 -1
  95. package/dist/components/heatmap/calendar-grid.js +25 -12
  96. package/dist/components/heatmap/heatmap-scale.d.ts +6 -0
  97. package/dist/components/heatmap/heatmap-scale.js +7 -1
  98. package/dist/components/heatmap/heatmap.d.ts +89 -3
  99. package/dist/components/heatmap/heatmap.js +158 -29
  100. package/dist/components/json-viewer/json-viewer.d.ts +53 -0
  101. package/dist/components/json-viewer/json-viewer.js +283 -0
  102. package/dist/components/json-viewer/json-viewer.styles.d.ts +1 -0
  103. package/dist/components/json-viewer/json-viewer.styles.js +176 -0
  104. package/dist/components/kbd/kbd.d.ts +77 -0
  105. package/dist/components/kbd/kbd.js +214 -0
  106. package/dist/components/kbd/kbd.styles.d.ts +1 -0
  107. package/dist/components/kbd/kbd.styles.js +52 -0
  108. package/dist/components/live-region/live-region.d.ts +81 -0
  109. package/dist/components/live-region/live-region.js +153 -0
  110. package/dist/components/live-region/live-region.styles.d.ts +1 -0
  111. package/dist/components/live-region/live-region.styles.js +10 -0
  112. package/dist/components/map/map.d.ts +8 -6
  113. package/dist/components/map/map.js +44 -8
  114. package/dist/components/markdown/markdown-loader.d.ts +34 -0
  115. package/dist/components/markdown/markdown-loader.js +43 -0
  116. package/dist/components/markdown/markdown.d.ts +81 -0
  117. package/dist/components/markdown/markdown.js +284 -0
  118. package/dist/components/markdown/markdown.styles.d.ts +1 -0
  119. package/dist/components/markdown/markdown.styles.js +82 -0
  120. package/dist/components/media-card/media-card.d.ts +122 -0
  121. package/dist/components/media-card/media-card.js +295 -0
  122. package/dist/components/media-card/media-card.styles.d.ts +1 -0
  123. package/dist/components/media-card/media-card.styles.js +132 -0
  124. package/dist/components/mention-popover/mention-popover.d.ts +200 -0
  125. package/dist/components/mention-popover/mention-popover.js +475 -0
  126. package/dist/components/mention-popover/mention-popover.styles.d.ts +1 -0
  127. package/dist/components/mention-popover/mention-popover.styles.js +101 -0
  128. package/dist/components/menu/menu-item.d.ts +62 -0
  129. package/dist/components/menu/menu-item.js +119 -0
  130. package/dist/components/menu/menu-item.styles.d.ts +1 -0
  131. package/dist/components/menu/menu-item.styles.js +67 -0
  132. package/dist/components/menu/menu.d.ts +143 -0
  133. package/dist/components/menu/menu.js +365 -0
  134. package/dist/components/menu/menu.styles.d.ts +1 -0
  135. package/dist/components/menu/menu.styles.js +66 -0
  136. package/dist/components/model-select/model-select.d.ts +125 -0
  137. package/dist/components/model-select/model-select.js +490 -0
  138. package/dist/components/model-select/model-select.styles.d.ts +1 -0
  139. package/dist/components/model-select/model-select.styles.js +189 -0
  140. package/dist/components/model-settings-panel/model-settings-panel.d.ts +69 -0
  141. package/dist/components/model-settings-panel/model-settings-panel.js +145 -0
  142. package/dist/components/model-settings-panel/model-settings-panel.styles.d.ts +1 -0
  143. package/dist/components/model-settings-panel/model-settings-panel.styles.js +82 -0
  144. package/dist/components/playback/playback.d.ts +13 -1
  145. package/dist/components/playback/playback.js +47 -14
  146. package/dist/components/responsive-panel/responsive-panel.d.ts +150 -0
  147. package/dist/components/responsive-panel/responsive-panel.js +365 -0
  148. package/dist/components/responsive-panel/responsive-panel.styles.d.ts +1 -0
  149. package/dist/components/responsive-panel/responsive-panel.styles.js +103 -0
  150. package/dist/components/result-card/result-card.d.ts +44 -0
  151. package/dist/components/result-card/result-card.js +83 -0
  152. package/dist/components/result-card/result-card.styles.d.ts +1 -0
  153. package/dist/components/result-card/result-card.styles.js +50 -0
  154. package/dist/components/result-card/result-field.d.ts +36 -0
  155. package/dist/components/result-card/result-field.js +87 -0
  156. package/dist/components/result-card/result-field.styles.d.ts +1 -0
  157. package/dist/components/result-card/result-field.styles.js +32 -0
  158. package/dist/components/select/select.d.ts +45 -1
  159. package/dist/components/select/select.js +114 -17
  160. package/dist/components/select/select.styles.js +26 -2
  161. package/dist/components/slider/slider.d.ts +89 -0
  162. package/dist/components/slider/slider.js +352 -0
  163. package/dist/components/slider/slider.styles.d.ts +1 -0
  164. package/dist/components/slider/slider.styles.js +87 -0
  165. package/dist/components/source-card/source-card.d.ts +92 -0
  166. package/dist/components/source-card/source-card.js +148 -0
  167. package/dist/components/source-card/source-card.styles.d.ts +1 -0
  168. package/dist/components/source-card/source-card.styles.js +69 -0
  169. package/dist/components/source-list/source-list.d.ts +63 -0
  170. package/dist/components/source-list/source-list.js +122 -0
  171. package/dist/components/source-list/source-list.styles.d.ts +1 -0
  172. package/dist/components/source-list/source-list.styles.js +59 -0
  173. package/dist/components/sparkline/sparkline.js +27 -13
  174. package/dist/components/split/split.d.ts +93 -1
  175. package/dist/components/split/split.js +284 -15
  176. package/dist/components/split/split.styles.js +30 -0
  177. package/dist/components/stat/stat.d.ts +22 -1
  178. package/dist/components/stat/stat.js +49 -3
  179. package/dist/components/stat/stat.styles.js +20 -0
  180. package/dist/components/stream-status/stream-status.d.ts +110 -0
  181. package/dist/components/stream-status/stream-status.js +210 -0
  182. package/dist/components/stream-status/stream-status.styles.d.ts +1 -0
  183. package/dist/components/stream-status/stream-status.styles.js +103 -0
  184. package/dist/components/streaming-text/streaming-text.d.ts +98 -0
  185. package/dist/components/streaming-text/streaming-text.js +209 -0
  186. package/dist/components/streaming-text/streaming-text.styles.d.ts +1 -0
  187. package/dist/components/streaming-text/streaming-text.styles.js +62 -0
  188. package/dist/components/switch/switch.d.ts +58 -0
  189. package/dist/components/switch/switch.js +179 -0
  190. package/dist/components/switch/switch.styles.d.ts +1 -0
  191. package/dist/components/switch/switch.styles.js +75 -0
  192. package/dist/components/table/table.d.ts +73 -4
  193. package/dist/components/table/table.js +169 -20
  194. package/dist/components/table/table.styles.js +6 -1
  195. package/dist/components/tabs/tabs.d.ts +51 -0
  196. package/dist/components/tabs/tabs.js +190 -0
  197. package/dist/components/tabs/tabs.styles.d.ts +1 -0
  198. package/dist/components/tabs/tabs.styles.js +63 -0
  199. package/dist/components/thinking-panel/thinking-panel.d.ts +121 -0
  200. package/dist/components/thinking-panel/thinking-panel.js +235 -0
  201. package/dist/components/thinking-panel/thinking-panel.styles.d.ts +1 -0
  202. package/dist/components/thinking-panel/thinking-panel.styles.js +117 -0
  203. package/dist/components/toast/toast-item.d.ts +1 -0
  204. package/dist/components/toast/toast-item.js +31 -6
  205. package/dist/components/tool-approval-dialog/tool-approval-dialog.d.ts +130 -0
  206. package/dist/components/tool-approval-dialog/tool-approval-dialog.js +403 -0
  207. package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.d.ts +1 -0
  208. package/dist/components/tool-approval-dialog/tool-approval-dialog.styles.js +160 -0
  209. package/dist/components/tool-call-chip/tool-call-chip.d.ts +89 -0
  210. package/dist/components/tool-call-chip/tool-call-chip.js +288 -0
  211. package/dist/components/tool-call-chip/tool-call-chip.styles.d.ts +1 -0
  212. package/dist/components/tool-call-chip/tool-call-chip.styles.js +203 -0
  213. package/dist/components/tool-param-form/tool-param-form.d.ts +145 -0
  214. package/dist/components/tool-param-form/tool-param-form.js +366 -0
  215. package/dist/components/tool-param-form/tool-param-form.styles.d.ts +1 -0
  216. package/dist/components/tool-param-form/tool-param-form.styles.js +75 -0
  217. package/dist/components/tool-result-dialog/tool-result-dialog.d.ts +102 -0
  218. package/dist/components/tool-result-dialog/tool-result-dialog.js +393 -0
  219. package/dist/components/tool-result-dialog/tool-result-dialog.styles.d.ts +1 -0
  220. package/dist/components/tool-result-dialog/tool-result-dialog.styles.js +176 -0
  221. package/dist/components/tool-result-view/registry.d.ts +81 -0
  222. package/dist/components/tool-result-view/registry.js +84 -0
  223. package/dist/components/tool-result-view/tool-result-view.d.ts +55 -0
  224. package/dist/components/tool-result-view/tool-result-view.js +138 -0
  225. package/dist/components/tool-result-view/tool-result-view.styles.d.ts +1 -0
  226. package/dist/components/tool-result-view/tool-result-view.styles.js +13 -0
  227. package/dist/components/tool-select-dialog/tool-select-dialog.d.ts +151 -0
  228. package/dist/components/tool-select-dialog/tool-select-dialog.js +439 -0
  229. package/dist/components/tool-select-dialog/tool-select-dialog.styles.d.ts +1 -0
  230. package/dist/components/tool-select-dialog/tool-select-dialog.styles.js +182 -0
  231. package/dist/components/tree/tree-node.d.ts +11 -2
  232. package/dist/components/tree/tree-node.js +16 -6
  233. package/dist/components/tree/tree.d.ts +8 -2
  234. package/dist/components/tree/tree.js +18 -11
  235. package/dist/components/typing-indicator/typing-indicator.d.ts +60 -0
  236. package/dist/components/typing-indicator/typing-indicator.js +96 -0
  237. package/dist/components/typing-indicator/typing-indicator.styles.d.ts +1 -0
  238. package/dist/components/typing-indicator/typing-indicator.styles.js +132 -0
  239. package/dist/components/virtual-list/virtual-list.d.ts +173 -0
  240. package/dist/components/virtual-list/virtual-list.js +461 -0
  241. package/dist/components/virtual-list/virtual-list.styles.d.ts +1 -0
  242. package/dist/components/virtual-list/virtual-list.styles.js +44 -0
  243. package/dist/components/widget/widget.d.ts +10 -0
  244. package/dist/components/widget/widget.js +24 -1
  245. package/dist/components/widget/widget.styles.js +9 -2
  246. package/dist/components/word-cloud/word-cloud-layout.js +14 -1
  247. package/dist/components/word-cloud/word-cloud.d.ts +6 -0
  248. package/dist/components/word-cloud/word-cloud.js +25 -14
  249. package/dist/components/word-cloud/word-cloud.styles.js +4 -2
  250. package/dist/internal/announcer.d.ts +57 -0
  251. package/dist/internal/announcer.js +71 -0
  252. package/dist/internal/form-associated.d.ts +1 -0
  253. package/dist/internal/form-associated.js +55 -11
  254. package/dist/internal/group-by-recency.d.ts +50 -0
  255. package/dist/internal/group-by-recency.js +80 -0
  256. package/dist/internal/rtl.d.ts +23 -0
  257. package/dist/internal/rtl.js +28 -0
  258. package/dist/internal/scroll-lock.d.ts +10 -5
  259. package/dist/internal/scroll-lock.js +30 -14
  260. package/dist/internal/tokens.styles.js +6 -0
  261. package/dist/lyra.d.ts +130 -31
  262. package/dist/lyra.js +94 -26
  263. package/llms-full.txt +3921 -32
  264. package/llms.txt +16 -1
  265. package/package.json +23 -5
  266. package/dist/components/chart/bar-chart.stories.d.ts +0 -5
  267. package/dist/components/chart/bar-chart.stories.js +0 -20
  268. package/dist/components/chart/box-plot.stories.d.ts +0 -5
  269. package/dist/components/chart/box-plot.stories.js +0 -29
  270. package/dist/components/chart/bubble-chart.stories.d.ts +0 -5
  271. package/dist/components/chart/bubble-chart.stories.js +0 -26
  272. package/dist/components/chart/chart.stories.d.ts +0 -32
  273. package/dist/components/chart/chart.stories.js +0 -141
  274. package/dist/components/chart/doughnut-chart.stories.d.ts +0 -5
  275. package/dist/components/chart/doughnut-chart.stories.js +0 -26
  276. package/dist/components/chart/histogram.stories.d.ts +0 -5
  277. package/dist/components/chart/histogram.stories.js +0 -17
  278. package/dist/components/chart/line-chart.stories.d.ts +0 -22
  279. package/dist/components/chart/line-chart.stories.js +0 -99
  280. package/dist/components/chart/lite-chart.stories.d.ts +0 -12
  281. package/dist/components/chart/lite-chart.stories.js +0 -98
  282. package/dist/components/chart/pie-chart.stories.d.ts +0 -5
  283. package/dist/components/chart/pie-chart.stories.js +0 -26
  284. package/dist/components/chart/polar-area-chart.stories.d.ts +0 -5
  285. package/dist/components/chart/polar-area-chart.stories.js +0 -20
  286. package/dist/components/chart/radar-chart.stories.d.ts +0 -5
  287. package/dist/components/chart/radar-chart.stories.js +0 -20
  288. package/dist/components/chart/scatter-chart.stories.d.ts +0 -5
  289. package/dist/components/chart/scatter-chart.stories.js +0 -31
  290. package/dist/components/combobox/combobox.stories.d.ts +0 -24
  291. package/dist/components/combobox/combobox.stories.js +0 -102
  292. package/dist/components/date-picker/date-input.stories.d.ts +0 -10
  293. package/dist/components/date-picker/date-input.stories.js +0 -48
  294. package/dist/components/date-picker/date-picker.stories.d.ts +0 -10
  295. package/dist/components/date-picker/date-picker.stories.js +0 -25
  296. package/dist/components/empty/empty.stories.d.ts +0 -7
  297. package/dist/components/empty/empty.stories.js +0 -34
  298. package/dist/components/export-button/export-button.stories.d.ts +0 -7
  299. package/dist/components/export-button/export-button.stories.js +0 -37
  300. package/dist/components/file-input/file-input.stories.d.ts +0 -7
  301. package/dist/components/file-input/file-input.stories.js +0 -19
  302. package/dist/components/flag/flag.stories.d.ts +0 -5
  303. package/dist/components/flag/flag.stories.js +0 -17
  304. package/dist/components/gauge/gauge.stories.d.ts +0 -8
  305. package/dist/components/gauge/gauge.stories.js +0 -19
  306. package/dist/components/graph/graph.stories.d.ts +0 -8
  307. package/dist/components/graph/graph.stories.js +0 -68
  308. package/dist/components/heatmap/heatmap.stories.d.ts +0 -22
  309. package/dist/components/heatmap/heatmap.stories.js +0 -154
  310. package/dist/components/map/map.stories.d.ts +0 -15
  311. package/dist/components/map/map.stories.js +0 -135
  312. package/dist/components/playback/playback.stories.d.ts +0 -7
  313. package/dist/components/playback/playback.stories.js +0 -16
  314. package/dist/components/select/select.stories.d.ts +0 -19
  315. package/dist/components/select/select.stories.js +0 -75
  316. package/dist/components/skeleton/skeleton.stories.d.ts +0 -5
  317. package/dist/components/skeleton/skeleton.stories.js +0 -16
  318. package/dist/components/sparkline/sparkline.stories.d.ts +0 -11
  319. package/dist/components/sparkline/sparkline.stories.js +0 -39
  320. package/dist/components/split/split.stories.d.ts +0 -7
  321. package/dist/components/split/split.stories.js +0 -36
  322. package/dist/components/stat/stat.stories.d.ts +0 -8
  323. package/dist/components/stat/stat.stories.js +0 -64
  324. package/dist/components/table/table.stories.d.ts +0 -12
  325. package/dist/components/table/table.stories.js +0 -67
  326. package/dist/components/time-range/time-range.stories.d.ts +0 -8
  327. package/dist/components/time-range/time-range.stories.js +0 -26
  328. package/dist/components/toast/toast.stories.d.ts +0 -8
  329. package/dist/components/toast/toast.stories.js +0 -72
  330. package/dist/components/tree/tree.stories.d.ts +0 -9
  331. package/dist/components/tree/tree.stories.js +0 -52
  332. package/dist/components/widget/widget.stories.d.ts +0 -7
  333. package/dist/components/widget/widget.stories.js +0 -41
  334. package/dist/components/word-cloud/word-cloud.stories.d.ts +0 -10
  335. package/dist/components/word-cloud/word-cloud.stories.js +0 -55
@@ -23,7 +23,10 @@ export declare function parseIsoDate(iso: string): Date;
23
23
  * string yields an `Invalid Date` (`getTime()` is `NaN`), and letting that
24
24
  * single `NaN` reach the min/max below would poison `firstWeekStart` (and
25
25
  * therefore every cell's `week`) for the whole grid instead of just the bad
26
- * entry.
26
+ * entry. Likewise, an entry whose `date` is numerically well-formed but
27
+ * calendar-invalid (e.g. `'2026-02-30'`, which `Date.UTC` silently rolls
28
+ * over into March) is dropped rather than silently renormalized — see
29
+ * `isCalendarValid()`.
27
30
  */
28
31
  export declare function buildCalendarGrid(days: CalendarDay[]): {
29
32
  cells: CalendarCell[];
@@ -5,6 +5,12 @@ export function parseIsoDate(iso) {
5
5
  const [y, m, d] = iso.split('-').map(Number);
6
6
  return new Date(Date.UTC(y, m - 1, d));
7
7
  }
8
+ /** Whether `iso` (already run through parseIsoDate) actually round-trips —
9
+ * Date.UTC silently rolls e.g. day 30 of a 28-day February into March. */
10
+ function isCalendarValid(iso, parsed) {
11
+ const [y, m, d] = iso.split('-').map(Number);
12
+ return parsed.getUTCFullYear() === y && parsed.getUTCMonth() === m - 1 && parsed.getUTCDate() === d;
13
+ }
8
14
  /**
9
15
  * Lays `days` out onto a GitHub-style Sunday-Saturday x week grid. `days`
10
16
  * need not be sorted or contiguous; the grid spans from the earliest to the
@@ -15,12 +21,15 @@ export function parseIsoDate(iso) {
15
21
  * string yields an `Invalid Date` (`getTime()` is `NaN`), and letting that
16
22
  * single `NaN` reach the min/max below would poison `firstWeekStart` (and
17
23
  * therefore every cell's `week`) for the whole grid instead of just the bad
18
- * entry.
24
+ * entry. Likewise, an entry whose `date` is numerically well-formed but
25
+ * calendar-invalid (e.g. `'2026-02-30'`, which `Date.UTC` silently rolls
26
+ * over into March) is dropped rather than silently renormalized — see
27
+ * `isCalendarValid()`.
19
28
  */
20
29
  export function buildCalendarGrid(days) {
21
30
  const parsed = days
22
31
  .map((d) => ({ ...d, dateObj: parseIsoDate(d.date) }))
23
- .filter((d) => !Number.isNaN(d.dateObj.getTime()));
32
+ .filter((d) => !Number.isNaN(d.dateObj.getTime()) && isCalendarValid(d.date, d.dateObj));
24
33
  if (parsed.length === 0) {
25
34
  return { cells: [], weekCount: 0, firstWeekStart: new Date(0), monthLabels: [] };
26
35
  }
@@ -34,18 +43,22 @@ export function buildCalendarGrid(days) {
34
43
  return { date: d.date, value: d.value, week, weekday };
35
44
  });
36
45
  const weekCount = minMax(cells.map((c) => c.week))[1] + 1;
46
+ // Label every distinct (year, month) present in the data, not just months
47
+ // that happen to contain a Sunday-anchored cell — a sparse calendar (few
48
+ // days per month, none landing on a Sunday) would otherwise render zero
49
+ // month labels for months that do have data.
37
50
  const monthLabels = [];
38
- let lastMonth = -1;
39
- for (const cell of cells.slice().sort((a, b) => a.week - b.week)) {
51
+ const seenMonths = new Set();
52
+ for (const cell of cells.slice().sort((a, b) => a.week - b.week || a.weekday - b.weekday)) {
40
53
  const cellDate = parseIsoDate(cell.date);
41
- const month = cellDate.getUTCMonth();
42
- if (cell.weekday === 0 && month !== lastMonth) {
43
- lastMonth = month;
44
- monthLabels.push({
45
- week: cell.week,
46
- label: cellDate.toLocaleString('en', { month: 'short', timeZone: 'UTC' }),
47
- });
48
- }
54
+ const key = `${cellDate.getUTCFullYear()}-${cellDate.getUTCMonth()}`;
55
+ if (seenMonths.has(key))
56
+ continue;
57
+ seenMonths.add(key);
58
+ monthLabels.push({
59
+ week: cell.week,
60
+ label: cellDate.toLocaleString(undefined, { month: 'short', timeZone: 'UTC' }),
61
+ });
49
62
  }
50
63
  return { cells, weekCount, firstWeekStart, monthLabels };
51
64
  }
@@ -12,5 +12,11 @@ export declare function minMax(values: number[]): [number, number] | null;
12
12
  * Square-root-scaled bucket index in `[0, steps-1]`, or `-1` for no-data.
13
13
  * Compresses large counts so a single heavy cell doesn't wash out the rest
14
14
  * of a sequential color ramp.
15
+ *
16
+ * Only a negative `count` means "no data" here — the true no-data sentinel
17
+ * (`v < 0 || !Number.isFinite(v)`) is already filtered out by the caller
18
+ * (`drawMatrix()`) before `sqrtStep` is ever invoked, so a real `count === 0`
19
+ * (e.g. "zero events that day") reaches this function and must bucket to the
20
+ * lowest ramp step like any other legitimate value, not render as no-data.
15
21
  */
16
22
  export declare function sqrtStep(count: number, max: number, steps: number): number;
@@ -29,9 +29,15 @@ export function minMax(values) {
29
29
  * Square-root-scaled bucket index in `[0, steps-1]`, or `-1` for no-data.
30
30
  * Compresses large counts so a single heavy cell doesn't wash out the rest
31
31
  * of a sequential color ramp.
32
+ *
33
+ * Only a negative `count` means "no data" here — the true no-data sentinel
34
+ * (`v < 0 || !Number.isFinite(v)`) is already filtered out by the caller
35
+ * (`drawMatrix()`) before `sqrtStep` is ever invoked, so a real `count === 0`
36
+ * (e.g. "zero events that day") reaches this function and must bucket to the
37
+ * lowest ramp step like any other legitimate value, not render as no-data.
32
38
  */
33
39
  export function sqrtStep(count, max, steps) {
34
- if (count <= 0 || max <= 0)
40
+ if (count < 0 || max <= 0)
35
41
  return -1;
36
42
  const ratio = Math.sqrt(count) / Math.sqrt(max);
37
43
  return Math.min(steps - 1, Math.floor(ratio * steps));
@@ -79,10 +79,18 @@ export declare function mixColor(fromColor: string, toColor: string, t: number):
79
79
  * specific cells (e.g. to call out an anomaly), each one optionally
80
80
  * surfaced in the legend too via `[part="legend-annotation"]`.
81
81
  *
82
+ * `columnX` (calendar mode only) overrides the x-origin computed for each
83
+ * week column — drawing, hit-testing, the focus ring, and month-label
84
+ * positioning all consult it consistently, so a consumer can pixel-align a
85
+ * calendar's week columns with a sibling chart's coordinate system. Unset
86
+ * (the default) keeps the original evenly-spaced formula.
87
+ *
82
88
  * @customElement lyra-heatmap
83
89
  * @event lyra-cell-click - Fired on click, or Enter/Space on the
84
90
  * focused/hovered cell. `detail: { row, col, value }` in matrix mode,
85
- * `detail: { date, value }` in calendar mode.
91
+ * `detail: { date, value }` in calendar mode. `cellText` overrides the
92
+ * built-in English "Row X, Col Y: value" / "Mon DD: value" template used for
93
+ * both the hover tooltip and the keyboard live-region announcement.
86
94
  * @csspart base, canvas, tooltip, live-region, legend, legend-lo, legend-hi, legend-annotation
87
95
  */
88
96
  export declare class LyraHeatmap extends LyraElement {
@@ -107,6 +115,23 @@ export declare class LyraHeatmap extends LyraElement {
107
115
  bucketCount: number;
108
116
  /** Cells to ring-highlight — `row`/`col` in matrix mode, `date` in calendar mode. See `HeatmapAnnotation`. */
109
117
  annotations: HeatmapAnnotation[];
118
+ /** Formats the per-cell tooltip and keyboard live-region text — receives the cell position
119
+ * (`MatrixCellPos` in matrix mode, `CalendarCellPos` in calendar mode) and its value. Falls back to
120
+ * the built-in English "Row X, Col Y: value" / "Mon DD: value" template when unset. */
121
+ cellText?: (pos: MatrixCellPos | CalendarCellPos, value: number) => string;
122
+ /**
123
+ * Calendar mode only: overrides the x-origin (canvas-local CSS px) of week
124
+ * column `index` (0-based, same indexing as `CalendarCellPos.week`).
125
+ * Consulted consistently by every calendar-mode geometry call site —
126
+ * drawing, hit-testing, the keyboard focus ring, and month-label
127
+ * positioning — via the private `columnXFor()` helper, so painted
128
+ * geometry and pointer hit-testing never disagree. Lets a consumer
129
+ * pixel-align a calendar's week columns with a sibling chart's bars by
130
+ * supplying that chart the same coordinate function. Unset (the default)
131
+ * keeps today's evenly-spaced `CAL_PAD_LEFT + week * (CAL_CELL + CAL_GAP)`
132
+ * formula unchanged. Ignored in matrix mode.
133
+ */
134
+ columnX?: (index: number) => number;
110
135
  private canvas?;
111
136
  private resizeObserver?;
112
137
  private dprQuery?;
@@ -119,6 +144,19 @@ export declare class LyraHeatmap extends LyraElement {
119
144
  private focusedCell;
120
145
  /** Text of the visually-hidden `aria-live="polite"` status announcement, refreshed on every focus move. */
121
146
  private liveText;
147
+ /**
148
+ * Whether `role`/`aria-label` were present on the host *before* this
149
+ * element's own code ever wrote to them — snapshotted once, in the very
150
+ * first `willUpdate()` (guarded by `hasUpdated`, which Lit only flips to
151
+ * `true` after that first update commits). Re-checking `hasAttribute()` on
152
+ * every update instead would self-poison: once this element writes its own
153
+ * default `aria-label` on the first render, `hasAttribute('aria-label')`
154
+ * is permanently `true` afterwards, which would wrongly look like an
155
+ * author-supplied value and freeze the label instead of refreshing it as
156
+ * `values`/`days` change on later updates.
157
+ */
158
+ private authorSuppliedRole;
159
+ private authorSuppliedAriaLabel;
122
160
  connectedCallback(): void;
123
161
  disconnectedCallback(): void;
124
162
  private watchDpr;
@@ -148,6 +186,40 @@ export declare class LyraHeatmap extends LyraElement {
148
186
  /** Reads the customizable canvas-drawn annotation-ring stroke color off the host's computed style. */
149
187
  private annotationColor;
150
188
  private draw;
189
+ /**
190
+ * Calendar-mode week-column x-origin (canvas-local CSS px) — `columnX(week)`
191
+ * when set, otherwise the original evenly-spaced formula. Shared by every
192
+ * calendar-mode drawing and hit-testing call site (`drawCalendar()`,
193
+ * `hitTestCalendar()`/`weekAtX()`, `cellRect()`) so painted geometry and
194
+ * interactive hit-testing never disagree — mirrors the existing
195
+ * `matrixCellSize()` invariant for matrix mode's
196
+ * `drawMatrix()`/`hitTestMatrix()`/`cellRect()`.
197
+ */
198
+ private columnXFor;
199
+ /**
200
+ * Inverse of `columnXFor()`: resolves an x position (canvas-local CSS px)
201
+ * to the week column it falls in, or `null` if it's outside every column
202
+ * (`weekCount` is 0, or `x` doesn't land inside `[0, weekCount)`'s span).
203
+ * The default spacing has a closed-form inverse (division); an arbitrary
204
+ * `columnX` override doesn't, so that case instead scans each column's
205
+ * `[columnXFor(week), columnXFor(week + 1))` span — algebraically the same
206
+ * span the default formula's division derives — so hit-testing always
207
+ * agrees with wherever `columnXFor()` actually painted that column.
208
+ */
209
+ private weekAtX;
210
+ /**
211
+ * Locale-derived weekday-axis labels for the Sunday..Saturday rows drawn
212
+ * down the left of the calendar grid. Only indices 1 (Mon), 3 (Wed), and 5
213
+ * (Fri) are filled in — matching today's sparse every-other-day label
214
+ * density — the rest stay blank. `firstWeekStart` is always a UTC Sunday
215
+ * by construction (see `buildCalendarGrid()`), so weekday index 0..6 is a
216
+ * fixed Sun..Sat positional mapping; only the label *text* for indices
217
+ * 1/3/5 is derived here, via `Intl.DateTimeFormat` on a real UTC date that
218
+ * actually falls on that weekday, so it follows the runtime locale instead
219
+ * of a hardcoded English array (see `calendarCellText()`'s tooltip text
220
+ * for the same pattern).
221
+ */
222
+ private weekdayLabels;
151
223
  private drawCalendar;
152
224
  /**
153
225
  * Effective per-cell size in matrix mode — `fitToWidth` derives it from
@@ -185,16 +257,30 @@ export declare class LyraHeatmap extends LyraElement {
185
257
  /**
186
258
  * Human-readable "<label>: <value>" text for a cell — shared by the hover
187
259
  * tooltip and the keyboard-focus live-region announcement, so both always
188
- * describe a cell the same way.
260
+ * describe a cell the same way. This is the built-in English fallback used
261
+ * when `cellText` isn't set — see `resolveCellText()`.
189
262
  */
190
- private cellText;
263
+ private defaultCellText;
191
264
  private matrixCellText;
192
265
  private calendarCellText;
266
+ /** The raw numeric value at a cell position, in either mode — shared by `resolveCellText()`
267
+ * so a custom `cellText` formatter receives the same value the built-in template would use. */
268
+ private valueAt;
269
+ /** Dispatches to the host-provided `cellText` formatter when set, otherwise the built-in template. */
270
+ private resolveCellText;
193
271
  /** Refreshes the visually-hidden live-region text for a newly-focused cell. */
194
272
  private announce;
195
273
  private emitCellClick;
196
274
  /** Inline `style` for `[part="tooltip"]`, centered above the given cell. */
197
275
  private tooltipStyle;
276
+ /**
277
+ * Value-equality check for a `CellPos`, since `hoverCell` is assigned a
278
+ * fresh object literal on every `pointermove` — Lit's `@state()`
279
+ * change-detection only no-ops an identical *reference*, so without this,
280
+ * `{row,col} !== {row,col}` by reference triggers a full canvas clear +
281
+ * redraw on every pixel of pointer movement, even within the same cell.
282
+ */
283
+ private samePos;
198
284
  private onPointerMove;
199
285
  private onPointerLeave;
200
286
  private onCanvasClick;
@@ -167,10 +167,18 @@ export function mixColor(fromColor, toColor, t) {
167
167
  * specific cells (e.g. to call out an anomaly), each one optionally
168
168
  * surfaced in the legend too via `[part="legend-annotation"]`.
169
169
  *
170
+ * `columnX` (calendar mode only) overrides the x-origin computed for each
171
+ * week column — drawing, hit-testing, the focus ring, and month-label
172
+ * positioning all consult it consistently, so a consumer can pixel-align a
173
+ * calendar's week columns with a sibling chart's coordinate system. Unset
174
+ * (the default) keeps the original evenly-spaced formula.
175
+ *
170
176
  * @customElement lyra-heatmap
171
177
  * @event lyra-cell-click - Fired on click, or Enter/Space on the
172
178
  * focused/hovered cell. `detail: { row, col, value }` in matrix mode,
173
- * `detail: { date, value }` in calendar mode.
179
+ * `detail: { date, value }` in calendar mode. `cellText` overrides the
180
+ * built-in English "Row X, Col Y: value" / "Mon DD: value" template used for
181
+ * both the hover tooltip and the keyboard live-region announcement.
174
182
  * @csspart base, canvas, tooltip, live-region, legend, legend-lo, legend-hi, legend-annotation
175
183
  */
176
184
  export class LyraHeatmap extends LyraElement {
@@ -205,21 +213,37 @@ export class LyraHeatmap extends LyraElement {
205
213
  this.focusedCell = null;
206
214
  /** Text of the visually-hidden `aria-live="polite"` status announcement, refreshed on every focus move. */
207
215
  this.liveText = '';
216
+ /**
217
+ * Whether `role`/`aria-label` were present on the host *before* this
218
+ * element's own code ever wrote to them — snapshotted once, in the very
219
+ * first `willUpdate()` (guarded by `hasUpdated`, which Lit only flips to
220
+ * `true` after that first update commits). Re-checking `hasAttribute()` on
221
+ * every update instead would self-poison: once this element writes its own
222
+ * default `aria-label` on the first render, `hasAttribute('aria-label')`
223
+ * is permanently `true` afterwards, which would wrongly look like an
224
+ * author-supplied value and freeze the label instead of refreshing it as
225
+ * `values`/`days` change on later updates.
226
+ */
227
+ this.authorSuppliedRole = false;
228
+ this.authorSuppliedAriaLabel = false;
208
229
  this.onDprChange = () => {
209
230
  this.watchDpr();
210
231
  this.draw();
211
232
  };
212
233
  this.onPointerMove = (e) => {
213
- this.hoverCell = this.hitTest(e.offsetX, e.offsetY);
234
+ const next = this.hitTest(e.offsetX, e.offsetY);
235
+ if (!this.samePos(this.hoverCell, next))
236
+ this.hoverCell = next;
214
237
  };
215
238
  this.onPointerLeave = () => {
216
239
  this.hoverCell = null;
217
240
  };
218
241
  this.onCanvasClick = (e) => {
219
- // Prefer the click's own position; fall back to whatever's already
220
- // keyboard-focused (e.g. a synthetic click dispatched via
221
- // HTMLElement.click(), which carries no real coordinates).
222
- const pos = this.hitTest(e.offsetX, e.offsetY) ?? this.focusedCell;
242
+ // Only the click's own hit-tested position counts a click that lands
243
+ // outside the grid (e.g. on the canvas's own padding) is a genuine
244
+ // out-of-grid click and should do nothing, not fall back to whatever
245
+ // cell happens to still be keyboard-focused from an earlier interaction.
246
+ const pos = this.hitTest(e.offsetX, e.offsetY);
223
247
  if (!pos)
224
248
  return;
225
249
  this.focusedCell = pos;
@@ -267,17 +291,31 @@ export class LyraHeatmap extends LyraElement {
267
291
  this.hoverCell = null;
268
292
  this.liveText = '';
269
293
  }
294
+ if (!this.hasUpdated) {
295
+ this.authorSuppliedRole = this.hasAttribute('role');
296
+ this.authorSuppliedAriaLabel = this.hasAttribute('aria-label');
297
+ }
270
298
  this.cachedValueRange = this.computeValueRange();
271
299
  const bounds = this.cachedValueRange;
272
300
  const range = bounds ? `${bounds[0]}–${bounds[1]}` : 'no data';
273
- this.setAttribute('role', 'img');
274
- if (this.mode === 'calendar') {
275
- this.setAttribute('aria-label', `Calendar heatmap of ${this.days.length} days, ${this.valueLabel} range ${range}`);
276
- }
277
- else {
278
- const rows = this.rowLabels.length;
279
- const cols = this.colLabels.length;
280
- this.setAttribute('aria-label', `Heatmap of ${rows} × ${cols} cells, ${this.valueLabel} range ${range}`);
301
+ // Only default role/aria-label when the author hasn't already supplied
302
+ // one the canvas is a genuinely focusable/interactive control
303
+ // (tabindex="0", click/keydown handlers) plus a live role="status"
304
+ // region, so unconditionally forcing a role here would tell assistive
305
+ // tech to flatten the whole subtree, hiding both from the accessibility
306
+ // tree; it would also silently overwrite any author-supplied
307
+ // role/aria-label on every update.
308
+ if (!this.authorSuppliedRole)
309
+ this.setAttribute('role', 'group');
310
+ if (!this.authorSuppliedAriaLabel) {
311
+ if (this.mode === 'calendar') {
312
+ this.setAttribute('aria-label', `Calendar heatmap of ${this.days.length} days, ${this.valueLabel} range ${range}`);
313
+ }
314
+ else {
315
+ const rows = this.rowLabels.length;
316
+ const cols = this.colLabels.length;
317
+ this.setAttribute('aria-label', `Heatmap of ${rows} × ${cols} cells, ${this.valueLabel} range ${range}`);
318
+ }
281
319
  }
282
320
  }
283
321
  /**
@@ -329,11 +367,66 @@ export class LyraHeatmap extends LyraElement {
329
367
  else
330
368
  this.drawMatrix();
331
369
  }
370
+ /**
371
+ * 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()`,
374
+ * `hitTestCalendar()`/`weekAtX()`, `cellRect()`) so painted geometry and
375
+ * interactive hit-testing never disagree — mirrors the existing
376
+ * `matrixCellSize()` invariant for matrix mode's
377
+ * `drawMatrix()`/`hitTestMatrix()`/`cellRect()`.
378
+ */
379
+ columnXFor(week) {
380
+ return this.columnX ? this.columnX(week) : CAL_PAD_LEFT + week * (CAL_CELL + CAL_GAP);
381
+ }
382
+ /**
383
+ * Inverse of `columnXFor()`: resolves an x position (canvas-local CSS px)
384
+ * to the week column it falls in, or `null` if it's outside every column
385
+ * (`weekCount` is 0, or `x` doesn't land inside `[0, weekCount)`'s span).
386
+ * The default spacing has a closed-form inverse (division); an arbitrary
387
+ * `columnX` override doesn't, so that case instead scans each column's
388
+ * `[columnXFor(week), columnXFor(week + 1))` span — algebraically the same
389
+ * span the default formula's division derives — so hit-testing always
390
+ * agrees with wherever `columnXFor()` actually painted that column.
391
+ */
392
+ weekAtX(x, weekCount) {
393
+ if (!this.columnX) {
394
+ const week = Math.floor((x - CAL_PAD_LEFT) / (CAL_CELL + CAL_GAP));
395
+ return week >= 0 && week < weekCount ? week : null;
396
+ }
397
+ for (let week = 0; week < weekCount; week++) {
398
+ const start = this.columnXFor(week);
399
+ const end = week + 1 < weekCount ? this.columnXFor(week + 1) : start + (CAL_CELL + CAL_GAP);
400
+ if (x >= start && x < end)
401
+ return week;
402
+ }
403
+ return null;
404
+ }
405
+ /**
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).
416
+ */
417
+ weekdayLabels(firstWeekStart) {
418
+ const formatter = new Intl.DateTimeFormat(undefined, { weekday: 'short', timeZone: 'UTC' });
419
+ const labels = ['', '', '', '', '', '', ''];
420
+ for (const weekday of [1, 3, 5]) {
421
+ labels[weekday] = formatter.format(new Date(firstWeekStart.getTime() + weekday * MS_PER_DAY));
422
+ }
423
+ return labels;
424
+ }
332
425
  drawCalendar() {
333
426
  if (!this.canvas)
334
427
  return;
335
- const { cells, weekCount, monthLabels } = buildCalendarGrid(this.days);
336
- const w = CAL_PAD_LEFT + Math.max(1, weekCount) * (CAL_CELL + CAL_GAP);
428
+ const { cells, weekCount, firstWeekStart, monthLabels } = buildCalendarGrid(this.days);
429
+ const w = this.columnXFor(Math.max(1, weekCount));
337
430
  const h = CAL_LABEL_H + 7 * (CAL_CELL + CAL_GAP);
338
431
  const dpr = window.devicePixelRatio || 1;
339
432
  this.canvas.width = w * dpr;
@@ -356,7 +449,7 @@ export class LyraHeatmap extends LyraElement {
356
449
  const ramp = Array.from({ length: buckets }, (_, i) => mixRgb(loRgb, hiRgb, i / (buckets - 1)));
357
450
  const noDataFill = this.noDataFill();
358
451
  for (const cell of cells) {
359
- const x = CAL_PAD_LEFT + cell.week * (CAL_CELL + CAL_GAP);
452
+ const x = this.columnXFor(cell.week);
360
453
  const y = CAL_LABEL_H + cell.weekday * (CAL_CELL + CAL_GAP);
361
454
  ctx.fillStyle =
362
455
  cell.value < 0 || !Number.isFinite(cell.value)
@@ -376,7 +469,7 @@ export class LyraHeatmap extends LyraElement {
376
469
  const match = cells.find((c) => c.date === ann.date);
377
470
  if (!match)
378
471
  continue;
379
- const x = CAL_PAD_LEFT + match.week * (CAL_CELL + CAL_GAP);
472
+ const x = this.columnXFor(match.week);
380
473
  const y = CAL_LABEL_H + match.weekday * (CAL_CELL + CAL_GAP);
381
474
  ctx.strokeRect(x + 0.5, y + 0.5, CAL_CELL - 1, CAL_CELL - 1);
382
475
  }
@@ -388,7 +481,7 @@ export class LyraHeatmap extends LyraElement {
388
481
  if (week < weekCount && weekday < 7) {
389
482
  ctx.lineWidth = RING_LINE_WIDTH;
390
483
  ctx.strokeStyle = this.focusRingColor();
391
- const x = CAL_PAD_LEFT + week * (CAL_CELL + CAL_GAP);
484
+ const x = this.columnXFor(week);
392
485
  const y = CAL_LABEL_H + weekday * (CAL_CELL + CAL_GAP);
393
486
  ctx.strokeRect(x + 0.5, y + 0.5, CAL_CELL - 1, CAL_CELL - 1);
394
487
  }
@@ -396,9 +489,9 @@ export class LyraHeatmap extends LyraElement {
396
489
  ctx.fillStyle = this.labelColor();
397
490
  ctx.font = this.labelFont();
398
491
  for (const m of monthLabels) {
399
- ctx.fillText(m.label, CAL_PAD_LEFT + m.week * (CAL_CELL + CAL_GAP), CAL_LABEL_H - 4);
492
+ ctx.fillText(m.label, this.columnXFor(m.week), CAL_LABEL_H - 4);
400
493
  }
401
- const WEEKDAY_LABELS = ['', 'Mon', '', 'Wed', '', 'Fri', ''];
494
+ const WEEKDAY_LABELS = this.weekdayLabels(firstWeekStart);
402
495
  WEEKDAY_LABELS.forEach((label, weekday) => {
403
496
  if (label)
404
497
  ctx.fillText(label, 2, CAL_LABEL_H + weekday * (CAL_CELL + CAL_GAP) + CAL_CELL - 1);
@@ -537,9 +630,9 @@ export class LyraHeatmap extends LyraElement {
537
630
  const { weekCount } = buildCalendarGrid(this.days);
538
631
  if (weekCount === 0)
539
632
  return null;
540
- const week = Math.floor((x - CAL_PAD_LEFT) / (CAL_CELL + CAL_GAP));
633
+ const week = this.weekAtX(x, weekCount);
541
634
  const weekday = Math.floor((y - CAL_LABEL_H) / (CAL_CELL + CAL_GAP));
542
- if (week < 0 || week >= weekCount || weekday < 0 || weekday > 6)
635
+ if (week === null || weekday < 0 || weekday > 6)
543
636
  return null;
544
637
  return { week, weekday };
545
638
  }
@@ -567,7 +660,7 @@ export class LyraHeatmap extends LyraElement {
567
660
  cellRect(pos) {
568
661
  if ('week' in pos) {
569
662
  return {
570
- x: CAL_PAD_LEFT + pos.week * (CAL_CELL + CAL_GAP),
663
+ x: this.columnXFor(pos.week),
571
664
  y: CAL_LABEL_H + pos.weekday * (CAL_CELL + CAL_GAP),
572
665
  w: CAL_CELL,
573
666
  h: CAL_CELL,
@@ -579,9 +672,10 @@ export class LyraHeatmap extends LyraElement {
579
672
  /**
580
673
  * Human-readable "<label>: <value>" text for a cell — shared by the hover
581
674
  * tooltip and the keyboard-focus live-region announcement, so both always
582
- * describe a cell the same way.
675
+ * describe a cell the same way. This is the built-in English fallback used
676
+ * when `cellText` isn't set — see `resolveCellText()`.
583
677
  */
584
- cellText(pos) {
678
+ defaultCellText(pos) {
585
679
  return 'week' in pos ? this.calendarCellText(pos) : this.matrixCellText(pos);
586
680
  }
587
681
  matrixCellText(pos) {
@@ -593,13 +687,24 @@ export class LyraHeatmap extends LyraElement {
593
687
  }
594
688
  calendarCellText(pos) {
595
689
  const { date, value } = this.calendarCellAt(pos);
596
- const label = parseIsoDate(date).toLocaleString('en', { month: 'short', day: 'numeric', timeZone: 'UTC' });
690
+ const label = parseIsoDate(date).toLocaleString(undefined, { month: 'short', day: 'numeric', timeZone: 'UTC' });
597
691
  const valueText = value < 0 || !Number.isFinite(value) ? 'no data' : String(value);
598
692
  return `${label}: ${valueText}`;
599
693
  }
694
+ /** The raw numeric value at a cell position, in either mode — shared by `resolveCellText()`
695
+ * so a custom `cellText` formatter receives the same value the built-in template would use. */
696
+ valueAt(pos) {
697
+ if ('week' in pos)
698
+ return this.calendarCellAt(pos).value;
699
+ return this.values[pos.row]?.[pos.col] ?? -1;
700
+ }
701
+ /** Dispatches to the host-provided `cellText` formatter when set, otherwise the built-in template. */
702
+ resolveCellText(pos) {
703
+ return this.cellText ? this.cellText(pos, this.valueAt(pos)) : this.defaultCellText(pos);
704
+ }
600
705
  /** Refreshes the visually-hidden live-region text for a newly-focused cell. */
601
706
  announce(pos) {
602
- this.liveText = this.cellText(pos);
707
+ this.liveText = this.resolveCellText(pos);
603
708
  }
604
709
  emitCellClick(pos) {
605
710
  if ('week' in pos) {
@@ -616,6 +721,24 @@ export class LyraHeatmap extends LyraElement {
616
721
  const rect = this.cellRect(pos);
617
722
  return { left: `${rect.x + rect.w / 2}px`, top: `${rect.y}px` };
618
723
  }
724
+ /**
725
+ * Value-equality check for a `CellPos`, since `hoverCell` is assigned a
726
+ * fresh object literal on every `pointermove` — Lit's `@state()`
727
+ * change-detection only no-ops an identical *reference*, so without this,
728
+ * `{row,col} !== {row,col}` by reference triggers a full canvas clear +
729
+ * redraw on every pixel of pointer movement, even within the same cell.
730
+ */
731
+ samePos(a, b) {
732
+ if (a === b)
733
+ return true;
734
+ if (!a || !b)
735
+ return false;
736
+ if ('week' in a && 'week' in b)
737
+ return a.week === b.week && a.weekday === b.weekday;
738
+ if ('row' in a && 'row' in b)
739
+ return a.row === b.row && a.col === b.col;
740
+ return false;
741
+ }
619
742
  onMatrixKeyDown(e) {
620
743
  const rows = this.rowLabels.length;
621
744
  const cols = this.colLabels.length;
@@ -703,7 +826,7 @@ export class LyraHeatmap extends LyraElement {
703
826
  ?hidden=${!this.hoverCell}
704
827
  style=${styleMap(this.hoverCell ? this.tooltipStyle(this.hoverCell) : {})}
705
828
  >
706
- ${this.hoverCell ? this.cellText(this.hoverCell) : ''}
829
+ ${this.hoverCell ? this.resolveCellText(this.hoverCell) : ''}
707
830
  </div>
708
831
  <div part="live-region" class="sr-only" role="status" aria-live="polite">${this.liveText}</div>
709
832
  <div part="legend">
@@ -750,6 +873,12 @@ __decorate([
750
873
  __decorate([
751
874
  property({ attribute: false })
752
875
  ], LyraHeatmap.prototype, "annotations", void 0);
876
+ __decorate([
877
+ property({ attribute: false })
878
+ ], LyraHeatmap.prototype, "cellText", void 0);
879
+ __decorate([
880
+ property({ attribute: false })
881
+ ], LyraHeatmap.prototype, "columnX", void 0);
753
882
  __decorate([
754
883
  query('canvas')
755
884
  ], LyraHeatmap.prototype, "canvas", void 0);
@@ -0,0 +1,53 @@
1
+ import { type TemplateResult } from 'lit';
2
+ import { LyraElement } from '../../internal/lyra-element.js';
3
+ /**
4
+ * `<lyra-json-viewer>` — a collapsible, copyable tree view for an arbitrary
5
+ * JSON-serializable value (object/array/string/number/boolean/null/
6
+ * undefined). Serves as the fallback renderer wherever a raw payload (tool
7
+ * call arguments, a tool result, an API response) needs inspecting without a
8
+ * bespoke view.
9
+ *
10
+ * Expand/collapse state is keyed by structural path (not by object identity),
11
+ * so it survives a `data` reassignment that keeps the same shape -- e.g. a
12
+ * streaming tool result being patched in place.
13
+ *
14
+ * @customElement lyra-json-viewer
15
+ * @event lyra-copy - `detail: { text }` -- fired by the top-level copy button or a per-node one. Fires even when `navigator.clipboard` silently failed or is unavailable (insecure context, older browser, stubbed-out test environment), so a consumer can still observe copy *intent*.
16
+ * @csspart base - The root scroll container; respects `max-height`.
17
+ * @csspart toolbar - The wrapper around the top-level copy button (only rendered when `copyable`).
18
+ * @csspart tree - The wrapper around the rendered node tree.
19
+ * @csspart key - An object property key or array index label.
20
+ * @csspart value - A primitive value's text -- carries `data-type` (`string`/`number`/`boolean`/`null`/`undefined`) for per-type coloring, and `data-match` while it matches `search`.
21
+ * @csspart bracket - A `{`, `}`, `[`, or `]` delimiter.
22
+ * @csspart toggle - A container node's expand/collapse button (hidden, but present for row alignment, on leaf/empty nodes).
23
+ * @csspart copy-button - A copy-to-clipboard button -- the top-level one (in `toolbar`) or a per-node one (only rendered when `copyable`).
24
+ */
25
+ export declare class LyraJsonViewer extends LyraElement {
26
+ static styles: import("lit").CSSResultGroup[];
27
+ /** The value to render. Any JSON-serializable value, plus `undefined`. */
28
+ data: unknown;
29
+ /** Nodes at or beyond this nesting depth (root = 0) start collapsed. Omit/undefined: nothing auto-collapses. */
30
+ collapsedDepth?: number;
31
+ /** A CSS length (e.g. `"20rem"`); once set, the viewer scrolls internally past this height instead of growing the page. */
32
+ maxHeight: string;
33
+ /** Shows copy-to-clipboard affordances: one for the whole value, plus one per node. */
34
+ copyable: boolean;
35
+ /** Case-insensitive substring match against keys/values; matches are highlighted and their ancestors auto-expanded. */
36
+ search: string;
37
+ /** Per-path (`JSON.stringify(path)`) explicit expand/collapse, overriding the `collapsedDepth`/search defaults once a node's toggle has been used. */
38
+ private expandedOverrides;
39
+ private isExpanded;
40
+ private toggleNode;
41
+ private writeClipboard;
42
+ private copy;
43
+ /** Builds the key/value-match sets and the ancestor-paths-of-a-match set that `search` drives. */
44
+ private computeSearch;
45
+ private renderCopyButton;
46
+ private renderNode;
47
+ render(): TemplateResult;
48
+ }
49
+ declare global {
50
+ interface HTMLElementTagNameMap {
51
+ 'lyra-json-viewer': LyraJsonViewer;
52
+ }
53
+ }