الانتقال إلى المحتوى

حقل

مساحات التسميات وعناصر التحكم ونصوص المساعدة ورسائل الخطأ الخاصة بلوحات الإعدادات ونماذج الدفع وعمليات التسجيل — مع تصميم النماذج بأسلوب «النيوبروتاليستي».

import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import {

يجمع هذا الحقل بين تسمية وعنصر تحكم ونص مساعد ورسالة خطأ في كتلة واحدة مُحاذاة، ويمكن توسيعه ليشمل أقسامًا مجمعة باستخدام FieldGroup وFieldSet. ولا يوجد أي عنصر أساسي تحته — حيث تتكون دلالات fieldset، وlegend وrole="group" المركبة مع Label وSeparator، والمصممة وفقًا لنهج «النيوبروتاليةي»: خط عناوين غامق في التوضيحات والتسميات، بينما تتميز عناصر التحكم المُجمَّعة بحدود سميكة وظلال قوية.

استخدمه عندما:

  • صفحات الإعدادات — قم بتكديس أقسام الملف الشخصي والإشعارات والفوترة باستخدام توضيحات FieldSet وFieldSeparator بين المجموعات.
  • نماذج الدفع والتسجيل — حافظ على اتساق محاذاة التسميات والأوصاف والأخطاء عبر حقول الإدخال وقوائم الاختيار ومفاتيح التبديل.
  • بطاقات الاختيار — قم بتضمين أزرار الاختيار أو مربعات الاختيار أو مفاتيح التبديل في بطاقات FieldLabel القابلة للتحديد لاختيار الخطط ولوحات التفضيلات.

التثبيت

pnpm dlx shadcn@latest add https://neobrutalism.com/r/base/field.json
npx shadcn@latest add https://neobrutalism.com/r/base/field.json
yarn dlx shadcn@latest add https://neobrutalism.com/r/base/field.json
bunx --bun shadcn@latest add https://neobrutalism.com/r/base/field.json

انسخ والصق الكود التالي في مشروعك.

components/ui/field.tsx
"use client"

import { useMemo } from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
import { Separator } from "@/components/ui/separator"

function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
  return (
    <fieldset
      data-slot="field-set"
      className={cn(
        "flex flex-col gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3",
        className
      )}
      {...props}
    />
  )
}

function FieldLegend({
  className,
  variant = "legend",
  ...props
}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) {
  return (
    <legend
      data-slot="field-legend"
      data-variant={variant}
      className={cn(
        "mb-1.5 font-head font-medium data-[variant=label]:text-sm data-[variant=legend]:text-base",
        className
      )}
      {...props}
    />
  )
}

function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="field-group"
      className={cn(
        "group/field-group @container/field-group flex w-full flex-col gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4",
        className
      )}
      {...props}
    />
  )
}

const fieldVariants = cva(
  "group/field flex w-full gap-2 data-[invalid=true]:text-destructive",
  {
    variants: {
      orientation: {
        vertical: "flex-col *:w-full [&>.sr-only]:w-auto",
        horizontal:
          "flex-row items-center has-[>[data-slot=field-content]]:items-start *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
        responsive:
          "flex-col *:w-full @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:*:data-[slot=field-label]:flex-auto [&>.sr-only]:w-auto @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px",
      },
    },
    defaultVariants: {
      orientation: "vertical",
    },
  }
)

function Field({
  className,
  orientation = "vertical",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) {
  return (
    <div
      role="group"
      data-slot="field"
      data-orientation={orientation}
      className={cn(fieldVariants({ orientation }), className)}
      {...props}
    />
  )
}

function FieldContent({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="field-content"
      className={cn(
        "group/field-content flex flex-1 flex-col gap-0.5 leading-snug",
        className
      )}
      {...props}
    />
  )
}

function FieldLabel({
  className,
  ...props
}: React.ComponentProps<typeof Label>) {
  return (
    <Label
      data-slot="field-label"
      className={cn(
        "group/field-label peer/field-label flex w-fit gap-2 leading-snug group-data-[disabled=true]/field:opacity-50 has-data-checked:border-primary/30 has-data-checked:bg-primary/5 has-[>[data-slot=field]]:rounded has-[>[data-slot=field]]:border-2 *:data-[slot=field]:p-2.5 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10",
        "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col",
        className
      )}
      {...props}
    />
  )
}

function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="field-label"
      className={cn(
        "flex w-fit items-center gap-2 text-sm font-head font-medium group-data-[disabled=true]/field:opacity-50",
        className
      )}
      {...props}
    />
  )
}

