@contractspec/example.learning-journey-ui-gamified 1.44.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.
Files changed (71) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +57 -0
  2. package/.turbo/turbo-build.log +58 -0
  3. package/CHANGELOG.md +262 -0
  4. package/LICENSE +21 -0
  5. package/README.md +32 -0
  6. package/dist/GamifiedMiniApp.d.ts +17 -0
  7. package/dist/GamifiedMiniApp.d.ts.map +1 -0
  8. package/dist/GamifiedMiniApp.js +63 -0
  9. package/dist/GamifiedMiniApp.js.map +1 -0
  10. package/dist/components/DayCalendar.d.ts +16 -0
  11. package/dist/components/DayCalendar.d.ts.map +1 -0
  12. package/dist/components/DayCalendar.js +33 -0
  13. package/dist/components/DayCalendar.js.map +1 -0
  14. package/dist/components/FlashCard.d.ts +19 -0
  15. package/dist/components/FlashCard.d.ts.map +1 -0
  16. package/dist/components/FlashCard.js +80 -0
  17. package/dist/components/FlashCard.js.map +1 -0
  18. package/dist/components/MasteryRing.d.ts +18 -0
  19. package/dist/components/MasteryRing.d.ts.map +1 -0
  20. package/dist/components/MasteryRing.js +82 -0
  21. package/dist/components/MasteryRing.js.map +1 -0
  22. package/dist/components/index.d.ts +4 -0
  23. package/dist/components/index.js +5 -0
  24. package/dist/docs/index.d.ts +1 -0
  25. package/dist/docs/index.js +1 -0
  26. package/dist/docs/learning-journey-ui-gamified.docblock.d.ts +1 -0
  27. package/dist/docs/learning-journey-ui-gamified.docblock.js +20 -0
  28. package/dist/docs/learning-journey-ui-gamified.docblock.js.map +1 -0
  29. package/dist/example.d.ts +33 -0
  30. package/dist/example.d.ts.map +1 -0
  31. package/dist/example.js +35 -0
  32. package/dist/example.js.map +1 -0
  33. package/dist/index.d.ts +12 -0
  34. package/dist/index.js +14 -0
  35. package/dist/views/Overview.d.ts +15 -0
  36. package/dist/views/Overview.d.ts.map +1 -0
  37. package/dist/views/Overview.js +161 -0
  38. package/dist/views/Overview.js.map +1 -0
  39. package/dist/views/Progress.d.ts +11 -0
  40. package/dist/views/Progress.d.ts.map +1 -0
  41. package/dist/views/Progress.js +143 -0
  42. package/dist/views/Progress.js.map +1 -0
  43. package/dist/views/Steps.d.ts +12 -0
  44. package/dist/views/Steps.d.ts.map +1 -0
  45. package/dist/views/Steps.js +56 -0
  46. package/dist/views/Steps.js.map +1 -0
  47. package/dist/views/Timeline.d.ts +11 -0
  48. package/dist/views/Timeline.d.ts.map +1 -0
  49. package/dist/views/Timeline.js +133 -0
  50. package/dist/views/Timeline.js.map +1 -0
  51. package/dist/views/index.d.ts +5 -0
  52. package/dist/views/index.js +6 -0
  53. package/example.ts +1 -0
  54. package/package.json +79 -0
  55. package/src/GamifiedMiniApp.tsx +93 -0
  56. package/src/components/DayCalendar.tsx +53 -0
  57. package/src/components/FlashCard.tsx +100 -0
  58. package/src/components/MasteryRing.tsx +81 -0
  59. package/src/components/index.ts +3 -0
  60. package/src/docs/index.ts +1 -0
  61. package/src/docs/learning-journey-ui-gamified.docblock.ts +18 -0
  62. package/src/example.ts +24 -0
  63. package/src/index.ts +10 -0
  64. package/src/views/Overview.tsx +164 -0
  65. package/src/views/Progress.tsx +183 -0
  66. package/src/views/Steps.tsx +50 -0
  67. package/src/views/Timeline.tsx +197 -0
  68. package/src/views/index.ts +4 -0
  69. package/tsconfig.json +10 -0
  70. package/tsconfig.tsbuildinfo +1 -0
  71. package/tsdown.config.js +17 -0
