@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,487 @@
1
+ import { memo, useCallback } from "react";
2
+ import {
3
+ GripVertical,
4
+ ChevronDown,
5
+ ChevronRight,
6
+ Trash2,
7
+ Copy,
8
+ } from "lucide-react";
9
+ import { Button } from "../../ui/button";
10
+ import { Input } from "../../ui/input";
11
+ import { Badge } from "../../ui/badge";
12
+ import { RichTextEditor } from "../../ui/rich-text-editor";
13
+ import { Separator } from "../../ui/separator";
14
+ import { cn } from "../../lib/utils";
15
+ import type { LessonBlock } from "../../content/types";
16
+ import type { AuthoringBlock } from "./types";
17
+ import { getBlockIcon, getBlockLabel } from "./block-type-picker";
18
+
19
+ interface BlockEditorItemProps {
20
+ item: AuthoringBlock;
21
+ onChange: (id: string, block: LessonBlock) => void;
22
+ onRemove: (id: string) => void;
23
+ onDuplicate: (id: string) => void;
24
+ onToggleCollapse: (id: string) => void;
25
+ dragProps: Record<string, unknown>;
26
+ isDragging: boolean;
27
+ isDragOver: boolean;
28
+ }
29
+
30
+ export const BlockEditorItem = memo(function BlockEditorItem({
31
+ item,
32
+ onChange,
33
+ onRemove,
34
+ onDuplicate,
35
+ onToggleCollapse,
36
+ dragProps,
37
+ isDragging,
38
+ isDragOver,
39
+ }: BlockEditorItemProps) {
40
+ const { id, block, collapsed } = item;
41
+ const Icon = getBlockIcon(block.type);
42
+ const label = getBlockLabel(block.type);
43
+
44
+ const handleChange = useCallback(
45
+ (updated: LessonBlock) => onChange(id, updated),
46
+ [id, onChange],
47
+ );
48
+ const handleRemove = useCallback(() => onRemove(id), [id, onRemove]);
49
+ const handleDuplicate = useCallback(() => onDuplicate(id), [id, onDuplicate]);
50
+ const handleToggle = useCallback(() => onToggleCollapse(id), [id, onToggleCollapse]);
51
+
52
+ return (
53
+ <div
54
+ {...dragProps}
55
+ className={cn(
56
+ "group rounded-lg border border-border bg-background transition-all",
57
+ isDragging && "opacity-50",
58
+ isDragOver && "border-primary shadow-sm",
59
+ )}
60
+ >
61
+ {/* Header bar */}
62
+ <div className="flex items-center gap-1 px-2 py-1.5 border-b border-border/50">
63
+ <span
64
+ className="cursor-grab text-muted-foreground/50 hover:text-muted-foreground"
65
+ aria-label="Drag to reorder"
66
+ >
67
+ <GripVertical className="size-4" />
68
+ </span>
69
+
70
+ <button
71
+ type="button"
72
+ onClick={handleToggle}
73
+ className="text-muted-foreground hover:text-foreground p-0.5"
74
+ >
75
+ {collapsed ? (
76
+ <ChevronRight className="size-3.5" />
77
+ ) : (
78
+ <ChevronDown className="size-3.5" />
79
+ )}
80
+ </button>
81
+
82
+ <Badge variant="secondary" className="text-[10px] gap-1 px-1.5 py-0">
83
+ <Icon className="size-3" />
84
+ {label}
85
+ </Badge>
86
+
87
+ <div className="flex-1" />
88
+
89
+ <Button
90
+ variant="ghost"
91
+ size="sm"
92
+ onClick={handleDuplicate}
93
+ className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 text-muted-foreground"
94
+ >
95
+ <Copy className="size-3" />
96
+ </Button>
97
+ <Button
98
+ variant="ghost"
99
+ size="sm"
100
+ onClick={handleRemove}
101
+ className="h-6 w-6 p-0 opacity-0 group-hover:opacity-100 text-destructive"
102
+ >
103
+ <Trash2 className="size-3" />
104
+ </Button>
105
+ </div>
106
+
107
+ {/* Editor body */}
108
+ {!collapsed && (
109
+ <div className="p-3">
110
+ <BlockEditor block={block} onChange={handleChange} />
111
+ </div>
112
+ )}
113
+ </div>
114
+ );
115
+ });
116
+
117
+ /* ── Per-type editors ──────────────────────────────────── */
118
+
119
+ function BlockEditor({
120
+ block,
121
+ onChange,
122
+ }: {
123
+ block: LessonBlock;
124
+ onChange: (block: LessonBlock) => void;
125
+ }) {
126
+ switch (block.type) {
127
+ case "richtext":
128
+ return (
129
+ <RichTextEditor
130
+ value={block.html}
131
+ onChange={(html) => onChange({ ...block, html })}
132
+ placeholder="Write your content..."
133
+ />
134
+ );
135
+
136
+ case "heading":
137
+ return (
138
+ <div className="flex gap-2">
139
+ <select
140
+ value={block.level ?? 2}
141
+ onChange={(e) =>
142
+ onChange({ ...block, level: Number(e.target.value) as 1 | 2 | 3 })
143
+ }
144
+ className="h-9 rounded-md border border-input bg-transparent px-2 text-sm"
145
+ >
146
+ <option value={1}>H1</option>
147
+ <option value={2}>H2</option>
148
+ <option value={3}>H3</option>
149
+ </select>
150
+ <Input
151
+ value={block.text}
152
+ onChange={(e) => onChange({ ...block, text: e.target.value })}
153
+ placeholder="Heading text..."
154
+ className="flex-1"
155
+ />
156
+ </div>
157
+ );
158
+
159
+ case "image":
160
+ return (
161
+ <div className="space-y-2">
162
+ <Input
163
+ value={block.src}
164
+ onChange={(e) => onChange({ ...block, src: e.target.value })}
165
+ placeholder="Image URL..."
166
+ />
167
+ <div className="flex gap-2">
168
+ <Input
169
+ value={block.alt ?? ""}
170
+ onChange={(e) => onChange({ ...block, alt: e.target.value })}
171
+ placeholder="Alt text..."
172
+ className="flex-1"
173
+ />
174
+ <Input
175
+ value={block.caption ?? ""}
176
+ onChange={(e) => onChange({ ...block, caption: e.target.value })}
177
+ placeholder="Caption..."
178
+ className="flex-1"
179
+ />
180
+ </div>
181
+ {block.src && (
182
+ <img
183
+ src={block.src}
184
+ alt={block.alt ?? ""}
185
+ className="max-h-40 rounded-md object-cover"
186
+ />
187
+ )}
188
+ </div>
189
+ );
190
+
191
+ case "video":
192
+ return (
193
+ <div className="space-y-2">
194
+ <Input
195
+ value={block.video.src ?? ""}
196
+ onChange={(e) =>
197
+ onChange({ ...block, video: { ...block.video, src: e.target.value } })
198
+ }
199
+ placeholder="Video URL..."
200
+ />
201
+ <Input
202
+ value={block.video.title ?? ""}
203
+ onChange={(e) =>
204
+ onChange({ ...block, video: { ...block.video, title: e.target.value } })
205
+ }
206
+ placeholder="Video title..."
207
+ />
208
+ </div>
209
+ );
210
+
211
+ case "code":
212
+ return (
213
+ <div className="space-y-2">
214
+ <div className="flex gap-2">
215
+ <Input
216
+ value={block.language ?? ""}
217
+ onChange={(e) => onChange({ ...block, language: e.target.value })}
218
+ placeholder="Language (e.g. javascript)"
219
+ className="w-40"
220
+ />
221
+ <Input
222
+ value={block.filename ?? ""}
223
+ onChange={(e) => onChange({ ...block, filename: e.target.value })}
224
+ placeholder="Filename (optional)"
225
+ className="flex-1"
226
+ />
227
+ </div>
228
+ <textarea
229
+ value={block.code}
230
+ onChange={(e) => onChange({ ...block, code: e.target.value })}
231
+ placeholder="Paste your code..."
232
+ className="w-full min-h-24 rounded-md border border-input bg-muted p-3 font-mono text-sm resize-y focus:outline-none focus:ring-2 focus:ring-ring"
233
+ spellCheck={false}
234
+ />
235
+ </div>
236
+ );
237
+
238
+ case "callout":
239
+ return (
240
+ <div className="space-y-2">
241
+ <select
242
+ value={block.variant ?? "info"}
243
+ onChange={(e) =>
244
+ onChange({
245
+ ...block,
246
+ variant: e.target.value as "info" | "warning" | "tip",
247
+ })
248
+ }
249
+ className="h-9 rounded-md border border-input bg-transparent px-2 text-sm"
250
+ >
251
+ <option value="info">Info</option>
252
+ <option value="warning">Warning</option>
253
+ <option value="tip">Tip</option>
254
+ </select>
255
+ <Input
256
+ value={block.content}
257
+ onChange={(e) => onChange({ ...block, content: e.target.value })}
258
+ placeholder="Callout text..."
259
+ />
260
+ </div>
261
+ );
262
+
263
+ case "audio":
264
+ return (
265
+ <div className="space-y-2">
266
+ <Input
267
+ value={block.src}
268
+ onChange={(e) => onChange({ ...block, src: e.target.value })}
269
+ placeholder="Audio URL..."
270
+ />
271
+ <Input
272
+ value={block.title ?? ""}
273
+ onChange={(e) => onChange({ ...block, title: e.target.value })}
274
+ placeholder="Title (optional)"
275
+ />
276
+ </div>
277
+ );
278
+
279
+ case "embed":
280
+ return (
281
+ <div className="space-y-2">
282
+ <Input
283
+ value={block.src}
284
+ onChange={(e) => onChange({ ...block, src: e.target.value })}
285
+ placeholder="Embed URL..."
286
+ />
287
+ <div className="flex gap-2">
288
+ <Input
289
+ value={block.title ?? ""}
290
+ onChange={(e) => onChange({ ...block, title: e.target.value })}
291
+ placeholder="Title (optional)"
292
+ className="flex-1"
293
+ />
294
+ <select
295
+ value={block.aspectRatio ?? "16/9"}
296
+ onChange={(e) =>
297
+ onChange({
298
+ ...block,
299
+ aspectRatio: e.target.value as "16/9" | "4/3" | "1/1",
300
+ })
301
+ }
302
+ className="h-9 rounded-md border border-input bg-transparent px-2 text-sm"
303
+ >
304
+ <option value="16/9">16:9</option>
305
+ <option value="4/3">4:3</option>
306
+ <option value="1/1">1:1</option>
307
+ </select>
308
+ </div>
309
+ </div>
310
+ );
311
+
312
+ case "table":
313
+ return <TableEditor block={block} onChange={onChange} />;
314
+
315
+ case "file":
316
+ return (
317
+ <p className="text-sm text-muted-foreground">
318
+ File attachments are managed via the <code className="text-xs">files</code> prop.
319
+ {block.files.length > 0 && (
320
+ <span className="ml-1">
321
+ ({block.files.length} file{block.files.length !== 1 && "s"} attached)
322
+ </span>
323
+ )}
324
+ </p>
325
+ );
326
+
327
+ case "divider":
328
+ return <Separator />;
329
+
330
+ case "question":
331
+ case "flashcards":
332
+ return (
333
+ <p className="text-sm text-muted-foreground italic">
334
+ Interactive {block.type} blocks are configured via props.
335
+ </p>
336
+ );
337
+
338
+ case "custom":
339
+ return (
340
+ <p className="text-sm text-muted-foreground italic">
341
+ Custom blocks are not editable in the authoring studio.
342
+ </p>
343
+ );
344
+
345
+ default:
346
+ return null;
347
+ }
348
+ }
349
+
350
+ /* ── Table editor ──────────────────────────────────────── */
351
+
352
+ function TableEditor({
353
+ block,
354
+ onChange,
355
+ }: {
356
+ block: Extract<LessonBlock, { type: "table" }>;
357
+ onChange: (block: LessonBlock) => void;
358
+ }) {
359
+ const { headers, rows, caption } = block;
360
+
361
+ function updateHeader(colIndex: number, value: string) {
362
+ const next = [...headers];
363
+ next[colIndex] = value;
364
+ onChange({ ...block, headers: next });
365
+ }
366
+
367
+ function updateCell(rowIndex: number, colIndex: number, value: string) {
368
+ const next = rows.map((row) => [...row]);
369
+ next[rowIndex][colIndex] = value;
370
+ onChange({ ...block, rows: next });
371
+ }
372
+
373
+ function addColumn() {
374
+ onChange({
375
+ ...block,
376
+ headers: [...headers, ""],
377
+ rows: rows.map((row) => [...row, ""]),
378
+ });
379
+ }
380
+
381
+ function removeColumn(colIndex: number) {
382
+ if (headers.length <= 1) return;
383
+ onChange({
384
+ ...block,
385
+ headers: headers.filter((_, i) => i !== colIndex),
386
+ rows: rows.map((row) => row.filter((_, i) => i !== colIndex)),
387
+ });
388
+ }
389
+
390
+ function addRow() {
391
+ onChange({
392
+ ...block,
393
+ rows: [...rows, headers.map(() => "")],
394
+ });
395
+ }
396
+
397
+ function removeRow(rowIndex: number) {
398
+ onChange({
399
+ ...block,
400
+ rows: rows.filter((_, i) => i !== rowIndex),
401
+ });
402
+ }
403
+
404
+ return (
405
+ <div className="space-y-2">
406
+ <Input
407
+ value={caption ?? ""}
408
+ onChange={(e) => onChange({ ...block, caption: e.target.value })}
409
+ placeholder="Table caption (optional)"
410
+ className="text-sm"
411
+ />
412
+ <div className="overflow-x-auto">
413
+ <table className="w-full text-sm">
414
+ <thead>
415
+ <tr>
416
+ {headers.map((header, i) => (
417
+ <th key={i} className="p-1">
418
+ <div className="flex gap-0.5">
419
+ <Input
420
+ value={header}
421
+ onChange={(e) => updateHeader(i, e.target.value)}
422
+ placeholder={`Col ${i + 1}`}
423
+ className="h-7 text-xs font-semibold"
424
+ />
425
+ {headers.length > 1 && (
426
+ <Button
427
+ variant="ghost"
428
+ size="sm"
429
+ onClick={() => removeColumn(i)}
430
+ className="h-7 w-7 p-0 text-destructive shrink-0"
431
+ >
432
+ <Trash2 className="size-3" />
433
+ </Button>
434
+ )}
435
+ </div>
436
+ </th>
437
+ ))}
438
+ <th className="p-1 w-8">
439
+ <Button
440
+ variant="ghost"
441
+ size="sm"
442
+ onClick={addColumn}
443
+ className="h-7 w-7 p-0 text-muted-foreground"
444
+ >
445
+ +
446
+ </Button>
447
+ </th>
448
+ </tr>
449
+ </thead>
450
+ <tbody>
451
+ {rows.map((row, ri) => (
452
+ <tr key={ri}>
453
+ {row.map((cell, ci) => (
454
+ <td key={ci} className="p-1">
455
+ <Input
456
+ value={cell}
457
+ onChange={(e) => updateCell(ri, ci, e.target.value)}
458
+ className="h-7 text-xs"
459
+ />
460
+ </td>
461
+ ))}
462
+ <td className="p-1">
463
+ <Button
464
+ variant="ghost"
465
+ size="sm"
466
+ onClick={() => removeRow(ri)}
467
+ className="h-7 w-7 p-0 text-destructive"
468
+ >
469
+ <Trash2 className="size-3" />
470
+ </Button>
471
+ </td>
472
+ </tr>
473
+ ))}
474
+ </tbody>
475
+ </table>
476
+ </div>
477
+ <Button
478
+ variant="ghost"
479
+ size="sm"
480
+ onClick={addRow}
481
+ className="text-xs text-muted-foreground"
482
+ >
483
+ + Add row
484
+ </Button>
485
+ </div>
486
+ );
487
+ }
@@ -0,0 +1,123 @@
1
+ import { useState, useRef, useEffect } from "react";
2
+ import {
3
+ Type,
4
+ Heading,
5
+ Image,
6
+ Video,
7
+ Code,
8
+ MessageSquare,
9
+ Music,
10
+ Globe,
11
+ Table,
12
+ Paperclip,
13
+ Minus,
14
+ Plus,
15
+ } from "lucide-react";
16
+ import { Button } from "../../ui/button";
17
+ import { cn } from "../../lib/utils";
18
+ import type { LessonBlock } from "../../content/types";
19
+ import type { BlockTypeOption } from "./types";
20
+
21
+ const ALL_BLOCK_TYPES: BlockTypeOption[] = [
22
+ { type: "richtext", label: "Rich Text", description: "Formatted text content" },
23
+ { type: "heading", label: "Heading", description: "Section heading" },
24
+ { type: "image", label: "Image", description: "Image with caption" },
25
+ { type: "video", label: "Video", description: "Video player" },
26
+ { type: "code", label: "Code", description: "Code snippet" },
27
+ { type: "callout", label: "Callout", description: "Info, warning, or tip" },
28
+ { type: "audio", label: "Audio", description: "Audio player" },
29
+ { type: "embed", label: "Embed", description: "External embed" },
30
+ { type: "table", label: "Table", description: "Data table" },
31
+ { type: "file", label: "Files", description: "File attachments" },
32
+ { type: "divider", label: "Divider", description: "Horizontal separator" },
33
+ ];
34
+
35
+ const BLOCK_ICONS: Record<string, React.ComponentType<{ className?: string }>> = {
36
+ richtext: Type,
37
+ heading: Heading,
38
+ image: Image,
39
+ video: Video,
40
+ code: Code,
41
+ callout: MessageSquare,
42
+ audio: Music,
43
+ embed: Globe,
44
+ table: Table,
45
+ file: Paperclip,
46
+ divider: Minus,
47
+ };
48
+
49
+ export function getBlockIcon(type: string) {
50
+ return BLOCK_ICONS[type] ?? Type;
51
+ }
52
+
53
+ export function getBlockLabel(type: string) {
54
+ return ALL_BLOCK_TYPES.find((b) => b.type === type)?.label ?? type;
55
+ }
56
+
57
+ interface BlockTypePickerProps {
58
+ onSelect: (type: LessonBlock["type"]) => void;
59
+ allowedTypes?: LessonBlock["type"][];
60
+ className?: string;
61
+ }
62
+
63
+ export function BlockTypePicker({
64
+ onSelect,
65
+ allowedTypes,
66
+ className,
67
+ }: BlockTypePickerProps) {
68
+ const [open, setOpen] = useState(false);
69
+ const ref = useRef<HTMLDivElement>(null);
70
+
71
+ const types = allowedTypes
72
+ ? ALL_BLOCK_TYPES.filter((t) => allowedTypes.includes(t.type))
73
+ : ALL_BLOCK_TYPES;
74
+
75
+ useEffect(() => {
76
+ if (!open) return;
77
+ function handleClick(e: MouseEvent) {
78
+ if (ref.current && !ref.current.contains(e.target as Node)) {
79
+ setOpen(false);
80
+ }
81
+ }
82
+ document.addEventListener("mousedown", handleClick);
83
+ return () => document.removeEventListener("mousedown", handleClick);
84
+ }, [open]);
85
+
86
+ return (
87
+ <div className={cn("relative", className)} ref={ref}>
88
+ <Button
89
+ variant="ghost"
90
+ size="sm"
91
+ onClick={() => setOpen(!open)}
92
+ className="text-muted-foreground hover:text-foreground gap-1 h-7 text-xs"
93
+ >
94
+ <Plus className="size-3.5" />
95
+ Add block
96
+ </Button>
97
+
98
+ {open && (
99
+ <div className="absolute left-1/2 -translate-x-1/2 top-full mt-1 z-50 w-72 rounded-lg border border-border bg-background shadow-lg p-2">
100
+ <div className="grid grid-cols-3 gap-1">
101
+ {types.map(({ type, label }) => {
102
+ const Icon = BLOCK_ICONS[type] ?? Type;
103
+ return (
104
+ <button
105
+ key={type}
106
+ type="button"
107
+ onClick={() => {
108
+ onSelect(type);
109
+ setOpen(false);
110
+ }}
111
+ className="flex flex-col items-center gap-1 rounded-md p-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors"
112
+ >
113
+ <Icon className="size-4" />
114
+ <span>{label}</span>
115
+ </button>
116
+ );
117
+ })}
118
+ </div>
119
+ </div>
120
+ )}
121
+ </div>
122
+ );
123
+ }
@@ -0,0 +1,67 @@
1
+ import type { LessonBlock } from "../../content/types";
2
+
3
+ /**
4
+ * A single authoring block wrapping a LessonBlock with editor metadata.
5
+ */
6
+ export interface AuthoringBlock {
7
+ /** Unique identifier for keying and drag operations */
8
+ id: string;
9
+ /** The underlying content block data */
10
+ block: LessonBlock;
11
+ /** Whether this block is collapsed in the editor UI */
12
+ collapsed?: boolean;
13
+ }
14
+
15
+ /**
16
+ * Metadata for a block type option in the picker.
17
+ */
18
+ export interface BlockTypeOption {
19
+ type: LessonBlock["type"];
20
+ label: string;
21
+ description: string;
22
+ }
23
+
24
+ /**
25
+ * ContentAuthoringStudio is a premium block-based content editor
26
+ * for creating lessons. It produces the same `LessonBlock[]` data
27
+ * model that `LessonPage` renders, providing a WYSIWYG authoring
28
+ * experience with drag-and-drop reordering, per-block editors,
29
+ * and live preview.
30
+ *
31
+ * @example
32
+ * <ContentAuthoringStudio
33
+ * title="React Hooks Deep Dive"
34
+ * blocks={lessonBlocks}
35
+ * onBlocksChange={setLessonBlocks}
36
+ * onTitleChange={setTitle}
37
+ * onSave={({ title, blocks }) => api.saveLesson(title, blocks)}
38
+ * />
39
+ */
40
+ export interface ContentAuthoringStudioProps {
41
+ /** Lesson title (editable in edit mode) */
42
+ title?: string;
43
+ /** Initial content blocks */
44
+ blocks?: LessonBlock[];
45
+ /** Called when blocks change (add, edit, remove, reorder) */
46
+ onBlocksChange?: (blocks: LessonBlock[]) => void;
47
+ /** Called when the title changes */
48
+ onTitleChange?: (title: string) => void;
49
+ /** Called when the user clicks Save */
50
+ onSave?: (data: { title: string; blocks: LessonBlock[] }) => void;
51
+ /** Show the edit/preview toggle. @default true */
52
+ showPreviewToggle?: boolean;
53
+ /** Restrict available block types. Defaults to all types. */
54
+ allowedBlockTypes?: LessonBlock["type"][];
55
+ /** When true, shows preview only (no editing controls). @default false */
56
+ readOnly?: boolean;
57
+ /** Render skeleton placeholders instead of content */
58
+ isLoading?: boolean;
59
+ /** Error message — renders an error state with optional retry */
60
+ error?: string | null;
61
+ /** Called when the user clicks retry in the error state */
62
+ onRetry?: () => void;
63
+ /** CSS class name for the root element */
64
+ className?: string;
65
+ /** Inline styles for the root element */
66
+ style?: React.CSSProperties;
67
+ }