@rio-m3e/styles 0.1.0 → 0.3.0

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/README.md CHANGED
@@ -9,4 +9,6 @@ import '@rio-m3e/styles/button.css';
9
9
  import '@rio-m3e/styles/base/ripple.css';
10
10
  ```
11
11
 
12
+ You can see the document page in [here](https://legenlee.github.io/rio/).
13
+
12
14
  Licensed under the Apache License 2.0.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rio-m3e/styles",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "description": "Framework-agnostic component styles for Rio",
5
5
  "license": "Apache-2.0",
6
6
  "author": "LegenLee",
@@ -0,0 +1,10 @@
1
+ /*
2
+ * Pairs with `attachAnchoredPosition` from `@rio-m3e/core`, which marks the element with
3
+ * `data-anchored` and writes `left` and `top` in viewport coordinates. The element is shown in the
4
+ * top layer, as a popover, which browsers center by default; this lets the coordinates apply.
5
+ */
6
+ :where([data-anchored]) {
7
+ position: fixed;
8
+ inset: auto;
9
+ margin: 0;
10
+ }
@@ -1,8 +1,7 @@
1
1
  /*
2
2
  * Reduced motion: when the user asks the OS to reduce motion, spatial motion (position, size,
3
3
  * rotation, corner radius) becomes instant. Effects motion (color, opacity) keeps its short fades,
4
- * which don't cause the discomfort reduced motion is meant to prevent. This is a Rio decision; the
5
- * M3 pages Rio has read don't define reduced-motion behavior.
4
+ * which don't cause the discomfort reduced motion is meant to prevent.
6
5
  *
7
6
  * Every element gets the override, with !important, so it beats the theme variables `applyTheme`
8
7
  * sets inline on the root, and any subtree that swaps motion schemes. Components that use the
@@ -1,9 +1,16 @@
1
1
  /*
2
2
  * Shape morphing, driven by `attachShapeMorph` from `@rio-m3e/core`. A component sets
3
- * `--rio-shape-morph-target` to the corner radius it wants in each state, and sets
4
- * `border-radius` to the same value, so it still works without JavaScript. The primitive reads
5
- * the target in px, which needs the registration below, and animates the inline radius with a
6
- * spring. It marks the element with `data-shape-morph`.
3
+ * `--rio-shape-morph-target` to the corner radius it wants in each state. The primitive reads
4
+ * the targets in px, which needs the registrations below, and springs the inline radii toward
5
+ * them. It marks the element with `data-shape-morph`.
6
+ *
7
+ * Shapes whose sides differ set `--rio-shape-morph-start-target` and `--rio-shape-morph-end-target`
8
+ * (the corners at the start and end of the line, in the writing direction) with any class or
9
+ * attribute selector; otherwise both follow `--rio-shape-morph-target`.
10
+ *
11
+ * The component also draws its corners from the per-side targets, so it still works without
12
+ * JavaScript, and lists both targets in its `transition` as `1ms step-end`, so the springs see
13
+ * them change (see "Primitives" in docs/conventions.md, and button.css).
7
14
  */
8
15
 
9
16
  @property --rio-shape-morph-target {
@@ -11,3 +18,20 @@
11
18
  inherits: false;
12
19
  initial-value: 0px;
13
20
  }
21
+
22
+ @property --rio-shape-morph-start-target {
23
+ syntax: '<length>';
24
+ inherits: false;
25
+ initial-value: 0px;
26
+ }
27
+
28
+ @property --rio-shape-morph-end-target {
29
+ syntax: '<length>';
30
+ inherits: false;
31
+ initial-value: 0px;
32
+ }
33
+
34
+ :where([data-shape-morph]) {
35
+ --rio-shape-morph-start-target: var(--rio-shape-morph-target);
36
+ --rio-shape-morph-end-target: var(--rio-shape-morph-target);
37
+ }
@@ -41,7 +41,21 @@
41
41
  opacity: var(--rio-state-pressed-state-layer-opacity);
42
42
  }
43
43
 
44
- .rio-state-layer[data-dragged]::before {
44
+ /*
45
+ * A button that opened a menu stays pressed while the menu is open. Only a button that opens a
46
+ * popup: other buttons that expand something, such as an accordion's, keep their look. Disabled
47
+ * ones are left out here, since this selector outweighs the disabled rule below.
48
+ */
49
+ .rio-state-layer[aria-haspopup]:not(
50
+ [aria-haspopup='false'],
51
+ :disabled,
52
+ [aria-disabled='true']
53
+ )[aria-expanded='true']::before {
54
+ opacity: var(--rio-state-pressed-state-layer-opacity);
55
+ }
56
+
57
+ /* `attachDrag` from `@rio-m3e/core` marks an element with `data-dragging` while it moves. */
58
+ .rio-state-layer[data-dragging]::before {
45
59
  opacity: var(--rio-state-dragged-state-layer-opacity);
46
60
  }
47
61
 
@@ -0,0 +1,120 @@
1
+ /*
2
+ * M3E button group: a row of buttons and icon buttons (`button.css`, `icon-button.css`). The group
3
+ * has no color of its own. It carries `data-variant`, `data-size`, and `data-shape`, which
4
+ * `createButtonGroup` from `@rio-m3e/core` sets; the size and shape reach the buttons through the
5
+ * framework adapter, so these rules read the buttons' own `data-size` and `data-shape`.
6
+ *
7
+ * Values come from Material Web's component tokens (`_md-comp-button-group-standard-*.scss` and
8
+ * `_md-comp-button-group-connected-*.scss`, Material 3 v34.0.21). The pressed button's width
9
+ * change in standard groups is JavaScript (`attachButtonGroup` from `@rio-m3e/core`).
10
+ */
11
+
12
+ .rio-button-group {
13
+ /* Standard, small */
14
+ --_between: 12px;
15
+
16
+ display: inline-flex;
17
+ align-items: center;
18
+ gap: var(--_between);
19
+ vertical-align: middle;
20
+ }
21
+
22
+ /* Standard: the space keeps every button's 48px target clear of its neighbors' */
23
+
24
+ .rio-button-group:where([data-size='xsmall']) {
25
+ --_between: 18px;
26
+ }
27
+
28
+ .rio-button-group:where([data-size='medium'], [data-size='large'], [data-size='xlarge']) {
29
+ --_between: 8px;
30
+ }
31
+
32
+ /*
33
+ * Connected: 2px apart, and as wide as the space it's given, shared equally by its buttons. The
34
+ * ends of the row keep the group's shape; the facing corners are smaller, tighten when pressed,
35
+ * and open up fully when the button is selected. Each button springs its two sides separately
36
+ * (see base/shape-morph.css).
37
+ *
38
+ * A button may sit in a tooltip's wrapper (`tooltip.css`), which has no box, so each rule also
39
+ * reaches a button one level down in one.
40
+ */
41
+
42
+ .rio-button-group:where([data-variant='connected']) {
43
+ --_between: 2px;
44
+
45
+ display: flex;
46
+ }
47
+
48
+ .rio-button-group[data-variant='connected'] > .rio-button,
49
+ .rio-button-group[data-variant='connected'] > .rio-tooltip-trigger > .rio-button {
50
+ /* Small, medium, and extra small */
51
+ --_inner: var(--rio-shape-corner-small);
52
+ --_inner-pressed: var(--rio-shape-corner-extra-small);
53
+ /* Corners that face another button, and corners at the ends of the row */
54
+ --_inner-now: var(--_inner);
55
+ --_outer: var(--_round);
56
+ --_start: var(--_inner-now);
57
+ --_end: var(--_inner-now);
58
+
59
+ flex: 1 1 0;
60
+ /* The corners show these targets (button.css). */
61
+ --rio-shape-morph-start-target: var(--_start);
62
+ --rio-shape-morph-end-target: var(--_end);
63
+ }
64
+
65
+ .rio-button-group[data-variant='connected'] > .rio-button:where([data-size='large']),
66
+ .rio-button-group[data-variant='connected']
67
+ > .rio-tooltip-trigger
68
+ > .rio-button:where([data-size='large']) {
69
+ --_inner: var(--rio-shape-corner-large);
70
+ --_inner-pressed: var(--rio-shape-corner-medium);
71
+ }
72
+
73
+ .rio-button-group[data-variant='connected'] > .rio-button:where([data-size='xlarge']),
74
+ .rio-button-group[data-variant='connected']
75
+ > .rio-tooltip-trigger
76
+ > .rio-button:where([data-size='xlarge']) {
77
+ --_inner: var(--rio-shape-corner-large-increased);
78
+ --_inner-pressed: var(--rio-shape-corner-large);
79
+ }
80
+
81
+ /* A square group's ends use the same radius as its facing corners. */
82
+ .rio-button-group[data-variant='connected'] > .rio-button[data-shape='square'],
83
+ .rio-button-group[data-variant='connected']
84
+ > .rio-tooltip-trigger
85
+ > .rio-button[data-shape='square'] {
86
+ --_outer: var(--_inner);
87
+ }
88
+
89
+ .rio-button-group[data-variant='connected'] > .rio-button:first-child,
90
+ .rio-button-group[data-variant='connected'] > .rio-tooltip-trigger:first-child > .rio-button {
91
+ --_start: var(--_outer);
92
+ }
93
+
94
+ .rio-button-group[data-variant='connected'] > .rio-button:last-child,
95
+ .rio-button-group[data-variant='connected'] > .rio-tooltip-trigger:last-child > .rio-button {
96
+ --_end: var(--_outer);
97
+ }
98
+
99
+ .rio-button-group[data-variant='connected'] > .rio-button[data-selected='true'],
100
+ .rio-button-group[data-variant='connected']
101
+ > .rio-tooltip-trigger
102
+ > .rio-button[data-selected='true'] {
103
+ --_inner-now: var(--_round);
104
+ --_outer: var(--_round);
105
+ }
106
+
107
+ /* After selected, so a pressed selected button still tightens its facing corners */
108
+ .rio-button-group[data-variant='connected'] > .rio-button[data-pressed],
109
+ .rio-button-group[data-variant='connected'] > .rio-tooltip-trigger > .rio-button[data-pressed] {
110
+ --_inner-now: var(--_inner-pressed);
111
+ }
112
+
113
+ /* Extra small and small icon buttons keep a 48px minimum width, matching their target. */
114
+ .rio-button-group[data-variant='connected']
115
+ > .rio-icon-button:where([data-size='xsmall'], [data-size='small']),
116
+ .rio-button-group[data-variant='connected']
117
+ > .rio-tooltip-trigger
118
+ > .rio-icon-button:where([data-size='xsmall'], [data-size='small']) {
119
+ min-inline-size: 48px;
120
+ }
package/src/button.css CHANGED
@@ -42,9 +42,19 @@
42
42
  block-size: var(--_height);
43
43
  padding-inline: var(--_padding);
44
44
  border: var(--_outline-width) solid var(--_outline);
45
- /* The shape-morph primitive springs the inline radius toward this target. */
45
+ /*
46
+ * The shape-morph primitive springs the inline radii toward these targets. The corners show the
47
+ * targets, not `--_radius`: when the state changes, a target holds its old value until the
48
+ * spring takes over (see `transition` below), so the new shape doesn't flash first. Button
49
+ * groups and split buttons set the per-side targets.
50
+ */
46
51
  --rio-shape-morph-target: var(--_radius);
47
- border-radius: var(--_radius);
52
+ --rio-shape-morph-start-target: var(--rio-shape-morph-target);
53
+ --rio-shape-morph-end-target: var(--rio-shape-morph-target);
54
+ border-start-start-radius: var(--rio-shape-morph-start-target);
55
+ border-end-start-radius: var(--rio-shape-morph-start-target);
56
+ border-start-end-radius: var(--rio-shape-morph-end-target);
57
+ border-end-end-radius: var(--rio-shape-morph-end-target);
48
58
  background-color: var(--_container);
49
59
  box-shadow: var(--_elevation);
50
60
  color: var(--_content);
@@ -54,11 +64,20 @@
54
64
  vertical-align: middle;
55
65
  cursor: pointer;
56
66
  user-select: none;
57
- /* No border-radius transition: corners move with the fast-spatial spring (see base/shape-morph.css). */
67
+ /*
68
+ * No border-radius transition: corners move with the fast-spatial spring (see
69
+ * base/shape-morph.css). The spring targets get an instant transition instead, which is how the
70
+ * spring sees them change (core's spring-property). `--rio-spring-targets` is public: an app
71
+ * that sets its own `transition` on a button keeps the morphing by including it, and
72
+ * stylesheets that spring more targets on a button extend it.
73
+ */
74
+ --rio-spring-targets:
75
+ --rio-shape-morph-start-target 1ms step-end, --rio-shape-morph-end-target 1ms step-end;
58
76
  transition:
59
77
  background-color var(--rio-motion-fast-effects),
60
78
  color var(--rio-motion-fast-effects),
61
- box-shadow var(--rio-motion-fast-effects);
79
+ box-shadow var(--rio-motion-fast-effects),
80
+ var(--rio-spring-targets);
62
81
  }
63
82
 
64
83
  /* Sizes */
@@ -189,7 +208,6 @@
189
208
 
190
209
  .rio-button[data-pressed] {
191
210
  --rio-shape-morph-target: var(--_pressed);
192
- border-radius: var(--_pressed);
193
211
  box-shadow: var(--_elevation);
194
212
  }
195
213
 
@@ -237,8 +255,10 @@
237
255
  /* Extra small and small buttons are shorter than the spec's 48px minimum target. */
238
256
  .rio-button__touch {
239
257
  position: absolute;
240
- inset-block-start: 50%;
241
- inset-inline-start: 50%;
258
+ /* Physical, like `translate`: a logical start would flip in right-to-left layouts and push the
259
+ target off the button. */
260
+ top: 50%;
261
+ left: 50%;
242
262
  inline-size: max(100%, 48px);
243
263
  block-size: max(100%, 48px);
244
264
  translate: -50% -50%;
package/src/dialog.css ADDED
@@ -0,0 +1,201 @@
1
+ /*
2
+ * M3 basic dialogs. `attachDialog` from `@rio-m3e/core` shows the `<dialog>` with `showModal()`,
3
+ * in the top layer above its scrim, centered in the window.
4
+ *
5
+ * Values come from Material Web's component tokens (`_md-comp-dialog.scss`, Material 3 v34.0.21)
6
+ * and the spec page's measurements: 280px to 560px wide, 24px padding, 16px from the icon to the
7
+ * headline and from the headline to the content, 24px from the content to the buttons, 8px
8
+ * between buttons. The scrim's color and 32% opacity come from `_md-comp-scrim.scss`.
9
+ */
10
+
11
+ /* How much of the container shows, from the top: it expands as the dialog opens. */
12
+ @property --_dialog-reveal {
13
+ syntax: '<percentage>';
14
+ inherits: true;
15
+ initial-value: 100%;
16
+ }
17
+
18
+ .rio-dialog {
19
+ box-sizing: border-box;
20
+ flex-direction: column;
21
+ min-inline-size: 280px;
22
+ max-inline-size: min(560px, 100% - 48px);
23
+ max-block-size: calc(100% - 48px);
24
+ margin: auto;
25
+ padding: 24px 0;
26
+ border: 0;
27
+ border-radius: var(--rio-shape-corner-extra-large);
28
+ outline: none;
29
+ /* The container is drawn behind the content, so it can expand; its shadow reaches past the box. */
30
+ isolation: isolate;
31
+ overflow: visible;
32
+ background: transparent;
33
+ color: var(--rio-color-on-surface-variant);
34
+ font: var(--rio-typescale-body-medium);
35
+ letter-spacing: var(--rio-typescale-body-medium-tracking);
36
+ overflow-wrap: break-word;
37
+ /* Closing */
38
+ transition:
39
+ opacity var(--rio-motion-fast-effects),
40
+ --_dialog-reveal var(--rio-motion-fast-spatial),
41
+ overlay var(--rio-motion-fast-effects) allow-discrete,
42
+ display var(--rio-motion-fast-effects) allow-discrete;
43
+ }
44
+
45
+ .rio-dialog::before {
46
+ content: '';
47
+ position: absolute;
48
+ z-index: -1;
49
+ inset: 0 0 auto;
50
+ block-size: var(--_dialog-reveal);
51
+ border-radius: inherit;
52
+ background-color: var(--rio-color-surface-container-high);
53
+ box-shadow: var(--rio-elevation-level3);
54
+ }
55
+
56
+ /*
57
+ * Opening: the container expands downward from the top, and its content fades in once the
58
+ * container is under it. Closing fades it all out as the container collapses.
59
+ */
60
+ .rio-dialog[open] {
61
+ display: flex;
62
+ transition:
63
+ opacity var(--rio-motion-fast-effects),
64
+ --_dialog-reveal var(--rio-motion-default-spatial),
65
+ overlay var(--rio-motion-fast-effects) allow-discrete,
66
+ display var(--rio-motion-fast-effects) allow-discrete;
67
+ }
68
+
69
+ .rio-dialog:not([open]) {
70
+ --_dialog-reveal: 35%;
71
+
72
+ opacity: 0;
73
+ }
74
+
75
+ .rio-dialog > * {
76
+ transition: opacity var(--rio-motion-fast-effects);
77
+ }
78
+
79
+ .rio-dialog[open] > * {
80
+ transition-delay: var(--rio-motion-fast-effects-duration);
81
+ }
82
+
83
+ @starting-style {
84
+ .rio-dialog[open] {
85
+ --_dialog-reveal: 35%;
86
+
87
+ opacity: 0;
88
+ }
89
+
90
+ .rio-dialog[open] > * {
91
+ opacity: 0;
92
+ }
93
+ }
94
+
95
+ /* Without spatial motion the container doesn't collapse at once as it starts fading out. */
96
+ @media (prefers-reduced-motion: reduce) {
97
+ .rio-dialog:not([open]) {
98
+ --_dialog-reveal: 100%;
99
+ }
100
+ }
101
+
102
+ .rio-dialog::backdrop {
103
+ background-color: color-mix(in srgb, var(--rio-color-scrim) 32%, transparent);
104
+ transition:
105
+ opacity var(--rio-motion-fast-effects),
106
+ overlay var(--rio-motion-fast-effects) allow-discrete,
107
+ display var(--rio-motion-fast-effects) allow-discrete;
108
+ }
109
+
110
+ .rio-dialog[open]::backdrop {
111
+ transition:
112
+ opacity var(--rio-motion-default-effects),
113
+ overlay var(--rio-motion-fast-effects) allow-discrete,
114
+ display var(--rio-motion-fast-effects) allow-discrete;
115
+ }
116
+
117
+ .rio-dialog:not([open])::backdrop {
118
+ opacity: 0;
119
+ }
120
+
121
+ @starting-style {
122
+ .rio-dialog[open]::backdrop {
123
+ opacity: 0;
124
+ }
125
+ }
126
+
127
+ /* Parts */
128
+
129
+ .rio-dialog__icon {
130
+ display: inline-flex;
131
+ flex: none;
132
+ align-self: center;
133
+ inline-size: 24px;
134
+ block-size: 24px;
135
+ margin-block-end: 16px;
136
+ color: var(--rio-color-secondary);
137
+ }
138
+
139
+ .rio-dialog__icon > :is(svg, img) {
140
+ inline-size: 100%;
141
+ block-size: 100%;
142
+ }
143
+
144
+ .rio-dialog__headline {
145
+ flex: none;
146
+ margin: 0 0 16px;
147
+ padding-inline: 24px;
148
+ color: var(--rio-color-on-surface);
149
+ font: var(--rio-typescale-headline-small);
150
+ letter-spacing: var(--rio-typescale-headline-small-tracking);
151
+ }
152
+
153
+ .rio-dialog[data-icon] .rio-dialog__headline {
154
+ text-align: center;
155
+ }
156
+
157
+ /*
158
+ * The headline and the buttons stay put while the content scrolls. A divider shows at the edge
159
+ * the content scrolls past, where it meets the headline or the buttons.
160
+ */
161
+ .rio-dialog__content {
162
+ --_divider-top: transparent;
163
+ --_divider-bottom: transparent;
164
+
165
+ flex: 1 1 auto;
166
+ min-block-size: 0;
167
+ padding-inline: 24px;
168
+ overflow-y: auto;
169
+ overscroll-behavior: contain;
170
+ box-shadow:
171
+ 0 -1px var(--_divider-top),
172
+ 0 1px var(--_divider-bottom);
173
+ }
174
+
175
+ .rio-dialog__content[data-overflow-top]:not(:first-child) {
176
+ --_divider-top: var(--rio-color-outline);
177
+ }
178
+
179
+ .rio-dialog__content[data-overflow-bottom]:not(:last-child) {
180
+ --_divider-bottom: var(--rio-color-outline);
181
+ }
182
+
183
+ /*
184
+ * Buttons sit at the end, the confirming one last, nearest the edge. When they don't fit side by
185
+ * side, they stack with the confirming one on top.
186
+ */
187
+ .rio-dialog__actions {
188
+ display: flex;
189
+ flex: none;
190
+ flex-wrap: wrap-reverse;
191
+ justify-content: flex-end;
192
+ gap: 8px;
193
+ margin-block-start: 24px;
194
+ padding-inline: 24px;
195
+ }
196
+
197
+ @media (forced-colors: active) {
198
+ .rio-dialog {
199
+ outline: 2px solid CanvasText;
200
+ }
201
+ }
package/src/menu.css ADDED
@@ -0,0 +1,255 @@
1
+ /*
2
+ * M3E vertical menu. `attachMenu` from `@rio-m3e/core` shows the menu in the top layer as a
3
+ * popover and places it next to its anchor (`base/anchored-position.css`), which also says where
4
+ * it ended up (`data-side`, `data-align`) and how much room there is.
5
+ *
6
+ * Values come from Material Web's component tokens (`_md-comp-menus.scss`, `-standard`,
7
+ * `-vibrant`, Material 3 v34.0.21). The spacing around the items comes from the M3 Design Kit: a
8
+ * 48px row per item, with the 44px item inset 2px above and below and 4px at the sides, so items
9
+ * sit 4px apart and 4px from the container's edges.
10
+ */
11
+
12
+ .rio-menu {
13
+ --_container: var(--rio-color-surface-container-low);
14
+ --_content: var(--rio-color-on-surface);
15
+ --_icon: var(--rio-color-on-surface-variant);
16
+ --_supporting: var(--rio-color-on-surface-variant);
17
+ --_selected-container: var(--rio-color-tertiary-container);
18
+ --_selected-content: var(--rio-color-on-tertiary-container);
19
+
20
+ /* Only while open: a `display` here would override the browser's hiding of closed popovers. */
21
+ flex-direction: column;
22
+ box-sizing: border-box;
23
+ /* Not in the tokens: the baseline menu's limits */
24
+ min-inline-size: 112px;
25
+ max-inline-size: min(280px, var(--rio-anchored-available-width, 280px));
26
+ max-block-size: var(--rio-anchored-available-height, none);
27
+ padding: 2px 4px;
28
+ overflow-x: hidden;
29
+ overflow-y: auto;
30
+ border: 0;
31
+ border-radius: var(--rio-shape-corner-large);
32
+ background-color: var(--_container);
33
+ color: var(--_content);
34
+ box-shadow: var(--rio-elevation-level2);
35
+ }
36
+
37
+ .rio-menu:where([data-variant='vibrant']) {
38
+ --_container: var(--rio-color-tertiary-container);
39
+ --_content: var(--rio-color-on-tertiary-container);
40
+ --_icon: var(--rio-color-on-tertiary-container);
41
+ --_supporting: var(--rio-color-on-tertiary-container);
42
+ --_selected-container: var(--rio-color-tertiary);
43
+ --_selected-content: var(--rio-color-on-tertiary);
44
+ }
45
+
46
+ /*
47
+ * Opening: the menu expands from the side facing its anchor and fades in; closing reverses it.
48
+ * Without `transition-behavior: allow-discrete` support for `overlay`, closing is instant.
49
+ */
50
+ .rio-menu {
51
+ --_origin-block: top;
52
+ --_origin-inline: left;
53
+
54
+ transform-origin: var(--_origin-inline) var(--_origin-block);
55
+ transition:
56
+ opacity var(--rio-motion-fast-effects),
57
+ scale var(--rio-motion-fast-spatial),
58
+ border-radius var(--rio-motion-fast-spatial),
59
+ overlay var(--rio-motion-fast-effects) allow-discrete,
60
+ display var(--rio-motion-fast-effects) allow-discrete;
61
+ }
62
+
63
+ /*
64
+ * With a submenu open, the menu the pointer or the focus is in keeps the active container shape
65
+ * (corner large), and the others take the inactive one (corner small), on the `fast-spatial` curve.
66
+ * `attachMenu` marks them with `data-inactive`.
67
+ */
68
+ .rio-menu[data-inactive] {
69
+ border-radius: var(--rio-shape-corner-small);
70
+ }
71
+
72
+ .rio-menu:popover-open {
73
+ display: flex;
74
+ }
75
+
76
+ .rio-menu:not(:popover-open) {
77
+ opacity: 0;
78
+ scale: 1 0.8;
79
+ }
80
+
81
+ @starting-style {
82
+ .rio-menu:popover-open {
83
+ opacity: 0;
84
+ scale: 1 0.8;
85
+ }
86
+ }
87
+
88
+ .rio-menu[data-side='top'] {
89
+ --_origin-block: bottom;
90
+ }
91
+
92
+ .rio-menu:is([data-side='start'], [data-side='end']) {
93
+ --_origin-block: top;
94
+ }
95
+
96
+ .rio-menu:is([data-align='end'], [data-side='start']):dir(ltr),
97
+ .rio-menu:not([data-align='end'], [data-side='start']):dir(rtl) {
98
+ --_origin-inline: right;
99
+ }
100
+
101
+ .rio-menu[data-instant] {
102
+ transition: none;
103
+ }
104
+
105
+ /* Items */
106
+
107
+ .rio-menu-item {
108
+ --_item-content: var(--_content);
109
+ --_item-icon: var(--_icon);
110
+ --_item-supporting: var(--_supporting);
111
+ --_item-container: transparent;
112
+ --_corner-block-start: var(--rio-shape-corner-extra-small);
113
+ --_corner-block-end: var(--rio-shape-corner-extra-small);
114
+
115
+ display: flex;
116
+ flex: none;
117
+ align-items: center;
118
+ gap: 12px;
119
+ box-sizing: border-box;
120
+ inline-size: 100%;
121
+ min-block-size: 44px;
122
+ margin: 2px 0;
123
+ padding: 8px 12px;
124
+ border: 0;
125
+ border-start-start-radius: var(--_corner-block-start);
126
+ border-start-end-radius: var(--_corner-block-start);
127
+ border-end-start-radius: var(--_corner-block-end);
128
+ border-end-end-radius: var(--_corner-block-end);
129
+ background-color: var(--_item-container);
130
+ color: var(--_item-content);
131
+ font: var(--rio-typescale-label-large);
132
+ letter-spacing: var(--rio-typescale-label-large-tracking);
133
+ text-align: start;
134
+ cursor: pointer;
135
+ user-select: none;
136
+ transition:
137
+ background-color var(--rio-motion-fast-effects),
138
+ color var(--rio-motion-fast-effects),
139
+ border-radius var(--rio-motion-fast-spatial);
140
+ --rio-focus-ring-color: var(--rio-color-secondary);
141
+ }
142
+
143
+ /* The first and last items of a menu or group round their outer corners to sit inside the container. */
144
+ .rio-menu-item:nth-child(1 of .rio-menu-item) {
145
+ --_corner-block-start: var(--rio-shape-corner-medium);
146
+ }
147
+
148
+ .rio-menu-item:nth-last-child(1 of .rio-menu-item) {
149
+ --_corner-block-end: var(--rio-shape-corner-medium);
150
+ }
151
+
152
+ .rio-menu-item[data-selected='true'] {
153
+ --_item-content: var(--_selected-content);
154
+ --_item-icon: var(--_selected-content);
155
+ --_item-supporting: var(--_selected-content);
156
+ --_item-container: var(--_selected-container);
157
+ --_corner-block-start: var(--rio-shape-corner-medium);
158
+ --_corner-block-end: var(--rio-shape-corner-medium);
159
+ }
160
+
161
+ /*
162
+ * The item whose submenu is open is active, with the hovered state layer
163
+ * (`menu-item.active.state-layer`), where `base/state-layer.css` would show a menu button's open
164
+ * menu as pressed. With the focus on it, that pressed layer stays, of the same strength as focus.
165
+ */
166
+ .rio-menu-item.rio-state-layer[aria-haspopup][aria-expanded='true']:not(:focus-visible)::before {
167
+ opacity: var(--rio-state-hover-state-layer-opacity);
168
+ }
169
+
170
+ .rio-menu-item[aria-disabled='true'] {
171
+ cursor: default;
172
+ }
173
+
174
+ .rio-menu-item[aria-disabled='true'] > :not(.rio-menu-item__touch) {
175
+ opacity: 0.38;
176
+ }
177
+
178
+ .rio-menu-item[aria-disabled='true'][data-selected='true'] {
179
+ --_item-container: color-mix(in srgb, var(--_selected-container) 38%, transparent);
180
+ }
181
+
182
+ .rio-menu-item__touch {
183
+ position: absolute;
184
+ inset: -2px 0;
185
+ }
186
+
187
+ .rio-menu-item__icon,
188
+ .rio-menu-item__trailing-icon {
189
+ display: inline-flex;
190
+ flex: none;
191
+ inline-size: 20px;
192
+ block-size: 20px;
193
+ color: var(--_item-icon);
194
+ }
195
+
196
+ .rio-menu-item__icon > :is(svg, img),
197
+ .rio-menu-item__trailing-icon > :is(svg, img) {
198
+ inline-size: 100%;
199
+ block-size: 100%;
200
+ }
201
+
202
+ .rio-menu-item__text {
203
+ display: flex;
204
+ flex: 1;
205
+ flex-direction: column;
206
+ min-inline-size: 0;
207
+ }
208
+
209
+ .rio-menu-item__label {
210
+ overflow: hidden;
211
+ text-overflow: ellipsis;
212
+ white-space: nowrap;
213
+ }
214
+
215
+ .rio-menu-item__supporting-text {
216
+ color: var(--_item-supporting);
217
+ font: var(--rio-typescale-body-small);
218
+ letter-spacing: var(--rio-typescale-body-small-tracking);
219
+ }
220
+
221
+ .rio-menu-item__trailing-text {
222
+ flex: none;
223
+ color: var(--_item-supporting);
224
+ }
225
+
226
+ /* Groups and dividers */
227
+
228
+ .rio-menu-group__items {
229
+ display: flex;
230
+ flex-direction: column;
231
+ }
232
+
233
+ .rio-menu-group__label {
234
+ display: flex;
235
+ align-items: center;
236
+ min-block-size: 32px;
237
+ padding-inline: 12px;
238
+ color: var(--_supporting);
239
+ font: var(--rio-typescale-label-large);
240
+ letter-spacing: var(--rio-typescale-label-large-tracking);
241
+ }
242
+
243
+ /* Not in the vertical menu's tokens: the baseline menu's divider, 1px of surface variant */
244
+ .rio-menu-divider {
245
+ flex: none;
246
+ block-size: 1px;
247
+ margin: 4px 12px;
248
+ background-color: var(--rio-color-surface-variant);
249
+ }
250
+
251
+ @media (forced-colors: active) {
252
+ .rio-menu {
253
+ border: 1px solid CanvasText;
254
+ }
255
+ }
@@ -0,0 +1,185 @@
1
+ /*
2
+ * M3E split button: a leading button and a trailing menu button, 2px apart. Both are buttons
3
+ * (`button.css`), so they keep the button's sizes, colors, elevation, disabled rules, state layer,
4
+ * ripple, and touch target. This file adds what differs: the paddings, the trailing button's
5
+ * width, the corners, and the menu icon. `createSplitButton` from `@rio-m3e/core` sets the
6
+ * attributes, and the buttons carry `data-size`, `data-variant`, and, on the trailing button,
7
+ * `data-open`.
8
+ *
9
+ * Values come from Material Web's component tokens (`_md-comp-split-button-*.scss`, Material 3
10
+ * v34.0.21). The menu icon's offset comes from the spec page's measurements, and its direction
11
+ * (toward the start) from the M3 Design Kit.
12
+ */
13
+
14
+ @property --rio-split-button-menu-turn-target {
15
+ syntax: '<angle>';
16
+ inherits: false;
17
+ initial-value: 0deg;
18
+ }
19
+
20
+ @property --rio-split-button-menu-shift-target {
21
+ syntax: '<length>';
22
+ inherits: false;
23
+ initial-value: 0px;
24
+ }
25
+
26
+ .rio-split-button {
27
+ display: inline-flex;
28
+ align-items: center;
29
+ gap: 2px;
30
+ vertical-align: middle;
31
+ }
32
+
33
+ /*
34
+ * Both buttons: the corners at the ends of the split button are round, and the facing ones are
35
+ * small. Hovering, focusing, or pressing a button rounds its facing corners more, and opening the
36
+ * menu rounds the trailing button fully. Each side springs on its own (see base/shape-morph.css).
37
+ */
38
+ .rio-split-button > .rio-button {
39
+ /* Small */
40
+ --_lead-start: 16px;
41
+ --_lead-end: 12px;
42
+ --_menu-size: 22px;
43
+ --_menu-padding: 13px;
44
+ --_menu-offset: 1px;
45
+ --_inner: var(--rio-shape-corner-extra-small);
46
+ --_inner-active: var(--rio-shape-corner-medium);
47
+
48
+ --_inner-now: var(--_inner);
49
+ }
50
+
51
+ /*
52
+ * `[data-variant]` is always there; it lifts this above button.css's per-side defaults in any
53
+ * order. The corners show these targets (button.css).
54
+ */
55
+ .rio-split-button > .rio-button[data-variant] {
56
+ --rio-shape-morph-start-target: var(--_start);
57
+ --rio-shape-morph-end-target: var(--_end);
58
+ }
59
+
60
+ .rio-split-button > .rio-button:where([data-size='xsmall']) {
61
+ --_lead-start: 12px;
62
+ --_lead-end: 10px;
63
+ --_inner-active: var(--rio-shape-corner-small);
64
+ }
65
+
66
+ .rio-split-button > .rio-button:where([data-size='medium']) {
67
+ --_lead-start: 24px;
68
+ --_lead-end: 24px;
69
+ --_menu-size: 26px;
70
+ --_menu-padding: 15px;
71
+ --_menu-offset: 2px;
72
+ }
73
+
74
+ .rio-split-button > .rio-button:where([data-size='large']) {
75
+ --_lead-start: 48px;
76
+ --_lead-end: 48px;
77
+ --_menu-size: 38px;
78
+ --_menu-padding: 29px;
79
+ --_menu-offset: 3px;
80
+ --_inner: var(--rio-shape-corner-small);
81
+ --_inner-active: var(--rio-shape-corner-large-increased);
82
+ }
83
+
84
+ .rio-split-button > .rio-button:where([data-size='xlarge']) {
85
+ --_lead-start: 64px;
86
+ --_lead-end: 64px;
87
+ --_menu-size: 50px;
88
+ --_menu-padding: 43px;
89
+ --_menu-offset: 6px;
90
+ --_inner: var(--rio-shape-corner-medium);
91
+ --_inner-active: var(--rio-shape-corner-large-increased);
92
+ }
93
+
94
+ @media (hover: hover) {
95
+ .rio-split-button > .rio-button:hover {
96
+ --_inner-now: var(--_inner-active);
97
+ }
98
+ }
99
+
100
+ .rio-split-button > .rio-button:is(:focus-visible, [data-pressed]) {
101
+ --_inner-now: var(--_inner-active);
102
+ }
103
+
104
+ /*
105
+ * Disabled buttons don't react, so their corners stay put. Being open still shows (that rule comes
106
+ * later): a disabled toggle button keeps its selected shape too.
107
+ */
108
+ .rio-split-button > .rio-button:is(:disabled, [aria-disabled='true']) {
109
+ --_inner-now: var(--_inner);
110
+ }
111
+
112
+ /* Leading button */
113
+
114
+ .rio-split-button > .rio-split-button__leading {
115
+ --_start: var(--_round);
116
+ --_end: var(--_inner-now);
117
+
118
+ padding-inline: var(--_lead-start) var(--_lead-end);
119
+ }
120
+
121
+ /* Trailing button: a square, or 48px wide when shorter, with the menu icon in it */
122
+
123
+ .rio-split-button > .rio-split-button__trailing {
124
+ --_start: var(--_inner-now);
125
+ --_end: var(--_round);
126
+ --rio-split-button-menu-shift-target: var(--_menu-offset);
127
+ /* Toward the start: left, or right in right-to-left layouts */
128
+ --_start-sign: -1;
129
+
130
+ inline-size: calc(var(--_menu-size) + 2 * var(--_menu-padding));
131
+ padding: 0;
132
+ }
133
+
134
+ .rio-split-button > .rio-split-button__trailing:dir(rtl) {
135
+ --_start-sign: 1;
136
+ }
137
+
138
+ /*
139
+ * Open: fully round, with the icon centered and turned half a circle, its point swinging through
140
+ * the start side, toward the leading button. The color doesn't change; a state layer shows
141
+ * instead, at the pressed opacity.
142
+ */
143
+ .rio-split-button > .rio-split-button__trailing[data-open] {
144
+ --_inner-now: var(--_round);
145
+ --rio-split-button-menu-turn-target: 180deg;
146
+ --rio-split-button-menu-shift-target: 0px;
147
+ }
148
+
149
+ .rio-split-button > .rio-split-button__trailing[data-open]:dir(rtl) {
150
+ --rio-split-button-menu-turn-target: -180deg;
151
+ }
152
+
153
+ .rio-split-button
154
+ > .rio-split-button__trailing[data-open]:not(:disabled, [aria-disabled='true'])::before {
155
+ opacity: var(--rio-state-pressed-state-layer-opacity);
156
+ }
157
+
158
+ /*
159
+ * The springs in `attachSplitButtonMenuIcon` write `--_menu-turn` and `--_menu-shift` while they
160
+ * move; at rest the targets apply. The targets are registered and don't inherit, so they are
161
+ * passed down through these.
162
+ */
163
+ .rio-split-button > .rio-split-button__trailing {
164
+ --_menu-turn-rest: var(--rio-split-button-menu-turn-target);
165
+ --_menu-shift-rest: var(--rio-split-button-menu-shift-target);
166
+ /* The button's spring targets (button.css), and the menu icon's. */
167
+ --rio-spring-targets:
168
+ --rio-shape-morph-start-target 1ms step-end, --rio-shape-morph-end-target 1ms step-end,
169
+ --rio-split-button-menu-turn-target 1ms step-end,
170
+ --rio-split-button-menu-shift-target 1ms step-end;
171
+ }
172
+
173
+ .rio-split-button__menu-icon {
174
+ display: inline-flex;
175
+ flex: none;
176
+ inline-size: var(--_menu-size);
177
+ block-size: var(--_menu-size);
178
+ rotate: var(--_menu-turn, var(--_menu-turn-rest));
179
+ translate: calc(var(--_menu-shift, var(--_menu-shift-rest)) * var(--_start-sign)) 0;
180
+ }
181
+
182
+ .rio-split-button__menu-icon > :is(svg, img) {
183
+ inline-size: 100%;
184
+ block-size: 100%;
185
+ }
package/src/switch.css CHANGED
@@ -4,7 +4,8 @@
4
4
  * Values come from Material Web's component tokens (`_md-comp-switch.scss`, Material 3 v34.0.21),
5
5
  * which match the spec's measurements table. The handle's center and size are declared as targets
6
6
  * for each state; `attachSwitchHandle` from `@rio-m3e/core` springs `--_handle-x` and
7
- * `--_handle-size` toward them. Without JavaScript the handle simply jumps.
7
+ * `--_handle-size` toward them, which show the targets at rest. The targets get an instant
8
+ * transition so the springs see them change. Without JavaScript the handle simply jumps.
8
9
  *
9
10
  * The root carries `data-selected`, plus `data-pressed` (from the ripple), `data-dragging` (from
10
11
  * the drag), and `data-icon` when the handle shows an icon. State rules sit inside `:where()` so
@@ -72,9 +73,14 @@
72
73
  /* Horizontal drags belong to the handle; vertical ones still scroll the page. */
73
74
  touch-action: pan-y;
74
75
  -webkit-tap-highlight-color: transparent;
76
+ /* The handle's spring targets get an instant transition, which is how the springs see them
77
+ change (core's spring-property). Public, as on the button (button.css). */
78
+ --rio-spring-targets:
79
+ --rio-switch-handle-x-target 1ms step-end, --rio-switch-handle-size-target 1ms step-end;
75
80
  transition:
76
81
  background-color var(--rio-motion-fast-effects),
77
- border-color var(--rio-motion-fast-effects);
82
+ border-color var(--rio-motion-fast-effects),
83
+ var(--rio-spring-targets);
78
84
  }
79
85
 
80
86
  /* States */
@@ -196,8 +202,10 @@
196
202
  /* The track is 32px tall, below the spec's 48px minimum target. */
197
203
  .rio-switch__touch {
198
204
  position: absolute;
199
- inset-block-start: 50%;
200
- inset-inline-start: 50%;
205
+ /* Physical, like `translate`: a logical start would flip in right-to-left layouts and push the
206
+ target off the switch. */
207
+ top: 50%;
208
+ left: 50%;
201
209
  inline-size: max(100%, 48px);
202
210
  block-size: max(100%, 48px);
203
211
  translate: -50% -50%;
@@ -0,0 +1,93 @@
1
+ /*
2
+ * M3 tooltips, plain and rich. `attachTooltip` from `@rio-m3e/core` shows the tooltip in the top
3
+ * layer as a popover and places it next to its element (`base/anchored-position.css`).
4
+ *
5
+ * Values come from Material Web's component tokens (`_md-comp-plain-tooltip.scss`,
6
+ * `_md-comp-rich-tooltip.scss`, Material 3 v34.0.21) and the spec page. Sizes the tokens leave out
7
+ * come from the M3 Design Kit: a plain tooltip at most 200px wide, a rich one at most 312px, with
8
+ * 4px between subhead and text and 12px from the text to the actions.
9
+ */
10
+
11
+ /* Wraps the element and the tooltip without a box, so the element lays out as if unwrapped. */
12
+ .rio-tooltip-trigger {
13
+ display: contents;
14
+ }
15
+
16
+ .rio-tooltip {
17
+ box-sizing: border-box;
18
+ max-inline-size: var(--rio-anchored-available-width, none);
19
+ border: 0;
20
+ overflow-wrap: break-word;
21
+ transition:
22
+ opacity var(--rio-motion-fast-effects),
23
+ overlay var(--rio-motion-fast-effects) allow-discrete,
24
+ display var(--rio-motion-fast-effects) allow-discrete;
25
+ }
26
+
27
+ /* Only while open: a `display` here would override the browser's hiding of closed popovers. */
28
+ .rio-tooltip:popover-open {
29
+ display: block;
30
+ }
31
+
32
+ .rio-tooltip:not(:popover-open) {
33
+ opacity: 0;
34
+ }
35
+
36
+ @starting-style {
37
+ .rio-tooltip:popover-open {
38
+ opacity: 0;
39
+ }
40
+ }
41
+
42
+ /* Plain: a brief label on the inverse surface, one 24px line high */
43
+
44
+ .rio-tooltip[data-variant='plain'] {
45
+ max-inline-size: min(200px, var(--rio-anchored-available-width, 200px));
46
+ min-block-size: 24px;
47
+ padding: 4px 8px;
48
+ border-radius: var(--rio-shape-corner-extra-small);
49
+ background-color: var(--rio-color-inverse-surface);
50
+ color: var(--rio-color-inverse-on-surface);
51
+ font: var(--rio-typescale-body-small);
52
+ letter-spacing: var(--rio-typescale-body-small-tracking);
53
+ }
54
+
55
+ /* Rich: a card with an optional subhead, the text, and up to two text buttons */
56
+
57
+ .rio-tooltip[data-variant='rich'] {
58
+ max-inline-size: min(312px, var(--rio-anchored-available-width, 312px));
59
+ padding: 12px 16px 8px;
60
+ border-radius: var(--rio-shape-corner-medium);
61
+ background-color: var(--rio-color-surface-container);
62
+ color: var(--rio-color-on-surface-variant);
63
+ box-shadow: var(--rio-elevation-level2);
64
+ font: var(--rio-typescale-body-medium);
65
+ letter-spacing: var(--rio-typescale-body-medium-tracking);
66
+ }
67
+
68
+ .rio-tooltip__subhead {
69
+ display: block;
70
+ margin-block-end: 4px;
71
+ font: var(--rio-typescale-title-small);
72
+ letter-spacing: var(--rio-typescale-title-small-tracking);
73
+ }
74
+
75
+ .rio-tooltip[data-variant='rich'] .rio-tooltip__supporting-text {
76
+ display: block;
77
+ padding-block-end: 4px;
78
+ }
79
+
80
+ /* The buttons sit 4px in from the container's edge. */
81
+ .rio-tooltip__actions {
82
+ display: flex;
83
+ flex-wrap: wrap;
84
+ gap: 8px;
85
+ margin-block-start: 8px;
86
+ margin-inline: -12px;
87
+ }
88
+
89
+ @media (forced-colors: active) {
90
+ .rio-tooltip {
91
+ border: 1px solid CanvasText;
92
+ }
93
+ }