actual-css 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +24 -9
  3. package/dist/actual-themes.min.css +1 -0
  4. package/dist/actual.css +1244 -915
  5. package/dist/actual.js +1 -3
  6. package/dist/actual.min.css +1 -1
  7. package/dist/optional.css +1048 -0
  8. package/dist/optional.min.css +1 -0
  9. package/docs/design-notes/browser-support.md +94 -14
  10. package/docs/design-notes/cascade-layer.md +1 -1
  11. package/docs/design-notes/enhancement-contract.md +11 -14
  12. package/docs/design-notes/forced-colors.md +105 -0
  13. package/docs/design-notes/naming.md +2 -1
  14. package/docs/design-notes/platform-alignment.md +492 -0
  15. package/docs/design-notes/references.md +8 -0
  16. package/docs/design-notes/widget-primitives.md +37 -1
  17. package/docs/navigation.json +135 -0
  18. package/docs/pages/components/accordion.md +80 -0
  19. package/docs/pages/components/alert.md +203 -0
  20. package/docs/pages/components/aura.md +35 -0
  21. package/docs/pages/components/avatar.md +95 -0
  22. package/docs/pages/components/badge.md +102 -0
  23. package/docs/pages/components/breadcrumb.md +19 -0
  24. package/docs/pages/components/button.md +205 -0
  25. package/docs/pages/components/card.md +183 -0
  26. package/docs/pages/components/chat.md +50 -0
  27. package/docs/pages/components/dialog.md +367 -0
  28. package/docs/pages/components/drawer.md +113 -0
  29. package/docs/pages/components/fab.md +118 -0
  30. package/docs/pages/components/key.md +33 -0
  31. package/docs/pages/components/meter.md +23 -0
  32. package/docs/pages/components/navbar.md +92 -0
  33. package/docs/pages/components/pagination.md +55 -0
  34. package/docs/pages/components/progress.md +29 -0
  35. package/docs/pages/components/skeleton.md +38 -0
  36. package/docs/pages/components/spinner.md +93 -0
  37. package/docs/pages/components/table.md +102 -0
  38. package/docs/pages/enhancements/flyout.md +391 -0
  39. package/docs/pages/enhancements/overview.md +36 -0
  40. package/docs/pages/enhancements/scrollspy.md +71 -0
  41. package/docs/pages/enhancements/status-bar.md +72 -0
  42. package/docs/pages/enhancements/tabs.md +80 -0
  43. package/docs/pages/enhancements/tooltip.md +130 -0
  44. package/docs/pages/examples/overview.md +41 -0
  45. package/docs/pages/forms/choice-cards.md +102 -0
  46. package/docs/pages/forms/controls.md +32 -0
  47. package/docs/pages/forms/detached-actions.md +59 -0
  48. package/docs/pages/forms/floating-field.md +88 -0
  49. package/docs/pages/forms/form-actions.md +56 -0
  50. package/docs/pages/forms/input-policy.md +91 -0
  51. package/docs/pages/forms/joined-controls.md +37 -0
  52. package/docs/pages/forms/labels.md +42 -0
  53. package/docs/pages/forms/otp.md +50 -0
  54. package/docs/pages/forms/overview.md +188 -0
  55. package/docs/pages/forms/password.md +40 -0
  56. package/docs/pages/forms/range.md +17 -0
  57. package/docs/pages/forms/states-and-sizes.md +321 -0
  58. package/docs/pages/forms/sticky-actions.md +187 -0
  59. package/docs/pages/forms/switch.md +31 -0
  60. package/docs/pages/forms/validation.md +241 -0
  61. package/docs/{mental-model.md → pages/foundations/philosophy.md} +33 -18
  62. package/docs/pages/foundations/theming.md +92 -0
  63. package/docs/pages/foundations/tokens.md +421 -0
  64. package/docs/{typography.md → pages/foundations/typography.md} +27 -9
  65. package/docs/pages/guides/bootstrap.md +438 -0
  66. package/docs/pages/guides/introduction.md +157 -0
  67. package/docs/pages/guides/optional-components.md +33 -0
  68. package/docs/pages/guides/progressive-enhancement.md +686 -0
  69. package/docs/pages/guides/tailwind.md +455 -0
  70. package/docs/pages/layout/app-shell.md +25 -0
  71. package/docs/pages/layout/center.md +29 -0
  72. package/docs/pages/layout/cluster.md +57 -0
  73. package/docs/pages/layout/frame.md +30 -0
  74. package/docs/pages/layout/grid.md +142 -0
  75. package/docs/pages/layout/list.md +41 -0
  76. package/docs/pages/layout/media.md +21 -0
  77. package/docs/pages/layout/overview.md +37 -0
  78. package/docs/pages/layout/scroll-snap.md +54 -0
  79. package/docs/pages/layout/sidebar-layout.md +27 -0
  80. package/docs/pages/layout/stack.md +18 -0
  81. package/docs/pages/layout/switcher.md +36 -0
  82. package/docs/pages/layout/topbar.md +23 -0
  83. package/docs/pages/patterns/actions.md +52 -0
  84. package/docs/pages/patterns/code-block.md +36 -0
  85. package/docs/pages/patterns/exclusive-accordion.md +16 -0
  86. package/docs/pages/patterns/hierarchical-nav.md +26 -0
  87. package/docs/pages/patterns/icon-input.md +25 -0
  88. package/docs/pages/patterns/intent-nesting.md +3 -0
  89. package/docs/pages/patterns/navigation-list.md +235 -0
  90. package/docs/pages/patterns/overview.md +58 -0
  91. package/docs/pages/patterns/structured-lists.md +47 -0
  92. package/docs/pages/utilities/accessibility.md +34 -0
  93. package/docs/pages/utilities/flex.md +105 -0
  94. package/docs/pages/utilities/links.md +18 -0
  95. package/docs/pages/utilities/list.md +31 -0
  96. package/docs/pages/utilities/overflow.md +43 -0
  97. package/docs/pages/utilities/overview.md +18 -0
  98. package/docs/pages/utilities/shape.md +25 -0
  99. package/docs/pages/utilities/sizing-wrapping.md +115 -0
  100. package/docs/pages/utilities/spacing.md +79 -0
  101. package/docs/pages/utilities/text.md +43 -0
  102. package/llms.txt +4 -4
  103. package/package.json +20 -12
  104. package/src/css/actual.css +12 -1
  105. package/src/css/base.css +3 -5
  106. package/src/css/components/accordion.css +18 -30
  107. package/src/css/components/actions.css +4 -1
  108. package/src/css/components/alert.css +126 -99
  109. package/src/css/components/avatar.css +16 -10
  110. package/src/css/components/badge.css +55 -35
  111. package/src/css/components/breadcrumb.css +7 -15
  112. package/src/css/components/busy.css +6 -0
  113. package/src/css/components/button.css +78 -32
  114. package/src/css/components/card.css +17 -37
  115. package/src/css/components/drawer.css +30 -13
  116. package/src/css/components/flyout.css +39 -109
  117. package/src/css/components/index.css +1 -0
  118. package/src/css/components/join.css +14 -2
  119. package/src/css/components/key.css +2 -0
  120. package/src/css/components/menu.css +125 -0
  121. package/src/css/components/meter.css +0 -23
  122. package/src/css/components/modal.css +162 -7
  123. package/src/css/components/navbar.css +17 -18
  124. package/src/css/components/overline.css +0 -7
  125. package/src/css/components/pagination.css +1 -1
  126. package/src/css/components/progress.css +8 -26
  127. package/src/css/components/skeleton.css +7 -0
  128. package/src/css/components/spinner.css +6 -7
  129. package/src/css/components/status-bar.css +15 -24
  130. package/src/css/components/surface.css +2 -8
  131. package/src/css/components/tab.css +19 -48
  132. package/src/css/components/table.css +7 -5
  133. package/src/css/components/tooltip.css +14 -7
  134. package/src/css/focus.css +14 -38
  135. package/src/css/forms/choice-card.css +13 -14
  136. package/src/css/forms/choice.css +21 -27
  137. package/src/css/forms/control.css +16 -17
  138. package/src/css/forms/custom-select.css +9 -11
  139. package/src/css/forms/form-actions.css +14 -3
  140. package/src/css/forms/form.css +7 -4
  141. package/src/css/forms/index.css +4 -4
  142. package/src/css/forms/input-icon.css +30 -12
  143. package/src/css/forms/native.css +50 -11
  144. package/src/css/forms/range.css +17 -9
  145. package/src/css/forms/select.css +8 -8
  146. package/src/css/forms/switch.css +14 -12
  147. package/src/css/forms/validation.css +32 -5
  148. package/src/css/grid.css +19 -18
  149. package/src/css/intents.css +2 -2
  150. package/src/css/layout.css +29 -1
  151. package/src/css/optional/aura.css +93 -0
  152. package/src/css/optional/chat.css +105 -0
  153. package/src/css/optional/fab.css +104 -0
  154. package/src/css/optional/floating-field.css +87 -0
  155. package/src/css/optional/index.css +5 -0
  156. package/src/css/optional/layout-extra.css +7 -4
  157. package/src/css/optional/otp.css +115 -0
  158. package/src/css/optional/scroll-snap.css +6 -0
  159. package/src/css/optional/scroller.css +13 -7
  160. package/src/css/optional/typography-fluid.css +5 -0
  161. package/src/css/optional/utilities-extra.css +17 -2
  162. package/src/css/prose.css +18 -8
  163. package/src/css/reset.css +0 -10
  164. package/src/css/theme.css +21 -5
  165. package/src/css/themes/cyberpunk.css +1 -1
  166. package/src/css/themes/dim.css +3 -3
  167. package/src/css/themes/edge.css +8 -10
  168. package/src/css/themes/index.css +3 -3
  169. package/src/css/themes/{dark.css → indigo.css} +5 -4
  170. package/src/css/themes/neon.css +5 -5
  171. package/src/css/themes/square.css +0 -3
  172. package/src/css/tokens.css +74 -20
  173. package/src/css/utilities.css +8 -10
  174. package/src/css/variants.css +37 -65
  175. package/src/js/command.js +1 -3
  176. package/src/js/context-menu.js +67 -35
  177. package/src/js/dialog.js +68 -79
  178. package/src/js/dismiss.js +15 -0
  179. package/src/js/enhance.js +54 -23
  180. package/src/js/enhancement-loader.js +301 -0
  181. package/src/js/events.js +3 -0
  182. package/src/js/filter.js +3 -3
  183. package/src/js/floating.js +101 -131
  184. package/src/js/flyout.js +104 -87
  185. package/src/js/focus-group.js +116 -0
  186. package/src/js/focus.js +6 -1
  187. package/src/js/index.js +4 -2
  188. package/src/js/input.js +1 -1
  189. package/src/js/keys.js +27 -0
  190. package/src/js/mask.js +7 -6
  191. package/src/js/menu.js +87 -34
  192. package/src/js/parse-config.js +47 -0
  193. package/src/js/password.js +9 -1
  194. package/src/js/scrollspy.js +12 -4
  195. package/src/js/selectors.js +0 -7
  196. package/src/js/status.js +3 -1
  197. package/src/js/surface.js +185 -136
  198. package/src/js/tab.js +36 -53
  199. package/src/js/tooltip.js +53 -31
  200. package/src/js/validation.js +34 -8
  201. package/dist/actual.js.map +0 -31
  202. package/docs/README.md +0 -28
  203. package/docs/components.md +0 -860
  204. package/docs/design-notes/previous-work.md +0 -5
  205. package/docs/forms.md +0 -1265
  206. package/docs/javascript.md +0 -496
  207. package/docs/layout.md +0 -480
  208. package/docs/patterns.md +0 -504
  209. package/docs/reserved.md +0 -7
  210. package/docs/tokens.md +0 -455
  211. package/docs/ui.md +0 -929
  212. package/docs/utilities.md +0 -384
  213. package/src/css/dialog-fallback.css +0 -56
  214. package/src/js/dialog-fallback.js +0 -163
