Inputs

Field

Label, control and hint composed as one row.

Preview

18 digits, no spaces

Install
npx shadcn@latest add https://ui.thjsecurity.tech/r/field.json

Pulls the component into components/ui/field.tsx and its token dependencies. The app owns the copy from then on.

Dependencies
(registry) label(registry) select
Source
"use client"

import * as React from "react"

import { Label } from "@/components/ui/label"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { cn } from "@/lib/utils"

/* Form rows. A Field is a label over a control with an optional hint; a
   SelectField is a Field around the console's Select, fed a list of options.
   Forms compose these instead of repeating the label-control markup. */

export function Field({
  id,
  label,
  hint,
  className,
  children,
}: {
  id?: string
  label: React.ReactNode
  hint?: React.ReactNode
  className?: string
  children: React.ReactNode
}) {
  return (
    <div className={cn("space-y-2", className)}>
      <Label htmlFor={id}>{label}</Label>
      {children}
      {hint && <p className="text-[12px] text-paper-faint">{hint}</p>}
    </div>
  )
}

export interface Option<V extends string | number = string> {
  value: V
  label: React.ReactNode
}

/** A labelled select over a list of options; the value round-trips in its own type. */
export function SelectField<V extends string | number>({
  id,
  label,
  hint,
  value,
  options,
  onChange,
  placeholder,
  disabled,
  className,
}: {
  id: string
  label: React.ReactNode
  hint?: React.ReactNode
  value: V
  options: readonly Option<V>[]
  onChange: (value: V) => void
  placeholder?: string
  disabled?: boolean
  className?: string
}) {
  const numeric = typeof value === "number"
  return (
    <Field id={id} label={label} hint={hint} className={className}>
      <Select
        value={String(value)}
        onValueChange={(v) => onChange((numeric ? Number(v) : v) as V)}
        disabled={disabled}
      >
        <SelectTrigger id={id}>
          <SelectValue placeholder={placeholder} />
        </SelectTrigger>
        <SelectContent>
          {options.map((o) => (
            <SelectItem key={String(o.value)} value={String(o.value)}>
              {o.label}
            </SelectItem>
          ))}
        </SelectContent>
      </Select>
    </Field>
  )
}