@hed-hog/core 0.0.276 → 0.0.278

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 (59) hide show
  1. package/dist/auth/auth.controller.d.ts +8 -1
  2. package/dist/auth/auth.controller.d.ts.map +1 -1
  3. package/dist/auth/auth.controller.js +7 -7
  4. package/dist/auth/auth.controller.js.map +1 -1
  5. package/dist/auth/auth.service.d.ts +10 -1
  6. package/dist/auth/auth.service.d.ts.map +1 -1
  7. package/dist/auth/auth.service.js +34 -8
  8. package/dist/auth/auth.service.js.map +1 -1
  9. package/dist/profile/profile.service.js +1 -1
  10. package/dist/profile/profile.service.js.map +1 -1
  11. package/dist/role/guards/role.guard.d.ts +1 -0
  12. package/dist/role/guards/role.guard.d.ts.map +1 -1
  13. package/dist/role/guards/role.guard.js +18 -0
  14. package/dist/role/guards/role.guard.js.map +1 -1
  15. package/dist/session/session.service.js +1 -1
  16. package/dist/session/session.service.js.map +1 -1
  17. package/dist/user/dto/reset-password.dto.d.ts +4 -0
  18. package/dist/user/dto/reset-password.dto.d.ts.map +1 -0
  19. package/dist/user/dto/reset-password.dto.js +26 -0
  20. package/dist/user/dto/reset-password.dto.js.map +1 -0
  21. package/dist/user/user.controller.d.ts +5 -0
  22. package/dist/user/user.controller.d.ts.map +1 -1
  23. package/dist/user/user.controller.js +13 -0
  24. package/dist/user/user.controller.js.map +1 -1
  25. package/dist/user/user.service.d.ts +6 -0
  26. package/dist/user/user.service.d.ts.map +1 -1
  27. package/dist/user/user.service.js +65 -0
  28. package/dist/user/user.service.js.map +1 -1
  29. package/hedhog/data/dashboard_component.yaml +77 -33
  30. package/hedhog/data/dashboard_component_role.yaml +132 -66
  31. package/hedhog/data/dashboard_item.yaml +100 -100
  32. package/hedhog/data/dashboard_role.yaml +18 -12
  33. package/hedhog/data/menu.yaml +6 -0
  34. package/hedhog/data/route.yaml +57 -1
  35. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
  36. package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +24 -24
  37. package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +4 -4
  38. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +23 -19
  39. package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +15 -14
  40. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
  41. package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +18 -18
  42. package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +18 -18
  43. package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +18 -18
  44. package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +18 -18
  45. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +3 -3
  46. package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +34 -33
  47. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
  48. package/hedhog/frontend/app/users/page.tsx.ejs +322 -1
  49. package/hedhog/frontend/messages/en.json +19 -1
  50. package/hedhog/frontend/messages/pt.json +19 -1
  51. package/package.json +4 -4
  52. package/src/auth/auth.controller.ts +21 -20
  53. package/src/auth/auth.service.ts +63 -15
  54. package/src/profile/profile.service.ts +1 -1
  55. package/src/role/guards/role.guard.ts +36 -7
  56. package/src/session/session.service.ts +2 -2
  57. package/src/user/dto/reset-password.dto.ts +11 -0
  58. package/src/user/user.controller.ts +24 -14
  59. package/src/user/user.service.ts +84 -0
@@ -19,6 +19,8 @@
19
19
  role:
20
20
  - where:
21
21
  slug: admin
22
+ - where:
23
+ slug: admin-mail
22
24
  - menu_id:
23
25
  where:
24
26
  slug: /core/management
@@ -89,6 +91,8 @@
89
91
  role:
90
92
  - where:
91
93
  slug: admin
94
+ - where:
95
+ slug: admin-mail
92
96
  - menu_id:
93
97
  where:
94
98
  slug: /core/management
@@ -128,3 +132,5 @@
128
132
  role:
