@captiveorigins/onepedigree-react 0.0.0-stage → 0.1.0-alpha.11

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.
@@ -0,0 +1,76 @@
1
+ import type { SVGProps } from 'react';
2
+ /**
3
+ * The brand family's line icons (brand/README.md), the same in OnePedigree and
4
+ * OneTaxonomy: drawings from Lucide (https://lucide.dev, ISC license, © Lucide
5
+ * Contributors), the set Captive Origins already uses, flattened to one path each.
6
+ * 24 × 24, 1.75 stroke, round caps and joins, currentColor. Decorative by default
7
+ * (aria-hidden): put the words in the button's text, or in aria-label plus a
8
+ * visible tooltip (`data-tip`) when the icon stands alone.
9
+ */
10
+ export declare const ICON_PATHS: {
11
+ /** lucide: plus */
12
+ readonly plus: "M5 12h14M12 5v14";
13
+ /** lucide: minus */
14
+ readonly minus: "M5 12h14";
15
+ /** lucide: zoom-in */
16
+ readonly zoomIn: "M3 11a8 8 0 1 0 16 0a8 8 0 1 0 -16 0M21 21L16.65 16.65M11 8L11 14M8 11L14 11";
17
+ /** lucide: zoom-out */
18
+ readonly zoomOut: "M3 11a8 8 0 1 0 16 0a8 8 0 1 0 -16 0M21 21L16.65 16.65M8 11L14 11";
19
+ /** lucide: scan */
20
+ readonly fit: "M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2";
21
+ /** lucide: download */
22
+ readonly download: "M12 15V3M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m7 10 5 5 5-5";
23
+ /** lucide: printer */
24
+ readonly print: "M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6M7 14h10a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1 -1v-6a1 1 0 0 1 1 -1Z";
25
+ /** lucide: undo-2 */
26
+ readonly undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11";
27
+ /** lucide: redo-2 */
28
+ readonly redo: "m15 14 5-5-5-5M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13";
29
+ /** lucide: chevrons-up-down */
30
+ readonly expand: "m7 15 5 5 5-5m7 9 5-5 5 5";
31
+ /** lucide: chevrons-down-up */
32
+ readonly collapse: "m7 20 5-5 5 5m7 4 5 5 5-5";
33
+ /** lucide: arrow-down-up */
34
+ readonly flip: "m3 16 4 4 4-4M7 20V4m21 8-4-4-4 4M17 4v16";
35
+ /** lucide: focus */
36
+ readonly focus: "M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2";
37
+ /** lucide: expand */
38
+ readonly unfocus: "m15 15 6 6m15 9 6-6M21 16v5h-5M21 8V3h-5M3 16v5h5m3 21 6-6M3 8V3h5M9 9 3 3";
39
+ /** lucide: network */
40
+ readonly layout: "M17 16h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1ZM3 16h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1ZM10 2h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1ZM5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3M12 12V8";
41
+ /** lucide: funnel */
42
+ readonly filter: "M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z";
43
+ /** lucide: palette */
44
+ readonly palette: "M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8zM13 6.5a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0M17 10.5a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0M6 12.5a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0M8 7.5a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0";
45
+ /** lucide: x */
46
+ readonly close: "M18 6 6 18m6 6 12 12";
47
+ /** lucide: external-link */
48
+ readonly open: "M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6";
49
+ /** lucide: grip-vertical */
50
+ readonly grip: "M8 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M8 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M8 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0";
51
+ /** lucide: info */
52
+ readonly info: "M2 12a10 10 0 1 0 20 0a10 10 0 1 0 -20 0M12 16v-4M12 8h.01";
53
+ /** lucide: check */
54
+ readonly check: "M20 6 9 17l-5-5";
55
+ /** lucide: percent */
56
+ readonly support: "M19 5L5 19M4 6.5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0M15 17.5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0";
57
+ /** lucide: bone */
58
+ readonly fossil: "M17 10c.7-.7 1.69 0 2.5 0a2.5 2.5 0 1 0 0-5 .5.5 0 0 1-.5-.5 2.5 2.5 0 1 0-5 0c0 .81.7 1.8 0 2.5l-7 7c-.7.7-1.69 0-2.5 0a2.5 2.5 0 0 0 0 5c.28 0 .5.22.5.5a2.5 2.5 0 1 0 5 0c0-.81-.7-1.8 0-2.5Z";
59
+ /** lucide: image */
60
+ readonly image: "M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2ZM7 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21";
61
+ /** lucide: file-json */
62
+ readonly file: "M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2zM14 2v5a1 1 0 0 0 1 1h5M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1";
63
+ /** lucide: book-open */
64
+ readonly book: "M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z";
65
+ /** lucide: chevron-left */
66
+ readonly previous: "m15 18-6-6 6-6";
67
+ /** lucide: chevron-right */
68
+ readonly next: "m9 18 6-6-6-6";
69
+ /** lucide: dna */
70
+ readonly dna: "m10 16 1.5 1.5m14 8-1.5-1.5M15 2c-1.798 1.998-2.518 3.995-2.807 5.993m16.5 10.5 1 1m17 6-2.891-2.891M2 15c6.667-6 13.333 0 20-6m20 9 .891.891M3.109 14.109 4 15m6.5 12.5 1 1m7 18 2.891 2.891M9 22c1.798-1.998 2.518-3.995 2.807-5.993";
71
+ };
72
+ export type IconName = keyof typeof ICON_PATHS;
73
+ export declare function Icon({ name, size, className, ...rest }: {
74
+ name: IconName;
75
+ size?: number;
76
+ } & Omit<SVGProps<SVGSVGElement>, 'name'>): import("react").JSX.Element;
package/dist/icons.js ADDED
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ /**
3
+ * The brand family's line icons (brand/README.md), the same in OnePedigree and
4
+ * OneTaxonomy: drawings from Lucide (https://lucide.dev, ISC license, © Lucide
5
+ * Contributors), the set Captive Origins already uses, flattened to one path each.
6
+ * 24 × 24, 1.75 stroke, round caps and joins, currentColor. Decorative by default
7
+ * (aria-hidden): put the words in the button's text, or in aria-label plus a
8
+ * visible tooltip (`data-tip`) when the icon stands alone.
9
+ */
10
+ export const ICON_PATHS = {
11
+ /** lucide: plus */
12
+ plus: 'M5 12h14M12 5v14',
13
+ /** lucide: minus */
14
+ minus: 'M5 12h14',
15
+ /** lucide: zoom-in */
16
+ zoomIn: 'M3 11a8 8 0 1 0 16 0a8 8 0 1 0 -16 0M21 21L16.65 16.65M11 8L11 14M8 11L14 11',
17
+ /** lucide: zoom-out */
18
+ zoomOut: 'M3 11a8 8 0 1 0 16 0a8 8 0 1 0 -16 0M21 21L16.65 16.65M8 11L14 11',
19
+ /** lucide: scan */
20
+ fit: 'M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2',
21
+ /** lucide: download */
22
+ download: 'M12 15V3M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4m7 10 5 5 5-5',
23
+ /** lucide: printer */
24
+ print: 'M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2M6 9V3a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v6M7 14h10a1 1 0 0 1 1 1v6a1 1 0 0 1 -1 1h-10a1 1 0 0 1 -1 -1v-6a1 1 0 0 1 1 -1Z',
25
+ /** lucide: undo-2 */
26
+ undo: 'M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11',
27
+ /** lucide: redo-2 */
28
+ redo: 'm15 14 5-5-5-5M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13',
29
+ /** lucide: chevrons-up-down */
30
+ expand: 'm7 15 5 5 5-5m7 9 5-5 5 5',
31
+ /** lucide: chevrons-down-up */
32
+ collapse: 'm7 20 5-5 5 5m7 4 5 5 5-5',
33
+ /** lucide: arrow-down-up */
34
+ flip: 'm3 16 4 4 4-4M7 20V4m21 8-4-4-4 4M17 4v16',
35
+ /** lucide: focus */
36
+ focus: 'M9 12a3 3 0 1 0 6 0a3 3 0 1 0 -6 0M3 7V5a2 2 0 0 1 2-2h2M17 3h2a2 2 0 0 1 2 2v2M21 17v2a2 2 0 0 1-2 2h-2M7 21H5a2 2 0 0 1-2-2v-2',
37
+ /** lucide: expand */
38
+ unfocus: 'm15 15 6 6m15 9 6-6M21 16v5h-5M21 8V3h-5M3 16v5h5m3 21 6-6M3 8V3h5M9 9 3 3',
39
+ /** lucide: network */
40
+ layout: 'M17 16h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1ZM3 16h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1ZM10 2h4a1 1 0 0 1 1 1v4a1 1 0 0 1 -1 1h-4a1 1 0 0 1 -1 -1v-4a1 1 0 0 1 1 -1ZM5 16v-3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v3M12 12V8',
41
+ /** lucide: funnel */
42
+ filter: 'M10 20a1 1 0 0 0 .553.895l2 1A1 1 0 0 0 14 21v-7a2 2 0 0 1 .517-1.341L21.74 4.67A1 1 0 0 0 21 3H3a1 1 0 0 0-.742 1.67l7.225 7.989A2 2 0 0 1 10 14z',
43
+ /** lucide: palette */
44
+ palette: 'M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8zM13 6.5a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0M17 10.5a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0M6 12.5a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0M8 7.5a0.5 0.5 0 1 0 1 0a0.5 0.5 0 1 0 -1 0',
45
+ /** lucide: x */
46
+ close: 'M18 6 6 18m6 6 12 12',
47
+ /** lucide: external-link */
48
+ open: 'M15 3h6v6M10 14 21 3M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6',
49
+ /** lucide: grip-vertical */
50
+ grip: 'M8 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M8 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M8 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 12a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 5a1 1 0 1 0 2 0a1 1 0 1 0 -2 0M14 19a1 1 0 1 0 2 0a1 1 0 1 0 -2 0',
51
+ /** lucide: info */
52
+ info: 'M2 12a10 10 0 1 0 20 0a10 10 0 1 0 -20 0M12 16v-4M12 8h.01',
53
+ /** lucide: check */
54
+ check: 'M20 6 9 17l-5-5',
55
+ /** lucide: percent */
56
+ support: 'M19 5L5 19M4 6.5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0M15 17.5a2.5 2.5 0 1 0 5 0a2.5 2.5 0 1 0 -5 0',
57
+ /** lucide: bone */
58
+ fossil: 'M17 10c.7-.7 1.69 0 2.5 0a2.5 2.5 0 1 0 0-5 .5.5 0 0 1-.5-.5 2.5 2.5 0 1 0-5 0c0 .81.7 1.8 0 2.5l-7 7c-.7.7-1.69 0-2.5 0a2.5 2.5 0 0 0 0 5c.28 0 .5.22.5.5a2.5 2.5 0 1 0 5 0c0-.81-.7-1.8 0-2.5Z',
59
+ /** lucide: image */
60
+ image: 'M5 3h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14a2 2 0 0 1 2 -2ZM7 9a2 2 0 1 0 4 0a2 2 0 1 0 -4 0m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21',
61
+ /** lucide: file-json */
62
+ file: 'M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2zM14 2v5a1 1 0 0 0 1 1h5M10 12a1 1 0 0 0-1 1v1a1 1 0 0 1-1 1 1 1 0 0 1 1 1v1a1 1 0 0 0 1 1M14 18a1 1 0 0 0 1-1v-1a1 1 0 0 1 1-1 1 1 0 0 1-1-1v-1a1 1 0 0 0-1-1',
63
+ /** lucide: book-open */
64
+ book: 'M12 7v14M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z',
65
+ /** lucide: chevron-left */
66
+ previous: 'm15 18-6-6 6-6',
67
+ /** lucide: chevron-right */
68
+ next: 'm9 18 6-6-6-6',
69
+ /** lucide: dna */
70
+ dna: 'm10 16 1.5 1.5m14 8-1.5-1.5M15 2c-1.798 1.998-2.518 3.995-2.807 5.993m16.5 10.5 1 1m17 6-2.891-2.891M2 15c6.667-6 13.333 0 20-6m20 9 .891.891M3.109 14.109 4 15m6.5 12.5 1 1m7 18 2.891 2.891M9 22c1.798-1.998 2.518-3.995 2.807-5.993',
71
+ };
72
+ export function Icon({ name, size = 16, className, ...rest }) {
73
+ 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] }) }));
74
+ }
@@ -0,0 +1,8 @@
1
+ export { PedigreeView, type CohortMarkerSlotProps, type IndividualSlotProps, type PedigreeClassNames, type PedigreeViewProps } from './PedigreeView.js';
2
+ export { PedigreeExplorer, type PedigreeExplorerProps, type QuickViewProps } from './PedigreeExplorer.js';
3
+ export * from './headless.js';
4
+ export { ThemeEditor, type ThemeEditorProps } from './ThemeEditor.js';
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';
8
+ export { dotsWidth, GeneLegend, GenotypeDots, GenotypeGlyph, GenotypeKey, GenotypeStrip, genotypeText, genotypeWord, GENOTYPE_WORDS, type GeneKey, type GeneOverlay, type GenotypeDisplay, type GenotypeMark, type GenotypeWords } from './genetics.js';
package/dist/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { PedigreeView } from './PedigreeView.js';
2
+ export { PedigreeExplorer } from './PedigreeExplorer.js';
3
+ export * from './headless.js';
4
+ export { ThemeEditor } from './ThemeEditor.js';
5
+ export { AhnentafelList, ahnentafelRelation } from './AhnentafelList.js';
6
+ export { Icon, ICON_PATHS } from './icons.js';
7
+ export { dropNodeOrder } from './reorder.js';
8
+ export { dotsWidth, GeneLegend, GenotypeDots, GenotypeGlyph, GenotypeKey, GenotypeStrip, genotypeText, genotypeWord, GENOTYPE_WORDS } from './genetics.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;
@@ -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
+ }
@@ -0,0 +1,176 @@
1
+ /*
2
+ * OnePedigree default styles. Optional: skip this file for unstyled mode and
3
+ * style the stable .op-* classes and data-* attributes yourself, or override
4
+ * the custom properties below.
5
+ */
6
+ .op-root {
7
+ --op-font: system-ui, -apple-system, 'Segoe UI', sans-serif;
8
+ --op-node-background: #ffffff;
9
+ --op-node-border: #6b7a90;
10
+ --op-node-border-width: 1.5px;
11
+ --op-node-border-style: solid;
12
+ --op-label: #1d2330;
13
+ --op-sublabel: #5b6577;
14
+ --op-connector: #8892a6;
15
+ --op-connector-width: 1.5px;
16
+ --op-focal-border: #2f7d5c;
17
+ --op-selected-ring: #f0b35a;
18
+ --op-highlight: #d97706;
19
+ --op-cohort-border: #9aa5b8;
20
+ --op-cohort-label: #5b6577;
21
+ --op-junction: #5b6577;
22
+ --op-fan-fill: #eef2f7;
23
+ --op-fan-fill-alt: #e3e9f1;
24
+ font-family: var(--op-font);
25
+ overflow: visible;
26
+ }
27
+ @media (prefers-color-scheme: dark) {
28
+ .op-root {
29
+ --op-node-background: #161a22;
30
+ --op-node-border: #7dc4a5;
31
+ --op-label: #eef1f6;
32
+ --op-sublabel: #a7b0c0;
33
+ --op-connector: #8892a6;
34
+ --op-cohort-border: #4b5567;
35
+ --op-cohort-label: #a7b0c0;
36
+ --op-junction: #c3cad6;
37
+ --op-fan-fill: #1d2330;
38
+ --op-fan-fill-alt: #232a37;
39
+ }
40
+ }
41
+ .op-node-box {
42
+ fill: var(--op-node-background);
43
+ stroke: var(--op-node-border);
44
+ stroke-width: var(--op-node-border-width);
45
+ stroke-dasharray: var(--op-node-border-dash, none);
46
+ }
47
+ .op-label { fill: var(--op-label); font-size: 13px; font-weight: 600; }
48
+ .op-sublabel { fill: var(--op-sublabel); font-size: 11px; }
49
+ .op-repeat-marker { fill: var(--op-highlight); font-size: 13px; }
50
+ .op-connector { fill: none; stroke: var(--op-connector); stroke-width: var(--op-connector-width); }
51
+ /* Reproductive mode by line pattern (never color alone). */
52
+ .op-connector[data-reproductive-mode='parthenogenesis'] { stroke-dasharray: 6 4; }
53
+ .op-connector[data-reproductive-mode='self'] { stroke-dasharray: 2 3; }
54
+ .op-connector[data-reproductive-mode='asexual'],
55
+ .op-connector[data-reproductive-mode='cloning'] { stroke-dasharray: 1 4; stroke-linecap: round; }
56
+ .op-connector[data-completeness='incomplete'] { stroke-opacity: 0.6; }
57
+ .op-connector[data-highlighted] { stroke: var(--op-highlight); stroke-width: 2.5px; }
58
+ .op-junction { fill: var(--op-junction); stroke: var(--op-junction); stroke-width: 1.5px; }
59
+ .op-junction[data-completeness='incomplete'] { fill: var(--op-node-background); }
60
+ .op-individual[data-repeated] { --op-node-border-dash: 5 3; }
61
+ .op-individual[data-focal] .op-node-box { stroke: var(--op-focal-border); stroke-width: 2.5px; }
62
+ .op-individual[data-highlighted] .op-node-box,
63
+ .op-individual[data-highlighted] .op-fan-segment { stroke: var(--op-highlight); stroke-width: 2.5px; }
64
+ .op-individual[data-selected] .op-node-box,
65
+ .op-individual[data-selected] .op-fan-segment { stroke: var(--op-selected-ring); stroke-width: 3.5px; }
66
+ .op-individual[data-collapsed] { opacity: 0.75; }
67
+ .op-individual[role='button'] { cursor: pointer; }
68
+ .op-individual:focus { outline: none; }
69
+ .op-individual:focus-visible .op-node-box,
70
+ .op-individual:focus-visible .op-fan-segment { stroke: var(--op-selected-ring); stroke-width: 3px; stroke-dasharray: 3 2; }
71
+ .op-individual a:focus-visible .op-node-box { stroke: var(--op-selected-ring); stroke-width: 3px; }
72
+ .op-fan-segment { fill: var(--op-fan-fill); stroke: var(--op-node-border); stroke-width: 1px; }
73
+ .op-individual[data-generation='1'] .op-fan-segment,
74
+ .op-individual[data-generation='3'] .op-fan-segment,
75
+ .op-individual[data-generation='5'] .op-fan-segment { fill: var(--op-fan-fill-alt); }
76
+ .op-fan-label { pointer-events: none; }
77
+ .op-life-bar[data-open] { stroke-dasharray: 4 3; }
78
+ .op-axis-line { stroke: var(--op-connector); stroke-width: 1px; }
79
+ .op-axis-tick { stroke: var(--op-connector); stroke-opacity: 0.18; stroke-width: 1px; }
80
+ .op-view-timeline .op-connector { stroke-opacity: 0.55; }
81
+ .op-cohort-box { fill: none; stroke: var(--op-cohort-border); stroke-dasharray: 4 4; }
82
+ .op-cohort-label { fill: var(--op-cohort-label); font-size: 11px; }
83
+
84
+ .op-explorer { position: relative; overflow: hidden; touch-action: none; border-radius: 12px; }
85
+ .op-explorer > svg { display: block; width: 100%; height: 100%; cursor: grab; }
86
+ .op-explorer > svg:active { cursor: grabbing; }
87
+ .op-toolbar { position: absolute; top: 8px; right: 8px; display: flex; gap: 4px; z-index: 1; }
88
+ .op-toolbar button {
89
+ display: inline-flex; align-items: center; justify-content: center;
90
+ min-width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--op-node-border, #6b7a90);
91
+ background: var(--op-node-background, #fff); color: inherit; font: inherit; cursor: pointer;
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
+ }
101
+ .op-quick-view {
102
+ position: absolute; left: 8px; bottom: 8px; max-width: 18rem; padding: 12px 14px; border-radius: 10px;
103
+ background: var(--op-node-background, #fff); border: 1px solid var(--op-node-border, #6b7a90);
104
+ font: 13px/1.4 system-ui, sans-serif; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
105
+ }
106
+ .op-quick-view p { margin: 0 0 4px; }
107
+ .op-quick-view-title { font-weight: 600; }
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; }
110
+ .op-theme-panel {
111
+ position: absolute; top: 48px; right: 8px; z-index: 2; max-height: calc(100% - 56px); overflow: auto; width: 20rem;
112
+ padding: 12px; border-radius: 10px; background: var(--op-node-background, #fff); color: var(--op-label, #1d2330);
113
+ border: 1px solid var(--op-node-border, #6b7a90); font: 13px/1.4 system-ui, sans-serif; box-shadow: 0 6px 24px rgba(0, 0, 0, 0.18);
114
+ }
115
+ .op-ahnentafel { padding: 56px 20px 20px; overflow: auto; height: 100%; box-sizing: border-box; color: var(--op-label, #1d2330); font: 14px/1.5 var(--op-font, system-ui, sans-serif); }
116
+ .op-ahnentafel h3 { margin: 16px 0 6px; font-size: 13px; text-transform: uppercase; letter-spacing: 0.06em; color: var(--op-sublabel, #5b6577); }
117
+ .op-ahnentafel ol { list-style: none; margin: 0; padding: 0; columns: 18rem; column-gap: 24px; }
118
+ .op-ahnentafel li { break-inside: avoid; padding: 2px 0; }
119
+ .op-ahnentafel li[data-selected] { font-weight: 600; }
120
+ .op-ahnentafel button { all: unset; cursor: pointer; text-decoration: underline; text-underline-offset: 2px; }
121
+ .op-ahnentafel-number { display: inline-block; min-width: 2.5em; text-align: end; color: var(--op-sublabel, #5b6577); font-variant-numeric: tabular-nums; }
122
+ .op-ahnentafel-years, .op-ahnentafel-relation, .op-ahnentafel-note { color: var(--op-sublabel, #5b6577); font-size: 13px; }
123
+ .op-theme-editor-title { font-weight: 600; margin: 0 0 8px; }
124
+ .op-theme-row { display: grid; grid-template-columns: 1fr 2.25rem 5.5rem; align-items: center; gap: 6px; margin: 4px 0; }
125
+ .op-theme-row select { grid-column: 2 / 4; }
126
+ .op-theme-row input[type='color'] { width: 2.25rem; height: 1.75rem; padding: 0; border: 1px solid #aab; border-radius: 6px; background: none; }
127
+ .op-theme-row input[type='text'] { font: 12px ui-monospace, monospace; padding: 3px 6px; border: 1px solid #aab; border-radius: 6px; }
128
+ .op-theme-row input[aria-invalid='true'] { border-color: #c00; }
129
+ .op-theme-hint { grid-column: 1 / 4; font-size: 11px; color: #b45309; }
130
+ .op-theme-scope { border: 0; padding: 0; margin: 6px 0; display: flex; flex-wrap: wrap; gap: 4px 12px; }
131
+ .op-theme-scope legend { width: 100%; margin-bottom: 2px; }
132
+ @media (prefers-reduced-motion: reduce) {
133
+ .op-root * { transition: none !important; animation: none !important; }
134
+ }
135
+ @media print {
136
+ html[data-op-printing] body * { visibility: hidden; }
137
+ html[data-op-printing] [data-op-printing], html[data-op-printing] [data-op-printing] * { visibility: visible; }
138
+ html[data-op-printing] [data-op-printing] { position: fixed; inset: 0; height: 100% !important; border-radius: 0; }
139
+ .op-toolbar, .op-quick-view, .op-theme-panel { display: none; }
140
+ .op-explorer { height: auto !important; overflow: visible; }
141
+ .op-root { --op-node-background: #fff; --op-label: #000; --op-connector: #555; }
142
+ }
143
+
144
+ /* Heritable trait overlay: genetic pedigree symbols (filled = visual, half = het, dot = possible het, open = not carrying). */
145
+ .op-gene-outline { fill: var(--op-node-background, #fff); stroke: var(--op-label, #4c312c); stroke-width: 1.5px; }
146
+ .op-gene-outline[data-dashed] { stroke-dasharray: 2 2; }
147
+ .op-gene-fill { fill: var(--op-label, #4c312c); }
148
+ .op-gene-ring { fill: none; stroke: var(--op-label, #4c312c); stroke-width: 1.25px; }
149
+ .op-gene-text { font-size: 9px; fill: var(--op-label, #4c312c); }
150
+ .op-gene[data-state='unknown'] .op-gene-outline { stroke: #9a8f8b; }
151
+ .op-gene[data-state='unknown'] .op-gene-text { fill: #9a8f8b; }
152
+ .op-gene-legends { position: absolute; left: 8px; bottom: 8px; z-index: 1; max-width: calc(100% - 16px); display: flex; flex-wrap: wrap; gap: 6px; align-items: flex-end; }
153
+ .op-gene-legend { padding: 6px 10px; border: 1px solid #eaddd6; border-radius: 10px; background: rgba(255, 255, 255, 0.94); font-size: 12px; color: #4c312c; }
154
+ .op-gene-legend ul { list-style: none; display: flex; flex-wrap: wrap; gap: 2px 12px; margin: 4px 0 0; padding: 0; }
155
+ .op-gene-legend li { display: inline-flex; align-items: center; gap: 4px; }
156
+ /* Visible tooltips for icon-only buttons, on hover and on keyboard focus (title= shows on hover only). */
157
+ .op-toolbar [data-tip] { position: relative; }
158
+ .op-toolbar [data-tip]:is(:hover, :focus-visible)::after {
159
+ content: attr(data-tip); position: absolute; top: calc(100% + 6px); right: 0; z-index: 2; white-space: nowrap; pointer-events: none;
160
+ padding: 4px 8px; border-radius: 6px; background: #4c312c; color: #fff; font-size: 12px; line-height: 1.3;
161
+ }
162
+
163
+ /* Allele dots: two per gene, one per copy. Filled = carries it (gene color), empty = normal copy,
164
+ dashed = possible, grey dashed = not known. Shape carries the meaning, not color. */
165
+ .op-dot { fill: var(--op-node-background, #fff); stroke: var(--op-label, #4c312c); stroke-width: 1.25px; }
166
+ .op-dot[data-dot='possible'] { stroke-dasharray: 1.6 1.6; }
167
+ /* Suggested from relatives (not proven): the copy is drawn with a dashed edge. */
168
+ .op-dot[data-inferred] { stroke-dasharray: 2 1.4; stroke-width: 1.5px; }
169
+ .op-dot[data-dot='unknown'] { stroke: #9a8f8b; stroke-dasharray: 1.6 1.6; }
170
+ .op-gene-swatch { display: inline-block; width: 10px; height: 10px; border-radius: 50%; border: 1px solid var(--op-label, #4c312c); }
171
+ .op-genotype-key ul + ul { margin-top: 2px; }
172
+
173
+ /* With a gene legend, the legend sits under the drawing instead of covering it. */
174
+ .op-explorer[data-legend] { display: flex; flex-direction: column; }
175
+ .op-explorer[data-legend] > svg { flex: 1; min-height: 0; height: auto; }
176
+ .op-explorer[data-legend] .op-gene-legends { position: static; margin: 0 8px 8px; max-width: none; }
@@ -0,0 +1,24 @@
1
+ import type { PedigreeViewId } from '@captiveorigins/onepedigree-core';
2
+ import type { CSSProperties } from 'react';
3
+ /** Colors a theme sets; each maps to a `--op-*` custom property. */
4
+ export declare const THEME_TOKENS: readonly ["background", "nodeBackground", "nodeBorder", "label", "sublabel", "connector", "focalBorder", "selectedRing", "highlight", "cohortBorder", "junction", "fanFill", "fanFillAlt"];
5
+ export type ThemeToken = (typeof THEME_TOKENS)[number];
6
+ export type ThemeColors = Record<ThemeToken, string>;
7
+ export declare const TOKEN_LABELS: Record<ThemeToken, string>;
8
+ export type PedigreeTheme = {
9
+ id: string;
10
+ label: string;
11
+ colors: ThemeColors;
12
+ /** Per-view adjustments on top of `colors` (e.g. warm maternal, cool paternal lines). */
13
+ views?: Partial<Record<PedigreeViewId, Partial<ThemeColors>>>;
14
+ };
15
+ export declare const isHexColor: (v: unknown) => v is string;
16
+ /** Built-in presets, each with its own per-view accents. */
17
+ export declare const THEME_PRESETS: readonly PedigreeTheme[];
18
+ export declare const presetTheme: (id: string) => PedigreeTheme | undefined;
19
+ /** Colors for one view: preset, then its view accents, then the theme's own overrides; invalid hex values are ignored. */
20
+ export declare function resolveThemeColors(themeOrId: PedigreeTheme | string | undefined, view: PedigreeViewId): ThemeColors;
21
+ /** Inline custom properties for a view (works in server rendering). No theme: none (the stylesheet decides). */
22
+ export declare function themeStyle(themeOrId: PedigreeTheme | string | undefined, view: PedigreeViewId): CSSProperties | undefined;
23
+ /** WCAG contrast ratio of two hex colors (1–21). */
24
+ export declare function contrastRatio(a: string, b: string): number;
package/dist/themes.js ADDED
@@ -0,0 +1,216 @@
1
+ /** Colors a theme sets; each maps to a `--op-*` custom property. */
2
+ export const THEME_TOKENS = [
3
+ 'background',
4
+ 'nodeBackground',
5
+ 'nodeBorder',
6
+ 'label',
7
+ 'sublabel',
8
+ 'connector',
9
+ 'focalBorder',
10
+ 'selectedRing',
11
+ 'highlight',
12
+ 'cohortBorder',
13
+ 'junction',
14
+ 'fanFill',
15
+ 'fanFillAlt',
16
+ ];
17
+ export const TOKEN_LABELS = {
18
+ background: 'Background',
19
+ nodeBackground: 'Card',
20
+ nodeBorder: 'Card border',
21
+ label: 'Name text',
22
+ sublabel: 'Detail text',
23
+ connector: 'Lines',
24
+ focalBorder: 'Focal individual',
25
+ selectedRing: 'Selection',
26
+ highlight: 'Highlight',
27
+ cohortBorder: 'Cohort outline',
28
+ junction: 'Junctions',
29
+ fanFill: 'Fan rings',
30
+ fanFillAlt: 'Fan rings (alternate)',
31
+ };
32
+ const VAR = {
33
+ background: '--op-background',
34
+ nodeBackground: '--op-node-background',
35
+ nodeBorder: '--op-node-border',
36
+ label: '--op-label',
37
+ sublabel: '--op-sublabel',
38
+ connector: '--op-connector',
39
+ focalBorder: '--op-focal-border',
40
+ selectedRing: '--op-selected-ring',
41
+ highlight: '--op-highlight',
42
+ cohortBorder: '--op-cohort-border',
43
+ junction: '--op-junction',
44
+ fanFill: '--op-fan-fill',
45
+ fanFillAlt: '--op-fan-fill-alt',
46
+ };
47
+ export const isHexColor = (v) => typeof v === 'string' && /^#(?:[0-9a-f]{3}|[0-9a-f]{6})$/i.test(v);
48
+ const theme = (id, label, colors, views) => ({ id, label, colors, views });
49
+ /** Built-in presets, each with its own per-view accents. */
50
+ export const THEME_PRESETS = [
51
+ theme('classic', 'Classic', {
52
+ background: '#ffffff',
53
+ nodeBackground: '#ffffff',
54
+ nodeBorder: '#6b7a90',
55
+ label: '#1d2330',
56
+ sublabel: '#5b6577',
57
+ connector: '#8892a6',
58
+ focalBorder: '#2f7d5c',
59
+ selectedRing: '#f0b35a',
60
+ highlight: '#d97706',
61
+ cohortBorder: '#9aa5b8',
62
+ junction: '#5b6577',
63
+ fanFill: '#eef2f7',
64
+ fanFillAlt: '#e3e9f1',
65
+ }, {
66
+ maternal: { focalBorder: '#b4436c', connector: '#c76b8e', nodeBorder: '#c76b8e' },
67
+ paternal: { focalBorder: '#2b6cb0', connector: '#5a8fd0', nodeBorder: '#5a8fd0' },
68
+ inbreeding: { highlight: '#c2410c' },
69
+ relationship: { highlight: '#7c3aed' },
70
+ fan: { fanFill: '#e8f1ec', fanFillAlt: '#d7e8de' },
71
+ timeline: { connector: '#a7b0c0' },
72
+ population: { connector: '#b4bccb' },
73
+ }),
74
+ theme('midnight', 'Midnight', {
75
+ background: '#0e1117',
76
+ nodeBackground: '#161a22',
77
+ nodeBorder: '#7dc4a5',
78
+ label: '#eef1f6',
79
+ sublabel: '#a7b0c0',
80
+ connector: '#8892a6',
81
+ focalBorder: '#7dc4a5',
82
+ selectedRing: '#f0b35a',
83
+ highlight: '#f59e0b',
84
+ cohortBorder: '#4b5567',
85
+ junction: '#c3cad6',
86
+ fanFill: '#1d2330',
87
+ fanFillAlt: '#232a37',
88
+ }, {
89
+ maternal: { nodeBorder: '#f29bb9', connector: '#f29bb9', focalBorder: '#f29bb9' },
90
+ paternal: { nodeBorder: '#8ab8f0', connector: '#8ab8f0', focalBorder: '#8ab8f0' },
91
+ inbreeding: { highlight: '#fb923c' },
92
+ relationship: { highlight: '#c4b5fd' },
93
+ fan: { fanFill: '#18232a', fanFillAlt: '#1f2e35' },
94
+ }),
95
+ theme('forest', 'Forest', {
96
+ background: '#f6f8f3',
97
+ nodeBackground: '#ffffff',
98
+ nodeBorder: '#5d7a52',
99
+ label: '#1f2a1c',
100
+ sublabel: '#56654f',
101
+ connector: '#8aa07e',
102
+ focalBorder: '#2f5d2a',
103
+ selectedRing: '#d9a441',
104
+ highlight: '#b45309',
105
+ cohortBorder: '#a3b398',
106
+ junction: '#56654f',
107
+ fanFill: '#e6efdf',
108
+ fanFillAlt: '#d6e4cb',
109
+ }, {
110
+ maternal: { nodeBorder: '#8f5e3b', connector: '#a8774f' },
111
+ paternal: { nodeBorder: '#3d6b62', connector: '#5b8a80' },
112
+ relationship: { highlight: '#9a3412' },
113
+ }),
114
+ theme('ocean', 'Ocean', {
115
+ background: '#f3f8fb',
116
+ nodeBackground: '#ffffff',
117
+ nodeBorder: '#4c7a99',
118
+ label: '#122533',
119
+ sublabel: '#4b6475',
120
+ connector: '#7fa3bb',
121
+ focalBorder: '#0f5e8c',
122
+ selectedRing: '#f2a65a',
123
+ highlight: '#e07a2e',
124
+ cohortBorder: '#9fbccf',
125
+ junction: '#4b6475',
126
+ fanFill: '#e2eef6',
127
+ fanFillAlt: '#d0e3ef',
128
+ }, {
129
+ maternal: { nodeBorder: '#a35a8a', connector: '#bb7aa4' },
130
+ paternal: { nodeBorder: '#1f6fa3', connector: '#4f92c0' },
131
+ inbreeding: { highlight: '#c2410c' },
132
+ }),
133
+ theme('terracotta', 'Terracotta', {
134
+ background: '#fbf6f1',
135
+ nodeBackground: '#fffdfa',
136
+ nodeBorder: '#a0674a',
137
+ label: '#2d1d14',
138
+ sublabel: '#6e5446',
139
+ connector: '#c49a83',
140
+ focalBorder: '#8a3f1f',
141
+ selectedRing: '#3f7f6f',
142
+ highlight: '#3f7f6f',
143
+ cohortBorder: '#d9bba8',
144
+ junction: '#6e5446',
145
+ fanFill: '#f5e6da',
146
+ fanFillAlt: '#ecd5c4',
147
+ }, {
148
+ maternal: { nodeBorder: '#b0546b', connector: '#c57789' },
149
+ paternal: { nodeBorder: '#4f6f8a', connector: '#7290a8' },
150
+ }),
151
+ theme('high-contrast', 'High contrast', {
152
+ background: '#ffffff',
153
+ nodeBackground: '#ffffff',
154
+ nodeBorder: '#000000',
155
+ label: '#000000',
156
+ sublabel: '#1a1a1a',
157
+ connector: '#000000',
158
+ focalBorder: '#0000cc',
159
+ selectedRing: '#cc0000',
160
+ highlight: '#cc0000',
161
+ cohortBorder: '#000000',
162
+ junction: '#000000',
163
+ fanFill: '#ffffff',
164
+ fanFillAlt: '#e6e6e6',
165
+ }, {}),
166
+ theme('print', 'Print (grayscale)', {
167
+ background: '#ffffff',
168
+ nodeBackground: '#ffffff',
169
+ nodeBorder: '#444444',
170
+ label: '#000000',
171
+ sublabel: '#333333',
172
+ connector: '#555555',
173
+ focalBorder: '#000000',
174
+ selectedRing: '#000000',
175
+ highlight: '#000000',
176
+ cohortBorder: '#888888',
177
+ junction: '#333333',
178
+ fanFill: '#f2f2f2',
179
+ fanFillAlt: '#e0e0e0',
180
+ }, {}),
181
+ ];
182
+ export const presetTheme = (id) => THEME_PRESETS.find((t) => t.id === id);
183
+ /** Colors for one view: preset, then its view accents, then the theme's own overrides; invalid hex values are ignored. */
184
+ export function resolveThemeColors(themeOrId, view) {
185
+ const t = typeof themeOrId === 'string' ? presetTheme(themeOrId) : themeOrId;
186
+ // Invalid or missing values fall back to the theme's own preset (else Classic).
187
+ const preset = (t && presetTheme(t.id)) ?? THEME_PRESETS[0];
188
+ const out = { ...preset.colors };
189
+ for (const layer of [preset !== t ? preset.views?.[view] : undefined, t?.colors, t?.views?.[view]])
190
+ for (const k of THEME_TOKENS) {
191
+ const v = layer?.[k];
192
+ if (isHexColor(v))
193
+ out[k] = v;
194
+ }
195
+ return out;
196
+ }
197
+ /** Inline custom properties for a view (works in server rendering). No theme: none (the stylesheet decides). */
198
+ export function themeStyle(themeOrId, view) {
199
+ if (!themeOrId)
200
+ return undefined;
201
+ const colors = resolveThemeColors(themeOrId, view);
202
+ return Object.fromEntries(THEME_TOKENS.map((k) => [VAR[k], colors[k]]));
203
+ }
204
+ /** WCAG contrast ratio of two hex colors (1–21). */
205
+ export function contrastRatio(a, b) {
206
+ const lum = (hex) => {
207
+ const h = hex.length === 4 ? hex.replace(/^#(.)(.)(.)$/, '#$1$1$2$2$3$3') : hex;
208
+ const [r, g, bl] = [1, 3, 5].map((i) => {
209
+ const c = parseInt(h.slice(i, i + 2), 16) / 255;
210
+ return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
211
+ });
212
+ return 0.2126 * r + 0.7152 * g + 0.0722 * bl;
213
+ };
214
+ const [x, y] = [lum(a), lum(b)].sort((m, n) => n - m);
215
+ return (x + 0.05) / (y + 0.05);
216
+ }