@kennycorrea/ui 0.4.0 → 0.5.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.
- package/CHANGELOG.md +27 -0
- package/README.md +19 -2
- package/dist/atoms/Slider/Slider.d.ts +15 -1
- package/dist/atoms/Slider/Slider.js +71 -4
- package/dist/atoms/Slider/geometry.d.ts +23 -0
- package/dist/atoms/Slider/geometry.js +27 -0
- package/package.json +11 -4
- package/tokens.css +113 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.5.0 — 2026-10-06
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- **`Slider` `fillFrom`**: fills from a fixed value to the thumb, in either direction, instead of
|
|
7
|
+
from `min`. Same units as `value`, clamped to [min, max]; nothing is filled when the thumb sits
|
|
8
|
+
on it. The fill's ends line up with the thumb's centre at every value, using Radix's own
|
|
9
|
+
in-bounds offset. Built on `@radix-ui/react-slider` and styled from Themes' public tokens
|
|
10
|
+
(`.ui-slider*` in `tokens.css`), because Themes' slider gives no way to place its range —
|
|
11
|
+
consumers overriding `.rt-SliderRange` can drop that CSS.
|
|
12
|
+
Single horizontal left-to-right thumb only: with two thumbs, `orientation="vertical"`,
|
|
13
|
+
`inverted` or `dir="rtl"` it is ignored with a development warning. Margin props are not
|
|
14
|
+
supported on this path; wrap it in a `Box`.
|
|
15
|
+
- **`Slider` `marks`**: `{ value, label }[]` drawn as an `aria-hidden` scale above the track,
|
|
16
|
+
each centred on its value, the first and last growing inwards so neither overflows.
|
|
17
|
+
- Unit tests (Vitest + Testing Library); `npm test` now runs them after lint and types.
|
|
18
|
+
|
|
19
|
+
### Unchanged
|
|
20
|
+
- Without `fillFrom`, `Slider` is still the Radix Themes slider, rendered exactly as in 0.4.0.
|
|
21
|
+
|
|
22
|
+
### Requires
|
|
23
|
+
- `tokens.css` imported, as it already must be for toasts — the `fillFrom` path is unstyled
|
|
24
|
+
without it.
|
|
25
|
+
|
|
26
|
+
## 0.4.0
|
|
27
|
+
- `Slider` atom over Radix Themes' slider; `label` required.
|
package/README.md
CHANGED
|
@@ -204,8 +204,8 @@ blaming the workflow:
|
|
|
204
204
|
because there is no one there to type a one-time password. That failure reads like a bad
|
|
205
205
|
credential and is not one.
|
|
206
206
|
|
|
207
|
-
⚠ **`npm test` has to keep existing.** It is lint
|
|
208
|
-
|
|
207
|
+
⚠ **`npm test` has to keep existing.** It is lint, a typecheck and (since 0.5.0) the Vitest suite — and
|
|
208
|
+
it is load-bearing for a reason that has nothing to do with quality:
|
|
209
209
|
without a `test` script the `build` job fails on `Missing script: "test"`, `publish-npm` never
|
|
210
210
|
runs because it needs `build`, and **the release publishes nothing while looking like it worked.**
|
|
211
211
|
That was the state when the workflow was first added.
|
|
@@ -270,3 +270,20 @@ becomes the `aria-label`, because a slider has no visible text of its own.
|
|
|
270
270
|
```
|
|
271
271
|
|
|
272
272
|
Values are arrays, as in Radix: one entry per thumb.
|
|
273
|
+
|
|
274
|
+
## Slider fill origin and marks — 0.5.0
|
|
275
|
+
|
|
276
|
+
```tsx
|
|
277
|
+
const marks = [0, 6, 11, 16, 22, 29].map((value) => ({ value, label: String(value) }));
|
|
278
|
+
<Slider label="Budget cycle" min={0} max={29} value={day} onValueChange={setDay}
|
|
279
|
+
fillFrom={16} marks={marks} />
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`fillFrom` fills from that value to the thumb, either way. Themes' slider cannot place its
|
|
283
|
+
range, so with `fillFrom` the atom is built on `@radix-ui/react-slider` and styled by
|
|
284
|
+
`.ui-slider*` in `tokens.css` from Themes' public tokens — no `.rt-*` class is touched.
|
|
285
|
+
Without it, nothing changed. Limits and details: [CHANGELOG.md](CHANGELOG.md); a full example
|
|
286
|
+
is in `examples/BudgetCycleSlider.tsx`.
|
|
287
|
+
|
|
288
|
+
⚠ `--ui-slider-thumb-size` mirrors Themes' size-2 thumb (`--space-2 + --space-1`). The fill
|
|
289
|
+
and the marks are placed from it, so if Themes ever resizes its thumb, change it there.
|
|
@@ -1,10 +1,24 @@
|
|
|
1
1
|
import { Slider as ThemedSlider } from '@radix-ui/themes';
|
|
2
2
|
import { type ComponentPropsWithoutRef } from 'react';
|
|
3
|
-
|
|
3
|
+
type ThemedProps = ComponentPropsWithoutRef<typeof ThemedSlider>;
|
|
4
|
+
export interface SliderMark {
|
|
5
|
+
value: number;
|
|
6
|
+
label: string;
|
|
7
|
+
}
|
|
8
|
+
export interface SliderProps extends Omit<ThemedProps, 'size' | 'aria-label'> {
|
|
4
9
|
/**
|
|
5
10
|
* Required: a slider has no visible text of its own, so without this a screen reader
|
|
6
11
|
* announces a bare number with nothing to say what it measures.
|
|
7
12
|
*/
|
|
8
13
|
label: string;
|
|
14
|
+
/**
|
|
15
|
+
* Fill from this value to the thumb instead of from `min`, in either direction — for a scale
|
|
16
|
+
* with a meaningful middle. Same units as `value`, clamped to [min, max]. Only for one
|
|
17
|
+
* horizontal left-to-right thumb; anything else ignores it and warns in development.
|
|
18
|
+
*/
|
|
19
|
+
fillFrom?: number;
|
|
20
|
+
/** A scale drawn above the track. Decorative: the slider already announces its value. */
|
|
21
|
+
marks?: SliderMark[];
|
|
9
22
|
}
|
|
10
23
|
export declare const Slider: import("react").ForwardRefExoticComponent<SliderProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
24
|
+
export {};
|
|
@@ -1,7 +1,74 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as SliderPrimitive from '@radix-ui/react-slider';
|
|
2
3
|
import { Slider as ThemedSlider } from '@radix-ui/themes';
|
|
3
|
-
import { forwardRef } from 'react';
|
|
4
|
+
import { forwardRef, useState } from 'react';
|
|
4
5
|
import { cn } from '../../utils/cn.js';
|
|
5
|
-
|
|
6
|
-
|
|
6
|
+
import { fillEdges, fillStyle, percentOf, thumbCentre } from './geometry.js';
|
|
7
|
+
const MARGIN_KEYS = ['m', 'mx', 'my', 'mt', 'mr', 'mb', 'ml'];
|
|
8
|
+
const isDev = globalThis.process?.env?.NODE_ENV !==
|
|
9
|
+
'production';
|
|
10
|
+
/** Why `fillFrom` cannot be honoured, or null when it can. */
|
|
11
|
+
function fillFromUnsupported(props, thumbs) {
|
|
12
|
+
if (thumbs !== 1)
|
|
13
|
+
return `it needs exactly one thumb, and this slider has ${thumbs}`;
|
|
14
|
+
if (props.orientation === 'vertical')
|
|
15
|
+
return 'vertical sliders are not supported';
|
|
16
|
+
if (props.inverted)
|
|
17
|
+
return 'inverted sliders are not supported';
|
|
18
|
+
if (props.dir === 'rtl')
|
|
19
|
+
return 'right-to-left sliders are not supported';
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
export const Slider = forwardRef(function Slider(props, ref) {
|
|
23
|
+
const { label, fillFrom, marks, className, onValueChange, ...rest } = props;
|
|
24
|
+
const min = rest.min ?? 0;
|
|
25
|
+
const max = rest.max ?? 100;
|
|
26
|
+
// Mirrored so the fill can follow an uncontrolled slider too.
|
|
27
|
+
const [inner, setInner] = useState(rest.defaultValue ?? [min]);
|
|
28
|
+
const values = rest.value ?? inner;
|
|
29
|
+
const handleChange = (next) => {
|
|
30
|
+
setInner(next);
|
|
31
|
+
onValueChange?.(next);
|
|
32
|
+
};
|
|
33
|
+
const unsupported = fillFrom === undefined ? null : fillFromUnsupported(props, values.length);
|
|
34
|
+
if (unsupported && isDev)
|
|
35
|
+
console.warn(`Slider: fillFrom is ignored — ${unsupported}.`);
|
|
36
|
+
const slider = fillFrom !== undefined && !unsupported ? (_jsx(FillFromSlider, { ref: ref, ...rest, label: label, className: className, values: values, fillFrom: fillFrom, min: min, max: max, onValueChange: handleChange })) : (_jsx(ThemedSlider, { ref: ref, size: "2", "aria-label": label, className: cn(className), ...rest, onValueChange: handleChange }));
|
|
37
|
+
if (!marks?.length)
|
|
38
|
+
return slider;
|
|
39
|
+
return (_jsxs("div", { className: "ui-slider-field", children: [_jsx(SliderMarks, { marks: marks, min: min, max: max }), slider] }));
|
|
7
40
|
});
|
|
41
|
+
/**
|
|
42
|
+
* Radix Themes draws its range itself and offers no way to place it, so this path is built on the
|
|
43
|
+
* primitives and styled in `tokens.css` (`.ui-slider*`) from Themes' public tokens — never by
|
|
44
|
+
* reaching into Themes' own class names.
|
|
45
|
+
*/
|
|
46
|
+
const FillFromSlider = forwardRef(function FillFromSlider({ label, values, fillFrom, min, max, className, color, radius, variant, highContrast, defaultValue: _uncontrolledHandledAbove, tabIndex, ...rest }, ref) {
|
|
47
|
+
const primitiveProps = {};
|
|
48
|
+
let droppedMargin = false;
|
|
49
|
+
for (const [key, val] of Object.entries(rest)) {
|
|
50
|
+
if (MARGIN_KEYS.includes(key))
|
|
51
|
+
droppedMargin = true;
|
|
52
|
+
else
|
|
53
|
+
primitiveProps[key] = val;
|
|
54
|
+
}
|
|
55
|
+
if (droppedMargin && isDev) {
|
|
56
|
+
console.warn('Slider: margin props are not supported with fillFrom — wrap it in a Box.');
|
|
57
|
+
}
|
|
58
|
+
const edges = fillEdges(values[0] ?? min, fillFrom, min, max);
|
|
59
|
+
return (_jsxs(SliderPrimitive.Root, { ref: ref, ...primitiveProps, value: values, min: min, max: max, "aria-label": label, "data-accent-color": color, "data-radius": radius, "data-variant": variant ?? 'surface', className: cn('ui-slider', highContrast && 'ui-slider-high-contrast', className), children: [_jsx(SliderPrimitive.Track, { className: "ui-slider-track", children: _jsx("span", { className: "ui-slider-fill", "data-fill": "", "data-empty": edges.from === edges.to ? '' : undefined, "data-disabled": rest.disabled ? '' : undefined, style: fillStyle(edges) }) }), _jsx(SliderPrimitive.Thumb, { className: "ui-slider-thumb", "aria-label": label, ...(tabIndex !== undefined ? { tabIndex } : undefined) })] }));
|
|
60
|
+
});
|
|
61
|
+
function SliderMarks({ marks, min, max }) {
|
|
62
|
+
const sorted = [...marks].sort((a, b) => a.value - b.value);
|
|
63
|
+
const last = sorted.length - 1;
|
|
64
|
+
return (_jsx("div", { className: "ui-slider-marks", "aria-hidden": true, children: sorted.map((mark, i) => {
|
|
65
|
+
const centre = thumbCentre(percentOf(mark.value, min, max));
|
|
66
|
+
// Centred over its value, except the two ends, which grow inwards so neither overflows.
|
|
67
|
+
let style = { left: centre, transform: 'translateX(-50%)' };
|
|
68
|
+
if (last > 0 && i === 0)
|
|
69
|
+
style = { left: centre };
|
|
70
|
+
if (last > 0 && i === last)
|
|
71
|
+
style = { right: `calc(100% - ${centre})` };
|
|
72
|
+
return (_jsx("span", { "data-mark": mark.value, style: style, children: mark.label }, `${mark.value}-${mark.label}`));
|
|
73
|
+
}) }));
|
|
74
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The same maths Radix's slider uses to place a thumb, so anything we draw on the track lines up
|
|
3
|
+
* with the thumb's centre. Radix keeps the thumb inside the track by nudging it inwards by up to
|
|
4
|
+
* half its width at the ends (`getThumbInBoundsOffset`): `+half` at 0%, nothing at 50%, `-half`
|
|
5
|
+
* at 100%. Written as CSS against `--ui-slider-thumb-size` so no element has to be measured.
|
|
6
|
+
*/
|
|
7
|
+
/** Radix's `convertValueToPercentage`: 0–100, clamped. */
|
|
8
|
+
export declare function percentOf(value: number, min: number, max: number): number;
|
|
9
|
+
/** Distance from the track's left edge to the centre of a thumb sitting at `percent`. */
|
|
10
|
+
export declare function thumbCentre(percent: number): string;
|
|
11
|
+
export interface FillEdges {
|
|
12
|
+
/** Percent of the lower end, clamped to the track. */
|
|
13
|
+
from: number;
|
|
14
|
+
/** Percent of the higher end. Equal to `from` when nothing is filled. */
|
|
15
|
+
to: number;
|
|
16
|
+
}
|
|
17
|
+
/** The stretch between `fillFrom` and the thumb, whichever is lower to whichever is higher. */
|
|
18
|
+
export declare function fillEdges(value: number, fillFrom: number, min: number, max: number): FillEdges;
|
|
19
|
+
/** Inline position of the fill: left edge at the lower centre, right edge at the higher one. */
|
|
20
|
+
export declare function fillStyle({ from, to }: FillEdges): {
|
|
21
|
+
left: string;
|
|
22
|
+
right: string;
|
|
23
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The same maths Radix's slider uses to place a thumb, so anything we draw on the track lines up
|
|
3
|
+
* with the thumb's centre. Radix keeps the thumb inside the track by nudging it inwards by up to
|
|
4
|
+
* half its width at the ends (`getThumbInBoundsOffset`): `+half` at 0%, nothing at 50%, `-half`
|
|
5
|
+
* at 100%. Written as CSS against `--ui-slider-thumb-size` so no element has to be measured.
|
|
6
|
+
*/
|
|
7
|
+
const clamp = (n, lo, hi) => Math.min(hi, Math.max(lo, n));
|
|
8
|
+
/** Radix's `convertValueToPercentage`: 0–100, clamped. */
|
|
9
|
+
export function percentOf(value, min, max) {
|
|
10
|
+
if (max === min)
|
|
11
|
+
return 0;
|
|
12
|
+
return clamp(((value - min) / (max - min)) * 100, 0, 100);
|
|
13
|
+
}
|
|
14
|
+
/** Distance from the track's left edge to the centre of a thumb sitting at `percent`. */
|
|
15
|
+
export function thumbCentre(percent) {
|
|
16
|
+
return `calc(${percent}% + var(--ui-slider-thumb-size) * ${0.5 - percent / 100})`;
|
|
17
|
+
}
|
|
18
|
+
/** The stretch between `fillFrom` and the thumb, whichever is lower to whichever is higher. */
|
|
19
|
+
export function fillEdges(value, fillFrom, min, max) {
|
|
20
|
+
const a = percentOf(value, min, max);
|
|
21
|
+
const b = percentOf(fillFrom, min, max);
|
|
22
|
+
return { from: Math.min(a, b), to: Math.max(a, b) };
|
|
23
|
+
}
|
|
24
|
+
/** Inline position of the fill: left edge at the lower centre, right edge at the higher one. */
|
|
25
|
+
export function fillStyle({ from, to }) {
|
|
26
|
+
return { left: thumbCentre(from), right: `calc(100% - ${thumbCentre(to)})` };
|
|
27
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kennycorrea/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Upfront's house component layer: atoms over Radix Themes, the token bridge, and the accent logic.",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
"files": [
|
|
9
9
|
"dist",
|
|
10
10
|
"tokens.css",
|
|
11
|
-
"README.md"
|
|
11
|
+
"README.md",
|
|
12
|
+
"CHANGELOG.md"
|
|
12
13
|
],
|
|
13
14
|
"main": "./dist/index.js",
|
|
14
15
|
"types": "./dist/index.d.ts",
|
|
@@ -25,7 +26,8 @@
|
|
|
25
26
|
"lint": "biome check .",
|
|
26
27
|
"format": "biome check --write .",
|
|
27
28
|
"prepublishOnly": "npm run build",
|
|
28
|
-
"test": "biome check . && tsc -p tsconfig.json
|
|
29
|
+
"test": "biome check . && tsc -p tsconfig.test.json && vitest run",
|
|
30
|
+
"test:watch": "vitest"
|
|
29
31
|
},
|
|
30
32
|
"peerDependencies": {
|
|
31
33
|
"@radix-ui/themes": "^3.3.0",
|
|
@@ -33,6 +35,7 @@
|
|
|
33
35
|
"react-dom": "^19.0.0"
|
|
34
36
|
},
|
|
35
37
|
"dependencies": {
|
|
38
|
+
"@radix-ui/react-slider": "^1.5.0",
|
|
36
39
|
"@radix-ui/react-toast": "^1.2.23",
|
|
37
40
|
"clsx": "^2.1.1",
|
|
38
41
|
"tailwind-merge": "^3.7.0"
|
|
@@ -40,11 +43,15 @@
|
|
|
40
43
|
"devDependencies": {
|
|
41
44
|
"@biomejs/biome": "^2.5.13",
|
|
42
45
|
"@radix-ui/themes": "^3.3.0",
|
|
46
|
+
"@testing-library/react": "^16.3.3",
|
|
47
|
+
"@testing-library/user-event": "^14.6.7",
|
|
43
48
|
"@types/react": "^19.2.18",
|
|
44
49
|
"@types/react-dom": "^19.2.7",
|
|
50
|
+
"jsdom": "^30.1.2",
|
|
45
51
|
"react": "^19.2.8",
|
|
46
52
|
"react-dom": "^19.2.8",
|
|
47
|
-
"typescript": "~6.0.2"
|
|
53
|
+
"typescript": "~6.0.2",
|
|
54
|
+
"vitest": "^5.0.3"
|
|
48
55
|
},
|
|
49
56
|
"publishConfig": {
|
|
50
57
|
"access": "public"
|
package/tokens.css
CHANGED
|
@@ -261,3 +261,116 @@
|
|
|
261
261
|
@media (prefers-reduced-motion: reduce) {
|
|
262
262
|
.ui-toast[data-state] { animation-duration: 1ms; }
|
|
263
263
|
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Slider with `fillFrom` — built on @radix-ui/react-slider because Radix Themes gives no way to
|
|
267
|
+
* place its range. Mirrors Themes' size-2 slider using only its public tokens, so a Themes
|
|
268
|
+
* release cannot break it by renaming a class. Keep `--ui-slider-thumb-size` in step with
|
|
269
|
+
* Themes' `--slider-thumb-size`: the fill and the marks are positioned from it.
|
|
270
|
+
*/
|
|
271
|
+
.ui-slider-field,
|
|
272
|
+
.ui-slider {
|
|
273
|
+
--ui-slider-track-size: var(--space-2);
|
|
274
|
+
--ui-slider-thumb-size: calc(var(--ui-slider-track-size) + var(--space-1));
|
|
275
|
+
}
|
|
276
|
+
.ui-slider-field { display: flex; flex-direction: column; gap: var(--space-2); flex-grow: 1; width: 100%; }
|
|
277
|
+
.ui-slider-marks {
|
|
278
|
+
position: relative;
|
|
279
|
+
height: var(--line-height-1);
|
|
280
|
+
font-size: var(--font-size-1);
|
|
281
|
+
line-height: var(--line-height-1);
|
|
282
|
+
color: var(--gray-11);
|
|
283
|
+
}
|
|
284
|
+
.ui-slider-marks > span { position: absolute; top: 0; white-space: nowrap; }
|
|
285
|
+
|
|
286
|
+
.ui-slider {
|
|
287
|
+
--ui-slider-thumb-shadow: 0 0 0 1px var(--black-a4);
|
|
288
|
+
position: relative;
|
|
289
|
+
display: flex;
|
|
290
|
+
align-items: center;
|
|
291
|
+
flex-grow: 1;
|
|
292
|
+
width: 100%;
|
|
293
|
+
height: var(--ui-slider-track-size);
|
|
294
|
+
border-radius: max(
|
|
295
|
+
calc(var(--radius-factor) * var(--ui-slider-track-size) / 3),
|
|
296
|
+
calc(var(--radius-factor) * var(--radius-thumb))
|
|
297
|
+
);
|
|
298
|
+
user-select: none;
|
|
299
|
+
touch-action: none;
|
|
300
|
+
}
|
|
301
|
+
.ui-slider-track {
|
|
302
|
+
position: relative;
|
|
303
|
+
overflow: hidden;
|
|
304
|
+
flex-grow: 1;
|
|
305
|
+
height: var(--ui-slider-track-size);
|
|
306
|
+
border-radius: inherit;
|
|
307
|
+
background-color: var(--gray-a3);
|
|
308
|
+
box-shadow: inset 0 0 0 1px var(--gray-a5);
|
|
309
|
+
}
|
|
310
|
+
.ui-slider-fill {
|
|
311
|
+
position: absolute;
|
|
312
|
+
height: 100%;
|
|
313
|
+
border-radius: inherit;
|
|
314
|
+
background-color: var(--accent-track);
|
|
315
|
+
box-shadow: inset 0 0 0 1px var(--gray-a5);
|
|
316
|
+
}
|
|
317
|
+
.ui-slider-fill[data-empty] { visibility: hidden; }
|
|
318
|
+
.ui-slider-high-contrast .ui-slider-fill { background-color: var(--accent-12); }
|
|
319
|
+
|
|
320
|
+
.ui-slider[data-variant='soft'] .ui-slider-track {
|
|
321
|
+
background-color: var(--gray-a4);
|
|
322
|
+
background-image: linear-gradient(var(--white-a1), var(--white-a1));
|
|
323
|
+
box-shadow: none;
|
|
324
|
+
}
|
|
325
|
+
.ui-slider[data-variant='soft'] .ui-slider-fill {
|
|
326
|
+
background-color: var(--accent-6);
|
|
327
|
+
background-image: linear-gradient(var(--accent-a5), var(--accent-a5));
|
|
328
|
+
box-shadow: none;
|
|
329
|
+
}
|
|
330
|
+
.ui-slider[data-variant='classic'] .ui-slider-track { box-shadow: var(--shadow-1); }
|
|
331
|
+
.ui-slider[data-variant='classic'] .ui-slider-fill {
|
|
332
|
+
box-shadow: inset 0 0 0 1px var(--accent-a4), inset 0 1.5px 2px 0 var(--black-a2);
|
|
333
|
+
}
|
|
334
|
+
.ui-slider[data-variant='classic'] {
|
|
335
|
+
--ui-slider-thumb-shadow: 0 0 0 1px var(--black-a3), 0 1px 3px var(--black-a1),
|
|
336
|
+
0 2px 4px -1px var(--black-a1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
.ui-slider-thumb {
|
|
340
|
+
display: block;
|
|
341
|
+
width: var(--ui-slider-thumb-size);
|
|
342
|
+
height: var(--ui-slider-thumb-size);
|
|
343
|
+
outline: 0;
|
|
344
|
+
}
|
|
345
|
+
/* A larger hit area, and a visible thumb that slightly overshoots the track, as in Themes. */
|
|
346
|
+
.ui-slider-thumb::before {
|
|
347
|
+
content: '';
|
|
348
|
+
position: absolute;
|
|
349
|
+
z-index: -1;
|
|
350
|
+
width: calc(var(--ui-slider-thumb-size) * 3);
|
|
351
|
+
height: calc(var(--ui-slider-thumb-size) * 3);
|
|
352
|
+
top: 50%;
|
|
353
|
+
left: 50%;
|
|
354
|
+
transform: translate(-50%, -50%);
|
|
355
|
+
}
|
|
356
|
+
.ui-slider-thumb::after {
|
|
357
|
+
content: '';
|
|
358
|
+
position: absolute;
|
|
359
|
+
inset: calc(-0.25 * var(--ui-slider-track-size));
|
|
360
|
+
background-color: white;
|
|
361
|
+
border-radius: max(var(--radius-1), var(--radius-thumb));
|
|
362
|
+
box-shadow: var(--ui-slider-thumb-shadow);
|
|
363
|
+
cursor: var(--cursor-slider-thumb);
|
|
364
|
+
}
|
|
365
|
+
.ui-slider-thumb:focus-visible::after {
|
|
366
|
+
box-shadow: var(--ui-slider-thumb-shadow), 0 0 0 3px var(--accent-3), 0 0 0 5px var(--focus-8);
|
|
367
|
+
}
|
|
368
|
+
.ui-slider-thumb:active { cursor: var(--cursor-slider-thumb-active); }
|
|
369
|
+
|
|
370
|
+
.ui-slider[data-disabled] { cursor: var(--cursor-disabled); mix-blend-mode: multiply; }
|
|
371
|
+
:is(.dark, .dark-theme) .ui-slider[data-disabled] { mix-blend-mode: screen; }
|
|
372
|
+
.ui-slider-track[data-disabled] { box-shadow: inset 0 0 0 1px var(--gray-a4); }
|
|
373
|
+
.ui-slider-fill[data-disabled] { background: none; box-shadow: none; }
|
|
374
|
+
.ui-slider-thumb[data-disabled],
|
|
375
|
+
.ui-slider-thumb[data-disabled]::after { cursor: var(--cursor-disabled); }
|
|
376
|
+
.ui-slider-thumb[data-disabled]::after { background-color: var(--gray-1); box-shadow: 0 0 0 1px var(--gray-6); }
|