actual-css 0.6.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 (143) hide show
  1. package/CHANGELOG.md +214 -1
  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 +1005 -550
  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 +82 -25
  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 +99 -2
  18. package/docs/design-notes/popover-manual-poc.md +328 -0
  19. package/docs/design-notes/surface-reparenting.md +131 -0
  20. package/docs/design-notes/tooltip-coordinate-space.md +132 -0
  21. package/docs/design-notes/widget-primitives.md +9 -6
  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 +144 -24
  37. package/docs/pages/enhancements/overview.md +25 -0
  38. package/docs/pages/enhancements/status-bar.md +20 -0
  39. package/docs/pages/enhancements/tabs.md +45 -37
  40. package/docs/pages/enhancements/tooltip.md +40 -2
  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 +67 -22
  52. package/docs/pages/foundations/theming.md +10 -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 +136 -49
  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 +19 -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 +46 -63
  84. package/src/css/components/index.css +0 -1
  85. package/src/css/components/indicator.css +21 -0
  86. package/src/css/components/join.css +7 -0
  87. package/src/css/components/key.css +10 -0
  88. package/src/css/components/list.css +40 -4
  89. package/src/css/components/menu.css +18 -11
  90. package/src/css/components/modal.css +48 -33
  91. package/src/css/components/navbar.css +37 -4
  92. package/src/css/components/pagination.css +1 -0
  93. package/src/css/components/spinner.css +6 -6
  94. package/src/css/components/status-bar.css +0 -5
  95. package/src/css/components/steps.css +45 -59
  96. package/src/css/components/tab.css +29 -9
  97. package/src/css/components/table.css +5 -5
  98. package/src/css/components/tooltip.css +45 -13
  99. package/src/css/core/base.css +23 -1
  100. package/src/css/core/intents.css +17 -6
  101. package/src/css/core/theme.css +27 -0
  102. package/src/css/core/tokens.css +35 -15
  103. package/src/css/core/variants.css +64 -15
  104. package/src/css/forms/choice-card.css +4 -4
  105. package/src/css/forms/choice.css +6 -3
  106. package/src/css/forms/control.css +2 -1
  107. package/src/css/forms/floating-field.css +3 -3
  108. package/src/css/forms/form.css +4 -3
  109. package/src/css/forms/input-icon.css +20 -14
  110. package/src/css/forms/native.css +6 -1
  111. package/src/css/forms/otp.css +9 -1
  112. package/src/css/forms/range.css +2 -0
  113. package/src/css/forms/select.css +29 -9
  114. package/src/css/forms/switch.css +19 -5
  115. package/src/css/layout/app-layout.css +1 -1
  116. package/src/css/layout/grid.css +50 -76
  117. package/src/css/layout/scroll-snap.css +11 -7
  118. package/src/css/layout/scroller.css +3 -4
  119. package/src/css/layout/stack.css +2 -2
  120. package/src/css/typography/fluid.css +4 -3
  121. package/src/css/typography/prose.css +6 -5
  122. package/src/css/utilities/base.css +2 -1
  123. package/src/css/utilities/extra.css +20 -17
  124. package/src/js/context-menu.js +27 -26
  125. package/src/js/dialog.js +2 -1
  126. package/src/js/dismiss.js +5 -0
  127. package/src/js/enhance.js +34 -2
  128. package/src/js/enhancement-loader.js +36 -43
  129. package/src/js/events.js +0 -3
  130. package/src/js/filter.js +0 -1
  131. package/src/js/floating-space.js +42 -0
  132. package/src/js/flyout.js +9 -44
  133. package/src/js/focus-group.js +7 -0
  134. package/src/js/focus.js +6 -0
  135. package/src/js/menu.js +10 -1
  136. package/src/js/selectors.js +5 -9
  137. package/src/js/status.js +44 -7
  138. package/src/js/surface.js +106 -132
  139. package/src/js/tab.js +6 -1
  140. package/src/js/tooltip.js +152 -53
  141. package/src/js/validation.js +1 -1
  142. package/src/css/components/surface.css +0 -49
  143. package/src/js/floating.js +0 -432
@@ -7,20 +7,40 @@
7
7
  * the CSS chevron, so the picker (appearance: base-select) enhancement in
