@hed-hog/core 0.0.298 → 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 (123) hide show
  1. package/dist/dashboard/dashboard/dashboard.controller.d.ts +9 -0
  2. package/dist/dashboard/dashboard/dashboard.controller.d.ts.map +1 -1
  3. package/dist/dashboard/dashboard/dashboard.service.d.ts +9 -0
  4. package/dist/dashboard/dashboard/dashboard.service.d.ts.map +1 -1
  5. package/dist/dashboard/dashboard-component/dashboard-component.controller.d.ts +14 -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 +28 -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 +22 -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 +185 -35
  12. package/dist/dashboard/dashboard-component/dashboard-component.service.js.map +1 -1
  13. package/dist/dashboard/dashboard-component/dto/create.dto.d.ts +1 -0
  14. package/dist/dashboard/dashboard-component/dto/create.dto.d.ts.map +1 -1
  15. package/dist/dashboard/dashboard-component/dto/create.dto.js +5 -0
  16. package/dist/dashboard/dashboard-component/dto/create.dto.js.map +1 -1
  17. package/dist/dashboard/dashboard-component/dto/update.dto.d.ts +1 -0
  18. package/dist/dashboard/dashboard-component/dto/update.dto.d.ts.map +1 -1
  19. package/dist/dashboard/dashboard-component/dto/update.dto.js +5 -0
  20. package/dist/dashboard/dashboard-component/dto/update.dto.js.map +1 -1
  21. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts +1 -0
  22. package/dist/dashboard/dashboard-component-role/dashboard-component-role.controller.d.ts.map +1 -1
  23. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts +1 -0
  24. package/dist/dashboard/dashboard-component-role/dashboard-component-role.service.d.ts.map +1 -1
  25. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +72 -1
  26. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  27. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +111 -0
  28. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  29. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +76 -1
  30. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  31. package/dist/dashboard/dashboard-core/dashboard-core.service.js +614 -23
  32. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  33. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts +3 -0
  34. package/dist/dashboard/dashboard-item/dashboard-item.controller.d.ts.map +1 -1
  35. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts +3 -0
  36. package/dist/dashboard/dashboard-item/dashboard-item.service.d.ts.map +1 -1
  37. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts +2 -0
  38. package/dist/dashboard/dashboard-role/dashboard-role.controller.d.ts.map +1 -1
  39. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts +2 -0
  40. package/dist/dashboard/dashboard-role/dashboard-role.service.d.ts.map +1 -1
  41. package/hedhog/data/dashboard.yaml +12 -6
  42. package/hedhog/data/dashboard_component_role.yaml +66 -0
  43. package/hedhog/data/dashboard_item.yaml +1 -1
  44. package/hedhog/data/dashboard_role.yaml +2 -8
  45. package/hedhog/data/route.yaml +84 -0
  46. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +457 -135
  47. package/hedhog/frontend/app/dashboard/[slug]/types.ts.ejs +3 -0
  48. package/hedhog/frontend/app/dashboard/[slug]/widget-renderer.tsx.ejs +365 -28
  49. package/hedhog/frontend/app/dashboard/components/add-widget-selector-dialog.tsx.ejs +376 -247
  50. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +64 -18
  51. package/hedhog/frontend/app/dashboard/dashboard-home-tabs.tsx.ejs +1389 -0
  52. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +37 -0
  53. package/hedhog/frontend/app/dashboard/management/page.tsx.ejs +1 -1
  54. package/hedhog/frontend/app/dashboard/management/tabs/components-tab.tsx.ejs +6 -6
  55. package/hedhog/frontend/app/dashboard/management/tabs/dashboards-tab.tsx.ejs +8 -8
  56. package/hedhog/frontend/app/dashboard/management/tabs/items-tab.tsx.ejs +3 -3
  57. package/hedhog/frontend/app/dashboard/page.tsx.ejs +3 -25
  58. package/hedhog/frontend/messages/en.json +115 -2
  59. package/hedhog/frontend/messages/pt.json +114 -1
  60. package/hedhog/frontend/public/dashboard-previews/.gitkeep +12 -0
  61. package/hedhog/frontend/public/dashboard-previews/account-security.png +0 -0
  62. package/hedhog/frontend/public/dashboard-previews/active-users-card.png +0 -0
  63. package/hedhog/frontend/public/dashboard-previews/activity-timeline.png +0 -0
  64. package/hedhog/frontend/public/dashboard-previews/cash-balance-kpi.png +0 -0
  65. package/hedhog/frontend/public/dashboard-previews/cash-flow-chart.png +0 -0
  66. package/hedhog/frontend/public/dashboard-previews/default-kpi.png +0 -0
  67. package/hedhog/frontend/public/dashboard-previews/email-notifications.png +0 -0
  68. package/hedhog/frontend/public/dashboard-previews/financial-alerts.png +0 -0
  69. package/hedhog/frontend/public/dashboard-previews/login-history-chart.png +0 -0
  70. package/hedhog/frontend/public/dashboard-previews/mail-sent-card.png +0 -0
  71. package/hedhog/frontend/public/dashboard-previews/mail-sent-chart.png +0 -0
  72. package/hedhog/frontend/public/dashboard-previews/menus-card.png +0 -0
  73. package/hedhog/frontend/public/dashboard-previews/payable-30d-kpi.png +0 -0
  74. package/hedhog/frontend/public/dashboard-previews/permissions-card.png +0 -0
  75. package/hedhog/frontend/public/dashboard-previews/permissions-chart.png +0 -0
  76. package/hedhog/frontend/public/dashboard-previews/profile-card.png +0 -0
  77. package/hedhog/frontend/public/dashboard-previews/receivable-30d-kpi.png +0 -0
  78. package/hedhog/frontend/public/dashboard-previews/routes-card.png +0 -0
  79. package/hedhog/frontend/public/dashboard-previews/session-activity-chart.png +0 -0
  80. package/hedhog/frontend/public/dashboard-previews/sessions-today-card.png +0 -0
  81. package/hedhog/frontend/public/dashboard-previews/stat-access-level.png +0 -0
  82. package/hedhog/frontend/public/dashboard-previews/stat-actions-today.png +0 -0
  83. package/hedhog/frontend/public/dashboard-previews/stat-consecutive-days.png +0 -0
  84. package/hedhog/frontend/public/dashboard-previews/stat-online-time.png +0 -0
  85. package/hedhog/frontend/public/dashboard-previews/upcoming-payable.png +0 -0
  86. package/hedhog/frontend/public/dashboard-previews/upcoming-receivable.png +0 -0
  87. package/hedhog/frontend/public/dashboard-previews/user-growth-chart.png +0 -0
  88. package/hedhog/frontend/public/dashboard-previews/user-roles.png +0 -0
  89. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/account-security.tsx.ejs +34 -30
  90. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/active-users-card.tsx.ejs +2 -2
  91. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/activity-timeline.tsx.ejs +1 -1
  92. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/email-notifications.tsx.ejs +1 -1
  93. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/locale-config.tsx.ejs +1 -1
  94. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/login-history-chart.tsx.ejs +1 -1
  95. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/mail-config.tsx.ejs +1 -1
  96. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/mail-sent-card.tsx.ejs +2 -2
  97. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/mail-sent-chart.tsx.ejs +1 -1
  98. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/menus-card.tsx.ejs +2 -2
  99. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/oauth-config.tsx.ejs +1 -1
  100. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/permissions-card.tsx.ejs +2 -2
  101. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/permissions-chart.tsx.ejs +1 -1
  102. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/profile-card.tsx.ejs +1 -1
  103. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/routes-card.tsx.ejs +2 -2
  104. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/session-activity-chart.tsx.ejs +1 -1
  105. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/sessions-today-card.tsx.ejs +2 -2
  106. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/stat-access-level.tsx.ejs +1 -1
  107. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/stat-actions-today.tsx.ejs +1 -1
  108. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/stat-consecutive-days.tsx.ejs +1 -1
  109. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/stat-online-time.tsx.ejs +1 -1
  110. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/storage-config.tsx.ejs +1 -1
  111. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/theme-config.tsx.ejs +1 -1
  112. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-growth-chart.tsx.ejs +1 -1
  113. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-roles.tsx.ejs +1 -1
  114. package/hedhog/frontend/{app/dashboard/components/widgets → widgets}/user-sessions.tsx.ejs +2 -2
  115. package/hedhog/table/dashboard.yaml +6 -0
  116. package/hedhog/table/dashboard_component.yaml +7 -0
  117. package/package.json +5 -5
  118. package/src/dashboard/dashboard-component/dashboard-component.controller.ts +51 -14
  119. package/src/dashboard/dashboard-component/dashboard-component.service.ts +254 -43
  120. package/src/dashboard/dashboard-component/dto/create.dto.ts +4 -0
  121. package/src/dashboard/dashboard-component/dto/update.dto.ts +4 -0
  122. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +112 -1
  123. package/src/dashboard/dashboard-core/dashboard-core.service.ts +782 -24
