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

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 (88) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/design/anchored-dialog.ts +98 -0
  3. package/design/button.ts +13 -1
  4. package/design/config.json +6 -1
  5. package/design/dialog.ts +5 -3
  6. package/design/form-field.ts +3 -0
  7. package/design/menu.ts +8 -4
  8. package/design/notification.ts +8 -4
  9. package/design/sdk/components/accordion/accordion.ts +5 -1
  10. package/design/sdk/components/anchored-dialog/anchored-dialog.css +63 -0
  11. package/design/sdk/components/anchored-dialog/anchored-dialog.ts +75 -0
  12. package/design/sdk/components/avatar/avatar.ts +3 -2
  13. package/design/sdk/components/badge/badge.ts +2 -1
  14. package/design/sdk/components/banner/banner.ts +2 -1
  15. package/design/sdk/components/bracket/bracket.ts +2 -1
  16. package/design/sdk/components/breadcrumb/breadcrumb.ts +2 -1
  17. package/design/sdk/components/button/button.css +11 -3
  18. package/design/sdk/components/button/button.ts +17 -4
  19. package/design/sdk/components/calendar/calendar.ts +2 -1
  20. package/design/sdk/components/card/card.ts +2 -1
  21. package/design/sdk/components/carousel/carousel.ts +2 -1
  22. package/design/sdk/components/chart/chart.ts +2 -1
  23. package/design/sdk/components/checkbox/checkbox.css +65 -0
  24. package/design/sdk/components/checkbox/checkbox.ts +180 -0
  25. package/design/sdk/components/chip/chip.ts +2 -1
  26. package/design/sdk/components/copy-button/copy-button.ts +2 -1
  27. package/design/sdk/components/description-list/description-list.ts +2 -1
  28. package/design/sdk/components/dialog/dialog.ts +2 -1
  29. package/design/sdk/components/divider/divider.ts +2 -1
  30. package/design/sdk/components/empty-state/empty-state.ts +2 -1
  31. package/design/sdk/components/grid/grid.ts +3 -2
  32. package/design/sdk/components/input/input.ts +2 -1
  33. package/design/sdk/components/kbd/kbd.ts +2 -1
  34. package/design/sdk/components/loader/loader.ts +2 -1
  35. package/design/sdk/components/masonry/masonry.ts +3 -2
  36. package/design/sdk/components/match/match.ts +2 -1
  37. package/design/sdk/components/menu/menu.ts +19 -7
  38. package/design/sdk/components/notification/notification.ts +2 -1
  39. package/design/sdk/components/otp-input/otp-input.ts +2 -1
  40. package/design/sdk/components/pagination/pagination.ts +2 -1
  41. package/design/sdk/components/picture/picture.ts +2 -1
  42. package/design/sdk/components/progress-steps/progress-steps.ts +2 -1
  43. package/design/sdk/components/query-error/query-error.ts +2 -1
  44. package/design/sdk/components/radio-group/radio-group.css +46 -0
  45. package/design/sdk/components/radio-group/radio-group.ts +167 -0
  46. package/design/sdk/components/scheduler/scheduler.ts +2 -1
  47. package/design/sdk/components/scrollable/scrollable.ts +2 -2
  48. package/design/sdk/components/scrollbar/scrollbar.ts +2 -1
  49. package/design/sdk/components/select/shared/select-part.ts +11 -2
  50. package/design/sdk/components/select-panel/select-panel.ts +2 -1
  51. package/design/sdk/components/skeleton/skeleton.ts +2 -1
  52. package/design/sdk/components/slider/slider.ts +2 -1
  53. package/design/sdk/components/standings/standings.ts +2 -1
  54. package/design/sdk/components/stat-tile/stat-tile.ts +2 -1
  55. package/design/sdk/components/stream/stream.ts +6 -1
  56. package/design/sdk/components/switch/switch.css +4 -2
  57. package/design/sdk/components/switch/switch.ts +2 -2
  58. package/design/sdk/components/table/table.ts +2 -1
  59. package/design/sdk/components/textarea/textarea.ts +4 -3
  60. package/design/sdk/components/timeline/timeline.ts +2 -2
  61. package/design/sdk/components/toolbar/toolbar.ts +2 -1
  62. package/design/sdk/foundations/colors/colors.css +16 -0
  63. package/design/sdk/foundations/colors/colors.ts +1 -1
  64. package/design/sdk/foundations/colors/shared/contrast.ts +1 -1
  65. package/design/sdk/lint-baseline.json +6 -0
  66. package/design/sdk/pictures.json +1195 -0
  67. package/design/sdk/ready.json +64 -0
  68. package/design/sdk/rule-baseline.json +104 -0
  69. package/design/select.ts +9 -5
  70. package/design/textarea.ts +2 -0
  71. package/design/tooltip.ts +5 -5
  72. package/fesm2022/ethlete-components.mjs +1190 -973
  73. package/fesm2022/ethlete-components.mjs.map +1 -1
  74. package/generators/generators.json +2 -0
  75. package/package.json +1 -1
  76. package/src/lib/button/button-properties-styles.component.css +6 -6
  77. package/src/lib/button/button.component.css +11 -7
  78. package/src/lib/button/button.types.ts +33 -0
  79. package/src/lib/button/icon-button.component.css +3 -3
  80. package/src/lib/button/window-control-button.component.css +70 -3
  81. package/src/lib/calendar/calendar.component.css +2 -2
  82. package/src/lib/chip/chip.component.css +4 -4
  83. package/src/lib/focus-ring/focus-ring-styles.component.css +20 -5
  84. package/src/lib/forms/switch/switch.component.css +1 -14
  85. package/src/lib/kbd/kbd.component.css +1 -1
  86. package/src/lib/menu/menu.component.css +2 -2
  87. package/src/lib/notification/notification.component.css +3 -3
  88. package/types/ethlete-components.d.ts +171 -139
