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

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 (270) hide show
  1. package/CHANGELOG.md +139 -0
  2. package/design/accordion.ts +73 -0
  3. package/design/anchored-dialog.ts +92 -0
  4. package/design/avatar.ts +90 -0
  5. package/design/badge.ts +48 -0
  6. package/design/banner.ts +77 -0
  7. package/design/bracket.ts +457 -0
  8. package/design/breadcrumb.ts +145 -0
  9. package/design/button.ts +62 -1
  10. package/design/calendar.ts +306 -0
  11. package/design/card.ts +92 -0
  12. package/design/carousel.ts +240 -0
  13. package/design/chart.ts +512 -0
  14. package/design/checkbox.ts +275 -0
  15. package/design/chip.ts +64 -0
  16. package/design/config.json +8 -0
  17. package/design/control.ts +19 -0
  18. package/design/copy-button.ts +59 -0
  19. package/design/description-list.ts +41 -0
  20. package/design/dialog.ts +216 -0
  21. package/design/divider.ts +27 -0
  22. package/design/empty-state.ts +45 -0
  23. package/design/floating-action.ts +55 -0
  24. package/design/form-field.ts +3 -0
  25. package/design/grid.ts +148 -0
  26. package/design/input.ts +86 -0
  27. package/design/kbd.ts +19 -0
  28. package/design/loader.ts +152 -0
  29. package/design/masonry.ts +115 -0
  30. package/design/match.ts +131 -0
  31. package/design/menu.ts +147 -0
  32. package/design/notification.ts +133 -0
  33. package/design/otp-input.ts +107 -0
  34. package/design/pagination.ts +120 -0
  35. package/design/picture.ts +80 -0
  36. package/design/progress-steps.ts +79 -0
  37. package/design/query-error.ts +60 -0
  38. package/design/radio-group.ts +132 -0
  39. package/design/scheduler.ts +369 -0
  40. package/design/scrollable.ts +218 -0
  41. package/design/scrollbar.ts +47 -0
  42. package/design/sdk/assets/fallback.svg +1 -0
  43. package/design/sdk/assets/tall.svg +1 -0
  44. package/design/sdk/assets/wide.svg +1 -0
  45. package/design/sdk/components/accent-on-light/accent-on-light.css +35 -0
  46. package/design/sdk/components/accent-on-light/accent-on-light.ts +10 -0
  47. package/design/sdk/components/accent-on-light/shared/controls.ts +46 -0
  48. package/design/sdk/components/accordion/accordion.css +17 -0
  49. package/design/sdk/components/accordion/accordion.ts +68 -0
  50. package/design/sdk/components/anchored-dialog/anchored-dialog.css +63 -0
  51. package/design/sdk/components/anchored-dialog/anchored-dialog.ts +65 -0
  52. package/design/sdk/components/avatar/avatar.css +14 -0
  53. package/design/sdk/components/avatar/avatar.ts +79 -0
  54. package/design/sdk/components/badge/badge.css +13 -0
  55. package/design/sdk/components/badge/badge.ts +53 -0
  56. package/design/sdk/components/banner/banner.css +18 -0
  57. package/design/sdk/components/banner/banner.ts +88 -0
  58. package/design/sdk/components/bracket/bracket.css +59 -0
  59. package/design/sdk/components/bracket/bracket.ts +144 -0
  60. package/design/sdk/components/breadcrumb/breadcrumb.css +25 -0
  61. package/design/sdk/components/breadcrumb/breadcrumb.ts +82 -0
  62. package/design/sdk/components/button/button.css +41 -0
  63. package/design/sdk/components/button/button.ts +59 -7
  64. package/design/sdk/components/calendar/calendar.css +33 -0
  65. package/design/sdk/components/calendar/calendar.ts +97 -0
  66. package/design/sdk/components/card/card.css +19 -0
  67. package/design/sdk/components/card/card.ts +62 -0
  68. package/design/sdk/components/carousel/carousel.css +44 -0
  69. package/design/sdk/components/carousel/carousel.ts +75 -0
  70. package/design/sdk/components/chart/chart.css +56 -0
  71. package/design/sdk/components/chart/chart.ts +99 -0
  72. package/design/sdk/components/checkbox/checkbox.css +65 -0
  73. package/design/sdk/components/checkbox/checkbox.ts +180 -0
  74. package/design/sdk/components/chip/chip.css +18 -0
  75. package/design/sdk/components/chip/chip.ts +77 -0
  76. package/design/sdk/components/copy-button/copy-button.css +14 -0
  77. package/design/sdk/components/copy-button/copy-button.ts +60 -0
  78. package/design/sdk/components/description-list/description-list.css +7 -0
  79. package/design/sdk/components/description-list/description-list.ts +54 -0
  80. package/design/sdk/components/dialog/dialog.css +29 -0
  81. package/design/sdk/components/dialog/dialog.ts +79 -0
  82. package/design/sdk/components/divider/divider.css +26 -0
  83. package/design/sdk/components/divider/divider.ts +44 -0
  84. package/design/sdk/components/empty-state/empty-state.css +15 -0
  85. package/design/sdk/components/empty-state/empty-state.ts +57 -0
  86. package/design/sdk/components/floating-action/floating-action.css +51 -0
  87. package/design/sdk/components/floating-action/floating-action.ts +63 -0
  88. package/design/sdk/components/form-field/form-field.ts +23 -5
  89. package/design/sdk/components/grid/grid.css +17 -0
  90. package/design/sdk/components/grid/grid.ts +81 -0
  91. package/design/sdk/components/input/input.css +75 -0
  92. package/design/sdk/components/input/input.ts +80 -0
  93. package/design/sdk/components/kbd/kbd.css +25 -0
  94. package/design/sdk/components/kbd/kbd.ts +58 -0
  95. package/design/sdk/components/loader/loader.css +37 -0
  96. package/design/sdk/components/loader/loader.ts +69 -0
  97. package/design/sdk/components/masonry/masonry.css +7 -0
  98. package/design/sdk/components/masonry/masonry.ts +43 -0
  99. package/design/sdk/components/match/match.css +25 -0
  100. package/design/sdk/components/match/match.ts +77 -0
  101. package/design/sdk/components/menu/menu.css +22 -0
  102. package/design/sdk/components/menu/menu.ts +103 -0
  103. package/design/sdk/components/notification/notification.css +32 -0
  104. package/design/sdk/components/notification/notification.ts +107 -0
  105. package/design/sdk/components/otp-input/otp-input.css +35 -0
  106. package/design/sdk/components/otp-input/otp-input.ts +65 -0
  107. package/design/sdk/components/pagination/pagination.css +14 -0
  108. package/design/sdk/components/pagination/pagination.ts +76 -0
  109. package/design/sdk/components/picture/picture.css +37 -0
  110. package/design/sdk/components/picture/picture.ts +97 -0
  111. package/design/sdk/components/progress-steps/progress-steps.css +37 -0
  112. package/design/sdk/components/progress-steps/progress-steps.ts +92 -0
  113. package/design/sdk/components/query-error/query-error.css +33 -0
  114. package/design/sdk/components/query-error/query-error.ts +121 -0
  115. package/design/sdk/components/radio-group/radio-group.css +46 -0
  116. package/design/sdk/components/radio-group/radio-group.ts +167 -0
  117. package/design/sdk/components/scheduler/scheduler.css +55 -0
  118. package/design/sdk/components/scheduler/scheduler.ts +240 -0
  119. package/design/sdk/components/scrollable/scrollable.css +58 -0
  120. package/design/sdk/components/scrollable/scrollable.ts +133 -0
  121. package/design/sdk/components/scrollbar/scrollbar.css +78 -0
  122. package/design/sdk/components/scrollbar/scrollbar.ts +124 -0
  123. package/design/sdk/components/select/select.ts +1 -1
  124. package/design/sdk/components/select/shared/select-part.ts +25 -3
  125. package/design/sdk/components/select-panel/select-panel.css +3 -0
  126. package/design/sdk/components/select-panel/select-panel.ts +125 -0
  127. package/design/sdk/components/skeleton/skeleton.css +22 -0
  128. package/design/sdk/components/skeleton/skeleton.ts +51 -0
  129. package/design/sdk/components/slider/slider.css +52 -0
  130. package/design/sdk/components/slider/slider.ts +87 -0
  131. package/design/sdk/components/standings/standings.css +33 -0
  132. package/design/sdk/components/standings/standings.ts +131 -0
  133. package/design/sdk/components/stat-tile/stat-tile.css +15 -0
  134. package/design/sdk/components/stat-tile/stat-tile.ts +85 -0
  135. package/design/sdk/components/stream/stream.css +48 -0
  136. package/design/sdk/components/stream/stream.ts +43 -0
  137. package/design/sdk/components/switch/switch.css +37 -0
  138. package/design/sdk/components/switch/switch.ts +63 -0
  139. package/design/sdk/components/table/table.css +67 -0
  140. package/design/sdk/components/table/table.ts +161 -0
  141. package/design/sdk/components/tabs/tabs.css +13 -0
  142. package/design/sdk/components/tabs/tabs.ts +74 -0
  143. package/design/sdk/components/textarea/textarea.css +116 -0
  144. package/design/sdk/components/textarea/textarea.ts +65 -0
  145. package/design/sdk/components/timeline/timeline.css +30 -0
  146. package/design/sdk/components/timeline/timeline.ts +87 -0
  147. package/design/sdk/components/toolbar/toolbar.css +24 -0
  148. package/design/sdk/components/toolbar/toolbar.ts +88 -0
  149. package/design/sdk/components/tooltip/tooltip.css +15 -0
  150. package/design/sdk/components/tooltip/tooltip.ts +28 -0
  151. package/design/sdk/components/tree/selected-tint.css +15 -0
  152. package/design/sdk/components/tree/tree.ts +70 -0
  153. package/design/sdk/foundations/colors/colors.css +124 -4
  154. package/design/sdk/foundations/colors/colors.ts +1 -1
  155. package/design/sdk/foundations/colors/shared/contrast.ts +1 -1
  156. package/design/sdk/foundations/elevation/elevation.css +6 -0
  157. package/design/sdk/foundations/elevation/elevation.ts +11 -0
  158. package/design/sdk/foundations/elevation/shared/elevation/scene.css +101 -0
  159. package/design/sdk/foundations/elevation/shared/elevation/scene.ts +74 -0
  160. package/design/sdk/foundations/icons/icons.ts +119 -0
  161. package/design/sdk/pictures.json +1183 -0
  162. package/design/select.ts +5 -4
  163. package/design/skeleton.ts +67 -0
  164. package/design/slider.ts +155 -0
  165. package/design/standings.ts +161 -0
  166. package/design/stat-tile.ts +109 -0
  167. package/design/stream.ts +92 -0
  168. package/design/switch.ts +83 -0
  169. package/design/table.ts +302 -0
  170. package/design/tabs.ts +75 -0
  171. package/design/textarea.ts +94 -0
  172. package/design/timeline.ts +52 -0
  173. package/design/toolbar.ts +95 -0
  174. package/design/tooltip.ts +87 -0
  175. package/fesm2022/ethlete-components.mjs +1440 -1209
  176. package/fesm2022/ethlete-components.mjs.map +1 -1
  177. package/generators/generators.json +2 -0
  178. package/package.json +1 -1
  179. package/src/lib/accordion/accordion.component.css +2 -1
  180. package/src/lib/avatar/avatar.component.css +10 -0
  181. package/src/lib/badge/badge.component.css +2 -2
  182. package/src/lib/banner/banner.component.css +3 -1
  183. package/src/lib/breadcrumb/breadcrumb-outlet.component.css +5 -0
  184. package/src/lib/breadcrumb/breadcrumb.component.css +5 -1
  185. package/src/lib/button/button-properties-styles.component.css +6 -6
  186. package/src/lib/button/button.component.css +27 -4
  187. package/src/lib/button/button.types.ts +33 -0
  188. package/src/lib/button/icon-button.component.css +21 -22
  189. package/src/lib/button/split-button.component.css +5 -1
  190. package/src/lib/button/window-control-button.component.css +43 -3
  191. package/src/lib/calendar/calendar.component.css +15 -5
  192. package/src/lib/card/card.component.css +4 -0
  193. package/src/lib/carousel/carousel-autoplay-styles.component.css +1 -1
  194. package/src/lib/carousel/carousel.component.css +2 -2
  195. package/src/lib/chart/chart-axis.component.css +3 -3
  196. package/src/lib/chip/chip.component.css +12 -6
  197. package/src/lib/description-list/description-list.component.css +2 -1
  198. package/src/lib/empty-state/empty-state.component.css +4 -0
  199. package/src/lib/floating-action/floating-action-styles.component.css +27 -0
  200. package/src/lib/focus-ring/focus-ring-styles.component.css +4 -1
  201. package/src/lib/forms/checkbox/checkbox.component.css +31 -5
  202. package/src/lib/forms/color-input/color-input.component.css +3 -1
  203. package/src/lib/forms/color-input/color-picker-panel.component.css +6 -2
  204. package/src/lib/forms/date-time/date-input/date-input.component.css +1 -1
  205. package/src/lib/forms/date-time/date-time-input/date-time-input.component.css +1 -1
  206. package/src/lib/forms/date-time/duration-input/duration-input.component.css +1 -1
  207. package/src/lib/forms/date-time/range-input-shell-styles.component.css +1 -1
  208. package/src/lib/forms/date-time/time-input/time-input.component.css +1 -1
  209. package/src/lib/forms/description/description.component.css +7 -0
  210. package/src/lib/forms/dropzone/dropzone.component.css +26 -0
  211. package/src/lib/forms/form-field/form-field-control-suffix-styles.component.css +4 -2
  212. package/src/lib/forms/form-field/form-field-text-shell-styles.component.css +54 -5
  213. package/src/lib/forms/form-field/form-field-textarea-styles.component.css +62 -0
  214. package/src/lib/forms/form-field/form-field.component.css +6 -0
  215. package/src/lib/forms/form-field/headless/label.directive.css +1 -1
  216. package/src/lib/forms/input/input.component.css +6 -2
  217. package/src/lib/forms/input/number-input.component.css +2 -2
  218. package/src/lib/forms/input/password-input.component.css +4 -3
  219. package/src/lib/forms/otp-input/otp-input.component.css +23 -4
  220. package/src/lib/forms/phone-input/phone-input.component.css +4 -3
  221. package/src/lib/forms/rating/rating.component.css +2 -1
  222. package/src/lib/forms/rich-text-editor/rich-text-editor.component.css +1 -1
  223. package/src/lib/forms/selection-card-styles.component.css +3 -2
  224. package/src/lib/forms/selection-list/checkbox-group/checkbox-group-select-all.component.css +4 -2
  225. package/src/lib/forms/selection-list/checkbox-group/checkbox-option.component.css +4 -2
  226. package/src/lib/forms/selection-list/radio-group/radio.component.css +5 -2
  227. package/src/lib/forms/selection-list/segmented-button-group/segmented-button.component.css +2 -1
  228. package/src/lib/forms/slider/range-slider.component.css +98 -6
  229. package/src/lib/forms/slider/slider.component.css +96 -4
  230. package/src/lib/forms/switch/switch.component.css +45 -15
  231. package/src/lib/forms/tag-input/tag-input.component.css +2 -2
  232. package/src/lib/forms/textarea/textarea-autosize-styles.component.css +2 -3
  233. package/src/lib/forms/textarea/textarea.component.css +7 -2
  234. package/src/lib/grid/grid-item-toolbar.component.css +10 -0
  235. package/src/lib/grid/grid-item.component.css +6 -1
  236. package/src/lib/kbd/kbd.component.css +1 -1
  237. package/src/lib/loader/brand-loader/brand-loader.component.css +4 -1
  238. package/src/lib/loader/progress-bar/progress-bar.component.css +1 -1
  239. package/src/lib/loader/spinner/spinner-styles.component.css +1 -1
  240. package/src/lib/match/match-card.component.css +9 -3
  241. package/src/lib/match/match-score.component.css +4 -1
  242. package/src/lib/menu/menu-selection-item.component.css +1 -1
  243. package/src/lib/menu/menu.component.css +6 -1
  244. package/src/lib/notification/notification.component.css +3 -3
  245. package/src/lib/overlay/overlay-container.component.css +4 -0
  246. package/src/lib/pagination/pagination.component.css +4 -0
  247. package/src/lib/progress-steps/progress-step.component.css +4 -4
  248. package/src/lib/query-error/query-error.component.css +4 -0
  249. package/src/lib/scheduler/scheduler-appointment-styles.component.css +21 -5
  250. package/src/lib/scheduler/scheduler-month-view.component.css +4 -2
  251. package/src/lib/scheduler/scheduler-time-grid-view.component.css +8 -2
  252. package/src/lib/scrollable/headless/scrollable-navigation.component.css +1 -1
  253. package/src/lib/standings/standings-pick.component.css +1 -1
  254. package/src/lib/standings/standings.component.css +10 -10
  255. package/src/lib/stat-tile/stat-tile-sparkline.component.css +1 -1
  256. package/src/lib/stream/consent/stream-consent.component.css +99 -0
  257. package/src/lib/stream/error/stream-player-error.component.css +100 -0
  258. package/src/lib/stream/loading/stream-player-loading.component.css +16 -0
  259. package/src/lib/stream/pip/pip-slot-placeholder.component.css +68 -0
  260. package/src/lib/stream/stream-player-slot-styles.component.css +4 -0
  261. package/src/lib/table/table-resize-grip.component.css +2 -1
  262. package/src/lib/table/table-select-cell.component.css +45 -15
  263. package/src/lib/table/table.component.css +15 -5
  264. package/src/lib/tabs/tab-bar-styles.component.css +1 -1
  265. package/src/lib/tabs/tabs/tab-group.component.css +111 -0
  266. package/src/lib/time-picker/time-picker.component.css +2 -1
  267. package/src/lib/timeline/timeline-item.component.css +5 -1
  268. package/src/lib/toolbar/toolbar.component.css +4 -0
  269. package/src/lib/tooltip/tooltip.component.css +14 -0
  270. package/types/ethlete-components.d.ts +156 -128
