// ~/src/siteplane/components/textarea/textarea.tsx "use client"; import { Field as FieldPrimitive } from "@base-ui/react/field"; import { mergeProps } from "@base-ui/react/merge-props"; import type * as React from "react"; import { type SiteplaneControlSize, useResolvedControlSize, } from "../../system/provider/siteplane-ui-provider"; import { formControlSurfaceClassNames, formControlTextClassNames, surfaceClassNames, type FormControlSurface, type FormControlTextSize, } from "../../system/styles/surface"; import { cn } from "../../system/utils/cn"; import { slotClass } from "../../system/utils/slot-class"; import { useFieldLabelPlacement } from "../field"; import type { KitTextareaProps } from "./textarea-internal"; import "./textarea.css"; export type TextareaProps = React.ComponentPropsWithoutRef<"textarea"> & React.RefAttributes & { size?: SiteplaneControlSize | undefined; textSize?: FormControlTextSize | undefined; variant?: FormControlSurface | undefined; unstyled?: boolean | undefined; /** * The least height as a number of text lines, fractions allowed, instead of * the minimum of the size. The textarea still grows with its content. The * native `rows` cannot do this: a textarea that sizes to its content * ignores it. */ minLines?: number | undefined; /** * The most height as a number of text lines, fractions allowed: the * textarea grows with its content up to it, then scrolls. Unset, it grows * without a limit. `minLines` wins where the two disagree. */ maxLines?: number | undefined; }; export function Textarea(ownProps: TextareaProps): React.ReactElement { // InputGroupTextarea passes the internal input-group profile through // KitTextarea (textarea-internal.ts), the same function. const { className, size, textSize, variant = "default", unstyled = false, profile = "default", insetEnd = false, minLines, maxLines, ref, ...props } = ownProps as KitTextareaProps; const resolvedSize = useResolvedControlSize(size); const resolvedTextSize = textSize ?? (resolvedSize === "lg" || resolvedSize === "xl" ? "base" : undefined); // A floating Field lays out the textarea it holds (design-rework.css, // "Floating label fields"); a textarea inside an InputGroup keeps the // group's layout. A blank placeholder lets CSS tell an empty field. const floating = useFieldLabelPlacement() === "floating" && profile !== "input-group"; const textareaProps = floating && props.placeholder === undefined ? { ...props, placeholder: " " } : props; const isLineCount = (value: number | undefined): value is number => value !== undefined && Number.isFinite(value) && value > 0; const hasMinLines = isLineCount(minLines); const hasMaxLines = isLineCount(maxLines); // The line counts are run-time values. The stylesheet turns them into // heights (design-rework.css) and keeps a height where a policy blocks the // style attribute: 2.5 lines for minLines, no limit for maxLines. const lineStyle = hasMinLines || hasMaxLines ? ({ ...(hasMinLines ? { "--textarea-min-lines": minLines } : {}), ...(hasMaxLines ? { "--textarea-max-lines": maxLines } : {}), } as React.CSSProperties) : undefined; return ( ) => (