@svadmin/ui 0.45.0 → 0.47.1

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/components/AboutSettings.svelte +21 -96
  2. package/dist/components/ApiSettings.svelte +54 -234
  3. package/dist/components/AppearanceSettings.svelte +36 -165
  4. package/dist/components/AuthPageShell.svelte +32 -0
  5. package/dist/components/AuthPageShell.svelte.d.ts +11 -0
  6. package/dist/components/AutoForm.svelte +21 -3
  7. package/dist/components/DetailDrawer.svelte +74 -0
  8. package/dist/components/DetailDrawer.svelte.d.ts +16 -0
  9. package/dist/components/FieldRenderer.svelte +70 -2
  10. package/dist/components/FieldRenderer.svelte.d.ts +2 -0
  11. package/dist/components/ForgotPasswordPage.svelte +4 -68
  12. package/dist/components/IntegrationsSettings.svelte +22 -61
  13. package/dist/components/LoginPage.svelte +33 -124
  14. package/dist/components/NotificationsSettings.svelte +22 -103
  15. package/dist/components/PermissionMatrix.svelte +18 -9
  16. package/dist/components/RegisterPage.svelte +7 -62
  17. package/dist/components/RowActions.svelte +154 -0
  18. package/dist/components/RowActions.svelte.d.ts +26 -0
  19. package/dist/components/SecuritySettings.svelte +18 -58
  20. package/dist/components/SettingsPage.svelte +12 -2
  21. package/dist/components/SvadminLogo.svelte +1 -1
  22. package/dist/components/UpdatePasswordPage.svelte +3 -55
  23. package/dist/components/account/CompanyProfilePage.svelte +19 -7
  24. package/dist/components/account/SettingsEnterprisePage.svelte +25 -4
  25. package/dist/components/account/SettingsPlainPage.svelte +25 -3
  26. package/dist/components/account/SettingsSidebarPage.svelte +31 -2
  27. package/dist/components/account/UserProfilePage.svelte +5 -4
  28. package/dist/components/content/ApiKeyList.svelte +40 -6
  29. package/dist/components/content/ApiKeyList.svelte.d.ts +10 -0
  30. package/dist/components/content/DataState.svelte +25 -9
  31. package/dist/components/content/DataState.svelte.d.ts +2 -0
  32. package/dist/components/content/FileList.svelte +12 -4
  33. package/dist/components/content/FileList.svelte.d.ts +9 -0
  34. package/dist/components/content/FilterToolbar.svelte +10 -2
  35. package/dist/components/content/FilterToolbar.svelte.d.ts +1 -0
  36. package/dist/components/content/MemberList.svelte +13 -5
  37. package/dist/components/content/MemberList.svelte.d.ts +9 -0
  38. package/dist/components/content/MetricBlock.svelte +19 -2
  39. package/dist/components/content/MetricBlock.svelte.d.ts +2 -0
  40. package/dist/components/content/NetworkTable.svelte +9 -3
  41. package/dist/components/content/NetworkTable.svelte.d.ts +9 -0
  42. package/dist/components/content/NetworkUserCard.svelte +47 -7
  43. package/dist/components/content/NetworkUserCard.svelte.d.ts +13 -0
  44. package/dist/components/content/SectionHeader.svelte +3 -2
  45. package/dist/components/content/SectionHeader.svelte.d.ts +1 -0
  46. package/dist/components/content/SecurityEventTable.svelte +14 -5
  47. package/dist/components/content/SecurityEventTable.svelte.d.ts +9 -0
  48. package/dist/components/content/SettingsFieldRow.svelte +28 -0
  49. package/dist/components/content/SettingsFieldRow.svelte.d.ts +11 -0
  50. package/dist/components/content/SettingsGroup.svelte +33 -0
  51. package/dist/components/content/SettingsGroup.svelte.d.ts +12 -0
  52. package/dist/components/content/WorkspaceLayout.svelte +31 -0
  53. package/dist/components/content/WorkspaceLayout.svelte.d.ts +12 -0
  54. package/dist/components/content/index.d.ts +4 -1
  55. package/dist/components/content/index.js +3 -0
  56. package/dist/components/network/UserCardsNFTPage.svelte +25 -6
  57. package/dist/index.d.ts +9 -1
  58. package/dist/index.js +6 -0
  59. package/package.json +4 -1
