actual-css 0.7.0 → 0.8.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 (141) hide show
  1. package/CHANGELOG.md +166 -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 +963 -603
  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 +80 -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 +16 -6
  30. package/docs/pages/components/fab.md +4 -3
  31. package/docs/pages/components/list.md +42 -4
  32. package/docs/pages/components/navbar.md +17 -1
  33. package/docs/pages/components/pagination.md +2 -2
  34. package/docs/pages/components/steps.md +19 -0
  35. package/docs/pages/components/table.md +3 -2
  36. package/docs/pages/enhancements/flyout.md +66 -36
  37. package/docs/pages/enhancements/overview.md +25 -0
  38. package/docs/pages/enhancements/status-bar.md +1 -1
  39. package/docs/pages/enhancements/tabs.md +45 -37
  40. package/docs/pages/enhancements/tooltip.md +36 -10
  41. package/docs/pages/examples/overview.md +38 -5
  42. package/docs/pages/forms/choice-cards.md +6 -2
  43. package/docs/pages/forms/combobox.md +1 -1
  44. package/docs/pages/forms/floating-field.md +4 -3
  45. package/docs/pages/forms/joined-controls.md +22 -0
  46. package/docs/pages/forms/otp.md +2 -1
  47. package/docs/pages/forms/overview.md +1 -1
  48. package/docs/pages/forms/states-and-sizes.md +16 -18
  49. package/docs/pages/forms/sticky-actions.md +2 -2
  50. package/docs/pages/forms/switch.md +1 -1
  51. package/docs/pages/foundations/philosophy.md +57 -26
  52. package/docs/pages/foundations/theming.md +2 -2
  53. package/docs/pages/foundations/tokens.md +118 -21
  54. package/docs/pages/foundations/typography.md +35 -14
  55. package/docs/pages/guides/introduction.md +11 -9
  56. package/docs/pages/guides/progressive-enhancement.md +115 -30
  57. package/docs/pages/guides/tailwind.md +3 -2
  58. package/docs/pages/layout/media.md +1 -1
  59. package/docs/pages/layout/overview.md +11 -1
  60. package/docs/pages/layout/scroll-snap.md +3 -3
  61. package/docs/pages/layout/stack.md +17 -9
  62. package/docs/pages/patterns/icon-input.md +4 -2
  63. package/docs/pages/patterns/icon-slot.md +2 -0
  64. package/docs/pages/patterns/intent-nesting.md +1 -1
  65. package/docs/pages/utilities/overview.md +23 -2
  66. package/docs/pages/utilities/sizing-wrapping.md +1 -1
  67. package/docs/pages/utilities/spacing.md +10 -8
  68. package/llms.txt +2 -1
  69. package/package.json +17 -8
  70. package/{scripts/reserved-classes.json → reserved-classes.json} +4 -6
  71. package/src/css/components/accordion.css +42 -3
  72. package/src/css/components/alert.css +25 -13
  73. package/src/css/components/app-nav.css +18 -8
  74. package/src/css/components/avatar.css +13 -1
  75. package/src/css/components/badge.css +78 -12
  76. package/src/css/components/breadcrumb.css +2 -3
  77. package/src/css/components/button.css +9 -16
  78. package/src/css/components/card.css +9 -2
  79. package/src/css/components/chat.css +1 -0
  80. package/src/css/components/data-list.css +1 -1
  81. package/src/css/components/drawer.css +79 -25
  82. package/src/css/components/fab.css +0 -6
  83. package/src/css/components/flyout.css +35 -98
  84. package/src/css/components/index.css +0 -1
  85. package/src/css/components/indicator.css +21 -0
  86. package/src/css/components/key.css +10 -0
  87. package/src/css/components/list.css +40 -4
  88. package/src/css/components/menu.css +2 -1
  89. package/src/css/components/modal.css +48 -33
  90. package/src/css/components/navbar.css +37 -4
  91. package/src/css/components/pagination.css +1 -0
  92. package/src/css/components/spinner.css +6 -6
  93. package/src/css/components/status-bar.css +0 -5
  94. package/src/css/components/steps.css +45 -59
  95. package/src/css/components/tab.css +29 -9
  96. package/src/css/components/table.css +5 -5
  97. package/src/css/components/tooltip.css +36 -40
  98. package/src/css/core/base.css +23 -1
  99. package/src/css/core/intents.css +17 -6
  100. package/src/css/core/theme.css +27 -0
  101. package/src/css/core/tokens.css +35 -15
  102. package/src/css/core/variants.css +64 -15
  103. package/src/css/forms/choice-card.css +4 -4
  104. package/src/css/forms/choice.css +6 -3
  105. package/src/css/forms/control.css +2 -1
  106. package/src/css/forms/floating-field.css +3 -3
  107. package/src/css/forms/form.css +4 -3
  108. package/src/css/forms/input-icon.css +20 -14
  109. package/src/css/forms/native.css +6 -1
  110. package/src/css/forms/otp.css +9 -1
  111. package/src/css/forms/range.css +2 -0
  112. package/src/css/forms/select.css +29 -9
  113. package/src/css/forms/switch.css +19 -5
  114. package/src/css/layout/app-layout.css +1 -1
  115. package/src/css/layout/grid.css +50 -76
  116. package/src/css/layout/scroll-snap.css +11 -7
  117. package/src/css/layout/scroller.css +3 -4
  118. package/src/css/layout/stack.css +2 -2
  119. package/src/css/typography/fluid.css +4 -3
  120. package/src/css/typography/prose.css +6 -5
  121. package/src/css/utilities/base.css +2 -1
  122. package/src/css/utilities/extra.css +20 -17
  123. package/src/js/context-menu.js +27 -26
  124. package/src/js/dialog.js +2 -1
  125. package/src/js/dismiss.js +5 -0
  126. package/src/js/enhance.js +34 -2
  127. package/src/js/enhancement-loader.js +36 -43
  128. package/src/js/events.js +0 -3
  129. package/src/js/filter.js +0 -1
  130. package/src/js/floating-space.js +42 -0
  131. package/src/js/flyout.js +9 -44
  132. package/src/js/focus-group.js +7 -0
  133. package/src/js/focus.js +6 -0
  134. package/src/js/menu.js +10 -1
  135. package/src/js/selectors.js +5 -9
  136. package/src/js/status.js +6 -5
  137. package/src/js/surface.js +97 -139
  138. package/src/js/tab.js +6 -1
  139. package/src/js/tooltip.js +150 -51
  140. package/src/js/validation.js +1 -1
  141. package/src/css/components/surface.css +0 -49
