diff --git a/packages/ui/src/v2/Select/Select.stories.tsx b/packages/ui/src/v2/Select/Select.stories.tsx index feaaecdd0..8dfb25476 100644 --- a/packages/ui/src/v2/Select/Select.stories.tsx +++ b/packages/ui/src/v2/Select/Select.stories.tsx @@ -48,6 +48,27 @@ export function Default() { ); } +export function Variants() { + return ( +
+ {(["classic", "surface", "soft", "ghost"] as const).map(variant => ( +
+ +
+ ))} +
+ ); +} + export function Controlled() { const [value, setValue] = useState(null); return ( diff --git a/packages/ui/src/v2/Select/SelectTrigger.tsx b/packages/ui/src/v2/Select/SelectTrigger.tsx index 12fbe1161..de0630ab5 100644 --- a/packages/ui/src/v2/Select/SelectTrigger.tsx +++ b/packages/ui/src/v2/Select/SelectTrigger.tsx @@ -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 ( diff --git a/packages/ui/src/v2/Select/variants.ts b/packages/ui/src/v2/Select/variants.ts index 774cc6c96..247c11f37 100644 --- a/packages/ui/src/v2/Select/variants.ts +++ b/packages/ui/src/v2/Select/variants.ts @@ -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", }, }); diff --git a/packages/ui/src/v2/form/TextField.stories.tsx b/packages/ui/src/v2/form/TextField.stories.tsx index 413a927d4..6fd892d03 100644 --- a/packages/ui/src/v2/form/TextField.stories.tsx +++ b/packages/ui/src/v2/form/TextField.stories.tsx @@ -39,6 +39,16 @@ export function WithIcon() { ); } +export function Variants() { + return ( +
+ } placeholder="Classic" /> + } placeholder="Surface" /> + } placeholder="Soft" /> +
+ ); +} + export function Controlled() { const [value, setValue] = useState(""); return ( diff --git a/packages/ui/src/v2/form/TextField.tsx b/packages/ui/src/v2/form/TextField.tsx index 0f01c71f3..2eaebbbfb 100644 --- a/packages/ui/src/v2/form/TextField.tsx +++ b/packages/ui/src/v2/form/TextField.tsx @@ -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 (
diff --git a/packages/ui/src/v2/form/variants.ts b/packages/ui/src/v2/form/variants.ts index 8f9fa0cd0..1864ee1eb 100644 --- a/packages/ui/src/v2/form/variants.ts +++ b/packages/ui/src/v2/form/variants.ts @@ -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", }, });