function FieldDescription({ className, ...props }: React.ComponentProps<"p">) {
  return (
    <p
      data-slot="field-description"
      className={cn(
        "text-left text-sm leading-normal font-normal text-muted-foreground group-has-data-horizontal/field:text-balance [[data-variant=legend]+&]:-mt-1.5",
        "last:mt-0 nth-last-2:-mt-1",
        "[&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary",
        className
      )}
      {...props}
    />
  )
}

function FieldSeparator({
  children,
  className,
  ...props
}: React.ComponentProps<"div"> & {
  children?: React.ReactNode
}) {
  return (
    <div
      data-slot="field-separator"
      data-content={!!children}
      className={cn(
        "relative -my-2 h-5 text-sm group-data-[variant=outline]/field-group:-mb-2",
        className
      )}
      {...props}
    >
      <Separator className="absolute inset-0 top-1/2" />
      {children && (
        <span
          className="relative mx-auto block w-fit bg-background px-2 text-muted-foreground"
          data-slot="field-separator-content"
        >
          {children}
        </span>
      )}
    </div>
  )
}

function FieldError({
  className,
  children,
  errors,
  ...props
}: React.ComponentProps<"div"> & {
  errors?: Array<{ message?: string } | undefined>
}) {
  const content = useMemo(() => {
    if (children) {
      return children
    }

    if (!errors?.length) {
      return null
    }

    const uniqueErrors = [
      ...new Map(errors.map((error) => [error?.message, error])).values(),
    ]

    if (uniqueErrors?.length == 1) {
      return uniqueErrors[0]?.message
    }

    return (
      <ul className="ml-4 flex list-disc flex-col gap-1">
        {uniqueErrors.map(
          (error, index) =>
            error?.message && <li key={index}>{error.message}</li>
        )}
      </ul>
    )
  }, [children, errors])

  if (!content) {
    return null
  }

  return (
    <div
      role="alert"
      data-slot="field-error"
      className={cn("text-sm font-normal text-destructive", className)}
      {...props}
    >
      {content}
    </div>
  )
}

export {
  Field,
  FieldLabel,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLegend,
  FieldSeparator,
  FieldSet,
  FieldContent,
  FieldTitle,
}

حدِّث مسارات الاستيراد لتطابق إعداد مشروعك.

الاستخدام

import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldLabel,
  FieldLegend,
  FieldSeparator,
  FieldSet,
  FieldTitle,
} from "@/components/ui/field"
<FieldSet>
  <FieldLegend>Profile</FieldLegend>
  <FieldDescription>This appears on invoices and emails.</FieldDescription>
  <FieldGroup>
    <Field>
      <FieldLabel htmlFor="name">Full name</FieldLabel>
      <Input id="name" autoComplete="off" placeholder="Evil Rabbit" />
      <FieldDescription>This appears on invoices and emails.</FieldDescription>
    </Field>
    <Field>
      <FieldLabel htmlFor="username">Username</FieldLabel>
      <Input id="username" autoComplete="off" aria-invalid />
      <FieldError>Choose another username.</FieldError>
    </Field>
    <Field orientation="horizontal">
      <Switch id="newsletter" />
      <FieldLabel htmlFor="newsletter">Subscribe to the newsletter</FieldLabel>
    </Field>
  </FieldGroup>
</FieldSet>

التركيب

Field

عنصر تحكم واحد مع تسمية ونص مساعدة وتحقّق.

Field
├── FieldLabel
├── Input / Textarea / Switch / Select
├── FieldDescription
└── FieldError

FieldGroup

حقول مرتبطة في مجموعة واحدة. استخدم FieldSeparator بين الأقسام عند الحاجة.

FieldGroup
├── Field
│   ├── FieldLabel
│   ├── Input / Textarea / Switch / Select
│   ├── FieldDescription
│   └── FieldError
├── FieldSeparator
└── Field
    ├── FieldLabel
    └── Input / Textarea / Switch / Select

FieldSet

تجميع دلالي مع عنوان (legend) ووصف، يحتوي عادةً على FieldGroup.

FieldSet
├── FieldLegend
├── FieldDescription
└── FieldGroup
    ├── Field
    │   ├── FieldLabel
    │   ├── Input / Textarea / Switch / Select
    │   ├── FieldDescription
    │   └── FieldError
    └── Field
        ├── FieldLabel
        └── Input / Textarea / Switch / Select

