sonance-brand-mcp 1.3.108 → 1.3.110
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/dist/assets/components/alert.tsx +35 -9
- package/dist/assets/components/badge.tsx +49 -20
- package/dist/assets/components/button.tsx +29 -20
- package/dist/assets/components/card.tsx +87 -33
- package/dist/assets/components/checkbox.tsx +36 -12
- package/dist/assets/components/dialog.tsx +73 -30
- package/dist/assets/components/dropdown-menu.tsx +57 -20
- package/dist/assets/components/input.tsx +35 -14
- package/dist/assets/components/pagination.tsx +86 -35
- package/dist/assets/components/popover.tsx +80 -36
- package/dist/assets/components/radio-group.tsx +40 -12
- package/dist/assets/components/select.tsx +62 -26
- package/dist/assets/components/switch.tsx +41 -13
- package/dist/assets/components/tabs.tsx +32 -12
- package/dist/assets/components/tooltip.tsx +34 -5
- package/dist/index.js +470 -115
- package/package.json +1 -1
|
@@ -1,12 +1,44 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState, useRef, useEffect, useCallback } from "react";
|
|
3
|
+
import { useState, useRef, useEffect, useCallback, createContext, useContext } from "react";
|
|
4
4
|
import { createPortal } from "react-dom";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
6
|
import { cn } from "@/lib/utils";
|
|
6
7
|
|
|
7
8
|
type PopoverPosition = "top" | "bottom" | "left" | "right";
|
|
8
|
-
|
|
9
|
-
|
|
9
|
+
type PopoverSize = "compact" | "default" | "spacious";
|
|
10
|
+
type PopoverVariant = "default" | "glass";
|
|
11
|
+
|
|
12
|
+
const PopoverContext = createContext<{ size: PopoverSize }>({ size: "default" });
|
|
13
|
+
|
|
14
|
+
const popoverVariants = cva(
|
|
15
|
+
"fixed z-50 min-w-[200px] border shadow-xl animate-in fade-in zoom-in-95 duration-150",
|
|
16
|
+
{
|
|
17
|
+
variants: {
|
|
18
|
+
size: {
|
|
19
|
+
compact: "rounded-lg",
|
|
20
|
+
default: "rounded-xl",
|
|
21
|
+
spacious: "rounded-2xl",
|
|
22
|
+
},
|
|
23
|
+
popoverVariant: {
|
|
24
|
+
default: "bg-card border-border",
|
|
25
|
+
glass: "bg-card/95 border-border/50 backdrop-blur-xl",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
defaultVariants: {
|
|
29
|
+
size: "default",
|
|
30
|
+
popoverVariant: "glass",
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const popoverContentPadding = {
|
|
36
|
+
compact: "p-3",
|
|
37
|
+
default: "p-4",
|
|
38
|
+
spacious: "p-5",
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface PopoverProps extends VariantProps<typeof popoverVariants> {
|
|
10
42
|
trigger: React.ReactNode;
|
|
11
43
|
children: React.ReactNode;
|
|
12
44
|
position?: PopoverPosition;
|
|
@@ -25,6 +57,8 @@ export function Popover({
|
|
|
25
57
|
position = "bottom",
|
|
26
58
|
triggerOn = "click",
|
|
27
59
|
className,
|
|
60
|
+
size = "default",
|
|
61
|
+
popoverVariant = "glass",
|
|
28
62
|
}: PopoverProps) {
|
|
29
63
|
const [isOpen, setIsOpen] = useState(false);
|
|
30
64
|
const [portalPosition, setPortalPosition] = useState<PortalPosition>({ top: 0, left: 0 });
|
|
@@ -144,41 +178,42 @@ export function Popover({
|
|
|
144
178
|
};
|
|
145
179
|
|
|
146
180
|
return (
|
|
147
|
-
<
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<div
|
|
154
|
-
ref={triggerRef}
|
|
155
|
-
onClick={triggerOn === "click" ? () => setIsOpen(!isOpen) : undefined}
|
|
156
|
-
className="cursor-pointer"
|
|
181
|
+
<PopoverContext.Provider value={{ size }}>
|
|
182
|
+
<div data-sonance-name="popover"
|
|
183
|
+
ref={containerRef}
|
|
184
|
+
className="relative inline-block"
|
|
185
|
+
onMouseEnter={handleMouseEnter}
|
|
186
|
+
onMouseLeave={handleMouseLeave}
|
|
157
187
|
>
|
|
158
|
-
{trigger}
|
|
159
|
-
</div>
|
|
160
|
-
{mounted && isOpen && createPortal(
|
|
161
188
|
<div
|
|
162
|
-
ref={
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
"animate-in fade-in zoom-in-95 duration-150",
|
|
166
|
-
position === "left" || position === "right" ? "-translate-y-1/2" : "",
|
|
167
|
-
className
|
|
168
|
-
)}
|
|
169
|
-
style={{
|
|
170
|
-
top: portalPosition.top,
|
|
171
|
-
left: portalPosition.left,
|
|
172
|
-
transformOrigin: transformOrigin[position],
|
|
173
|
-
}}
|
|
174
|
-
onMouseEnter={handleMouseEnter}
|
|
175
|
-
onMouseLeave={handleMouseLeave}
|
|
189
|
+
ref={triggerRef}
|
|
190
|
+
onClick={triggerOn === "click" ? () => setIsOpen(!isOpen) : undefined}
|
|
191
|
+
className="cursor-pointer"
|
|
176
192
|
>
|
|
177
|
-
{
|
|
178
|
-
</div
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
{trigger}
|
|
194
|
+
</div>
|
|
195
|
+
{mounted && isOpen && createPortal(
|
|
196
|
+
<div
|
|
197
|
+
ref={popoverRef}
|
|
198
|
+
className={cn(
|
|
199
|
+
popoverVariants({ size, popoverVariant }),
|
|
200
|
+
position === "left" || position === "right" ? "-translate-y-1/2" : "",
|
|
201
|
+
className
|
|
202
|
+
)}
|
|
203
|
+
style={{
|
|
204
|
+
top: portalPosition.top,
|
|
205
|
+
left: portalPosition.left,
|
|
206
|
+
transformOrigin: transformOrigin[position],
|
|
207
|
+
}}
|
|
208
|
+
onMouseEnter={handleMouseEnter}
|
|
209
|
+
onMouseLeave={handleMouseLeave}
|
|
210
|
+
>
|
|
211
|
+
{children}
|
|
212
|
+
</div>,
|
|
213
|
+
document.body
|
|
214
|
+
)}
|
|
215
|
+
</div>
|
|
216
|
+
</PopoverContext.Provider>
|
|
182
217
|
);
|
|
183
218
|
}
|
|
184
219
|
|
|
@@ -189,6 +224,15 @@ export function PopoverContent({
|
|
|
189
224
|
className?: string;
|
|
190
225
|
children: React.ReactNode;
|
|
191
226
|
}) {
|
|
192
|
-
|
|
227
|
+
const { size } = useContext(PopoverContext);
|
|
228
|
+
return (
|
|
229
|
+
<div
|
|
230
|
+
data-sonance-name="popover"
|
|
231
|
+
className={cn(popoverContentPadding[size], className)}
|
|
232
|
+
>
|
|
233
|
+
{children}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
193
236
|
}
|
|
194
237
|
|
|
238
|
+
export { popoverVariants };
|
|
@@ -1,17 +1,42 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { forwardRef, createContext, useContext, useState, useId } from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
5
|
import { cn } from "@/lib/utils";
|
|
5
6
|
|
|
6
7
|
export type RadioGroupItemState = "default" | "hover" | "focus" | "checked" | "disabled";
|
|
7
8
|
|
|
9
|
+
const radioItemVariants = cva(
|
|
10
|
+
"peer shrink-0 appearance-none rounded-full border border-border bg-input transition-all duration-150 hover:border-border-hover checked:border-primary focus:outline-none focus:ring-2 focus:ring-primary/20 focus:ring-offset-2 focus:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50",
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
xs: "h-3.5 w-3.5",
|
|
15
|
+
sm: "h-4 w-4",
|
|
16
|
+
md: "h-5 w-5",
|
|
17
|
+
lg: "h-6 w-6",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
size: "sm",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const radioIndicatorSizes = {
|
|
27
|
+
xs: "h-1.5 w-1.5",
|
|
28
|
+
sm: "h-2 w-2",
|
|
29
|
+
md: "h-2.5 w-2.5",
|
|
30
|
+
lg: "h-3 w-3",
|
|
31
|
+
};
|
|
32
|
+
|
|
8
33
|
// State styles for Storybook/Figma visualization
|
|
9
34
|
const getStateStyles = (state?: RadioGroupItemState) => {
|
|
10
35
|
if (!state || state === "default") return "";
|
|
11
36
|
|
|
12
37
|
const stateMap: Record<string, string> = {
|
|
13
38
|
hover: "border-border-hover",
|
|
14
|
-
focus: "ring-2 ring-
|
|
39
|
+
focus: "ring-2 ring-primary/20 ring-offset-2 ring-offset-background",
|
|
15
40
|
checked: "border-primary",
|
|
16
41
|
disabled: "opacity-50 cursor-not-allowed",
|
|
17
42
|
};
|
|
@@ -23,6 +48,7 @@ interface RadioGroupContextValue {
|
|
|
23
48
|
value: string;
|
|
24
49
|
onChange: (value: string) => void;
|
|
25
50
|
name: string;
|
|
51
|
+
size: "xs" | "sm" | "md" | "lg";
|
|
26
52
|
}
|
|
27
53
|
|
|
28
54
|
const RadioGroupContext = createContext<RadioGroupContextValue | null>(null);
|
|
@@ -35,6 +61,8 @@ interface RadioGroupProps {
|
|
|
35
61
|
className?: string;
|
|
36
62
|
children: React.ReactNode;
|
|
37
63
|
orientation?: "horizontal" | "vertical";
|
|
64
|
+
/** Size variant for radio items */
|
|
65
|
+
size?: "xs" | "sm" | "md" | "lg";
|
|
38
66
|
}
|
|
39
67
|
|
|
40
68
|
export function RadioGroup({
|
|
@@ -45,6 +73,7 @@ export function RadioGroup({
|
|
|
45
73
|
className,
|
|
46
74
|
children,
|
|
47
75
|
orientation = "vertical",
|
|
76
|
+
size = "sm",
|
|
48
77
|
}: RadioGroupProps) {
|
|
49
78
|
const [internalValue, setInternalValue] = useState(defaultValue);
|
|
50
79
|
const value = controlledValue ?? internalValue;
|
|
@@ -57,12 +86,12 @@ export function RadioGroup({
|
|
|
57
86
|
};
|
|
58
87
|
|
|
59
88
|
return (
|
|
60
|
-
<RadioGroupContext.Provider value={{ value, onChange, name: groupName }}>
|
|
89
|
+
<RadioGroupContext.Provider value={{ value, onChange, name: groupName, size }}>
|
|
61
90
|
<div
|
|
62
91
|
role="radiogroup"
|
|
63
92
|
className={cn(
|
|
64
93
|
"flex",
|
|
65
|
-
orientation === "vertical" ? "flex-col gap-
|
|
94
|
+
orientation === "vertical" ? "flex-col gap-2.5" : "flex-row gap-5",
|
|
66
95
|
className
|
|
67
96
|
)}
|
|
68
97
|
>
|
|
@@ -87,13 +116,13 @@ export const RadioGroupItem = forwardRef<HTMLInputElement, RadioGroupItemProps>(
|
|
|
87
116
|
throw new Error("RadioGroupItem must be used within a RadioGroup");
|
|
88
117
|
}
|
|
89
118
|
|
|
90
|
-
const { value: groupValue, onChange, name } = context;
|
|
119
|
+
const { value: groupValue, onChange, name, size } = context;
|
|
91
120
|
const inputId = id || `radio-${value}`;
|
|
92
121
|
const isChecked = groupValue === value || state === "checked";
|
|
93
122
|
const isDisabled = disabled || state === "disabled";
|
|
94
123
|
|
|
95
124
|
return (
|
|
96
|
-
<div data-sonance-name="radio-group" className="flex items-start gap-
|
|
125
|
+
<div data-sonance-name="radio-group" className="flex items-start gap-2.5">
|
|
97
126
|
<div className="relative flex items-center justify-center">
|
|
98
127
|
<input
|
|
99
128
|
type="radio"
|
|
@@ -105,11 +134,7 @@ export const RadioGroupItem = forwardRef<HTMLInputElement, RadioGroupItemProps>(
|
|
|
105
134
|
onChange={() => onChange(value)}
|
|
106
135
|
disabled={isDisabled}
|
|
107
136
|
className={cn(
|
|
108
|
-
|
|
109
|
-
"checked:border-primary",
|
|
110
|
-
"focus:outline-none focus:ring-2 focus:ring-border-focus focus:ring-offset-2 focus:ring-offset-background",
|
|
111
|
-
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
112
|
-
"transition-colors duration-150",
|
|
137
|
+
radioItemVariants({ size }),
|
|
113
138
|
getStateStyles(state),
|
|
114
139
|
className
|
|
115
140
|
)}
|
|
@@ -117,8 +142,9 @@ export const RadioGroupItem = forwardRef<HTMLInputElement, RadioGroupItemProps>(
|
|
|
117
142
|
/>
|
|
118
143
|
<div data-sonance-name="radio-group"
|
|
119
144
|
className={cn(
|
|
120
|
-
"pointer-events-none absolute
|
|
121
|
-
"scale-0 transition-
|
|
145
|
+
"pointer-events-none absolute rounded-full bg-primary",
|
|
146
|
+
"scale-0 transition-all duration-150",
|
|
147
|
+
radioIndicatorSizes[size],
|
|
122
148
|
isChecked && "scale-100"
|
|
123
149
|
)}
|
|
124
150
|
/>
|
|
@@ -145,3 +171,5 @@ export const RadioGroupItem = forwardRef<HTMLInputElement, RadioGroupItemProps>(
|
|
|
145
171
|
|
|
146
172
|
RadioGroupItem.displayName = "RadioGroupItem";
|
|
147
173
|
|
|
174
|
+
export { radioItemVariants };
|
|
175
|
+
|
|
@@ -2,19 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
import { forwardRef, useState, useRef, useEffect } from "react";
|
|
4
4
|
import { ChevronDown, Check } from "lucide-react";
|
|
5
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
5
6
|
import { cn } from "@/lib/utils";
|
|
6
7
|
|
|
7
8
|
export type SelectState = "default" | "hover" | "focus" | "open" | "error" | "disabled";
|
|
8
9
|
|
|
10
|
+
const selectTriggerVariants = cva(
|
|
11
|
+
"flex w-full items-center justify-between border border-input-border bg-input text-left text-foreground transition-all duration-200 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
|
|
12
|
+
{
|
|
13
|
+
variants: {
|
|
14
|
+
size: {
|
|
15
|
+
xs: "h-7 px-2.5 text-xs rounded-md",
|
|
16
|
+
sm: "h-8 px-3 text-sm rounded-lg",
|
|
17
|
+
md: "h-9 px-3.5 text-sm rounded-lg",
|
|
18
|
+
lg: "h-10 px-4 text-sm rounded-xl",
|
|
19
|
+
},
|
|
20
|
+
selectVariant: {
|
|
21
|
+
default: "hover:border-border-hover focus:border-input-focus focus:ring-2 focus:ring-primary/10",
|
|
22
|
+
glass: "bg-input/80 backdrop-blur-sm hover:border-border-hover focus:border-input-focus focus:ring-2 focus:ring-primary/20",
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
defaultVariants: {
|
|
26
|
+
size: "sm",
|
|
27
|
+
selectVariant: "default",
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
);
|
|
31
|
+
|
|
9
32
|
// State styles for Storybook/Figma visualization
|
|
10
33
|
const getStateStyles = (state?: SelectState) => {
|
|
11
34
|
if (!state || state === "default") return "";
|
|
12
35
|
|
|
13
36
|
const stateMap: Record<string, string> = {
|
|
14
37
|
hover: "border-border-hover",
|
|
15
|
-
focus: "border-input-focus",
|
|
16
|
-
open: "border-input-focus",
|
|
17
|
-
error: "border-error",
|
|
38
|
+
focus: "border-input-focus ring-2 ring-primary/10",
|
|
39
|
+
open: "border-input-focus ring-2 ring-primary/10",
|
|
40
|
+
error: "border-error ring-2 ring-error/10",
|
|
18
41
|
disabled: "opacity-50 cursor-not-allowed",
|
|
19
42
|
};
|
|
20
43
|
|
|
@@ -27,7 +50,7 @@ interface SelectOption {
|
|
|
27
50
|
disabled?: boolean;
|
|
28
51
|
}
|
|
29
52
|
|
|
30
|
-
interface SelectProps {
|
|
53
|
+
interface SelectProps extends VariantProps<typeof selectTriggerVariants> {
|
|
31
54
|
id?: string;
|
|
32
55
|
value?: string;
|
|
33
56
|
defaultValue?: string;
|
|
@@ -57,6 +80,8 @@ export function Select({
|
|
|
57
80
|
className,
|
|
58
81
|
state,
|
|
59
82
|
style,
|
|
83
|
+
size,
|
|
84
|
+
selectVariant,
|
|
60
85
|
}: SelectProps) {
|
|
61
86
|
const isDisabled = disabled || state === "disabled";
|
|
62
87
|
const hasError = error || state === "error";
|
|
@@ -101,7 +126,7 @@ export function Select({
|
|
|
101
126
|
return (
|
|
102
127
|
<div data-sonance-name="select" className={cn("w-full", className)} ref={containerRef}>
|
|
103
128
|
{label && (
|
|
104
|
-
<label className="mb-
|
|
129
|
+
<label className="mb-1.5 block text-[11px] font-medium uppercase tracking-wide text-foreground-muted">
|
|
105
130
|
{label}
|
|
106
131
|
</label>
|
|
107
132
|
)}
|
|
@@ -113,12 +138,9 @@ export function Select({
|
|
|
113
138
|
disabled={isDisabled}
|
|
114
139
|
style={style}
|
|
115
140
|
className={cn(
|
|
116
|
-
|
|
117
|
-
"
|
|
118
|
-
"
|
|
119
|
-
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
120
|
-
hasError && "border-error",
|
|
121
|
-
(isOpen || isOpenState) && "border-input-focus",
|
|
141
|
+
selectTriggerVariants({ size, selectVariant }),
|
|
142
|
+
hasError && "border-error ring-2 ring-error/10",
|
|
143
|
+
(isOpen || isOpenState) && "border-input-focus ring-2 ring-primary/10",
|
|
122
144
|
getStateStyles(state)
|
|
123
145
|
)}
|
|
124
146
|
>
|
|
@@ -127,7 +149,7 @@ export function Select({
|
|
|
127
149
|
</span>
|
|
128
150
|
<ChevronDown
|
|
129
151
|
className={cn(
|
|
130
|
-
"h-
|
|
152
|
+
"h-3.5 w-3.5 text-foreground-muted transition-transform duration-200",
|
|
131
153
|
(isOpen || isOpenState) && "rotate-180"
|
|
132
154
|
)}
|
|
133
155
|
/>
|
|
@@ -136,8 +158,9 @@ export function Select({
|
|
|
136
158
|
{isOpen && (
|
|
137
159
|
<div
|
|
138
160
|
className={cn(
|
|
139
|
-
"absolute z-50 mt-1 w-full border border-border bg-card shadow-
|
|
140
|
-
"max-h-60 overflow-auto"
|
|
161
|
+
"absolute z-50 mt-1.5 w-full rounded-xl border border-border bg-card/95 backdrop-blur-md shadow-xl",
|
|
162
|
+
"max-h-60 overflow-auto p-1",
|
|
163
|
+
"animate-in fade-in-0 zoom-in-95 duration-150"
|
|
141
164
|
)}
|
|
142
165
|
>
|
|
143
166
|
{options.map((option) => (
|
|
@@ -147,7 +170,7 @@ export function Select({
|
|
|
147
170
|
onClick={() => !option.disabled && handleSelect(option.value)}
|
|
148
171
|
disabled={option.disabled}
|
|
149
172
|
className={cn(
|
|
150
|
-
"flex w-full items-center justify-between px-
|
|
173
|
+
"flex w-full items-center justify-between px-3 py-2 text-left text-sm rounded-lg",
|
|
151
174
|
"transition-colors duration-150",
|
|
152
175
|
option.value === value
|
|
153
176
|
? "bg-primary text-primary-foreground"
|
|
@@ -156,13 +179,13 @@ export function Select({
|
|
|
156
179
|
)}
|
|
157
180
|
>
|
|
158
181
|
{option.label}
|
|
159
|
-
{option.value === value && <Check className="h-
|
|
182
|
+
{option.value === value && <Check className="h-3.5 w-3.5" />}
|
|
160
183
|
</button>
|
|
161
184
|
))}
|
|
162
185
|
</div>
|
|
163
186
|
)}
|
|
164
187
|
</div>
|
|
165
|
-
{error && <p id="p-error" className="mt-1 text-
|
|
188
|
+
{error && <p id="p-error" className="mt-1 text-xs text-error">{error}</p>}
|
|
166
189
|
</div>
|
|
167
190
|
);
|
|
168
191
|
}
|
|
@@ -184,23 +207,32 @@ const getNativeSelectStateStyles = (state?: NativeSelectState) => {
|
|
|
184
207
|
};
|
|
185
208
|
|
|
186
209
|
// Native Select for forms
|
|
187
|
-
interface NativeSelectProps extends React.SelectHTMLAttributes<HTMLSelectElement> {
|
|
210
|
+
interface NativeSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "size"> {
|
|
188
211
|
label?: string;
|
|
189
212
|
error?: string;
|
|
190
213
|
options: SelectOption[];
|
|
191
214
|
/** Visual state for Storybook/Figma documentation */
|
|
192
215
|
state?: NativeSelectState;
|
|
216
|
+
/** Size variant */
|
|
217
|
+
size?: "xs" | "sm" | "md" | "lg";
|
|
193
218
|
}
|
|
194
219
|
|
|
195
220
|
export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
196
|
-
({ className, label, error, options, state, disabled, ...props }, ref) => {
|
|
221
|
+
({ className, label, error, options, state, disabled, size = "sm", ...props }, ref) => {
|
|
197
222
|
const isDisabled = disabled || state === "disabled";
|
|
198
223
|
const hasError = error || state === "error";
|
|
199
224
|
|
|
225
|
+
const sizeClasses = {
|
|
226
|
+
xs: "h-7 px-2.5 pr-8 text-xs rounded-md",
|
|
227
|
+
sm: "h-8 px-3 pr-9 text-sm rounded-lg",
|
|
228
|
+
md: "h-9 px-3.5 pr-9 text-sm rounded-lg",
|
|
229
|
+
lg: "h-10 px-4 pr-10 text-sm rounded-xl",
|
|
230
|
+
};
|
|
231
|
+
|
|
200
232
|
return (
|
|
201
233
|
<div data-sonance-name="select" className="w-full">
|
|
202
234
|
{label && (
|
|
203
|
-
<label className="mb-
|
|
235
|
+
<label className="mb-1.5 block text-[11px] font-medium uppercase tracking-wide text-foreground-muted">
|
|
204
236
|
{label}
|
|
205
237
|
</label>
|
|
206
238
|
)}
|
|
@@ -209,11 +241,13 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
|
209
241
|
ref={ref}
|
|
210
242
|
disabled={isDisabled}
|
|
211
243
|
className={cn(
|
|
212
|
-
"w-full appearance-none border border-input-border bg-input
|
|
213
|
-
|
|
214
|
-
"
|
|
244
|
+
"w-full appearance-none border border-input-border bg-input",
|
|
245
|
+
sizeClasses[size],
|
|
246
|
+
"text-foreground transition-all duration-200",
|
|
247
|
+
"hover:border-border-hover",
|
|
248
|
+
"focus:border-input-focus focus:outline-none focus:ring-2 focus:ring-primary/10",
|
|
215
249
|
"disabled:cursor-not-allowed disabled:opacity-50",
|
|
216
|
-
hasError && "border-error",
|
|
250
|
+
hasError && "border-error ring-2 ring-error/10",
|
|
217
251
|
getNativeSelectStateStyles(state),
|
|
218
252
|
className
|
|
219
253
|
)} data-sonance-name="select"
|
|
@@ -225,9 +259,9 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
|
225
259
|
</option>
|
|
226
260
|
))}
|
|
227
261
|
</select>
|
|
228
|
-
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 h-
|
|
262
|
+
<ChevronDown className="pointer-events-none absolute right-3 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-foreground-muted" />
|
|
229
263
|
</div>
|
|
230
|
-
{error && <p id="native-select-p-error" className="mt-1 text-
|
|
264
|
+
{error && <p id="native-select-p-error" className="mt-1 text-xs text-error">{error}</p>}
|
|
231
265
|
</div>
|
|
232
266
|
);
|
|
233
267
|
}
|
|
@@ -235,3 +269,5 @@ export const NativeSelect = forwardRef<HTMLSelectElement, NativeSelectProps>(
|
|
|
235
269
|
|
|
236
270
|
NativeSelect.displayName = "NativeSelect";
|
|
237
271
|
|
|
272
|
+
export { selectTriggerVariants };
|
|
273
|
+
|
|
@@ -1,11 +1,37 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { forwardRef, useId } from "react";
|
|
4
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
4
5
|
import { cn } from "@/lib/utils";
|
|
5
6
|
|
|
6
7
|
export type SwitchState = "default" | "hover" | "focus" | "checked" | "disabled";
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
const switchTrackVariants = cva(
|
|
10
|
+
"relative inline-flex shrink-0 cursor-pointer items-center rounded-full border border-border bg-input transition-all duration-200 has-[:checked]:border-primary has-[:checked]:bg-primary has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50 has-[:focus]:ring-2 has-[:focus]:ring-primary/20 has-[:focus]:ring-offset-2 has-[:focus]:ring-offset-background",
|
|
11
|
+
{
|
|
12
|
+
variants: {
|
|
13
|
+
size: {
|
|
14
|
+
xs: "h-4 w-7",
|
|
15
|
+
sm: "h-5 w-9",
|
|
16
|
+
md: "h-6 w-11",
|
|
17
|
+
lg: "h-7 w-12",
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
defaultVariants: {
|
|
21
|
+
size: "sm",
|
|
22
|
+
},
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const switchThumbSizes = {
|
|
27
|
+
xs: { size: "h-3 w-3", translate: "peer-checked:translate-x-3" },
|
|
28
|
+
sm: { size: "h-4 w-4", translate: "peer-checked:translate-x-4" },
|
|
29
|
+
md: { size: "h-5 w-5", translate: "peer-checked:translate-x-5" },
|
|
30
|
+
lg: { size: "h-6 w-6", translate: "peer-checked:translate-x-5" },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
interface SwitchProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "type" | "size">,
|
|
34
|
+
VariantProps<typeof switchTrackVariants> {
|
|
9
35
|
label?: string;
|
|
10
36
|
description?: string;
|
|
11
37
|
/** Visual state for Storybook/Figma documentation */
|
|
@@ -18,7 +44,7 @@ const getTrackStateStyles = (state?: SwitchState) => {
|
|
|
18
44
|
|
|
19
45
|
const stateMap: Record<string, string> = {
|
|
20
46
|
hover: "border-border-hover",
|
|
21
|
-
focus: "ring-2 ring-
|
|
47
|
+
focus: "ring-2 ring-primary/20 ring-offset-2 ring-offset-background",
|
|
22
48
|
checked: "border-primary bg-primary",
|
|
23
49
|
disabled: "opacity-50 cursor-not-allowed",
|
|
24
50
|
};
|
|
@@ -27,7 +53,7 @@ const getTrackStateStyles = (state?: SwitchState) => {
|
|
|
27
53
|
};
|
|
28
54
|
|
|
29
55
|
export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
|
30
|
-
({ className, label, description, id, state, disabled, checked, defaultChecked, onChange, style, ...props }, ref) => {
|
|
56
|
+
({ className, label, description, id, state, disabled, checked, defaultChecked, onChange, style, size = "sm", ...props }, ref) => {
|
|
31
57
|
const uniqueId = useId();
|
|
32
58
|
const inputId = id || `switch-${uniqueId}`;
|
|
33
59
|
const isDisabled = disabled || state === "disabled";
|
|
@@ -36,17 +62,15 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
|
|
36
62
|
const isControlled = checked !== undefined || onChange !== undefined;
|
|
37
63
|
const isCheckedForState = state === "checked";
|
|
38
64
|
|
|
65
|
+
const thumbConfig = switchThumbSizes[size || "sm"];
|
|
66
|
+
|
|
39
67
|
return (
|
|
40
|
-
<div data-sonance-name="switch" className="flex items-start gap-
|
|
68
|
+
<div data-sonance-name="switch" className="flex items-start gap-2.5">
|
|
41
69
|
<label
|
|
42
70
|
htmlFor={inputId}
|
|
43
71
|
style={style}
|
|
44
72
|
className={cn(
|
|
45
|
-
|
|
46
|
-
"border border-border bg-input transition-colors duration-200",
|
|
47
|
-
"has-[:checked]:border-primary has-[:checked]:bg-primary",
|
|
48
|
-
"has-[:disabled]:cursor-not-allowed has-[:disabled]:opacity-50",
|
|
49
|
-
"has-[:focus]:ring-2 has-[:focus]:ring-border-focus has-[:focus]:ring-offset-2 has-[:focus]:ring-offset-background",
|
|
73
|
+
switchTrackVariants({ size }),
|
|
50
74
|
getTrackStateStyles(state),
|
|
51
75
|
className
|
|
52
76
|
)}
|
|
@@ -66,10 +90,12 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
|
|
66
90
|
<span
|
|
67
91
|
id="switch-span"
|
|
68
92
|
className={cn(
|
|
69
|
-
"pointer-events-none absolute left-0.5
|
|
70
|
-
"transition-
|
|
71
|
-
"peer-checked:
|
|
72
|
-
|
|
93
|
+
"pointer-events-none absolute left-0.5 rounded-full bg-foreground-muted shadow-sm",
|
|
94
|
+
"transition-all duration-200 ease-out",
|
|
95
|
+
"peer-checked:bg-primary-foreground",
|
|
96
|
+
thumbConfig.size,
|
|
97
|
+
thumbConfig.translate,
|
|
98
|
+
state === "checked" && "translate-x-4 bg-primary-foreground"
|
|
73
99
|
)}
|
|
74
100
|
/>
|
|
75
101
|
</label>
|
|
@@ -95,3 +121,5 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
|
|
95
121
|
|
|
96
122
|
Switch.displayName = "Switch";
|
|
97
123
|
|
|
124
|
+
export { switchTrackVariants };
|
|
125
|
+
|