@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.
- package/dist/StudentProfile-BPsZBaJj.cjs +1 -0
- package/dist/{StudentProfile-DeMxdrL3.js → StudentProfile-Cw2p-RZn.js} +577 -579
- package/dist/index.cjs +1 -1
- package/dist/index.js +172 -166
- package/dist/license/index.d.ts +2 -2
- package/dist/license/tiers.d.ts +3 -0
- package/dist/modules.cjs +1 -1
- package/dist/modules.js +111 -110
- package/dist/sections/AdaptiveLearningPath/AdaptiveLearningPath.d.ts +5 -0
- package/dist/sections/AdaptiveLearningPath/path-connector.d.ts +8 -0
- package/dist/sections/AdaptiveLearningPath/path-milestone-marker.d.ts +7 -0
- package/dist/sections/AdaptiveLearningPath/path-node-card.d.ts +10 -0
- package/dist/sections/AdaptiveLearningPath/path-skill-bar.d.ts +8 -0
- package/dist/sections/AdaptiveLearningPath/types.d.ts +136 -0
- package/dist/sections/ContentAuthoringStudio/ContentAuthoringStudio.d.ts +5 -0
- package/dist/sections/ContentAuthoringStudio/block-editor-item.d.ts +14 -0
- package/dist/sections/ContentAuthoringStudio/block-type-picker.d.ts +12 -0
- package/dist/sections/ContentAuthoringStudio/types.d.ts +67 -0
- package/dist/sections/index.d.ts +4 -0
- package/dist/sections.cjs +1 -1
- package/dist/sections.js +1325 -232
- package/dist/withProGate-BJdu1T9Y.cjs +2 -0
- package/dist/withProGate-BvFc7Jwy.js +4975 -0
- package/package.json +24 -7
- package/src/license/index.ts +2 -2
- package/src/license/tiers.ts +12 -2
- package/src/modules/CoursePlayer/CoursePlayer.tsx +3 -1
- package/src/progress/stat-card.tsx +10 -5
- package/src/sections/AdaptiveLearningPath/AdaptiveLearningPath.tsx +251 -0
- package/src/sections/AdaptiveLearningPath/path-connector.tsx +27 -0
- package/src/sections/AdaptiveLearningPath/path-milestone-marker.tsx +50 -0
- package/src/sections/AdaptiveLearningPath/path-node-card.tsx +166 -0
- package/src/sections/AdaptiveLearningPath/path-skill-bar.tsx +49 -0
- package/src/sections/AdaptiveLearningPath/types.ts +159 -0
- package/src/sections/ContentAuthoringStudio/ContentAuthoringStudio.tsx +289 -0
- package/src/sections/ContentAuthoringStudio/block-editor-item.tsx +487 -0
- package/src/sections/ContentAuthoringStudio/block-type-picker.tsx +123 -0
- package/src/sections/ContentAuthoringStudio/types.ts +67 -0
- package/src/sections/ForumBoard/ForumBoard.tsx +8 -6
- package/src/sections/LessonPage/LessonPage.tsx +4 -7
- package/src/sections/index.ts +18 -0
- package/src/video/video-player.tsx +14 -5
- package/dist/StudentProfile-BVfZMbnV.cjs +0 -1
- package/dist/tabs-BsfVo2Bl.cjs +0 -173
- package/dist/tabs-BuY1iNJE.js +0 -22305
- package/dist/withProGate-BWqcKdPM.js +0 -137
- 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
|
+
);
|