@expressivecss/expressive 0.12.0 → 0.13.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 (171) hide show
  1. package/CHANGELOG.md +231 -1
  2. package/README.md +37 -1
  3. package/dist/css/expressive.css +5111 -36
  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-HLDXZ7XB.mjs → chunk-3CLXLPYS.mjs} +2 -2
  11. package/dist/js/chunks/chunk-3DHK5CZU.mjs +105 -0
  12. package/dist/js/chunks/chunk-3DHK5CZU.mjs.map +7 -0
  13. package/dist/js/chunks/chunk-4PWAPV6F.mjs +7 -0
  14. package/dist/js/chunks/chunk-4PWAPV6F.mjs.map +7 -0
  15. package/dist/js/chunks/{chunk-HB6IW7QQ.mjs → chunk-55IA2XUC.mjs} +2 -2
  16. package/dist/js/chunks/chunk-CCWDV5CR.mjs +292 -0
  17. package/dist/js/chunks/chunk-CCWDV5CR.mjs.map +7 -0
  18. package/dist/js/chunks/chunk-DTKR54I6.mjs +270 -0
  19. package/dist/js/chunks/chunk-DTKR54I6.mjs.map +7 -0
  20. package/dist/js/chunks/chunk-GVGT6MIU.mjs +47 -0
  21. package/dist/js/chunks/chunk-GVGT6MIU.mjs.map +7 -0
  22. package/dist/js/chunks/chunk-HKT2S7VM.mjs +48 -0
  23. package/dist/js/chunks/chunk-HKT2S7VM.mjs.map +7 -0
  24. package/dist/js/chunks/chunk-K4CJCRGE.mjs +262 -0
  25. package/dist/js/chunks/chunk-K4CJCRGE.mjs.map +7 -0
  26. package/dist/js/chunks/chunk-LNCCYGPC.mjs +163 -0
  27. package/dist/js/chunks/chunk-LNCCYGPC.mjs.map +7 -0
  28. package/dist/js/chunks/chunk-NQLC3YMW.mjs +1 -0
  29. package/dist/js/chunks/chunk-O5XPWXIQ.mjs +406 -0
  30. package/dist/js/chunks/chunk-O5XPWXIQ.mjs.map +7 -0
  31. package/dist/js/chunks/chunk-P3G3QHII.mjs +166 -0
  32. package/dist/js/chunks/chunk-P3G3QHII.mjs.map +7 -0
  33. package/dist/js/chunks/{chunk-H2WA4XHH.mjs → chunk-PLYXLSH3.mjs} +4 -4
  34. package/dist/js/chunks/chunk-QKWPQK5N.mjs +179 -0
  35. package/dist/js/chunks/chunk-QKWPQK5N.mjs.map +7 -0
  36. package/dist/js/chunks/chunk-RHRQKTTX.mjs +139 -0
  37. package/dist/js/chunks/chunk-RHRQKTTX.mjs.map +7 -0
  38. package/dist/js/chunks/chunk-SPETGK25.mjs +59 -0
  39. package/dist/js/chunks/chunk-SPETGK25.mjs.map +7 -0
  40. package/dist/js/chunks/{chunk-YSBCHYXP.mjs → chunk-U2OTHOTI.mjs} +116 -12
  41. package/dist/js/chunks/chunk-U2OTHOTI.mjs.map +7 -0
  42. package/dist/js/chunks/chunk-VTLBW7QS.mjs +124 -0
  43. package/dist/js/chunks/chunk-VTLBW7QS.mjs.map +7 -0
  44. package/dist/js/chunks/{chunk-RFZZCYVU.mjs → chunk-XGHORT7J.mjs} +2 -2
  45. package/dist/js/chunks/{chunk-HNFSMIBY.mjs → chunk-XH6BEEMY.mjs} +69 -16
  46. package/dist/js/chunks/chunk-XH6BEEMY.mjs.map +7 -0
  47. package/dist/js/expressive.cjs +2209 -32
  48. package/dist/js/expressive.cjs.map +4 -4
  49. package/dist/js/expressive.js +2209 -32
  50. package/dist/js/expressive.js.map +4 -4
  51. package/dist/js/expressive.min.js +10 -10
  52. package/dist/js/expressive.min.js.map +4 -4
  53. package/dist/js/expressive.mjs +68 -20
  54. package/dist/js/expressive.mjs.map +1 -1
  55. package/dist/js/internal/behaviors/forms.mjs +1 -1
  56. package/dist/js/internal/components/autocomplete.mjs +2 -2
  57. package/dist/js/internal/components/barChart.mjs +10 -0
  58. package/dist/js/internal/components/barChart.mjs.map +7 -0
  59. package/dist/js/internal/components/chips.mjs +3 -3
  60. package/dist/js/internal/components/columnChart.mjs +9 -0
  61. package/dist/js/internal/components/columnChart.mjs.map +7 -0
  62. package/dist/js/internal/components/commandPalette.mjs +9 -0
  63. package/dist/js/internal/components/commandPalette.mjs.map +7 -0
  64. package/dist/js/internal/components/datepicker.mjs +3 -3
  65. package/dist/js/internal/components/heatmapChart.mjs +9 -0
  66. package/dist/js/internal/components/heatmapChart.mjs.map +7 -0
  67. package/dist/js/internal/components/index.mjs +65 -17
  68. package/dist/js/internal/components/lineChart.mjs +12 -0
  69. package/dist/js/internal/components/lineChart.mjs.map +7 -0
  70. package/dist/js/internal/components/menu.mjs +1 -1
  71. package/dist/js/internal/components/messageScroller.mjs +9 -0
  72. package/dist/js/internal/components/messageScroller.mjs.map +7 -0
  73. package/dist/js/internal/components/mixedChart.mjs +10 -0
  74. package/dist/js/internal/components/mixedChart.mjs.map +7 -0
  75. package/dist/js/internal/components/pieChart.mjs +9 -0
  76. package/dist/js/internal/components/pieChart.mjs.map +7 -0
  77. package/dist/js/internal/components/questionnaire.mjs +9 -0
  78. package/dist/js/internal/components/questionnaire.mjs.map +7 -0
  79. package/dist/js/internal/components/radarChart.mjs +9 -0
  80. package/dist/js/internal/components/radarChart.mjs.map +7 -0
  81. package/dist/js/internal/components/radialChart.mjs +9 -0
  82. package/dist/js/internal/components/radialChart.mjs.map +7 -0
  83. package/dist/js/internal/components/registry.mjs +23 -11
  84. package/dist/js/internal/components/richTextEditor.mjs +9 -0
  85. package/dist/js/internal/components/richTextEditor.mjs.map +7 -0
  86. package/dist/js/internal/components/select.mjs +2 -2
  87. package/dist/js/modular.mjs +68 -20
  88. package/dist/types/behaviors/forms.d.ts +22 -0
  89. package/dist/types/components/barChart.d.ts +24 -0
  90. package/dist/types/components/columnChart.d.ts +21 -0
  91. package/dist/types/components/commandPalette.d.ts +96 -0
  92. package/dist/types/components/heatmapChart.d.ts +49 -0
  93. package/dist/types/components/index.d.ts +12 -0
  94. package/dist/types/components/lineChart.d.ts +134 -0
  95. package/dist/types/components/menu.d.ts +31 -0
  96. package/dist/types/components/messageScroller.d.ts +111 -0
  97. package/dist/types/components/mixedChart.d.ts +25 -0
  98. package/dist/types/components/pieChart.d.ts +40 -0
  99. package/dist/types/components/questionnaire.d.ts +96 -0
  100. package/dist/types/components/radarChart.d.ts +39 -0
  101. package/dist/types/components/radialChart.d.ts +45 -0
  102. package/dist/types/components/registry.d.ts +44 -0
  103. package/dist/types/components/richTextEditor.d.ts +71 -0
  104. package/dist/types/modular.d.ts +10 -1
  105. package/package.json +4 -1
  106. package/src/sass/README.md +3 -6
  107. package/src/sass/abstracts/_index.scss +1 -1
  108. package/src/sass/abstracts/_mixins.scss +226 -0
  109. package/src/sass/abstracts/_variables.scss +14 -0
  110. package/src/sass/base/_typography.scss +45 -0
  111. package/src/sass/components/_accordion.scss +161 -0
  112. package/src/sass/components/_aura.scss +119 -0
  113. package/src/sass/components/_avatar.scss +86 -0
  114. package/src/sass/components/_bar-chart.scss +285 -0
  115. package/src/sass/components/_bottom-sheet.scss +1 -0
  116. package/src/sass/components/_button-groups.scss +13 -3
  117. package/src/sass/components/_cards.scss +16 -13
  118. package/src/sass/components/_column-chart.scss +134 -0
  119. package/src/sass/components/_command-palette.scss +140 -0
  120. package/src/sass/components/_countdown.scss +89 -0
  121. package/src/sass/components/_data-table.scss +188 -0
  122. package/src/sass/components/_dialog.scss +1 -0
  123. package/src/sass/components/_empty-state.scss +78 -0
  124. package/src/sass/components/_expanding-card.scss +3 -0
  125. package/src/sass/components/_heatmap-chart.scss +156 -0
  126. package/src/sass/components/_index.scss +27 -0
  127. package/src/sass/components/_kpi.scss +162 -0
  128. package/src/sass/components/_line-chart.scss +102 -0
  129. package/src/sass/components/_marquee.scss +111 -0
  130. package/src/sass/components/_menu.scss +10 -0
  131. package/src/sass/components/_message-scroller.scss +66 -0
  132. package/src/sass/components/_message.scss +202 -0
  133. package/src/sass/components/_mixed-chart.scss +157 -0
  134. package/src/sass/components/_navigation-bar.scss +1 -0
  135. package/src/sass/components/_navigation-rail.scss +9 -0
  136. package/src/sass/components/_pie-chart.scss +203 -0
  137. package/src/sass/components/_popover.scss +112 -0
  138. package/src/sass/components/_questionnaire.scss +144 -0
  139. package/src/sass/components/_radar-chart.scss +205 -0
  140. package/src/sass/components/_radial-chart.scss +174 -0
  141. package/src/sass/components/_rich-text-editor.scss +99 -0
  142. package/src/sass/components/_search.scss +1 -0
  143. package/src/sass/components/_side-sheet.scss +1 -0
  144. package/src/sass/components/_skeleton.scss +81 -0
  145. package/src/sass/components/_snackbar.scss +1 -0
  146. package/src/sass/components/_stat.scss +86 -0
  147. package/src/sass/components/_stepper.scss +190 -0
  148. package/src/sass/components/_timeline.scss +114 -0
  149. package/src/sass/components/_toolbar.scss +1 -0
  150. package/src/sass/components/_tooltip.scss +2 -0
  151. package/src/sass/components/_tree.scss +139 -0
  152. package/src/sass/components/forms/_drop-zone.scss +128 -0
  153. package/src/sass/components/forms/_index.scss +2 -0
  154. package/src/sass/components/forms/_input-fields.scss +133 -2
  155. package/src/sass/components/forms/_rating.scss +165 -0
  156. package/src/sass/tokens/_reference.scss +3 -15
  157. package/src/sass/utilities/_index.scss +2 -0
  158. package/src/sass/utilities/_scroll-area.scss +35 -0
  159. package/src/sass/utilities/_shapes.scss +164 -0
  160. package/dist/js/chunks/chunk-G3I5H2U3.mjs +0 -7
  161. package/dist/js/chunks/chunk-G3I5H2U3.mjs.map +0 -7
  162. package/dist/js/chunks/chunk-HNFSMIBY.mjs.map +0 -7
  163. package/dist/js/chunks/chunk-PSSHQWCX.mjs +0 -100
  164. package/dist/js/chunks/chunk-PSSHQWCX.mjs.map +0 -7
  165. package/dist/js/chunks/chunk-WWPAPHMA.mjs +0 -1
  166. package/dist/js/chunks/chunk-YSBCHYXP.mjs.map +0 -7
  167. /package/dist/js/chunks/{chunk-HLDXZ7XB.mjs.map → chunk-3CLXLPYS.mjs.map} +0 -0
  168. /package/dist/js/chunks/{chunk-HB6IW7QQ.mjs.map → chunk-55IA2XUC.mjs.map} +0 -0
  169. /package/dist/js/chunks/{chunk-WWPAPHMA.mjs.map → chunk-NQLC3YMW.mjs.map} +0 -0
  170. /package/dist/js/chunks/{chunk-H2WA4XHH.mjs.map → chunk-PLYXLSH3.mjs.map} +0 -0
  171. /package/dist/js/chunks/{chunk-RFZZCYVU.mjs.map → chunk-XGHORT7J.mjs.map} +0 -0
@@ -23,22 +23,34 @@ 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,
32
35
  Datepicker: () => Datepicker,
33
36
  Dialogs: () => Dialogs,
34
37
  ExpandingCard: () => ExpandingCard,
35
38
  FloatingActionButton: () => FloatingActionButton,
36
39
  FormSelect: () => FormSelect,
37
40
  Forms: () => Forms,
41
+ HeatmapChart: () => HeatmapChart,
38
42
  Lightbox: () => Lightbox,
43
+ LineChart: () => LineChart,
39
44
  Menu: () => Menu,
45
+ MessageScroller: () => MessageScroller,
46
+ MixedChart: () => MixedChart,
40
47
  NavigationRail: () => NavigationRail,
48
+ PieChart: () => PieChart,
49
+ Questionnaire: () => Questionnaire,
50
+ RadarChart: () => RadarChart,
51
+ RadialChart: () => RadialChart,
41
52
  Range: () => Slider,
53
+ RichTextEditor: () => RichTextEditor,
42
54
  ScrollSpy: () => ScrollSpy,
43
55
  SideSheets: () => SideSheets,
44
56
  Slider: () => Slider,
