@hed-hog/core 0.0.215 → 0.0.217

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/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +5 -5
  2. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +1 -1
  3. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +312 -0
  4. package/hedhog/frontend/app/dashboard/components/dashboard-grid.tsx.ejs +54 -0
  5. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +132 -0
  6. package/hedhog/frontend/app/dashboard/components/dynamic-widget.tsx.ejs +88 -0
  7. package/hedhog/frontend/app/dashboard/components/index.ts.ejs +6 -0
  8. package/hedhog/frontend/app/dashboard/components/stats.tsx.ejs +93 -0
  9. package/hedhog/frontend/app/dashboard/components/widget-wrapper.tsx.ejs +150 -0
  10. package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +184 -0
  11. package/hedhog/frontend/app/dashboard/components/widgets/active-users-card.tsx.ejs +58 -0
  12. package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +219 -0
  13. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +191 -0
  14. package/hedhog/frontend/app/dashboard/components/widgets/locale-config.tsx.ejs +309 -0
  15. package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +111 -0
  16. package/hedhog/frontend/app/dashboard/components/widgets/mail-config.tsx.ejs +445 -0
  17. package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-card.tsx.ejs +58 -0
  18. package/hedhog/frontend/app/dashboard/components/widgets/mail-sent-chart.tsx.ejs +149 -0
  19. package/hedhog/frontend/app/dashboard/components/widgets/oauth-config.tsx.ejs +296 -0
  20. package/hedhog/frontend/app/dashboard/components/widgets/permissions-card.tsx.ejs +61 -0
  21. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +152 -0
  22. package/hedhog/frontend/app/dashboard/components/widgets/profile-card.tsx.ejs +186 -0
  23. package/hedhog/frontend/app/dashboard/components/widgets/session-activity-chart.tsx.ejs +183 -0
  24. package/hedhog/frontend/app/dashboard/components/widgets/sessions-today-card.tsx.ejs +62 -0
  25. package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +57 -0
  26. package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +57 -0
  27. package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +57 -0
  28. package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +57 -0
  29. package/hedhog/frontend/app/dashboard/components/widgets/storage-config.tsx.ejs +340 -0
  30. package/hedhog/frontend/app/dashboard/components/widgets/theme-config.tsx.ejs +275 -0
  31. package/hedhog/frontend/app/dashboard/components/widgets/user-growth-chart.tsx.ejs +210 -0
  32. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +130 -0
  33. package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +233 -0
  34. package/hedhog/frontend/messages/en.json +143 -1
  35. package/hedhog/frontend/messages/pt.json +143 -1
  36. package/hedhog/table/dashboard_user.yaml +2 -10
  37. package/package.json +2 -2
@@ -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';