mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-12 18:31:46 +00:00
Compare commits
1 Commits
shadcn@3.8
...
shadcn/deb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f1131db844 |
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
Normal file
63
.github/ISSUE_TEMPLATE/registry_directory.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
name: Add registry to directory
|
||||
description: Add your registry to the directory
|
||||
title: "[Registry Directory]: "
|
||||
labels: ["registry", "directory"]
|
||||
assignees: []
|
||||
body:
|
||||
- type: input
|
||||
id: name
|
||||
attributes:
|
||||
label: Name
|
||||
description: The name of your registry. This is also the namespace.
|
||||
placeholder: e.g., "@acme"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: url
|
||||
attributes:
|
||||
label: URL
|
||||
description: The URL to your registry index. Use {name} placeholder.
|
||||
placeholder: https://ui.acme.com/r/{name}.json
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: homepage
|
||||
attributes:
|
||||
label: Homepage
|
||||
description: The URL to your registry homepage. This is where users can browse your registry.
|
||||
placeholder: https://ui.acme.com
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: description
|
||||
attributes:
|
||||
label: Description
|
||||
description: Briefly describe what is your registry and what type of components or code it distributes.
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logo
|
||||
attributes:
|
||||
label: Logo
|
||||
description: Add your SVG logo here.
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: checkboxes
|
||||
id: requirements
|
||||
attributes:
|
||||
label: Checklist
|
||||
description: Verify that your registry meets the following requirements.
|
||||
options:
|
||||
- label: The registry must be open source and publicly accessible.
|
||||
- label: The registry must be a valid JSON file that conforms to the [registry schema](https://ui.shadcn.com/docs/registry/registry-json) specification.
|
||||
- label: The `files` array, if present on your registry items, must NOT include a `content` property.
|
||||
- label: I've attached a square SVG logo to this issue
|
||||
validations:
|
||||
required: true
|
||||
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 { execSync } from "child_process"
|
||||
import { exec } 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 pnpm-lock.yaml file.
|
||||
// So we also run `pnpm install`, which does this update.
|
||||
// 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.
|
||||
// This is a workaround until this is handled automatically by `changeset version`.
|
||||
// See https://github.com/changesets/changesets/issues/421.
|
||||
execSync("npx changeset version", { stdio: "inherit" })
|
||||
execSync("pnpm install --lockfile-only", { stdio: "inherit" })
|
||||
exec("npx changeset version")
|
||||
exec("npm install")
|
||||
|
||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -7,11 +7,6 @@ on:
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
release:
|
||||
if: ${{ github.repository_owner == 'shadcn-ui' }}
|
||||
@@ -29,15 +24,12 @@ jobs:
|
||||
version: 9.0.6
|
||||
|
||||
- name: Use Node.js 20
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
version: 9.0.6
|
||||
node-version: 20
|
||||
registry-url: "https://registry.npmjs.org"
|
||||
cache: "pnpm"
|
||||
|
||||
- name: Update npm for OIDC support
|
||||
run: npm install -g npm@latest
|
||||
|
||||
- name: Install NPM Dependencies
|
||||
run: pnpm install
|
||||
|
||||
@@ -57,4 +49,5 @@ jobs:
|
||||
publish: npx changeset publish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_ACCESS_TOKEN }}
|
||||
NODE_ENV: "production"
|
||||
|
||||
5
.github/workflows/validate-registries.yml
vendored
5
.github/workflows/validate-registries.yml
vendored
@@ -4,13 +4,11 @@ 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:
|
||||
@@ -49,5 +47,8 @@ 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,4 +41,3 @@ tsconfig.tsbuildinfo
|
||||
.vscode
|
||||
|
||||
.notes
|
||||
.playwright-mcp
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
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 {
|
||||
Field,
|
||||
FieldContent,
|
||||
@@ -13,11 +15,13 @@ import {
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} 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"
|
||||
} 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"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
@@ -93,7 +97,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 !w-14 font-mono"
|
||||
className="h-8 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
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 {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -15,18 +27,7 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
@@ -56,7 +57,7 @@ export function ButtonGroupDemo() {
|
||||
<MoreHorizontalIcon />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="w-48">
|
||||
<DropdownMenuContent align="end" className="w-48 [--radius:1rem]">
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem>
|
||||
<MailCheckIcon />
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
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 {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
"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,13 +1,14 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
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 {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} 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"
|
||||
} 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"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
@@ -21,7 +22,7 @@ export function ButtonGroupPopover() {
|
||||
<ChevronDownIcon />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" className="gap-0 rounded-xl p-0 text-sm">
|
||||
<PopoverContent align="end" className="rounded-xl p-0 text-sm">
|
||||
<div className="px-4 py-3">
|
||||
<div className="text-sm font-medium">Agent Tasks</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/examples/radix/ui/avatar"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
} from "@/registry/new-york-v4/ui/avatar"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -12,15 +13,14 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
<Empty className="flex-none border py-10">
|
||||
<Empty className="flex-none border">
|
||||
<EmptyHeader>
|
||||
<EmptyMedia>
|
||||
<AvatarGroup className="grayscale">
|
||||
<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" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
@@ -39,7 +39,7 @@ export function EmptyAvatarGroup() {
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</AvatarGroup>
|
||||
</div>
|
||||
</EmptyMedia>
|
||||
<EmptyTitle>No Team Members</EmptyTitle>
|
||||
<EmptyDescription>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,21 +8,20 @@ import {
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Input } from "@/registry/new-york-v4/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectGroup,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/examples/radix/ui/select"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
} from "@/registry/new-york-v4/ui/select"
|
||||
import { Textarea } from "@/registry/new-york-v4/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
<div className="w-full max-w-md rounded-xl border p-6">
|
||||
<div className="w-full max-w-md rounded-lg border p-6">
|
||||
<form>
|
||||
<FieldGroup>
|
||||
<FieldSet>
|
||||
@@ -70,20 +69,18 @@ export function FieldDemo() {
|
||||
<SelectValue placeholder="MM" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<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>
|
||||
<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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
@@ -96,14 +93,12 @@ export function FieldDemo() {
|
||||
<SelectValue placeholder="YYYY" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<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>
|
||||
<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>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</Field>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, CardContent } from "@/examples/radix/ui/card"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Card, CardContent } from "@/registry/new-york-v4/ui/card"
|
||||
import { Checkbox } from "@/registry/new-york-v4/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/examples/radix/ui/field"
|
||||
} from "@/registry/new-york-v4/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,8 +1,13 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
|
||||
import { Slider } from "@/examples/radix/ui/slider"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/registry/new-york-v4/ui/field"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FieldSeparator } from "@/examples/radix/ui/field"
|
||||
import { FieldSeparator } from "@/registry/new-york-v4/ui/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Label } from "@/examples/radix/ui/label"
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
@@ -11,15 +14,13 @@ import {
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
@@ -66,7 +67,11 @@ export function InputGroupDemo() {
|
||||
<DropdownMenuTrigger asChild>
|
||||
<InputGroupButton variant="ghost">Auto</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start">
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:0.95rem]"
|
||||
>
|
||||
<DropdownMenuItem>Auto</DropdownMenuItem>
|
||||
<DropdownMenuItem>Agent</DropdownMenuItem>
|
||||
<DropdownMenuItem>Manual</DropdownMenuItem>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
@@ -6,8 +8,7 @@ import {
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/examples/radix/ui/item"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
} from "@/registry/new-york-v4/ui/item"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,8 +1,24 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
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 {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -10,7 +26,7 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/examples/radix/ui/command"
|
||||
} from "@/registry/new-york-v4/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
@@ -23,36 +39,25 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/registry/new-york-v4/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
} from "@/registry/new-york-v4/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
} from "@/registry/new-york-v4/ui/popover"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
@@ -185,17 +190,17 @@ export function NotionPromptForm() {
|
||||
const hasMentions = mentions.length > 0
|
||||
|
||||
return (
|
||||
<form>
|
||||
<form className="[--radius:1.2rem]">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="notion-prompt" className="sr-only">
|
||||
Prompt
|
||||
</FieldLabel>
|
||||
<InputGroup className="rounded-xl">
|
||||
<InputGroup>
|
||||
<InputGroupTextarea
|
||||
id="notion-prompt"
|
||||
placeholder="Ask, search, or make anything..."
|
||||
/>
|
||||
<InputGroupAddon align="block-start" className="pt-3">
|
||||
<InputGroupAddon align="block-start">
|
||||
<Popover
|
||||
open={mentionPopoverOpen}
|
||||
onOpenChange={setMentionPopoverOpen}
|
||||
@@ -209,7 +214,7 @@ export function NotionPromptForm() {
|
||||
<InputGroupButton
|
||||
variant="outline"
|
||||
size={!hasMentions ? "sm" : "icon-sm"}
|
||||
className="transition-transform"
|
||||
className="rounded-full transition-transform"
|
||||
>
|
||||
<IconAt /> {!hasMentions && "Add context"}
|
||||
</InputGroupButton>
|
||||
@@ -217,7 +222,7 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Mention a person, page, or date</TooltipContent>
|
||||
</Tooltip>
|
||||
<PopoverContent className="p-0" align="start">
|
||||
<PopoverContent className="p-0 [--radius:1.2rem]" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder="Search pages..." />
|
||||
<CommandList>
|
||||
@@ -235,7 +240,6 @@ export function NotionPromptForm() {
|
||||
setMentions((prev) => [...prev, currentValue])
|
||||
setMentionPopoverOpen(false)
|
||||
}}
|
||||
className="rounded-lg"
|
||||
>
|
||||
<MentionableIcon item={item} />
|
||||
{item.title}
|
||||
@@ -302,8 +306,12 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup className="w-42">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
@@ -338,7 +346,11 @@ export function NotionPromptForm() {
|
||||
<IconWorld /> All Sources
|
||||
</InputGroupButton>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="end" className="w-72">
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="end"
|
||||
className="[--radius:1rem]"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuItem
|
||||
asChild
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
} from "@/registry/new-york-v4/ui/empty"
|
||||
import { Spinner } from "@/registry/new-york-v4/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { type Metadata } from "next"
|
||||
import Image from "next/image"
|
||||
import Link from "next/link"
|
||||
import { PlusSignIcon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Announcement } from "@/components/announcement"
|
||||
import { ExamplesNav } from "@/components/examples-nav"
|
||||
@@ -56,7 +58,10 @@ export default function IndexPage() {
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm" className="h-[31px] rounded-lg">
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
<Link href="/create">
|
||||
<HugeiconsIcon icon={PlusSignIcon} />
|
||||
New Project
|
||||
</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost" className="rounded-lg">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
|
||||
@@ -2,11 +2,7 @@ import * as React from "react"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
ChartDisplay,
|
||||
getCachedRegistryItem,
|
||||
getChartHighlightedCode,
|
||||
} from "@/components/chart-display"
|
||||
import { ChartDisplay } from "@/components/chart-display"
|
||||
import { getActiveStyle } from "@/registry/_legacy-styles"
|
||||
import { charts } from "@/app/(app)/charts/charts"
|
||||
|
||||
@@ -48,26 +44,6 @@ export default async function ChartPage({ params }: ChartPageProps) {
|
||||
const chartList = charts[chartType]
|
||||
const activeStyle = await getActiveStyle()
|
||||
|
||||
// Prefetch all chart data in parallel for better performance.
|
||||
// Charts are rendered via iframes, so we only need the metadata and highlighted code.
|
||||
const chartDataPromises = chartList.map(async (chart) => {
|
||||
const registryItem = await getCachedRegistryItem(chart.id, activeStyle.name)
|
||||
if (!registryItem) return null
|
||||
|
||||
const highlightedCode = await getChartHighlightedCode(
|
||||
registryItem.files?.[0]?.content ?? ""
|
||||
)
|
||||
if (!highlightedCode) return null
|
||||
|
||||
return {
|
||||
...registryItem,
|
||||
highlightedCode,
|
||||
fullWidth: chart.fullWidth,
|
||||
}
|
||||
})
|
||||
|
||||
const prefetchedCharts = await Promise.all(chartDataPromises)
|
||||
|
||||
return (
|
||||
<div className="grid flex-1 gap-12 lg:gap-24">
|
||||
<h2 className="sr-only">
|
||||
@@ -75,14 +51,16 @@ export default async function ChartPage({ params }: ChartPageProps) {
|
||||
</h2>
|
||||
<div className="grid flex-1 scroll-mt-20 items-stretch gap-10 md:grid-cols-2 md:gap-6 lg:grid-cols-3 xl:gap-10">
|
||||
{Array.from({ length: 12 }).map((_, index) => {
|
||||
const chart = prefetchedCharts[index]
|
||||
const chart = chartList[index]
|
||||
return chart ? (
|
||||
<ChartDisplay
|
||||
key={chart.name}
|
||||
chart={chart}
|
||||
style={activeStyle.name}
|
||||
key={chart.id}
|
||||
name={chart.id}
|
||||
styleName={activeStyle.name}
|
||||
className={cn(chart.fullWidth && "md:col-span-2 lg:col-span-3")}
|
||||
/>
|
||||
>
|
||||
<chart.component />
|
||||
</ChartDisplay>
|
||||
) : (
|
||||
<div
|
||||
key={`empty-${index}`}
|
||||
|
||||
@@ -63,8 +63,9 @@ export default function ChartsLayout({
|
||||
</PageHeader>
|
||||
<PageNav id="charts">
|
||||
<ChartsNav />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex-1">
|
||||
<div className="container-wrapper section-soft flex-1">
|
||||
<div className="container pb-6">
|
||||
<section className="theme-container">{children}</section>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
import Link from "next/link"
|
||||
import { notFound } from "next/navigation"
|
||||
import { mdxComponents } from "@/mdx-components"
|
||||
import { IconArrowLeft, IconArrowRight } from "@tabler/icons-react"
|
||||
import {
|
||||
IconArrowLeft,
|
||||
IconArrowRight,
|
||||
IconArrowUpRight,
|
||||
} from "@tabler/icons-react"
|
||||
import fm from "front-matter"
|
||||
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
|
||||
@@ -79,116 +85,127 @@ export default async function Page(props: {
|
||||
|
||||
const doc = page.data
|
||||
const MDX = doc.body
|
||||
const isChangelog = params.slug?.[0] === "changelog"
|
||||
const neighbours = isChangelog
|
||||
? { previous: null, next: null }
|
||||
: findNeighbour(source.pageTree, page.url)
|
||||
const neighbours = 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
|
||||
data-slot="docs"
|
||||
className="flex scroll-mt-24 items-stretch pb-8 text-[1.05rem] sm:text-[15px] xl:w-full"
|
||||
>
|
||||
<div className="flex items-stretch 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="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="flex flex-col gap-2">
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex items-center justify-between md:items-start">
|
||||
<h1 className="scroll-m-24 text-3xl font-semibold tracking-tight sm:text-3xl">
|
||||
<div className="flex items-start justify-between">
|
||||
<h1 className="scroll-m-20 text-4xl font-semibold tracking-tight sm:text-3xl xl:text-4xl">
|
||||
{doc.title}
|
||||
</h1>
|
||||
<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 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>
|
||||
</div>
|
||||
{doc.description && (
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
<p className="text-muted-foreground text-[1.05rem] text-balance sm:text-base">
|
||||
{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 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"
|
||||
/>
|
||||
)}
|
||||
<div className="w-full flex-1 *:data-[slot=alert]:first:mt-0">
|
||||
<MDX components={mdxComponents} />
|
||||
</div>
|
||||
<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 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>
|
||||
</div>
|
||||
<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>
|
||||
<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" />
|
||||
{doc.toc?.length ? (
|
||||
<div className="no-scrollbar flex flex-col gap-8 overflow-y-auto px-8">
|
||||
<div className="no-scrollbar overflow-y-auto px-8">
|
||||
<DocsTableOfContents toc={doc.toc} />
|
||||
<div className="h-12" />
|
||||
</div>
|
||||
) : null}
|
||||
<div className="hidden flex-1 flex-col gap-6 px-6 xl:flex">
|
||||
<div className="flex flex-1 flex-col gap-12 px-6">
|
||||
<OpenInV0Cta />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,144 +0,0 @@
|
||||
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,14 +9,7 @@ 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 [--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
|
||||
}
|
||||
>
|
||||
<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)]">
|
||||
<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 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">
|
||||
<div className="bg-background flex flex-col overflow-hidden rounded-lg border bg-clip-padding md:flex-1 xl:rounded-xl">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import type { Slider as SliderPrimitive } from "radix-ui"
|
||||
import { type SliderProps } from "@radix-ui/react-slider"
|
||||
|
||||
import {
|
||||
HoverCard,
|
||||
@@ -12,9 +12,7 @@ import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
|
||||
interface MaxLengthSelectorProps {
|
||||
defaultValue: React.ComponentProps<
|
||||
typeof SliderPrimitive.Root
|
||||
>["defaultValue"]
|
||||
defaultValue: SliderProps["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,8 +29,7 @@ import {
|
||||
|
||||
import { type Model, type ModelType } from "../data/models"
|
||||
|
||||
interface ModelSelectorProps
|
||||
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
|
||||
interface ModelSelectorProps extends PopoverProps {
|
||||
types: readonly ModelType[]
|
||||
models: Model[]
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Dialog } from "@radix-ui/react-dialog"
|
||||
import { MoreHorizontal } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
@@ -15,7 +16,6 @@ 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,8 +23,7 @@ import {
|
||||
|
||||
import { type Preset } from "../data/presets"
|
||||
|
||||
interface PresetSelectorProps
|
||||
extends React.ComponentProps<typeof PopoverPrimitive.Root> {
|
||||
interface PresetSelectorProps extends PopoverProps {
|
||||
presets: Preset[]
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import type { Slider as SliderPrimitive } from "radix-ui"
|
||||
import { type SliderProps } from "@radix-ui/react-slider"
|
||||
|
||||
import {
|
||||
HoverCard,
|
||||
@@ -12,9 +12,7 @@ import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
|
||||
interface TemperatureSelectorProps {
|
||||
defaultValue: React.ComponentProps<
|
||||
typeof SliderPrimitive.Root
|
||||
>["defaultValue"]
|
||||
defaultValue: SliderProps["defaultValue"]
|
||||
}
|
||||
|
||||
export function TemperatureSelector({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import type { Slider as SliderPrimitive } from "radix-ui"
|
||||
import { type SliderProps } from "@radix-ui/react-slider"
|
||||
|
||||
import {
|
||||
HoverCard,
|
||||
@@ -12,9 +12,7 @@ import { Label } from "@/registry/new-york-v4/ui/label"
|
||||
import { Slider } from "@/registry/new-york-v4/ui/slider"
|
||||
|
||||
interface TopPSelectorProps {
|
||||
defaultValue: React.ComponentProps<
|
||||
typeof SliderPrimitive.Root
|
||||
>["defaultValue"]
|
||||
defaultValue: SliderProps["defaultValue"]
|
||||
}
|
||||
|
||||
export function TopPSelector({ defaultValue }: TopPSelectorProps) {
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,179 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,516 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
"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>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
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,5 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { DropdownMenuTrigger } from "@radix-ui/react-dropdown-menu"
|
||||
import { type Table } from "@tanstack/react-table"
|
||||
import { Settings2 } from "lucide-react"
|
||||
|
||||
@@ -10,7 +11,6 @@ import {
|
||||
DropdownMenuContent,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/registry/new-york-v4/ui/dropdown-menu"
|
||||
|
||||
export function DataTableViewOptions<TData>({
|
||||
|
||||
@@ -3,23 +3,10 @@ import { NextResponse, type NextRequest } from "next/server"
|
||||
|
||||
import { processMdxForLLMs } from "@/lib/llm"
|
||||
import { source } from "@/lib/source"
|
||||
import { getActiveStyle, type Style } from "@/registry/_legacy-styles"
|
||||
import { getActiveStyle } 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[] }> }
|
||||
@@ -32,11 +19,9 @@ export async function GET(
|
||||
notFound()
|
||||
}
|
||||
|
||||
const effectiveStyle = getStyleFromSlug(slug, activeStyle.name)
|
||||
|
||||
const processedContent = processMdxForLLMs(
|
||||
await page.data.getText("raw"),
|
||||
effectiveStyle as Style["name"]
|
||||
activeStyle.name
|
||||
)
|
||||
|
||||
return new NextResponse(processedContent, {
|
||||
|
||||
@@ -26,8 +26,8 @@ export function MenuAccentPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Menu Accent</div>
|
||||
@@ -35,7 +35,7 @@ export function MenuAccentPicker({
|
||||
{currentAccent?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
@@ -65,31 +65,31 @@ export function MenuAccentPicker({
|
||||
</svg>
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
<LockButton
|
||||
param="menuAccent"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentAccent?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuAccent: value as MenuAccentValue })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentAccent?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuAccent: value as MenuAccentValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_ACCENTS.map((accent) => (
|
||||
<PickerRadioItem key={accent.value} value={accent.value}>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuAccent"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{MENU_ACCENTS.map((accent) => (
|
||||
<PickerRadioItem key={accent.value} value={accent.value}>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -35,8 +35,8 @@ export function BaseColorPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Base Color</div>
|
||||
@@ -54,72 +54,72 @@ export function BaseColorPicker({
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
|
||||
className="absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBaseColor?.name}
|
||||
onValueChange={(value) => {
|
||||
if (value === "dark") {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
return
|
||||
}
|
||||
<LockButton
|
||||
param="baseColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBaseColor?.name}
|
||||
onValueChange={(value) => {
|
||||
if (value === "dark") {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
return
|
||||
}
|
||||
|
||||
setParams({ baseColor: value as BaseColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASE_COLORS.map((baseColor) => (
|
||||
<PickerRadioItem key={baseColor.name} value={baseColor.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
baseColor.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{baseColor.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerItem
|
||||
onClick={() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>
|
||||
Switch to {resolvedTheme === "dark" ? "Light" : "Dark"} Mode
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Base colors are easier to see in dark mode.
|
||||
</div>
|
||||
setParams({ baseColor: value as BaseColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASE_COLORS.map((baseColor) => (
|
||||
<PickerRadioItem key={baseColor.name} value={baseColor.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
baseColor.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["muted-foreground"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{baseColor.title}
|
||||
</div>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="baseColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerItem
|
||||
onClick={() => {
|
||||
setTheme(resolvedTheme === "dark" ? "light" : "dark")
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>
|
||||
Switch to {resolvedTheme === "dark" ? "Light" : "Dark"} Mode
|
||||
</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Base colors are easier to see in dark mode.
|
||||
</div>
|
||||
</div>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ export function BasePicker({
|
||||
</div>
|
||||
{currentBase?.meta?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
|
||||
className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 size-4 -translate-y-1/2 *:[svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentBase.meta.logo,
|
||||
}}
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
|
||||
import * as React from "react"
|
||||
import Script from "next/script"
|
||||
import { DiceFaces05Icon } from "@hugeicons/core-free-icons"
|
||||
import { DiceFaces05Icon, Undo02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
BASE_COLORS,
|
||||
DEFAULT_CONFIG,
|
||||
getThemesForBaseColor,
|
||||
iconLibraries,
|
||||
MENU_ACCENTS,
|
||||
@@ -16,11 +18,6 @@ import {
|
||||
} from "@/registry/config"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Kbd } from "@/registry/new-york-v4/ui/kbd"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
import { useLocks } from "@/app/(create)/hooks/use-locks"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import {
|
||||
@@ -36,10 +33,26 @@ function randomItem<T>(array: readonly T[]): T {
|
||||
return array[Math.floor(Math.random() * array.length)]
|
||||
}
|
||||
|
||||
export function RandomButton() {
|
||||
export function CustomizerControls({ className }: { className?: string }) {
|
||||
const { locks } = useLocks()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const handleReset = React.useCallback(() => {
|
||||
setParams({
|
||||
base: params.base, // Keep the current base value
|
||||
style: DEFAULT_CONFIG.style,
|
||||
baseColor: DEFAULT_CONFIG.baseColor,
|
||||
theme: DEFAULT_CONFIG.theme,
|
||||
iconLibrary: DEFAULT_CONFIG.iconLibrary,
|
||||
font: DEFAULT_CONFIG.font,
|
||||
menuAccent: DEFAULT_CONFIG.menuAccent,
|
||||
menuColor: DEFAULT_CONFIG.menuColor,
|
||||
radius: DEFAULT_CONFIG.radius,
|
||||
template: DEFAULT_CONFIG.template,
|
||||
item: "preview",
|
||||
})
|
||||
}, [setParams, params.base])
|
||||
|
||||
const handleRandomize = React.useCallback(() => {
|
||||
// Use current value if locked, otherwise randomize.
|
||||
const baseColor = locks.has("baseColor")
|
||||
@@ -117,30 +130,33 @@ export function RandomButton() {
|
||||
}, [handleRandomize])
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRandomize}
|
||||
className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
|
||||
>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Shuffle</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
Try Random
|
||||
</div>
|
||||
</div>
|
||||
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
|
||||
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">
|
||||
R
|
||||
</Kbd>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="left">
|
||||
Use browser back/forward to navigate history
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className={cn("items-center gap-0", className)}>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleRandomize}
|
||||
className="border-foreground/10 bg-muted/50 h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
|
||||
>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Shuffle</div>
|
||||
<div className="text-foreground text-sm font-medium">Try Random</div>
|
||||
</div>
|
||||
<HugeiconsIcon icon={DiceFaces05Icon} className="size-5 md:hidden" />
|
||||
<Kbd className="bg-foreground/10 text-foreground hidden md:flex">R</Kbd>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={handleReset}
|
||||
className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
|
||||
>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Reset</div>
|
||||
<div className="text-foreground text-sm font-medium">Start Over</div>
|
||||
</div>
|
||||
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,17 +7,15 @@ import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { getThemesForBaseColor, PRESETS, STYLES } from "@/registry/config"
|
||||
import { FieldGroup } from "@/registry/new-york-v4/ui/field"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { MenuAccentPicker } from "@/app/(create)/components/accent-picker"
|
||||
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
|
||||
import { BasePicker } from "@/app/(create)/components/base-picker"
|
||||
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
|
||||
import { FontPicker } from "@/app/(create)/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
|
||||
import { MenuColorPicker } from "@/app/(create)/components/menu-picker"
|
||||
import { PresetPicker } from "@/app/(create)/components/preset-picker"
|
||||
import { RadiusPicker } from "@/app/(create)/components/radius-picker"
|
||||
import { RandomButton } from "@/app/(create)/components/random-button"
|
||||
import { ResetButton } from "@/app/(create)/components/reset-button"
|
||||
import { StylePicker } from "@/app/(create)/components/style-picker"
|
||||
import { ThemePicker } from "@/app/(create)/components/theme-picker"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
@@ -38,6 +36,21 @@ 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
|
||||
@@ -62,10 +75,7 @@ export function Customizer() {
|
||||
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<div className="mt-auto hidden w-full flex-col items-center gap-0 md:flex">
|
||||
<RandomButton />
|
||||
<ResetButton />
|
||||
</div>
|
||||
<CustomizerControls className="mt-auto hidden w-full flex-col md:flex" />
|
||||
</FieldGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -7,23 +7,21 @@ import {
|
||||
DEFAULT_CONFIG,
|
||||
type DesignSystemConfig,
|
||||
} from "@/registry/config"
|
||||
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import { useDesignSystemParam } from "@/app/(create)/hooks/use-design-system"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function DesignSystemProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [
|
||||
{ style, theme, font, baseColor, menuAccent, menuColor, radius },
|
||||
setSearchParams,
|
||||
] = useDesignSystemSearchParams({
|
||||
shallow: true, // No need to go through the server…
|
||||
history: "replace", // …or push updates into the iframe history.
|
||||
})
|
||||
useIframeMessageListener("design-system-params", setSearchParams)
|
||||
const style = useDesignSystemParam("style")
|
||||
const theme = useDesignSystemParam("theme")
|
||||
const font = useDesignSystemParam("font")
|
||||
const baseColor = useDesignSystemParam("baseColor")
|
||||
const menuAccent = useDesignSystemParam("menuAccent")
|
||||
const menuColor = useDesignSystemParam("menuColor")
|
||||
const radius = useDesignSystemParam("radius")
|
||||
const [isReady, setIsReady] = React.useState(false)
|
||||
|
||||
// Use useLayoutEffect for synchronous style updates to prevent flash.
|
||||
|
||||
@@ -39,8 +39,8 @@ export function FontPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Font</div>
|
||||
@@ -49,55 +49,54 @@ export function FontPicker({
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none"
|
||||
className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base"
|
||||
style={{ fontFamily: currentFont?.font.style.fontFamily }}
|
||||
>
|
||||
Aa
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-96 md:w-72"
|
||||
<LockButton
|
||||
param="font"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-80 md:w-72"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentFont?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ font: value as FontValue })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentFont?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ font: value as FontValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{fonts.map((font, index) => (
|
||||
<React.Fragment key={font.value}>
|
||||
<PickerRadioItem value={font.value}>
|
||||
<Item size="xs">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemTitle className="text-muted-foreground text-xs font-medium">
|
||||
{font.name}
|
||||
</ItemTitle>
|
||||
<ItemDescription
|
||||
style={{ fontFamily: font.font.style.fontFamily }}
|
||||
>
|
||||
Designers love packing quirky glyphs into test
|
||||
phrases.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</PickerRadioItem>
|
||||
{index < fonts.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="font"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{fonts.map((font, index) => (
|
||||
<React.Fragment key={font.value}>
|
||||
<PickerRadioItem value={font.value}>
|
||||
<Item size="xs">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemTitle className="text-muted-foreground text-xs font-medium">
|
||||
{font.name}
|
||||
</ItemTitle>
|
||||
<ItemDescription
|
||||
style={{ fontFamily: font.font.style.fontFamily }}
|
||||
>
|
||||
Designers love packing quirky glyphs into test phrases.
|
||||
</ItemDescription>
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</PickerRadioItem>
|
||||
{index < fonts.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -39,18 +39,6 @@ const IconHugeicons = lazy(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const IconPhosphor = lazy(() =>
|
||||
import("@/registry/icons/icon-phosphor").then((mod) => ({
|
||||
default: mod.IconPhosphor,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconRemixicon = lazy(() =>
|
||||
import("@/registry/icons/icon-remixicon").then((mod) => ({
|
||||
default: mod.IconRemixicon,
|
||||
}))
|
||||
)
|
||||
|
||||
const PREVIEW_ICONS = {
|
||||
lucide: [
|
||||
"CopyIcon",
|
||||
@@ -100,38 +88,6 @@ const PREVIEW_ICONS = {
|
||||
"ArrowDown01Icon",
|
||||
"ArrowRight01Icon",
|
||||
],
|
||||
phosphor: [
|
||||
"CopyIcon",
|
||||
"WarningCircleIcon",
|
||||
"TrashIcon",
|
||||
"ShareIcon",
|
||||
"BagIcon",
|
||||
"DotsThreeIcon",
|
||||
"SpinnerIcon",
|
||||
"PlusIcon",
|
||||
"MinusIcon",
|
||||
"ArrowLeftIcon",
|
||||
"ArrowRightIcon",
|
||||
"CheckIcon",
|
||||
"CaretDownIcon",
|
||||
"CaretRightIcon",
|
||||
],
|
||||
remixicon: [
|
||||
"RiFileCopyLine",
|
||||
"RiErrorWarningLine",
|
||||
"RiDeleteBinLine",
|
||||
"RiShareLine",
|
||||
"RiShoppingBagLine",
|
||||
"RiMoreLine",
|
||||
"RiLoaderLine",
|
||||
"RiAddLine",
|
||||
"RiSubtractLine",
|
||||
"RiArrowLeftLine",
|
||||
"RiArrowRightLine",
|
||||
"RiCheckLine",
|
||||
"RiArrowDownSLine",
|
||||
"RiArrowRightSLine",
|
||||
],
|
||||
}
|
||||
|
||||
const logos = {
|
||||
@@ -198,35 +154,6 @@ const logos = {
|
||||
></path>
|
||||
</svg>
|
||||
),
|
||||
phosphor: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32 32"
|
||||
width="32"
|
||||
height="32"
|
||||
>
|
||||
<path fill="none" d="M0 0h32v32H0z" />
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
d="M9 5h9v16H9zm9 16v9a9 9 0 0 1-9-9M9 5l9 16m0 0h1a8 8 0 0 0 0-16h-1"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
remixicon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M12 2C17.5228 2 22 6.47715 22 12C22 15.3137 19.3137 18 16 18C12.6863 18 10 15.3137 10 12C10 11.4477 9.55228 11 9 11C8.44772 11 8 11.4477 8 12C8 16.4183 11.5817 20 16 20C16.8708 20 17.7084 19.8588 18.4932 19.6016C16.7458 21.0956 14.4792 22 12 22C6.6689 22 2.3127 17.8283 2.0166 12.5713C2.23647 9.45772 4.83048 7 8 7C11.3137 7 14 9.68629 14 13C14 13.5523 14.4477 14 15 14C15.5523 14 16 13.5523 16 13C16 8.58172 12.4183 5 8 5C6.50513 5 5.1062 5.41032 3.90918 6.12402C5.72712 3.62515 8.67334 2 12 2Z" />
|
||||
</svg>
|
||||
),
|
||||
}
|
||||
|
||||
export function IconLibraryPicker({
|
||||
@@ -244,8 +171,8 @@ export function IconLibraryPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Icon Library</div>
|
||||
@@ -253,42 +180,42 @@ export function IconLibraryPicker({
|
||||
{currentIconLibrary?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
<div className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
|
||||
{logos[currentIconLibrary?.name as keyof typeof logos]}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
<LockButton
|
||||
param="iconLibrary"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentIconLibrary?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ iconLibrary: value as IconLibraryName })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentIconLibrary?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ iconLibrary: value as IconLibraryName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{Object.values(iconLibraries).map((iconLibrary, index) => (
|
||||
<React.Fragment key={iconLibrary.name}>
|
||||
<IconLibraryPickerItem
|
||||
iconLibrary={iconLibrary}
|
||||
value={iconLibrary.name}
|
||||
/>
|
||||
{index < Object.values(iconLibraries).length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="iconLibrary"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{Object.values(iconLibraries).map((iconLibrary, index) => (
|
||||
<React.Fragment key={iconLibrary.name}>
|
||||
<IconLibraryPickerItem
|
||||
iconLibrary={iconLibrary}
|
||||
value={iconLibrary.name}
|
||||
/>
|
||||
{index < Object.values(iconLibraries).length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -332,11 +259,7 @@ const IconLibraryPreview = memo(function IconLibraryPreview({
|
||||
? IconLucide
|
||||
: iconLibrary === "tabler"
|
||||
? IconTabler
|
||||
: iconLibrary === "hugeicons"
|
||||
? IconHugeicons
|
||||
: iconLibrary === "phosphor"
|
||||
? IconPhosphor
|
||||
: IconRemixicon
|
||||
: IconHugeicons
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
|
||||
@@ -4,7 +4,7 @@ import { lazy, Suspense } from "react"
|
||||
import { SquareIcon } from "lucide-react"
|
||||
import type { IconLibraryName } from "shadcn/icons"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { useDesignSystemParam } from "@/app/(create)/hooks/use-design-system"
|
||||
|
||||
const IconLucide = lazy(() =>
|
||||
import("@/registry/icons/icon-lucide").then((mod) => ({
|
||||
@@ -24,24 +24,12 @@ const IconHugeicons = lazy(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
const IconPhosphor = lazy(() =>
|
||||
import("@/registry/icons/icon-phosphor").then((mod) => ({
|
||||
default: mod.IconPhosphor,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconRemixicon = lazy(() =>
|
||||
import("@/registry/icons/icon-remixicon").then((mod) => ({
|
||||
default: mod.IconRemixicon,
|
||||
}))
|
||||
)
|
||||
|
||||
export function IconPlaceholder({
|
||||
...props
|
||||
}: {
|
||||
[K in IconLibraryName]: string
|
||||
} & React.ComponentProps<"svg">) {
|
||||
const [{ iconLibrary }] = useDesignSystemSearchParams()
|
||||
const iconLibrary = useDesignSystemParam("iconLibrary")
|
||||
const iconName = props[iconLibrary]
|
||||
|
||||
if (!iconName) {
|
||||
@@ -55,12 +43,6 @@ export function IconPlaceholder({
|
||||
{iconLibrary === "hugeicons" && (
|
||||
<IconHugeicons name={iconName} {...props} />
|
||||
)}
|
||||
{iconLibrary === "phosphor" && (
|
||||
<IconPhosphor name={iconName} {...props} />
|
||||
)}
|
||||
{iconLibrary === "remixicon" && (
|
||||
<IconRemixicon name={iconName} {...props} />
|
||||
)}
|
||||
</Suspense>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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-64"
|
||||
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"
|
||||
/>
|
||||
}
|
||||
>
|
||||
@@ -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 xl:w-96"
|
||||
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"
|
||||
side="bottom"
|
||||
align="end"
|
||||
align="center"
|
||||
>
|
||||
<ComboboxInput
|
||||
showTrigger={false}
|
||||
|
||||
@@ -119,8 +119,8 @@ export function MenuColorPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Menu Color</div>
|
||||
@@ -128,36 +128,36 @@ export function MenuColorPicker({
|
||||
{currentMenu?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base select-none">
|
||||
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base">
|
||||
{currentMenu?.icon}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
<LockButton
|
||||
param="menuColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentMenu?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuColor: value as MenuColorValue })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentMenu?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ menuColor: value as MenuColorValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_OPTIONS.map((menu) => (
|
||||
<PickerRadioItem key={menu.value} value={menu.value}>
|
||||
{menu.icon}
|
||||
{menu.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuColor"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{MENU_OPTIONS.map((menu) => (
|
||||
<PickerRadioItem key={menu.value} value={menu.value}>
|
||||
{menu.icon}
|
||||
{menu.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
@@ -138,8 +138,6 @@ function PickerSubTrigger({
|
||||
lucide="ChevronRightIcon"
|
||||
tabler="IconChevronRight"
|
||||
hugeicons="ArrowRight01Icon"
|
||||
phosphor="CaretRightIcon"
|
||||
remixicon="RiArrowRightSLine"
|
||||
className="ml-auto"
|
||||
/>
|
||||
</MenuPrimitive.SubmenuTrigger>
|
||||
@@ -180,7 +178,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}
|
||||
@@ -192,8 +190,6 @@ function PickerCheckboxItem({
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
remixicon="RiCheckLine"
|
||||
/>
|
||||
</MenuPrimitive.CheckboxItemIndicator>
|
||||
</span>
|
||||
@@ -220,7 +216,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}
|
||||
@@ -234,8 +230,6 @@ function PickerRadioItem({
|
||||
lucide="CheckIcon"
|
||||
tabler="IconCheck"
|
||||
hugeicons="Tick02Icon"
|
||||
phosphor="CheckIcon"
|
||||
remixicon="RiCheckLine"
|
||||
className="size-4 pointer-coarse:size-5"
|
||||
/>
|
||||
</MenuPrimitive.RadioItemIndicator>
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type PanelImperativeHandle } from "react-resizable-panels"
|
||||
import { type ImperativePanelHandle } from "react-resizable-panels"
|
||||
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/customizer-controls"
|
||||
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/item-picker"
|
||||
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
|
||||
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import {
|
||||
serializeDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
import { useDesignSystemSync } from "@/app/(create)/hooks/use-design-system"
|
||||
|
||||
const MESSAGE_TYPE = "design-system-params"
|
||||
|
||||
export function Preview() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const params = useDesignSystemSync()
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
const resizablePanelRef = React.useRef<PanelImperativeHandle>(null)
|
||||
const resizablePanelRef = React.useRef<ImperativePanelHandle>(null)
|
||||
const [initialParams] = React.useState(params)
|
||||
const [iframeKey, setIframeKey] = React.useState(0)
|
||||
|
||||
// Sync resizable panel with URL param changes.
|
||||
React.useEffect(() => {
|
||||
@@ -32,7 +32,13 @@ export function Preview() {
|
||||
}
|
||||
|
||||
const sendParams = () => {
|
||||
sendToIframe(iframe, "design-system-params", params)
|
||||
iframe.contentWindow?.postMessage(
|
||||
{
|
||||
type: MESSAGE_TYPE,
|
||||
params,
|
||||
},
|
||||
"*"
|
||||
)
|
||||
}
|
||||
|
||||
if (iframe.contentWindow) {
|
||||
@@ -90,25 +96,18 @@ export function Preview() {
|
||||
}
|
||||
}, [])
|
||||
|
||||
const iframeSrc = React.useMemo(() => {
|
||||
// The iframe src needs to include the serialized design system params
|
||||
// for the initial load, but not be reactive to them as it would cause
|
||||
// full-iframe reloads on every param change (flashes & loss of state).
|
||||
// Further updates of the search params will be sent to the iframe
|
||||
// via a postMessage channel, for it to sync its own history onto the host's.
|
||||
return serializeDesignSystemSearchParams(
|
||||
`/preview/${params.base}/${params.item}`,
|
||||
params
|
||||
)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [params.base, params.item])
|
||||
if (!params.item || !params.base) {
|
||||
return null
|
||||
}
|
||||
|
||||
const iframeSrc = `/preview/${params.base}/${params.item}?theme=${initialParams.theme ?? "neutral"}&iconLibrary=${initialParams.iconLibrary ?? "lucide"}&style=${initialParams.style ?? "vega"}&font=${initialParams.font ?? "inter"}&baseColor=${initialParams.baseColor ?? "neutral"}`
|
||||
|
||||
return (
|
||||
<div className="relative -mx-1 flex flex-1 flex-col justify-center sm:mx-0">
|
||||
<div className="ring-foreground/15 3xl:max-h-[1200px] 3xl:max-w-[1800px] relative -z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden rounded-2xl ring-1">
|
||||
<div className="bg-muted dark:bg-muted/30 absolute inset-0 rounded-2xl" />
|
||||
<iframe
|
||||
key={params.base + params.item}
|
||||
key={`${params.item}-${iframeKey}`}
|
||||
ref={iframeRef}
|
||||
src={iframeSrc}
|
||||
className="z-10 size-full flex-1"
|
||||
|
||||
@@ -27,8 +27,8 @@ export function RadiusPicker({
|
||||
const otherRadii = RADII.filter((radius) => radius.name !== "default")
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Radius</div>
|
||||
@@ -36,7 +36,7 @@ export function RadiusPicker({
|
||||
{currentRadius?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base select-none">
|
||||
<div className="text-foreground absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
@@ -55,47 +55,47 @@ export function RadiusPicker({
|
||||
</svg>
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
<LockButton
|
||||
param="radius"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentRadius?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ radius: value as RadiusValue })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentRadius?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ radius: value as RadiusValue })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{defaultRadius && (
|
||||
<PickerRadioItem
|
||||
key={defaultRadius.name}
|
||||
value={defaultRadius.name}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{defaultRadius.label}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Use radius from style
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{defaultRadius && (
|
||||
<PickerRadioItem
|
||||
key={defaultRadius.name}
|
||||
value={defaultRadius.name}
|
||||
>
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{defaultRadius.label}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Use radius from style
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{otherRadii.map((radius) => (
|
||||
<PickerRadioItem key={radius.name} value={radius.name}>
|
||||
{radius.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="radius"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{otherRadii.map((radius) => (
|
||||
<PickerRadioItem key={radius.name} value={radius.name}>
|
||||
{radius.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Undo02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
AlertDialogCancel,
|
||||
AlertDialogContent,
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/alert-dialog"
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function ResetButton() {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const handleReset = React.useCallback(() => {
|
||||
setParams({
|
||||
base: params.base, // Keep the current base value.
|
||||
style: DEFAULT_CONFIG.style,
|
||||
baseColor: DEFAULT_CONFIG.baseColor,
|
||||
theme: DEFAULT_CONFIG.theme,
|
||||
iconLibrary: DEFAULT_CONFIG.iconLibrary,
|
||||
font: DEFAULT_CONFIG.font,
|
||||
menuAccent: DEFAULT_CONFIG.menuAccent,
|
||||
menuColor: DEFAULT_CONFIG.menuColor,
|
||||
radius: DEFAULT_CONFIG.radius,
|
||||
template: DEFAULT_CONFIG.template,
|
||||
item: "preview",
|
||||
})
|
||||
}, [setParams, params.base])
|
||||
|
||||
return (
|
||||
<AlertDialog>
|
||||
<AlertDialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="border-foreground/10 bg-muted/50 hidden h-[calc(--spacing(13.5))] w-[140px] touch-manipulation justify-between rounded-xl border select-none focus-visible:border-transparent focus-visible:ring-1 sm:rounded-lg md:flex md:w-full md:rounded-lg md:border-transparent md:bg-transparent md:pr-3.5! md:pl-2!"
|
||||
>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Reset</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
Start Over
|
||||
</div>
|
||||
</div>
|
||||
<HugeiconsIcon icon={Undo02Icon} className="-translate-x-0.5" />
|
||||
</Button>
|
||||
</AlertDialogTrigger>
|
||||
<AlertDialogContent className="dialog-ring p-4 sm:max-w-sm">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>Reset to defaults?</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
This will reset all customization options to their default values.
|
||||
This action cannot be undone.
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel className="rounded-lg">Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction className="rounded-lg" onClick={handleReset}>
|
||||
Reset
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export function ShareButton() {
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="rounded-lg shadow-none lg:w-8 xl:w-fit"
|
||||
className="rounded-lg shadow-none"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
@@ -64,7 +64,7 @@ export function ShareButton() {
|
||||
) : (
|
||||
<HugeiconsIcon icon={Share03Icon} strokeWidth={2} />
|
||||
)}
|
||||
<span className="lg:hidden xl:block">Share</span>
|
||||
Share
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Copy Link</TooltipContent>
|
||||
|
||||
@@ -29,8 +29,8 @@ export function StylePicker({
|
||||
const currentStyle = styles.find((style) => style.name === params.style)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Style</div>
|
||||
@@ -39,58 +39,58 @@ export function StylePicker({
|
||||
</div>
|
||||
</div>
|
||||
{currentStyle?.icon && (
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center select-none">
|
||||
<div className="absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center">
|
||||
{React.cloneElement(currentStyle.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="md:w-64"
|
||||
<LockButton
|
||||
param="style"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="md:w-64"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentStyle?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ style: value as StyleName })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentStyle?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ style: value as StyleName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{styles.map((style, index) => (
|
||||
<React.Fragment key={style.name}>
|
||||
<PickerRadioItem value={style.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{style.icon && (
|
||||
<div className="flex size-4 translate-y-0.5 items-center justify-center">
|
||||
{React.cloneElement(style.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{style.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
{style.description}
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{styles.map((style, index) => (
|
||||
<React.Fragment key={style.name}>
|
||||
<PickerRadioItem value={style.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{style.icon && (
|
||||
<div className="flex size-4 translate-y-0.5 items-center justify-center">
|
||||
{React.cloneElement(style.icon, {
|
||||
className: "size-4",
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{style.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
{style.description}
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
{index < styles.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="style"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
{index < styles.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -52,7 +52,7 @@ export function TemplatePicker({
|
||||
</div>
|
||||
{currentTemplate?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 select-none *:[svg]:size-4"
|
||||
className="text-foreground *:[svg]:text-foreground! absolute top-1/2 right-4 size-4 -translate-y-1/2 *:[svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentTemplate.logo,
|
||||
}}
|
||||
|
||||
@@ -47,8 +47,8 @@ export function ThemePicker({
|
||||
}, [currentTheme, themes, setParams])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<Picker>
|
||||
<div className="group/picker relative">
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Theme</div>
|
||||
@@ -68,99 +68,99 @@ export function ThemePicker({
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
|
||||
className="absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-[23rem]"
|
||||
<LockButton
|
||||
param="theme"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-96"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentTheme?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ theme: value as ThemeName })
|
||||
}}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentTheme?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ theme: value as ThemeName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter((theme) =>
|
||||
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter((theme) =>
|
||||
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
|
||||
)
|
||||
.map((theme) => {
|
||||
const isBaseColor = BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
.map((theme) => {
|
||||
const isBaseColor = BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.[
|
||||
isBaseColor ? "muted-foreground" : "primary"
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 translate-y-1 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{theme.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Match base color
|
||||
</div>
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-start gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.[
|
||||
isBaseColor ? "muted-foreground" : "primary"
|
||||
],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 translate-y-1 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
<div className="flex flex-col justify-start pointer-coarse:gap-1">
|
||||
<div>{theme.title}</div>
|
||||
<div className="text-muted-foreground text-xs pointer-coarse:text-sm">
|
||||
Match base color
|
||||
</div>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter(
|
||||
(theme) =>
|
||||
!BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
.map((theme) => {
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["primary"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{theme.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
})}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{themes
|
||||
.filter(
|
||||
(theme) =>
|
||||
!BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="theme"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
.map((theme) => {
|
||||
return (
|
||||
<PickerRadioItem key={theme.name} value={theme.name}>
|
||||
<div className="flex items-center gap-2">
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
theme.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["primary"],
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className="size-4 rounded-full bg-(--color)"
|
||||
/>
|
||||
)}
|
||||
{theme.title}
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
})}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Link from "next/link"
|
||||
import {
|
||||
ComputerTerminal01Icon,
|
||||
Copy01Icon,
|
||||
@@ -24,8 +23,6 @@ import {
|
||||
} from "@/registry/new-york-v4/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldDescription,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldTitle,
|
||||
@@ -34,7 +31,6 @@ import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/radio-group"
|
||||
import { Switch } from "@/registry/new-york-v4/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
@@ -75,15 +71,14 @@ export function ToolbarControls() {
|
||||
const packageManager = config.packageManager || "pnpm"
|
||||
|
||||
const commands = React.useMemo(() => {
|
||||
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 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 templateFlag = params.template ? ` --template ${params.template}` : ""
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
return {
|
||||
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}`,
|
||||
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}`,
|
||||
}
|
||||
}, [
|
||||
params.base,
|
||||
@@ -96,7 +91,6 @@ export function ToolbarControls() {
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
params.template,
|
||||
params.rtl,
|
||||
])
|
||||
|
||||
const command = commands[packageManager]
|
||||
@@ -170,7 +164,7 @@ export function ToolbarControls() {
|
||||
{selectedTemplate?.title} + shadcn/ui project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<FieldGroup className="gap-3">
|
||||
<FieldGroup>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="template" className="sr-only">
|
||||
Template
|
||||
@@ -189,7 +183,7 @@ export function ToolbarControls() {
|
||||
<FieldLabel
|
||||
key={template.value}
|
||||
htmlFor={template.value}
|
||||
className="has-data-[state=checked]:border-primary/10 rounded-lg!"
|
||||
className="rounded-lg!"
|
||||
>
|
||||
<Field className="flex min-w-0 flex-col items-center justify-center gap-2 p-3! text-center *:w-auto!">
|
||||
<RadioGroupItem
|
||||
@@ -211,81 +205,59 @@ export function ToolbarControls() {
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Field>
|
||||
<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>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
</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>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<DialogFooter className="bg-muted/50 -mx-6 mt-2 -mb-6 flex flex-col gap-2 border-t p-6 sm:flex-col">
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@@ -14,12 +14,14 @@ import {
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function V0Button({ className }: { className?: string }) {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const isMobile = useIsMobile()
|
||||
const isMounted = useMounted()
|
||||
|
||||
const url = `${process.env.NEXT_PUBLIC_APP_URL}/create/v0?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}&item=${params.item}`
|
||||
|
||||
console.log(url)
|
||||
|
||||
if (!isMounted) {
|
||||
return <Skeleton className="h-8 w-24 rounded-lg" />
|
||||
}
|
||||
@@ -32,7 +34,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] lg:w-8 xl:w-24",
|
||||
"w-24 rounded-lg shadow-none data-[variant=default]:h-[31px]",
|
||||
className
|
||||
)}
|
||||
asChild
|
||||
@@ -41,8 +43,7 @@ export function V0Button({ className }: { className?: string }) {
|
||||
href={`${process.env.NEXT_PUBLIC_V0_URL}/chat/api/open?url=${encodeURIComponent(url)}&title=${params.item}`}
|
||||
target="_blank"
|
||||
>
|
||||
<span className="lg:hidden xl:block">Open in</span>
|
||||
<Icons.v0 className="size-5" />
|
||||
Open in <Icons.v0 className="size-5" />
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -4,11 +4,7 @@ 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"
|
||||
@@ -16,11 +12,10 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import { Separator } from "@/registry/new-york-v4/ui/separator"
|
||||
import { SidebarProvider } from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { Customizer } from "@/app/(create)/components/customizer"
|
||||
import { CustomizerControls } from "@/app/(create)/components/customizer-controls"
|
||||
import { ItemExplorer } from "@/app/(create)/components/item-explorer"
|
||||
import { ItemPicker } from "@/app/(create)/components/item-picker"
|
||||
import { Preview } from "@/app/(create)/components/preview"
|
||||
import { RandomButton } from "@/app/(create)/components/random-button"
|
||||
import { ResetButton } from "@/app/(create)/components/reset-button"
|
||||
import { ShareButton } from "@/app/(create)/components/share-button"
|
||||
import { ToolbarControls } from "@/app/(create)/components/toolbar-controls"
|
||||
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||
@@ -68,7 +63,6 @@ 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
|
||||
@@ -78,7 +72,6 @@ export default async function CreatePage({
|
||||
title: item.title,
|
||||
type: item.type,
|
||||
}))
|
||||
.filter((item) => !/\d+$/.test(item.name))
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -87,35 +80,36 @@ export default async function CreatePage({
|
||||
>
|
||||
<header className="sticky top-0 z-50 w-full">
|
||||
<div className="container-wrapper 3xl:fixed:px-0 px-6">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:h-4!">
|
||||
<div className="3xl:fixed:container flex h-(--header-height) items-center **:data-[slot=separator]:h-4!">
|
||||
<MobileNav
|
||||
tree={pageTree}
|
||||
items={siteConfig.navItems}
|
||||
className="flex lg:hidden"
|
||||
/>
|
||||
<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">
|
||||
<Button
|
||||
asChild
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="hidden size-8 lg:flex"
|
||||
variant="outline"
|
||||
size="sm"
|
||||
className="rounded-lg shadow-none"
|
||||
>
|
||||
<Link href="/">
|
||||
<Icons.logo className="size-5" />
|
||||
<span className="sr-only">{siteConfig.name}</span>
|
||||
<ArrowLeftIcon />
|
||||
Back
|
||||
</Link>
|
||||
</Button>
|
||||
<MainNav items={siteConfig.navItems} className="hidden lg:flex" />
|
||||
</div>
|
||||
<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 />
|
||||
<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>
|
||||
<Separator orientation="vertical" className="mr-2 flex" />
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end">
|
||||
<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">
|
||||
<ItemPicker items={filteredItems} />
|
||||
<CustomizerControls className="sm:hidden" />
|
||||
<Separator
|
||||
orientation="vertical"
|
||||
className="mr-2 hidden sm:flex xl:hidden"
|
||||
/>
|
||||
</div>
|
||||
<div className="ml-auto flex items-center gap-2 sm:ml-0 md:justify-end xl:ml-auto xl:w-1/3">
|
||||
<SiteConfig className="3xl:flex hidden" />
|
||||
<Separator orientation="vertical" className="3xl:flex hidden" />
|
||||
<ModeSwitcher />
|
||||
|
||||
@@ -45,6 +45,18 @@ 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)
|
||||
|
||||
@@ -63,23 +75,28 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
async function buildV0Payload(designSystemConfig: DesignSystemConfig) {
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
const files: z.infer<typeof registryItemFileSchema>[] = []
|
||||
|
||||
// Build all files in parallel.
|
||||
const [globalsCss, layoutFile, componentFiles] = await Promise.all([
|
||||
buildGlobalsCss(registryBase),
|
||||
buildLayoutFile(designSystemConfig),
|
||||
buildComponentFiles(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)
|
||||
|
||||
return registryItemSchema.parse({
|
||||
name: designSystemConfig.item ?? "Item",
|
||||
type: "registry:item",
|
||||
files: [globalsCss, layoutFile, ...componentFiles],
|
||||
files,
|
||||
})
|
||||
}
|
||||
|
||||
function buildGlobalsCss(registryBase: RegistryItem) {
|
||||
function buildGlobalsCss(designSystemConfig: DesignSystemConfig) {
|
||||
const registryBase = buildRegistryBase(designSystemConfig)
|
||||
|
||||
const lightVars = Object.entries(registryBase.cssVars?.light ?? {})
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
41
apps/v4/app/(create)/hooks/use-canva.tsx
Normal file
41
apps/v4/app/(create)/hooks/use-canva.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
sendToIframe,
|
||||
sendToParent,
|
||||
useParentMessageListener,
|
||||
} from "@/app/(create)/hooks/use-iframe-sync"
|
||||
|
||||
const MESSAGE_TYPE = "canva-zoom"
|
||||
|
||||
export type ZoomCommand =
|
||||
| { type: "ZOOM_IN" }
|
||||
| { type: "ZOOM_OUT" }
|
||||
| { type: "ZOOM_SET"; value: number }
|
||||
| { type: "ZOOM_FIT" }
|
||||
| { type: "RESET" }
|
||||
|
||||
export function sendCanvaZoomCommand(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
command: ZoomCommand
|
||||
) {
|
||||
sendToIframe(iframe, MESSAGE_TYPE, { command })
|
||||
}
|
||||
|
||||
export function sendCanvaZoomUpdate(zoom: number) {
|
||||
sendToParent(MESSAGE_TYPE, { zoom })
|
||||
}
|
||||
|
||||
export function useCanvaZoomSync() {
|
||||
const [zoom, setZoom] = React.useState(1)
|
||||
|
||||
useParentMessageListener<{ zoom: number }>(MESSAGE_TYPE, (data) => {
|
||||
if (typeof data.zoom === "number") {
|
||||
setZoom(data.zoom)
|
||||
}
|
||||
})
|
||||
|
||||
return zoom
|
||||
}
|
||||
44
apps/v4/app/(create)/hooks/use-design-system.tsx
Normal file
44
apps/v4/app/(create)/hooks/use-design-system.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
"use client"
|
||||
|
||||
import {
|
||||
createIframeSyncStore,
|
||||
useIframeSyncAll,
|
||||
useIframeSyncValue,
|
||||
} from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import {
|
||||
loadDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
const MESSAGE_TYPE = "design-system-params"
|
||||
|
||||
const getInitialValues = (): DesignSystemSearchParams => {
|
||||
const initialValues = loadDesignSystemSearchParams(
|
||||
typeof window === "undefined" ? "" : location.search
|
||||
)
|
||||
// Override defaults for iframe use case
|
||||
initialValues.item = "cover-example"
|
||||
return initialValues
|
||||
}
|
||||
|
||||
const designSystemStore = createIframeSyncStore(
|
||||
MESSAGE_TYPE,
|
||||
getInitialValues()
|
||||
)
|
||||
|
||||
export function useDesignSystemSync() {
|
||||
const [urlParams] = useDesignSystemSearchParams()
|
||||
|
||||
const keys = Object.keys(urlParams) as (keyof DesignSystemSearchParams)[]
|
||||
|
||||
return useIframeSyncAll(designSystemStore, keys, urlParams)
|
||||
}
|
||||
|
||||
export function useDesignSystemParam<K extends keyof DesignSystemSearchParams>(
|
||||
key: K
|
||||
) {
|
||||
const [urlParams] = useDesignSystemSearchParams()
|
||||
|
||||
return useIframeSyncValue(designSystemStore, key, urlParams[key])
|
||||
}
|
||||
@@ -1,14 +1,8 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import type { DesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
type ParentToIframeMessage = {
|
||||
type: "design-system-params"
|
||||
data: DesignSystemSearchParams
|
||||
}
|
||||
|
||||
export const isInIframe = () => {
|
||||
if (typeof window === "undefined") {
|
||||
return false
|
||||
@@ -16,21 +10,157 @@ export const isInIframe = () => {
|
||||
return window.self !== window.top
|
||||
}
|
||||
|
||||
export function useIframeMessageListener<
|
||||
Message extends ParentToIframeMessage,
|
||||
MessageType extends Message["type"],
|
||||
>(
|
||||
messageType: MessageType,
|
||||
onMessage: (data: Extract<Message, { type: MessageType }>["data"]) => void
|
||||
export function createIframeSyncStore<T extends Record<string, any>>(
|
||||
messageType: string,
|
||||
defaultValues: T
|
||||
) {
|
||||
const store = new Map<keyof T, any>()
|
||||
const listeners = new Map<keyof T, Set<() => void>>()
|
||||
|
||||
if (typeof window !== "undefined") {
|
||||
Object.entries(defaultValues).forEach(([key, value]) => {
|
||||
store.set(key as keyof T, value)
|
||||
})
|
||||
}
|
||||
|
||||
if (typeof window !== "undefined" && isInIframe()) {
|
||||
window.addEventListener("message", (event: MessageEvent) => {
|
||||
if (event.data.type === messageType && event.data.params) {
|
||||
Object.keys(event.data.params).forEach((key) => {
|
||||
const newValue = event.data.params[key]
|
||||
const oldValue = store.get(key as keyof T)
|
||||
|
||||
if (newValue !== oldValue) {
|
||||
store.set(key as keyof T, newValue)
|
||||
|
||||
const keyListeners = listeners.get(key as keyof T)
|
||||
if (keyListeners) {
|
||||
keyListeners.forEach((listener) => listener())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
store,
|
||||
listeners,
|
||||
subscribe(key: keyof T, callback: () => void) {
|
||||
if (!isInIframe()) return () => {}
|
||||
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set())
|
||||
}
|
||||
const keyListeners = listeners.get(key)!
|
||||
keyListeners.add(callback)
|
||||
|
||||
return () => {
|
||||
keyListeners.delete(callback)
|
||||
if (keyListeners.size === 0) {
|
||||
listeners.delete(key)
|
||||
}
|
||||
}
|
||||
},
|
||||
subscribeAll(keys: (keyof T)[], callback: () => void) {
|
||||
if (!isInIframe()) return () => {}
|
||||
|
||||
keys.forEach((key) => {
|
||||
if (!listeners.has(key)) {
|
||||
listeners.set(key, new Set())
|
||||
}
|
||||
listeners.get(key)!.add(callback)
|
||||
})
|
||||
|
||||
return () => {
|
||||
keys.forEach((key) => {
|
||||
const keyListeners = listeners.get(key)
|
||||
if (keyListeners) {
|
||||
keyListeners.delete(callback)
|
||||
if (keyListeners.size === 0) {
|
||||
listeners.delete(key)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
},
|
||||
get(key: keyof T) {
|
||||
return store.get(key) as T[keyof T]
|
||||
},
|
||||
getAll() {
|
||||
const result = {} as T
|
||||
store.forEach((value, key) => {
|
||||
result[key as keyof T] = value
|
||||
})
|
||||
return result
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function useIframeSyncValue<T>(
|
||||
store: ReturnType<typeof createIframeSyncStore<any>>,
|
||||
key: string,
|
||||
urlValue: T
|
||||
) {
|
||||
const subscribe = React.useCallback(
|
||||
(callback: () => void) => {
|
||||
return store.subscribe(key, callback)
|
||||
},
|
||||
[store, key]
|
||||
)
|
||||
|
||||
const getSnapshot = React.useCallback(() => {
|
||||
if (!isInIframe()) {
|
||||
return urlValue
|
||||
}
|
||||
return store.get(key) as T
|
||||
}, [store, key, urlValue])
|
||||
|
||||
const getServerSnapshot = React.useCallback(() => {
|
||||
return urlValue
|
||||
}, [urlValue])
|
||||
|
||||
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
||||
}
|
||||
|
||||
export function useIframeSyncAll<T extends Record<string, any>>(
|
||||
store: ReturnType<typeof createIframeSyncStore<T>>,
|
||||
keys: (keyof T)[],
|
||||
urlValues: T
|
||||
) {
|
||||
const subscribe = React.useCallback(
|
||||
(callback: () => void) => {
|
||||
return store.subscribeAll(keys, callback)
|
||||
},
|
||||
[store, keys]
|
||||
)
|
||||
|
||||
const getSnapshot = React.useCallback(() => {
|
||||
if (!isInIframe()) {
|
||||
return urlValues
|
||||
}
|
||||
return store.getAll()
|
||||
}, [store, urlValues])
|
||||
|
||||
const getServerSnapshot = React.useCallback(() => {
|
||||
return urlValues
|
||||
}, [urlValues])
|
||||
|
||||
return React.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot)
|
||||
}
|
||||
|
||||
export function useParentMessageListener<T>(
|
||||
messageType: string,
|
||||
onMessage: (data: T) => void
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
if (!isInIframe()) {
|
||||
if (isInIframe()) {
|
||||
return
|
||||
}
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === messageType) {
|
||||
onMessage(event.data.data)
|
||||
onMessage(event.data)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,22 +171,33 @@ export function useIframeMessageListener<
|
||||
}, [messageType, onMessage])
|
||||
}
|
||||
|
||||
export function sendToIframe<
|
||||
Message extends ParentToIframeMessage,
|
||||
MessageType extends Message["type"],
|
||||
>(
|
||||
export function sendToIframe(
|
||||
iframe: HTMLIFrameElement | null,
|
||||
messageType: MessageType,
|
||||
data: Extract<Message, { type: MessageType }>["data"]
|
||||
messageType: string,
|
||||
data: any
|
||||
) {
|
||||
if (!iframe?.contentWindow) {
|
||||
if (!iframe || !iframe.contentWindow) {
|
||||
return
|
||||
}
|
||||
|
||||
iframe.contentWindow.postMessage(
|
||||
{
|
||||
type: messageType,
|
||||
data,
|
||||
...data,
|
||||
},
|
||||
"*"
|
||||
)
|
||||
}
|
||||
|
||||
export function sendToParent(messageType: string, data: any) {
|
||||
if (!isInIframe()) {
|
||||
return
|
||||
}
|
||||
|
||||
window.parent.postMessage(
|
||||
{
|
||||
type: messageType,
|
||||
...data,
|
||||
},
|
||||
"*"
|
||||
)
|
||||
|
||||
@@ -19,7 +19,6 @@ 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",
|
||||
value: "geist",
|
||||
font: geistSans,
|
||||
type: "sans",
|
||||
},
|
||||
// {
|
||||
// name: "Geist Sans",
|
||||
// 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]
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useQueryStates } from "nuqs"
|
||||
import {
|
||||
createLoader,
|
||||
createSerializer,
|
||||
parseAsBoolean,
|
||||
parseAsInteger,
|
||||
parseAsString,
|
||||
@@ -36,7 +35,7 @@ const designSystemSearchParams = {
|
||||
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
|
||||
DEFAULT_CONFIG.base
|
||||
),
|
||||
item: parseAsString.withDefault("preview").withOptions({ shallow: true }),
|
||||
item: parseAsString.withDefault("preview"),
|
||||
iconLibrary: parseAsStringLiteral<IconLibraryName>(
|
||||
Object.values(iconLibraries).map((i) => i.name)
|
||||
).withDefault(DEFAULT_CONFIG.iconLibrary),
|
||||
@@ -66,7 +65,6 @@ const designSystemSearchParams = {
|
||||
"start",
|
||||
"vite",
|
||||
] as const).withDefault("next"),
|
||||
rtl: parseAsBoolean.withDefault(false),
|
||||
size: parseAsInteger.withDefault(100),
|
||||
custom: parseAsBoolean.withDefault(false),
|
||||
}
|
||||
@@ -75,10 +73,6 @@ export const loadDesignSystemSearchParams = createLoader(
|
||||
designSystemSearchParams
|
||||
)
|
||||
|
||||
export const serializeDesignSystemSearchParams = createSerializer(
|
||||
designSystemSearchParams
|
||||
)
|
||||
|
||||
export const useDesignSystemSearchParams = (options: Options = {}) =>
|
||||
useQueryStates(designSystemSearchParams, {
|
||||
shallow: false,
|
||||
|
||||
@@ -7,10 +7,10 @@ import { absoluteUrl } from "@/lib/utils"
|
||||
import { DarkModeScript } from "@/components/mode-switcher"
|
||||
import { TailwindIndicator } from "@/components/tailwind-indicator"
|
||||
import { BASES, type Base } from "@/registry/config"
|
||||
import { RandomizeScript } from "@/app/(create)/components/customizer-controls"
|
||||
import { DesignSystemProvider } from "@/app/(create)/components/design-system-provider"
|
||||
import { ItemPickerScript } from "@/app/(create)/components/item-picker"
|
||||
import { PreviewStyle } from "@/app/(create)/components/preview-style"
|
||||
import { RandomizeScript } from "@/app/(create)/components/random-button"
|
||||
import { getBaseComponent, getBaseItem } from "@/app/(create)/lib/api"
|
||||
import { ALLOWED_ITEM_TYPES } from "@/app/(create)/lib/constants"
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export function ExampleForm() {
|
||||
password: "",
|
||||
},
|
||||
validators: {
|
||||
onBlur: exampleFormSchema,
|
||||
onChange: exampleFormSchema,
|
||||
},
|
||||
onSubmit: async ({ value }) => {
|
||||
setValues(value)
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { TrashIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -8,7 +6,6 @@ import {
|
||||
AlertDialogDescription,
|
||||
AlertDialogFooter,
|
||||
AlertDialogHeader,
|
||||
AlertDialogMedia,
|
||||
AlertDialogTitle,
|
||||
AlertDialogTrigger,
|
||||
} from "@/registry/new-york-v4/ui/alert-dialog"
|
||||
@@ -16,66 +13,23 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function AlertDialogDemo() {
|
||||
return (
|
||||
<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>
|
||||
<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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,173 +1,89 @@
|
||||
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-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>
|
||||
<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>
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
{/* Fallback. */}
|
||||
<div className="flex flex-row flex-wrap items-center gap-4">
|
||||
<Avatar size="sm">
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<Avatar>
|
||||
<AvatarImage
|
||||
src="https://github.com/maxleiter.png"
|
||||
alt="@maxleiter"
|
||||
/>
|
||||
<AvatarFallback>LR</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar>
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<AvatarImage
|
||||
src="https://github.com/evilrabbit.png"
|
||||
alt="@evilrabbit"
|
||||
/>
|
||||
<AvatarFallback>ER</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
{/* 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>
|
||||
<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" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
<AvatarBadge />
|
||||
</Avatar>
|
||||
<Avatar size="lg">
|
||||
<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>
|
||||
</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">
|
||||
<Avatar>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -10,10 +10,6 @@ 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
|
||||
@@ -59,16 +55,6 @@ 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, PlusIcon, SendIcon } from "lucide-react"
|
||||
import { ArrowRightIcon, Loader2Icon, SendIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
@@ -6,25 +6,22 @@ export function ButtonDemo() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<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">
|
||||
<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 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>
|
||||
@@ -46,21 +43,10 @@ export function ButtonDemo() {
|
||||
<Button variant="outline" size="sm">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
</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">
|
||||
<Button variant="outline" size="sm">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button disabled variant="outline">
|
||||
<Button disabled size="sm" variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
@@ -85,19 +71,12 @@ export function ButtonDemo() {
|
||||
<Button variant="outline" size="lg">
|
||||
<SendIcon /> Send
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2 md:flex-row">
|
||||
<Button size="icon-xs" variant="outline">
|
||||
<PlusIcon />
|
||||
<Button variant="outline" size="lg">
|
||||
Learn More <ArrowRightIcon />
|
||||
</Button>
|
||||
<Button size="icon-sm" variant="outline">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Button size="icon" variant="outline">
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
<Button size="icon-lg" variant="outline">
|
||||
<PlusIcon />
|
||||
<Button disabled size="lg" variant="outline">
|
||||
<Loader2Icon className="animate-spin" />
|
||||
Please wait
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,183 +1,405 @@
|
||||
"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 {
|
||||
Combobox,
|
||||
ComboboxChip,
|
||||
ComboboxChips,
|
||||
ComboboxChipsInput,
|
||||
ComboboxCollection,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxLabel,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
useComboboxAnchor,
|
||||
} from "@/registry/new-york-v4/ui/combobox"
|
||||
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"
|
||||
|
||||
const frameworks = [
|
||||
"Next.js",
|
||||
"SvelteKit",
|
||||
"Nuxt.js",
|
||||
"Remix",
|
||||
"Astro",
|
||||
{
|
||||
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",
|
||||
},
|
||||
] as const
|
||||
|
||||
type User = (typeof users)[number]
|
||||
|
||||
const timezones = [
|
||||
{
|
||||
value: "Americas",
|
||||
items: ["(GMT-5) New York", "(GMT-8) Los Angeles", "(GMT-6) Chicago"],
|
||||
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: "Europe",
|
||||
items: ["(GMT+0) London", "(GMT+1) Paris", "(GMT+1) Berlin"],
|
||||
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: "Asia/Pacific",
|
||||
items: ["(GMT+9) Tokyo", "(GMT+8) Shanghai", "(GMT+8) Singapore"],
|
||||
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" },
|
||||
],
|
||||
},
|
||||
] as const
|
||||
|
||||
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" },
|
||||
]
|
||||
type Timezone = (typeof timezones)[number]
|
||||
|
||||
export function ComboboxDemo() {
|
||||
return (
|
||||
<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 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>
|
||||
)
|
||||
}
|
||||
|
||||
function ComboboxMultiple() {
|
||||
const anchor = useComboboxAnchor()
|
||||
function FrameworkCombobox({ frameworks }: { frameworks: Framework[] }) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [value, setValue] = React.useState("")
|
||||
|
||||
return (
|
||||
<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>
|
||||
))}
|
||||
<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>
|
||||
<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>
|
||||
))}
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -117,8 +117,6 @@ function DropdownMenuCheckboxes() {
|
||||
lucide="UserIcon"
|
||||
tabler="IconUser"
|
||||
hugeicons="UserIcon"
|
||||
phosphor="UserIcon"
|
||||
remixicon="RiUserLine"
|
||||
/>
|
||||
Profile
|
||||
</DropdownMenuItem>
|
||||
@@ -127,8 +125,6 @@ function DropdownMenuCheckboxes() {
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
@@ -137,8 +133,6 @@ function DropdownMenuCheckboxes() {
|
||||
lucide="SettingsIcon"
|
||||
tabler="IconSettings"
|
||||
hugeicons="SettingsIcon"
|
||||
phosphor="GearIcon"
|
||||
remixicon="RiSettings3Line"
|
||||
/>
|
||||
Settings
|
||||
</DropdownMenuItem>
|
||||
@@ -173,8 +167,6 @@ function DropdownMenuCheckboxes() {
|
||||
lucide="LogOutIcon"
|
||||
tabler="IconLogout"
|
||||
hugeicons="LogoutIcon"
|
||||
phosphor="SignOutIcon"
|
||||
remixicon="RiLogoutBoxLine"
|
||||
/>
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
@@ -230,8 +222,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="ChevronsUpDownIcon"
|
||||
tabler="IconChevronsUpDown"
|
||||
hugeicons="ChevronUpDownIcon"
|
||||
phosphor="CaretUpDownIcon"
|
||||
remixicon="RiExpandUpDownLine"
|
||||
className="text-muted-foreground ml-auto"
|
||||
/>
|
||||
</Button>
|
||||
@@ -261,8 +251,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="SparklesIcon"
|
||||
tabler="IconSparkles"
|
||||
hugeicons="SparklesIcon"
|
||||
phosphor="SparklesIcon"
|
||||
remixicon="RiSparklingLine"
|
||||
/>
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
@@ -274,8 +262,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="BadgeCheckIcon"
|
||||
tabler="IconBadgeCheck"
|
||||
hugeicons="BadgeCheckIcon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiVerifiedBadgeLine"
|
||||
/>
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
@@ -284,8 +270,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
@@ -294,8 +278,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="BellIcon"
|
||||
tabler="IconBell"
|
||||
hugeicons="BellIcon"
|
||||
phosphor="BellIcon"
|
||||
remixicon="RiNotification3Line"
|
||||
/>
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
@@ -306,8 +288,6 @@ function DropdownMenuWithAvatar() {
|
||||
lucide="LogOutIcon"
|
||||
tabler="IconLogout"
|
||||
hugeicons="LogoutIcon"
|
||||
phosphor="SignOutIcon"
|
||||
remixicon="RiLogoutBoxLine"
|
||||
/>
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
@@ -361,8 +341,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="SparklesIcon"
|
||||
tabler="IconSparkles"
|
||||
hugeicons="SparklesIcon"
|
||||
phosphor="SparklesIcon"
|
||||
remixicon="RiSparklingLine"
|
||||
/>
|
||||
Upgrade to Pro
|
||||
</DropdownMenuItem>
|
||||
@@ -374,8 +352,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="BadgeCheckIcon"
|
||||
tabler="IconBadgeCheck"
|
||||
hugeicons="BadgeCheckIcon"
|
||||
phosphor="CheckCircleIcon"
|
||||
remixicon="RiVerifiedBadgeLine"
|
||||
/>
|
||||
Account
|
||||
</DropdownMenuItem>
|
||||
@@ -384,8 +360,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="CreditCardIcon"
|
||||
tabler="IconCreditCard"
|
||||
hugeicons="CreditCardIcon"
|
||||
phosphor="CreditCardIcon"
|
||||
remixicon="RiBankCardLine"
|
||||
/>
|
||||
Billing
|
||||
</DropdownMenuItem>
|
||||
@@ -394,8 +368,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="BellIcon"
|
||||
tabler="IconBell"
|
||||
hugeicons="BellIcon"
|
||||
phosphor="BellIcon"
|
||||
remixicon="RiNotification3Line"
|
||||
/>
|
||||
Notifications
|
||||
</DropdownMenuItem>
|
||||
@@ -406,8 +378,6 @@ function DropdownMenuAvatarOnly() {
|
||||
lucide="LogOutIcon"
|
||||
tabler="IconLogout"
|
||||
hugeicons="LogoutIcon"
|
||||
phosphor="SignOutIcon"
|
||||
remixicon="RiLogoutBoxLine"
|
||||
/>
|
||||
Sign Out
|
||||
</DropdownMenuItem>
|
||||
@@ -425,8 +395,6 @@ function DropdownMenuIconColor() {
|
||||
lucide="MoreHorizontalIcon"
|
||||
tabler="IconDots"
|
||||
hugeicons="MoreHorizontalCircle01Icon"
|
||||
phosphor="DotsThreeOutlineIcon"
|
||||
remixicon="RiMoreLine"
|
||||
/>
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
@@ -438,8 +406,6 @@ function DropdownMenuIconColor() {
|
||||
lucide="PencilIcon"
|
||||
tabler="IconPencil"
|
||||
hugeicons="EditIcon"
|
||||
phosphor="PencilIcon"
|
||||
remixicon="RiPencilLine"
|
||||
/>
|
||||
Edit
|
||||
</DropdownMenuItem>
|
||||
@@ -448,8 +414,6 @@ function DropdownMenuIconColor() {
|
||||
lucide="ShareIcon"
|
||||
tabler="IconShare"
|
||||
hugeicons="ShareIcon"
|
||||
phosphor="ShareIcon"
|
||||
remixicon="RiShareLine"
|
||||
/>
|
||||
Share
|
||||
</DropdownMenuItem>
|
||||
@@ -459,8 +423,6 @@ function DropdownMenuIconColor() {
|
||||
lucide="TrashIcon"
|
||||
tabler="IconTrash"
|
||||
hugeicons="DeleteIcon"
|
||||
phosphor="TrashIcon"
|
||||
remixicon="RiDeleteBinLine"
|
||||
/>
|
||||
Delete
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -4,64 +4,59 @@ 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 (
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -8,24 +8,24 @@ export function ResizableDemo() {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-6">
|
||||
<ResizablePanelGroup
|
||||
orientation="horizontal"
|
||||
direction="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 orientation="vertical">
|
||||
<ResizablePanel defaultSize="25%">
|
||||
<ResizablePanel defaultSize={50}>
|
||||
<ResizablePanelGroup direction="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
|
||||
orientation="horizontal"
|
||||
direction="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
|
||||
orientation="vertical"
|
||||
direction="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,27 +47,6 @@ 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,17 +4,6 @@ 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,45 +101,6 @@ 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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,13 +1,5 @@
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
export function ComponentPreview({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"bg-background *:data-[slot=card]:has-[[data-slot=chart]]:shadow-none"
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
return <div className={cn("bg-background")}>{children}</div>
|
||||
}
|
||||
|
||||
@@ -4,16 +4,10 @@ import { type Metadata } from "next"
|
||||
import { notFound } from "next/navigation"
|
||||
|
||||
import { siteConfig } from "@/lib/config"
|
||||
import {
|
||||
getDemoItem,
|
||||
getRegistryComponent,
|
||||
getRegistryItem,
|
||||
} from "@/lib/registry"
|
||||
import { getRegistryComponent, getRegistryItem } from "@/lib/registry"
|
||||
import { absoluteUrl } from "@/lib/utils"
|
||||
import { getStyle, legacyStyles, type Style } from "@/registry/_legacy-styles"
|
||||
|
||||
import "@/styles/legacy-themes.css"
|
||||
|
||||
import { ComponentPreview } from "./component-preview"
|
||||
|
||||
export const revalidate = false
|
||||
@@ -22,12 +16,7 @@ export const dynamicParams = false
|
||||
|
||||
const getCachedRegistryItem = React.cache(
|
||||
async (name: string, styleName: Style["name"]) => {
|
||||
// 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)
|
||||
return await getRegistryItem(name, styleName)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -84,54 +73,9 @@ 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
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user