@@ -62,8 +62,18 @@ dialog.drawer[open] {
62
62
  transform var(--drawer-motion-duration) var(--ease-enter);
63
63
  }
64
64
 
65
- /* Backdrop color sourced from components/surface.css so it matches .modal
66
- and .surface-backdrop across the dialog family. */
65
+ /* A form is the drawer's content wrapper when actions need method="dialog".
66
+ Span the drawer tracks so the form can keep the header at the top, let the
67
+ body absorb the available height, and keep the footer at the bottom. */
68
+ dialog.drawer > form {
69
+ display: grid;
70
+ grid-row: 1 / -1;
71
+ grid-template-rows: auto minmax(0, 1fr) auto;
72
+ gap: var(--space-40);
73
+ min-block-size: 100%;
74
+ }
75
+
76
+ /* The shared backdrop token keeps drawer and modal scrims consistent. */
67
77
  dialog.drawer::backdrop {
68
78
  background: var(--backdrop-fill);
69
79
  opacity: 0;
@@ -128,33 +138,33 @@ dialog.drawer nav {
128
138
  }
129
139
 
130
140
  /* Safari before 16.4 does not support :dir(); keep the ancestor-attribute
131
- path as the Minimal-tier fallback. */
141
+ path as the Degraded-tier fallback. The :dir() rules are dropped on their
142
+ own there, so they need no wrapper. :dir() follows inherited HTML
143
+ directionality and handles nested direction changes; the explicit ltr rules
144
+ neutralize an outer RTL fallback selector. Each specificity group keeps the
145
+ fallback before the :dir() rules. */
132
146
  [dir="rtl"] dialog.drawer {
133
147
  --drawer-closed-translate: 100%;
134
148
  }
135
149
 
136
- [dir="rtl"] dialog.drawer[data-side="end"] {
150
+ dialog.drawer:dir(ltr) {
137
151
  --drawer-closed-translate: -100%;
138
152
  }
139
153
 
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
- }
154
+ dialog.drawer:dir(rtl) {
155
+ --drawer-closed-translate: 100%;
156
+ }
146
157
 
147
- dialog.drawer[data-side="end"]:dir(ltr) {
148
- --drawer-closed-translate: 100%;
149
- }
158
+ [dir="rtl"] dialog.drawer[data-side="end"] {
159
+ --drawer-closed-translate: -100%;
160
+ }
150
161
 
151
- dialog.drawer:dir(rtl) {
152
- --drawer-closed-translate: 100%;
153
- }
162
+ dialog.drawer[data-side="end"]:dir(ltr) {
163
+ --drawer-closed-translate: 100%;
164
+ }
154
165
 
155
- dialog.drawer[data-side="end"]:dir(rtl) {
156
- --drawer-closed-translate: -100%;
157
- }
166
+ dialog.drawer[data-side="end"]:dir(rtl) {
167
+ --drawer-closed-translate: -100%;
158
168
  }
159
169
 
160
170
  @media (prefers-reduced-motion: no-preference) {
@@ -163,15 +173,59 @@ dialog.drawer nav {
163
173
  }
164
174
  }
165
175
 
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. */
176
+ /* The header reserves inline room so the title never runs under the close
177
+ control. */
169
178
  dialog.drawer > header,
170
179
  dialog.drawer > form > header {
171
180
  padding-inline-end: calc(var(--control-size) + var(--space-30));
172
181
  }
173
- @media print {
174
- dialog.drawer[open] {
175
- display: none !important;
182
+
183
+ /* Close control, owned here so a drawer-only build does not depend on
184
+ modal.css. Mirrors the modal's .dialog-close placement: absolute at the
185
+ panel's top inline-end, out of the content flow. */
186
+ .drawer-close {
187
+ --drawer-close-icon-size: 1rem;
188
+
189
+ display: inline-grid;
190
+ place-items: center;
191
+ padding: 0;
192
+ border: 0;
193
+ border-radius: var(--radius);
194
+ color: var(--text-muted);
195
+ cursor: pointer;
196
+
197
+ position: absolute;
198
+ z-index: 1;
199
+ inset-block-start: var(--space-20);
200
+ inset-inline-end: var(--space-20);
201
+
202
+ inline-size: var(--control-size);
203
+ block-size: var(--control-size);
204
+
205
+ background: var(--surface-subtle);
206
+ }
207
+
208
+ .drawer-close::before {
209
+ content: "";
210
+ inline-size: var(--drawer-close-icon-size);
211
+ block-size: var(--drawer-close-icon-size);
212
+ background: currentColor;
213
+ -webkit-mask: var(--icon-close) center / contain no-repeat;
214
+ mask: var(--icon-close) center / contain no-repeat;
215
+ }
216
+
217
+ @media (hover: hover) {
218
+ .drawer-close:hover {
219
+ box-shadow: inset 0 0 0 999px var(--hover-overlay);
220
+ color: var(--text);
176
221
  }
177
222
  }
223
+
224
+ .drawer-close:active {
225
+ box-shadow: inset 0 0 0 999px var(--hover-overlay);
226
+ }
227
+
228
+ .drawer-close:focus-visible {
229
+ outline: 2px solid transparent;
230
+ box-shadow: var(--focus-ring-shadow);
231
+ }
@@ -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
- }
@@ -6,7 +6,6 @@
6
6
 
7
7
  /* Surfaces */
8
8
  @import "./card.css";
9
- @import "./surface.css";
10
9
  @import "./modal.css";
11
10
  @import "./drawer.css";
12
11
 
@@ -38,10 +38,31 @@
38
38
  inset-block-end: 0;
39
39
  }
40
40
 
41
+ /* Safari before 16.4 has no :dir(); the ancestor-attribute path is the
42
+ Degraded-tier fallback and the :dir() rules are dropped on their own there.
43
+ :dir() follows inherited directionality, so the explicit ltr rules
44
+ neutralize an outer RTL fallback for a nested direction change. The rules
45
+ stay grouped by specificity so the fallback never follows the :dir() rules. */
46
+ [dir="rtl"] .indicator-item {
47
+ --indicator-translate-x: -50%;
48
+ }
49
+
50
+ .indicator-item:dir(ltr) {
51
+ --indicator-translate-x: 50%;
52
+ }
53
+
41
54
  .indicator-item:dir(rtl) {
42
55
  --indicator-translate-x: -50%;
43
56
  }
44
57
 
58
+ [dir="rtl"] .indicator-item.start {
59
+ --indicator-translate-x: 50%;
60
+ }
61
+
62
+ .indicator-item.start:dir(ltr) {
63
+ --indicator-translate-x: -50%;
64
+ }
65
+
45
66
  .indicator-item.start:dir(rtl) {
46
67
  --indicator-translate-x: 50%;
47
68
  }
@@ -27,3 +27,13 @@
27
27
  vertical-align: 0.08em;
28
28
  }
