Add v2 Select and TextField UI kit components
The compliance-portal Subprocessors toolbar needs a dropdown filter and a search input, which the v2 kit did not provide. Add Select (styled over Base UI's headless select) and TextField (over Base UI's input) following the v2 conventions: Base UI primitives, tailwind-variants/lite slots, flat folders, bundle-safe variants, and paired skeletons. Both map the Figma semantic colors to the Radix sand/gold token scales. Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
74
packages/ui/src/v2/Select/Select.stories.tsx
Normal file
74
packages/ui/src/v2/Select/Select.stories.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { useState } from "react";
|
||||
|
||||
import { Select } from "./Select";
|
||||
import { SelectItem } from "./SelectItem";
|
||||
import { SelectPopup } from "./SelectPopup";
|
||||
import { SelectSkeleton } from "./SelectSkeleton";
|
||||
import { SelectTrigger } from "./SelectTrigger";
|
||||
|
||||
export default {
|
||||
title: "v2/Select",
|
||||
component: Select,
|
||||
};
|
||||
|
||||
const fruits: Record<string, string> = {
|
||||
apple: "Apple",
|
||||
banana: "Banana",
|
||||
cherry: "Cherry",
|
||||
};
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<div className="w-40">
|
||||
<Select>
|
||||
<SelectTrigger placeholder="Select a fruit">
|
||||
{(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>
|
||||
);
|
||||
}
|
||||
|
||||
export function Controlled() {
|
||||
const [value, setValue] = useState<string | null>(null);
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-40">
|
||||
<Select value={value} onValueChange={setValue}>
|
||||
<SelectTrigger placeholder="All categories">
|
||||
{(current: string | null) => (current ? fruits[current] : null)}
|
||||
</SelectTrigger>
|
||||
<SelectPopup>
|
||||
{Object.entries(fruits).map(([key, label]) => (
|
||||
<SelectItem key={key} value={key}>{label}</SelectItem>
|
||||
))}
|
||||
</SelectPopup>
|
||||
</Select>
|
||||
</div>
|
||||
<span className="text-2 text-sand-11">Selected: {value ?? "none"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Skeleton() {
|
||||
return <SelectSkeleton />;
|
||||
}
|
||||
27
packages/ui/src/v2/Select/Select.tsx
Normal file
27
packages/ui/src/v2/Select/Select.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Select as BaseSelect } from "@base-ui/react/select";
|
||||
|
||||
// Root of the select (Radix "Select"). Controlled the same way as Base UI's
|
||||
// Select: `value` / `onValueChange`, or left uncontrolled. Generic over the
|
||||
// option value type. See contrib/claude/ui.md.
|
||||
export type SelectProps<Value = string, Multiple extends boolean | undefined = false>
|
||||
= BaseSelect.Root.Props<Value, Multiple>;
|
||||
|
||||
export function Select<Value = string, Multiple extends boolean | undefined = false>(
|
||||
props: SelectProps<Value, Multiple>,
|
||||
) {
|
||||
return <BaseSelect.Root {...props} />;
|
||||
}
|
||||
41
packages/ui/src/v2/Select/SelectItem.tsx
Normal file
41
packages/ui/src/v2/Select/SelectItem.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Select as BaseSelect } from "@base-ui/react/select";
|
||||
import { CheckIcon } from "@phosphor-icons/react";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { selectItem } from "./variants";
|
||||
|
||||
export type SelectItemProps
|
||||
= & Omit<ComponentProps<typeof BaseSelect.Item>, "className">
|
||||
& { className?: string };
|
||||
|
||||
// A single option. Shows a trailing check when selected. Inherits highlight
|
||||
// styling from the popup surface.
|
||||
export function SelectItem(props: SelectItemProps) {
|
||||
const { className, children, ...rest } = props;
|
||||
const { item, label, indicator } = selectItem();
|
||||
|
||||
return (
|
||||
<BaseSelect.Item className={item({ className })} {...rest}>
|
||||
<span className={label()}>
|
||||
<BaseSelect.ItemText>{children}</BaseSelect.ItemText>
|
||||
</span>
|
||||
<BaseSelect.ItemIndicator className={indicator()}>
|
||||
<CheckIcon />
|
||||
</BaseSelect.ItemIndicator>
|
||||
</BaseSelect.Item>
|
||||
);
|
||||
}
|
||||
47
packages/ui/src/v2/Select/SelectPopup.tsx
Normal file
47
packages/ui/src/v2/Select/SelectPopup.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Select as BaseSelect } from "@base-ui/react/select";
|
||||
import type { ComponentProps } from "react";
|
||||
|
||||
import { selectPopup } from "./variants";
|
||||
|
||||
export type SelectPopupProps
|
||||
= & Omit<ComponentProps<typeof BaseSelect.Popup>, "className">
|
||||
& {
|
||||
className?: string;
|
||||
// Positioner placement passthrough.
|
||||
side?: ComponentProps<typeof BaseSelect.Positioner>["side"];
|
||||
align?: ComponentProps<typeof BaseSelect.Positioner>["align"];
|
||||
sideOffset?: ComponentProps<typeof BaseSelect.Positioner>["sideOffset"];
|
||||
};
|
||||
|
||||
// Portal + positioner + styled popup holding the select items.
|
||||
export function SelectPopup(props: SelectPopupProps) {
|
||||
const {
|
||||
className, children,
|
||||
side = "bottom", align = "start", sideOffset = 4,
|
||||
...popupProps
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<BaseSelect.Portal>
|
||||
<BaseSelect.Positioner side={side} align={align} sideOffset={sideOffset}>
|
||||
<BaseSelect.Popup className={selectPopup({ className })} {...popupProps}>
|
||||
{children}
|
||||
</BaseSelect.Popup>
|
||||
</BaseSelect.Positioner>
|
||||
</BaseSelect.Portal>
|
||||
);
|
||||
}
|
||||
28
packages/ui/src/v2/Select/SelectSkeleton.tsx
Normal file
28
packages/ui/src/v2/Select/SelectSkeleton.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
import type { VariantProps } from "tailwind-variants/lite";
|
||||
|
||||
import { selectSkeleton } from "./variants";
|
||||
|
||||
export type SelectSkeletonProps = Omit<ComponentProps<"span">, "children"> & VariantProps<typeof selectSkeleton>;
|
||||
|
||||
// Loading placeholder paired with the Select trigger: a pulse block matching its
|
||||
// size and radius.
|
||||
export function SelectSkeleton(props: SelectSkeletonProps) {
|
||||
const { size, className, ...rest } = props;
|
||||
|
||||
return <span className={selectSkeleton({ size, className })} {...rest} aria-hidden />;
|
||||
}
|
||||
48
packages/ui/src/v2/Select/SelectTrigger.tsx
Normal file
48
packages/ui/src/v2/Select/SelectTrigger.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Select as BaseSelect } from "@base-ui/react/select";
|
||||
import { CaretDownIcon } from "@phosphor-icons/react";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { selectTrigger } from "./variants";
|
||||
|
||||
export type SelectTriggerProps
|
||||
= & Omit<ComponentProps<typeof BaseSelect.Trigger>, "className" | "children">
|
||||
& {
|
||||
className?: string;
|
||||
size?: 1 | 2;
|
||||
// Shown when no value is selected.
|
||||
placeholder?: ReactNode;
|
||||
// Renders the selected value; pass a function to map a value to its label.
|
||||
children?: BaseSelect.Value.Props["children"];
|
||||
};
|
||||
|
||||
// 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 });
|
||||
|
||||
return (
|
||||
<BaseSelect.Trigger className={trigger({ className })} {...rest}>
|
||||
<BaseSelect.Value className={value()} placeholder={placeholder}>
|
||||
{children}
|
||||
</BaseSelect.Value>
|
||||
<BaseSelect.Icon className={icon()}>
|
||||
<CaretDownIcon />
|
||||
</BaseSelect.Icon>
|
||||
</BaseSelect.Trigger>
|
||||
);
|
||||
}
|
||||
72
packages/ui/src/v2/Select/variants.ts
Normal file
72
packages/ui/src/v2/Select/variants.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Select (Radix "Select" over Base UI's Select). A bordered surface trigger on
|
||||
// the neutral background, with an accent-highlighted popup of items.
|
||||
|
||||
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",
|
||||
"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",
|
||||
],
|
||||
value: "min-w-0 flex-1 truncate text-left",
|
||||
icon: "flex size-4 shrink-0 items-center justify-center text-sand-a10 [&_svg]:size-4",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
1: { trigger: "h-7 px-2" },
|
||||
2: { trigger: "h-8 px-3" },
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const selectPopup = tv({
|
||||
base: [
|
||||
"max-h-(--available-height) min-w-(--anchor-width) origin-(--transform-origin) overflow-y-auto rounded-3 bg-sand-1 p-1 shadow-3 outline-none",
|
||||
"transition-[scale,opacity] duration-150 ease-out data-starting-style:scale-95 data-starting-style:opacity-0",
|
||||
"data-ending-style:scale-95 data-ending-style:opacity-0",
|
||||
],
|
||||
});
|
||||
|
||||
export const selectItem = tv({
|
||||
slots: {
|
||||
item: [
|
||||
"flex h-8 cursor-pointer items-center justify-between gap-2 rounded-2 px-3 text-2 text-sand-12 outline-none select-none",
|
||||
"data-disabled:pointer-events-none data-disabled:opacity-50 data-highlighted:bg-sand-3",
|
||||
],
|
||||
label: "min-w-0 flex-1 truncate",
|
||||
indicator: "flex size-4 shrink-0 items-center justify-center text-sand-12 [&_svg]:size-4",
|
||||
},
|
||||
});
|
||||
|
||||
export const selectSkeleton = tv({
|
||||
base: "inline-block animate-pulse rounded-2 bg-sand-3 align-middle",
|
||||
variants: {
|
||||
size: {
|
||||
1: "h-7 w-40",
|
||||
2: "h-8 w-40",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 2,
|
||||
},
|
||||
});
|
||||
56
packages/ui/src/v2/form/TextField.stories.tsx
Normal file
56
packages/ui/src/v2/form/TextField.stories.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { MagnifyingGlassIcon } from "@phosphor-icons/react";
|
||||
import { useState } from "react";
|
||||
|
||||
import { TextField } from "./TextField";
|
||||
import { TextFieldSkeleton } from "./TextFieldSkeleton";
|
||||
|
||||
export default {
|
||||
title: "v2/form/TextField",
|
||||
component: TextField,
|
||||
};
|
||||
|
||||
export function Default() {
|
||||
return (
|
||||
<div className="w-60">
|
||||
<TextField placeholder="Search..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function WithIcon() {
|
||||
return (
|
||||
<div className="w-60">
|
||||
<TextField icon={<MagnifyingGlassIcon />} placeholder="Search..." />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Controlled() {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="w-60">
|
||||
<TextField value={value} onValueChange={setValue} placeholder="Type here" />
|
||||
</div>
|
||||
<span className="text-2 text-sand-11">Value: {value || "(empty)"}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function Skeleton() {
|
||||
return <TextFieldSkeleton />;
|
||||
}
|
||||
43
packages/ui/src/v2/form/TextField.tsx
Normal file
43
packages/ui/src/v2/form/TextField.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { Input } from "@base-ui/react/input";
|
||||
import type { ComponentProps, ReactNode } from "react";
|
||||
|
||||
import { textField } from "./variants";
|
||||
|
||||
export type TextFieldProps
|
||||
= & Omit<ComponentProps<typeof Input>, "className">
|
||||
& {
|
||||
// Applied to the bordered container (the top-level element).
|
||||
className?: string;
|
||||
size?: 1 | 2;
|
||||
// Leading icon slot (e.g. a MagnifyingGlassIcon for search).
|
||||
icon?: ReactNode;
|
||||
};
|
||||
|
||||
// Single-line text input on a bordered surface. The container is the top-level
|
||||
// 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 });
|
||||
|
||||
return (
|
||||
<div className={root({ className })}>
|
||||
{icon != null && <span className={iconSlot()}>{icon}</span>}
|
||||
<Input className={input()} {...inputProps} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
28
packages/ui/src/v2/form/TextFieldSkeleton.tsx
Normal file
28
packages/ui/src/v2/form/TextFieldSkeleton.tsx
Normal file
@@ -0,0 +1,28 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import type { ComponentProps } from "react";
|
||||
import type { VariantProps } from "tailwind-variants/lite";
|
||||
|
||||
import { textFieldSkeleton } from "./variants";
|
||||
|
||||
export type TextFieldSkeletonProps = Omit<ComponentProps<"span">, "children"> & VariantProps<typeof textFieldSkeleton>;
|
||||
|
||||
// Loading placeholder paired with TextField: a pulse block matching its size and
|
||||
// radius.
|
||||
export function TextFieldSkeleton(props: TextFieldSkeletonProps) {
|
||||
const { size, className, ...rest } = props;
|
||||
|
||||
return <span className={textFieldSkeleton({ size, className })} {...rest} aria-hidden />;
|
||||
}
|
||||
51
packages/ui/src/v2/form/variants.ts
Normal file
51
packages/ui/src/v2/form/variants.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
// Copyright (c) 2026 Probo Inc <hello@probo.com>.
|
||||
//
|
||||
// Permission to use, copy, modify, and/or distribute this software for any
|
||||
// purpose with or without fee is hereby granted, provided that the above
|
||||
// copyright notice and this permission notice appear in all copies.
|
||||
//
|
||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||
// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||
// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||
// PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
import { tv } from "tailwind-variants/lite";
|
||||
|
||||
// Text input (Radix "Text Field" over Base UI's Input). A bordered surface on
|
||||
// the neutral background with an optional leading icon slot.
|
||||
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",
|
||||
"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",
|
||||
],
|
||||
icon: "flex size-4 shrink-0 items-center justify-center text-sand-a9 [&_svg]:size-4",
|
||||
input: "min-w-0 flex-1 bg-transparent text-sand-12 outline-none placeholder:text-sand-a9",
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
1: { root: "h-7 px-2" },
|
||||
2: { root: "h-8 px-2" },
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 2,
|
||||
},
|
||||
});
|
||||
|
||||
export const textFieldSkeleton = tv({
|
||||
base: "inline-block animate-pulse rounded-2 bg-sand-3 align-middle",
|
||||
variants: {
|
||||
size: {
|
||||
1: "h-7 w-60",
|
||||
2: "h-8 w-60",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 2,
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user