Compare commits

...

79 Commits

Author SHA1 Message Date
shadcn
9fbd3b1a72 Merge pull request #9552 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-02-04 21:10:30 +04:00
github-actions[bot]
c6dd35a092 chore(release): version packages 2026-02-04 17:07:09 +00:00
shadcn
470c6f42b0 Merge pull request #9539 from shadcn-ui/shadcn/fix-canonical-classes
fix: canonical classes in bases
2026-02-04 21:05:56 +04:00
shadcn
e6956e45ac Merge branch 'main' into shadcn/fix-canonical-classes 2026-02-04 20:44:08 +04:00
shadcn
a2b9dedbb7 fix: resizable version in registry 2026-02-04 19:52:12 +04:00
shadcn
384129609f fix 2026-02-04 19:48:14 +04:00
shadcn
5be0811f01 fix 2026-02-04 19:23:20 +04:00
shadcn
1a10b4671a Merge branch 'main' into shadcn/fix-canonical-classes
# Conflicts:
#	apps/v4/public/r/styles/base-lyra/tooltip.json
#	apps/v4/public/r/styles/base-maia/tooltip.json
#	apps/v4/public/r/styles/base-mira/tooltip.json
#	apps/v4/public/r/styles/base-nova/tooltip.json
#	apps/v4/public/r/styles/base-vega/tooltip.json
#	apps/v4/public/r/styles/radix-lyra/tooltip.json
#	apps/v4/public/r/styles/radix-maia/tooltip.json
#	apps/v4/public/r/styles/radix-mira/tooltip.json
#	apps/v4/public/r/styles/radix-nova/tooltip.json
#	apps/v4/public/r/styles/radix-vega/tooltip.json
2026-02-04 18:25:27 +04:00
shadcn
e7d36b7e21 Merge pull request #9548 from shadcn-ui/shadcn/fix-code-blocks
feat: update tooltip provider handling
2026-02-04 18:14:54 +04:00
shadcn
290fac9115 Merge branch 'main' into shadcn/fix-code-blocks 2026-02-04 18:09:22 +04:00
shadcn
0633333db4 chore: rebuild registry 2026-02-04 18:06:46 +04:00
shadcn
630323ad47 feat: update TooltipProvider handling 2026-02-04 18:04:19 +04:00
shadcn
44a9b3bd12 fix 2026-02-04 15:18:05 +04:00
shadcn
2b879a5ec8 Merge branch 'shadcn/fix-canonical-classes' of github.com:shadcn-ui/ui into shadcn/fix-canonical-classes 2026-02-04 15:11:48 +04:00
shadcn
381f2ef165 fix 2026-02-04 15:11:30 +04:00
shadcn
825ebca3f0 Merge branch 'main' into shadcn/fix-canonical-classes 2026-02-04 13:15:17 +04:00
shadcn
e0063070a6 feat: add a test:apps script 2026-02-04 13:14:04 +04:00
shadcn
013ae51d10 Merge pull request #9541 from francescopesoli/fix/rtl-password-link-margin
fix(rtl): use logical margin for password link alignment
2026-02-04 11:02:36 +04:00
Francesco
44c8f02d06 fix(rtl): use logical margin for password link in RTL examples
Change ml-auto to ms-auto (margin-inline-start) so the Forgot your
  password link aligns correctly in both LTR and RTL layouts.

  Fixes #9515