@@ -0,0 +1,154 @@
1
+ <script lang="ts">
2
+ import { Ellipsis } from '@lucide/svelte';
3
+ import type { Component, Snippet } from 'svelte';
4
+ import type { HTMLAttributes } from 'svelte/elements';
5
+ import { cn, type WithElementRef } from '../utils.js';
6
+ import * as DropdownMenu from './ui/dropdown-menu/index.js';
7
+ import { Button } from './ui/button/index.js';
8
+
9
+ export type RowActionItem = {
10
+ label: string;
11
+ icon?: Component<{ class?: string }>;
12
+ href?: string;
13
+ onclick?: (e: MouseEvent) => void;
14
+ variant?: 'default' | 'outline' | 'ghost' | 'destructive' | 'secondary';
15
+ disabled?: boolean;
16
+ title?: string;
17
+ danger?: boolean;
18
+ hidden?: boolean;
19
+ };
20
+
21
+ type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
22
+ actions?: RowActionItem[];
23
+ maxVisible?: number;
24
+ moreLabel?: string;
25
+ children?: Snippet;
26
+ moreContent?: Snippet;
27
+ };
28
+
29
+ let {
30
+ actions = [],
31
+ maxVisible = 2,
32
+ moreLabel = 'More actions',
33
+ ref = $bindable(null),
34
+ class: className,
35
+ children,
36
+ moreContent,
37
+ ...restProps
38
+ }: Props = $props();
39
+
40
+ let overflowOpen = $state(false);
41
+ const availableActions = $derived(actions.filter((action) => !action.hidden));
42
+ const visibleLimit = $derived(
43
+ Number.isFinite(maxVisible)
44
+ ? Math.max(0, Math.floor(maxVisible))
45
+ : availableActions.length
46
+ );
47
+ const visibleActions = $derived(availableActions.slice(0, visibleLimit));
48
+ const overflowActions = $derived(availableActions.slice(visibleLimit));
49
+
50
+ function resolvedVariant(action: RowActionItem): RowActionItem['variant'] {
51
+ return action.variant ?? (action.danger ? 'destructive' : 'ghost');
52
+ }
53
+
54
+ function handleOverflowAction(action: RowActionItem, event: MouseEvent): void {
55
+ if (action.disabled) {
56
+ event.preventDefault();
57
+ return;
58
+ }
59
+ overflowOpen = false;
60
+ action.onclick?.(event);
61
+ }
62
+ </script>
63
+
64
+ <div
65
+ bind:this={ref}
66
+ data-slot="row-actions"
67
+ class={cn("inline-flex items-center gap-1.5", className)}
68
+ {...restProps}
69
+ >
70
+ {#if children}
71
+ {@render children()}
72
+ {/if}
73
+
74
+ {#each visibleActions as action (action)}
75
+ {#if action.href}
76
+ <Button
77
+ variant={resolvedVariant(action)}
78
+ size="sm"
79
+ href={action.href}
80
+ disabled={action.disabled}
81
+ title={action.title || action.label}
82
+ class="h-8 px-2 text-xs"
83
+ >
84
+ {#if action.icon}
85
+ {@const Icon = action.icon}
86
+ <Icon class="mr-1 size-3.5" />
87
+ {/if}
88
+ {action.label}
89
+ </Button>
90
+ {:else}
91
+ <Button
92
+ variant={resolvedVariant(action)}
93
+ size="sm"
94
+ onclick={action.onclick}
95
+ disabled={action.disabled}
96
+ title={action.title || action.label}
97
+ class="h-8 px-2 text-xs"
98
+ >
99
+ {#if action.icon}
100
+ {@const Icon = action.icon}
101
+ <Icon class="mr-1 size-3.5" />
102
+ {/if}
103
+ {action.label}
104
+ </Button>
105
+ {/if}
106
+ {/each}
107
+
108
+ {#if overflowActions.length > 0 || moreContent}
109
+ <DropdownMenu.Root bind:open={overflowOpen}>
110
+ <DropdownMenu.Trigger>
111
+ {#snippet child({ props })}
112
+ <Button {...props} variant="ghost" size="icon-sm" title={moreLabel} aria-label={moreLabel}>
113
+ <Ellipsis class="size-4" />
114
+ </Button>
115
+ {/snippet}
116
+ </DropdownMenu.Trigger>
117
+ <DropdownMenu.Content align="end" class="min-w-[8rem]">
118
+ {#each overflowActions as action (action)}
119
+ {#if action.href}
120
+ <Button
121
+ href={action.href}
122
+ variant={resolvedVariant(action)}
123
+ size="sm"
124
+ disabled={action.disabled}
125
+ onclick={(event) => handleOverflowAction(action, event)}
126
+ class="h-auto w-full justify-start rounded-md px-2 py-1.5 text-sm font-normal"
127
+ >
128
+ {#if action.icon}
129
+ {@const Icon = action.icon}
130
+ <Icon class="size-3.5" />
131
+ {/if}
132
+ {action.label}
133
+ </Button>
134
+ {:else}
135
+ <DropdownMenu.Item
136
+ onclick={(event) => handleOverflowAction(action, event)}
137
+ disabled={action.disabled}
138
+ destructive={action.danger || action.variant === 'destructive'}
139
+ >
140
+ {#if action.icon}
141
+ {@const Icon = action.icon}
142
+ <Icon class="size-3.5" />
143
+ {/if}
144
+ {action.label}
145
+ </DropdownMenu.Item>
146
+ {/if}
147
+ {/each}
148
+ {#if moreContent}
149
+ {@render moreContent()}
150
+ {/if}
151
+ </DropdownMenu.Content>
152
+ </DropdownMenu.Root>
153
+ {/if}
154
+ </div>
@@ -0,0 +1,26 @@
1
+ import type { Component, Snippet } from 'svelte';
2
+ import type { HTMLAttributes } from 'svelte/elements';
3
+ import { type WithElementRef } from '../utils.js';
4
+ export type RowActionItem = {
5
+ label: string;
6
+ icon?: Component<{
7
+ class?: string;
8
+ }>;
9
+ href?: string;
10
+ onclick?: (e: MouseEvent) => void;
11
+ variant?: 'default' | 'outline' | 'ghost' | 'destructive' | 'secondary';
12
+ disabled?: boolean;
13
+ title?: string;
14
+ danger?: boolean;
15
+ hidden?: boolean;
16
+ };
17
+ type Props = WithElementRef<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
18
+ actions?: RowActionItem[];
19
+ maxVisible?: number;
20
+ moreLabel?: string;
21
+ children?: Snippet;
22
+ moreContent?: Snippet;
23
+ };
24
+ declare const RowActions: Component<Props, {}, "ref">;
25
+ type RowActions = ReturnType<typeof RowActions>;
26
+ export default RowActions;
@@ -2,14 +2,15 @@
2
2
  import { useNotification } from '@svadmin/core';
3
3
  import { useTranslation } from '@svadmin/core/i18n';
4
4
 
5
- import { AlertTriangle, KeyRound, Monitor, Shield, ShieldAlert, ShieldCheck, Smartphone, Trash2 } from '@lucide/svelte';
5
+ import { AlertTriangle, Monitor, Smartphone, Trash2 } from '@lucide/svelte';
6
6
  import type { Component } from 'svelte';
7
7
  import * as Alert from './ui/alert/index.js';
8
- import * as Card from './ui/card/index.js';
9
8
  import { Button } from './ui/button/index.js';
10
- import { Label } from './ui/label/index.js';
11
9
  import { Switch } from './ui/switch/index.js';
12
10
  import PasswordInput from './PasswordInput.svelte';
11
+ import SettingsGroup from './content/SettingsGroup.svelte';
12
+ import SettingsFieldRow from './content/SettingsFieldRow.svelte';
13
+ import DataState from './content/DataState.svelte';
13
14
 
14
15
  const i18n = useTranslation();
15
16
  const notification = useNotification();
@@ -76,41 +77,14 @@
76
77
  <p class="mt-1 text-sm text-muted-foreground">{i18n.t('settings.securityDescription')}</p>
77
78
  </div>
78
79
 
79
- <Card.Card>
80
- <Card.CardHeader>
81
- <Card.CardTitle class="flex items-center gap-2 text-base">
82
- {#if is2faEnabled}
83
- <ShieldCheck class="h-5 w-5 text-green-500" />
84
- {:else}
85
- <ShieldAlert class="h-5 w-5 text-amber-500" />
86
- {/if}
87
- {i18n.t('security.twoFactorAuth')}
88
- </Card.CardTitle>
89
- <Card.CardDescription>{i18n.t('security.twoFactorDescription')}</Card.CardDescription>
90
- </Card.CardHeader>
91
- <Card.CardContent>
92
- <div class="flex items-center justify-between gap-4 border-t pt-4">
93
- <div class="space-y-1">
94
- <Label for="two-factor" class="text-sm font-medium">{i18n.t('security.enable2fa')}</Label>
95
- <p class="text-xs text-muted-foreground">
96
- {is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}
97
- </p>
98
- </div>
99
- <Switch id="two-factor" bind:checked={is2faEnabled} />
100
- </div>
101
- </Card.CardContent>
102
- </Card.Card>
103
-
104
- <Card.Card>
105
- <Card.CardHeader>
106
- <Card.CardTitle class="flex items-center gap-2 text-base">
107
- <KeyRound class="h-4 w-4 text-muted-foreground" />
108
- {i18n.t('profile.changePassword')}
109
- </Card.CardTitle>
110
- <Card.CardDescription>{i18n.t('security.passwordLastChanged', { time: lastChanged })}</Card.CardDescription>
111
- </Card.CardHeader>
112
- <Card.CardContent>
113
- <form onsubmit={handlePasswordChange} class="max-w-sm space-y-4">
80
+ <SettingsGroup title={i18n.t('security.twoFactorAuth')} description={i18n.t('security.twoFactorDescription')}>
81
+ <SettingsFieldRow label={i18n.t('security.enable2fa')} description={is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}>
82
+ {#snippet control()}<Switch id="two-factor" bind:checked={is2faEnabled} />{/snippet}
83
+ </SettingsFieldRow>
84
+ </SettingsGroup>
85
+
86
+ <SettingsGroup title={i18n.t('profile.changePassword')} description={i18n.t('security.passwordLastChanged', { time: lastChanged })}>
87
+ <form onsubmit={handlePasswordChange} class="max-w-lg space-y-4">
114
88
  {#if passwordErrorMessage}
115
89
  <Alert.Root variant="destructive">
116
90
  <AlertTriangle class="h-4 w-4" />
@@ -124,23 +98,10 @@
124
98
 
125
99
  <Button type="submit">{i18n.t('profile.updatePassword')}</Button>
126
100
  </form>
127
- </Card.CardContent>
128
- </Card.Card>
129
-
130
- <Card.Card>
131
- <Card.CardHeader class="flex flex-row items-start justify-between gap-4">
132
- <div class="space-y-1">
133
- <Card.CardTitle class="flex items-center gap-2 text-base">
134
- <Shield class="h-4 w-4 text-muted-foreground" />
135
- {i18n.t('security.activeSessions')}
136
- </Card.CardTitle>
137
- <Card.CardDescription>{i18n.t('security.sessionsDescription')}</Card.CardDescription>
138
- </div>
139
- {#if sessions.length > 1}
140
- <Button variant="outline" size="sm" onclick={revokeAllOthers}>{i18n.t('security.revokeOthers')}</Button>
141
- {/if}
142
- </Card.CardHeader>
143
- <Card.CardContent>
101
+ </SettingsGroup>
102
+
103
+ <SettingsGroup title={i18n.t('security.activeSessions')} description={i18n.t('security.sessionsDescription')}>
104
+ {#snippet actions()}{#if sessions.length > 1}<Button variant="outline" size="sm" onclick={revokeAllOthers}>{i18n.t('security.revokeOthers')}</Button>{/if}{/snippet}
144
105
  <div class="divide-y">
145
106
  {#each sessions as session (session.id)}
146
107
  <div class="flex items-center justify-between gap-4 py-3">
@@ -168,9 +129,8 @@
168
129
  {/if}
169
130
  </div>
170
131
  {:else}
171
- <div class="py-6 text-center text-sm text-muted-foreground">{i18n.t('security.noSessions')}</div>
132
+ <DataState state="empty" title={i18n.t('security.noSessions')} />
172
133
  {/each}
173
134
  </div>
174
- </Card.CardContent>
175
- </Card.Card>
135
+ </SettingsGroup>
176
136
  </div>
@@ -115,6 +115,16 @@
115
115
  return content ? alias : "profile";
116
116
  });
117
117
 
118
+ const contentWidth = $derived(
119
+ activeKey === 'roles' || activeKey === 'audit'
120
+ ? 'max-w-[92rem]'
121
+ : activeKey === 'api'
122
+ ? 'max-w-[78rem]'
123
+ : activeKey === 'integrations'
124
+ ? 'max-w-[64rem]'
125
+ : 'max-w-3xl',
126
+ );
127
+
118
128
  // Default redirect to profile
119
129
  $effect(() => {
120
130
  if (getRoute() === "/settings") {
@@ -151,7 +161,7 @@
151
161
  </div>
152
162
  {#each resolvedSections as section, _i (_i)}
153
163
  <div>
154
- <h3 class="px-3 mb-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
164
+ <h3 class="px-3 mb-1 text-xs font-medium text-muted-foreground">
155
165
  {i18n.t(section.group)}
156
166
  </h3>
157
167
  <div class="space-y-0.5">
@@ -175,7 +185,7 @@
175
185
  </nav>
176
186
 
177
187
  <!-- Right content area -->
178
- <div class="flex-1 p-6 lg:p-8 {activeKey === 'roles' || activeKey === 'audit' || activeKey === 'integrations' || activeKey === 'api' ? 'max-w-6xl' : 'max-w-3xl'}">
188
+ <div class="min-w-0 w-full flex-1 p-6 lg:p-8 {contentWidth}">
179
189
  {#if activeKey === "profile"}
180
190
  {#if profile}{@render profile()}{:else}<ProfilePage />{/if}
181
191
  {:else if activeKey === "appearance"}
@@ -10,7 +10,7 @@
10
10
 
11
11
  <span
12
12
  aria-hidden="true"
13
- class="inline-block shrink-0 rounded-lg bg-gradient-to-tr from-primary to-purple-500 shadow-sm {onDark ? 'ring-1 ring-white/35' : ''}"
13
+ class="inline-block shrink-0 rounded-lg bg-primary shadow-sm {onDark ? 'ring-1 ring-white/35' : ''}"
14
14
  style:width={`${size}px`}
15
15
  style:height={`${size}px`}
16
16
  ></span>
@@ -4,8 +4,8 @@
4
4
 
5
5
  import { Button } from './ui/button/index.js';
6
6
  import * as Alert from './ui/alert/index.js';
7
+ import AuthPageShell from './AuthPageShell.svelte';
7
8
  import PasswordInput from './PasswordInput.svelte';
8
- import SvadminLogo from './SvadminLogo.svelte';
9
9
  import { Loader2, AlertCircle } from '@lucide/svelte';
10
10
 
11
11
  const i18n = useTranslation();
@@ -35,53 +35,7 @@
35
35
  }
36
36
  </script>
37
37
 
38
- <div class="min-h-screen flex flex-col lg:flex-row">
39
- <!-- Left brand panel -->
40
- <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">
41
- <div class="absolute inset-0">
42
- <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>
43
- <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>
44
- <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>
45
- <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>
46
- <div class="absolute inset-0 opacity-[0.03]" style="background-image: radial-gradient(circle, white 1px, transparent 1px); background-size: 24px 24px;"></div>
47
- </div>
48
-
49
- <div class="relative z-10 flex flex-col justify-between p-10 xl:p-12 w-full">
50
- <div class="flex items-center gap-3">
51
- <SvadminLogo size={36} onDark />
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
- {i18n.t('auth.resetPassword')}
58
- </h2>
59
- <p class="text-base text-white/70 leading-relaxed max-w-[320px]">
60
- {i18n.t('auth.resetPasswordDescription')}
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="mb-8 flex items-center gap-3 lg:hidden">
76
- <SvadminLogo size={36} />
77
- <span class="text-lg font-semibold tracking-tight">{title}</span>
78
- </div>
79
-
80
- <div class="space-y-1 mb-8">
81
- <h1 class="text-2xl font-semibold tracking-tight">{i18n.t('auth.resetPassword')}</h1>
82
- <p class="text-sm text-muted-foreground">{i18n.t('auth.resetPasswordDescription')}</p>
83
- </div>
84
-
38
+ <AuthPageShell brand={title} title={i18n.t('auth.resetPassword')} description={i18n.t('auth.resetPasswordDescription')}>
85
39
  <form onsubmit={handleSubmit} class="space-y-5">
86
40
  {#if error}
87
41
  <Alert.Root variant="destructive">
@@ -118,10 +72,4 @@
118
72
  </Button>
119
73
  </div>
120
74
  </form>
121
-
122
- <p class="text-xs text-muted-foreground/60 mt-8 text-center">
123
- Powered by {title}
124
- </p>
125
- </div>
126
- </div>
127
- </div>
75
+ </AuthPageShell>
@@ -4,11 +4,14 @@
4
4
  import { Button } from '../ui/button/index.js';
5
5
  import { Input } from '../ui/input/index.js';
6
6
  import { Label } from '../ui/label/index.js';
7
- import * as Card from '../ui/card/index.js';
7
+ import { Textarea } from '../ui/textarea/index.js';
8
8
  import ContentPageShell from '../content/ContentPageShell.svelte';
9
9
  import ContentPageHeader from '../content/ContentPageHeader.svelte';
10
10
  import DescriptionList from '../content/DescriptionList.svelte';
11
11
  import MemberList from '../content/MemberList.svelte';
12
+ import SectionHeader from '../content/SectionHeader.svelte';
13
+ import SettingsGroup from '../content/SettingsGroup.svelte';
14
+ import WorkspaceLayout from '../content/WorkspaceLayout.svelte';
12
15
  import type { MemberSummary } from '../content/MemberList.svelte';
13
16
  const i18n = useTranslation();
14
17
  let companyName = $state('Acme Corporation');
@@ -25,11 +28,20 @@
25
28
  async function save() { saving = true; await new Promise((resolve) => setTimeout(resolve, 400)); saving = false; }
26
29
  </script>
27
30
 
31
+ {#snippet headerActions()}
32
+ <Button onclick={save} disabled={saving} size="sm">{#if saving}<Loader2 class="size-3.5 animate-spin" />{/if}{i18n.t('common.save')}</Button>
33
+ {/snippet}
34
+
35
+ {#snippet primary()}
36
+ <SettingsGroup title={i18n.t('account.basicSettings')} description={i18n.t('account.companyProfileDescription')} bodyClass="space-y-4"><div class="grid gap-4 sm:grid-cols-2"><div class="space-y-2"><Label for="company-name">{i18n.t('profile.companyName')}</Label><Input id="company-name" bind:value={companyName} /></div><div class="space-y-2"><Label for="company-industry">{i18n.t('profile.industry')}</Label><Input id="company-industry" bind:value={industry} /></div><div class="space-y-2 sm:col-span-2"><Label for="company-website">{i18n.t('profile.website')}</Label><Input id="company-website" bind:value={website} type="url" /></div></div><div class="space-y-2"><Label for="company-description">{i18n.t('profile.companyDescription')}</Label><Textarea id="company-description" bind:value={description} rows={4} /></div></SettingsGroup>
37
+ {/snippet}
38
+
39
+ {#snippet secondary()}
40
+ <SettingsGroup title={i18n.t('profile.companyName')} description={industry} bodyClass="space-y-4"><p class="text-lg font-semibold text-foreground">{companyName}</p><DescriptionList columns={1} items={[{ label: i18n.t('profile.website'), value: website, href: website }, { label: i18n.t('profile.employees'), value: '1,250' }, { label: i18n.t('profile.founded'), value: '2015' }]} /></SettingsGroup>
41
+ {/snippet}
42
+
28
43
  <ContentPageShell pageId="account-company-profile" width="wide">
29
- <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><ContentPageHeader title={i18n.t("account.companyProfile")} description={i18n.t("account.companyProfileDescription")} /><Button onclick={save} disabled={saving} size="sm">{#if saving}<Loader2 class="size-3.5 animate-spin" />{/if}{i18n.t("common.save")}</Button></div>
30
- <div class="grid gap-4 lg:grid-cols-[minmax(0,1fr)_20rem]">
31
- <Card.Card><Card.CardContent class="space-y-4 p-5"><div class="grid gap-4 sm:grid-cols-2"><div class="space-y-2"><Label for="company-name">{i18n.t('profile.companyName')}</Label><Input id="company-name" bind:value={companyName} /></div><div class="space-y-2"><Label for="company-industry">{i18n.t('profile.industry')}</Label><Input id="company-industry" bind:value={industry} /></div><div class="space-y-2 sm:col-span-2"><Label for="company-website">{i18n.t('profile.website')}</Label><Input id="company-website" bind:value={website} type="url" /></div></div><div class="space-y-2"><Label for="company-description">{i18n.t('profile.companyDescription')}</Label><textarea id="company-description" bind:value={description} rows={4} class="flex w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"></textarea></div></Card.CardContent></Card.Card>
32
- <Card.Card><Card.CardContent class="space-y-4 p-5"><div><p class="text-xs font-medium text-muted-foreground">{i18n.t('profile.companyName')}</p><p class="mt-1 text-lg font-semibold text-foreground">{companyName}</p><p class="text-sm text-muted-foreground">{industry}</p></div><DescriptionList columns={1} items={[{ label: i18n.t('profile.website'), value: website, href: website }, { label: i18n.t('profile.employees'), value: '1,250' }, { label: i18n.t('profile.founded'), value: '2015' }]} /></Card.CardContent></Card.Card>
33
- </div>
34
- <section class="space-y-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('profileSections.members')}</h2><MemberList {members} /></section>
44
+ <ContentPageHeader title={i18n.t('account.companyProfile')} description={i18n.t('account.companyProfileDescription')} actions={headerActions} />
45
+ <WorkspaceLayout {primary} {secondary} secondaryWidth="20rem" />
46
+ <section class="space-y-3"><SectionHeader title={i18n.t('profileSections.members')} /><MemberList {members} emptyTitle={i18n.t('common.noData')} emptyDescription={i18n.t('empty.description')} /></section>
35
47
  </ContentPageShell>
@@ -4,9 +4,11 @@
4
4
  import { Input } from '../ui/input/index.js';
5
5
  import { Label } from '../ui/label/index.js';
6
6
  import { Switch } from '../ui/switch/index.js';
7
- import * as Card from '../ui/card/index.js';
8
7
  import ContentPageShell from '../content/ContentPageShell.svelte';
9
8
  import ContentPageHeader from '../content/ContentPageHeader.svelte';
9
+ import SectionHeader from '../content/SectionHeader.svelte';
10
+ import SettingsGroup from '../content/SettingsGroup.svelte';
11
+ import SettingsFieldRow from '../content/SettingsFieldRow.svelte';
10
12
  import IntegrationCard from '../content/IntegrationCard.svelte';
11
13
  import type { IntegrationSummary } from '../content/IntegrationCard.svelte';
12
14
  import SecurityEventTable from '../content/SecurityEventTable.svelte';
@@ -18,9 +20,28 @@
18
20
  const events: SecurityEvent[] = [{ id: '1', event: 'SSO policy updated', actor: 'Alex Chen', location: 'Shanghai, CN', createdAt: 'Today, 09:12', severity: 'info' }, { id: '2', event: 'Audit log retention changed', actor: 'Alex Chen', location: 'Shanghai, CN', createdAt: 'Yesterday, 16:40', severity: 'warning' }];
19
21
  </script>
20
22
 
23
+ {#snippet auditControl()}
24
+ <Switch bind:checked={auditLogging} aria-label={i18n.t('account.auditLogging')} />
25
+ {/snippet}
26
+
27
+ {#snippet ssoControl()}
28
+ <Switch bind:checked={ssoEnabled} aria-label={i18n.t('account.enableSso')} />
29
+ {/snippet}
30
+
21
31
  <ContentPageShell pageId="account-settings-enterprise" width="wide">
22
32
  <ContentPageHeader title={i18n.t('account.settingsEnterprise')} description={i18n.t('account.settingsEnterpriseDescription')} />
23
- <div class="grid gap-4 lg:grid-cols-2"><Card.Card><Card.CardContent class="space-y-4 p-5"><div><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.securityPolicy')}</h2><p class="mt-1 text-sm text-muted-foreground">Set organization-wide controls for authentication and sessions.</p></div><div class="space-y-2"><Label for="session-timeout">{i18n.t('account.sessionTimeout')}</Label><Input id="session-timeout" value="30" /></div><div class="space-y-2"><Label for="retention">{i18n.t('account.dataRetention')}</Label><Input id="retention" value="365" /></div><div class="flex items-center justify-between border-t border-border pt-4"><span class="text-sm text-foreground">{i18n.t('account.auditLogging')}</span><Switch bind:checked={auditLogging} /></div><Button size="sm">{i18n.t('common.save')}</Button></Card.CardContent></Card.Card><Card.Card><Card.CardContent class="space-y-4 p-5"><div><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.singleSignOn')}</h2><p class="mt-1 text-sm text-muted-foreground">{i18n.t('account.ssoDescription')}</p></div><div class="flex items-center justify-between border-t border-border pt-4"><span class="text-sm text-foreground">{i18n.t('account.enableSso')}</span><Switch bind:checked={ssoEnabled} /></div><Input value="https://idp.example.com/metadata" aria-label={i18n.t('account.ssoProvider')} /><Button variant="outline" size="sm">{i18n.t('common.test')}</Button></Card.CardContent></Card.Card></div>
24
- <section class="space-y-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('account.connections')}</h2>{#each identity as integration (integration.id)}<IntegrationCard {integration} />{/each}</section>
25
- <section class="space-y-3"><h2 class="text-base font-semibold text-foreground">{i18n.t('account.securityLog')}</h2><SecurityEventTable events={events} /></section>
33
+ <div class="grid items-start gap-4 lg:grid-cols-2">
34
+ <SettingsGroup title={i18n.t('account.securityPolicy')} description="Set organization-wide controls for authentication and sessions." bodyClass="space-y-4">
35
+ <div class="grid gap-4 sm:grid-cols-2"><div class="space-y-2"><Label for="session-timeout">{i18n.t('account.sessionTimeout')}</Label><Input id="session-timeout" value="30" /></div><div class="space-y-2"><Label for="retention">{i18n.t('account.dataRetention')}</Label><Input id="retention" value="365" /></div></div>
36
+ <SettingsFieldRow label={i18n.t('account.auditLogging')} description="Preserve an auditable record of organization-level changes." control={auditControl} separated />
37
+ <Button size="sm">{i18n.t('common.save')}</Button>
38
+ </SettingsGroup>
39
+ <SettingsGroup title={i18n.t('account.singleSignOn')} description={i18n.t('account.ssoDescription')} bodyClass="space-y-4">
40
+ <SettingsFieldRow label={i18n.t('account.enableSso')} description="Require the configured identity provider for organization members." control={ssoControl} />
41
+ <Input value="https://idp.example.com/metadata" aria-label={i18n.t('account.ssoProvider')} />
42
+ <Button variant="outline" size="sm">{i18n.t('common.test')}</Button>
43
+ </SettingsGroup>
44
+ </div>
45
+ <section class="space-y-3"><SectionHeader title={i18n.t('account.connections')} />{#each identity as integration (integration.id)}<IntegrationCard {integration} />{/each}</section>
46
+ <section class="space-y-3"><SectionHeader title={i18n.t('account.securityLog')} /><SecurityEventTable events={events} emptyTitle={i18n.t('common.noData')} emptyDescription={i18n.t('empty.description')} /></section>
26
47
  </ContentPageShell>
@@ -4,9 +4,10 @@
4
4
  import { Input } from '../ui/input/index.js';
5
5
  import { Label } from '../ui/label/index.js';
6
6
  import { Switch } from '../ui/switch/index.js';
7
- import * as Card from '../ui/card/index.js';
8
7
  import ContentPageShell from '../content/ContentPageShell.svelte';
9
8
  import ContentPageHeader from '../content/ContentPageHeader.svelte';
9
+ import SettingsGroup from '../content/SettingsGroup.svelte';
10
+ import SettingsFieldRow from '../content/SettingsFieldRow.svelte';
10
11
  const i18n = useTranslation();
11
12
  let displayName = $state('Alex Chen');
12
13
  let email = $state('alex@example.com');
@@ -16,7 +17,28 @@
16
17
  async function save() { saving = true; await new Promise((resolve) => setTimeout(resolve, 400)); saving = false; }
17
18
  </script>
18
19
 
20
+ {#snippet headerActions()}
21
+ <Button size="sm" onclick={save} disabled={saving}>{i18n.t('common.save')}</Button>
22
+ {/snippet}
23
+
24
+ {#snippet notificationControl()}
25
+ <Switch bind:checked={notifications} aria-label={i18n.t('settings.notifications')} />
26
+ {/snippet}
27
+
28
+ {#snippet twoFactorControl()}
29
+ <Switch bind:checked={twoFactor} aria-label={i18n.t('account.twoFactorAuth')} />
30
+ {/snippet}
31
+
19
32
  <ContentPageShell pageId="account-settings-plain" width="narrow">
20
- <div class="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between"><ContentPageHeader title={i18n.t('account.settingsPlain')} description={i18n.t('account.settingsPlainDescription')} /><Button size="sm" onclick={save} disabled={saving}>{i18n.t('common.save')}</Button></div>
21
- <Card.Card><Card.CardContent class="space-y-5 p-5"><div class="space-y-2"><Label for="settings-name">{i18n.t('profile.name')}</Label><Input id="settings-name" bind:value={displayName} /></div><div class="space-y-2"><Label for="settings-email">Email</Label><Input id="settings-email" bind:value={email} type="email" /></div><div class="flex items-center justify-between gap-4 border-t border-border pt-4"><div><p class="text-sm font-medium text-foreground">{i18n.t('settings.notifications')}</p><p class="mt-1 text-sm text-muted-foreground">Receive account and workflow updates.</p></div><Switch bind:checked={notifications} /></div><div class="flex items-center justify-between gap-4 border-t border-border pt-4"><div><p class="text-sm font-medium text-foreground">{i18n.t('account.twoFactorAuth')}</p><p class="mt-1 text-sm text-muted-foreground">Require an additional verification code when signing in.</p></div><Switch bind:checked={twoFactor} /></div></Card.CardContent></Card.Card>
33
+ <ContentPageHeader title={i18n.t('account.settingsPlain')} description={i18n.t('account.settingsPlainDescription')} actions={headerActions} />
34
+ <SettingsGroup title={i18n.t('account.basicSettings')} description={i18n.t('account.settingsPlainDescription')} bodyClass="space-y-5">
35
+ <div class="grid gap-4 sm:grid-cols-2">
36
+ <div class="space-y-2"><Label for="settings-name">{i18n.t('profile.name')}</Label><Input id="settings-name" bind:value={displayName} /></div>
37
+ <div class="space-y-2"><Label for="settings-email">Email</Label><Input id="settings-email" bind:value={email} type="email" /></div>
38
+ </div>
39
+ <div>
40
+ <SettingsFieldRow label={i18n.t('settings.notifications')} description="Receive account and workflow updates." control={notificationControl} />
41
+ <SettingsFieldRow label={i18n.t('account.twoFactorAuth')} description="Require an additional verification code when signing in." control={twoFactorControl} separated />
42
+ </div>
43
+ </SettingsGroup>
22
44
  </ContentPageShell>
@@ -3,9 +3,10 @@
3
3
  import { Button } from '../ui/button/index.js';
4
4
  import { Input } from '../ui/input/index.js';
5
5
  import { Switch } from '../ui/switch/index.js';
6
- import * as Card from '../ui/card/index.js';
7
6
  import ContentPageShell from '../content/ContentPageShell.svelte';
8
7
  import ContentPageHeader from '../content/ContentPageHeader.svelte';
8
+ import SettingsGroup from '../content/SettingsGroup.svelte';
9
+ import SettingsFieldRow from '../content/SettingsFieldRow.svelte';
9
10
  const i18n = useTranslation();
10
11
  const sections = [{ id: 'profile', title: i18n.t('account.basicSettings') }, { id: 'signin', title: i18n.t('account.socialSignIn') }, { id: 'preferences', title: i18n.t('account.preferences') }, { id: 'api', title: i18n.t('account.manageApi') }];
11
12
  let active = $state('profile');
@@ -13,7 +14,35 @@
13
14
  let available = $state(true);
14
15
  </script>
15
16
 
17
+ {#snippet visibilityControl()}
18
+ <Switch bind:checked={visible} aria-label={i18n.t('account.visibility')} />
19
+ {/snippet}
20
+
21
+ {#snippet availabilityControl()}
22
+ <Switch bind:checked={available} aria-label={i18n.t('account.availability')} />
23
+ {/snippet}
24
+
16
25
  <ContentPageShell pageId="account-settings-sidebar" width="wide">
17
26
  <ContentPageHeader title={i18n.t('account.settingsSidebar')} description={i18n.t('account.settingsSidebarDescription')} />
18
- <div class="grid gap-6 lg:grid-cols-[12rem_minmax(0,1fr)]"><nav class="flex gap-1 overflow-x-auto lg:flex-col">{#each sections as section (section.id)}<button type="button" class={'whitespace-nowrap rounded-md px-3 py-2 text-left text-sm ' + (active === section.id ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground hover:text-foreground')} onclick={() => active = section.id}>{section.title}</button>{/each}</nav><div class="space-y-4">{#if active === 'profile'}<Card.Card><Card.CardContent class="space-y-4 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.basicSettings')}</h2><Input value="Alex Chen" aria-label={i18n.t('profile.name')} /><Input value="Nebula Labs" aria-label="Company" /><div class="flex items-center justify-between border-t border-border pt-4"><span class="text-sm text-foreground">{i18n.t('account.visibility')}</span><Switch bind:checked={visible} /></div><div class="flex items-center justify-between"><span class="text-sm text-foreground">{i18n.t('account.availability')}</span><Switch bind:checked={available} /></div></Card.CardContent></Card.Card>{:else if active === 'signin'}<Card.Card><Card.CardContent class="space-y-4 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.socialSignIn')}</h2><p class="text-sm text-muted-foreground">Connect the providers your team trusts for sign in.</p><Button variant="outline">GitHub</Button><Button variant="outline">Google</Button></Card.CardContent></Card.Card>{:else if active === 'preferences'}<Card.Card><Card.CardContent class="space-y-4 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.preferences')}</h2><Input value="English" aria-label={i18n.t('account.language')} /><Input value="Asia/Shanghai" aria-label={i18n.t('account.timezone')} /><Input value="CNY" aria-label={i18n.t('account.currency')} /></Card.CardContent></Card.Card>{:else}<Card.Card><Card.CardContent class="space-y-4 p-5"><h2 class="text-sm font-semibold text-foreground">{i18n.t('account.manageApi')}</h2><Input readonly value="sv_demo_pub_9f3e********41ab" aria-label="API key" /><Button variant="outline">{i18n.t('common.copy')}</Button></Card.CardContent></Card.Card>{/if}</div></div>
27
+ <div class="grid gap-6 lg:grid-cols-[12rem_minmax(0,1fr)]">
28
+ <nav class="flex gap-1 overflow-x-auto lg:flex-col" aria-label={i18n.t('account.settingsSidebar')}>
29
+ {#each sections as section (section.id)}
30
+ <button type="button" class={'whitespace-nowrap rounded-md px-3 py-2 text-left text-sm ' + (active === section.id ? 'bg-muted font-medium text-foreground' : 'text-muted-foreground hover:bg-muted/50 hover:text-foreground')} aria-current={active === section.id ? 'page' : undefined} onclick={() => active = section.id}>{section.title}</button>
31
+ {/each}
32
+ </nav>
33
+ <div class="min-w-0">
34
+ {#if active === 'profile'}
35
+ <SettingsGroup title={i18n.t('account.basicSettings')} description={i18n.t('account.settingsPlainDescription')} bodyClass="space-y-4">
36
+ <div class="grid gap-4 sm:grid-cols-2"><Input value="Alex Chen" aria-label={i18n.t('profile.name')} /><Input value="Nebula Labs" aria-label="Company" /></div>
37
+ <div><SettingsFieldRow label={i18n.t('account.visibility')} control={visibilityControl} separated /><SettingsFieldRow label={i18n.t('account.availability')} control={availabilityControl} separated /></div>
38
+ </SettingsGroup>
39
+ {:else if active === 'signin'}
40
+ <SettingsGroup title={i18n.t('account.socialSignIn')} description="Connect the providers your team trusts for sign in." bodyClass="flex flex-wrap gap-2"><Button variant="outline">GitHub</Button><Button variant="outline">Google</Button></SettingsGroup>
41
+ {:else if active === 'preferences'}
42
+ <SettingsGroup title={i18n.t('account.preferences')} bodyClass="grid gap-4 sm:grid-cols-3"><Input value="English" aria-label={i18n.t('account.language')} /><Input value="Asia/Shanghai" aria-label={i18n.t('account.timezone')} /><Input value="CNY" aria-label={i18n.t('account.currency')} /></SettingsGroup>
43
+ {:else}
44
+ <SettingsGroup title={i18n.t('account.manageApi')} description="Use scoped credentials and rotate them when access changes." bodyClass="flex flex-col gap-3 sm:flex-row"><Input readonly value="sv_demo_pub_9f3e********41ab" aria-label="API key" /><Button variant="outline">{i18n.t('common.copy')}</Button></SettingsGroup>
45
+ {/if}
46
+ </div>
47
+ </div>
19
48
  </ContentPageShell>