@vvedantb/ui 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/package.json +124 -4
- package/src/ai-elements/artifact.tsx +99 -0
- package/src/ai-elements/attachments.tsx +366 -0
- package/src/ai-elements/chain-of-thought.tsx +212 -0
- package/src/ai-elements/code-block.tsx +478 -0
- package/src/ai-elements/conversation.tsx +157 -0
- package/src/ai-elements/image.tsx +25 -0
- package/src/ai-elements/inline-citation.tsx +252 -0
- package/src/ai-elements/message.tsx +269 -0
- package/src/ai-elements/model-selector.tsx +211 -0
- package/src/ai-elements/prompt-input.tsx +1018 -0
- package/src/ai-elements/queue.tsx +251 -0
- package/src/ai-elements/reasoning.tsx +178 -0
- package/src/ai-elements/shimmer.tsx +56 -0
- package/src/ai-elements/sources.tsx +77 -0
- package/src/ai-elements/speech-input.tsx +290 -0
- package/src/ai-elements/suggestion.tsx +191 -0
- package/src/css.d.ts +2 -0
- package/src/index.ts +96 -0
- package/src/motion/presets.ts +35 -0
- package/src/motion/presets.vibot.ts +105 -0
- package/src/ui/_menu-classes.ts +30 -0
- package/src/ui/_menu-classes.vibot.ts +33 -0
- package/src/ui/accordion.tsx +81 -0
- package/src/ui/accordion.verve.tsx +56 -0
- package/src/ui/badge.tsx +34 -0
- package/src/ui/breadcrumb.tsx +83 -0
- package/src/ui/button-group.tsx +84 -0
- package/src/ui/button.tsx +57 -0
- package/src/ui/card.tsx +27 -0
- package/src/ui/carousel.tsx +211 -0
- package/src/ui/checkbox.tsx +27 -0
- package/src/ui/clear-input.tsx +170 -0
- package/src/ui/clear-input.vibot.tsx +76 -0
- package/src/ui/clearInputDissolve.ts +214 -0
- package/src/ui/collapsible.tsx +7 -0
- package/src/ui/command.tsx +148 -0
- package/src/ui/context-menu.tsx +173 -0
- package/src/ui/dialog.tsx +149 -0
- package/src/ui/dropdown-menu.tsx +178 -0
- package/src/ui/hover-card.tsx +31 -0
- package/src/ui/input-group.tsx +162 -0
- package/src/ui/input-group.verve.tsx +150 -0
- package/src/ui/input.tsx +21 -0
- package/src/ui/kbd.tsx +18 -0
- package/src/ui/label.tsx +20 -0
- package/src/ui/labeled-switch-row.tsx +53 -0
- package/src/ui/modalTransition.ts +85 -0
- package/src/ui/pagination.tsx +47 -0
- package/src/ui/popover.tsx +33 -0
- package/src/ui/progress.tsx +25 -0
- package/src/ui/radix-select.tsx +202 -0
- package/src/ui/scroll-area.tsx +55 -0
- package/src/ui/scroll-area.verve.tsx +43 -0
- package/src/ui/search-input.tsx +29 -0
- package/src/ui/select.tsx +163 -0
- package/src/ui/separator.tsx +28 -0
- package/src/ui/sheet.tsx +87 -0
- package/src/ui/skeleton.tsx +18 -0
- package/src/ui/sonner.css +16 -0
- package/src/ui/sonner.tsx +47 -0
- package/src/ui/spinner.tsx +27 -0
- package/src/ui/surface-classes.ts +55 -0
- package/src/ui/switch.tsx +26 -0
- package/src/ui/table.tsx +123 -0
- package/src/ui/tabs.tsx +140 -0
- package/src/ui/tabsSliding.ts +56 -0
- package/src/ui/textarea.tsx +21 -0
- package/src/ui/time-picker.tsx +170 -0
- package/src/ui/tooltip.tsx +25 -0
- package/src/ui/visually-hidden.tsx +3 -0
- package/src/utils/cn.ts +16 -0
- package/src/utils/ref.ts +10 -0
- package/README.md +0 -3
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import useEmblaCarousel, {
|
|
4
|
+
type UseEmblaCarouselType,
|
|
5
|
+
} from "embla-carousel-react";
|
|
6
|
+
import { IconChevronLeft, IconChevronRight } from "@tabler/icons-react";
|
|
7
|
+
import { cn } from "../utils/cn";
|
|
8
|
+
import { Button } from "./button";
|
|
9
|
+
type CarouselApi = UseEmblaCarouselType[1];
|
|
10
|
+
type UseCarouselParameters = Parameters<typeof useEmblaCarousel>;
|
|
11
|
+
type CarouselOptions = UseCarouselParameters[0];
|
|
12
|
+
type CarouselPlugin = UseCarouselParameters[1];
|
|
13
|
+
type CarouselProps = {
|
|
14
|
+
opts?: CarouselOptions;
|
|
15
|
+
plugins?: CarouselPlugin;
|
|
16
|
+
orientation?: "horizontal" | "vertical";
|
|
17
|
+
setApi?: (api: CarouselApi) => void;
|
|
18
|
+
};
|
|
19
|
+
type CarouselContextProps = {
|
|
20
|
+
carouselRef: ReturnType<typeof useEmblaCarousel>[0];
|
|
21
|
+
api: ReturnType<typeof useEmblaCarousel>[1];
|
|
22
|
+
scrollPrev: () => void;
|
|
23
|
+
scrollNext: () => void;
|
|
24
|
+
canScrollPrev: boolean;
|
|
25
|
+
canScrollNext: boolean;
|
|
26
|
+
} & CarouselProps;
|
|
27
|
+
const CarouselContext = React.createContext<CarouselContextProps | null>(null);
|
|
28
|
+
function useCarousel() {
|
|
29
|
+
const context = React.useContext(CarouselContext);
|
|
30
|
+
if (!context) {
|
|
31
|
+
throw new Error("useCarousel must be used within a <Carousel />");
|
|
32
|
+
}
|
|
33
|
+
return context;
|
|
34
|
+
}
|
|
35
|
+
function Carousel({
|
|
36
|
+
orientation = "horizontal",
|
|
37
|
+
opts,
|
|
38
|
+
setApi,
|
|
39
|
+
plugins,
|
|
40
|
+
className,
|
|
41
|
+
children,
|
|
42
|
+
...props
|
|
43
|
+
}: React.ComponentProps<"div"> & CarouselProps) {
|
|
44
|
+
const [carouselRef, api] = useEmblaCarousel(
|
|
45
|
+
{
|
|
46
|
+
...opts,
|
|
47
|
+
axis: orientation === "horizontal" ? "x" : "y",
|
|
48
|
+
},
|
|
49
|
+
plugins,
|
|
50
|
+
);
|
|
51
|
+
const [canScrollPrev, setCanScrollPrev] = React.useState(false);
|
|
52
|
+
const [canScrollNext, setCanScrollNext] = React.useState(false);
|
|
53
|
+
const onSelect = (api: CarouselApi) => {
|
|
54
|
+
if (!api) return;
|
|
55
|
+
setCanScrollPrev(api.canScrollPrev());
|
|
56
|
+
setCanScrollNext(api.canScrollNext());
|
|
57
|
+
};
|
|
58
|
+
const scrollPrev = () => {
|
|
59
|
+
api?.scrollPrev();
|
|
60
|
+
};
|
|
61
|
+
const scrollNext = () => {
|
|
62
|
+
api?.scrollNext();
|
|
63
|
+
};
|
|
64
|
+
const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
|
|
65
|
+
if (event.key === "ArrowLeft") {
|
|
66
|
+
event.preventDefault();
|
|
67
|
+
scrollPrev();
|
|
68
|
+
} else if (event.key === "ArrowRight") {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
scrollNext();
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
React.useEffect(() => {
|
|
74
|
+
if (!api || !setApi) return;
|
|
75
|
+
setApi(api);
|
|
76
|
+
}, [api, setApi]);
|
|
77
|
+
React.useEffect(() => {
|
|
78
|
+
if (!api) return;
|
|
79
|
+
onSelect(api);
|
|
80
|
+
api.on("reInit", onSelect);
|
|
81
|
+
api.on("select", onSelect);
|
|
82
|
+
return () => {
|
|
83
|
+
api?.off("select", onSelect);
|
|
84
|
+
};
|
|
85
|
+
}, [api, onSelect]);
|
|
86
|
+
return (
|
|
87
|
+
<CarouselContext.Provider
|
|
88
|
+
value={{
|
|
89
|
+
carouselRef,
|
|
90
|
+
api: api,
|
|
91
|
+
opts,
|
|
92
|
+
orientation:
|
|
93
|
+
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
|
|
94
|
+
scrollPrev,
|
|
95
|
+
scrollNext,
|
|
96
|
+
canScrollPrev,
|
|
97
|
+
canScrollNext,
|
|
98
|
+
}}
|
|
99
|
+
>
|
|
100
|
+
<div
|
|
101
|
+
onKeyDownCapture={handleKeyDown}
|
|
102
|
+
className={cn("relative", className)}
|
|
103
|
+
role="region"
|
|
104
|
+
aria-roledescription="carousel"
|
|
105
|
+
data-slot="carousel"
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
{children}
|
|
109
|
+
</div>
|
|
110
|
+
</CarouselContext.Provider>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
function CarouselContent({ className, ...props }: React.ComponentProps<"div">) {
|
|
114
|
+
const { carouselRef, orientation } = useCarousel();
|
|
115
|
+
return (
|
|
116
|
+
<div
|
|
117
|
+
ref={carouselRef}
|
|
118
|
+
className="overflow-hidden"
|
|
119
|
+
data-slot="carousel-content"
|
|
120
|
+
>
|
|
121
|
+
<div
|
|
122
|
+
className={cn(
|
|
123
|
+
"flex",
|
|
124
|
+
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
|
|
125
|
+
className,
|
|
126
|
+
)}
|
|
127
|
+
{...props}
|
|
128
|
+
/>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
function CarouselItem({ className, ...props }: React.ComponentProps<"div">) {
|
|
133
|
+
const { orientation } = useCarousel();
|
|
134
|
+
return (
|
|
135
|
+
<div
|
|
136
|
+
role="group"
|
|
137
|
+
aria-roledescription="slide"
|
|
138
|
+
data-slot="carousel-item"
|
|
139
|
+
className={cn(
|
|
140
|
+
"min-w-0 shrink-0 grow-0 basis-full",
|
|
141
|
+
orientation === "horizontal" ? "pl-4" : "pt-4",
|
|
142
|
+
className,
|
|
143
|
+
)}
|
|
144
|
+
{...props}
|
|
145
|
+
/>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
function CarouselPrevious({
|
|
149
|
+
className,
|
|
150
|
+
variant = "outline",
|
|
151
|
+
size = "icon",
|
|
152
|
+
...props
|
|
153
|
+
}: React.ComponentProps<typeof Button>) {
|
|
154
|
+
const { orientation, scrollPrev, canScrollPrev } = useCarousel();
|
|
155
|
+
return (
|
|
156
|
+
<Button
|
|
157
|
+
data-slot="carousel-previous"
|
|
158
|
+
variant={variant}
|
|
159
|
+
size={size}
|
|
160
|
+
className={cn(
|
|
161
|
+
"absolute size-8 rounded-full",
|
|
162
|
+
orientation === "horizontal"
|
|
163
|
+
? "top-1/2 -left-12 -translate-y-1/2"
|
|
164
|
+
: "-top-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
165
|
+
className,
|
|
166
|
+
)}
|
|
167
|
+
disabled={!canScrollPrev}
|
|
168
|
+
onClick={scrollPrev}
|
|
169
|
+
{...props}
|
|
170
|
+
>
|
|
171
|
+
<IconChevronLeft />
|
|
172
|
+
<span className="sr-only">Previous slide</span>
|
|
173
|
+
</Button>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
function CarouselNext({
|
|
177
|
+
className,
|
|
178
|
+
variant = "outline",
|
|
179
|
+
size = "icon",
|
|
180
|
+
...props
|
|
181
|
+
}: React.ComponentProps<typeof Button>) {
|
|
182
|
+
const { orientation, scrollNext, canScrollNext } = useCarousel();
|
|
183
|
+
return (
|
|
184
|
+
<Button
|
|
185
|
+
data-slot="carousel-next"
|
|
186
|
+
variant={variant}
|
|
187
|
+
size={size}
|
|
188
|
+
className={cn(
|
|
189
|
+
"absolute size-8 rounded-full",
|
|
190
|
+
orientation === "horizontal"
|
|
191
|
+
? "top-1/2 -right-12 -translate-y-1/2"
|
|
192
|
+
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90",
|
|
193
|
+
className,
|
|
194
|
+
)}
|
|
195
|
+
disabled={!canScrollNext}
|
|
196
|
+
onClick={scrollNext}
|
|
197
|
+
{...props}
|
|
198
|
+
>
|
|
199
|
+
<IconChevronRight />
|
|
200
|
+
<span className="sr-only">Next slide</span>
|
|
201
|
+
</Button>
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
export {
|
|
205
|
+
type CarouselApi,
|
|
206
|
+
Carousel,
|
|
207
|
+
CarouselContent,
|
|
208
|
+
CarouselItem,
|
|
209
|
+
CarouselPrevious,
|
|
210
|
+
CarouselNext,
|
|
211
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
|
|
3
|
+
import { IconCheck } from "@tabler/icons-react";
|
|
4
|
+
import { cn } from "../utils/cn";
|
|
5
|
+
|
|
6
|
+
const Checkbox = React.forwardRef<
|
|
7
|
+
React.ComponentRef<typeof CheckboxPrimitive.Root>,
|
|
8
|
+
React.ComponentPropsWithoutRef<typeof CheckboxPrimitive.Root>
|
|
9
|
+
>(({ className, ...props }, ref) => (
|
|
10
|
+
<CheckboxPrimitive.Root
|
|
11
|
+
ref={ref}
|
|
12
|
+
className={cn(
|
|
13
|
+
"peer relative h-4 w-4 shrink-0 rounded-md border border-border bg-field-background transition-[background-color,border-color] duration-200 ease-smooth focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-accent data-[state=checked]:bg-accent data-[state=checked]:text-accent-foreground before:absolute before:inset-[-12px] before:content-['']",
|
|
14
|
+
className,
|
|
15
|
+
)}
|
|
16
|
+
{...props}
|
|
17
|
+
>
|
|
18
|
+
<CheckboxPrimitive.Indicator
|
|
19
|
+
className={cn("flex items-center justify-center text-current")}
|
|
20
|
+
>
|
|
21
|
+
<IconCheck className="h-3.5 w-3.5" />
|
|
22
|
+
</CheckboxPrimitive.Indicator>
|
|
23
|
+
</CheckboxPrimitive.Root>
|
|
24
|
+
));
|
|
25
|
+
Checkbox.displayName = CheckboxPrimitive.Root.displayName;
|
|
26
|
+
|
|
27
|
+
export { Checkbox };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { IconX } from "@tabler/icons-react";
|
|
5
|
+
import { cn } from "../utils/cn";
|
|
6
|
+
import { runClearInputDissolve } from "./clearInputDissolve";
|
|
7
|
+
|
|
8
|
+
export interface ClearInputProps extends Omit<
|
|
9
|
+
React.ComponentProps<"input">,
|
|
10
|
+
"value" | "onChange"
|
|
11
|
+
> {
|
|
12
|
+
value: string;
|
|
13
|
+
onChange: (value: string) => void;
|
|
14
|
+
wrapperClassName?: string;
|
|
15
|
+
/** Icon or adornment positioned in the field's left gutter. */
|
|
16
|
+
leading?: React.ReactNode;
|
|
17
|
+
clearLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
21
|
+
(
|
|
22
|
+
{
|
|
23
|
+
value,
|
|
24
|
+
onChange,
|
|
25
|
+
wrapperClassName,
|
|
26
|
+
leading,
|
|
27
|
+
clearLabel = "Clear",
|
|
28
|
+
className,
|
|
29
|
+
placeholder = "",
|
|
30
|
+
...props
|
|
31
|
+
},
|
|
32
|
+
ref,
|
|
33
|
+
) => {
|
|
34
|
+
const wrapRef = React.useRef<HTMLDivElement>(null);
|
|
35
|
+
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
36
|
+
const mirrorRef = React.useRef<HTMLDivElement>(null);
|
|
37
|
+
const placeholderRef = React.useRef<HTMLDivElement>(null);
|
|
38
|
+
const glowRef = React.useRef<HTMLDivElement>(null);
|
|
39
|
+
const clearingRef = React.useRef(false);
|
|
40
|
+
const [clearing, setClearing] = React.useState(false);
|
|
41
|
+
|
|
42
|
+
function mergedRef(node: HTMLInputElement | null) {
|
|
43
|
+
inputRef.current = node;
|
|
44
|
+
if (typeof ref === "function") {
|
|
45
|
+
ref(node);
|
|
46
|
+
} else if (ref) {
|
|
47
|
+
ref.current = node;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const hasValue = value.length > 0 || clearing;
|
|
52
|
+
|
|
53
|
+
React.useEffect(() => {
|
|
54
|
+
const wrap = wrapRef.current;
|
|
55
|
+
const mirror = mirrorRef.current;
|
|
56
|
+
if (!wrap || !mirror || clearingRef.current) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
wrap.classList.toggle("has-value", value.length > 0);
|
|
60
|
+
if (value.length > 0) {
|
|
61
|
+
mirror.textContent = value.replace(/ /g, "\u00a0");
|
|
62
|
+
} else if (!clearingRef.current) {
|
|
63
|
+
mirror.textContent = "";
|
|
64
|
+
}
|
|
65
|
+
}, [value]);
|
|
66
|
+
|
|
67
|
+
const preventFocusSteal = (event: React.SyntheticEvent) => {
|
|
68
|
+
if (document.activeElement === inputRef.current) {
|
|
69
|
+
event.preventDefault();
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const handleClear = () => {
|
|
74
|
+
const wrap = wrapRef.current;
|
|
75
|
+
const input = inputRef.current;
|
|
76
|
+
const mirror = mirrorRef.current;
|
|
77
|
+
const placeholderLayer = placeholderRef.current;
|
|
78
|
+
const glow = glowRef.current;
|
|
79
|
+
if (
|
|
80
|
+
!wrap ||
|
|
81
|
+
!input ||
|
|
82
|
+
!mirror ||
|
|
83
|
+
!placeholderLayer ||
|
|
84
|
+
!glow ||
|
|
85
|
+
clearingRef.current ||
|
|
86
|
+
value.length === 0
|
|
87
|
+
) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
clearingRef.current = true;
|
|
92
|
+
setClearing(true);
|
|
93
|
+
|
|
94
|
+
runClearInputDissolve(
|
|
95
|
+
{ wrap, input, mirror, placeholderLayer, glow },
|
|
96
|
+
() => {
|
|
97
|
+
onChange("");
|
|
98
|
+
},
|
|
99
|
+
() => {
|
|
100
|
+
clearingRef.current = false;
|
|
101
|
+
setClearing(false);
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const fieldClassName = cn(hasValue && "pr-9", className);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<div
|
|
110
|
+
ref={wrapRef}
|
|
111
|
+
className={cn(
|
|
112
|
+
"t-clear",
|
|
113
|
+
hasValue && "has-value",
|
|
114
|
+
clearing && "is-clearing",
|
|
115
|
+
wrapperClassName,
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
{leading}
|
|
119
|
+
<input
|
|
120
|
+
ref={mergedRef}
|
|
121
|
+
type="text"
|
|
122
|
+
value={value}
|
|
123
|
+
onChange={(event) => {
|
|
124
|
+
onChange(event.target.value);
|
|
125
|
+
}}
|
|
126
|
+
placeholder={placeholder}
|
|
127
|
+
className={cn(
|
|
128
|
+
"flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-base sm:text-sm transition-[border-color,box-shadow,background-color] duration-200 ease-smooth placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
129
|
+
fieldClassName,
|
|
130
|
+
)}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
<div
|
|
134
|
+
ref={mirrorRef}
|
|
135
|
+
className={cn(
|
|
136
|
+
"t-clear-mirror border-0 bg-transparent text-foreground shadow-none",
|
|
137
|
+
fieldClassName,
|
|
138
|
+
)}
|
|
139
|
+
aria-hidden="true"
|
|
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
|
+
aria-hidden="true"
|
|
148
|
+
>
|
|
149
|
+
{placeholder}
|
|
150
|
+
</div>
|
|
151
|
+
<div ref={glowRef} className="t-clear-glow" aria-hidden="true" />
|
|
152
|
+
{value.length > 0 && !clearing ? (
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
className="t-clear-btn absolute right-2 top-1/2 z-4 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted-foreground transition-[background-color,color,transform] duration-100 ease-out active:scale-[0.92] hover:bg-surface-tertiary/50 hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring"
|
|
156
|
+
aria-label={clearLabel}
|
|
157
|
+
onPointerDown={preventFocusSteal}
|
|
158
|
+
onMouseDown={preventFocusSteal}
|
|
159
|
+
onClick={handleClear}
|
|
160
|
+
>
|
|
161
|
+
<IconX size={14} stroke={1.75} />
|
|
162
|
+
</button>
|
|
163
|
+
) : null}
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
},
|
|
167
|
+
);
|
|
168
|
+
ClearInput.displayName = "ClearInput";
|
|
169
|
+
|
|
170
|
+
export { ClearInput };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { IconX } from "@tabler/icons-react";
|
|
4
|
+
import { cn } from "../utils/cn";
|
|
5
|
+
export interface ClearInputProps
|
|
6
|
+
extends Omit<React.ComponentProps<"input">, "value" | "onChange"> {
|
|
7
|
+
value: string;
|
|
8
|
+
onChange: (value: string) => void;
|
|
9
|
+
wrapperClassName?: string;
|
|
10
|
+
/** Icon or adornment positioned in the field's left gutter. */
|
|
11
|
+
leading?: React.ReactNode;
|
|
12
|
+
clearLabel?: string;
|
|
13
|
+
}
|
|
14
|
+
const ClearInput = React.forwardRef<HTMLInputElement, ClearInputProps>(
|
|
15
|
+
(
|
|
16
|
+
{
|
|
17
|
+
value,
|
|
18
|
+
onChange,
|
|
19
|
+
wrapperClassName,
|
|
20
|
+
leading,
|
|
21
|
+
clearLabel = "Clear",
|
|
22
|
+
className,
|
|
23
|
+
placeholder,
|
|
24
|
+
...props
|
|
25
|
+
},
|
|
26
|
+
ref,
|
|
27
|
+
) => {
|
|
28
|
+
const inputRef = React.useRef<HTMLInputElement | null>(null);
|
|
29
|
+
const mergedRef = (node: HTMLInputElement | null) => {
|
|
30
|
+
inputRef.current = node;
|
|
31
|
+
if (typeof ref === "function") {
|
|
32
|
+
ref(node);
|
|
33
|
+
} else if (ref) {
|
|
34
|
+
ref.current = node;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
const hasValue = value.length > 0;
|
|
38
|
+
return (
|
|
39
|
+
<div className={cn("relative", wrapperClassName)}>
|
|
40
|
+
{leading}
|
|
41
|
+
<input
|
|
42
|
+
ref={mergedRef}
|
|
43
|
+
type="text"
|
|
44
|
+
value={value}
|
|
45
|
+
onChange={(event) => {
|
|
46
|
+
onChange(event.target.value);
|
|
47
|
+
}}
|
|
48
|
+
placeholder={placeholder}
|
|
49
|
+
className={cn(
|
|
50
|
+
"flex h-10 w-full rounded-field border border-border bg-field-background px-3.5 py-2 text-sm transition-[border-color,background-color] duration-200 ease-smooth placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
|
|
51
|
+
"pr-9",
|
|
52
|
+
className,
|
|
53
|
+
)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
<button
|
|
57
|
+
type="button"
|
|
58
|
+
tabIndex={hasValue ? 0 : -1}
|
|
59
|
+
className={cn(
|
|
60
|
+
"absolute right-2 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-muted transition-[opacity,background-color,color,transform] duration-150 ease-out active:duration-100 active:ease-out active:scale-[0.97] hover:bg-surface-tertiary hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-focus-ring before:absolute before:inset-[-8px] before:content-['']",
|
|
61
|
+
hasValue ? "opacity-100" : "pointer-events-none opacity-0",
|
|
62
|
+
)}
|
|
63
|
+
aria-hidden={!hasValue}
|
|
64
|
+
aria-label={clearLabel}
|
|
65
|
+
onClick={() => {
|
|
66
|
+
onChange("");
|
|
67
|
+
inputRef.current?.focus({ preventScroll: true });
|
|
68
|
+
}}
|
|
69
|
+
>
|
|
70
|
+
<IconX size={14} stroke={1.75} />
|
|
71
|
+
</button>
|
|
72
|
+
</div>
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
export { ClearInput };
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/** transitions.dev input clear with dissolve — see 13-input-clear-dissolve.md */
|
|
2
|
+
|
|
3
|
+
function readCssNumber(
|
|
4
|
+
root: HTMLElement,
|
|
5
|
+
name: string,
|
|
6
|
+
fallback: number,
|
|
7
|
+
): number {
|
|
8
|
+
const raw = getComputedStyle(root).getPropertyValue(name);
|
|
9
|
+
const parsed = parseFloat(raw);
|
|
10
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function readCssEase(root: HTMLElement, name: string): (t: number) => number {
|
|
14
|
+
const raw = getComputedStyle(root).getPropertyValue(name);
|
|
15
|
+
const match = raw.match(
|
|
16
|
+
/cubic-bezier\(([-\d.]+),([-\d.]+),([-\d.]+),([-\d.]+)\)/,
|
|
17
|
+
);
|
|
18
|
+
if (!match) {
|
|
19
|
+
return (t) => t;
|
|
20
|
+
}
|
|
21
|
+
const x1 = parseFloat(match[1] ?? "0");
|
|
22
|
+
const y1 = parseFloat(match[2] ?? "0");
|
|
23
|
+
const x2 = parseFloat(match[3] ?? "0");
|
|
24
|
+
const y2 = parseFloat(match[4] ?? "0");
|
|
25
|
+
const cx = 3 * x1;
|
|
26
|
+
const bx = 3 * (x2 - x1) - cx;
|
|
27
|
+
const ax = 1 - cx - bx;
|
|
28
|
+
const cy = 3 * y1;
|
|
29
|
+
const by = 3 * (y2 - y1) - cy;
|
|
30
|
+
const ay = 1 - cy - by;
|
|
31
|
+
return (t) => {
|
|
32
|
+
if (t <= 0) {
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
if (t >= 1) {
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
let sample = t;
|
|
39
|
+
for (let i = 0; i < 8; i += 1) {
|
|
40
|
+
const dx = ((ax * sample + bx) * sample + cx) * sample - t;
|
|
41
|
+
const derivative = (3 * ax * sample + 2 * bx) * sample + cx;
|
|
42
|
+
if (Math.abs(dx) < 1e-6 || derivative === 0) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
sample -= dx / derivative;
|
|
46
|
+
}
|
|
47
|
+
return ((ay * sample + by) * sample + cy) * sample;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isDarkTheme(root: HTMLElement): boolean {
|
|
52
|
+
return (
|
|
53
|
+
root.classList.contains("dark") ||
|
|
54
|
+
root.getAttribute("data-theme") === "dark"
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function measureCanvas(): CanvasRenderingContext2D | null {
|
|
59
|
+
const canvas = document.createElement("canvas");
|
|
60
|
+
return canvas.getContext("2d");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let sharedMeasureCanvas: CanvasRenderingContext2D | null = null;
|
|
64
|
+
|
|
65
|
+
function getMeasureCanvas(): CanvasRenderingContext2D | null {
|
|
66
|
+
if (sharedMeasureCanvas === null) {
|
|
67
|
+
sharedMeasureCanvas = measureCanvas();
|
|
68
|
+
}
|
|
69
|
+
return sharedMeasureCanvas;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface ClearDissolveElements {
|
|
73
|
+
wrap: HTMLElement;
|
|
74
|
+
input: HTMLInputElement;
|
|
75
|
+
mirror: HTMLElement;
|
|
76
|
+
placeholderLayer: HTMLElement;
|
|
77
|
+
glow: HTMLElement;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function buildGlowBackground(
|
|
81
|
+
wrap: HTMLElement,
|
|
82
|
+
input: HTMLInputElement,
|
|
83
|
+
text: string,
|
|
84
|
+
root: HTMLElement,
|
|
85
|
+
): string {
|
|
86
|
+
const canvas = getMeasureCanvas();
|
|
87
|
+
if (canvas === null) {
|
|
88
|
+
return "";
|
|
89
|
+
}
|
|
90
|
+
canvas.font = getComputedStyle(input).font;
|
|
91
|
+
const dark = isDarkTheme(root);
|
|
92
|
+
const rgb = dark ? "255,255,255" : "0,0,0";
|
|
93
|
+
const width = wrap.clientWidth || 280;
|
|
94
|
+
const padLeft = parseFloat(getComputedStyle(input).paddingLeft) || 12;
|
|
95
|
+
const spread = readCssNumber(root, "--glow-spread", 1.5);
|
|
96
|
+
const layers: string[] = [];
|
|
97
|
+
let x = 0;
|
|
98
|
+
for (const segment of text.split(/(\s+)/)) {
|
|
99
|
+
const segmentWidth = canvas.measureText(segment).width;
|
|
100
|
+
if (segment.trim()) {
|
|
101
|
+
const centerX = padLeft + x + segmentWidth / 2;
|
|
102
|
+
const halfWidth = Math.max(segmentWidth * 0.45, 8) * spread;
|
|
103
|
+
const streaks: Array<[number, number, number, number]> = [
|
|
104
|
+
[0, 0.8, 7, 0.22],
|
|
105
|
+
[halfWidth * 0.45, 0.55, 8, 0.18],
|
|
106
|
+
[-halfWidth * 0.4, 0.65, 6, 0.16],
|
|
107
|
+
[halfWidth * 0.15, 0.9, 5, 0.14],
|
|
108
|
+
];
|
|
109
|
+
for (const [dx, rwm, rh, alpha] of streaks) {
|
|
110
|
+
const leftPercent = (((centerX + dx) / width) * 100).toFixed(2);
|
|
111
|
+
layers.push(
|
|
112
|
+
`radial-gradient(ellipse ${Math.max(halfWidth * rwm, 2).toFixed(1)}px ${rh}px at ${leftPercent}% 100%, rgba(${rgb},${alpha}), transparent)`,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
x += segmentWidth;
|
|
117
|
+
}
|
|
118
|
+
return layers.join(", ");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resetLayerStyles(
|
|
122
|
+
mirror: HTMLElement,
|
|
123
|
+
placeholderLayer: HTMLElement,
|
|
124
|
+
): void {
|
|
125
|
+
mirror.style.cssText = "";
|
|
126
|
+
placeholderLayer.style.cssText = "";
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function runClearInputDissolve(
|
|
130
|
+
elements: ClearDissolveElements,
|
|
131
|
+
onValueClear: () => void,
|
|
132
|
+
onDone: () => void,
|
|
133
|
+
): void {
|
|
134
|
+
const { wrap, input, mirror, placeholderLayer, glow } = elements;
|
|
135
|
+
const root = document.documentElement;
|
|
136
|
+
const text = input.value;
|
|
137
|
+
if (text.length === 0) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const keepFocus = document.activeElement === input;
|
|
142
|
+
mirror.textContent = text.replace(/ /g, "\u00a0");
|
|
143
|
+
|
|
144
|
+
const total = readCssNumber(root, "--clear-dur", 1000);
|
|
145
|
+
const outDur = readCssNumber(root, "--clear-out-dur", 400);
|
|
146
|
+
const inDur = readCssNumber(root, "--clear-in-dur", 400);
|
|
147
|
+
const outFly = readCssNumber(root, "--clear-out-fly", 12);
|
|
148
|
+
const inFly = readCssNumber(root, "--clear-in-fly", 12);
|
|
149
|
+
const delay = readCssNumber(root, "--glow-delay", 50);
|
|
150
|
+
const peakAt = readCssNumber(root, "--glow-peak-at", 0.15);
|
|
151
|
+
const glowOpacity = readCssNumber(root, "--glow-opacity", 0.85);
|
|
152
|
+
const easeOut = readCssEase(root, "--clear-out-ease");
|
|
153
|
+
const easeIn = readCssEase(root, "--clear-in-ease");
|
|
154
|
+
|
|
155
|
+
onValueClear();
|
|
156
|
+
wrap.classList.remove("has-value");
|
|
157
|
+
wrap.classList.add("is-clearing");
|
|
158
|
+
glow.style.background = buildGlowBackground(
|
|
159
|
+
wrap,
|
|
160
|
+
input,
|
|
161
|
+
mirror.textContent,
|
|
162
|
+
root,
|
|
163
|
+
);
|
|
164
|
+
glow.style.opacity = "0";
|
|
165
|
+
placeholderLayer.style.transform = `translateY(-${inFly}px)`;
|
|
166
|
+
placeholderLayer.style.opacity = "0.9";
|
|
167
|
+
placeholderLayer.style.filter = "none";
|
|
168
|
+
mirror.style.filter = "none";
|
|
169
|
+
|
|
170
|
+
const startedAt = performance.now();
|
|
171
|
+
|
|
172
|
+
function tick(now: number): void {
|
|
173
|
+
const elapsed = now - startedAt;
|
|
174
|
+
const outProgress = easeOut(Math.min(1, elapsed / outDur));
|
|
175
|
+
mirror.style.transform = `translateY(${(outProgress * outFly).toFixed(1)}px)`;
|
|
176
|
+
mirror.style.opacity = (1 - outProgress).toFixed(3);
|
|
177
|
+
|
|
178
|
+
const inProgress = easeIn(Math.min(1, elapsed / inDur));
|
|
179
|
+
placeholderLayer.style.transform = `translateY(${(-inFly + inProgress * inFly).toFixed(1)}px)`;
|
|
180
|
+
placeholderLayer.style.opacity = (0.9 + inProgress * 0.1).toFixed(3);
|
|
181
|
+
|
|
182
|
+
let glowStrength = 0;
|
|
183
|
+
if (elapsed > delay) {
|
|
184
|
+
const glowProgress = Math.min(
|
|
185
|
+
1,
|
|
186
|
+
(elapsed - delay) / Math.max(1, total - delay),
|
|
187
|
+
);
|
|
188
|
+
glowStrength =
|
|
189
|
+
glowProgress < peakAt
|
|
190
|
+
? glowProgress / peakAt
|
|
191
|
+
: 1 - (glowProgress - peakAt) / (1 - peakAt);
|
|
192
|
+
}
|
|
193
|
+
glow.style.opacity = (glowStrength * glowOpacity).toFixed(3);
|
|
194
|
+
|
|
195
|
+
if (elapsed < total) {
|
|
196
|
+
requestAnimationFrame(tick);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
wrap.classList.remove("is-clearing");
|
|
201
|
+
resetLayerStyles(mirror, placeholderLayer);
|
|
202
|
+
mirror.textContent = "";
|
|
203
|
+
glow.style.opacity = "0";
|
|
204
|
+
glow.style.background = "";
|
|
205
|
+
onDone();
|
|
206
|
+
if (keepFocus) {
|
|
207
|
+
requestAnimationFrame(() => {
|
|
208
|
+
input.focus({ preventScroll: true });
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
requestAnimationFrame(tick);
|
|
214
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible";
|
|
2
|
+
|
|
3
|
+
const Collapsible = CollapsiblePrimitive.Root;
|
|
4
|
+
const CollapsibleTrigger = CollapsiblePrimitive.CollapsibleTrigger;
|
|
5
|
+
const CollapsibleContent = CollapsiblePrimitive.CollapsibleContent;
|
|
6
|
+
|
|
7
|
+
export { Collapsible, CollapsibleTrigger, CollapsibleContent };
|