@@ -53,9 +53,10 @@ export default definePart({
53
53
  cancel: { kind: 'button', input: ['pointer', 'keyboard'] },
54
54
  confirm: { kind: 'button', input: ['pointer', 'keyboard'] },
55
55
  },
56
+ frame: (body) => `<div class="card-frame">${body}</div>`,
56
57
  render: (props: Props) =>
57
58
  drawing({
58
- body: `<div class="card-frame">${card({ variant: props.variant, content: content(props) })}</div>`,
59
+ body: card({ variant: props.variant, content: content(props) }),
59
60
  styles: [cardStyles, frame].join('\n'),
60
61
  }),
61
62
  });
@@ -66,9 +66,10 @@ export default definePart({
66
66
  'single-slide': { count: 1 },
67
67
  },
68
68
  widths: [480],
69
+ frame: (body) => `<div class="carousel-frame">${body}</div>`,
69
70
  render: (props: Props) =>
70
71
  drawing({
71
- body: `<div class="carousel-frame">${render(props)}</div>`,
72
+ body: render(props),
72
73
  styles: [carouselStyles, frame].join('\n'),
73
74
  }),
74
75
  });
@@ -90,9 +90,10 @@ export default definePart({
90
90
  'pie-long-label': { kind: 'pie', longLabel: true },
91
91
  },
92
92
  widths: [480],
93
+ frame: (body) => `<div class="t-frame">${body}</div>`,
93
94
  render: (props: Props) =>
94
95
  drawing({
95
- body: `<div class="t-frame">${render(props)}</div>`,
96
+ body: render(props),
96
97
  styles: [chartStyles, frame].join('\n'),
97
98
  }),
98
99
  });
