mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-15 03:41:33 +00:00
Compare commits
2 Commits
shadcn@4.1
...
shadcn/rhe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bec77bccbf | ||
|
|
c2f88d459d |
@@ -1,22 +0,0 @@
|
||||
---
|
||||
description: Keep registry base and radix trees in sync when editing shared UI
|
||||
globs: apps/v4/registry/bases/**/*
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
# Registry bases: Base UI ↔ Radix parity
|
||||
|
||||
`apps/v4/registry/bases/base` and `apps/v4/registry/bases/radix` are **parallel registries**. Anything that exists in both trees for the same purpose (preview blocks, mirrored examples, shared card layouts, etc.) **must stay in sync**.
|
||||
|
||||
## When editing
|
||||
|
||||
- If you change a file under **`bases/base/...`**, apply the **same behavioral and visual change** to the matching path under **`bases/radix/...`** (and the reverse).
|
||||
- Only diverge where APIs differ (e.g. import paths like `@/registry/bases/base/ui/*` vs `@/registry/bases/radix/ui/*`, or Base UI vs Radix component props).
|
||||
- Do **not** update only one side unless the user explicitly asks for a single-base change.
|
||||
|
||||
## Typical mirrored paths
|
||||
|
||||
- `blocks/preview/**` — preview cards and blocks
|
||||
- Parallel `ui/*` components when both exist for the same component
|
||||
|
||||
After edits, briefly confirm both trees were updated (or state why one side is intentionally unchanged).
|
||||
40
.github/dependabot.yml
vendored
40
.github/dependabot.yml
vendored
@@ -4,43 +4,3 @@ updates:
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/astro-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/next-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/react-router-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/start-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-app"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/templates/vite-monorepo"
|
||||
schedule:
|
||||
interval: "weekly"
|
||||
|
||||
3
.github/workflows/code-check.yml
vendored
3
.github/workflows/code-check.yml
vendored
@@ -77,9 +77,6 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm --filter=shadcn build
|
||||
|
||||
- run: pnpm format:check
|
||||
|
||||
tsc:
|
||||
|
||||
78
.github/workflows/deprecated.yml
vendored
Normal file
78
.github/workflows/deprecated.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
name: Deprecated
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [opened, synchronize]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
contents: read
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
deprecated:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout PR
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ github.event.pull_request.head.sha }}
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Get changed files
|
||||
id: changed-files
|
||||
uses: tj-actions/changed-files@v46
|
||||
with:
|
||||
files: |
|
||||
apps/www/**
|
||||
files_ignore: |
|
||||
apps/www/public/r/**
|
||||
base_sha: ${{ github.event.pull_request.base.sha }}
|
||||
sha: ${{ github.event.pull_request.head.sha }}
|
||||
|
||||
- name: Comment on PR if www files changed
|
||||
if: steps.changed-files.outputs.any_changed == 'true'
|
||||
uses: actions/github-script@v7
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const changedFiles = `${{ steps.changed-files.outputs.all_changed_files }}`.split(' ');
|
||||
const wwwFiles = changedFiles.filter(file =>
|
||||
file.startsWith('apps/www/') &&
|
||||
!file.startsWith('apps/www/public/r/') &&
|
||||
file !== 'apps/www/package.json'
|
||||
);
|
||||
|
||||
if (wwwFiles.length > 0) {
|
||||
const comment = `Looks like this PR modifies files in \`apps/www\`, which is deprecated.
|
||||
|
||||
Consider applying the change to \`apps/v4\` if relevant.`;
|
||||
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
body: comment
|
||||
});
|
||||
|
||||
// Add deprecated label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
labels: ['deprecated']
|
||||
});
|
||||
} else {
|
||||
// Remove deprecated label if no www files are changed
|
||||
try {
|
||||
await github.rest.issues.removeLabel({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.issue.number,
|
||||
name: 'deprecated'
|
||||
});
|
||||
} catch (error) {
|
||||
// Label doesn't exist, which is fine
|
||||
console.log('Deprecated label not found, skipping removal');
|
||||
}
|
||||
}
|
||||
5
.github/workflows/test.yml
vendored
5
.github/workflows/test.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Install Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 22
|
||||
node-version: 20
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
@@ -42,4 +42,7 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build packages
|
||||
run: pnpm build --filter=shadcn
|
||||
|
||||
- run: pnpm test
|
||||
|
||||
81
.github/workflows/validate-registries.yml
vendored
81
.github/workflows/validate-registries.yml
vendored
@@ -4,53 +4,13 @@ on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
paths:
|
||||
- "apps/v4/public/r/registries.json"
|
||||
- "apps/v4/registry/directory.json"
|
||||
|
||||
jobs:
|
||||
check-registry-sync:
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
name: check-registry-sync
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
steps:
|
||||
- name: Check changed files
|
||||
id: changed
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
CHANGED_FILES=$(gh pr diff ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --name-only)
|
||||
|
||||
DIRECTORY_CHANGED=false
|
||||
REGISTRIES_CHANGED=false
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "^apps/v4/registry/directory.json$"; then
|
||||
DIRECTORY_CHANGED=true
|
||||
fi
|
||||
|
||||
if echo "$CHANGED_FILES" | grep -q "^apps/v4/public/r/registries.json$"; then
|
||||
REGISTRIES_CHANGED=true
|
||||
fi
|
||||
|
||||
echo "directory_changed=$DIRECTORY_CHANGED" >> $GITHUB_OUTPUT
|
||||
echo "registries_changed=$REGISTRIES_CHANGED" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Flag missing registries.json update
|
||||
if: steps.changed.outputs.directory_changed == 'true' && steps.changed.outputs.registries_changed == 'false'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh pr edit ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --add-label "registries: invalid"
|
||||
gh pr comment ${{ github.event.pull_request.number }} --repo ${{ github.repository }} --body "can you run \`pnpm registry:build\` and commit the json files please?"
|
||||
exit 1
|
||||
|
||||
validate:
|
||||
runs-on: ubuntu-latest
|
||||
name: pnpm validate:registries
|
||||
@@ -66,44 +26,6 @@ jobs:
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
- name: Block reserved registry namespaces
|
||||
env:
|
||||
RESERVED_NAMESPACES: "@shadcn,@ui,@blocks,@components,@block,@component,@util,@utils,@registry,@lib,@hook,@hooks,@theme,@themes,@chart,@charts"
|
||||
run: |
|
||||
node <<'EOF'
|
||||
const fs = require("node:fs")
|
||||
|
||||
const files = [
|
||||
"apps/v4/public/r/registries.json",
|
||||
"apps/v4/registry/directory.json",
|
||||
]
|
||||
const reservedNamespaces = new Set(
|
||||
process.env.RESERVED_NAMESPACES.split(",").filter(Boolean)
|
||||
)
|
||||
|
||||
function readNames(filePath) {
|
||||
return JSON.parse(fs.readFileSync(filePath, "utf8")).map(
|
||||
(entry) => entry.name
|
||||
)
|
||||
}
|
||||
|
||||
const violations = files.flatMap((filePath) => {
|
||||
return readNames(filePath)
|
||||
.filter((name) => reservedNamespaces.has(name))
|
||||
.map((name) => `${filePath}: ${name}`)
|
||||
})
|
||||
|
||||
if (violations.length > 0) {
|
||||
console.error("Reserved registry namespaces are not allowed:")
|
||||
|
||||
for (const violation of violations) {
|
||||
console.error(`- ${violation}`)
|
||||
}
|
||||
|
||||
process.exit(1)
|
||||
}
|
||||
EOF
|
||||
|
||||
- uses: pnpm/action-setup@v4
|
||||
name: Install pnpm
|
||||
id: pnpm-install
|
||||
@@ -125,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
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -15,7 +15,6 @@ build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.eslintcache
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
@@ -42,6 +41,3 @@ tsconfig.tsbuildinfo
|
||||
.vscode
|
||||
|
||||
.notes
|
||||
.playwright-mcp
|
||||
shadcn-workspace
|
||||
.codex-artifacts
|
||||
|
||||
@@ -5,4 +5,3 @@ build
|
||||
.contentlayer
|
||||
**/fixtures
|
||||
deprecated
|
||||
apps/v4/registry/styles/**/*.css
|
||||
|
||||
@@ -6,7 +6,7 @@ A set of beautifully designed components that you can customize, extend, and bui
|
||||
|
||||
## Documentation
|
||||
|
||||
Visit https://ui.shadcn.com/docs to view the documentation.
|
||||
Visit http://ui.shadcn.com/docs to view the documentation.
|
||||
|
||||
## Contributing
|
||||
|
||||
@@ -14,4 +14,4 @@ Please read the [contributing guide](/CONTRIBUTING.md).
|
||||
|
||||
## License
|
||||
|
||||
Licensed under the [MIT license](./LICENSE.md).
|
||||
Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md).
|
||||
|
||||
@@ -5,4 +5,3 @@ build
|
||||
.contentlayer
|
||||
registry/__index__.tsx
|
||||
content/docs/components/calendar.mdx
|
||||
registry/styles/**/*.css
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@@ -15,10 +13,11 @@ import {
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Input } from "@/styles/radix-nova/ui/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/radix-nova/ui/radio-group"
|
||||
import { Switch } from "@/styles/radix-nova/ui/switch"
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
import { RadioGroup, RadioGroupItem } from "@/examples/radix/ui/radio-group"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
export function AppearanceSettings() {
|
||||
const [gpuCount, setGpuCount] = React.useState(8)
|
||||
@@ -94,7 +93,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 w-14! font-mono"
|
||||
className="h-7 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,20 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -27,7 +15,18 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
export function ButtonGroupDemo() {
|
||||
const [label, setLabel] = React.useState("personal")
|
||||
|
||||
@@ -1,21 +1,20 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
export function ButtonGroupInputGroup() {
|
||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
|
||||
export function ButtonGroupNested() {
|
||||
return (
|
||||
<ButtonGroup>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
import { Separator } from "@/styles/radix-nova/ui/separator"
|
||||
import { Textarea } from "@/styles/radix-nova/ui/textarea"
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
export function ButtonGroupPopover() {
|
||||
return (
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/styles/radix-nova/ui/avatar"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
} from "@/examples/radix/ui/avatar"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -14,7 +12,8 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/radix-nova/ui/empty"
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
export function EmptyAvatarGroup() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
|
||||
export function FieldCheckbox() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,8 +8,8 @@ import {
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Input } from "@/styles/radix-nova/ui/input"
|
||||
} from "@/examples/radix/ui/field"
|
||||
import { Input } from "@/examples/radix/ui/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -17,8 +17,8 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/radix-nova/ui/select"
|
||||
import { Textarea } from "@/styles/radix-nova/ui/textarea"
|
||||
} from "@/examples/radix/ui/select"
|
||||
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||
|
||||
export function FieldDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Card, CardContent } from "@/styles/radix-nova/ui/card"
|
||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
||||
import { Card, CardContent } from "@/examples/radix/ui/card"
|
||||
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
} from "@/examples/radix/ui/field"
|
||||
|
||||
const options = [
|
||||
{
|
||||
@@ -46,7 +46,7 @@ export function FieldHear() {
|
||||
<FieldLabel
|
||||
htmlFor={option.value}
|
||||
key={option.value}
|
||||
className="w-fit!"
|
||||
className="!w-fit"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
|
||||
@@ -1,13 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/styles/radix-nova/ui/field"
|
||||
import { Slider } from "@/styles/radix-nova/ui/slider"
|
||||
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
|
||||
import { Slider } from "@/examples/radix/ui/slider"
|
||||
|
||||
export function FieldSlider() {
|
||||
const [value, setValue] = useState([200, 800])
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { FieldSeparator } from "@/styles/radix-nova/ui/field"
|
||||
import { FieldSeparator } from "@/examples/radix/ui/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
@@ -19,7 +19,7 @@ import { SpinnerEmpty } from "./spinner-empty"
|
||||
|
||||
export function RootComponents() {
|
||||
return (
|
||||
<div className="mx-auto grid gap-8 py-1 theme-container md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="theme-container mx-auto grid gap-8 py-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xl:gap-6 2xl:gap-8">
|
||||
<div className="flex flex-col gap-6 *:[div]:w-full *:[div]:max-w-full">
|
||||
<FieldDemo />
|
||||
</div>
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import { Label } from "@/styles/radix-nova/ui/label"
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Label } from "@/examples/radix/ui/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
export function InputGroupButtonExample() {
|
||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||
@@ -25,7 +24,7 @@ export function InputGroupButtonExample() {
|
||||
Input Secure
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-19" className="pl-0.5!" />
|
||||
<InputGroupInput id="input-secure-19" className="!pl-0.5" />
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<InputGroupAddon>
|
||||
@@ -47,7 +46,7 @@ export function InputGroupButtonExample() {
|
||||
<p>You should not enter any sensitive information on this site.</p>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<InputGroupAddon className="pl-1! text-muted-foreground">
|
||||
<InputGroupAddon className="text-muted-foreground !pl-1">
|
||||
https://
|
||||
</InputGroupAddon>
|
||||
<InputGroupAddon align="inline-end">
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
@@ -14,13 +11,15 @@ import {
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
import { Separator } from "@/styles/radix-nova/ui/separator"
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import { Separator } from "@/examples/radix/ui/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
export function InputGroupDemo() {
|
||||
return (
|
||||
@@ -33,7 +32,7 @@ export function InputGroupDemo() {
|
||||
<InputGroupAddon align="inline-end">12 results</InputGroupAddon>
|
||||
</InputGroup>
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="example.com" className="pl-1!" />
|
||||
<InputGroupInput placeholder="example.com" className="!pl-1" />
|
||||
<InputGroupAddon>
|
||||
<InputGroupText>https://</InputGroupText>
|
||||
</InputGroupAddon>
|
||||
@@ -74,7 +73,7 @@ export function InputGroupDemo() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ml-auto">52% used</InputGroupText>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
@@ -88,8 +87,8 @@ export function InputGroupDemo() {
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="@shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||
<IconCheck className="size-3 text-background" />
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
|
||||
@@ -42,7 +42,7 @@ export function ItemAvatar() {
|
||||
</Item>
|
||||
<Item variant="outline">
|
||||
<ItemMedia>
|
||||
<div className="flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background *:data-[slot=avatar]:grayscale">
|
||||
<div className="*:data-[slot=avatar]:ring-background flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:grayscale">
|
||||
<Avatar className="hidden sm:flex">
|
||||
<AvatarImage src="https://github.com/shadcn.png" alt="@shadcn" />
|
||||
<AvatarFallback>CN</AvatarFallback>
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
@@ -8,7 +6,8 @@ import {
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/styles/radix-nova/ui/item"
|
||||
} from "@/examples/radix/ui/item"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
export function ItemDemo() {
|
||||
return (
|
||||
|
||||
@@ -1,24 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/styles/radix-nova/ui/avatar"
|
||||
import { Badge } from "@/styles/radix-nova/ui/badge"
|
||||
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -26,7 +10,7 @@ import {
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/styles/radix-nova/ui/command"
|
||||
} from "@/examples/radix/ui/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
@@ -39,25 +23,36 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
|
||||
} from "@/examples/radix/ui/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/styles/radix-nova/ui/input-group"
|
||||
} from "@/examples/radix/ui/input-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/radix-nova/ui/popover"
|
||||
import { Switch } from "@/styles/radix-nova/ui/switch"
|
||||
} from "@/examples/radix/ui/popover"
|
||||
import { Switch } from "@/examples/radix/ui/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/radix-nova/ui/tooltip"
|
||||
} from "@/examples/radix/ui/tooltip"
|
||||
import {
|
||||
IconApps,
|
||||
IconArrowUp,
|
||||
IconAt,
|
||||
IconBook,
|
||||
IconCircleDashedPlus,
|
||||
IconPaperclip,
|
||||
IconPlus,
|
||||
IconWorld,
|
||||
IconX,
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
const SAMPLE_DATA = {
|
||||
mentionable: [
|
||||
@@ -252,7 +247,7 @@ export function NotionPromptForm() {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
|
||||
<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
|
||||
@@ -267,7 +262,7 @@ export function NotionPromptForm() {
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full pl-2!"
|
||||
className="rounded-full !pl-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
@@ -307,13 +302,9 @@ export function NotionPromptForm() {
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Select AI model</TooltipContent>
|
||||
</Tooltip>
|
||||
<DropdownMenuContent
|
||||
side="top"
|
||||
align="start"
|
||||
className="min-w-48"
|
||||
>
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
<DropdownMenuContent side="top" align="start" className="w-48">
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
Select Agent Mode
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
@@ -431,7 +422,7 @@ export function NotionPromptForm() {
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> Connect Apps
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
We'll only search in the sources selected here.
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Badge } from "@/styles/radix-nova/ui/badge"
|
||||
import { Spinner } from "@/styles/radix-nova/ui/spinner"
|
||||
import { Badge } from "@/examples/radix/ui/badge"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
|
||||
export function SpinnerBadge() {
|
||||
return (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
import { Button } from "@/examples/radix/ui/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/radix-nova/ui/empty"
|
||||
import { Spinner } from "@/styles/radix-nova/ui/spinner"
|
||||
} from "@/examples/radix/ui/empty"
|
||||
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||
|
||||
export function SpinnerEmpty() {
|
||||
return (
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function IndexPage() {
|
||||
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||
<PageActions>
|
||||
<Button asChild size="sm" className="h-[31px] rounded-lg">
|
||||
<Link href="/create">New Project</Link>
|
||||
<Link href="/docs/installation">Get Started</Link>
|
||||
</Button>
|
||||
<Button asChild size="sm" variant="ghost" className="rounded-lg">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
@@ -64,12 +64,12 @@ export default function IndexPage() {
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav className="hidden md:flex">
|
||||
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex-1 section-soft pb-6">
|
||||
<div className="container-wrapper section-soft flex-1 pb-6">
|
||||
<div className="container overflow-hidden">
|
||||
<section className="-mx-4 w-[160vw] overflow-hidden rounded-lg border border-border/50 md:hidden md:w-[150vw]">
|
||||
<section className="border-border/50 -mx-4 w-[160vw] overflow-hidden rounded-lg border md:hidden md:w-[150vw]">
|
||||
<Image
|
||||
src="/r/styles/new-york-v4/dashboard-01-light.png"
|
||||
width={1400}
|
||||
@@ -87,7 +87,7 @@ export default function IndexPage() {
|
||||
priority
|
||||
/>
|
||||
</section>
|
||||
<section className="hidden theme-container md:block">
|
||||
<section className="theme-container hidden md:block">
|
||||
<RootComponents />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function BlocksLayout({
|
||||
<a href="#blocks">Browse Blocks</a>
|
||||
</Button>
|
||||
<Button asChild variant="ghost" size="sm">
|
||||
<Link href="/docs/components">View Components</Link>
|
||||
<Link href="/docs/blocks">Add a block</Link>
|
||||
</Button>
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
@@ -71,7 +71,7 @@ export default function BlocksLayout({
|
||||
<Link href="/blocks/sidebar">Browse all blocks</Link>
|
||||
</Button>
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex-1 section-soft md:py-12">
|
||||
<div className="container-wrapper section-soft flex-1 md:py-12">
|
||||
<div className="container">{children}</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -62,7 +62,7 @@ export default function ColorsLayout({
|
||||
<div className="hidden">
|
||||
<div className="container-wrapper">
|
||||
<div className="container flex items-center justify-between gap-8 py-4">
|
||||
<ColorsNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<ColorsNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -134,7 +134,7 @@ export default async function Page(props: {
|
||||
</div>
|
||||
</div>
|
||||
{doc.description && (
|
||||
<p className="text-[1.05rem] text-muted-foreground sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
{doc.description}
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
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"
|
||||
import { Button } from "@/styles/radix-nova/ui/button"
|
||||
|
||||
export const revalidate = false
|
||||
export const dynamic = "force-static"
|
||||
@@ -57,7 +57,7 @@ export default function ChangelogPage() {
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<p className="text-[1.05rem] text-muted-foreground sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
<p className="text-muted-foreground text-[1.05rem] sm:text-base sm:text-balance md:max-w-[80%]">
|
||||
Latest updates and announcements.
|
||||
</p>
|
||||
</div>
|
||||
@@ -79,7 +79,7 @@ export default function ChangelogPage() {
|
||||
})}
|
||||
{olderPages.length > 0 && (
|
||||
<div id="more-updates" className="mb-24 scroll-mt-24">
|
||||
<h2 className="mb-6 font-heading text-xl font-semibold tracking-tight">
|
||||
<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">
|
||||
@@ -91,9 +91,9 @@ export default function ChangelogPage() {
|
||||
<Link
|
||||
key={page.url}
|
||||
href={page.url}
|
||||
className="flex w-full flex-col rounded-xl bg-surface px-4 py-3 text-surface-foreground transition-colors hover:bg-surface/80"
|
||||
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-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground text-xs">
|
||||
{date}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{title}</span>
|
||||
@@ -110,7 +110,7 @@ export default function ChangelogPage() {
|
||||
<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="sticky top-0 h-6 bg-background text-xs font-medium text-muted-foreground">
|
||||
<p className="text-muted-foreground bg-background sticky top-0 h-6 text-xs font-medium">
|
||||
On This Page
|
||||
</p>
|
||||
{latestPages.map((page) => {
|
||||
@@ -119,7 +119,7 @@ export default function ChangelogPage() {
|
||||
<Link
|
||||
key={page.url}
|
||||
href={page.url}
|
||||
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground"
|
||||
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
|
||||
>
|
||||
{data.title}
|
||||
</Link>
|
||||
@@ -128,7 +128,7 @@ export default function ChangelogPage() {
|
||||
{olderPages.length > 0 && (
|
||||
<a
|
||||
href="#more-updates"
|
||||
className="text-[0.8rem] text-muted-foreground no-underline transition-colors hover:text-foreground"
|
||||
className="text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors"
|
||||
>
|
||||
More Updates
|
||||
</a>
|
||||
|
||||
@@ -10,7 +10,7 @@ export default function DocsLayout({
|
||||
return (
|
||||
<div className="container-wrapper flex flex-1 flex-col px-2">
|
||||
<SidebarProvider
|
||||
className="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)] 3xl:fixed:container 3xl:fixed:px-3"
|
||||
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)",
|
||||
|
||||
@@ -43,8 +43,8 @@ export default function AuthenticationPage() {
|
||||
>
|
||||
Login
|
||||
</Link>
|
||||
<div className="relative hidden h-full flex-col p-10 text-primary lg:flex dark:border-r">
|
||||
<div className="absolute inset-0 bg-primary/5" />
|
||||
<div className="text-primary relative hidden h-full flex-col p-10 lg:flex dark:border-r">
|
||||
<div className="bg-primary/5 absolute inset-0" />
|
||||
<div className="relative z-20 flex items-center text-lg font-medium">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -74,7 +74,7 @@ export default function AuthenticationPage() {
|
||||
<h1 className="text-2xl font-semibold tracking-tight">
|
||||
Create an account
|
||||
</h1>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Enter your email below to create your account
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -159,10 +159,10 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
<SidebarMenuItem>
|
||||
<SidebarMenuButton
|
||||
asChild
|
||||
className="data-[slot=sidebar-menu-button]:p-1.5!"
|
||||
className="data-[slot=sidebar-menu-button]:!p-1.5"
|
||||
>
|
||||
<Link href="#">
|
||||
<IconInnerShadowTop className="size-5!" />
|
||||
<IconInnerShadowTop className="!size-5" />
|
||||
<span className="text-base font-semibold">Acme Inc.</span>
|
||||
</Link>
|
||||
</SidebarMenuButton>
|
||||
|
||||
@@ -174,7 +174,7 @@ export function ChartAreaInteractive() {
|
||||
value={timeRange}
|
||||
onValueChange={setTimeRange}
|
||||
variant="outline"
|
||||
className="hidden *:data-[slot=toggle-group-item]:px-4! @[767px]/card:flex"
|
||||
className="hidden *:data-[slot=toggle-group-item]:!px-4 @[767px]/card:flex"
|
||||
>
|
||||
<ToggleGroupItem value="90d">Last 3 months</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30d">Last 30 days</ToggleGroupItem>
|
||||
|
||||
@@ -128,9 +128,9 @@ function DragHandle({ id }: { id: number }) {
|
||||
{...listeners}
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-7 text-muted-foreground hover:bg-transparent"
|
||||
className="text-muted-foreground size-7 hover:bg-transparent"
|
||||
>
|
||||
<IconGripVertical className="size-3 text-muted-foreground" />
|
||||
<IconGripVertical className="text-muted-foreground size-3" />
|
||||
<span className="sr-only">Drag to reorder</span>
|
||||
</Button>
|
||||
)
|
||||
@@ -181,7 +181,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
header: "Section Type",
|
||||
cell: ({ row }) => (
|
||||
<div className="w-32">
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
{row.original.type}
|
||||
</Badge>
|
||||
</div>
|
||||
@@ -191,7 +191,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
accessorKey: "status",
|
||||
header: "Status",
|
||||
cell: ({ row }) => (
|
||||
<Badge variant="outline" className="px-1.5 text-muted-foreground">
|
||||
<Badge variant="outline" className="text-muted-foreground px-1.5">
|
||||
{row.original.status === "Done" ? (
|
||||
<IconCircleCheckFilled className="fill-green-500 dark:fill-green-400" />
|
||||
) : (
|
||||
@@ -219,7 +219,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
Target
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||
defaultValue={row.original.target}
|
||||
id={`${row.original.id}-target`}
|
||||
/>
|
||||
@@ -244,7 +244,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
Limit
|
||||
</Label>
|
||||
<Input
|
||||
className="h-8 w-16 border-transparent bg-transparent text-right shadow-none hover:bg-input/30 focus-visible:border focus-visible:bg-background dark:bg-transparent dark:hover:bg-input/30 dark:focus-visible:bg-input/30"
|
||||
className="hover:bg-input/30 focus-visible:bg-background dark:hover:bg-input/30 dark:focus-visible:bg-input/30 h-8 w-16 border-transparent bg-transparent text-right shadow-none focus-visible:border dark:bg-transparent"
|
||||
defaultValue={row.original.limit}
|
||||
id={`${row.original.id}-limit`}
|
||||
/>
|
||||
@@ -292,7 +292,7 @@ const columns: ColumnDef<z.infer<typeof schema>>[] = [
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
className="flex size-8 text-muted-foreground data-[state=open]:bg-muted"
|
||||
className="data-[state=open]:bg-muted text-muted-foreground flex size-8"
|
||||
size="icon"
|
||||
>
|
||||
<IconDotsVertical />
|
||||
@@ -425,7 +425,7 @@ export function DataTable({
|
||||
<SelectItem value="focus-documents">Focus Documents</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<TabsList className="hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:bg-muted-foreground/30 **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsList className="**:data-[slot=badge]:bg-muted-foreground/30 hidden **:data-[slot=badge]:size-5 **:data-[slot=badge]:rounded-full **:data-[slot=badge]:px-1 @4xl/main:flex">
|
||||
<TabsTrigger value="outline">Outline</TabsTrigger>
|
||||
<TabsTrigger value="past-performance">
|
||||
Past Performance <Badge variant="secondary">3</Badge>
|
||||
@@ -488,7 +488,7 @@ export function DataTable({
|
||||
id={sortableId}
|
||||
>
|
||||
<Table>
|
||||
<TableHeader className="sticky top-0 z-10 bg-muted">
|
||||
<TableHeader className="bg-muted sticky top-0 z-10">
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
@@ -531,7 +531,7 @@ export function DataTable({
|
||||
</DndContext>
|
||||
</div>
|
||||
<div className="flex items-center justify-between px-4">
|
||||
<div className="hidden flex-1 text-sm text-muted-foreground lg:flex">
|
||||
<div className="text-muted-foreground hidden flex-1 text-sm lg:flex">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
@@ -653,7 +653,7 @@ function TableCellViewer({ item }: { item: z.infer<typeof schema> }) {
|
||||
return (
|
||||
<Drawer direction={isMobile ? "bottom" : "right"}>
|
||||
<DrawerTrigger asChild>
|
||||
<Button variant="link" className="w-fit px-0 text-left text-foreground">
|
||||
<Button variant="link" className="text-foreground w-fit px-0 text-left">
|
||||
{item.header}
|
||||
</Button>
|
||||
</DrawerTrigger>
|
||||
|
||||
@@ -52,7 +52,7 @@ export function NavDocuments({
|
||||
<DropdownMenuTrigger asChild>
|
||||
<SidebarMenuAction
|
||||
showOnHover
|
||||
className="rounded-sm data-[state=open]:bg-accent"
|
||||
className="data-[state=open]:bg-accent rounded-sm"
|
||||
>
|
||||
<IconDots />
|
||||
<span className="sr-only">More</span>
|
||||
|
||||
@@ -55,7 +55,7 @@ export function NavUser({
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
@@ -76,7 +76,7 @@ export function NavUser({
|
||||
</Avatar>
|
||||
<div className="grid flex-1 text-left text-sm leading-tight">
|
||||
<span className="truncate font-medium">{user.name}</span>
|
||||
<span className="truncate text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground truncate text-xs">
|
||||
{user.email}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
|
||||
export function SiteHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b bg-background/90 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<header className="bg-background/90 sticky top-0 z-10 flex h-(--header-height) shrink-0 items-center gap-2 border-b transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-(--header-height)">
|
||||
<div className="flex w-full items-center gap-1 px-4 lg:gap-2 lg:px-6">
|
||||
<h1 className="text-base font-medium">Documents</h1>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
|
||||
@@ -65,12 +65,12 @@ export default function ExamplesLayout({
|
||||
</PageActions>
|
||||
</PageHeader>
|
||||
<PageNav id="examples" className="hidden md:flex">
|
||||
<ExamplesNav className="flex-1 overflow-hidden [&>a:first-child]:text-primary" />
|
||||
<ExamplesNav className="[&>a:first-child]:text-primary flex-1 overflow-hidden" />
|
||||
<ThemeSelector className="mr-4 hidden md:flex" />
|
||||
</PageNav>
|
||||
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
|
||||
<div className="container flex flex-1 scroll-mt-20 flex-col theme-container">
|
||||
<div className="flex flex-col overflow-hidden rounded-lg border bg-background 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="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">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -76,7 +76,7 @@ export function CodeViewer() {
|
||||
</pre>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Your API Key can be found here. You should use environment
|
||||
variables or a secret management tool to expose your key to your
|
||||
applications.
|
||||
|
||||
@@ -27,7 +27,7 @@ export function MaxLengthSelector({ defaultValue }: MaxLengthSelectorProps) {
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="maxlength">Maximum Length</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -78,7 +78,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
>
|
||||
<div className="grid gap-2">
|
||||
<h4 className="leading-none font-medium">{peekedModel.name}</h4>
|
||||
<div className="text-sm text-muted-foreground">
|
||||
<div className="text-muted-foreground text-sm">
|
||||
{peekedModel.description}
|
||||
</div>
|
||||
{peekedModel.strengths ? (
|
||||
@@ -86,7 +86,7 @@ export function ModelSelector({ models, types, ...props }: ModelSelectorProps) {
|
||||
<h5 className="text-sm leading-none font-medium">
|
||||
Strengths
|
||||
</h5>
|
||||
<ul className="text-sm text-muted-foreground">
|
||||
<ul className="text-muted-foreground text-sm">
|
||||
{peekedModel.strengths}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -70,7 +70,7 @@ export function PresetActions() {
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="py-6">
|
||||
<h4 className="text-sm text-muted-foreground">
|
||||
<h4 className="text-muted-foreground text-sm">
|
||||
Playground Warnings
|
||||
</h4>
|
||||
<div className="flex items-start justify-between gap-4 pt-3">
|
||||
@@ -79,7 +79,7 @@ export function PresetActions() {
|
||||
<span className="font-semibold">
|
||||
Show a warning when content is flagged
|
||||
</span>
|
||||
<span className="text-sm text-muted-foreground">
|
||||
<span className="text-muted-foreground text-sm">
|
||||
A warning will be shown when sexual, hateful, violent or
|
||||
self-harm content is detected.
|
||||
</span>
|
||||
|
||||
@@ -18,7 +18,7 @@ export function PresetShare() {
|
||||
<PopoverContent align="end" className="flex w-[520px] flex-col gap-4">
|
||||
<div className="flex flex-col gap-1 text-center sm:text-left">
|
||||
<h3 className="text-lg font-semibold">Share preset</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
<p className="text-muted-foreground text-sm">
|
||||
Anyone who has this link and an OpenAI account will be able to view
|
||||
this.
|
||||
</p>
|
||||
|
||||
@@ -29,7 +29,7 @@ export function TemperatureSelector({
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="temperature">Temperature</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -27,7 +27,7 @@ export function TopPSelector({ defaultValue }: TopPSelectorProps) {
|
||||
<div className="grid gap-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<Label htmlFor="top-p">Top P</Label>
|
||||
<span className="w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm text-muted-foreground hover:border-border">
|
||||
<span className="text-muted-foreground hover:border-border w-12 rounded-md border border-transparent px-2 py-0.5 text-right text-sm">
|
||||
{value}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -278,7 +278,7 @@ export default function PlaygroundPage() {
|
||||
placeholder="We're writing to [inset]. Congrats from OpenAI!"
|
||||
className="h-full min-h-[300px] p-4 lg:min-h-[700px] xl:min-h-[700px]"
|
||||
/>
|
||||
<div className="rounded-md border bg-muted"></div>
|
||||
<div className="bg-muted rounded-md border"></div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button>Submit</Button>
|
||||
@@ -312,7 +312,7 @@ export default function PlaygroundPage() {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="min-h-[400px] rounded-md border bg-muted lg:min-h-[700px]" />
|
||||
<div className="bg-muted min-h-[400px] rounded-md border lg:min-h-[700px]" />
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button>Submit</Button>
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
@@ -16,13 +13,13 @@ import {
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/styles/base-nova/ui-rtl/field"
|
||||
import { Input } from "@/styles/base-nova/ui-rtl/input"
|
||||
import {
|
||||
RadioGroup,
|
||||
RadioGroupItem,
|
||||
} from "@/styles/base-nova/ui-rtl/radio-group"
|
||||
import { Switch } from "@/styles/base-nova/ui-rtl/switch"
|
||||
} 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: {
|
||||
@@ -131,7 +128,7 @@ export function AppearanceSettings() {
|
||||
value={gpuCount}
|
||||
onChange={handleGpuInputChange}
|
||||
size={3}
|
||||
className="h-7 w-14! font-mono"
|
||||
className="h-7 !w-14 font-mono"
|
||||
maxLength={3}
|
||||
/>
|
||||
<Button
|
||||
|
||||
@@ -1,21 +1,8 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import {
|
||||
ArchiveIcon,
|
||||
ArrowLeftIcon,
|
||||
CalendarPlusIcon,
|
||||
ClockIcon,
|
||||
ListFilterIcon,
|
||||
MailCheckIcon,
|
||||
MoreHorizontalIcon,
|
||||
TagIcon,
|
||||
Trash2Icon,
|
||||
} from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -29,7 +16,20 @@ import {
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/dropdown-menu"
|
||||
} 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: {
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
||||
} from "@/examples/base/ui-rtl/input-group"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/tooltip"
|
||||
} from "@/examples/base/ui-rtl/tooltip"
|
||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
"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"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
"use client"
|
||||
|
||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/styles/base-nova/ui-rtl/button-group"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/popover"
|
||||
import { Separator } from "@/styles/base-nova/ui-rtl/separator"
|
||||
import { Textarea } from "@/styles/base-nova/ui-rtl/textarea"
|
||||
} 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: {
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
"use client"
|
||||
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarGroup,
|
||||
AvatarImage,
|
||||
} from "@/styles/base-nova/ui-rtl/avatar"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
} from "@/examples/base/ui-rtl/avatar"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -17,7 +14,10 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/base-nova/ui-rtl/empty"
|
||||
} from "@/examples/base/ui-rtl/empty"
|
||||
import { PlusIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"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"
|
||||
import { Checkbox } from "@/styles/base-nova/ui-rtl/checkbox"
|
||||
import { Field, FieldLabel } from "@/styles/base-nova/ui-rtl/field"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { Checkbox } from "@/styles/base-nova/ui-rtl/checkbox"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -11,8 +10,8 @@ import {
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
} from "@/styles/base-nova/ui-rtl/field"
|
||||
import { Input } from "@/styles/base-nova/ui-rtl/input"
|
||||
} from "@/examples/base/ui-rtl/field"
|
||||
import { Input } from "@/examples/base/ui-rtl/input"
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
@@ -20,8 +19,10 @@ import {
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@/styles/base-nova/ui-rtl/select"
|
||||
import { Textarea } from "@/styles/base-nova/ui-rtl/textarea"
|
||||
} from "@/examples/base/ui-rtl/select"
|
||||
import { Textarea } from "@/examples/base/ui-rtl/textarea"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
@@ -139,7 +140,7 @@ export function FieldDemo() {
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rtl-exp-month">{t.month}</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<Select defaultValue="" items={months}>
|
||||
<SelectTrigger id="rtl-exp-month">
|
||||
<SelectValue placeholder="MM" />
|
||||
</SelectTrigger>
|
||||
@@ -156,7 +157,7 @@ export function FieldDemo() {
|
||||
</Field>
|
||||
<Field>
|
||||
<FieldLabel htmlFor="rtl-exp-year">{t.year}</FieldLabel>
|
||||
<Select defaultValue="">
|
||||
<Select defaultValue="" items={years}>
|
||||
<SelectTrigger id="rtl-exp-year">
|
||||
<SelectValue placeholder="YYYY" />
|
||||
</SelectTrigger>
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Card, CardContent } from "@/styles/base-nova/ui-rtl/card"
|
||||
import { Checkbox } from "@/styles/base-nova/ui-rtl/checkbox"
|
||||
import { Card, CardContent } from "@/examples/base/ui-rtl/card"
|
||||
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
@@ -11,7 +10,9 @@ import {
|
||||
FieldLegend,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/styles/base-nova/ui-rtl/field"
|
||||
} from "@/examples/base/ui-rtl/field"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
@@ -62,7 +63,7 @@ export function FieldHear() {
|
||||
<FieldLabel
|
||||
htmlFor={`rtl-${option.value}`}
|
||||
key={option.value}
|
||||
className="w-fit!"
|
||||
className="!w-fit"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
Field,
|
||||
FieldDescription,
|
||||
FieldTitle,
|
||||
} from "@/styles/base-nova/ui-rtl/field"
|
||||
import { Slider } from "@/styles/base-nova/ui-rtl/slider"
|
||||
} from "@/examples/base/ui-rtl/field"
|
||||
import { Slider } from "@/examples/base/ui-rtl/slider"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
"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 { DirectionProvider } from "@/styles/base-nova/ui-rtl/direction"
|
||||
import { FieldSeparator } from "@/styles/base-nova/ui-rtl/field"
|
||||
|
||||
import { AppearanceSettings } from "./appearance-settings"
|
||||
import { ButtonGroupDemo } from "./button-group-demo"
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
||||
import { Label } from "@/styles/base-nova/ui-rtl/label"
|
||||
} from "@/examples/base/ui-rtl/input-group"
|
||||
import { Label } from "@/examples/base/ui-rtl/label"
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/popover"
|
||||
} from "@/examples/base/ui-rtl/popover"
|
||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
@@ -50,7 +50,7 @@ export function InputGroupButtonExample() {
|
||||
{t.inputLabel}
|
||||
</Label>
|
||||
<InputGroup className="[--radius:9999px]">
|
||||
<InputGroupInput id="input-secure-rtl" className="pr-0.5!" />
|
||||
<InputGroupInput id="input-secure-rtl" className="!pr-0.5" />
|
||||
<InputGroupAddon>
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
"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,
|
||||
@@ -9,26 +29,6 @@ import {
|
||||
import { ArrowUpIcon, Search } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/dropdown-menu"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupInput,
|
||||
InputGroupText,
|
||||
InputGroupTextarea,
|
||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
||||
import { Separator } from "@/styles/base-nova/ui-rtl/separator"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/tooltip"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
@@ -116,7 +116,7 @@ export function InputGroupDemo() {
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<InputGroupText className="ms-auto">{t.used}</InputGroupText>
|
||||
<Separator orientation="vertical" className="h-4!" />
|
||||
<Separator orientation="vertical" className="!h-4" />
|
||||
<InputGroupButton
|
||||
variant="default"
|
||||
className="rounded-full"
|
||||
@@ -130,7 +130,7 @@ export function InputGroupDemo() {
|
||||
<InputGroup>
|
||||
<InputGroupInput placeholder="shadcn" />
|
||||
<InputGroupAddon align="inline-end">
|
||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||
<div className="bg-primary text-foreground flex size-4 items-center justify-center rounded-full">
|
||||
<IconCheck className="size-3 text-white" />
|
||||
</div>
|
||||
</InputGroupAddon>
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import {
|
||||
Item,
|
||||
ItemActions,
|
||||
@@ -11,7 +8,10 @@ import {
|
||||
ItemDescription,
|
||||
ItemMedia,
|
||||
ItemTitle,
|
||||
} from "@/styles/base-nova/ui-rtl/item"
|
||||
} from "@/examples/base/ui-rtl/item"
|
||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,6 +1,47 @@
|
||||
"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,
|
||||
@@ -14,47 +55,6 @@ import {
|
||||
} from "@tabler/icons-react"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@/styles/base-nova/ui-rtl/avatar"
|
||||
import { Badge } from "@/styles/base-nova/ui-rtl/badge"
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/styles/base-nova/ui-rtl/command"
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/dropdown-menu"
|
||||
import { Field, FieldLabel } from "@/styles/base-nova/ui-rtl/field"
|
||||
import {
|
||||
InputGroup,
|
||||
InputGroupAddon,
|
||||
InputGroupButton,
|
||||
InputGroupTextarea,
|
||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
||||
import { Popover, PopoverContent } from "@/styles/base-nova/ui-rtl/popover"
|
||||
import { Switch } from "@/styles/base-nova/ui-rtl/switch"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/styles/base-nova/ui-rtl/tooltip"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
@@ -288,7 +288,7 @@ export function NotionPromptForm() {
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
<div className="-m-1.5 no-scrollbar flex gap-1 overflow-y-auto p-1.5">
|
||||
<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
|
||||
@@ -303,7 +303,7 @@ export function NotionPromptForm() {
|
||||
key={mention}
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="rounded-full pr-2!"
|
||||
className="rounded-full !pr-2"
|
||||
onClick={() => {
|
||||
setMentions((prev) => prev.filter((m) => m !== mention))
|
||||
}}
|
||||
@@ -352,7 +352,7 @@ export function NotionPromptForm() {
|
||||
dir={t.dir}
|
||||
>
|
||||
<DropdownMenuGroup className="w-48">
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
{t.selectAgentMode}
|
||||
</DropdownMenuLabel>
|
||||
{SAMPLE_DATA.models.map((model) => (
|
||||
@@ -493,7 +493,7 @@ export function NotionPromptForm() {
|
||||
<DropdownMenuItem>
|
||||
<IconPlus /> {t.connectApps}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuLabel className="text-xs text-muted-foreground">
|
||||
<DropdownMenuLabel className="text-muted-foreground text-xs">
|
||||
{t.searchSourcesNote}
|
||||
</DropdownMenuLabel>
|
||||
</DropdownMenuGroup>
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
"use client"
|
||||
|
||||
import { Badge } from "@/examples/base/ui-rtl/badge"
|
||||
import { Spinner } from "@/examples/base/ui-rtl/spinner"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Badge } from "@/styles/base-nova/ui-rtl/badge"
|
||||
import { Spinner } from "@/styles/base-nova/ui-rtl/spinner"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
||||
import { Button } from "@/examples/base/ui-rtl/button"
|
||||
import {
|
||||
Empty,
|
||||
EmptyContent,
|
||||
@@ -9,8 +8,10 @@ import {
|
||||
EmptyHeader,
|
||||
EmptyMedia,
|
||||
EmptyTitle,
|
||||
} from "@/styles/base-nova/ui-rtl/empty"
|
||||
import { Spinner } from "@/styles/base-nova/ui-rtl/spinner"
|
||||
} from "@/examples/base/ui-rtl/empty"
|
||||
import { Spinner } from "@/examples/base/ui-rtl/spinner"
|
||||
|
||||
import { useLanguageContext } from "@/components/language-selector"
|
||||
|
||||
const translations = {
|
||||
ar: {
|
||||
|
||||
@@ -79,7 +79,7 @@ export const columns: ColumnDef<Task>[] = [
|
||||
return (
|
||||
<div className="flex w-[100px] items-center gap-2">
|
||||
{status.icon && (
|
||||
<status.icon className="size-4 text-muted-foreground" />
|
||||
<status.icon className="text-muted-foreground size-4" />
|
||||
)}
|
||||
<span>{status.label}</span>
|
||||
</div>
|
||||
@@ -106,7 +106,7 @@ export const columns: ColumnDef<Task>[] = [
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{priority.icon && (
|
||||
<priority.icon className="size-4 text-muted-foreground" />
|
||||
<priority.icon className="text-muted-foreground size-4" />
|
||||
)}
|
||||
<span>{priority.label}</span>
|
||||
</div>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function DataTableColumnHeader<TData, TValue>({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="-ml-3 h-8 data-[state=open]:bg-accent"
|
||||
className="data-[state=open]:bg-accent -ml-3 h-8"
|
||||
>
|
||||
<span>{title}</span>
|
||||
{column.getIsSorted() === "desc" ? (
|
||||
|
||||
@@ -107,18 +107,18 @@ export function DataTableFacetedFilter<TData, TValue>({
|
||||
className={cn(
|
||||
"flex size-4 items-center justify-center rounded-[4px] border",
|
||||
isSelected
|
||||
? "border-primary bg-primary text-primary-foreground"
|
||||
? "bg-primary border-primary text-primary-foreground"
|
||||
: "border-input [&_svg]:invisible"
|
||||
)}
|
||||
>
|
||||
<Check className="size-3.5 text-primary-foreground" />
|
||||
<Check className="text-primary-foreground size-3.5" />
|
||||
</div>
|
||||
{option.icon && (
|
||||
<option.icon className="size-4 text-muted-foreground" />
|
||||
<option.icon className="text-muted-foreground size-4" />
|
||||
)}
|
||||
<span>{option.label}</span>
|
||||
{facets?.get(option.value) && (
|
||||
<span className="ml-auto flex size-4 items-center justify-center font-mono text-xs text-muted-foreground">
|
||||
<span className="text-muted-foreground ml-auto flex size-4 items-center justify-center font-mono text-xs">
|
||||
{facets.get(option.value)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function DataTablePagination<TData>({
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="flex-1 text-sm text-muted-foreground">
|
||||
<div className="text-muted-foreground flex-1 text-sm">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of{" "}
|
||||
{table.getFilteredRowModel().rows.length} row(s) selected.
|
||||
</div>
|
||||
|
||||
@@ -36,7 +36,7 @@ export function DataTableRowActions<TData>({
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="size-8 data-[state=open]:bg-muted"
|
||||
className="data-[state=open]:bg-muted size-8"
|
||||
>
|
||||
<MoreHorizontal />
|
||||
<span className="sr-only">Open menu</span>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function UserNav() {
|
||||
<DropdownMenuLabel className="font-normal">
|
||||
<div className="flex flex-col space-y-1">
|
||||
<p className="text-sm leading-none font-medium">shadcn</p>
|
||||
<p className="text-xs leading-none text-muted-foreground">
|
||||
<p className="text-muted-foreground text-xs leading-none">
|
||||
m@example.com
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div
|
||||
data-slot="layout"
|
||||
className="relative z-10 flex min-h-svh flex-col bg-background"
|
||||
className="bg-background relative z-10 flex min-h-svh flex-col"
|
||||
>
|
||||
<SiteHeader />
|
||||
<main className="flex flex-1 flex-col">{children}</main>
|
||||
|
||||
@@ -12,8 +12,8 @@ export default function ThemesPage() {
|
||||
<ThemeCustomizer />
|
||||
</div>
|
||||
</div>
|
||||
<div className="container-wrapper flex flex-1 flex-col section-soft pb-6">
|
||||
<div className="container flex flex-1 flex-col theme-container">
|
||||
<div className="container-wrapper section-soft flex flex-1 flex-col pb-6">
|
||||
<div className="theme-container container flex flex-1 flex-col">
|
||||
<CardsDemo />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -26,23 +26,23 @@ export function MenuAccentPicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative pr-3 md:pr-0">
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Menu Accent</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-muted-foreground text-xs">Menu Accent</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentAccent?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
|
||||
<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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
className="size-4 text-foreground"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M19 12.1294L12.9388 18.207C11.1557 19.9949 10.2641 20.8889 9.16993 20.9877C8.98904 21.0041 8.80705 21.0041 8.62616 20.9877C7.53195 20.8889 6.64039 19.9949 4.85726 18.207L2.83687 16.1811C1.72104 15.0622 1.72104 13.2482 2.83687 12.1294M19 12.1294L10.9184 4.02587M19 12.1294H2.83687M10.9184 4.02587L2.83687 12.1294M10.9184 4.02587L8.89805 2"
|
||||
@@ -51,7 +51,7 @@ export function MenuAccentPicker({
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-accent={currentAccent?.value}
|
||||
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
|
||||
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
|
||||
></path>
|
||||
<path
|
||||
d="M22 20C22 21.1046 21.1046 22 20 22C18.8954 22 18 21.1046 18 20C18 18.8954 20 17 20 17C20 17 22 18.8954 22 20Z"
|
||||
@@ -60,7 +60,7 @@ export function MenuAccentPicker({
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
data-accent={currentAccent?.value}
|
||||
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
|
||||
className="data-[accent=bold]:fill-foreground fill-muted-foreground/30"
|
||||
></path>
|
||||
</svg>
|
||||
</div>
|
||||
@@ -78,16 +78,7 @@ export function MenuAccentPicker({
|
||||
>
|
||||
<PickerGroup>
|
||||
{MENU_ACCENTS.map((accent) => (
|
||||
<PickerRadioItem
|
||||
key={accent.value}
|
||||
value={accent.value}
|
||||
closeOnClick={isMobile}
|
||||
disabled={
|
||||
accent.value === "bold" &&
|
||||
(params.menuColor === "default-translucent" ||
|
||||
params.menuColor === "inverted-translucent")
|
||||
}
|
||||
>
|
||||
<PickerRadioItem key={accent.value} value={accent.value}>
|
||||
{accent.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
@@ -97,7 +88,7 @@ export function MenuAccentPicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="menuAccent"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,88 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import Script from "next/script"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import {
|
||||
Command,
|
||||
CommandDialog,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
} from "@/styles/base-nova/ui/command"
|
||||
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
|
||||
|
||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||
|
||||
export function ActionMenu({
|
||||
itemsByBase,
|
||||
}: {
|
||||
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
|
||||
}) {
|
||||
const {
|
||||
activeRegistryName,
|
||||
getCommandValue,
|
||||
groups,
|
||||
handleSelect,
|
||||
open,
|
||||
setOpen,
|
||||
} = useActionMenu(itemsByBase)
|
||||
|
||||
return (
|
||||
<CommandDialog open={open} onOpenChange={setOpen} className="animate-none!">
|
||||
<Command loop>
|
||||
<CommandInput placeholder="Search" />
|
||||
<CommandList>
|
||||
<CommandEmpty>No items found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{groups.map((group) =>
|
||||
group.items.map((item) => (
|
||||
<CommandItem
|
||||
key={item.id}
|
||||
value={getCommandValue(item)}
|
||||
data-checked={activeRegistryName === item.registryName}
|
||||
className="px-2"
|
||||
onSelect={() => {
|
||||
handleSelect(item.registryName)
|
||||
}}
|
||||
>
|
||||
{item.label}
|
||||
</CommandItem>
|
||||
))
|
||||
)}
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</CommandDialog>
|
||||
)
|
||||
}
|
||||
|
||||
export function ActionMenuScript() {
|
||||
return (
|
||||
<Script
|
||||
id="design-system-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward Cmd/Ctrl + K (and P) to parent
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'k' || e.key === 'p') && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: '${CMD_K_FORWARD_TYPE}',
|
||||
key: e.key
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
|
||||
@@ -9,8 +10,10 @@ import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerItem,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
@@ -22,6 +25,7 @@ export function BaseColorPicker({
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const { resolvedTheme, setTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
@@ -35,20 +39,22 @@ export function BaseColorPicker({
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Base Color</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-muted-foreground text-xs">Base Color</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentBaseColor?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && (
|
||||
{mounted && resolvedTheme && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentBaseColor?.cssVars?.dark?.["muted-foreground"],
|
||||
currentBaseColor?.cssVars?.[
|
||||
resolvedTheme as "light" | "dark"
|
||||
]?.["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 md:right-2.5"
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
@@ -60,26 +66,59 @@ export function BaseColorPicker({
|
||||
<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}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{baseColor.title}
|
||||
<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>
|
||||
</div>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="baseColor"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -40,47 +40,49 @@ export function BasePicker({
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Base</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentBase?.title}
|
||||
</div>
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-muted-foreground text-xs">Component Library</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentBase?.title}
|
||||
</div>
|
||||
{currentBase?.meta?.logo && (
|
||||
<div
|
||||
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 text-foreground select-none md:right-2.5 *:[svg]:size-4 *:[svg]:text-foreground!"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentBase.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
</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"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: currentBase.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBase?.name}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentBase?.name}
|
||||
onValueChange={handleValueChange}
|
||||
>
|
||||
<PickerGroup>
|
||||
{BASES.map((base) => (
|
||||
<PickerRadioItem
|
||||
key={base.name}
|
||||
value={base.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{base.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
</div>
|
||||
<PickerGroup>
|
||||
{BASES.map((base) => (
|
||||
<PickerRadioItem key={base.name} value={base.name}>
|
||||
{base.meta?.logo && (
|
||||
<div
|
||||
className="text-foreground *:[svg]:text-foreground! size-4 shrink-0 [&_svg]:size-4"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: base.meta.logo,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{base.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,136 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
import {
|
||||
BASE_COLORS,
|
||||
getThemesForBaseColor,
|
||||
type ChartColorName,
|
||||
} from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function ChartColorPicker({
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const mounted = useMounted()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const availableChartColors = React.useMemo(
|
||||
() => getThemesForBaseColor(params.baseColor),
|
||||
[params.baseColor]
|
||||
)
|
||||
|
||||
const currentChartColor = React.useMemo(
|
||||
() =>
|
||||
availableChartColors.find((theme) => theme.name === params.chartColor),
|
||||
[availableChartColors, params.chartColor]
|
||||
)
|
||||
|
||||
const currentChartColorIsBaseColor = React.useMemo(
|
||||
() => BASE_COLORS.find((baseColor) => baseColor.name === params.chartColor),
|
||||
[params.chartColor]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!currentChartColor && availableChartColors.length > 0) {
|
||||
setParams({ chartColor: availableChartColors[0].name })
|
||||
}
|
||||
}, [currentChartColor, availableChartColors, setParams])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Chart Color</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{currentChartColor?.title}
|
||||
</div>
|
||||
</div>
|
||||
{mounted && (
|
||||
<div
|
||||
style={
|
||||
{
|
||||
"--color":
|
||||
currentChartColor?.cssVars?.dark?.[
|
||||
currentChartColorIsBaseColor
|
||||
? "muted-foreground"
|
||||
: "primary"
|
||||
],
|
||||
} 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 md:right-2.5"
|
||||
/>
|
||||
)}
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-92"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentChartColor?.name}
|
||||
onValueChange={(value) => {
|
||||
setParams({ chartColor: value as ChartColorName })
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{availableChartColors
|
||||
.filter((theme) =>
|
||||
BASE_COLORS.find((baseColor) => baseColor.name === theme.name)
|
||||
)
|
||||
.map((theme) => (
|
||||
<PickerRadioItem
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{theme.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{availableChartColors
|
||||
.filter(
|
||||
(theme) =>
|
||||
!BASE_COLORS.find(
|
||||
(baseColor) => baseColor.name === theme.name
|
||||
)
|
||||
)
|
||||
.map((theme) => (
|
||||
<PickerRadioItem
|
||||
key={theme.name}
|
||||
value={theme.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{theme.title}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="chartColor"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
|
||||
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||
const presetCode = usePresetCode()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => setHasCopied(false), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasCopied])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
copyToClipboardWithMeta(`--preset ${presetCode}`, {
|
||||
name: "copy_preset_command",
|
||||
properties: {
|
||||
preset: presetCode,
|
||||
},
|
||||
})
|
||||
setHasCopied(true)
|
||||
}, [presetCode])
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCopy}
|
||||
className={cn(
|
||||
"touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span>{hasCopied ? "Copied" : `--preset ${presetCode}`}</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -1,41 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
import { Settings05Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { useIsMobile } from "@/hooks/use-mobile"
|
||||
import { getThemesForBaseColor, STYLES } from "@/registry/config"
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from "@/styles/base-nova/ui/card"
|
||||
import { FieldGroup, FieldSeparator } from "@/styles/base-nova/ui/field"
|
||||
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 { ActionMenu } from "@/app/(create)/components/action-menu"
|
||||
import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
|
||||
import { BasePicker } from "@/app/(create)/components/base-picker"
|
||||
import { ChartColorPicker } from "@/app/(create)/components/chart-color-picker"
|
||||
import { CopyPreset } from "@/app/(create)/components/copy-preset"
|
||||
import { FontPicker } from "@/app/(create)/components/font-picker"
|
||||
import { IconLibraryPicker } from "@/app/(create)/components/icon-library-picker"
|
||||
import { MainMenu } from "@/app/(create)/components/main-menu"
|
||||
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 { ResetDialog } from "@/app/(create)/components/reset-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 { V0Button } from "@/app/(create)/components/v0-button"
|
||||
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function Customizer({
|
||||
itemsByBase,
|
||||
}: {
|
||||
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
|
||||
}) {
|
||||
export function Customizer() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const isMobile = useIsMobile()
|
||||
const anchorRef = React.useRef<HTMLDivElement | null>(null)
|
||||
@@ -46,59 +34,40 @@ export function Customizer({
|
||||
)
|
||||
|
||||
return (
|
||||
<Card
|
||||
className="dark top-24 right-12 isolate z-10 max-h-full min-h-0 w-full self-start rounded-2xl bg-card/90 shadow-xl backdrop-blur-xl md:w-(--customizer-width)"
|
||||
<div
|
||||
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}
|
||||
size="sm"
|
||||
>
|
||||
<CardHeader className="hidden items-center justify-between gap-2 border-b group-data-reversed/layout:flex-row-reverse md:flex">
|
||||
<MainMenu />
|
||||
</CardHeader>
|
||||
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden md:overflow-y-auto">
|
||||
<FieldGroup className="flex-row gap-2.5 py-px **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25">
|
||||
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
|
||||
<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
|
||||
presets={PRESETS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<StylePicker
|
||||
styles={STYLES}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<ThemePicker
|
||||
themes={availableThemes}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<ChartColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<FontPicker
|
||||
label="Heading"
|
||||
param="fontHeading"
|
||||
fonts={FONT_HEADING_OPTIONS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FontPicker
|
||||
label="Font"
|
||||
param="font"
|
||||
fonts={FONTS}
|
||||
isMobile={isMobile}
|
||||
anchorRef={anchorRef}
|
||||
/>
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||
<FieldSeparator className="hidden md:block" />
|
||||
<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>
|
||||
</FieldGroup>
|
||||
</CardContent>
|
||||
<CardFooter className="flex min-w-0 gap-2 md:flex-col md:**:[button,a]:w-full">
|
||||
<CopyPreset className="flex-1 md:flex-none" />
|
||||
<RandomButton className="flex-1 md:flex-none" />
|
||||
<ActionMenu itemsByBase={itemsByBase} />
|
||||
<ResetDialog />
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,131 +9,22 @@ import {
|
||||
} from "@/registry/config"
|
||||
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
|
||||
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
|
||||
|
||||
type RegistryThemeCssVars = NonNullable<
|
||||
ReturnType<typeof buildRegistryTheme>["cssVars"]
|
||||
>
|
||||
|
||||
function removeManagedBodyClasses(body: Element) {
|
||||
for (const className of Array.from(body.classList)) {
|
||||
if (
|
||||
MANAGED_BODY_CLASS_PREFIXES.some((prefix) => className.startsWith(prefix))
|
||||
) {
|
||||
body.classList.remove(className)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function buildCssRule(selector: string, cssVars?: Record<string, string>) {
|
||||
const declarations = Object.entries(cssVars ?? {})
|
||||
.filter(([, value]) => Boolean(value))
|
||||
.map(([key, value]) => ` --${key}: ${value};`)
|
||||
.join("\n")
|
||||
|
||||
if (!declarations) {
|
||||
return `${selector} {}\n`
|
||||
}
|
||||
|
||||
return `${selector} {\n${declarations}\n}\n`
|
||||
}
|
||||
|
||||
function buildThemeCssText(cssVars: RegistryThemeCssVars) {
|
||||
return [
|
||||
buildCssRule(":root", {
|
||||
...(cssVars.theme ?? {}),
|
||||
...(cssVars.light ?? {}),
|
||||
}),
|
||||
buildCssRule(".dark", cssVars.dark),
|
||||
].join("\n")
|
||||
}
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function DesignSystemProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
const [searchParams, setSearchParams] = useDesignSystemSearchParams({
|
||||
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 [isReady, setIsReady] = React.useState(false)
|
||||
const {
|
||||
style,
|
||||
theme,
|
||||
font,
|
||||
fontHeading,
|
||||
baseColor,
|
||||
chartColor,
|
||||
menuAccent,
|
||||
menuColor,
|
||||
radius,
|
||||
} = searchParams
|
||||
const effectiveRadius = style === "lyra" ? "none" : radius
|
||||
const selectedFont = React.useMemo(
|
||||
() => FONTS.find((fontOption) => fontOption.value === font),
|
||||
[font]
|
||||
)
|
||||
const selectedHeadingFont = React.useMemo(() => {
|
||||
if (fontHeading === "inherit" || fontHeading === font) {
|
||||
return selectedFont
|
||||
}
|
||||
|
||||
return FONTS.find((fontOption) => fontOption.value === fontHeading)
|
||||
}, [font, fontHeading, selectedFont])
|
||||
const initialFontSansRef = React.useRef<string | null>(null)
|
||||
const initialFontHeadingRef = React.useRef<string | null>(null)
|
||||
|
||||
React.useEffect(() => {
|
||||
initialFontSansRef.current =
|
||||
document.documentElement.style.getPropertyValue("--font-sans")
|
||||
initialFontHeadingRef.current =
|
||||
document.documentElement.style.getPropertyValue("--font-heading")
|
||||
|
||||
return () => {
|
||||
removeManagedBodyClasses(document.body)
|
||||
document.getElementById(THEME_STYLE_ELEMENT_ID)?.remove()
|
||||
|
||||
if (initialFontSansRef.current) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-sans",
|
||||
initialFontSansRef.current
|
||||
)
|
||||
} else {
|
||||
document.documentElement.style.removeProperty("--font-sans")
|
||||
}
|
||||
|
||||
if (initialFontHeadingRef.current) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-heading",
|
||||
initialFontHeadingRef.current
|
||||
)
|
||||
} else {
|
||||
document.documentElement.style.removeProperty("--font-heading")
|
||||
}
|
||||
}
|
||||
}, [])
|
||||
|
||||
const handleDesignSystemMessage = React.useCallback(
|
||||
(nextParams: DesignSystemSearchParams) => {
|
||||
setSearchParams(nextParams)
|
||||
},
|
||||
[setSearchParams]
|
||||
)
|
||||
|
||||
useIframeMessageListener("design-system-params", handleDesignSystemMessage)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (style === "lyra" && radius !== "none") {
|
||||
setSearchParams({ radius: "none" })
|
||||
}
|
||||
}, [style, radius, setSearchParams])
|
||||
|
||||
// Use useLayoutEffect for synchronous style updates to prevent flash.
|
||||
React.useLayoutEffect(() => {
|
||||
@@ -143,40 +34,34 @@ export function DesignSystemProvider({
|
||||
|
||||
const body = document.body
|
||||
|
||||
// Iterate over a snapshot so removals do not affect traversal.
|
||||
removeManagedBodyClasses(body)
|
||||
body.classList.add(`style-${style}`, `base-color-${baseColor}`)
|
||||
// Update style class in place (remove old, add new).
|
||||
body.classList.forEach((className) => {
|
||||
if (className.startsWith("style-")) {
|
||||
body.classList.remove(className)
|
||||
}
|
||||
})
|
||||
body.classList.add(`style-${style}`)
|
||||
|
||||
// Update base color class in place.
|
||||
body.classList.forEach((className) => {
|
||||
if (className.startsWith("base-color-")) {
|
||||
body.classList.remove(className)
|
||||
}
|
||||
})
|
||||
body.classList.add(`base-color-${baseColor}`)
|
||||
|
||||
// Update font.
|
||||
// Always set --font-sans for the preview so the selected font is visible.
|
||||
// The font type (sans/serif/mono) is metadata for the CLI updater.
|
||||
const selectedFont = FONTS.find((f) => f.value === font)
|
||||
if (selectedFont) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-sans",
|
||||
selectedFont.font.style.fontFamily
|
||||
)
|
||||
}
|
||||
|
||||
if (selectedHeadingFont) {
|
||||
document.documentElement.style.setProperty(
|
||||
"--font-heading",
|
||||
selectedHeadingFont.font.style.fontFamily
|
||||
)
|
||||
const fontFamily = selectedFont.font.style.fontFamily
|
||||
document.documentElement.style.setProperty("--font-sans", fontFamily)
|
||||
}
|
||||
|
||||
setIsReady(true)
|
||||
}, [
|
||||
style,
|
||||
theme,
|
||||
font,
|
||||
fontHeading,
|
||||
baseColor,
|
||||
selectedFont,
|
||||
selectedHeadingFont,
|
||||
])
|
||||
}, [style, theme, font, baseColor])
|
||||
|
||||
const registryTheme = React.useMemo(() => {
|
||||
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
|
||||
if (!baseColor || !theme || !menuAccent || !radius) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -184,13 +69,12 @@ export function DesignSystemProvider({
|
||||
...DEFAULT_CONFIG,
|
||||
baseColor,
|
||||
theme,
|
||||
chartColor,
|
||||
menuAccent,
|
||||
radius: effectiveRadius,
|
||||
radius,
|
||||
}
|
||||
|
||||
return buildRegistryTheme(config)
|
||||
}, [baseColor, theme, chartColor, menuAccent, effectiveRadius])
|
||||
}, [baseColor, theme, menuAccent, radius])
|
||||
|
||||
// Use useLayoutEffect for synchronous CSS var updates.
|
||||
React.useLayoutEffect(() => {
|
||||
@@ -198,83 +82,69 @@ export function DesignSystemProvider({
|
||||
return
|
||||
}
|
||||
|
||||
const styleId = "design-system-theme-vars"
|
||||
let styleElement = document.getElementById(
|
||||
THEME_STYLE_ELEMENT_ID
|
||||
styleId
|
||||
) as HTMLStyleElement | null
|
||||
|
||||
if (!styleElement) {
|
||||
styleElement = document.createElement("style")
|
||||
styleElement.id = THEME_STYLE_ELEMENT_ID
|
||||
styleElement.id = styleId
|
||||
document.head.appendChild(styleElement)
|
||||
}
|
||||
|
||||
styleElement.textContent = buildThemeCssText(registryTheme.cssVars)
|
||||
const {
|
||||
light: lightVars,
|
||||
dark: darkVars,
|
||||
theme: themeVars,
|
||||
} = registryTheme.cssVars
|
||||
|
||||
let cssText = ":root {\n"
|
||||
// Add theme vars (shared across light/dark).
|
||||
if (themeVars) {
|
||||
Object.entries(themeVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
// Add light mode vars.
|
||||
if (lightVars) {
|
||||
Object.entries(lightVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
cssText += "}\n\n"
|
||||
|
||||
cssText += ".dark {\n"
|
||||
if (darkVars) {
|
||||
Object.entries(darkVars).forEach(([key, value]) => {
|
||||
if (value) {
|
||||
cssText += ` --${key}: ${value};\n`
|
||||
}
|
||||
})
|
||||
}
|
||||
cssText += "}\n"
|
||||
|
||||
styleElement.textContent = cssText
|
||||
}, [registryTheme])
|
||||
|
||||
// Handle menu color inversion by adding/removing dark class to elements with cn-menu-target.
|
||||
// useLayoutEffect to apply classes synchronously before paint, avoiding flash.
|
||||
React.useLayoutEffect(() => {
|
||||
React.useEffect(() => {
|
||||
if (!menuColor) {
|
||||
return
|
||||
}
|
||||
|
||||
const isInvertedMenu =
|
||||
menuColor === "inverted" || menuColor === "inverted-translucent"
|
||||
const isTranslucentMenu =
|
||||
menuColor === "default-translucent" ||
|
||||
menuColor === "inverted-translucent"
|
||||
let frameId = 0
|
||||
|
||||
const updateMenuElements = () => {
|
||||
const allElements = document.querySelectorAll<HTMLElement>(
|
||||
".cn-menu-target, [data-menu-translucent]"
|
||||
)
|
||||
|
||||
if (allElements.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
// Disable transitions while toggling classes.
|
||||
allElements.forEach((element) => {
|
||||
element.style.transition = "none"
|
||||
})
|
||||
|
||||
allElements.forEach((element) => {
|
||||
if (element.classList.contains("cn-menu-target")) {
|
||||
if (isInvertedMenu) {
|
||||
element.classList.add("dark")
|
||||
} else {
|
||||
element.classList.remove("dark")
|
||||
}
|
||||
const menuElements = document.querySelectorAll(".cn-menu-target")
|
||||
menuElements.forEach((element) => {
|
||||
if (menuColor === "inverted") {
|
||||
element.classList.add("dark")
|
||||
} else {
|
||||
element.classList.remove("dark")
|
||||
}
|
||||
|
||||
// When translucent is enabled, move from data-attr to class so styles apply.
|
||||
// When disabled, move back to a data-attr so the element stays queryable
|
||||
// for future toggles without losing its identity as a menu element.
|
||||
if (isTranslucentMenu) {
|
||||
element.classList.add("cn-menu-translucent")
|
||||
element.removeAttribute("data-menu-translucent")
|
||||
} else if (element.classList.contains("cn-menu-translucent")) {
|
||||
element.classList.remove("cn-menu-translucent")
|
||||
element.setAttribute("data-menu-translucent", "")
|
||||
}
|
||||
})
|
||||
|
||||
// Force a reflow, then re-enable transitions.
|
||||
void document.body.offsetHeight
|
||||
allElements.forEach((element) => {
|
||||
element.style.transition = ""
|
||||
})
|
||||
}
|
||||
|
||||
const scheduleMenuUpdate = () => {
|
||||
if (frameId) {
|
||||
return
|
||||
}
|
||||
|
||||
frameId = window.requestAnimationFrame(() => {
|
||||
frameId = 0
|
||||
updateMenuElements()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -283,7 +153,7 @@ export function DesignSystemProvider({
|
||||
|
||||
// Watch for new menu elements being added to the DOM.
|
||||
const observer = new MutationObserver(() => {
|
||||
scheduleMenuUpdate()
|
||||
updateMenuElements()
|
||||
})
|
||||
|
||||
observer.observe(document.body, {
|
||||
@@ -293,9 +163,6 @@ export function DesignSystemProvider({
|
||||
|
||||
return () => {
|
||||
observer.disconnect()
|
||||
if (frameId) {
|
||||
window.cancelAnimationFrame(frameId)
|
||||
}
|
||||
}
|
||||
}, [menuColor])
|
||||
|
||||
|
||||
@@ -2,112 +2,55 @@
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import {
|
||||
Item,
|
||||
ItemContent,
|
||||
ItemDescription,
|
||||
ItemTitle,
|
||||
} from "@/registry/bases/radix/ui/item"
|
||||
import { type FontValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerLabel,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
type FontPickerOption = {
|
||||
name: string
|
||||
value: string
|
||||
type: string
|
||||
font: {
|
||||
style: {
|
||||
fontFamily: string
|
||||
}
|
||||
} | null
|
||||
}
|
||||
import { type Font } from "@/app/(create)/lib/fonts"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function FontPicker({
|
||||
label,
|
||||
param,
|
||||
fonts,
|
||||
isMobile,
|
||||
anchorRef,
|
||||
}: {
|
||||
label: string
|
||||
param: "font" | "fontHeading"
|
||||
fonts: readonly FontPickerOption[]
|
||||
fonts: readonly Font[]
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const currentValue = param === "font" ? params.font : params.fontHeading
|
||||
const handleFontChange = React.useCallback(
|
||||
(value: string) => {
|
||||
setParams({
|
||||
[param]: value,
|
||||
} as Partial<DesignSystemSearchParams>)
|
||||
},
|
||||
[param, setParams]
|
||||
)
|
||||
|
||||
const currentFont = React.useMemo(
|
||||
() => fonts.find((font) => font.value === currentValue),
|
||||
[fonts, currentValue]
|
||||
() => fonts.find((font) => font.value === params.font),
|
||||
[fonts, params.font]
|
||||
)
|
||||
const currentBodyFont = React.useMemo(
|
||||
() => FONTS.find((font) => font.value === params.font),
|
||||
[params.font]
|
||||
)
|
||||
const inheritsBodyFont = param === "fontHeading" && currentValue === "inherit"
|
||||
const displayFontName = inheritsBodyFont
|
||||
? currentBodyFont?.name
|
||||
: currentFont?.name
|
||||
const inheritFontLabel = currentBodyFont ? currentBodyFont.name : "Body font"
|
||||
const groupedFonts = React.useMemo(() => {
|
||||
const pickerFonts =
|
||||
param === "fontHeading"
|
||||
? fonts.filter((font) => font.value !== "inherit")
|
||||
: fonts
|
||||
const groups = new Map<string, FontPickerOption[]>()
|
||||
|
||||
for (const font of pickerFonts) {
|
||||
const existing = groups.get(font.type)
|
||||
if (existing) {
|
||||
existing.push(font)
|
||||
continue
|
||||
}
|
||||
|
||||
groups.set(font.type, [font])
|
||||
}
|
||||
|
||||
return Array.from(groups.entries()).map(([type, items]) => ({
|
||||
type,
|
||||
label: `${type.charAt(0).toUpperCase()}${type.slice(1)}`,
|
||||
items,
|
||||
}))
|
||||
}, [fonts, param])
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">{label}</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
{displayFontName}
|
||||
<div className="text-muted-foreground text-xs">Font</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentFont?.name}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5"
|
||||
style={{
|
||||
fontFamily:
|
||||
currentFont?.font?.style.fontFamily ??
|
||||
currentBodyFont?.font.style.fontFamily,
|
||||
}}
|
||||
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"
|
||||
style={{ fontFamily: currentFont?.font.style.fontFamily }}
|
||||
>
|
||||
Aa
|
||||
</div>
|
||||
@@ -116,42 +59,44 @@ export function FontPicker({
|
||||
anchor={isMobile ? anchorRef : undefined}
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
className="max-h-96"
|
||||
className="max-h-96 md:w-72"
|
||||
>
|
||||
<PickerRadioGroup
|
||||
value={currentValue}
|
||||
onValueChange={handleFontChange}
|
||||
value={currentFont?.value}
|
||||
onValueChange={(value) => {
|
||||
setParams({ font: value as FontValue })
|
||||
}}
|
||||
>
|
||||
{param === "fontHeading" ? (
|
||||
<>
|
||||
<PickerGroup>
|
||||
<PickerRadioItem value="inherit" closeOnClick={isMobile}>
|
||||
{inheritFontLabel}
|
||||
<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>
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
</>
|
||||
) : null}
|
||||
{groupedFonts.map((group) => (
|
||||
<PickerGroup key={group.type}>
|
||||
<PickerLabel>{group.label}</PickerLabel>
|
||||
{group.items.map((font) => (
|
||||
<PickerRadioItem
|
||||
key={font.value}
|
||||
value={font.value}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{font.name}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
</PickerGroup>
|
||||
))}
|
||||
{index < fonts.length - 1 && (
|
||||
<PickerSeparator className="opacity-50" />
|
||||
)}
|
||||
</React.Fragment>
|
||||
))}
|
||||
</PickerGroup>
|
||||
</PickerRadioGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
<LockButton
|
||||
param={param}
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
param="font"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import Script from "next/script"
|
||||
import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { useHistory } from "@/app/(create)/hooks/use-history"
|
||||
|
||||
export const UNDO_FORWARD_TYPE = "undo-forward"
|
||||
export const REDO_FORWARD_TYPE = "redo-forward"
|
||||
|
||||
export function HistoryButtons() {
|
||||
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Undo"
|
||||
disabled={!canGoBack}
|
||||
onClick={goBack}
|
||||
>
|
||||
<HugeiconsIcon icon={Undo02Icon} />
|
||||
<span className="sr-only">Undo</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
title="Redo"
|
||||
disabled={!canGoForward}
|
||||
onClick={goForward}
|
||||
>
|
||||
<HugeiconsIcon icon={Redo02Icon} />
|
||||
<span className="sr-only">Redo</span>
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function HistoryScript() {
|
||||
return (
|
||||
<Script
|
||||
id="history-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (!e.metaKey && !e.ctrlKey) return;
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
var key = e.key.toLowerCase();
|
||||
if ((key === 'z' && e.shiftKey) || (key === 'y' && e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: '${REDO_FORWARD_TYPE}' }, '*');
|
||||
}
|
||||
} else if (key === 'z') {
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({ type: '${UNDO_FORWARD_TYPE}' }, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,14 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { lazy, memo, Suspense } from "react"
|
||||
|
||||
import { iconLibraries, type IconLibraryName } from "@/registry/config"
|
||||
import { Item, ItemContent, ItemTitle } from "@/registry/bases/radix/ui/item"
|
||||
import {
|
||||
iconLibraries,
|
||||
type IconLibrary,
|
||||
type IconLibraryName,
|
||||
} from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
Picker,
|
||||
@@ -10,10 +16,124 @@ import {
|
||||
PickerGroup,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
const IconLucide = lazy(() =>
|
||||
import("@/registry/icons/icon-lucide").then((mod) => ({
|
||||
default: mod.IconLucide,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconTabler = lazy(() =>
|
||||
import("@/registry/icons/icon-tabler").then((mod) => ({
|
||||
default: mod.IconTabler,
|
||||
}))
|
||||
)
|
||||
|
||||
const IconHugeicons = lazy(() =>
|
||||
import("@/registry/icons/icon-hugeicons").then((mod) => ({
|
||||
default: mod.IconHugeicons,
|
||||
}))
|
||||
)
|
||||
|
||||
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",
|
||||
"CircleAlertIcon",
|
||||
"TrashIcon",
|
||||
"ShareIcon",
|
||||
"ShoppingBagIcon",
|
||||
"MoreHorizontalIcon",
|
||||
"Loader2Icon",
|
||||
"PlusIcon",
|
||||
"MinusIcon",
|
||||
"ArrowLeftIcon",
|
||||
"ArrowRightIcon",
|
||||
"CheckIcon",
|
||||
"ChevronDownIcon",
|
||||
"ChevronRightIcon",
|
||||
],
|
||||
tabler: [
|
||||
"IconCopy",
|
||||
"IconExclamationCircle",
|
||||
"IconTrash",
|
||||
"IconShare",
|
||||
"IconShoppingBag",
|
||||
"IconDots",
|
||||
"IconLoader",
|
||||
"IconPlus",
|
||||
"IconMinus",
|
||||
"IconArrowLeft",
|
||||
"IconArrowRight",
|
||||
"IconCheck",
|
||||
"IconChevronDown",
|
||||
"IconChevronRight",
|
||||
],
|
||||
hugeicons: [
|
||||
"Copy01Icon",
|
||||
"AlertCircleIcon",
|
||||
"Delete02Icon",
|
||||
"Share03Icon",
|
||||
"ShoppingBag01Icon",
|
||||
"MoreHorizontalCircle01Icon",
|
||||
"Loading03Icon",
|
||||
"PlusSignIcon",
|
||||
"MinusSignIcon",
|
||||
"ArrowLeft02Icon",
|
||||
"ArrowRight02Icon",
|
||||
"Tick02Icon",
|
||||
"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 = {
|
||||
lucide: (
|
||||
<svg
|
||||
@@ -128,12 +248,12 @@ export function IconLibraryPicker({
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Icon Library</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-muted-foreground text-xs">Icon Library</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentIconLibrary?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5 *:[svg]:text-foreground!">
|
||||
<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">
|
||||
{logos[currentIconLibrary?.name as keyof typeof logos]}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
@@ -149,14 +269,16 @@ export function IconLibraryPicker({
|
||||
}}
|
||||
>
|
||||
<PickerGroup>
|
||||
{Object.values(iconLibraries).map((iconLibrary) => (
|
||||
<PickerRadioItem
|
||||
key={iconLibrary.name}
|
||||
value={iconLibrary.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{iconLibrary.title}
|
||||
</PickerRadioItem>
|
||||
{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>
|
||||
@@ -164,8 +286,81 @@ export function IconLibraryPicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="iconLibrary"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function IconLibraryPickerItem({
|
||||
iconLibrary,
|
||||
value,
|
||||
}: {
|
||||
iconLibrary: IconLibrary
|
||||
value: string
|
||||
}) {
|
||||
return (
|
||||
<PickerRadioItem
|
||||
value={value}
|
||||
className="pr-2 *:data-[slot=dropdown-menu-radio-item-indicator]:hidden"
|
||||
>
|
||||
<Item size="xs">
|
||||
<ItemContent className="gap-1">
|
||||
<ItemTitle className="text-muted-foreground text-xs font-medium">
|
||||
{iconLibrary.title}
|
||||
</ItemTitle>
|
||||
<IconLibraryPreview iconLibrary={iconLibrary.name} />
|
||||
</ItemContent>
|
||||
</Item>
|
||||
</PickerRadioItem>
|
||||
)
|
||||
}
|
||||
|
||||
const IconLibraryPreview = memo(function IconLibraryPreview({
|
||||
iconLibrary,
|
||||
}: {
|
||||
iconLibrary: IconLibraryName
|
||||
}) {
|
||||
const previewIcons = PREVIEW_ICONS[iconLibrary]
|
||||
|
||||
if (!previewIcons) {
|
||||
return null
|
||||
}
|
||||
|
||||
const IconRenderer =
|
||||
iconLibrary === "lucide"
|
||||
? IconLucide
|
||||
: iconLibrary === "tabler"
|
||||
? IconTabler
|
||||
: iconLibrary === "hugeicons"
|
||||
? IconHugeicons
|
||||
: iconLibrary === "phosphor"
|
||||
? IconPhosphor
|
||||
: IconRemixicon
|
||||
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="-mx-1 grid w-full grid-cols-7 gap-2">
|
||||
{previewIcons.map((iconName) => (
|
||||
<div
|
||||
key={iconName}
|
||||
className="bg-muted size-6 animate-pulse rounded"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div className="-mx-1 grid w-full grid-cols-7 gap-2">
|
||||
{previewIcons.map((iconName) => (
|
||||
<div
|
||||
key={iconName}
|
||||
className="flex size-6 items-center justify-center *:[svg]:size-5"
|
||||
>
|
||||
<IconRenderer name={iconName} />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Suspense>
|
||||
)
|
||||
})
|
||||
|
||||
@@ -36,15 +36,6 @@ const IconRemixicon = lazy(() =>
|
||||
}))
|
||||
)
|
||||
|
||||
// Preload all icon renderer modules so switching libraries is instant.
|
||||
// These warm the browser module cache; React.lazy resolves immediately
|
||||
// for modules that are already loaded.
|
||||
void import("@/registry/icons/icon-lucide")
|
||||
void import("@/registry/icons/icon-tabler")
|
||||
void import("@/registry/icons/icon-hugeicons")
|
||||
void import("@/registry/icons/icon-phosphor")
|
||||
void import("@/registry/icons/icon-remixicon")
|
||||
|
||||
export function IconPlaceholder({
|
||||
...props
|
||||
}: {
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from "@/styles/base-nova/ui/collapsible"
|
||||
} from "@/registry/new-york-v4/ui/collapsible"
|
||||
import {
|
||||
Sidebar,
|
||||
SidebarContent,
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
SidebarMenu,
|
||||
SidebarMenuButton,
|
||||
SidebarMenuItem,
|
||||
} from "@/styles/base-nova/ui/sidebar"
|
||||
} from "@/registry/new-york-v4/ui/sidebar"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||
|
||||
@@ -48,10 +48,10 @@ export function ItemExplorer({
|
||||
|
||||
return (
|
||||
<Sidebar
|
||||
className="sticky z-30 hidden h-full overscroll-none bg-transparent xl:flex"
|
||||
className="sticky z-30 hidden h-[calc(100svh-var(--header-height)-2rem)] overscroll-none bg-transparent xl:flex"
|
||||
collapsible="none"
|
||||
>
|
||||
<SidebarContent className="-mx-1 no-scrollbar overflow-x-hidden">
|
||||
<SidebarContent className="no-scrollbar -mx-1 overflow-x-hidden">
|
||||
{groupedItems.map((group) => (
|
||||
<Collapsible
|
||||
key={group.type}
|
||||
@@ -60,26 +60,26 @@ export function ItemExplorer({
|
||||
>
|
||||
<SidebarGroup className="px-1 py-0">
|
||||
<CollapsibleTrigger className="flex w-full items-center gap-1 py-1.5 text-[0.8rem] font-medium [&[data-state=open]>svg]:rotate-90">
|
||||
<ChevronRightIcon className="size-3.5 text-muted-foreground transition-transform" />
|
||||
<ChevronRightIcon className="text-muted-foreground size-3.5 transition-transform" />
|
||||
<span>{group.title}</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<SidebarGroupContent>
|
||||
<SidebarMenu className="relative ml-1.5 border-l border-border/50 pl-2">
|
||||
<SidebarMenu className="border-border/50 relative ml-1.5 border-l pl-2">
|
||||
{group.items.map((item, index) => (
|
||||
<SidebarMenuItem key={item.name} className="relative">
|
||||
<div
|
||||
className={cn(
|
||||
"absolute top-1/2 -left-2 h-px w-2 border-t border-border/50",
|
||||
"border-border/50 absolute top-1/2 -left-2 h-px w-2 border-t",
|
||||
index === group.items.length - 1 && "bg-sidebar"
|
||||
)}
|
||||
/>
|
||||
{index === group.items.length - 1 && (
|
||||
<div className="absolute top-1/2 -bottom-1 -left-2.5 w-1 bg-sidebar" />
|
||||
<div className="bg-sidebar absolute top-1/2 -bottom-1 -left-2.5 w-1" />
|
||||
)}
|
||||
<SidebarMenuButton
|
||||
onClick={() => setParams({ item: item.name })}
|
||||
className="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=true]:border-accent data-[active=true]:bg-accent 3xl:fixed:w-full 3xl:fixed:max-w-48"
|
||||
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}
|
||||
>
|
||||
|
||||
193
apps/v4/app/(create)/components/item-picker.tsx
Normal file
193
apps/v4/app/(create)/components/item-picker.tsx
Normal file
@@ -0,0 +1,193 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Script from "next/script"
|
||||
import { Search01Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { type RegistryItem } from "shadcn/schema"
|
||||
|
||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||
import {
|
||||
Combobox,
|
||||
ComboboxCollection,
|
||||
ComboboxContent,
|
||||
ComboboxEmpty,
|
||||
ComboboxGroup,
|
||||
ComboboxInput,
|
||||
ComboboxItem,
|
||||
ComboboxLabel,
|
||||
ComboboxList,
|
||||
ComboboxTrigger,
|
||||
ComboboxValue,
|
||||
} from "@/registry/new-york-v4/ui/combobox"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||
|
||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||
|
||||
const cachedGroupedItems = React.cache(
|
||||
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
|
||||
return groupItemsByType(items)
|
||||
}
|
||||
)
|
||||
|
||||
export function ItemPicker({
|
||||
items,
|
||||
}: {
|
||||
items: Pick<RegistryItem, "name" | "title" | "type">[]
|
||||
}) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
|
||||
const groupedItems = React.useMemo(() => cachedGroupedItems(items), [items])
|
||||
|
||||
const currentItem = React.useMemo(
|
||||
() => items.find((item) => item.name === params.item) ?? null,
|
||||
[items, params.item]
|
||||
)
|
||||
|
||||
React.useEffect(() => {
|
||||
const down = (e: KeyboardEvent) => {
|
||||
if ((e.key === "k" || e.key === "p") && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault()
|
||||
setOpen((open) => !open)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener("keydown", down)
|
||||
return () => document.removeEventListener("keydown", down)
|
||||
}, [])
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(item: Pick<RegistryItem, "name" | "title" | "type">) => {
|
||||
setParams({ item: item.name })
|
||||
setOpen(false)
|
||||
},
|
||||
[setParams]
|
||||
)
|
||||
|
||||
const comboboxValue = React.useMemo(() => {
|
||||
return currentItem ?? null
|
||||
}, [currentItem])
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
autoHighlight
|
||||
items={groupedItems}
|
||||
value={comboboxValue}
|
||||
onValueChange={(value) => {
|
||||
if (value) {
|
||||
handleSelect(value)
|
||||
}
|
||||
}}
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
itemToStringValue={(item) => {
|
||||
if (!item) {
|
||||
return ""
|
||||
}
|
||||
// Handle both groups and items.
|
||||
if ("items" in item) {
|
||||
return item.title ?? ""
|
||||
}
|
||||
return item.title ?? item.name ?? ""
|
||||
}}
|
||||
>
|
||||
<ComboboxTrigger
|
||||
render={
|
||||
<Button
|
||||
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"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<ComboboxValue>
|
||||
{(value) => (
|
||||
<>
|
||||
<div className="flex flex-col justify-start text-left sm:hidden">
|
||||
<div className="text-muted-foreground text-xs font-normal">
|
||||
Preview
|
||||
</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{value?.title || "Not Found"}
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-foreground hidden flex-1 text-sm sm:flex">
|
||||
{value?.title || "Not Found"}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</ComboboxValue>
|
||||
<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"
|
||||
side="bottom"
|
||||
align="end"
|
||||
>
|
||||
<ComboboxInput
|
||||
showTrigger={false}
|
||||
placeholder="Search"
|
||||
className="bg-muted h-8 rounded-lg shadow-none has-focus-visible:border-inherit! has-focus-visible:ring-0! pointer-coarse:hidden"
|
||||
/>
|
||||
<ComboboxEmpty>No items found.</ComboboxEmpty>
|
||||
<ComboboxList className="no-scrollbar scroll-my-1 pb-1">
|
||||
{(group) => (
|
||||
<ComboboxGroup key={group.type} items={group.items}>
|
||||
<ComboboxLabel>{group.title}</ComboboxLabel>
|
||||
<ComboboxCollection>
|
||||
{(item) => (
|
||||
<ComboboxItem
|
||||
key={item.name}
|
||||
value={item}
|
||||
className="group/combobox-item rounded-lg pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base"
|
||||
>
|
||||
{item.title}
|
||||
<span className="text-muted-foreground ml-auto text-xs opacity-0 group-data-[selected=true]/combobox-item:opacity-100">
|
||||
{group.title}
|
||||
</span>
|
||||
</ComboboxItem>
|
||||
)}
|
||||
</ComboboxCollection>
|
||||
</ComboboxGroup>
|
||||
)}
|
||||
</ComboboxList>
|
||||
</ComboboxContent>
|
||||
<div
|
||||
data-open={open}
|
||||
className="fixed inset-0 z-50 hidden bg-transparent data-[open=true]:block"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
</Combobox>
|
||||
)
|
||||
}
|
||||
|
||||
export function ItemPickerScript() {
|
||||
return (
|
||||
<Script
|
||||
id="design-system-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward Cmd/Ctrl + K and Cmd/Ctrl + P
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'k' || e.key === 'p') && (e.metaKey || e.ctrlKey)) {
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: '${CMD_K_FORWARD_TYPE}',
|
||||
key: e.key
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -7,6 +7,11 @@ import {
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/registry/new-york-v4/ui/tooltip"
|
||||
import { useLocks, type LockableParam } from "@/app/(create)/hooks/use-locks"
|
||||
|
||||
export function LockButton({
|
||||
@@ -20,22 +25,26 @@ export function LockButton({
|
||||
const locked = isLocked(param)
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
title={locked ? "Unlock" : "Lock"}
|
||||
aria-label={locked ? "Unlock" : "Lock"}
|
||||
onClick={() => toggleLock(param)}
|
||||
data-locked={locked}
|
||||
className={cn(
|
||||
"flex size-4 cursor-pointer items-center justify-center rounded opacity-0 ring-foreground/60 transition-opacity outline-none group-focus-within/picker:opacity-100 group-hover/picker:opacity-100 focus:opacity-100 focus-visible:ring-1 data-[locked=true]:opacity-100 pointer-coarse:hidden",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={locked ? SquareLock01Icon : SquareUnlock01Icon}
|
||||
strokeWidth={2}
|
||||
className="size-5 text-foreground"
|
||||
/>
|
||||
</button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleLock(param)}
|
||||
data-locked={locked}
|
||||
className={cn(
|
||||
"flex size-4 cursor-pointer items-center justify-center rounded opacity-0 transition-opacity group-focus-within/picker:opacity-100 group-hover/picker:opacity-100 focus:opacity-100 data-[locked=true]:opacity-100 pointer-coarse:hidden",
|
||||
className
|
||||
)}
|
||||
aria-label={locked ? "Unlock" : "Lock"}
|
||||
>
|
||||
<HugeiconsIcon
|
||||
icon={locked ? SquareLock01Icon : SquareUnlock01Icon}
|
||||
strokeWidth={2}
|
||||
className="text-foreground size-5"
|
||||
/>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{locked ? "Unlock" : "Lock"}</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Menu09Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { type Button } from "@/styles/base-nova/ui/button"
|
||||
import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerItem,
|
||||
PickerSeparator,
|
||||
PickerShortcut,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import { useActionMenuTrigger } from "@/app/(create)/hooks/use-action-menu"
|
||||
import { useHistory } from "@/app/(create)/hooks/use-history"
|
||||
import { useRandom } from "@/app/(create)/hooks/use-random"
|
||||
import { useReset } from "@/app/(create)/hooks/use-reset"
|
||||
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
||||
|
||||
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
|
||||
|
||||
export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
|
||||
const [isMac, setIsMac] = React.useState(false)
|
||||
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
|
||||
const { openActionMenu } = useActionMenuTrigger()
|
||||
const { randomize } = useRandom()
|
||||
const { toggleTheme } = useThemeToggle()
|
||||
const { setShowResetDialog } = useReset()
|
||||
|
||||
React.useEffect(() => {
|
||||
const platform = navigator.platform
|
||||
const userAgent = navigator.userAgent
|
||||
setIsMac(APPLE_PLATFORM_REGEX.test(platform || userAgent))
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<Picker>
|
||||
<PickerTrigger
|
||||
className={cn(
|
||||
"flex items-center justify-between gap-2 rounded-lg px-1.75 ring-1 ring-foreground/10 focus-visible:ring-1",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="font-medium">Menu</span>
|
||||
<HugeiconsIcon icon={Menu09Icon} strokeWidth={2} className="size-5" />
|
||||
</PickerTrigger>
|
||||
<PickerContent side="right" align="start" alignOffset={-8}>
|
||||
<PickerGroup>
|
||||
<PickerItem onClick={openActionMenu}>
|
||||
Navigate...
|
||||
<PickerShortcut>{isMac ? "⌘P" : "Ctrl+P"}</PickerShortcut>
|
||||
</PickerItem>
|
||||
<PickerItem onClick={randomize}>
|
||||
Shuffle <PickerShortcut>R</PickerShortcut>
|
||||
</PickerItem>
|
||||
<PickerItem onClick={toggleTheme}>
|
||||
Light/Dark <PickerShortcut>D</PickerShortcut>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerItem onClick={goBack} disabled={!canGoBack}>
|
||||
Undo <PickerShortcut>{isMac ? "⌘Z" : "Ctrl+Z"}</PickerShortcut>
|
||||
</PickerItem>
|
||||
<PickerItem onClick={goForward} disabled={!canGoForward}>
|
||||
Redo{" "}
|
||||
<PickerShortcut>{isMac ? "⇧⌘Z" : "Ctrl+Shift+Z"}</PickerShortcut>
|
||||
</PickerItem>
|
||||
<PickerSeparator />
|
||||
<PickerItem onClick={() => setShowResetDialog(true)}>
|
||||
Reset <PickerShortcut>⇧R</PickerShortcut>
|
||||
</PickerItem>
|
||||
</PickerGroup>
|
||||
</PickerContent>
|
||||
</Picker>
|
||||
</React.Fragment>
|
||||
)
|
||||
}
|
||||
@@ -1,8 +1,6 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Menu02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
import { useTheme } from "next-themes"
|
||||
|
||||
import { useMounted } from "@/hooks/use-mounted"
|
||||
@@ -12,37 +10,99 @@ import {
|
||||
Picker,
|
||||
PickerContent,
|
||||
PickerGroup,
|
||||
PickerLabel,
|
||||
PickerRadioGroup,
|
||||
PickerRadioItem,
|
||||
PickerSeparator,
|
||||
PickerTrigger,
|
||||
} from "@/app/(create)/components/picker"
|
||||
import {
|
||||
isTranslucentMenuColor,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
type ColorChoice = "default" | "inverted"
|
||||
type SurfaceChoice = "solid" | "translucent"
|
||||
|
||||
function getMenuColorValue(
|
||||
color: ColorChoice,
|
||||
translucent: boolean
|
||||
): MenuColorValue {
|
||||
if (color === "default") {
|
||||
return translucent ? "default-translucent" : "default"
|
||||
}
|
||||
|
||||
return translucent ? "inverted-translucent" : "inverted"
|
||||
}
|
||||
|
||||
const MENU_OPTIONS: { value: MenuColorValue; label: string }[] = [
|
||||
{ value: "default", label: "Default / Solid" },
|
||||
{ value: "default-translucent", label: "Default / Translucent" },
|
||||
{ value: "inverted", label: "Inverted / Solid" },
|
||||
{ value: "inverted-translucent", label: "Inverted / Translucent" },
|
||||
]
|
||||
const MENU_OPTIONS = [
|
||||
{
|
||||
value: "default" as const,
|
||||
label: "Default",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
stroke="currentColor"
|
||||
className="text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M8.5 11.5L14.5001 11.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.5 15H13.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 8H15.5"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: "inverted" as const,
|
||||
label: "Inverted",
|
||||
icon: (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="128"
|
||||
height="128"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
role="img"
|
||||
className="fill-foreground text-foreground"
|
||||
>
|
||||
<path
|
||||
d="M2 11.5C2 7.02166 2 4.78249 3.39124 3.39124C4.78249 2 7.02166 2 11.5 2C15.9783 2 18.2175 2 19.6088 3.39124C21 4.78249 21 7.02166 21 11.5C21 15.9783 21 18.2175 19.6088 19.6088C18.2175 21 15.9783 21 11.5 21C7.02166 21 4.78249 21 3.39124 19.6088C2 18.2175 2 15.9783 2 11.5Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
/>
|
||||
<path
|
||||
d="M8.5 11.5L14.5001 11.5"
|
||||
stroke="var(--background)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M9.5 15H13.5"
|
||||
stroke="var(--background)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path
|
||||
d="M7.5 8H15.5"
|
||||
stroke="var(--background)"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
] as const
|
||||
|
||||
export function MenuColorPicker({
|
||||
isMobile,
|
||||
@@ -51,69 +111,25 @@ export function MenuColorPicker({
|
||||
isMobile: boolean
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const { resolvedTheme } = useTheme()
|
||||
const mounted = useMounted()
|
||||
const lastSolidMenuAccentRef = React.useRef(params.menuAccent)
|
||||
const isDark = mounted && resolvedTheme === "dark"
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const currentMenu = MENU_OPTIONS.find(
|
||||
(menu) => menu.value === params.menuColor
|
||||
)
|
||||
const colorChoice: ColorChoice =
|
||||
params.menuColor === "inverted" ||
|
||||
params.menuColor === "inverted-translucent"
|
||||
? "inverted"
|
||||
: "default"
|
||||
const surfaceChoice: SurfaceChoice =
|
||||
params.menuColor === "default-translucent" ||
|
||||
params.menuColor === "inverted-translucent"
|
||||
? "translucent"
|
||||
: "solid"
|
||||
|
||||
React.useEffect(() => {
|
||||
if (surfaceChoice === "solid") {
|
||||
lastSolidMenuAccentRef.current = params.menuAccent
|
||||
}
|
||||
}, [params.menuAccent, surfaceChoice])
|
||||
|
||||
const setColor = (color: ColorChoice) => {
|
||||
const nextMenuColor = getMenuColorValue(
|
||||
color,
|
||||
surfaceChoice === "translucent"
|
||||
)
|
||||
|
||||
setParams({
|
||||
menuColor: nextMenuColor,
|
||||
...(isTranslucentMenuColor(nextMenuColor) && { menuAccent: "subtle" }),
|
||||
})
|
||||
}
|
||||
|
||||
const setSurface = (choice: SurfaceChoice) => {
|
||||
const isTranslucent = choice === "translucent"
|
||||
const nextMenuColor = getMenuColorValue(colorChoice, isTranslucent)
|
||||
|
||||
setParams({
|
||||
menuColor: nextMenuColor,
|
||||
menuAccent: isTranslucent ? "subtle" : lastSolidMenuAccentRef.current,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<PickerTrigger disabled={mounted && resolvedTheme === "dark"}>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Menu</div>
|
||||
<div className="line-clamp-1 max-w-[80%] truncate text-sm font-medium text-foreground">
|
||||
<div className="text-muted-foreground text-xs">Menu Color</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentMenu?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
|
||||
<HugeiconsIcon
|
||||
icon={Menu02Icon}
|
||||
strokeWidth={2}
|
||||
className="size-4"
|
||||
/>
|
||||
<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">
|
||||
{currentMenu?.icon}
|
||||
</div>
|
||||
</PickerTrigger>
|
||||
<PickerContent
|
||||
@@ -121,48 +137,26 @@ export function MenuColorPicker({
|
||||
side={isMobile ? "top" : "right"}
|
||||
align={isMobile ? "center" : "start"}
|
||||
>
|
||||
<PickerGroup>
|
||||
<PickerLabel>Color</PickerLabel>
|
||||
<PickerRadioGroup
|
||||
value={colorChoice}
|
||||
onValueChange={(value) => {
|
||||
setColor(value as ColorChoice)
|
||||
}}
|
||||
>
|
||||
<PickerRadioItem value="default" closeOnClick={isMobile}>
|
||||
Default
|
||||
</PickerRadioItem>
|
||||
<PickerRadioItem
|
||||
value="inverted"
|
||||
closeOnClick={isMobile}
|
||||
disabled={isDark}
|
||||
>
|
||||
Inverted
|
||||
</PickerRadioItem>
|
||||
</PickerRadioGroup>
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
<PickerLabel>Appearance</PickerLabel>
|
||||
<PickerRadioGroup
|
||||
value={surfaceChoice}
|
||||
onValueChange={(value) => {
|
||||
setSurface(value as SurfaceChoice)
|
||||
}}
|
||||
>
|
||||
<PickerRadioItem value="solid" closeOnClick={isMobile}>
|
||||
Solid
|
||||
</PickerRadioItem>
|
||||
<PickerRadioItem value="translucent" closeOnClick={isMobile}>
|
||||
Translucent
|
||||
</PickerRadioItem>
|
||||
</PickerRadioGroup>
|
||||
</PickerGroup>
|
||||
<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-8 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import Script from "next/script"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
||||
|
||||
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
||||
|
||||
export function ModeSwitcher({
|
||||
variant = "ghost",
|
||||
className,
|
||||
}: {
|
||||
variant?: React.ComponentProps<typeof Button>["variant"]
|
||||
className?: React.ComponentProps<typeof Button>["className"]
|
||||
}) {
|
||||
const { toggleTheme } = useThemeToggle()
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={variant}
|
||||
size="icon"
|
||||
className={cn("group/toggle extend-touch-target", className)}
|
||||
onClick={toggleTheme}
|
||||
id="mode-switcher-button"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className="size-4.5"
|
||||
>
|
||||
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
|
||||
<path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" />
|
||||
<path d="M12 3l0 18" />
|
||||
<path d="M12 9l4.65 -4.65" />
|
||||
<path d="M12 14.3l7.37 -7.37" />
|
||||
<path d="M12 19.6l8.85 -8.85" />
|
||||
</svg>
|
||||
<span className="sr-only">Toggle theme</span>
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
export function DarkModeScript() {
|
||||
return (
|
||||
<Script
|
||||
id="dark-mode-listener"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `
|
||||
(function() {
|
||||
// Forward D key
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if ((e.key === 'd' || e.key === 'D') && !e.metaKey && !e.ctrlKey && !e.altKey) {
|
||||
if (
|
||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||
e.target instanceof HTMLInputElement ||
|
||||
e.target instanceof HTMLTextAreaElement ||
|
||||
e.target instanceof HTMLSelectElement
|
||||
) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
if (window.parent && window.parent !== window) {
|
||||
window.parent.postMessage({
|
||||
type: '${DARK_MODE_FORWARD_TYPE}',
|
||||
key: e.key
|
||||
}, '*');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
})();
|
||||
`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
|
||||
<MenuPrimitive.Trigger
|
||||
data-slot="dropdown-menu-trigger"
|
||||
className={cn(
|
||||
"relative w-40 shrink-0 touch-manipulation rounded-xl p-3 ring-1 ring-foreground/10 select-none hover:bg-muted focus-visible:ring-foreground/50 focus-visible:outline-none disabled:opacity-50 data-popup-open:bg-muted md:w-full md:rounded-lg md:px-2.5 md:py-2",
|
||||
"hover:bg-muted data-popup-open:bg-muted border-foreground/10 bg-muted/50 relative w-[160px] shrink-0 touch-manipulation rounded-xl border p-2 select-none disabled:opacity-50 md:w-full md:rounded-lg md:border-transparent md:bg-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -31,7 +31,7 @@ function PickerContent({
|
||||
align = "start",
|
||||
alignOffset = 0,
|
||||
side = "bottom",
|
||||
sideOffset = 20,
|
||||
sideOffset = 4,
|
||||
anchor,
|
||||
className,
|
||||
...props
|
||||
@@ -53,13 +53,13 @@ function PickerContent({
|
||||
<MenuPrimitive.Popup
|
||||
data-slot="dropdown-menu-content"
|
||||
className={cn(
|
||||
"cn-menu-target z-50 no-scrollbar max-h-(--available-height) w-[calc(var(--available-width)-(--spacing(6)))] min-w-32 origin-(--transform-origin) translate-y-2 overflow-x-hidden overflow-y-auto rounded-xl border-0 bg-neutral-950/80 p-1.5 text-neutral-100 ring-1 ring-neutral-950/80 backdrop-blur-xl outline-none md:w-52 dark:bg-neutral-800/90 dark:ring-neutral-700/50 data-closed:overflow-hidden",
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 bg-popover text-popover-foreground cn-menu-target ring-foreground/10 no-scrollbar z-50 max-h-(--available-height) w-[calc(var(--available-width)-(--spacing(3.5)))] min-w-32 origin-(--transform-origin) overflow-x-hidden overflow-y-auto rounded-xl border-0 p-1 shadow-md ring-1 duration-100 outline-none data-closed:overflow-hidden md:w-52",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
</MenuPrimitive.Positioner>
|
||||
<div className="absolute inset-y-0 right-0 left-62 z-40 bg-transparent" />
|
||||
<div className="absolute inset-0 z-40 bg-transparent" />
|
||||
</MenuPrimitive.Portal>
|
||||
)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ function PickerLabel({
|
||||
data-slot="dropdown-menu-label"
|
||||
data-inset={inset}
|
||||
className={cn(
|
||||
"px-2 py-1.5 text-xs font-medium text-neutral-400 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(
|
||||
"group/dropdown-menu-item relative flex cursor-default items-center gap-2 rounded-lg px-2 py-1.5 text-sm font-medium outline-hidden select-none **:text-neutral-100 focus:bg-neutral-600 focus:text-neutral-100 focus:**:text-neutral-100 data-inset:pl-8 dark:focus:bg-neutral-700/80 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base 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 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(
|
||||
"flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent/95 focus:text-accent-foreground focus:ring-1 focus:ring-foreground/20 not-data-[variant=destructive]:focus:**:text-accent-foreground data-inset:pl-8 data-open:bg-accent/95 data-open:text-accent-foreground [&_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}
|
||||
@@ -158,7 +158,7 @@ function PickerSubContent({
|
||||
<PickerContent
|
||||
data-slot="dropdown-menu-sub-content"
|
||||
className={cn(
|
||||
"w-auto min-w-[96px] rounded-md bg-popover/90 p-1 text-popover-foreground shadow-lg ring-1 ring-foreground/10 backdrop-blur-xs duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95",
|
||||
"data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground w-auto min-w-[96px] rounded-md p-1 shadow-lg ring-1 duration-100",
|
||||
className
|
||||
)}
|
||||
align={align}
|
||||
@@ -180,7 +180,7 @@ function PickerCheckboxItem({
|
||||
<MenuPrimitive.CheckboxItem
|
||||
data-slot="dropdown-menu-checkbox-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none focus:bg-accent/95 focus:text-accent-foreground focus:ring-1 focus:ring-foreground/20 focus:**:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
"focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||
className
|
||||
)}
|
||||
checked={checked}
|
||||
@@ -220,7 +220,7 @@ function PickerRadioItem({
|
||||
<MenuPrimitive.RadioItem
|
||||
data-slot="dropdown-menu-radio-item"
|
||||
className={cn(
|
||||
"relative flex cursor-default items-center gap-2 rounded-lg py-1.5 pr-8 pl-2 text-sm font-medium outline-hidden select-none **:text-neutral-100 focus:bg-neutral-600 focus:text-neutral-100 focus:**:text-neutral-100 data-inset:pl-8 dark:focus:bg-neutral-700/80 pointer-coarse:gap-3 pointer-coarse:py-2.5 pointer-coarse:pl-3 pointer-coarse:text-base 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-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}
|
||||
@@ -252,10 +252,7 @@ function PickerSeparator({
|
||||
return (
|
||||
<MenuPrimitive.Separator
|
||||
data-slot="dropdown-menu-separator"
|
||||
className={cn(
|
||||
"-mx-1.5 my-1.5 h-px bg-neutral-600 dark:bg-neutral-700",
|
||||
className
|
||||
)}
|
||||
className={cn("bg-border -mx-1 my-1 h-px", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -266,7 +263,7 @@ function PickerShortcut({ className, ...props }: React.ComponentProps<"span">) {
|
||||
<span
|
||||
data-slot="dropdown-menu-shortcut"
|
||||
className={cn(
|
||||
"ml-auto text-xs tracking-widest text-neutral-400! group-focus/dropdown-menu-item:text-neutral-100",
|
||||
"text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
|
||||
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function PresetHandler() {
|
||||
const router = useRouter()
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const hasConverted = React.useRef(false)
|
||||
|
||||
React.useEffect(() => {
|
||||
if (params.preset === "random") {
|
||||
router.replace(`/create?preset=${generateRandomPreset()}`)
|
||||
}
|
||||
}, [params.preset, router])
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasConverted.current) {
|
||||
return
|
||||
}
|
||||
hasConverted.current = true
|
||||
|
||||
if (!params.preset || params.preset === "random") {
|
||||
return
|
||||
}
|
||||
|
||||
if (isPresetCode(params.preset)) {
|
||||
return
|
||||
}
|
||||
|
||||
setParams({ base: params.base })
|
||||
}, [params.preset, params.base, setParams])
|
||||
|
||||
return null
|
||||
}
|
||||
@@ -31,10 +31,8 @@ export function PresetPicker({
|
||||
preset.style === params.style &&
|
||||
preset.baseColor === params.baseColor &&
|
||||
preset.theme === params.theme &&
|
||||
preset.chartColor === params.chartColor &&
|
||||
preset.iconLibrary === params.iconLibrary &&
|
||||
preset.font === params.font &&
|
||||
preset.fontHeading === params.fontHeading &&
|
||||
preset.menuAccent === params.menuAccent &&
|
||||
preset.menuColor === params.menuColor &&
|
||||
preset.radius === params.radius
|
||||
@@ -45,10 +43,8 @@ export function PresetPicker({
|
||||
params.style,
|
||||
params.baseColor,
|
||||
params.theme,
|
||||
params.chartColor,
|
||||
params.iconLibrary,
|
||||
params.font,
|
||||
params.fontHeading,
|
||||
params.menuAccent,
|
||||
params.menuColor,
|
||||
params.radius,
|
||||
@@ -71,10 +67,8 @@ export function PresetPicker({
|
||||
style: preset.style,
|
||||
baseColor: preset.baseColor,
|
||||
theme: preset.theme,
|
||||
chartColor: preset.chartColor,
|
||||
iconLibrary: preset.iconLibrary,
|
||||
font: preset.font,
|
||||
fontHeading: preset.fontHeading,
|
||||
menuAccent: preset.menuAccent,
|
||||
menuColor: preset.menuColor,
|
||||
radius: preset.radius,
|
||||
@@ -86,8 +80,8 @@ export function PresetPicker({
|
||||
<Picker>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Preset</div>
|
||||
<div className="line-clamp-1 text-sm font-medium text-foreground">
|
||||
<div className="text-muted-foreground text-xs">Preset</div>
|
||||
<div className="text-foreground line-clamp-1 text-sm font-medium">
|
||||
{currentPreset?.description ?? "Custom"}
|
||||
</div>
|
||||
</div>
|
||||
@@ -106,11 +100,7 @@ export function PresetPicker({
|
||||
{currentBasePresets.map((preset) => {
|
||||
const style = STYLES.find((s) => s.name === preset.style)
|
||||
return (
|
||||
<PickerRadioItem
|
||||
key={preset.title}
|
||||
value={preset.title}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
<PickerRadioItem key={preset.title} value={preset.title}>
|
||||
<div className="flex items-center gap-2">
|
||||
{style?.icon && (
|
||||
<div className="flex size-4 shrink-0 items-center justify-center">
|
||||
|
||||
40
apps/v4/app/(create)/components/preview-controls.tsx
Normal file
40
apps/v4/app/(create)/components/preview-controls.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client"
|
||||
|
||||
import { Monitor, Smartphone, Tablet } from "lucide-react"
|
||||
|
||||
import {
|
||||
ToggleGroup,
|
||||
ToggleGroupItem,
|
||||
} from "@/registry/new-york-v4/ui/toggle-group"
|
||||
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||
|
||||
export function PreviewControls() {
|
||||
const [params, setParams] = useDesignSystemSearchParams({
|
||||
history: "replace",
|
||||
})
|
||||
|
||||
return (
|
||||
<div className="flex h-8 items-center gap-1.5 rounded-md border p-1">
|
||||
<ToggleGroup
|
||||
type="single"
|
||||
value={params.size.toString()}
|
||||
onValueChange={(newValue) => {
|
||||
if (newValue) {
|
||||
setParams({ size: parseInt(newValue) })
|
||||
}
|
||||
}}
|
||||
className="gap-1 *:data-[slot=toggle-group-item]:!size-6 *:data-[slot=toggle-group-item]:!rounded-sm"
|
||||
>
|
||||
<ToggleGroupItem value="100" title="Desktop">
|
||||
<Monitor />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="60" title="Tablet">
|
||||
<Tablet />
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem value="30" title="Mobile">
|
||||
<Smartphone />
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,99 +1,29 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { type PanelImperativeHandle } from "react-resizable-panels"
|
||||
|
||||
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/action-menu"
|
||||
import {
|
||||
REDO_FORWARD_TYPE,
|
||||
UNDO_FORWARD_TYPE,
|
||||
} from "@/app/(create)/components/history-buttons"
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/app/(create)/components/mode-switcher"
|
||||
import { DARK_MODE_FORWARD_TYPE } from "@/components/mode-switcher"
|
||||
import { Badge } from "@/registry/new-york-v4/ui/badge"
|
||||
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 { RESET_FORWARD_TYPE } from "@/app/(create)/hooks/use-reset"
|
||||
import {
|
||||
serializeDesignSystemSearchParams,
|
||||
useDesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
|
||||
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
|
||||
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
|
||||
|
||||
// Hoisted — only uses module-level constants, no component state. (rendering-hoist-jsx)
|
||||
function handleMessage(event: MessageEvent) {
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
event.origin !== window.location.origin
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const type = event.data.type
|
||||
if (type === CMD_K_FORWARD_TYPE) {
|
||||
const isMac = MAC_REGEX.test(navigator.userAgent)
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: event.data.key || "k",
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === RANDOMIZE_FORWARD_TYPE) {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: event.data.key || "r",
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === UNDO_FORWARD_TYPE) {
|
||||
const isMac = MAC_REGEX.test(navigator.userAgent)
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: "z",
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === REDO_FORWARD_TYPE) {
|
||||
const isMac = MAC_REGEX.test(navigator.userAgent)
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: "z",
|
||||
shiftKey: true,
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === RESET_FORWARD_TYPE) {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: "R",
|
||||
shiftKey: true,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
} else if (type === DARK_MODE_FORWARD_TYPE) {
|
||||
document.dispatchEvent(
|
||||
new KeyboardEvent("keydown", {
|
||||
key: event.data.key || "d",
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export function Preview() {
|
||||
const [params] = useDesignSystemSearchParams()
|
||||
const iframeRef = React.useRef<HTMLIFrameElement>(null)
|
||||
const resizablePanelRef = React.useRef<PanelImperativeHandle>(null)
|
||||
|
||||
// Sync resizable panel with URL param changes.
|
||||
React.useEffect(() => {
|
||||
if (resizablePanelRef.current && params.size) {
|
||||
resizablePanelRef.current.resize(params.size)
|
||||
}
|
||||
}, [params.size])
|
||||
|
||||
React.useEffect(() => {
|
||||
const iframe = iframeRef.current
|
||||
@@ -115,6 +45,44 @@ export function Preview() {
|
||||
}
|
||||
}, [params])
|
||||
|
||||
const handleMessage = (event: MessageEvent) => {
|
||||
if (event.data.type === CMD_K_FORWARD_TYPE) {
|
||||
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
|
||||
const key = event.data.key || "k"
|
||||
|
||||
const syntheticEvent = new KeyboardEvent("keydown", {
|
||||
key,
|
||||
metaKey: isMac,
|
||||
ctrlKey: !isMac,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
document.dispatchEvent(syntheticEvent)
|
||||
}
|
||||
|
||||
if (event.data.type === RANDOMIZE_FORWARD_TYPE) {
|
||||
const key = event.data.key || "r"
|
||||
|
||||
const syntheticEvent = new KeyboardEvent("keydown", {
|
||||
key,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
document.dispatchEvent(syntheticEvent)
|
||||
}
|
||||
|
||||
if (event.data.type === DARK_MODE_FORWARD_TYPE) {
|
||||
const key = event.data.key || "d"
|
||||
|
||||
const syntheticEvent = new KeyboardEvent("keydown", {
|
||||
key,
|
||||
bubbles: true,
|
||||
cancelable: true,
|
||||
})
|
||||
document.dispatchEvent(syntheticEvent)
|
||||
}
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
window.addEventListener("message", handleMessage)
|
||||
return () => {
|
||||
@@ -136,9 +104,9 @@ export function Preview() {
|
||||
}, [params.base, params.item])
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-1 flex-col justify-center overflow-hidden rounded-2xl ring ring-foreground/10 md:ring-muted dark:ring-foreground/10">
|
||||
<div className="relative z-0 mx-auto flex w-full flex-1 flex-col overflow-hidden">
|
||||
<div className="absolute inset-0 bg-muted dark:bg-muted/30" />
|
||||
<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}
|
||||
ref={iframeRef}
|
||||
@@ -146,6 +114,12 @@ export function Preview() {
|
||||
className="z-10 size-full flex-1"
|
||||
title="Preview"
|
||||
/>
|
||||
<Badge
|
||||
className="absolute right-2 bottom-2 isolate z-10"
|
||||
variant="secondary"
|
||||
>
|
||||
Preview
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,375 +0,0 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import { Copy01Icon, Globe02Icon, Tick02Icon } from "@hugeicons/core-free-icons"
|
||||
import { HugeiconsIcon } from "@hugeicons/react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { useConfig } from "@/hooks/use-config"
|
||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||
import { BASES, type BaseName } from "@/registry/config"
|
||||
import { Button } from "@/styles/base-nova/ui/button"
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/styles/base-nova/ui/dialog"
|
||||
import {
|
||||
Field,
|
||||
FieldContent,
|
||||
FieldGroup,
|
||||
FieldLabel,
|
||||
FieldLegend,
|
||||
FieldSeparator,
|
||||
FieldSet,
|
||||
FieldTitle,
|
||||
} from "@/styles/base-nova/ui/field"
|
||||
import { RadioGroup, RadioGroupItem } from "@/styles/base-nova/ui/radio-group"
|
||||
import { Switch } from "@/styles/base-nova/ui/switch"
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@/styles/base-nova/ui/tabs"
|
||||
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||
import {
|
||||
useDesignSystemSearchParams,
|
||||
type DesignSystemSearchParams,
|
||||
} from "@/app/(create)/lib/search-params"
|
||||
import {
|
||||
getFramework,
|
||||
getTemplateValue,
|
||||
NO_MONOREPO_FRAMEWORKS,
|
||||
TEMPLATES,
|
||||
} from "@/app/(create)/lib/templates"
|
||||
|
||||
const TURBOREPO_LOGO =
|
||||
'<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Turborepo</title><path d="M11.9906 4.1957c-4.2998 0-7.7981 3.501-7.7981 7.8043s3.4983 7.8043 7.7981 7.8043c4.2999 0 7.7982-3.501 7.7982-7.8043s-3.4983-7.8043-7.7982-7.8043m0 11.843c-2.229 0-4.0356-1.8079-4.0356-4.0387s1.8065-4.0387 4.0356-4.0387S16.0262 9.7692 16.0262 12s-1.8065 4.0388-4.0356 4.0388m.6534-13.1249V0C18.9726.3386 24 5.5822 24 12s-5.0274 11.66-11.356 12v-2.9139c4.7167-.3372 8.4516-4.2814 8.4516-9.0861s-3.735-8.749-8.4516-9.0861M5.113 17.9586c-1.2502-1.4446-2.0562-3.2845-2.2-5.3046H0c.151 2.8266 1.2808 5.3917 3.051 7.3668l2.0606-2.0622zM11.3372 24v-2.9139c-2.02-.1439-3.8584-.949-5.3019-2.2018l-2.0606 2.0623c1.975 1.773 4.538 2.9022 7.361 3.0534z"/></svg>'
|
||||
const ORIGIN = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:4000"
|
||||
const IS_LOCAL_DEV = ORIGIN.includes("localhost")
|
||||
const SHADCN_VERSION = process.env.NEXT_PUBLIC_RC ? "@rc" : "@latest"
|
||||
const PACKAGE_MANAGERS = ["pnpm", "npm", "yarn", "bun"] as const
|
||||
type PackageManager = (typeof PACKAGE_MANAGERS)[number]
|
||||
|
||||
export function ProjectForm({
|
||||
className,
|
||||
}: React.ComponentProps<typeof Button>) {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const presetCode = usePresetCode()
|
||||
const [config, setConfig] = useConfig()
|
||||
const [hasCopied, setHasCopied] = React.useState(false)
|
||||
|
||||
const packageManager = (config.packageManager || "pnpm") as PackageManager
|
||||
const framework = React.useMemo(
|
||||
() => getFramework(params.template ?? "next"),
|
||||
[params.template]
|
||||
)
|
||||
const isMonorepo = React.useMemo(
|
||||
() => params.template?.endsWith("-monorepo") ?? false,
|
||||
[params.template]
|
||||
)
|
||||
|
||||
const hasMonorepo = !NO_MONOREPO_FRAMEWORKS.includes(
|
||||
framework as (typeof NO_MONOREPO_FRAMEWORKS)[number]
|
||||
)
|
||||
|
||||
const commands = React.useMemo(() => {
|
||||
const presetFlag = ` --preset ${presetCode}`
|
||||
const baseFlag = params.base !== "radix" ? ` --base ${params.base}` : ""
|
||||
const templateFlag = ` --template ${framework}`
|
||||
const monorepoFlag = isMonorepo ? " --monorepo" : ""
|
||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
|
||||
|
||||
return IS_LOCAL_DEV
|
||||
? {
|
||||
pnpm: `shadcn init${flags}`,
|
||||
npm: `shadcn init${flags}`,
|
||||
yarn: `shadcn init${flags}`,
|
||||
bun: `shadcn init${flags}`,
|
||||
}
|
||||
: {
|
||||
pnpm: `pnpm dlx shadcn${SHADCN_VERSION} init${flags}`,
|
||||
npm: `npx shadcn${SHADCN_VERSION} init${flags}`,
|
||||
yarn: `yarn dlx shadcn${SHADCN_VERSION} init${flags}`,
|
||||
bun: `bunx --bun shadcn${SHADCN_VERSION} init${flags}`,
|
||||
}
|
||||
}, [framework, isMonorepo, params.base, params.rtl, presetCode])
|
||||
|
||||
const command = commands[packageManager]
|
||||
|
||||
React.useEffect(() => {
|
||||
if (hasCopied) {
|
||||
const timer = setTimeout(() => setHasCopied(false), 2000)
|
||||
return () => clearTimeout(timer)
|
||||
}
|
||||
}, [hasCopied])
|
||||
|
||||
const handleCopy = React.useCallback(() => {
|
||||
const properties: Record<string, string> = {
|
||||
command,
|
||||
}
|
||||
if (params.template) {
|
||||
properties.template = params.template
|
||||
}
|
||||
copyToClipboardWithMeta(command, {
|
||||
name: "copy_npm_command",
|
||||
properties,
|
||||
})
|
||||
setHasCopied(true)
|
||||
}, [command, params.template])
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogTrigger render={<Button className={cn(className)} />}>
|
||||
Create Project
|
||||
</DialogTrigger>
|
||||
<DialogContent className="dark no-scrollbar max-h-[calc(100svh-2rem)] overflow-y-auto rounded-2xl p-6 shadow-xl **:data-[slot=field-separator]:h-2 sm:max-w-sm">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Create Project</DialogTitle>
|
||||
<DialogDescription>
|
||||
Pick a template and configure your project.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div>
|
||||
<FieldGroup>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field className="-mt-2 gap-3">
|
||||
<FieldLabel>Template</FieldLabel>
|
||||
<TemplateGrid template={params.template} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field className="-mt-2">
|
||||
<FieldLabel>Base</FieldLabel>
|
||||
<BaseGrid base={params.base} setParams={setParams} />
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<FieldSet>
|
||||
<FieldLegend variant="label" className="sr-only">
|
||||
Options
|
||||
</FieldLegend>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
data-disabled={hasMonorepo ? undefined : "true"}
|
||||
>
|
||||
<FieldLabel htmlFor="monorepo">
|
||||
<span
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 [&_svg]:fill-current"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: TURBOREPO_LOGO,
|
||||
}}
|
||||
/>
|
||||
Create a monorepo
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="monorepo"
|
||||
checked={params.template?.endsWith("-monorepo") ?? false}
|
||||
disabled={!hasMonorepo}
|
||||
onCheckedChange={(checked) => {
|
||||
const framework = getFramework(params.template ?? "next")
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
framework,
|
||||
checked === true
|
||||
) as typeof params.template,
|
||||
})
|
||||
}}
|
||||
/>
|
||||
</Field>
|
||||
<FieldSeparator className="-mx-6" />
|
||||
<Field orientation="horizontal">
|
||||
<FieldLabel htmlFor="rtl">
|
||||
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
|
||||
Enable RTL support
|
||||
</FieldLabel>
|
||||
<Switch
|
||||
id="rtl"
|
||||
checked={params.rtl}
|
||||
onCheckedChange={(checked) =>
|
||||
setParams({ rtl: checked === true })
|
||||
}
|
||||
/>
|
||||
</Field>
|
||||
</FieldSet>
|
||||
</FieldGroup>
|
||||
</div>
|
||||
<DialogFooter className="-mx-6 -mb-6 min-w-0">
|
||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||
<Tabs
|
||||
value={packageManager}
|
||||
onValueChange={(value) => {
|
||||
setConfig((prev) => ({
|
||||
...prev,
|
||||
packageManager: value as PackageManager,
|
||||
}))
|
||||
}}
|
||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
||||
>
|
||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
||||
<TabsList className="bg-transparent font-mono">
|
||||
{PACKAGE_MANAGERS.map((manager) => {
|
||||
return (
|
||||
<TabsTrigger
|
||||
key={manager}
|
||||
value={manager}
|
||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
||||
>
|
||||
{manager}
|
||||
</TabsTrigger>
|
||||
)
|
||||
})}
|
||||
</TabsList>
|
||||
<Button
|
||||
size="icon-sm"
|
||||
variant="ghost"
|
||||
className="ml-auto"
|
||||
onClick={handleCopy}
|
||||
>
|
||||
{hasCopied ? (
|
||||
<HugeiconsIcon icon={Tick02Icon} />
|
||||
) : (
|
||||
<HugeiconsIcon icon={Copy01Icon} />
|
||||
)}
|
||||
<span className="sr-only">Copy command</span>
|
||||
</Button>
|
||||
</div>
|
||||
{Object.entries(commands).map(([key, cmd]) => {
|
||||
return (
|
||||
<TabsContent key={key} value={key}>
|
||||
<div className="relative overflow-hidden border-t bg-popover p-3">
|
||||
<div className="no-scrollbar overflow-x-auto">
|
||||
<code className="font-mono text-sm whitespace-nowrap">
|
||||
{cmd}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
)
|
||||
})}
|
||||
</Tabs>
|
||||
<Button onClick={handleCopy} className="h-9 w-full">
|
||||
{hasCopied ? "Copied" : "Copy Command"}
|
||||
</Button>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
|
||||
const TemplateGrid = React.memo(function TemplateGrid({
|
||||
template,
|
||||
setParams,
|
||||
}: {
|
||||
template: DesignSystemSearchParams["template"]
|
||||
setParams: ReturnType<typeof useDesignSystemSearchParams>[1]
|
||||
}) {
|
||||
const isMonorepo = template?.endsWith("-monorepo") ?? false
|
||||
const framework = getFramework(template ?? "next")
|
||||
|
||||
const handleTemplateChange = React.useCallback(
|
||||
(value: string) => {
|
||||
setParams({
|
||||
template: getTemplateValue(
|
||||
value,
|
||||
isMonorepo
|
||||
) as DesignSystemSearchParams["template"],
|
||||
})
|
||||
},
|
||||
[isMonorepo, setParams]
|
||||
)
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
value={framework}
|
||||
onValueChange={handleTemplateChange}
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{TEMPLATES.map((item) => (
|
||||
<FieldLabel
|
||||
key={item.value}
|
||||
htmlFor={`template-${item.value}`}
|
||||
className="block w-full"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="w-full rounded-md transition-colors duration-150 hover:bg-neutral-700/45"
|
||||
>
|
||||
<FieldContent className="flex flex-row items-center gap-2 px-2.5 py-1.5">
|
||||
<div
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.logo,
|
||||
}}
|
||||
></div>
|
||||
<FieldTitle>{item.title}</FieldTitle>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={item.value}
|
||||
id={`template-${item.value}`}
|
||||
className="sr-only absolute"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
})
|
||||
|
||||
const BaseGrid = React.memo(function BaseGrid({
|
||||
base,
|
||||
setParams,
|
||||
}: {
|
||||
base: DesignSystemSearchParams["base"]
|
||||
setParams: ReturnType<typeof useDesignSystemSearchParams>[1]
|
||||
}) {
|
||||
const handleBaseChange = React.useCallback(
|
||||
(value: string) => {
|
||||
setParams({ base: value as BaseName })
|
||||
},
|
||||
[setParams]
|
||||
)
|
||||
|
||||
return (
|
||||
<RadioGroup
|
||||
value={base}
|
||||
onValueChange={handleBaseChange}
|
||||
aria-label="Base"
|
||||
className="grid grid-cols-2 gap-2"
|
||||
>
|
||||
{BASES.map((item) => (
|
||||
<FieldLabel
|
||||
key={item.name}
|
||||
htmlFor={`base-${item.name}`}
|
||||
className="block w-full"
|
||||
>
|
||||
<Field
|
||||
orientation="horizontal"
|
||||
className="w-full rounded-md transition-colors duration-150 hover:bg-neutral-700/45"
|
||||
>
|
||||
<FieldContent className="flex flex-row items-center gap-2 px-2.5 py-1.5">
|
||||
<div
|
||||
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item.meta?.logo ?? "",
|
||||
}}
|
||||
/>
|
||||
<FieldTitle>{item.title}</FieldTitle>
|
||||
</FieldContent>
|
||||
<RadioGroupItem
|
||||
value={item.name}
|
||||
id={`base-${item.name}`}
|
||||
className="sr-only absolute"
|
||||
/>
|
||||
</Field>
|
||||
</FieldLabel>
|
||||
))}
|
||||
</RadioGroup>
|
||||
)
|
||||
})
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
|
||||
import { RADII, type RadiusValue } from "@/registry/config"
|
||||
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||
import {
|
||||
@@ -23,26 +21,22 @@ export function RadiusPicker({
|
||||
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||
}) {
|
||||
const [params, setParams] = useDesignSystemSearchParams()
|
||||
const isRadiusLocked = params.style === "lyra"
|
||||
const selectedRadiusName = isRadiusLocked ? "none" : params.radius
|
||||
|
||||
const currentRadius = RADII.find(
|
||||
(radius) => radius.name === selectedRadiusName
|
||||
)
|
||||
const currentRadius = RADII.find((radius) => radius.name === params.radius)
|
||||
const defaultRadius = RADII.find((radius) => radius.name === "default")
|
||||
const otherRadii = RADII.filter((radius) => radius.name !== "default")
|
||||
|
||||
return (
|
||||
<div className="group/picker relative">
|
||||
<Picker>
|
||||
<PickerTrigger disabled={isRadiusLocked}>
|
||||
<PickerTrigger>
|
||||
<div className="flex flex-col justify-start text-left">
|
||||
<div className="text-xs text-muted-foreground">Radius</div>
|
||||
<div className="text-sm font-medium text-foreground">
|
||||
<div className="text-muted-foreground text-xs">Radius</div>
|
||||
<div className="text-foreground text-sm font-medium">
|
||||
{currentRadius?.label}
|
||||
</div>
|
||||
</div>
|
||||
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base text-foreground select-none md:right-2.5">
|
||||
<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">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
@@ -69,9 +63,6 @@ export function RadiusPicker({
|
||||
<PickerRadioGroup
|
||||
value={currentRadius?.name}
|
||||
onValueChange={(value) => {
|
||||
if (isRadiusLocked) {
|
||||
return
|
||||
}
|
||||
setParams({ radius: value as RadiusValue })
|
||||
}}
|
||||
>
|
||||
@@ -80,20 +71,20 @@ export function RadiusPicker({
|
||||
<PickerRadioItem
|
||||
key={defaultRadius.name}
|
||||
value={defaultRadius.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
{defaultRadius.label}
|
||||
<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>
|
||||
</div>
|
||||
</PickerRadioItem>
|
||||
)}
|
||||
</PickerGroup>
|
||||
<PickerSeparator />
|
||||
<PickerGroup>
|
||||
{otherRadii.map((radius) => (
|
||||
<PickerRadioItem
|
||||
key={radius.name}
|
||||
value={radius.name}
|
||||
closeOnClick={isMobile}
|
||||
>
|
||||
<PickerRadioItem key={radius.name} value={radius.name}>
|
||||
{radius.label}
|
||||
</PickerRadioItem>
|
||||
))}
|
||||
@@ -103,7 +94,7 @@ export function RadiusPicker({
|
||||
</Picker>
|
||||
<LockButton
|
||||
param="radius"
|
||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||
className="absolute top-1/2 right-10 -translate-y-1/2"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user