Add relationship child creation, auto-align, delete shortcut, and improve UX
Features: - Add "Add Child" buttons (Male/Female/Unknown) in RelationshipPanel to create children directly from a selected relationship - Add "Auto Align" button in toolbar to reset all element positions - Add Delete/Backspace keyboard shortcut to delete selected elements - Add text labels to all toolbar buttons for better discoverability Documentation: - Update README with Node.js installation instructions for beginners - Add npm install step for first-time setup - Document keyboard shortcuts and relationship editing features - Add Windows-specific instructions 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
78
README.md
78
README.md
@@ -18,30 +18,70 @@ A professional pedigree (family tree) drawing tool for genetic counselors and bi
|
||||
|
||||
You need **Node.js** (version 18 or higher) installed on your computer.
|
||||
|
||||
Check if you have it:
|
||||
#### Check if Node.js is installed:
|
||||
|
||||
```bash
|
||||
node --version
|
||||
npm --version
|
||||
```
|
||||
|
||||
If not installed, download from: https://nodejs.org/ (choose LTS version)
|
||||
If both commands show version numbers (e.g., `v20.10.0` and `10.2.0`), you're ready to go!
|
||||
|
||||
#### If Node.js is NOT installed:
|
||||
|
||||
**Windows:**
|
||||
1. Go to https://nodejs.org/
|
||||
2. Download the **LTS** version (recommended)
|
||||
3. Run the installer, click "Next" through all steps
|
||||
4. Restart your terminal/command prompt after installation
|
||||
|
||||
**macOS:**
|
||||
```bash
|
||||
# Using Homebrew (recommended)
|
||||
brew install node
|
||||
|
||||
# Or download from https://nodejs.org/
|
||||
```
|
||||
|
||||
**Linux (Ubuntu/Debian):**
|
||||
```bash
|
||||
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
|
||||
sudo apt-get install -y nodejs
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Step 1: Start the Server
|
||||
### Step 1: Install Dependencies (First Time Only)
|
||||
|
||||
Open your terminal and navigate to the project folder:
|
||||
|
||||
```bash
|
||||
cd /home/gbanyan/projects/pedigree-draw
|
||||
cd /path/to/pedigree-draw
|
||||
```
|
||||
|
||||
Install the required packages:
|
||||
|
||||
```bash
|
||||
npm install
|
||||
```
|
||||
|
||||
This will download all necessary dependencies. You only need to do this once (or after updating the code).
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Start the Server
|
||||
|
||||
Run the start script:
|
||||
|
||||
```bash
|
||||
./start.sh
|
||||
```
|
||||
|
||||
**On Windows**, use:
|
||||
```bash
|
||||
npm run dev
|
||||
```
|
||||
|
||||
You should see output like:
|
||||
```
|
||||
==========================================
|
||||
@@ -57,7 +97,7 @@ You should see output like:
|
||||
|
||||
---
|
||||
|
||||
### Step 2: Open in Browser
|
||||
### Step 3: Open in Browser
|
||||
|
||||
Open your web browser and go to:
|
||||
|
||||
@@ -66,7 +106,7 @@ Open your web browser and go to:
|
||||
|
||||
---
|
||||
|
||||
### Step 3: Stop the Server
|
||||
### Step 4: Stop the Server
|
||||
|
||||
When you're done, stop the server:
|
||||
|
||||
@@ -74,6 +114,8 @@ When you're done, stop the server:
|
||||
./stop.sh
|
||||
```
|
||||
|
||||
**On Windows**, press `Ctrl+C` in the terminal to stop the server.
|
||||
|
||||
---
|
||||
|
||||
## How to Use the Application
|
||||
@@ -145,11 +187,27 @@ You can also:
|
||||
| Export PNG | Raster image (for documents, presentations) |
|
||||
| Export PED | GATK PED format file |
|
||||
|
||||
### Undo/Redo
|
||||
### Keyboard Shortcuts
|
||||
|
||||
Use the Undo/Redo buttons in the toolbar, or:
|
||||
- **Ctrl+Z** to Undo
|
||||
- **Ctrl+Y** to Redo
|
||||
| Shortcut | Action |
|
||||
|----------|--------|
|
||||
| **Delete** or **Backspace** | Delete selected person or relationship |
|
||||
| **Escape** | Clear selection |
|
||||
| **Ctrl+Z** | Undo |
|
||||
| **Ctrl+Y** or **Ctrl+Shift+Z** | Redo |
|
||||
|
||||
### Editing Relationships
|
||||
|
||||
1. Click on the **connection line** between two people to select the relationship
|
||||
2. The right panel will show relationship options:
|
||||
- **Add Child**: Add a Male/Female/Unknown child to this couple
|
||||
- **Partnership Status**: Married, Unmarried, Separated, Divorced
|
||||
- **Consanguinity**: Mark as consanguineous (blood relatives)
|
||||
- **Children Status**: Infertility or No children by choice
|
||||
|
||||
### Auto Align
|
||||
|
||||
If elements get messy after dragging, click the **Auto Align** button in the toolbar to automatically reposition all family members according to their generation and relationships.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -14,12 +14,24 @@ import { usePedigreeStore, useTemporalStore } from '@/store/pedigreeStore';
|
||||
import styles from './App.module.css';
|
||||
|
||||
export function App() {
|
||||
const { clearSelection, selectedRelationshipId } = usePedigreeStore();
|
||||
const {
|
||||
clearSelection,
|
||||
selectedPersonId,
|
||||
selectedRelationshipId,
|
||||
deletePerson,
|
||||
deleteRelationship,
|
||||
} = usePedigreeStore();
|
||||
const temporal = useTemporalStore();
|
||||
|
||||
// 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();
|
||||
@@ -36,11 +48,21 @@ export function App() {
|
||||
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, temporal]);
|
||||
}, [clearSelection, selectedPersonId, selectedRelationshipId, deletePerson, deleteRelationship, temporal]);
|
||||
|
||||
return (
|
||||
<div className={styles.app}>
|
||||
|
||||
@@ -9,6 +9,8 @@ import {
|
||||
RelationshipType,
|
||||
PartnershipStatus,
|
||||
ChildlessReason,
|
||||
Sex,
|
||||
createPerson,
|
||||
} from '@/core/model/types';
|
||||
import styles from './RelationshipPanel.module.css';
|
||||
|
||||
@@ -16,9 +18,12 @@ export function RelationshipPanel() {
|
||||
const {
|
||||
pedigree,
|
||||
selectedRelationshipId,
|
||||
addPerson,
|
||||
updatePerson,
|
||||
updateRelationship,
|
||||
deleteRelationship,
|
||||
clearSelection,
|
||||
recalculateLayout,
|
||||
} = usePedigreeStore();
|
||||
|
||||
const selectedRelationship = selectedRelationshipId && pedigree
|
||||
@@ -55,6 +60,48 @@ export function RelationshipPanel() {
|
||||
clearSelection();
|
||||
};
|
||||
|
||||
const handleAddChild = (sex: Sex) => {
|
||||
if (!pedigree || !selectedRelationship) return;
|
||||
|
||||
const parent1 = pedigree.persons.get(selectedRelationship.person1Id);
|
||||
const parent2 = pedigree.persons.get(selectedRelationship.person2Id);
|
||||
if (!parent1 || !parent2) return;
|
||||
|
||||
// Determine father and mother based on sex
|
||||
const father = parent1.sex === Sex.Male ? parent1 : (parent2.sex === Sex.Male ? parent2 : null);
|
||||
const mother = parent1.sex === Sex.Female ? parent1 : (parent2.sex === Sex.Female ? parent2 : null);
|
||||
|
||||
// Create child
|
||||
const childId = `P${Date.now().toString(36)}`;
|
||||
const child = createPerson(childId, pedigree.familyId, sex);
|
||||
child.metadata.label = childId;
|
||||
child.fatherId = father?.id ?? null;
|
||||
child.motherId = mother?.id ?? null;
|
||||
|
||||
// Position child below parents
|
||||
const parentX = ((parent1.x ?? 0) + (parent2.x ?? 0)) / 2;
|
||||
const parentY = Math.max(parent1.y ?? 0, parent2.y ?? 0);
|
||||
child.x = parentX;
|
||||
child.y = parentY + 120;
|
||||
|
||||
addPerson(child);
|
||||
|
||||
// Update parents' childrenIds
|
||||
updatePerson(parent1.id, {
|
||||
childrenIds: [...parent1.childrenIds, childId],
|
||||
});
|
||||
updatePerson(parent2.id, {
|
||||
childrenIds: [...parent2.childrenIds, childId],
|
||||
});
|
||||
|
||||
// Update relationship's childrenIds
|
||||
updateRelationship(selectedRelationship.id, {
|
||||
childrenIds: [...selectedRelationship.childrenIds, childId],
|
||||
});
|
||||
|
||||
recalculateLayout();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.panel}>
|
||||
<div className={styles.header}>
|
||||
@@ -150,6 +197,31 @@ export function RelationshipPanel() {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Child Section */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionTitle}>Add Child</div>
|
||||
<div className={styles.buttonGroup}>
|
||||
<button
|
||||
className={styles.optionButton}
|
||||
onClick={() => handleAddChild(Sex.Male)}
|
||||
>
|
||||
Male
|
||||
</button>
|
||||
<button
|
||||
className={styles.optionButton}
|
||||
onClick={() => handleAddChild(Sex.Female)}
|
||||
>
|
||||
Female
|
||||
</button>
|
||||
<button
|
||||
className={styles.optionButton}
|
||||
onClick={() => handleAddChild(Sex.Unknown)}
|
||||
>
|
||||
Unknown
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Childlessness Section */}
|
||||
<div className={styles.section}>
|
||||
<div className={styles.sectionTitle}>Children Status</div>
|
||||
|
||||
@@ -23,8 +23,8 @@
|
||||
}
|
||||
|
||||
.toolButton {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #ddd;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
@@ -32,8 +32,15 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
color: #333;
|
||||
transition: all 0.15s ease;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.toolButton svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.toolButton:hover:not(:disabled) {
|
||||
|
||||
@@ -151,26 +151,26 @@ export function Toolbar() {
|
||||
return (
|
||||
<div className={styles.toolbar}>
|
||||
<div className={styles.toolGroup}>
|
||||
<span className={styles.groupLabel}>Tools</span>
|
||||
<button
|
||||
className={`${styles.toolButton} ${currentTool === 'select' ? styles.active : ''}`}
|
||||
onClick={() => setCurrentTool('select')}
|
||||
title="Select (V)"
|
||||
>
|
||||
<SelectIcon />
|
||||
Select
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
<div className={styles.toolGroup}>
|
||||
<span className={styles.groupLabel}>Add Person</span>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
onClick={() => handleAddPerson(Sex.Male)}
|
||||
title="Add Male"
|
||||
>
|
||||
<MaleIcon />
|
||||
Male
|
||||
</button>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
@@ -178,6 +178,7 @@ export function Toolbar() {
|
||||
title="Add Female"
|
||||
>
|
||||
<FemaleIcon />
|
||||
Female
|
||||
</button>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
@@ -185,13 +186,13 @@ export function Toolbar() {
|
||||
title="Add Unknown"
|
||||
>
|
||||
<UnknownIcon />
|
||||
Unknown
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
<div className={styles.toolGroup}>
|
||||
<span className={styles.groupLabel}>Relationships</span>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
onClick={handleAddSpouse}
|
||||
@@ -199,6 +200,7 @@ export function Toolbar() {
|
||||
title="Add Spouse"
|
||||
>
|
||||
<SpouseIcon />
|
||||
Spouse
|
||||
</button>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
@@ -207,6 +209,7 @@ export function Toolbar() {
|
||||
title="Add Child"
|
||||
>
|
||||
<ChildIcon />
|
||||
Child
|
||||
</button>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
@@ -215,27 +218,27 @@ export function Toolbar() {
|
||||
title="Add Parents"
|
||||
>
|
||||
<ParentsIcon />
|
||||
Parents
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
<div className={styles.toolGroup}>
|
||||
<span className={styles.groupLabel}>Edit</span>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
onClick={handleDelete}
|
||||
disabled={!selectedPersonId}
|
||||
title="Delete Selected"
|
||||
title="Delete Selected (Del)"
|
||||
>
|
||||
<DeleteIcon />
|
||||
Delete
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
<div className={styles.toolGroup}>
|
||||
<span className={styles.groupLabel}>History</span>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
onClick={() => undo()}
|
||||
@@ -243,6 +246,7 @@ export function Toolbar() {
|
||||
title="Undo (Ctrl+Z)"
|
||||
>
|
||||
<UndoIcon />
|
||||
Undo
|
||||
</button>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
@@ -251,6 +255,21 @@ export function Toolbar() {
|
||||
title="Redo (Ctrl+Y)"
|
||||
>
|
||||
<RedoIcon />
|
||||
Redo
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className={styles.divider} />
|
||||
|
||||
<div className={styles.toolGroup}>
|
||||
<button
|
||||
className={styles.toolButton}
|
||||
onClick={() => recalculateLayout()}
|
||||
disabled={!pedigree || pedigree.persons.size === 0}
|
||||
title="Auto Align - Reset all positions"
|
||||
>
|
||||
<AutoAlignIcon />
|
||||
Auto Align
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -347,3 +366,18 @@ function ParentsIcon() {
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function AutoAlignIcon() {
|
||||
return (
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2">
|
||||
{/* Grid lines */}
|
||||
<line x1="4" y1="4" x2="4" y2="20" strokeDasharray="2,2" />
|
||||
<line x1="12" y1="4" x2="12" y2="20" strokeDasharray="2,2" />
|
||||
<line x1="20" y1="4" x2="20" y2="20" strokeDasharray="2,2" />
|
||||
<line x1="4" y1="12" x2="20" y2="12" strokeDasharray="2,2" />
|
||||
{/* Alignment arrows */}
|
||||
<path d="M7 8 L12 4 L17 8" fill="none" />
|
||||
<path d="M7 16 L12 20 L17 16" fill="none" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user