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:
Jeff Emmett 2025-12-08 00:51:23 -08:00
parent 89289dc5c8
commit fd7c015b9e
3 changed files with 188 additions and 0 deletions

View File

@ -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)}

130
src/hooks/useMaximize.ts Normal file
View File

@ -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,
}
}

View File

@ -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}