@contractspec/example.learning-journey-ui-shared 0.0.0-canary-20260113170453

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 (60) hide show
  1. package/.turbo/turbo-build$colon$bundle.log +48 -0
  2. package/.turbo/turbo-build.log +49 -0
  3. package/CHANGELOG.md +379 -0
  4. package/LICENSE +21 -0
  5. package/README.md +42 -0
  6. package/dist/components/BadgeDisplay.d.ts +12 -0
  7. package/dist/components/BadgeDisplay.d.ts.map +1 -0
  8. package/dist/components/BadgeDisplay.js +45 -0
  9. package/dist/components/BadgeDisplay.js.map +1 -0
  10. package/dist/components/StreakCounter.d.ts +12 -0
  11. package/dist/components/StreakCounter.d.ts.map +1 -0
  12. package/dist/components/StreakCounter.js +46 -0
  13. package/dist/components/StreakCounter.js.map +1 -0
  14. package/dist/components/ViewTabs.d.ts +12 -0
  15. package/dist/components/ViewTabs.d.ts.map +1 -0
  16. package/dist/components/ViewTabs.js +49 -0
  17. package/dist/components/ViewTabs.js.map +1 -0
  18. package/dist/components/XpBar.d.ts +14 -0
  19. package/dist/components/XpBar.d.ts.map +1 -0
  20. package/dist/components/XpBar.js +47 -0
  21. package/dist/components/XpBar.js.map +1 -0
  22. package/dist/components/index.d.ts +5 -0
  23. package/dist/components/index.js +6 -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-shared.docblock.d.ts +1 -0
  27. package/dist/docs/learning-journey-ui-shared.docblock.js +20 -0
  28. package/dist/docs/learning-journey-ui-shared.docblock.js.map +1 -0
  29. package/dist/example.d.ts +7 -0
  30. package/dist/example.d.ts.map +1 -0
  31. package/dist/example.js +42 -0
  32. package/dist/example.js.map +1 -0
  33. package/dist/hooks/index.d.ts +2 -0
  34. package/dist/hooks/index.js +3 -0
  35. package/dist/hooks/useLearningProgress.d.ts +22 -0
  36. package/dist/hooks/useLearningProgress.d.ts.map +1 -0
  37. package/dist/hooks/useLearningProgress.js +74 -0
  38. package/dist/hooks/useLearningProgress.js.map +1 -0
  39. package/dist/index.d.ts +9 -0
  40. package/dist/index.js +11 -0
  41. package/dist/types.d.ts +58 -0
  42. package/dist/types.d.ts.map +1 -0
  43. package/dist/types.js +0 -0
  44. package/example.ts +1 -0
  45. package/package.json +72 -0
  46. package/src/components/BadgeDisplay.tsx +65 -0
  47. package/src/components/StreakCounter.tsx +49 -0
  48. package/src/components/ViewTabs.tsx +46 -0
  49. package/src/components/XpBar.tsx +54 -0
  50. package/src/components/index.ts +4 -0
  51. package/src/docs/index.ts +1 -0
  52. package/src/docs/learning-journey-ui-shared.docblock.ts +17 -0
  53. package/src/example.ts +31 -0
  54. package/src/hooks/index.ts +1 -0
  55. package/src/hooks/useLearningProgress.ts +101 -0
  56. package/src/index.ts +20 -0
  57. package/src/types.ts +61 -0
  58. package/tsconfig.json +9 -0
  59. package/tsconfig.tsbuildinfo +1 -0
  60. package/tsdown.config.js +17 -0
