@hed-hog/core 0.0.299 → 0.0.300

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 (112) hide show
  1. package/dist/dashboard/dashboard/dashboard.controller.d.ts +6 -0
  2. package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
  3. package/dist/dashboard/dashboard/dashboard.service.d.ts +6 -0
  4. package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
  5. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +2 -1
  6. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts.map +1 -1
  7. package/dist/dashboard/dashboard-component/dashboard-component.controller.js +6 -3
  8. package/dist/dashboard/dashboard-component/dashboard-component.controller.js.map +1 -1
  9. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts +7 -1
  10. package/dist/dashboard/dashboard-component/dashboard-component.service.d.ts.map +1 -1
  11. package/dist/dashboard/dashboard-component/dashboard-component.service.js +76 -33
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
  13. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +65 -0
  14. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  15. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +111 -0
  16. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  17. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +69 -0
  18. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  19. package/dist/dashboard/dashboard-core/dashboard-core.service.js +526 -19
  20. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  21. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +2 -0
  22. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
  23. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +2 -0
  24. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
  25. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +2 -0
  26. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts.map +1 -1
  27. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +2 -0
  28. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts.map +1 -1
  29. package/hedhog/data/dashboard.yaml +12 -6
  30. package/hedhog/data/dashboard_component_role.yaml +66 -0
  31. package/hedhog/data/dashboard_role.yaml +2 -8
  32. package/hedhog/data/route.yaml +72 -0
  33. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +333 -128
  34. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +277 -53
  35. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +179 -231
  36. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +64 -18
  37. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +1389 -0
  38. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +37 -0
  39. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +1 -1
  40. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +6 -6
  41. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +8 -8
  42. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +3 -3
  43. package/hedhog/frontend/app/dashboard/page.tsx.ejs +3 -25
  44. package/hedhog/frontend/messages/en.json +112 -2
  45. package/hedhog/frontend/messages/pt.json +111 -1
  46. package/hedhog/frontend/widgets/account-security.tsx.ejs +1 -1
  47. package/hedhog/frontend/widgets/active-users-card.tsx.ejs +2 -2
  48. package/hedhog/frontend/widgets/activity-timeline.tsx.ejs +1 -1
  49. package/hedhog/frontend/widgets/email-notifications.tsx.ejs +1 -1
  50. package/hedhog/frontend/widgets/locale-config.tsx.ejs +1 -1
  51. package/hedhog/frontend/widgets/login-history-chart.tsx.ejs +1 -1
  52. package/hedhog/frontend/widgets/mail-config.tsx.ejs +1 -1
  53. package/hedhog/frontend/widgets/mail-sent-card.tsx.ejs +2 -2
  54. package/hedhog/frontend/widgets/mail-sent-chart.tsx.ejs +1 -1
  55. package/hedhog/frontend/widgets/menus-card.tsx.ejs +2 -2
  56. package/hedhog/frontend/widgets/oauth-config.tsx.ejs +1 -1
  57. package/hedhog/frontend/widgets/permissions-card.tsx.ejs +2 -2
  58. package/hedhog/frontend/widgets/permissions-chart.tsx.ejs +1 -1
  59. package/hedhog/frontend/widgets/profile-card.tsx.ejs +1 -1
  60. package/hedhog/frontend/widgets/routes-card.tsx.ejs +2 -2
  61. package/hedhog/frontend/widgets/session-activity-chart.tsx.ejs +1 -1
  62. package/hedhog/frontend/widgets/sessions-today-card.tsx.ejs +2 -2
  63. package/hedhog/frontend/widgets/stat-access-level.tsx.ejs +1 -1
  64. package/hedhog/frontend/widgets/stat-actions-today.tsx.ejs +1 -1
  65. package/hedhog/frontend/widgets/stat-consecutive-days.tsx.ejs +1 -1
  66. package/hedhog/frontend/widgets/stat-online-time.tsx.ejs +1 -1
  67. package/hedhog/frontend/widgets/storage-config.tsx.ejs +1 -1
  68. package/hedhog/frontend/widgets/theme-config.tsx.ejs +1 -1
  69. package/hedhog/frontend/widgets/user-growth-chart.tsx.ejs +1 -1
  70. package/hedhog/frontend/widgets/user-roles.tsx.ejs +1 -1
  71. package/hedhog/frontend/widgets/user-sessions.tsx.ejs +1 -1
  72. package/hedhog/table/dashboard.yaml +6 -0
  73. package/package.json +5 -5
  74. package/src/dashboard/dashboard-component/dashboard-component.controller.ts +15 -2
  75. package/src/dashboard/dashboard-component/dashboard-component.service.ts +107 -43
  76. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +112 -1
  77. package/src/dashboard/dashboard-core/dashboard-core.service.ts +674 -19
  78. package/hedhog/frontend/app/dashboard/components/widgets/core..gitkeep.ejs +0 -11
  79. package/hedhog/frontend/app/dashboard/components/widgets/core.account-security.tsx.ejs +0 -192
  80. package/hedhog/frontend/app/dashboard/components/widgets/core.active-users-card.tsx.ejs +0 -58
  81. package/hedhog/frontend/app/dashboard/components/widgets/core.activity-timeline.tsx.ejs +0 -223
  82. package/hedhog/frontend/app/dashboard/components/widgets/core.email-notifications.tsx.ejs +0 -226
  83. package/hedhog/frontend/app/dashboard/components/widgets/core.locale-config.tsx.ejs +0 -168
  84. package/hedhog/frontend/app/dashboard/components/widgets/core.login-history-chart.tsx.ejs +0 -115
  85. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-config.tsx.ejs +0 -199
  86. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-sent-card.tsx.ejs +0 -58
  87. package/hedhog/frontend/app/dashboard/components/widgets/core.mail-sent-chart.tsx.ejs +0 -149
  88. package/hedhog/frontend/app/dashboard/components/widgets/core.menus-card.tsx.ejs +0 -58
  89. package/hedhog/frontend/app/dashboard/components/widgets/core.oauth-config.tsx.ejs +0 -175
  90. package/hedhog/frontend/app/dashboard/components/widgets/core.permissions-card.tsx.ejs +0 -61
  91. package/hedhog/frontend/app/dashboard/components/widgets/core.permissions-chart.tsx.ejs +0 -156
  92. package/hedhog/frontend/app/dashboard/components/widgets/core.profile-card.tsx.ejs +0 -186
  93. package/hedhog/frontend/app/dashboard/components/widgets/core.routes-card.tsx.ejs +0 -58
  94. package/hedhog/frontend/app/dashboard/components/widgets/core.session-activity-chart.tsx.ejs +0 -183
  95. package/hedhog/frontend/app/dashboard/components/widgets/core.sessions-today-card.tsx.ejs +0 -62
  96. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-access-level.tsx.ejs +0 -57
  97. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-actions-today.tsx.ejs +0 -57
  98. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-consecutive-days.tsx.ejs +0 -57
  99. package/hedhog/frontend/app/dashboard/components/widgets/core.stat-online-time.tsx.ejs +0 -57
  100. package/hedhog/frontend/app/dashboard/components/widgets/core.storage-config.tsx.ejs +0 -196
  101. package/hedhog/frontend/app/dashboard/components/widgets/core.theme-config.tsx.ejs +0 -213
  102. package/hedhog/frontend/app/dashboard/components/widgets/core.user-growth-chart.tsx.ejs +0 -210
  103. package/hedhog/frontend/app/dashboard/components/widgets/core.user-roles.tsx.ejs +0 -132
  104. package/hedhog/frontend/app/dashboard/components/widgets/core.user-sessions.tsx.ejs +0 -236
  105. package/hedhog/frontend/app/dashboard/components/widgets/finance.alerts.tsx.ejs +0 -108
  106. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-balance-kpi.tsx.ejs +0 -66
  107. package/hedhog/frontend/app/dashboard/components/widgets/finance.cash-flow-chart.tsx.ejs +0 -122
  108. package/hedhog/frontend/app/dashboard/components/widgets/finance.default-kpi.tsx.ejs +0 -63
  109. package/hedhog/frontend/app/dashboard/components/widgets/finance.payable-30d-kpi.tsx.ejs +0 -73
  110. package/hedhog/frontend/app/dashboard/components/widgets/finance.receivable-30d-kpi.tsx.ejs +0 -73
  111. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-payable.tsx.ejs +0 -123
  112. package/hedhog/frontend/app/dashboard/components/widgets/finance.upcoming-receivable.tsx.ejs +0 -118
