@ropav/styles 0.5.0 → 0.6.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 (98) hide show
  1. package/README.md +79 -0
  2. package/dist/components/accordion.css +1 -9
  3. package/dist/components/alert-dialog.css +0 -12
  4. package/dist/components/autocomplete.css +25 -35
  5. package/dist/components/breadcrumbs.css +9 -2
  6. package/dist/components/button-group.css +0 -4
  7. package/dist/components/button.css +3 -7
  8. package/dist/components/calendar-year-picker.css +19 -7
  9. package/dist/components/calendar.css +46 -21
  10. package/dist/components/checkbox.css +54 -56
  11. package/dist/components/chip.css +3 -1
  12. package/dist/components/close-button.css +7 -6
  13. package/dist/components/color-area.css +6 -6
  14. package/dist/components/color-input-group.css +10 -27
  15. package/dist/components/color-picker.css +1 -5
  16. package/dist/components/color-slider.css +14 -12
  17. package/dist/components/color-swatch-picker.css +2 -85
  18. package/dist/components/color-swatch.css +1 -1
  19. package/dist/components/combo-box.css +13 -3
  20. package/dist/components/date-input-group.css +34 -32
  21. package/dist/components/date-picker.css +1 -5
  22. package/dist/components/date-range-picker.css +1 -5
  23. package/dist/components/disclosure.css +0 -4
  24. package/dist/components/drawer.css +4 -8
  25. package/dist/components/drop-zone/drop-zone.styles.d.ts +34 -0
  26. package/dist/components/drop-zone/drop-zone.styles.d.ts.map +1 -0
  27. package/dist/components/drop-zone/drop-zone.styles.js +8 -0
  28. package/dist/components/drop-zone/index.d.ts +2 -0
  29. package/dist/components/drop-zone/index.d.ts.map +1 -0
  30. package/dist/components/drop-zone/index.js +2 -0
  31. package/dist/components/drop-zone.css +78 -0
  32. package/dist/components/dropdown.css +0 -4
  33. package/dist/components/error-message.css +0 -4
  34. package/dist/components/field-error.css +0 -4
  35. package/dist/components/index.css +1 -0
  36. package/dist/components/index.d.ts +1 -0
  37. package/dist/components/index.d.ts.map +1 -1
  38. package/dist/components/input-group.css +12 -37
  39. package/dist/components/input-otp.css +14 -26
  40. package/dist/components/input.css +11 -32
  41. package/dist/components/kbd.css +3 -1
  42. package/dist/components/label.css +7 -2
  43. package/dist/components/link.css +13 -11
  44. package/dist/components/list-box-item.css +4 -10
  45. package/dist/components/menu-item.css +4 -10
  46. package/dist/components/meter.css +8 -16
  47. package/dist/components/modal.css +0 -12
  48. package/dist/components/number-field.css +23 -35
  49. package/dist/components/pagination.css +0 -4
  50. package/dist/components/popover.css +0 -4
  51. package/dist/components/progress-bar/progress-bar.styles.d.ts +36 -0
  52. package/dist/components/progress-bar/progress-bar.styles.d.ts.map +1 -1
  53. package/dist/components/progress-bar/progress-bar.styles.js +10 -0
  54. package/dist/components/progress-bar.css +82 -16
  55. package/dist/components/progress-circle.css +0 -4
  56. package/dist/components/radio.css +7 -9
  57. package/dist/components/range-calendar.css +73 -31
  58. package/dist/components/search-field.css +11 -28
  59. package/dist/components/segmented-control.css +9 -14
  60. package/dist/components/select.css +18 -32
  61. package/dist/components/sidebar/sidebar.styles.d.ts +36 -0
  62. package/dist/components/sidebar/sidebar.styles.d.ts.map +1 -1
  63. package/dist/components/sidebar/sidebar.styles.js +14 -1
  64. package/dist/components/sidebar.css +132 -38
  65. package/dist/components/slider.css +10 -11
  66. package/dist/components/splitter.css +44 -36
  67. package/dist/components/switch.css +16 -15
  68. package/dist/components/table.css +69 -27
  69. package/dist/components/tabs.css +8 -19
  70. package/dist/components/tag.css +32 -24
  71. package/dist/components/textarea.css +11 -32
  72. package/dist/components/toast.css +3 -1
  73. package/dist/components/toggle-button-group.css +0 -4
  74. package/dist/components/toggle-button.css +2 -6
  75. package/dist/components/tooltip.css +0 -4
  76. package/dist/components/typography.css +12 -4
  77. package/dist/components/virtualizer.css +11 -3
  78. package/dist/index.js +2 -1
  79. package/dist/ropav.min.css +1 -1
  80. package/dist/themes/airbnb.css +6 -4
  81. package/dist/themes/coinbase.css +6 -4
  82. package/dist/themes/default.css +24 -4
  83. package/dist/themes/discord.css +6 -4
  84. package/dist/themes/hero.css +24 -4
  85. package/dist/themes/lavender.css +6 -4
  86. package/dist/themes/mint.css +6 -4
  87. package/dist/themes/netflix.css +6 -4
  88. package/dist/themes/rabbit.css +6 -4
  89. package/dist/themes/shared/theme.css +1 -0
  90. package/dist/themes/sky.css +6 -4
  91. package/dist/themes/spotify.css +6 -4
  92. package/dist/themes/uber.css +6 -4
  93. package/package.json +1 -1
  94. package/src/components/drop-zone/drop-zone.styles.ts +12 -0
  95. package/src/components/drop-zone/index.ts +1 -0
  96. package/src/components/index.ts +1 -0
  97. package/src/components/progress-bar/progress-bar.styles.ts +14 -0
  98. package/src/components/sidebar/sidebar.styles.ts +23 -0
