@ethlete/components 1.0.0-next.78 → 1.0.0-next.79

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 (252) hide show
  1. package/CHANGELOG.md +113 -0
  2. package/design/accordion.ts +73 -0
  3. package/design/avatar.ts +90 -0
  4. package/design/badge.ts +48 -0
  5. package/design/banner.ts +77 -0
  6. package/design/bracket.ts +457 -0
  7. package/design/breadcrumb.ts +145 -0
  8. package/design/button.ts +49 -0
  9. package/design/calendar.ts +306 -0
  10. package/design/card.ts +92 -0
  11. package/design/carousel.ts +240 -0
  12. package/design/chart.ts +512 -0
  13. package/design/checkbox.ts +275 -0
  14. package/design/chip.ts +64 -0
  15. package/design/config.json +4 -1
  16. package/design/control.ts +19 -0
  17. package/design/copy-button.ts +59 -0
  18. package/design/description-list.ts +41 -0
  19. package/design/dialog.ts +214 -0
  20. package/design/divider.ts +27 -0
  21. package/design/empty-state.ts +45 -0
  22. package/design/floating-action.ts +55 -0
  23. package/design/grid.ts +148 -0
  24. package/design/input.ts +86 -0
  25. package/design/kbd.ts +19 -0
  26. package/design/loader.ts +152 -0
  27. package/design/masonry.ts +115 -0
  28. package/design/match.ts +131 -0
  29. package/design/menu.ts +143 -0
  30. package/design/notification.ts +129 -0
  31. package/design/otp-input.ts +107 -0
  32. package/design/pagination.ts +120 -0
  33. package/design/picture.ts +80 -0
  34. package/design/progress-steps.ts +79 -0
  35. package/design/query-error.ts +60 -0
  36. package/design/radio-group.ts +132 -0
  37. package/design/scheduler.ts +369 -0
  38. package/design/scrollable.ts +218 -0
  39. package/design/scrollbar.ts +47 -0
  40. package/design/sdk/assets/fallback.svg +1 -0
  41. package/design/sdk/assets/tall.svg +1 -0
  42. package/design/sdk/assets/wide.svg +1 -0
  43. package/design/sdk/components/accent-on-light/accent-on-light.css +35 -0
  44. package/design/sdk/components/accent-on-light/accent-on-light.ts +10 -0
  45. package/design/sdk/components/accent-on-light/shared/controls.ts +46 -0
  46. package/design/sdk/components/accordion/accordion.css +17 -0
  47. package/design/sdk/components/accordion/accordion.ts +64 -0
  48. package/design/sdk/components/avatar/avatar.css +14 -0
  49. package/design/sdk/components/avatar/avatar.ts +78 -0
  50. package/design/sdk/components/badge/badge.css +13 -0
  51. package/design/sdk/components/badge/badge.ts +52 -0
  52. package/design/sdk/components/banner/banner.css +18 -0
  53. package/design/sdk/components/banner/banner.ts +87 -0
  54. package/design/sdk/components/bracket/bracket.css +59 -0
  55. package/design/sdk/components/bracket/bracket.ts +143 -0
  56. package/design/sdk/components/breadcrumb/breadcrumb.css +25 -0
  57. package/design/sdk/components/breadcrumb/breadcrumb.ts +81 -0
  58. package/design/sdk/components/button/button.css +33 -0
  59. package/design/sdk/components/button/button.ts +50 -7
  60. package/design/sdk/components/calendar/calendar.css +33 -0
  61. package/design/sdk/components/calendar/calendar.ts +96 -0
  62. package/design/sdk/components/card/card.css +19 -0
  63. package/design/sdk/components/card/card.ts +61 -0
  64. package/design/sdk/components/carousel/carousel.css +44 -0
  65. package/design/sdk/components/carousel/carousel.ts +74 -0
  66. package/design/sdk/components/chart/chart.css +56 -0
  67. package/design/sdk/components/chart/chart.ts +98 -0
  68. package/design/sdk/components/chip/chip.css +18 -0
  69. package/design/sdk/components/chip/chip.ts +76 -0
  70. package/design/sdk/components/copy-button/copy-button.css +14 -0
  71. package/design/sdk/components/copy-button/copy-button.ts +59 -0
  72. package/design/sdk/components/description-list/description-list.css +7 -0
  73. package/design/sdk/components/description-list/description-list.ts +53 -0
  74. package/design/sdk/components/dialog/dialog.css +29 -0
  75. package/design/sdk/components/dialog/dialog.ts +78 -0
  76. package/design/sdk/components/divider/divider.css +26 -0
  77. package/design/sdk/components/divider/divider.ts +43 -0
  78. package/design/sdk/components/empty-state/empty-state.css +15 -0
  79. package/design/sdk/components/empty-state/empty-state.ts +56 -0
  80. package/design/sdk/components/floating-action/floating-action.css +51 -0
  81. package/design/sdk/components/floating-action/floating-action.ts +63 -0
  82. package/design/sdk/components/form-field/form-field.ts +23 -5
  83. package/design/sdk/components/grid/grid.css +17 -0
  84. package/design/sdk/components/grid/grid.ts +80 -0
  85. package/design/sdk/components/input/input.css +75 -0
  86. package/design/sdk/components/input/input.ts +79 -0
  87. package/design/sdk/components/kbd/kbd.css +25 -0
  88. package/design/sdk/components/kbd/kbd.ts +57 -0
  89. package/design/sdk/components/loader/loader.css +37 -0
  90. package/design/sdk/components/loader/loader.ts +68 -0
  91. package/design/sdk/components/masonry/masonry.css +7 -0
  92. package/design/sdk/components/masonry/masonry.ts +42 -0
  93. package/design/sdk/components/match/match.css +25 -0
  94. package/design/sdk/components/match/match.ts +76 -0
  95. package/design/sdk/components/menu/menu.css +22 -0
  96. package/design/sdk/components/menu/menu.ts +91 -0
  97. package/design/sdk/components/notification/notification.css +32 -0
  98. package/design/sdk/components/notification/notification.ts +106 -0
  99. package/design/sdk/components/otp-input/otp-input.css +35 -0
  100. package/design/sdk/components/otp-input/otp-input.ts +64 -0
  101. package/design/sdk/components/pagination/pagination.css +14 -0
  102. package/design/sdk/components/pagination/pagination.ts +75 -0
  103. package/design/sdk/components/picture/picture.css +37 -0
  104. package/design/sdk/components/picture/picture.ts +96 -0
  105. package/design/sdk/components/progress-steps/progress-steps.css +37 -0
  106. package/design/sdk/components/progress-steps/progress-steps.ts +91 -0
  107. package/design/sdk/components/query-error/query-error.css +33 -0
  108. package/design/sdk/components/query-error/query-error.ts +120 -0
  109. package/design/sdk/components/scheduler/scheduler.css +55 -0
  110. package/design/sdk/components/scheduler/scheduler.ts +239 -0
  111. package/design/sdk/components/scrollable/scrollable.css +58 -0
  112. package/design/sdk/components/scrollable/scrollable.ts +133 -0
  113. package/design/sdk/components/scrollbar/scrollbar.css +78 -0
  114. package/design/sdk/components/scrollbar/scrollbar.ts +123 -0
  115. package/design/sdk/components/select/select.ts +1 -1
  116. package/design/sdk/components/select/shared/select-part.ts +16 -1
  117. package/design/sdk/components/select-panel/select-panel.css +3 -0
  118. package/design/sdk/components/select-panel/select-panel.ts +124 -0
  119. package/design/sdk/components/skeleton/skeleton.css +22 -0
  120. package/design/sdk/components/skeleton/skeleton.ts +50 -0
  121. package/design/sdk/components/slider/slider.css +52 -0
  122. package/design/sdk/components/slider/slider.ts +86 -0
  123. package/design/sdk/components/standings/standings.css +33 -0
  124. package/design/sdk/components/standings/standings.ts +130 -0
  125. package/design/sdk/components/stat-tile/stat-tile.css +15 -0
  126. package/design/sdk/components/stat-tile/stat-tile.ts +84 -0
  127. package/design/sdk/components/stream/stream.css +48 -0
  128. package/design/sdk/components/stream/stream.ts +38 -0
  129. package/design/sdk/components/switch/switch.css +35 -0
  130. package/design/sdk/components/switch/switch.ts +63 -0
  131. package/design/sdk/components/table/table.css +67 -0
  132. package/design/sdk/components/table/table.ts +160 -0
  133. package/design/sdk/components/tabs/tabs.css +13 -0
  134. package/design/sdk/components/tabs/tabs.ts +74 -0
  135. package/design/sdk/components/textarea/textarea.css +116 -0
  136. package/design/sdk/components/textarea/textarea.ts +64 -0
  137. package/design/sdk/components/timeline/timeline.css +30 -0
  138. package/design/sdk/components/timeline/timeline.ts +87 -0
  139. package/design/sdk/components/toolbar/toolbar.css +24 -0
  140. package/design/sdk/components/toolbar/toolbar.ts +87 -0
  141. package/design/sdk/components/tooltip/tooltip.css +15 -0
  142. package/design/sdk/components/tooltip/tooltip.ts +28 -0
  143. package/design/sdk/components/tree/selected-tint.css +15 -0
  144. package/design/sdk/components/tree/tree.ts +70 -0
  145. package/design/sdk/foundations/colors/colors.css +108 -4
  146. package/design/sdk/foundations/elevation/elevation.css +6 -0
  147. package/design/sdk/foundations/elevation/elevation.ts +11 -0
  148. package/design/sdk/foundations/elevation/shared/elevation/scene.css +101 -0
  149. package/design/sdk/foundations/elevation/shared/elevation/scene.ts +74 -0
  150. package/design/sdk/foundations/icons/icons.ts +119 -0
  151. package/design/skeleton.ts +67 -0
  152. package/design/slider.ts +155 -0
  153. package/design/standings.ts +161 -0
  154. package/design/stat-tile.ts +109 -0
  155. package/design/stream.ts +92 -0
  156. package/design/switch.ts +83 -0
  157. package/design/table.ts +302 -0
  158. package/design/tabs.ts +75 -0
  159. package/design/textarea.ts +92 -0
  160. package/design/timeline.ts +52 -0
  161. package/design/toolbar.ts +95 -0
  162. package/design/tooltip.ts +87 -0
  163. package/fesm2022/ethlete-components.mjs +228 -187
  164. package/fesm2022/ethlete-components.mjs.map +1 -1
  165. package/package.json +1 -1
  166. package/src/lib/accordion/accordion.component.css +2 -1
  167. package/src/lib/avatar/avatar.component.css +10 -0
  168. package/src/lib/badge/badge.component.css +2 -2
  169. package/src/lib/banner/banner.component.css +3 -1
  170. package/src/lib/breadcrumb/breadcrumb-outlet.component.css +5 -0
  171. package/src/lib/breadcrumb/breadcrumb.component.css +5 -1
  172. package/src/lib/button/button.component.css +19 -0
  173. package/src/lib/button/icon-button.component.css +19 -20
  174. package/src/lib/button/split-button.component.css +5 -1
  175. package/src/lib/calendar/calendar.component.css +13 -3
  176. package/src/lib/card/card.component.css +4 -0
  177. package/src/lib/carousel/carousel-autoplay-styles.component.css +1 -1
  178. package/src/lib/carousel/carousel.component.css +2 -2
  179. package/src/lib/chart/chart-axis.component.css +3 -3
  180. package/src/lib/chip/chip.component.css +8 -2
  181. package/src/lib/description-list/description-list.component.css +2 -1
  182. package/src/lib/empty-state/empty-state.component.css +4 -0
  183. package/src/lib/floating-action/floating-action-styles.component.css +27 -0
  184. package/src/lib/focus-ring/focus-ring-styles.component.css +4 -1
  185. package/src/lib/forms/checkbox/checkbox.component.css +31 -5
  186. package/src/lib/forms/color-input/color-input.component.css +3 -1
  187. package/src/lib/forms/color-input/color-picker-panel.component.css +6 -2
  188. package/src/lib/forms/date-time/date-input/date-input.component.css +1 -1
  189. package/src/lib/forms/date-time/date-time-input/date-time-input.component.css +1 -1
  190. package/src/lib/forms/date-time/duration-input/duration-input.component.css +1 -1
  191. package/src/lib/forms/date-time/range-input-shell-styles.component.css +1 -1
  192. package/src/lib/forms/date-time/time-input/time-input.component.css +1 -1
  193. package/src/lib/forms/description/description.component.css +7 -0
  194. package/src/lib/forms/dropzone/dropzone.component.css +26 -0
  195. package/src/lib/forms/form-field/form-field-control-suffix-styles.component.css +4 -2
  196. package/src/lib/forms/form-field/form-field-text-shell-styles.component.css +54 -5
  197. package/src/lib/forms/form-field/form-field-textarea-styles.component.css +62 -0
  198. package/src/lib/forms/form-field/form-field.component.css +6 -0
  199. package/src/lib/forms/form-field/headless/label.directive.css +1 -1
  200. package/src/lib/forms/input/input.component.css +6 -2
  201. package/src/lib/forms/input/number-input.component.css +2 -2
  202. package/src/lib/forms/input/password-input.component.css +4 -3
  203. package/src/lib/forms/otp-input/otp-input.component.css +23 -4
  204. package/src/lib/forms/phone-input/phone-input.component.css +4 -3
  205. package/src/lib/forms/rating/rating.component.css +2 -1
  206. package/src/lib/forms/rich-text-editor/rich-text-editor.component.css +1 -1
  207. package/src/lib/forms/selection-card-styles.component.css +3 -2
  208. package/src/lib/forms/selection-list/checkbox-group/checkbox-group-select-all.component.css +4 -2
  209. package/src/lib/forms/selection-list/checkbox-group/checkbox-option.component.css +4 -2
  210. package/src/lib/forms/selection-list/radio-group/radio.component.css +5 -2
  211. package/src/lib/forms/selection-list/segmented-button-group/segmented-button.component.css +2 -1
  212. package/src/lib/forms/slider/range-slider.component.css +98 -6
  213. package/src/lib/forms/slider/slider.component.css +96 -4
  214. package/src/lib/forms/switch/switch.component.css +47 -4
  215. package/src/lib/forms/tag-input/tag-input.component.css +2 -2
  216. package/src/lib/forms/textarea/textarea-autosize-styles.component.css +2 -3
  217. package/src/lib/forms/textarea/textarea.component.css +7 -2
  218. package/src/lib/grid/grid-item-toolbar.component.css +10 -0
  219. package/src/lib/grid/grid-item.component.css +6 -1
  220. package/src/lib/loader/brand-loader/brand-loader.component.css +4 -1
  221. package/src/lib/loader/progress-bar/progress-bar.component.css +1 -1
  222. package/src/lib/loader/spinner/spinner-styles.component.css +1 -1
  223. package/src/lib/match/match-card.component.css +9 -3
  224. package/src/lib/match/match-score.component.css +4 -1
  225. package/src/lib/menu/menu-selection-item.component.css +1 -1
  226. package/src/lib/menu/menu.component.css +5 -0
  227. package/src/lib/overlay/overlay-container.component.css +4 -0
  228. package/src/lib/pagination/pagination.component.css +4 -0
  229. package/src/lib/progress-steps/progress-step.component.css +4 -4
  230. package/src/lib/query-error/query-error.component.css +4 -0
  231. package/src/lib/scheduler/scheduler-appointment-styles.component.css +21 -5
  232. package/src/lib/scheduler/scheduler-month-view.component.css +4 -2
  233. package/src/lib/scheduler/scheduler-time-grid-view.component.css +8 -2
  234. package/src/lib/scrollable/headless/scrollable-navigation.component.css +1 -1
  235. package/src/lib/standings/standings-pick.component.css +1 -1
  236. package/src/lib/standings/standings.component.css +10 -10
  237. package/src/lib/stat-tile/stat-tile-sparkline.component.css +1 -1
  238. package/src/lib/stream/consent/stream-consent.component.css +99 -0
  239. package/src/lib/stream/error/stream-player-error.component.css +100 -0
  240. package/src/lib/stream/loading/stream-player-loading.component.css +16 -0
  241. package/src/lib/stream/pip/pip-slot-placeholder.component.css +68 -0
  242. package/src/lib/stream/stream-player-slot-styles.component.css +4 -0
  243. package/src/lib/table/table-resize-grip.component.css +2 -1
  244. package/src/lib/table/table-select-cell.component.css +45 -15
  245. package/src/lib/table/table.component.css +15 -5
  246. package/src/lib/tabs/tab-bar-styles.component.css +1 -1
  247. package/src/lib/tabs/tabs/tab-group.component.css +111 -0
  248. package/src/lib/time-picker/time-picker.component.css +2 -1
  249. package/src/lib/timeline/timeline-item.component.css +5 -1
  250. package/src/lib/toolbar/toolbar.component.css +4 -0
  251. package/src/lib/tooltip/tooltip.component.css +14 -0
  252. package/types/ethlete-components.d.ts +11 -2
