@captiveorigins/onetaxonomy-react 0.1.0-alpha.0 → 0.1.0-alpha.3

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 CHANGED
@@ -1,21 +1,75 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Captive Origins and OneTaxonomy contributors
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
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.
@@ -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?.parent)
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: "check" }), 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: "dagger" }), 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: () => {
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, title: labels.zoomIn, children: _jsx(Icon, { name: "plus" }) }), _jsx("button", { type: "button", onClick: () => zoom(1.25), "aria-label": labels.zoomOut, title: labels.zoomOut, children: _jsx(Icon, { name: "minus" }) }), _jsx("button", { type: "button", onClick: () => setBox(null), "aria-label": labels.fit, title: 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, title: 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, title: 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, overlays: marks }), _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: [_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 }), _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 }) }), _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))) })] }))] })] }));
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
+ }
@@ -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
+ }
@@ -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): 24 × 24, 1.75 stroke, round
4
- * caps and joins, currentColor. The same drawings ship in OnePedigree and
5
- * OneTaxonomy. Decorative by default (aria-hidden): put the words in the button's
6
- * text or aria-label.
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
- readonly plus: "M12 5v14M5 12h14";
11
+ /** lucide: plus */
12
+ readonly plus: "M5 12h14M12 5v14";
13
+ /** lucide: minus */
10
14
  readonly minus: "M5 12h14";
11
- readonly fit: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5";
12
- readonly download: "M12 4v11M7 10l5 5 5-5M5 20h14";
13
- readonly print: "M7 9V4h10v5M7 17H5a1 1 0 0 1-1-1v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5a1 1 0 0 1-1 1h-2M7 14h10v6H7z";
14
- readonly undo: "M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11";
15
- readonly redo: "m15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13";
16
- readonly expand: "m7 15 5 5 5-5M7 9l5-5 5 5";
17
- readonly collapse: "m7 20 5-5 5 5M7 4l5 5 5-5";
18
- readonly flip: "M7 4v16M4 7l3-3 3 3M17 20V4m-3 13 3 3 3-3";
19
- readonly focus: "M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM12 2v4M12 18v4M2 12h4M18 12h4";
20
- readonly unfocus: "M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5M9 12h6";
21
- readonly layout: "M4 12h4M8 6v12M8 6h5M8 18h5M8 12h9M15 4v4M15 16v4";
22
- readonly filter: "M4 5h16l-6 8v6l-4 2v-8z";
23
- readonly palette: "M12 3a9 9 0 1 0 0 18c1.1 0 1.5-.8 1.2-1.6-.4-1 .2-2.4 1.6-2.4H17a4 4 0 0 0 4-4c0-5.5-4-10-9-10zM7.5 11.5h.01M10 7.5h.01M15 7.5h.01";
24
- readonly close: "M6 6l12 12M18 6 6 18";
25
- readonly open: "M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5";
26
- readonly grip: "M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01";
27
- readonly info: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v6M12 7.5h.01";
28
- readonly check: "M5 12.5 9.5 17 19 7";
29
- readonly dagger: "M12 3v18M7 8h10";
30
- readonly image: "M4 5h16v14H4zM4 16l5-5 4 4 3-3 4 4M15.5 9h.01";
31
- readonly file: "M7 3h7l5 5v13H7zM14 3v5h5";
32
- readonly book: "M5 5.5A2.5 2.5 0 0 1 7.5 3H19v15H7.5A2.5 2.5 0 0 0 5 20.5zM5 20.5A2.5 2.5 0 0 0 7.5 23H19v-5";
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): 24 × 24, 1.75 stroke, round
4
- * caps and joins, currentColor. The same drawings ship in OnePedigree and
5
- * OneTaxonomy. Decorative by default (aria-hidden): put the words in the button's
6
- * text or aria-label.
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
- plus: 'M12 5v14M5 12h14',
11
+ /** lucide: plus */
12
+ plus: 'M5 12h14M12 5v14',
13
+ /** lucide: minus */
10
14
  minus: 'M5 12h14',
