@expressivecss/expressive 0.12.0 → 0.14.1

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 (258) hide show
  1. package/CHANGELOG.md +339 -1
  2. package/README.md +38 -1
  3. package/dist/css/expressive.css +5342 -105
  4. package/dist/css/expressive.css.map +1 -1
  5. package/dist/css/expressive.min.css +1 -1
  6. package/dist/css/expressive.min.css.map +1 -1
  7. package/dist/fonts/material-symbols-outlined.woff2 +0 -0
  8. package/dist/fonts/material-symbols-rounded.woff2 +0 -0
  9. package/dist/fonts/material-symbols-sharp.woff2 +0 -0
  10. package/dist/js/chunks/chunk-3DHK5CZU.mjs +105 -0
  11. package/dist/js/chunks/chunk-3DHK5CZU.mjs.map +7 -0
  12. package/dist/js/chunks/{chunk-HB6IW7QQ.mjs → chunk-3GVFZ4XO.mjs} +3 -3
  13. package/dist/js/chunks/{chunk-HLDXZ7XB.mjs → chunk-4HZMB7LI.mjs} +22 -8
  14. package/dist/js/chunks/chunk-4HZMB7LI.mjs.map +7 -0
  15. package/dist/js/chunks/chunk-4KMNFIFZ.mjs +270 -0
  16. package/dist/js/chunks/chunk-4KMNFIFZ.mjs.map +7 -0
  17. package/dist/js/chunks/chunk-4TKTLGEM.mjs +262 -0
  18. package/dist/js/chunks/chunk-4TKTLGEM.mjs.map +7 -0
  19. package/dist/js/chunks/{chunk-3VOK6J7U.mjs → chunk-5BKHWHYY.mjs} +13 -6
  20. package/dist/js/chunks/chunk-5BKHWHYY.mjs.map +7 -0
  21. package/dist/js/chunks/{chunk-H2WA4XHH.mjs → chunk-5HQSGDZ5.mjs} +16 -7
  22. package/dist/js/chunks/chunk-5HQSGDZ5.mjs.map +7 -0
  23. package/dist/js/chunks/{chunk-LWBQIX5J.mjs → chunk-A7ALMYUR.mjs} +41 -44
  24. package/dist/js/chunks/chunk-A7ALMYUR.mjs.map +7 -0
  25. package/dist/js/chunks/chunk-BG6ZRB5H.mjs +154 -0
  26. package/dist/js/chunks/chunk-BG6ZRB5H.mjs.map +7 -0
  27. package/dist/js/chunks/{chunk-DLWYKFEI.mjs → chunk-CUQPB25U.mjs} +3 -3
  28. package/dist/js/chunks/{chunk-6WLSDZ4O.mjs → chunk-DAIU5SLG.mjs} +2 -2
  29. package/dist/js/chunks/chunk-FS7XY3HT.mjs +124 -0
  30. package/dist/js/chunks/chunk-FS7XY3HT.mjs.map +7 -0
  31. package/dist/js/chunks/{chunk-WJXDBPXQ.mjs → chunk-GIW4K4SE.mjs} +8 -12
  32. package/dist/js/chunks/chunk-GIW4K4SE.mjs.map +7 -0
  33. package/dist/js/chunks/chunk-GUB3TNO2.mjs +164 -0
  34. package/dist/js/chunks/chunk-GUB3TNO2.mjs.map +7 -0
  35. package/dist/js/chunks/chunk-GVGT6MIU.mjs +47 -0
  36. package/dist/js/chunks/chunk-GVGT6MIU.mjs.map +7 -0
  37. package/dist/js/chunks/{chunk-HNFSMIBY.mjs → chunk-GZKEFWPM.mjs} +83 -22
  38. package/dist/js/chunks/chunk-GZKEFWPM.mjs.map +7 -0
  39. package/dist/js/chunks/chunk-HKT2S7VM.mjs +48 -0
  40. package/dist/js/chunks/chunk-HKT2S7VM.mjs.map +7 -0
  41. package/dist/js/chunks/chunk-ILP3PDTL.mjs +187 -0
  42. package/dist/js/chunks/chunk-ILP3PDTL.mjs.map +7 -0
  43. package/dist/js/chunks/{chunk-YSBCHYXP.mjs → chunk-JIDMQGK6.mjs} +118 -13
  44. package/dist/js/chunks/chunk-JIDMQGK6.mjs.map +7 -0
  45. package/dist/js/chunks/chunk-KBNN3QDH.mjs +7 -0
  46. package/dist/js/chunks/chunk-KBNN3QDH.mjs.map +7 -0
  47. package/dist/js/chunks/{chunk-NNYP33NB.mjs → chunk-KHV6DZXX.mjs} +5 -4
  48. package/dist/js/chunks/chunk-KHV6DZXX.mjs.map +7 -0
  49. package/dist/js/chunks/{chunk-RFZZCYVU.mjs → chunk-KY6U46SM.mjs} +3 -3
  50. package/dist/js/chunks/{chunk-CKQP6P76.mjs → chunk-LRZMJZ3T.mjs} +4 -3
  51. package/dist/js/chunks/chunk-LRZMJZ3T.mjs.map +7 -0
  52. package/dist/js/chunks/{chunk-6OMRGML6.mjs → chunk-MNPROCZF.mjs} +4 -4
  53. package/dist/js/chunks/{chunk-6OMRGML6.mjs.map → chunk-MNPROCZF.mjs.map} +2 -2
  54. package/dist/js/chunks/{chunk-QV2JAWIZ.mjs → chunk-MZ6Z24BX.mjs} +3 -2
  55. package/dist/js/chunks/{chunk-QV2JAWIZ.mjs.map → chunk-MZ6Z24BX.mjs.map} +2 -2
  56. package/dist/js/chunks/chunk-NS2UMNZV.mjs +292 -0
  57. package/dist/js/chunks/chunk-NS2UMNZV.mjs.map +7 -0
  58. package/dist/js/chunks/chunk-O5XPWXIQ.mjs +406 -0
  59. package/dist/js/chunks/chunk-O5XPWXIQ.mjs.map +7 -0
  60. package/dist/js/chunks/{chunk-UBLRRJ7N.mjs → chunk-PM655FXL.mjs} +2 -2
  61. package/dist/js/chunks/{chunk-IFXVDDDH.mjs → chunk-QERLECVU.mjs} +3 -3
  62. package/dist/js/chunks/{chunk-GZWPXXY6.mjs → chunk-QJABDLSD.mjs} +5 -2
  63. package/dist/js/chunks/chunk-QJABDLSD.mjs.map +7 -0
  64. package/dist/js/chunks/chunk-RHRQKTTX.mjs +139 -0
  65. package/dist/js/chunks/chunk-RHRQKTTX.mjs.map +7 -0
  66. package/dist/js/chunks/{chunk-UNEMYWEW.mjs → chunk-S3XDFGSZ.mjs} +2 -2
  67. package/dist/js/chunks/chunk-SPETGK25.mjs +59 -0
  68. package/dist/js/chunks/chunk-SPETGK25.mjs.map +7 -0
  69. package/dist/js/chunks/chunk-TBM3FT7G.mjs +398 -0
  70. package/dist/js/chunks/chunk-TBM3FT7G.mjs.map +7 -0
  71. package/dist/js/chunks/chunk-UEXCIMJZ.mjs +214 -0
  72. package/dist/js/chunks/chunk-UEXCIMJZ.mjs.map +7 -0
  73. package/dist/js/chunks/chunk-VGZSAFTX.mjs +173 -0
  74. package/dist/js/chunks/chunk-VGZSAFTX.mjs.map +7 -0
  75. package/dist/js/chunks/{chunk-UAEAQBRC.mjs → chunk-X7OCHZVF.mjs} +2 -2
  76. package/dist/js/chunks/{chunk-EC63CHXU.mjs → chunk-YMSXJON6.mjs} +7 -6
  77. package/dist/js/chunks/chunk-YMSXJON6.mjs.map +7 -0
  78. package/dist/js/chunks/{chunk-QVVEWBOF.mjs → chunk-Z2NRKI7R.mjs} +2 -2
  79. package/dist/js/chunks/{chunk-PIJ2TAR3.mjs → chunk-ZAS25YFD.mjs} +2 -2
  80. package/dist/js/chunks/chunk-ZKZKNSLC.mjs +1 -0
  81. package/dist/js/expressive.cjs +3113 -148
  82. package/dist/js/expressive.cjs.map +4 -4
  83. package/dist/js/expressive.js +3113 -148
  84. package/dist/js/expressive.js.map +4 -4
  85. package/dist/js/expressive.min.js +11 -11
  86. package/dist/js/expressive.min.js.map +4 -4
  87. package/dist/js/expressive.mjs +95 -35
  88. package/dist/js/expressive.mjs.map +1 -1
  89. package/dist/js/internal/behaviors/bottomSheets.mjs +3 -3
  90. package/dist/js/internal/behaviors/dialogs.mjs +2 -2
  91. package/dist/js/internal/behaviors/forms.mjs +2 -2
  92. package/dist/js/internal/behaviors/sheetDrag.mjs +2 -2
  93. package/dist/js/internal/behaviors/sideSheets.mjs +3 -3
  94. package/dist/js/internal/components/autocomplete.mjs +3 -3
  95. package/dist/js/internal/components/barChart.mjs +10 -0
  96. package/dist/js/internal/components/barChart.mjs.map +7 -0
  97. package/dist/js/internal/components/buttons.mjs +2 -2
  98. package/dist/js/internal/components/cards.mjs +2 -2
  99. package/dist/js/internal/components/carousel.mjs +2 -2
  100. package/dist/js/internal/components/chips.mjs +4 -4
  101. package/dist/js/internal/components/columnChart.mjs +9 -0
  102. package/dist/js/internal/components/columnChart.mjs.map +7 -0
  103. package/dist/js/internal/components/commandPalette.mjs +9 -0
  104. package/dist/js/internal/components/commandPalette.mjs.map +7 -0
  105. package/dist/js/internal/components/confetti.mjs +8 -0
  106. package/dist/js/internal/components/confetti.mjs.map +7 -0
  107. package/dist/js/internal/components/datepicker.mjs +5 -5
  108. package/dist/js/internal/components/expandingCard.mjs +2 -2
  109. package/dist/js/internal/components/heatmapChart.mjs +9 -0
  110. package/dist/js/internal/components/heatmapChart.mjs.map +7 -0
  111. package/dist/js/internal/components/index.mjs +88 -28
  112. package/dist/js/internal/components/lightbox.mjs +2 -2
  113. package/dist/js/internal/components/lineChart.mjs +12 -0
  114. package/dist/js/internal/components/lineChart.mjs.map +7 -0
  115. package/dist/js/internal/components/mapView.mjs +9 -0
  116. package/dist/js/internal/components/mapView.mjs.map +7 -0
  117. package/dist/js/internal/components/menu.mjs +2 -2
  118. package/dist/js/internal/components/messageScroller.mjs +9 -0
  119. package/dist/js/internal/components/messageScroller.mjs.map +7 -0
  120. package/dist/js/internal/components/mixedChart.mjs +10 -0
  121. package/dist/js/internal/components/mixedChart.mjs.map +7 -0
  122. package/dist/js/internal/components/particles.mjs +8 -0
  123. package/dist/js/internal/components/particles.mjs.map +7 -0
  124. package/dist/js/internal/components/pieChart.mjs +9 -0
  125. package/dist/js/internal/components/pieChart.mjs.map +7 -0
  126. package/dist/js/internal/components/questionnaire.mjs +9 -0
  127. package/dist/js/internal/components/questionnaire.mjs.map +7 -0
  128. package/dist/js/internal/components/radarChart.mjs +9 -0
  129. package/dist/js/internal/components/radarChart.mjs.map +7 -0
  130. package/dist/js/internal/components/radialChart.mjs +9 -0
  131. package/dist/js/internal/components/radialChart.mjs.map +7 -0
  132. package/dist/js/internal/components/registry.mjs +35 -20
  133. package/dist/js/internal/components/richTextEditor.mjs +9 -0
  134. package/dist/js/internal/components/richTextEditor.mjs.map +7 -0
  135. package/dist/js/internal/components/scrollspy.mjs +1 -1
  136. package/dist/js/internal/components/select.mjs +3 -3
  137. package/dist/js/internal/components/slider.mjs +2 -2
  138. package/dist/js/internal/components/snackbar.mjs +2 -2
  139. package/dist/js/internal/components/tabs.mjs +3 -3
  140. package/dist/js/internal/components/timepicker.mjs +3 -3
  141. package/dist/js/internal/components/tooltip.mjs +2 -2
  142. package/dist/js/internal/core/utils.mjs +1 -1
  143. package/dist/js/internal/plugins/dockedDisplayPlugin.mjs +2 -2
  144. package/dist/js/modular.mjs +95 -35
  145. package/dist/types/behaviors/forms.d.ts +27 -0
  146. package/dist/types/components/barChart.d.ts +24 -0
  147. package/dist/types/components/carousel.d.ts +2 -0
  148. package/dist/types/components/columnChart.d.ts +21 -0
  149. package/dist/types/components/commandPalette.d.ts +96 -0
  150. package/dist/types/components/confetti.d.ts +103 -0
  151. package/dist/types/components/datepicker.d.ts +2 -0
  152. package/dist/types/components/heatmapChart.d.ts +49 -0
  153. package/dist/types/components/index.d.ts +15 -0
  154. package/dist/types/components/lineChart.d.ts +134 -0
  155. package/dist/types/components/mapView.d.ts +151 -0
  156. package/dist/types/components/menu.d.ts +31 -0
  157. package/dist/types/components/messageScroller.d.ts +111 -0
  158. package/dist/types/components/mixedChart.d.ts +25 -0
  159. package/dist/types/components/particles.d.ts +72 -0
  160. package/dist/types/components/pieChart.d.ts +43 -0
  161. package/dist/types/components/questionnaire.d.ts +96 -0
  162. package/dist/types/components/radarChart.d.ts +39 -0
  163. package/dist/types/components/radialChart.d.ts +45 -0
  164. package/dist/types/components/registry.d.ts +52 -0
  165. package/dist/types/components/richTextEditor.d.ts +71 -0
  166. package/dist/types/components/slider.d.ts +1 -1
  167. package/dist/types/components/snackbar.d.ts +2 -0
  168. package/dist/types/components/tabs.d.ts +1 -1
  169. package/dist/types/modular.d.ts +10 -1
  170. package/dist/types/plugins/dockedDisplayPlugin.d.ts +3 -3
  171. package/package.json +5 -1
  172. package/src/sass/README.md +3 -6
  173. package/src/sass/abstracts/_index.scss +1 -1
  174. package/src/sass/abstracts/_mixins.scss +226 -0
  175. package/src/sass/abstracts/_variables.scss +14 -0
  176. package/src/sass/base/_typography.scss +45 -0
  177. package/src/sass/components/_accordion.scss +161 -0
  178. package/src/sass/components/_aura.scss +119 -0
  179. package/src/sass/components/_avatar.scss +86 -0
  180. package/src/sass/components/_bar-chart.scss +285 -0
  181. package/src/sass/components/_bottom-sheet.scss +1 -0
  182. package/src/sass/components/_button-groups.scss +13 -3
  183. package/src/sass/components/_buttons.scss +1 -1
  184. package/src/sass/components/_cards.scss +16 -13
  185. package/src/sass/components/_column-chart.scss +134 -0
  186. package/src/sass/components/_command-palette.scss +140 -0
  187. package/src/sass/components/_confetti.scss +14 -0
  188. package/src/sass/components/_countdown.scss +89 -0
  189. package/src/sass/components/_data-table.scss +188 -0
  190. package/src/sass/components/_dialog.scss +1 -0
  191. package/src/sass/components/_empty-state.scss +78 -0
  192. package/src/sass/components/_expanding-card.scss +3 -0
  193. package/src/sass/components/_heatmap-chart.scss +156 -0
  194. package/src/sass/components/_index.scss +30 -0
  195. package/src/sass/components/_kpi.scss +162 -0
  196. package/src/sass/components/_line-chart.scss +102 -0
  197. package/src/sass/components/_map.scss +188 -0
  198. package/src/sass/components/_marquee.scss +111 -0
  199. package/src/sass/components/_menu.scss +10 -0
  200. package/src/sass/components/_message-scroller.scss +66 -0
  201. package/src/sass/components/_message.scss +202 -0
  202. package/src/sass/components/_mixed-chart.scss +157 -0
  203. package/src/sass/components/_navigation-bar.scss +1 -0
  204. package/src/sass/components/_navigation-rail.scss +9 -0
  205. package/src/sass/components/_particles.scss +14 -0
  206. package/src/sass/components/_pie-chart.scss +203 -0
  207. package/src/sass/components/_popover.scss +112 -0
  208. package/src/sass/components/_questionnaire.scss +144 -0
  209. package/src/sass/components/_radar-chart.scss +205 -0
  210. package/src/sass/components/_radial-chart.scss +174 -0
  211. package/src/sass/components/_rich-text-editor.scss +99 -0
  212. package/src/sass/components/_search.scss +1 -0
  213. package/src/sass/components/_side-sheet.scss +1 -0
  214. package/src/sass/components/_skeleton.scss +81 -0
  215. package/src/sass/components/_snackbar.scss +1 -0
  216. package/src/sass/components/_stat.scss +86 -0
  217. package/src/sass/components/_stepper.scss +190 -0
  218. package/src/sass/components/_table_of_contents.scss +3 -1
  219. package/src/sass/components/_timeline.scss +114 -0
  220. package/src/sass/components/_toolbar.scss +1 -0
  221. package/src/sass/components/_tooltip.scss +2 -0
  222. package/src/sass/components/_tree.scss +139 -0
  223. package/src/sass/components/forms/_checkboxes.scss +7 -2
  224. package/src/sass/components/forms/_drop-zone.scss +128 -0
  225. package/src/sass/components/forms/_index.scss +2 -0
  226. package/src/sass/components/forms/_input-fields.scss +133 -2
  227. package/src/sass/components/forms/_rating.scss +165 -0
  228. package/src/sass/tokens/_reference.scss +3 -15
  229. package/src/sass/utilities/_index.scss +2 -0
  230. package/src/sass/utilities/_scroll-area.scss +35 -0
  231. package/src/sass/utilities/_shapes.scss +164 -0
  232. package/dist/js/chunks/chunk-3VOK6J7U.mjs.map +0 -7
  233. package/dist/js/chunks/chunk-CKQP6P76.mjs.map +0 -7
  234. package/dist/js/chunks/chunk-EC63CHXU.mjs.map +0 -7
  235. package/dist/js/chunks/chunk-G3I5H2U3.mjs +0 -7
  236. package/dist/js/chunks/chunk-G3I5H2U3.mjs.map +0 -7
  237. package/dist/js/chunks/chunk-GZWPXXY6.mjs.map +0 -7
  238. package/dist/js/chunks/chunk-H2WA4XHH.mjs.map +0 -7
  239. package/dist/js/chunks/chunk-HLDXZ7XB.mjs.map +0 -7
  240. package/dist/js/chunks/chunk-HNFSMIBY.mjs.map +0 -7
  241. package/dist/js/chunks/chunk-LWBQIX5J.mjs.map +0 -7
  242. package/dist/js/chunks/chunk-NNYP33NB.mjs.map +0 -7
  243. package/dist/js/chunks/chunk-PSSHQWCX.mjs +0 -100
  244. package/dist/js/chunks/chunk-PSSHQWCX.mjs.map +0 -7
  245. package/dist/js/chunks/chunk-WJXDBPXQ.mjs.map +0 -7
  246. package/dist/js/chunks/chunk-WWPAPHMA.mjs +0 -1
  247. package/dist/js/chunks/chunk-YSBCHYXP.mjs.map +0 -7
  248. /package/dist/js/chunks/{chunk-HB6IW7QQ.mjs.map → chunk-3GVFZ4XO.mjs.map} +0 -0
  249. /package/dist/js/chunks/{chunk-DLWYKFEI.mjs.map → chunk-CUQPB25U.mjs.map} +0 -0
  250. /package/dist/js/chunks/{chunk-6WLSDZ4O.mjs.map → chunk-DAIU5SLG.mjs.map} +0 -0
  251. /package/dist/js/chunks/{chunk-RFZZCYVU.mjs.map → chunk-KY6U46SM.mjs.map} +0 -0
  252. /package/dist/js/chunks/{chunk-UBLRRJ7N.mjs.map → chunk-PM655FXL.mjs.map} +0 -0
  253. /package/dist/js/chunks/{chunk-IFXVDDDH.mjs.map → chunk-QERLECVU.mjs.map} +0 -0
  254. /package/dist/js/chunks/{chunk-UNEMYWEW.mjs.map → chunk-S3XDFGSZ.mjs.map} +0 -0
  255. /package/dist/js/chunks/{chunk-UAEAQBRC.mjs.map → chunk-X7OCHZVF.mjs.map} +0 -0
  256. /package/dist/js/chunks/{chunk-QVVEWBOF.mjs.map → chunk-Z2NRKI7R.mjs.map} +0 -0
  257. /package/dist/js/chunks/{chunk-PIJ2TAR3.mjs.map → chunk-ZAS25YFD.mjs.map} +0 -0
  258. /package/dist/js/chunks/{chunk-WWPAPHMA.mjs.map → chunk-ZKZKNSLC.mjs.map} +0 -0
