@medicine-wheel/app 0.17.6 โ 0.17.7
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/components/episode-lineage-web.tsx +45 -165
- package/package.json +27 -27
|
@@ -4,18 +4,16 @@
|
|
|
4
4
|
* The chronicle as a lineage: every episode on a spine read left to right, in
|
|
5
5
|
* its direction's band, with the relations between episodes drawn as arcs.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* Tap an episode to see its relations and the words that say why each holds;
|
|
12
|
-
* tap it again, or its name, to open it. Touch first โ a phone has no hover.
|
|
13
|
-
* Ref jgwill/medicine-wheel#156.
|
|
7
|
+
* The web itself is `LineageWeb` from `@medicine-wheel/graph-viz`, the one the
|
|
8
|
+
* Miadi chronicle draws too (#162). This file adapts the wheel's edges to it
|
|
9
|
+
* and shows, under it, the selected episode's relations and the words that say
|
|
10
|
+
* why each holds. A tap selects, a second tap opens. Ref jgwill/medicine-wheel#156.
|
|
14
11
|
*/
|
|
15
12
|
|
|
16
|
-
import {
|
|
13
|
+
import { useMemo, useState } from "react";
|
|
17
14
|
import Link from "next/link";
|
|
18
15
|
import { useRouter } from "next/navigation";
|
|
16
|
+
import { LineageWeb, type LineageWebEdge, type LineageWebLayoutConfig } from "@medicine-wheel/graph-viz";
|
|
19
17
|
import { isContinuation, relationsOf, type LineageEdge } from "@/lib/episode-lineage";
|
|
20
18
|
|
|
21
19
|
export interface LineageEpisode {
|
|
@@ -26,19 +24,14 @@ export interface LineageEpisode {
|
|
|
26
24
|
date?: string;
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
const
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
const TOP = 28;
|
|
33
|
-
const AXIS = 26;
|
|
34
|
-
const STEP = 15;
|
|
35
|
-
const MARGIN_L = 72;
|
|
36
|
-
const MARGIN_R = 24;
|
|
37
|
-
const H = TOP + BAND_H * BANDS.length + AXIS;
|
|
27
|
+
const BAND_LABELS = { east: "๐
east", south: "๐ฅ south", west: "๐ west", north: "โ๏ธ north" };
|
|
28
|
+
// The wheel's page is narrower than the chronicle's: closer bands, less room for arcs.
|
|
29
|
+
const LAYOUT: Partial<LineageWebLayoutConfig> = { headroom: 90, bandGap: 84, footroom: 84, minStep: 15, minWidth: 360 };
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
31
|
+
/** `E044` from a `chronicle:<date>-episode-044-โฆ` id; the dot's chip shows the name under it. */
|
|
32
|
+
function markOf(id: string): string {
|
|
33
|
+
const n = /episode-(\d+)/i.exec(id)?.[1];
|
|
34
|
+
return n ? `E${n.padStart(3, "0")}` : "ยท";
|
|
42
35
|
}
|
|
43
36
|
|
|
44
37
|
export function EpisodeLineageWeb({
|
|
@@ -50,58 +43,27 @@ export function EpisodeLineageWeb({
|
|
|
50
43
|
episodes: LineageEpisode[];
|
|
51
44
|
/** Episodeโepisode relations only. */
|
|
52
45
|
edges: LineageEdge[];
|
|
53
|
-
/** When a filter is typed: the episodes it matches.
|
|
46
|
+
/** When a filter is typed: the episodes it matches. Their relatives stay, as rings. */
|
|
54
47
|
matches?: Set<string> | null;
|
|
55
48
|
}) {
|
|
56
49
|
const router = useRouter();
|
|
57
|
-
const scroller = useRef<HTMLDivElement>(null);
|
|
58
50
|
const [selected, setSelected] = useState<string | null>(null);
|
|
59
|
-
const [hovered, setHovered] = useState<string | null>(null);
|
|
60
|
-
|
|
61
|
-
const layout = useMemo(() => {
|
|
62
|
-
const position = new Map<string, { x: number; y: number }>();
|
|
63
|
-
episodes.forEach((episode, i) => {
|
|
64
|
-
const x = MARGIN_L + i * STEP;
|
|
65
|
-
// A small stagger so neighbours in one band do not sit on one line.
|
|
66
|
-
const y = TOP + (bandOf(episode.direction) + 0.5) * BAND_H + ((i % 3) - 1) * 13;
|
|
67
|
-
position.set(episode.id, { x, y });
|
|
68
|
-
});
|
|
69
|
-
const width = Math.max(MARGIN_L + (episodes.length - 1) * STEP + MARGIN_R, 360);
|
|
70
|
-
|
|
71
|
-
const months: Array<{ x: number; label: string }> = [];
|
|
72
|
-
let last = "";
|
|
73
|
-
episodes.forEach((episode, i) => {
|
|
74
|
-
const month = episode.date?.slice(0, 7) ?? "";
|
|
75
|
-
if (month && month !== last) {
|
|
76
|
-
months.push({ x: MARGIN_L + i * STEP, label: month });
|
|
77
|
-
last = month;
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
const degree = new Map<string, number>();
|
|
82
|
-
for (const edge of edges) {
|
|
83
|
-
degree.set(edge.from_id, (degree.get(edge.from_id) ?? 0) + 1);
|
|
84
|
-
degree.set(edge.to_id, (degree.get(edge.to_id) ?? 0) + 1);
|
|
85
|
-
}
|
|
86
|
-
return { position, width, months, degree };
|
|
87
|
-
}, [episodes, edges]);
|
|
88
51
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}, [focus, edges]);
|
|
52
|
+
const webEpisodes = useMemo(
|
|
53
|
+
() => episodes.map((e) => ({ id: e.id, label: markOf(e.id), title: e.name, direction: e.direction, date: e.date })),
|
|
54
|
+
[episodes],
|
|
55
|
+
);
|
|
56
|
+
const webEdges = useMemo<LineageWebEdge[]>(
|
|
57
|
+
() =>
|
|
58
|
+
edges.map((edge) => ({
|
|
59
|
+
id: edge.id,
|
|
60
|
+
from: edge.from_id,
|
|
61
|
+
to: edge.to_id,
|
|
62
|
+
kind: isContinuation(edge) ? "continuation" : "relation",
|
|
63
|
+
words: edge.description,
|
|
64
|
+
})),
|
|
65
|
+
[edges],
|
|
66
|
+
);
|
|
105
67
|
|
|
106
68
|
const byId = useMemo(() => new Map(episodes.map((e) => [e.id, e])), [episodes]);
|
|
107
69
|
const chosen = selected ? byId.get(selected) : undefined;
|
|
@@ -110,103 +72,20 @@ export function EpisodeLineageWeb({
|
|
|
110
72
|
[selected, edges],
|
|
111
73
|
);
|
|
112
74
|
|
|
113
|
-
function open(id: string) {
|
|
114
|
-
router.push(`/episodes/${encodeURIComponent(id)}`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function tap(id: string) {
|
|
118
|
-
if (selected === id) open(id);
|
|
119
|
-
else setSelected(id);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const faded = (id: string) =>
|
|
123
|
-
(near !== null && !near.has(id)) || (matches != null && !matches.has(id));
|
|
124
|
-
|
|
125
75
|
return (
|
|
126
76
|
<div className="space-y-3">
|
|
127
|
-
<div
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<g key={band}>
|
|
140
|
-
<line
|
|
141
|
-
x1={0}
|
|
142
|
-
x2={layout.width}
|
|
143
|
-
y1={TOP + (i + 1) * BAND_H}
|
|
144
|
-
y2={TOP + (i + 1) * BAND_H}
|
|
145
|
-
stroke="var(--mw-border)"
|
|
146
|
-
strokeWidth={1}
|
|
147
|
-
opacity={0.5}
|
|
148
|
-
/>
|
|
149
|
-
<text x={8} y={TOP + i * BAND_H + 16} fontSize={11} fill="var(--mw-muted)">
|
|
150
|
-
{BAND_ICON[band]} {band}
|
|
151
|
-
</text>
|
|
152
|
-
</g>
|
|
153
|
-
))}
|
|
154
|
-
|
|
155
|
-
{layout.months.map((m) => (
|
|
156
|
-
<text key={m.label} x={m.x} y={H - 8} fontSize={10} fill="var(--mw-muted)">
|
|
157
|
-
{m.label}
|
|
158
|
-
</text>
|
|
159
|
-
))}
|
|
160
|
-
|
|
161
|
-
{edges.map((edge) => {
|
|
162
|
-
const a = layout.position.get(edge.from_id);
|
|
163
|
-
const b = layout.position.get(edge.to_id);
|
|
164
|
-
if (!a || !b) return null;
|
|
165
|
-
const lit = focus !== null && (edge.from_id === focus || edge.to_id === focus);
|
|
166
|
-
const lift = Math.min(110, Math.abs(b.x - a.x) * 0.3) + 18;
|
|
167
|
-
const top = Math.max(6, Math.min(a.y, b.y) - lift);
|
|
168
|
-
const continuation = isContinuation(edge);
|
|
169
|
-
return (
|
|
170
|
-
<path
|
|
171
|
-
key={edge.id}
|
|
172
|
-
d={`M ${a.x} ${a.y} Q ${(a.x + b.x) / 2} ${top} ${b.x} ${b.y}`}
|
|
173
|
-
fill="none"
|
|
174
|
-
stroke={lit ? "var(--mw-primary)" : "var(--mw-muted)"}
|
|
175
|
-
strokeWidth={lit ? 2 : 1}
|
|
176
|
-
strokeDasharray={continuation ? undefined : "3 3"}
|
|
177
|
-
opacity={lit ? 1 : focus ? 0.12 : continuation ? 0.55 : 0.3}
|
|
178
|
-
/>
|
|
179
|
-
);
|
|
180
|
-
})}
|
|
181
|
-
|
|
182
|
-
{episodes.map((episode) => {
|
|
183
|
-
const p = layout.position.get(episode.id)!;
|
|
184
|
-
const related = (layout.degree.get(episode.id) ?? 0) > 0;
|
|
185
|
-
const isSelected = episode.id === selected;
|
|
186
|
-
return (
|
|
187
|
-
<g
|
|
188
|
-
key={episode.id}
|
|
189
|
-
onClick={() => tap(episode.id)}
|
|
190
|
-
onMouseEnter={() => setHovered(episode.id)}
|
|
191
|
-
onMouseLeave={() => setHovered(null)}
|
|
192
|
-
style={{ cursor: "pointer" }}
|
|
193
|
-
opacity={faded(episode.id) ? 0.2 : 1}
|
|
194
|
-
>
|
|
195
|
-
<title>{episode.name}</title>
|
|
196
|
-
{/* A target a finger can hit, larger than the dot it holds. */}
|
|
197
|
-
<circle cx={p.x} cy={p.y} r={9} fill="transparent" />
|
|
198
|
-
<circle
|
|
199
|
-
cx={p.x}
|
|
200
|
-
cy={p.y}
|
|
201
|
-
r={related ? 4.5 : 3}
|
|
202
|
-
fill={`var(--mw-${episode.direction ?? "west"}-ink, var(--mw-muted))`}
|
|
203
|
-
stroke={isSelected ? "var(--mw-primary)" : "none"}
|
|
204
|
-
strokeWidth={isSelected ? 2.5 : 0}
|
|
205
|
-
/>
|
|
206
|
-
</g>
|
|
207
|
-
);
|
|
208
|
-
})}
|
|
209
|
-
</svg>
|
|
77
|
+
<div className="border rounded-lg bg-card p-2">
|
|
78
|
+
<LineageWeb
|
|
79
|
+
episodes={webEpisodes}
|
|
80
|
+
edges={webEdges}
|
|
81
|
+
selected={selected}
|
|
82
|
+
onSelect={setSelected}
|
|
83
|
+
onOpen={(id) => router.push(`/episodes/${encodeURIComponent(id)}`)}
|
|
84
|
+
matches={matches}
|
|
85
|
+
layout={LAYOUT}
|
|
86
|
+
bandLabels={BAND_LABELS}
|
|
87
|
+
startAt="end"
|
|
88
|
+
/>
|
|
210
89
|
</div>
|
|
211
90
|
|
|
212
91
|
{chosen ? (
|
|
@@ -237,9 +116,10 @@ export function EpisodeLineageWeb({
|
|
|
237
116
|
</div>
|
|
238
117
|
) : (
|
|
239
118
|
<p className="text-xs text-muted-foreground">
|
|
240
|
-
Each dot is an episode, oldest on the left, in its direction's band. Solid arcs carry
|
|
241
|
-
a thread forward (continues from); dashed arcs relate two episodes. Tap a dot to see why
|
|
242
|
-
they are related; tap it again to open it.
|
|
119
|
+
Each dot is an episode, oldest on the left, in its direction's band. Solid arcs above carry
|
|
120
|
+
a thread forward (continues from); dashed arcs below relate two episodes. Tap a dot to see why
|
|
121
|
+
they are related; tap it again to open it. A filter keeps what it found and, as rings, the
|
|
122
|
+
episodes related to it.
|
|
243
123
|
</p>
|
|
244
124
|
)}
|
|
245
125
|
</div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@medicine-wheel/app",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.7",
|
|
4
4
|
"description": "Medicine Wheel โ Interactive visual layer for Indigenous relational research with Four Directions, ceremonies, and narrative arcs",
|
|
5
5
|
"bin": {
|
|
6
6
|
"mw": "dist/cli/mw.js",
|
|
@@ -91,32 +91,32 @@
|
|
|
91
91
|
"release:major": "npm run version:major && npm run publish:all && npm run release:commit"
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
|
-
"@medicine-wheel/ceremonial-diary": "^0.17.
|
|
95
|
-
"@medicine-wheel/ceremony-protocol": "^0.17.
|
|
96
|
-
"@medicine-wheel/community-identity": "^0.17.
|
|
97
|
-
"@medicine-wheel/community-review": "^0.17.
|
|
98
|
-
"@medicine-wheel/consent-lifecycle": "^0.17.
|
|
99
|
-
"@medicine-wheel/creative-orientation": "^0.17.
|
|
100
|
-
"@medicine-wheel/data-store": "^0.17.
|
|
101
|
-
"@medicine-wheel/data-store-postgres": "^0.17.
|
|
102
|
-
"@medicine-wheel/fire-keeper": "^0.17.
|
|
103
|
-
"@medicine-wheel/github-ceremony": "^0.17.
|
|
104
|
-
"@medicine-wheel/graph-viz": "^0.17.
|
|
105
|
-
"@medicine-wheel/honcho": "^0.17.
|
|
106
|
-
"@medicine-wheel/importance-unit": "^0.17.
|
|
107
|
-
"@medicine-wheel/mcp": "^4.17.
|
|
108
|
-
"@medicine-wheel/memory": "^0.17.
|
|
109
|
-
"@medicine-wheel/narrative-cluster": "^0.17.
|
|
110
|
-
"@medicine-wheel/narrative-engine": "^0.17.
|
|
111
|
-
"@medicine-wheel/ontology-core": "^0.17.
|
|
112
|
-
"@medicine-wheel/perception-layer": "^0.17.
|
|
113
|
-
"@medicine-wheel/prompt-decomposition": "^0.17.
|
|
114
|
-
"@medicine-wheel/relational-index": "^0.17.
|
|
115
|
-
"@medicine-wheel/relational-query": "^0.17.
|
|
116
|
-
"@medicine-wheel/session-reader": "^0.17.
|
|
117
|
-
"@medicine-wheel/storage-provider": "^0.17.
|
|
118
|
-
"@medicine-wheel/transformation-tracker": "^0.17.
|
|
119
|
-
"@medicine-wheel/ui-components": "^0.17.
|
|
94
|
+
"@medicine-wheel/ceremonial-diary": "^0.17.7",
|
|
95
|
+
"@medicine-wheel/ceremony-protocol": "^0.17.7",
|
|
96
|
+
"@medicine-wheel/community-identity": "^0.17.7",
|
|
97
|
+
"@medicine-wheel/community-review": "^0.17.7",
|
|
98
|
+
"@medicine-wheel/consent-lifecycle": "^0.17.7",
|
|
99
|
+
"@medicine-wheel/creative-orientation": "^0.17.7",
|
|
100
|
+
"@medicine-wheel/data-store": "^0.17.7",
|
|
101
|
+
"@medicine-wheel/data-store-postgres": "^0.17.7",
|
|
102
|
+
"@medicine-wheel/fire-keeper": "^0.17.7",
|
|
103
|
+
"@medicine-wheel/github-ceremony": "^0.17.7",
|
|
104
|
+
"@medicine-wheel/graph-viz": "^0.17.7",
|
|
105
|
+
"@medicine-wheel/honcho": "^0.17.7",
|
|
106
|
+
"@medicine-wheel/importance-unit": "^0.17.7",
|
|
107
|
+
"@medicine-wheel/mcp": "^4.17.7",
|
|
108
|
+
"@medicine-wheel/memory": "^0.17.7",
|
|
109
|
+
"@medicine-wheel/narrative-cluster": "^0.17.7",
|
|
110
|
+
"@medicine-wheel/narrative-engine": "^0.17.7",
|
|
111
|
+
"@medicine-wheel/ontology-core": "^0.17.7",
|
|
112
|
+
"@medicine-wheel/perception-layer": "^0.17.7",
|
|
113
|
+
"@medicine-wheel/prompt-decomposition": "^0.17.7",
|
|
114
|
+
"@medicine-wheel/relational-index": "^0.17.7",
|
|
115
|
+
"@medicine-wheel/relational-query": "^0.17.7",
|
|
116
|
+
"@medicine-wheel/session-reader": "^0.17.7",
|
|
117
|
+
"@medicine-wheel/storage-provider": "^0.17.7",
|
|
118
|
+
"@medicine-wheel/transformation-tracker": "^0.17.7",
|
|
119
|
+
"@medicine-wheel/ui-components": "^0.17.7",
|
|
120
120
|
"@miadi/episode-vessel": "^0.2.1",
|
|
121
121
|
"@neondatabase/serverless": "^0.10.0",
|
|
122
122
|
"@xyflow/react": "^12.3.0",
|