8
8
  * custom-select.css can be opted out independently. Add .native to a specific
9
9
  * select to opt out of the enhancement.
10
+ *
11
+ * Public hooks:
12
+ * --select-icon-size
10
13
  */
11
- .select:not([multiple], [size]:not([size="1"])) {
14
+ /* --select-icon-size is typographic (em), so the reserved slot and the
15
+ pictogram scale with the control's font size, not a fixed rem sliver. */
16
+ .select:not(:is([multiple], [size]:not([size="1"]))) {
17
+ --select-icon-size: 1em;
18
+
12
19
  appearance: none;
13
- padding-inline-end: calc(var(--space-30) + var(--space-60));
20
+ -webkit-appearance: none;
21
+ padding-inline-end: calc(var(--control-pad-x) * 2 + var(--select-icon-size));
14
22
  background-image: var(--icon-chevron);
15
- background-position: center right var(--space-30);
16
- background-size: 1rem auto;
23
+ background-position: center right var(--control-pad-x);
24
+ background-size: var(--select-icon-size) auto;
17
25
  background-repeat: no-repeat;
18
26
  }
19
27
 
20
28
  /* The chevron sits on the inline-end side; flip it in RTL so it follows
21
- the padding-inline-end space reserved for it. */
22
- .select:not([multiple], [size]:not([size="1"])):dir(rtl) {
23
- background-position: center left var(--space-30);
29
+ the padding-inline-end space reserved for it. Safari before 16.4 has no
30
+ :dir(); the ancestor-attribute path is the Degraded-tier fallback and the
31
+ :dir() rules are dropped on their own there. :dir() follows inherited
32
+ directionality, so the explicit ltr rule neutralizes an outer RTL fallback
33
+ for a nested direction change. */
34
+ [dir="rtl"] .select:not(:is([multiple], [size]:not([size="1"]))) {
35
+ background-position: center left var(--control-pad-x);
36
+ }
37
+
38
+ .select:not(:is([multiple], [size]:not([size="1"]))):dir(ltr) {
39
+ background-position: center right var(--control-pad-x);
40
+ }
41
+
42
+ .select:not(:is([multiple], [size]:not([size="1"]))):dir(rtl) {
43
+ background-position: center left var(--control-pad-x);
24
44
  }
25
45
 
26
46
  /* Both forced-colors and prefers-contrast hand the arrow back to the
@@ -28,9 +48,9 @@
28
48
  simpler than maintaining contrast variants, and native rendering already
29
49
  matches whatever contrast mode the user picked. */
30
50
  @media (forced-colors: active), (prefers-contrast: more) {
31
- .select:not([multiple], [size]:not([size="1"])) {
51
+ .select:not(:is([multiple], [size]:not([size="1"]))) {
32
52
  appearance: auto;
33
53
  background-image: none;
34
- padding-inline-end: var(--space-30);
54
+ padding-inline-end: var(--control-pad-x);
35
55
  }
36
56
  }
@@ -65,6 +65,7 @@
65
65
  border-radius: var(--radius-full);
66
66
  background: var(--switch-bg);
67
67
  appearance: none;
68
+ -webkit-appearance: none;
68
69
  cursor: pointer;
69
70
  }
70
71
 
@@ -85,14 +86,17 @@
85
86
  background-color var(--duration);
86
87
  }
87
88
 
88
- .switch:hover:not(:disabled) {
89
+ /* Hover is not selected: an unchecked switch keeps the brand fallback on hover,
90
+ so a theme that separates --primary from --state-selected never tints an
91
+ OFF control with the selection accent before it is checked. */
92
+ .switch:not(:checked):hover:not(:disabled) {
89
93
  --switch-border: var(--intent, var(--primary));
90
94
  }
91
95
 
92
96
  .switch:checked {
93
- --switch-bg: var(--intent, var(--primary));
94
- --switch-border: var(--intent, var(--primary));
95
- --switch-knob: var(--intent-fg, var(--primary-fg));
97
+ --switch-bg: var(--intent, var(--state-selected));
98
+ --switch-border: var(--intent, var(--state-selected));
99
+ --switch-knob: var(--intent-fg, var(--state-selected-fg));
96
100
  }
97
101
 
98
102
  .switch:checked::before {
@@ -100,7 +104,17 @@
100
104
  }
101
105
 
102
106
  /* The knob travels toward inline-end; in RTL that is leftward, so the
103
- translateX sign flips. translateX is physical, not logical. */
107
+ translateX sign flips. translateX is physical, not logical. Safari before
108
+ 16.4 has no :dir(); the ancestor-attribute path is the Degraded-tier
109
+ fallback and the :dir() rules are dropped on their own there. */
110
+ [dir="rtl"] .switch:checked::before {
111
+ transform: translateX(calc(var(--switch-travel) * -1));
112
+ }
113
+
114
+ .switch:checked:dir(ltr)::before {
115
+ transform: translateX(var(--switch-travel));
116
+ }
117
+
104
118
  .switch:checked:dir(rtl)::before {
105
119
  transform: translateX(calc(var(--switch-travel) * -1));
106
120
  }
@@ -83,7 +83,7 @@
83
83
  text-align: start;
84
84
  }
85
85
 
86
- /* Side navigation no longer occupies the viewport's bottom safe area. */
86
+ /* In the side layout the FAB owns the viewport's bottom safe-area inset. */
87
87
  .app-layout > .fab {
88
88
  margin-block-end: max(var(--fab-offset), env(safe-area-inset-bottom));
89
89
  }
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * .grid is space-driven: it fits as many tracks as --grid-min allows.
5
5
  * .grid-N is count-driven: it declares a structural density of N and collapses
6
- * through natural divisors of N (see the subdivision block below).
6
+ * through natural divisors of N.
7
7
  *
8
8
  * Public hooks:
9
9
  * --grid-min
@@ -14,22 +14,15 @@
14
14
  *
15
15
  * Child contract: each direct child is a grid item placed in the tracks the
16
16
  * recipe generates.
17
+ *
18
+ * Choosing between these and the other layout primitives:
19
+ * docs/pages/layout/grid.md. Why divisors, why a *named* container, and how
20
+ * the thresholds were arrived at: docs/design-notes/grid.md.
17
21
  */
18
22
 
19
- /* --grid-columns replaces the intrinsic auto-fit template: once set, the
20
- grid no longer collapses on its own — the author owns narrow-container
21
- collapse. Prefer .sidebar-layout for content + aside layouts; reserve
22
- --grid-columns for exact custom track templates. */
23
-
24
- /* Recipe:
25
- - collection whose item width is the contract -> .grid, tune --grid-min
26
- - collection whose column density is the contract -> .grid-2/3/4/6
27
- - balanced subdivision of that density -> place it inside any size query
28
- container (.container-query is the convenience helper for that context)
29
- - peer regions that switch as a group -> .switcher
30
- - content + aside -> .sidebar-layout
31
- - exact custom track templates -> --grid-columns with an explicit collapse
32
- rule of your own */
23
+ /* --grid-columns replaces the intrinsic auto-fit template: once set, the grid
24
+ no longer collapses on its own and the author owns narrow-container
25
+ collapse. */
33
26
  .grid {
34
27
  display: grid;
35
28
  gap: var(--gap);
@@ -39,16 +32,14 @@
39
32
  );
40
33
  }