@@ -23,22 +23,37 @@ var Expressive = (() => {
23
23
  AppBar: () => AppBar,
24
24
  AutoInit: () => AutoInit,
25
25
  Autocomplete: () => Autocomplete,
26
+ BarChart: () => BarChart,
26
27
  BottomSheets: () => BottomSheets,
27
28
  ButtonGroup: () => ButtonGroup,
28
29
  Cards: () => Cards,
29
30
  Carousel: () => Carousel,
30
31
  CharacterCounter: () => CharacterCounter,
31
32
  Chips: () => Chips,
33
+ ColumnChart: () => ColumnChart,
34
+ CommandPalette: () => CommandPalette,
35
+ Confetti: () => Confetti,
32
36
  Datepicker: () => Datepicker,
33
37
  Dialogs: () => Dialogs,
34
38
  ExpandingCard: () => ExpandingCard,
35
39
  FloatingActionButton: () => FloatingActionButton,
36
40
  FormSelect: () => FormSelect,
37
41
  Forms: () => Forms,
42
+ HeatmapChart: () => HeatmapChart,
38
43
  Lightbox: () => Lightbox,
44
+ LineChart: () => LineChart,
45
+ MapView: () => MapView,
39
46
  Menu: () => Menu,
47
+ MessageScroller: () => MessageScroller,
48
+ MixedChart: () => MixedChart,
40
49
  NavigationRail: () => NavigationRail,
50
+ Particles: () => Particles,
51
+ PieChart: () => PieChart,
52
+ Questionnaire: () => Questionnaire,
53
+ RadarChart: () => RadarChart,
54
+ RadialChart: () => RadialChart,
41
55
  Range: () => Slider,
56
+ RichTextEditor: () => RichTextEditor,
42
57
  ScrollSpy: () => ScrollSpy,
43
58
  SideSheets: () => SideSheets,
44
59
  Slider: () => Slider,
@@ -227,9 +242,9 @@ var Expressive = (() => {
227
242
  static throttle(func, wait) {
228
243
  let timeout = null;
229
244
  let lastArgs = null;
230
- let previous = 0;
245
+ let previous = -Infinity;
231
246
  return (...args) => {
232
- const now = Date.now();
247
+ const now = performance.now();
233
248
  const remaining = wait - (now - previous);
234
249
  if (remaining <= 0) {
235
250
  clearTimeout(timeout);
@@ -240,7 +255,7 @@ var Expressive = (() => {
240
255
  } else if (!timeout) {
241
256
  lastArgs = args;
242
257
  timeout = setTimeout(() => {
243
- previous = Date.now();
258
+ previous = performance.now();
244
259
  timeout = null;
245
260
  func(...lastArgs ?? []);
246
261
  lastArgs = null;
@@ -772,6 +787,7 @@ var Expressive = (() => {
772
787
  coverTrigger: false,
773
788
  closeOnClick: true,
774
789
  hover: false,
790
+ contextMenu: false,
775
791
  inDuration: 150,
776
792
  outDuration: 250,
777
793
  onOpenStart: null,
@@ -787,6 +803,40 @@ var Expressive = (() => {
787
803
  this._pendingTimers = /* @__PURE__ */ new Set();
788
804
  this._transition = 0;
789
805
  this._menuClickTargets = /* @__PURE__ */ new WeakMap();
806
+ /**
807
+ * Viewport point the open context menu belongs to. Null outside a
808
+ * right-click, so a keyboard or programmatic open uses the region's corner
809
+ * rather than an old pointer position.
810
+ */
811
+ this._contextPoint = null;
812
+ /** What had focus when a context menu opened; focus goes back there. */
813
+ this._contextReturn = null;
814
+ this._handleContextMenu = (e) => {
815
+ if (e.defaultPrevented) {
816
+ this.close();
817
+ return;
818
+ }
819
+ const path = e.composedPath();
820
+ if (_Menu._menus.some((menu) => menu !== this && menu.isOpen && path.includes(menu.menuEl))) return;
821
+ e.preventDefault();
822
+ if (!this.isOpen) {
823
+ const root = this.el.getRootNode();
824
+ this._contextReturn = root.activeElement instanceof HTMLElement ? root.activeElement : null;
825
+ }
826
+ const rect = this.el.getBoundingClientRect();
827
+ const fromPointer = (e.clientX !== 0 || e.clientY !== 0) && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
828
+ this._contextPoint = fromPointer ? { x: e.clientX, y: e.clientY } : null;
829
+ if (this.isOpen) this.recalculateDimensions();
830
+ else this.open();
831
+ };
832
+ this._handleDocumentContextMenu = (e) => {
833
+ const path = e.composedPath();
834
+ if (path.includes(this.menuEl)) {
835
+ e.preventDefault();
836
+ return;
837
+ }
838
+ if (!path.includes(this.el)) this.close();
839
+ };
790
840
  this._handleClick = (e) => {
791
841
  e.preventDefault();
792
842
  if (this.isOpen) {
@@ -833,6 +883,7 @@ var Expressive = (() => {
833
883
  this.isTouchMoving = false;
834
884
  };
835
885
  this._handleTriggerKeydown = (e) => {
886
+ if (this.options.contextMenu) return;
836
887
  const opensMenu = e.key === SPACE || e.key === Utils.keys.ENTER || e.key === Utils.keys.ARROW_DOWN || e.key === Utils.keys.ARROW_UP;
837
888
  if (opensMenu && !this.isOpen) {
838
889
  e.preventDefault();
@@ -991,7 +1042,7 @@ var Expressive = (() => {
991
1042
  this._placeMenu();
992
1043
  this._schedule(() => this._setupTemporaryEventHandlers(), 0);
993
1044
  this._animateIn();
994
- this.el.ariaExpanded = "true";
1045
+ this._setExpanded(true);
995
1046
  };
996
1047
  /**
997
1048
  * Close menu.
@@ -1013,10 +1064,13 @@ var Expressive = (() => {
1013
1064
  if (transition !== this._transition) return;
1014
1065
  this._animateOut();
1015
1066
  this._removeTemporaryEventHandlers();
1016
- this.el.ariaExpanded = "false";
1067
+ this._setExpanded(false);
1017
1068
  if (this.options.autoFocus && this.menuEl.contains(focused) && (root.activeElement === focused || this.el.ownerDocument.activeElement === this.el.ownerDocument.body)) {
1018
- this.el.focus();
1069
+ const back = this._contextReturn;
1070
+ (back?.isConnected && back !== this.el.ownerDocument.body ? back : this.el).focus();
1019
1071
  }
1072
+ this._contextReturn = null;
1073
+ this._contextPoint = null;
1020
1074
  };
1021
1075
  /**
1022
1076
  * While menu is open, you can recalculate its dimensions if its contents have changed.
@@ -1034,6 +1088,7 @@ var Expressive = (() => {
1034
1088
  this._originalInert = this.menuEl?.getAttribute("inert") ?? null;
1035
1089
  this.options = {
1036
1090
  ..._Menu.defaults,
1091
+ contextMenu: el.hasAttribute("data-context-menu"),
1037
1092
  ...options
1038
1093
  };
1039
1094
  this.isOpen = false;
@@ -1041,10 +1096,12 @@ var Expressive = (() => {
1041
1096
  this.isTouchMoving = false;
1042
1097
  this.focusedIndex = -1;
1043
1098
  this.filterQuery = [];
1044
- this.el.ariaExpanded = "false";
1045
- if (!this.el.hasAttribute("aria-haspopup")) this.el.setAttribute("aria-haspopup", "menu");
1046
- if (this.id && !this.el.hasAttribute("aria-controls"))
1047
- this.el.setAttribute("aria-controls", this.id);
1099
+ if (!this.options.contextMenu) {
1100
+ this.el.ariaExpanded = "false";
1101
+ if (!this.el.hasAttribute("aria-haspopup")) this.el.setAttribute("aria-haspopup", "menu");
1102
+ if (this.id && !this.el.hasAttribute("aria-controls"))
1103
+ this.el.setAttribute("aria-controls", this.id);
1104
+ }
1048
1105
  this._moveMenuToElement();
1049
1106
  this._setupAccessibility();
1050
1107
  this._makeMenuFocusable();
@@ -1068,7 +1125,7 @@ var Expressive = (() => {
1068
1125
  destroy() {
1069
1126
  this._clearPendingTimers();
1070
1127
  this.isOpen = false;
1071
- this.el.ariaExpanded = "false";
1128
+ this._setExpanded(false);
1072
1129
  clearTimeout(this.filterTimeout);
1073
1130
  this._resetMenuStyles();
1074
1131
  this._restoreInert();
@@ -1084,7 +1141,9 @@ var Expressive = (() => {
1084
1141
  if (this.menuEl?.querySelector(":scope > li > menu")) {
1085
1142
  this.menuEl.addEventListener("mouseover", this._handleSubmenuAlign);
1086
1143
  }
1087
- if (this.options.hover) {
1144
+ if (this.options.contextMenu) {
1145
+ this.el.addEventListener("contextmenu", this._handleContextMenu);
1146
+ } else if (this.options.hover) {
1088
1147
  this.el.addEventListener("mouseenter", this._handleMouseEnter);
1089
1148
  this.el.addEventListener("mouseleave", this._handleMouseLeave);
1090
1149
  this.menuEl.addEventListener("mouseleave", this._handleMouseLeave);
@@ -1096,7 +1155,9 @@ var Expressive = (() => {
1096
1155
  this.el.removeEventListener("keydown", this._handleTriggerKeydown);
1097
1156
  this.menuEl?.removeEventListener("click", this._handleMenuClick);
1098
1157
  this.menuEl?.removeEventListener("mouseover", this._handleSubmenuAlign);
1099
- if (this.options.hover) {
1158
+ if (this.options.contextMenu) {
1159
+ this.el.removeEventListener("contextmenu", this._handleContextMenu);
1160
+ } else if (this.options.hover) {
1100
1161
  this.el.removeEventListener("mouseenter", this._handleMouseEnter);
1101
1162
  this.el.removeEventListener("mouseleave", this._handleMouseLeave);
1102
1163
  this.menuEl?.removeEventListener("mouseleave", this._handleMouseLeave);
@@ -1112,6 +1173,7 @@ var Expressive = (() => {
1112
1173
  });
1113
1174
  this.menuEl?.addEventListener("keydown", this._handleMenuKeydown);
1114
1175
  window.addEventListener("resize", this._handleWindowResize);
1176
+ if (this.options.contextMenu) document.addEventListener("contextmenu", this._handleDocumentContextMenu);
1115
1177
  }
1116
1178
  _removeTemporaryEventHandlers() {
1117
1179
  document.body.removeEventListener("click", this._handleDocumentClick);
@@ -1119,6 +1181,7 @@ var Expressive = (() => {
1119
1181
  document.body.removeEventListener("touchmove", this._handleDocumentTouchmove);
1120
1182
  this.menuEl?.removeEventListener("keydown", this._handleMenuKeydown);
1121
1183
  window.removeEventListener("resize", this._handleWindowResize);
1184
+ document.removeEventListener("contextmenu", this._handleDocumentContextMenu);
1122
1185
  }
1123
1186
  _setupAccessibility() {
1124
1187
  if (!this.menuEl) return;
@@ -1216,6 +1279,7 @@ var Expressive = (() => {
1216
1279
  item.scrollIntoView({ block: "nearest", inline: "nearest" });
1217
1280
  }
1218
1281
  _resetMenuStyles() {
1282
+ if (!this.menuEl) return;
1219
1283
  this.menuEl.style.display = "";
1220
1284
  this._resetMenuPositioningStyles();
1221
1285
  this.menuEl.style.transform = "";
@@ -1408,8 +1472,12 @@ var Expressive = (() => {
1408
1472
  closestOverflowParent.style.position = "relative";
1409
1473
  const natural = this.menuEl.getBoundingClientRect().width;
1410
1474
  const triggerWidth = this.el.getBoundingClientRect().width;
1411
- const idealWidth = this.options.constrainWidth && triggerWidth >= MIN_SURFACE_WIDTH ? triggerWidth : Math.min(MAX_SURFACE_WIDTH, Math.max(MIN_SURFACE_WIDTH, natural));
1475
+ const idealWidth = this.options.constrainWidth && !this.options.contextMenu && triggerWidth >= MIN_SURFACE_WIDTH ? triggerWidth : Math.min(MAX_SURFACE_WIDTH, Math.max(MIN_SURFACE_WIDTH, natural));
1412
1476
  this.menuEl.style.width = idealWidth + "px";
1477
+ if (this.options.contextMenu) {
1478
+ this._placeMenuAtPoint(this._contextPoint ?? this._regionCorner(), closestOverflowParent);
1479
+ return;
1480
+ }
1413
1481
  const positionInfo = this._getMenuPosition(closestOverflowParent);
1414
1482
  this.menuEl.style.left = positionInfo.x + "px";
1415
1483
  this.menuEl.style.top = positionInfo.y + "px";
@@ -1417,6 +1485,58 @@ var Expressive = (() => {
1417
1485
  this.menuEl.style.width = positionInfo.width + "px";
1418
1486
  this.menuEl.style.transformOrigin = `${positionInfo.horizontalAlignment === "left" ? "0" : "100%"} ${positionInfo.verticalAlignment === "top" ? "0" : "100%"}`;
1419
1487
  }
1488
+ /** Where a context menu opens without a pointer: the region's start corner. */
1489
+ _regionCorner() {
1490
+ const rect = this.el.getBoundingClientRect();
1491
+ const rtl = getComputedStyle(this.el).direction === "rtl";
1492
+ return { x: rtl ? rect.right : rect.left, y: rect.bottom };
1493
+ }
1494
+ /**
1495
+ * Places a context menu with its start corner on the point, flipping to
1496
+ * the other side of the point where it would not fit. The bounds are the
1497
+ * viewport, narrowed to a scrolling or clipping ancestor the menu sits in;
1498
+ * a menu taller than them gets their height and scrolls.
1499
+ *
1500
+ * The menu rests at left/top 0 of its containing block, so its rect is the
1501
+ * offset to subtract - the same trick _getMenuPosition uses. It is measured
1502
+ * without the entry animation's scale, which a second right-click can catch.
1503
+ */
1504
+ _placeMenuAtPoint({ x, y }, container) {
1505
+ const { transform, transition } = this.menuEl.style;
1506
+ this.menuEl.style.transition = "none";
1507
+ this.menuEl.style.transform = "none";
1508
+ const menu = this.menuEl.getBoundingClientRect();
1509
+ this.menuEl.style.transform = transform;
1510
+ this.menuEl.style.transition = transition;
1511
+ let bounds = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
1512
+ if (container && getComputedStyle(container).overflow !== "visible") {
1513
+ const clip = container.getBoundingClientRect();
1514
+ bounds = {
1515
+ left: Math.max(bounds.left, clip.left),
1516
+ top: Math.max(bounds.top, clip.top),
1517
+ right: Math.min(bounds.right, clip.right),
1518
+ bottom: Math.min(bounds.bottom, clip.bottom)
1519
+ };
1520
+ }
1521
+ const width = menu.width;
1522
+ const height = Math.min(menu.height, bounds.bottom - bounds.top);
1523
+ this.isScrollable = height < menu.height;
1524
+ if (this.isScrollable) this.menuEl.style.height = height + "px";
1525
+ const rtl = getComputedStyle(this.el).direction === "rtl";
1526
+ const fitsRight = x + width <= bounds.right;
1527
+ const fitsLeft = x - width >= bounds.left;
1528
+ const leftward = rtl ? fitsLeft || !fitsRight : !fitsRight && fitsLeft;
1529
+ const upward = y + height > bounds.bottom && y - height >= bounds.top;
1530
+ const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
1531
+ const left = clamp(leftward ? x - width : x, bounds.left, bounds.right - width);
1532
+ const top = clamp(upward ? y - height : y, bounds.top, bounds.bottom - height);
1533
+ this.menuEl.style.left = left - menu.left + "px";
1534
+ this.menuEl.style.top = top - menu.top + "px";
1535
+ this.menuEl.style.transformOrigin = `${leftward ? "100%" : "0"} ${upward ? "100%" : "0"}`;
1536
+ }
1537
+ _setExpanded(expanded) {
1538
+ if (!this.options.contextMenu) this.el.ariaExpanded = String(expanded);
1539
+ }
1420
1540
  };
1421
1541
  _Menu._menus = [];
1422
1542
  var Menu = _Menu;
@@ -1471,8 +1591,9 @@ var Expressive = (() => {
1471
1591
  this._handleInput = () => {
1472
1592
  this.count = 0;
1473
1593
  const actualValue = this.el.value.toLocaleLowerCase();
1474
- if (this.oldVal !== actualValue && this.el.matches(":focus-visible")) {
1475
- this.open();
1594
+ if (this.oldVal !== actualValue) {
1595
+ this._resetCurrentElementPosition();
1596
+ if (this.el.matches(":focus-visible") && (!this.menu.isOpen || actualValue.length < this.options.minLength)) this.open();
1476
1597
  }
1477
1598
  this._inputChangeDetection(actualValue);
1478
1599
  };
@@ -1782,11 +1903,24 @@ var Expressive = (() => {
1782
1903
  item.appendChild(img);
1783
1904
  }
1784
1905
  const inputText = this.el.value.toLocaleLowerCase();
1785
- const parts = this._highlightPartialText(inputText, (entry.text || entry.id).toString());
1906
+ const label = (entry.text || entry.id).toString();
1907
+ const parts = this._highlightPartialText(inputText, label);
1786
1908
  const div = document.createElement("div");
1787
1909
  div.setAttribute("style", "line-height:1.2;font-weight:500;");
1788
1910
  if (this.options.allowUnsafeHTML) {
1789
- div.innerHTML = parts[0] + '<span class="highlight">' + parts[1] + "</span>" + parts[2];
1911
+ div.innerHTML = label;
1912
+ const walker = document.createTreeWalker(div, NodeFilter.SHOW_TEXT);
1913
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
1914
+ const [before, match] = this._highlightPartialText(inputText, node.data);
1915
+ if (!match) continue;
1916
+ const matched = node.splitText(before.length);
1917
+ matched.splitText(match.length);
1918
+ const highlight = document.createElement("span");
1919
+ highlight.classList.add("highlight");
1920
+ matched.replaceWith(highlight);
1921
+ highlight.append(matched);
1922
+ break;
1923
+ }
1790
1924
  } else {
1791
1925
  div.appendChild(document.createTextNode(parts[0]));
1792
1926
  if (parts[1]) {
@@ -1878,7 +2012,7 @@ var Expressive = (() => {
1878
2012
  );
1879
2013
  }
1880
2014
  if (this.options.isMultiSelect) {
1881
- this._renderMenu();
2015
+ if (!open) this._renderMenu();
1882
2016
  } else {
1883
2017
  this._refreshInputText();
1884
2018
  }
@@ -2276,12 +2410,10 @@ var Expressive = (() => {
2276
2410
  this._sync();
2277
2411
  this.thumb.classList.add("active");
2278
2412
  };
2279
- this._handleRangeInputOrMove = (e) => {
2280
- if (e.type === "input" || this._pointerDown) {
2281
- this._clampDual();
2282
- this._sync();
2283
- this.thumb.classList.add("active");
2284
- }
2413
+ this._handleRangeInput = () => {
2414
+ this._clampDual();
2415
+ this._sync();
2416
+ this.thumb.classList.add("active");
2285
2417
  };
2286
2418
  this._handleRangePointerUp = (e) => {
2287
2419
  if (!e.isPrimary) return;
@@ -2345,8 +2477,7 @@ var Expressive = (() => {
2345
2477
  _setupEventHandlers() {
2346
2478
  this.el.addEventListener("change", this._handleRangeChange);
2347
2479
  this.el.addEventListener("pointerdown", this._handleRangePointerDown);
2348
- this.el.addEventListener("input", this._handleRangeInputOrMove);
2349
- this.el.addEventListener("pointermove", this._handleRangeInputOrMove, { passive: true });
2480
+ this.el.addEventListener("input", this._handleRangeInput);
2350
2481
  this.el.addEventListener("pointerup", this._handleRangePointerUp);
2351
2482
  this.el.addEventListener("pointercancel", this._handleRangePointerUp);
2352
2483
  this.el.addEventListener("blur", this._handleRangeRelease);
@@ -2355,8 +2486,7 @@ var Expressive = (() => {
2355
2486
  _removeEventHandlers() {
2356
2487
  this.el.removeEventListener("change", this._handleRangeChange);
2357
2488
  this.el.removeEventListener("pointerdown", this._handleRangePointerDown);
2358
- this.el.removeEventListener("input", this._handleRangeInputOrMove);
2359
- this.el.removeEventListener("pointermove", this._handleRangeInputOrMove);
2489
+ this.el.removeEventListener("input", this._handleRangeInput);
2360
2490
  this.el.removeEventListener("pointerup", this._handleRangePointerUp);
2361
2491
  this.el.removeEventListener("pointercancel", this._handleRangePointerUp);
2362
2492
  this.el.removeEventListener("blur", this._handleRangeRelease);
@@ -2524,7 +2654,10 @@ var Expressive = (() => {
2524
2654
  var DISMISS_VELOCITY = 0.5;
2525
2655
  var FLICK_TIMEOUT = 100;
2526
2656
  var DRAG_SLOP = 4;
2657
+ var installed = /* @__PURE__ */ new Set();
2527
2658
  function installSheetDrag(config) {
2659
+ if (installed.has(config.selector)) return;
2660
+ installed.add(config.selector);
2528
2661
  const coord = (event) => config.axis === "x" ? event.clientX : event.clientY;
2529
2662
  let observer;
2530
2663
  let drag = null;
@@ -2685,7 +2818,7 @@ var Expressive = (() => {
2685
2818
  };
2686
2819
 
2687
2820
  // src/ts/behaviors/forms.ts
2688
- var Forms = class _Forms {
2821
+ var _Forms = class _Forms {
2689
2822
  /**
2690
2823
  * Checks if the label has validation and apply
2691
2824
  * the correct class and styles
@@ -2727,10 +2860,20 @@ var Expressive = (() => {
2727
2860
  const contentHeight = textarea.scrollHeight + borders;
2728
2861
  textarea.style.height = Math.max(originalHeight, contentHeight) + "px";
2729
2862
  }
2863
+ /**
2864
+ * Safe to call more than once: the root entry calls it on import, and a page
2865
+ * that also loads the modular entry may call it again.
2866
+ */
2730
2867
  static Init() {
2868
+ if (_Forms._initialized) return;
2869
+ _Forms._initialized = true;
2731
2870
  Utils.onDocumentReady(() => {
2732
2871
  document.addEventListener("change", (e) => {
2733
2872
  const target = e.target;
2873
+ if (target instanceof HTMLInputElement && target.type === "file") {
2874
+ const zone = target.closest(".drop-zone");
2875
+ if (zone) _Forms.listDropZoneFiles(zone, target);
2876
+ }
2734
2877
  if (target instanceof HTMLInputElement) {
2735
2878
  _Forms.validateField(target);
2736
2879
  }
@@ -2746,6 +2889,12 @@ var Expressive = (() => {
2746
2889
  }
2747
2890
  }
2748
2891
  });
2892
+ document.addEventListener("click", _Forms.togglePassword);
2893
+ document.addEventListener("submit", _Forms.resetFormExtras, true);
2894
+ document.addEventListener("reset", _Forms.resetFormExtras, true);
2895
+ for (const type of ["dragenter", "dragover", "dragleave", "drop"]) {
2896
+ document.addEventListener(type, _Forms.markDropZone);
2897
+ }
2749
2898
  document.querySelectorAll(".expressive-textarea").forEach((textArea) => {
2750
2899
  _Forms.InitTextarea(textArea);
2751
2900
  });
@@ -2754,6 +2903,75 @@ var Expressive = (() => {
2754
2903
  });
2755
2904
  });
2756
2905
  }
2906
+ /**
2907
+ * A .password-toggle button shows or hides the password in its field. The
2908
+ * button's own name stays put ("Show password"); aria-pressed carries the
2909
+ * state, so there is no generated text to translate.
2910
+ */
2911
+ static togglePassword(e) {
2912
+ const button = e.target.closest?.(".password-toggle");
2913
+ const input = button?.closest(".field")?.querySelector(
2914
+ 'input[type="password"], input[data-password]'
2915
+ );
2916
+ if (!input) return;
2917
+ const show = input.type === "password";
2918
+ input.toggleAttribute("data-password", show);
2919
+ input.type = show ? "text" : "password";
2920
+ button.setAttribute("aria-pressed", String(show));
2921
+ }
2922
+ /**
2923
+ * A revealed password goes back to type="password" when its form is
2924
+ * submitted or reset, so it is never sent or autofilled as plain text and
2925
+ * a reset field does not stay revealed. A reset form also drops the file
2926
+ * lists it would otherwise leave showing: reset fires no change event.
2927
+ */
2928
+ static resetFormExtras(e) {
2929
+ const form = e.target;
2930
+ if (!(form instanceof HTMLFormElement)) return;
2931
+ form.querySelectorAll("input[data-password]").forEach((input) => {
2932
+ input.removeAttribute("data-password");
2933
+ input.type = "password";
2934
+ input.closest(".field")?.querySelector(".password-toggle")?.setAttribute("aria-pressed", "false");
2935
+ });
2936
+ if (e.type === "reset") {
2937
+ form.querySelectorAll(".drop-zone-files").forEach((list) => list.replaceChildren());
2938
+ form.querySelectorAll(".drop-zone.dragover").forEach((zone) => zone.classList.remove("dragover"));
2939
+ }
2940
+ }
2941
+ /**
2942
+ * The native file input covers its .drop-zone, so the browser already takes
2943
+ * a dropped file. This only shows the drag: the input is the topmost
2944
+ * element, so every drag event over the zone targets it and leaving it is
2945
+ * leaving the zone.
2946
+ */
2947
+ static markDropZone(e) {
2948
+ const input = e.target;
2949
+ if (!(input instanceof HTMLInputElement) || input.type !== "file") return;
2950
+ const zone = input.closest(".drop-zone");
2951
+ if (!zone) return;
2952
+ const over = e.type === "dragenter" || e.type === "dragover";
2953
+ zone.classList.toggle("dragover", over && !input.disabled);
2954
+ }
2955
+ /** Lists the chosen files, name and size, in the zone's .drop-zone-files. */
2956
+ static listDropZoneFiles(zone, input) {
2957
+ const list = zone.querySelector(".drop-zone-files");
2958
+ if (!list) return;
2959
+ const size = (bytes) => {
2960
+ const [value, unit] = bytes >= 1e6 ? [bytes / 1e6, "megabyte"] : bytes >= 1e3 ? [bytes / 1e3, "kilobyte"] : [bytes, "byte"];
2961
+ return new Intl.NumberFormat(void 0, { style: "unit", unit, maximumFractionDigits: 1 }).format(value);
2962
+ };
2963
+ list.replaceChildren(
2964
+ ...Array.from(input.files ?? [], (file) => {
2965
+ const item = document.createElement("li");
2966
+ const name = document.createElement("span");
2967
+ const meta = document.createElement("span");
2968
+ name.textContent = file.name;
2969
+ meta.textContent = size(file.size);
2970
+ item.append(name, meta);
2971
+ return item;
2972
+ })
2973
+ );
2974
+ }
2757
2975
  static InitTextarea(textarea) {
2758
2976
  textarea.setAttribute("original-height", textarea.getBoundingClientRect().height.toString());
2759
2977
  _Forms.textareaAutoResize(textarea);
@@ -2775,6 +2993,8 @@ var Expressive = (() => {
2775
2993
  });
2776
2994
  }
2777
2995
  };
2996
+ _Forms._initialized = false;
2997
+ var Forms = _Forms;
2778
2998
 
2779
2999
  // src/ts/components/appBar.ts
2780
3000
  var _defaults7 = {};
@@ -3267,6 +3487,7 @@ var Expressive = (() => {
3267
3487
  this._dragStartX = e.clientX;
3268
3488
  this._dragStartY = e.clientY;
3269
3489
  this._dragStartScroll = this._vertical ? this._scroller.scrollTop : this._scroller.scrollLeft;
3490
+ this._setDragListeners(true);
3270
3491
  };
3271
3492
  this._handleTrackPointerMove = (e) => {
3272
3493
  if (!this.pressed || e.pointerId !== this._dragPointerId) return;
@@ -3291,6 +3512,7 @@ var Expressive = (() => {
3291
3512
  if (!this.pressed || e.pointerId !== this._dragPointerId) return;
3292
3513
  this.pressed = false;
3293
3514
  this._dragPointerId = null;
3515
+ this._setDragListeners(false);
3294
3516
  this.el.classList.remove("dragging");
3295
3517
  try {
3296
3518
  this._scroller.releasePointerCapture(e.pointerId);
@@ -3357,9 +3579,8 @@ var Expressive = (() => {
3357
3579
  if (this._trackRaf !== null) return;
3358
3580
  this._trackRaf = requestAnimationFrame(() => {
3359
3581
  this._trackRaf = null;
3582
+ const index = this._ignoreScroll ? this.center : this._nearestIndex();
3360
3583
  this._updateParallax();
3361
- if (this._ignoreScroll) return;
3362
- const index = this._nearestIndex();
3363
3584
  if (index !== this.center) this._syncActive(index, this.dragged);
3364
3585
  });
3365
3586
  };
@@ -3372,6 +3593,7 @@ var Expressive = (() => {
3372
3593
  };
3373
3594
  this._finishScroll = () => {
3374
3595
  this._ignoreScroll = false;
3596
+ this._scroller.style.scrollSnapType = "";
3375
3597
  clearTimeout(this._scrollEndTimeout);
3376
3598
  this._scroller.removeEventListener("scrollend", this._handleScrollEnd);
3377
3599
  };
@@ -3551,9 +3773,6 @@ var Expressive = (() => {
3551
3773
  });
3552
3774
  this._scroller.addEventListener("pointerdown", this._handleTrackPointerDown);
3553
3775
  this._scroller.addEventListener("wheel", this._handleNativeScroll, { passive: true });
3554
- document.addEventListener("pointermove", this._handleTrackPointerMove);
3555
- document.addEventListener("pointerup", this._handleTrackPointerUp);
3556
- document.addEventListener("pointercancel", this._handleTrackPointerUp);
3557
3776
  this._scroller.addEventListener("click", this._handleTrackClick, true);
3558
3777
  this._scroller.addEventListener("dragstart", this._handleTrackDragStart);
3559
3778
  this._scroller.addEventListener("transitionend", this._handleItemResize);
@@ -3572,9 +3791,7 @@ var Expressive = (() => {
3572
3791
  this._scroller.removeEventListener("scroll", this._handleTrackScroll);
3573
3792
  this._scroller.removeEventListener("pointerdown", this._handleTrackPointerDown);
3574
3793
  this._scroller.removeEventListener("wheel", this._handleNativeScroll);
3575
- document.removeEventListener("pointermove", this._handleTrackPointerMove);
3576
- document.removeEventListener("pointerup", this._handleTrackPointerUp);
3577
- document.removeEventListener("pointercancel", this._handleTrackPointerUp);
3794
+ this._setDragListeners(false);
3578
3795
  this._scroller.removeEventListener("click", this._handleTrackClick, true);
3579
3796
  this._scroller.removeEventListener("dragstart", this._handleTrackDragStart);
3580
3797
  this._scroller.removeEventListener("transitionend", this._handleItemResize);
@@ -3641,6 +3858,13 @@ var Expressive = (() => {
3641
3858
  this._autoAdvanceTimer = null;
3642
3859
  }
3643
3860
  }
3861
+ /** The document only hears from a carousel while a mouse press is down on its track. */
3862
+ _setDragListeners(on) {
3863
+ const method = on ? "addEventListener" : "removeEventListener";
3864
+ document[method]("pointermove", this._handleTrackPointerMove);
3865
+ document[method]("pointerup", this._handleTrackPointerUp);
3866
+ document[method]("pointercancel", this._handleTrackPointerUp);
3867
+ }
3644
3868
  _paintIndicators(center) {
3645
3869
  if (!this.showIndicators || !this._indicators) return;
3646
3870
  const pos = (center % this.count + this.count) % this.count;
@@ -3667,40 +3891,31 @@ var Expressive = (() => {
3667
3891
  }
3668
3892
  }
3669
3893
  _syncLayoutRoles(index) {
3670
- const setSize = (item, size) => {
3671
- if (this._authoredItemSizes.has(item)) return;
3672
- item.setAttribute("data-carousel-size", size);
3673
- this._generatedItemSizes.add(item);
3894
+ const sizes = this.images.map(() => "large");
3895
+ const setSize = (i, size) => {
3896
+ if (this.images[i]) sizes[i] = size;
3674
3897
  };
3675
- this.images.forEach((item) => setSize(item, "large"));
3676
3898
  if (this.el.classList.contains("flat") || this.el.classList.contains("uncontained") || this.el.classList.contains("full-screen") && this._vertical) {
3677
- return;
3678
- }
3679
- if (this.el.classList.contains("hero") || this.el.classList.contains("full-screen-horizontal")) {
3680
- const next = this.images[index + 1];
3681
- if (next) setSize(next, "small");
3682
- if (this.el.classList.contains("center-aligned")) {
3683
- const previous = this.images[index - 1];
3684
- if (previous) setSize(previous, "small");
3685
- }
3686
- return;
3687
- }
3688
- const width = this.el.clientWidth;
3689
- const desiredLargeCount = width >= 1600 ? 4 : width >= 1200 ? 3 : width >= 600 ? 2 : 1;
3690
- const largeCount = Math.min(desiredLargeCount, Math.max(1, this.count - 2));
3691
- this.el.classList.remove("multi-wide", "multi-large", "multi-extra-large");
3692
- if (largeCount === 2) this.el.classList.add("multi-wide");
3693
- else if (largeCount === 3) this.el.classList.add("multi-large");
3694
- else if (largeCount >= 4) this.el.classList.add("multi-extra-large");
3695
- this._generatedWideLayout = this._generatedWideLayout || largeCount > 1;
3696
- for (let offset = 1; offset < largeCount; offset += 1) {
3697
- const item = this.images[index + offset];
3698
- if (item) setSize(item, "large");
3699
- }
3700
- const medium = this.images[index + largeCount];
3701
- const small = this.images[index + largeCount + 1];
3702
- if (medium) setSize(medium, "medium");
3703
- if (small) setSize(small, "small");
3899
+ } else if (this.el.classList.contains("hero") || this.el.classList.contains("full-screen-horizontal")) {
3900
+ setSize(index + 1, "small");
3901
+ if (this.el.classList.contains("center-aligned")) setSize(index - 1, "small");
3902
+ } else {
3903
+ const width = this.el.clientWidth;
3904
+ const desiredLargeCount = width >= 1600 ? 4 : width >= 1200 ? 3 : width >= 600 ? 2 : 1;
3905
+ const largeCount = Math.min(desiredLargeCount, Math.max(1, this.count - 2));
3906
+ this.el.classList.remove("multi-wide", "multi-large", "multi-extra-large");
3907
+ if (largeCount === 2) this.el.classList.add("multi-wide");
3908
+ else if (largeCount === 3) this.el.classList.add("multi-large");
3909
+ else if (largeCount >= 4) this.el.classList.add("multi-extra-large");
3910
+ this._generatedWideLayout = this._generatedWideLayout || largeCount > 1;
3911
+ setSize(index + largeCount, "medium");
3912
+ setSize(index + largeCount + 1, "small");
3913
+ }
3914
+ this.images.forEach((item, i) => {
3915
+ if (this._authoredItemSizes.has(item)) return;
3916
+ item.setAttribute("data-carousel-size", sizes[i]);
3917
+ this._generatedItemSizes.add(item);
3918
+ });
3704
3919
  }
3705
3920
  _syncAdaptiveMode() {
3706
3921
  if (!this.el.classList.contains("full-screen")) return;
@@ -3725,7 +3940,7 @@ var Expressive = (() => {
3725
3940
  (focusable ?? item).focus({ preventScroll: true });
3726
3941
  }
3727
3942
  _updateParallax() {
3728
- if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
3943
+ if (this._motion.matches) {
3729
3944
  this.images.forEach(
3730
3945
  (item) => item.style.setProperty("--md-comp-carousel-item-parallax", "0px")
3731
3946
  );
@@ -3734,8 +3949,9 @@ var Expressive = (() => {
3734
3949
  const scrollerRect = this._scroller.getBoundingClientRect();
3735
3950
  const viewportCenter = this._vertical ? scrollerRect.top + scrollerRect.height / 2 : scrollerRect.left + scrollerRect.width / 2;
3736
3951
  const viewportSize = Math.max(1, this._vertical ? scrollerRect.height : scrollerRect.width);
3737
- this.images.forEach((item) => {
3738
- const rect = item.getBoundingClientRect();
3952
+ const rects = this.images.map((item) => item.getBoundingClientRect());
3953
+ this.images.forEach((item, i) => {
3954
+ const rect = rects[i];
3739
3955
  const itemCenter = this._vertical ? rect.top + rect.height / 2 : rect.left + rect.width / 2;
3740
3956
  const progress = Math.max(-1, Math.min(1, (itemCenter - viewportCenter) / viewportSize));
3741
3957
  item.style.setProperty("--md-comp-carousel-item-parallax", `${progress * -24}px`);
@@ -3788,6 +4004,7 @@ var Expressive = (() => {
3788
4004
  const left = centered ? el.offsetLeft - (this._scroller.clientWidth - el.offsetWidth) / 2 : style.direction === "rtl" ? el.offsetLeft + el.offsetWidth - this._scroller.clientWidth + inlinePadding : el.offsetLeft - inlinePadding;
3789
4005
  const top = el.offsetTop - blockPadding;
3790
4006
  this._ignoreScroll = true;
4007
+ this._scroller.style.scrollSnapType = "none";
3791
4008
  this._scroller.scrollTo({
3792
4009
  left: this._vertical ? 0 : left,
3793
4010
  top: this._vertical ? top : 0,
@@ -3865,60 +4082,1988 @@ var Expressive = (() => {
3865
4082
  this._setupCounter();
3866
4083
  this._setupEventHandlers();
3867
4084
  }
3868
- static get defaults() {
3869
- return _defaults11;
4085
+ static get defaults() {
4086
+ return _defaults11;
4087
+ }
4088
+ /**
4089
+ * Initializes instances of CharacterCounter.
4090
+ * @param els HTML elements.
4091
+ * @param options Component options.
4092
+ */
4093
+ static init(els, options = {}) {
4094
+ return super.init(els, options, _CharacterCounter);
4095
+ }
4096
+ static getInstance(el) {
4097
+ return el["Expressive_CharacterCounter"];
4098
+ }
4099
+ destroy() {
4100
+ this._removeEventHandlers();
4101
+ this.el["Expressive_CharacterCounter"] = void 0;
4102
+ this._removeCounter();
4103
+ }
4104
+ _setupEventHandlers() {
4105
+ this.el.addEventListener("focus", this.updateCounter, true);
4106
+ this.el.addEventListener("input", this.updateCounter, true);
4107
+ }
4108
+ _removeEventHandlers() {
4109
+ this.el.removeEventListener("focus", this.updateCounter, true);
4110
+ this.el.removeEventListener("input", this.updateCounter, true);
4111
+ }
4112
+ _setupCounter() {
4113
+ this.counterEl = document.createElement("span");
4114
+ this.counterEl.classList.add("character-counter");
4115
+ this.counterEl.setAttribute("aria-live", "polite");
4116
+ this.counterEl.setAttribute("aria-atomic", "true");
4117
+ this.counterEl.style.float = "right";
4118
+ this.counterEl.style.fontSize = "12px";
4119
+ this.counterEl.style.height = "1";
4120
+ this.el.parentElement.appendChild(this.counterEl);
4121
+ }
4122
+ _removeCounter() {
4123
+ this.counterEl.remove();
4124
+ }
4125
+ _validateInput() {
4126
+ if (this.isValidLength && this.isInvalid) {
4127
+ this.isInvalid = false;
4128
+ this.el.classList.remove("invalid");
4129
+ } else if (!this.isValidLength && !this.isInvalid) {
4130
+ this.isInvalid = true;
4131
+ this.el.classList.remove("valid");
4132
+ this.el.classList.add("invalid");
4133
+ }
4134
+ }
4135
+ };
4136
+
4137
+ // src/ts/components/confetti.ts
4138
+ var _defaults12 = {
4139
+ particleCount: 50,
4140
+ angle: 90,
4141
+ spread: 45,
4142
+ startVelocity: 45,
4143
+ decay: 0.9,
4144
+ gravity: 1,
4145
+ drift: 0,
4146
+ ticks: 200,
4147
+ origin: { x: 0.5, y: 0.5 },
4148
+ colors: [],
4149
+ shapes: ["square", "circle"],
4150
+ scalar: 1,
4151
+ manualStart: false
4152
+ };
4153
+ var THEME_COLORS = ["primary", "secondary", "tertiary", "primary-container", "secondary-container", "tertiary-container"];
4154
+ var FALLBACK_COLORS = ["#26ccff", "#a25afd", "#ff5e7e", "#88ff5a", "#fcff42", "#ffa62d", "#ff36ff"];
4155
+ var RAD = Math.PI / 180;
4156
+ var scenes = /* @__PURE__ */ new Map();
4157
+ var overlay = null;
4158
+ var Confetti = class _Confetti extends Component {
4159
+ constructor(el, options) {
4160
+ super(el, options, _Confetti);
4161
+ this._onClick = () => {
4162
+ this.fire();
4163
+ };
4164
+ this.el["Expressive_Confetti"] = this;
4165
+ this.options = { ..._Confetti.defaults, ...options };
4166
+ if (!(el instanceof HTMLCanvasElement)) el.addEventListener("click", this._onClick);
4167
+ else if (!this.options.manualStart) this.fire();
4168
+ }
4169
+ static get defaults() {
4170
+ return _defaults12;
4171
+ }
4172
+ static init(els, options = {}) {
4173
+ return super.init(els, options, _Confetti);
4174
+ }
4175
+ static getInstance(el) {
4176
+ return el["Expressive_Confetti"];
4177
+ }
4178
+ /**
4179
+ * Bursts over the whole viewport. Resolves when the last piece is gone.
4180
+ */
4181
+ static fire(options = {}) {
4182
+ return burst(viewportCanvas, document.body, { ..._Confetti.defaults, ...options });
4183
+ }
4184
+ /**
4185
+ * Bursts on this canvas, or from this element's center over the viewport.
4186
+ * Resolves when the last piece is gone.
4187
+ */
4188
+ fire(options = {}) {
4189
+ const merged = { ...this.options, ...options };
4190
+ if (this.el instanceof HTMLCanvasElement) {
4191
+ const canvas = this.el;
4192
+ return burst(() => canvas, canvas.parentElement ?? document.body, merged);
4193
+ }
4194
+ const r = this.el.getBoundingClientRect();
4195
+ merged.origin = { x: (r.left + r.width / 2) / innerWidth, y: (r.top + r.height / 2) / innerHeight };
4196
+ return burst(viewportCanvas, this.el.parentElement ?? document.body, merged);
4197
+ }
4198
+ destroy() {
4199
+ this.el.removeEventListener("click", this._onClick);
4200
+ if (this.el instanceof HTMLCanvasElement) stop(this.el);
4201
+ this.el["Expressive_Confetti"] = void 0;
4202
+ }
4203
+ };
4204
+ function burst(getCanvas, themed, o) {
4205
+ if (typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches) return Promise.resolve();
4206
+ const canvas = getCanvas();
4207
+ let scene = scenes.get(canvas);
4208
+ if (!scene) {
4209
+ const ctx = canvas.getContext("2d");
4210
+ if (!ctx) return Promise.resolve();
4211
+ scene = { ctx, pieces: [], frame: 0, last: 0, done: [], overlay: canvas === overlay };
4212
+ scenes.set(canvas, scene);
4213
+ }
4214
+ const { width, height } = fit(canvas);
4215
+ const colors = o.colors.length ? o.colors : themeColors(themed);
4216
+ const x = (o.origin.x ?? 0.5) * width;
4217
+ const y = (o.origin.y ?? 0.5) * height;
4218
+ for (let i = 0; i < o.particleCount; i++) {
4219
+ scene.pieces.push({
4220
+ x,
4221
+ y,
4222
+ velocity: o.startVelocity * (0.5 + Math.random()),
4223
+ angle: -o.angle * RAD + (0.5 - Math.random()) * o.spread * RAD,
4224
+ wobble: Math.random() * 10,
4225
+ wobbleSpeed: 0.05 + Math.random() * 0.05,
4226
+ tilt: (0.25 + Math.random() / 2) * Math.PI,
4227
+ tick: 0,
4228
+ color: colors[i % colors.length],
4229
+ shape: o.shapes[Math.floor(Math.random() * o.shapes.length)] ?? "square",
4230
+ options: o
4231
+ });
4232
+ }
4233
+ const s = scene;
4234
+ if (!s.frame) s.frame = requestAnimationFrame((now) => {
4235
+ s.last = now;
4236
+ draw(canvas, s, now);
4237
+ });
4238
+ return new Promise((resolve) => s.done.push(resolve));
4239
+ }
4240
+ function draw(canvas, scene, now) {
4241
+ const step = Math.min((now - scene.last) / (1e3 / 60), 4);
4242
+ scene.last = now;
4243
+ const { width, height } = fit(canvas);
4244
+ const { ctx } = scene;
4245
+ const dpr = canvas.width / (width || 1);
4246
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
4247
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
4248
+ scene.pieces = scene.pieces.filter((p) => {
4249
+ const o = p.options;
4250
+ p.x += (Math.cos(p.angle) * p.velocity + o.drift) * step;
4251
+ p.y += (Math.sin(p.angle) * p.velocity + o.gravity * 3) * step;
4252
+ p.velocity *= o.decay ** step;
4253
+ p.wobble += p.wobbleSpeed * step;
4254
+ p.tilt += 0.1 * step;
4255
+ p.tick += step;
4256
+ if (p.tick >= o.ticks) return false;
4257
+ const size = 8 * o.scalar;
4258
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
4259
+ ctx.translate(p.x + 10 * o.scalar * Math.cos(p.wobble), p.y + 10 * o.scalar * Math.sin(p.wobble));
4260
+ ctx.rotate(p.tilt);
4261
+ ctx.scale(1, Math.cos(p.wobble));
4262
+ ctx.globalAlpha = 1 - p.tick / o.ticks;
4263
+ ctx.fillStyle = p.color;
4264
+ ctx.beginPath();
4265
+ if (p.shape === "circle") ctx.arc(0, 0, size / 2, 0, 2 * Math.PI);
4266
+ else if (p.shape === "star") star(ctx, size * 0.75);
4267
+ else ctx.rect(-size / 2, -size / 2, size, size);
4268
+ ctx.fill();
4269
+ return true;
4270
+ });
4271
+ if (scene.pieces.length) scene.frame = requestAnimationFrame((t) => draw(canvas, scene, t));
4272
+ else stop(canvas);
4273
+ }
4274
+ function stop(canvas) {
4275
+ const scene = scenes.get(canvas);
4276
+ if (!scene) return;
4277
+ cancelAnimationFrame(scene.frame);
4278
+ scenes.delete(canvas);
4279
+ scene.ctx.setTransform(1, 0, 0, 1, 0, 0);
4280
+ scene.ctx.clearRect(0, 0, canvas.width, canvas.height);
4281
+ if (scene.overlay) {
4282
+ canvas.remove();
4283
+ overlay = null;
4284
+ }
4285
+ for (const resolve of scene.done) resolve();
4286
+ }
4287
+ function star(ctx, r) {
4288
+ for (let i = 0; i < 10; i++) {
4289
+ const a = i * Math.PI / 5 - Math.PI / 2;
4290
+ const d = i % 2 ? r / 2 : r;
4291
+ ctx.lineTo(Math.cos(a) * d, Math.sin(a) * d);
4292
+ }
4293
+ ctx.closePath();
4294
+ }
4295
+ function fit(canvas) {
4296
+ const width = canvas.clientWidth;
4297
+ const height = canvas.clientHeight;
4298
+ const w = Math.round(width * devicePixelRatio);
4299
+ const h = Math.round(height * devicePixelRatio);
4300
+ if (canvas.width !== w || canvas.height !== h) {
4301
+ canvas.width = w;
4302
+ canvas.height = h;
4303
+ }
4304
+ return { width, height };
4305
+ }
4306
+ function viewportCanvas() {
4307
+ if (overlay?.isConnected) return overlay;
4308
+ overlay = document.createElement("canvas");
4309
+ overlay.className = "confetti-overlay";
4310
+ overlay.setAttribute("aria-hidden", "true");
4311
+ Object.assign(overlay.style, {
4312
+ position: "fixed",
4313
+ inset: "0",
4314
+ width: "100%",
4315
+ height: "100%",
4316
+ margin: "0",
4317
+ padding: "0",
4318
+ border: "0",
4319
+ background: "transparent",
4320
+ pointerEvents: "none",
4321
+ zIndex: "10001"
4322
+ });
4323
+ document.body.append(overlay);
4324
+ if (typeof overlay.showPopover === "function") {
4325
+ overlay.popover = "manual";
4326
+ overlay.showPopover();
4327
+ }
4328
+ return overlay;
4329
+ }
4330
+ function themeColors(themed) {
4331
+ const probe = document.createElement("span");
4332
+ probe.hidden = true;
4333
+ themed.append(probe);
4334
+ const style = getComputedStyle(probe);
4335
+ const colors = THEME_COLORS.filter((role) => style.getPropertyValue(`--md-sys-color-${role}`).trim()).map((role) => {
4336
+ probe.style.color = `var(--md-sys-color-${role})`;
4337
+ return getComputedStyle(probe).color;
4338
+ });
4339
+ probe.remove();
4340
+ return colors.length ? colors : FALLBACK_COLORS;
4341
+ }
4342
+
4343
+ // src/ts/components/particles.ts
4344
+ var _defaults13 = {
4345
+ quantity: 100,
4346
+ staticity: 50,
4347
+ ease: 50,
4348
+ size: 0.4,
4349
+ color: "",
4350
+ vx: 0,
4351
+ vy: 0
4352
+ };
4353
+ var Particles = class _Particles extends Component {
4354
+ constructor(el, options) {
4355
+ super(el, options, _Particles);
4356
+ this._particles = [];
4357
+ this._width = 0;
4358
+ this._height = 0;
4359
+ this._frame = 0;
4360
+ this._last = 0;
4361
+ // Pointer offset from the canvas center, kept from its last move inside.
4362
+ this._mouse = { x: 0, y: 0 };
4363
+ this._still = false;
4364
+ this._resizeObserver = null;
4365
+ this._visibility = null;
4366
+ this._onPointerMove = (e) => {
4367
+ const r = this.el.getBoundingClientRect();
4368
+ const x = e.clientX - r.left - r.width / 2;
4369
+ const y = e.clientY - r.top - r.height / 2;
4370
+ if (Math.abs(x) < r.width / 2 && Math.abs(y) < r.height / 2) this._mouse = { x, y };
4371
+ };
4372
+ this.el["Expressive_Particles"] = this;
4373
+ this.options = { ..._Particles.defaults, ...options };
4374
+ this._ctx = this.el.getContext("2d");
4375
+ if (!this._ctx) return;
4376
+ this._style = getComputedStyle(this.el);
4377
+ this._still = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
4378
+ this._reset();
4379
+ if (typeof ResizeObserver !== "undefined") {
4380
+ this._resizeObserver = new ResizeObserver(() => {
4381
+ if (this.el.clientWidth !== this._width || this.el.clientHeight !== this._height) this._reset();
4382
+ });
4383
+ this._resizeObserver.observe(this.el);
4384
+ }
4385
+ if (this._still) return;
4386
+ if (typeof IntersectionObserver === "undefined") {
4387
+ this._start();
4388
+ return;
4389
+ }
4390
+ this._visibility = new IntersectionObserver(([entry]) => entry.isIntersecting ? this._start() : this._stop());
4391
+ this._visibility.observe(this.el);
4392
+ }
4393
+ static get defaults() {
4394
+ return _defaults13;
4395
+ }
4396
+ static init(els, options = {}) {
4397
+ return super.init(els, options, _Particles);
4398
+ }
4399
+ static getInstance(el) {
4400
+ return el["Expressive_Particles"];
4401
+ }
4402
+ destroy() {
4403
+ this._stop();
4404
+ this._resizeObserver?.disconnect();
4405
+ this._visibility?.disconnect();
4406
+ if (this._ctx) {
4407
+ this._ctx.setTransform(1, 0, 0, 1, 0, 0);
4408
+ this._ctx.clearRect(0, 0, this.el.width, this.el.height);
4409
+ }
4410
+ this.el["Expressive_Particles"] = void 0;
4411
+ }
4412
+ _start() {
4413
+ if (this._frame) return;
4414
+ window.addEventListener("pointermove", this._onPointerMove, { passive: true });
4415
+ this._frame = requestAnimationFrame((now) => {
4416
+ this._last = now;
4417
+ this._draw(now);
4418
+ });
4419
+ }
4420
+ _stop() {
4421
+ cancelAnimationFrame(this._frame);
4422
+ this._frame = 0;
4423
+ window.removeEventListener("pointermove", this._onPointerMove);
4424
+ }
4425
+ // Matches the pixel buffer to the canvas box and scatters a new set.
4426
+ _reset() {
4427
+ this._width = this.el.clientWidth;
4428
+ this._height = this.el.clientHeight;
4429
+ this.el.width = Math.round(this._width * devicePixelRatio);
4430
+ this.el.height = Math.round(this._height * devicePixelRatio);
4431
+ this._particles = Array.from({ length: this.options.quantity }, () => this._particle());
4432
+ if (this._still) {
4433
+ for (const p of this._particles) p.alpha = p.targetAlpha;
4434
+ this._paint();
4435
+ }
4436
+ }
4437
+ _particle() {
4438
+ return {
4439
+ x: Math.floor(Math.random() * this._width),
4440
+ y: Math.floor(Math.random() * this._height),
4441
+ translateX: 0,
4442
+ translateY: 0,
4443
+ size: Math.floor(Math.random() * 2) + this.options.size,
4444
+ alpha: 0,
4445
+ targetAlpha: Math.round((Math.random() * 0.6 + 0.1) * 10) / 10,
4446
+ dx: (Math.random() - 0.5) * 0.1,
4447
+ dy: (Math.random() - 0.5) * 0.1,
4448
+ magnetism: 0.1 + Math.random() * 4
4449
+ };
4450
+ }
4451
+ _draw(now) {
4452
+ const step = Math.min((now - this._last) / (1e3 / 60), 4);
4453
+ this._last = now;
4454
+ const { staticity, ease, vx, vy } = this.options;
4455
+ const follow = Math.min(step / ease, 1);
4456
+ this._particles = this._particles.map((p) => {
4457
+ p.x += (p.dx + vx) * step;
4458
+ p.y += (p.dy + vy) * step;
4459
+ p.translateX += (this._mouse.x / (staticity / p.magnetism) - p.translateX) * follow;
4460
+ p.translateY += (this._mouse.y / (staticity / p.magnetism) - p.translateY) * follow;
4461
+ const x = p.x + p.translateX;
4462
+ const y = p.y + p.translateY;
4463
+ const edge = Math.min(x - p.size, this._width - x - p.size, y - p.size, this._height - y - p.size) / 20;
4464
+ p.alpha = edge > 1 ? Math.min(p.alpha + 0.02 * step, p.targetAlpha) : p.targetAlpha * edge;
4465
+ const gone = p.x < -p.size || p.x > this._width + p.size || p.y < -p.size || p.y > this._height + p.size;
4466
+ return gone ? this._particle() : p;
4467
+ });
4468
+ this._paint();
4469
+ this._frame = requestAnimationFrame((t) => this._draw(t));
4470
+ }
4471
+ _paint() {
4472
+ const ctx = this._ctx;
4473
+ const dpr = devicePixelRatio;
4474
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
4475
+ ctx.clearRect(0, 0, this.el.width, this.el.height);
4476
+ ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
4477
+ ctx.fillStyle = this.options.color || this._style.color;
4478
+ for (const p of this._particles) {
4479
+ if (p.alpha <= 0) continue;
4480
+ ctx.globalAlpha = p.alpha;
4481
+ ctx.beginPath();
4482
+ ctx.arc(p.x + p.translateX, p.y + p.translateY, p.size, 0, 2 * Math.PI);
4483
+ ctx.fill();
4484
+ }
4485
+ ctx.globalAlpha = 1;
4486
+ }
4487
+ };
4488
+
4489
+ // src/ts/components/commandPalette.ts
4490
+ var _defaults14 = {
4491
+ shortcut: "k",
4492
+ closeOnRun: true
4493
+ };
4494
+ var APPLE = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
4495
+ var fold = (text) => text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
4496
+ var CommandPalette = class _CommandPalette extends Component {
4497
+ constructor(el, options) {
4498
+ super(el, options, _CommandPalette);
4499
+ this._active = null;
4500
+ this._generatedIds = [];
4501
+ /** Wires and refilters commands the page adds or removes later. */
4502
+ this._observer = null;
4503
+ /** Opens the palette as a modal dialog with the search input focused. */
4504
+ this.open = () => {
4505
+ if (!this.el.open) this.el.showModal();
4506
+ this.input?.focus();
4507
+ };
4508
+ /** Closes the palette. The dialog returns focus to where it was. */
4509
+ this.close = () => {
4510
+ if (this.el.open) this.el.close();
4511
+ };
4512
+ this._handleInput = () => this._filter();
4513
+ this._handleKeydown = (e) => {
4514
+ if (e.key === Utils.keys.ARROW_DOWN || e.key === Utils.keys.ARROW_UP) {
4515
+ e.preventDefault();
4516
+ this._move(e.key === Utils.keys.ARROW_DOWN ? 1 : -1);
4517
+ } else if (e.key === Utils.keys.ENTER && !e.isComposing && e.keyCode !== 229 && this._active) {
4518
+ e.preventDefault();
4519
+ this._run(this._active);
4520
+ }
4521
+ };
4522
+ this._handleListClick = (e) => {
4523
+ const item = e.target.closest('[role="option"]');
4524
+ if (!item || !this.list.contains(item) || item.getAttribute("aria-disabled") === "true") return;
4525
+ const action = this._action(item);
4526
+ if (action && !action.contains(e.target)) {
4527
+ action.click();
4528
+ return;
4529
+ }
4530
+ if (this.options.closeOnRun) this.close();
4531
+ };
4532
+ this._handlePointerMove = (e) => {
4533
+ const item = e.target.closest('[role="option"]');
4534
+ if (item && this.list.contains(item) && item.getAttribute("aria-disabled") !== "true") this._setActive(item);
4535
+ };
4536
+ /**
4537
+ * Each opening starts from the full list. The close event is queued, so a
4538
+ * palette reopened straight away may already be open again; its new search
4539
+ * is left alone.
4540
+ */
4541
+ this._handleClose = () => {
4542
+ if (this.el.open) return;
4543
+ this.input.value = "";
4544
+ this._filter();
4545
+ };
4546
+ this._handleShortcut = (e) => {
4547
+ const { shortcut } = this.options;
4548
+ if (!shortcut || e.defaultPrevented || e.altKey || e.shiftKey) return;
4549
+ if (!(APPLE ? e.metaKey && !e.ctrlKey : e.ctrlKey && !e.metaKey)) return;
4550
+ const letter = shortcut.toLowerCase();
4551
+ const latin = /^[a-z]$/i.test(e.key);
4552
+ const hit = e.key.toLowerCase() === letter || !latin && /^[a-z]$/.test(letter) && e.code === `Key${letter.toUpperCase()}`;
4553
+ if (!hit) return;
4554
+ e.preventDefault();
4555
+ if (this.el.open) this.close();
4556
+ else this.open();
4557
+ };
4558
+ this.el["Expressive_CommandPalette"] = this;
4559
+ const shortcut = el.dataset.shortcut;
4560
+ this.options = {
4561
+ ..._CommandPalette.defaults,
4562
+ ...shortcut !== void 0 ? { shortcut: shortcut || null } : {},
4563
+ ...options
4564
+ };
4565
+ this.input = el.querySelector(":scope > input");
4566
+ this.list = el.querySelector(":scope > :is(ul, ol)");
4567
+ if (!this.input || !this.list) {
4568
+ console.error("CommandPalette needs an <input> and a <ul> as children of the dialog.");
4569
+ return;
4570
+ }
4571
+ this._setupAccessibility();
4572
+ this._filter();
4573
+ this._observer = new MutationObserver(() => this._filter(true));
4574
+ this._observer.observe(this.list, {
4575
+ childList: true,
4576
+ subtree: true,
4577
+ attributes: true,
4578
+ attributeFilter: ["hidden", "disabled"]
4579
+ });
4580
+ this.input.addEventListener("input", this._handleInput);
4581
+ this.input.addEventListener("keydown", this._handleKeydown);
4582
+ this.list.addEventListener("click", this._handleListClick);
4583
+ this.list.addEventListener("pointermove", this._handlePointerMove);
4584
+ this.el.addEventListener("close", this._handleClose);
4585
+ document.addEventListener("keydown", this._handleShortcut);
4586
+ }
4587
+ static get defaults() {
4588
+ return _defaults14;
4589
+ }
4590
+ /**
4591
+ * Initializes instances of CommandPalette.
4592
+ * @param els HTML elements.
4593
+ * @param options Component options.
4594
+ */
4595
+ static init(els, options = {}) {
4596
+ return super.init(els, options, _CommandPalette);
4597
+ }
4598
+ static getInstance(el) {
4599
+ return el["Expressive_CommandPalette"];
4600
+ }
4601
+ /** If the palette is open. */
4602
+ get isOpen() {
4603
+ return this.el.open;
4604
+ }
4605
+ destroy() {
4606
+ this._observer?.disconnect();
4607
+ document.removeEventListener("keydown", this._handleShortcut);
4608
+ this.el.removeEventListener("close", this._handleClose);
4609
+ if (this.input && this.list) {
4610
+ this.input.removeEventListener("input", this._handleInput);
4611
+ this.input.removeEventListener("keydown", this._handleKeydown);
4612
+ this.list.removeEventListener("click", this._handleListClick);
4613
+ this.list.removeEventListener("pointermove", this._handlePointerMove);
4614
+ for (const name of ["role", "aria-controls", "aria-expanded", "aria-autocomplete", "aria-activedescendant"]) {
4615
+ this.input.removeAttribute(name);
4616
+ }
4617
+ this.list.removeAttribute("role");
4618
+ for (const item of this._items()) {
4619
+ for (const name of ["role", "aria-selected", "aria-disabled", "data-filtered"]) item.removeAttribute(name);
4620
+ item.classList.remove("active");
4621
+ this._action(item)?.removeAttribute("tabindex");
4622
+ }
4623
+ for (const label of this._labels()) {
4624
+ label.removeAttribute("role");
4625
+ label.removeAttribute("data-filtered");
4626
+ }
4627
+ for (const el of this._generatedIds) el.removeAttribute("id");
4628
+ const empty = this._empty();
4629
+ if (empty) empty.hidden = false;
4630
+ }
4631
+ this.el["Expressive_CommandPalette"] = void 0;
4632
+ }
4633
+ _items() {
4634
+ return Array.from(this.list.children).filter(
4635
+ (child) => child instanceof HTMLElement && !child.classList.contains("label")
4636
+ );
4637
+ }
4638
+ _labels() {
4639
+ return Array.from(this.list.querySelectorAll(":scope > .label"));
4640
+ }
4641
+ _empty() {
4642
+ return this.el.querySelector(":scope > .command-palette-empty");
4643
+ }
4644
+ _action(item) {
4645
+ return item.querySelector(":scope > :is(a[href], button)");
4646
+ }
4647
+ _ensureId(el, prefix) {
4648
+ if (!el.id) {
4649
+ el.id = `${prefix}-${Utils.guid()}`;
4650
+ this._generatedIds.push(el);
4651
+ }
4652
+ return el.id;
4653
+ }
4654
+ _setupAccessibility() {
4655
+ this.list.setAttribute("role", "listbox");
4656
+ this.input.setAttribute("role", "combobox");
4657
+ this.input.setAttribute("aria-controls", this._ensureId(this.list, "command-palette-list"));
4658
+ this.input.setAttribute("aria-expanded", "true");
4659
+ this.input.setAttribute("aria-autocomplete", "list");
4660
+ }
4661
+ /** Every command, including ones added since the last pass, is an option. */
4662
+ _wireItems() {
4663
+ for (const item of this._items()) {
4664
+ if (item.getAttribute("role") === "option") continue;
4665
+ item.setAttribute("role", "option");
4666
+ this._ensureId(item, "command-palette-option");
4667
+ this._action(item)?.setAttribute("tabindex", "-1");
4668
+ }
4669
+ for (const label of this._labels()) label.setAttribute("role", "none");
4670
+ }
4671
+ /** Commands on screen: neither hidden by the page nor filtered out. */
4672
+ _shown() {
4673
+ return this._items().filter((item) => !item.hidden && !item.hasAttribute("data-filtered"));
4674
+ }
4675
+ /** Shown commands the arrows and Enter can reach: disabled ones are skipped. */
4676
+ _visible() {
4677
+ return this._shown().filter((item) => item.getAttribute("aria-disabled") !== "true");
4678
+ }
4679
+ /** A disabled link or button cannot run, so its option says so. */
4680
+ _isDisabled(item) {
4681
+ const action = this._action(item);
4682
+ return !!action && (action.matches(":disabled") || action.getAttribute("aria-disabled") === "true");
4683
+ }
4684
+ _setActive(item) {
4685
+ if (this._active === item) return;
4686
+ this._active?.classList.remove("active");
4687
+ this._active?.removeAttribute("aria-selected");
4688
+ this._active = item;
4689
+ if (item) {
4690
+ item.classList.add("active");
4691
+ item.setAttribute("aria-selected", "true");
4692
+ this.input.setAttribute("aria-activedescendant", item.id);
4693
+ item.scrollIntoView({ block: "nearest" });
4694
+ } else {
4695
+ this.input.removeAttribute("aria-activedescendant");
4696
+ }
4697
+ }
4698
+ /**
4699
+ * Shows the commands matching the query. Filtering marks rows with
4700
+ * data-filtered rather than hidden, so a command the page hides stays
4701
+ * hidden. `keepActive` holds the active command while it is still
4702
+ * reachable, for list changes the user did not make.
4703
+ */
4704
+ _filter(keepActive = false) {
4705
+ this._wireItems();
4706
+ const terms = fold(this.input.value).split(/\s+/).filter(Boolean);
4707
+ for (const item of this._items()) {
4708
+ const keywords = item.dataset.keywords ?? this._action(item)?.dataset.keywords ?? "";
4709
+ const text = fold(`${item.textContent} ${keywords}`);
4710
+ item.toggleAttribute("data-filtered", !terms.every((term) => text.includes(term)));
4711
+ if (this._isDisabled(item)) item.setAttribute("aria-disabled", "true");
4712
+ else item.removeAttribute("aria-disabled");
4713
+ }
4714
+ for (const label of this._labels()) {
4715
+ let next = label.nextElementSibling;
4716
+ let any = false;
4717
+ while (next && !next.classList.contains("label")) {
4718
+ if (!next.hidden && !next.hasAttribute("data-filtered")) any = true;
4719
+ next = next.nextElementSibling;
4720
+ }
4721
+ label.toggleAttribute("data-filtered", !any);
4722
+ }
4723
+ const visible = this._visible();
4724
+ const empty = this._empty();
4725
+ if (empty) empty.hidden = this._shown().length > 0;
4726
+ this._setActive(keepActive && visible.includes(this._active) ? this._active : visible[0] ?? null);
4727
+ }
4728
+ _move(step) {
4729
+ const visible = this._visible();
4730
+ if (!visible.length) return;
4731
+ const index = visible.indexOf(this._active);
4732
+ this._setActive(visible[(index + step + visible.length) % visible.length]);
4733
+ }
4734
+ /** Runs a command by clicking its link or button; the click handler closes. */
4735
+ _run(item) {
4736
+ (this._action(item) ?? item).click();
4737
+ }
4738
+ };
4739
+
4740
+ // src/ts/components/questionnaire.ts
4741
+ var _defaults15 = {
4742
+ progressLabel: "Question {current} of {total}",
4743
+ requiredMessage: "Choose an answer to continue."
4744
+ };
4745
+ var NAVIGATION = ["previous", "skip", "next"];
4746
+ var Questionnaire = class _Questionnaire extends Component {
4747
+ constructor(el, options) {
4748
+ super(el, options, _Questionnaire);
4749
+ this._active = null;
4750
+ this._generated = [];
4751
+ this._generatedIds = [];
4752
+ /** Set while one submission's invalid events arrive, so only the first opens. */
4753
+ this._reporting = false;
4754
+ /** Shows the question at `index` without checking the current one. */
4755
+ this.goTo = (index) => {
4756
+ const item = this.items[index];
4757
+ if (!item) return;
4758
+ this._active = item;
4759
+ this._render();
4760
+ const controls = this._controls(item).filter((control) => !control.disabled);
4761
+ (controls.find((control) => control.checked) ?? controls[0])?.focus();
4762
+ };
4763
+ /** Checks the shown question and moves to the next one if it is answered. */
4764
+ this.next = () => {
4765
+ if (this._validate(this._active)) this.goTo(this.index + 1);
4766
+ };
4767
+ /** Moves to the previous question. */
4768
+ this.previous = () => this.goTo(this.index - 1);
4769
+ /** Clears the answer to the shown question and moves to the next one. */
4770
+ this.skip = () => {
4771
+ if (!this._active) return;
4772
+ for (const control of this._controls(this._active)) {
4773
+ const isChoice = control instanceof HTMLInputElement && (control.type === "radio" || control.type === "checkbox");
4774
+ if (isChoice ? !control.checked : !control.value) continue;
4775
+ if (isChoice) control.checked = false;
4776
+ else control.value = "";
4777
+ control.dispatchEvent(new Event("change", { bubbles: true }));
4778
+ }
4779
+ this.goTo(this.index + 1);
4780
+ };
4781
+ this._handleClick = (e) => {
4782
+ const button = e.target.closest?.("button");
4783
+ if (!button || !this._buttons().includes(button)) return;
4784
+ if (NAVIGATION.includes(button.value)) {
4785
+ e.preventDefault();
4786
+ this[button.value]();
4787
+ return;
4788
+ }
4789
+ this._syncRequired();
4790
+ };
4791
+ // Enter in a field moves on instead of submitting the form early.
4792
+ this._handleKeydown = (e) => {
4793
+ if (e.key !== "Enter" || e.defaultPrevented || e.isComposing) return;
4794
+ if (!(e.target instanceof HTMLInputElement) || this.index >= this.items.length - 1) return;
4795
+ e.preventDefault();
4796
+ this.next();
4797
+ };
4798
+ // A submission found an unanswered question, possibly a hidden one. Open the
4799
+ // first instead of letting the browser report a control it cannot show.
4800
+ // One submission fires its invalid events in a single task.
4801
+ this._handleInvalid = (e) => {
4802
+ const item = this.items.find((item2) => item2.contains(e.target));
4803
+ if (!item) return;
4804
+ e.preventDefault();
4805
+ if (this._reporting) return;
4806
+ this._reporting = true;
4807
+ setTimeout(() => this._reporting = false);
4808
+ this._active = item;
4809
+ this._render();
4810
+ this._validate(item);
4811
+ };
4812
+ // An answer clears the question's error.
4813
+ this._handleInput = (e) => {
4814
+ this._syncRequired();
4815
+ const item = e.target.closest?.("fieldset");
4816
+ if (item instanceof HTMLFieldSetElement && item.classList.contains("invalid")) this._setInvalid(item, false);
4817
+ };
4818
+ this.el["Expressive_Questionnaire"] = this;
4819
+ const progressLabel = el.dataset.progressLabel;
4820
+ this.options = {
4821
+ ..._Questionnaire.defaults,
4822
+ ...progressLabel ? { progressLabel } : {},
4823
+ ...options
4824
+ };
4825
+ this._progress = document.createElement("div");
4826
+ this._progress.className = "questionnaire-progress";
4827
+ this._progressText = document.createElement("span");
4828
+ this._progressText.id = `questionnaire-progress-${Utils.guid()}`;
4829
+ this._progressBar = document.createElement("progress");
4830
+ this._progressBar.className = "progress";
4831
+ this._progressBar.setAttribute("aria-labelledby", this._progressText.id);
4832
+ this._progress.append(this._progressText, this._progressBar);
4833
+ this.el.prepend(this._progress);
4834
+ for (const item of this._fieldsets()) {
4835
+ const ids = Array.from(item.querySelectorAll(":scope > p:not(.questionnaire-error)")).map((p) => this._ensureId(p, "questionnaire-description"));
4836
+ if (ids.length && !item.hasAttribute("aria-describedby")) {
4837
+ item.setAttribute("aria-describedby", ids.join(" "));
4838
+ this._generatedIds.push(item);
4839
+ }
4840
+ }
4841
+ this._active = this.items[0] ?? null;
4842
+ this._syncRequired();
4843
+ this._render();
4844
+ this._observer = new MutationObserver(() => {
4845
+ this._syncRequired();
4846
+ this._render();
4847
+ });
4848
+ this._observer.observe(this.el, { attributes: true, subtree: true, attributeFilter: ["disabled"] });
4849
+ this.el.addEventListener("click", this._handleClick);
4850
+ this.el.addEventListener("keydown", this._handleKeydown);
4851
+ this.el.addEventListener("input", this._handleInput);
4852
+ this.el.addEventListener("invalid", this._handleInvalid, true);
4853
+ }
4854
+ static get defaults() {
4855
+ return _defaults15;
4856
+ }
4857
+ /**
4858
+ * Initializes instances of Questionnaire.
4859
+ * @param els HTML elements.
4860
+ * @param options Component options.
4861
+ */
4862
+ static init(els, options = {}) {
4863
+ return super.init(els, options, _Questionnaire);
4864
+ }
4865
+ static getInstance(el) {
4866
+ return el["Expressive_Questionnaire"];
4867
+ }
4868
+ /** The questions that apply: every fieldset child that is not disabled. */
4869
+ get items() {
4870
+ return this._fieldsets().filter((item) => !item.disabled);
4871
+ }
4872
+ /** Position of the shown question in `items`. */
4873
+ get index() {
4874
+ return this.items.indexOf(this._active);
4875
+ }
4876
+ destroy() {
4877
+ this._observer.disconnect();
4878
+ this.el.removeEventListener("click", this._handleClick);
4879
+ this.el.removeEventListener("keydown", this._handleKeydown);
4880
+ this.el.removeEventListener("input", this._handleInput);
4881
+ this.el.removeEventListener("invalid", this._handleInvalid, true);
4882
+ for (const item of this._fieldsets()) {
4883
+ if (item.hasAttribute("data-required")) for (const control of this._controls(item)) control.setCustomValidity("");
4884
+ item.hidden = false;
4885
+ this._setInvalid(item, false);
4886
+ }
4887
+ for (const button of this._buttons()) button.hidden = false;
4888
+ this._progress.remove();
4889
+ for (const el of this._generated) el.remove();
4890
+ for (const el of this._generatedIds) {
4891
+ el.removeAttribute(el instanceof HTMLFieldSetElement ? "aria-describedby" : "id");
4892
+ }
4893
+ this.el["Expressive_Questionnaire"] = void 0;
4894
+ }
4895
+ _fieldsets() {
4896
+ return Array.from(this.el.querySelectorAll(":scope > fieldset"));
4897
+ }
4898
+ /** The footer's navigation and submit buttons. */
4899
+ _buttons() {
4900
+ return Array.from(this.el.querySelectorAll(":scope > footer > button")).filter(
4901
+ (button) => NAVIGATION.includes(button.value) || button.type === "submit"
4902
+ );
4903
+ }
4904
+ _controls(item) {
4905
+ return Array.from(item.elements).filter(
4906
+ (el) => el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement || el instanceof HTMLInputElement && el.type !== "button" && el.type !== "submit"
4907
+ );
4908
+ }
4909
+ _required(item) {
4910
+ return item.hasAttribute("data-required") || this._controls(item).some((control) => control.required);
4911
+ }
4912
+ _ensureId(el, prefix) {
4913
+ if (!el.id) {
4914
+ el.id = `${prefix}-${Utils.guid()}`;
4915
+ this._generatedIds.push(el);
4916
+ }
4917
+ return el.id;
4918
+ }
4919
+ _render() {
4920
+ const items = this.items;
4921
+ if (!items.includes(this._active)) {
4922
+ const all = this._fieldsets();
4923
+ const after = all.slice(all.indexOf(this._active) + 1).find((item) => !item.disabled);
4924
+ this._active = after ?? items[items.length - 1] ?? null;
4925
+ }
4926
+ const index = items.indexOf(this._active);
4927
+ const last = index === items.length - 1;
4928
+ for (const item of this._fieldsets()) item.hidden = item !== this._active;
4929
+ for (const button of this._buttons()) {
4930
+ if (button.value === "previous") button.hidden = index <= 0;
4931
+ else if (button.value === "skip") button.hidden = last || !this._active || this._required(this._active);
4932
+ else if (button.value === "next") button.hidden = last;
4933
+ else button.hidden = !last;
4934
+ }
4935
+ this._progressText.textContent = this.options.progressLabel.replace("{current}", String(index + 1)).replace("{total}", String(items.length));
4936
+ this._progressBar.max = Math.max(items.length, 1);
4937
+ this._progressBar.value = index + 1;
4938
+ }
4939
+ /**
4940
+ * Puts "choose at least one" on the first enabled checkbox of each
4941
+ * data-required group with none checked, so the browser's validation, and
4942
+ * every way of submitting, sees it. A disabled checkbox is not sent, so it
4943
+ * does not count.
4944
+ */
4945
+ _syncRequired() {
4946
+ for (const item of this._fieldsets()) {
4947
+ if (!item.hasAttribute("data-required")) continue;
4948
+ const boxes = this._controls(item).filter(
4949
+ (control) => control instanceof HTMLInputElement && control.type === "checkbox"
4950
+ );
4951
+ const enabled = boxes.filter((box) => !box.disabled);
4952
+ for (const box of boxes) box.setCustomValidity("");
4953
+ if (enabled.length && !enabled.some((box) => box.checked)) {
4954
+ const error = item.querySelector(":scope > .questionnaire-error:not([data-generated])");
4955
+ enabled[0].setCustomValidity(error?.textContent.trim() || this.options.requiredMessage);
4956
+ }
4957
+ }
4958
+ }
4959
+ /** The first control in the question that still needs an answer. */
4960
+ _invalidControl(item) {
4961
+ return this._controls(item).find((control) => control.willValidate && !control.validity.valid);
4962
+ }
4963
+ /** Shows the question's error and returns false when it needs an answer. */
4964
+ _validate(item) {
4965
+ this._syncRequired();
4966
+ const invalid = item && this._invalidControl(item);
4967
+ if (!invalid) return true;
4968
+ let error = item.querySelector(":scope > .questionnaire-error");
4969
+ if (!error) {
4970
+ error = document.createElement("p");
4971
+ error.className = "questionnaire-error";
4972
+ error.dataset.generated = "";
4973
+ error.textContent = invalid.validationMessage || this.options.requiredMessage;
4974
+ item.append(error);
4975
+ this._generated.push(error);
4976
+ }
4977
+ this._setInvalid(item, true);
4978
+ invalid.focus();
4979
+ return false;
4980
+ }
4981
+ _setInvalid(item, invalid) {
4982
+ item.classList.toggle("invalid", invalid);
4983
+ const error = item.querySelector(":scope > .questionnaire-error");
4984
+ const described = (item.getAttribute("aria-describedby") ?? "").split(" ").filter((id) => id && id !== error?.id);
4985
+ if (invalid && error) described.push(this._ensureId(error, "questionnaire-error"));
4986
+ if (described.length) item.setAttribute("aria-describedby", described.join(" "));
4987
+ else item.removeAttribute("aria-describedby");
4988
+ for (const control of this._controls(item)) {
4989
+ if (invalid) control.setAttribute("aria-invalid", "true");
4990
+ else control.removeAttribute("aria-invalid");
4991
+ }
4992
+ if (!invalid && this._generated.includes(error)) {
4993
+ error.remove();
4994
+ this._generated.splice(this._generated.indexOf(error), 1);
4995
+ }
4996
+ }
4997
+ };
4998
+
4999
+ // src/ts/components/lineChart.ts
5000
+ var _defaults16 = {
5001
+ min: null,
5002
+ max: null
5003
+ };
5004
+ var SVG = "http://www.w3.org/2000/svg";
5005
+ var COLORS = 4;
5006
+ function svgNode(name, attributes = {}) {
5007
+ const element = document.createElementNS(SVG, name);
5008
+ for (const key in attributes) element.setAttribute(key, attributes[key]);
5009
+ return element;
5010
+ }
5011
+ var _gradientId = 0;
5012
+ function cellValue(cell) {
5013
+ if (!cell) return null;
5014
+ const raw = cell.dataset.value ?? cell.textContent;
5015
+ const text = raw.replace(/\u2212/g, "-").replace(/[\s%\p{Sc}]/gu, "");
5016
+ if (!/^[+-]?(\d{1,3}(,\d{3})+|\d+)?(\.\d+)?(e[+-]?\d+)?$/i.test(text) || !/\d/.test(text)) return null;
5017
+ return parseFloat(text.replace(/,/g, ""));
5018
+ }
5019
+ function monotonePath(points) {
5020
+ const n = points.length;
5021
+ if (n === 0) return "";
5022
+ if (n === 1) return `M${points[0][0]},${points[0][1]}h0`;
5023
+ const slopes = [];
5024
+ for (let i = 0; i < n - 1; i++) {
5025
+ slopes.push((points[i + 1][1] - points[i][1]) / (points[i + 1][0] - points[i][0]));
5026
+ }
5027
+ const tangents = points.map((_, i) => {
5028
+ if (i === 0) return slopes[0];
5029
+ if (i === n - 1) return slopes[n - 2];
5030
+ const [a, b] = [slopes[i - 1], slopes[i]];
5031
+ return a * b <= 0 ? 0 : 2 / (1 / a + 1 / b);
5032
+ });
5033
+ let d = `M${points[0][0]},${points[0][1]}`;
5034
+ for (let i = 0; i < n - 1; i++) {
5035
+ const [[x0, y0], [x1, y1]] = [points[i], points[i + 1]];
5036
+ const h = (x1 - x0) / 3;
5037
+ d += `C${x0 + h},${y0 + tangents[i] * h} ${x1 - h},${y1 - tangents[i + 1] * h} ${x1},${y1}`;
5038
+ }
5039
+ return d;
5040
+ }
5041
+ var LineChart = class extends Component {
5042
+ constructor(el, options) {
5043
+ super(el, options, new.target);
5044
+ /** The values' x positions and series, read from the table. */
5045
+ this.labels = [];
5046
+ this.series = [];
5047
+ /** The highlighted row, or -1. */
5048
+ this.activeIndex = -1;
5049
+ this._generated = [];
5050
+ /** The table's header cells and body rows, as read. */
5051
+ this._header = [];
5052
+ this._rows = [];
5053
+ /**
5054
+ * Whether the rows run right to left: a horizontal row axis in a
5055
+ * right-to-left context, like the labels. Bar chart rows keep their order.
5056
+ */
5057
+ this._reversed = false;
5058
+ this._onPointerMove = (e) => {
5059
+ this.show(this._indexAt(e));
5060
+ };
5061
+ this._onFocus = () => {
5062
+ if (this.activeIndex < 0 && this._plot.matches(":focus-visible")) this.show(0);
5063
+ };
5064
+ this._onLeave = (e) => {
5065
+ if (e.pointerType === "touch" || this._plot.matches(":focus-visible")) return;
5066
+ this.show(-1);
5067
+ };
5068
+ /** The browser took the touch to scroll the page. */
5069
+ this._onCancel = () => {
5070
+ this.show(-1);
5071
+ };
5072
+ this._onKeyDown = (e) => {
5073
+ const next = this._keyTarget(e.key);
5074
+ if (next === void 0) return;
5075
+ e.preventDefault();
5076
+ this.show(next);
5077
+ };
5078
+ this.el[this._key] = this;
5079
+ this.options = {
5080
+ ...new.target.defaults,
5081
+ ...options
5082
+ };
5083
+ this._read();
5084
+ if (!this.labels.length || !this.series.length) return;
5085
+ this._reversed = !this._horizontal && getComputedStyle(this.el).direction === "rtl";
5086
+ this._render();
5087
+ if (!this.el.classList.contains("sparkline")) {
5088
+ this._plot.tabIndex = 0;
5089
+ this._plot.addEventListener("pointerdown", this._onPointerMove);
5090
+ this._plot.addEventListener("pointermove", this._onPointerMove);
5091
+ this._plot.addEventListener("pointerleave", this._onLeave);
5092
+ this._plot.addEventListener("pointercancel", this._onCancel);
5093
+ this._plot.addEventListener("keydown", this._onKeyDown);
5094
+ this._plot.addEventListener("focus", this._onFocus);
5095
+ this._plot.addEventListener("blur", this._onLeave);
5096
+ }
5097
+ }
5098
+ /** The class prefix of the generated parts; ColumnChart reuses this class. */
5099
+ get _type() {
5100
+ return "line-chart";
5101
+ }
5102
+ /** Whether the y axis always includes zero, as columns grow from it. */
5103
+ get _fromZero() {
5104
+ return false;
5105
+ }
5106
+ /**
5107
+ * Whether the rows run down the plot and the values across it, as in a
5108
+ * bar chart. The rows then keep their order in a right-to-left context.
5109
+ */
5110
+ get _horizontal() {
5111
+ return false;
5112
+ }
5113
+ /**
5114
+ * Whether the rows go round a circle, as in a pie or radar chart. The
5115
+ * arrows then step round it, wrapping from the last row to the first.
5116
+ */
5117
+ get _circular() {
5118
+ return false;
5119
+ }
5120
+ /** Whether the cursor marks each series with a point. */
5121
+ get _points() {
5122
+ return true;
5123
+ }
5124
+ get _key() {
5125
+ return "Expressive_LineChart";
5126
+ }
5127
+ static get defaults() {
5128
+ return _defaults16;
5129
+ }
5130
+ static init(els, options = {}) {
5131
+ return super.init(els, options, this);
5132
+ }
5133
+ static getInstance(el) {
5134
+ return el["Expressive_LineChart"];
5135
+ }
5136
+ destroy() {
5137
+ this._generated.forEach((node) => node.remove());
5138
+ this._generated = [];
5139
+ this.el[this._key] = void 0;
5140
+ }
5141
+ /**
5142
+ * Highlights one row: moves the cursor and fills the tooltip.
5143
+ * Pass -1 to hide them. Does nothing for a chart without data.
5144
+ */
5145
+ show(index) {
5146
+ if (!this._tooltip || index === this.activeIndex) return;
5147
+ this.activeIndex = index;
5148
+ const hidden = index < 0 || index >= this.labels.length;
5149
+ this._cursor.hidden = this._tooltip.hidden = hidden;
5150
+ if (hidden) return;
5151
+ this._cursor.replaceChildren();
5152
+ const title = document.createElement("strong");
5153
+ title.textContent = this.labels[index];
5154
+ this._tooltip.replaceChildren(title);
5155
+ this.series.forEach((series, i) => {
5156
+ const value = series.values[index];
5157
+ if (value === null) return;
5158
+ const color = String(i % COLORS + 1);
5159
+ const top = series.tops[index];
5160
+ if (top !== null && this._points) {
5161
+ const dot = document.createElement("span");
5162
+ dot.className = series.className;
5163
+ dot.dataset.series = color;
5164
+ dot.style.top = `${series.y(top)}%`;
5165
+ this._cursor.append(dot);
5166
+ }
5167
+ const name = document.createElement("span");
5168
+ name.dataset.series = color;
5169
+ name.textContent = series.name;
5170
+ const text = document.createElement("span");
5171
+ text.textContent = series.text[index];
5172
+ this._tooltip.append(name, text);
5173
+ });
5174
+ this._place(index);
5175
+ }
5176
+ /**
5177
+ * Moves the filled cursor and tooltip to a row: beside its band, with the
5178
+ * tooltip opening back over the plot past the middle.
5179
+ */
5180
+ _place(index) {
5181
+ const x = this._x(index);
5182
+ const side = this._horizontal ? "top" : "left";
5183
+ this._cursor.style[side] = this._tooltip.style[side] = `${x}%`;
5184
+ this._tooltip.classList.toggle("end", x > 50);
5185
+ }
5186
+ _read() {
5187
+ const table = this.el.querySelector(":scope > table");
5188
+ if (!table) return;
5189
+ const headerRow = table.tHead?.rows[0] ?? table.rows[0];
5190
+ const header = Array.from(headerRow?.cells ?? []);
5191
+ const rows = Array.from(table.tBodies).flatMap((body) => Array.from(body.rows)).filter((row) => row !== headerRow);
5192
+ [this._header, this._rows] = [header, rows];
5193
+ this.labels = rows.map((row) => row.cells[0]?.textContent.trim() ?? "");
5194
+ this.series = header.slice(1).map((cell, i) => {
5195
+ const values = rows.map((row) => cellValue(row.cells[i + 1]));
5196
+ return {
5197
+ name: cell.textContent.trim(),
5198
+ className: cell.className,
5199
+ text: rows.map((row) => row.cells[i + 1]?.textContent.trim() ?? ""),
5200
+ values,
5201
+ tops: values,
5202
+ y: void 0
5203
+ };
5204
+ });
5205
+ const stacked = this.el.classList.contains("stacked");
5206
+ if (stacked) {
5207
+ const totals = this.labels.map(() => 0);
5208
+ for (const series of this.series) {
5209
+ series.base = [...totals];
5210
+ series.tops = series.values.map((value, i) => totals[i] = value === null || totals[i] === null ? null : totals[i] + value);
5211
+ }
5212
+ }
5213
+ if (this.series.every((series) => series.tops.every((value) => value === null))) this.series = [];
5214
+ const option = (name) => this._number(name) ?? this.options[name];
5215
+ this._y = this._scale(this.series, option("min"), option("max"));
5216
+ }
5217
+ /** A number from one of the chart's data attributes, or null. */
5218
+ _number(name) {
5219
+ const value = parseFloat(this.el.dataset[name] ?? "");
5220
+ return Number.isFinite(value) ? value : null;
5221
+ }
5222
+ /**
5223
+ * Fits a y scale to the series' tops, between `min` and `max` where they
5224
+ * are set, and gives it to each series.
5225
+ */
5226
+ _scale(series, min, max, fromZero = this._fromZero) {
5227
+ const stacked = this.el.classList.contains("stacked");
5228
+ let [low, high] = [Infinity, -Infinity];
5229
+ for (const { tops } of series) {
5230
+ for (const value of tops) {
5231
+ if (value === null) continue;
5232
+ low = Math.min(low, value);
5233
+ high = Math.max(high, value);
5234
+ }
5235
+ }
5236
+ if (low > high) low = high = 0;
5237
+ if (fromZero) {
5238
+ const [bottom, top] = [Math.min(0, low), Math.max(0, high)];
5239
+ const room = (top - (min ?? bottom)) * 0.1 || 1;
5240
+ min ?? (min = bottom < 0 ? bottom - room : 0);
5241
+ max ?? (max = top + room);
5242
+ } else {
5243
+ const pad = (high - low) * 0.1 || 1;
5244
+ min ?? (min = stacked ? Math.min(0, low) : low - pad);
5245
+ max ?? (max = high + (stacked ? (high - min) * 0.1 || 1 : pad));
5246
+ }
5247
+ const y = (value) => 100 - (value - min) / (max - min || 1) * 100;
5248
+ for (const each of series) each.y = y;
5249
+ return y;
5250
+ }
5251
+ /** Each row sits in the middle of an equal band, beside its label. */
5252
+ _x(index) {
5253
+ const x = (index + 0.5) / this.labels.length * 100;
5254
+ return this._reversed ? 100 - x : x;
5255
+ }
5256
+ _render() {
5257
+ const type = this._type;
5258
+ this._plot = document.createElement("div");
5259
+ this._plot.className = `${type}-plot`;
5260
+ const caption = this.el.querySelector(":scope > figcaption")?.textContent.trim();
5261
+ this._plot.setAttribute("role", "group");
5262
+ this._plot.setAttribute("aria-roledescription", type.replace(/-/g, " "));
5263
+ if (caption) this._plot.setAttribute("aria-label", caption);
5264
+ this._cursor = document.createElement("div");
5265
+ this._cursor.className = `${type}-cursor`;
5266
+ this._cursor.hidden = true;
5267
+ this._tooltip = document.createElement("div");
5268
+ this._tooltip.className = `${type}-tooltip`;
5269
+ this._tooltip.hidden = true;
5270
+ this._tooltip.setAttribute("aria-live", "polite");
5271
+ this._plot.append(this._draw(), this._cursor, this._tooltip);
5272
+ this._generated = [this._plot, ...this._guides()];
5273
+ this.el.append(...this._generated);
5274
+ }
5275
+ /**
5276
+ * The <tfoot> row as a total to show over the plot, its value over its
5277
+ * label, or null without one.
5278
+ */
5279
+ _total(className) {
5280
+ const footer = this.el.querySelector(":scope > table")?.tFoot?.rows[0];
5281
+ if (!footer) return null;
5282
+ const total = document.createElement("div");
5283
+ total.className = className;
5284
+ const [label, value] = [document.createElement("span"), document.createElement("strong")];
5285
+ label.textContent = footer.cells[0]?.textContent.trim() ?? "";
5286
+ value.textContent = footer.cells[1]?.textContent.trim() ?? "";
5287
+ total.append(value, label);
5288
+ return total;
5289
+ }
5290
+ /** The parts below the plot: the row labels and the legend. */
5291
+ _guides() {
5292
+ const labels = document.createElement("ol");
5293
+ labels.className = `${this._type}-labels`;
5294
+ labels.setAttribute("aria-hidden", "true");
5295
+ for (const text of this.labels) {
5296
+ const li = document.createElement("li");
5297
+ li.textContent = text;
5298
+ labels.append(li);
5299
+ }
5300
+ return [labels, ...this._legend()];
5301
+ }
5302
+ /** The series legend, for more than one series. */
5303
+ _legend() {
5304
+ if (this.series.length < 2) return [];
5305
+ const legend = document.createElement("ul");
5306
+ legend.className = `${this._type}-legend`;
5307
+ legend.setAttribute("aria-hidden", "true");
5308
+ legend.append(...this.series.map((series, i) => {
5309
+ const li = document.createElement("li");
5310
+ li.dataset.series = String(i % COLORS + 1);
5311
+ li.textContent = series.name;
5312
+ return li;
5313
+ }));
5314
+ return [legend];
5315
+ }
5316
+ /** Draws the series, which fill the plot. */
5317
+ _draw(drawn = this.series) {
5318
+ const svg = document.createElementNS(SVG, "svg");
5319
+ svg.setAttribute("viewBox", "0 0 100 100");
5320
+ svg.setAttribute("preserveAspectRatio", "none");
5321
+ svg.setAttribute("aria-hidden", "true");
5322
+ svg.setAttribute("focusable", "false");
5323
+ for (const series of drawn) {
5324
+ const group = document.createElementNS(SVG, "g");
5325
+ group.setAttribute("class", `${this._type}-series ${series.className}`.trim());
5326
+ group.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
5327
+ const runs = [[]];
5328
+ series.tops.forEach((value, index) => {
5329
+ if (value === null) runs.push([]);
5330
+ else runs[runs.length - 1].push(index);
5331
+ });
5332
+ const filled = runs.filter((run) => run.length);
5333
+ const points = (run, values) => run.map((index) => [this._x(index), series.y(values[index])]);
5334
+ const tops = filled.map((run) => monotonePath(points(run, series.tops)));
5335
+ const id = `line-chart-gradient-${++_gradientId}`;
5336
+ const gradient = document.createElementNS(SVG, "linearGradient");
5337
+ gradient.id = id;
5338
+ gradient.setAttribute("x2", "0");
5339
+ gradient.setAttribute("y2", "1");
5340
+ gradient.append(document.createElementNS(SVG, "stop"), document.createElementNS(SVG, "stop"));
5341
+ gradient.lastElementChild.setAttribute("offset", "1");
5342
+ const area = document.createElementNS(SVG, "path");
5343
+ area.setAttribute("class", `${this._type}-area`);
5344
+ area.setAttribute("fill", `url(#${id})`);
5345
+ area.setAttribute("d", filled.map((run, r) => {
5346
+ if (!series.base) return `${tops[r]}V100H${this._x(run[0])}Z`;
5347
+ const base = points(run, series.base).map(([x, y]) => [x, Math.min(100, y)]);
5348
+ return `${tops[r]}L${monotonePath(base.reverse()).slice(1)}Z`;
5349
+ }).join(""));
5350
+ const line = document.createElementNS(SVG, "path");
5351
+ line.setAttribute("class", `${this._type}-line`);
5352
+ line.setAttribute("d", tops.join(""));
5353
+ group.append(gradient, area, line);
5354
+ svg.append(group);
5355
+ }
5356
+ return svg;
5357
+ }
5358
+ /** The row under the pointer, or -1. */
5359
+ _indexAt(e) {
5360
+ const box = this._plot.getBoundingClientRect();
5361
+ const fraction = this._horizontal ? (e.clientY - box.top) / box.height : (e.clientX - box.left) / box.width;
5362
+ const index = Math.floor((this._reversed ? 1 - fraction : fraction) * this.labels.length);
5363
+ return Math.min(this.labels.length - 1, Math.max(0, index));
5364
+ }
5365
+ /**
5366
+ * For a circular chart, the pointer's position from the plot's middle: a
5367
+ * fraction of a turn clockwise from the top, and its distance as a share
5368
+ * of the plot's width.
5369
+ */
5370
+ _polarAt(e) {
5371
+ const box = this._plot.getBoundingClientRect();
5372
+ const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.height / 2];
5373
+ return [(Math.atan2(x, -y) / (2 * Math.PI) + 1) % 1, Math.hypot(x, y) / box.width];
5374
+ }
5375
+ /** The keys that move to the next row and to the one before. */
5376
+ get _arrows() {
5377
+ if (this._circular) return [["ArrowRight", "ArrowDown"], ["ArrowLeft", "ArrowUp"]];
5378
+ if (this._horizontal) return [["ArrowDown"], ["ArrowUp"]];
5379
+ return this._reversed ? [["ArrowLeft"], ["ArrowRight"]] : [["ArrowRight"], ["ArrowLeft"]];
5380
+ }
5381
+ /** The index a key moves to, or undefined for a key the chart ignores. */
5382
+ _keyTarget(key) {
5383
+ const last = this.labels.length - 1;
5384
+ const [forward, back] = this._arrows;
5385
+ const step = (by) => {
5386
+ if (!this._circular) return Math.min(last, Math.max(0, this.activeIndex + by));
5387
+ if (this.activeIndex < 0) return by > 0 ? 0 : last;
5388
+ return (this.activeIndex + by + last + 1) % (last + 1);
5389
+ };
5390
+ if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
5391
+ return forward.includes(key) ? step(1) : back.includes(key) ? step(-1) : { Home: 0, End: last }[key];
5392
+ }
5393
+ };
5394
+
5395
+ // src/ts/components/columnChart.ts
5396
+ var ColumnChart = class extends LineChart {
5397
+ get _type() {
5398
+ return "column-chart";
5399
+ }
5400
+ get _key() {
5401
+ return "Expressive_ColumnChart";
5402
+ }
5403
+ get _fromZero() {
5404
+ return true;
5405
+ }
5406
+ get _points() {
5407
+ return false;
5408
+ }
5409
+ static init(els, options = {}) {
5410
+ return super.init(els, options);
5411
+ }
5412
+ static getInstance(el) {
5413
+ return el["Expressive_ColumnChart"];
5414
+ }
5415
+ _draw(drawn = this.series) {
5416
+ const columns = document.createElement("div");
5417
+ columns.className = "column-chart-columns";
5418
+ columns.setAttribute("aria-hidden", "true");
5419
+ const clamp = (value, y = drawn[0]?.y ?? this._y) => Math.min(100, Math.max(0, y(value)));
5420
+ columns.style.setProperty("--zero", `${clamp(0)}%`);
5421
+ this._plot.style.setProperty("--rows", String(this.labels.length));
5422
+ this.labels.forEach((_, index) => {
5423
+ const row = document.createElement("div");
5424
+ drawn.forEach((series) => {
5425
+ const to = series.tops[index];
5426
+ const from = series.base?.[index] ?? 0;
5427
+ if (series.base && (to === null || from === null)) return;
5428
+ const column = document.createElement("span");
5429
+ if (to !== null) {
5430
+ column.className = series.className;
5431
+ column.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
5432
+ column.classList.toggle("negative", !series.base && to < from);
5433
+ column.style.setProperty("--top", `${clamp(Math.max(from, to), series.y)}%`);
5434
+ column.style.setProperty("--bottom", `${100 - clamp(Math.min(from, to), series.y)}%`);
5435
+ if (this._horizontal) column.dataset.text = series.text[index];
5436
+ }
5437
+ row.append(column);
5438
+ });
5439
+ columns.append(row);
5440
+ });
5441
+ return columns;
5442
+ }
5443
+ };
5444
+
5445
+ // src/ts/components/barChart.ts
5446
+ var BarChart = class extends ColumnChart {
5447
+ get _type() {
5448
+ return "bar-chart";
5449
+ }
5450
+ get _key() {
5451
+ return "Expressive_BarChart";
5452
+ }
5453
+ get _horizontal() {
5454
+ return true;
5455
+ }
5456
+ static init(els, options = {}) {
5457
+ return super.init(els, options);
5458
+ }
5459
+ static getInstance(el) {
5460
+ return el["Expressive_BarChart"];
5461
+ }
5462
+ /**
5463
+ * The column chart's spans; CSS reads their --top and --bottom across the
5464
+ * plot. With .values, a copy of the bars without their clipping holds each
5465
+ * cell's text, so text longer than a short bar can run past its end.
5466
+ */
5467
+ _draw() {
5468
+ const bars = super._draw();
5469
+ bars.className = "bar-chart-bars";
5470
+ bars.classList.toggle("rtl", getComputedStyle(this.el).direction === "rtl");
5471
+ if (!this.el.classList.contains("values")) return bars;
5472
+ const values = bars.cloneNode(true);
5473
+ values.classList.replace("bar-chart-bars", "bar-chart-values");
5474
+ values.querySelectorAll("span[data-text]").forEach((span) => {
5475
+ span.textContent = span.dataset.text;
5476
+ });
5477
+ const layers = document.createElement("div");
5478
+ layers.className = "bar-chart-layers";
5479
+ layers.append(bars, values);
5480
+ return layers;
5481
+ }
5482
+ };
5483
+
5484
+ // src/ts/components/pieChart.ts
5485
+ var COLORS2 = 6;
5486
+ var RADIUS = 46;
5487
+ var GROWN = 1.06;
5488
+ var LABEL_SHARE = 0.05;
5489
+ var _maskId = 0;
5490
+ var PieChart = class extends LineChart {
5491
+ get _type() {
5492
+ return "pie-chart";
5493
+ }
5494
+ get _key() {
5495
+ return "Expressive_PieChart";
5496
+ }
5497
+ get _points() {
5498
+ return false;
5499
+ }
5500
+ get _circular() {
5501
+ return true;
5502
+ }
5503
+ static init(els, options = {}) {
5504
+ return super.init(els, options);
5505
+ }
5506
+ static getInstance(el) {
5507
+ return el["Expressive_PieChart"];
5508
+ }
5509
+ show(index) {
5510
+ if (!this._tooltip || index === this.activeIndex) return;
5511
+ super.show(index);
5512
+ this._slices.forEach((slice, i) => slice?.classList.toggle("active", i === index));
5513
+ if (this._tooltip.hidden) return;
5514
+ const color = this._colors[index];
5515
+ this._tooltip.querySelectorAll("[data-series]").forEach((span) => {
5516
+ if (color) span.dataset.series = color;
5517
+ else delete span.dataset.series;
5518
+ });
5519
+ const share = this._share(index);
5520
+ const value = this._tooltip.querySelector("span:nth-of-type(2)");
5521
+ if (share && value) value.textContent += ` (${share})`;
5522
+ }
5523
+ /** CSS places the tooltip on the slice's middle from --turn. */
5524
+ _place(index) {
5525
+ const middle = (this._turns[index][0] + this._turns[index][1]) / 2;
5526
+ this._tooltip.style.setProperty("--turn", String(middle));
5527
+ this._tooltip.classList.toggle("end", middle < 0.5);
5528
+ }
5529
+ /** The row's share of the whole as a percentage, or '' for none. */
5530
+ _share(index) {
5531
+ var _a;
5532
+ const [start, end] = this._turns[index];
5533
+ if (end <= start) return "";
5534
+ const share = end - start;
5535
+ const digits = share < 0.1 ? 1 : 0;
5536
+ (_a = this._percents ?? (this._percents = []))[digits] ?? (_a[digits] = this._percent(digits));
5537
+ return this._percents[digits].format(share);
5538
+ }
5539
+ _percent(digits) {
5540
+ const options = { style: "percent", maximumFractionDigits: digits };
5541
+ try {
5542
+ return new Intl.NumberFormat(this.el.closest("[lang]")?.lang || void 0, options);
5543
+ } catch {
5544
+ return new Intl.NumberFormat(void 0, options);
5545
+ }
5546
+ }
5547
+ /** The legend: every row with its color, its cell and its share. */
5548
+ _guides() {
5549
+ this._percents = [];
5550
+ const legend = document.createElement("ul");
5551
+ legend.className = "pie-chart-legend";
5552
+ legend.setAttribute("aria-hidden", "true");
5553
+ const text = this.series[0].text;
5554
+ legend.append(...this.labels.map((label, i) => {
5555
+ const li = document.createElement("li");
5556
+ if (this._colors[i]) li.dataset.series = this._colors[i];
5557
+ const [name, value, share] = [label, text[i], this._share(i)].map((content) => {
5558
+ const span = document.createElement("span");
5559
+ span.textContent = content;
5560
+ return span;
5561
+ });
5562
+ li.append(name, value, share);
5563
+ return li;
5564
+ }));
5565
+ return [legend];
5566
+ }
5567
+ /** The row under the pointer by its angle from the middle, or -1 off the slices. */
5568
+ _indexAt(e) {
5569
+ const [turn, share] = this._polarAt(e);
5570
+ const distance = share * 100;
5571
+ const hole = this.el.classList.contains("donut") ? parseFloat(getComputedStyle(this.el).getPropertyValue("--md-comp-pie-chart-hole")) || 0 : 0;
5572
+ if (distance < RADIUS * hole) return -1;
5573
+ const index = this._turns.findIndex(([start, end]) => start <= turn && turn < end);
5574
+ return distance > RADIUS * (index === this.activeIndex ? GROWN : 1) ? -1 : index;
5575
+ }
5576
+ /**
5577
+ * An SVG of slices in a -50 to 50 box, under a mask that cuts the gaps
5578
+ * between them and the donut's hole, which CSS sizes. The values and the
5579
+ * total are HTML over it.
5580
+ */
5581
+ _draw() {
5582
+ const values = this.series[0].values.map((value) => Math.max(0, value ?? 0));
5583
+ const total = values.reduce((sum, value) => sum + value, 0);
5584
+ let turn = 0;
5585
+ this._turns = values.map((value) => [turn, turn += total ? value / total : 0]);
5586
+ const drawn = this._turns.filter(([start, end]) => end > start).length;
5587
+ let slice = 0;
5588
+ this._colors = this._turns.map(([start, end]) => {
5589
+ if (end <= start) return void 0;
5590
+ const index = slice++;
5591
+ return String(index === drawn - 1 && index > 0 && index % COLORS2 === 0 ? 2 : index % COLORS2 + 1);
5592
+ });
5593
+ const at = (t, r = RADIUS) => {
5594
+ const angle = t * 2 * Math.PI;
5595
+ return `${(r * Math.sin(angle)).toFixed(3)},${(-r * Math.cos(angle)).toFixed(3)}`;
5596
+ };
5597
+ const pie = document.createElement("div");
5598
+ pie.className = "pie-chart-pie";
5599
+ pie.setAttribute("aria-hidden", "true");
5600
+ const svg = svgNode("svg", { viewBox: "-50 -50 100 100", focusable: "false" });
5601
+ const id = `pie-chart-mask-${++_maskId}`;
5602
+ const area = { x: "-50", y: "-50", width: "100", height: "100" };
5603
+ const mask = svgNode("mask", { id, maskUnits: "userSpaceOnUse", ...area });
5604
+ const edges = this._turns.filter(([start, end]) => end > start);
5605
+ mask.append(
5606
+ svgNode("rect", { ...area, fill: "white" }),
5607
+ svgNode("path", {
5608
+ class: "pie-chart-gaps",
5609
+ stroke: "black",
5610
+ d: edges.length > 1 ? edges.map(([start]) => `M0,0L${at(start, 50)}`).join("") : ""
5611
+ }),
5612
+ svgNode("circle", { class: "pie-chart-hole", r: String(RADIUS), fill: "black" })
5613
+ );
5614
+ const slices = svgNode("g", { mask: `url(#${id})` });
5615
+ const arc2 = `A${RADIUS},${RADIUS} 0`;
5616
+ this._slices = this._turns.map(([start, end], i) => {
5617
+ if (end <= start) return null;
5618
+ const slice2 = svgNode("path", {
5619
+ class: "pie-chart-slice",
5620
+ "data-series": this._colors[i],
5621
+ // A whole circle is two half arcs; one arc cannot end where it starts.
5622
+ d: end - start >= 1 ? `M0,${-RADIUS}${arc2} 1 1 0,${RADIUS}${arc2} 1 1 0,${-RADIUS}Z` : `M0,0L${at(start)}${arc2} ${end - start > 0.5 ? 1 : 0} 1 ${at(end)}Z`
5623
+ });
5624
+ slices.append(slice2);
5625
+ return slice2;
5626
+ });
5627
+ svg.append(mask, slices);
5628
+ pie.append(svg);
5629
+ if (this.el.classList.contains("values")) {
5630
+ const labels = document.createElement("div");
5631
+ labels.className = "pie-chart-values";
5632
+ this._turns.forEach(([start, end], i) => {
5633
+ if (end - start < LABEL_SHARE) return;
5634
+ const label = document.createElement("span");
5635
+ label.dataset.series = this._colors[i];
5636
+ label.style.setProperty("--turn", String((start + end) / 2));
5637
+ label.textContent = this._share(i);
5638
+ labels.append(label);
5639
+ });
5640
+ pie.append(labels);
5641
+ }
5642
+ const middle = this._total("pie-chart-total");
5643
+ if (middle) pie.append(middle);
5644
+ return pie;
5645
+ }
5646
+ };
5647
+
5648
+ // src/ts/components/heatmapChart.ts
5649
+ var HeatmapChart = class extends LineChart {
5650
+ get _type() {
5651
+ return "heatmap-chart";
5652
+ }
5653
+ get _key() {
5654
+ return "Expressive_HeatmapChart";
5655
+ }
5656
+ static init(els, options = {}) {
5657
+ return super.init(els, options);
5658
+ }
5659
+ static getInstance(el) {
5660
+ return el["Expressive_HeatmapChart"];
5661
+ }
5662
+ /** Highlights one cell, numbered row by row. Pass -1 to hide the tooltip. */
5663
+ show(index) {
5664
+ if (!this._tooltip || index === this.activeIndex) return;
5665
+ const cell = this._cells[index];
5666
+ const [left, top, width, plotWidth] = cell ? [cell.offsetLeft, cell.offsetTop, cell.offsetWidth, this._plot.clientWidth] : [];
5667
+ this._cells[this.activeIndex]?.classList.remove("active");
5668
+ this.activeIndex = index;
5669
+ this._tooltip.hidden = !cell;
5670
+ if (!cell) return;
5671
+ cell.classList.add("active");
5672
+ const columns = this.series.length;
5673
+ const [row, series] = [Math.floor(index / columns), this.series[index % columns]];
5674
+ const title = document.createElement("strong");
5675
+ title.textContent = this.labels[row];
5676
+ const [name, text] = [document.createElement("span"), document.createElement("span")];
5677
+ name.textContent = series.name;
5678
+ if (this._levels[index] === null) name.className = "gap";
5679
+ else name.style.setProperty("--level", String(this._levels[index]));
5680
+ text.textContent = series.text[row];
5681
+ this._tooltip.replaceChildren(title, name, text);
5682
+ const end = left + width / 2 > plotWidth / 2;
5683
+ this._tooltip.classList.toggle("end", end);
5684
+ this._tooltip.style.left = `${left + (end ? 0 : width)}px`;
5685
+ this._tooltip.style.top = `${top}px`;
5686
+ }
5687
+ /** Arrows move the way they point, so left and right swap in RTL. */
5688
+ _keyTarget(key) {
5689
+ if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
5690
+ const [columns, last] = [this.series.length, this.labels.length - 1];
5691
+ const index = Math.max(0, this.activeIndex);
5692
+ const [row, column] = [Math.floor(index / columns), index % columns];
5693
+ const [next, back] = this._reversed ? ["ArrowLeft", "ArrowRight"] : ["ArrowRight", "ArrowLeft"];
5694
+ const to = {
5695
+ [next]: [row, column + 1],
5696
+ [back]: [row, column - 1],
5697
+ ArrowDown: [row + 1, column],
5698
+ ArrowUp: [row - 1, column],
5699
+ Home: [row, 0],
5700
+ End: [row, columns - 1]
5701
+ }[key];
5702
+ if (!to) return void 0;
5703
+ if (this.activeIndex < 0) return 0;
5704
+ const clamp = (value, max) => Math.min(max, Math.max(0, value));
5705
+ return clamp(to[0], last) * columns + clamp(to[1], columns - 1);
5706
+ }
5707
+ /**
5708
+ * The cell under the pointer by its position, or -1 off the grid. A touch
5709
+ * keeps its first target while it drags, so the target cannot be used. The
5710
+ * cells and gaps are even, so each band's edge falls in a gap and a gap
5711
+ * counts as the nearer cell.
5712
+ */
5713
+ _indexAt(e) {
5714
+ const [columns, rows] = [this.series.length, this.labels.length];
5715
+ const first = this._cells[0].getBoundingClientRect();
5716
+ const last = this._cells[this._cells.length - 1].getBoundingClientRect();
5717
+ const [left, right] = [Math.min(first.left, last.left), Math.max(first.right, last.right)];
5718
+ const [x, y] = [e.clientX, e.clientY];
5719
+ if (x < left || x > right || y < first.top || y > last.bottom) return -1;
5720
+ const band = (offset, size, count) => Math.min(count - 1, Math.floor(offset / (size || 1) * count));
5721
+ const column = band(x - left, right - left, columns);
5722
+ return band(y - first.top, last.bottom - first.top, rows) * columns + (this._reversed ? columns - 1 - column : column);
5723
+ }
5724
+ /**
5725
+ * The legend: a gradient from the lowest cell's shade to the highest's,
5726
+ * labelled with their text. A fixed scale wider than the data shows only
5727
+ * the part the data uses.
5728
+ */
5729
+ _guides() {
5730
+ const legend = document.createElement("div");
5731
+ legend.className = "heatmap-chart-legend";
5732
+ legend.setAttribute("aria-hidden", "true");
5733
+ const [low, scale, high] = ["span", "span", "span"].map((tag) => document.createElement(tag));
5734
+ [low.textContent, high.textContent] = this._ends;
5735
+ scale.className = "heatmap-chart-scale";
5736
+ if (this._reversed) scale.style.setProperty("--heatmap-chart-to", "left");
5737
+ scale.style.setProperty("--low", String(this._endLevels[0]));
5738
+ scale.style.setProperty("--high", String(this._endLevels[1]));
5739
+ legend.append(low, scale, high);
5740
+ return [legend];
5741
+ }
5742
+ /**
5743
+ * A grid of the row headers and cells, row by row, with the column headers
5744
+ * below. Each cell's --level shades it in CSS.
5745
+ */
5746
+ _draw() {
5747
+ const columns = this.series.length;
5748
+ let [low, high] = [Infinity, -Infinity];
5749
+ this._ends = ["", ""];
5750
+ this.series.forEach((series) => series.values.forEach((value, row) => {
5751
+ if (value === null) return;
5752
+ if (value < low) [low, this._ends[0]] = [value, series.text[row]];
5753
+ if (value > high) [high, this._ends[1]] = [value, series.text[row]];
5754
+ }));
5755
+ const option = (name) => {
5756
+ const value = parseFloat(this.el.dataset[name] ?? "");
5757
+ return Number.isFinite(value) ? value : this.options[name];
5758
+ };
5759
+ const [min, max] = [option("min") ?? low, option("max") ?? high];
5760
+ const shade = (value) => Math.min(1, Math.max(0, (value - min) / (max - min || 1)));
5761
+ this._endLevels = [shade(low), shade(high)];
5762
+ const grid = document.createElement("div");
5763
+ grid.className = "heatmap-chart-grid";
5764
+ grid.setAttribute("aria-hidden", "true");
5765
+ grid.style.setProperty("--columns", String(columns));
5766
+ grid.style.setProperty("--rows", String(this.labels.length));
5767
+ const span = (className, text = "") => {
5768
+ const element = document.createElement("span");
5769
+ element.className = className;
5770
+ element.textContent = text;
5771
+ return element;
5772
+ };
5773
+ const label = (cell) => cell?.dataset.label ?? cell?.textContent.trim() ?? "";
5774
+ const values = this.el.classList.contains("values");
5775
+ this._cells = [];
5776
+ this._levels = [];
5777
+ this.labels.forEach((_, row) => {
5778
+ grid.append(span("heatmap-chart-row", label(this._rows[row].cells[0])));
5779
+ this.series.forEach((series) => {
5780
+ const value = series.values[row];
5781
+ const level = value === null ? null : shade(value);
5782
+ const cell = span("heatmap-chart-cell", values ? series.text[row] : "");
5783
+ if (level === null) cell.classList.add("gap");
5784
+ else cell.style.setProperty("--level", String(level));
5785
+ this._cells.push(cell);
5786
+ this._levels.push(level);
5787
+ grid.append(cell);
5788
+ });
5789
+ });
5790
+ const labels = this._header.slice(1, columns + 1).map(label);
5791
+ labels.forEach((text, column) => {
5792
+ if (!text) return;
5793
+ let end = column + 1;
5794
+ while (end < columns && !labels[end]) end++;
5795
+ const element = span("heatmap-chart-column", text);
5796
+ element.classList.toggle("run", end > column + 1);
5797
+ element.style.gridArea = `${this.labels.length + 1} / ${column + 2} / auto / ${end + 2}`;
5798
+ grid.append(element);
5799
+ });
5800
+ return grid;
5801
+ }
5802
+ };
5803
+
5804
+ // src/ts/components/radarChart.ts
5805
+ var RINGS = 4;
5806
+ var RadarChart = class extends LineChart {
5807
+ get _type() {
5808
+ return "radar-chart";
5809
+ }
5810
+ get _key() {
5811
+ return "Expressive_RadarChart";
5812
+ }
5813
+ /** The scale starts at zero, the middle, so shapes compare by size. */
5814
+ get _fromZero() {
5815
+ return true;
5816
+ }
5817
+ get _circular() {
5818
+ return true;
5819
+ }
5820
+ static init(els, options = {}) {
5821
+ return super.init(els, options);
5822
+ }
5823
+ static getInstance(el) {
5824
+ return el["Expressive_RadarChart"];
5825
+ }
5826
+ /**
5827
+ * The cursor is a spoke turned by --turn, and the tooltip sits past its
5828
+ * tip. LineChart's points on the spoke keep their top, a share of the
5829
+ * radius, which stops at the rim and the middle like the shapes.
5830
+ */
5831
+ _place(index) {
5832
+ for (const dot of this._cursor.children) {
5833
+ dot.style.top = `${Math.min(100, Math.max(0, parseFloat(dot.style.top)))}%`;
5834
+ }
5835
+ const turn = this._turn(index);
5836
+ this._cursor.style.setProperty("--turn", String(turn));
5837
+ this._tooltip.style.setProperty("--turn", String(turn));
5838
+ this._tooltip.classList.toggle("end", turn > 0 && turn < 0.5);
5839
+ }
5840
+ /** A row's spoke as a fraction of a turn clockwise from the top. */
5841
+ _turn(index) {
5842
+ return index / this.labels.length;
5843
+ }
5844
+ /** Only the legend; the labels go round the rim in _draw. */
5845
+ _guides() {
5846
+ return this._legend();
5847
+ }
5848
+ /** The nearest spoke to the pointer, by its angle from the middle. */
5849
+ _indexAt(e) {
5850
+ const [turn] = this._polarAt(e);
5851
+ return Math.round(turn * this.labels.length) % this.labels.length;
5852
+ }
5853
+ /**
5854
+ * An SVG in a -50 to 50 box: the rings and spokes of the grid, then each
5855
+ * series' shape, with its points for .points. The rim is the box's edge;
5856
+ * the labels round it are HTML.
5857
+ */
5858
+ _draw() {
5859
+ const at = (index, share2) => {
5860
+ const angle = this._turn(index) * 2 * Math.PI;
5861
+ return `${(50 * share2 * Math.sin(angle)).toFixed(3)},${(-50 * share2 * Math.cos(angle)).toFixed(3)}`;
5862
+ };
5863
+ const share = (value) => Math.min(1, Math.max(0, 1 - this._y(value) / 100));
5864
+ const rows = this.labels.map((_, i) => i);
5865
+ const ring = (r) => `M${rows.map((i) => at(i, r)).join("L")}Z`;
5866
+ const svg = svgNode("svg", { viewBox: "-50 -50 100 100", "aria-hidden": "true", focusable: "false" });
5867
+ svg.append(svgNode("path", {
5868
+ class: "radar-chart-grid",
5869
+ d: Array.from({ length: RINGS }, (_, r) => ring((r + 1) / RINGS)).join("") + rows.map((i) => `M0,0L${at(i, 1)}`).join("")
5870
+ }));
5871
+ const points = this.el.classList.contains("points");
5872
+ this.series.forEach((series, s) => {
5873
+ const group = svgNode("g", {
5874
+ class: `radar-chart-series ${series.className}`.trim(),
5875
+ "data-series": String(s % COLORS + 1)
5876
+ });
5877
+ const drawn = rows.filter((i) => series.values[i] !== null);
5878
+ const corners = drawn.map((i) => at(i, share(series.values[i])));
5879
+ group.append(svgNode("path", { class: "radar-chart-shape", d: corners.length ? `M${corners.join("L")}Z` : "" }));
5880
+ if (points) group.append(svgNode("path", { class: "radar-chart-points", d: corners.map((c) => `M${c}h0`).join("") }));
5881
+ svg.append(group);
5882
+ });
5883
+ const labels = document.createElement("ol");
5884
+ labels.className = "radar-chart-axes";
5885
+ labels.setAttribute("aria-hidden", "true");
5886
+ this.labels.forEach((text, i) => {
5887
+ const li = document.createElement("li");
5888
+ li.textContent = text;
5889
+ li.style.setProperty("--turn", String(this._turn(i)));
5890
+ labels.append(li);
5891
+ });
5892
+ const web = document.createElement("div");
5893
+ web.className = "radar-chart-web";
5894
+ web.append(svg, labels);
5895
+ return web;
5896
+ }
5897
+ };
5898
+
5899
+ // src/ts/components/radialChart.ts
5900
+ var RADIUS2 = 46;
5901
+ var FILL = 0.8;
5902
+ var RadialChart = class extends LineChart {
5903
+ get _type() {
5904
+ return "radial-chart";
5905
+ }
5906
+ get _key() {
5907
+ return "Expressive_RadialChart";
5908
+ }
5909
+ get _points() {
5910
+ return false;
5911
+ }
5912
+ get _circular() {
5913
+ return true;
5914
+ }
5915
+ get _gauge() {
5916
+ return this.el.classList.contains("gauge");
3870
5917
  }
3871
- /**
3872
- * Initializes instances of CharacterCounter.
3873
- * @param els HTML elements.
3874
- * @param options Component options.
3875
- */
3876
5918
  static init(els, options = {}) {
3877
- return super.init(els, options, _CharacterCounter);
5919
+ return super.init(els, options);
3878
5920
  }
3879
5921
  static getInstance(el) {
3880
- return el["Expressive_CharacterCounter"];
5922
+ return el["Expressive_RadialChart"];
5923
+ }
5924
+ show(index) {
5925
+ if (!this._tooltip || index === this.activeIndex) return;
5926
+ super.show(index);
5927
+ this._rings.forEach((ring, i) => ring.classList.toggle("active", i === index));
5928
+ if (this._tooltip.hidden) return;
5929
+ const color = this._color(index);
5930
+ this._tooltip.querySelectorAll("[data-series]").forEach((span) => {
5931
+ span.dataset.series = color;
5932
+ });
3881
5933
  }
3882
- destroy() {
3883
- this._removeEventHandlers();
3884
- this.el["Expressive_CharacterCounter"] = void 0;
3885
- this._removeCounter();
5934
+ /** The tooltip sits where the ring's fill ends. */
5935
+ _place(index) {
5936
+ const turn = this._turn(this._fractions[index]);
5937
+ const r = this._radii[index];
5938
+ const [x, y] = [r * Math.sin(turn * 2 * Math.PI), -r * Math.cos(turn * 2 * Math.PI)];
5939
+ this._tooltip.style.left = `${50 + x}%`;
5940
+ this._tooltip.style.top = `${(50 + y) / (this._gauge ? 50 : 100) * 100}%`;
5941
+ this._tooltip.classList.toggle("end", x > 0);
5942
+ }
5943
+ _color(index) {
5944
+ return String(index % COLORS + 1);
5945
+ }
5946
+ /** A fraction of the ring as a turn clockwise from the top. */
5947
+ _turn(fraction) {
5948
+ return this._gauge ? fraction / 2 - 0.25 : fraction;
5949
+ }
5950
+ /** For more than one row, a legend of every row with its color and cell. */
5951
+ _guides() {
5952
+ if (this.labels.length < 2) return [];
5953
+ const legend = document.createElement("ul");
5954
+ legend.className = "radial-chart-legend";
5955
+ legend.setAttribute("aria-hidden", "true");
5956
+ const text = this.series[0].text;
5957
+ legend.append(...this.labels.map((label, i) => {
5958
+ const li = document.createElement("li");
5959
+ li.dataset.series = this._color(i);
5960
+ const [name, value] = [label, text[i]].map((content) => {
5961
+ const span = document.createElement("span");
5962
+ span.textContent = content;
5963
+ return span;
5964
+ });
5965
+ li.append(name, value);
5966
+ return li;
5967
+ }));
5968
+ return [legend];
5969
+ }
5970
+ /** The ring under the pointer by its distance from the middle, or -1. */
5971
+ _indexAt(e) {
5972
+ const box = this._plot.getBoundingClientRect();
5973
+ const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.width / 2];
5974
+ if (this._gauge && y > 0) return -1;
5975
+ const distance = Math.hypot(x, y) / box.width * 100;
5976
+ const step = this._radii.length > 1 ? this._radii[0] - this._radii[1] : this._width;
5977
+ return this._radii.findIndex((r) => Math.abs(distance - r) <= step / 2);
3886
5978
  }
3887
- _setupEventHandlers() {
3888
- this.el.addEventListener("focus", this.updateCounter, true);
3889
- this.el.addEventListener("input", this.updateCounter, true);
5979
+ /**
5980
+ * An SVG of rings in a -50 to 50 box, the top half for a gauge. Each row
5981
+ * is a track circle and a fill circle whose dash is its share; CSS draws
5982
+ * them from --radial-chart-dash. The total is HTML over it.
5983
+ */
5984
+ _draw() {
5985
+ const style = getComputedStyle(this.el);
5986
+ const token = (name, fallback) => {
5987
+ const value = parseFloat(style.getPropertyValue(`--md-comp-radial-chart-${name}`));
5988
+ return Number.isFinite(value) ? value : fallback;
5989
+ };
5990
+ const count = this.labels.length;
5991
+ let [width, gap] = [Math.max(0, token("thickness", 8)), Math.max(0, token("gap", 2))];
5992
+ const scale = Math.min(1, RADIUS2 * FILL / (count * width + (count - 1) * gap || 1));
5993
+ [width, gap] = [width * scale, gap * scale];
5994
+ this._width = width;
5995
+ this._radii = this.labels.map((_, i) => RADIUS2 - width / 2 - i * (width + gap));
5996
+ const max = [parseFloat(this.el.dataset.max ?? ""), this.options.max ?? NaN, 100].find((value) => value > 0);
5997
+ this._fractions = this.series[0].values.map((value) => Math.min(1, Math.max(0, (value ?? 0) / max)));
5998
+ const gauge = this._gauge;
5999
+ const svg = svgNode("svg", { viewBox: gauge ? "-50 -50 100 50" : "-50 -50 100 100", focusable: "false" });
6000
+ const transform = gauge ? "rotate(180)" : "rotate(-90)";
6001
+ const span = gauge ? 0.5 : 1;
6002
+ this._rings = this._radii.map((r, i) => {
6003
+ const length = 2 * Math.PI * r;
6004
+ const circle = (name, fraction, capped) => {
6005
+ const ring2 = svgNode("circle", { class: name, r: r.toFixed(3), transform, "stroke-width": width.toFixed(3) });
6006
+ const dash = Math.max(1e-3, fraction * span * length - (capped ? width : 0));
6007
+ ring2.style.setProperty("--radial-chart-dash", `${dash.toFixed(3)} ${length.toFixed(3)}`);
6008
+ if (capped) ring2.style.setProperty("--radial-chart-offset", (-width / 2).toFixed(3));
6009
+ return ring2;
6010
+ };
6011
+ const ring = circle("radial-chart-ring", this._fractions[i], true);
6012
+ ring.dataset.series = this._color(i);
6013
+ if (!this._fractions[i]) ring.classList.add("empty");
6014
+ svg.append(circle("radial-chart-track", 1, gauge), ring);
6015
+ return ring;
6016
+ });
6017
+ const rings = document.createElement("div");
6018
+ rings.className = "radial-chart-rings";
6019
+ rings.setAttribute("aria-hidden", "true");
6020
+ rings.style.setProperty("--radial-chart-middle", `${(this._radii[count - 1] - this._width / 2) * 2}%`);
6021
+ rings.append(svg);
6022
+ const total = this._total("radial-chart-total");
6023
+ if (total) rings.append(total);
6024
+ return rings;
3890
6025
  }
3891
- _removeEventHandlers() {
3892
- this.el.removeEventListener("focus", this.updateCounter, true);
3893
- this.el.removeEventListener("input", this.updateCounter, true);
6026
+ };
6027
+
6028
+ // src/ts/components/mixedChart.ts
6029
+ var has = (series, name) => series.className.split(/\s+/).includes(name);
6030
+ var MixedChart = class extends ColumnChart {
6031
+ get _type() {
6032
+ return "mixed-chart";
3894
6033
  }
3895
- _setupCounter() {
3896
- this.counterEl = document.createElement("span");
3897
- this.counterEl.classList.add("character-counter");
3898
- this.counterEl.setAttribute("aria-live", "polite");
3899
- this.counterEl.setAttribute("aria-atomic", "true");
3900
- this.counterEl.style.float = "right";
3901
- this.counterEl.style.fontSize = "12px";
3902
- this.counterEl.style.height = "1";
3903
- this.el.parentElement.appendChild(this.counterEl);
6034
+ get _key() {
6035
+ return "Expressive_MixedChart";
3904
6036
  }
3905
- _removeCounter() {
3906
- this.counterEl.remove();
6037
+ get _points() {
6038
+ return true;
3907
6039
  }
3908
- _validateInput() {
3909
- if (this.isValidLength && this.isInvalid) {
3910
- this.isInvalid = false;
3911
- this.el.classList.remove("invalid");
3912
- } else if (!this.isValidLength && !this.isInvalid) {
3913
- this.isInvalid = true;
3914
- this.el.classList.remove("valid");
3915
- this.el.classList.add("invalid");
3916
- }
6040
+ static init(els, options = {}) {
6041
+ return super.init(els, options);
6042
+ }
6043
+ static getInstance(el) {
6044
+ return el["Expressive_MixedChart"];
6045
+ }
6046
+ /** The start scale, then the end scale; each includes zero if it has columns. */
6047
+ _scale(series, min, max) {
6048
+ const end = series.filter((each) => has(each, "end"));
6049
+ const start = series.filter((each) => !end.includes(each));
6050
+ const columns = (list) => list.some((each) => has(each, "column"));
6051
+ super._scale(end, this._number("endMin"), this._number("endMax"), columns(end));
6052
+ return super._scale(start, min, max, columns(start));
6053
+ }
6054
+ /** Columns behind, then the lines and areas over them. */
6055
+ _draw() {
6056
+ const marks = document.createElement("div");
6057
+ marks.className = "mixed-chart-marks";
6058
+ const columns = super._draw(this.series.filter((series) => has(series, "column")));
6059
+ columns.className = "mixed-chart-columns";
6060
+ marks.append(columns, LineChart.prototype["_draw"].call(this, this.series.filter((series) => !has(series, "column"))));
6061
+ return marks;
3917
6062
  }
3918
6063
  };
3919
6064
 
3920
6065
  // src/ts/components/select.ts
3921
- var _defaults12 = {
6066
+ var _defaults17 = {
3922
6067
  classes: "",
3923
6068
  menuOptions: {}
3924
6069
  };
@@ -4000,7 +6145,7 @@ var Expressive = (() => {
4000
6145
  this._setupEventHandlers();
4001
6146
  }
4002
6147
  static get defaults() {
4003
- return _defaults12;
6148
+ return _defaults17;
4004
6149
  }
4005
6150
  /**
4006
6151
  * Initializes instances of FormSelect.
@@ -4425,7 +6570,7 @@ var Expressive = (() => {
4425
6570
  };
4426
6571
 
4427
6572
  // src/ts/plugins/dockedDisplayPlugin.ts
4428
- var _defaults13 = {
6573
+ var _defaults18 = {
4429
6574
  margin: 5,
4430
6575
  transition: 10,
4431
6576
  duration: 250,
@@ -4439,9 +6584,9 @@ var Expressive = (() => {
4439
6584
  }
4440
6585
  };
4441
6586
  /**
4442
- * Tears the plugin down. The document listener holds a reference to this
4443
- * instance - and through it the whole picker subtree - so the owning
4444
- * component must call this from its own destroy().
6587
+ * Tears the plugin down. While shown, the document listener holds a
6588
+ * reference to this instance - and through it the whole picker subtree - so
6589
+ * the owning component must call this from its own destroy().
4445
6590
  */
4446
6591
  this.destroy = () => {
4447
6592
  document.removeEventListener("click", this._handleDocumentClick);
@@ -4450,6 +6595,7 @@ var Expressive = (() => {
4450
6595
  this.show = () => {
4451
6596
  if (this.visible) return;
4452
6597
  this.visible = true;
6598
+ document.addEventListener("click", this._handleDocumentClick);
4453
6599
  const coordinates = Utils._setAbsolutePosition(this.el, this.container, "bottom", this.options.margin, this.options.transition, this.options.align);
4454
6600
  this.container.style.visibility = "visible";
4455
6601
  this.container.style.transition = `
@@ -4463,6 +6609,7 @@ var Expressive = (() => {
4463
6609
  this.hide = () => {
4464
6610
  if (!this.visible) return;
4465
6611
  this.visible = false;
6612
+ document.removeEventListener("click", this._handleDocumentClick);
4466
6613
  this.container.removeAttribute("style");
4467
6614
  this.container.style.transition = `
4468
6615
  transform ${this.options.duration}ms ease-out,
@@ -4474,14 +6621,13 @@ var Expressive = (() => {
4474
6621
  };
4475
6622
  this.el = el;
4476
6623
  this.options = {
4477
- ..._defaults13,
6624
+ ..._defaults18,
4478
6625
  ...options
4479
6626
  };
4480
6627
  this.container = document.createElement("div");
4481
6628
  this.container.classList.add("display-docked");
4482
6629
  this.container.append(container);
4483
6630
  el.parentElement.append(this.container);
4484
- document.addEventListener("click", this._handleDocumentClick);
4485
6631
  }
4486
6632
  /**
4487
6633
  * Initializes instance of DockedDisplayPlugin
@@ -4495,7 +6641,7 @@ var Expressive = (() => {
4495
6641
  };
4496
6642
 
4497
6643
  // src/ts/components/datepicker.ts
4498
- var _defaults14 = {
6644
+ var _defaults19 = {
4499
6645
  // the default output format for the input field value
4500
6646
  format: "mmm dd, yyyy",
4501
6647
  // Used to create date object from current input string
@@ -4599,6 +6745,8 @@ var Expressive = (() => {
4599
6745
  constructor(el, options) {
4600
6746
  super(el, options, _Datepicker);
4601
6747
  this.multiple = false;
6748
+ /** Rows renderRow has already checked, so renderBody does not parse them again. */
6749
+ this._checkedRows = /* @__PURE__ */ new Set();
4602
6750
  /** Nesting depth of {@link _batchDraws}; > 0 means draws are being collected. */
4603
6751
  this._drawDepth = 0;
4604
6752
  /** Whether a draw was requested while batching. */
@@ -4665,7 +6813,9 @@ var Expressive = (() => {
4665
6813
  if (maxDate && date > maxDate) date.setTime(maxDate.getTime());
4666
6814
  if (date.getFullYear() < minYear || date.getFullYear() > maxYear || date.getFullYear() === minYear && date.getMonth() < (minMonth ?? 0) || date.getFullYear() === maxYear && date.getMonth() > (maxMonth ?? 11)) return;
4667
6815
  this._focusedDate = date;
4668
- this.gotoDate(date);
6816
+ const { year, month } = this.calendars[0];
6817
+ if (date.getFullYear() === year && date.getMonth() === month) this._syncCalendarFocus(true);
6818
+ else this.gotoDate(date);
4669
6819
  };
4670
6820
  this._handleCalendarClick = (e) => {
4671
6821
  const target = e.target;
@@ -4780,7 +6930,7 @@ var Expressive = (() => {
4780
6930
  }
4781
6931
  }
4782
6932
  static get defaults() {
4783
- return _defaults14;
6933
+ return _defaults19;
4784
6934
  }
4785
6935
  /**
4786
6936
  * Initializes instances of Datepicker.
@@ -5222,7 +7372,9 @@ var Expressive = (() => {
5222
7372
  }
5223
7373
  /** Assemble calendar cells; reject unsupported elements and attributes. */
5224
7374
  renderRow(days, isRTL, isRowSelected) {
5225
- return '<tr class="datepicker-row' + (isRowSelected ? " is-selected" : "") + '">' + _Datepicker._calendarMarkup((isRTL ? days.reverse() : days).join(""), "tr") + "</tr>";
7375
+ const row = '<tr class="datepicker-row' + (isRowSelected ? " is-selected" : "") + '">' + _Datepicker._calendarMarkup((isRTL ? days.reverse() : days).join(""), "tr") + "</tr>";
7376
+ this._checkedRows.add(row);
7377
+ return row;
5226
7378
  }
5227
7379
  /** Assemble calendar rows; reject unsupported elements and attributes. */
5228
7380
  renderTable(opts, data, randId) {
@@ -5240,7 +7392,10 @@ var Expressive = (() => {
5240
7392
  }
5241
7393
  /** Wrap calendar rows after validating and encoding their contents. */
5242
7394
  renderBody(rows) {
5243
- return "<tbody>" + _Datepicker._calendarMarkup(rows.join(""), "tbody") + "</tbody>";
7395
+ const html = rows.join("");
7396
+ const checked = rows.every((row) => this._checkedRows.has(row));
7397
+ this._checkedRows.clear();
7398
+ return "<tbody>" + (checked ? html : _Datepicker._calendarMarkup(html, "tbody")) + "</tbody>";
5244
7399
  }
5245
7400
  static _calendarMarkup(html, parent) {
5246
7401
  const template = document.createElement("template");
@@ -5595,8 +7750,789 @@ var Expressive = (() => {
5595
7750
  </div>`;
5596
7751
  var Datepicker = _Datepicker;
5597
7752
 
7753
+ // src/ts/components/messageScroller.ts
7754
+ var _defaults20 = {
7755
+ autoScroll: true,
7756
+ defaultScrollPosition: "end",
7757
+ peek: 64
7758
+ };
7759
+ var EDGE = 2;
7760
+ var UP_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "PageUp", "Home"]);
7761
+ var RELEASE_EVENTS = ["pointerup", "pointercancel", "touchend", "touchcancel"];
7762
+ var MessageScroller = class _MessageScroller extends Component {
7763
+ constructor(el, options) {
7764
+ super(el, options, _MessageScroller);
7765
+ this._opened = false;
7766
+ this._following = false;
7767
+ /** A press is down: it may be a tap or a scrollbar drag. */
7768
+ this._holding = false;
7769
+ this._lastTop = 0;
7770
+ /** Where a programmatic scroll is heading; null when none is running. */
7771
+ this._target = null;
7772
+ /** Distance left to the target at the last scroll event. */
7773
+ this._distance = Infinity;
7774
+ /** The first visible message and its offset from the viewport top. */
7775
+ this._ref = null;
7776
+ this._refOffset = 0;
7777
+ /** The latest anchored turn, which the spacer lets reach the top. */
7778
+ this._anchor = null;
7779
+ this._spacer = 0;
7780
+ this._last = null;
7781
+ this._resizeObserver = null;
7782
+ this._mutationObserver = null;
7783
+ // A programmatic scroll ends when it arrives, or when a scroll moves away
7784
+ // from its target because something else took over.
7785
+ this._handleScroll = () => {
7786
+ if (this._target !== null) {
7787
+ const distance = Math.abs(this.viewport.scrollTop - this._resolve(this._target));
7788
+ if (distance > EDGE && distance < this._distance) {
7789
+ this._distance = distance;
7790
+ return;
7791
+ }
7792
+ this._target = null;
7793
+ }
7794
+ this._settle();
7795
+ };
7796
+ this._handleUserScroll = (e) => {
7797
+ if (e.type === "wheel" && e.deltaY >= 0) return;
7798
+ if (e.type === "keydown" && !UP_KEYS.has(e.key)) return;
7799
+ this._target = null;
7800
+ if (e.type === "pointerdown" || e.type === "touchstart") this._holding = true;
7801
+ else this._following = false;
7802
+ };
7803
+ this._handleRelease = () => {
7804
+ if (!this._holding) return;
7805
+ this._holding = false;
7806
+ if (this._following) this.viewport.scrollTop = this.viewport.scrollHeight;
7807
+ this._sync();
7808
+ };
7809
+ this._handleButtonClick = () => this.scrollToEnd();
7810
+ this._handleMutations = (records) => {
7811
+ for (const record of records) {
7812
+ record.addedNodes.forEach((node) => {
7813
+ if (node instanceof Element) this._resizeObserver?.observe(node);
7814
+ });
7815
+ record.removedNodes.forEach((node) => {
7816
+ if (node instanceof Element) this._resizeObserver?.unobserve(node);
7817
+ });
7818
+ }
7819
+ const previous = this._last;
7820
+ this._last = this.viewport.lastElementChild;
7821
+ if (!this._opened) return;
7822
+ if (!this._following && this._target === null) this._restore();
7823
+ if (!this.options.autoScroll) return;
7824
+ if (previous && previous.parentElement !== this.viewport) return;
7825
+ for (let item = this._last; item && item !== previous; item = item.previousElementSibling) {
7826
+ if (item.hasAttribute("data-scroll-anchor")) {
7827
+ this._anchor = item;
7828
+ this._updateSpacer();
7829
+ this._following = true;
7830
+ this._scrollTo(this._top(item) - this.options.peek);
7831
+ return;
7832
+ }
7833
+ }
7834
+ };
7835
+ this._handleResize = () => {
7836
+ if (!this._opened) {
7837
+ if (this.viewport.clientHeight > 0) this._open();
7838
+ return;
7839
+ }
7840
+ this._updateSpacer();
7841
+ const v = this.viewport;
7842
+ if (this._target === "end") this._scrollTo("end");
7843
+ else if (this._target === null) {
7844
+ if (this._following && !this._holding) v.scrollTop = v.scrollHeight;
7845
+ else this._restore();
7846
+ }
7847
+ this._sync();
7848
+ };
7849
+ this.el["Expressive_MessageScroller"] = this;
7850
+ this.options = {
7851
+ ..._MessageScroller.defaults,
7852
+ ...options
7853
+ };
7854
+ this.viewport = el.querySelector(":scope > .message-scroller-viewport");
7855
+ this.button = el.querySelector(":scope > .message-scroller-button");
7856
+ if (!this.viewport) {
7857
+ console.error("MessageScroller needs a .message-scroller-viewport child.");
7858
+ return;
7859
+ }
7860
+ this._last = this.viewport.lastElementChild;
7861
+ if (this.viewport.clientHeight > 0) this._open();
7862
+ if (typeof ResizeObserver !== "undefined") {
7863
+ this._resizeObserver = new ResizeObserver(this._handleResize);
7864
+ this._resizeObserver.observe(this.viewport, { box: "border-box" });
7865
+ for (const item of Array.from(this.viewport.children)) this._resizeObserver.observe(item);
7866
+ }
7867
+ this._mutationObserver = new MutationObserver(this._handleMutations);
7868
+ this._mutationObserver.observe(this.viewport, { childList: true });
7869
+ this.viewport.addEventListener("scroll", this._handleScroll, { passive: true });
7870
+ for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
7871
+ this.viewport.addEventListener(type, this._handleUserScroll, { passive: true });
7872
+ }
7873
+ for (const type of RELEASE_EVENTS) document.addEventListener(type, this._handleRelease);
7874
+ this.button?.addEventListener("click", this._handleButtonClick);
7875
+ }
7876
+ static get defaults() {
7877
+ return _defaults20;
7878
+ }
7879
+ /**
7880
+ * Initializes instances of MessageScroller.
7881
+ * @param els HTML elements.
7882
+ * @param options Component options.
7883
+ */
7884
+ static init(els, options = {}) {
7885
+ return super.init(els, options, _MessageScroller);
7886
+ }
7887
+ static getInstance(el) {
7888
+ return el["Expressive_MessageScroller"];
7889
+ }
7890
+ destroy() {
7891
+ this.el["Expressive_MessageScroller"] = void 0;
7892
+ if (!this.viewport) return;
7893
+ this._resizeObserver?.disconnect();
7894
+ this._mutationObserver?.disconnect();
7895
+ this.viewport.removeEventListener("scroll", this._handleScroll);
7896
+ for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
7897
+ this.viewport.removeEventListener(type, this._handleUserScroll);
7898
+ }
7899
+ for (const type of RELEASE_EVENTS) document.removeEventListener(type, this._handleRelease);
7900
+ this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
7901
+ if (this.button) {
7902
+ this.button.removeEventListener("click", this._handleButtonClick);
7903
+ this.button.inert = false;
7904
+ }
7905
+ }
7906
+ /** Scrolls to the latest message and resumes following. */
7907
+ scrollToEnd() {
7908
+ this._following = this.options.autoScroll;
7909
+ this._scrollTo("end");
7910
+ }
7911
+ /** Scrolls to the first message. */
7912
+ scrollToStart() {
7913
+ this._following = false;
7914
+ this._scrollTo(0);
7915
+ }
7916
+ /**
7917
+ * Scrolls the element with this id to the top of the viewport, below the
7918
+ * peek, and stops following.
7919
+ * @returns false when no such element is inside the viewport.
7920
+ */
7921
+ scrollToMessage(id) {
7922
+ const target = Utils.getElementById(this.viewport, id);
7923
+ if (!target || !this.viewport.contains(target)) return false;
7924
+ this._following = false;
7925
+ this._scrollTo(this._top(target) - this.options.peek);
7926
+ return true;
7927
+ }
7928
+ _open() {
7929
+ this._opened = true;
7930
+ const v = this.viewport;
7931
+ const position = this.options.defaultScrollPosition;
7932
+ const anchors = v.querySelectorAll(":scope > [data-scroll-anchor]");
7933
+ const anchor = position === "last-anchor" ? anchors[anchors.length - 1] : null;
7934
+ if (position === "start") v.scrollTop = 0;
7935
+ else if (anchor && this._contentEnd() - this._top(anchor) + this.options.peek > v.clientHeight) {
7936
+ v.scrollTop = this._top(anchor) - this.options.peek;
7937
+ } else v.scrollTop = v.scrollHeight;
7938
+ this._settle();
7939
+ }
7940
+ /**
7941
+ * Records where the reader is once no programmatic scroll is running.
7942
+ * Content can grow between a scroll and the resize that re-pins the end,
7943
+ * so only moving toward the start releases following.
7944
+ */
7945
+ _settle() {
7946
+ const top = this.viewport.scrollTop;
7947
+ const kept = this._following && top >= this._lastTop - EDGE;
7948
+ this._following = this.options.autoScroll && (kept || this._atEnd());
7949
+ this._lastTop = top;
7950
+ this._capture();
7951
+ this._sync();
7952
+ }
7953
+ _scrollTo(target) {
7954
+ const v = this.viewport;
7955
+ if (Math.abs(v.scrollTop - this._resolve(target)) <= EDGE) {
7956
+ this._target = null;
7957
+ this._settle();
7958
+ return;
7959
+ }
7960
+ this._target = target;
7961
+ this._distance = Infinity;
7962
+ const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
7963
+ v.scrollTo({
7964
+ top: target === "end" ? v.scrollHeight : target,
7965
+ behavior: reduce ? "auto" : "smooth"
7966
+ });
7967
+ }
7968
+ _resolve(target) {
7969
+ const max = this.viewport.scrollHeight - this.viewport.clientHeight;
7970
+ return target === "end" ? max : Math.max(0, Math.min(target, max));
7971
+ }
7972
+ _atEnd() {
7973
+ const v = this.viewport;
7974
+ return v.scrollHeight - v.scrollTop - v.clientHeight <= EDGE;
7975
+ }
7976
+ /** Offset of an element from the top of the scrolled content. */
7977
+ _top(el) {
7978
+ const v = this.viewport;
7979
+ return el.getBoundingClientRect().top - v.getBoundingClientRect().top - v.clientTop + v.scrollTop;
7980
+ }
7981
+ /** Bottom of the content without the spacer, padding included. */
7982
+ _contentEnd() {
7983
+ const last = this.viewport.lastElementChild;
7984
+ if (!last) return 0;
7985
+ const padding = parseFloat(getComputedStyle(this.viewport).paddingBlockEnd) - this._spacer;
7986
+ return this._top(last) + last.getBoundingClientRect().height + padding;
7987
+ }
7988
+ /** Remembers the first visible message so prepends can keep it in place. */
7989
+ _capture() {
7990
+ const items = this.viewport.children;
7991
+ const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
7992
+ let low = 0;
7993
+ let high = items.length - 1;
7994
+ while (low < high) {
7995
+ const mid = low + high >> 1;
7996
+ if (items[mid].getBoundingClientRect().bottom > top) high = mid;
7997
+ else low = mid + 1;
7998
+ }
7999
+ this._ref = items[low] ?? null;
8000
+ this._refOffset = this._ref ? this._ref.getBoundingClientRect().top - top : 0;
8001
+ }
8002
+ /** Moves the remembered message back to where the reader last saw it. */
8003
+ _restore() {
8004
+ if (this._ref?.parentElement !== this.viewport) {
8005
+ this._capture();
8006
+ return;
8007
+ }
8008
+ const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
8009
+ const delta = this._ref.getBoundingClientRect().top - top - this._refOffset;
8010
+ if (Math.abs(delta) >= 1) this.viewport.scrollTop += delta;
8011
+ }
8012
+ /** Sizes the blank space that lets the latest anchored turn reach the top. */
8013
+ _updateSpacer() {
8014
+ let space = 0;
8015
+ if (this._anchor?.parentElement === this.viewport) {
8016
+ const room = this._top(this._anchor) - this.options.peek + this.viewport.clientHeight;
8017
+ space = Math.max(0, Math.ceil(room - this._contentEnd()));
8018
+ } else this._anchor = null;
8019
+ if (space === this._spacer) return;
8020
+ this._spacer = space;
8021
+ if (space) this.viewport.style.setProperty("--md-comp-message-scroller-spacer", `${space}px`);
8022
+ else this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
8023
+ }
8024
+ /** The button is inert while there is nothing below. */
8025
+ _sync() {
8026
+ if (!this.button) return;
8027
+ const idle = this._atEnd();
8028
+ const root = this.button.getRootNode();
8029
+ if (idle && root.activeElement === this.button) this.viewport.focus({ preventScroll: true });
8030
+ this.button.inert = idle;
8031
+ }
8032
+ };
8033
+
8034
+ // src/ts/components/richTextEditor.ts
8035
+ var _defaults21 = {
8036
+ Editor: null,
8037
+ extensions: []
8038
+ };
8039
+ var NAMING = ["aria-label", "aria-labelledby", "aria-describedby"];
8040
+ var RichTextEditor = class _RichTextEditor extends Component {
8041
+ constructor(el, options) {
8042
+ super(el, options, _RichTextEditor);
8043
+ /** The Tiptap editor. */
8044
+ this.editor = null;
8045
+ /** The element the editor is mounted in. */
8046
+ this.content = null;
8047
+ this._actions = /* @__PURE__ */ new Map();
8048
+ this._label = null;
8049
+ this._handleClick = (e) => {
8050
+ const button = e.target.closest("button[data-command]");
8051
+ const action = this._actions.get(button);
8052
+ if (action && !button.disabled) this.editor.chain().focus()[action.command](action.attributes).run();
8053
+ };
8054
+ this._handleLabelClick = () => {
8055
+ this.editor.chain().focus().run();
8056
+ };
8057
+ this._handleUpdate = () => {
8058
+ this.textarea.value = this.editor.isEmpty ? "" : this.editor.getHTML();
8059
+ this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
8060
+ };
8061
+ this._handleTransaction = () => {
8062
+ const can = this.editor.isEditable && this.editor.can();
8063
+ for (const [button, { command, attributes, active }] of this._actions) {
8064
+ button.disabled = !can || !(command in this.editor.commands) || !can[command](attributes);
8065
+ if (!active) continue;
8066
+ const on = this.editor.isActive(active, attributes);
8067
+ button.setAttribute("aria-pressed", String(on));
8068
+ button.classList.toggle("active", on);
8069
+ }
8070
+ };
8071
+ this.el["Expressive_RichTextEditor"] = this;
8072
+ this.options = {
8073
+ ..._RichTextEditor.defaults,
8074
+ ...options
8075
+ };
8076
+ this.textarea = el.querySelector(":scope > textarea");
8077
+ if (!this.textarea || !this.options.Editor) {
8078
+ console.error("RichTextEditor needs a <textarea> child and the Tiptap Editor class in options.Editor.");
8079
+ return;
8080
+ }
8081
+ for (const button of el.querySelectorAll("button[data-command]")) {
8082
+ const command = button.dataset.command;
8083
+ const attributes2 = button.dataset.attributes ? JSON.parse(button.dataset.attributes) : void 0;
8084
+ const active = command.startsWith("toggle") ? command[6].toLowerCase() + command.slice(7) : void 0;
8085
+ this._actions.set(button, { command, attributes: attributes2, active });
8086
+ }
8087
+ const attributes = { role: "textbox", "aria-multiline": "true" };
8088
+ for (const name of NAMING) {
8089
+ const value = this.textarea.getAttribute(name);
8090
+ if (value) attributes[name] = value;
8091
+ }
8092
+ this._label = this.textarea.labels?.[0] ?? null;
8093
+ if (this._label && !attributes["aria-labelledby"]) {
8094
+ if (!this._label.id) this._label.id = `rich-text-editor-label-${Utils.guid()}`;
8095
+ attributes["aria-labelledby"] = this._label.id;
8096
+ this._label.addEventListener("click", this._handleLabelClick);
8097
+ }
8098
+ this.content = document.createElement("div");
8099
+ this.content.className = "rich-text-editor-content";
8100
+ this.textarea.after(this.content);
8101
+ this.textarea.hidden = true;
8102
+ this.editor = new this.options.Editor({
8103
+ element: this.content,
8104
+ extensions: this.options.extensions,
8105
+ content: this.textarea.value,
8106
+ editable: !(this.textarea.disabled || this.textarea.readOnly),
8107
+ editorProps: { attributes }
8108
+ });
8109
+ this.editor.on("update", this._handleUpdate);
8110
+ this.editor.on("transaction", this._handleTransaction);
8111
+ this._handleTransaction();
8112
+ el.addEventListener("click", this._handleClick);
8113
+ }
8114
+ static get defaults() {
8115
+ return _defaults21;
8116
+ }
8117
+ /**
8118
+ * Initializes instances of RichTextEditor.
8119
+ * @param els HTML elements.
8120
+ * @param options Component options.
8121
+ */
8122
+ static init(els, options = {}) {
8123
+ return super.init(els, options, _RichTextEditor);
8124
+ }
8125
+ static getInstance(el) {
8126
+ return el["Expressive_RichTextEditor"];
8127
+ }
8128
+ destroy() {
8129
+ this.el["Expressive_RichTextEditor"] = void 0;
8130
+ if (!this.editor) return;
8131
+ this.el.removeEventListener("click", this._handleClick);
8132
+ this._label?.removeEventListener("click", this._handleLabelClick);
8133
+ this.editor.off("update", this._handleUpdate);
8134
+ this.editor.off("transaction", this._handleTransaction);
8135
+ this.editor.destroy();
8136
+ this.editor = null;
8137
+ this.content.remove();
8138
+ this.textarea.hidden = false;
8139
+ for (const button of this._actions.keys()) {
8140
+ button.disabled = false;
8141
+ button.classList.remove("active");
8142
+ button.removeAttribute("aria-pressed");
8143
+ }
8144
+ }
8145
+ };
8146
+
8147
+ // src/ts/components/mapView.ts
8148
+ var OPENFREEMAP = "https://tiles.openfreemap.org/styles/";
8149
+ var _defaults22 = {
8150
+ maplibregl: null,
8151
+ style: { light: `${OPENFREEMAP}positron`, dark: `${OPENFREEMAP}dark` },
8152
+ center: [0, 20],
8153
+ zoom: 1,
8154
+ mapOptions: {}
8155
+ };
8156
+ var lngLat = (el) => el.dataset.lngLat.split(",").map(Number);
8157
+ function arc([x0, y0], [x2, y2], steps = 32) {
8158
+ const bow = x2 >= x0 ? 0.25 : -0.25;
8159
+ const x1 = (x0 + x2) / 2 - (y2 - y0) * bow;
8160
+ const y1 = (y0 + y2) / 2 + (x2 - x0) * bow;
8161
+ return Array.from({ length: steps + 1 }, (_, i) => {
8162
+ const t = i / steps, u = 1 - t;
8163
+ return [u * u * x0 + 2 * u * t * x1 + t * t * x2, u * u * y0 + 2 * u * t * y1 + t * t * y2];
8164
+ });
8165
+ }
8166
+ var MapView = class _MapView extends Component {
8167
+ constructor(el, options) {
8168
+ super(el, options, _MapView);
8169
+ /** The MapLibre map. */
8170
+ this.map = null;
8171
+ /** The MapLibre markers made from `.map-marker` children, in order. */
8172
+ this.markers = [];
8173
+ this._children = [];
8174
+ this._popups = [];
8175
+ this._markerPopups = /* @__PURE__ */ new Map();
8176
+ this._overlays = /* @__PURE__ */ new Map();
8177
+ this._styleReady = false;
8178
+ this._dark = false;
8179
+ this._location = null;
8180
+ this._media = null;
8181
+ this._observer = null;
8182
+ // Looked up once: `move` fires every frame of a pan or zoom.
8183
+ this._zoomIn = [];
8184
+ this._zoomOut = [];
8185
+ this._handleStyleLoad = () => {
8186
+ this._styleReady = true;
8187
+ for (const overlay2 of this._overlays.values()) overlay2.draw();
8188
+ };
8189
+ this._handleScheme = () => {
8190
+ const dark = this._isDark();
8191
+ if (dark === this._dark) return;
8192
+ this._dark = dark;
8193
+ this._styleReady = false;
8194
+ this.map.setStyle(this._style(), { diff: false });
8195
+ };
8196
+ this._sync = () => {
8197
+ const zoom = this.map.getZoom();
8198
+ for (const button of this._zoomIn) button.disabled = zoom >= this.map.getMaxZoom();
8199
+ for (const button of this._zoomOut) button.disabled = zoom <= this.map.getMinZoom();
8200
+ this.el.style.setProperty("--md-comp-map-bearing", `${-this.map.getBearing()}deg`);
8201
+ };
8202
+ this._handleFullscreen = () => {
8203
+ for (const button of this._buttons("fullscreen")) {
8204
+ button.setAttribute("aria-pressed", String(document.fullscreenElement === this.el));
8205
+ }
8206
+ };
8207
+ this._handleClick = (e) => {
8208
+ const target = e.target;
8209
+ const button = target.closest(".map-controls [data-action]");
8210
+ const action = button?.dataset.action;
8211
+ if (action === "zoom-in") this.map.zoomIn();
8212
+ else if (action === "zoom-out") this.map.zoomOut();
8213
+ else if (action === "compass") this.map.resetNorthPitch();
8214
+ else if (action === "locate") this._locate(button);
8215
+ else if (action === "fullscreen") {
8216
+ if (document.fullscreenElement === this.el) document.exitFullscreen();
8217
+ else this.el.requestFullscreen();
8218
+ } else {
8219
+ const marker = target.closest(".map-marker");
8220
+ if (this._markerPopups.has(marker)) this._toggle(marker);
8221
+ }
8222
+ };
8223
+ // A click on the map closes marker popups, but not the one being toggled.
8224
+ this._handleMapClick = (e) => {
8225
+ const target = e.originalEvent.target;
8226
+ for (const [marker, popup] of this._markerPopups) {
8227
+ if (!marker.contains(target) && !popup.getElement()?.contains(target)) popup.remove();
8228
+ }
8229
+ };
8230
+ this._handleKeydown = (e) => {
8231
+ if (e.key !== "Escape") return;
8232
+ for (const popup of this._popups) {
8233
+ if (popup.getElement()?.contains(e.target)) {
8234
+ e.preventDefault();
8235
+ popup.remove();
8236
+ }
8237
+ }
8238
+ };
8239
+ this.el["Expressive_MapView"] = this;
8240
+ this.options = {
8241
+ ..._MapView.defaults,
8242
+ ...options
8243
+ };
8244
+ const lib = this.options.maplibregl;
8245
+ if (!lib) {
8246
+ console.error("MapView needs the maplibre-gl module in options.maplibregl.");
8247
+ return;
8248
+ }
8249
+ this._children = [...el.children];
8250
+ this._zoomIn = [...this._buttons("zoom-in")];
8251
+ this._zoomOut = [...this._buttons("zoom-out")];
8252
+ this._dark = this._isDark();
8253
+ this.map = new lib.Map({
8254
+ ...this.options.mapOptions,
8255
+ container: el,
8256
+ style: this._style(),
8257
+ center: this.options.center,
8258
+ zoom: this.options.zoom
8259
+ });
8260
+ this.map.on("style.load", this._handleStyleLoad);
8261
+ this.map.on("move", this._sync);
8262
+ this.map.on("click", this._handleMapClick);
8263
+ const children = this._children.filter((child) => child instanceof HTMLElement);
8264
+ const claimed = /* @__PURE__ */ new Set();
8265
+ for (const marker of children.filter((child) => child.matches(".map-marker[data-lng-lat]"))) {
8266
+ const target = marker.hasAttribute("aria-controls") ? Utils.getElementById(marker, marker.getAttribute("aria-controls")) : null;
8267
+ const popup = target?.parentElement === el && target.matches(".map-popup") ? target : null;
8268
+ if (popup) claimed.add(popup);
8269
+ this._addMarker(marker, popup);
8270
+ }
8271
+ for (const popup of children.filter((child) => child.matches(".map-popup[data-lng-lat]") && !claimed.has(child))) {
8272
+ this._popup(popup, 0).setLngLat(lngLat(popup)).addTo(this.map);
8273
+ }
8274
+ if (this._themed()) {
8275
+ this._media = matchMedia("(prefers-color-scheme: dark)");
8276
+ this._media.addEventListener("change", this._handleScheme);
8277
+ this._observer = new MutationObserver(this._handleScheme);
8278
+ this._observer.observe(document.documentElement, { attributes: true, attributeFilter: ["theme"], subtree: true });
8279
+ }
8280
+ for (const button of this._buttons("fullscreen")) button.hidden = !document.fullscreenEnabled;
8281
+ document.addEventListener("fullscreenchange", this._handleFullscreen);
8282
+ el.addEventListener("click", this._handleClick);
8283
+ el.addEventListener("keydown", this._handleKeydown);
8284
+ this._sync();
8285
+ }
8286
+ static get defaults() {
8287
+ return _defaults22;
8288
+ }
8289
+ /**
8290
+ * Initializes instances of MapView.
8291
+ * @param els HTML elements.
8292
+ * @param options Component options.
8293
+ */
8294
+ static init(els, options = {}) {
8295
+ return super.init(els, options, _MapView);
8296
+ }
8297
+ static getInstance(el) {
8298
+ return el["Expressive_MapView"];
8299
+ }
8300
+ destroy() {
8301
+ this.el["Expressive_MapView"] = void 0;
8302
+ if (!this.map) return;
8303
+ this.el.removeEventListener("click", this._handleClick);
8304
+ this.el.removeEventListener("keydown", this._handleKeydown);
8305
+ document.removeEventListener("fullscreenchange", this._handleFullscreen);
8306
+ this._media?.removeEventListener("change", this._handleScheme);
8307
+ this._observer?.disconnect();
8308
+ for (const overlay2 of this._overlays.values()) overlay2.off?.();
8309
+ for (const popup of this._popups) popup.remove();
8310
+ for (const marker of this.markers) {
8311
+ marker.remove();
8312
+ const element = marker.getElement();
8313
+ element.classList.remove(...[...element.classList].filter((name) => name.startsWith("maplibregl-")));
8314
+ element.style.removeProperty("transform");
8315
+ element.style.removeProperty("opacity");
8316
+ if (element.hasAttribute("aria-controls")) element.removeAttribute("aria-expanded");
8317
+ }
8318
+ this._location?.remove();
8319
+ this._location = null;
8320
+ this.map.remove();
8321
+ this.map = null;
8322
+ this.markers = [];
8323
+ this._popups = [];
8324
+ this._markerPopups.clear();
8325
+ this._overlays.clear();
8326
+ this.el.style.removeProperty("--md-comp-map-bearing");
8327
+ for (const button of this.el.querySelectorAll(".map-controls [data-action]")) {
8328
+ button.disabled = false;
8329
+ button.hidden = false;
8330
+ button.removeAttribute("aria-pressed");
8331
+ }
8332
+ this.el.append(...this._children);
8333
+ }
8334
+ /** Draws a line through `[longitude, latitude]` points. Replaces an overlay with the same id. */
8335
+ addRoute(id, coordinates, options = {}) {
8336
+ this._line(id, { type: "LineString", coordinates }, "route", { width: 4, ...options });
8337
+ }
8338
+ /** Draws a curved line for each `{ from, to }`. Replaces an overlay with the same id. */
8339
+ addArcs(id, arcs, options = {}) {
8340
+ this._line(id, { type: "MultiLineString", coordinates: arcs.map(({ from, to }) => arc(from, to)) }, "arc", { width: 2, ...options });
8341
+ }
8342
+ /**
8343
+ * Draws GeoJSON points, or the URL of a GeoJSON file, as clusters that
8344
+ * show their count and zoom in when clicked. The layers are
8345
+ * `${id}-clusters`, `${id}-count` and `${id}-points`.
8346
+ */
8347
+ addClusters(id, data, options = {}) {
8348
+ const clusters = `${id}-clusters`, points = `${id}-points`;
8349
+ const zoomIn = async (e) => {
8350
+ const [feature] = e.features;
8351
+ const zoom = await this.map.getSource(id).getClusterExpansionZoom(feature.properties.cluster_id);
8352
+ this.map.easeTo({ center: feature.geometry.coordinates, zoom });
8353
+ };
8354
+ const pointer = () => {
8355
+ this.map.getCanvas().style.cursor = "pointer";
8356
+ };
8357
+ const reset = () => {
8358
+ this.map.getCanvas().style.cursor = "";
8359
+ };
8360
+ this._draw(id, {
8361
+ draw: () => {
8362
+ const color = options.color ?? this._color("var(--md-comp-map-cluster-color)");
8363
+ const surface = this._color("var(--md-comp-map-marker-ring-color)");
8364
+ this.map.addSource(id, {
8365
+ type: "geojson",
8366
+ data,
8367
+ cluster: true,
8368
+ clusterRadius: options.radius ?? 50,
8369
+ clusterMaxZoom: options.maxZoom ?? 14
8370
+ });
8371
+ this.map.addLayer({
8372
+ id: clusters,
8373
+ type: "circle",
8374
+ source: id,
8375
+ filter: ["has", "point_count"],
8376
+ paint: {
8377
+ "circle-color": color,
8378
+ "circle-radius": ["step", ["get", "point_count"], 16, 10, 20, 100, 26],
8379
+ "circle-stroke-width": 6,
8380
+ "circle-stroke-color": color,
8381
+ "circle-stroke-opacity": 0.3
8382
+ }
8383
+ });
8384
+ const style = this.map.getStyle();
8385
+ const font = style.glyphs && style.layers.find((layer) => layer.layout?.["text-font"])?.layout["text-font"];
8386
+ if (font) {
8387
+ this.map.addLayer({
8388
+ id: `${id}-count`,
8389
+ type: "symbol",
8390
+ source: id,
8391
+ filter: ["has", "point_count"],
8392
+ layout: { "text-field": ["get", "point_count_abbreviated"], "text-font": font, "text-size": 12 },
8393
+ paint: { "text-color": this._color("var(--md-comp-map-cluster-label-color)") }
8394
+ });
8395
+ }
8396
+ this.map.addLayer({
8397
+ id: points,
8398
+ type: "circle",
8399
+ source: id,
8400
+ filter: ["!", ["has", "point_count"]],
8401
+ paint: { "circle-color": color, "circle-radius": 6, "circle-stroke-width": 2, "circle-stroke-color": surface }
8402
+ });
8403
+ },
8404
+ off: () => {
8405
+ this.map.off("click", clusters, zoomIn);
8406
+ for (const layer of [clusters, points]) {
8407
+ this.map.off("mouseenter", layer, pointer);
8408
+ this.map.off("mouseleave", layer, reset);
8409
+ }
8410
+ }
8411
+ });
8412
+ this.map.on("click", clusters, zoomIn);
8413
+ for (const layer of [clusters, points]) {
8414
+ this.map.on("mouseenter", layer, pointer);
8415
+ this.map.on("mouseleave", layer, reset);
8416
+ }
8417
+ }
8418
+ /** Removes a route, arcs or clusters added with this id. */
8419
+ removeOverlay(id) {
8420
+ const overlay2 = this._overlays.get(id);
8421
+ if (!overlay2) return;
8422
+ this._overlays.delete(id);
8423
+ overlay2.off?.();
8424
+ if (!this._styleReady || !this.map.getSource(id)) return;
8425
+ for (const layer of this.map.getStyle().layers) if (layer.source === id) this.map.removeLayer(layer.id);
8426
+ this.map.removeSource(id);
8427
+ }
8428
+ _line(id, geometry, token, options) {
8429
+ this._draw(id, {
8430
+ draw: () => {
8431
+ this.map.addSource(id, { type: "geojson", data: { type: "Feature", properties: {}, geometry } });
8432
+ this.map.addLayer({
8433
+ id,
8434
+ type: "line",
8435
+ source: id,
8436
+ layout: { "line-cap": "round", "line-join": "round" },
8437
+ paint: {
8438
+ "line-color": options.color ?? this._color(`var(--md-comp-map-${token}-color)`),
8439
+ "line-width": options.width,
8440
+ "line-opacity": options.opacity ?? 1,
8441
+ ...options.dashArray && { "line-dasharray": options.dashArray }
8442
+ }
8443
+ });
8444
+ }
8445
+ });
8446
+ }
8447
+ // Sources and layers go with the style, so every overlay is drawn again
8448
+ // when a theme change swaps it.
8449
+ _draw(id, overlay2) {
8450
+ this.removeOverlay(id);
8451
+ this._overlays.set(id, overlay2);
8452
+ if (this._styleReady) overlay2.draw();
8453
+ }
8454
+ _addMarker(element, content) {
8455
+ const marker = new this.options.maplibregl.Marker({
8456
+ element,
8457
+ draggable: element.hasAttribute("data-draggable")
8458
+ }).setLngLat(lngLat(element)).addTo(this.map);
8459
+ marker.on("dragend", () => {
8460
+ const { lng, lat } = marker.getLngLat();
8461
+ element.dataset.lngLat = `${lng},${lat}`;
8462
+ element.dispatchEvent(new Event("change", { bubbles: true }));
8463
+ });
8464
+ this.markers.push(marker);
8465
+ if (!content) return;
8466
+ element.setAttribute("aria-expanded", "false");
8467
+ const popup = this._popup(content, 16);
8468
+ popup.on("open", () => element.setAttribute("aria-expanded", "true"));
8469
+ popup.on("close", () => {
8470
+ element.setAttribute("aria-expanded", "false");
8471
+ if (!document.activeElement || document.activeElement === document.body) element.focus();
8472
+ });
8473
+ this._markerPopups.set(element, popup);
8474
+ }
8475
+ _popup(content, offset) {
8476
+ const popup = new this.options.maplibregl.Popup({ closeOnClick: false, maxWidth: "none", offset }).setDOMContent(content);
8477
+ this._popups.push(popup);
8478
+ return popup;
8479
+ }
8480
+ _toggle(marker) {
8481
+ const popup = this._markerPopups.get(marker);
8482
+ if (popup.isOpen()) popup.remove();
8483
+ else popup.setLngLat(this.markers.find((m) => m.getElement() === marker).getLngLat()).addTo(this.map);
8484
+ }
8485
+ _buttons(action) {
8486
+ return this.el.querySelectorAll(`.map-controls [data-action="${action}"]`);
8487
+ }
8488
+ _locate(button) {
8489
+ navigator.geolocation?.getCurrentPosition(
8490
+ ({ coords }) => {
8491
+ if (!this.map) return;
8492
+ const center = [coords.longitude, coords.latitude];
8493
+ if (!this._location) {
8494
+ const dot = document.createElement("span");
8495
+ dot.className = "map-marker location";
8496
+ dot.setAttribute("aria-hidden", "true");
8497
+ this._location = new this.options.maplibregl.Marker({ element: dot });
8498
+ }
8499
+ this._location.setLngLat(center).addTo(this.map);
8500
+ this.map.flyTo({ center, zoom: Math.max(this.map.getZoom(), 14) });
8501
+ },
8502
+ (error) => {
8503
+ if (this.map && error.code === error.PERMISSION_DENIED) button.disabled = true;
8504
+ }
8505
+ );
8506
+ }
8507
+ // Resolves a CSS color where the map is, so light-dark() and scoped
8508
+ // themes apply, then paints it on a canvas to read back the sRGB value.
8509
+ // MapLibre does not parse the oklch() the theme tokens compute to.
8510
+ _color(value) {
8511
+ const probe = document.createElement("span");
8512
+ probe.hidden = true;
8513
+ probe.style.color = value;
8514
+ this.el.append(probe);
8515
+ const context = document.createElement("canvas").getContext("2d", { willReadFrequently: true });
8516
+ context.fillStyle = getComputedStyle(probe).color;
8517
+ probe.remove();
8518
+ context.fillRect(0, 0, 1, 1);
8519
+ const [r, g, b, a] = context.getImageData(0, 0, 1, 1).data;
8520
+ return `rgba(${r}, ${g}, ${b}, ${Math.round(a / 255 * 100) / 100})`;
8521
+ }
8522
+ _isDark() {
8523
+ return this._color("light-dark(black, white)") === "rgba(255, 255, 255, 1)";
8524
+ }
8525
+ _themed() {
8526
+ const style = this.options.style;
8527
+ return typeof style === "object" && "light" in style && "dark" in style;
8528
+ }
8529
+ _style() {
8530
+ return this._themed() ? this.options.style[this._dark ? "dark" : "light"] : this.options.style;
8531
+ }
8532
+ };
8533
+
5598
8534
  // src/ts/components/lightbox.ts
5599
- var _defaults15 = {
8535
+ var _defaults23 = {
5600
8536
  inDuration: 275,
5601
8537
  outDuration: 200,
5602
8538
  onOpenStart: null,
@@ -5745,7 +8681,7 @@ var Expressive = (() => {
5745
8681
  this._setupEventHandlers();
5746
8682
  }
5747
8683
  static get defaults() {
5748
- return _defaults15;
8684
+ return _defaults23;
5749
8685
  }
5750
8686
  /**
5751
8687
  * Initializes instances of Lightbox.
@@ -5951,7 +8887,7 @@ var Expressive = (() => {
5951
8887
 
5952
8888
  // src/ts/components/scrollspy.ts
5953
8889
  var _defaultGetActiveElement = (id) => `a[href="#${id}"]`;
5954
- var _defaults16 = {
8890
+ var _defaults24 = {
5955
8891
  scrollOffset: 200,
5956
8892
  activeClass: "active",
5957
8893
  getActiveElement: _defaultGetActiveElement,
@@ -5987,7 +8923,7 @@ var Expressive = (() => {
5987
8923
  }
5988
8924
  }
5989
8925
  static get defaults() {
5990
- return _defaults16;
8926
+ return _defaults24;
5991
8927
  }
5992
8928
  static init(els, options = {}) {
5993
8929
  return super.init(els, options, _ScrollSpy);
@@ -6021,8 +8957,9 @@ var Expressive = (() => {
6021
8957
  const retained = elements.filter((spy) => spy.options.keepTopElementActive);
6022
8958
  best = _ScrollSpy._nearestAbove(retained) ?? retained[0];
6023
8959
  }
6024
- const next = best ? _ScrollSpy._linkFor(best.el.id, best.options) : null;
6025
8960
  const active = _ScrollSpy._active.get(resolver);
8961
+ if (best && active === best && best._link?.isConnected) return;
8962
+ const next = best ? _ScrollSpy._linkFor(best.el.id, best.options) : null;
6026
8963
  if (active === best && active?._link === next) return;
6027
8964
  _ScrollSpy._clearActive(resolver);
6028
8965
  if (!next) return;
@@ -6077,7 +9014,7 @@ var Expressive = (() => {
6077
9014
  var ScrollSpy = _ScrollSpy;
6078
9015
 
6079
9016
  // src/ts/components/navigationRail.ts
6080
- var _defaults17 = {
9017
+ var _defaults25 = {
6081
9018
  onOpenStart: null,
6082
9019
  onOpenEnd: null,
6083
9020
  onCloseStart: null,
@@ -6164,7 +9101,7 @@ var Expressive = (() => {
6164
9101
  document.addEventListener("keydown", this._onKeyDown);
6165
9102
  }
6166
9103
  static get defaults() {
6167
- return _defaults17;
9104
+ return _defaults25;
6168
9105
  }
6169
9106
  static init(els, options = {}) {
6170
9107
  return super.init(els, options, _NavigationRail);
@@ -6194,7 +9131,7 @@ var Expressive = (() => {
6194
9131
  };
6195
9132
 
6196
9133
  // src/ts/components/tabs.ts
6197
- var _defaults18 = {
9134
+ var _defaults26 = {
6198
9135
  duration: 300,
6199
9136
  onShow: null,
6200
9137
  swipeable: false,
@@ -6207,13 +9144,17 @@ var Expressive = (() => {
6207
9144
  super(el, options, _Tabs);
6208
9145
  this._originalPanels = /* @__PURE__ */ new Map();
6209
9146
  this._originalLinks = /* @__PURE__ */ new Map();
6210
- this._handleWindowResize = () => {
9147
+ // Throttled like Carousel's: every bar re-measures on resize, and the left
9148
+ // and right positions are both read before either is written.
9149
+ this._handleWindowResize = Utils.throttle(() => {
6211
9150
  this._setTabsAndTabWidth();
6212
9151
  if (this._tabWidth !== 0 && this._tabsWidth !== 0) {
6213
- this._indicator.style.left = this._calcLeftPos(this._activeTabLink) + "px";
6214
- this._indicator.style.right = this._calcRightPos(this._activeTabLink) + "px";
9152
+ const left = this._calcLeftPos(this._activeTabLink);
9153
+ const right = this._calcRightPos(this._activeTabLink);
9154
+ this._indicator.style.left = left + "px";
9155
+ this._indicator.style.right = right + "px";
6215
9156
  }
6216
- };
9157
+ }, 200);
6217
9158
  this._handleTabClick = (e) => {
6218
9159
  const target = e.target;
6219
9160
  if (!target) return;
@@ -6281,7 +9222,7 @@ var Expressive = (() => {
6281
9222
  this._setupEventHandlers();
6282
9223
  }
6283
9224
  static get defaults() {
6284
- return _defaults18;
9225
+ return _defaults26;
6285
9226
  }
6286
9227
  /**
6287
9228
  * Initializes instances of Tabs.
@@ -6468,7 +9409,7 @@ var Expressive = (() => {
6468
9409
  };
6469
9410
 
6470
9411
  // src/ts/components/timepicker.ts
6471
- var _defaults19 = {
9412
+ var _defaults27 = {
6472
9413
  dialRadius: 135,
6473
9414
  outerRadius: 105,
6474
9415
  innerRadius: 70,
@@ -6664,7 +9605,7 @@ var Expressive = (() => {
6664
9605
  }
6665
9606
  }
6666
9607
  static get defaults() {
6667
- return _defaults19;
9608
+ return _defaults27;
6668
9609
  }
6669
9610
  /**
6670
9611
  * Initializes instances of Timepicker.
@@ -7026,7 +9967,7 @@ var Expressive = (() => {
7026
9967
  var Timepicker = _Timepicker;
7027
9968
 
7028
9969
  // src/ts/components/snackbar.ts
7029
- var _defaults20 = {
9970
+ var _defaults28 = {
7030
9971
  text: "",
7031
9972
  action: "",
7032
9973
  onAction: null,
@@ -7076,7 +10017,7 @@ var Expressive = (() => {
7076
10017
  this._setTimer();
7077
10018
  }
7078
10019
  static get defaults() {
7079
- return _defaults20;
10020
+ return _defaults28;
7080
10021
  }
7081
10022
  static getInstance(el) {
7082
10023
  return el["Expressive_Snackbar"];
@@ -7116,6 +10057,7 @@ var Expressive = (() => {
7116
10057
  snackbar.velocityX = 0;
7117
10058
  snackbar.time = Date.now();
7118
10059
  snackbar.xPos = e.clientX;
10060
+ snackbar._activationDistance = snackbar.el.offsetWidth * snackbar.options.activationPercent;
7119
10061
  }
7120
10062
  }
7121
10063
  static _onDragMove(e) {
@@ -7128,7 +10070,7 @@ var Expressive = (() => {
7128
10070
  snackbar.velocityX = snackbar.deltaX / (Date.now() - snackbar.time);
7129
10071
  snackbar.time = Date.now();
7130
10072
  const totalDeltaX = snackbar.xPos - snackbar.startingXPos;
7131
- const activationDistance = snackbar.el.offsetWidth * snackbar.options.activationPercent;
10073
+ const activationDistance = snackbar._activationDistance;
7132
10074
  snackbar.el.style.transform = `translateX(${totalDeltaX}px)`;
7133
10075
  snackbar.el.style.opacity = (1 - Math.abs(totalDeltaX / activationDistance)).toString();
7134
10076
  }
@@ -7141,7 +10083,7 @@ var Expressive = (() => {
7141
10083
  snackbar.panning = false;
7142
10084
  snackbar.el.classList.remove("panning");
7143
10085
  const totalDeltaX = snackbar.xPos - snackbar.startingXPos;
7144
- const activationDistance = snackbar.el.offsetWidth * snackbar.options.activationPercent;
10086
+ const activationDistance = snackbar._activationDistance;
7145
10087
  const velocity = Date.now() - snackbar.time < 100 ? snackbar.velocityX : 0;
7146
10088
  const shouldBeDismissed = Math.abs(totalDeltaX) > activationDistance || velocity > 1;
7147
10089
  if (shouldBeDismissed) {
@@ -7373,7 +10315,7 @@ var Expressive = (() => {
7373
10315
  var Snackbar = _Snackbar;
7374
10316
 
7375
10317
  // src/ts/components/tooltip.ts
7376
- var _defaults21 = {
10318
+ var _defaults29 = {
7377
10319
  exitDelay: 200,
7378
10320
  enterDelay: 0,
7379
10321
  text: "",
@@ -7395,6 +10337,7 @@ var Expressive = (() => {
7395
10337
  clearTimeout(this._exitDelayTimeout);
7396
10338
  clearTimeout(this._animationTimeout);
7397
10339
  this.isOpen = true;
10340
+ this.el.ownerDocument.addEventListener("keydown", this._handleKeydown, true);
7398
10341
  this.options = { ...this.options, ...this._getAttributeOptions() };
7399
10342
  this._updateTooltipContent();
7400
10343
  this._setEnterDelayTimeout(isManual);
@@ -7407,6 +10350,7 @@ var Expressive = (() => {
7407
10350
  this.isHovered = false;
7408
10351
  this.isFocused = false;
7409
10352
  this.isOpen = false;
10353
+ this.el.ownerDocument.removeEventListener("keydown", this._handleKeydown, true);
7410
10354
  clearTimeout(this._enterDelayTimeout);
7411
10355
  this._setExitDelayTimeout();
7412
10356
  };
@@ -7455,7 +10399,7 @@ var Expressive = (() => {
7455
10399
  this._setupEventHandlers();
7456
10400
  }
7457
10401
  static get defaults() {
7458
- return _defaults21;
10402
+ return _defaults29;
7459
10403
  }
7460
10404
  /**
7461
10405
  * Initializes instances of Tooltip.
@@ -7536,7 +10480,6 @@ var Expressive = (() => {
7536
10480
  el.addEventListener("focus", this._handleFocus, true);
7537
10481
  el.addEventListener("blur", this._handleBlur, true);
7538
10482
  }
7539
- this.el.ownerDocument.addEventListener("keydown", this._handleKeydown, true);
7540
10483
  }
7541
10484
  _removeEventHandlers() {
7542
10485
  for (const el of [this.el, this.tooltipEl]) {
@@ -7674,11 +10617,33 @@ var Expressive = (() => {
7674
10617
  },
7675
10618
  Carousel: { component: Carousel, selector: ".carousel" },
7676
10619
  Chips: { component: Chips, selector: ".chips" },
10620
+ Confetti: { component: Confetti, selector: ".confetti" },
10621
+ Particles: { component: Particles, selector: "canvas.particles" },
10622
+ ColumnChart: { component: ColumnChart, selector: ".column-chart" },
10623
+ BarChart: { component: BarChart, selector: ".bar-chart" },
10624
+ PieChart: { component: PieChart, selector: ".pie-chart" },
10625
+ HeatmapChart: { component: HeatmapChart, selector: ".heatmap-chart" },
10626
+ RadarChart: { component: RadarChart, selector: ".radar-chart" },
10627
+ RadialChart: { component: RadialChart, selector: ".radial-chart" },
10628
+ MixedChart: { component: MixedChart, selector: ".mixed-chart" },
10629
+ CommandPalette: {
10630
+ component: CommandPalette,
10631
+ selector: "dialog.command-palette"
10632
+ },
10633
+ Questionnaire: {
10634
+ component: Questionnaire,
10635
+ selector: "form.questionnaire"
10636
+ },
7677
10637
  Datepicker: {
7678
10638
  component: Datepicker,
7679
10639
  selector: ".datepicker, .date-picker"
7680
10640
  },
10641
+ LineChart: { component: LineChart, selector: ".line-chart" },
7681
10642
  Menu: { component: Menu, selector: ".menu-trigger" },
10643
+ MessageScroller: {
10644
+ component: MessageScroller,
10645
+ selector: ".message-scroller"
10646
+ },
7682
10647
  Lightbox: { component: Lightbox, selector: ".lightboxed" },
7683
10648
  ScrollSpy: { component: ScrollSpy, selector: ".scrollspy" },
7684
10649
  FormSelect: { component: FormSelect, selector: "select" },
@@ -7712,7 +10677,7 @@ var Expressive = (() => {
7712
10677
  }
7713
10678
 
7714
10679
  // src/ts/modular.ts
7715
- var version = "0.12.0";
10680
+ var version = "0.14.1";
7716
10681
 
7717
10682
  // src/ts/index.ts
7718
10683
  Forms.Init();