@@ -0,0 +1,65 @@
1
+ .t-frame {
2
+ box-sizing: border-box;
3
+ display: grid;
4
+ justify-items: start;
5
+ gap: 1.6rem;
6
+ padding: 1.6rem;
7
+ background: var(--et-surface-background-solid);
8
+ color: var(--et-surface-color-solid);
9
+ font-size: 1.4rem;
10
+ }
11
+
12
+ @layer components {
13
+ et-checkbox.et-focus-ring,
14
+ et-choice-field[data-error]
15
+ et-checkbox.et-checkbox:focus-visible:not([aria-checked='true']):not([aria-checked='mixed']) {
16
+ --_et-focus-ring-color: var(--et-theme-color-ink-solid);
17
+ }
18
+
19
+ et-checkbox:is([aria-checked='true'], [aria-checked='mixed']) .et-checkbox-box {
20
+ outline: 1px solid light-dark(var(--et-theme-color-ink-solid), transparent);
21
+ outline-offset: -1px;
22
+ }
23
+
24
+ et-choice-field[data-variant='card']
25
+ et-checkbox:focus-visible:not([aria-checked='true']):not([aria-checked='mixed'])
26
+ .et-checkbox-box {
27
+ border-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
28
+ }
29
+
30
+ et-choice-field
31
+ .et-selection-card:where([aria-checked='true'], :has(:is([aria-checked='true'], [aria-checked='mixed']))) {
32
+ border-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
33
+ }
34
+
35
+ et-choice-field .et-selection-card:where(:focus-visible, :has(:focus-visible)) {
36
+ border-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
37
+ outline-color: var(--et-theme-color-ink-solid);
38
+ }
39
+
40
+ et-checkbox-option.et-checkbox-option:focus-visible .et-checkbox-option-box,
41
+ .et-checkbox-group-select-all.et-checkbox-group-select-all:focus-visible .et-checkbox-group-select-all-box {
42
+ border-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
43
+ outline-color: var(--et-theme-color-ink-solid);
44
+ }
45
+
46
+ et-checkbox-option[aria-checked='true']:not(:focus-visible) .et-checkbox-option-box,
47
+ .et-checkbox-group-select-all:is([aria-checked='true'], [aria-checked='mixed']):not(:focus-visible)
48
+ .et-checkbox-group-select-all-box {
49
+ outline: 1px solid light-dark(var(--et-theme-color-ink-solid), transparent);
50
+ outline-offset: -1px;
51
+ }
52
+
53
+ @media (hover: hover) {
54
+ et-checkbox:is([aria-checked='true'], [aria-checked='mixed']):hover,
55
+ .et-color-interactive-container:hover et-checkbox:is([aria-checked='true'], [aria-checked='mixed']),
56
+ et-checkbox-option[aria-checked='true']:hover,
57
+ .et-checkbox-group-select-all:is([aria-checked='true'], [aria-checked='mixed']):hover {
58
+ --et-theme-color-primary-rgb: var(--et-color-primary-active);
59
+ }
60
+ }
61
+
62
+ .et-label-required-marker {
63
+ color: var(--et-theme-color-ink-solid);
64
+ }
65
+ }
@@ -0,0 +1,180 @@
1
+ import {
2
+ checkbox,
3
+ checkboxGroup,
4
+ type CheckboxGroupMockOption,
5
+ type CheckboxGroupMockOptions,
6
+ type CheckboxMockForce,
7
+ type CheckboxMockOptions,
8
+ checkboxGroupStyles,
9
+ checkboxStyles,
10
+ } from '@ethlete/components/design/checkbox';
11
+ import { control } from '@ethlete/components/design/control';
12
+ import { definePart, drawing } from '@design-system';
13
+ import frame from './checkbox.css?inline';
14
+
15
+ type Props = ({ checkbox: CheckboxMockOptions } | { group: CheckboxGroupMockOptions }) &
16
+ Partial<CheckboxMockOptions & CheckboxGroupMockOptions>;
17
+
18
+ const LABEL = 'Send me the match report';
19
+
20
+ const ROWS = {
21
+ unchecked: {},
22
+ checked: { checked: true },
23
+ indeterminate: { indeterminate: true },
24
+ } satisfies Record<string, CheckboxMockOptions>;
25
+
26
+ const COLUMNS = ['rest', 'hover', 'focus-visible', 'active', 'disabled', 'readonly'] as const;
27
+
28
+ type Row = keyof typeof ROWS;
29
+ type Column = (typeof COLUMNS)[number];
30
+
31
+ const cellOf = (row: Row, column: Column): CheckboxMockOptions => ({
32
+ ...ROWS[row],
33
+ id: `${row}-${column}`,
34
+ label: LABEL,
35
+ ...(column === 'disabled' ? { disabled: true } : column === 'readonly' ? { readonly: true } : {}),
36
+ ...(column === 'rest' || column === 'disabled' || column === 'readonly'
37
+ ? {}
38
+ : { force: column satisfies CheckboxMockForce }),
39
+ });
40
+
41
+ const stateOf = (row: Row, column: Column) => (column === 'rest' ? row : `${row}-${column}`);
42
+
43
+ const gridStates = Object.fromEntries(
44
+ (Object.keys(ROWS) as Row[]).flatMap((row) =>
45
+ COLUMNS.map((column) => [stateOf(row, column), { checkbox: cellOf(row, column) }]),
46
+ ),
47
+ ) as Record<string, Props>;
48
+
49
+ const CARD: CheckboxMockOptions = {
50
+ id: 'card',
51
+ label: 'Weekly digest',
52
+ description: 'A summary of every match day, each Monday.',
53
+ variant: 'card',
54
+ };
55
+
56
+ const EVENTS = ['Kick-off', 'Goals', 'Half-time', 'Full-time'];
57
+
58
+ const eventsGroup = (
59
+ checked: number,
60
+ extra: { force?: CheckboxMockForce[]; selectAllForce?: CheckboxMockForce[] } = {},
61
+ ): CheckboxGroupMockOptions => ({
62
+ id: 'events',
63
+ label: 'Notify me about',
64
+ selectAll: 'All events',
65
+ selectAllForce: extra.selectAllForce,
66
+ options: EVENTS.map((label, index): CheckboxGroupMockOption => ({
67
+ label,
68
+ checked: index < checked,
69
+ force: index === 1 ? extra.force : undefined,
70
+ })),
71
+ });
72
+
73
+ const venueGroup: CheckboxGroupMockOptions = {
74
+ id: 'venue',
75
+ label: 'Show matches',
76
+ orientation: 'horizontal',
77
+ options: [{ label: 'Home', checked: true }, { label: 'Away', checked: true }, { label: 'Neutral ground' }],
78
+ };
79
+
80
+ const marked = (markup: string) =>
81
+ markup.replace(/<(et-checkbox|et-checkbox-option|et-checkbox-group-select-all)(?=[\s>])/gu, (_, tag: string) =>
82
+ control(
83
+ `<${tag}>`,
84
+ tag === 'et-checkbox-option' ? 'option' : tag === 'et-checkbox' ? 'checkbox' : 'select-all',
85
+ ).slice(0, -1),
86
+ );
87
+
88
+ const render = (props: Props) =>
89
+ 'group' in props
90
+ ? marked(checkboxGroup(props.group))
91
+ : marked(
92
+ checkbox(props.checkbox).replace(
93
+ 'class="et-choice-field-label-area',
94
+ 'data-et-skip="CLIPPED" class="et-choice-field-label-area',
95
+ ),
96
+ );
97
+
98
+ export default definePart({
99
+ note: "The SDK checkbox (et-checkbox) in flow, first drawing, drawn by the mock from @ethlete/components/design/checkbox with the real markup and CSS: et-checkbox alone and inside et-choice-field (label, et-description, hint, error, required marker), the card variant (et-selection-card), and et-checkbox-group with et-checkbox-group-select-all and et-checkbox-option, plus the color-interactive and focus-ring sheets; the brand theme, danger as the error theme. Fixtures are fictional: a match-report preferences form (Send me the match report, One email per match, no ads., hint We send it after the final whistle.; I accept the terms of use, required, error Accept the terms to continue.; card Weekly digest, A summary of every match day, each Monday.; group Notify me about with All events over Kick-off, Goals, Half-time, Full-time; horizontal group Show matches: Home, Away, Neutral ground). Framed as a page with 16px padding at 360 and 640. States: a grid of unchecked, checked and indeterminate by rest, hover, focus-visible, active, disabled and readonly (all labelled); box-only (checked, no label); support (description and hint); invalid and invalid-focus-visible; card-rest, card-checked, card-hover, card-focus-visible, card-checked-focus-visible; group-unchecked, group-partial (2 of 4, select-all mixed), group-checked, group-option-hover, group-option-pressed (force hover and active), group-option-focus-visible, group-select-all-focus-visible and group-horizontal. Numbers (light / dark), from shot pixels at scale 2 at 360, brand mint, a: unchecked box border (surface border) vs page 1.26 / 1.73, the same for the option and select-all boxes and the card edge; hover border 10.37 / 13.07; checked and indeterminate fill (primary) vs page 1.33 / 13.50, check and dash on it 15.82; checked hover fill (-hover) 1.37 / 13.06, 1.03 from rest; active fill 2.24 / 8.01; focus-visible fill (-focus) 1.37 / 13.06; focus ring on a checked or indeterminate box (primary -focus) 1.37 / 13.06, on an unchecked box (surface text colour) 17.93 / 17.18; card checked edge and card ring (primary) 1.33 / 13.50; option and select-all ring 1.37 / 13.06; invalid box border (danger) 4.83 / 3.71, its focus ring (danger -focus) 6.47 / 2.77; disabled checked fill 2.20 / 1.70, disabled unchecked border 1.12 / 1.27 (exempt under 1.4.11). Decided rules that arise: Q88 (the checked and indeterminate ring, the invalid ring, the card ring, the option and select-all rings in ink; the unchecked ring is the surface text colour, not a primary ring, so it stays), Q99a (the card checked and focus edge and the focused option, select-all and card box borders take light-dark(ink, primary); the checked and indeterminate box, the checked option box and the checked or mixed select-all box are mint fills, so they take a 1px inset ink edge, an outline at -1px as in the table round; on the option and select-all the focus ring owns the outline, so the edge leaves while it shows) and Q101 (a filled checked or indeterminate box hovers to -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 error border is the danger theme, not mint, and passes 3:1, so it is kept. Findings for later: (1) lib bug: .et-checkbox-box, .et-checkbox-option-box and .et-checkbox-group-select-all-box were content-box, so --et-checkbox-size 20px drew a 24px box (48 sheet px at scale 2) in an app without a global border-box reset, and the hint and error started 4px left of the label column. Fixed in the lib (box-sizing: border-box on the three boxes): every box draws 20px (40 sheet px), and the label, description and hint share one column (44 CSS px from the frame edge). (2) WCAG 1.4.11: the unchecked box border is 1.26 / 1.73 vs page, under 3:1; it is the only edge of an unchecked box (the same question as the switch off track, switch r1, open). Not drawn. (3) The required marker takes the error theme primary: 3.71:1 in dark, under 4.5:1; exempted here. (4) Focus-visible lightens the checked fill to -focus (1.37 vs page, light), so a focused checked box reads paler than at rest; the ink ring carries the state in b and c. (5) Readonly draws exactly like rest (no affordance by design); a checked readonly box cannot be told from an editable one. (6) Mock: et-description now has its own stylesheet (description.component.css, f5722a1f5), so the mock's inline copy can import it instead. Later rounds, left out here: the sizes sm and lg, the card with the control at start, a coloured (non-brand) checkbox, the group's bulk-edit mixed state (dashed option boxes, a primary mark that Q99a would reach), a disabled or readonly group, a group with error and the group card variant, long labels that wrap, RTL and the check and uncheck animation.",
100
+ madeBy: ['claude-opus-5-5'],
101
+ states: {
102
+ ...gridStates,
103
+ 'box-only': { checkbox: { id: 'box-only', checked: true } },
104
+ support: {
105
+ checkbox: {
106
+ id: 'support',
107
+ label: LABEL,
108
+ description: 'One email per match, no ads.',
109
+ hint: 'We send it after the final whistle.',
110
+ },
111
+ },
112
+ invalid: {
113
+ checkbox: {
114
+ id: 'invalid',
115
+ label: 'I accept the terms of use',
116
+ required: true,
117
+ error: 'Accept the terms to continue.',
118
+ errorColor: 'danger',
119
+ },
120
+ },
121
+ 'invalid-focus-visible': {
122
+ checkbox: {
123
+ id: 'invalid-focus-visible',
124
+ label: 'I accept the terms of use',
125
+ required: true,
126
+ error: 'Accept the terms to continue.',
127
+ errorColor: 'danger',
128
+ force: 'focus-visible',
129
+ },
130
+ },
131
+ 'card-rest': { checkbox: CARD },
132
+ 'card-checked': { checkbox: { ...CARD, checked: true } },
133
+ 'card-hover': { checkbox: { ...CARD, force: 'hover' } },
134
+ 'card-focus-visible': { checkbox: { ...CARD, force: 'focus-visible' } },
135
+ 'card-checked-focus-visible': { checkbox: { ...CARD, checked: true, force: 'focus-visible' } },
136
+ 'group-unchecked': { group: eventsGroup(0) },
137
+ 'group-partial': { group: eventsGroup(2) },
138
+ 'group-checked': { group: eventsGroup(4) },
139
+ 'group-option-hover': { group: eventsGroup(0, { force: ['hover'] }) },
140
+ 'group-option-pressed': { group: eventsGroup(0, { force: ['hover', 'active'] }) },
141
+ 'group-option-focus-visible': { group: eventsGroup(2, { force: ['focus-visible'] }) },
142
+ 'group-select-all-focus-visible': { group: eventsGroup(2, { selectAllForce: ['focus-visible'] }) },
143
+ 'group-horizontal': { group: venueGroup },
144
+ },
145
+ grid: {
146
+ columns: COLUMNS,
147
+ rows: Object.fromEntries(
148
+ (Object.keys(ROWS) as Row[]).map((row) => [
149
+ row,
150
+ Object.fromEntries(COLUMNS.map((column) => [column, stateOf(row, column)])),
151
+ ]),
152
+ ),
153
+ },
154
+ omits: [
155
+ { option: 'size', value: 'md', reason: 'The default size, which every state draws without setting it.' },
156
+ { option: 'variant', value: 'plain', reason: 'The default variant, which the grid draws without setting it.' },
157
+ { option: 'size', value: 'sm', reason: 'Not drawn in this round: sizes are a later round.' },
158
+ { option: 'size', value: 'lg', reason: 'Not drawn in this round: sizes are a later round.' },
159
+ { option: 'orientation', value: 'vertical', reason: 'Drawn as the group-* states, outside the grid.' },
160
+ { option: 'orientation', value: 'horizontal', reason: 'Drawn as the group-horizontal state, outside the grid.' },
161
+ { option: 'variant', value: 'card', reason: 'Drawn as the card-* states, outside the grid.' },
162
+ {
163
+ option: 'controlPosition',
164
+ value: 'start',
165
+ reason: 'Not drawn in this round: the card with the control at start is a later round.',
166
+ },
167
+ { option: 'controlPosition', value: 'end', reason: 'Drawn as the card-* states, outside the grid.' },
168
+ ],
169
+ controls: {
170
+ checkbox: { kind: 'button', input: ['pointer', 'keyboard'], disabled: true },
171
+ option: { kind: 'button', input: ['pointer', 'keyboard'], disabled: true },
172
+ 'select-all': { kind: 'button', input: ['pointer', 'keyboard'], disabled: true },
173
+ },
174
+ widths: [360, 640],
175
+ render: (props: Props) =>
176
+ drawing({
177
+ body: `<div class="t-frame">${render(props)}</div>`,
178
+ styles: [checkboxStyles, checkboxGroupStyles, frame].join('\n'),
179
+ }),
180
+ });
@@ -68,9 +68,10 @@ export default definePart({
68
68
  chip: { kind: 'button', input: ['pointer', 'keyboard'], disabled: true },
69
69
  remove: { kind: 'button', input: ['pointer', 'keyboard'], disabled: true },
70
70
  },
71
+ frame: (body) => `<div class="chip-frame">${body}</div>`,
71
72
  render: (props: Props) =>
72
73
  drawing({
73
- body: `<div class="chip-frame">${props.row ? chipRow(ROW.map((each) => draw(each))) : draw(props)}</div>`,
74
+ body: props.row ? chipRow(ROW.map((each) => draw(each))) : draw(props),
74
75
  styles: [chipStyles, frame].join('\n'),
75
76
  }),
76
77
  });
