Revert "feat: update customizer (#10129)" (#10130)

This reverts commit e327cef2c1.
This commit is contained in:
shadcn
2026-03-20 16:56:57 +04:00
committed by GitHub
parent e327cef2c1
commit 5b40b9de5a
21 changed files with 265 additions and 922 deletions

View File

@@ -1,7 +1,6 @@
"use client"
import { MENU_ACCENTS, type MenuAccentValue } from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -9,21 +8,18 @@ import {
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function MenuAccentPicker({
isMobile,
anchorRef,
collapsed = false,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const currentAccent = MENU_ACCENTS.find(
(accent) => accent.value === params.menuAccent
@@ -32,11 +28,14 @@ export function MenuAccentPicker({
return (
<div className="group/picker relative pr-3 md:pr-0">
<Picker>
<PickerValueTrigger
label="Menu Accent"
value={currentAccent?.label}
valueText={currentAccent?.label}
indicator={
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Menu Accent</div>
<div className="text-sm font-medium text-foreground">
{currentAccent?.label}
</div>
</div>
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
<svg
xmlns="http://www.w3.org/2000/svg"
width="128"
@@ -64,12 +63,11 @@ export function MenuAccentPicker({
className="fill-muted-foreground/30 data-[accent=bold]:fill-foreground"
></path>
</svg>
}
collapsed={collapsed}
/>
</div>
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
@@ -97,12 +95,10 @@ export function MenuAccentPicker({
</PickerRadioGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param="menuAccent"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param="menuAccent"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -4,7 +4,6 @@ import * as React from "react"
import { useMounted } from "@/hooks/use-mounted"
import { BASE_COLORS, type BaseColorName } from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -12,51 +11,50 @@ import {
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function BaseColorPicker({
isMobile,
anchorRef,
collapsed = false,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const mounted = useMounted()
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const currentBaseColor = React.useMemo(
() => BASE_COLORS.find((baseColor) => baseColor.name === params.baseColor),
[params.baseColor]
)
const baseColorIndicator = mounted ? (
<div
style={
{
"--color": currentBaseColor?.cssVars?.dark?.["muted-foreground"],
} as React.CSSProperties
}
className="size-4 rounded-full bg-(--color)"
/>
) : null
return (
<div className="group/picker relative">
<Picker>
<PickerValueTrigger
label="Base Color"
value={currentBaseColor?.title}
valueText={currentBaseColor?.title}
indicator={baseColorIndicator}
collapsed={collapsed}
/>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Base Color</div>
<div className="text-sm font-medium text-foreground">
{currentBaseColor?.title}
</div>
</div>
{mounted && (
<div
style={
{
"--color":
currentBaseColor?.cssVars?.dark?.["muted-foreground"],
} as React.CSSProperties
}
className="pointer-events-none absolute top-1/2 right-4 size-4 -translate-y-1/2 rounded-full bg-(--color) select-none md:right-2.5"
/>
)}
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
@@ -79,12 +77,10 @@ export function BaseColorPicker({
</PickerRadioGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param="baseColor"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param="baseColor"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -3,7 +3,6 @@
import * as React from "react"
import { BASES } from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import {
Picker,
PickerContent,
@@ -22,7 +21,6 @@ export function BasePicker({
anchorRef: React.RefObject<HTMLDivElement | null>
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const currentBase = React.useMemo(
() => BASES.find((base) => base.name === params.base),
@@ -62,7 +60,7 @@ export function BasePicker({
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup

View File

@@ -8,7 +8,6 @@ import {
getThemesForBaseColor,
type ChartColorName,
} from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -17,22 +16,19 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function ChartColorPicker({
isMobile,
anchorRef,
collapsed = false,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const mounted = useMounted()
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const availableChartColors = React.useMemo(
() => getThemesForBaseColor(params.baseColor),
@@ -56,33 +52,35 @@ export function ChartColorPicker({
}
}, [currentChartColor, availableChartColors, setParams])
const chartColorIndicator = mounted ? (
<div
style={
{
"--color":
currentChartColor?.cssVars?.dark?.[
currentChartColorIsBaseColor ? "muted-foreground" : "primary"
],
} as React.CSSProperties
}
className="size-4 rounded-full bg-(--color)"
/>
) : null
return (
<div className="group/picker relative">
<Picker>
<PickerValueTrigger
label="Chart Color"
value={currentChartColor?.title}
valueText={currentChartColor?.title}
indicator={chartColorIndicator}
collapsed={collapsed}
/>
<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" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-92"
>
@@ -129,12 +127,10 @@ export function ChartColorPicker({
</PickerRadioGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param="chartColor"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param="chartColor"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -2,24 +2,12 @@
import * as React from "react"
import { Button } from "@/examples/base/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui/tooltip"
import { Copy01Icon, Tick02Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
import { cn } from "@/lib/utils"
import { copyToClipboardWithMeta } from "@/components/copy-button"
import { usePresetCode } from "@/app/(create)/hooks/use-design-system"
export function CopyPreset({
className,
collapsed = false,
}: React.ComponentProps<typeof Button> & {
collapsed?: boolean
}) {
export function CopyPreset({ className }: React.ComponentProps<typeof Button>) {
const presetCode = usePresetCode()
const [hasCopied, setHasCopied] = React.useState(false)
@@ -40,40 +28,6 @@ export function CopyPreset({
setHasCopied(true)
}, [presetCode])
const tooltipLabel = hasCopied
? "Copied preset"
: `Copy --preset ${presetCode}`
if (collapsed) {
return (
<Tooltip>
<TooltipTrigger
render={
<Button
variant="outline"
size="icon"
aria-label={tooltipLabel}
onClick={handleCopy}
className={cn(
"size-10 touch-manipulation rounded-xl bg-transparent! transition-none select-none hover:bg-muted!",
className
)}
/>
}
>
<HugeiconsIcon
icon={hasCopied ? Tick02Icon : Copy01Icon}
strokeWidth={2}
/>
<span className="sr-only">{tooltipLabel}</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
{tooltipLabel}
</TooltipContent>
</Tooltip>
)
}
return (
<Button
variant="outline"

View File

@@ -1,50 +0,0 @@
"use client"
import * as React from "react"
import {
getCustomizerDesktopPickerSide,
type CustomizerDesktopPickerSide,
type CustomizerPosition,
} from "@/app/(create)/lib/customizer"
type CustomizerLayoutContextValue = {
position: CustomizerPosition
desktopPickerSide: CustomizerDesktopPickerSide
}
const CustomizerLayoutContext =
React.createContext<CustomizerLayoutContextValue | null>(null)
export function CustomizerLayoutProvider({
position,
children,
}: React.PropsWithChildren<{
position: CustomizerPosition
}>) {
const value = React.useMemo(
() => ({
position,
desktopPickerSide: getCustomizerDesktopPickerSide(position),
}),
[position]
)
return (
<CustomizerLayoutContext.Provider value={value}>
{children}
</CustomizerLayoutContext.Provider>
)
}
export function useCustomizerLayout() {
const context = React.useContext(CustomizerLayoutContext)
if (!context) {
throw new Error(
"useCustomizerLayout must be used within a CustomizerLayoutProvider"
)
}
return context
}

View File

@@ -1,7 +1,6 @@
"use client"
import * as React from "react"
import Script from "next/script"
import {
Card,
CardContent,
@@ -11,7 +10,7 @@ import {
import { FieldGroup, FieldSeparator } from "@/examples/base/ui/field"
import { type RegistryItem } from "shadcn/schema"
import { cn } from "@/lib/utils"
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"
@@ -19,7 +18,6 @@ import { BaseColorPicker } from "@/app/(create)/components/base-color-picker"
import { BasePicker } from "@/app/(create)/components/base-picker"
import { ChartColorPicker } from "@/app/(create)/components/chart-color-picker"
import { CopyPreset } from "@/app/(create)/components/copy-preset"
import { CustomizerLayoutProvider } from "@/app/(create)/components/customizer-layout"
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"
@@ -29,293 +27,78 @@ 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 {
CUSTOMIZER_POSITION_COOKIE_NAME,
CUSTOMIZER_STATE_COOKIE_MAX_AGE,
CUSTOMIZER_STATE_COOKIE_NAME,
type CustomizerPosition,
} from "@/app/(create)/lib/customizer"
import { V0Button } from "@/app/(create)/components/v0-button"
import { FONT_HEADING_OPTIONS, FONTS } from "@/app/(create)/lib/fonts"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export const CUSTOMIZER_TOGGLE_FORWARD_TYPE = "customizer-toggle-forward"
export function Customizer({
itemsByBase,
defaultCollapsed = false,
defaultIsMobile = false,
defaultPosition = "left",
}: {
itemsByBase: Record<string, Pick<RegistryItem, "name" | "title" | "type">[]>
defaultCollapsed?: boolean
defaultIsMobile?: boolean
defaultPosition?: CustomizerPosition
}) {
const [params] = useDesignSystemSearchParams()
const [isMobile, setIsMobile] = React.useState(defaultIsMobile)
const isMobile = useIsMobile()
const anchorRef = React.useRef<HTMLDivElement | null>(null)
const [isCollapsed, setIsCollapsed] = React.useState(defaultCollapsed)
const [position, setPosition] =
React.useState<CustomizerPosition>(defaultPosition)
const isDesktopCollapsed = !isMobile && isCollapsed
const toggleCollapse = React.useCallback(() => {
setIsCollapsed((collapsed) => {
const nextCollapsed = !collapsed
document.cookie = `${CUSTOMIZER_STATE_COOKIE_NAME}=${nextCollapsed}; path=/; max-age=${CUSTOMIZER_STATE_COOKIE_MAX_AGE}`
return nextCollapsed
})
}, [])
const handlePositionChange = React.useCallback(
(nextPosition: CustomizerPosition) => {
setPosition(nextPosition)
},
[]
)
const availableThemes = React.useMemo(
() => getThemesForBaseColor(params.baseColor),
[params.baseColor]
)
React.useEffect(() => {
const mql = window.matchMedia("(max-width: 767px)")
const onChange = () => {
setIsMobile(window.innerWidth < 768)
}
mql.addEventListener("change", onChange)
onChange()
return () => {
mql.removeEventListener("change", onChange)
}
}, [])
React.useEffect(() => {
document.cookie = `${CUSTOMIZER_POSITION_COOKIE_NAME}=${position}; path=/; max-age=${CUSTOMIZER_STATE_COOKIE_MAX_AGE}`
const layout = document.querySelector<HTMLElement>('[data-slot="layout"]')
const designer = document.querySelector<HTMLElement>(
'[data-slot="designer"]'
)
if (layout) {
layout.dataset.customizerPosition = position
}
if (designer) {
designer.dataset.customizerPosition = position
}
}, [position])
React.useEffect(() => {
if (isMobile) {
return
}
const down = (e: KeyboardEvent) => {
if ((!e.metaKey && !e.ctrlKey) || e.shiftKey || e.altKey) {
return
}
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return
}
if (e.key.toLowerCase() !== "b") {
return
}
e.preventDefault()
toggleCollapse()
}
document.addEventListener("keydown", down)
return () => {
document.removeEventListener("keydown", down)
}
}, [isMobile, toggleCollapse])
return (
<CustomizerLayoutProvider position={position}>
<Card
ref={anchorRef}
className={cn(
"dark isolate z-10 max-h-full min-h-0 w-full self-start rounded-2xl bg-card/90 shadow-xl backdrop-blur-xl transition-[width] duration-300 ease-out md:w-(--customizer-width)",
isDesktopCollapsed && "md:w-16"
)}
size="sm"
>
<CardHeader
className={cn(
"hidden items-center gap-2 border-b transition-[padding,border-color] duration-200 ease-out group-data-[customizer-position=right]/layout:flex-row-reverse md:flex",
isDesktopCollapsed ? "justify-center" : "justify-between"
)}
>
<MainMenu
onToggleCollapse={toggleCollapse}
onPositionChange={handlePositionChange}
position={position}
collapsed={isDesktopCollapsed}
<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">
{isMobile && <BasePicker isMobile={isMobile} anchorRef={anchorRef} />}
<StylePicker
styles={STYLES}
isMobile={isMobile}
anchorRef={anchorRef}
/>
</CardHeader>
<CardContent className="no-scrollbar min-h-0 flex-1 overflow-x-auto overflow-y-hidden transition-[padding] duration-200 ease-out md:overflow-x-hidden md:overflow-y-auto">
<FieldGroup
className={cn(
"flex-row gap-2.5 py-px transition-[gap] duration-200 ease-out **:data-[slot=field-separator]:-mx-4 **:data-[slot=field-separator]:w-auto md:flex-col md:gap-3.25",
isDesktopCollapsed && "md:gap-2"
)}
>
{isMobile && (
<BasePicker isMobile={isMobile} anchorRef={anchorRef} />
)}
<StylePicker
styles={STYLES}
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<FieldSeparator
className={cn(
"hidden transition-opacity duration-150 ease-out md:block",
isDesktopCollapsed && "pointer-events-none opacity-0"
)}
/>
<BaseColorPicker
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<ThemePicker
themes={availableThemes}
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<ChartColorPicker
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<FieldSeparator
className={cn(
"hidden transition-opacity duration-150 ease-out md:block",
isDesktopCollapsed && "pointer-events-none opacity-0"
)}
/>
<FontPicker
label="Heading"
param="fontHeading"
fonts={FONT_HEADING_OPTIONS}
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<FontPicker
label="Font"
param="font"
fonts={FONTS}
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<FieldSeparator
className={cn(
"hidden transition-opacity duration-150 ease-out md:block",
isDesktopCollapsed && "pointer-events-none opacity-0"
)}
/>
<IconLibraryPicker
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<RadiusPicker
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<FieldSeparator
className={cn(
"hidden transition-opacity duration-150 ease-out md:block",
isDesktopCollapsed && "pointer-events-none opacity-0"
)}
/>
<MenuColorPicker
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
<MenuAccentPicker
isMobile={isMobile}
anchorRef={anchorRef}
collapsed={isDesktopCollapsed}
/>
</FieldGroup>
</CardContent>
<CardFooter
className={cn(
"flex min-w-0 gap-2 transition-[max-height,opacity,padding,border-color] duration-200 ease-out md:max-h-72 md:flex-col md:**:[button,a]:w-full",
isDesktopCollapsed && "justify-center md:max-h-28 md:px-2 md:py-2"
)}
>
<CopyPreset
className="flex-1 md:flex-none"
collapsed={isDesktopCollapsed}
<FieldSeparator className="hidden md:block" />
<BaseColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<ThemePicker
themes={availableThemes}
isMobile={isMobile}
anchorRef={anchorRef}
/>
<RandomButton
className="flex-1 md:flex-none"
collapsed={isDesktopCollapsed}
<ChartColorPicker isMobile={isMobile} anchorRef={anchorRef} />
<FieldSeparator className="hidden md:block" />
<FontPicker
label="Heading"
param="fontHeading"
fonts={FONT_HEADING_OPTIONS}
isMobile={isMobile}
anchorRef={anchorRef}
/>
{!isDesktopCollapsed ? (
<ActionMenu itemsByBase={itemsByBase} />
) : null}
<ResetDialog />
</CardFooter>
</Card>
</CustomizerLayoutProvider>
)
}
export function CustomizerToggleScript() {
return (
<Script
id="customizer-toggle-listener"
strategy="beforeInteractive"
dangerouslySetInnerHTML={{
__html: `
(function() {
document.addEventListener('keydown', function(e) {
if ((!e.metaKey && !e.ctrlKey) || e.shiftKey || e.altKey) {
return;
}
if (
(e.target instanceof HTMLElement && e.target.isContentEditable) ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLTextAreaElement ||
e.target instanceof HTMLSelectElement
) {
return;
}
if (e.key.toLowerCase() !== 'b') {
return;
}
e.preventDefault();
if (window.parent && window.parent !== window) {
window.parent.postMessage({
type: '${CUSTOMIZER_TOGGLE_FORWARD_TYPE}',
key: e.key
}, '*');
}
});
})();
`,
}}
/>
<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} />
</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>
)
}

View File

@@ -2,7 +2,6 @@
import * as React from "react"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -12,7 +11,7 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { FONTS } from "@/app/(create)/lib/fonts"
import {
@@ -37,17 +36,14 @@ export function FontPicker({
fonts,
isMobile,
anchorRef,
collapsed = false,
}: {
label: string
param: "font" | "fontHeading"
fonts: readonly FontPickerOption[]
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const currentValue = param === "font" ? params.font : params.fontHeading
const handleFontChange = React.useCallback(
(value: string) => {
@@ -98,26 +94,27 @@ export function FontPicker({
return (
<div className="group/picker relative">
<Picker>
<PickerValueTrigger
label={label}
value={displayFontName}
valueText={displayFontName}
indicator={
<span
style={{
fontFamily:
currentFont?.font?.style.fontFamily ??
currentBodyFont?.font.style.fontFamily,
}}
>
Aa
</span>
}
collapsed={collapsed}
/>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">{label}</div>
<div className="text-sm font-medium text-foreground">
{displayFontName}
</div>
</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" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-96"
>
@@ -152,12 +149,10 @@ export function FontPicker({
</PickerRadioGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param={param}
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param={param}
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -3,7 +3,6 @@
import * as React from "react"
import { iconLibraries, type IconLibraryName } from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -11,7 +10,7 @@ import {
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
@@ -113,14 +112,11 @@ const logos = {
export function IconLibraryPicker({
isMobile,
anchorRef,
collapsed = false,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const currentIconLibrary = React.useMemo(
() => iconLibraries[params.iconLibrary as keyof typeof iconLibraries],
@@ -130,16 +126,20 @@ export function IconLibraryPicker({
return (
<div className="group/picker relative">
<Picker>
<PickerValueTrigger
label="Icon Library"
value={currentIconLibrary?.title}
valueText={currentIconLibrary?.title}
indicator={logos[currentIconLibrary?.name as keyof typeof logos]}
collapsed={collapsed}
/>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Icon Library</div>
<div className="text-sm font-medium text-foreground">
{currentIconLibrary?.title}
</div>
</div>
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5 *:[svg]:text-foreground!">
{logos[currentIconLibrary?.name as keyof typeof logos]}
</div>
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
@@ -162,12 +162,10 @@ export function IconLibraryPicker({
</PickerRadioGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param="iconLibrary"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param="iconLibrary"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -1,6 +1,7 @@
"use client"
import * as React from "react"
import { type Button } from "@/examples/base/ui/button"
import { Menu09Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
@@ -10,9 +11,6 @@ import {
PickerContent,
PickerGroup,
PickerItem,
PickerLabel,
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerShortcut,
PickerTrigger,
@@ -22,23 +20,10 @@ import { useHistory } from "@/app/(create)/hooks/use-history"
import { useRandom } from "@/app/(create)/hooks/use-random"
import { useReset } from "@/app/(create)/hooks/use-reset"
import { useThemeToggle } from "@/app/(create)/hooks/use-theme-toggle"
import { type CustomizerPosition } from "@/app/(create)/lib/customizer"
const APPLE_PLATFORM_REGEX = /Mac|iPhone|iPad|iPod/
export function MainMenu({
className,
onToggleCollapse,
onPositionChange,
position = "left",
collapsed = false,
}: {
className?: string
onToggleCollapse?: () => void
onPositionChange?: (position: CustomizerPosition) => void
position?: CustomizerPosition
collapsed?: boolean
}) {
export function MainMenu({ className }: React.ComponentProps<typeof Button>) {
const [isMac, setIsMac] = React.useState(false)
const { canGoBack, canGoForward, goBack, goForward } = useHistory()
const { openActionMenu } = useActionMenuTrigger()
@@ -56,42 +41,15 @@ export function MainMenu({
<React.Fragment>
<Picker>
<PickerTrigger
aria-label="Menu"
collapsed={collapsed}
tooltip="Menu"
className={cn("relative block w-full text-left", className)}
className={cn(
"flex items-center justify-between gap-2 rounded-lg px-1.75 ring-1 ring-foreground/10 focus-visible:ring-1",
className
)}
>
<div
aria-hidden={collapsed}
className={cn(
"w-full pr-8 transition-[opacity,transform] duration-150 ease-out",
collapsed
? "translate-x-1 opacity-0"
: "translate-x-0 opacity-100"
)}
>
<div className="truncate font-medium text-foreground">Menu</div>
</div>
<div
className={cn(
"pointer-events-none absolute top-1/2 flex size-4 -translate-y-1/2 items-center justify-center text-foreground transition-[left,transform] duration-200 ease-out",
collapsed
? "left-1/2 -translate-x-1/2"
: "left-[calc(100%-1.625rem)] translate-x-0"
)}
>
<HugeiconsIcon
icon={Menu09Icon}
strokeWidth={2}
className="size-5"
/>
</div>
<span className="font-medium">Menu</span>
<HugeiconsIcon icon={Menu09Icon} strokeWidth={2} className="size-5" />
</PickerTrigger>
<PickerContent
side={position === "right" ? "left" : "right"}
align="start"
alignOffset={-8}
>
<PickerContent side="right" align="start" alignOffset={-8}>
<PickerGroup>
<PickerItem onClick={openActionMenu}>
Navigate...
@@ -103,35 +61,8 @@ export function MainMenu({
<PickerItem onClick={toggleTheme}>
Light/Dark <PickerShortcut>D</PickerShortcut>
</PickerItem>
{onToggleCollapse ? (
<PickerItem onClick={onToggleCollapse}>
{collapsed ? "Expand" : "Collapse"}{" "}
<PickerShortcut>{isMac ? "⌘B" : "Ctrl+B"}</PickerShortcut>
</PickerItem>
) : null}
</PickerGroup>
<PickerSeparator />
{onPositionChange ? (
<>
<PickerGroup>
<PickerLabel>Position</PickerLabel>
<PickerRadioGroup
value={position}
onValueChange={(value) => {
onPositionChange(value as CustomizerPosition)
}}
>
<PickerRadioItem value="left" closeOnClick>
Left
</PickerRadioItem>
<PickerRadioItem value="right" closeOnClick>
Right
</PickerRadioItem>
</PickerRadioGroup>
</PickerGroup>
<PickerSeparator />
</>
) : null}
<PickerGroup>
<PickerItem onClick={goBack} disabled={!canGoBack}>
Undo <PickerShortcut>{isMac ? "⌘Z" : "Ctrl+Z"}</PickerShortcut>

View File

@@ -7,7 +7,6 @@ import { useTheme } from "next-themes"
import { useMounted } from "@/hooks/use-mounted"
import { type MenuColorValue } from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -17,7 +16,7 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import {
isTranslucentMenuColor,
@@ -48,14 +47,11 @@ const MENU_OPTIONS: { value: MenuColorValue; label: string }[] = [
export function MenuColorPicker({
isMobile,
anchorRef,
collapsed = false,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const { resolvedTheme } = useTheme()
const mounted = useMounted()
const lastSolidMenuAccentRef = React.useRef(params.menuAccent)
@@ -105,23 +101,24 @@ export function MenuColorPicker({
return (
<div className="group/picker relative">
<Picker>
<PickerValueTrigger
label="Menu"
value={currentMenu?.label}
valueText={currentMenu?.label}
valueClassName="line-clamp-1"
indicator={
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Menu</div>
<div className="line-clamp-1 text-sm font-medium text-foreground">
{currentMenu?.label}
</div>
</div>
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center text-base text-foreground select-none md:right-2.5">
<HugeiconsIcon
icon={Menu02Icon}
strokeWidth={2}
className="size-4"
/>
}
collapsed={collapsed}
/>
</div>
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerGroup>
@@ -163,12 +160,10 @@ export function MenuColorPicker({
</PickerGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param="menuColor"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param="menuColor"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -5,12 +5,6 @@ import { Menu as MenuPrimitive } from "@base-ui/react/menu"
import { cn } from "@/registry/bases/base/lib/utils"
import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
import {
createTooltipHandle,
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/app/(create)/components/tooltip"
function Picker({ ...props }: MenuPrimitive.Root.Props) {
return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />
@@ -20,129 +14,16 @@ function PickerPortal({ ...props }: MenuPrimitive.Portal.Props) {
return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
}
function PickerTrigger({
className,
collapsed = false,
tooltip,
...triggerProps
}: MenuPrimitive.Trigger.Props & {
collapsed?: boolean
tooltip?: string
}) {
const [tooltipHandle] = React.useState(() =>
createTooltipHandle<{ text: string }>()
)
const trigger = (
function PickerTrigger({ className, ...props }: MenuPrimitive.Trigger.Props) {
return (
<MenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
data-collapsed={collapsed ? "true" : undefined}
className={cn(
"relative w-40 shrink-0 touch-manipulation overflow-hidden rounded-xl p-3 ring-1 ring-foreground/10 transition-[width,padding,border-radius] duration-200 ease-out 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",
collapsed &&
"flex size-10 w-10 items-center justify-center rounded-xl p-0 md:size-10 md:w-10 md:rounded-xl md:p-0",
className
)}
{...triggerProps}
/>
)
if (!collapsed || !tooltip) {
return trigger
}
return (
<React.Fragment>
<TooltipTrigger
handle={tooltipHandle}
payload={{ text: tooltip }}
render={trigger}
/>
<Tooltip handle={tooltipHandle}>
{(state) => {
const payload = state.payload as { text: string } | null | undefined
return payload ? (
<TooltipContent side="right" sideOffset={10}>
{payload.text}
</TooltipContent>
) : null
}}
</Tooltip>
</React.Fragment>
)
}
function PickerValueTrigger({
label,
value,
valueText,
indicator,
valueClassName,
indicatorClassName,
collapsed = false,
className,
...props
}: MenuPrimitive.Trigger.Props & {
label: string
value?: React.ReactNode
valueText?: string
indicator?: React.ReactNode
valueClassName?: string
indicatorClassName?: string
collapsed?: boolean
}) {
const accessibleValue =
valueText ?? (typeof value === "string" ? value : undefined)
const accessibleLabel = accessibleValue
? `${label}: ${accessibleValue}`
: label
const tooltip = accessibleValue ? `${label}: ${accessibleValue}` : label
return (
<PickerTrigger
aria-label={accessibleLabel}
collapsed={collapsed}
tooltip={tooltip}
className={cn(
"flex items-center",
collapsed ? "justify-center gap-0" : "justify-start gap-2.5",
"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
)}
{...props}
>
<div
aria-hidden={collapsed}
className={cn(
"min-w-0 flex-1 overflow-hidden text-left transition-[flex-basis,max-width,opacity,transform] duration-200 ease-out",
collapsed
? "max-w-0 basis-0 -translate-x-1 opacity-0"
: "max-w-full basis-auto opacity-100"
)}
>
<div className="flex flex-col justify-start text-left">
<div className="truncate text-xs text-muted-foreground">{label}</div>
<div
className={cn(
"truncate text-sm font-medium text-foreground",
valueClassName
)}
>
{value}
</div>
</div>
</div>
{indicator ? (
<div
className={cn(
"pointer-events-none flex size-4 shrink-0 items-center justify-center text-base text-foreground transition-transform duration-200 ease-out select-none *:[svg]:size-4 *:[svg]:text-foreground!",
indicatorClassName
)}
>
{indicator}
</div>
) : null}
</PickerTrigger>
/>
)
}
@@ -159,13 +40,6 @@ function PickerContent({
MenuPrimitive.Positioner.Props,
"align" | "alignOffset" | "side" | "sideOffset" | "anchor"
>) {
const bridgeClassName =
side === "left"
? "absolute inset-y-0 left-0 right-62 z-40 bg-transparent"
: side === "right"
? "absolute inset-y-0 right-0 left-62 z-40 bg-transparent"
: null
return (
<MenuPrimitive.Portal>
<MenuPrimitive.Positioner
@@ -185,7 +59,7 @@ function PickerContent({
{...props}
/>
</MenuPrimitive.Positioner>
{bridgeClassName ? <div className={bridgeClassName} /> : null}
<div className="absolute inset-y-0 right-0 left-62 z-40 bg-transparent" />
</MenuPrimitive.Portal>
)
}
@@ -404,7 +278,6 @@ export {
Picker,
PickerPortal,
PickerTrigger,
PickerValueTrigger,
PickerContent,
PickerGroup,
PickerLabel,

View File

@@ -3,7 +3,6 @@
import * as React from "react"
import { CMD_K_FORWARD_TYPE } from "@/app/(create)/components/action-menu"
import { CUSTOMIZER_TOGGLE_FORWARD_TYPE } from "@/app/(create)/components/customizer"
import {
REDO_FORWARD_TYPE,
UNDO_FORWARD_TYPE,
@@ -79,17 +78,6 @@ function handleMessage(event: MessageEvent) {
cancelable: true,
})
)
} else if (type === CUSTOMIZER_TOGGLE_FORWARD_TYPE) {
const isMac = MAC_REGEX.test(navigator.userAgent)
document.dispatchEvent(
new KeyboardEvent("keydown", {
key: event.data.key || "b",
metaKey: isMac,
ctrlKey: !isMac,
bubbles: true,
cancelable: true,
})
)
}
}

View File

@@ -3,7 +3,6 @@
import * as React from "react"
import { RADII, type RadiusValue } from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -12,21 +11,18 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
export function RadiusPicker({
isMobile,
anchorRef,
collapsed = false,
}: {
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const isRadiusLocked = params.style === "lyra"
const selectedRadiusName = isRadiusLocked ? "none" : params.radius
@@ -39,17 +35,20 @@ export function RadiusPicker({
return (
<div className="group/picker relative">
<Picker>
<PickerValueTrigger
label="Radius"
value={currentRadius?.label}
valueText={currentRadius?.label}
indicator={
<PickerTrigger disabled={isRadiusLocked}>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Radius</div>
<div className="text-sm font-medium text-foreground">
{currentRadius?.label}
</div>
</div>
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 rotate-90 items-center justify-center text-base text-foreground select-none md:right-2.5">
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
className="rotate-90 text-foreground"
className="text-foreground"
>
<path
fill="none"
@@ -60,13 +59,11 @@ export function RadiusPicker({
d="M4 20v-5C4 8.925 8.925 4 15 4h5"
/>
</svg>
}
collapsed={collapsed}
disabled={isRadiusLocked}
/>
</div>
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
@@ -104,12 +101,10 @@ export function RadiusPicker({
</PickerRadioGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param="radius"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param="radius"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -2,11 +2,6 @@
import Script from "next/script"
import { Button } from "@/examples/base/ui/button"
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from "@/examples/base/ui/tooltip"
import { DiceFaces05Icon } from "@hugeicons/core-free-icons"
import { HugeiconsIcon } from "@hugeicons/react"
@@ -18,41 +13,10 @@ export const RANDOMIZE_FORWARD_TYPE = "randomize-forward"
export function RandomButton({
variant = "outline",
className,
collapsed = false,
...props
}: React.ComponentProps<typeof Button> & {
collapsed?: boolean
}) {
}: React.ComponentProps<typeof Button>) {
const { randomize } = useRandom()
if (collapsed) {
return (
<Tooltip>
<TooltipTrigger
render={
<Button
variant={variant}
size="icon"
aria-label="Shuffle"
onClick={randomize}
className={cn(
"size-10 touch-manipulation rounded-xl bg-transparent! transition-none select-none hover:bg-muted!",
className
)}
{...props}
/>
}
>
<HugeiconsIcon icon={DiceFaces05Icon} strokeWidth={2} />
<span className="sr-only">Shuffle</span>
</TooltipTrigger>
<TooltipContent side="right" sideOffset={10}>
Shuffle
</TooltipContent>
</Tooltip>
)
}
return (
<Button
variant={variant}

View File

@@ -3,7 +3,6 @@
import * as React from "react"
import { type Style, type StyleName } from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -11,7 +10,7 @@ import {
PickerGroup,
PickerRadioGroup,
PickerRadioItem,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
@@ -19,36 +18,36 @@ export function StylePicker({
styles,
isMobile,
anchorRef,
collapsed = false,
}: {
styles: readonly Style[]
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const currentStyle = styles.find((style) => style.name === params.style)
const currentStyleIcon = currentStyle?.icon
? React.cloneElement(currentStyle.icon, {
className: "size-4",
})
: null
return (
<div className="group/picker relative">
<Picker>
<PickerValueTrigger
label="Style"
value={currentStyle?.title}
valueText={currentStyle?.title}
indicator={currentStyleIcon}
collapsed={collapsed}
/>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Style</div>
<div className="text-sm font-medium text-foreground">
{currentStyle?.title}
</div>
</div>
{currentStyle?.icon && (
<div className="pointer-events-none absolute top-1/2 right-4 flex size-4 -translate-y-1/2 items-center justify-center select-none md:right-2.5">
{React.cloneElement(currentStyle.icon, {
className: "size-4",
})}
</div>
)}
</PickerTrigger>
<PickerContent
anchor={isMobile ? anchorRef : undefined}
side={isMobile ? "top" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
>
<PickerRadioGroup
@@ -71,12 +70,10 @@ export function StylePicker({
</PickerRadioGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param="style"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param="style"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -4,7 +4,6 @@ import * as React from "react"
import { useMounted } from "@/hooks/use-mounted"
import { BASE_COLORS, type Theme, type ThemeName } from "@/registry/config"
import { useCustomizerLayout } from "@/app/(create)/components/customizer-layout"
import { LockButton } from "@/app/(create)/components/lock-button"
import {
Picker,
@@ -13,7 +12,7 @@ import {
PickerRadioGroup,
PickerRadioItem,
PickerSeparator,
PickerValueTrigger,
PickerTrigger,
} from "@/app/(create)/components/picker"
import { useDesignSystemSearchParams } from "@/app/(create)/lib/search-params"
@@ -21,16 +20,13 @@ export function ThemePicker({
themes,
isMobile,
anchorRef,
collapsed = false,
}: {
themes: readonly Theme[]
isMobile: boolean
anchorRef: React.RefObject<HTMLDivElement | null>
collapsed?: boolean
}) {
const mounted = useMounted()
const [params, setParams] = useDesignSystemSearchParams()
const { desktopPickerSide } = useCustomizerLayout()
const currentTheme = React.useMemo(
() => themes.find((theme) => theme.name === params.theme),
@@ -48,33 +44,33 @@ export function ThemePicker({
}
}, [currentTheme, themes, setParams])
const themeIndicator = mounted ? (
<div
style={
{
"--color":
currentTheme?.cssVars?.dark?.[
currentThemeIsBaseColor ? "muted-foreground" : "primary"
],
} as React.CSSProperties
}
className="size-4 rounded-full bg-(--color)"
/>
) : null
return (
<div className="group/picker relative">
<Picker>
<PickerValueTrigger
label="Theme"
value={currentTheme?.title}
valueText={currentTheme?.title}
indicator={themeIndicator}
collapsed={collapsed}
/>
<PickerTrigger>
<div className="flex flex-col justify-start text-left">
<div className="text-xs text-muted-foreground">Theme</div>
<div className="text-sm font-medium text-foreground">
{currentTheme?.title}
</div>
</div>
{mounted && (
<div
style={
{
"--color":
currentTheme?.cssVars?.dark?.[
currentThemeIsBaseColor ? "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" : desktopPickerSide}
side={isMobile ? "top" : "right"}
align={isMobile ? "center" : "start"}
className="max-h-92"
>
@@ -125,12 +121,10 @@ export function ThemePicker({
</PickerRadioGroup>
</PickerContent>
</Picker>
{!collapsed ? (
<LockButton
param="theme"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
) : null}
<LockButton
param="theme"
className="absolute top-1/2 right-8 -translate-y-1/2"
/>
</div>
)
}

View File

@@ -1,12 +0,0 @@
"use client"
import { Tooltip as TooltipPrimitive } from "@base-ui/react/tooltip"
export {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/examples/base/ui/tooltip"
export const createTooltipHandle = TooltipPrimitive.createHandle

View File

@@ -1,5 +1,4 @@
import { type Metadata } from "next"
import { cookies, headers } from "next/headers"
import { siteConfig } from "@/lib/config"
import { absoluteUrl } from "@/lib/utils"
@@ -9,11 +8,6 @@ import { PresetHandler } from "@/app/(create)/components/preset-handler"
import { Preview } from "@/app/(create)/components/preview"
import { WelcomeDialog } from "@/app/(create)/components/welcome-dialog"
import { getAllItems } from "@/app/(create)/lib/api"
import {
CUSTOMIZER_POSITION_COOKIE_NAME,
CUSTOMIZER_STATE_COOKIE_NAME,
parseCustomizerPosition,
} from "@/app/(create)/lib/customizer"
export const metadata: Metadata = {
title: "New Project",
@@ -45,39 +39,20 @@ export const metadata: Metadata = {
}
export default async function CreatePage() {
const cookieStore = await cookies()
const headersList = await headers()
const itemsByBase = await getAllItems()
const defaultCollapsed =
cookieStore.get(CUSTOMIZER_STATE_COOKIE_NAME)?.value === "true"
const defaultPosition = parseCustomizerPosition(
cookieStore.get(CUSTOMIZER_POSITION_COOKIE_NAME)?.value
)
const defaultIsMobile =
headersList.get("sec-ch-ua-mobile") === "?1" ||
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(
headersList.get("user-agent") ?? ""
)
return (
<div
data-slot="layout"
data-customizer-position={defaultPosition}
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-customizer-position={defaultPosition}
className="flex min-h-0 flex-1 flex-col gap-(--gap) p-(--gap) pt-[calc(var(--gap)*0.25)] md:flex-row data-[customizer-position=left]: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 />
<Customizer
itemsByBase={itemsByBase}
defaultCollapsed={defaultCollapsed}
defaultIsMobile={defaultIsMobile}
defaultPosition={defaultPosition}
/>
<Customizer itemsByBase={itemsByBase} />
<PresetHandler />
<WelcomeDialog />
</main>

View File

@@ -1,21 +0,0 @@
export const CUSTOMIZER_STATE_COOKIE_NAME = "customizer_state"
export const CUSTOMIZER_STATE_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
export const CUSTOMIZER_POSITION_COOKIE_NAME = "customizer_position"
export const CUSTOMIZER_POSITIONS = ["left", "right"] as const
export type CustomizerPosition = (typeof CUSTOMIZER_POSITIONS)[number]
export type CustomizerDesktopPickerSide = "left" | "right"
export function parseCustomizerPosition(
value: string | undefined
): CustomizerPosition {
return value === "right" ? "right" : "left"
}
export function getCustomizerDesktopPickerSide(
position: CustomizerPosition
): CustomizerDesktopPickerSide {
return position === "right" ? "left" : "right"
}

View File

@@ -7,7 +7,6 @@ import { absoluteUrl } from "@/lib/utils"
import { TailwindIndicator } from "@/components/tailwind-indicator"
import { BASES, type Base, type BaseName } from "@/registry/config"
import { ActionMenuScript } from "@/app/(create)/components/action-menu"
import { CustomizerToggleScript } from "@/app/(create)/components/customizer"
import { DesignSystemProvider } from "@/app/(create)/components/design-system-provider"
import { HistoryScript } from "@/app/(create)/components/history-buttons"
import { DarkModeScript } from "@/app/(create)/components/mode-switcher"
@@ -143,7 +142,6 @@ export default async function BlockPage({
<RandomizeScript />
<HistoryScript />
<DarkModeScript />
<CustomizerToggleScript />
<DesignSystemProvider>
<Component />
</DesignSystemProvider>