Compare commits

...

2 Commits

Author SHA1 Message Date
Jeff Emmett b4c4d36e56 feat: add maximize button to all tool shapes
Add useMaximize hook to all shapes using StandardizedToolWrapper:
- MapShapeUtil, MultmuxShapeUtil, MarkdownShapeUtil
- ObsNoteShapeUtil, ImageGenShapeUtil, VideoGenShapeUtil
- HolonShapeUtil, PromptShapeUtil, EmbedShapeUtil
- FathomMeetingsBrowserShapeUtil, FathomNoteShapeUtil
- HolonBrowserShapeUtil, ObsidianBrowserShapeUtil
- TranscriptionShapeUtil, VideoChatShapeUtil

All tools now have maximize/fullscreen functionality via the
standardized header bar.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-08 01:02:57 -08:00
Jeff Emmett b63491f19a Create task task-046 2025-12-08 00:51:43 -08:00
16 changed files with 210 additions and 1 deletions

View File

@ -0,0 +1,19 @@
---
id: task-046
title: Add maximize button to StandardizedToolWrapper
status: Done
assignee: []
created_date: '2025-12-08 08:51'
labels:
- feature
- ui
- shapes
dependencies: []
priority: medium
---
## Description
<!-- SECTION:DESCRIPTION:BEGIN -->
Added a maximize/fullscreen button to the standardized header bar. When clicked, the tool fills the viewport. Press Esc or click again to restore original dimensions. Created useMaximize hook that shape utils can use. Implemented on ChatBoxShapeUtil as example.
<!-- SECTION:DESCRIPTION:END -->

View File

@ -2,6 +2,7 @@ import { BaseBoxShapeUtil, TLBaseShape, HTMLContainer } from "tldraw"
import { useCallback, useState } from "react"
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
import { usePinnedToView } from "../hooks/usePinnedToView"
import { useMaximize } from "../hooks/useMaximize"
export type IEmbedShape = TLBaseShape<
"Embed",
@ -173,6 +174,15 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
// Use the pinning hook
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: 'Embed',
})
const handleClose = () => {
this.editor.deleteShape(shape.id)
}
@ -271,6 +281,8 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}
@ -361,6 +373,8 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}
@ -429,6 +443,8 @@ export class EmbedShape extends BaseBoxShapeUtil<IEmbedShape> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -9,6 +9,7 @@ import React, { useState, useContext } from "react"
import { FathomMeetingsPanel } from "../components/FathomMeetingsPanel"
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
import { usePinnedToView } from "../hooks/usePinnedToView"
import { useMaximize } from "../hooks/useMaximize"
import { FathomNoteShape } from "./FathomNoteShapeUtil"
import { WORKER_URL, LOCAL_WORKER_URL } from "../constants/workerUrl"
import { getFathomApiKey } from "../lib/fathomApiKey"
@ -48,6 +49,15 @@ export class FathomMeetingsBrowserShape extends BaseBoxShapeUtil<IFathomMeetings
// 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: w,
currentH: h,
shapeType: 'FathomMeetingsBrowser',
})
const handleClose = () => {
setIsOpen(false)
// Delete the browser shape immediately so it's tracked in undo/redo history
@ -518,6 +528,8 @@ export class FathomMeetingsBrowserShape extends BaseBoxShapeUtil<IFathomMeetings
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -3,6 +3,7 @@ import { BaseBoxShapeUtil, TLBaseShape, createShapeId, IndexKey, TLParentId, HTM
import type { JSX } from 'react'
import { StandardizedToolWrapper } from '../components/StandardizedToolWrapper'
import { usePinnedToView } from '../hooks/usePinnedToView'
import { useMaximize } from '../hooks/useMaximize'
export type IFathomNoteShape = TLBaseShape<
'FathomNote',
@ -45,6 +46,15 @@ export class FathomNoteShape extends BaseBoxShapeUtil<IFathomNoteShape> {
// Use the pinning hook
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: 'FathomNote',
})
const handleClose = () => {
this.editor.deleteShape(shape.id)
}
@ -513,6 +523,8 @@ export class FathomNoteShape extends BaseBoxShapeUtil<IFathomNoteShape> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -8,6 +8,7 @@ import { HolonBrowser } from "../components/HolonBrowser"
import { HolonData } from "../lib/HoloSphereService"
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
import { usePinnedToView } from "../hooks/usePinnedToView"
import { useMaximize } from "../hooks/useMaximize"
type IHolonBrowser = TLBaseShape<
"HolonBrowser",
@ -43,6 +44,15 @@ export class HolonBrowserShape extends BaseBoxShapeUtil<IHolonBrowser> {
// 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: w,
currentH: h,
shapeType: 'HolonBrowser',
})
const handleSelectHolon = (holonData: HolonData) => {
// Store current camera position to prevent it from changing
const currentCamera = this.editor.getCamera()
@ -146,6 +156,8 @@ export class HolonBrowserShape extends BaseBoxShapeUtil<IHolonBrowser> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -8,6 +8,7 @@ import { holosphereService, HoloSphereService, HolonConnection } from "@/lib/Hol
import * as h3 from 'h3-js'
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
import { usePinnedToView } from "../hooks/usePinnedToView"
import { useMaximize } from "../hooks/useMaximize"
type IHolon = TLBaseShape<
"Holon",
@ -112,6 +113,15 @@ export class HolonShape extends BaseBoxShapeUtil<IHolon> {
// 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: 'Holon',
})
// Note: Auto-initialization is disabled. Users must manually enter Holon IDs.
// This prevents the shape from auto-generating IDs based on coordinates.
@ -763,6 +773,8 @@ export class HolonShape extends BaseBoxShapeUtil<IHolon> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
headerContent={headerContent}
editor={this.editor}
shapeId={shape.id}

