@vvedantb/ui 0.15.0 → 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 +16 -0
- package/package.json +3 -1
- package/src/index.ts +2 -0
- package/src/ui/clear-input.tsx +18 -18
- package/src/ui/labeled-slider-row.tsx +86 -0
- package/src/ui/slider.tsx +56 -0
- package/tokens/README.md +1 -1
- package/tokens/src/css/clear-input.css +26 -15
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.
|
|
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";
|
package/src/ui/clear-input.tsx
CHANGED
|
@@ -130,25 +130,25 @@ const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
|
130
130
|
)}
|
|
131
131
|
{...props}
|
|
132
132
|
/>
|
|
133
|
-
<div
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
133
|
+
<div className="t-clear-clip" aria-hidden="true">
|
|
134
|
+
<div
|
|
135
|
+
ref={mirrorRef}
|
|
136
|
+
className={cn(
|
|
137
|
+
"t-clear-mirror border-0 bg-transparent text-foreground shadow-none",
|
|
138
|
+
fieldClassName,
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
<div
|
|
142
|
+
ref={placeholderRef}
|
|
143
|
+
className={cn(
|
|
144
|
+
"t-clear-placeholder border-0 bg-transparent text-field-placeholder shadow-none",
|
|
145
|
+
fieldClassName,
|
|
146
|
+
)}
|
|
147
|
+
>
|
|
148
|
+
{placeholder}
|
|
149
|
+
</div>
|
|
150
|
+
<div ref={glowRef} className="t-clear-glow" />
|
|
150
151
|
</div>
|
|
151
|
-
<div ref={glowRef} className="t-clear-glow" aria-hidden="true" />
|
|
152
152
|
{value.length > 0 && !clearing ? (
|
|
153
153
|
<button
|
|
154
154
|
type="button"
|
|
@@ -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 };
|
package/tokens/README.md
CHANGED
|
@@ -15,7 +15,7 @@ Design tokens and shared Tailwind v4 CSS for the vvedantb web apps. The CSS and
|
|
|
15
15
|
| `@vvedantb/ui/tokens/css/tabs-pills.css` | `src/css/tabs-pills.css` | `--tabs-*` colour variables, `.t-tabs--segmented`, `.t-tabs--pills` and pill hover / active fills. |
|
|
16
16
|
| `@vvedantb/ui/tokens/css/utilities.css` | `src/css/utilities.css` | `@utility hit-target` and `@utility reveal-on-hover`. `rounded-surface` moved to `tokens.css` in 0.7.1. |
|
|
17
17
|
| `@vvedantb/ui/tokens/css/scrollbar-thin.css` | `src/css/scrollbar-thin.css` | `.scrollbar-thin` (`@layer utilities`, always emitted). Imported by `tokens.css` from 0.15.0. |
|
|
18
|
-
| `@vvedantb/ui/tokens/css/clear-input.css` | `src/css/clear-input.css` | `ClearInput` layers (`.t-clear*`) and the `--clear-*` / `--glow-*` timings. Imported by `tokens.css`. |
|
|
18
|
+
| `@vvedantb/ui/tokens/css/clear-input.css` | `src/css/clear-input.css` | `ClearInput` layers (`.t-clear*`) and the `--clear-*` / `--glow-*` timings. Imported by `tokens.css`. From 0.15.1 the layers clip in `.t-clear-clip`, so `.t-clear` keeps the focus ring, and the placeholder layer only shows during `.is-clearing`. |
|
|
19
19
|
| `@vvedantb/ui/tokens/css/motion.css` | `src/css/motion.css` | `.ui-page-enter`, `.ui-enter`, `.ui-enter-up`, `.ui-stagger`, their keyframes and reduced-motion rules, and the `html[data-page-motion="off"]` overrides for `PageMotionProvider`. Imported by `tokens.css`. |
|
|
20
20
|
| `@vvedantb/ui/tokens/css/animations.css` | `src/css/animations.css` | `animate-aurora`, `animate-fade-in-up`, `animate-indeterminate` as `@theme` (CSS-first apps). |
|
|
21
21
|
| `@vvedantb/ui/tokens/css/compat-shadcn-rgb.css`, `@vvedantb/ui/tokens/compat/shadcn-rgb.css` | `src/css/compat-shadcn-rgb.css` | Replaces `tokens.css` for apps with shadcn-style `R G B` channel variables. Defines only the UI-only `--color-*` names as `rgb(var(--x) / alpha)` references, `--radius-field`, `--scrollbar`, `--field-radius`, the same three radius utilities as `tokens.css` and `.glass-panel-strong` (solid `--popover` from 0.15.0). Import it **or** `tokens.css`, never both. |
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*
|
|
2
2
|
* @vvedantb/ui/tokens/css/clear-input.css: layers for @vvedantb/ui
|
|
3
|
-
* ClearInput (`.t-clear`, mirror, placeholder and glow), the transitions.dev
|
|
3
|
+
* ClearInput (`.t-clear`, clip, mirror, placeholder and glow), the transitions.dev
|
|
4
4
|
* input clear with dissolve. tokens.css imports it.
|
|
5
5
|
*
|
|
6
6
|
* clearInputDissolve reads the variables below from `html`. Set any of them
|
|
@@ -27,39 +27,50 @@
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
@layer components {
|
|
30
|
+
/* No overflow clip on .t-clear: it would cut the input's focus ring.
|
|
31
|
+
.t-clear-clip clips the layers to the field instead. It never moves, so
|
|
32
|
+
the mirror and placeholder can fly in and out under it. Its radius
|
|
33
|
+
follows .t-clear; put a rounded-* class on wrapperClassName to match an
|
|
34
|
+
input with another radius. */
|
|
30
35
|
.t-clear {
|
|
31
36
|
position: relative;
|
|
32
|
-
|
|
37
|
+
border-radius: var(--radius-field, 0);
|
|
33
38
|
}
|
|
39
|
+
.t-clear-clip,
|
|
34
40
|
.t-clear-mirror,
|
|
35
|
-
.t-clear-placeholder
|
|
41
|
+
.t-clear-placeholder,
|
|
42
|
+
.t-clear-glow {
|
|
36
43
|
position: absolute;
|
|
37
44
|
inset: 0;
|
|
38
|
-
display: flex;
|
|
39
|
-
align-items: center;
|
|
40
45
|
pointer-events: none;
|
|
41
|
-
white-space: nowrap;
|
|
42
46
|
overflow: hidden;
|
|
43
|
-
z-index: 2;
|
|
44
47
|
}
|
|
45
|
-
.t-clear-
|
|
48
|
+
.t-clear-clip {
|
|
49
|
+
border-radius: inherit;
|
|
50
|
+
}
|
|
51
|
+
.t-clear-mirror,
|
|
52
|
+
.t-clear-placeholder {
|
|
53
|
+
display: flex;
|
|
54
|
+
align-items: center;
|
|
55
|
+
white-space: nowrap;
|
|
46
56
|
opacity: 0;
|
|
57
|
+
z-index: 2;
|
|
47
58
|
}
|
|
59
|
+
/* The input's own placeholder shows at rest. The layer only shows while
|
|
60
|
+
clearInputDissolve drops it in, and the native one hides meanwhile. */
|
|
48
61
|
.t-clear.has-value .t-clear-mirror,
|
|
49
|
-
.t-clear.is-clearing .t-clear-mirror
|
|
62
|
+
.t-clear.is-clearing .t-clear-mirror,
|
|
63
|
+
.t-clear.is-clearing .t-clear-placeholder {
|
|
50
64
|
opacity: 1;
|
|
51
65
|
}
|
|
66
|
+
.t-clear.is-clearing > input::placeholder {
|
|
67
|
+
opacity: 0;
|
|
68
|
+
}
|
|
52
69
|
.t-clear.has-value > input,
|
|
53
70
|
.t-clear.is-clearing > input {
|
|
54
71
|
-webkit-text-fill-color: transparent;
|
|
55
72
|
}
|
|
56
|
-
.t-clear.has-value .t-clear-placeholder {
|
|
57
|
-
opacity: 0;
|
|
58
|
-
}
|
|
59
73
|
.t-clear-glow {
|
|
60
|
-
position: absolute;
|
|
61
|
-
inset: 0;
|
|
62
|
-
pointer-events: none;
|
|
63
74
|
opacity: 0;
|
|
64
75
|
z-index: 3;
|
|
65
76
|
mix-blend-mode: multiply;
|