import { useState, useEffect } from "react" import { BaseBoxShapeUtil, TLBaseShape, HTMLContainer, TLShapeId } from "tldraw" import { usePinnedToView } from "../hooks/usePinnedToView" export type IPrivateWorkspaceShape = TLBaseShape< "PrivateWorkspace", { w: number h: number pinnedToView: boolean isCollapsed: boolean } > // Storage key for persisting workspace position/size const STORAGE_KEY = 'private-workspace-state' interface WorkspaceState { x: number y: number w: number h: number } function saveWorkspaceState(state: WorkspaceState) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)) } catch (e) { console.warn('Failed to save workspace state:', e) } } function loadWorkspaceState(): WorkspaceState | null { try { const stored = localStorage.getItem(STORAGE_KEY) if (stored) { return JSON.parse(stored) } } catch (e) { console.warn('Failed to load workspace state:', e) } return null } export class PrivateWorkspaceShape extends BaseBoxShapeUtil { static override type = "PrivateWorkspace" as const // Privacy zone color: Indigo static readonly PRIMARY_COLOR = "#6366f1" getDefaultProps(): IPrivateWorkspaceShape["props"] { const saved = loadWorkspaceState() return { w: saved?.w ?? 400, h: saved?.h ?? 500, pinnedToView: false, isCollapsed: false, } } override canResize() { return true } override canBind() { return false } indicator(shape: IPrivateWorkspaceShape) { return ( ) } component(shape: IPrivateWorkspaceShape) { const isSelected = this.editor.getSelectedShapeIds().includes(shape.id) // Use the pinning hook to keep the shape fixed to viewport when pinned usePinnedToView(this.editor, shape.id, shape.props.pinnedToView) // Save position/size when shape changes useEffect(() => { const shapeData = this.editor.getShape(shape.id) if (shapeData) { saveWorkspaceState({ x: shapeData.x, y: shapeData.y, w: shape.props.w, h: shape.props.h, }) } }, [shape.props.w, shape.props.h, shape.id]) const handleClose = () => { this.editor.deleteShape(shape.id) } const handlePinToggle = () => { this.editor.updateShape({ id: shape.id, type: shape.type, props: { ...shape.props, pinnedToView: !shape.props.pinnedToView, }, }) } const handleCollapse = () => { this.editor.updateShape({ id: shape.id, type: shape.type, props: { ...shape.props, isCollapsed: !shape.props.isCollapsed, }, }) } // Detect dark mode const isDarkMode = typeof document !== 'undefined' && document.documentElement.classList.contains('dark') const colors = isDarkMode ? { bg: 'rgba(99, 102, 241, 0.12)', headerBg: 'rgba(99, 102, 241, 0.25)', border: 'rgba(99, 102, 241, 0.4)', text: '#e4e4e7', textMuted: '#a1a1aa', btnHover: 'rgba(255, 255, 255, 0.1)', } : { bg: 'rgba(99, 102, 241, 0.06)', headerBg: 'rgba(99, 102, 241, 0.15)', border: 'rgba(99, 102, 241, 0.3)', text: '#3730a3', textMuted: '#6366f1', btnHover: 'rgba(99, 102, 241, 0.1)', } const collapsedHeight = 44 return (
{/* Header */}
{ // Allow dragging from header e.stopPropagation() }} >
🔒 Private Workspace
{/* Pin button */} {/* Collapse button */} {/* Close button */}
{/* Content area */} {!shape.props.isCollapsed && (
🔐

Drop items here to keep them private

Encrypted in your browser • Only you can see these

)} {/* Footer hint */} {!shape.props.isCollapsed && (
Drag items outside to share with collaborators
)}
) } } // Helper function to check if a shape is inside the private workspace export function isShapeInPrivateWorkspace( editor: any, shapeId: TLShapeId, workspaceId: TLShapeId ): boolean { const shape = editor.getShape(shapeId) const workspace = editor.getShape(workspaceId) if (!shape || !workspace || workspace.type !== 'PrivateWorkspace') { return false } const shapeBounds = editor.getShapeGeometry(shape).bounds const workspaceBounds = editor.getShapeGeometry(workspace).bounds // Check if shape center is within workspace bounds const shapeCenterX = shape.x + shapeBounds.width / 2 const shapeCenterY = shape.y + shapeBounds.height / 2 return ( shapeCenterX >= workspace.x && shapeCenterX <= workspace.x + workspaceBounds.width && shapeCenterY >= workspace.y && shapeCenterY <= workspace.y + workspaceBounds.height ) } // Helper to find the private workspace shape on the canvas export function findPrivateWorkspace(editor: any): IPrivateWorkspaceShape | null { const shapes = editor.getCurrentPageShapes() return shapes.find((s: any) => s.type === 'PrivateWorkspace') || null }