@turtleclub/ui 0.0.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.
Files changed (50) hide show
  1. package/.turbo/turbo-build.log +15 -0
  2. package/.turbo/turbo-type-check.log +360 -0
  3. package/README.md +3 -0
  4. package/components.json +21 -0
  5. package/dist/index.cjs +2 -0
  6. package/dist/index.cjs.map +1 -0
  7. package/dist/index.js +1672 -0
  8. package/dist/index.js.map +1 -0
  9. package/dist/styles.css +1 -0
  10. package/package.json +66 -0
  11. package/src/components/molecules/index.ts +7 -0
  12. package/src/components/molecules/opportunity-details.tsx +145 -0
  13. package/src/components/molecules/opportunity-item.tsx +63 -0
  14. package/src/components/molecules/route-details.tsx +87 -0
  15. package/src/components/molecules/swap-details.tsx +95 -0
  16. package/src/components/molecules/swap-input.tsx +115 -0
  17. package/src/components/molecules/token-selector.tsx +72 -0
  18. package/src/components/molecules/tx-status.tsx +254 -0
  19. package/src/components/ui/button.tsx +65 -0
  20. package/src/components/ui/card.tsx +101 -0
  21. package/src/components/ui/chip.tsx +48 -0
  22. package/src/components/ui/icon-animation.tsx +82 -0
  23. package/src/components/ui/index.ts +18 -0
  24. package/src/components/ui/info-card.tsx +128 -0
  25. package/src/components/ui/input.tsx +78 -0
  26. package/src/components/ui/label-with-icon.tsx +112 -0
  27. package/src/components/ui/label.tsx +22 -0
  28. package/src/components/ui/navigation-bar.tsx +135 -0
  29. package/src/components/ui/opportunity-details-v1.tsx +90 -0
  30. package/src/components/ui/scroll-area.tsx +56 -0
  31. package/src/components/ui/select.tsx +180 -0
  32. package/src/components/ui/separator.tsx +26 -0
  33. package/src/components/ui/sonner.tsx +23 -0
  34. package/src/components/ui/switch.tsx +29 -0
  35. package/src/components/ui/toggle-group.tsx +71 -0
  36. package/src/components/ui/toggle.tsx +47 -0
  37. package/src/components/ui/tooltip.tsx +59 -0
  38. package/src/index.ts +9 -0
  39. package/src/lib/utils.ts +6 -0
  40. package/src/styles/globals.css +75 -0
  41. package/src/styles/themes/index.css +9 -0
  42. package/src/styles/themes/semantic.css +107 -0
  43. package/src/styles/tokens/colors.css +77 -0
  44. package/src/styles/tokens/index.css +15 -0
  45. package/src/styles/tokens/radius.css +46 -0
  46. package/src/styles/tokens/spacing.css +52 -0
  47. package/src/styles/tokens/typography.css +86 -0
  48. package/src/tokens/index.ts +108 -0
  49. package/tsconfig.json +21 -0
  50. package/vite.config.js +65 -0