package/README.md CHANGED
@@ -112,6 +112,11 @@ component that publishes it as an attribute.
112
112
 
113
113
  `components/button.css` is the densest example of both — read it before writing a new component file.
114
114
 
115
+ **Every `ropav` component takes a `class` prop**, merged through its `tv()` recipe rather than
116
+ concatenated, so a utility that conflicts with one the recipe already carries replaces it. That is
117
+ the first thing to reach for. It cannot reach a part drawn in `::before`/`::after`, and it flattens
118
+ every state of whatever property it sets — see [State colors](#state-colors) for the way round both.
119
+
115
120
  ## Theming
116
121
 
117
122
  Two independent axes. `data-theme` picks the palette, a `light`/`dark` class picks the appearance:
@@ -203,6 +208,80 @@ scrollbar set, and shadows.
203
208
  stale the moment a token moves. `themes/shared/theme.css` holds what is derived from those tokens: the
204
209
  `--radius-xs` … `--radius-4xl` scale and the easing curves.
205
210
 
211
+ ### State colors
212
+
213
+ A component's own properties are the third tier, declared on its block and private to its file —
214
+ `--button-bg`, `--switch-control-bg-checked-hover`, `--sidebar-rail-line`. The convention:
215
+
216
+ > **A color a state rule paints goes through a custom property — where more than one state paints
217
+ > that property.**
218
+
219
+ That is what makes one state retunable on its own. A color written straight into the rule is not:
220
+ the resting and the lit value are the same property on the same element, so the one declaration a
221
+ caller writes to change either beats every state rule at once — and a utility beats them all
222
+ whatever their specificity, `utilities` being a later layer than `components`. The only way left is
223
+ to restate the whole state set behind `:not()`, which drifts silently the day a state is added.
224
+
225
+ So a caller sets the property instead, and the states that read the others are left standing:
226
+
227
+ ```html
228
+ <!-- No line at rest; it still flares on hover and while dragging. -->
229
+ <SidebarRoot class="[--sidebar-rail-line:transparent]">
230
+ ```
231
+
232
+ The qualifier is what keeps this from becoming surface for its own sake. Where exactly one state
233
+ paints a property, a caller can name that state from the call site — `hover:bg-*`, `focus-visible:outline-*` —
234
+ and there is no other state for it to flatten, so no property is minted. It is the second state
235
+ painting the same thing that makes the call site unable to tell them apart.
236
+
237
+ Defaults chain, so retuning one carries the states below it unless they are set too —
238
+ `--sidebar-rail-line-dragging` follows `--sidebar-rail-line-hover`, the way `--button-bg-pressed`
239
+ follows `--button-bg-hover`. Read `components/button.css` for the shape and
240
+ `components/sidebar.css` for a part painted on a pseudo-element, which a class cannot reach at all.
241
+
242
+ Two limits. The properties are per component and resolve to the tokens above, so a palette change
243
+ belongs in a theme rather than here. And Forced Colors Mode is not covered: those blocks paint the
244
+ system keywords, which are the only colors exempt from the override and not a component's to
245
+ retune — `[--sidebar-rail-line:transparent]` leaves the line drawn under High Contrast, on purpose.
246
+
247
+ `state-colors.test.ts` in the `ropav` package reads every component file and fails on a state color
248
+ that does not go through a property. Its ledger of exceptions is currently empty, so an entry
249
+ appearing there is debt to pay down rather than a licence to add more.
250
+
251
+ ### Translucent surfaces
252
+
253
+ A `surface--transparent` paints nothing of its own, and so does a surface a caller has thinned to
254
+ let something through — a blurred header, a panel over an image, a window whose material the OS
255
+ draws. What sits on one of those has no way to know what is behind it, and that splits the palette
256
+ in two.
257
+
258
+ **Safe on anything: the tokens that are an alpha.** `--state-layer` is what a state paints, and
259
+ `--separator` and `--border` are the lines — each an alpha of `--surface-foreground` rather than a
260
+ neutral of its own. An alpha darkens or lightens whatever it lands on, so it holds its contrast
261
+ against a fill it was never tuned for: another surface, the far half of the theme, or a photograph.
262
+ The `-soft` family and `--scrollbar-thumb` are alphas too, which is why a selection is
263
+ `--accent-soft` wherever the library marks one.
264
+
265
+ **Not safe: the fills.** `--default`, `--accent`, the status colors, `--surface` and its secondary
266
+ and tertiary steps are opaque, and they are meant to be — a badge you can read the page through is a
267
+ broken badge. They assume a background because they *are* one. Use them to fill a box, never to mark
268
+ a state on a box that is already transparent.
269
+
270
+ `--surface-hover` sits on the fill side of that line despite the name: it is a surface that has been
271
+ tinted and stays opaque, for a component opaque at rest whose hover must stay that way.
272
+ `--state-layer` is the one for a component transparent at rest. Reaching for the wrong one fails
273
+ quietly — a translucent hover on an opaque tag shows the page through it, and a flat neutral on a
274
+ transparent row vanishes the moment the fill behind it matches.
275
+
276
+ The line between the two is what a color is for, not what it looks like. `--default` is a fine grey
277
+ until it is asked to be a state layer: then it is a fixed lightness laid on an unknown one, and the
278
+ day those two match it is gone with nothing to fail but the eye.
279
+
280
+ **Contrast on a translucent surface is the app's to guarantee.** `contrast.browser.test.ts` holds
281
+ the floor for text on the fills, where both sides are known. Once the background is whatever happens
282
+ to be behind the window, ropav cannot see it and does not claim to — an app that thins a surface
283
+ owns the legibility of what it puts there.
284
+
206
285
  ### Reduced motion
207
286
 
208
287
  `data-reduce-motion="true"` on any ancestor forces animations off, `"false"` forces them on regardless
@@ -59,10 +59,6 @@
59
59
  /* Base Styles */
60
60
  @apply flex flex-1 items-center justify-between px-4 py-4 text-start text-sm font-medium no-highlight;
61
61
 
62
- /**
63
- * Transitions
64
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
65
- */
66
62
  transition:
67
63
  opacity 150ms var(--ease-out),
68
64
  box-shadow 150ms var(--ease-out);
@@ -98,10 +94,6 @@
98
94
  height: var(--disclosure-panel-height);
99
95
  overflow: clip;
100
96
 
101
- /**
102
- * Transitions
103
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
104
- */
105
97
  transition:
106
98
  height 200ms var(--ease-out-quad),
107
99
  opacity 200ms var(--ease-out);
@@ -129,7 +121,7 @@
129
121
  @media (hover: hover) {
130
122
  &:hover:not([aria-expanded="true"]),
131
123
  &[data-hovered="true"]:not([aria-expanded="true"]) {
132
- @apply bg-default;
124
+ @apply bg-state-layer;
133
125
  }
134
126
  }
135
127
  }