التشريح

عائلة Field مصمّمة لتكوين نماذج سهلة الوصول. يُبنى الحقل النموذجي على النحو التالي:

<Field>
  <FieldLabel htmlFor="input-id">Label</FieldLabel>
  {/* Input, Select, Switch, etc. */}
  <FieldDescription>Optional helper text.</FieldDescription>
  <FieldError>Validation message.</FieldError>
</Field>
  • Field هو الغلاف الأساسي لحقل واحد.
  • FieldContent هو عمود flex يجمع التسمية والوصف. غير مطلوب إذا لم يكن لديك وصف.
  • غلّف الحقول المرتبطة بـ FieldGroup، واستخدم FieldSet مع FieldLegend للتجميع الدلالي.

النموذج

راجع وثائق Form لبناء النماذج باستخدام مكوّن Field ومع React Hook Form أو Tanstack Form أو Formisch.

أمثلة

Input

import {
  Field,
  FieldDescription,

Textarea

import {
  Field,
  FieldDescription,

Select

import {
  Field,
  FieldDescription,

Slider

"use client"

import * as React from "react"

Fieldset

import {
  Field,
  FieldDescription,

Checkbox

import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,

Radio

import {
  Field,
  FieldDescription,

Switch

import { Field, FieldLabel } from "@/components/ui/field"
import { Switch } from "@/components/ui/switch"

بطاقة اختيار

غلّف مكوّنات Field داخل FieldLabel لإنشاء مجموعات حقول قابلة للاختيار. يعمل هذا مع مكوّنات RadioItem وCheckbox وSwitch.

import {
  Field,
  FieldContent,

مجموعة حقول

كدّس مكوّنات Field باستخدام FieldGroup. أضف FieldSeparator للفصل بينها.

import { Checkbox } from "@/components/ui/checkbox"
import {
  Field,

RTL

لتفعيل RTL في Neobrutalism، راجع دليل إعداد RTL.

"use client"

import * as React from "react"

التخطيط المتجاوب

  • الحقول العمودية: الاتجاه الافتراضي يكدّس التسمية وعنصر التحكم ونص المساعدة — مثالي للتخطيطات التي تُصمَّم للأجهزة المحمولة أولًا.
  • الحقول الأفقية: اضبط orientation="horizontal" على Field لمحاذاة التسمية وعنصر التحكم جنبًا إلى جنب. اقرنها بـ FieldContent للحفاظ على محاذاة الأوصاف.
  • الحقول المتجاوبة: اضبط orientation="responsive" للحصول على تخطيطات أعمدة تلقائية داخل العناصر الأصل المدركة للحاوية. طبِّق فئات @container/field-group على FieldGroup لتبديل الاتجاهات عند نقاط توقّف محدّدة.
import { Button } from "@/components/ui/button"
import {
  Field,

التحقق والأخطاء

  • أضف data-invalid إلى Field لتحويل الكتلة بأكملها إلى حالة خطأ.
  • أضف aria-invalid على الإدخال نفسه من أجل التقنيات المساعِدة.
  • اعرض FieldError مباشرةً بعد عنصر التحكم أو داخل FieldContent للحفاظ على محاذاة رسائل الخطأ مع الحقل.
<Field data-invalid>
  <FieldLabel htmlFor="email">Email</FieldLabel>
  <Input id="email" type="email" aria-invalid />
  <FieldError>Enter a valid email address.</FieldError>
</Field>

إمكانية الوصول

  • FieldSet وFieldLegend يُبقيان عناصر التحكم المرتبطة مجمّعة لمستخدمي لوحة المفاتيح والتقنيات المساعِدة.
  • يُخرج Field القيمة role="group" بحيث ترث عناصر التحكم المتداخلة التسمية من FieldLabel وFieldLegend عند دمجها.
  • طبِّق FieldSeparator باعتدال لضمان أن يواجه قارئو الشاشة حدود أقسام واضحة.

إمكانية الوصول

تتبع عائلة الحقول نهج «الدلالة أولاً»: يعرض FieldSet و FieldLegend fieldset/legend الأصليين، ويعرض Field role="group"، ويعرض FieldError role="alert"، بحيث يتم الإعلان عن رسائل التحقق من الصحة فور ظهورها.

  • قم بإقران كل FieldLabel بعنصر التحكم الخاص به عبر htmlFor/id — يؤدي النقر على التسمية إلى تركيز عنصر التحكم، وتقوم برامج قراءة الشاشة بالإعلان عن الارتباط.
  • في حالة الإدخال غير الصحيح، قم بتعيين aria-invalid على عنصر التحكم نفسه؛ أما data-invalid على Field فيقوم فقط بتحديد نمط الخطأ.
  • سلوك لوحة المفاتيح يخص عناصر التحكم الموجودة بالداخل — لا تضيف العناصر المغلقة أي نقاط توقف للتركيز أو ارتباطات مفاتيح خاصة بها.

مرجع API

FieldSet

حاوية تعرض fieldset دلاليًا مع إعدادات تباعد مسبقة.

PropTypeDefault
classNamestring
<FieldSet>
  <FieldLegend>Delivery</FieldLegend>
  <FieldGroup>{/* Fields */}</FieldGroup>
</FieldSet>

FieldLegend

عنصر legend لـ FieldSet. بدِّل إلى النمط label للتوافق مع حجم التسمية.

PropTypeDefault
variant"legend" | "label""legend"
classNamestring
<FieldLegend variant="label">Notification Preferences</FieldLegend>

يحتوي FieldLegend على نمطين: legend وlabel. يطبِّق النمط label حجم التسمية ومحاذاتها. مفيد إذا كان لديك FieldSet متداخل.

FieldGroup

غلاف تخطيط يكدّس مكوّنات Field ويُفعّل استعلامات الحاوية للاتجاهات المتجاوبة.

PropTypeDefault
classNamestring
<FieldGroup className="@container/field-group flex flex-col gap-6">
  <Field>{/* ... */}</Field>
  <Field>{/* ... */}</Field>
</FieldGroup>

Field

الغلاف الأساسي لحقل واحد. يوفّر التحكم في الاتجاه وتنسيق الحالة غير الصالحة والتباعد.

PropTypeDefault
orientation"vertical" | "horizontal" | "responsive""vertical"
classNamestring
data-invalidboolean
<Field orientation="horizontal">
  <FieldLabel htmlFor="remember">Remember me</FieldLabel>
  <Switch id="remember" />
</Field>

FieldContent

عمود flex يجمع عنصر التحكم والأوصاف عندما تكون التسمية بجانب عنصر التحكم. غير مطلوب إذا لم يكن لديك وصف.

PropTypeDefault
classNamestring
<Field>
  <Checkbox id="notifications" />
  <FieldContent>
    <FieldLabel htmlFor="notifications">Notifications</FieldLabel>
    <FieldDescription>Email, SMS, and push options.</FieldDescription>
  </FieldContent>
</Field>

FieldLabel

تسمية مصمّمة لكل من عناصر الإدخال المباشرة وعناصر Field الأبناء المتداخلة.

PropTypeDefault
classNamestring
asChildbooleanfalse
<FieldLabel htmlFor="email">Email</FieldLabel>

FieldTitle

يعرض عنوانًا بتنسيق تسمية داخل FieldContent.

PropTypeDefault
classNamestring
<FieldContent>
  <FieldTitle>Enable Touch ID</FieldTitle>
  <FieldDescription>Unlock your device faster.</FieldDescription>
</FieldContent>

FieldDescription

فتحة نص مساعدة توازن تلقائيًا الأسطر الطويلة في التخطيطات الأفقية.

PropTypeDefault
classNamestring
<FieldDescription>We never share your email with anyone.</FieldDescription>

FieldSeparator

فاصل بصري لفصل الأقسام داخل FieldGroup. يقبل محتوى مضمّنًا اختياريًا.

PropTypeDefault
classNamestring
<FieldSeparator>Or continue with</FieldSeparator>

FieldError

حاوية أخطاء سهلة الوصول تقبل عناصر أبناء أو مصفوفة errors (مثلًا من react-hook-form).

PropTypeDefault
errorsArray<{ message?: string } | undefined>
classNamestring
<FieldError errors={errors.username} />

عندما تحتوي مصفوفة errors على رسائل متعدّدة، يعرض المكوّن قائمة تلقائيًا.

يقبل FieldError أيضًا المشكلات (issues) التي يُنتجها أي أداة تحقّق تُطبّق Standard Schema، بما في ذلك Zod وValibot وArkType. مرِّر مصفوفة issues من نتيجة المخطّط مباشرةً لعرض قائمة أخطاء موحّدة بين المكتبات.