feat(rithmomachia): show real preview layout when dragging guide to dock

Replace ghost panel overlay with actual docked layout preview:
- Add onDockPreview callback to communicate preview state to parent
- Parent renders real PanelGroup layout when dockPreviewSide is set
- Guide appears in docked position showing real final layout
- Board resizes automatically to show what docked state will look like
- Dragging modal shown at 0.8 opacity during preview
- Both guide (in panel) and dragging modal visible simultaneously
- Clear preview when drag ends or moves away from edges

This provides much better visual feedback - user sees exactly what
the final docked layout will look like before releasing the drag.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock 2025-11-02 11:02:44 -06:00
parent 34cd3247e5
commit 17d2460a87
2 changed files with 33 additions and 40 deletions

View File

@ -19,6 +19,7 @@ interface PlayingGuideModalProps {
docked?: boolean // True when docked to side
onDock?: (side: 'left' | 'right') => void
onUndock?: () => void
onDockPreview?: (side: 'left' | 'right' | null) => void // Preview docking without committing
}
type Section = 'overview' | 'pieces' | 'capture' | 'strategy' | 'harmony' | 'victory'
@ -30,6 +31,7 @@ export function PlayingGuideModal({
docked = false,
onDock,
onUndock,
onDockPreview,
}: PlayingGuideModalProps) {
const t = useTranslations('rithmomachia.guide')
const { data: abacusSettings } = useAbacusSettings()
@ -124,14 +126,17 @@ export function PlayingGuideModal({
})
// Check if we're near edges for docking preview
if (onDock && !docked) {
if (onDock && onDockPreview && !docked) {
const DOCK_THRESHOLD = 100
if (e.clientX < DOCK_THRESHOLD) {
setDockPreview('left')
onDockPreview('left')
} else if (e.clientX > window.innerWidth - DOCK_THRESHOLD) {
setDockPreview('right')
onDockPreview('right')
} else {
setDockPreview(null)
onDockPreview(null)
}
}
} else if (isResizing) {
@ -227,6 +232,9 @@ export function PlayingGuideModal({
setIsResizing(false)
setResizeDirection('')
setDockPreview(null) // Clear dock preview when drag ends
if (onDockPreview) {
onDockPreview(null) // Clear parent preview state
}
}
if (isDragging || isResizing) {
@ -409,8 +417,13 @@ export function PlayingGuideModal({
height: `${size.height}px`,
zIndex: Z_INDEX.MODAL,
}),
// 80% opacity on desktop when not hovered, full opacity otherwise
opacity: !standalone && !docked && window.innerWidth >= 768 && !isHovered ? 0.8 : 1,
// 80% opacity when showing dock preview or when not hovered on desktop
opacity:
dockPreview !== null
? 0.8
: !standalone && !docked && window.innerWidth >= 768 && !isHovered
? 0.8
: 1,
transition: 'opacity 0.2s ease',
}}
onMouseEnter={() => setIsHovered(true)}
@ -696,36 +709,6 @@ export function PlayingGuideModal({
return modalContent
}
// Otherwise, render the modal with optional dock preview
return (
<>
{/* Dock preview ghost panel */}
{dockPreview && !docked && (
<div
style={{
position: 'fixed',
top: 0,
[dockPreview === 'left' ? 'left' : 'right']: 0,
width: '35%',
height: '100%',
background: 'rgba(139, 92, 246, 0.15)',
border: `3px dashed rgba(139, 92, 246, 0.5)`,
borderRadius: 0,
pointerEvents: 'none',
zIndex: 9999,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '48px',
color: 'rgba(139, 92, 246, 0.6)',
fontWeight: 'bold',
transition: 'none',
}}
>
{dockPreview === 'left' ? '←' : '→'}
</div>
)}
{modalContent}
</>
)
// Otherwise, just render the modal (parent will handle preview rendering)
return modalContent
}

View File

@ -41,6 +41,7 @@ export function RithmomachiaGame() {
const [isGuideOpen, setIsGuideOpen] = useState(false)
const [guideDocked, setGuideDocked] = useState(false)
const [guideDockSide, setGuideDockSide] = useState<'left' | 'right'>('right')
const [dockPreviewSide, setDockPreviewSide] = useState<'left' | 'right' | null>(null)
useEffect(() => {
// Register this component's main div as the fullscreen element
@ -97,6 +98,7 @@ export function RithmomachiaGame() {
console.log('[RithmomachiaGame] handleDock called', { side })
setGuideDockSide(side)
setGuideDocked(true)
setDockPreviewSide(null) // Clear preview when committing to dock
console.log('[RithmomachiaGame] Docked state updated', {
guideDocked: true,
guideDockSide: side,
@ -109,6 +111,11 @@ export function RithmomachiaGame() {
console.log('[RithmomachiaGame] Undocked state updated', { guideDocked: false })
}
const handleDockPreview = (side: 'left' | 'right' | null) => {
console.log('[RithmomachiaGame] handleDockPreview called', { side })
setDockPreviewSide(side)
}
const gameContent = (
<div
className={css({
@ -174,16 +181,17 @@ export function RithmomachiaGame() {
overflow: 'hidden',
})}
>
{guideDocked && isGuideOpen ? (
{(guideDocked || dockPreviewSide) && isGuideOpen ? (
<PanelGroup direction="horizontal" style={{ flex: 1 }}>
{guideDockSide === 'left' && (
{(guideDocked ? guideDockSide : dockPreviewSide) === 'left' && (
<>
<Panel defaultSize={35} minSize={20} maxSize={50}>
<PlayingGuideModal
isOpen={true}
onClose={() => setIsGuideOpen(false)}
docked={true}
docked={guideDocked} // Only truly docked if guideDocked is true
onUndock={handleUndock}
onDockPreview={handleDockPreview}
/>
</Panel>
<PanelResizeHandle
@ -200,7 +208,7 @@ export function RithmomachiaGame() {
<Panel minSize={50}>{gameContent}</Panel>
</>
)}
{guideDockSide === 'right' && (
{(guideDocked ? guideDockSide : dockPreviewSide) === 'right' && (
<>
<Panel minSize={50}>{gameContent}</Panel>
<PanelResizeHandle
@ -218,8 +226,9 @@ export function RithmomachiaGame() {
<PlayingGuideModal
isOpen={true}
onClose={() => setIsGuideOpen(false)}
docked={true}
docked={guideDocked} // Only truly docked if guideDocked is true
onUndock={handleUndock}
onDockPreview={handleDockPreview}
/>
</Panel>
</>
@ -238,6 +247,7 @@ export function RithmomachiaGame() {
onClose={() => setIsGuideOpen(false)}
docked={false}
onDock={handleDock}
onDockPreview={handleDockPreview}
/>
)}
</PageWithNav>