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

مجموعة إدخال

أضف الرموز والأزرار وتلميحات لوحة المفاتيح إلى حقول الإدخال الخاصة بأشرطة البحث وحقول عناوين URL وأدوات كتابة الرسائل في الدردشة — مع حدود وظلال على الطراز «النيوبروتاليستي».

import { Search } from "lucide-react"

import {

تقوم مجموعة «الإدخال» بتجميع حقل إدخال أو منطقة نص مع ملحقاتها — مثل الرموز، والنص المضمن، والأزرار، وتلميحات لوحة المفاتيح — داخل عنصر تحكم واحد محاط بإطار. وهي تركيبة داخلية من عناصر Input وTextarea وButton الخاصة بهذه المكتبة، مصممة وفقًا لأسلوب «النيوبروتاليةي»: حدود سميكة، وظلال حادة، وخط عريض.

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

  • حقول البحث — أيقونة بحث في المقدمة بالإضافة إلى تلميح ⌘K أو زر مسح في الخلف.
  • حقول الإدخال ذات البادئة واللاحقة — نص ثابت من نوع https:// أو @ أو .com بحيث يكتب المستخدمون الجزء المتغير فقط.
  • أدوات إنشاء الدردشات والمطالبات — منطقة نصية مع شريط أدوات block-end يحتوي على أزرار وإجراء إرسال.

التثبيت

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

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

components/ui/input-group.tsx
"use client"

import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"

function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
  return (
    <div
      data-slot="input-group"
      role="group"
      className={cn(
        "group/input-group relative flex h-8 w-full min-w-0 items-center rounded border-2 bg-input shadow-sm transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-disabled:bg-input/50 has-disabled:opacity-50 has-[[data-slot=input-group-control]:focus-visible]:outline-2 has-[[data-slot=input-group-control]:focus-visible]:outline-offset-2 has-[[data-slot=input-group-control]:focus-visible]:outline-primary has-[[data-slot][aria-invalid=true]]:border-destructive has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5",
        className
      )}
      {...props}
    />
  )
}

const inputGroupAddonVariants = cva(
  "flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 [&>kbd]:rounded-[calc(var(--radius)-5px)] [&>svg:not([class*='size-'])]:size-4",
  {
    variants: {
      align: {
        "inline-start":
          "order-first pl-2 has-[>button]:ml-[-0.3rem] has-[>kbd]:ml-[-0.15rem]",
        "inline-end":
          "order-last pr-2 has-[>button]:mr-[-0.3rem] has-[>kbd]:mr-[-0.15rem]",
        "block-start":
          "order-first w-full justify-start px-2.5 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2",
        "block-end":
          "order-last w-full justify-start px-2.5 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2",
      },
    },
    defaultVariants: {
      align: "inline-start",
    },
  }
)

