@hed-hog/lms 0.0.314 → 0.0.315

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 (68) hide show
  1. package/dist/class-group/class-group.controller.d.ts +2 -2
  2. package/dist/class-group/class-group.service.d.ts +2 -2
  3. package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
  4. package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
  5. package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
  6. package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
  7. package/dist/enterprise/enterprise.controller.d.ts +3 -0
  8. package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
  9. package/dist/enterprise/enterprise.controller.js +14 -0
  10. package/dist/enterprise/enterprise.controller.js.map +1 -1
  11. package/dist/enterprise/enterprise.service.d.ts +3 -0
  12. package/dist/enterprise/enterprise.service.d.ts.map +1 -1
  13. package/dist/enterprise/enterprise.service.js +128 -1
  14. package/dist/enterprise/enterprise.service.js.map +1 -1
  15. package/dist/instructor/instructor.controller.d.ts +23 -0
  16. package/dist/instructor/instructor.controller.d.ts.map +1 -1
  17. package/dist/instructor/instructor.controller.js +41 -0
  18. package/dist/instructor/instructor.controller.js.map +1 -1
  19. package/dist/instructor/instructor.service.d.ts +25 -0
  20. package/dist/instructor/instructor.service.d.ts.map +1 -1
  21. package/dist/instructor/instructor.service.js +126 -8
  22. package/dist/instructor/instructor.service.js.map +1 -1
  23. package/hedhog/data/menu.yaml +23 -7
  24. package/hedhog/data/role.yaml +9 -1
  25. package/hedhog/data/route.yaml +54 -0
  26. package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
  27. package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
  28. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
  29. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
  30. package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
  31. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
  32. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
  33. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
  34. package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
  35. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
  36. package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
  37. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
  38. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
  39. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
  40. package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
  41. package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
  42. package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
  43. package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
  44. package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
  45. package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
  46. package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
  47. package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
  48. package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
  49. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
  50. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
  51. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
  52. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
  53. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
  54. package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
  55. package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
  56. package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
  57. package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
  58. package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
  59. package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
  60. package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
  61. package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
  62. package/hedhog/table/enterprise_user.yaml +1 -1
  63. package/package.json +8 -8
  64. package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
  65. package/src/enterprise/enterprise.controller.ts +9 -1
  66. package/src/enterprise/enterprise.service.ts +147 -4
  67. package/src/instructor/instructor.controller.ts +36 -9
  68. package/src/instructor/instructor.service.ts +140 -10