@@ -10,10 +10,6 @@
10
10
  @apply inline-block;
11
11
  @apply cursor-(--cursor-interactive);
12
12
 
13
- /**
14
- * Transitions
15
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
16
- */
17
13
  transition:
18
14
  transform 250ms var(--ease-out-quart),
19
15
  background-color 150ms var(--ease-smooth),
@@ -57,10 +53,6 @@
57
53
  @apply animate-out duration-100 ease-out fade-out-0;
58
54
  }
59
55
 
60
- /**
61
- * Reduce Animation
62
- * CRITICAL: motion-reduce must be AFTER animate for correct override specificity
63
- */
64
56
  &[data-exiting="true"],
65
57
  &[data-entering="true"] {
66
58
  @apply will-change-[opacity];
@@ -126,10 +118,6 @@
126
118
  @apply animate-out duration-100 ease-out-quad fade-out-0 zoom-out-95;
127
119
  }
128
120
 
129
- /**
130
- * Reduce Animation
131
- * CRITICAL: motion-reduce must be AFTER animate for correct override specificity
132
- */
133
121
  &[data-exiting="true"],
134
122
  &[data-entering="true"] {
135
123
  @apply will-change-[opacity,transform];
@@ -4,7 +4,14 @@
4
4
  }
5
5
 