29
29
  /* @sync key-recipe */
30
+
31
+ /* .key has a compact typographic default of its own, so its local size roles
32
+ step around that default instead of inheriting the generic mapping. */
33
+ .key.sm {
34
+ font-size: var(--font-size-xs);
35
+ }
36
+
37
+ .key.lg {
38
+ font-size: var(--font-size-md);
39
+ }
@@ -18,6 +18,7 @@
18
18
  --list-divider: var(--border-width) solid var(--border);
19
19
 
20
20
  display: grid;
21
+ grid-template-columns: minmax(0, 1fr);
21
22
  gap: 0;
22
23
  margin: 0;
23
24
  padding: 0;
@@ -29,10 +30,17 @@
29
30
  margin: 0;
30
31
  }
31
32
 
33
+ /*
34
+ * Flex, not a three-track grid: the leading and trailing regions are optional,
35
+ * and a fixed `auto minmax(0, 1fr) auto` template gives an absent region a
36
+ * track anyway — the content of a row with no leading region lands in the
37
+ * leading track and the trailing control inherits the flexible one, which is
38
+ * the opposite of the intent. Flex drops both the track and its gap when a
39
+ * region is not there, so all four shapes work off one rule.
40
+ */
32
41
  .list > :where(.list-item),
33
42
  .list > li > .list-item {
34
- display: grid;
35
- grid-template-columns: auto minmax(0, 1fr) auto;
43
+ display: flex;
36
44
  /* Regions align to the first line, so leading, title, and trailing controls
37
45
  share one horizontal line while supporting text wraps beneath. */
38
46
  align-items: start;
@@ -48,6 +56,7 @@
48
56
  }