41
34
 
42
- /* Structural density presets. The baseline is an auto-fill grid bounded at N
43
- tracks: responsive and overflow-safe on its own, so a preset used without a
44
- query container is never a trap — it simply reaches its balanced states less
45
- precisely. auto-fill rather than auto-fit keeps item width stable when a
46
- partial final page holds fewer items than the grid's capacity.
35
+ /* Baseline: auto-fill bounded at N, so a preset is responsive and
36
+ overflow-safe with no wrapper at all. auto-fill rather than auto-fit keeps
37
+ item width stable when a partial final page holds fewer items than the
38
+ grid's capacity.
47
39
 
48
- --grid-min is deliberately NOT read here. The subdivision below runs on
49
- @container thresholds, which cannot resolve a custom property, so a hook
50
- honored in the baseline would fall silent the moment a query container is
51
- added. Item width is .grid's contract; column density is .grid-N's. */
40
+ --grid-min is deliberately NOT read here: @container thresholds cannot
41
+ resolve a custom property, so a hook honored in the baseline would fall
42
+ silent the moment a query container is added. */
52
43
  :where(.grid-2, .grid-3, .grid-4, .grid-6) {
53
44
  display: grid;
54
45
  gap: var(--gap);
@@ -77,79 +68,62 @@
77
68
  --grid-count: 6;
78
69
  }
