@svadmin/ui 0.34.10 → 0.36.0

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 (71) hide show
  1. package/dist/app.css +746 -22
  2. package/dist/component-registry.svelte.d.ts +1 -0
  3. package/dist/components/AdminApp.svelte +94 -15
  4. package/dist/components/AdminApp.svelte.d.ts +32 -2
  5. package/dist/components/ApiSettings.svelte +191 -0
  6. package/dist/components/ApiSettings.svelte.d.ts +3 -0
  7. package/dist/components/AutoForm.svelte +47 -42
  8. package/dist/components/AutoTable.svelte +29 -24
  9. package/dist/components/ErrorPage.svelte +58 -0
  10. package/dist/components/ErrorPage.svelte.d.ts +8 -0
  11. package/dist/components/FieldRenderer.svelte +2 -2
  12. package/dist/components/ForgotPasswordPage.svelte +111 -68
  13. package/dist/components/Header.svelte +6 -7
  14. package/dist/components/IntegrationsSettings.svelte +70 -0
  15. package/dist/components/IntegrationsSettings.svelte.d.ts +4 -0
  16. package/dist/components/Layout.svelte +7 -7
  17. package/dist/components/LoginPage.svelte +207 -105
  18. package/dist/components/LoginPage.svelte.d.ts +3 -15
  19. package/dist/components/NotificationsSettings.svelte +115 -0
  20. package/dist/components/NotificationsSettings.svelte.d.ts +3 -0
  21. package/dist/components/PageHeader.svelte +9 -7
  22. package/dist/components/RegisterPage.svelte +103 -65
  23. package/dist/components/ResourceOperationsPage.svelte +151 -0
  24. package/dist/components/ResourceOperationsPage.svelte.d.ts +37 -0
  25. package/dist/components/SecuritySettings.svelte +179 -0
  26. package/dist/components/SecuritySettings.svelte.d.ts +4 -0
  27. package/dist/components/SettingsPage.svelte +120 -87
  28. package/dist/components/SettingsPage.svelte.d.ts +25 -1
  29. package/dist/components/ShowPage.svelte +9 -9
  30. package/dist/components/Sidebar.svelte +278 -189
  31. package/dist/components/SidebarItem.svelte +77 -18
  32. package/dist/components/TwoFactorAuthPage.svelte +247 -0
  33. package/dist/components/TwoFactorAuthPage.svelte.d.ts +3 -0
  34. package/dist/components/UpdatePasswordPage.svelte +86 -48
  35. package/dist/components/account/CompanyProfilePage.svelte +99 -0
  36. package/dist/components/account/CompanyProfilePage.svelte.d.ts +3 -0
  37. package/dist/components/account/GetStartedPage.svelte +123 -0
  38. package/dist/components/account/GetStartedPage.svelte.d.ts +4 -0
  39. package/dist/components/account/ImportMembersPage.svelte +143 -0
  40. package/dist/components/account/ImportMembersPage.svelte.d.ts +3 -0
  41. package/dist/components/account/MembersStarterPage.svelte +104 -0
  42. package/dist/components/account/MembersStarterPage.svelte.d.ts +3 -0
  43. package/dist/components/account/SecurityLogPage.svelte +135 -0
  44. package/dist/components/account/SecurityLogPage.svelte.d.ts +3 -0
  45. package/dist/components/account/SettingsEnterprisePage.svelte +131 -0
  46. package/dist/components/account/SettingsEnterprisePage.svelte.d.ts +3 -0
  47. package/dist/components/account/SettingsPlainPage.svelte +81 -0
  48. package/dist/components/account/SettingsPlainPage.svelte.d.ts +3 -0
  49. package/dist/components/account/SettingsSidebarPage.svelte +98 -0
  50. package/dist/components/account/SettingsSidebarPage.svelte.d.ts +3 -0
  51. package/dist/components/account/TeamMembersPage.svelte +98 -0
  52. package/dist/components/account/TeamMembersPage.svelte.d.ts +3 -0
  53. package/dist/components/network/TeamCrewTablePage.svelte +155 -0
  54. package/dist/components/network/TeamCrewTablePage.svelte.d.ts +3 -0
  55. package/dist/components/network/UserCardsNFTPage.svelte +134 -0
  56. package/dist/components/network/UserCardsNFTPage.svelte.d.ts +3 -0
  57. package/dist/components/profile/ActivityTimeline.svelte +77 -0
  58. package/dist/components/profile/ActivityTimeline.svelte.d.ts +16 -0
  59. package/dist/components/profile/ProfileCard.svelte +216 -0
  60. package/dist/components/profile/ProfileCard.svelte.d.ts +27 -0
  61. package/dist/components/profile/ProjectsGrid.svelte +91 -0
  62. package/dist/components/profile/ProjectsGrid.svelte.d.ts +17 -0
  63. package/dist/components/profile/PublicProfilePage.svelte +179 -0
  64. package/dist/components/profile/PublicProfilePage.svelte.d.ts +10 -0
  65. package/dist/components/profile/TeamsShowcase.svelte +98 -0
  66. package/dist/components/profile/TeamsShowcase.svelte.d.ts +19 -0
  67. package/dist/components/ui/pagination/pagination-ellipsis.svelte +1 -1
  68. package/dist/index.d.ts +23 -0
  69. package/dist/index.js +27 -0
  70. package/dist/router-state.svelte.js +17 -0
  71. package/package.json +2 -2