function InputGroupAddon({
  className,
  align = "inline-start",
  ...props
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
  return (
    <div
      role="group"
      data-slot="input-group-addon"
      data-align={align}
      className={cn(inputGroupAddonVariants({ align }), className)}
      onClick={(e) => {
        if ((e.target as HTMLElement).closest("button")) {
          return
        }
        e.currentTarget.parentElement?.querySelector("input")?.focus()
      }}
      {...props}
    />
  )
}

const inputGroupButtonVariants = cva(
  "flex items-center gap-2 text-sm shadow-none",
  {
    variants: {
      size: {
        xs: "h-6 gap-1 rounded-[calc(var(--radius)-3px)] px-1.5 [&>svg:not([class*='size-'])]:size-3.5",
        sm: "",
        "icon-xs":
          "size-6 rounded-[calc(var(--radius)-3px)] p-0 has-[>svg]:p-0",
        "icon-sm": "size-8 p-0 has-[>svg]:p-0",
      },
    },
    defaultVariants: {
      size: "xs",
    },
  }
)

function InputGroupButton({
  className,
  type = "button",
  variant = "ghost",
  size = "xs",
  ...props
}: Omit<React.ComponentProps<typeof Button>, "size"> &
  VariantProps<typeof inputGroupButtonVariants>) {
  return (
    <Button
      type={type}
      data-size={size}
      variant={variant}
      className={cn(inputGroupButtonVariants({ size }), className)}
      {...props}
    />
  )
}

function InputGroupText({ className, ...props }: React.ComponentProps<"span">) {
  return (
    <span
      className={cn(
        "flex items-center gap-2 text-sm text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    />
  )
}

function InputGroupInput({
  className,
  ...props
}: React.ComponentProps<"input">) {
  return (
    <Input
      data-slot="input-group-control"
      className={cn(
        "flex-1 rounded-none border-0 bg-transparent shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:outline-none dark:bg-transparent dark:disabled:bg-transparent",
        className
      )}
      {...props}
    />
  )
}

function InputGroupTextarea({
  className,
  ...props
}: React.ComponentProps<"textarea">) {
  return (
    <Textarea
      data-slot="input-group-control"
      className={cn(
        "flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none outline-none focus-visible:outline-none disabled:bg-transparent aria-invalid:outline-none dark:bg-transparent dark:disabled:bg-transparent",
        className
      )}
      {...props}
    />
  )
}

export {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupText,
  InputGroupInput,
  InputGroupTextarea,
}

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

الاستخدام

import {
  InputGroup,
  InputGroupAddon,
  InputGroupButton,
  InputGroupInput,
  InputGroupText,
  InputGroupTextarea,
} from "@/components/ui/input-group"
<InputGroup>
  <InputGroupInput placeholder="Search..." />
  <InputGroupAddon>
    <SearchIcon />
  </InputGroupAddon>
</InputGroup>

التركيب

استخدم التركيب التالي لبناء InputGroup:

InputGroup
├── InputGroupInput or InputGroupTextarea
├── InputGroupAddon
├── InputGroupButton
└── InputGroupText

المحاذاة

استخدم الخاصية align على InputGroupAddon لتحديد موضع المُلحَق بالنسبة إلى حقل الإدخال.

inline-start

استخدم align="inline-start" لوضع المُلحَق في بداية حقل الإدخال. هذا هو الافتراضي.

import { SearchIcon } from "lucide-react"

import {

inline-end

استخدم align="inline-end" لوضع المُلحَق في نهاية حقل الإدخال.

import { EyeOffIcon } from "lucide-react"

import {

block-start

استخدم align="block-start" لوضع المُلحَق فوق حقل الإدخال.

import { CopyIcon, FileCodeIcon } from "lucide-react"

import {

block-end

استخدم align="block-end" لوضع المُلحَق أسفل حقل الإدخال.

import {
  Field,
  FieldDescription,

أمثلة

أيقونة

import {
  CheckIcon,
  CreditCardIcon,

نص

import {
  InputGroup,
  InputGroupAddon,

زر

"use client"

import * as React from "react"

Kbd

import { SearchIcon } from "lucide-react"

import {

قائمة منسدلة

"use client"

import { ChevronDownIcon, MoreHorizontal } from "lucide-react"

Spinner

import { LoaderIcon } from "lucide-react"

import {

Textarea

import {
  IconBrandJavascript,
  IconCopy,

إدخال مخصّص

أضف السمة data-slot="input-group-control" إلى إدخالك المخصّص للتعامل التلقائي مع حالة التركيز.

فيما يلي مثال على textarea مخصّص قابل لتغيير الحجم من مكتبة خارجية.

"use client"

import TextareaAutosize from "react-textarea-autosize"

RTL

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

"use client"

import * as React from "react"

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

لا يوجد نمط عنصر ARIA لمجموعات حقول الإدخال — حيث يعرض العنصر المُغلف role="group" بحيث تربط التقنيات المساعدة الإضافات بعنصر التحكم، كما أن الوصول عبر لوحة المفاتيح متوفر بشكل أصلي: يؤدي Tab إلى الوصول إلى حقل الإدخال وأي InputGroupButton وفقًا لترتيب DOM، ولهذا السبب يجب أن تأتي الإضافات بعد حقل الإدخال (يتم وضعها بصريًّا باستخدام align). هناك أمران يجب الانتباه إليهما: نص الملحق مثل https:// هو نص مرئي، وليس تسمية برمجية، لذا لا يزال حقل الإدخال بحاجة إلى <Label> أو aria-label؛ والأزرار التي تحتوي على أيقونة فقط تحتاج إلى aria-label — النقر على ملحق يركز على حقل الإدخال، لكن هذه ميزة مريحة للمؤشر، وليست مسارًا للوحة المفاتيح.

مرجع API

InputGroup

المكوّن الرئيسي الذي يغلّف حقول الإدخال والمُلحقات.

PropTypeDefault
classNamestring
<InputGroup>
  <InputGroupInput />
  <InputGroupAddon />
</InputGroup>

InputGroupAddon

يعرض الأيقونات أو النصوص أو الأزرار أو محتوى آخر بجانب حقول الإدخال.

PropTypeDefault
align"inline-start" | "inline-end" | "block-start" | "block-end""inline-start"
classNamestring
<InputGroupAddon align="inline-end">
  <SearchIcon />
</InputGroupAddon>

بالنسبة إلى <InputGroupInput />، استخدم المحاذاة inline-start أو inline-end. وبالنسبة إلى <InputGroupTextarea />، استخدم المحاذاة block-start أو block-end.

يمكن أن يحتوي مكوّن InputGroupAddon على عدة مكوّنات InputGroupButton وأيقونات.

<InputGroupAddon>
  <InputGroupButton>Button</InputGroupButton>
  <InputGroupButton>Button</InputGroupButton>
</InputGroupAddon>

InputGroupButton

يعرض الأزرار داخل مجموعات الإدخال.

PropTypeDefault
size"xs" | "icon-xs" | "sm" | "icon-sm""xs"
variant"default" | "destructive" | "outline" | "secondary" | "ghost" | "link""ghost"
classNamestring
<InputGroupButton>Button</InputGroupButton>
<InputGroupButton size="icon-xs" aria-label="Copy">
  <CopyIcon />
</InputGroupButton>

InputGroupInput

بديل لـ <Input /> عند بناء مجموعات الإدخال. هذا المكوّن تُطبَّق عليه أنماط مجموعة الإدخال مسبقًا ويستخدم data-slot="input-group-control" الموحّد للتعامل مع حالة التركيز.

PropTypeDefault
classNamestring

تُمرَّر جميع الخصائص الأخرى إلى مكوّن <Input /> الأساسي.

<InputGroup>
  <InputGroupInput placeholder="Enter text..." />
  <InputGroupAddon>
    <SearchIcon />
  </InputGroupAddon>
</InputGroup>

InputGroupTextarea

بديل لـ <Textarea /> عند بناء مجموعات الإدخال. هذا المكوّن تُطبَّق عليه أنماط مجموعة textarea مسبقًا ويستخدم data-slot="input-group-control" الموحّد للتعامل مع حالة التركيز.

PropTypeDefault
classNamestring

تُمرَّر جميع الخصائص الأخرى إلى مكوّن <Textarea /> الأساسي.

<InputGroup>
  <InputGroupTextarea placeholder="Enter message..." />
  <InputGroupAddon align="block-end">
    <InputGroupButton>Send</InputGroupButton>
  </InputGroupAddon>
</InputGroup>