@@ -772,6 +784,7 @@ var Expressive = (() => {
772
784
  coverTrigger: false,
773
785
  closeOnClick: true,
774
786
  hover: false,
787
+ contextMenu: false,
775
788
  inDuration: 150,
776
789
  outDuration: 250,
777
790
  onOpenStart: null,
@@ -787,6 +800,40 @@ var Expressive = (() => {
787
800
  this._pendingTimers = /* @__PURE__ */ new Set();
788
801
  this._transition = 0;
789
802
  this._menuClickTargets = /* @__PURE__ */ new WeakMap();
803
+ /**
804
+ * Viewport point the open context menu belongs to. Null outside a
805
+ * right-click, so a keyboard or programmatic open uses the region's corner
806
+ * rather than an old pointer position.
807
+ */
808
+ this._contextPoint = null;
809
+ /** What had focus when a context menu opened; focus goes back there. */
810
+ this._contextReturn = null;
811
+ this._handleContextMenu = (e) => {
812
+ if (e.defaultPrevented) {
813
+ this.close();
814
+ return;
815
+ }
816
+ const path = e.composedPath();
817
+ if (_Menu._menus.some((menu) => menu !== this && menu.isOpen && path.includes(menu.menuEl))) return;
818
+ e.preventDefault();
819
+ if (!this.isOpen) {
820
+ const root = this.el.getRootNode();
821
+ this._contextReturn = root.activeElement instanceof HTMLElement ? root.activeElement : null;
822
+ }
823
+ const rect = this.el.getBoundingClientRect();
824
+ const fromPointer = (e.clientX !== 0 || e.clientY !== 0) && e.clientX >= rect.left && e.clientX <= rect.right && e.clientY >= rect.top && e.clientY <= rect.bottom;
825
+ this._contextPoint = fromPointer ? { x: e.clientX, y: e.clientY } : null;
826
+ if (this.isOpen) this.recalculateDimensions();
827
+ else this.open();
828
+ };
829
+ this._handleDocumentContextMenu = (e) => {
830
+ const path = e.composedPath();
831
+ if (path.includes(this.menuEl)) {
832
+ e.preventDefault();
833
+ return;
834
+ }
835
+ if (!path.includes(this.el)) this.close();
836
+ };
790
837
  this._handleClick = (e) => {
791
838
  e.preventDefault();
792
839
  if (this.isOpen) {
@@ -833,6 +880,7 @@ var Expressive = (() => {
833
880
  this.isTouchMoving = false;
834
881
  };
835
882
  this._handleTriggerKeydown = (e) => {
883
+ if (this.options.contextMenu) return;
836
884
  const opensMenu = e.key === SPACE || e.key === Utils.keys.ENTER || e.key === Utils.keys.ARROW_DOWN || e.key === Utils.keys.ARROW_UP;
837
885
  if (opensMenu && !this.isOpen) {
838
886
  e.preventDefault();
@@ -991,7 +1039,7 @@ var Expressive = (() => {
991
1039
  this._placeMenu();
992
1040
  this._schedule(() => this._setupTemporaryEventHandlers(), 0);
993
1041
  this._animateIn();
994
- this.el.ariaExpanded = "true";
1042
+ this._setExpanded(true);
995
1043
  };
996
1044
  /**
997
1045
  * Close menu.
@@ -1013,10 +1061,13 @@ var Expressive = (() => {
1013
1061
  if (transition !== this._transition) return;
1014
1062
  this._animateOut();
1015
1063
  this._removeTemporaryEventHandlers();
1016
- this.el.ariaExpanded = "false";
1064
+ this._setExpanded(false);
1017
1065
  if (this.options.autoFocus && this.menuEl.contains(focused) && (root.activeElement === focused || this.el.ownerDocument.activeElement === this.el.ownerDocument.body)) {
1018
- this.el.focus();
1066
+ const back = this._contextReturn;
1067
+ (back?.isConnected && back !== this.el.ownerDocument.body ? back : this.el).focus();
1019
1068
  }
1069
+ this._contextReturn = null;
1070
+ this._contextPoint = null;
1020
1071
  };
1021
1072
  /**
1022
1073
  * While menu is open, you can recalculate its dimensions if its contents have changed.
@@ -1034,6 +1085,7 @@ var Expressive = (() => {
1034
1085
  this._originalInert = this.menuEl?.getAttribute("inert") ?? null;
1035
1086
  this.options = {
1036
1087
  ..._Menu.defaults,
1088
+ contextMenu: el.hasAttribute("data-context-menu"),
1037
1089
  ...options
1038
1090
  };
1039
1091
  this.isOpen = false;
@@ -1041,10 +1093,12 @@ var Expressive = (() => {
1041
1093
  this.isTouchMoving = false;
1042
1094
  this.focusedIndex = -1;
1043
1095
  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);
1096
+ if (!this.options.contextMenu) {
1097
+ this.el.ariaExpanded = "false";
1098
+ if (!this.el.hasAttribute("aria-haspopup")) this.el.setAttribute("aria-haspopup", "menu");
1099
+ if (this.id && !this.el.hasAttribute("aria-controls"))
1100
+ this.el.setAttribute("aria-controls", this.id);
1101
+ }
1048
1102
  this._moveMenuToElement();
1049
1103
  this._setupAccessibility();
1050
1104
  this._makeMenuFocusable();
@@ -1068,7 +1122,7 @@ var Expressive = (() => {
1068
1122
  destroy() {
1069
1123
  this._clearPendingTimers();
1070
1124
  this.isOpen = false;
1071
- this.el.ariaExpanded = "false";
1125
+ this._setExpanded(false);
1072
1126
  clearTimeout(this.filterTimeout);
1073
1127
  this._resetMenuStyles();
1074
1128
  this._restoreInert();
@@ -1084,7 +1138,9 @@ var Expressive = (() => {
1084
1138
  if (this.menuEl?.querySelector(":scope > li > menu")) {
1085
1139
  this.menuEl.addEventListener("mouseover", this._handleSubmenuAlign);
1086
1140
  }
1087
- if (this.options.hover) {
1141
+ if (this.options.contextMenu) {
1142
+ this.el.addEventListener("contextmenu", this._handleContextMenu);
1143
+ } else if (this.options.hover) {
1088
1144
  this.el.addEventListener("mouseenter", this._handleMouseEnter);
1089
1145
  this.el.addEventListener("mouseleave", this._handleMouseLeave);
1090
1146
  this.menuEl.addEventListener("mouseleave", this._handleMouseLeave);
@@ -1096,7 +1152,9 @@ var Expressive = (() => {
1096
1152
  this.el.removeEventListener("keydown", this._handleTriggerKeydown);
1097
1153
  this.menuEl?.removeEventListener("click", this._handleMenuClick);
1098
1154
  this.menuEl?.removeEventListener("mouseover", this._handleSubmenuAlign);
1099
- if (this.options.hover) {
1155
+ if (this.options.contextMenu) {
1156
+ this.el.removeEventListener("contextmenu", this._handleContextMenu);
1157
+ } else if (this.options.hover) {
1100
1158
  this.el.removeEventListener("mouseenter", this._handleMouseEnter);
1101
1159
  this.el.removeEventListener("mouseleave", this._handleMouseLeave);
1102
1160
  this.menuEl?.removeEventListener("mouseleave", this._handleMouseLeave);
@@ -1112,6 +1170,7 @@ var Expressive = (() => {
1112
1170
  });
1113
1171
  this.menuEl?.addEventListener("keydown", this._handleMenuKeydown);
1114
1172
  window.addEventListener("resize", this._handleWindowResize);
1173
+ if (this.options.contextMenu) document.addEventListener("contextmenu", this._handleDocumentContextMenu);
1115
1174
  }
1116
1175
  _removeTemporaryEventHandlers() {
1117
1176
  document.body.removeEventListener("click", this._handleDocumentClick);
@@ -1119,6 +1178,7 @@ var Expressive = (() => {
1119
1178
  document.body.removeEventListener("touchmove", this._handleDocumentTouchmove);
1120
1179
  this.menuEl?.removeEventListener("keydown", this._handleMenuKeydown);
1121
1180
  window.removeEventListener("resize", this._handleWindowResize);
1181
+ document.removeEventListener("contextmenu", this._handleDocumentContextMenu);
1122
1182
  }
1123
1183
  _setupAccessibility() {
1124
1184
  if (!this.menuEl) return;
@@ -1408,8 +1468,12 @@ var Expressive = (() => {
1408
1468
  closestOverflowParent.style.position = "relative";
1409
1469
  const natural = this.menuEl.getBoundingClientRect().width;
1410
1470
  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));
1471
+ 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
1472
  this.menuEl.style.width = idealWidth + "px";
1473
+ if (this.options.contextMenu) {
1474
+ this._placeMenuAtPoint(this._contextPoint ?? this._regionCorner(), closestOverflowParent);
1475
+ return;
1476
+ }
1413
1477
  const positionInfo = this._getMenuPosition(closestOverflowParent);
1414
1478
  this.menuEl.style.left = positionInfo.x + "px";
1415
1479
  this.menuEl.style.top = positionInfo.y + "px";
@@ -1417,6 +1481,58 @@ var Expressive = (() => {
1417
1481
  this.menuEl.style.width = positionInfo.width + "px";
1418
1482
  this.menuEl.style.transformOrigin = `${positionInfo.horizontalAlignment === "left" ? "0" : "100%"} ${positionInfo.verticalAlignment === "top" ? "0" : "100%"}`;
1419
1483
  }
1484
+ /** Where a context menu opens without a pointer: the region's start corner. */
1485
+ _regionCorner() {
1486
+ const rect = this.el.getBoundingClientRect();
1487
+ const rtl = getComputedStyle(this.el).direction === "rtl";
1488
+ return { x: rtl ? rect.right : rect.left, y: rect.bottom };
1489
+ }
1490
+ /**
1491
+ * Places a context menu with its start corner on the point, flipping to
1492
+ * the other side of the point where it would not fit. The bounds are the
1493
+ * viewport, narrowed to a scrolling or clipping ancestor the menu sits in;
1494
+ * a menu taller than them gets their height and scrolls.
1495
+ *
1496
+ * The menu rests at left/top 0 of its containing block, so its rect is the
1497
+ * offset to subtract - the same trick _getMenuPosition uses. It is measured
1498
+ * without the entry animation's scale, which a second right-click can catch.
1499
+ */
1500
+ _placeMenuAtPoint({ x, y }, container) {
1501
+ const { transform, transition } = this.menuEl.style;
1502
+ this.menuEl.style.transition = "none";
1503
+ this.menuEl.style.transform = "none";
1504
+ const menu = this.menuEl.getBoundingClientRect();
1505
+ this.menuEl.style.transform = transform;
1506
+ this.menuEl.style.transition = transition;
1507
+ let bounds = { left: 0, top: 0, right: window.innerWidth, bottom: window.innerHeight };
1508
+ if (container && getComputedStyle(container).overflow !== "visible") {
1509
+ const clip = container.getBoundingClientRect();
1510
+ bounds = {
1511
+ left: Math.max(bounds.left, clip.left),
1512
+ top: Math.max(bounds.top, clip.top),
1513
+ right: Math.min(bounds.right, clip.right),
1514
+ bottom: Math.min(bounds.bottom, clip.bottom)
1515
+ };
1516
+ }
1517
+ const width = menu.width;
1518
+ const height = Math.min(menu.height, bounds.bottom - bounds.top);
1519
+ this.isScrollable = height < menu.height;
1520
+ if (this.isScrollable) this.menuEl.style.height = height + "px";
1521
+ const rtl = getComputedStyle(this.el).direction === "rtl";
1522
+ const fitsRight = x + width <= bounds.right;
1523
+ const fitsLeft = x - width >= bounds.left;
1524
+ const leftward = rtl ? fitsLeft || !fitsRight : !fitsRight && fitsLeft;
1525
+ const upward = y + height > bounds.bottom && y - height >= bounds.top;
1526
+ const clamp = (value, min, max) => Math.max(min, Math.min(value, max));
1527
+ const left = clamp(leftward ? x - width : x, bounds.left, bounds.right - width);
1528
+ const top = clamp(upward ? y - height : y, bounds.top, bounds.bottom - height);
1529
+ this.menuEl.style.left = left - menu.left + "px";
1530
+ this.menuEl.style.top = top - menu.top + "px";
1531
+ this.menuEl.style.transformOrigin = `${leftward ? "100%" : "0"} ${upward ? "100%" : "0"}`;
1532
+ }
1533
+ _setExpanded(expanded) {
1534
+ if (!this.options.contextMenu) this.el.ariaExpanded = String(expanded);
1535
+ }
1420
1536
  };
1421
1537
  _Menu._menus = [];
1422
1538
  var Menu = _Menu;
@@ -2731,6 +2847,10 @@ var Expressive = (() => {
2731
2847
  Utils.onDocumentReady(() => {
2732
2848
  document.addEventListener("change", (e) => {
2733
2849
  const target = e.target;
2850
+ if (target instanceof HTMLInputElement && target.type === "file") {
2851
+ const zone = target.closest(".drop-zone");
2852
+ if (zone) _Forms.listDropZoneFiles(zone, target);
2853
+ }
2734
2854
  if (target instanceof HTMLInputElement) {
2735
2855
  _Forms.validateField(target);
2736
2856
  }
@@ -2746,6 +2866,12 @@ var Expressive = (() => {
2746
2866
  }
2747
2867
  }
2748
2868
  });
2869
+ document.addEventListener("click", _Forms.togglePassword);
2870
+ document.addEventListener("submit", _Forms.resetFormExtras, true);
2871
+ document.addEventListener("reset", _Forms.resetFormExtras, true);
2872
+ for (const type of ["dragenter", "dragover", "dragleave", "drop"]) {
2873
+ document.addEventListener(type, _Forms.markDropZone);
2874
+ }
2749
2875
  document.querySelectorAll(".expressive-textarea").forEach((textArea) => {
2750
2876
  _Forms.InitTextarea(textArea);
2751
2877
  });
@@ -2754,6 +2880,75 @@ var Expressive = (() => {
2754
2880
  });
2755
2881
  });
2756
2882
  }
2883
+ /**
2884
+ * A .password-toggle button shows or hides the password in its field. The
2885
+ * button's own name stays put ("Show password"); aria-pressed carries the
2886
+ * state, so there is no generated text to translate.
2887
+ */
2888
+ static togglePassword(e) {
2889
+ const button = e.target.closest?.(".password-toggle");
2890
+ const input = button?.closest(".field")?.querySelector(
2891
+ 'input[type="password"], input[data-password]'
2892
+ );
2893
+ if (!input) return;
2894
+ const show = input.type === "password";
2895
+ input.toggleAttribute("data-password", show);
2896
+ input.type = show ? "text" : "password";
2897
+ button.setAttribute("aria-pressed", String(show));
2898
+ }
2899
+ /**
2900
+ * A revealed password goes back to type="password" when its form is
2901
+ * submitted or reset, so it is never sent or autofilled as plain text and
2902
+ * a reset field does not stay revealed. A reset form also drops the file
2903
+ * lists it would otherwise leave showing: reset fires no change event.
2904
+ */
2905
+ static resetFormExtras(e) {
2906
+ const form = e.target;
2907
+ if (!(form instanceof HTMLFormElement)) return;
2908
+ form.querySelectorAll("input[data-password]").forEach((input) => {
2909
+ input.removeAttribute("data-password");
2910
+ input.type = "password";
2911
+ input.closest(".field")?.querySelector(".password-toggle")?.setAttribute("aria-pressed", "false");
2912
+ });
2913
+ if (e.type === "reset") {
2914
+ form.querySelectorAll(".drop-zone-files").forEach((list) => list.replaceChildren());
2915
+ form.querySelectorAll(".drop-zone.dragover").forEach((zone) => zone.classList.remove("dragover"));
2916
+ }
2917
+ }
2918
+ /**
2919
+ * The native file input covers its .drop-zone, so the browser already takes
2920
+ * a dropped file. This only shows the drag: the input is the topmost
2921
+ * element, so every drag event over the zone targets it and leaving it is
2922
+ * leaving the zone.
2923
+ */
2924
+ static markDropZone(e) {
2925
+ const input = e.target;
2926
+ if (!(input instanceof HTMLInputElement) || input.type !== "file") return;
2927
+ const zone = input.closest(".drop-zone");
2928
+ if (!zone) return;
2929
+ const over = e.type === "dragenter" || e.type === "dragover";
2930
+ zone.classList.toggle("dragover", over && !input.disabled);
2931
+ }
2932
+ /** Lists the chosen files, name and size, in the zone's .drop-zone-files. */
2933
+ static listDropZoneFiles(zone, input) {
2934
+ const list = zone.querySelector(".drop-zone-files");
2935
+ if (!list) return;
2936
+ const size = (bytes) => {
2937
+ const [value, unit] = bytes >= 1e6 ? [bytes / 1e6, "megabyte"] : bytes >= 1e3 ? [bytes / 1e3, "kilobyte"] : [bytes, "byte"];
2938
+ return new Intl.NumberFormat(void 0, { style: "unit", unit, maximumFractionDigits: 1 }).format(value);
2939
+ };
2940
+ list.replaceChildren(
2941
+ ...Array.from(input.files ?? [], (file) => {
2942
+ const item = document.createElement("li");
2943
+ const name = document.createElement("span");
2944
+ const meta = document.createElement("span");
2945
+ name.textContent = file.name;
2946
+ meta.textContent = size(file.size);
2947
+ item.append(name, meta);
2948
+ return item;
2949
+ })
2950
+ );
2951
+ }
2757
2952
  static InitTextarea(textarea) {
2758
2953
  textarea.setAttribute("original-height", textarea.getBoundingClientRect().height.toString());
2759
2954
  _Forms.textareaAutoResize(textarea);
@@ -3917,8 +4112,1576 @@ var Expressive = (() => {
3917
4112
  }
3918
4113
  };
3919
4114
 
3920
- // src/ts/components/select.ts
4115
+ // src/ts/components/commandPalette.ts
3921
4116
  var _defaults12 = {
4117
+ shortcut: "k",
4118
+ closeOnRun: true
4119
+ };
4120
+ var APPLE = typeof navigator !== "undefined" && /Mac|iPhone|iPad|iPod/.test(navigator.platform);
4121
+ var fold = (text) => text.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase();
4122
+ var CommandPalette = class _CommandPalette extends Component {
4123
+ constructor(el, options) {
4124
+ super(el, options, _CommandPalette);
4125
+ this._active = null;
4126
+ this._generatedIds = [];
4127
+ /** Wires and refilters commands the page adds or removes later. */
4128
+ this._observer = null;
4129
+ /** Opens the palette as a modal dialog with the search input focused. */
4130
+ this.open = () => {
4131
+ if (!this.el.open) this.el.showModal();
4132
+ this.input?.focus();
4133
+ };
4134
+ /** Closes the palette. The dialog returns focus to where it was. */
4135
+ this.close = () => {
4136
+ if (this.el.open) this.el.close();
4137
+ };
4138
+ this._handleInput = () => this._filter();
4139
+ this._handleKeydown = (e) => {
4140
+ if (e.key === Utils.keys.ARROW_DOWN || e.key === Utils.keys.ARROW_UP) {
4141
+ e.preventDefault();
4142
+ this._move(e.key === Utils.keys.ARROW_DOWN ? 1 : -1);
4143
+ } else if (e.key === Utils.keys.ENTER && !e.isComposing && e.keyCode !== 229 && this._active) {
4144
+ e.preventDefault();
4145
+ this._run(this._active);
4146
+ }
4147
+ };
4148
+ this._handleListClick = (e) => {
4149
+ const item = e.target.closest('[role="option"]');
4150
+ if (!item || !this.list.contains(item) || item.getAttribute("aria-disabled") === "true") return;
4151
+ const action = this._action(item);
4152
+ if (action && !action.contains(e.target)) {
4153
+ action.click();
4154
+ return;
4155
+ }
4156
+ if (this.options.closeOnRun) this.close();
4157
+ };
4158
+ this._handlePointerMove = (e) => {
4159
+ const item = e.target.closest('[role="option"]');
4160
+ if (item && this.list.contains(item) && item.getAttribute("aria-disabled") !== "true") this._setActive(item);
4161
+ };
4162
+ /**
4163
+ * Each opening starts from the full list. The close event is queued, so a
4164
+ * palette reopened straight away may already be open again; its new search
4165
+ * is left alone.
4166
+ */
4167
+ this._handleClose = () => {
4168
+ if (this.el.open) return;
4169
+ this.input.value = "";
4170
+ this._filter();
4171
+ };
4172
+ this._handleShortcut = (e) => {
4173
+ const { shortcut } = this.options;
4174
+ if (!shortcut || e.defaultPrevented || e.altKey || e.shiftKey) return;
4175
+ if (!(APPLE ? e.metaKey && !e.ctrlKey : e.ctrlKey && !e.metaKey)) return;
4176
+ const letter = shortcut.toLowerCase();
4177
+ const latin = /^[a-z]$/i.test(e.key);
4178
+ const hit = e.key.toLowerCase() === letter || !latin && /^[a-z]$/.test(letter) && e.code === `Key${letter.toUpperCase()}`;
4179
+ if (!hit) return;
4180
+ e.preventDefault();
4181
+ if (this.el.open) this.close();
4182
+ else this.open();
4183
+ };
4184
+ this.el["Expressive_CommandPalette"] = this;
4185
+ const shortcut = el.dataset.shortcut;
4186
+ this.options = {
4187
+ ..._CommandPalette.defaults,
4188
+ ...shortcut !== void 0 ? { shortcut: shortcut || null } : {},
4189
+ ...options
4190
+ };
4191
+ this.input = el.querySelector(":scope > input");
4192
+ this.list = el.querySelector(":scope > :is(ul, ol)");
4193
+ if (!this.input || !this.list) {
4194
+ console.error("CommandPalette needs an <input> and a <ul> as children of the dialog.");
4195
+ return;
4196
+ }
4197
+ this._setupAccessibility();
4198
+ this._filter();
4199
+ this._observer = new MutationObserver(() => this._filter(true));
4200
+ this._observer.observe(this.list, {
4201
+ childList: true,
4202
+ subtree: true,
4203
+ attributes: true,
4204
+ attributeFilter: ["hidden", "disabled"]
4205
+ });
4206
+ this.input.addEventListener("input", this._handleInput);
4207
+ this.input.addEventListener("keydown", this._handleKeydown);
4208
+ this.list.addEventListener("click", this._handleListClick);
4209
+ this.list.addEventListener("pointermove", this._handlePointerMove);
4210
+ this.el.addEventListener("close", this._handleClose);
4211
+ document.addEventListener("keydown", this._handleShortcut);
4212
+ }
4213
+ static get defaults() {
4214
+ return _defaults12;
4215
+ }
4216
+ /**
4217
+ * Initializes instances of CommandPalette.
4218
+ * @param els HTML elements.
4219
+ * @param options Component options.
4220
+ */
4221
+ static init(els, options = {}) {
4222
+ return super.init(els, options, _CommandPalette);
4223
+ }
4224
+ static getInstance(el) {
4225
+ return el["Expressive_CommandPalette"];
4226
+ }
4227
+ /** If the palette is open. */
4228
+ get isOpen() {
4229
+ return this.el.open;
4230
+ }
4231
+ destroy() {
4232
+ this._observer?.disconnect();
4233
+ document.removeEventListener("keydown", this._handleShortcut);
4234
+ this.el.removeEventListener("close", this._handleClose);
4235
+ if (this.input && this.list) {
4236
+ this.input.removeEventListener("input", this._handleInput);
4237
+ this.input.removeEventListener("keydown", this._handleKeydown);
4238
+ this.list.removeEventListener("click", this._handleListClick);
4239
+ this.list.removeEventListener("pointermove", this._handlePointerMove);
4240
+ for (const name of ["role", "aria-controls", "aria-expanded", "aria-autocomplete", "aria-activedescendant"]) {
4241
+ this.input.removeAttribute(name);
4242
+ }
4243
+ this.list.removeAttribute("role");
4244
+ for (const item of this._items()) {
4245
+ for (const name of ["role", "aria-selected", "aria-disabled", "data-filtered"]) item.removeAttribute(name);
4246
+ item.classList.remove("active");
4247
+ this._action(item)?.removeAttribute("tabindex");
4248
+ }
4249
+ for (const label of this._labels()) {
4250
+ label.removeAttribute("role");
4251
+ label.removeAttribute("data-filtered");
4252
+ }
4253
+ for (const el of this._generatedIds) el.removeAttribute("id");
4254
+ const empty = this._empty();
4255
+ if (empty) empty.hidden = false;
4256
+ }
4257
+ this.el["Expressive_CommandPalette"] = void 0;
4258
+ }
4259
+ _items() {
4260
+ return Array.from(this.list.children).filter(
4261
+ (child) => child instanceof HTMLElement && !child.classList.contains("label")
4262
+ );
4263
+ }
4264
+ _labels() {
4265
+ return Array.from(this.list.querySelectorAll(":scope > .label"));
4266
+ }
4267
+ _empty() {
4268
+ return this.el.querySelector(":scope > .command-palette-empty");
4269
+ }
4270
+ _action(item) {
4271
+ return item.querySelector(":scope > :is(a[href], button)");
4272
+ }
4273
+ _ensureId(el, prefix) {
4274
+ if (!el.id) {
4275
+ el.id = `${prefix}-${Utils.guid()}`;
4276
+ this._generatedIds.push(el);
4277
+ }
4278
+ return el.id;
4279
+ }
4280
+ _setupAccessibility() {
4281
+ this.list.setAttribute("role", "listbox");
4282
+ this.input.setAttribute("role", "combobox");
4283
+ this.input.setAttribute("aria-controls", this._ensureId(this.list, "command-palette-list"));
4284
+ this.input.setAttribute("aria-expanded", "true");
4285
+ this.input.setAttribute("aria-autocomplete", "list");
4286
+ }
4287
+ /** Every command, including ones added since the last pass, is an option. */
4288
+ _wireItems() {
4289
+ for (const item of this._items()) {
4290
+ if (item.getAttribute("role") === "option") continue;
4291
+ item.setAttribute("role", "option");
4292
+ this._ensureId(item, "command-palette-option");
4293
+ this._action(item)?.setAttribute("tabindex", "-1");
4294
+ }
4295
+ for (const label of this._labels()) label.setAttribute("role", "none");
4296
+ }
4297
+ /** Commands on screen: neither hidden by the page nor filtered out. */
4298
+ _shown() {
4299
+ return this._items().filter((item) => !item.hidden && !item.hasAttribute("data-filtered"));
4300
+ }
4301
+ /** Shown commands the arrows and Enter can reach: disabled ones are skipped. */
4302
+ _visible() {
4303
+ return this._shown().filter((item) => item.getAttribute("aria-disabled") !== "true");
4304
+ }
4305
+ /** A disabled link or button cannot run, so its option says so. */
4306
+ _isDisabled(item) {
4307
+ const action = this._action(item);
4308
+ return !!action && (action.matches(":disabled") || action.getAttribute("aria-disabled") === "true");
4309
+ }
4310
+ _setActive(item) {
4311
+ if (this._active === item) return;
4312
+ this._active?.classList.remove("active");
4313
+ this._active?.removeAttribute("aria-selected");
4314
+ this._active = item;
4315
+ if (item) {
4316
+ item.classList.add("active");
4317
+ item.setAttribute("aria-selected", "true");
4318
+ this.input.setAttribute("aria-activedescendant", item.id);
4319
+ item.scrollIntoView({ block: "nearest" });
4320
+ } else {
4321
+ this.input.removeAttribute("aria-activedescendant");
4322
+ }
4323
+ }
4324
+ /**
4325
+ * Shows the commands matching the query. Filtering marks rows with
4326
+ * data-filtered rather than hidden, so a command the page hides stays
4327
+ * hidden. `keepActive` holds the active command while it is still
4328
+ * reachable, for list changes the user did not make.
4329
+ */
4330
+ _filter(keepActive = false) {
4331
+ this._wireItems();
4332
+ const terms = fold(this.input.value).split(/\s+/).filter(Boolean);
4333
+ for (const item of this._items()) {
4334
+ const keywords = item.dataset.keywords ?? this._action(item)?.dataset.keywords ?? "";
4335
+ const text = fold(`${item.textContent} ${keywords}`);
4336
+ item.toggleAttribute("data-filtered", !terms.every((term) => text.includes(term)));
4337
+ if (this._isDisabled(item)) item.setAttribute("aria-disabled", "true");
4338
+ else item.removeAttribute("aria-disabled");
4339
+ }
4340
+ for (const label of this._labels()) {
4341
+ let next = label.nextElementSibling;
4342
+ let any = false;
4343
+ while (next && !next.classList.contains("label")) {
4344
+ if (!next.hidden && !next.hasAttribute("data-filtered")) any = true;
4345
+ next = next.nextElementSibling;
4346
+ }
4347
+ label.toggleAttribute("data-filtered", !any);
4348
+ }
4349
+ const visible = this._visible();
4350
+ const empty = this._empty();
4351
+ if (empty) empty.hidden = this._shown().length > 0;
4352
+ this._setActive(keepActive && visible.includes(this._active) ? this._active : visible[0] ?? null);
4353
+ }
4354
+ _move(step) {
4355
+ const visible = this._visible();
4356
+ if (!visible.length) return;
4357
+ const index = visible.indexOf(this._active);
4358
+ this._setActive(visible[(index + step + visible.length) % visible.length]);
4359
+ }
4360
+ /** Runs a command by clicking its link or button; the click handler closes. */
4361
+ _run(item) {
4362
+ (this._action(item) ?? item).click();
4363
+ }
4364
+ };
4365
+
4366
+ // src/ts/components/questionnaire.ts
4367
+ var _defaults13 = {
4368
+ progressLabel: "Question {current} of {total}",
4369
+ requiredMessage: "Choose an answer to continue."
4370
+ };
4371
+ var NAVIGATION = ["previous", "skip", "next"];
4372
+ var Questionnaire = class _Questionnaire extends Component {
4373
+ constructor(el, options) {
4374
+ super(el, options, _Questionnaire);
4375
+ this._active = null;
4376
+ this._generated = [];
4377
+ this._generatedIds = [];
4378
+ /** Set while one submission's invalid events arrive, so only the first opens. */
4379
+ this._reporting = false;
4380
+ /** Shows the question at `index` without checking the current one. */
4381
+ this.goTo = (index) => {
4382
+ const item = this.items[index];
4383
+ if (!item) return;
4384
+ this._active = item;
4385
+ this._render();
4386
+ const controls = this._controls(item).filter((control) => !control.disabled);
4387
+ (controls.find((control) => control.checked) ?? controls[0])?.focus();
4388
+ };
4389
+ /** Checks the shown question and moves to the next one if it is answered. */
4390
+ this.next = () => {
4391
+ if (this._validate(this._active)) this.goTo(this.index + 1);
4392
+ };
4393
+ /** Moves to the previous question. */
4394
+ this.previous = () => this.goTo(this.index - 1);
4395
+ /** Clears the answer to the shown question and moves to the next one. */
4396
+ this.skip = () => {
4397
+ if (!this._active) return;
4398
+ for (const control of this._controls(this._active)) {
4399
+ const isChoice = control instanceof HTMLInputElement && (control.type === "radio" || control.type === "checkbox");
4400
+ if (isChoice ? !control.checked : !control.value) continue;
4401
+ if (isChoice) control.checked = false;
4402
+ else control.value = "";
4403
+ control.dispatchEvent(new Event("change", { bubbles: true }));
4404
+ }
4405
+ this.goTo(this.index + 1);
4406
+ };
4407
+ this._handleClick = (e) => {
4408
+ const button = e.target.closest?.("button");
4409
+ if (!button || !this._buttons().includes(button)) return;
4410
+ if (NAVIGATION.includes(button.value)) {
4411
+ e.preventDefault();
4412
+ this[button.value]();
4413
+ return;
4414
+ }
4415
+ this._syncRequired();
4416
+ };
4417
+ // Enter in a field moves on instead of submitting the form early.
4418
+ this._handleKeydown = (e) => {
4419
+ if (e.key !== "Enter" || e.defaultPrevented || e.isComposing) return;
4420
+ if (!(e.target instanceof HTMLInputElement) || this.index >= this.items.length - 1) return;
4421
+ e.preventDefault();
4422
+ this.next();
4423
+ };
4424
+ // A submission found an unanswered question, possibly a hidden one. Open the
4425
+ // first instead of letting the browser report a control it cannot show.
4426
+ // One submission fires its invalid events in a single task.
4427
+ this._handleInvalid = (e) => {
4428
+ const item = this.items.find((item2) => item2.contains(e.target));
4429
+ if (!item) return;
4430
+ e.preventDefault();
4431
+ if (this._reporting) return;
4432
+ this._reporting = true;
4433
+ setTimeout(() => this._reporting = false);
4434
+ this._active = item;
4435
+ this._render();
4436
+ this._validate(item);
4437
+ };
4438
+ // An answer clears the question's error.
4439
+ this._handleInput = (e) => {
4440
+ this._syncRequired();
4441
+ const item = e.target.closest?.("fieldset");
4442
+ if (item instanceof HTMLFieldSetElement && item.classList.contains("invalid")) this._setInvalid(item, false);
4443
+ };
4444
+ this.el["Expressive_Questionnaire"] = this;
4445
+ const progressLabel = el.dataset.progressLabel;
4446
+ this.options = {
4447
+ ..._Questionnaire.defaults,
4448
+ ...progressLabel ? { progressLabel } : {},
4449
+ ...options
4450
+ };
4451
+ this._progress = document.createElement("div");
4452
+ this._progress.className = "questionnaire-progress";
4453
+ this._progressText = document.createElement("span");
4454
+ this._progressText.id = `questionnaire-progress-${Utils.guid()}`;
4455
+ this._progressBar = document.createElement("progress");
4456
+ this._progressBar.className = "progress";
4457
+ this._progressBar.setAttribute("aria-labelledby", this._progressText.id);
4458
+ this._progress.append(this._progressText, this._progressBar);
4459
+ this.el.prepend(this._progress);
4460
+ for (const item of this._fieldsets()) {
4461
+ const ids = Array.from(item.querySelectorAll(":scope > p:not(.questionnaire-error)")).map((p) => this._ensureId(p, "questionnaire-description"));
4462
+ if (ids.length && !item.hasAttribute("aria-describedby")) {
4463
+ item.setAttribute("aria-describedby", ids.join(" "));
4464
+ this._generatedIds.push(item);
4465
+ }
4466
+ }
4467
+ this._active = this.items[0] ?? null;
4468
+ this._syncRequired();
4469
+ this._render();
4470
+ this._observer = new MutationObserver(() => {
4471
+ this._syncRequired();
4472
+ this._render();
4473
+ });
4474
+ this._observer.observe(this.el, { attributes: true, subtree: true, attributeFilter: ["disabled"] });
4475
+ this.el.addEventListener("click", this._handleClick);
4476
+ this.el.addEventListener("keydown", this._handleKeydown);
4477
+ this.el.addEventListener("input", this._handleInput);
4478
+ this.el.addEventListener("invalid", this._handleInvalid, true);
4479
+ }
4480
+ static get defaults() {
4481
+ return _defaults13;
4482
+ }
4483
+ /**
4484
+ * Initializes instances of Questionnaire.
4485
+ * @param els HTML elements.
4486
+ * @param options Component options.
4487
+ */
4488
+ static init(els, options = {}) {
4489
+ return super.init(els, options, _Questionnaire);
4490
+ }
4491
+ static getInstance(el) {
4492
+ return el["Expressive_Questionnaire"];
4493
+ }
4494
+ /** The questions that apply: every fieldset child that is not disabled. */
4495
+ get items() {
4496
+ return this._fieldsets().filter((item) => !item.disabled);
4497
+ }
4498
+ /** Position of the shown question in `items`. */
4499
+ get index() {
4500
+ return this.items.indexOf(this._active);
4501
+ }
4502
+ destroy() {
4503
+ this._observer.disconnect();
4504
+ this.el.removeEventListener("click", this._handleClick);
4505
+ this.el.removeEventListener("keydown", this._handleKeydown);
4506
+ this.el.removeEventListener("input", this._handleInput);
4507
+ this.el.removeEventListener("invalid", this._handleInvalid, true);
4508
+ for (const item of this._fieldsets()) {
4509
+ if (item.hasAttribute("data-required")) for (const control of this._controls(item)) control.setCustomValidity("");
4510
+ item.hidden = false;
4511
+ this._setInvalid(item, false);
4512
+ }
4513
+ for (const button of this._buttons()) button.hidden = false;
4514
+ this._progress.remove();
4515
+ for (const el of this._generated) el.remove();
4516
+ for (const el of this._generatedIds) {
4517
+ el.removeAttribute(el instanceof HTMLFieldSetElement ? "aria-describedby" : "id");
4518
+ }
4519
+ this.el["Expressive_Questionnaire"] = void 0;
4520
+ }
4521
+ _fieldsets() {
4522
+ return Array.from(this.el.querySelectorAll(":scope > fieldset"));
4523
+ }
4524
+ /** The footer's navigation and submit buttons. */
4525
+ _buttons() {
4526
+ return Array.from(this.el.querySelectorAll(":scope > footer > button")).filter(
4527
+ (button) => NAVIGATION.includes(button.value) || button.type === "submit"
4528
+ );
4529
+ }
4530
+ _controls(item) {
4531
+ return Array.from(item.elements).filter(
4532
+ (el) => el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement || el instanceof HTMLInputElement && el.type !== "button" && el.type !== "submit"
4533
+ );
4534
+ }
4535
+ _required(item) {
4536
+ return item.hasAttribute("data-required") || this._controls(item).some((control) => control.required);
4537
+ }
4538
+ _ensureId(el, prefix) {
4539
+ if (!el.id) {
4540
+ el.id = `${prefix}-${Utils.guid()}`;
4541
+ this._generatedIds.push(el);
4542
+ }
4543
+ return el.id;
4544
+ }
4545
+ _render() {
4546
+ const items = this.items;
4547
+ if (!items.includes(this._active)) {
4548
+ const all = this._fieldsets();
4549
+ const after = all.slice(all.indexOf(this._active) + 1).find((item) => !item.disabled);
4550
+ this._active = after ?? items[items.length - 1] ?? null;
4551
+ }
4552
+ const index = items.indexOf(this._active);
4553
+ const last = index === items.length - 1;
4554
+ for (const item of this._fieldsets()) item.hidden = item !== this._active;
4555
+ for (const button of this._buttons()) {
4556
+ if (button.value === "previous") button.hidden = index <= 0;
4557
+ else if (button.value === "skip") button.hidden = last || !this._active || this._required(this._active);
4558
+ else if (button.value === "next") button.hidden = last;
4559
+ else button.hidden = !last;
4560
+ }
4561
+ this._progressText.textContent = this.options.progressLabel.replace("{current}", String(index + 1)).replace("{total}", String(items.length));
4562
+ this._progressBar.max = Math.max(items.length, 1);
4563
+ this._progressBar.value = index + 1;
4564
+ }
4565
+ /**
4566
+ * Puts "choose at least one" on the first enabled checkbox of each
4567
+ * data-required group with none checked, so the browser's validation, and
4568
+ * every way of submitting, sees it. A disabled checkbox is not sent, so it
4569
+ * does not count.
4570
+ */
4571
+ _syncRequired() {
4572
+ for (const item of this._fieldsets()) {
4573
+ if (!item.hasAttribute("data-required")) continue;
4574
+ const boxes = this._controls(item).filter(
4575
+ (control) => control instanceof HTMLInputElement && control.type === "checkbox"
4576
+ );
4577
+ const enabled = boxes.filter((box) => !box.disabled);
4578
+ for (const box of boxes) box.setCustomValidity("");
4579
+ if (enabled.length && !enabled.some((box) => box.checked)) {
4580
+ const error = item.querySelector(":scope > .questionnaire-error:not([data-generated])");
4581
+ enabled[0].setCustomValidity(error?.textContent.trim() || this.options.requiredMessage);
4582
+ }
4583
+ }
4584
+ }
4585
+ /** The first control in the question that still needs an answer. */
4586
+ _invalidControl(item) {
4587
+ return this._controls(item).find((control) => control.willValidate && !control.validity.valid);
4588
+ }
4589
+ /** Shows the question's error and returns false when it needs an answer. */
4590
+ _validate(item) {
4591
+ this._syncRequired();
4592
+ const invalid = item && this._invalidControl(item);
4593
+ if (!invalid) return true;
4594
+ let error = item.querySelector(":scope > .questionnaire-error");
4595
+ if (!error) {
4596
+ error = document.createElement("p");
4597
+ error.className = "questionnaire-error";
4598
+ error.dataset.generated = "";
4599
+ error.textContent = invalid.validationMessage || this.options.requiredMessage;
4600
+ item.append(error);
4601
+ this._generated.push(error);
4602
+ }
4603
+ this._setInvalid(item, true);
4604
+ invalid.focus();
4605
+ return false;
4606
+ }
4607
+ _setInvalid(item, invalid) {
4608
+ item.classList.toggle("invalid", invalid);
4609
+ const error = item.querySelector(":scope > .questionnaire-error");
4610
+ const described = (item.getAttribute("aria-describedby") ?? "").split(" ").filter((id) => id && id !== error?.id);
4611
+ if (invalid && error) described.push(this._ensureId(error, "questionnaire-error"));
4612
+ if (described.length) item.setAttribute("aria-describedby", described.join(" "));
4613
+ else item.removeAttribute("aria-describedby");
4614
+ for (const control of this._controls(item)) {
4615
+ if (invalid) control.setAttribute("aria-invalid", "true");
4616
+ else control.removeAttribute("aria-invalid");
4617
+ }
4618
+ if (!invalid && this._generated.includes(error)) {
4619
+ error.remove();
4620
+ this._generated.splice(this._generated.indexOf(error), 1);
4621
+ }
4622
+ }
4623
+ };
4624
+
4625
+ // src/ts/components/lineChart.ts
4626
+ var _defaults14 = {
4627
+ min: null,
4628
+ max: null
4629
+ };
4630
+ var SVG = "http://www.w3.org/2000/svg";
4631
+ var COLORS = 4;
4632
+ function svgNode(name, attributes = {}) {
4633
+ const element = document.createElementNS(SVG, name);
4634
+ for (const key in attributes) element.setAttribute(key, attributes[key]);
4635
+ return element;
4636
+ }
4637
+ var _gradientId = 0;
4638
+ function cellValue(cell) {
4639
+ if (!cell) return null;
4640
+ const raw = cell.dataset.value ?? cell.textContent;
4641
+ const text = raw.replace(/\u2212/g, "-").replace(/[\s%\p{Sc}]/gu, "");
4642
+ if (!/^[+-]?(\d{1,3}(,\d{3})+|\d+)?(\.\d+)?(e[+-]?\d+)?$/i.test(text) || !/\d/.test(text)) return null;
4643
+ return parseFloat(text.replace(/,/g, ""));
4644
+ }
4645
+ function monotonePath(points) {
4646
+ const n = points.length;
4647
+ if (n === 0) return "";
4648
+ if (n === 1) return `M${points[0][0]},${points[0][1]}h0`;
4649
+ const slopes = [];
4650
+ for (let i = 0; i < n - 1; i++) {
4651
+ slopes.push((points[i + 1][1] - points[i][1]) / (points[i + 1][0] - points[i][0]));
4652
+ }
4653
+ const tangents = points.map((_, i) => {
4654
+ if (i === 0) return slopes[0];
4655
+ if (i === n - 1) return slopes[n - 2];
4656
+ const [a, b] = [slopes[i - 1], slopes[i]];
4657
+ return a * b <= 0 ? 0 : 2 / (1 / a + 1 / b);
4658
+ });
4659
+ let d = `M${points[0][0]},${points[0][1]}`;
4660
+ for (let i = 0; i < n - 1; i++) {
4661
+ const [[x0, y0], [x1, y1]] = [points[i], points[i + 1]];
4662
+ const h = (x1 - x0) / 3;
4663
+ d += `C${x0 + h},${y0 + tangents[i] * h} ${x1 - h},${y1 - tangents[i + 1] * h} ${x1},${y1}`;
4664
+ }
4665
+ return d;
4666
+ }
4667
+ var LineChart = class extends Component {
4668
+ constructor(el, options) {
4669
+ super(el, options, new.target);
4670
+ /** The values' x positions and series, read from the table. */
4671
+ this.labels = [];
4672
+ this.series = [];
4673
+ /** The highlighted row, or -1. */
4674
+ this.activeIndex = -1;
4675
+ this._generated = [];
4676
+ /** The table's header cells and body rows, as read. */
4677
+ this._header = [];
4678
+ this._rows = [];
4679
+ /**
4680
+ * Whether the rows run right to left: a horizontal row axis in a
4681
+ * right-to-left context, like the labels. Bar chart rows keep their order.
4682
+ */
4683
+ this._reversed = false;
4684
+ this._onPointerMove = (e) => {
4685
+ this.show(this._indexAt(e));
4686
+ };
4687
+ this._onFocus = () => {
4688
+ if (this.activeIndex < 0 && this._plot.matches(":focus-visible")) this.show(0);
4689
+ };
4690
+ this._onLeave = (e) => {
4691
+ if (e.pointerType === "touch" || this._plot.matches(":focus-visible")) return;
4692
+ this.show(-1);
4693
+ };
4694
+ /** The browser took the touch to scroll the page. */
4695
+ this._onCancel = () => {
4696
+ this.show(-1);
4697
+ };
4698
+ this._onKeyDown = (e) => {
4699
+ const next = this._keyTarget(e.key);
4700
+ if (next === void 0) return;
4701
+ e.preventDefault();
4702
+ this.show(next);
4703
+ };
4704
+ this.el[this._key] = this;
4705
+ this.options = {
4706
+ ...new.target.defaults,
4707
+ ...options
4708
+ };
4709
+ this._read();
4710
+ if (!this.labels.length || !this.series.length) return;
4711
+ this._reversed = !this._horizontal && getComputedStyle(this.el).direction === "rtl";
4712
+ this._render();
4713
+ if (!this.el.classList.contains("sparkline")) {
4714
+ this._plot.tabIndex = 0;
4715
+ this._plot.addEventListener("pointerdown", this._onPointerMove);
4716
+ this._plot.addEventListener("pointermove", this._onPointerMove);
4717
+ this._plot.addEventListener("pointerleave", this._onLeave);
4718
+ this._plot.addEventListener("pointercancel", this._onCancel);
4719
+ this._plot.addEventListener("keydown", this._onKeyDown);
4720
+ this._plot.addEventListener("focus", this._onFocus);
4721
+ this._plot.addEventListener("blur", this._onLeave);
4722
+ }
4723
+ }
4724
+ /** The class prefix of the generated parts; ColumnChart reuses this class. */
4725
+ get _type() {
4726
+ return "line-chart";
4727
+ }
4728
+ /** Whether the y axis always includes zero, as columns grow from it. */
4729
+ get _fromZero() {
4730
+ return false;
4731
+ }
4732
+ /**
4733
+ * Whether the rows run down the plot and the values across it, as in a
4734
+ * bar chart. The rows then keep their order in a right-to-left context.
4735
+ */
4736
+ get _horizontal() {
4737
+ return false;
4738
+ }
4739
+ /**
4740
+ * Whether the rows go round a circle, as in a pie or radar chart. The
4741
+ * arrows then step round it, wrapping from the last row to the first.
4742
+ */
4743
+ get _circular() {
4744
+ return false;
4745
+ }
4746
+ /** Whether the cursor marks each series with a point. */
4747
+ get _points() {
4748
+ return true;
4749
+ }
4750
+ get _key() {
4751
+ return "Expressive_LineChart";
4752
+ }
4753
+ static get defaults() {
4754
+ return _defaults14;
4755
+ }
4756
+ static init(els, options = {}) {
4757
+ return super.init(els, options, this);
4758
+ }
4759
+ static getInstance(el) {
4760
+ return el["Expressive_LineChart"];
4761
+ }
4762
+ destroy() {
4763
+ this._generated.forEach((node) => node.remove());
4764
+ this._generated = [];
4765
+ this.el[this._key] = void 0;
4766
+ }
4767
+ /**
4768
+ * Highlights one row: moves the cursor and fills the tooltip.
4769
+ * Pass -1 to hide them. Does nothing for a chart without data.
4770
+ */
4771
+ show(index) {
4772
+ if (!this._tooltip || index === this.activeIndex) return;
4773
+ this.activeIndex = index;
4774
+ const hidden = index < 0 || index >= this.labels.length;
4775
+ this._cursor.hidden = this._tooltip.hidden = hidden;
4776
+ if (hidden) return;
4777
+ this._cursor.replaceChildren();
4778
+ const title = document.createElement("strong");
4779
+ title.textContent = this.labels[index];
4780
+ this._tooltip.replaceChildren(title);
4781
+ this.series.forEach((series, i) => {
4782
+ const value = series.values[index];
4783
+ if (value === null) return;
4784
+ const color = String(i % COLORS + 1);
4785
+ const top = series.tops[index];
4786
+ if (top !== null && this._points) {
4787
+ const dot = document.createElement("span");
4788
+ dot.className = series.className;
4789
+ dot.dataset.series = color;
4790
+ dot.style.top = `${series.y(top)}%`;
4791
+ this._cursor.append(dot);
4792
+ }
4793
+ const name = document.createElement("span");
4794
+ name.dataset.series = color;
4795
+ name.textContent = series.name;
4796
+ const text = document.createElement("span");
4797
+ text.textContent = series.text[index];
4798
+ this._tooltip.append(name, text);
4799
+ });
4800
+ this._place(index);
4801
+ }
4802
+ /**
4803
+ * Moves the filled cursor and tooltip to a row: beside its band, with the
4804
+ * tooltip opening back over the plot past the middle.
4805
+ */
4806
+ _place(index) {
4807
+ const x = this._x(index);
4808
+ const side = this._horizontal ? "top" : "left";
4809
+ this._cursor.style[side] = this._tooltip.style[side] = `${x}%`;
4810
+ this._tooltip.classList.toggle("end", x > 50);
4811
+ }
4812
+ _read() {
4813
+ const table = this.el.querySelector(":scope > table");
4814
+ if (!table) return;
4815
+ const headerRow = table.tHead?.rows[0] ?? table.rows[0];
4816
+ const header = Array.from(headerRow?.cells ?? []);
4817
+ const rows = Array.from(table.tBodies).flatMap((body) => Array.from(body.rows)).filter((row) => row !== headerRow);
4818
+ [this._header, this._rows] = [header, rows];
4819
+ this.labels = rows.map((row) => row.cells[0]?.textContent.trim() ?? "");
4820
+ this.series = header.slice(1).map((cell, i) => {
4821
+ const values = rows.map((row) => cellValue(row.cells[i + 1]));
4822
+ return {
4823
+ name: cell.textContent.trim(),
4824
+ className: cell.className,
4825
+ text: rows.map((row) => row.cells[i + 1]?.textContent.trim() ?? ""),
4826
+ values,
4827
+ tops: values,
4828
+ y: void 0
4829
+ };
4830
+ });
4831
+ const stacked = this.el.classList.contains("stacked");
4832
+ if (stacked) {
4833
+ const totals = this.labels.map(() => 0);
4834
+ for (const series of this.series) {
4835
+ series.base = [...totals];
4836
+ series.tops = series.values.map((value, i) => totals[i] = value === null || totals[i] === null ? null : totals[i] + value);
4837
+ }
4838
+ }
4839
+ if (this.series.every((series) => series.tops.every((value) => value === null))) this.series = [];
4840
+ const option = (name) => this._number(name) ?? this.options[name];
4841
+ this._y = this._scale(this.series, option("min"), option("max"));
4842
+ }
4843
+ /** A number from one of the chart's data attributes, or null. */
4844
+ _number(name) {
4845
+ const value = parseFloat(this.el.dataset[name] ?? "");
4846
+ return Number.isFinite(value) ? value : null;
4847
+ }
4848
+ /**
4849
+ * Fits a y scale to the series' tops, between `min` and `max` where they
4850
+ * are set, and gives it to each series.
4851
+ */
4852
+ _scale(series, min, max, fromZero = this._fromZero) {
4853
+ const stacked = this.el.classList.contains("stacked");
4854
+ let [low, high] = [Infinity, -Infinity];
4855
+ for (const { tops } of series) {
4856
+ for (const value of tops) {
4857
+ if (value === null) continue;
4858
+ low = Math.min(low, value);
4859
+ high = Math.max(high, value);
4860
+ }
4861
+ }
4862
+ if (low > high) low = high = 0;
4863
+ if (fromZero) {
4864
+ const [bottom, top] = [Math.min(0, low), Math.max(0, high)];
4865
+ const room = (top - (min ?? bottom)) * 0.1 || 1;
4866
+ min ?? (min = bottom < 0 ? bottom - room : 0);
4867
+ max ?? (max = top + room);
4868
+ } else {
4869
+ const pad = (high - low) * 0.1 || 1;
4870
+ min ?? (min = stacked ? Math.min(0, low) : low - pad);
4871
+ max ?? (max = high + (stacked ? (high - min) * 0.1 || 1 : pad));
4872
+ }
4873
+ const y = (value) => 100 - (value - min) / (max - min || 1) * 100;
4874
+ for (const each of series) each.y = y;
4875
+ return y;
4876
+ }
4877
+ /** Each row sits in the middle of an equal band, beside its label. */
4878
+ _x(index) {
4879
+ const x = (index + 0.5) / this.labels.length * 100;
4880
+ return this._reversed ? 100 - x : x;
4881
+ }
4882
+ _render() {
4883
+ const type = this._type;
4884
+ this._plot = document.createElement("div");
4885
+ this._plot.className = `${type}-plot`;
4886
+ const caption = this.el.querySelector(":scope > figcaption")?.textContent.trim();
4887
+ this._plot.setAttribute("role", "group");
4888
+ this._plot.setAttribute("aria-roledescription", type.replace(/-/g, " "));
4889
+ if (caption) this._plot.setAttribute("aria-label", caption);
4890
+ this._cursor = document.createElement("div");
4891
+ this._cursor.className = `${type}-cursor`;
4892
+ this._cursor.hidden = true;
4893
+ this._tooltip = document.createElement("div");
4894
+ this._tooltip.className = `${type}-tooltip`;
4895
+ this._tooltip.hidden = true;
4896
+ this._tooltip.setAttribute("aria-live", "polite");
4897
+ this._plot.append(this._draw(), this._cursor, this._tooltip);
4898
+ this._generated = [this._plot, ...this._guides()];
4899
+ this.el.append(...this._generated);
4900
+ }
4901
+ /**
4902
+ * The <tfoot> row as a total to show over the plot, its value over its
4903
+ * label, or null without one.
4904
+ */
4905
+ _total(className) {
4906
+ const footer = this.el.querySelector(":scope > table")?.tFoot?.rows[0];
4907
+ if (!footer) return null;
4908
+ const total = document.createElement("div");
4909
+ total.className = className;
4910
+ const [label, value] = [document.createElement("span"), document.createElement("strong")];
4911
+ label.textContent = footer.cells[0]?.textContent.trim() ?? "";
4912
+ value.textContent = footer.cells[1]?.textContent.trim() ?? "";
4913
+ total.append(value, label);
4914
+ return total;
4915
+ }
4916
+ /** The parts below the plot: the row labels and the legend. */
4917
+ _guides() {
4918
+ const labels = document.createElement("ol");
4919
+ labels.className = `${this._type}-labels`;
4920
+ labels.setAttribute("aria-hidden", "true");
4921
+ for (const text of this.labels) {
4922
+ const li = document.createElement("li");
4923
+ li.textContent = text;
4924
+ labels.append(li);
4925
+ }
4926
+ return [labels, ...this._legend()];
4927
+ }
4928
+ /** The series legend, for more than one series. */
4929
+ _legend() {
4930
+ if (this.series.length < 2) return [];
4931
+ const legend = document.createElement("ul");
4932
+ legend.className = `${this._type}-legend`;
4933
+ legend.setAttribute("aria-hidden", "true");
4934
+ legend.append(...this.series.map((series, i) => {
4935
+ const li = document.createElement("li");
4936
+ li.dataset.series = String(i % COLORS + 1);
4937
+ li.textContent = series.name;
4938
+ return li;
4939
+ }));
4940
+ return [legend];
4941
+ }
4942
+ /** Draws the series, which fill the plot. */
4943
+ _draw(drawn = this.series) {
4944
+ const svg = document.createElementNS(SVG, "svg");
4945
+ svg.setAttribute("viewBox", "0 0 100 100");
4946
+ svg.setAttribute("preserveAspectRatio", "none");
4947
+ svg.setAttribute("aria-hidden", "true");
4948
+ svg.setAttribute("focusable", "false");
4949
+ for (const series of drawn) {
4950
+ const group = document.createElementNS(SVG, "g");
4951
+ group.setAttribute("class", `${this._type}-series ${series.className}`.trim());
4952
+ group.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
4953
+ const runs = [[]];
4954
+ series.tops.forEach((value, index) => {
4955
+ if (value === null) runs.push([]);
4956
+ else runs[runs.length - 1].push(index);
4957
+ });
4958
+ const filled = runs.filter((run) => run.length);
4959
+ const points = (run, values) => run.map((index) => [this._x(index), series.y(values[index])]);
4960
+ const tops = filled.map((run) => monotonePath(points(run, series.tops)));
4961
+ const id = `line-chart-gradient-${++_gradientId}`;
4962
+ const gradient = document.createElementNS(SVG, "linearGradient");
4963
+ gradient.id = id;
4964
+ gradient.setAttribute("x2", "0");
4965
+ gradient.setAttribute("y2", "1");
4966
+ gradient.append(document.createElementNS(SVG, "stop"), document.createElementNS(SVG, "stop"));
4967
+ gradient.lastElementChild.setAttribute("offset", "1");
4968
+ const area = document.createElementNS(SVG, "path");
4969
+ area.setAttribute("class", `${this._type}-area`);
4970
+ area.setAttribute("fill", `url(#${id})`);
4971
+ area.setAttribute("d", filled.map((run, r) => {
4972
+ if (!series.base) return `${tops[r]}V100H${this._x(run[0])}Z`;
4973
+ const base = points(run, series.base).map(([x, y]) => [x, Math.min(100, y)]);
4974
+ return `${tops[r]}L${monotonePath(base.reverse()).slice(1)}Z`;
4975
+ }).join(""));
4976
+ const line = document.createElementNS(SVG, "path");
4977
+ line.setAttribute("class", `${this._type}-line`);
4978
+ line.setAttribute("d", tops.join(""));
4979
+ group.append(gradient, area, line);
4980
+ svg.append(group);
4981
+ }
4982
+ return svg;
4983
+ }
4984
+ /** The row under the pointer, or -1. */
4985
+ _indexAt(e) {
4986
+ const box = this._plot.getBoundingClientRect();
4987
+ const fraction = this._horizontal ? (e.clientY - box.top) / box.height : (e.clientX - box.left) / box.width;
4988
+ const index = Math.floor((this._reversed ? 1 - fraction : fraction) * this.labels.length);
4989
+ return Math.min(this.labels.length - 1, Math.max(0, index));
4990
+ }
4991
+ /**
4992
+ * For a circular chart, the pointer's position from the plot's middle: a
4993
+ * fraction of a turn clockwise from the top, and its distance as a share
4994
+ * of the plot's width.
4995
+ */
4996
+ _polarAt(e) {
4997
+ const box = this._plot.getBoundingClientRect();
4998
+ const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.height / 2];
4999
+ return [(Math.atan2(x, -y) / (2 * Math.PI) + 1) % 1, Math.hypot(x, y) / box.width];
5000
+ }
5001
+ /** The keys that move to the next row and to the one before. */
5002
+ get _arrows() {
5003
+ if (this._circular) return [["ArrowRight", "ArrowDown"], ["ArrowLeft", "ArrowUp"]];
5004
+ if (this._horizontal) return [["ArrowDown"], ["ArrowUp"]];
5005
+ return this._reversed ? [["ArrowLeft"], ["ArrowRight"]] : [["ArrowRight"], ["ArrowLeft"]];
5006
+ }
5007
+ /** The index a key moves to, or undefined for a key the chart ignores. */
5008
+ _keyTarget(key) {
5009
+ const last = this.labels.length - 1;
5010
+ const [forward, back] = this._arrows;
5011
+ const step = (by) => {
5012
+ if (!this._circular) return Math.min(last, Math.max(0, this.activeIndex + by));
5013
+ if (this.activeIndex < 0) return by > 0 ? 0 : last;
5014
+ return (this.activeIndex + by + last + 1) % (last + 1);
5015
+ };
5016
+ if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
5017
+ return forward.includes(key) ? step(1) : back.includes(key) ? step(-1) : { Home: 0, End: last }[key];
5018
+ }
5019
+ };
5020
+
5021
+ // src/ts/components/columnChart.ts
5022
+ var ColumnChart = class extends LineChart {
5023
+ get _type() {
5024
+ return "column-chart";
5025
+ }
5026
+ get _key() {
5027
+ return "Expressive_ColumnChart";
5028
+ }
5029
+ get _fromZero() {
5030
+ return true;
5031
+ }
5032
+ get _points() {
5033
+ return false;
5034
+ }
5035
+ static init(els, options = {}) {
5036
+ return super.init(els, options);
5037
+ }
5038
+ static getInstance(el) {
5039
+ return el["Expressive_ColumnChart"];
5040
+ }
5041
+ _draw(drawn = this.series) {
5042
+ const columns = document.createElement("div");
5043
+ columns.className = "column-chart-columns";
5044
+ columns.setAttribute("aria-hidden", "true");
5045
+ const clamp = (value, y = drawn[0]?.y ?? this._y) => Math.min(100, Math.max(0, y(value)));
5046
+ columns.style.setProperty("--zero", `${clamp(0)}%`);
5047
+ this._plot.style.setProperty("--rows", String(this.labels.length));
5048
+ this.labels.forEach((_, index) => {
5049
+ const row = document.createElement("div");
5050
+ drawn.forEach((series) => {
5051
+ const to = series.tops[index];
5052
+ const from = series.base?.[index] ?? 0;
5053
+ if (series.base && (to === null || from === null)) return;
5054
+ const column = document.createElement("span");
5055
+ if (to !== null) {
5056
+ column.className = series.className;
5057
+ column.dataset.series = String(this.series.indexOf(series) % COLORS + 1);
5058
+ column.classList.toggle("negative", !series.base && to < from);
5059
+ column.style.setProperty("--top", `${clamp(Math.max(from, to), series.y)}%`);
5060
+ column.style.setProperty("--bottom", `${100 - clamp(Math.min(from, to), series.y)}%`);
5061
+ if (this._horizontal) column.dataset.text = series.text[index];
5062
+ }
5063
+ row.append(column);
5064
+ });
5065
+ columns.append(row);
5066
+ });
5067
+ return columns;
5068
+ }
5069
+ };
5070
+
5071
+ // src/ts/components/barChart.ts
5072
+ var BarChart = class extends ColumnChart {
5073
+ get _type() {
5074
+ return "bar-chart";
5075
+ }
5076
+ get _key() {
5077
+ return "Expressive_BarChart";
5078
+ }
5079
+ get _horizontal() {
5080
+ return true;
5081
+ }
5082
+ static init(els, options = {}) {
5083
+ return super.init(els, options);
5084
+ }
5085
+ static getInstance(el) {
5086
+ return el["Expressive_BarChart"];
5087
+ }
5088
+ /**
5089
+ * The column chart's spans; CSS reads their --top and --bottom across the
5090
+ * plot. With .values, a copy of the bars without their clipping holds each
5091
+ * cell's text, so text longer than a short bar can run past its end.
5092
+ */
5093
+ _draw() {
5094
+ const bars = super._draw();
5095
+ bars.className = "bar-chart-bars";
5096
+ bars.classList.toggle("rtl", getComputedStyle(this.el).direction === "rtl");
5097
+ if (!this.el.classList.contains("values")) return bars;
5098
+ const values = bars.cloneNode(true);
5099
+ values.classList.replace("bar-chart-bars", "bar-chart-values");
5100
+ values.querySelectorAll("span[data-text]").forEach((span) => {
5101
+ span.textContent = span.dataset.text;
5102
+ });
5103
+ const layers = document.createElement("div");
5104
+ layers.className = "bar-chart-layers";
5105
+ layers.append(bars, values);
5106
+ return layers;
5107
+ }
5108
+ };
5109
+
5110
+ // src/ts/components/pieChart.ts
5111
+ var COLORS2 = 6;
5112
+ var RADIUS = 46;
5113
+ var GROWN = 1.06;
5114
+ var LABEL_SHARE = 0.05;
5115
+ var _maskId = 0;
5116
+ var PieChart = class extends LineChart {
5117
+ get _type() {
5118
+ return "pie-chart";
5119
+ }
5120
+ get _key() {
5121
+ return "Expressive_PieChart";
5122
+ }
5123
+ get _points() {
5124
+ return false;
5125
+ }
5126
+ get _circular() {
5127
+ return true;
5128
+ }
5129
+ static init(els, options = {}) {
5130
+ return super.init(els, options);
5131
+ }
5132
+ static getInstance(el) {
5133
+ return el["Expressive_PieChart"];
5134
+ }
5135
+ show(index) {
5136
+ if (!this._tooltip || index === this.activeIndex) return;
5137
+ super.show(index);
5138
+ this._slices.forEach((slice, i) => slice?.classList.toggle("active", i === index));
5139
+ if (this._tooltip.hidden) return;
5140
+ const color = this._colors[index];
5141
+ this._tooltip.querySelectorAll("[data-series]").forEach((span) => {
5142
+ if (color) span.dataset.series = color;
5143
+ else delete span.dataset.series;
5144
+ });
5145
+ const share = this._share(index);
5146
+ const value = this._tooltip.querySelector("span:nth-of-type(2)");
5147
+ if (share && value) value.textContent += ` (${share})`;
5148
+ }
5149
+ /** CSS places the tooltip on the slice's middle from --turn. */
5150
+ _place(index) {
5151
+ const middle = (this._turns[index][0] + this._turns[index][1]) / 2;
5152
+ this._tooltip.style.setProperty("--turn", String(middle));
5153
+ this._tooltip.classList.toggle("end", middle < 0.5);
5154
+ }
5155
+ /** The row's share of the whole as a percentage, or '' for none. */
5156
+ _share(index) {
5157
+ const [start, end] = this._turns[index];
5158
+ if (end <= start) return "";
5159
+ const share = end - start;
5160
+ const options = { style: "percent", maximumFractionDigits: share < 0.1 ? 1 : 0 };
5161
+ try {
5162
+ return share.toLocaleString(this.el.closest("[lang]")?.lang || void 0, options);
5163
+ } catch {
5164
+ return share.toLocaleString(void 0, options);
5165
+ }
5166
+ }
5167
+ /** The legend: every row with its color, its cell and its share. */
5168
+ _guides() {
5169
+ const legend = document.createElement("ul");
5170
+ legend.className = "pie-chart-legend";
5171
+ legend.setAttribute("aria-hidden", "true");
5172
+ const text = this.series[0].text;
5173
+ legend.append(...this.labels.map((label, i) => {
5174
+ const li = document.createElement("li");
5175
+ if (this._colors[i]) li.dataset.series = this._colors[i];
5176
+ const [name, value, share] = [label, text[i], this._share(i)].map((content) => {
5177
+ const span = document.createElement("span");
5178
+ span.textContent = content;
5179
+ return span;
5180
+ });
5181
+ li.append(name, value, share);
5182
+ return li;
5183
+ }));
5184
+ return [legend];
5185
+ }
5186
+ /** The row under the pointer by its angle from the middle, or -1 off the slices. */
5187
+ _indexAt(e) {
5188
+ const [turn, share] = this._polarAt(e);
5189
+ const distance = share * 100;
5190
+ const hole = this.el.classList.contains("donut") ? parseFloat(getComputedStyle(this.el).getPropertyValue("--md-comp-pie-chart-hole")) || 0 : 0;
5191
+ if (distance < RADIUS * hole) return -1;
5192
+ const index = this._turns.findIndex(([start, end]) => start <= turn && turn < end);
5193
+ return distance > RADIUS * (index === this.activeIndex ? GROWN : 1) ? -1 : index;
5194
+ }
5195
+ /**
5196
+ * An SVG of slices in a -50 to 50 box, under a mask that cuts the gaps
5197
+ * between them and the donut's hole, which CSS sizes. The values and the
5198
+ * total are HTML over it.
5199
+ */
5200
+ _draw() {
5201
+ const values = this.series[0].values.map((value) => Math.max(0, value ?? 0));
5202
+ const total = values.reduce((sum, value) => sum + value, 0);
5203
+ let turn = 0;
5204
+ this._turns = values.map((value) => [turn, turn += total ? value / total : 0]);
5205
+ const drawn = this._turns.filter(([start, end]) => end > start).length;
5206
+ let slice = 0;
5207
+ this._colors = this._turns.map(([start, end]) => {
5208
+ if (end <= start) return void 0;
5209
+ const index = slice++;
5210
+ return String(index === drawn - 1 && index > 0 && index % COLORS2 === 0 ? 2 : index % COLORS2 + 1);
5211
+ });
5212
+ const at = (t, r = RADIUS) => {
5213
+ const angle = t * 2 * Math.PI;
5214
+ return `${(r * Math.sin(angle)).toFixed(3)},${(-r * Math.cos(angle)).toFixed(3)}`;
5215
+ };
5216
+ const pie = document.createElement("div");
5217
+ pie.className = "pie-chart-pie";
5218
+ pie.setAttribute("aria-hidden", "true");
5219
+ const svg = svgNode("svg", { viewBox: "-50 -50 100 100", focusable: "false" });
5220
+ const id = `pie-chart-mask-${++_maskId}`;
5221
+ const area = { x: "-50", y: "-50", width: "100", height: "100" };
5222
+ const mask = svgNode("mask", { id, maskUnits: "userSpaceOnUse", ...area });
5223
+ const edges = this._turns.filter(([start, end]) => end > start);
5224
+ mask.append(
5225
+ svgNode("rect", { ...area, fill: "white" }),
5226
+ svgNode("path", {
5227
+ class: "pie-chart-gaps",
5228
+ stroke: "black",
5229
+ d: edges.length > 1 ? edges.map(([start]) => `M0,0L${at(start, 50)}`).join("") : ""
5230
+ }),
5231
+ svgNode("circle", { class: "pie-chart-hole", r: String(RADIUS), fill: "black" })
5232
+ );
5233
+ const slices = svgNode("g", { mask: `url(#${id})` });
5234
+ const arc = `A${RADIUS},${RADIUS} 0`;
5235
+ this._slices = this._turns.map(([start, end], i) => {
5236
+ if (end <= start) return null;
5237
+ const slice2 = svgNode("path", {
5238
+ class: "pie-chart-slice",
5239
+ "data-series": this._colors[i],
5240
+ // A whole circle is two half arcs; one arc cannot end where it starts.
5241
+ d: end - start >= 1 ? `M0,${-RADIUS}${arc} 1 1 0,${RADIUS}${arc} 1 1 0,${-RADIUS}Z` : `M0,0L${at(start)}${arc} ${end - start > 0.5 ? 1 : 0} 1 ${at(end)}Z`
5242
+ });
5243
+ slices.append(slice2);
5244
+ return slice2;
5245
+ });
5246
+ svg.append(mask, slices);
5247
+ pie.append(svg);
5248
+ if (this.el.classList.contains("values")) {
5249
+ const labels = document.createElement("div");
5250
+ labels.className = "pie-chart-values";
5251
+ this._turns.forEach(([start, end], i) => {
5252
+ if (end - start < LABEL_SHARE) return;
5253
+ const label = document.createElement("span");
5254
+ label.dataset.series = this._colors[i];
5255
+ label.style.setProperty("--turn", String((start + end) / 2));
5256
+ label.textContent = this._share(i);
5257
+ labels.append(label);
5258
+ });
5259
+ pie.append(labels);
5260
+ }
5261
+ const middle = this._total("pie-chart-total");
5262
+ if (middle) pie.append(middle);
5263
+ return pie;
5264
+ }
5265
+ };
5266
+
5267
+ // src/ts/components/heatmapChart.ts
5268
+ var HeatmapChart = class extends LineChart {
5269
+ get _type() {
5270
+ return "heatmap-chart";
5271
+ }
5272
+ get _key() {
5273
+ return "Expressive_HeatmapChart";
5274
+ }
5275
+ static init(els, options = {}) {
5276
+ return super.init(els, options);
5277
+ }
5278
+ static getInstance(el) {
5279
+ return el["Expressive_HeatmapChart"];
5280
+ }
5281
+ /** Highlights one cell, numbered row by row. Pass -1 to hide the tooltip. */
5282
+ show(index) {
5283
+ if (!this._tooltip || index === this.activeIndex) return;
5284
+ this._cells[this.activeIndex]?.classList.remove("active");
5285
+ this.activeIndex = index;
5286
+ const cell = this._cells[index];
5287
+ this._tooltip.hidden = !cell;
5288
+ if (!cell) return;
5289
+ cell.classList.add("active");
5290
+ const columns = this.series.length;
5291
+ const [row, series] = [Math.floor(index / columns), this.series[index % columns]];
5292
+ const title = document.createElement("strong");
5293
+ title.textContent = this.labels[row];
5294
+ const [name, text] = [document.createElement("span"), document.createElement("span")];
5295
+ name.textContent = series.name;
5296
+ if (this._levels[index] === null) name.className = "gap";
5297
+ else name.style.setProperty("--level", String(this._levels[index]));
5298
+ text.textContent = series.text[row];
5299
+ this._tooltip.replaceChildren(title, name, text);
5300
+ const end = cell.offsetLeft + cell.offsetWidth / 2 > this._plot.clientWidth / 2;
5301
+ this._tooltip.classList.toggle("end", end);
5302
+ this._tooltip.style.left = `${cell.offsetLeft + (end ? 0 : cell.offsetWidth)}px`;
5303
+ this._tooltip.style.top = `${cell.offsetTop}px`;
5304
+ }
5305
+ /** Arrows move the way they point, so left and right swap in RTL. */
5306
+ _keyTarget(key) {
5307
+ if (key === "Escape") return this.activeIndex < 0 ? void 0 : -1;
5308
+ const [columns, last] = [this.series.length, this.labels.length - 1];
5309
+ const index = Math.max(0, this.activeIndex);
5310
+ const [row, column] = [Math.floor(index / columns), index % columns];
5311
+ const [next, back] = this._reversed ? ["ArrowLeft", "ArrowRight"] : ["ArrowRight", "ArrowLeft"];
5312
+ const to = {
5313
+ [next]: [row, column + 1],
5314
+ [back]: [row, column - 1],
5315
+ ArrowDown: [row + 1, column],
5316
+ ArrowUp: [row - 1, column],
5317
+ Home: [row, 0],
5318
+ End: [row, columns - 1]
5319
+ }[key];
5320
+ if (!to) return void 0;
5321
+ if (this.activeIndex < 0) return 0;
5322
+ const clamp = (value, max) => Math.min(max, Math.max(0, value));
5323
+ return clamp(to[0], last) * columns + clamp(to[1], columns - 1);
5324
+ }
5325
+ /**
5326
+ * The cell under the pointer by its position, or -1 off the grid. A touch
5327
+ * keeps its first target while it drags, so the target cannot be used. The
5328
+ * cells and gaps are even, so each band's edge falls in a gap and a gap
5329
+ * counts as the nearer cell.
5330
+ */
5331
+ _indexAt(e) {
5332
+ const [columns, rows] = [this.series.length, this.labels.length];
5333
+ const first = this._cells[0].getBoundingClientRect();
5334
+ const last = this._cells[this._cells.length - 1].getBoundingClientRect();
5335
+ const [left, right] = [Math.min(first.left, last.left), Math.max(first.right, last.right)];
5336
+ const [x, y] = [e.clientX, e.clientY];
5337
+ if (x < left || x > right || y < first.top || y > last.bottom) return -1;
5338
+ const band = (offset, size, count) => Math.min(count - 1, Math.floor(offset / (size || 1) * count));
5339
+ const column = band(x - left, right - left, columns);
5340
+ return band(y - first.top, last.bottom - first.top, rows) * columns + (this._reversed ? columns - 1 - column : column);
5341
+ }
5342
+ /**
5343
+ * The legend: a gradient from the lowest cell's shade to the highest's,
5344
+ * labelled with their text. A fixed scale wider than the data shows only
5345
+ * the part the data uses.
5346
+ */
5347
+ _guides() {
5348
+ const legend = document.createElement("div");
5349
+ legend.className = "heatmap-chart-legend";
5350
+ legend.setAttribute("aria-hidden", "true");
5351
+ const [low, scale, high] = ["span", "span", "span"].map((tag) => document.createElement(tag));
5352
+ [low.textContent, high.textContent] = this._ends;
5353
+ scale.className = "heatmap-chart-scale";
5354
+ if (this._reversed) scale.style.setProperty("--heatmap-chart-to", "left");
5355
+ scale.style.setProperty("--low", String(this._endLevels[0]));
5356
+ scale.style.setProperty("--high", String(this._endLevels[1]));
5357
+ legend.append(low, scale, high);
5358
+ return [legend];
5359
+ }
5360
+ /**
5361
+ * A grid of the row headers and cells, row by row, with the column headers
5362
+ * below. Each cell's --level shades it in CSS.
5363
+ */
5364
+ _draw() {
5365
+ const columns = this.series.length;
5366
+ let [low, high] = [Infinity, -Infinity];
5367
+ this._ends = ["", ""];
5368
+ this.series.forEach((series) => series.values.forEach((value, row) => {
5369
+ if (value === null) return;
5370
+ if (value < low) [low, this._ends[0]] = [value, series.text[row]];
5371
+ if (value > high) [high, this._ends[1]] = [value, series.text[row]];
5372
+ }));
5373
+ const option = (name) => {
5374
+ const value = parseFloat(this.el.dataset[name] ?? "");
5375
+ return Number.isFinite(value) ? value : this.options[name];
5376
+ };
5377
+ const [min, max] = [option("min") ?? low, option("max") ?? high];
5378
+ const shade = (value) => Math.min(1, Math.max(0, (value - min) / (max - min || 1)));
5379
+ this._endLevels = [shade(low), shade(high)];
5380
+ const grid = document.createElement("div");
5381
+ grid.className = "heatmap-chart-grid";
5382
+ grid.setAttribute("aria-hidden", "true");
5383
+ grid.style.setProperty("--columns", String(columns));
5384
+ grid.style.setProperty("--rows", String(this.labels.length));
5385
+ const span = (className, text = "") => {
5386
+ const element = document.createElement("span");
5387
+ element.className = className;
5388
+ element.textContent = text;
5389
+ return element;
5390
+ };
5391
+ const label = (cell) => cell?.dataset.label ?? cell?.textContent.trim() ?? "";
5392
+ const values = this.el.classList.contains("values");
5393
+ this._cells = [];
5394
+ this._levels = [];
5395
+ this.labels.forEach((_, row) => {
5396
+ grid.append(span("heatmap-chart-row", label(this._rows[row].cells[0])));
5397
+ this.series.forEach((series) => {
5398
+ const value = series.values[row];
5399
+ const level = value === null ? null : shade(value);
5400
+ const cell = span("heatmap-chart-cell", values ? series.text[row] : "");
5401
+ if (level === null) cell.classList.add("gap");
5402
+ else cell.style.setProperty("--level", String(level));
5403
+ this._cells.push(cell);
5404
+ this._levels.push(level);
5405
+ grid.append(cell);
5406
+ });
5407
+ });
5408
+ const labels = this._header.slice(1, columns + 1).map(label);
5409
+ labels.forEach((text, column) => {
5410
+ if (!text) return;
5411
+ let end = column + 1;
5412
+ while (end < columns && !labels[end]) end++;
5413
+ const element = span("heatmap-chart-column", text);
5414
+ element.classList.toggle("run", end > column + 1);
5415
+ element.style.gridArea = `${this.labels.length + 1} / ${column + 2} / auto / ${end + 2}`;
5416
+ grid.append(element);
5417
+ });
5418
+ return grid;
5419
+ }
5420
+ };
5421
+
5422
+ // src/ts/components/radarChart.ts
5423
+ var RINGS = 4;
5424
+ var RadarChart = class extends LineChart {
5425
+ get _type() {
5426
+ return "radar-chart";
5427
+ }
5428
+ get _key() {
5429
+ return "Expressive_RadarChart";
5430
+ }
5431
+ /** The scale starts at zero, the middle, so shapes compare by size. */
5432
+ get _fromZero() {
5433
+ return true;
5434
+ }
5435
+ get _circular() {
5436
+ return true;
5437
+ }
5438
+ static init(els, options = {}) {
5439
+ return super.init(els, options);
5440
+ }
5441
+ static getInstance(el) {
5442
+ return el["Expressive_RadarChart"];
5443
+ }
5444
+ /**
5445
+ * The cursor is a spoke turned by --turn, and the tooltip sits past its
5446
+ * tip. LineChart's points on the spoke keep their top, a share of the
5447
+ * radius, which stops at the rim and the middle like the shapes.
5448
+ */
5449
+ _place(index) {
5450
+ for (const dot of this._cursor.children) {
5451
+ dot.style.top = `${Math.min(100, Math.max(0, parseFloat(dot.style.top)))}%`;
5452
+ }
5453
+ const turn = this._turn(index);
5454
+ this._cursor.style.setProperty("--turn", String(turn));
5455
+ this._tooltip.style.setProperty("--turn", String(turn));
5456
+ this._tooltip.classList.toggle("end", turn > 0 && turn < 0.5);
5457
+ }
5458
+ /** A row's spoke as a fraction of a turn clockwise from the top. */
5459
+ _turn(index) {
5460
+ return index / this.labels.length;
5461
+ }
5462
+ /** Only the legend; the labels go round the rim in _draw. */
5463
+ _guides() {
5464
+ return this._legend();
5465
+ }
5466
+ /** The nearest spoke to the pointer, by its angle from the middle. */
5467
+ _indexAt(e) {
5468
+ const [turn] = this._polarAt(e);
5469
+ return Math.round(turn * this.labels.length) % this.labels.length;
5470
+ }
5471
+ /**
5472
+ * An SVG in a -50 to 50 box: the rings and spokes of the grid, then each
5473
+ * series' shape, with its points for .points. The rim is the box's edge;
5474
+ * the labels round it are HTML.
5475
+ */
5476
+ _draw() {
5477
+ const at = (index, share2) => {
5478
+ const angle = this._turn(index) * 2 * Math.PI;
5479
+ return `${(50 * share2 * Math.sin(angle)).toFixed(3)},${(-50 * share2 * Math.cos(angle)).toFixed(3)}`;
5480
+ };
5481
+ const share = (value) => Math.min(1, Math.max(0, 1 - this._y(value) / 100));
5482
+ const rows = this.labels.map((_, i) => i);
5483
+ const ring = (r) => `M${rows.map((i) => at(i, r)).join("L")}Z`;
5484
+ const svg = svgNode("svg", { viewBox: "-50 -50 100 100", "aria-hidden": "true", focusable: "false" });
5485
+ svg.append(svgNode("path", {
5486
+ class: "radar-chart-grid",
5487
+ d: Array.from({ length: RINGS }, (_, r) => ring((r + 1) / RINGS)).join("") + rows.map((i) => `M0,0L${at(i, 1)}`).join("")
5488
+ }));
5489
+ const points = this.el.classList.contains("points");
5490
+ this.series.forEach((series, s) => {
5491
+ const group = svgNode("g", {
5492
+ class: `radar-chart-series ${series.className}`.trim(),
5493
+ "data-series": String(s % COLORS + 1)
5494
+ });
5495
+ const drawn = rows.filter((i) => series.values[i] !== null);
5496
+ const corners = drawn.map((i) => at(i, share(series.values[i])));
5497
+ group.append(svgNode("path", { class: "radar-chart-shape", d: corners.length ? `M${corners.join("L")}Z` : "" }));
5498
+ if (points) group.append(svgNode("path", { class: "radar-chart-points", d: corners.map((c) => `M${c}h0`).join("") }));
5499
+ svg.append(group);
5500
+ });
5501
+ const labels = document.createElement("ol");
5502
+ labels.className = "radar-chart-axes";
5503
+ labels.setAttribute("aria-hidden", "true");
5504
+ this.labels.forEach((text, i) => {
5505
+ const li = document.createElement("li");
5506
+ li.textContent = text;
5507
+ li.style.setProperty("--turn", String(this._turn(i)));
5508
+ labels.append(li);
5509
+ });
5510
+ const web = document.createElement("div");
5511
+ web.className = "radar-chart-web";
5512
+ web.append(svg, labels);
5513
+ return web;
5514
+ }
5515
+ };
5516
+
5517
+ // src/ts/components/radialChart.ts
5518
+ var RADIUS2 = 46;
5519
+ var FILL = 0.8;
5520
+ var RadialChart = class extends LineChart {
5521
+ get _type() {
5522
+ return "radial-chart";
5523
+ }
5524
+ get _key() {
5525
+ return "Expressive_RadialChart";
5526
+ }
5527
+ get _points() {
5528
+ return false;
5529
+ }
5530
+ get _circular() {
5531
+ return true;
5532
+ }
5533
+ get _gauge() {
5534
+ return this.el.classList.contains("gauge");
5535
+ }
5536
+ static init(els, options = {}) {
5537
+ return super.init(els, options);
5538
+ }
5539
+ static getInstance(el) {
5540
+ return el["Expressive_RadialChart"];
5541
+ }
5542
+ show(index) {
5543
+ if (!this._tooltip || index === this.activeIndex) return;
5544
+ super.show(index);
5545
+ this._rings.forEach((ring, i) => ring.classList.toggle("active", i === index));
5546
+ if (this._tooltip.hidden) return;
5547
+ const color = this._color(index);
5548
+ this._tooltip.querySelectorAll("[data-series]").forEach((span) => {
5549
+ span.dataset.series = color;
5550
+ });
5551
+ }
5552
+ /** The tooltip sits where the ring's fill ends. */
5553
+ _place(index) {
5554
+ const turn = this._turn(this._fractions[index]);
5555
+ const r = this._radii[index];
5556
+ const [x, y] = [r * Math.sin(turn * 2 * Math.PI), -r * Math.cos(turn * 2 * Math.PI)];
5557
+ this._tooltip.style.left = `${50 + x}%`;
5558
+ this._tooltip.style.top = `${(50 + y) / (this._gauge ? 50 : 100) * 100}%`;
5559
+ this._tooltip.classList.toggle("end", x > 0);
5560
+ }
5561
+ _color(index) {
5562
+ return String(index % COLORS + 1);
5563
+ }
5564
+ /** A fraction of the ring as a turn clockwise from the top. */
5565
+ _turn(fraction) {
5566
+ return this._gauge ? fraction / 2 - 0.25 : fraction;
5567
+ }
5568
+ /** For more than one row, a legend of every row with its color and cell. */
5569
+ _guides() {
5570
+ if (this.labels.length < 2) return [];
5571
+ const legend = document.createElement("ul");
5572
+ legend.className = "radial-chart-legend";
5573
+ legend.setAttribute("aria-hidden", "true");
5574
+ const text = this.series[0].text;
5575
+ legend.append(...this.labels.map((label, i) => {
5576
+ const li = document.createElement("li");
5577
+ li.dataset.series = this._color(i);
5578
+ const [name, value] = [label, text[i]].map((content) => {
5579
+ const span = document.createElement("span");
5580
+ span.textContent = content;
5581
+ return span;
5582
+ });
5583
+ li.append(name, value);
5584
+ return li;
5585
+ }));
5586
+ return [legend];
5587
+ }
5588
+ /** The ring under the pointer by its distance from the middle, or -1. */
5589
+ _indexAt(e) {
5590
+ const box = this._plot.getBoundingClientRect();
5591
+ const [x, y] = [e.clientX - box.left - box.width / 2, e.clientY - box.top - box.width / 2];
5592
+ if (this._gauge && y > 0) return -1;
5593
+ const distance = Math.hypot(x, y) / box.width * 100;
5594
+ const step = this._radii.length > 1 ? this._radii[0] - this._radii[1] : this._width;
5595
+ return this._radii.findIndex((r) => Math.abs(distance - r) <= step / 2);
5596
+ }
5597
+ /**
5598
+ * An SVG of rings in a -50 to 50 box, the top half for a gauge. Each row
5599
+ * is a track circle and a fill circle whose dash is its share; CSS draws
5600
+ * them from --radial-chart-dash. The total is HTML over it.
5601
+ */
5602
+ _draw() {
5603
+ const style = getComputedStyle(this.el);
5604
+ const token = (name, fallback) => {
5605
+ const value = parseFloat(style.getPropertyValue(`--md-comp-radial-chart-${name}`));
5606
+ return Number.isFinite(value) ? value : fallback;
5607
+ };
5608
+ const count = this.labels.length;
5609
+ let [width, gap] = [Math.max(0, token("thickness", 8)), Math.max(0, token("gap", 2))];
5610
+ const scale = Math.min(1, RADIUS2 * FILL / (count * width + (count - 1) * gap || 1));
5611
+ [width, gap] = [width * scale, gap * scale];
5612
+ this._width = width;
5613
+ this._radii = this.labels.map((_, i) => RADIUS2 - width / 2 - i * (width + gap));
5614
+ const max = [parseFloat(this.el.dataset.max ?? ""), this.options.max ?? NaN, 100].find((value) => value > 0);
5615
+ this._fractions = this.series[0].values.map((value) => Math.min(1, Math.max(0, (value ?? 0) / max)));
5616
+ const gauge = this._gauge;
5617
+ const svg = svgNode("svg", { viewBox: gauge ? "-50 -50 100 50" : "-50 -50 100 100", focusable: "false" });
5618
+ const transform = gauge ? "rotate(180)" : "rotate(-90)";
5619
+ const span = gauge ? 0.5 : 1;
5620
+ this._rings = this._radii.map((r, i) => {
5621
+ const length = 2 * Math.PI * r;
5622
+ const circle = (name, fraction, capped) => {
5623
+ const ring2 = svgNode("circle", { class: name, r: r.toFixed(3), transform, "stroke-width": width.toFixed(3) });
5624
+ const dash = Math.max(1e-3, fraction * span * length - (capped ? width : 0));
5625
+ ring2.style.setProperty("--radial-chart-dash", `${dash.toFixed(3)} ${length.toFixed(3)}`);
5626
+ if (capped) ring2.style.setProperty("--radial-chart-offset", (-width / 2).toFixed(3));
5627
+ return ring2;
5628
+ };
5629
+ const ring = circle("radial-chart-ring", this._fractions[i], true);
5630
+ ring.dataset.series = this._color(i);
5631
+ if (!this._fractions[i]) ring.classList.add("empty");
5632
+ svg.append(circle("radial-chart-track", 1, gauge), ring);
5633
+ return ring;
5634
+ });
5635
+ const rings = document.createElement("div");
5636
+ rings.className = "radial-chart-rings";
5637
+ rings.setAttribute("aria-hidden", "true");
5638
+ rings.style.setProperty("--radial-chart-middle", `${(this._radii[count - 1] - this._width / 2) * 2}%`);
5639
+ rings.append(svg);
5640
+ const total = this._total("radial-chart-total");
5641
+ if (total) rings.append(total);
5642
+ return rings;
5643
+ }
5644
+ };
5645
+
5646
+ // src/ts/components/mixedChart.ts
5647
+ var has = (series, name) => series.className.split(/\s+/).includes(name);
5648
+ var MixedChart = class extends ColumnChart {
5649
+ get _type() {
5650
+ return "mixed-chart";
5651
+ }
5652
+ get _key() {
5653
+ return "Expressive_MixedChart";
5654
+ }
5655
+ get _points() {
5656
+ return true;
5657
+ }
5658
+ static init(els, options = {}) {
5659
+ return super.init(els, options);
5660
+ }
5661
+ static getInstance(el) {
5662
+ return el["Expressive_MixedChart"];
5663
+ }
5664
+ /** The start scale, then the end scale; each includes zero if it has columns. */
5665
+ _scale(series, min, max) {
5666
+ const end = series.filter((each) => has(each, "end"));
5667
+ const start = series.filter((each) => !end.includes(each));
5668
+ const columns = (list) => list.some((each) => has(each, "column"));
5669
+ super._scale(end, this._number("endMin"), this._number("endMax"), columns(end));
5670
+ return super._scale(start, min, max, columns(start));
5671
+ }
5672
+ /** Columns behind, then the lines and areas over them. */
5673
+ _draw() {
5674
+ const marks = document.createElement("div");
5675
+ marks.className = "mixed-chart-marks";
5676
+ const columns = super._draw(this.series.filter((series) => has(series, "column")));
5677
+ columns.className = "mixed-chart-columns";
5678
+ marks.append(columns, LineChart.prototype["_draw"].call(this, this.series.filter((series) => !has(series, "column"))));
5679
+ return marks;
5680
+ }
5681
+ };
5682
+
5683
+ // src/ts/components/select.ts
5684
+ var _defaults15 = {
3922
5685
  classes: "",
3923
5686
  menuOptions: {}
3924
5687
  };
@@ -4000,7 +5763,7 @@ var Expressive = (() => {
4000
5763
  this._setupEventHandlers();
4001
5764
  }
4002
5765
  static get defaults() {
4003
- return _defaults12;
5766
+ return _defaults15;
4004
5767
  }
4005
5768
  /**
4006
5769
  * Initializes instances of FormSelect.
@@ -4425,7 +6188,7 @@ var Expressive = (() => {
4425
6188
  };
4426
6189
 
4427
6190
  // src/ts/plugins/dockedDisplayPlugin.ts
4428
- var _defaults13 = {
6191
+ var _defaults16 = {
4429
6192
  margin: 5,
4430
6193
  transition: 10,
4431
6194
  duration: 250,
@@ -4474,7 +6237,7 @@ var Expressive = (() => {
4474
6237
  };
4475
6238
  this.el = el;
4476
6239
  this.options = {
4477
- ..._defaults13,
6240
+ ..._defaults16,
4478
6241
  ...options
4479
6242
  };
4480
6243
  this.container = document.createElement("div");
@@ -4495,7 +6258,7 @@ var Expressive = (() => {
4495
6258
  };
4496
6259
 
4497
6260
  // src/ts/components/datepicker.ts
4498
- var _defaults14 = {
6261
+ var _defaults17 = {
4499
6262
  // the default output format for the input field value
4500
6263
  format: "mmm dd, yyyy",
4501
6264
  // Used to create date object from current input string
@@ -4780,7 +6543,7 @@ var Expressive = (() => {
4780
6543
  }
4781
6544
  }
4782
6545
  static get defaults() {
4783
- return _defaults14;
6546
+ return _defaults17;
4784
6547
  }
4785
6548
  /**
4786
6549
  * Initializes instances of Datepicker.
@@ -5595,8 +7358,402 @@ var Expressive = (() => {
5595
7358
  </div>`;
5596
7359
  var Datepicker = _Datepicker;
5597
7360
 
7361
+ // src/ts/components/messageScroller.ts
7362
+ var _defaults18 = {
7363
+ autoScroll: true,
7364
+ defaultScrollPosition: "end",
7365
+ peek: 64
7366
+ };
7367
+ var EDGE = 2;
7368
+ var UP_KEYS = /* @__PURE__ */ new Set(["ArrowUp", "PageUp", "Home"]);
7369
+ var RELEASE_EVENTS = ["pointerup", "pointercancel", "touchend", "touchcancel"];
7370
+ var MessageScroller = class _MessageScroller extends Component {
7371
+ constructor(el, options) {
7372
+ super(el, options, _MessageScroller);
7373
+ this._opened = false;
7374
+ this._following = false;
7375
+ /** A press is down: it may be a tap or a scrollbar drag. */
7376
+ this._holding = false;
7377
+ this._lastTop = 0;
7378
+ /** Where a programmatic scroll is heading; null when none is running. */
7379
+ this._target = null;
7380
+ /** Distance left to the target at the last scroll event. */
7381
+ this._distance = Infinity;
7382
+ /** The first visible message and its offset from the viewport top. */
7383
+ this._ref = null;
7384
+ this._refOffset = 0;
7385
+ /** The latest anchored turn, which the spacer lets reach the top. */
7386
+ this._anchor = null;
7387
+ this._spacer = 0;
7388
+ this._last = null;
7389
+ this._resizeObserver = null;
7390
+ this._mutationObserver = null;
7391
+ // A programmatic scroll ends when it arrives, or when a scroll moves away
7392
+ // from its target because something else took over.
7393
+ this._handleScroll = () => {
7394
+ if (this._target !== null) {
7395
+ const distance = Math.abs(this.viewport.scrollTop - this._resolve(this._target));
7396
+ if (distance > EDGE && distance < this._distance) {
7397
+ this._distance = distance;
7398
+ return;
7399
+ }
7400
+ this._target = null;
7401
+ }
7402
+ this._settle();
7403
+ };
7404
+ this._handleUserScroll = (e) => {
7405
+ if (e.type === "wheel" && e.deltaY >= 0) return;
7406
+ if (e.type === "keydown" && !UP_KEYS.has(e.key)) return;
7407
+ this._target = null;
7408
+ if (e.type === "pointerdown" || e.type === "touchstart") this._holding = true;
7409
+ else this._following = false;
7410
+ };
7411
+ this._handleRelease = () => {
7412
+ if (!this._holding) return;
7413
+ this._holding = false;
7414
+ if (this._following) this.viewport.scrollTop = this.viewport.scrollHeight;
7415
+ this._sync();
7416
+ };
7417
+ this._handleButtonClick = () => this.scrollToEnd();
7418
+ this._handleMutations = (records) => {
7419
+ for (const record of records) {
7420
+ record.addedNodes.forEach((node) => {
7421
+ if (node instanceof Element) this._resizeObserver?.observe(node);
7422
+ });
7423
+ record.removedNodes.forEach((node) => {
7424
+ if (node instanceof Element) this._resizeObserver?.unobserve(node);
7425
+ });
7426
+ }
7427
+ const previous = this._last;
7428
+ this._last = this.viewport.lastElementChild;
7429
+ if (!this._opened) return;
7430
+ if (!this._following && this._target === null) this._restore();
7431
+ if (!this.options.autoScroll) return;
7432
+ if (previous && previous.parentElement !== this.viewport) return;
7433
+ for (let item = this._last; item && item !== previous; item = item.previousElementSibling) {
7434
+ if (item.hasAttribute("data-scroll-anchor")) {
7435
+ this._anchor = item;
7436
+ this._updateSpacer();
7437
+ this._following = true;
7438
+ this._scrollTo(this._top(item) - this.options.peek);
7439
+ return;
7440
+ }
7441
+ }
7442
+ };
7443
+ this._handleResize = () => {
7444
+ if (!this._opened) {
7445
+ if (this.viewport.clientHeight > 0) this._open();
7446
+ return;
7447
+ }
7448
+ this._updateSpacer();
7449
+ const v = this.viewport;
7450
+ if (this._target === "end") this._scrollTo("end");
7451
+ else if (this._target === null) {
7452
+ if (this._following && !this._holding) v.scrollTop = v.scrollHeight;
7453
+ else this._restore();
7454
+ }
7455
+ this._sync();
7456
+ };
7457
+ this.el["Expressive_MessageScroller"] = this;
7458
+ this.options = {
7459
+ ..._MessageScroller.defaults,
7460
+ ...options
7461
+ };
7462
+ this.viewport = el.querySelector(":scope > .message-scroller-viewport");
7463
+ this.button = el.querySelector(":scope > .message-scroller-button");
7464
+ if (!this.viewport) {
7465
+ console.error("MessageScroller needs a .message-scroller-viewport child.");
7466
+ return;
7467
+ }
7468
+ this._last = this.viewport.lastElementChild;
7469
+ if (this.viewport.clientHeight > 0) this._open();
7470
+ if (typeof ResizeObserver !== "undefined") {
7471
+ this._resizeObserver = new ResizeObserver(this._handleResize);
7472
+ this._resizeObserver.observe(this.viewport, { box: "border-box" });
7473
+ for (const item of Array.from(this.viewport.children)) this._resizeObserver.observe(item);
7474
+ }
7475
+ this._mutationObserver = new MutationObserver(this._handleMutations);
7476
+ this._mutationObserver.observe(this.viewport, { childList: true });
7477
+ this.viewport.addEventListener("scroll", this._handleScroll, { passive: true });
7478
+ for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
7479
+ this.viewport.addEventListener(type, this._handleUserScroll, { passive: true });
7480
+ }
7481
+ for (const type of RELEASE_EVENTS) document.addEventListener(type, this._handleRelease);
7482
+ this.button?.addEventListener("click", this._handleButtonClick);
7483
+ }
7484
+ static get defaults() {
7485
+ return _defaults18;
7486
+ }
7487
+ /**
7488
+ * Initializes instances of MessageScroller.
7489
+ * @param els HTML elements.
7490
+ * @param options Component options.
7491
+ */
7492
+ static init(els, options = {}) {
7493
+ return super.init(els, options, _MessageScroller);
7494
+ }
7495
+ static getInstance(el) {
7496
+ return el["Expressive_MessageScroller"];
7497
+ }
7498
+ destroy() {
7499
+ this.el["Expressive_MessageScroller"] = void 0;
7500
+ if (!this.viewport) return;
7501
+ this._resizeObserver?.disconnect();
7502
+ this._mutationObserver?.disconnect();
7503
+ this.viewport.removeEventListener("scroll", this._handleScroll);
7504
+ for (const type of ["wheel", "touchstart", "pointerdown", "keydown"]) {
7505
+ this.viewport.removeEventListener(type, this._handleUserScroll);
7506
+ }
7507
+ for (const type of RELEASE_EVENTS) document.removeEventListener(type, this._handleRelease);
7508
+ this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
7509
+ if (this.button) {
7510
+ this.button.removeEventListener("click", this._handleButtonClick);
7511
+ this.button.inert = false;
7512
+ }
7513
+ }
7514
+ /** Scrolls to the latest message and resumes following. */
7515
+ scrollToEnd() {
7516
+ this._following = this.options.autoScroll;
7517
+ this._scrollTo("end");
7518
+ }
7519
+ /** Scrolls to the first message. */
7520
+ scrollToStart() {
7521
+ this._following = false;
7522
+ this._scrollTo(0);
7523
+ }
7524
+ /**
7525
+ * Scrolls the element with this id to the top of the viewport, below the
7526
+ * peek, and stops following.
7527
+ * @returns false when no such element is inside the viewport.
7528
+ */
7529
+ scrollToMessage(id) {
7530
+ const target = Utils.getElementById(this.viewport, id);
7531
+ if (!target || !this.viewport.contains(target)) return false;
7532
+ this._following = false;
7533
+ this._scrollTo(this._top(target) - this.options.peek);
7534
+ return true;
7535
+ }
7536
+ _open() {
7537
+ this._opened = true;
7538
+ const v = this.viewport;
7539
+ const position = this.options.defaultScrollPosition;
7540
+ const anchors = v.querySelectorAll(":scope > [data-scroll-anchor]");
7541
+ const anchor = position === "last-anchor" ? anchors[anchors.length - 1] : null;
7542
+ if (position === "start") v.scrollTop = 0;
7543
+ else if (anchor && this._contentEnd() - this._top(anchor) + this.options.peek > v.clientHeight) {
7544
+ v.scrollTop = this._top(anchor) - this.options.peek;
7545
+ } else v.scrollTop = v.scrollHeight;
7546
+ this._settle();
7547
+ }
7548
+ /**
7549
+ * Records where the reader is once no programmatic scroll is running.
7550
+ * Content can grow between a scroll and the resize that re-pins the end,
7551
+ * so only moving toward the start releases following.
7552
+ */
7553
+ _settle() {
7554
+ const top = this.viewport.scrollTop;
7555
+ const kept = this._following && top >= this._lastTop - EDGE;
7556
+ this._following = this.options.autoScroll && (kept || this._atEnd());
7557
+ this._lastTop = top;
7558
+ this._capture();
7559
+ this._sync();
7560
+ }
7561
+ _scrollTo(target) {
7562
+ const v = this.viewport;
7563
+ if (Math.abs(v.scrollTop - this._resolve(target)) <= EDGE) {
7564
+ this._target = null;
7565
+ this._settle();
7566
+ return;
7567
+ }
7568
+ this._target = target;
7569
+ this._distance = Infinity;
7570
+ const reduce = typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches;
7571
+ v.scrollTo({
7572
+ top: target === "end" ? v.scrollHeight : target,
7573
+ behavior: reduce ? "auto" : "smooth"
7574
+ });
7575
+ }
7576
+ _resolve(target) {
7577
+ const max = this.viewport.scrollHeight - this.viewport.clientHeight;
7578
+ return target === "end" ? max : Math.max(0, Math.min(target, max));
7579
+ }
7580
+ _atEnd() {
7581
+ const v = this.viewport;
7582
+ return v.scrollHeight - v.scrollTop - v.clientHeight <= EDGE;
7583
+ }
7584
+ /** Offset of an element from the top of the scrolled content. */
7585
+ _top(el) {
7586
+ const v = this.viewport;
7587
+ return el.getBoundingClientRect().top - v.getBoundingClientRect().top - v.clientTop + v.scrollTop;
7588
+ }
7589
+ /** Bottom of the content without the spacer, padding included. */
7590
+ _contentEnd() {
7591
+ const last = this.viewport.lastElementChild;
7592
+ if (!last) return 0;
7593
+ const padding = parseFloat(getComputedStyle(this.viewport).paddingBlockEnd) - this._spacer;
7594
+ return this._top(last) + last.getBoundingClientRect().height + padding;
7595
+ }
7596
+ /** Remembers the first visible message so prepends can keep it in place. */
7597
+ _capture() {
7598
+ const items = this.viewport.children;
7599
+ const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
7600
+ let low = 0;
7601
+ let high = items.length - 1;
7602
+ while (low < high) {
7603
+ const mid = low + high >> 1;
7604
+ if (items[mid].getBoundingClientRect().bottom > top) high = mid;
7605
+ else low = mid + 1;
7606
+ }
7607
+ this._ref = items[low] ?? null;
7608
+ this._refOffset = this._ref ? this._ref.getBoundingClientRect().top - top : 0;
7609
+ }
7610
+ /** Moves the remembered message back to where the reader last saw it. */
7611
+ _restore() {
7612
+ if (this._ref?.parentElement !== this.viewport) {
7613
+ this._capture();
7614
+ return;
7615
+ }
7616
+ const top = this.viewport.getBoundingClientRect().top + this.viewport.clientTop;
7617
+ const delta = this._ref.getBoundingClientRect().top - top - this._refOffset;
7618
+ if (Math.abs(delta) >= 1) this.viewport.scrollTop += delta;
7619
+ }
7620
+ /** Sizes the blank space that lets the latest anchored turn reach the top. */
7621
+ _updateSpacer() {
7622
+ let space = 0;
7623
+ if (this._anchor?.parentElement === this.viewport) {
7624
+ const room = this._top(this._anchor) - this.options.peek + this.viewport.clientHeight;
7625
+ space = Math.max(0, Math.ceil(room - this._contentEnd()));
7626
+ } else this._anchor = null;
7627
+ if (space === this._spacer) return;
7628
+ this._spacer = space;
7629
+ if (space) this.viewport.style.setProperty("--md-comp-message-scroller-spacer", `${space}px`);
7630
+ else this.viewport.style.removeProperty("--md-comp-message-scroller-spacer");
7631
+ }
7632
+ /** The button is inert while there is nothing below. */
7633
+ _sync() {
7634
+ if (!this.button) return;
7635
+ const idle = this._atEnd();
7636
+ const root = this.button.getRootNode();
7637
+ if (idle && root.activeElement === this.button) this.viewport.focus({ preventScroll: true });
7638
+ this.button.inert = idle;
7639
+ }
7640
+ };
7641
+
7642
+ // src/ts/components/richTextEditor.ts
7643
+ var _defaults19 = {
7644
+ Editor: null,
7645
+ extensions: []
7646
+ };
7647
+ var NAMING = ["aria-label", "aria-labelledby", "aria-describedby"];
7648
+ var RichTextEditor = class _RichTextEditor extends Component {
7649
+ constructor(el, options) {
7650
+ super(el, options, _RichTextEditor);
7651
+ /** The Tiptap editor. */
7652
+ this.editor = null;
7653
+ /** The element the editor is mounted in. */
7654
+ this.content = null;
7655
+ this._actions = /* @__PURE__ */ new Map();
7656
+ this._label = null;
7657
+ this._handleClick = (e) => {
7658
+ const button = e.target.closest("button[data-command]");
7659
+ const action = this._actions.get(button);
7660
+ if (action && !button.disabled) this.editor.chain().focus()[action.command](action.attributes).run();
7661
+ };
7662
+ this._handleLabelClick = () => {
7663
+ this.editor.chain().focus().run();
7664
+ };
7665
+ this._handleUpdate = () => {
7666
+ this.textarea.value = this.editor.isEmpty ? "" : this.editor.getHTML();
7667
+ this.textarea.dispatchEvent(new Event("input", { bubbles: true }));
7668
+ };
7669
+ this._handleTransaction = () => {
7670
+ const can = this.editor.isEditable && this.editor.can();
7671
+ for (const [button, { command, attributes, active }] of this._actions) {
7672
+ button.disabled = !can || !(command in this.editor.commands) || !can[command](attributes);
7673
+ if (!active) continue;
7674
+ const on = this.editor.isActive(active, attributes);
7675
+ button.setAttribute("aria-pressed", String(on));
7676
+ button.classList.toggle("active", on);
7677
+ }
7678
+ };
7679
+ this.el["Expressive_RichTextEditor"] = this;
7680
+ this.options = {
7681
+ ..._RichTextEditor.defaults,
7682
+ ...options
7683
+ };
7684
+ this.textarea = el.querySelector(":scope > textarea");
7685
+ if (!this.textarea || !this.options.Editor) {
7686
+ console.error("RichTextEditor needs a <textarea> child and the Tiptap Editor class in options.Editor.");
7687
+ return;
7688
+ }
7689
+ for (const button of el.querySelectorAll("button[data-command]")) {
7690
+ const command = button.dataset.command;
7691
+ const attributes2 = button.dataset.attributes ? JSON.parse(button.dataset.attributes) : void 0;
7692
+ const active = command.startsWith("toggle") ? command[6].toLowerCase() + command.slice(7) : void 0;
7693
+ this._actions.set(button, { command, attributes: attributes2, active });
7694
+ }
7695
+ const attributes = { role: "textbox", "aria-multiline": "true" };
7696
+ for (const name of NAMING) {
7697
+ const value = this.textarea.getAttribute(name);
7698
+ if (value) attributes[name] = value;
7699
+ }
7700
+ this._label = this.textarea.labels?.[0] ?? null;
7701
+ if (this._label && !attributes["aria-labelledby"]) {
7702
+ if (!this._label.id) this._label.id = `rich-text-editor-label-${Utils.guid()}`;
7703
+ attributes["aria-labelledby"] = this._label.id;
7704
+ this._label.addEventListener("click", this._handleLabelClick);
7705
+ }
7706
+ this.content = document.createElement("div");
7707
+ this.content.className = "rich-text-editor-content";
7708
+ this.textarea.after(this.content);
7709
+ this.textarea.hidden = true;
7710
+ this.editor = new this.options.Editor({
7711
+ element: this.content,
7712
+ extensions: this.options.extensions,
7713
+ content: this.textarea.value,
7714
+ editable: !(this.textarea.disabled || this.textarea.readOnly),
7715
+ editorProps: { attributes }
7716
+ });
7717
+ this.editor.on("update", this._handleUpdate);
7718
+ this.editor.on("transaction", this._handleTransaction);
7719
+ this._handleTransaction();
7720
+ el.addEventListener("click", this._handleClick);
7721
+ }
7722
+ static get defaults() {
7723
+ return _defaults19;
7724
+ }
7725
+ /**
7726
+ * Initializes instances of RichTextEditor.
7727
+ * @param els HTML elements.
7728
+ * @param options Component options.
7729
+ */
7730
+ static init(els, options = {}) {
7731
+ return super.init(els, options, _RichTextEditor);
7732
+ }
7733
+ static getInstance(el) {
7734
+ return el["Expressive_RichTextEditor"];
7735
+ }
7736
+ destroy() {
7737
+ this.el["Expressive_RichTextEditor"] = void 0;
7738
+ if (!this.editor) return;
7739
+ this.el.removeEventListener("click", this._handleClick);
7740
+ this._label?.removeEventListener("click", this._handleLabelClick);
7741
+ this.editor.off("update", this._handleUpdate);
7742
+ this.editor.off("transaction", this._handleTransaction);
7743
+ this.editor.destroy();
7744
+ this.editor = null;
7745
+ this.content.remove();
7746
+ this.textarea.hidden = false;
7747
+ for (const button of this._actions.keys()) {
7748
+ button.disabled = false;
7749
+ button.classList.remove("active");
7750
+ button.removeAttribute("aria-pressed");
7751
+ }
7752
+ }
7753
+ };
7754
+
5598
7755
  // src/ts/components/lightbox.ts
5599
- var _defaults15 = {
7756
+ var _defaults20 = {
5600
7757
  inDuration: 275,
5601
7758
  outDuration: 200,
5602
7759
  onOpenStart: null,
@@ -5745,7 +7902,7 @@ var Expressive = (() => {
5745
7902
  this._setupEventHandlers();
5746
7903
  }
5747
7904
  static get defaults() {
5748
- return _defaults15;
7905
+ return _defaults20;
5749
7906
  }
5750
7907
  /**
5751
7908
  * Initializes instances of Lightbox.
@@ -5951,7 +8108,7 @@ var Expressive = (() => {
5951
8108
 
5952
8109
  // src/ts/components/scrollspy.ts
5953
8110
  var _defaultGetActiveElement = (id) => `a[href="#${id}"]`;
5954
- var _defaults16 = {
8111
+ var _defaults21 = {
5955
8112
  scrollOffset: 200,
5956
8113
  activeClass: "active",
5957
8114
  getActiveElement: _defaultGetActiveElement,
@@ -5987,7 +8144,7 @@ var Expressive = (() => {
5987
8144
  }
5988
8145
  }
5989
8146
  static get defaults() {
5990
- return _defaults16;
8147
+ return _defaults21;
5991
8148
  }
5992
8149
  static init(els, options = {}) {
5993
8150
  return super.init(els, options, _ScrollSpy);
@@ -6077,7 +8234,7 @@ var Expressive = (() => {
6077
8234
  var ScrollSpy = _ScrollSpy;
6078
8235
 
6079
8236
  // src/ts/components/navigationRail.ts
6080
- var _defaults17 = {
8237
+ var _defaults22 = {
6081
8238
  onOpenStart: null,
6082
8239
  onOpenEnd: null,
6083
8240
  onCloseStart: null,
@@ -6164,7 +8321,7 @@ var Expressive = (() => {
6164
8321
  document.addEventListener("keydown", this._onKeyDown);
6165
8322
  }
6166
8323
  static get defaults() {
6167
- return _defaults17;
8324
+ return _defaults22;
6168
8325
  }
6169
8326
  static init(els, options = {}) {
6170
8327
  return super.init(els, options, _NavigationRail);
@@ -6194,7 +8351,7 @@ var Expressive = (() => {
6194
8351
  };
6195
8352
 
6196
8353
  // src/ts/components/tabs.ts
6197
- var _defaults18 = {
8354
+ var _defaults23 = {
6198
8355
  duration: 300,
6199
8356
  onShow: null,
6200
8357
  swipeable: false,
@@ -6281,7 +8438,7 @@ var Expressive = (() => {
6281
8438
  this._setupEventHandlers();
6282
8439
  }
6283
8440
  static get defaults() {
6284
- return _defaults18;
8441
+ return _defaults23;
6285
8442
  }
6286
8443
  /**
6287
8444
  * Initializes instances of Tabs.
@@ -6468,7 +8625,7 @@ var Expressive = (() => {
6468
8625
  };
6469
8626
 
6470
8627
  // src/ts/components/timepicker.ts
6471
- var _defaults19 = {
8628
+ var _defaults24 = {
6472
8629
  dialRadius: 135,
6473
8630
  outerRadius: 105,
6474
8631
  innerRadius: 70,
@@ -6664,7 +8821,7 @@ var Expressive = (() => {
6664
8821
  }
6665
8822
  }
6666
8823
  static get defaults() {
6667
- return _defaults19;
8824
+ return _defaults24;
6668
8825
  }
6669
8826
  /**
6670
8827
  * Initializes instances of Timepicker.
@@ -7026,7 +9183,7 @@ var Expressive = (() => {
7026
9183
  var Timepicker = _Timepicker;
7027
9184
 
7028
9185
  // src/ts/components/snackbar.ts
7029
- var _defaults20 = {
9186
+ var _defaults25 = {
7030
9187
  text: "",
7031
9188
  action: "",
7032
9189
  onAction: null,
@@ -7076,7 +9233,7 @@ var Expressive = (() => {
7076
9233
  this._setTimer();
7077
9234
  }
7078
9235
  static get defaults() {
7079
- return _defaults20;
9236
+ return _defaults25;
7080
9237
  }
7081
9238
  static getInstance(el) {
7082
9239
  return el["Expressive_Snackbar"];
@@ -7373,7 +9530,7 @@ var Expressive = (() => {
7373
9530
  var Snackbar = _Snackbar;
7374
9531
 
7375
9532
  // src/ts/components/tooltip.ts
7376
- var _defaults21 = {
9533
+ var _defaults26 = {
7377
9534
  exitDelay: 200,
7378
9535
  enterDelay: 0,
7379
9536
  text: "",
@@ -7455,7 +9612,7 @@ var Expressive = (() => {
7455
9612
  this._setupEventHandlers();
7456
9613
  }
7457
9614
  static get defaults() {
7458
- return _defaults21;
9615
+ return _defaults26;
7459
9616
  }
7460
9617
  /**
7461
9618
  * Initializes instances of Tooltip.
@@ -7674,11 +9831,31 @@ var Expressive = (() => {
7674
9831
  },
7675
9832
  Carousel: { component: Carousel, selector: ".carousel" },
7676
9833
  Chips: { component: Chips, selector: ".chips" },
9834
+ ColumnChart: { component: ColumnChart, selector: ".column-chart" },
9835
+ BarChart: { component: BarChart, selector: ".bar-chart" },
9836
+ PieChart: { component: PieChart, selector: ".pie-chart" },
9837
+ HeatmapChart: { component: HeatmapChart, selector: ".heatmap-chart" },
9838
+ RadarChart: { component: RadarChart, selector: ".radar-chart" },
9839
+ RadialChart: { component: RadialChart, selector: ".radial-chart" },
9840
+ MixedChart: { component: MixedChart, selector: ".mixed-chart" },
9841
+ CommandPalette: {
9842
+ component: CommandPalette,
9843
+ selector: "dialog.command-palette"
9844
+ },
9845
+ Questionnaire: {
9846
+ component: Questionnaire,
9847
+ selector: "form.questionnaire"
9848
+ },
7677
9849
  Datepicker: {
7678
9850
  component: Datepicker,
7679
9851
  selector: ".datepicker, .date-picker"
7680
9852
  },
9853
+ LineChart: { component: LineChart, selector: ".line-chart" },
7681
9854
  Menu: { component: Menu, selector: ".menu-trigger" },
9855
+ MessageScroller: {
9856
+ component: MessageScroller,
9857
+ selector: ".message-scroller"
9858
+ },
7682
9859
  Lightbox: { component: Lightbox, selector: ".lightboxed" },
7683
9860
  ScrollSpy: { component: ScrollSpy, selector: ".scrollspy" },
7684
9861
  FormSelect: { component: FormSelect, selector: "select" },
@@ -7712,7 +9889,7 @@ var Expressive = (() => {
7712
9889
  }
7713
9890
 
7714
9891
  // src/ts/modular.ts
7715
- var version = "0.12.0";
9892
+ var version = "0.13.1";
7716
9893
 
7717
9894
  // src/ts/index.ts
7718
9895
  Forms.Init();