snapshot of https://github.com/shadcn-ui/ui.git (main, 2026-09-15; blobs > 131072B omitted)
All checks were successful
Release / Create Version PR or Publish Stable Release (push) Has been skipped
Release / Publish Prerelease to NPM (push) Has been skipped
Monitor Registries / Monitor registry health (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-15 22:44:43 +05:00
commit 1416db152c
5727 changed files with 630238 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
# Examples
This directory contains demo components for the component documentation.
## Directory Structure
```
examples
├── base
│ ├── button-demo.tsx
│ └── ...
├── radix
│ └── ...
└── __index__.tsx # Auto-generated by registry:build
```
## Adding a New Example
1. Create a new `.tsx` file in `examples/base` or `examples/radix`:
```tsx
// examples/base/button-loading.tsx
import { Button } from "@/styles/base-nova/ui/button"
export function ButtonLoading() {
return <Button disabled>Loading...</Button>
}
```
2. Run the registry build to regenerate the index:
```bash
pnpm --filter=v4 registry:build
```
3. Use the example in documentation by referencing its name (filename without `.tsx`):
```tsx
<ComponentPreview name="button-loading" />
```
## Notes
- Example files should be placed directly in `examples/base` or `examples/radix`, not in subdirectories.
- Base examples should import from `@/styles/base-nova/*`. Radix examples should import from `@/styles/radix-nova/*`.
- Both named exports and default exports are supported.
- After adding or removing examples, run `pnpm --filter=v4 registry:build` to update the index.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/styles/aria-nova/ui/accordion"
const items = [
{
value: "item-1",
trigger: "How do I reset my password?",
content:
"Click on 'Forgot Password' on the login page, enter your email address, and we'll send you a link to reset your password. The link will expire in 24 hours.",
},
{
value: "item-2",
trigger: "Can I change my subscription plan?",
content:
"Yes, you can upgrade or downgrade your plan at any time from your account settings. Changes will be reflected in your next billing cycle.",
},
{
value: "item-3",
trigger: "What payment methods do you accept?",
content:
"We accept all major credit cards, PayPal, and bank transfers. All payments are processed securely through our payment partners.",
},
]
export function AccordionBasic() {
return (
<Accordion defaultExpandedKeys={["item-1"]} className="max-w-lg">
{items.map((item) => (
<AccordionItem key={item.value} id={item.value}>
<AccordionTrigger>{item.trigger}</AccordionTrigger>
<AccordionContent>{item.content}</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}

View File

@@ -0,0 +1,47 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/styles/aria-nova/ui/accordion"
const items = [
{
value: "billing",
trigger: "How does billing work?",
content:
"We offer monthly and annual subscription plans. Billing is charged at the beginning of each cycle, and you can cancel anytime. All plans include automatic backups, 24/7 support, and unlimited team members.",
},
{
value: "security",
trigger: "Is my data secure?",
content:
"Yes. We use end-to-end encryption, SOC 2 Type II compliance, and regular third-party security audits. All data is encrypted at rest and in transit using industry-standard protocols.",
},
{
value: "integration",
trigger: "What integrations do you support?",
content:
"We integrate with 500+ popular tools including Slack, Zapier, Salesforce, HubSpot, and more. You can also build custom integrations using our REST API and webhooks.",
},
]
export default function AccordionBorders() {
return (
<Accordion
className="max-w-lg rounded-lg border"
defaultExpandedKeys={["billing"]}
>
{items.map((item) => (
<AccordionItem
key={item.value}
id={item.value}
className="border-b px-4 last:border-b-0"
>
<AccordionTrigger>{item.trigger}</AccordionTrigger>
<AccordionContent>{item.content}</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}

View File

@@ -0,0 +1,58 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/styles/aria-nova/ui/accordion"
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/styles/aria-nova/ui/card"
const items = [
{
value: "plans",
trigger: "What subscription plans do you offer?",
content:
"We offer three subscription tiers: Starter ($9/month), Professional ($29/month), and Enterprise ($99/month). Each plan includes increasing storage limits, API access, priority support, and team collaboration features.",
},
{
value: "billing",
trigger: "How does billing work?",
content:
"Billing occurs automatically at the start of each billing cycle. We accept all major credit cards, PayPal, and ACH transfers for enterprise customers. You'll receive an invoice via email after each payment.",
},
{
value: "cancel",
trigger: "How do I cancel my subscription?",
content:
"You can cancel your subscription anytime from your account settings. There are no cancellation fees or penalties. Your access will continue until the end of your current billing period.",
},
]
export default function AccordionCard() {
return (
<Card className="w-full max-w-sm">
<CardHeader>
<CardTitle>Subscription & Billing</CardTitle>
<CardDescription>
Common questions about your account, plans, payments and
cancellations.
</CardDescription>
</CardHeader>
<CardContent>
<Accordion defaultExpandedKeys={["plans"]}>
{items.map((item) => (
<AccordionItem key={item.value} id={item.value}>
<AccordionTrigger>{item.trigger}</AccordionTrigger>
<AccordionContent>{item.content}</AccordionContent>
</AccordionItem>
))}
</Accordion>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,34 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/styles/aria-nova/ui/accordion"
export default function AccordionDemo() {
return (
<Accordion defaultExpandedKeys={["shipping"]} className="max-w-lg">
<AccordionItem id="shipping">
<AccordionTrigger>What are your shipping options?</AccordionTrigger>
<AccordionContent>
We offer standard (5-7 days), express (2-3 days), and overnight
shipping. Free shipping on international orders.
</AccordionContent>
</AccordionItem>
<AccordionItem id="returns">
<AccordionTrigger>What is your return policy?</AccordionTrigger>
<AccordionContent>
Returns accepted within 30 days. Items must be unused and in original
packaging. Refunds processed within 5-7 business days.
</AccordionContent>
</AccordionItem>
<AccordionItem id="support">
<AccordionTrigger>How can I contact customer support?</AccordionTrigger>
<AccordionContent>
Reach us via email, live chat, or phone. We respond within 24 hours
during business days.
</AccordionContent>
</AccordionItem>
</Accordion>
)
}

View File

@@ -0,0 +1,36 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/styles/aria-nova/ui/accordion"
export default function AccordionDisabled() {
return (
<Accordion className="w-full">
<AccordionItem id="item-1">
<AccordionTrigger>Can I access my account history?</AccordionTrigger>
<AccordionContent>
Yes, you can view your complete account history including all
transactions, plan changes, and support tickets in the Account History
section of your dashboard.
</AccordionContent>
</AccordionItem>
<AccordionItem id="item-2" isDisabled>
<AccordionTrigger>Premium feature information</AccordionTrigger>
<AccordionContent>
This section contains information about premium features. Upgrade your
plan to access this content.
</AccordionContent>
</AccordionItem>
<AccordionItem id="item-3">
<AccordionTrigger>How do I update my email address?</AccordionTrigger>
<AccordionContent>
You can update your email address in your account settings.
You&apos;ll receive a verification email at your new address to
confirm the change.
</AccordionContent>
</AccordionItem>
</Accordion>
)
}

View File

@@ -0,0 +1,44 @@
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/styles/aria-nova/ui/accordion"
const items = [
{
value: "notifications",
trigger: "Notification Settings",
content:
"Manage how you receive notifications. You can enable email alerts for updates or push notifications for mobile devices.",
},
{
value: "privacy",
trigger: "Privacy & Security",
content:
"Control your privacy settings and security preferences. Enable two-factor authentication, manage connected devices, review active sessions, and configure data sharing preferences. You can also download your data or delete your account.",
},
{
value: "billing",
trigger: "Billing & Subscription",
content:
"View your current plan, payment history, and upcoming invoices. Update your payment method, change your subscription tier, or cancel your subscription.",
},
]
export function AccordionMultiple() {
return (
<Accordion
allowsMultipleExpanded
className="max-w-lg"
defaultExpandedKeys={["notifications"]}
>
{items.map((item) => (
<AccordionItem key={item.value} id={item.value}>
<AccordionTrigger>{item.trigger}</AccordionTrigger>
<AccordionContent>{item.content}</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}

View File

@@ -0,0 +1,91 @@
"use client"
import * as React from "react"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@/styles/aria-nova/ui-rtl/accordion"
const translations: Translations = {
en: {
dir: "ltr",
values: {
question1: "How do I reset my password?",
answer1:
"Click on 'Forgot Password' on the login page, enter your email address, and we'll send you a link to reset your password.",
question2: "Can I change my subscription plan?",
answer2:
"Yes, you can upgrade or downgrade your plan at any time from your account settings. Changes will be reflected in your next billing cycle.",
question3: "What payment methods do you accept?",
answer3:
"We accept all major credit cards, PayPal, and bank transfers. All payments are processed securely through our payment partners.",
},
},
ar: {
dir: "rtl",
values: {
question1: "كيف يمكنني إعادة تعيين كلمة المرور؟",
answer1:
"انقر على 'نسيت كلمة المرور' في صفحة تسجيل الدخول، أدخل عنوان بريدك الإلكتروني، وسنرسل لك رابطًا لإعادة تعيين كلمة المرور. سينتهي صلاحية الرابط خلال 24 ساعة.",
question2: "هل يمكنني تغيير خطة الاشتراك الخاصة بي؟",
answer2:
"نعم، يمكنك ترقية أو تخفيض خطتك في أي وقت من إعدادات حسابك. ستظهر التغييرات في دورة الفوترة التالية.",
question3: "ما هي طرق الدفع التي تقبلونها؟",
answer3:
"نقبل جميع بطاقات الائتمان الرئيسية و PayPal والتحويلات المصرفية. تتم معالجة جميع المدفوعات بأمان من خلال شركاء الدفع لدينا.",
},
},
he: {
dir: "rtl",
values: {
question1: "איך אני מאפס את הסיסמה שלי?",
answer1:
"לחץ על 'שכחתי סיסמה' בעמוד ההתחברות, הזן את כתובת האימייל שלך, ונשלח לך קישור לאיפוס הסיסמה. הקישור יפוג תוך 24 שעות.",
question2: "האם אני יכול לשנות את תוכנית המנוי שלי?",
answer2:
"כן, אתה יכול לשדרג או להוריד את התוכנית שלך בכל עת מההגדרות של החשבון שלך. השינויים יבואו לידי ביטוי במחזור החיוב הבא.",
question3: "אילו אמצעי תשלום אתם מקבלים?",
answer3: "אנו מקבלים כרטיסי אשראי, PayPal והעברות בנקאיות.",
},
},
}
const items = [
{
value: "item-1",
questionKey: "question1" as const,
answerKey: "answer1" as const,
},
{
value: "item-2",
questionKey: "question2" as const,
answerKey: "answer2" as const,
},
{
value: "item-3",
questionKey: "question3" as const,
answerKey: "answer3" as const,
},
] as const
export function AccordionRtl() {
const { t } = useTranslation(translations, "ar")
return (
<Accordion defaultExpandedKeys={["item-1"]} className="max-w-md">
{items.map((item) => (
<AccordionItem key={item.value} id={item.value}>
<AccordionTrigger>{t[item.questionKey]}</AccordionTrigger>
<AccordionContent>{t[item.answerKey]}</AccordionContent>
</AccordionItem>
))}
</Accordion>
)
}

View File

@@ -0,0 +1,23 @@
import {
Alert,
AlertAction,
AlertDescription,
AlertTitle,
} from "@/styles/aria-nova/ui/alert"
import { Button } from "@/styles/aria-nova/ui/button"
export default function AlertActionExample() {
return (
<Alert className="max-w-md">
<AlertTitle>Dark mode is now available</AlertTitle>
<AlertDescription>
Enable it under your profile settings to get started.
</AlertDescription>
<AlertAction>
<Button size="xs" variant="default">
Enable
</Button>
</AlertAction>
</Alert>
)
}

View File

@@ -0,0 +1,20 @@
import { CheckCircle2Icon } from "lucide-react"
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/styles/aria-nova/ui/alert"
export default function AlertBasic() {
return (
<Alert className="max-w-md">
<CheckCircle2Icon />
<AlertTitle>Account updated successfully</AlertTitle>
<AlertDescription>
Your profile information has been saved. Changes will be reflected
immediately.
</AlertDescription>
</Alert>
)
}

View File

@@ -0,0 +1,20 @@
import { AlertTriangleIcon } from "lucide-react"
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/styles/aria-nova/ui/alert"
export default function AlertColors() {
return (
<Alert className="max-w-md border-amber-200 bg-amber-50 text-amber-900 dark:border-amber-900 dark:bg-amber-950 dark:text-amber-50">
<AlertTriangleIcon />
<AlertTitle>Your subscription will expire in 3 days.</AlertTitle>
<AlertDescription>
Renew now to avoid service interruption or upgrade to a paid plan to
continue using the service.
</AlertDescription>
</Alert>
)
}

View File

@@ -0,0 +1,30 @@
import { CheckCircle2Icon, InfoIcon } from "lucide-react"
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/styles/aria-nova/ui/alert"
export default function AlertDemo() {
return (
<div className="grid w-full max-w-md items-start gap-4">
<Alert>
<CheckCircle2Icon />
<AlertTitle>Payment successful</AlertTitle>
<AlertDescription>
Your payment of $29.99 has been processed. A receipt has been sent to
your email address.
</AlertDescription>
</Alert>
<Alert>
<InfoIcon />
<AlertTitle>New feature available</AlertTitle>
<AlertDescription>
We&apos;ve added dark mode support. You can enable it in your account
settings.
</AlertDescription>
</Alert>
</div>
)
}

View File

@@ -0,0 +1,20 @@
import { AlertCircleIcon } from "lucide-react"
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/styles/aria-nova/ui/alert"
export default function AlertDestructive() {
return (
<Alert variant="destructive" className="max-w-md">
<AlertCircleIcon />
<AlertTitle>Payment failed</AlertTitle>
<AlertDescription>
Your payment could not be processed. Please check your payment method
and try again.
</AlertDescription>
</Alert>
)
}

View File

@@ -0,0 +1,32 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/styles/aria-nova/ui/alert-dialog"
import { Button } from "@/styles/aria-nova/ui/button"
export function AlertDialogBasic() {
return (
<AlertDialogTrigger>
<Button variant="outline">Show Dialog</Button>
<AlertDialog>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account and remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
</AlertDialogTrigger>
)
}

View File

@@ -0,0 +1,32 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/styles/aria-nova/ui/alert-dialog"
import { Button } from "@/styles/aria-nova/ui/button"
export default function AlertDialogDemo() {
return (
<AlertDialogTrigger>
<Button variant="outline">Show Dialog</Button>
<AlertDialog>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your
account from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Continue</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
</AlertDialogTrigger>
)
}

View File

@@ -0,0 +1,38 @@
import { Trash2Icon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/styles/aria-nova/ui/alert-dialog"
import { Button } from "@/styles/aria-nova/ui/button"
export function AlertDialogDestructive() {
return (
<AlertDialogTrigger>
<Button variant="destructive">Delete Chat</Button>
<AlertDialog size="sm">
<AlertDialogHeader>
<AlertDialogMedia className="bg-destructive/10 text-destructive dark:bg-destructive/20 dark:text-destructive">
<Trash2Icon />
</AlertDialogMedia>
<AlertDialogTitle>Delete chat?</AlertDialogTitle>
<AlertDialogDescription>
This will permanently delete this chat conversation. View{" "}
<a href="#">Settings</a> delete any memories saved during this chat.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel variant="outline">Cancel</AlertDialogCancel>
<AlertDialogAction variant="destructive">Delete</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
</AlertDialogTrigger>
)
}

View File

@@ -0,0 +1,37 @@
import { CircleFadingPlusIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/styles/aria-nova/ui/alert-dialog"
import { Button } from "@/styles/aria-nova/ui/button"
export function AlertDialogWithMedia() {
return (
<AlertDialogTrigger>
<Button variant="outline">Share Project</Button>
<AlertDialog>
<AlertDialogHeader>
<AlertDialogMedia>
<CircleFadingPlusIcon />
</AlertDialogMedia>
<AlertDialogTitle>Share this project?</AlertDialogTitle>
<AlertDialogDescription>
Anyone with the link will be able to view and edit this project.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction>Share</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
</AlertDialogTrigger>
)
}

View File

@@ -0,0 +1,116 @@
"use client"
import { BluetoothIcon } from "lucide-react"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/styles/aria-nova/ui-rtl/alert-dialog"
import { Button } from "@/styles/aria-nova/ui-rtl/button"
const translations: Translations = {
en: {
dir: "ltr",
values: {
showDialog: "Show Dialog",
showDialogSm: "Show Dialog (sm)",
title: "Are you absolutely sure?",
description:
"This action cannot be undone. This will permanently delete your account from our servers.",
cancel: "Cancel",
continue: "Continue",
smallTitle: "Allow accessory to connect?",
smallDescription:
"Do you want to allow the USB accessory to connect to this device?",
dontAllow: "Don't allow",
allow: "Allow",
},
},
ar: {
dir: "rtl",
values: {
showDialog: "إظهار الحوار",
showDialogSm: "إظهار الحوار (صغير)",
title: "هل أنت متأكد تمامًا؟",
description:
"لا يمكن التراجع عن هذا الإجراء. سيؤدي هذا إلى حذف حسابك نهائيًا من خوادمنا.",
cancel: "إلغاء",
continue: "متابعة",
smallTitle: "السماح للملحق بالاتصال؟",
smallDescription: "هل تريد السماح لملحق USB بالاتصال بهذا الجهاز؟",
dontAllow: "عدم السماح",
allow: "السماح",
},
},
he: {
dir: "rtl",
values: {
showDialog: "הצג דיאלוג",
showDialogSm: "הצג דיאלוג (קטן)",
title: "האם אתה בטוח לחלוטין?",
description:
"פעולה זו לא ניתנת לביטול. זה ימחק לצמיתות את החשבון שלך מהשרתים שלנו.",
cancel: "ביטול",
continue: "המשך",
smallTitle: "לאפשר להתקן להתחבר?",
smallDescription: "האם אתה רוצה לאפשר להתקן USB להתחבר למכשיר זה?",
dontAllow: "אל תאפשר",
allow: "אפשר",
},
},
}
export function AlertDialogRtl() {
const { dir, t, language } = useTranslation(translations, "ar")
return (
<div className="flex gap-4" dir={dir}>
<AlertDialogTrigger>
<Button variant="outline">{t.showDialog}</Button>
<AlertDialog dir={dir} data-lang={dir === "rtl" ? language : undefined}>
<AlertDialogHeader>
<AlertDialogTitle>{t.title}</AlertDialogTitle>
<AlertDialogDescription>{t.description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t.cancel}</AlertDialogCancel>
<AlertDialogAction>{t.continue}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
</AlertDialogTrigger>
<AlertDialogTrigger>
<Button variant="outline">{t.showDialogSm}</Button>
<AlertDialog
size="sm"
dir={dir}
data-lang={dir === "rtl" ? language : undefined}
>
<AlertDialogHeader>
<AlertDialogMedia>
<BluetoothIcon />
</AlertDialogMedia>
<AlertDialogTitle>{t.smallTitle}</AlertDialogTitle>
<AlertDialogDescription>
{t.smallDescription}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t.dontAllow}</AlertDialogCancel>
<AlertDialogAction>{t.allow}</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
</AlertDialogTrigger>
</div>
)
}

View File

@@ -0,0 +1,37 @@
import { BluetoothIcon } from "lucide-react"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogMedia,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/styles/aria-nova/ui/alert-dialog"
import { Button } from "@/styles/aria-nova/ui/button"
export function AlertDialogSmallWithMedia() {
return (
<AlertDialogTrigger>
<Button variant="outline">Show Dialog</Button>
<AlertDialog size="sm">
<AlertDialogHeader>
<AlertDialogMedia>
<BluetoothIcon />
</AlertDialogMedia>
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
<AlertDialogDescription>
Do you want to allow the USB accessory to connect to this device?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Don&apos;t allow</AlertDialogCancel>
<AlertDialogAction>Allow</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
</AlertDialogTrigger>
)
}

View File

@@ -0,0 +1,31 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
AlertDialogTrigger,
} from "@/styles/aria-nova/ui/alert-dialog"
import { Button } from "@/styles/aria-nova/ui/button"
export function AlertDialogSmall() {
return (
<AlertDialogTrigger>
<Button variant="outline">Show Dialog</Button>
<AlertDialog size="sm">
<AlertDialogHeader>
<AlertDialogTitle>Allow accessory to connect?</AlertDialogTitle>
<AlertDialogDescription>
Do you want to allow the USB accessory to connect to this device?
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Don&apos;t allow</AlertDialogCancel>
<AlertDialogAction>Allow</AlertDialogAction>
</AlertDialogFooter>
</AlertDialog>
</AlertDialogTrigger>
)
}

View File

@@ -0,0 +1,82 @@
"use client"
import * as React from "react"
import { CheckCircle2Icon, InfoIcon } from "lucide-react"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import {
Alert,
AlertDescription,
AlertTitle,
} from "@/styles/aria-nova/ui-rtl/alert"
const translations: Translations = {
en: {
dir: "ltr",
values: {
paymentTitle: "Payment successful",
paymentDescription:
"Your payment of $29.99 has been processed. A receipt has been sent to your email address.",
featureTitle: "New feature available",
featureDescription:
"We've added dark mode support. You can enable it in your account settings.",
},
},
ar: {
dir: "rtl",
values: {
paymentTitle: "تم الدفع بنجاح",
paymentDescription:
"تمت معالجة دفعتك البالغة 29.99 دولارًا. تم إرسال إيصال إلى عنوان بريدك الإلكتروني.",
featureTitle: "ميزة جديدة متاحة",
featureDescription:
"لقد أضفنا دعم الوضع الداكن. يمكنك تفعيله في إعدادات حسابك.",
},
},
he: {
dir: "rtl",
values: {
paymentTitle: "התשלום בוצע בהצלחה",
paymentDescription:
"התשלום שלך בסך 29.99 דולר עובד. קבלה נשלחה לכתובת האימייל שלך.",
featureTitle: "תכונה חדשה זמינה",
featureDescription:
"הוספנו תמיכה במצב כהה. אתה יכול להפעיל אותו בהגדרות החשבון שלך.",
},
},
}
const alerts = [
{
icon: CheckCircle2Icon,
titleKey: "paymentTitle" as const,
descriptionKey: "paymentDescription" as const,
},
{
icon: InfoIcon,
titleKey: "featureTitle" as const,
descriptionKey: "featureDescription" as const,
},
] as const
export function AlertRtl() {
const { dir, t } = useTranslation(translations, "ar")
return (
<div className="grid w-full max-w-md items-start gap-4" dir={dir}>
{alerts.map((alert, index) => {
const Icon = alert.icon
return (
<Alert key={index}>
<Icon />
<AlertTitle>{t[alert.titleKey]}</AlertTitle>
<AlertDescription>{t[alert.descriptionKey]}</AlertDescription>
</Alert>
)
})}
</div>
)
}

View File

@@ -0,0 +1,16 @@
import Image from "next/image"
import { AspectRatio } from "@/styles/aria-nova/ui/aspect-ratio"
export default function AspectRatioDemo() {
return (
<AspectRatio ratio={16 / 9} className="w-full max-w-sm rounded-lg bg-muted">
<Image
src="https://avatar.vercel.sh/shadcn1"
alt="Photo"
fill
className="rounded-lg object-cover grayscale dark:brightness-20"
/>
</AspectRatio>
)
}

View File

@@ -0,0 +1,19 @@
import Image from "next/image"
import { AspectRatio } from "@/styles/aria-nova/ui/aspect-ratio"
export function AspectRatioPortrait() {
return (
<AspectRatio
ratio={9 / 16}
className="w-full max-w-[10rem] rounded-lg bg-muted"
>
<Image
src="https://avatar.vercel.sh/shadcn1"
alt="Photo"
fill
className="rounded-lg object-cover grayscale dark:brightness-20"
/>
</AspectRatio>
)
}

View File

@@ -0,0 +1,51 @@
"use client"
import * as React from "react"
import Image from "next/image"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import { AspectRatio } from "@/styles/aria-nova/ui-rtl/aspect-ratio"
const translations: Translations = {
en: {
dir: "ltr",
values: {
caption: "Beautiful landscape",
},
},
ar: {
dir: "rtl",
values: {
caption: "منظر طبيعي جميل",
},
},
he: {
dir: "rtl",
values: {
caption: "נוף יפה",
},
},
}
export function AspectRatioRtl() {
const { dir, t } = useTranslation(translations, "ar")
return (
<figure className="w-full max-w-sm" dir={dir}>
<AspectRatio ratio={16 / 9} className="rounded-lg bg-muted">
<Image
src="https://avatar.vercel.sh/shadcn1"
alt="Photo"
fill
className="rounded-lg object-cover grayscale dark:brightness-20"
/>
</AspectRatio>
<figcaption className="mt-2 text-center text-sm text-muted-foreground">
{t.caption}
</figcaption>
</figure>
)
}

View File

@@ -0,0 +1,19 @@
import Image from "next/image"
import { AspectRatio } from "@/styles/aria-nova/ui/aspect-ratio"
export function AspectRatioSquare() {
return (
<AspectRatio
ratio={1 / 1}
className="w-full max-w-[12rem] rounded-lg bg-muted"
>
<Image
src="https://avatar.vercel.sh/shadcn1"
alt="Photo"
fill
className="rounded-lg object-cover grayscale dark:brightness-20"
/>
</AspectRatio>
)
}

View File

@@ -0,0 +1,82 @@
import { FileCodeIcon, XIcon } from "lucide-react"
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@/styles/aria-rhea/ui/attachment"
import { Spinner } from "@/styles/aria-rhea/ui/spinner"
const images = [
{
name: "workspace.png",
meta: "PNG · 820 KB",
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
alt: "Workspace",
},
{
name: "desk-reference.jpg",
meta: "JPG · 1.1 MB",
src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80",
alt: "Desk",
},
{
name: "office-reference.jpg",
meta: "JPG · 940 KB",
src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
alt: "Office",
},
]
export function AttachmentDemo() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-3 py-12">
<AttachmentGroup>
{images.map((image) => (
<Attachment key={image.name} orientation="vertical">
<AttachmentMedia variant="image">
<img src={image.src} alt={image.alt} />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>{image.name}</AttachmentTitle>
<AttachmentDescription>{image.meta}</AttachmentDescription>
</AttachmentContent>
</Attachment>
))}
</AttachmentGroup>
<Attachment state="uploading" className="w-full">
<AttachmentMedia>
<Spinner />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>sales-dashboard.pdf</AttachmentTitle>
<AttachmentDescription>Uploading · 64%</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Cancel upload">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
<Attachment className="w-full">
<AttachmentMedia>
<FileCodeIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>message-renderer.tsx</AttachmentTitle>
<AttachmentDescription>TypeScript · 12 KB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove message-renderer.tsx">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</div>
)
}

View File

@@ -0,0 +1,71 @@
import {
FileCodeIcon,
FileTextIcon,
TableIcon,
XIcon,
type LucideIcon,
} from "lucide-react"
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
} from "@/styles/aria-rhea/ui/attachment"
type Item = {
name: string
meta: string
icon?: LucideIcon
src?: string
}
const items: Item[] = [
{ name: "briefing-notes.pdf", meta: "PDF · 1.4 MB", icon: FileTextIcon },
{
name: "workspace.png",
meta: "PNG · 820 KB",
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
},
{ name: "customers.csv", meta: "CSV · 18 KB", icon: TableIcon },
{ name: "renderer.tsx", meta: "TSX · 12 KB", icon: FileCodeIcon },
]
export function AttachmentGroupDemo() {
return (
<div className="mx-auto w-full max-w-sm py-12">
<AttachmentGroup className="w-full">
{items.map((item) => {
const Icon = item.icon
return (
<Attachment key={item.name} className="w-64">
{item.src ? (
<AttachmentMedia variant="image">
<img src={item.src} alt={item.name} />
</AttachmentMedia>
) : Icon ? (
<AttachmentMedia>
<Icon />
</AttachmentMedia>
) : null}
<AttachmentContent>
<AttachmentTitle>{item.name}</AttachmentTitle>
<AttachmentDescription>{item.meta}</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label={`Remove ${item.name}`}>
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
)
})}
</AttachmentGroup>
</div>
)
}

View File

@@ -0,0 +1,70 @@
import { XIcon } from "lucide-react"
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentGroup,
AttachmentMedia,
AttachmentTitle,
AttachmentTrigger,
} from "@/styles/aria-rhea/ui/attachment"
const images = [
{
name: "workspace.png",
meta: "PNG · 820 KB",
src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80",
alt: "Workspace",
},
{
name: "desk-reference.jpg",
meta: "JPG · 1.1 MB",
src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80",
alt: "Desk",
},
{
name: "office-reference.jpg",
meta: "JPG · 940 KB",
src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80",
alt: "Office",
},
]
export function AttachmentImage() {
return (
<div className="mx-auto w-full max-w-sm py-12">
<AttachmentGroup className="w-full">
{images.map((image) => (
<Attachment key={image.name} orientation="vertical">
<AttachmentMedia variant="image">
<img src={image.src} alt={image.alt} />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>{image.name}</AttachmentTitle>
<AttachmentDescription>{image.meta}</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label={`Remove ${image.name}`}>
<XIcon />
</AttachmentAction>
</AttachmentActions>
<AttachmentTrigger
render={(props) => (
<a
{...props}
href={image.src}
target="_blank"
rel="noreferrer"
aria-label={`Open ${image.name}`}
/>
)}
/>
</Attachment>
))}
</AttachmentGroup>
</div>
)
}

View File

@@ -0,0 +1,42 @@
import { FileTextIcon } from "lucide-react"
import {
Attachment,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@/styles/aria-rhea/ui/attachment"
export function AttachmentSizes() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-3 py-12">
<Attachment size="default" className="w-full">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Default attachment</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
</Attachment>
<Attachment size="sm" className="w-full">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Small attachment</AttachmentTitle>
<AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>
</AttachmentContent>
</Attachment>
<Attachment size="xs" className="w-full">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>Extra small attachment</AttachmentTitle>
</AttachmentContent>
</Attachment>
</div>
)
}

View File

@@ -0,0 +1,101 @@
import {
CheckIcon,
ClockIcon,
FileTextIcon,
FileWarningIcon,
RefreshCwIcon,
XIcon,
} from "lucide-react"
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
} from "@/styles/aria-rhea/ui/attachment"
import { Spinner } from "@/styles/aria-rhea/ui/spinner"
export function AttachmentStates() {
return (
<div className="mx-auto flex w-full max-w-sm flex-col gap-2 py-12">
<Attachment state="idle" className="w-full">
<AttachmentMedia>
<ClockIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>selected-file.pdf</AttachmentTitle>
<AttachmentDescription>Ready to upload</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove selected-file.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
<Attachment state="uploading" className="w-full">
<AttachmentMedia>
<Spinner />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>design-system.zip</AttachmentTitle>
<AttachmentDescription>Uploading · 64%</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Cancel upload">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
<Attachment state="processing" className="w-full">
<AttachmentMedia>
<FileTextIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>market-research.pdf</AttachmentTitle>
<AttachmentDescription>Processing document</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove market-research.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
<Attachment state="error" className="w-full">
<AttachmentMedia>
<FileWarningIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>financial-model.xlsx</AttachmentTitle>
<AttachmentDescription>
Upload failed. Try again.
</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Retry upload">
<RefreshCwIcon />
</AttachmentAction>
<AttachmentAction aria-label="Remove financial-model.xlsx">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
<Attachment state="done" className="w-full">
<AttachmentMedia>
<CheckIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>uploaded-report.pdf</AttachmentTitle>
<AttachmentDescription>Uploaded · 1.8 MB</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Remove uploaded-report.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
</Attachment>
</div>
)
}

View File

@@ -0,0 +1,57 @@
import { CopyIcon, FileSearchIcon, XIcon } from "lucide-react"
import {
Attachment,
AttachmentAction,
AttachmentActions,
AttachmentContent,
AttachmentDescription,
AttachmentMedia,
AttachmentTitle,
AttachmentTrigger,
} from "@/styles/aria-rhea/ui/attachment"
import {
Dialog,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/styles/aria-rhea/ui/dialog"
export function AttachmentTriggerDemo() {
return (
<div className="mx-auto w-full max-w-sm py-12">
<DialogTrigger>
<Attachment className="w-full">
<AttachmentMedia>
<FileSearchIcon />
</AttachmentMedia>
<AttachmentContent>
<AttachmentTitle>research-summary.pdf</AttachmentTitle>
<AttachmentDescription>Open preview dialog</AttachmentDescription>
</AttachmentContent>
<AttachmentActions>
<AttachmentAction aria-label="Copy link">
<CopyIcon />
</AttachmentAction>
<AttachmentAction aria-label="Remove research-summary.pdf">
<XIcon />
</AttachmentAction>
</AttachmentActions>
<DialogTrigger>
<AttachmentTrigger aria-label="Preview research-summary.pdf" />
</DialogTrigger>
</Attachment>
<Dialog className="sm:max-w-md">
<DialogHeader>
<DialogTitle>research-summary.pdf</DialogTitle>
<DialogDescription>
The attachment trigger fills the card and opens the dialog, while
the actions stay independently clickable above it.
</DialogDescription>
</DialogHeader>
</Dialog>
</DialogTrigger>
</div>
)
}

View File

@@ -0,0 +1,20 @@
import { PlusIcon } from "lucide-react"
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
export function AvatarBadgeIconExample() {
return (
<Avatar className="grayscale">
<AvatarImage src="https://github.com/pranathip.png" alt="@pranathip" />
<AvatarFallback>PP</AvatarFallback>
<AvatarBadge>
<PlusIcon />
</AvatarBadge>
</Avatar>
)
}

View File

@@ -0,0 +1,16 @@
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
export function AvatarWithBadge() {
return (
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
<AvatarBadge className="bg-green-600 dark:bg-green-800" />
</Avatar>
)
}

View File

@@ -0,0 +1,18 @@
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
export default function AvatarDemo() {
return (
<Avatar>
<AvatarImage
src="https://github.com/shadcn.png"
alt="@shadcn"
className="grayscale"
/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
)
}

View File

@@ -0,0 +1,52 @@
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
export default function AvatarDemo() {
return (
<div className="flex flex-row flex-wrap items-center gap-6 md:gap-12">
<Avatar>
<AvatarImage
src="https://github.com/shadcn.png"
alt="@shadcn"
className="grayscale"
/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
<AvatarBadge className="bg-green-600 dark:bg-green-800" />
</Avatar>
<AvatarGroup className="grayscale">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<AvatarGroupCount>+3</AvatarGroupCount>
</AvatarGroup>
</div>
)
}

View File

@@ -0,0 +1,39 @@
"use client"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
import { Button } from "@/styles/aria-nova/ui/button"
import {
DropdownMenu,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/styles/aria-nova/ui/dropdown-menu"
export function AvatarDropdown() {
return (
<DropdownMenuTrigger>
<Button variant="ghost" size="icon" className="rounded-full">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</Button>
<DropdownMenu className="w-32">
<DropdownMenuGroup>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">Log out</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
)
}

View File

@@ -0,0 +1,34 @@
import { PlusIcon } from "lucide-react"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
export function AvatarGroupCountIconExample() {
return (
<AvatarGroup className="grayscale">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<AvatarGroupCount>
<PlusIcon />
</AvatarGroupCount>
</AvatarGroup>
)
}

View File

@@ -0,0 +1,30 @@
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
export function AvatarGroupCountExample() {
return (
<AvatarGroup className="grayscale">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<AvatarGroupCount>+3</AvatarGroupCount>
</AvatarGroup>
)
}

View File

@@ -0,0 +1,28 @@
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
export function AvatarGroupExample() {
return (
<AvatarGroup className="grayscale">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/maxleiter.png" alt="@maxleiter" />
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
</AvatarGroup>
)
}

View File

@@ -0,0 +1,86 @@
"use client"
import * as React from "react"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarGroup,
AvatarGroupCount,
AvatarImage,
} from "@/styles/aria-nova/ui-rtl/avatar"
const translations: Translations = {
en: {
dir: "ltr",
values: {
moreUsers: "+3",
},
},
ar: {
dir: "rtl",
values: {
moreUsers: "+٣",
},
},
he: {
dir: "rtl",
values: {
moreUsers: "+3",
},
},
}
export function AvatarRtl() {
const { dir, t } = useTranslation(translations, "ar")
return (
<div
className="flex flex-row flex-wrap items-center gap-6 md:gap-12"
dir={dir}
>
<Avatar>
<AvatarImage
src="https://github.com/shadcn.png"
alt="@shadcn"
className="grayscale"
/>
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
<AvatarBadge className="bg-green-600 dark:bg-green-800" />
</Avatar>
<AvatarGroup className="grayscale">
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/maxleiter.png"
alt="@maxleiter"
/>
<AvatarFallback>LR</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage
src="https://github.com/evilrabbit.png"
alt="@evilrabbit"
/>
<AvatarFallback>ER</AvatarFallback>
</Avatar>
<AvatarGroupCount>{t.moreUsers}</AvatarGroupCount>
</AvatarGroup>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/styles/aria-nova/ui/avatar"
export function AvatarSizeExample() {
return (
<div className="flex flex-wrap items-center gap-2 grayscale">
<Avatar size="sm">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar>
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
<Avatar size="lg">
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
</div>
)
}

View File

@@ -0,0 +1,23 @@
import { Badge } from "@/styles/aria-nova/ui/badge"
export function BadgeCustomColors() {
return (
<div className="flex flex-wrap gap-2">
<Badge className="bg-blue-50 text-blue-700 dark:bg-blue-950 dark:text-blue-300">
Blue
</Badge>
<Badge className="bg-green-50 text-green-700 dark:bg-green-950 dark:text-green-300">
Green
</Badge>
<Badge className="bg-sky-50 text-sky-700 dark:bg-sky-950 dark:text-sky-300">
Sky
</Badge>
<Badge className="bg-purple-50 text-purple-700 dark:bg-purple-950 dark:text-purple-300">
Purple
</Badge>
<Badge className="bg-red-50 text-red-700 dark:bg-red-950 dark:text-red-300">
Red
</Badge>
</div>
)
}

View File

@@ -0,0 +1,12 @@
import { Badge } from "@/styles/aria-nova/ui/badge"
export default function BadgeDemo() {
return (
<div className="flex w-full flex-wrap justify-center gap-2">
<Badge>Badge</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
</div>
)
}

View File

@@ -0,0 +1,18 @@
import { BadgeCheck, BookmarkIcon } from "lucide-react"
import { Badge } from "@/styles/aria-nova/ui/badge"
export function BadgeWithIconLeft() {
return (
<div className="flex flex-wrap gap-2">
<Badge variant="secondary">
<BadgeCheck data-icon="inline-start" />
Verified
</Badge>
<Badge variant="outline">
Bookmark
<BookmarkIcon data-icon="inline-end" />
</Badge>
</div>
)
}

View File

@@ -0,0 +1,11 @@
import { ArrowUpRightIcon } from "lucide-react"
import { Badge } from "@/styles/aria-nova/ui/badge"
export function BadgeAsLink() {
return (
<Badge render={(props) => <a {...props} href="#link" />}>
Open Link <ArrowUpRightIcon data-icon="inline-end" />
</Badge>
)
}

View File

@@ -0,0 +1,67 @@
"use client"
import * as React from "react"
import { BadgeCheck, BookmarkIcon } from "lucide-react"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import { Badge } from "@/styles/aria-nova/ui-rtl/badge"
const translations: Translations = {
en: {
dir: "ltr",
values: {
badge: "Badge",
secondary: "Secondary",
destructive: "Destructive",
outline: "Outline",
verified: "Verified",
bookmark: "Bookmark",
},
},
ar: {
dir: "rtl",
values: {
badge: "شارة",
secondary: "ثانوي",
destructive: "مدمر",
outline: "مخطط",
verified: "متحقق",
bookmark: "إشارة مرجعية",
},
},
he: {
dir: "rtl",
values: {
badge: "תג",
secondary: "משני",
destructive: "הרסני",
outline: "קווי מתאר",
verified: "מאומת",
bookmark: "סימנייה",
},
},
}
export function BadgeRtl() {
const { dir, t } = useTranslation(translations, "ar")
return (
<div className="flex w-full flex-wrap justify-center gap-2" dir={dir}>
<Badge>{t.badge}</Badge>
<Badge variant="secondary">{t.secondary}</Badge>
<Badge variant="destructive">{t.destructive}</Badge>
<Badge variant="outline">{t.outline}</Badge>
<Badge variant="secondary">
<BadgeCheck data-icon="inline-start" />
{t.verified}
</Badge>
<Badge variant="outline">
{t.bookmark}
<BookmarkIcon data-icon="inline-end" />
</Badge>
</div>
)
}

View File

@@ -0,0 +1,17 @@
import { Badge } from "@/styles/aria-nova/ui/badge"
import { Spinner } from "@/styles/aria-nova/ui/spinner"
export function BadgeWithSpinner() {
return (
<div className="flex flex-wrap gap-2">
<Badge variant="destructive">
<Spinner data-icon="inline-start" />
Deleting
</Badge>
<Badge variant="secondary">
Generating
<Spinner data-icon="inline-end" />
</Badge>
</div>
)
}

View File

@@ -0,0 +1,13 @@
import { Badge } from "@/styles/aria-nova/ui/badge"
export function BadgeVariants() {
return (
<div className="flex flex-wrap gap-2">
<Badge>Default</Badge>
<Badge variant="secondary">Secondary</Badge>
<Badge variant="destructive">Destructive</Badge>
<Badge variant="outline">Outline</Badge>
<Badge variant="ghost">Ghost</Badge>
</div>
)
}

View File

@@ -0,0 +1,27 @@
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/styles/aria-nova/ui/breadcrumb"
export function BreadcrumbBasic() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbLink href="#">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -0,0 +1,53 @@
"use client"
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/styles/aria-nova/ui/breadcrumb"
import { Button } from "@/styles/aria-nova/ui/button"
import {
DropdownMenu,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/aria-nova/ui/dropdown-menu"
export function BreadcrumbDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="#">Home</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<DropdownMenuTrigger>
<Button size="icon-sm" variant="ghost">
<BreadcrumbEllipsis />
<span className="sr-only">Toggle menu</span>
</Button>
<DropdownMenu placement="bottom start">
<DropdownMenuGroup>
<DropdownMenuItem>Documentation</DropdownMenuItem>
<DropdownMenuItem>Themes</DropdownMenuItem>
<DropdownMenuItem>GitHub</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbLink href="#">Components</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -0,0 +1,58 @@
"use client"
import Link from "next/link"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "react-aria-components"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/styles/aria-nova/ui/breadcrumb"
import {
DropdownMenu,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/aria-nova/ui/dropdown-menu"
export function BreadcrumbDropdown() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
href="/"
render={(props) =>
"href" in props ? <Link {...props} /> : <span {...props} />
}
>
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<DropdownMenuTrigger>
<Button className="flex items-center gap-1">
Components
<ChevronDownIcon data-icon="inline-end" className="size-3.5" />
</Button>
<DropdownMenu placement="bottom start">
<DropdownMenuGroup>
<DropdownMenuItem>Documentation</DropdownMenuItem>
<DropdownMenuItem>Themes</DropdownMenuItem>
<DropdownMenuItem>GitHub</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -0,0 +1,50 @@
"use client"
import Link from "next/link"
import {
Breadcrumb,
BreadcrumbEllipsis,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/styles/aria-nova/ui/breadcrumb"
export function BreadcrumbEllipsisDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
href="/"
render={(props) =>
"href" in props ? <Link {...props} /> : <span {...props} />
}
>
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbEllipsis />
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbLink
href="/docs/components"
render={(props) =>
"href" in props ? <Link {...props} /> : <span {...props} />
}
>
Components
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import Link from "next/link"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/styles/aria-nova/ui/breadcrumb"
export function BreadcrumbLinkDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
href="#link-component"
render={(props) =>
"href" in props ? <Link {...props} /> : <span {...props} />
}
>
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbLink
href="#link-component"
render={(props) =>
"href" in props ? <Link {...props} /> : <span {...props} />
}
>
Components
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -0,0 +1,104 @@
"use client"
import Link from "next/link"
import { ChevronDownIcon } from "lucide-react"
import { Button } from "react-aria-components"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/styles/aria-nova/ui-rtl/breadcrumb"
import {
DropdownMenu,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/styles/aria-nova/ui-rtl/dropdown-menu"
const translations: Translations = {
en: {
dir: "ltr",
values: {
home: "Home",
components: "Components",
documentation: "Documentation",
themes: "Themes",
github: "GitHub",
breadcrumb: "Breadcrumb",
},
},
ar: {
dir: "rtl",
values: {
home: "الرئيسية",
components: "المكونات",
documentation: "التوثيق",
themes: "السمات",
github: "جيت هاب",
breadcrumb: "مسار التنقل",
},
},
he: {
dir: "rtl",
values: {
home: "בית",
components: "רכיבים",
documentation: "תיעוד",
themes: "ערכות נושא",
github: "גיטהאב",
breadcrumb: "פירורי לחם",
},
},
}
export function BreadcrumbRtl() {
const { dir, t, language } = useTranslation(translations, "ar")
return (
<Breadcrumb dir={dir}>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
href="/"
render={(props) =>
"href" in props ? <Link {...props} /> : <span {...props} />
}
>
{t.home}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<DropdownMenuTrigger>
<Button className="flex items-center gap-1">
{t.components}
<ChevronDownIcon data-icon="inline-end" className="size-3.5" />
</Button>
<DropdownMenu
placement={dir === "rtl" ? "bottom end" : "bottom start"}
data-lang={dir === "rtl" ? language : undefined}
dir={dir}
>
<DropdownMenuGroup>
<DropdownMenuItem>{t.documentation}</DropdownMenuItem>
<DropdownMenuItem>{t.themes}</DropdownMenuItem>
<DropdownMenuItem>{t.github}</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbPage>{t.breadcrumb}</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -0,0 +1,45 @@
"use client"
import Link from "next/link"
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
} from "@/styles/aria-nova/ui/breadcrumb"
export function BreadcrumbSeparatorDemo() {
return (
<Breadcrumb>
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink
href="/"
render={(props) =>
"href" in props ? <Link {...props} /> : <span {...props} />
}
>
Home
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbLink
href="/components"
render={(props) =>
"href" in props ? <Link {...props} /> : <span {...props} />
}
>
Components
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbItem>
<BreadcrumbPage>Breadcrumb</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
)
}

View File

@@ -0,0 +1,18 @@
import { Bubble, BubbleContent } from "@/styles/aria-rhea/ui/bubble"
export function BubbleAlignmentDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-8 py-12">
<Bubble variant="muted">
<BubbleContent>
This bubble is aligned to the start. This is the default alignment.
</BubbleContent>
</Bubble>
<Bubble align="end">
<BubbleContent>
This bubble is aligned to the end. Use this for user messages.
</BubbleContent>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,53 @@
"use client"
import * as React from "react"
import { ChevronDownIcon } from "lucide-react"
import { Bubble, BubbleContent } from "@/styles/aria-rhea/ui/bubble"
import { Button } from "@/styles/aria-rhea/ui/button"
import { Collapsible } from "@/styles/aria-rhea/ui/collapsible"
const text = `The accessibility review found two focus states that were visually too subtle in dark mode.
I checked the dialog, menu, and drawer paths because each one renders focusable controls inside a layered surface.
The dialog and drawer are fine. The menu needs the hover and focus tokens split so keyboard focus stays visible when the pointer is not involved.
I also recommend keeping the change in the style file instead of the primitive so the other themes can choose their own focus treatment later.`
const previewLength = 180
export function BubbleCollapsible() {
const [open, setOpen] = React.useState(false)
const isLong = text.length > previewLength
const preview = `${text.slice(0, previewLength)}...`
return (
<div className="flex w-full max-w-sm flex-col gap-8 py-12">
<Bubble variant="muted">
<BubbleContent>How can I help you today?</BubbleContent>
</Bubble>
<Bubble variant="muted" align="end">
<BubbleContent className="whitespace-pre-line">
<Collapsible isExpanded={open} onExpandedChange={setOpen}>
<div>{open || !isLong ? text : preview}</div>
{isLong ? (
<Button
slot="trigger"
variant="link"
className="gap-1 p-0 text-muted-foreground"
>
{open ? "Show less" : "Show more"}
<ChevronDownIcon
data-icon="inline-end"
className="group-data-panel-open/button:rotate-180"
/>
</Button>
) : null}
</Collapsible>
</BubbleContent>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,48 @@
import {
Bubble,
BubbleContent,
BubbleGroup,
BubbleReactions,
} from "@/styles/aria-rhea/ui/bubble"
export function BubbleDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-8 py-12">
<Bubble align="end">
<BubbleContent>Hey there! what&apos;s up?</BubbleContent>
</Bubble>
<BubbleGroup>
<Bubble variant="muted">
<BubbleContent>Hey! Want to see chat bubbles?</BubbleContent>
</Bubble>
<Bubble variant="muted">
<BubbleContent>
I can group messages, switch sides, and keep the whole thread easy
to scan.
</BubbleContent>
<BubbleReactions role="img" aria-label="Reaction: thumbs up">
<span>👍</span>
</BubbleReactions>
</Bubble>
</BubbleGroup>
<Bubble align="end">
<BubbleContent>Sure. Hit me with your best demo.</BubbleContent>
</Bubble>
<Bubble variant="muted">
<BubbleContent>
Yes. You are reading a demo that is demoing itself. Very meta. Very
on-brand.
</BubbleContent>
<BubbleReactions
role="img"
aria-label="Reactions: thumbs up, fire, eyes, and 2 more"
>
<span>👍</span>
<span>🔥</span>
<span>👀</span>
<span>+2</span>
</BubbleReactions>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,36 @@
import {
Bubble,
BubbleContent,
BubbleGroup,
BubbleReactions,
} from "@/styles/aria-rhea/ui/bubble"
export function BubbleGroupDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-8 py-12">
<Bubble variant="muted">
<BubbleContent>Can you tell me what&apos;s the issue?</BubbleContent>
</Bubble>
<BubbleGroup>
<Bubble align="end">
<BubbleContent>You tell me!</BubbleContent>
</Bubble>
<Bubble align="end">
<BubbleContent>It worked yesterday. You broke it!</BubbleContent>
</Bubble>
<Bubble align="end">
<BubbleContent>Find the bug and fix it.</BubbleContent>
<BubbleReactions aria-label="Reactions: eyes" align="start">
<span>👀</span>
</BubbleReactions>
</Bubble>
</BubbleGroup>
<Bubble variant="muted">
<BubbleContent>
Want me to diff yesterday&apos;s you against today&apos;s you?
It&apos;s a bit embarrassing.
</BubbleContent>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,59 @@
"use client"
import { toast } from "sonner"
import {
Bubble,
BubbleContent,
BubbleGroup,
} from "@/styles/aria-rhea/ui/bubble"
export function BubbleLinkButtonDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-8 py-12">
<Bubble variant="muted">
<BubbleContent>How can I help you today?</BubbleContent>
</Bubble>
<BubbleGroup>
<Bubble variant="tinted" align="end">
<BubbleContent
render={(props) => (
<button
onClick={() => toast("You clicked forgot password")}
{...props}
/>
)}
>
I forgot my password
</BubbleContent>
</Bubble>
<Bubble variant="tinted" align="end">
<BubbleContent
render={(props) => (
<button
onClick={() => toast("You clicked help with subscription")}
{...props}
/>
)}
>
I need help with my subscription
</BubbleContent>
</Bubble>
<Bubble variant="tinted" align="end">
<BubbleContent
render={(props) => (
<button
onClick={() =>
toast("You clicked something else. Talk to a human.")
}
{...props}
/>
)}
>
Something else. Talk to a human.
</BubbleContent>
</Bubble>
</BubbleGroup>
</div>
)
}

View File

@@ -0,0 +1,24 @@
import { Markdown } from "@/components/markdown"
import { Bubble, BubbleContent } from "@/styles/aria-rhea/ui/bubble"
export function BubbleMarkdownDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-8 py-12">
<Bubble align="end" variant="muted">
<BubbleContent>
<Markdown>{`Hello! Are you actually **thinking**?`}</Markdown>
</BubbleContent>
</Bubble>
<Bubble variant="ghost">
<BubbleContent>
<Markdown>{`Ghost bubbles work for assistant text, **markdown**, and other content that should not be framed.
This is perfect for assistant messages that should not have a frame and can take the full width of the container. You can also render \`code\` in it.
Ghost bubbles are full width and can take the full width of the container.
`}</Markdown>
</BubbleContent>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,50 @@
import { InfoIcon } from "lucide-react"
import {
Bubble,
BubbleContent,
BubbleReactions,
} from "@/styles/aria-rhea/ui/bubble"
import { Button } from "@/styles/aria-rhea/ui/button"
import {
Popover,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/styles/aria-rhea/ui/popover"
export function BubblePopoverDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-4 py-12">
<Bubble align="end">
<BubbleContent>Run the build script.</BubbleContent>
</Bubble>
<Bubble variant="destructive">
<BubbleContent>Failed to run the command.</BubbleContent>
<BubbleReactions>
<PopoverTrigger>
<Button
variant="ghost"
size="icon-xs"
aria-label="Show error details"
className="aria-expanded:text-destructive"
>
<InfoIcon />
</Button>
<Popover>
<PopoverHeader>
<PopoverTitle className="text-sm">
Command failed with exit code 1
</PopoverTitle>
<PopoverDescription className="text-sm">
ENOENT: no such file or directory, open pnpm-lock.yaml
</PopoverDescription>
</PopoverHeader>
</Popover>
</PopoverTrigger>
</BubbleReactions>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,70 @@
"use client"
import { toast } from "sonner"
import {
Bubble,
BubbleContent,
BubbleReactions,
} from "@/styles/aria-rhea/ui/bubble"
import { Button } from "@/styles/aria-rhea/ui/button"
export function BubbleReactionsDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-12 py-12">
<Bubble variant="muted" align="end">
<BubbleContent>
I don&apos;t need tests, I know my code works.
</BubbleContent>
<BubbleReactions
align="start"
role="img"
aria-label="Reactions: thumbs up, surprised"
>
<span>👍</span>
<span>😮</span>
</BubbleReactions>
</Bubble>
<Bubble variant="muted">
<BubbleContent>
Bold. Fine I&apos;ll add some tests. I&apos;ll let you know when
they&apos;re done.
</BubbleContent>
<BubbleReactions
role="img"
aria-label="Reactions: eyes, rocket, and 2 more"
>
<span>👀</span>
<span>🚀</span>
<span>+2</span>
</BubbleReactions>
</Bubble>
<Bubble variant="default" align="end">
<BubbleContent>
Tests passed on the first try. All 142 of them. Looking good!
</BubbleContent>
<BubbleReactions
side="top"
align="start"
role="img"
aria-label="Reactions: party popper, clapping hands"
>
<span>🎉</span>
<span>👏</span>
</BubbleReactions>
</Bubble>
<Bubble variant="destructive">
<BubbleContent>Are you sure I can run this command?</BubbleContent>
<BubbleReactions>
<Button
variant="ghost"
size="xs"
onClick={() => toast.success("You clicked yes, running command...")}
>
Yes, run it
</Button>
</BubbleReactions>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,30 @@
import { CheckIcon } from "lucide-react"
import {
Bubble,
BubbleContent,
BubbleReactions,
} from "@/styles/aria-rhea/ui/bubble"
import { Button } from "@/styles/aria-rhea/ui/button"
import { Tooltip, TooltipTrigger } from "@/styles/aria-rhea/ui/tooltip"
export function BubbleTooltipDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-4 py-12">
<Bubble variant="secondary">
<BubbleContent>Did you remove the stale route?</BubbleContent>
</Bubble>
<Bubble align="end">
<BubbleContent>Yes, removed it from the registry.</BubbleContent>
<BubbleReactions>
<TooltipTrigger>
<Button variant="ghost" size="icon-xs">
<CheckIcon />
</Button>
<Tooltip>Read on Jan 5, 2026 at 4:32 PM</Tooltip>
</TooltipTrigger>
</BubbleReactions>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,52 @@
import { Markdown } from "@/components/markdown"
import {
Bubble,
BubbleContent,
BubbleReactions,
} from "@/styles/aria-rhea/ui/bubble"
export function BubbleVariantsDemo() {
return (
<div className="flex w-full max-w-sm flex-col gap-12 py-12">
<Bubble>
<BubbleContent>This is the default primary bubble.</BubbleContent>
</Bubble>
<Bubble variant="secondary" align="end">
<BubbleContent>This is the secondary variant.</BubbleContent>
</Bubble>
<Bubble variant="muted">
<BubbleContent>
This one is muted. It uses a lower emphasis color for the chat bubble.
</BubbleContent>
<BubbleReactions role="img" aria-label="Reaction: thumbs up">
<span>👍</span>
</BubbleReactions>
</Bubble>
<Bubble variant="tinted" align="end">
<BubbleContent>
This one is tinted. The tint is a softer color derived from the
primary color.
</BubbleContent>
</Bubble>
<Bubble variant="outline">
<BubbleContent>We can also use an outlined variant.</BubbleContent>
</Bubble>
<Bubble variant="destructive" align="end">
<BubbleContent>Or a destructive variant with a reaction.</BubbleContent>
<BubbleReactions role="img" aria-label="Reaction: fire">
<span>🔥</span>
</BubbleReactions>
</Bubble>
<Bubble variant="ghost">
<BubbleContent>
<Markdown>{`Ghost bubbles work for assistant text, **markdown**, and other content that should not be framed.
This is perfect for assistant messages that should not have a frame and can take the full width of the container. You can also render \`code\` in it.
Ghost bubbles are full width and can take the full width of the container.
`}</Markdown>
</BubbleContent>
</Bubble>
</div>
)
}

View File

@@ -0,0 +1,5 @@
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonDefault() {
return <Button>Button</Button>
}

View File

@@ -0,0 +1,14 @@
import { ArrowUpIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonDemo() {
return (
<div className="flex flex-wrap items-center gap-2 md:flex-row">
<Button variant="outline">Button</Button>
<Button variant="outline" size="icon" aria-label="Submit">
<ArrowUpIcon />
</Button>
</div>
)
}

View File

@@ -0,0 +1,5 @@
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonDestructive() {
return <Button variant="destructive">Destructive</Button>
}

View File

@@ -0,0 +1,5 @@
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonGhost() {
return <Button variant="ghost">Ghost</Button>
}

View File

@@ -0,0 +1,102 @@
"use client"
import * as React from "react"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
import {
DropdownMenu,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/styles/aria-nova/ui/dropdown-menu"
export default function ButtonGroupDemo() {
const [label, setLabel] = React.useState("personal")
return (
<ButtonGroup>
<ButtonGroup className="hidden sm:flex">
<Button variant="outline" size="icon" aria-label="Go Back">
<ArrowLeftIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Archive</Button>
<Button variant="outline">Report</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Snooze</Button>
<DropdownMenuTrigger>
<Button variant="outline" size="icon" aria-label="More Options">
<MoreHorizontalIcon />
</Button>
<DropdownMenu placement="bottom end" className="w-40">
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
Archive
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<ClockIcon />
Snooze
</DropdownMenuItem>
<DropdownMenuItem>
<CalendarPlusIcon />
Add to Calendar
</DropdownMenuItem>
<DropdownMenuItem>
<ListFilterIcon />
Add to List
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<TagIcon />
Label As...
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
selectionMode="single"
selectedKeys={[label]}
onSelectionChange={(keys) => setLabel([...keys][0] as string)}
>
<DropdownMenuItem id="personal">Personal</DropdownMenuItem>
<DropdownMenuItem id="work">Work</DropdownMenuItem>
<DropdownMenuItem id="other">Other</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
Trash
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,70 @@
"use client"
import {
AlertTriangleIcon,
CheckIcon,
ChevronDownIcon,
CopyIcon,
ShareIcon,
TrashIcon,
UserRoundXIcon,
VolumeOffIcon,
} from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
import {
DropdownMenu,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/styles/aria-nova/ui/dropdown-menu"
export default function ButtonGroupDropdown() {
return (
<ButtonGroup>
<Button variant="outline">Follow</Button>
<DropdownMenuTrigger>
<Button variant="outline" className="pl-2!">
<ChevronDownIcon />
</Button>
<DropdownMenu placement="bottom end" className="w-44">
<DropdownMenuGroup>
<DropdownMenuItem>
<VolumeOffIcon />
Mute Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<CheckIcon />
Mark as Read
</DropdownMenuItem>
<DropdownMenuItem>
<AlertTriangleIcon />
Report Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<UserRoundXIcon />
Block User
</DropdownMenuItem>
<DropdownMenuItem>
<ShareIcon />
Share Conversation
</DropdownMenuItem>
<DropdownMenuItem>
<CopyIcon />
Copy Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<TrashIcon />
Delete Conversation
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,52 @@
"use client"
import * as React from "react"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/styles/aria-nova/ui/input-group"
import { Tooltip, TooltipTrigger } from "@/styles/aria-nova/ui/tooltip"
export default function ButtonGroupInputGroup() {
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
return (
<ButtonGroup className="[--radius:9999rem]">
<ButtonGroup>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<InputGroup>
<InputGroupInput
placeholder={
voiceEnabled ? "Record and send audio..." : "Send a message..."
}
disabled={voiceEnabled}
/>
<InputGroupAddon align="inline-end">
<TooltipTrigger>
<InputGroupButton
onClick={() => setVoiceEnabled(!voiceEnabled)}
size="icon-xs"
data-active={voiceEnabled}
className="data-[active=true]:bg-orange-100 data-[active=true]:text-orange-700 dark:data-[active=true]:bg-orange-800 dark:data-[active=true]:text-orange-100"
aria-pressed={voiceEnabled}
>
<AudioLinesIcon />
</InputGroupButton>
<Tooltip>Voice Mode</Tooltip>
</TooltipTrigger>
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,16 @@
import { SearchIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
import { Input } from "@/styles/aria-nova/ui/input"
export default function ButtonGroupInput() {
return (
<ButtonGroup>
<Input placeholder="Search..." />
<Button variant="outline" aria-label="Search">
<SearchIcon />
</Button>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,34 @@
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
import { Input } from "@/styles/aria-nova/ui/input"
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
} from "@/styles/aria-nova/ui/input-group"
import { Tooltip, TooltipTrigger } from "@/styles/aria-nova/ui/tooltip"
export function ButtonGroupNested() {
return (
<ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<InputGroup>
<InputGroupInput placeholder="Send a message..." />
<TooltipTrigger>
<InputGroupAddon align="inline-end">
<AudioLinesIcon />
</InputGroupAddon>
<Tooltip>Voice Mode</Tooltip>
</TooltipTrigger>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,21 @@
import { MinusIcon, PlusIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
export default function ButtonGroupOrientation() {
return (
<ButtonGroup
orientation="vertical"
aria-label="Media controls"
className="h-fit"
>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
<Button variant="outline" size="icon">
<MinusIcon />
</Button>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,53 @@
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
import {
Field,
FieldDescription,
FieldLabel,
} from "@/styles/aria-nova/ui/field"
import {
Popover,
PopoverDescription,
PopoverHeader,
PopoverTitle,
PopoverTrigger,
} from "@/styles/aria-nova/ui/popover"
import { Textarea } from "@/styles/aria-nova/ui/textarea"
export default function ButtonGroupPopover() {
return (
<ButtonGroup>
<Button variant="outline">
<BotIcon /> Copilot
</Button>
<PopoverTrigger>
<Button variant="outline" size="icon" aria-label="Open Popover">
<ChevronDownIcon />
</Button>
<Popover placement="bottom end" className="rounded-xl text-sm">
<PopoverHeader>
<PopoverTitle>Start a new task with Copilot</PopoverTitle>
<PopoverDescription>
Describe your task in natural language.
</PopoverDescription>
</PopoverHeader>
<Field>
<FieldLabel htmlFor="task" className="sr-only">
Task Description
</FieldLabel>
<Textarea
id="task"
placeholder="I need to..."
className="resize-none"
/>
<FieldDescription>
Copilot will open a pull request for review.
</FieldDescription>
</Field>
</Popover>
</PopoverTrigger>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,171 @@
"use client"
import * as React from "react"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import { Button } from "@/styles/aria-nova/ui-rtl/button"
import { ButtonGroup } from "@/styles/aria-nova/ui-rtl/button-group"
import {
DropdownMenu,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/styles/aria-nova/ui-rtl/dropdown-menu"
const translations: Translations = {
en: {
dir: "ltr",
values: {
archive: "Archive",
report: "Report",
snooze: "Snooze",
markAsRead: "Mark as Read",
addToCalendar: "Add to Calendar",
addToList: "Add to List",
labelAs: "Label As...",
personal: "Personal",
work: "Work",
other: "Other",
trash: "Trash",
},
},
ar: {
dir: "rtl",
values: {
archive: "أرشفة",
report: "تقرير",
snooze: "تأجيل",
markAsRead: "وضع علامة كمقروء",
addToCalendar: "إضافة إلى التقويم",
addToList: "إضافة إلى القائمة",
labelAs: "تصنيف كـ...",
personal: "شخصي",
work: "عمل",
other: "آخر",
trash: "سلة المهملات",
},
},
he: {
dir: "rtl",
values: {
archive: "ארכיון",
report: "דוח",
snooze: "דחה",
markAsRead: "סמן כנקרא",
addToCalendar: "הוסף ליומן",
addToList: "הוסף לרשימה",
labelAs: "תייג כ...",
personal: "אישי",
work: "עבודה",
other: "אחר",
trash: "פח",
},
},
}
export function ButtonGroupRtl() {
const { dir, t, language } = useTranslation(translations, "ar")
const [label, setLabel] = React.useState("personal")
return (
<div dir={dir}>
<ButtonGroup>
<ButtonGroup className="hidden sm:flex">
<Button variant="outline" size="icon" aria-label="Go Back">
<ArrowLeftIcon className="rtl:rotate-180" />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">{t.archive}</Button>
<Button variant="outline">{t.report}</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">{t.snooze}</Button>
<DropdownMenuTrigger>
<Button variant="outline" size="icon" aria-label="More Options">
<MoreHorizontalIcon />
</Button>
<DropdownMenu
placement={dir === "rtl" ? "bottom start" : "bottom end"}
data-lang={dir === "rtl" ? language : undefined}
dir={dir}
className="w-40"
>
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />
{t.markAsRead}
</DropdownMenuItem>
<DropdownMenuItem>
<ArchiveIcon />
{t.archive}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<ClockIcon />
{t.snooze}
</DropdownMenuItem>
<DropdownMenuItem>
<CalendarPlusIcon />
{t.addToCalendar}
</DropdownMenuItem>
<DropdownMenuItem>
<ListFilterIcon />
{t.addToList}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<TagIcon />
{t.labelAs}
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
dir={dir}
data-lang={dir === "rtl" ? language : undefined}
selectionMode="single"
selectedKeys={[label]}
onSelectionChange={(keys) =>
setLabel([...keys][0] as string)
}
>
<DropdownMenuItem id="personal">
{t.personal}
</DropdownMenuItem>
<DropdownMenuItem id="work">{t.work}</DropdownMenuItem>
<DropdownMenuItem id="other">{t.other}</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
{t.trash}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenu>
</DropdownMenuTrigger>
</ButtonGroup>
</ButtonGroup>
</div>
)
}

View File

@@ -0,0 +1,54 @@
"use client"
import * as React from "react"
import { ArrowRightIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
import { Input } from "@/styles/aria-nova/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
} from "@/styles/aria-nova/ui/select"
const CURRENCIES = [
{ label: "US Dollar", value: "$" },
{ label: "Euro", value: "€" },
{ label: "British Pound", value: "£" },
]
export default function ButtonGroupSelect() {
const [currency, setCurrency] = React.useState("$")
return (
<ButtonGroup>
<ButtonGroup>
<Select
value={currency}
onChange={(value) => setCurrency(value as string)}
>
<SelectTrigger className="font-mono">{currency}</SelectTrigger>
<SelectContent placement="bottom start">
<SelectGroup>
{CURRENCIES.map((item) => (
<SelectItem key={item.value} id={item.value}>
{item.value}{" "}
<span className="text-muted-foreground">{item.label}</span>
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<Input placeholder="10.00" pattern="[0-9]*" />
</ButtonGroup>
<ButtonGroup>
<Button aria-label="Send" size="icon" variant="outline">
<ArrowRightIcon />
</Button>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,19 @@
import { Button } from "@/styles/aria-nova/ui/button"
import {
ButtonGroup,
ButtonGroupSeparator,
} from "@/styles/aria-nova/ui/button-group"
export default function ButtonGroupSeparatorDemo() {
return (
<ButtonGroup>
<Button variant="secondary" size="sm">
Copy
</Button>
<ButtonGroupSeparator />
<Button variant="secondary" size="sm">
Paste
</Button>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,47 @@
import { PlusIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
import { ButtonGroup } from "@/styles/aria-nova/ui/button-group"
export default function ButtonGroupSize() {
return (
<div className="flex flex-col items-start gap-8">
<ButtonGroup>
<Button variant="outline" size="sm">
Small
</Button>
<Button variant="outline" size="sm">
Button
</Button>
<Button variant="outline" size="sm">
Group
</Button>
<Button variant="outline" size="icon-sm">
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline">Default</Button>
<Button variant="outline">Button</Button>
<Button variant="outline">Group</Button>
<Button variant="outline" size="icon">
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="lg">
Large
</Button>
<Button variant="outline" size="lg">
Button
</Button>
<Button variant="outline" size="lg">
Group
</Button>
<Button variant="outline" size="icon-lg">
<PlusIcon />
</Button>
</ButtonGroup>
</div>
)
}

View File

@@ -0,0 +1,19 @@
import { IconPlus } from "@tabler/icons-react"
import { Button } from "@/styles/aria-nova/ui/button"
import {
ButtonGroup,
ButtonGroupSeparator,
} from "@/styles/aria-nova/ui/button-group"
export default function ButtonGroupSplit() {
return (
<ButtonGroup>
<Button variant="secondary">Button</Button>
<ButtonGroupSeparator />
<Button size="icon" variant="secondary">
<IconPlus />
</Button>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,11 @@
import { CircleFadingArrowUpIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonIcon() {
return (
<Button variant="outline" size="icon">
<CircleFadingArrowUpIcon />
</Button>
)
}

View File

@@ -0,0 +1,5 @@
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonLink() {
return <Button variant="link">Link</Button>
}

View File

@@ -0,0 +1,5 @@
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonOutline() {
return <Button variant="outline">Outline</Button>
}

View File

@@ -0,0 +1,14 @@
"use client"
import { buttonVariants } from "@/styles/aria-nova/ui/button"
export default function ButtonRender() {
return (
<a
href="#"
className={buttonVariants({ variant: "secondary", size: "sm" })}
>
Login
</a>
)
}

View File

@@ -0,0 +1,14 @@
import { ArrowUpIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonRounded() {
return (
<div className="flex gap-2">
<Button className="rounded-full">Get Started</Button>
<Button variant="outline" size="icon" className="rounded-full">
<ArrowUpIcon />
</Button>
</div>
)
}

View File

@@ -0,0 +1,61 @@
"use client"
import { ArrowRightIcon, PlusIcon } from "lucide-react"
import {
useTranslation,
type Translations,
} from "@/components/language-selector"
import { Button } from "@/styles/aria-nova/ui-rtl/button"
import { Spinner } from "@/styles/aria-nova/ui-rtl/spinner"
const translations: Translations = {
en: {
dir: "ltr",
values: {
button: "Button",
submit: "Submit",
delete: "Delete",
loading: "Loading",
},
},
ar: {
dir: "rtl",
values: {
button: "زر",
submit: "إرسال",
delete: "حذف",
loading: "جاري التحميل",
},
},
he: {
dir: "rtl",
values: {
button: "כפתור",
submit: "שלח",
delete: "מחק",
loading: "טוען",
},
},
}
export function ButtonRtl() {
const { dir, t } = useTranslation(translations, "ar")
return (
<div className="flex flex-wrap items-center gap-2 md:flex-row" dir={dir}>
<Button variant="outline">{t.button}</Button>
<Button variant="destructive">{t.delete}</Button>
<Button variant="outline">
{t.submit}{" "}
<ArrowRightIcon className="rtl:rotate-180" data-icon="inline-end" />
</Button>
<Button variant="outline" size="icon" aria-label="Add">
<PlusIcon />
</Button>
<Button variant="secondary" isDisabled>
<Spinner data-icon="inline-start" /> {t.loading}
</Button>
</div>
)
}

View File

@@ -0,0 +1,5 @@
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonSecondary() {
return <Button variant="secondary">Secondary</Button>
}

View File

@@ -0,0 +1,40 @@
import { ArrowUpRightIcon } from "lucide-react"
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonSize() {
return (
<div className="flex flex-col items-start gap-8 sm:flex-row">
<div className="flex items-start gap-2">
<Button size="xs" variant="outline">
Extra Small
</Button>
<Button size="icon-xs" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
<div className="flex items-start gap-2">
<Button size="sm" variant="outline">
Small
</Button>
<Button size="icon-sm" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
<div className="flex items-start gap-2">
<Button variant="outline">Default</Button>
<Button size="icon" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
<div className="flex items-start gap-2">
<Button variant="outline" size="lg">
Large
</Button>
<Button size="icon-lg" aria-label="Submit" variant="outline">
<ArrowUpRightIcon />
</Button>
</div>
</div>
)
}

View File

@@ -0,0 +1,17 @@
import { Button } from "@/styles/aria-nova/ui/button"
import { Spinner } from "@/styles/aria-nova/ui/spinner"
export default function ButtonLoading() {
return (
<div className="flex gap-2">
<Button variant="outline" isDisabled>
<Spinner data-icon="inline-start" />
Generating
</Button>
<Button variant="secondary" isDisabled>
Downloading
<Spinner data-icon="inline-start" />
</Button>
</div>
)
}

View File

@@ -0,0 +1,17 @@
import { IconGitBranch, IconGitFork } from "@tabler/icons-react"
import { Button } from "@/styles/aria-nova/ui/button"
export default function ButtonWithIcon() {
return (
<div className="flex gap-2">
<Button variant="outline">
<IconGitBranch data-icon="inline-start" /> New Branch
</Button>
<Button variant="outline">
Fork
<IconGitFork data-icon="inline-end" />
</Button>
</div>
)
}

View File

@@ -0,0 +1,7 @@
"use client"
import { Calendar } from "@/styles/aria-nova/ui/calendar"
export default function CalendarBasic() {
return <Calendar className="rounded-lg border" />
}

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import { CalendarDate, isSameDay } from "@internationalized/date"
import { Calendar } from "@/styles/aria-nova/ui/calendar"
import { Card, CardContent } from "@/styles/aria-nova/ui/card"
export function CalendarBookedDates() {
const [date, setDate] = React.useState<CalendarDate | undefined>(
new CalendarDate(new Date().getFullYear(), 2, 3)
)
const bookedDates = Array.from(
{ length: 15 },
(_, i) => new CalendarDate(new Date().getFullYear(), 2, 12 + i)
)
return (
<Card className="mx-auto w-fit p-0">
<CardContent className="p-0">
<Calendar
value={date}
onChange={setDate}
isDateUnavailable={(date) =>
bookedDates.some((d) => isSameDay(date, d))
}
/>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,7 @@
"use client"
import { Calendar } from "@/styles/aria-nova/ui/calendar"
export function CalendarCaption() {
return <Calendar captionLayout="dropdown" className="rounded-lg border" />
}

View File

@@ -0,0 +1,39 @@
"use client"
import * as React from "react"
import { CalendarDate, isWeekend } from "@internationalized/date"
import { useLocale, type DateRange } from "react-aria-components"
import { RangeCalendar } from "@/styles/aria-nova/ui/calendar"
import { Card, CardContent } from "@/styles/aria-nova/ui/card"
export function CalendarCustomDays() {
const { locale } = useLocale()
const [range, setRange] = React.useState<DateRange | undefined>({
start: new CalendarDate(new Date().getFullYear(), 12, 8),
end: new CalendarDate(new Date().getFullYear(), 12, 8).add({ days: 10 }),
})
return (
<Card className="mx-auto w-fit p-0">
<CardContent className="p-0">
<RangeCalendar
value={range}
onChange={setRange}
numberOfMonths={1}
captionLayout="dropdown"
className="[--cell-size:--spacing(10)] md:[--cell-size:--spacing(12)]"
headerFormat={{ month: "long" }}
renderCell={({ isOutsideMonth, date, defaultChildren }) => (
<>
{defaultChildren}
{!isOutsideMonth && (
<span>{isWeekend(date, locale) ? "$120" : "$100"}</span>
)}
</>
)}
/>
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,25 @@
"use client"
import * as React from "react"
import {
getLocalTimeZone,
today,
type CalendarDate,
} from "@internationalized/date"
import { Calendar } from "@/styles/aria-nova/ui/calendar"
export default function CalendarDemo() {
const [date, setDate] = React.useState<CalendarDate | undefined>(
today(getLocalTimeZone())
)
return (
<Calendar
value={date}
onChange={setDate}
className="rounded-lg border"
captionLayout="dropdown"
/>
)
}

View File

@@ -0,0 +1,236 @@
"use client"
import * as React from "react"
import { Vazirmatn } from "next/font/google"
import { CalendarDate } from "@internationalized/date"
import { cn } from "cn"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { I18nProvider } from "react-aria-components"
import { getDefaultClassNames, type DayButton } from "react-day-picker"
import { DayPicker } from "react-day-picker/persian"
import { Calendar } from "@/styles/aria-nova/ui-rtl/calendar"
import { Button, buttonVariants } from "@/styles/aria-nova/ui/button"
const vazirmatn = Vazirmatn({ subsets: ["arabic"] })
export default function CalendarHijri() {
const [date, setDate] = React.useState<CalendarDate | undefined>(
new CalendarDate(2025, 6, 12)
)
return (
<div className={vazirmatn.className}>
<I18nProvider locale="fa-AF-u-ca-persian">
<Calendar
value={date}
onChange={setDate}
className="rounded-lg border"
/>
</I18nProvider>
</div>
)
}
// // ----------------------------------------------------------------------------
// // The code below is for this example only.
// // For your own calendar, you would edit the calendar.tsx component directly.
// // ----------------------------------------------------------------------------
// function Calendar({
// className,
// classNames,
// showOutsideDays = true,
// captionLayout = "label",
// buttonVariant = "ghost",
// formatters,
// components,
// ...props
// }: React.ComponentProps<typeof DayPicker> & {
// buttonVariant?: React.ComponentProps<typeof Button>["variant"]
// }) {
// const defaultClassNames = getDefaultClassNames()
// return (
// <DayPicker
// showOutsideDays={showOutsideDays}
// className={cn(
// "group/calendar bg-background p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
// String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
// String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
// className
// )}
// captionLayout={captionLayout}
// formatters={{
// formatMonthDropdown: (date) =>
// date.toLocaleString("default", { month: "short" }),
// ...formatters,
// }}
// classNames={{
// root: cn("w-fit", defaultClassNames.root),
// months: cn(
// "relative flex flex-col gap-4 md:flex-row",
// defaultClassNames.months
// ),
// month: cn("flex w-full flex-col gap-4", defaultClassNames.month),
// nav: cn(
// "absolute inset-x-0 top-0 flex w-full items-center justify-between gap-1",
// defaultClassNames.nav
// ),
// button_previous: cn(
// buttonVariants({ variant: buttonVariant }),
// "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
// defaultClassNames.button_previous
// ),
// button_next: cn(
// buttonVariants({ variant: buttonVariant }),
// "size-(--cell-size) p-0 select-none aria-disabled:opacity-50",
// defaultClassNames.button_next
// ),
// month_caption: cn(
// "flex h-(--cell-size) w-full items-center justify-center px-(--cell-size)",
// defaultClassNames.month_caption
// ),
// dropdowns: cn(
// "flex h-(--cell-size) w-full items-center justify-center gap-1.5 text-sm font-medium",
// defaultClassNames.dropdowns
// ),
// dropdown_root: cn(
// "relative rounded-md border border-input shadow-xs has-focus:border-ring has-focus:ring-[3px] has-focus:ring-ring/50",
// defaultClassNames.dropdown_root
// ),
// dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
// caption_label: cn(
// "font-medium select-none",
// captionLayout === "label"
// ? "text-sm"
// : "flex h-8 items-center gap-1 rounded-md pr-1 pl-2 text-sm [&>svg]:size-3.5 [&>svg]:text-muted-foreground",
// defaultClassNames.caption_label
// ),
// table: "w-full border-collapse",
// weekdays: cn("flex", defaultClassNames.weekdays),
// weekday: cn(
// "flex-1 rounded-md text-[0.8rem] font-normal text-muted-foreground select-none",
// defaultClassNames.weekday
// ),
// week: cn("mt-2 flex w-full", defaultClassNames.week),
// week_number_header: cn(
// "w-(--cell-size) select-none",
// defaultClassNames.week_number_header
// ),
// week_number: cn(
// "text-[0.8rem] text-muted-foreground select-none",
// defaultClassNames.week_number
// ),
// day: cn(
// "group/day relative aspect-square h-full w-full p-0 text-center select-none [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md",
// defaultClassNames.day
// ),
// range_start: cn(
// "rounded-l-md bg-accent",
// defaultClassNames.range_start
// ),
// range_middle: cn("rounded-none", defaultClassNames.range_middle),
// range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
// today: cn(
// "rounded-md bg-accent text-accent-foreground data-[selected=true]:rounded-none",
// defaultClassNames.today
// ),
// outside: cn(
// "text-muted-foreground aria-selected:text-muted-foreground",
// defaultClassNames.outside
// ),
// disabled: cn(
// "text-muted-foreground opacity-50",
// defaultClassNames.disabled
// ),
// hidden: cn("invisible", defaultClassNames.hidden),
// ...classNames,
// }}
// components={{
// Root: ({ className, rootRef, ...props }) => {
// return (
// <div
// data-slot="calendar"
// ref={rootRef}
// className={cn(className)}
// {...props}
// />
// )
// },
// Chevron: ({ className, orientation, ...props }) => {
// if (orientation === "left") {
// return (
// <ChevronLeftIcon className={cn("size-4", className)} {...props} />
// )
// }
// if (orientation === "right") {
// return (
// <ChevronRightIcon
// className={cn("size-4", className)}
// {...props}
// />
// )
// }
// return (
// <ChevronDownIcon className={cn("size-4", className)} {...props} />
// )
// },
// DayButton: CalendarDayButton,
// WeekNumber: ({ children, ...props }) => {
// return (
// <td {...props}>
// <div className="flex size-(--cell-size) items-center justify-center text-center">
// {children}
// </div>
// </td>
// )
// },
// ...components,
// }}
// {...props}
// />
// )
// }
// function CalendarDayButton({
// className,
// day,
// modifiers,
// ...props
// }: React.ComponentProps<typeof DayButton>) {
// const defaultClassNames = getDefaultClassNames()
// const ref = React.useRef<HTMLButtonElement>(null)
// React.useEffect(() => {
// if (modifiers.focused) ref.current?.focus()
// }, [modifiers.focused])
// return (
// <Button
// variant="ghost"
// size="icon"
// data-day={day.date.toLocaleDateString()}
// data-selected-single={
// modifiers.selected &&
// !modifiers.range_start &&
// !modifiers.range_end &&
// !modifiers.range_middle
// }
// data-range-start={modifiers.range_start}
// data-range-end={modifiers.range_end}
// data-range-middle={modifiers.range_middle}
// className={cn(
// "flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-[3px] group-data-[focused=true]/day:ring-ring/50 data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground data-[range-middle=true]:rounded-none data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground dark:hover:text-accent-foreground [&>span]:text-xs [&>span]:opacity-70",
// defaultClassNames.day,
// className
// )}
// {...(props as React.ComponentProps<typeof Button>)}
// />
// )
// }

View File

@@ -0,0 +1,12 @@
import { Calendar } from "@/styles/aria-nova/ui/calendar"
import { Card, CardContent } from "@/styles/aria-nova/ui/card"
export function CalendarMultiple() {
return (
<Card className="mx-auto w-fit p-0">
<CardContent className="p-0">
<Calendar selectionMode="multiple" />
</CardContent>
</Card>
)
}

View File

@@ -0,0 +1,57 @@
"use client"
import * as React from "react"
import { CalendarDate, getLocalTimeZone, today } from "@internationalized/date"
import { Button } from "@/styles/aria-nova/ui/button"
import { Calendar } from "@/styles/aria-nova/ui/calendar"
import { Card, CardContent, CardFooter } from "@/styles/aria-nova/ui/card"
export function CalendarWithPresets() {
const [date, setDate] = React.useState<CalendarDate | undefined>(
new CalendarDate(new Date().getFullYear(), 2, 12)
)
const [currentMonth, setCurrentMonth] = React.useState<CalendarDate>(
new CalendarDate(new Date().getFullYear(), new Date().getMonth() + 1, 1)
)
return (
<Card className="mx-auto w-fit max-w-[300px]" size="sm">
<CardContent>
<Calendar
value={date}
onChange={setDate}
focusedValue={currentMonth}
onFocusChange={setCurrentMonth}
weeksInMonth={6}
className="p-0 [--cell-size:--spacing(9.5)]"
/>
</CardContent>
<CardFooter className="flex flex-wrap gap-2 border-t">
{[
{ label: "Today", value: 0 },
{ label: "Tomorrow", value: 1 },
{ label: "In 3 days", value: 3 },
{ label: "In a week", value: 7 },
{ label: "In 2 weeks", value: 14 },
].map((preset) => (
<Button
key={preset.value}
variant="outline"
size="sm"
className="flex-1"
onPress={() => {
const newDate = today(getLocalTimeZone()).add({
days: preset.value,
})
setDate(newDate)
setCurrentMonth(newDate)
}}
>
{preset.label}
</Button>
))}
</CardFooter>
</Card>
)
}

Some files were not shown because too many files have changed in this diff Show More