@@ -51,9 +51,10 @@ export default definePart({
51
51
  icon: { kind: 'button', input: ['pointer', 'keyboard'] },
52
52
  text: { kind: 'button', input: ['pointer', 'keyboard'] },
53
53
  },
54
+ frame: (body) => `<div class="copy-button-frame">${body}</div>`,
54
55
  render: (props: Props) =>
55
56
  drawing({
56
- body: `<div class="copy-button-frame">${render(props)}</div>`,
57
+ body: render(props),
57
58
  styles: [copyButtonStyles, frame].join('\n'),
58
59
  }),
59
60
  });
@@ -45,9 +45,10 @@ export default definePart({
45
45
  'grouped-stacked': { items: GROUPED, variant: 'stacked' },
46
46
  },
47
47
  widths: [480],
48
+ frame: (body) => `<div class="description-list-frame">${body}</div>`,
48
49
  render: ({ variant, items = STORY }: Props) =>
49
50
  drawing({
50
- body: `<div class="description-list-frame">${descriptionList({ items, variant })}</div>`,
51
+ body: descriptionList({ items, variant }),
51
52
  styles: [descriptionListStyles, frame].join('\n'),
52
53
  }),
53
54
  });
@@ -70,9 +70,10 @@ export default definePart({
70
70
  cancel: { kind: 'button', input: ['pointer', 'keyboard'] },
71
71
  confirm: { kind: 'button', input: ['pointer', 'keyboard'] },
72
72
  },
73
+ frame: (body) => `<div class="dialog-frame">${body}</div>`,
73
74
  render: (props: Props) =>
74
75
  drawing({
75
- body: `<div class="dialog-frame">${overlayBackdrop(render(props))}</div>`,
76
+ body: overlayBackdrop(render(props)),
76
77
  styles: [dialogStyles, edge].join('\n'),
77
78
  }),
78
79
  });