@@ -0,0 +1,49 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@contractspec/lib.ui-kit-web/ui/utils';
4
+ import type { StreakCounterProps } from '../types';
5
+
6
+ const sizeStyles = {
7
+ sm: {
8
+ container: 'gap-1 px-2 py-1',
9
+ icon: 'text-base',
10
+ text: 'text-xs',
11
+ },
12
+ md: {
13
+ container: 'gap-1.5 px-3 py-1.5',
14
+ icon: 'text-lg',
15
+ text: 'text-sm',
16
+ },
17
+ lg: {
18
+ container: 'gap-2 px-4 py-2',
19
+ icon: 'text-xl',
20
+ text: 'text-base',
21
+ },
22
+ };
23
+
24
+ export function StreakCounter({
25
+ days,
26
+ isActive = true,
27
+ size = 'md',
28
+ }: StreakCounterProps) {
29
+ const styles = sizeStyles[size];
30
+
31
+ return (
32
+ <div
33
+ className={cn(
34
+ 'inline-flex items-center rounded-full font-semibold',
35
+ styles.container,
36
+ isActive
37
+ ? 'bg-orange-500/10 text-orange-500'
38
+ : 'bg-muted text-muted-foreground'
39
+ )}
40
+ >
41
+ <span className={styles.icon} role="img" aria-label="streak">
42
+ 🔥
43
+ </span>
44
+ <span className={styles.text}>
45
+ {days} {days === 1 ? 'day' : 'days'}
46
+ </span>
47
+ </div>
48
+ );
49
+ }
@@ -0,0 +1,46 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@contractspec/lib.design-system';
4
+ import type { ViewTabsProps, LearningView } from '../types';
5
+
6
+ const VIEW_LABELS: Record<LearningView, { label: string; icon: string }> = {
7
+ overview: { label: 'Overview', icon: '📊' },
8
+ steps: { label: 'Steps', icon: '📝' },
9
+ progress: { label: 'Progress', icon: '📈' },
10
+ timeline: { label: 'Timeline', icon: '📅' },
11
+ };
12
+
13
+ const DEFAULT_VIEWS: LearningView[] = [
14
+ 'overview',
15
+ 'steps',
16
+ 'progress',
17
+ 'timeline',
18
+ ];
19
+
20
+ export function ViewTabs({
21
+ currentView,
22
+ onViewChange,
23
+ availableViews = DEFAULT_VIEWS,
24
+ }: ViewTabsProps) {
25
+ return (
26
+ <div className="flex flex-wrap gap-2">
27
+ {availableViews.map((view) => {
28
+ const { label, icon } = VIEW_LABELS[view];
29
+ const isActive = currentView === view;
30
+
31
+ return (
32
+ <Button
33
+ key={view}
34
+ variant={isActive ? 'default' : 'outline'}
35
+ size="sm"
36
+ onClick={() => onViewChange(view)}
37
+ className="gap-1.5"
38
+ >
39
+ <span>{icon}</span>
40
+ <span>{label}</span>
41
+ </Button>
42
+ );
43
+ })}
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { Progress } from '@contractspec/lib.ui-kit-web/ui/progress';
4
+ import { cn } from '@contractspec/lib.ui-kit-web/ui/utils';
5
+ import type { XpBarProps } from '../types';
6
+
7
+ const sizeStyles = {
8
+ sm: 'h-2',
9
+ md: 'h-3',
10
+ lg: 'h-4',
11
+ };
12
+
13
+ const labelSizeStyles = {
14
+ sm: 'text-xs',
15
+ md: 'text-sm',
16
+ lg: 'text-base',
17
+ };
18
+
19
+ export function XpBar({
20
+ current,
21
+ max,
22
+ level,
23
+ showLabel = true,
24
+ size = 'md',
25
+ }: XpBarProps) {
26
+ const percentage = max > 0 ? Math.min((current / max) * 100, 100) : 0;
27
+
28
+ return (
29
+ <div className="w-full space-y-1">
30
+ {showLabel && (
31
+ <div
32
+ className={cn(
33
+ 'flex items-center justify-between',
34
+ labelSizeStyles[size]
35
+ )}
36
+ >
37
+ <span className="text-muted-foreground font-medium">
38
+ {level !== undefined && (
39
+ <span className="text-primary mr-1">Lvl {level}</span>
40
+ )}
41
+ XP
42
+ </span>
43
+ <span className="font-semibold">
44
+ {current.toLocaleString()} / {max.toLocaleString()}
45
+ </span>
46
+ </div>
47
+ )}
48
+ <Progress
49
+ value={percentage}
50
+ className={cn('bg-muted', sizeStyles[size])}
51
+ />
52
+ </div>
53
+ );
54
+ }
@@ -0,0 +1,4 @@
1
+ export { XpBar } from './XpBar';
2
+ export { StreakCounter } from './StreakCounter';
3
+ export { BadgeDisplay } from './BadgeDisplay';
4
+ export { ViewTabs } from './ViewTabs';
@@ -0,0 +1 @@
1
+ import './learning-journey-ui-shared.docblock';
@@ -0,0 +1,17 @@
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-shared',
7
+ title: 'Learning Journey UI — Shared',
8
+ summary: 'Shared UI components and hooks for learning journey mini-apps.',
9
+ kind: 'reference',
10
+ visibility: 'public',
11
+ route: '/docs/examples/learning-journey-ui-shared',
12
+ tags: ['learning', 'ui', 'shared'],
13
+ body: `## Includes\n- Hooks: useLearningProgress\n- Components: XpBar, StreakCounter, BadgeDisplay, ViewTabs\n\n## Notes\n- Keep components accessible (labels, focus, contrast).\n- Prefer design-system tokens and components.`,
14
+ },
15
+ ];
16
+
17
+ registerDocBlocks(blocks);
package/src/example.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { defineExample } from '@contractspec/lib.contracts';
2
+
3
+ const example = defineExample({
4
+ meta: {
5
+ key: 'learning-journey-ui-shared',
6
+ version: '1.0.0',
7
+ title: 'Learning Journey UI — Shared',
8
+ description:
9
+ 'Shared UI components and hooks for learning journey mini-apps.',
10
+ kind: 'ui',
11
+ visibility: 'public',
12
+ stability: 'experimental',
13
+ owners: ['@platform.core'],
14
+ tags: ['learning', 'ui', 'shared'],
15
+ },
16
+ docs: {
17
+ rootDocId: 'docs.examples.learning-journey-ui-shared',
18
+ },
19
+ entrypoints: {
20
+ packageName: '@contractspec/example.learning-journey-ui-shared',
21
+ docs: './docs',
22
+ },
23
+ surfaces: {
24
+ templates: true,
25
+ sandbox: { enabled: true, modes: ['playground', 'markdown'] },
26
+ studio: { enabled: true, installable: true },
27
+ mcp: { enabled: true },
28
+ },
29
+ });
30
+
31
+ export default example;
@@ -0,0 +1 @@
1
+ export { useLearningProgress } from './useLearningProgress';
@@ -0,0 +1,101 @@
1
+ 'use client';
2
+
3
+ import { useState, useCallback, useMemo } from 'react';
4
+ import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-journey/track-spec';
5
+ import type { LearningProgressState } from '../types';
6
+
7
+ /** Default progress state for a new track */
8
+ function createDefaultProgress(trackId: string): LearningProgressState {
9
+ return {
10
+ trackId,
11
+ completedStepIds: [],
12
+ currentStepId: null,
13
+ xpEarned: 0,
14
+ streakDays: 0,
15
+ lastActivityDate: null,
16
+ badges: [],
17
+ };
18
+ }
19
+
20
+ /** Hook for managing learning progress state */
21
+ export function useLearningProgress(track: LearningJourneyTrackSpec) {
22
+ const [progress, setProgress] = useState<LearningProgressState>(() =>
23
+ createDefaultProgress(track.id)
24
+ );
25
+
26
+ const completeStep = useCallback(
27
+ (stepId: string) => {
28
+ const step = track.steps.find((s) => s.id === stepId);
29
+ if (!step || progress.completedStepIds.includes(stepId)) return;
30
+
31
+ setProgress((prev) => {
32
+ const newCompletedIds = [...prev.completedStepIds, stepId];
33
+ const xpReward = step.xpReward ?? 0;
34
+
35
+ // Find next incomplete step
36
+ const nextStep = track.steps.find(
37
+ (s) => !newCompletedIds.includes(s.id)
38
+ );
39
+
40
+ // Check if track is complete
41
+ const isTrackComplete = newCompletedIds.length === track.steps.length;
42
+ const completionBonus = isTrackComplete
43
+ ? (track.completionRewards?.xpBonus ?? 0)
44
+ : 0;
45
+
46
+ return {
47
+ ...prev,
48
+ completedStepIds: newCompletedIds,
49
+ currentStepId: nextStep?.id ?? null,
50
+ xpEarned: prev.xpEarned + xpReward + completionBonus,
51
+ lastActivityDate: new Date().toISOString(),
52
+ badges:
53
+ isTrackComplete && track.completionRewards?.badgeKey
54
+ ? [...prev.badges, track.completionRewards.badgeKey]
55
+ : prev.badges,
56
+ };
57
+ });
58
+ },
59
+ [track, progress.completedStepIds]
60
+ );
61
+
62
+ const resetProgress = useCallback(() => {
63
+ setProgress(createDefaultProgress(track.id));
64
+ }, [track.id]);
65
+
66
+ const incrementStreak = useCallback(() => {
67
+ setProgress((prev) => ({
68
+ ...prev,
69
+ streakDays: prev.streakDays + 1,
70
+ lastActivityDate: new Date().toISOString(),
71
+ }));
72
+ }, []);
73
+
74
+ const stats = useMemo(() => {
75
+ const totalSteps = track.steps.length;
76
+ const completedSteps = progress.completedStepIds.length;
77
+ const percentComplete =
78
+ totalSteps > 0 ? Math.round((completedSteps / totalSteps) * 100) : 0;
79
+ const totalXp =
80
+ track.totalXp ??
81
+ track.steps.reduce((sum, s) => sum + (s.xpReward ?? 0), 0) +
82
+ (track.completionRewards?.xpBonus ?? 0);
83
+
84
+ return {
85
+ totalSteps,
86
+ completedSteps,
87
+ remainingSteps: totalSteps - completedSteps,
88
+ percentComplete,
89
+ totalXp,
90
+ isComplete: completedSteps === totalSteps,
91
+ };
92
+ }, [track, progress.completedStepIds]);
93
+
94
+ return {
95
+ progress,
96
+ stats,
97
+ completeStep,
98
+ resetProgress,
99
+ incrementStreak,
100
+ };
101
+ }
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ // Hooks
2
+ export { useLearningProgress } from './hooks';
3
+
4
+ // Components
5
+ export { XpBar, StreakCounter, BadgeDisplay, ViewTabs } from './components';
6
+
7
+ // Types
8
+ export type {
9
+ LearningView,
10
+ LearningProgressState,
11
+ LearningMiniAppProps,
12
+ LearningViewProps,
13
+ XpBarProps,
14
+ StreakCounterProps,
15
+ BadgeDisplayProps,
16
+ ViewTabsProps,
17
+ } from './types';
18
+
19
+ export { default as example } from './example';
20
+ import './docs';
package/src/types.ts ADDED
@@ -0,0 +1,61 @@
1
+ import type { LearningJourneyTrackSpec } from '@contractspec/module.learning-journey/track-spec';
2
+
3
+ /** View types for learning mini-apps */
4
+ export type LearningView = 'overview' | 'steps' | 'progress' | 'timeline';
5
+
6
+ /** Progress state for a learning track */
7
+ export interface LearningProgressState {
8
+ trackId: string;
9
+ completedStepIds: string[];
10
+ currentStepId: string | null;
11
+ xpEarned: number;
12
+ streakDays: number;
13
+ lastActivityDate: string | null;
14
+ badges: string[];
15
+ }
16
+
17
+ /** Props for mini-app components */
18
+ export interface LearningMiniAppProps {
19
+ track: LearningJourneyTrackSpec;
20
+ progress: LearningProgressState;
21
+ onStepComplete?: (stepId: string) => void;
22
+ onViewChange?: (view: LearningView) => void;
23
+ initialView?: LearningView;
24
+ }
25
+
26
+ /** Props for view components */
27
+ export interface LearningViewProps {
28
+ track: LearningJourneyTrackSpec;
29
+ progress: LearningProgressState;
30
+ onStepComplete?: (stepId: string) => void;
31
+ }
32
+
33
+ /** XP bar props */
34
+ export interface XpBarProps {
35
+ current: number;
36
+ max: number;
37
+ level?: number;
38
+ showLabel?: boolean;
39
+ size?: 'sm' | 'md' | 'lg';
40
+ }
41
+
42
+ /** Streak counter props */
43
+ export interface StreakCounterProps {
44
+ days: number;
45
+ isActive?: boolean;
46
+ size?: 'sm' | 'md' | 'lg';
47
+ }
48
+
49
+ /** Badge display props */
50
+ export interface BadgeDisplayProps {
51
+ badges: string[];
52
+ maxVisible?: number;
53
+ size?: 'sm' | 'md' | 'lg';
54
+ }
55
+
56
+ /** View tabs props */
57
+ export interface ViewTabsProps {
58
+ currentView: LearningView;
59
+ onViewChange: (view: LearningView) => void;
60
+ availableViews?: LearningView[];
61
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,9 @@
1
+ {
2
+ "extends": "@contractspec/tool.typescript/react-library.json",
3
+ "include": ["src"],
4
+ "exclude": ["node_modules"],
5
+ "compilerOptions": {
6
+ "rootDir": "src",
7
+ "outDir": "dist"
8
+ }
9
+ }