@@ -3,27 +3,17 @@
3
3
  import { PaginationFooter } from '@/components/entity-list/pagination-footer';
4
4
  import { Badge } from '@/components/ui/badge';
5
5
  import { Button } from '@/components/ui/button';
6
- import {
7
- Card,
8
- CardDescription,
9
- CardHeader,
10
- CardTitle,
11
- } from '@/components/ui/card';
12
- import {
13
- Dialog,
14
- DialogContent,
15
- DialogDescription,
16
- DialogFooter,
17
- DialogHeader,
18
- DialogTitle,
19
- } from '@/components/ui/dialog';
20
- import {
21
- DropdownMenu,
22
- DropdownMenuContent,
23
- DropdownMenuItem,
24
- DropdownMenuTrigger,
25
- } from '@/components/ui/dropdown-menu';
6
+ import { Card, CardDescription, CardTitle } from '@/components/ui/card';
7
+ import { Checkbox } from '@/components/ui/checkbox';
8
+ import { Input } from '@/components/ui/input';
26
9
  import { ScrollArea } from '@/components/ui/scroll-area';
10
+ import {
11
+ Select,
12
+ SelectContent,
13
+ SelectItem,
14
+ SelectTrigger,
15
+ SelectValue,
16
+ } from '@/components/ui/select';
27
17
  import {
28
18
  Sheet,
29
19
  SheetContent,
@@ -34,16 +24,9 @@ import {
34
24
  } from '@/components/ui/sheet';
35
25
  import { Skeleton } from '@/components/ui/skeleton';
36
26
  import { cn } from '@/lib/utils';
37
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
38
- import {
39
- IconArrowsRightLeft,
40
- IconLayoutDashboard,
41
- IconPlus,
42
- IconSettings,
43
- } from '@tabler/icons-react';
27
+ import { IconLayoutDashboard, IconPlus } from '@tabler/icons-react';
44
28
  import { useTranslations } from 'next-intl';
45
- import { useRouter } from 'next/navigation';
46
- import { useState } from 'react';
29
+ import { useEffect, useRef, useState } from 'react';
47
30
 
48
31
  interface DashboardComponent {
49
32
  id: number;
@@ -65,27 +48,24 @@ interface DashboardComponent {
65
48
  }>;
66
49
  }
67
50
 
68
- interface Dashboard {
69
- id: number;
70
- slug: string;
71
- dashboard_locale?: Array<{
72
- name: string;
73
- locale: {
74
- code: string;
75
- };
76
- }>;
77
- }
78
-
79
51
  interface AddWidgetSelectorDialogProps {
80
52
  availableComponents: DashboardComponent[];
81
53
  totalItems: number;
82
54
  currentPage: number;
83
55
  pageSize: number;
84
56
  isLoading: boolean;
57
+ searchQuery: string;
58
+ moduleFilter: string;
59
+ modules: string[];
60
+ onSearchQueryChange: (value: string) => void;
61
+ onModuleFilterChange: (value: string) => void;
85
62
  onPageChange: (page: number) => void;
86
63
  onPageSizeChange: (pageSize: number) => void;
87
64
  onAdd: (slugs: string[]) => Promise<void> | void;
88
- currentSlug?: string;
65
+ buttonLabel?: string;
66
+ buttonVariant?: 'default' | 'outline';
67
+ openSignal?: number;
68
+ onOpenSignalHandled?: () => void;
89
69
  }
90
70
 
91
71
  const getWidgetLookupKey = (slug: string, librarySlug?: string): string => {
@@ -104,6 +84,12 @@ const getWidgetPreviewSlug = (slug: string): string => {
104
84
  return parts[parts.length - 1] || slug;
105
85
  };
106
86
 
87
+ const formatModuleLabel = (moduleSlug?: string): string => {
88
+ const normalized = (moduleSlug || 'core').replace(/[._-]+/g, ' ').trim();
89
+
90
+ return normalized.replace(/\b\w/g, (char) => char.toUpperCase());
91
+ };
92
+
107
93
  function WidgetPreview({
108
94
  name,
109
95
  slug,
@@ -125,33 +111,27 @@ function WidgetPreview({
125
111
  const previewUrl = `/libraries/core/dashboard-previews/${previewSlug}.png`;
126
112
 
127
113
  return (
128
- <div className="h-44 overflow-hidden rounded-xl border bg-background p-3">
129
- <div className="flex h-full flex-col rounded-lg border border-dashed bg-muted/20 p-3">
114
+ <div className="overflow-hidden rounded-lg border bg-background/80 p-2">
115
+ <div className="flex items-center gap-2">
130
116
  <img
131
117
  src={previewUrl}
132
118
  alt={name}
133
- className="mb-2 h-full w-full rounded-md border object-cover"
134
- onError={(e) => {
135
- e.currentTarget.style.display = 'none';
119
+ className="h-16 w-24 shrink-0 rounded-md border object-cover"
120
+ onError={(event) => {
121
+ event.currentTarget.style.display = 'none';
136
122
  }}
137
123
  />
138
- <div className="mb-2 flex items-center gap-2">
139
- <div className="size-2 rounded-full bg-primary/70" />
140
- <div className="h-2.5 w-20 rounded bg-muted" />
141
- </div>
142
- <div className="h-3 w-2/3 rounded bg-muted" />
143
- <div className="mt-2 h-2.5 w-1/2 rounded bg-muted" />
144
- <div className="mt-auto flex items-center gap-2">
145
- <Badge variant="secondary" className="text-[10px]">
146
- {width}x{height}
147
- </Badge>
148
- <Badge variant="outline" className="text-[10px]">
149
- {isResizable ? resizableLabel : fixedLabel}
150
- </Badge>
124
+ <div className="min-w-0 flex-1">
125
+ <div className="mb-1.5 flex flex-wrap items-center gap-1.5">
126
+ <Badge variant="secondary" className="px-1.5 py-0 text-[10px]">
127
+ {width}x{height}
128
+ </Badge>
129
+ <Badge variant="outline" className="px-1.5 py-0 text-[10px]">
130
+ {isResizable ? resizableLabel : fixedLabel}
131
+ </Badge>
132
+ </div>
133
+ <p className="truncate text-[10px] text-muted-foreground">{name}</p>
151
134
  </div>
152
- <p className="mt-2 truncate text-[10px] text-muted-foreground">
153
- {name}
154
- </p>
155
135
  </div>
156
136
  </div>
157
137
  );
@@ -163,54 +143,61 @@ export function AddWidgetSelectorDialog({
163
143
  currentPage,
164
144
  pageSize,
165
145
  isLoading,
146
+ searchQuery,
147
+ moduleFilter,
148
+ modules,
149
+ onSearchQueryChange,
150
+ onModuleFilterChange,
166
151
  onPageChange,
167
152
  onPageSizeChange,
168
153
  onAdd,
169
- currentSlug = 'default',
154
+ buttonLabel,
155
+ buttonVariant = 'outline',
156
+ openSignal,
157
+ onOpenSignalHandled,
170
158
  }: AddWidgetSelectorDialogProps) {
171
159
  const tWidget = useTranslations('core.AddWidgetDialog');
172
160
  const tMenu = useTranslations('core.DashboardMenu');
173
- const { request } = useApp();
174
- const router = useRouter();
175
161
 
176
162
  const [openWidgets, setOpenWidgets] = useState(false);
177
- const [openDashboards, setOpenDashboards] = useState(false);
178
163
  const [selectedWidgets, setSelectedWidgets] = useState<Set<string>>(
179
164
  new Set()
180
165
  );
181
166
  const [isAdding, setIsAdding] = useState(false);
182
- const [selectedDashboard, setSelectedDashboard] = useState<string | null>(
183
- null
184
- );
167
+ const lastHandledOpenSignalRef = useRef<number | null>(null);
168
+
169
+ const availableModuleOptions = Array.from(new Set(modules.filter(Boolean)));
185
170
 
186
- // Buscar dashboards disponíveis para o usuário
187
- const { data: userDashboards, isLoading: isLoadingDashboards } = useQuery<
188
- Dashboard[]
189
- >({
190
- queryKey: ['user-dashboards'],
191
- queryFn: async () => {
192
- const { data } = await request<Dashboard[]>({
193
- url: '/dashboard-core/user-dashboards',
194
- method: 'GET',
195
- });
196
- return data;
197
- },
198
- });
171
+ useEffect(() => {
172
+ if (!openSignal) {
173
+ lastHandledOpenSignalRef.current = 0;
174
+ return;
175
+ }
176
+
177
+ if (openSignal !== lastHandledOpenSignalRef.current) {
178
+ lastHandledOpenSignalRef.current = openSignal;
179
+ setOpenWidgets(true);
180
+ onOpenSignalHandled?.();
181
+ }
182
+ }, [onOpenSignalHandled, openSignal]);
199
183
 
200
184
  const toggleSelectedWidget = (slug: string) => {
201
185
  setSelectedWidgets((prev) => {
202
186
  const next = new Set(prev);
187
+
203
188
  if (next.has(slug)) {
204
189
  next.delete(slug);
205
190
  } else {
206
191
  next.add(slug);
207
192
  }
193
+
208
194
  return next;
209
195
  });
210
196
  };
211
197
 
212
198
  const handleAdd = async () => {
213
199
  if (selectedWidgets.size === 0 || isAdding) return;
200
+
214
201
  setIsAdding(true);
215
202
  try {
216
203
  await onAdd(Array.from(selectedWidgets));
@@ -221,34 +208,18 @@ export function AddWidgetSelectorDialog({
221
208
  }
222
209
  };
223
210
 
224
- const handleSwitchDashboard = () => {
225
- if (selectedDashboard && selectedDashboard !== currentSlug) {
226
- router.push(`/core/dashboard/${selectedDashboard}`);
227
- setOpenDashboards(false);
228
- }
229
- };
230
-
231
211
  return (
232
212
  <>
233
- <DropdownMenu>
234
- <DropdownMenuTrigger asChild>
235
- <Button size="sm" className="gap-2" variant="outline">
236
- <IconSettings className="size-4" />
237
- </Button>
238
- </DropdownMenuTrigger>
239
- <DropdownMenuContent align="end">
240
- <DropdownMenuItem onClick={() => setOpenWidgets(true)}>
241
- <IconPlus className="mr-2 size-4" />
242
- {tMenu('addWidgets')}
243
- </DropdownMenuItem>
244
- <DropdownMenuItem onClick={() => setOpenDashboards(true)}>
245
- <IconArrowsRightLeft className="mr-2 size-4" />
246
- {tMenu('switchDashboard')}
247
- </DropdownMenuItem>
248
- </DropdownMenuContent>
249
- </DropdownMenu>
213
+ <Button
214
+ size="sm"
215
+ variant={buttonVariant}
216
+ className="cursor-pointer gap-2"
217
+ onClick={() => setOpenWidgets(true)}
218
+ >
219
+ <IconPlus className="size-4" />
220
+ {buttonLabel || tMenu('addWidgets')}
221
+ </Button>
250
222
 
251
- {/* Sheet de Adicionar Widgets */}
252
223
  <Sheet
253
224
  open={openWidgets}
254
225
  onOpenChange={(open) => {
@@ -270,16 +241,61 @@ export function AddWidgetSelectorDialog({
270
241
  </SheetDescription>
271
242
  </SheetHeader>
272
243
 
244
+ <div className="grid gap-3 border-b px-4 py-4 md:grid-cols-[minmax(0,1fr)_220px]">
245
+ <div className="space-y-2">
246
+ <label
247
+ htmlFor="dashboard-widget-search"
248
+ className="text-sm font-medium"
249
+ >
250
+ {tWidget('search')}
251
+ </label>
252
+ <Input
253
+ id="dashboard-widget-search"
254
+ value={searchQuery}
255
+ onChange={(event) => onSearchQueryChange(event.target.value)}
256
+ placeholder={tWidget('searchPlaceholder')}
257
+ />
258
+ </div>
259
+
260
+ <div className="space-y-2">
261
+ <label
262
+ htmlFor="dashboard-widget-module-filter"
263
+ className="text-sm font-medium"
264
+ >
265
+ {tWidget('moduleFilterLabel')}
266
+ </label>
267
+ <Select
268
+ value={moduleFilter}
269
+ onValueChange={onModuleFilterChange}
270
+ >
271
+ <SelectTrigger
272
+ id="dashboard-widget-module-filter"
273
+ className="w-full"
274
+ >
275
+ <SelectValue placeholder={tWidget('allModules')} />
276
+ </SelectTrigger>
277
+ <SelectContent>
278
+ <SelectItem value="all">{tWidget('allModules')}</SelectItem>
279
+ {availableModuleOptions.map((module) => (
280
+ <SelectItem key={module} value={module}>
281
+ {formatModuleLabel(module)}
282
+ </SelectItem>
283
+ ))}
284
+ </SelectContent>
285
+ </Select>
286
+ </div>
287
+ </div>
288
+
273
289
  <div className="grid min-h-0 flex-1 gap-4 p-4">
274
290
  <ScrollArea className="min-h-0 pr-4">
275
291
  {isLoading ? (
276
- <div className="grid gap-3">
277
- <Skeleton className="h-28 w-full" />
278
- <Skeleton className="h-28 w-full" />
279
- <Skeleton className="h-28 w-full" />
292
+ <div className="grid gap-2.5">
293
+ <Skeleton className="h-24 w-full" />
294
+ <Skeleton className="h-24 w-full" />
295
+ <Skeleton className="h-24 w-full" />
280
296
  </div>
281
297
  ) : (
282
- <div className="grid gap-3">
298
+ <div className="grid gap-2.5">
283
299
  {availableComponents.length === 0 ? (
284
300
  <div className="flex min-h-55 flex-col items-center justify-center rounded-xl border border-dashed text-center">
285
301
  <p className="text-sm text-muted-foreground">
@@ -295,64 +311,84 @@ export function AddWidgetSelectorDialog({
295
311
  component.slug,
296
312
  component.library_slug
297
313
  );
314
+ const isSelected = selectedWidgets.has(selectionKey);
315
+ const moduleLabel = formatModuleLabel(
316
+ component.library_slug
317
+ );
298
318
 
299
319
  return (
300
320
  <Card
301
321
  key={selectionKey}
322
+ role="checkbox"
323
+ tabIndex={0}
324
+ aria-checked={isSelected}
302
325
  className={cn(
303
- 'cursor-pointer border p-4 transition-all hover:border-primary/40 hover:bg-accent/40',
304
- selectedWidgets.has(selectionKey) &&
305
- 'border-primary bg-accent/50'
326
+ 'cursor-pointer border px-3 py-2.5 transition-all hover:border-primary/40 hover:bg-accent/30',
327
+ isSelected &&
328
+ 'border-primary bg-primary/10 shadow-sm ring-1 ring-primary/25'
306
329
  )}
307
330
  onClick={() => toggleSelectedWidget(selectionKey)}
331
+ onKeyDown={(event) => {
332
+ if (event.key === 'Enter' || event.key === ' ') {
333
+ event.preventDefault();
334
+ toggleSelectedWidget(selectionKey);
335
+ }
336
+ }}
308
337
  >
309
- <div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_220px]">
338
+ <div className="grid gap-3 lg:grid-cols-[minmax(0,1fr)_180px] lg:items-center">
310
339
  <div className="flex items-start gap-3">
311
- <div className="bg-primary/10 text-primary flex size-10 items-center justify-center rounded-xl">
312
- <IconLayoutDashboard className="size-5" />
340
+ <div
341
+ className={cn(
342
+ 'flex size-9 items-center justify-center rounded-lg',
343
+ isSelected
344
+ ? 'bg-primary text-primary-foreground'
345
+ : 'bg-primary/10 text-primary'
346
+ )}
347
+ >
348
+ <IconLayoutDashboard className="size-4" />
313
349
  </div>
314
350
  <div className="min-w-0 flex-1">
315
- <CardTitle className="truncate text-base">
316
- {name}
317
- </CardTitle>
318
- <CardDescription className="mt-1 text-xs">
319
- {selectionKey}
320
- </CardDescription>
321
- <div className="mt-3 flex flex-wrap items-center gap-2">
351
+ <div className="flex items-start justify-between gap-3">
352
+ <div className="min-w-0">
353
+ <CardTitle className="truncate text-sm">
354
+ {name}
355
+ </CardTitle>
356
+ <CardDescription className="mt-0.5 truncate text-[11px]">
357
+ {selectionKey}
358
+ </CardDescription>
359
+ </div>
360
+ <Checkbox
361
+ checked={isSelected}
362
+ aria-label={name}
363
+ className="pointer-events-none mt-0.5"
364
+ />
365
+ </div>
366
+ <div className="mt-2 flex flex-wrap items-center gap-1.5">
367
+ <Badge
368
+ variant="outline"
369
+ className="px-1.5 py-0 text-[10px]"
370
+ >
371
+ {tWidget('module')}: {moduleLabel}
372
+ </Badge>
322
373
  <Badge
323
374
  variant="secondary"
324
- className="text-[11px]"
375
+ className="px-1.5 py-0 text-[10px]"
325
376
  >
326
377
  {tWidget('dimensions')}: {component.width}
327
378
  x{component.height}
328
379
  </Badge>
329
380
  <Badge
330
381
  variant="outline"
331
- className="text-[11px]"
382
+ className="px-1.5 py-0 text-[10px]"
332
383
  >
333
384
  {component.is_resizable
334
385
  ? tWidget('resizable')
335
386
  : tWidget('fixedSize')}
336
387
  </Badge>
337
388
  </div>
338
- <div className="mt-3">
339
- <Button
340
- type="button"
341
- size="sm"
342
- className="cursor-pointer"
343
- onClick={(event) => {
344
- event.stopPropagation();
345
- toggleSelectedWidget(selectionKey);
346
- }}
347
- >
348
- {selectedWidgets.has(selectionKey)
349
- ? tWidget('selected')
350
- : tWidget('select')}
351
- </Button>
352
- </div>
353
389
  </div>
354
390
  </div>
355
- <div className="hidden lg:block">
391
+ <div className="lg:block">
356
392
  <WidgetPreview
357
393
  name={name}
358
394
  slug={component.slug}
@@ -400,94 +436,6 @@ export function AddWidgetSelectorDialog({
400
436
  </div>
401
437
  </SheetContent>
402
438
  </Sheet>
403
-
404
- {/* Diálogo de Trocar Dashboard */}
405
- <Dialog open={openDashboards} onOpenChange={setOpenDashboards}>
406
- <DialogContent className="sm:max-w-150">
407
- <DialogHeader>
408
- <DialogTitle>{tMenu('selectDashboardTitle')}</DialogTitle>
409
- <DialogDescription>
410
- {tMenu('selectDashboardDescription')}
411
- </DialogDescription>
412
- </DialogHeader>
413
- <ScrollArea className="max-h-100 pr-4">
414
- {isLoadingDashboards ? (
415
- <div className="grid gap-3">
416
- <Skeleton className="h-24 w-full" />
417
- <Skeleton className="h-24 w-full" />
418
- <Skeleton className="h-24 w-full" />
419
- </div>
420
- ) : (
421
- <div className="grid gap-3">
422
- {!userDashboards || userDashboards.length === 0 ? (
423
- <div className="flex min-h-50 flex-col items-center justify-center text-center">
424
- <p className="text-muted-foreground text-sm">
425
- {tMenu('noDashboardsAvailable')}
426
- </p>
427
- </div>
428
- ) : (
429
- userDashboards.map((dashboard) => {
430
- const name =
431
- dashboard.dashboard_locale?.[0]?.name || dashboard.slug;
432
- const isCurrent = dashboard.slug === currentSlug;
433
- return (
434
- <Card
435
- key={dashboard.slug}
436
- className={cn(
437
- 'cursor-pointer transition-colors py-4 hover:bg-accent',
438
- selectedDashboard === dashboard.slug &&
439
- 'border-primary bg-accent',
440
- isCurrent && 'opacity-50'
441
- )}
442
- onClick={() =>
443
- !isCurrent && setSelectedDashboard(dashboard.slug)
444
- }
445
- >
446
- <CardHeader>
447
- <div className="flex items-start gap-3">
448
- <div className="bg-primary/10 flex size-10 items-center justify-center rounded-lg">
449
- <IconArrowsRightLeft className="text-primary size-5" />
450
- </div>
451
- <div className="flex-1">
452
- <CardTitle className="text-base">
453
- {name}
454
- {isCurrent && ' (atual)'}
455
- </CardTitle>
456
- <CardDescription className="text-sm">
457
- {dashboard.slug}
458
- </CardDescription>
459
- </div>
460
- </div>
461
- </CardHeader>
462
- </Card>
463
- );
464
- })
465
- )}
466
- </div>
467
- )}
468
- </ScrollArea>
469
- <DialogFooter>
470
- <Button
471
- type="button"
472
- variant="outline"
473
- onClick={() => setOpenDashboards(false)}
474
- >
475
- {tWidget('cancel')}
476
- </Button>
477
- <Button
478
- type="button"
479
- onClick={handleSwitchDashboard}
480
- disabled={
481
- !selectedDashboard ||
482
- isLoadingDashboards ||
483
- selectedDashboard === currentSlug
484
- }
485
- >
486
- {tMenu('switch')}
487
- </Button>
488
- </DialogFooter>
489
- </DialogContent>
490
- </Dialog>
491
439
  </>
492
440
  );
493
441
  }
@@ -84,6 +84,60 @@ const deriveResponsiveLayout = (
84
84
  });
85
85
  };
86
86
 
87
+ const scaleToColumnCount = (
88
+ value: number,
89
+ sourceCols: number,
90
+ targetCols: number
91
+ ) => {
92
+ if (sourceCols === targetCols) {
93
+ return value;
94
+ }
95
+
96
+ return Math.round((value * targetCols) / sourceCols);
97
+ };
98
+
99
+ const mapLayoutToBaseColumns = (
100
+ nextLayout: RGLLayout,
101
+ sourceCols: number,
102
+ targetCols: number,
103
+ previousLayout: Layout
104
+ ): Layout => {
105
+ const previousItems = new Map(previousLayout.map((item) => [item.i, item]));
106
+
107
+ return nextLayout.map((item) => {
108
+ const previousItem = previousItems.get(item.i);
109
+ const nextW =
110
+ sourceCols === 1
111
+ ? (previousItem?.w ?? 1)
112
+ : clamp(
113
+ scaleToColumnCount(item.w, sourceCols, targetCols),
114
+ 1,
115
+ targetCols
116
+ );
117
+ const nextX =
118
+ sourceCols === 1
119
+ ? clamp(previousItem?.x ?? 0, 0, Math.max(0, targetCols - nextW))
120
+ : clamp(
121
+ scaleToColumnCount(item.x, sourceCols, targetCols),
122
+ 0,
123
+ Math.max(0, targetCols - nextW)
124
+ );
125
+
126
+ return {
127
+ i: item.i,
128
+ x: nextX,
129
+ y: item.y,
130
+ w: nextW,
131
+ h: item.h,
132
+ minW: previousItem?.minW ?? item.minW,
133
+ maxW: previousItem?.maxW ?? item.maxW,
134
+ minH: previousItem?.minH ?? item.minH,
135
+ maxH: previousItem?.maxH ?? item.maxH,
136
+ static: previousItem?.static ?? item.static,
137
+ } satisfies LayoutItem;
138
+ });
139
+ };
140
+
87
141
  // Simple compaction function for grid layout
88
142
  const compactLayout = (
89
143
  layout: RGLLayout,
@@ -161,24 +215,15 @@ export function DraggableGrid({
161
215
  };
162
216
  }, []);
163
217
 
164
- const handleLayoutChange = (newLayout: RGLLayout) => {
165
- if (effectiveCols !== cols) {
166
- return;
167
- }
168
-
218
+ const emitUserLayoutChange = (newLayout: RGLLayout) => {
169
219
  const layouts = Array.isArray(newLayout) ? newLayout : [newLayout];
170
- const convertedLayout = layouts.map((item: any) => ({
171
- i: item.i,
172
- x: item.x,
173
- y: item.y,
174
- w: item.w,
175
- h: item.h,
176
- minW: item.minW,
177
- maxW: item.maxW,
178
- minH: item.minH,
179
- maxH: item.maxH,
180
- static: item.static,
181
- })) as LayoutItem[];
220
+ const convertedLayout = mapLayoutToBaseColumns(
221
+ layouts,
222
+ effectiveCols,
223
+ cols,
224
+ layout
225
+ );
226
+
182
227
  onLayoutChange(convertedLayout);
183
228
  };
184
229
 
@@ -208,7 +253,8 @@ export function DraggableGrid({
208
253
  allowOverlap: !preventCollision,
209
254
  compact: (layout, cols) => compactLayout(layout, cols, compactType),
210
255
  }}
211
- onLayoutChange={handleLayoutChange}
256
+ onDragStop={(nextLayout) => emitUserLayoutChange(nextLayout)}
257
+ onResizeStop={(nextLayout) => emitUserLayoutChange(nextLayout)}
212
258
  >
213
259
  {children}
214
260
  </GridLayout>