@hed-hog/core 0.0.276 → 0.0.279

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 (77) hide show
  1. package/README.md +60 -0
  2. package/dist/auth/auth.controller.d.ts +8 -1
  3. package/dist/auth/auth.controller.d.ts.map +1 -1
  4. package/dist/auth/auth.controller.js +7 -7
  5. package/dist/auth/auth.controller.js.map +1 -1
  6. package/dist/auth/auth.service.d.ts +10 -1
  7. package/dist/auth/auth.service.d.ts.map +1 -1
  8. package/dist/auth/auth.service.js +34 -8
  9. package/dist/auth/auth.service.js.map +1 -1
  10. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts +12 -0
  11. package/dist/dashboard/dashboard-core/dashboard-core.controller.d.ts.map +1 -1
  12. package/dist/dashboard/dashboard-core/dashboard-core.controller.js +9 -0
  13. package/dist/dashboard/dashboard-core/dashboard-core.controller.js.map +1 -1
  14. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts +12 -0
  15. package/dist/dashboard/dashboard-core/dashboard-core.service.d.ts.map +1 -1
  16. package/dist/dashboard/dashboard-core/dashboard-core.service.js +25 -0
  17. package/dist/dashboard/dashboard-core/dashboard-core.service.js.map +1 -1
  18. package/dist/profile/profile.service.js +1 -1
  19. package/dist/profile/profile.service.js.map +1 -1
  20. package/dist/role/guards/role.guard.d.ts +1 -0
  21. package/dist/role/guards/role.guard.d.ts.map +1 -1
  22. package/dist/role/guards/role.guard.js +18 -0
  23. package/dist/role/guards/role.guard.js.map +1 -1
  24. package/dist/session/session.service.js +1 -1
  25. package/dist/session/session.service.js.map +1 -1
  26. package/dist/user/dto/reset-password.dto.d.ts +4 -0
  27. package/dist/user/dto/reset-password.dto.d.ts.map +1 -0
  28. package/dist/user/dto/reset-password.dto.js +26 -0
  29. package/dist/user/dto/reset-password.dto.js.map +1 -0
  30. package/dist/user/user.controller.d.ts +5 -0
  31. package/dist/user/user.controller.d.ts.map +1 -1
  32. package/dist/user/user.controller.js +13 -0
  33. package/dist/user/user.controller.js.map +1 -1
  34. package/dist/user/user.service.d.ts +6 -0
  35. package/dist/user/user.service.d.ts.map +1 -1
  36. package/dist/user/user.service.js +65 -0
  37. package/dist/user/user.service.js.map +1 -1
  38. package/hedhog/data/dashboard_component.yaml +74 -12
  39. package/hedhog/data/dashboard_component_role.yaml +223 -145
  40. package/hedhog/data/dashboard_item.yaml +42 -22
  41. package/hedhog/data/dashboard_role.yaml +18 -12
  42. package/hedhog/data/menu.yaml +6 -0
  43. package/hedhog/data/route.yaml +65 -1
  44. package/hedhog/frontend/app/account/components/change-password-form.tsx.ejs +2 -1
  45. package/hedhog/frontend/app/ai_agent/page.tsx.ejs +17 -17
  46. package/hedhog/frontend/app/dashboard/[slug]/dashboard-content.tsx.ejs +23 -12
  47. package/hedhog/frontend/app/dashboard/components/draggable-grid.tsx.ejs +80 -5
  48. package/hedhog/frontend/app/dashboard/components/widgets/account-security.tsx.ejs +17 -13
  49. package/hedhog/frontend/app/dashboard/components/widgets/activity-timeline.tsx.ejs +16 -12
  50. package/hedhog/frontend/app/dashboard/components/widgets/email-notifications.tsx.ejs +27 -16
  51. package/hedhog/frontend/app/dashboard/components/widgets/login-history-chart.tsx.ejs +13 -9
  52. package/hedhog/frontend/app/dashboard/components/widgets/menus-card.tsx.ejs +58 -0
  53. package/hedhog/frontend/app/dashboard/components/widgets/permissions-chart.tsx.ejs +62 -58
  54. package/hedhog/frontend/app/dashboard/components/widgets/routes-card.tsx.ejs +58 -0
  55. package/hedhog/frontend/app/dashboard/components/widgets/stat-access-level.tsx.ejs +6 -6
  56. package/hedhog/frontend/app/dashboard/components/widgets/stat-actions-today.tsx.ejs +6 -6
  57. package/hedhog/frontend/app/dashboard/components/widgets/stat-consecutive-days.tsx.ejs +6 -6
  58. package/hedhog/frontend/app/dashboard/components/widgets/stat-online-time.tsx.ejs +6 -6
  59. package/hedhog/frontend/app/dashboard/components/widgets/user-roles.tsx.ejs +15 -11
  60. package/hedhog/frontend/app/dashboard/components/widgets/user-sessions.tsx.ejs +18 -15
  61. package/hedhog/frontend/app/dashboard/dashboard.css.ejs +20 -4
  62. package/hedhog/frontend/app/dashboard/page.tsx.ejs +29 -14
  63. package/hedhog/frontend/app/mail/log/page.tsx.ejs +5 -11
  64. package/hedhog/frontend/app/users/page.tsx.ejs +331 -10
  65. package/hedhog/frontend/messages/en.json +29 -3
  66. package/hedhog/frontend/messages/pt.json +29 -3
  67. package/package.json +4 -4
  68. package/src/auth/auth.controller.ts +21 -20
  69. package/src/auth/auth.service.ts +63 -15
  70. package/src/dashboard/dashboard-core/dashboard-core.controller.ts +5 -0
  71. package/src/dashboard/dashboard-core/dashboard-core.service.ts +34 -0
  72. package/src/profile/profile.service.ts +1 -1
  73. package/src/role/guards/role.guard.ts +36 -7
  74. package/src/session/session.service.ts +2 -2
  75. package/src/user/dto/reset-password.dto.ts +11 -0
  76. package/src/user/user.controller.ts +24 -14
  77. package/src/user/user.service.ts +84 -0