@@ -0,0 +1,216 @@
1
+ import iconButtonBase from '../src/lib/button/icon-button.component.css?inline';
2
+ import { TIMES_ICON } from '../src/lib/icon/headless/times-icon';
3
+ import iconBase from '../src/lib/icon/headless/icon-styles.component.css?inline';
4
+ import alertDialogBase from '../src/lib/overlay/alert-dialog/alert-dialog.component.css?inline';
5
+ import overlayContainer from '../src/lib/overlay/overlay-container.component.css?inline';
6
+ import overlayMain from '../src/lib/overlay/overlay-main-styles.component.css?inline';
7
+ import dialogStrategy from '../src/lib/overlay/strategies/dialog-styles.component.css?inline';
8
+ import { button, buttonStyles } from './button';
9
+ import { iconMarkup } from './icon';
10
+
11
+ export type DialogMockButton = {
12
+ label: string;
13
+ variant?: 'filled' | 'outline' | 'tonal' | 'transparent';
14
+ /** A color theme name the app registers. Left out, the button takes the dialog's color theme. */
15
+ color?: string;
16
+ /** Extra attributes on the button, e.g. design markers. */
17
+ attributes?: string;
18
+ };
19
+
20
+ export type DialogMockOptions = {
21
+ /** The title, as HTML. */
22
+ title: string;
23
+ /** The body content, as HTML. */
24
+ body: string;
25
+ /** The footer buttons, in order. Leave it out for a dialog without a footer. */
26
+ actions?: DialogMockButton[];
27
+ /** Draws an `et-icon-button` with `etOverlayClose` in the header; the string adds attributes to it. */
28
+ close?: boolean | string;
29
+ /** As the body's `dividers` input. `dynamic` draws the body scrolled to its start. */
30
+ dividers?: false | 'static' | 'dynamic';
31
+ /** A surface theme name the app registers, for the pane. Left out, the pane takes the surface one elevation above its parent. */
32
+ surface?: string;
33
+ /** Extra attributes on the pane, e.g. design markers. */
34
+ attributes?: string;
35
+ /** Unique per frame: the title id derives from it. */
36
+ id?: string;
37
+ };
38
+
39
+ export type AlertDialogMockOptions = {
40
+ kind: 'alert' | 'confirm';
41
+ title: string;
42
+ message?: string;
43
+ /** The confirm button's color theme name, as `confirmColor` sets it. */
44
+ confirmColor?: string;
45
+ confirmLabel?: string;
46
+ cancelLabel?: string;
47
+ acknowledgeLabel?: string;
48
+ /** Extra attributes on the confirm (or acknowledge) button. */
49
+ confirmAttributes?: string;
50
+ /** Extra attributes on the cancel button. */
51
+ cancelAttributes?: string;
52
+ /** A surface theme name the app registers, for the pane. Left out, the pane takes the surface one elevation above its parent. */
53
+ surface?: string;
54
+ attributes?: string;
55
+ id?: string;
56
+ };
57
+
58
+ /** The stylesheets `dialog()` and `alertDialog()` draw with: the real overlay pane, dialog, button and icon button CSS. */
59
+ export const dialogStyles = /* @__PURE__ */ [
60
+ buttonStyles,
61
+ iconBase,
62
+ iconButtonBase,
63
+ overlayContainer,
64
+ overlayMain,
65
+ dialogStrategy,
66
+ alertDialogBase,
67
+ `@layer components {
68
+ .et-dialog-mock-header {
69
+ display: flex;
70
+ align-items: center;
71
+ justify-content: space-between;
72
+ gap: 1.2rem;
73
+ }
74
+ .et-dialog-mock-title {
75
+ margin: 0;
76
+ font-size: 1.8rem;
77
+ font-weight: 600;
78
+ }
79
+ .et-dialog-mock-actions {
80
+ display: flex;
81
+ flex-wrap: wrap;
82
+ justify-content: flex-end;
83
+ gap: 0.8rem;
84
+ }
85
+ .et-dialog-mock-scene {
86
+ position: relative;
87
+ display: grid;
88
+ place-items: center;
89
+ padding: 4rem 2.4rem;
90
+ }
91
+ .et-dialog-mock-scene > .et-overlay-runtime-backdrop {
92
+ position: absolute;
93
+ inset: 0;
94
+ }
95
+ }`,
96
+ ].join('\n');
97
+
98
+ const footerButton = (action: DialogMockButton) => {
99
+ const markup = button({ label: action.label, variant: action.variant ?? 'filled', color: action.color, size: 'md' });
100
+
101
+ return action.attributes ? markup.replace(/^<button /, `<button ${action.attributes} `) : markup;
102
+ };
103
+
104
+ const closeButton = (attributes?: string) => {
105
+ const classes = [
106
+ 'et-surface-interactive',
107
+ 'et-color-interactive',
108
+ 'et-icon-button',
109
+ 'et-focus-ring',
110
+ 'et-color--inherited',
111
+ ];
112
+
113
+ return `<button ${attributes ?? ''} etOverlayClose type="button" aria-label="Close" class="${classes.join(' ')}" data-variant="transparent" data-size="sm"><div class="et-icon-button-icon">${iconMarkup(TIMES_ICON)}</div></button>`;
114
+ };
115
+
116
+ const pane = (
117
+ options: { surface?: string; attributes?: string; role: string; labelledBy: string; describedBy?: string },
118
+ content: string,
119
+ ) => {
120
+ const classes = [
121
+ 'et-overlay',
122
+ 'et-overlay-runtime-pane',
123
+ 'et-overlay--dialog',
124
+ 'et-color--inherited',
125
+ options.surface && `et-surface--${options.surface}`,
126
+ 'et-with-default-animation',
127
+ 'et-animation-enter-done',
128
+ ].filter(Boolean);
129
+ const attributes = [
130
+ options.attributes,
131
+ !options.surface && 'data-et-auto-surface',
132
+ `role="${options.role}"`,
133
+ 'aria-modal="true"',
134
+ 'tabindex="-1"',
135
+ `aria-labelledby="${options.labelledBy}"`,
136
+ options.describedBy && `aria-describedby="${options.describedBy}"`,
137
+ `class="${classes.join(' ')}"`,
138
+ ].filter(Boolean);
139
+
140
+ return `<et-overlay-container ${attributes.join(' ')}>${content}</et-overlay-container>`;
141
+ };
142
+
143
+ const body = (content: string, dividers: DialogMockOptions['dividers'] = false) => {
144
+ const classes = [
145
+ 'et-overlay-body',
146
+ dividers && 'et-overlay-body--render-dividers',
147
+ dividers === 'dynamic' && 'et-overlay-body--dynamic-dividers',
148
+ dividers === 'dynamic' && 'et-scrollable-body--can-scroll',
149
+ dividers === 'dynamic' && 'et-scrollable-body--is-at-start',
150
+ ].filter(Boolean);
151
+
152
+ return `<div class="${classes.join(' ')}"><div class="et-overlay-body-container"><div class="et-overlay-body-start-element"></div>${content}<div class="et-overlay-body-end-element"></div></div></div>`;
153
+ };
154
+
155
+ /**
156
+ * Draws an open dialog pane (`dialogOverlayStrategy`) with an `et-overlay-main` of header, body and
157
+ * footer, as HTML. The pane sits in flow; wrap it in `overlayBackdrop()` to draw the scrim.
158
+ */
159
+ export const dialog = (options: DialogMockOptions) => {
160
+ const titleId = `et-overlay-title-${options.id ?? 'dialog'}`;
161
+ const close = options.close ? closeButton(typeof options.close === 'string' ? options.close : undefined) : '';
162
+ const header = `<div class="et-overlay-header et-dialog-mock-header"><h2 class="et-dialog-mock-title" id="${titleId}" et-overlay-title>${options.title}</h2>${close}</div>`;
163
+ const footer = options.actions
164
+ ? `<div class="et-overlay-footer et-dialog-mock-actions">${options.actions.map(footerButton).join('')}</div>`
165
+ : '';
166
+
167
+ return pane(
168
+ { surface: options.surface, attributes: options.attributes, role: 'dialog', labelledBy: titleId },
169
+ `<et-overlay-main class="et-overlay-main">${header}${body(options.body, options.dividers)}${footer}</et-overlay-main>`,
170
+ );
171
+ };
172
+
173
+ /** Draws the open `et-alert-dialog` from `injectAlertDialog()` in its dialog pane, as HTML. */
174
+ export const alertDialog = (options: AlertDialogMockOptions) => {
175
+ const id = options.id ?? 'alert';
176
+ const titleId = `et-overlay-title-${id}`;
177
+ const messageId = `et-alert-dialog-message-${id}`;
178
+ const actions =
179
+ options.kind === 'confirm'
180
+ ? [
181
+ footerButton({
182
+ label: options.cancelLabel ?? 'Cancel',
183
+ variant: 'outline',
184
+ attributes: ['data-et-alert-dialog-initial-focus', options.cancelAttributes].filter(Boolean).join(' '),
185
+ }),
186
+ footerButton({
187
+ label: options.confirmLabel ?? 'Confirm',
188
+ color: options.confirmColor,
189
+ attributes: options.confirmAttributes,
190
+ }),
191
+ ]
192
+ : [
193
+ footerButton({
194
+ label: options.acknowledgeLabel ?? 'OK',
195
+ attributes: ['data-et-alert-dialog-initial-focus', options.confirmAttributes].filter(Boolean).join(' '),
196
+ }),
197
+ ];
198
+ const message = options.message
199
+ ? body(`<p id="${messageId}" class="et-alert-dialog-message">${options.message}</p>`)
200
+ : '';
201
+
202
+ return pane(
203
+ {
204
+ surface: options.surface,
205
+ attributes: options.attributes,
206
+ role: 'alertdialog',
207
+ labelledBy: titleId,
208
+ describedBy: options.message ? messageId : undefined,
209
+ },
210
+ `<et-alert-dialog class="et-alert-dialog et-overlay-main" data-kind="${options.kind}"><div class="et-overlay-header"><h2 class="et-alert-dialog-title" id="${titleId}" et-overlay-title>${options.title}</h2></div>${message}<div class="et-alert-dialog-actions et-overlay-footer">${actions.join('')}</div></et-alert-dialog>`,
211
+ );
212
+ };
213
+
214
+ /** Draws the visible `et-overlay-runtime-backdrop` behind `content`, both in flow. */
215
+ export const overlayBackdrop = (content: string) =>
216
+ `<div class="et-dialog-mock-scene"><div class="et-overlay-runtime-backdrop et-overlay-backdrop--visible"></div>${content}</div>`;
@@ -0,0 +1,27 @@
1
+ import dividerBase from '../src/lib/divider/divider.component.css?inline';
2
+
3
+ export type DividerMockOptions = {
4
+ orientation?: 'horizontal' | 'vertical';
5
+ /** Draws the divider as `decorative`: no separator role, hidden from assistive tech. */
6
+ decorative?: boolean;
7
+ /** Extra attributes on the divider, e.g. design markers. */
8
+ attributes?: string;
9
+ };
10
+
11
+ /** The stylesheets `divider()` draws with: the real divider CSS. */
12
+ export const dividerStyles = /* @__PURE__ */ [dividerBase].join('\n');
13
+
14
+ /** Draws an `et-divider` as the component renders it, as HTML. */
15
+ export const divider = (options: DividerMockOptions = {}) => {
16
+ const orientation = options.orientation ?? 'horizontal';
17
+ const attributes = [
18
+ options.attributes,
19
+ 'class="et-divider"',
20
+ `data-orientation="${orientation}"`,
21
+ options.decorative ? 'role="presentation"' : 'role="separator"',
22
+ !options.decorative && `aria-orientation="${orientation}"`,
23
+ options.decorative && 'aria-hidden="true"',
24
+ ].filter(Boolean);
25
+
26
+ return `<et-divider ${attributes.join(' ')}></et-divider>`;
27
+ };
@@ -0,0 +1,45 @@
1
+ import emptyStateBase from '../src/lib/empty-state/empty-state.component.css?inline';
2
+ import { FILE_ICON } from '../src/lib/icon/headless/file-icon';
3
+ import iconBase from '../src/lib/icon/headless/icon-styles.component.css?inline';
4
+ import { TRIANGLE_EXCLAMATION_ICON } from '../src/lib/icon/headless/triangle-exclamation-icon';
5
+ import { button, ButtonMockOptions, buttonStyles } from './button';
6
+ import { iconMarkup } from './icon';
7
+
8
+ export type EmptyStateMockOptions = {
9
+ heading?: string;
10
+ description?: string;
11
+ /** The projected `[etIcon]`. Left out, the file icon; `false` draws none. */
12
+ icon?: 'file' | 'triangle' | false;
13
+ /** The label of a projected `et-button etEmptyStateAction`. Left out, no action. */
14
+ action?: string;
15
+ /** The action's button variant. Left out, `filled`, as `et-button` defaults to. */
16
+ actionVariant?: ButtonMockOptions['variant'];
17
+ /** Extra attributes on the action button, e.g. design markers. */
18
+ actionAttributes?: string;
19
+ /** Extra attributes on the empty state, e.g. design markers. */
20
+ attributes?: string;
21
+ };
22
+
23
+ const ICONS = {
24
+ file: FILE_ICON,
25
+ triangle: TRIANGLE_EXCLAMATION_ICON,
26
+ } as const;
27
+
28
+ /** The stylesheets `emptyState()` draws with: the real empty-state, button and icon CSS. */
29
+ export const emptyStateStyles = /* @__PURE__ */ [buttonStyles, iconBase, emptyStateBase].join('\n');
30
+
31
+ /** Draws an `et-empty-state` with its projected icon and action as the component renders it, as HTML. */
32
+ export const emptyState = (options: EmptyStateMockOptions) => {
33
+ const icon = options.icon === false ? '' : iconMarkup(ICONS[options.icon ?? 'file']);
34
+ const heading = options.heading ? `<p class="et-empty-state-title">${options.heading}</p>` : '';
35
+ const description = options.description ? `<p class="et-empty-state-description">${options.description}</p>` : '';
36
+ const action = options.action
37
+ ? button({ label: options.action, variant: options.actionVariant }).replace(
38
+ /^<button /,
39
+ `<button ${options.actionAttributes ?? ''} etEmptyStateAction `,
40
+ )
41
+ : '';
42
+ const attributes = [options.attributes, 'class="et-empty-state"'].filter(Boolean);
43
+
44
+ return `<et-empty-state ${attributes.join(' ')}>${icon}${heading}${description}${action}</et-empty-state>`;
45
+ };
@@ -0,0 +1,55 @@
1
+ import floatingActionBase from '../src/lib/floating-action/floating-action-styles.component.css?inline';
2
+ import { FILTER_ICON } from '../src/lib/icon/headless/filter-icon';
3
+ import iconBase from '../src/lib/icon/headless/icon-styles.component.css?inline';
4
+ import { button, buttonStyles, ButtonMockOptions } from './button';
5
+ import { iconMarkup } from './icon';
6
+
7
+ export type FloatingActionMockOptions = {
8
+ /** The trigger, drawn as an `et-button`. */
9
+ trigger: ButtonMockOptions;
10
+ /** Draws the filter icon in the trigger, as the story's Filter button could carry. */
11
+ filterIcon?: boolean;
12
+ /** Where the trigger is. Left out, `inline`. */
13
+ state?: 'inline' | 'floating' | 'hidden';
14
+ /** The in-flow size the anchor holds while the trigger is out of the flow, in px. */
15
+ heldSize?: { inlineSize: number; blockSize: number };
16
+ /** Extra attributes on the trigger, e.g. design markers. */
17
+ triggerAttributes?: string;
18
+ /** Content before the anchor, as HTML. */
19
+ before?: string;
20
+ /** The region the trigger acts on (`etFloatingActionScope`), as HTML. Left out, no scope. */
21
+ scope?: string;
22
+ /** Content after the scope, as HTML. */
23
+ after?: string;
24
+ /** Extra attributes on the host, e.g. a style or design markers. */
25
+ attributes?: string;
26
+ };
27
+
28
+ /** The stylesheets `floatingAction()` draws with: the real floating-action, button and icon CSS. */
29
+ export const floatingActionStyles = /* @__PURE__ */ [buttonStyles, iconBase, floatingActionBase].join('\n');
30
+
31
+ /** Draws `[etFloatingAction]` with its anchor, trigger and scope as a static snapshot of one state. */
32
+ export const floatingAction = (options: FloatingActionMockOptions) => {
33
+ const { state = 'inline', heldSize } = options;
34
+ const trigger = button({
35
+ ...options.trigger,
36
+ icon: options.filterIcon ? iconMarkup(FILTER_ICON) : options.trigger.icon,
37
+ class: ['et-floating-action-trigger', options.trigger.class].filter(Boolean).join(' '),
38
+ style: [state !== 'inline' && 'animation: none; transition: none', options.trigger.style]
39
+ .filter(Boolean)
40
+ .join('; '),
41
+ }).replace(
42
+ /^<(button|a) /,
43
+ `<$1 ${[options.triggerAttributes, `data-floating-action-state="${state}"`].filter(Boolean).join(' ')} `,
44
+ );
45
+
46
+ const held =
47
+ state !== 'inline' && heldSize
48
+ ? ` style="min-inline-size: ${heldSize.inlineSize}px; min-block-size: ${heldSize.blockSize}px"`
49
+ : '';
50
+ const anchor = `<div class="et-floating-action-anchor"${held}>${trigger}</div>`;
51
+ const scope = options.scope === undefined ? '' : `<div class="et-floating-action-scope">${options.scope}</div>`;
52
+ const host = [options.attributes, 'class="et-floating-action"', `data-state="${state}"`].filter(Boolean);
53
+
54
+ return `<div ${host.join(' ')}>${options.before ?? ''}${anchor}${scope}${options.after ?? ''}</div>`;
55
+ };
@@ -84,6 +84,8 @@ export type TextareaMockOptions = {
84
84
  /** Grows with its content, from `minRows` (default `rows`). On by default, like the real textarea. */
85
85
  autosize?: boolean;
86
86
  minRows?: number;
87
+ /** The native textarea's `aria-label`, for a field without a visible label. */
88
+ ariaLabel?: string;
87
89
  };