2026-02-03 18:36:19 +01:00
shadcn
a012542015 fix: duplicate classes 2026-02-03 17:37:11 +04:00
shadcn
926df433a7 fix 2026-02-03 16:43:17 +04:00
shadcn
5c09e0d8fa chore: update canonical classes across styles 2026-02-03 16:41:44 +04:00
shadcn
dba86053f5 fix: canonical classes in base 2026-02-03 14:08:56 +04:00
shadcn
cd188b267d Merge branch 'main' into shadcn/fix-canonical-classes 2026-02-03 11:14:09 +04:00
shadcn
8a09fbaac9 deps: upgrade tailwind 2026-02-03 11:10:54 +04:00
shadcn
9676c8f4ee Merge pull request #9461 from jaem0629/fix/resizable-v4-upgrade
fix(resizable): upgrade to react-resizable-panels v4
2026-02-03 11:09:34 +04:00
Jaem
9b5aeab889 Merge branch 'main' into fix/resizable-v4-upgrade 2026-02-03 09:16:53 +09:00
shadcn
28ebf1b88a Merge pull request #9531 from WebDevSimplified/add-wds-registry
fix: Re-add WDS registry
2026-02-02 21:10:46 +04:00
shadcn
f922e82f53 fix: ring for focus visible 2026-02-02 21:09:10 +04:00
Web Dev Simplified
beec1e060e Add WDS registry 2026-02-02 07:15:58 -06:00
shadcn
26a24d3d5c Merge branch 'fix/resizable-v4-upgrade' of github.com:jaem0629/ui into fix/resizable-v4-upgrade 2026-02-02 16:33:55 +04:00
shadcn
c3c7f03f04 fix: update props, migrate components and add changelog 2026-02-02 16:33:35 +04:00
Jaem
4af29d6c20 Update pnpm-lock.yaml 2026-02-02 21:05:38 +09:00
shadcn
b28f77f893 Merge branch 'main' into fix/resizable-v4-upgrade 2026-02-02 15:57:45 +04:00
shadcn
b8c7ae8088 Merge pull request #9528 from shadcn-ui/changeset-release/main
chore(release): version packages
2026-02-02 15:29:24 +04:00
github-actions[bot]
d21c74fb3a chore(release): version packages 2026-02-02 11:25:41 +00:00
shadcn
d6548b4ae8 Merge pull request #9507 from shadcn-ui/ny-radix-ui
feat: update new-york to radix-ui
2026-02-02 15:24:41 +04:00
shadcn
110a4ec10b docs: add changelog 2026-02-02 15:15:02 +04:00
shadcn
851562f4f2 Merge branch 'ny-radix-ui' of github.com:shadcn-ui/ui into ny-radix-ui 2026-02-02 14:32:21 +04:00
shadcn
b7b839ebc2 chore: changeset 2026-02-02 14:32:01 +04:00
shadcn
8d9be074a3 feat: update migrate radix command 2026-02-02 14:31:40 +04:00
shadcn
a0c077da9e Merge branch 'main' into ny-radix-ui 2026-02-02 13:15:42 +04:00
shadcn
540cd031c3 fix 2026-02-02 13:14:09 +04:00
Jaem
4d9720449f Merge branch 'main' into fix/resizable-v4-upgrade 2026-02-02 15:43:50 +09:00
shadcn
f1e10f3da8 Merge pull request #9495 from withden/patch-3 2026-02-02 08:40:02 +04:00
Denish Navadiya
e2225d4a93 Rename @paceui to @pacekit with updated details
Updated '@paceui' to '@pacekit' with new homepage, URL, and description.
2026-02-02 10:07:22 +05:30
Jaem
444f6889c8 Merge remote-tracking branch 'upstream/main' into fix/resizable-v4-upgrade 2026-02-02 01:16:10 +09:00
Copilot
03a7804c42 Update callout component to use rounded-xl (#9512)
* Initial plan

* Fix callout component to use rounded-lg in style-lyra.css

Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>

* Revert CSS change - callout.tsx already has rounded-lg

Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>

* Change callout rounded class from rounded-lg to rounded-xl

Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: shadcn <124599+shadcn@users.noreply.github.com>
2026-02-01 15:44:23 +04:00
Chad Bell
acc847bed3 docs(select): add SelectGroup to usage examples (#9508) 2026-02-01 10:19:59 +04:00
shadcn
abfa2ddb74 Merge branch 'main' into ny-radix-ui 2026-01-31 21:19:59 +04:00
shadcn
5e92c160dd feat: update new-york to radix-ui 2026-01-31 21:18:49 +04:00
shadcn
d41e857ba3 fix: select group in field-demo (#9504) 2026-01-31 15:32:07 +04:00
dependabot[bot]
99651191cc chore(deps): bump next in /templates/monorepo-next/apps/web (#9499)
Bumps [next](https://github.com/vercel/next.js) from 16.0.10 to 16.1.5.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v16.0.10...v16.1.5)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.1.5
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 15:25:45 +04:00
dependabot[bot]
712285f60e chore(deps): bump eslint from 8.57.1 to 9.26.0 (#9500)
Bumps [eslint](https://github.com/eslint/eslint) from 8.57.1 to 9.26.0.
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/v9.26.0/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v8.57.1...v9.26.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.26.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-31 15:25:21 +04:00
github-actions[bot]
aed95086e0 chore(release): version packages (#9503)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
2026-01-31 15:10:28 +04:00
shadcn
1990280d66 ci: update changesets 2026-01-31 15:04:54 +04:00
shadcn
2bf55c9133 feat: add geist fonts (#9502) 2026-01-31 14:52:43 +04:00
shadcn
3192a3db55 fix: registry script 2026-01-31 11:34:35 +04:00
shadcn
afa2a7adf2 fix 2026-01-30 22:14:48 +04:00
github-actions[bot]
728d8af275 chore(release): version packages (#9363)
* chore(release): version packages

* chore: deps

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: shadcn <m@shadcn.com>
2026-01-30 21:13:27 +04:00
shadcn
38de7fddc2 feat: rtl (#9498)
* feat: rtl

* feat

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* feat: add sidebar

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* chore: changeset

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix
2026-01-30 21:08:39 +04:00
shadcn
4479965555 fix: directory.json 2026-01-30 16:41:57 +04:00
Denish Navadiya
7ea124b25d Rename @paceui to @pacekit with updated details 2026-01-30 17:42:39 +05:30
Jaem
759003c781 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 15:11:00 +09:00
Jaem
6d467d2e1d fix: allow vertical scroll pass-through on code blocks (#9454) 2026-01-27 09:57:47 +04:00
Harit
893cddd2dc add satoriui registry (#9432)
* add satoriui registry

* updated registries.json file

---------

Co-authored-by: Harit Patel <harit.ptl.business.com>
2026-01-27 09:50:14 +04:00
Nicolas Vargas
1781186def fix(docs): update navigation-menu docs package name and add styleName (#9455) 2026-01-27 09:48:54 +04:00
Wolfr
89b9a76368 fix - Update copy (#9453) 2026-01-27 09:47:58 +04:00
Jaem
6529256e98 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 11:57:41 +09:00
Saullo Bretas Silva
0266253841 Fix JSON formatting in registries.json (#9464) 2026-01-27 00:49:18 +04:00
Jaem
4a39de5c56 Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 03:30:02 +09:00
Nirav joshi
e5fda2c139 Fixed: directory json issue for shadcnspace (#9460)
* feat(registry): add my custom registry

* Feat: Added Shadcnspace into  registries.json

* Updated directory.json

---------

Co-authored-by: ShadcnSpace <shadcnspace@gmail.com>
2026-01-26 22:28:06 +04:00
Jaem
d53f7489ce Merge branch 'main' into fix/resizable-v4-upgrade 2026-01-27 03:19:30 +09:00
Jaem
dfe784b44a fix(resizable): upgrade to react-resizable-panels v4
- Update component API: PanelGroup → Group, PanelResizeHandle → Separator
- Update prop: direction → orientation
- Update size values: number → string with units (e.g., "50%")
- Update CSS selectors: data-[panel-group-direction] → aria-[orientation]
- Update controlled component: onLayout → onLayoutChange with Layout type

Closes #9118, #9136, #9200
2026-01-27 03:10:54 +09:00
Usman Sabuwala
40b9de46e9 Fix Base UI dropdown menu links (#9457)
Base UI does not have a `dropdown-menu` but rather just `menu`
This PR fixes the link that lead to Base UI docs
2026-01-26 22:01:09 +04:00
shadcn
6d97ab0b9b Revert "feat(registry): added new registry(@shadcn-space , @shadcn-dashboard)…" (#9458)
This reverts commit d06e84a007.
2026-01-26 21:03:04 +04:00
ShadcnSpace
d06e84a007 feat(registry): added new registry(@shadcn-space , @shadcn-dashboard) (#9102)
* feat(registry): add my custom registry

* Feat: Added Shadcnspace into  registries.json

---------

Co-authored-by: ShadcnSpace <shadcnspace@gmail.com>
Co-authored-by: Nirav joshi <31440272+Niravjoshi-Wrappixel@users.noreply.github.com>
2026-01-26 20:56:47 +04:00
Akash Moradiya
a29185c9cf fix(directory): basecn registry url typo (#9452) 2026-01-26 09:20:03 +04:00
Sitsiilia
84c801ac67 docs(figma): add shadcn/ui components kit by Sitsiilia Bergmann (#9416)
* docs(figma): add shadcn/ui components kit by Sitsiilia Bergmann

* docs: updates

---------

Co-authored-by: shadcn <m@shadcn.com>
2026-01-26 09:06:35 +04:00
1172 changed files with 42605 additions and 7137 deletions

View File

@@ -1,5 +0,0 @@
---
"shadcn": patch
---
Fix: skip all transforms for universal registry items

View File

@@ -1,12 +1,12 @@
// ORIGINALLY FROM CLOUDFLARE WRANGLER:
// https://github.com/cloudflare/wrangler2/blob/main/.github/changeset-version.js
import { exec } from "child_process"
import { execSync } from "child_process"
// This script is used by the `release.yml` workflow to update the version of the packages being released.
// The standard step is only to run `changeset version` but this does not update the package-lock.json file.
// So we also run `npm install`, which does this update.
// The standard step is only to run `changeset version` but this does not update the pnpm-lock.yaml file.
// So we also run `pnpm install`, which does this update.
// This is a workaround until this is handled automatically by `changeset version`.
// See https://github.com/changesets/changesets/issues/421.
exec("npx changeset version")
exec("npm install")
execSync("npx changeset version", { stdio: "inherit" })
execSync("pnpm install --lockfile-only", { stdio: "inherit" })

View File

@@ -56,7 +56,7 @@ export function ButtonGroupDemo() {
<MoreHorizontalIcon />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48 [--radius:1rem]">
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuGroup>
<DropdownMenuItem>
<MailCheckIcon />

View File

@@ -13,6 +13,7 @@ import { Input } from "@/examples/radix/ui/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
@@ -69,18 +70,20 @@ export function FieldDemo() {
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
<SelectGroup>
<SelectItem value="01">01</SelectItem>
<SelectItem value="02">02</SelectItem>
<SelectItem value="03">03</SelectItem>
<SelectItem value="04">04</SelectItem>
<SelectItem value="05">05</SelectItem>
<SelectItem value="06">06</SelectItem>
<SelectItem value="07">07</SelectItem>
<SelectItem value="08">08</SelectItem>
<SelectItem value="09">09</SelectItem>
<SelectItem value="10">10</SelectItem>
<SelectItem value="11">11</SelectItem>
<SelectItem value="12">12</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>
@@ -93,12 +96,14 @@ export function FieldDemo() {
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
<SelectGroup>
<SelectItem value="2024">2024</SelectItem>
<SelectItem value="2025">2025</SelectItem>
<SelectItem value="2026">2026</SelectItem>
<SelectItem value="2027">2027</SelectItem>
<SelectItem value="2028">2028</SelectItem>
<SelectItem value="2029">2029</SelectItem>
</SelectGroup>
</SelectContent>
</Select>
</Field>

View File

@@ -50,7 +50,7 @@ export function FieldHear() {
>
<Field
orientation="horizontal"
className="gap-1.5 overflow-hidden !px-3 !py-1.5 transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:!px-2"
className="gap-1.5 overflow-hidden px-3! py-1.5! transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:px-2!"
>
<Checkbox
value={option.value}

View File

@@ -66,11 +66,7 @@ export function InputGroupDemo() {
<DropdownMenuTrigger asChild>
<InputGroupButton variant="ghost">Auto</InputGroupButton>
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="start"
className="[--radius:0.95rem]"
>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>Auto</DropdownMenuItem>
<DropdownMenuItem>Agent</DropdownMenuItem>
<DropdownMenuItem>Manual</DropdownMenuItem>

View File

@@ -95,12 +95,14 @@ export default async function Page(props: {
<div className="mx-auto flex w-full max-w-[40rem] min-w-0 flex-1 flex-col gap-6 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
<div className="flex flex-col gap-2">
<div className="flex flex-col gap-2">
<div className="flex items-start justify-between">
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">
<div className="flex items-center justify-between md:items-start">
<h1 className="scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl">
{doc.title}
</h1>
<div className="docs-nav bg-background/80 border-border/50 fixed inset-x-0 bottom-0 isolate z-50 flex items-center gap-2 border-t px-6 py-4 backdrop-blur-sm sm:static sm:z-0 sm:border-t-0 sm:bg-transparent sm:px-0 sm:py-1.5 sm:backdrop-blur-none">
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
<div className="docs-nav flex items-center gap-2">
<div className="hidden sm:block">
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
</div>
<div className="ml-auto flex gap-2">
{neighbours.previous && (
<Button
@@ -179,7 +181,7 @@ export default async function Page(props: {
</div>
</div>
</div>
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 lg:flex">
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[90svh] w-(--sidebar-width) flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
<div className="h-(--top-spacing) shrink-0"></div>
{doc.toc?.length ? (
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">

View File

@@ -82,21 +82,25 @@ export default function ChangelogPage() {
<h2 className="font-heading mb-6 text-xl font-semibold tracking-tight">
More Updates
</h2>
<ul className="flex flex-col gap-4">
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
{olderPages.map((page) => {
const data = page.data as ChangelogPageData
const [date, ...titleParts] = data.title.split(" - ")
const title = titleParts.join(" - ")
return (
<li key={page.url} className="flex items-center gap-3">
<Link
href={page.url}
className="font-medium hover:underline"
>
{data.title}
</Link>
</li>
<Link
key={page.url}
href={page.url}
className="bg-surface text-surface-foreground hover:bg-surface/80 flex w-full flex-col rounded-xl px-4 py-3 transition-colors"
>
<span className="text-muted-foreground text-xs">
{date}
</span>
<span className="text-sm font-medium">{title}</span>
</Link>
)
})}
</ul>
</div>
</div>
)}
</div>

View File

@@ -11,7 +11,11 @@ export default function DocsLayout({
<div className="container-wrapper flex flex-1 flex-col px-2">
<SidebarProvider
className="3xl:fixed:container 3xl:fixed:px-3 min-h-min flex-1 items-start px-0 [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--top-spacing:calc(var(--spacing)*4)]"
style={{ "--sidebar-width": "220px" } as React.CSSProperties}
style={
{
"--sidebar-width": "calc(var(--spacing) * 72)",
} as React.CSSProperties
}
>
<DocsSidebar tree={source.pageTree} />
<div className="h-full w-full">{children}</div>

View File

@@ -70,7 +70,7 @@ export default function ExamplesLayout({
</PageNav>
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
<div className="theme-container container flex flex-1 scroll-mt-20 flex-col">
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding md:flex-1 xl:rounded-xl">
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding has-[[data-slot=rtl-components]]:overflow-visible has-[[data-slot=rtl-components]]:border-0 has-[[data-slot=rtl-components]]:bg-transparent md:flex-1 xl:rounded-xl">
{children}
</div>
</div>

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface MaxLengthSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) {

View File

@@ -1,8 +1,8 @@
"use client"
import * as React from "react"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react"
import type { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { useMutationObserver } from "@/hooks/use-mutation-observer"
@@ -29,7 +29,8 @@ import {
import { type Model, type ModelType } from "../data/models"
interface ModelSelectorProps extends PopoverProps {
interface ModelSelectorProps
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
types: readonly ModelType[]
models: Model[]
}

View File

@@ -1,7 +1,6 @@
"use client"
import * as React from "react"
import { Dialog } from "@radix-ui/react-dialog"
import { MoreHorizontal } from "lucide-react"
import { toast } from "sonner"
@@ -16,6 +15,7 @@ import {
} from "@/registry/new-york-v4/ui/alert-dialog"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,

View File

@@ -1,8 +1,8 @@
"use client"
import * as React from "react"
import { type PopoverProps } from "@radix-ui/react-popover"
import { Check, ChevronsUpDown } from "lucide-react"
import type { Popover as PopoverPrimitive } from "radix-ui"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button"
@@ -23,7 +23,8 @@ import {
import { type Preset } from "../data/presets"
interface PresetSelectorProps extends PopoverProps {
interface PresetSelectorProps
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
presets: Preset[]
}

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TemperatureSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function TemperatureSelector({

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type SliderProps } from "@radix-ui/react-slider"
import type { Slider as SliderPrimitive } from "radix-ui"
import {
HoverCard,
@@ -12,7 +12,9 @@ import { Label } from "@/registry/new-york-v4/ui/label"
import { Slider } from "@/registry/new-york-v4/ui/slider"
interface TopPSelectorProps {
defaultValue: SliderProps["defaultValue"]
defaultValue: React.ComponentProps<
typeof SliderPrimitive.Root
>["defaultValue"]
}
export function TopPSelector({ defaultValue }: TopPSelectorProps) {

View File

@@ -0,0 +1,170 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { Input } from "@/examples/base/ui-rtl/input"
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui-rtl/radio-group"
import { Switch } from "@/examples/base/ui-rtl/switch"
import { IconMinus, IconPlus } from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
computeEnvironment: "بيئة الحوسبة",
computeDescription: "اختر بيئة الحوسبة لمجموعتك.",
kubernetes: "كوبرنيتس",
kubernetesDescription:
"تشغيل أحمال عمل GPU على مجموعة مُهيأة بـ K8s. هذا هو الافتراضي.",
virtualMachine: "جهاز افتراضي",
vmDescription: "الوصول إلى مجموعة VM مُهيأة لتشغيل أحمال العمل. (قريبًا)",
numberOfGpus: "عدد وحدات GPU",
gpuDescription: "يمكنك إضافة المزيد لاحقًا.",
decrement: "إنقاص",
increment: "زيادة",
wallpaperTinting: "تلوين الخلفية",
wallpaperDescription: "السماح بتلوين الخلفية.",
},
he: {
dir: "rtl" as const,
computeEnvironment: "סביבת מחשוב",
computeDescription: "בחר את סביבת המחשוב לאשכול שלך.",
kubernetes: "קוברנטיס",
kubernetesDescription:
"הפעל עומסי עבודה של GPU באשכול מוגדר K8s. זו ברירת המחדל.",
virtualMachine: "מכונה וירטואלית",
vmDescription: "גש לאשכול VM מוגדר להפעלת עומסי עבודה. (בקרוב)",
numberOfGpus: "מספר GPUs",
gpuDescription: "תוכל להוסיף עוד מאוחר יותר.",
decrement: "הפחת",
increment: "הגדל",
wallpaperTinting: "צביעת טפט",
wallpaperDescription: "אפשר לטפט להיצבע.",
},
}
export function AppearanceSettings() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [gpuCount, setGpuCount] = React.useState(8)
const handleGpuAdjustment = React.useCallback((adjustment: number) => {
setGpuCount((prevCount) =>
Math.max(1, Math.min(99, prevCount + adjustment))
)
}, [])
const handleGpuInputChange = React.useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value, 10)
if (!isNaN(value) && value >= 1 && value <= 99) {
setGpuCount(value)
}
},
[]
)
return (
<div dir={t.dir}>
<FieldSet>
<FieldGroup>
<FieldSet>
<FieldLegend>{t.computeEnvironment}</FieldLegend>
<FieldDescription>{t.computeDescription}</FieldDescription>
<RadioGroup defaultValue="kubernetes">
<FieldLabel htmlFor="rtl-kubernetes">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{t.kubernetes}</FieldTitle>
<FieldDescription>
{t.kubernetesDescription}
</FieldDescription>
</FieldContent>
<RadioGroupItem
value="kubernetes"
id="rtl-kubernetes"
aria-label={t.kubernetes}
/>
</Field>
</FieldLabel>
<FieldLabel htmlFor="rtl-vm">
<Field orientation="horizontal">
<FieldContent>
<FieldTitle>{t.virtualMachine}</FieldTitle>
<FieldDescription>{t.vmDescription}</FieldDescription>
</FieldContent>
<RadioGroupItem
value="vm"
id="rtl-vm"
aria-label={t.virtualMachine}
/>
</Field>
</FieldLabel>
</RadioGroup>
</FieldSet>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="rtl-gpu-count">{t.numberOfGpus}</FieldLabel>
<FieldDescription>{t.gpuDescription}</FieldDescription>
</FieldContent>
<ButtonGroup>
<Input
id="rtl-gpu-count"
value={gpuCount}
onChange={handleGpuInputChange}
size={3}
className="h-7 !w-14 font-mono"
maxLength={3}
/>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label={t.decrement}
onClick={() => handleGpuAdjustment(-1)}
disabled={gpuCount <= 1}
>
<IconMinus />
</Button>
<Button
variant="outline"
size="icon-sm"
type="button"
aria-label={t.increment}
onClick={() => handleGpuAdjustment(1)}
disabled={gpuCount >= 99}
>
<IconPlus />
</Button>
</ButtonGroup>
</Field>
<FieldSeparator />
<Field orientation="horizontal">
<FieldContent>
<FieldLabel htmlFor="rtl-tinting">
{t.wallpaperTinting}
</FieldLabel>
<FieldDescription>{t.wallpaperDescription}</FieldDescription>
</FieldContent>
<Switch id="rtl-tinting" defaultChecked />
</Field>
</FieldGroup>
</FieldSet>
</div>
)
}

View File

@@ -0,0 +1,179 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import {
ArchiveIcon,
ArrowLeftIcon,
CalendarPlusIcon,
ClockIcon,
ListFilterIcon,
MailCheckIcon,
MoreHorizontalIcon,
TagIcon,
Trash2Icon,
} from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
goBack: "رجوع",
archive: "أرشفة",
report: "إبلاغ",
snooze: "تأجيل",
moreOptions: "خيارات أخرى",
markAsRead: "تحديد كمقروء",
addToCalendar: "إضافة إلى التقويم",
addToList: "إضافة إلى القائمة",
labelAs: "تصنيف كـ...",
personal: "شخصي",
work: "عمل",
other: "أخرى",
trash: "حذف",
},
he: {
dir: "rtl" as const,
goBack: "חזור",
archive: "ארכיון",
report: "דווח",
snooze: "נודניק",
moreOptions: "אפשרויות נוספות",
markAsRead: "סמן כנקרא",
addToCalendar: "הוסף ליומן",
addToList: "הוסף לרשימה",
labelAs: "תייג כ...",
personal: "אישי",
work: "עבודה",
other: "אחר",
trash: "מחק",
},
}
export function ButtonGroupDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [label, setLabel] = React.useState("personal")
return (
<div dir={t.dir}>
<ButtonGroup>
<ButtonGroup className="hidden sm:flex">
<Button variant="outline" size="icon-sm" aria-label={t.goBack}>
<ArrowLeftIcon className="rtl:rotate-180" />
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
{t.archive}
</Button>
<Button variant="outline" size="sm">
{t.report}
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="sm">
{t.snooze}
</Button>
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label={t.moreOptions}
/>
}
>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent
align="start"
dir={t.dir}
data-lang={lang}
className="w-44"
>
<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>
<DropdownMenuPortal>
<DropdownMenuSubContent
side="left"
dir={t.dir}
data-lang={lang}
>
<DropdownMenuRadioGroup
value={label}
onValueChange={setLabel}
>
<DropdownMenuRadioItem value="personal">
{t.personal}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="work">
{t.work}
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="other">
{t.other}
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem variant="destructive">
<Trash2Icon />
{t.trash}
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
</ButtonGroup>
</ButtonGroup>
</div>
)
}

View File

@@ -0,0 +1,82 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/examples/base/ui-rtl/input-group"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import { AudioLinesIcon, PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
add: "إضافة",
voicePlaceholder: "سجل وأرسل صوتًا...",
messagePlaceholder: "أرسل رسالة...",
voiceMode: "الوضع الصوتي",
},
he: {
dir: "rtl" as const,
add: "הוסף",
voicePlaceholder: "הקלט ושלח אודיו...",
messagePlaceholder: "שלח הודעה...",
voiceMode: "מצב קולי",
},
}
export function ButtonGroupInputGroup() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
return (
<ButtonGroup dir={t.dir}>
<ButtonGroup>
<Button variant="outline" size="icon" aria-label={t.add}>
<PlusIcon />
</Button>
</ButtonGroup>
<ButtonGroup className="flex-1">
<InputGroup>
<InputGroupInput
placeholder={
voiceEnabled ? t.voicePlaceholder : t.messagePlaceholder
}
disabled={voiceEnabled}
/>
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
onClick={() => setVoiceEnabled(!voiceEnabled)}
data-active={voiceEnabled}
className="data-[active=true]:bg-primary data-[active=true]:text-primary-foreground"
aria-pressed={voiceEnabled}
size="icon-xs"
aria-label={t.voiceMode}
/>
}
>
<AudioLinesIcon />
</TooltipTrigger>
<TooltipContent>{t.voiceMode}</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,56 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
previous: "السابق",
next: "التالي",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
previous: "הקודם",
next: "הבא",
},
}
function formatNumber(value: number, locale: string) {
return new Intl.NumberFormat(locale).format(value)
}
export function ButtonGroupNested() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<ButtonGroup dir={t.dir}>
<ButtonGroup>
<Button variant="outline" size="sm">
{formatNumber(1, t.locale)}
</Button>
<Button variant="outline" size="sm">
{formatNumber(2, t.locale)}
</Button>
<Button variant="outline" size="sm">
{formatNumber(3, t.locale)}
</Button>
</ButtonGroup>
<ButtonGroup>
<Button variant="outline" size="icon-sm" aria-label={t.previous}>
<ArrowLeftIcon className="rtl:rotate-180" />
</Button>
<Button variant="outline" size="icon-sm" aria-label={t.next}>
<ArrowRightIcon className="rtl:rotate-180" />
</Button>
</ButtonGroup>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,83 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui-rtl/popover"
import { Separator } from "@/examples/base/ui-rtl/separator"
import { Textarea } from "@/examples/base/ui-rtl/textarea"
import { BotIcon, ChevronDownIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
copilot: "المساعد",
openPopover: "فتح القائمة",
agentTasks: "مهام الوكيل",
placeholder: "صف مهمتك بلغة طبيعية.",
startTask: "ابدأ مهمة جديدة مع المساعد",
description:
"صف مهمتك بلغة طبيعية. سيعمل المساعد في الخلفية ويفتح طلب سحب لمراجعتك.",
},
he: {
dir: "rtl" as const,
copilot: "עוזר",
openPopover: "פתח תפריט",
agentTasks: "משימות סוכן",
placeholder: "תאר את המשימה שלך בשפה טבעית.",
startTask: "התחל משימה חדשה עם העוזר",
description:
"תאר את המשימה שלך בשפה טבעית. העוזר יעבוד ברקע ויפתח בקשת משיכה לבדיקתך.",
},
}
export function ButtonGroupPopover() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<ButtonGroup dir={t.dir}>
<Button variant="outline" size="sm">
<BotIcon /> {t.copilot}
</Button>
<Popover>
<PopoverTrigger
render={
<Button
variant="outline"
size="icon-sm"
aria-label={t.openPopover}
/>
}
>
<ChevronDownIcon />
</PopoverTrigger>
<PopoverContent
align="start"
dir={t.dir}
data-lang={lang}
className="p-0"
>
<div className="px-4 py-3">
<div className="text-sm font-medium">{t.agentTasks}</div>
</div>
<Separator />
<div className="p-4 text-sm *:[p:not(:last-child)]:mb-2">
<Textarea
placeholder={t.placeholder}
className="mb-4 resize-none"
/>
<p className="font-medium">{t.startTask}</p>
<p className="text-muted-foreground">{t.description}</p>
</div>
</PopoverContent>
</Popover>
</ButtonGroup>
)
}

View File

@@ -0,0 +1,78 @@
"use client"
import {
Avatar,
AvatarFallback,
AvatarGroup,
AvatarImage,
} from "@/examples/base/ui-rtl/avatar"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/examples/base/ui-rtl/empty"
import { PlusIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
title: "لا يوجد أعضاء في الفريق",
description: "قم بدعوة فريقك للتعاون في هذا المشروع.",
invite: "دعوة أعضاء",
},
he: {
dir: "rtl" as const,
title: "אין חברי צוות",
description: "הזמן את הצוות שלך לשתף פעולה בפרויקט זה.",
invite: "הזמן חברים",
},
}
export function EmptyAvatarGroup() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<Empty className="flex-none border py-10" dir={t.dir}>
<EmptyHeader>
<EmptyMedia>
<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>
</EmptyMedia>
<EmptyTitle>{t.title}</EmptyTitle>
<EmptyDescription>{t.description}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button size="sm">
<PlusIcon />
{t.invite}
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -0,0 +1,36 @@
"use client"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import { Field, FieldLabel } from "@/examples/base/ui-rtl/field"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
terms: "أوافق على الشروط والأحكام",
},
he: {
dir: "rtl" as const,
terms: "אני מסכים לתנאים וההגבלות",
},
}
export function FieldCheckbox() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const { dir, terms } = translations[lang]
return (
<div dir={dir}>
<FieldLabel htmlFor="checkbox-demo-rtl">
<Field orientation="horizontal">
<Checkbox id="checkbox-demo-rtl" defaultChecked />
<FieldLabel htmlFor="checkbox-demo-rtl" className="line-clamp-1">
{terms}
</FieldLabel>
</Field>
</FieldLabel>
</div>
)
}

View File

@@ -0,0 +1,217 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSeparator,
FieldSet,
} from "@/examples/base/ui-rtl/field"
import { Input } from "@/examples/base/ui-rtl/input"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/examples/base/ui-rtl/select"
import { Textarea } from "@/examples/base/ui-rtl/textarea"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
paymentMethod: "طريقة الدفع",
secureEncrypted: "جميع المعاملات آمنة ومشفرة",
nameOnCard: "الاسم على البطاقة",
namePlaceholder: "أحمد محمد",
cardNumber: "رقم البطاقة",
cardDescription: "أدخل رقمك المكون من 16 رقمًا.",
cvv: "رمز الأمان",
month: "الشهر",
year: "السنة",
billingAddress: "عنوان الفواتير",
billingDescription: "عنوان الفواتير المرتبط بطريقة الدفع الخاصة بك",
sameAsShipping: "نفس عنوان الشحن",
comments: "تعليقات",
commentsPlaceholder: "أضف أي تعليقات إضافية",
submit: "إرسال",
cancel: "إلغاء",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
paymentMethod: "אמצעי תשלום",
secureEncrypted: "כל העסקאות מאובטחות ומוצפנות",
nameOnCard: "שם על הכרטיס",
namePlaceholder: "ישראל ישראלי",
cardNumber: "מספר כרטיס",
cardDescription: "הזן את המספר בן 16 הספרות שלך.",
cvv: "קוד אבטחה",
month: "חודש",
year: "שנה",
billingAddress: "כתובת לחיוב",
billingDescription: "כתובת החיוב המשויכת לאמצעי התשלום שלך",
sameAsShipping: "זהה לכתובת המשלוח",
comments: "הערות",
commentsPlaceholder: "הוסף הערות נוספות",
submit: "שלח",
cancel: "ביטול",
},
}
function formatCardNumber(locale: string) {
const formatter = new Intl.NumberFormat(locale, { useGrouping: false })
return `${formatter.format(1234)} ${formatter.format(5678)} ${formatter.format(9012)} ${formatter.format(3456)}`
}
function formatCvv(locale: string) {
return new Intl.NumberFormat(locale, { useGrouping: false }).format(123)
}
function getMonths(locale: string) {
const formatter = new Intl.NumberFormat(locale, {
minimumIntegerDigits: 2,
useGrouping: false,
})
return Array.from({ length: 12 }, (_, i) => {
const value = String(i + 1).padStart(2, "0")
return { label: formatter.format(i + 1), value }
})
}
function getYears(locale: string) {
const formatter = new Intl.NumberFormat(locale, { useGrouping: false })
return Array.from({ length: 6 }, (_, i) => {
const year = 2024 + i
return { label: formatter.format(year), value: String(year) }
})
}
export function FieldDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const months = getMonths(t.locale)
const years = getYears(t.locale)
const cardPlaceholder = formatCardNumber(t.locale)
const cvvPlaceholder = formatCvv(t.locale)
return (
<div dir={t.dir} className="w-full max-w-md rounded-lg border p-6">
<form>
<FieldGroup>
<FieldSet>
<FieldLegend>{t.paymentMethod}</FieldLegend>
<FieldDescription>{t.secureEncrypted}</FieldDescription>
<FieldGroup>
<Field>
<FieldLabel htmlFor="rtl-card-name">{t.nameOnCard}</FieldLabel>
<Input
id="rtl-card-name"
placeholder={t.namePlaceholder}
required
/>
</Field>
<div className="grid grid-cols-3 gap-4">
<Field className="col-span-2">
<FieldLabel htmlFor="rtl-card-number">
{t.cardNumber}
</FieldLabel>
<Input
id="rtl-card-number"
placeholder={cardPlaceholder}
required
/>
<FieldDescription>{t.cardDescription}</FieldDescription>
</Field>
<Field className="col-span-1">
<FieldLabel htmlFor="rtl-cvv">{t.cvv}</FieldLabel>
<Input id="rtl-cvv" placeholder={cvvPlaceholder} required />
</Field>
</div>
<div className="grid grid-cols-2 gap-4">
<Field>
<FieldLabel htmlFor="rtl-exp-month">{t.month}</FieldLabel>
<Select defaultValue="" items={months}>
<SelectTrigger id="rtl-exp-month">
<SelectValue placeholder="MM" />
</SelectTrigger>
<SelectContent data-lang={lang} dir={t.dir}>
<SelectGroup>
{months.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
<Field>
<FieldLabel htmlFor="rtl-exp-year">{t.year}</FieldLabel>
<Select defaultValue="" items={years}>
<SelectTrigger id="rtl-exp-year">
<SelectValue placeholder="YYYY" />
</SelectTrigger>
<SelectContent data-lang={lang} dir={t.dir}>
<SelectGroup>
{years.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
</Field>
</div>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldLegend>{t.billingAddress}</FieldLegend>
<FieldDescription>{t.billingDescription}</FieldDescription>
<FieldGroup>
<Field orientation="horizontal">
<Checkbox id="rtl-same-as-shipping" defaultChecked />
<FieldLabel
htmlFor="rtl-same-as-shipping"
className="font-normal"
>
{t.sameAsShipping}
</FieldLabel>
</Field>
</FieldGroup>
</FieldSet>
<FieldSeparator />
<FieldSet>
<FieldGroup>
<Field>
<FieldLabel htmlFor="rtl-comments">{t.comments}</FieldLabel>
<Textarea
id="rtl-comments"
placeholder={t.commentsPlaceholder}
className="resize-none"
/>
</Field>
</FieldGroup>
</FieldSet>
<Field orientation="horizontal">
<Button type="submit">{t.submit}</Button>
<Button variant="outline" type="button">
{t.cancel}
</Button>
</Field>
</FieldGroup>
</form>
</div>
)
}

View File

@@ -0,0 +1,90 @@
"use client"
import { Card, CardContent } from "@/examples/base/ui-rtl/card"
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
import {
Field,
FieldDescription,
FieldGroup,
FieldLabel,
FieldLegend,
FieldSet,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
legend: "كيف سمعت عنا؟",
description: "اختر الخيار الذي يصف أفضل طريقة سمعت عنا من خلالها.",
socialMedia: "التواصل الاجتماعي",
searchEngine: "البحث",
referral: "إحالة",
other: "أخرى",
},
he: {
dir: "rtl" as const,
legend: "איך שמעת עלינו?",
description: "בחר את האפשרות שמתארת בצורה הטובה ביותר כיצד שמעת עלינו.",
socialMedia: "חברתיות",
searchEngine: "חיפוש",
referral: "הפניה",
other: "אחר",
},
}
export function FieldHear() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const options = [
{ label: t.socialMedia, value: "social-media" },
{ label: t.searchEngine, value: "search-engine" },
{ label: t.referral, value: "referral" },
{ label: t.other, value: "other" },
]
return (
<div dir={t.dir}>
<Card className="border-0 py-4 shadow-none">
<CardContent className="px-4">
<form>
<FieldGroup>
<FieldSet className="gap-4">
<FieldLegend>{t.legend}</FieldLegend>
<FieldDescription className="line-clamp-1">
{t.description}
</FieldDescription>
<FieldGroup className="flex flex-row flex-wrap gap-2 [--radius:9999rem]">
{options.map((option) => (
<FieldLabel
htmlFor={`rtl-${option.value}`}
key={option.value}
className="!w-fit"
>
<Field
orientation="horizontal"
className="gap-1.5 overflow-hidden px-3! py-1.5! transition-all duration-100 ease-linear group-has-data-[state=checked]/field-label:px-2!"
>
<Checkbox
value={option.value}
id={`rtl-${option.value}`}
defaultChecked={option.value === "social-media"}
className="-ms-6 translate-x-1 rounded-full transition-all duration-100 ease-linear data-checked:ms-0 data-checked:translate-x-0"
/>
<FieldTitle>{option.label}</FieldTitle>
</Field>
</FieldLabel>
))}
</FieldGroup>
</FieldSet>
</FieldGroup>
</form>
</CardContent>
</Card>
</div>
)
}

View File

@@ -0,0 +1,67 @@
"use client"
import { useState } from "react"
import {
Field,
FieldDescription,
FieldTitle,
} from "@/examples/base/ui-rtl/field"
import { Slider } from "@/examples/base/ui-rtl/slider"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
locale: "ar-SA",
title: "نطاق السعر",
description: "حدد نطاق ميزانيتك",
ariaLabel: "نطاق السعر",
currency: "﷼",
},
he: {
dir: "rtl" as const,
locale: "he-IL",
title: "טווח מחירים",
description: "הגדר את טווח התקציב שלך",
ariaLabel: "טווח מחירים",
currency: "₪",
},
}
function formatNumber(value: number, locale: string) {
return new Intl.NumberFormat(locale).format(value)
}
export function FieldSlider() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [value, setValue] = useState([200, 800])
return (
<Field dir={t.dir}>
<FieldTitle>{t.title}</FieldTitle>
<FieldDescription>
{t.description} ({t.currency}
<span className="font-medium tabular-nums">
{formatNumber(value[0], t.locale)}
</span>{" "}
-{" "}
<span className="font-medium tabular-nums">
{formatNumber(value[1], t.locale)}
</span>
).
</FieldDescription>
<Slider
value={value}
onValueChange={(value) => setValue(value as [number, number])}
max={1000}
min={0}
step={10}
className="mt-2 w-full"
aria-label={t.ariaLabel}
/>
</Field>
)
}

View File

@@ -0,0 +1,92 @@
"use client"
import { DirectionProvider } from "@/examples/base/ui-rtl/direction"
import { FieldSeparator } from "@/examples/base/ui-rtl/field"
import {
LanguageProvider,
LanguageSelector,
useLanguageContext,
} from "@/components/language-selector"
import { AppearanceSettings } from "./appearance-settings"
import { ButtonGroupDemo } from "./button-group-demo"
import { ButtonGroupInputGroup } from "./button-group-input-group"
import { ButtonGroupNested } from "./button-group-nested"
import { ButtonGroupPopover } from "./button-group-popover"
import { EmptyAvatarGroup } from "./empty-avatar-group"
import { FieldCheckbox } from "./field-checkbox"
import { FieldDemo } from "./field-demo"
import { FieldHear } from "./field-hear"
import { FieldSlider } from "./field-slider"
import { InputGroupButtonExample } from "./input-group-button"
import { InputGroupDemo } from "./input-group-demo"
import { ItemDemo } from "./item-demo"
import { NotionPromptForm } from "./notion-prompt-form"
import { SpinnerBadge } from "./spinner-badge"
import { SpinnerEmpty } from "./spinner-empty"
function RtlComponentsContent() {
const context = useLanguageContext()
if (!context) {
return null
}
const { language } = context
return (
<div
className="relative grid gap-8 p-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8"
dir="rtl"
data-lang={language}
data-slot="rtl-components"
>
<LanguageSelector
value={language}
onValueChange={context.setLanguage}
className="absolute -top-12 right-52 hidden h-8! data-[size=sm]:rounded-lg lg:flex"
languages={["ar", "he"]}
/>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<FieldDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<EmptyAvatarGroup />
<SpinnerBadge />
<ButtonGroupInputGroup />
<FieldSlider />
<InputGroupDemo />
</div>
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
<InputGroupButtonExample />
<ItemDemo />
<FieldSeparator className="my-4">
{language === "he" ? "הגדרות מראה" : "إعدادات المظهر"}
</FieldSeparator>
<AppearanceSettings />
</div>
<div className="order-first flex flex-col gap-6 lg:hidden xl:order-last xl:flex *:[div]:w-full *:[div]:max-w-full">
<NotionPromptForm />
<ButtonGroupDemo />
<FieldCheckbox />
<div className="flex justify-between gap-4">
<ButtonGroupNested />
<ButtonGroupPopover />
</div>
<FieldHear />
<SpinnerEmpty />
</div>
</div>
)
}
export function RtlComponents() {
return (
<LanguageProvider defaultLanguage="ar">
<DirectionProvider direction="rtl">
<RtlComponentsContent />
</DirectionProvider>
</LanguageProvider>
)
}

View File

@@ -0,0 +1,97 @@
"use client"
import * as React from "react"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
} from "@/examples/base/ui-rtl/input-group"
import { Label } from "@/examples/base/ui-rtl/label"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui-rtl/popover"
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
inputLabel: "السعر",
info: "معلومات",
priceInfo: "أدخل السعر بالريال السعودي.",
priceDescription: "سيتم تحويل السعر تلقائياً.",
favorite: "مفضل",
currency: "ر.س",
},
he: {
dir: "rtl" as const,
inputLabel: "מחיר",
info: "מידע",
priceInfo: "הזן את המחיר בשקלים.",
priceDescription: "המחיר יומר אוטומטית.",
favorite: "מועדף",
currency: "₪",
},
}
export function InputGroupButtonExample() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const [isFavorite, setIsFavorite] = React.useState(false)
return (
<div dir={t.dir} className="grid w-full max-w-sm gap-6">
<Label htmlFor="input-secure-rtl" className="sr-only">
{t.inputLabel}
</Label>
<InputGroup className="[--radius:9999px]">
<InputGroupInput id="input-secure-rtl" className="!pr-0.5" />
<InputGroupAddon>
<Popover>
<PopoverTrigger
render={
<InputGroupButton
variant="secondary"
size="icon-xs"
aria-label={t.info}
/>
}
>
<IconInfoCircle />
</PopoverTrigger>
<PopoverContent
align="end"
alignOffset={10}
className="flex flex-col gap-1 rounded-xl text-sm"
data-lang={lang}
dir={t.dir}
>
<p className="font-medium">{t.priceInfo}</p>
<p>{t.priceDescription}</p>
</PopoverContent>
</Popover>
</InputGroupAddon>
<InputGroupAddon className="text-muted-foreground">
{t.currency}
</InputGroupAddon>
<InputGroupAddon align="inline-end">
<InputGroupButton
onClick={() => setIsFavorite(!isFavorite)}
size="icon-xs"
aria-label={t.favorite}
>
<IconStar
data-favorite={isFavorite}
className="data-[favorite=true]:fill-primary data-[favorite=true]:stroke-primary"
/>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -0,0 +1,140 @@
"use client"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupInput,
InputGroupText,
InputGroupTextarea,
} from "@/examples/base/ui-rtl/input-group"
import { Separator } from "@/examples/base/ui-rtl/separator"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import {
IconCheck,
IconChevronDown,
IconInfoCircle,
IconPlus,
} from "@tabler/icons-react"
import { ArrowUpIcon, Search } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
search: "بحث...",
results: "12 نتيجة",
example: "example.com",
tooltipContent: "هذا محتوى في تلميح.",
askSearchChat: "اسأل، ابحث أو تحدث...",
add: "إضافة",
auto: "تلقائي",
agent: "وكيل",
manual: "يدوي",
used: "52% مستخدم",
send: "إرسال",
},
he: {
dir: "rtl" as const,
search: "חיפוש...",
results: "12 תוצאות",
example: "example.com",
tooltipContent: "זה תוכן בטולטיפ.",
askSearchChat: "שאל, חפש או שוחח...",
add: "הוסף",
auto: "אוטומטי",
agent: "סוכן",
manual: "ידני",
used: "52% בשימוש",
send: "שלח",
},
}
export function InputGroupDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="grid w-full max-w-sm gap-6">
<InputGroup>
<InputGroupInput placeholder={t.search} />
<InputGroupAddon>
<Search />
</InputGroupAddon>
<InputGroupAddon align="inline-end">{t.results}</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder={t.example} />
<InputGroupAddon align="inline-end">
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
className="rounded-full"
size="icon-xs"
aria-label={t.add}
/>
}
>
<IconInfoCircle />
</TooltipTrigger>
<TooltipContent>{t.tooltipContent}</TooltipContent>
</Tooltip>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupTextarea placeholder={t.askSearchChat} />
<InputGroupAddon align="block-end">
<InputGroupButton
variant="outline"
className="rounded-full"
size="icon-xs"
aria-label={t.add}
>
<IconPlus />
</InputGroupButton>
<DropdownMenu>
<DropdownMenuTrigger render={<InputGroupButton variant="ghost" />}>
<IconChevronDown />
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
<DropdownMenuItem>{t.auto}</DropdownMenuItem>
<DropdownMenuItem>{t.agent}</DropdownMenuItem>
<DropdownMenuItem>{t.manual}</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupText className="ms-auto">{t.used}</InputGroupText>
<Separator orientation="vertical" className="!h-4" />
<InputGroupButton
variant="default"
className="rounded-full"
size="icon-xs"
>
<ArrowUpIcon />
<span className="sr-only">{t.send}</span>
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
<InputGroup>
<InputGroupInput placeholder="shadcn" />
<InputGroupAddon align="inline-end">
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
<IconCheck className="size-3 text-white" />
</div>
</InputGroupAddon>
</InputGroup>
</div>
)
}

View File

@@ -0,0 +1,64 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemMedia,
ItemTitle,
} from "@/examples/base/ui-rtl/item"
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
twoFactor: "المصادقة الثنائية",
twoFactorDescription: "التحقق عبر البريد الإلكتروني أو رقم الهاتف.",
enable: "تفعيل",
verified: "تم التحقق من ملفك الشخصي.",
},
he: {
dir: "rtl" as const,
twoFactor: "אימות דו-שלבי",
twoFactorDescription: "אמת באמצעות אימייל או מספר טלפון.",
enable: "הפעל",
verified: "הפרופיל שלך אומת.",
},
}
export function ItemDemo() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="flex w-full max-w-md flex-col gap-6">
<Item variant="outline">
<ItemContent>
<ItemTitle>{t.twoFactor}</ItemTitle>
<ItemDescription className="text-pretty xl:hidden 2xl:block">
{t.twoFactorDescription}
</ItemDescription>
</ItemContent>
<ItemActions>
<Button size="sm">{t.enable}</Button>
</ItemActions>
</Item>
<Item variant="outline" size="sm">
<ItemMedia>
<BadgeCheckIcon className="size-5" />
</ItemMedia>
<ItemContent>
<ItemTitle>{t.verified}</ItemTitle>
</ItemContent>
<ItemActions>
<ChevronRightIcon className="size-4 rtl:rotate-180" />
</ItemActions>
</Item>
</div>
)
}

View File

@@ -0,0 +1,516 @@
"use client"
import { useMemo, useState } from "react"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@/examples/base/ui-rtl/avatar"
import { Badge } from "@/examples/base/ui-rtl/badge"
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/examples/base/ui-rtl/command"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/examples/base/ui-rtl/dropdown-menu"
import { Field, FieldLabel } from "@/examples/base/ui-rtl/field"
import {
InputGroup,
InputGroupAddon,
InputGroupButton,
InputGroupTextarea,
} from "@/examples/base/ui-rtl/input-group"
import { Popover, PopoverContent } from "@/examples/base/ui-rtl/popover"
import { Switch } from "@/examples/base/ui-rtl/switch"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui-rtl/tooltip"
import {
IconApps,
IconArrowUp,
IconAt,
IconBook,
IconCircleDashedPlus,
IconPaperclip,
IconPlus,
IconWorld,
IconX,
} from "@tabler/icons-react"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
prompt: "الأمر",
placeholder: "اسأل، ابحث، أو أنشئ أي شيء...",
addContext: "أضف سياق",
mentionTooltip: "اذكر شخصًا أو صفحة أو تاريخًا",
searchPages: "البحث في الصفحات...",
noPagesFound: "لم يتم العثور على صفحات",
pages: "الصفحات",
users: "المستخدمون",
attachFile: "إرفاق ملف",
selectModel: "اختر نموذج الذكاء الاصطناعي",
selectAgentMode: "اختر وضع الوكيل",
webSearch: "البحث على الويب",
appsIntegrations: "التطبيقات والتكاملات",
allSourcesAccess: "جميع المصادر التي يمكنني الوصول إليها",
findKnowledge: "ابحث أو استخدم المعرفة في...",
noKnowledgeFound: "لم يتم العثور على معرفة",
helpCenter: "مركز المساعدة",
connectApps: "ربط التطبيقات",
searchSourcesNote: "سنبحث فقط في المصادر المحددة هنا.",
send: "إرسال",
allSources: "جميع المصادر",
auto: "تلقائي",
agentMode: "وضع الوكيل",
planMode: "وضع التخطيط",
beta: "تجريبي",
workspace: "مساحة العمل",
meetingNotes: "ملاحظات الاجتماع",
projectDashboard: "لوحة المشروع",
ideasBrainstorming: "أفكار وعصف ذهني",
calendarEvents: "التقويم والأحداث",
documentation: "التوثيق",
goalsObjectives: "الأهداف والغايات",
budgetPlanning: "تخطيط الميزانية",
teamDirectory: "دليل الفريق",
technicalSpecs: "المواصفات التقنية",
analyticsReport: "تقرير التحليلات",
},
he: {
dir: "rtl" as const,
prompt: "פקודה",
placeholder: "שאל, חפש, או צור משהו...",
addContext: "הוסף הקשר",
mentionTooltip: "הזכר אדם, עמוד או תאריך",
searchPages: "חפש עמודים...",
noPagesFound: "לא נמצאו עמודים",
pages: "עמודים",
users: "משתמשים",
attachFile: "צרף קובץ",
selectModel: "בחר מודל AI",
selectAgentMode: "בחר מצב סוכן",
webSearch: "חיפוש באינטרנט",
appsIntegrations: "אפליקציות ואינטגרציות",
allSourcesAccess: "כל המקורות שיש לי גישה אליהם",
findKnowledge: "מצא או השתמש בידע ב...",
noKnowledgeFound: "לא נמצא ידע",
helpCenter: "מרכז עזרה",
connectApps: "חבר אפליקציות",
searchSourcesNote: "נחפש רק במקורות שנבחרו כאן.",
send: "שלח",
allSources: "כל המקורות",
auto: "אוטומטי",
agentMode: "מצב סוכן",
planMode: "מצב תכנון",
beta: "בטא",
workspace: "סביבת עבודה",
meetingNotes: "הערות פגישה",
projectDashboard: "לוח מחוונים לפרויקט",
ideasBrainstorming: "רעיונות וסיעור מוחות",
calendarEvents: "יומן ואירועים",
documentation: "תיעוד",
goalsObjectives: "מטרות ויעדים",
budgetPlanning: "תכנון תקציב",
teamDirectory: "ספריית צוות",
technicalSpecs: "מפרט טכני",
analyticsReport: "דוח אנליטיקה",
},
}
function MentionableIcon({
item,
}: {
item: { type: string; title: string; image: string }
}) {
return item.type === "page" ? (
<span className="flex size-4 items-center justify-center">
{item.image}
</span>
) : (
<Avatar className="size-4">
<AvatarImage src={item.image} />
<AvatarFallback>{item.title[0]}</AvatarFallback>
</Avatar>
)
}
export function NotionPromptForm() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
const SAMPLE_DATA = useMemo(
() => ({
mentionable: [
{ type: "page", title: t.meetingNotes, image: "📝" },
{ type: "page", title: t.projectDashboard, image: "📊" },
{ type: "page", title: t.ideasBrainstorming, image: "💡" },
{ type: "page", title: t.calendarEvents, image: "📅" },
{ type: "page", title: t.documentation, image: "📚" },
{ type: "page", title: t.goalsObjectives, image: "🎯" },
{ type: "page", title: t.budgetPlanning, image: "💰" },
{ type: "page", title: t.teamDirectory, image: "👥" },
{ type: "page", title: t.technicalSpecs, image: "🔧" },
{ type: "page", title: t.analyticsReport, image: "📈" },
{
type: "user",
title: "shadcn",
image: "https://github.com/shadcn.png",
workspace: t.workspace,
},
{
type: "user",
title: "maxleiter",
image: "https://github.com/maxleiter.png",
workspace: t.workspace,
},
{
type: "user",
title: "evilrabbit",
image: "https://github.com/evilrabbit.png",
workspace: t.workspace,
},
],
models: [
{ name: t.auto },
{ name: t.agentMode, badge: t.beta },
{ name: t.planMode },
],
}),
[t]
)
const [mentions, setMentions] = useState<string[]>([])
const [mentionPopoverOpen, setMentionPopoverOpen] = useState(false)
const [modelPopoverOpen, setModelPopoverOpen] = useState(false)
const [selectedModel, setSelectedModel] = useState<
(typeof SAMPLE_DATA.models)[0]
>(SAMPLE_DATA.models[0])
const [scopeMenuOpen, setScopeMenuOpen] = useState(false)
const grouped = useMemo(() => {
return SAMPLE_DATA.mentionable.reduce(
(acc, item) => {
const isAvailable = !mentions.includes(item.title)
if (isAvailable) {
if (!acc[item.type]) {
acc[item.type] = []
}
acc[item.type].push(item)
}
return acc
},
{} as Record<string, typeof SAMPLE_DATA.mentionable>
)
}, [mentions, SAMPLE_DATA])
const hasMentions = mentions.length > 0
return (
<div dir={t.dir}>
<form>
<Field>
<FieldLabel htmlFor="rtl-notion-prompt" className="sr-only">
{t.prompt}
</FieldLabel>
<InputGroup>
<InputGroupTextarea
id="rtl-notion-prompt"
placeholder={t.placeholder}
/>
<InputGroupAddon align="block-start">
<Popover
open={mentionPopoverOpen}
onOpenChange={setMentionPopoverOpen}
>
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
variant="outline"
size={!hasMentions ? "sm" : "icon-sm"}
className="rounded-full transition-transform"
/>
}
onFocusCapture={(e) => e.stopPropagation()}
>
<IconAt /> {!hasMentions && t.addContext}
</TooltipTrigger>
<TooltipContent>{t.mentionTooltip}</TooltipContent>
</Tooltip>
<PopoverContent className="p-0" align="start" dir={t.dir}>
<Command>
<CommandInput placeholder={t.searchPages} />
<CommandList>
<CommandEmpty>{t.noPagesFound}</CommandEmpty>
{Object.entries(grouped).map(([type, items]) => (
<CommandGroup
key={type}
heading={type === "page" ? t.pages : t.users}
>
{items.map((item) => (
<CommandItem
key={item.title}
value={item.title}
onSelect={(currentValue) => {
setMentions((prev) => [...prev, currentValue])
setMentionPopoverOpen(false)
}}
>
<MentionableIcon item={item} />
{item.title}
</CommandItem>
))}
</CommandGroup>
))}
</CommandList>
</Command>
</PopoverContent>
</Popover>
<div className="no-scrollbar -m-1.5 flex gap-1 overflow-y-auto p-1.5">
{mentions.map((mention) => {
const item = SAMPLE_DATA.mentionable.find(
(item) => item.title === mention
)
if (!item) {
return null
}
return (
<InputGroupButton
key={mention}
size="sm"
variant="secondary"
className="rounded-full !pr-2"
onClick={() => {
setMentions((prev) => prev.filter((m) => m !== mention))
}}
>
<MentionableIcon item={item} />
{item.title}
<IconX />
</InputGroupButton>
)
})}
</div>
</InputGroupAddon>
<InputGroupAddon align="block-end" className="gap-1">
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton
size="icon-sm"
className="rounded-full"
aria-label={t.attachFile}
/>
}
>
<IconPaperclip />
</TooltipTrigger>
<TooltipContent>{t.attachFile}</TooltipContent>
</Tooltip>
<DropdownMenu
open={modelPopoverOpen}
onOpenChange={setModelPopoverOpen}
>
<Tooltip>
<TooltipTrigger
render={
<InputGroupButton size="sm" className="rounded-full" />
}
>
{selectedModel.name}
</TooltipTrigger>
<TooltipContent>{t.selectModel}</TooltipContent>
</Tooltip>
<DropdownMenuContent
side="top"
align="start"
className="w-48"
dir={t.dir}
>
<DropdownMenuGroup className="w-48">
<DropdownMenuLabel className="text-muted-foreground text-xs">
{t.selectAgentMode}
</DropdownMenuLabel>
{SAMPLE_DATA.models.map((model) => (
<DropdownMenuCheckboxItem
key={model.name}
checked={model.name === selectedModel.name}
onCheckedChange={(checked) => {
if (checked) {
setSelectedModel(model)
}
}}
className="pr-2 *:[span:first-child]:right-auto *:[span:first-child]:left-2"
>
{model.name}
{model.badge && (
<Badge
variant="secondary"
className="h-5 rounded-sm bg-blue-100 px-1 text-xs text-blue-800 dark:bg-blue-900 dark:text-blue-100"
>
{model.badge}
</Badge>
)}
</DropdownMenuCheckboxItem>
))}
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<DropdownMenu
open={scopeMenuOpen}
onOpenChange={setScopeMenuOpen}
>
<DropdownMenuTrigger
render={
<InputGroupButton size="sm" className="rounded-full" />
}
>
<IconWorld /> {t.allSources}
</DropdownMenuTrigger>
<DropdownMenuContent
side="top"
align="end"
className="w-72"
dir={t.dir}
>
<DropdownMenuGroup>
<DropdownMenuItem
render={
<label htmlFor="rtl-web-search">
<IconWorld /> {t.webSearch}{" "}
<Switch
id="rtl-web-search"
className="ms-auto"
defaultChecked
size="sm"
/>
</label>
}
onSelect={(e) => e.preventDefault()}
></DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
render={
<label htmlFor="rtl-apps">
<IconApps /> {t.appsIntegrations}
<Switch
id="rtl-apps"
className="ms-auto"
defaultChecked
size="sm"
/>
</label>
}
onSelect={(e) => e.preventDefault()}
></DropdownMenuItem>
<DropdownMenuItem>
<IconCircleDashedPlus /> {t.allSourcesAccess}
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<Avatar className="size-4">
<AvatarImage src="https://github.com/shadcn.png" />
<AvatarFallback>CN</AvatarFallback>
</Avatar>
shadcn
</DropdownMenuSubTrigger>
<DropdownMenuSubContent
className="w-72 rounded-lg p-0"
dir={t.dir}
side="left"
>
<Command>
<CommandInput
placeholder={t.findKnowledge}
autoFocus
/>
<CommandList>
<CommandEmpty>{t.noKnowledgeFound}</CommandEmpty>
<CommandGroup>
{SAMPLE_DATA.mentionable
.filter((item) => item.type === "user")
.map((user) => (
<CommandItem
key={user.title}
value={user.title}
onSelect={() => {
console.log("Selected user:", user.title)
}}
>
<Avatar className="size-4">
<AvatarImage src={user.image} />
<AvatarFallback>
{user.title[0]}
</AvatarFallback>
</Avatar>
{user.title}{" "}
<span className="text-muted-foreground">
-{" "}
{
(user as { workspace?: string })
.workspace
}
</span>
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</DropdownMenuSubContent>
</DropdownMenuSub>
<DropdownMenuItem>
<IconBook /> {t.helpCenter}
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<IconPlus /> {t.connectApps}
</DropdownMenuItem>
<DropdownMenuLabel className="text-muted-foreground text-xs">
{t.searchSourcesNote}
</DropdownMenuLabel>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<InputGroupButton
aria-label={t.send}
className="ms-auto rounded-full"
variant="default"
size="icon-sm"
>
<IconArrowUp />
</InputGroupButton>
</InputGroupAddon>
</InputGroup>
</Field>
</form>
</div>
)
}

View File

@@ -0,0 +1,44 @@
"use client"
import { Badge } from "@/examples/base/ui-rtl/badge"
import { Spinner } from "@/examples/base/ui-rtl/spinner"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
syncing: "جارٍ المزامنة",
updating: "جارٍ التحديث",
loading: "جارٍ التحميل",
},
he: {
dir: "rtl" as const,
syncing: "מסנכרן",
updating: "מעדכן",
loading: "טוען",
},
}
export function SpinnerBadge() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<div dir={t.dir} className="flex items-center gap-2">
<Badge>
<Spinner />
{t.syncing}
</Badge>
<Badge variant="secondary">
<Spinner />
{t.updating}
</Badge>
<Badge variant="outline">
<Spinner />
{t.loading}
</Badge>
</div>
)
}

