@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.
- package/CHANGELOG.md +41 -0
- package/design/anchored-dialog.ts +98 -0
- package/design/button.ts +13 -1
- package/design/config.json +6 -1
- package/design/dialog.ts +5 -3
- package/design/form-field.ts +3 -0
- package/design/menu.ts +8 -4
- package/design/notification.ts +8 -4
- package/design/sdk/components/accordion/accordion.ts +5 -1
- package/design/sdk/components/anchored-dialog/anchored-dialog.css +63 -0
- package/design/sdk/components/anchored-dialog/anchored-dialog.ts +75 -0
- package/design/sdk/components/avatar/avatar.ts +3 -2
- package/design/sdk/components/badge/badge.ts +2 -1
- package/design/sdk/components/banner/banner.ts +2 -1
- package/design/sdk/components/bracket/bracket.ts +2 -1
- package/design/sdk/components/breadcrumb/breadcrumb.ts +2 -1
- package/design/sdk/components/button/button.css +11 -3
- package/design/sdk/components/button/button.ts +17 -4
- package/design/sdk/components/calendar/calendar.ts +2 -1
- package/design/sdk/components/card/card.ts +2 -1
- package/design/sdk/components/carousel/carousel.ts +2 -1
- package/design/sdk/components/chart/chart.ts +2 -1
- package/design/sdk/components/checkbox/checkbox.css +65 -0
- package/design/sdk/components/checkbox/checkbox.ts +180 -0
- package/design/sdk/components/chip/chip.ts +2 -1
- package/design/sdk/components/copy-button/copy-button.ts +2 -1
- package/design/sdk/components/description-list/description-list.ts +2 -1
- package/design/sdk/components/dialog/dialog.ts +2 -1
- package/design/sdk/components/divider/divider.ts +2 -1
- package/design/sdk/components/empty-state/empty-state.ts +2 -1
- package/design/sdk/components/grid/grid.ts +3 -2
- package/design/sdk/components/input/input.ts +2 -1
- package/design/sdk/components/kbd/kbd.ts +2 -1
- package/design/sdk/components/loader/loader.ts +2 -1
- package/design/sdk/components/masonry/masonry.ts +3 -2
- package/design/sdk/components/match/match.ts +2 -1
- package/design/sdk/components/menu/menu.ts +19 -7
- package/design/sdk/components/notification/notification.ts +2 -1
- package/design/sdk/components/otp-input/otp-input.ts +2 -1
- package/design/sdk/components/pagination/pagination.ts +2 -1
- package/design/sdk/components/picture/picture.ts +2 -1
- package/design/sdk/components/progress-steps/progress-steps.ts +2 -1
- package/design/sdk/components/query-error/query-error.ts +2 -1
- package/design/sdk/components/radio-group/radio-group.css +46 -0
- package/design/sdk/components/radio-group/radio-group.ts +167 -0
- package/design/sdk/components/scheduler/scheduler.ts +2 -1
- package/design/sdk/components/scrollable/scrollable.ts +2 -2
- package/design/sdk/components/scrollbar/scrollbar.ts +2 -1
- package/design/sdk/components/select/shared/select-part.ts +11 -2
- package/design/sdk/components/select-panel/select-panel.ts +2 -1
- package/design/sdk/components/skeleton/skeleton.ts +2 -1
- package/design/sdk/components/slider/slider.ts +2 -1
- package/design/sdk/components/standings/standings.ts +2 -1
- package/design/sdk/components/stat-tile/stat-tile.ts +2 -1
- package/design/sdk/components/stream/stream.ts +6 -1
- package/design/sdk/components/switch/switch.css +4 -2
- package/design/sdk/components/switch/switch.ts +2 -2
- package/design/sdk/components/table/table.ts +2 -1
- package/design/sdk/components/textarea/textarea.ts +4 -3
- package/design/sdk/components/timeline/timeline.ts +2 -2
- package/design/sdk/components/toolbar/toolbar.ts +2 -1
- package/design/sdk/foundations/colors/colors.css +16 -0
- package/design/sdk/foundations/colors/colors.ts +1 -1
- package/design/sdk/foundations/colors/shared/contrast.ts +1 -1
- package/design/sdk/lint-baseline.json +6 -0
- package/design/sdk/pictures.json +1195 -0
- package/design/sdk/ready.json +64 -0
- package/design/sdk/rule-baseline.json +104 -0
- package/design/select.ts +9 -5
- package/design/textarea.ts +2 -0
- package/design/tooltip.ts +5 -5
- package/fesm2022/ethlete-components.mjs +1190 -973
- package/fesm2022/ethlete-components.mjs.map +1 -1
- package/generators/generators.json +2 -0
- package/package.json +1 -1
- package/src/lib/button/button-properties-styles.component.css +6 -6
- package/src/lib/button/button.component.css +11 -7
- package/src/lib/button/button.types.ts +33 -0
- package/src/lib/button/icon-button.component.css +3 -3
- package/src/lib/button/window-control-button.component.css +70 -3
- package/src/lib/calendar/calendar.component.css +2 -2
- package/src/lib/chip/chip.component.css +4 -4
- package/src/lib/focus-ring/focus-ring-styles.component.css +20 -5
- package/src/lib/forms/switch/switch.component.css +1 -14
- package/src/lib/kbd/kbd.component.css +1 -1
- package/src/lib/menu/menu.component.css +2 -2
- package/src/lib/notification/notification.component.css +3 -3
- package/types/ethlete-components.d.ts +171 -139
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.0-next.81
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Anchored overlays take `arrowAnchor`, an element the arrow points at instead of the origin; the `anchoredDialog()` design mock takes a matching `anchor` selector.
|
|
8
|
+
- The focus ring takes public `--et-focus-ring-*` tokens; `design check` stops flagging base parts and spinner clipping.
|
|
9
|
+
- Breaking: the `et-progress-step` input `state` is now `stepState`, so it no longer collides with the `state` input of `RouterLink`.
|
|
10
|
+
- The window control button takes `shape: 'caption'`, a full-height 46px Windows caption tile with a 10px glyph.
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- Name the scheduler appointment buttons with `aria-label` (the appointment title) in the month and time grid views. The `title` attribute stays. The scheduler test driver now selects appointments by `aria-label`.
|
|
15
|
+
- The select design mock takes an `ariaLabel` and names its trigger like `et-select` does.
|
|
16
|
+
- `etTooltip` now opens on touch devices with a long press. A normal tap still does only the trigger's own action. The tooltip hides 1.5s after the finger lifts.
|
|
17
|
+
|
|
18
|
+
## 1.0.0-next.80
|
|
19
|
+
|
|
20
|
+
### Minor Changes
|
|
21
|
+
|
|
22
|
+
- Add a `stretchContents` input to `et-button`: the label box fills the button beside the icon, so a wide button can push a trailing element to its far edge.
|
|
23
|
+
- Add the `anchoredDialog()` design renderer, and draw the dialog, menu, select panel and tooltip panes of the design renderers with `data-et-auto-surface` when no `surface` is passed.
|
|
24
|
+
- Add the `--et-kbd-color` custom property to `et-kbd` to set the key text colour.
|
|
25
|
+
- Add a `shape` input to the window control button: `round` draws a GNOME (Adwaita) style circle next to the default `square` tile.
|
|
26
|
+
|
|
27
|
+
### Patch Changes
|
|
28
|
+
|
|
29
|
+
- The off switch track draws no edge and a 38% surface-interaction fill, and keeps that look on hover.
|
|
30
|
+
- The pressed filled button now darkens only slightly, so it keeps clear contrast with its label, and a loading or disabled button no longer lights up while pressed.
|
|
31
|
+
- The button design mock draws the busy state with its spinner when `loading` is set.
|
|
32
|
+
- The `tooltip()` design renderer keeps `et-surface--inherited` on the inner `et-tooltip`, like the real component, and puts `data-et-auto-surface` on the pane only.
|
|
33
|
+
- The pressed filled icon button, and so the split button trigger, darkens less and keeps its disabled colour when inactive.
|
|
34
|
+
- The notification takes `--et-font-family` when it is set, like the menu and the tooltip, and the notification design mock draws its surface one elevation above its parent when no `surface` is given, as the component forces elevation 1.
|
|
35
|
+
- Table: the stamped feature cells inject their feature through a token, so no table file imports itself in a cycle.
|
|
36
|
+
- The textarea design mock takes an `ariaLabel` for a field without a visible label.
|
|
37
|
+
- Button, icon button, chip and calendar nav buttons no longer change their background on hover or press while disabled.
|
|
38
|
+
- The `menu()` design mock takes `activeSource` on a row and draws it as `data-active-source`, so a forced focus-visible row with `activeSource: 'keyboard'` shows the keyboard highlight the real menu paints.
|
|
39
|
+
- The notification dismiss icon now meets the 3:1 non-text contrast minimum at rest.
|
|
40
|
+
- `et-select` and `et-menu` show the option focus ring only during keyboard use; a click open or a mouse move shows none.
|
|
41
|
+
- Closing a query-param overlay no longer cancels a pending navigation that already drops its query param, such as a `routerLink` inside the overlay.
|
|
42
|
+
- Escape that hides an `etTooltip` now also reaches the trigger's own key handlers; a dialog beneath it still stays open.
|
|
43
|
+
|
|
3
44
|
## 1.0.0-next.79
|
|
4
45
|
|
|
5
46
|
### Minor Changes
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Placement } from '@floating-ui/dom';
|
|
2
|
+
import overlayContainer from '../src/lib/overlay/overlay-container.component.css?inline';
|
|
3
|
+
import overlayMain from '../src/lib/overlay/overlay-main-styles.component.css?inline';
|
|
4
|
+
import anchoredDialogStrategy from '../src/lib/overlay/strategies/anchored-dialog-styles.component.css?inline';
|
|
5
|
+
import overlayArrow from '../src/lib/overlay/strategies/overlay-arrow-styles.component.css?inline';
|
|
6
|
+
|
|
7
|
+
export type AnchoredDialogMockOptions = {
|
|
8
|
+
/** The pane's content, as HTML. */
|
|
9
|
+
content: string;
|
|
10
|
+
/** Draws the `et-overlay-arrow` pointing back at the trigger. Defaults to `true`, like `anchoredDialogOverlayStrategy`. */
|
|
11
|
+
arrow?: boolean;
|
|
12
|
+
/** The side the pane opened on, which places the arrow. Defaults to `bottom-end`, the strategy's default. */
|
|
13
|
+
placement?: Placement;
|
|
14
|
+
/**
|
|
15
|
+
* A CSS selector of the drawn element the arrow points at, like `arrowAnchor` of the strategy. The design frame aims
|
|
16
|
+
* the arrow at its centre, and the render check reports a tip that misses it.
|
|
17
|
+
*/
|
|
18
|
+
anchor?: string;
|
|
19
|
+
/** The pane's role. Defaults to `dialog`. */
|
|
20
|
+
role?: 'dialog' | 'alertdialog';
|
|
21
|
+
/** Draws `aria-modal="true"`. Defaults to `true`, as for a modal overlay; `false` for `mode: 'non-modal'`. */
|
|
22
|
+
modal?: boolean;
|
|
23
|
+
/** The id of the element that labels the pane. */
|
|
24
|
+
labelledBy?: string;
|
|
25
|
+
/** An `aria-label` for a pane without a visible title. */
|
|
26
|
+
label?: string;
|
|
27
|
+
/** A surface theme name the app registers. Left out, the pane takes the surface one elevation above its parent. */
|
|
28
|
+
surface?: string;
|
|
29
|
+
/** Extra attributes on the pane, e.g. design markers. */
|
|
30
|
+
attributes?: string;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** The stylesheets `anchoredDialog()` draws with: the real overlay pane, anchored-dialog strategy and arrow CSS. */
|
|
34
|
+
export const anchoredDialogStyles = /* @__PURE__ */ [
|
|
35
|
+
overlayContainer,
|
|
36
|
+
overlayMain,
|
|
37
|
+
anchoredDialogStrategy,
|
|
38
|
+
overlayArrow,
|
|
39
|
+
`@layer components {
|
|
40
|
+
.et-anchored-dialog-mock {
|
|
41
|
+
--_et-overlay-arrow-pane-border-width: var(--et-overlay-surface-border-width);
|
|
42
|
+
inline-size: max-content;
|
|
43
|
+
min-inline-size: 288px;
|
|
44
|
+
max-inline-size: 80vw;
|
|
45
|
+
}
|
|
46
|
+
.et-anchored-dialog-mock:is([data-overlay-placement^='top'], [data-overlay-placement^='bottom']) > .et-overlay-arrow {
|
|
47
|
+
left: calc(50% - 10px);
|
|
48
|
+
}
|
|
49
|
+
.et-anchored-dialog-mock:is([data-overlay-placement^='left'], [data-overlay-placement^='right']) > .et-overlay-arrow {
|
|
50
|
+
top: calc(50% - 10px);
|
|
51
|
+
}
|
|
52
|
+
.et-anchored-dialog-mock:is([data-overlay-placement$='-start']):is([data-overlay-placement^='top'], [data-overlay-placement^='bottom']) > .et-overlay-arrow {
|
|
53
|
+
left: 16px;
|
|
54
|
+
}
|
|
55
|
+
.et-anchored-dialog-mock:is([data-overlay-placement$='-end']):is([data-overlay-placement^='top'], [data-overlay-placement^='bottom']) > .et-overlay-arrow {
|
|
56
|
+
left: calc(100% - 36px);
|
|
57
|
+
}
|
|
58
|
+
.et-anchored-dialog-mock:is([data-overlay-placement$='-start']):is([data-overlay-placement^='left'], [data-overlay-placement^='right']) > .et-overlay-arrow {
|
|
59
|
+
top: 16px;
|
|
60
|
+
}
|
|
61
|
+
.et-anchored-dialog-mock:is([data-overlay-placement$='-end']):is([data-overlay-placement^='left'], [data-overlay-placement^='right']) > .et-overlay-arrow {
|
|
62
|
+
top: calc(100% - 36px);
|
|
63
|
+
}
|
|
64
|
+
}`,
|
|
65
|
+
].join('\n');
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Draws an open anchored dialog pane (`anchoredDialogOverlayStrategy`) around `content`, as HTML. The pane
|
|
69
|
+
* sits in flow instead of where the overlay runtime would position it, so place it next to the drawn trigger.
|
|
70
|
+
*/
|
|
71
|
+
export const anchoredDialog = (options: AnchoredDialogMockOptions) => {
|
|
72
|
+
const { placement = 'bottom-end', arrow = true, role = 'dialog', modal = true } = options;
|
|
73
|
+
const classes = [
|
|
74
|
+
'et-overlay',
|
|
75
|
+
'et-overlay-runtime-pane',
|
|
76
|
+
'et-overlay--anchored-dialog',
|
|
77
|
+
'et-anchored-dialog-mock',
|
|
78
|
+
'et-color--inherited',
|
|
79
|
+
options.surface && `et-surface--${options.surface}`,
|
|
80
|
+
'et-with-default-animation',
|
|
81
|
+
'et-animation-enter-done',
|
|
82
|
+
].filter(Boolean);
|
|
83
|
+
const attributes = [
|
|
84
|
+
options.attributes,
|
|
85
|
+
!options.surface && 'data-et-auto-surface',
|
|
86
|
+
`role="${role}"`,
|
|
87
|
+
modal && 'aria-modal="true"',
|
|
88
|
+
'tabindex="-1"',
|
|
89
|
+
options.labelledBy && `aria-labelledby="${options.labelledBy}"`,
|
|
90
|
+
options.label && `aria-label="${options.label}"`,
|
|
91
|
+
`data-overlay-placement="${placement}"`,
|
|
92
|
+
options.anchor && `data-et-arrow-anchor="${options.anchor.replaceAll('"', '"')}"`,
|
|
93
|
+
`class="${classes.join(' ')}"`,
|
|
94
|
+
].filter(Boolean);
|
|
95
|
+
const arrowMarkup = arrow ? '<div class="et-overlay-arrow" et-floating-arrow></div>' : '';
|
|
96
|
+
|
|
97
|
+
return `<et-overlay-container ${attributes.join(' ')}><div>${options.content}</div>${arrowMarkup}</et-overlay-container>`;
|
|
98
|
+
};
|
package/design/button.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { interactiveStyles } from '@ethlete/core/design/interactive-styles';
|
|
|
2
2
|
import focusRing from '../src/lib/focus-ring/focus-ring-styles.component.css?inline';
|
|
3
3
|
import buttonProperties from '../src/lib/button/button-properties-styles.component.css?inline';
|
|
4
4
|
import buttonBase from '../src/lib/button/button.component.css?inline';
|
|
5
|
+
import { BUTTON_SPINNER_CONFIG } from '../src/lib/button/button.types';
|
|
5
6
|
import iconButtonBase from '../src/lib/button/icon-button.component.css?inline';
|
|
7
|
+
import { spinner } from './loader';
|
|
6
8
|
|
|
7
9
|
export type ButtonMockOptions = {
|
|
8
10
|
/** The label, as HTML. */
|
|
@@ -10,6 +12,8 @@ export type ButtonMockOptions = {
|
|
|
10
12
|
/** The icon, as HTML. Leave it out for a button without one. */
|
|
11
13
|
icon?: string;
|
|
12
14
|
iconAlignment?: 'start' | 'end';
|
|
15
|
+
/** Lets the label fill the space beside the icon, like `stretchContents` on `et-button`. */
|
|
16
|
+
stretchContents?: boolean;
|
|
13
17
|
size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
14
18
|
variant?: 'filled' | 'outline' | 'tonal' | 'transparent';
|
|
15
19
|
/** A color theme name the app registers. Left out, the button takes the color theme around it. */
|
|
@@ -17,6 +21,8 @@ export type ButtonMockOptions = {
|
|
|
17
21
|
/** Makes the button a toggle: `false` draws it unpressed, left out it is no toggle. */
|
|
18
22
|
pressed?: boolean;
|
|
19
23
|
disabled?: boolean;
|
|
24
|
+
/** Draws the busy button with its spinner, like `loading` on `et-button`. Needs `loaderStyles` from the loader mock. */
|
|
25
|
+
loading?: boolean;
|
|
20
26
|
tag?: 'button' | 'a';
|
|
21
27
|
class?: string;
|
|
22
28
|
style?: string;
|
|
@@ -47,15 +53,21 @@ export const button = (options: ButtonMockOptions) => {
|
|
|
47
53
|
options.pressed && 'data-pressed="true"',
|
|
48
54
|
options.pressed !== undefined && `aria-pressed="${options.pressed}"`,
|
|
49
55
|
options.disabled && (tag === 'button' ? 'disabled aria-disabled="true"' : 'aria-disabled="true"'),
|
|
56
|
+
options.loading && `data-loading="true" aria-busy="true"${options.disabled ? '' : ' aria-disabled="true"'}`,
|
|
50
57
|
`data-icon-alignment="${iconAlignment}"`,
|
|
51
58
|
`data-variant="${variant}"`,
|
|
52
59
|
`data-size="${size}"`,
|
|
60
|
+
options.stretchContents && 'data-stretch-contents="true"',
|
|
53
61
|
'data-can-animate="true"',
|
|
54
62
|
options.pressed && `data-pressed-variant="${PRESSED_VARIANT[variant]}"`,
|
|
55
63
|
options.style && `style="${options.style}"`,
|
|
56
64
|
].filter(Boolean);
|
|
57
65
|
|
|
58
|
-
|
|
66
|
+
const loader = options.loading
|
|
67
|
+
? `<div class="et-button-loader" aria-hidden="true">${spinner(BUTTON_SPINNER_CONFIG[size]).replace('class="et-spinner', 'class="et-button-loader-spinner et-spinner')}</div>`
|
|
68
|
+
: '';
|
|
69
|
+
|
|
70
|
+
return `<${tag} ${attributes.join(' ')}>${iconAlignment === 'start' ? iconBox : ''}<div class="et-button-contents">${label}</div>${iconAlignment === 'end' ? iconBox : ''}${loader}</${tag}>`;
|
|
59
71
|
};
|
|
60
72
|
|
|
61
73
|
export type IconButtonMockOptions = Omit<ButtonMockOptions, 'label' | 'icon' | 'iconAlignment'> & {
|
package/design/config.json
CHANGED
|
@@ -16,7 +16,12 @@
|
|
|
16
16
|
},
|
|
17
17
|
"generatedFiles": [
|
|
18
18
|
"foundations/colors/colors.css"
|
|
19
|
-
]
|
|
19
|
+
],
|
|
20
|
+
"harness": {
|
|
21
|
+
"app": "apps/docs-examples",
|
|
22
|
+
"map": "apps/docs-examples/src/app/design-harness/design-harness-parts.ts",
|
|
23
|
+
"baseBranch": "origin/next"
|
|
24
|
+
}
|
|
20
25
|
}
|
|
21
26
|
}
|
|
22
27
|
}
|
package/design/dialog.ts
CHANGED
|
@@ -28,7 +28,7 @@ export type DialogMockOptions = {
|
|
|
28
28
|
close?: boolean | string;
|
|
29
29
|
/** As the body's `dividers` input. `dynamic` draws the body scrolled to its start. */
|
|
30
30
|
dividers?: false | 'static' | 'dynamic';
|
|
31
|
-
/** A surface theme name the app registers, for the pane. */
|
|
31
|
+
/** A surface theme name the app registers, for the pane. Left out, the pane takes the surface one elevation above its parent. */
|
|
32
32
|
surface?: string;
|
|
33
33
|
/** Extra attributes on the pane, e.g. design markers. */
|
|
34
34
|
attributes?: string;
|
|
@@ -49,6 +49,7 @@ export type AlertDialogMockOptions = {
|
|
|
49
49
|
confirmAttributes?: string;
|
|
50
50
|
/** Extra attributes on the cancel button. */
|
|
51
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. */
|
|
52
53
|
surface?: string;
|
|
53
54
|
attributes?: string;
|
|
54
55
|
id?: string;
|
|
@@ -121,12 +122,13 @@ const pane = (
|
|
|
121
122
|
'et-overlay-runtime-pane',
|
|
122
123
|
'et-overlay--dialog',
|
|
123
124
|
'et-color--inherited',
|
|
124
|
-
`et-surface--${options.surface
|
|
125
|
+
options.surface && `et-surface--${options.surface}`,
|
|
125
126
|
'et-with-default-animation',
|
|
126
127
|
'et-animation-enter-done',
|
|
127
|
-
];
|
|
128
|
+
].filter(Boolean);
|
|
128
129
|
const attributes = [
|
|
129
130
|
options.attributes,
|
|
131
|
+
!options.surface && 'data-et-auto-surface',
|
|
130
132
|
`role="${options.role}"`,
|
|
131
133
|
'aria-modal="true"',
|
|
132
134
|
'tabindex="-1"',
|
package/design/form-field.ts
CHANGED
|
@@ -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/menu.ts
CHANGED
|
@@ -13,6 +13,8 @@ type MenuMockRowState = {
|
|
|
13
13
|
disabled?: boolean;
|
|
14
14
|
/** The menu's active item: the keyboard or the pointer points at it. */
|
|
15
15
|
active?: boolean;
|
|
16
|
+
/** What made the row active. `keyboard` draws the keyboard highlight on a focus-visible row. */
|
|
17
|
+
activeSource?: 'keyboard' | 'pointer';
|
|
16
18
|
/** Extra attributes on the row's element, e.g. design markers. */
|
|
17
19
|
attributes?: string;
|
|
18
20
|
};
|
|
@@ -48,7 +50,7 @@ export type MenuMockOptions = {
|
|
|
48
50
|
id?: string;
|
|
49
51
|
/** The app's default color theme name, as the menu syncs it from its trigger. */
|
|
50
52
|
color?: string;
|
|
51
|
-
/** A surface theme name the app registers, for the panel. */
|
|
53
|
+
/** A surface theme name the app registers, for the panel. Left out, the panel takes the surface one elevation above its parent. */
|
|
52
54
|
surface?: string;
|
|
53
55
|
};
|
|
54
56
|
|
|
@@ -69,6 +71,7 @@ const rowAttributes = (row: MenuMockRowState) =>
|
|
|
69
71
|
`tabindex="${row.active && !row.disabled ? 0 : -1}"`,
|
|
70
72
|
row.disabled && 'aria-disabled="true"',
|
|
71
73
|
row.active && 'data-active="true"',
|
|
74
|
+
row.active && row.activeSource && `data-active-source="${row.activeSource}"`,
|
|
72
75
|
].filter(Boolean);
|
|
73
76
|
|
|
74
77
|
const menuItem = (row: MenuMockItem, submenuId: string) => {
|
|
@@ -136,8 +139,9 @@ export const menu = (options: MenuMockOptions) => {
|
|
|
136
139
|
const classes = [
|
|
137
140
|
'et-menu',
|
|
138
141
|
`et-color--${options.color ?? 'inherited'}`,
|
|
139
|
-
`et-surface--${options.surface
|
|
140
|
-
];
|
|
142
|
+
options.surface && `et-surface--${options.surface}`,
|
|
143
|
+
].filter(Boolean);
|
|
144
|
+
const autoSurface = options.surface ? '' : ' data-et-auto-surface';
|
|
141
145
|
|
|
142
|
-
return `<et-menu id="et-menu-panel-${id}" class="${classes.join(' ')}" role="menu" aria-orientation="vertical" tabindex="-1"><div class="et-menu-header"><div class="et-menu-search-field"></div></div><div class="et-menu-body-wrapper"><div class="et-menu-body"><div class="et-menu-body-content">${options.rows.map(rowMarkup).join('')}</div></div></div></et-menu>`;
|
|
146
|
+
return `<et-menu id="et-menu-panel-${id}"${autoSurface} class="${classes.join(' ')}" role="menu" aria-orientation="vertical" tabindex="-1"><div class="et-menu-header"><div class="et-menu-search-field"></div></div><div class="et-menu-body-wrapper"><div class="et-menu-body"><div class="et-menu-body-content">${options.rows.map(rowMarkup).join('')}</div></div></div></et-menu>`;
|
|
143
147
|
};
|
package/design/notification.ts
CHANGED
|
@@ -29,7 +29,10 @@ export type NotificationMockOptions = {
|
|
|
29
29
|
* its type). Left out, the notification takes the color theme around it.
|
|
30
30
|
*/
|
|
31
31
|
color?: string;
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* A surface theme name the app registers. Left out, the notification takes the surface one elevation above its
|
|
34
|
+
* parent, as the component forces elevation 1 on the page.
|
|
35
|
+
*/
|
|
33
36
|
surface?: string;
|
|
34
37
|
/** Extra attributes on the dismiss button, e.g. design markers. */
|
|
35
38
|
dismissAttributes?: string;
|
|
@@ -117,11 +120,12 @@ export const notification = (options: NotificationMockOptions) => {
|
|
|
117
120
|
const classes = [
|
|
118
121
|
'et-notification',
|
|
119
122
|
`et-color--${options.color ?? 'inherited'}`,
|
|
120
|
-
`et-surface--${options.surface
|
|
123
|
+
options.surface && `et-surface--${options.surface}`,
|
|
121
124
|
'et-animation-enter-done',
|
|
122
|
-
];
|
|
125
|
+
].filter(Boolean);
|
|
126
|
+
const autoSurface = options.surface ? '' : ' data-et-auto-surface';
|
|
123
127
|
|
|
124
|
-
return `<et-notification ${options.attributes ?? ''} class="${classes.join(' ')}"><div class="et-notification-content">${header}${message}${footer}</div></et-notification>`;
|
|
128
|
+
return `<et-notification ${options.attributes ?? ''}${autoSurface} class="${classes.join(' ')}"><div class="et-notification-content">${header}${message}${footer}</div></et-notification>`;
|
|
125
129
|
};
|
|
126
130
|
|
|
127
131
|
/** Draws notifications in the stack's column and gap, in flow instead of fixed to the viewport. */
|
|
@@ -56,9 +56,13 @@ export default definePart({
|
|
|
56
56
|
shipping: { kind: 'button', input: ['pointer', 'keyboard'] },
|
|
57
57
|
archive: { kind: 'button', input: ['pointer', 'keyboard'] },
|
|
58
58
|
},
|
|
59
|
+
frame: (body) => `<div class="accordion-frame">${body}</div>`,
|
|
59
60
|
render: (props: Props) =>
|
|
60
61
|
drawing({
|
|
61
|
-
body:
|
|
62
|
+
body: accordionGroup(items(props)).replaceAll(
|
|
63
|
+
'inert=""><div class="et-accordion-panel-clip"',
|
|
64
|
+
'inert=""><div data-et-skip="CLIPPED" class="et-accordion-panel-clip"',
|
|
65
|
+
),
|
|
62
66
|
styles: [accordionStyles, frame].join('\n'),
|
|
63
67
|
}),
|
|
64
68
|
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
.anchored-dialog-frame {
|
|
2
|
+
box-sizing: border-box;
|
|
3
|
+
display: flex;
|
|
4
|
+
justify-content: var(--anchored-dialog-justify, flex-end);
|
|
5
|
+
padding: 1.6rem 3.2rem 2.4rem;
|
|
6
|
+
background: var(--et-surface-background-solid);
|
|
7
|
+
color: var(--et-surface-color-solid);
|
|
8
|
+
font-size: 1.4rem;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.anchored-dialog-frame [data-et-anchor] {
|
|
12
|
+
position: relative;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.anchored-dialog-frame .et-anchored-dialog-mock {
|
|
16
|
+
margin-block-start: 1.2rem;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.anchored-dialog-header {
|
|
20
|
+
display: flex;
|
|
21
|
+
align-items: baseline;
|
|
22
|
+
gap: 0.8rem;
|
|
23
|
+
padding: 1.2rem 1.2rem 0.8rem;
|
|
24
|
+
border-block-end: 1px solid var(--et-surface-border-solid);
|
|
25
|
+
font-weight: 600;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.anchored-dialog-header > :last-child {
|
|
29
|
+
margin-inline-start: auto;
|
|
30
|
+
color: var(--et-surface-color-muted-solid);
|
|
31
|
+
font-size: 1.2rem;
|
|
32
|
+
font-weight: 400;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.anchored-dialog-list {
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-direction: column;
|
|
38
|
+
margin: 0;
|
|
39
|
+
padding: 0.4rem 0;
|
|
40
|
+
list-style: none;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.anchored-dialog-list > li {
|
|
44
|
+
display: flex;
|
|
45
|
+
gap: 0.8rem;
|
|
46
|
+
align-items: baseline;
|
|
47
|
+
padding: 0.6rem 1.2rem;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.anchored-dialog-list label {
|
|
51
|
+
display: flex;
|
|
52
|
+
gap: 0.8rem;
|
|
53
|
+
align-items: center;
|
|
54
|
+
flex: 1;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.anchored-dialog-footer {
|
|
58
|
+
display: flex;
|
|
59
|
+
justify-content: flex-end;
|
|
60
|
+
gap: 0.8rem;
|
|
61
|
+
padding: 0.8rem 1.2rem 1.2rem;
|
|
62
|
+
border-block-start: 1px solid var(--et-surface-border-solid);
|
|
63
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { anchoredDialog, anchoredDialogStyles } from '@ethlete/components/design/anchored-dialog';
|
|
2
|
+
import { definePart, drawing } from '@design-system';
|
|
3
|
+
import button from '@system/components/button';
|
|
4
|
+
import edge from './anchored-dialog.css?inline';
|
|
5
|
+
|
|
6
|
+
type Props = {
|
|
7
|
+
kind?: 'open-points' | 'filter';
|
|
8
|
+
content?: string;
|
|
9
|
+
label?: string;
|
|
10
|
+
placement?: 'bottom-end' | 'bottom-start' | 'top-end' | 'top-start';
|
|
11
|
+
arrow?: boolean;
|
|
12
|
+
anchor?: string;
|
|
13
|
+
modal?: boolean;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const POINTS = ['Hover on the tonal button', 'Focus ring in dark mode', 'Disabled outline'];
|
|
17
|
+
|
|
18
|
+
const openPoints =
|
|
19
|
+
() => `<header class="anchored-dialog-header"><span>Open points</span><span>components/button</span></header>
|
|
20
|
+
<ul class="anchored-dialog-list">${POINTS.map((point) => `<li><span>${point}</span></li>`).join('')}</ul>`;
|
|
21
|
+
|
|
22
|
+
const FILTERS = [
|
|
23
|
+
{ label: 'Ready', checked: true },
|
|
24
|
+
{ label: 'Open sketch', checked: false },
|
|
25
|
+
{ label: 'Has comments', checked: true },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const filter = (apply: string) => `<header class="anchored-dialog-header"><span>Filter</span></header>
|
|
29
|
+
<ul class="anchored-dialog-list">${FILTERS.map(
|
|
30
|
+
({ label, checked }) =>
|
|
31
|
+
`<li><label><input type="checkbox" data-et-control="option"${checked ? ' checked' : ''} />${label}</label></li>`,
|
|
32
|
+
).join('')}</ul>
|
|
33
|
+
<footer class="anchored-dialog-footer">${apply}</footer>`;
|
|
34
|
+
|
|
35
|
+
export default definePart({
|
|
36
|
+
note: 'An open popover anchored to its trigger: surface pane, arrow, free content',
|
|
37
|
+
madeBy: ['claude-sonnet-5-5', 'claude-opus-5-5'],
|
|
38
|
+
states: {
|
|
39
|
+
'open-points': { kind: 'open-points', modal: false },
|
|
40
|
+
filter: { kind: 'filter', placement: 'bottom-start' },
|
|
41
|
+
'no-arrow': { kind: 'filter', placement: 'bottom-start', arrow: false },
|
|
42
|
+
'top-start': { kind: 'open-points', placement: 'top-start', modal: false },
|
|
43
|
+
},
|
|
44
|
+
widths: [480],
|
|
45
|
+
controls: {
|
|
46
|
+
option: { kind: 'row', input: ['pointer', 'keyboard'] },
|
|
47
|
+
apply: { kind: 'button', part: 'components/button' },
|
|
48
|
+
},
|
|
49
|
+
frame: (body, { placement = 'bottom-end' }) =>
|
|
50
|
+
`<div class="anchored-dialog-frame" style="--anchored-dialog-justify:${placement.endsWith('start') ? 'flex-start' : 'flex-end'}"><div data-et-anchor>${body}</div></div>`,
|
|
51
|
+
render: async ({
|
|
52
|
+
kind = 'open-points',
|
|
53
|
+
content,
|
|
54
|
+
label,
|
|
55
|
+
placement = 'bottom-end',
|
|
56
|
+
arrow,
|
|
57
|
+
anchor,
|
|
58
|
+
modal = true,
|
|
59
|
+
}: Props) => {
|
|
60
|
+
const apply = await button.render({ label: 'Apply', variant: 'filled' });
|
|
61
|
+
const pane = content ?? (kind === 'filter' ? filter(apply.body) : openPoints());
|
|
62
|
+
|
|
63
|
+
return drawing({
|
|
64
|
+
body: anchoredDialog({
|
|
65
|
+
label: label ?? (kind === 'filter' ? 'Filter' : 'Open points'),
|
|
66
|
+
placement,
|
|
67
|
+
arrow,
|
|
68
|
+
anchor,
|
|
69
|
+
modal,
|
|
70
|
+
content: pane,
|
|
71
|
+
}),
|
|
72
|
+
styles: [anchoredDialogStyles, apply.styles, edge].join('\n'),
|
|
73
|
+
});
|
|
74
|
+
},
|
|
75
|
+
});
|
|
@@ -67,12 +67,13 @@ export default definePart({
|
|
|
67
67
|
},
|
|
68
68
|
},
|
|
69
69
|
widths: [480],
|
|
70
|
+
frame: (body) => `<div class="avatar-frame">${body}</div>`,
|
|
70
71
|
render: (props: Props) =>
|
|
71
72
|
drawing({
|
|
72
|
-
body:
|
|
73
|
+
body: avatarRow([
|
|
73
74
|
...(props.avatars ?? []).map((each) => avatar(each)),
|
|
74
75
|
...(props.groups ?? []).map((each) => avatarGroup(each.avatars, each.maxVisible)),
|
|
75
|
-
])
|
|
76
|
+
]),
|
|
76
77
|
styles: [avatarStyles, frame].join('\n'),
|
|
77
78
|
}),
|
|
78
79
|
});
|
|
@@ -44,9 +44,10 @@ export default definePart({
|
|
|
44
44
|
'outline-sizes': sizes('outline'),
|
|
45
45
|
},
|
|
46
46
|
widths: [480],
|
|
47
|
+
frame: (body) => `<div class="badge-frame">${body}</div>`,
|
|
47
48
|
render: (props: Props) =>
|
|
48
49
|
drawing({
|
|
49
|
-
body:
|
|
50
|
+
body: badgeRow(props.badges.map((each) => badge(each))),
|
|
50
51
|
styles: [badgeStyles, frame].join('\n'),
|
|
51
52
|
}),
|
|
52
53
|
});
|
|
@@ -79,9 +79,10 @@ export default definePart({
|
|
|
79
79
|
close: { kind: 'button', input: ['pointer', 'keyboard'] },
|
|
80
80
|
action: { kind: 'button', input: ['pointer', 'keyboard'] },
|
|
81
81
|
},
|
|
82
|
+
frame: (body) => `<div class="banner-frame">${body}</div>`,
|
|
82
83
|
render: (props: Props) =>
|
|
83
84
|
drawing({
|
|
84
|
-
body:
|
|
85
|
+
body: render(props),
|
|
85
86
|
styles: [bannerStyles, frame].join('\n'),
|
|
86
87
|
}),
|
|
87
88
|
});
|
|
@@ -135,9 +135,10 @@ export default definePart({
|
|
|
135
135
|
],
|
|
136
136
|
},
|
|
137
137
|
widths: [1040, 1680],
|
|
138
|
+
frame: (body) => `<div class="t-frame">${body}</div>`,
|
|
138
139
|
render: (props: Props) =>
|
|
139
140
|
drawing({
|
|
140
|
-
body:
|
|
141
|
+
body: render(props),
|
|
141
142
|
styles: [bracketStyles, frame].join('\n'),
|
|
142
143
|
}),
|
|
143
144
|
});
|
|
@@ -73,9 +73,10 @@ export default definePart({
|
|
|
73
73
|
link: { kind: 'button', input: ['pointer', 'keyboard'] },
|
|
74
74
|
overflow: { kind: 'disclosure', input: ['pointer', 'keyboard'], surfaces: { body: 'overflow-open' } },
|
|
75
75
|
},
|
|
76
|
+
frame: (body) => `<div class="breadcrumb-frame">${body}</div>`,
|
|
76
77
|
render: (props: Props) =>
|
|
77
78
|
drawing({
|
|
78
|
-
body:
|
|
79
|
+
body: props.long ? longMarkup(props) : breadcrumb({ crumbs: shortTrail(props) }),
|
|
79
80
|
styles: [breadcrumbStyles, frame].join('\n'),
|
|
80
81
|
}),
|
|
81
82
|
});
|
|
@@ -3,16 +3,24 @@
|
|
|
3
3
|
--_et-focus-ring-color: light-dark(var(--et-theme-color-ink-solid), var(--et-theme-color-primary-solid));
|
|
4
4
|
}
|
|
5
5
|
|
|
6
|
+
.et-button:where([data-variant='filled']):not([disabled], [aria-disabled='true']):active {
|
|
7
|
+
--_et-button-background: color-mix(
|
|
8
|
+
in srgb,
|
|
9
|
+
rgb(var(--et-color-primary)) 96%,
|
|
10
|
+
var(--et-theme-color-on-primary-solid)
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
.et-icon-button:where([data-variant='filled']) {
|
|
7
15
|
--et-button-shadow: inset 0 0 0 1px light-dark(var(--et-theme-color-ink-solid), transparent);
|
|
8
16
|
|
|
9
17
|
@media (hover: hover) {
|
|
10
|
-
&:hover {
|
|
18
|
+
&:not([disabled], [aria-disabled='true']):hover {
|
|
11
19
|
--et-theme-color-primary-rgb: var(--et-color-primary-active);
|
|
12
20
|
}
|
|
13
21
|
}
|
|
14
22
|
|
|
15
|
-
&:active {
|
|
23
|
+
&:not([disabled], [aria-disabled='true']):active {
|
|
16
24
|
--_et-button-background: color-mix(
|
|
17
25
|
in srgb,
|
|
18
26
|
var(--et-theme-color-primary-solid) 84%,
|
|
@@ -25,7 +33,7 @@
|
|
|
25
33
|
box-shadow: var(--et-button-shadow, none);
|
|
26
34
|
|
|
27
35
|
@media (hover: hover) {
|
|
28
|
-
&:hover {
|
|
36
|
+
&:not([disabled], [aria-disabled='true']):hover {
|
|
29
37
|
--et-theme-color-primary-opacity: 1;
|
|
30
38
|
}
|
|
31
39
|
}
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
iconButtonStyles,
|
|
8
8
|
} from '@ethlete/components/design/button';
|
|
9
9
|
import { iconMarkup } from '@ethlete/components/design/icon';
|
|
10
|
+
import { loaderStyles } from '@ethlete/components/design/loader';
|
|
10
11
|
import { SDK_ICONS } from '@ethlete/components/design/icon-set';
|
|
11
12
|
import { definePart, drawing, forceInteraction, interactionRows } from '@design-system';
|
|
12
13
|
import hover from './button.css?inline';
|
|
@@ -19,12 +20,18 @@ type Props = (ButtonMockOptions | IconOnly) & { interaction?: ButtonInteraction
|
|
|
19
20
|
|
|
20
21
|
const control = ({ interaction, ...options }: Props) => {
|
|
21
22
|
const drawn = 'iconOnly' in options ? iconButton(options) : button(options);
|
|
22
|
-
const markup = drawn
|
|
23
|
+
const markup = drawn
|
|
24
|
+
.replace(/^<(button|a) /, '<$1 data-et-control="button" data-can-animate ')
|
|
25
|
+
.replace('<div class="et-button-loader"', '<div class="et-button-loader" data-et-skip="CLIPPED"');
|
|
23
26
|
|
|
24
27
|
return interaction ? forceInteraction(markup, interaction) : markup;
|
|
25
28
|
};
|
|
26
29
|
|
|
27
|
-
const
|
|
30
|
+
const fail = (message: string): never => {
|
|
31
|
+
throw new Error(message);
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const plus = iconMarkup(SDK_ICONS.find((each) => each.name === 'et-plus') ?? fail('SDK_ICONS has no et-plus'));
|
|
28
35
|
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const;
|
|
29
36
|
const VARIANTS = ['filled', 'outline', 'tonal', 'transparent'] as const;
|
|
30
37
|
type Size = (typeof SIZES)[number];
|
|
@@ -70,13 +77,19 @@ export const grid = {
|
|
|
70
77
|
|
|
71
78
|
export default definePart({
|
|
72
79
|
note: 'scale of the decided shape, Q182, lead merge',
|
|
73
|
-
madeBy: ['claude-opus-5-5', 'claude-sonnet-5-5'],
|
|
80
|
+
madeBy: ['claude-opus-5-5', 'claude-sonnet-5-5', 'design check'],
|
|
74
81
|
states,
|
|
75
82
|
grid,
|
|
76
83
|
controls: { button: { kind: 'button', input: ['pointer', 'keyboard'], disabled: true } },
|
|
77
84
|
render: (props: Props) =>
|
|
78
85
|
drawing({
|
|
79
86
|
body: control(props),
|
|
80
|
-
styles: [
|
|
87
|
+
styles: [
|
|
88
|
+
'iconOnly' in props ? iconButtonStyles : buttonStyles,
|
|
89
|
+
hover,
|
|
90
|
+
!('iconOnly' in props) && props.loading && loaderStyles,
|
|
91
|
+
]
|
|
92
|
+
.filter(Boolean)
|
|
93
|
+
.join('\n'),
|
|
81
94
|
}),
|
|
82
95
|
});
|
|
@@ -88,9 +88,10 @@ export default definePart({
|
|
|
88
88
|
'long-label': { compact: true, selected: true },
|
|
89
89
|
},
|
|
90
90
|
widths: [480],
|
|
91
|
+
frame: (body) => `<div class="t-frame">${body}</div>`,
|
|
91
92
|
render: (props: Props) =>
|
|
92
93
|
drawing({
|
|
93
|
-
body:
|
|
94
|
+
body: render(props),
|
|
94
95
|
styles: [calendarStyles, frame].join('\n'),
|
|
95
96
|
}),
|
|
96
97
|
});
|