129
133
  - where:
130
134
  slug: admin
135
+ - where:
136
+ slug: admin-user
@@ -28,6 +28,22 @@
28
28
  slug: admin
29
29
  - where:
30
30
  slug: admin-access
31
+ - url: /auth/refresh
32
+ method: POST
33
+ relations:
34
+ role:
35
+ - where:
36
+ slug: admin
37
+ - where:
38
+ slug: admin-access
39
+ - url: /auth/logout
40
+ method: POST
41
+ relations:
42
+ role:
43
+ - where:
44
+ slug: admin
45
+ - where:
46
+ slug: admin-access
31
47
  - url: /sessions/revoke-all
32
48
  method: DELETE
33
49
  relations:
@@ -344,60 +360,86 @@
344
360
  role:
345
361
  - where:
346
362
  slug: admin
363
+ - where:
364
+ slug: admin-user
347
365
  - url: /user/:userId/role
348
366
  method: GET
349
367
  relations:
350
368
  role:
351
369
  - where:
352
370
  slug: admin
371
+ - where:
372
+ slug: admin-user
353
373
  - url: /user/:userId/role
354
374
  method: PATCH
355
375
  relations:
356
376
  role:
357
377
  - where:
358
378
  slug: admin
379
+ - where:
380
+ slug: admin-user
359
381
  - url: /user/:userId/role/:roleId
360
382
  method: DELETE
361
383
  relations:
362
384
  role:
363
385
  - where:
364
386
  slug: admin
387
+ - where:
388
+ slug: admin-user
365
389
  - url: /user/:userId/role/:roleId
366
390
  method: POST
367
391
  relations:
368
392
  role:
369
393
  - where:
370
394
  slug: admin
395
+ - where:
396
+ slug: admin-user
371
397
  - url: /user/:userId
372
398
  method: GET
373
399
  relations:
374
400
  role:
375
401
  - where:
376
402
  slug: admin
403
+ - where:
404
+ slug: admin-user
377
405
  - url: /user
378
406
  method: POST
379
407
  relations:
380
408
  role:
381
409
  - where:
382
410
  slug: admin
411
+ - where:
412
+ slug: admin-user
383
413
  - url: /user/:userId/avatar
384
414
  method: POST
385
415
  relations:
386
416
  role:
387
417
  - where:
388
418
  slug: admin
419
+ - where:
420
+ slug: admin-user
389
421
  - url: /user/:userId
390
422
  method: PATCH
391
423
  relations:
392
424
  role:
393
425
  - where:
394
426
  slug: admin
427
+ - url: /user/:userId/reset-password
428
+ method: PATCH
429
+ relations:
430
+ role:
431
+ - where:
432
+ slug: admin
433
+ - where:
434
+ slug: admin-user
395
435
  - url: /user
396
436
  method: DELETE
397
437
  relations:
398
438
  role:
399
439
  - where:
400
440
  slug: admin
441
+ - where:
442
+ slug: admin-user
401
443
  - url: /locale
402
444
  method: GET
403
445
  relations:
@@ -652,12 +694,16 @@
652
694
  role:
653
695
  - where:
654
696
  slug: admin
697
+ - where:
698
+ slug: admin-access
655
699
  - url: /dashboard-core/user-dashboards
656
700
  method: GET
657
701
  relations:
658
702
  role:
659
703
  - where:
660
704
  slug: admin
705
+ - where:
706
+ slug: admin-access
661
707
  - url: /dashboard-core/stats/overview/users
662
708
  method: GET
663
709
  relations:
@@ -676,12 +722,16 @@
676
722
  role:
677
723
  - where:
678
724
  slug: admin
725
+ - where:
726
+ slug: admin-access
679
727
  - url: /dashboard-core/layout/:slug
680
728
  method: POST
681
729
  relations:
682
730
  role:
683
731
  - where:
684
732
  slug: admin