@@ -0,0 +1,512 @@
1
+ import barChartBase from '../src/lib/chart/bar-chart.component.css?inline';
2
+ import axisBase from '../src/lib/chart/chart-axis.component.css?inline';
3
+ import gridBase from '../src/lib/chart/chart-grid.component.css?inline';
4
+ import legendBase from '../src/lib/chart/chart-legend.component.css?inline';
5
+ import chartTooltipBase from '../src/lib/chart/chart-tooltip.component.css?inline';
6
+ import lineChartTooltipBase from '../src/lib/chart/line-chart-tooltip.component.css?inline';
7
+ import lineChartBase from '../src/lib/chart/line-chart.component.css?inline';
8
+ import pieChartBase from '../src/lib/chart/pie-chart.component.css?inline';
9
+ import { tooltip, tooltipStyles } from './tooltip';
10
+
11
+ export type ChartMockSeries = {
12
+ label: string;
13
+ values: readonly number[];
14
+ };
15
+
16
+ export type ChartMockMark = {
17
+ /** The bar (in category, then series order), x or slice index. */
18
+ index: number;
19
+ /** A design marker for the mark, e.g. `hover` or `focus-visible`. */
20
+ force?: 'hover' | 'focus-visible';
21
+ /** Draws the mark's tooltip open, as on hover or tap. */
22
+ tooltip?: boolean;
23
+ };
24
+
25
+ type ChartMockBase = {
26
+ /** Left out, 400: the plot width the component would measure, in px. */
27
+ plotWidth?: number;
28
+ /** Left out, 240, the component default. */
29
+ height?: number;
30
+ mark?: ChartMockMark;
31
+ /** Extra attributes on the host, e.g. a style or design markers. */
32
+ attributes?: string;
33
+ };
34
+
35
+ export type BarChartMockOptions = ChartMockBase & {
36
+ categories: readonly string[];
37
+ /** One series draws no legend; two or more draw it, in accent mix steps. */
38
+ series: readonly ChartMockSeries[];
39
+ /** Projected `etBarChartEmpty` content; drawn when every value is missing. */
40
+ empty?: string;
41
+ };
42
+
43
+ export type LineChartMockOptions = ChartMockBase & {
44
+ categories: readonly string[];
45
+ series: readonly ChartMockSeries[];
46
+ points?: boolean;
47
+ };
48
+
49
+ export type PieChartMockOptions = {
50
+ data: readonly { label: string; value: number }[];
51
+ /** Left out, 200, the component default. */
52
+ size?: number;
53
+ /** The donut hole as a share of the radius; 0 draws a pie. */
54
+ innerRadius?: number;
55
+ totalLabel?: string;
56
+ mark?: ChartMockMark;
57
+ attributes?: string;
58
+ };
59
+
60
+ /** The stylesheets the chart mocks draw with: the real bar, line and pie chart, axis, grid, legend and tooltip CSS. */
61
+ export const chartStyles = /* @__PURE__ */ [
62
+ tooltipStyles,
63
+ axisBase,
64
+ gridBase,
65
+ legendBase,
66
+ chartTooltipBase,
67
+ lineChartTooltipBase,
68
+ barChartBase,
69
+ lineChartBase,
70
+ pieChartBase,
71
+ `@layer components {
72
+ .et-chart-mock-plot {
73
+ position: relative;
74
+ }
75
+ .et-chart-mock-tip {
76
+ position: absolute;
77
+ z-index: 1;
78
+ inset-inline-start: var(--_et-chart-mock-tip-x);
79
+ inset-block-start: var(--_et-chart-mock-tip-y);
80
+ translate: -50% calc(-100% - 4px);
81
+ }
82
+ .et-chart-mock-tip[data-placement='right'] {
83
+ translate: 4px -50%;
84
+ }
85
+ .et-chart-mock-tip[data-placement='left'] {
86
+ translate: calc(-100% - 4px) -50%;
87
+ }
88
+ .et-chart-mock-tip[data-placement='bottom'] {
89
+ translate: -50% 4px;
90
+ }
91
+ }`,
92
+ ].join('\n');
93
+
94
+ const BAR_GAP = 2;
95
+ const BAR_RADIUS = 4;
96
+ const MAX_BAR_WIDTH = 24;
97
+ const GROUP_PADDING = 0.2;
98
+ const MIN_ACCENT_MIX = 40;
99
+ const SLICE_GAP = 2;
100
+ const TAU = 6.283185307179586;
101
+
102
+ const round = (value: number) => {
103
+ const rounded = Math.round(value * 1000) / 1000;
104
+
105
+ return Object.is(rounded, -0) ? 0 : rounded;
106
+ };
107
+
108
+ const labelStride = (step: number, spacing: number) => (step > 0 ? Math.max(1, Math.ceil(spacing / step - 1e-9)) : 1);
109
+
110
+ const thinnedLabels = (
111
+ labels: readonly { text: string; position: number }[],
112
+ { step, stride }: { step: number; stride: number },
113
+ ) => labels.filter((_, index) => !(index % stride)).map((label) => ({ ...label, extent: step * stride }));
114
+
115
+ const styled = (css: string) => ` style="${css}"`;
116
+
117
+ const accentMixes = (count: number) =>
118
+ Array.from({ length: count }, (_, step) =>
119
+ count < 2 ? null : Math.round(100 - (step * (100 - MIN_ACCENT_MIX)) / (count - 1)),
120
+ );
121
+
122
+ const mixStyle = (mix: number | null, name = '--_et-chart-accent-mix') =>
123
+ mix === null ? '' : styled(`${name}: ${mix}%`);
124
+
125
+ const niceTicks = (max: number, count = 5) => {
126
+ if (!(max > 0)) return [0, 1];
127
+
128
+ const raw = max / count;
129
+ const magnitude = 10 ** Math.floor(Math.log10(raw));
130
+ const step = ([1, 2, 5, 10].find((factor) => factor * magnitude >= raw) ?? 10) * magnitude;
131
+ const top = Math.ceil(max / step) * step;
132
+
133
+ return Array.from({ length: Math.round(top / step) + 1 }, (_, index) => index * step);
134
+ };
135
+
136
+ const axis = (
137
+ labels: readonly { text: string; position: number; extent?: number }[],
138
+ {
139
+ orientation,
140
+ className,
141
+ blockSize,
142
+ }: { orientation: 'horizontal' | 'vertical'; className: string; blockSize?: number },
143
+ ) => {
144
+ const spans = labels
145
+ .map((label) => {
146
+ const vars = [
147
+ `--_et-chart-axis-position: ${round(label.position)}px`,
148
+ label.extent !== undefined && `--_et-chart-axis-extent: ${round(label.extent)}px`,
149
+ ].filter(Boolean);
150
+
151
+ return `<span class="et-chart-axis-label"${label.extent !== undefined ? ' data-extent=""' : ''}${styled(`${vars.join('; ')}`)}>${label.text}</span>`;
152
+ })
153
+ .join('');
154
+ const size = blockSize === undefined ? '' : styled(`block-size: ${blockSize}px`);
155
+
156
+ return `<et-chart-axis class="${className} et-chart-axis" aria-hidden="true" data-orientation="${orientation}"${size}>${spans}</et-chart-axis>`;
157
+ };
158
+
159
+ const grid = (ticks: readonly { value: number; position: number }[], length: number) =>
160
+ `<g class="et-chart-grid" aria-hidden="true">${ticks
161
+ .map(
162
+ (tick) =>
163
+ `<line x1="0" y1="${round(tick.position)}" x2="${length}" y2="${round(tick.position)}" class="et-chart-grid-line${tick.value === 0 ? ' et-chart-baseline' : ''}" shape-rendering="crispEdges"></line>`,
164
+ )
165
+ .join('')}</g>`;
166
+
167
+ const legend = (
168
+ series: readonly ChartMockSeries[],
169
+ { className, mark }: { className: string; mark: 'rect' | 'line' },
170
+ ) => {
171
+ const mixes = accentMixes(series.length);
172
+ const items = series
173
+ .map(
174
+ (entry, index) =>
175
+ `<li class="et-chart-legend-item"><span class="et-chart-legend-swatch et-color--inherited" aria-hidden="true"${mixStyle(mixes[index] ?? null)}></span><span class="et-chart-legend-label">${entry.label}</span></li>`,
176
+ )
177
+ .join('');
178
+
179
+ return `<et-chart-legend class="${className} et-chart-legend et-color--inherited" data-mark="${mark}"><ul class="et-chart-legend-list">${items}</ul></et-chart-legend>`;
180
+ };
181
+
182
+ type TipAnchor = { x: number; y: number; placement: 'top' | 'right' | 'bottom' | 'left' };
183
+
184
+ const tip = (content: string, { x, y, placement }: TipAnchor) =>
185
+ `<div class="et-chart-mock-tip" data-placement="${placement}"${styled(`--_et-chart-mock-tip-x: ${round(x)}px; --_et-chart-mock-tip-y: ${round(y)}px`)}>${tooltip({ content, placement })}</div>`;
186
+
187
+ const chartTooltip = ({
188
+ value,
189
+ label,
190
+ series,
191
+ }: {
192
+ value: string;
193
+ label: string;
194
+ series?: { label: string; mix: number | null };
195
+ }) => {
196
+ const key = series
197
+ ? `<span class="et-chart-tooltip-key et-color--inherited" aria-hidden="true"${mixStyle(series.mix)}></span>`
198
+ : '';
199
+ const seriesLabel = series ? `<span class="et-chart-tooltip-series">${series.label}</span>` : '';
200
+
201
+ return `<et-chart-tooltip class="et-chart-tooltip">${key}<strong class="et-chart-tooltip-value">${value}</strong>${seriesLabel}<span class="et-chart-tooltip-label">${label}</span></et-chart-tooltip>`;
202
+ };
203
+
204
+ const markAttributes = (mark: ChartMockMark | undefined, index: number) =>
205
+ mark?.index === index && mark.force ? ` data-et-force="${mark.force}"` : '';
206
+
207
+ const barPath = ({ x, y, width, height }: { x: number; y: number; width: number; height: number }) => {
208
+ if (width <= 0 || height <= 0) return '';
209
+
210
+ const r = Math.min(BAR_RADIUS, width / 2, height);
211
+ const right = round(x + width);
212
+ const bottom = round(y + height);
213
+
214
+ return `M${round(x)},${bottom}V${round(y + r)}A${r},${r} 0 0 1 ${round(x + r)},${round(y)}H${round(right - r)}A${r},${r} 0 0 1 ${right},${round(y + r)}V${bottom}Z`;
215
+ };
216
+
217
+ /** Draws an `et-bar-chart` (vertical, grouped) with the geometry the component computes for the given plot width, as HTML, without its visually hidden data table. Enter animations are drawn at rest. */
218
+ export const barChart = (options: BarChartMockOptions) => {
219
+ const width = options.plotWidth ?? 400;
220
+ const height = options.height ?? 240;
221
+ const { categories, series, mark } = options;
222
+ const values = series.flatMap((entry) => entry.values.filter((value) => Number.isFinite(value)));
223
+ const isEmpty = !values.length;
224
+ const ticks = niceTicks(Math.max(0, ...values));
225
+ const top = ticks[ticks.length - 1] ?? 1;
226
+ const scale = (value: number) => height - (value / top) * height;
227
+ const tickModels = ticks.map((value) => ({ value, position: scale(value) }));
228
+
229
+ const groupSize = Math.max(1, series.length);
230
+ const step = categories.length ? width / categories.length : 0;
231
+ const groupGap = groupSize > 1 ? Math.max(BAR_GAP, step * GROUP_PADDING) : BAR_GAP;
232
+ const innerGaps = (groupSize - 1) * BAR_GAP;
233
+ const bandWidth = Math.max(0, Math.min(MAX_BAR_WIDTH, (step - groupGap - innerGaps) / groupSize));
234
+ const groupWidth = bandWidth * groupSize + innerGaps;
235
+ const bandStart = (index: number, member: number) =>
236
+ index * step + (step - groupWidth) / 2 + member * (bandWidth + BAR_GAP);
237
+ const mixes = accentMixes(series.length);
238
+
239
+ let tipMarkup = '';
240
+ let barIndex = 0;
241
+
242
+ const bars = categories
243
+ .flatMap((category, categoryIndex) =>
244
+ series.map((entry, member) => {
245
+ const value = entry.values[categoryIndex];
246
+
247
+ if (value === undefined || !Number.isFinite(value)) return '';
248
+
249
+ const index = barIndex++;
250
+ const x = bandStart(categoryIndex, member);
251
+ const y = scale(value);
252
+ const slotStart = member === 0 ? categoryIndex * step : x - BAR_GAP / 2;
253
+ const slotEnd = member === groupSize - 1 ? (categoryIndex + 1) * step : x + bandWidth + BAR_GAP / 2;
254
+ const mix = series.length > 1 ? (mixes[member] ?? null) : null;
255
+ const name = series.length > 1 ? `${category}, ${entry.label}` : category;
256
+
257
+ if (mark?.index === index && mark.tooltip) {
258
+ tipMarkup = tip(
259
+ chartTooltip({
260
+ value: `${value}`,
261
+ label: category,
262
+ series: series.length > 1 ? { label: entry.label, mix } : undefined,
263
+ }),
264
+ { x: x + bandWidth / 2, y, placement: 'top' },
265
+ );
266
+ }
267
+
268
+ return `<g class="et-bar-chart-bar et-color--inherited" tabindex="0" role="img" aria-label="${name}"${mixStyle(mix)}${markAttributes(mark, index)}><rect x="${round(slotStart)}" y="0" width="${round(slotEnd - slotStart)}" height="${height}" class="et-bar-chart-bar-target" rx="4"></rect><path d="${barPath({ x, y, width: bandWidth, height: height - y })}" class="et-bar-chart-bar-mark"></path><rect x="${round(x)}" y="${round(y)}" width="${round(bandWidth)}" height="0" class="et-bar-chart-bar-anchor"></rect></g>`;
269
+ }),
270
+ )
271
+ .join('');
272
+
273
+ const svg = `<svg width="${width}" height="${height}" aria-label="Bar chart" class="et-bar-chart-svg" role="group"${styled(`--_et-bar-chart-baseline: ${height}px`)}>${grid(tickModels, width)}<g class="et-bar-chart-bars et-color--inherited">${bars}</g></svg>`;
274
+ const valueAxis = axis(
275
+ isEmpty ? [] : tickModels.map((tick) => ({ text: `${tick.value}`, position: tick.position })),
276
+ { orientation: 'vertical', className: 'et-bar-chart-value-axis', blockSize: height },
277
+ );
278
+ const categoryAxis = axis(
279
+ thinnedLabels(
280
+ categories.map((text, index) => ({ text, position: index * step + step / 2 })),
281
+ {
282
+ step,
283
+ stride: labelStride(
284
+ step,
285
+ Math.min(96, Math.max(24, Math.max(0, ...categories.map((text) => [...text].length)) * 7 + 12)),
286
+ ),
287
+ },
288
+ ),
289
+ { orientation: 'horizontal', className: 'et-bar-chart-category-axis' },
290
+ );
291
+ const empty = isEmpty
292
+ ? `<div class="et-bar-chart-empty"${styled(`block-size: ${height}px`)}><span etBarChartEmpty>${options.empty ?? ''}</span></div>`
293
+ : '';
294
+ const legendMarkup = series.length > 1 ? legend(series, { className: 'et-bar-chart-legend', mark: 'rect' }) : '';
295
+ const host = [options.attributes, 'class="et-bar-chart"'].filter(Boolean);
296
+
297
+ return `<et-bar-chart ${host.join(' ')}>${legendMarkup}<div class="et-bar-chart-figure" data-orientation="vertical">${valueAxis}<div class="et-bar-chart-plot et-chart-mock-plot"${styled(`block-size: ${height}px`)}>${svg}${tipMarkup}</div>${empty}${categoryAxis}</div></et-bar-chart>`;
298
+ };
299
+
300
+ /** Draws an `et-line-chart` on a category scale with the geometry the component computes for the given plot width, as HTML, without its visually hidden data table. The reveal animation is drawn at rest. */
301
+ export const lineChart = (options: LineChartMockOptions) => {
302
+ const width = options.plotWidth ?? 400;
303
+ const height = options.height ?? 240;
304
+ const { categories, series, mark } = options;
305
+ const ticks = niceTicks(Math.max(0, ...series.flatMap((entry) => entry.values)));
306
+ const top = ticks[ticks.length - 1] ?? 1;
307
+ const scale = (value: number) => height - (value / top) * height;
308
+ const tickModels = ticks.map((value) => ({ value, position: scale(value) }));
309
+ const step = categories.length ? width / categories.length : 0;
310
+ const positions = categories.map((_, index) => index * step + step / 2);
311
+ const mixes = accentMixes(series.length);
312
+
313
+ const lines = series
314
+ .map((entry, seriesIndex) => {
315
+ const points = entry.values.map((value, index) => ({ x: positions[index] ?? 0, y: scale(value) }));
316
+ const path = points.map((point, index) => `${index ? 'L' : 'M'}${round(point.x)},${round(point.y)}`).join('');
317
+ const dots = options.points
318
+ ? points
319
+ .map(
320
+ (point) =>
321
+ `<circle cx="${round(point.x)}" cy="${round(point.y)}" class="et-line-chart-point" r="5"></circle>`,
322
+ )
323
+ .join('')
324
+ : '';
325
+
326
+ return `<g class="et-line-chart-series et-color--inherited"${mixStyle(mixes[seriesIndex] ?? null)}><path d="${path}" class="et-line-chart-line"></path>${dots}</g>`;
327
+ })
328
+ .join('');
329
+
330
+ let tipMarkup = '';
331
+
332
+ const slices = categories
333
+ .map((category, index) => {
334
+ const position = positions[index] ?? 0;
335
+ const start = index === 0 ? 0 : (position + (positions[index - 1] ?? 0)) / 2;
336
+ const end = index === categories.length - 1 ? width : (position + (positions[index + 1] ?? width)) / 2;
337
+ const entries = series.map((entry, seriesIndex) => ({
338
+ label: entry.label,
339
+ value: entry.values[index] ?? 0,
340
+ y: scale(entry.values[index] ?? 0),
341
+ mix: mixes[seriesIndex] ?? null,
342
+ }));
343
+ const anchorY = Math.min(...entries.map((entry) => entry.y));
344
+ const isActive = mark?.index === index && mark.tooltip;
345
+ const dots = entries
346
+ .map(
347
+ (entry) =>
348
+ `<circle cx="${round(position)}" cy="${round(entry.y)}" class="et-line-chart-slice-point et-color--inherited" r="5"${mixStyle(entry.mix)}></circle>`,
349
+ )
350
+ .join('');
351
+ const vars = `--_et-line-chart-slice-x: ${round(start)}px; --_et-line-chart-slice-width: ${round(end - start)}px; --_et-line-chart-slice-position: ${round(position)}px`;
352
+
353
+ if (isActive) {
354
+ const rows = entries
355
+ .map(
356
+ (entry) =>
357
+ `<li class="et-line-chart-tooltip-row et-color--inherited"${mixStyle(entry.mix)}><span class="et-line-chart-tooltip-key" aria-hidden="true"></span><span class="et-line-chart-tooltip-series">${entry.label}</span><strong class="et-line-chart-tooltip-value">${entry.value}</strong></li>`,
358
+ )
359
+ .join('');
360
+ const content =
361
+ series.length > 1
362
+ ? `<et-line-chart-tooltip class="et-line-chart-tooltip"><span class="et-line-chart-tooltip-label">${category}</span><ul class="et-line-chart-tooltip-list">${rows}</ul></et-line-chart-tooltip>`
363
+ : chartTooltip({ value: `${entries[0]?.value ?? ''}`, label: category });
364
+
365
+ tipMarkup = tip(content, { x: position, y: anchorY, placement: 'top' });
366
+ }
367
+
368
+ return `<g class="et-line-chart-slice" tabindex="0" role="img" aria-label="${category}"${isActive ? ' data-active=""' : ''}${markAttributes(mark, index)}><rect x="${round(start)}" y="0" width="${round(end - start)}" height="${height}" class="et-line-chart-slice-target" rx="4"></rect><rect x="${round(start)}" y="0" width="${round(end - start)}" height="${height}" class="et-line-chart-focus-ring" rx="4"${styled(`${vars}`)}></rect><line x1="${round(position)}" x2="${round(position)}" y1="0" y2="${height}" class="et-line-chart-crosshair" shape-rendering="crispEdges"></line>${dots}<rect x="${round(position)}" y="${round(anchorY)}" width="0" height="0" class="et-line-chart-slice-anchor"></rect></g>`;
369
+ })
370
+ .join('');
371
+
372
+ const svg = `<svg width="${width}" height="${height}" aria-label="Line chart" class="et-line-chart-svg" role="group"${options.points ? ' data-points=""' : ''}>${grid(tickModels, width)}<g class="et-line-chart-series-layer et-color--inherited" aria-hidden="true">${lines}</g><g class="et-line-chart-slices et-color--inherited">${slices}</g></svg>`;
373
+ const valueAxis = axis(
374
+ tickModels.map((tick) => ({ text: `${tick.value}`, position: tick.position })),
375
+ { orientation: 'vertical', className: 'et-line-chart-value-axis', blockSize: height },
376
+ );
377
+ const xAxis = axis(
378
+ thinnedLabels(
379
+ categories.map((text, index) => ({ text, position: positions[index] ?? 0 })),
380
+ { step, stride: labelStride(step, 48) },
381
+ ),
382
+ {
383
+ orientation: 'horizontal',
384
+ className: 'et-line-chart-x-axis',
385
+ },
386
+ );
387
+ const legendMarkup = series.length > 1 ? legend(series, { className: 'et-line-chart-legend', mark: 'line' }) : '';
388
+ const host = [options.attributes, 'class="et-line-chart"'].filter(Boolean);
389
+
390
+ return `<et-line-chart ${host.join(' ')}>${legendMarkup}<div class="et-line-chart-figure" data-scale="category">${valueAxis}<div class="et-line-chart-plot et-chart-mock-plot"${styled(`block-size: ${height}px`)}>${svg}${tipMarkup}</div>${xAxis}</div></et-line-chart>`;
391
+ };
392
+
393
+ const arcPoint = (center: number, { radius, angle }: { radius: number; angle: number }) =>
394
+ `${round(center + radius * Math.sin(angle))},${round(center - radius * Math.cos(angle))}`;
395
+
396
+ type ArcOptions = { radius: number; inner: number; startAngle: number; endAngle: number; gap: number };
397
+
398
+ const arcPath = ({ radius, inner, startAngle, endAngle, gap }: ArcOptions) => {
399
+ const sweep = endAngle - startAngle;
400
+ const c = radius;
401
+
402
+ if (sweep >= TAU - 1e-9) {
403
+ const circle = (r: number, dir: 0 | 1) =>
404
+ `M${c},${round(c - r)}A${round(r)},${round(r)} 0 1 ${dir} ${c},${round(c + r)}A${round(r)},${round(r)} 0 1 ${dir} ${c},${round(c - r)}Z`;
405
+
406
+ return inner > 0 ? `${circle(radius, 1)}${circle(inner, 0)}` : circle(radius, 1);
407
+ }
408
+
409
+ const minSweep = Math.min(sweep, 1 / radius);
410
+ const outerPad = Math.max(0, Math.min(Math.asin(Math.min(1, gap / 2 / radius)), (sweep - minSweep) / 2));
411
+ const halfGap = radius * Math.sin(outerPad);
412
+ const r = round(radius);
413
+ const outerArc = `M${arcPoint(c, { radius, angle: startAngle + outerPad })}A${r},${r} 0 ${sweep - 2 * outerPad > Math.PI ? 1 : 0} 1 ${arcPoint(c, { radius, angle: endAngle - outerPad })}`;
414
+ const apexDistance = halfGap > 0 ? halfGap / Math.sin(sweep / 2) : 0;
415
+
416
+ if (inner <= apexDistance) {
417
+ return `${outerArc}L${arcPoint(c, { radius: Math.min(apexDistance, radius), angle: (startAngle + endAngle) / 2 })}Z`;
418
+ }
419
+
420
+ const innerPad = Math.asin(Math.min(1, halfGap / inner));
421
+ const ri = round(inner);
422
+
423
+ return `${outerArc}L${arcPoint(c, { radius: inner, angle: endAngle - innerPad })}A${ri},${ri} 0 ${sweep - 2 * innerPad > Math.PI ? 1 : 0} 0 ${arcPoint(c, { radius: inner, angle: startAngle + innerPad })}Z`;
424
+ };
425
+
426
+ const placementForAngle = (angle: number) => {
427
+ const degrees = (((angle % TAU) + TAU) % TAU) * (180 / Math.PI);
428
+
429
+ if (degrees < 45 || degrees >= 315) return 'top';
430
+ if (degrees < 135) return 'right';
431
+ if (degrees < 225) return 'bottom';
432
+
433
+ return 'left';
434
+ };
435
+
436
+ const wholePercentages = (values: readonly number[]) => {
437
+ const total = values.reduce((sum, value) => sum + Math.max(0, value), 0);
438
+
439
+ if (!(total > 0)) return values.map(() => 0);
440
+
441
+ const exact = values.map((value) => (Math.max(0, value) / total) * 100);
442
+ const floors = exact.map((value) => Math.floor(value));
443
+ let remaining = 100 - floors.reduce((sum, value) => sum + value, 0);
444
+
445
+ exact
446
+ .map((value, index) => ({ index, remainder: value - Math.floor(value) }))
447
+ .sort((a, b) => b.remainder - a.remainder)
448
+ .forEach(({ index }) => {
449
+ if (remaining-- > 0) floors[index] = (floors[index] ?? 0) + 1;
450
+ });
451
+
452
+ return floors;
453
+ };
454
+
455
+ /** Draws an `et-pie-chart` (pie or donut) with its legend and the geometry the component computes, as HTML, without its visually hidden data table. Enter animations are drawn at rest. */
456
+ export const pieChart = (options: PieChartMockOptions) => {
457
+ const diameter = options.size ?? 200;
458
+ const radius = diameter / 2;
459
+ const hole = radius * Math.min(0.9, Math.max(0, options.innerRadius ?? 0));
460
+ const { data, mark } = options;
461
+ const total = data.reduce((sum, datum) => sum + Math.max(0, datum.value), 0);
462
+ const percents = wholePercentages(data.map((datum) => datum.value));
463
+ const mixes = accentMixes(data.length);
464
+ const drawn = data.map((datum, index) => ({ datum, index })).filter(({ datum }) => datum.value > 0);
465
+ const gap = drawn.length > 1 ? SLICE_GAP : 0;
466
+
467
+ let angle = 0;
468
+ let tipMarkup = '';
469
+
470
+ const slices =
471
+ total > 0
472
+ ? drawn
473
+ .map(({ datum, index }, order) => {
474
+ const start = angle;
475
+ const end = drawn.length === 1 ? TAU : angle + (datum.value / total) * TAU;
476
+ const mid = drawn.length === 1 ? 0 : (start + end) / 2;
477
+
478
+ angle = end;
479
+
480
+ const style = [
481
+ mixes[index] !== null && `--_et-pie-chart-accent-mix: ${mixes[index]}%`,
482
+ `--_et-pie-chart-slice-order: ${order}`,
483
+ ].filter(Boolean);
484
+ const anchorX = radius + radius * Math.sin(mid);
485
+ const anchorY = radius - radius * Math.cos(mid);
486
+
487
+ if (mark?.index === index && mark.tooltip) {
488
+ tipMarkup = tip(
489
+ chartTooltip({ value: `${datum.value}`, label: `${datum.label} · ${percents[index]}%` }),
490
+ { x: anchorX, y: anchorY, placement: placementForAngle(mid) },
491
+ );
492
+ }
493
+
494
+ return `<g class="et-pie-chart-slice et-color--inherited" tabindex="0" role="img" aria-label="${datum.label}"${styled(`${style.join('; ')}`)}${markAttributes(mark, index)}><path d="${arcPath({ radius, inner: hole, startAngle: start, endAngle: end, gap })}" class="et-pie-chart-slice-mark"></path><rect x="${round(anchorX)}" y="${round(anchorY)}" width="0" height="0" class="et-pie-chart-slice-anchor"></rect></g>`;
495
+ })
496
+ .join('')
497
+ : `<circle cx="${radius}" cy="${radius}" r="${radius - 0.5}" class="et-pie-chart-empty"></circle>`;
498
+
499
+ const center =
500
+ hole > 0
501
+ ? `<div class="et-pie-chart-center"${styled(`--_et-pie-chart-hole-size: ${round(hole * 2)}px`)}><strong class="et-pie-chart-total">${total}</strong><span class="et-pie-chart-total-label">${options.totalLabel ?? 'Total'}</span></div>`
502
+ : '';
503
+ const legendItems = data
504
+ .map(
505
+ (datum, index) =>
506
+ `<li class="et-pie-chart-legend-item et-color--inherited"${mixStyle(mixes[index] ?? null, '--_et-pie-chart-accent-mix')}><span class="et-pie-chart-legend-swatch" aria-hidden="true"></span><span class="et-pie-chart-legend-label">${datum.label}</span><span class="et-pie-chart-legend-value">${datum.value}</span><span class="et-pie-chart-legend-percent">${percents[index]}%</span></li>`,
507
+ )
508
+ .join('');
509
+ const host = [options.attributes, 'class="et-pie-chart"'].filter(Boolean);
510
+
511
+ return `<et-pie-chart ${host.join(' ')}><div class="et-pie-chart-figure et-color--inherited"><div class="et-pie-chart-plot et-chart-mock-plot"${styled(`inline-size: ${diameter}px`)}><svg width="${diameter}" height="${diameter}" aria-label="Pie chart" class="et-pie-chart-svg" role="group">${slices}</svg>${center}${tipMarkup}</div><ul class="et-pie-chart-legend">${legendItems}</ul></div></et-pie-chart>`;
512
+ };