@vllnt/ui 0.2.1-canary.9c473e0 → 0.2.1-canary.9e6a7be
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/dist/components/checklist/checklist.js +7 -0
- package/dist/components/checklist/index.js +3 -1
- package/dist/components/curriculum/curriculum.js +349 -0
- package/dist/components/curriculum/index.js +10 -0
- package/dist/components/form/form.js +220 -51
- package/dist/components/form/index.js +6 -2
- package/dist/components/index.js +31 -1
- package/dist/components/progress-tracker/index.js +20 -0
- package/dist/components/progress-tracker/progress-tracker.js +527 -0
- package/dist/index.d.ts +142 -10
- package/package.json +7 -3
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
3
|
import { useState } from "react";
|
|
4
4
|
import { cn } from "../../lib/utils";
|
|
5
|
+
const CHECKLIST_PROGRESS_EVENT = "vllnt:checklist-progress-change";
|
|
5
6
|
function ChecklistItemRow({
|
|
6
7
|
isChecked,
|
|
7
8
|
item,
|
|
@@ -133,6 +134,11 @@ function Checklist({
|
|
|
133
134
|
`checklist:${persistKey}`,
|
|
134
135
|
JSON.stringify([...newChecked])
|
|
135
136
|
);
|
|
137
|
+
window.dispatchEvent(
|
|
138
|
+
new CustomEvent(CHECKLIST_PROGRESS_EVENT, {
|
|
139
|
+
detail: { persistKey }
|
|
140
|
+
})
|
|
141
|
+
);
|
|
136
142
|
} catch {
|
|
137
143
|
}
|
|
138
144
|
}
|
|
@@ -177,5 +183,6 @@ function Checklist({
|
|
|
177
183
|
] });
|
|
178
184
|
}
|
|
179
185
|
export {
|
|
186
|
+
CHECKLIST_PROGRESS_EVENT,
|
|
180
187
|
Checklist
|
|
181
188
|
};
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import {
|
|
4
|
+
Children,
|
|
5
|
+
createContext,
|
|
6
|
+
isValidElement,
|
|
7
|
+
useCallback,
|
|
8
|
+
useContext,
|
|
9
|
+
useMemo,
|
|
10
|
+
useState
|
|
11
|
+
} from "react";
|
|
12
|
+
import {
|
|
13
|
+
BookOpen,
|
|
14
|
+
CheckCircle2,
|
|
15
|
+
ChevronDown,
|
|
16
|
+
Clock,
|
|
17
|
+
GraduationCap,
|
|
18
|
+
Link2,
|
|
19
|
+
Lock,
|
|
20
|
+
PlayCircle
|
|
21
|
+
} from "lucide-react";
|
|
22
|
+
import { cn } from "../../lib/utils";
|
|
23
|
+
const CurriculumContext = createContext(null);
|
|
24
|
+
function useCurriculumContext() {
|
|
25
|
+
const ctx = useContext(CurriculumContext);
|
|
26
|
+
if (!ctx) {
|
|
27
|
+
throw new Error("CurriculumModule must be used within a Curriculum");
|
|
28
|
+
}
|
|
29
|
+
return ctx;
|
|
30
|
+
}
|
|
31
|
+
function summarizeLessonProgress(children) {
|
|
32
|
+
let completed = 0;
|
|
33
|
+
let total = 0;
|
|
34
|
+
Children.forEach(children, (child) => {
|
|
35
|
+
if (!isValidElement(child)) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (child.type === CurriculumLesson) {
|
|
39
|
+
total += 1;
|
|
40
|
+
if (child.props.status === "completed") {
|
|
41
|
+
completed += 1;
|
|
42
|
+
}
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const nested = summarizeLessonProgress(child.props.children);
|
|
46
|
+
total += nested.total;
|
|
47
|
+
completed += nested.completed;
|
|
48
|
+
});
|
|
49
|
+
return { completed, total };
|
|
50
|
+
}
|
|
51
|
+
function statusLabel(status) {
|
|
52
|
+
if (status === "completed") return "Completed";
|
|
53
|
+
if (status === "in-progress") return "In progress";
|
|
54
|
+
if (status === "locked") return "Locked";
|
|
55
|
+
return "Available";
|
|
56
|
+
}
|
|
57
|
+
function statusIcon(status) {
|
|
58
|
+
if (status === "completed") {
|
|
59
|
+
return /* @__PURE__ */ jsx(CheckCircle2, { className: "h-4 w-4 flex-shrink-0 text-green-500" });
|
|
60
|
+
}
|
|
61
|
+
if (status === "in-progress") {
|
|
62
|
+
return /* @__PURE__ */ jsx(PlayCircle, { className: "h-4 w-4 flex-shrink-0 text-primary" });
|
|
63
|
+
}
|
|
64
|
+
if (status === "locked") {
|
|
65
|
+
return /* @__PURE__ */ jsx(Lock, { className: "h-4 w-4 flex-shrink-0 text-muted-foreground" });
|
|
66
|
+
}
|
|
67
|
+
return /* @__PURE__ */ jsx(BookOpen, { className: "h-4 w-4 flex-shrink-0 text-muted-foreground" });
|
|
68
|
+
}
|
|
69
|
+
const difficultyStyles = {
|
|
70
|
+
advanced: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
|
|
71
|
+
beginner: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
|
|
72
|
+
intermediate: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400"
|
|
73
|
+
};
|
|
74
|
+
function ProgressBar({
|
|
75
|
+
completed,
|
|
76
|
+
progressPct,
|
|
77
|
+
total
|
|
78
|
+
}) {
|
|
79
|
+
if (total === 0) return null;
|
|
80
|
+
return /* @__PURE__ */ jsxs("div", { className: "mt-1 flex items-center gap-2", children: [
|
|
81
|
+
/* @__PURE__ */ jsx("div", { className: "h-1.5 max-w-[120px] flex-1 overflow-hidden rounded-full bg-muted", children: /* @__PURE__ */ jsx(
|
|
82
|
+
"div",
|
|
83
|
+
{
|
|
84
|
+
className: "h-full rounded-full bg-primary transition-all",
|
|
85
|
+
style: { width: `${progressPct}%` }
|
|
86
|
+
}
|
|
87
|
+
) }),
|
|
88
|
+
/* @__PURE__ */ jsxs("span", { className: "text-xs text-muted-foreground", children: [
|
|
89
|
+
completed,
|
|
90
|
+
"/",
|
|
91
|
+
total
|
|
92
|
+
] })
|
|
93
|
+
] });
|
|
94
|
+
}
|
|
95
|
+
function LessonMeta({
|
|
96
|
+
difficulty,
|
|
97
|
+
duration,
|
|
98
|
+
prerequisites
|
|
99
|
+
}) {
|
|
100
|
+
const prerequisitesLabel = prerequisites?.length ? `Requires: ${prerequisites.join(", ")}` : null;
|
|
101
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex flex-shrink-0 items-center gap-2", children: [
|
|
102
|
+
prerequisitesLabel ? /* @__PURE__ */ jsx(
|
|
103
|
+
"span",
|
|
104
|
+
{
|
|
105
|
+
"aria-label": prerequisitesLabel,
|
|
106
|
+
className: "flex items-center gap-1 text-xs text-muted-foreground",
|
|
107
|
+
title: prerequisitesLabel,
|
|
108
|
+
children: /* @__PURE__ */ jsx(Link2, { "aria-hidden": "true", className: "h-3 w-3" })
|
|
109
|
+
}
|
|
110
|
+
) : null,
|
|
111
|
+
difficulty ? /* @__PURE__ */ jsx(
|
|
112
|
+
"span",
|
|
113
|
+
{
|
|
114
|
+
className: cn(
|
|
115
|
+
"rounded px-1.5 py-0.5 text-xs font-medium capitalize",
|
|
116
|
+
difficultyStyles[difficulty]
|
|
117
|
+
),
|
|
118
|
+
children: difficulty
|
|
119
|
+
}
|
|
120
|
+
) : null,
|
|
121
|
+
duration ? /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
122
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3 w-3" }),
|
|
123
|
+
duration
|
|
124
|
+
] }) : null
|
|
125
|
+
] });
|
|
126
|
+
}
|
|
127
|
+
function ModuleTrigger({
|
|
128
|
+
completed,
|
|
129
|
+
description,
|
|
130
|
+
estimatedHours,
|
|
131
|
+
id,
|
|
132
|
+
isExpanded,
|
|
133
|
+
progressPct,
|
|
134
|
+
title,
|
|
135
|
+
toggle,
|
|
136
|
+
total
|
|
137
|
+
}) {
|
|
138
|
+
return /* @__PURE__ */ jsxs(
|
|
139
|
+
"button",
|
|
140
|
+
{
|
|
141
|
+
"aria-controls": `module-content-${id}`,
|
|
142
|
+
"aria-expanded": isExpanded,
|
|
143
|
+
className: cn(
|
|
144
|
+
"w-full flex items-start justify-between gap-3 rounded-md px-6 py-4 text-left transition-colors",
|
|
145
|
+
"hover:bg-muted/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
|
|
146
|
+
isExpanded && "bg-muted/30"
|
|
147
|
+
),
|
|
148
|
+
onClick: toggle,
|
|
149
|
+
type: "button",
|
|
150
|
+
children: [
|
|
151
|
+
/* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-col gap-1", children: [
|
|
152
|
+
/* @__PURE__ */ jsx("span", { className: "text-sm font-medium", children: title }),
|
|
153
|
+
description ? /* @__PURE__ */ jsx("span", { className: "line-clamp-1 text-xs text-muted-foreground", children: description }) : null,
|
|
154
|
+
/* @__PURE__ */ jsx(
|
|
155
|
+
ProgressBar,
|
|
156
|
+
{
|
|
157
|
+
completed,
|
|
158
|
+
progressPct,
|
|
159
|
+
total
|
|
160
|
+
}
|
|
161
|
+
)
|
|
162
|
+
] }),
|
|
163
|
+
/* @__PURE__ */ jsxs("div", { className: "mt-0.5 flex flex-shrink-0 items-center gap-3", children: [
|
|
164
|
+
estimatedHours === void 0 ? null : /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 text-xs text-muted-foreground", children: [
|
|
165
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-3.5 w-3.5" }),
|
|
166
|
+
estimatedHours,
|
|
167
|
+
"h"
|
|
168
|
+
] }),
|
|
169
|
+
/* @__PURE__ */ jsx(
|
|
170
|
+
ChevronDown,
|
|
171
|
+
{
|
|
172
|
+
className: cn(
|
|
173
|
+
"h-4 w-4 text-muted-foreground transition-transform duration-200",
|
|
174
|
+
isExpanded && "rotate-180"
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
] })
|
|
179
|
+
]
|
|
180
|
+
}
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
function CurriculumRoot({
|
|
184
|
+
children,
|
|
185
|
+
className,
|
|
186
|
+
defaultExpandedModules,
|
|
187
|
+
title,
|
|
188
|
+
totalHours
|
|
189
|
+
}) {
|
|
190
|
+
const [expandedModules, setExpandedModules] = useState(
|
|
191
|
+
() => new Set(defaultExpandedModules ?? [])
|
|
192
|
+
);
|
|
193
|
+
const toggleModule = useCallback((id) => {
|
|
194
|
+
setExpandedModules((previous) => {
|
|
195
|
+
const next = new Set(previous);
|
|
196
|
+
if (next.has(id)) {
|
|
197
|
+
next.delete(id);
|
|
198
|
+
} else {
|
|
199
|
+
next.add(id);
|
|
200
|
+
}
|
|
201
|
+
return next;
|
|
202
|
+
});
|
|
203
|
+
}, []);
|
|
204
|
+
const contextValue = useMemo(
|
|
205
|
+
() => ({ expandedModules, toggleModule }),
|
|
206
|
+
[expandedModules, toggleModule]
|
|
207
|
+
);
|
|
208
|
+
return /* @__PURE__ */ jsx(CurriculumContext.Provider, { value: contextValue, children: /* @__PURE__ */ jsxs(
|
|
209
|
+
"div",
|
|
210
|
+
{
|
|
211
|
+
className: cn(
|
|
212
|
+
"my-6 rounded-lg border bg-card text-card-foreground shadow-sm",
|
|
213
|
+
className
|
|
214
|
+
),
|
|
215
|
+
children: [
|
|
216
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center justify-between border-b px-6 py-4", children: [
|
|
217
|
+
/* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
|
|
218
|
+
/* @__PURE__ */ jsx(GraduationCap, { className: "h-5 w-5 text-primary" }),
|
|
219
|
+
/* @__PURE__ */ jsx("h2", { className: "text-lg font-semibold", children: title })
|
|
220
|
+
] }),
|
|
221
|
+
totalHours === void 0 ? null : /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1 text-sm text-muted-foreground", children: [
|
|
222
|
+
/* @__PURE__ */ jsx(Clock, { className: "h-4 w-4" }),
|
|
223
|
+
/* @__PURE__ */ jsxs("span", { children: [
|
|
224
|
+
totalHours,
|
|
225
|
+
"h total"
|
|
226
|
+
] })
|
|
227
|
+
] })
|
|
228
|
+
] }),
|
|
229
|
+
/* @__PURE__ */ jsx("div", { "aria-label": title, className: "divide-y", children })
|
|
230
|
+
]
|
|
231
|
+
}
|
|
232
|
+
) });
|
|
233
|
+
}
|
|
234
|
+
function CurriculumModule({
|
|
235
|
+
children,
|
|
236
|
+
className,
|
|
237
|
+
description,
|
|
238
|
+
estimatedHours,
|
|
239
|
+
id,
|
|
240
|
+
title
|
|
241
|
+
}) {
|
|
242
|
+
const { expandedModules, toggleModule } = useCurriculumContext();
|
|
243
|
+
const isExpanded = expandedModules.has(id);
|
|
244
|
+
const { completed, total } = useMemo(
|
|
245
|
+
() => summarizeLessonProgress(children),
|
|
246
|
+
[children]
|
|
247
|
+
);
|
|
248
|
+
const progressPct = total > 0 ? Math.round(completed / total * 100) : 0;
|
|
249
|
+
return /* @__PURE__ */ jsxs("section", { className, children: [
|
|
250
|
+
/* @__PURE__ */ jsx(
|
|
251
|
+
ModuleTrigger,
|
|
252
|
+
{
|
|
253
|
+
completed,
|
|
254
|
+
description,
|
|
255
|
+
estimatedHours,
|
|
256
|
+
id,
|
|
257
|
+
isExpanded,
|
|
258
|
+
progressPct,
|
|
259
|
+
title,
|
|
260
|
+
toggle: () => {
|
|
261
|
+
toggleModule(id);
|
|
262
|
+
},
|
|
263
|
+
total
|
|
264
|
+
}
|
|
265
|
+
),
|
|
266
|
+
/* @__PURE__ */ jsx(
|
|
267
|
+
"div",
|
|
268
|
+
{
|
|
269
|
+
"aria-hidden": !isExpanded,
|
|
270
|
+
className: cn(
|
|
271
|
+
"overflow-hidden transition-all duration-200",
|
|
272
|
+
isExpanded ? "max-h-[9999px] opacity-100" : "max-h-0 opacity-0"
|
|
273
|
+
),
|
|
274
|
+
hidden: !isExpanded,
|
|
275
|
+
id: `module-content-${id}`,
|
|
276
|
+
children: /* @__PURE__ */ jsx("div", { className: "divide-y divide-border/50 pb-2", children })
|
|
277
|
+
}
|
|
278
|
+
)
|
|
279
|
+
] });
|
|
280
|
+
}
|
|
281
|
+
function CurriculumLesson({
|
|
282
|
+
className,
|
|
283
|
+
difficulty,
|
|
284
|
+
duration,
|
|
285
|
+
href,
|
|
286
|
+
prerequisites,
|
|
287
|
+
status = "available",
|
|
288
|
+
title
|
|
289
|
+
}) {
|
|
290
|
+
const isLocked = status === "locked";
|
|
291
|
+
const inner = /* @__PURE__ */ jsxs(
|
|
292
|
+
"div",
|
|
293
|
+
{
|
|
294
|
+
"aria-disabled": isLocked || void 0,
|
|
295
|
+
className: cn(
|
|
296
|
+
"flex items-center gap-3 px-6 py-3 pl-10 text-sm transition-colors",
|
|
297
|
+
isLocked ? "cursor-not-allowed opacity-60" : "",
|
|
298
|
+
!isLocked && href ? "cursor-pointer hover:bg-muted/40" : "",
|
|
299
|
+
className
|
|
300
|
+
),
|
|
301
|
+
children: [
|
|
302
|
+
statusIcon(status),
|
|
303
|
+
/* @__PURE__ */ jsx("span", { className: "sr-only", children: statusLabel(status) }),
|
|
304
|
+
/* @__PURE__ */ jsxs(
|
|
305
|
+
"span",
|
|
306
|
+
{
|
|
307
|
+
className: cn(
|
|
308
|
+
"min-w-0 flex-1 truncate",
|
|
309
|
+
status === "completed" && "text-muted-foreground line-through",
|
|
310
|
+
status === "in-progress" && "font-medium"
|
|
311
|
+
),
|
|
312
|
+
children: [
|
|
313
|
+
title,
|
|
314
|
+
isLocked ? /* @__PURE__ */ jsx("span", { className: "sr-only", children: " (Locked)" }) : null
|
|
315
|
+
]
|
|
316
|
+
}
|
|
317
|
+
),
|
|
318
|
+
/* @__PURE__ */ jsx(
|
|
319
|
+
LessonMeta,
|
|
320
|
+
{
|
|
321
|
+
difficulty,
|
|
322
|
+
duration,
|
|
323
|
+
prerequisites
|
|
324
|
+
}
|
|
325
|
+
)
|
|
326
|
+
]
|
|
327
|
+
}
|
|
328
|
+
);
|
|
329
|
+
if (href && !isLocked) {
|
|
330
|
+
return /* @__PURE__ */ jsx(
|
|
331
|
+
"a",
|
|
332
|
+
{
|
|
333
|
+
className: "block rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-2",
|
|
334
|
+
href,
|
|
335
|
+
children: inner
|
|
336
|
+
}
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
return inner;
|
|
340
|
+
}
|
|
341
|
+
const Curriculum = Object.assign(CurriculumRoot, {
|
|
342
|
+
Lesson: CurriculumLesson,
|
|
343
|
+
Module: CurriculumModule
|
|
344
|
+
});
|
|
345
|
+
export {
|
|
346
|
+
Curriculum,
|
|
347
|
+
CurriculumLesson,
|
|
348
|
+
CurriculumModule
|
|
349
|
+
};
|