11
- fit: 'M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5',
12
- download: 'M12 4v11M7 10l5 5 5-5M5 20h14',
13
- print: 'M7 9V4h10v5M7 17H5a1 1 0 0 1-1-1v-5a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v5a1 1 0 0 1-1 1h-2M7 14h10v6H7z',
14
- undo: 'M9 14 4 9l5-5M4 9h10.5a5.5 5.5 0 0 1 0 11H11',
15
- redo: 'm15 14 5-5-5-5M20 9H9.5a5.5 5.5 0 0 0 0 11H13',
16
- expand: 'm7 15 5 5 5-5M7 9l5-5 5 5',
17
- collapse: 'm7 20 5-5 5 5M7 4l5 5 5-5',
18
- flip: 'M7 4v16M4 7l3-3 3 3M17 20V4m-3 13 3 3 3-3',
19
- focus: 'M12 9a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM12 2v4M12 18v4M2 12h4M18 12h4',
20
- unfocus: 'M4 9V4h5M20 9V4h-5M4 15v5h5M20 15v5h-5M9 12h6',
21
- layout: 'M4 12h4M8 6v12M8 6h5M8 18h5M8 12h9M15 4v4M15 16v4',
22
- filter: 'M4 5h16l-6 8v6l-4 2v-8z',
23
- palette: 'M12 3a9 9 0 1 0 0 18c1.1 0 1.5-.8 1.2-1.6-.4-1 .2-2.4 1.6-2.4H17a4 4 0 0 0 4-4c0-5.5-4-10-9-10zM7.5 11.5h.01M10 7.5h.01M15 7.5h.01',
24
- close: 'M6 6l12 12M18 6 6 18',
25
- open: 'M14 4h6v6M20 4l-9 9M18 14v5a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h5',
26
- grip: 'M9 6h.01M15 6h.01M9 12h.01M15 12h.01M9 18h.01M15 18h.01',
27
- info: 'M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v6M12 7.5h.01',
28
- check: 'M5 12.5 9.5 17 19 7',
29
- dagger: 'M12 3v18M7 8h10',
30
- image: 'M4 5h16v14H4zM4 16l5-5 4 4 3-3 4 4M15.5 9h.01',
31
- file: 'M7 3h7l5 5v13H7zM14 3v5h5',
32
- book: 'M5 5.5A2.5 2.5 0 0 1 7.5 3H19v15H7.5A2.5 2.5 0 0 0 5 20.5zM5 20.5A2.5 2.5 0 0 0 7.5 23H19v-5',
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.0",
3
+ "version": "0.1.0-alpha.3",
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",
@@ -11,14 +11,14 @@
11
11
  "accessibility",
12
12
  "education"
13
13
  ],
14
- "homepage": "https://github.com/mgilardi/onetaxonomy#readme",
14
+ "homepage": "https://github.com/CaptiveOrigins/onetaxonomy#readme",
15
15
  "bugs": {
16
- "url": "https://github.com/mgilardi/onetaxonomy/issues"
16
+ "url": "https://github.com/CaptiveOrigins/onetaxonomy/issues"
17
17
  },
18
- "license": "MIT",
18
+ "license": "PolyForm-Noncommercial-1.0.0",
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "git+https://github.com/mgilardi/onetaxonomy.git",
21
+ "url": "git+https://github.com/CaptiveOrigins/onetaxonomy.git",
22
22
  "directory": "packages/react"
23
23
  },
24
24
  "type": "module",
@@ -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-core": "^0.1.0-alpha.0",
47
- "@captiveorigins/onetaxonomy-layout": "^0.1.0-alpha.0"
47
+ "@captiveorigins/onetaxonomy-core": "^0.1.0-alpha.3",
48
+ "@captiveorigins/onetaxonomy-layout": "^0.1.0-alpha.3",
49
+ "@captiveorigins/onetaxonomy-education": "^0.1.0-alpha.3"
48
50
  },
49
51
  "peerDependencies": {
50
52
  "react": ">=18.2"