feat: add maximize button to StandardizedToolWrapper
- Add maximize/fullscreen button to standardized header bar - Create useMaximize hook for shape utils to enable fullscreen - Shape fills viewport when maximized, restores on Esc or toggle - Implement on ChatBoxShapeUtil as example (other shapes can add easily) - Button shows ⤢ for maximize, ⊡ for exit fullscreen 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
89289dc5c8
commit
fd7c015b9e
|
|
@ -44,6 +44,10 @@ export interface StandardizedToolWrapperProps {
|
|||
onMinimize?: () => void
|
||||
/** Whether the tool is minimized */
|
||||
isMinimized?: boolean
|
||||
/** Callback when maximize button is clicked */
|
||||
onMaximize?: () => void
|
||||
/** Whether the tool is maximized (fullscreen) */
|
||||
isMaximized?: boolean
|
||||
/** Optional custom header content */
|
||||
headerContent?: ReactNode
|
||||
/** Editor instance for shape selection */
|
||||
|
|
@ -76,6 +80,8 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
|
|||
onClose,
|
||||
onMinimize,
|
||||
isMinimized = false,
|
||||
onMaximize,
|
||||
isMaximized = false,
|
||||
headerContent,
|
||||
editor,
|
||||
shapeId,
|
||||
|
|
@ -91,6 +97,22 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
|
|||
const tagInputRef = useRef<HTMLInputElement>(null)
|
||||
const isDarkMode = useIsDarkMode()
|
||||
|
||||
// Handle Esc key to exit maximize mode
|
||||
useEffect(() => {
|
||||
if (!isMaximized || !onMaximize) return
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
onMaximize()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown, true)
|
||||
return () => window.removeEventListener('keydown', handleKeyDown, true)
|
||||
}, [isMaximized, onMaximize])
|
||||
|
||||
// Dark mode aware colors
|
||||
const colors = useMemo(() => isDarkMode ? {
|
||||
contentBg: '#1a1a1a',
|
||||
|
|
@ -243,6 +265,16 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
|
|||
color: isSelected ? 'white' : primaryColor,
|
||||
}
|
||||
|
||||
const maximizeButtonStyle: React.CSSProperties = {
|
||||
...buttonBaseStyle,
|
||||
backgroundColor: isMaximized
|
||||
? (isSelected ? 'rgba(255,255,255,0.4)' : primaryColor)
|
||||
: (isSelected ? 'rgba(255,255,255,0.2)' : `${primaryColor}20`),
|
||||
color: isMaximized
|
||||
? (isSelected ? 'white' : 'white')
|
||||
: (isSelected ? 'white' : primaryColor),
|
||||
}
|
||||
|
||||
const contentStyle: React.CSSProperties = {
|
||||
width: '100%',
|
||||
height: isMinimized ? 0 : 'calc(100% - 40px)',
|
||||
|
|
@ -488,6 +520,20 @@ export const StandardizedToolWrapper: React.FC<StandardizedToolWrapperProps> = (
|
|||
>
|
||||
_
|
||||
</button>
|
||||
{onMaximize && (
|
||||
<button
|
||||
style={maximizeButtonStyle}
|
||||
onClick={(e) => handleButtonClick(e, onMaximize)}
|
||||
onPointerDown={(e) => e.stopPropagation()}
|
||||
onMouseDown={(e) => e.stopPropagation()}
|
||||
onTouchStart={(e) => handleButtonTouch(e, onMaximize)}
|
||||
onTouchEnd={(e) => e.stopPropagation()}
|
||||
title={isMaximized ? "Exit fullscreen (Esc)" : "Maximize"}
|
||||
aria-label={isMaximized ? "Exit fullscreen" : "Maximize"}
|
||||
>
|
||||
{isMaximized ? '⊡' : '⤢'}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
style={closeButtonStyle}
|
||||
onClick={(e) => handleButtonClick(e, onClose)}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
import { useState, useCallback, useRef, useEffect } from 'react'
|
||||
import { Editor } from 'tldraw'
|
||||
|
||||
interface OriginalDimensions {
|
||||
x: number
|
||||
y: number
|
||||
w: number
|
||||
h: number
|
||||
}
|
||||
|
||||
interface UseMaximizeOptions {
|
||||
/** Editor instance */
|
||||
editor: Editor
|
||||
/** Shape ID to maximize */
|
||||
shapeId: string
|
||||
/** Current width of the shape */
|
||||
currentW: number
|
||||
/** Current height of the shape */
|
||||
currentH: number
|
||||
/** Shape type for updateShape call */
|
||||
shapeType: string
|
||||
/** Padding from viewport edges in pixels */
|
||||
padding?: number
|
||||
}
|
||||
|
||||
interface UseMaximizeReturn {
|
||||
/** Whether the shape is currently maximized */
|
||||
isMaximized: boolean
|
||||
/** Toggle maximize state */
|
||||
toggleMaximize: () => void
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to enable maximize/fullscreen functionality for shapes.
|
||||
* When maximized, the shape fills the viewport.
|
||||
* Press Esc or click maximize again to restore original size.
|
||||
*/
|
||||
export function useMaximize({
|
||||
editor,
|
||||
shapeId,
|
||||
currentW,
|
||||
currentH,
|
||||
shapeType,
|
||||
padding = 40,
|
||||
}: UseMaximizeOptions): UseMaximizeReturn {
|
||||
const [isMaximized, setIsMaximized] = useState(false)
|
||||
const originalDimensionsRef = useRef<OriginalDimensions | null>(null)
|
||||
|
||||
const toggleMaximize = useCallback(() => {
|
||||
if (!editor || !shapeId) return
|
||||
|
||||
const shape = editor.getShape(shapeId)
|
||||
if (!shape) return
|
||||
|
||||
if (isMaximized) {
|
||||
// Restore original dimensions
|
||||
const original = originalDimensionsRef.current
|
||||
if (original) {
|
||||
editor.updateShape({
|
||||
id: shapeId,
|
||||
type: shapeType,
|
||||
x: original.x,
|
||||
y: original.y,
|
||||
props: {
|
||||
w: original.w,
|
||||
h: original.h,
|
||||
},
|
||||
})
|
||||
}
|
||||
originalDimensionsRef.current = null
|
||||
setIsMaximized(false)
|
||||
} else {
|
||||
// Store current dimensions before maximizing
|
||||
originalDimensionsRef.current = {
|
||||
x: shape.x,
|
||||
y: shape.y,
|
||||
w: currentW,
|
||||
h: currentH,
|
||||
}
|
||||
|
||||
// Get viewport bounds in page coordinates
|
||||
const viewportBounds = editor.getViewportPageBounds()
|
||||
|
||||
// Calculate new dimensions to fill viewport with padding
|
||||
const newX = viewportBounds.x + padding
|
||||
const newY = viewportBounds.y + padding
|
||||
const newW = viewportBounds.width - (padding * 2)
|
||||
const newH = viewportBounds.height - (padding * 2)
|
||||
|
||||
editor.updateShape({
|
||||
id: shapeId,
|
||||
type: shapeType,
|
||||
x: newX,
|
||||
y: newY,
|
||||
props: {
|
||||
w: newW,
|
||||
h: newH,
|
||||
},
|
||||
})
|
||||
|
||||
// Center the view on the maximized shape
|
||||
editor.centerOnPoint({ x: newX + newW / 2, y: newY + newH / 2 })
|
||||
|
||||
setIsMaximized(true)
|
||||
}
|
||||
}, [editor, shapeId, shapeType, currentW, currentH, padding, isMaximized])
|
||||
|
||||
// Clean up when shape is deleted or unmounted
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
originalDimensionsRef.current = null
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Reset maximize state if shape dimensions change externally while maximized
|
||||
useEffect(() => {
|
||||
if (isMaximized && originalDimensionsRef.current) {
|
||||
const shape = editor.getShape(shapeId)
|
||||
if (!shape) {
|
||||
setIsMaximized(false)
|
||||
originalDimensionsRef.current = null
|
||||
}
|
||||
}
|
||||
}, [editor, shapeId, isMaximized])
|
||||
|
||||
return {
|
||||
isMaximized,
|
||||
toggleMaximize,
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react"
|
|||
import { BaseBoxShapeUtil, TLBaseShape, HTMLContainer } from "tldraw"
|
||||
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
|
||||
import { usePinnedToView } from "../hooks/usePinnedToView"
|
||||
import { useMaximize } from "../hooks/useMaximize"
|
||||
|
||||
export type IChatBoxShape = TLBaseShape<
|
||||
"ChatBox",
|
||||
|
|
@ -43,6 +44,15 @@ export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
|
|||
// Use the pinning hook to keep the shape fixed to viewport when pinned
|
||||
usePinnedToView(this.editor, shape.id, shape.props.pinnedToView)
|
||||
|
||||
// Use the maximize hook for fullscreen functionality
|
||||
const { isMaximized, toggleMaximize } = useMaximize({
|
||||
editor: this.editor,
|
||||
shapeId: shape.id,
|
||||
currentW: shape.props.w,
|
||||
currentH: shape.props.h,
|
||||
shapeType: 'ChatBox',
|
||||
})
|
||||
|
||||
const handleClose = () => {
|
||||
this.editor.deleteShape(shape.id)
|
||||
}
|
||||
|
|
@ -73,6 +83,8 @@ export class ChatBoxShape extends BaseBoxShapeUtil<IChatBoxShape> {
|
|||
onClose={handleClose}
|
||||
onMinimize={handleMinimize}
|
||||
isMinimized={isMinimized}
|
||||
onMaximize={toggleMaximize}
|
||||
isMaximized={isMaximized}
|
||||
editor={this.editor}
|
||||
shapeId={shape.id}
|
||||
isPinnedToView={shape.props.pinnedToView}
|
||||
|
|
|
|||
Loading…
Reference in New Issue