@@ -0,0 +1,53 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@contractspec/lib.ui-kit-web/ui/utils';
4
+
5
+ interface DayCalendarProps {
6
+ totalDays: number;
7
+ currentDay: number;
8
+ completedDays: number[];
9
+ }
10
+
11
+ export function DayCalendar({
12
+ totalDays,
13
+ currentDay,
14
+ completedDays,
15
+ }: DayCalendarProps) {
16
+ const days = Array.from({ length: totalDays }, (_, i) => i + 1);
17
+
18
+ return (
19
+ <div className="grid grid-cols-7 gap-2">
20
+ {days.map((day) => {
21
+ const isCompleted = completedDays.includes(day);
22
+ const isCurrent = day === currentDay;
23
+ const isLocked = day > currentDay;
24
+
25
+ return (
26
+ <div
27
+ key={day}
28
+ className={cn(
29
+ 'flex h-12 w-12 flex-col items-center justify-center rounded-lg border text-sm font-medium transition-all',
30
+ isCompleted && 'border-green-500 bg-green-500/10 text-green-500',
31
+ isCurrent &&
32
+ !isCompleted &&
33
+ 'border-violet-500 bg-violet-500/10 text-violet-500 ring-2 ring-violet-500/50',
34
+ isLocked && 'border-muted bg-muted/50 text-muted-foreground',
35
+ !isCompleted && !isCurrent && !isLocked && 'border-border bg-card'
36
+ )}
37
+ >
38
+ {isCompleted ? (
39
+ <span className="text-lg">✓</span>
40
+ ) : isLocked ? (
41
+ <span className="text-lg">🔒</span>
42
+ ) : (
43
+ <>
44
+ <span className="text-muted-foreground text-xs">Day</span>
45
+ <span>{day}</span>
46
+ </>
47
+ )}
48
+ </div>
49
+ );
50
+ })}
51
+ </div>
52
+ );
53
+ }
@@ -0,0 +1,100 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { Button } from '@contractspec/lib.design-system';
5
+ import { Card, CardContent } from '@contractspec/lib.ui-kit-web/ui/card';
6
+ import { cn } from '@contractspec/lib.ui-kit-web/ui/utils';
7
+ import type { LearningJourneyStepSpec } from '@contractspec/module.learning-journey/track-spec';
8
+
9
+ interface FlashCardProps {
10
+ step: LearningJourneyStepSpec;
11
+ isCompleted: boolean;
12
+ isCurrent: boolean;
13
+ onComplete?: () => void;
14
+ }
15
+
16
+ export function FlashCard({
17
+ step,
18
+ isCompleted,
19
+ isCurrent,
20
+ onComplete,
21
+ }: FlashCardProps) {
22
+ const [isFlipped, setIsFlipped] = useState(false);
23
+
24
+ return (
25
+ <Card
26
+ className={cn(
27
+ 'relative cursor-pointer overflow-hidden transition-all duration-300',
28
+ isCurrent && 'ring-primary ring-2',
29
+ isCompleted && 'opacity-60'
30
+ )}
31
+ onClick={() => !isCompleted && setIsFlipped(!isFlipped)}
32
+ >
33
+ <CardContent className="p-6">
34
+ {/* Front of card */}
35
+ <div
36
+ className={cn(
37
+ 'space-y-4 transition-opacity duration-200',
38
+ isFlipped ? 'opacity-0' : 'opacity-100'
39
+ )}
40
+ >
41
+ <div className="flex items-start justify-between">
42
+ <div className="flex-1">
43
+ <h3 className="text-lg font-semibold">{step.title}</h3>
44
+ {step.description && (
45
+ <p className="text-muted-foreground mt-1 text-sm">
46
+ {step.description}
47
+ </p>
48
+ )}
49
+ </div>
50
+ {step.xpReward && (
51
+ <span className="rounded-full bg-green-500/10 px-2 py-1 text-xs font-semibold text-green-500">
52
+ +{step.xpReward} XP
53
+ </span>
54
+ )}
55
+ </div>
56
+
57
+ {isCompleted && (
58
+ <div className="flex items-center gap-2 text-green-500">
59
+ <span>✓</span>
60
+ <span className="text-sm font-medium">Completed</span>
61
+ </div>
62
+ )}
63
+
64
+ {isCurrent && !isCompleted && (
65
+ <p className="text-muted-foreground text-xs">
66
+ Tap to reveal action
67
+ </p>
68
+ )}
69
+ </div>
70
+
71
+ {/* Back of card (action) */}
72
+ {isFlipped && !isCompleted && (
73
+ <div className="absolute inset-0 flex flex-col items-center justify-center gap-4 bg-gradient-to-br from-violet-500/10 to-violet-600/10 p-6">
74
+ <p className="text-center text-sm">
75
+ {step.instructions ?? 'Complete this step to earn XP'}
76
+ </p>
77
+ <div className="flex gap-2">
78
+ <Button
79
+ variant="outline"
80
+ size="sm"
81
+ onClick={() => setIsFlipped(false)}
82
+ >
83
+ Back
84
+ </Button>
85
+ <Button
86
+ size="sm"
87
+ onClick={(e) => {
88
+ e.stopPropagation();
89
+ onComplete?.();
90
+ }}
91
+ >
92
+ Mark Complete
93
+ </Button>
94
+ </div>
95
+ </div>
96
+ )}
97
+ </CardContent>
98
+ </Card>
99
+ );
100
+ }
@@ -0,0 +1,81 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@contractspec/lib.ui-kit-web/ui/utils';
4
+
5
+ interface MasteryRingProps {
6
+ label: string;
7
+ percentage: number;
8
+ size?: 'sm' | 'md' | 'lg';
9
+ color?: 'green' | 'blue' | 'violet' | 'orange';
10
+ }
11
+
12
+ const sizeStyles = {
13
+ sm: { container: 'h-16 w-16', text: 'text-xs', ring: 48, stroke: 4 },
14
+ md: { container: 'h-24 w-24', text: 'text-sm', ring: 72, stroke: 6 },
15
+ lg: { container: 'h-32 w-32', text: 'text-base', ring: 96, stroke: 8 },
16
+ };
17
+
18
+ const colorStyles = {
19
+ green: 'stroke-green-500',
20
+ blue: 'stroke-blue-500',
21
+ violet: 'stroke-violet-500',
22
+ orange: 'stroke-orange-500',
23
+ };
24
+
25
+ export function MasteryRing({
26
+ label,
27
+ percentage,
28
+ size = 'md',
29
+ color = 'violet',
30
+ }: MasteryRingProps) {
31
+ const styles = sizeStyles[size];
32
+ const radius = (styles.ring - styles.stroke) / 2;
33
+ const circumference = 2 * Math.PI * radius;
34
+ const strokeDashoffset = circumference - (percentage / 100) * circumference;
35
+
36
+ return (
37
+ <div
38
+ className={cn(
39
+ 'relative flex flex-col items-center gap-1',
40
+ styles.container
41
+ )}
42
+ >
43
+ <svg
44
+ className="absolute -rotate-90"
45
+ width={styles.ring}
46
+ height={styles.ring}
47
+ viewBox={`0 0 ${styles.ring} ${styles.ring}`}
48
+ >
49
+ {/* Background ring */}
50
+ <circle
51
+ cx={styles.ring / 2}
52
+ cy={styles.ring / 2}
53
+ r={radius}
54
+ fill="none"
55
+ strokeWidth={styles.stroke}
56
+ className="stroke-muted"
57
+ />
58
+ {/* Progress ring */}
59
+ <circle
60
+ cx={styles.ring / 2}
61
+ cy={styles.ring / 2}
62
+ r={radius}
63
+ fill="none"
64
+ strokeWidth={styles.stroke}
65
+ strokeLinecap="round"
66
+ strokeDasharray={circumference}
67
+ strokeDashoffset={strokeDashoffset}
68
+ className={cn('transition-all duration-500', colorStyles[color])}
69
+ />
70
+ </svg>
71
+ <div className="flex h-full flex-col items-center justify-center">
72
+ <span className={cn('font-bold', styles.text)}>
73
+ {Math.round(percentage)}%
74
+ </span>
75
+ </div>
76
+ <span className={cn('text-muted-foreground mt-1 truncate', styles.text)}>
77
+ {label}
78
+ </span>
79
+ </div>
80
+ );
81
+ }
@@ -0,0 +1,3 @@
1
+ export { FlashCard } from './FlashCard';
2
+ export { MasteryRing } from './MasteryRing';
3
+ export { DayCalendar } from './DayCalendar';
@@ -0,0 +1 @@
1
+ import './learning-journey-ui-gamified.docblock';
@@ -0,0 +1,18 @@
1
+ import type { DocBlock } from '@contractspec/lib.contracts/docs';
2
+ import { registerDocBlocks } from '@contractspec/lib.contracts/docs';
3
+
4
+ const blocks: DocBlock[] = [
5
+ {
6
+ id: 'docs.examples.learning-journey-ui-gamified',
7
+ title: 'Learning Journey UI — Gamified',
8
+ summary:
9
+ 'UI mini-app components for gamified learning: flashcards, mastery, streak/calendar.',
10
+ kind: 'reference',
11
+ visibility: 'public',
12
+ route: '/docs/examples/learning-journey-ui-gamified',
13
+ tags: ['learning', 'ui', 'gamified'],
14
+ body: `## Includes\n- Gamified mini-app shell\n- Views: overview, steps, progress, timeline\n- Components: flash card, mastery ring, day calendar\n\n## Notes\n- Compose with design system components.\n- Respect prefers-reduced-motion; keep tap targets large.`,
15
+ },
16
+ ];
17
+
18
+ registerDocBlocks(blocks);
package/src/example.ts ADDED
@@ -0,0 +1,24 @@
1
+ const example = {
2
+ id: 'learning-journey-ui-gamified',
3
+ title: 'Learning Journey UI — Gamified',
4
+ summary:
5
+ 'UI mini-app for gamified learning: flashcards, mastery ring, calendar.',
6
+ tags: ['learning', 'ui', 'gamified'],
7
+ kind: 'ui',
8
+ visibility: 'public',
9
+ docs: {
10
+ rootDocId: 'docs.examples.learning-journey-ui-gamified',
11
+ },
12
+ entrypoints: {
13
+ packageName: '@contractspec/example.learning-journey-ui-gamified',
14
+ docs: './docs',
15
+ },
16
+ surfaces: {
17
+ templates: true,
18
+ sandbox: { enabled: true, modes: ['playground', 'markdown'] },
19
+ studio: { enabled: true, installable: true },
20
+ mcp: { enabled: true },
21
+ },
22
+ } as const;
23
+
24
+ export default example;
package/src/index.ts ADDED
@@ -0,0 +1,10 @@
1
+ // Main mini-app
2
+ export { GamifiedMiniApp } from './GamifiedMiniApp';
3
+
4
+ // Views
5
+ export { Overview, Steps, Progress, Timeline } from './views';
6
+
7
+ // Components
8
+ export { FlashCard, MasteryRing, DayCalendar } from './components';
9
+ export { default as example } from './example';
10
+ import './docs';
@@ -0,0 +1,164 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@contractspec/lib.design-system';
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from '@contractspec/lib.ui-kit-web/ui/card';
10
+ import {
11
+ XpBar,
12
+ StreakCounter,
13
+ BadgeDisplay,
14
+ } from '@contractspec/example.learning-journey-ui-shared';
15
+ import type { LearningViewProps } from '@contractspec/example.learning-journey-ui-shared';
16
+
17
+ interface GamifiedOverviewProps extends LearningViewProps {
18
+ onStart?: () => void;
19
+ }
20
+
21
+ export function Overview({ track, progress, onStart }: GamifiedOverviewProps) {
22
+ const totalXp =
23
+ track.totalXp ??
24
+ track.steps.reduce((sum, s) => sum + (s.xpReward ?? 0), 0) +
25
+ (track.completionRewards?.xpBonus ?? 0);
26
+
27
+ const completedSteps = progress.completedStepIds.length;
28
+ const totalSteps = track.steps.length;
29
+ const isComplete = completedSteps === totalSteps;
30
+
31
+ return (
32
+ <div className="space-y-6">
33
+ {/* Hero Card */}
34
+ <Card className="overflow-hidden bg-gradient-to-br from-violet-500/10 via-purple-500/10 to-fuchsia-500/10">
35
+ <CardContent className="p-6">
36
+ <div className="flex flex-col items-center gap-4 text-center md:flex-row md:text-left">
37
+ <div className="flex h-20 w-20 items-center justify-center rounded-2xl bg-gradient-to-br from-violet-500 to-purple-600 text-4xl shadow-lg">
38
+ {isComplete ? '🏆' : '🎯'}
39
+ </div>
40
+ <div className="flex-1">
41
+ <h1 className="text-2xl font-bold">{track.name}</h1>
42
+ <p className="text-muted-foreground mt-1">{track.description}</p>
43
+ </div>
44
+ <div className="flex items-center gap-3">
45
+ <StreakCounter days={progress.streakDays} size="lg" />
46
+ </div>
47
+ </div>
48
+ </CardContent>
49
+ </Card>
50
+
51
+ {/* Stats Grid */}
52
+ <div className="grid gap-4 md:grid-cols-3">
53
+ <Card>
54
+ <CardHeader className="pb-2">
55
+ <CardTitle className="text-muted-foreground text-sm font-medium">
56
+ XP Progress
57
+ </CardTitle>
58
+ </CardHeader>
59
+ <CardContent>
60
+ <div className="text-3xl font-bold text-violet-500">
61
+ {progress.xpEarned.toLocaleString()}
62
+ </div>
63
+ <XpBar
64
+ current={progress.xpEarned}
65
+ max={totalXp}
66
+ showLabel={false}
67
+ size="sm"
68
+ />
69
+ </CardContent>
70
+ </Card>
71
+
72
+ <Card>
73
+ <CardHeader className="pb-2">
74
+ <CardTitle className="text-muted-foreground text-sm font-medium">
75
+ Steps Completed
76
+ </CardTitle>
77
+ </CardHeader>
78
+ <CardContent>
79
+ <div className="text-3xl font-bold">
80
+ {completedSteps}{' '}
81
+ <span className="text-muted-foreground text-lg">
82
+ / {totalSteps}
83
+ </span>
84
+ </div>
85
+ <div className="bg-muted mt-2 h-2 w-full overflow-hidden rounded-full">
86
+ <div
87
+ className="h-full bg-green-500 transition-all duration-500"
88
+ style={{ width: `${(completedSteps / totalSteps) * 100}%` }}
89
+ />
90
+ </div>
91
+ </CardContent>
92
+ </Card>
93
+
94
+ <Card>
95
+ <CardHeader className="pb-2">
96
+ <CardTitle className="text-muted-foreground text-sm font-medium">
97
+ Badges Earned
98
+ </CardTitle>
99
+ </CardHeader>
100
+ <CardContent>
101
+ <BadgeDisplay badges={progress.badges} size="lg" />
102
+ </CardContent>
103
+ </Card>
104
+ </div>
105
+
106
+ {/* Next Step Preview */}
107
+ {!isComplete && (
108
+ <Card>
109
+ <CardHeader>
110
+ <CardTitle className="flex items-center gap-2">
111
+ <span>🎯</span>
112
+ <span>Next Challenge</span>
113
+ </CardTitle>
114
+ </CardHeader>
115
+ <CardContent>
116
+ {(() => {
117
+ const nextStep = track.steps.find(
118
+ (s) => !progress.completedStepIds.includes(s.id)
119
+ );
120
+ if (!nextStep) return null;
121
+
122
+ return (
123
+ <div className="flex items-center justify-between gap-4">
124
+ <div>
125
+ <h3 className="font-semibold">{nextStep.title}</h3>
126
+ <p className="text-muted-foreground text-sm">
127
+ {nextStep.description}
128
+ </p>
129
+ </div>
130
+ <div className="flex items-center gap-3">
131
+ {nextStep.xpReward && (
132
+ <span className="rounded-full bg-green-500/10 px-3 py-1 text-sm font-semibold text-green-500">
133
+ +{nextStep.xpReward} XP
134
+ </span>
135
+ )}
136
+ <Button onClick={onStart}>Start</Button>
137
+ </div>
138
+ </div>
139
+ );
140
+ })()}
141
+ </CardContent>
142
+ </Card>
143
+ )}
144
+
145
+ {/* Completion Message */}
146
+ {isComplete && (
147
+ <Card className="border-green-500/50 bg-green-500/5">
148
+ <CardContent className="flex items-center gap-4 p-6">
149
+ <div className="text-4xl">🎉</div>
150
+ <div>
151
+ <h3 className="text-lg font-semibold text-green-500">
152
+ Track Complete!
153
+ </h3>
154
+ <p className="text-muted-foreground">
155
+ You've mastered all {totalSteps} challenges and earned{' '}
156
+ {progress.xpEarned} XP.
157
+ </p>
158
+ </div>
159
+ </CardContent>
160
+ </Card>
161
+ )}
162
+ </div>
163
+ );
164
+ }
@@ -0,0 +1,183 @@
1
+ 'use client';
2
+
3
+ import {
4
+ Card,
5
+ CardContent,
6
+ CardHeader,
7
+ CardTitle,
8
+ } from '@contractspec/lib.ui-kit-web/ui/card';
9
+ import {
10
+ XpBar,
11
+ BadgeDisplay,
12
+ } from '@contractspec/example.learning-journey-ui-shared';
13
+ import { MasteryRing } from '../components/MasteryRing';
14
+ import type { LearningViewProps } from '@contractspec/example.learning-journey-ui-shared';
15
+
16
+ export function Progress({ track, progress }: LearningViewProps) {
17
+ const totalXp =
18
+ track.totalXp ??
19
+ track.steps.reduce((sum, s) => sum + (s.xpReward ?? 0), 0) +
20
+ (track.completionRewards?.xpBonus ?? 0);
21
+
22
+ const completedSteps = progress.completedStepIds.length;
23
+ const totalSteps = track.steps.length;
24
+ const percentComplete =
25
+ totalSteps > 0 ? (completedSteps / totalSteps) * 100 : 0;
26
+
27
+ // Group steps by metadata surface for mastery rings
28
+ const surfaces = new Map<string, { total: number; completed: number }>();
29
+ track.steps.forEach((step) => {
30
+ const surface = (step.metadata?.surface as string) ?? 'general';
31
+ const current = surfaces.get(surface) ?? { total: 0, completed: 0 };
32
+ surfaces.set(surface, {
33
+ total: current.total + 1,
34
+ completed:
35
+ current.completed +
36
+ (progress.completedStepIds.includes(step.id) ? 1 : 0),
37
+ });
38
+ });
39
+
40
+ const surfaceColors: ('green' | 'blue' | 'violet' | 'orange')[] = [
41
+ 'green',
42
+ 'blue',
43
+ 'violet',
44
+ 'orange',
45
+ ];
46
+
47
+ return (
48
+ <div className="space-y-6">
49
+ {/* XP Progress */}
50
+ <Card>
51
+ <CardHeader>
52
+ <CardTitle className="flex items-center gap-2">
53
+ <span>⚡</span>
54
+ <span>Experience Points</span>
55
+ </CardTitle>
56
+ </CardHeader>
57
+ <CardContent className="space-y-4">
58
+ <div className="flex items-baseline gap-2">
59
+ <span className="text-4xl font-bold text-violet-500">
60
+ {progress.xpEarned.toLocaleString()}
61
+ </span>
62
+ <span className="text-muted-foreground">
63
+ / {totalXp.toLocaleString()} XP
64
+ </span>
65
+ </div>
66
+ <XpBar
67
+ current={progress.xpEarned}
68
+ max={totalXp}
69
+ showLabel={false}
70
+ size="lg"
71
+ />
72
+
73
+ {track.completionRewards?.xpBonus && percentComplete < 100 && (
74
+ <p className="text-muted-foreground text-sm">
75
+ 🎁 Complete all steps for a{' '}
76
+ <span className="font-semibold text-green-500">
77
+ +{track.completionRewards.xpBonus} XP
78
+ </span>{' '}
79
+ bonus!
80
+ </p>
81
+ )}
82
+ </CardContent>
83
+ </Card>
84
+
85
+ {/* Mastery Rings */}
86
+ <Card>
87
+ <CardHeader>
88
+ <CardTitle className="flex items-center gap-2">
89
+ <span>🎯</span>
90
+ <span>Skill Mastery</span>
91
+ </CardTitle>
92
+ </CardHeader>
93
+ <CardContent>
94
+ <div className="flex flex-wrap justify-center gap-6">
95
+ {Array.from(surfaces.entries()).map(([surface, data], index) => (
96
+ <MasteryRing
97
+ key={surface}
98
+ label={surface.charAt(0).toUpperCase() + surface.slice(1)}
99
+ percentage={(data.completed / data.total) * 100}
100
+ color={surfaceColors[index % surfaceColors.length]}
101
+ size="lg"
102
+ />
103
+ ))}
104
+ <MasteryRing
105
+ label="Overall"
106
+ percentage={percentComplete}
107
+ color="violet"
108
+ size="lg"
109
+ />
110
+ </div>
111
+ </CardContent>
112
+ </Card>
113
+
114
+ {/* Badges */}
115
+ <Card>
116
+ <CardHeader>
117
+ <CardTitle className="flex items-center gap-2">
118
+ <span>🏅</span>
119
+ <span>Badges Earned</span>
120
+ </CardTitle>
121
+ </CardHeader>
122
+ <CardContent>
123
+ <BadgeDisplay badges={progress.badges} size="lg" maxVisible={10} />
124
+ {progress.badges.length === 0 && (
125
+ <p className="text-muted-foreground text-sm">
126
+ Complete the track to earn your first badge!
127
+ </p>
128
+ )}
129
+ </CardContent>
130
+ </Card>
131
+
132
+ {/* Step Breakdown */}
133
+ <Card>
134
+ <CardHeader>
135
+ <CardTitle className="flex items-center gap-2">
136
+ <span>📊</span>
137
+ <span>Step Breakdown</span>
138
+ </CardTitle>
139
+ </CardHeader>
140
+ <CardContent>
141
+ <div className="space-y-2">
142
+ {track.steps.map((step) => {
143
+ const isCompleted = progress.completedStepIds.includes(step.id);
144
+ return (
145
+ <div
146
+ key={step.id}
147
+ className="flex items-center justify-between rounded-lg border p-3"
148
+ >
149
+ <div className="flex items-center gap-3">
150
+ <span
151
+ className={
152
+ isCompleted ? 'text-green-500' : 'text-muted-foreground'
153
+ }
154
+ >
155
+ {isCompleted ? '✓' : '○'}
156
+ </span>
157
+ <span
158
+ className={
159
+ isCompleted
160
+ ? 'text-foreground'
161
+ : 'text-muted-foreground'
162
+ }
163
+ >
164
+ {step.title}
165
+ </span>
166
+ </div>
167
+ {step.xpReward && (
168
+ <span
169
+ className={`text-sm font-medium ${isCompleted ? 'text-green-500' : 'text-muted-foreground'}`}
170
+ >
171
+ {isCompleted ? '+' : ''}
172
+ {step.xpReward} XP
173
+ </span>
174
+ )}
175
+ </div>
176
+ );
177
+ })}
178
+ </div>
179
+ </CardContent>
180
+ </Card>
181
+ </div>
182
+ );
183
+ }