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.
Files changed (144) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +13 -11
  3. package/components.json +668 -0
  4. package/dist/actual.css +166 -37
  5. package/dist/actual.full.css +1087 -643
  6. package/dist/actual.full.js +1 -1
  7. package/dist/actual.full.min.css +1 -1
  8. package/dist/actual.js +1 -1
  9. package/dist/actual.min.css +1 -1
  10. package/docs/design-notes/accessibility-decisions.md +33 -0
  11. package/docs/design-notes/browser-support.md +62 -30
  12. package/docs/design-notes/column-layout.md +3 -3
  13. package/docs/design-notes/css-anchor.md +75 -21
  14. package/docs/design-notes/enhancement-contract.md +14 -6
  15. package/docs/design-notes/grid.md +4 -4
  16. package/docs/design-notes/naming.md +6 -1
  17. package/docs/design-notes/platform-alignment.md +53 -7
  18. package/docs/design-notes/popover-manual-poc.md +105 -25
  19. package/docs/design-notes/surface-reparenting.md +56 -47
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +7 -4
  22. package/docs/pages/components/accordion.md +41 -2
  23. package/docs/pages/components/alert.md +85 -30
  24. package/docs/pages/components/app-navigation.md +6 -0
  25. package/docs/pages/components/avatar.md +6 -6
  26. package/docs/pages/components/badge.md +101 -16
  27. package/docs/pages/components/button.md +16 -8
  28. package/docs/pages/components/card.md +10 -10
  29. package/docs/pages/components/dialog.md +21 -6
  30. package/docs/pages/components/drawer.md +13 -0
  31. package/docs/pages/components/fab.md +4 -3
  32. package/docs/pages/components/list.md +42 -4
  33. package/docs/pages/components/navbar.md +17 -1
  34. package/docs/pages/components/pagination.md +2 -2
  35. package/docs/pages/components/skeleton.md +3 -1
  36. package/docs/pages/components/steps.md +19 -0
  37. package/docs/pages/components/table.md +3 -2
  38. package/docs/pages/enhancements/flyout.md +82 -38
  39. package/docs/pages/enhancements/overview.md +25 -0
  40. package/docs/pages/enhancements/status-bar.md +1 -1
  41. package/docs/pages/enhancements/tabs.md +45 -37
  42. package/docs/pages/enhancements/tooltip.md +36 -10
  43. package/docs/pages/examples/overview.md +38 -5
  44. package/docs/pages/forms/choice-cards.md +11 -6
  45. package/docs/pages/forms/combobox.md +1 -1
  46. package/docs/pages/forms/floating-field.md +4 -3
  47. package/docs/pages/forms/joined-controls.md +22 -0
  48. package/docs/pages/forms/otp.md +2 -1
  49. package/docs/pages/forms/overview.md +1 -1
  50. package/docs/pages/forms/states-and-sizes.md +16 -18
  51. package/docs/pages/forms/sticky-actions.md +2 -2
  52. package/docs/pages/forms/switch.md +1 -1
  53. package/docs/pages/foundations/philosophy.md +57 -26
  54. package/docs/pages/foundations/theming.md +2 -2
  55. package/docs/pages/foundations/tokens.md +118 -21
  56. package/docs/pages/foundations/typography.md +35 -14
  57. package/docs/pages/guides/introduction.md +11 -9
  58. package/docs/pages/guides/progressive-enhancement.md +115 -30
  59. package/docs/pages/guides/tailwind.md +3 -2
  60. package/docs/pages/layout/media.md +1 -1
  61. package/docs/pages/layout/overview.md +11 -1
  62. package/docs/pages/layout/scroll-snap.md +3 -3
  63. package/docs/pages/layout/stack.md +17 -9
  64. package/docs/pages/patterns/icon-input.md +4 -2
  65. package/docs/pages/patterns/icon-slot.md +2 -0
  66. package/docs/pages/patterns/intent-nesting.md +1 -1
  67. package/docs/pages/utilities/overview.md +23 -2
  68. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  69. package/docs/pages/utilities/spacing.md +10 -8
  70. package/llms.txt +2 -1
  71. package/package.json +159 -149
  72. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  73. package/src/css/components/accordion.css +42 -3
  74. package/src/css/components/alert.css +25 -13
  75. package/src/css/components/app-nav.css +18 -8
  76. package/src/css/components/avatar.css +13 -1
  77. package/src/css/components/badge.css +78 -12
  78. package/src/css/components/breadcrumb.css +2 -3
  79. package/src/css/components/button.css +9 -16
  80. package/src/css/components/card.css +9 -2
  81. package/src/css/components/chat.css +1 -0
  82. package/src/css/components/data-list.css +1 -1
  83. package/src/css/components/drawer.css +101 -32
  84. package/src/css/components/fab.css +0 -6
  85. package/src/css/components/flyout.css +35 -98
  86. package/src/css/components/index.css +0 -1
  87. package/src/css/components/indicator.css +21 -0
  88. package/src/css/components/key.css +10 -0
  89. package/src/css/components/list.css +40 -4
  90. package/src/css/components/menu.css +2 -1
  91. package/src/css/components/modal.css +48 -33
  92. package/src/css/components/navbar.css +37 -4
  93. package/src/css/components/pagination.css +1 -0
  94. package/src/css/components/skeleton.css +15 -4
  95. package/src/css/components/spinner.css +6 -6
  96. package/src/css/components/status-bar.css +0 -5
  97. package/src/css/components/steps.css +45 -59
  98. package/src/css/components/tab.css +29 -9
  99. package/src/css/components/table.css +13 -5
  100. package/src/css/components/tooltip.css +36 -40
  101. package/src/css/core/base.css +23 -1
  102. package/src/css/core/intents.css +17 -6
  103. package/src/css/core/theme.css +27 -0
  104. package/src/css/core/tokens.css +35 -15
  105. package/src/css/core/variants.css +64 -15
  106. package/src/css/forms/choice-card.css +4 -4
  107. package/src/css/forms/choice.css +76 -29
  108. package/src/css/forms/control.css +2 -1
  109. package/src/css/forms/floating-field.css +3 -3
  110. package/src/css/forms/form.css +4 -3
  111. package/src/css/forms/input-icon.css +20 -14
  112. package/src/css/forms/native.css +6 -1
  113. package/src/css/forms/otp.css +9 -1
  114. package/src/css/forms/range.css +2 -0
  115. package/src/css/forms/select.css +29 -9
  116. package/src/css/forms/switch.css +28 -8
  117. package/src/css/layout/app-layout.css +1 -1
  118. package/src/css/layout/grid.css +50 -76
  119. package/src/css/layout/scroll-snap.css +11 -7
  120. package/src/css/layout/scroller.css +3 -4
  121. package/src/css/layout/stack.css +2 -2
  122. package/src/css/typography/fluid.css +4 -3
  123. package/src/css/typography/prose.css +6 -5
  124. package/src/css/utilities/base.css +2 -1
  125. package/src/css/utilities/extra.css +20 -17
  126. package/src/js/context-menu.js +27 -26
  127. package/src/js/dialog.js +2 -1
  128. package/src/js/dismiss.js +5 -0
  129. package/src/js/enhance.js +34 -2
  130. package/src/js/enhancement-loader.js +36 -43
  131. package/src/js/events.js +0 -3
  132. package/src/js/filter.js +0 -1
  133. package/src/js/floating-space.js +42 -0
  134. package/src/js/flyout.js +9 -44
  135. package/src/js/focus-group.js +7 -0
  136. package/src/js/focus.js +6 -0
  137. package/src/js/menu.js +10 -1
  138. package/src/js/selectors.js +5 -9
  139. package/src/js/status.js +6 -5
  140. package/src/js/surface.js +97 -139
  141. package/src/js/tab.js +6 -1
  142. package/src/js/tooltip.js +150 -51
  143. package/src/js/validation.js +1 -1
  144. 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: var(--density-compact-size);