View File

@@ -0,0 +1,52 @@
"use client"
import { Button } from "@/examples/base/ui-rtl/button"
import {
Empty,
EmptyContent,
EmptyDescription,
EmptyHeader,
EmptyMedia,
EmptyTitle,
} from "@/examples/base/ui-rtl/empty"
import { Spinner } from "@/examples/base/ui-rtl/spinner"
import { useLanguageContext } from "@/components/language-selector"
const translations = {
ar: {
dir: "rtl" as const,
title: "جارٍ معالجة طلبك",
description: "يرجى الانتظار بينما نعالج طلبك. لا تقم بتحديث الصفحة.",
cancel: "إلغاء",
},
he: {
dir: "rtl" as const,
title: "מעבד את הבקשה שלך",
description: "אנא המתן בזמן שאנו מעבדים את בקשתך. אל תרענן את הדף.",
cancel: "ביטול",
},
}
export function SpinnerEmpty() {
const context = useLanguageContext()
const lang = context?.language === "he" ? "he" : "ar"
const t = translations[lang]
return (
<Empty className="w-full border md:p-6" dir={t.dir}>
<EmptyHeader>
<EmptyMedia variant="icon">
<Spinner />
</EmptyMedia>
<EmptyTitle>{t.title}</EmptyTitle>
<EmptyDescription>{t.description}</EmptyDescription>
</EmptyHeader>
<EmptyContent>
<Button variant="outline" size="sm">
{t.cancel}
</Button>
</EmptyContent>
</Empty>
)
}

