@captiveorigins/onetaxonomy-react 0.1.0-alpha.0 → 0.1.0-alpha.2
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 +75 -21
- package/THIRD-PARTY-LICENSE-LUCIDE.txt +43 -0
- package/dist/Explorer.d.ts +1 -1
- package/dist/Explorer.js +84 -6
- package/dist/Lesson.d.ts +35 -0
- package/dist/Lesson.js +104 -0
- package/dist/TreeView.d.ts +4 -0
- package/dist/TreeView.js +2 -2
- package/dist/drag.d.ts +8 -0
- package/dist/drag.js +23 -0
- package/dist/icons.d.ts +65 -27
- package/dist/icons.js +65 -27
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/styles.css +49 -6
- package/package.json +7 -5
package/LICENSE
CHANGED
|
@@ -1,21 +1,75 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
1
|
+
Required Notice: Copyright 2026 Captive Origins (https://captiveorigins.com)
|
|
2
|
+
|
|
3
|
+
# PolyForm Noncommercial License 1.0.0
|
|
4
|
+
|
|
5
|
+
<https://polyformproject.org/licenses/noncommercial/1.0.0>
|
|
6
|
+
|
|
7
|
+
## Acceptance
|
|
8
|
+
|
|
9
|
+
In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
|
|
10
|
+
|
|
11
|
+
## Copyright License
|
|
12
|
+
|
|
13
|
+
The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
|
|
14
|
+
|
|
15
|
+
## Distribution License
|
|
16
|
+
|
|
17
|
+
The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
|
|
18
|
+
|
|
19
|
+
## Notices
|
|
20
|
+
|
|
21
|
+
You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
|
|
22
|
+
|
|
23
|
+
> Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
|
|
24
|
+
|
|
25
|
+
## Changes and New Works License
|
|
26
|
+
|
|
27
|
+
The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
|
|
28
|
+
|
|
29
|
+
## Patent License
|
|
30
|
+
|
|
31
|
+
The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
|
|
32
|
+
|
|
33
|
+
## Noncommercial Purposes
|
|
34
|
+
|
|
35
|
+
Any noncommercial purpose is a permitted purpose.
|
|
36
|
+
|
|
37
|
+
## Personal Uses
|
|
38
|
+
|
|
39
|
+
Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
|
|
40
|
+
|
|
41
|
+
## Noncommercial Organizations
|
|
42
|
+
|
|
43
|
+
Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
|
|
44
|
+
|
|
45
|
+
## Fair Use
|
|
46
|
+
|
|
47
|
+
You may have "fair use" rights for the software under the law. These terms do not limit them.
|
|
48
|
+
|
|
49
|
+
## No Other Rights
|
|
50
|
+
|
|
51
|
+
These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
|
|
52
|
+
|
|
53
|
+
## Patent Defense
|
|
54
|
+
|
|
55
|
+
If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
|
|
56
|
+
|
|
57
|
+
## Violations
|
|
58
|
+
|
|
59
|
+
The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
|
|
60
|
+
|
|
61
|
+
## No Liability
|
|
62
|
+
|
|
63
|
+
***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
|
|
64
|
+
|
|
65
|
+
## Definitions
|
|
66
|
+
|
|
67
|
+
The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
|
|
68
|
+
|
|
69
|
+
**You** refers to the individual or entity agreeing to these terms.
|
|
70
|
+
|
|
71
|
+
**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
|
|
72
|
+
|
|
73
|
+
**Your licenses** are all the licenses granted to you for the software under these terms.
|
|
74
|
+
|
|
75
|
+
**Use** means anything you do with the software requiring one of your licenses.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
ISC License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Lucide Icons and Contributors
|
|
4
|
+
|
|
5
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
6
|
+
purpose with or without fee is hereby granted, provided that the above
|
|
7
|
+
copyright notice and this permission notice appear in all copies.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
10
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
11
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
12
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
13
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
14
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
15
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
The following Lucide icons are derived from the Feather project:
|
|
20
|
+
|
|
21
|
+
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
|
22
|
+
|
|
23
|
+
The MIT License (MIT) (for the icons listed above)
|
|
24
|
+
|
|
25
|
+
Copyright (c) 2013-present Cole Bemis
|
|
26
|
+
|
|
27
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
28
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
29
|
+
in the Software without restriction, including without limitation the rights
|
|
30
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
31
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
32
|
+
furnished to do so, subject to the following conditions:
|
|
33
|
+
|
|
34
|
+
The above copyright notice and this permission notice shall be included in all
|
|
35
|
+
copies or substantial portions of the Software.
|
|
36
|
+
|
|
37
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
38
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
39
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
40
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
41
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
42
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
43
|
+
SOFTWARE.
|
package/dist/Explorer.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare function useTaxonomyView(dataset: DatasetIndex, initial: Taxonomi
|
|
|
18
18
|
}): UseTaxonomyView;
|
|
19
19
|
/** Lays out a projection for a view's layout kind (sunburst is drawn as a rectangular outline here). */
|
|
20
20
|
export declare function layoutFor(projection: Projection, kind: LayoutKind): TreeLayout;
|
|
21
|
-
export type ExplorerLabels = Record<'undo' | 'redo' | 'expandAll' | 'collapseAll' | 'rotate' | 'layout' | 'status' | 'allStatuses' | 'outline' | 'details' | 'nothingSelected' | 'clade' | 'group' | 'hidden' | 'rank' | 'lifeStatus' | 'focus' | 'unfocus' | 'zoomIn' | 'zoomOut' | 'fit' | 'download' | 'print' | 'canvas' | 'controls' | 'dragHint' | 'overlays' | 'support' | 'extinction' | 'trait' | 'noTrait' | 'noStatedValue' | 'mixed' | 'weakSupport' | 'selection' | 'checkClade' | 'addHighlight' | 'highlightName' | 'checkAgainst' | 'clearSelection' | 'remove' | 'multiHint' | 'downloadSvg' | 'downloadPng' | 'downloadView' | 'about' | 'sources' | 'references' | 'synonyms' | 'commonNames' | 'age' | 'branchLength' | 'unmapped' | 'traits', string>;
|
|
21
|
+
export type ExplorerLabels = Record<'undo' | 'redo' | 'expandAll' | 'collapseAll' | 'rotate' | 'layout' | 'status' | 'allStatuses' | 'outline' | 'details' | 'nothingSelected' | 'clade' | 'group' | 'hidden' | 'rank' | 'lifeStatus' | 'focus' | 'unfocus' | 'zoomIn' | 'zoomOut' | 'fit' | 'download' | 'print' | 'canvas' | 'controls' | 'dragHint' | 'propose' | 'proposalTitle' | 'proposalAuthor' | 'proposalStart' | 'proposalCancel' | 'proposalHint' | 'proposalBy' | 'proposalLeave' | 'proposalNoMoves' | 'proposalUnder' | 'proposalRoot' | 'proposalRevert' | 'proposalAccepted' | 'proposalProposed' | 'proposalMoveUnder' | 'proposalGained' | 'proposalLost' | 'proposalCompare' | 'proposalDefaultTitle' | 'overlays' | 'support' | 'extinction' | 'trait' | 'noTrait' | 'noStatedValue' | 'mixed' | 'weakSupport' | 'selection' | 'checkClade' | 'addHighlight' | 'highlightName' | 'checkAgainst' | 'clearSelection' | 'remove' | 'multiHint' | 'downloadSvg' | 'downloadPng' | 'downloadView' | 'about' | 'sources' | 'references' | 'synonyms' | 'commonNames' | 'age' | 'branchLength' | 'unmapped' | 'traits', string>;
|
|
22
22
|
export type ExplorerProps = {
|
|
23
23
|
dataset: DatasetIndex;
|
|
24
24
|
initialView: TaxonomicView;
|
package/dist/Explorer.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
-
import { createHistory, describeAssessment, describeView, makeHighlight, dispatch as dispatchAction, inspectNode, nodeOverlays, serializeView, layoutProblem, projectView, redo as redoHistory, undo as undoHistory, } from '@captiveorigins/onetaxonomy-core';
|
|
3
|
+
import { createHistory, describeAssessment, describeView, makeHighlight, dispatch as dispatchAction, inspectNode, nodeOverlays, acceptedTree, compareHierarchies, viewTree, serializeView, layoutProblem, projectView, redo as redoHistory, undo as undoHistory, } from '@captiveorigins/onetaxonomy-core';
|
|
4
4
|
import { radial, rectangular } from '@captiveorigins/onetaxonomy-layout';
|
|
5
5
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
-
import { dropOrder, drawnSubtree } from './drag.js';
|
|
6
|
+
import { dropOrder, drawnSubtree, dropTarget } from './drag.js';
|
|
7
7
|
import { citationLine, disclosureLines, downloadBlob, standaloneSvg, svgToPng } from './export.js';
|
|
8
8
|
import { Icon } from './icons.js';
|
|
9
9
|
import { TreeOutline } from './TreeOutline.js';
|
|
@@ -75,6 +75,25 @@ const LABELS = {
|
|
|
75
75
|
print: 'Print',
|
|
76
76
|
canvas: 'Tree drawing: drag to pan, Ctrl or ⌘ + scroll to zoom, drag a taxon to reorder it among its siblings, double-click to fold',
|
|
77
77
|
controls: 'Drawing controls',
|
|
78
|
+
propose: 'Propose changes',
|
|
79
|
+
proposalTitle: 'Name of this proposal',
|
|
80
|
+
proposalAuthor: 'Your name',
|
|
81
|
+
proposalStart: 'Start proposing',
|
|
82
|
+
proposalCancel: 'Cancel',
|
|
83
|
+
proposalHint: 'Drag a taxon onto its new parent, or select it and choose below. The accepted data never changes.',
|
|
84
|
+
proposalBy: 'Proposed by',
|
|
85
|
+
proposalLeave: 'Leave Proposed mode',
|
|
86
|
+
proposalNoMoves: 'No moves yet.',
|
|
87
|
+
proposalUnder: 'under',
|
|
88
|
+
proposalRoot: 'the top level',
|
|
89
|
+
proposalRevert: 'Revert',
|
|
90
|
+
proposalAccepted: 'Accepted',
|
|
91
|
+
proposalProposed: 'Proposed',
|
|
92
|
+
proposalMoveUnder: 'Move under',
|
|
93
|
+
proposalGained: 'Gains',
|
|
94
|
+
proposalLost: 'Loses',
|
|
95
|
+
proposalCompare: 'Compare with the accepted arrangement',
|
|
96
|
+
proposalDefaultTitle: 'My proposal',
|
|
78
97
|
dragHint: 'Drag a taxon up or down to reorder it among its siblings; double-click a branch to fold it.',
|
|
79
98
|
overlays: 'Overlays',
|
|
80
99
|
support: 'Support values',
|
|
@@ -139,6 +158,7 @@ export function Explorer({ dataset, initialView, title, height = '32rem', labels
|
|
|
139
158
|
const disclosure = useMemo(() => describeView(dataset, view, projection), [dataset, view, projection]);
|
|
140
159
|
const details = useMemo(() => (selected && node ? inspectNode(dataset, view, selected) : null), [dataset, view, selected, node]);
|
|
141
160
|
const traits = dataset.snapshot.traits ?? [];
|
|
161
|
+
const isUnrooted = view.target.kind === 'phylogeny' && !dataset.phylogeny(view.target.id).rooted;
|
|
142
162
|
const hasSupport = projection.nodes.some((n) => n.support !== undefined);
|
|
143
163
|
const hasExtinct = projection.nodes.some((n) => n.status === 'extinct');
|
|
144
164
|
const overlays = view.overlays ?? {};
|
|
@@ -152,6 +172,9 @@ export function Explorer({ dataset, initialView, title, height = '32rem', labels
|
|
|
152
172
|
const canvas = useRef(null);
|
|
153
173
|
const pointer = useRef(null);
|
|
154
174
|
const [drag, setDrag] = useState(null);
|
|
175
|
+
const [proposing, setProposing] = useState(false);
|
|
176
|
+
const proposal = view.proposal;
|
|
177
|
+
const landing = proposal && drag ? dropTarget(layout, drag.id, drag.ids, drag) : null;
|
|
155
178
|
const scale = () => {
|
|
156
179
|
const r = canvas.current?.querySelector('svg.ot-root')?.getBoundingClientRect();
|
|
157
180
|
if (!r || !r.width || !r.height)
|
|
@@ -196,7 +219,7 @@ export function Explorer({ dataset, initialView, title, height = '32rem', labels
|
|
|
196
219
|
return;
|
|
197
220
|
p.moved = true;
|
|
198
221
|
const n = p.node ? projection.nodes.find((x) => x.id === p.node) : undefined;
|
|
199
|
-
if (n
|
|
222
|
+
if (n && (n.parent || proposal))
|
|
200
223
|
setDrag({ id: n.id, ids: drag?.id === n.id ? drag.ids : drawnSubtree(projection, n.id), dx, dy });
|
|
201
224
|
else
|
|
202
225
|
setBox({ ...p.box, x: p.box.x - dx, y: p.box.y - dy });
|
|
@@ -206,6 +229,12 @@ export function Explorer({ dataset, initialView, title, height = '32rem', labels
|
|
|
206
229
|
pointer.current = null;
|
|
207
230
|
if (!p)
|
|
208
231
|
return;
|
|
232
|
+
if (drag && landing) {
|
|
233
|
+
setDrag(null);
|
|
234
|
+
if (projection.nodes.find((n) => n.id === drag.id)?.parent !== landing)
|
|
235
|
+
dispatch({ type: 'proposeMove', node: drag.id, to: landing });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
209
238
|
if (drag) {
|
|
210
239
|
const parent = projection.nodes.find((n) => n.id === drag.id)?.parent;
|
|
211
240
|
const order = parent ? dropOrder(projection, layout, drag.id, drag, allChildren(parent)) : null;
|
|
@@ -262,17 +291,20 @@ export function Explorer({ dataset, initialView, title, height = '32rem', labels
|
|
|
262
291
|
return downloadBlob(new Blob([text], { type: 'image/svg+xml' }), `${view.id}.svg`);
|
|
263
292
|
downloadBlob(await svgToPng(text), `${view.id}.png`);
|
|
264
293
|
};
|
|
265
|
-
return (_jsxs("div", { className: "ot-explorer", children: [_jsxs("div", { className: "ot-toolbar", role: "toolbar", "aria-label": title, children: [_jsxs("button", { type: "button", onClick: t.undo, disabled: !t.canUndo, children: [_jsx(Icon, { name: "undo" }), labels.undo] }), _jsxs("button", { type: "button", onClick: t.redo, disabled: !t.canRedo, children: [_jsx(Icon, { name: "redo" }), labels.redo] }), _jsx("span", { className: "ot-toolbar-gap", "aria-hidden": "true" }), _jsxs("button", { type: "button", onClick: () => dispatch({ type: 'expandAll' }), children: [_jsx(Icon, { name: "expand" }), labels.expandAll] }), _jsxs("button", { type: "button", onClick: () => dispatch({ type: 'collapseAll' }), children: [_jsx(Icon, { name: "collapse" }), labels.collapseAll] }), _jsx("span", { className: "ot-toolbar-gap", "aria-hidden": "true" }), _jsxs("label", { className: "ot-select", children: [_jsx(Icon, { name: "layout" }), _jsx("span", { children: labels.layout }), _jsx("select", { value: view.layout.kind, onChange: (e) => dispatch({ type: 'setLayout', layout: e.target.value }), children: LAYOUTS.map((k) => {
|
|
294
|
+
return (_jsxs("div", { className: "ot-explorer", children: [_jsxs("div", { className: "ot-toolbar", role: "toolbar", "aria-label": title, children: [_jsxs("button", { type: "button", onClick: t.undo, disabled: !t.canUndo, children: [_jsx(Icon, { name: "undo" }), labels.undo] }), _jsxs("button", { type: "button", onClick: t.redo, disabled: !t.canRedo, children: [_jsx(Icon, { name: "redo" }), labels.redo] }), !proposal && !isUnrooted && (_jsxs("button", { type: "button", onClick: () => setProposing((v) => !v), "aria-expanded": proposing, children: [_jsx(Icon, { name: "grip" }), labels.propose] })), _jsx("span", { className: "ot-toolbar-gap", "aria-hidden": "true" }), _jsxs("button", { type: "button", onClick: () => dispatch({ type: 'expandAll' }), children: [_jsx(Icon, { name: "expand" }), labels.expandAll] }), _jsxs("button", { type: "button", onClick: () => dispatch({ type: 'collapseAll' }), children: [_jsx(Icon, { name: "collapse" }), labels.collapseAll] }), _jsx("span", { className: "ot-toolbar-gap", "aria-hidden": "true" }), _jsxs("label", { className: "ot-select", children: [_jsx(Icon, { name: "layout" }), _jsx("span", { children: labels.layout }), _jsx("select", { value: view.layout.kind, onChange: (e) => dispatch({ type: 'setLayout', layout: e.target.value }), children: LAYOUTS.map((k) => {
|
|
266
295
|
const problem = layoutProblem(dataset, view.target, k);
|
|
267
296
|
return (_jsx("option", { value: k, disabled: Boolean(problem), title: problem ?? undefined, children: k }, k));
|
|
268
297
|
}) })] }), _jsxs("label", { className: "ot-select", children: [_jsx(Icon, { name: "filter" }), _jsx("span", { children: labels.status }), _jsx("select", { value: statusValue, onChange: (e) => {
|
|
269
298
|
const f = STATUS_FILTERS.find((x) => x.value === e.target.value);
|
|
270
299
|
const { status: _, ...rest } = view.filter;
|
|
271
300
|
dispatch({ type: 'setFilter', filter: f.statuses ? { ...rest, status: f.statuses } : rest });
|
|
272
|
-
}, children: STATUS_FILTERS.map((f) => (_jsx("option", { value: f.value, children: f.value === 'all' ? labels.allStatuses : f.value }, f.value))) })] }), (hasSupport || hasExtinct || traits.length > 0) && _jsx("span", { className: "ot-toolbar-gap", "aria-hidden": "true" }), hasSupport && (_jsxs("button", { type: "button", "aria-pressed": Boolean(overlays.support), onClick: () => { const { support, ...rest } = overlays; setOverlays(support ? rest : { ...rest, support: {} }); }, children: [_jsx(Icon, { name: "
|
|
301
|
+
}, children: STATUS_FILTERS.map((f) => (_jsx("option", { value: f.value, children: f.value === 'all' ? labels.allStatuses : f.value }, f.value))) })] }), (hasSupport || hasExtinct || traits.length > 0) && _jsx("span", { className: "ot-toolbar-gap", "aria-hidden": "true" }), hasSupport && (_jsxs("button", { type: "button", "aria-pressed": Boolean(overlays.support), onClick: () => { const { support, ...rest } = overlays; setOverlays(support ? rest : { ...rest, support: {} }); }, children: [_jsx(Icon, { name: "support" }), labels.support] })), hasExtinct && (_jsxs("button", { type: "button", "aria-pressed": Boolean(overlays.extinction), onClick: () => { const { extinction, ...rest } = overlays; setOverlays(extinction ? rest : { ...rest, extinction: true }); }, children: [_jsx(Icon, { name: "fossil" }), labels.extinction] })), traits.length > 0 && (_jsxs("label", { className: "ot-select", children: [_jsx(Icon, { name: "palette" }), _jsx("span", { children: labels.trait }), _jsxs("select", { value: overlays.trait ?? '', onChange: (e) => { const { trait: _, ...rest } = overlays; setOverlays(e.target.value ? { ...rest, trait: e.target.value } : rest); }, children: [_jsx("option", { value: "", children: labels.noTrait }), traits.map((tr) => (_jsx("option", { value: tr.id, children: tr.title }, tr.id)))] })] }))] }), t.error && (_jsx("p", { className: "ot-error", role: "status", children: t.error })), _jsxs("div", { className: "ot-panes", children: [_jsxs("div", { ref: canvas, className: "ot-canvas", style: { height }, tabIndex: 0, role: "group", "aria-label": labels.canvas, "data-dragging": drag ? '' : undefined, onPointerDown: onPointerDown, onPointerMove: onPointerMove, onPointerUp: onPointerUp, onPointerCancel: () => {
|
|
273
302
|
pointer.current = null;
|
|
274
303
|
setDrag(null);
|
|
275
|
-
}, onDoubleClick: onDoubleClick, onWheel: onWheel, onKeyDown: onKeyDown, children: [_jsxs("div", { className: "ot-floating", role: "toolbar", "aria-label": labels.controls, children: [_jsx("button", { type: "button", onClick: () => zoom(0.8), "aria-label": labels.zoomIn,
|
|
304
|
+
}, onDoubleClick: onDoubleClick, onWheel: onWheel, onKeyDown: onKeyDown, children: [_jsxs("div", { className: "ot-floating", role: "toolbar", "aria-label": labels.controls, children: [_jsx("button", { type: "button", onClick: () => zoom(0.8), "aria-label": labels.zoomIn, "data-tip": labels.zoomIn, children: _jsx(Icon, { name: "zoomIn" }) }), _jsx("button", { type: "button", onClick: () => zoom(1.25), "aria-label": labels.zoomOut, "data-tip": labels.zoomOut, children: _jsx(Icon, { name: "zoomOut" }) }), _jsx("button", { type: "button", onClick: () => setBox(null), "aria-label": labels.fit, "data-tip": labels.fit, children: _jsx(Icon, { name: "fit" }) }), _jsxs("div", { className: "ot-menu", children: [_jsx("button", { type: "button", onClick: () => setMenu((m) => !m), "aria-label": labels.download, "data-tip": labels.download, "aria-expanded": menu, "aria-haspopup": "menu", children: _jsx(Icon, { name: "download" }) }), menu && (_jsxs("div", { className: "ot-menu-list", role: "menu", onKeyDown: (e) => e.key === 'Escape' && setMenu(false), children: [_jsxs("button", { type: "button", role: "menuitem", onClick: () => download('svg'), children: [_jsx(Icon, { name: "download" }), labels.downloadSvg] }), _jsxs("button", { type: "button", role: "menuitem", onClick: () => download('png'), children: [_jsx(Icon, { name: "image" }), labels.downloadPng] }), _jsxs("button", { type: "button", role: "menuitem", onClick: () => download('view'), children: [_jsx(Icon, { name: "file" }), labels.downloadView] })] }))] }), _jsx("button", { type: "button", onClick: () => printDrawing(canvas.current), "aria-label": labels.print, "data-tip": labels.print, children: _jsx(Icon, { name: "print" }) })] }), _jsx(TreeView, { projection: projection, layout: layout, highlights: view.highlights, title: title, interactive: true, hiddenLabel: hidden, viewBox: shown, drag: drag, dropTarget: landing ?? null, overlays: marks }), proposal && (_jsxs("p", { className: "ot-proposed-badge", role: "note", children: [labels.proposalBy, " ", proposal.author, ": ", proposal.title] })), _jsx("div", { className: "ot-print-disclosure", "aria-hidden": "true", children: disclosureLines(disclosure, labels.sources).map((l, i) => (_jsx("p", { children: l }, i))) })] }), _jsxs("div", { className: "ot-side", children: [proposing && !proposal && (_jsx(ProposalStart, { labels: labels, onCancel: () => setProposing(false), onStart: (title, author) => {
|
|
305
|
+
dispatch({ type: 'startProposal', proposal: { id: `p${Date.now().toString(36)}`, title, author, created: new Date().toISOString() } });
|
|
306
|
+
setProposing(false);
|
|
307
|
+
} })), proposal && _jsx(ProposalPanel, { dataset: dataset, view: view, dispatch: dispatch, labels: labels }), _jsx(TreeOutline, { projection: projection, label: labels.outline, hiddenLabel: hidden, onToggle: (id) => dispatch({ type: 'toggle', node: id }), onSelect: (id, o) => choose(id, o?.additive), onMove: move }), _jsx("section", { className: "ot-inspector", "aria-label": labels.details, children: view.selected.length > 1 ? (_jsx(CladeCheck, { dataset: dataset, view: view, projection: projection, dispatch: dispatch, labels: labels })) : node ? (_jsxs(_Fragment, { children: [_jsx("h3", { className: "ot-inspector-title", children: node.label }), proposal && _jsx(ProposalNode, { dataset: dataset, view: view, node: node.id, projection: projection, dispatch: dispatch, labels: labels }), _jsxs("dl", { children: [node.rank && (_jsxs(_Fragment, { children: [_jsx("dt", { children: labels.rank }), _jsx("dd", { children: node.rank })] })), _jsx("dt", { children: labels.lifeStatus }), _jsx("dd", { children: node.status }), details?.age && (_jsxs(_Fragment, { children: [_jsx("dt", { children: labels.age }), _jsxs("dd", { children: [details.age.value, details.age.min !== undefined && details.age.max !== undefined ? ` (${details.age.min}–${details.age.max})` : '', " ", details.age.unit !== 'unspecified' ? details.age.unit : ''] })] })), details?.support && (_jsxs(_Fragment, { children: [_jsx("dt", { children: labels.support }), _jsxs("dd", { children: [details.support.value, details.support.method ? ` (${details.support.method})` : ''] })] })), details?.branchLength && (_jsxs(_Fragment, { children: [_jsx("dt", { children: labels.branchLength }), _jsxs("dd", { children: [details.branchLength.value, " ", details.branchLength.unit !== 'unspecified' ? details.branchLength.unit : ''] })] }))] }), details && _jsx(NodeFacts, { details: details, labels: labels }), node.hiddenDescendants > 0 && _jsx("p", { children: hidden(node.hiddenDescendants) }), _jsx(HighlightList, { highlights: view.highlights.filter((h) => node.highlights.includes(h.id)), labels: labels }), _jsxs("div", { className: "ot-inspector-actions", children: [!node.isTip && (_jsxs("button", { type: "button", onClick: () => dispatch({ type: 'rotate', node: node.id }), children: [_jsx(Icon, { name: "flip" }), labels.rotate] })), _jsxs("button", { type: "button", onClick: () => dispatch({ type: 'focus', node: view.focus === node.id ? null : node.id }), children: [_jsx(Icon, { name: view.focus === node.id ? 'unfocus' : 'focus' }), view.focus === node.id ? labels.unfocus : labels.focus] })] }), (view.target.kind === 'classification' && dataset.snapshot.phylogenies.length > 0) && _jsx(CladeCheck, { dataset: dataset, view: view, projection: projection, dispatch: dispatch, labels: labels, single: true }), renderDetails?.(node.id)] })) : (_jsxs("p", { className: "ot-hint", children: [_jsx(Icon, { name: "info" }), labels.nothingSelected, " ", labels.dragHint, " ", labels.multiHint] })) })] })] }), _jsx(Legend, { layout: layout, highlights: view.highlights, labels: labels, trait: traits.find((tr) => tr.id === overlays.trait), support: Boolean(overlays.support), onRemoveHighlight: (id) => dispatch({ type: 'removeHighlight', id }) }), proposal && proposal.moves.length > 0 && _jsx(ProposalCompare, { dataset: dataset, view: view, labels: labels, title: title }), _jsxs("details", { className: "ot-about", children: [_jsxs("summary", { children: [_jsx(Icon, { name: "book" }), labels.about] }), _jsx("ul", { children: disclosure.lines.map((l, i) => (_jsx("li", { children: l }, i))) }), disclosure.citations.length > 0 && (_jsxs(_Fragment, { children: [_jsx("h4", { children: labels.sources }), _jsx("ul", { children: disclosure.citations.map((c) => (_jsx("li", { children: citationLine(c) }, c.source))) })] }))] })] }));
|
|
276
308
|
}
|
|
277
309
|
const PALETTE = ['#cf8137', '#2f7d5c', '#91645d', '#3b6fb6', '#b9a8d9', '#d9a400'];
|
|
278
310
|
/**
|
|
@@ -315,3 +347,49 @@ export function Legend({ layout, highlights, labels: custom, trait, support, onR
|
|
|
315
347
|
const labels = { clade: LABELS.clade, group: LABELS.group, noStatedValue: LABELS.noStatedValue, weakSupport: LABELS.weakSupport, mixed: LABELS.mixed, remove: LABELS.remove, ...custom };
|
|
316
348
|
return (_jsxs("div", { className: "ot-legend", children: [_jsx("p", { className: "ot-notice", children: layout.notice }), _jsx(HighlightList, { highlights: highlights, labels: labels, ...(onRemoveHighlight ? { onRemove: onRemoveHighlight } : {}) }), trait && (_jsxs("ul", { className: "ot-legend-traits", "aria-label": trait.title, children: [trait.states.map((s) => (_jsxs("li", { children: [_jsx("span", { className: "ot-swatch", "data-trait": s.id, style: s.color ? { background: s.color } : undefined, "aria-hidden": "true" }), s.label] }, s.id))), _jsxs("li", { children: [_jsx("span", { className: "ot-swatch", "data-trait": "mixed", "aria-hidden": "true" }), labels.mixed] }), _jsxs("li", { children: [_jsx("span", { className: "ot-swatch", "data-trait": "none", "aria-hidden": "true" }), labels.noStatedValue] })] })), support && _jsx("p", { className: "ot-notice", children: labels.weakSupport })] }));
|
|
317
349
|
}
|
|
350
|
+
const nodeName = (dataset, view, id, labels) => {
|
|
351
|
+
if (id === null)
|
|
352
|
+
return labels.proposalRoot;
|
|
353
|
+
if (view.target.kind === 'classification')
|
|
354
|
+
return dataset.nameOf(id) ?? id;
|
|
355
|
+
const n = dataset.phylogeny(view.target.id).node.get(id);
|
|
356
|
+
return n?.label ?? (n?.concepts?.[0] ? dataset.nameOf(n.concepts[0]) : undefined) ?? id;
|
|
357
|
+
};
|
|
358
|
+
/** Names a new proposal: a proposal always says whose it is. */
|
|
359
|
+
function ProposalStart({ labels, onStart, onCancel }) {
|
|
360
|
+
const [title, setTitle] = useState('');
|
|
361
|
+
const [author, setAuthor] = useState('');
|
|
362
|
+
return (_jsxs("form", { className: "ot-proposal", onSubmit: (e) => {
|
|
363
|
+
e.preventDefault();
|
|
364
|
+
if (author.trim())
|
|
365
|
+
onStart(title.trim() || labels.proposalDefaultTitle, author.trim());
|
|
366
|
+
}, children: [_jsx("h3", { children: labels.propose }), _jsx("p", { className: "ot-hint", children: labels.proposalHint }), _jsxs("label", { children: [labels.proposalAuthor, _jsx("input", { value: author, onChange: (e) => setAuthor(e.target.value), required: true })] }), _jsxs("label", { children: [labels.proposalTitle, _jsx("input", { value: title, onChange: (e) => setTitle(e.target.value) })] }), _jsxs("div", { className: "ot-inspector-actions", children: [_jsx("button", { type: "submit", children: labels.proposalStart }), _jsx("button", { type: "button", onClick: onCancel, children: labels.proposalCancel })] })] }));
|
|
367
|
+
}
|
|
368
|
+
/** The moves so far, each reversible (undo and redo work too). */
|
|
369
|
+
function ProposalPanel({ dataset, view, dispatch, labels }) {
|
|
370
|
+
const p = view.proposal;
|
|
371
|
+
return (_jsxs("section", { className: "ot-proposal", "aria-label": labels.propose, children: [_jsxs("h3", { children: [labels.proposalBy, " ", p.author, ": ", p.title] }), _jsx("p", { className: "ot-hint", children: labels.proposalHint }), p.moves.length ? (_jsx("ul", { children: p.moves.map((m) => (_jsxs("li", { children: [_jsxs("span", { children: [_jsx("strong", { children: nodeName(dataset, view, m.node, labels) }), " ", labels.proposalUnder, " ", nodeName(dataset, view, m.to, labels)] }), _jsx("button", { type: "button", onClick: () => dispatch({ type: 'revertMove', node: m.node }), children: labels.proposalRevert })] }, m.node))) })) : (_jsx("p", { className: "ot-hint", children: labels.proposalNoMoves })), _jsx("div", { className: "ot-inspector-actions", children: _jsx("button", { type: "button", onClick: () => dispatch({ type: 'endProposal' }), children: labels.proposalLeave }) })] }));
|
|
372
|
+
}
|
|
373
|
+
/** For the selected taxon: its accepted and proposed places, what its parent groups gain or lose, and a keyboard way to move it. */
|
|
374
|
+
function ProposalNode({ dataset, view, node, projection, dispatch, labels }) {
|
|
375
|
+
const accepted = acceptedTree(dataset, view);
|
|
376
|
+
const proposed = viewTree(dataset, view);
|
|
377
|
+
const from = accepted.parent.get(node) ?? null;
|
|
378
|
+
const to = proposed.parent.get(node) ?? null;
|
|
379
|
+
const comparison = useMemo(() => compareHierarchies(accepted, proposed), [accepted, proposed]);
|
|
380
|
+
const change = comparison.changedGroups.find((g) => g.node === node);
|
|
381
|
+
const under = new Set(drawnSubtree(projection, node));
|
|
382
|
+
const name = (id) => nodeName(dataset, view, id, labels);
|
|
383
|
+
return (_jsxs("div", { className: "ot-proposal", children: [_jsxs("p", { children: [_jsxs("strong", { children: [labels.proposalAccepted, ":"] }), " ", labels.proposalUnder, " ", name(from), _jsx("br", {}), _jsxs("strong", { children: [labels.proposalProposed, ":"] }), " ", labels.proposalUnder, " ", name(to)] }), change && change.gained.length > 0 && (_jsxs("p", { children: [_jsxs("strong", { children: [labels.proposalGained, ":"] }), " ", change.gained.map(name).join(', ')] })), change && change.lost.length > 0 && (_jsxs("p", { children: [_jsxs("strong", { children: [labels.proposalLost, ":"] }), " ", change.lost.map(name).join(', ')] })), _jsxs("label", { children: [labels.proposalMoveUnder, _jsxs("select", { value: to ?? '', onChange: (e) => dispatch({ type: 'proposeMove', node, to: e.target.value || null }), children: [view.target.kind === 'classification' && _jsx("option", { value: "", children: labels.proposalRoot }), projection.nodes
|
|
384
|
+
.filter((n) => !under.has(n.id) || n.id === to)
|
|
385
|
+
.map((n) => (_jsx("option", { value: n.id, children: n.label }, n.id)))] })] })] }));
|
|
386
|
+
}
|
|
387
|
+
/** The accepted and proposed trees side by side, same folds and filters. */
|
|
388
|
+
function ProposalCompare({ dataset, view, labels, title }) {
|
|
389
|
+
const accepted = useMemo(() => {
|
|
390
|
+
const { proposal: _p, ...rest } = view;
|
|
391
|
+
return projectView(dataset, { ...rest, order: {} });
|
|
392
|
+
}, [dataset, view]);
|
|
393
|
+
const proposed = useMemo(() => projectView(dataset, view), [dataset, view]);
|
|
394
|
+
return (_jsxs("details", { className: "ot-compare", children: [_jsx("summary", { children: labels.proposalCompare }), _jsxs("div", { className: "ot-compare-panes", children: [_jsxs("div", { children: [_jsx("h4", { children: labels.proposalAccepted }), _jsx(TreeView, { projection: accepted, layout: layoutFor(accepted, 'rectangular'), highlights: [], title: `${title}: ${labels.proposalAccepted}` })] }), _jsxs("div", { children: [_jsxs("h4", { children: [labels.proposalBy, " ", view.proposal.author] }), _jsx(TreeView, { projection: proposed, layout: layoutFor(proposed, 'rectangular'), highlights: [], title: `${title}: ${labels.proposalProposed}` })] })] })] }));
|
|
395
|
+
}
|
package/dist/Lesson.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type DatasetIndex } from '@captiveorigins/onetaxonomy-core';
|
|
2
|
+
import { type Camera, type LessonDeck } from '@captiveorigins/onetaxonomy-education';
|
|
3
|
+
import type { TreeLayout } from '@captiveorigins/onetaxonomy-layout';
|
|
4
|
+
import { type ViewBox } from './TreeView.js';
|
|
5
|
+
export type LessonMode =
|
|
6
|
+
/** Teacher presents: the drawing follows the lesson, step by step. */
|
|
7
|
+
'present'
|
|
8
|
+
/** Students explore from the current step with the full explorer (changes stay local). */
|
|
9
|
+
| 'explore'
|
|
10
|
+
/** Authoring: build scenes from the explorer, annotate nodes. */
|
|
11
|
+
| 'studio';
|
|
12
|
+
export type LessonLabels = Record<'previous' | 'next' | 'scene' | 'step' | 'present' | 'explore' | 'studio' | 'addScene' | 'sceneTitle' | 'annotate' | 'annotation' | 'remove' | 'notes' | 'selectToAnnotate' | 'modes', string>;
|
|
13
|
+
/** The viewBox for a camera: centered on its node (or the whole tree), zoom 1 = fit. */
|
|
14
|
+
export declare function cameraBox(layout: TreeLayout, camera: Camera | undefined, full?: ViewBox): ViewBox;
|
|
15
|
+
export type LessonPlayerProps = {
|
|
16
|
+
dataset: DatasetIndex;
|
|
17
|
+
deck: LessonDeck;
|
|
18
|
+
/** Starting mode (default present). Studio needs `onChange`. */
|
|
19
|
+
mode?: LessonMode;
|
|
20
|
+
/** Modes offered in the switcher (default: present and explore, plus studio when `onChange` is given). */
|
|
21
|
+
modes?: LessonMode[];
|
|
22
|
+
/** Called with the edited lesson (studio). */
|
|
23
|
+
onChange?: (deck: LessonDeck) => void;
|
|
24
|
+
/** Show teacher notes (default false). */
|
|
25
|
+
showNotes?: boolean;
|
|
26
|
+
height?: string;
|
|
27
|
+
labels?: Partial<LessonLabels>;
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Plays a lesson deck. Present mode draws exactly what the lesson says at each step
|
|
31
|
+
* (camera, reveals, annotations as numbered markers with their text beside the
|
|
32
|
+
* drawing); explore mode opens the full explorer at that point; studio mode lets an
|
|
33
|
+
* author add scenes from the explorer's current view and annotate taxa.
|
|
34
|
+
*/
|
|
35
|
+
export declare function LessonPlayer({ dataset, deck, mode: initialMode, modes, onChange, showNotes, height, labels: custom }: LessonPlayerProps): import("react").JSX.Element;
|
package/dist/Lesson.js
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
3
|
+
import { projectView } from '@captiveorigins/onetaxonomy-core';
|
|
4
|
+
import { sceneAt, sceneFromView } from '@captiveorigins/onetaxonomy-education';
|
|
5
|
+
import { useMemo, useState } from 'react';
|
|
6
|
+
import { Explorer, layoutFor } from './Explorer.js';
|
|
7
|
+
import { Icon } from './icons.js';
|
|
8
|
+
import { treeViewBox, TreeView } from './TreeView.js';
|
|
9
|
+
const LABELS = {
|
|
10
|
+
previous: 'Previous',
|
|
11
|
+
next: 'Next',
|
|
12
|
+
scene: 'Scene',
|
|
13
|
+
step: 'Step',
|
|
14
|
+
present: 'Present',
|
|
15
|
+
explore: 'Explore',
|
|
16
|
+
studio: 'Studio',
|
|
17
|
+
addScene: 'Add scene from this view',
|
|
18
|
+
sceneTitle: 'Scene title',
|
|
19
|
+
annotate: 'Annotate the selected taxon',
|
|
20
|
+
annotation: 'Note',
|
|
21
|
+
remove: 'Remove',
|
|
22
|
+
notes: 'Teacher notes',
|
|
23
|
+
selectToAnnotate: 'Choose a taxon in the drawing to annotate it.',
|
|
24
|
+
modes: 'Lesson mode',
|
|
25
|
+
};
|
|
26
|
+
/** The viewBox for a camera: centered on its node (or the whole tree), zoom 1 = fit. */
|
|
27
|
+
export function cameraBox(layout, camera, full = treeViewBox(layout)) {
|
|
28
|
+
const zoom = camera?.zoom && camera.zoom > 0 ? camera.zoom : 1;
|
|
29
|
+
const node = camera?.focus ? layout.nodes.find((n) => n.id === camera.focus) : undefined;
|
|
30
|
+
const cx = node ? node.x : full.x + full.width / 2;
|
|
31
|
+
const cy = node ? node.y : full.y + full.height / 2;
|
|
32
|
+
const width = full.width / zoom;
|
|
33
|
+
const height = full.height / zoom;
|
|
34
|
+
return { x: cx - width / 2, y: cy - height / 2, width, height };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Plays a lesson deck. Present mode draws exactly what the lesson says at each step
|
|
38
|
+
* (camera, reveals, annotations as numbered markers with their text beside the
|
|
39
|
+
* drawing); explore mode opens the full explorer at that point; studio mode lets an
|
|
40
|
+
* author add scenes from the explorer's current view and annotate taxa.
|
|
41
|
+
*/
|
|
42
|
+
export function LessonPlayer({ dataset, deck, mode: initialMode = 'present', modes, onChange, showNotes = false, height = '30rem', labels: custom }) {
|
|
43
|
+
const labels = { ...LABELS, ...custom };
|
|
44
|
+
const offered = modes ?? ['present', 'explore', ...(onChange ? ['studio'] : [])];
|
|
45
|
+
const [mode, setMode] = useState(initialMode);
|
|
46
|
+
const [sceneIndex, setSceneIndex] = useState(0);
|
|
47
|
+
const [step, setStep] = useState(0);
|
|
48
|
+
const scene = deck.scenes[Math.min(sceneIndex, deck.scenes.length - 1)];
|
|
49
|
+
const frame = useMemo(() => (scene ? sceneAt(scene, step, dataset) : null), [scene, step, dataset]);
|
|
50
|
+
const go = (delta) => {
|
|
51
|
+
if (!scene)
|
|
52
|
+
return;
|
|
53
|
+
const next = step + delta;
|
|
54
|
+
if (next >= 0 && next <= scene.steps.length)
|
|
55
|
+
return setStep(next);
|
|
56
|
+
const s = sceneIndex + delta;
|
|
57
|
+
if (s < 0 || s >= deck.scenes.length)
|
|
58
|
+
return;
|
|
59
|
+
setSceneIndex(s);
|
|
60
|
+
setStep(delta > 0 ? 0 : deck.scenes[s].steps.length);
|
|
61
|
+
};
|
|
62
|
+
return (_jsxs("div", { className: "ot-lesson", "data-mode": mode, children: [_jsxs("div", { className: "ot-toolbar", role: "toolbar", "aria-label": deck.title, children: [_jsx("div", { className: "ot-segmented", role: "radiogroup", "aria-label": labels.modes, children: offered.map((m) => (_jsx("button", { type: "button", role: "radio", "aria-checked": mode === m, onClick: () => setMode(m), children: labels[m] }, m))) }), _jsx("span", { className: "ot-toolbar-gap", "aria-hidden": "true" }), _jsxs("button", { type: "button", onClick: () => go(-1), disabled: sceneIndex === 0 && step === 0, children: [_jsx(Icon, { name: "previous" }), labels.previous] }), _jsxs("span", { className: "ot-lesson-position", "aria-live": "polite", children: [labels.scene, " ", deck.scenes.length ? sceneIndex + 1 : 0, "/", deck.scenes.length, scene && scene.steps.length > 0 ? ` · ${labels.step} ${step}/${scene.steps.length}` : ''] }), _jsxs("button", { type: "button", onClick: () => go(1), disabled: !scene || (sceneIndex === deck.scenes.length - 1 && step === scene.steps.length), children: [labels.next, _jsx(Icon, { name: "next" })] })] }), scene && frame && (_jsxs(_Fragment, { children: [_jsx("h2", { className: "ot-lesson-title", children: scene.title }), frame.caption && _jsx("p", { className: "ot-lesson-caption", children: frame.caption }), mode === 'present' && _jsx(Present, { dataset: dataset, view: frame.view, camera: frame.camera, annotations: frame.annotations, title: scene.title, height: height }), mode === 'explore' && _jsx(Explorer, { dataset: dataset, initialView: frame.view, title: scene.title, height: height }, `${scene.id}:${step}`), mode === 'studio' && onChange && _jsx(Studio, { dataset: dataset, deck: deck, sceneIndex: sceneIndex, startView: frame.view, onChange: onChange, labels: labels, height: height, onAdded: (i) => { setSceneIndex(i); setStep(0); } }), showNotes && scene.notes && (_jsxs("aside", { className: "ot-lesson-notes", children: [_jsxs("strong", { children: [labels.notes, ":"] }), " ", scene.notes] }))] }))] }));
|
|
63
|
+
}
|
|
64
|
+
function Present({ dataset, view, camera, annotations, title, height }) {
|
|
65
|
+
const projection = useMemo(() => projectView(dataset, view), [dataset, view]);
|
|
66
|
+
const layout = useMemo(() => layoutFor(projection, view.layout.kind), [projection, view.layout.kind]);
|
|
67
|
+
const box = useMemo(() => cameraBox(layout, camera), [layout, camera]);
|
|
68
|
+
const badges = new Map();
|
|
69
|
+
annotations.forEach((a, i) => {
|
|
70
|
+
const target = a.target.kind === 'node' ? a.target.node : a.target.kind === 'group' ? a.target.members[0] : undefined;
|
|
71
|
+
if (target && !badges.has(target))
|
|
72
|
+
badges.set(target, String(i + 1));
|
|
73
|
+
});
|
|
74
|
+
return (_jsxs("div", { className: "ot-panes", children: [_jsx("div", { className: "ot-canvas ot-present", style: { height }, children: _jsx(TreeView, { projection: projection, layout: layout, highlights: view.highlights, title: title, viewBox: box, badges: badges }) }), annotations.length > 0 && (_jsx("ol", { className: "ot-annotations", children: annotations.map((a) => (_jsx("li", { "data-tone": a.tone ?? 'note', children: a.text }, a.id))) }))] }));
|
|
75
|
+
}
|
|
76
|
+
function Studio({ dataset, deck, sceneIndex, startView, onChange, labels, height, onAdded }) {
|
|
77
|
+
const [view, setView] = useState(startView);
|
|
78
|
+
const [title, setTitle] = useState('');
|
|
79
|
+
const [note, setNote] = useState('');
|
|
80
|
+
const scene = deck.scenes[sceneIndex];
|
|
81
|
+
const selected = view.selected[0];
|
|
82
|
+
const uid = (p) => `${p}-${Date.now().toString(36)}`;
|
|
83
|
+
const update = (fn) => {
|
|
84
|
+
const next = JSON.parse(JSON.stringify(deck));
|
|
85
|
+
fn(next);
|
|
86
|
+
onChange(next);
|
|
87
|
+
};
|
|
88
|
+
return (_jsxs("div", { className: "ot-studio", children: [_jsx(Explorer, { dataset: dataset, initialView: startView, title: scene?.title ?? deck.title, height: height, onViewChange: setView }, `${scene?.id ?? 'new'}`), _jsxs("div", { className: "ot-studio-tools", children: [_jsxs("form", { className: "ot-inspector-actions", onSubmit: (e) => {
|
|
89
|
+
e.preventDefault();
|
|
90
|
+
update((d) => {
|
|
91
|
+
d.scenes.splice(sceneIndex + 1, 0, sceneFromView({ ...view, selected: [] }, { id: uid('scene'), title: title || `${labels.scene} ${d.scenes.length + 1}` }));
|
|
92
|
+
});
|
|
93
|
+
setTitle('');
|
|
94
|
+
onAdded(sceneIndex + 1);
|
|
95
|
+
}, children: [_jsxs("label", { className: "ot-select", children: [_jsx("span", { children: labels.sceneTitle }), _jsx("input", { value: title, onChange: (e) => setTitle(e.target.value) })] }), _jsxs("button", { type: "submit", children: [_jsx(Icon, { name: "plus" }), labels.addScene] })] }), scene && (_jsxs("form", { className: "ot-inspector-actions", onSubmit: (e) => {
|
|
96
|
+
e.preventDefault();
|
|
97
|
+
if (!selected || !note.trim())
|
|
98
|
+
return;
|
|
99
|
+
update((d) => {
|
|
100
|
+
d.scenes[sceneIndex].annotations.push({ id: uid('note'), text: note.trim(), target: { kind: 'node', node: selected } });
|
|
101
|
+
});
|
|
102
|
+
setNote('');
|
|
103
|
+
}, children: [_jsxs("label", { className: "ot-select", children: [_jsx("span", { children: labels.annotation }), _jsx("input", { value: note, onChange: (e) => setNote(e.target.value), disabled: !selected, placeholder: selected ? '' : labels.selectToAnnotate })] }), _jsxs("button", { type: "submit", disabled: !selected || !note.trim(), children: [_jsx(Icon, { name: "info" }), labels.annotate] })] })), scene && scene.annotations.length > 0 && (_jsx("ol", { className: "ot-annotations", children: scene.annotations.map((a) => (_jsxs("li", { children: [a.text, ' ', _jsx("button", { type: "button", className: "ot-remove", "aria-label": `${labels.remove}: ${a.text}`, onClick: () => update((d) => void (d.scenes[sceneIndex].annotations = d.scenes[sceneIndex].annotations.filter((x) => x.id !== a.id))), children: _jsx(Icon, { name: "close", size: 12 }) })] }, a.id))) }))] })] }));
|
|
104
|
+
}
|
package/dist/TreeView.d.ts
CHANGED
|
@@ -22,6 +22,10 @@ export type TreeViewProps = {
|
|
|
22
22
|
} | null;
|
|
23
23
|
/** Overlay marks per node (`nodeOverlays` from core): support, fossil ages, observed trait states. */
|
|
24
24
|
overlays?: ReadonlyMap<Id, NodeOverlay>;
|
|
25
|
+
/** The node a dragged taxon would land on (Proposed mode). */
|
|
26
|
+
dropTarget?: Id | null;
|
|
27
|
+
/** Small numbered markers on nodes (e.g. lesson annotations), by node id. */
|
|
28
|
+
badges?: ReadonlyMap<Id, string>;
|
|
25
29
|
/** Nodes are focusable and clickable. */
|
|
26
30
|
interactive?: boolean;
|
|
27
31
|
onNodeClick?: (id: Id, event: MouseEvent<SVGGElement>) => void;
|
package/dist/TreeView.js
CHANGED
|
@@ -44,7 +44,7 @@ export function TreeView(props) {
|
|
|
44
44
|
const fossil = mark?.fossilAge ? ` ${mark.fossilAge.value}${mark.fossilAge.unit && mark.fossilAge.unit !== 'unspecified' ? ` ${mark.fossilAge.unit}` : ''}` : '';
|
|
45
45
|
const name = `${n.label}${n.status === 'extinct' ? ` †${fossil}` : ''}`;
|
|
46
46
|
const text = n.collapsed && n.hiddenDescendants ? `${name} (${hidden(n.hiddenDescendants)})` : name;
|
|
47
|
-
return (_jsxs("g", { className: "ot-node", transform: `translate(${ln.x + (moving(ln.id) ? props.drag.dx : 0)} ${ln.y + (moving(ln.id) ? props.drag.dy : 0)})`, "data-dragging": moving(ln.id) ? '' : undefined, "data-id": ln.id, "data-leaf": ln.leaf ? '' : undefined, "data-collapsed": n.collapsed ? '' : undefined, "data-selected": n.selected ? '' : undefined, "data-status": n.status, "data-rank": n.rank, "data-trait": mark?.trait?.state, ...(interactive
|
|
47
|
+
return (_jsxs("g", { className: "ot-node", transform: `translate(${ln.x + (moving(ln.id) ? props.drag.dx : 0)} ${ln.y + (moving(ln.id) ? props.drag.dy : 0)})`, "data-dragging": moving(ln.id) ? '' : undefined, "data-id": ln.id, "data-leaf": ln.leaf ? '' : undefined, "data-collapsed": n.collapsed ? '' : undefined, "data-selected": n.selected ? '' : undefined, "data-moved": n.moved ? '' : undefined, "data-drop-target": props.dropTarget === ln.id ? '' : undefined, "data-status": n.status, "data-rank": n.rank, "data-trait": mark?.trait?.state, ...(interactive
|
|
48
48
|
? {
|
|
49
49
|
tabIndex: -1,
|
|
50
50
|
role: 'button',
|
|
@@ -52,6 +52,6 @@ export function TreeView(props) {
|
|
|
52
52
|
onClick: props.onNodeClick ? (e) => props.onNodeClick(ln.id, e) : undefined,
|
|
53
53
|
onKeyDown: props.onNodeKeyDown ? (e) => props.onNodeKeyDown(ln.id, e) : undefined,
|
|
54
54
|
}
|
|
55
|
-
: {}), children: [hl && _jsx("circle", { className: "ot-node-halo", r: 7, style: { fill: hl } }), mark?.trait && _jsx("circle", { className: "ot-trait", r: 5.5, style: 'color' in mark.trait && mark.trait.color ? { fill: mark.trait.color } : undefined }), mark?.support && (_jsx("text", { className: "ot-support", x: -5, y: -5, textAnchor: "end", "data-weak": mark.support.weak ? '' : undefined, children: mark.support.value })), n.collapsed ? _jsx("path", { className: "ot-node-collapsed", d: "M0 0 L12 -6 L12 6 Z" }) : _jsx("circle", { className: "ot-node-dot", r: ln.leaf ? 3 : 2.5 }), (ln.leaf || !radialLayout) && text && (_jsx("text", { className: "ot-label", x: ln.leaf ? (flip ? -8 : 8) : 4, y: ln.leaf ? 4 : -6, textAnchor: ln.leaf && flip ? 'end' : 'start', transform: ln.leaf ? labelTransform : undefined, "data-italic": /^(genus|species|subspecies|variety|form)$/i.test(n.rank ?? '') ? '' : undefined, children: text }))] }, ln.id));
|
|
55
|
+
: {}), children: [hl && _jsx("circle", { className: "ot-node-halo", r: 7, style: { fill: hl } }), mark?.trait && _jsx("circle", { className: "ot-trait", r: 5.5, style: 'color' in mark.trait && mark.trait.color ? { fill: mark.trait.color } : undefined }), props.badges?.has(ln.id) && (_jsxs("g", { className: "ot-badge", transform: "translate(-12 -12)", "aria-hidden": "true", children: [_jsx("circle", { r: 7 }), _jsx("text", { y: 3.5, textAnchor: "middle", children: props.badges.get(ln.id) })] })), mark?.support && (_jsx("text", { className: "ot-support", x: -5, y: -5, textAnchor: "end", "data-weak": mark.support.weak ? '' : undefined, children: mark.support.value })), n.collapsed ? _jsx("path", { className: "ot-node-collapsed", d: "M0 0 L12 -6 L12 6 Z" }) : _jsx("circle", { className: "ot-node-dot", r: ln.leaf ? 3 : 2.5 }), (ln.leaf || !radialLayout) && text && (_jsx("text", { className: "ot-label", x: ln.leaf ? (flip ? -8 : 8) : 4, y: ln.leaf ? 4 : -6, textAnchor: ln.leaf && flip ? 'end' : 'start', transform: ln.leaf ? labelTransform : undefined, "data-italic": /^(genus|species|subspecies|variety|form)$/i.test(n.rank ?? '') ? '' : undefined, children: text }))] }, ln.id));
|
|
56
56
|
}) })] }));
|
|
57
57
|
}
|
package/dist/drag.d.ts
CHANGED
|
@@ -16,3 +16,11 @@ export declare function dropOrder(projection: Projection, layout: TreeLayout, id
|
|
|
16
16
|
}, allChildren: readonly Id[]): Id[] | null;
|
|
17
17
|
/** The node and everything drawn below it (moves together while dragging). */
|
|
18
18
|
export declare function drawnSubtree(projection: Projection, id: Id): Set<Id>;
|
|
19
|
+
/**
|
|
20
|
+
* Proposed mode: the node a dragged taxon is dropped onto (its new parent), or null when
|
|
21
|
+
* it wasn't dropped on one (a drop between siblings just reorders). A node can't land on itself or on what moves with it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function dropTarget(layout: TreeLayout, id: Id, moving: ReadonlySet<Id>, delta: {
|
|
24
|
+
dx: number;
|
|
25
|
+
dy: number;
|
|
26
|
+
}, reach?: number): Id | null;
|
package/dist/drag.js
CHANGED
|
@@ -60,3 +60,26 @@ export function drawnSubtree(projection, id) {
|
|
|
60
60
|
}
|
|
61
61
|
return out;
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Proposed mode: the node a dragged taxon is dropped onto (its new parent), or null when
|
|
65
|
+
* it wasn't dropped on one (a drop between siblings just reorders). A node can't land on itself or on what moves with it.
|
|
66
|
+
*/
|
|
67
|
+
export function dropTarget(layout, id, moving, delta, reach = 9) {
|
|
68
|
+
const self = layout.nodes.find((n) => n.id === id);
|
|
69
|
+
if (!self)
|
|
70
|
+
return null;
|
|
71
|
+
const x = self.x + delta.dx;
|
|
72
|
+
const y = self.y + delta.dy;
|
|
73
|
+
let best = null;
|
|
74
|
+
let nearest = reach;
|
|
75
|
+
for (const n of layout.nodes) {
|
|
76
|
+
if (moving.has(n.id))
|
|
77
|
+
continue;
|
|
78
|
+
const d = Math.hypot(n.x - x, n.y - y);
|
|
79
|
+
if (d < nearest) {
|
|
80
|
+
nearest = d;
|
|
81
|
+
best = n.id;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return best;
|
|
85
|
+
}
|
package/dist/icons.d.ts
CHANGED
|
@@ -1,35 +1,73 @@
|
|
|
1
1
|
import type { SVGProps } from 'react';
|
|
2
2
|
/**
|
|
3
|
-
* The brand family's line icons (brand/README.md)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
9
|
*/
|
|
8
10
|
export declare const ICON_PATHS: {
|
|
9
|
-
|
|
11
|
+
/** lucide: plus */
|
|
12
|
+
readonly plus: "M5 12h14M12 5v14";
|
|
13
|
+
/** lucide: minus */
|
|
10
14
|
readonly minus: "M5 12h14";
|
|
11
|
-
|
|
12
|
-
readonly
|
|
13
|
-
|
|
14
|
-
readonly
|
|
15
|
-
|
|
16
|
-
readonly
|
|
17
|
-
|
|
18
|
-
readonly
|
|
19
|
-
|
|
20
|
-
readonly
|
|
21
|
-
|
|
22
|
-
readonly
|
|
23
|
-
|
|
24
|
-
readonly
|
|
25
|
-
|
|
26
|
-
readonly
|
|
27
|
-
|
|
28
|
-
readonly
|
|
29
|
-
|
|
30
|
-
readonly
|
|
31
|
-
|
|
32
|
-
readonly
|
|
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";
|
|
33
71
|
};
|
|
34
72
|
export type IconName = keyof typeof ICON_PATHS;
|
|
35
73
|
export declare function Icon({ name, size, className, ...rest }: {
|
package/dist/icons.js
CHANGED
|
@@ -1,35 +1,73 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
|
-
* The brand family's line icons (brand/README.md)
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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.
|
|
7
9
|
*/
|
|
8
10
|
export const ICON_PATHS = {
|
|
9
|
-
|
|
11
|
+
/** lucide: plus */
|
|
12
|
+
plus: 'M5 12h14M12 5v14',
|
|
13
|
+
/** lucide: minus */
|
|
10
14
|
minus: 'M5 12h14',
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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',
|
|
33
71
|
};
|
|
34
72
|
export function Icon({ name, size = 16, className, ...rest }) {
|
|
35
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: ["ot-icon", className].filter(Boolean).join(" "), ...rest, children: _jsx("path", { d: ICON_PATHS[name] }) }));
|
package/dist/index.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ export { TreeView, type TreeViewProps } from './TreeView.js';
|
|
|
2
2
|
export { TreeOutline, type TreeOutlineProps } from './TreeOutline.js';
|
|
3
3
|
export { Explorer, Legend, layoutFor, useTaxonomyView, type ExplorerProps, type ExplorerLabels, type UseTaxonomyView } from './Explorer.js';
|
|
4
4
|
export { treeViewBox, type ViewBox } from './TreeView.js';
|
|
5
|
-
export { dropOrder, drawnSubtree } from './drag.js';
|
|
5
|
+
export { dropOrder, dropTarget, drawnSubtree } from './drag.js';
|
|
6
6
|
export { Icon, ICON_PATHS, type IconName } from './icons.js';
|
|
7
7
|
export { citationLine, disclosureLines, downloadBlob, standaloneSvg, svgToPng } from './export.js';
|
|
8
|
+
export { cameraBox, LessonPlayer, type LessonLabels, type LessonMode, type LessonPlayerProps } from './Lesson.js';
|
package/dist/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { TreeView } from './TreeView.js';
|
|
|
2
2
|
export { TreeOutline } from './TreeOutline.js';
|
|
3
3
|
export { Explorer, Legend, layoutFor, useTaxonomyView } from './Explorer.js';
|
|
4
4
|
export { treeViewBox } from './TreeView.js';
|
|
5
|
-
export { dropOrder, drawnSubtree } from './drag.js';
|
|
5
|
+
export { dropOrder, dropTarget, drawnSubtree } from './drag.js';
|
|
6
6
|
export { Icon, ICON_PATHS } from './icons.js';
|
|
7
7
|
export { citationLine, disclosureLines, downloadBlob, standaloneSvg, svgToPng } from './export.js';
|
|
8
|
+
export { cameraBox, LessonPlayer } from './Lesson.js';
|
package/dist/styles.css
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* .ot-* classes and data-* attributes yourself, or override the custom properties.
|
|
4
4
|
*/
|
|
5
5
|
.ot-root,
|
|
6
|
-
.ot-explorer
|
|
6
|
+
.ot-explorer,
|
|
7
|
+
.ot-lesson {
|
|
7
8
|
--ot-font: system-ui, -apple-system, 'Segoe UI', sans-serif;
|
|
8
9
|
/* Brand family tokens (brand/README.md): Captive Origins brown ink, orange for selection and focus. */
|
|
9
10
|
--ot-ink: #4c312c;
|
|
@@ -22,7 +23,8 @@
|
|
|
22
23
|
}
|
|
23
24
|
@media (prefers-color-scheme: dark) {
|
|
24
25
|
.ot-root,
|
|
25
|
-
.ot-explorer
|
|
26
|
+
.ot-explorer,
|
|
27
|
+
.ot-lesson {
|
|
26
28
|
--ot-ink: #eda9ac;
|
|
27
29
|
--ot-text: #eef1f6;
|
|
28
30
|
--ot-muted: #a7b0c0;
|
|
@@ -60,14 +62,14 @@
|
|
|
60
62
|
/* Controls: 32 px, radius 8, 1 px border (shared with OnePedigree). */
|
|
61
63
|
.ot-toolbar { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-bottom: 8px; }
|
|
62
64
|
.ot-toolbar-gap { width: 6px; }
|
|
63
|
-
.ot-explorer button, .ot-select {
|
|
65
|
+
.ot-explorer button, .ot-lesson button, .ot-select {
|
|
64
66
|
display: inline-flex; align-items: center; gap: 6px; height: 32px; padding: 0 10px; border-radius: 8px;
|
|
65
67
|
border: 1px solid var(--ot-border); background: var(--ot-surface); color: inherit; font: inherit; font-size: 13px; cursor: pointer;
|
|
66
68
|
}
|
|
67
69
|
.ot-select select { font: inherit; border: 0; background: transparent; color: inherit; cursor: pointer; }
|
|
68
|
-
.ot-explorer button:hover:not(:disabled), .ot-select:hover { border-color: var(--ot-ink); }
|
|
69
|
-
.ot-explorer button:disabled { opacity: 0.45; cursor: default; }
|
|
70
|
-
.ot-explorer button:focus-visible, .ot-select:focus-within, .ot-canvas:focus-visible { outline: 2px solid var(--ot-focus); outline-offset: 2px; }
|
|
70
|
+
.ot-explorer button:hover:not(:disabled), .ot-lesson button:hover:not(:disabled), .ot-select:hover { border-color: var(--ot-ink); }
|
|
71
|
+
.ot-explorer button:disabled, .ot-lesson button:disabled { opacity: 0.45; cursor: default; }
|
|
72
|
+
.ot-explorer button:focus-visible, .ot-lesson button:focus-visible, .ot-select:focus-within, .ot-canvas:focus-visible { outline: 2px solid var(--ot-focus); outline-offset: 2px; }
|
|
71
73
|
.ot-panes { display: grid; grid-template-columns: minmax(0, 2fr) minmax(15rem, 1fr); gap: 12px; }
|
|
72
74
|
@media (max-width: 48rem) { .ot-panes { grid-template-columns: 1fr; } }
|
|
73
75
|
.ot-canvas { position: relative; overflow: hidden; touch-action: none; border: 1px solid var(--ot-border); border-radius: 12px; background: var(--ot-surface); cursor: grab; }
|
|
@@ -138,3 +140,44 @@
|
|
|
138
140
|
@media (prefers-reduced-motion: no-preference) {
|
|
139
141
|
.ot-node:not([data-dragging]) { transition: transform 150ms ease; }
|
|
140
142
|
}
|
|
143
|
+
/* Visible tooltips for icon-only buttons, on hover and on keyboard focus (title= shows on hover only). */
|
|
144
|
+
.ot-floating [data-tip] { position: relative; }
|
|
145
|
+
.ot-floating [data-tip]:is(:hover, :focus-visible)::after {
|
|
146
|
+
content: attr(data-tip); position: absolute; top: calc(100% + 6px); right: 0; z-index: 2; white-space: nowrap; pointer-events: none;
|
|
147
|
+
padding: 4px 8px; border-radius: 6px; background: var(--ot-ink); color: var(--ot-surface); font-size: 12px; line-height: 1.3;
|
|
148
|
+
}
|
|
149
|
+
.ot-floating .ot-menu:has(.ot-menu-list) [data-tip]::after { display: none; }
|
|
150
|
+
/* Lessons */
|
|
151
|
+
.ot-lesson { font-family: var(--ot-font); color: var(--ot-text); }
|
|
152
|
+
.ot-segmented { display: inline-flex; border: 1px solid var(--ot-border); border-radius: 8px; overflow: hidden; }
|
|
153
|
+
.ot-lesson .ot-segmented button { border: 0; border-radius: 0; }
|
|
154
|
+
.ot-lesson .ot-segmented button[aria-checked='true'] { background: color-mix(in srgb, var(--ot-selected) 16%, var(--ot-surface)); font-weight: 600; }
|
|
155
|
+
.ot-lesson-position { font-size: 13px; color: var(--ot-muted); min-width: 9rem; text-align: center; }
|
|
156
|
+
.ot-lesson-title { margin: 8px 0 2px; font-size: 18px; }
|
|
157
|
+
.ot-lesson-caption { margin: 0 0 8px; color: var(--ot-muted); }
|
|
158
|
+
.ot-present { cursor: default; }
|
|
159
|
+
.ot-annotations { align-self: start; margin: 0; padding: 10px 12px 10px 2rem; border: 1px solid var(--ot-border); border-radius: 10px; background: var(--ot-panel); font-size: 14px; }
|
|
160
|
+
.ot-annotations li { margin: 4px 0; }
|
|
161
|
+
.ot-annotations li[data-tone='question'] { font-style: italic; }
|
|
162
|
+
.ot-annotations li[data-tone='caution'] { color: #b45309; }
|
|
163
|
+
.ot-badge circle { fill: var(--ot-selected); }
|
|
164
|
+
.ot-badge text { fill: #fff; font-size: 9px; font-weight: 700; }
|
|
165
|
+
.ot-lesson-notes { margin-top: 8px; padding: 8px 12px; border-inline-start: 3px solid var(--ot-muted); font-size: 13px; color: var(--ot-muted); }
|
|
166
|
+
.ot-studio-tools { display: flex; flex-direction: column; gap: 8px; margin-top: 10px; }
|
|
167
|
+
|
|
168
|
+
/* Proposed mode: always visibly someone's proposal, never the accepted arrangement. */
|
|
169
|
+
.ot-node[data-moved] .ot-label { font-style: italic; text-decoration: underline dashed; text-underline-offset: 3px; }
|
|
170
|
+
.ot-node[data-moved] .ot-node-dot { stroke: var(--ot-selected); stroke-width: 2; stroke-dasharray: 2 2; }
|
|
171
|
+
.ot-node[data-drop-target] .ot-node-dot { r: 8px; fill: var(--ot-selected); }
|
|
172
|
+
.ot-node[data-drop-target] .ot-label { fill: var(--ot-selected); font-weight: 600; }
|
|
173
|
+
.ot-proposed-badge { position: absolute; bottom: 8px; inset-inline-start: 8px; z-index: 2; max-width: 70%; padding: 4px 8px; border: 1px dashed var(--ot-selected); border-radius: 6px; background: var(--ot-bg, #fff); color: var(--ot-selected); font-size: 12px; font-weight: 600; }
|
|
174
|
+
.ot-proposal { display: flex; flex-direction: column; gap: 8px; padding: 12px; border: 1px dashed var(--ot-selected); border-radius: 8px; margin-block-end: 8px; }
|
|
175
|
+
.ot-proposal h3 { margin: 0; font-size: 14px; }
|
|
176
|
+
.ot-proposal label { display: flex; flex-direction: column; gap: 2px; font-size: 13px; }
|
|
177
|
+
.ot-proposal input, .ot-proposal textarea { padding: 4px 6px; font: inherit; border: 1px solid var(--ot-border, #ccc); border-radius: 4px; background: transparent; color: inherit; }
|
|
178
|
+
.ot-proposal ul { display: flex; flex-direction: column; gap: 4px; margin: 0; padding: 0; list-style: none; font-size: 13px; }
|
|
179
|
+
.ot-proposal li { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; }
|
|
180
|
+
.ot-compare { margin-block-start: 8px; }
|
|
181
|
+
.ot-compare-panes { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: 12px; margin-block-start: 8px; }
|
|
182
|
+
.ot-compare-panes h4 { margin: 0 0 4px; font-size: 13px; }
|
|
183
|
+
.ot-compare-panes svg { width: 100%; height: auto; max-height: 28rem; }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@captiveorigins/onetaxonomy-react",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
4
|
"description": "OneTaxonomy for React: a server-renderable SVG tree, an accessible keyboard tree outline, and an explorer with undo/redo, filters, layouts and an inspector that keeps clades and groups apart.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"taxonomy",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"bugs": {
|
|
16
16
|
"url": "https://github.com/mgilardi/onetaxonomy/issues"
|
|
17
17
|
},
|
|
18
|
-
"license": "
|
|
18
|
+
"license": "PolyForm-Noncommercial-1.0.0",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
21
|
"url": "git+https://github.com/mgilardi/onetaxonomy.git",
|
|
@@ -37,14 +37,16 @@
|
|
|
37
37
|
"files": [
|
|
38
38
|
"dist",
|
|
39
39
|
"README.md",
|
|
40
|
-
"LICENSE"
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"THIRD-PARTY-LICENSE-LUCIDE.txt"
|
|
41
42
|
],
|
|
42
43
|
"engines": {
|
|
43
44
|
"node": ">=22"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
|
-
"@captiveorigins/onetaxonomy-
|
|
47
|
-
"@captiveorigins/onetaxonomy-
|
|
47
|
+
"@captiveorigins/onetaxonomy-layout": "^0.1.0-alpha.2",
|
|
48
|
+
"@captiveorigins/onetaxonomy-education": "^0.1.0-alpha.2",
|
|
49
|
+
"@captiveorigins/onetaxonomy-core": "^0.1.0-alpha.2"
|
|
48
50
|
},
|
|
49
51
|
"peerDependencies": {
|
|
50
52
|
"react": ">=18.2"
|