@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.
@@ -2,9 +2,12 @@
2
2
 
3
3
  import { useEffect, useState } from "react";
4
4
  import type { FormEvent } from "react";
5
- import { type NarrativeBeat, DIRECTION_COLORS, type DirectionName } from "@/lib/types";
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 created");
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("Failed"); }
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="text-2xl font-bold">Narrative Beats</h1>
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 Direction" : "Timeline"}</button>
61
- <button onClick={() => setShowForm(!showForm)} className="px-4 py-1.5 rounded-md bg-primary text-primary-foreground text-sm font-medium">+ Add Beat</button>
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: DIRECTION_COLORS[dir] + "40" }}>
71
- <span className="w-3 h-3 rounded-full inline-block mb-1" style={{ backgroundColor: DIRECTION_COLORS[dir] }} />
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">Create Beat</button>
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: DIRECTION_COLORS[beat.direction as DirectionName] }} />
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: DIRECTION_COLORS[beat.direction as DirectionName], borderLeftWidth: 3 }}>
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: DIRECTION_COLORS[dir] + "20", color: DIRECTION_COLORS[dir] }}>
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) => (
@@ -0,0 +1,7 @@
1
+ import type { Metadata } from "next";
2
+
3
+ export const metadata: Metadata = { title: "Cycles" };
4
+
5
+ export default function CyclesLayout({ children }: { children: React.ReactNode }) {
6
+ return children;
7
+ }
@@ -2,10 +2,13 @@
2
2
 
3
3
  import { useEffect, useState } from "react";
4
4
  import type { FormEvent } from "react";
5
- import { type MedicineWheelCycle, DIRECTION_COLORS, type DirectionName } from "@/lib/types";
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("Failed"); }
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="text-2xl font-bold">Medicine Wheel Cycles</h1>
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">+ New Cycle</button>
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 Cycle</button>
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={DIRECTION_COLORS[dir]}
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: DIRECTION_COLORS[cycle.current_direction as DirectionName] }}>
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: `linear-gradient(to right, ${DIRECTION_COLORS.east}, ${DIRECTION_COLORS.south}, ${DIRECTION_COLORS.west}, ${DIRECTION_COLORS.north})`,
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 ? DIRECTION_COLORS[dir] : "var(--color-border)", opacity: active ? 1 : 0.4 }}>
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
+ }
@@ -1,10 +1,13 @@
1
1
  "use client";
2
2
 
3
3
  import { useEffect, useState } from "react";
4
- import { type NarrativeBeat, type MedicineWheelCycle, DIRECTION_COLORS, type DirectionName } from "@/lib/types";
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="text-2xl font-bold mb-2">Narrative Arc</h1>
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: DIRECTION_COLORS[activeCycle.current_direction as DirectionName] }}>
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: `linear-gradient(to right, ${DIRECTION_COLORS.east}, ${DIRECTION_COLORS.south}, ${DIRECTION_COLORS.west}, ${DIRECTION_COLORS.north})` }} />
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: DIRECTION_COLORS[dir] + "40" }}>
77
- <span className="w-3 h-3 rounded-full inline-block mb-1" style={{ backgroundColor: DIRECTION_COLORS[dir] }} />
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 recorded yet.</div>}
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: DIRECTION_COLORS[beat.direction as DirectionName], borderLeftWidth: 4 }}
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">