@svadmin/ui 0.35.0 → 0.37.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.
- package/dist/app.css +436 -66
- package/dist/components/AutoForm.svelte +33 -48
- package/dist/components/AutoTable.svelte +24 -29
- package/dist/components/ErrorPage.svelte +1 -1
- package/dist/components/Header.svelte +6 -6
- package/dist/components/Layout.svelte +8 -8
- package/dist/components/PageHeader.svelte +7 -9
- package/dist/components/ResourceOperationsPage.svelte +12 -14
- package/dist/components/SettingsPage.svelte +14 -14
- package/dist/components/ShowPage.svelte +9 -9
- package/dist/components/Sidebar.svelte +73 -115
- package/dist/components/SidebarItem.svelte +37 -32
- package/dist/components/TwoFactorAuthPage.svelte +1 -1
- package/dist/components/account/CompanyProfilePage.svelte +1 -1
- package/dist/components/account/GetStartedPage.svelte +1 -1
- package/dist/components/account/ImportMembersPage.svelte +1 -1
- package/dist/components/account/MembersStarterPage.svelte +1 -1
- package/dist/components/account/SecurityLogPage.svelte +29 -2
- package/dist/components/account/SettingsEnterprisePage.svelte +1 -1
- package/dist/components/account/SettingsPlainPage.svelte +1 -1
- package/dist/components/account/SettingsSidebarPage.svelte +1 -1
- package/dist/components/account/TeamMembersPage.svelte +1 -1
- package/dist/components/network/TeamCrewTablePage.svelte +35 -2
- package/dist/components/network/UserCardsNFTPage.svelte +1 -1
- package/dist/components/profile/PublicProfilePage.svelte +1 -1
- package/dist/components/ui/pagination/pagination-ellipsis.svelte +1 -1
- package/package.json +2 -2
|
@@ -44,17 +44,14 @@
|
|
|
44
44
|
const hasGroups = $derived(formFields.some(f => f.group));
|
|
45
45
|
const groups = $derived((() => {
|
|
46
46
|
if (!hasGroups) return [];
|
|
47
|
-
const
|
|
47
|
+
const order: string[] = [];
|
|
48
|
+
const map = new Map<string, FieldDefinition[]>();
|
|
48
49
|
for (const f of formFields) {
|
|
49
50
|
const g = f.group ?? '';
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
group = { name: g, fields: [] };
|
|
53
|
-
result.push(group);
|
|
54
|
-
}
|
|
55
|
-
group.fields.push(f);
|
|
51
|
+
if (!map.has(g)) { order.push(g); map.set(g, []); }
|
|
52
|
+
(map.get(g) ?? []).push(f);
|
|
56
53
|
}
|
|
57
|
-
return
|
|
54
|
+
return order.map(g => ({ name: g, fields: map.get(g) ?? [] }));
|
|
58
55
|
})());
|
|
59
56
|
|
|
60
57
|
// ─── Default values from field metadata ───────────────────────────
|
|
@@ -135,37 +132,26 @@
|
|
|
135
132
|
}
|
|
136
133
|
</script>
|
|
137
134
|
|
|
138
|
-
<div class="space-y-6"
|
|
139
|
-
<div class="
|
|
140
|
-
<
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</Badge>
|
|
154
|
-
<p class="text-sm text-muted-foreground">{resource.label}</p>
|
|
155
|
-
</div>
|
|
156
|
-
</div>
|
|
157
|
-
{#if headerContent}
|
|
158
|
-
{@render headerContent()}
|
|
159
|
-
{/if}
|
|
160
|
-
{#if form.isTainted()}
|
|
161
|
-
<Badge variant="outline" class="border-warning/30 bg-warning/10 text-warning-foreground">{t('common.unsaved')}</Badge>
|
|
162
|
-
{/if}
|
|
163
|
-
</div>
|
|
135
|
+
<div class="space-y-6">
|
|
136
|
+
<div class="flex items-center gap-4">
|
|
137
|
+
<TooltipButton
|
|
138
|
+
tooltip={t('common.back')}
|
|
139
|
+
onclick={() => guardNavigate(() => navigate(`/${resourceName}`))}
|
|
140
|
+
>
|
|
141
|
+
<ArrowLeft class="h-5 w-5" />
|
|
142
|
+
</TooltipButton>
|
|
143
|
+
<h1 class="text-xl font-semibold text-foreground">{pageTitle}</h1>
|
|
144
|
+
{#if headerContent}
|
|
145
|
+
{@render headerContent()}
|
|
146
|
+
{/if}
|
|
147
|
+
{#if form.isTainted()}
|
|
148
|
+
<Badge variant="outline" class="border-warning/30 bg-warning/10 text-warning-foreground">{t('common.unsaved')}</Badge>
|
|
149
|
+
{/if}
|
|
164
150
|
</div>
|
|
165
151
|
|
|
166
152
|
{#if form.loading}
|
|
167
|
-
<div class="max-w-
|
|
168
|
-
<div class="
|
|
153
|
+
<div class="max-w-3xl space-y-6">
|
|
154
|
+
<div class="rounded-lg shadow-sm ring-1 ring-border/10 p-6 space-y-5">
|
|
169
155
|
{#each Array(4) as _, _i (_i)}
|
|
170
156
|
<div class="space-y-2">
|
|
171
157
|
<Skeleton class="h-4 w-24" />
|
|
@@ -175,7 +161,7 @@
|
|
|
175
161
|
</div>
|
|
176
162
|
</div>
|
|
177
163
|
{:else}
|
|
178
|
-
<form onsubmit={(e: Event) => { e.preventDefault(); handleSubmit(); }} class="max-w-
|
|
164
|
+
<form onsubmit={(e: Event) => { e.preventDefault(); handleSubmit(); }} class="max-w-3xl space-y-6">
|
|
179
165
|
{#if submitError}
|
|
180
166
|
<div transition:slide={{ duration: 300, axis: 'y' }} class="svadmin-shake">
|
|
181
167
|
<Alert.Root variant="destructive">
|
|
@@ -187,15 +173,15 @@
|
|
|
187
173
|
|
|
188
174
|
{#if hasGroups}
|
|
189
175
|
{#each groups as group, _i (_i)}
|
|
190
|
-
<Card.Root class="
|
|
176
|
+
<Card.Root class="border-border/40 shadow-sm">
|
|
191
177
|
{#if group.name}
|
|
192
|
-
<Card.Header
|
|
193
|
-
<Card.Title class="text-
|
|
178
|
+
<Card.Header>
|
|
179
|
+
<Card.Title class="text-lg">{group.name}</Card.Title>
|
|
194
180
|
</Card.Header>
|
|
195
181
|
{/if}
|
|
196
|
-
<Card.Content class="px-4
|
|
182
|
+
<Card.Content class="space-y-5 px-4 sm:px-6 pb-4 sm:pb-6 pt-0">
|
|
197
183
|
{#each group.fields as field (field.key)}
|
|
198
|
-
<div
|
|
184
|
+
<div class:border-destructive={!!form.errors[field.key]}>
|
|
199
185
|
{#if fieldRenderer}
|
|
200
186
|
{@render fieldRenderer({ field, value: form.values[field.key], onchange: (val: unknown) => form.setFieldValue(field.key, val) })}
|
|
201
187
|
{:else}
|
|
@@ -215,10 +201,10 @@
|
|
|
215
201
|
</Card.Root>
|
|
216
202
|
{/each}
|
|
217
203
|
{:else}
|
|
218
|
-
<Card.Root class="
|
|
219
|
-
<Card.Content class="px-4
|
|
204
|
+
<Card.Root class="border-border/40 shadow-sm">
|
|
205
|
+
<Card.Content class="space-y-5 px-4 pt-5 pb-4 sm:px-6 sm:pt-6 sm:pb-6">
|
|
220
206
|
{#each formFields as field (field.key)}
|
|
221
|
-
<div
|
|
207
|
+
<div class:border-destructive={!!form.errors[field.key]}>
|
|
222
208
|
{#if fieldRenderer}
|
|
223
209
|
{@render fieldRenderer({ field, value: form.values[field.key], onchange: (val: unknown) => form.setFieldValue(field.key, val) })}
|
|
224
210
|
{:else}
|
|
@@ -238,11 +224,11 @@
|
|
|
238
224
|
</Card.Root>
|
|
239
225
|
{/if}
|
|
240
226
|
|
|
241
|
-
<div class="
|
|
227
|
+
<div class="flex items-center gap-3">
|
|
242
228
|
{#if formActions}
|
|
243
229
|
{@render formActions({ isLoading: form.submitting, onSubmit: handleSubmit })}
|
|
244
230
|
{:else if !isReadonly}
|
|
245
|
-
<Button type="submit" disabled={form.submitting}
|
|
231
|
+
<Button type="submit" disabled={form.submitting}>
|
|
246
232
|
{#if form.submitting}
|
|
247
233
|
<Loader2 class="h-4 w-4 animate-spin" data-icon="inline-start" />
|
|
248
234
|
{:else}
|
|
@@ -253,7 +239,6 @@
|
|
|
253
239
|
<Button
|
|
254
240
|
type="button"
|
|
255
241
|
variant="outline"
|
|
256
|
-
class="h-10 rounded-xl"
|
|
257
242
|
onclick={() => guardNavigate(() => navigate(`/${resourceName}`))}
|
|
258
243
|
>
|
|
259
244
|
{t('common.cancel')}
|
|
@@ -484,17 +484,13 @@
|
|
|
484
484
|
}
|
|
485
485
|
</script>
|
|
486
486
|
|
|
487
|
-
<div class="space-y-
|
|
487
|
+
<div class="space-y-4">
|
|
488
488
|
<!-- Header -->
|
|
489
|
-
<div class="
|
|
490
|
-
<
|
|
491
|
-
|
|
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>
|
|
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">
|
|
496
492
|
{#if canExport}
|
|
497
|
-
<Button variant="outline" size="sm"
|
|
493
|
+
<Button variant="outline" size="sm" onclick={exportCSV}>
|
|
498
494
|
<Download class="h-4 w-4" data-icon="inline-start" /> {t('common.export')}
|
|
499
495
|
</Button>
|
|
500
496
|
{/if}
|
|
@@ -517,8 +513,8 @@
|
|
|
517
513
|
<DropdownMenu.Root>
|
|
518
514
|
<DropdownMenu.Trigger>
|
|
519
515
|
{#snippet child({ props })}
|
|
520
|
-
<Button variant="outline" size="sm"
|
|
521
|
-
<SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {t('common.columns')}
|
|
516
|
+
<Button variant="outline" size="sm" {...props}>
|
|
517
|
+
<SlidersHorizontal class="h-4 w-4" data-icon="inline-start" /> {t('common.columns') || 'Columns'}
|
|
522
518
|
</Button>
|
|
523
519
|
{/snippet}
|
|
524
520
|
</DropdownMenu.Trigger>
|
|
@@ -537,16 +533,15 @@
|
|
|
537
533
|
{@render headerActions()}
|
|
538
534
|
{/if}
|
|
539
535
|
{#if canCreate}
|
|
540
|
-
<Button
|
|
536
|
+
<Button onclick={() => navigate(`/${resourceName}/create`)}>
|
|
541
537
|
<Plus class="h-4 w-4" data-icon="inline-start" /> {t('common.create')}
|
|
542
538
|
</Button>
|
|
543
539
|
{/if}
|
|
544
|
-
</div>
|
|
545
540
|
</div>
|
|
546
541
|
</div>
|
|
547
542
|
|
|
548
543
|
<!-- Search and Advanced Filters -->
|
|
549
|
-
<div class="flex flex-wrap items-center gap-
|
|
544
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
550
545
|
{#if searchableFields.length > 0}
|
|
551
546
|
<div class="relative max-w-sm flex-1 sm:min-w-[250px]">
|
|
552
547
|
<Search class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
|
@@ -555,7 +550,7 @@
|
|
|
555
550
|
bind:value={searchText}
|
|
556
551
|
oninput={() => { pagination = { ...pagination, current: 1 }; }}
|
|
557
552
|
placeholder={t('common.search')}
|
|
558
|
-
class="
|
|
553
|
+
class="pl-10 h-9"
|
|
559
554
|
/>
|
|
560
555
|
</div>
|
|
561
556
|
{/if}
|
|
@@ -564,7 +559,7 @@
|
|
|
564
559
|
<Popover.Root>
|
|
565
560
|
<Popover.Trigger>
|
|
566
561
|
{#snippet child({ props })}
|
|
567
|
-
<Button variant="outline" size="sm" class="h-
|
|
562
|
+
<Button variant="outline" size="sm" class="h-9 px-3" {...props}>
|
|
568
563
|
<FilterIcon class="h-4 w-4" data-icon="inline-start" />
|
|
569
564
|
{t('common.filter')}
|
|
570
565
|
{#if activeFilterCount > 0}
|
|
@@ -582,7 +577,7 @@
|
|
|
582
577
|
{#if field.type === 'select' && field.options}
|
|
583
578
|
<select
|
|
584
579
|
id="filter-{field.key}"
|
|
585
|
-
class="
|
|
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"
|
|
586
581
|
value={filterValues[field.key] ?? ''}
|
|
587
582
|
onchange={(e) => filterValues[field.key] = (e.currentTarget as HTMLSelectElement).value}
|
|
588
583
|
>
|
|
@@ -598,7 +593,7 @@
|
|
|
598
593
|
value={filterValues[field.key] ?? ''}
|
|
599
594
|
oninput={(e) => filterValues[field.key] = (e.currentTarget as HTMLInputElement).value}
|
|
600
595
|
placeholder={field.label}
|
|
601
|
-
class="h-
|
|
596
|
+
class="h-9 text-sm"
|
|
602
597
|
/>
|
|
603
598
|
{/if}
|
|
604
599
|
</div>
|
|
@@ -621,7 +616,7 @@
|
|
|
621
616
|
</div>
|
|
622
617
|
|
|
623
618
|
<!-- Table (TanStack-powered) -->
|
|
624
|
-
<div class="
|
|
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')}">
|
|
625
620
|
{#if query.isLoading}
|
|
626
621
|
<div class="p-4 space-y-3">
|
|
627
622
|
<div class="flex gap-4 mb-2">
|
|
@@ -645,8 +640,8 @@
|
|
|
645
640
|
<div in:fade={{ duration: 150 }}>
|
|
646
641
|
<!-- Desktop Table (hidden on mobile) -->
|
|
647
642
|
<div class="hidden md:block">
|
|
648
|
-
<Table.Root
|
|
649
|
-
<Table.Header
|
|
643
|
+
<Table.Root>
|
|
644
|
+
<Table.Header>
|
|
650
645
|
{#each table_getHeaderGroups(tbl) as headerGroup, _i (_i)}
|
|
651
646
|
<DraggableHeader
|
|
652
647
|
columns={headerGroup.headers.map((h: any) => ({ id: h.column.id, header: h }))}
|
|
@@ -657,7 +652,7 @@
|
|
|
657
652
|
{@const header = col.header as typeof headerGroup.headers[0]}
|
|
658
653
|
<Table.Head
|
|
659
654
|
{...dragProps}
|
|
660
|
-
class={cn("
|
|
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)}
|
|
661
656
|
style={header_getSize(header) != null && header_getSize(header) !== 150 ? `width:${header_getSize(header)}px` : undefined}
|
|
662
657
|
>
|
|
663
658
|
{#if header.id === '_select'}
|
|
@@ -699,9 +694,9 @@
|
|
|
699
694
|
<ContextMenu.Root>
|
|
700
695
|
<ContextMenu.Trigger>
|
|
701
696
|
{#snippet child({ props })}
|
|
702
|
-
<Table.Row {...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'}">
|
|
703
698
|
{#each row_getVisibleCells(row) as cell, _i (_i)}
|
|
704
|
-
<Table.Cell
|
|
699
|
+
<Table.Cell>
|
|
705
700
|
{#if cell.column.id === '_select'}
|
|
706
701
|
<Checkbox
|
|
707
702
|
checked={row_getIsSelected(row)}
|
|
@@ -760,9 +755,9 @@
|
|
|
760
755
|
onSave={() => listResult.refetch()}
|
|
761
756
|
/>
|
|
762
757
|
{:else if field?.key === primaryKey}
|
|
763
|
-
<span class="font-mono text-xs">{cell_getValue(cell) ?? '—'}</span>
|
|
758
|
+
<span class="block truncate font-mono text-xs" title={String(cell_getValue(cell) ?? '—')}>{cell_getValue(cell) ?? '—'}</span>
|
|
764
759
|
{:else}
|
|
765
|
-
{cell_getValue(cell) ?? '—'}
|
|
760
|
+
<span class="block truncate" title={String(cell_getValue(cell) ?? '—')}>{cell_getValue(cell) ?? '—'}</span>
|
|
766
761
|
{/if}
|
|
767
762
|
{/if}
|
|
768
763
|
</Table.Cell>
|
|
@@ -808,7 +803,7 @@
|
|
|
808
803
|
<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" />
|
|
809
804
|
</svg>
|
|
810
805
|
<p class="text-sm font-medium text-muted-foreground mb-1">{t('common.noData')}</p>
|
|
811
|
-
<p class="text-xs text-muted-foreground/60 mb-4">{t('common.noDataHint')}</p>
|
|
806
|
+
<p class="text-xs text-muted-foreground/60 mb-4">{t('common.noDataHint') || 'Get started by creating your first record.'}</p>
|
|
812
807
|
{#if canCreate}
|
|
813
808
|
<Button variant="outline" size="sm" class="gap-2" onclick={() => navigate(`/${resourceName}/create`)}>
|
|
814
809
|
<Plus class="h-3.5 w-3.5" />
|
|
@@ -830,7 +825,7 @@
|
|
|
830
825
|
{@const record = row.original}
|
|
831
826
|
{@const id = record[primaryKey] as string | number}
|
|
832
827
|
<div
|
|
833
|
-
class="rounded-
|
|
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' : ''}"
|
|
834
829
|
>
|
|
835
830
|
<!-- Card header: ID + select + actions -->
|
|
836
831
|
<div class="flex items-center justify-between mb-3">
|
|
@@ -911,7 +906,7 @@
|
|
|
911
906
|
|
|
912
907
|
<!-- Pagination (shadcn) -->
|
|
913
908
|
{#if totalPages > 0}
|
|
914
|
-
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground"
|
|
909
|
+
<div class="flex flex-col sm:flex-row items-center justify-between gap-2 text-sm text-muted-foreground">
|
|
915
910
|
<div class="flex items-center gap-2">
|
|
916
911
|
<span>{t('common.total', { total: query.data?.total ?? 0 })}</span>
|
|
917
912
|
<select
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
);
|
|
27
27
|
</script>
|
|
28
28
|
|
|
29
|
-
<div class="flex min-h-[70vh] items-center justify-center p-4">
|
|
29
|
+
<div class="flex min-h-[70vh] items-center justify-center p-4" data-svadmin-content-page="error">
|
|
30
30
|
<Card.Card class="w-full max-w-md border-border/60">
|
|
31
31
|
<Card.CardContent class="space-y-6 px-6 py-10 text-center">
|
|
32
32
|
<div class="mx-auto inline-flex h-16 w-16 items-center justify-center rounded-lg bg-muted/50 text-muted-foreground">
|
|
@@ -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-
|
|
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">
|
|
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')} href={siteUrl} target="_blank" rel="noopener noreferrer" class="flex items-center justify-center
|
|
57
|
+
<TooltipButton tooltip={t('common.goToSite') || 'Site'} href={siteUrl} target="_blank" rel="noopener noreferrer" class="rounded-full flex items-center justify-center">
|
|
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="
|
|
67
|
+
<Button variant="outline" size="sm" onclick={onSearchClick} class="gap-2 text-muted-foreground h-8 px-3">
|
|
68
68
|
<Search class="h-3.5 w-3.5" />
|
|
69
|
-
<span class="hidden text-xs
|
|
70
|
-
<kbd class="
|
|
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">
|
|
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-
|
|
80
|
+
<TooltipButton tooltip={t('common.toggleTheme')} onclick={() => toggleTheme()} class="rounded-full">
|
|
81
81
|
{#if getResolvedTheme() === 'dark'}
|
|
82
82
|
<Moon class="h-4 w-4 transition-all" />
|
|
83
83
|
{:else}
|
|
@@ -104,8 +104,8 @@
|
|
|
104
104
|
ontouchend={handleTouchEnd}
|
|
105
105
|
/>
|
|
106
106
|
{#if loading}
|
|
107
|
-
<div class="flex h-screen
|
|
108
|
-
<div class="hidden w-64
|
|
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">
|
|
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 space-y-6
|
|
116
|
+
<div class="flex-1 p-8 space-y-6">
|
|
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-
|
|
126
|
+
<div class="flex h-screen bg-background" 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,8 +138,8 @@
|
|
|
138
138
|
|
|
139
139
|
<div
|
|
140
140
|
class="flex-1 flex flex-col overflow-hidden transition-all duration-300"
|
|
141
|
-
class:md:ml-
|
|
142
|
-
class:md:ml-
|
|
141
|
+
class:md:ml-64={!collapsed}
|
|
142
|
+
class:md:ml-16={collapsed}
|
|
143
143
|
>
|
|
144
144
|
<!-- Header with mobile hamburger -->
|
|
145
145
|
<Header
|
|
@@ -163,9 +163,9 @@
|
|
|
163
163
|
</Header>
|
|
164
164
|
|
|
165
165
|
<!-- Content area -->
|
|
166
|
-
<main class="flex-1 overflow-y-auto
|
|
166
|
+
<main class="flex-1 overflow-y-auto p-6 md:p-8">
|
|
167
167
|
{#key getPath()}
|
|
168
|
-
<div
|
|
168
|
+
<div in:fly={{ x: 20, duration: 150 }} out:fade={{ duration: 80 }}>
|
|
169
169
|
{@render children()}
|
|
170
170
|
</div>
|
|
171
171
|
{/key}
|
|
@@ -19,21 +19,19 @@
|
|
|
19
19
|
}: Props = $props();
|
|
20
20
|
</script>
|
|
21
21
|
|
|
22
|
-
<div class="
|
|
22
|
+
<div class="space-y-4 {className}">
|
|
23
23
|
{#if showBreadcrumbs}
|
|
24
|
-
<
|
|
25
|
-
<Breadcrumbs />
|
|
26
|
-
</div>
|
|
24
|
+
<Breadcrumbs />
|
|
27
25
|
{/if}
|
|
28
|
-
<div class="flex flex-col gap-
|
|
29
|
-
<div
|
|
30
|
-
<h1 class="
|
|
26
|
+
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
27
|
+
<div>
|
|
28
|
+
<h1 class="text-xl font-semibold tracking-tight text-foreground">{title}</h1>
|
|
31
29
|
{#if description}
|
|
32
|
-
<p class="
|
|
30
|
+
<p class="text-sm text-muted-foreground">{description}</p>
|
|
33
31
|
{/if}
|
|
34
32
|
</div>
|
|
35
33
|
{#if actions}
|
|
36
|
-
<div class="flex
|
|
34
|
+
<div class="flex items-center gap-2">
|
|
37
35
|
{@render actions()}
|
|
38
36
|
</div>
|
|
39
37
|
{/if}
|
|
@@ -59,13 +59,12 @@
|
|
|
59
59
|
</script>
|
|
60
60
|
|
|
61
61
|
<div class="space-y-6" data-svadmin-resource-operations={resourceName}>
|
|
62
|
-
<section class="grid gap-4 xl:grid-cols-[0.82fr_1.18fr]">
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
<Badge class="mb-4 rounded-full bg-primary/10 text-primary hover:bg-primary/10">{eyebrow}</Badge>
|
|
62
|
+
<section class="grid gap-4 border-b border-border/60 pb-5 xl:grid-cols-[0.82fr_1.18fr]" data-svadmin-ops-hero>
|
|
63
|
+
<div class="min-w-0 py-1">
|
|
64
|
+
<Badge>{eyebrow}</Badge>
|
|
66
65
|
<div class="flex items-start gap-4">
|
|
67
66
|
{#if Icon}
|
|
68
|
-
<span class="flex h-
|
|
67
|
+
<span class="flex h-10 w-10 shrink-0 items-center justify-center text-primary">
|
|
69
68
|
<Icon class="h-6 w-6" />
|
|
70
69
|
</span>
|
|
71
70
|
{/if}
|
|
@@ -74,15 +73,14 @@
|
|
|
74
73
|
<p class="mt-2 max-w-2xl text-sm text-muted-foreground">{description}</p>
|
|
75
74
|
</div>
|
|
76
75
|
</div>
|
|
77
|
-
<Button class="mt-6
|
|
76
|
+
<Button class="mt-6" onclick={() => navigate(`/${resourceName}/create`)}>
|
|
78
77
|
{actionLabel}
|
|
79
78
|
</Button>
|
|
80
|
-
|
|
81
|
-
</Card.Root>
|
|
79
|
+
</div>
|
|
82
80
|
|
|
83
81
|
<div class="grid gap-4 md:grid-cols-3">
|
|
84
82
|
{#each metrics as metric (metric.label)}
|
|
85
|
-
<Card.Root
|
|
83
|
+
<Card.Root data-svadmin-metric-card>
|
|
86
84
|
<Card.Content class="p-5">
|
|
87
85
|
<p class="text-xs font-semibold uppercase tracking-[0.14em] text-muted-foreground">{metric.label}</p>
|
|
88
86
|
<p class="mt-2 text-3xl font-semibold">{metric.value}</p>
|
|
@@ -97,7 +95,7 @@
|
|
|
97
95
|
|
|
98
96
|
<div class="grid gap-4 xl:grid-cols-[0.76fr_1.24fr]">
|
|
99
97
|
<div class="space-y-4">
|
|
100
|
-
<Card.Root
|
|
98
|
+
<Card.Root data-svadmin-panel-card>
|
|
101
99
|
<Card.Header>
|
|
102
100
|
<Card.Title>{tableLabel}</Card.Title>
|
|
103
101
|
{#if tableDescription}
|
|
@@ -106,7 +104,7 @@
|
|
|
106
104
|
</Card.Header>
|
|
107
105
|
<Card.Content class="space-y-3">
|
|
108
106
|
{#each lanes as lane (lane.label)}
|
|
109
|
-
<div class="
|
|
107
|
+
<div class="py-3">
|
|
110
108
|
<div class="flex items-center justify-between gap-3">
|
|
111
109
|
<p class="font-medium capitalize">{lane.label}</p>
|
|
112
110
|
<Badge variant="outline">{lane.value}</Badge>
|
|
@@ -116,19 +114,19 @@
|
|
|
116
114
|
{/if}
|
|
117
115
|
</div>
|
|
118
116
|
{:else}
|
|
119
|
-
<p class="
|
|
117
|
+
<p class="py-3 text-sm text-muted-foreground">{emptyLanesText}</p>
|
|
120
118
|
{/each}
|
|
121
119
|
</Card.Content>
|
|
122
120
|
</Card.Root>
|
|
123
121
|
|
|
124
122
|
{#if highlights.length > 0}
|
|
125
|
-
<Card.Root
|
|
123
|
+
<Card.Root data-svadmin-panel-card>
|
|
126
124
|
<Card.Header>
|
|
127
125
|
<Card.Title>{highlightsLabel}</Card.Title>
|
|
128
126
|
</Card.Header>
|
|
129
127
|
<Card.Content class="space-y-3">
|
|
130
128
|
{#each highlights as item (item.title)}
|
|
131
|
-
<div class="
|
|
129
|
+
<div class="py-3">
|
|
132
130
|
<div class="flex items-center justify-between gap-3">
|
|
133
131
|
<p class="font-medium">{item.title}</p>
|
|
134
132
|
{#if item.badge}
|
|
@@ -107,17 +107,17 @@
|
|
|
107
107
|
});
|
|
108
108
|
</script>
|
|
109
109
|
|
|
110
|
-
<div class="flex
|
|
110
|
+
<div class="flex flex-col lg:flex-row gap-0 min-h-full">
|
|
111
111
|
<!-- Left sidebar navigation -->
|
|
112
|
-
<nav class="w-full shrink-0
|
|
112
|
+
<nav class="w-full lg:w-56 shrink-0 bg-muted/10 shadow-[inset_0_-1px_0_rgba(0,0,0,0.05)] lg:shadow-[inset_-1px_0_0_rgba(0,0,0,0.05)] dark:shadow-[inset_-1px_0_0_rgba(255,255,255,0.05)]">
|
|
113
113
|
<!-- Mobile: horizontal scroll tabs -->
|
|
114
|
-
<div class="flex
|
|
114
|
+
<div class="flex lg:hidden overflow-x-auto px-4 py-2 gap-1">
|
|
115
115
|
{#each resolvedSections as section, _i (_i)}
|
|
116
116
|
{#each section.items as item, _j (_j)}
|
|
117
117
|
{@const active = activeKey === item.key}
|
|
118
118
|
<button
|
|
119
|
-
class="flex items-center gap-2
|
|
120
|
-
{active ? 'bg-
|
|
119
|
+
class="flex items-center gap-2 px-3 py-2 text-sm font-medium rounded-md whitespace-nowrap transition-colors
|
|
120
|
+
{active ? 'bg-background text-foreground shadow-sm' : 'text-muted-foreground hover:text-foreground hover:bg-background/50'}"
|
|
121
121
|
onclick={() => navigate(item.path)}
|
|
122
122
|
>
|
|
123
123
|
<item.icon class="h-4 w-4" />
|
|
@@ -128,24 +128,24 @@
|
|
|
128
128
|
</div>
|
|
129
129
|
|
|
130
130
|
<!-- Desktop: vertical nav with groups -->
|
|
131
|
-
<div class="hidden
|
|
132
|
-
<div class="
|
|
133
|
-
<h2 class="text-lg font-semibold
|
|
131
|
+
<div class="hidden lg:block py-6 px-3 space-y-6">
|
|
132
|
+
<div class="px-3">
|
|
133
|
+
<h2 class="text-lg font-semibold text-foreground">{t("settings.title")}</h2>
|
|
134
134
|
<p class="text-xs text-muted-foreground mt-1">{t("settings.settingsDescription")}</p>
|
|
135
135
|
</div>
|
|
136
136
|
{#each resolvedSections as section, _i (_i)}
|
|
137
137
|
<div>
|
|
138
|
-
<h3 class="
|
|
138
|
+
<h3 class="px-3 mb-1 text-[11px] font-semibold uppercase tracking-wider text-muted-foreground/60">
|
|
139
139
|
{t(section.group)}
|
|
140
140
|
</h3>
|
|
141
|
-
<div class="space-y-
|
|
141
|
+
<div class="space-y-0.5">
|
|
142
142
|
{#each section.items as item, _j (_j)}
|
|
143
143
|
{@const active = activeKey === item.key}
|
|
144
144
|
<button
|
|
145
|
-
class="flex w-full items-center gap-3 rounded-
|
|
145
|
+
class="flex w-full items-center gap-3 rounded-lg px-3 py-2 text-sm transition-all
|
|
146
146
|
{active
|
|
147
|
-
? 'bg-primary/10 text-primary font-medium
|
|
148
|
-
: 'text-muted-foreground hover:bg-
|
|
147
|
+
? 'bg-primary/10 text-primary font-medium'
|
|
148
|
+
: 'text-muted-foreground hover:bg-accent hover:text-accent-foreground'}"
|
|
149
149
|
onclick={() => navigate(item.path)}
|
|
150
150
|
>
|
|
151
151
|
<item.icon class="h-4 w-4 shrink-0" />
|
|
@@ -159,7 +159,7 @@
|
|
|
159
159
|
</nav>
|
|
160
160
|
|
|
161
161
|
<!-- Right content area -->
|
|
162
|
-
<div class="flex-1
|
|
162
|
+
<div class="flex-1 p-6 lg:p-8 {activeKey === 'roles' || activeKey === 'audit' || activeKey === 'integrations' || activeKey === 'api' ? 'max-w-6xl' : 'max-w-3xl'}">
|
|
163
163
|
{#if activeKey === "profile"}
|
|
164
164
|
{#if profile}{@render profile()}{:else}<ProfilePage />{/if}
|
|
165
165
|
{:else if activeKey === "appearance"}
|