@vvedantb/ui 0.15.1 → 0.16.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/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)
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`, the range `bg-primary`, and the thumb a `size-4` circle on `bg-background` with the `Switch` focus ring. 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.15.1",
3
+ "version": "0.16.0",
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,56 @@
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
+ };
9
+
10
+ // `id`, `aria-label`, `aria-labelledby` and `aria-valuetext` go to the first
11
+ // Thumb, not Root: the thumb is the focusable role="slider" element, so
12
+ // `<Label htmlFor={id}>` and the accessible name land on it. The ref stays on Root.
13
+ const Slider = React.forwardRef<React.ComponentRef<typeof SliderPrimitive.Root>, SliderProps>(
14
+ (
15
+ {
16
+ className,
17
+ id,
18
+ "aria-label": ariaLabel,
19
+ "aria-labelledby": ariaLabelledBy,
20
+ "aria-valuetext": ariaValueText,
21
+ thumbLabels,
22
+ ...props
23
+ },
24
+ ref,
25
+ ) => {
26
+ const count = (props.value ?? props.defaultValue ?? [props.min ?? 0]).length;
27
+ return (
28
+ <SliderPrimitive.Root
29
+ className={cn(
30
+ "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",
31
+ className,
32
+ )}
33
+ {...props}
34
+ ref={ref}
35
+ >
36
+ <SliderPrimitive.Track className="relative grow overflow-hidden rounded-full bg-default data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5">
37
+ <SliderPrimitive.Range className="absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full" />
38
+ </SliderPrimitive.Track>
39
+ {Array.from({ length: count }, (_, i) => (
40
+ <SliderPrimitive.Thumb
41
+ key={i}
42
+ id={i === 0 ? id : undefined}
43
+ aria-label={thumbLabels?.[i] ?? (i === 0 ? ariaLabel : undefined)}
44
+ aria-labelledby={i === 0 ? ariaLabelledBy : undefined}
45
+ aria-valuetext={i === 0 ? ariaValueText : undefined}
46
+ className="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"
47
+ />
48
+ ))}
49
+ </SliderPrimitive.Root>
50
+ );
51
+ },
52
+ );
53
+ Slider.displayName = SliderPrimitive.Root.displayName;
54
+
55
+ export { Slider };
56
+ export type { SliderProps };