Add relationship child creation, auto-align, delete shortcut, and improve UX
Some checks failed
Deploy to GitHub Pages / build (push) Has been cancelled
Deploy to GitHub Pages / deploy (push) Has been cancelled

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:
gbanyan
2025-12-14 22:19:13 +08:00
parent 8b07e483d2
commit 07a4902e45
5 changed files with 212 additions and 19 deletions

View File

@@ -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.
---

View File

@@ -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}>

View File

@@ -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>

View File

@@ -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) {

View File

@@ -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>
);
}