/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ /** * TitleBlockEditor - Modal dialog for editing title block fields * * Allows users to: * - Edit field values (project name, drawing number, etc.) * - Add/remove custom fields * - Configure field properties (label, auto-populate) * - Add revision entries */ import React, { useCallback, useState, useMemo } from 'react'; import { Plus, Trash2, Upload, Calendar, Hash, FileText, User } from 'lucide-react'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { useViewerStore } from '@/store'; import type { TitleBlockField, RevisionEntry } from '@ifc-lite/drawing-2d'; interface TitleBlockEditorProps { open: boolean; onOpenChange: (open: boolean) => void; } // Icon mapping for common field types const FIELD_ICONS: Record = { 'project-name': , 'drawing-title': , 'drawing-number': , revision: , scale: , date: , 'drawn-by': , 'checked-by': , 'sheet-number': , }; // Standard field IDs const STANDARD_FIELD_IDS = [ 'project-name', 'drawing-title', 'drawing-number', 'revision', 'scale', 'date', 'drawn-by', 'checked-by', 'sheet-number', ]; export function TitleBlockEditor({ open, onOpenChange }: TitleBlockEditorProps): React.ReactElement { const activeSheet = useViewerStore((s) => s.activeSheet); const updateTitleBlockField = useViewerStore((s) => s.updateTitleBlockField); const addTitleBlockField = useViewerStore((s) => s.addTitleBlockField); const removeTitleBlockField = useViewerStore((s) => s.removeTitleBlockField); const setTitleBlockLogo = useViewerStore((s) => s.setTitleBlockLogo); const addRevision = useViewerStore((s) => s.addRevision); const removeRevision = useViewerStore((s) => s.removeRevision); // Local state for new field form const [newFieldLabel, setNewFieldLabel] = useState(''); const [showNewFieldForm, setShowNewFieldForm] = useState(false); // Local state for new revision form const [newRevision, setNewRevision] = useState({ revision: '', date: new Date().toLocaleDateString(), description: '', author: '', }); const [showRevisionForm, setShowRevisionForm] = useState(false); // Group fields by category const fieldGroups = useMemo(() => { if (!activeSheet) return { standard: [], custom: [] }; const standard: TitleBlockField[] = []; const custom: TitleBlockField[] = []; for (const field of activeSheet.titleBlock.fields) { if (STANDARD_FIELD_IDS.includes(field.id)) { standard.push(field); } else { custom.push(field); } } return { standard, custom }; }, [activeSheet]); // Handle field value change const handleFieldChange = useCallback((fieldId: string, value: string) => { updateTitleBlockField(fieldId, value); }, [updateTitleBlockField]); // Add new custom field const handleAddField = useCallback(() => { if (!newFieldLabel.trim()) return; // Find max row from existing fields const maxRow = activeSheet?.titleBlock.fields.reduce( (max, f) => Math.max(max, f.row ?? 0), 0 ) ?? 0; const newField: TitleBlockField = { id: `custom-${Date.now()}`, label: newFieldLabel.trim(), value: '', editable: true, autoPopulate: false, fontSize: 3, fontWeight: 'normal', row: maxRow + 1, col: 0, colSpan: 2, }; addTitleBlockField(newField); setNewFieldLabel(''); setShowNewFieldForm(false); }, [newFieldLabel, activeSheet, addTitleBlockField]); // Handle logo upload const handleLogoUpload = useCallback((e: React.ChangeEvent) => { const file = e.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = (event) => { const dataUrl = event.target?.result as string; setTitleBlockLogo({ source: dataUrl, widthMm: 30, heightMm: 15, position: 'top-left', }); }; reader.readAsDataURL(file); }, [setTitleBlockLogo]); // Add revision entry const handleAddRevision = useCallback(() => { if (!newRevision.revision || !newRevision.description) return; addRevision({ revision: newRevision.revision, date: newRevision.date || new Date().toLocaleDateString(), description: newRevision.description, author: newRevision.author, }); setNewRevision({ revision: '', date: new Date().toLocaleDateString(), description: '', author: '', }); setShowRevisionForm(false); }, [newRevision, addRevision]); if (!activeSheet) return <>; return ( Edit Title Block
{/* Standard Fields */}

Standard Fields

{fieldGroups.standard.map((field) => (
{FIELD_ICONS[field.id] || } {field.label}
handleFieldChange(field.id, e.target.value)} placeholder={`Enter ${field.label.toLowerCase()}...`} className="h-8" disabled={!field.editable} />
))}
{/* Custom Fields */}

Custom Fields

{showNewFieldForm && (
setNewFieldLabel(e.target.value)} placeholder="Field label..." className="h-8 flex-1" autoFocus />
)} {fieldGroups.custom.length === 0 && !showNewFieldForm ? (
No custom fields yet
) : (
{fieldGroups.custom.map((field) => (
{field.label} handleFieldChange(field.id, e.target.value)} placeholder={`Enter ${field.label.toLowerCase()}...`} className="h-8" />
))}
)}
{/* Logo */}

Company Logo

{activeSheet.titleBlock.logo ? (
Logo
) : (
PNG, JPG, or SVG
)}
{/* Revisions */}

Revision History

{showRevisionForm && (
setNewRevision(prev => ({ ...prev, revision: e.target.value }))} placeholder="A, B, 01..." className="h-8 mt-1" />
setNewRevision(prev => ({ ...prev, date: e.target.value }))} placeholder="2024-01-15" className="h-8 mt-1" />
setNewRevision(prev => ({ ...prev, description: e.target.value }))} placeholder="Description of changes..." className="h-8 mt-1" />
setNewRevision(prev => ({ ...prev, author: e.target.value }))} placeholder="Initials..." className="h-8 mt-1" />
)} {activeSheet.revisions.length === 0 && !showRevisionForm ? (
No revisions yet
) : (
{activeSheet.revisions.map((rev, index) => (
{rev.revision} {rev.date} {rev.description} {rev.author && by {rev.author}}
))}
)}
); }