Features: - Display welcome popup on first visit - Auto-detect browser language (Chinese/English) - Show author motivation (Usher syndrome research support) - Privacy policy: no data collection, browser-only operations - Use localStorage to remember dismissal - Responsive design for mobile devices 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
105 lines
3.2 KiB
TypeScript
105 lines
3.2 KiB
TypeScript
/**
|
|
* App Component
|
|
*
|
|
* Main application layout
|
|
*/
|
|
|
|
import { useEffect, useState } from 'react';
|
|
import { PedigreeCanvas } from '../PedigreeCanvas/PedigreeCanvas';
|
|
import { Toolbar } from '../Toolbar/Toolbar';
|
|
import { PropertyPanel } from '../PropertyPanel/PropertyPanel';
|
|
import { RelationshipPanel } from '../RelationshipPanel/RelationshipPanel';
|
|
import { FilePanel } from '../FilePanel/FilePanel';
|
|
import { WelcomeModal } from '../WelcomeModal/WelcomeModal';
|
|
import { usePedigreeStore, useTemporalStore } from '@/store/pedigreeStore';
|
|
import styles from './App.module.css';
|
|
|
|
const WELCOME_DISMISSED_KEY = 'pedigree-draw-welcome-dismissed';
|
|
|
|
export function App() {
|
|
const {
|
|
clearSelection,
|
|
selectedPersonId,
|
|
selectedRelationshipId,
|
|
deletePerson,
|
|
deleteRelationship,
|
|
} = usePedigreeStore();
|
|
const temporal = useTemporalStore();
|
|
|
|
// Welcome modal state - check localStorage on init
|
|
const [showWelcome, setShowWelcome] = useState(() => {
|
|
return localStorage.getItem(WELCOME_DISMISSED_KEY) !== 'true';
|
|
});
|
|
|
|
const handleCloseWelcome = () => {
|
|
setShowWelcome(false);
|
|
};
|
|
|
|
// Keyboard shortcuts
|
|
useEffect(() => {
|
|
const handleKeyDown = (e: KeyboardEvent) => {
|
|
// Don't handle shortcuts when typing in inputs
|
|
const target = e.target as HTMLElement;
|
|
if (target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.tagName === 'SELECT') {
|
|
return;
|
|
}
|
|
|
|
// Undo: Ctrl/Cmd + Z
|
|
if ((e.ctrlKey || e.metaKey) && e.key === 'z' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
temporal.getState().undo();
|
|
}
|
|
|
|
// Redo: Ctrl/Cmd + Shift + Z or Ctrl/Cmd + Y
|
|
if ((e.ctrlKey || e.metaKey) && (e.key === 'y' || (e.key === 'z' && e.shiftKey))) {
|
|
e.preventDefault();
|
|
temporal.getState().redo();
|
|
}
|
|
|
|
// Escape: Clear selection
|
|
if (e.key === 'Escape') {
|
|
clearSelection();
|
|
}
|
|
|
|
// Delete or Backspace: Delete selected element
|
|
if (e.key === 'Delete' || e.key === 'Backspace') {
|
|
e.preventDefault();
|
|
if (selectedPersonId) {
|
|
deletePerson(selectedPersonId);
|
|
} else if (selectedRelationshipId) {
|
|
deleteRelationship(selectedRelationshipId);
|
|
}
|
|
}
|
|
};
|
|
|
|
window.addEventListener('keydown', handleKeyDown);
|
|
return () => window.removeEventListener('keydown', handleKeyDown);
|
|
}, [clearSelection, selectedPersonId, selectedRelationshipId, deletePerson, deleteRelationship, temporal]);
|
|
|
|
return (
|
|
<div className={styles.app}>
|
|
<header className={styles.header}>
|
|
<h1 className={styles.title}>Pedigree Draw</h1>
|
|
<span className={styles.subtitle}>Professional Pedigree Chart Editor</span>
|
|
</header>
|
|
|
|
<Toolbar />
|
|
|
|
<main className={styles.main}>
|
|
<FilePanel />
|
|
<div className={styles.canvasArea}>
|
|
<PedigreeCanvas />
|
|
</div>
|
|
{selectedRelationshipId ? <RelationshipPanel /> : <PropertyPanel />}
|
|
</main>
|
|
|
|
<footer className={styles.footer}>
|
|
<span>Pedigree Draw - For genetic counselors and bioinformatics professionals</span>
|
|
<span>NSGC Standard Symbols</span>
|
|
</footer>
|
|
|
|
{showWelcome && <WelcomeModal onClose={handleCloseWelcome} />}
|
|
</div>
|
|
);
|
|
}
|