@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.
Files changed (75) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +124 -4
  3. package/src/ai-elements/artifact.tsx +99 -0
  4. package/src/ai-elements/attachments.tsx +366 -0
  5. package/src/ai-elements/chain-of-thought.tsx +212 -0
  6. package/src/ai-elements/code-block.tsx +478 -0
  7. package/src/ai-elements/conversation.tsx +157 -0
  8. package/src/ai-elements/image.tsx +25 -0
  9. package/src/ai-elements/inline-citation.tsx +252 -0
  10. package/src/ai-elements/message.tsx +269 -0
  11. package/src/ai-elements/model-selector.tsx +211 -0
  12. package/src/ai-elements/prompt-input.tsx +1018 -0
  13. package/src/ai-elements/queue.tsx +251 -0
  14. package/src/ai-elements/reasoning.tsx +178 -0
  15. package/src/ai-elements/shimmer.tsx +56 -0
  16. package/src/ai-elements/sources.tsx +77 -0
  17. package/src/ai-elements/speech-input.tsx +290 -0
  18. package/src/ai-elements/suggestion.tsx +191 -0
  19. package/src/css.d.ts +2 -0
  20. package/src/index.ts +96 -0
  21. package/src/motion/presets.ts +35 -0
  22. package/src/motion/presets.vibot.ts +105 -0
  23. package/src/ui/_menu-classes.ts +30 -0
  24. package/src/ui/_menu-classes.vibot.ts +33 -0
  25. package/src/ui/accordion.tsx +81 -0
  26. package/src/ui/accordion.verve.tsx +56 -0
  27. package/src/ui/badge.tsx +34 -0
  28. package/src/ui/breadcrumb.tsx +83 -0
  29. package/src/ui/button-group.tsx +84 -0
  30. package/src/ui/button.tsx +57 -0
  31. package/src/ui/card.tsx +27 -0
  32. package/src/ui/carousel.tsx +211 -0
  33. package/src/ui/checkbox.tsx +27 -0
  34. package/src/ui/clear-input.tsx +170 -0
  35. package/src/ui/clear-input.vibot.tsx +76 -0
  36. package/src/ui/clearInputDissolve.ts +214 -0
  37. package/src/ui/collapsible.tsx +7 -0
  38. package/src/ui/command.tsx +148 -0
  39. package/src/ui/context-menu.tsx +173 -0
  40. package/src/ui/dialog.tsx +149 -0
  41. package/src/ui/dropdown-menu.tsx +178 -0
  42. package/src/ui/hover-card.tsx +31 -0
  43. package/src/ui/input-group.tsx +162 -0
  44. package/src/ui/input-group.verve.tsx +150 -0
  45. package/src/ui/input.tsx +21 -0
  46. package/src/ui/kbd.tsx +18 -0
  47. package/src/ui/label.tsx +20 -0
  48. package/src/ui/labeled-switch-row.tsx +53 -0
  49. package/src/ui/modalTransition.ts +85 -0
  50. package/src/ui/pagination.tsx +47 -0
  51. package/src/ui/popover.tsx +33 -0
  52. package/src/ui/progress.tsx +25 -0
  53. package/src/ui/radix-select.tsx +202 -0
  54. package/src/ui/scroll-area.tsx +55 -0
  55. package/src/ui/scroll-area.verve.tsx +43 -0
  56. package/src/ui/search-input.tsx +29 -0
  57. package/src/ui/select.tsx +163 -0
  58. package/src/ui/separator.tsx +28 -0
  59. package/src/ui/sheet.tsx +87 -0
  60. package/src/ui/skeleton.tsx +18 -0
  61. package/src/ui/sonner.css +16 -0
  62. package/src/ui/sonner.tsx +47 -0
  63. package/src/ui/spinner.tsx +27 -0
  64. package/src/ui/surface-classes.ts +55 -0
  65. package/src/ui/switch.tsx +26 -0
  66. package/src/ui/table.tsx +123 -0
  67. package/src/ui/tabs.tsx +140 -0
  68. package/src/ui/tabsSliding.ts +56 -0
  69. package/src/ui/textarea.tsx +21 -0
  70. package/src/ui/time-picker.tsx +170 -0
  71. package/src/ui/tooltip.tsx +25 -0
  72. package/src/ui/visually-hidden.tsx +3 -0
  73. package/src/utils/cn.ts +16 -0
  74. package/src/utils/ref.ts +10 -0
  75. package/README.md +0 -3
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+
6
+ import { cn } from "../utils/cn";
7
+ import { Button } from "./button";
8
+ import { Input } from "./input";
9
+ import { Textarea } from "./textarea";
10
+
11
+ function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
12
+ return (
13
+ <div
14
+ data-slot="input-group"
15
+ role="group"
16
+ className={cn(
17
+ "group/input-group relative flex w-full flex-wrap items-center rounded-field border border-border bg-field-background text-sm transition-[border-color,box-shadow] duration-200 ease-smooth outline-hidden",
18
+ "h-10 min-w-0 has-[>textarea]:h-auto",
19
+ "[&>input]:has-[>[data-align=inline-start]]:pl-2",
20
+ "[&>input]:has-[>[data-align=inline-end]]:pr-2",
21
+ "has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col [&>input]:has-[>[data-align=block-start]]:pb-3",
22
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col [&>input]:has-[>[data-align=block-end]]:pt-3",
23
+ "has-[[data-slot=input-group-control]:focus-visible]:border-focus-border has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-focus-ring",
24
+ "has-[[data-slot][aria-invalid=true]]:border-danger-border has-[[data-slot][aria-invalid=true]]:ring-danger-ring",
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ );
30
+ }
31
+
32
+ const inputGroupAddonVariants = cva(
33
+ "text-muted flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium select-none [&>svg:not([class*='size-'])]:size-4 [&>kbd]:rounded-md group-data-[disabled=true]/input-group:opacity-50",
34
+ {
35
+ variants: {
36
+ align: {
37
+ "inline-start":
38
+ "order-first pl-3 has-[>button]:ml-[-0.45rem] has-[>kbd]:ml-[-0.35rem]",
39
+ "inline-end":
40
+ "order-last pr-3 has-[>button]:mr-[-0.45rem] has-[>kbd]:mr-[-0.35rem]",
41
+ "block-start":
42
+ "order-first w-full justify-start px-3 pt-3 [.border-b]:pb-3 group-has-[>input]/input-group:pt-2.5",
43
+ "block-end":
44
+ "order-last w-full justify-start px-3 pb-3 [.border-t]:pt-3 group-has-[>input]/input-group:pb-2.5",
45
+ },
46
+ },
47
+ defaultVariants: {
48
+ align: "inline-start",
49
+ },
50
+ },
51
+ );
52
+
53
+ function InputGroupAddon({
54
+ className,
55
+ align = "inline-start",
56
+ ...props
57
+ }: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
58
+ return (
59
+ <div
60
+ role="group"
61
+ data-slot="input-group-addon"
62
+ data-align={align}
63
+ className={cn(inputGroupAddonVariants({ align }), className)}
64
+ onClick={(e) => {
65
+ if ((e.target as HTMLElement).closest("button")) {
66
+ return;
67
+ }
68
+ e.currentTarget.parentElement?.querySelector("input")?.focus();
69
+ }}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+
75
+ const inputGroupButtonVariants = cva(
76
+ "text-sm shadow-none flex gap-2 items-center",
77
+ {
78
+ variants: {
79
+ size: {
80
+ xs: "h-7 gap-1 rounded-lg px-2 text-xs [&>svg:not([class*='size-'])]:size-3.5 has-[>svg]:px-2",
81
+ sm: "h-8 rounded-lg px-2.5 gap-1.5 text-xs has-[>svg]:px-2.5",
82
+ "icon-xs": "size-7 rounded-lg p-0 has-[>svg]:p-0",
83
+ "icon-sm": "size-8 rounded-lg p-0 has-[>svg]:p-0",
84
+ },
85
+ },
86
+ defaultVariants: {
87
+ size: "xs",
88
+ },
89
+ },
90
+ );
91
+
92
+ function InputGroupButton({
93
+ className,
94
+ type = "button",
95
+ variant = "ghost",
96
+ size = "xs",
97
+ ...props
98
+ }: Omit<React.ComponentProps<typeof Button>, "size"> &
99
+ VariantProps<typeof inputGroupButtonVariants>) {
100
+ return (
101
+ <Button
102
+ type={type}
103
+ data-size={size}
104
+ variant={variant}
105
+ className={cn(inputGroupButtonVariants({ size }), className)}
106
+ {...props}
107
+ />
108
+ );
109
+ }
110
+
111
+ function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
112
+ return (
113
+ <span
114
+ className={cn(
115
+ "text-muted flex items-center gap-2 text-sm [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
116
+ className,
117
+ )}
118
+ {...props}
119
+ />
120
+ );
121
+ }
122
+
123
+ function InputGroupInput({
124
+ className,
125
+ ...props
126
+ }: React.ComponentProps<"input">) {
127
+ return (
128
+ <Input
129
+ data-slot="input-group-control"
130
+ className={cn(
131
+ "flex-1 rounded-none border-0 bg-transparent shadow-none focus:outline-hidden focus-visible:ring-0",
132
+ className,
133
+ )}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function InputGroupTextarea({
140
+ className,
141
+ ...props
142
+ }: React.ComponentProps<"textarea">) {
143
+ return (
144
+ <Textarea
145
+ data-slot="input-group-control"
146
+ className={cn(
147
+ "flex-1 resize-none rounded-none border-0 bg-transparent py-3 shadow-none focus:outline-hidden focus-visible:ring-0",
148
+ className,
149
+ )}
150
+ {...props}
151
+ />
152
+ );
153
+ }
154
+
155
+ export {
156
+ InputGroup,
157
+ InputGroupAddon,
158
+ InputGroupButton,
159
+ InputGroupText,
160
+ InputGroupInput,
161
+ InputGroupTextarea,
162
+ };
@@ -0,0 +1,150 @@
1
+ "use client";
2
+
3
+ import * as React from "react";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "../utils/cn";
6
+ import { Button, type ButtonProps } from "./button";
7
+ import { Input } from "./input";
8
+ import { Textarea } from "./textarea";
9
+
10
+ const InputGroup = React.forwardRef<
11
+ HTMLDivElement,
12
+ React.HTMLAttributes<HTMLDivElement> & { error?: boolean }
13
+ >(({ className, error, ...props }, ref) => (
14
+ <div
15
+ ref={ref}
16
+ data-input-group=""
17
+ className={cn(
18
+ "flex h-10 flex-wrap rounded-field border border-border bg-field-background text-sm transition-[border-color,box-shadow] duration-200 ease-smooth",
19
+ "has-[textarea]:h-auto",
20
+ "has-data-[slot=input-group-control]:h-auto",
21
+ "has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col",
22
+ "has-focus-visible:border-focus-border has-focus-visible:outline-hidden has-focus-visible:ring-2 has-focus-visible:ring-focus-ring",
23
+ error &&
24
+ "border-danger-border has-focus-visible:border-danger has-focus-visible:ring-danger-ring",
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ ));
30
+ InputGroup.displayName = "InputGroup";
31
+
32
+ const inputGroupAddonVariants = cva("flex items-center", {
33
+ variants: {
34
+ align: {
35
+ "inline-start": "flex-row ps-3",
36
+ "inline-end": "flex-row pe-3",
37
+ "block-start": "w-full flex-row border-b border-separator px-3 py-2",
38
+ "block-end": "w-full flex-row px-3 py-2",
39
+ },
40
+ },
41
+ defaultVariants: {
42
+ align: "inline-start",
43
+ },
44
+ });
45
+
46
+ const InputGroupAddon = React.forwardRef<
47
+ HTMLDivElement,
48
+ React.HTMLAttributes<HTMLDivElement> &
49
+ VariantProps<typeof inputGroupAddonVariants>
50
+ >(({ className, align, onClick, ...props }, ref) => (
51
+ <div
52
+ ref={ref}
53
+ data-align={align}
54
+ className={cn(inputGroupAddonVariants({ align }), className)}
55
+ onClick={(e) => {
56
+ if (onClick) {
57
+ onClick(e);
58
+ return;
59
+ }
60
+ const group = (e.currentTarget as HTMLElement).closest(
61
+ "[data-input-group]",
62
+ );
63
+ const input = group?.querySelector("textarea, input");
64
+ if (input instanceof HTMLElement) input.focus();
65
+ }}
66
+ {...props}
67
+ />
68
+ ));
69
+ InputGroupAddon.displayName = "InputGroupAddon";
70
+
71
+ const inputGroupButtonVariants = cva("shrink-0 shadow-none", {
72
+ variants: {
73
+ size: {
74
+ xs: "h-7 rounded-lg px-2 text-xs",
75
+ sm: "h-8 rounded-lg px-2.5 text-xs",
76
+ "icon-xs": "h-7 w-7 rounded-lg",
77
+ "icon-sm": "h-8 w-8 rounded-lg",
78
+ },
79
+ },
80
+ defaultVariants: {
81
+ size: "xs",
82
+ },
83
+ });
84
+
85
+ const InputGroupButton = React.forwardRef<
86
+ HTMLButtonElement,
87
+ ButtonProps & VariantProps<typeof inputGroupButtonVariants>
88
+ >(({ className, size, variant = "ghost", ...props }, ref) => (
89
+ <Button
90
+ ref={ref}
91
+ variant={variant}
92
+ className={cn(inputGroupButtonVariants({ size }), className)}
93
+ {...props}
94
+ />
95
+ ));
96
+ InputGroupButton.displayName = "InputGroupButton";
97
+
98
+ function InputGroupText({
99
+ className,
100
+ ...props
101
+ }: React.HTMLAttributes<HTMLSpanElement>) {
102
+ return (
103
+ <span
104
+ className={cn(
105
+ "flex items-center text-sm text-muted-foreground/95",
106
+ className,
107
+ )}
108
+ {...props}
109
+ />
110
+ );
111
+ }
112
+
113
+ const InputGroupInput = React.forwardRef<
114
+ HTMLInputElement,
115
+ React.ComponentProps<typeof Input>
116
+ >(({ className, ...props }, ref) => (
117
+ <Input
118
+ ref={ref}
119
+ className={cn(
120
+ "h-full flex-1 border-0 bg-transparent px-3 py-2 shadow-none focus-visible:border-0 focus-visible:ring-0",
121
+ className,
122
+ )}
123
+ {...props}
124
+ />
125
+ ));
126
+ InputGroupInput.displayName = "InputGroupInput";
127
+
128
+ const InputGroupTextarea = React.forwardRef<
129
+ HTMLTextAreaElement,
130
+ React.ComponentProps<typeof Textarea>
131
+ >(({ className, ...props }, ref) => (
132
+ <Textarea
133
+ ref={ref}
134
+ className={cn(
135
+ "flex-1 resize-none border-0 bg-transparent px-3 py-2.5 shadow-none focus-visible:border-0 focus-visible:ring-0 field-sizing-content",
136
+ className,
137
+ )}
138
+ {...props}
139
+ />
140
+ ));
141
+ InputGroupTextarea.displayName = "InputGroupTextarea";
142
+
143
+ export {
144
+ InputGroup,
145
+ InputGroupAddon,
146
+ InputGroupButton,
147
+ InputGroupText,
148
+ InputGroupInput,
149
+ InputGroupTextarea,
150
+ };
@@ -0,0 +1,21 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils/cn";
3
+
4
+ const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
5
+ ({ className, type, ...props }, ref) => {
6
+ return (
7
+ <input
8
+ type={type}
9
+ className={cn(
10
+ "flex h-10 w-full max-sm:min-w-0 rounded-none 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 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-field-placeholder focus-visible:border-focus-border focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring disabled:cursor-not-allowed disabled:opacity-50",
11
+ className,
12
+ )}
13
+ ref={ref}
14
+ {...props}
15
+ />
16
+ );
17
+ },
18
+ );
19
+ Input.displayName = "Input";
20
+
21
+ export { Input };
package/src/ui/kbd.tsx ADDED
@@ -0,0 +1,18 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils/cn";
3
+
4
+ const Kbd = React.forwardRef<HTMLElement, React.HTMLAttributes<HTMLElement>>(
5
+ ({ className, ...props }, ref) => (
6
+ <kbd
7
+ ref={ref}
8
+ className={cn(
9
+ "pointer-events-none inline-flex h-5 select-none items-center gap-1 rounded-md bg-surface-secondary px-1.5 font-mono text-[10px] font-medium text-muted-foreground",
10
+ className,
11
+ )}
12
+ {...props}
13
+ />
14
+ ),
15
+ );
16
+ Kbd.displayName = "Kbd";
17
+
18
+ export { Kbd };
@@ -0,0 +1,20 @@
1
+ import * as React from "react";
2
+ import * as LabelPrimitive from "@radix-ui/react-label";
3
+ import { cn } from "../utils/cn";
4
+
5
+ const labelClass =
6
+ "text-sm font-medium leading-none tracking-normal text-foreground/90 peer-disabled:cursor-not-allowed peer-disabled:opacity-70";
7
+
8
+ const Label = React.forwardRef<
9
+ React.ComponentRef<typeof LabelPrimitive.Root>,
10
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
11
+ >(({ className, ...props }, ref) => (
12
+ <LabelPrimitive.Root
13
+ ref={ref}
14
+ className={cn(labelClass, className)}
15
+ {...props}
16
+ />
17
+ ));
18
+ Label.displayName = LabelPrimitive.Root.displayName;
19
+
20
+ export { Label };
@@ -0,0 +1,53 @@
1
+ import type { ReactNode } from "react";
2
+ import { Label } from "./label";
3
+ import { Switch } from "./switch";
4
+
5
+ interface LabeledSwitchRowProps {
6
+ id: string;
7
+ label: string;
8
+ description?: string;
9
+ checked: boolean;
10
+ onCheckedChange?: (checked: boolean) => void;
11
+ disabled?: boolean;
12
+ icon?: ReactNode;
13
+ trailing?: ReactNode;
14
+ }
15
+
16
+ export function LabeledSwitchRow({
17
+ id,
18
+ label,
19
+ description,
20
+ checked,
21
+ onCheckedChange,
22
+ disabled,
23
+ icon,
24
+ trailing,
25
+ }: LabeledSwitchRowProps) {
26
+ return (
27
+ <div className="flex items-center justify-between gap-3 max-sm:flex-wrap">
28
+ <div className="flex min-w-0 items-start gap-2">
29
+ {icon ? (
30
+ <span className="mt-0.5 shrink-0 text-muted">{icon}</span>
31
+ ) : null}
32
+ <div className="min-w-0">
33
+ <Label htmlFor={id} className="text-sm font-medium">
34
+ {label}
35
+ </Label>
36
+ {description ? (
37
+ <p className="mt-1 text-xs text-muted text-pretty">{description}</p>
38
+ ) : null}
39
+ </div>
40
+ </div>
41
+ {trailing ?? (
42
+ <div className="flex min-h-10 shrink-0 items-center">
43
+ <Switch
44
+ id={id}
45
+ checked={checked}
46
+ onCheckedChange={onCheckedChange}
47
+ disabled={disabled}
48
+ />
49
+ </div>
50
+ )}
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,85 @@
1
+ const closeTimers = new WeakMap<HTMLElement, ReturnType<typeof setTimeout>>();
2
+
3
+ function readModalCloseMs(): number {
4
+ const raw = getComputedStyle(document.documentElement).getPropertyValue(
5
+ "--modal-close-dur",
6
+ );
7
+ const parsed = parseFloat(raw);
8
+ return Number.isFinite(parsed) ? parsed : 150;
9
+ }
10
+
11
+ function clearCloseTimer(el: HTMLElement): void {
12
+ const timer = closeTimers.get(el);
13
+ if (timer !== undefined) {
14
+ clearTimeout(timer);
15
+ closeTimers.delete(el);
16
+ }
17
+ }
18
+
19
+ // open: scales up from, modal, scale clears any in, flight close cleanup
20
+ function openModalSurface(el: HTMLElement): void {
21
+ clearCloseTimer(el);
22
+ el.classList.remove("is-closing");
23
+ el.classList.add("is-open");
24
+ }
25
+
26
+ // close: swap to is, closing, remove after, modal, close, dur
27
+ function closeModalSurface(el: HTMLElement): void {
28
+ clearCloseTimer(el);
29
+ el.classList.remove("is-open");
30
+ el.classList.add("is-closing");
31
+ const closeMs = readModalCloseMs();
32
+ const timer = setTimeout(() => {
33
+ el.classList.remove("is-closing");
34
+ closeTimers.delete(el);
35
+ }, closeMs);
36
+ closeTimers.set(el, timer);
37
+ }
38
+
39
+ // first paint entrance, start at resting scale, then open on next frame
40
+ function primeModalSurface(el: HTMLElement): void {
41
+ clearCloseTimer(el);
42
+ el.classList.remove("is-open", "is-closing");
43
+ requestAnimationFrame(() => {
44
+ openModalSurface(el);
45
+ });
46
+ }
47
+
48
+ // sync is, open / is, closing with radix data, state on the same node
49
+ function syncModalSurfaceFromDataState(el: HTMLElement): void {
50
+ const state = el.getAttribute("data-state");
51
+ if (state === "open") {
52
+ if (!el.classList.contains("is-open")) {
53
+ primeModalSurface(el);
54
+ } else {
55
+ clearCloseTimer(el);
56
+ el.classList.remove("is-closing");
57
+ }
58
+ return;
59
+ }
60
+ if (state === "closed" && el.classList.contains("is-open")) {
61
+ closeModalSurface(el);
62
+ }
63
+ }
64
+
65
+ function disconnectModalSurface(el: HTMLElement): void {
66
+ clearCloseTimer(el);
67
+ }
68
+
69
+ // wire radix data, state → is, open / is, closing on the dialog surface
70
+ // call from the content ref callback so portal, mounted nodes are never missed
71
+ export function connectModalSurface(el: HTMLElement): () => void {
72
+ if (el.getAttribute("data-state") === "open") {
73
+ primeModalSurface(el);
74
+ }
75
+
76
+ const observer = new MutationObserver(() => {
77
+ syncModalSurfaceFromDataState(el);
78
+ });
79
+ observer.observe(el, { attributes: true, attributeFilter: ["data-state"] });
80
+
81
+ return () => {
82
+ observer.disconnect();
83
+ disconnectModalSurface(el);
84
+ };
85
+ }
@@ -0,0 +1,47 @@
1
+ import * as React from "react";
2
+ import { cn } from "../utils/cn";
3
+ import { Button } from "./button";
4
+
5
+ interface PaginationProps {
6
+ total: number;
7
+ page: number;
8
+ onChange: (page: number) => void;
9
+ className?: string;
10
+ }
11
+
12
+ function Pagination({ total, page, onChange, className }: PaginationProps) {
13
+ if (total <= 1) return null;
14
+
15
+ return (
16
+ <nav className={cn("flex items-center justify-center gap-1", className)}>
17
+ <Button
18
+ variant="outline"
19
+ size="sm"
20
+ onClick={() => onChange(Math.max(1, page - 1))}
21
+ disabled={page <= 1}
22
+ >
23
+ Previous
24
+ </Button>
25
+ {Array.from({ length: total }, (_, i) => i + 1).map((p) => (
26
+ <Button
27
+ key={p}
28
+ variant={p === page ? "default" : "outline"}
29
+ size="sm"
30
+ onClick={() => onChange(p)}
31
+ >
32
+ {p}
33
+ </Button>
34
+ ))}
35
+ <Button
36
+ variant="outline"
37
+ size="sm"
38
+ onClick={() => onChange(Math.min(total, page + 1))}
39
+ disabled={page >= total}
40
+ >
41
+ Next
42
+ </Button>
43
+ </nav>
44
+ );
45
+ }
46
+
47
+ export { Pagination, type PaginationProps };
@@ -0,0 +1,33 @@
1
+ import * as React from "react";
2
+ import * as PopoverPrimitive from "@radix-ui/react-popover";
3
+ import { cn } from "../utils/cn";
4
+ import {
5
+ floatingSurfaceAnimateClass,
6
+ floatingSurfaceClass,
7
+ } from "./_menu-classes";
8
+
9
+ const Popover = PopoverPrimitive.Root;
10
+ const PopoverTrigger = PopoverPrimitive.Trigger;
11
+
12
+ const PopoverContent = React.forwardRef<
13
+ React.ComponentRef<typeof PopoverPrimitive.Content>,
14
+ React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
15
+ >(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
16
+ <PopoverPrimitive.Portal>
17
+ <PopoverPrimitive.Content
18
+ ref={ref}
19
+ align={align}
20
+ sideOffset={sideOffset}
21
+ className={cn(
22
+ "z-50 w-72 max-sm:max-w-[calc(100vw-2rem)] max-sm:max-h-[85dvh] max-sm:overflow-y-auto p-4 outline-none",
23
+ floatingSurfaceClass,
24
+ floatingSurfaceAnimateClass,
25
+ className,
26
+ )}
27
+ {...props}
28
+ />
29
+ </PopoverPrimitive.Portal>
30
+ ));
31
+ PopoverContent.displayName = PopoverPrimitive.Content.displayName;
32
+
33
+ export { Popover, PopoverTrigger, PopoverContent };
@@ -0,0 +1,25 @@
1
+ import * as React from "react";
2
+ import * as ProgressPrimitive from "@radix-ui/react-progress";
3
+ import { cn } from "../utils/cn";
4
+
5
+ const Progress = React.forwardRef<
6
+ React.ComponentRef<typeof ProgressPrimitive.Root>,
7
+ React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
8
+ >(({ className, value, ...props }, ref) => (
9
+ <ProgressPrimitive.Root
10
+ ref={ref}
11
+ className={cn(
12
+ "relative h-2.5 w-full overflow-hidden rounded-full bg-default/85",
13
+ className,
14
+ )}
15
+ {...props}
16
+ >
17
+ <ProgressPrimitive.Indicator
18
+ className="h-full w-full flex-1 rounded-full bg-accent transition-transform duration-300 ease-smooth"
19
+ style={{ transform: `translateX(-${100 - (value ?? 0)}%)` }}
20
+ />
21
+ </ProgressPrimitive.Root>
22
+ ));
23
+ Progress.displayName = ProgressPrimitive.Root.displayName;
24
+
25
+ export { Progress };