@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,167 @@
1
+ import {
2
+ radioGroup,
3
+ type RadioGroupMockOption,
4
+ type RadioGroupMockOptions,
5
+ type RadioMockForce,
6
+ radioGroupStyles,
7
+ } from '@ethlete/components/design/radio-group';
8
+ import { control } from '@ethlete/components/design/control';
9
+ import { definePart, drawing } from '@design-system';
10
+ import frame from './radio-group.css?inline';
11
+
12
+ type Props = { group: RadioGroupMockOptions } & Partial<RadioGroupMockOptions>;
13
+
14
+ const LABEL = 'Remind me one hour before';
15
+
16
+ const ROWS = {
17
+ unchecked: {},
18
+ checked: { checked: true },
19
+ } satisfies Record<string, Partial<RadioGroupMockOption>>;
20
+
21
+ const COLUMNS = ['rest', 'hover', 'focus-visible', 'active', 'disabled', 'readonly'] as const;
22
+
23
+ type Row = keyof typeof ROWS;
24
+ type Column = (typeof COLUMNS)[number];
25
+
26
+ const cellOf = (row: Row, column: Column): RadioGroupMockOptions => ({
27
+ id: `${row}-${column}`,
28
+ readonly: column === 'readonly',
29
+ options: [
30
+ {
31
+ ...ROWS[row],
32
+ label: LABEL,
33
+ disabled: column === 'disabled',
34
+ force:
35
+ column === 'rest' || column === 'disabled' || column === 'readonly'
36
+ ? undefined
37
+ : (column satisfies RadioMockForce),
38
+ },
39
+ ],
40
+ });
41
+
42
+ const stateOf = (row: Row, column: Column) => (column === 'rest' ? row : `${row}-${column}`);
43
+
44
+ const gridStates = Object.fromEntries(
45
+ (Object.keys(ROWS) as Row[]).flatMap((row) =>
46
+ COLUMNS.map((column) => [stateOf(row, column), { group: cellOf(row, column) }]),
47
+ ),
48
+ ) as Record<string, Props>;
49
+
50
+ const marked = (markup: string) =>
51
+ markup.replace(/<et-radio(?=[\s>])/gu, (tag) => control(`${tag}>`, 'radio').slice(0, -1));
52
+
53
+ const REMINDERS = ['One hour before', 'Fifteen minutes before', 'At kick-off'];
54
+
55
+ const reminderGroup = (
56
+ checked: number | null,
57
+ extra: Partial<RadioGroupMockOptions> & { force?: RadioMockForce[] } = {},
58
+ ): RadioGroupMockOptions => {
59
+ const { force, ...rest } = extra;
60
+
61
+ return {
62
+ id: 'reminder',
63
+ label: 'Kick-off reminder',
64
+ ...rest,
65
+ options: REMINDERS.map((label, index) => ({
66
+ label,
67
+ checked: index === checked,
68
+ force: index === 1 ? force : undefined,
69
+ })),
70
+ };
71
+ };
72
+
73
+ const formatGroup = (checked: number | null, force?: RadioMockForce[]): RadioGroupMockOptions => ({
74
+ id: 'format',
75
+ label: 'Match report format',
76
+ variant: 'card',
77
+ options: [
78
+ { label: 'Short', description: 'The score and the scorers.', checked: checked === 0, force },
79
+ { label: 'Full', description: 'Every event, with the line-ups.', checked: checked === 1 },
80
+ ],
81
+ });
82
+
83
+ const INVALID = {
84
+ required: true,
85
+ error: 'Pick a reminder to continue.',
86
+ errorColor: 'danger',
87
+ } satisfies Partial<RadioGroupMockOptions>;
88
+
89
+ export default definePart({
90
+ note: 'The SDK radio group (et-radio-group with et-radio) in flow, first drawing, drawn by the mock from @ethlete/components/design/radio-group with the real markup and CSS: et-label, et-radio in the plain and the card variant (et-selection-card), et-description, et-form-support with hint and error, the required marker, plus the color-interactive sheet; the brand theme, danger as the error theme. Fixtures are fictional: a match reminder form (Kick-off reminder: One hour before, Fifteen minutes before, At kick-off; hint We send it as a push notification.; required with error Pick a reminder to continue.; mixed hint The selected matches disagree.; card group Match report format: Short, The score and the scorers. / Full, Every event, with the line-ups.; horizontal group Show matches at: Home, Away, Neutral ground; grid label Remind me one hour before). Framed as a page with 16px padding at 360 and 640. States: a grid of unchecked and checked by rest, hover, focus-visible, active, disabled and readonly (one-option groups); group-none, group-checked, group-support (descriptions and hint), group-option-hover, group-option-pressed, group-option-focus-visible, group-mixed (bulk edit, dashed circles), group-disabled, group-readonly, invalid, invalid-focus-visible, invalid-checked, group-horizontal, card-rest, card-checked, card-hover, card-focus-visible and card-checked-focus-visible. Numbers (light / dark), from shot pixels at scale 2 at 360, brand mint, a: circle 20px with a 2px border and a 10px dot (border-box, fixed in 00072d935); unchecked circle border (surface border) vs page 1.26 / 1.73; checked ring and dot (primary) 1.33 / 13.50; checked hover dot and ring (-hover via color-interactive; the lib hover rule skips a checked radio) 1.37 / 13.06, 1.03 from rest; active 2.24 / 8.01; focus ring and focused border (primary -focus), checked or unchecked, 1.37 / 13.06; card checked edge and ring (primary) 1.33 / 13.50; mixed dashed border (primary) 1.33 / 13.50; invalid circle border (danger) 4.83 / 3.71; disabled checked ring 2.12 / 1.65, disabled label 1.41 / 1.65 (exempt under 1.4.11). Decided rules that arise: Q88 (every radio focus ring in ink, the unchecked one too, since unlike the checkbox it is primary, not the surface text colour; the invalid ring takes the danger ink; the card ring in ink), Q99a (the checked circle border, the focused circle border, the mixed dashed border and the card checked and focus edge take light-dark(ink, primary); the checked dot is a mint fill, so it takes a 1px inset ink edge, an outline at -1px) and Q101 (a checked radio hovers to -active). The ink edge on a mint fill arises, so Q102 arises: b draws the dot edge in both schemes, c keeps dark as today (light-dark(ink, transparent)). The error border is the danger theme, not mint, and passes 3:1, so it is kept. Findings for later: no new lib CSS bug; the radio circle box-sizing bug is fixed (00072d935, measured 40 sheet px = 20px). (1) WCAG 1.4.11: the unchecked circle border is 1.26 / 1.73 vs page, under 3:1, the only edge of an unchecked radio (the same open question as the checkbox box and the switch off track). Not drawn. (2) The required marker takes the error theme primary: 3.71:1 in dark, under 4.5:1; exempted here as in checkbox r1. (3) A checked radio has no hover rule of its own; only color-interactive shifts it to -hover, 1.03 from rest, so hover on a checked radio is invisible in a; Q101 fixes it in b and c. (4) Readonly draws exactly like rest, as with the checkbox. (5) A disabled group dims its options (opacity 0.5 and the disabled label colour, both at once) but not the group label, which keeps 7.81 in light; the same holds for the checkbox group. Later rounds, left out here: the sizes sm and lg, the card with the control at start and the card slots, a coloured (non-brand) radio, the warning state, a disabled option inside an enabled group, a pending (aria-busy) group, long labels that wrap, RTL and the check animation.',
91
+ madeBy: ['claude-opus-5-5'],
92
+ states: {
93
+ ...gridStates,
94
+ 'group-none': { group: reminderGroup(null) },
95
+ 'group-checked': { group: reminderGroup(0) },
96
+ 'group-support': {
97
+ group: {
98
+ id: 'support',
99
+ label: 'Kick-off reminder',
100
+ hint: 'We send it as a push notification.',
101
+ options: [
102
+ { label: 'One hour before', description: 'Time to get to the ground.', checked: true },
103
+ { label: 'At kick-off', description: 'Only when the whistle goes.' },
104
+ ],
105
+ },
106
+ },
107
+ 'group-option-hover': { group: reminderGroup(0, { force: ['hover'] }) },
108
+ 'group-option-pressed': { group: reminderGroup(0, { force: ['hover', 'active'] }) },
109
+ 'group-option-focus-visible': { group: reminderGroup(1, { force: ['focus-visible'] }) },
110
+ 'group-mixed': { group: reminderGroup(0, { mixed: true, hint: 'The selected matches disagree.' }) },
111
+ 'group-disabled': { group: reminderGroup(0, { disabled: true }) },
112
+ 'group-readonly': { group: reminderGroup(0, { readonly: true }) },
113
+ invalid: { group: reminderGroup(null, INVALID) },
114
+ 'invalid-focus-visible': { group: reminderGroup(null, { ...INVALID, force: ['focus-visible'] }) },
115
+ 'invalid-checked': { group: reminderGroup(1, INVALID) },
116
+ 'group-horizontal': {
117
+ group: {
118
+ id: 'venue',
119
+ label: 'Show matches at',
120
+ orientation: 'horizontal',
121
+ options: [{ label: 'Home', checked: true }, { label: 'Away' }, { label: 'Neutral ground' }],
122
+ },
123
+ },
124
+ 'card-rest': { group: formatGroup(null) },
125
+ 'card-checked': { group: formatGroup(0) },
126
+ 'card-hover': { group: formatGroup(1, ['hover']) },
127
+ 'card-focus-visible': { group: formatGroup(1, ['focus-visible']) },
128
+ 'card-checked-focus-visible': { group: formatGroup(0, ['focus-visible']) },
129
+ },
130
+ grid: {
131
+ columns: COLUMNS,
132
+ rows: Object.fromEntries(
133
+ (Object.keys(ROWS) as Row[]).map((row) => [
134
+ row,
135
+ Object.fromEntries(COLUMNS.map((column) => [column, stateOf(row, column)])),
136
+ ]),
137
+ ),
138
+ },
139
+ omits: [
140
+ { option: 'size', value: 'sm', reason: 'Not drawn in this round: sizes are a later round.' },
141
+ { option: 'size', value: 'md', reason: 'The default size, which every state draws without setting it.' },
142
+ { option: 'size', value: 'lg', reason: 'Not drawn in this round: sizes are a later round.' },
143
+ {
144
+ option: 'orientation',
145
+ value: 'vertical',
146
+ reason: 'The default orientation, drawn by the group-* states, outside the grid.',
147
+ },
148
+ { option: 'orientation', value: 'horizontal', reason: 'Drawn as the group-horizontal state, outside the grid.' },
149
+ { option: 'variant', value: 'plain', reason: 'The default variant, which the grid draws without setting it.' },
150
+ { option: 'variant', value: 'card', reason: 'Drawn as the card-* states, outside the grid.' },
151
+ {
152
+ option: 'controlPosition',
153
+ value: 'start',
154
+ reason: 'Not drawn in this round: the card with the control at start is a later round.',
155
+ },
156
+ { option: 'controlPosition', value: 'end', reason: 'Drawn as the card-* states, outside the grid.' },
157
+ ],
158
+ controls: {
159
+ radio: { kind: 'button', input: ['pointer', 'keyboard'], disabled: true },
160
+ },
161
+ widths: [360, 640],
162
+ render: (props: Props) =>
163
+ drawing({
164
+ body: `<div class="t-frame">${marked(radioGroup(props.group))}</div>`,
165
+ styles: [radioGroupStyles, frame].join('\n'),
166
+ }),
167
+ });
@@ -0,0 +1,55 @@
1
+ .t-frame {
2
+ box-sizing: border-box;
3
+ padding: 1.6rem;
4
+ background: var(--et-surface-background-solid);
5
+ color: var(--et-surface-color-solid);
6
+ font-size: 1.4rem;
7
+ }
8
+
9
+ @layer components {
10
+ .et-button,
11
+ .et-icon-button {
12
+ --_et-focus-ring-color: var(--et-theme-color-ink-solid);
13
+ }
14
+
15
+ .et-button:where([data-variant='filled']) {
16
+ --et-button-shadow: inset 0 0 0 1px var(--et-theme-color-ink-solid);
17
+
18
+ @media (hover: hover) {
19
+ &:hover {
20
+ --et-theme-color-primary-rgb: var(--et-color-primary-active);
21
+ }
22
+ }
23
+ }
24
+
25
+ .et-segmented-button,
26
+ .et-scheduler-time-grid-slot,
27
+ .et-scheduler-time-grid-all-day-cell,
28
+ .et-scheduler-month-view-cell,
29
+ .et-scheduler-appointment,
30
+ .et-scheduler-time-grid-block,
31
+ .et-scheduler-month-view-overflow-trigger {
32
+ &:focus-visible {
33
+ outline-color: var(--et-theme-color-ink-solid);
34
+ }
35
+ }
36
+
37
+ :is(.et-scheduler-appointment, .et-scheduler-time-grid-block):not([data-selected]) .et-scheduler-appointment-dot {
38
+ background: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
39
+ }
40
+
41
+ .et-scheduler-time-grid-now {
42
+ border-block-start-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
43
+
44
+ &::before {
45
+ background: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
46
+ }
47
+ }
48
+
49
+ .et-scheduler-time-grid-header-day[data-today] .et-scheduler-time-grid-header-date,
50
+ .et-scheduler-month-view-cell[data-today] .et-scheduler-month-view-cell-date,
51
+ .et-scheduler-agenda-view-day-header[data-today] .et-scheduler-agenda-view-day-date,
52
+ :is(.et-scheduler-appointment, .et-scheduler-time-grid-block)[data-selected] {
53
+ box-shadow: inset 0 0 0 1px var(--et-theme-color-ink-solid);
54
+ }
55
+ }
@@ -0,0 +1,240 @@
1
+ import {
2
+ scheduler,
3
+ schedulerAgenda,
4
+ SchedulerMockAppointment,
5
+ SchedulerMockDay,
6
+ SchedulerMockMonthCell,
7
+ SchedulerMockTimeBlock,
8
+ SchedulerMockView,
9
+ schedulerMonth,
10
+ schedulerStyles,
11
+ schedulerTimeGrid,
12
+ } from '@ethlete/components/design/scheduler';
13
+ import { definePart, drawing } from '@design-system';
14
+ import frame from './scheduler.css?inline';
15
+
16
+ type Interaction = 'hover' | 'focus-visible';
17
+
18
+ type Props = {
19
+ view: SchedulerMockView;
20
+ appointment?: Interaction;
21
+ slot?: Interaction;
22
+ cell?: Interaction;
23
+ dragging?: boolean;
24
+ };
25
+
26
+ const clock = (time: number) => {
27
+ const hour = Math.floor(time);
28
+ const minutes = String(Math.round((time - hour) * 60)).padStart(2, '0');
29
+
30
+ return `${hour % 12 || 12}:${minutes} ${hour < 12 ? 'AM' : 'PM'}`;
31
+ };
32
+
33
+ const range = (start: number, end: number) => `${clock(start)}–${clock(end)}`;
34
+
35
+ const WED: SchedulerMockDay = { weekday: 'Wed', date: 4, label: 'Wednesday, March 4th, 2026', today: true };
36
+ const THU: SchedulerMockDay = { weekday: 'Thu', date: 5, label: 'Thursday, March 5th, 2026' };
37
+ const FRI: SchedulerMockDay = { weekday: 'Fri', date: 6, label: 'Friday, March 6th, 2026' };
38
+
39
+ const WEEK: SchedulerMockDay[] = [
40
+ { weekday: 'Mon', date: 2, label: 'Monday, March 2nd, 2026' },
41
+ { weekday: 'Tue', date: 3, label: 'Tuesday, March 3rd, 2026' },
42
+ WED,
43
+ THU,
44
+ FRI,
45
+ { weekday: 'Sat', date: 7, label: 'Saturday, March 7th, 2026' },
46
+ { weekday: 'Sun', date: 8, label: 'Sunday, March 8th, 2026' },
47
+ ];
48
+
49
+ const TODAY = 2;
50
+
51
+ const block = (
52
+ day: number,
53
+ start: number,
54
+ end: number,
55
+ appointment: SchedulerMockAppointment,
56
+ extra: Partial<SchedulerMockTimeBlock> = {},
57
+ ): SchedulerMockTimeBlock => ({ day, start, end, timeRange: range(start, end), ...appointment, ...extra });
58
+
59
+ const weekBlocks = (props: Props): SchedulerMockTimeBlock[] => [
60
+ block(0, 9, 9.5, { title: 'Daily sync', force: props.appointment }),
61
+ block(1, 9, 9.5, { title: 'Daily sync' }),
62
+ block(
63
+ 1,
64
+ 10,
65
+ 12,
66
+ { title: 'Roadmap planning', color: 'warning', location: 'Room 2', chainCount: 1 },
67
+ { inlineOffset: 50, inlineSize: 50 },
68
+ ),
69
+ block(1, 10.5, 11.5, { title: 'Budget check', color: 'warning' }, { inlineOffset: 0, inlineSize: 50, depth: 1 }),
70
+ block(TODAY, 9, 9.5, { title: 'Daily sync' }),
71
+ block(TODAY, 13, 14.5, { title: 'Design review', location: 'Studio A', selected: true }),
72
+ block(3, 9, 9.5, { title: 'Daily sync' }),
73
+ block(3, 14, 16, { title: 'Hiring panel', color: 'danger', location: 'Room 4' }),
74
+ block(4, 9, 9.5, { title: 'Daily sync' }),
75
+ block(4, props.dragging ? 15.5 : 15, props.dragging ? 16.5 : 16, { title: 'Retro', dragging: props.dragging }),
76
+ ];
77
+
78
+ const weekNonBusiness = WEEK.flatMap((_, day) =>
79
+ day >= 5
80
+ ? [{ day, from: 8, to: 18 }]
81
+ : [
82
+ { day, from: 8, to: 9 },
83
+ { day, from: 17, to: 18 },
84
+ ],
85
+ );
86
+
87
+ const timeGrid = (props: Props) => {
88
+ const single = props.view === 'day';
89
+ const days = single ? [WED] : WEEK;
90
+ const pick = (day: number) => (single ? (day === TODAY ? 0 : -1) : day);
91
+ const blocks = weekBlocks(props)
92
+ .map((entry) => ({ ...entry, day: pick(entry.day) }))
93
+ .filter((entry) => entry.day >= 0);
94
+
95
+ return schedulerTimeGrid({
96
+ days,
97
+ fromHour: 8,
98
+ toHour: 18,
99
+ hourLabel: (hour) => clock(hour),
100
+ blocks,
101
+ allDay: single ? [] : [{ day: 3, span: 2, title: 'Conference', color: 'danger' }],
102
+ now: { day: pick(TODAY), time: 11.25 },
103
+ nonBusiness: weekNonBusiness
104
+ .map((segment) => ({ ...segment, day: pick(segment.day) }))
105
+ .filter((segment) => segment.day >= 0),
106
+ forcedSlot: props.slot ? { day: single ? 0 : 3, hour: 11, force: props.slot } : undefined,
107
+ ariaLabel: single ? 'Wednesday, March 4, 2026' : 'March 2 – 8, 2026',
108
+ });
109
+ };
110
+
111
+ const MONTH_APPOINTMENTS: Record<number, SchedulerMockAppointment[]> = {
112
+ 2: [{ title: 'Daily sync', timeRange: range(9, 9.5) }],
113
+ 3: [
114
+ { title: 'Daily sync', timeRange: range(9, 9.5) },
115
+ { title: 'Roadmap planning', timeRange: range(10, 12) },
116
+ ],
117
+ 4: [
118
+ { title: 'Daily sync', timeRange: range(9, 9.5) },
119
+ { title: 'Design review', timeRange: range(13, 14.5), selected: true },
120
+ ],
121
+ 5: [{ title: 'Conference', color: 'danger' }],
122
+ 6: [{ title: 'Conference', color: 'danger' }],
123
+ 12: [{ title: 'Quarterly review', timeRange: range(14, 16) }],
124
+ 17: [
125
+ { title: 'Daily sync', timeRange: range(9, 9.5) },
126
+ { title: 'Workshop', color: 'warning', timeRange: range(10, 13) },
127
+ ],
128
+ 25: [{ title: 'Release', color: 'danger' }],
129
+ };
130
+
131
+ const monthCell = (date: number, outsideMonth: boolean, props: Props): SchedulerMockMonthCell => {
132
+ const appointments = outsideMonth ? [] : (MONTH_APPOINTMENTS[date] ?? []);
133
+
134
+ return {
135
+ date,
136
+ label: date === 4 && !outsideMonth ? 'Wednesday, March 4th, 2026' : `${date}`,
137
+ outsideMonth,
138
+ today: !outsideMonth && date === 4,
139
+ appointments: (date === 3 && !outsideMonth ? appointments.slice(0, 2) : appointments).map((appointment) =>
140
+ date === 2 && !outsideMonth ? { ...appointment, force: props.appointment } : appointment,
141
+ ),
142
+ overflow: date === 17 && !outsideMonth ? 3 : undefined,
143
+ force: date === 19 && !outsideMonth ? props.cell : undefined,
144
+ };
145
+ };
146
+
147
+ const month = (props: Props) => {
148
+ const dates = [23, 24, 25, 26, 27, 28, ...Array.from({ length: 31 }, (_, index) => index + 1), 1, 2, 3, 4, 5];
149
+ const weeks = Array.from({ length: 6 }, (_, week) =>
150
+ dates.slice(week * 7, week * 7 + 7).map((date, index) => {
151
+ const position = week * 7 + index;
152
+
153
+ return monthCell(date, position < 6 || position > 36, props);
154
+ }),
155
+ );
156
+
157
+ return schedulerMonth({
158
+ weekdays: WEEK.map((day) => ({ short: day.weekday, long: day.label.split(',')[0] ?? day.weekday })),
159
+ weeks,
160
+ ariaLabel: 'March 2026',
161
+ });
162
+ };
163
+
164
+ const agenda = (props: Props) =>
165
+ schedulerAgenda({
166
+ days: [
167
+ {
168
+ ...WED,
169
+ month: 'March 2026',
170
+ rows: [
171
+ { title: 'Daily sync', timeRange: range(9, 9.5), force: props.appointment },
172
+ {
173
+ title: 'Design review',
174
+ timeRange: range(13, 14.5),
175
+ location: 'Studio A',
176
+ selected: true,
177
+ },
178
+ ],
179
+ },
180
+ {
181
+ ...THU,
182
+ rows: [
183
+ { title: 'Conference', color: 'danger' },
184
+ { title: 'Daily sync', timeRange: range(9, 9.5) },
185
+ { title: 'Hiring panel', color: 'danger', timeRange: range(14, 16), location: 'Room 4', chainCount: 2 },
186
+ { title: 'Intro call', color: 'danger', timeRange: range(14, 14.5), guides: ['branch'] },
187
+ { title: 'Debrief', color: 'danger', timeRange: range(15.5, 16), guides: ['last-branch'] },
188
+ ],
189
+ },
190
+ {
191
+ ...FRI,
192
+ rows: [
193
+ { title: 'Conference', color: 'danger' },
194
+ { title: 'Retro', timeRange: range(15, 16) },
195
+ ],
196
+ },
197
+ ],
198
+ });
199
+
200
+ const HEADER_LABELS: Record<SchedulerMockView, string> = {
201
+ week: 'March 2 – 8, 2026',
202
+ day: 'Wednesday, March 4, 2026',
203
+ month: 'March 2026',
204
+ agenda: 'March 4 – 10, 2026',
205
+ };
206
+
207
+ const render = (props: Props) => {
208
+ const body = props.view === 'month' ? month(props) : props.view === 'agenda' ? agenda(props) : timeGrid(props);
209
+
210
+ return scheduler({ view: props.view, headerLabel: HEADER_LABELS[props.view], addAction: true, body });
211
+ };
212
+
213
+ const skipClipped = (markup: string) =>
214
+ markup.replaceAll(
215
+ 'class="et-scheduler-time-grid-block',
216
+ 'data-et-skip="CLIPPED" class="et-scheduler-time-grid-block',
217
+ );
218
+
219
+ export default definePart({
220
+ note: "The SDK scheduler (et-scheduler) in flow, first drawing, drawn by the mock from @ethlete/components/design/scheduler with the real markup and CSS: the wide header (outline Today button, filled Add appointment, previous and next icon buttons, the range label, the tabs view switch), et-scheduler-time-grid-view (week and day), et-scheduler-month-view and et-scheduler-agenda-view, the appointment badges (color dot, title, time range, location, chain count) and the resize handles; default labels, en-US times. Fixtures are fictional: one week (Mon March 2 to Sun March 8, 2026, today Wed March 4) with a daily sync, an overlapping parent and nested child (Roadmap planning / Budget check), a selected appointment (Design review, brand), a two-day all-day Conference, Hiring panel and Retro; the colors are the sdk product's registered themes brand (default), warning and danger. The time grid draws 8:00 to 18:00 as if the scrolling body sat on that window, with business hours 9 to 17 on weekdays and the weekend outside them; the current time line sits on today at 11:15. Framed as a page with 16px padding. Widths 560, 800 and 1200 (all above the 480px container width where the header switches to floating action buttons). States: week, week-dragging (Retro lifted, with the drag shadow), appointment-hover, appointment-focus-visible (Daily sync on Monday forced), slot-focus-visible (Thursday 11:00 forced), day, month (outside-month days, today, a +3 more trigger, the selected appointment), month-cell-focus-visible, agenda (a month heading, today, nested rows with tree guides, the selected appointment). Numbers (light / dark), from shot pixels at scale 2 at 800, brand mint, a: appointment tint (14 %) vs page 1.07 / 1.37, its title ink on it 5.12 / 9.84; selected appointment (mint fill) vs page 1.33 / 13.50, its text 15.82 / 15.82; today date circle (mint fill) vs page 1.33 / 13.50, its digit 15.82; current time line and its dot vs the today column 1.29 / 12.14; focus ring (primary) vs page 1.35 / 13.50, vs the appointment tint 1.26 / 9.84; Add appointment fill vs page 1.33 / 13.50, its label 15.82. Q98 numbers only: grid lines (hour rows, day columns, header and all-day edges, month cells) vs page 1.26 / 1.73; the non-business band 1.07 / 1.08; the today column tint is 6 % mint. Decided rules that arise: Q88 (every focus ring: slots, all-day cells, month cells, appointments, the overflow trigger, the view switch, the header buttons), Q99a (the current time line and its dot, the appointment color dot; the selected appointment, the today date circles and the filled Add appointment button are mint fills, so they take a 1px inset ink edge) and Q101 (the filled button hover = -active). The ink edge on a mint fill arises, so Q102 arises: b draws the edges in both schemes, c keeps dark as today (light-dark(ink, transparent)). The 32 / 38 / 44 % selection step does not arise: a selected appointment is a solid mint fill, not a tint. Findings for later: (1) lib bug: the badge pieces render inside their component hosts (et-scheduler-badge-color-dot, -title, -time-range, -location, -chain-count), which are inline, so the flex rules on the inner spans never apply: the color dot is 0 x 0 and never shows (also in Storybook), the title does not take flex: 1 or ellipsis, and the time range and location are cut hard at the block edge (the agenda location shrinks to one letter while the title has room). Fixed in the lib (display: contents on the five hosts; the time range and location shrink with an ellipsis before the title). Narrow week blocks still clip, because the gaps, the dot and the chain count alone are wider than the block (31 px at 560), so data-et-skip CLIPPED stays on every time grid block; the month cell skip is gone. (2) The first hour label sat half above the scroll body (translateY(-50%)) and was clipped by its overflow; fixed in the lib with a top padding of half the label line height. (3) Outside-month dates read nearly like in-month ones. (4) The selected appointment edge and the drag shadow share box-shadow, so a selected appointment being dragged would lose one (not drawn); the lib has no selected edge yet, so whoever merges b or c must list the edge and the drag shadow in one rule on [data-selected][data-dragging]. (5) The Add appointment hover (Q101) is not forced here; it is the same et-button as in the stream round (hover 1.69 from rest). Later rounds, left out here: the edit surface (scheduler-edit-surface and its parts, an overlay), the month +n more menu, the narrow header with floating action buttons, the drag-to-create draft and committed blocks, month draft and drop-target cells, the scrollbar, custom badge adornments, the read-only scheduler, RTL and long labels.",
221
+ madeBy: ['claude-opus-5-5'],
222
+ states: {
223
+ week: { view: 'week' },
224
+ 'week-dragging': { view: 'week', dragging: true },
225
+ 'appointment-hover': { view: 'week', appointment: 'hover' },
226
+ 'appointment-focus-visible': { view: 'week', appointment: 'focus-visible' },
227
+ 'slot-focus-visible': { view: 'week', slot: 'focus-visible' },
228
+ day: { view: 'day' },
229
+ month: { view: 'month' },
230
+ 'month-cell-focus-visible': { view: 'month', cell: 'focus-visible' },
231
+ agenda: { view: 'agenda' },
232
+ },
233
+ widths: [560, 800, 1200],
234
+ frame: (body) => `<div class="t-frame">${body}</div>`,
235
+ render: (props: Props) =>
236
+ drawing({
237
+ body: skipClipped(render(props)),
238
+ styles: [schedulerStyles, frame].join('\n'),
239
+ }),
240
+ });
@@ -0,0 +1,58 @@
1
+ .scrollable-frame {
2
+ padding: 2.4rem;
3
+ background: var(--et-surface-background-solid);
4
+ color: var(--et-surface-color-solid);
5
+ font-size: 1.4rem;
6
+
7
+ .et-scrollable[direction='vertical'] {
8
+ inline-size: 400px;
9
+
10
+ .et-scrollable-container {
11
+ block-size: 258px;
12
+ }
13
+
14
+ .et-scrollable-container > * {
15
+ translate: 0 var(--scrollable-offset);
16
+ }
17
+ }
18
+
19
+ .et-scrollable-container {
20
+ block-size: 128px;
21
+ gap: 12px;
22
+ padding-block: 4px;
23
+ margin-block: -4px;
24
+ }
25
+
26
+ .et-scrollable-container > * {
27
+ translate: var(--scrollable-offset) 0;
28
+ }
29
+
30
+ .scrollable-item {
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ min-inline-size: 200px;
35
+ min-block-size: 80px;
36
+ block-size: 100%;
37
+ border: none;
38
+ border-radius: 12px;
39
+ font-size: 1.8rem;
40
+ font-weight: 600;
41
+ background: color-mix(in srgb, var(--et-surface-color-solid) 14%, transparent);
42
+ color: var(--et-surface-color-solid);
43
+ }
44
+
45
+ .scrollable-item:focus-visible {
46
+ outline: -webkit-focus-ring-color auto 1px;
47
+ }
48
+ }
49
+
50
+ .scrollable-frame--sized .scrollable-item {
51
+ min-inline-size: 0;
52
+ }
53
+
54
+ @layer components {
55
+ .et-scrollable-navigation-item.et-scrollable-navigation-item--active::after {
56
+ background-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary));
57
+ }
58
+ }
@@ -0,0 +1,133 @@
1
+ import { scrollable, scrollableStyles } from '@ethlete/components/design/scrollable';
2
+ import { definePart, drawing } from '@design-system';
3
+ import frame from './scrollable.css?inline';
4
+
5
+ type Position = 'start' | 'middle' | 'end';
6
+ type Interaction = 'hover' | 'active';
7
+
8
+ const ITEM_SIZES = ['same', 'full', 'half', 'third', 'quarter'] as const;
9
+ type ItemSize = (typeof ITEM_SIZES)[number];
10
+
11
+ type Props = {
12
+ direction?: 'horizontal' | 'vertical';
13
+ position?: Position;
14
+ items?: number;
15
+ maskVariant?: 'gradient' | 'border';
16
+ buttons?: 'inside' | 'footer' | 'none';
17
+ navigation?: boolean;
18
+ darken?: boolean;
19
+ color?: string;
20
+ endButton?: Interaction;
21
+ focusItem?: number;
22
+ itemSize?: ItemSize;
23
+ };
24
+
25
+ const ITEM = { horizontal: 200, vertical: 80 };
26
+ const VIEWPORT = { horizontal: 432, vertical: 258 };
27
+ const GAP = 12;
28
+
29
+ const extent = (count: number, size: number) => count * size + (count - 1) * GAP;
30
+
31
+ const offsetOf = (props: Required<Pick<Props, 'direction' | 'position' | 'items'>>) => {
32
+ const size = ITEM[props.direction];
33
+ const max = Math.max(0, extent(props.items, size) - VIEWPORT[props.direction]);
34
+
35
+ if (props.position === 'start') return 0;
36
+ if (props.position === 'end') return max;
37
+
38
+ return Math.min(max, 3 * (size + GAP) - Math.round((VIEWPORT[props.direction] - size) / 2));
39
+ };
40
+
41
+ const visibleRange = (offset: number, size: number, viewport: number, count: number) =>
42
+ Array.from({ length: count }, (_, index) => {
43
+ const start = index * (size + GAP) - offset;
44
+ const end = start + size;
45
+ const ratio = Math.max(0, Math.min(end, viewport) - Math.max(start, 0)) / size;
46
+
47
+ return ratio;
48
+ });
49
+
50
+ const render = (props: Props) => {
51
+ const direction = props.direction ?? 'horizontal';
52
+ const items = props.items ?? 8;
53
+ const position = props.position ?? 'start';
54
+ const size = ITEM[direction];
55
+ const canScroll = extent(items, size) > VIEWPORT[direction];
56
+ const offset = offsetOf({ direction, position, items });
57
+ const ratios = visibleRange(offset, size, VIEWPORT[direction], items);
58
+ const best = ratios.reduce((found, ratio, index) => (ratio > (ratios[found] ?? 0) ? index : found), 0);
59
+ const active = position === 'end' ? items - 1 : position === 'start' ? 0 : best;
60
+ const buttons = props.buttons ?? 'inside';
61
+
62
+ const markup = scrollable({
63
+ direction,
64
+ canScroll,
65
+ position,
66
+ maskVariant: props.maskVariant,
67
+ buttons: buttons === 'none' ? undefined : buttons,
68
+ navigationActive: props.navigation ? active : undefined,
69
+ darken: props.darken,
70
+ itemSize: props.itemSize,
71
+ itemGap: props.itemSize ? GAP : undefined,
72
+ color: props.color,
73
+ endButtonAttributes: props.endButton ? `data-et-force="${props.endButton}"` : undefined,
74
+ containerAttributes: `data-et-skip="CLIPPED" style="--scrollable-offset: -${offset}px"`,
75
+ items: Array.from({ length: items }, (_, index) => ({
76
+ tag: 'button',
77
+ content: `Item ${index}`,
78
+ notIntersecting: props.darken && (ratios[index] ?? 0) < 1,
79
+ attributes: [
80
+ 'class="scrollable-item"',
81
+ 'type="button"',
82
+ props.focusItem === index && 'data-et-force="focus-visible"',
83
+ ]
84
+ .filter(Boolean)
85
+ .join(' '),
86
+ })),
87
+ });
88
+
89
+ return markup.replace(
90
+ 'class="et-scrollable-progress-bar"',
91
+ 'data-et-skip="CLIPPED" class="et-scrollable-progress-bar"',
92
+ );
93
+ };
94
+
95
+ export default definePart({
96
+ note: 'scale of the decided shape, Q182, lead merge',
97
+ madeBy: ['claude-opus-5-5', 'claude-sonnet-5-5'],
98
+ states: {
99
+ 'horizontal-start': {},
100
+ 'horizontal-middle': { position: 'middle' },
101
+ 'horizontal-end': { position: 'end' },
102
+ 'vertical-start': { direction: 'vertical' },
103
+ 'vertical-middle': { direction: 'vertical', position: 'middle' },
104
+ 'vertical-end': { direction: 'vertical', position: 'end' },
105
+ 'button-hover': { position: 'middle', endButton: 'hover' },
106
+ 'button-active': { position: 'middle', endButton: 'active' },
107
+ 'border-mask': { position: 'middle', maskVariant: 'border' },
108
+ 'vertical-border-mask': { direction: 'vertical', position: 'middle', maskVariant: 'border' },
109
+ 'navigation-start': { navigation: true },
110
+ 'navigation-middle': { position: 'middle', navigation: true },
111
+ 'navigation-end': { position: 'end', navigation: true },
112
+ 'vertical-navigation': { direction: 'vertical', position: 'middle', navigation: true },
113
+ 'footer-buttons': { position: 'middle', buttons: 'footer', navigation: true },
114
+ 'footer-button-hover': { position: 'middle', buttons: 'footer', navigation: true, endButton: 'hover' },
115
+ 'danger-navigation': { position: 'middle', navigation: true, color: 'danger' },
116
+ darken: { position: 'middle', darken: true },
117
+ 'item-focus-visible': { focusItem: 1 },
118
+ 'short-content': { items: 2 },
119
+ 'short-content-navigation': { items: 2, navigation: true },
120
+ ...(Object.fromEntries(
121
+ ITEM_SIZES.map((itemSize) => [
122
+ `item-size-${itemSize}`,
123
+ itemSize === 'same' ? { itemSize, items: 2 } : { itemSize },
124
+ ]),
125
+ ) as Record<`item-size-${ItemSize}`, Props>),
126
+ },
127
+ widths: [480],
128
+ render: (props: Props) =>
129
+ drawing({
130
+ body: `<div class="scrollable-frame ${props.itemSize ? 'scrollable-frame--sized' : ''}">${render(props)}</div>`,
131
+ styles: [scrollableStyles, frame].join('\n'),
132
+ }),
133
+ });