@medicine-wheel/app 0.4.12 → 0.5.0
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/app/accountability/layout.tsx +7 -0
- package/app/accountability/page.tsx +7 -7
- package/app/api/edges/route.ts +198 -15
- package/app/api/nodes/[id]/route.ts +101 -0
- package/app/api/nodes/route.ts +39 -12
- package/app/api/plan-perspectives/[id]/route.ts +28 -0
- package/app/api/plan-perspectives/route.ts +138 -0
- package/app/ceremonies/layout.tsx +7 -0
- package/app/ceremonies/page.tsx +17 -14
- package/app/globals.css +78 -36
- package/app/graph/medicine-wheel-flow.css +157 -0
- package/app/graph/page.tsx +152 -1
- package/app/layout.tsx +32 -3
- package/app/narrative/beats/layout.tsx +7 -0
- package/app/narrative/beats/page.tsx +17 -14
- package/app/narrative/cycles/layout.tsx +7 -0
- package/app/narrative/cycles/page.tsx +13 -10
- package/app/narrative/layout.tsx +9 -0
- package/app/narrative/page.tsx +11 -8
- package/app/nodes/page.tsx +256 -82
- package/app/page.tsx +144 -61
- package/app/relations/page.tsx +338 -157
- package/components/direction-panel.tsx +21 -9
- package/components/navigation.tsx +24 -31
- package/components/workspaces-panel.tsx +10 -10
- package/package.json +21 -21
- package/public/favicon.svg +14 -0
- package/public/fonts/Stereohead.otf +0 -0
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import type { FormEvent } from "react";
|
|
5
|
-
import { type NarrativeBeat,
|
|
5
|
+
import { type NarrativeBeat, type DirectionName } from "@/lib/types";
|
|
6
6
|
import { toast } from "sonner";
|
|
7
7
|
|
|
8
|
+
const dirVar = (d: string) => `var(--mw-${d})`;
|
|
9
|
+
const dirInk = (d: string) => `var(--mw-${d}-ink)`;
|
|
10
|
+
|
|
8
11
|
export default function BeatsPage() {
|
|
9
12
|
const [beats, setBeats] = useState<NarrativeBeat[]>([]);
|
|
10
13
|
const [viewMode, setViewMode] = useState<"timeline" | "direction">("timeline");
|
|
@@ -34,11 +37,11 @@ export default function BeatsPage() {
|
|
|
34
37
|
};
|
|
35
38
|
const res = await fetch("/api/narrative/beats", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body) });
|
|
36
39
|
if (res.ok) {
|
|
37
|
-
toast.success("Beat
|
|
40
|
+
toast.success("Beat added");
|
|
38
41
|
setShowForm(false);
|
|
39
42
|
const data = await fetch("/api/narrative/beats").then((r) => r.json());
|
|
40
43
|
setBeats(Array.isArray(data) ? data : []);
|
|
41
|
-
} else { toast.error("
|
|
44
|
+
} else { toast.error("Could not save the beat — check the form and try again"); }
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
// Direction summary
|
|
@@ -52,13 +55,13 @@ export default function BeatsPage() {
|
|
|
52
55
|
<div className="p-6 max-w-6xl mx-auto">
|
|
53
56
|
<div className="flex items-center justify-between mb-6">
|
|
54
57
|
<div>
|
|
55
|
-
<h1 className="
|
|
58
|
+
<h1 className="mw-h1">Narrative beats</h1>
|
|
56
59
|
<p className="text-sm text-muted-foreground">{beats.length} beats across the Four Directions</p>
|
|
57
60
|
</div>
|
|
58
61
|
<div className="flex gap-2">
|
|
59
62
|
<button onClick={() => setViewMode(viewMode === "timeline" ? "direction" : "timeline")}
|
|
60
|
-
className="px-3 py-1.5 rounded-md border text-sm">{viewMode === "timeline" ? "By
|
|
61
|
-
<button onClick={() => setShowForm(!showForm)} className="px-4 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium"
|
|
63
|
+
className="px-3 py-1.5 rounded-md border text-sm hover:bg-secondary/60">{viewMode === "timeline" ? "By direction" : "Timeline"}</button>
|
|
64
|
+
<button onClick={() => setShowForm(!showForm)} className="px-4 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90">Add beat</button>
|
|
62
65
|
</div>
|
|
63
66
|
</div>
|
|
64
67
|
|
|
@@ -66,9 +69,9 @@ export default function BeatsPage() {
|
|
|
66
69
|
<div className="grid grid-cols-4 gap-3 mb-6">
|
|
67
70
|
{directions.map((dir) => (
|
|
68
71
|
<button key={dir} onClick={() => setFilterDir(filterDir === dir ? "all" : dir)}
|
|
69
|
-
className={`p-3 rounded-lg border text-center transition-all ${filterDir === dir ? "ring-2 ring-primary" : ""}`}
|
|
70
|
-
style={{ borderColor:
|
|
71
|
-
<span className="w-3 h-3 rounded-full inline-block mb-1" style={{ backgroundColor:
|
|
72
|
+
className={`p-3 rounded-lg border text-center transition-all hover:bg-secondary/40 ${filterDir === dir ? "ring-2 ring-primary" : ""}`}
|
|
73
|
+
style={{ borderColor: `var(--mw-${dir}-border)` }}>
|
|
74
|
+
<span className="w-3 h-3 rounded-full inline-block mb-1" style={{ backgroundColor: dirVar(dir) }} />
|
|
72
75
|
<p className="text-sm capitalize font-medium">{dir}</p>
|
|
73
76
|
<p className="text-lg font-bold">{dirCounts[dir]}</p>
|
|
74
77
|
<p className="text-xs text-muted-foreground">{dirToPhase[dir]} {visitedDirs.has(dir) ? "✓" : ""}</p>
|
|
@@ -88,19 +91,19 @@ export default function BeatsPage() {
|
|
|
88
91
|
<option value="1">Act 1 (East)</option><option value="2">Act 2 (South)</option><option value="3">Act 3 (West)</option><option value="4">Act 4 (North)</option>
|
|
89
92
|
</select>
|
|
90
93
|
<input name="learnings" placeholder="Learnings (comma-separated)" className="px-3 py-2 rounded-md border bg-background text-sm sm:col-span-2" />
|
|
91
|
-
<button type="submit" className="px-4 py-2 rounded-md bg-primary text-primary-foreground text-sm">
|
|
94
|
+
<button type="submit" className="px-4 py-2 rounded-md bg-primary text-primary-foreground text-sm hover:opacity-90">Add beat</button>
|
|
92
95
|
</form>
|
|
93
96
|
)}
|
|
94
97
|
|
|
95
98
|
{viewMode === "timeline" ? (
|
|
96
99
|
<div className="relative pl-8">
|
|
97
100
|
<div className="absolute left-3 top-0 bottom-0 w-0.5 bg-border" />
|
|
98
|
-
{filteredBeats.length === 0 && <div className="text-center py-8 text-muted-foreground">No beats yet.</div>}
|
|
101
|
+
{filteredBeats.length === 0 && <div className="text-center py-8 text-muted-foreground">No beats yet. Add the first one to begin the arc.</div>}
|
|
99
102
|
{filteredBeats.map((beat) => (
|
|
100
103
|
<div key={beat.id} className="relative mb-4" onClick={() => setExpandedBeat(expandedBeat === beat.id ? null : beat.id)}>
|
|
101
|
-
<div className="absolute -left-5 top-4 w-3 h-3 rounded-full border-2 border-background" style={{ backgroundColor:
|
|
104
|
+
<div className="absolute -left-5 top-4 w-3 h-3 rounded-full border-2 border-background" style={{ backgroundColor: dirVar(beat.direction) }} />
|
|
102
105
|
<div className="border rounded-lg bg-card p-4 cursor-pointer hover:border-ring/50"
|
|
103
|
-
style={{ borderLeftColor:
|
|
106
|
+
style={{ borderLeftColor: dirVar(beat.direction), borderLeftWidth: 3 }}>
|
|
104
107
|
<div className="flex items-center justify-between">
|
|
105
108
|
<div>
|
|
106
109
|
<h3 className="font-medium">{beat.title}</h3>
|
|
@@ -125,7 +128,7 @@ export default function BeatsPage() {
|
|
|
125
128
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
|
126
129
|
{directions.map((dir) => (
|
|
127
130
|
<div key={dir} className="space-y-2">
|
|
128
|
-
<h3 className="font-semibold capitalize text-center py-2 rounded-md" style={{ backgroundColor:
|
|
131
|
+
<h3 className="font-semibold capitalize text-center py-2 rounded-md" style={{ backgroundColor: `var(--mw-${dir}-tint)`, color: dirInk(dir) }}>
|
|
129
132
|
{dir} ({dirCounts[dir]})
|
|
130
133
|
</h3>
|
|
131
134
|
{beats.filter((b) => b.direction === dir).map((beat) => (
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
4
|
import type { FormEvent } from "react";
|
|
5
|
-
import { type MedicineWheelCycle,
|
|
5
|
+
import { type MedicineWheelCycle, type DirectionName } from "@/lib/types";
|
|
6
6
|
import { extractCycles } from "@/lib/cycle-response";
|
|
7
7
|
import { toast } from "sonner";
|
|
8
8
|
|
|
9
|
+
const dirVar = (d: string) => `var(--mw-${d})`;
|
|
10
|
+
const dirInk = (d: string) => `var(--mw-${d}-ink)`;
|
|
11
|
+
|
|
9
12
|
export default function CyclesPage() {
|
|
10
13
|
const [cycles, setCycles] = useState<MedicineWheelCycle[]>([]);
|
|
11
14
|
const [showForm, setShowForm] = useState(false);
|
|
@@ -28,7 +31,7 @@ export default function CyclesPage() {
|
|
|
28
31
|
setShowForm(false);
|
|
29
32
|
const data = await fetch("/api/narrative/cycles").then((r) => r.json());
|
|
30
33
|
setCycles(extractCycles(data));
|
|
31
|
-
} else { toast.error("
|
|
34
|
+
} else { toast.error("Could not create the cycle — try again"); }
|
|
32
35
|
}
|
|
33
36
|
|
|
34
37
|
const dirOrder: DirectionName[] = ["east", "south", "west", "north"];
|
|
@@ -37,18 +40,18 @@ export default function CyclesPage() {
|
|
|
37
40
|
<div className="p-6 max-w-5xl mx-auto">
|
|
38
41
|
<div className="flex items-center justify-between mb-6">
|
|
39
42
|
<div>
|
|
40
|
-
<h1 className="
|
|
43
|
+
<h1 className="mw-h1">Cycles</h1>
|
|
41
44
|
<p className="text-sm text-muted-foreground">{cycles.length} research cycles</p>
|
|
42
45
|
</div>
|
|
43
|
-
<button onClick={() => setShowForm(!showForm)} className="px-4 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium"
|
|
46
|
+
<button onClick={() => setShowForm(!showForm)} className="px-4 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium hover:opacity-90">New cycle</button>
|
|
44
47
|
</div>
|
|
45
48
|
|
|
46
49
|
{showForm && (
|
|
47
50
|
<form onSubmit={createCycle} className="mb-6 p-4 border rounded-lg bg-card">
|
|
48
51
|
<textarea name="question" placeholder="What is your research question?" rows={3} required className="w-full px-3 py-2 rounded-md border bg-background text-sm mb-3" />
|
|
49
52
|
<div className="flex gap-2">
|
|
50
|
-
<button type="submit" className="px-4 py-2 rounded-md bg-primary text-primary-foreground text-sm">Create
|
|
51
|
-
<button type="button" onClick={() => setShowForm(false)} className="px-4 py-2 rounded-md border text-sm">Cancel</button>
|
|
53
|
+
<button type="submit" className="px-4 py-2 rounded-md bg-primary text-primary-foreground text-sm hover:opacity-90">Create cycle</button>
|
|
54
|
+
<button type="button" onClick={() => setShowForm(false)} className="px-4 py-2 rounded-md border text-sm hover:bg-secondary/60">Cancel</button>
|
|
52
55
|
</div>
|
|
53
56
|
</form>
|
|
54
57
|
)}
|
|
@@ -78,7 +81,7 @@ export default function CyclesPage() {
|
|
|
78
81
|
return (
|
|
79
82
|
<path key={dir}
|
|
80
83
|
d={`M 24 24 L ${x1} ${y1} A 20 20 0 0 1 ${x2} ${y2} Z`}
|
|
81
|
-
fill={
|
|
84
|
+
fill={dirVar(dir)}
|
|
82
85
|
opacity={completed ? 0.9 : 0.2}
|
|
83
86
|
/>
|
|
84
87
|
);
|
|
@@ -89,7 +92,7 @@ export default function CyclesPage() {
|
|
|
89
92
|
<div className="flex-1 min-w-0">
|
|
90
93
|
<h3 className="font-semibold mb-1">{cycle.research_question}</h3>
|
|
91
94
|
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
|
92
|
-
<span className="capitalize" style={{ color:
|
|
95
|
+
<span className="capitalize" style={{ color: dirInk(cycle.current_direction) }}>
|
|
93
96
|
● {cycle.current_direction}
|
|
94
97
|
</span>
|
|
95
98
|
<span>{cycle.beats.length} beats</span>
|
|
@@ -101,7 +104,7 @@ export default function CyclesPage() {
|
|
|
101
104
|
<div className="mt-3 h-2 bg-secondary rounded-full overflow-hidden">
|
|
102
105
|
<div className="h-full rounded-full transition-all" style={{
|
|
103
106
|
width: `${progress}%`,
|
|
104
|
-
background:
|
|
107
|
+
background: "linear-gradient(to right, var(--mw-east), var(--mw-south), var(--mw-west), var(--mw-north))",
|
|
105
108
|
}} />
|
|
106
109
|
</div>
|
|
107
110
|
</div>
|
|
@@ -114,7 +117,7 @@ export default function CyclesPage() {
|
|
|
114
117
|
{dirOrder.map((dir) => {
|
|
115
118
|
const active = dirOrder.indexOf(dir) <= dirIdx;
|
|
116
119
|
return (
|
|
117
|
-
<div key={dir} className="text-center p-2 rounded-md border" style={{ borderColor: active ?
|
|
120
|
+
<div key={dir} className="text-center p-2 rounded-md border" style={{ borderColor: active ? dirVar(dir) : "var(--color-border)", opacity: active ? 1 : 0.4 }}>
|
|
118
121
|
<p className="text-xs capitalize font-medium">{dir}</p>
|
|
119
122
|
<p className="text-sm">{active ? "✓" : "○"}</p>
|
|
120
123
|
</div>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
|
|
3
|
+
export const metadata: Metadata = {
|
|
4
|
+
title: { default: "Narrative", template: "%s · Medicine Wheel" },
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function NarrativeLayout({ children }: { children: React.ReactNode }) {
|
|
8
|
+
return children;
|
|
9
|
+
}
|
package/app/narrative/page.tsx
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { useEffect, useState } from "react";
|
|
4
|
-
import { type NarrativeBeat, type MedicineWheelCycle,
|
|
4
|
+
import { type NarrativeBeat, type MedicineWheelCycle, type DirectionName } from "@/lib/types";
|
|
5
5
|
import { extractCycles } from "@/lib/cycle-response";
|
|
6
6
|
import { relativeTime, absoluteTime } from "@/lib/format-time";
|
|
7
7
|
|
|
8
|
+
const dirVar = (d: string) => `var(--mw-${d})`;
|
|
9
|
+
const dirInk = (d: string) => `var(--mw-${d}-ink)`;
|
|
10
|
+
|
|
8
11
|
export default function NarrativePage() {
|
|
9
12
|
const [beats, setBeats] = useState<NarrativeBeat[]>([]);
|
|
10
13
|
const [cycles, setCycles] = useState<MedicineWheelCycle[]>([]);
|
|
@@ -25,7 +28,7 @@ export default function NarrativePage() {
|
|
|
25
28
|
|
|
26
29
|
return (
|
|
27
30
|
<div className="p-6 max-w-5xl mx-auto">
|
|
28
|
-
<h1 className="
|
|
31
|
+
<h1 className="mw-h1 mb-2">Narrative arc</h1>
|
|
29
32
|
<p className="text-sm text-muted-foreground mb-6">Journey through the Four Directions</p>
|
|
30
33
|
|
|
31
34
|
{activeCycle && (
|
|
@@ -40,7 +43,7 @@ export default function NarrativePage() {
|
|
|
40
43
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-center">
|
|
41
44
|
<div>
|
|
42
45
|
<p className="text-sm text-muted-foreground">Current Direction</p>
|
|
43
|
-
<p className="text-lg font-bold capitalize" style={{ color:
|
|
46
|
+
<p className="text-lg font-bold capitalize" style={{ color: dirInk(activeCycle.current_direction) }}>
|
|
44
47
|
{activeCycle.current_direction}
|
|
45
48
|
</p>
|
|
46
49
|
</div>
|
|
@@ -59,7 +62,7 @@ export default function NarrativePage() {
|
|
|
59
62
|
</div>
|
|
60
63
|
<div className="mt-4">
|
|
61
64
|
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
|
62
|
-
<div className="h-full rounded-full transition-all" style={{ width: `${progress}%`, background:
|
|
65
|
+
<div className="h-full rounded-full transition-all" style={{ width: `${progress}%`, background: "linear-gradient(to right, var(--mw-east), var(--mw-south), var(--mw-west), var(--mw-north))" }} />
|
|
63
66
|
</div>
|
|
64
67
|
<div className="flex justify-between mt-1 text-xs text-muted-foreground">
|
|
65
68
|
{dirOrder.map((d) => <span key={d} className="capitalize">{d}</span>)}
|
|
@@ -73,8 +76,8 @@ export default function NarrativePage() {
|
|
|
73
76
|
{dirOrder.map((dir) => {
|
|
74
77
|
const count = beats.filter((b) => b.direction === dir).length;
|
|
75
78
|
return (
|
|
76
|
-
<div key={dir} className="p-3 rounded-lg border text-center" style={{ borderColor:
|
|
77
|
-
<span className="w-3 h-3 rounded-full inline-block mb-1" style={{ backgroundColor:
|
|
79
|
+
<div key={dir} className="p-3 rounded-lg border text-center" style={{ borderColor: `var(--mw-${dir}-border)` }}>
|
|
80
|
+
<span className="w-3 h-3 rounded-full inline-block mb-1" style={{ backgroundColor: dirVar(dir) }} />
|
|
78
81
|
<p className="text-sm capitalize font-medium">{dir}</p>
|
|
79
82
|
<p className="text-lg font-bold">{count}</p>
|
|
80
83
|
<p className="text-xs text-muted-foreground">beats</p>
|
|
@@ -87,10 +90,10 @@ export default function NarrativePage() {
|
|
|
87
90
|
<h2 className="text-lg font-semibold mb-3">Narrative Beats ({beats.length})</h2>
|
|
88
91
|
<div className="space-y-3">
|
|
89
92
|
{loading && <div className="text-center py-8 text-muted-foreground">Loading narrative beats…</div>}
|
|
90
|
-
{!loading && beats.length === 0 && <div className="text-center py-8 text-muted-foreground">No beats
|
|
93
|
+
{!loading && beats.length === 0 && <div className="text-center py-8 text-muted-foreground">No beats yet. Record the first one on the Beats page to begin the arc.</div>}
|
|
91
94
|
{!loading && beats.map((beat) => (
|
|
92
95
|
<div key={beat.id} className="border rounded-lg bg-card overflow-hidden cursor-pointer hover:border-ring/50"
|
|
93
|
-
style={{ borderLeftColor:
|
|
96
|
+
style={{ borderLeftColor: dirVar(beat.direction), borderLeftWidth: 4 }}
|
|
94
97
|
onClick={() => setExpandedBeat(expandedBeat === beat.id ? null : beat.id)}>
|
|
95
98
|
<div className="p-4">
|
|
96
99
|
<div className="flex items-center justify-between">
|