@svadmin/ui 0.36.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 CHANGED
@@ -1,9 +1,9 @@
1
1
  /* Typography — Inter font for clean, modern look */
2
2
 
3
- /* @svadmin/ui — Design Tokens (Elygate-inspired)
3
+ /* @svadmin/ui — Design Tokens (clean admin shell)
4
4
  * Auto-imported by AdminApp. Can be overridden by consumer's CSS.
5
5
  *
6
- * Based on Elygate dashboard style: Indigo primary, light glassmorphism,
6
+ * Clean dashboard style: indigo primary, light glassmorphism,
7
7
  * professional admin aesthetic with subtle depth.
8
8
  */
9
9
 
@@ -253,7 +253,7 @@
253
253
  --chart-1: oklch(0.541 0.281 293.009);
254
254
  }
255
255
 
256
- /* Stripe-inspired Soft Shadows — premium, multi-layered depth */
256
+ /* Soft shadows — premium, multi-layered depth */
257
257
  [data-slot="button"],
258
258
  [data-slot="input"],
259
259
  [data-slot="select-trigger"],
@@ -263,7 +263,7 @@
263
263
  box-shadow: 0 1px 2px 0 rgb(0 0 0 / 0.04), 0 0 0 1px rgb(0 0 0 / 0.02);
264
264
  }
265
265
 
266
- /* Stripe-style: form inputs use card background instead of transparent */
266
+ /* Form inputs use card background instead of transparent */
267
267
  [data-slot="input"],
268
268
  [data-slot="textarea"],
269
269
  [data-slot="select"] {
@@ -271,7 +271,7 @@
271
271
  }
272
272
 
273
273
  [data-slot="card"] {
274
- /* Classic Stripe soft shadow: faint ambient + directional drop */
274
+ /* Classic soft shadow: faint ambient + directional drop */
275
275
  box-shadow: 0 2px 5px -1px rgb(0 0 0 / 0.04), 0 1px 1px -1px rgb(0 0 0 / 0.04), 0 0 0 1px rgb(0 0 0 / 0.02);
276
276
  }
277
277
 
