feat(editor): fold canvas display controls into one Display dropdown (#370)
Replace the seven separate display-control buttons in the canvas toolbar (render, theme, edges, grid, shadows, units, camera) with a single "Display" dropdown, matching the simplification already shipped in the hosted editor. Grid, shadows, camera and units are direct toggles that keep the menu open; render, edges and theme are submenus. Portal DropdownMenuSubContent so the side-opening submenus escape the parent content's overflow-x-hidden clip, and re-export the submenu primitives (Sub/SubTrigger/SubContent/Separator) from @pascal-app/editor. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
ee98c55b65
commit
86db5decb8
@@ -5,6 +5,10 @@ import {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
useEditor,
|
useEditor,
|
||||||
useSidebarStore,
|
useSidebarStore,
|
||||||
@@ -29,6 +33,7 @@ import {
|
|||||||
Footprints,
|
Footprints,
|
||||||
Grid2X2,
|
Grid2X2,
|
||||||
PenLine,
|
PenLine,
|
||||||
|
SlidersHorizontal,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
SwatchBook,
|
SwatchBook,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
@@ -239,94 +244,8 @@ function WallModeToggle() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function RenderModeMenu() {
|
// One dropdown that gathers every "how the scene looks" control: grid, shadows,
|
||||||
const shading = useViewer((state) => state.shading)
|
// camera projection, units, render mode, edges and scene theme.
|
||||||
const setShading = useViewer((state) => state.setShading)
|
|
||||||
const active = SHADING_OPTIONS.find((option) => option.id === shading) ?? SHADING_OPTIONS[0]
|
|
||||||
const ActiveIcon = active.icon
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<ToolbarTooltip label={`Render: ${active.name}`}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
aria-label={`Render: ${active.name}`}
|
|
||||||
className={cn(
|
|
||||||
TOOLBAR_BTN,
|
|
||||||
'w-auto gap-1.5 px-2.5',
|
|
||||||
shading === 'rendered' && 'bg-white/10 text-foreground/90',
|
|
||||||
)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<ActiveIcon className="h-3.5 w-3.5" />
|
|
||||||
<span className="font-medium text-xs">{active.name}</span>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</ToolbarTooltip>
|
|
||||||
<DropdownMenuContent align="center" className="min-w-56" side="bottom">
|
|
||||||
{SHADING_OPTIONS.map((option) => {
|
|
||||||
const OptionIcon = option.icon
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem key={option.id} onSelect={() => setShading(option.id)}>
|
|
||||||
<OptionIcon className="h-4 w-4" />
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-foreground">{option.name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
||||||
</div>
|
|
||||||
{shading === option.id ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function SceneThemeMenu() {
|
|
||||||
const sceneTheme = useViewer((state) => state.sceneTheme)
|
|
||||||
const setSceneTheme = useViewer((state) => state.setSceneTheme)
|
|
||||||
const active = getSceneTheme(sceneTheme)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownMenu>
|
|
||||||
<ToolbarTooltip label={`Scene theme: ${active.name}`}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
aria-label={`Scene theme: ${active.name}`}
|
|
||||||
className={cn(TOOLBAR_BTN, 'w-28 gap-1.5 px-2.5 text-foreground/90')}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<SwatchBook className="h-3.5 w-3.5 shrink-0" />
|
|
||||||
<span className="truncate font-medium text-xs">{active.name}</span>
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</ToolbarTooltip>
|
|
||||||
<DropdownMenuContent align="center" className="min-w-48" side="bottom">
|
|
||||||
{SCENE_THEMES.map((theme) => {
|
|
||||||
const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
|
|
||||||
(role) => theme.clayTints?.[role] ?? CLAY_PALETTE[role],
|
|
||||||
)
|
|
||||||
return (
|
|
||||||
<DropdownMenuItem key={theme.id} onSelect={() => setSceneTheme(theme.id)}>
|
|
||||||
<span
|
|
||||||
className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
|
|
||||||
style={{ backgroundColor: theme.background }}
|
|
||||||
>
|
|
||||||
{swatches.map((color, index) => (
|
|
||||||
<span key={`${theme.id}-${index}`} style={{ backgroundColor: color }} />
|
|
||||||
))}
|
|
||||||
</span>
|
|
||||||
<span className="text-foreground">{theme.name}</span>
|
|
||||||
{sceneTheme === theme.id ? (
|
|
||||||
<Check className="ml-auto h-4 w-4 text-foreground" />
|
|
||||||
) : null}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const EDGE_OPTIONS = [
|
const EDGE_OPTIONS = [
|
||||||
{ id: 'off', name: 'Off', detail: 'No edge lines' },
|
{ id: 'off', name: 'Off', detail: 'No edge lines' },
|
||||||
@@ -334,127 +253,178 @@ const EDGE_OPTIONS = [
|
|||||||
{ id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' },
|
{ id: 'strong', name: 'Strong', detail: 'Crisp, opaque edge lines' },
|
||||||
] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[]
|
] as const satisfies readonly { id: EdgeMode; name: string; detail: string }[]
|
||||||
|
|
||||||
function EdgesMenu() {
|
const SUBMENU_CONTENT_CLASS = 'min-w-56 rounded-xl border-border/45 bg-popover/95 backdrop-blur-xl'
|
||||||
const edges = useViewer((state) => state.edges)
|
|
||||||
const setEdges = useViewer((state) => state.setEdges)
|
|
||||||
const active = EDGE_OPTIONS.find((option) => option.id === edges) ?? EDGE_OPTIONS[0]
|
|
||||||
|
|
||||||
return (
|
function DisplayMenu() {
|
||||||
<DropdownMenu>
|
|
||||||
<ToolbarTooltip label={`Edges: ${active.name}`}>
|
|
||||||
<DropdownMenuTrigger asChild>
|
|
||||||
<button
|
|
||||||
aria-label={`Edges: ${active.name}`}
|
|
||||||
className={cn(TOOLBAR_BTN, edges !== 'off' && 'bg-white/10 text-foreground/90')}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<PenLine className="h-4 w-4" />
|
|
||||||
</button>
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
</ToolbarTooltip>
|
|
||||||
<DropdownMenuContent align="center" className="min-w-56" side="bottom">
|
|
||||||
{EDGE_OPTIONS.map((option) => (
|
|
||||||
<DropdownMenuItem key={option.id} onSelect={() => setEdges(option.id)}>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<span className="text-foreground">{option.name}</span>
|
|
||||||
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
|
||||||
</div>
|
|
||||||
{edges === option.id ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
|
|
||||||
</DropdownMenuItem>
|
|
||||||
))}
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function GridVisibilityToggle() {
|
|
||||||
const showGrid = useViewer((state) => state.showGrid)
|
const showGrid = useViewer((state) => state.showGrid)
|
||||||
const setShowGrid = useViewer((state) => state.setShowGrid)
|
const setShowGrid = useViewer((state) => state.setShowGrid)
|
||||||
|
const unit = useViewer((state) => state.unit)
|
||||||
return (
|
const setUnit = useViewer((state) => state.setUnit)
|
||||||
<ToolbarTooltip label={`Grid: ${showGrid ? 'Visible' : 'Hidden'}`}>
|
const cameraMode = useViewer((state) => state.cameraMode)
|
||||||
<button
|
const setCameraMode = useViewer((state) => state.setCameraMode)
|
||||||
aria-label={`Grid: ${showGrid ? 'Visible' : 'Hidden'}`}
|
const shading = useViewer((state) => state.shading)
|
||||||
aria-pressed={showGrid}
|
const setShading = useViewer((state) => state.setShading)
|
||||||
className={cn(
|
const sceneTheme = useViewer((state) => state.sceneTheme)
|
||||||
TOOLBAR_BTN,
|
const setSceneTheme = useViewer((state) => state.setSceneTheme)
|
||||||
'w-auto gap-1.5 px-2.5',
|
const edges = useViewer((state) => state.edges)
|
||||||
showGrid
|
const setEdges = useViewer((state) => state.setEdges)
|
||||||
? 'bg-white/10 text-foreground/90'
|
|
||||||
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
|
|
||||||
)}
|
|
||||||
onClick={() => setShowGrid(!showGrid)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Grid2X2 className="h-3.5 w-3.5" />
|
|
||||||
{showGrid ? <Eye className="h-3.5 w-3.5" /> : <EyeOff className="h-3.5 w-3.5" />}
|
|
||||||
</button>
|
|
||||||
</ToolbarTooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function ShadowsToggle() {
|
|
||||||
const shadows = useViewer((state) => state.shadows)
|
const shadows = useViewer((state) => state.shadows)
|
||||||
const setShadows = useViewer((state) => state.setShadows)
|
const setShadows = useViewer((state) => state.setShadows)
|
||||||
|
|
||||||
return (
|
const activeShading =
|
||||||
<ToolbarTooltip label={`Shadows: ${shadows ? 'On' : 'Off'}`}>
|
SHADING_OPTIONS.find((option) => option.id === shading) ?? SHADING_OPTIONS[0]
|
||||||
<button
|
const activeEdges = EDGE_OPTIONS.find((option) => option.id === edges) ?? EDGE_OPTIONS[0]
|
||||||
aria-label={`Shadows: ${shadows ? 'On' : 'Off'}`}
|
const activeTheme = getSceneTheme(sceneTheme)
|
||||||
aria-pressed={shadows}
|
|
||||||
className={cn(
|
|
||||||
TOOLBAR_BTN,
|
|
||||||
shadows
|
|
||||||
? 'bg-white/10 text-foreground/90'
|
|
||||||
: 'opacity-60 grayscale hover:opacity-100 hover:grayscale-0',
|
|
||||||
)}
|
|
||||||
onClick={() => setShadows(!shadows)}
|
|
||||||
type="button"
|
|
||||||
>
|
|
||||||
<Contrast className="h-3.5 w-3.5" />
|
|
||||||
</button>
|
|
||||||
</ToolbarTooltip>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
function UnitToggle() {
|
// Keep the menu open when flipping a toggle.
|
||||||
const unit = useViewer((state) => state.unit)
|
const keepOpen = (event: Event, fn: () => void) => {
|
||||||
const setUnit = useViewer((state) => state.setUnit)
|
event.preventDefault()
|
||||||
|
fn()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ToolbarTooltip label={unit === 'metric' ? 'Metric (m)' : 'Imperial (ft)'}>
|
<DropdownMenu>
|
||||||
<button
|
<ToolbarTooltip label="Display settings">
|
||||||
className={TOOLBAR_BTN}
|
<DropdownMenuTrigger asChild>
|
||||||
onClick={() => setUnit(unit === 'metric' ? 'imperial' : 'metric')}
|
<button
|
||||||
type="button"
|
aria-label="Display settings"
|
||||||
|
className={cn(TOOLBAR_BTN, 'w-auto gap-1.5 px-2.5 text-foreground/90')}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
<SlidersHorizontal className="h-3.5 w-3.5 shrink-0" />
|
||||||
|
<span className="font-medium text-xs">Display</span>
|
||||||
|
</button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
</ToolbarTooltip>
|
||||||
|
<DropdownMenuContent
|
||||||
|
align="end"
|
||||||
|
className="w-60 rounded-xl border-border/45 bg-popover/95 backdrop-blur-xl"
|
||||||
|
side="bottom"
|
||||||
|
sideOffset={8}
|
||||||
>
|
>
|
||||||
<span className="font-semibold text-[10px]">{unit === 'metric' ? 'm' : 'ft'}</span>
|
<DropdownMenuItem onSelect={(e) => keepOpen(e, () => setShowGrid(!showGrid))}>
|
||||||
</button>
|
<Grid2X2 className="h-4 w-4" />
|
||||||
</ToolbarTooltip>
|
<span>Grid</span>
|
||||||
)
|
{showGrid ? (
|
||||||
}
|
<Eye className="ml-auto h-4 w-4 text-foreground" />
|
||||||
|
) : (
|
||||||
|
<EyeOff className="ml-auto h-4 w-4 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem onSelect={(e) => keepOpen(e, () => setShadows(!shadows))}>
|
||||||
|
<Contrast className="h-4 w-4" />
|
||||||
|
<span>Shadows</span>
|
||||||
|
<span className="ml-auto text-muted-foreground text-xs">{shadows ? 'On' : 'Off'}</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) =>
|
||||||
|
keepOpen(e, () =>
|
||||||
|
setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective'),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconifyIcon
|
||||||
|
height={16}
|
||||||
|
icon={cameraMode === 'perspective' ? 'icon-park-outline:perspective' : 'vaadin:grid'}
|
||||||
|
width={16}
|
||||||
|
/>
|
||||||
|
<span>Camera</span>
|
||||||
|
<span className="ml-auto text-muted-foreground text-xs">
|
||||||
|
{cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
onSelect={(e) => keepOpen(e, () => setUnit(unit === 'metric' ? 'imperial' : 'metric'))}
|
||||||
|
>
|
||||||
|
<span className="flex h-4 w-4 items-center justify-center font-semibold text-[10px]">
|
||||||
|
{unit === 'metric' ? 'm' : 'ft'}
|
||||||
|
</span>
|
||||||
|
<span>Units</span>
|
||||||
|
<span className="ml-auto text-muted-foreground text-xs">
|
||||||
|
{unit === 'metric' ? 'Metric' : 'Imperial'}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
|
||||||
function CameraModeToggle() {
|
<DropdownMenuSeparator />
|
||||||
const cameraMode = useViewer((state) => state.cameraMode)
|
|
||||||
const setCameraMode = useViewer((state) => state.setCameraMode)
|
|
||||||
|
|
||||||
return (
|
<DropdownMenuSub>
|
||||||
<ToolbarTooltip label={cameraMode === 'perspective' ? 'Perspective' : 'Orthographic'}>
|
<DropdownMenuSubTrigger>
|
||||||
<button
|
<activeShading.icon className="h-4 w-4" />
|
||||||
className={cn(
|
<span>Render</span>
|
||||||
TOOLBAR_BTN,
|
<span className="ml-auto text-muted-foreground text-xs">{activeShading.name}</span>
|
||||||
cameraMode === 'orthographic' && 'bg-white/10 text-foreground/90',
|
</DropdownMenuSubTrigger>
|
||||||
)}
|
<DropdownMenuSubContent className={SUBMENU_CONTENT_CLASS}>
|
||||||
onClick={() => setCameraMode(cameraMode === 'perspective' ? 'orthographic' : 'perspective')}
|
{SHADING_OPTIONS.map((option) => {
|
||||||
type="button"
|
const OptionIcon = option.icon
|
||||||
>
|
return (
|
||||||
{cameraMode === 'perspective' ? (
|
<DropdownMenuItem key={option.id} onSelect={() => setShading(option.id)}>
|
||||||
<IconifyIcon height={16} icon="icon-park-outline:perspective" width={16} />
|
<OptionIcon className="h-4 w-4" />
|
||||||
) : (
|
<div className="flex flex-col">
|
||||||
<IconifyIcon height={16} icon="vaadin:grid" width={16} />
|
<span className="text-foreground">{option.name}</span>
|
||||||
)}
|
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
||||||
</button>
|
</div>
|
||||||
</ToolbarTooltip>
|
{shading === option.id ? (
|
||||||
|
<Check className="ml-auto h-4 w-4 text-foreground" />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<PenLine className="h-4 w-4" />
|
||||||
|
<span>Edges</span>
|
||||||
|
<span className="ml-auto text-muted-foreground text-xs">{activeEdges.name}</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent className={SUBMENU_CONTENT_CLASS}>
|
||||||
|
{EDGE_OPTIONS.map((option) => (
|
||||||
|
<DropdownMenuItem key={option.id} onSelect={() => setEdges(option.id)}>
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="text-foreground">{option.name}</span>
|
||||||
|
<span className="text-muted-foreground text-xs">{option.detail}</span>
|
||||||
|
</div>
|
||||||
|
{edges === option.id ? <Check className="ml-auto h-4 w-4 text-foreground" /> : null}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
|
||||||
|
<DropdownMenuSub>
|
||||||
|
<DropdownMenuSubTrigger>
|
||||||
|
<SwatchBook className="h-4 w-4" />
|
||||||
|
<span>Theme</span>
|
||||||
|
<span className="ml-auto truncate text-muted-foreground text-xs">
|
||||||
|
{activeTheme.name}
|
||||||
|
</span>
|
||||||
|
</DropdownMenuSubTrigger>
|
||||||
|
<DropdownMenuSubContent className="min-w-48 rounded-xl border-border/45 bg-popover/95 backdrop-blur-xl">
|
||||||
|
{SCENE_THEMES.map((theme) => {
|
||||||
|
const swatches = (['wall', 'roof', 'floor', 'glazing'] as const).map(
|
||||||
|
(role) => theme.clayTints?.[role] ?? CLAY_PALETTE[role],
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<DropdownMenuItem key={theme.id} onSelect={() => setSceneTheme(theme.id)}>
|
||||||
|
<span
|
||||||
|
className="grid h-5 w-5 shrink-0 grid-cols-2 overflow-hidden rounded-sm border border-black/10"
|
||||||
|
style={{ backgroundColor: theme.background }}
|
||||||
|
>
|
||||||
|
{swatches.map((color, index) => (
|
||||||
|
<span key={`${theme.id}-${index}`} style={{ backgroundColor: color }} />
|
||||||
|
))}
|
||||||
|
</span>
|
||||||
|
<span className="text-foreground">{theme.name}</span>
|
||||||
|
{sceneTheme === theme.id ? (
|
||||||
|
<Check className="ml-auto h-4 w-4 text-foreground" />
|
||||||
|
) : null}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</DropdownMenuSubContent>
|
||||||
|
</DropdownMenuSub>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -507,14 +477,8 @@ export function CommunityViewerToolbarRight() {
|
|||||||
<div className={TOOLBAR_CONTAINER}>
|
<div className={TOOLBAR_CONTAINER}>
|
||||||
<LevelModeToggle />
|
<LevelModeToggle />
|
||||||
<WallModeToggle />
|
<WallModeToggle />
|
||||||
<RenderModeMenu />
|
|
||||||
<SceneThemeMenu />
|
|
||||||
<EdgesMenu />
|
|
||||||
<GridVisibilityToggle />
|
|
||||||
<ShadowsToggle />
|
|
||||||
<div className="my-1.5 w-px bg-border/50" />
|
<div className="my-1.5 w-px bg-border/50" />
|
||||||
<UnitToggle />
|
<DisplayMenu />
|
||||||
<CameraModeToggle />
|
|
||||||
<div className="my-1.5 w-px bg-border/50" />
|
<div className="my-1.5 w-px bg-border/50" />
|
||||||
<WalkthroughButton />
|
<WalkthroughButton />
|
||||||
<PreviewButton />
|
<PreviewButton />
|
||||||
|
|||||||
@@ -197,15 +197,19 @@ function DropdownMenuSubContent({
|
|||||||
className,
|
className,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
|
||||||
|
// Portalled so the side-opening submenu escapes the parent content's
|
||||||
|
// `overflow-x-hidden` clip (otherwise it renders but is invisible).
|
||||||
return (
|
return (
|
||||||
<DropdownMenuPrimitive.SubContent
|
<DropdownMenuPrimitive.Portal>
|
||||||
className={cn(
|
<DropdownMenuPrimitive.SubContent
|
||||||
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in',
|
className={cn(
|
||||||
className,
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-32 origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=closed]:animate-out data-[state=open]:animate-in',
|
||||||
)}
|
className,
|
||||||
data-slot="dropdown-menu-sub-content"
|
)}
|
||||||
{...props}
|
data-slot="dropdown-menu-sub-content"
|
||||||
/>
|
{...props}
|
||||||
|
/>
|
||||||
|
</DropdownMenuPrimitive.Portal>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ export {
|
|||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
|
DropdownMenuSeparator,
|
||||||
|
DropdownMenuSub,
|
||||||
|
DropdownMenuSubContent,
|
||||||
|
DropdownMenuSubTrigger,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from './components/ui/primitives/dropdown-menu'
|
} from './components/ui/primitives/dropdown-menu'
|
||||||
export { useSidebarStore } from './components/ui/primitives/sidebar'
|
export { useSidebarStore } from './components/ui/primitives/sidebar'
|
||||||
|
|||||||
Reference in New Issue
Block a user