From 0b34d581f9acbb60e030bb1645e82a7f1bed9ade Mon Sep 17 00:00:00 2001 From: shadcn Date: Wed, 8 Apr 2026 16:33:32 +0400 Subject: [PATCH] feat: add open preset --- .../(app)/create/components/copy-preset.tsx | 4 +- .../(app)/create/components/customizer.tsx | 10 +- .../app/(app)/create/components/main-menu.tsx | 5 + .../(app)/create/components/open-preset.tsx | 200 ++++++++++++++++++ .../app/(app)/create/components/preview.tsx | 156 +++++++------- .../(app)/create/components/random-button.tsx | 2 +- .../(app)/create/hooks/use-open-preset.tsx | 81 +++++++ .../create/lib/parse-preset-input.test.ts | 17 ++ .../(app)/create/lib/parse-preset-input.ts | 15 ++ .../(create)/preview/[base]/[name]/page.tsx | 2 + 10 files changed, 415 insertions(+), 77 deletions(-) create mode 100644 apps/v4/app/(app)/create/components/open-preset.tsx create mode 100644 apps/v4/app/(app)/create/hooks/use-open-preset.tsx create mode 100644 apps/v4/app/(app)/create/lib/parse-preset-input.test.ts create mode 100644 apps/v4/app/(app)/create/lib/parse-preset-input.ts diff --git a/apps/v4/app/(app)/create/components/copy-preset.tsx b/apps/v4/app/(app)/create/components/copy-preset.tsx index 55024fbe4c..32aea91d81 100644 --- a/apps/v4/app/(app)/create/components/copy-preset.tsx +++ b/apps/v4/app/(app)/create/components/copy-preset.tsx @@ -10,6 +10,7 @@ import { usePresetCode } from "@/app/(app)/create/hooks/use-design-system" export function CopyPreset({ className }: React.ComponentProps) { const presetCode = usePresetCode() const [hasCopied, setHasCopied] = React.useState(false) + const label = hasCopied ? "Copied" : `--preset ${presetCode}` React.useEffect(() => { if (hasCopied) { @@ -32,12 +33,13 @@ export function CopyPreset({ className }: React.ComponentProps) { ) } diff --git a/apps/v4/app/(app)/create/components/customizer.tsx b/apps/v4/app/(app)/create/components/customizer.tsx index 91ba9db7b6..934ccf8039 100644 --- a/apps/v4/app/(app)/create/components/customizer.tsx +++ b/apps/v4/app/(app)/create/components/customizer.tsx @@ -23,12 +23,12 @@ 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 { OpenPreset } from "@/app/(app)/create/components/open-preset" 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" @@ -102,8 +102,12 @@ export function Customizer({ - - + + + diff --git a/apps/v4/app/(app)/create/components/main-menu.tsx b/apps/v4/app/(app)/create/components/main-menu.tsx index 75c3839358..b16b6a9135 100644 --- a/apps/v4/app/(app)/create/components/main-menu.tsx +++ b/apps/v4/app/(app)/create/components/main-menu.tsx @@ -17,6 +17,7 @@ import { } from "@/app/(app)/create/components/picker" import { useActionMenuTrigger } from "@/app/(app)/create/hooks/use-action-menu" import { useHistory } from "@/app/(app)/create/hooks/use-history" +import { useOpenPresetTrigger } from "@/app/(app)/create/hooks/use-open-preset" import { useRandom } from "@/app/(app)/create/hooks/use-random" import { useReset } from "@/app/(app)/create/hooks/use-reset" import { useThemeToggle } from "@/app/(app)/create/hooks/use-theme-toggle" @@ -27,6 +28,7 @@ export function MainMenu({ className }: React.ComponentProps) { const [isMac, setIsMac] = React.useState(false) const { canGoBack, canGoForward, goBack, goForward } = useHistory() const { openActionMenu } = useActionMenuTrigger() + const { openPreset } = useOpenPresetTrigger() const { randomize } = useRandom() const { toggleTheme } = useThemeToggle() const { setShowResetDialog } = useReset() @@ -55,6 +57,9 @@ export function MainMenu({ className }: React.ComponentProps) { Navigate... {isMac ? "⌘P" : "Ctrl+P"} + + Open Preset... O + Shuffle R diff --git a/apps/v4/app/(app)/create/components/open-preset.tsx b/apps/v4/app/(app)/create/components/open-preset.tsx new file mode 100644 index 0000000000..11767a516d --- /dev/null +++ b/apps/v4/app/(app)/create/components/open-preset.tsx @@ -0,0 +1,200 @@ +"use client" + +import * as React from "react" +import Script from "next/script" + +import { cn } from "@/lib/utils" +import { useIsMobile } from "@/hooks/use-mobile" +import { Button } from "@/styles/base-nova/ui/button" +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/styles/base-nova/ui/dialog" +import { + Drawer, + DrawerClose, + DrawerContent, + DrawerDescription, + DrawerFooter, + DrawerHeader, + DrawerTitle, + DrawerTrigger, +} from "@/styles/base-nova/ui/drawer" +import { Field, FieldContent, FieldLabel } from "@/styles/base-nova/ui/field" +import { Input } from "@/styles/base-nova/ui/input" +import { + OPEN_PRESET_FORWARD_TYPE, + useOpenPreset, +} from "@/app/(app)/create/hooks/use-open-preset" +import { parsePresetInput } from "@/app/(app)/create/lib/parse-preset-input" +import { useDesignSystemSearchParams } from "@/app/(app)/create/lib/search-params" + +const PRESET_EXAMPLE = "b2D0wqNxT" +const PRESET_TITLE = "Open Preset" +const PRESET_DESCRIPTION = "Paste a preset code to load a saved configuration." + +export function OpenPreset({ + className, + label = "Open Preset", +}: React.ComponentProps & { + label?: string +}) { + const [input, setInput] = React.useState("") + const [, setParams] = useDesignSystemSearchParams() + const isMobile = useIsMobile() + const { open, setOpen } = useOpenPreset() + + const nextPreset = React.useMemo(() => parsePresetInput(input), [input]) + const isInvalid = input.trim().length > 0 && nextPreset === null + + const handleOpenChange = React.useCallback( + (nextOpen: boolean) => { + setOpen(nextOpen) + + if (!nextOpen) { + setInput("") + } + }, + [setOpen] + ) + + const handleSubmit = React.useCallback( + (event: React.FormEvent) => { + event.preventDefault() + + if (!nextPreset) { + return + } + + setParams({ preset: nextPreset }) + handleOpenChange(false) + }, + [handleOpenChange, nextPreset, setParams] + ) + + const triggerClassName = cn( + "touch-manipulation bg-transparent! px-2! py-0! text-sm! transition-none select-none hover:bg-muted! pointer-coarse:h-10!", + className + ) + + const desktopTrigger = ( + + + + + {PRESET_TITLE} + {PRESET_DESCRIPTION} + +
+
{fields}
+ + + + + + +
+
+ + ) + } + + return ( + + {label} + +
+ + {PRESET_TITLE} + {PRESET_DESCRIPTION} + +
{fields}
+ + }> + Cancel + + + +
+
+
+ ) +} + +export function OpenPresetScript() { + return ( +