@@ -0,0 +1,78 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const inputVariants = cva(
7
+ "flex w-full bg-transparent text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground transition-colors outline-none disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50",
8
+ {
9
+ variants: {
10
+ variant: {
11
+ // Turtle Design System - transparent input with no borders
12
+ default: "border-none focus:ring-0 focus:border-none caret-primary",
13
+ // Optional bordered version for other use cases
14
+ bordered: "border border-border rounded-md focus:border-primary focus:ring-2 focus:ring-primary/20 caret-primary",
15
+ // No focus variant - cursor color only, no focus styles
16
+ nofocus: "border-none focus:ring-0 focus:border-none focus:outline-none caret-primary",
17
+ },
18
+ size: {
19
+ default: "h-10 px-3 py-2 text-sm",
20
+ sm: "h-8 px-2 py-1 text-xs",
21
+ lg: "h-12 px-4 py-3 text-base",
22
+ },
23
+ cursor: {
24
+ primary: "caret-primary",
25
+ foreground: "caret-foreground",
26
+ white: "caret-white",
27
+ green: "caret-green-500",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ size: "default",
33
+ cursor: "primary",
34
+ },
35
+ }
36
+ );
37
+
38
+ export interface InputProps
39
+ extends Omit<React.ComponentProps<"input">, "size">,
40
+ VariantProps<typeof inputVariants> {
41
+ // Optional prompt text that appears before the input
42
+ prompt?: string;
43
+ }
44
+
45
+ const Input = React.forwardRef<HTMLInputElement, InputProps>(
46
+ ({ className, variant, size, cursor, type, prompt, ...props }, ref) => {
47
+ if (prompt) {
48
+ return (
49
+ <div className="flex items-center gap-2">
50
+ <span className="text-primary text-sm font-medium shrink-0">
51
+ {prompt}
52
+ </span>
53
+ <input
54
+ type={type}
55
+ data-slot="input"
56
+ className={cn(inputVariants({ variant, size, cursor, className }))}
57
+ ref={ref}
58
+ {...props}
59
+ />
60
+ </div>
61
+ );
62
+ }
63
+
64
+ return (
65
+ <input
66
+ type={type}
67
+ data-slot="input"
68
+ className={cn(inputVariants({ variant, size, cursor, className }))}
69
+ ref={ref}
70
+ {...props}
71
+ />
72
+ );
73
+ }
74
+ );
75
+
76
+ Input.displayName = "Input";
77
+
78
+ export { Input, inputVariants };
@@ -0,0 +1,112 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const labelWithIconVariants = cva("inline-flex items-center gap-2 font-medium", {
7
+ variants: {
8
+ variant: {
9
+ default: "text-foreground",
10
+ muted: "text-muted-foreground",
11
+ primary: "text-primary",
12
+ secondary: "text-secondary-foreground",
13
+ },
14
+ textSize: {
15
+ xs: "text-xs",
16
+ sm: "text-sm",
17
+ base: "text-base",
18
+ lg: "text-lg",
19
+ xl: "text-xl",
20
+ "2xl": "text-2xl",
21
+ "3xl": "text-3xl",
22
+ "4xl": "text-4xl",
23
+ "5xl": "text-5xl",
24
+ "6xl": "text-6xl",
25
+ "7xl": "text-7xl",
26
+ "8xl": "text-8xl",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ variant: "default",
31
+ textSize: "sm",
32
+ },
33
+ });
34
+
35
+ const iconSizeClasses = {
36
+ xs: "w-3 h-3",
37
+ sm: "w-4 h-4",
38
+ base: "w-5 h-5",
39
+ lg: "w-6 h-6",
40
+ xl: "w-8 h-8",
41
+ };
42
+
43
+ export interface LabelWithIconProps
44
+ extends React.ComponentProps<"div">,
45
+ VariantProps<typeof labelWithIconVariants> {
46
+ icon: React.ReactNode | string; // Can be a component or URL string
47
+ children: React.ReactNode;
48
+ iconPosition?: "left" | "right";
49
+ iconSize?: keyof typeof iconSizeClasses;
50
+ iconClassName?: string; // Additional classes for the icon
51
+ }
52
+
53
+ const LabelWithIcon = React.forwardRef<HTMLDivElement, LabelWithIconProps>(
54
+ (
55
+ {
56
+ className,
57
+ variant,
58
+ textSize,
59
+ icon,
60
+ children,
61
+ iconPosition = "left",
62
+ iconSize = "sm",
63
+ iconClassName,
64
+ ...props
65
+ },
66
+ ref
67
+ ) => {
68
+ const renderIcon = () => {
69
+ // If icon is a string (URL), render as img
70
+ if (typeof icon === "string") {
71
+ return (
72
+ <img
73
+ src={icon}
74
+ alt=""
75
+ className={cn(iconSizeClasses[iconSize], "object-contain", iconClassName)}
76
+ />
77
+ );
78
+ }
79
+
80
+ // If icon is a React component, render it directly
81
+ // If it's a Lucide icon or any other component, it will be rendered as-is
82
+ return (
83
+ <span className={cn("shrink-0", iconClassName)}>
84
+ {React.isValidElement(icon)
85
+ ? React.cloneElement(icon as React.ReactElement<any>, {
86
+ className: cn(iconSizeClasses[iconSize], (icon as any).props?.className),
87
+ })
88
+ : icon}
89
+ </span>
90
+ );
91
+ };
92
+
93
+ return (
94
+ <div
95
+ ref={ref}
96
+ className={cn(
97
+ labelWithIconVariants({ variant, textSize }),
98
+ iconPosition === "right" && "flex-row-reverse",
99
+ className
100
+ )}
101
+ {...props}
102
+ >
103
+ {renderIcon()}
104
+ <span>{children}</span>
105
+ </div>
106
+ );
107
+ }
108
+ );
109
+
110
+ LabelWithIcon.displayName = "LabelWithIcon";
111
+
112
+ export { LabelWithIcon, labelWithIconVariants };
@@ -0,0 +1,22 @@
1
+ import * as React from "react"
2
+ import * as LabelPrimitive from "@radix-ui/react-label"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Label({
7
+ className,
8
+ ...props
9
+ }: React.ComponentProps<typeof LabelPrimitive.Root>) {
10
+ return (
11
+ <LabelPrimitive.Root
12
+ data-slot="label"
13
+ className={cn(
14
+ "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
15
+ className
16
+ )}
17
+ {...props}
18
+ />
19
+ )
20
+ }
21
+
22
+ export { Label }
@@ -0,0 +1,135 @@
1
+ import * as React from "react";
2
+ import { cva, type VariantProps } from "class-variance-authority";
3
+
4
+ import { cn } from "@/lib/utils";
5
+
6
+ const navigationBarVariants = cva("flex w-full", {
7
+ variants: {
8
+ variant: {
9
+ default: "justify-between border border-border shadow-sm bg-background",
10
+ transparent: "justify-between border border-border shadow-sm bg-transparent",
11
+ menuBar: "relative h-12 gap-3 rounded-full border border-border bg-background font-medium shadow-sm",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ variant: "default",
16
+ },
17
+ });
18
+
19
+ const navigationItemVariants = cva(
20
+ "flex items-center justify-center whitespace-nowrap font-medium transition-all disabled:pointer-events-none disabled:opacity-50 cursor-pointer",
21
+ {
22
+ variants: {
23
+ variant: {
24
+ default: "text-sm text-muted-foreground hover:text-foreground",
25
+ active: "text-sm text-primary bg-muted",
26
+ menuBarDefault: "relative z-[1] w-full text-base text-foreground rounded-full px-3 py-2 hover:text-primary",
27
+ menuBarActive: "relative z-[1] w-full text-base text-primary rounded-full px-3 py-2",
28
+ },
29
+ },
30
+ defaultVariants: {
31
+ variant: "default",
32
+ },
33
+ }
34
+ );
35
+
36
+ export interface NavigationBarProps
37
+ extends React.ComponentProps<"nav">,
38
+ VariantProps<typeof navigationBarVariants> {
39
+ activeValue?: string;
40
+ }
41
+
42
+ export interface NavigationItemProps
43
+ extends React.ComponentProps<"button">,
44
+ VariantProps<typeof navigationItemVariants> {
45
+ active?: boolean;
46
+ value?: string;
47
+ }
48
+
49
+ const NavigationBar = React.forwardRef<HTMLElement, NavigationBarProps>(
50
+ ({ className, variant, activeValue, children, ...props }, ref) => {
51
+ const containerRef = React.useRef<HTMLElement>(null);
52
+ const indicatorRef = React.useRef<HTMLDivElement>(null);
53
+
54
+ // Function to update the indicator position and size based on the active button
55
+ const updateIndicatorPosition = React.useCallback(() => {
56
+ if (variant !== "menuBar" || !containerRef.current || !indicatorRef.current) return;
57
+
58
+ const activeButton = containerRef.current.querySelector(`[data-active="true"]`) as HTMLElement;
59
+ if (activeButton) {
60
+ indicatorRef.current.style.width = `${activeButton.offsetWidth}px`;
61
+ indicatorRef.current.style.left = `${activeButton.offsetLeft}px`;
62
+ }
63
+ }, [variant]);
64
+
65
+ // Create ResizeObserver to handle indicator container size changes
66
+ React.useEffect(() => {
67
+ if (variant !== "menuBar") return;
68
+
69
+ updateIndicatorPosition();
70
+
71
+ const resizeObserver = new ResizeObserver(updateIndicatorPosition);
72
+
73
+ if (containerRef.current) {
74
+ resizeObserver.observe(containerRef.current);
75
+ }
76
+
77
+ return () => {
78
+ resizeObserver.disconnect();
79
+ };
80
+ }, [activeValue, updateIndicatorPosition, variant]);
81
+
82
+ return (
83
+ <nav
84
+ ref={ref || containerRef}
85
+ className={cn(navigationBarVariants({ variant, className }))}
86
+ {...props}
87
+ >
88
+ {variant === "menuBar" && (
89
+ <div
90
+ ref={indicatorRef}
91
+ className="absolute bottom-0 h-full origin-left rounded-full bg-secondary transition-all duration-300"
92
+ />
93
+ )}
94
+ {children}
95
+ </nav>
96
+ );
97
+ }
98
+ );
99
+
100
+ NavigationBar.displayName = "NavigationBar";
101
+
102
+ const NavigationItem = React.forwardRef<HTMLButtonElement, NavigationItemProps>(
103
+ ({ className, variant, active, value, ...props }, ref) => {
104
+ // Determine the correct variant based on parent and state
105
+ const getItemVariant = () => {
106
+ if (variant === "menuBarDefault" || variant === "menuBarActive") {
107
+ return active ? "menuBarActive" : "menuBarDefault";
108
+ }
109
+ return active ? "active" : variant || "default";
110
+ };
111
+
112
+ const appliedVariant = getItemVariant();
113
+ const isMenuBar = appliedVariant === "menuBarDefault" || appliedVariant === "menuBarActive";
114
+
115
+ return (
116
+ <button
117
+ ref={ref}
118
+ data-active={active}
119
+ data-value={value}
120
+ className={cn(
121
+ navigationItemVariants({
122
+ variant: appliedVariant,
123
+ className,
124
+ }),
125
+ !isMenuBar && "px-4 py-2 rounded-md"
126
+ )}
127
+ {...props}
128
+ />
129
+ );
130
+ }
131
+ );
132
+
133
+ NavigationItem.displayName = "NavigationItem";
134
+
135
+ export { NavigationBar, NavigationItem, navigationBarVariants, navigationItemVariants };
@@ -0,0 +1,90 @@
1
+ import * as React from "react";
2
+ import { cn } from "@/lib/utils";
3
+ import { Card } from "./card";
4
+ import { InfoCard } from "./info-card";
5
+ import { LabelWithIcon } from "./label-with-icon";
6
+
7
+ interface InfoCardData {
8
+ title: string;
9
+ value: string;
10
+ color?: "primary" | "secondary" | "accent" | "success" | "warning" | "error";
11
+ icon?: React.ReactNode;
12
+ }
13
+
14
+ interface OpportunityDetailsV1Props {
15
+ title?: string;
16
+ titleIcon?: React.ReactNode;
17
+ topCards: readonly InfoCardData[]; // Flexible array for top row
18
+ bottomCards: readonly InfoCardData[]; // Flexible array for bottom
19
+ className?: string;
20
+ }
21
+
22
+ function OpportunityDetailsV1({
23
+ className,
24
+ title,
25
+ titleIcon,
26
+ topCards,
27
+ bottomCards,
28
+ }: OpportunityDetailsV1Props) {
29
+ const defaultTitleIcon = (
30
+ <div className="w-8 h-8 rounded-full bg-primary/20 flex items-center justify-center">
31
+ <svg className="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
32
+ <path d="M12 2L13.09 8.26L20 7L18.74 13.09L22 14L16.74 19.26L17 21L10.91 19.74L10 22L8.09 15.74L2 17L3.26 10.91L0 10L5.26 4.74L5 3L11.09 4.26L12 2Z" />
33
+ </svg>
34
+ </div>
35
+ );
36
+
37
+ return (
38
+ <Card className={cn("space-y-4", className)}>
39
+ {/* TODO: Add scroll area */}
40
+ <div className="h-full overflow-y-auto">
41
+ <div className="space-y-4">
42
+ {/* Header with LabelWithIcon */}
43
+ {(title || titleIcon) && (
44
+ <div className="flex items-center">
45
+ <LabelWithIcon icon={titleIcon || defaultTitleIcon} textSize="lg" iconSize="lg">
46
+ {title}
47
+ </LabelWithIcon>
48
+ </div>
49
+ )}
50
+
51
+ {/* Top row: 3 InfoCards side by side */}
52
+ <div className="grid grid-cols-3 gap-3">
53
+ {topCards.map((card, index) => (
54
+ <InfoCard
55
+ key={index}
56
+ title={card.title}
57
+ value={card.value}
58
+ color={card.color || "primary"}
59
+ icon={card.icon}
60
+ size="sm"
61
+ valueSize="sm"
62
+ titleSize="sm"
63
+ align="left"
64
+ />
65
+ ))}
66
+ </div>
67
+
68
+ {/* Bottom section: 4 InfoCards full width, one below another */}
69
+ <div className="space-y-2">
70
+ {bottomCards.map((card, index) => (
71
+ <InfoCard
72
+ key={index}
73
+ title={card.title}
74
+ value={card.value}
75
+ color={card.color || "primary"}
76
+ size="sm"
77
+ valueSize="sm"
78
+ titleSize="sm"
79
+ align="left"
80
+ className="w-full"
81
+ />
82
+ ))}
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </Card>
87
+ );
88
+ }
89
+
90
+ export { OpportunityDetailsV1, type InfoCardData };
@@ -0,0 +1,56 @@
1
+ import * as React from "react"
2
+ import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function ScrollArea({
7
+ className,
8
+ children,
9
+ ...props
10
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
11
+ return (
12
+ <ScrollAreaPrimitive.Root
13
+ data-slot="scroll-area"
14
+ className={cn("relative", className)}
15
+ {...props}
16
+ >
17
+ <ScrollAreaPrimitive.Viewport
18
+ data-slot="scroll-area-viewport"
19
+ className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
20
+ >
21
+ {children}
22
+ </ScrollAreaPrimitive.Viewport>
23
+ <ScrollBar />
24
+ <ScrollAreaPrimitive.Corner />
25
+ </ScrollAreaPrimitive.Root>
26
+ )
27
+ }
28
+
29
+ function ScrollBar({
30
+ className,
31
+ orientation = "vertical",
32
+ ...props
33
+ }: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
34
+ return (
35
+ <ScrollAreaPrimitive.ScrollAreaScrollbar
36
+ data-slot="scroll-area-scrollbar"
37
+ orientation={orientation}
38
+ className={cn(
39
+ "flex touch-none p-px transition-colors select-none",
40
+ orientation === "vertical" &&
41
+ "h-full w-2.5 border-l border-l-transparent",
42
+ orientation === "horizontal" &&
43
+ "h-2.5 flex-col border-t border-t-transparent",
44
+ className
45
+ )}
46
+ {...props}
47
+ >
48
+ <ScrollAreaPrimitive.ScrollAreaThumb
49
+ data-slot="scroll-area-thumb"
50
+ className="bg-border relative flex-1 rounded-full"
51
+ />
52
+ </ScrollAreaPrimitive.ScrollAreaScrollbar>
53
+ )
54
+ }
55
+
56
+ export { ScrollArea, ScrollBar }
@@ -0,0 +1,180 @@
1
+ import * as React from "react";
2
+ import * as SelectPrimitive from "@radix-ui/react-select";
3
+ import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ function Select({ ...props }: React.ComponentProps<typeof SelectPrimitive.Root>) {
8
+ return <SelectPrimitive.Root data-slot="select" {...props} />;
9
+ }
10
+
11
+ function SelectGroup({ ...props }: React.ComponentProps<typeof SelectPrimitive.Group>) {
12
+ return <SelectPrimitive.Group data-slot="select-group" {...props} />;
13
+ }
14
+
15
+ function SelectValue({ ...props }: React.ComponentProps<typeof SelectPrimitive.Value>) {
16
+ return <SelectPrimitive.Value data-slot="select-value" {...props} />;
17
+ }
18
+
19
+ function SelectTrigger({
20
+ className,
21
+ size = "default",
22
+ children,
23
+ ...props
24
+ }: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
25
+ size?: "sm" | "default";
26
+ }) {
27
+ return (
28
+ <SelectPrimitive.Trigger
29
+ data-slot="select-trigger"
30
+ data-size={size}
31
+ className={cn(
32
+ // Turtle Design System - transparent trigger with wise white text
33
+ "flex w-fit items-center justify-between gap-2 bg-transparent px-3 py-2 text-sm text-foreground whitespace-nowrap transition-colors outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 border-none focus:ring-0",
34
+ "data-[placeholder]:text-muted-foreground",
35
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg]:text-muted-foreground",
36
+ "*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
37
+ className
38
+ )}
39
+ {...props}
40
+ >
41
+ {children}
42
+ <SelectPrimitive.Icon asChild>
43
+ <ChevronDownIcon className="size-4 opacity-50" />
44
+ </SelectPrimitive.Icon>
45
+ </SelectPrimitive.Trigger>
46
+ );
47
+ }
48
+
49
+ function SelectContent({
50
+ className,
51
+ children,
52
+ position = "popper",
53
+ ...props
54
+ }: React.ComponentProps<typeof SelectPrimitive.Content>) {
55
+ return (
56
+ <SelectPrimitive.Portal>
57
+ <SelectPrimitive.Content
58
+ data-slot="select-content"
59
+ className={cn(
60
+ // Turtle Design System - ninja black background for content
61
+ "bg-background text-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border border-border shadow-md",
62
+ position === "popper" &&
63
+ "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
64
+ className
65
+ )}
66
+ position={position}
67
+ {...props}
68
+ >
69
+ <SelectScrollUpButton />
70
+ <SelectPrimitive.Viewport
71
+ className={cn(
72
+ "p-1",
73
+ position === "popper" &&
74
+ "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
75
+ )}
76
+ >
77
+ {children}
78
+ </SelectPrimitive.Viewport>
79
+ <SelectScrollDownButton />
80
+ </SelectPrimitive.Content>
81
+ </SelectPrimitive.Portal>
82
+ );
83
+ }
84
+
85
+ function SelectLabel({ className, ...props }: React.ComponentProps<typeof SelectPrimitive.Label>) {
86
+ return (
87
+ <SelectPrimitive.Label
88
+ data-slot="select-label"
89
+ className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
90
+ {...props}
91
+ />
92
+ );
93
+ }
94
+
95
+ function SelectItem({
96
+ className,
97
+ children,
98
+ ...props
99
+ }: React.ComponentProps<typeof SelectPrimitive.Item>) {
100
+ return (
101
+ <SelectPrimitive.Item
102
+ data-slot="select-item"
103
+ className={cn(
104
+ // Turtle Design System - items with wise white alpha (2%) background and ninja black text
105
+ "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm text-foreground outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
106
+ "bg-muted hover:bg-muted/80 focus:bg-muted/80", // wise white alpha (2%) background
107
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
108
+ "*:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
109
+ className
110
+ )}
111
+ {...props}
112
+ >
113
+ <span className="absolute right-2 flex size-3.5 items-center justify-center">
114
+ {/* Single dot that changes color based on selection */}
115
+ <div className="w-2 h-2 rounded-full bg-muted-foreground/30 transition-colors" />
116
+ {/* Use Radix's ItemIndicator to show selected state */}
117
+ <SelectPrimitive.ItemIndicator className="absolute">
118
+ <div className="w-2 h-2 rounded-full bg-primary" />
119
+ </SelectPrimitive.ItemIndicator>
120
+ </span>
121
+ <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
122
+ </SelectPrimitive.Item>
123
+ );
124
+ }
125
+
126
+ function SelectSeparator({
127
+ className,
128
+ ...props
129
+ }: React.ComponentProps<typeof SelectPrimitive.Separator>) {
130
+ return (
131
+ <SelectPrimitive.Separator
132
+ data-slot="select-separator"
133
+ className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
134
+ {...props}
135
+ />
136
+ );
137
+ }
138
+
139
+ function SelectScrollUpButton({
140
+ className,
141
+ ...props
142
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
143
+ return (
144
+ <SelectPrimitive.ScrollUpButton
145
+ data-slot="select-scroll-up-button"
146
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
147
+ {...props}
148
+ >
149
+ <ChevronUpIcon className="size-4" />
150
+ </SelectPrimitive.ScrollUpButton>
151
+ );
152
+ }
153
+
154
+ function SelectScrollDownButton({
155
+ className,
156
+ ...props
157
+ }: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
158
+ return (
159
+ <SelectPrimitive.ScrollDownButton
160
+ data-slot="select-scroll-down-button"
161
+ className={cn("flex cursor-default items-center justify-center py-1", className)}
162
+ {...props}
163
+ >
164
+ <ChevronDownIcon className="size-4" />
165
+ </SelectPrimitive.ScrollDownButton>
166
+ );
167
+ }
168
+
169
+ export {
170
+ Select,
171
+ SelectContent,
172
+ SelectGroup,
173
+ SelectItem,
174
+ SelectLabel,
175
+ SelectScrollDownButton,
176
+ SelectScrollUpButton,
177
+ SelectSeparator,
178
+ SelectTrigger,
179
+ SelectValue,
180
+ };