733
+ - where:
734
+ slug: admin-access
685
735
  - url: /dashboard-core/widgets/me
686
736
  method: GET
687
737
  relations:
@@ -694,12 +744,16 @@
694
744
  role:
695
745
  - where:
696
746
  slug: admin
747
+ - where:
748
+ slug: admin-access
697
749
  - url: /dashboard-core/widget/:slug/:widgetId
698
750
  method: DELETE
699
751
  relations:
700
752
  role:
701
753
  - where:
702
754
  slug: admin
755
+ - where:
756
+ slug: admin-access
703
757
  - url: /dashboard
704
758
  method: GET
705
759
  relations:
@@ -742,6 +796,8 @@
742
796
  role:
743
797
  - where:
744
798
  slug: admin
799
+ - where:
800
+ slug: admin-access
745
801
  - url: /dashboard-component
746
802
  method: POST
747
803
  relations:
@@ -1325,4 +1381,4 @@
1325
1381
  - where:
1326
1382
  slug: admin
1327
1383
  - where:
1328
- slug: admin-access
1384
+ slug: admin-access
@@ -25,7 +25,7 @@ import { useForm } from 'react-hook-form';
25
25
  import { z } from 'zod';
26
26
 
27
27
  export function ChangePasswordForm() {
28
- const { request, showToastHandler, getSettingValue } = useApp();
28
+ const { request, showToastHandler, getSettingValue, refetchUser } = useApp();
29
29
  const t = useTranslations('core.ChangePasswordForm');
30
30
  const minPasswordLength = getSettingValue('password-min-length') || 6;
31
31
  const minPasswordSymbols = getSettingValue('password-min-symbols') || 0;
@@ -105,6 +105,7 @@ export function ChangePasswordForm() {
105
105
  });
106
106
 
107
107
  showToastHandler('success', t('updateSuccess'));
108
+ await refetchUser();
108
109
 
109
110
  form.reset();
