mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-15 03:41:33 +00:00
Compare commits
228 Commits
shadcn-pat
...
shadcn/v4-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f0ae22516 | ||
|
|
360a649d2a | ||
|
|
4bdd23291c | ||
|
|
1ee480122b | ||
|
|
382a5220e0 | ||
|
|
627155b13c | ||
|
|
0ca4dd1b32 | ||
|
|
383bcc4fc1 | ||
|
|
8028a0d75d | ||
|
|
31c1c5eb56 | ||
|
|
ca9295016a | ||
|
|
7b90fe9833 | ||
|
|
330786352c | ||
|
|
da309ae929 | ||
|
|
3877ae5328 | ||
|
|
9c99070d54 | ||
|
|
5751250a7f | ||
|
|
f97ff8124c | ||
|
|
7f37ed96d1 | ||
|
|
7ff7049018 | ||
|
|
ae895787c1 | ||
|
|
305f5c7d47 | ||
|
|
f0d3984376 | ||
|
|
b8f355ac4f | ||
|
|
29195a17a7 | ||
|
|
e90efd4fa9 | ||
|
|
70c158990d | ||
|
|
6e2efb4b55 | ||
|
|
18db1a78ab | ||
|
|
bd87d729fd | ||
|
|
a6f3ef591f | ||
|
|
aaed0a186c | ||
|
|
2b74bbca5c | ||
|
|
f9de81f032 | ||
|
|
444aa53803 | ||
|
|
4e9f3e6e05 | ||
|
|
3fc4482d7c | ||
|
|
dd3e942057 | ||
|
|
e81d850438 | ||
|
|
779453be26 | ||
|
|
867d341182 | ||
|
|
78b51f9a11 | ||
|
|
417772dd9c | ||
|
|
b86885512f | ||
|
|
65381cd614 | ||
|
|
e3f11d8fe1 | ||
|
|
093eb419a8 | ||
|
|
ad25490cf9 | ||
|
|
e94d3d80fa | ||
|
|
0e6b6d90bc | ||
|
|
ce1f9259bf | ||
|
|
8cec12b98b | ||
|
|
028b1b2d93 | ||
|
|
d8e5d0d4f1 | ||
|
|
0da9826821 | ||
|
|
b416e09e8b | ||
|
|
2ef58bd75d | ||
|
|
cac794208e | ||
|
|
a22aec8694 | ||
|
|
6f11e820b5 | ||
|
|
6a75b60b4f | ||
|
|
c494adbd87 | ||
|
|
3aa0f13869 | ||
|
|
e9af9efaf3 | ||
|
|
1ecc8066db | ||
|
|
525775fb36 | ||
|
|
3e4c608aca | ||
|
|
bd5028e331 | ||
|
|
4207614600 | ||
|
|
e1af950724 | ||
|
|
e91388a010 | ||
|
|
8648ddb528 | ||
|
|
feff5b6a57 | ||
|
|
32198910ce | ||
|
|
07f7147ff3 | ||
|
|
0e8a006adc | ||
|
|
d2f91d6f1e | ||
|
|
9ed5093474 | ||
|
|
a12dd019d3 | ||
|
|
e53bc92f41 | ||
|
|
597a8db2d9 | ||
|
|
0b0f639cd0 | ||
|
|
6b4ba6bca1 | ||
|
|
3cdd67b5b4 | ||
|
|
2b03bc7a53 | ||
|
|
f6447b8936 | ||
|
|
4069c33671 | ||
|
|
4dbc5581a5 | ||
|
|
3fc5c1c995 | ||
|
|
f123057ae5 | ||
|
|
5025ec818f | ||
|
|
65c6c8146d | ||
|
|
b93745f24a | ||
|
|
bbb59c9fe1 | ||
|
|
fb56f6571a | ||
|
|
082af1f82c | ||
|
|
f5b3a0cbad | ||
|
|
d602ccc224 | ||
|
|
ab54e7b7bd | ||
|
|
0137b07f66 | ||
|
|
ae95fbd1be | ||
|
|
625bd97d8b | ||
|
|
603fce7cd3 | ||
|
|
c759f460d5 | ||
|
|
e1c00667f7 | ||
|
|
46631fc4d4 | ||
|
|
f235a5d951 | ||
|
|
b0b711f181 | ||
|
|
f1b7102583 | ||
|
|
f076420e68 | ||
|
|
4ce0a7eaa1 | ||
|
|
270b730c21 | ||
|
|
14a6cc5999 | ||
|
|
0067873f60 | ||
|
|
fc16e1461f | ||
|
|
8f01916bb2 | ||
|
|
87d522f249 | ||
|
|
ead138b4cd | ||
|
|
ef39979548 | ||
|
|
ab6c8caf2f | ||
|
|
ba9206bded | ||
|
|
c5838cf955 | ||
|
|
0c41fc30e4 | ||
|
|
8270cfa39e | ||
|
|
06e356cab9 | ||
|
|
f24631dc48 | ||
|
|
ec936bcd06 | ||
|
|
6c7975e400 | ||
|
|
8acef7ab66 | ||
|
|
4ddfd39b0d | ||
|
|
3ba37cc24c | ||
|
|
da080118b0 | ||
|
|
e8897ea80a | ||
|
|
9d26f582fa | ||
|
|
0a2ad2176c | ||
|
|
7c36439836 | ||
|
|
a1e3afed06 | ||
|
|
be5b1bbae3 | ||
|
|
52de23bf95 | ||
|
|
1d16fe46cd | ||
|
|
cbecda13f9 | ||
|
|
24649ec103 | ||
|
|
b9f62a8399 | ||
|
|
689d45e095 | ||
|
|
33f7b3f2bb | ||
|
|
2cce072393 | ||
|
|
d64bdec2f9 | ||
|
|
5adacdecad | ||
|
|
f2552d3f3b | ||
|
|
b435e01199 | ||
|
|
cd576df6e4 | ||
|
|
9fbd3b1a72 | ||
|
|
c6dd35a092 | ||
|
|
470c6f42b0 | ||
|
|
e6956e45ac | ||
|
|
a2b9dedbb7 | ||
|
|
384129609f | ||
|
|
5be0811f01 | ||
|
|
1a10b4671a | ||
|
|
e7d36b7e21 | ||
|
|
290fac9115 | ||
|
|
0633333db4 | ||
|
|
630323ad47 | ||
|
|
44a9b3bd12 | ||
|
|
2b879a5ec8 | ||
|
|
381f2ef165 | ||
|
|
825ebca3f0 | ||
|
|
e0063070a6 | ||
|
|
013ae51d10 | ||
|
|
08e54510ed | ||
|
|
a95606cee9 | ||
|
|
c990476d99 | ||
|
|
44c8f02d06 | ||
|
|
a012542015 | ||
|
|
926df433a7 | ||
|
|
5c09e0d8fa | ||
|
|
dba86053f5 | ||
|
|
cd188b267d | ||
|
|
8a09fbaac9 | ||
|
|
9676c8f4ee | ||
|
|
9b5aeab889 | ||
|
|
28ebf1b88a | ||
|
|
f922e82f53 | ||
|
|
beec1e060e | ||
|
|
26a24d3d5c | ||
|
|
c3c7f03f04 | ||
|
|
4af29d6c20 | ||
|
|
b28f77f893 | ||
|
|
b8c7ae8088 | ||
|
|
d21c74fb3a | ||
|
|
d6548b4ae8 | ||
|
|
110a4ec10b | ||
|
|
851562f4f2 | ||
|
|
b7b839ebc2 | ||
|
|
8d9be074a3 | ||
|
|
a0c077da9e | ||
|
|
540cd031c3 | ||
|
|
4d9720449f | ||
|
|
f1e10f3da8 | ||
|
|
e2225d4a93 | ||
|
|
444f6889c8 | ||
|
|
03a7804c42 | ||
|
|
acc847bed3 | ||
|
|
abfa2ddb74 | ||
|
|
5e92c160dd | ||
|
|
d41e857ba3 | ||
|
|
99651191cc | ||
|
|
712285f60e | ||
|
|
aed95086e0 | ||
|
|
1990280d66 | ||
|
|
2bf55c9133 | ||
|
|
3192a3db55 | ||
|
|
afa2a7adf2 | ||
|
|
728d8af275 | ||
|
|
38de7fddc2 | ||
|
|
c719d24f3a | ||
|
|
4479965555 | ||
|
|
7ea124b25d | ||
|
|
f746368369 | ||
|
|
164b6ff6c1 | ||
|
|
759003c781 | ||
|
|
6529256e98 | ||
|
|
4a39de5c56 | ||
|
|
d53f7489ce | ||
|
|
dfe784b44a | ||
|
|
3dbe9e6a3e | ||
|
|
31f8af8409 | ||
|
|
9317a93152 |
12
.changeset/few-ghosts-move.md
Normal file
12
.changeset/few-ghosts-move.md
Normal file
@@ -0,0 +1,12 @@
|
||||
---
|
||||
"shadcn": major
|
||||
---
|
||||
|
||||
added `--preset` flag to `init` command
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- The `--base-color` flag has been removed. Use `--preset` with a custom preset URL instead.
|
||||
- The `--src-dir` and `--no-src-dir` flags have been removed. Use create-next-app then run shadcn init.
|
||||
- The `--no-base-style` flag has been removed. Use `extends: none` in your registry items.
|
||||
- The `--css-variables` and `--no-css-variables` flags have been removed from the `add` command.
|
||||
5
.changeset/hot-socks-love.md
Normal file
5
.changeset/hot-socks-love.md
Normal file
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"shadcn": major
|
||||
---
|
||||
|
||||
add --reinstall flag for init
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"shadcn": patch
|
||||
---
|
||||
|
||||
Fix: skip all transforms for universal registry items
|
||||
10
.github/changeset-version.js
vendored
10
.github/changeset-version.js
vendored
@@ -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" })
|
||||
|
||||
2
.github/workflows/test.yml
vendored
2
.github/workflows/test.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 20
|
||||
node-version: 22
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
|
||||
5
.github/workflows/validate-registries.yml
vendored
5
.github/workflows/validate-registries.yml
vendored
@@ -4,11 +4,13 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
|
||||
jobs:
|
||||
validate:
|
||||
@@ -47,8 +49,5 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build --filter=shadcn
|
||||
|
||||
- name: Validate registries
|
||||
run: pnpm --filter=v4 validate:registries
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -41,3 +41,4 @@ tsconfig.tsbuildinfo
|
||||
.vscode
|
||||
|
||||
.notes
|
||||
.playwright-mcp
|
||||
|
||||
@@ -6,7 +6,7 @@ A set of beautifully designed components that you can customize, extend, and bui
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit http://ui.shadcn.com/docs to view the documentation.
|
||||
Visit https://ui.shadcn.com/docs to view the documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -14,4 +14,4 @@ Please read the [contributing guide](/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).
|
||||
Licensed under the [MIT license](./LICENSE.md).
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -21,7 +21,7 @@ export function ButtonGroupPopover() {
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<PopoverContent align="end" className="gap-0 rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
|
||||
@@ -13,6 +13,7 @@ import { Input } from "@/examples/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
@@ -21,7 +22,7 @@ import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-lg border p-6">
|
||||
<div className="w-full max-w-md rounded-xl border p-6">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -190,12 +190,12 @@ export function NotionPromptForm() {
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup>
|
||||
<InputGroup className="rounded-xl">
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start">
|
||||
<InputGroupAddon align="block-start" className="pt-3">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
@@ -209,7 +209,7 @@ export function NotionPromptForm() {
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="rounded-full transition-transform"
|
||||
className="transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
@@ -235,6 +235,7 @@ export function NotionPromptForm() {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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[]
|
||||
}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
179
apps/v4/app/(app)/examples/rtl/components/button-group-demo.tsx
Normal file
179
apps/v4/app/(app)/examples/rtl/components/button-group-demo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
36
apps/v4/app/(app)/examples/rtl/components/field-checkbox.tsx
Normal file
36
apps/v4/app/(app)/examples/rtl/components/field-checkbox.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
217
apps/v4/app/(app)/examples/rtl/components/field-demo.tsx
Normal file
217
apps/v4/app/(app)/examples/rtl/components/field-demo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
90
apps/v4/app/(app)/examples/rtl/components/field-hear.tsx
Normal file
90
apps/v4/app/(app)/examples/rtl/components/field-hear.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
67
apps/v4/app/(app)/examples/rtl/components/field-slider.tsx
Normal file
67
apps/v4/app/(app)/examples/rtl/components/field-slider.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
92
apps/v4/app/(app)/examples/rtl/components/index.tsx
Normal file
92
apps/v4/app/(app)/examples/rtl/components/index.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
140
apps/v4/app/(app)/examples/rtl/components/input-group-demo.tsx
Normal file
140
apps/v4/app/(app)/examples/rtl/components/input-group-demo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
64
apps/v4/app/(app)/examples/rtl/components/item-demo.tsx
Normal file
64
apps/v4/app/(app)/examples/rtl/components/item-demo.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
516
apps/v4/app/(app)/examples/rtl/components/notion-prompt-form.tsx
Normal file
516
apps/v4/app/(app)/examples/rtl/components/notion-prompt-form.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
44
apps/v4/app/(app)/examples/rtl/components/spinner-badge.tsx
Normal file
44
apps/v4/app/(app)/examples/rtl/components/spinner-badge.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
52
apps/v4/app/(app)/examples/rtl/components/spinner-empty.tsx
Normal file
52
apps/v4/app/(app)/examples/rtl/components/spinner-empty.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
14
apps/v4/app/(app)/examples/rtl/page.tsx
Normal file
14
apps/v4/app/(app)/examples/rtl/page.tsx
Normal 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
|
||||
@@ -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>({
|
||||
|
||||
@@ -38,21 +38,6 @@ export function Customizer() {
|
||||
className="no-scrollbar -mx-2.5 flex flex-col overflow-y-auto p-1 md:mx-0 md:h-[calc(100svh-var(--header-height)-2rem)] md:w-48 md:gap-0 md:py-0"
|
||||
ref={anchorRef}
|
||||
>
|
||||
<div className="hidden items-center gap-2 px-[calc(--spacing(2.5))] pb-1 md:flex md:flex-col md:items-start">
|
||||
<HugeiconsIcon
|
||||
icon={Settings05Icon}
|
||||
className="size-4"
|
||||
strokeWidth={2}
|
||||
/>
|
||||
<div className="relative flex flex-col gap-1 rounded-lg text-[13px]/snug">
|
||||
<div className="flex items-center gap-1 font-medium text-balance">
|
||||
Build your own shadcn/ui
|
||||
</div>
|
||||
<div className="hidden md:flex">
|
||||
When you're done, click Create Project to start a new project.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="no-scrollbar h-14 overflow-x-auto overflow-y-hidden p-px md:h-full md:overflow-x-hidden md:overflow-y-auto">
|
||||
<FieldGroup className="flex h-full flex-1 flex-row gap-2 md:flex-col md:gap-0">
|
||||
<PresetPicker
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -79,7 +79,7 @@ export function ItemExplorer({
|
||||
)}
|
||||
<SidebarMenuButton
|
||||
onClick={() => setParams({ item: item.name })}
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[26px] w-fit cursor-pointer overflow-visible border border-transparent text-[0.8rem] font-normal after:absolute after:inset-x-0 after:-inset-y-1 after:-z-0 after:rounded-md"
|
||||
className="data-[active=true]:bg-accent data-[active=true]:border-accent 3xl:fixed:w-full 3xl:fixed:max-w-48 relative h-[26px] w-fit cursor-pointer overflow-visible border border-transparent text-[0.8rem] font-normal after:absolute after:inset-x-0 after:-inset-y-1 after:z-0 after:rounded-md"
|
||||
data-active={item.name === currentItem?.name}
|
||||
isActive={item.name === currentItem?.name}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -7,11 +7,15 @@ import {
|
||||
Tick02Icon,
|
||||
} from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@@ -23,6 +27,8 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
@@ -31,17 +37,13 @@ import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tabs"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const TEMPLATES = [
|
||||
@@ -55,14 +57,24 @@ const TEMPLATES = [
|
||||
title: "TanStack Start",
|
||||
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TanStack</title><path d="M11.078.042c.316-.042.65-.014.97-.014 1.181 0 2.341.184 3.472.532a12.3 12.3 0 0 1 3.973 2.086 11.9 11.9 0 0 1 3.432 4.33c1.446 3.15 1.436 6.97-.046 10.107-.958 2.029-2.495 3.727-4.356 4.965-1.518 1.01-3.293 1.629-5.1 1.848-2.298.279-4.784-.129-6.85-1.188-3.88-1.99-6.518-5.994-6.57-10.382-.01-.846.003-1.697.17-2.534.273-1.365.748-2.683 1.463-3.88a12 12 0 0 1 2.966-3.36A12.3 12.3 0 0 1 9.357.3a12 12 0 0 1 1.255-.2l.133-.016zM7.064 19.99c-.535.057-1.098.154-1.557.454.103.025.222 0 .33 0 .258 0 .52-.01.778.002.647.028 1.32.131 1.945.303.8.22 1.505.65 2.275.942.813.307 1.622.402 2.484.402.435 0 .866-.001 1.287-.12-.22-.117-.534-.095-.778-.144a11 11 0 0 1-1.556-.416 12 12 0 0 1-1.093-.467l-.23-.108a15 15 0 0 0-1.012-.44c-.905-.343-1.908-.512-2.873-.408m.808-2.274c-1.059 0-2.13.187-3.083.667q-.346.177-.659.41c-.063.046-.175.106-.199.188s.061.151.11.204c.238-.127.464-.261.718-.357 1.64-.624 3.63-.493 5.268.078.817.285 1.569.712 2.365 1.046.89.374 1.798.616 2.753.74 1.127.147 2.412.028 3.442-.48.362-.179.865-.451 1.018-.847-.189.017-.36.098-.539.154a9 9 0 0 1-.868.222c-.994.2-2.052.24-3.053.06-.943-.17-1.82-.513-2.693-.873l-.111-.046-.223-.092-.112-.046a26 26 0 0 0-1.35-.527c-.89-.31-1.842-.5-2.784-.5M9.728 1.452c-1.27.28-2.407.826-3.502 1.514-.637.4-1.245.81-1.796 1.323-.82.765-1.447 1.695-1.993 2.666-.563 1-.924 2.166-1.098 3.297-.172 1.11-.2 2.277-.004 3.388.245 1.388.712 2.691 1.448 3.897.248-.116.424-.38.629-.557.414-.359.85-.691 1.317-.978a3.5 3.5 0 0 1 .539-.264c.07-.029.187-.055.22-.132.053-.124-.045-.34-.062-.468a7 7 0 0 1-.068-1.109 9.7 9.7 0 0 1 .61-3.177c.29-.76.73-1.45 1.254-2.069.177-.21.365-.405.56-.6.115-.114.258-.212.33-.359-.376 0-.751.108-1.108.218-.769.237-1.518.588-2.155 1.084-.291.226-.504.522-.779.76-.084.073-.235.17-.352.116-.176-.083-.149-.43-.169-.59-.078-.612.154-1.387.45-1.918.473-.852 1.348-1.58 2.376-1.555.444.011.833.166 1.257.266-.107-.153-.252-.264-.389-.39a5.4 5.4 0 0 0-1.107-.8c-.163-.085-.338-.136-.509-.2-.086-.03-.195-.074-.227-.17-.06-.177.26-.342.377-.417.453-.289 1.01-.527 1.556-.54.854-.021 1.688.452 2.04 1.258.123.284.16.583.184.885l.004.057.006.085.002.029.005.057.004.056c.268-.218.457-.54.718-.774.612-.547 1.45-.79 2.245-.544a2.97 2.97 0 0 1 1.71 1.378c.097.173.365.595.171.767-.152.134-.344.03-.504-.026a3 3 0 0 0-.372-.094l-.068-.014-.069-.013a3.9 3.9 0 0 0-1.377-.002c-.282.05-.557.15-.838.192v.06c.768.006 1.51.444 1.89 1.109.157.275.235.59.295.9.075.38.022.796-.082 1.168-.035.125-.098.336-.247.365-.106.02-.195-.085-.256-.155a4.6 4.6 0 0 0-.492-.522 20 20 0 0 0-1.467-1.14c-.267-.19-.56-.44-.868-.556.087.208.171.402.2.63.088.667-.192 1.296-.612 1.798a2.6 2.6 0 0 1-.426.427c-.067.05-.151.114-.24.1-.277-.044-.31-.463-.353-.677-.144-.726-.086-1.447.114-2.158-.178.09-.307.287-.418.45a5.3 5.3 0 0 0-.612 1.138c-.61 1.617-.604 3.51.186 5.066.088.174.221.15.395.15h.157a3 3 0 0 1 .472.018c.08.01.193 0 .257.06.077.072.036.194.018.282-.05.246-.066.469-.066.72.328-.051.419-.576.535-.84.131-.298.265-.597.387-.9.06-.148.14-.314.119-.479-.024-.185-.157-.381-.25-.54-.177-.298-.378-.606-.508-.929-.104-.258-.007-.58.286-.672.161-.05.334.049.439.166.22.244.363.609.523.896l1.249 2.248q.159.286.32.57c.043.074.086.188.173.219.077.028.182-.012.26-.027.198-.04.398-.083.598-.12.24-.043.605-.035.778-.222-.253-.08-.545-.075-.808-.057-.158.01-.333.067-.479-.025-.216-.137-.36-.455-.492-.667-.326-.525-.633-1.057-.945-1.59l-.05-.084-.1-.17q-.075-.126-.149-.255c-.037-.066-.092-.153-.039-.227.056-.076.179-.08.29-.081h.021q.066.001.117-.004a10 10 0 0 1 1.347-.107c-.035-.122-.135-.26-.103-.39.071-.292.49-.383.686-.174.131.14.207.334.292.504.113.223.24.44.361.66.211.383.441.757.658 1.138l.055.094.028.047c.093.156.187.314.238.489-.753-.035-1.318-.909-1.646-1.499-.027.095.016.179.05.27q.103.282.262.54c.152.244.326.495.556.673.408.315.945.317 1.436.283.315-.022.708-.165 1.018-.068s.434.438.25.7c-.138.196-.321.27-.55.3.162.346.373.667.527 1.02.064.146.13.37.283.448.102.051.248.003.358 0-.11-.292-.317-.54-.419-.839.31.015.61.176.898.28.567.202 1.128.424 1.687.648l.258.104c.23.092.462.183.689.283.083.037.198.123.29.07.074-.043.123-.146.169-.215a10.3 10.3 0 0 0 1.393-3.208c.75-2.989.106-6.287-1.695-8.783-.692-.96-1.562-1.789-2.522-2.476-2.401-1.718-5.551-2.407-8.44-1.768m4.908 14.904c-.636.166-1.292.317-1.945.401.086.293.296.577.45.84.059.101.122.237.24.281.132.05.292-.03.417-.072-.058-.158-.155-.3-.235-.45-.033-.06-.084-.133-.056-.206.05-.137.263-.13.381-.153.31-.063.617-.142.928-.204.114-.023.274-.085.389-.047.086.03.138.1.187.174l.022.033q.043.07.097.122c.125.113.313.13.472.162-.097-.219-.259-.41-.362-.63-.06-.127-.11-.315-.242-.388-.182-.102-.557.089-.743.137m-4.01-1.457c-.03.38-.147.689-.33 1.019.21.026.423.036.629.087.154.038.296.11.449.153-.082-.224-.233-.423-.35-.63-.12-.208-.226-.462-.398-.63" fill="currentColor"/></svg>',
|
||||
},
|
||||
{
|
||||
value: "react-router",
|
||||
title: "React Router",
|
||||
logo: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"><path d="M12.118 5.466a2.306 2.306 0 0 0-.623.08c-.278.067-.702.332-.953.583-.41.423-.49.609-.662 1.469-.08.423.41 1.43.847 1.734.45.317 1.085.502 2.065.608 1.429.16 1.84.636 1.84 2.197 0 1.377-.385 1.747-1.96 1.906-1.707.172-2.58.834-2.765 2.117-.106.781.41 1.76 1.125 2.091 1.627.768 3.15-.198 3.467-2.196.211-1.284.622-1.642 1.998-1.747 1.588-.133 2.409-.675 2.713-1.787.278-1.02-.304-2.157-1.297-2.554-.264-.106-.873-.238-1.35-.291-1.495-.16-1.879-.424-2.038-1.39-.225-1.337-.317-1.562-.794-2.09a2.174 2.174 0 0 0-1.613-.73zm-4.785 4.36a2.145 2.145 0 0 0-.497.048c-1.469.318-2.17 2.051-1.35 3.295 1.178 1.774 3.944.953 3.97-1.177.012-1.193-.98-2.143-2.123-2.166zM2.089 14.19a2.22 2.22 0 0 0-.427.052c-2.158.476-2.237 3.626-.106 4.182.53.145.582.145 1.111.013 1.191-.318 1.866-1.456 1.549-2.607-.278-1.02-1.144-1.664-2.127-1.64zm19.824.008c-.233.002-.477.058-.784.162-1.39.477-1.866 2.092-.98 3.336.557.794 1.96 1.058 2.82.516 1.416-.874 1.363-3.057-.093-3.746-.38-.186-.663-.271-.963-.268z" /></svg>',
|
||||
},
|
||||
{
|
||||
value: "vite",
|
||||
title: "Vite",
|
||||
logo: '<svg xmlns="http://www.w3.org/2000/svg" width="410" height="404" fill="none" viewBox="0 0 410 404"><path fill="var(--foreground)" d="m399.641 59.525-183.998 329.02c-3.799 6.793-13.559 6.833-17.415.073L10.582 59.556C6.38 52.19 12.68 43.266 21.028 44.76l184.195 32.923c1.175.21 2.378.208 3.553-.006l180.343-32.87c8.32-1.517 14.649 7.337 10.522 14.719"/><path fill="var(--background)" d="M292.965 1.574 156.801 28.255a5 5 0 0 0-4.03 4.611l-8.376 141.464c-.197 3.332 2.863 5.918 6.115 5.168l37.91-8.749c3.547-.818 6.752 2.306 6.023 5.873l-11.263 55.153c-.758 3.712 2.727 6.886 6.352 5.785l23.415-7.114c3.63-1.102 7.118 2.081 6.35 5.796l-17.899 86.633c-1.12 5.419 6.088 8.374 9.094 3.728l2.008-3.103 110.954-221.428c1.858-3.707-1.346-7.935-5.418-7.15l-39.022 7.532c-3.667.707-6.787-2.708-5.752-6.296l25.469-88.291c1.036-3.594-2.095-7.012-5.766-6.293"/></svg>',
|
||||
},
|
||||
{
|
||||
value: "next-monorepo",
|
||||
title: "Next.js (Monorepo)",
|
||||
logo: '<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Next.js</title><path d="M18.665 21.978C16.758 23.255 14.465 24 12 24 5.377 24 0 18.623 0 12S5.377 0 12 0s12 5.377 12 12c0 3.583-1.574 6.801-4.067 9.001L9.219 7.2H7.2v9.596h1.615V9.251l9.85 12.727Zm-3.332-8.533 1.6 2.061V7.2h-1.6v6.245Z" fill="currentColor"/></svg>',
|
||||
},
|
||||
] as const
|
||||
|
||||
export function ToolbarControls() {
|
||||
export function ProjectForm() {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const [config, setConfig] = useConfig()
|
||||
@@ -71,15 +83,25 @@ 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 rtlFlag = params.rtl ? " --rtl" : ""
|
||||
const templateFlag = params.template ? ` --template ${params.template}` : ""
|
||||
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}`,
|
||||
}
|
||||
const isLocalDev = origin.includes("localhost")
|
||||
|
||||
return isLocalDev
|
||||
? {
|
||||
pnpm: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `pnpm shadcn init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
: {
|
||||
pnpm: `pnpm dlx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest init${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
}, [
|
||||
params.base,
|
||||
params.style,
|
||||
@@ -91,6 +113,7 @@ export function ToolbarControls() {
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
params.template,
|
||||
params.rtl,
|
||||
])
|
||||
|
||||
const command = commands[packageManager]
|
||||
@@ -103,31 +126,6 @@ export function ToolbarControls() {
|
||||
}, [hasCopied])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
const properties: Record<string, string> = {
|
||||
command,
|
||||
}
|
||||
if (params.template) {
|
||||
properties.template = params.template
|
||||
}
|
||||
copyToClipboardWithMeta(command, {
|
||||
name: "copy_npm_command",
|
||||
properties,
|
||||
})
|
||||
setOpen(false)
|
||||
setHasCopied(true)
|
||||
toast("Command copied to clipboard.", {
|
||||
description:
|
||||
"Paste and run the command in your terminal to create a new shadcn/ui project.",
|
||||
position: "bottom-center",
|
||||
classNames: {
|
||||
content: "rounded-xl",
|
||||
toast: "rounded-xl!",
|
||||
description: "text-sm/leading-normal!",
|
||||
},
|
||||
})
|
||||
}, [command, params.template, setOpen])
|
||||
|
||||
const handleCopyFromTabs = React.useCallback(() => {
|
||||
const properties: Record<string, string> = {
|
||||
command,
|
||||
}
|
||||
@@ -153,112 +151,164 @@ export function ToolbarControls() {
|
||||
icon={ComputerTerminal01Icon}
|
||||
className="hidden xl:flex"
|
||||
/>
|
||||
Create Project
|
||||
Copy Command
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="dialog-ring min-w-0 overflow-hidden rounded-xl sm:max-w-md">
|
||||
<DialogContent className="dialog-ring min-w-0 overflow-hidden rounded-xl sm:max-w-lg">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription className="text-balance">
|
||||
Select a template and run this command to create a{" "}
|
||||
{selectedTemplate?.title} + shadcn/ui project.
|
||||
Configure your project to use shadcn/ui.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<FieldGroup className="dark:**:data-[slot=field-label]:has-data-[state=checked]:bg-primary/10 dark:**:data-[slot=field-label]:has-data-[state=checked]:border-primary **:data-[slot=field-description]:text-balance **:data-[slot=field-label]:rounded-lg! **:data-[slot=field-label]:has-data-[state=checked]:border-blue-600 **:data-[slot=field-label]:has-data-[state=checked]:bg-blue-50/50 **:data-[slot=radio-group-item]:sr-only **:data-[slot=radio-group-item]:absolute">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="template" className="sr-only">
|
||||
Template
|
||||
<FieldLabel htmlFor="template" className="text-base">
|
||||
Choose a template
|
||||
</FieldLabel>
|
||||
<RadioGroup
|
||||
id="template"
|
||||
value={params.template}
|
||||
onValueChange={(value) => {
|
||||
setParams({
|
||||
template: value as "next" | "start" | "vite",
|
||||
template: value as
|
||||
| "next"
|
||||
| "next-monorepo"
|
||||
| "start"
|
||||
| "react-router"
|
||||
| "vite",
|
||||
})
|
||||
}}
|
||||
className="grid grid-cols-3 gap-2"
|
||||
>
|
||||
{TEMPLATES.map((template) => (
|
||||
<FieldLabel
|
||||
key={template.value}
|
||||
htmlFor={template.value}
|
||||
className="rounded-lg!"
|
||||
>
|
||||
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
|
||||
<FieldLabel key={template.value} htmlFor={template.value}>
|
||||
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-4! text-center *:w-auto!">
|
||||
<RadioGroupItem
|
||||
value={template.value}
|
||||
id={template.value}
|
||||
className="sr-only"
|
||||
/>
|
||||
{template.logo && (
|
||||
{template.logo ? (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! size-6 [&_svg]:size-6"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: template.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
) : null}
|
||||
<FieldTitle>{template.title}</FieldTitle>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
<FieldDescription>
|
||||
See the{" "}
|
||||
<a
|
||||
href="/docs/installation"
|
||||
className="text-foreground underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
installation guides
|
||||
</a>{" "}
|
||||
for more templates and frameworks.
|
||||
</FieldDescription>
|
||||
</Field>
|
||||
<Collapsible className="rounded-lg border">
|
||||
<CollapsibleTrigger className="text-muted-foreground hover:text-foreground flex w-full items-center justify-between px-3 py-2.5 text-sm font-medium transition-colors">
|
||||
Options
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="transition-transform [[data-state=open]>&]:rotate-180"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent className="border-t px-3 py-3">
|
||||
<Field orientation="horizontal" className="items-center">
|
||||
<FieldContent className="gap-0.5">
|
||||
<FieldLabel htmlFor="rtl" className="text-sm">
|
||||
Enable RTL
|
||||
</FieldLabel>
|
||||
<FieldDescription className="text-balance">
|
||||
Enable right-to-left support. See the{" "}
|
||||
<a
|
||||
href={`/docs/rtl/${params.template === "next-monorepo" ? "next" : params.template}`}
|
||||
className="text-foreground underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
RTL setup guide
|
||||
</a>{" "}
|
||||
for {selectedTemplate?.title}.
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
id="rtl"
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ rtl: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</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>
|
||||
<DialogFooter className="bg-muted/30 -mx-6 mt-2 -mb-6 flex min-w-0 flex-col gap-3 border-t p-6 sm:flex-col">
|
||||
<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 px-1.5 py-1">
|
||||
<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>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto size-7 rounded-lg"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</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>
|
||||
<DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col">
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button
|
||||
size="sm"
|
||||
onClick={handleCopy}
|
||||
@@ -68,7 +68,7 @@ export function TemplatePicker({
|
||||
value={params.template}
|
||||
onValueChange={(value) => {
|
||||
setParams({
|
||||
template: value as "next" | "start" | "vite",
|
||||
template: value as "next" | "next-monorepo" | "start" | "vite",
|
||||
})
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -19,10 +19,10 @@ import { Customizer } from "@/app/(create)/components/customizer"
|
||||
import { ItemExplorer } from "@/app/(create)/components/item-explorer"
|
||||
import { ItemPicker } from "@/app/(create)/components/item-picker"
|
||||
import { Preview } from "@/app/(create)/components/preview"
|
||||
import { ProjectForm } from "@/app/(create)/components/project-form"
|
||||
import { RandomButton } from "@/app/(create)/components/random-button"
|
||||
import { ResetButton } from "@/app/(create)/components/reset-button"
|
||||
import { ShareButton } from "@/app/(create)/components/share-button"
|
||||
import { ToolbarControls } from "@/app/(create)/components/toolbar-controls"
|
||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
|
||||
import { getItemsForBase } from "@/app/(create)/lib/api"
|
||||
@@ -78,6 +78,7 @@ export default async function CreatePage({
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
}))
|
||||
.filter((item) => !/\d+$/.test(item.name))
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -86,8 +87,8 @@ export default async function CreatePage({
|
||||
>
|
||||
<header className="sticky top-0 z-50 w-full">
|
||||
<div className="container-wrapper 3xl:fixed:px-0 px-6">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:!h-4">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:h-4!">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:h-4!">
|
||||
<MobileNav
|
||||
tree={pageTree}
|
||||
items={siteConfig.navItems}
|
||||
@@ -124,7 +125,7 @@ export default async function CreatePage({
|
||||
/>
|
||||
<ShareButton />
|
||||
<V0Button />
|
||||
<ToolbarControls />
|
||||
<ProjectForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -18,7 +18,8 @@ export async function GET(request: NextRequest) {
|
||||
menuAccent: searchParams.get("menuAccent"),
|
||||
menuColor: searchParams.get("menuColor"),
|
||||
radius: searchParams.get("radius"),
|
||||
template: searchParams.get("template"),
|
||||
template: searchParams.get("template") ?? undefined,
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -63,9 +63,12 @@ const designSystemSearchParams = {
|
||||
).withDefault("default"),
|
||||
template: parseAsStringLiteral([
|
||||
"next",
|
||||
"next-monorepo",
|
||||
"start",
|
||||
"react-router",
|
||||
"vite",
|
||||
] as const).withDefault("next"),
|
||||
rtl: parseAsBoolean.withDefault(false),
|
||||
size: parseAsInteger.withDefault(100),
|
||||
custom: parseAsBoolean.withDefault(false),
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ export function ExampleForm() {
|
||||
password: "",
|
||||
},
|
||||
validators: {
|
||||
onChange: exampleFormSchema,
|
||||
onBlur: exampleFormSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
setValues(value)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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 (
|
||||
@@ -154,12 +154,12 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
value={view}
|
||||
onValueChange={(value) => setView(value as "preview" | "code")}
|
||||
>
|
||||
<TabsList className="grid h-8 grid-cols-2 items-center rounded-md p-1 *:data-[slot=tabs-trigger]:h-6 *:data-[slot=tabs-trigger]:rounded-sm *:data-[slot=tabs-trigger]:px-2 *:data-[slot=tabs-trigger]:text-xs">
|
||||
<TabsList className="grid h-8! grid-cols-2 items-center rounded-lg p-1 *:data-[slot=tabs-trigger]:h-6 *:data-[slot=tabs-trigger]:rounded-sm *:data-[slot=tabs-trigger]:px-2 *:data-[slot=tabs-trigger]:text-xs">
|
||||
<TabsTrigger value="preview">Preview</TabsTrigger>
|
||||
<TabsTrigger value="code">Code</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
<Separator orientation="vertical" className="mx-2 !h-4" />
|
||||
<Separator orientation="vertical" className="mx-2 h-4!" />
|
||||
<a
|
||||
href={`#${item.name}`}
|
||||
className="flex-1 text-center text-sm font-medium underline-offset-2 hover:underline md:flex-auto md:text-left"
|
||||
@@ -167,7 +167,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
{item.description?.replace(/\.$/, "")}
|
||||
</a>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<div className="h-8 items-center gap-1.5 rounded-md border p-1 shadow-none">
|
||||
<div className="h-8 items-center gap-1.5 rounded-md border p-[3px] shadow-none">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
defaultValue="100"
|
||||
@@ -177,7 +177,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
resizablePanelRef.current.resize(parseInt(value))
|
||||
}
|
||||
}}
|
||||
className="gap-1 *:data-[slot=toggle-group-item]:!size-6 *:data-[slot=toggle-group-item]:!rounded-sm"
|
||||
className="gap-1 *:data-[slot=toggle-group-item]:size-6! *:data-[slot=toggle-group-item]:rounded-sm!"
|
||||
>
|
||||
<ToggleGroupItem value="100" title="Desktop">
|
||||
<Monitor />
|
||||
@@ -188,7 +188,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
<ToggleGroupItem value="30" title="Mobile">
|
||||
<Smartphone />
|
||||
</ToggleGroupItem>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
@@ -201,7 +201,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
<Fullscreen />
|
||||
</Link>
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<Button
|
||||
size="icon"
|
||||
variant="ghost"
|
||||
@@ -218,7 +218,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
</Button>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="mx-1 !h-4" />
|
||||
<Separator orientation="vertical" className="mx-1 h-4!" />
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-fit gap-1 px-2 shadow-none"
|
||||
@@ -230,7 +230,7 @@ function BlockViewerToolbar({ styleName }: { styleName: Style["name"] }) {
|
||||
{isCopied ? <Check /> : <Terminal />}
|
||||
<span>npx shadcn add {item.name}</span>
|
||||
</Button>
|
||||
<Separator orientation="vertical" className="mx-1 !h-4" />
|
||||
<Separator orientation="vertical" className="mx-1 h-4!" />
|
||||
<OpenInV0Button name={item.name} />
|
||||
</div>
|
||||
</div>
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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-[15%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -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'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>
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
125
apps/v4/components/language-selector.tsx
Normal file
125
apps/v4/components/language-selector.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -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}
|
||||
|
||||
@@ -29,7 +29,12 @@ export function ModeSwitcher() {
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if ((e.key === "d" || e.key === "D") && !e.metaKey && !e.ctrlKey) {
|
||||
if (
|
||||
(e.key === "d" || e.key === "D") &&
|
||||
!e.metaKey &&
|
||||
!e.ctrlKey &&
|
||||
!e.altKey
|
||||
) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
@@ -96,7 +101,7 @@ export function DarkModeScript() {
|
||||
(function() {
|
||||
// Forward D key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'd' || e.key === 'D') && !e.metaKey && !e.ctrlKey) {
|
||||
if ((e.key === 'd' || e.key === 'D') && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
"components-json",
|
||||
"theming",
|
||||
"[Dark Mode](/docs/dark-mode)",
|
||||
"[RTL](/docs/rtl)",
|
||||
"[CLI](/docs/cli)",
|
||||
"monorepo",
|
||||
"typography",
|
||||
|
||||
@@ -47,7 +47,7 @@ Build resizable panel groups and layouts with this `<Resizable />` component.
|
||||
|
||||
### Sonner
|
||||
|
||||
Another one by [emilkowalski](https://twitter.com/emilkowalski). The last toast component you'll ever need. Sonner is now availabe in shadcn/ui.
|
||||
Another one by [emilkowalski](https://twitter.com/emilkowalski). The last toast component you'll ever need. Sonner is now available in shadcn/ui.
|
||||
|
||||
<ComponentPreview name="sonner-demo" />
|
||||
|
||||
|
||||
@@ -6,9 +6,7 @@ date: 2025-06-06
|
||||
|
||||
We've upgraded the `Calendar` component to the latest version of [React DayPicker](https://daypicker.dev).
|
||||
|
||||
This is a major upgrade and includes a lot of new features and improvements. We've also built a collection of 30+ calendar blocks that you can use to build your own calendar components.
|
||||
|
||||
See all calendar blocks in the [Blocks Library](/blocks/calendar) page.
|
||||
This is a major upgrade and includes a lot of new features and improvements.
|
||||
|
||||
<Image src="/images/calendar-2.png" alt="Calendar" width={676} height={895} />
|
||||
|
||||
|
||||
82
apps/v4/content/docs/changelog/2026-01-rtl.mdx
Normal file
82
apps/v4/content/docs/changelog/2026-01-rtl.mdx
Normal 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.
|
||||
|
||||
[](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)
|
||||
18
apps/v4/content/docs/changelog/2026-02-blocks.mdx
Normal file
18
apps/v4/content/docs/changelog/2026-02-blocks.mdx
Normal file
@@ -0,0 +1,18 @@
|
||||
---
|
||||
title: February 2026 - Blocks for Radix and Base UI
|
||||
description: All blocks are now available for both Radix and Base UI.
|
||||
date: 2026-02-06
|
||||
---
|
||||
|
||||
All [blocks](/blocks) are now available for both Radix and Base UI.
|
||||
|
||||
- **All blocks for both libraries** - Every block, including login, signup, sidebar and dashboard blocks, is now available in both Radix and Base UI variants.
|
||||
- **Same CLI workflow** - Run `npx shadcn add` and the CLI will pull the correct block variant based on your project configuration.
|
||||
|
||||
If you've already set up your project with `npx shadcn create`, blocks will automatically use your chosen library. No additional configuration needed.
|
||||
|
||||
```bash
|
||||
npx shadcn@latest add login-01
|
||||
```
|
||||
|
||||
Browse the full collection on the [blocks](/blocks) page.
|
||||
38
apps/v4/content/docs/changelog/2026-02-radix-ui.mdx
Normal file
38
apps/v4/content/docs/changelog/2026-02-radix-ui.mdx
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -137,12 +137,18 @@ To create a button group, use the `ButtonGroup` component. See the [Button Group
|
||||
|
||||
### Link
|
||||
|
||||
You can use the `render` prop on `<Button />` to make another component look like a button. Here's an example of a link that looks like a button.
|
||||
You can use the `buttonVariants` helper to make a link look like a button.
|
||||
|
||||
Remember to set the `nativeButton` prop to `false` if you're returning an element that is not a button.
|
||||
**Do not use `<Button render={<a />} nativeButton={false} />` for links.** The Base UI `Button` component always applies `role="button"`, which overrides the semantic link role on `<a>` elements. Use `buttonVariants` with a plain `<a>` tag instead.
|
||||
|
||||
<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
|
||||
|
||||
@@ -13,12 +13,6 @@ links:
|
||||
previewClassName="h-96"
|
||||
/>
|
||||
|
||||
## Blocks
|
||||
|
||||
We have built a collection of 30+ calendar blocks that you can use to build your own calendar components.
|
||||
|
||||
See all calendar blocks in the [Blocks Library](/blocks/calendar) page.
|
||||
|
||||
## Installation
|
||||
|
||||
<CodeTabs>
|
||||
@@ -237,270 +231,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} />
|
||||
```
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user