@@ -1,264 +1,264 @@
1
- 'use client';
2
-
3
- import { Button } from '@/components/ui/button';
4
- import { Separator } from '@/components/ui/separator';
5
- import { cn } from '@/lib/utils';
6
- import { Copy, FolderOpen, Loader2, Trash2, X } from 'lucide-react';
7
- import { toast } from 'sonner';
8
- import {
9
- useBulkDeleteMutation,
10
- useDuplicateLessonMutation,
11
- useDuplicateSessionMutation,
12
- useMoveLessonsMutation,
13
- } from '../_data/use-course-structure-mutations';
14
- import { useStructureStore } from './store';
15
-
16
- /**
17
- * Compact action bar shown when 2+ items are selected.
18
- * Fixed below the search toolbar inside CourseTreePanel.
19
- */
20
- export function MultiSelectBar() {
21
- const selectedIds = useStructureStore((s) => s.selectedIds);
22
- const lessons = useStructureStore((s) => s.lessons);
23
- const sessions = useStructureStore((s) => s.sessions);
24
- const clearSelection = useStructureStore((s) => s.clearSelection);
25
- const copyItems = useStructureStore((s) => s.copyItems);
26
- const showConfirm = useStructureStore((s) => s.showConfirm);
27
- const showSessionPicker = useStructureStore((s) => s.showSessionPicker);
28
-
29
- const bulkDelete = useBulkDeleteMutation();
30
- const duplicateLessonMutation = useDuplicateLessonMutation();
31
- const duplicateSessionMutation = useDuplicateSessionMutation();
32
- const moveLessonsMutation = useMoveLessonsMutation();
33
-
34
- const isDuplicating =
35
- duplicateLessonMutation.isPending || duplicateSessionMutation.isPending;
36
- const isMoving = moveLessonsMutation.isPending;
37
-
38
- const count = selectedIds.size;
39
- if (count <= 1) return null;
40
-
41
- const selectedLessonIds = lessons
42
- .filter((l) => selectedIds.has(`lesson:${l.id}`))
43
- .map((l) => l.id);
44
- const selectedSessionIds = sessions
45
- .filter((ss) => selectedIds.has(`session:${ss.id}`))
46
- .map((ss) => ss.id);
47
-
48
- const allAreLessons = selectedLessonIds.length === count;
49
- const allAreSessions = selectedSessionIds.length === count;
50
-
51
- const label = allAreLessons
52
- ? `${count} aulas`
53
- : allAreSessions
54
- ? `${count} sessões`
55
- : `${count} itens`;
56
-
57
- const canMove = allAreLessons && sessions.length > 1;
58
-
59
- function handleCopy() {
60
- if (allAreLessons) {
61
- copyItems(selectedLessonIds, 'lesson');
62
- toast.success(`${count} aulas copiadas`);
63
- } else if (allAreSessions) {
64
- copyItems(selectedSessionIds, 'session');
65
- toast.success(`${count} sessões copiadas`);
66
- } else {
67
- toast('Selecione apenas aulas ou apenas sessões para copiar.');
68
- }
69
- }
70
-
71
- function handleDuplicate() {
72
- if (allAreLessons) {
73
- // Duplicate each lesson sequentially
74
- const duplicateNext = (index: number) => {
75
- if (index >= selectedLessonIds.length) return;
76
- const lessonId = selectedLessonIds[index]!;
77
- const lesson = lessons.find((l) => l.id === lessonId);
78
- if (!lesson) return;
79
- duplicateLessonMutation.mutate(
80
- { sessionId: lesson.sessionId, lessonId },
81
- { onSettled: () => duplicateNext(index + 1) }
82
- );
83
- };
84
- duplicateNext(0);
85
- toast.success(`${count} aulas sendo duplicadas...`);
86
- } else if (allAreSessions) {
87
- const duplicateNext = (index: number) => {
88
- if (index >= selectedSessionIds.length) return;
89
- duplicateSessionMutation.mutate(
90
- { sessionId: selectedSessionIds[index]! },
91
- { onSettled: () => duplicateNext(index + 1) }
92
- );
93
- };
94
- duplicateNext(0);
95
- toast.success(`${count} sessões sendo duplicadas...`);
96
- }
97
- }
98
-
99
- function handleMove() {
100
- if (!allAreLessons) return;
101
- const parentSessionIds = new Set(
102
- lessons
103
- .filter((l) => selectedLessonIds.includes(l.id))
104
- .map((l) => l.sessionId)
105
- );
106
- const excludeSessionId =
107
- parentSessionIds.size === 1 ? ([...parentSessionIds][0] ?? null) : null;
108
-
109
- showSessionPicker({
110
- title: `Mover ${count} aulas para`,
111
- excludeSessionId,
112
- onPick: (targetId) => {
113
- const previousLessons = [...lessons];
114
- const movingLessons = selectedLessonIds
115
- .map((id) => lessons.find((l) => l.id === id))
116
- .filter(Boolean) as import('./types').Lesson[];
117
- const moves = movingLessons.map((l, i) => ({
118
- lessonId: l.id,
119
- fromSessionId: l.sessionId,
120
- toSessionId: targetId,
121
- toIndex: i,
122
- }));
123
- // Optimistic
124
- useStructureStore.getState().moveLessons(selectedLessonIds, targetId);
125
- moveLessonsMutation.mutate({ moves, previousLessons });
126
- toast.success(`${count} aulas movidas`);
127
- },
128
- });
129
- }
130
-
131
- function handleDelete() {
132
- // Compute the split: sessions + orphan lessons (lessons whose session is NOT deleted)
133
- const sessionIdSet = new Set(selectedSessionIds);
134
- const orphanLessons = lessons
135
- .filter(
136
- (l) =>
137
- selectedIds.has(`lesson:${l.id}`) && !sessionIdSet.has(l.sessionId)
138
- )
139
- .map((l) => ({ lessonId: l.id, sessionId: l.sessionId }));
140
-
141
- showConfirm({
142
- title: `Excluir ${label}?`,
143
- description: allAreSessions
144
- ? 'As sessões e todas as suas aulas serão excluídas permanentemente.'
145
- : 'Esta ação não pode ser desfeita.',
146
- onConfirm: () =>
147
- bulkDelete.mutate({
148
- sessionIds: selectedSessionIds,
149
- lessons: orphanLessons,
150
- }),
151
- });
152
- }
153
-
154
- return (
155
- <div
156
- role="toolbar"
157
- aria-label={`${count} itens selecionados — ações`}
158
- className={cn(
159
- 'flex items-center gap-0.5 px-2 py-1 shrink-0 border-b',
160
- 'bg-primary/5 dark:bg-primary/10'
161
- )}
162
- >
163
- {/* Count chip */}
164
- <span
165
- className={cn(
166
- 'text-[0.65rem] font-semibold px-1.5 py-0.5 rounded-md mr-1 shrink-0',
167
- 'bg-primary/15 text-primary leading-none'
168
- )}
169
- >
170
- {label}
171
- </span>
172
-
173
- <div className="flex-1" />
174
-
175
- {/* Copy */}
176
- <Button
177
- variant="ghost"
178
- size="icon"
179
- className="size-6 text-muted-foreground hover:text-foreground"
180
- title="Copiar selecionados (Ctrl+C)"
181
- aria-label="Copiar selecionados"
182
- onClick={handleCopy}
183
- >
184
- <Copy className="size-3" />
185
- </Button>
186
-
187
- {/* Duplicate */}
188
- <Button
189
- variant="ghost"
190
- size="icon"
191
- className="size-6 text-muted-foreground hover:text-foreground"
192
- title="Duplicar selecionados (Ctrl+D)"
193
- aria-label="Duplicar selecionados"
194
- disabled={isDuplicating}
195
- onClick={handleDuplicate}
196
- >
197
- {isDuplicating ? (
198
- <Loader2 className="size-3 animate-spin" />
199
- ) : (
200
- <svg
201
- viewBox="0 0 24 24"
202
- className="size-3 fill-none stroke-current stroke-2"
203
- strokeLinecap="round"
204
- strokeLinejoin="round"
205
- aria-hidden
206
- >
207
- <rect x="9" y="9" width="13" height="13" rx="2" />
208
- <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
209
- </svg>
210
- )}
211
- </Button>
212
-
213
- {/* Move (lessons only) */}
214
- {canMove && (
215
- <Button
216
- variant="ghost"
217
- size="icon"
218
- className="size-6 text-muted-foreground hover:text-foreground"
219
- title="Mover para outra sessão"
220
- aria-label="Mover para outra sessão"
221
- disabled={isMoving}
222
- onClick={handleMove}
223
- >
224
- {isMoving ? (
225
- <Loader2 className="size-3 animate-spin" />
226
- ) : (
227
- <FolderOpen className="size-3" />
228
- )}
229
- </Button>
230
- )}
231
-
232
- {/* Delete */}
233
- <Button
234
- variant="ghost"
235
- size="icon"
236
- className="size-6 text-destructive/60 hover:text-destructive"
237
- title="Excluir selecionados (Delete)"
238
- aria-label="Excluir selecionados"
239
- disabled={bulkDelete.isPending}
240
- onClick={handleDelete}
241
- >
242
- {bulkDelete.isPending ? (
243
- <Loader2 className="size-3 animate-spin" />
244
- ) : (
245
- <Trash2 className="size-3" />
246
- )}
247
- </Button>
248
-
249
- <Separator orientation="vertical" className="mx-0.5 h-3.5" />
250
-
251
- {/* Deselect */}
252
- <Button
253
- variant="ghost"
254
- size="icon"
255
- className="size-6 text-muted-foreground hover:text-foreground"
256
- title="Limpar seleção (Escape)"
257
- aria-label="Limpar seleção"
258
- onClick={clearSelection}
259
- >
260
- <X className="size-3" />
261
- </Button>
262
- </div>
263
- );
264
- }
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { Separator } from '@/components/ui/separator';
5
+ import { cn } from '@/lib/utils';
6
+ import { Copy, FolderOpen, Loader2, Trash2, X } from 'lucide-react';
7
+ import { toast } from 'sonner';
8
+ import {
9
+ useBulkDeleteMutation,
10
+ useDuplicateLessonMutation,
11
+ useDuplicateSessionMutation,
12
+ useMoveLessonsMutation,
13
+ } from '../_data/use-course-structure-mutations';
14
+ import { useStructureStore } from './store';
15
+
16
+ /**
17
+ * Compact action bar shown when 2+ items are selected.
18
+ * Fixed below the search toolbar inside CourseTreePanel.
19
+ */
20
+ export function MultiSelectBar() {
21
+ const selectedIds = useStructureStore((s) => s.selectedIds);
22
+ const lessons = useStructureStore((s) => s.lessons);
23
+ const sessions = useStructureStore((s) => s.sessions);
24
+ const clearSelection = useStructureStore((s) => s.clearSelection);
25
+ const copyItems = useStructureStore((s) => s.copyItems);
26
+ const showConfirm = useStructureStore((s) => s.showConfirm);
27
+ const showSessionPicker = useStructureStore((s) => s.showSessionPicker);
28
+
29
+ const bulkDelete = useBulkDeleteMutation();
30
+ const duplicateLessonMutation = useDuplicateLessonMutation();
31
+ const duplicateSessionMutation = useDuplicateSessionMutation();
32
+ const moveLessonsMutation = useMoveLessonsMutation();
33
+
34
+ const isDuplicating =
35
+ duplicateLessonMutation.isPending || duplicateSessionMutation.isPending;
36
+ const isMoving = moveLessonsMutation.isPending;
37
+
38
+ const count = selectedIds.size;
39
+ if (count <= 1) return null;
40
+
41
+ const selectedLessonIds = lessons
42
+ .filter((l) => selectedIds.has(`lesson:${l.id}`))
43
+ .map((l) => l.id);
44
+ const selectedSessionIds = sessions
45
+ .filter((ss) => selectedIds.has(`session:${ss.id}`))
46
+ .map((ss) => ss.id);
47
+
48
+ const allAreLessons = selectedLessonIds.length === count;
49
+ const allAreSessions = selectedSessionIds.length === count;
50
+
51
+ const label = allAreLessons
52
+ ? `${count} aulas`
53
+ : allAreSessions
54
+ ? `${count} sessões`
55
+ : `${count} itens`;
56
+
57
+ const canMove = allAreLessons && sessions.length > 1;
58
+
59
+ function handleCopy() {
60
+ if (allAreLessons) {
61
+ copyItems(selectedLessonIds, 'lesson');
62
+ toast.success(`${count} aulas copiadas`);
63
+ } else if (allAreSessions) {
64
+ copyItems(selectedSessionIds, 'session');
65
+ toast.success(`${count} sessões copiadas`);
66
+ } else {
67
+ toast('Selecione apenas aulas ou apenas sessões para copiar.');
68
+ }
69
+ }
70
+
71
+ function handleDuplicate() {
72
+ if (allAreLessons) {
73
+ // Duplicate each lesson sequentially
74
+ const duplicateNext = (index: number) => {
75
+ if (index >= selectedLessonIds.length) return;
76
+ const lessonId = selectedLessonIds[index]!;
77
+ const lesson = lessons.find((l) => l.id === lessonId);
78
+ if (!lesson) return;
79
+ duplicateLessonMutation.mutate(
80
+ { sessionId: lesson.sessionId, lessonId },
81
+ { onSettled: () => duplicateNext(index + 1) }
82
+ );
83
+ };
84
+ duplicateNext(0);
85
+ toast.success(`${count} aulas sendo duplicadas...`);
86
+ } else if (allAreSessions) {
87
+ const duplicateNext = (index: number) => {
88
+ if (index >= selectedSessionIds.length) return;
89
+ duplicateSessionMutation.mutate(
90
+ { sessionId: selectedSessionIds[index]! },
91
+ { onSettled: () => duplicateNext(index + 1) }
92
+ );
93
+ };
94
+ duplicateNext(0);
95
+ toast.success(`${count} sessões sendo duplicadas...`);
96
+ }
97
+ }
98
+
99
+ function handleMove() {
100
+ if (!allAreLessons) return;
101
+ const parentSessionIds = new Set(
102
+ lessons
103
+ .filter((l) => selectedLessonIds.includes(l.id))
104
+ .map((l) => l.sessionId)
105
+ );
106
+ const excludeSessionId =
107
+ parentSessionIds.size === 1 ? ([...parentSessionIds][0] ?? null) : null;
108
+
109
+ showSessionPicker({
110
+ title: `Mover ${count} aulas para`,
111
+ excludeSessionId,
112
+ onPick: (targetId) => {
113
+ const previousLessons = [...lessons];
114
+ const movingLessons = selectedLessonIds
115
+ .map((id) => lessons.find((l) => l.id === id))
116
+ .filter(Boolean) as import('./types').Lesson[];
117
+ const moves = movingLessons.map((l, i) => ({
118
+ lessonId: l.id,
119
+ fromSessionId: l.sessionId,
120
+ toSessionId: targetId,
121
+ toIndex: i,
122
+ }));
123
+ // Optimistic
124
+ useStructureStore.getState().moveLessons(selectedLessonIds, targetId);
125
+ moveLessonsMutation.mutate({ moves, previousLessons });
126
+ toast.success(`${count} aulas movidas`);
127
+ },
128
+ });
129
+ }
130
+
131
+ function handleDelete() {
132
+ // Compute the split: sessions + orphan lessons (lessons whose session is NOT deleted)
133
+ const sessionIdSet = new Set(selectedSessionIds);
134
+ const orphanLessons = lessons
135
+ .filter(
136
+ (l) =>
137
+ selectedIds.has(`lesson:${l.id}`) && !sessionIdSet.has(l.sessionId)
138
+ )
139
+ .map((l) => ({ lessonId: l.id, sessionId: l.sessionId }));
140
+
141
+ showConfirm({
142
+ title: `Excluir ${label}?`,
143
+ description: allAreSessions
144
+ ? 'As sessões e todas as suas aulas serão excluídas permanentemente.'
145
+ : 'Esta ação não pode ser desfeita.',
146
+ onConfirm: () =>
147
+ bulkDelete.mutate({
148
+ sessionIds: selectedSessionIds,
149
+ lessons: orphanLessons,
150
+ }),
151
+ });
152
+ }
153
+
154
+ return (
155
+ <div
156
+ role="toolbar"
157
+ aria-label={`${count} itens selecionados — ações`}
158
+ className={cn(
159
+ 'flex items-center gap-0.5 px-2 py-1 shrink-0 border-b',
160
+ 'bg-primary/5 dark:bg-primary/10'
161
+ )}
162
+ >
163
+ {/* Count chip */}
164
+ <span
165
+ className={cn(
166
+ 'text-[0.65rem] font-semibold px-1.5 py-0.5 rounded-md mr-1 shrink-0',
167
+ 'bg-primary/15 text-primary leading-none'
168
+ )}
169
+ >
170
+ {label}
171
+ </span>
172
+
173
+ <div className="flex-1" />
174
+
175
+ {/* Copy */}
176
+ <Button
177
+ variant="ghost"
178
+ size="icon"
179
+ className="size-6 text-muted-foreground hover:text-foreground"
180
+ title="Copiar selecionados (Ctrl+C)"
181
+ aria-label="Copiar selecionados"
182
+ onClick={handleCopy}
183
+ >
184
+ <Copy className="size-3" />
185
+ </Button>
186
+
187
+ {/* Duplicate */}
188
+ <Button
189
+ variant="ghost"
190
+ size="icon"
191
+ className="size-6 text-muted-foreground hover:text-foreground"
192
+ title="Duplicar selecionados (Ctrl+D)"
193
+ aria-label="Duplicar selecionados"
194
+ disabled={isDuplicating}
195
+ onClick={handleDuplicate}
196
+ >
197
+ {isDuplicating ? (
198
+ <Loader2 className="size-3 animate-spin" />
199
+ ) : (
200
+ <svg
201
+ viewBox="0 0 24 24"
202
+ className="size-3 fill-none stroke-current stroke-2"
203
+ strokeLinecap="round"
204
+ strokeLinejoin="round"
205
+ aria-hidden
206
+ >
207
+ <rect x="9" y="9" width="13" height="13" rx="2" />
208
+ <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1" />
209
+ </svg>
210
+ )}
211
+ </Button>
212
+
213
+ {/* Move (lessons only) */}
214
+ {canMove && (
215
+ <Button
216
+ variant="ghost"
217
+ size="icon"
218
+ className="size-6 text-muted-foreground hover:text-foreground"
219
+ title="Mover para outra sessão"
220
+ aria-label="Mover para outra sessão"
221
+ disabled={isMoving}
222
+ onClick={handleMove}
223
+ >
224
+ {isMoving ? (
225
+ <Loader2 className="size-3 animate-spin" />
226
+ ) : (
227
+ <FolderOpen className="size-3" />
228
+ )}
229
+ </Button>
230
+ )}
231
+
232
+ {/* Delete */}
233
+ <Button
234
+ variant="ghost"
235
+ size="icon"
236
+ className="size-6 text-destructive/60 hover:text-destructive"
237
+ title="Excluir selecionados (Delete)"
238
+ aria-label="Excluir selecionados"
239
+ disabled={bulkDelete.isPending}
240
+ onClick={handleDelete}
241
+ >
242
+ {bulkDelete.isPending ? (
243
+ <Loader2 className="size-3 animate-spin" />
244
+ ) : (
245
+ <Trash2 className="size-3" />
246
+ )}
247
+ </Button>
248
+
249
+ <Separator orientation="vertical" className="mx-0.5 h-3.5" />
250
+
251
+ {/* Deselect */}
252
+ <Button
253
+ variant="ghost"
254
+ size="icon"
255
+ className="size-6 text-muted-foreground hover:text-foreground"
256
+ title="Limpar seleção (Escape)"
257
+ aria-label="Limpar seleção"
258
+ onClick={clearSelection}
259
+ >
260
+ <X className="size-3" />
261
+ </Button>
262
+ </div>
263
+ );
264
+ }