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