@@ -1,4 +1,21 @@
1
1
  :root {
2
+ /*
3
+ * Global framework tokens.
4
+ *
5
+ * Public: properties in the named groups below are public API (typography,
6
+ * spacing, shape, motion, focus, controls, viewport, z-index, icons) unless
7
+ * a group is explicitly marked Internal or Derived.
8
+ *
9
+ * Derived: "Theme-derived aliases" and "State tokens" reference intent and
10
+ * surface tokens and follow the theme; do not hard-code values into them.
11
+ * Internal: indicator offsets, --bar-height, --disabled-opacity and the
12
+ * like are consumed by components, not authored.
13
+ *
14
+ * Component-local hooks and internal state relays live with their
15
+ * components. See docs/pages/foundations/tokens.md for the public/internal
16
+ * token contract.
17
+ */
18
+
2
19
  /* Typography */
3
20
  --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
4
21
  --font-mono: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
@@ -16,20 +33,22 @@
16
33
  --font-size-md: 1rem;
17
34
  --font-size-lg: 1.125rem;
18
35
 
19
- /* Spacing */
36
+ /* Spacing — official steps are named in tens so the scale stays open for
37
+ application themes to intercalate intermediate values (--space-15,
38
+ --space-45…) without colliding with future framework steps. */
20
39
  --gap: 0.75rem;
21
- --variant-space: var(--space-4);
22
- --space-1: 0.25rem;
23
- --space-2: 0.5rem;
24
- --space-3: 0.75rem;
25
- --space-4: 1rem;
26
- --space-5: 1.5rem;
27
- --space-6: 2rem;
40
+ --density-space: var(--space-40);
41
+ --density-compact-size: 1.5rem;
42
+ --space-10: 0.25rem;
43
+ --space-20: 0.5rem;
44
+ --space-30: 0.75rem;
45
+ --space-40: 1rem;
46
+ --space-50: 1.5rem;
47
+ --space-60: 2rem;
28
48
 
29
49
  /* Shape */
30
50
  --border-width: 1px;
31
51
  --radius-full: 9999px;
32
- --shadow-xs: 0 1px 2px rgb(0 0 0 / 0.04);
33
52
  --shadow: 0 1px 3px rgb(0 0 0 / 0.05), 0 4px 12px rgb(0 0 0 / 0.04);
34
53
  --shadow-popout: 0 0.25rem 0.75rem rgb(0 0 0 / 0.15);
35
54
 
@@ -53,7 +72,6 @@
53
72
  the circle-aware inset (1 - 1/√2) / 2 ≈ 14.65%; the ring surrounds the
54
73
  dot with the parent surface color. */
55
74
  --indicator-offset: 14.65%;
56
- --indicator-ring: var(--surface);
57
75
 
58
76
  /* Shared backdrop — consumed by modal, drawer, and mobile flyout sheets. */
59
77
  --backdrop-color: rgb(0 0 0);
@@ -64,7 +82,8 @@
64
82
  --bar-height: 0.65rem;
65
83
 
66
84
  /* Controls — inputs and buttons share --control-size so heights always match.
67
- .sm/.lg (variants.css) switch --control-size and --control-font-size. */
85
+ .sm/.lg (variants.css) switch --control-size; --control-font-size stays
86
+ at its baseline because density does not change typography. */
68
87
  --control-size-sm: 2rem;
69
88
  --control-size-md: 2.375rem;
70
89
  --control-size-lg: 2.75rem;
@@ -72,19 +91,38 @@
72
91
  --control-pad-x: 1em;
73
92
  --control-font-size: var(--font-size-md);
74
93
  --disabled-opacity: 0.65;
94
+ }
95
+
96
+ /* Theme-derived aliases. These reference intent/surface tokens, so they must
97
+ be re-declared on every theme boundary: a custom property resolves its
98
+ var() references at computed-value time on the element that declares it,
99
+ so an alias declared only on :root is inherited as an already-resolved
100
+ value and would not follow a [data-theme] island's overridden tokens.
101
+ Themes that want a distinct alias override it explicitly afterwards. */
102
+ :root,
103
+ [data-theme] {
104
+ /* State tokens — semantic colors for selected/pressed/current and disabled.
105
+ Components consume these instead of hard-wiring --primary/--text-subtle
106
+ so forced-colors can override them in one place. */
107
+ --state-selected: var(--primary);
108
+ --state-selected-fg: var(--primary-fg);
109
+ --state-disabled: var(--text-subtle);
110
+
111
+ /* Status dot ring — follows the island's surface. */
112
+ --indicator-ring: var(--surface);
113
+ }
75
114
 
