@ropav/styles 0.3.0 → 0.4.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 (62) hide show
  1. package/README.md +155 -19
  2. package/dist/components/accordion.css +7 -0
  3. package/dist/components/alert.css +8 -0
  4. package/dist/components/autocomplete.css +8 -0
  5. package/dist/components/avatar.css +9 -0
  6. package/dist/components/badge.css +7 -0
  7. package/dist/components/button.css +27 -0
  8. package/dist/components/calendar-year-picker.css +1 -0
  9. package/dist/components/calendar.css +8 -0
  10. package/dist/components/card.css +21 -0
  11. package/dist/components/chip.css +21 -0
  12. package/dist/components/close-button.css +8 -0
  13. package/dist/components/color-input-group.css +9 -0
  14. package/dist/components/color-swatch-picker.css +11 -0
  15. package/dist/components/date-input-group.css +9 -0
  16. package/dist/components/dropdown/dropdown.styles.d.ts.map +1 -1
  17. package/dist/components/dropdown/dropdown.styles.js +1 -1
  18. package/dist/components/dropdown.css +16 -11
  19. package/dist/components/input-group.css +9 -0
  20. package/dist/components/input.css +10 -0
  21. package/dist/components/menu.css +6 -0
  22. package/dist/components/meter.css +12 -0
  23. package/dist/components/number-field.css +8 -0
  24. package/dist/components/pagination.css +4 -0
  25. package/dist/components/progress-bar.css +12 -0
  26. package/dist/components/progress-circle.css +12 -0
  27. package/dist/components/radio.css +7 -0
  28. package/dist/components/range-calendar.css +28 -0
  29. package/dist/components/search-field.css +8 -0
  30. package/dist/components/select.css +9 -0
  31. package/dist/components/separator.css +18 -1
  32. package/dist/components/skeleton.css +9 -0
  33. package/dist/components/slider.css +61 -0
  34. package/dist/components/surface.css +24 -0
  35. package/dist/components/switch.css +30 -0
  36. package/dist/components/table.css +8 -0
  37. package/dist/components/tabs.css +28 -0
  38. package/dist/components/tag.css +10 -0
  39. package/dist/components/textarea.css +10 -0
  40. package/dist/components/toggle-button.css +10 -0
  41. package/dist/components/toolbar.css +7 -0
  42. package/dist/components/typography.css +21 -0
  43. package/dist/index.css +7 -2
  44. package/dist/ropav.min.css +1 -1
  45. package/dist/themes/airbnb.css +226 -0
  46. package/dist/themes/all.css +16 -0
  47. package/dist/themes/coinbase.css +226 -0
  48. package/dist/themes/{default/variables.css → default.css} +12 -5
  49. package/dist/themes/discord.css +226 -0
  50. package/dist/themes/lavender.css +226 -0
  51. package/dist/themes/mint.css +226 -0
  52. package/dist/themes/netflix.css +226 -0
  53. package/dist/themes/rabbit.css +226 -0
  54. package/dist/themes/sky.css +226 -0
  55. package/dist/themes/spotify.css +226 -0
  56. package/dist/themes/uber.css +226 -0
  57. package/dist/utilities/index.css +70 -0
  58. package/dist/variants/index.css +5 -15
  59. package/package.json +2 -3
  60. package/src/components/dropdown/dropdown.styles.ts +3 -1
  61. package/dist/themes/default/components/index.css +0 -6
  62. package/dist/themes/default/index.css +0 -8
package/README.md CHANGED
@@ -35,7 +35,7 @@ That single line pulls in, in layer order (`theme, base, components, utilities`)
35
35
  - Tailwind CSS v4 and `tw-animate-css`
36
36
  - base styles and the scrollbar system
37
37
  - the component layer — 85 files, one per component
38
- - the default theme: variables for light and dark, plus per-component theme overrides
38
+ - the default theme: tokens for light and dark
39
39
  - utilities and custom variants
40
40
 
41
41
  ### Importing only what you need
@@ -45,11 +45,12 @@ That single line pulls in, in layer order (`theme, base, components, utilities`)
45
45
 
46
46
  @import "@ropav/styles/components/button.css" layer(components);
47
47
  @import "@ropav/styles/components/chip.css" layer(components);