@@ -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:
@@ -670,18 +716,30 @@
670
716
  role:
671
717
  - where:
672
718
  slug: admin-mail
719
+ - url: /dashboard-core/stats/overview/system
720
+ method: GET
721
+ relations:
722
+ role:
723
+ - where:
724
+ slug: admin
725
+ - where:
726
+ slug: admin-access
673
727
  - url: /dashboard-core/layout/:slug
674
728
  method: GET
675
729
  relations:
676
730
  role:
677
731
  - where:
678
732
  slug: admin
733
+ - where:
734
+ slug: admin-access
679
735
  - url: /dashboard-core/layout/:slug
680
736
  method: POST
681
737
  relations:
682
738
  role:
683
739
  - where:
684
740
  slug: admin
741
+ - where:
742
+ slug: admin-access
685
743
  - url: /dashboard-core/widgets/me
686
744
  method: GET
687
745
  relations:
@@ -694,12 +752,16 @@
694
752
  role:
695
753
  - where:
696
754
  slug: admin
755
+ - where:
756
+ slug: admin-access
697
757
  - url: /dashboard-core/widget/:slug/:widgetId
698
758
  method: DELETE
699
759
  relations:
700
760
  role:
701
761
  - where:
702
762
  slug: admin
763
+ - where:
764
+ slug: admin-access
703
765
  - url: /dashboard
704
766
  method: GET
705
767
  relations:
@@ -742,6 +804,8 @@
742
804
  role:
743
805
  - where:
744
806
  slug: admin
807
+ - where:
808
+ slug: admin-access
745
809
  - url: /dashboard-component
746
810
  method: POST
747
811
  relations:
@@ -1325,4 +1389,4 @@
1325
1389
  - where:
1326
1390
  slug: admin
1327
1391
  - where:
1328
- slug: admin-access
1392
+ 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) {
@@ -115,7 +115,7 @@ export default function AiAgentPage() {
115
115
  },
116
116
  });
117
117
 
118
- const { data, isLoading, refetch } = useQuery<PaginatedResponse<Agent>>({
118
+ const { data, isLoading, refetch } = useQuery<PaginatedResponse<Agent>>({
119
119
  queryKey: ['ai-agents', page, pageSize, debouncedSearch],
120
120
  queryFn: async () => {
121
121
  const params = new URLSearchParams();
@@ -130,14 +130,14 @@ export default function AiAgentPage() {
130
130
 
131
131
  return response.data;
132
132
  },
133
- initialData: {
134
- data: [],
135
- total: 0,
136
- page: 1,
137
- pageSize: 10,
138
- totalPages: 1,
139
- },
140
- });
133
+ });
134
+ const agentList = data ?? {
135
+ data: [],
136
+ total: 0,
137
+ page: 1,
138
+ pageSize: 10,
139
+ totalPages: 1,
140
+ };
141
141
 
142
142
  useEffect(() => {
143
143
  if (editingAgent) {
@@ -280,12 +280,12 @@ export default function AiAgentPage() {
280
280
  <TableRow>
281
281
  <TableCell colSpan={5}>{t('loading')}</TableCell>
282
282
  </TableRow>
283
- ) : data.data.length === 0 ? (
284
- <TableRow>
285
- <TableCell colSpan={5}>{t('empty')}</TableCell>
286
- </TableRow>
287
- ) : (
288
- data.data.map((agent) => (
283
+ ) : agentList.data.length === 0 ? (
284
+ <TableRow>
285
+ <TableCell colSpan={5}>{t('empty')}</TableCell>
286
+ </TableRow>
287
+ ) : (
288
+ agentList.data.map((agent) => (
289
289
  <TableRow key={agent.id}>
290
290
  <TableCell>{agent.slug}</TableCell>
291
291
  <TableCell className="uppercase">
@@ -322,8 +322,8 @@ export default function AiAgentPage() {
322
322
  <PaginationFooter
323
323
  currentPage={page}
324
324
  pageSize={pageSize}
325
- totalItems={data.total}
326
- onPageChange={setPage}
325
+ totalItems={agentList.total}
326
+ onPageChange={setPage}
327
327
  onPageSizeChange={(value) => {
328
328
  setPageSize(value);
329
329
  setPage(1);
@@ -12,6 +12,7 @@ import { Button } from '@/components/ui/button';
12
12
  import { Separator } from '@/components/ui/separator';
13
13
  import { SidebarTrigger } from '@/components/ui/sidebar';
14
14
  import { Skeleton } from '@/components/ui/skeleton';
15
+ import { useIsMobile } from '@/components/ui/use-mobile';
15
16
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
16
17
  import { IconDeviceFloppy } from '@tabler/icons-react';
17
18
  import { useTranslations } from 'next-intl';
@@ -38,6 +39,7 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
38
39
  const t = useTranslations('core.DashboardPage');
39
40
  const { request } = useApp();
40
41
  const router = useRouter();
42
+ const isMobile = useIsMobile();
41
43
 
42
44
  const [layout, setLayout] = useState<LayoutItem[]>([]);
43
45
  const [widgets, setWidgets] = useState<WidgetLayout[]>([]);
@@ -106,7 +108,10 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
106
108
  x: item.x,
107
109
  y: item.y,
108
110
  w: item.w,
109
- h: item.h,
111
+ h:
112
+ dashboardSlug === 'user' && item.slug === 'profile-card'
113
+ ? Math.max(item.h, 3)
114
+ : item.h,
110
115
  minW: item.minW || 1,
111
116
  maxW: item.maxW || 12,
112
117
  minH: item.minH || 1,
@@ -277,37 +282,42 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
277
282
 
278
283
  return (
279
284
  <>
280
- <header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12">
281
- <div className="flex w-full items-center justify-between gap-2 px-4">
282
- <div className="flex items-center gap-2">
285
+ <header className="flex min-h-16 shrink-0 items-center gap-2 py-2 transition-[width,height] ease-linear group-has-data-[collapsible=icon]/sidebar-wrapper:h-12 sm:h-16 sm:py-0">
286
+ <div className="flex w-full flex-col gap-2 px-4 sm:flex-row sm:items-center sm:justify-between">
287
+ <div className="flex min-w-0 items-center gap-2">
283
288
  <SidebarTrigger className="-ml-1" />
284
289
  <Separator
285
290
  orientation="vertical"
286
291
  className="mr-2 data-[orientation=vertical]:h-4"
287
292
  />
288
- <Breadcrumb>
293
+ <Breadcrumb className="min-w-0">
289
294
  <BreadcrumbList>
290
295
  <BreadcrumbItem className="hidden md:block">
291
296
  <BreadcrumbLink href="#">{t('dashboard')}</BreadcrumbLink>
292
297
  </BreadcrumbItem>
293
298
  <BreadcrumbSeparator className="hidden md:block" />
294
299
  <BreadcrumbItem>
295
- <BreadcrumbPage>{dashboardName}</BreadcrumbPage>
300
+ <BreadcrumbPage className="truncate">
301
+ {dashboardName}
302
+ </BreadcrumbPage>
296
303
  </BreadcrumbItem>
297
304
  </BreadcrumbList>
298
305
  </Breadcrumb>
299
306
  </div>
300
- <div className="flex items-center gap-2">
307
+ <div className="flex w-full items-center justify-end gap-2 sm:w-auto">
301
308
  {hasChanges && (
302
309
  <Button
303
310
  size="sm"
304
311
  variant="default"
305
- className="gap-2"
312
+ className="gap-1 px-2 sm:gap-2 sm:px-3"
306
313
  onClick={handleSaveLayout}
307
314
  disabled={isSaving}
315
+ aria-label={isSaving ? t('saving') : t('saveLayout')}
308
316
  >
309
317
  <IconDeviceFloppy className="size-4" />
310
- {isSaving ? t('saving') : t('saveLayout')}
318
+ <span className="hidden sm:inline">
319
+ {isSaving ? t('saving') : t('saveLayout')}
320
+ </span>
311
321
  </Button>
312
322
  )}
313
323
  <AddWidgetSelectorDialog
@@ -321,15 +331,16 @@ export const DashboardContent = ({ dashboardSlug }: DashboardContentProps) => {
321
331
  </header>
322
332
  <div className="flex flex-1 flex-col gap-4 overflow-auto p-4 pt-0">
323
333
  {widgets.length > 0 ? (
324
- <div className="min-h-[600px]">
334
+ <div className="min-h-[420px] sm:min-h-[520px] lg:min-h-[600px]">
325
335
  <DraggableGrid
326
336
  className="dashboard-grid"
327
337
  layout={layout}
328
338
  onLayoutChange={handleLayoutChange}
329
339
  cols={12}
330
340
  rowHeight={80}
331
- isDraggable={true}
332
- isResizable={true}
341
+ preventCollision
342
+ isDraggable={!isMobile}
343
+ isResizable={!isMobile}
333
344
  resizeHandles={['se']}
334
345
  >
335
346
  {widgets.map((widget) => (
@@ -36,6 +36,54 @@ interface DraggableGridProps {
36
36
  resizeHandles?: Array<'s' | 'w' | 'e' | 'n' | 'sw' | 'nw' | 'se' | 'ne'>;
37
37
  }
38
38
 
39
+ const MOBILE_WIDTH = 640;
40
+ const TABLET_WIDTH = 1024;
41
+
42
+ const clamp = (value: number, min: number, max: number) =>
43
+ Math.max(min, Math.min(max, value));
44
+
45
+ const deriveResponsiveLayout = (
46
+ layout: Layout,
47
+ effectiveCols: number,
48
+ cols: number
49
+ ): Layout => {
50
+ const sorted = [...layout].sort((a, b) => a.y - b.y || a.x - b.x);
51
+ const ratio = effectiveCols / cols;
52
+
53
+ let cursorX = 0;
54
+ let cursorY = 0;
55
+ let rowHeight = 0;
56
+
57
+ return sorted.map((item) => {
58
+ const nextW =
59
+ effectiveCols === 1
60
+ ? 1
61
+ : clamp(Math.round(item.w * ratio), 1, effectiveCols);
62
+ const nextH = Math.max(1, item.h || 1);
63
+
64
+ if (cursorX + nextW > effectiveCols) {
65
+ cursorY += Math.max(1, rowHeight);
66
+ cursorX = 0;
67
+ rowHeight = 0;
68
+ }
69
+
70
+ const mappedItem: LayoutItem = {
71
+ ...item,
72
+ x: cursorX,
73
+ y: cursorY,
74
+ w: nextW,
75
+ h: nextH,
76
+ minW: clamp(Math.min(item.minW || 1, nextW), 1, nextW),
77
+ maxW: clamp(item.maxW || effectiveCols, nextW, effectiveCols),
78
+ };
79
+
80
+ cursorX += nextW;
81
+ rowHeight = Math.max(rowHeight, nextH);
82
+
83
+ return mappedItem;
84
+ });
85
+ };
86
+
39
87
  // Simple compaction function for grid layout
40
88
  const compactLayout = (
41
89
  layout: RGLLayout,
@@ -64,7 +112,7 @@ export function DraggableGrid({
64
112
  isDraggable = true,
65
113
  isResizable = true,
66
114
  compactType = 'vertical',
67
- preventCollision = false,
115
+ preventCollision = true,
68
116
  margin = [16, 16],
69
117
  containerPadding = [0, 0],
70
118
  resizeHandles = ['se'],
@@ -72,6 +120,29 @@ export function DraggableGrid({
72
120
  const containerRef = useRef<HTMLDivElement>(null);
73
121
  const [containerWidth, setContainerWidth] = useState(1200);
74
122
 
123
+ const effectiveCols =
124
+ containerWidth < MOBILE_WIDTH
125
+ ? 1
126
+ : containerWidth < TABLET_WIDTH
127
+ ? Math.min(6, cols)
128
+ : cols;
129
+
130
+ const effectiveRowHeight =
131
+ containerWidth < MOBILE_WIDTH ? Math.max(76, rowHeight - 4) : rowHeight;
132
+
133
+ const effectiveMargin: [number, number] =
134
+ containerWidth < MOBILE_WIDTH
135
+ ? [Math.min(10, margin[0]), Math.min(10, margin[1])]
136
+ : margin;
137
+
138
+ const responsiveLayout = (() => {
139
+ if (effectiveCols === cols) {
140
+ return layout;
141
+ }
142
+
143
+ return deriveResponsiveLayout(layout, effectiveCols, cols);
144
+ })();
145
+
75
146
  useEffect(() => {
76
147
  const updateWidth = () => {
77
148
  if (containerRef.current) {
@@ -91,6 +162,10 @@ export function DraggableGrid({
91
162
  }, []);
92
163
 
93
164
  const handleLayoutChange = (newLayout: RGLLayout) => {
165
+ if (effectiveCols !== cols) {
166
+ return;
167
+ }
168
+
94
169
  const layouts = Array.isArray(newLayout) ? newLayout : [newLayout];
95
170
  const convertedLayout = layouts.map((item: any) => ({
96
171
  i: item.i,
@@ -111,12 +186,12 @@ export function DraggableGrid({
111
186
  <div ref={containerRef} className="w-full">
112
187
  <GridLayout
113
188
  className={`layout ${className}`}
114
- layout={layout}
189
+ layout={responsiveLayout}
115
190
  gridConfig={{
116
- cols,
117
- rowHeight,
191
+ cols: effectiveCols,
192
+ rowHeight: effectiveRowHeight,
118
193
  containerPadding,
119
- margin,
194
+ margin: effectiveMargin,
120
195
  }}
121
196
  dragConfig={{
122
197
  enabled: isDraggable,
@@ -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,13 +71,17 @@ 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 flex-col overflow-auto pt-0">
75
+ <div className="mb-4 flex flex-col items-center gap-2.5 rounded-xl bg-muted/50 p-3 sm:mb-5 sm:gap-3 sm:p-5">
76
76
  <div className="flex items-baseline gap-1">
77
- <span className={`text-5xl font-bold tracking-tight ${scoreColor}`}>
77
+ <span
78
+ className={`text-4xl font-bold tracking-tight sm:text-5xl ${scoreColor}`}
79
+ >
78
80
  {score}
79
81
  </span>
80
- <span className="text-lg text-muted-foreground">/100</span>
82
+ <span className="text-base text-muted-foreground sm:text-lg">
83
+ /100
84
+ </span>
81
85
  </div>
82
86
  <Progress
83
87
  value={score}
@@ -88,7 +92,7 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
88
92
  } as any
89
93
  }
90
94
  />
91
- <p className="text-xs text-muted-foreground">
95
+ <p className="text-[11px] text-muted-foreground sm:text-xs">
92
96
  {score >= 80 ? t('wellProtected') : t('recommendProtections')}
93
97
  </p>
94
98
  </div>
@@ -99,17 +103,17 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
99
103
  return (
100
104
  <div
101
105
  key={item.id}
102
- className="group flex items-center gap-3 rounded-lg p-3 transition-colors hover:bg-muted/50"
106
+ className="group flex flex-wrap items-start gap-2.5 rounded-lg p-2.5 transition-colors hover:bg-muted/50 sm:items-center sm:gap-3 sm:p-3"
103
107
  >
104
108
  <div
105
- className={`flex h-9 w-9 shrink-0 items-center justify-center rounded-lg ${
109
+ className={`flex h-8 w-8 shrink-0 items-center justify-center rounded-lg sm:h-9 sm:w-9 ${
106
110
  item.enabled
107
111
  ? 'bg-emerald-50 dark:bg-emerald-950/40'
108
112
  : 'bg-muted'
109
113
  }`}
110
114
  >
111
115
  <Icon
112
- className={`h-4 w-4 ${
116
+ className={`h-3.5 w-3.5 sm:h-4 sm:w-4 ${
113
117
  item.enabled
114
118
  ? 'text-emerald-600 dark:text-emerald-400'
115
119
  : 'text-muted-foreground'
@@ -117,8 +121,8 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
117
121
  />
118
122
  </div>
119
123
  <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">
124
+ <div className="flex flex-wrap items-center gap-2">
125
+ <span className="wrap-break-word text-[13px] font-medium text-foreground sm:text-sm">
122
126
  {t(`labels.${item.labelKey}` as any) || item.labelKey}
123
127
  </span>
124
128
  {item.enabled ? (
@@ -127,7 +131,7 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
127
131
  <AlertTriangle className="h-3.5 w-3.5 text-amber-500" />
128
132
  )}
129
133
  </div>
130
- <span className="text-xs text-muted-foreground">
134
+ <span className="text-[11px] text-muted-foreground sm:text-xs">
131
135
  {t(`descriptions.${item.descriptionKey}` as any) ||
132
136
  item.descriptionKey}
133
137
  </span>
@@ -137,7 +141,7 @@ function AccountSecurityContent({ data }: { data: AccountSecurityData }) {
137
141
  variant="ghost"
138
142
  size="sm"
139
143
  onClick={() => router.push('/core/account/2fa')}
140
- className="shrink-0 gap-1 text-xs"
144
+ className="mt-1 w-full shrink-0 gap-1 text-xs sm:mt-0 sm:w-auto"
141
145
  >
142
146
  {t('activate')}
143
147
  <ChevronRight className="h-3 w-3" />
@@ -119,24 +119,26 @@ 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>
126
- <CardTitle className="text-base font-semibold">
126
+ <CardTitle className="text-sm font-semibold sm:text-base">
127
127
  {t('title')}
128
128
  </CardTitle>
129
- <CardDescription>{t('description')}</CardDescription>
129
+ <CardDescription className="text-xs sm:text-sm">
130
+ {t('description')}
131
+ </CardDescription>
130
132
  </div>
131
- <Badge variant="secondary">
133
+ <Badge variant="secondary" className="text-[10px] sm:text-xs">
132
134
  {t('events', { count: events.length })}
133
135
  </Badge>
134
136
  </div>
135
137
  </CardHeader>
136
138
  <CardContent className="flex min-h-0 flex-1 flex-col overflow-hidden pt-0">
137
- <ScrollArea className="h-full pr-3">
139
+ <ScrollArea className="h-full pr-2 sm:pr-3">
138
140
  <div className="relative flex flex-col pb-2">
139
- <div className="absolute bottom-0 left-[15px] top-0 w-px bg-border/60" />
141
+ <div className="absolute bottom-0 left-[13px] top-0 w-px bg-border/60 sm:left-[15px]" />
140
142
  {events.map((event, index) => {
141
143
  const type = detectType(event.action);
142
144
  const config =
@@ -155,23 +157,25 @@ function TimelineContent({ events }: { events: ActivityEvent[] }) {
155
157
  <div key={event.id}>
156
158
  {showDate && (
157
159
  <div className="relative z-10 mb-1 mt-4 first:mt-0">
158
- <span className="ml-11 inline-block rounded-md border border-border bg-muted px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-muted-foreground">
160
+ <span className="ml-9 inline-block rounded-md border border-border bg-muted px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground sm:ml-11 sm:px-2 sm:text-[11px]">
159
161
  {dateLabel}
160
162
  </span>
161
163
  </div>
162
164
  )}
163
165
  <div className="group relative flex items-start gap-3 py-1.5">
164
166
  <div
165
- className={`relative z-10 mt-0.5 flex h-8 w-8 shrink-0 items-center justify-center rounded-lg ${config.bg}`}
167
+ className={`relative z-10 mt-0.5 flex h-7 w-7 shrink-0 items-center justify-center rounded-lg sm:h-8 sm:w-8 ${config.bg}`}
166
168
  >
167
- <Icon className={`h-3.5 w-3.5 ${config.color}`} />
169
+ <Icon
170
+ className={`h-3 w-3 sm:h-3.5 sm:w-3.5 ${config.color}`}
171
+ />
168
172
  </div>
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">
173
+ <div className="flex min-w-0 flex-1 flex-col gap-0.5 rounded-lg px-1.5 py-1.5 transition-colors group-hover:bg-muted/40 sm:px-2">
170
174
  <div className="flex items-center justify-between gap-2">
171
- <span className="text-sm font-medium leading-snug text-foreground">
175
+ <span className="min-w-0 wrap-break-word text-xs font-medium leading-snug text-foreground sm:text-sm">
172
176
  {event.action}
173
177
  </span>
174
- <span className="shrink-0 text-[11px] tabular-nums text-muted-foreground">
178
+ <span className="shrink-0 text-[10px] tabular-nums text-muted-foreground sm:text-[11px]">
175
179
  {formatTime(event.created_at)}
176
180
  </span>
177
181
  </div>