mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-15 11:51:34 +00:00
Compare commits
186 Commits
shadcn/fro
...
shadcn@3.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad25490cf9 | ||
|
|
e94d3d80fa | ||
|
|
0e6b6d90bc | ||
|
|
0da9826821 | ||
|
|
2ef58bd75d | ||
|
|
cac794208e | ||
|
|
a22aec8694 | ||
|
|
6a75b60b4f | ||
|
|
c494adbd87 | ||
|
|
3aa0f13869 | ||
|
|
e1af950724 | ||
|
|
e91388a010 | ||
|
|
8648ddb528 | ||
|
|
feff5b6a57 | ||
|
|
07f7147ff3 | ||
|
|
0e8a006adc | ||
|
|
d2f91d6f1e | ||
|
|
e53bc92f41 | ||
|
|
597a8db2d9 | ||
|
|
0b0f639cd0 | ||
|
|
6b4ba6bca1 | ||
|
|
3cdd67b5b4 | ||
|
|
2b03bc7a53 | ||
|
|
f6447b8936 | ||
|
|
4069c33671 | ||
|
|
f123057ae5 | ||
|
|
65c6c8146d | ||
|
|
bbb59c9fe1 | ||
|
|
fb56f6571a | ||
|
|
082af1f82c | ||
|
|
ae95fbd1be | ||
|
|
625bd97d8b | ||
|
|
603fce7cd3 | ||
|
|
c759f460d5 | ||
|
|
e1c00667f7 | ||
|
|
46631fc4d4 | ||
|
|
f235a5d951 | ||
|
|
b0b711f181 | ||
|
|
f1b7102583 | ||
|
|
f076420e68 | ||
|
|
4ce0a7eaa1 | ||
|
|
270b730c21 | ||
|
|
14a6cc5999 | ||
|
|
0067873f60 | ||
|
|
fc16e1461f | ||
|
|
8f01916bb2 | ||
|
|
ead138b4cd | ||
|
|
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 | ||
|
|
6d467d2e1d | ||
|
|
893cddd2dc | ||
|
|
1781186def | ||
|
|
89b9a76368 | ||
|
|
6529256e98 | ||
|
|
0266253841 | ||
|
|
4a39de5c56 | ||
|
|
e5fda2c139 | ||
|
|
d53f7489ce | ||
|
|
dfe784b44a | ||
|
|
40b9de46e9 | ||
|
|
6d97ab0b9b | ||
|
|
d06e84a007 | ||
|
|
a29185c9cf | ||
|
|
84c801ac67 | ||
|
|
3dbe9e6a3e | ||
|
|
267d45ac7a | ||
|
|
caadc3d7e8 | ||
|
|
a4ee54836e | ||
|
|
7b5c919eae | ||
|
|
f1cacdc051 | ||
|
|
8cb8fb66b3 | ||
|
|
ef01cd4315 | ||
|
|
6cb2a1fd65 | ||
|
|
ee88d296f4 | ||
|
|
598f17812d | ||
|
|
0ae734bdb2 | ||
|
|
18bd8f07cb | ||
|
|
5fc9ced0fd | ||
|
|
b5dff005f6 | ||
|
|
c5c08bb773 | ||
|
|
5998e59839 | ||
|
|
4b7e38ab42 | ||
|
|
e2ba2d241e | ||
|
|
13e2a6c598 | ||
|
|
47c47eaed2 | ||
|
|
9317a93152 |
@@ -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" })
|
||||
|
||||
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
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@@ -15,13 +13,11 @@ import {
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/examples/radix/ui/radio-group"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
@@ -97,7 +93,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-8 !w-14 font-mono"
|
||||
className="h-7 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -27,7 +15,18 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
@@ -57,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 />
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { ButtonGroup } from "@/registry/new-york-v4/ui/button-group"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
@@ -22,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>
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
} from "@/examples/radix/ui/avatar"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -13,14 +12,15 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty className="flex-none border">
|
||||
<Empty className="flex-none border py-10">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<AvatarGroup className="grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
@@ -39,7 +39,7 @@ export function EmptyAvatarGroup() {
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
</AvatarGroup>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,20 +8,21 @@ import {
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
} from "@/examples/radix/ui/select"
|
||||
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>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Card, CardContent } from "@/examples/radix/ui/card"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
} from "@/examples/radix/ui/field"
|
||||
|
||||
const options = [
|
||||
{
|
||||
@@ -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}
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
|
||||
import { Slider } from "@/examples/radix/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FieldSeparator } from "@/registry/new-york-v4/ui/field"
|
||||
import { FieldSeparator } from "@/examples/radix/ui/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Label } from "@/examples/radix/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
@@ -14,13 +11,15 @@ import {
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
@@ -67,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>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
@@ -8,7 +6,8 @@ import {
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
} from "@/examples/radix/ui/item"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -26,7 +10,7 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
} from "@/examples/radix/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
@@ -39,25 +23,36 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
@@ -190,17 +185,17 @@ export function NotionPromptForm() {
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form className="[--radius:1.2rem]">
|
||||
<form>
|
||||
<Field>
|
||||
<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}
|
||||
@@ -214,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>
|
||||
@@ -222,7 +217,7 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
@@ -240,6 +235,7 @@ export function NotionPromptForm() {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
@@ -306,12 +302,8 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-42">
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
@@ -346,11 +338,7 @@ export function NotionPromptForm() {
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="end"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuContent side="top" align="end" className="w-72">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
|
||||
@@ -64,7 +64,7 @@ export default function ChartsLayout({
|
||||
<PageNav id="charts">
|
||||
<ChartsNav />
|
||||
</PageNav>
|
||||
<div className="container-wrapper section-soft flex-1">
|
||||
<div className="container-wrapper flex-1">
|
||||
<div className="container pb-6">
|
||||
<section className="theme-container">{children}</section>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { mdxComponents } from "@/mdx-components"
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconArrowUpRight,
|
||||
} from "@tabler/icons-react"
|
||||
import fm from "front-matter"
|
||||
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
|
||||
import { findNeighbour } from "fumadocs-core/page-tree"
|
||||
import z from "zod"
|
||||
|
||||
import { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { DocsBaseSwitcher } from "@/components/docs-base-switcher"
|
||||
import { DocsCopyPage } from "@/components/docs-copy-page"
|
||||
import { DocsTableOfContents } from "@/components/docs-toc"
|
||||
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export const revalidate = false
|
||||
@@ -85,127 +79,116 @@ export default async function Page(props: {
|
||||
|
||||
const doc = page.data
|
||||
const MDX = doc.body
|
||||
const neighbours = findNeighbour(source.pageTree, page.url)
|
||||
|
||||
const isChangelog = params.slug?.[0] === "changelog"
|
||||
const neighbours = isChangelog
|
||||
? { previous: null, next: null }
|
||||
: findNeighbour(source.pageTree, page.url)
|
||||
const raw = await page.data.getText("raw")
|
||||
const { attributes } = fm(raw)
|
||||
const { links } = z
|
||||
.object({
|
||||
links: z
|
||||
.object({
|
||||
doc: z.string().optional(),
|
||||
api: z.string().optional(),
|
||||
})
|
||||
.optional(),
|
||||
})
|
||||
.parse(attributes)
|
||||
|
||||
return (
|
||||
<div className="flex items-stretch text-[1.05rem] sm:text-[15px] xl:w-full">
|
||||
<div
|
||||
data-slot="docs"
|
||||
className="flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
<div className="mx-auto flex w-full max-w-2xl min-w-0 flex-1 flex-col gap-8 px-4 py-6 text-neutral-800 md:px-0 lg:py-8 dark:text-neutral-300">
|
||||
<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-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
|
||||
<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:pt-1.5 sm:backdrop-blur-none">
|
||||
<DocsCopyPage page={raw} url={absoluteUrl(page.url)} />
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target ml-auto size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
<IconArrowLeft />
|
||||
<span className="sr-only">Previous</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{neighbours.next && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.next.url}>
|
||||
<span className="sr-only">Next</span>
|
||||
<IconArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<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
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
<IconArrowLeft />
|
||||
<span className="sr-only">Previous</span>
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{neighbours.next && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
className="extend-touch-target size-8 shadow-none md:size-7"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.next.url}>
|
||||
<span className="sr-only">Next</span>
|
||||
<IconArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{doc.description && (
|
||||
<p className="text-muted-foreground text-[1.05rem] text-balance sm:text-base">
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
{doc.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{links ? (
|
||||
<div className="flex items-center gap-2 pt-4">
|
||||
{links?.doc && (
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<a href={links.doc} target="_blank" rel="noreferrer">
|
||||
Docs <IconArrowUpRight />
|
||||
</a>
|
||||
</Badge>
|
||||
)}
|
||||
{links?.api && (
|
||||
<Badge asChild variant="secondary" className="rounded-full">
|
||||
<a href={links.api} target="_blank" rel="noreferrer">
|
||||
API Reference <IconArrowUpRight />
|
||||
</a>
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="w-full flex-1 *:data-[slot=alert]:first:mt-0">
|
||||
<div className="w-full flex-1 pb-16 *:data-[slot=alert]:first:mt-0 sm:pb-0">
|
||||
{params.slug &&
|
||||
params.slug[0] === "components" &&
|
||||
params.slug[1] &&
|
||||
params.slug[2] && (
|
||||
<DocsBaseSwitcher
|
||||
base={params.slug[1]}
|
||||
component={params.slug[2]}
|
||||
className="mb-4"
|
||||
/>
|
||||
)}
|
||||
<MDX components={mdxComponents} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto hidden h-16 w-full max-w-2xl items-center gap-2 px-4 sm:flex md:px-0">
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
asChild
|
||||
className="shadow-none"
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
<IconArrowLeft /> {neighbours.previous.name}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{neighbours.next && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.next.url}>
|
||||
{neighbours.next.name} <IconArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
<div className="hidden h-16 w-full items-center gap-2 px-4 sm:flex sm:px-0">
|
||||
{neighbours.previous && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
asChild
|
||||
className="shadow-none"
|
||||
>
|
||||
<Link href={neighbours.previous.url}>
|
||||
<IconArrowLeft /> {neighbours.previous.name}
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
{neighbours.next && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="ml-auto shadow-none"
|
||||
asChild
|
||||
>
|
||||
<Link href={neighbours.next.url}>
|
||||
{neighbours.next.name} <IconArrowRight />
|
||||
</Link>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="sticky top-[calc(var(--header-height)+1px)] z-30 ml-auto hidden h-[calc(100svh-var(--footer-height)+2rem)] w-72 flex-col gap-4 overflow-hidden overscroll-none pb-8 xl:flex">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
<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 overflow-y-auto px-8">
|
||||
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
|
||||
<DocsTableOfContents toc={doc.toc} />
|
||||
<div className="h-12" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex flex-1 flex-col gap-12 px-6">
|
||||
<div className="hidden flex-1 flex-col gap-6 px-6 xl:flex">
|
||||
<OpenInV0Cta />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
144
apps/v4/app/(app)/docs/changelog/page.tsx
Normal file
144
apps/v4/app/(app)/docs/changelog/page.tsx
Normal file
@@ -0,0 +1,144 @@
|
||||
import Link from "next/link"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { mdxComponents } from "@/mdx-components"
|
||||
import { IconRss } from "@tabler/icons-react"
|
||||
|
||||
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
|
||||
export function generateMetadata() {
|
||||
return {
|
||||
title: "Changelog",
|
||||
description: "Latest updates and announcements.",
|
||||
openGraph: {
|
||||
title: "Changelog",
|
||||
description: "Latest updates and announcements.",
|
||||
type: "article",
|
||||
url: absoluteUrl("/docs/changelog"),
|
||||
images: [
|
||||
{
|
||||
url: `/og?title=${encodeURIComponent(
|
||||
"Changelog"
|
||||
)}&description=${encodeURIComponent(
|
||||
"Latest updates and announcements."
|
||||
)}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default function ChangelogPage() {
|
||||
const pages = getChangelogPages()
|
||||
const latestPages = pages.slice(0, 5)
|
||||
const olderPages = pages.slice(5)
|
||||
|
||||
return (
|
||||
<div
|
||||
data-slot="docs"
|
||||
className="flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full"
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<div className="h-(--top-spacing) shrink-0" />
|
||||
<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 items-center justify-between">
|
||||
<h1 className="scroll-m-24 text-4xl font-semibold tracking-tight sm:text-3xl">
|
||||
Changelog
|
||||
</h1>
|
||||
<Button variant="secondary" size="sm" asChild>
|
||||
<a href="/rss.xml" target="_blank" rel="noopener noreferrer">
|
||||
<IconRss />
|
||||
RSS
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
Latest updates and announcements.
|
||||
</p>
|
||||
</div>
|
||||
<div className="w-full flex-1 pb-16 sm:pb-0">
|
||||
{latestPages.map((page) => {
|
||||
const data = page.data as ChangelogPageData
|
||||
const MDX = page.data.body
|
||||
|
||||
return (
|
||||
<article key={page.url} className="mb-12 border-b pb-12">
|
||||
<h2 className="font-heading text-xl font-semibold tracking-tight">
|
||||
{data.title}
|
||||
</h2>
|
||||
<div className="prose-changelog mt-6 *:first:mt-0">
|
||||
<MDX components={mdxComponents} />
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
})}
|
||||
{olderPages.length > 0 && (
|
||||
<div id="more-updates" className="mb-24 scroll-mt-24">
|
||||
<h2 className="font-heading mb-6 text-xl font-semibold tracking-tight">
|
||||
More Updates
|
||||
</h2>
|
||||
<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 (
|
||||
<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>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</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="h-(--top-spacing) shrink-0"></div>
|
||||
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
|
||||
<div className="flex flex-col gap-2 p-4 pt-0 text-sm">
|
||||
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
|
||||
On This Page
|
||||
</p>
|
||||
{latestPages.map((page) => {
|
||||
const data = page.data as ChangelogPageData
|
||||
return (
|
||||
<Link
|
||||
key={page.url}
|
||||
href={page.url}
|
||||
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
|
||||
>
|
||||
{data.title}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
{olderPages.length > 0 && (
|
||||
<a
|
||||
href="#more-updates"
|
||||
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
|
||||
>
|
||||
More Updates
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden flex-1 flex-col gap-6 px-6 xl:flex">
|
||||
<OpenInV0Cta />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -9,7 +9,14 @@ export default function DocsLayout({
|
||||
}) {
|
||||
return (
|
||||
<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 [--sidebar-width:220px] [--top-spacing:0] lg:grid lg:grid-cols-[var(--sidebar-width)_minmax(0,1fr)] lg:[--sidebar-width:240px] lg:[--top-spacing:calc(var(--spacing)*4)]">
|
||||
<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": "calc(var(--spacing) * 72)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<DocsSidebar tree={source.pageTree} />
|
||||
<div className="h-full w-full">{children}</div>
|
||||
</SidebarProvider>
|
||||
|
||||
@@ -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>({
|
||||
|
||||
@@ -3,10 +3,23 @@ import { NextResponse, type NextRequest } from "next/server"
|
||||
|
||||
import { processMdxForLLMs } from "@/lib/llm"
|
||||
import { source } from "@/lib/source"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
import { getActiveStyle, type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
export const revalidate = false
|
||||
|
||||
function getStyleFromSlug(slug: string[] | undefined, fallbackStyle: string) {
|
||||
// Detect base from URL: /docs/components/base/... or /docs/components/radix/...
|
||||
if (slug && slug[0] === "components" && slug[1]) {
|
||||
if (slug[1] === "base") {
|
||||
return "base-nova"
|
||||
}
|
||||
if (slug[1] === "radix") {
|
||||
return "new-york-v4"
|
||||
}
|
||||
}
|
||||
return fallbackStyle
|
||||
}
|
||||
|
||||
export async function GET(
|
||||
_req: NextRequest,
|
||||
{ params }: { params: Promise<{ slug?: string[] }> }
|
||||
@@ -19,9 +32,11 @@ export async function GET(
|
||||
notFound()
|
||||
}
|
||||
|
||||
const effectiveStyle = getStyleFromSlug(slug, activeStyle.name)
|
||||
|
||||
const processedContent = processMdxForLLMs(
|
||||
await page.data.getText("raw"),
|
||||
activeStyle.name
|
||||
effectiveStyle as Style["name"]
|
||||
)
|
||||
|
||||
return new NextResponse(processedContent, {
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -99,7 +99,7 @@ export function ItemPicker({
|
||||
variant="outline"
|
||||
aria-label="Select item"
|
||||
size="sm"
|
||||
className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-md"
|
||||
className="data-popup-open:bg-muted dark:data-popup-open:bg-muted/50 bg-muted/50 sm:bg-background md:dark:bg-background border-foreground/10 dark:bg-muted/50 h-[calc(--spacing(13.5))] flex-1 touch-manipulation justify-between gap-2 rounded-xl pr-4! pl-2.5 text-left shadow-none select-none *:data-[slot=combobox-trigger-icon]:hidden sm:h-8 sm:max-w-56 sm:rounded-lg sm:pr-2! xl:max-w-64"
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -123,9 +123,9 @@ export function ItemPicker({
|
||||
<HugeiconsIcon icon={Search01Icon} />
|
||||
</ComboboxTrigger>
|
||||
<ComboboxContent
|
||||
className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0"
|
||||
className="ring-foreground/10 min-w-[calc(var(--available-width)---spacing(4))] translate-x-2 animate-none rounded-xl border-0 ring-1 data-open:animate-none sm:min-w-[calc(var(--anchor-width)+--spacing(7))] sm:translate-x-0 xl:w-96"
|
||||
side="bottom"
|
||||
align="center"
|
||||
align="end"
|
||||
>
|
||||
<ComboboxInput
|
||||
showTrigger={false}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -56,7 +56,7 @@ export function ShareButton() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="rounded-lg shadow-none"
|
||||
className="rounded-lg shadow-none lg:w-8 xl:w-fit"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
@@ -64,7 +64,7 @@ export function ShareButton() {
|
||||
) : (
|
||||
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
|
||||
)}
|
||||
Share
|
||||
<span className="lg:hidden xl:block">Share</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy Link</TooltipContent>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
ComputerTerminal01Icon,
|
||||
Copy01Icon,
|
||||
@@ -23,6 +24,8 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
@@ -31,6 +34,7 @@ import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
@@ -71,14 +75,15 @@ export function ToolbarControls() {
|
||||
const packageManager = config.packageManager || "pnpm"
|
||||
|
||||
const commands = React.useMemo(() => {
|
||||
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"
|
||||
const url = `${origin}/init?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&template=${params.template}`
|
||||
const origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"
|
||||
const url = `${origin}/init?base=${params.base}&style=${params.style}&baseColor=${params.baseColor}&theme=${params.theme}&iconLibrary=${params.iconLibrary}&font=${params.font}&menuAccent=${params.menuAccent}&menuColor=${params.menuColor}&radius=${params.radius}&template=${params.template}&rtl=${params.rtl}`
|
||||
const templateFlag = params.template ? ` --template ${params.template}` : ""
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
return {
|
||||
pnpm: `pnpm dlx shadcn@latest create --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest create --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest create --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest create --preset "${url}"${templateFlag}`,
|
||||
pnpm: `pnpm dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
npm: `npx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
yarn: `yarn dlx shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
bun: `bunx --bun shadcn@latest create${rtlFlag} --preset "${url}"${templateFlag}`,
|
||||
}
|
||||
}, [
|
||||
params.base,
|
||||
@@ -91,6 +96,7 @@ export function ToolbarControls() {
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
params.template,
|
||||
params.rtl,
|
||||
])
|
||||
|
||||
const command = commands[packageManager]
|
||||
@@ -164,7 +170,7 @@ export function ToolbarControls() {
|
||||
{selectedTemplate?.title} + shadcn/ui project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup>
|
||||
<FieldGroup className="gap-3">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="template" className="sr-only">
|
||||
Template
|
||||
@@ -183,7 +189,7 @@ export function ToolbarControls() {
|
||||
<FieldLabel
|
||||
key={template.value}
|
||||
htmlFor={template.value}
|
||||
className="rounded-lg!"
|
||||
className="has-data-[state=checked]:border-primary/10 rounded-lg!"
|
||||
>
|
||||
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
|
||||
<RadioGroupItem
|
||||
@@ -205,59 +211,81 @@ export function ToolbarControls() {
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
</FieldGroup>
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig({
|
||||
...config,
|
||||
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
|
||||
})
|
||||
}}
|
||||
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
|
||||
>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
|
||||
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
|
||||
<TabsTrigger value="npm">npm</TabsTrigger>
|
||||
<TabsTrigger value="yarn">yarn</TabsTrigger>
|
||||
<TabsTrigger value="bun">bun</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto size-7 rounded-lg"
|
||||
onClick={handleCopyFromTabs}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{hasCopied ? "Copied!" : "Copy command"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
<FieldLabel className="has-data-[state=checked]:border-primary/10 rounded-lg!">
|
||||
<Field orientation="horizontal">
|
||||
<FieldContent className="gap-1">
|
||||
<FieldTitle>Enable RTL</FieldTitle>
|
||||
<FieldDescription>
|
||||
<a
|
||||
href={`/docs/rtl/${params.template}`}
|
||||
className="text-foreground underline"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
View the RTL setup guide for {selectedTemplate?.title}.
|
||||
</a>
|
||||
</FieldDescription>
|
||||
</FieldContent>
|
||||
<Switch
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(rtl) => setParams({ rtl })}
|
||||
className="shadow-none"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig({
|
||||
...config,
|
||||
packageManager: value as "pnpm" | "npm" | "yarn" | "bun",
|
||||
})
|
||||
}}
|
||||
className="bg-surface min-w-0 gap-0 overflow-hidden rounded-lg border"
|
||||
>
|
||||
<div className="flex items-center gap-2 p-2">
|
||||
<TabsList className="*:data-[slot=tabs-trigger]:data-[state=active]:border-input h-auto rounded-none bg-transparent p-0 font-mono group-data-[orientation=horizontal]/tabs:h-8 *:data-[slot=tabs-trigger]:h-7 *:data-[slot=tabs-trigger]:border *:data-[slot=tabs-trigger]:border-transparent *:data-[slot=tabs-trigger]:pt-0.5 *:data-[slot=tabs-trigger]:shadow-none!">
|
||||
<TabsTrigger value="pnpm">pnpm</TabsTrigger>
|
||||
<TabsTrigger value="npm">npm</TabsTrigger>
|
||||
<TabsTrigger value="yarn">yarn</TabsTrigger>
|
||||
<TabsTrigger value="bun">bun</TabsTrigger>
|
||||
</TabsList>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto size-7 rounded-lg"
|
||||
onClick={handleCopyFromTabs}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} className="size-4" />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} className="size-4" />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{hasCopied ? "Copied!" : "Copy command"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="bg-surface border-border/50 text-surface-foreground relative overflow-hidden border-t px-3 py-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
</FieldGroup>
|
||||
<DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col">
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function V0Button({ className }: { className?: string }) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isMobile = useIsMobile()
|
||||
const isMounted = useMounted()
|
||||
|
||||
@@ -32,7 +32,7 @@ export function V0Button({ className }: { className?: string }) {
|
||||
size="sm"
|
||||
variant={isMobile ? "default" : "outline"}
|
||||
className={cn(
|
||||
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px]",
|
||||
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px] lg:w-8 xl:w-24",
|
||||
className
|
||||
)}
|
||||
asChild
|
||||
@@ -41,7 +41,8 @@ export function V0Button({ className }: { className?: string }) {
|
||||
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
|
||||
target="_blank"
|
||||
>
|
||||
Open in <Icons.v0 className="size-5" />
|
||||
<span className="lg:hidden xl:block">Open in</span>
|
||||
<Icons.v0 className="size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -4,7 +4,11 @@ import { ArrowLeftIcon } from "lucide-react"
|
||||
import type { SearchParams } from "nuqs/server"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { source } from "@/lib/source"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { Icons } from "@/components/icons"
|
||||
import { MainNav } from "@/components/main-nav"
|
||||
import { MobileNav } from "@/components/mobile-nav"
|
||||
import { ModeSwitcher } from "@/components/mode-switcher"
|
||||
import { SiteConfig } from "@/components/site-config"
|
||||
import { BASES } from "@/registry/config"
|
||||
@@ -64,6 +68,7 @@ export default async function CreatePage({
|
||||
const params = await loadDesignSystemSearchParams(searchParams)
|
||||
const base = BASES.find((b) => b.name === params.base) ?? BASES[0]
|
||||
|
||||
const pageTree = source.pageTree
|
||||
const items = await getItemsForBase(base.name)
|
||||
|
||||
const filteredItems = items
|
||||
@@ -73,6 +78,7 @@ export default async function CreatePage({
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
}))
|
||||
.filter((item) => !/\d+$/.test(item.name))
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -81,39 +87,35 @@ 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="flex items-center xl:w-1/3">
|
||||
<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}
|
||||
className="flex lg:hidden"
|
||||
/>
|
||||
<Button
|
||||
asChild
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-lg shadow-none"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
>
|
||||
<Link href="/">
|
||||
<ArrowLeftIcon />
|
||||
Back
|
||||
<Icons.logo className="size-5" />
|
||||
<span className="sr-only">{siteConfig.name}</span>
|
||||
</Link>
|
||||
</Button>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mx-2 hidden sm:mx-4 lg:flex"
|
||||
/>
|
||||
<div className="text-muted-foreground hidden text-sm font-medium lg:flex">
|
||||
New Project
|
||||
</div>
|
||||
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
|
||||
</div>
|
||||
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center gap-2 px-4.5 pb-4 sm:static sm:justify-end sm:p-0 lg:ml-0 xl:justify-center">
|
||||
<div className="fixed inset-x-0 bottom-0 ml-auto flex flex-1 items-center justify-end gap-2 px-4.5 pb-4 sm:static sm:p-0 lg:ml-0">
|
||||
<ItemPicker items={filteredItems} />
|
||||
<div className="items-center gap-0 sm:hidden">
|
||||
<RandomButton />
|
||||
<ResetButton />
|
||||
</div>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 hidden sm:flex xl:hidden"
|
||||
/>
|
||||
<Separator orientation="vertical" className="mr-2 flex" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end xl:ml-auto xl:w-1/3">
|
||||
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end">
|
||||
<SiteConfig className="3xl:flex hidden" />
|
||||
<Separator orientation="vertical" className="3xl:flex hidden" />
|
||||
<ModeSwitcher />
|
||||
|
||||
@@ -45,18 +45,6 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const designSystemConfig = parseResult.data
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
const validateResult = registryItemSchema.safeParse(registryBase)
|
||||
|
||||
if (!validateResult.success) {
|
||||
return NextResponse.json(
|
||||
{
|
||||
error: "Invalid registry base item",
|
||||
details: validateResult.error.format(),
|
||||
},
|
||||
{ status: 500 }
|
||||
)
|
||||
}
|
||||
|
||||
track("create_open_in_v0", designSystemConfig)
|
||||
|
||||
@@ -75,28 +63,23 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
|
||||
const files: z.infer<typeof registryItemFileSchema>[] = []
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
|
||||
// Build globals.css file.
|
||||
files.push(buildGlobalsCss(designSystemConfig))
|
||||
|
||||
// Build layout.tsx file.
|
||||
files.push(buildLayoutFile(designSystemConfig))
|
||||
|
||||
// Build component files.
|
||||
const componentFiles = await buildComponentFiles(designSystemConfig)
|
||||
files.push(...componentFiles)
|
||||
// Build all files in parallel.
|
||||
const [globalsCss, layoutFile, componentFiles] = await Promise.all([
|
||||
buildGlobalsCss(registryBase),
|
||||
buildLayoutFile(designSystemConfig),
|
||||
buildComponentFiles(designSystemConfig),
|
||||
])
|
||||
|
||||
return registryItemSchema.parse({
|
||||
name: designSystemConfig.item ?? "Item",
|
||||
type: "registry:item",
|
||||
files,
|
||||
files: [globalsCss, layoutFile, ...componentFiles],
|
||||
})
|
||||
}
|
||||
|
||||
function buildGlobalsCss(designSystemConfig: DesignSystemConfig) {
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
|
||||
function buildGlobalsCss(registryBase: RegistryItem) {
|
||||
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
@@ -19,6 +19,7 @@ export async function GET(request: NextRequest) {
|
||||
menuColor: searchParams.get("menuColor"),
|
||||
radius: searchParams.get("radius"),
|
||||
template: searchParams.get("template"),
|
||||
rtl: searchParams.get("rtl") === "true",
|
||||
})
|
||||
|
||||
if (!result.success) {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -66,6 +66,7 @@ const designSystemSearchParams = {
|
||||
"start",
|
||||
"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)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TrashIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -6,6 +8,7 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/alert-dialog"
|
||||
@@ -13,23 +16,66 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function AlertDialogDemo() {
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Show Dialog</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
account and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Continue</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<div className="flex flex-wrap gap-4">
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Default</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete your
|
||||
account and remove your data from our servers.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction>Continue</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">With Media</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent>
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia>
|
||||
<TrashIcon className="size-8" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Delete this item?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone. This will permanently delete the
|
||||
item from your account.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive">Delete</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button variant="outline">Small Size</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent size="sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogMedia>
|
||||
<TrashIcon className="size-8" />
|
||||
</AlertDialogMedia>
|
||||
<AlertDialogTitle>Delete this item?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction variant="destructive">Delete</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,90 +1,174 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarBadge,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarGroupCount,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
|
||||
export function AvatarDemo() {
|
||||
return (
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="size-12">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar className="rounded-lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Sizes. */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<Avatar size="sm">
|
||||
<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>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
{/* Fallback. */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<Avatar size="sm">
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
<Avatar size="lg">
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 hover:space-x-1 *:data-[slot=avatar]:size-12 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale *:data-[slot=avatar]:transition-all *:data-[slot=avatar]:duration-300 *:data-[slot=avatar]:ease-in-out">
|
||||
{/* With badge. */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<Avatar size="sm">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<AvatarBadge />
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<AvatarBadge />
|
||||
</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 size="lg">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<AvatarBadge>
|
||||
<PlusIcon />
|
||||
</AvatarBadge>
|
||||
</Avatar>
|
||||
</div>
|
||||
{/* Avatar group. */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<AvatarGroup>
|
||||
<Avatar size="sm">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="sm">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>ML</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="sm">
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
<AvatarGroup>
|
||||
<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>ML</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
<AvatarGroup>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>ML</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
{/* Avatar group with count. */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<AvatarGroup>
|
||||
<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>ML</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
<AvatarGroupCount>+3</AvatarGroupCount>
|
||||
</AvatarGroup>
|
||||
<AvatarGroup>
|
||||
<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>ML</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
<AvatarGroupCount>
|
||||
<PlusIcon />
|
||||
</AvatarGroupCount>
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,10 @@ export function BadgeDemo() {
|
||||
<Badge variant="secondary">Secondary</Badge>
|
||||
<Badge variant="destructive">Destructive</Badge>
|
||||
<Badge variant="outline">Outline</Badge>
|
||||
<Badge variant="ghost">Ghost</Badge>
|
||||
<Badge variant="link">Link</Badge>
|
||||
</div>
|
||||
<div className="flex w-full flex-wrap gap-2">
|
||||
<Badge variant="outline">
|
||||
<CheckIcon />
|
||||
Badge
|
||||
@@ -55,6 +59,16 @@ export function BadgeDemo() {
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
<Badge asChild variant="ghost">
|
||||
<a href="#">
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
<Badge asChild variant="link">
|
||||
<a href="#">
|
||||
Link <ArrowRightIcon />
|
||||
</a>
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ArrowRightIcon, Loader2Icon, SendIcon } from "lucide-react"
|
||||
import { ArrowRightIcon, Loader2Icon, PlusIcon, SendIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
@@ -6,22 +6,25 @@ export function ButtonDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button>Button</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button variant="outline">
|
||||
<Button size="xs">Extra Small</Button>
|
||||
<Button variant="outline" size="xs">
|
||||
Outline
|
||||
</Button>
|
||||
<Button variant="ghost" size="xs">
|
||||
Ghost
|
||||
</Button>
|
||||
<Button variant="destructive" size="xs">
|
||||
Destructive
|
||||
</Button>
|
||||
<Button variant="secondary" size="xs">
|
||||
Secondary
|
||||
</Button>
|
||||
<Button variant="link" size="xs">
|
||||
Link
|
||||
</Button>
|
||||
<Button variant="outline" size="xs">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button disabled variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button size="sm">Small</Button>
|
||||
@@ -43,10 +46,21 @@ export function ButtonDemo() {
|
||||
<Button variant="outline" size="sm">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
<Button variant="outline" size="sm">
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button>Button</Button>
|
||||
<Button variant="outline">Outline</Button>
|
||||
<Button variant="ghost">Ghost</Button>
|
||||
<Button variant="destructive">Destructive</Button>
|
||||
<Button variant="secondary">Secondary</Button>
|
||||
<Button variant="link">Link</Button>
|
||||
<Button variant="outline">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
<Button variant="outline">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button disabled size="sm" variant="outline">
|
||||
<Button disabled variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
@@ -71,12 +85,19 @@ export function ButtonDemo() {
|
||||
<Button variant="outline" size="lg">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
<Button variant="outline" size="lg">
|
||||
Learn More <ArrowRightIcon />
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button size="icon-xs" variant="outline">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Button disabled size="lg" variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
<Button size="icon-sm" variant="outline">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Button size="icon" variant="outline">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Button size="icon-lg" variant="outline">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,405 +1,183 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
CheckIcon,
|
||||
ChevronDownIcon,
|
||||
ChevronsUpDown,
|
||||
PlusCircleIcon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxCollection,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxLabel,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/registry/new-york-v4/ui/combobox"
|
||||
|
||||
const frameworks = [
|
||||
{
|
||||
value: "next.js",
|
||||
label: "Next.js",
|
||||
},
|
||||
{
|
||||
value: "sveltekit",
|
||||
label: "SvelteKit",
|
||||
},
|
||||
{
|
||||
value: "nuxt.js",
|
||||
label: "Nuxt.js",
|
||||
},
|
||||
{
|
||||
value: "remix",
|
||||
label: "Remix",
|
||||
},
|
||||
{
|
||||
value: "astro",
|
||||
label: "Astro",
|
||||
},
|
||||
]
|
||||
|
||||
type Framework = (typeof frameworks)[number]
|
||||
|
||||
const users = [
|
||||
{
|
||||
id: "1",
|
||||
username: "shadcn",
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
username: "maxleiter",
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
username: "evilrabbit",
|
||||
},
|
||||
"Next.js",
|
||||
"SvelteKit",
|
||||
"Nuxt.js",
|
||||
"Remix",
|
||||
"Astro",
|
||||
] as const
|
||||
|
||||
type User = (typeof users)[number]
|
||||
|
||||
const timezones = [
|
||||
{
|
||||
label: "Americas",
|
||||
timezones: [
|
||||
{ value: "America/New_York", label: "(GMT-5) New York" },
|
||||
{ value: "America/Los_Angeles", label: "(GMT-8) Los Angeles" },
|
||||
{ value: "America/Chicago", label: "(GMT-6) Chicago" },
|
||||
{ value: "America/Toronto", label: "(GMT-5) Toronto" },
|
||||
{ value: "America/Vancouver", label: "(GMT-8) Vancouver" },
|
||||
{ value: "America/Sao_Paulo", label: "(GMT-3) São Paulo" },
|
||||
],
|
||||
value: "Americas",
|
||||
items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-6) Chicago"],
|
||||
},
|
||||
{
|
||||
label: "Europe",
|
||||
timezones: [
|
||||
{ value: "Europe/London", label: "(GMT+0) London" },
|
||||
{ value: "Europe/Paris", label: "(GMT+1) Paris" },
|
||||
{ value: "Europe/Berlin", label: "(GMT+1) Berlin" },
|
||||
{ value: "Europe/Rome", label: "(GMT+1) Rome" },
|
||||
{ value: "Europe/Madrid", label: "(GMT+1) Madrid" },
|
||||
{ value: "Europe/Amsterdam", label: "(GMT+1) Amsterdam" },
|
||||
],
|
||||
value: "Europe",
|
||||
items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin"],
|
||||
},
|
||||
{
|
||||
label: "Asia/Pacific",
|
||||
timezones: [
|
||||
{ value: "Asia/Tokyo", label: "(GMT+9) Tokyo" },
|
||||
{ value: "Asia/Shanghai", label: "(GMT+8) Shanghai" },
|
||||
{ value: "Asia/Singapore", label: "(GMT+8) Singapore" },
|
||||
{ value: "Asia/Dubai", label: "(GMT+4) Dubai" },
|
||||
{ value: "Australia/Sydney", label: "(GMT+11) Sydney" },
|
||||
{ value: "Asia/Seoul", label: "(GMT+9) Seoul" },
|
||||
],
|
||||
value: "Asia/Pacific",
|
||||
items: ["(GMT+9) Tokyo", "(GMT+8) Shanghai", "(GMT+8) Singapore"],
|
||||
},
|
||||
] as const
|
||||
|
||||
type Timezone = (typeof timezones)[number]
|
||||
const countries = [
|
||||
{ code: "", value: "", label: "Select country" },
|
||||
{ code: "us", value: "united-states", label: "United States" },
|
||||
{ code: "ca", value: "canada", label: "Canada" },
|
||||
{ code: "gb", value: "united-kingdom", label: "United Kingdom" },
|
||||
{ code: "de", value: "germany", label: "Germany" },
|
||||
{ code: "fr", value: "france", label: "France" },
|
||||
{ code: "jp", value: "japan", label: "Japan" },
|
||||
]
|
||||
|
||||
export function ComboboxDemo() {
|
||||
return (
|
||||
<div className="flex w-full flex-wrap items-start gap-4">
|
||||
<FrameworkCombobox frameworks={[...frameworks]} />
|
||||
<UserCombobox users={[...users]} selectedUserId={users[0].id} />
|
||||
<TimezoneCombobox
|
||||
timezones={[...timezones]}
|
||||
selectedTimezone={timezones[0].timezones[0]}
|
||||
/>
|
||||
<ComboboxWithCheckbox frameworks={[...frameworks]} />
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
{/* Basic combobox. */}
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<Combobox items={frameworks}>
|
||||
<ComboboxInput placeholder="Select a framework" />
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
{/* With clear button. */}
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<Combobox items={frameworks} defaultValue={frameworks[0]}>
|
||||
<ComboboxInput placeholder="Select a framework" showClear />
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
{/* With groups. */}
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<Combobox items={timezones}>
|
||||
<ComboboxInput placeholder="Select a timezone" />
|
||||
<ComboboxContent>
|
||||
<ComboboxEmpty>No timezones found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(group) => (
|
||||
<ComboboxGroup key={group.value} items={group.items}>
|
||||
<ComboboxLabel>{group.value}</ComboboxLabel>
|
||||
<ComboboxCollection>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxCollection>
|
||||
</ComboboxGroup>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
{/* With trigger button. */}
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<Combobox items={countries} defaultValue={countries[0]}>
|
||||
<ComboboxTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-64 justify-between font-normal"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ComboboxValue />
|
||||
</ComboboxTrigger>
|
||||
<ComboboxContent>
|
||||
<ComboboxInput showTrigger={false} placeholder="Search" />
|
||||
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item.code} value={item}>
|
||||
{item.label}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
{/* Multiple selection with chips. */}
|
||||
<ComboboxMultiple />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function FrameworkCombobox({ frameworks }: { frameworks: Framework[] }) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState("")
|
||||
function ComboboxMultiple() {
|
||||
const anchor = useComboboxAnchor()
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between md:max-w-[200px]"
|
||||
>
|
||||
{value
|
||||
? frameworks.find((framework) => framework.value === value)?.label
|
||||
: "Select framework..."}
|
||||
<ChevronsUpDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-(--radix-popover-trigger-width) p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{frameworks.map((framework) => (
|
||||
<CommandItem
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{framework.label}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
value === framework.value ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function UserCombobox({
|
||||
users,
|
||||
selectedUserId,
|
||||
}: {
|
||||
users: User[]
|
||||
selectedUserId: string
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState(selectedUserId)
|
||||
|
||||
const selectedUser = React.useMemo(
|
||||
() => users.find((user) => user.id === value),
|
||||
[value, users]
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-full justify-between px-2 md:max-w-[200px]"
|
||||
>
|
||||
{selectedUser ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="size-5">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${selectedUser.username}.png`}
|
||||
/>
|
||||
<AvatarFallback>{selectedUser.username[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
{selectedUser.username}
|
||||
</div>
|
||||
) : (
|
||||
"Select user..."
|
||||
)}
|
||||
<ChevronsUpDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-(--radix-popover-trigger-width) p-0">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search user..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No user found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{users.map((user) => (
|
||||
<CommandItem
|
||||
key={user.id}
|
||||
value={user.id}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(currentValue === value ? "" : currentValue)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
<Avatar className="size-5">
|
||||
<AvatarImage
|
||||
src={`https://github.com/${user.username}.png`}
|
||||
/>
|
||||
<AvatarFallback>{user.username[0]}</AvatarFallback>
|
||||
</Avatar>
|
||||
{user.username}
|
||||
<CheckIcon
|
||||
className={cn(
|
||||
"ml-auto",
|
||||
value === user.id ? "opacity-100" : "opacity-0"
|
||||
)}
|
||||
/>
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem>
|
||||
<PlusCircleIcon />
|
||||
Create user
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function TimezoneCombobox({
|
||||
timezones,
|
||||
selectedTimezone,
|
||||
}: {
|
||||
timezones: Timezone[]
|
||||
selectedTimezone: Timezone["timezones"][number]
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState(selectedTimezone.value)
|
||||
|
||||
const selectedGroup = React.useMemo(
|
||||
() =>
|
||||
timezones.find((group) =>
|
||||
group.timezones.find((tz) => tz.value === value)
|
||||
),
|
||||
[value, timezones]
|
||||
)
|
||||
|
||||
const selectedTimezoneLabel = React.useMemo(
|
||||
() => selectedGroup?.timezones.find((tz) => tz.value === value)?.label,
|
||||
[value, selectedGroup]
|
||||
)
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="h-12 w-full justify-between px-2.5 md:max-w-[200px]"
|
||||
>
|
||||
{selectedTimezone ? (
|
||||
<div className="flex flex-col items-start gap-0.5">
|
||||
<span className="text-muted-foreground text-xs font-normal">
|
||||
{selectedGroup?.label}
|
||||
</span>
|
||||
<span>{selectedTimezoneLabel}</span>
|
||||
</div>
|
||||
) : (
|
||||
"Select timezone"
|
||||
)}
|
||||
<ChevronDownIcon className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search timezone..." />
|
||||
<CommandList className="scroll-pb-12">
|
||||
<CommandEmpty>No timezone found.</CommandEmpty>
|
||||
{timezones.map((region) => (
|
||||
<CommandGroup key={region.label} heading={region.label}>
|
||||
{region.timezones.map((timezone) => (
|
||||
<CommandItem
|
||||
key={timezone.value}
|
||||
value={timezone.value}
|
||||
onSelect={(currentValue) => {
|
||||
setValue(
|
||||
currentValue as Timezone["timezones"][number]["value"]
|
||||
)
|
||||
setOpen(false)
|
||||
}}
|
||||
>
|
||||
{timezone.label}
|
||||
<CheckIcon
|
||||
className="ml-auto opacity-0 data-[selected=true]:opacity-100"
|
||||
data-selected={value === timezone.value}
|
||||
/>
|
||||
</CommandItem>
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
<Combobox
|
||||
multiple
|
||||
autoHighlight
|
||||
items={frameworks}
|
||||
defaultValue={[frameworks[0]]}
|
||||
>
|
||||
<ComboboxChips ref={anchor}>
|
||||
<ComboboxValue>
|
||||
{(values) => (
|
||||
<React.Fragment>
|
||||
{values.map((value: string) => (
|
||||
<ComboboxChip key={value}>{value}</ComboboxChip>
|
||||
))}
|
||||
</CommandGroup>
|
||||
))}
|
||||
<CommandSeparator className="sticky bottom-10" />
|
||||
<CommandGroup className="bg-popover sticky bottom-0">
|
||||
<CommandItem>
|
||||
<PlusCircleIcon />
|
||||
Create timezone
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxWithCheckbox({ frameworks }: { frameworks: Framework[] }) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [selectedFrameworks, setSelectedFrameworks] = React.useState<
|
||||
Framework[]
|
||||
>([])
|
||||
|
||||
return (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="w-fit min-w-[280px] justify-between"
|
||||
>
|
||||
{selectedFrameworks.length > 0
|
||||
? selectedFrameworks.map((framework) => framework.label).join(", ")
|
||||
: "Select frameworks (multi-select)..."}
|
||||
<ChevronsUpDown className="text-muted-foreground" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[300px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search framework..." />
|
||||
<CommandList>
|
||||
<CommandEmpty>No framework found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{frameworks.map((framework) => (
|
||||
<CommandItem
|
||||
key={framework.value}
|
||||
value={framework.value}
|
||||
onSelect={(currentValue) => {
|
||||
setSelectedFrameworks(
|
||||
selectedFrameworks.some((f) => f.value === currentValue)
|
||||
? selectedFrameworks.filter(
|
||||
(f) => f.value !== currentValue
|
||||
)
|
||||
: [...selectedFrameworks, framework]
|
||||
)
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="border-input data-[selected=true]:border-primary data-[selected=true]:bg-primary data-[selected=true]:text-primary-foreground pointer-events-none size-4 shrink-0 rounded-[4px] border transition-all select-none *:[svg]:opacity-0 data-[selected=true]:*:[svg]:opacity-100"
|
||||
data-selected={selectedFrameworks.some(
|
||||
(f) => f.value === framework.value
|
||||
)}
|
||||
>
|
||||
<CheckIcon className="size-3.5 text-current" />
|
||||
</div>
|
||||
{framework.label}
|
||||
</CommandItem>
|
||||
))}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<ComboboxChipsInput placeholder="Add framework..." />
|
||||
</React.Fragment>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
</ComboboxChips>
|
||||
<ComboboxContent anchor={anchor}>
|
||||
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||
<ComboboxList>
|
||||
{(item) => (
|
||||
<ComboboxItem key={item} value={item}>
|
||||
{item}
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
</Combobox>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,59 +4,64 @@ import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverDescription,
|
||||
PopoverHeader,
|
||||
PopoverTitle,
|
||||
PopoverTrigger,
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function PopoverDemo() {
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80" align="start">
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-1.5">
|
||||
<h4 className="leading-none font-medium">Dimensions</h4>
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Set the dimensions for the layer.
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<Input
|
||||
id="width"
|
||||
defaultValue="100%"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxWidth">Max. width</Label>
|
||||
<Input
|
||||
id="maxWidth"
|
||||
defaultValue="300px"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="height">Height</Label>
|
||||
<Input
|
||||
id="height"
|
||||
defaultValue="25px"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxHeight">Max. height</Label>
|
||||
<Input
|
||||
id="maxHeight"
|
||||
defaultValue="none"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
<div className="flex gap-4">
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button variant="outline">Open popover</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-80" align="start">
|
||||
<div className="grid gap-4">
|
||||
<PopoverHeader>
|
||||
<PopoverTitle>Dimensions</PopoverTitle>
|
||||
<PopoverDescription>
|
||||
Set the dimensions for the layer.
|
||||
</PopoverDescription>
|
||||
</PopoverHeader>
|
||||
<div className="grid gap-2">
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="width">Width</Label>
|
||||
<Input
|
||||
id="width"
|
||||
defaultValue="100%"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxWidth">Max. width</Label>
|
||||
<Input
|
||||
id="maxWidth"
|
||||
defaultValue="300px"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="height">Height</Label>
|
||||
<Input
|
||||
id="height"
|
||||
defaultValue="25px"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-3 items-center gap-4">
|
||||
<Label htmlFor="maxHeight">Max. height</Label>
|
||||
<Input
|
||||
id="maxHeight"
|
||||
defaultValue="none"
|
||||
className="col-span-2 h-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -47,6 +47,27 @@ export function SheetDemo() {
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">No Close Button</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent showCloseButton={false}>
|
||||
<SheetHeader>
|
||||
<SheetTitle>Custom Close</SheetTitle>
|
||||
<SheetDescription>
|
||||
This sheet has no default close button. Use the footer buttons
|
||||
instead.
|
||||
</SheetDescription>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 px-4" />
|
||||
<SheetFooter>
|
||||
<SheetClose asChild>
|
||||
<Button variant="outline">Cancel</Button>
|
||||
</SheetClose>
|
||||
<Button type="submit">Save</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex gap-2">
|
||||
{SHEET_SIDES.map((side) => (
|
||||
<Sheet key={side}>
|
||||
|
||||
@@ -4,6 +4,17 @@ import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
export function SwitchDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Sizes. */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch id="switch-demo-sm" size="sm" />
|
||||
<Label htmlFor="switch-demo-sm">Small</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch id="switch-demo-default" />
|
||||
<Label htmlFor="switch-demo-default">Default</Label>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch id="switch-demo-airplane-mode" />
|
||||
<Label htmlFor="switch-demo-airplane-mode">Airplane Mode</Label>
|
||||
|
||||
@@ -101,6 +101,45 @@ export function TabsDemo() {
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{/* Line variant. */}
|
||||
<Tabs defaultValue="preview">
|
||||
<TabsList variant="line">
|
||||
<TabsTrigger value="preview">
|
||||
<AppWindowIcon />
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="code">
|
||||
<CodeIcon />
|
||||
Code
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{/* Vertical orientation. */}
|
||||
<Tabs defaultValue="preview" orientation="vertical">
|
||||
<TabsList>
|
||||
<TabsTrigger value="preview">
|
||||
<AppWindowIcon />
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="code">
|
||||
<CodeIcon />
|
||||
Code
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
{/* Vertical orientation with line variant. */}
|
||||
<Tabs defaultValue="preview" orientation="vertical">
|
||||
<TabsList variant="line">
|
||||
<TabsTrigger value="preview">
|
||||
<AppWindowIcon />
|
||||
Preview
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="code">
|
||||
<CodeIcon />
|
||||
Code
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</Tabs>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4,7 +4,11 @@ import { type Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
|
||||
import {
|
||||
getDemoItem,
|
||||
getRegistryComponent,
|
||||
getRegistryItem,
|
||||
} from "@/lib/registry"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
@@ -18,7 +22,12 @@ export const dynamicParams = false
|
||||
|
||||
const getCachedRegistryItem = React.cache(
|
||||
async (name: string, styleName: Style["name"]) => {
|
||||
return await getRegistryItem(name, styleName)
|
||||
// Try registry item first, then fallback to demo item (for examples).
|
||||
const item = await getRegistryItem(name, styleName)
|
||||
if (item) {
|
||||
return item
|
||||
}
|
||||
return await getDemoItem(name, styleName)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -75,9 +84,54 @@ export async function generateMetadata({
|
||||
|
||||
export async function generateStaticParams() {
|
||||
const { Index } = await import("@/registry/__index__")
|
||||
// const { Index: BasesIndex } = await import("@/registry/bases/__index__")
|
||||
const { ExamplesIndex } = await import("@/examples/__index__")
|
||||
const params: Array<{ style: string; name: string }> = []
|
||||
|
||||
for (const style of legacyStyles) {
|
||||
// Check if this is a base-prefixed style (e.g., base-nova, radix-nova).
|
||||
const baseMatch = style.name.match(/^(base|radix)-/)
|
||||
if (baseMatch) {
|
||||
const baseName = baseMatch[1]
|
||||
|
||||
// Add examples from ExamplesIndex.
|
||||
const examples = ExamplesIndex[baseName]
|
||||
if (examples) {
|
||||
for (const exampleName of Object.keys(examples)) {
|
||||
if (exampleName.startsWith("sidebar-")) {
|
||||
params.push({
|
||||
style: style.name,
|
||||
name: exampleName,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// // Add UI components from BasesIndex.
|
||||
// const baseIndex = BasesIndex[baseName]
|
||||
// if (baseIndex) {
|
||||
// for (const itemName in baseIndex) {
|
||||
// const item = baseIndex[itemName]
|
||||
// if (
|
||||
// [
|
||||
// "registry:block",
|
||||
// "registry:component",
|
||||
// "registry:example",
|
||||
// "registry:internal",
|
||||
// ].includes(item.type)
|
||||
// ) {
|
||||
// params.push({
|
||||
// style: style.name,
|
||||
// name: item.name,
|
||||
// })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
// Handle legacy styles (e.g., new-york-v4).
|
||||
if (!Index[style.name]) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -57,6 +59,11 @@ export const metadata: Metadata = {
|
||||
apple: "/apple-touch-icon.png",
|
||||
},
|
||||
manifest: `${siteConfig.url}/site.webmanifest`,
|
||||
alternates: {
|
||||
types: {
|
||||
"application/rss+xml": `${siteConfig.url}/rss.xml`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
export default function RootLayout({
|
||||
@@ -92,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 />
|
||||
|
||||
44
apps/v4/app/rss.xml/route.ts
Normal file
44
apps/v4/app/rss.xml/route.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { NextResponse } from "next/server"
|
||||
|
||||
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
|
||||
import { siteConfig } from "@/lib/config"
|
||||
|
||||
export const revalidate = false
|
||||
|
||||
export async function GET() {
|
||||
const pages = getChangelogPages()
|
||||
|
||||
const items = pages
|
||||
.map((page) => {
|
||||
const data = page.data as ChangelogPageData
|
||||
const date = page.date?.toUTCString() ?? new Date().toUTCString()
|
||||
const link = `${siteConfig.url}/docs/${page.slugs.join("/")}`
|
||||
|
||||
return ` <item>
|
||||
<title><![CDATA[${data.title}]]></title>
|
||||
<link>${link}</link>
|
||||
<guid>${link}</guid>
|
||||
<description><![CDATA[${data.description || ""}]]></description>
|
||||
<pubDate>${date}</pubDate>
|
||||
</item>`
|
||||
})
|
||||
.join("\n")
|
||||
|
||||
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
|
||||
<channel>
|
||||
<title>${siteConfig.name} Changelog</title>
|
||||
<link>${siteConfig.url}</link>
|
||||
<description>${siteConfig.description}</description>
|
||||
<language>en-us</language>
|
||||
<atom:link href="${siteConfig.url}/rss.xml" rel="self" type="application/rss+xml"/>
|
||||
${items}
|
||||
</channel>
|
||||
</rss>`
|
||||
|
||||
return new NextResponse(xml, {
|
||||
headers: {
|
||||
"Content-Type": "application/rss+xml; charset=utf-8",
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -3,12 +3,26 @@ import { ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
|
||||
function BaseUILogo() {
|
||||
return (
|
||||
<svg width="17" height="24" viewBox="0 0 17 24" className="size-3">
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M9.5001 7.01537C9.2245 6.99837 9 7.22385 9 7.49999V23C13.4183 23 17 19.4183 17 15C17 10.7497 13.6854 7.27351 9.5001 7.01537Z"
|
||||
/>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M8 9.8V12V23C3.58172 23 0 19.0601 0 14.2V12V1C4.41828 1 8 4.93989 8 9.8Z"
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function Announcement() {
|
||||
return (
|
||||
<Badge asChild variant="secondary" className="bg-transparent">
|
||||
<Link href="/docs/changelog">
|
||||
<span className="flex size-2 rounded-full bg-blue-500" title="New" />
|
||||
npx shadcn create <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}
|
||||
|
||||
@@ -35,7 +35,7 @@ export function ChartDisplay({
|
||||
<div className="bg-background relative z-10 overflow-hidden rounded-xl">
|
||||
<ChartIframe
|
||||
src={`/view/${style}/${chart.name}`}
|
||||
height={430}
|
||||
height={460}
|
||||
title={chart.name}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function CodeTabs({ children }: React.ComponentProps<typeof Tabs>) {
|
||||
onValueChange={(value) =>
|
||||
setConfig({ ...config, installationType: value as "cli" | "manual" })
|
||||
}
|
||||
className="relative mt-6 w-full"
|
||||
className="relative mt-6 w-full *:data-[slot=tabs-list]:gap-6"
|
||||
>
|
||||
{children}
|
||||
</Tabs>
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { type DialogProps } from "@radix-ui/react-dialog"
|
||||
import { usePathname, useRouter } from "next/navigation"
|
||||
import { IconArrowRight } from "@tabler/icons-react"
|
||||
import { useDocsSearch } from "fumadocs-core/search/client"
|
||||
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"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
|
||||
import { type source } from "@/lib/source"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
@@ -26,13 +27,13 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogOverlay,
|
||||
DialogPortal,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import { Kbd, KbdGroup } 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"
|
||||
|
||||
@@ -42,15 +43,18 @@ export function CommandMenu({
|
||||
blocks,
|
||||
navItems,
|
||||
...props
|
||||
}: DialogProps & {
|
||||
}: React.ComponentProps<typeof Dialog> & {
|
||||
tree: typeof source.pageTree
|
||||
colors: ColorPalette[]
|
||||
blocks?: { name: string; description: string; categories: string[] }[]
|
||||
navItems?: { href: string; label: string }[]
|
||||
}) {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
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)
|
||||
@@ -90,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) {
|
||||
@@ -106,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) {
|
||||
@@ -138,10 +169,175 @@ export function CommandMenu({
|
||||
[setSelectedType, setCopyPayload, packageManager]
|
||||
)
|
||||
|
||||
const runCommand = React.useCallback((command: () => unknown) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
}, [])
|
||||
const runCommand = React.useCallback(
|
||||
(command: () => unknown) => {
|
||||
setOpen(false)
|
||||
command()
|
||||
},
|
||||
[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) => {
|
||||
@@ -195,39 +391,29 @@ 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
|
||||
showCloseButton={false}
|
||||
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"
|
||||
>
|
||||
<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">
|
||||
<DialogHeader className="sr-only">
|
||||
<DialogTitle>Search documentation...</DialogTitle>
|
||||
<DialogDescription>Search for a command to run...</DialogDescription>
|
||||
</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" />
|
||||
@@ -238,151 +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" &&
|
||||
group.children.map((item) => {
|
||||
if (item.type === "page") {
|
||||
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>
|
||||
)
|
||||
}
|
||||
return null
|
||||
})}
|
||||
</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">
|
||||
@@ -468,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
|
||||
@@ -523,3 +578,27 @@ function SearchResults({
|
||||
</CommandGroup>
|
||||
)
|
||||
}
|
||||
|
||||
function DialogContent({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<DialogPortal data-slot="dialog-portal">
|
||||
{/* <DialogOverlay /> */}
|
||||
<DialogPrimitive.Content
|
||||
data-slot="dialog-content"
|
||||
className={cn(
|
||||
"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}
|
||||
>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPortal>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,51 +1,267 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
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,
|
||||
previewClassName,
|
||||
align = "center",
|
||||
hideCode = false,
|
||||
chromeLessOnMobile = false,
|
||||
component,
|
||||
source,
|
||||
sourcePreview,
|
||||
direction = "ltr",
|
||||
styleName,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
previewClassName?: string
|
||||
align?: "center" | "start" | "end"
|
||||
hideCode?: boolean
|
||||
chromeLessOnMobile?: boolean
|
||||
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 rounded-lg 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}
|
||||
className={cn(
|
||||
"preview flex w-full justify-center data-[align=center]:items-center data-[align=end]:items-end data-[align=start]:items-start",
|
||||
chromeLessOnMobile ? "sm:p-10" : "h-[450px] p-10"
|
||||
)}
|
||||
>
|
||||
{component}
|
||||
</div>
|
||||
{!hideCode && (
|
||||
<div
|
||||
data-slot="code"
|
||||
className="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-[400px]"
|
||||
>
|
||||
{source}
|
||||
{direction === "rtl" ? (
|
||||
<LanguageProvider defaultLanguage="ar">
|
||||
<div className="flex h-16 items-center border-b px-4">
|
||||
<RtlLanguageSelector />
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="ml-auto size-7"
|
||||
>
|
||||
<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>
|
||||
<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>
|
||||
}
|
||||
|
||||
@@ -1,45 +1,37 @@
|
||||
import * as React from "react"
|
||||
import Image from "next/image"
|
||||
|
||||
import { getRegistryComponent } from "@/lib/registry"
|
||||
import { ComponentPreviewTabs } from "@/components/component-preview-tabs"
|
||||
import { ComponentSource } from "@/components/component-source"
|
||||
import { Index } from "@/registry/__index__"
|
||||
import { type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
export function ComponentPreview({
|
||||
name,
|
||||
styleName = "new-york-v4",
|
||||
type,
|
||||
className,
|
||||
previewClassName,
|
||||
align = "center",
|
||||
hideCode = false,
|
||||
chromeLessOnMobile = false,
|
||||
styleName = "new-york-v4",
|
||||
direction = "ltr",
|
||||
caption,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
name: string
|
||||
styleName?: Style["name"]
|
||||
styleName?: string
|
||||
align?: "center" | "start" | "end"
|
||||
description?: string
|
||||
hideCode?: boolean
|
||||
type?: "block" | "component" | "example"
|
||||
chromeLessOnMobile?: boolean
|
||||
previewClassName?: string
|
||||
direction?: "ltr" | "rtl"
|
||||
caption?: string
|
||||
}) {
|
||||
const Component = Index[styleName]?.[name]?.component
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<p className="text-muted-foreground mt-6 text-sm">
|
||||
Component{" "}
|
||||
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
||||
{name}
|
||||
</code>{" "}
|
||||
not found in registry.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === "block") {
|
||||
return (
|
||||
<div className="relative aspect-[4/2.5] w-full overflow-hidden rounded-md 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}
|
||||
@@ -59,14 +51,42 @@ 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
|
||||
}
|
||||
|
||||
return (
|
||||
const Component = getRegistryComponent(name, styleName)
|
||||
|
||||
if (!Component) {
|
||||
return (
|
||||
<p className="text-muted-foreground mt-6 text-sm">
|
||||
Component{" "}
|
||||
<code className="bg-muted relative rounded px-[0.3rem] py-[0.2rem] font-mono text-sm">
|
||||
{name}
|
||||
</code>{" "}
|
||||
not found in registry.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
|
||||
const content = (
|
||||
<ComponentPreviewTabs
|
||||
className={className}
|
||||
previewClassName={previewClassName}
|
||||
align={align}
|
||||
hideCode={hideCode}
|
||||
component={<Component />}
|
||||
component={React.createElement(Component)}
|
||||
source={
|
||||
<ComponentSource
|
||||
name={name}
|
||||
@@ -74,8 +94,34 @@ export function ComponentPreview({
|
||||
styleName={styleName}
|
||||
/>
|
||||
}
|
||||
sourcePreview={
|
||||
<ComponentSource
|
||||
name={name}
|
||||
collapsible={false}
|
||||
styleName={styleName}
|
||||
maxLines={3}
|
||||
/>
|
||||
}
|
||||
chromeLessOnMobile={chromeLessOnMobile}
|
||||
direction={direction}
|
||||
styleName={styleName}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
|
||||
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 content
|
||||
}
|
||||
|
||||
@@ -3,12 +3,12 @@ import path from "node:path"
|
||||
import * as React from "react"
|
||||
|
||||
import { highlightCode } from "@/lib/highlight-code"
|
||||
import { getRegistryItem } from "@/lib/registry"
|
||||
import { getDemoItem, getRegistryItem } from "@/lib/registry"
|
||||
import { formatCode } from "@/lib/rehype"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { CodeCollapsibleWrapper } from "@/components/code-collapsible-wrapper"
|
||||
import { CopyButton } from "@/components/copy-button"
|
||||
import { getIconForLanguageExtension } from "@/components/icons"
|
||||
import { type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
export async function ComponentSource({
|
||||
name,
|
||||
@@ -18,13 +18,15 @@ export async function ComponentSource({
|
||||
collapsible = true,
|
||||
className,
|
||||
styleName = "new-york-v4",
|
||||
maxLines,
|
||||
}: React.ComponentProps<"div"> & {
|
||||
name?: string
|
||||
src?: string
|
||||
title?: string
|
||||
language?: string
|
||||
collapsible?: boolean
|
||||
styleName?: Style["name"]
|
||||
styleName?: string
|
||||
maxLines?: number
|
||||
}) {
|
||||
if (!name && !src) {
|
||||
return null
|
||||
@@ -33,7 +35,9 @@ export async function ComponentSource({
|
||||
let code: string | undefined
|
||||
|
||||
if (name) {
|
||||
const item = await getRegistryItem(name, styleName)
|
||||
const item =
|
||||
(await getDemoItem(name, styleName)) ??
|
||||
(await getRegistryItem(name, styleName))
|
||||
code = item?.files?.[0]?.content
|
||||
}
|
||||
|
||||
@@ -46,14 +50,14 @@ export async function ComponentSource({
|
||||
return null
|
||||
}
|
||||
|
||||
// Fix imports.
|
||||
// Replace @/registry/${style}/ with @/components/.
|
||||
code = code.replaceAll(`@/registry/${styleName}/`, "@/components/")
|
||||
|
||||
// Replace export default with export.
|
||||
code = code.replaceAll("export default", "export")
|
||||
code = await formatCode(code, styleName)
|
||||
code = code.replaceAll("/* eslint-disable react/no-children-prop */\n", "")
|
||||
|
||||
// Truncate code if maxLines is set.
|
||||
if (maxLines) {
|
||||
code = code.split("\n").slice(0, maxLines).join("\n")
|
||||
}
|
||||
|
||||
const lang = language ?? title?.split(".").pop() ?? "tsx"
|
||||
const highlightedCode = await highlightCode(code, lang)
|
||||
|
||||
|
||||
@@ -1,23 +1,19 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { source } from "@/lib/source"
|
||||
import { getPagesFromFolder, type PageTreeFolder } from "@/lib/page-tree"
|
||||
|
||||
export function ComponentsList() {
|
||||
const components = source.pageTree.children.find(
|
||||
(page) => page.$id === "components"
|
||||
)
|
||||
|
||||
if (components?.type !== "folder") {
|
||||
return
|
||||
}
|
||||
|
||||
const list = components.children.filter(
|
||||
(component) => component.type === "page"
|
||||
)
|
||||
export function ComponentsList({
|
||||
componentsFolder,
|
||||
currentBase,
|
||||
}: {
|
||||
componentsFolder: PageTreeFolder
|
||||
currentBase: string
|
||||
}) {
|
||||
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>
|
||||
)
|
||||
}
|
||||
|
||||
39
apps/v4/components/docs-base-switcher.tsx
Normal file
39
apps/v4/components/docs-base-switcher.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import Link from "next/link"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { BASES } from "@/registry/bases"
|
||||
|
||||
export function DocsBaseSwitcher({
|
||||
base,
|
||||
component,
|
||||
className,
|
||||
}: {
|
||||
base: string
|
||||
component: string
|
||||
className?: string
|
||||
}) {
|
||||
const activeBase = BASES.find((baseItem) => base === baseItem.name)
|
||||
|
||||
return (
|
||||
<div className={cn("inline-flex w-full items-center gap-6", className)}>
|
||||
{BASES.map((baseItem) => (
|
||||
<Link
|
||||
key={baseItem.name}
|
||||
href={`/docs/components/${baseItem.name}/${component}`}
|
||||
data-active={base === baseItem.name}
|
||||
className="text-muted-foreground hover:text-foreground data-[active=true]:text-foreground after:bg-foreground relative inline-flex items-center justify-center gap-1 pt-1 pb-0.5 text-base font-medium transition-colors after:absolute after:inset-x-0 after:bottom-[-4px] after:h-0.5 after:opacity-0 after:transition-opacity data-[active=true]:after:opacity-100"
|
||||
>
|
||||
{baseItem.title}
|
||||
</Link>
|
||||
))}
|
||||
{activeBase?.meta?.logo && (
|
||||
<div
|
||||
className="text-muted-foreground ml-auto size-4 shrink-0 opacity-80 [&_svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: activeBase.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -183,7 +183,10 @@ export function DocsCopyPage({ page, url }: { page: string; url: string }) {
|
||||
<DropdownMenuTrigger asChild className="hidden sm:flex">
|
||||
{trigger}
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="shadow-none">
|
||||
<DropdownMenuContent
|
||||
align="end"
|
||||
className="animate-none! rounded-lg shadow-none"
|
||||
>
|
||||
{Object.entries(menuItems).map(([key, value]) => (
|
||||
<DropdownMenuItem key={key} asChild>
|
||||
{value(url)}
|
||||
@@ -193,13 +196,13 @@ export function DocsCopyPage({ page, url }: { page: string; url: string }) {
|
||||
</DropdownMenu>
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="!bg-foreground/10 absolute top-0 right-8 z-0 !h-8 peer-focus-visible:opacity-0 sm:right-7 sm:!h-7"
|
||||
className="!bg-foreground/5 absolute top-1 right-8 z-0 !h-6 peer-focus-visible:opacity-0 sm:right-7 sm:!h-5"
|
||||
/>
|
||||
<PopoverTrigger asChild className="flex sm:hidden">
|
||||
{trigger}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-sm backdrop-blur-sm"
|
||||
className="bg-background/70 dark:bg-background/60 w-52 !origin-center rounded-lg p-1 shadow-none backdrop-blur-sm"
|
||||
align="start"
|
||||
>
|
||||
{Object.entries(menuItems).map(([key, value]) => (
|
||||
|
||||
179
apps/v4/components/docs-page-links.tsx
Normal file
179
apps/v4/components/docs-page-links.tsx
Normal file
@@ -0,0 +1,179 @@
|
||||
"use client"
|
||||
|
||||
import { IconCheck, IconCopy } from "@tabler/icons-react"
|
||||
|
||||
import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard"
|
||||
|
||||
function getPromptUrl(baseURL: string, url: string) {
|
||||
return `${baseURL}?q=${encodeURIComponent(
|
||||
`I'm looking at this shadcn/ui documentation: ${url}.
|
||||
Help me understand how to use it. Be ready to explain concepts, give examples, or help debug based on it.
|
||||
`
|
||||
)}`
|
||||
}
|
||||
|
||||
export function DocsPageLinks({ page, url }: { page: string; url: string }) {
|
||||
const { copyToClipboard, isCopied } = useCopyToClipboard()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3 px-6">
|
||||
<ul className="text-muted-foreground flex flex-col gap-2 text-[0.8rem]">
|
||||
<li>
|
||||
<button
|
||||
onClick={() => copyToClipboard(page)}
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
{isCopied ? (
|
||||
<IconCheck className="size-4" />
|
||||
) : (
|
||||
<IconCopy className="size-4" />
|
||||
)}
|
||||
Copy page
|
||||
</button>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={`${url}.md`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg strokeLinejoin="round" viewBox="0 0 22 16" className="size-4">
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M19.5 2.25H2.5C1.80964 2.25 1.25 2.80964 1.25 3.5V12.5C1.25 13.1904 1.80964 13.75 2.5 13.75H19.5C20.1904 13.75 20.75 13.1904 20.75 12.5V3.5C20.75 2.80964 20.1904 2.25 19.5 2.25ZM2.5 1C1.11929 1 0 2.11929 0 3.5V12.5C0 13.8807 1.11929 15 2.5 15H19.5C20.8807 15 22 13.8807 22 12.5V3.5C22 2.11929 20.8807 1 19.5 1H2.5ZM3 4.5H4H4.25H4.6899L4.98715 4.82428L7 7.02011L9.01285 4.82428L9.3101 4.5H9.75H10H11V5.5V11.5H9V7.79807L7.73715 9.17572L7 9.97989L6.26285 9.17572L5 7.79807V11.5H3V5.5V4.5ZM15 8V4.5H17V8H19.5L17 10.5L16 11.5L15 10.5L12.5 8H15Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
View as Markdown
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPromptUrl("https://v0.dev", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="currentColor"
|
||||
viewBox="0 0 147 70"
|
||||
className="size-4"
|
||||
>
|
||||
<path d="M56 50.203V14h14v46.156C70 65.593 65.593 70 60.156 70c-2.596 0-5.158-1-7-2.843L0 14h19.797L56 50.203ZM147 56h-14V23.953L100.953 56H133v14H96.687C85.814 70 77 61.186 77 50.312V14h14v32.156L123.156 14H91V0h36.312C138.186 0 147 8.814 147 19.688V56Z" />
|
||||
</svg>
|
||||
Open in v0
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPromptUrl("https://chatgpt.com", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
d="M22.282 9.821a5.985 5.985 0 0 0-.516-4.91 6.046 6.046 0 0 0-6.51-2.9A6.065 6.065 0 0 0 4.981 4.18a5.985 5.985 0 0 0-3.998 2.9 6.046 6.046 0 0 0 .743 7.097 5.98 5.98 0 0 0 .51 4.911 6.051 6.051 0 0 0 6.515 2.9A5.985 5.985 0 0 0 13.26 24a6.056 6.056 0 0 0 5.772-4.206 5.99 5.99 0 0 0 3.997-2.9 6.056 6.056 0 0 0-.747-7.073zM13.26 22.43a4.476 4.476 0 0 1-2.876-1.04l.141-.081 4.779-2.758a.795.795 0 0 0 .392-.681v-6.737l2.02 1.168a.071.071 0 0 1 .038.052v5.583a4.504 4.504 0 0 1-4.494 4.494zM3.6 18.304a4.47 4.47 0 0 1-.535-3.014l.142.085 4.783 2.759a.771.771 0 0 0 .78 0l5.843-3.369v2.332a.08.08 0 0 1-.033.062L9.74 19.95a4.5 4.5 0 0 1-6.14-1.646zM2.34 7.896a4.485 4.485 0 0 1 2.366-1.973V11.6a.766.766 0 0 0 .388.676l5.815 3.355-2.02 1.168a.076.076 0 0 1-.071 0l-4.83-2.786A4.504 4.504 0 0 1 2.34 7.872zm16.597 3.855-5.833-3.387L15.119 7.2a.076.076 0 0 1 .071 0l4.83 2.791a4.494 4.494 0 0 1-.676 8.105v-5.678a.79.79 0 0 0-.407-.667zm2.01-3.023-.141-.085-4.774-2.782a.776.776 0 0 0-.785 0L9.409 9.23V6.897a.066.066 0 0 1 .028-.061l4.83-2.787a4.5 4.5 0 0 1 6.68 4.66zm-12.64 4.135-2.02-1.164a.08.08 0 0 1-.038-.057V6.075a4.5 4.5 0 0 1 7.375-3.453l-.142.08-4.778 2.758a.795.795 0 0 0-.393.681zm1.097-2.365 2.602-1.5 2.607 1.5v2.999l-2.597 1.5-2.607-1.5Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Open in ChatGPT
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPromptUrl("https://claude.ai/new", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
d="m4.714 15.956 4.718-2.648.079-.23-.08-.128h-.23l-.79-.048-2.695-.073-2.337-.097-2.265-.122-.57-.121-.535-.704.055-.353.48-.321.685.06 1.518.104 2.277.157 1.651.098 2.447.255h.389l.054-.158-.133-.097-.103-.098-2.356-1.596-2.55-1.688-1.336-.972-.722-.491L2 6.223l-.158-1.008.655-.722.88.06.225.061.893.686 1.906 1.476 2.49 1.833.364.304.146-.104.018-.072-.164-.274-1.354-2.446-1.445-2.49-.644-1.032-.17-.619a2.972 2.972 0 0 1-.103-.729L6.287.133 6.7 0l.995.134.42.364.619 1.415L9.735 4.14l1.555 3.03.455.898.243.832.09.255h.159V9.01l.127-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.583.28.48.685-.067.444-.286 1.851-.558 2.903-.365 1.942h.213l.243-.242.983-1.306 1.652-2.064.728-.82.85-.904.547-.431h1.032l.759 1.129-.34 1.166-1.063 1.347-.88 1.142-1.263 1.7-.79 1.36.074.11.188-.02 2.853-.606 1.542-.28 1.84-.315.832.388.09.395-.327.807-1.967.486-2.307.462-3.436.813-.043.03.049.061 1.548.146.662.036h1.62l3.018.225.79.522.473.638-.08.485-1.213.62-1.64-.389-3.825-.91-1.31-.329h-.183v.11l1.093 1.068 2.003 1.81 2.508 2.33.127.578-.321.455-.34-.049-2.204-1.657-.85-.747-1.925-1.62h-.127v.17l.443.649 2.343 3.521.122 1.08-.17.353-.607.213-.668-.122-1.372-1.924-1.415-2.168-1.141-1.943-.14.08-.674 7.254-.316.37-.728.28-.607-.461-.322-.747.322-1.476.388-1.924.316-1.53.285-1.9.17-.632-.012-.042-.14.018-1.432 1.967-2.18 2.945-1.724 1.845-.413.164-.716-.37.066-.662.401-.589 2.386-3.036 1.439-1.882.929-1.086-.006-.158h-.055L4.138 18.56l-1.13.146-.485-.456.06-.746.231-.243 1.907-1.312Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
Open in Claude
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a
|
||||
href={getPromptUrl("https://scira.ai/", url)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:text-foreground inline-flex items-center gap-2 transition-colors"
|
||||
>
|
||||
<svg
|
||||
width="910"
|
||||
height="934"
|
||||
viewBox="0 0 910 934"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className="size-4"
|
||||
>
|
||||
<path
|
||||
d="M647.664 197.775C569.13 189.049 525.5 145.419 516.774 66.8849C508.048 145.419 464.418 189.049 385.884 197.775C464.418 206.501 508.048 250.131 516.774 328.665C525.5 250.131 569.13 206.501 647.664 197.775Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M516.774 304.217C510.299 275.491 498.208 252.087 480.335 234.214C462.462 216.341 439.058 204.251 410.333 197.775C439.059 191.3 462.462 179.209 480.335 161.336C498.208 143.463 510.299 120.06 516.774 91.334C523.25 120.059 535.34 143.463 553.213 161.336C571.086 179.209 594.49 191.3 623.216 197.775C594.49 204.251 571.086 216.341 553.213 234.214C535.34 252.087 523.25 275.491 516.774 304.217Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M857.5 508.116C763.259 497.644 710.903 445.288 700.432 351.047C689.961 445.288 637.605 497.644 543.364 508.116C637.605 518.587 689.961 570.943 700.432 665.184C710.903 570.943 763.259 518.587 857.5 508.116Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="20"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M700.432 615.957C691.848 589.05 678.575 566.357 660.383 548.165C642.191 529.973 619.499 516.7 592.593 508.116C619.499 499.533 642.191 486.258 660.383 468.066C678.575 449.874 691.848 427.181 700.432 400.274C709.015 427.181 722.289 449.874 740.481 468.066C758.673 486.258 781.365 499.533 808.271 508.116C781.365 516.7 758.673 529.973 740.481 548.165C722.289 566.357 709.015 589.05 700.432 615.957Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="20"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M889.949 121.237C831.049 114.692 798.326 81.9698 791.782 23.0692C785.237 81.9698 752.515 114.692 693.614 121.237C752.515 127.781 785.237 160.504 791.782 219.404C798.326 160.504 831.049 127.781 889.949 121.237Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M791.782 196.795C786.697 176.937 777.869 160.567 765.16 147.858C752.452 135.15 736.082 126.322 716.226 121.237C736.082 116.152 752.452 107.324 765.16 94.6152C777.869 81.9065 786.697 65.5368 791.782 45.6797C796.867 65.5367 805.695 81.9066 818.403 94.6152C831.112 107.324 847.481 116.152 867.338 121.237C847.481 126.322 831.112 135.15 818.403 147.858C805.694 160.567 796.867 176.937 791.782 196.795Z"
|
||||
fill="currentColor"
|
||||
stroke="currentColor"
|
||||
strokeWidth="8"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M760.632 764.337C720.719 814.616 669.835 855.1 611.872 882.692C553.91 910.285 490.404 924.255 426.213 923.533C362.022 922.812 298.846 907.419 241.518 878.531C184.19 849.643 134.228 808.026 95.4548 756.863C56.6815 705.7 30.1238 646.346 17.8129 583.343C5.50207 520.339 7.76433 455.354 24.4266 393.359C41.089 331.364 71.7099 274.001 113.947 225.658C156.184 177.315 208.919 139.273 268.117 114.442"
|
||||
stroke="currentColor"
|
||||
strokeWidth="30"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
Open in Scira
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import { usePathname } from "next/navigation"
|
||||
|
||||
import { PAGES_NEW } from "@/lib/docs"
|
||||
import { showMcpDocs } from "@/lib/flags"
|
||||
import { getCurrentBase, getPagesFromFolder } from "@/lib/page-tree"
|
||||
import type { source } from "@/lib/source"
|
||||
import {
|
||||
Sidebar,
|
||||
@@ -18,19 +19,31 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
|
||||
const TOP_LEVEL_SECTIONS = [
|
||||
{ name: "Get Started", href: "/docs" },
|
||||
{ name: "Introduction", href: "/docs" },
|
||||
{
|
||||
name: "Components",
|
||||
href: "/docs/components",
|
||||
},
|
||||
{
|
||||
name: "Installation",
|
||||
href: "/docs/installation",
|
||||
},
|
||||
{
|
||||
name: "Directory",
|
||||
href: "/docs/directory",
|
||||
},
|
||||
{
|
||||
name: "RTL",
|
||||
href: "/docs/rtl",
|
||||
},
|
||||
{
|
||||
name: "MCP Server",
|
||||
href: "/docs/mcp",
|
||||
},
|
||||
{
|
||||
name: "Registry",
|
||||
href: "/docs/registry",
|
||||
},
|
||||
{
|
||||
name: "Forms",
|
||||
href: "/docs/forms",
|
||||
@@ -40,24 +53,27 @@ const TOP_LEVEL_SECTIONS = [
|
||||
href: "/docs/changelog",
|
||||
},
|
||||
]
|
||||
const EXCLUDED_SECTIONS = ["installation", "dark-mode"]
|
||||
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,
|
||||
...props
|
||||
}: React.ComponentProps<typeof Sidebar> & { tree: typeof source.pageTree }) {
|
||||
const pathname = usePathname()
|
||||
const currentBase = getCurrentBase(pathname)
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className="sticky top-[calc(var(--header-height)+1px)] z-30 hidden h-[calc(100svh-var(--footer-height)-4rem)] 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>
|
||||
@@ -79,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>
|
||||
@@ -102,37 +124,34 @@ export function DocsSidebar({
|
||||
<SidebarGroupContent>
|
||||
{item.type === "folder" && (
|
||||
<SidebarMenu className="gap-0.5">
|
||||
{item.children.map((item) => {
|
||||
if (
|
||||
!showMcpDocs &&
|
||||
item.type === "page" &&
|
||||
item.url?.includes("/mcp")
|
||||
) {
|
||||
{getPagesFromFolder(item, currentBase).map((page) => {
|
||||
if (!showMcpDocs && page.url.includes("/mcp")) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (EXCLUDED_PAGES.includes(page.url)) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
item.type === "page" &&
|
||||
!EXCLUDED_PAGES.includes(item.url) && (
|
||||
<SidebarMenuItem key={item.url}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={item.url === pathname}
|
||||
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={item.url}>
|
||||
<span className="absolute inset-0 flex w-(--sidebar-width) bg-transparent" />
|
||||
{item.name}
|
||||
{PAGES_NEW.includes(item.url) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
<SidebarMenuItem key={page.url}>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
isActive={page.url === pathname}
|
||||
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-menu-width) bg-transparent" />
|
||||
{page.name}
|
||||
{PAGES_NEW.includes(page.url) && (
|
||||
<span
|
||||
className="flex size-2 rounded-full bg-blue-500"
|
||||
title="New"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
)
|
||||
})}
|
||||
</SidebarMenu>
|
||||
|
||||
@@ -107,14 +107,14 @@ export function DocsTableOfContents({
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2 p-4 pt-0 text-sm", className)}>
|
||||
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs">
|
||||
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
|
||||
On This Page
|
||||
</p>
|
||||
{toc.map((item) => (
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user