Add missing variants on TextField and Select

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-08 09:56:48 -04:00
parent c6e1f8e3b1
commit 1933c914ac
6 changed files with 60 additions and 7 deletions

View File

@@ -48,6 +48,27 @@ export function Default() {
);
}
export function Variants() {
return (
<div className="flex flex-col gap-3">
{(["classic", "surface", "soft", "ghost"] as const).map(variant => (
<div key={variant} className="w-40">
<Select>
<SelectTrigger variant={variant} placeholder={variant}>
{(value: string | null) => (value ? fruits[value] : null)}
</SelectTrigger>
<SelectPopup>
{Object.entries(fruits).map(([value, label]) => (
<SelectItem key={value} value={value}>{label}</SelectItem>
))}
</SelectPopup>
</Select>
</div>
))}
</div>
);
}
export function Controlled() {
const [value, setValue] = useState<string | null>(null);
return (

View File

@@ -23,6 +23,8 @@ export type SelectTriggerProps
& {
className?: string;
size?: 1 | 2;
// Surface treatment (defaults to "surface").
variant?: "classic" | "surface" | "soft" | "ghost";
// Shown when no value is selected.
placeholder?: ReactNode;
// Renders the selected value; pass a function to map a value to its label.
@@ -32,8 +34,8 @@ export type SelectTriggerProps
// The bordered surface button that opens the popup, showing the selected value
// (or placeholder) and a trailing chevron.
export function SelectTrigger(props: SelectTriggerProps) {
const { className, size, placeholder, children, ...rest } = props;
const { trigger, value, icon } = selectTrigger({ size });
const { className, size, variant, placeholder, children, ...rest } = props;
const { trigger, value, icon } = selectTrigger({ size, variant });
return (
<BaseSelect.Trigger className={trigger({ className })} {...rest}>

View File

@@ -20,8 +20,8 @@ import { tv } from "tailwind-variants/lite";
export const selectTrigger = tv({
slots: {
trigger: [
"flex w-full items-center justify-between gap-2 rounded-2 border border-sand-a7 bg-sand-1 text-2 text-sand-12",
"cursor-pointer outline-none transition-colors hover:bg-sand-2",
"flex w-full items-center justify-between gap-2 rounded-2 text-2 text-sand-12",
"cursor-pointer outline-none transition-colors",
"focus-visible:ring-2 focus-visible:ring-sand-8 focus-visible:ring-offset-1 focus-visible:ring-offset-sand-1",
"data-disabled:pointer-events-none data-disabled:opacity-50 data-placeholder:text-sand-a10",
],
@@ -33,9 +33,19 @@ export const selectTrigger = tv({
1: { trigger: "h-7 px-2" },
2: { trigger: "h-8 px-3" },
},
// Surface treatment. Only the accent (gold) color ships, matching Figma;
// classic adds a recessed inset shadow (Figma can't express its gradient),
// soft drops the border for a tint, ghost is transparent until hover.
variant: {
classic: { trigger: "border border-sand-a7 bg-sand-1 inset-shadow-2 hover:bg-sand-2" },
surface: { trigger: "border border-sand-a7 bg-sand-1 hover:bg-sand-2" },
soft: { trigger: "bg-gold-3 hover:bg-gold-4" },
ghost: { trigger: "hover:bg-sand-2" },
},
},
defaultVariants: {
size: 2,
variant: "surface",
},
});

View File

@@ -39,6 +39,16 @@ export function WithIcon() {
);
}
export function Variants() {
return (
<div className="flex w-60 flex-col gap-3">
<TextField variant="classic" icon={<MagnifyingGlassIcon />} placeholder="Classic" />
<TextField variant="surface" icon={<MagnifyingGlassIcon />} placeholder="Surface" />
<TextField variant="soft" icon={<MagnifyingGlassIcon />} placeholder="Soft" />
</div>
);
}
export function Controlled() {
const [value, setValue] = useState("");
return (

View File

@@ -23,6 +23,8 @@ export type TextFieldProps
// Applied to the bordered container (the top-level element).
className?: string;
size?: 1 | 2;
// Surface treatment (defaults to "surface").
variant?: "classic" | "surface" | "soft";
// Leading icon slot (e.g. a MagnifyingGlassIcon for search).
icon?: ReactNode;
};
@@ -31,8 +33,8 @@ export type TextFieldProps
// node; all native input props spread onto the inner control. Controlled with
// Base UI's `value` / `onValueChange` (or native `value` / `onChange`).
export function TextField(props: TextFieldProps) {
const { className, size, icon, ...inputProps } = props;
const { root, icon: iconSlot, input } = textField({ size });
const { className, size, variant, icon, ...inputProps } = props;
const { root, icon: iconSlot, input } = textField({ size, variant });
return (
<div className={root({ className })}>

View File

@@ -19,7 +19,7 @@ import { tv } from "tailwind-variants/lite";
export const textField = tv({
slots: {
root: [
"flex items-center gap-2 rounded-2 border border-sand-a5 bg-sand-1 text-2 text-sand-12 transition-colors",
"flex items-center gap-2 rounded-2 text-2 text-sand-12 transition-colors",
"focus-within:ring-2 focus-within:ring-sand-8 focus-within:ring-offset-1 focus-within:ring-offset-sand-1",
"has-[input:disabled]:pointer-events-none has-[input:disabled]:opacity-50",
],
@@ -31,9 +31,17 @@ export const textField = tv({
1: { root: "h-7 px-2" },
2: { root: "h-8 px-2" },
},
// Surface treatment. Only the accent (gold) color ships, matching Figma;
// classic adds a recessed inset shadow, soft drops the border for a tint.
variant: {
classic: { root: "border border-sand-a5 bg-sand-1 inset-shadow-2" },
surface: { root: "border border-sand-a5 bg-sand-1" },
soft: { root: "bg-gold-3" },
},
},
defaultVariants: {
size: 2,
variant: "surface",
},
});