49
57
 
50
58
  a.list-item {
59
+ position: relative;
51
60
  color: inherit;
52
61
  text-decoration: none;
53
62
  }
@@ -56,7 +65,31 @@ a.list-item:hover {
56
65
  background: var(--surface-subtle);
57
66
  }
58
67
 
68
+ /* Current row in a navigational list (master/detail). Generic by design: the
69
+ visual treatment only cares that the row is current, so presence is the
70
+ contract — the attribute is set on the current row and removed everywhere
71
+ else, never serialized as aria-current="false". The value (page, location,
72
+ …) stays free for assistive technology.
73
+ Calm by design: the row keeps its normal text metrics and color on a subtle
74
+ surface, and the selected accent lives on an inset inline-start trait only.
75
+ The trait is a real border, so it survives forced colors without a repaint
76
+ block. The positioning context lives on every row link, so becoming current
77
+ never moves an app-positioned decoration to a new containing block. */
78
+ a.list-item[aria-current] {
79
+ background: var(--surface-subtle);
80
+ }
81
+
82
+ a.list-item[aria-current]::before {
83
+ content: "";
84
+ position: absolute;
85
+ inset-block: var(--list-item-pad-block);
86
+ inset-inline-start: 0;
87
+ border-inline-start: 2px solid var(--state-selected);
88
+ }
89
+
90
+ /* The content region is the one that flexes, whichever position it holds. */
59
91
  .list-item-content {
92
+ flex: 1 1 0;
60
93
  min-inline-size: 0;
61
94
  }
62
95
 