@@ -484,13 +484,17 @@
484
484
  }
485
485
  </script>
486
486
 
487
- <div class="space-y-4">
487
+ <div class="space-y-5" data-svadmin-list>
488
488
  <!-- Header -->
489
- <div class="flex flex-wrap items-center justify-between gap-2">
490
- <h1 class="text-lg sm:text-xl font-semibold text-foreground">{resource.label}</h1>
491
- <div class="flex flex-wrap items-center gap-2">
489
+ <div class="rounded-3xl border border-border/70 bg-card/90 p-5 shadow-sm shadow-slate-900/[0.03] backdrop-blur" data-svadmin-list-header>
490
+ <div class="flex flex-wrap items-center justify-between gap-4">
491
+ <div class="min-w-0">
492
+ <h1 class="truncate text-2xl font-semibold tracking-tight text-foreground">{resource.label}</h1>
493
+ <p class="mt-1 text-sm text-muted-foreground">{t('common.list')}</p>
494
+ </div>
495
+ <div class="flex flex-wrap items-center gap-2" data-svadmin-list-actions>
492
496
  {#if canExport}
493
- <Button variant="outline" size="sm" onclick={exportCSV}>
497
+ <Button variant="outline" size="sm" class="h-10 rounded-xl" onclick={exportCSV}>
494
498
  <Download class="h-4 w-4" data-icon="inline-start" /> {t('common.export')}
495
499
  </Button>
496
500
  {/if}
@@ -513,8 +517,8 @@
513
517
  <DropdownMenu.Root>
514
518
  <DropdownMenu.Trigger>
515
519
  {#snippet child({ props })}
516
- <Button variant="outline" size="sm" {...props}>
517
- <SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {t('common.columns') || 'Columns'}
520
+ <Button variant="outline" size="sm" class="h-10 rounded-xl" {...props}>
521
+ <SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {t('common.columns')}
518
522
  </Button>
519
523
  {/snippet}
520
524
  </DropdownMenu.Trigger>
@@ -533,15 +537,16 @@
533
537
  {@render headerActions()}
534
538
  {/if}
535
539
  {#if canCreate}
536
- <Button onclick={() => navigate(`/${resourceName}/create`)}>
540
+ <Button class="h-10 rounded-xl px-4 shadow-sm shadow-primary/15" onclick={() => navigate(`/${resourceName}/create`)}>
537
541
  <Plus class="h-4 w-4" data-icon="inline-start" /> {t('common.create')}
538
542
  </Button>
539
543
  {/if}
544
+ </div>
540
545
  </div>
541
546
  </div>
542
547
 
543
548
  <!-- Search and Advanced Filters -->
544
- <div class="flex flex-wrap items-center gap-2">
549
+ <div class="flex flex-wrap items-center gap-3 rounded-3xl border border-border/70 bg-card/80 p-3 shadow-sm shadow-slate-900/[0.02] backdrop-blur" data-svadmin-table-toolbar>
545
550
  {#if searchableFields.length > 0}
546
551
  <div class="relative max-w-sm flex-1 sm:min-w-[250px]">
547
552
  <Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
@@ -550,7 +555,7 @@
550
555
  bind:value={searchText}
551
556
  oninput={() => { pagination = { ...pagination, current: 1 }; }}
552
557
  placeholder={t('common.search')}
553
- class="pl-10 h-9"
558
+ class="h-10 rounded-xl border-border/70 bg-background/85 pl-10"
554
559
  />
555
560
  </div>
556
561
  {/if}
@@ -559,7 +564,7 @@
559
564
  <Popover.Root>
560
565
  <Popover.Trigger>
561
566
  {#snippet child({ props })}
562
- <Button variant="outline" size="sm" class="h-9 px-3" {...props}>
567
+ <Button variant="outline" size="sm" class="h-10 rounded-xl px-3" {...props}>
563
568
  <FilterIcon class="h-4 w-4" data-icon="inline-start" />
564
569
  {t('common.filter')}
565
570
  {#if activeFilterCount > 0}
@@ -577,7 +582,7 @@
577
582
  {#if field.type === 'select' && field.options}
578
583
  <select
579
584
  id="filter-{field.key}"
580
- class="h-9 text-sm w-full font-normal flex w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
585
+ class="flex h-10 w-full items-center justify-between rounded-xl border border-input bg-background px-3 py-2 text-sm font-normal ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
581
586
  value={filterValues[field.key] ?? ''}
582
587
  onchange={(e) => filterValues[field.key] = (e.currentTarget as HTMLSelectElement).value}
583
588
  >
@@ -593,7 +598,7 @@
593
598
  value={filterValues[field.key] ?? ''}
594
599
  oninput={(e) => filterValues[field.key] = (e.currentTarget as HTMLInputElement).value}
595
600
  placeholder={field.label}
596
- class="h-9 text-sm"
601
+ class="h-10 rounded-xl text-sm"
597
602
  />
598
603
  {/if}
599
604
  </div>
@@ -616,7 +621,7 @@
616
621
  </div>
617
622
 
618
623
  <!-- Table (TanStack-powered) -->
619
- <div class="rounded-[24px] bg-card/80 backdrop-blur-3xl shadow-[0_8px_30px_rgb(0,0,0,0.04)] overflow-hidden ring-1 ring-border/30" role="region" aria-label="{resource.label} {t('common.list')}">
624
+ <div class="overflow-hidden rounded-3xl border border-border/70 bg-card/95 shadow-sm shadow-slate-900/[0.03] backdrop-blur" data-svadmin-table-card role="region" aria-label="{resource.label} {t('common.list')}">
620
625
  {#if query.isLoading}
621
626
  <div class="p-4 space-y-3">
622
627
  <div class="flex gap-4 mb-2">
@@ -640,8 +645,8 @@
640
645
  <div in:fade={{ duration: 150 }}>
641
646
  <!-- Desktop Table (hidden on mobile) -->
642
647
  <div class="hidden md:block">
643
- <Table.Root>
644
- <Table.Header>
648
+ <Table.Root class="svadmin-data-table" data-svadmin-datatable>
649
+ <Table.Header data-svadmin-table-head>
645
650
  {#each table_getHeaderGroups(tbl) as headerGroup, _i (_i)}
646
651
  <DraggableHeader
647
652
  columns={headerGroup.headers.map((h: any) => ({ id: h.column.id, header: h }))}
@@ -652,7 +657,7 @@
652
657
  {@const header = col.header as typeof headerGroup.headers[0]}
653
658
  <Table.Head
654
659
  {...dragProps}
655
- class={cn("bg-transparent hover:bg-muted/10 border-b border-border/20 uppercase tracking-[0.12em] text-[10px] text-muted-foreground/70 font-semibold py-4", dragProps.class)}
660
+ class={cn("border-b border-border/50 bg-muted/35 px-4 py-4 text-[10px] font-semibold uppercase tracking-[0.12em] text-muted-foreground/75 hover:bg-muted/45", dragProps.class)}
656
661
  style={header_getSize(header) != null && header_getSize(header) !== 150 ? `width:${header_getSize(header)}px` : undefined}
657
662
  >
658
663
  {#if header.id === '_select'}
@@ -694,9 +699,9 @@
694
699
  <ContextMenu.Root>
695
700
  <ContextMenu.Trigger>
696
701
  {#snippet child({ props })}
697
- <Table.Row {...props} class="transition-all duration-300 border-b border-border/10 {row_getIsSelected(row) ? 'bg-primary/5' : 'hover:bg-muted/20'}">
702
+ <Table.Row {...props} data-svadmin-table-row class="border-b border-border/30 transition-colors duration-200 {row_getIsSelected(row) ? 'bg-primary/5' : 'hover:bg-muted/25'}">
698
703
  {#each row_getVisibleCells(row) as cell, _i (_i)}
699
- <Table.Cell>
704
+ <Table.Cell class="px-4 py-3.5">
700
705
  {#if cell.column.id === '_select'}
701
706
  <Checkbox
702
707
  checked={row_getIsSelected(row)}
@@ -803,7 +808,7 @@
803
808
  <path stroke-linecap="round" stroke-linejoin="round" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" />
804
809
  </svg>
805
810
  <p class="text-sm font-medium text-muted-foreground mb-1">{t('common.noData')}</p>
806
- <p class="text-xs text-muted-foreground/60 mb-4">{t('common.noDataHint') || 'Get started by creating your first record.'}</p>
811
+ <p class="text-xs text-muted-foreground/60 mb-4">{t('common.noDataHint')}</p>
807
812
  {#if canCreate}
808
813
  <Button variant="outline" size="sm" class="gap-2" onclick={() => navigate(`/${resourceName}/create`)}>
809
814
  <Plus class="h-3.5 w-3.5" />
@@ -825,7 +830,7 @@
825
830
  {@const record = row.original}
826
831
  {@const id = record[primaryKey] as string | number}
827
832
  <div
828
- class="rounded-[20px] shadow-[0_4px_20px_rgb(0,0,0,0.03)] ring-1 ring-border/20 bg-card p-5 transition-all {row_getIsSelected(row) ? 'ring-2 ring-primary/50 bg-primary/5' : ''}"
833
+ class="rounded-3xl border border-border/60 bg-card p-5 shadow-sm shadow-slate-900/[0.03] transition-all {row_getIsSelected(row) ? 'border-primary/35 bg-primary/5 ring-2 ring-primary/20' : ''}"
829
834
  >
830
835
  <!-- Card header: ID + select + actions -->
831
836
  <div class="flex items-center justify-between mb-3">
@@ -906,11 +911,11 @@
906
911
 
907
912
  <!-- Pagination (shadcn) -->
908
913
  {#if totalPages > 0}
909
- <div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
914
+ <div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground" data-svadmin-pagination>
910
915
  <div class="flex items-center gap-2">
911
916
  <span>{t('common.total', { total: query.data?.total ?? 0 })}</span>
912
917
  <select
913
- class="h-8 w-[70px] px-1 py-1 flex items-center justify-between rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
918
+ class="h-8 w-[70px] px-2 rounded-md border border-input bg-background text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
914
919
  value={String(pagination.pageSize ?? 10)}
915
920
  onchange={(e) => {
916
921
  const size = Number((e.target as HTMLSelectElement).value);
@@ -925,7 +930,7 @@
925
930
  <option value="100">100</option>
926
931
  </select>
927
932
  </div>
928
- <PaginationUI.Root>
933
+ <PaginationUI.Root class="sm:w-auto sm:justify-end sm:mx-0">
929
934
  <PaginationUI.Content>
930
935
  <PaginationUI.Item>
931
936
  <PaginationUI.Previous
@@ -0,0 +1,58 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { navigate } from '@svadmin/core/router';
4
+ import { AlertTriangle, ArrowLeft, FileQuestion, Home } from '@lucide/svelte';
5
+ import { Button } from './ui/button/index.js';
6
+ import * as Card from './ui/card/index.js';
7
+
8
+ interface Props {
9
+ status?: '404' | '500';
10
+ title?: string;
11
+ description?: string;
12
+ }
13
+
14
+ let { status = '404', title, description }: Props = $props();
15
+
16
+ function goBack() {
17
+ if (typeof window !== 'undefined') {
18
+ window.history.back();
19
+ }
20
+ }
21
+
22
+ const isNotFound = $derived(status === '404');
23
+ const resolvedTitle = $derived(title ?? (isNotFound ? t('common.pageNotFound') : t('common.error')));
24
+ const resolvedDescription = $derived(
25
+ description ?? (isNotFound ? t('error.pageNotFoundDescription') : t('error.internalServerErrorDescription')),
26
+ );
27
+ </script>
28
+
29
+ <div class="flex min-h-[70vh] items-center justify-center p-4" data-svadmin-content-page="error">
30
+ <Card.Card class="w-full max-w-md border-border/60">
31
+ <Card.CardContent class="space-y-6 px-6 py-10 text-center">
32
+ <div class="mx-auto inline-flex h-16 w-16 items-center justify-center rounded-lg bg-muted/50 text-muted-foreground">
33
+ {#if isNotFound}
34
+ <FileQuestion class="h-8 w-8" />
35
+ {:else}
36
+ <AlertTriangle class="h-8 w-8 text-destructive" />
37
+ {/if}
38
+ </div>
39
+
40
+ <div class="space-y-2">
41
+ <p class="text-xs font-semibold uppercase text-muted-foreground">{status}</p>
42
+ <h1 class="text-2xl font-semibold text-foreground">{resolvedTitle}</h1>
43
+ <p class="mx-auto max-w-sm text-sm leading-relaxed text-muted-foreground">{resolvedDescription}</p>
44
+ </div>
45
+
46
+ <div class="flex flex-col justify-center gap-2 sm:flex-row">
47
+ <Button variant="outline" onclick={goBack}>
48
+ <ArrowLeft class="h-4 w-4" />
49
+ {t('common.back')}
50
+ </Button>
51
+ <Button onclick={() => navigate('/')}>
52
+ <Home class="h-4 w-4" />
53
+ {t('common.returnHome')}
54
+ </Button>
55
+ </div>
56
+ </Card.CardContent>
57
+ </Card.Card>
58
+ </div>
@@ -0,0 +1,8 @@
1
+ interface Props {
2
+ status?: '404' | '500';
3
+ title?: string;
4
+ description?: string;
5
+ }
6
+ declare const ErrorPage: import("svelte").Component<Props, {}, "">;
7
+ type ErrorPage = ReturnType<typeof ErrorPage>;
8
+ export default ErrorPage;
@@ -85,8 +85,8 @@
85
85
  }
86
86
  </script>
87
87
 
88
- <div class="space-y-1.5">
89
- <Label for={field.key} id="label-{field.key}">
88
+ <div class="space-y-1.5" data-svadmin-field>
89
+ <Label for={field.key} id="label-{field.key}" data-svadmin-field-label>
90
90
  {field.label}
91
91
  {#if field.required}
92
92
  <span class="text-destructive">*</span>
@@ -5,9 +5,8 @@
5
5
  import { Button } from './ui/button/index.js';
6
6
  import { Input } from './ui/input/index.js';
7
7
  import { Label } from './ui/label/index.js';
8
- import * as Card from './ui/card/index.js';
9
8
  import * as Alert from './ui/alert/index.js';
10
- import { KeyRound, User, ArrowLeft, CheckCircle, Loader2, AlertCircle } from '@lucide/svelte';
9
+ import { User, ArrowLeft, CheckCircle, Loader2, AlertCircle, Shield } from '@lucide/svelte';
11
10
 
12
11
  let { title = 'Admin' } = $props<{
13
12
  title?: string;
@@ -34,79 +33,123 @@
34
33
  }
35
34
  </script>
36
35
 
37
- <div class="min-h-screen flex items-center justify-center bg-background p-4">
38
- <div class="w-full max-w-[380px]">
39
- <Card.Card>
40
- <Card.CardHeader class="text-center pb-2">
41
- <div class="inline-flex items-center justify-center w-10 h-10 rounded-lg bg-primary/10 text-primary mx-auto mb-3">
36
+ <div class="min-h-screen flex flex-col lg:flex-row">
37
+ <!-- Left brand panel -->
38
+ <div class="hidden lg:flex lg:w-[480px] xl:w-[520px] relative overflow-hidden bg-gradient-to-br from-primary/90 via-primary to-primary/80 shrink-0">
39
+ <div class="absolute inset-0">
40
+ <div class="absolute top-[12%] left-[8%] w-28 h-28 rounded-3xl border border-white/10 bg-white/5 rotate-12 backdrop-blur-sm"></div>
41
+ <div class="absolute top-[55%] left-[55%] w-40 h-40 rounded-full border border-white/10 bg-white/5 -rotate-6 backdrop-blur-sm"></div>
42
+ <div class="absolute bottom-[15%] left-[15%] w-24 h-24 rounded-2xl border border-white/10 bg-white/5 -rotate-12 backdrop-blur-sm"></div>
43
+ <div class="absolute top-[30%] right-[10%] w-16 h-16 rounded-xl border border-white/10 bg-white/5 rotate-45 backdrop-blur-sm"></div>
44
+ <div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(circle, white 1px, transparent 1px); background-size: 24px 24px;"></div>
45
+ </div>
46
+
47
+ <div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
48
+ <div class="flex items-center gap-3">
49
+ <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-white/15 backdrop-blur-sm">
50
+ <Shield class="h-5 w-5 text-white" />
51
+ </div>
52
+ <span class="text-lg font-semibold text-white tracking-tight">{title}</span>
53
+ </div>
54
+
55
+ <div class="space-y-4">
56
+ <h2 class="text-3xl xl:text-4xl font-bold text-white leading-tight">
57
+ {t('auth.forgotPassword')}
58
+ </h2>
59
+ <p class="text-base text-white/70 leading-relaxed max-w-[320px]">
60
+ {t('auth.forgotPasswordDescription')}
61
+ </p>
62
+ </div>
63
+
64
+ <div class="flex items-center gap-3">
65
+ <div class="h-1 w-8 rounded-full bg-white/30"></div>
66
+ <div class="h-1 w-4 rounded-full bg-white/15"></div>
67
+ <div class="h-1 w-2 rounded-full bg-white/10"></div>
68
+ </div>
69
+ </div>
70
+ </div>
71
+
72
+ <!-- Right form panel -->
73
+ <div class="flex-1 flex items-center justify-center bg-background p-6 sm:p-8 lg:p-12">
74
+ <div class="w-full max-w-[400px]">
75
+ <div class="flex items-center gap-3 mb-8 lg:hidden">
76
+ <div class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 text-primary">
77
+ <Shield class="h-5 w-5" />
78
+ </div>
79
+ <span class="text-lg font-semibold tracking-tight">{title}</span>
80
+ </div>
81
+
82
+ <div class="space-y-1 mb-8">
83
+ <h1 class="text-2xl font-semibold tracking-tight">
42
84
  {#if sent}
43
- <CheckCircle class="h-5 w-5" />
85
+ {t('auth.resetLinkSent')}
44
86
  {:else}
45
- <KeyRound class="h-5 w-5" />
87
+ {t('auth.forgotPassword')}
46
88
  {/if}
47
- </div>
48
- <Card.CardTitle class="text-xl">
49
- {sent ? t('auth.resetLinkSent') : t('auth.forgotPassword')}
50
- </Card.CardTitle>
51
- {#if !sent}
52
- <p class="text-sm text-muted-foreground">{t('auth.forgotPasswordDescription')}</p>
53
- {/if}
54
- </Card.CardHeader>
55
- <Card.CardContent>
56
- {#if sent}
57
- <div class="py-2">
58
- <p class="text-sm text-muted-foreground text-center mb-4">
59
- {t('auth.resetLinkSent')}
60
- </p>
61
- <Button variant="outline" class="w-full" onclick={() => navigate('/login')}>
62
- <ArrowLeft class="h-4 w-4 mr-2" />
63
- {t('auth.backToLogin')}
64
- </Button>
89
+ </h1>
90
+ <p class="text-sm text-muted-foreground">
91
+ {#if sent}
92
+ {t('auth.resetLinkSent')}
93
+ {:else}
94
+ {t('auth.forgotPasswordDescription')}
95
+ {/if}
96
+ </p>
97
+ </div>
98
+
99
+ {#if sent}
100
+ <div class="space-y-4">
101
+ <div class="flex items-center gap-3 rounded-lg border bg-muted/50 p-4">
102
+ <CheckCircle class="h-5 w-5 text-primary shrink-0" />
103
+ <p class="text-sm text-muted-foreground">{t('auth.resetLinkSent')}</p>
65
104
  </div>
66
- {:else}
67
- <form onsubmit={handleSubmit} class="space-y-4">
68
- {#if error}
69
- <Alert.Root variant="destructive">
70
- <AlertCircle class="h-4 w-4" />
71
- <Alert.Description>{error}</Alert.Description>
72
- </Alert.Root>
73
- {/if}
105
+ <Button variant="outline" class="w-full" onclick={() => navigate('/login')}>
106
+ <ArrowLeft class="h-4 w-4 mr-2" />
107
+ {t('auth.backToLogin')}
108
+ </Button>
109
+ </div>
110
+ {:else}
111
+ <form onsubmit={handleSubmit} class="space-y-5">
112
+ {#if error}
113
+ <Alert.Root variant="destructive">
114
+ <AlertCircle class="h-4 w-4" />
115
+ <Alert.Description>{error}</Alert.Description>
116
+ </Alert.Root>
117
+ {/if}
74
118
 
75
- <div class="space-y-2">
76
- <Label for="forgot-identifier">{t('auth.usernameOrEmail')}</Label>
77
- <div class="relative">
78
- <User class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
79
- <Input
80
- id="forgot-identifier"
81
- type="text"
82
- placeholder={t('auth.identifierPlaceholder')}
83
- bind:value={identifier}
84
- class="pl-9"
85
- autocomplete="username"
86
- />
87
- </div>
119
+ <div class="space-y-2">
120
+ <Label for="forgot-identifier">{t('auth.usernameOrEmail')}</Label>
121
+ <div class="relative">
122
+ <User class="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground pointer-events-none z-[1]" />
123
+ <Input
124
+ id="forgot-identifier"
125
+ type="text"
126
+ placeholder={t('auth.identifierPlaceholder')}
127
+ bind:value={identifier}
128
+ class="pl-9"
129
+ autocomplete="username"
130
+ />
88
131
  </div>
89
-
90
- <Button type="submit" class="w-full" disabled={forgot.isLoading}>
91
- {#if forgot.isLoading}
92
- <Loader2 class="h-4 w-4 animate-spin mr-2" />
93
- {/if}
94
- {t('auth.sendResetLink')}
95
- </Button>
96
- </form>
97
-
98
- <div class="flex items-center justify-center mt-5 pt-5 border-t">
99
- <Button variant="link" class="text-sm h-auto p-0 font-medium inline-flex items-center gap-1" onclick={() => navigate('/login')}>
100
- <ArrowLeft class="h-3 w-3" />
101
- {t('auth.backToLogin')}
102
- </Button>
103
132
  </div>
104
- {/if}
105
- </Card.CardContent>
106
- </Card.Card>
107
133
 
108
- <p class="text-xs text-muted-foreground mt-4 text-center opacity-60">
109
- Powered by {title}
110
- </p>
134
+ <Button type="submit" class="w-full h-10" disabled={forgot.isLoading}>
135
+ {#if forgot.isLoading}
136
+ <Loader2 class="h-4 w-4 animate-spin mr-2" />
137
+ {/if}
138
+ {t('auth.sendResetLink')}
139
+ </Button>
140
+ </form>
141
+
142
+ <div class="flex items-center justify-center mt-8 pt-6 border-t">
143
+ <Button variant="link" class="text-sm h-auto p-0 font-medium inline-flex items-center gap-1" onclick={() => navigate('/login')}>
144
+ <ArrowLeft class="h-3 w-3" />
145
+ {t('auth.backToLogin')}
146
+ </Button>
147
+ </div>
148
+ {/if}
149
+
150
+ <p class="text-xs text-muted-foreground/60 mt-8 text-center">
151
+ Powered by {title}
152
+ </p>
153
+ </div>
111
154
  </div>
112
155
  </div>
@@ -35,7 +35,7 @@
35
35
  const CustomNotificationPanel = registry.NotificationPanel;
36
36
  </script>
37
37
 
38
- <header class="sticky top-0 z-30 flex h-14 w-full shrink-0 items-center justify-between bg-card/60 backdrop-blur-md px-4 md:px-6">
38
+ <header class="sticky top-0 z-30 flex h-16 w-full shrink-0 items-center justify-between border-b border-border/70 bg-card/85 px-4 shadow-sm shadow-slate-900/[0.02] backdrop-blur-xl md:px-6">
39
39
  <div class="flex items-center gap-4">
40
40
  {#if children}
41
41
  {@render children()}
@@ -54,7 +54,7 @@
54
54
  {/if}
55
55
 
56
56
  {#if siteUrl}
57
- <TooltipButton tooltip={t('common.goToSite') || 'Site'} href={siteUrl} target="_blank" rel="noopener noreferrer" class="rounded-full flex items-center justify-center">
57
+ <TooltipButton tooltip={t('common.goToSite')} href={siteUrl} target="_blank" rel="noopener noreferrer" class="flex items-center justify-center rounded-xl border border-border/60 bg-background/80">
58
58
  <MonitorUp class="h-4 w-4 text-muted-foreground transition-colors hover:text-foreground" />
59
59
  </TooltipButton>
60
60
  {/if}
@@ -64,10 +64,10 @@
64
64
  {/if}
65
65
 
66
66
  {#if showSearch && onSearchClick}
67
- <Button variant="outline" size="sm" onclick={onSearchClick} class="gap-2 text-muted-foreground h-8 px-3">
67
+ <Button variant="outline" size="sm" onclick={onSearchClick} class="h-10 gap-2 rounded-xl border-border/70 bg-background/85 px-3 text-muted-foreground shadow-sm shadow-slate-900/[0.02] hover:bg-muted/45 sm:min-w-56 sm:justify-start">
68
68
  <Search class="h-3.5 w-3.5" />
69
- <span class="hidden sm:inline text-xs">Search...</span>
70
- <kbd class="hidden sm:inline-flex items-center gap-0.5 rounded border bg-muted px-1.5 py-0.5 text-[10px] font-medium font-mono text-muted-foreground">
69
+ <span class="hidden text-xs sm:inline">{t('common.search')}</span>
70
+ <kbd class="ml-auto hidden items-center gap-0.5 rounded-md border bg-muted/60 px-1.5 py-0.5 font-mono text-[10px] font-medium text-muted-foreground sm:inline-flex">
71
71
  <span class="text-xs">⌘</span>K
72
72
  </kbd>
73
73
  </Button>
@@ -77,7 +77,7 @@
77
77
  {#if CustomThemeToggle}
78
78
  <CustomThemeToggle />
79
79
  {:else}
80
- <TooltipButton tooltip={t('common.toggleTheme')} onclick={() => toggleTheme()} class="rounded-full">
80
+ <TooltipButton tooltip={t('common.toggleTheme')} onclick={() => toggleTheme()} class="rounded-xl border border-border/60 bg-background/80">
81
81
  {#if getResolvedTheme() === 'dark'}
82
82
  <Moon class="h-4 w-4 transition-all" />
83
83
  {:else}
@@ -92,4 +92,3 @@
92
92
  {/if}
93
93
  </div>
94
94
  </header>
95
-
@@ -0,0 +1,70 @@
1
+ <script lang="ts">
2
+ import { t } from '@svadmin/core/i18n';
3
+ import { AlertCircle, Cloud, FolderGit, MessageCircle, Plug } from '@lucide/svelte';
4
+ import type { Component } from 'svelte';
5
+ import * as Card from './ui/card/index.js';
6
+ import { Switch } from './ui/switch/index.js';
7
+
8
+ interface Integration {
9
+ id: string;
10
+ name: string;
11
+ description: string;
12
+ connected: boolean;
13
+ Icon: Component;
14
+ tone: string;
15
+ }
16
+
17
+ let integrations = $state<Integration[]>([
18
+ { id: 'source-control', name: 'Source Control', description: 'Sync repository metadata, deployments, and commit activity.', connected: true, Icon: FolderGit, tone: 'bg-zinc-900 text-white' },
19
+ { id: 'chatops', name: 'ChatOps', description: 'Send operational alerts and audit summaries to workspace channels.', connected: false, Icon: MessageCircle, tone: 'bg-violet-700 text-white' },
20
+ { id: 'identity-cloud', name: 'Identity Cloud', description: 'Configure SSO and workspace identity handoff.', connected: true, Icon: Cloud, tone: 'bg-blue-600 text-white' },
21
+ { id: 'webhooks', name: 'Webhooks', description: 'Register outbound webhooks for internal tools and monitors.', connected: false, Icon: Plug, tone: 'bg-emerald-700 text-white' },
22
+ ]);
23
+
24
+ function toggleConnection(id: string, connected: boolean) {
25
+ integrations = integrations.map((item) => (item.id === id ? { ...item, connected } : item));
26
+ }
27
+ </script>
28
+
29
+ <div class="space-y-6">
30
+ <div>
31
+ <h2 class="text-xl font-semibold text-foreground">{t('settings.integrations')}</h2>
32
+ <p class="mt-1 text-sm text-muted-foreground">{t('settings.integrationsDescription')}</p>
33
+ </div>
34
+
35
+ <div class="grid gap-4">
36
+ {#each integrations as integration (integration.id)}
37
+ <Card.Card>
38
+ <Card.CardContent class="p-5">
39
+ <div class="flex flex-col justify-between gap-4 sm:flex-row sm:items-center">
40
+ <div class="flex items-start gap-4">
41
+ <div class="shrink-0 rounded-lg p-3 {integration.tone}">
42
+ <integration.Icon class="h-5 w-5" />
43
+ </div>
44
+ <div class="space-y-1">
45
+ <div class="flex flex-wrap items-center gap-2">
46
+ <span class="font-semibold text-foreground">{integration.name}</span>
47
+ <span class="rounded-full px-2 py-0.5 text-[10px] font-semibold {integration.connected ? 'bg-green-500/10 text-green-600' : 'bg-muted text-muted-foreground'}">
48
+ {integration.connected ? t('integrations.statusConnected') : t('integrations.statusDisconnected')}
49
+ </span>
50
+ </div>
51
+ <p class="text-sm text-muted-foreground">{integration.description}</p>
52
+ </div>
53
+ </div>
54
+ <div class="flex items-center gap-3">
55
+ <span class="text-xs text-muted-foreground">{integration.connected ? t('integrations.enabled') : t('integrations.disabled')}</span>
56
+ <Switch checked={integration.connected} onCheckedChange={(checked) => toggleConnection(integration.id, checked)} />
57
+ </div>
58
+ </div>
59
+ </Card.CardContent>
60
+ </Card.Card>
61
+ {/each}
62
+ </div>
63
+
64
+ <Card.Card class="border-dashed bg-muted/20">
65
+ <Card.CardContent class="flex items-center gap-3 p-5 text-sm text-muted-foreground">
66
+ <AlertCircle class="h-4 w-4 shrink-0" />
67
+ <span>{t('integrations.addMoreHint')}</span>
68
+ </Card.CardContent>
69
+ </Card.Card>
70
+ </div>
@@ -0,0 +1,4 @@
1
+ import type { Component } from 'svelte';
2
+ declare const IntegrationsSettings: Component<Record<string, never>, {}, "">;
3
+ type IntegrationsSettings = ReturnType<typeof IntegrationsSettings>;
4
+ export default IntegrationsSettings;
@@ -104,8 +104,8 @@
104
104
  ontouchend={handleTouchEnd}
105
105
  />
106
106
  {#if loading}
107
- <div class="flex h-screen" in:fade={{ duration: 150 }}>
108
- <div class="hidden md:block w-64 bg-sidebar/80 p-4 space-y-4">
107
+ <div class="flex h-screen bg-muted/30" in:fade={{ duration: 150 }}>
108
+ <div class="hidden w-64 space-y-4 border-r border-border/70 bg-card p-4 md:block">
109
109
  <Skeleton class="h-8 w-32" />
110
110
  <div class="space-y-2 mt-6">
111
111
  {#each Array(5) as _, _i (_i)}
@@ -113,7 +113,7 @@
113
113
  {/each}
114
114
  </div>
115
115
  </div>
116
- <div class="flex-1 p-8 space-y-6">
116
+ <div class="flex-1 space-y-6 p-8">
117
117
  <Skeleton class="h-8 w-48" />
118
118
  <div class="space-y-3">
119
119
  {#each Array(4) as _, _i (_i)}
@@ -123,7 +123,7 @@
123
123
  </div>
124
124
  </div>
125
125
  {:else}
126
- <div class="flex h-screen bg-background" in:fade={{ duration: 200, delay: 50 }}>
126
+ <div class="flex h-screen bg-[radial-gradient(circle_at_top_left,hsl(var(--primary)/0.08),transparent_32rem),linear-gradient(180deg,hsl(var(--muted)/0.48),hsl(var(--background))_42rem)]" in:fade={{ duration: 200, delay: 50 }}>
127
127
  <!-- Desktop sidebar -->
128
128
  <div class="hidden md:block">
129
129
  <Sidebar {collapsed} {identity} {title} {menu} {routeMode} onToggle={() => collapsed = !collapsed} onLogout={handleLogout} />
@@ -138,7 +138,7 @@
138
138
 
139
139
  <div
140
140
  class="flex-1 flex flex-col overflow-hidden transition-all duration-300"
141
- class:md:ml-64={!collapsed}
141
+ class:md:ml-72={!collapsed}
142
142
  class:md:ml-16={collapsed}
143
143
  >
144
144
  <!-- Header with mobile hamburger -->
@@ -163,9 +163,9 @@
163
163
  </Header>
164
164
 
165
165
  <!-- Content area -->
166
- <main class="flex-1 overflow-y-auto p-6 md:p-8">
166
+ <main class="flex-1 overflow-y-auto px-4 py-5 sm:px-6 md:px-8 md:py-7">
167
167
  {#key getPath()}
168
- <div in:fly={{ x: 20, duration: 150 }} out:fade={{ duration: 80 }}>
168
+ <div class="mx-auto w-full max-w-[1680px]" in:fly={{ x: 20, duration: 150 }} out:fade={{ duration: 80 }}>
169
169
  {@render children()}
170
170
  </div>
171
171
  {/key}