@hydralms/components 0.3.0 → 0.3.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.
Files changed (47) hide show
  1. package/dist/StudentProfile-BPsZBaJj.cjs +1 -0
  2. package/dist/{StudentProfile-DeMxdrL3.js → StudentProfile-Cw2p-RZn.js} +577 -579
  3. package/dist/index.cjs +1 -1
  4. package/dist/index.js +172 -166
  5. package/dist/license/index.d.ts +2 -2
  6. package/dist/license/tiers.d.ts +3 -0
  7. package/dist/modules.cjs +1 -1
  8. package/dist/modules.js +111 -110
  9. package/dist/sections/AdaptiveLearningPath/AdaptiveLearningPath.d.ts +5 -0
  10. package/dist/sections/AdaptiveLearningPath/path-connector.d.ts +8 -0
  11. package/dist/sections/AdaptiveLearningPath/path-milestone-marker.d.ts +7 -0
  12. package/dist/sections/AdaptiveLearningPath/path-node-card.d.ts +10 -0
  13. package/dist/sections/AdaptiveLearningPath/path-skill-bar.d.ts +8 -0
  14. package/dist/sections/AdaptiveLearningPath/types.d.ts +136 -0
  15. package/dist/sections/ContentAuthoringStudio/ContentAuthoringStudio.d.ts +5 -0
  16. package/dist/sections/ContentAuthoringStudio/block-editor-item.d.ts +14 -0
  17. package/dist/sections/ContentAuthoringStudio/block-type-picker.d.ts +12 -0
  18. package/dist/sections/ContentAuthoringStudio/types.d.ts +67 -0
  19. package/dist/sections/index.d.ts +4 -0
  20. package/dist/sections.cjs +1 -1
  21. package/dist/sections.js +1325 -232
  22. package/dist/withProGate-BJdu1T9Y.cjs +2 -0
  23. package/dist/withProGate-BvFc7Jwy.js +4975 -0
  24. package/package.json +24 -7
  25. package/src/license/index.ts +2 -2
  26. package/src/license/tiers.ts +12 -2
  27. package/src/modules/CoursePlayer/CoursePlayer.tsx +3 -1
  28. package/src/progress/stat-card.tsx +10 -5
  29. package/src/sections/AdaptiveLearningPath/AdaptiveLearningPath.tsx +251 -0
  30. package/src/sections/AdaptiveLearningPath/path-connector.tsx +27 -0
  31. package/src/sections/AdaptiveLearningPath/path-milestone-marker.tsx +50 -0
  32. package/src/sections/AdaptiveLearningPath/path-node-card.tsx +166 -0
  33. package/src/sections/AdaptiveLearningPath/path-skill-bar.tsx +49 -0
  34. package/src/sections/AdaptiveLearningPath/types.ts +159 -0
  35. package/src/sections/ContentAuthoringStudio/ContentAuthoringStudio.tsx +289 -0
  36. package/src/sections/ContentAuthoringStudio/block-editor-item.tsx +487 -0
  37. package/src/sections/ContentAuthoringStudio/block-type-picker.tsx +123 -0
  38. package/src/sections/ContentAuthoringStudio/types.ts +67 -0
  39. package/src/sections/ForumBoard/ForumBoard.tsx +8 -6
  40. package/src/sections/LessonPage/LessonPage.tsx +4 -7
  41. package/src/sections/index.ts +18 -0
  42. package/src/video/video-player.tsx +14 -5
  43. package/dist/StudentProfile-BVfZMbnV.cjs +0 -1
  44. package/dist/tabs-BsfVo2Bl.cjs +0 -173
  45. package/dist/tabs-BuY1iNJE.js +0 -22305
  46. package/dist/withProGate-BWqcKdPM.js +0 -137
  47. package/dist/withProGate-DX6XqKLp.cjs +0 -1