48
- @import "@ropav/styles/themes/default" layer(theme);
48
+ @import "@ropav/styles/themes/shared/theme.css";
49
+ @import "@ropav/styles/themes/default";
49
50
  ```
50
51
 
51
- > The granular subpaths — `./components/*.css`, `./base`, `./base/*.css`, `./themes/*`, `./utilities`,
52
- > `./variants` — exist **only in the published tarball**; `clean-package.config.json` writes them into
52
+ > The granular subpaths — `./components/*.css`, `./base`, `./base/*.css`, `./themes/*`, `./themes/*.css`,
53
+ > `./utilities`, `./variants` — exist **only in the published tarball**; `clean-package.config.json` writes them into
53
54
  > `exports` at `prepack` time. Inside the workspace, import the files from `packages/styles/` by relative path
54
55
  > instead.
55
56
 
@@ -74,14 +75,14 @@ packages/styles/
74
75
  │ └── scrollbar.css # Scrollbar system
75
76
  ├── components/ # 85 CSS files, one per component
76
77
  ├── themes/
77
- │ ├── default/
78
- ├── variables.css # Light and dark token sets
79
- ├── index.css # Theme entry point
80
- │ │ └── components/ # Per-component theme overrides
78
+ │ ├── default.css # Default theme — hand-written, light and dark token sets
79
+ │ ├── sky.css … rabbit.css # Ten more themes generated, do not edit
80
+ │ ├── all.css # Every bundled theme, for docs and playgrounds
81
81
  │ └── shared/
82
82
  │ └── theme.css # @theme block — derived values, radius scale, easing curves
83
83
  ├── utilities/index.css # Tailwind v4 @utility definitions
84
84
  ├── variants/index.css # Tailwind v4 @custom-variant definitions
85
+ ├── scripts/themes/ # Build-time theme generator — not published
85
86
  └── src/ # TypeScript: tv() variants + shared utility class strings
86
87
  ```
87
88
 
@@ -113,31 +114,166 @@ component that publishes it as an attribute.
113
114
 
114
115
  ## Theming
115
116
 
116
- Light is the default on `:root`; dark turns on with a `.dark` class or `[data-theme="dark"]`:
117
+ Two independent axes. `data-theme` picks the palette, a `light`/`dark` class picks the appearance:
117
118
 
118
119
  ```html
119
- <html data-theme="dark"></html>
120
+ <html data-theme="netflix" class="dark"></html>
120
121
  ```
121
122
 
122
- Every token is a CSS custom property, so overriding them in your own stylesheet is enough to retheme — no
123
- rebuild of this package:
123
+ Either can be left out. With no `data-theme` you get the default theme; with no class you get light.
124
+ The default theme also answers to `[data-theme="light"]` and `[data-theme="dark"]`, which is what it
125
+ did before the other themes existed.
126
+
127
+ ### Bundled themes
128
+
129
+ Eleven, ported from HeroUI's theme gallery. The default one is already in `@ropav/styles`; the rest are
130
+ opt-in, one file each:
131
+
132
+ ```css
133
+ @import "@ropav/styles";
134
+ @import "@ropav/styles/themes/netflix";
135
+ ```
136
+
137
+ | `data-theme` | | `data-theme` | | `data-theme` |
138
+ | --- | --- | --- | --- | --- |
139
+ | `default` | | `netflix` | | `airbnb` |
140
+ | `sky` | | `uber` | | `discord` |
141
+ | `lavender` | | `spotify` | | `rabbit` |
142
+ | `mint` | | `coinbase` | | |
143
+
144
+ Each carries both a light and a dark palette, so a theme is a brand rather than an appearance. There is
145
+ no `netflix-dark` — it is `data-theme="netflix"` plus `class="dark"`.
146
+
147
+ `themes/all.css` pulls in all of them at once. That is for docs and playgrounds; an app should import
148
+ only the themes it actually offers, since each is around 10 kB before compression.
149
+
150
+ The ten non-default themes are **generated** — edit `scripts/themes/presets.ts` and run
151
+ `pnpm generate:themes`, never the CSS. A preset is four numbers (accent lightness, chroma and hue, plus
152
+ the chroma of the neutral ramp), a radius pair, and any exact brand colours that should beat the
153
+ calculated ones.
154
+
155
+ ### Retheming
156
+
157
+ Every token is a CSS custom property, so overriding them in your own stylesheet is enough — no rebuild
158
+ of this package, and no need to go through a theme file at all:
124
159
 
125
160
  ```css
126
161
  :root {
127
162
  --accent: oklch(0.62 0.19 253.83);
128
163
  --radius: 0.5rem;
129
- --field-radius: calc(var(--radius) * 1.5);
130
164
  }
131
165
  ```
132
166
 
133
- Tokens cover base colors (`--background`, `--surface`, `--overlay`, `--muted`), interactive and status colors
134
- (`--accent`, `--success`, `--warning`, `--danger`, each with a `-foreground` and a derived `-hover`), form
135
- fields (`--field-background`, `--field-border`, `--field-radius`, …), layout knobs (`--spacing`,
136
- `--border-width`, `--radius`, `--ring-offset-width`, `--cursor-interactive`), the scrollbar set, and shadows.
167
+ Author CSS outside a cascade layer outranks everything here, so these win wherever you put them.
137
168
 
138
- **`themes/default/variables.css` is the source of truth read it rather than a list in a README**, which goes
169
+ To add a theme rather than change the default one, write the same token block under your own attribute.
170
+ It has to redeclare the *derived* tokens too, not just the authored ones: a custom property substitutes
171
+ `var()` where it is declared, so an `--accent-hover` inherited from `:root` would still be mixed from
172
+ the root's `--accent`. Copy a generated theme and edit it, or generate one.
173
+
174
+ ```css
175
+ @layer theme {
176
+ [data-theme="ocean"] {
177
+ color-scheme: light;
178
+ --accent: oklch(0.62 0.14 220);
179
+ --accent-hover: color-mix(in oklab, var(--accent) 90%, var(--accent-foreground) 10%);
180
+ /* … */
181
+ }
182
+ }
183
+ ```
184
+
185
+ A theme only needs to carry colours. Everything keyed on neither the palette nor the appearance —
186
+ `--spacing`, `--cursor-*`, the primitives, the shadows, `--backdrop` — stays on `:root` and `.dark` in
187
+ `themes/default.css`, both of which keep matching an element that carries a `data-theme`.
188
+
189
+ ### Tokens
190
+
191
+ Base colors (`--background`, `--surface`, `--overlay`, `--muted`), interactive and status colors
192
+ (`--accent`, `--success`, `--warning`, `--danger`, each with a `-foreground` and a derived `-hover` and
193
+ `-soft`), form fields (`--field-background`, `--field-border`, `--field-radius`, …), layout knobs
194
+ (`--spacing`, `--border-width`, `--radius`, `--ring-offset-width`, `--cursor-interactive`), the
195
+ scrollbar set, and shadows.
196
+
197
+ **`themes/default.css` is the source of truth — read it rather than a list in a README**, which goes
139
198
  stale the moment a token moves. `themes/shared/theme.css` holds what is derived from those tokens: the
140
- `--radius-xs` … `--radius-4xl` scale, `color-mix()` hover and soft variants, and the easing curves.
199
+ `--radius-xs` … `--radius-4xl` scale and the easing curves.
200
+
201
+ ### Reduced motion
202
+
203
+ `data-reduce-motion="true"` on any ancestor forces animations off, `"false"` forces them on regardless
204
+ of the OS setting, and with neither the `prefers-reduced-motion` media query decides. Defined as the
205
+ `motion-reduce` / `motion-safe` variants in `variants/index.css`.
206
+
207
+ ### Forced colors
208
+
209
+ Forced Colors Mode - Windows High Contrast - replaces author colours with the user's own palette and
210
+ strips `box-shadow` outright. That second part is what makes it more than a colour question here:
211
+ every ring in this library *is* a `box-shadow`, because that is what `ring-*` compiles to, and it sits
212
+ on top of `outline-none`. Left alone, a focused control would have no indicator at all.
213
+
214
+ Nothing to opt into. The three focus utilities - `focus-ring`, `focus-field-ring`,
215
+ `invalid-field-ring` - draw an outline back in `Highlight` under `forced-colors: active`, so any
216
+ component that goes through `status-focused`, `status-focused-field` or `status-invalid-field` is
217
+ covered without a line of its own. `status-disabled` picks up `GrayText` the same way.
218
+
219
+ The other half is state carried only by `background-color`, which the override flattens into its
220
+ surroundings. Where selection is *just* a background - a tag, a calendar day, a table row - apply
221
+ `forced-selected`, **as an `@apply` statement of its own**:
222
+
223
+ ```css
224
+ .thing[data-selected="true"] {
225
+ @apply bg-accent text-accent-foreground;
226
+ @apply forced-selected;
227
+ }
228
+ ```
229
+
230
+ Folded into the line above it, Tailwind sorts the list and hoists the nested media query over the
231
+ plain declarations, and the `background-color` it exists to override wins instead. Where selection
232
+ also moves a thumb or shows a glyph, the component writes its own `forced-colors` block, because
233
+ those parts need colours of their own - see `switch.css`, `radio.css`, `tabs.css`, `range-calendar.css`,
234
+ `slider.css` and `skeleton.css`.
235
+
236
+ Use the system colour keywords, not tokens: `Highlight` / `HighlightText` for a selected control,
237
+ `CanvasText` on `Canvas` for ordinary content, `ButtonBorder` for a control's edge, `GrayText` for
238
+ disabled. They are the only colours exempt from the override.
239
+
240
+ Watch for `transparent` used as a spacer. Forced colors preserves a transparent *background*, but a
241
+ transparent `border-color` is turned **opaque** - so a border held open purely to reserve layout space
242
+ (`slider.css` does this at both ends of the track) stops being invisible and renders as a solid block.
243
+ Restate those as `Canvas` inside the media query.
244
+
245
+ One trap, and `getComputedStyle` cannot see it. Chromium paints a `Canvas`-coloured **backplate**
246
+ behind the text of any element that has text, so that text over an image stays legible. It lands on
247
+ top of that element's own background, so a `Highlight` fill carrying `HighlightText` renders as a
248
+ solid plate with the label invisible inside it - the colours are all correct and the component is
249
+ unreadable. `forced-color-adjust: none` is what suppresses the backplate. `forced-selected` already
250
+ carries it; anything hand-rolling the same pairing needs it too, including the case where the fill
251
+ sits on a different element than the text (`tabs.css`, `range-calendar.css`). Reach for it only
252
+ alongside explicit system colours - on its own it just hands the author's palette back, which is the
253
+ opposite of the point.
254
+
255
+ Structure has to survive the mode as well as state. A component that is only a tinted fill and a
256
+ shadow - a card, an alert, a text field, a chip - renders as loose text once both are taken, which
257
+ reads as no component at all. So every container carries an inset `CanvasText` outline under the
258
+ mode and every control a `ButtonBorder` one, on the variants that actually paint; a transparent
259
+ surface is left alone, since framing it would invent a box that was never there. A separator is the
260
+ exception that proves it: the whole thing *is* its background, so it takes a colour rather than an
261
+ edge.
262
+
263
+ Two things guard it. `forced-colors.browser.test.ts` in the `ropav` package asserts the stylesheet
264
+ directly, and the Storybook package runs **every story** through an audit that renders it twice - the
265
+ mode off, then on over CDP - and fails on anything that painted something and stops painting it. The
266
+ second one exists because the first cannot see this class of bug on its own: the colour override runs
267
+ after the cascade and the backplate is painted later still, so a component reports every declared
268
+ colour correctly while rendering as a blank block. Three bugs shipped that way before the audit
269
+ existed.
270
+
271
+ To look at it by hand, use Chromium's rendering panel - DevTools, `Cmd+Shift+P`, "Show Rendering",
272
+ then *Emulate CSS media feature forced-colors* - and note that **macOS has no Forced Colors Mode at
273
+ all**: "Increase contrast" maps to `prefers-contrast: more`, so switching it on tests nothing. There
274
+ is deliberately no toolbar control in Storybook. `data-reduce-motion` is an attribute a decorator can
275
+ set, but this is a browser-level media feature an iframe cannot turn on for itself, and faking it with
276
+ a stylesheet of system colours would miss the `box-shadow` removal - the part that actually breaks.
141
277
 
142
278
  ## Build
143
279
 
@@ -118,6 +118,13 @@
118
118
  @apply bg-surface;
119
119
  border-radius: min(32px, var(--radius-3xl));
120
120
 
121
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, so the container
122
+ * stops having any shape and its contents sit loose on the page. */
123
+ @media (forced-colors: active) {
124
+ outline: 1px solid CanvasText;
125
+ outline-offset: -1px;
126
+ }
127
+
121
128
  .accordion__trigger {
122
129
  @media (hover: hover) {
123
130
  &:hover:not([aria-expanded="true"]),
@@ -3,6 +3,14 @@
3
3
  /* Base alert styles */
4
4
  .alert {
5
5
  @apply flex w-full flex-row items-start justify-start gap-4 bg-surface px-4 py-3 shadow-surface;
6
+
7
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, so the container
8
+ * stops having any shape - its contents just sit loose on the page. An outline rather than a
9
+ * border so nothing shifts, and it follows the radius the component sets. */
10
+ @media (forced-colors: active) {
11
+ outline: 1px solid CanvasText;
12
+ outline-offset: -1px;
13
+ }
6
14
  border-radius: min(32px, var(--radius-3xl));
7
15
  }
8
16
 
@@ -6,6 +6,14 @@
6
6
  .autocomplete__trigger {
7
7
  @apply relative isolate inline-flex min-h-9 rounded-field border bg-field px-3 py-2 text-sm text-field-foreground shadow-field outline-none select-none no-highlight;
8
8
 
9
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, leaving a control
10
+ * with nothing to say it is one. `ButtonBorder` is the keyword the platform uses for a
11
+ * control's edge. */
12
+ @media (forced-colors: active) {
13
+ outline: 1px solid ButtonBorder;
14
+ outline-offset: -1px;
15
+ }
16
+
9
17
  /**
10
18
  * Transitions
11
19
  * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
@@ -1,6 +1,15 @@
1
1
  /* Base avatar styles */
2
2
  .avatar {
3
3
  @apply relative flex size-10 shrink-0 items-center justify-center overflow-hidden rounded-3xl bg-default;
4
+
5
+ /* Forced Colors Mode flattens the fill onto the page, so a fallback avatar is initials sitting
6
+ * loose on the page with no shape around them. The edge goes here rather than on the fallback:
7
+ * this is the element carrying the radius, and the `overflow-hidden` above would clip a
8
+ * square outline drawn inside it down to four arcs. */
9
+ @media (forced-colors: active) {
10
+ outline: 1px solid CanvasText;
11
+ outline-offset: -1px;
12
+ }
4
13
  }
5
14
 
6
15
  /* Avatar fallback element */
@@ -18,6 +18,13 @@
18
18
  color: var(--badge-fg);
19
19
  border: 1px solid var(--badge-border);
20
20
  background-clip: padding-box;
21
+
22
+ /* That border is the page colour, there to cut the badge out of whatever it overlaps, and
23
+ * forced colors turns it into a visible ring - which is the only reason a badge still reads
24
+ * as one. Stated here so it stays true if `--badge-border` is ever repointed. */
25
+ @media (forced-colors: active) {
26
+ border-color: CanvasText;
27
+ }
21
28
  }
22
29
 
23
30
  .badge__label {
@@ -26,6 +26,15 @@
26
26
  background-color: var(--button-bg);
27
27
  color: var(--button-fg);
28
28
 
29
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, leaving a control
30
+ * that reads as bare text. `ButtonBorder` is the keyword the platform uses for a control's
31
+ * edge; an outline rather than a border so nothing shifts, and the focus ring replaces it
32
+ * while focused, which is the right order of importance. */
33
+ @media (forced-colors: active) {
34
+ outline: 1px solid ButtonBorder;
35
+ outline-offset: -1px;
36
+ }
37
+
29
38
  /* Focus state */
30
39
  &:focus-visible:not(:focus),
31
40
  &[data-focus-visible="true"] {
@@ -97,6 +106,16 @@
97
106
  --button-bg-hover: var(--accent-hover);
98
107
  --button-bg-pressed: var(--accent-hover);
99
108
  --button-fg: var(--accent-foreground);
109
+
110
+ /* Solid against outlined is the one distinction that survives - forced colors has no keyword
111
+ * for "this is the important one", so the emphasis has to come from the fill. `Highlight` is
112
+ * spoken for by selection, which a button is not in. `forced-color-adjust` keeps the backplate
113
+ * off the label. */
114
+ @media (forced-colors: active) {
115
+ forced-color-adjust: none;
116
+ background-color: CanvasText;
117
+ color: Canvas;
118
+ }
100
119
  }
101
120
 
102
121
  .button--secondary {
@@ -130,6 +149,14 @@
130
149
  --button-bg-hover: var(--danger-hover);
131
150
  --button-bg-pressed: var(--danger-hover);
132
151
  --button-fg: var(--danger-foreground);
152
+
153
+ /* Solid like the primary one. The danger colour itself cannot survive - there is no keyword
154
+ * for it - so the label is what says this is the destructive action. */
155
+ @media (forced-colors: active) {
156
+ forced-color-adjust: none;
157
+ background-color: CanvasText;
158
+ color: Canvas;
159
+ }
133
160
  }
134
161
 
135
162
  .button--danger-soft {
@@ -144,6 +144,7 @@
144
144
  &[data-selected="true"],
145
145
  &[aria-selected="true"] {
146
146
  @apply bg-accent text-accent-foreground;
147
+ @apply forced-selected;
147
148
 
148
149
  @media (hover: hover) and (pointer: fine) {
149
150
  &:is(:hover, [data-hovered="true"]) {
@@ -207,6 +207,13 @@
207
207
  &[data-today="true"] {
208
208
  @apply bg-accent-soft text-accent-soft-foreground;
209
209
 
210
+ /* A tinted background is all that marks today, and forced colors flattens it onto the page.
211
+ * An edge rather than a fill, so today still reads as itself once it is also selected. */
212
+ @media (forced-colors: active) {
213
+ outline: 1px solid CanvasText;
214
+ outline-offset: -1px;
215
+ }
216
+
210
217
  @media (hover: hover) {
211
218
  &:hover:not([data-selected="true"]),
212
219
  &[data-hovered="true"]:not([data-selected="true"]) {
@@ -218,6 +225,7 @@
218
225
  /* Selected state */
219
226
  &[data-selected="true"] {
220
227
  @apply bg-accent text-accent-foreground;
228
+ @apply forced-selected;
221
229
  }
222
230
 
223
231
  /* Pressed state */
@@ -40,12 +40,33 @@
40
40
 
41
41
  .card--default {
42
42
  @apply bg-surface;
43
+
44
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, so the container
45
+ * stops having any shape and its contents sit loose on the page. */
46
+ @media (forced-colors: active) {
47
+ outline: 1px solid CanvasText;
48
+ outline-offset: -1px;
49
+ }
43
50
  }
44
51
 
45
52
  .card--secondary {
46
53
  @apply bg-surface-secondary;
54
+
55
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, so the container
56
+ * stops having any shape and its contents sit loose on the page. */
57
+ @media (forced-colors: active) {
58
+ outline: 1px solid CanvasText;
59
+ outline-offset: -1px;
60
+ }
47
61
  }
48
62
 
49
63
  .card--tertiary {
50
64
  @apply bg-surface-tertiary;
65
+
66
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, so the container
67
+ * stops having any shape and its contents sit loose on the page. */
68
+ @media (forced-colors: active) {
69
+ outline: 1px solid CanvasText;
70
+ outline-offset: -1px;
71
+ }
51
72
  }
@@ -8,6 +8,19 @@
8
8
 
9
9
  background-color: var(--chip-bg);
10
10
  color: var(--chip-fg);
11
+
12
+ /* A chip is nothing but its colour, so Forced Colors Mode leaves it as bare label text -
13
+ * not a weaker chip, no chip at all, indistinguishable from the copy around it. The edge
14
+ * brings the shape back.
15
+ *
16
+ * What cannot come back is the status. The mode has no keyword for success or warning, so
17
+ * every colour collapses onto one appearance and the label is what has to carry the
18
+ * meaning - which is the same reason a chip should not lean on colour alone in the first
19
+ * place. Only the emphasis split survives, on `.chip--primary` below. */
20
+ @media (forced-colors: active) {
21
+ outline: 1px solid CanvasText;
22
+ outline-offset: -1px;
23
+ }
11
24
  }
12
25
 
13
26
  .chip__label {
@@ -37,6 +50,14 @@
37
50
 
38
51
  /* Variant styles */
39
52
  .chip--primary {
53
+ /* The one distinction worth keeping: solid against outlined. `forced-color-adjust` is what
54
+ * makes it readable - without it Chromium paints a `Canvas` backplate over this fill and the
55
+ * label goes with it. */
56
+ @media (forced-colors: active) {
57
+ forced-color-adjust: none;
58
+ background-color: CanvasText;
59
+ color: Canvas;
60
+ }
40
61
  }
41
62
 
42
63
  .chip--secondary {
@@ -47,6 +47,14 @@
47
47
  .close-button--default {
48
48
  @apply bg-default text-muted;
49
49
 
50
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, leaving a control
51
+ * with nothing to say it is one. `ButtonBorder` is the keyword the platform uses for a
52
+ * control's edge. */
53
+ @media (forced-colors: active) {
54
+ outline: 1px solid ButtonBorder;
55
+ outline-offset: -1px;
56
+ }
57
+
50
58
  &:active,
51
59
  &[data-pressed="true"] {
52
60
  }
@@ -8,6 +8,15 @@
8
8
  border-width: var(--border-width-field);
9
9
  border-color: var(--field-border);
10
10
 
11
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, leaving a field that
12
+ * reads as bare text - `--border-width-field` is 0, so the declared border brings nothing back on
13
+ * its own. An outline rather than a width so nothing shifts, and the focus ring replaces it while
14
+ * focused, which is the right order of importance. */
15
+ @media (forced-colors: active) {
16
+ outline: 1px solid ButtonBorder;
17
+ outline-offset: -1px;
18
+ }
19
+
11
20
  /**
12
21
  * Transitions
13
22
  * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
@@ -14,6 +14,13 @@
14
14
  .color-swatch-picker__item {
15
15
  @apply relative flex size-8 items-center justify-center rounded-2xl border-2 border-transparent outline-none no-highlight;
16
16
 
17
+ /* The unselected border is transparent purely to hold the ring's width, and forced colors turns
18
+ * a transparent border-color opaque - so every swatch drew the same border and the selected one
19
+ * stopped being the selected one. */
20
+ @media (forced-colors: active) {
21
+ border-color: Canvas;
22
+ }
23
+
17
24
  /* Cursor */
18
25
  cursor: var(--cursor-interactive);
19
26
 
@@ -41,6 +48,10 @@
41
48
  &[data-selected="true"] {
42
49
  border-color: var(--color-swatch-current);
43
50
  box-shadow: var(--field-shadow);
51
+
52
+ @media (forced-colors: active) {
53
+ border-color: Highlight;
54
+ }
44
55
  /* Shrink the swatch to reveal the white gap */
45
56
  .color-swatch-picker__swatch {
46
57
  transform: scale(0.77);
@@ -4,6 +4,15 @@
4
4
  border-width: var(--border-width-field);
5
5
  border-color: var(--field-border);
6
6
 
7
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, leaving a field that
8
+ * reads as bare text - `--border-width-field` is 0, so the declared border brings nothing back on
9
+ * its own. An outline rather than a width so nothing shifts, and the focus ring replaces it while
10
+ * focused, which is the right order of importance. */
11
+ @media (forced-colors: active) {
12
+ outline: 1px solid ButtonBorder;
13
+ outline-offset: -1px;
14
+ }
15
+
7
16
  /**
8
17
  * Transitions
9
18
  * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
@@ -1 +1 @@
1
- {"version":3,"file":"dropdown.styles.d.ts","sourceRoot":"","sources":["../../../src/components/dropdown/dropdown.styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAO3B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
1
+ {"version":3,"file":"dropdown.styles.d.ts","sourceRoot":"","sources":["../../../src/components/dropdown/dropdown.styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AAItD,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAS3B,CAAC;AAEH,MAAM,MAAM,gBAAgB,GAAG,YAAY,CAAC,OAAO,gBAAgB,CAAC,CAAC"}
@@ -1,7 +1,7 @@
1
1
  import { tv } from "tailwind-variants";
2
2
  //#region src/components/dropdown/dropdown.styles.ts
3
3
  const dropdownVariants = tv({ slots: {
4
- menu: "dropdown__menu",
4
+ menu: "menu dropdown__menu",
5
5
  popover: "dropdown__popover",
6
6
  root: "dropdown",
7
7
  trigger: "dropdown__trigger"
@@ -10,6 +10,18 @@
10
10
  @apply inline-block;
11
11
  /* Only visible when using a custom trigger - not passing "Button" as a child - hence we use some button styles (custom trigger) */
12
12
  @apply outline-none;
13
+
14
+ /* A custom trigger borrows the button's look by wearing its classes, and the `outline-none`
15
+ * above lands later in the cascade than the button's own forced-colors edge, stripping it back
16
+ * off. Handed back here rather than on the bare trigger: when a `Button` is passed as a child
17
+ * instead, this element is only a wrapper around it and an outline would draw a second ring
18
+ * around the real one. */
19
+ &.button {
20
+ @media (forced-colors: active) {
21
+ outline: 1px solid ButtonBorder;
22
+ outline-offset: -1px;
23
+ }
24
+ }
13
25
  /**
14
26
  * Transitions
15
27
  * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
@@ -109,21 +121,14 @@
109
121
  rotate: 90deg;
110
122
  }
111
123
 
112
- /* Menu styles */
124
+ /* Menu styles. The block itself is `.menu`; these are the dropdown's departures from it, and
125
+ * they sit here rather than on `.dropdown__menu` because `menu.css` is imported after this file
126
+ * and would otherwise win at equal specificity. */
113
127
  [data-slot="dropdown-menu"] {
114
- @apply p-1.5 outline-none;
128
+ @apply gap-0.5 p-1.5 outline-none;
115
129
  }
116
130
 
117
131
  [data-slot="menu-item"] {
118
132
  @apply px-2.5;
119
133
  }
120
134
  }
121
-
122
- .dropdown__menu {
123
- @apply relative flex w-full flex-col gap-0.5 overflow-clip p-1;
124
-
125
- /* This makes the separator take up 94% of the width and be centered */
126
- [data-slot="separator"] {
127
- @apply ms-[3%] w-[94%];
128
- }
129
- }
@@ -4,6 +4,15 @@
4
4
  border-width: var(--border-width-field);
5
5
  border-color: var(--field-border);
6
6
 
7
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, leaving a field that
8
+ * reads as bare text - `--border-width-field` is 0, so the declared border brings nothing back on
9
+ * its own. An outline rather than a width so nothing shifts, and the focus ring replaces it while
10
+ * focused, which is the right order of importance. */
11
+ @media (forced-colors: active) {
12
+ outline: 1px solid ButtonBorder;
13
+ outline-offset: -1px;
14
+ }
15
+
7
16
  /* When textarea is present, align items to start and allow height to grow */
8
17
  &:has([data-slot="input-group-textarea"]) {
9
18
  @apply items-start;
@@ -4,6 +4,16 @@
4
4
  border-width: var(--border-width-field);
5
5
  border-color: var(--field-border);
6
6
 
7
+ /* Forced Colors Mode flattens the fill onto the page and strips the shadow, leaving a field that
8
+ * reads as bare text - `--border-width-field` is 0, so the declared border brings nothing
9
+ * back on its own. `ButtonBorder` is the keyword the platform uses for a control's
10
+ * edge; an outline rather than a border so nothing shifts, and the focus ring replaces it
11
+ * while focused, which is the right order of importance. */
12
+ @media (forced-colors: active) {
13
+ outline: 1px solid ButtonBorder;
14
+ outline-offset: -1px;
15
+ }
16
+
7
17
  /**
8
18
  * Transitions
9
19
  * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
@@ -2,6 +2,12 @@
2
2
  Base menu styles
3
3
  ========================================================================== */
4
4
 
5
+ /**
6
+ * Worn by both menus — the standalone one and the one inside a dropdown, which carries
7
+ * `dropdown__menu` alongside it. The two used to define the same rules twice, differing only in
8
+ * `gap`; that difference now lives with the dropdown's other overrides in `dropdown.css`, nested
9
+ * where it has the specificity to win, since this file is imported after that one.
10
+ */
5
11
  .menu {
6
12
  @apply relative flex w-full flex-col gap-1 overflow-clip p-1;
7
13
 
@@ -32,6 +32,14 @@
32
32
 
33
33
  /* Default height (matches --md) */
34
34
  @apply h-2;
35
+
36
+ /* Track and fill are both plain backgrounds, so forced colors flattens the pair onto the
37
+ * page and the whole meter stops rendering - no bar, no value. */
38
+ @media (forced-colors: active) {
39
+ background-color: Canvas;
40
+ outline: 1px solid CanvasText;
41
+ outline-offset: -1px;
42
+ }
35
43
  }
36
44
 
37
45
  .meter__fill {
@@ -39,6 +47,10 @@
39
47
 
40
48
  background-color: var(--meter-fill);
41
49
 
50
+ @media (forced-colors: active) {
51
+ background-color: Highlight;
52
+ }
53
+
42
54
  /**
43
55
  * Transitions
44
56
  * CRITICAL: motion-reduce must be AFTER transition for correct override specificity