@@ -64,16 +97,19 @@ a.list-item:hover {
64
97
  .list-item-text {
65
98
  display: block;
66
99
  margin: 0;
100
+ overflow-wrap: anywhere;
67
101
  }
68
102
 
69
103
  .list-item-text {
70
104
  color: var(--text-muted);
71
105
  font-size: var(--font-size-sm);
72
- overflow-wrap: anywhere;
73
106
  }
74
107
 
108
+ /* The auto margin keeps the trailing region at the row's end even in a row
109
+ with no content region to absorb the free space. */
75
110
  .list-item-trailing {
76
- justify-self: end;
111
+ flex: none;
112
+ margin-inline-start: auto;
77
113
  min-inline-size: 0;
78
114
  }
79
115
 
@@ -44,6 +44,7 @@
44
44
  background: transparent;
45
45
  color: var(--intent, var(--text));
46
46
  font: inherit;
47
+ font-size: var(--control-font-size);
47
48
  text-align: start;
48
49
  text-decoration: none;
49
50
  white-space: nowrap;
@@ -124,7 +125,7 @@
124
125
  padding: var(--space-20);
125
126
  color: var(--text-muted);
126
127
  font-size: var(--font-size-xs);
127
- font-weight: var(--font-weight-strong);
128
+ font-weight: var(--font-weight-semibold);
128
129
  }
129
130
 
130
131
  .menu-separator {
@@ -12,21 +12,40 @@
12
12
  *
13
13
  * Internal:
14
14
  * --dialog-close-icon-size
15
+ * --dialog-viewport-block
16
+ * --dialog-viewport-gap-default
15
17
  * --dialog-viewport-gap-2x
16
18
  */
17
19
  dialog.modal {
18
- --modal-size: 32rem;
19
- --dialog-viewport-gap: var(--space-40);
20
- /* calc() multiplication is not in the supported browser range, so the
20
+ /* --dialog-viewport-gap stays fallback-only for the same reason as
21
+ --modal-size below: declaring the public hook here would give it
22
+ type+class specificity that a single author class cannot outrank. The
23
+ responsive default lives in an internal token instead, so a media query
24
+ can still move it without closing the hook.
25
+
26
+ calc() multiplication is not in the supported browser range, so the
21
27
  doubled gap is spelled out with an additive calc. */
22
- --dialog-viewport-gap-2x: calc(var(--dialog-viewport-gap) + var(--dialog-viewport-gap));
28
+ --dialog-viewport-gap-default: var(--space-40);
29
+ --dialog-viewport-gap-2x: calc(
30
+ var(--dialog-viewport-gap, var(--dialog-viewport-gap-default)) +
31
+ var(--dialog-viewport-gap, var(--dialog-viewport-gap-default))
32
+ );
23
33
 
24
34
  /* Keep the surface anchored to the viewport in the top layer. This also
25
35
  provides the containing block for the absolutely positioned close button. */
26
36
  position: fixed;
27
37
 
28
- max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size));
29
- max-block-size: calc(var(--viewport-block) - var(--dialog-viewport-gap-2x));
38
+ /* --modal-size stays fallback-only: declaring it on dialog.modal gives it
39
+ type+class specificity, which a single author class cannot outrank, so the
40
+ public hook would silently keep this default. Same reason choice.css never
41
+ declares --choice-control-offset. */
42
+ max-inline-size: min(calc(100% - var(--dialog-viewport-gap-2x)), var(--modal-size, 32rem));
43
+
44
+ /* Stable viewport cap for the panel: dvh tracks the dynamic browser toolbar
45
+ and makes the dialog reflow on mobile, svh does not. App shells keep the
46
+ dynamic token; only the modal pins the small viewport. */
47
+ --dialog-viewport-block: var(--viewport-block);
48
+ max-block-size: calc(var(--dialog-viewport-block) - var(--dialog-viewport-gap-2x));
30
49
  border: var(--border-width) solid var(--border);
31
50
  border-radius: var(--radius-lg);
32
51
  background: var(--surface-raised);
