// ~/src/siteplane/components/select/select.tsx
"use client";
import { Select as SelectPrimitive } from "@base-ui/react/select";
import { useRender } from "@base-ui/react/use-render";
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
import * as React from "react";
import { useFormReset } from "../../system/form/use-form-reset";
import {
SiteplaneUIPortalScope,
useResolvedControlSize,
type SiteplaneControlSize,
} from "../../system/provider/siteplane-ui-provider";
import { ChoiceMenuItemCheck } from "../../system/styles/choice-menu-item";
import { surfaceClassNames } from "../../system/styles/surface";
import { cn, type WithStringClassName } from "../../system/utils/cn";
import { dropSlotClass, slotClass } from "../../system/utils/slot-class";
import { KitButton } from "../button/button-internal";
import { useFieldLabelPlacement } from "../field";
import { Separator } from "../separator";
import { Input } from "../input";
function SelectCaretIcon(
props: React.ComponentProps<"svg">,
): React.ReactElement {
return (
);
}
export function Select(
props: SelectPrimitive.Root.Props,
): React.ReactElement {
const [resetKey, setResetKey] = React.useState(0);
const markerRef = useFormReset(props.form, () => {
if (props.value === undefined) setResetKey((current) => current + 1);
});
return (
<>
>
);
}
export type SelectTriggerSize = SiteplaneControlSize;
/** The trigger surface, the same four as the other form controls. */
export type SelectTriggerMode = "default" | "elevated" | "ghost" | "inset";
export interface SelectButtonProps extends useRender.ComponentProps<"button"> {
size?: SelectTriggerSize;
variant?: SelectTriggerMode;
}
export function SelectButton({
className,
size,
variant,
render,
children,
...props
}: SelectButtonProps): React.ReactElement {
const resolvedSize = useResolvedControlSize(size);
const resolvedMode = variant ?? "default";
return (
<>
{children}
>
);
}
export function SelectTrigger({
className,
size,
variant,
children,
render,
...props
}: WithStringClassName & {
size?: SelectTriggerSize | undefined;
variant?: SelectTriggerMode | undefined;
}): React.ReactElement {
const resolvedSize = useResolvedControlSize(size);
const resolvedMode = variant ?? "default";
// A floating Field lays out the trigger it holds (design-rework.css,
// "Floating label fields"); the ghost mode draws no field to hold a label.
const floating =
useFieldLabelPlacement() === "floating" && resolvedMode !== "ghost";
return (
)
}
>
{children}
);
}
export function SelectValue({
className,
...props
}: WithStringClassName): React.ReactElement {
return (
);
}
export function SelectPopup({
className,
children,
side = "bottom",
// 8 px, measured across the reference's selects; action and filter menus
// keep 4 px.
sideOffset = 8,
align = "start",
alignOffset = 0,
alignItemWithTrigger = false,
anchor,
portalProps,
...props
}: WithStringClassName & {
portalProps?: WithStringClassName;
side?: SelectPrimitive.Positioner.Props["side"];
sideOffset?: SelectPrimitive.Positioner.Props["sideOffset"];
align?: SelectPrimitive.Positioner.Props["align"];
alignOffset?: SelectPrimitive.Positioner.Props["alignOffset"];
alignItemWithTrigger?: SelectPrimitive.Positioner.Props["alignItemWithTrigger"];
anchor?: SelectPrimitive.Positioner.Props["anchor"];
}): React.ReactElement {
return (
{/* The scroll arrows are strips inside the panel, with the fade
into the list on their ::before (design-rework.css). */}
{/* The panel: its surface rule names this element alone, so the
arrows beside it keep their own look (design-rework.css). */}
{children}
);
}
export function SelectItem({
className,
children,
...props
}: WithStringClassName): React.ReactElement {
return (
{/* The check leads the row, in a slot every option keeps, so every
label starts at the same x, as in Combobox and the menus (I8). */}
{children}
);
}
export function SelectSeparator({
className,
...props
}: WithStringClassName): React.ReactElement {
return (
}
{...props}
/>
);
}
export function SelectGroup(
props: WithStringClassName,
): React.ReactElement {
return (
);
}
/**
* The label of a Select. Like FieldLabel and Label it carries no outer
* margin: the stack around it spaces it, as a Field's gap does (S10). Its
* colour and disabled state are the labels' shared rules (design-rework.css).
*/
export function SelectLabel({
className,
...props
}: WithStringClassName): React.ReactElement {
return (
);
}
export function SelectGroupLabel({
className,
...props
}: WithStringClassName): React.ReactElement {
return (
);
}
// SelectContent is the shadcn name of SelectPopup.
export { SelectPrimitive, SelectPopup as SelectContent };
// ~/src/siteplane/components/select/index.ts
export * from "./select";