@@ -2,6 +2,7 @@ export interface WidgetLayout {
2
2
  i: string;
3
3
  component_id: number;
4
4
  slug: string;
5
+ library_slug?: string;
5
6
  name: string;
6
7
  description: string;
7
8
  x: number;
@@ -18,6 +19,7 @@ export interface WidgetLayout {
18
19
  export interface DashboardComponent {
19
20
  id: number;
20
21
  slug: string;
22
+ library_slug?: string;
21
23
  path: string;
22
24
  min_width: number;
23
25
  max_width?: number;
@@ -59,4 +61,5 @@ export interface LayoutItem {
59
61
  export interface WidgetRendererProps {
60
62
  widget: WidgetLayout;
61
63
  onRemove: () => void;
64
+ onCapture?: () => void;
62
65
  }
@@ -1,45 +1,382 @@
1
1
  'use client';
2
2
 
3
+ import { Button } from '@/components/ui/button';
4
+ import {
5
+ Dialog,
6
+ DialogContent,
7
+ DialogDescription,
8
+ DialogFooter,
9
+ DialogHeader,
10
+ DialogTitle,
11
+ } from '@/components/ui/dialog';
3
12
  import { Skeleton } from '@/components/ui/skeleton';
4
- import { useEffect, useState } from 'react';
13
+ import { IconCamera, IconScreenshot } from '@tabler/icons-react';
14
+ import { toBlob } from 'html-to-image';
15
+ import { useTranslations } from 'next-intl';
16
+ import {
17
+ type ComponentType,
18
+ type MouseEvent,
19
+ useEffect,
20
+ useState,
21
+ } from 'react';
22
+ import { toast } from 'sonner';
5
23
  import { DynamicWidget } from '../components';
6
- import { WidgetRendererProps } from './types';
24
+ import { WidgetLayout, WidgetRendererProps } from './types';
7
25
 
8
- export const WidgetRenderer = ({ widget, onRemove }: WidgetRendererProps) => {
9
- const [Component, setComponent] = useState<any>(null);
26
+ const getWidgetBaseSlug = (slug: string): string => {
27
+ const parts = slug.split('.');
28
+ return parts[parts.length - 1] || slug;
29
+ };
30
+
31
+ const getWidgetLibrarySlug = (
32
+ slug: string,
33
+ librarySlug?: string
34
+ ): string | undefined => {
35
+ if (librarySlug) {
36
+ return librarySlug;
37
+ }
38
+
39
+ const parts = slug.split('.');
40
+ return parts.length > 1 ? parts[0] : 'core';
41
+ };
42
+
43
+ const getWidgetImportCandidates = (
44
+ slug: string,
45
+ librarySlug?: string
46
+ ): string[] => {
47
+ const baseSlug = getWidgetBaseSlug(slug);
48
+ const effectiveLibrarySlug = getWidgetLibrarySlug(slug, librarySlug);
49
+ const libraryPrefixedSlug = effectiveLibrarySlug
50
+ ? `${effectiveLibrarySlug}.${baseSlug}`
51
+ : null;
52
+
53
+ return Array.from(
54
+ new Set(
55
+ [libraryPrefixedSlug, slug, baseSlug].filter(
56
+ (candidate): candidate is string => Boolean(candidate)
57
+ )
58
+ )
59
+ );
60
+ };
61
+
62
+ const importWidgetComponent = (candidate: string) =>
63
+ import(`@/components/widgets/${candidate}`);
64
+
65
+ type LoadedWidgetComponent = ComponentType<{
66
+ widget: WidgetLayout;
67
+ onRemove: () => void;
68
+ }>;
69
+
70
+ export const WidgetRenderer = ({
71
+ widget,
72
+ onRemove,
73
+ onCapture,
74
+ }: WidgetRendererProps) => {
75
+ const t = useTranslations('core.WidgetRenderer');
76
+ const [Component, setComponent] = useState<LoadedWidgetComponent | null>(
77
+ null
78
+ );
10
79
  const [useFallback, setUseFallback] = useState(false);
80
+ const [isScreenshotDialogOpen, setIsScreenshotDialogOpen] = useState(false);
81
+ const [isGeneratingScreenshot, setIsGeneratingScreenshot] = useState(false);
82
+ const [isSharingScreenshot, setIsSharingScreenshot] = useState(false);
83
+ const [screenshotUrl, setScreenshotUrl] = useState<string | null>(null);
84
+ const [screenshotBlob, setScreenshotBlob] = useState<Blob | null>(null);
11
85
 
12
86
  useEffect(() => {
13
- import(`../components/widgets/${widget.slug}`)
14
- .then((mod) => {
15
- setComponent(() => mod.default);
16
- })
17
- .catch((error) => {
18
- console.warn(
19
- `Widget component not found for slug: ${widget.slug}`,
20
- error
21
- );
87
+ let cancelled = false;
88
+
89
+ setComponent(null);
90
+ setUseFallback(false);
91
+
92
+ const loadWidgetComponent = async () => {
93
+ const candidates = getWidgetImportCandidates(
94
+ widget.slug,
95
+ widget.library_slug
96
+ );
97
+
98
+ for (const candidate of candidates) {
99
+ try {
100
+ const mod = await importWidgetComponent(candidate);
101
+ if (!cancelled) {
102
+ setComponent(() => mod.default);
103
+ }
104
+ return;
105
+ } catch {
106
+ // Try next candidate.
107
+ }
108
+ }
109
+
110
+ if (!cancelled) {
111
+ console.warn(`Widget component not found for slug: ${widget.slug}`, {
112
+ candidates,
113
+ });
22
114
  setUseFallback(true);
115
+ }
116
+ };
117
+
118
+ void loadWidgetComponent();
119
+
120
+ return () => {
121
+ cancelled = true;
122
+ };
123
+ }, [widget.slug, widget.library_slug]);
124
+
125
+ useEffect(() => {
126
+ return () => {
127
+ if (screenshotUrl) {
128
+ URL.revokeObjectURL(screenshotUrl);
129
+ }
130
+ };
131
+ }, [screenshotUrl]);
132
+
133
+ const getScreenshotFileName = () => {
134
+ const baseSlug = getWidgetBaseSlug(widget.slug)
135
+ .replace(/[^a-z0-9-_]+/gi, '-')
136
+ .toLowerCase();
137
+
138
+ return `dashboard-widget-${baseSlug}-${Date.now()}.png`;
139
+ };
140
+
141
+ const generateScreenshotBlob = async () => {
142
+ const widgetElement = document.querySelector(
143
+ `[data-widget-instance-id="${widget.i}"]`
144
+ ) as HTMLElement | null;
145
+
146
+ if (!widgetElement) {
147
+ throw new Error('Widget element not found for screenshot');
148
+ }
149
+
150
+ const screenshot = await toBlob(widgetElement, {
151
+ cacheBust: true,
152
+ pixelRatio: 2,
153
+ backgroundColor: '#ffffff',
154
+ filter: (node) =>
155
+ !(node instanceof HTMLElement && node.dataset.widgetAction === 'true'),
156
+ });
157
+
158
+ if (!screenshot) {
159
+ throw new Error('Failed to generate screenshot blob');
160
+ }
161
+
162
+ return screenshot;
163
+ };
164
+
165
+ const handleOpenScreenshotDialog = async (
166
+ event: MouseEvent<HTMLButtonElement>
167
+ ) => {
168
+ event.stopPropagation();
169
+ event.preventDefault();
170
+
171
+ setIsGeneratingScreenshot(true);
172
+
173
+ try {
174
+ const screenshot = await generateScreenshotBlob();
175
+ const nextScreenshotUrl = URL.createObjectURL(screenshot);
176
+
177
+ setScreenshotBlob(screenshot);
178
+ setScreenshotUrl((currentUrl) => {
179
+ if (currentUrl) {
180
+ URL.revokeObjectURL(currentUrl);
181
+ }
182
+
183
+ return nextScreenshotUrl;
23
184
  });
24
- }, [widget.slug]);
185
+ setIsScreenshotDialogOpen(true);
186
+ } catch (error) {
187
+ console.error('Erro ao gerar print do widget:', error);
188
+ toast.error(t('generateScreenshotError'));
189
+ } finally {
190
+ setIsGeneratingScreenshot(false);
191
+ }
192
+ };
193
+
194
+ const handleDownloadScreenshot = () => {
195
+ if (!screenshotBlob) {
196
+ toast.error(t('generateAgainToDownload'));
197
+ return;
198
+ }
199
+
200
+ const downloadUrl = URL.createObjectURL(screenshotBlob);
201
+ const link = document.createElement('a');
202
+ link.href = downloadUrl;
203
+ link.download = getScreenshotFileName();
204
+ document.body.appendChild(link);
205
+ link.click();
206
+ document.body.removeChild(link);
207
+ URL.revokeObjectURL(downloadUrl);
208
+ };
209
+
210
+ const handleShareScreenshot = async () => {
211
+ if (!screenshotBlob) {
212
+ toast.error(t('generateAgainToShare'));
213
+ return;
214
+ }
215
+
216
+ if (
217
+ typeof navigator === 'undefined' ||
218
+ typeof navigator.share !== 'function' ||
219
+ typeof File === 'undefined'
220
+ ) {
221
+ toast.error(t('shareNotSupported'));
222
+ return;
223
+ }
25
224
 
26
- if (useFallback) {
225
+ setIsSharingScreenshot(true);
226
+
227
+ try {
228
+ const file = new File([screenshotBlob], getScreenshotFileName(), {
229
+ type: screenshotBlob.type || 'image/png',
230
+ });
231
+
232
+ if (
233
+ typeof navigator.canShare === 'function' &&
234
+ !navigator.canShare({ files: [file] })
235
+ ) {
236
+ throw new Error('Browser does not support sharing this file');
237
+ }
238
+
239
+ await navigator.share({
240
+ title: widget.name,
241
+ text: t('shareText', { name: widget.name }),
242
+ files: [file],
243
+ });
244
+ } catch (error) {
245
+ if (error instanceof DOMException && error.name === 'AbortError') {
246
+ return;
247
+ }
248
+
249
+ console.error('Erro ao compartilhar print do widget:', error);
250
+ toast.error(t('shareImageError'));
251
+ } finally {
252
+ setIsSharingScreenshot(false);
253
+ }
254
+ };
255
+
256
+ const actionButtons = (
257
+ <>
258
+ {onCapture ? (
259
+ <Button
260
+ type="button"
261
+ variant="ghost"
262
+ size="icon"
263
+ className="no-drag absolute right-17 top-3 z-30 size-6 cursor-pointer shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
264
+ title={t('saveWidgetPreview')}
265
+ aria-label={t('saveWidgetPreview')}
266
+ data-widget-action="true"
267
+ onClick={(event) => {
268
+ event.stopPropagation();
269
+ event.preventDefault();
270
+ onCapture();
271
+ }}
272
+ >
273
+ <IconCamera className="size-3" />
274
+ </Button>
275
+ ) : null}
276
+ <Button
277
+ type="button"
278
+ variant="ghost"
279
+ size="icon"
280
+ className="no-drag absolute right-10 top-3 z-30 size-6 cursor-pointer shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
281
+ title={
282
+ isGeneratingScreenshot
283
+ ? t('generatingWidgetScreenshot')
284
+ : t('generateWidgetScreenshot')
285
+ }
286
+ aria-label={
287
+ isGeneratingScreenshot
288
+ ? t('generatingWidgetScreenshot')
289
+ : t('generateWidgetScreenshot')
290
+ }
291
+ data-widget-action="true"
292
+ onClick={(event) => {
293
+ void handleOpenScreenshotDialog(event);
294
+ }}
295
+ disabled={isGeneratingScreenshot}
296
+ >
297
+ <IconScreenshot className="size-3" />
298
+ </Button>
299
+ </>
300
+ );
301
+
302
+ if (!Component && !useFallback) {
27
303
  return (
28
- <DynamicWidget
29
- title={widget.name}
30
- value={0}
31
- description={widget.description || ''}
32
- iconName="settings"
33
- color="#000000"
34
- draggable
35
- onRemove={onRemove}
36
- />
304
+ <div className="h-full w-full" data-widget-instance-id={widget.i}>
305
+ <Skeleton className="h-full w-full" />
306
+ </div>
37
307
  );
38
308
  }
39
309
 
40
- if (!Component) {
41
- return <Skeleton className="h-full w-full" />;
42
- }
310
+ return (
311
+ <>
312
+ <div
313
+ className="group relative h-full w-full"
314
+ data-widget-instance-id={widget.i}
315
+ >
316
+ {actionButtons}
317
+ {useFallback ? (
318
+ <DynamicWidget
319
+ title={widget.name}
320
+ value={0}
321
+ description={widget.description || ''}
322
+ iconName="settings"
323
+ color="#000000"
324
+ draggable
325
+ onRemove={onRemove}
326
+ />
327
+ ) : Component ? (
328
+ <Component widget={widget} onRemove={onRemove} />
329
+ ) : null}
330
+ </div>
331
+
332
+ <Dialog
333
+ open={isScreenshotDialogOpen}
334
+ onOpenChange={setIsScreenshotDialogOpen}
335
+ >
336
+ <DialogContent className="sm:max-w-3xl">
337
+ <DialogHeader>
338
+ <DialogTitle>{t('screenshotTitle')}</DialogTitle>
339
+ <DialogDescription>{t('screenshotDescription')}</DialogDescription>
340
+ </DialogHeader>
341
+
342
+ <div className="overflow-hidden rounded-lg border bg-muted/30">
343
+ {screenshotUrl ? (
344
+ <img
345
+ src={screenshotUrl}
346
+ alt={t('screenshotAlt', { name: widget.name })}
347
+ className="max-h-[70vh] w-full object-contain"
348
+ />
349
+ ) : (
350
+ <div className="flex h-60 items-center justify-center p-4">
351
+ <Skeleton className="h-full w-full" />
352
+ </div>
353
+ )}
354
+ </div>
43
355
 
44
- return <Component widget={widget} onRemove={onRemove} />;
356
+ <DialogFooter className="gap-2 sm:justify-between">
357
+ <p className="text-muted-foreground text-xs">{widget.name}</p>
358
+ <div className="flex flex-wrap gap-2">
359
+ <Button
360
+ type="button"
361
+ variant="outline"
362
+ onClick={() => {
363
+ void handleShareScreenshot();
364
+ }}
365
+ disabled={!screenshotBlob || isSharingScreenshot}
366
+ >
367
+ {isSharingScreenshot ? t('sharing') : t('share')}
368
+ </Button>
369
+ <Button
370
+ type="button"
371
+ onClick={handleDownloadScreenshot}
372
+ disabled={!screenshotBlob}
373
+ >
374
+ {t('downloadImage')}
375
+ </Button>
376
+ </div>
377
+ </DialogFooter>
378
+ </DialogContent>
379
+ </Dialog>
380
+ </>
381
+ );
45
382
  };