@vllnt/ui 0.2.0 → 0.2.1-canary.73a93ee
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/CHANGELOG.md +12 -1
- package/README.md +27 -12
- package/dist/components/activity-log/activity-log.js +1 -0
- package/dist/components/anchor-port/anchor-port.js +51 -0
- package/dist/components/anchor-port/index.js +4 -0
- package/dist/components/animated-text/animated-text.js +1 -0
- package/dist/components/bottom-bar/bottom-bar.js +25 -0
- package/dist/components/bottom-bar/index.js +4 -0
- package/dist/components/canvas-shell/canvas-foundation-demo.js +183 -0
- package/dist/components/canvas-shell/canvas-shell-route-config.js +0 -0
- package/dist/components/canvas-shell/canvas-shell.js +261 -0
- package/dist/components/canvas-shell/index.js +4 -0
- package/dist/components/canvas-view/canvas-view.js +461 -0
- package/dist/components/canvas-view/index.js +6 -0
- package/dist/components/chart/area-chart.js +1 -0
- package/dist/components/chart/line-chart.js +1 -0
- package/dist/components/chat-dock-section/chat-dock-section.js +56 -0
- package/dist/components/chat-dock-section/index.js +6 -0
- package/dist/components/checklist/checklist.js +7 -0
- package/dist/components/checklist/index.js +3 -1
- package/dist/components/comment-pin/comment-pin.js +104 -0
- package/dist/components/comment-pin/index.js +6 -0
- package/dist/components/connector-edge/connector-edge.js +66 -0
- package/dist/components/connector-edge/index.js +6 -0
- package/dist/components/conversation-thread/conversation-thread.js +348 -0
- package/dist/components/conversation-thread/index.js +20 -0
- package/dist/components/curriculum/curriculum.js +349 -0
- package/dist/components/curriculum/index.js +10 -0
- package/dist/components/data-list/data-list.js +1 -0
- package/dist/components/edge-label/edge-label.js +26 -0
- package/dist/components/edge-label/index.js +4 -0
- package/dist/components/form/form.js +432 -0
- package/dist/components/form/index.js +20 -0
- package/dist/components/glass-panel/glass-panel.js +21 -0
- package/dist/components/glass-panel/index.js +4 -0
- package/dist/components/group-hull/group-hull.js +29 -0
- package/dist/components/group-hull/index.js +4 -0
- package/dist/components/index.js +176 -0
- package/dist/components/infinite-plane/index.js +6 -0
- package/dist/components/infinite-plane/infinite-plane.js +75 -0
- package/dist/components/left-rail/index.js +4 -0
- package/dist/components/left-rail/left-rail.js +25 -0
- package/dist/components/live-cursor/index.js +6 -0
- package/dist/components/live-cursor/live-cursor.js +62 -0
- package/dist/components/mini-map-panel/index.js +6 -0
- package/dist/components/mini-map-panel/mini-map-panel.js +74 -0
- package/dist/components/multi-select/index.js +6 -0
- package/dist/components/multi-select/multi-select.js +258 -0
- package/dist/components/object-card/index.js +6 -0
- package/dist/components/object-card/object-card.js +126 -0
- package/dist/components/object-handle/index.js +4 -0
- package/dist/components/object-handle/object-handle.js +38 -0
- package/dist/components/overview-board/index.js +8 -0
- package/dist/components/overview-board/overview-board.js +127 -0
- package/dist/components/presence-stack/index.js +6 -0
- package/dist/components/presence-stack/presence-stack.js +108 -0
- package/dist/components/presence-sync-indicator/index.js +6 -0
- package/dist/components/presence-sync-indicator/presence-sync-indicator.js +73 -0
- package/dist/components/progress-tracker/index.js +20 -0
- package/dist/components/progress-tracker/progress-tracker.js +527 -0
- package/dist/components/right-dock/index.js +4 -0
- package/dist/components/right-dock/right-dock.js +28 -0
- package/dist/components/run-timeline/index.js +6 -0
- package/dist/components/run-timeline/run-timeline.js +221 -0
- package/dist/components/segmented-control/index.js +12 -0
- package/dist/components/segmented-control/segmented-control.js +61 -0
- package/dist/components/selection-presence/index.js +6 -0
- package/dist/components/selection-presence/selection-presence.js +50 -0
- package/dist/components/spinner/unicode-spinner.js +1 -0
- package/dist/components/tags-input/index.js +4 -0
- package/dist/components/tags-input/tags-input.js +178 -0
- package/dist/components/thread-bubble/index.js +6 -0
- package/dist/components/thread-bubble/thread-bubble.js +85 -0
- package/dist/components/top-bar/index.js +4 -0
- package/dist/components/top-bar/top-bar.js +31 -0
- package/dist/components/usage-breakdown/usage-breakdown.js +1 -0
- package/dist/components/viewport-bookmarks/index.js +6 -0
- package/dist/components/viewport-bookmarks/viewport-bookmarks.js +116 -0
- package/dist/components/workspace-switcher/index.js +6 -0
- package/dist/components/workspace-switcher/workspace-switcher.js +61 -0
- package/dist/components/world-breadcrumbs/index.js +6 -0
- package/dist/components/world-breadcrumbs/world-breadcrumbs.js +114 -0
- package/dist/components/zoom-hud/index.js +4 -0
- package/dist/components/zoom-hud/zoom-hud.js +61 -0
- package/dist/index.d.ts +1468 -6
- package/package.json +7 -3
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
forwardRef
|
|
5
|
+
} from "react";
|
|
6
|
+
import { cn } from "../../lib/utils";
|
|
7
|
+
const STATE_LABEL = {
|
|
8
|
+
error: "Sync error",
|
|
9
|
+
live: "Live",
|
|
10
|
+
offline: "Offline",
|
|
11
|
+
reconnecting: "Reconnecting",
|
|
12
|
+
syncing: "Syncing"
|
|
13
|
+
};
|
|
14
|
+
const STATE_DOT = {
|
|
15
|
+
error: "bg-red-500",
|
|
16
|
+
live: "bg-emerald-500",
|
|
17
|
+
offline: "bg-muted-foreground",
|
|
18
|
+
reconnecting: "bg-amber-500 animate-pulse",
|
|
19
|
+
syncing: "bg-blue-500 animate-pulse"
|
|
20
|
+
};
|
|
21
|
+
const STATE_TEXT = {
|
|
22
|
+
error: "text-red-700 dark:text-red-300",
|
|
23
|
+
live: "text-emerald-700 dark:text-emerald-300",
|
|
24
|
+
offline: "text-muted-foreground",
|
|
25
|
+
reconnecting: "text-amber-700 dark:text-amber-300",
|
|
26
|
+
syncing: "text-blue-700 dark:text-blue-300"
|
|
27
|
+
};
|
|
28
|
+
const DEFAULT_LABELS = {
|
|
29
|
+
region: "Presence sync"
|
|
30
|
+
};
|
|
31
|
+
const PresenceSyncIndicator = forwardRef((props, ref) => {
|
|
32
|
+
const { className, label, labels, state, status, ...rest } = props;
|
|
33
|
+
const resolvedLabels = { ...DEFAULT_LABELS, ...labels };
|
|
34
|
+
const text = label ?? STATE_LABEL[state];
|
|
35
|
+
return /* @__PURE__ */ jsxs(
|
|
36
|
+
"div",
|
|
37
|
+
{
|
|
38
|
+
"aria-label": `${resolvedLabels.region}: ${STATE_LABEL[state]}`,
|
|
39
|
+
className: cn(
|
|
40
|
+
"inline-flex items-center gap-1.5 rounded-full border border-border bg-background px-2 py-1 text-[11px] shadow-sm",
|
|
41
|
+
className
|
|
42
|
+
),
|
|
43
|
+
"data-presence-state": state,
|
|
44
|
+
"data-presence-sync": true,
|
|
45
|
+
ref,
|
|
46
|
+
role: "status",
|
|
47
|
+
...rest,
|
|
48
|
+
children: [
|
|
49
|
+
/* @__PURE__ */ jsx(
|
|
50
|
+
"span",
|
|
51
|
+
{
|
|
52
|
+
"aria-hidden": "true",
|
|
53
|
+
className: cn("h-1.5 w-1.5 rounded-full", STATE_DOT[state]),
|
|
54
|
+
"data-presence-sync-dot": true
|
|
55
|
+
}
|
|
56
|
+
),
|
|
57
|
+
/* @__PURE__ */ jsx("span", { className: cn("font-medium", STATE_TEXT[state]), children: text }),
|
|
58
|
+
status ? /* @__PURE__ */ jsx(
|
|
59
|
+
"span",
|
|
60
|
+
{
|
|
61
|
+
className: "text-[10px] text-muted-foreground",
|
|
62
|
+
"data-presence-sync-status": true,
|
|
63
|
+
children: status
|
|
64
|
+
}
|
|
65
|
+
) : null
|
|
66
|
+
]
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
});
|
|
70
|
+
PresenceSyncIndicator.displayName = "PresenceSyncIndicator";
|
|
71
|
+
export {
|
|
72
|
+
PresenceSyncIndicator
|
|
73
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ProgressTracker,
|
|
3
|
+
ProgressTrackerBadge,
|
|
4
|
+
ProgressTrackerModule,
|
|
5
|
+
ProgressTrackerModules,
|
|
6
|
+
ProgressTrackerOverview,
|
|
7
|
+
ProgressTrackerStat,
|
|
8
|
+
ProgressTrackerStats,
|
|
9
|
+
useProgressTrackerContext
|
|
10
|
+
} from "./progress-tracker";
|
|
11
|
+
export {
|
|
12
|
+
ProgressTracker,
|
|
13
|
+
ProgressTrackerBadge,
|
|
14
|
+
ProgressTrackerModule,
|
|
15
|
+
ProgressTrackerModules,
|
|
16
|
+
ProgressTrackerOverview,
|
|
17
|
+
ProgressTrackerStat,
|
|
18
|
+
ProgressTrackerStats,
|
|
19
|
+
useProgressTrackerContext
|
|
20
|
+
};
|
|
@@ -0,0 +1,527 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
5
|
+
import { Badge } from "../badge";
|
|
6
|
+
import {
|
|
7
|
+
Card,
|
|
8
|
+
CardContent,
|
|
9
|
+
CardDescription,
|
|
10
|
+
CardHeader,
|
|
11
|
+
CardTitle
|
|
12
|
+
} from "../card";
|
|
13
|
+
import { CHECKLIST_PROGRESS_EVENT } from "../checklist";
|
|
14
|
+
import { ProgressBar } from "../progress-bar";
|
|
15
|
+
const ProgressTrackerContext = React.createContext(null);
|
|
16
|
+
function clampPercentage(value) {
|
|
17
|
+
if (Number.isNaN(value)) return 0;
|
|
18
|
+
if (value <= 1) return Math.round(Math.max(0, value) * 100);
|
|
19
|
+
return Math.round(Math.min(Math.max(0, value), 100));
|
|
20
|
+
}
|
|
21
|
+
const STATUS_LABELS = {
|
|
22
|
+
available: "Available",
|
|
23
|
+
completed: "Completed",
|
|
24
|
+
"in-progress": "In progress",
|
|
25
|
+
locked: "Locked"
|
|
26
|
+
};
|
|
27
|
+
const STATUS_CLASSES = {
|
|
28
|
+
available: "border-secondary bg-secondary text-secondary-foreground",
|
|
29
|
+
completed: "border-primary/20 bg-primary text-primary-foreground",
|
|
30
|
+
"in-progress": "border-primary/20 bg-primary/10 text-primary",
|
|
31
|
+
locked: "border-border bg-muted text-muted-foreground"
|
|
32
|
+
};
|
|
33
|
+
function getStatusLabel(status) {
|
|
34
|
+
return STATUS_LABELS[status];
|
|
35
|
+
}
|
|
36
|
+
function getStatusClasses(status) {
|
|
37
|
+
return STATUS_CLASSES[status];
|
|
38
|
+
}
|
|
39
|
+
function readPersistedChecklistItems(persistKey) {
|
|
40
|
+
if (!persistKey || typeof window === "undefined") return [];
|
|
41
|
+
try {
|
|
42
|
+
const saved = localStorage.getItem(`checklist:${persistKey}`);
|
|
43
|
+
if (!saved) return [];
|
|
44
|
+
const parsed = JSON.parse(saved);
|
|
45
|
+
return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
function areStringArraysEqual(left, right) {
|
|
51
|
+
if (left.length !== right.length) return false;
|
|
52
|
+
return left.every((value, index) => value === right[index]);
|
|
53
|
+
}
|
|
54
|
+
function getChecklistPersistKey(event) {
|
|
55
|
+
if (!(event instanceof CustomEvent)) return null;
|
|
56
|
+
const detail = event.detail;
|
|
57
|
+
if (typeof detail !== "object" || detail === null) return null;
|
|
58
|
+
if (!("persistKey" in detail)) return null;
|
|
59
|
+
const { persistKey } = detail;
|
|
60
|
+
return typeof persistKey === "string" ? persistKey : null;
|
|
61
|
+
}
|
|
62
|
+
function getResolvedLessonProgress(module) {
|
|
63
|
+
if (!module.persistKey || !module.checklistItems?.length) {
|
|
64
|
+
return {
|
|
65
|
+
completedLessons: module.completedLessons ?? 0,
|
|
66
|
+
totalLessons: module.lessons
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
const validIds = new Set(module.checklistItems.map((item) => item.id));
|
|
70
|
+
const persistedIds = readPersistedChecklistItems(module.persistKey);
|
|
71
|
+
const completedLessons = persistedIds.filter((id) => validIds.has(id)).length;
|
|
72
|
+
return {
|
|
73
|
+
completedLessons,
|
|
74
|
+
totalLessons: module.checklistItems.length
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function useChecklistProgress(checklistItems = [], persistKey) {
|
|
78
|
+
const total = checklistItems.length;
|
|
79
|
+
const [persistedIds, setPersistedIds] = React.useState(
|
|
80
|
+
() => readPersistedChecklistItems(persistKey)
|
|
81
|
+
);
|
|
82
|
+
const setPersistedIdsIfChanged = React.useCallback((nextIds) => {
|
|
83
|
+
setPersistedIds(
|
|
84
|
+
(currentIds) => areStringArraysEqual(currentIds, nextIds) ? currentIds : nextIds
|
|
85
|
+
);
|
|
86
|
+
}, []);
|
|
87
|
+
React.useEffect(() => {
|
|
88
|
+
setPersistedIdsIfChanged(readPersistedChecklistItems(persistKey));
|
|
89
|
+
}, [persistKey, setPersistedIdsIfChanged]);
|
|
90
|
+
React.useEffect(() => {
|
|
91
|
+
if (!persistKey || typeof window === "undefined") return;
|
|
92
|
+
const sync = (event) => {
|
|
93
|
+
const eventPersistKey = getChecklistPersistKey(event);
|
|
94
|
+
if (eventPersistKey && eventPersistKey !== persistKey) return;
|
|
95
|
+
setPersistedIdsIfChanged(readPersistedChecklistItems(persistKey));
|
|
96
|
+
};
|
|
97
|
+
const syncEventListener = (event) => {
|
|
98
|
+
sync(event);
|
|
99
|
+
};
|
|
100
|
+
window.addEventListener("storage", sync);
|
|
101
|
+
window.addEventListener("focus", sync);
|
|
102
|
+
window.addEventListener(CHECKLIST_PROGRESS_EVENT, syncEventListener);
|
|
103
|
+
return () => {
|
|
104
|
+
window.removeEventListener("storage", sync);
|
|
105
|
+
window.removeEventListener("focus", sync);
|
|
106
|
+
window.removeEventListener(CHECKLIST_PROGRESS_EVENT, syncEventListener);
|
|
107
|
+
};
|
|
108
|
+
}, [persistKey, setPersistedIdsIfChanged]);
|
|
109
|
+
if (!persistKey || total === 0) return null;
|
|
110
|
+
const validIds = new Set(checklistItems.map((item) => item.id));
|
|
111
|
+
const completedCount = persistedIds.filter((id) => validIds.has(id)).length;
|
|
112
|
+
return {
|
|
113
|
+
completedCount,
|
|
114
|
+
progress: total > 0 ? Math.round(completedCount / total * 100) : 0,
|
|
115
|
+
total
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function useProgressTrackerContext() {
|
|
119
|
+
const context = React.useContext(ProgressTrackerContext);
|
|
120
|
+
if (!context) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
"ProgressTracker compound components must be used within <ProgressTracker />."
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
return context;
|
|
126
|
+
}
|
|
127
|
+
function ProgressTrackerRoot({
|
|
128
|
+
children,
|
|
129
|
+
className,
|
|
130
|
+
modules = [],
|
|
131
|
+
overallProgress,
|
|
132
|
+
streak = 0,
|
|
133
|
+
title = "Learning progress",
|
|
134
|
+
...props
|
|
135
|
+
}) {
|
|
136
|
+
const contextValue = React.useMemo(
|
|
137
|
+
() => ({
|
|
138
|
+
modules,
|
|
139
|
+
overallProgress: clampPercentage(overallProgress),
|
|
140
|
+
streak,
|
|
141
|
+
title
|
|
142
|
+
}),
|
|
143
|
+
[modules, overallProgress, streak, title]
|
|
144
|
+
);
|
|
145
|
+
return /* @__PURE__ */ jsx(ProgressTrackerContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsx(
|
|
146
|
+
"section",
|
|
147
|
+
{
|
|
148
|
+
"aria-label": title,
|
|
149
|
+
className: cn("grid gap-6", className),
|
|
150
|
+
...props,
|
|
151
|
+
children
|
|
152
|
+
}
|
|
153
|
+
) });
|
|
154
|
+
}
|
|
155
|
+
function ProgressTrackerOverview({
|
|
156
|
+
className,
|
|
157
|
+
description = "Track completion across modules, lessons, and exercises.",
|
|
158
|
+
label = "Overall progress",
|
|
159
|
+
...props
|
|
160
|
+
}) {
|
|
161
|
+
const { modules, overallProgress, streak, title } = useProgressTrackerContext();
|
|
162
|
+
const trackedPersistKeys = React.useMemo(
|
|
163
|
+
() => modules.map((module) => module.persistKey).filter(Boolean),
|
|
164
|
+
[modules]
|
|
165
|
+
);
|
|
166
|
+
const [, forceChecklistRefresh] = React.useState(0);
|
|
167
|
+
React.useEffect(() => {
|
|
168
|
+
if (trackedPersistKeys.length === 0 || typeof window === "undefined") {
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
const trackedKeys = new Set(trackedPersistKeys);
|
|
172
|
+
const sync = (event) => {
|
|
173
|
+
const eventPersistKey = getChecklistPersistKey(event);
|
|
174
|
+
if (eventPersistKey && !trackedKeys.has(eventPersistKey)) return;
|
|
175
|
+
forceChecklistRefresh((version) => version + 1);
|
|
176
|
+
};
|
|
177
|
+
const syncEventListener = (event) => {
|
|
178
|
+
sync(event);
|
|
179
|
+
};
|
|
180
|
+
window.addEventListener("storage", sync);
|
|
181
|
+
window.addEventListener("focus", sync);
|
|
182
|
+
window.addEventListener(CHECKLIST_PROGRESS_EVENT, syncEventListener);
|
|
183
|
+
return () => {
|
|
184
|
+
window.removeEventListener("storage", sync);
|
|
185
|
+
window.removeEventListener("focus", sync);
|
|
186
|
+
window.removeEventListener(CHECKLIST_PROGRESS_EVENT, syncEventListener);
|
|
187
|
+
};
|
|
188
|
+
}, [trackedPersistKeys]);
|
|
189
|
+
const radius = 54;
|
|
190
|
+
const circumference = 2 * Math.PI * radius;
|
|
191
|
+
const offset = circumference - overallProgress / 100 * circumference;
|
|
192
|
+
const completedModules = modules.filter(
|
|
193
|
+
(module) => module.status === "completed"
|
|
194
|
+
).length;
|
|
195
|
+
const lessonTotals = modules.reduce(
|
|
196
|
+
(totals, module) => {
|
|
197
|
+
const resolvedProgress = getResolvedLessonProgress(module);
|
|
198
|
+
return {
|
|
199
|
+
completedLessons: totals.completedLessons + resolvedProgress.completedLessons,
|
|
200
|
+
totalLessons: totals.totalLessons + resolvedProgress.totalLessons
|
|
201
|
+
};
|
|
202
|
+
},
|
|
203
|
+
{ completedLessons: 0, totalLessons: 0 }
|
|
204
|
+
);
|
|
205
|
+
const totalLessons = lessonTotals.totalLessons;
|
|
206
|
+
const completedLessons = lessonTotals.completedLessons;
|
|
207
|
+
const totalExercises = modules.reduce(
|
|
208
|
+
(sum, module) => sum + (module.exercises ?? 0),
|
|
209
|
+
0
|
|
210
|
+
);
|
|
211
|
+
const completedExercises = modules.reduce(
|
|
212
|
+
(sum, module) => sum + (module.completedExercises ?? 0),
|
|
213
|
+
0
|
|
214
|
+
);
|
|
215
|
+
return /* @__PURE__ */ jsxs(Card, { className: cn("overflow-hidden", className), ...props, children: [
|
|
216
|
+
/* @__PURE__ */ jsxs(CardHeader, { className: "gap-4 sm:flex-row sm:items-start sm:justify-between", children: [
|
|
217
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
218
|
+
/* @__PURE__ */ jsx(Badge, { className: "w-fit", variant: "secondary", children: label }),
|
|
219
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
220
|
+
/* @__PURE__ */ jsx(CardTitle, { children: title }),
|
|
221
|
+
/* @__PURE__ */ jsx(CardDescription, { children: description })
|
|
222
|
+
] })
|
|
223
|
+
] }),
|
|
224
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2 rounded-full border bg-background px-3 py-1 text-sm text-muted-foreground whitespace-nowrap", children: [
|
|
225
|
+
/* @__PURE__ */ jsx(
|
|
226
|
+
"span",
|
|
227
|
+
{
|
|
228
|
+
"aria-hidden": "true",
|
|
229
|
+
className: "inline-flex h-2.5 w-2.5 rounded-full bg-primary"
|
|
230
|
+
}
|
|
231
|
+
),
|
|
232
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
233
|
+
/* @__PURE__ */ jsx("span", { className: "font-semibold text-foreground", children: streak }),
|
|
234
|
+
" day",
|
|
235
|
+
streak === 1 ? "" : "s",
|
|
236
|
+
" streak"
|
|
237
|
+
] })
|
|
238
|
+
] })
|
|
239
|
+
] }),
|
|
240
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "grid gap-6 lg:grid-cols-[auto,1fr] lg:items-center", children: [
|
|
241
|
+
/* @__PURE__ */ jsxs("div", { className: "mx-auto flex flex-col items-center gap-3 text-center", children: [
|
|
242
|
+
/* @__PURE__ */ jsxs("div", { className: "relative flex h-36 w-36 items-center justify-center", children: [
|
|
243
|
+
/* @__PURE__ */ jsxs("svg", { className: "h-36 w-36 -rotate-90", viewBox: "0 0 120 120", children: [
|
|
244
|
+
/* @__PURE__ */ jsx(
|
|
245
|
+
"circle",
|
|
246
|
+
{
|
|
247
|
+
className: "stroke-muted",
|
|
248
|
+
cx: "60",
|
|
249
|
+
cy: "60",
|
|
250
|
+
fill: "none",
|
|
251
|
+
r: radius,
|
|
252
|
+
strokeWidth: "10"
|
|
253
|
+
}
|
|
254
|
+
),
|
|
255
|
+
/* @__PURE__ */ jsx(
|
|
256
|
+
"circle",
|
|
257
|
+
{
|
|
258
|
+
"aria-label": label,
|
|
259
|
+
"aria-valuemax": 100,
|
|
260
|
+
"aria-valuemin": 0,
|
|
261
|
+
"aria-valuenow": overallProgress,
|
|
262
|
+
className: "stroke-primary transition-all duration-500",
|
|
263
|
+
cx: "60",
|
|
264
|
+
cy: "60",
|
|
265
|
+
fill: "none",
|
|
266
|
+
r: radius,
|
|
267
|
+
role: "progressbar",
|
|
268
|
+
strokeDasharray: circumference,
|
|
269
|
+
strokeDashoffset: offset,
|
|
270
|
+
strokeLinecap: "round",
|
|
271
|
+
strokeWidth: "10"
|
|
272
|
+
}
|
|
273
|
+
)
|
|
274
|
+
] }),
|
|
275
|
+
/* @__PURE__ */ jsxs("div", { className: "absolute inset-0 flex flex-col items-center justify-center", children: [
|
|
276
|
+
/* @__PURE__ */ jsxs("span", { className: "text-3xl font-semibold text-foreground", children: [
|
|
277
|
+
overallProgress,
|
|
278
|
+
"%"
|
|
279
|
+
] }),
|
|
280
|
+
/* @__PURE__ */ jsx("span", { className: "text-xs uppercase tracking-[0.2em] text-muted-foreground", children: "Complete" })
|
|
281
|
+
] })
|
|
282
|
+
] }),
|
|
283
|
+
/* @__PURE__ */ jsxs("p", { className: "max-w-52 text-sm text-muted-foreground", children: [
|
|
284
|
+
completedModules,
|
|
285
|
+
" of ",
|
|
286
|
+
modules.length,
|
|
287
|
+
" modules completed."
|
|
288
|
+
] })
|
|
289
|
+
] }),
|
|
290
|
+
/* @__PURE__ */ jsxs("dl", { className: "grid gap-4 sm:grid-cols-2 xl:grid-cols-4", children: [
|
|
291
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-muted/30 p-4", children: [
|
|
292
|
+
/* @__PURE__ */ jsx("dt", { className: "text-sm text-muted-foreground", children: "Modules" }),
|
|
293
|
+
/* @__PURE__ */ jsxs("dd", { className: "mt-2 text-2xl font-semibold text-foreground", children: [
|
|
294
|
+
completedModules,
|
|
295
|
+
"/",
|
|
296
|
+
modules.length
|
|
297
|
+
] })
|
|
298
|
+
] }),
|
|
299
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-muted/30 p-4", children: [
|
|
300
|
+
/* @__PURE__ */ jsx("dt", { className: "text-sm text-muted-foreground", children: "Lessons" }),
|
|
301
|
+
/* @__PURE__ */ jsxs("dd", { className: "mt-2 text-2xl font-semibold text-foreground", children: [
|
|
302
|
+
completedLessons,
|
|
303
|
+
"/",
|
|
304
|
+
totalLessons
|
|
305
|
+
] })
|
|
306
|
+
] }),
|
|
307
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-muted/30 p-4", children: [
|
|
308
|
+
/* @__PURE__ */ jsx("dt", { className: "text-sm text-muted-foreground", children: "Exercises" }),
|
|
309
|
+
/* @__PURE__ */ jsxs("dd", { className: "mt-2 text-2xl font-semibold text-foreground", children: [
|
|
310
|
+
completedExercises,
|
|
311
|
+
"/",
|
|
312
|
+
totalExercises
|
|
313
|
+
] })
|
|
314
|
+
] }),
|
|
315
|
+
/* @__PURE__ */ jsxs("div", { className: "rounded-xl border bg-muted/30 p-4", children: [
|
|
316
|
+
/* @__PURE__ */ jsx("dt", { className: "text-sm text-muted-foreground", children: "Momentum" }),
|
|
317
|
+
/* @__PURE__ */ jsxs("dd", { className: "mt-2 text-2xl font-semibold text-foreground", children: [
|
|
318
|
+
streak,
|
|
319
|
+
" day",
|
|
320
|
+
streak === 1 ? "" : "s"
|
|
321
|
+
] })
|
|
322
|
+
] })
|
|
323
|
+
] })
|
|
324
|
+
] })
|
|
325
|
+
] });
|
|
326
|
+
}
|
|
327
|
+
function ProgressTrackerModules({
|
|
328
|
+
children,
|
|
329
|
+
className,
|
|
330
|
+
...props
|
|
331
|
+
}) {
|
|
332
|
+
return /* @__PURE__ */ jsx(
|
|
333
|
+
"div",
|
|
334
|
+
{
|
|
335
|
+
className: cn("grid gap-4 md:grid-cols-2 xl:grid-cols-3", className),
|
|
336
|
+
...props,
|
|
337
|
+
children
|
|
338
|
+
}
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
function ProgressTrackerModule({
|
|
342
|
+
badge,
|
|
343
|
+
checklistItems,
|
|
344
|
+
className,
|
|
345
|
+
completedExercises,
|
|
346
|
+
completedLessons = 0,
|
|
347
|
+
currentLesson,
|
|
348
|
+
description,
|
|
349
|
+
exercises,
|
|
350
|
+
href,
|
|
351
|
+
id,
|
|
352
|
+
lessons,
|
|
353
|
+
persistKey,
|
|
354
|
+
progress,
|
|
355
|
+
skills = [],
|
|
356
|
+
status,
|
|
357
|
+
timeSpent,
|
|
358
|
+
title,
|
|
359
|
+
...props
|
|
360
|
+
}) {
|
|
361
|
+
const checklistProgress = useChecklistProgress(checklistItems, persistKey);
|
|
362
|
+
const resolvedLessons = checklistProgress?.completedCount ?? completedLessons;
|
|
363
|
+
const resolvedLessonTotal = checklistProgress?.total || lessons;
|
|
364
|
+
const progressPercent = checklistProgress?.progress ?? clampPercentage(progress);
|
|
365
|
+
const progressValue = Math.min(resolvedLessons, resolvedLessonTotal);
|
|
366
|
+
const safeExerciseTotal = exercises ?? 0;
|
|
367
|
+
const safeExerciseComplete = Math.min(
|
|
368
|
+
completedExercises ?? 0,
|
|
369
|
+
safeExerciseTotal
|
|
370
|
+
);
|
|
371
|
+
const card = /* @__PURE__ */ jsxs(
|
|
372
|
+
Card,
|
|
373
|
+
{
|
|
374
|
+
"aria-disabled": status === "locked",
|
|
375
|
+
className: cn(
|
|
376
|
+
"h-full",
|
|
377
|
+
status === "locked" && "opacity-80",
|
|
378
|
+
href && "transition-shadow hover:shadow-md focus-within:shadow-md",
|
|
379
|
+
className
|
|
380
|
+
),
|
|
381
|
+
id,
|
|
382
|
+
...props,
|
|
383
|
+
children: [
|
|
384
|
+
/* @__PURE__ */ jsxs(CardHeader, { className: "gap-4", children: [
|
|
385
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-start justify-between gap-3", children: [
|
|
386
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
387
|
+
/* @__PURE__ */ jsx(CardTitle, { className: "text-xl", children: title }),
|
|
388
|
+
description ? /* @__PURE__ */ jsx(CardDescription, { children: description }) : null
|
|
389
|
+
] }),
|
|
390
|
+
/* @__PURE__ */ jsx(
|
|
391
|
+
Badge,
|
|
392
|
+
{
|
|
393
|
+
className: cn("whitespace-nowrap", getStatusClasses(status)),
|
|
394
|
+
variant: "outline",
|
|
395
|
+
children: getStatusLabel(status)
|
|
396
|
+
}
|
|
397
|
+
)
|
|
398
|
+
] }),
|
|
399
|
+
/* @__PURE__ */ jsxs("div", { className: "flex flex-wrap gap-2", children: [
|
|
400
|
+
badge ? /* @__PURE__ */ jsx(Badge, { variant: "secondary", children: badge }) : null,
|
|
401
|
+
currentLesson ? /* @__PURE__ */ jsxs(Badge, { variant: "outline", children: [
|
|
402
|
+
"Current: ",
|
|
403
|
+
currentLesson
|
|
404
|
+
] }) : null,
|
|
405
|
+
timeSpent ? /* @__PURE__ */ jsx(Badge, { variant: "outline", children: timeSpent }) : null
|
|
406
|
+
] })
|
|
407
|
+
] }),
|
|
408
|
+
/* @__PURE__ */ jsxs(CardContent, { className: "space-y-4", children: [
|
|
409
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
|
|
410
|
+
/* @__PURE__ */ jsx(
|
|
411
|
+
"div",
|
|
412
|
+
{
|
|
413
|
+
"aria-label": `${title} progress`,
|
|
414
|
+
"aria-valuemax": 100,
|
|
415
|
+
"aria-valuemin": 0,
|
|
416
|
+
"aria-valuenow": progressPercent,
|
|
417
|
+
role: "progressbar",
|
|
418
|
+
children: /* @__PURE__ */ jsx(
|
|
419
|
+
ProgressBar,
|
|
420
|
+
{
|
|
421
|
+
completedLabel: "lessons",
|
|
422
|
+
currentLabel: `${progressPercent}% complete`,
|
|
423
|
+
isComplete: status === "completed",
|
|
424
|
+
max: resolvedLessonTotal,
|
|
425
|
+
showLabels: true,
|
|
426
|
+
value: progressValue
|
|
427
|
+
}
|
|
428
|
+
)
|
|
429
|
+
}
|
|
430
|
+
),
|
|
431
|
+
/* @__PURE__ */ jsxs("div", { className: "grid gap-2 text-sm text-muted-foreground sm:grid-cols-2", children: [
|
|
432
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
433
|
+
"Lessons:",
|
|
434
|
+
" ",
|
|
435
|
+
/* @__PURE__ */ jsxs("span", { className: "font-medium text-foreground", children: [
|
|
436
|
+
resolvedLessons,
|
|
437
|
+
"/",
|
|
438
|
+
resolvedLessonTotal
|
|
439
|
+
] })
|
|
440
|
+
] }),
|
|
441
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
442
|
+
"Exercises:",
|
|
443
|
+
" ",
|
|
444
|
+
/* @__PURE__ */ jsxs("span", { className: "font-medium text-foreground", children: [
|
|
445
|
+
safeExerciseComplete,
|
|
446
|
+
"/",
|
|
447
|
+
safeExerciseTotal
|
|
448
|
+
] })
|
|
449
|
+
] })
|
|
450
|
+
] })
|
|
451
|
+
] }),
|
|
452
|
+
skills.length > 0 ? /* @__PURE__ */ jsx("div", { className: "flex flex-wrap gap-2", children: skills.map((skill) => /* @__PURE__ */ jsx(ProgressTrackerBadge, { children: skill }, skill)) }) : null
|
|
453
|
+
] })
|
|
454
|
+
]
|
|
455
|
+
}
|
|
456
|
+
);
|
|
457
|
+
if (!href || status === "locked") return card;
|
|
458
|
+
return /* @__PURE__ */ jsx(
|
|
459
|
+
"a",
|
|
460
|
+
{
|
|
461
|
+
className: "block rounded-xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
462
|
+
href,
|
|
463
|
+
children: card
|
|
464
|
+
}
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
function ProgressTrackerStats({
|
|
468
|
+
children,
|
|
469
|
+
className,
|
|
470
|
+
...props
|
|
471
|
+
}) {
|
|
472
|
+
return /* @__PURE__ */ jsx(
|
|
473
|
+
"div",
|
|
474
|
+
{
|
|
475
|
+
className: cn("grid gap-4 sm:grid-cols-2 xl:grid-cols-4", className),
|
|
476
|
+
...props,
|
|
477
|
+
children
|
|
478
|
+
}
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
function ProgressTrackerStat({
|
|
482
|
+
className,
|
|
483
|
+
label,
|
|
484
|
+
value,
|
|
485
|
+
...props
|
|
486
|
+
}) {
|
|
487
|
+
return /* @__PURE__ */ jsx(Card, { className: cn("h-full", className), ...props, children: /* @__PURE__ */ jsxs(CardContent, { className: "flex h-full flex-col justify-center gap-2 p-6", children: [
|
|
488
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm text-muted-foreground", children: label }),
|
|
489
|
+
/* @__PURE__ */ jsx("span", { className: "text-2xl font-semibold text-foreground", children: value })
|
|
490
|
+
] }) });
|
|
491
|
+
}
|
|
492
|
+
function ProgressTrackerBadge({
|
|
493
|
+
children,
|
|
494
|
+
className,
|
|
495
|
+
...props
|
|
496
|
+
}) {
|
|
497
|
+
return /* @__PURE__ */ jsx(
|
|
498
|
+
Badge,
|
|
499
|
+
{
|
|
500
|
+
className: cn(
|
|
501
|
+
"px-3 py-1 text-[11px] uppercase tracking-wide whitespace-nowrap",
|
|
502
|
+
className
|
|
503
|
+
),
|
|
504
|
+
variant: "secondary",
|
|
505
|
+
...props,
|
|
506
|
+
children
|
|
507
|
+
}
|
|
508
|
+
);
|
|
509
|
+
}
|
|
510
|
+
const ProgressTracker = Object.assign(ProgressTrackerRoot, {
|
|
511
|
+
Badge: ProgressTrackerBadge,
|
|
512
|
+
Module: ProgressTrackerModule,
|
|
513
|
+
Modules: ProgressTrackerModules,
|
|
514
|
+
Overview: ProgressTrackerOverview,
|
|
515
|
+
Stat: ProgressTrackerStat,
|
|
516
|
+
Stats: ProgressTrackerStats
|
|
517
|
+
});
|
|
518
|
+
export {
|
|
519
|
+
ProgressTracker,
|
|
520
|
+
ProgressTrackerBadge,
|
|
521
|
+
ProgressTrackerModule,
|
|
522
|
+
ProgressTrackerModules,
|
|
523
|
+
ProgressTrackerOverview,
|
|
524
|
+
ProgressTrackerStat,
|
|
525
|
+
ProgressTrackerStats,
|
|
526
|
+
useProgressTrackerContext
|
|
527
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
const RightDock = forwardRef(
|
|
5
|
+
({ children, className, footer, header, title, ...props }, ref) => /* @__PURE__ */ jsxs(
|
|
6
|
+
"aside",
|
|
7
|
+
{
|
|
8
|
+
className: cn(
|
|
9
|
+
"flex h-full min-w-[18rem] max-w-[24rem] shrink-0 flex-col border-l border-border bg-background",
|
|
10
|
+
className
|
|
11
|
+
),
|
|
12
|
+
ref,
|
|
13
|
+
...props,
|
|
14
|
+
children: [
|
|
15
|
+
header || title ? /* @__PURE__ */ jsxs("div", { className: "border-b border-border/60 px-4 py-3", children: [
|
|
16
|
+
title ? /* @__PURE__ */ jsx("div", { className: "text-sm font-medium text-foreground", children: title }) : null,
|
|
17
|
+
header ? /* @__PURE__ */ jsx("div", { className: "mt-2", children: header }) : null
|
|
18
|
+
] }) : null,
|
|
19
|
+
/* @__PURE__ */ jsx("div", { className: "flex-1 overflow-auto p-4", children }),
|
|
20
|
+
footer ? /* @__PURE__ */ jsx("div", { className: "border-t border-border/60 p-4", children: footer }) : null
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
)
|
|
24
|
+
);
|
|
25
|
+
RightDock.displayName = "RightDock";
|
|
26
|
+
export {
|
|
27
|
+
RightDock
|
|
28
|
+
};
|