79
70
 
80
- /* Grid items keep an automatic minimum inline size (min-content), which can
81
- overflow a track sized minmax(0, 1fr). Layout primitives remove that
82
- intrinsic floor so their overflow-safe track sizing holds; authors only
83
- need .min-inline-size-0 outside primitives that do not already remove this
84
- floor. */
71
+ /* Grid items keep an automatic min-content floor that can overflow a
72
+ minmax(0, 1fr) track. Layout primitives remove it so their overflow-safe
73
+ track sizing holds; .min-inline-size-0 is for the ones that do not. */
85
74
  :where(.grid, .grid-2, .grid-3, .grid-4, .grid-6) > * {
86
75
  min-inline-size: 0;
87
76
  }
88
77
 
89
- /* Balanced subdivision, activated by an ancestor container named actual-container.
90
- * A .grid-N inside such a container enters only divisor states of N. The name
91
- * is deliberate: a grid must not key off the nearest anonymous query container,
92
- * or a region that sets container-type: inline-size for an unrelated reason
93
- * would silently change the grid. The author opts the grid in by naming the
94
- * context — .container-query is the official helper, or declare
95
- * container: actual-container / inline-size on your own region.
78
+ /* Balanced subdivision, keyed on an ancestor container *named*
79
+ * actual-container — never an anonymous one, so a region that sets
80
+ * container-type: inline-size for an unrelated reason cannot change a grid.
96
81
  *
97
82
  * .grid-2 2 -> 1
98
83
  * .grid-3 3 -> 1
99
84
  * .grid-4 4 -> 2 -> 1
100
85
  * .grid-6 6 -> 3 -> 2 -> 1
101
86
  *
102
- * Every state is a divisor of N, so all items keep the same width and no
103
- * state produces a visibly uneven split such as 5 + 1 or 4 + 2. Intermediate
104
- * counts are skipped on purpose: do not "complete" these chains with the
105
- * missing integers, and do not replace them with an unbounded auto-fit.
87
+ * Every state is a divisor of N. Do not complete these chains with the
88
+ * missing integers and do not replace them with an unbounded auto-fit: both
89
+ * bring back uneven splits such as 5 + 1.
106
90
  *
107
- * The query container must be an ancestor. An element cannot query its own
108
- * size without taking inline-size containment, which destroys its intrinsic
109
- * inline contribution and collapses it inside .cluster, .topbar, floats, and
110
- * any shrink-to-fit box. See docs/design-notes/grid.md.
91
+ * The container must be an ANCESTOR. An element cannot query its own size
92
+ * without inline-size containment, which destroys its intrinsic inline
93
+ * contribution and collapses it inside .cluster, .topbar, floats and any
94
+ * shrink-to-fit box.
111
95
  *
112
- * Without an actual-container ancestor none of these @container rules resolve, so
113
- * the auto-fill baseline above stays. The min-width: 0 rule is the lowest
114
- * step: it pins the grid to one column inside a container before any larger
115
- * threshold fires (a bare grid would otherwise show a 2 + 1 orphan at 28-48rem).
116
- *
117
- * Three shared thresholds, not one calibrated per preset: 28rem opens the
118
- * first horizontal density, 48rem opens three columns, 64rem opens the dense
119
- * 4 and 6. N already carries the author's statement about item size, so
120
- * tuning every step to land the same item width would erase that signal —
121
- * .grid-6 at 64rem gives ~161px per tile, which is the point of choosing it.
122
- * Probed with the content each preset targets: large cards for .grid-2 down
123
- * to stats, avatars, and thumbnails for .grid-6.
96
+ * With no such ancestor none of these resolve and the baseline above stays.
97
+ * min-width: 0 is the lowest step: it pins the grid to one column before any
98
+ * larger threshold fires, which a bare grid would otherwise show as a 2 + 1
99
+ * orphan between 28 and 48rem.
124
100
  *
125
101
  * Ascending min-width only: the last matching step wins by cascade order.
126
102
  */
