@hed-hog/lms 0.0.312 → 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.
- package/dist/class-group/class-group.controller.d.ts +2 -2
- package/dist/class-group/class-group.service.d.ts +2 -2
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts +13 -0
- package/dist/enterprise/dto/enterprise-profile.dto.d.ts.map +1 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js +3 -0
- package/dist/enterprise/dto/enterprise-profile.dto.js.map +1 -0
- package/dist/enterprise/enterprise.controller.d.ts +3 -0
- package/dist/enterprise/enterprise.controller.d.ts.map +1 -1
- package/dist/enterprise/enterprise.controller.js +14 -0
- package/dist/enterprise/enterprise.controller.js.map +1 -1
- package/dist/enterprise/enterprise.service.d.ts +3 -0
- package/dist/enterprise/enterprise.service.d.ts.map +1 -1
- package/dist/enterprise/enterprise.service.js +128 -1
- package/dist/enterprise/enterprise.service.js.map +1 -1
- package/dist/instructor/instructor.controller.d.ts +23 -0
- package/dist/instructor/instructor.controller.d.ts.map +1 -1
- package/dist/instructor/instructor.controller.js +41 -0
- package/dist/instructor/instructor.controller.js.map +1 -1
- package/dist/instructor/instructor.service.d.ts +25 -0
- package/dist/instructor/instructor.service.d.ts.map +1 -1
- package/dist/instructor/instructor.service.js +126 -8
- package/dist/instructor/instructor.service.js.map +1 -1
- package/hedhog/data/menu.yaml +23 -7
- package/hedhog/data/role.yaml +9 -1
- package/hedhog/data/route.yaml +54 -0
- package/hedhog/frontend/app/_components/class-form-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/courses/[id]/structure/_components/confirm-dialog.tsx.ejs +44 -44
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-dnd.tsx.ejs +362 -362
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree-panel.tsx.ejs +111 -111
- package/hedhog/frontend/app/courses/[id]/structure/_components/course-tree.tsx.ejs +134 -134
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-course.tsx.ejs +113 -113
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-lesson.tsx.ejs +314 -314
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-panel.tsx.ejs +62 -62
- package/hedhog/frontend/app/courses/[id]/structure/_components/detail-session.tsx.ejs +173 -173
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-handle.tsx.ejs +58 -58
- package/hedhog/frontend/app/courses/[id]/structure/_components/drag-overlay.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-bulk.tsx.ejs +276 -276
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-course.tsx.ejs +1216 -1216
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-lesson.tsx.ejs +1824 -1824
- package/hedhog/frontend/app/courses/[id]/structure/_components/editor-session.tsx.ejs +443 -443
- package/hedhog/frontend/app/courses/[id]/structure/_components/highlighted-text.tsx.ejs +40 -40
- package/hedhog/frontend/app/courses/[id]/structure/_components/mock-data.ts.ejs +185 -185
- package/hedhog/frontend/app/courses/[id]/structure/_components/multi-select-bar.tsx.ejs +264 -264
- package/hedhog/frontend/app/courses/[id]/structure/_components/search-filter.tsx.ejs +95 -95
- package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs +73 -73
- package/hedhog/frontend/app/courses/[id]/structure/_components/shortcuts-help.tsx.ejs +136 -136
- package/hedhog/frontend/app/courses/[id]/structure/_components/sortable-tree-row.tsx.ejs +80 -80
- package/hedhog/frontend/app/courses/[id]/structure/_components/store.ts.ejs +949 -949
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-context-menu.tsx.ejs +525 -525
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-helpers.ts.ejs +181 -181
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-course.tsx.ejs +51 -51
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-lesson.tsx.ejs +271 -271
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row-session.tsx.ejs +167 -167
- package/hedhog/frontend/app/courses/[id]/structure/_components/tree-row.tsx.ejs +108 -108
- package/hedhog/frontend/app/courses/[id]/structure/_components/use-course-structure-shortcuts.ts.ejs +318 -318
- package/hedhog/frontend/app/courses/[id]/structure/page.tsx.ejs +10 -10
- package/hedhog/frontend/app/enterprise/_components/enterprise-course-create-sheet.tsx.ejs +2 -1
- package/hedhog/frontend/app/instructors/_components/instructor-form-sheet.tsx.ejs +438 -0
- package/hedhog/frontend/app/instructors/_components/instructor-types.ts.ejs +40 -0
- package/hedhog/frontend/app/instructors/page.tsx.ejs +696 -0
- package/hedhog/frontend/app/training/page.tsx.ejs +2339 -0
- package/hedhog/table/enterprise_user.yaml +1 -1
- package/package.json +8 -8
- package/src/enterprise/dto/enterprise-profile.dto.ts +17 -0
- package/src/enterprise/enterprise.controller.ts +9 -1
- package/src/enterprise/enterprise.service.ts +147 -4
- package/src/instructor/instructor.controller.ts +36 -9
- package/src/instructor/instructor.service.ts +140 -10
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Search, X } from 'lucide-react';
|
|
4
|
-
import { useTranslations } from 'next-intl';
|
|
5
|
-
import {
|
|
6
|
-
forwardRef,
|
|
7
|
-
useEffect,
|
|
8
|
-
useImperativeHandle,
|
|
9
|
-
useRef,
|
|
10
|
-
useState,
|
|
11
|
-
} from 'react';
|
|
12
|
-
|
|
13
|
-
import { Button } from '@/components/ui/button';
|
|
14
|
-
import { Input } from '@/components/ui/input';
|
|
15
|
-
import { cn } from '@/lib/utils';
|
|
16
|
-
import { useDebounce } from '@/hooks/use-debounce';
|
|
17
|
-
import { useStructureStore } from './store';
|
|
18
|
-
|
|
19
|
-
export interface SearchFilterHandle {
|
|
20
|
-
focus(): void;
|
|
21
|
-
clear(): void;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
interface SearchFilterProps {
|
|
25
|
-
/** Number of matching items to show alongside the input. */
|
|
26
|
-
resultCount?: number;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const SearchFilter = forwardRef<SearchFilterHandle, SearchFilterProps>(
|
|
30
|
-
({ resultCount }, ref) => {
|
|
31
|
-
const t = useTranslations('lms.CoursesPage.StructurePage');
|
|
32
|
-
const setFilter = useStructureStore((s) => s.setFilter);
|
|
33
|
-
const [value, setValue] = useState('');
|
|
34
|
-
const debounced = useDebounce(value, 250);
|
|
35
|
-
const inputRef = useRef<HTMLInputElement>(null);
|
|
36
|
-
|
|
37
|
-
useEffect(() => {
|
|
38
|
-
setFilter(debounced);
|
|
39
|
-
}, [debounced, setFilter]);
|
|
40
|
-
|
|
41
|
-
useImperativeHandle(ref, () => ({
|
|
42
|
-
focus() {
|
|
43
|
-
inputRef.current?.focus();
|
|
44
|
-
inputRef.current?.select();
|
|
45
|
-
},
|
|
46
|
-
clear() {
|
|
47
|
-
setValue('');
|
|
48
|
-
},
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
const isActive = value.trim().length > 0;
|
|
52
|
-
|
|
53
|
-
return (
|
|
54
|
-
<div className="relative">
|
|
55
|
-
<Search
|
|
56
|
-
className={cn(
|
|
57
|
-
'absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 pointer-events-none transition-colors',
|
|
58
|
-
isActive ? 'text-primary' : 'text-muted-foreground'
|
|
59
|
-
)}
|
|
60
|
-
aria-hidden
|
|
61
|
-
/>
|
|
62
|
-
<Input
|
|
63
|
-
ref={inputRef}
|
|
64
|
-
value={value}
|
|
65
|
-
onChange={(e) => setValue(e.target.value)}
|
|
66
|
-
placeholder={t('search.placeholder')}
|
|
67
|
-
className={cn(
|
|
68
|
-
'pl-8 h-8 text-sm transition-colors',
|
|
69
|
-
isActive ? 'pr-16 ring-1 ring-primary/30' : 'pr-2'
|
|
70
|
-
)}
|
|
71
|
-
aria-label="Buscar sessões e aulas"
|
|
72
|
-
/>
|
|
73
|
-
{isActive && (
|
|
74
|
-
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
|
75
|
-
{resultCount !== undefined && (
|
|
76
|
-
<span className="text-[0.6rem] text-muted-foreground tabular-nums leading-none">
|
|
77
|
-
{resultCount}
|
|
78
|
-
</span>
|
|
79
|
-
)}
|
|
80
|
-
<Button
|
|
81
|
-
variant="ghost"
|
|
82
|
-
size="icon"
|
|
83
|
-
className="size-5 rounded-sm shrink-0"
|
|
84
|
-
onClick={() => setValue('')}
|
|
85
|
-
aria-label="Limpar busca"
|
|
86
|
-
>
|
|
87
|
-
<X className="size-3" />
|
|
88
|
-
</Button>
|
|
89
|
-
</div>
|
|
90
|
-
)}
|
|
91
|
-
</div>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
94
|
-
);
|
|
95
|
-
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Search, X } from 'lucide-react';
|
|
4
|
+
import { useTranslations } from 'next-intl';
|
|
5
|
+
import {
|
|
6
|
+
forwardRef,
|
|
7
|
+
useEffect,
|
|
8
|
+
useImperativeHandle,
|
|
9
|
+
useRef,
|
|
10
|
+
useState,
|
|
11
|
+
} from 'react';
|
|
12
|
+
|
|
13
|
+
import { Button } from '@/components/ui/button';
|
|
14
|
+
import { Input } from '@/components/ui/input';
|
|
15
|
+
import { cn } from '@/lib/utils';
|
|
16
|
+
import { useDebounce } from '@/hooks/use-debounce';
|
|
17
|
+
import { useStructureStore } from './store';
|
|
18
|
+
|
|
19
|
+
export interface SearchFilterHandle {
|
|
20
|
+
focus(): void;
|
|
21
|
+
clear(): void;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
interface SearchFilterProps {
|
|
25
|
+
/** Number of matching items to show alongside the input. */
|
|
26
|
+
resultCount?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const SearchFilter = forwardRef<SearchFilterHandle, SearchFilterProps>(
|
|
30
|
+
({ resultCount }, ref) => {
|
|
31
|
+
const t = useTranslations('lms.CoursesPage.StructurePage');
|
|
32
|
+
const setFilter = useStructureStore((s) => s.setFilter);
|
|
33
|
+
const [value, setValue] = useState('');
|
|
34
|
+
const debounced = useDebounce(value, 250);
|
|
35
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
setFilter(debounced);
|
|
39
|
+
}, [debounced, setFilter]);
|
|
40
|
+
|
|
41
|
+
useImperativeHandle(ref, () => ({
|
|
42
|
+
focus() {
|
|
43
|
+
inputRef.current?.focus();
|
|
44
|
+
inputRef.current?.select();
|
|
45
|
+
},
|
|
46
|
+
clear() {
|
|
47
|
+
setValue('');
|
|
48
|
+
},
|
|
49
|
+
}));
|
|
50
|
+
|
|
51
|
+
const isActive = value.trim().length > 0;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="relative">
|
|
55
|
+
<Search
|
|
56
|
+
className={cn(
|
|
57
|
+
'absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 pointer-events-none transition-colors',
|
|
58
|
+
isActive ? 'text-primary' : 'text-muted-foreground'
|
|
59
|
+
)}
|
|
60
|
+
aria-hidden
|
|
61
|
+
/>
|
|
62
|
+
<Input
|
|
63
|
+
ref={inputRef}
|
|
64
|
+
value={value}
|
|
65
|
+
onChange={(e) => setValue(e.target.value)}
|
|
66
|
+
placeholder={t('search.placeholder')}
|
|
67
|
+
className={cn(
|
|
68
|
+
'pl-8 h-8 text-sm transition-colors',
|
|
69
|
+
isActive ? 'pr-16 ring-1 ring-primary/30' : 'pr-2'
|
|
70
|
+
)}
|
|
71
|
+
aria-label="Buscar sessões e aulas"
|
|
72
|
+
/>
|
|
73
|
+
{isActive && (
|
|
74
|
+
<div className="absolute right-1 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
|
75
|
+
{resultCount !== undefined && (
|
|
76
|
+
<span className="text-[0.6rem] text-muted-foreground tabular-nums leading-none">
|
|
77
|
+
{resultCount}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
<Button
|
|
81
|
+
variant="ghost"
|
|
82
|
+
size="icon"
|
|
83
|
+
className="size-5 rounded-sm shrink-0"
|
|
84
|
+
onClick={() => setValue('')}
|
|
85
|
+
aria-label="Limpar busca"
|
|
86
|
+
>
|
|
87
|
+
<X className="size-3" />
|
|
88
|
+
</Button>
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
);
|
|
95
|
+
|
|
96
96
|
SearchFilter.displayName = 'SearchFilter';
|
package/hedhog/frontend/app/courses/[id]/structure/_components/session-picker-dialog.tsx.ejs
CHANGED
|
@@ -1,74 +1,74 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { FolderOpen } from 'lucide-react';
|
|
4
|
-
import {
|
|
5
|
-
Dialog,
|
|
6
|
-
DialogContent,
|
|
7
|
-
DialogHeader,
|
|
8
|
-
DialogTitle,
|
|
9
|
-
} from '@/components/ui/dialog';
|
|
10
|
-
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
11
|
-
import { cn } from '@/lib/utils';
|
|
12
|
-
import { useStructureStore } from './store';
|
|
13
|
-
|
|
14
|
-
export function SessionPickerDialog() {
|
|
15
|
-
const sessions = useStructureStore((s) => s.sessions);
|
|
16
|
-
const lessons = useStructureStore((s) => s.lessons);
|
|
17
|
-
const sessionPickerDialog = useStructureStore((s) => s.sessionPickerDialog);
|
|
18
|
-
const closeSessionPicker = useStructureStore((s) => s.closeSessionPicker);
|
|
19
|
-
|
|
20
|
-
const sorted = [...sessions].sort((a, b) => a.order - b.order);
|
|
21
|
-
const visibleSessions = sorted.filter(
|
|
22
|
-
(ss) => ss.id !== sessionPickerDialog.excludeSessionId
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
function handlePick(sessionId: string) {
|
|
26
|
-
sessionPickerDialog.onPick?.(sessionId);
|
|
27
|
-
closeSessionPicker();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return (
|
|
31
|
-
<Dialog
|
|
32
|
-
open={sessionPickerDialog.open}
|
|
33
|
-
onOpenChange={(open) => !open && closeSessionPicker()}
|
|
34
|
-
>
|
|
35
|
-
<DialogContent className="max-w-xs p-0 gap-0">
|
|
36
|
-
<DialogHeader className="px-4 py-3 border-b">
|
|
37
|
-
<DialogTitle className="text-sm flex items-center gap-2">
|
|
38
|
-
<FolderOpen className="size-4 text-muted-foreground" />
|
|
39
|
-
{sessionPickerDialog.title}
|
|
40
|
-
</DialogTitle>
|
|
41
|
-
</DialogHeader>
|
|
42
|
-
|
|
43
|
-
<ScrollArea className="max-h-72">
|
|
44
|
-
<div className="flex flex-col p-2 gap-0.5">
|
|
45
|
-
{visibleSessions.length === 0 && (
|
|
46
|
-
<p className="text-xs text-muted-foreground text-center py-4">
|
|
47
|
-
Nenhuma sessão disponível.
|
|
48
|
-
</p>
|
|
49
|
-
)}
|
|
50
|
-
{visibleSessions.map((ss) => {
|
|
51
|
-
const count = lessons.filter((l) => l.sessionId === ss.id).length;
|
|
52
|
-
return (
|
|
53
|
-
<button
|
|
54
|
-
key={ss.id}
|
|
55
|
-
onClick={() => handlePick(ss.id)}
|
|
56
|
-
className={cn(
|
|
57
|
-
'flex items-center gap-2 px-2.5 py-2 rounded-md text-left text-sm',
|
|
58
|
-
'hover:bg-accent transition-colors cursor-pointer'
|
|
59
|
-
)}
|
|
60
|
-
>
|
|
61
|
-
<FolderOpen className="size-3.5 shrink-0 text-muted-foreground" />
|
|
62
|
-
<span className="flex-1 truncate font-medium">{ss.title}</span>
|
|
63
|
-
<span className="text-[0.65rem] text-muted-foreground shrink-0">
|
|
64
|
-
{count} aulas
|
|
65
|
-
</span>
|
|
66
|
-
</button>
|
|
67
|
-
);
|
|
68
|
-
})}
|
|
69
|
-
</div>
|
|
70
|
-
</ScrollArea>
|
|
71
|
-
</DialogContent>
|
|
72
|
-
</Dialog>
|
|
73
|
-
);
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { FolderOpen } from 'lucide-react';
|
|
4
|
+
import {
|
|
5
|
+
Dialog,
|
|
6
|
+
DialogContent,
|
|
7
|
+
DialogHeader,
|
|
8
|
+
DialogTitle,
|
|
9
|
+
} from '@/components/ui/dialog';
|
|
10
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
11
|
+
import { cn } from '@/lib/utils';
|
|
12
|
+
import { useStructureStore } from './store';
|
|
13
|
+
|
|
14
|
+
export function SessionPickerDialog() {
|
|
15
|
+
const sessions = useStructureStore((s) => s.sessions);
|
|
16
|
+
const lessons = useStructureStore((s) => s.lessons);
|
|
17
|
+
const sessionPickerDialog = useStructureStore((s) => s.sessionPickerDialog);
|
|
18
|
+
const closeSessionPicker = useStructureStore((s) => s.closeSessionPicker);
|
|
19
|
+
|
|
20
|
+
const sorted = [...sessions].sort((a, b) => a.order - b.order);
|
|
21
|
+
const visibleSessions = sorted.filter(
|
|
22
|
+
(ss) => ss.id !== sessionPickerDialog.excludeSessionId
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
function handlePick(sessionId: string) {
|
|
26
|
+
sessionPickerDialog.onPick?.(sessionId);
|
|
27
|
+
closeSessionPicker();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Dialog
|
|
32
|
+
open={sessionPickerDialog.open}
|
|
33
|
+
onOpenChange={(open) => !open && closeSessionPicker()}
|
|
34
|
+
>
|
|
35
|
+
<DialogContent className="max-w-xs p-0 gap-0">
|
|
36
|
+
<DialogHeader className="px-4 py-3 border-b">
|
|
37
|
+
<DialogTitle className="text-sm flex items-center gap-2">
|
|
38
|
+
<FolderOpen className="size-4 text-muted-foreground" />
|
|
39
|
+
{sessionPickerDialog.title}
|
|
40
|
+
</DialogTitle>
|
|
41
|
+
</DialogHeader>
|
|
42
|
+
|
|
43
|
+
<ScrollArea className="max-h-72">
|
|
44
|
+
<div className="flex flex-col p-2 gap-0.5">
|
|
45
|
+
{visibleSessions.length === 0 && (
|
|
46
|
+
<p className="text-xs text-muted-foreground text-center py-4">
|
|
47
|
+
Nenhuma sessão disponível.
|
|
48
|
+
</p>
|
|
49
|
+
)}
|
|
50
|
+
{visibleSessions.map((ss) => {
|
|
51
|
+
const count = lessons.filter((l) => l.sessionId === ss.id).length;
|
|
52
|
+
return (
|
|
53
|
+
<button
|
|
54
|
+
key={ss.id}
|
|
55
|
+
onClick={() => handlePick(ss.id)}
|
|
56
|
+
className={cn(
|
|
57
|
+
'flex items-center gap-2 px-2.5 py-2 rounded-md text-left text-sm',
|
|
58
|
+
'hover:bg-accent transition-colors cursor-pointer'
|
|
59
|
+
)}
|
|
60
|
+
>
|
|
61
|
+
<FolderOpen className="size-3.5 shrink-0 text-muted-foreground" />
|
|
62
|
+
<span className="flex-1 truncate font-medium">{ss.title}</span>
|
|
63
|
+
<span className="text-[0.65rem] text-muted-foreground shrink-0">
|
|
64
|
+
{count} aulas
|
|
65
|
+
</span>
|
|
66
|
+
</button>
|
|
67
|
+
);
|
|
68
|
+
})}
|
|
69
|
+
</div>
|
|
70
|
+
</ScrollArea>
|
|
71
|
+
</DialogContent>
|
|
72
|
+
</Dialog>
|
|
73
|
+
);
|
|
74
74
|
}
|
|
@@ -1,136 +1,136 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import { Keyboard } from 'lucide-react';
|
|
4
|
-
|
|
5
|
-
import { Badge } from '@/components/ui/badge';
|
|
6
|
-
import { Button } from '@/components/ui/button';
|
|
7
|
-
import {
|
|
8
|
-
Dialog,
|
|
9
|
-
DialogContent,
|
|
10
|
-
DialogHeader,
|
|
11
|
-
DialogTitle,
|
|
12
|
-
} from '@/components/ui/dialog';
|
|
13
|
-
import { Separator } from '@/components/ui/separator';
|
|
14
|
-
|
|
15
|
-
// ── Data ──────────────────────────────────────────────────────────────────────
|
|
16
|
-
|
|
17
|
-
const SHORTCUT_GROUPS: {
|
|
18
|
-
heading: string;
|
|
19
|
-
items: { keys: string[]; description: string }[];
|
|
20
|
-
}[] = [
|
|
21
|
-
{
|
|
22
|
-
heading: 'Navegação',
|
|
23
|
-
items: [
|
|
24
|
-
{ keys: ['↑', '↓'], description: 'Navegar entre itens' },
|
|
25
|
-
{ keys: ['→'], description: 'Expandir sessão selecionada' },
|
|
26
|
-
{ keys: ['←'], description: 'Recolher sessão / ir para pai' },
|
|
27
|
-
{ keys: ['Enter'], description: 'Focar primeiro campo do editor' },
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
heading: 'Ações',
|
|
32
|
-
items: [
|
|
33
|
-
{ keys: ['Ctrl', 'S'], description: 'Salvar formulário do painel' },
|
|
34
|
-
{ keys: ['Ctrl', 'N'], description: 'Criar nova sessão ou aula' },
|
|
35
|
-
{ keys: ['Ctrl', 'C'], description: 'Copiar item selecionado' },
|
|
36
|
-
{ keys: ['Ctrl', 'V'], description: 'Colar no contexto atual' },
|
|
37
|
-
{ keys: ['Ctrl', 'D'], description: 'Duplicar item' },
|
|
38
|
-
{ keys: ['Delete'], description: 'Excluir item(ns) selecionado(s)' },
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
{
|
|
42
|
-
heading: 'Busca & Interface',
|
|
43
|
-
items: [
|
|
44
|
-
{ keys: ['Ctrl', 'F'], description: 'Focar campo de busca' },
|
|
45
|
-
{ keys: ['Ctrl', '/'], description: 'Abrir ajuda de atalhos' },
|
|
46
|
-
{ keys: ['Esc'], description: 'Limpar busca / seleção / foco' },
|
|
47
|
-
],
|
|
48
|
-
},
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
// ── Component ─────────────────────────────────────────────────────────────────
|
|
52
|
-
|
|
53
|
-
interface ShortcutsHelpProps {
|
|
54
|
-
open: boolean;
|
|
55
|
-
onOpenChange: (open: boolean) => void;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
|
|
59
|
-
return (
|
|
60
|
-
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
61
|
-
<DialogContent className="max-w-sm">
|
|
62
|
-
<DialogHeader>
|
|
63
|
-
<DialogTitle className="flex items-center gap-2">
|
|
64
|
-
<Keyboard className="size-4" />
|
|
65
|
-
Atalhos de teclado
|
|
66
|
-
</DialogTitle>
|
|
67
|
-
</DialogHeader>
|
|
68
|
-
|
|
69
|
-
<div className="flex flex-col gap-4 mt-1">
|
|
70
|
-
{SHORTCUT_GROUPS.map((group, gi) => (
|
|
71
|
-
<div key={gi}>
|
|
72
|
-
<p className="text-[0.65rem] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
73
|
-
{group.heading}
|
|
74
|
-
</p>
|
|
75
|
-
<div className="flex flex-col gap-1.5">
|
|
76
|
-
{group.items.map(({ keys, description }, ki) => (
|
|
77
|
-
<div
|
|
78
|
-
key={ki}
|
|
79
|
-
className="flex items-center justify-between gap-3"
|
|
80
|
-
>
|
|
81
|
-
<span className="text-sm text-muted-foreground">
|
|
82
|
-
{description}
|
|
83
|
-
</span>
|
|
84
|
-
<div className="flex items-center gap-1 shrink-0">
|
|
85
|
-
{keys.map((k, i) => (
|
|
86
|
-
<Badge
|
|
87
|
-
key={i}
|
|
88
|
-
variant="outline"
|
|
89
|
-
className="h-5 px-1.5 text-[0.65rem] font-mono font-medium"
|
|
90
|
-
>
|
|
91
|
-
{k}
|
|
92
|
-
</Badge>
|
|
93
|
-
))}
|
|
94
|
-
</div>
|
|
95
|
-
</div>
|
|
96
|
-
))}
|
|
97
|
-
</div>
|
|
98
|
-
{gi < SHORTCUT_GROUPS.length - 1 && (
|
|
99
|
-
<Separator className="mt-3" />
|
|
100
|
-
)}
|
|
101
|
-
</div>
|
|
102
|
-
))}
|
|
103
|
-
</div>
|
|
104
|
-
|
|
105
|
-
<p className="text-[0.65rem] text-muted-foreground mt-1">
|
|
106
|
-
Atalhos são desativados quando o foco está em inputs, exceto Ctrl+S,
|
|
107
|
-
Ctrl+F e Ctrl+/ que funcionam em qualquer contexto.
|
|
108
|
-
</p>
|
|
109
|
-
</DialogContent>
|
|
110
|
-
</Dialog>
|
|
111
|
-
);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Convenience trigger button — renders standalone when no external state is needed.
|
|
116
|
-
*/
|
|
117
|
-
export function ShortcutsHelpTrigger({ onOpen }: { onOpen: () => void }) {
|
|
118
|
-
return (
|
|
119
|
-
<Button
|
|
120
|
-
variant="ghost"
|
|
121
|
-
size="sm"
|
|
122
|
-
className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
|
|
123
|
-
onClick={onOpen}
|
|
124
|
-
title="Atalhos de teclado (Ctrl+/)"
|
|
125
|
-
>
|
|
126
|
-
<Keyboard className="size-3.5" />
|
|
127
|
-
Atalhos
|
|
128
|
-
<Badge
|
|
129
|
-
variant="outline"
|
|
130
|
-
className="h-4 px-1 text-[0.6rem] font-mono ml-0.5"
|
|
131
|
-
>
|
|
132
|
-
Ctrl+/
|
|
133
|
-
</Badge>
|
|
134
|
-
</Button>
|
|
135
|
-
);
|
|
136
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Keyboard } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import { Badge } from '@/components/ui/badge';
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import {
|
|
8
|
+
Dialog,
|
|
9
|
+
DialogContent,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from '@/components/ui/dialog';
|
|
13
|
+
import { Separator } from '@/components/ui/separator';
|
|
14
|
+
|
|
15
|
+
// ── Data ──────────────────────────────────────────────────────────────────────
|
|
16
|
+
|
|
17
|
+
const SHORTCUT_GROUPS: {
|
|
18
|
+
heading: string;
|
|
19
|
+
items: { keys: string[]; description: string }[];
|
|
20
|
+
}[] = [
|
|
21
|
+
{
|
|
22
|
+
heading: 'Navegação',
|
|
23
|
+
items: [
|
|
24
|
+
{ keys: ['↑', '↓'], description: 'Navegar entre itens' },
|
|
25
|
+
{ keys: ['→'], description: 'Expandir sessão selecionada' },
|
|
26
|
+
{ keys: ['←'], description: 'Recolher sessão / ir para pai' },
|
|
27
|
+
{ keys: ['Enter'], description: 'Focar primeiro campo do editor' },
|
|
28
|
+
],
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
heading: 'Ações',
|
|
32
|
+
items: [
|
|
33
|
+
{ keys: ['Ctrl', 'S'], description: 'Salvar formulário do painel' },
|
|
34
|
+
{ keys: ['Ctrl', 'N'], description: 'Criar nova sessão ou aula' },
|
|
35
|
+
{ keys: ['Ctrl', 'C'], description: 'Copiar item selecionado' },
|
|
36
|
+
{ keys: ['Ctrl', 'V'], description: 'Colar no contexto atual' },
|
|
37
|
+
{ keys: ['Ctrl', 'D'], description: 'Duplicar item' },
|
|
38
|
+
{ keys: ['Delete'], description: 'Excluir item(ns) selecionado(s)' },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
heading: 'Busca & Interface',
|
|
43
|
+
items: [
|
|
44
|
+
{ keys: ['Ctrl', 'F'], description: 'Focar campo de busca' },
|
|
45
|
+
{ keys: ['Ctrl', '/'], description: 'Abrir ajuda de atalhos' },
|
|
46
|
+
{ keys: ['Esc'], description: 'Limpar busca / seleção / foco' },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// ── Component ─────────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
interface ShortcutsHelpProps {
|
|
54
|
+
open: boolean;
|
|
55
|
+
onOpenChange: (open: boolean) => void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function ShortcutsHelp({ open, onOpenChange }: ShortcutsHelpProps) {
|
|
59
|
+
return (
|
|
60
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
61
|
+
<DialogContent className="max-w-sm">
|
|
62
|
+
<DialogHeader>
|
|
63
|
+
<DialogTitle className="flex items-center gap-2">
|
|
64
|
+
<Keyboard className="size-4" />
|
|
65
|
+
Atalhos de teclado
|
|
66
|
+
</DialogTitle>
|
|
67
|
+
</DialogHeader>
|
|
68
|
+
|
|
69
|
+
<div className="flex flex-col gap-4 mt-1">
|
|
70
|
+
{SHORTCUT_GROUPS.map((group, gi) => (
|
|
71
|
+
<div key={gi}>
|
|
72
|
+
<p className="text-[0.65rem] font-semibold uppercase tracking-wider text-muted-foreground mb-2">
|
|
73
|
+
{group.heading}
|
|
74
|
+
</p>
|
|
75
|
+
<div className="flex flex-col gap-1.5">
|
|
76
|
+
{group.items.map(({ keys, description }, ki) => (
|
|
77
|
+
<div
|
|
78
|
+
key={ki}
|
|
79
|
+
className="flex items-center justify-between gap-3"
|
|
80
|
+
>
|
|
81
|
+
<span className="text-sm text-muted-foreground">
|
|
82
|
+
{description}
|
|
83
|
+
</span>
|
|
84
|
+
<div className="flex items-center gap-1 shrink-0">
|
|
85
|
+
{keys.map((k, i) => (
|
|
86
|
+
<Badge
|
|
87
|
+
key={i}
|
|
88
|
+
variant="outline"
|
|
89
|
+
className="h-5 px-1.5 text-[0.65rem] font-mono font-medium"
|
|
90
|
+
>
|
|
91
|
+
{k}
|
|
92
|
+
</Badge>
|
|
93
|
+
))}
|
|
94
|
+
</div>
|
|
95
|
+
</div>
|
|
96
|
+
))}
|
|
97
|
+
</div>
|
|
98
|
+
{gi < SHORTCUT_GROUPS.length - 1 && (
|
|
99
|
+
<Separator className="mt-3" />
|
|
100
|
+
)}
|
|
101
|
+
</div>
|
|
102
|
+
))}
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<p className="text-[0.65rem] text-muted-foreground mt-1">
|
|
106
|
+
Atalhos são desativados quando o foco está em inputs, exceto Ctrl+S,
|
|
107
|
+
Ctrl+F e Ctrl+/ que funcionam em qualquer contexto.
|
|
108
|
+
</p>
|
|
109
|
+
</DialogContent>
|
|
110
|
+
</Dialog>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Convenience trigger button — renders standalone when no external state is needed.
|
|
116
|
+
*/
|
|
117
|
+
export function ShortcutsHelpTrigger({ onOpen }: { onOpen: () => void }) {
|
|
118
|
+
return (
|
|
119
|
+
<Button
|
|
120
|
+
variant="ghost"
|
|
121
|
+
size="sm"
|
|
122
|
+
className="h-8 gap-1.5 text-xs text-muted-foreground hover:text-foreground"
|
|
123
|
+
onClick={onOpen}
|
|
124
|
+
title="Atalhos de teclado (Ctrl+/)"
|
|
125
|
+
>
|
|
126
|
+
<Keyboard className="size-3.5" />
|
|
127
|
+
Atalhos
|
|
128
|
+
<Badge
|
|
129
|
+
variant="outline"
|
|
130
|
+
className="h-4 px-1 text-[0.6rem] font-mono ml-0.5"
|
|
131
|
+
>
|
|
132
|
+
Ctrl+/
|
|
133
|
+
</Badge>
|
|
134
|
+
</Button>
|
|
135
|
+
);
|
|
136
|
+
}
|