@alexbrand09/famtreejs 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,259 @@
1
+ # @alexbrand09/famtreejs
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@alexbrand09/famtreejs.svg)](https://www.npmjs.com/package/@alexbrand09/famtreejs)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
6
+ A React library for rendering interactive family trees with a partnership-centric data model. Unlike traditional family tree libraries where children belong to a single parent, this library treats partnerships (marriages, unions) as first-class entities, with children descending from partnerships rather than individuals.
7
+
8
+ ## Features
9
+
10
+ - **Partnership-centric data model** - Children belong to partnerships, not individuals
11
+ - **Multiple orientations** - Top-down, bottom-up, left-right, right-left layouts
12
+ - **Interactive** - Pan, zoom, click, hover, expand/collapse branches
13
+ - **Keyboard accessible** - Full keyboard navigation with ARIA support
14
+ - **Themeable** - Light/dark themes with CSS variables for customization
15
+ - **Animated** - Smooth transitions with Framer Motion, respects `prefers-reduced-motion`
16
+ - **TypeScript** - Full type safety with generics for custom data
17
+
18
+ ## Installation
19
+
20
+ ```bash
21
+ npm install @alexbrand09/famtreejs
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```tsx
27
+ import { FamilyTree, BasicPersonCard } from '@alexbrand09/famtreejs';
28
+ import '@alexbrand09/famtreejs/styles.css';
29
+ import type { FamilyTreeData } from '@alexbrand09/famtreejs';
30
+
31
+ const data: FamilyTreeData<{ name: string }> = {
32
+ people: [
33
+ { id: 'p1', data: { name: 'John' } },
34
+ { id: 'p2', data: { name: 'Mary' } },
35
+ { id: 'c1', data: { name: 'Alice' } },
36
+ ],
37
+ partnerships: [
38
+ { id: 'u1', partnerIds: ['p1', 'p2'], childIds: ['c1'] }
39
+ ],
40
+ };
41
+
42
+ function App() {
43
+ return (
44
+ <div style={{ width: '100%', height: '600px' }}>
45
+ <FamilyTree
46
+ data={data}
47
+ nodeComponent={BasicPersonCard}
48
+ onPersonClick={(id, data) => console.log('Clicked:', id, data)}
49
+ />
50
+ </div>
51
+ );
52
+ }
53
+ ```
54
+
55
+ ## Data Model
56
+
57
+ ### PersonNode
58
+
59
+ ```typescript
60
+ interface PersonNode<T = unknown> {
61
+ id: string; // Unique identifier
62
+ data: T; // Your custom data (passed to nodeComponent)
63
+ }
64
+ ```
65
+
66
+ ### Partnership
67
+
68
+ ```typescript
69
+ interface Partnership {
70
+ id: string;
71
+ partnerIds: [string, string | null]; // [partner1, partner2] or [parent, null] for single parent
72
+ childIds: string[]; // Children from this partnership
73
+ type?: 'marriage' | 'civil-union' | 'partnership' | 'other';
74
+ }
75
+ ```
76
+
77
+ ### FamilyTreeData
78
+
79
+ ```typescript
80
+ interface FamilyTreeData<T = unknown> {
81
+ people: PersonNode<T>[];
82
+ partnerships: Partnership[];
83
+ rootPersonId?: string; // Optional starting point
84
+ }
85
+ ```
86
+
87
+ ## Props
88
+
89
+ | Prop | Type | Default | Description |
90
+ |------|------|---------|-------------|
91
+ | `data` | `FamilyTreeData<T>` | required | The family tree data |
92
+ | `nodeComponent` | `ComponentType<NodeComponentProps<T>>` | required | Component to render each person |
93
+ | `orientation` | `'top-down' \| 'bottom-up' \| 'left-right' \| 'right-left'` | `'top-down'` | Tree layout direction |
94
+ | `theme` | `'light' \| 'dark'` | `'light'` | Built-in theme |
95
+ | `spacing` | `SpacingConfig` | `{ generation: 120, siblings: 60, partners: 40 }` | Spacing between nodes |
96
+ | `lineStyle` | `LineStyle` | - | Custom line styling |
97
+ | `initialZoom` | `number` | `1` | Initial zoom level |
98
+ | `minZoom` | `number` | `0.1` | Minimum zoom level |
99
+ | `maxZoom` | `number` | `3` | Maximum zoom level |
100
+ | `disableAnimations` | `boolean` | `false` | Disable all animations |
101
+ | `animationDuration` | `number` | `300` | Animation duration in ms |
102
+ | `onPersonClick` | `(id: string, data: T) => void` | - | Called when a person is clicked |
103
+ | `onPersonHover` | `(id: string \| null, data: T \| null) => void` | - | Called on hover |
104
+ | `onPartnershipClick` | `(id: string) => void` | - | Called when a partnership line is clicked |
105
+ | `onZoomChange` | `(zoom: number) => void` | - | Called when zoom changes |
106
+
107
+ ## Custom Node Components
108
+
109
+ Create your own node component by implementing `NodeComponentProps`:
110
+
111
+ ```tsx
112
+ import type { NodeComponentProps } from '@alexbrand09/famtreejs';
113
+
114
+ interface MyPersonData {
115
+ name: string;
116
+ birthYear?: number;
117
+ photoUrl?: string;
118
+ }
119
+
120
+ function MyPersonCard({ data, isSelected, isHovered, isExpanded, onToggleExpand }: NodeComponentProps<MyPersonData>) {
121
+ return (
122
+ <div style={{
123
+ padding: '10px',
124
+ border: isSelected ? '2px solid blue' : '1px solid gray',
125
+ backgroundColor: isHovered ? '#f0f0f0' : 'white',
126
+ }}>
127
+ {data.photoUrl && <img src={data.photoUrl} alt={data.name} />}
128
+ <div>{data.name}</div>
129
+ {data.birthYear && <div>Born: {data.birthYear}</div>}
130
+ <button onClick={onToggleExpand}>
131
+ {isExpanded ? 'Collapse' : 'Expand'}
132
+ </button>
133
+ </div>
134
+ );
135
+ }
136
+ ```
137
+
138
+ ## Built-in Components
139
+
140
+ ### BasicPersonCard
141
+
142
+ Simple card showing just the name:
143
+
144
+ ```tsx
145
+ import { BasicPersonCard } from '@alexbrand09/famtreejs';
146
+ ```
147
+
148
+ ### DetailedPersonCard
149
+
150
+ Card with photo, name, and dates:
151
+
152
+ ```tsx
153
+ import { DetailedPersonCard } from '@alexbrand09/famtreejs';
154
+
155
+ // Data should include: name, birthDate?, deathDate?, photoUrl?
156
+ ```
157
+
158
+ ## Ref API
159
+
160
+ Access imperative methods via ref:
161
+
162
+ ```tsx
163
+ import { useRef } from 'react';
164
+ import { FamilyTree } from '@alexbrand09/famtreejs';
165
+ import type { FamilyTreeHandle } from '@alexbrand09/famtreejs';
166
+
167
+ function App() {
168
+ const treeRef = useRef<FamilyTreeHandle>(null);
169
+
170
+ return (
171
+ <>
172
+ <button onClick={() => treeRef.current?.zoomIn()}>Zoom In</button>
173
+ <button onClick={() => treeRef.current?.fitToView()}>Fit</button>
174
+ <FamilyTree ref={treeRef} data={data} nodeComponent={BasicPersonCard} />
175
+ </>
176
+ );
177
+ }
178
+ ```
179
+
180
+ ### Available Methods
181
+
182
+ - `zoomTo(level: number)` - Set zoom level
183
+ - `zoomIn()` / `zoomOut()` - Zoom by step
184
+ - `centerOnPerson(id: string)` - Center view on a person
185
+ - `fitToView()` - Fit entire tree in view
186
+ - `expandAll()` / `collapseAll()` - Expand/collapse all branches
187
+ - `toggleBranch(id: string)` - Toggle a specific branch
188
+ - `setRoot(id: string)` - Re-root the tree
189
+ - `getZoom()` - Get current zoom level
190
+ - `getRoot()` - Get current root person ID
191
+
192
+ ## Hooks
193
+
194
+ Use hooks inside the FamilyTree component tree:
195
+
196
+ ```tsx
197
+ import { useFamilyTreeState, useFamilyTreeActions } from '@alexbrand09/famtreejs';
198
+
199
+ function MyControls() {
200
+ const { zoomLevel, selectedPersonId } = useFamilyTreeState();
201
+ const { zoomIn, zoomOut, fitToView } = useFamilyTreeActions();
202
+
203
+ return (
204
+ <div>
205
+ <span>Zoom: {Math.round(zoomLevel * 100)}%</span>
206
+ <button onClick={zoomIn}>+</button>
207
+ <button onClick={zoomOut}>-</button>
208
+ </div>
209
+ );
210
+ }
211
+ ```
212
+
213
+ ## Theming
214
+
215
+ ### CSS Variables
216
+
217
+ Customize the appearance using CSS variables:
218
+
219
+ ```css
220
+ .family-tree {
221
+ --ft-line-color: #333;
222
+ --ft-line-width: 2px;
223
+ --ft-node-background: #fff;
224
+ --ft-node-border: #ddd;
225
+ --ft-node-text: #333;
226
+ --ft-node-shadow: rgba(0, 0, 0, 0.1);
227
+ --ft-node-hover-border: #999;
228
+ --ft-node-selected-border: #0066cc;
229
+ --ft-node-selected-shadow: rgba(0, 102, 204, 0.3);
230
+ }
231
+ ```
232
+
233
+ ## Keyboard Navigation
234
+
235
+ | Key | Action |
236
+ |-----|--------|
237
+ | Arrow keys | Navigate between nodes |
238
+ | Enter / Space | Select focused node |
239
+ | E | Toggle expand/collapse on focused node |
240
+ | + / - | Zoom in / out |
241
+ | 0 | Fit to view |
242
+ | Home / End | Jump to first / last node |
243
+
244
+ ## Accessibility
245
+
246
+ - ARIA tree structure with `role="tree"` and `role="treeitem"`
247
+ - `aria-selected`, `aria-expanded` states
248
+ - Visible focus indicators
249
+ - Respects `prefers-reduced-motion` media query
250
+ - WCAG AA compliant contrast ratios
251
+
252
+ ## Browser Support
253
+
254
+ - Modern browsers (Chrome, Firefox, Safari, Edge)
255
+ - React 18+
256
+
257
+ ## License
258
+
259
+ MIT
@@ -0,0 +1,7 @@
1
+ import type { FamilyTreeProps, FamilyTreeHandle } from '../types';
2
+ import '../styles/theme.css';
3
+ export declare const FamilyTree: <T>(props: FamilyTreeProps<T> & {
4
+ ref?: React.ForwardedRef<FamilyTreeHandle>;
5
+ }) => React.ReactElement;
6
+ export type { FamilyTreeProps, FamilyTreeHandle };
7
+ //# sourceMappingURL=FamilyTree.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FamilyTree.d.ts","sourceRoot":"","sources":["../../src/components/FamilyTree.tsx"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAsB,MAAM,UAAU,CAAC;AAItF,OAAO,qBAAqB,CAAC;AAszB7B,eAAO,MAAM,UAAU,EAAkC,CAAC,CAAC,EACzD,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAA;CAAE,KACvE,KAAK,CAAC,YAAY,CAAC;AAExB,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,CAAC"}
@@ -0,0 +1,5 @@
1
+ import type { FamilyTreeProps, FamilyTreeHandle } from '../types';
2
+ export declare const FamilyTreeWithProvider: <T>(props: FamilyTreeProps<T> & {
3
+ ref?: React.ForwardedRef<FamilyTreeHandle>;
4
+ }) => React.ReactElement;
5
+ //# sourceMappingURL=FamilyTreeWithProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FamilyTreeWithProvider.d.ts","sourceRoot":"","sources":["../../src/components/FamilyTreeWithProvider.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AA2DlE,eAAO,MAAM,sBAAsB,EAA8C,CAAC,CAAC,EACjF,KAAK,EAAE,eAAe,CAAC,CAAC,CAAC,GAAG;IAAE,GAAG,CAAC,EAAE,KAAK,CAAC,YAAY,CAAC,gBAAgB,CAAC,CAAA;CAAE,KACvE,KAAK,CAAC,YAAY,CAAC"}
@@ -0,0 +1,7 @@
1
+ import type { NodeComponentProps } from '../../types';
2
+ interface BasicPersonData {
3
+ name: string;
4
+ }
5
+ export declare function BasicPersonCard({ data, isSelected, isHovered, }: NodeComponentProps<BasicPersonData>): import("react/jsx-runtime").JSX.Element;
6
+ export {};
7
+ //# sourceMappingURL=BasicPersonCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"BasicPersonCard.d.ts","sourceRoot":"","sources":["../../../src/components/defaults/BasicPersonCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,UAAU,EACV,SAAS,GACV,EAAE,kBAAkB,CAAC,eAAe,CAAC,2CA0BrC"}
@@ -0,0 +1,10 @@
1
+ import type { NodeComponentProps } from '../../types';
2
+ interface DetailedPersonData {
3
+ name: string;
4
+ birthDate?: string;
5
+ deathDate?: string;
6
+ photoUrl?: string;
7
+ }
8
+ export declare function DetailedPersonCard({ data, isSelected, isHovered, }: NodeComponentProps<DetailedPersonData>): import("react/jsx-runtime").JSX.Element;
9
+ export {};
10
+ //# sourceMappingURL=DetailedPersonCard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DetailedPersonCard.d.ts","sourceRoot":"","sources":["../../../src/components/defaults/DetailedPersonCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AAEtD,UAAU,kBAAkB;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,wBAAgB,kBAAkB,CAAC,EACjC,IAAI,EACJ,UAAU,EACV,SAAS,GACV,EAAE,kBAAkB,CAAC,kBAAkB,CAAC,2CAyDxC"}
@@ -0,0 +1,2 @@
1
+ export { useFamilyTreeState, useFamilyTreeActions } from '../store/FamilyTreeContext';
2
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,4BAA4B,CAAC"}
package/dist/index.css ADDED
@@ -0,0 +1 @@
1
+ .family-tree{--ft-line-color: #333;--ft-line-width: 2px;--ft-background: transparent;--ft-node-background: #fff;--ft-node-border: #ddd;--ft-node-text: #333;--ft-node-shadow: rgba(0, 0, 0, .1);--ft-node-hover-border: #999;--ft-node-selected-border: #0066cc;--ft-node-selected-shadow: rgba(0, 102, 204, .3)}.family-tree--dark{--ft-line-color: #888;--ft-line-width: 2px;--ft-background: transparent;--ft-node-background: #2a2a2a;--ft-node-border: #444;--ft-node-text: #e0e0e0;--ft-node-shadow: rgba(0, 0, 0, .3);--ft-node-hover-border: #666;--ft-node-selected-border: #4da6ff;--ft-node-selected-shadow: rgba(77, 166, 255, .3)}.family-tree--light{--ft-line-color: #333;--ft-line-width: 2px;--ft-background: transparent;--ft-node-background: #fff;--ft-node-border: #ddd;--ft-node-text: #333;--ft-node-shadow: rgba(0, 0, 0, .1);--ft-node-hover-border: #999;--ft-node-selected-border: #0066cc;--ft-node-selected-shadow: rgba(0, 102, 204, .3)}
@@ -0,0 +1,9 @@
1
+ export { FamilyTreeWithProvider as FamilyTree } from './components/FamilyTreeWithProvider';
2
+ export { FamilyTree as FamilyTreeCore } from './components/FamilyTree';
3
+ export type { FamilyTreeProps, FamilyTreeHandle } from './components/FamilyTree';
4
+ export { useFamilyTreeState, useFamilyTreeActions } from './hooks';
5
+ export { FamilyTreeProvider } from './store/FamilyTreeContext';
6
+ export { BasicPersonCard } from './components/defaults/BasicPersonCard';
7
+ export { DetailedPersonCard } from './components/defaults/DetailedPersonCard';
8
+ export type { PersonNode, Partnership, FamilyTreeData, NodeComponentProps, FamilyTreeState, Orientation, SpacingConfig, LineStyle, Theme, } from './types';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,sBAAsB,IAAI,UAAU,EAAE,MAAM,qCAAqC,CAAC;AAG3F,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAGvE,YAAY,EAAE,eAAe,EAAE,gBAAgB,EAAE,MAAM,yBAAyB,CAAC;AAGjF,OAAO,EAAE,kBAAkB,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAGnE,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAG/D,OAAO,EAAE,eAAe,EAAE,MAAM,uCAAuC,CAAC;AACxE,OAAO,EAAE,kBAAkB,EAAE,MAAM,0CAA0C,CAAC;AAG9E,YAAY,EACV,UAAU,EACV,WAAW,EACX,cAAc,EACd,kBAAkB,EAClB,eAAe,EACf,WAAW,EACX,aAAa,EACb,SAAS,EACT,KAAK,GACN,MAAM,SAAS,CAAC"}