@@ -333,7 +333,7 @@ button:active:not(:disabled) {
333
333
  .dark ::-webkit-scrollbar-thumb { background: oklch(0.35 0 0); }
334
334
  .dark ::-webkit-scrollbar-thumb:hover { background: oklch(0.45 0 0); }
335
335
 
336
- /* ① Card Hover Elevation Lift — Stripe-style float on hover */
336
+ /* Card Hover Elevation Lift — subtle float on hover */
337
337
  [data-slot="card"] {
338
338
  transition: box-shadow 0.2s ease, transform 0.2s ease;
339
339
  }
@@ -1245,16 +1245,16 @@ main {
1245
1245
  }
1246
1246
 
1247
1247
  .layout-clean-flat main [data-slot="card"] {
1248
- border-left: 0 !important;
1249
- border-right: 0 !important;
1250
- border-radius: 0 !important;
1248
+ border-left: 1px solid color-mix(in oklch, var(--border) 72%, transparent) !important;
1249
+ border-right: 1px solid color-mix(in oklch, var(--border) 72%, transparent) !important;
1250
+ border-radius: 0.5rem !important;
1251
1251
  }
1252
1252
 
1253
1253
  .layout-clean-flat main [data-slot="card-header"],
1254
1254
  .layout-clean-flat main [data-slot="card-content"],
1255
1255
  .layout-clean-flat main [data-slot="card-footer"] {
1256
- padding-left: 0 !important;
1257
- padding-right: 0 !important;
1256
+ padding-left: 1rem !important;
1257
+ padding-right: 1rem !important;
1258
1258
  }
1259
1259
 
1260
1260
  .layout-clean-flat main > div[class*="space-y"] > section:first-child {
@@ -1275,6 +1275,24 @@ main {
1275
1275
  .layout-clean-flat [data-svadmin-form-actions] {
1276
1276
  width: 100% !important;
1277
1277
  }
1278
+
1279
+ .layout-clean-flat [data-svadmin-table-card],
1280
+ .layout-clean-flat [data-svadmin-show-card],
1281
+ .layout-clean-flat [data-svadmin-form-card] {
1282
+ border-left: 1px solid color-mix(in oklch, var(--border) 72%, transparent) !important;
1283
+ border-right: 1px solid color-mix(in oklch, var(--border) 72%, transparent) !important;
1284
+ border-radius: 0.5rem !important;
1285
+ }
1286
+
1287
+ .layout-clean-flat [data-svadmin-table-card] [data-slot="card-header"],
1288
+ .layout-clean-flat [data-svadmin-table-card] [data-slot="card-content"],
1289
+ .layout-clean-flat [data-svadmin-show-card] [data-slot="card-header"],
1290
+ .layout-clean-flat [data-svadmin-show-card] [data-slot="card-content"],
1291
+ .layout-clean-flat [data-svadmin-form-card] [data-slot="card-header"],
1292
+ .layout-clean-flat [data-svadmin-form-card] [data-slot="card-content"] {
1293
+ padding-left: 1rem !important;
1294
+ padding-right: 1rem !important;
1295
+ }
1278
1296
  }
1279
1297
 
1280
1298
  @media (min-width: 768px) and (max-width: 1023px) {
@@ -1375,15 +1393,15 @@ main {
1375
1393
  }
1376
1394
 
1377
1395
  .layout-clean-flat [data-svadmin-content-page] [data-slot="card"] {
1378
- border-left: 0 !important;
1379
- border-right: 0 !important;
1380
- border-radius: 0 !important;
1396
+ border-left: 1px solid var(--svadmin-content-border) !important;
1397
+ border-right: 1px solid var(--svadmin-content-border) !important;
1398
+ border-radius: 0.5rem !important;
1381
1399
  }
1382
1400
 
1383
1401
  .layout-clean-flat [data-svadmin-content-page] [data-slot="card-content"],
1384
1402
  .layout-clean-flat [data-svadmin-content-page] [data-slot="card-header"] {
1385
- padding-left: 0 !important;
1386
- padding-right: 0 !important;
1403
+ padding-left: 1rem !important;
1404
+ padding-right: 1rem !important;
1387
1405
  }
1388
1406
 
1389
1407
  .layout-clean-flat [data-svadmin-content-page] [class*="grid-cols-3"] {
@@ -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 result: Array<{ name: string; fields: FieldDefinition[] }> = [];
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
- let group = result.find(item => item.name === g);
51
- if (!group) {
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 result;
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" data-svadmin-form-shell>
139
- <div class="border-b border-border/60 pb-5" data-svadmin-form-header>
140
- <div class="flex flex-wrap items-center gap-4">
141
- <TooltipButton
142
- tooltip={t('common.back')}
143
- onclick={() => guardNavigate(() => navigate(`/${resourceName}`))}
144
- class="h-10 w-10"
145
- >
146
- <ArrowLeft class="h-5 w-5" />
147
- </TooltipButton>
148
- <div class="min-w-0 flex-1">
149
- <h1 class="truncate text-2xl font-semibold tracking-tight text-foreground">{pageTitle}</h1>
150
- <div class="mt-2 flex flex-wrap items-center gap-2">
151
- <Badge variant="secondary" class="rounded-full px-2.5 py-0.5 text-[0.6875rem] font-semibold">
152
- {mode === 'create' ? t('common.create') : mode === 'show' ? t('common.detail') : t('common.edit')}
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-4xl space-y-6">
168
- <div class="space-y-5 border-y border-border/60 py-6">
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-5xl space-y-6" data-svadmin-form-settings>
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="border-border/60 bg-card shadow-none" data-svadmin-form-card>
176
+ <Card.Root class="border-border/40 shadow-sm">
191
177
  {#if group.name}
192
178
  <Card.Header>
193
- <Card.Title class="text-base font-semibold">{group.name}</Card.Title>
179
+ <Card.Title class="text-lg">{group.name}</Card.Title>
194
180
  </Card.Header>
195
181
  {/if}
196
- <Card.Content class="px-4 py-0 sm:px-6" data-svadmin-form-content>
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 data-svadmin-form-row class:border-destructive={!!form.errors[field.key]}>
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="border-border/60 bg-card shadow-none" data-svadmin-form-card>
219
- <Card.Content class="px-4 py-0 sm:px-6" data-svadmin-form-content>
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 data-svadmin-form-row class:border-destructive={!!form.errors[field.key]}>
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="sticky bottom-4 z-10 flex flex-wrap items-center gap-3 border-t border-border/60 bg-background/95 p-3 backdrop-blur" data-svadmin-form-actions>
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} class="h-10 px-5">
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-5" data-svadmin-list>
487
+ <div class="space-y-4">
488
488
  <!-- Header -->
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>
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" class="h-10 rounded-xl" onclick={exportCSV}>
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" class="h-10 rounded-xl" {...props}>
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 class="h-10 rounded-xl px-4 shadow-sm shadow-primary/15" onclick={() => navigate(`/${resourceName}/create`)}>
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-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>
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="h-10 rounded-xl border-border/70 bg-background/85 pl-10"
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-10 rounded-xl px-3" {...props}>
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="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"
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-10 rounded-xl text-sm"
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="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')}">
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 class="svadmin-data-table" data-svadmin-datatable>
649
- <Table.Header data-svadmin-table-head>
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("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)}
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} data-svadmin-table-row class="border-b border-border/30 transition-colors duration-200 {row_getIsSelected(row) ? 'bg-primary/5' : 'hover:bg-muted/25'}">
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 class="px-4 py-3.5">
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-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' : ''}"
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,11 +906,11 @@
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" data-svadmin-pagination>
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
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"
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"
919
914
  value={String(pagination.pageSize ?? 10)}
920
915
  onchange={(e) => {
921
916
  const size = Number((e.target as HTMLSelectElement).value);
@@ -930,7 +925,7 @@
930
925
  <option value="100">100</option>
931
926
  </select>
932
927
  </div>
933
- <PaginationUI.Root class="sm:w-auto sm:justify-end sm:mx-0">
928
+ <PaginationUI.Root>
934
929
  <PaginationUI.Content>
935
930
  <PaginationUI.Item>
936
931
  <PaginationUI.Previous
@@ -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-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">
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 rounded-xl border border-border/60 bg-background/80">
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="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">
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 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">
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-xl border border-border/60 bg-background/80">
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 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">
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 p-8">
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-[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 }}>
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,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-72={!collapsed}
141
+ class:md:ml-64={!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 px-4 py-5 sm:px-6 md:px-8 md:py-7">
166
+ <main class="flex-1 overflow-y-auto p-6 md:p-8">
167
167
  {#key getPath()}
168
- <div class="mx-auto w-full max-w-[1680px]" in:fly={{ x: 20, duration: 150 }} out:fade={{ duration: 80 }}>
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="border-b border-border/70 pb-5 {className}" data-svadmin-page-header>
22
+ <div class="space-y-4 {className}">
23
23
  {#if showBreadcrumbs}
24
- <div class="mb-4">
25
- <Breadcrumbs />
26
- </div>
24
+ <Breadcrumbs />
27
25
  {/if}
28
- <div class="flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between">
29
- <div class="min-w-0">
30
- <h1 class="truncate text-2xl font-semibold tracking-tight text-foreground">{title}</h1>
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="mt-1 max-w-3xl text-sm text-muted-foreground">{description}</p>
30
+ <p class="text-sm text-muted-foreground">{description}</p>
33
31
  {/if}
34
32
  </div>
35
33
  {#if actions}
36
- <div class="flex w-full flex-wrap items-center gap-2 lg:w-auto lg:justify-end">
34
+ <div class="flex items-center gap-2">
37
35
  {@render actions()}
38
36
  </div>
39
37
  {/if}