115
+ :root {
76
116
  /* Variant tuning — themes may adjust generated soft surfaces without
77
117
  replacing the component rules. */
78
118
  --soft-bg-mix: 88%;
79
119
  --soft-border-mix: 65%;
80
120
  --soft-hover-alpha: 12%;
81
121
 
82
- /* Shared icons */
83
- --icon-chevron-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
84
- --icon-check-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
85
- /* Native select uses background-image, which cannot inherit currentColor.
86
- The stroke matches the default theme's text-subtle (hue 258 neutral). */
87
- --icon-chevron-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
122
+ /* Shared icon assets. Override these URLs when a strict CSP disallows data:. */
123
+ --icon-chevron: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%238a8594' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");
124
+ --icon-check: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='20 6 9 17 4 12'/%3E%3C/svg%3E");
125
+ --icon-close: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cline x1='6' y1='6' x2='18' y2='18'/%3E%3Cline x1='18' y1='6' x2='6' y2='18'/%3E%3C/svg%3E");
88
126
 
89
127
  /* Viewport size tokens — fallback vh/vw with dvh/vi enhancement.
90
128
  Consumed by full-viewport components (app-shell, drawer, modal, flyout).
@@ -93,13 +131,24 @@
93
131
  --viewport-block: 100vh;
94
132
  --viewport-inline: 100vw;
95
133
 
96
- /* Layering */
134
+ /* Z-index scale — used ONLY by non-dialog overlays (see the layering
135
+ note below for the full model). Actual CSS reserves multiples of 10 for
136
+ framework layers; applications may use intermediate values for local
137
+ layering. */
97
138
  --z-sticky: 10;
98
139
  --z-menu: 20;
99
140
  --z-tooltip: 50;
100
141
  --z-status: 60;
101
142
  }