@@ -35,9 +35,10 @@ export default definePart({
35
35
  list: { set: 'list' },
36
36
  },
37
37
  widths: [480],
38
+ frame: (body) => `<div class="divider-frame">${body}</div>`,
38
39
  render: (props: Props) =>
39
40
  drawing({
40
- body: `<div class="divider-frame">${body(props)}</div>`,
41
+ body: body(props),
41
42
  styles: [dividerStyles, frame].join('\n'),
42
43
  }),
43
44
  });
@@ -48,9 +48,10 @@ export default definePart({
48
48
  controls: {
49
49
  action: { kind: 'button', input: ['pointer', 'keyboard'] },
50
50
  },
51
+ frame: (body) => `<div class="empty-state-frame">${body}</div>`,
51
52
  render: (props: Props) =>
52
53
  drawing({
53
- body: `<div class="empty-state-frame">${render(props)}</div>`,
54
+ body: render(props),
54
55
  styles: [emptyStateStyles, frame].join('\n'),
55
56
  }),
56
57
  });
@@ -68,13 +68,14 @@ export default definePart({
68
68
  'long-title': { layout: 'spans', actions: true, longTitle: true },
69
69
  },
70
70
  widths: [480],
71
+ frame: (body) => `<div class="t-frame">${body}</div>`,
71
72
  render: (props: Props) =>
72
73
  drawing({
73
- body: `<div class="t-frame">${grid({
74
+ body: grid({
74
75
  items: itemsOf(props),
75
76
  actions: props.actions,
76
77
  ghost: props.drag ? { col: 1, row: 1, colSpan: 1, rowSpan: 1 } : undefined,
77
- })}</div>`,
78
+ }),
78
79
  styles: [gridStyles, frame].join('\n'),
79
80
  }),
