actual-css 0.7.0 → 0.9.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/CHANGELOG.md +217 -0
- package/README.md +13 -11
- package/components.json +668 -0
- package/dist/actual.css +166 -37
- package/dist/actual.full.css +1087 -643
- package/dist/actual.full.js +1 -1
- package/dist/actual.full.min.css +1 -1
- package/dist/actual.js +1 -1
- package/dist/actual.min.css +1 -1
- package/docs/design-notes/accessibility-decisions.md +33 -0
- package/docs/design-notes/browser-support.md +62 -30
- package/docs/design-notes/column-layout.md +3 -3
- package/docs/design-notes/css-anchor.md +75 -21
- package/docs/design-notes/enhancement-contract.md +14 -6
- package/docs/design-notes/grid.md +4 -4
- package/docs/design-notes/naming.md +6 -1
- package/docs/design-notes/platform-alignment.md +53 -7
- package/docs/design-notes/popover-manual-poc.md +105 -25
- package/docs/design-notes/surface-reparenting.md +56 -47
- package/docs/design-notes/tooltip-coordinate-space.md +132 -0
- package/docs/design-notes/widget-primitives.md +7 -4
- package/docs/pages/components/accordion.md +41 -2
- package/docs/pages/components/alert.md +85 -30
- package/docs/pages/components/app-navigation.md +6 -0
- package/docs/pages/components/avatar.md +6 -6
- package/docs/pages/components/badge.md +101 -16
- package/docs/pages/components/button.md +16 -8
- package/docs/pages/components/card.md +10 -10
- package/docs/pages/components/dialog.md +21 -6
- package/docs/pages/components/drawer.md +13 -0
- package/docs/pages/components/fab.md +4 -3
- package/docs/pages/components/list.md +42 -4
- package/docs/pages/components/navbar.md +17 -1
- package/docs/pages/components/pagination.md +2 -2
- package/docs/pages/components/skeleton.md +3 -1
- package/docs/pages/components/steps.md +19 -0
- package/docs/pages/components/table.md +3 -2
- package/docs/pages/enhancements/flyout.md +82 -38
- package/docs/pages/enhancements/overview.md +25 -0
- package/docs/pages/enhancements/status-bar.md +1 -1
- package/docs/pages/enhancements/tabs.md +45 -37
- package/docs/pages/enhancements/tooltip.md +36 -10
- package/docs/pages/examples/overview.md +38 -5
- package/docs/pages/forms/choice-cards.md +11 -6
- package/docs/pages/forms/combobox.md +1 -1
- package/docs/pages/forms/floating-field.md +4 -3
- package/docs/pages/forms/joined-controls.md +22 -0
- package/docs/pages/forms/otp.md +2 -1
- package/docs/pages/forms/overview.md +1 -1
- package/docs/pages/forms/states-and-sizes.md +16 -18
- package/docs/pages/forms/sticky-actions.md +2 -2
- package/docs/pages/forms/switch.md +1 -1
- package/docs/pages/foundations/philosophy.md +57 -26
- package/docs/pages/foundations/theming.md +2 -2
- package/docs/pages/foundations/tokens.md +118 -21
- package/docs/pages/foundations/typography.md +35 -14
- package/docs/pages/guides/introduction.md +11 -9
- package/docs/pages/guides/progressive-enhancement.md +115 -30
- package/docs/pages/guides/tailwind.md +3 -2
- package/docs/pages/layout/media.md +1 -1
- package/docs/pages/layout/overview.md +11 -1
- package/docs/pages/layout/scroll-snap.md +3 -3
- package/docs/pages/layout/stack.md +17 -9
- package/docs/pages/patterns/icon-input.md +4 -2
- package/docs/pages/patterns/icon-slot.md +2 -0
- package/docs/pages/patterns/intent-nesting.md +1 -1
- package/docs/pages/utilities/overview.md +23 -2
- package/docs/pages/utilities/sizing-wrapping.md +1 -1
- package/docs/pages/utilities/spacing.md +10 -8
- package/llms.txt +2 -1
- package/package.json +159 -149
- package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
- package/src/css/components/accordion.css +42 -3
- package/src/css/components/alert.css +25 -13
- package/src/css/components/app-nav.css +18 -8
- package/src/css/components/avatar.css +13 -1
- package/src/css/components/badge.css +78 -12
- package/src/css/components/breadcrumb.css +2 -3
- package/src/css/components/button.css +9 -16
- package/src/css/components/card.css +9 -2
- package/src/css/components/chat.css +1 -0
- package/src/css/components/data-list.css +1 -1
- package/src/css/components/drawer.css +101 -32
- package/src/css/components/fab.css +0 -6
- package/src/css/components/flyout.css +35 -98
- package/src/css/components/index.css +0 -1
- package/src/css/components/indicator.css +21 -0
- package/src/css/components/key.css +10 -0
- package/src/css/components/list.css +40 -4
- package/src/css/components/menu.css +2 -1
- package/src/css/components/modal.css +48 -33
- package/src/css/components/navbar.css +37 -4
- package/src/css/components/pagination.css +1 -0
- package/src/css/components/skeleton.css +15 -4
- package/src/css/components/spinner.css +6 -6
- package/src/css/components/status-bar.css +0 -5
- package/src/css/components/steps.css +45 -59
- package/src/css/components/tab.css +29 -9
- package/src/css/components/table.css +13 -5
- package/src/css/components/tooltip.css +36 -40
- package/src/css/core/base.css +23 -1
- package/src/css/core/intents.css +17 -6
- package/src/css/core/theme.css +27 -0
- package/src/css/core/tokens.css +35 -15
- package/src/css/core/variants.css +64 -15
- package/src/css/forms/choice-card.css +4 -4
- package/src/css/forms/choice.css +76 -29
- package/src/css/forms/control.css +2 -1
- package/src/css/forms/floating-field.css +3 -3
- package/src/css/forms/form.css +4 -3
- package/src/css/forms/input-icon.css +20 -14
- package/src/css/forms/native.css +6 -1
- package/src/css/forms/otp.css +9 -1
- package/src/css/forms/range.css +2 -0
- package/src/css/forms/select.css +29 -9
- package/src/css/forms/switch.css +28 -8
- package/src/css/layout/app-layout.css +1 -1
- package/src/css/layout/grid.css +50 -76
- package/src/css/layout/scroll-snap.css +11 -7
- package/src/css/layout/scroller.css +3 -4
- package/src/css/layout/stack.css +2 -2
- package/src/css/typography/fluid.css +4 -3
- package/src/css/typography/prose.css +6 -5
- package/src/css/utilities/base.css +2 -1
- package/src/css/utilities/extra.css +20 -17
- package/src/js/context-menu.js +27 -26
- package/src/js/dialog.js +2 -1
- package/src/js/dismiss.js +5 -0
- package/src/js/enhance.js +34 -2
- package/src/js/enhancement-loader.js +36 -43
- package/src/js/events.js +0 -3
- package/src/js/filter.js +0 -1
- package/src/js/floating-space.js +42 -0
- package/src/js/flyout.js +9 -44
- package/src/js/focus-group.js +7 -0
- package/src/js/focus.js +6 -0
- package/src/js/menu.js +10 -1
- package/src/js/selectors.js +5 -9
- package/src/js/status.js +6 -5
- package/src/js/surface.js +97 -139
- package/src/js/tab.js +6 -1
- package/src/js/tooltip.js +150 -51
- package/src/js/validation.js +1 -1
- package/src/css/components/surface.css +0 -49
|
@@ -9,8 +9,10 @@
|
|
|
9
9
|
* --badge-size
|
|
10
10
|
* --badge-pad-x
|
|
11
11
|
* --badge-font-size
|
|
12
|
+
* --badge-icon-size
|
|
12
13
|
* --badge-dot-size
|
|
13
14
|
* --badge-radius
|
|
15
|
+
* --badge-dismiss-icon-size
|
|
14
16
|
*
|
|
15
17
|
* Internal:
|
|
16
18
|
* --badge-default-bg
|
|
@@ -26,6 +28,7 @@
|
|
|
26
28
|
:where(.badge) {
|
|
27
29
|
--intent: initial;
|
|
28
30
|
--intent-fg: initial;
|
|
31
|
+
--intent-soft-fg: initial;
|
|
29
32
|
--ui-bg: initial;
|
|
30
33
|
--ui-fg: initial;
|
|
31
34
|
--ui-border: initial;
|
|
@@ -33,10 +36,14 @@
|
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
.badge {
|
|
36
|
-
--badge-size:
|
|
37
|
-
|
|
38
|
-
|
|
39
|
+
--badge-size: 1.5rem;
|
|
40
|
+
/* A third of the height keeps the pill proportional across local sizes
|
|
41
|
+
without coupling it to the label typography. */
|
|
42
|
+
--badge-pad-x: calc(var(--badge-size) / 3);
|
|
43
|
+
--badge-font-size: var(--font-size-sm);
|
|
44
|
+
--badge-icon-size: 1em;
|
|
39
45
|
--badge-dot-size: 0.5rem;
|
|
46
|
+
--badge-dismiss-icon-size: 0.875em;
|
|
40
47
|
--badge-default-bg: var(--surface-subtle);
|
|
41
48
|
--badge-default-fg: var(--intent, var(--text));
|
|
42
49
|
--badge-default-border: var(--border);
|
|
@@ -49,7 +56,11 @@
|
|
|
49
56
|
display: inline-flex;
|
|
50
57
|
align-items: center;
|
|
51
58
|
justify-content: center;
|
|
59
|
+
vertical-align: middle;
|
|
52
60
|
gap: 0.35em;
|
|
61
|
+
/* Content-sized: a flex parent must not squeeze the pill and push its
|
|
62
|
+
nowrap label outside the background. */
|
|
63
|
+
flex: none;
|
|
53
64
|
inline-size: fit-content;
|
|
54
65
|
max-inline-size: 100%;
|
|
55
66
|
min-block-size: var(--badge-size);
|
|
@@ -59,7 +70,7 @@
|
|
|
59
70
|
background: var(--badge-bg);
|
|
60
71
|
color: var(--badge-fg);
|
|
61
72
|
font-size: var(--badge-font-size);
|
|
62
|
-
font-weight: var(--font-weight-
|
|
73
|
+
font-weight: var(--font-weight-medium);
|
|
63
74
|
line-height: 1;
|
|
64
75
|
text-decoration: none;
|
|
65
76
|
white-space: nowrap;
|
|
@@ -70,6 +81,31 @@
|
|
|
70
81
|
box-shadow var(--duration);
|
|
71
82
|
}
|
|
72
83
|
|
|
84
|
+
.badge.sm {
|
|
85
|
+
--badge-size: 1.25rem;
|
|
86
|
+
--badge-font-size: var(--font-size-xs);
|
|
87
|
+
--badge-dot-size: 0.375rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.badge.lg {
|
|
91
|
+
--badge-size: 1.875rem;
|
|
92
|
+
--badge-font-size: var(--font-size-md);
|
|
93
|
+
--badge-dot-size: 0.625rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/* Decorative children follow the badge's optical size. Text and interactive
|
|
97
|
+
dismiss buttons retain their own boxes. */
|
|
98
|
+
.badge > :where(svg, img, [aria-hidden="true"]:not(.dot)) {
|
|
99
|
+
flex: none;
|
|
100
|
+
/* The shared rule maps --badge-icon-size onto font-size. One em is therefore
|
|
101
|
+
the requested optical size; reusing the em-valued hook here would apply its
|
|
102
|
+
factor a second time. */
|
|
103
|
+
inline-size: 1em;
|
|
104
|
+
block-size: 1em;
|
|
105
|
+
font-size: var(--badge-icon-size);
|
|
106
|
+
line-height: 1;
|
|
107
|
+
}
|
|
108
|
+
|
|
73
109
|
/* Soft by default. Local defaults remain behind --ui-* in the resolution
|
|
74
110
|
chain, so explicit shared treatments compose without selector exclusions. */
|
|
75
111
|
@supports (color: color-mix(in oklch, red, white)) {
|
|
@@ -85,10 +121,9 @@
|
|
|
85
121
|
var(--surface) var(--soft-border-mix),
|
|
86
122
|
var(--intent, var(--neutral))
|
|
87
123
|
);
|
|
88
|
-
--badge-default-fg:
|
|
89
|
-
|
|
90
|
-
var(--text),
|
|
91
|
-
var(--intent, var(--text)) var(--soft-fg-mix)
|
|
124
|
+
--badge-default-fg: var(
|
|
125
|
+
--intent-soft-fg,
|
|
126
|
+
color-mix(in oklab, var(--text), var(--intent, var(--text)) var(--soft-fg-mix))
|
|
92
127
|
);
|
|
93
128
|
/* @sync soft-recipe */
|
|
94
129
|
}
|
|
@@ -105,12 +140,22 @@
|
|
|
105
140
|
block-size: var(--badge-size);
|
|
106
141
|
}
|
|
107
142
|
|
|
143
|
+
/* The shared .dot separator doubles as a status dot inside a badge.
|
|
144
|
+
currentColor follows the badge's resolved ink, so the dot matches a soft
|
|
145
|
+
intent without repeating the intent class on it. */
|
|
146
|
+
.badge > .dot {
|
|
147
|
+
background-color: currentColor;
|
|
148
|
+
}
|
|
149
|
+
|
|
108
150
|
.badge > button {
|
|
109
151
|
display: inline-grid;
|
|
110
152
|
place-items: center;
|
|
111
153
|
flex: 0 0 auto;
|
|
112
|
-
|
|
113
|
-
|
|
154
|
+
/* WCAG 2.5.8 floors pointer targets at 24 CSS px. Density and local size may
|
|
155
|
+
keep the badge visually compact through the negative margins, but never
|
|
156
|
+
shrink its action target below that literal floor. */
|
|
157
|
+
min-inline-size: max(var(--badge-size), 24px);
|
|
158
|
+
min-block-size: max(var(--badge-size), 24px);
|
|
114
159
|
margin-block: -0.25rem;
|
|
115
160
|
margin-inline-start: -0.15em;
|
|
116
161
|
margin-inline-end: -0.45em;
|
|
@@ -124,6 +169,21 @@
|
|
|
124
169
|
cursor: pointer;
|
|
125
170
|
}
|
|
126
171
|
|
|
172
|
+
/* Dismiss mark — an empty dismiss button paints its own X from the shared
|
|
173
|
+
--icon-close mask, so no icon font or text glyph is required in the markup
|
|
174
|
+
(same contract as .alert-dismiss and .dialog-close). A mask box is a block
|
|
175
|
+
box, so place-items centers the ink itself; a text glyph would only center
|
|
176
|
+
its line box and land the ink low, because a lowercase x has no descender
|
|
177
|
+
and only x-height of ascent. A button with its own content keeps it. */
|
|
178
|
+
.badge > button:empty::before {
|
|
179
|
+
content: "";
|
|
180
|
+
inline-size: var(--badge-dismiss-icon-size);
|
|
181
|
+
block-size: var(--badge-dismiss-icon-size);
|
|
182
|
+
background: currentColor;
|
|
183
|
+
-webkit-mask: var(--icon-close) center / contain no-repeat;
|
|
184
|
+
mask: var(--icon-close) center / contain no-repeat;
|
|
185
|
+
}
|
|
186
|
+
|
|
127
187
|
.badge > button:not(:disabled):hover {
|
|
128
188
|
background: var(--hover-overlay);
|
|
129
189
|
}
|
|
@@ -156,7 +216,14 @@
|
|
|
156
216
|
background: var(--ui-hover-bg, var(--badge-default-hover-bg));
|
|
157
217
|
}
|
|
158
218
|
|
|
159
|
-
|
|
219
|
+
/* Solid and empty badges keep the overlay darkening on hover; soft badges do
|
|
220
|
+
not — their hover fill (--soft-hover-alpha) is already the state change, and
|
|
221
|
+
stacking a second darkening drags the soft ink below its resting contrast.
|
|
222
|
+
Soft here means :not(.solid, .outline, :empty), mirroring the soft hover-bg
|
|
223
|
+
recipe above. */
|
|
224
|
+
:where(a.badge, button.badge):is(.solid, :empty):not(
|
|
225
|
+
:where(:disabled, [aria-disabled="true"])
|
|
226
|
+
):hover {
|
|
160
227
|
/* Huge inset spread paints the overlay over the pill radius (see button.css). */
|
|
161
228
|
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
162
229
|
}
|
|
@@ -188,6 +255,5 @@ a.badge[aria-disabled="true"] {
|
|
|
188
255
|
@media print {
|
|
189
256
|
.badge {
|
|
190
257
|
border: var(--border-width) solid CanvasText;
|
|
191
|
-
break-inside: avoid;
|
|
192
258
|
}
|
|
193
259
|
}
|
|
@@ -33,8 +33,7 @@
|
|
|
33
33
|
text-decoration: none;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
.breadcrumb a:not([aria-current]):hover
|
|
37
|
-
.breadcrumb a[aria-current="false"]:hover {
|
|
36
|
+
.breadcrumb a:not([aria-current="page"]):hover {
|
|
38
37
|
color: var(--text);
|
|
39
38
|
text-decoration: underline;
|
|
40
39
|
}
|
|
@@ -43,7 +42,7 @@
|
|
|
43
42
|
border-radius: var(--radius-sm);
|
|
44
43
|
}
|
|
45
44
|
|
|
46
|
-
.breadcrumb :where(li, a, span)
|
|
45
|
+
.breadcrumb :where(li, a, span)[aria-current="page"] {
|
|
47
46
|
color: var(--text);
|
|
48
47
|
font-weight: var(--font-weight-medium);
|
|
49
48
|
}
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
:where(.btn) {
|
|
40
40
|
--intent: initial;
|
|
41
41
|
--intent-fg: initial;
|
|
42
|
+
--intent-soft-fg: initial;
|
|
42
43
|
--ui-bg: initial;
|
|
43
44
|
--ui-fg: initial;
|
|
44
45
|
--ui-border: initial;
|
|
@@ -110,13 +111,16 @@
|
|
|
110
111
|
--ui-border: transparent;
|
|
111
112
|
}
|
|
112
113
|
|
|
113
|
-
/* Solid
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
114
|
+
/* Solid keeps the overlay darkening on hover; soft/outline/ghost/link stay
|
|
115
|
+
flat. The soft hover is its own fill change (--soft-hover-alpha) — a second
|
|
116
|
+
overlay on top would double the fill step and drag its ink below the resting
|
|
117
|
+
contrast on every intent. Routed through a variable instead of a
|
|
118
|
+
:not(.outline, .ghost, .link) exclusion so the hover rule below only has to
|
|
119
|
+
state the interactive element list once. */
|
|
117
120
|
.btn.outline,
|
|
118
121
|
.btn.ghost,
|
|
119
|
-
.btn.link
|
|
122
|
+
.btn.link,
|
|
123
|
+
.btn.soft {
|
|
120
124
|
--btn-hover-overlay: transparent;
|
|
121
125
|
}
|
|
122
126
|
|
|
@@ -239,14 +243,3 @@ a.btn[aria-disabled="true"] {
|
|
|
239
243
|
border-color: GrayText;
|
|
240
244
|
}
|
|
241
245
|
}
|
|
242
|
-
|
|
243
|
-
@media print {
|
|
244
|
-
:is(button, input[type="button"], input[type="submit"], input[type="reset"]).btn {
|
|
245
|
-
display: none !important;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
a.btn[href^="http"]::after {
|
|
249
|
-
content: " (" attr(href) ")";
|
|
250
|
-
font-size: 0.875em;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
@@ -26,8 +26,15 @@
|
|
|
26
26
|
* --busy-overlay-bg
|
|
27
27
|
*/
|
|
28
28
|
/* Intent boundary — inherited intents do not tint this component.
|
|
29
|
-
Local intent classes still win because :where() has zero specificity.
|
|
29
|
+
Local intent classes still win because :where() has zero specificity.
|
|
30
|
+
The intent tokens are reset alongside --ui-*: the shared variants
|
|
31
|
+
(.soft/.solid/.outline/.surface) read --intent / --intent-fg / --intent-soft-fg
|
|
32
|
+
when they paint this surface, so an ancestor intent would otherwise leak
|
|
33
|
+
through a variant that has no local intent class. */
|
|
30
34
|
:where(.card) {
|
|
35
|
+
--intent: initial;
|
|
36
|
+
--intent-fg: initial;
|
|
37
|
+
--intent-soft-fg: initial;
|
|
31
38
|
--ui-bg: initial;
|
|
32
39
|
--ui-fg: initial;
|
|
33
40
|
--ui-border: initial;
|
|
@@ -100,7 +107,7 @@
|
|
|
100
107
|
box-shadow: var(--shadow);
|
|
101
108
|
}
|
|
102
109
|
.card.subtle {
|
|
103
|
-
|
|
110
|
+
--card-bg: var(--surface-subtle);
|
|
104
111
|
}
|
|
105
112
|
.card.compact {
|
|
106
113
|
--card-pad: var(--space-20);
|
|
@@ -26,15 +26,24 @@ dialog.drawer {
|
|
|
26
26
|
|
|
27
27
|
/* Keep the surface anchored to the viewport in the top layer. This also
|
|
28
28
|
provides the containing block for the absolutely positioned close button.
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
|
|
30
|
+
The panel reaches both viewport edges, so it has to follow the space that
|
|
31
|
+
is actually on screen: --viewport-block resolves to 100dvh and tracks a
|
|
32
|
+
mobile browser's collapsing URL bar. A percentage resolves against the
|
|
33
|
+
layout viewport instead, which those browsers size as if the bar were
|
|
34
|
+
hidden — that put the footer below the fold for as long as it showed. The
|
|
35
|
+
modal pins the small viewport for the opposite reason: it floats, so a
|
|
36
|
+
stable height beats a panel that breathes while the bar moves.
|
|
37
|
+
|
|
38
|
+
An embedded preview that fakes a viewport overrides --viewport-block on
|
|
39
|
+
its own shell; the component does not degrade to a percentage for it. */
|
|
31
40
|
position: fixed;
|
|
32
41
|
inset-block-start: 0;
|
|
33
42
|
|
|
34
43
|
inline-size: min(calc(var(--viewport-inline) * 0.9), var(--drawer-size));
|
|
35
|
-
block-size:
|
|
44
|
+
block-size: var(--viewport-block);
|
|
36
45
|
max-inline-size: none;
|
|
37
|
-
max-block-size:
|
|
46
|
+
max-block-size: var(--viewport-block);
|
|
38
47
|
margin: 0;
|
|
39
48
|
padding: var(--drawer-pad);
|
|
40
49
|
border: 0;
|
|
@@ -62,8 +71,18 @@ dialog.drawer[open] {
|
|
|
62
71
|
transform var(--drawer-motion-duration) var(--ease-enter);
|
|
63
72
|
}
|
|
64
73
|
|
|
65
|
-
/*
|
|
66
|
-
|
|
74
|
+
/* A form is the drawer's content wrapper when actions need method="dialog".
|
|
75
|
+
Span the drawer tracks so the form can keep the header at the top, let the
|
|
76
|
+
body absorb the available height, and keep the footer at the bottom. */
|
|
77
|
+
dialog.drawer > form {
|
|
78
|
+
display: grid;
|
|
79
|
+
grid-row: 1 / -1;
|
|
80
|
+
grid-template-rows: auto minmax(0, 1fr) auto;
|
|
81
|
+
gap: var(--space-40);
|
|
82
|
+
min-block-size: 100%;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* The shared backdrop token keeps drawer and modal scrims consistent. */
|
|
67
86
|
dialog.drawer::backdrop {
|
|
68
87
|
background: var(--backdrop-fill);
|
|
69
88
|
opacity: 0;
|
|
@@ -96,10 +115,16 @@ dialog.drawer[open]::backdrop {
|
|
|
96
115
|
}
|
|
97
116
|
}
|
|
98
117
|
|
|
99
|
-
/*
|
|
100
|
-
|
|
118
|
+
/* The body region absorbs the panel's height and scrolls. A drawer is a
|
|
119
|
+
full-height shell by definition, so it always scrolls its body — unlike the
|
|
120
|
+
modal, which sizes to its content and opts in with .scrollable. Header and
|
|
121
|
+
footer keep their own tracks; .drawer-close is named because a generic
|
|
122
|
+
exclusion list is easier to trust than its `position: absolute`. */
|
|
123
|
+
dialog.drawer > :not(:is(header, footer, form, .drawer-close)),
|
|
124
|
+
dialog.drawer > form > :not(:is(header, footer, .drawer-close)) {
|
|
101
125
|
min-block-size: 0;
|
|
102
|
-
overflow
|
|
126
|
+
overflow: auto;
|
|
127
|
+
overscroll-behavior: contain;
|
|
103
128
|
}
|
|
104
129
|
|
|
105
130
|
@supports (transition-behavior: allow-discrete) {
|
|
@@ -128,33 +153,33 @@ dialog.drawer nav {
|
|
|
128
153
|
}
|
|
129
154
|
|
|
130
155
|
/* Safari before 16.4 does not support :dir(); keep the ancestor-attribute
|
|
131
|
-
path as the
|
|
156
|
+
path as the Degraded-tier fallback. The :dir() rules are dropped on their
|
|
157
|
+
own there, so they need no wrapper. :dir() follows inherited HTML
|
|
158
|
+
directionality and handles nested direction changes; the explicit ltr rules
|
|
159
|
+
neutralize an outer RTL fallback selector. Each specificity group keeps the
|
|
160
|
+
fallback before the :dir() rules. */
|
|
132
161
|
[dir="rtl"] dialog.drawer {
|
|
133
162
|
--drawer-closed-translate: 100%;
|
|
134
163
|
}
|
|
135
164
|
|
|
136
|
-
|
|
165
|
+
dialog.drawer:dir(ltr) {
|
|
137
166
|
--drawer-closed-translate: -100%;
|
|
138
167
|
}
|
|
139
168
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
dialog.drawer:dir(ltr) {
|
|
144
|
-
--drawer-closed-translate: -100%;
|
|
145
|
-
}
|
|
169
|
+
dialog.drawer:dir(rtl) {
|
|
170
|
+
--drawer-closed-translate: 100%;
|
|
171
|
+
}
|
|
146
172
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
173
|
+
[dir="rtl"] dialog.drawer[data-side="end"] {
|
|
174
|
+
--drawer-closed-translate: -100%;
|
|
175
|
+
}
|
|
150
176
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
177
|
+
dialog.drawer[data-side="end"]:dir(ltr) {
|
|
178
|
+
--drawer-closed-translate: 100%;
|
|
179
|
+
}
|
|
154
180
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
}
|
|
181
|
+
dialog.drawer[data-side="end"]:dir(rtl) {
|
|
182
|
+
--drawer-closed-translate: -100%;
|
|
158
183
|
}
|
|
159
184
|
|
|
160
185
|
@media (prefers-reduced-motion: no-preference) {
|
|
@@ -163,15 +188,59 @@ dialog.drawer nav {
|
|
|
163
188
|
}
|
|
164
189
|
}
|
|
165
190
|
|
|
166
|
-
/* The
|
|
167
|
-
|
|
168
|
-
inline room so the title never runs under it. */
|
|
191
|
+
/* The header reserves inline room so the title never runs under the close
|
|
192
|
+
control. */
|
|
169
193
|
dialog.drawer > header,
|
|
170
194
|
dialog.drawer > form > header {
|
|
171
195
|
padding-inline-end: calc(var(--control-size) + var(--space-30));
|
|
172
196
|
}
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
197
|
+
|
|
198
|
+
/* Close control, owned here so a drawer-only build does not depend on
|
|
199
|
+
modal.css. Mirrors the modal's .dialog-close placement: absolute at the
|
|
200
|
+
panel's top inline-end, out of the content flow. */
|
|
201
|
+
.drawer-close {
|
|
202
|
+
--drawer-close-icon-size: 1rem;
|
|
203
|
+
|
|
204
|
+
display: inline-grid;
|
|
205
|
+
place-items: center;
|
|
206
|
+
padding: 0;
|
|
207
|
+
border: 0;
|
|
208
|
+
border-radius: var(--radius);
|
|
209
|
+
color: var(--text-muted);
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
|
|
212
|
+
position: absolute;
|
|
213
|
+
z-index: 1;
|
|
214
|
+
inset-block-start: var(--space-20);
|
|
215
|
+
inset-inline-end: var(--space-20);
|
|
216
|
+
|
|
217
|
+
inline-size: var(--control-size);
|
|
218
|
+
block-size: var(--control-size);
|
|
219
|
+
|
|
220
|
+
background: var(--surface-subtle);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.drawer-close::before {
|
|
224
|
+
content: "";
|
|
225
|
+
inline-size: var(--drawer-close-icon-size);
|
|
226
|
+
block-size: var(--drawer-close-icon-size);
|
|
227
|
+
background: currentColor;
|
|
228
|
+
-webkit-mask: var(--icon-close) center / contain no-repeat;
|
|
229
|
+
mask: var(--icon-close) center / contain no-repeat;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
@media (hover: hover) {
|
|
233
|
+
.drawer-close:hover {
|
|
234
|
+
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
235
|
+
color: var(--text);
|
|
176
236
|
}
|
|
177
237
|
}
|
|
238
|
+
|
|
239
|
+
.drawer-close:active {
|
|
240
|
+
box-shadow: inset 0 0 0 999px var(--hover-overlay);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
.drawer-close:focus-visible {
|
|
244
|
+
outline: 2px solid transparent;
|
|
245
|
+
box-shadow: var(--focus-ring-shadow);
|
|
246
|
+
}
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* Flyout — floating panel for actions and links.
|
|
3
3
|
*
|
|
4
|
-
* JavaScript
|
|
5
|
-
*
|
|
6
|
-
* panel appearance, item styles,
|
|
4
|
+
* JavaScript promotes the panel to the top layer and chooses viewport or
|
|
5
|
+
* document coordinates from its anchor. It also owns state (.is-open), focus,
|
|
6
|
+
* and keyboard behavior. CSS owns the surfaced panel appearance, item styles,
|
|
7
|
+
* and animation.
|
|
7
8
|
*
|
|
8
|
-
* Runtime
|
|
9
|
-
* .is-sheet marks "auto" resolved to bottom-sheet presentation on coarse
|
|
10
|
-
* pointers. Authors opt into mobile behavior with
|
|
11
|
-
* [data-flyout-mobile="sheet|auto|none|anchored"].
|
|
9
|
+
* Runtime marker (managed by surface.js): .is-open marks an open flyout.
|
|
12
10
|
* --surface-anchor-width is set by surface.js while an open flyout is positioned.
|
|
13
11
|
*
|
|
14
12
|
* Public hooks:
|
|
@@ -17,7 +15,7 @@
|
|
|
17
15
|
*
|
|
18
16
|
* Internal:
|
|
19
17
|
* --menu-item-size (derived from --control-size for menu rows)
|
|
20
|
-
* --available-height (measured by
|
|
18
|
+
* --available-height (measured by the positioner)
|
|
21
19
|
* --surface-anchor-width (measured by surface.js)
|
|
22
20
|
*/
|
|
23
21
|
|
|
@@ -44,8 +42,8 @@
|
|
|
44
42
|
--menu-item-size: var(--control-size);
|
|
45
43
|
z-index: var(--z-menu);
|
|
46
44
|
/* No-JS fallback: below the trigger inside the relative .flyout-trigger.
|
|
47
|
-
surface.js
|
|
48
|
-
|
|
45
|
+
surface.js replaces this with the anchor's coupled position/coordinate
|
|
46
|
+
mode; this only covers a flyout revealed without the runtime. */
|
|
49
47
|
position: absolute;
|
|
50
48
|
inset-block-start: calc(100% + var(--space-10));
|
|
51
49
|
inset-inline-start: 0;
|
|
@@ -73,101 +71,53 @@
|
|
|
73
71
|
box-shadow: var(--shadow-popout);
|
|
74
72
|
list-style: none;
|
|
75
73
|
opacity: 1;
|
|
76
|
-
/*
|
|
77
|
-
|
|
78
|
-
below adds its own translate motion. */
|
|
74
|
+
/* Flyouts fade in and out; scaling around the center would detach the
|
|
75
|
+
anchored edge from its trigger on wide panels. */
|
|
79
76
|
transition: opacity var(--duration-fast) ease;
|
|
80
77
|
}
|
|
81
78
|
|
|
82
|
-
/*
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
reset would strand a permanently visible panel at inset: auto. Popover is
|
|
87
|
-
not part of Actual's Minimal contract — engines inside the Minimal range
|
|
88
|
-
may lack it — so the gate is about the capability, not the floor. Kept
|
|
89
|
-
scoped to Actual surfaces so bare [popover] elements retain their platform
|
|
79
|
+
/* A popover enters the top layer with UA geometry that conflicts with anchored
|
|
80
|
+
positioning. surface.js sets popover="manual" on every surface it manages,
|
|
81
|
+
so this is the transport's own geometry rather than an opt-in. Kept scoped to
|
|
82
|
+
Actual surfaces so bare [popover] elements retain their platform
|
|
90
83
|
presentation. */
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
}
|
|
84
|
+
.flyout[popover] {
|
|
85
|
+
position: fixed;
|
|
86
|
+
inset: auto;
|
|
87
|
+
margin: 0;
|
|
88
|
+
}
|
|
97
89
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
display: none;
|
|
109
|
-
opacity: 0;
|
|
110
|
-
}
|
|
90
|
+
/* A presentation class may own its *visible* display type, but must not
|
|
91
|
+
override the platform's hidden state. The UA closes a popover with
|
|
92
|
+
display: none, and an author declaration beats the UA origin whatever the
|
|
93
|
+
specificity — so .flyout's own display: grid would leave a closed popover
|
|
94
|
+
painted at its static position. The opacity is the exit target: with nothing
|
|
95
|
+
to animate, the overlay/display allow-discrete transition below would hold a
|
|
96
|
+
fully opaque panel in the top layer and then snap it away. */
|
|
97
|
+
.flyout[popover]:not(:popover-open) {
|
|
98
|
+
display: none;
|
|
99
|
+
opacity: 0;
|
|
111
100
|
}
|
|
112
101
|
|
|
113
|
-
/*
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
a plain selector list an unsupported member would invalidate the whole rule.
|
|
117
|
-
Keep it inside :is()/:where() or gate it. */
|
|
118
|
-
.flyout:is(.is-open, :popover-open):not(.is-sheet) {
|
|
102
|
+
/* Both lifecycles in one rule: .is-open for a surface.js panel, :popover-open
|
|
103
|
+
for one an author drives natively with popovertarget. */
|
|
104
|
+
.flyout:is(.is-open, :popover-open) {
|
|
119
105
|
inset: auto;
|
|
120
106
|
}
|
|
121
107
|
|
|
122
|
-
/*
|
|
123
|
-
|
|
108
|
+
/* @starting-style is above the floor (Safari 17.5), so where it is missing the
|
|
109
|
+
entry simply has no animated origin and the panel appears at full opacity. */
|
|
124
110
|
@starting-style {
|
|
125
111
|
.flyout:is(.is-open, :popover-open) {
|
|
126
112
|
opacity: 0;
|
|
127
113
|
}
|
|
128
114
|
}
|
|
129
115
|
|
|
130
|
-
/* Bottom-sheet presentation. surface.js toggles .is-sheet when the flyout is
|
|
131
|
-
opened on coarse pointer / narrow viewports. */
|
|
132
|
-
.flyout.is-sheet {
|
|
133
|
-
--menu-item-size: var(--control-size-lg);
|
|
134
|
-
position: fixed;
|
|
135
|
-
inset-inline: 0;
|
|
136
|
-
inset-block-start: auto;
|
|
137
|
-
inset-block-end: 0;
|
|
138
|
-
inline-size: auto;
|
|
139
|
-
min-inline-size: 0;
|
|
140
|
-
max-inline-size: none;
|
|
141
|
-
max-block-size: min(calc(var(--viewport-block) * 0.75), 32rem);
|
|
142
|
-
overflow: auto;
|
|
143
|
-
grid-template-columns: 1fr;
|
|
144
|
-
padding: max(var(--space-30), env(safe-area-inset-top)) var(--space-30)
|
|
145
|
-
max(var(--space-30), env(safe-area-inset-bottom));
|
|
146
|
-
border-end-start-radius: 0;
|
|
147
|
-
border-end-end-radius: 0;
|
|
148
|
-
/* Sheet motion is a directional slide from the viewport edge; it replaces
|
|
149
|
-
the base fade-only recipe. */
|
|
150
|
-
transition:
|
|
151
|
-
opacity var(--duration-fast) ease,
|
|
152
|
-
translate var(--duration-fast) ease;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
116
|
.flyout[hidden] {
|
|
156
117
|
display: none;
|
|
157
118
|
opacity: 0;
|
|
158
119
|
}
|
|
159
120
|
|
|
160
|
-
.flyout.is-sheet[hidden] {
|
|
161
|
-
translate: 0 var(--space-30);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/* compat-ok: forgiving :is() inside @starting-style, as above. */
|
|
165
|
-
@starting-style {
|
|
166
|
-
.flyout.is-sheet:is(.is-open, :popover-open) {
|
|
167
|
-
translate: 0 var(--space-30);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
|
|
171
121
|
/* display keeps the exit transition alive for the [hidden] path; overlay does
|
|
172
122
|
the same for the native popover path, where hidePopover() would otherwise
|
|
173
123
|
drop the panel out of the top layer on the first frame and let a parent
|
|
@@ -180,14 +130,6 @@
|
|
|
180
130
|
overlay var(--duration-fast) allow-discrete,
|
|
181
131
|
display var(--duration-fast) allow-discrete;
|
|
182
132
|
}
|
|
183
|
-
|
|
184
|
-
.flyout.is-sheet {
|
|
185
|
-
transition:
|
|
186
|
-
opacity var(--duration-fast) ease,
|
|
187
|
-
translate var(--duration-fast) ease,
|
|
188
|
-
overlay var(--duration-fast) allow-discrete,
|
|
189
|
-
display var(--duration-fast) allow-discrete;
|
|
190
|
-
}
|
|
191
133
|
}
|
|
192
134
|
|
|
193
135
|
.flyout > li {
|
|
@@ -203,7 +145,7 @@
|
|
|
203
145
|
margin: 0;
|
|
204
146
|
color: var(--text-muted);
|
|
205
147
|
font-size: var(--font-size-xs);
|
|
206
|
-
font-weight: var(--font-weight-
|
|
148
|
+
font-weight: var(--font-weight-semibold);
|
|
207
149
|
}
|
|
208
150
|
|
|
209
151
|
.flyout > section > ul {
|
|
@@ -221,8 +163,3 @@
|
|
|
221
163
|
/* Vertical dividers — .menu-separator (menu.css) is the canonical way to
|
|
222
164
|
separate menu items. Flyout panels that are not menus use auto grid gap
|
|
223
165
|
declared on .flyout itself. */
|
|
224
|
-
@media print {
|
|
225
|
-
.flyout {
|
|
226
|
-
display: none !important;
|
|
227
|
-
}
|
|
228
|
-
}
|