88
90
 
89
91
  /** The stylesheets `formField()`, `input()` and `textarea()` draw with: the real `et-form-field` CSS. */
@@ -144,6 +146,7 @@ export const textarea = (options: TextareaMockOptions = {}): FormFieldControlMoc
144
146
  state.invalid && 'aria-invalid="true"',
145
147
  state.describedBy && `aria-describedby="${state.describedBy}"`,
146
148
  state.labelId && `aria-labelledby="${state.labelId}"`,
149
+ options.ariaLabel && `aria-label="${options.ariaLabel}"`,
147
150
  `data-resize="${autosize ? 'none' : 'vertical'}"`,
148
151
  autosize && 'data-et-textarea-autosize=""',
149
152
  autosize && `style="--et-textarea-min-rows: ${options.minRows ?? rows}"`,
package/design/grid.ts ADDED
@@ -0,0 +1,148 @@
1
+ import { resizeHandlesStyles } from '@ethlete/core/design/resize-handles';
2
+ import iconButtonBase from '../src/lib/button/icon-button.component.css?inline';
3
+ import defaultActionsBase from '../src/lib/grid/grid-item-default-actions.component.css?inline';
4
+ import toolbarBase from '../src/lib/grid/grid-item-toolbar.component.css?inline';
5
+ import gridItemBase from '../src/lib/grid/grid-item.component.css?inline';
6
+ import gridBase from '../src/lib/grid/grid.component.css?inline';
7
+ import iconBase from '../src/lib/icon/headless/icon-styles.component.css?inline';
8
+ import { TIMES_ICON } from '../src/lib/icon/headless/times-icon';
9
+ import { buttonStyles } from './button';
10
+ import { iconMarkup } from './icon';
11
+
12
+ export type GridMockPosition = {
13
+ col: number;
14
+ row: number;
15
+ colSpan: number;
16
+ rowSpan: number;
17
+ };
18
+
19
+ export type GridMockItem = GridMockPosition & {
20
+ title: string;
21
+ /** A muted second line under the title. */
22
+ subtitle?: string;
23
+ /** A design marker for the item, e.g. `hover` or `focus-visible`. */
24
+ force?: string;
25
+ /** A design marker for the default remove action, e.g. `hover` or `focus-visible`. */
26
+ removeForce?: string;
27
+ /** `dragging` lifts the item off its slot by `offset`; `resizing` shows every handle, `activeEdge` held. */
28
+ state?: 'dragging' | 'resizing';
29
+ /** The pixel offset from the slot a dragged item follows the pointer by. */
30
+ offset?: { x: number; y: number };
31
+ activeEdge?: 'n' | 'e' | 's' | 'w' | 'ne' | 'nw' | 'se' | 'sw';
32
+ };
33
+
34
+ export type GridMockOptions = {
35
+ items: readonly GridMockItem[];
36
+ /** Left out, 448: the content width the grid would measure, in px. */
37
+ width?: number;
38
+ /** Left out, 4: the columns of the active breakpoint. */
39
+ columns?: number;
40
+ /** Left out, 100, the component default. */
41
+ rowHeight?: number;
42
+ /** Left out, 16, the component default. */
43
+ gap?: number;
44
+ /** Draws `et-grid-item-default-actions` in every item, as a registered `actionsComponent` does. */
45
+ actions?: boolean;
46
+ /** The drop placeholder (`.et-grid-ghost`) at the dragged item's target slot. */
47
+ ghost?: GridMockPosition;
48
+ readOnly?: boolean;
49
+ /** Extra attributes on the host, e.g. a style or design markers. */
50
+ attributes?: string;
51
+ };
52
+
53
+ const RESIZE_EDGES = ['n', 'e', 's', 'w', 'ne', 'nw', 'se', 'sw'] as const;
54
+
55
+ /** The stylesheets the grid mock draws with: the real grid, grid item, toolbar, default actions and resize handle CSS. */
56
+ export const gridStyles = /* @__PURE__ */ [
57
+ buttonStyles,
58
+ iconBase,
59
+ iconButtonBase,
60
+ resizeHandlesStyles,
61
+ gridBase,
62
+ gridItemBase,
63
+ toolbarBase,
64
+ defaultActionsBase,
65
+ `@layer components {
66
+ .et-grid-mock-content {
67
+ display: flex;
68
+ flex-direction: column;
69
+ gap: 2px;
70
+ box-sizing: border-box;
71
+ height: 100%;
72
+ padding: 12px;
73
+ }
74
+ .et-grid-mock-title {
75
+ overflow: hidden;
76
+ font-size: 1.4rem;
77
+ font-weight: 600;
78
+ white-space: nowrap;
79
+ text-overflow: ellipsis;
80
+ color: var(--et-surface-color-solid);
81
+ }
82
+ .et-grid-mock-subtitle {
83
+ font-size: 1.2rem;
84
+ color: var(--et-surface-color-muted-solid);
85
+ }
86
+ }`,
87
+ ].join('\n');
88
+
89
+ const geometryOf = (options: GridMockOptions) => {
90
+ const width = options.width ?? 448;
91
+ const columns = options.columns ?? 4;
92
+ const gap = options.gap ?? 16;
93
+ const rowHeight = options.rowHeight ?? 100;
94
+ const cellWidth = Math.max(0, (width - gap * (columns - 1)) / columns);
95
+
96
+ return { gap, rowHeight, cellWidth };
97
+ };
98
+
99
+ const rectOf = (position: GridMockPosition, geometry: ReturnType<typeof geometryOf>) => ({
100
+ x: position.col * (geometry.cellWidth + geometry.gap),
101
+ y: position.row * (geometry.rowHeight + geometry.gap),
102
+ width: position.colSpan * geometry.cellWidth + (position.colSpan - 1) * geometry.gap,
103
+ height: position.rowSpan * geometry.rowHeight + (position.rowSpan - 1) * geometry.gap,
104
+ });
105
+
106
+ const styled = (css: string) => ` style="${css}"`;
107
+
108
+ const sizeStyle = (rect: { x: number; y: number; width: number; height: number }) =>
109
+ styled(`translate: ${rect.x}px ${rect.y}px; width: ${rect.width}px; height: ${rect.height}px;`);
110
+
111
+ const removeAction = (item: GridMockItem) =>
112
+ `<div etGridItemAction><et-grid-item-default-actions><et-grid-item-toolbar class="et-grid-item-toolbar"><button aria-label="Remove ${item.title}" type="button" class="et-grid-item-default-actions__remove et-surface-interactive et-color-interactive et-icon-button et-focus-ring et-color--inherited" data-variant="transparent" data-size="xs"${item.removeForce ? ` data-et-force="${item.removeForce}"` : ''}><div class="et-icon-button-icon">${iconMarkup(TIMES_ICON)}</div></button></et-grid-item-toolbar></et-grid-item-default-actions></div>`;
113
+
114
+ const gridItem = (item: GridMockItem, options: GridMockOptions) => {
115
+ const geometry = geometryOf(options);
116
+ const slot = rectOf(item, geometry);
117
+ const rect =
118
+ item.state === 'dragging' && item.offset ? { ...slot, x: slot.x + item.offset.x, y: slot.y + item.offset.y } : slot;
119
+ const classes = [
120
+ 'et-grid-item',
121
+ 'et-surface--inherited',
122
+ item.state === 'dragging' && 'et-grid-item--dragging et-grid-item--direct',
123
+ item.state === 'resizing' && 'et-grid-item--resizing',
124
+ ]
125
+ .filter(Boolean)
126
+ .join(' ');
127
+ const handles = RESIZE_EDGES.map((edge) => `<div class="et-resize-handle et-resize-handle--${edge}"></div>`).join('');
128
+ const subtitle = item.subtitle ? `<div class="et-grid-mock-subtitle">${item.subtitle}</div>` : '';
129
+
130
+ return [
131
+ `<et-grid-item class="${classes}" data-et-surface-fill role="group" aria-label="${item.title}" tabindex="0" ${sizeStyle(rect)}${item.force ? ` data-et-force="${item.force}"` : ''}>`,
132
+ `<div class="et-grid-item__content"><div class="et-grid-mock-content"><div class="et-grid-mock-title">${item.title}</div>${subtitle}</div></div>`,
133
+ `<et-resize-handles class="et-resize-handles"${item.activeEdge ? ` data-active-edge="${item.activeEdge}"` : ''}${options.readOnly ? ' inert=""' : ''}>${handles}</et-resize-handles>`,
134
+ `<div class="et-grid-item__actions">${options.actions ? removeAction(item) : ''}</div>`,
135
+ `</et-grid-item>`,
136
+ ].join('');
137
+ };
138
+
139
+ /** Draws an `et-grid` with its items laid out by the grid maths, at rest or mid-drag or mid-resize. */
140
+ export const grid = (options: GridMockOptions) => {
141
+ const geometry = geometryOf(options);
142
+ const rows = Math.max(0, ...options.items.map((item) => item.row + item.rowSpan));
143
+ const height = rows <= 0 ? 0 : rows * geometry.rowHeight + (rows - 1) * geometry.gap;
144
+ const classes = ['et-grid', options.readOnly && 'et-grid--readonly'].filter(Boolean).join(' ');
145
+ const ghost = options.ghost ? `<div class="et-grid-ghost" ${sizeStyle(rectOf(options.ghost, geometry))}></div>` : '';
146
+
147
+ return `<et-grid class="${classes}" role="region" aria-label="Dashboard" ${styled(`height: ${height}px; --et-grid-gap: ${geometry.gap}px;`)} ${options.attributes ?? ''}>${options.items.map((item) => gridItem(item, options)).join('')}${ghost}</et-grid>`;
148
+ };
@@ -0,0 +1,86 @@
1
+ import description from '../src/lib/forms/description/description.component.css?inline';
2
+ import { CHECK_ICON } from '../src/lib/icon/headless/check-icon';
3
+ import { LOCK_ICON } from '../src/lib/icon/headless/lock-icon';
4
+ import { formField, formFieldStyles, FormFieldMockOptions, input, InputMockOptions } from './form-field';
5
+ import { iconMarkup } from './icon';
6
+
7
+ /** An interaction a frame forces with the `data-et-force` design marker. */
8
+ export type TextInputMockForce = 'hover' | 'focus-visible';
9
+
10
+ /** The icons the input stories put in the affixes. */
11
+ export const textInputAffixIcons = { lock: LOCK_ICON, check: CHECK_ICON };
12
+
13
+ export type TextInputMockOptions = Omit<FormFieldMockOptions, 'control'> &
14
+ InputMockOptions & {
15
+ /** An unresolved bulk-edit value: the raw value stays hidden and `mixedLabel` shows as the placeholder. */
16
+ mixed?: boolean;
17
+ mixedLabel?: string;
18
+ /** An `et-description` under the field, as HTML. */
19
+ description?: string;
20
+ prefixIcon?: keyof typeof textInputAffixIcons;
21
+ suffixIcon?: keyof typeof textInputAffixIcons;
22
+ /** Forced interactions: `hover` on the control frame, `focus-visible` on the native input. */
23
+ force?: TextInputMockForce | TextInputMockForce[];
24
+ };
25
+
26
+ /** The stylesheets `textInput()` draws with: the real `et-form-field`, `et-input` and `et-description` CSS. */
27
+ export const textInputStyles = /* @__PURE__ */ [formFieldStyles, description].join('\n');
28
+
29
+ const forceList = (force: TextInputMockForce | TextInputMockForce[] | undefined) =>
30
+ force === undefined ? [] : Array.isArray(force) ? force : [force];
31
+
32
+ /** Draws `<et-form-field>` around `<et-input>` as HTML, with the DOM the components render. */
33
+ export const textInput = (options: TextInputMockOptions = {}) => {
34
+ const { id = 'field', value, placeholder, type, textAlign, mixed, mixedLabel = 'Mixed', force, ...field } = options;
35
+ const forced = forceList(force);
36
+ const descriptionId = `et-description-${id}`;
37
+ const control = input({
38
+ value: mixed ? undefined : value,
39
+ placeholder: mixed ? mixedLabel : placeholder,
40
+ type,
41
+ textAlign,
42
+ });
43
+ const prefix = options.prefixIcon ? iconMarkup(textInputAffixIcons[options.prefixIcon]) : options.prefix;
44
+ const suffix = options.suffixIcon ? iconMarkup(textInputAffixIcons[options.suffixIcon]) : options.suffix;
45
+ const prefixOffset = options.prefixIcon
46
+ ? '--_et-form-field-prefix-offset: calc(16px + var(--et-form-field-control-affix-gap));'
47
+ : undefined;
48
+
49
+ let html = formField({
50
+ ...field,
51
+ id,
52
+ prefix,
53
+ suffix,
54
+ style: [prefixOffset, options.style].filter(Boolean).join(' ') || undefined,
55
+ control: {
56
+ ...control,
57
+ hasValue: !!mixed || control.hasValue,
58
+ html: (state) => {
59
+ const describedBy = [options.description !== undefined && descriptionId, state.describedBy]
60
+ .filter(Boolean)
61
+ .join(' ');
62
+ let markup = control.html({ ...state, describedBy: describedBy || null });
63
+
64
+ if (mixed) markup = markup.replace('<et-input class="et-color-interactive et-input"', '$& data-mixed="true"');
65
+ if (forced.includes('focus-visible'))
66
+ markup = markup.replace('<input ', '<input data-et-force="focus-visible" ');
67
+
68
+ return markup;
69
+ },
70
+ },
71
+ });
72
+
73
+ if (forced.includes('hover')) {
74
+ html = html.replace('<div class="et-form-field-control-frame"', '$& data-et-force="hover"');
75
+ }
76
+
77
+ if (options.description !== undefined) {
78
+ const descriptionHtml = `<et-description class="et-description" id="${descriptionId}">${options.description}</et-description>`;
79
+ const supportStart = html.indexOf('<div class="et-color-interactive-exclude et-form-field-support"');
80
+ const at = supportStart === -1 ? html.lastIndexOf('</et-form-field>') : supportStart;
81
+
82
+ html = `${html.slice(0, at)}${descriptionHtml}${html.slice(at)}`;
83
+ }
84
+
85
+ return html;
86
+ };
package/design/kbd.ts ADDED
@@ -0,0 +1,19 @@
1
+ import kbdBase from '../src/lib/kbd/kbd.component.css?inline';
2
+
3
+ export type KbdMockOptions = {
4
+ /** The glyph printed on each keycap, in order, e.g. `['⌘', '⇧', 'Z']` for `mod+shift+z` on Apple. */
5
+ keys: string[];
6
+ /** Extra attributes on the kbd, e.g. design markers. */
7
+ attributes?: string;
8
+ };
9
+
10
+ /** The stylesheet `kbd()` draws with: the real kbd CSS. */
11
+ export const kbdStyles = kbdBase;
12
+
13
+ /** Draws an `et-kbd` with one keycap per key as the component renders it, as HTML, without its visually hidden spoken label. */
14
+ export const kbd = (options: KbdMockOptions) => {
15
+ const keys = options.keys.map((key) => `<kbd class="et-kbd-key" aria-hidden="true">${key}</kbd>`).join('');
16
+ const attributes = [options.attributes, 'class="et-kbd"'].filter(Boolean);
17
+
18
+ return `<et-kbd ${attributes.join(' ')}>${keys}</et-kbd>`;
19
+ };