80
81
  });
@@ -71,9 +71,10 @@ export default definePart({
71
71
  'align-center': email({ value: VALUE, textAlign: 'center' }),
72
72
  },
73
73
  widths: [320, 360, 640],
74
+ frame: (body) => `<div class="t-frame">${body}</div>`,
74
75
  render: (props: Props) =>
75
76
  drawing({
76
- body: `<div class="t-frame">${textInput(props.field)}</div>`,
77
+ body: textInput(props.field),
77
78
  styles: [textInputStyles, frame].join('\n'),
78
79
  }),
79
80
  });
@@ -49,9 +49,10 @@ export default definePart({
49
49
  'shortcut-list-other': { form: 'shortcut-list', platform: 'other' },
50
50
  },
51
51
  widths: [480],
52
+ frame: (body) => `<div class="kbd-frame">${body}</div>`,
52
53
  render: (props: Props) =>
53
54
  drawing({
54
- body: `<div class="kbd-frame">${render(props)}</div>`,
55
+ body: render(props),
55
56
  styles: [kbdStyles, frame].join('\n'),
56
57
  }),
57
58
  });
@@ -60,9 +60,10 @@ export default definePart({
60
60
  },
61
61
  checks: { skip: ['CLIPPED'] },
62
62
  widths: [480],
63
+ frame: (body) => `<div class="loader-frame">${body}</div>`,
63
64
  render: (props: Props) =>
64
65
  drawing({
65
- body: `<div class="loader-frame">${render(props)}</div>`,
66
+ body: render(props),
66
67
  styles: [loaderStyles, frame].join('\n'),
67
68
  }),
68
69
  });