@@ -0,0 +1,159 @@
1
+ import type { ReactNode } from "react";
2
+
3
+ /**
4
+ * Status of a single node in the learning path.
5
+ * - `locked` — prerequisites not met, node is inaccessible
6
+ * - `available` — prerequisites met, learner can start
7
+ * - `in_progress` — learner is currently working on this node
8
+ * - `completed` — learner finished but below mastery threshold
9
+ * - `mastered` — learner finished and reached the mastery threshold
10
+ * - `skipped` — node was bypassed by the adaptive engine
11
+ */
12
+ export type PathNodeStatus =
13
+ | "locked"
14
+ | "available"
15
+ | "in_progress"
16
+ | "completed"
17
+ | "mastered"
18
+ | "skipped";
19
+
20
+ /**
21
+ * The type of learning activity a path node represents.
22
+ * Aligns with LearningObjectIcon type strings.
23
+ */
24
+ export type PathNodeType =
25
+ | "lesson"
26
+ | "quiz"
27
+ | "assignment"
28
+ | "video"
29
+ | "discussion"
30
+ | "document"
31
+ | "assessment"
32
+ | "audio"
33
+ | "link";
34
+
35
+ /** A skill or competency area that the learning path develops. */
36
+ export interface PathSkill {
37
+ uid: string;
38
+ name: string;
39
+ /** Current proficiency level 0–100 */
40
+ proficiency: number;
41
+ /** Target proficiency level 0–100 */
42
+ targetProficiency?: number;
43
+ }
44
+
45
+ /** A milestone marker along the learning path. */
46
+ export interface PathMilestone {
47
+ uid: string;
48
+ title: string;
49
+ description?: string;
50
+ /** Whether the milestone has been reached */
51
+ reached: boolean;
52
+ /** Index of the node after which this milestone appears (zero-based) */
53
+ afterNodeIndex: number;
54
+ variant?: "default" | "gold" | "silver" | "bronze";
55
+ }
56
+
57
+ /** A single node in the adaptive learning path. */
58
+ export interface PathNode {
59
+ uid: string;
60
+ title: string;
61
+ description?: string;
62
+ /** Learning activity type — used for icon selection via LearningObjectIcon */
63
+ type: PathNodeType;
64
+ status: PathNodeStatus;
65
+ /** Estimated duration in seconds */
66
+ estimatedDuration?: number;
67
+ /** Performance score if completed (0–100) */
68
+ score?: number;
69
+ /** Whether this node is recommended as the next step */
70
+ recommended?: boolean;
71
+ /** Skill UIDs that this node develops */
72
+ skillUids?: string[];
73
+ /** UIDs of prerequisite nodes */
74
+ prerequisites?: string[];
75
+ /** Optional custom icon (overrides type-based icon) */
76
+ icon?: ReactNode;
77
+ }
78
+
79
+ /** A branch point where the adaptive engine may redirect the learner. */
80
+ export interface PathBranch {
81
+ uid: string;
82
+ /** UID of the node where the branch originates */
83
+ fromNodeUid: string;
84
+ /** Label for this branch (e.g., "Review Fundamentals") */
85
+ label: string;
86
+ /** UIDs of nodes in this branch path */
87
+ nodeUids: string[];
88
+ /** Whether this branch is the active/chosen branch */
89
+ active?: boolean;
90
+ /** Condition description (e.g., "Score below 70%") */
91
+ condition?: string;
92
+ }
93
+
94
+ /** Overall progress summary for the path. */
95
+ export interface PathProgress {
96
+ /** Percentage of path completed (0–100) */
97
+ completionPercentage: number;
98
+ completedNodes: number;
99
+ totalNodes: number;
100
+ /** Total time spent in seconds */
101
+ totalTimeSpent: number;
102
+ /** Average score across completed nodes (0–100) */
103
+ averageScore?: number;
104
+ /** Current streak in days */
105
+ currentStreak?: number;
106
+ }
107
+
108
+ /**
109
+ * AdaptiveLearningPath section — a premium visualization of a personalized
110
+ * learning sequence that adapts based on learner performance.
111
+ *
112
+ * Shows a visual path map with nodes, connectors, milestones, skill bars,
113
+ * and summary stats. Each node represents a learning activity with status,
114
+ * score, and recommended-next indicators.
115
+ *
116
+ * @example
117
+ * <AdaptiveLearningPath
118
+ * title="React Mastery Path"
119
+ * description="A personalized journey for mastering React"
120
+ * nodes={pathNodes}
121
+ * progress={pathProgress}
122
+ * onNodeClick={(uid) => navigate(`/activity/${uid}`)}
123
+ * onNodeStart={(uid) => startActivity(uid)}
124
+ * />
125
+ */
126
+ export interface AdaptiveLearningPathProps {
127
+ /** Path title */
128
+ title: string;
129
+ /** Optional path description */
130
+ description?: string;
131
+ /** Difficulty level displayed in the header */
132
+ difficulty?: "beginner" | "intermediate" | "advanced";
133
+ /** Ordered list of learning path nodes */
134
+ nodes: PathNode[];
135
+ /** Overall path progress data */
136
+ progress: PathProgress;
137
+ /** Skill/competency areas covered by this path */
138
+ skills?: PathSkill[];
139
+ /** Milestones along the path */
140
+ milestones?: PathMilestone[];
141
+ /** Branch points for adaptive routing */
142
+ branches?: PathBranch[];
143
+ /** Called when the user clicks a node */
144
+ onNodeClick?: (nodeUid: string) => void;
145
+ /** Called when the user starts a recommended or available activity */
146
+ onNodeStart?: (nodeUid: string) => void;
147
+ /** Called when the user clicks a skill */
148
+ onSkillClick?: (skillUid: string) => void;
149
+ /** When true, disables all interactive elements. @default false */
150
+ readOnly?: boolean;
151
+ /** Render skeleton placeholders instead of content */
152
+ isLoading?: boolean;
153
+ /** Error message — renders an error state with optional retry */
154
+ error?: string | null;
155
+ /** Called when the user clicks retry in the error state */
156
+ onRetry?: () => void;
157
+ className?: string;
158
+ style?: React.CSSProperties;
159
+ }
@@ -0,0 +1,289 @@
1
+ import { useState, useCallback, useMemo, useRef } from "react";
2
+ import { Eye, Pencil, Save } from "lucide-react";
3
+ import { Button } from "../../ui/button";
4
+ import { Input } from "../../ui/input";
5
+ import { Skeleton } from "../../ui/skeleton";
6
+ import { ContentBlock } from "../../content";
7
+ import { EmptyState } from "../../common/empty-state";
8
+ import { SectionShell } from "../_shared/section-shell";
9
+ import { useDragReorder } from "../../questions/use-drag-reorder";
10
+ import { withProGate } from "../../license/withProGate";
11
+ import type { LessonBlock } from "../../content/types";
12
+ import type { ContentAuthoringStudioProps, AuthoringBlock } from "./types";
13
+ import { BlockEditorItem } from "./block-editor-item";
14
+ import { BlockTypePicker } from "./block-type-picker";
15
+
16
+ let idCounter = 0;
17
+ function genId() {
18
+ return `ab_${++idCounter}_${Date.now().toString(36)}`;
19
+ }
20
+
21
+ function toAuthoring(blocks: LessonBlock[]): AuthoringBlock[] {
22
+ return blocks.map((block) => ({ id: genId(), block }));
23
+ }
24
+
25
+ function createEmptyBlock(type: LessonBlock["type"]): LessonBlock {
26
+ switch (type) {
27
+ case "richtext":
28
+ return { type: "richtext", html: "" };
29
+ case "heading":
30
+ return { type: "heading", text: "", level: 2 };
31
+ case "image":
32
+ return { type: "image", src: "", alt: "" };
33
+ case "video":
34
+ return { type: "video", video: { src: "" } };
35
+ case "code":
36
+ return { type: "code", code: "", language: "javascript" };
37
+ case "callout":
38
+ return { type: "callout", content: "", variant: "info" };
39
+ case "audio":
40
+ return { type: "audio", src: "" };
41
+ case "embed":
42
+ return { type: "embed", src: "" };
43
+ case "table":
44
+ return { type: "table", headers: ["Column 1", "Column 2"], rows: [["", ""]] };
45
+ case "file":
46
+ return { type: "file", files: [] };
47
+ case "divider":
48
+ return { type: "divider" };
49
+ case "question":
50
+ return {
51
+ type: "question",
52
+ question: {
53
+ uid: genId(),
54
+ type: "multiple_choice",
55
+ content: "",
56
+ answers: [],
57
+ },
58
+ };
59
+ case "flashcards":
60
+ return { type: "flashcards", cards: [] };
61
+ case "custom":
62
+ return { type: "custom", render: null };
63
+ default:
64
+ return { type: "richtext", html: "" };
65
+ }
66
+ }
67
+
68
+ /**
69
+ * ContentAuthoringStudio — A premium block-based content editor for creating
70
+ * lessons. Produces `LessonBlock[]` compatible with `LessonPage`.
71
+ *
72
+ * Supports adding, editing, removing, reordering, and previewing content blocks
73
+ * including rich text, headings, images, videos, code, callouts, and more.
74
+ */
75
+ function ContentAuthoringStudioBase({
76
+ title: titleProp = "",
77
+ blocks: blocksProp,
78
+ onBlocksChange,
79
+ onTitleChange,
80
+ onSave,
81
+ showPreviewToggle = true,
82
+ allowedBlockTypes,
83
+ readOnly = false,
84
+ isLoading,
85
+ error,
86
+ onRetry,
87
+ className,
88
+ style,
89
+ }: ContentAuthoringStudioProps) {
90
+ const [title, setTitle] = useState(titleProp);
91
+ const [items, setItems] = useState<AuthoringBlock[]>(() =>
92
+ toAuthoring(blocksProp ?? []),
93
+ );
94
+ const [mode, setMode] = useState<"edit" | "preview">(readOnly ? "preview" : "edit");
95
+
96
+ const rawBlocks = useMemo(() => items.map((i) => i.block), [items]);
97
+
98
+ const emitChange = useCallback(
99
+ (nextItems: AuthoringBlock[]) => {
100
+ setItems(nextItems);
101
+ onBlocksChange?.(nextItems.map((i) => i.block));
102
+ },
103
+ [onBlocksChange],
104
+ );
105
+
106
+ /* ── Block operations ──────────────────────────── */
107
+
108
+ const itemsRef = useRef(items);
109
+ itemsRef.current = items;
110
+
111
+ function handleAddBlock(type: LessonBlock["type"], atIndex: number) {
112
+ const newItem: AuthoringBlock = { id: genId(), block: createEmptyBlock(type) };
113
+ const next = [...items];
114
+ next.splice(atIndex, 0, newItem);
115
+ emitChange(next);
116
+ }
117
+
118
+ const handleUpdateBlock = useCallback((id: string, block: LessonBlock) => {
119
+ emitChange(itemsRef.current.map((i) => (i.id === id ? { ...i, block } : i)));
120
+ }, [emitChange]);
121
+
122
+ const handleRemoveBlock = useCallback((id: string) => {
123
+ emitChange(itemsRef.current.filter((i) => i.id !== id));
124
+ }, [emitChange]);
125
+
126
+ const handleDuplicateBlock = useCallback((id: string) => {
127
+ const current = itemsRef.current;
128
+ const index = current.findIndex((i) => i.id === id);
129
+ if (index === -1) return;
130
+ const copy: AuthoringBlock = {
131
+ id: genId(),
132
+ block: structuredClone(current[index].block),
133
+ };
134
+ const next = [...current];
135
+ next.splice(index + 1, 0, copy);
136
+ emitChange(next);
137
+ }, [emitChange]);
138
+
139
+ const handleToggleCollapse = useCallback((id: string) => {
140
+ setItems((prev) =>
141
+ prev.map((i) =>
142
+ i.id === id ? { ...i, collapsed: !i.collapsed } : i,
143
+ ),
144
+ );
145
+ }, []);
146
+
147
+ function handleTitleChange(value: string) {
148
+ setTitle(value);
149
+ onTitleChange?.(value);
150
+ }
151
+
152
+ function handleSave() {
153
+ onSave?.({ title, blocks: rawBlocks });
154
+ }
155
+
156
+ /* ── Drag-and-drop ─────────────────────────────── */
157
+
158
+ const { getDragProps, dragIndex, dragOverIndex } = useDragReorder({
159
+ items,
160
+ onReorder: emitChange,
161
+ disabled: readOnly || mode === "preview",
162
+ });
163
+
164
+ /* ── Render ────────────────────────────────────── */
165
+
166
+ const isEditing = mode === "edit" && !readOnly;
167
+
168
+ return (
169
+ <SectionShell
170
+ isLoading={isLoading}
171
+ error={error}
172
+ onRetry={onRetry}
173
+ className={className}
174
+ style={style}
175
+ skeleton={
176
+ <>
177
+ <Skeleton className="h-10 w-64" />
178
+ <Skeleton className="h-6 w-32" />
179
+ <Skeleton className="h-32 w-full" />
180
+ <Skeleton className="h-24 w-full" />
181
+ <Skeleton className="h-32 w-full" />
182
+ </>
183
+ }
184
+ >
185
+ <div>
186
+ {/* Toolbar */}
187
+ <div className="flex items-center gap-2 mb-4">
188
+ {isEditing ? (
189
+ <Input
190
+ value={title}
191
+ onChange={(e) => handleTitleChange(e.target.value)}
192
+ placeholder="Lesson title..."
193
+ className="text-lg font-semibold flex-1 h-10"
194
+ />
195
+ ) : (
196
+ <h2 className="text-xl font-bold flex-1 text-foreground">{title || "Untitled Lesson"}</h2>
197
+ )}
198
+
199
+ {showPreviewToggle && !readOnly && (
200
+ <Button
201
+ variant="outline"
202
+ size="sm"
203
+ onClick={() => setMode(mode === "edit" ? "preview" : "edit")}
204
+ className="gap-1.5"
205
+ >
206
+ {mode === "edit" ? (
207
+ <>
208
+ <Eye className="size-3.5" /> Preview
209
+ </>
210
+ ) : (
211
+ <>
212
+ <Pencil className="size-3.5" /> Edit
213
+ </>
214
+ )}
215
+ </Button>
216
+ )}
217
+
218
+ {onSave && !readOnly && (
219
+ <Button size="sm" onClick={handleSave} className="gap-1.5">
220
+ <Save className="size-3.5" /> Save
221
+ </Button>
222
+ )}
223
+ </div>
224
+
225
+ {/* Content */}
226
+ {isEditing ? (
227
+ <div className="space-y-1">
228
+ {/* Top insertion point */}
229
+ <div className="flex justify-center py-1">
230
+ <BlockTypePicker
231
+ onSelect={(type) => handleAddBlock(type, 0)}
232
+ allowedTypes={allowedBlockTypes}
233
+ />
234
+ </div>
235
+
236
+ {items.length === 0 && (
237
+ <EmptyState
238
+ title="No content blocks"
239
+ description="Click 'Add block' above to start building your lesson."
240
+ />
241
+ )}
242
+
243
+ {items.map((item, index) => (
244
+ <div key={item.id}>
245
+ <BlockEditorItem
246
+ item={item}
247
+ onChange={handleUpdateBlock}
248
+ onRemove={handleRemoveBlock}
249
+ onDuplicate={handleDuplicateBlock}
250
+ onToggleCollapse={handleToggleCollapse}
251
+ dragProps={getDragProps(index)}
252
+ isDragging={dragIndex === index}
253
+ isDragOver={dragOverIndex === index}
254
+ />
255
+
256
+ {/* Insertion point after each block */}
257
+ <div className="flex justify-center py-1">
258
+ <BlockTypePicker
259
+ onSelect={(type) => handleAddBlock(type, index + 1)}
260
+ allowedTypes={allowedBlockTypes}
261
+ />
262
+ </div>
263
+ </div>
264
+ ))}
265
+ </div>
266
+ ) : (
267
+ /* Preview mode */
268
+ <div className="flex flex-col gap-3">
269
+ {items.length > 0 ? (
270
+ items.map((item) => (
271
+ <ContentBlock key={item.id} block={item.block} readOnly />
272
+ ))
273
+ ) : (
274
+ <EmptyState
275
+ title="No content yet"
276
+ description="Switch to edit mode to add content blocks."
277
+ />
278
+ )}
279
+ </div>
280
+ )}
281
+ </div>
282
+ </SectionShell>
283
+ );
284
+ }
285
+
286
+ export const ContentAuthoringStudio = withProGate(
287
+ ContentAuthoringStudioBase,
288
+ "ContentAuthoringStudio",
289
+ );