37
- --badge-pad-x: 0.55em;
38
- --badge-font-size: var(--font-size-xs);
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-strong);
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: color-mix(
89
- in oklab,
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
- min-inline-size: var(--density-compact-size);
113
- min-block-size: var(--density-compact-size);
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
- :where(a.badge, button.badge):not(:where(:disabled, [aria-disabled="true"], .outline)):hover {
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):where([aria-current]:not([aria-current="false"])) {
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/soft get the overlay darkening on hover; outline/ghost/link stay flat.
114
- Routed through a variable instead of a :not(.outline, .ghost, .link)
115
- exclusion so the hover rule below only has to state the interactive
116
- element list once. */
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
- background: var(--surface-subtle);
110
+ --card-bg: var(--surface-subtle);
104
111
  }
105
112
  .card.compact {
106
113
  --card-pad: var(--space-20);
@@ -62,6 +62,7 @@
62
62
  :where(.chat-bubble) {
63
63
  --intent: initial;
64
64
  --intent-fg: initial;
65
+ --intent-soft-fg: initial;
65
66
  --ui-bg: initial;
66
67
  --ui-fg: initial;
67
68
  --ui-border: initial;
@@ -18,7 +18,7 @@
18
18
  .data-list > dt {
19
19
  min-inline-size: 0;
20
20
  margin-block-start: 0;
21
- font-weight: var(--font-weight-strong);
21
+ font-weight: var(--font-weight-semibold);
22
22
  }
23
23
 
24
24
  .data-list > dd {
@@ -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
- A percentage height resolves against the fixed containing viewport, while
30
- dynamic viewport units can be stale in embedded previews. */
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: 100%;
44
+ block-size: var(--viewport-block);
36
45
  max-inline-size: none;
37
- max-block-size: 100%;
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
- /* Backdrop color sourced from components/surface.css so it matches .modal
66
- and .surface-backdrop across the dialog family. */
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
- /* Scrollable navigation region inside the drawer. */
100
- dialog.drawer nav {
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-y: auto;
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 Minimal-tier fallback. */
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
- [dir="rtl"] dialog.drawer[data-side="end"] {
165
+ dialog.drawer:dir(ltr) {
137
166
  --drawer-closed-translate: -100%;
138
167
  }
139
168
 
140
- /* :dir() follows inherited HTML directionality and handles nested direction
141
- changes. Explicit ltr rules neutralize an outer RTL fallback selector. */
142
- @supports selector(:dir(rtl)) {
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
- dialog.drawer[data-side="end"]:dir(ltr) {
148
- --drawer-closed-translate: 100%;
149
- }
173
+ [dir="rtl"] dialog.drawer[data-side="end"] {
174
+ --drawer-closed-translate: -100%;
175
+ }
150
176
 
151
- dialog.drawer:dir(rtl) {
152
- --drawer-closed-translate: 100%;
153
- }
177
+ dialog.drawer[data-side="end"]:dir(ltr) {
178
+ --drawer-closed-translate: 100%;
179
+ }
154
180
 
155
- dialog.drawer[data-side="end"]:dir(rtl) {
156
- --drawer-closed-translate: -100%;
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 close button shares the modal's interior placement (modal.css): absolute
167
- at the panel's top inline-end, out of the content flow. The header reserves
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
- @media print {
174
- dialog.drawer[open] {
175
- display: none !important;
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
+ }
@@ -126,9 +126,3 @@
126
126
  opacity: 1;
127
127
  transform: none;
128
128
  }
129
-
130
- @media print {
131
- .fab {
132
- display: none;
133
- }
134
- }
@@ -1,14 +1,12 @@
1
1
  /*
2
2
  * Flyout — floating panel for actions and links.
3
3
  *
4
- * JavaScript owns positioning (position: fixed + left/top), state
5
- * (hidden + .is-open), focus, and keyboard behavior. CSS owns the surfaced
6
- * panel appearance, item styles, and animation.
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 markers (managed by surface.js): .is-open marks an open flyout,
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 floating.js)
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 switches an open flyout to position: fixed with measured
48
- coordinates; this only covers a flyout revealed without the runtime. */
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
- /* Anchored flyouts fade in and out; scaling around the center would detach
77
- the anchored edge from its trigger on wide panels. The sheet presentation
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
- /* Native popovers enter the top layer with UA geometry that conflicts with
83
- anchored positioning. The reset is gated on real Popover support because an
84
- attribute selector matches whether or not the engine implements Popover:
85
- without it the UA neither hides nor promotes the element, so an ungated
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
- @supports selector(:popover-open) {
92
- .flyout[popover] {
93
- position: fixed;
94
- inset: auto;
95
- margin: 0;
96
- }
84
+ .flyout[popover] {
85
+ position: fixed;
86
+ inset: auto;
87
+ margin: 0;
88
+ }
97
89
 
98
- /* A presentation class may own its *visible* display type, but must not
99
- override the platform's hidden state. The UA closes a popover with
100
- display: none, and an author declaration beats the UA origin whatever the
101
- specificity — so .flyout's own display: grid would leave a closed popover
102
- painted at its static position. Restoring both properties here also gives
103
- the native path the exit target the runtime path already has in
104
- .flyout[hidden]: without an opacity to animate, the overlay/display
105
- allow-discrete transition below would hold a fully opaque panel in the top
106
- layer and then snap it away. */
107
- .flyout[popover]:not(:popover-open) {
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
- /* compat-ok: :popover-open rides in a forgiving :is(), where an engine without
114
- Popover discards just that member and .is-open alone keeps driving the open
115
- state. This is a property of the forgiving list, not of the pseudo-class: in
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
- /* compat-ok: forgiving :is() again, and @starting-style is itself unsupported
123
- across part of the Minimal range, so the rule simply does not exist there. */
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-strong);
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
- }