@@ -29,14 +29,15 @@ export default definePart({
29
29
  rtl: { items: 'rest', columnWidth: 130, dir: 'rtl' },
30
30
  },
31
31
  widths: [480],
32
+ frame: (body) => `<div class="t-frame">${body}</div>`,
32
33
  render: (props: Props) =>
33
34
  drawing({
34
- body: `<div class="t-frame">${masonry({
35
+ body: masonry({
35
36
  items: ITEMS[props.items],
36
37
  width: props.width,
37
38
  columnWidth: props.columnWidth,
38
39
  dir: props.dir,
39
- })}</div>`,
40
+ }),
40
41
  styles: [masonryStyles, frame].join('\n'),
41
42
  }),
42
43
  });
@@ -67,10 +67,11 @@ export default definePart({
67
67
  'focus-visible': { card: { ...finished, interactive: true, force: 'focus-visible' } },
68
68
  },
69
69
  widths: [280, 480, 720],
70
+ frame: (body) => `<div class="t-frame">${body}</div>`,
70
71
  render: (props: Props) =>
71
72
  drawing({
72
73
  // Lib bug: et-match-score clips its 25px digit box to 24px (overflow: hidden at line-height 1.2), reported.
73
- body: `<div class="t-frame">${matchCard(props.card)}</div>`,
74
+ body: matchCard(props.card),
74
75
  styles: [matchStyles, frame].join('\n'),
75
76
  }),
76
77
  });
@@ -1,4 +1,10 @@
1
- import { menu, type MenuMockRow, menuStyles } from '@ethlete/components/design/menu';
1
+ import {
2
+ menu,
3
+ type MenuMockItem,
4
+ type MenuMockRow,
5
+ type MenuMockSelectionItem,
6
+ menuStyles,
7
+ } from '@ethlete/components/design/menu';
2
8
  import { definePart, drawing, interactionRows } from '@design-system';
3
9
  import edge from './menu.css?inline';
4
10
 
@@ -11,9 +17,6 @@ type Props = {
11
17
 
12
18
  const MARK = 'data-et-control="item"';
13
19
 
14
- const force = (interaction?: ItemInteraction) =>
15
- [MARK, interaction && `data-et-force="${interaction}"`].filter(Boolean).join(' ');
16
-
17
20
  const row = (label: string, extra: Partial<Extract<MenuMockRow, { label: string }>> = {}) =>
18
21
  ({ label, attributes: MARK, ...extra }) as MenuMockRow;
19
22
 
@@ -45,10 +48,18 @@ const FULL: MenuMockRow[] = [
45
48
  row('Delete', { destructive: true }),
46
49
  ];
47
50
 
51
+ const isItemRow = (entry: MenuMockRow): entry is MenuMockItem | MenuMockSelectionItem =>
52
+ entry.kind === undefined || entry.kind === 'item' || entry.kind === 'checkbox' || entry.kind === 'radio';
53
+
48
54
  const withForced = (rows: MenuMockRow[], forced?: ItemInteraction): MenuMockRow[] =>
49
55
  rows.map((entry, index) =>
50
- index === 1 && 'label' in entry && entry.kind !== 'label'
51
- ? ({ ...entry, active: !!forced, attributes: force(forced) } as MenuMockRow)
56
+ forced && index === 1 && isItemRow(entry)
57
+ ? {
58
+ ...entry,
59
+ active: true,
60
+ activeSource: forced === 'focus-visible' ? 'keyboard' : entry.activeSource,
61
+ attributes: [entry.attributes, `data-et-force="${forced}"`].filter(Boolean).join(' '),
62
+ }
52
63
  : entry,
53
64
  );
54
65
 
@@ -83,9 +94,10 @@ export default definePart({
83
94
  controls: {
84
95
  item: { kind: 'row', input: ['pointer', 'keyboard'], disabled: true },
85
96
  },
97
+ frame: (body) => `<div class="menu-frame">${body}</div>`,
86
98
  render: ({ rows, forced }: Props) =>
87
99
  drawing({
88
- body: `<div class="menu-frame">${menu({ rows: withForced(rows, forced), color: 'brand' })}</div>`,
100
+ body: menu({ rows: withForced(rows, forced), color: 'brand' }),
89
101
  styles: [menuStyles, edge].join('\n'),
90
102
  }),
91
103
  });
@@ -98,9 +98,10 @@ export default definePart({
98
98
  action: { kind: 'button', input: ['pointer', 'keyboard'] },
99
99
  secondary: { kind: 'button', input: ['pointer', 'keyboard'] },
100
100
  },
101
+ frame: (body) => `<div class="notification-frame">${body}</div>`,
101
102
  render: (props: Props) =>
102
103
  drawing({
103
- body: `<div class="notification-frame">${render(props)}</div>`,
104
+ body: render(props),
104
105
  styles: [notificationStyles, edge].join('\n'),
105
106
  }),
106
107
  });