102
143
 
144
+ @media (forced-colors: active) {
145
+ :root {
146
+ --state-selected: Highlight;
147
+ --state-selected-fg: HighlightText;
148
+ --state-disabled: GrayText;
149
+ }
150
+ }
151
+
103
152
  @supports (block-size: 100dvh) {
104
153
  :root {
105
154
  --viewport-block: 100dvh;
@@ -113,12 +162,17 @@
113
162
  }
114
163
 
115
164
  @supports (color: color-mix(in oklch, red, white)) {
116
- :root {
117
- --shadow-xs: 0 1px 2px color-mix(in oklch, var(--shadow-color) 4%, transparent);
165
+ :root,
166
+ [data-theme] {
167
+ /* Shadows derive from --shadow-color (theme.css) — re-declared on every
168
+ theme boundary so islands follow their own shadow-color. */
118
169
  --shadow:
119
170
  0 1px 3px color-mix(in oklch, var(--shadow-color) 5%, transparent),
120
171
  0 4px 12px color-mix(in oklch, var(--shadow-color) 4%, transparent);
121
172
  --shadow-popout: 0 0.25rem 0.75rem color-mix(in oklch, var(--shadow-color) 15%, transparent);
173
+ }
174
+
175
+ :root {
122
176
  --backdrop-fill: color-mix(
123
177
  in oklch,
124
178
  var(--backdrop-color) calc(var(--backdrop-opacity) * 100%),
@@ -30,7 +30,7 @@
30
30
  min-inline-size: 0;
31
31
  }
32
32
 
33
- .nowrap {
33
+ .text-nowrap {
34
34
  white-space: nowrap;
35
35
  }
36
36
 
@@ -125,15 +125,13 @@
125
125
  text-decoration: none;
126
126
  }
127
127
 
128
- @media (hover: hover) {
129
- .link-muted:hover {
130
- color: var(--text);
131
- }
128
+ .link-muted:hover {
129
+ color: var(--text);
132
130
  }
133
131
 
134
132
  /* Spacing step helpers — semantic scale (sm / default / lg).
135
133
  Names map to logical properties for writing-direction safety. The default
136
- step uses --space-4, which matches the framework's built-in padding on
134
+ step uses --space-40, which matches the framework's built-in padding on
137
135
  cards, dialogs, and form-actions. */
138
136
 
139
137
  /* Gap */
@@ -143,20 +141,20 @@
143
141
 
144
142
  /* Padding — block (vertical) */
145
143
  .py {
146
- padding-block: var(--variant-space);
144
+ padding-block: var(--density-space);
147
145
  }
148
146
 
149
147
  /* Padding — inline (horizontal) */
150
148
  .px {
151
- padding-inline: var(--variant-space);
149
+ padding-inline: var(--density-space);
152
150
  }
153
151
 
154
152
  /* Margin — block-start */
155
153
  .mbs {
156
- margin-block-start: var(--variant-space);
154
+ margin-block-start: var(--density-space);
157
155
  }
158
156
 
159
157
  /* Margin — block-end */
160
158
  .mbe {
161
- margin-block-end: var(--variant-space);
159
+ margin-block-end: var(--density-space);
162
160
  }
@@ -2,15 +2,20 @@
2
2
  * Cross-cutting variant token mapping.
3
3
  *
4
4
  * Variant scope: .solid/.soft/.outline set --ui-* for components that opt into
5
- * shared variants. Component-specific defaults (.btn solid-by-default, .badge
6
- * solid-by-default, .alert soft-by-default) live with their components:
5
+ * shared variants. .inverted is a surface modifier, not a variant: it also
6
+ * paints the element directly (background/color/border-color) so it works on
7
+ * any block, competing with component surface treatments (.card.subtle).
8
+ * Component-specific defaults (.btn solid-by-default, .badge
9
+ * soft-by-default, .alert soft-by-default) live with their components:
7
10
  * components/button.css — .btn default solid + .btn.ghost + .btn.link
8
- * components/badge.css — .badge default solid
9
- * components/alert.css — .alert default + .alert.solid/.alert.outline
11
+ * components/badge.css — .badge default soft
12
+ * components/alert.css — .alert default soft (consumes --ui-* directly)
10
13
  *
11
- * .sm/.lg set shared size tokens. Controls consume --control-*; non-control
12
- * components may consume --variant-* when the variant should affect their
13
- * local density. Compact components may consume --variant-compact-*.
14
+ * .sm/.lg set shared density tokens: spacing rhythm and geometry only.
15
+ * Density never changes typography — controls keep their font-size and
16
+ * content components keep theirs. Controls consume --control-size; non-control
17
+ * components may consume --density-* when the density context should affect
18
+ * their local rhythm. Compact components may consume --density-compact-size.
14
19
  * Components with bespoke sizing (spinner, avatar) define local .sm/.lg;
15
20
  * applying .sm/.lg to card, dialog, accordion, navbar, table is
16
21
  * a no-op.
@@ -36,27 +41,34 @@
36
41
  --ui-border: var(--intent, var(--border));
37
42
  }
38
43
 
44
+ /* Inverse surface treatment. Components (e.g. .card) compose through --ui-*;
45
+ the direct background/color/border-color also make .inverted useful on a
46
+ generic element. --heading is set so global headings (var(--heading, inherit))
47
+ do not inherit an outside heading color onto the dark surface. */
48
+ .inverted {
49
+ --ui-bg: var(--surface-solid);
50
+ --ui-fg: var(--surface);
51
+ --ui-border: transparent;
52
+
53
+ --heading: var(--surface);
54
+ --busy-overlay-bg: var(--surface-solid);
55
+
56
+ background: var(--ui-bg);
57
+ color: var(--ui-fg);
58
+ border-color: var(--ui-border);
59
+ }
60
+
39
61
  .sm {
40
- --gap: var(--space-2);
41
- --variant-space: var(--space-2);
62
+ --gap: var(--space-20);
63
+ --density-space: var(--space-20);
42
64
  --control-size: var(--control-size-sm);
43
- --control-font-size: var(--font-size-sm);
44
- --variant-font-size: var(--font-size-sm);
45
- --variant-icon-size: 1rem;
46
- --variant-pad-block: var(--space-3);
47
- --variant-compact-size: 1.25rem;
48
- --variant-compact-font-size: 0.75rem;
65
+ --density-compact-size: 1.25rem;
49
66
  }
50
67
  .lg {
51
- --gap: var(--space-5);
52
- --variant-space: var(--space-5);
68
+ --gap: var(--space-50);
69
+ --density-space: var(--space-50);
53
70
  --control-size: var(--control-size-lg);
54
- --control-font-size: var(--font-size-lg);
55
- --variant-font-size: var(--font-size-lg);
56
- --variant-icon-size: 1.5rem;
57
- --variant-pad-block: var(--space-5);
58
- --variant-compact-size: 1.875rem;
59
- --variant-compact-font-size: var(--font-size-sm);
71
+ --density-compact-size: 1.875rem;
60
72
  }
61
73
 
62
74
  /* Modern color enhancement. Baseline variant values above remain usable
@@ -74,45 +86,5 @@
74
86
  }
75
87
  }
76
88
 
77
- /* Interactive hover recipe shared by .btn and .badge. Declared outside :hover
78
- on purpose: --ui-hover-bg is only consumed inside their @media (hover: hover)
79
- rules, so these values are inert until hovered. :is() carries real
80
- specificity so the components' :where() intent boundaries
81
- (--ui-hover-bg: initial) cannot override the recipe. The solid/soft overlay
82
- darkening lives in the consuming components. */
83
- :is(.btn, a.badge, button.badge):is(.outline, .ghost) {
84
- --ui-hover-bg: var(--surface-subtle);
85
- }
86
-
87
- @supports (color: color-mix(in oklch, red, white)) {
88
- /* Solid has no literal class requirement: bare .btn/.badge default to solid,
89
- so this must match anything that isn't one of the other variants rather
90
- than an explicit .solid class. Stacks with the box-shadow overlay the
91
- consuming components apply on top for extra depth. */
92
- :is(.btn, a.badge, button.badge):not(:where(.soft, .outline, .ghost, .link)) {
93
- --ui-hover-bg: color-mix(in oklch, var(--intent, var(--neutral)) 88%, black);
94
- }
95
-
96
- /* Same recipe as the resting .soft bg with the surface share reduced by
97
- --soft-hover-alpha: stays opaque, so it reads on any backdrop instead of
98
- letting the container show through on hover. */
99
- :is(.btn, a.badge, button.badge):is(.soft) {
100
- --ui-hover-bg: color-mix(
101
- in oklch,
102
- var(--surface) calc(var(--soft-bg-mix) - var(--soft-hover-alpha)),
103
- var(--intent, var(--neutral))
104
- );
105
- }
106
-
107
- /* Mixed against the opaque --surface-subtle rather than transparent: a
108
- transparent-based tint reads clean on light surfaces but goes muddy and
109
- low-contrast on dark ones, since the same low alpha lands very
110
- differently over a light vs. a near-black backdrop. */
111
- :is(.btn, a.badge, button.badge):is(.outline) {
112
- --ui-hover-bg: color-mix(in oklch, var(--surface-subtle) 92%, var(--intent, var(--neutral)));
113
- }
114
-
115
- :is(.btn, a.badge, button.badge):is(.ghost) {
116
- --ui-hover-bg: color-mix(in oklch, var(--intent, var(--text)) 8%, transparent);
117
- }
118
- }
89
+ /* Interactive hover recipes live in the components that consume them:
90
+ components/button.css and components/badge.css. */
package/src/js/command.js CHANGED
@@ -25,9 +25,7 @@ function commandNames(commands, caller) {
25
25
  // Native command keywords are ASCII case-insensitive. Custom commands keep
26
26
  // their exact spelling, as required by the command invoker contract.
27
27
  function commandKey(command) {
28
- return command.startsWith("--")
29
- ? command
30
- : command.replace(/[A-Z]/g, (character) => character.toLowerCase());
28
+ return command.startsWith("--") ? command : command.toLowerCase();
31
29
  }
32
30
 
33
31
  /**
@@ -1,17 +1,16 @@
1
1
  import enhance from "./enhance.js";
2
2
  import { EVENTS } from "./events.js";
3
- import { focusFirstMenuItem, hasMenuItems } from "./menu.js";
4
- import { closeSurface, isSurfaceOpen, openSurface, prepareSurface } from "./surface.js";
3
+ import { connectMenu, focusFirstMenuItem, hasMenuItems } from "./menu.js";
4
+ import { closeSurface, isSurfaceOpen, openSurface, retainSurface } from "./surface.js";
5
5
 
6
6
  const LONG_PRESS_MS = 450;
7
+ const LONG_PRESS_CLICK_SUPPRESSION_MS = 750;
7
8
  const MOVE_TOLERANCE = 10;
8
9
  const CONTEXT_MENU_SELECTOR = 'menu, [role="menu"]';
9
10
  const CONTEXT_TARGET_SELECTOR = "[data-context-menu]";
10
11
  const contextMap = new WeakMap();
11
12
  const contextByMenu = new WeakMap();
12
13
 
13
- // Returns the detail from the last accepted `actual:context-menu` event for a
14
- // menu. This is intentionally optional: static menus need no application JS.
15
14
  export function contextFor(menu) {
16
15
  return contextByMenu.get(menu) || null;
17
16
  }
@@ -48,27 +47,29 @@ function getLongPressDelay(target, menu) {
48
47
  if (value === "") return LONG_PRESS_MS;
49
48
 
50
49
  const delay = Number.parseInt(value, 10);
51
- return Number.isFinite(delay) ? delay : LONG_PRESS_MS;
50
+ return Number.isFinite(delay) ? Math.max(0, delay) : LONG_PRESS_MS;
51
+ }
52
+
53
+ function readPanelOptions(menu) {
54
+ const opts = { autoClose: "true" };
55
+ const ds = menu.dataset;
56
+ if (ds.flyoutMobile) opts.mobile = ds.flyoutMobile;
57
+ if (ds.flyoutAutoClose) opts.autoClose = ds.flyoutAutoClose;
58
+ return opts;
52
59
  }
53
60
 
54
61
  function focusMenuContainer(menu) {
55
62
  if (!menu.hasAttribute("tabindex")) {
56
63
  menu.tabIndex = -1;
57
64
  }
58
- menu.focus();
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.
67
+ menu.focus({ preventScroll: true });
59
68
  }
60
69
 
61
- // Pointer-triggered opens (right-click, long-press) focus the menu container,
62
- // not the first item: a pointer-set focus ring is invisible (:focus-visible
63
- // doesn't match), so focusing "Open" makes the first ArrowDown look like it
64
- // skips to the second item. Focusing the container keeps the ring hidden and
65
- // lets keys.js's out-of-list fallback (indexOf === -1) land ArrowDown/ArrowUp
66
- // on the first/last item, matching native OS context menus. Keyboard-triggered
67
- // opens (openFromKeyboard) still focus the first item directly.
68
70
  function focusMenu(menu, mode) {
69
- if (!hasMenuItems(menu)) return;
70
71
  if (mode === "first-item") {
71
- focusFirstMenuItem(menu);
72
+ if (hasMenuItems(menu)) focusFirstMenuItem(menu);
72
73
  } else {
73
74
  focusMenuContainer(menu);
74
75
  }
@@ -101,9 +102,11 @@ function openContextMenu(context, menu, opts = {}) {
101
102
  y: opts.y,
102
103
  placement: opts.placement || "bottom-start",
103
104
  distance: opts.distance ?? 2,
104
- mobile: opts.mobile || menu.dataset.flyoutMobile || "auto",
105
+ dismissOnScroll: true,
106
+ mobile: opts.mobile || "auto",
105
107
  scope: opts.scope || getContextScope(context),
106
108
  restoreFocusTo: opts.restoreFocusTo,
109
+ ...readPanelOptions(menu),
107
110
  })
108
111
  ) {
109
112
  focusMenu(menu, opts.focus);
@@ -137,11 +140,16 @@ function connectContextTarget(target) {
137
140
  const menu = menuFor(target);
138
141
  if (!menu) return;
139
142
 
140
- prepareSurface(menu);
143
+ const release = retainSurface(menu);
141
144
  const controller = new AbortController();
145
+ const releaseMenu = connectMenu(menu, {
146
+ close: (menu) => closeSurface(menu),
147
+ });
142
148
  const state = {
143
149
  controller,
150
+ releaseMenu,
144
151
  menu,
152
+ release,
145
153
  timer: null,
146
154
  pointerId: null,
147
155
  startX: 0,
@@ -190,7 +198,7 @@ function connectContextTarget(target) {
190
198
  state.startY = e.clientY;
191
199
  state.timer = setTimeout(() => {
192
200
  state.timer = null;
193
- state.suppressClickUntil = Date.now() + 750;
201
+ state.suppressClickUntil = Date.now() + LONG_PRESS_CLICK_SUPPRESSION_MS;
194
202
  openContextMenu(target, menu, {
195
203
  x: state.startX,
196
204
  y: state.startY,
@@ -238,6 +246,45 @@ function connectContextTarget(target) {
238
246
  { signal: controller.signal, capture: true },
239
247
  );
240
248
 
249
+ target.addEventListener(
250
+ "click",
251
+ (e) => {
252
+ const trigger = e.target.closest("[data-context-menu-trigger]");
253
+ if (
254
+ !trigger ||
255
+ trigger.getAttribute("aria-controls") !== target.getAttribute("data-context-menu")
256
+ )
257
+ return;
258
+
259
+ e.preventDefault();
260
+ e.stopPropagation();
261
+
262
+ if (isSurfaceOpen(menu)) {
263
+ closeSurface(menu);
264
+ return;
265
+ }
266
+
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
+ }
284
+ },
285
+ { signal: controller.signal },
286
+ );
287
+
241
288
  contextMap.set(target, state);
242
289
  }
243
290
 
@@ -246,33 +293,18 @@ function disconnectContextTarget(target) {
246
293
  if (!state) return;
247
294
  clearLongPress(state);
248
295
  state.controller.abort();
296
+ state.releaseMenu?.();
249
297
  if (contextFor(state.menu)?.context === target) {
250
298
  contextByMenu.delete(state.menu);
251
299
  closeSurface(state.menu, { restoreFocus: false });
252
300
  }
301
+ state.release();
253
302
  contextMap.delete(target);
254
303
  }
255
304
 
256
- function onSurfaceOpen(event) {
257
- const options = event.detail?.options;
258
- const trigger = options?.trigger;
259
- const context = trigger?.closest?.(CONTEXT_TARGET_SELECTOR);
260
- if (!context || menuFor(context) !== event.detail?.surface) return;
261
- if (!requestContextMenu(context, event.detail.surface, { origin: trigger, trigger: "button" })) {
262
- event.preventDefault();
263
- return;
264
- }
265
- options.source = context;
266
- options.restoreFocusTo = trigger;
267
- }
268
-
269
305
  enhance({
270
306
  [CONTEXT_TARGET_SELECTOR]: (target) => {
271
307
  connectContextTarget(target);
272
308
  return () => disconnectContextTarget(target);
273
309
  },
274
310
  });
275
-
276
- if (typeof document !== "undefined") {
277
- document.addEventListener(EVENTS.surfaceOpen, onSurfaceOpen);
278
- }