Files
zguiyang bbb2f41591 feat: add new OpenSpec skills for change management and onboarding
- Created `openspec-ff-change` skill for fast-forward artifact creation.
- Introduced `openspec-new-change` skill for structured change creation.
- Developed `openspec-onboard` skill for guided onboarding through OpenSpec workflow.
- Added `openspec-sync-specs` skill for syncing delta specs to main specs.
- Implemented `openspec-verify-change` skill for verifying implementation against change artifacts.
- Updated `.gitignore` to exclude OpenSpec generated files.
- Added `skills-lock.json` to manage skill dependencies.
2026-03-13 13:18:03 +08:00

4.7 KiB
Raw Permalink Blame History

Forms & Inputs

Contents

  • Forms use FieldGroup + Field
  • InputGroup requires InputGroupInput/InputGroupTextarea
  • Buttons inside inputs use InputGroup + InputGroupAddon
  • Option sets (27 choices) use ToggleGroup
  • FieldSet + FieldLegend for grouping related fields
  • Field validation and disabled states

Forms use FieldGroup + Field

Always use FieldGroup + Field — never raw div with space-y-*:

<FieldGroup>
  <Field>
    <FieldLabel htmlFor="email">Email</FieldLabel>
    <Input id="email" type="email" />
  </Field>
  <Field>
    <FieldLabel htmlFor="password">Password</FieldLabel>
    <Input id="password" type="password" />
  </Field>
</FieldGroup>

Use Field orientation="horizontal" for settings pages. Use FieldLabel className="sr-only" for visually hidden labels.

Choosing form controls:

  • Simple text input → Input
  • Dropdown with predefined options → Select
  • Searchable dropdown → Combobox
  • Native HTML select (no JS) → native-select
  • Boolean toggle → Switch (for settings) or Checkbox (for forms)
  • Single choice from few options → RadioGroup
  • Toggle between 25 options → ToggleGroup + ToggleGroupItem
  • OTP/verification code → InputOTP
  • Multi-line text → Textarea

InputGroup requires InputGroupInput/InputGroupTextarea

Never use raw Input or Textarea inside an InputGroup.

Incorrect:

<InputGroup>
  <Input placeholder="Search..." />
</InputGroup>

Correct:

import { InputGroup, InputGroupInput } from "@/components/ui/input-group"

<InputGroup>
  <InputGroupInput placeholder="Search..." />
</InputGroup>

Buttons inside inputs use InputGroup + InputGroupAddon

Never place a Button directly inside or adjacent to an Input with custom positioning.

Incorrect:

<div className="relative">
  <Input placeholder="Search..." className="pr-10" />
  <Button className="absolute right-0 top-0" size="icon">
    <SearchIcon />
  </Button>
</div>

Correct:

import { InputGroup, InputGroupInput, InputGroupAddon } from "@/components/ui/input-group"

<InputGroup>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon>
    <Button size="icon">
      <SearchIcon data-icon="inline-start" />
    </Button>
  </InputGroupAddon>
</InputGroup>

Option sets (27 choices) use ToggleGroup

Don't manually loop Button components with active state.

Incorrect:

const [selected, setSelected] = useState("daily")

<div className="flex gap-2">
  {["daily", "weekly", "monthly"].map((option) => (
    <Button
      key={option}
      variant={selected === option ? "default" : "outline"}
      onClick={() => setSelected(option)}
    >
      {option}
    </Button>
  ))}
</div>

Correct:

import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"

<ToggleGroup spacing={2}>
  <ToggleGroupItem value="daily">Daily</ToggleGroupItem>
  <ToggleGroupItem value="weekly">Weekly</ToggleGroupItem>
  <ToggleGroupItem value="monthly">Monthly</ToggleGroupItem>
</ToggleGroup>

Combine with Field for labelled toggle groups:

<Field orientation="horizontal">
  <FieldTitle id="theme-label">Theme</FieldTitle>
  <ToggleGroup aria-labelledby="theme-label" spacing={2}>
    <ToggleGroupItem value="light">Light</ToggleGroupItem>
    <ToggleGroupItem value="dark">Dark</ToggleGroupItem>
    <ToggleGroupItem value="system">System</ToggleGroupItem>
  </ToggleGroup>
</Field>

Note: defaultValue and type/multiple props differ between base and radix. See base-vs-radix.md.


Use FieldSet + FieldLegend for related checkboxes, radios, or switches — not div with a heading:

<FieldSet>
  <FieldLegend variant="label">Preferences</FieldLegend>
  <FieldDescription>Select all that apply.</FieldDescription>
  <FieldGroup className="gap-3">
    <Field orientation="horizontal">
      <Checkbox id="dark" />
      <FieldLabel htmlFor="dark" className="font-normal">Dark mode</FieldLabel>
    </Field>
  </FieldGroup>
</FieldSet>

Field validation and disabled states

Both attributes are needed — data-invalid/data-disabled styles the field (label, description), while aria-invalid/disabled styles the control.

// Invalid.
<Field data-invalid>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" aria-invalid />
  <FieldDescription>Invalid email address.</FieldDescription>
</Field>

// Disabled.
<Field data-disabled>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" disabled />
</Field>

Works for all controls: Input, Textarea, Select, Checkbox, RadioGroupItem, Switch, Slider, NativeSelect, InputOTP.