@@ -56,9 +56,10 @@ export default definePart({
56
56
  ],
57
57
  },
58
58
  widths: [320, 360, 640],
59
+ frame: (body) => `<div class="t-frame">${body}</div>`,
59
60
  render: (props: Props) =>
60
61
  drawing({
61
- body: `<div class="t-frame">${otpInput(props.field)}</div>`,
62
+ body: otpInput(props.field),
62
63
  styles: [otpInputStyles, frame].join('\n'),
63
64
  }),
64
65
  });
@@ -67,9 +67,10 @@ export default definePart({
67
67
  controls: {
68
68
  item: { kind: 'button', input: ['pointer', 'keyboard'], disabled: true },
69
69
  },
70
+ frame: (body) => `<div class="pagination-frame">${body}</div>`,
70
71
  render: (props: Props) =>
71
72
  drawing({
72
- body: `<div class="pagination-frame">${pagination({ ...props, items: items(props) })}</div>`,
73
+ body: pagination({ ...props, items: items(props) }),
73
74
  styles: [paginationStyles, edge].join('\n'),
74
75
  }),
75
76
  });
@@ -88,9 +88,10 @@ export default definePart({
88
88
  'error-bare': { error: 'bare' },
89
89
  },
90
90
  widths: [480],
91
+ frame: (body) => `<div class="picture-frame">${body}</div>`,
91
92
  render: (props: Props) =>
92
93
  drawing({
93
- body: `<div class="picture-frame">${body(props)}</div>`,
94
+ body: body(props),
94
95
  styles: [skeletonStyles, pictureStyles, frame].join('\n'),
95
96
  }),
96
97
  });
@@ -83,9 +83,10 @@ export default definePart({
83
83
  complete: { kind: 'button', input: ['pointer', 'keyboard'] },
84
84
  upcoming: { kind: 'button', input: ['pointer', 'keyboard'] },
85
85
  },
86
+ frame: (body) => `<div class="progress-steps-frame">${body}</div>`,
86
87
  render: (props: Props) =>
87
88
  drawing({
88
- body: `<div class="progress-steps-frame">${steps(props)}</div>`,
89
+ body: steps(props),
89
90
  styles: [progressStepsStyles, frame].join('\n'),
90
91
  }),
91
92
  });
@@ -112,9 +112,10 @@ export default definePart({
112
112
  controls: {
113
113
  retry: { kind: 'button', input: ['pointer', 'keyboard'] },
114
114
  },
115
+ frame: (body) => `<div class="query-error-frame">${body}</div>`,
115
116
  render: (props: Props) =>
116
117
  drawing({
117
- body: `<div class="query-error-frame">${render(props)}</div>`,
118
+ body: render(props),
118
119
  styles: [queryErrorStyles, frame].join('\n'),
119
120
  }),
120
121
  });
@@ -0,0 +1,46 @@
1
+ .t-frame {
2
+ box-sizing: border-box;
3
+ display: grid;
4
+ justify-items: start;
5
+ gap: 1.6rem;
6
+ padding: 1.6rem;
7
+ background: var(--et-surface-background-solid);
8
+ color: var(--et-surface-color-solid);
9
+ font-size: 1.4rem;
10
+ }
11
+
12
+ @layer components {
13
+ et-radio:focus-visible .et-radio-circle {
14
+ border-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
15
+ outline-color: var(--et-theme-color-ink-solid);
16
+ }
17
+
18
+ et-radio[aria-checked='true'] .et-radio-circle,
19
+ et-radio-group[data-mixed] .et-radio-circle {
20
+ border-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
21
+ }
22
+
23
+ et-radio[aria-checked='true'] .et-radio-circle::after {
24
+ outline: 1px solid light-dark(var(--et-theme-color-ink-solid), transparent);
25
+ outline-offset: -1px;
26
+ }
27
+
28
+ et-radio.et-selection-card:where([aria-checked='true']) {
29
+ border-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
30
+ }
31
+
32
+ et-radio.et-selection-card:where(:focus-visible) {
33
+ border-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
34
+ outline-color: var(--et-theme-color-ink-solid);
35
+ }
36
+
37
+ @media (hover: hover) {
38
+ et-radio[aria-checked='true']:hover {
39
+ --et-theme-color-primary-rgb: var(--et-color-primary-active);
40
+ }
41
+ }
42
+
43
+ .et-label-required-marker {
44
+ color: var(--et-theme-color-ink-solid);
45
+ }
46
+ }