110
111
  } catch (error) {
@@ -59,7 +59,7 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
59
59
  : 'hsl(0, 84%, 60%)';
60
60
 
61
61
  return (
62
- <Card className="flex h-full flex-col">
62
+ <Card className="flex h-full min-h-0 flex-col overflow-hidden">
63
63
  <CardHeader className="shrink-0 pb-3">
64
64
  <div className="flex items-center gap-2">
65
65
  <ShieldCheck className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
@@ -71,8 +71,8 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
71
71
  </div>
72
72
  </div>
73
73
  </CardHeader>
74
- <CardContent className="flex-1 overflow-auto pt-0">
75
- <div className="mb-5 flex flex-col items-center gap-3 rounded-xl bg-muted/50 p-5">
74
+ <CardContent className="flex min-h-0 flex-1 overflow-auto pt-0">
75
+ <div className="mb-5 flex flex-col items-center gap-3 rounded-xl bg-muted/50 p-4 sm:p-5">
76
76
  <div className="flex items-baseline gap-1">
77
77
  <span className={`text-5xl font-bold tracking-tight ${scoreColor}`}>
78
78
  {score}
@@ -97,10 +97,10 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
97
97
  {data.checks.map((item) => {
98
98
  const Icon = ICON_MAP[item.id] ?? ShieldCheck;
99
99
  return (
100
- <div
101
- key={item.id}
102
- className="group flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-muted/50"
103
- >
100
+ <div
101
+ key={item.id}
102
+ className="group flex flex-wrap items-center gap-3 rounded-lg p-3 transition-colors hover:bg-muted/50"
103
+ >
104
104
  <div
105
105
  className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
106
106
  item.enabled
@@ -116,29 +116,29 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
116
116
  }`}
117
117
  />
118
118
  </div>
119
- <div className="flex min-w-0 flex-1 flex-col">
120
- <div className="flex items-center gap-2">
121
- <span className="text-sm font-medium text-foreground">
122
- {t(`labels.${item.labelKey}` as any) || item.labelKey}
123
- </span>
119
+ <div className="flex min-w-0 flex-1 flex-col">
120
+ <div className="flex flex-wrap items-center gap-2">
121
+ <span className="break-words text-sm font-medium text-foreground">
122
+ {t(`labels.${item.labelKey}` as any) || item.labelKey}
123
+ </span>
124
124
  {item.enabled ? (
125
125
  <CheckCircle2 className="h-3.5 w-3.5 text-emerald-500" />
126
126
  ) : (
127
127
  <AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
128
128
  )}
129
129
  </div>
130
- <span className="text-xs text-muted-foreground">
131
- {t(`descriptions.${item.descriptionKey}` as any) ||
132
- item.descriptionKey}
133
- </span>
134
- </div>
135
- {!item.enabled && (
136
- <Button
137
- variant="ghost"
138
- size="sm"
139
- onClick={() => router.push('/core/account/2fa')}
140
- className="shrink-0 gap-1 text-xs"
141
- >
130
+ <span className="text-xs text-muted-foreground">
131
+ {t(`descriptions.${item.descriptionKey}` as any) ||
132
+ item.descriptionKey}
133
+ </span>
134
+ </div>
135
+ {!item.enabled && (
136
+ <Button
137
+ variant="ghost"
138
+ size="sm"
139
+ onClick={() => router.push('/core/account/2fa')}
140
+ className="mt-2 w-full shrink-0 gap-1 text-xs sm:mt-0 sm:w-auto"
141
+ >
142
142
  {t('activate')}
143
143
  <ChevronRight className="h-3 w-3" />
144
144
  </Button>
@@ -119,7 +119,7 @@ function TimelineContent({ events }: { events: ActivityEvent[] }) {
119
119
  let lastDate = '';
120
120
 
121
121
  return (
122
- <Card className="flex h-full flex-col">
122
+ <Card className="flex h-full min-h-0 flex-col overflow-hidden">
123
123
  <CardHeader className="shrink-0 pb-3">
124
124
  <div className="flex items-center justify-between">
125
125
  <div>
@@ -168,9 +168,9 @@ function TimelineContent({ events }: { events: ActivityEvent[] }) {
168
168
  </div>
169
169
  <div className="flex min-w-0 flex-1 flex-col gap-0.5 rounded-lg px-2 py-1.5 transition-colors group-hover:bg-muted/40">
170
170
  <div className="flex items-center justify-between gap-2">
171
- <span className="text-sm font-medium leading-snug text-foreground">
172
- {event.action}
173
- </span>
171
+ <span className="min-w-0 break-words text-sm font-medium leading-snug text-foreground">
172
+ {event.action}
173
+ </span>
174
174
  <span className="shrink-0 text-[11px] tabular-nums text-muted-foreground">
175
175
  {formatTime(event.created_at)}
176
176
  </span>
@@ -85,7 +85,7 @@ export default function EmailNotifications({
85
85
  widgetName={widget?.name ?? 'email-notifications'}
86
86
  onRemove={onRemove}
87
87
  >
88
- <Card className="flex h-full flex-col">
88
+ <Card className="flex h-full min-h-0 flex-col overflow-hidden">
89
89
  <CardHeader className="shrink-0 pb-2">
90
90
  <div className="flex items-center gap-2">
91
91
  <Mail className="h-5 w-5 text-rose-600 dark:text-rose-400" />
@@ -99,15 +99,15 @@ export default function EmailNotifications({
99
99
  </div>
100
100
  </div>
101
101
  </CardHeader>
102
- <CardContent className="flex-1 overflow-auto pt-0">
103
- <div className="mb-4 grid grid-cols-4 gap-2">
104
- {emailStats.map((stat) => {
105
- const Icon = stat.icon;
106
- return (
107
- <div
108
- key={stat.label}
109
- className="flex flex-col items-center gap-1 rounded-lg border p-2.5 transition-colors hover:bg-muted/30"
110
- >
102
+ <CardContent className="flex min-h-0 flex-1 flex-col gap-4 overflow-hidden pt-0">
103
+ <div className="grid grid-cols-1 gap-2 md:grid-cols-2 xl:grid-cols-4">
104
+ {emailStats.map((stat) => {
105
+ const Icon = stat.icon;
106
+ return (
107
+ <div
108
+ key={stat.label}
109
+ className="flex min-w-0 flex-col items-center gap-1 rounded-lg border p-2.5 text-center transition-colors hover:bg-muted/30"
110
+ >
111
111
  <div
112
112
  className={`flex h-7 w-7 items-center justify-center rounded-md ${stat.bg}`}
113
113
  >
@@ -124,16 +124,20 @@ export default function EmailNotifications({
124
124
  })}
125
125
  </div>
126
126
 
127
- <ChartContainer config={chartConfig} className="h-[180px] w-full">
128
- <AreaChart data={data}>
127
+ <ChartContainer
128
+ config={chartConfig}
129
+ className="min-h-[220px] w-full flex-1 overflow-hidden"
130
+ >
131
+ <AreaChart data={data}>
129
132
  <CartesianGrid vertical={false} strokeDasharray="3 3" />
130
- <XAxis
131
- dataKey="date"
132
- tickLine={false}
133
- axisLine={false}
134
- fontSize={11}
135
- tickMargin={8}
136
- />
133
+ <XAxis
134
+ dataKey="date"
135
+ tickLine={false}
136
+ axisLine={false}
137
+ fontSize={11}
138
+ tickMargin={8}
139
+ minTickGap={24}
140
+ />
137
141
  <YAxis
138
142
  tickLine={false}
139
143
  axisLine={false}
@@ -28,7 +28,7 @@ function LoginChart({ data }: { data: LoginDay[] }) {
28
28
  };
29
29
 
30
30
  return (
31
- <Card className="flex h-full flex-col">
31
+ <Card className="flex h-full min-h-0 flex-col overflow-hidden">
32
32
  <CardHeader className="shrink-0 pb-2">
33
33
  <div className="flex items-center gap-2">
34
34
  <LogIn className="h-5 w-5 text-blue-600" />
@@ -40,20 +40,21 @@ function LoginChart({ data }: { data: LoginDay[] }) {
40
40
  </div>
41
41
  </div>
42
42
  </CardHeader>
43
- <CardContent className="flex flex-1 flex-col pt-0">
44
- <ChartContainer
45
- config={chartConfig}
46
- className="h-full min-h-[120px] w-full flex-1"
47
- >
48
- <BarChart data={data as any} barGap={2}>
43
+ <CardContent className="flex min-h-0 flex-1 flex-col overflow-hidden pt-0">
44
+ <ChartContainer
45
+ config={chartConfig}
46
+ className="h-full min-h-[160px] w-full flex-1 overflow-hidden"
47
+ >
48
+ <BarChart data={data as any} barGap={2}>
49
49
  <CartesianGrid vertical={false} strokeDasharray="3 3" />
50
- <XAxis
51
- dataKey="day"
52
- tickLine={false}
53
- axisLine={false}
54
- fontSize={12}
55
- tickMargin={8}
56
- />
50
+ <XAxis
51
+ dataKey="day"
52
+ tickLine={false}
53
+ axisLine={false}
54
+ fontSize={12}
55
+ tickMargin={8}
56
+ minTickGap={16}
57
+ />
57
58
  <YAxis
58
59
  tickLine={false}
59
60
  axisLine={false}
@@ -37,20 +37,24 @@ function CustomTooltip({
37
37
  );
38
38
  }
39
39
 
40
- interface PermissionsChartProps {
41
- widget?: any;
42
- onRemove?: () => void;
43
- }
44
-
45
- interface UserStatsData {
46
- charts?: {
47
- permissionDistribution?: Array<{
48
- name: string;
49
- value: number;
50
- color: string;
51
- }>;
52
- };
53
- }
40
+ interface PermissionsChartProps {
41
+ widget?: {
42
+ name?: string;
43
+ } | null;
44
+ onRemove?: () => void;
45
+ }
46
+
47
+ interface PermissionDistributionItem {
48
+ name: string;
49
+ value: number;
50
+ color: string;
51
+ }
52
+
53
+ interface UserStatsData {
54
+ charts?: {
55
+ permissionDistribution?: PermissionDistributionItem[];
56
+ };
57
+ }
54
58
 
55
59
  export default function PermissionsChart({
56
60
  widget,
@@ -66,10 +70,10 @@ export default function PermissionsChart({
66
70
  } = useWidgetData<UserStatsData>({
67
71
  endpoint: '/dashboard-core/stats/overview/users',
68
72
  queryKey: 'dashboard-stats-users',
69
- });
70
-
71
- const data = statsData?.charts?.permissionDistribution || [];
72
- const total = data.reduce((sum: number, item: any) => sum + item.value, 0);
73
+ });
74
+
75
+ const data = statsData?.charts?.permissionDistribution || [];
76
+ const total = data.reduce((sum, item) => sum + item.value, 0);
73
77
 
74
78
  return (
75
79
  <WidgetWrapper
@@ -86,19 +90,19 @@ export default function PermissionsChart({
86
90
  >
87
91
  <IconGripVertical className="text-muted-foreground/50 size-4 shrink-0" />
88
92
  </div>
89
- <CardHeader className="pb-2 pt-4 pl-10">
90
- <CardTitle className="text-base font-semibold">
91
- {t('permissionsDistributionTitle')}
92
- </CardTitle>
93
+ <CardHeader className="pb-2 pt-4 pl-10">
94
+ <CardTitle className="text-base font-semibold">
95
+ {t('permissionsDistributionTitle')}
96
+ </CardTitle>
93
97
  <CardDescription>
94
98
  {t('permissionsDistributionDescription')}
95
99
  </CardDescription>
96
- </CardHeader>
97
- <CardContent className="flex-1 pt-0">
98
- <div className="flex justify-between items-center gap-4">
99
- <div className="relative h-[280px] w-[280px] shrink-0">
100
- <ResponsiveContainer width="100%" height="100%">
101
- <PieChart>
100
+ </CardHeader>
101
+ <CardContent className="flex-1 pt-0">
102
+ <div className="flex h-full items-center justify-between gap-4 overflow-hidden">
103
+ <div className="relative h-[280px] w-[280px] shrink-0">
104
+ <ResponsiveContainer width="100%" height="100%">
105
+ <PieChart>
102
106
  <Pie
103
107
  data={data}
104
108
  cx="50%"
@@ -106,14 +110,14 @@ export default function PermissionsChart({
106
110
  innerRadius={70}
107
111
  outerRadius={110}
108
112
  paddingAngle={4}
109
- dataKey="value"
110
- animationDuration={1200}
111
- strokeWidth={0}
112
- >
113
- {data.map((entry: any, index: number) => (
114
- <Cell key={`cell-${index}`} fill={entry.color} />
115
- ))}
116
- </Pie>
113
+ dataKey="value"
114
+ animationDuration={1200}
115
+ strokeWidth={0}
116
+ >
117
+ {data.map((entry, index: number) => (
118
+ <Cell key={`cell-${index}`} fill={entry.color} />
119
+ ))}
120
+ </Pie>
117
121
  <Tooltip content={<CustomTooltip />} />
118
122
  </PieChart>
119
123
  </ResponsiveContainer>
@@ -121,28 +125,28 @@ export default function PermissionsChart({
121
125
  <span className="text-2xl font-bold text-foreground">
122
126
  {total}
123
127
  </span>
124
- <span className="text-[10px] text-muted-foreground">
125
- {t('total')}
126
- </span>
127
- </div>
128
- </div>
129
- <div className="grid grid-cols-2 gap-3">
130
- {data.map((item: any) => (
131
- <div key={item.name} className="flex items-center gap-3">
132
- <div
133
- className="h-3 w-3 rounded-sm"
134
- style={{ backgroundColor: item.color }}
135
- />
136
- <div className="flex flex-col">
137
- <span className="text-sm font-medium text-foreground">
138
- {item.name}
139
- </span>
140
- <span className="text-xs text-muted-foreground">
141
- {item.value} ({Math.round((item.value / total) * 100)}%)
142
- </span>
143
- </div>
144
- </div>
145
- ))}
128
+ <span className="text-[10px] text-muted-foreground">
129
+ {t('total')}
130
+ </span>
131
+ </div>
132
+ </div>
133
+ <div className="grid max-h-[280px] min-w-0 flex-1 grid-cols-2 content-start gap-x-4 gap-y-2 overflow-y-auto pr-2">
134
+ {data.map((item) => (
135
+ <div key={item.name} className="flex min-w-0 items-start gap-2.5">
136
+ <div
137
+ className="mt-1 h-2.5 w-2.5 shrink-0 rounded-sm"
138
+ style={{ backgroundColor: item.color }}
139
+ />
140
+ <div className="flex min-w-0 flex-col">
141
+ <span className="break-words text-xs font-medium leading-tight text-foreground">
142
+ {item.name}
143
+ </span>
144
+ <span className="text-[11px] text-muted-foreground">
145
+ {item.value} ({total ? Math.round((item.value / total) * 100) : 0}%)
146
+ </span>
147
+ </div>
148
+ </div>
149
+ ))}
146
150
  </div>
147
151
  </div>
148
152
  </CardContent>
@@ -34,24 +34,24 @@ export default function StatAccessLevel({
34
34
  widgetName={widget?.name ?? 'stat-access-level'}
35
35
  onRemove={onRemove}
36
36
  >
37
- <Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
38
- <CardContent className="flex h-full items-center gap-4 p-4">
39
- <div className="flex h-11 w-11 shrink-0 items-center justify-center rounded-xl bg-amber-50 dark:bg-amber-950/40">
40
- <Zap className="h-5 w-5 text-amber-600 dark:text-amber-400" />
41
- </div>
42
- <div className="flex min-w-0 flex-col">
43
- <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
44
- {t('accessLevel')}
45
- </span>
46
- <span className="text-2xl font-bold tracking-tight text-foreground">
47
- {data ? t('accessLevelValue', { level: data }) : '—'}
48
- </span>
49
- <span className="text-[11px] text-muted-foreground">
50
- {t('accessLevelSubtitle')}
51
- </span>
52
- </div>
53
- </CardContent>
54
- </Card>
37
+ <Card className="h-full overflow-hidden transition-all duration-300 hover:shadow-md">
38
+ <CardContent className="flex h-full items-center gap-3 p-3 md:gap-4 md:p-4">
39
+ <div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-xl bg-amber-50 dark:bg-amber-950/40 md:h-11 md:w-11">
40
+ <Zap className="h-4 w-4 text-amber-600 dark:text-amber-400 md:h-5 md:w-5" />
41
+ </div>
42
+ <div className="flex min-w-0 flex-col">
43
+ <span className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
44
+ {t('accessLevel')}
45
+ </span>
46
+ <span className="truncate text-xl font-bold tracking-tight text-foreground md:text-2xl">
47
+ {data ? t('accessLevelValue', { level: data }) : '—'}
48
+ </span>
49
+ <span className="hidden text-[11px] text-muted-foreground sm:block">
50
+ {t('accessLevelSubtitle')}
51
+ </span>
52
+ </div>
53
+ </CardContent>
54
+ </Card>
55
55
  </WidgetWrapper>
56
56
  );
57
57
  }