وسم
شريحة تسمية لموضوع، تُعرض على الخادم. تمرير onRemove يضيف زر إزالة، وهو الجزء الوحيد الذي يعمل في المتصفح.
split - الحجم لم يُقَس بعد
وسم
split - الحجم لم يُقَس بعدشريحة تسمية لموضوع، تُعرض على الخادم. تمرير onRemove يضيف زر إزالة، وهو الجزء الوحيد الذي يعمل في المتصفح.
Design systems
جرّبه
tone
size
الشيفرة
tag.tsx
import { Tag } from 'topclassui/tag'
<Tag>Design systems</Tag>الوسم العادي يبقى على الخادم، وonRemove هو ما يضيف زر الإزالة، وإخراج الوسم من القائمة مسؤوليتك.
كل الحالات، بدون تمرير الفأرة
حالتانPlainRemovable
أو استورده من الحزمة
topclassui/tag
import { Tag } from 'topclassui/tag'ثبّت مرة واحدة:npm i topclassui@beta zod
أمثلة
- لموضوع أو فئة أو كلمة مفتاحية، وعادةً عدة وسوم في صف يلتف. ولحالة مثل «مدفوع» أو «متأخر» استخدم
Badge. - لا دور ARIA لـ
Tag، فالنص هو ما يحمل المعنى، واللون وحده لا يحمله أبدًا. القيمة الافتراضية لـtoneهي neutral، فيبقى صف الوسوم هادئًا. - زر الإزالة يستدعي
onRemoveفقط. لا يمرّره مكوّن خادم إلا كإجراء خادم، ولحفظ القائمة في الحالة ضعها في مكوّن عميل. - يبقى الوسم في سطر واحد: النص العادي ينتهي بعلامة حذف، والعنصر المُمرَّر كتسمية يُقصّ عند الحافة، ويبقى زر الإزالة كاملًا دائمًا. وفي صف لا يلتف أضف
className="shrink"ليتقلّص. - للوسم المرتبط برابط ضع
tagVariants()على الرابط نفسه والتسمية فيspanخاص بها، ولا تضع رابطًا داخلTagأبدًا.
topics
Design systemsAccessibilityReactTailwindRight to leftServer Components
topics.tsx
import { Tag } from 'topclassui/tag'
const TOPICS = ['Design systems', 'Accessibility', 'React', 'Tailwind', 'Right to left', 'Server Components']
/** The docs sample: a wrapping row of topics, each at the default tone. */
export default function Demo() {
return (
<div className="flex flex-wrap gap-2">
{TOPICS.map((topic) => <Tag key={topic} tone="neutral">{topic}</Tag>)}
</div>
)
}
tones
PrimaryNeutralSuccessWarningDangerInfo
tones.tsx
import { Tag } from 'topclassui/tag'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-2">
<Tag tone="primary">Primary</Tag>
<Tag tone="neutral">Neutral</Tag>
<Tag tone="success">Success</Tag>
<Tag tone="warning">Warning</Tag>
<Tag tone="danger">Danger</Tag>
<Tag tone="info">Info</Tag>
</div>
)
}
sizes
Extra smallSmallMediumLargeExtra large
sizes.tsx
import { Tag } from 'topclassui/tag'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Tag size="xs">Extra small</Tag>
<Tag size="sm">Small</Tag>
<Tag size="md">Medium</Tag>
<Tag size="lg">Large</Tag>
<Tag size="xl">Extra large</Tag>
</div>
)
}
with-icon
VerifiedDraft
with-icon.tsx
import { checkCircle } from 'topclassui/icons/check-circle'
import { Svg } from 'topclassui/svg'
import { Tag } from 'topclassui/tag'
/** The icon is decoration, in its own flex slot; the word carries the meaning. */
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-2">
<Tag tone="success">
<Svg icon={checkCircle} />
Verified
</Tag>
<Tag>Draft</Tag>
</div>
)
}
removable
- Design systems
- Accessibility
- React
- Tailwind
- Right to left
- Server Components
removable.tsx
'use client'
import { useEffect, useRef, useState } from 'react'
import { Button } from 'topclassui/button'
import { TagRemovable } from 'topclassui/tag'
const TOPICS = ['Design systems', 'Accessibility', 'React', 'Tailwind', 'Right to left', 'Server Components']
/** TagRemovable takes a handler, so a client file renders it. The caller owns the list and takes the topic out; the x hands the focus to the
* next x, or the one before, and when the list empties the caller moves it to the button that brings it back. */
export default function Demo() {
const [topics, setTopics] = useState(TOPICS)
const restore = useRef<HTMLButtonElement>(null)
// Runs after the x's own move, so the button that brings the list back takes the focus.
useEffect(() => {
if (topics.length === 0) restore.current?.focus()
}, [topics])
return (
<div className="flex flex-col items-start gap-3">
<ul aria-label="Topics" className="flex flex-wrap gap-2">
{topics.map((topic) => (
<li key={topic} className="flex max-w-full">
<TagRemovable onRemove={() => { setTopics(topics.filter((other) => other !== topic)) }}>
{topic}
</TagRemovable>
</li>
))}
</ul>
{topics.length === 0 ? (
<Button ref={restore} variant="outline" size="sm" onClick={() => { setTopics(TOPICS) }}>Restore the topics</Button>
) : null}
</div>
)
}
removable-sizes
Extra smallSmallMediumLargeExtra large
removable-sizes.tsx
'use client'
import { useRef, useState } from 'react'
import { Button } from 'topclassui/button'
import { TagRemovable } from 'topclassui/tag'
const SIZES = ['xs', 'sm', 'md', 'lg', 'xl'] as const
const NAMES = { xs: 'Extra small', sm: 'Small', md: 'Medium', lg: 'Large', xl: 'Extra large' } as const
/** Every step of the scale with its x. A removed tag sends the focus to the button that brings it back. */
export default function Demo() {
const [shown, setShown] = useState<readonly string[]>(SIZES)
const back = useRef<HTMLButtonElement>(null)
return (
<div className="flex flex-col items-start gap-3">
<div className="flex flex-wrap items-center gap-3">
{SIZES.filter((size) => shown.includes(size)).map((size) => (
<TagRemovable key={size} size={size} tone="info" onRemove={() => { setShown(shown.filter((other) => other !== size)); back.current?.focus() }}>
{NAMES[size]}
</TagRemovable>
))}
</div>
<Button ref={back} variant="outline" size="sm" onClick={() => { setShown(SIZES) }}>Show every size</Button>
</div>
)
}
link
link.tsx
import { cn } from 'topclassui/cn'
import { FOCUS_RING, LABEL, TOUCH_TARGET } from 'topclassui/classes'
import { tagVariants } from 'topclassui/tag'
const LONG = 'Infrastructure, platform reliability and developer experience working group'
const chip = cn(tagVariants({ tone: 'info' }), FOCUS_RING, TOUCH_TARGET)
/** A linked tag: the link is the chip, so its focus ring is never clipped. */
export default function Demo() {
return (
<div className="flex flex-col gap-3">
<div className="flex flex-wrap gap-2">
<a href="#design-systems" className={chip}>Design systems</a>
<a href="#accessibility" className={chip}>Accessibility</a>
</div>
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<a href="#working-group" className={chip}><span className={LABEL}>{LONG}</span></a>
</div>
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<a href="#platform" className={chip}>{LONG}</a>
</div>
</div>
)
}
long-label
Infrastructure, platform reliability and developer experience working group
Infrastructure, platform reliability and developer experience working group
Infrastructure, platform reliability and developer experience working group
Infrastructure, platform reliability and developer experience working groupInfrastructure, platform reliability and developer experience working group
long-label.tsx
import { checkCircle } from 'topclassui/icons/check-circle'
import { Svg } from 'topclassui/svg'
import { Tag } from 'topclassui/tag'
const LONG = 'Infrastructure, platform reliability and developer experience working group'
export default function Demo() {
return (
<div className="flex flex-col gap-3">
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Tag>{LONG}</Tag>
</div>
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Tag tone="warning">
<Svg icon={checkCircle} />
{LONG}
</Tag>
</div>
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Tag tone="info" size="xs">{LONG}</Tag>
</div>
<div className="flex w-50 gap-2 rounded-box border border-line p-2" data-narrow>
<Tag className="shrink">{LONG}</Tag>
<Tag className="shrink" tone="success">{LONG}</Tag>
</div>
</div>
)
}
element-label
Infrastructure, platform reliability and developer experience working group
Infrastructure, platform reliability and developer experience working group
sha256_9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08element-label.tsx
import { checkCircle } from 'topclassui/icons/check-circle'
import { Svg } from 'topclassui/svg'
import { Tag } from 'topclassui/tag'
const LONG = 'Infrastructure, platform reliability and developer experience working group'
const DIGEST = 'sha256_9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08'
/** An element as the label is clipped to the chip on one line; only bare text gets the ellipsis. */
export default function Demo() {
return (
<div className="flex flex-col gap-3">
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Tag><strong>{LONG}</strong></Tag>
</div>
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Tag tone="warning">
<Svg icon={checkCircle} />
<em>{LONG}</em>
</Tag>
</div>
<div className="w-50 rounded-box border border-line p-2" data-narrow>
<Tag tone="info" size="xs"><code>{DIGEST}</code></Tag>
</div>
</div>
)
}
custom-size
The scaleYour own row
custom-size.tsx
import { Tag } from 'topclassui/tag'
export default function Demo() {
return (
<div className="flex flex-wrap items-center gap-3">
<Tag size="md">The scale</Tag>
<Tag size="custom" className="px-pad-x-xs py-pad-y-xs text-xs">Your own row</Tag>
</div>
)
}
بالعربية، من اليمين إلى اليسار
موثّقتصميمإمكانية الوصولعاجل
rtl.tsx
import { checkCircle } from 'topclassui/icons/check-circle'
import { Svg } from 'topclassui/svg'
import { Tag } from 'topclassui/tag'
export default function Demo() {
return (
<div dir="rtl" lang="ar" className="flex flex-wrap items-center gap-2">
<Tag tone="success">
<Svg icon={checkCircle} />
موثّق
</Tag>
<Tag>تصميم</Tag>
<Tag tone="info">إمكانية الوصول</Tag>
<Tag tone="danger">عاجل</Tag>
</div>
)
}
الخصائص
لوحة المفاتيح وإمكانية الوصول
- يُسمّى زر الإزالة بكلمته ثم بالتسمية، فيُقرأ «إزالة React»، والكلمة تأتي من تسميات المزوّد («Remove» بالإنجليزية).
- زر الإزالة المحذوف يأخذ التركيز معه: أرسله إلى الزر التالي أو السابق، وإلى عنصر تحكم بعد القائمة عندما تفرغ.
- زر الإزالة 24 بكسل، و44 تحت مؤشر لمس، مع حلقة تركيز ظاهرة.
من اليمين إلى اليسار
الحشو منطقي، فينتقل زر الإزالة إلى نهاية السطر والأيقونة إلى بدايته بلا شيفرة إضافية.
الخادم والمتصفح
يُعرض على الخادم، والجزء التفاعلي مكوّن متصفح لا يُرسل إلا عند استخدامه.
الملفات: index.tsx, client.tsx.
على الشارة: split - الحجم لم يُقَس بعد.