@captiveorigins/onepedigree-react 0.1.0-alpha.8 → 0.1.0-alpha.9
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/README.md +17 -0
- package/dist/PedigreeExplorer.d.ts +9 -1
- package/dist/PedigreeExplorer.js +68 -6
- package/dist/icons.d.ts +33 -0
- package/dist/icons.js +31 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/reorder.d.ts +14 -0
- package/dist/reorder.js +28 -0
- package/dist/styles.css +10 -1
- package/package.json +7 -3
package/README.md
CHANGED
|
@@ -26,6 +26,19 @@ All 13 views of the core: `pedigree`, `fan`, `ahnentafel`, `descendants`, `hourg
|
|
|
26
26
|
`paternal`. Repeated individuals follow the projection's `merge`, `separate` or `hybrid` mode;
|
|
27
27
|
selecting one occurrence highlights every occurrence of that individual.
|
|
28
28
|
|
|
29
|
+
## Rearranging by drag
|
|
30
|
+
|
|
31
|
+
In views where order is a choice (descendants, hourglass, cohort, population, relationship), people can drag a node sideways to reorder it among its siblings. Ancestor charts keep the father/mother convention. A node only moves within its own drawn groups (a clutch or litter frame) and among individuals with the same parents, so frames and family lines stay truthful. The data never changes: the order is a layout option you keep (and may save).
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
const [nodeOrder, setNodeOrder] = useState<string[]>([])
|
|
35
|
+
const layout = useMemo(() => layoutSync(projection, { nodeOrder }), [projection, nodeOrder])
|
|
36
|
+
|
|
37
|
+
<PedigreeExplorer projection={projection} layout={layout} nodeOrder={nodeOrder} onNodeOrderChange={setNodeOrder} />
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`dropNodeOrder(layout, occurrenceId, delta, current, groupOf)` is exported for custom UIs.
|
|
41
|
+
|
|
29
42
|
## Themes and colors
|
|
30
43
|
|
|
31
44
|
Seven presets, each with its own per-view accents (e.g. warm maternal and cool paternal
|
|
@@ -43,6 +56,10 @@ below WCAG AA. `onThemeChange` receives the edited theme (a plain object) to sto
|
|
|
43
56
|
back as `theme`. `ThemeEditor` is also exported for use in your own settings pages. Themes are
|
|
44
57
|
rendered as inline custom properties, so they work in server rendering.
|
|
45
58
|
|
|
59
|
+
## Brand family
|
|
60
|
+
|
|
61
|
+
OnePedigree and OneTaxonomy share a backbone (`brand/README.md`): the same line icons (`Icon`, `ICON_PATHS`), 32 px controls, a floating canvas toolbar and a Captive Origins orange focus ring, while each keeps its own views and character.
|
|
62
|
+
|
|
46
63
|
## Styling
|
|
47
64
|
|
|
48
65
|
- Default stylesheet `styles.css` (optional; leave it out for unstyled mode).
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Individual, type IndividualOccurrence } from '@captiveorigins/onepedigree-core';
|
|
2
2
|
import { type ReactNode } from 'react';
|
|
3
3
|
import { type PedigreeViewProps } from './PedigreeView.js';
|
|
4
4
|
import { type PedigreeTheme } from './themes.js';
|
|
@@ -27,6 +27,14 @@ export type PedigreeExplorerProps = Omit<PedigreeViewProps, 'interactive' | 'vie
|
|
|
27
27
|
paternal: string;
|
|
28
28
|
maternal: string;
|
|
29
29
|
};
|
|
30
|
+
/**
|
|
31
|
+
* Turns on click-and-drag rearranging in views where order is a choice
|
|
32
|
+
* (REORDERABLE_VIEWS: descendants, hourglass, cohort, population, relationship).
|
|
33
|
+
* Called with the new order; pass it back as `layoutSync(projection, { nodeOrder })`.
|
|
34
|
+
*/
|
|
35
|
+
onNodeOrderChange?: (nodeOrder: string[]) => void;
|
|
36
|
+
/** The current chosen order (what you passed to the layout), so a drag extends it. */
|
|
37
|
+
nodeOrder?: readonly string[];
|
|
30
38
|
/** Accessible labels for the controls. */
|
|
31
39
|
labels?: Partial<Record<'zoomIn' | 'zoomOut' | 'fit' | 'download' | 'print' | 'close' | 'open' | 'controls' | 'colors', string>>;
|
|
32
40
|
};
|
package/dist/PedigreeExplorer.js
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { REORDERABLE_VIEWS } from '@captiveorigins/onepedigree-core';
|
|
3
4
|
import { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
5
|
import { AhnentafelList } from './AhnentafelList.js';
|
|
5
6
|
import { DEFAULT_TERMS, describeStructure, labelOf } from './describe.js';
|
|
6
7
|
import { nearestNode, useOccurrenceHighlighting, usePedigreeSelection } from './hooks.js';
|
|
7
8
|
import { PedigreeView } from './PedigreeView.js';
|
|
9
|
+
import { Icon } from './icons.js';
|
|
10
|
+
import { dropNodeOrder } from './reorder.js';
|
|
8
11
|
import { ThemeEditor } from './ThemeEditor.js';
|
|
9
12
|
import { presetTheme, THEME_PRESETS, themeStyle } from './themes.js';
|
|
10
13
|
const LABELS = {
|
|
@@ -65,6 +68,10 @@ export function PedigreeExplorer(props) {
|
|
|
65
68
|
props.onThemeChange?.(t);
|
|
66
69
|
};
|
|
67
70
|
const drag = useRef(null);
|
|
71
|
+
// Dragging a node (rearranging) moves the element directly while the pointer is down: no re-render per frame.
|
|
72
|
+
const nodeDrag = useRef(null);
|
|
73
|
+
const suppressClick = useRef(false);
|
|
74
|
+
const reorderable = Boolean(props.onNodeOrderChange) && REORDERABLE_VIEWS.has(projection.view);
|
|
68
75
|
const choose = useCallback((occ) => {
|
|
69
76
|
select(occ?.individualId ?? null);
|
|
70
77
|
setSelectedOccurrence(occ?.occurrenceId ?? null);
|
|
@@ -83,7 +90,7 @@ export function PedigreeExplorer(props) {
|
|
|
83
90
|
}, [full]);
|
|
84
91
|
const pan = (dx, dy) => setBox((b) => ({ ...b, x: b.x + dx * b.width, y: b.y + dy * b.height }));
|
|
85
92
|
const svgPoint = (e) => {
|
|
86
|
-
const svg = root.current?.querySelector('svg');
|
|
93
|
+
const svg = root.current?.querySelector('svg.op-root');
|
|
87
94
|
const r = svg?.getBoundingClientRect();
|
|
88
95
|
if (!r || !r.width || !r.height)
|
|
89
96
|
return undefined;
|
|
@@ -128,24 +135,73 @@ export function PedigreeExplorer(props) {
|
|
|
128
135
|
return;
|
|
129
136
|
zoom(e.deltaY > 0 ? 1.1 : 0.9, svgPoint(e));
|
|
130
137
|
};
|
|
138
|
+
const svgScale = () => {
|
|
139
|
+
const r = root.current?.querySelector('svg.op-root')?.getBoundingClientRect();
|
|
140
|
+
return r?.width ? Math.max(box.width / r.width, box.height / r.height) : 1;
|
|
141
|
+
};
|
|
131
142
|
const onPointerDown = (e) => {
|
|
132
|
-
|
|
143
|
+
const target = e.target;
|
|
144
|
+
if (target.closest('button, a, .op-toolbar, .op-quick-view, .op-theme-panel'))
|
|
145
|
+
return;
|
|
146
|
+
const nodeEl = target.closest('.op-node');
|
|
147
|
+
if (nodeEl) {
|
|
148
|
+
const id = nodeEl.getAttribute('data-occurrence');
|
|
149
|
+
if (reorderable && id)
|
|
150
|
+
nodeDrag.current = { x: e.clientX, y: e.clientY, id, el: nodeEl, moved: false };
|
|
133
151
|
return;
|
|
152
|
+
}
|
|
134
153
|
drag.current = { x: e.clientX, y: e.clientY, box };
|
|
135
154
|
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
136
155
|
};
|
|
137
156
|
const onPointerMove = (e) => {
|
|
157
|
+
const n = nodeDrag.current;
|
|
158
|
+
if (n) {
|
|
159
|
+
if (!n.moved && Math.hypot(e.clientX - n.x, e.clientY - n.y) < 4)
|
|
160
|
+
return;
|
|
161
|
+
if (!n.moved)
|
|
162
|
+
e.currentTarget.setPointerCapture?.(e.pointerId);
|
|
163
|
+
n.moved = true;
|
|
164
|
+
const k = svgScale();
|
|
165
|
+
n.el.style.translate = `${(e.clientX - n.x) * k}px ${(e.clientY - n.y) * k}px`;
|
|
166
|
+
n.el.setAttribute('data-dragging', '');
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
138
169
|
const d = drag.current;
|
|
139
|
-
const r = root.current?.querySelector('svg')?.getBoundingClientRect();
|
|
170
|
+
const r = root.current?.querySelector('svg.op-root')?.getBoundingClientRect();
|
|
140
171
|
if (!d || !r?.width)
|
|
141
172
|
return;
|
|
142
173
|
setBox({ ...d.box, x: d.box.x - ((e.clientX - d.x) / r.width) * d.box.width, y: d.box.y - ((e.clientY - d.y) / r.height) * d.box.height });
|
|
143
174
|
};
|
|
144
|
-
const onPointerUp = () => {
|
|
175
|
+
const onPointerUp = (e) => {
|
|
145
176
|
drag.current = null;
|
|
177
|
+
const n = nodeDrag.current;
|
|
178
|
+
nodeDrag.current = null;
|
|
179
|
+
if (!n?.moved)
|
|
180
|
+
return;
|
|
181
|
+
n.el.style.translate = '';
|
|
182
|
+
n.el.removeAttribute('data-dragging');
|
|
183
|
+
suppressClick.current = true;
|
|
184
|
+
const k = svgScale();
|
|
185
|
+
// Only among nodes in the same drawn groups (clutch/litter frames, other-parent groups) and with the same
|
|
186
|
+
// parents, so frames and family lines stay truthful.
|
|
187
|
+
const groupOf = (id) => [
|
|
188
|
+
...(projection.groups ?? []).filter((g) => g.occurrenceIds.includes(id)).map((g) => g.id),
|
|
189
|
+
projection.edges.filter((ed) => ed.target === id).map((ed) => ed.source).sort().join('+'),
|
|
190
|
+
].join('|');
|
|
191
|
+
const order = dropNodeOrder(layout, n.id, { dx: (e.clientX - n.x) * k, dy: (e.clientY - n.y) * k }, props.nodeOrder, groupOf);
|
|
192
|
+
if (order)
|
|
193
|
+
props.onNodeOrderChange(order);
|
|
194
|
+
};
|
|
195
|
+
const onPointerCancel = () => {
|
|
196
|
+
drag.current = null;
|
|
197
|
+
if (nodeDrag.current) {
|
|
198
|
+
nodeDrag.current.el.style.translate = '';
|
|
199
|
+
nodeDrag.current.el.removeAttribute('data-dragging');
|
|
200
|
+
}
|
|
201
|
+
nodeDrag.current = null;
|
|
146
202
|
};
|
|
147
203
|
const download = () => {
|
|
148
|
-
const svg = root.current?.querySelector('svg');
|
|
204
|
+
const svg = root.current?.querySelector('svg.op-root');
|
|
149
205
|
if (!svg)
|
|
150
206
|
return;
|
|
151
207
|
const clone = svg.cloneNode(true);
|
|
@@ -177,5 +233,11 @@ export function PedigreeExplorer(props) {
|
|
|
177
233
|
const individual = occurrence ? projection.individuals[occurrence.individualId] : undefined;
|
|
178
234
|
const href = individual ? (props.getIndividualHref?.(individual) ?? null) : null;
|
|
179
235
|
const close = () => choose(null);
|
|
180
|
-
return (_jsxs("div", { ref: root, className: "op-explorer", style: { height: props.height ?? '32rem', ...(theme ? { ...themeStyle(theme, projection.view), background: 'var(--op-background)' } : {}) }, onKeyDown: onKeyDown, onWheel: onWheel, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel:
|
|
236
|
+
return (_jsxs("div", { ref: root, className: "op-explorer", style: { height: props.height ?? '32rem', ...(theme ? { ...themeStyle(theme, projection.view), background: 'var(--op-background)' } : {}) }, onKeyDown: onKeyDown, onWheel: onWheel, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: onPointerCancel, onClickCapture: (e) => {
|
|
237
|
+
// A drag ends with a click on the node: don't let it select.
|
|
238
|
+
if (suppressClick.current) {
|
|
239
|
+
suppressClick.current = false;
|
|
240
|
+
e.stopPropagation();
|
|
241
|
+
}
|
|
242
|
+
}, "data-reorderable": reorderable ? '' : undefined, children: [_jsxs("div", { className: "op-toolbar", role: "toolbar", "aria-label": labels.controls, children: [_jsx("button", { type: "button", onClick: () => zoom(0.8), "aria-label": labels.zoomIn, title: labels.zoomIn, children: _jsx(Icon, { name: "plus" }) }), _jsx("button", { type: "button", onClick: () => zoom(1.25), "aria-label": labels.zoomOut, title: labels.zoomOut, children: _jsx(Icon, { name: "minus" }) }), _jsx("button", { type: "button", onClick: () => setBox(full), "aria-label": labels.fit, title: labels.fit, children: _jsx(Icon, { name: "fit" }) }), _jsx("button", { type: "button", onClick: download, "aria-label": labels.download, title: labels.download, children: _jsx(Icon, { name: "download" }) }), _jsx("button", { type: "button", onClick: () => (props.onPrint ? props.onPrint() : printChart(root.current)), "aria-label": labels.print, title: labels.print, children: _jsx(Icon, { name: "print" }) }), props.themeEditor && (_jsx("button", { type: "button", onClick: () => setEditing((v) => !v), "aria-label": labels.colors, title: labels.colors, "aria-expanded": editing, children: _jsx(Icon, { name: "palette" }) })), props.toolbar] }), props.themeEditor && editing && (_jsx("div", { className: "op-theme-panel", children: _jsx(ThemeEditor, { value: theme ?? structuredClone(THEME_PRESETS[0]), view: projection.view, onChange: changeTheme }) })), projection.view === 'ahnentafel' ? (_jsx(AhnentafelList, { projection: projection, terminology: props.terminology, parentWords: props.parentWords, getIndividualHref: props.getIndividualHref, selectedIndividualId: selectedId, onSelect: (individualId) => choose(projection.occurrences.find((o) => o.individualId === individualId) ?? null) })) : (_jsx(PedigreeView, { ...props, theme: theme, interactive: true, viewBox: box, selectedIndividualId: selectedId, highlightedOccurrenceIds: highlighted, onNodeClick: (occ) => choose(selectedOccurrence === occ.occurrenceId ? null : occ), onNodeKeyDown: onNodeKeyDown, svgProps: { ...props.svgProps, width: '100%', height: '100%', preserveAspectRatio: 'xMidYMid meet' } })), occurrence && (_jsx("div", { className: "op-quick-view", role: "dialog", "aria-label": labelOf(individual, terms), children: props.renderQuickView ? (props.renderQuickView({ individual, occurrence, href, close })) : (_jsxs(_Fragment, { children: [_jsx("p", { className: "op-quick-view-title", children: labelOf(individual, terms) }), individual?.sex && _jsx("p", { children: individual.sex }), _jsx("p", { children: describeStructure(projection.structures[occurrence.individualId]) }), occurrence.repeated && _jsx("p", { children: terms.repeated }), _jsxs("div", { className: "op-quick-view-actions", children: [href && (_jsxs("a", { href: href, children: [_jsx(Icon, { name: "open" }), labels.open] })), _jsxs("button", { type: "button", onClick: close, children: [_jsx(Icon, { name: "close" }), labels.close] })] })] })) }))] }));
|
|
181
243
|
}
|
package/dist/icons.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { SVGProps } from 'react';
|
|
2
|
+
/**
|
|
3
|
+
* The brand family's line icons (brand/README.md): 24 × 24, 1.75 stroke, round
|
|
4
|
+
* caps and joins, currentColor. The same drawings ship in OnePedigree and
|
|
5
|
+
* OneTaxonomy. Decorative by default (aria-hidden): put the words in the button's
|
|
6
|
+
* text or aria-label.
|
|
7
|
+
*/
|
|
8
|
+
export declare const ICON_PATHS: {
|
|
9
|
+
readonly plus: "M12 5v14M5 12h14";
|
|
10
|
+
readonly minus: "M5 12h14";
|
|
11
|
+
readonly fit: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5";
|
|
12
|
+
readonly download: "M12 4v11M7 10l5 5 5-5M5 20h14";
|
|
13
|
+
readonly print: "M7 9V4h10v5M7 17H5a1 1 0 0 1-1-1v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5a1 1 0 0 1-1 1h-2M7 14h10v6H7z";
|
|
14
|
+
readonly undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11";
|
|
15
|
+
readonly redo: "m15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13";
|
|
16
|
+
readonly expand: "m7 15 5 5 5-5M7 9l5-5 5 5";
|
|
17
|
+
readonly collapse: "m7 20 5-5 5 5M7 4l5 5 5-5";
|
|
18
|
+
readonly flip: "M7 4v16M4 7l3-3 3 3M17 20V4m-3 13 3 3 3-3";
|
|
19
|
+
readonly focus: "M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM12 2v4M12 18v4M2 12h4M18 12h4";
|
|
20
|
+
readonly unfocus: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5M9 12h6";
|
|
21
|
+
readonly layout: "M4 12h4M8 6v12M8 6h5M8 18h5M8 12h9M15 4v4M15 16v4";
|
|
22
|
+
readonly filter: "M4 5h16l-6 8v6l-4 2v-8z";
|
|
23
|
+
readonly palette: "M12 3a9 9 0 1 0 0 18c1.1 0 1.5-.8 1.2-1.6-.4-1 .2-2.4 1.6-2.4H17a4 4 0 0 0 4-4c0-5.5-4-10-9-10zM7.5 11.5h.01M10 7.5h.01M15 7.5h.01";
|
|
24
|
+
readonly close: "M6 6l12 12M18 6 6 18";
|
|
25
|
+
readonly open: "M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5";
|
|
26
|
+
readonly grip: "M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01";
|
|
27
|
+
readonly info: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v6M12 7.5h.01";
|
|
28
|
+
};
|
|
29
|
+
export type IconName = keyof typeof ICON_PATHS;
|
|
30
|
+
export declare function Icon({ name, size, className, ...rest }: {
|
|
31
|
+
name: IconName;
|
|
32
|
+
size?: number;
|
|
33
|
+
} & Omit<SVGProps<SVGSVGElement>, 'name'>): import("react").JSX.Element;
|
package/dist/icons.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* The brand family's line icons (brand/README.md): 24 × 24, 1.75 stroke, round
|
|
4
|
+
* caps and joins, currentColor. The same drawings ship in OnePedigree and
|
|
5
|
+
* OneTaxonomy. Decorative by default (aria-hidden): put the words in the button's
|
|
6
|
+
* text or aria-label.
|
|
7
|
+
*/
|
|
8
|
+
export const ICON_PATHS = {
|
|
9
|
+
plus: 'M12 5v14M5 12h14',
|
|
10
|
+
minus: 'M5 12h14',
|
|
11
|
+
fit: 'M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5',
|
|
12
|
+
download: 'M12 4v11M7 10l5 5 5-5M5 20h14',
|
|
13
|
+
print: 'M7 9V4h10v5M7 17H5a1 1 0 0 1-1-1v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5a1 1 0 0 1-1 1h-2M7 14h10v6H7z',
|
|
14
|
+
undo: 'M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11',
|
|
15
|
+
redo: 'm15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13',
|
|
16
|
+
expand: 'm7 15 5 5 5-5M7 9l5-5 5 5',
|
|
17
|
+
collapse: 'm7 20 5-5 5 5M7 4l5 5 5-5',
|
|
18
|
+
flip: 'M7 4v16M4 7l3-3 3 3M17 20V4m-3 13 3 3 3-3',
|
|
19
|
+
focus: 'M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM12 2v4M12 18v4M2 12h4M18 12h4',
|
|
20
|
+
unfocus: 'M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5M9 12h6',
|
|
21
|
+
layout: 'M4 12h4M8 6v12M8 6h5M8 18h5M8 12h9M15 4v4M15 16v4',
|
|
22
|
+
filter: 'M4 5h16l-6 8v6l-4 2v-8z',
|
|
23
|
+
palette: 'M12 3a9 9 0 1 0 0 18c1.1 0 1.5-.8 1.2-1.6-.4-1 .2-2.4 1.6-2.4H17a4 4 0 0 0 4-4c0-5.5-4-10-9-10zM7.5 11.5h.01M10 7.5h.01M15 7.5h.01',
|
|
24
|
+
close: 'M6 6l12 12M18 6 6 18',
|
|
25
|
+
open: 'M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5',
|
|
26
|
+
grip: 'M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01',
|
|
27
|
+
info: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v6M12 7.5h.01',
|
|
28
|
+
};
|
|
29
|
+
export function Icon({ name, size = 16, className, ...rest }) {
|
|
30
|
+
return (_jsx("svg", { viewBox: "0 0 24 24", width: size, height: size, fill: "none", stroke: "currentColor", strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": "true", focusable: "false", className: ["op-icon", className].filter(Boolean).join(" "), ...rest, children: _jsx("path", { d: ICON_PATHS[name] }) }));
|
|
31
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -3,3 +3,5 @@ export { PedigreeExplorer, type PedigreeExplorerProps, type QuickViewProps } fro
|
|
|
3
3
|
export * from './headless.js';
|
|
4
4
|
export { ThemeEditor, type ThemeEditorProps } from './ThemeEditor.js';
|
|
5
5
|
export { AhnentafelList, ahnentafelRelation, type AhnentafelListProps } from './AhnentafelList.js';
|
|
6
|
+
export { Icon, ICON_PATHS, type IconName } from './icons.js';
|
|
7
|
+
export { dropNodeOrder } from './reorder.js';
|
package/dist/index.js
CHANGED
|
@@ -3,3 +3,5 @@ export { PedigreeExplorer } from './PedigreeExplorer.js';
|
|
|
3
3
|
export * from './headless.js';
|
|
4
4
|
export { ThemeEditor } from './ThemeEditor.js';
|
|
5
5
|
export { AhnentafelList, ahnentafelRelation } from './AhnentafelList.js';
|
|
6
|
+
export { Icon, ICON_PATHS } from './icons.js';
|
|
7
|
+
export { dropNodeOrder } from './reorder.js';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { LayoutResult } from '@captiveorigins/onepedigree-core';
|
|
2
|
+
/**
|
|
3
|
+
* Where a dragged node lands in its row (click-and-drag rearranging, shared idea with
|
|
4
|
+
* OneTaxonomy). Rows are generations: same y in vertical charts, same x in horizontal
|
|
5
|
+
* ones. Returns the new `nodeOrder` for `layoutSync` (the row's order replaces any
|
|
6
|
+
* earlier choice for those nodes), or null when nothing changes. The layout applies
|
|
7
|
+
* it only where order is a choice (REORDERABLE_VIEWS), e.g. among siblings.
|
|
8
|
+
*/
|
|
9
|
+
export declare function dropNodeOrder(layout: LayoutResult, occurrenceId: string, delta: {
|
|
10
|
+
dx: number;
|
|
11
|
+
dy: number;
|
|
12
|
+
}, current?: readonly string[],
|
|
13
|
+
/** Nodes only move among others of the same group (e.g. the same clutch), so grouped frames stay truthful. */
|
|
14
|
+
groupOf?: (occurrenceId: string) => string | null | undefined): string[] | null;
|
package/dist/reorder.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a dragged node lands in its row (click-and-drag rearranging, shared idea with
|
|
3
|
+
* OneTaxonomy). Rows are generations: same y in vertical charts, same x in horizontal
|
|
4
|
+
* ones. Returns the new `nodeOrder` for `layoutSync` (the row's order replaces any
|
|
5
|
+
* earlier choice for those nodes), or null when nothing changes. The layout applies
|
|
6
|
+
* it only where order is a choice (REORDERABLE_VIEWS), e.g. among siblings.
|
|
7
|
+
*/
|
|
8
|
+
export function dropNodeOrder(layout, occurrenceId, delta, current = [],
|
|
9
|
+
/** Nodes only move among others of the same group (e.g. the same clutch), so grouped frames stay truthful. */
|
|
10
|
+
groupOf) {
|
|
11
|
+
const vertical = layout.orientation === 'vertical';
|
|
12
|
+
const self = layout.nodes.find((n) => n.occurrenceId === occurrenceId);
|
|
13
|
+
if (!self)
|
|
14
|
+
return null;
|
|
15
|
+
const rowOf = (n) => (vertical ? n.y : n.x);
|
|
16
|
+
const along = (n) => (vertical ? n.x + n.width / 2 : n.y + n.height / 2);
|
|
17
|
+
const group = groupOf?.(occurrenceId);
|
|
18
|
+
const row = layout.nodes.filter((n) => Math.abs(rowOf(n) - rowOf(self)) < 0.5 && (!groupOf || groupOf(n.occurrenceId) === group));
|
|
19
|
+
if (row.length < 2)
|
|
20
|
+
return null;
|
|
21
|
+
const before = [...row].sort((a, b) => along(a) - along(b)).map((n) => n.occurrenceId);
|
|
22
|
+
const moved = along(self) + (vertical ? delta.dx : delta.dy);
|
|
23
|
+
const after = [...row].sort((a, b) => (a === self ? moved : along(a)) - (b === self ? moved : along(b))).map((n) => n.occurrenceId);
|
|
24
|
+
if (after.every((id, i) => id === before[i]))
|
|
25
|
+
return null;
|
|
26
|
+
const inRow = new Set(after);
|
|
27
|
+
return [...current.filter((id) => !inRow.has(id)), ...after];
|
|
28
|
+
}
|
package/dist/styles.css
CHANGED
|
@@ -86,10 +86,18 @@
|
|
|
86
86
|
.op-explorer > svg:active { cursor: grabbing; }
|
|
87
87
|
.op-toolbar { position: absolute; top: 8px; right: 8px; display: flex; gap: 4px; z-index: 1; }
|
|
88
88
|
.op-toolbar button {
|
|
89
|
+
display: inline-flex; align-items: center; justify-content: center;
|
|
89
90
|
min-width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--op-node-border, #6b7a90);
|
|
90
91
|
background: var(--op-node-background, #fff); color: inherit; font: inherit; cursor: pointer;
|
|
91
92
|
}
|
|
92
|
-
|
|
93
|
+
/* Brand family (brand/README.md): shared line icons, Captive Origins orange focus ring. */
|
|
94
|
+
.op-icon { flex: none; }
|
|
95
|
+
.op-explorer[data-reorderable] .op-node { cursor: grab; }
|
|
96
|
+
.op-node[data-dragging] { cursor: grabbing; opacity: 0.85; filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.18)); }
|
|
97
|
+
.op-toolbar button:focus-visible, .op-quick-view-actions :is(a, button):focus-visible { outline: 2px solid #cf8137; outline-offset: 2px; }
|
|
98
|
+
@media (prefers-color-scheme: dark) {
|
|
99
|
+
.op-toolbar button:focus-visible, .op-quick-view-actions :is(a, button):focus-visible { outline-color: #e7a066; }
|
|
100
|
+
}
|
|
93
101
|
.op-quick-view {
|
|
94
102
|
position: absolute; left: 8px; bottom: 8px; max-width: 18rem; padding: 12px 14px; border-radius: 10px;
|
|
95
103
|
background: var(--op-node-background, #fff); border: 1px solid var(--op-node-border, #6b7a90);
|
|
@@ -98,6 +106,7 @@
|
|
|
98
106
|
.op-quick-view p { margin: 0 0 4px; }
|
|
99
107
|
.op-quick-view-title { font-weight: 600; }
|
|
100
108
|
.op-quick-view-actions { display: flex; gap: 8px; margin-top: 8px; }
|
|
109
|
+
.op-quick-view-actions :is(a, button) { display: inline-flex; align-items: center; gap: 4px; }
|
|
101
110
|
.op-theme-panel {
|
|
102
111
|
position: absolute; top: 48px; right: 8px; z-index: 2; max-height: calc(100% - 56px); overflow: auto; width: 20rem;
|
|
103
112
|
padding: 12px; border-radius: 10px; background: var(--op-node-background, #fff); color: var(--op-label, #1d2330);
|
package/package.json
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@captiveorigins/onepedigree-react",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
4
|
-
"description": "OnePedigree for React: server-renderable SVG pedigree views (all 13), interactive explorer, headless hooks and CSS-variable styling.",
|
|
3
|
+
"version": "0.1.0-alpha.9",
|
|
4
|
+
"description": "OnePedigree for React: server-renderable SVG pedigree views (all 13), an interactive explorer, headless hooks and CSS-variable styling.",
|
|
5
|
+
"homepage": "https://github.com/mgilardi/onepedigree#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/mgilardi/onepedigree/issues"
|
|
8
|
+
},
|
|
5
9
|
"keywords": [
|
|
6
10
|
"pedigree",
|
|
7
11
|
"genealogy",
|
|
@@ -42,7 +46,7 @@
|
|
|
42
46
|
"node": ">=22"
|
|
43
47
|
},
|
|
44
48
|
"dependencies": {
|
|
45
|
-
"@captiveorigins/onepedigree-core": "^0.1.0-alpha.
|
|
49
|
+
"@captiveorigins/onepedigree-core": "^0.1.0-alpha.9"
|
|
46
50
|
},
|
|
47
51
|
"peerDependencies": {
|
|
48
52
|
"react": ">=18.2"
|