View File

@ -10,6 +10,7 @@ import { getRunPodConfig } from "@/lib/clientConfig"
import { aiOrchestrator, isAIOrchestratorAvailable } from "@/lib/aiOrchestrator"
import { StandardizedToolWrapper } from "@/components/StandardizedToolWrapper"
import { usePinnedToView } from "@/hooks/usePinnedToView"
import { useMaximize } from "@/hooks/useMaximize"
// Feature flag: Set to false when AI Orchestrator or RunPod API is ready for production
const USE_MOCK_API = false
@ -326,6 +327,15 @@ export class ImageGenShape extends BaseBoxShapeUtil<IImageGen> {
// Pin to view functionality
usePinnedToView(editor, shape.id, shape.props.pinnedToView)
// Use the maximize hook for fullscreen functionality
const { isMaximized, toggleMaximize } = useMaximize({
editor: editor,
shapeId: shape.id,
currentW: shape.props.w,
currentH: shape.props.h,
shapeType: 'ImageGen',
})
const handlePinToggle = () => {
editor.updateShape<IImageGen>({
id: shape.id,
@ -589,6 +599,8 @@ export class ImageGenShape extends BaseBoxShapeUtil<IImageGen> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={editor}
shapeId={shape.id}
tags={shape.props.tags || []}

View File

@ -19,6 +19,8 @@ import { useRef, useEffect, useState, useCallback } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { StandardizedToolWrapper } from '../components/StandardizedToolWrapper';
import { usePinnedToView } from '../hooks/usePinnedToView';
import { useMaximize } from '../hooks/useMaximize';
// =============================================================================
// Types
@ -344,6 +346,18 @@ function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor:
const styleKey = (shape.props.styleKey || 'voyager') as StyleKey;
const currentStyle = MAP_STYLES[styleKey] || MAP_STYLES.voyager;
// Use the pinning hook to keep the shape fixed to viewport when pinned
usePinnedToView(editor, shape.id, shape.props.pinnedToView);
// Use the maximize hook for fullscreen functionality
const { isMaximized, toggleMaximize } = useMaximize({
editor: editor,
shapeId: shape.id,
currentW: shape.props.w,
currentH: shape.props.h,
shapeType: 'Map',
});
// Track mounted state for cleanup
useEffect(() => {
isMountedRef.current = true;
@ -908,6 +922,8 @@ function MapComponent({ shape, editor, isSelected }: { shape: IMapShape; editor:
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={shape.props.isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -28,6 +28,7 @@ import '@mdxeditor/editor/style.css'
import { BaseBoxShapeUtil, TLBaseShape, HTMLContainer } from '@tldraw/tldraw'
import { StandardizedToolWrapper } from '../components/StandardizedToolWrapper'
import { usePinnedToView } from '../hooks/usePinnedToView'
import { useMaximize } from '../hooks/useMaximize'
export type IMarkdownShape = TLBaseShape<
'Markdown',
@ -89,6 +90,15 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
// Use the pinning hook
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: 'Markdown',
})
const handleClose = () => {
this.editor.deleteShape(shape.id)
}
@ -140,6 +150,8 @@ export class MarkdownShape extends BaseBoxShapeUtil<IMarkdownShape> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -2,6 +2,7 @@ import React, { useState, useEffect, useRef } from 'react'
import { BaseBoxShapeUtil, TLBaseShape, HTMLContainer, Geometry2d, Rectangle2d, T, createShapePropsMigrationIds, createShapePropsMigrationSequence } from 'tldraw'
import { StandardizedToolWrapper } from '../components/StandardizedToolWrapper'
import { usePinnedToView } from '../hooks/usePinnedToView'
import { useMaximize } from '../hooks/useMaximize'
import { Terminal } from '@xterm/xterm'
import { FitAddon } from '@xterm/addon-fit'
import '@xterm/xterm/css/xterm.css'
@ -158,6 +159,15 @@ export class MultmuxShape extends BaseBoxShapeUtil<IMultmuxShape> {
// Use the pinning hook
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: 'Multmux',
})
// Runtime fix: correct old serverUrl port (3000 -> 3002)
// This handles shapes that may not have been migrated yet
useEffect(() => {
@ -511,6 +521,8 @@ export class MultmuxShape extends BaseBoxShapeUtil<IMultmuxShape> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}
@ -718,6 +730,8 @@ export class MultmuxShape extends BaseBoxShapeUtil<IMultmuxShape> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -32,6 +32,7 @@ import { logGitHubSetupStatus } from '@/lib/githubSetupValidator'
import { getClientConfig } from '@/lib/clientConfig'
import { StandardizedToolWrapper } from '../components/StandardizedToolWrapper'
import { usePinnedToView } from '../hooks/usePinnedToView'
import { useMaximize } from '../hooks/useMaximize'
// Main ObsNote component with full markdown editing
const ObsNoteComponent: React.FC<{
@ -50,6 +51,15 @@ const ObsNoteComponent: React.FC<{
// Use the pinning hook to keep the shape fixed to viewport when pinned
usePinnedToView(shapeUtil.editor, shape.id, shape.props.pinnedToView)
// Use the maximize hook for fullscreen functionality
const { isMaximized, toggleMaximize } = useMaximize({
editor: shapeUtil.editor,
shapeId: shape.id,
currentW: shape.props.w,
currentH: shape.props.h,
shapeType: 'ObsNote',
})
// Track content changes for sync button visibility
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(shape.props.isModified)
@ -486,6 +496,8 @@ ${contentToSync}`
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
headerContent={headerContent}
editor={shapeUtil.editor}
shapeId={shape.id}

View File

@ -12,6 +12,7 @@ import { findNonOverlappingPosition } from "@/utils/shapeCollisionUtils"
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
import { AuthContext } from "../context/AuthContext"
import { usePinnedToView } from "../hooks/usePinnedToView"
import { useMaximize } from "../hooks/useMaximize"
type IObsidianBrowser = TLBaseShape<
"ObsidianBrowser",
@ -46,7 +47,16 @@ export class ObsidianBrowserShape extends BaseBoxShapeUtil<IObsidianBrowser> {
// 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: w,
currentH: h,
shapeType: 'ObsidianBrowser',
})
// Wrapper component to access auth context
const ObsidianBrowserContent: React.FC<{ vaultName?: string }> = ({ vaultName }) => {
@ -342,6 +352,8 @@ export class ObsidianBrowserShape extends BaseBoxShapeUtil<IObsidianBrowser> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
headerContent={headerContent}

View File

@ -16,6 +16,7 @@ import { findNonOverlappingPosition } from "@/utils/shapeCollisionUtils"
import React, { useState } from "react"
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
import { usePinnedToView } from "../hooks/usePinnedToView"
import { useMaximize } from "../hooks/useMaximize"
type IPrompt = TLBaseShape<
"Prompt",
@ -373,6 +374,15 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
// Use the pinning hook
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: 'Prompt',
})
const handleClose = () => {
this.editor.deleteShape(shape.id)
}
@ -403,6 +413,8 @@ export class PromptShape extends BaseBoxShapeUtil<IPrompt> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -8,6 +8,7 @@ import { useWhisperTranscription } from "../hooks/useWhisperTranscriptionSimple"
import { useWebSpeechTranscription } from "../hooks/useWebSpeechTranscription"
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
import { usePinnedToView } from "../hooks/usePinnedToView"
import { useMaximize } from "../hooks/useMaximize"
type ITranscription = TLBaseShape<
"Transcription",
@ -102,6 +103,15 @@ export class TranscriptionShape extends BaseBoxShapeUtil<ITranscription> {
// 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: w,
currentH: h,
shapeType: 'Transcription',
})
// Local Whisper model is always available (no API key needed)
const isLocalWhisperAvailable = true
@ -682,6 +692,8 @@ export class TranscriptionShape extends BaseBoxShapeUtil<ITranscription> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
headerContent={headerContent}
editor={this.editor}
shapeId={shape.id}

View File

@ -3,6 +3,7 @@ import { useEffect, useState } from "react"
import { WORKER_URL } from "../constants/workerUrl"
import { StandardizedToolWrapper } from "../components/StandardizedToolWrapper"
import { usePinnedToView } from "../hooks/usePinnedToView"
import { useMaximize } from "../hooks/useMaximize"
interface DailyApiResponse {
url: string;
@ -464,6 +465,15 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
// 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: 'VideoChat',
})
if (error) {
return <div>Error creating room: {error.message}</div>
}
@ -562,6 +572,8 @@ export class VideoChatShape extends BaseBoxShapeUtil<IVideoChatShape> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={this.editor}
shapeId={shape.id}
isPinnedToView={shape.props.pinnedToView}

View File

@ -9,6 +9,7 @@ import React, { useState, useRef, useEffect } from "react"
import { getRunPodVideoConfig } from "@/lib/clientConfig"
import { StandardizedToolWrapper } from "@/components/StandardizedToolWrapper"
import { usePinnedToView } from "@/hooks/usePinnedToView"
import { useMaximize } from "@/hooks/useMaximize"
// Type for RunPod job response
interface RunPodJobResponse {
@ -105,6 +106,15 @@ export class VideoGenShape extends BaseBoxShapeUtil<IVideoGen> {
// Pin to view functionality
usePinnedToView(editor, shape.id, shape.props.pinnedToView)
// Use the maximize hook for fullscreen functionality
const { isMaximized, toggleMaximize } = useMaximize({
editor: editor,
shapeId: shape.id,
currentW: shape.props.w,
currentH: shape.props.h,
shapeType: 'VideoGen',
})
const handlePinToggle = () => {
editor.updateShape<IVideoGen>({
id: shape.id,
@ -387,6 +397,8 @@ export class VideoGenShape extends BaseBoxShapeUtil<IVideoGen> {
onClose={handleClose}
onMinimize={handleMinimize}
isMinimized={isMinimized}
onMaximize={toggleMaximize}
isMaximized={isMaximized}
editor={editor}
shapeId={shape.id}
tags={shape.props.tags}