mirror of
https://github.com/shadcn-ui/ui.git
synced 2026-06-16 04:11:34 +00:00
Compare commits
2 Commits
shadcn@4.1
...
shadcn/cod
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69a88f9579 | ||
|
|
4e8263d7a3 |
@@ -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).
|
|
||||||
125
.github/workflows/code-format.yml
vendored
Normal file
125
.github/workflows/code-format.yml
vendored
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
name: Code format
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_run:
|
||||||
|
workflows: ["Code check"]
|
||||||
|
types:
|
||||||
|
- completed
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
contents: write
|
||||||
|
pull-requests: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
format:
|
||||||
|
if: |
|
||||||
|
github.event.workflow_run.conclusion == 'failure' &&
|
||||||
|
github.event.workflow_run.event == 'pull_request'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
name: Run pnpm format:write
|
||||||
|
steps:
|
||||||
|
- name: Inspect failed workflow run
|
||||||
|
id: meta
|
||||||
|
uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
const pr = context.payload.workflow_run.pull_requests[0]
|
||||||
|
|
||||||
|
if (!pr) {
|
||||||
|
core.notice("Skipping because the failed run is not attached to a pull request.")
|
||||||
|
core.setOutput("should_run", "false")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data: pullRequest } = await github.rest.pulls.get({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
pull_number: pr.number,
|
||||||
|
})
|
||||||
|
|
||||||
|
const { data: jobs } = await github.rest.actions.listJobsForWorkflowRun({
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
run_id: context.payload.workflow_run.id,
|
||||||
|
per_page: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
const formatJob = jobs.jobs.find((job) => job.name === "pnpm format:check")
|
||||||
|
const sameRepo =
|
||||||
|
pullRequest.head.repo.full_name === `${context.repo.owner}/${context.repo.repo}`
|
||||||
|
const shouldRun = formatJob?.conclusion === "failure" && sameRepo
|
||||||
|
|
||||||
|
if (!formatJob) {
|
||||||
|
core.notice("Skipping because the format job could not be found in the failed run.")
|
||||||
|
} else if (formatJob.conclusion !== "failure") {
|
||||||
|
core.notice(
|
||||||
|
`Skipping because the format job concluded with "${formatJob.conclusion}".`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sameRepo) {
|
||||||
|
core.notice(
|
||||||
|
`Skipping PR #${pullRequest.number} because its head branch lives in ${pullRequest.head.repo.full_name}.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
core.setOutput("head_ref", pullRequest.head.ref)
|
||||||
|
core.setOutput("should_run", shouldRun ? "true" : "false")
|
||||||
|
|
||||||
|
- name: Checkout pull request branch
|
||||||
|
if: steps.meta.outputs.should_run == 'true'
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
ref: ${{ steps.meta.outputs.head_ref }}
|
||||||
|
|
||||||
|
- name: Install Node.js
|
||||||
|
if: steps.meta.outputs.should_run == 'true'
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
if: steps.meta.outputs.should_run == 'true'
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 9.0.6
|
||||||
|
run_install: false
|
||||||
|
|
||||||
|
- name: Get pnpm store directory
|
||||||
|
if: steps.meta.outputs.should_run == 'true'
|
||||||
|
id: pnpm-cache
|
||||||
|
run: |
|
||||||
|
echo "pnpm_cache_dir=$(pnpm store path)" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Setup pnpm cache
|
||||||
|
if: steps.meta.outputs.should_run == 'true'
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: ${{ steps.pnpm-cache.outputs.pnpm_cache_dir }}
|
||||||
|
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-pnpm-store-
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.meta.outputs.should_run == 'true'
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
|
- name: Apply formatting
|
||||||
|
if: steps.meta.outputs.should_run == 'true'
|
||||||
|
run: pnpm format:write
|
||||||
|
|
||||||
|
- name: Commit formatting changes
|
||||||
|
if: steps.meta.outputs.should_run == 'true'
|
||||||
|
run: |
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "No formatting changes to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config user.name "github-actions[bot]"
|
||||||
|
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||||
|
git add -A
|
||||||
|
git commit -m "style: apply automated formatting"
|
||||||
|
git push origin HEAD:${{ steps.meta.outputs.head_ref }}
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -15,7 +15,6 @@ build
|
|||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.eslintcache
|
|
||||||
*.pem
|
*.pem
|
||||||
|
|
||||||
# debug
|
# debug
|
||||||
@@ -44,4 +43,3 @@ tsconfig.tsbuildinfo
|
|||||||
.notes
|
.notes
|
||||||
.playwright-mcp
|
.playwright-mcp
|
||||||
shadcn-workspace
|
shadcn-workspace
|
||||||
.codex-artifacts
|
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
|
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||||
import { Button } from "@/styles/radix-nova/ui/button"
|
|
||||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldContent,
|
FieldContent,
|
||||||
@@ -15,10 +13,11 @@ import {
|
|||||||
FieldSeparator,
|
FieldSeparator,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
FieldTitle,
|
FieldTitle,
|
||||||
} from "@/styles/radix-nova/ui/field"
|
} from "@/examples/radix/ui/field"
|
||||||
import { Input } from "@/styles/radix-nova/ui/input"
|
import { Input } from "@/examples/radix/ui/input"
|
||||||
import { RadioGroup, RadioGroupItem } from "@/styles/radix-nova/ui/radio-group"
|
import { RadioGroup, RadioGroupItem } from "@/examples/radix/ui/radio-group"
|
||||||
import { Switch } from "@/styles/radix-nova/ui/switch"
|
import { Switch } from "@/examples/radix/ui/switch"
|
||||||
|
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||||
|
|
||||||
export function AppearanceSettings() {
|
export function AppearanceSettings() {
|
||||||
const [gpuCount, setGpuCount] = React.useState(8)
|
const [gpuCount, setGpuCount] = React.useState(8)
|
||||||
|
|||||||
@@ -1,20 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
ArchiveIcon,
|
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||||
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 {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -27,7 +15,18 @@ import {
|
|||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
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() {
|
export function ButtonGroupDemo() {
|
||||||
const [label, setLabel] = React.useState("personal")
|
const [label, setLabel] = React.useState("personal")
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
|
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||||
import { Button } from "@/styles/radix-nova/ui/button"
|
|
||||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
} from "@/styles/radix-nova/ui/input-group"
|
} from "@/examples/radix/ui/input-group"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/styles/radix-nova/ui/tooltip"
|
} from "@/examples/radix/ui/tooltip"
|
||||||
|
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
||||||
|
|
||||||
export function ButtonGroupInputGroup() {
|
export function ButtonGroupInputGroup() {
|
||||||
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
const [voiceEnabled, setVoiceEnabled] = React.useState(false)
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
|
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
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() {
|
export function ButtonGroupNested() {
|
||||||
return (
|
return (
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
|
import { ButtonGroup } from "@/examples/radix/ui/button-group"
|
||||||
import { Button } from "@/styles/radix-nova/ui/button"
|
|
||||||
import { ButtonGroup } from "@/styles/radix-nova/ui/button-group"
|
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/styles/radix-nova/ui/popover"
|
} from "@/examples/radix/ui/popover"
|
||||||
import { Separator } from "@/styles/radix-nova/ui/separator"
|
import { Separator } from "@/examples/radix/ui/separator"
|
||||||
import { Textarea } from "@/styles/radix-nova/ui/textarea"
|
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||||
|
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
export function ButtonGroupPopover() {
|
export function ButtonGroupPopover() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { PlusIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarGroup,
|
AvatarGroup,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from "@/styles/radix-nova/ui/avatar"
|
} from "@/examples/radix/ui/avatar"
|
||||||
import { Button } from "@/styles/radix-nova/ui/button"
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
@@ -14,7 +12,8 @@ import {
|
|||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from "@/styles/radix-nova/ui/empty"
|
} from "@/examples/radix/ui/empty"
|
||||||
|
import { PlusIcon } from "lucide-react"
|
||||||
|
|
||||||
export function EmptyAvatarGroup() {
|
export function EmptyAvatarGroup() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||||
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
|
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||||
|
|
||||||
export function FieldCheckbox() {
|
export function FieldCheckbox() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from "@/styles/radix-nova/ui/button"
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
FieldLegend,
|
FieldLegend,
|
||||||
FieldSeparator,
|
FieldSeparator,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
} from "@/styles/radix-nova/ui/field"
|
} from "@/examples/radix/ui/field"
|
||||||
import { Input } from "@/styles/radix-nova/ui/input"
|
import { Input } from "@/examples/radix/ui/input"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -17,8 +17,8 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/styles/radix-nova/ui/select"
|
} from "@/examples/radix/ui/select"
|
||||||
import { Textarea } from "@/styles/radix-nova/ui/textarea"
|
import { Textarea } from "@/examples/radix/ui/textarea"
|
||||||
|
|
||||||
export function FieldDemo() {
|
export function FieldDemo() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Card, CardContent } from "@/styles/radix-nova/ui/card"
|
import { Card, CardContent } from "@/examples/radix/ui/card"
|
||||||
import { Checkbox } from "@/styles/radix-nova/ui/checkbox"
|
import { Checkbox } from "@/examples/radix/ui/checkbox"
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
FieldLegend,
|
FieldLegend,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
FieldTitle,
|
FieldTitle,
|
||||||
} from "@/styles/radix-nova/ui/field"
|
} from "@/examples/radix/ui/field"
|
||||||
|
|
||||||
const options = [
|
const options = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
import { Field, FieldDescription, FieldTitle } from "@/examples/radix/ui/field"
|
||||||
import {
|
import { Slider } from "@/examples/radix/ui/slider"
|
||||||
Field,
|
|
||||||
FieldDescription,
|
|
||||||
FieldTitle,
|
|
||||||
} from "@/styles/radix-nova/ui/field"
|
|
||||||
import { Slider } from "@/styles/radix-nova/ui/slider"
|
|
||||||
|
|
||||||
export function FieldSlider() {
|
export function FieldSlider() {
|
||||||
const [value, setValue] = useState([200, 800])
|
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 { AppearanceSettings } from "./appearance-settings"
|
||||||
import { ButtonGroupDemo } from "./button-group-demo"
|
import { ButtonGroupDemo } from "./button-group-demo"
|
||||||
|
|||||||
@@ -1,20 +1,19 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
} from "@/styles/radix-nova/ui/input-group"
|
} from "@/examples/radix/ui/input-group"
|
||||||
import { Label } from "@/styles/radix-nova/ui/label"
|
import { Label } from "@/examples/radix/ui/label"
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/styles/radix-nova/ui/popover"
|
} from "@/examples/radix/ui/popover"
|
||||||
|
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
||||||
|
|
||||||
export function InputGroupButtonExample() {
|
export function InputGroupButtonExample() {
|
||||||
const [isFavorite, setIsFavorite] = React.useState(false)
|
const [isFavorite, setIsFavorite] = React.useState(false)
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
import { IconCheck, IconInfoCircle, IconPlus } from "@tabler/icons-react"
|
|
||||||
import { ArrowUpIcon, Search } from "lucide-react"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
} from "@/examples/radix/ui/dropdown-menu"
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
@@ -14,13 +11,15 @@ import {
|
|||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
InputGroupText,
|
InputGroupText,
|
||||||
InputGroupTextarea,
|
InputGroupTextarea,
|
||||||
} from "@/styles/radix-nova/ui/input-group"
|
} from "@/examples/radix/ui/input-group"
|
||||||
import { Separator } from "@/styles/radix-nova/ui/separator"
|
import { Separator } from "@/examples/radix/ui/separator"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
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() {
|
export function InputGroupDemo() {
|
||||||
return (
|
return (
|
||||||
@@ -89,7 +88,7 @@ export function InputGroupDemo() {
|
|||||||
<InputGroupInput placeholder="@shadcn" />
|
<InputGroupInput placeholder="@shadcn" />
|
||||||
<InputGroupAddon align="inline-end">
|
<InputGroupAddon align="inline-end">
|
||||||
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
<div className="flex size-4 items-center justify-center rounded-full bg-primary text-foreground">
|
||||||
<IconCheck className="size-3 text-background" />
|
<IconCheck className="size-3 text-white" />
|
||||||
</div>
|
</div>
|
||||||
</InputGroupAddon>
|
</InputGroupAddon>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
|
|
||||||
import { Button } from "@/styles/radix-nova/ui/button"
|
|
||||||
import {
|
import {
|
||||||
Item,
|
Item,
|
||||||
ItemActions,
|
ItemActions,
|
||||||
@@ -8,7 +6,8 @@ import {
|
|||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemMedia,
|
ItemMedia,
|
||||||
ItemTitle,
|
ItemTitle,
|
||||||
} from "@/styles/radix-nova/ui/item"
|
} from "@/examples/radix/ui/item"
|
||||||
|
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
||||||
|
|
||||||
export function ItemDemo() {
|
export function ItemDemo() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,24 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import {
|
import { Avatar, AvatarFallback, AvatarImage } from "@/examples/radix/ui/avatar"
|
||||||
IconApps,
|
import { Badge } from "@/examples/radix/ui/badge"
|
||||||
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 {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -26,7 +10,7 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/styles/radix-nova/ui/command"
|
} from "@/examples/radix/ui/command"
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuCheckboxItem,
|
DropdownMenuCheckboxItem,
|
||||||
@@ -39,25 +23,36 @@ import {
|
|||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/styles/radix-nova/ui/dropdown-menu"
|
} from "@/examples/radix/ui/dropdown-menu"
|
||||||
import { Field, FieldLabel } from "@/styles/radix-nova/ui/field"
|
import { Field, FieldLabel } from "@/examples/radix/ui/field"
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupTextarea,
|
InputGroupTextarea,
|
||||||
} from "@/styles/radix-nova/ui/input-group"
|
} from "@/examples/radix/ui/input-group"
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/styles/radix-nova/ui/popover"
|
} from "@/examples/radix/ui/popover"
|
||||||
import { Switch } from "@/styles/radix-nova/ui/switch"
|
import { Switch } from "@/examples/radix/ui/switch"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
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 = {
|
const SAMPLE_DATA = {
|
||||||
mentionable: [
|
mentionable: [
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Badge } from "@/styles/radix-nova/ui/badge"
|
import { Badge } from "@/examples/radix/ui/badge"
|
||||||
import { Spinner } from "@/styles/radix-nova/ui/spinner"
|
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||||
|
|
||||||
export function SpinnerBadge() {
|
export function SpinnerBadge() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Button } from "@/styles/radix-nova/ui/button"
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
@@ -6,8 +6,8 @@ import {
|
|||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from "@/styles/radix-nova/ui/empty"
|
} from "@/examples/radix/ui/empty"
|
||||||
import { Spinner } from "@/styles/radix-nova/ui/spinner"
|
import { Spinner } from "@/examples/radix/ui/spinner"
|
||||||
|
|
||||||
export function SpinnerEmpty() {
|
export function SpinnerEmpty() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export default function BlocksLayout({
|
|||||||
<a href="#blocks">Browse Blocks</a>
|
<a href="#blocks">Browse Blocks</a>
|
||||||
</Button>
|
</Button>
|
||||||
<Button asChild variant="ghost" size="sm">
|
<Button asChild variant="ghost" size="sm">
|
||||||
<Link href="/docs/components">View Components</Link>
|
<Link href="/docs/blocks">Add a block</Link>
|
||||||
</Button>
|
</Button>
|
||||||
</PageActions>
|
</PageActions>
|
||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|||||||
@@ -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/(app)/create/components/lock-button"
|
|
||||||
import {
|
|
||||||
Picker,
|
|
||||||
PickerContent,
|
|
||||||
PickerGroup,
|
|
||||||
PickerRadioGroup,
|
|
||||||
PickerRadioItem,
|
|
||||||
PickerSeparator,
|
|
||||||
PickerTrigger,
|
|
||||||
} from "@/app/(app)/create/components/picker"
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(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,104 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import { type RegistryItem } from "shadcn/schema"
|
|
||||||
|
|
||||||
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 { MenuAccentPicker } from "@/app/(app)/create/components/accent-picker"
|
|
||||||
import { ActionMenu } from "@/app/(app)/create/components/action-menu"
|
|
||||||
import { BaseColorPicker } from "@/app/(app)/create/components/base-color-picker"
|
|
||||||
import { BasePicker } from "@/app/(app)/create/components/base-picker"
|
|
||||||
import { ChartColorPicker } from "@/app/(app)/create/components/chart-color-picker"
|
|
||||||
import { CopyPreset } from "@/app/(app)/create/components/copy-preset"
|
|
||||||
import { FontPicker } from "@/app/(app)/create/components/font-picker"
|
|
||||||
import { IconLibraryPicker } from "@/app/(app)/create/components/icon-library-picker"
|
|
||||||
import { MainMenu } from "@/app/(app)/create/components/main-menu"
|
|
||||||
import { MenuColorPicker } from "@/app/(app)/create/components/menu-picker"
|
|
||||||
import { RadiusPicker } from "@/app/(app)/create/components/radius-picker"
|
|
||||||
import { RandomButton } from "@/app/(app)/create/components/random-button"
|
|
||||||
import { ResetDialog } from "@/app/(app)/create/components/reset-button"
|
|
||||||
import { StylePicker } from "@/app/(app)/create/components/style-picker"
|
|
||||||
import { ThemePicker } from "@/app/(app)/create/components/theme-picker"
|
|
||||||
import { V0Button } from "@/app/(app)/create/components/v0-button"
|
|
||||||
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(app)/create/lib/fonts"
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
|
||||||
|
|
||||||
export function Customizer({
|
|
||||||
itemsByBase,
|
|
||||||
}: {
|
|
||||||
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
|
|
||||||
}) {
|
|
||||||
const [params] = useDesignSystemSearchParams()
|
|
||||||
const isMobile = useIsMobile()
|
|
||||||
const anchorRef = React.useRef<HTMLDivElement | null>(null)
|
|
||||||
|
|
||||||
const availableThemes = React.useMemo(
|
|
||||||
() => getThemesForBaseColor(params.baseColor),
|
|
||||||
[params.baseColor]
|
|
||||||
)
|
|
||||||
|
|
||||||
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)"
|
|
||||||
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">
|
|
||||||
<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} />
|
|
||||||
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
|
||||||
<FieldSeparator className="hidden md:block" />
|
|
||||||
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
|
||||||
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
|
||||||
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
|
|
||||||
</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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,158 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
|
|
||||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
|
||||||
import {
|
|
||||||
Picker,
|
|
||||||
PickerContent,
|
|
||||||
PickerGroup,
|
|
||||||
PickerLabel,
|
|
||||||
PickerRadioGroup,
|
|
||||||
PickerRadioItem,
|
|
||||||
PickerSeparator,
|
|
||||||
PickerTrigger,
|
|
||||||
} from "@/app/(app)/create/components/picker"
|
|
||||||
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
|
||||||
import {
|
|
||||||
useDesignSystemSearchParams,
|
|
||||||
type DesignSystemSearchParams,
|
|
||||||
} from "@/app/(app)/create/lib/search-params"
|
|
||||||
|
|
||||||
type FontPickerOption = {
|
|
||||||
name: string
|
|
||||||
value: string
|
|
||||||
type: string
|
|
||||||
font: {
|
|
||||||
style: {
|
|
||||||
fontFamily: string
|
|
||||||
}
|
|
||||||
} | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export function FontPicker({
|
|
||||||
label,
|
|
||||||
param,
|
|
||||||
fonts,
|
|
||||||
isMobile,
|
|
||||||
anchorRef,
|
|
||||||
}: {
|
|
||||||
label: string
|
|
||||||
param: "font" | "fontHeading"
|
|
||||||
fonts: readonly FontPickerOption[]
|
|
||||||
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]
|
|
||||||
)
|
|
||||||
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="line-clamp-1 max-w-[80%] truncate text-sm font-medium text-foreground">
|
|
||||||
{displayFontName}
|
|
||||||
</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,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Aa
|
|
||||||
</div>
|
|
||||||
</PickerTrigger>
|
|
||||||
<PickerContent
|
|
||||||
anchor={isMobile ? anchorRef : undefined}
|
|
||||||
side={isMobile ? "top" : "right"}
|
|
||||||
align={isMobile ? "center" : "start"}
|
|
||||||
className="max-h-96"
|
|
||||||
>
|
|
||||||
<PickerRadioGroup
|
|
||||||
value={currentValue}
|
|
||||||
onValueChange={handleFontChange}
|
|
||||||
>
|
|
||||||
{param === "fontHeading" ? (
|
|
||||||
<>
|
|
||||||
<PickerGroup>
|
|
||||||
<PickerRadioItem value="inherit" closeOnClick={isMobile}>
|
|
||||||
{inheritFontLabel}
|
|
||||||
</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>
|
|
||||||
))}
|
|
||||||
</PickerRadioGroup>
|
|
||||||
</PickerContent>
|
|
||||||
</Picker>
|
|
||||||
<LockButton
|
|
||||||
param={param}
|
|
||||||
className="absolute top-1/2 right-8 -translate-y-1/2"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { lazy, Suspense } from "react"
|
|
||||||
import { SquareIcon } from "lucide-react"
|
|
||||||
import type { IconLibraryName } from "shadcn/icons"
|
|
||||||
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(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,
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}: {
|
|
||||||
[K in IconLibraryName]: string
|
|
||||||
} & React.ComponentProps<"svg">) {
|
|
||||||
const [{ iconLibrary }] = useDesignSystemSearchParams()
|
|
||||||
const iconName = props[iconLibrary]
|
|
||||||
|
|
||||||
if (!iconName) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Suspense fallback={<SquareIcon {...props} />}>
|
|
||||||
{iconLibrary === "lucide" && <IconLucide name={iconName} {...props} />}
|
|
||||||
{iconLibrary === "tabler" && <IconTabler name={iconName} {...props} />}
|
|
||||||
{iconLibrary === "hugeicons" && (
|
|
||||||
<IconHugeicons name={iconName} {...props} />
|
|
||||||
)}
|
|
||||||
{iconLibrary === "phosphor" && (
|
|
||||||
<IconPhosphor name={iconName} {...props} />
|
|
||||||
)}
|
|
||||||
{iconLibrary === "remixicon" && (
|
|
||||||
<IconRemixicon name={iconName} {...props} />
|
|
||||||
)}
|
|
||||||
</Suspense>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { Button } from "@/registry/new-york-v4/ui/button"
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
|
||||||
|
|
||||||
const PREVIEW_ITEMS = [
|
|
||||||
{ label: "01", value: "preview-02" },
|
|
||||||
{ label: "02", value: "preview" },
|
|
||||||
]
|
|
||||||
|
|
||||||
export function PreviewSwitcher() {
|
|
||||||
const [params, setParams] = useDesignSystemSearchParams()
|
|
||||||
|
|
||||||
const isPreview =
|
|
||||||
params.item === "preview" || params.item.startsWith("preview-0")
|
|
||||||
|
|
||||||
if (!isPreview) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="dark absolute right-3 bottom-3 z-20 flex items-center gap-1 rounded-xl bg-card/90 p-1 shadow-xl backdrop-blur-xl">
|
|
||||||
{PREVIEW_ITEMS.map((item) => (
|
|
||||||
<Button
|
|
||||||
key={item.value}
|
|
||||||
variant="ghost"
|
|
||||||
size="sm"
|
|
||||||
data-active={params.item === item.value}
|
|
||||||
className="h-7 min-w-8 cursor-pointer rounded-lg px-2.5 text-xs font-medium text-muted-foreground transition-colors hover:text-foreground data-[active=true]:bg-accent data-[active=true]:text-accent-foreground"
|
|
||||||
onClick={() => setParams({ item: item.value })}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
|
||||||
|
|
||||||
// Returns the canonical preset code derived from the current search params.
|
|
||||||
export function usePresetCode() {
|
|
||||||
const [params] = useDesignSystemSearchParams()
|
|
||||||
|
|
||||||
return getPresetCode(params)
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
"use client"
|
|
||||||
|
|
||||||
import * as React from "react"
|
|
||||||
import useSWR from "swr"
|
|
||||||
|
|
||||||
import { DEFAULT_CONFIG, PRESETS } from "@/registry/config"
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
|
||||||
|
|
||||||
const RESET_DIALOG_KEY = "create:reset-dialog-open"
|
|
||||||
export const RESET_FORWARD_TYPE = "reset-forward"
|
|
||||||
|
|
||||||
export function useReset() {
|
|
||||||
const [params, setParams] = useDesignSystemSearchParams()
|
|
||||||
const { data: showResetDialog = false, mutate: setShowResetDialogData } =
|
|
||||||
useSWR<boolean>(RESET_DIALOG_KEY, {
|
|
||||||
fallbackData: false,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateIfStale: false,
|
|
||||||
revalidateOnReconnect: false,
|
|
||||||
})
|
|
||||||
|
|
||||||
const reset = React.useCallback(() => {
|
|
||||||
const preset =
|
|
||||||
PRESETS.find(
|
|
||||||
(preset) => preset.base === params.base && preset.style === params.style
|
|
||||||
) ?? DEFAULT_CONFIG
|
|
||||||
|
|
||||||
setParams({
|
|
||||||
base: params.base,
|
|
||||||
style: params.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,
|
|
||||||
template: DEFAULT_CONFIG.template,
|
|
||||||
item: params.item,
|
|
||||||
})
|
|
||||||
}, [setParams, params.base, params.style, params.item])
|
|
||||||
|
|
||||||
const handleShowResetDialogChange = React.useCallback(
|
|
||||||
(open: boolean) => {
|
|
||||||
void setShowResetDialogData(open, { revalidate: false })
|
|
||||||
},
|
|
||||||
[setShowResetDialogData]
|
|
||||||
)
|
|
||||||
|
|
||||||
const confirmReset = React.useCallback(() => {
|
|
||||||
reset()
|
|
||||||
void setShowResetDialogData(false, { revalidate: false })
|
|
||||||
}, [reset, setShowResetDialogData])
|
|
||||||
|
|
||||||
const showResetDialogRef = React.useRef(showResetDialog)
|
|
||||||
React.useEffect(() => {
|
|
||||||
showResetDialogRef.current = showResetDialog
|
|
||||||
}, [showResetDialog])
|
|
||||||
|
|
||||||
const confirmResetRef = React.useRef(confirmReset)
|
|
||||||
React.useEffect(() => {
|
|
||||||
confirmResetRef.current = confirmReset
|
|
||||||
}, [confirmReset])
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const down = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "R" && e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
|
||||||
if (
|
|
||||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
|
||||||
e.target instanceof HTMLInputElement ||
|
|
||||||
e.target instanceof HTMLTextAreaElement ||
|
|
||||||
e.target instanceof HTMLSelectElement
|
|
||||||
) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
e.preventDefault()
|
|
||||||
|
|
||||||
// If the dialog is already open, confirm the reset.
|
|
||||||
if (showResetDialogRef.current) {
|
|
||||||
confirmResetRef.current()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handleShowResetDialogChange(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("keydown", down)
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("keydown", down)
|
|
||||||
}
|
|
||||||
}, [handleShowResetDialogChange])
|
|
||||||
|
|
||||||
return {
|
|
||||||
reset,
|
|
||||||
showResetDialog,
|
|
||||||
setShowResetDialog: handleShowResetDialogChange,
|
|
||||||
confirmReset,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,233 +0,0 @@
|
|||||||
import {
|
|
||||||
DM_Sans,
|
|
||||||
Figtree,
|
|
||||||
Geist,
|
|
||||||
Geist_Mono,
|
|
||||||
IBM_Plex_Sans,
|
|
||||||
Instrument_Sans,
|
|
||||||
Inter,
|
|
||||||
JetBrains_Mono,
|
|
||||||
Lora,
|
|
||||||
Manrope,
|
|
||||||
Merriweather,
|
|
||||||
Montserrat,
|
|
||||||
Noto_Sans,
|
|
||||||
Noto_Serif,
|
|
||||||
Nunito_Sans,
|
|
||||||
Outfit,
|
|
||||||
Oxanium,
|
|
||||||
Playfair_Display,
|
|
||||||
Public_Sans,
|
|
||||||
Raleway,
|
|
||||||
Roboto,
|
|
||||||
Roboto_Slab,
|
|
||||||
Source_Sans_3,
|
|
||||||
Space_Grotesk,
|
|
||||||
} from "next/font/google"
|
|
||||||
|
|
||||||
import { FONT_DEFINITIONS, type FontName } from "@/lib/font-definitions"
|
|
||||||
|
|
||||||
type PreviewFont = ReturnType<typeof Inter>
|
|
||||||
|
|
||||||
const geistSans = Geist({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-geist-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const inter = Inter({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-inter",
|
|
||||||
})
|
|
||||||
|
|
||||||
const notoSans = Noto_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-noto-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const nunitoSans = Nunito_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-nunito-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const figtree = Figtree({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-figtree",
|
|
||||||
})
|
|
||||||
|
|
||||||
const roboto = Roboto({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-roboto",
|
|
||||||
})
|
|
||||||
|
|
||||||
const raleway = Raleway({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-raleway",
|
|
||||||
})
|
|
||||||
|
|
||||||
const dmSans = DM_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-dm-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const publicSans = Public_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-public-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const outfit = Outfit({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-outfit",
|
|
||||||
})
|
|
||||||
|
|
||||||
const oxanium = Oxanium({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-oxanium",
|
|
||||||
})
|
|
||||||
|
|
||||||
const manrope = Manrope({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-manrope",
|
|
||||||
})
|
|
||||||
|
|
||||||
const spaceGrotesk = Space_Grotesk({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-space-grotesk",
|
|
||||||
})
|
|
||||||
|
|
||||||
const montserrat = Montserrat({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-montserrat",
|
|
||||||
})
|
|
||||||
|
|
||||||
const ibmPlexSans = IBM_Plex_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-ibm-plex-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const sourceSans3 = Source_Sans_3({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-source-sans-3",
|
|
||||||
})
|
|
||||||
|
|
||||||
const instrumentSans = Instrument_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-instrument-sans",
|
|
||||||
})
|
|
||||||
|
|
||||||
const jetbrainsMono = JetBrains_Mono({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-jetbrains-mono",
|
|
||||||
})
|
|
||||||
|
|
||||||
const geistMono = Geist_Mono({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-geist-mono",
|
|
||||||
})
|
|
||||||
|
|
||||||
const notoSerif = Noto_Serif({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-noto-serif",
|
|
||||||
})
|
|
||||||
|
|
||||||
const robotoSlab = Roboto_Slab({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-roboto-slab",
|
|
||||||
})
|
|
||||||
|
|
||||||
const merriweather = Merriweather({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-merriweather",
|
|
||||||
})
|
|
||||||
|
|
||||||
const lora = Lora({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-lora",
|
|
||||||
})
|
|
||||||
|
|
||||||
const playfairDisplay = Playfair_Display({
|
|
||||||
subsets: ["latin"],
|
|
||||||
variable: "--font-playfair-display",
|
|
||||||
})
|
|
||||||
|
|
||||||
const PREVIEW_FONTS = {
|
|
||||||
geist: geistSans,
|
|
||||||
inter,
|
|
||||||
"noto-sans": notoSans,
|
|
||||||
"nunito-sans": nunitoSans,
|
|
||||||
figtree,
|
|
||||||
roboto,
|
|
||||||
raleway,
|
|
||||||
"dm-sans": dmSans,
|
|
||||||
"public-sans": publicSans,
|
|
||||||
outfit,
|
|
||||||
oxanium,
|
|
||||||
manrope,
|
|
||||||
"space-grotesk": spaceGrotesk,
|
|
||||||
montserrat,
|
|
||||||
"ibm-plex-sans": ibmPlexSans,
|
|
||||||
"source-sans-3": sourceSans3,
|
|
||||||
"instrument-sans": instrumentSans,
|
|
||||||
"jetbrains-mono": jetbrainsMono,
|
|
||||||
"geist-mono": geistMono,
|
|
||||||
"noto-serif": notoSerif,
|
|
||||||
"roboto-slab": robotoSlab,
|
|
||||||
merriweather,
|
|
||||||
lora,
|
|
||||||
"playfair-display": playfairDisplay,
|
|
||||||
} satisfies Record<FontName, PreviewFont>
|
|
||||||
|
|
||||||
function createFontOption(name: FontName) {
|
|
||||||
const definition = FONT_DEFINITIONS.find((font) => font.name === name)
|
|
||||||
|
|
||||||
if (!definition) {
|
|
||||||
throw new Error(`Unknown font definition: ${name}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: definition.title,
|
|
||||||
value: definition.name,
|
|
||||||
font: PREVIEW_FONTS[name],
|
|
||||||
type: definition.type,
|
|
||||||
} as const
|
|
||||||
}
|
|
||||||
|
|
||||||
export const FONTS = [
|
|
||||||
createFontOption("geist"),
|
|
||||||
createFontOption("inter"),
|
|
||||||
createFontOption("noto-sans"),
|
|
||||||
createFontOption("nunito-sans"),
|
|
||||||
createFontOption("figtree"),
|
|
||||||
createFontOption("roboto"),
|
|
||||||
createFontOption("raleway"),
|
|
||||||
createFontOption("dm-sans"),
|
|
||||||
createFontOption("public-sans"),
|
|
||||||
createFontOption("outfit"),
|
|
||||||
createFontOption("oxanium"),
|
|
||||||
createFontOption("manrope"),
|
|
||||||
createFontOption("space-grotesk"),
|
|
||||||
createFontOption("montserrat"),
|
|
||||||
createFontOption("ibm-plex-sans"),
|
|
||||||
createFontOption("source-sans-3"),
|
|
||||||
createFontOption("instrument-sans"),
|
|
||||||
createFontOption("geist-mono"),
|
|
||||||
createFontOption("jetbrains-mono"),
|
|
||||||
createFontOption("noto-serif"),
|
|
||||||
createFontOption("roboto-slab"),
|
|
||||||
createFontOption("merriweather"),
|
|
||||||
createFontOption("lora"),
|
|
||||||
createFontOption("playfair-display"),
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type Font = (typeof FONTS)[number]
|
|
||||||
|
|
||||||
export const FONT_HEADING_OPTIONS = [
|
|
||||||
{
|
|
||||||
name: "Inherit",
|
|
||||||
value: "inherit",
|
|
||||||
font: null,
|
|
||||||
type: "default",
|
|
||||||
},
|
|
||||||
...FONTS,
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export type FontHeadingOption = (typeof FONT_HEADING_OPTIONS)[number]
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { encodePreset, type PresetConfig } from "shadcn/preset"
|
|
||||||
|
|
||||||
import { type DesignSystemConfig } from "@/registry/config"
|
|
||||||
|
|
||||||
type PresetCodeConfig = Pick<
|
|
||||||
DesignSystemConfig,
|
|
||||||
| "style"
|
|
||||||
| "baseColor"
|
|
||||||
| "theme"
|
|
||||||
| "chartColor"
|
|
||||||
| "iconLibrary"
|
|
||||||
| "font"
|
|
||||||
| "fontHeading"
|
|
||||||
| "radius"
|
|
||||||
| "menuAccent"
|
|
||||||
| "menuColor"
|
|
||||||
>
|
|
||||||
|
|
||||||
export function getPresetCode(config: PresetCodeConfig) {
|
|
||||||
const presetConfig: Partial<PresetConfig> = {
|
|
||||||
style: config.style as PresetConfig["style"],
|
|
||||||
baseColor: config.baseColor as PresetConfig["baseColor"],
|
|
||||||
theme: config.theme as PresetConfig["theme"],
|
|
||||||
chartColor: config.chartColor as PresetConfig["chartColor"],
|
|
||||||
iconLibrary: config.iconLibrary as PresetConfig["iconLibrary"],
|
|
||||||
font: config.font as PresetConfig["font"],
|
|
||||||
fontHeading: config.fontHeading as PresetConfig["fontHeading"],
|
|
||||||
radius: config.radius as PresetConfig["radius"],
|
|
||||||
menuAccent: config.menuAccent as PresetConfig["menuAccent"],
|
|
||||||
menuColor: config.menuColor as PresetConfig["menuColor"],
|
|
||||||
}
|
|
||||||
|
|
||||||
return encodePreset(presetConfig)
|
|
||||||
}
|
|
||||||
@@ -1,34 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest"
|
|
||||||
|
|
||||||
import { resolvePresetOverrides } from "./preset-query"
|
|
||||||
|
|
||||||
describe("resolvePresetOverrides", () => {
|
|
||||||
it("prefers explicit fontHeading and chartColor query params", () => {
|
|
||||||
const overrides = resolvePresetOverrides(
|
|
||||||
new URLSearchParams("fontHeading=playfair-display&chartColor=emerald"),
|
|
||||||
{
|
|
||||||
theme: "neutral",
|
|
||||||
chartColor: "blue",
|
|
||||||
fontHeading: "inherit",
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(overrides).toEqual({
|
|
||||||
fontHeading: "playfair-display",
|
|
||||||
chartColor: "emerald",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it("falls back to decoded preset values when no overrides are present", () => {
|
|
||||||
const overrides = resolvePresetOverrides(new URLSearchParams(), {
|
|
||||||
theme: "neutral",
|
|
||||||
chartColor: "blue",
|
|
||||||
fontHeading: "inherit",
|
|
||||||
})
|
|
||||||
|
|
||||||
expect(overrides).toEqual({
|
|
||||||
fontHeading: "inherit",
|
|
||||||
chartColor: "blue",
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { V1_CHART_COLOR_MAP, type PresetConfig } from "shadcn/preset"
|
|
||||||
|
|
||||||
import { type ChartColorName, type FontHeadingValue } from "@/registry/config"
|
|
||||||
|
|
||||||
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
|
|
||||||
|
|
||||||
export function resolvePresetOverrides(
|
|
||||||
searchParams: SearchParamsLike,
|
|
||||||
decoded: Pick<PresetConfig, "theme" | "chartColor" | "fontHeading">
|
|
||||||
) {
|
|
||||||
const hasFontHeadingOverride = searchParams.has("fontHeading")
|
|
||||||
const hasChartColorOverride = searchParams.has("chartColor")
|
|
||||||
|
|
||||||
const fontHeading = hasFontHeadingOverride
|
|
||||||
? ((searchParams.get("fontHeading") ??
|
|
||||||
decoded.fontHeading) as FontHeadingValue)
|
|
||||||
: decoded.fontHeading
|
|
||||||
|
|
||||||
const chartColor = hasChartColorOverride
|
|
||||||
? ((searchParams.get("chartColor") ??
|
|
||||||
decoded.chartColor ??
|
|
||||||
V1_CHART_COLOR_MAP[decoded.theme] ??
|
|
||||||
decoded.theme) as ChartColorName)
|
|
||||||
: (decoded.chartColor ?? V1_CHART_COLOR_MAP[decoded.theme] ?? decoded.theme)
|
|
||||||
|
|
||||||
return {
|
|
||||||
fontHeading,
|
|
||||||
chartColor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,309 +0,0 @@
|
|||||||
import * as React from "react"
|
|
||||||
import { useSearchParams } from "next/navigation"
|
|
||||||
import { useQueryStates } from "nuqs"
|
|
||||||
import {
|
|
||||||
createLoader,
|
|
||||||
createSerializer,
|
|
||||||
parseAsBoolean,
|
|
||||||
parseAsInteger,
|
|
||||||
parseAsString,
|
|
||||||
parseAsStringLiteral,
|
|
||||||
type inferParserType,
|
|
||||||
type Options,
|
|
||||||
} from "nuqs/server"
|
|
||||||
import { decodePreset, isPresetCode } from "shadcn/preset"
|
|
||||||
|
|
||||||
import {
|
|
||||||
BASE_COLORS,
|
|
||||||
BASES,
|
|
||||||
DEFAULT_CONFIG,
|
|
||||||
getThemesForBaseColor,
|
|
||||||
iconLibraries,
|
|
||||||
MENU_ACCENTS,
|
|
||||||
MENU_COLORS,
|
|
||||||
RADII,
|
|
||||||
STYLES,
|
|
||||||
THEMES,
|
|
||||||
type BaseColorName,
|
|
||||||
type BaseName,
|
|
||||||
type ChartColorName,
|
|
||||||
type FontHeadingValue,
|
|
||||||
type FontValue,
|
|
||||||
type IconLibraryName,
|
|
||||||
type MenuAccentValue,
|
|
||||||
type MenuColorValue,
|
|
||||||
type RadiusValue,
|
|
||||||
type StyleName,
|
|
||||||
type ThemeName,
|
|
||||||
} from "@/registry/config"
|
|
||||||
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
|
||||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
|
||||||
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
|
|
||||||
|
|
||||||
const designSystemSearchParams = {
|
|
||||||
preset: parseAsString.withDefault("b2D0wqNxT"),
|
|
||||||
base: parseAsStringLiteral<BaseName>(BASES.map((b) => b.name)).withDefault(
|
|
||||||
DEFAULT_CONFIG.base
|
|
||||||
),
|
|
||||||
item: parseAsString.withDefault("preview-02").withOptions({ shallow: true }),
|
|
||||||
iconLibrary: parseAsStringLiteral<IconLibraryName>(
|
|
||||||
Object.values(iconLibraries).map((i) => i.name)
|
|
||||||
).withDefault(DEFAULT_CONFIG.iconLibrary),
|
|
||||||
style: parseAsStringLiteral<StyleName>(STYLES.map((s) => s.name)).withDefault(
|
|
||||||
DEFAULT_CONFIG.style
|
|
||||||
),
|
|
||||||
theme: parseAsStringLiteral<ThemeName>(THEMES.map((t) => t.name)).withDefault(
|
|
||||||
DEFAULT_CONFIG.theme
|
|
||||||
),
|
|
||||||
chartColor: parseAsStringLiteral<ChartColorName>(
|
|
||||||
THEMES.map((t) => t.name)
|
|
||||||
).withDefault(DEFAULT_CONFIG.chartColor ?? "neutral"),
|
|
||||||
font: parseAsStringLiteral<FontValue>(FONTS.map((f) => f.value)).withDefault(
|
|
||||||
DEFAULT_CONFIG.font
|
|
||||||
),
|
|
||||||
fontHeading: parseAsStringLiteral<FontHeadingValue>([
|
|
||||||
"inherit",
|
|
||||||
...FONTS.map((f) => f.value),
|
|
||||||
]).withDefault(DEFAULT_CONFIG.fontHeading),
|
|
||||||
baseColor: parseAsStringLiteral<BaseColorName>(
|
|
||||||
BASE_COLORS.map((b) => b.name)
|
|
||||||
).withDefault(DEFAULT_CONFIG.baseColor),
|
|
||||||
menuAccent: parseAsStringLiteral<MenuAccentValue>(
|
|
||||||
MENU_ACCENTS.map((a) => a.value)
|
|
||||||
).withDefault(DEFAULT_CONFIG.menuAccent),
|
|
||||||
menuColor: parseAsStringLiteral<MenuColorValue>(
|
|
||||||
MENU_COLORS.map((m) => m.value)
|
|
||||||
).withDefault(DEFAULT_CONFIG.menuColor),
|
|
||||||
radius: parseAsStringLiteral<RadiusValue>(
|
|
||||||
RADII.map((r) => r.name)
|
|
||||||
).withDefault("default"),
|
|
||||||
template: parseAsStringLiteral([
|
|
||||||
"next",
|
|
||||||
"next-monorepo",
|
|
||||||
"start",
|
|
||||||
"start-monorepo",
|
|
||||||
"react-router",
|
|
||||||
"react-router-monorepo",
|
|
||||||
"vite",
|
|
||||||
"vite-monorepo",
|
|
||||||
"astro",
|
|
||||||
"astro-monorepo",
|
|
||||||
"laravel",
|
|
||||||
] as const).withDefault("next"),
|
|
||||||
rtl: parseAsBoolean.withDefault(false),
|
|
||||||
size: parseAsInteger.withDefault(100),
|
|
||||||
custom: parseAsBoolean.withDefault(false),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Design system param keys that get encoded into the preset code.
|
|
||||||
const DESIGN_SYSTEM_KEYS = [
|
|
||||||
"style",
|
|
||||||
"baseColor",
|
|
||||||
"theme",
|
|
||||||
"chartColor",
|
|
||||||
"iconLibrary",
|
|
||||||
"font",
|
|
||||||
"fontHeading",
|
|
||||||
"radius",
|
|
||||||
"menuAccent",
|
|
||||||
"menuColor",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
function normalizeFontHeading(
|
|
||||||
font: FontValue,
|
|
||||||
fontHeading: FontHeadingValue
|
|
||||||
): FontHeadingValue {
|
|
||||||
// Persist "same as body" as an explicit inherit sentinel so the body font
|
|
||||||
// can change later without freezing headings to a concrete previous value.
|
|
||||||
return fontHeading === font ? "inherit" : fontHeading
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-design-system keys that get passed through as-is.
|
|
||||||
// `base` is not encoded in preset codes — it's an architectural choice, not visual.
|
|
||||||
const NON_DESIGN_SYSTEM_KEYS = [
|
|
||||||
"base",
|
|
||||||
"item",
|
|
||||||
"preset",
|
|
||||||
"template",
|
|
||||||
"rtl",
|
|
||||||
"size",
|
|
||||||
"custom",
|
|
||||||
] as const
|
|
||||||
|
|
||||||
export const loadDesignSystemSearchParams = createLoader(
|
|
||||||
designSystemSearchParams
|
|
||||||
)
|
|
||||||
|
|
||||||
export const serializeDesignSystemSearchParams = createSerializer(
|
|
||||||
designSystemSearchParams
|
|
||||||
)
|
|
||||||
|
|
||||||
export type DesignSystemSearchParams = inferParserType<
|
|
||||||
typeof designSystemSearchParams
|
|
||||||
>
|
|
||||||
|
|
||||||
export function isTranslucentMenuColor(
|
|
||||||
menuColor?: MenuColorValue | null
|
|
||||||
): menuColor is "default-translucent" | "inverted-translucent" {
|
|
||||||
return (
|
|
||||||
menuColor === "default-translucent" || menuColor === "inverted-translucent"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizePartialDesignSystemParams(
|
|
||||||
params: Partial<DesignSystemSearchParams>
|
|
||||||
): Partial<DesignSystemSearchParams> {
|
|
||||||
if (
|
|
||||||
params.menuAccent === "bold" &&
|
|
||||||
isTranslucentMenuColor(params.menuColor ?? undefined)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...params,
|
|
||||||
menuAccent: "subtle",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return params
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeDesignSystemParams(
|
|
||||||
params: DesignSystemSearchParams
|
|
||||||
): DesignSystemSearchParams {
|
|
||||||
let result = {
|
|
||||||
...params,
|
|
||||||
fontHeading: normalizeFontHeading(params.font, params.fontHeading),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate theme and chartColor against baseColor.
|
|
||||||
if (result.baseColor) {
|
|
||||||
const available = getThemesForBaseColor(result.baseColor)
|
|
||||||
const themeValid = available.some((t) => t.name === result.theme)
|
|
||||||
const chartColorValid = available.some((t) => t.name === result.chartColor)
|
|
||||||
|
|
||||||
if (!themeValid || !chartColorValid) {
|
|
||||||
const fallback = (available[0]?.name ?? result.baseColor) as ThemeName
|
|
||||||
result = {
|
|
||||||
...result,
|
|
||||||
...(!themeValid && { theme: fallback }),
|
|
||||||
...(!chartColorValid && { chartColor: fallback as ChartColorName }),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
result.menuAccent === "bold" &&
|
|
||||||
isTranslucentMenuColor(result.menuColor)
|
|
||||||
) {
|
|
||||||
return {
|
|
||||||
...result,
|
|
||||||
menuAccent: "subtle",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// If preset param exists, decode it and overlay on raw params.
|
|
||||||
// V1 presets don't encode chartColor — fall back to the colored
|
|
||||||
// theme that base-color themes originally borrowed charts from.
|
|
||||||
type SearchParamsLike = Pick<URLSearchParams, "get" | "has">
|
|
||||||
|
|
||||||
function resolvePresetParams(
|
|
||||||
rawParams: DesignSystemSearchParams,
|
|
||||||
searchParams: SearchParamsLike
|
|
||||||
) {
|
|
||||||
if (rawParams.preset && isPresetCode(rawParams.preset)) {
|
|
||||||
const decoded = decodePreset(rawParams.preset)
|
|
||||||
if (decoded) {
|
|
||||||
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
|
|
||||||
return normalizeDesignSystemParams({
|
|
||||||
...decoded,
|
|
||||||
...presetOverrides,
|
|
||||||
base: rawParams.base,
|
|
||||||
item: rawParams.item,
|
|
||||||
preset: rawParams.preset,
|
|
||||||
template: rawParams.template,
|
|
||||||
rtl: rawParams.rtl,
|
|
||||||
size: rawParams.size,
|
|
||||||
custom: rawParams.custom,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return normalizeDesignSystemParams(rawParams)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wraps nuqs useQueryStates with transparent preset encoding/decoding.
|
|
||||||
// - Reads: if ?preset=CODE is in the URL, decodes it and returns individual values.
|
|
||||||
// - Writes: when design system params are set, encodes them into a preset code.
|
|
||||||
export function useDesignSystemSearchParams(options: Options = {}) {
|
|
||||||
const searchParams = useSearchParams()
|
|
||||||
const [rawParams, rawSetParams] = useQueryStates(designSystemSearchParams, {
|
|
||||||
shallow: false,
|
|
||||||
history: "push",
|
|
||||||
...options,
|
|
||||||
})
|
|
||||||
|
|
||||||
const params = React.useMemo(
|
|
||||||
() => resolvePresetParams(rawParams, searchParams),
|
|
||||||
[rawParams, searchParams]
|
|
||||||
)
|
|
||||||
|
|
||||||
// Use ref so setParams callback stays stable across renders.
|
|
||||||
const paramsRef = React.useRef(params)
|
|
||||||
React.useEffect(() => {
|
|
||||||
paramsRef.current = params
|
|
||||||
}, [params])
|
|
||||||
|
|
||||||
type RawSetParamsInput = Parameters<typeof rawSetParams>[0]
|
|
||||||
|
|
||||||
const setParams = React.useCallback(
|
|
||||||
(
|
|
||||||
updates:
|
|
||||||
| Partial<DesignSystemSearchParams>
|
|
||||||
| ((
|
|
||||||
old: DesignSystemSearchParams
|
|
||||||
) => Partial<DesignSystemSearchParams>),
|
|
||||||
setOptions?: Options
|
|
||||||
) => {
|
|
||||||
const resolvedUpdates = normalizePartialDesignSystemParams(
|
|
||||||
typeof updates === "function" ? updates(paramsRef.current) : updates
|
|
||||||
)
|
|
||||||
|
|
||||||
const hasDesignSystemUpdate = DESIGN_SYSTEM_KEYS.some(
|
|
||||||
(key) => key in resolvedUpdates
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!hasDesignSystemUpdate) {
|
|
||||||
// No design system change, pass through directly.
|
|
||||||
return rawSetParams(resolvedUpdates as RawSetParamsInput, setOptions)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Merge current decoded values with updates.
|
|
||||||
const merged = normalizeDesignSystemParams({
|
|
||||||
...paramsRef.current,
|
|
||||||
...resolvedUpdates,
|
|
||||||
})
|
|
||||||
// Encode design system fields into a preset code.
|
|
||||||
// Cast needed: merged values may include null from nuqs resets,
|
|
||||||
// but encodePreset handles missing values by falling back to defaults.
|
|
||||||
const code = getPresetCode(merged)
|
|
||||||
// Build update: set preset, clear individual DS params from URL.
|
|
||||||
const rawUpdate: Record<string, unknown> = { preset: code }
|
|
||||||
for (const key of DESIGN_SYSTEM_KEYS) {
|
|
||||||
rawUpdate[key] = null
|
|
||||||
}
|
|
||||||
|
|
||||||
// Pass through non-DS params that were explicitly in the update.
|
|
||||||
for (const key of NON_DESIGN_SYSTEM_KEYS) {
|
|
||||||
if (key in resolvedUpdates) {
|
|
||||||
rawUpdate[key] = (resolvedUpdates as Record<string, unknown>)[key]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return rawSetParams(rawUpdate as RawSetParamsInput, setOptions)
|
|
||||||
},
|
|
||||||
[rawSetParams]
|
|
||||||
)
|
|
||||||
|
|
||||||
return [params, setParams] as const
|
|
||||||
}
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
||||||
|
|
||||||
import { DEFAULT_CONFIG } from "@/registry/config"
|
|
||||||
import { buildV0Payload } from "@/app/(app)/create/lib/v0"
|
|
||||||
|
|
||||||
vi.mock("shadcn/schema", async () => {
|
|
||||||
return await vi.importActual("shadcn/schema")
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock("shadcn/utils", async () => {
|
|
||||||
const actual = (await vi.importActual("shadcn/utils")) as {
|
|
||||||
transformFont: unknown
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
transformFont: actual.transformFont,
|
|
||||||
transformIcons: async ({ sourceFile }: { sourceFile: unknown }) =>
|
|
||||||
sourceFile,
|
|
||||||
transformMenu: async ({ sourceFile }: { sourceFile: unknown }) =>
|
|
||||||
sourceFile,
|
|
||||||
transformRender: async ({ sourceFile }: { sourceFile: unknown }) =>
|
|
||||||
sourceFile,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
vi.mock("@/registry/bases/__index__", () => ({
|
|
||||||
Index: {
|
|
||||||
base: {
|
|
||||||
card: {
|
|
||||||
name: "card",
|
|
||||||
type: "registry:ui",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
radix: {
|
|
||||||
card: {
|
|
||||||
name: "card",
|
|
||||||
type: "registry:ui",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}))
|
|
||||||
|
|
||||||
describe("buildV0Payload", () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
process.env.NEXT_PUBLIC_APP_URL = "http://example.test"
|
|
||||||
|
|
||||||
vi.stubGlobal(
|
|
||||||
"fetch",
|
|
||||||
vi.fn(async (input: string | URL | Request) => {
|
|
||||||
const url =
|
|
||||||
typeof input === "string"
|
|
||||||
? input
|
|
||||||
: input instanceof URL
|
|
||||||
? input.toString()
|
|
||||||
: input.url
|
|
||||||
const name = url.split("/").pop()?.replace(".json", "") ?? "component"
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
name,
|
|
||||||
type: "registry:ui",
|
|
||||||
files: [
|
|
||||||
{
|
|
||||||
path: `registry/base-nova/ui/${name}.tsx`,
|
|
||||||
type: "registry:ui",
|
|
||||||
content: `import * as React from "react"\n\nexport function Component() {\n return <div className="cn-font-heading text-xl" />\n}\n`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
{
|
|
||||||
status: 200,
|
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
)
|
|
||||||
})
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.unstubAllGlobals()
|
|
||||||
delete process.env.NEXT_PUBLIC_APP_URL
|
|
||||||
})
|
|
||||||
|
|
||||||
it("rewrites cn-font-heading to font-heading when heading inherits the body font", async () => {
|
|
||||||
const payload = await buildV0Payload({
|
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
item: undefined,
|
|
||||||
fontHeading: "inherit",
|
|
||||||
})
|
|
||||||
|
|
||||||
const cardFile = payload.files?.find(
|
|
||||||
(file) => file.target === "components/ui/card.tsx"
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(cardFile?.content).toContain("font-heading")
|
|
||||||
expect(cardFile?.content).not.toContain("cn-font-heading")
|
|
||||||
})
|
|
||||||
|
|
||||||
it("rewrites cn-font-heading to font-heading when a distinct heading font is selected", async () => {
|
|
||||||
const payload = await buildV0Payload({
|
|
||||||
...DEFAULT_CONFIG,
|
|
||||||
item: undefined,
|
|
||||||
fontHeading: "playfair-display",
|
|
||||||
})
|
|
||||||
|
|
||||||
const cardFile = payload.files?.find(
|
|
||||||
(file) => file.target === "components/ui/card.tsx"
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(cardFile?.content).toContain("font-heading")
|
|
||||||
expect(cardFile?.content).not.toContain("cn-font-heading")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
|
import { Button } from "@/examples/radix/ui/button"
|
||||||
import { mdxComponents } from "@/mdx-components"
|
import { mdxComponents } from "@/mdx-components"
|
||||||
import { IconRss } from "@tabler/icons-react"
|
import { IconRss } from "@tabler/icons-react"
|
||||||
|
|
||||||
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
|
import { getChangelogPages, type ChangelogPageData } from "@/lib/changelog"
|
||||||
import { absoluteUrl } from "@/lib/utils"
|
import { absoluteUrl } from "@/lib/utils"
|
||||||
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
import { OpenInV0Cta } from "@/components/open-in-v0-cta"
|
||||||
import { Button } from "@/styles/radix-nova/ui/button"
|
|
||||||
|
|
||||||
export const revalidate = false
|
export const revalidate = false
|
||||||
export const dynamic = "force-static"
|
export const dynamic = "force-static"
|
||||||
@@ -79,7 +79,7 @@ export default function ChangelogPage() {
|
|||||||
})}
|
})}
|
||||||
{olderPages.length > 0 && (
|
{olderPages.length > 0 && (
|
||||||
<div id="more-updates" className="mb-24 scroll-mt-24">
|
<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
|
More Updates
|
||||||
</h2>
|
</h2>
|
||||||
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
|
<div className="grid auto-rows-fr gap-3 sm:grid-cols-2">
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
import { Button } from "@/examples/base/ui-rtl/button"
|
||||||
|
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||||
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 {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldContent,
|
FieldContent,
|
||||||
@@ -16,13 +13,13 @@ import {
|
|||||||
FieldSeparator,
|
FieldSeparator,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
FieldTitle,
|
FieldTitle,
|
||||||
} from "@/styles/base-nova/ui-rtl/field"
|
} from "@/examples/base/ui-rtl/field"
|
||||||
import { Input } from "@/styles/base-nova/ui-rtl/input"
|
import { Input } from "@/examples/base/ui-rtl/input"
|
||||||
import {
|
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui-rtl/radio-group"
|
||||||
RadioGroup,
|
import { Switch } from "@/examples/base/ui-rtl/switch"
|
||||||
RadioGroupItem,
|
import { IconMinus, IconPlus } from "@tabler/icons-react"
|
||||||
} from "@/styles/base-nova/ui-rtl/radio-group"
|
|
||||||
import { Switch } from "@/styles/base-nova/ui-rtl/switch"
|
import { useLanguageContext } from "@/components/language-selector"
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import {
|
import { Button } from "@/examples/base/ui-rtl/button"
|
||||||
ArchiveIcon,
|
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||||
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 {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -29,7 +16,20 @@ import {
|
|||||||
DropdownMenuSubContent,
|
DropdownMenuSubContent,
|
||||||
DropdownMenuSubTrigger,
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,22 +1,22 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { AudioLinesIcon, PlusIcon } from "lucide-react"
|
import { Button } from "@/examples/base/ui-rtl/button"
|
||||||
|
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||||
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 {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
} from "@/examples/base/ui-rtl/input-group"
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipContent,
|
TooltipContent,
|
||||||
TooltipTrigger,
|
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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
"use client"
|
"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 { ArrowLeftIcon, ArrowRightIcon } from "lucide-react"
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
import { Button } from "@/examples/base/ui-rtl/button"
|
||||||
|
import { ButtonGroup } from "@/examples/base/ui-rtl/button-group"
|
||||||
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 {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
PopoverTrigger,
|
||||||
} from "@/styles/base-nova/ui-rtl/popover"
|
} from "@/examples/base/ui-rtl/popover"
|
||||||
import { Separator } from "@/styles/base-nova/ui-rtl/separator"
|
import { Separator } from "@/examples/base/ui-rtl/separator"
|
||||||
import { Textarea } from "@/styles/base-nova/ui-rtl/textarea"
|
import { Textarea } from "@/examples/base/ui-rtl/textarea"
|
||||||
|
import { BotIcon, ChevronDownIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { useLanguageContext } from "@/components/language-selector"
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { PlusIcon } from "lucide-react"
|
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
|
||||||
import {
|
import {
|
||||||
Avatar,
|
Avatar,
|
||||||
AvatarFallback,
|
AvatarFallback,
|
||||||
AvatarGroup,
|
AvatarGroup,
|
||||||
AvatarImage,
|
AvatarImage,
|
||||||
} from "@/styles/base-nova/ui-rtl/avatar"
|
} from "@/examples/base/ui-rtl/avatar"
|
||||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
import { Button } from "@/examples/base/ui-rtl/button"
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
@@ -17,7 +14,10 @@ import {
|
|||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client"
|
"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 { 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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
import { Button } from "@/examples/base/ui-rtl/button"
|
||||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
|
||||||
import { Checkbox } from "@/styles/base-nova/ui-rtl/checkbox"
|
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
@@ -11,8 +10,8 @@ import {
|
|||||||
FieldLegend,
|
FieldLegend,
|
||||||
FieldSeparator,
|
FieldSeparator,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
} from "@/styles/base-nova/ui-rtl/field"
|
} from "@/examples/base/ui-rtl/field"
|
||||||
import { Input } from "@/styles/base-nova/ui-rtl/input"
|
import { Input } from "@/examples/base/ui-rtl/input"
|
||||||
import {
|
import {
|
||||||
Select,
|
Select,
|
||||||
SelectContent,
|
SelectContent,
|
||||||
@@ -20,8 +19,10 @@ import {
|
|||||||
SelectItem,
|
SelectItem,
|
||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from "@/styles/base-nova/ui-rtl/select"
|
} from "@/examples/base/ui-rtl/select"
|
||||||
import { Textarea } from "@/styles/base-nova/ui-rtl/textarea"
|
import { Textarea } from "@/examples/base/ui-rtl/textarea"
|
||||||
|
|
||||||
|
import { useLanguageContext } from "@/components/language-selector"
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
@@ -139,7 +140,7 @@ export function FieldDemo() {
|
|||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="rtl-exp-month">{t.month}</FieldLabel>
|
<FieldLabel htmlFor="rtl-exp-month">{t.month}</FieldLabel>
|
||||||
<Select defaultValue="">
|
<Select defaultValue="" items={months}>
|
||||||
<SelectTrigger id="rtl-exp-month">
|
<SelectTrigger id="rtl-exp-month">
|
||||||
<SelectValue placeholder="MM" />
|
<SelectValue placeholder="MM" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -156,7 +157,7 @@ export function FieldDemo() {
|
|||||||
</Field>
|
</Field>
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel htmlFor="rtl-exp-year">{t.year}</FieldLabel>
|
<FieldLabel htmlFor="rtl-exp-year">{t.year}</FieldLabel>
|
||||||
<Select defaultValue="">
|
<Select defaultValue="" items={years}>
|
||||||
<SelectTrigger id="rtl-exp-year">
|
<SelectTrigger id="rtl-exp-year">
|
||||||
<SelectValue placeholder="YYYY" />
|
<SelectValue placeholder="YYYY" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
import { Card, CardContent } from "@/examples/base/ui-rtl/card"
|
||||||
import { Card, CardContent } from "@/styles/base-nova/ui-rtl/card"
|
import { Checkbox } from "@/examples/base/ui-rtl/checkbox"
|
||||||
import { Checkbox } from "@/styles/base-nova/ui-rtl/checkbox"
|
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
@@ -11,7 +10,9 @@ import {
|
|||||||
FieldLegend,
|
FieldLegend,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
FieldTitle,
|
FieldTitle,
|
||||||
} from "@/styles/base-nova/ui-rtl/field"
|
} from "@/examples/base/ui-rtl/field"
|
||||||
|
|
||||||
|
import { useLanguageContext } from "@/components/language-selector"
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react"
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldDescription,
|
FieldDescription,
|
||||||
FieldTitle,
|
FieldTitle,
|
||||||
} from "@/styles/base-nova/ui-rtl/field"
|
} from "@/examples/base/ui-rtl/field"
|
||||||
import { Slider } from "@/styles/base-nova/ui-rtl/slider"
|
import { Slider } from "@/examples/base/ui-rtl/slider"
|
||||||
|
|
||||||
|
import { useLanguageContext } from "@/components/language-selector"
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
|
import { DirectionProvider } from "@/examples/base/ui-rtl/direction"
|
||||||
|
import { FieldSeparator } from "@/examples/base/ui-rtl/field"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LanguageProvider,
|
LanguageProvider,
|
||||||
LanguageSelector,
|
LanguageSelector,
|
||||||
useLanguageContext,
|
useLanguageContext,
|
||||||
} from "@/components/language-selector"
|
} 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 { AppearanceSettings } from "./appearance-settings"
|
||||||
import { ButtonGroupDemo } from "./button-group-demo"
|
import { ButtonGroupDemo } from "./button-group-demo"
|
||||||
|
|||||||
@@ -1,21 +1,21 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { IconInfoCircle, IconStar } from "@tabler/icons-react"
|
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
|
||||||
import {
|
import {
|
||||||
InputGroup,
|
InputGroup,
|
||||||
InputGroupAddon,
|
InputGroupAddon,
|
||||||
InputGroupButton,
|
InputGroupButton,
|
||||||
InputGroupInput,
|
InputGroupInput,
|
||||||
} from "@/styles/base-nova/ui-rtl/input-group"
|
} from "@/examples/base/ui-rtl/input-group"
|
||||||
import { Label } from "@/styles/base-nova/ui-rtl/label"
|
import { Label } from "@/examples/base/ui-rtl/label"
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
PopoverTrigger,
|
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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,5 +1,25 @@
|
|||||||
"use client"
|
"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 {
|
import {
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
@@ -9,26 +29,6 @@ import {
|
|||||||
import { ArrowUpIcon, Search } from "lucide-react"
|
import { ArrowUpIcon, Search } from "lucide-react"
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { BadgeCheckIcon, ChevronRightIcon } from "lucide-react"
|
import { Button } from "@/examples/base/ui-rtl/button"
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
|
||||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
|
||||||
import {
|
import {
|
||||||
Item,
|
Item,
|
||||||
ItemActions,
|
ItemActions,
|
||||||
@@ -11,7 +8,10 @@ import {
|
|||||||
ItemDescription,
|
ItemDescription,
|
||||||
ItemMedia,
|
ItemMedia,
|
||||||
ItemTitle,
|
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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,6 +1,47 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useMemo, useState } from "react"
|
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 {
|
import {
|
||||||
IconApps,
|
IconApps,
|
||||||
IconArrowUp,
|
IconArrowUp,
|
||||||
@@ -14,47 +55,6 @@ import {
|
|||||||
} from "@tabler/icons-react"
|
} from "@tabler/icons-react"
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
"use client"
|
"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 { 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 = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useLanguageContext } from "@/components/language-selector"
|
import { Button } from "@/examples/base/ui-rtl/button"
|
||||||
import { Button } from "@/styles/base-nova/ui-rtl/button"
|
|
||||||
import {
|
import {
|
||||||
Empty,
|
Empty,
|
||||||
EmptyContent,
|
EmptyContent,
|
||||||
@@ -9,8 +8,10 @@ import {
|
|||||||
EmptyHeader,
|
EmptyHeader,
|
||||||
EmptyMedia,
|
EmptyMedia,
|
||||||
EmptyTitle,
|
EmptyTitle,
|
||||||
} from "@/styles/base-nova/ui-rtl/empty"
|
} from "@/examples/base/ui-rtl/empty"
|
||||||
import { Spinner } from "@/styles/base-nova/ui-rtl/spinner"
|
import { Spinner } from "@/examples/base/ui-rtl/spinner"
|
||||||
|
|
||||||
|
import { useLanguageContext } from "@/components/language-selector"
|
||||||
|
|
||||||
const translations = {
|
const translations = {
|
||||||
ar: {
|
ar: {
|
||||||
|
|||||||
@@ -5,10 +5,10 @@ export default function AppLayout({ children }: { children: React.ReactNode }) {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-slot="layout"
|
data-slot="layout"
|
||||||
className="group/layout relative z-10 flex min-h-svh flex-col bg-background has-data-[slot=designer]:h-svh has-data-[slot=designer]:overflow-hidden"
|
className="relative z-10 flex min-h-svh flex-col bg-background"
|
||||||
>
|
>
|
||||||
<SiteHeader />
|
<SiteHeader />
|
||||||
<main className="flex min-h-0 flex-1 flex-col">{children}</main>
|
<main className="flex flex-1 flex-col">{children}</main>
|
||||||
<SiteFooter />
|
<SiteFooter />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
64
apps/v4/app/(app)/themes/layout.tsx
Normal file
64
apps/v4/app/(app)/themes/layout.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import { type Metadata } from "next"
|
||||||
|
import Link from "next/link"
|
||||||
|
|
||||||
|
import { Announcement } from "@/components/announcement"
|
||||||
|
import {
|
||||||
|
PageActions,
|
||||||
|
PageHeader,
|
||||||
|
PageHeaderDescription,
|
||||||
|
PageHeaderHeading,
|
||||||
|
} from "@/components/page-header"
|
||||||
|
import { Button } from "@/registry/new-york-v4/ui/button"
|
||||||
|
|
||||||
|
const title = "Pick a Color. Make it yours."
|
||||||
|
const description =
|
||||||
|
"Try our hand-picked themes. Copy and paste them into your project. New theme editor coming soon."
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
openGraph: {
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `/og?title=${encodeURIComponent(
|
||||||
|
title
|
||||||
|
)}&description=${encodeURIComponent(description)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: `/og?title=${encodeURIComponent(
|
||||||
|
title
|
||||||
|
)}&description=${encodeURIComponent(description)}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ThemesLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<PageHeader>
|
||||||
|
<Announcement />
|
||||||
|
<PageHeaderHeading>{title}</PageHeaderHeading>
|
||||||
|
<PageHeaderDescription>{description}</PageHeaderDescription>
|
||||||
|
<PageActions>
|
||||||
|
<Button asChild size="sm">
|
||||||
|
<a href="#themes">Browse Themes</a>
|
||||||
|
</Button>
|
||||||
|
<Button asChild variant="ghost" size="sm">
|
||||||
|
<Link href="/docs/theming">Documentation</Link>
|
||||||
|
</Button>
|
||||||
|
</PageActions>
|
||||||
|
</PageHeader>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
22
apps/v4/app/(app)/themes/page.tsx
Normal file
22
apps/v4/app/(app)/themes/page.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import { CardsDemo } from "@/components/cards"
|
||||||
|
import { ThemeCustomizer } from "@/components/theme-customizer"
|
||||||
|
|
||||||
|
export const dynamic = "force-static"
|
||||||
|
export const revalidate = false
|
||||||
|
|
||||||
|
export default function ThemesPage() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="themes" className="container-wrapper scroll-mt-20">
|
||||||
|
<div className="container flex items-center justify-between gap-8 px-6 py-4 md:px-8">
|
||||||
|
<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">
|
||||||
|
<CardsDemo />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
|
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -9,8 +9,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
export function MenuAccentPicker({
|
export function MenuAccentPicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
import { type RegistryItem } from "shadcn/schema"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandDialog,
|
CommandDialog,
|
||||||
@@ -11,8 +9,10 @@ import {
|
|||||||
CommandInput,
|
CommandInput,
|
||||||
CommandItem,
|
CommandItem,
|
||||||
CommandList,
|
CommandList,
|
||||||
} from "@/styles/base-nova/ui/command"
|
} from "@/examples/base/ui/command"
|
||||||
import { useActionMenu } from "@/app/(app)/create/hooks/use-action-menu"
|
import { type RegistryItem } from "shadcn/schema"
|
||||||
|
|
||||||
|
import { useActionMenu } from "@/app/(create)/hooks/use-action-menu"
|
||||||
|
|
||||||
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
export const CMD_K_FORWARD_TYPE = "cmd-k-forward"
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { useMounted } from "@/hooks/use-mounted"
|
import { useMounted } from "@/hooks/use-mounted"
|
||||||
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
|
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
export function BaseColorPicker({
|
export function BaseColorPicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
export function BasePicker({
|
export function BasePicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Button } from "@/examples/base/ui/button"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||||
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
|
||||||
|
|
||||||
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
|
||||||
const presetCode = usePresetCode()
|
const presetCode = usePresetCode()
|
||||||
88
apps/v4/app/(create)/components/customizer.tsx
Normal file
88
apps/v4/app/(create)/components/customizer.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardFooter,
|
||||||
|
CardHeader,
|
||||||
|
} from "@/examples/base/ui/card"
|
||||||
|
import { FieldGroup } from "@/examples/base/ui/field"
|
||||||
|
import { Separator } from "@/examples/base/ui/separator"
|
||||||
|
import { CardTitle } from "@/examples/radix/ui/card"
|
||||||
|
import { type RegistryItem } from "shadcn/schema"
|
||||||
|
|
||||||
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
|
import { getThemesForBaseColor, STYLES } from "@/registry/config"
|
||||||
|
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 { 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 { ProjectForm } from "@/app/(create)/components/project-form"
|
||||||
|
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 { StylePicker } from "@/app/(create)/components/style-picker"
|
||||||
|
import { ThemePicker } from "@/app/(create)/components/theme-picker"
|
||||||
|
import { V0Button } from "@/app/(create)/components/v0-button"
|
||||||
|
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">[]>
|
||||||
|
}) {
|
||||||
|
const [params] = useDesignSystemSearchParams()
|
||||||
|
const isMobile = useIsMobile()
|
||||||
|
const anchorRef = React.useRef<HTMLDivElement | null>(null)
|
||||||
|
|
||||||
|
const availableThemes = React.useMemo(
|
||||||
|
() => getThemesForBaseColor(params.baseColor),
|
||||||
|
[params.baseColor]
|
||||||
|
)
|
||||||
|
|
||||||
|
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)"
|
||||||
|
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 md:flex-col md:gap-3.25">
|
||||||
|
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
|
<StylePicker
|
||||||
|
styles={STYLES}
|
||||||
|
isMobile={isMobile}
|
||||||
|
anchorRef={anchorRef}
|
||||||
|
/>
|
||||||
|
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
|
<ThemePicker
|
||||||
|
themes={availableThemes}
|
||||||
|
isMobile={isMobile}
|
||||||
|
anchorRef={anchorRef}
|
||||||
|
/>
|
||||||
|
<IconLibraryPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
|
<FontPicker fonts={FONTS} isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
|
<RadiusPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
|
<MenuColorPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
|
<MenuAccentPicker isMobile={isMobile} anchorRef={anchorRef} />
|
||||||
|
</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>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -7,12 +7,12 @@ import {
|
|||||||
DEFAULT_CONFIG,
|
DEFAULT_CONFIG,
|
||||||
type DesignSystemConfig,
|
type DesignSystemConfig,
|
||||||
} from "@/registry/config"
|
} from "@/registry/config"
|
||||||
import { useIframeMessageListener } from "@/app/(app)/create/hooks/use-iframe-sync"
|
import { useIframeMessageListener } from "@/app/(create)/hooks/use-iframe-sync"
|
||||||
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||||
import {
|
import {
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
type DesignSystemSearchParams,
|
type DesignSystemSearchParams,
|
||||||
} from "@/app/(app)/create/lib/search-params"
|
} from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
|
const THEME_STYLE_ELEMENT_ID = "design-system-theme-vars"
|
||||||
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
|
const MANAGED_BODY_CLASS_PREFIXES = ["style-", "base-color-"] as const
|
||||||
@@ -64,37 +64,18 @@ export function DesignSystemProvider({
|
|||||||
history: "replace", // …or push updates into the iframe history.
|
history: "replace", // …or push updates into the iframe history.
|
||||||
})
|
})
|
||||||
const [isReady, setIsReady] = React.useState(false)
|
const [isReady, setIsReady] = React.useState(false)
|
||||||
const {
|
const { style, theme, font, baseColor, menuAccent, menuColor, radius } =
|
||||||
style,
|
searchParams
|
||||||
theme,
|
|
||||||
font,
|
|
||||||
fontHeading,
|
|
||||||
baseColor,
|
|
||||||
chartColor,
|
|
||||||
menuAccent,
|
|
||||||
menuColor,
|
|
||||||
radius,
|
|
||||||
} = searchParams
|
|
||||||
const effectiveRadius = style === "lyra" ? "none" : radius
|
const effectiveRadius = style === "lyra" ? "none" : radius
|
||||||
const selectedFont = React.useMemo(
|
const selectedFont = React.useMemo(
|
||||||
() => FONTS.find((fontOption) => fontOption.value === font),
|
() => FONTS.find((fontOption) => fontOption.value === font),
|
||||||
[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 initialFontSansRef = React.useRef<string | null>(null)
|
||||||
const initialFontHeadingRef = React.useRef<string | null>(null)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
initialFontSansRef.current =
|
initialFontSansRef.current =
|
||||||
document.documentElement.style.getPropertyValue("--font-sans")
|
document.documentElement.style.getPropertyValue("--font-sans")
|
||||||
initialFontHeadingRef.current =
|
|
||||||
document.documentElement.style.getPropertyValue("--font-heading")
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
removeManagedBodyClasses(document.body)
|
removeManagedBodyClasses(document.body)
|
||||||
@@ -105,18 +86,10 @@ export function DesignSystemProvider({
|
|||||||
"--font-sans",
|
"--font-sans",
|
||||||
initialFontSansRef.current
|
initialFontSansRef.current
|
||||||
)
|
)
|
||||||
} else {
|
return
|
||||||
document.documentElement.style.removeProperty("--font-sans")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (initialFontHeadingRef.current) {
|
document.documentElement.style.removeProperty("--font-sans")
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--font-heading",
|
|
||||||
initialFontHeadingRef.current
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
document.documentElement.style.removeProperty("--font-heading")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
@@ -151,29 +124,12 @@ export function DesignSystemProvider({
|
|||||||
// Always set --font-sans for the preview so the selected font is visible.
|
// 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.
|
// The font type (sans/serif/mono) is metadata for the CLI updater.
|
||||||
if (selectedFont) {
|
if (selectedFont) {
|
||||||
document.documentElement.style.setProperty(
|
const fontFamily = selectedFont.font.style.fontFamily
|
||||||
"--font-sans",
|
document.documentElement.style.setProperty("--font-sans", fontFamily)
|
||||||
selectedFont.font.style.fontFamily
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (selectedHeadingFont) {
|
|
||||||
document.documentElement.style.setProperty(
|
|
||||||
"--font-heading",
|
|
||||||
selectedHeadingFont.font.style.fontFamily
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsReady(true)
|
setIsReady(true)
|
||||||
}, [
|
}, [style, theme, font, baseColor, selectedFont])
|
||||||
style,
|
|
||||||
theme,
|
|
||||||
font,
|
|
||||||
fontHeading,
|
|
||||||
baseColor,
|
|
||||||
selectedFont,
|
|
||||||
selectedHeadingFont,
|
|
||||||
])
|
|
||||||
|
|
||||||
const registryTheme = React.useMemo(() => {
|
const registryTheme = React.useMemo(() => {
|
||||||
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
|
if (!baseColor || !theme || !menuAccent || !effectiveRadius) {
|
||||||
@@ -184,13 +140,12 @@ export function DesignSystemProvider({
|
|||||||
...DEFAULT_CONFIG,
|
...DEFAULT_CONFIG,
|
||||||
baseColor,
|
baseColor,
|
||||||
theme,
|
theme,
|
||||||
chartColor,
|
|
||||||
menuAccent,
|
menuAccent,
|
||||||
radius: effectiveRadius,
|
radius: effectiveRadius,
|
||||||
}
|
}
|
||||||
|
|
||||||
return buildRegistryTheme(config)
|
return buildRegistryTheme(config)
|
||||||
}, [baseColor, theme, chartColor, menuAccent, effectiveRadius])
|
}, [baseColor, theme, menuAccent, effectiveRadius])
|
||||||
|
|
||||||
// Use useLayoutEffect for synchronous CSS var updates.
|
// Use useLayoutEffect for synchronous CSS var updates.
|
||||||
React.useLayoutEffect(() => {
|
React.useLayoutEffect(() => {
|
||||||
113
apps/v4/app/(create)/components/font-picker.tsx
Normal file
113
apps/v4/app/(create)/components/font-picker.tsx
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
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 { type Font } from "@/app/(create)/lib/fonts"
|
||||||
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
|
export function FontPicker({
|
||||||
|
fonts,
|
||||||
|
isMobile,
|
||||||
|
anchorRef,
|
||||||
|
}: {
|
||||||
|
fonts: readonly Font[]
|
||||||
|
isMobile: boolean
|
||||||
|
anchorRef: React.RefObject<HTMLDivElement | null>
|
||||||
|
}) {
|
||||||
|
const [params, setParams] = useDesignSystemSearchParams()
|
||||||
|
|
||||||
|
const currentFont = React.useMemo(
|
||||||
|
() => fonts.find((font) => font.value === params.font),
|
||||||
|
[fonts, params.font]
|
||||||
|
)
|
||||||
|
const groupedFonts = React.useMemo(() => {
|
||||||
|
const groups = new Map<Font["type"], Font[]>()
|
||||||
|
|
||||||
|
for (const font of fonts) {
|
||||||
|
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])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="group/picker relative">
|
||||||
|
<Picker>
|
||||||
|
<PickerTrigger>
|
||||||
|
<div className="flex flex-col justify-start text-left">
|
||||||
|
<div className="text-xs text-muted-foreground">Font</div>
|
||||||
|
<div className="text-sm font-medium text-foreground">
|
||||||
|
{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 }}
|
||||||
|
>
|
||||||
|
Aa
|
||||||
|
</div>
|
||||||
|
</PickerTrigger>
|
||||||
|
<PickerContent
|
||||||
|
anchor={isMobile ? anchorRef : undefined}
|
||||||
|
side={isMobile ? "top" : "right"}
|
||||||
|
align={isMobile ? "center" : "start"}
|
||||||
|
className="max-h-96"
|
||||||
|
>
|
||||||
|
<PickerRadioGroup
|
||||||
|
value={currentFont?.value}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
setParams({ font: value as FontValue })
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{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>
|
||||||
|
))}
|
||||||
|
</PickerRadioGroup>
|
||||||
|
</PickerContent>
|
||||||
|
</Picker>
|
||||||
|
<LockButton
|
||||||
|
param="font"
|
||||||
|
className="absolute top-1/2 right-8 -translate-y-1/2"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
import { Button } from "@/examples/base/ui/button"
|
||||||
import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
|
import { Redo02Icon, Undo02Icon } from "@hugeicons/core-free-icons"
|
||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
import { HugeiconsIcon } from "@hugeicons/react"
|
||||||
|
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { useHistory } from "@/app/(create)/hooks/use-history"
|
||||||
import { useHistory } from "@/app/(app)/create/hooks/use-history"
|
|
||||||
|
|
||||||
export const UNDO_FORWARD_TYPE = "undo-forward"
|
export const UNDO_FORWARD_TYPE = "undo-forward"
|
||||||
export const REDO_FORWARD_TYPE = "redo-forward"
|
export const REDO_FORWARD_TYPE = "redo-forward"
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { iconLibraries, type IconLibraryName } from "@/registry/config"
|
import { iconLibraries, type IconLibraryName } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
const logos = {
|
const logos = {
|
||||||
lucide: (
|
lucide: (
|
||||||
@@ -1,3 +1,75 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
export { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
import { lazy, Suspense } from "react"
|
||||||
|
import { SquareIcon } from "lucide-react"
|
||||||
|
import type { IconLibraryName } from "shadcn/icons"
|
||||||
|
|
||||||
|
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,
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}: {
|
||||||
|
[K in IconLibraryName]: string
|
||||||
|
} & React.ComponentProps<"svg">) {
|
||||||
|
const [{ iconLibrary }] = useDesignSystemSearchParams()
|
||||||
|
const iconName = props[iconLibrary]
|
||||||
|
|
||||||
|
if (!iconName) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<SquareIcon {...props} />}>
|
||||||
|
{iconLibrary === "lucide" && <IconLucide name={iconName} {...props} />}
|
||||||
|
{iconLibrary === "tabler" && <IconTabler name={iconName} {...props} />}
|
||||||
|
{iconLibrary === "hugeicons" && (
|
||||||
|
<IconHugeicons name={iconName} {...props} />
|
||||||
|
)}
|
||||||
|
{iconLibrary === "phosphor" && (
|
||||||
|
<IconPhosphor name={iconName} {...props} />
|
||||||
|
)}
|
||||||
|
{iconLibrary === "remixicon" && (
|
||||||
|
<IconRemixicon name={iconName} {...props} />
|
||||||
|
)}
|
||||||
|
</Suspense>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,16 +2,11 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import Link from "next/link"
|
import Link from "next/link"
|
||||||
import { ChevronRightIcon } from "lucide-react"
|
|
||||||
import { type RegistryItem } from "shadcn/schema"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
import { type Base } from "@/registry/bases"
|
|
||||||
import {
|
import {
|
||||||
Collapsible,
|
Collapsible,
|
||||||
CollapsibleContent,
|
CollapsibleContent,
|
||||||
CollapsibleTrigger,
|
CollapsibleTrigger,
|
||||||
} from "@/styles/base-nova/ui/collapsible"
|
} from "@/examples/base/ui/collapsible"
|
||||||
import {
|
import {
|
||||||
Sidebar,
|
Sidebar,
|
||||||
SidebarContent,
|
SidebarContent,
|
||||||
@@ -20,9 +15,14 @@ import {
|
|||||||
SidebarMenu,
|
SidebarMenu,
|
||||||
SidebarMenuButton,
|
SidebarMenuButton,
|
||||||
SidebarMenuItem,
|
SidebarMenuItem,
|
||||||
} from "@/styles/base-nova/ui/sidebar"
|
} from "@/examples/base/ui/sidebar"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { ChevronRightIcon } from "lucide-react"
|
||||||
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
|
import { type RegistryItem } from "shadcn/schema"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
import { type Base } from "@/registry/bases"
|
||||||
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||||
|
|
||||||
const cachedGroupedItems = React.cache(
|
const cachedGroupedItems = React.cache(
|
||||||
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
|
(items: Pick<RegistryItem, "name" | "title" | "type">[]) => {
|
||||||
@@ -7,10 +7,7 @@ import {
|
|||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
import { HugeiconsIcon } from "@hugeicons/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import {
|
import { useLocks, type LockableParam } from "@/app/(create)/hooks/use-locks"
|
||||||
useLocks,
|
|
||||||
type LockableParam,
|
|
||||||
} from "@/app/(app)/create/hooks/use-locks"
|
|
||||||
|
|
||||||
export function LockButton({
|
export function LockButton({
|
||||||
param,
|
param,
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { type Button } from "@/examples/base/ui/button"
|
||||||
import { Menu09Icon } from "@hugeicons/core-free-icons"
|
import { Menu09Icon } from "@hugeicons/core-free-icons"
|
||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
import { HugeiconsIcon } from "@hugeicons/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { type Button } from "@/styles/base-nova/ui/button"
|
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -14,12 +14,12 @@ import {
|
|||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerShortcut,
|
PickerShortcut,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useActionMenuTrigger } from "@/app/(app)/create/hooks/use-action-menu"
|
import { useActionMenuTrigger } from "@/app/(create)/hooks/use-action-menu"
|
||||||
import { useHistory } from "@/app/(app)/create/hooks/use-history"
|
import { useHistory } from "@/app/(create)/hooks/use-history"
|
||||||
import { useRandom } from "@/app/(app)/create/hooks/use-random"
|
import { useRandom } from "@/app/(create)/hooks/use-random"
|
||||||
import { useReset } from "@/app/(app)/create/hooks/use-reset"
|
import { useReset } from "@/app/(create)/hooks/use-reset"
|
||||||
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
|
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
||||||
|
|
||||||
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
|
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
|
||||||
|
|
||||||
@@ -73,7 +73,7 @@ export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
|
|||||||
</PickerItem>
|
</PickerItem>
|
||||||
<PickerSeparator />
|
<PickerSeparator />
|
||||||
<PickerItem onClick={() => setShowResetDialog(true)}>
|
<PickerItem onClick={() => setShowResetDialog(true)}>
|
||||||
Reset <PickerShortcut>⇧R</PickerShortcut>
|
Reset
|
||||||
</PickerItem>
|
</PickerItem>
|
||||||
</PickerGroup>
|
</PickerGroup>
|
||||||
</PickerContent>
|
</PickerContent>
|
||||||
@@ -7,7 +7,7 @@ import { useTheme } from "next-themes"
|
|||||||
|
|
||||||
import { useMounted } from "@/hooks/use-mounted"
|
import { useMounted } from "@/hooks/use-mounted"
|
||||||
import { type MenuColorValue } from "@/registry/config"
|
import { type MenuColorValue } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -17,11 +17,11 @@ import {
|
|||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import {
|
import {
|
||||||
isTranslucentMenuColor,
|
isTranslucentMenuColor,
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
} from "@/app/(app)/create/lib/search-params"
|
} from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
type ColorChoice = "default" | "inverted"
|
type ColorChoice = "default" | "inverted"
|
||||||
type SurfaceChoice = "solid" | "translucent"
|
type SurfaceChoice = "solid" | "translucent"
|
||||||
@@ -104,7 +104,7 @@ export function MenuColorPicker({
|
|||||||
<PickerTrigger>
|
<PickerTrigger>
|
||||||
<div className="flex flex-col justify-start text-left">
|
<div className="flex flex-col justify-start text-left">
|
||||||
<div className="text-xs text-muted-foreground">Menu</div>
|
<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-sm font-medium text-foreground">
|
||||||
{currentMenu?.label}
|
{currentMenu?.label}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
import { Button } from "@/examples/base/ui/button"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
|
||||||
import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle"
|
|
||||||
|
|
||||||
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
export const DARK_MODE_FORWARD_TYPE = "dark-mode-forward"
|
||||||
|
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
import { Menu as MenuPrimitive } from "@base-ui/react/menu"
|
||||||
|
|
||||||
import { cn } from "@/registry/bases/base/lib/utils"
|
import { cn } from "@/registry/bases/base/lib/utils"
|
||||||
import { IconPlaceholder } from "@/app/(app)/create/components/icon-placeholder"
|
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
|
||||||
|
|
||||||
function Picker({ ...props }: MenuPrimitive.Root.Props) {
|
function Picker({ ...props }: MenuPrimitive.Root.Props) {
|
||||||
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
|
||||||
@@ -19,7 +19,7 @@ function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
|
|||||||
<MenuPrimitive.Trigger
|
<MenuPrimitive.Trigger
|
||||||
data-slot="dropdown-menu-trigger"
|
data-slot="dropdown-menu-trigger"
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative w-36 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",
|
"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",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation"
|
||||||
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
|
import { generateRandomPreset, isPresetCode } from "shadcn/preset"
|
||||||
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
export function PresetHandler() {
|
export function PresetHandler() {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -10,8 +10,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
export function PresetPicker({
|
export function PresetPicker({
|
||||||
presets,
|
presets,
|
||||||
@@ -31,10 +31,8 @@ export function PresetPicker({
|
|||||||
preset.style === params.style &&
|
preset.style === params.style &&
|
||||||
preset.baseColor === params.baseColor &&
|
preset.baseColor === params.baseColor &&
|
||||||
preset.theme === params.theme &&
|
preset.theme === params.theme &&
|
||||||
preset.chartColor === params.chartColor &&
|
|
||||||
preset.iconLibrary === params.iconLibrary &&
|
preset.iconLibrary === params.iconLibrary &&
|
||||||
preset.font === params.font &&
|
preset.font === params.font &&
|
||||||
preset.fontHeading === params.fontHeading &&
|
|
||||||
preset.menuAccent === params.menuAccent &&
|
preset.menuAccent === params.menuAccent &&
|
||||||
preset.menuColor === params.menuColor &&
|
preset.menuColor === params.menuColor &&
|
||||||
preset.radius === params.radius
|
preset.radius === params.radius
|
||||||
@@ -45,10 +43,8 @@ export function PresetPicker({
|
|||||||
params.style,
|
params.style,
|
||||||
params.baseColor,
|
params.baseColor,
|
||||||
params.theme,
|
params.theme,
|
||||||
params.chartColor,
|
|
||||||
params.iconLibrary,
|
params.iconLibrary,
|
||||||
params.font,
|
params.font,
|
||||||
params.fontHeading,
|
|
||||||
params.menuAccent,
|
params.menuAccent,
|
||||||
params.menuColor,
|
params.menuColor,
|
||||||
params.radius,
|
params.radius,
|
||||||
@@ -71,10 +67,8 @@ export function PresetPicker({
|
|||||||
style: preset.style,
|
style: preset.style,
|
||||||
baseColor: preset.baseColor,
|
baseColor: preset.baseColor,
|
||||||
theme: preset.theme,
|
theme: preset.theme,
|
||||||
chartColor: preset.chartColor,
|
|
||||||
iconLibrary: preset.iconLibrary,
|
iconLibrary: preset.iconLibrary,
|
||||||
font: preset.font,
|
font: preset.font,
|
||||||
fontHeading: preset.fontHeading,
|
|
||||||
menuAccent: preset.menuAccent,
|
menuAccent: preset.menuAccent,
|
||||||
menuColor: preset.menuColor,
|
menuColor: preset.menuColor,
|
||||||
radius: preset.radius,
|
radius: preset.radius,
|
||||||
@@ -112,6 +106,13 @@ export function PresetPicker({
|
|||||||
closeOnClick={isMobile}
|
closeOnClick={isMobile}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
|
{style?.icon && (
|
||||||
|
<div className="flex size-4 shrink-0 items-center justify-center">
|
||||||
|
{React.cloneElement(style.icon, {
|
||||||
|
className: "size-4",
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{preset.description}
|
{preset.description}
|
||||||
</div>
|
</div>
|
||||||
</PickerRadioItem>
|
</PickerRadioItem>
|
||||||
@@ -2,20 +2,18 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { CMD_K_FORWARD_TYPE } from "@/app/(app)/create/components/action-menu"
|
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/action-menu"
|
||||||
import {
|
import {
|
||||||
REDO_FORWARD_TYPE,
|
REDO_FORWARD_TYPE,
|
||||||
UNDO_FORWARD_TYPE,
|
UNDO_FORWARD_TYPE,
|
||||||
} from "@/app/(app)/create/components/history-buttons"
|
} from "@/app/(create)/components/history-buttons"
|
||||||
import { DARK_MODE_FORWARD_TYPE } from "@/app/(app)/create/components/mode-switcher"
|
import { DARK_MODE_FORWARD_TYPE } from "@/app/(create)/components/mode-switcher"
|
||||||
import { PreviewSwitcher } from "@/app/(app)/create/components/preview-switcher"
|
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(create)/components/random-button"
|
||||||
import { RANDOMIZE_FORWARD_TYPE } from "@/app/(app)/create/components/random-button"
|
import { sendToIframe } from "@/app/(create)/hooks/use-iframe-sync"
|
||||||
import { sendToIframe } from "@/app/(app)/create/hooks/use-iframe-sync"
|
|
||||||
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
|
|
||||||
import {
|
import {
|
||||||
serializeDesignSystemSearchParams,
|
serializeDesignSystemSearchParams,
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
} from "@/app/(app)/create/lib/search-params"
|
} from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
|
// Hoisted — avoids recreating on every message event. (js-hoist-regexp)
|
||||||
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
|
const MAC_REGEX = /Mac|iPhone|iPad|iPod/
|
||||||
@@ -72,15 +70,6 @@ function handleMessage(event: MessageEvent) {
|
|||||||
cancelable: 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) {
|
} else if (type === DARK_MODE_FORWARD_TYPE) {
|
||||||
document.dispatchEvent(
|
document.dispatchEvent(
|
||||||
new KeyboardEvent("keydown", {
|
new KeyboardEvent("keydown", {
|
||||||
@@ -148,7 +137,6 @@ export function Preview() {
|
|||||||
title="Preview"
|
title="Preview"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<PreviewSwitcher />
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
import { Copy01Icon, Globe02Icon, Tick02Icon } from "@hugeicons/core-free-icons"
|
import { Button } from "@/examples/base/ui/button"
|
||||||
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 {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
@@ -17,7 +10,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/styles/base-nova/ui/dialog"
|
} from "@/examples/base/ui/dialog"
|
||||||
import {
|
import {
|
||||||
Field,
|
Field,
|
||||||
FieldContent,
|
FieldContent,
|
||||||
@@ -27,26 +20,32 @@ import {
|
|||||||
FieldSeparator,
|
FieldSeparator,
|
||||||
FieldSet,
|
FieldSet,
|
||||||
FieldTitle,
|
FieldTitle,
|
||||||
} from "@/styles/base-nova/ui/field"
|
} from "@/examples/base/ui/field"
|
||||||
import { RadioGroup, RadioGroupItem } from "@/styles/base-nova/ui/radio-group"
|
import { RadioGroup, RadioGroupItem } from "@/examples/base/ui/radio-group"
|
||||||
import { Switch } from "@/styles/base-nova/ui/switch"
|
import { Switch } from "@/examples/base/ui/switch"
|
||||||
import {
|
import {
|
||||||
Tabs,
|
Tabs,
|
||||||
TabsContent,
|
TabsContent,
|
||||||
TabsList,
|
TabsList,
|
||||||
TabsTrigger,
|
TabsTrigger,
|
||||||
} from "@/styles/base-nova/ui/tabs"
|
} from "@/examples/base/ui/tabs"
|
||||||
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
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 { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||||
import {
|
import {
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
type DesignSystemSearchParams,
|
type DesignSystemSearchParams,
|
||||||
} from "@/app/(app)/create/lib/search-params"
|
} from "@/app/(create)/lib/search-params"
|
||||||
import {
|
import {
|
||||||
getFramework,
|
getFramework,
|
||||||
getTemplateValue,
|
getTemplateValue,
|
||||||
NO_MONOREPO_FRAMEWORKS,
|
NO_MONOREPO_FRAMEWORKS,
|
||||||
TEMPLATES,
|
TEMPLATES,
|
||||||
} from "@/app/(app)/create/lib/templates"
|
} from "@/app/(create)/lib/templates"
|
||||||
|
|
||||||
const TURBOREPO_LOGO =
|
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>'
|
'<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>'
|
||||||
@@ -87,7 +86,7 @@ export function ProjectForm({
|
|||||||
const rtlFlag = params.rtl ? " --rtl" : ""
|
const rtlFlag = params.rtl ? " --rtl" : ""
|
||||||
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
|
const flags = `${presetFlag}${baseFlag}${templateFlag}${monorepoFlag}${rtlFlag}`
|
||||||
|
|
||||||
return IS_LOCAL_DEV
|
return IS_LOCAL_DEV && !process.env.NEXT_PUBLIC_RC
|
||||||
? {
|
? {
|
||||||
pnpm: `shadcn init${flags}`,
|
pnpm: `shadcn init${flags}`,
|
||||||
npm: `shadcn init${flags}`,
|
npm: `shadcn init${flags}`,
|
||||||
@@ -130,76 +129,69 @@ export function ProjectForm({
|
|||||||
<DialogTrigger render={<Button className={cn(className)} />}>
|
<DialogTrigger render={<Button className={cn(className)} />}>
|
||||||
Create Project
|
Create Project
|
||||||
</DialogTrigger>
|
</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">
|
<DialogContent className="min-w-0 sm:max-w-sm">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>Create Project</DialogTitle>
|
<DialogTitle>Create Project</DialogTitle>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
Pick a template and configure your project.
|
Pick a template and configure your project. Available for all major
|
||||||
|
React frameworks.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div>
|
<FieldGroup>
|
||||||
<FieldGroup>
|
<Field>
|
||||||
<FieldSeparator className="-mx-6" />
|
<FieldLabel className="sr-only">Template</FieldLabel>
|
||||||
<Field className="-mt-2 gap-3">
|
<TemplateGrid template={params.template} setParams={setParams} />
|
||||||
<FieldLabel>Template</FieldLabel>
|
</Field>
|
||||||
<TemplateGrid template={params.template} setParams={setParams} />
|
<FieldSeparator />
|
||||||
</Field>
|
<FieldSet>
|
||||||
<FieldSeparator className="-mx-6" />
|
<FieldLegend variant="label" className="sr-only">
|
||||||
<Field className="-mt-2">
|
Options
|
||||||
<FieldLabel>Base</FieldLabel>
|
</FieldLegend>
|
||||||
<BaseGrid base={params.base} setParams={setParams} />
|
<Field
|
||||||
</Field>
|
orientation="horizontal"
|
||||||
<FieldSeparator className="-mx-6" />
|
data-disabled={hasMonorepo ? undefined : "true"}
|
||||||
<FieldSet>
|
>
|
||||||
<FieldLegend variant="label" className="sr-only">
|
<FieldLabel htmlFor="monorepo">
|
||||||
Options
|
<span
|
||||||
</FieldLegend>
|
className="size-4 text-foreground [&_svg]:size-4 [&_svg]:fill-current"
|
||||||
<Field
|
dangerouslySetInnerHTML={{
|
||||||
orientation="horizontal"
|
__html: TURBOREPO_LOGO,
|
||||||
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>
|
Create a monorepo
|
||||||
<FieldSeparator className="-mx-6" />
|
</FieldLabel>
|
||||||
<Field orientation="horizontal">
|
<Switch
|
||||||
<FieldLabel htmlFor="rtl">
|
id="monorepo"
|
||||||
<HugeiconsIcon icon={Globe02Icon} className="size-4" />
|
checked={params.template?.endsWith("-monorepo") ?? false}
|
||||||
Enable RTL support
|
disabled={!hasMonorepo}
|
||||||
</FieldLabel>
|
onCheckedChange={(checked) => {
|
||||||
<Switch
|
const framework = getFramework(params.template ?? "next")
|
||||||
id="rtl"
|
setParams({
|
||||||
checked={params.rtl}
|
template: getTemplateValue(
|
||||||
onCheckedChange={(checked) =>
|
framework,
|
||||||
setParams({ rtl: checked === true })
|
checked === true
|
||||||
}
|
) as typeof params.template,
|
||||||
/>
|
})
|
||||||
</Field>
|
}}
|
||||||
</FieldSet>
|
/>
|
||||||
</FieldGroup>
|
</Field>
|
||||||
</div>
|
<FieldSeparator />
|
||||||
<DialogFooter className="-mx-6 -mb-6 min-w-0">
|
<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>
|
||||||
|
<DialogFooter className="min-w-0">
|
||||||
<div className="flex w-full min-w-0 flex-col gap-3">
|
<div className="flex w-full min-w-0 flex-col gap-3">
|
||||||
<Tabs
|
<Tabs
|
||||||
value={packageManager}
|
value={packageManager}
|
||||||
@@ -209,16 +201,16 @@ export function ProjectForm({
|
|||||||
packageManager: value as PackageManager,
|
packageManager: value as PackageManager,
|
||||||
}))
|
}))
|
||||||
}}
|
}}
|
||||||
className="min-w-0 gap-0 overflow-hidden rounded-xl border-0 ring-1 ring-border"
|
className="min-w-0 gap-0 overflow-hidden rounded-lg border bg-surface"
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 py-1 pr-1.5 pl-1">
|
<div className="flex items-center gap-2 px-1 py-1">
|
||||||
<TabsList className="bg-transparent font-mono">
|
<TabsList className="font-mono">
|
||||||
{PACKAGE_MANAGERS.map((manager) => {
|
{PACKAGE_MANAGERS.map((manager) => {
|
||||||
return (
|
return (
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
key={manager}
|
key={manager}
|
||||||
value={manager}
|
value={manager}
|
||||||
className="py-0 leading-none data-[state=active]:shadow-none"
|
className="data-[state=active]:shadow-none"
|
||||||
>
|
>
|
||||||
{manager}
|
{manager}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
@@ -242,7 +234,7 @@ export function ProjectForm({
|
|||||||
{Object.entries(commands).map(([key, cmd]) => {
|
{Object.entries(commands).map(([key, cmd]) => {
|
||||||
return (
|
return (
|
||||||
<TabsContent key={key} value={key}>
|
<TabsContent key={key} value={key}>
|
||||||
<div className="relative overflow-hidden border-t bg-popover p-3">
|
<div className="relative overflow-hidden border-t border-border/50 bg-surface px-3 py-3 text-surface-foreground">
|
||||||
<div className="no-scrollbar overflow-x-auto">
|
<div className="no-scrollbar overflow-x-auto">
|
||||||
<code className="font-mono text-sm whitespace-nowrap">
|
<code className="font-mono text-sm whitespace-nowrap">
|
||||||
{cmd}
|
{cmd}
|
||||||
@@ -289,26 +281,23 @@ const TemplateGrid = React.memo(function TemplateGrid({
|
|||||||
<RadioGroup
|
<RadioGroup
|
||||||
value={framework}
|
value={framework}
|
||||||
onValueChange={handleTemplateChange}
|
onValueChange={handleTemplateChange}
|
||||||
className="grid grid-cols-2 gap-2"
|
className="grid grid-cols-3 gap-2"
|
||||||
>
|
>
|
||||||
{TEMPLATES.map((item) => (
|
{TEMPLATES.map((item) => (
|
||||||
<FieldLabel
|
<FieldLabel
|
||||||
key={item.value}
|
key={item.value}
|
||||||
htmlFor={`template-${item.value}`}
|
htmlFor={`template-${item.value}`}
|
||||||
className="block w-full"
|
className="py-1"
|
||||||
>
|
>
|
||||||
<Field
|
<Field className="gap-0" orientation="horizontal">
|
||||||
orientation="horizontal"
|
<FieldContent className="flex flex-col items-center justify-center gap-2">
|
||||||
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
|
<div
|
||||||
className="size-4 text-neutral-100 [&_svg]:size-4 *:[svg]:text-neutral-100!"
|
className="size-6 text-foreground [&_svg]:size-6 *:[svg]:text-foreground!"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: item.logo,
|
__html: item.logo,
|
||||||
}}
|
}}
|
||||||
></div>
|
></div>
|
||||||
<FieldTitle>{item.title}</FieldTitle>
|
<FieldTitle className="text-xs">{item.title}</FieldTitle>
|
||||||
</FieldContent>
|
</FieldContent>
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
value={item.value}
|
value={item.value}
|
||||||
@@ -321,55 +310,3 @@ const TemplateGrid = React.memo(function TemplateGrid({
|
|||||||
</RadioGroup>
|
</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>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { RADII, type RadiusValue } from "@/registry/config"
|
import { RADII, type RadiusValue } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -12,8 +12,8 @@ import {
|
|||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
export function RadiusPicker({
|
export function RadiusPicker({
|
||||||
isMobile,
|
isMobile,
|
||||||
@@ -1,13 +1,12 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import Script from "next/script"
|
import Script from "next/script"
|
||||||
|
import { Button } from "@/examples/base/ui/button"
|
||||||
import { DiceFaces05Icon } from "@hugeicons/core-free-icons"
|
import { DiceFaces05Icon } from "@hugeicons/core-free-icons"
|
||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
import { HugeiconsIcon } from "@hugeicons/react"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { useRandom } from "@/app/(create)/hooks/use-random"
|
||||||
import { useRandom } from "@/app/(app)/create/hooks/use-random"
|
|
||||||
import { RESET_FORWARD_TYPE } from "@/app/(app)/create/hooks/use-reset"
|
|
||||||
|
|
||||||
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
|
export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
|
||||||
|
|
||||||
@@ -41,7 +40,7 @@ export function RandomizeScript() {
|
|||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `
|
__html: `
|
||||||
(function() {
|
(function() {
|
||||||
// Forward r key (shuffle) and Shift+R (reset).
|
// Forward R key
|
||||||
document.addEventListener('keydown', function(e) {
|
document.addEventListener('keydown', function(e) {
|
||||||
if ((e.key === 'r' || e.key === 'R') && !e.metaKey && !e.ctrlKey) {
|
if ((e.key === 'r' || e.key === 'R') && !e.metaKey && !e.ctrlKey) {
|
||||||
if (
|
if (
|
||||||
@@ -54,11 +53,8 @@ export function RandomizeScript() {
|
|||||||
}
|
}
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (window.parent && window.parent !== window) {
|
if (window.parent && window.parent !== window) {
|
||||||
var type = e.shiftKey
|
|
||||||
? '${RESET_FORWARD_TYPE}'
|
|
||||||
: '${RANDOMIZE_FORWARD_TYPE}';
|
|
||||||
window.parent.postMessage({
|
window.parent.postMessage({
|
||||||
type: type,
|
type: '${RANDOMIZE_FORWARD_TYPE}',
|
||||||
key: e.key
|
key: e.key
|
||||||
}, '*');
|
}, '*');
|
||||||
}
|
}
|
||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
AlertDialogFooter,
|
AlertDialogFooter,
|
||||||
AlertDialogHeader,
|
AlertDialogHeader,
|
||||||
AlertDialogTitle,
|
AlertDialogTitle,
|
||||||
} from "@/styles/base-nova/ui/alert-dialog"
|
} from "@/examples/base/ui/alert-dialog"
|
||||||
import { useReset } from "@/app/(app)/create/hooks/use-reset"
|
|
||||||
|
import { useReset } from "@/app/(create)/hooks/use-reset"
|
||||||
|
|
||||||
export function ResetDialog() {
|
export function ResetDialog() {
|
||||||
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()
|
const { showResetDialog, setShowResetDialog, confirmReset } = useReset()
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Button } from "@/examples/base/ui/button"
|
||||||
import { Share03Icon, Tick02Icon } from "@hugeicons/core-free-icons"
|
import { Share03Icon, Tick02Icon } from "@hugeicons/core-free-icons"
|
||||||
import { HugeiconsIcon } from "@hugeicons/react"
|
import { HugeiconsIcon } from "@hugeicons/react"
|
||||||
|
|
||||||
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
import { copyToClipboardWithMeta } from "@/components/copy-button"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
|
||||||
import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
|
||||||
|
|
||||||
export function ShareButton() {
|
export function ShareButton() {
|
||||||
const [params] = useDesignSystemSearchParams()
|
const [params] = useDesignSystemSearchParams()
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import { type Style, type StyleName } from "@/registry/config"
|
import { type Style, type StyleName } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
PickerRadioGroup,
|
PickerRadioGroup,
|
||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
export function StylePicker({
|
export function StylePicker({
|
||||||
styles,
|
styles,
|
||||||
@@ -4,7 +4,7 @@ import * as React from "react"
|
|||||||
|
|
||||||
import { useMounted } from "@/hooks/use-mounted"
|
import { useMounted } from "@/hooks/use-mounted"
|
||||||
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
|
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
|
||||||
import { LockButton } from "@/app/(app)/create/components/lock-button"
|
import { LockButton } from "@/app/(create)/components/lock-button"
|
||||||
import {
|
import {
|
||||||
Picker,
|
Picker,
|
||||||
PickerContent,
|
PickerContent,
|
||||||
@@ -13,8 +13,8 @@ import {
|
|||||||
PickerRadioItem,
|
PickerRadioItem,
|
||||||
PickerSeparator,
|
PickerSeparator,
|
||||||
PickerTrigger,
|
PickerTrigger,
|
||||||
} from "@/app/(app)/create/components/picker"
|
} from "@/app/(create)/components/picker"
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
export function ThemePicker({
|
export function ThemePicker({
|
||||||
themes,
|
themes,
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Button } from "@/examples/base/ui/button"
|
||||||
|
import { Skeleton } from "@/examples/base/ui/skeleton"
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
import { useIsMobile } from "@/hooks/use-mobile"
|
import { useIsMobile } from "@/hooks/use-mobile"
|
||||||
import { useMounted } from "@/hooks/use-mounted"
|
import { useMounted } from "@/hooks/use-mounted"
|
||||||
import { Icons } from "@/components/icons"
|
import { Icons } from "@/components/icons"
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
import { Skeleton } from "@/styles/base-nova/ui/skeleton"
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
|
||||||
|
|
||||||
export function V0Button({ className }: { className?: string }) {
|
export function V0Button({ className }: { className?: string }) {
|
||||||
const [params] = useDesignSystemSearchParams()
|
const [params] = useDesignSystemSearchParams()
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
import { Button } from "@/examples/base/ui/button"
|
||||||
import { Icons } from "@/components/icons"
|
|
||||||
import { Button } from "@/styles/base-nova/ui/button"
|
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@@ -12,7 +10,9 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@/styles/base-nova/ui/dialog"
|
} from "@/examples/base/ui/dialog"
|
||||||
|
|
||||||
|
import { Icons } from "@/components/icons"
|
||||||
|
|
||||||
const STORAGE_KEY = "shadcn-create-welcome-dialog"
|
const STORAGE_KEY = "shadcn-create-welcome-dialog"
|
||||||
|
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Suspense } from "react"
|
import { Suspense } from "react"
|
||||||
|
|
||||||
import { HistoryProvider } from "@/app/(app)/create/hooks/use-history"
|
import { HistoryProvider } from "@/app/(create)/hooks/use-history"
|
||||||
import { LocksProvider } from "@/app/(app)/create/hooks/use-locks"
|
import { LocksProvider } from "@/app/(create)/hooks/use-locks"
|
||||||
|
|
||||||
export default function CreateLayout({
|
export default function CreateLayout({
|
||||||
children,
|
children,
|
||||||
@@ -2,11 +2,12 @@ import { type Metadata } from "next"
|
|||||||
|
|
||||||
import { siteConfig } from "@/lib/config"
|
import { siteConfig } from "@/lib/config"
|
||||||
import { absoluteUrl } from "@/lib/utils"
|
import { absoluteUrl } from "@/lib/utils"
|
||||||
import { Customizer } from "@/app/(app)/create/components/customizer"
|
import { SiteHeader } from "@/components/site-header"
|
||||||
import { PresetHandler } from "@/app/(app)/create/components/preset-handler"
|
import { Customizer } from "@/app/(create)/components/customizer"
|
||||||
import { Preview } from "@/app/(app)/create/components/preview"
|
import { PresetHandler } from "@/app/(create)/components/preset-handler"
|
||||||
import { WelcomeDialog } from "@/app/(app)/create/components/welcome-dialog"
|
import { Preview } from "@/app/(create)/components/preview"
|
||||||
import { getAllItems } from "@/app/(app)/create/lib/api"
|
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
|
||||||
|
import { getAllItems } from "@/app/(create)/lib/api"
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "New Project",
|
title: "New Project",
|
||||||
@@ -41,16 +42,20 @@ export default async function CreatePage() {
|
|||||||
const itemsByBase = await getAllItems()
|
const itemsByBase = await getAllItems()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 flex min-h-0 flex-1 flex-col overflow-hidden section-soft [--customizer-width:--spacing(48)] [--gap:--spacing(4)] md:[--gap:--spacing(6)] 2xl:[--customizer-width:--spacing(56)]">
|
<div
|
||||||
<div
|
data-slot="layout"
|
||||||
|
className="group/layout relative z-10 flex h-svh flex-col overflow-hidden section-soft [--customizer-width:--spacing(56)] [--gap:--spacing(4)] md:[--gap:--spacing(6)]"
|
||||||
|
>
|
||||||
|
<SiteHeader />
|
||||||
|
<main
|
||||||
data-slot="designer"
|
data-slot="designer"
|
||||||
className="flex min-h-0 flex-1 flex-col gap-(--gap) p-(--gap) pt-[calc(var(--gap)*0.25)] md:flex-row-reverse"
|
className="flex min-h-0 flex-1 flex-col gap-(--gap) p-(--gap) pt-[calc(var(--gap)*0.25)] md:flex-row-reverse"
|
||||||
>
|
>
|
||||||
<Preview />
|
<Preview />
|
||||||
<Customizer itemsByBase={itemsByBase} />
|
<Customizer itemsByBase={itemsByBase} />
|
||||||
</div>
|
<PresetHandler />
|
||||||
<PresetHandler />
|
<WelcomeDialog />
|
||||||
<WelcomeDialog />
|
</main>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -4,8 +4,8 @@ import * as React from "react"
|
|||||||
import { type RegistryItem } from "shadcn/schema"
|
import { type RegistryItem } from "shadcn/schema"
|
||||||
import useSWR from "swr"
|
import useSWR from "swr"
|
||||||
|
|
||||||
import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
import { groupItemsByType } from "@/app/(app)/create/lib/utils"
|
import { groupItemsByType } from "@/app/(create)/lib/utils"
|
||||||
|
|
||||||
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"
|
const ACTION_MENU_OPEN_KEY = "create:action-menu-open"
|
||||||
|
|
||||||
40
apps/v4/app/(create)/hooks/use-design-system.ts
Normal file
40
apps/v4/app/(create)/hooks/use-design-system.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import { encodePreset, isPresetCode } from "shadcn/preset"
|
||||||
|
|
||||||
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
|
// Returns the current preset code derived from search params.
|
||||||
|
export function usePresetCode() {
|
||||||
|
const [params] = useDesignSystemSearchParams()
|
||||||
|
|
||||||
|
return React.useMemo(() => {
|
||||||
|
// If preset is already in the URL, return it.
|
||||||
|
if (params.preset && isPresetCode(params.preset)) {
|
||||||
|
return params.preset
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise encode current params (e.g. on initial load before first interaction).
|
||||||
|
return encodePreset({
|
||||||
|
style: params.style ?? undefined,
|
||||||
|
baseColor: params.baseColor ?? undefined,
|
||||||
|
theme: params.theme ?? undefined,
|
||||||
|
iconLibrary: params.iconLibrary ?? undefined,
|
||||||
|
font: params.font ?? undefined,
|
||||||
|
radius: params.radius ?? undefined,
|
||||||
|
menuAccent: params.menuAccent ?? undefined,
|
||||||
|
menuColor: params.menuColor ?? undefined,
|
||||||
|
} as Parameters<typeof encodePreset>[0])
|
||||||
|
}, [
|
||||||
|
params.preset,
|
||||||
|
params.style,
|
||||||
|
params.baseColor,
|
||||||
|
params.theme,
|
||||||
|
params.iconLibrary,
|
||||||
|
params.font,
|
||||||
|
params.radius,
|
||||||
|
params.menuAccent,
|
||||||
|
params.menuColor,
|
||||||
|
])
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react"
|
||||||
|
|
||||||
import type { DesignSystemSearchParams } from "@/app/(app)/create/lib/search-params"
|
import type { DesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
type ParentToIframeMessage = {
|
type ParentToIframeMessage = {
|
||||||
type: "design-system-params"
|
type: "design-system-params"
|
||||||
@@ -6,10 +6,8 @@ export type LockableParam =
|
|||||||
| "style"
|
| "style"
|
||||||
| "baseColor"
|
| "baseColor"
|
||||||
| "theme"
|
| "theme"
|
||||||
| "chartColor"
|
|
||||||
| "iconLibrary"
|
| "iconLibrary"
|
||||||
| "font"
|
| "font"
|
||||||
| "fontHeading"
|
|
||||||
| "menuAccent"
|
| "menuAccent"
|
||||||
| "menuColor"
|
| "menuColor"
|
||||||
| "radius"
|
| "radius"
|
||||||
@@ -10,19 +10,18 @@ import {
|
|||||||
MENU_COLORS,
|
MENU_COLORS,
|
||||||
RADII,
|
RADII,
|
||||||
STYLES,
|
STYLES,
|
||||||
type FontHeadingValue,
|
|
||||||
} from "@/registry/config"
|
} from "@/registry/config"
|
||||||
import { useLocks } from "@/app/(app)/create/hooks/use-locks"
|
import { useLocks } from "@/app/(create)/hooks/use-locks"
|
||||||
import { FONTS } from "@/app/(app)/create/lib/fonts"
|
import { FONTS } from "@/app/(create)/lib/fonts"
|
||||||
import {
|
import {
|
||||||
applyBias,
|
applyBias,
|
||||||
RANDOMIZE_BIASES,
|
RANDOMIZE_BIASES,
|
||||||
type RandomizeContext,
|
type RandomizeContext,
|
||||||
} from "@/app/(app)/create/lib/randomize-biases"
|
} from "@/app/(create)/lib/randomize-biases"
|
||||||
import {
|
import {
|
||||||
isTranslucentMenuColor,
|
isTranslucentMenuColor,
|
||||||
useDesignSystemSearchParams,
|
useDesignSystemSearchParams,
|
||||||
} from "@/app/(app)/create/lib/search-params"
|
} from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
function randomItem<T>(array: readonly T[]): T {
|
function randomItem<T>(array: readonly T[]): T {
|
||||||
return array[Math.floor(Math.random() * array.length)]
|
return array[Math.floor(Math.random() * array.length)]
|
||||||
@@ -63,41 +62,9 @@ export function useRandom() {
|
|||||||
const selectedTheme = locks.has("theme")
|
const selectedTheme = locks.has("theme")
|
||||||
? paramsRef.current.theme
|
? paramsRef.current.theme
|
||||||
: randomItem(availableThemes).name
|
: randomItem(availableThemes).name
|
||||||
context.theme = selectedTheme
|
|
||||||
|
|
||||||
const availableChartColors = applyBias(
|
|
||||||
getThemesForBaseColor(baseColor),
|
|
||||||
context,
|
|
||||||
RANDOMIZE_BIASES.chartColors
|
|
||||||
)
|
|
||||||
const selectedChartColor = locks.has("chartColor")
|
|
||||||
? paramsRef.current.chartColor
|
|
||||||
: randomItem(availableChartColors).name
|
|
||||||
context.chartColor = selectedChartColor
|
|
||||||
const selectedFont = locks.has("font")
|
const selectedFont = locks.has("font")
|
||||||
? paramsRef.current.font
|
? paramsRef.current.font
|
||||||
: randomItem(availableFonts).value
|
: randomItem(availableFonts).value
|
||||||
context.font = selectedFont
|
|
||||||
|
|
||||||
// Pick heading font: ~70% inherit, ~30% distinct with cross-category contrast.
|
|
||||||
let selectedFontHeading: FontHeadingValue
|
|
||||||
if (locks.has("fontHeading")) {
|
|
||||||
selectedFontHeading = paramsRef.current.fontHeading
|
|
||||||
} else if (Math.random() < 0.7) {
|
|
||||||
selectedFontHeading = "inherit"
|
|
||||||
} else {
|
|
||||||
const bodyType = availableFonts.find(
|
|
||||||
(f) => f.value === selectedFont
|
|
||||||
)?.type
|
|
||||||
const contrastFonts = availableFonts.filter(
|
|
||||||
(f) => f.type !== bodyType && f.value !== selectedFont
|
|
||||||
)
|
|
||||||
selectedFontHeading = (
|
|
||||||
contrastFonts.length > 0
|
|
||||||
? randomItem(contrastFonts)
|
|
||||||
: randomItem(availableFonts)
|
|
||||||
).value as FontHeadingValue
|
|
||||||
}
|
|
||||||
const selectedRadius = locks.has("radius")
|
const selectedRadius = locks.has("radius")
|
||||||
? paramsRef.current.radius
|
? paramsRef.current.radius
|
||||||
: randomItem(availableRadii).name
|
: randomItem(availableRadii).name
|
||||||
@@ -124,16 +91,16 @@ export function useRandom() {
|
|||||||
: paramsRef.current.menuAccent
|
: paramsRef.current.menuAccent
|
||||||
: randomItem(MENU_ACCENTS).value
|
: randomItem(MENU_ACCENTS).value
|
||||||
|
|
||||||
|
context.theme = selectedTheme
|
||||||
|
context.font = selectedFont
|
||||||
context.radius = selectedRadius
|
context.radius = selectedRadius
|
||||||
|
|
||||||
const nextParams = {
|
const nextParams = {
|
||||||
style: selectedStyle,
|
style: selectedStyle,
|
||||||
baseColor,
|
baseColor,
|
||||||
theme: selectedTheme,
|
theme: selectedTheme,
|
||||||
chartColor: selectedChartColor,
|
|
||||||
iconLibrary: selectedIconLibrary,
|
iconLibrary: selectedIconLibrary,
|
||||||
font: selectedFont,
|
font: selectedFont,
|
||||||
fontHeading: selectedFontHeading,
|
|
||||||
menuAccent: selectedMenuAccent,
|
menuAccent: selectedMenuAccent,
|
||||||
menuColor: selectedMenuColor,
|
menuColor: selectedMenuColor,
|
||||||
radius: selectedRadius,
|
radius: selectedRadius,
|
||||||
@@ -156,7 +123,7 @@ export function useRandom() {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const down = (e: KeyboardEvent) => {
|
const down = (e: KeyboardEvent) => {
|
||||||
if (e.key === "r" && !e.shiftKey && !e.metaKey && !e.ctrlKey) {
|
if ((e.key === "r" || e.key === "R") && !e.metaKey && !e.ctrlKey) {
|
||||||
if (
|
if (
|
||||||
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
|
||||||
e.target instanceof HTMLInputElement ||
|
e.target instanceof HTMLInputElement ||
|
||||||
55
apps/v4/app/(create)/hooks/use-reset.tsx
Normal file
55
apps/v4/app/(create)/hooks/use-reset.tsx
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import useSWR from "swr"
|
||||||
|
|
||||||
|
import { DEFAULT_CONFIG } from "@/registry/config"
|
||||||
|
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
|
||||||
|
|
||||||
|
const RESET_DIALOG_KEY = "create:reset-dialog-open"
|
||||||
|
|
||||||
|
export function useReset() {
|
||||||
|
const [params, setParams] = useDesignSystemSearchParams()
|
||||||
|
const { data: showResetDialog = false, mutate: setShowResetDialogData } =
|
||||||
|
useSWR<boolean>(RESET_DIALOG_KEY, {
|
||||||
|
fallbackData: false,
|
||||||
|
revalidateOnFocus: false,
|
||||||
|
revalidateIfStale: false,
|
||||||
|
revalidateOnReconnect: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const reset = React.useCallback(() => {
|
||||||
|
setParams({
|
||||||
|
base: params.base,
|
||||||
|
style: DEFAULT_CONFIG.style,
|
||||||
|
baseColor: DEFAULT_CONFIG.baseColor,
|
||||||
|
theme: DEFAULT_CONFIG.theme,
|
||||||
|
iconLibrary: DEFAULT_CONFIG.iconLibrary,
|
||||||
|
font: DEFAULT_CONFIG.font,
|
||||||
|
menuAccent: DEFAULT_CONFIG.menuAccent,
|
||||||
|
menuColor: DEFAULT_CONFIG.menuColor,
|
||||||
|
radius: DEFAULT_CONFIG.radius,
|
||||||
|
template: DEFAULT_CONFIG.template,
|
||||||
|
item: "preview",
|
||||||
|
})
|
||||||
|
}, [setParams, params.base])
|
||||||
|
|
||||||
|
const handleShowResetDialogChange = React.useCallback(
|
||||||
|
(open: boolean) => {
|
||||||
|
void setShowResetDialogData(open, { revalidate: false })
|
||||||
|
},
|
||||||
|
[setShowResetDialogData]
|
||||||
|
)
|
||||||
|
|
||||||
|
const confirmReset = React.useCallback(() => {
|
||||||
|
reset()
|
||||||
|
void setShowResetDialogData(false, { revalidate: false })
|
||||||
|
}, [reset, setShowResetDialogData])
|
||||||
|
|
||||||
|
return {
|
||||||
|
reset,
|
||||||
|
showResetDialog,
|
||||||
|
setShowResetDialog: handleShowResetDialogChange,
|
||||||
|
confirmReset,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,17 +3,13 @@ import dedent from "dedent"
|
|||||||
import { UI_COMPONENTS } from "@/lib/components"
|
import { UI_COMPONENTS } from "@/lib/components"
|
||||||
import {
|
import {
|
||||||
buildRegistryBase,
|
buildRegistryBase,
|
||||||
getBodyFont,
|
fonts,
|
||||||
getHeadingFont,
|
|
||||||
getInheritedHeadingFontValue,
|
|
||||||
type DesignSystemConfig,
|
type DesignSystemConfig,
|
||||||
} from "@/registry/config"
|
} from "@/registry/config"
|
||||||
|
|
||||||
// Builds step-by-step markdown instructions for manually setting up a project.
|
// Builds step-by-step markdown instructions for manually setting up a project.
|
||||||
export function buildInstructions(config: DesignSystemConfig) {
|
export function buildInstructions(config: DesignSystemConfig) {
|
||||||
const registryBase = buildRegistryBase(config)
|
const registryBase = buildRegistryBase(config)
|
||||||
const normalizedFontHeading =
|
|
||||||
config.fontHeading === config.font ? "inherit" : config.fontHeading
|
|
||||||
|
|
||||||
const dependencies = [
|
const dependencies = [
|
||||||
...(registryBase.dependencies ?? []),
|
...(registryBase.dependencies ?? []),
|
||||||
@@ -29,23 +25,13 @@ export function buildInstructions(config: DesignSystemConfig) {
|
|||||||
.map(([key, value]) => ` --${key}: ${value};`)
|
.map(([key, value]) => ` --${key}: ${value};`)
|
||||||
.join("\n")
|
.join("\n")
|
||||||
|
|
||||||
const font = getBodyFont(config.font)
|
const font = fonts.find((f) => f.name === `font-${config.font}`)
|
||||||
const headingFont =
|
|
||||||
normalizedFontHeading === "inherit"
|
|
||||||
? undefined
|
|
||||||
: getHeadingFont(normalizedFontHeading)
|
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
buildDependenciesSection(dependencies),
|
buildDependenciesSection(dependencies),
|
||||||
buildUtilsSection(),
|
buildUtilsSection(),
|
||||||
buildCssSection(
|
buildCssSection(lightVars, darkVars),
|
||||||
lightVars,
|
buildFontSection(font),
|
||||||
darkVars,
|
|
||||||
normalizedFontHeading === "inherit"
|
|
||||||
? getInheritedHeadingFontValue(config.font)
|
|
||||||
: "var(--font-heading)"
|
|
||||||
),
|
|
||||||
buildFontSection(font, headingFont),
|
|
||||||
buildComponentsJsonSection(config),
|
buildComponentsJsonSection(config),
|
||||||
buildAvailableComponentsSection(config),
|
buildAvailableComponentsSection(config),
|
||||||
config.rtl ? buildRtlSection(config) : null,
|
config.rtl ? buildRtlSection(config) : null,
|
||||||
@@ -81,11 +67,7 @@ function buildUtilsSection() {
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildCssSection(
|
function buildCssSection(lightVars: string, darkVars: string) {
|
||||||
lightVars: string,
|
|
||||||
darkVars: string,
|
|
||||||
fontHeadingValue: string
|
|
||||||
) {
|
|
||||||
return dedent`
|
return dedent`
|
||||||
## Step 3: Set up CSS
|
## Step 3: Set up CSS
|
||||||
|
|
||||||
@@ -98,7 +80,6 @@ function buildCssSection(
|
|||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-sans: var(--font-sans);
|
--font-sans: var(--font-sans);
|
||||||
--font-heading: ${fontHeadingValue};
|
|
||||||
--font-mono: var(--font-mono);
|
--font-mono: var(--font-mono);
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
@@ -161,74 +142,40 @@ function buildCssSection(
|
|||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildFontSection(
|
function buildFontSection(font: (typeof fonts)[number] | undefined) {
|
||||||
font: ReturnType<typeof getBodyFont>,
|
|
||||||
headingFont: ReturnType<typeof getHeadingFont>
|
|
||||||
) {
|
|
||||||
if (!font) {
|
if (!font) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
const googleFontsUrl = `https://fonts.google.com/specimen/${font.font.import.replace(/_/g, "+")}`
|
const googleFontsUrl = `https://fonts.google.com/specimen/${font.font.import.replace(/_/g, "+")}`
|
||||||
const headingGoogleFontsUrl = headingFont
|
|
||||||
? `https://fonts.google.com/specimen/${headingFont.font.import.replace(/_/g, "+")}`
|
|
||||||
: null
|
|
||||||
const nextImports = headingFont
|
|
||||||
? `${font.font.import}, ${headingFont.font.import}`
|
|
||||||
: font.font.import
|
|
||||||
const nextDeclarations = [
|
|
||||||
`const fontSans = ${font.font.import}({`,
|
|
||||||
` subsets: ["latin"],`,
|
|
||||||
` variable: "${font.font.variable}",`,
|
|
||||||
`})`,
|
|
||||||
headingFont ? `const fontHeading = ${headingFont.font.import}({` : null,
|
|
||||||
headingFont ? ` subsets: ["latin"],` : null,
|
|
||||||
headingFont ? ` variable: "${headingFont.font.variable}",` : null,
|
|
||||||
headingFont ? `})` : null,
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join("\n")
|
|
||||||
const nextHtmlClassName = headingFont
|
|
||||||
? 'className={fontSans.variable + " " + fontHeading.variable}'
|
|
||||||
: `className={fontSans.variable}`
|
|
||||||
const otherFrameworkCss = [
|
|
||||||
":root {",
|
|
||||||
` ${font.font.variable}: ${font.font.family};`,
|
|
||||||
...(headingFont
|
|
||||||
? [` ${headingFont.font.variable}: ${headingFont.font.family};`]
|
|
||||||
: []),
|
|
||||||
"}",
|
|
||||||
].join("\n")
|
|
||||||
const headingSection = headingFont
|
|
||||||
? dedent`
|
|
||||||
|
|
||||||
This config also uses **${headingFont.title.replace(" (Heading)", "")}** for headings (\`${headingFont.font.variable}\`).
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
|
|
||||||
return dedent`
|
return dedent`
|
||||||
## Step 4: Set up the font
|
## Step 4: Set up the font
|
||||||
|
|
||||||
This config uses **${font.title}** (\`${font.font.variable}\`).
|
This config uses **${font.title}** (\`${font.font.variable}\`).
|
||||||
${headingSection}
|
|
||||||
|
|
||||||
### Next.js
|
### Next.js
|
||||||
|
|
||||||
\`\`\`tsx
|
\`\`\`tsx
|
||||||
import { ${nextImports} } from "next/font/google"
|
import { ${font.font.import} } from "next/font/google"
|
||||||
|
|
||||||
${nextDeclarations}
|
const fontSans = ${font.font.import}({
|
||||||
|
subsets: ["latin"],
|
||||||
|
variable: "${font.font.variable}",
|
||||||
|
})
|
||||||
|
|
||||||
// Add the font variable classes to your <html> className.
|
// Add fontSans.variable to your <html> className.
|
||||||
// <html ${nextHtmlClassName}>
|
// <html className={fontSans.variable}>
|
||||||
\`\`\`
|
\`\`\`
|
||||||
|
|
||||||
### Other frameworks
|
### Other frameworks
|
||||||
|
|
||||||
Add the font${headingFont ? "s" : ""} from [Google Fonts](${googleFontsUrl})${headingGoogleFontsUrl ? ` and [Google Fonts](${headingGoogleFontsUrl})` : ""} and set the CSS variable${headingFont ? "s" : ""} to the font famil${headingFont ? "ies" : "y"}:
|
Add the font from [Google Fonts](${googleFontsUrl}) and set the \`${font.font.variable}\` CSS variable to the font family:
|
||||||
|
|
||||||
\`\`\`css
|
\`\`\`css
|
||||||
${otherFrameworkCss}
|
:root {
|
||||||
|
${font.font.variable}: ${font.font.family};
|
||||||
|
}
|
||||||
\`\`\`
|
\`\`\`
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { describe, expect, it } from "vitest"
|
|
||||||
|
|
||||||
import { parseDesignSystemConfig } from "./parse-config"
|
|
||||||
|
|
||||||
describe("parseDesignSystemConfig", () => {
|
|
||||||
it("honors explicit fontHeading and chartColor overrides when a preset is present", () => {
|
|
||||||
const result = parseDesignSystemConfig(
|
|
||||||
new URLSearchParams(
|
|
||||||
"preset=a0&fontHeading=playfair-display&chartColor=emerald"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
expect(result.success).toBe(true)
|
|
||||||
if (!result.success) {
|
|
||||||
throw new Error(result.error)
|
|
||||||
}
|
|
||||||
|
|
||||||
expect(result.data.fontHeading).toBe("playfair-display")
|
|
||||||
expect(result.data.chartColor).toBe("emerald")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
@@ -4,7 +4,6 @@ import {
|
|||||||
designSystemConfigSchema,
|
designSystemConfigSchema,
|
||||||
type DesignSystemConfig,
|
type DesignSystemConfig,
|
||||||
} from "@/registry/config"
|
} from "@/registry/config"
|
||||||
import { resolvePresetOverrides } from "@/app/(app)/create/lib/preset-query"
|
|
||||||
|
|
||||||
// Parses design system config from URL search params.
|
// Parses design system config from URL search params.
|
||||||
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
||||||
@@ -16,10 +15,8 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
|||||||
if (!decoded) {
|
if (!decoded) {
|
||||||
return { success: false as const, error: "Invalid preset code" }
|
return { success: false as const, error: "Invalid preset code" }
|
||||||
}
|
}
|
||||||
const presetOverrides = resolvePresetOverrides(searchParams, decoded)
|
|
||||||
configInput = {
|
configInput = {
|
||||||
...decoded,
|
...decoded,
|
||||||
...presetOverrides,
|
|
||||||
base: searchParams.get("base") ?? "radix",
|
base: searchParams.get("base") ?? "radix",
|
||||||
template: searchParams.get("template") ?? undefined,
|
template: searchParams.get("template") ?? undefined,
|
||||||
rtl: searchParams.get("rtl") === "true",
|
rtl: searchParams.get("rtl") === "true",
|
||||||
@@ -31,9 +28,7 @@ export function parseDesignSystemConfig(searchParams: URLSearchParams) {
|
|||||||
iconLibrary: searchParams.get("iconLibrary"),
|
iconLibrary: searchParams.get("iconLibrary"),
|
||||||
baseColor: searchParams.get("baseColor"),
|
baseColor: searchParams.get("baseColor"),
|
||||||
theme: searchParams.get("theme"),
|
theme: searchParams.get("theme"),
|
||||||
chartColor: searchParams.get("chartColor") ?? undefined,
|
|
||||||
font: searchParams.get("font"),
|
font: searchParams.get("font"),
|
||||||
fontHeading: searchParams.get("fontHeading") ?? undefined,
|
|
||||||
menuAccent: searchParams.get("menuAccent"),
|
menuAccent: searchParams.get("menuAccent"),
|
||||||
menuColor: searchParams.get("menuColor"),
|
menuColor: searchParams.get("menuColor"),
|
||||||
radius: searchParams.get("radius"),
|
radius: searchParams.get("radius"),
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { NextResponse, type NextRequest } from "next/server"
|
import { NextResponse, type NextRequest } from "next/server"
|
||||||
import { track } from "@vercel/analytics/server"
|
import { track } from "@vercel/analytics/server"
|
||||||
import { isPresetCode } from "shadcn/preset"
|
import { encodePreset, isPresetCode } from "shadcn/preset"
|
||||||
import { registryItemSchema } from "shadcn/schema"
|
import { registryItemSchema } from "shadcn/schema"
|
||||||
|
|
||||||
import { buildRegistryBase } from "@/registry/config"
|
import { buildRegistryBase } from "@/registry/config"
|
||||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
|
||||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
@@ -16,11 +15,21 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPreset = searchParams.get("preset")
|
// Use the preset code from the URL if provided, otherwise encode one.
|
||||||
|
const presetParam = searchParams.get("preset")
|
||||||
const presetCode =
|
const presetCode =
|
||||||
rawPreset && isPresetCode(rawPreset)
|
presetParam && isPresetCode(presetParam)
|
||||||
? rawPreset
|
? presetParam
|
||||||
: getPresetCode(result.data)
|
: encodePreset({
|
||||||
|
style: result.data.style,
|
||||||
|
baseColor: result.data.baseColor,
|
||||||
|
theme: result.data.theme,
|
||||||
|
iconLibrary: result.data.iconLibrary,
|
||||||
|
font: result.data.font,
|
||||||
|
radius: result.data.radius,
|
||||||
|
menuAccent: result.data.menuAccent,
|
||||||
|
menuColor: result.data.menuColor,
|
||||||
|
} as Parameters<typeof encodePreset>[0])
|
||||||
|
|
||||||
const registryBase = buildRegistryBase(result.data)
|
const registryBase = buildRegistryBase(result.data)
|
||||||
const parseResult = registryItemSchema.safeParse(registryBase)
|
const parseResult = registryItemSchema.safeParse(registryBase)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
import { after, NextResponse, type NextRequest } from "next/server"
|
import { after, NextResponse, type NextRequest } from "next/server"
|
||||||
import { track } from "@vercel/analytics/server"
|
import { track } from "@vercel/analytics/server"
|
||||||
import { isPresetCode } from "shadcn/preset"
|
|
||||||
|
|
||||||
import { getPresetCode } from "@/app/(app)/create/lib/preset-code"
|
|
||||||
import { buildV0Payload } from "@/app/(app)/create/lib/v0"
|
|
||||||
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
import { parseDesignSystemConfig } from "@/app/(create)/init/parse-config"
|
||||||
|
import { buildV0Payload } from "@/app/(create)/lib/v0"
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
try {
|
try {
|
||||||
@@ -15,17 +13,11 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: result.error }, { status: 400 })
|
return NextResponse.json({ error: result.error }, { status: 400 })
|
||||||
}
|
}
|
||||||
|
|
||||||
const rawPreset = searchParams.get("preset")
|
|
||||||
const presetCode =
|
|
||||||
rawPreset && isPresetCode(rawPreset)
|
|
||||||
? rawPreset
|
|
||||||
: getPresetCode(result.data)
|
|
||||||
|
|
||||||
// Defer analytics to after response is sent.
|
// Defer analytics to after response is sent.
|
||||||
after(() => {
|
after(() => {
|
||||||
track("create_open_in_v0", {
|
track("create_open_in_v0", {
|
||||||
...result.data,
|
...result.data,
|
||||||
preset: presetCode,
|
preset: searchParams.get("preset") ?? "",
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user