@hed-hog/core 0.0.214 → 0.0.216

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 (37) hide show
  1. package/hedhog/data/dashboard_item.yaml +9 -9
  2. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +5 -5
  3. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +1 -1
  4. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +312 -0
  5. package/hedhog/frontend/app/dashboard/components/dashboard-grid.tsx.ejs +54 -0
  6. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +132 -0
  7. package/hedhog/frontend/app/dashboard/components/dynamic-widget.tsx.ejs +88 -0
  8. package/hedhog/frontend/app/dashboard/components/index.ts.ejs +6 -0
  9. package/hedhog/frontend/app/dashboard/components/stats.tsx.ejs +93 -0
  10. package/hedhog/frontend/app/dashboard/components/widget-wrapper.tsx.ejs +150 -0
  11. package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +184 -0
  12. package/hedhog/frontend/app/dashboard/components/widgets/active-users-card.tsx.ejs +58 -0
  13. package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +219 -0
  14. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +191 -0
  15. package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +309 -0
  16. package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +111 -0
  17. package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +445 -0
  18. package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-card.tsx.ejs +58 -0
  19. package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-chart.tsx.ejs +149 -0
  20. package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +296 -0
  21. package/hedhog/frontend/app/dashboard/components/widgets/permissions-card.tsx.ejs +61 -0
  22. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +152 -0
  23. package/hedhog/frontend/app/dashboard/components/widgets/profile-card.tsx.ejs +186 -0
  24. package/hedhog/frontend/app/dashboard/components/widgets/session-activity-chart.tsx.ejs +183 -0
  25. package/hedhog/frontend/app/dashboard/components/widgets/sessions-today-card.tsx.ejs +62 -0
  26. package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +57 -0
  27. package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +57 -0
  28. package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +57 -0
  29. package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +57 -0
  30. package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +340 -0
  31. package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +275 -0
  32. package/hedhog/frontend/app/dashboard/components/widgets/user-growth-chart.tsx.ejs +210 -0
  33. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +130 -0
  34. package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +233 -0
  35. package/hedhog/frontend/messages/en.json +143 -1
  36. package/hedhog/frontend/messages/pt.json +143 -1
  37. package/package.json +3 -3
@@ -207,10 +207,10 @@
207
207
  dashboard_id:
208
208
  where:
209
209
  slug: config
210
- width: 6
211
- height: 4
212
- x_axis: 6
213
- y_axis: 0
210
+ width: 12
211
+ height: 6
212
+ x_axis: 0
213
+ y_axis: 9
214
214
 
215
215
  - component_id:
216
216
  where:
@@ -219,7 +219,7 @@
219
219
  where:
220
220
  slug: config
221
221
  width: 6
222
- height: 4
222
+ height: 5
223
223
  x_axis: 0
224
224
  y_axis: 4
225
225
 
@@ -232,7 +232,7 @@
232
232
  width: 6
233
233
  height: 4
234
234
  x_axis: 6
235
- y_axis: 4
235
+ y_axis: 0
236
236
 
237
237
  - component_id:
238
238
  where:
@@ -241,6 +241,6 @@
241
241
  where:
242
242
  slug: config
243
243
  width: 6
244
- height: 4
245
- x_axis: 0
246
- y_axis: 8
244
+ height: 5
245
+ x_axis: 6
246
+ y_axis: 4
@@ -1,10 +1,5 @@
1
1
  'use client';
2
2
 