View File

@@ -0,0 +1,14 @@
import { type Metadata } from "next"
import { RtlComponents } from "./components"
export const metadata: Metadata = {
title: "RTL",
description: "RTL example page with right-to-left language support.",
}
export function RtlPage() {
return <RtlComponents />
}
export default RtlPage

View File

@@ -1,6 +1,5 @@
"use client"
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
import { type Table } from "@tanstack/react-table"
import { Settings2 } from "lucide-react"
@@ -11,6 +10,7 @@ import {
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/registry/new-york-v4/ui/dropdown-menu"
export function DataTableViewOptions<TData>({

View File

@@ -59,7 +59,7 @@ export function FontPicker({
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-80 md:w-72"
className="max-h-96 md:w-72"
>
<PickerRadioGroup
value={currentFont?.value}

View File

@@ -80,7 +80,7 @@ function PickerLabel({
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"text-muted-foreground px-2 py-1.5 text-xs font-medium data-[inset]:pl-8",
"text-muted-foreground px-2 py-1.5 text-xs font-medium data-inset:pl-8",
className
)}
{...props}
@@ -103,7 +103,7 @@ function PickerItem({
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-[inset]:pl-8 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 data-inset:pl-8 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@@ -128,7 +128,7 @@ function PickerSubTrigger({
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-inset:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
@@ -180,7 +180,7 @@ function PickerCheckboxItem({
<MenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
@@ -220,7 +220,7 @@ function PickerRadioItem({
<MenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}

View File

@@ -1,7 +1,7 @@
"use client"
import * as React from "react"
import { type ImperativePanelHandle } from "react-resizable-panels"
import { type PanelImperativeHandle } from "react-resizable-panels"
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
import { Badge } from "@/registry/new-york-v4/ui/badge"
@@ -16,7 +16,7 @@ import {
export function Preview() {
const [params] = useDesignSystemSearchParams()
const iframeRef = React.useRef<HTMLIFrameElement>(null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
const resizablePanelRef = React.useRef<PanelImperativeHandle>(null)
// Sync resizable panel with URL param changes.
React.useEffect(() => {

View File

@@ -76,7 +76,7 @@ export function ThemePicker({
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-96"
className="max-h-[23rem]"
>
<PickerRadioGroup
value={currentTheme?.name}

View File

@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
import Link from "next/link"
import {
ComputerTerminal01Icon,
Copy01Icon,
@@ -23,6 +24,8 @@ import {
} from "@/registry/new-york-v4/ui/dialog"
import {
Field,
FieldContent,
FieldDescription,
FieldGroup,
FieldLabel,
FieldTitle,
@@ -31,6 +34,7 @@ import {
RadioGroup,
RadioGroupItem,
} from "@/registry/new-york-v4/ui/radio-group"
import { Switch } from "@/registry/new-york-v4/ui/switch"
import {
Tabs,
TabsContent,
@@ -71,14 +75,15 @@ export function ToolbarControls() {
const packageManager = config.packageManager || "pnpm"
const commands = React.useMemo(() => {
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
const url = `${origin}/init?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&template=${params.template}`
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"
const url = `${origin}/init?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&template=${params.template}&rtl=${params.rtl}`
const templateFlag = params.template ? ` --template ${params.template}` : ""
const rtlFlag = params.rtl ? " --rtl" : ""
return {
pnpm: `pnpm dlx shadcn@latest create --preset "${url}"${templateFlag}`,
npm: `npx shadcn@latest create --preset "${url}"${templateFlag}`,
yarn: `yarn dlx shadcn@latest create --preset "${url}"${templateFlag}`,
bun: `bunx --bun shadcn@latest create --preset "${url}"${templateFlag}`,
pnpm: `pnpm dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
npm: `npx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
yarn: `yarn dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
bun: `bunx --bun shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
}
}, [
params.base,
@@ -91,6 +96,7 @@ export function ToolbarControls() {
params.menuColor,
params.radius,
params.template,
params.rtl,
])
const command = commands[packageManager]
@@ -164,7 +170,7 @@ export function ToolbarControls() {
{selectedTemplate?.title} + shadcn/ui project.
</DialogDescription>
</DialogHeader>
<FieldGroup>
<FieldGroup className="gap-3">
<Field>
<FieldLabel htmlFor="template" className="sr-only">
Template
@@ -183,7 +189,7 @@ export function ToolbarControls() {
<FieldLabel
key={template.value}
htmlFor={template.value}
className="rounded-lg!"
className="has-data-[state=checked]:border-primary/10 rounded-lg!"
>
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
<RadioGroupItem
@@ -205,59 +211,81 @@ export function ToolbarControls() {
))}
</RadioGroup>
</Field>
</FieldGroup>
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig({
...config,
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
})
}}
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
>
<div className="flex items-center gap-2 p-2">
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
<TabsTrigger value="npm">npm</TabsTrigger>
<TabsTrigger value="yarn">yarn</TabsTrigger>
<TabsTrigger value="bun">bun</TabsTrigger>
</TabsList>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto size-7 rounded-lg"
onClick={handleCopyFromTabs}
>
{hasCopied ? (
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
) : (
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
)}
<span className="sr-only">Copy command</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied!" : "Copy command"}
</TooltipContent>
</Tooltip>
</div>
{Object.entries(commands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
</code>
<FieldLabel className="has-data-[state=checked]:border-primary/10 rounded-lg!">
<Field orientation="horizontal">
<FieldContent className="gap-1">
<FieldTitle>Enable RTL</FieldTitle>
<FieldDescription>
<a
href={`/docs/rtl/${params.template}`}
className="text-foreground underline"
target="_blank"
rel="noopener noreferrer"
>
View the RTL setup guide for {selectedTemplate?.title}.
</a>
</FieldDescription>
</FieldContent>
<Switch
checked={params.rtl}
onCheckedChange={(rtl) => setParams({ rtl })}
className="shadow-none"
/>
</Field>
</FieldLabel>
<Tabs
value={packageManager}
onValueChange={(value) => {
setConfig({
...config,
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
})
}}
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
>
<div className="flex items-center gap-2 p-2">
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono group-data-[orientation=horizontal]/tabs:h-8 *:data-[slot=tabs-trigger]:h-7 *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
<TabsTrigger value="npm">npm</TabsTrigger>
<TabsTrigger value="yarn">yarn</TabsTrigger>
<TabsTrigger value="bun">bun</TabsTrigger>
</TabsList>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="icon-sm"
variant="ghost"
className="ml-auto size-7 rounded-lg"
onClick={handleCopyFromTabs}
>
{hasCopied ? (
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
) : (
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
)}
<span className="sr-only">Copy command</span>
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied!" : "Copy command"}
</TooltipContent>
</Tooltip>
</div>
{Object.entries(commands).map(([key, cmd]) => {
return (
<TabsContent key={key} value={key}>
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3">
<div className="no-scrollbar overflow-x-auto">
<code className="font-mono text-sm whitespace-nowrap">
{cmd}
</code>
</div>
</div>
</div>
</TabsContent>
)
})}
</Tabs>
</TabsContent>
)
})}
</Tabs>
</FieldGroup>
<DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col">
<Button
size="sm"

View File

@@ -19,6 +19,7 @@ export async function GET(request: NextRequest) {
menuColor: searchParams.get("menuColor"),
radius: searchParams.get("radius"),
template: searchParams.get("template"),
rtl: searchParams.get("rtl") === "true",
})
if (!result.success) {

View File

@@ -38,15 +38,15 @@ const jetbrainsMono = JetBrains_Mono({
variable: "--font-jetbrains-mono",
})
// const geistSans = Geist({
// subsets: ["latin"],
// variable: "--font-geist-sans",
// })
const geistSans = Geist({
subsets: ["latin"],
variable: "--font-geist-sans",
})
// const geistMono = Geist_Mono({
// subsets: ["latin"],
// variable: "--font-geist-mono",
// })
const geistMono = Geist_Mono({
subsets: ["latin"],
variable: "--font-geist-mono",
})
const roboto = Roboto({
subsets: ["latin"],
@@ -74,12 +74,12 @@ const outfit = Outfit({
})
export const FONTS = [
// {
// name: "Geist Sans",
// value: "geist",
// font: geistSans,
// type: "sans",
// },
{
name: "Geist",
value: "geist",
font: geistSans,
type: "sans",
},
{
name: "Inter",
value: "inter",
@@ -134,18 +134,18 @@ export const FONTS = [
font: outfit,
type: "sans",
},
{
name: "Geist Mono",
value: "geist-mono",
font: geistMono,
type: "mono",
},
{
name: "JetBrains Mono",
value: "jetbrains-mono",
font: jetbrainsMono,
type: "mono",
},
// {
// name: "Geist Mono",
// value: "geist-mono",
// font: geistMono,
// type: "mono",
// },
] as const
export type Font = (typeof FONTS)[number]

View File

@@ -66,6 +66,7 @@ const designSystemSearchParams = {
"start",
"vite",
] as const).withDefault("next"),
rtl: parseAsBoolean.withDefault(false),
size: parseAsInteger.withDefault(100),
custom: parseAsBoolean.withDefault(false),
}

View File

@@ -8,24 +8,24 @@ export function ResizableDemo() {
return (
<div className="flex w-full flex-col gap-6">
<ResizablePanelGroup
direction="horizontal"
orientation="horizontal"
className="max-w-md rounded-lg border md:min-w-[450px]"
>
<ResizablePanel defaultSize={50}>
<ResizablePanel defaultSize="50%">
<div className="flex h-[200px] items-center justify-center p-6">
<span className="font-semibold">One</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={50}>
<ResizablePanelGroup direction="vertical">
<ResizablePanel defaultSize={25}>
<ResizablePanel defaultSize="50%">
<ResizablePanelGroup orientation="vertical">
<ResizablePanel defaultSize="25%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Two</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>
<ResizablePanel defaultSize="75%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Three</span>
</div>
@@ -34,32 +34,32 @@ export function ResizableDemo() {
</ResizablePanel>
</ResizablePanelGroup>
<ResizablePanelGroup
direction="horizontal"
orientation="horizontal"
className="min-h-[200px] max-w-md rounded-lg border md:min-w-[450px]"
>
<ResizablePanel defaultSize={25}>
<ResizablePanel defaultSize="25%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Sidebar</span>
</div>
</ResizablePanel>
<ResizableHandle withHandle />
<ResizablePanel defaultSize={75}>
<ResizablePanel defaultSize="75%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Content</span>
</div>
</ResizablePanel>
</ResizablePanelGroup>
<ResizablePanelGroup
direction="vertical"
orientation="vertical"
className="min-h-[200px] max-w-md rounded-lg border md:min-w-[450px]"
>
<ResizablePanel defaultSize={25}>
<ResizablePanel defaultSize="25%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Header</span>
</div>
</ResizablePanel>
<ResizableHandle />
<ResizablePanel defaultSize={75}>
<ResizablePanel defaultSize="75%">
<div className="flex h-full items-center justify-center p-6">
<span className="font-semibold">Content</span>
</div>

View File

@@ -9,7 +9,9 @@ import { ActiveThemeProvider } from "@/components/active-theme"
import { Analytics } from "@/components/analytics"
import { TailwindIndicator } from "@/components/tailwind-indicator"
import { ThemeProvider } from "@/components/theme-provider"
import { TooltipProvider as BaseTooltipProvider } from "@/registry/bases/base/ui/tooltip"
import { Toaster } from "@/registry/bases/radix/ui/sonner"
import { TooltipProvider as RadixTooltipProvider } from "@/registry/bases/radix/ui/tooltip"
import "@/styles/globals.css"
@@ -97,8 +99,12 @@ export default function RootLayout({
<LayoutProvider>
<ActiveThemeProvider>
<NuqsAdapter>
{children}
<Toaster position="top-center" />
<BaseTooltipProvider delay={0}>
<RadixTooltipProvider delayDuration={0}>
{children}
<Toaster position="top-center" />
</RadixTooltipProvider>
</BaseTooltipProvider>
</NuqsAdapter>
<TailwindIndicator />
<Analytics />

View File

@@ -20,10 +20,9 @@ function BaseUILogo() {
export function Announcement() {
return (
<Badge asChild variant="secondary" className="bg-transparent">
<Link href="/docs/changelog">
<BaseUILogo />
Base UI Documentation <ArrowRightIcon />
<Badge asChild variant="secondary" className="bg-muted">
<Link href="/docs/changelog/2026-01-rtl">
RTL Support <ArrowRightIcon />
</Link>
</Badge>
)

View File

@@ -16,7 +16,7 @@ import {
Tablet,
Terminal,
} from "lucide-react"
import { type ImperativePanelHandle } from "react-resizable-panels"
import { type PanelImperativeHandle } from "react-resizable-panels"
import {
type registryItemFileSchema,
type registryItemSchema,
@@ -68,7 +68,7 @@ type BlockViewerContext = {
setView: (view: "code" | "preview") => void
activeFile: string | null
setActiveFile: (file: string) => void
resizablePanelRef: React.RefObject<ImperativePanelHandle | null> | null
resizablePanelRef: React.RefObject<PanelImperativeHandle | null> | null
tree: ReturnType<typeof createFileTreeForRegistryItemFiles> | null
highlightedFiles:
| (z.infer<typeof registryItemFileSchema> & {
@@ -101,7 +101,7 @@ function BlockViewerProvider({
const [activeFile, setActiveFile] = React.useState<
BlockViewerContext["activeFile"]
>(highlightedFiles?.[0].target ?? null)
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
const resizablePanelRef = React.useRef<PanelImperativeHandle>(null)
const [iframeKey, setIframeKey] = React.useState(0)
return (
@@ -268,19 +268,19 @@ function BlockViewerView({ styleName }: { styleName: Style["name"] }) {
<div className="relative grid w-full gap-4">
<div className="absolute inset-0 right-4 [background-image:radial-gradient(#d4d4d4_1px,transparent_1px)] [background-size:20px_20px] dark:[background-image:radial-gradient(#404040_1px,transparent_1px)]"></div>
<ResizablePanelGroup
direction="horizontal"
orientation="horizontal"
className="after:bg-surface/50 relative z-10 after:absolute after:inset-0 after:right-3 after:z-0 after:rounded-xl"
>
<ResizablePanel
ref={resizablePanelRef}
panelRef={resizablePanelRef}
className="bg-background relative aspect-[4/2.5] overflow-hidden rounded-lg border md:aspect-auto md:rounded-xl"
defaultSize={100}
minSize={30}
defaultSize="100%"
minSize="30%"
>
<BlockViewerIframe styleName={styleName} />
</ResizablePanel>
<ResizableHandle className="after:bg-border relative hidden w-3 bg-transparent p-0 after:absolute after:top-1/2 after:right-0 after:h-8 after:w-[6px] after:translate-x-[-1px] after:-translate-y-1/2 after:rounded-full after:transition-all after:hover:h-10 md:block" />
<ResizablePanel defaultSize={0} minSize={0} />
<ResizablePanel defaultSize="0%" minSize="0%" />
</ResizablePanelGroup>
</div>
</div>

View File

@@ -20,7 +20,7 @@ export function Callout({
<Alert
data-variant={variant}
className={cn(
"bg-background text-foreground mt-6 w-auto border md:-mx-1",
"bg-surface text-surface-foreground border-surface mt-6 w-auto rounded-xl md:-mx-1 **:[code]:border",
className
)}
{...props}

View File

@@ -88,7 +88,7 @@ export function CodeBlockCommand({
<TabsTrigger
key={key}
value={key}
className="data-[state=active]:bg-accent data-[state=active]:border-input h-7 border border-transparent pt-0.5 data-[state=active]:shadow-none"
className="data-[state=active]:bg-background! data-[state=active]:border-input h-7 border border-transparent pt-0.5 shadow-none!"
>
{key}
</TabsTrigger>
@@ -113,23 +113,16 @@ export function CodeBlockCommand({
})}
</div>
</Tabs>
<Tooltip>
<TooltipTrigger asChild>
<Button
data-slot="copy-button"
size="icon"
variant="ghost"
className="absolute top-2 right-2 z-10 size-7 opacity-70 hover:opacity-100 focus-visible:opacity-100"
onClick={copyCommand}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</TooltipTrigger>
<TooltipContent>
{hasCopied ? "Copied" : "Copy to Clipboard"}
</TooltipContent>
</Tooltip>
<Button
data-slot="copy-button"
size="icon"
variant="ghost"
className="absolute top-2 right-2 z-10 size-7 opacity-70 hover:opacity-100 focus-visible:opacity-100"
onClick={copyCommand}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</div>
)
}

View File

@@ -39,7 +39,7 @@ export function CodeCollapsibleWrapper({
</CollapsibleTrigger>
<CollapsibleContent
forceMount
className="relative mt-6 overflow-hidden data-[state=closed]:max-h-64 [&>figure]:mt-0 [&>figure]:md:!mx-0"
className="relative mt-6 overflow-hidden data-[state=closed]:max-h-64 data-[state=closed]:[content-visibility:auto] [&>figure]:mt-0 [&>figure]:md:!mx-0"
>
{children}
</CollapsibleContent>

View File

@@ -2,11 +2,10 @@
import * as React from "react"
import { usePathname, useRouter } from "next/navigation"
import { type DialogProps } from "@radix-ui/react-dialog"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { IconArrowRight } from "@tabler/icons-react"
import { useDocsSearch } from "fumadocs-core/search/client"
import { CornerDownLeftIcon, SquareDashedIcon, XIcon } from "lucide-react"
import { CornerDownLeftIcon, SquareDashedIcon } from "lucide-react"
import { Dialog as DialogPrimitive } from "radix-ui"
import { type Color, type ColorPalette } from "@/lib/colors"
import { trackEvent } from "@/lib/events"
@@ -35,7 +34,6 @@ import {
DialogTitle,
DialogTrigger,
} from "@/registry/new-york-v4/ui/dialog"
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
import { Separator } from "@/registry/new-york-v4/ui/separator"
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
@@ -45,7 +43,7 @@ export function CommandMenu({
blocks,
navItems,
...props
}: DialogProps & {
}: React.ComponentProps<typeof Dialog> & {
tree: typeof source.pageTree
colors: ColorPalette[]
blocks?: { name: string; description: string; categories: string[] }[]
@@ -56,6 +54,7 @@ export function CommandMenu({
const [config] = useConfig()
const currentBase = getCurrentBase(pathname)
const [open, setOpen] = React.useState(false)
const [renderDelayedGroups, setRenderDelayedGroups] = React.useState(false)
const [selectedType, setSelectedType] = React.useState<
"color" | "page" | "component" | "block" | null
>(null)
@@ -95,14 +94,30 @@ export function CommandMenu({
// Set new timeout to debounce both search and tracking.
searchTimeoutRef.current = setTimeout(() => {
setSearch(value)
trackSearchQuery(value)
React.startTransition(() => {
setSearch(value)
trackSearchQuery(value)
})
}, 500)
},
[setSearch, trackSearchQuery]
)
// Cleanup timeout on unmount.
React.useEffect(() => {
if (open) {
const frame = requestAnimationFrame(() => {
setRenderDelayedGroups(true)
})
return () => {
cancelAnimationFrame(frame)
}
}
setRenderDelayedGroups(false)
}, [open])
React.useEffect(() => {
return () => {
if (searchTimeoutRef.current) {
@@ -111,6 +126,17 @@ export function CommandMenu({
}
}, [])
const commandFilter = React.useCallback(
(value: string, searchValue: string, keywords?: string[]) => {
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(searchValue.toLowerCase())) {
return 1
}
return 0
},
[]
)
const handlePageHighlight = React.useCallback(
(isComponent: boolean, item: { url: string; name?: React.ReactNode }) => {
if (isComponent) {
@@ -151,6 +177,168 @@ export function CommandMenu({
[setOpen]
)
const navItemsSection = React.useMemo(() => {
if (!navItems || navItems.length === 0) {
return null
}
return (
<CommandGroup
heading="Pages"
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{navItems.map((item) => (
<CommandMenuItem
key={item.href}
value={`Navigation ${item.label}`}
keywords={["nav", "navigation", item.label.toLowerCase()]}
onHighlight={() => {
setSelectedType("page")
setCopyPayload("")
}}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
>
<IconArrowRight />
{item.label}
</CommandMenuItem>
))}
</CommandGroup>
)
}, [navItems, runCommand, router])
const pageGroupsSection = React.useMemo(() => {
return tree.children.map((group) => {
if (group.type !== "folder") {
return null
}
const pages = getPagesFromFolder(group, currentBase).filter((item) => {
if (!showMcpDocs && item.url.includes("/mcp")) {
return false
}
return true
})
if (pages.length === 0) {
return null
}
return (
<CommandGroup
key={group.$id}
heading={group.name}
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{pages.map((item) => {
const isComponent = item.url.includes("/components/")
return (
<CommandMenuItem
key={item.url}
value={
item.name?.toString() ? `${group.name} ${item.name}` : ""
}
keywords={isComponent ? ["component"] : undefined}
onHighlight={() => handlePageHighlight(isComponent, item)}
onSelect={() => {
runCommand(() => router.push(item.url))
}}
>
{isComponent ? (
<div className="border-muted-foreground aspect-square size-4 rounded-full border border-dashed" />
) : (
<IconArrowRight />
)}
{item.name}
</CommandMenuItem>
)
})}
</CommandGroup>
)
})
}, [tree.children, currentBase, handlePageHighlight, runCommand, router])
const colorGroupsSection = React.useMemo(() => {
return colors.map((colorPalette) => (
<CommandGroup
key={colorPalette.name}
heading={
colorPalette.name.charAt(0).toUpperCase() + colorPalette.name.slice(1)
}
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{colorPalette.colors.map((color) => (
<CommandMenuItem
key={color.hex}
value={color.className}
keywords={["color", color.name, color.className]}
onHighlight={() => handleColorHighlight(color)}
onSelect={() => {
runCommand(() =>
copyToClipboardWithMeta(color.oklch, {
name: "copy_color",
properties: { color: color.oklch },
})
)
}}
>
<div
className="border-ghost aspect-square size-4 rounded-sm bg-(--color) after:rounded-sm"
style={{ "--color": color.oklch } as React.CSSProperties}
/>
{color.className}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{color.oklch}
</span>
</CommandMenuItem>
))}
</CommandGroup>
))
}, [colors, handleColorHighlight, runCommand])
const blocksSection = React.useMemo(() => {
if (!blocks || blocks.length === 0) {
return null
}
return (
<CommandGroup
heading="Blocks"
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{blocks.map((block) => (
<CommandMenuItem
key={block.name}
value={block.name}
onHighlight={() => {
handleBlockHighlight(block)
}}
keywords={[
"block",
block.name,
block.description,
...block.categories,
]}
onSelect={() => {
runCommand(() =>
router.push(`/blocks/${block.categories[0]}#${block.name}`)
)
}}
>
<SquareDashedIcon />
{block.description}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{block.name}
</span>
</CommandMenuItem>
))}
</CommandGroup>
)
}, [blocks, handleBlockHighlight, runCommand, router])
React.useEffect(() => {
const down = (e: KeyboardEvent) => {
if ((e.key === "k" && (e.metaKey || e.ctrlKey)) || e.key === "/") {
@@ -203,16 +391,13 @@ export function CommandMenu({
<Button
variant="outline"
className={cn(
"text-foreground dark:bg-card hover:bg-muted/50 relative h-8 w-full justify-start pl-3 font-normal shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
"text-foreground dark:bg-card hover:bg-muted/50 relative h-8 w-full justify-start rounded-lg pl-3 font-normal shadow-none sm:pr-12 md:w-48 lg:w-56 xl:w-64"
)}
onClick={() => setOpen(true)}
{...props}
>
<span className="hidden lg:inline-flex">Search documentation...</span>
<span className="inline-flex lg:hidden">Search...</span>
<div className="absolute top-1.5 right-1.5 hidden gap-1 group-has-[[data-slot=designer]]/body:hidden sm:flex">
<Kbd>K</Kbd>
</div>
</Button>
</DialogTrigger>
<DialogContent className="rounded-xl border-none bg-clip-padding p-2 pb-11 shadow-2xl ring-4 ring-neutral-200/80 dark:bg-neutral-900 dark:ring-neutral-800">
@@ -222,17 +407,13 @@ export function CommandMenu({
</DialogHeader>
<Command
className="**:data-[slot=command-input-wrapper]:bg-input/50 **:data-[slot=command-input-wrapper]:border-input rounded-none bg-transparent **:data-[slot=command-input]:!h-9 **:data-[slot=command-input]:py-0 **:data-[slot=command-input-wrapper]:mb-0 **:data-[slot=command-input-wrapper]:!h-9 **:data-[slot=command-input-wrapper]:rounded-md **:data-[slot=command-input-wrapper]:border"
filter={(value, search, keywords) => {
handleSearchChange(search)
const extendValue = value + " " + (keywords?.join(" ") || "")
if (extendValue.toLowerCase().includes(search.toLowerCase())) {
return 1
}
return 0
}}
filter={commandFilter}
>
<div className="relative">
<CommandInput placeholder="Search documentation..." />
<CommandInput
placeholder="Search documentation..."
onValueChange={handleSearchChange}
/>
{query.isLoading && (
<div className="pointer-events-none absolute top-1/2 right-3 z-10 flex -translate-y-1/2 items-center justify-center">
<Spinner className="text-muted-foreground size-4" />
@@ -243,148 +424,19 @@ export function CommandMenu({
<CommandEmpty className="text-muted-foreground py-12 text-center text-sm">
{query.isLoading ? "Searching..." : "No results found."}
</CommandEmpty>
{navItems && navItems.length > 0 && (
<CommandGroup
heading="Pages"
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{navItems.map((item) => (
<CommandMenuItem
key={item.href}
value={`Navigation ${item.label}`}
keywords={["nav", "navigation", item.label.toLowerCase()]}
onHighlight={() => {
setSelectedType("page")
setCopyPayload("")
}}
onSelect={() => {
runCommand(() => router.push(item.href))
}}
>
<IconArrowRight />
{item.label}
</CommandMenuItem>
))}
</CommandGroup>
)}
{tree.children.map((group) => (
<CommandGroup
key={group.$id}
heading={group.name}
className="!p-0 [&_[cmdk-group-heading]]:scroll-mt-16 [&_[cmdk-group-heading]]:!p-3 [&_[cmdk-group-heading]]:!pb-1"
>
{group.type === "folder" &&
getPagesFromFolder(group, currentBase).map((item) => {
const isComponent = item.url.includes("/components/")
if (!showMcpDocs && item.url.includes("/mcp")) {
return null
}
return (
<CommandMenuItem
key={item.url}
value={
item.name?.toString()
? `${group.name} ${item.name}`
: ""
}
keywords={isComponent ? ["component"] : undefined}
onHighlight={() =>
handlePageHighlight(isComponent, item)
}
onSelect={() => {
runCommand(() => router.push(item.url))
}}
>
{isComponent ? (
<div className="border-muted-foreground aspect-square size-4 rounded-full border border-dashed" />
) : (
<IconArrowRight />
)}
{item.name}
</CommandMenuItem>
)
})}
</CommandGroup>
))}
{colors.map((colorPalette) => (
<CommandGroup
key={colorPalette.name}
heading={
colorPalette.name.charAt(0).toUpperCase() +
colorPalette.name.slice(1)
}
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{colorPalette.colors.map((color) => (
<CommandMenuItem
key={color.hex}
value={color.className}
keywords={["color", color.name, color.className]}
onHighlight={() => handleColorHighlight(color)}
onSelect={() => {
runCommand(() =>
copyToClipboardWithMeta(color.oklch, {
name: "copy_color",
properties: { color: color.oklch },
})
)
}}
>
<div
className="border-ghost aspect-square size-4 rounded-sm bg-(--color) after:rounded-sm"
style={{ "--color": color.oklch } as React.CSSProperties}
/>
{color.className}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{color.oklch}
</span>
</CommandMenuItem>
))}
</CommandGroup>
))}
{blocks?.length ? (
<CommandGroup
heading="Blocks"
className="!p-0 [&_[cmdk-group-heading]]:!p-3"
>
{blocks.map((block) => (
<CommandMenuItem
key={block.name}
value={block.name}
onHighlight={() => {
handleBlockHighlight(block)
}}
keywords={[
"block",
block.name,
block.description,
...block.categories,
]}
onSelect={() => {
runCommand(() =>
router.push(
`/blocks/${block.categories[0]}#${block.name}`
)
)
}}
>
<SquareDashedIcon />
{block.description}
<span className="text-muted-foreground ml-auto font-mono text-xs font-normal tabular-nums">
{block.name}
</span>
</CommandMenuItem>
))}
</CommandGroup>
{navItemsSection}
{renderDelayedGroups ? (
<>
{pageGroupsSection}
{colorGroupsSection}
{blocksSection}
<SearchResults
setOpen={setOpen}
query={query}
search={search}
/>
</>
) : null}
<SearchResults
open={open}
setOpen={setOpen}
query={query}
search={search}
/>
</CommandList>
</Command>
<div className="text-muted-foreground absolute inset-x-0 bottom-0 z-20 flex h-10 items-center gap-2 rounded-b-xl border-t border-t-neutral-100 bg-neutral-50 px-4 text-xs font-medium dark:border-t-neutral-700 dark:bg-neutral-800">
@@ -470,23 +522,24 @@ function SearchResults({
query,
search,
}: {
open: boolean
setOpen: (open: boolean) => void
query: Query
search: string
}) {
const router = useRouter()
const uniqueResults =
query.data && Array.isArray(query.data)
? query.data.filter(
(item, index, self) =>
!(
item.type === "text" &&
item.content.trim().split(/\s+/).length <= 1
) && index === self.findIndex((t) => t.content === item.content)
)
: []
const uniqueResults = React.useMemo(() => {
if (!query.data || !Array.isArray(query.data)) {
return []
}
return query.data.filter(
(item, index, self) =>
!(
item.type === "text" && item.content.trim().split(/\s+/).length <= 1
) && index === self.findIndex((t) => t.content === item.content)
)
}, [query.data])
if (!search.trim()) {
return null
@@ -535,11 +588,11 @@ function DialogContent({
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
{/* <DialogOverlay /> */}
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
"bg-background fixed top-1/3 left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}

View File

@@ -1,9 +1,26 @@
"use client"
import * as React from "react"
import { Button } from "@/examples/base/ui/button"
import Link from "next/link"
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/examples/base/ui/popover"
import { IconAlertCircle } from "@tabler/icons-react"
import { cn } from "@/lib/utils"
import {
LanguageProvider,
LanguageSelector,
useLanguageContext,
useTranslation,
type Translations,
} from "@/components/language-selector"
import { DirectionProvider as BaseDirectionProvider } from "@/registry/bases/base/ui/direction"
import { DirectionProvider as RadixDirectionProvider } from "@/registry/bases/radix/ui/direction"
import { Button } from "@/registry/new-york-v4/ui/button"
import { Separator } from "@/registry/new-york-v4/ui/separator"
export function ComponentPreviewTabs({
className,
@@ -14,6 +31,8 @@ export function ComponentPreviewTabs({
component,
source,
sourcePreview,
direction = "ltr",
styleName,
...props
}: React.ComponentProps<"div"> & {
previewClassName?: string
@@ -23,65 +42,226 @@ export function ComponentPreviewTabs({
component: React.ReactNode
source: React.ReactNode
sourcePreview?: React.ReactNode
direction?: "ltr" | "rtl"
styleName?: string
}) {
const [isMobileCodeVisible, setIsMobileCodeVisible] = React.useState(false)
const base = styleName?.split("-")[0]
return (
<div
data-slot="component-preview"
className={cn(
"group relative mt-4 mb-12 flex flex-col gap-2 overflow-hidden rounded-xl border",
"group relative mt-4 mb-12 flex flex-col overflow-hidden rounded-xl border",
className
)}
{...props}
>
<div data-slot="preview">
<div
data-align={align}
data-chromeless={chromeLessOnMobile}
className={cn(
"preview flex h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start data-[chromeless=true]:h-auto data-[chromeless=true]:p-0",
previewClassName
)}
>
{component}
</div>
{!hideCode && (
<div
data-slot="code"
data-mobile-code-visible={isMobileCodeVisible}
className="relative overflow-hidden [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-72"
>
{isMobileCodeVisible ? (
source
) : (
<div className="relative">
{sourcePreview}
<div className="absolute inset-0 flex items-center justify-center pb-4">
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(to top, var(--color-code), color-mix(in oklab, var(--color-code) 60%, transparent), transparent)",
}}
/>
{direction === "rtl" ? (
<LanguageProvider defaultLanguage="ar">
<div className="flex h-16 items-center border-b px-4">
<RtlLanguageSelector />
<Popover>
<PopoverTrigger
render={
<Button
type="button"
size="sm"
variant="outline"
className="bg-background text-foreground dark:bg-background dark:text-foreground hover:bg-muted dark:hover:bg-muted relative z-10"
onClick={() => {
setIsMobileCodeVisible(true)
}}
variant="ghost"
size="icon-sm"
className="ml-auto size-7"
>
View Code
<IconAlertCircle />
<span className="sr-only">Toggle</span>
</Button>
}
></PopoverTrigger>
<PopoverContent
side="bottom"
align="end"
className="w-56 text-xs"
>
<div>
I used AI to translate the text for demonstration purposes.
It&apos;s not perfect and may contain errors.
</div>
</div>
)}
<Separator className="-mx-2.5 w-auto!" />
<div data-lang="ar">
لقد استخدمت الذكاء الاصطناعي لترجمة النص للأغراض التجريبية
فقط. قد لا تكون الترجمة دقيقة وقد تحتوي على أخطاء.
</div>
<Separator className="-mx-2.5 w-auto!" />
<div data-lang="he">
השתמשתי בבינה מלאכותית כדי לתרגם את הטקסט למטרות הדגמה. זה לא
מושלם ויכול להכיל שגיאות.
</div>
</PopoverContent>
</Popover>
</div>
<PreviewWrapper
align={align}
chromeLessOnMobile={chromeLessOnMobile}
previewClassName={previewClassName}
>
<DirectionProviderWrapper base={base}>
{component}
</DirectionProviderWrapper>
</PreviewWrapper>
</LanguageProvider>
) : (
<DirectionProviderWrapper base={base} dir="ltr">
<PreviewWrapper
align={align}
chromeLessOnMobile={chromeLessOnMobile}
previewClassName={previewClassName}
dir="ltr"
>
{component}
</PreviewWrapper>
</DirectionProviderWrapper>
)}
{!hideCode && (
<div
data-slot="code"
data-mobile-code-visible={isMobileCodeVisible}
className="relative overflow-hidden **:data-[slot=copy-button]:right-4 **:data-[slot=copy-button]:hidden data-[mobile-code-visible=true]:**:data-[slot=copy-button]:flex [&_[data-rehype-pretty-code-figure]]:!m-0 [&_[data-rehype-pretty-code-figure]]:rounded-t-none [&_[data-rehype-pretty-code-figure]]:border-t [&_pre]:max-h-72"
>
{isMobileCodeVisible ? (
<>
{direction === "rtl" && (
<div className="bg-code text-muted-foreground no-scrollbar relative z-10 overflow-x-auto border-t p-6 font-mono text-sm">
<pre>{`// You will notice this example uses dir and data-lang attributes.
// This is because this site is not RTL by default.
// In your application, you won't need these.`}</pre>
<span>
{"// See the "}
<Link
href="/docs/rtl"
className="underline underline-offset-4"
>
RTL guide
</Link>
{" for more information."}
</span>
</div>
)}
{source}
</>
) : (
<div className="relative">
{sourcePreview}
<div className="absolute inset-0 flex items-center justify-center pb-4">
<div
className="absolute inset-0"
style={{
background:
"linear-gradient(to top, var(--color-code), color-mix(in oklab, var(--color-code) 60%, transparent), transparent)",
}}
/>
<Button
type="button"
size="sm"
variant="outline"
className="bg-background text-foreground dark:bg-background dark:text-foreground hover:bg-muted dark:hover:bg-muted relative z-10 rounded-lg shadow-none"
onClick={() => {
setIsMobileCodeVisible(true)
}}
>
View Code
</Button>
</div>
</div>
)}
</div>
)}
</div>
)
}
const directionTranslations: Translations<Record<string, never>> = {
en: {
dir: "ltr",
values: {},
},
ar: {
dir: "rtl",
values: {},
},
he: {
dir: "rtl",
values: {},
},
}
function RtlLanguageSelector({ className }: { className?: string }) {
const context = useLanguageContext()
if (!context) {
return null
}
return (
<LanguageSelector
value={context.language}
onValueChange={context.setLanguage}
className={className}
/>
)
}
function PreviewWrapper({
align,
chromeLessOnMobile,
previewClassName,
dir: explicitDir,
children,
}: {
align: "center" | "start" | "end"
chromeLessOnMobile: boolean
previewClassName?: string
dir?: "ltr" | "rtl"
children: React.ReactNode
}) {
// useTranslation handles the case when there's no LanguageProvider context.
// It will fall back to local state with defaultLanguage.
const translation = useTranslation(directionTranslations, "ar")
const dir = explicitDir ?? translation.dir
return (
<div
data-slot="preview"
dir={dir}
data-lang={dir === "rtl" ? translation.language : undefined}
>
<div
data-align={align}
data-chromeless={chromeLessOnMobile}
className={cn(
"preview relative flex h-72 w-full justify-center p-10 data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start data-[chromeless=true]:h-auto data-[chromeless=true]:p-0",
previewClassName
)}
>
{children}
</div>
</div>
)
}
function DirectionProviderWrapper({
base,
dir: explicitDir,
children,
}: {
base?: string
dir?: "ltr" | "rtl"
children: React.ReactNode
}) {
// useTranslation handles the case when there's no LanguageProvider context.
// It will fall back to local state with defaultLanguage.
const translation = useTranslation(directionTranslations, "ar")
const dir = explicitDir ?? translation.dir
if (base === "base") {
return (
<BaseDirectionProvider direction={dir}>{children}</BaseDirectionProvider>
)
}
return <RadixDirectionProvider dir={dir}>{children}</RadixDirectionProvider>
}

View File

@@ -14,6 +14,8 @@ export function ComponentPreview({
hideCode = false,
chromeLessOnMobile = false,
styleName = "new-york-v4",
direction = "ltr",
caption,
...props
}: React.ComponentProps<"div"> & {
name: string
@@ -24,10 +26,12 @@ export function ComponentPreview({
type?: "block" | "component" | "example"
chromeLessOnMobile?: boolean
previewClassName?: string
direction?: "ltr" | "rtl"
caption?: string
}) {
if (type === "block") {
return (
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-xl border md:-mx-1">
const content = (
<div className="relative mt-6 aspect-[4/2.5] w-full overflow-hidden rounded-xl border md:-mx-1">
<Image
src={`/r/styles/new-york-v4/${name}-light.png`}
alt={name}
@@ -47,6 +51,19 @@ export function ComponentPreview({
</div>
</div>
)
if (caption) {
return (
<figure className="flex flex-col gap-4">
{content}
<figcaption className="text-muted-foreground text-center text-sm">
{caption}
</figcaption>
</figure>
)
}
return content
}
const Component = getRegistryComponent(name, styleName)
@@ -63,13 +80,13 @@ export function ComponentPreview({
)
}
return (
const content = (
<ComponentPreviewTabs
className={className}
previewClassName={previewClassName}
align={align}
hideCode={hideCode}
component={<DynamicComponent name={name} styleName={styleName} />}
component={React.createElement(Component)}
source={
<ComponentSource
name={name}
@@ -86,26 +103,25 @@ export function ComponentPreview({
/>
}
chromeLessOnMobile={chromeLessOnMobile}
direction={direction}
styleName={styleName}
{...props}
/>
)
}
function DynamicComponent({
name,
styleName,
}: {
name: string
styleName: string
}) {
const Component = React.useMemo(
() => getRegistryComponent(name, styleName),
[name, styleName]
)
if (!Component) {
return null
if (caption) {
return (
<figure
data-hide-code={hideCode}
className="flex flex-col data-[hide-code=true]:gap-4"
>
{content}
<figcaption className="text-muted-foreground -mt-8 text-center text-sm data-[hide-code=true]:mt-0">
{caption}
</figcaption>
</figure>
)
}
return React.createElement(Component)
return content
}

View File

@@ -13,7 +13,7 @@ export function ComponentsList({
const list = getPagesFromFolder(componentsFolder, currentBase)
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20">
<div className="grid grid-cols-2 gap-4 md:grid-cols-3 md:gap-x-8 lg:gap-x-16 lg:gap-y-6 xl:gap-x-20">
{list.map((component) => (
<Link
key={component.$id}

View File

@@ -6,11 +6,6 @@ import { IconCheck, IconCopy } from "@tabler/icons-react"
import { trackEvent, type Event } from "@/lib/events"
import { cn } from "@/lib/utils"
import { Button } from "@/registry/new-york-v4/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/registry/new-york-v4/ui/tooltip"
export function copyToClipboardWithMeta(value: string, event?: Event) {
navigator.clipboard.writeText(value)
@@ -24,7 +19,6 @@ export function CopyButton({
className,
variant = "ghost",
event,
tooltip = "Copy to Clipboard",
...props
}: React.ComponentProps<typeof Button> & {
value: string
@@ -35,44 +29,40 @@ export function CopyButton({
const [hasCopied, setHasCopied] = React.useState(false)
React.useEffect(() => {
setTimeout(() => {
setHasCopied(false)
}, 2000)
}, [])
if (hasCopied) {
const timer = setTimeout(() => setHasCopied(false), 2000)
return () => clearTimeout(timer)
}
}, [hasCopied])
return (
<Tooltip>
<TooltipTrigger asChild>
<Button
data-slot="copy-button"
data-copied={hasCopied}
size="icon"
variant={variant}
className={cn(
"bg-code absolute top-3 right-2 z-10 size-7 hover:opacity-100 focus-visible:opacity-100",
className
)}
onClick={() => {
copyToClipboardWithMeta(
value,
event
? {
name: event,
properties: {
code: value,
},
}
: undefined
)
setHasCopied(true)
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
</TooltipTrigger>
<TooltipContent>{hasCopied ? "Copied" : tooltip}</TooltipContent>
</Tooltip>
<Button
data-slot="copy-button"
data-copied={hasCopied}
size="icon"
variant={variant}
className={cn(
"bg-code absolute top-3 right-2 z-10 size-7 hover:opacity-100 focus-visible:opacity-100",
className
)}
onClick={() => {
copyToClipboardWithMeta(
value,
event
? {
name: event,
properties: {
code: value,
},
}
: undefined
)
setHasCopied(true)
}}
{...props}
>
<span className="sr-only">Copy</span>
{hasCopied ? <IconCheck /> : <IconCopy />}
</Button>
)
}

View File

@@ -32,6 +32,10 @@ const TOP_LEVEL_SECTIONS = [
name: "Directory",
href: "/docs/directory",
},
{
name: "RTL",
href: "/docs/rtl",
},
{
name: "MCP Server",
href: "/docs/mcp",
@@ -49,8 +53,8 @@ const TOP_LEVEL_SECTIONS = [
href: "/docs/changelog",
},
]
const EXCLUDED_SECTIONS = ["installation", "dark-mode", "changelog"]
const EXCLUDED_PAGES = ["/docs", "/docs/changelog"]
const EXCLUDED_SECTIONS = ["installation", "dark-mode", "changelog", "rtl"]
const EXCLUDED_PAGES = ["/docs", "/docs/changelog", "/docs/rtl"]
export function DocsSidebar({
tree,
@@ -61,13 +65,15 @@ export function DocsSidebar({
return (
<Sidebar
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-6rem)] overscroll-none bg-transparent lg:flex"
className="sticky top-[calc(var(--header-height)+0.6rem)] z-30 hidden h-[calc(100svh-10rem)] overscroll-none bg-transparent [--sidebar-menu-width:--spacing(56)] lg:flex"
collapsible="none"
{...props}
>
<SidebarContent className="no-scrollbar overflow-x-hidden px-2">
<div className="from-background via-background/80 to-background/50 sticky -top-1 z-10 h-8 shrink-0 bg-gradient-to-b blur-xs" />
<SidebarGroup>
<div className="h-9" />
<div className="from-background via-background/80 to-background/50 absolute top-8 z-10 h-8 w-(--sidebar-menu-width) shrink-0 bg-gradient-to-b blur-xs" />
<div className="via-border absolute top-12 right-2 bottom-0 hidden h-full w-px bg-gradient-to-b from-transparent to-transparent lg:flex" />
<SidebarContent className="no-scrollbar mx-auto w-(--sidebar-menu-width) overflow-x-hidden px-2">
<SidebarGroup className="pt-6">
<SidebarGroupLabel className="text-muted-foreground font-medium">
Sections
</SidebarGroupLabel>
@@ -89,8 +95,14 @@ export function DocsSidebar({
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={href}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
<span className="absolute inset-0 flex w-(--sidebar-menu-width) bg-transparent" />
{name}
{PAGES_NEW.includes(href) && (
<span
className="flex size-2 rounded-full bg-blue-500"
title="New"
/>
)}
</Link>
</SidebarMenuButton>
</SidebarMenuItem>
@@ -129,7 +141,7 @@ export function DocsSidebar({
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[30px] w-fit overflow-visible border border-transparent text-[0.8rem] font-medium after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
>
<Link href={page.url}>
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
<span className="absolute inset-0 flex w-(--sidebar-menu-width) bg-transparent" />
{page.name}
{PAGES_NEW.includes(page.url) && (
<span

View File

@@ -114,7 +114,7 @@ export function DocsTableOfContents({
<a
key={item.url}
href={item.url}
className="text-muted-foreground hover:text-foreground data-[active=true]:text-foreground text-[0.8rem] no-underline transition-colors data-[depth=3]:pl-4 data-[depth=4]:pl-6"
className="text-muted-foreground hover:text-foreground data-[active=true]:text-foreground text-[0.8rem] no-underline transition-colors data-[active=true]:font-medium data-[depth=3]:pl-4 data-[depth=4]:pl-6"
data-active={item.url === `#${activeHeading}`}
data-depth={item.depth}
>

View File

@@ -31,6 +31,12 @@ const examples = [
code: "https://github.com/shadcn/ui/tree/main/apps/v4/app/(app)/examples/authentication",
hidden: false,
},
{
name: "RTL",
href: "/examples/rtl",
code: "https://github.com/shadcn/ui/tree/main/apps/v4/app/(app)/examples/rtl",
hidden: false,
},
]
export function ExamplesNav({
@@ -76,10 +82,13 @@ function ExampleLink({
<Link
href={example.href}
key={example.href}
className="text-muted-foreground hover:text-primary data-[active=true]:text-primary flex h-7 items-center justify-center px-4 text-center text-base font-medium transition-colors"
className="text-muted-foreground hover:text-primary data-[active=true]:text-primary flex h-7 items-center justify-center gap-2 px-4 text-center text-base font-medium transition-colors"
data-active={isActive}
>
{example.name}
{example.name === "RTL" && (
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
)}
</Link>
)
}

View File

@@ -0,0 +1,125 @@
"use client"
import * as React from "react"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/examples/base/ui/select"
import { cn } from "@/lib/utils"
export type Language = "en" | "ar" | "he"
export type Direction = "ltr" | "rtl"
export type Translations<
T extends Record<string, string> = Record<string, string>,
> = Record<
Language,
{
dir: Direction
locale?: string
values: T
}
>
export const languageOptions = [
{ value: "en", label: "English" },
{ value: "ar", label: "Arabic (العربية)" },
{ value: "he", label: "Hebrew (עברית)" },
] as const
type LanguageContextType = {
language: Language
setLanguage: (language: Language) => void
}
const LanguageContext = React.createContext<LanguageContextType | undefined>(
undefined
)
export function LanguageProvider({
children,
defaultLanguage = "ar",
}: {
children: React.ReactNode
defaultLanguage?: Language
}) {
const [language, setLanguage] = React.useState<Language>(defaultLanguage)
return (
<LanguageContext.Provider value={{ language, setLanguage }}>
{children}
</LanguageContext.Provider>
)
}
export function useLanguageContext() {
const context = React.useContext(LanguageContext)
return context
}
export function useTranslation<T extends Record<string, string>>(
translations: Translations<T>,
defaultLanguage: Language = "ar"
) {
const context = useLanguageContext()
const [localLanguage, setLocalLanguage] =
React.useState<Language>(defaultLanguage)
const language = context?.language ?? localLanguage
const setLanguage = context?.setLanguage ?? setLocalLanguage
const { dir, locale, values: t } = translations[language]
return { language, setLanguage, dir, locale, t }
}
export interface LanguageSelectorProps {
value: Language
onValueChange: (value: Language) => void
}
export function LanguageSelector({
value,
onValueChange,
className,
languages = ["en", "ar", "he"],
}: LanguageSelectorProps & {
className?: string
languages?: Language[]
}) {
return (
<Select
items={languageOptions}
value={value}
onValueChange={(value) => onValueChange(value as Language)}
>
<SelectTrigger
size="sm"
className={cn("w-36", className)}
dir="ltr"
data-name="language-selector"
>
<SelectValue />
</SelectTrigger>
<SelectContent
dir="ltr"
className="data-closed:animate-none data-open:animate-none"
>
<SelectGroup>
{languageOptions
.filter((option) => languages.includes(option.value as Language))
.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
)
}

View File

@@ -30,6 +30,10 @@ const TOP_LEVEL_SECTIONS = [
name: "Directory",
href: "/docs/directory",
},
{
name: "RTL",
href: "/docs/rtl",
},
{
name: "MCP Server",
href: "/docs/mcp",
@@ -94,7 +98,7 @@ export function MobileNav({
</Button>
</PopoverTrigger>
<PopoverContent
className="bg-background/90 no-scrollbar h-(--radix-popper-available-height) w-(--radix-popper-available-width) overflow-y-auto rounded-none border-none p-0 shadow-none backdrop-blur duration-100"
className="bg-background/90 no-scrollbar h-(--radix-popper-available-height) w-(--radix-popper-available-width) overflow-y-auto rounded-none border-none p-0 shadow-none backdrop-blur duration-100 data-open:animate-none!"
align="start"
side="bottom"
alignOffset={-16}
@@ -128,6 +132,12 @@ export function MobileNav({
return (
<MobileLink key={name} href={href} onOpenChange={setOpen}>
{name}
{PAGES_NEW.includes(href) && (
<span
className="flex size-2 rounded-full bg-blue-500"
title="New"
/>
)}
</MobileLink>
)
})}
@@ -192,7 +202,7 @@ function MobileLink({
router.push(href.toString())
onOpenChange?.(false)
}}
className={cn("text-2xl font-medium", className)}
className={cn("flex items-center gap-2 text-2xl font-medium", className)}
{...props}
>
{children}

View File

@@ -8,7 +8,7 @@ function PageHeader({
return (
<section className={cn("border-grid", className)} {...props}>
<div className="container-wrapper">
<div className="container flex flex-col items-center gap-2 py-8 text-center md:py-16 lg:py-20 xl:gap-4">
<div className="container flex flex-col items-center gap-2 px-6 py-8 text-center md:py-16 lg:py-20 xl:gap-4">
{children}
</div>
</div>
@@ -23,7 +23,7 @@ function PageHeaderHeading({
return (
<h1
className={cn(
"text-primary leading-tighter max-w-2xl text-4xl font-semibold tracking-tight text-balance lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tighter",
"text-primary leading-tighter max-w-3xl text-3xl font-semibold tracking-tight text-balance lg:leading-[1.1] lg:font-semibold xl:text-5xl xl:tracking-tighter",
className
)}
{...props}
@@ -38,7 +38,7 @@ function PageHeaderDescription({
return (
<p
className={cn(
"text-foreground max-w-3xl text-base text-balance sm:text-lg",
"text-foreground max-w-4xl text-base text-balance sm:text-lg",
className
)}
{...props}

View File

@@ -1,16 +1,19 @@
"use client"
import { Label } from "@/examples/base/ui/label"
import {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectTrigger,
SelectValue,
} from "@/examples/base/ui/select"
import { THEMES } from "@/lib/themes"
import { cn } from "@/lib/utils"
import { useThemeConfig } from "@/components/active-theme"
import { Label } from "@/registry/new-york-v4/ui/label"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/registry/new-york-v4/ui/select"
import { CopyCodeButton } from "./theme-customizer"
@@ -19,33 +22,44 @@ export function ThemeSelector({ className }: React.ComponentProps<"div">) {
const value = activeTheme === "default" ? "neutral" : activeTheme
const items = THEMES.map((theme) => ({
label: theme.label,
value: theme.name,
}))
return (
<div className={cn("flex items-center gap-2", className)}>
<Label htmlFor="theme-selector" className="sr-only">
Theme
</Label>
<Select value={value} onValueChange={setActiveTheme}>
<SelectTrigger
id="theme-selector"
size="sm"
className="bg-secondary text-secondary-foreground border-secondary justify-start shadow-none *:data-[slot=select-value]:w-12"
>
<span className="font-medium">Theme:</span>
<Select
items={items}
value={value}
onValueChange={(value) => value && setActiveTheme(value)}
>
<SelectTrigger id="theme-selector" className="w-36">
<SelectValue placeholder="Select a theme" />
</SelectTrigger>
<SelectContent align="end">
{THEMES.map((theme) => (
<SelectItem
key={theme.name}
value={theme.name}
className="data-[state=checked]:opacity-50"
>
{theme.label}
</SelectItem>
))}
<SelectContent>
<SelectGroup>
<SelectLabel>Theme</SelectLabel>
{items.map((item) => (
<SelectItem
key={item.value}
value={item.value}
className="data-[state=checked]:opacity-50"
>
{item.label}
</SelectItem>
))}
</SelectGroup>
</SelectContent>
</Select>
<CopyCodeButton variant="secondary" size="icon-sm" />
<CopyCodeButton
variant="secondary"
size="icon-sm"
className="rounded-lg border bg-transparent"
/>
</div>
)
}

View File

@@ -223,3 +223,113 @@ To customize the output directory, use the `--output` option.
```bash
npx shadcn@latest build --output ./public/registry
```
---
## migrate
Use the `migrate` command to run migrations on your project.
```bash
npx shadcn@latest migrate [migration]
```
**Available Migrations**
| Migration | Description |
| --------- | ------------------------------------------------------- |
| `icons` | Migrate your UI components to a different icon library. |
| `radix` | Migrate to radix-ui. |
| `rtl` | Migrate your components to support RTL (right-to-left). |
**Options**
```bash
Usage: shadcn migrate [options] [migration] [path]
run a migration.
Arguments:
migration the migration to run.
path optional path or glob pattern to migrate.
Options:
-c, --cwd <cwd> the working directory. defaults to the current directory.
-l, --list list all migrations. (default: false)
-y, --yes skip confirmation prompt. (default: false)
-h, --help display help for command
```
---
### migrate rtl
The `rtl` migration transforms your components to support RTL (right-to-left) languages.
```bash
npx shadcn@latest migrate rtl
```
This will:
1. Update `components.json` to set `rtl: true`
2. Transform physical CSS properties to logical equivalents (e.g., `ml-4` → `ms-4`, `text-left` → `text-start`)
3. Add `rtl:` variants where needed (e.g., `space-x-4` → `space-x-4 rtl:space-x-reverse`)
**Migrate specific files**
You can migrate specific files or use glob patterns:
```bash
# Migrate a specific file
npx shadcn@latest migrate rtl src/components/ui/button.tsx
# Migrate files matching a glob pattern
npx shadcn@latest migrate rtl "src/components/ui/**"
```
If no path is provided, the migration will transform all files in your `ui` directory (from `components.json`).
---
### migrate radix
The `radix` migration updates your imports from individual `@radix-ui/react-*` packages to the unified `radix-ui` package.
```bash
npx shadcn@latest migrate radix
```
This will:
1. Transform imports from `@radix-ui/react-*` to `radix-ui`
2. Add the `radix-ui` package to your `package.json`
**Before**
```tsx
import * as DialogPrimitive from "@radix-ui/react-dialog"
import * as SelectPrimitive from "@radix-ui/react-select"
```
**After**
```tsx
import { Dialog as DialogPrimitive, Select as SelectPrimitive } from "radix-ui"
```
**Migrate specific files**
You can migrate specific files or use glob patterns:
```bash
# Migrate a specific file.
npx shadcn@latest migrate radix src/components/ui/dialog.tsx
# Migrate files matching a glob pattern.
npx shadcn@latest migrate radix "src/components/ui/**"
```
If no path is provided, the migration will transform all files in your `ui` directory (from `components.json`).
Once complete, you can remove any unused `@radix-ui/react-*` packages from your `package.json`.

View File

@@ -10,7 +10,8 @@ description: Every component recreated in Figma. With customizable props, typogr
## Free
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://obra.studio/) - Carefully crafted kit designed in the philosophy of shadcn, tracks v4, MIT licensed
- [Obra shadcn/ui](https://www.figma.com/community/file/1514746685758799870/obra-shadcn-ui) by [Obra Studio](https://obra.studio/) - Carefully crafted shadcn/ui kit, MIT licensed, maintained by team of designers, with free design to code plugin
- [shadcn/ui components](https://www.figma.com/community/file/1342715840824755935) by [Sitsiilia Bergmann](https://x.com/sitsiilia) - A well-structured component library aligned with the shadcn component system, regularly maintained.
- [shadcn/ui design system](https://www.figma.com/community/file/1203061493325953101) by [Pietro Schirano](https://twitter.com/skirano) - A design companion for shadcn/ui. Each component was painstakingly crafted to perfectly match the code implementation.
## Paid

View File

@@ -6,6 +6,7 @@
"components-json",
"theming",
"[Dark Mode](/docs/dark-mode)",
"[RTL](/docs/rtl)",
"[CLI](/docs/cli)",
"monorepo",
"typography",

View File

@@ -0,0 +1,82 @@
---
title: January 2026 - RTL Support
description: The shadcn CLI now supports RTL (right-to-left) layouts by automatically converting physical CSS classes to logical equivalents.
date: 2026-01-28
---
shadcn/ui now has first-class support for right-to-left (RTL) layouts. Your components automatically adapt for languages like Arabic, Hebrew, and Persian.
**This works with the [shadcn/ui components](/docs/components) as well as any component distributed on the shadcn registry.**
<ComponentPreview
styleName="base-nova"
name="alert-rtl"
direction="rtl"
previewClassName="h-auto sm:h-72 p-6"
/>
### Our approach to RTL
Traditionally, component libraries that support RTL ship with logical classes baked in. This means everyone has to work with classes like `ms-4` and `start-2`, even if they're only building for LTR layouts.
We took a different approach. The shadcn CLI transforms classes at install time, so you only see logical classes when you actually need them. If you're not building for RTL, you work with familiar classes like `ml-4` and `left-2`. When you enable RTL, the CLI handles the conversion for you.
**You don't have to learn RTL until you need it.**
### How it works
When you add components with `rtl: true` set in your `components.json`, the CLI automatically converts physical CSS classes like `ml-4` and `text-left` to their logical equivalents like `ms-4` and `text-start`.
- Physical positioning classes like `left-*` and `right-*` become `start-*` and `end-*`.
- Margin and padding classes like `ml-*` and `pr-*` become `ms-*` and `pe-*`.
- Text alignment classes like `text-left` become `text-start`.
- Directional props are updated to use logical values.
- Supported icons are automatically flipped using `rtl:rotate-180`.
- Animations like `slide-in-from-left` become `slide-in-from-start`.
### RTL examples for every component
We've added RTL examples for every component. You'll find live previews and code on each [component page](/docs/components).
<ComponentPreview
styleName="base-nova"
name="card-rtl"
direction="rtl"
previewClassName="h-auto"
/>
### CLI updates
**New projects**: Use the `--rtl` flag with `init` or `create` to enable RTL from the start.
```bash
npx shadcn@latest init --rtl
```
```bash
npx shadcn@latest create --rtl
```
**Existing projects**: Migrate your components with the `migrate rtl` command.
```bash
npx shadcn@latest migrate rtl
```
This transforms all components in your `ui` directory to use logical classes. You can also pass a specific path or glob pattern.
## Try it out
Click the link below to open a Next.js project with RTL support in v0.
[![Open in v0](https://v0.app/chat-static/button.svg)](https://v0.app/chat/api/open?url=https://github.com/shadcn-ui/next-template-rtl)
### Links
- [RTL Documentation](/docs/rtl)
- [Font Recommendations](/docs/rtl#font-recommendations)
- [Animations](/docs/rtl#animations)
- [Migrating Existing Components](/docs/rtl#migrating-existing-components)
- [Next.js Setup](/docs/rtl/next)
- [Vite Setup](/docs/rtl/vite)
- [TanStack Start Setup](/docs/rtl/start)

View File

@@ -0,0 +1,38 @@
---
title: February 2026 - Unified Radix UI Package
description: The new-york style now uses the unified radix-ui package.
date: 2026-02-02
---
The `new-york` style now uses the unified `radix-ui` package instead of individual `@radix-ui/react-*` packages.
### What's Changed
When you add components using the `new-york` style, they will now import from `radix-ui` instead of separate packages:
```diff title="components/ui/dialog.tsx"
- import * as DialogPrimitive from "@radix-ui/react-dialog"
+ import { Dialog as DialogPrimitive } from "radix-ui"
```
This results in a cleaner `package.json` with a single `radix-ui` dependency instead of multiple `@radix-ui/react-*` packages.
### Migrating Existing Projects
If you have an existing project using the `new-york` style, you can migrate to the new `radix-ui` package using the migrate command:
```bash
npx shadcn@latest migrate radix
```
This will update all imports in your UI components and add `radix-ui` to your dependencies.
To migrate components outside of your `ui` directory, use the `path` argument:
```bash
npx shadcn@latest migrate radix src/components/custom
```
Once complete, you can remove any unused `@radix-ui/react-*` packages from your `package.json`.
See the [migrate radix documentation](/docs/cli#migrate-radix) for more details.

View File

@@ -137,6 +137,17 @@ Wrap the `Accordion` in a `Card` component.
previewClassName="*:data-[slot=accordion]:max-w-sm h-[435px]"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="accordion-rtl"
align="start"
direction="rtl"
/>
## API Reference
See the [Base UI](https://base-ui.com/react/components/accordion#api-reference) documentation for more information.

View File

@@ -146,6 +146,17 @@ Use the `AlertDialogAction` component to add a destructive action button to the
previewClassName="h-56"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="alert-dialog-rtl"
direction="rtl"
previewClassName="h-56"
/>
## API Reference
### size

View File

@@ -109,6 +109,17 @@ You can customize the alert colors by adding custom classes such as `bg-amber-50
previewClassName="h-auto sm:h-72 p-6"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="alert-rtl"
direction="rtl"
previewClassName="h-auto sm:h-72 p-6"
/>
## API Reference
### Alert

View File

@@ -79,6 +79,17 @@ A portrait aspect ratio component using the `ratio={9 / 16}` prop. This is usefu
previewClassName="h-96"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="aspect-ratio-rtl"
direction="rtl"
previewClassName="h-96"
/>
## API Reference
### AspectRatio

View File

@@ -129,6 +129,17 @@ You can use the `Avatar` component as a trigger for a dropdown menu.
<ComponentPreview styleName="base-nova" name="avatar-dropdown" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="avatar-rtl"
direction="rtl"
previewClassName="h-72"
/>
## API Reference
### Avatar

View File

@@ -85,6 +85,12 @@ You can customize the colors of a badge by adding custom classes such as `bg-gre
<ComponentPreview styleName="base-nova" name="badge-colors" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="badge-rtl" direction="rtl" />
## API Reference
### Badge

View File

@@ -116,6 +116,17 @@ To use a custom link component from your routing library, you can use the `rende
<ComponentPreview styleName="base-nova" name="breadcrumb-link" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="breadcrumb-rtl"
direction="rtl"
previewClassName="p-2"
/>
## API Reference
### Breadcrumb

View File

@@ -148,6 +148,16 @@ Use with a `Popover` component.
<ComponentPreview styleName="base-nova" name="button-group-popover" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="button-group-rtl"
direction="rtl"
/>
## API Reference
### ButtonGroup

View File

@@ -143,6 +143,12 @@ Remember to set the `nativeButton` prop to `false` if you're returning an elemen
<ComponentPreview styleName="base-nova" name="button-render" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="button-rtl" direction="rtl" />
## API Reference
### Button

View File

@@ -237,270 +237,174 @@ Use `showWeekNumber` to show week numbers.
previewClassName="h-96"
/>
## Upgrade Guide
## RTL
### Tailwind v4
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
If you're already using Tailwind v4, you can upgrade to the latest version of the `Calendar` component by running the following command:
See also the [Hijri Guide](#persian--hijri--jalali-calendar) for enabling the Persian / Hijri / Jalali calendar.
```bash
npx shadcn@latest add calendar
<ComponentPreview
styleName="base-nova"
name="calendar-rtl"
direction="rtl"
previewClassName="h-96"
/>
When using RTL, import the locale from `react-day-picker/locale` and pass both the `locale` and `dir` props to the Calendar component:
```tsx showLineNumbers
import { arSA } from "react-day-picker/locale"
;<Calendar
mode="single"
selected={date}
onSelect={setDate}
locale={arSA}
dir="rtl"
/>
```
When you're prompted to overwrite the existing `Calendar` component, select `Yes`. **If you have made any changes to the `Calendar` component, you will need to merge your changes with the new version.**
This will update the `Calendar` component and `react-day-picker` to the latest version.
Next, follow the [React DayPicker](https://daypicker.dev/upgrading) upgrade guide to upgrade your existing components to the latest version.
#### Installing Blocks
After upgrading the `Calendar` component, you can install the new blocks by running the `shadcn@latest add` command.
```bash
npx shadcn@latest add calendar-02
```
This will install the latest version of the calendar blocks.
### Tailwind v3
If you're using Tailwind v3, you can upgrade to the latest version of the `Calendar` by copying the following code to your `calendar.tsx` file.
<CodeCollapsibleWrapper>
```tsx showLineNumbers title="components/ui/calendar.tsx"
"use client"
import * as React from "react"
import {
ChevronDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
} from "lucide-react"
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker"
import { cn } from "@/lib/utils"
import { Button, buttonVariants } from "@/components/ui/button"
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(
"bg-background group/calendar p-3 [--cell-size:2rem] [[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 }),
"h-[--cell-size] w-[--cell-size] select-none p-0 aria-disabled:opacity-50",
defaultClassNames.button_previous
),
button_next: cn(
buttonVariants({ variant: buttonVariant }),
"h-[--cell-size] w-[--cell-size] select-none p-0 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(
"has-focus:border-ring border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] relative rounded-md border",
defaultClassNames.dropdown_root
),
dropdown: cn("absolute inset-0 opacity-0", defaultClassNames.dropdown),
caption_label: cn(
"select-none font-medium",
captionLayout === "label"
? "text-sm"
: "[&>svg]:text-muted-foreground flex h-8 items-center gap-1 rounded-md pl-2 pr-1 text-sm [&>svg]:size-3.5",
defaultClassNames.caption_label
),
table: "w-full border-collapse",
weekdays: cn("flex", defaultClassNames.weekdays),
weekday: cn(
"text-muted-foreground flex-1 select-none rounded-md text-[0.8rem] font-normal",
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-muted-foreground select-none text-[0.8rem]",
defaultClassNames.week_number
),
day: cn(
"relative w-full h-full p-0 text-center [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
props.showWeekNumber
? "[&:nth-child(2)[data-selected=true]_button]:rounded-l-md"
: "[&:first-child[data-selected=true]_button]:rounded-l-md",
defaultClassNames.day
),
range_start: cn(
"bg-accent rounded-l-md",
defaultClassNames.range_start
),
range_middle: cn("rounded-none", defaultClassNames.range_middle),
range_end: cn("bg-accent rounded-r-md", defaultClassNames.range_end),
today: cn(
"bg-accent text-accent-foreground rounded-md 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
ref={ref}
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(
"data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 flex aspect-square h-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:ring-[3px] data-[range-end=true]:rounded-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md [&>span]:text-xs [&>span]:opacity-70",
defaultClassNames.day,
className
)}
{...props}
/>
)
}
export { Calendar, CalendarDayButton }
```
</CodeCollapsibleWrapper>
**If you have made any changes to the `Calendar` component, you will need to merge your changes with the new version.**
Then follow the [React DayPicker](https://daypicker.dev/upgrading) upgrade guide to upgrade your dependencies and existing components to the latest version.
#### Installing Blocks
After upgrading the `Calendar` component, you can install the new blocks by running the `shadcn@latest add` command.
```bash
npx shadcn@latest add calendar-02
```
This will install the latest version of the calendar blocks.
## API Reference
See the [React DayPicker](https://react-day-picker.js.org) documentation for more information on the `Calendar` component API.
See the [React DayPicker](https://react-day-picker.js.org) documentation for more information on the `Calendar` component.
## Changelog
### RTL Support
If you're upgrading from a previous version of the `Calendar` component, you'll need to apply the following updates to add locale support:
<Steps>
<Step>Import the `Locale` type.</Step>
Add `Locale` to your imports from `react-day-picker`:
```diff
import {
DayPicker,
getDefaultClassNames,
type DayButton,
+ type Locale,
} from "react-day-picker"
```
<Step>Add `locale` prop to the Calendar component.</Step>
Add the `locale` prop to the component's props:
```diff
function Calendar({
className,
classNames,
showOutsideDays = true,
captionLayout = "label",
buttonVariant = "ghost",
+ locale,
formatters,
components,
...props
}: React.ComponentProps<typeof DayPicker> & {
buttonVariant?: React.ComponentProps<typeof Button>["variant"]
}) {
```
<Step>Pass `locale` to DayPicker.</Step>
Pass the `locale` prop to the `DayPicker` component:
```diff
<DayPicker
showOutsideDays={showOutsideDays}
className={cn(...)}
captionLayout={captionLayout}
+ locale={locale}
formatters={{
formatMonthDropdown: (date) =>
- date.toLocaleString("default", { month: "short" }),
+ date.toLocaleString(locale?.code, { month: "short" }),
...formatters,
}}
```
<Step>Update CalendarDayButton to accept locale.</Step>
Update the `CalendarDayButton` component signature and pass `locale`:
```diff
function CalendarDayButton({
className,
day,
modifiers,
+ locale,
...props
- }: React.ComponentProps<typeof DayButton>) {
+ }: React.ComponentProps<typeof DayButton> & { locale?: Partial<Locale> }) {
```
<Step>Update date formatting in CalendarDayButton.</Step>
Use `locale?.code` in the date formatting:
```diff
<Button
variant="ghost"
size="icon"
- data-day={day.date.toLocaleDateString()}
+ data-day={day.date.toLocaleDateString(locale?.code)}
...
/>
```
<Step>Pass locale to DayButton component.</Step>
Update the `DayButton` component usage to pass the `locale` prop:
```diff
components={{
...
- DayButton: CalendarDayButton,
+ DayButton: ({ ...props }) => (
+ <CalendarDayButton locale={locale} {...props} />
+ ),
...
}}
```
<Step>Update RTL-aware CSS classes.</Step>
Replace directional classes with logical properties for better RTL support:
```diff
// In the day classNames:
- [&:last-child[data-selected=true]_button]:rounded-r-(--cell-radius)
+ [&:last-child[data-selected=true]_button]:rounded-e-(--cell-radius)
- [&:nth-child(2)[data-selected=true]_button]:rounded-l-(--cell-radius)
+ [&:nth-child(2)[data-selected=true]_button]:rounded-s-(--cell-radius)
- [&:first-child[data-selected=true]_button]:rounded-l-(--cell-radius)
+ [&:first-child[data-selected=true]_button]:rounded-s-(--cell-radius)
// In range_start classNames:
- rounded-l-(--cell-radius) ... after:right-0
+ rounded-s-(--cell-radius) ... after:end-0
// In range_end classNames:
- rounded-r-(--cell-radius) ... after:left-0
+ rounded-e-(--cell-radius) ... after:start-0
// In CalendarDayButton className:
- data-[range-end=true]:rounded-r-(--cell-radius)
+ data-[range-end=true]:rounded-e-(--cell-radius)
- data-[range-start=true]:rounded-l-(--cell-radius)
+ data-[range-start=true]:rounded-s-(--cell-radius)
```
</Steps>
After applying these changes, you can use the `locale` prop to provide locale-specific formatting:
```tsx
import { enUS } from "react-day-picker/locale"
;<Calendar mode="single" selected={date} onSelect={setDate} locale={enUS} />
```

View File

@@ -99,6 +99,17 @@ Add an image before the card header to create a card with an image.
previewClassName="h-[32rem]"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="card-rtl"
direction="rtl"
previewClassName="h-[30rem]"
/>
## API Reference
### Card

View File

@@ -285,6 +285,38 @@ export function Example() {
previewClassName="sm:h-[32rem]"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="carousel-rtl"
direction="rtl"
previewClassName="h-80 sm:h-[32rem]"
/>
When localizing the carousel for RTL languages, you need to set the `direction` option in the `opts` prop to match the text direction. This ensures the carousel scrolls in the correct direction.
```tsx showLineNumbers {2-5}
<Carousel
dir={dir}
opts={{
direction: dir,
}}
>
<CarouselContent>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
<CarouselItem>...</CarouselItem>
</CarouselContent>
<CarouselPrevious className="rtl:rotate-180" />
<CarouselNext className="rtl:rotate-180" />
</Carousel>
```
The `direction` option accepts `"ltr"` or `"rtl"` and should match the `dir` prop value. You may also want to rotate the navigation buttons using the `rtl:rotate-180` class to ensure they point in the correct direction.
## API Reference
See the [Embla Carousel docs](https://www.embla-carousel.com/api/plugins/) for more information on props and plugins.

View File

@@ -580,3 +580,14 @@ This prop adds keyboard access and screen reader support to your charts.
```tsx title="components/example-chart.tsx"
<LineChart accessibilityLayer />
```
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="chart-rtl"
direction="rtl"
previewClassName="h-92"
/>

View File

@@ -122,6 +122,17 @@ Use multiple fields to create a checkbox list.
previewClassName="p-4 md:p-8"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="checkbox-rtl"
direction="rtl"
previewClassName="h-80"
/>
## API Reference
See the [Base UI](https://base-ui.com/react/components/checkbox#api-reference) documentation for more information.

View File

@@ -118,6 +118,17 @@ Use nested collapsibles to build a file tree.
previewClassName="h-[36rem]"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="collapsible-rtl"
direction="rtl"
align="start"
/>
## API Reference
See the [Base UI](https://base-ui.com/react/components/collapsible#api-reference) documentation for more information.

View File

@@ -256,6 +256,17 @@ You can add an addon to the combobox by using the `InputGroupAddon` component in
<ComponentPreview styleName="base-nova" name="combobox-input-group" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="combobox-rtl"
direction="rtl"
align="start"
/>
## API Reference
See the [Base UI](https://base-ui.com/react/components/combobox#api-reference) documentation for more information.

View File

@@ -120,6 +120,18 @@ Scrollable command menu with multiple items.
<ComponentPreview styleName="base-nova" name="command-scrollable" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="command-rtl"
direction="rtl"
align="start"
previewClassName="h-[24.5rem]"
/>
## API Reference
See the [cmdk](https://github.com/dip/cmdk) documentation for more information.

View File

@@ -135,6 +135,30 @@ Control submenu placement with side and align props.
<ComponentPreview styleName="base-nova" name="context-menu-sides" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="context-menu-rtl"
direction="rtl"
/>
Use `side="inline-end"` to place the menu on the logical right side of the trigger.
```tsx showLineNumbers
<ContextMenu>
<ContextMenuTrigger>Right click here</ContextMenuTrigger>
<ContextMenuContent side="inline-end">
<ContextMenuItem>Profile</ContextMenuItem>
<ContextMenuItem>Billing</ContextMenuItem>
<ContextMenuItem>Team</ContextMenuItem>
<ContextMenuItem>Subscription</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
```
## API Reference
See the [Base UI](https://base-ui.com/react/components/context-menu#api-reference) documentation for more information.

View File

@@ -11,7 +11,8 @@ links:
styleName="radix-nova"
name="data-table-demo"
align="start"
previewClassName="items-start h-[28rem] px-4 md:px-8"
previewClassName="items-start h-auto px-4 md:px-8"
hideCode
/>
## Introduction
@@ -883,3 +884,15 @@ A component to toggle column visibility.
```tsx
<DataTableViewOptions table={table} />
```
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="data-table-rtl"
direction="rtl"
previewClassName="items-start h-auto px-4 md:px-8"
hideCode
/>

View File

@@ -95,3 +95,13 @@ A date picker component with a time input field for selecting a time.
This component uses the `chrono-node` library to parse natural language dates.
<ComponentPreview styleName="base-nova" name="date-picker-natural-language" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="date-picker-rtl"
direction="rtl"
/>

View File

@@ -111,6 +111,12 @@ Long content can scroll while the header stays in view.
<ComponentPreview styleName="base-nova" name="dialog-scrollable-content" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="dialog-rtl" direction="rtl" />
## API Reference
See the [Base UI](https://base-ui.com/react/components/dialog#api-reference) documentation for more information.

View File

@@ -0,0 +1,93 @@
---
title: Direction
description: A provider component that sets the text direction for your application.
base: base
component: true
links:
doc: https://base-ui.com/react/utils/direction-provider
api: https://base-ui.com/react/utils/direction-provider#api-reference
---
The `DirectionProvider` component is used to set the text direction (`ltr` or `rtl`) for your application. This is essential for supporting right-to-left languages like Arabic, Hebrew, and Persian.
Here's a preview of the component in RTL mode. Use the language selector to switch the language. To see more examples, look for the RTL section on components pages.
<ComponentPreview
styleName="base-nova"
name="card-rtl"
direction="rtl"
previewClassName="h-auto"
hideCode
/>
## Installation
<CodeTabs>
<TabsList>
<TabsTrigger value="cli">Command</TabsTrigger>
<TabsTrigger value="manual">Manual</TabsTrigger>
</TabsList>
<TabsContent value="cli">
```bash
npx shadcn@latest add direction
```
</TabsContent>
<TabsContent value="manual">
<Steps className="mb-0 pt-2">
<Step>Install the following dependencies:</Step>
```bash
npm install @base-ui/react
```
<Step>Copy and paste the following code into your project.</Step>
<ComponentSource
name="direction"
title="components/ui/direction.tsx"
styleName="base-nova"
/>
<Step>Update the import paths to match your project setup.</Step>
</Steps>
</TabsContent>
</CodeTabs>
## Usage
```tsx showLineNumbers
import { DirectionProvider } from "@/components/ui/direction"
```
```tsx showLineNumbers
<html dir="rtl">
<body>
<DirectionProvider direction="rtl">
{/* Your app content */}
</DirectionProvider>
</body>
</html>
```
## useDirection
The `useDirection` hook is used to get the current direction of the application.
```tsx showLineNumbers
import { useDirection } from "@/components/ui/direction"
function MyComponent() {
const direction = useDirection()
return <div>Current direction: {direction}</div>
}
```

View File

@@ -108,6 +108,12 @@ You can combine the `Dialog` and `Drawer` components to create a responsive dial
<ComponentPreview styleName="base-nova" name="drawer-dialog" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="drawer-rtl" direction="rtl" />
## API Reference
See the [Vaul documentation](https://vaul.emilkowal.ski/getting-started) for the full API reference.

View File

@@ -5,8 +5,8 @@ featured: true
base: base
component: true
links:
doc: https://base-ui.com/react/components/dropdown-menu
api: https://base-ui.com/react/components/dropdown-menu#api-reference
doc: https://base-ui.com/react/components/menu
api: https://base-ui.com/react/components/menu#api-reference
---
<ComponentPreview
@@ -160,6 +160,16 @@ A richer example combining groups, icons, and submenus.
<ComponentPreview styleName="base-nova" name="dropdown-menu-complex" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="dropdown-menu-rtl"
direction="rtl"
/>
## API Reference
See the [Base UI documentation](https://base-ui.com/react/components/dropdown-menu) for the full API reference.
See the [Base UI documentation](https://base-ui.com/react/components/menu) for the full API reference.

View File

@@ -127,6 +127,12 @@ You can add an `InputGroup` component to the `EmptyContent` component.
previewClassName="h-96 p-0"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="empty-rtl" direction="rtl" />
## API Reference
### Empty

View File

@@ -162,6 +162,17 @@ Stack `Field` components with `FieldGroup`. Add `FieldSeparator` to divide them.
previewClassName="h-96"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="field-rtl"
direction="rtl"
previewClassName="h-auto p-6"
/>
## Responsive Layout
- **Vertical fields:** Default orientation stacks label, control, and helper text—ideal for mobile-first layouts.

View File

@@ -120,6 +120,17 @@ Use the `side` and `align` props on `HoverCardContent` to control placement.
previewClassName="h-[22rem]"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="hover-card-rtl"
direction="rtl"
previewClassName="h-80"
/>
## API Reference
See the [Base UI](https://base-ui.com/react/components/hover-card#api-reference) documentation for more information.

View File

@@ -191,6 +191,17 @@ Here's an example of a custom resizable textarea from a third-party library.
previewClassName="h-56"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="input-group-rtl"
direction="rtl"
previewClassName="h-[30rem]"
/>
## API Reference
### InputGroup

View File

@@ -142,6 +142,12 @@ Use `REGEXP_ONLY_DIGITS_AND_CHARS` to accept both letters and numbers.
previewClassName="h-[30rem]"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="input-otp-rtl" direction="rtl" />
## API Reference
See the [input-otp](https://input-otp.rodz.dev) documentation for more information.

View File

@@ -188,3 +188,14 @@ A full form example with multiple inputs, a select, and a button.
name="input-form"
previewClassName="h-[32rem]"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview
styleName="base-nova"
name="input-rtl"
direction="rtl"
previewClassName="*:max-w-xs"
/>

View File

@@ -161,6 +161,12 @@ Use the `render` prop to render the item as a link. The hover and focus states w
<ComponentPreview styleName="base-nova" name="item-dropdown" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="item-rtl" direction="rtl" />
## API Reference
### Item

View File

@@ -79,6 +79,12 @@ You can use the `Kbd` component inside a `InputGroupAddon` component to display
<ComponentPreview styleName="base-nova" name="kbd-input-group" />
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="kbd-rtl" direction="rtl" />
## API Reference
### Kbd

View File

@@ -85,6 +85,12 @@ includes built-in `FieldLabel`, `FieldDescription`, and `FieldError` components.
previewClassName="h-[44rem]"
/>
## RTL
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
<ComponentPreview styleName="base-nova" name="label-rtl" direction="rtl" />
## API Reference
See the [Base UI Label](https://base-ui.com/react/components/label#api-reference) documentation for more information.

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