Wawa e618175bba Reset core/viewer/editor/mcp packages and apps/editor from private-editor
Wholesale swap of packages/{core,viewer,editor,mcp} and apps/editor with the
versions from the private editor repo, which is the production source of truth.

Setup changes:
- packages/{core,viewer,editor} versions held at 0.7.0 baseline (matching
  the most recent published release) so a bump=minor publishes 0.8.0
- packages/mcp held at 0.1.1 (never published; first publish will go through
  the new release.yml flow)
- peerDependencies and devDependencies for inter-package @pascal-app/*
  references pinned to ^0.7.0 instead of '*' / 'workspace:*' so they are
  valid for npm consumers
- Root package.json: TypeScript bumped to 6.0.2, added overrides for
  @types/react, @types/react-dom, @types/three to prevent JSX namespace
  fragmentation across the workspace
- release.yml extended to also publish editor and mcp; 'both' option renamed
  to 'all'; added a sync step that updates inter-package peerDeps/devDeps to
  match the new versions on every bump (so viewer/editor/mcp tarballs always
  reference the version of core they were built against)
- Root scripts gained release:editor and release:mcp shortcuts

Verification:
- bun install --frozen-lockfile is consistent
- packages/{core,viewer,mcp} build cleanly, dist/index.d.ts emitted
- packages/editor check-types reports 21 pre-existing errors, identical to
  what private-editor currently reports

Open PRs against editor-v2 will need rebasing/conflict resolution.
2026-05-09 20:52:26 +00:00

192 lines
7.0 KiB
TypeScript

'use client'
import { useScene } from '@pascal-app/core'
import { useViewer } from '@pascal-app/viewer'
import { AnimatePresence, motion } from 'motion/react'
import { useEffect, useMemo } from 'react'
import { MaterialPicker } from './../../../components/ui/controls/material-picker'
import { TooltipProvider } from './../../../components/ui/primitives/tooltip'
import { useIsMobile } from './../../../hooks/use-mobile'
import { useReducedMotion } from './../../../hooks/use-reduced-motion'
import { resolvePaintTargetFromSelection } from './../../../lib/material-paint'
import { cn } from './../../../lib/utils'
import useEditor from './../../../store/use-editor'
import { CameraActions } from './camera-actions'
import { ControlModes } from './control-modes'
import { StructureTools } from './structure-tools'
import { GridSnapControl, SecondaryToggles } from './view-toggles'
// Mobile bottom offset matches the viewer's overlap behind the sheet's
// rounded corners (SHEET_OVERLAP_PX in editor-layout-mobile) so the menu sits
// just above that strip instead of inside it.
const MOBILE_BOTTOM_OFFSET = 24
function PaintMaterialTray() {
const activePaintMaterial = useEditor((state) => state.activePaintMaterial)
const activePaintTarget = useEditor((state) => state.activePaintTarget)
const setActivePaintMaterial = useEditor((state) => state.setActivePaintMaterial)
const setActivePaintTarget = useEditor((state) => state.setActivePaintTarget)
const selectedIds = useViewer((state) => state.selection.selectedIds)
const nodes = useScene((state) => state.nodes)
const selectedId = selectedIds.length === 1 ? (selectedIds[0] ?? null) : null
useEffect(() => {
const selectedPaintTarget = resolvePaintTargetFromSelection({
nodes,
selectedId,
})
if (selectedPaintTarget) {
setActivePaintTarget(selectedPaintTarget)
}
}, [nodes, selectedId, setActivePaintTarget])
return (
<div className="w-[42rem] max-w-[calc(100vw-2rem)]">
<MaterialPicker
onChange={(material) => {
setActivePaintMaterial({ material, sourceTarget: activePaintTarget })
}}
onSelectMaterialPreset={(materialPreset) => {
setActivePaintMaterial({ materialPreset, sourceTarget: activePaintTarget })
}}
selectedMaterialPreset={activePaintMaterial?.materialPreset}
value={activePaintMaterial?.material}
/>
</div>
)
}
export function ActionMenu({ className }: { className?: string }) {
const phase = useEditor((state) => state.phase)
const mode = useEditor((state) => state.mode)
const tool = useEditor((state) => state.tool)
const catalogCategory = useEditor((state) => state.catalogCategory)
const isMobile = useIsMobile()
const hasSelectionOnMobile = useViewer((s) => isMobile && s.selection.selectedIds.length > 0)
const hasReferenceOnMobile = useEditor((s) => isMobile && Boolean(s.selectedReferenceId))
const CONTEXTUAL_TABS = new Set(['ai', 'items', 'studio'])
const isContextualPanelOnMobile = useEditor(
(s) => isMobile && CONTEXTUAL_TABS.has(s.activeSidebarPanel),
)
const reducedMotion = useReducedMotion()
const showPaintTray = useMemo(() => mode === 'material-paint', [mode])
// On mobile, defer the bottom rail to the selection bar when something
// is selected — the contextual actions take priority over mode controls.
// Also hide on Chat / Items / Studio tabs; those are contextual workflows
// (composing / picking furniture / generating renders) where the build
// menu is irrelevant.
if (hasSelectionOnMobile || hasReferenceOnMobile || isContextualPanelOnMobile) return null
const transition = reducedMotion
? { duration: 0 }
: { type: 'spring' as const, bounce: 0.2, duration: 0.4 }
return (
<TooltipProvider>
<motion.div
className={cn(
'left-1/2 z-50 -translate-x-1/2',
isMobile ? 'absolute origin-bottom scale-90' : 'fixed bottom-6',
'rounded-2xl border border-border bg-background/90 shadow-2xl backdrop-blur-md',
'transition-colors duration-200 ease-out',
className,
)}
layout
style={isMobile ? { bottom: MOBILE_BOTTOM_OFFSET } : undefined}
transition={transition}
>
{/* Structure Tools Row - Animated */}
<AnimatePresence>
{phase === 'structure' && mode === 'build' && (
<motion.div
animate={{
opacity: 1,
maxHeight: 80,
paddingTop: 8,
paddingBottom: 8,
borderBottomWidth: 1,
}}
className={cn('max-h-20 overflow-hidden border-border border-b px-2 py-2')}
exit={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
initial={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
transition={transition}
>
<div className="w-max">
<StructureTools />
</div>
</motion.div>
)}
</AnimatePresence>
<AnimatePresence>
{showPaintTray && (
<motion.div
animate={{
opacity: 1,
maxHeight: 96,
paddingTop: 8,
paddingBottom: 8,
borderBottomWidth: 1,
}}
className={cn('overflow-hidden border-border border-b px-3')}
exit={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
initial={{
opacity: 0,
maxHeight: 0,
paddingTop: 0,
paddingBottom: 0,
borderBottomWidth: 0,
}}
transition={transition}
>
<PaintMaterialTray />
</motion.div>
)}
</AnimatePresence>
{isMobile ? (
<div className="flex flex-col items-stretch gap-0.5 px-2 py-1.5">
{/* Row 1: control modes only */}
<div className="flex items-center justify-center gap-1">
<ControlModes />
</div>
{/* Row 2: grid snap + secondary toggles (orbit + top view hidden) */}
<div className="flex items-center justify-center gap-1 border-border/50 border-t pt-1">
<GridSnapControl />
<SecondaryToggles />
</div>
</div>
) : (
<div className="flex items-center justify-center gap-1 px-2 py-1.5">
<ControlModes />
<div className="mx-1 h-5 w-px bg-border" />
<GridSnapControl />
<SecondaryToggles />
<div className="mx-1 h-5 w-px bg-border" />
<CameraActions />
</div>
)}
</motion.div>
</TooltipProvider>
)
}