@@ -36,7 +55,7 @@ dialog.modal {
36
55
  overflow: visible;
37
56
  opacity: 0;
38
57
  /*
39
- * Keep the open dialog root untransformed. Fixed overlays mounted inside a
58
+ * Keep the open dialog root untransformed. Fixed overlays authored inside a
40
59
  * modal dialog use viewport coordinates; transform/translate/scale on this
41
60
  * element would make them position against the dialog instead. The closed
42
61
  * state can still carry a small transform for enter/exit motion. The lift
@@ -53,16 +72,23 @@ dialog.modal > form,
53
72
  dialog.modal > .stack {
54
73
  display: grid;
55
74
  gap: var(--space-40);
56
- max-block-size: calc(var(--viewport-block) - var(--dialog-viewport-gap-2x));
75
+ max-block-size: calc(var(--dialog-viewport-block) - var(--dialog-viewport-gap-2x));
57
76
  overflow: auto;
58
77
  overscroll-behavior: contain;
59
78
  padding: var(--space-40);
60
79
  }
61
80
 
62
- /* Keep the dialog off the viewport edges on small screens. */
81
+ @supports (block-size: 100svh) {
82
+ dialog.modal {
83
+ --dialog-viewport-block: 100svh;
84
+ }
85
+ }
86
+
87
+ /* Keep the dialog off the viewport edges on small screens. Moves the internal
88
+ default, not the public hook, so an author override still wins here too. */
63
89
  @media (max-width: 30rem) {
64
90
  dialog.modal {
65
- --dialog-viewport-gap: var(--space-30);
91
+ --dialog-viewport-gap-default: var(--space-30);
66
92
  }
67
93
  }
68
94
 
@@ -72,8 +98,8 @@ dialog.modal.scrollable > .stack {
72
98
  overflow: hidden;
73
99
  }
74
100
 
75
- dialog.modal.scrollable > form > :not(header, footer),
76
- dialog.modal.scrollable > .stack > :not(header, footer) {
101
+ dialog.modal.scrollable > form > :not(:is(header, footer)),
102
+ dialog.modal.scrollable > .stack > :not(:is(header, footer)) {
77
103
  min-block-size: 0;
78
104
  overflow: auto;
79
105
  overscroll-behavior: contain;
@@ -128,6 +154,7 @@ dialog.modal.dialog-confirmation > form > footer {
128
154
  :where(.dialog-icon) {
129
155
  --intent: initial;
130
156
  --intent-fg: initial;
157
+ --intent-soft-fg: initial;
131
158
  --ui-bg: initial;
132
159
  --ui-fg: initial;
133
160
  }
@@ -256,14 +283,11 @@ html.has-modal-open.had-scrollbar {
256
283
  }
257
284
  }
258
285
 
259
- /* Close controls. A close button is an action attached to a surface, so it
260
- never participates in the content flow. The modal and the drawer share the
261
- same simple placement — absolute at the panel's top inline-end, inside the
262
- surface — so both close buttons look and behave identically. The glyph comes
263
- from --icon-close, masked to currentColor, so no icon font or text glyph is
264
- required in the markup. */
265
- .dialog-close,
266
- .drawer-close {
286
+ /* Close control. A close button is an action attached to a surface, so it
287
+ never participates in the content flow — absolute at the panel's top
288
+ inline-end, inside the surface. The glyph comes from --icon-close, masked to
289
+ currentColor, so no icon font or text glyph is required in the markup. */
290
+ .dialog-close {
267
291
  --dialog-close-icon-size: 1rem;
268
292
 
269
293
  display: inline-grid;
@@ -287,8 +311,7 @@ html.has-modal-open.had-scrollbar {
287
311
  background: var(--surface-subtle);
288
312
  }
289
313
 
290
- .dialog-close::before,
291
- .drawer-close::before {
314
+ .dialog-close::before {
292
315
  content: "";
293
316
  inline-size: var(--dialog-close-icon-size);
294
317
  block-size: var(--dialog-close-icon-size);
@@ -298,25 +321,17 @@ html.has-modal-open.had-scrollbar {
298
321
  }
299
322
 
300
323
  @media (hover: hover) {
301
- .dialog-close:hover,
302
- .drawer-close:hover {
324
+ .dialog-close:hover {
303
325
  box-shadow: inset 0 0 0 999px var(--hover-overlay);
304
326
  color: var(--text);
305
327
  }
306
328
  }
307
329
 
308
- .dialog-close:active,
309
- .drawer-close:active {
330
+ .dialog-close:active {
310
331
  box-shadow: inset 0 0 0 999px var(--hover-overlay);
311
332
  }
312
333
 
313
- .dialog-close:focus-visible,
314
- .drawer-close:focus-visible {
334
+ .dialog-close:focus-visible {
315
335
  outline: 2px solid transparent;
316
336
  box-shadow: var(--focus-ring-shadow);
317
337
  }
318
- @media print {
319
- dialog.modal[open] {
320
- display: none !important;
321
- }
322
- }