3
- import {
4
- AddWidgetSelectorDialog,
5
- DraggableGrid,
6
- LayoutItem,
7
- } from '@/components/dashboard';
8
3
  import {
9
4
  Breadcrumb,
10
5
  BreadcrumbItem,
@@ -22,6 +17,11 @@ import { IconDeviceFloppy } from '@tabler/icons-react';
22
17
  import { useTranslations } from 'next-intl';
23
18
  import { useRouter } from 'next/navigation';
24
19
  import { useCallback, useEffect, useState } from 'react';
20
+ import {
21
+ AddWidgetSelectorDialog,
22
+ DraggableGrid,
23
+ LayoutItem,
24
+ } from '../components';
25
25
  import '../dashboard.css';
26
26
  import {
27
27
  DashboardAccessResponse,
@@ -1,8 +1,8 @@
1
1
  'use client';
2
2
 
3
- import { DynamicWidget } from '@/components/dashboard';
4
3
  import { Skeleton } from '@/components/ui/skeleton';
5
4
  import { useEffect, useState } from 'react';
5
+ import { DynamicWidget } from '../components';
6
6
  import { WidgetRendererProps } from './types';
7
7
 
8
8
  export const WidgetRenderer = ({ widget, onRemove }: WidgetRendererProps) => {
@@ -0,0 +1,312 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Card,
6
+ CardDescription,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from '@/components/ui/card';
10
+ import {
11
+ Dialog,
12
+ DialogContent,
13
+ DialogDescription,
14
+ DialogFooter,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ } from '@/components/ui/dialog';
18
+ import {
19
+ DropdownMenu,
20
+ DropdownMenuContent,
21
+ DropdownMenuItem,
22
+ DropdownMenuTrigger,
23
+ } from '@/components/ui/dropdown-menu';
24
+ import { ScrollArea } from '@/components/ui/scroll-area';
25
+ import { Skeleton } from '@/components/ui/skeleton';
26
+ import { cn } from '@/lib/utils';
27
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
28
+ import {
29
+ IconArrowsRightLeft,
30
+ IconPlus,
31
+ IconSettings,
32
+ } from '@tabler/icons-react';
33
+ import { useTranslations } from 'next-intl';
34
+ import { useRouter } from 'next/navigation';
35
+ import { useState } from 'react';
36
+
37
+ interface DashboardComponent {
38
+ id: number;
39
+ slug: string;
40
+ path: string;
41
+ min_width: number;
42
+ max_width?: number;
43
+ min_height: number;
44
+ max_height?: number;
45
+ width: number;
46
+ height: number;
47
+ is_resizable: boolean;
48
+ dashboard_component_locale?: Array<{
49
+ name: string;
50
+ locale: {
51
+ code: string;
52
+ };
53
+ }>;
54
+ }
55
+
56
+ interface Dashboard {
57
+ id: number;
58
+ slug: string;
59
+ dashboard_locale?: Array<{
60
+ name: string;
61
+ locale: {
62
+ code: string;
63
+ };
64
+ }>;
65
+ }
66
+
67
+ interface AddWidgetSelectorDialogProps {
68
+ availableComponents: DashboardComponent[];
69
+ isLoading: boolean;
70
+ onAdd: (slug: string) => void;
71
+ currentSlug?: string;
72
+ }
73
+
74
+ export function AddWidgetSelectorDialog({
75
+ availableComponents,
76
+ isLoading,
77
+ onAdd,
78
+ currentSlug = 'default',
79
+ }: AddWidgetSelectorDialogProps) {
80
+ const tWidget = useTranslations('core.AddWidgetDialog');
81
+ const tMenu = useTranslations('core.DashboardMenu');
82
+ const { request } = useApp();
83
+ const router = useRouter();
84
+
85
+ const [openWidgets, setOpenWidgets] = useState(false);
86
+ const [openDashboards, setOpenDashboards] = useState(false);
87
+ const [selectedWidget, setSelectedWidget] = useState<string | null>(null);
88
+ const [selectedDashboard, setSelectedDashboard] = useState<string | null>(
89
+ null
90
+ );
91
+
92
+ // Buscar dashboards disponíveis para o usuário
93
+ const { data: userDashboards, isLoading: isLoadingDashboards } = useQuery<
94
+ Dashboard[]
95
+ >({
96
+ queryKey: ['user-dashboards'],
97
+ queryFn: async () => {
98
+ const { data } = await request<Dashboard[]>({
99
+ url: '/dashboard-core/user-dashboards',
100
+ method: 'GET',
101
+ });
102
+ return data;
103
+ },
104
+ });
105
+
106
+ const handleAdd = () => {
107
+ if (selectedWidget) {
108
+ onAdd(selectedWidget);
109
+ setSelectedWidget(null);
110
+ setOpenWidgets(false);
111
+ }
112
+ };
113
+
114
+ const handleSwitchDashboard = () => {
115
+ if (selectedDashboard && selectedDashboard !== currentSlug) {
116
+ router.push(`/core/dashboard/${selectedDashboard}`);
117
+ setOpenDashboards(false);
118
+ }
119
+ };
120
+
121
+ return (
122
+ <>
123
+ <DropdownMenu>
124
+ <DropdownMenuTrigger asChild>
125
+ <Button size="sm" className="gap-2" variant="outline">
126
+ <IconSettings className="size-4" />
127
+ </Button>
128
+ </DropdownMenuTrigger>
129
+ <DropdownMenuContent align="end">
130
+ <DropdownMenuItem onClick={() => setOpenWidgets(true)}>
131
+ <IconPlus className="mr-2 size-4" />
132
+ {tMenu('addWidgets')}
133
+ </DropdownMenuItem>
134
+ <DropdownMenuItem onClick={() => setOpenDashboards(true)}>
135
+ <IconArrowsRightLeft className="mr-2 size-4" />
136
+ {tMenu('switchDashboard')}
137
+ </DropdownMenuItem>
138
+ </DropdownMenuContent>
139
+ </DropdownMenu>
140
+
141
+ {/* Diálogo de Adicionar Widgets */}
142
+ <Dialog open={openWidgets} onOpenChange={setOpenWidgets}>
143
+ <DialogContent className="sm:max-w-[600px]">
144
+ <DialogHeader>
145
+ <DialogTitle>{tWidget('title')}</DialogTitle>
146
+ <DialogDescription>{tWidget('description')}</DialogDescription>
147
+ </DialogHeader>
148
+ <ScrollArea className="max-h-[400px] pr-4">
149
+ {isLoading ? (
150
+ <div className="grid gap-3">
151
+ <Skeleton className="h-24 w-full" />
152
+ <Skeleton className="h-24 w-full" />
153
+ <Skeleton className="h-24 w-full" />
154
+ </div>
155
+ ) : (
156
+ <div className="grid gap-3">
157
+ {availableComponents.length === 0 ? (
158
+ <div className="flex min-h-[200px] flex-col items-center justify-center text-center">
159
+ <p className="text-muted-foreground text-sm">
160
+ {tWidget('noComponentsAvailable')}
161
+ </p>
162
+ </div>
163
+ ) : (
164
+ availableComponents.map((component) => {
165
+ const name =
166
+ component.dashboard_component_locale?.[0]?.name ||
167
+ component.slug;
168
+ return (
169
+ <Card
170
+ key={component.slug}
171
+ className={cn(
172
+ 'cursor-pointer py-4 transition-colors hover:bg-accent',
173
+ selectedWidget === component.slug &&
174
+ 'border-primary bg-accent'
175
+ )}
176
+ onClick={() => setSelectedWidget(component.slug)}
177
+ >
178
+ <CardHeader>
179
+ <div className="flex items-start gap-3">
180
+ <div className="bg-primary/10 flex size-10 items-center justify-center rounded-lg">
181
+ <IconPlus className="text-primary size-5" />
182
+ </div>
183
+ <div className="flex-1">
184
+ <CardTitle className="text-base">
185
+ {name}
186
+ </CardTitle>
187
+ <CardDescription className="text-sm">
188
+ {tWidget('dimensions')}: {component.width}x
189
+ {component.height} |
190
+ {component.is_resizable
191
+ ? ` ${tWidget('resizable')}`
192
+ : ` ${tWidget('fixedSize')}`}
193
+ </CardDescription>
194
+ </div>
195
+ </div>
196
+ </CardHeader>
197
+ </Card>
198
+ );
199
+ })
200
+ )}
201
+ </div>
202
+ )}
203
+ </ScrollArea>
204
+ <DialogFooter>
205
+ <Button
206
+ type="button"
207
+ variant="outline"
208
+ onClick={() => setOpenWidgets(false)}
209
+ >
210
+ {tWidget('cancel')}
211
+ </Button>
212
+ <Button
213
+ type="button"
214
+ onClick={handleAdd}
215
+ disabled={!selectedWidget || isLoading}
216
+ >
217
+ {tWidget('add')}
218
+ </Button>
219
+ </DialogFooter>
220
+ </DialogContent>
221
+ </Dialog>
222
+
223
+ {/* Diálogo de Trocar Dashboard */}
224
+ <Dialog open={openDashboards} onOpenChange={setOpenDashboards}>
225
+ <DialogContent className="sm:max-w-[600px]">
226
+ <DialogHeader>
227
+ <DialogTitle>{tMenu('selectDashboardTitle')}</DialogTitle>
228
+ <DialogDescription>
229
+ {tMenu('selectDashboardDescription')}
230
+ </DialogDescription>
231
+ </DialogHeader>
232
+ <ScrollArea className="max-h-[400px] pr-4">
233
+ {isLoadingDashboards ? (
234
+ <div className="grid gap-3">
235
+ <Skeleton className="h-24 w-full" />
236
+ <Skeleton className="h-24 w-full" />
237
+ <Skeleton className="h-24 w-full" />
238
+ </div>
239
+ ) : (
240
+ <div className="grid gap-3">
241
+ {!userDashboards || userDashboards.length === 0 ? (
242
+ <div className="flex min-h-[200px] flex-col items-center justify-center text-center">
243
+ <p className="text-muted-foreground text-sm">
244
+ {tMenu('noDashboardsAvailable')}
245
+ </p>
246
+ </div>
247
+ ) : (
248
+ userDashboards.map((dashboard) => {
249
+ const name =
250
+ dashboard.dashboard_locale?.[0]?.name || dashboard.slug;
251
+ const isCurrent = dashboard.slug === currentSlug;
252
+ return (
253
+ <Card
254
+ key={dashboard.slug}
255
+ className={cn(
256
+ 'cursor-pointer transition-colors py-4 hover:bg-accent',
257
+ selectedDashboard === dashboard.slug &&
258
+ 'border-primary bg-accent',
259
+ isCurrent && 'opacity-50'
260
+ )}
261
+ onClick={() =>
262
+ !isCurrent && setSelectedDashboard(dashboard.slug)
263
+ }
264
+ >
265
+ <CardHeader>
266
+ <div className="flex items-start gap-3">
267
+ <div className="bg-primary/10 flex size-10 items-center justify-center rounded-lg">
268
+ <IconArrowsRightLeft className="text-primary size-5" />
269
+ </div>
270
+ <div className="flex-1">
271
+ <CardTitle className="text-base">
272
+ {name}
273
+ {isCurrent && ' (atual)'}
274
+ </CardTitle>
275
+ <CardDescription className="text-sm">
276
+ {dashboard.slug}
277
+ </CardDescription>
278
+ </div>
279
+ </div>
280
+ </CardHeader>
281
+ </Card>
282
+ );
283
+ })
284
+ )}
285
+ </div>
286
+ )}
287
+ </ScrollArea>
288
+ <DialogFooter>
289
+ <Button
290
+ type="button"
291
+ variant="outline"
292
+ onClick={() => setOpenDashboards(false)}
293
+ >
294
+ {tWidget('cancel')}
295
+ </Button>
296
+ <Button
297
+ type="button"
298
+ onClick={handleSwitchDashboard}
299
+ disabled={
300
+ !selectedDashboard ||
301
+ isLoadingDashboards ||
302
+ selectedDashboard === currentSlug
303
+ }
304
+ >
305
+ {tMenu('switch')}
306
+ </Button>
307
+ </DialogFooter>
308
+ </DialogContent>
309
+ </Dialog>
310
+ </>
311
+ );
312
+ }
@@ -0,0 +1,54 @@
1
+ 'use client';
2
+
3
+ import { cn } from '@/lib/utils';
4
+ import { ReactNode } from 'react';
5
+
6
+ interface DashboardGridProps {
7
+ children: ReactNode;
8
+ className?: string;
9
+ }
10
+
11
+ export function DashboardGrid({ children, className }: DashboardGridProps) {
12
+ return (
13
+ <div
14
+ className={cn(
15
+ 'grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4',
16
+ className
17
+ )}
18
+ >
19
+ {children}
20
+ </div>
21
+ );
22
+ }
23
+
24
+ interface DashboardGridItemProps {
25
+ children: ReactNode;
26
+ className?: string;
27
+ colSpan?: 1 | 2 | 3 | 4;
28
+ rowSpan?: 1 | 2 | 3 | 4;
29
+ }
30
+
31
+ export function DashboardGridItem({
32
+ children,
33
+ className,
34
+ colSpan = 1,
35
+ rowSpan = 1,
36
+ }: DashboardGridItemProps) {
37
+ const colSpanClass = {
38
+ 1: 'col-span-1',
39
+ 2: 'md:col-span-2',
40
+ 3: 'lg:col-span-3',
41
+ 4: 'xl:col-span-4',
42
+ }[colSpan];
43
+
44
+ const rowSpanClass = {
45
+ 1: 'row-span-1',
46
+ 2: 'md:row-span-2',
47
+ 3: 'lg:row-span-3',
48
+ 4: 'xl:row-span-4',
49
+ }[rowSpan];
50
+
51
+ return (
52
+ <div className={cn(colSpanClass, rowSpanClass, className)}>{children}</div>
53
+ );
54
+ }
@@ -0,0 +1,132 @@
1
+ 'use client';
2
+
3
+ import { ReactNode, useEffect, useRef, useState } from 'react';
4
+ import GridLayout, { Layout as RGLLayout } from 'react-grid-layout';
5
+ import 'react-grid-layout/css/styles.css';
6
+ import 'react-resizable/css/styles.css';
7
+
8
+ export interface LayoutItem {
9
+ i: string;
10
+ x: number;
11
+ y: number;
12
+ w: number;
13
+ h: number;
14
+ minW?: number;
15
+ maxW?: number;
16
+ minH?: number;
17
+ maxH?: number;
18
+ static?: boolean;
19
+ }
20
+
21
+ export type Layout = LayoutItem[];
22
+
23
+ interface DraggableGridProps {
24
+ layout: Layout;
25
+ onLayoutChange: (layout: Layout) => void;
26
+ children: ReactNode;
27
+ className?: string;
28
+ cols?: number;
29
+ rowHeight?: number;
30
+ isDraggable?: boolean;
31
+ isResizable?: boolean;
32
+ compactType?: 'vertical' | 'horizontal' | null;
33
+ preventCollision?: boolean;
34
+ margin?: [number, number];
35
+ containerPadding?: [number, number];
36
+ }
37
+
38
+ export function DraggableGrid({
39
+ layout,
40
+ onLayoutChange,
41
+ children,
42
+ className = '',
43
+ cols = 12,
44
+ rowHeight = 100,
45
+ isDraggable = true,
46
+ isResizable = true,
47
+ compactType = 'vertical',
48
+ preventCollision = false,
49
+ margin = [16, 16],
50
+ containerPadding = [0, 0],
51
+ }: DraggableGridProps) {
52
+ const containerRef = useRef<HTMLDivElement>(null);
53
+ const [containerWidth, setContainerWidth] = useState(1200);
54
+
55
+ useEffect(() => {
56
+ const updateWidth = () => {
57
+ if (containerRef.current) {
58
+ setContainerWidth(containerRef.current.offsetWidth);
59
+ }
60
+ };
61
+
62
+ updateWidth();
63
+ const resizeObserver = new ResizeObserver(updateWidth);
64
+ if (containerRef.current) {
65
+ resizeObserver.observe(containerRef.current);
66
+ }
67
+
68
+ return () => {
69
+ resizeObserver.disconnect();
70
+ };
71
+ }, []);
72
+
73
+ const handleLayoutChange = (newLayout: RGLLayout) => {
74
+ const layouts = Array.isArray(newLayout) ? newLayout : [newLayout];
75
+ const convertedLayout = layouts.map((item: any) => ({
76
+ i: item.i,
77
+ x: item.x,
78
+ y: item.y,
79
+ w: item.w,
80
+ h: item.h,
81
+ minW: item.minW,
82
+ maxW: item.maxW,
83
+ minH: item.minH,
84
+ maxH: item.maxH,
85
+ static: item.static,
86
+ })) as LayoutItem[];
87
+ onLayoutChange(convertedLayout);
88
+ };
89
+
90
+ return (
91
+ <div ref={containerRef} className="w-full">
92
+ <GridLayout
93
+ className={`layout ${className}`}
94
+ layout={layout}
95
+ cols={cols}
96
+ rowHeight={rowHeight}
97
+ width={containerWidth}
98
+ isDraggable={isDraggable}
99
+ isResizable={isResizable}
100
+ compactType={compactType}
101
+ preventCollision={preventCollision}
102
+ margin={margin}
103
+ containerPadding={containerPadding}
104
+ onLayoutChange={handleLayoutChange}
105
+ useCSSTransforms={true}
106
+ draggableHandle=".drag-handle"
107
+ resizeHandles={['se', 's', 'e', 'sw', 'w', 'n', 'ne', 'nw']}
108
+ draggableCancel="button,.no-drag"
109
+ >
110
+ {children}
111
+ </GridLayout>
112
+ </div>
113
+ );
114
+ }
115
+
116
+ interface DraggableGridItemProps {
117
+ id: string;
118
+ children: ReactNode;
119
+ className?: string;
120
+ }
121
+
122
+ export function DraggableGridItem({
123
+ id,
124
+ children,
125
+ className = '',
126
+ }: DraggableGridItemProps) {
127
+ return (
128
+ <div key={id} className={className} style={{ position: 'relative' }}>
129
+ {children}
130
+ </div>
131
+ );
132
+ }
@@ -0,0 +1,88 @@
1
+ 'use client';
2
+
3
+ import { Button } from '@/components/ui/button';
4
+ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
5
+ import { cn } from '@/lib/utils';
6
+ import * as TablerIcons from '@tabler/icons-react';
7
+ import { IconGripVertical, IconX } from '@tabler/icons-react';
8
+ import { ComponentType } from 'react';
9
+
10
+ interface DynamicWidgetProps {
11
+ title: string;
12
+ value: string | number;
13
+ description?: string;
14
+ iconName?: string;
15
+ color?: string;
16
+ draggable?: boolean;
17
+ onRemove?: () => void;
18
+ className?: string;
19
+ }
20
+
21
+ export function DynamicWidget({
22
+ title,
23
+ value,
24
+ description,
25
+ iconName,
26
+ color,
27
+ draggable = false,
28
+ onRemove,
29
+ className,
30
+ }: DynamicWidgetProps) {
31
+ // Dynamically get icon from Tabler Icons
32
+ const Icon: ComponentType<{ className?: string }> | null = iconName
33
+ ? (TablerIcons as any)[iconName] || null
34
+ : null;
35
+
36
+ return (
37
+ <Card className={cn('h-full flex flex-col', className)}>
38
+ <CardHeader
39
+ className={cn(
40
+ 'flex flex-row items-center justify-between space-y-0 pb-2',
41
+ draggable && 'drag-handle'
42
+ )}
43
+ style={draggable ? { cursor: 'grab', userSelect: 'none' } : undefined}
44
+ onMouseDown={(e) =>
45
+ draggable && (e.currentTarget.style.cursor = 'grabbing')
46
+ }
47
+ onMouseUp={(e) => draggable && (e.currentTarget.style.cursor = 'grab')}
48
+ >
49
+ <div className="flex items-center gap-2">
50
+ {draggable && (
51
+ <IconGripVertical className="text-muted-foreground size-4 shrink-0" />
52
+ )}
53
+ <CardTitle className="text-md font-medium">{title}</CardTitle>
54
+ </div>
55
+ <div className="flex items-center gap-2">
56
+ {onRemove && (
57
+ <Button
58
+ variant="ghost"
59
+ size="icon"
60
+ className="size-6 shrink-0 no-drag"
61
+ onClick={(e) => {
62
+ e.stopPropagation();
63
+ e.preventDefault();
64
+ onRemove();
65
+ }}
66
+ >
67
+ <IconX className="size-3" />
68
+ </Button>
69
+ )}
70
+ </div>
71
+ </CardHeader>
72
+ <CardContent className="flex-1">
73
+ <div className="text-2xl font-bold">{value}</div>
74
+ {description && (
75
+ <p className="text-muted-foreground text-xs mt-1">{description}</p>
76
+ )}
77
+ {Icon && (
78
+ <Icon
79
+ className={cn(
80
+ 'size-6 absolute bottom-4 right-4 shrink-0',
81
+ color ? `text-${color}` : 'text-muted-foreground'
82
+ )}
83
+ />
84
+ )}
85
+ </CardContent>
86
+ </Card>
87
+ );
88
+ }
@@ -0,0 +1,6 @@
1
+ export { AddWidgetSelectorDialog } from './add-widget-selector-dialog';
2
+ export { DashboardGrid, DashboardGridItem } from './dashboard-grid';
3
+ export { DraggableGrid, DraggableGridItem } from './draggable-grid';
4
+ export type { Layout, LayoutItem } from './draggable-grid';
5
+ export { DynamicWidget } from './dynamic-widget';
6
+ export { WidgetWrapper } from './widget-wrapper';