@vvedantb/ui 0.15.1 → 0.16.1
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/README.md +16 -0
- package/package.json +3 -1
- package/src/index.ts +2 -0
- package/src/ui/labeled-slider-row.tsx +86 -0
- package/src/ui/slider.tsx +78 -0
package/README.md
CHANGED
|
@@ -1002,6 +1002,22 @@ import { DatePicker, DateTimePicker, MonthPicker } from "@vvedantb/ui";
|
|
|
1002
1002
|
- `MonthPicker`: `value` "YYYY-MM" or "", with year navigation and a month grid. `min` and `max` are "YYYY-MM".
|
|
1003
1003
|
- `Calendar`: the styled `DayPicker` for custom layouts (every `DayPicker` prop). `parseDateValue` and `formatDateValue` convert between "YYYY-MM-DD" and a local `Date`.
|
|
1004
1004
|
|
|
1005
|
+
## Slider (0.16.0, dark contrast and className props 0.16.1)
|
|
1006
|
+
|
|
1007
|
+
shadcn's slider on `@radix-ui/react-slider` (a peer dependency), with package tokens.
|
|
1008
|
+
|
|
1009
|
+
```tsx
|
|
1010
|
+
import { LabeledSliderRow, Slider } from "@vvedantb/ui";
|
|
1011
|
+
|
|
1012
|
+
<Slider value={volume} onValueChange={setVolume} max={100} step={1} aria-label="Volume" />
|
|
1013
|
+
<Slider defaultValue={[20, 80]} thumbLabels={["Minimum price", "Maximum price"]} />
|
|
1014
|
+
<LabeledSliderRow id="temperature" label="Temperature" value={temp} onValueChange={setTemp} max={2} step={0.1} formatValue={(v) => v.map((n) => n.toFixed(1)).join("–")} minLabel="Precise" maxLabel="Creative" />
|
|
1015
|
+
```
|
|
1016
|
+
|
|
1017
|
+
- `Slider`: every Radix `Root` prop (`value`, `defaultValue`, `min`, `max`, `step`, `orientation`, `disabled`, `onValueChange`, `onValueCommit`, and so on). It renders one thumb per value, so `[n]` gives one thumb and `[a, b]` a range. The track is `bg-default` (`dark:bg-muted/40`), the range `bg-primary`, and the thumb a `size-4` circle on `bg-background` with `border-border` (`dark:bg-foreground dark:border-muted`, a light thumb in dark mode) and the `Switch` focus ring. `trackClassName`, `rangeClassName` and `thumbClassName` merge after the defaults, so caller classes win; override a `dark:` default with a `dark:` class. Disabled dims to 50%. A vertical slider needs a height from its parent or `className` (default `min-h-40`). The ref goes to `Root`.
|
|
1018
|
+
- Names: `id`, `aria-label`, `aria-labelledby` and `aria-valuetext` go to the first thumb, the focusable `role="slider"` element, not `Root`. `thumbLabels` sets `aria-label` per thumb and wins over `aria-label` on the first. Without a name, Radix labels a two-thumb range "Minimum" and "Maximum". The thumb is a `<span>`, so `<Label htmlFor>` does not name it; pass `aria-labelledby` with the label's `id`.
|
|
1019
|
+
- `LabeledSliderRow`: the `LabeledSwitchRow` layout with the slider below. `id`, `label`, `description`, `icon`, `value`, `onValueChange`, `onValueCommit`, `min`, `max`, `step`, `disabled`, `thumbLabels`, `className`. `formatValue` sets the right-aligned value (default: values joined by an en dash). `minLabel` and `maxLabel` add captions under the ends. The label names the first thumb through `aria-labelledby`, unless `thumbLabels` is set.
|
|
1020
|
+
|
|
1005
1021
|
## Toaster
|
|
1006
1022
|
|
|
1007
1023
|
```tsx
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vvedantb/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "Shared React 19 UI for web apps: one JavaScript entry (primitives, shell pieces, app helpers including the Convex and Clerk provider, VITE env parsing and ensure-user, artifact viewers over the @vvedantb/server artifacts core, native tokens), design-token CSS, shared Tailwind v4 CSS and a Tailwind preset.",
|
|
6
6
|
"type": "module",
|
|
@@ -84,6 +84,7 @@
|
|
|
84
84
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
85
85
|
"@radix-ui/react-select": "^2.2.6",
|
|
86
86
|
"@radix-ui/react-separator": "^1.1.8",
|
|
87
|
+
"@radix-ui/react-slider": "^1.4.7",
|
|
87
88
|
"@radix-ui/react-slot": "^1.2.4",
|
|
88
89
|
"@radix-ui/react-switch": "^1.1.7",
|
|
89
90
|
"@radix-ui/react-tabs": "^1.1.13",
|
|
@@ -131,6 +132,7 @@
|
|
|
131
132
|
"@radix-ui/react-scroll-area": "^1.2.10",
|
|
132
133
|
"@radix-ui/react-select": "^2.2.6",
|
|
133
134
|
"@radix-ui/react-separator": "^1.1.8",
|
|
135
|
+
"@radix-ui/react-slider": "^1.4.7",
|
|
134
136
|
"@radix-ui/react-slot": "^1.2.4",
|
|
135
137
|
"@radix-ui/react-switch": "^1.1.7",
|
|
136
138
|
"@radix-ui/react-tabs": "^1.1.13",
|
package/src/index.ts
CHANGED
|
@@ -25,6 +25,7 @@ export * from "./ui/empty-state";
|
|
|
25
25
|
export * from "./ui/hover-card";
|
|
26
26
|
export * from "./ui/input";
|
|
27
27
|
export * from "./ui/label";
|
|
28
|
+
export * from "./ui/labeled-slider-row";
|
|
28
29
|
export * from "./ui/labeled-switch-row";
|
|
29
30
|
export * from "./ui/pagination";
|
|
30
31
|
export * from "./ui/popover";
|
|
@@ -47,6 +48,7 @@ export * from "./ui/select-field";
|
|
|
47
48
|
export * from "./ui/separator";
|
|
48
49
|
export * from "./ui/skeleton";
|
|
49
50
|
export * from "./ui/slide-over-panel";
|
|
51
|
+
export * from "./ui/slider";
|
|
50
52
|
export * from "./ui/sonner";
|
|
51
53
|
export * from "./ui/spinner";
|
|
52
54
|
export * from "./ui/switch";
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "../utils/cn";
|
|
3
|
+
import { Label } from "./label";
|
|
4
|
+
import { Slider } from "./slider";
|
|
5
|
+
|
|
6
|
+
interface LabeledSliderRowProps {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
description?: string;
|
|
10
|
+
value: number[];
|
|
11
|
+
onValueChange?: (value: number[]) => void;
|
|
12
|
+
onValueCommit?: (value: number[]) => void;
|
|
13
|
+
min?: number;
|
|
14
|
+
max?: number;
|
|
15
|
+
step?: number;
|
|
16
|
+
disabled?: boolean;
|
|
17
|
+
icon?: ReactNode;
|
|
18
|
+
/** Formats the right-aligned value display. Default joins the values with an en dash. */
|
|
19
|
+
formatValue?: (value: number[]) => ReactNode;
|
|
20
|
+
minLabel?: ReactNode;
|
|
21
|
+
maxLabel?: ReactNode;
|
|
22
|
+
thumbLabels?: string[];
|
|
23
|
+
className?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function LabeledSliderRow({
|
|
27
|
+
id,
|
|
28
|
+
label,
|
|
29
|
+
description,
|
|
30
|
+
value,
|
|
31
|
+
onValueChange,
|
|
32
|
+
onValueCommit,
|
|
33
|
+
min,
|
|
34
|
+
max,
|
|
35
|
+
step,
|
|
36
|
+
disabled,
|
|
37
|
+
icon,
|
|
38
|
+
formatValue = (v) => v.join("–"),
|
|
39
|
+
minLabel,
|
|
40
|
+
maxLabel,
|
|
41
|
+
thumbLabels,
|
|
42
|
+
className,
|
|
43
|
+
}: LabeledSliderRowProps) {
|
|
44
|
+
const labelId = `${id}-label`;
|
|
45
|
+
return (
|
|
46
|
+
<div className={cn("flex flex-col gap-3", className)}>
|
|
47
|
+
<div className="flex items-start justify-between gap-3">
|
|
48
|
+
<div className="flex min-w-0 items-start gap-2">
|
|
49
|
+
{icon ? (
|
|
50
|
+
<span className="mt-0.5 shrink-0 text-muted-foreground">{icon}</span>
|
|
51
|
+
) : null}
|
|
52
|
+
<div className="min-w-0">
|
|
53
|
+
<Label id={labelId} htmlFor={id} className="text-sm font-medium">
|
|
54
|
+
{label}
|
|
55
|
+
</Label>
|
|
56
|
+
{description ? (
|
|
57
|
+
<p className="mt-1 text-xs text-muted-foreground text-pretty">{description}</p>
|
|
58
|
+
) : null}
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
<output htmlFor={id} className="shrink-0 text-sm tabular-nums text-muted-foreground">
|
|
62
|
+
{formatValue(value)}
|
|
63
|
+
</output>
|
|
64
|
+
</div>
|
|
65
|
+
{/* The thumb is a span, which `htmlFor` cannot name, so aria-labelledby carries the name unless thumbLabels does. */}
|
|
66
|
+
<Slider
|
|
67
|
+
id={id}
|
|
68
|
+
aria-labelledby={thumbLabels ? undefined : labelId}
|
|
69
|
+
value={value}
|
|
70
|
+
onValueChange={onValueChange}
|
|
71
|
+
onValueCommit={onValueCommit}
|
|
72
|
+
min={min}
|
|
73
|
+
max={max}
|
|
74
|
+
step={step}
|
|
75
|
+
disabled={disabled}
|
|
76
|
+
thumbLabels={thumbLabels}
|
|
77
|
+
/>
|
|
78
|
+
{minLabel != null || maxLabel != null ? (
|
|
79
|
+
<div className="flex justify-between text-xs text-muted-foreground">
|
|
80
|
+
<span>{minLabel}</span>
|
|
81
|
+
<span>{maxLabel}</span>
|
|
82
|
+
</div>
|
|
83
|
+
) : null}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as SliderPrimitive from "@radix-ui/react-slider";
|
|
3
|
+
import { cn } from "../utils/cn";
|
|
4
|
+
|
|
5
|
+
type SliderProps = React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root> & {
|
|
6
|
+
/** Accessible name per thumb, by index. Wins over `aria-label` on the first thumb. */
|
|
7
|
+
thumbLabels?: string[];
|
|
8
|
+
/** Classes for the track, merged after the defaults so they win (`bg-default dark:bg-muted/40`). */
|
|
9
|
+
trackClassName?: string;
|
|
10
|
+
/** Classes for the filled range, merged after the defaults so they win (`bg-primary`). */
|
|
11
|
+
rangeClassName?: string;
|
|
12
|
+
/** Classes for every thumb, merged after the defaults so they win (`bg-background dark:bg-foreground dark:border-muted`). */
|
|
13
|
+
thumbClassName?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// `id`, `aria-label`, `aria-labelledby` and `aria-valuetext` go to the first
|
|
17
|
+
// Thumb, not Root: the thumb is the focusable role="slider" element, so
|
|
18
|
+
// `<Label htmlFor={id}>` and the accessible name land on it. The ref stays on Root.
|
|
19
|
+
const Slider = React.forwardRef<React.ComponentRef<typeof SliderPrimitive.Root>, SliderProps>(
|
|
20
|
+
(
|
|
21
|
+
{
|
|
22
|
+
className,
|
|
23
|
+
id,
|
|
24
|
+
"aria-label": ariaLabel,
|
|
25
|
+
"aria-labelledby": ariaLabelledBy,
|
|
26
|
+
"aria-valuetext": ariaValueText,
|
|
27
|
+
thumbLabels,
|
|
28
|
+
trackClassName,
|
|
29
|
+
rangeClassName,
|
|
30
|
+
thumbClassName,
|
|
31
|
+
...props
|
|
32
|
+
},
|
|
33
|
+
ref,
|
|
34
|
+
) => {
|
|
35
|
+
const count = (props.value ?? props.defaultValue ?? [props.min ?? 0]).length;
|
|
36
|
+
return (
|
|
37
|
+
<SliderPrimitive.Root
|
|
38
|
+
className={cn(
|
|
39
|
+
"relative flex w-full touch-none select-none items-center data-[disabled]:cursor-not-allowed data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-40 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
|
|
40
|
+
className,
|
|
41
|
+
)}
|
|
42
|
+
{...props}
|
|
43
|
+
ref={ref}
|
|
44
|
+
>
|
|
45
|
+
<SliderPrimitive.Track
|
|
46
|
+
className={cn(
|
|
47
|
+
"relative grow overflow-hidden rounded-full bg-default dark:bg-muted/40 data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5",
|
|
48
|
+
trackClassName,
|
|
49
|
+
)}
|
|
50
|
+
>
|
|
51
|
+
<SliderPrimitive.Range
|
|
52
|
+
className={cn(
|
|
53
|
+
"absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full",
|
|
54
|
+
rangeClassName,
|
|
55
|
+
)}
|
|
56
|
+
/>
|
|
57
|
+
</SliderPrimitive.Track>
|
|
58
|
+
{Array.from({ length: count }, (_, i) => (
|
|
59
|
+
<SliderPrimitive.Thumb
|
|
60
|
+
key={i}
|
|
61
|
+
id={i === 0 ? id : undefined}
|
|
62
|
+
aria-label={thumbLabels?.[i] ?? (i === 0 ? ariaLabel : undefined)}
|
|
63
|
+
aria-labelledby={i === 0 ? ariaLabelledBy : undefined}
|
|
64
|
+
aria-valuetext={i === 0 ? ariaValueText : undefined}
|
|
65
|
+
className={cn(
|
|
66
|
+
"block size-4 shrink-0 rounded-full border border-border bg-background shadow-sm transition-[box-shadow] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring data-[disabled]:cursor-not-allowed dark:border-muted dark:bg-foreground",
|
|
67
|
+
thumbClassName,
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
))}
|
|
71
|
+
</SliderPrimitive.Root>
|
|
72
|
+
);
|
|
73
|
+
},
|
|
74
|
+
);
|
|
75
|
+
Slider.displayName = SliderPrimitive.Root.displayName;
|
|
76
|
+
|
|
77
|
+
export { Slider };
|
|
78
|
+
export type { SliderProps };
|