6
6
  .autocomplete__trigger {
7
- @apply relative isolate inline-flex min-h-9 rounded-field border bg-field px-3 field-line-md text-field-foreground shadow-field outline-none select-none no-highlight;
7
+ @apply relative isolate inline-flex min-h-9 rounded-field border px-3 field-line-md text-field-foreground shadow-field outline-none select-none no-highlight;
8
+
9
+ /* The fill, per state. The `--secondary` variant re-points these and declares nothing else. */
10
+ --autocomplete-trigger-bg: var(--field-background, var(--default));
11
+ --autocomplete-trigger-bg-hover: var(--field-hover);
12
+ --autocomplete-trigger-bg-focus: var(--field-focus);
13
+
14
+ background-color: var(--autocomplete-trigger-bg);
8
15
 
9
16
  /* Forced Colors Mode flattens the fill onto the page and strips the shadow, leaving a control
10
17
  * with nothing to say it is one. `ButtonBorder` is the keyword the platform uses for a
@@ -14,10 +21,6 @@
14
21
  outline-offset: -1px;
15
22
  }
16
23
 
17
- /**
18
- * Transitions
19
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
20
- */
21
24
  transition:
22
25
  background-color 150ms var(--ease-smooth),
23
26
  border-color 150ms var(--ease-smooth),
@@ -39,7 +42,7 @@
39
42
  @media (hover: hover) {
40
43
  &:hover:not(:has(.autocomplete__clear-button:hover)),
41
44
  &[data-hovered="true"]:not(:has(.autocomplete__clear-button:hover)) {
42
- @apply bg-field-hover;
45
+ background-color: var(--autocomplete-trigger-bg-hover);
43
46
  border-color: var(--field-border-hover);
44
47
  }
45
48
  }
@@ -49,7 +52,7 @@
49
52
  &[data-focus-visible="true"] {
50
53
  @apply status-focused;
51
54
  border-color: var(--field-border-focus);
52
- background-color: var(--field-focus);
55
+ background-color: var(--autocomplete-trigger-bg-focus);
53
56
  }
54
57
 
55
58
  /* Focus state */
@@ -61,7 +64,7 @@
61
64
  .autocomplete[data-invalid="true"] &,
62
65
  .autocomplete[aria-invalid="true"] & {
63
66
  @apply status-invalid-field;
64
- background-color: var(--field-focus);
67
+ background-color: var(--autocomplete-trigger-bg-focus);
65
68
  }
66
69
 
67
70
  /* Disabled state */
@@ -79,40 +82,22 @@
79
82
 
80
83
  .autocomplete--secondary .autocomplete__trigger {
81
84
  @apply shadow-none;
82
- background-color: var(--autocomplete-trigger-bg);
83
85
 
84
86
  --autocomplete-trigger-bg: var(--default);
85
87
  --autocomplete-trigger-bg-hover: var(--default-hover);
86
88
  --autocomplete-trigger-bg-focus: var(--default);
87
-
88
- /* Exclude when clear button is hovered to prevent double hover effect */
89
- @media (hover: hover) {
90
- &:hover:not(:has(.autocomplete__clear-button:hover)),
91
- &[data-hovered="true"]:not(:has(.autocomplete__clear-button:hover)) {
92
- background-color: var(--autocomplete-trigger-bg-hover);
93
- }
94
- }
95
-
96
- &:focus-visible:not(:focus),
97
- &[data-focus-visible="true"] {
98
- background-color: var(--autocomplete-trigger-bg-focus);
99
- }
100
-
101
- &:focus,
102
- &[data-focus="true"] {
103
- }
104
-
105
- .autocomplete[data-invalid="true"] &,
106
- .autocomplete[aria-invalid="true"] & {
107
- background-color: var(--autocomplete-trigger-bg-focus);
108
- }
109
89
  }
110
90
 
111
91
  .autocomplete__value {
112
- @apply flex-1 text-start wrap-break-word text-current;
92
+ @apply flex-1 text-start wrap-break-word;
93
+
94
+ --autocomplete-value-fg: currentcolor;
95
+ --autocomplete-value-fg-placeholder: var(--field-placeholder, var(--muted));
96
+
97
+ color: var(--autocomplete-value-fg);
113
98
 
114
99
  &[data-placeholder="true"] {
115
- @apply text-field-placeholder;
100
+ color: var(--autocomplete-value-fg-placeholder);
116
101
  }
117
102
 
118
103
  /* Removes the list-box-item indicator element from the autocomplete value*/
@@ -252,7 +237,12 @@
252
237
  /* Clear Button styles */
253
238
  .autocomplete__clear-button {
254
239
  @apply relative isolate inline-flex h-6 w-6 shrink-0 origin-center items-center justify-center rounded-xl p-1 text-muted select-none no-highlight;
255
- @apply me-0 size-5 shrink-0 self-center bg-transparent;
240
+ @apply me-0 size-5 shrink-0 self-center;
241
+
242
+ --autocomplete-clear-button-bg: transparent;
243
+ --autocomplete-clear-button-bg-hover: var(--default-hover);
244
+
245
+ background-color: var(--autocomplete-clear-button-bg);
256
246
 
257
247
  /* Cursor */
258
248
  cursor: var(--cursor-interactive);
@@ -280,7 +270,7 @@
280
270
  @media (hover: hover) {
281
271
  &:hover,
282
272
  &[data-hovered="true"] {
283
- @apply bg-default-hover;
273
+ background-color: var(--autocomplete-clear-button-bg-hover);
284
274
  }
285
275
  }
286
276
 
@@ -4,7 +4,12 @@
4
4
 
5
5
  /* Breadcrumb link */
6
6
  .breadcrumbs__link {
7
- @apply relative px-0.5 text-sm leading-5 font-medium text-muted no-underline opacity-100;
7
+ @apply relative px-0.5 text-sm leading-5 font-medium no-underline opacity-100;
8
+
9
+ --breadcrumbs-link-fg: var(--muted);
10
+ --breadcrumbs-link-fg-current: var(--link);
11
+
12
+ color: var(--breadcrumbs-link-fg);
8
13
 
9
14
  &:hover,
10
15
  &[data-hovered="true"] {
@@ -12,7 +17,9 @@
12
17
  }
13
18
 
14
19
  &[data-current="true"] {
15
- @apply text-link opacity-100;
20
+ @apply opacity-100;
21
+
22
+ color: var(--breadcrumbs-link-fg-current);
16
23
  }
17
24
  }
18
25
 
@@ -75,10 +75,6 @@
75
75
  position: absolute;
76
76
  pointer-events: none;
77
77
 
78
- /**
79
- * Transitions
80
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
81
- */
82
78
  transition: opacity 150ms var(--ease-smooth);
83
79
  @apply motion-reduce:transition-none;
84
80
 
@@ -4,10 +4,6 @@
4
4
  .button {
5
5
  @apply relative isolate inline-flex h-9 w-fit origin-center items-center justify-center gap-2 rounded-component px-4 text-sm font-medium whitespace-nowrap outline-none select-none no-highlight;
6
6
 
7
- /**
8
- * Transitions
9
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
10
- */
11
7
  transition:
12
8
  transform 250ms var(--ease-smooth),
13
9
  background-color 100ms var(--ease-out),
@@ -130,14 +126,14 @@
130
126
  .button--ghost,
131
127
  .button--outline {
132
128
  --button-bg: transparent;
133
- --button-bg-hover: var(--default);
134
- --button-bg-pressed: var(--default);
129
+ --button-bg-hover: var(--state-layer);
130
+ --button-bg-pressed: var(--state-layer);
135
131
  --button-fg: var(--default-foreground);
136
132
  }
137
133
 
138
134
  .button--outline {
139
135
  @apply border border-border;
140
- --button-bg-hover: color-mix(in srgb, var(--default) 60%, transparent);
136
+ --button-bg-hover: color-mix(in oklab, var(--state-layer) 60%, transparent);
141
137
  }
142
138
 
143
139
  .button--danger {
@@ -119,10 +119,19 @@
119
119
  @apply relative inline-flex h-8 items-center justify-center rounded-component px-2.5 text-sm font-medium outline-none select-none no-highlight;
120
120
  touch-action: manipulation;
121
121
 
122
- /**
123
- * Transitions – same as .tag
124
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
125
- */
122
+ /* The fill and label, per state — selected and hovered land together on the current year. */
123
+ --calendar-year-picker-year-cell-bg: transparent;
124
+ --calendar-year-picker-year-cell-bg-hover: var(--state-layer);
125
+ --calendar-year-picker-year-cell-bg-selected: var(--accent);
126
+ --calendar-year-picker-year-cell-bg-selected-hover: var(--accent-hover);
127
+ --calendar-year-picker-year-cell-fg: currentcolor;
128
+ --calendar-year-picker-year-cell-fg-hover: var(--default-foreground);
129
+ --calendar-year-picker-year-cell-fg-selected: var(--accent-foreground);
130
+
131
+ background-color: var(--calendar-year-picker-year-cell-bg);
132
+ color: var(--calendar-year-picker-year-cell-fg);
133
+
134
+ /* Same as `.tag`. */
126
135
  transition:
127
136
  color 100ms var(--ease-smooth),
128
137
  scale 100ms var(--ease-smooth),
@@ -136,19 +145,22 @@
136
145
  /* Hover state (when not selected) – only on devices that support hover */
137
146
  @media (hover: hover) and (pointer: fine) {
138
147
  &:is(:hover, [data-hovered="true"]):not([data-selected="true"]) {
139
- @apply bg-default text-default-foreground;
148
+ background-color: var(--calendar-year-picker-year-cell-bg-hover);
149
+ color: var(--calendar-year-picker-year-cell-fg-hover);
140
150
  }
141
151
  }
142
152
 
143
153
  /* Selected state (current year) – same as .tag[data-selected] */
144
154
  &[data-selected="true"],
145
155
  &[aria-selected="true"] {
146
- @apply bg-accent text-accent-foreground;
156
+ background-color: var(--calendar-year-picker-year-cell-bg-selected);
157
+ color: var(--calendar-year-picker-year-cell-fg-selected);
158
+
147
159
  @apply forced-selected;
148
160
 
149
161
  @media (hover: hover) and (pointer: fine) {
150
162
  &:is(:hover, [data-hovered="true"]) {
151
- @apply bg-accent-hover;
163
+ background-color: var(--calendar-year-picker-year-cell-bg-selected-hover);
152
164
  }
153
165
  }
154
166
  }
@@ -80,14 +80,12 @@
80
80
  }
81
81
 
82
82
  .calendar__nav-button {
83
- @apply flex size-6 items-center justify-center rounded-2xl text-accent-soft-foreground;
83
+ @apply flex size-6 items-center justify-center text-accent-soft-foreground;
84
+
85
+ border-radius: min(32px, var(--component-radius));
84
86
 
85
87
  will-change: scale;
86
88
 
87
- /**
88
- * Transitions
89
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
90
- */
91
89
  transition:
92
90
  transform 250ms var(--ease-out),
93
91
  background-color 100ms var(--ease-out),
@@ -101,7 +99,7 @@
101
99
  @media (hover: hover) {
102
100
  &:hover,
103
101
  &[data-hovered="true"] {
104
- @apply bg-default text-accent-soft-foreground;
102
+ @apply bg-state-layer;
105
103
  }
106
104
  }
107
105
 
@@ -183,13 +181,30 @@
183
181
  * -------------------------------------------------------------------------- */
184
182
  .calendar__cell {
185
183
  /* Cell (td) wrapper styles - margins now work with grid layout */
186
- @apply relative flex aspect-square size-full items-center justify-center rounded-3xl text-center text-sm font-medium outline-none no-highlight;
184
+ @apply relative flex aspect-square size-full items-center justify-center text-center text-sm font-medium outline-none no-highlight;
185
+
186
+ border-radius: min(32px, var(--component-radius));
187
+
188
+ /* Eight states repaint a day, several of them in pairs — today and hovered, selected and
189
+ * pressed, selected and outside the month. One declaration cannot tell them apart, so each
190
+ * reads its own. */
191
+ --calendar-cell-bg: transparent;
192
+ --calendar-cell-bg-hover: var(--state-layer);
193
+ --calendar-cell-bg-pressed: var(--calendar-cell-bg-hover);
194
+ --calendar-cell-bg-today: var(--accent-soft);
195
+ --calendar-cell-bg-today-hover: var(--accent-soft-hover);
196
+ --calendar-cell-bg-selected: var(--accent);
197
+ --calendar-cell-bg-selected-pressed: var(--accent-hover);
198
+ --calendar-cell-bg-selected-outside-month: var(--state-layer);
199
+ --calendar-cell-fg: currentcolor;
200
+ --calendar-cell-fg-today: var(--accent-soft-foreground);
201
+ --calendar-cell-fg-selected: var(--accent-foreground);
202
+ --calendar-cell-fg-outside-month: var(--muted);
203
+
204
+ background-color: var(--calendar-cell-bg);
205
+ color: var(--calendar-cell-fg);
187
206
 
188
207
  will-change: scale;
189
- /**
190
- * Transitions
191
- * CRITICAL: motion-reduce must be AFTER transition for correct override specificity
192
- */
193
208
  transition:
194
209
  transform 250ms var(--ease-out),
195
210
  box-shadow 100ms var(--ease-out);
@@ -205,7 +220,8 @@
205
220
 
206
221
  /* Today indicator */
207
222
  &[data-today="true"] {
208
- @apply bg-accent-soft text-accent-soft-foreground;
223
+ background-color: var(--calendar-cell-bg-today);
224
+ color: var(--calendar-cell-fg-today);
209
225
 
210
226
  /* A tinted background is all that marks today, and forced colors flattens it onto the page.
211
227
  * An edge rather than a fill, so today still reads as itself once it is also selected. */
@@ -217,25 +233,27 @@
217
233
  @media (hover: hover) {
218
234
  &:hover:not([data-selected="true"]),
219
235
  &[data-hovered="true"]:not([data-selected="true"]) {
220
- @apply bg-accent-soft-hover;
236
+ background-color: var(--calendar-cell-bg-today-hover);
221
237
  }
222
238
  }
223
239
  }
224
240
 
225
241
  /* Selected state */
226
242
  &[data-selected="true"] {
227
- @apply bg-accent text-accent-foreground;
243
+ background-color: var(--calendar-cell-bg-selected);
244
+ color: var(--calendar-cell-fg-selected);
245
+
228
246
  @apply forced-selected;
229
247
  }
230
248
 
231
249
  /* Pressed state */
232
250
  &:active,
233
251
  &[data-pressed="true"] {
234
- @apply bg-default;
252
+ background-color: var(--calendar-cell-bg-pressed);
235
253
  transform: scale(0.95);
236
254
 
237
255
  &[data-selected="true"] {
238
- @apply bg-accent-hover;
256
+ background-color: var(--calendar-cell-bg-selected-pressed);
239
257
  }
240
258
  }
241
259
 
@@ -243,17 +261,19 @@
243
261
  @media (hover: hover) {
244
262
  &:hover:not([data-selected="true"]),
245
263
  &[data-hovered="true"]:not([data-selected="true"]) {
246
- @apply bg-default;
264
+ background-color: var(--calendar-cell-bg-hover);
247
265
  }
248
266
  }
249
267
 
250
268
  /* Outside month (previous/next month days) */
251
269
  &[data-outside-month="true"] {
252
- @apply text-muted opacity-50;
270
+ @apply opacity-50;
271
+
272
+ color: var(--calendar-cell-fg-outside-month);
253
273
  }
254
274
 
255
275
  &[data-selected="true"][data-outside-month="true"] {
256
- @apply bg-default;
276
+ background-color: var(--calendar-cell-bg-selected-outside-month);
257
277
  }
258
278
 
259
279
  /* Unavailable dates */
@@ -272,10 +292,15 @@
272
292
 
273
293
  /* Cell indicator - small dot at bottom center of cell */
274
294
  .calendar__cell-indicator {
275
- @apply absolute bottom-1 left-1/2 size-[3px] -translate-x-1/2 rounded-xs bg-muted;
295
+ @apply absolute bottom-1 left-1/2 size-[3px] -translate-x-1/2 rounded-xs;
296
+
297
+ --calendar-cell-indicator-bg: var(--muted);
298
+ --calendar-cell-indicator-bg-selected: var(--accent-foreground);
299
+
300
+ background-color: var(--calendar-cell-indicator-bg);
276
301
 
277
302
  /* Indicator color changes when cell is selected */
278
303
  [data-selected="true"] > & {
279
- @apply bg-accent-foreground;
304
+ background-color: var(--calendar-cell-indicator-bg-selected);
280
305
  }
281
306
  }