@medicine-wheel/app 0.4.13 → 0.5.1

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/page.tsx CHANGED
@@ -1,103 +1,181 @@
1
1
  "use client";
2
2
 
3
3
  import { useState } from "react";
4
- import { DIRECTIONS, DIRECTION_COLORS, type DirectionName } from "@/lib/types";
4
+ import { DIRECTIONS, type DirectionName } from "@/lib/types";
5
5
  import { DirectionPanel } from "@/components/direction-panel";
6
6
 
7
+ /** Presentation colors from tokens.css — West's canonical color is invisible on dark ground. */
8
+ const DIR_VAR: Record<DirectionName, string> = {
9
+ east: "var(--mw-east)",
10
+ south: "var(--mw-south)",
11
+ west: "var(--mw-west)",
12
+ north: "var(--mw-north)",
13
+ };
14
+ const DIR_INK: Record<DirectionName, string> = {
15
+ east: "var(--mw-east-ink)",
16
+ south: "var(--mw-south-ink)",
17
+ west: "var(--mw-west-ink)",
18
+ north: "var(--mw-north-ink)",
19
+ };
20
+ /** Text drawn ON a filled quadrant — dark on light fills, light on dark fills. */
21
+ const QUADRANT_TEXT: Record<DirectionName, { main: string; sub: string }> = {
22
+ east: { main: "#1a1408", sub: "#3d3210" },
23
+ south: { main: "#fff5f4", sub: "#ffd9d6" },
24
+ west: { main: "#f4f6fa", sub: "#dbe2ee" },
25
+ north: { main: "#1c1a14", sub: "#4a463a" },
26
+ };
27
+
28
+ const R = 200;
29
+ const CX = 250;
30
+ const CY = 250;
31
+ const toRad = (a: number) => (a * Math.PI) / 180;
32
+
33
+ function quadrantPath(startAngle: number, endAngle: number) {
34
+ const x1 = CX + R * Math.cos(toRad(startAngle));
35
+ const y1 = CY + R * Math.sin(toRad(startAngle));
36
+ const x2 = CX + R * Math.cos(toRad(endAngle));
37
+ const y2 = CY + R * Math.sin(toRad(endAngle));
38
+ return `M ${CX} ${CY} L ${x1} ${y1} A ${R} ${R} 0 0 1 ${x2} ${y2} Z`;
39
+ }
40
+
41
+ const QUADRANTS: { dir: DirectionName; start: number; end: number }[] = [
42
+ { dir: "east", start: -45, end: 45 },
43
+ { dir: "south", start: 45, end: 135 },
44
+ { dir: "west", start: 135, end: 225 },
45
+ { dir: "north", start: 225, end: 315 },
46
+ ];
47
+
7
48
  export default function HomePage() {
8
49
  const [selected, setSelected] = useState<DirectionName | null>(null);
9
50
  const [hovered, setHovered] = useState<DirectionName | null>(null);
10
51
 
11
52
  const selectedDir = selected ? DIRECTIONS.find((d) => d.name === selected) : null;
12
53
 
13
- const R = 200;
14
- const CX = 250;
15
- const CY = 250;
16
-
17
- function quadrantPath(startAngle: number, endAngle: number) {
18
- const toRad = (a: number) => (a * Math.PI) / 180;
19
- const x1 = CX + R * Math.cos(toRad(startAngle));
20
- const y1 = CY + R * Math.sin(toRad(startAngle));
21
- const x2 = CX + R * Math.cos(toRad(endAngle));
22
- const y2 = CY + R * Math.sin(toRad(endAngle));
23
- return `M ${CX} ${CY} L ${x1} ${y1} A ${R} ${R} 0 0 1 ${x2} ${y2} Z`;
24
- }
25
-
26
- const quadrants: { dir: DirectionName; start: number; end: number }[] = [
27
- { dir: "east", start: -45, end: 45 },
28
- { dir: "south", start: 45, end: 135 },
29
- { dir: "west", start: 135, end: 225 },
30
- { dir: "north", start: 225, end: 315 },
31
- ];
54
+ const toggle = (dir: DirectionName) => setSelected(selected === dir ? null : dir);
32
55
 
33
56
  return (
34
57
  <div className="flex flex-col items-center gap-8 p-6">
35
58
  <div className="text-center max-w-2xl">
36
- <h1 className="text-3xl font-bold mb-2">Medicine Wheel</h1>
59
+ <h1 className="mw-display mb-2">Medicine Wheel</h1>
37
60
  <p className="text-muted-foreground">
38
- Navigate the Four Directions — click a direction to explore its teachings, ceremonies, and relational obligations.
61
+ Navigate the Four Directions — select a direction to explore its teachings,
62
+ ceremonies, and relational obligations.
39
63
  </p>
40
64
  </div>
41
65
 
42
66
  <div className="relative">
43
- <svg viewBox="0 0 500 500" className="w-full max-w-[500px]" aria-label="Medicine Wheel">
44
- {quadrants.map(({ dir, start, end }) => {
67
+ <svg
68
+ viewBox="0 0 500 500"
69
+ className="w-full max-w-[500px] mw-breathe"
70
+ aria-label="Medicine Wheel — four directions"
71
+ role="group"
72
+ >
73
+ {/* Concentric rings + four-direction cross — the wheel made visible */}
74
+ <g aria-hidden="true">
75
+ <circle cx={CX} cy={CY} r={222} fill="none" stroke="var(--mw-border)" strokeWidth={1} opacity={0.6} />
76
+ <circle cx={CX} cy={CY} r={140} fill="none" stroke="var(--mw-fg)" strokeWidth={0.5} opacity={0.12} />
77
+ <circle cx={CX} cy={CY} r={80} fill="none" stroke="var(--mw-fg)" strokeWidth={0.5} opacity={0.12} />
78
+ {QUADRANTS.map(({ dir, start }) => {
79
+ const x = CX + 222 * Math.cos(toRad(start));
80
+ const y = CY + 222 * Math.sin(toRad(start));
81
+ return (
82
+ <line
83
+ key={`cross-${dir}`}
84
+ x1={CX} y1={CY} x2={x} y2={y}
85
+ stroke="var(--mw-border)" strokeWidth={1} opacity={0.7}
86
+ />
87
+ );
88
+ })}
89
+ </g>
90
+
91
+ {QUADRANTS.map(({ dir, start, end }) => {
45
92
  const dirData = DIRECTIONS.find((d) => d.name === dir)!;
46
93
  const isHovered = hovered === dir;
47
94
  const isSelected = selected === dir;
48
- const toRad = (a: number) => (a * Math.PI) / 180;
95
+ const active = isHovered || isSelected;
49
96
  const midAngle = (start + end) / 2;
50
- const labelR = R * 0.6;
97
+ const labelR = R * 0.62;
51
98
  const lx = CX + labelR * Math.cos(toRad(midAngle));
52
99
  const ly = CY + labelR * Math.sin(toRad(midAngle));
100
+ // Lift: the hovered quadrant drifts outward along its own direction
101
+ const dx = 6 * Math.cos(toRad(midAngle));
102
+ const dy = 6 * Math.sin(toRad(midAngle));
103
+ const text = QUADRANT_TEXT[dir];
53
104
 
54
105
  return (
55
- <g key={dir}>
106
+ <g
107
+ key={dir}
108
+ style={{
109
+ transform: active ? `translate(${dx}px, ${dy}px)` : undefined,
110
+ transition: "transform var(--duration-default) var(--ease-out)",
111
+ }}
112
+ >
56
113
  <path
57
114
  d={quadrantPath(start, end)}
58
- fill={DIRECTION_COLORS[dir]}
59
- stroke="var(--color-border)"
115
+ fill={DIR_VAR[dir]}
116
+ stroke="var(--mw-bg)"
60
117
  strokeWidth={2}
61
- opacity={isHovered || isSelected ? 1 : 0.75}
62
- className="cursor-pointer transition-all duration-300"
63
- style={{
64
- filter: isHovered ? "brightness(1.25)" : undefined,
65
- transform: isSelected ? `scale(1.02)` : undefined,
66
- transformOrigin: `${CX}px ${CY}px`,
118
+ opacity={active ? 0.95 : 0.72}
119
+ className="cursor-pointer"
120
+ style={{ transition: "opacity var(--duration-default) ease" }}
121
+ role="button"
122
+ tabIndex={0}
123
+ aria-label={`${dirData.ojibwe} — ${dir}: ${dirData.season}, ${dirData.medicine.join(", ")}`}
124
+ onClick={() => toggle(dir)}
125
+ onKeyDown={(e) => {
126
+ if (e.key === "Enter" || e.key === " ") {
127
+ e.preventDefault();
128
+ toggle(dir);
129
+ }
67
130
  }}
68
- onClick={() => setSelected(selected === dir ? null : dir)}
69
131
  onMouseEnter={() => setHovered(dir)}
70
132
  onMouseLeave={() => setHovered(null)}
133
+ onFocus={() => setHovered(dir)}
134
+ onBlur={() => setHovered(null)}
71
135
  />
72
136
  <text
73
137
  x={lx}
74
- y={ly - 8}
138
+ y={ly - 12}
75
139
  textAnchor="middle"
76
- className="pointer-events-none text-sm font-bold"
77
- style={{ fill: dir === "east" || dir === "north" ? "#1a1a2e" : "#f5f5f5" }}
140
+ className="pointer-events-none text-base font-semibold"
141
+ style={{ fill: text.main }}
78
142
  >
79
143
  {dirData.ojibwe}
80
144
  </text>
81
145
  <text
82
146
  x={lx}
83
- y={ly + 10}
147
+ y={ly + 6}
148
+ textAnchor="middle"
149
+ className="pointer-events-none text-xs capitalize"
150
+ style={{ fill: text.sub }}
151
+ >
152
+ {dir}
153
+ </text>
154
+ {/* Reveal on hover/select: season and medicines */}
155
+ <text
156
+ x={lx}
157
+ y={ly + 24}
84
158
  textAnchor="middle"
85
159
  className="pointer-events-none text-xs"
86
- style={{ fill: dir === "east" || dir === "north" ? "#333" : "#ccc" }}
160
+ style={{
161
+ fill: text.sub,
162
+ opacity: active ? 1 : 0,
163
+ transition: "opacity var(--duration-default) ease",
164
+ }}
87
165
  >
88
- {dirData.season} · {dirData.medicine[0]}
166
+ {dirData.season} · {dirData.medicine.join(" · ")}
89
167
  </text>
90
168
  </g>
91
169
  );
92
170
  })}
93
171
 
94
- {/* Center circle */}
172
+ {/* Center — balance */}
95
173
  <circle
96
174
  cx={CX}
97
175
  cy={CY}
98
- r={30}
99
- fill="var(--color-background)"
100
- stroke="var(--color-border)"
176
+ r={32}
177
+ fill="var(--mw-bg)"
178
+ stroke="var(--mw-border)"
101
179
  strokeWidth={2}
102
180
  className="animate-pulse-center"
103
181
  />
@@ -112,36 +190,41 @@ export default function HomePage() {
112
190
  </svg>
113
191
  </div>
114
192
 
115
- {/* Direction cards grid */}
193
+ {/* Direction cards */}
116
194
  <div className="grid grid-cols-2 md:grid-cols-4 gap-4 w-full max-w-4xl">
117
195
  {DIRECTIONS.map((dir) => (
118
196
  <button
119
197
  key={dir.name}
120
- onClick={() => setSelected(selected === dir.name ? null : dir.name)}
121
- className={`p-4 rounded-lg border text-left transition-all hover:scale-[1.02] ${
122
- selected === dir.name ? "ring-2 ring-primary" : ""
198
+ onClick={() => toggle(dir.name)}
199
+ className={`mw-dir-card p-4 text-left ${
200
+ selected === dir.name ? "mw-dir-card--selected" : ""
123
201
  }`}
124
- style={{ borderColor: DIRECTION_COLORS[dir.name] + "60" }}
202
+ style={
203
+ {
204
+ "--dir": DIR_VAR[dir.name],
205
+ borderColor: `var(--mw-${dir.name}-border)`,
206
+ } as React.CSSProperties
207
+ }
208
+ aria-pressed={selected === dir.name}
125
209
  >
126
- <div className="flex items-center gap-2 mb-2">
210
+ <div className="flex items-center gap-2 mb-1">
127
211
  <span
128
- className="w-3 h-3 rounded-full"
129
- style={{ backgroundColor: DIRECTION_COLORS[dir.name] }}
212
+ className="w-3 h-3 rounded-full shrink-0"
213
+ style={{ backgroundColor: DIR_VAR[dir.name] }}
130
214
  />
131
215
  <span className="font-semibold capitalize">{dir.name}</span>
132
216
  </div>
133
- <p className="text-xs text-muted-foreground">{dir.ojibwe}</p>
134
- <p className="text-xs text-muted-foreground">{dir.season}</p>
217
+ <p className="mw-ojibwe" style={{ color: DIR_INK[dir.name], opacity: 1 }}>
218
+ {dir.ojibwe}
219
+ </p>
220
+ <p className="text-xs text-muted-foreground mt-1">{dir.season}</p>
221
+ <p className="text-xs text-muted-foreground">{dir.medicine.join(" · ")}</p>
135
222
  </button>
136
223
  ))}
137
224
  </div>
138
225
 
139
- {/* Direction Panel */}
140
226
  {selectedDir && (
141
- <DirectionPanel
142
- direction={selectedDir}
143
- onClose={() => setSelected(null)}
144
- />
227
+ <DirectionPanel direction={selectedDir} onClose={() => setSelected(null)} />
145
228
  )}
146
229
  </div>
147
230
  );