127
- @supports (container-type: inline-size) {
128
- @container actual-container (min-width: 0px) {
129
- :where(.grid-2, .grid-3, .grid-4, .grid-6) {
130
- grid-template-columns: 1fr;
131
- }
103
+ @container actual-container (min-width: 0px) {
104
+ :where(.grid-2, .grid-3, .grid-4, .grid-6) {
105
+ grid-template-columns: 1fr;
132
106
  }
107
+ }
133
108
 
134
- @container actual-container (min-width: 28rem) {
135
- :where(.grid-2, .grid-4, .grid-6) {
136
- grid-template-columns: repeat(2, minmax(0, 1fr));
137
- }
109
+ @container actual-container (min-width: 28rem) {
110
+ :where(.grid-2, .grid-4, .grid-6) {
111
+ grid-template-columns: repeat(2, minmax(0, 1fr));
138
112
  }
113
+ }
139
114
 
140
- @container actual-container (min-width: 48rem) {
141
- :where(.grid-3, .grid-6) {
142
- grid-template-columns: repeat(3, minmax(0, 1fr));
143
- }
115
+ @container actual-container (min-width: 48rem) {
116
+ :where(.grid-3, .grid-6) {
117
+ grid-template-columns: repeat(3, minmax(0, 1fr));
144
118
  }
119
+ }
145
120
 
146
- @container actual-container (min-width: 64rem) {
147
- :where(.grid-4) {
148
- grid-template-columns: repeat(4, minmax(0, 1fr));
149
- }
121
+ @container actual-container (min-width: 64rem) {
122
+ :where(.grid-4) {
123
+ grid-template-columns: repeat(4, minmax(0, 1fr));
124
+ }
150
125
 
151
- :where(.grid-6) {
152
- grid-template-columns: repeat(6, minmax(0, 1fr));
153
- }
126
+ :where(.grid-6) {
127
+ grid-template-columns: repeat(6, minmax(0, 1fr));
154
128
  }
155
129
  }
@@ -31,24 +31,28 @@
31
31
  .scroll-snap {
32
32
  overscroll-behavior-inline: contain;
33
33
  scroll-padding-inline: var(--scroll-snap-padding, var(--scroll-snap-gap));
34
+ /* x then inline: an engine that lacks the logical keyword keeps x.
35
+ proximity is the default, so a short rail cannot trap the pointer. */
36
+ scroll-snap-type: x proximity;
37
+ scroll-snap-type: inline proximity;
38
+ }
39
+
40
+ /* Strict snapping is opt-in; proximity keeps a rail from fighting the
41
+ pointer on a short strip. */
42
+ .scroll-snap[data-snap="mandatory"] {
34
43
  scroll-snap-type: x mandatory;
44
+ scroll-snap-type: inline mandatory;
35
45
  }
36
46
 
37
47
  .scroll-snap > * {
38
48
  scroll-snap-align: var(--scroll-snap-align);
39
49
  }
40
50
 
41
- .scroll-snap[data-snap="center"] {
51
+ .scroll-snap[data-snap-align="center"] {
42
52
  --scroll-snap-align: center;
43
53
  }
44
54
  }
45
55
 
46
- @supports (scroll-snap-type: inline mandatory) {
47
- .scroll-snap {
48
- scroll-snap-type: inline mandatory;
49
- }
50
- }
51
-
52
56
  .scroll-snap[data-scrollbar="hidden"] {
53
57
  scrollbar-width: none;
54
58
  }
@@ -8,10 +8,9 @@
8
8
  * the light/dark adaptation that color-scheme already gives for free, and the
9
9
  * two never match anyway.
10
10
  *
11
- * Above the capability floor: scrollbar-color lands in Chromium 121 and
12
- * Safari 18.2, the floor is Chromium 99 / Safari 15.4. Older engines ignore
13
- * both declarations and draw their native scrollbar — the intended fallback,
14
- * not a broken state.
11
+ * Above every tier: scrollbar-color lands in Chromium 121 and Safari 18.2, so
12
+ * even a Recommended engine may lack it. Engines that ignore both declarations
13
+ * draw their native scrollbar — the intended fallback, not a broken state.
15
14
  *
16
15
  * Styles the scrollbar only; it does not create the overflow. Pair it with
17
16
  * .overflow-auto or a component that already scrolls.
@@ -7,8 +7,8 @@
7
7
  --gap is inherited, so a container that composes other primitives sets its
8
8
  own rhythm on the `gap` property, not on the token: setting --gap on a
9
9
  region re-spaces every nested stack, cluster and grid at any depth. Setting
10
- the token stays correct on the element that consumes it, and .sm/.lg set it
11
- deliberately so a density context reaches nested layouts. See
10
+ the token stays correct on the element that consumes it, and
11
+ .compact/.spacious set it deliberately so a density context reaches nested layouts. See
12
12
  docs/pages/layout/stack.md. */
13
13
  .stack {
14
14
  display: flex;
@@ -1,9 +1,10 @@
1
1
  /*
2
2
  * Optional fluid typography module.
3
3
  *
4
- * Not imported by actual.css. Import this file manually after actual.css when
5
- * you need display, title, and lead sizes that scale with the viewport. Typical
6
- * use cases: marketing pages, landing pages, hero sections, documentation
4
+ * Part of the typography family (typography/index.css) and the full bundle; it
5
+ * is not in the minimal core, so a core-only import adds it explicitly when
6
+ * display, title, and lead sizes must scale with the viewport. Typical use
7
+ * cases: marketing pages, landing pages, hero sections, documentation
7
8
  * homepages.
8
9
  *
9
10
  * Do not use this for body text, app UI, or the default prose scale. Fluid
@@ -51,7 +51,7 @@
51
51
  .prose :where(h1, h2, h3, h4, h5, h6) {
52
52
  margin-block: 1.5em 0;
53
53
  color: var(--heading, var(--text));
54
- font-weight: var(--font-weight-strong);
54
+ font-weight: var(--font-weight-semibold);
55
55
  line-height: var(--prose-heading-line-height);
56
56
  }
57
57
 
@@ -92,12 +92,12 @@
92
92
  text-underline-offset: 0.18em;
93
93
  }
94
94
 
95
- .prose :where(a:hover) {
95
+ .prose :where(a:hover, a:focus-visible) {
96
96
  text-decoration-thickness: 0.12em;
97
97
  }
98
98
 
99
99
  @supports (color: color-mix(in oklch, red, white)) {
100
- .prose :where(a:hover) {
100
+ .prose :where(a:hover, a:focus-visible) {
101
101
  color: color-mix(in oklab, var(--link, var(--primary)), var(--text) 15%);
102
102
  }
103
103
  }
@@ -115,7 +115,7 @@
115
115
  /* Description lists keep document flow: a valid <dl> may pair several <dt>
116
116
  with one <dd> or the reverse, so no grid layout is imposed here. */
117
117
  .prose :where(dt) {
118
- font-weight: var(--font-weight-strong);
118
+ font-weight: var(--font-weight-semibold);
119
119
  }
120
120
 
121
121
  .prose :where(dd) {
@@ -157,8 +157,8 @@
157
157
  }
158
158
  /* @sync key-recipe */
159
159
 
160
+ /* Overflow containment is global on `pre` (base.css); only the box is prose's. */
160
161
  .prose :where(pre) {
161
- overflow-x: auto;
162
162
  padding: 1em;
163
163
  border: var(--border-width) solid var(--border);
164
164
  border-radius: var(--radius);
@@ -171,6 +171,7 @@
171
171
  }
172
172
 
173
173
  .prose :where(blockquote) {
174
+ margin-inline: 0;
174
175
  padding-inline-start: 1em;
175
176
  border-inline-start: var(--border-width) solid var(--border);
176
177
  color: var(--text-muted);
@@ -143,7 +143,8 @@
143
143
  text-decoration: none;
144
144
  }
145
145
 
146
- .link-muted:hover {
146
+ .link-muted:hover,
147
+ .link-muted:focus-visible {
147
148
  color: var(--text);
148
149
  }
149
150
 
@@ -5,7 +5,8 @@
5
5
  * They are escape hatches for layout nudges, not a full atomic scale.
6
6
  *
7
7
  * Core principle:
8
- * - .sm / .lg provide local rhythm through --gap and --density-space.
8
+ * - .compact / .spacious provide inherited rhythm through --gap and
9
+ * --density-space.
9
10
  * - utilities consume the current context instead of defining their own -sm/-lg scale.
10
11
  * - optional utilities extend core utilities, they do not re-express them.
11
12
  * - follow verbose .{property}-{value} naming.
@@ -86,18 +87,6 @@
86
87
  flex-wrap: nowrap;
87
88
  }
88
89
 
89
- .gap-sm {
90
- gap: var(--space-20);
91
- }
92
-
93
- .gap-md {
94
- gap: var(--space-40);
95
- }
96
-
97
- .gap-lg {
98
- gap: var(--space-50);
99
- }
100
-
101
90
  .justify-content-start {
102
91
  --cluster-justify: flex-start;
103
92
  justify-content: flex-start;
@@ -155,6 +144,10 @@
155
144
  margin-block-end: auto;
156
145
  }
157
146
 
147
+ /* Consume the inherited --gap rhythm on an arbitrary layout container.
148
+ Setting --gap alone only defines the custom property; it has no visual
149
+ effect unless the element already uses gap: var(--gap) (as .stack,
150
+ .cluster, .grid, etc. do). */
158
151
  .gap-context {
159
152
  gap: var(--gap);
160
153
  }
@@ -206,15 +199,15 @@
206
199
  }
207
200
 
208
201
  .font-weight-normal {
209
- font-weight: var(--font-weight);
202
+ font-weight: var(--font-weight-normal);
210
203
  }
211
204
 
212
205
  .font-weight-medium {
213
206
  font-weight: var(--font-weight-medium);
214
207
  }
215
208
 
216
- .font-weight-strong {
217
- font-weight: var(--font-weight-strong);
209
+ .font-weight-semibold {
210
+ font-weight: var(--font-weight-semibold);
218
211
  }
219
212
 
220
213
  .font-weight-bold {
@@ -255,18 +248,28 @@
255
248
  border-inline-end: var(--border-width) solid var(--border);
256
249
  }
257
250
 
258
- /* Surface */
251
+ /* Surface. These are semantic surfaces, not arbitrary paint: each owns its
252
+ matching ink and rebinds --heading to that ink, so it stays readable when it
253
+ recreates a light surface inside a context that installed a dark one
254
+ (.inverted). Intent ink is not inherited through a surface utility; use
255
+ .intent-color for that. */
259
256
  .background-surface {
257
+ --heading: currentColor;
258
+
260
259
  background: var(--surface);
261
260
  color: var(--text);
262
261
  }
263
262
 
264
263
  .background-raised {
264
+ --heading: currentColor;
265
+
265
266
  background: var(--surface-raised);
266
267
  color: var(--text);
267
268
  }
268
269
 
269
270
  .background-subtle {
271
+ --heading: currentColor;
272
+
270
273
  background: var(--surface-subtle);
271
274
  color: var(--text);
272
275
  }
@@ -1,3 +1,9 @@
1
+ /*
2
+ * Context menu — `data-context-menu` targets opening a shared menu surface at
3
+ * a pointer, keyboard, or button origin. Reuses surface for the lifecycle and
4
+ * menu for item navigation; the panel gets no token.
5
+ */
6
+
1
7
  import enhance from "./enhance.js";
2
8
  import { EVENTS } from "./events.js";
3
9
  import { connectMenu, focusFirstMenuItem, hasMenuItems } from "./menu.js";
@@ -53,7 +59,6 @@ function getLongPressDelay(target, menu) {
53
59
  function readPanelOptions(menu) {
54
60
  const opts = { autoClose: "true" };
55
61
  const ds = menu.dataset;
56
- if (ds.flyoutMobile) opts.mobile = ds.flyoutMobile;
57
62
  if (ds.flyoutAutoClose) opts.autoClose = ds.flyoutAutoClose;
58
63
  return opts;
59
64
  }
@@ -62,8 +67,10 @@ function focusMenuContainer(menu) {
62
67
  if (!menu.hasAttribute("tabindex")) {
63
68
  menu.tabIndex = -1;
64
69
  }
65
- // Moving the mounted menu into the surface root must not scroll the page.
66
- // Genuine user scrolling is handled separately by the surface policy.
70
+ // The menu is promoted where it stands rather than moved, so there is no
71
+ // relocation left to compensate for. preventScroll stays for its own sake:
72
+ // focusing a panel must not move the page under the user. Genuine user
73
+ // scrolling is handled separately by the surface policy.
67
74
  menu.focus({ preventScroll: true });
68
75
  }
69
76
 
@@ -95,15 +102,23 @@ function requestContextMenu(context, menu, opts = {}) {
95
102
  function openContextMenu(context, menu, opts = {}) {
96
103
  if (!requestContextMenu(context, menu, opts)) return;
97
104
  if (isSurfaceOpen(menu)) closeSurface(menu);
105
+
106
+ // Measured after the cancelable event: a handler may have moved the anchor.
107
+ let { x, y } = opts;
108
+ if (x == null || y == null) {
109
+ const rect = (opts.origin || context).getBoundingClientRect();
110
+ x ??= rect.left;
111
+ y ??= rect.bottom;
112
+ }
113
+
98
114
  if (
99
115
  openSurface(menu, {
100
116
  source: context,
101
- x: opts.x,
102
- y: opts.y,
117
+ x,
118
+ y,
103
119
  placement: opts.placement || "bottom-start",
104
120
  distance: opts.distance ?? 2,
105
121
  dismissOnScroll: true,
106
- mobile: opts.mobile || "auto",
107
122
  scope: opts.scope || getContextScope(context),
108
123
  restoreFocusTo: opts.restoreFocusTo,
109
124
  ...readPanelOptions(menu),
@@ -114,10 +129,7 @@ function openContextMenu(context, menu, opts = {}) {
114
129
  }
115
130
 
116
131
  function openFromKeyboard(context, menu, origin = context, restoreFocusTo) {
117
- const rect = origin.getBoundingClientRect();
118
132
  openContextMenu(context, menu, {
119
- x: rect.left,
120
- y: rect.bottom,
121
133
  placement: "bottom-start",
122
134
  distance: 4,
123
135
  focus: "first-item",
@@ -264,23 +276,12 @@ function connectContextTarget(target) {
264
276
  return;
265
277
  }
266
278
 
267
- if (!requestContextMenu(target, menu, { origin: trigger, trigger: "button" })) return;
268
-
269
- const rect = trigger.getBoundingClientRect();
270
- if (
271
- openSurface(menu, {
272
- source: target,
273
- x: rect.left,
274
- y: rect.bottom,
275
- placement: "bottom-start",
276
- distance: 4,
277
- dismissOnScroll: true,
278
- restoreFocusTo: trigger,
279
- ...readPanelOptions(menu),
280
- })
281
- ) {
282
- focusMenuContainer(menu);
283
- }
279
+ openContextMenu(target, menu, {
280
+ distance: 4,
281
+ origin: trigger,
282
+ trigger: "button",
283
+ restoreFocusTo: trigger,
284
+ });
284
285
  },
285
286
  { signal: controller.signal },
286
287
  );
package/src/js/dialog.js CHANGED
@@ -37,6 +37,7 @@ const wiredDialogs = new Set();
37
37
  const DIALOG_COMMANDS = ["show-modal", "show", "request-close", "close"];
38
38
  const DIALOG_SELECTOR = "dialog";
39
39
  const DIALOG_TITLE_SELECTOR = "[data-title], h1, h2, h3, h4, h5, h6";
40
+ const STATIC_SHAKE_MS = 250;
40
41
  let uid = 0;
41
42
 
42
43
  function isDialogElement(el) {
@@ -213,7 +214,7 @@ function flashStatic(dialog) {
213
214
  state.staticTimer = win?.setTimeout(() => {
214
215
  dialog.classList.remove(CLASSES.static);
215
216
  state.staticTimer = null;
216
- }, 250);
217
+ }, STATIC_SHAKE_MS);
217
218
  }
218
219
 
219
220
  function finishClose(dialog, returnValue = "") {
package/src/js/dismiss.js CHANGED
@@ -1,3 +1,8 @@
1
+ /*
2
+ * Dismiss command — the generic `--dismiss` action. Hides the resolved target
3
+ * and emits the bubbling `actual:dismiss` event.
4
+ */
5
+
1
6
  import { registerCommands } from "./command.js";
2
7
  import { EVENTS } from "./events.js";
3
8