@svadmin/ui 0.77.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/app.css +99 -79
  2. package/dist/app.theme.css +99 -79
  3. package/dist/components/AboutSettings.svelte +4 -3
  4. package/dist/components/AboutSettings.svelte.d.ts +1 -0
  5. package/dist/components/AdminApp.svelte +30 -15
  6. package/dist/components/ApiSettings.svelte +39 -2
  7. package/dist/components/AppearanceSettings.svelte +35 -9
  8. package/dist/components/AppearanceSettings.svelte.d.ts +4 -1
  9. package/dist/components/AutoTable.svelte +10 -6
  10. package/dist/components/AutoTable.svelte.d.ts +2 -0
  11. package/dist/components/IntegrationsSettings.svelte +5 -3
  12. package/dist/components/IntegrationsSettings.svelte.d.ts +1 -0
  13. package/dist/components/NotificationsSettings.svelte +18 -12
  14. package/dist/components/NotificationsSettings.svelte.d.ts +1 -0
  15. package/dist/components/ProfilePage.svelte +3 -3
  16. package/dist/components/ProfilePage.svelte.d.ts +4 -1
  17. package/dist/components/ResourceOverview.svelte +36 -0
  18. package/dist/components/ResourceOverview.svelte.d.ts +6 -0
  19. package/dist/components/ResourceOverviewCard.svelte +50 -0
  20. package/dist/components/ResourceOverviewCard.svelte.d.ts +7 -0
  21. package/dist/components/SecuritySettings.svelte +9 -6
  22. package/dist/components/SecuritySettings.svelte.d.ts +4 -1
  23. package/dist/components/account/SettingsEnterprisePage.svelte +19 -19
  24. package/dist/components/account/SettingsPlainPage.svelte +7 -3
  25. package/dist/components/account/SettingsSidebarPage.svelte +3 -3
  26. package/dist/components/content/ContentPageHeader.svelte +3 -1
  27. package/dist/components/content/ContentPageHeader.svelte.d.ts +1 -0
  28. package/dist/components/content/DashboardPage.svelte +35 -0
  29. package/dist/components/content/DashboardPage.svelte.d.ts +13 -0
  30. package/dist/components/content/FeedbackNotice.svelte +1 -1
  31. package/dist/components/content/PageSection.svelte +28 -0
  32. package/dist/components/content/PageSection.svelte.d.ts +10 -0
  33. package/dist/components/content/SettingsFieldRow.svelte +10 -5
  34. package/dist/components/content/SettingsFieldRow.svelte.d.ts +1 -0
  35. package/dist/components/content/SettingsGroup.svelte +3 -3
  36. package/dist/components/content/SettingsGroup.svelte.d.ts +1 -0
  37. package/dist/components/content/index.d.ts +2 -0
  38. package/dist/components/content/index.js +2 -0
  39. package/dist/components/ui/button/button.svelte +17 -1
  40. package/dist/components/ui/button/button.svelte.d.ts +1 -0
  41. package/dist/components/ui/sheet/sheet.svelte +3 -0
  42. package/dist/default-theme.d.ts +3 -0
  43. package/dist/default-theme.js +4 -0
  44. package/dist/index.d.ts +3 -0
  45. package/dist/index.js +3 -0
  46. package/dist/page-chrome.css +4 -4
  47. package/package.json +1 -1
@@ -62,6 +62,10 @@
62
62
  import { setComponentRegistry, type ComponentRegistry } from '../component-registry.svelte.js';
63
63
  import Layout, { type LayoutAIAssistantProps } from './Layout.svelte';
64
64
  import AutoTable from './LazyAutoTable.svelte';
65
+ import ResourceOverview from './ResourceOverview.svelte';
66
+ import DataState from './content/DataState.svelte';
67
+ import { parseContractRouteId } from '@svadmin/core/schema';
68
+ import { resolveAdminThemeConfig } from '../default-theme.js';
65
69
  import AutoForm from './LazyAutoForm.svelte';
66
70
  import ShowPage from './LazyShowPage.svelte';
67
71
  import Toast from './Toast.svelte';
@@ -270,11 +274,13 @@
270
274
  });
271
275
 
272
276
  let themeOwner = $state.raw<ReturnType<typeof registerThemeOwner>>(undefined);
277
+ const resolvedThemeConfig = $derived(userThemeConfig === undefined ? undefined : resolveAdminThemeConfig(userThemeConfig));
273
278
 
274
279
  onMount(() => {
275
280
  themeOwner = registerThemeOwner(definedOptions({
276
281
  defaultTheme,
277
- themeConfig: userThemeConfig,
282
+ themeConfig: resolvedThemeConfig,
283
+ fallbackThemeConfig: resolveAdminThemeConfig(),
278
284
  }));
279
285
 
280
286
  return () => {
@@ -287,7 +293,8 @@
287
293
  const owner = themeOwner;
288
294
  const ownerOptions = definedOptions({
289
295
  defaultTheme,
290
- themeConfig: userThemeConfig,
296
+ themeConfig: resolvedThemeConfig,
297
+ fallbackThemeConfig: resolveAdminThemeConfig(),
291
298
  });
292
299
  untrack(() => updateThemeOwner(owner, ownerOptions));
293
300
  });
@@ -364,6 +371,15 @@
364
371
  const renderedRoute = $derived(route);
365
372
  const renderedParams = $derived(params);
366
373
  const renderedResourceName = $derived(renderedParams['resource']);
374
+ const renderedRecord = $derived.by(() => {
375
+ const id = renderedParams['id'];
376
+ const contract = resources.find(resource => resource.name === renderedResourceName)?.contract;
377
+ try {
378
+ return { id: id === undefined || !contract ? id : parseContractRouteId(contract, id), invalid: false };
379
+ } catch {
380
+ return { id: undefined, invalid: true };
381
+ }
382
+ });
367
383
  const renderedHasRouteResource = $derived(renderedResourceName !== undefined && resourceNames.has(renderedResourceName));
368
384
  const renderedResourcePages = $derived(renderedParams['resource'] ? resourcePages?.[renderedParams['resource']] : undefined);
369
385
  const documentTitle = $derived.by(() => {
@@ -616,10 +632,7 @@
616
632
  {#if dashboard}
617
633
  {@render dashboard()}
618
634
  {:else}
619
- <div class="svadmin-u-3e7ce58d64fa">
620
- <h1 class="svadmin-u-d5c9b0001e7e svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{translation.t('common.welcome', { title })}</h1>
621
- <p class="svadmin-u-bfa603190748">{translation.t('common.dashboardHint')}</p>
622
- </div>
635
+ <ResourceOverview />
623
636
  {/if}
624
637
  {:else if (renderedRoute === '/:resource' || renderedRoute === '/:parent/:parentId/:resource') && renderedHasRouteResource && renderedResourceName !== undefined}
625
638
  {#key renderedParams['resource']}
@@ -635,25 +648,27 @@
635
648
  <Comp resourceName={renderedResourceName} mode="create" />
636
649
  </ResourceAccessGuard>
637
650
  {/key}
638
- {:else if (renderedRoute === '/:resource/edit/:id' || renderedRoute === '/:resource/:id/edit' || renderedRoute === '/:parent/:parentId/:resource/edit/:id') && renderedHasRouteResource && renderedResourceName !== undefined && renderedParams['id'] !== undefined}
651
+ {:else if renderedHasRouteResource && renderedRecord.invalid}
652
+ <DataState state="error" title={translation.t('common.operationFailed')} />
653
+ {:else if (renderedRoute === '/:resource/edit/:id' || renderedRoute === '/:resource/:id/edit' || renderedRoute === '/:parent/:parentId/:resource/edit/:id') && renderedHasRouteResource && renderedResourceName !== undefined && renderedRecord.id !== undefined}
639
654
  {#key `${renderedParams['resource']}-${renderedParams['id']}`}
640
655
  {@const Comp = renderedResourcePages?.edit ?? mergedComponents.AutoForm}
641
- <ResourceAccessGuard resourceName={renderedResourceName} action="edit" {...definedOptions({ "id": renderedParams['id'] })}>
642
- <Comp resourceName={renderedResourceName} mode="edit" {...definedOptions({ "id": renderedParams['id'] })} />
656
+ <ResourceAccessGuard resourceName={renderedResourceName} action="edit" id={renderedRecord.id}>
657
+ <Comp resourceName={renderedResourceName} mode="edit" id={renderedRecord.id} />
643
658
  </ResourceAccessGuard>
644
659
  {/key}
645
- {:else if (renderedRoute === '/:resource/show/:id' || renderedRoute === '/:resource/:id' || renderedRoute === '/:parent/:parentId/:resource/show/:id') && renderedHasRouteResource && renderedResourceName !== undefined && renderedParams['id'] !== undefined}
660
+ {:else if (renderedRoute === '/:resource/show/:id' || renderedRoute === '/:resource/:id' || renderedRoute === '/:parent/:parentId/:resource/show/:id') && renderedHasRouteResource && renderedResourceName !== undefined && renderedRecord.id !== undefined}
646
661
  {#key `${renderedParams['resource']}-${renderedParams['id']}`}
647
662
  {@const Comp = renderedResourcePages?.show ?? mergedComponents.ShowPage}
648
- <ResourceAccessGuard resourceName={renderedResourceName} action="show" {...definedOptions({ "id": renderedParams['id'] })}>
649
- <Comp resourceName={renderedResourceName} id={renderedParams['id']} />
663
+ <ResourceAccessGuard resourceName={renderedResourceName} action="show" id={renderedRecord.id}>
664
+ <Comp resourceName={renderedResourceName} id={renderedRecord.id} />
650
665
  </ResourceAccessGuard>
651
666
  {/key}
652
- {:else if (renderedRoute === '/:resource/clone/:id' || renderedRoute === '/:parent/:parentId/:resource/clone/:id') && renderedHasRouteResource && renderedResourceName !== undefined && renderedParams['id'] !== undefined}
667
+ {:else if (renderedRoute === '/:resource/clone/:id' || renderedRoute === '/:parent/:parentId/:resource/clone/:id') && renderedHasRouteResource && renderedResourceName !== undefined && renderedRecord.id !== undefined}
653
668
  {#key `${renderedParams['resource']}-clone-${renderedParams['id']}`}
654
669
  {@const Comp = renderedResourcePages?.clone ?? mergedComponents.AutoForm}
655
- <ResourceAccessGuard resourceName={renderedResourceName} action="create" {...definedOptions({ "id": renderedParams['id'] })} requireSourceRead>
656
- <Comp resourceName={renderedResourceName} mode="clone" {...definedOptions({ "id": renderedParams['id'] })} />
670
+ <ResourceAccessGuard resourceName={renderedResourceName} action="create" id={renderedRecord.id} requireSourceRead>
671
+ <Comp resourceName={renderedResourceName} mode="clone" id={renderedRecord.id} />
657
672
  </ResourceAccessGuard>
658
673
  {/key}
659
674
  {:else}
@@ -16,6 +16,7 @@
16
16
  import WorkspaceLayout from './content/WorkspaceLayout.svelte';
17
17
 
18
18
  const i18n = useTranslation();
19
+ const controlId = $props.id();
19
20
  const adminContext = captureAdminContext();
20
21
  const credentialProvider = $derived(adminContext.credentialProvider);
21
22
  const isZh = $derived(i18n.locale === 'zh-CN');
@@ -221,14 +222,50 @@
221
222
  {/if}
222
223
  </SettingsGroup>
223
224
  <SettingsGroup title={i18n.t('api.webhooks')} description={i18n.t('api.webhooksDesc')} bodyClass="space-y-5">
224
- <form class="svadmin-u-f3c543ad5fe9 svadmin-u-7e0b7cdf1a94 svadmin-u-0c3bc98565dd svadmin-u-e4d6f343b9ff" onsubmit={handleAddWebhook}><div class="svadmin-u-6f7e013d6499"><Label for="webhook-name">{i18n.t('api.webhookName')}</Label><Input id="webhook-name" bind:value={newWebhookName} placeholder="Order Events" disabled={!credentialProvider || submitting} /></div><div class="svadmin-u-6f7e013d6499 svadmin-u-eea04c60c0ca"><Label for="webhook-url">{i18n.t('api.webhookUrl')}</Label><Input id="webhook-url" type="url" bind:value={newWebhookUrl} placeholder="https://api.example.com/hooks" disabled={!credentialProvider || submitting} /></div><div class="svadmin-u-6f7e013d6499"><Label for="webhook-event">{i18n.t('api.eventType')}</Label><select id="webhook-event" bind:value={newWebhookEvent} disabled={!credentialProvider || submitting} class="svadmin-u-60fbb7713999 svadmin-u-e7a768f922d2 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b svadmin-u-fc7473ca09eb svadmin-u-582e6ef4b245 svadmin-u-55d048ebfb1c svadmin-u-608dd26cd5ba svadmin-u-80b9d0ae125f svadmin-u-6b22a22a9752 svadmin-u-b29d8adbad2e"><option value="resource.created">resource.created</option><option value="resource.updated">resource.updated</option><option value="resource.deleted">resource.deleted</option><option value="auth.login">auth.login</option></select></div><div class="svadmin-u-60fbb7713999 svadmin-u-6f27f4f79e55"><Button type="submit" class="svadmin-u-6da6a3c3f741" disabled={!credentialProvider || submitting || !newWebhookName.trim() || !newWebhookUrl.trim()}><Plus class="svadmin-u-f7b5fa971871" />{i18n.t('api.addWebhook')}</Button></div></form>
225
+ <form class="svadmin-u-f3c543ad5fe9 svadmin-u-7e0b7cdf1a94 svadmin-u-0c3bc98565dd svadmin-u-e4d6f343b9ff" onsubmit={handleAddWebhook}>
226
+ <div class="svadmin-u-6f7e013d6499">
227
+ <Label for={`${controlId}-webhook-name`}>{i18n.t('api.webhookName')}</Label>
228
+ <Input id={`${controlId}-webhook-name`} bind:value={newWebhookName} placeholder="Order Events" disabled={!credentialProvider || submitting} />
229
+ </div>
230
+ <div class="svadmin-u-6f7e013d6499 svadmin-u-eea04c60c0ca">
231
+ <Label for={`${controlId}-webhook-url`}>{i18n.t('api.webhookUrl')}</Label>
232
+ <Input id={`${controlId}-webhook-url`} type="url" bind:value={newWebhookUrl} placeholder="https://api.example.com/hooks" disabled={!credentialProvider || submitting} />
233
+ </div>
234
+ <div class="svadmin-u-6f7e013d6499">
235
+ <Label for={`${controlId}-webhook-event`}>{i18n.t('api.eventType')}</Label>
236
+ <select id={`${controlId}-webhook-event`} bind:value={newWebhookEvent} disabled={!credentialProvider || submitting} class="svadmin-u-60fbb7713999 svadmin-u-e7a768f922d2 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b svadmin-u-fc7473ca09eb svadmin-u-582e6ef4b245 svadmin-u-55d048ebfb1c svadmin-u-608dd26cd5ba svadmin-u-80b9d0ae125f svadmin-u-6b22a22a9752 svadmin-u-b29d8adbad2e">
237
+ <option value="resource.created">resource.created</option>
238
+ <option value="resource.updated">resource.updated</option>
239
+ <option value="resource.deleted">resource.deleted</option>
240
+ <option value="auth.login">auth.login</option>
241
+ </select>
242
+ </div>
243
+ <div class="svadmin-u-60fbb7713999 svadmin-u-6f27f4f79e55">
244
+ <Button type="submit" class="svadmin-u-6da6a3c3f741" disabled={!credentialProvider || submitting || !newWebhookName.trim() || !newWebhookUrl.trim()}><Plus class="svadmin-u-f7b5fa971871" />{i18n.t('api.addWebhook')}</Button>
245
+ </div>
246
+ </form>
225
247
  {#if webhooks.length === 0}<DataState state="empty" title={i18n.t('api.noWebhooks')} description={credentialProvider ? i18n.t('api.webhooksDesc') : (isZh ? '配置 CredentialProvider 后可管理真实 Webhook。' : 'Configure CredentialProvider to manage real webhooks.')} />{:else}<div class="svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3 svadmin-u-b950dda299d3 svadmin-u-18049387f0af">{#each webhooks as hook (hook.id)}<div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-1004c0c3954c svadmin-u-cb11fec3bb46 svadmin-u-020ba687fa12 svadmin-u-9f76a62f4f44 svadmin-u-3b9871a0bf93"><div class="svadmin-u-7e0b7cdf1a94"><p class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{hook.name}</p><p class="svadmin-u-f283ea9bea0e svadmin-u-0e65706bcccd svadmin-u-359090c2d529 svadmin-u-bfa603190748">{hook.url}</p></div><div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-1004c0c3954c svadmin-u-1f51d781e606"><span class="svadmin-u-07389a777c1f svadmin-u-2ef11f1cb219 svadmin-u-d5eab218aa34 svadmin-u-660d2effb880 svadmin-u-0e65706bcccd svadmin-u-359090c2d529 svadmin-u-bfa603190748">{hook.eventType}</span><Button variant="ghost" size="icon-sm" onclick={() => deleteWebhook(hook.id)} aria-label={i18n.t('common.delete') + ' ' + hook.name}><Trash2 class="svadmin-u-f7b5fa971871" /></Button></div></div>{/each}</div>{/if}
226
248
  </SettingsGroup>
227
249
  </div>
228
250
  {/snippet}
229
251
  {#snippet secondary()}
230
252
  <SettingsGroup title={i18n.t('api.generateTitle')} description={i18n.t('api.generateDesc')}>
231
- <form onsubmit={handleGenerateKey} class="svadmin-u-b43b4c086d9a"><div class="svadmin-u-6f7e013d6499"><Label for="api-key-name">{i18n.t('api.keyName')}</Label><Input id="api-key-name" placeholder="CI deployment token" bind:value={newKeyName} required disabled={!credentialProvider || submitting} /></div><fieldset class="svadmin-u-6ed543e2fbbb" disabled={!credentialProvider || submitting}><legend class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{i18n.t('api.permissions')}</legend>{#each Object.keys(newKeyPermissions) as permission (permission)}<div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4"><Checkbox id={`perm-${permission}`} bind:checked={newKeyPermissions[permission as keyof typeof newKeyPermissions]} /><Label for={`perm-${permission}`} class="svadmin-u-34516836730d svadmin-u-fc7473ca09eb svadmin-u-8ecebc9f80e6">{permission}</Label></div>{/each}</fieldset><Button type="submit" class="svadmin-u-6da6a3c3f741" disabled={!credentialProvider || submitting || !newKeyName.trim()}>{i18n.t('api.generateButton')}</Button></form>
253
+ <form onsubmit={handleGenerateKey} class="svadmin-u-b43b4c086d9a">
254
+ <div class="svadmin-u-6f7e013d6499">
255
+ <Label for={`${controlId}-api-key-name`}>{i18n.t('api.keyName')}</Label>
256
+ <Input id={`${controlId}-api-key-name`} placeholder="CI deployment token" bind:value={newKeyName} required disabled={!credentialProvider || submitting} />
257
+ </div>
258
+ <fieldset class="svadmin-u-6ed543e2fbbb" disabled={!credentialProvider || submitting}>
259
+ <legend class="svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{i18n.t('api.permissions')}</legend>
260
+ {#each Object.keys(newKeyPermissions) as permission (permission)}
261
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
262
+ <Checkbox id={`${controlId}-perm-${permission}`} bind:checked={newKeyPermissions[permission as keyof typeof newKeyPermissions]} />
263
+ <Label for={`${controlId}-perm-${permission}`} class="svadmin-u-34516836730d svadmin-u-fc7473ca09eb svadmin-u-8ecebc9f80e6">{permission}</Label>
264
+ </div>
265
+ {/each}
266
+ </fieldset>
267
+ <Button type="submit" class="svadmin-u-6da6a3c3f741" disabled={!credentialProvider || submitting || !newKeyName.trim()}>{i18n.t('api.generateButton')}</Button>
268
+ </form>
232
269
  </SettingsGroup>
233
270
  {/snippet}
234
271
  </WorkspaceLayout>
@@ -4,7 +4,10 @@
4
4
  import { Check, Monitor, Moon, Sun } from '@lucide/svelte';
5
5
  import SettingsGroup from './content/SettingsGroup.svelte';
6
6
  import SettingsFieldRow from './content/SettingsFieldRow.svelte';
7
+ import ContentPageHeader from './content/ContentPageHeader.svelte';
7
8
 
9
+ let { headingLevel = 'h1' }: { headingLevel?: 'h1' | 'h2' } = $props();
10
+ const controlId = $props.id();
8
11
  const i18n = useTranslation();
9
12
  const themeOptions = [
10
13
  { value: 'light' as const, icon: Sun, label: 'settings.light' },
@@ -17,18 +20,20 @@
17
20
  const availableLocales = i18n.getAvailableLocales();
18
21
  const presets = getColorPresets();
19
22
  const DENSITY_KEY = 'svadmin-sidebar-density';
20
- let density = $state<'compact' | 'standard'>((typeof window !== 'undefined' ? localStorage.getItem(DENSITY_KEY) : null) as 'compact' | 'standard' ?? 'standard');
23
+ const storedDensity = typeof window !== 'undefined' ? localStorage.getItem(DENSITY_KEY) : null;
24
+ let density = $state<'compact' | 'standard'>(storedDensity === 'compact' ? 'compact' : 'standard');
21
25
  const PAGE_SIZE_KEY = 'svadmin-default-page-size';
22
26
  const pageSizeOptions = [10, 20, 50];
23
- let pageSize = $state(typeof window !== 'undefined' ? parseInt(localStorage.getItem(PAGE_SIZE_KEY) ?? '10', 10) : 10);
27
+ const storedPageSize = typeof window !== 'undefined' ? Number(localStorage.getItem(PAGE_SIZE_KEY) ?? '10') : 10;
28
+ let pageSize = $state(pageSizeOptions.includes(storedPageSize) ? storedPageSize : 10);
24
29
  const localeNames: Record<string, string> = { 'zh-CN': '中文(简体)', en: 'English', ja: '日本語', ko: '한국어' };
25
30
  function setDensity(value: 'compact' | 'standard') { density = value; if (typeof window !== 'undefined') { localStorage.setItem(DENSITY_KEY, value); window.dispatchEvent(new CustomEvent('svadmin-density-change', { detail: value })); } }
26
31
  function setPageSize(size: number) { pageSize = size; if (typeof window !== 'undefined') localStorage.setItem(PAGE_SIZE_KEY, String(size)); }
27
32
  </script>
28
33
 
29
34
  <div class="svadmin-u-b3542e058833">
30
- <div><h2 class="svadmin-u-d5c9b0001e7e svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{i18n.t('settings.appearance')}</h2><p class="svadmin-u-b6b02c0ebef6 svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('settings.settingsDescription')}</p></div>
31
- <SettingsGroup title={i18n.t('settings.themeMode')}>
35
+ <ContentPageHeader title={i18n.t('settings.appearance')} description={i18n.t('settings.settingsDescription')} {headingLevel} />
36
+ <SettingsGroup title={i18n.t('settings.themeMode')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
32
37
  <div class="svadmin-u-f3c543ad5fe9 svadmin-u-be2e831be5dd svadmin-u-1004c0c3954c">
33
38
  {#each themeOptions as option (option.value)}
34
39
  {@const active = currentTheme === option.value}
@@ -40,7 +45,7 @@
40
45
  {/each}
41
46
  </div>
42
47
  </SettingsGroup>
43
- <SettingsGroup title={i18n.t('settings.colorAccent')}>
48
+ <SettingsGroup title={i18n.t('settings.colorAccent')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
44
49
  <div class="svadmin-u-60fbb7713999 svadmin-u-1eb5c6df38c1 svadmin-u-1004c0c3954c">
45
50
  {#each presets as preset (preset.name)}
46
51
  {@const active = currentColor === preset.name}
@@ -48,11 +53,32 @@
48
53
  {/each}
49
54
  </div>
50
55
  </SettingsGroup>
51
- <SettingsGroup title={i18n.t('settings.interface')}>
56
+ <SettingsGroup title={i18n.t('settings.interface')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
52
57
  <div class="svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3">
53
- <SettingsFieldRow label={i18n.t('settings.language')} description={localeNames[currentLocale] ?? currentLocale}>{#snippet control()}<select class="svadmin-u-e7a768f922d2 svadmin-u-3713c4c7843d svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0e17f2bd9074 svadmin-u-fc7473ca09eb svadmin-u-582e6ef4b245 svadmin-u-55d048ebfb1c svadmin-u-608dd26cd5ba svadmin-u-80b9d0ae125f svadmin-u-6b22a22a9752" value={currentLocale} onchange={(event) => i18n.setLocale((event.target as HTMLSelectElement).value)}>{#each availableLocales as locale (locale)}<option value={locale}>{localeNames[locale] ?? locale}</option>{/each}</select>{/snippet}</SettingsFieldRow>
54
- <SettingsFieldRow label={i18n.t('settings.sidebarDensity')}>{#snippet control()}<div class="svadmin-u-60fbb7713999 svadmin-u-2cd02d11d1af svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22"><button class="svadmin-u-0e17f2bd9074 svadmin-u-ec0091ee009b svadmin-u-359090c2d529 svadmin-u-2689f3958069 {density === 'compact' ? 'svadmin-u-75b1bec3ea0e svadmin-u-30ca335ae9c2' : 'svadmin-u-e6f9e383a762 svadmin-u-bfa603190748 svadmin-u-0557b88819cd'}" onclick={() => setDensity('compact')}>{i18n.t('settings.compact')}</button><button class="svadmin-u-0e17f2bd9074 svadmin-u-ec0091ee009b svadmin-u-359090c2d529 svadmin-u-2689f3958069 {density === 'standard' ? 'svadmin-u-75b1bec3ea0e svadmin-u-30ca335ae9c2' : 'svadmin-u-e6f9e383a762 svadmin-u-bfa603190748 svadmin-u-0557b88819cd'}" onclick={() => setDensity('standard')}>{i18n.t('settings.standard')}</button></div>{/snippet}</SettingsFieldRow>
55
- <SettingsFieldRow label={i18n.t('settings.defaultPageSize')}>{#snippet control()}<div class="svadmin-u-60fbb7713999 svadmin-u-2cd02d11d1af svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22">{#each pageSizeOptions as size (size)}<button class="svadmin-u-0e17f2bd9074 svadmin-u-ec0091ee009b svadmin-u-0e65706bcccd svadmin-u-359090c2d529 svadmin-u-2689f3958069 {pageSize === size ? 'svadmin-u-75b1bec3ea0e svadmin-u-30ca335ae9c2' : 'svadmin-u-e6f9e383a762 svadmin-u-bfa603190748 svadmin-u-0557b88819cd'}" onclick={() => setPageSize(size)}>{size}</button>{/each}</div>{/snippet}</SettingsFieldRow>
58
+ <SettingsFieldRow label={i18n.t('settings.language')} description={localeNames[currentLocale] ?? currentLocale} controlId={`${controlId}-language`}>
59
+ {#snippet control()}
60
+ <select id={`${controlId}-language`} class="svadmin-u-e7a768f922d2 svadmin-u-3713c4c7843d svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22 svadmin-u-e6f9e383a762 svadmin-u-0e17f2bd9074 svadmin-u-fc7473ca09eb svadmin-u-582e6ef4b245 svadmin-u-55d048ebfb1c svadmin-u-608dd26cd5ba svadmin-u-80b9d0ae125f svadmin-u-6b22a22a9752" value={currentLocale} onchange={(event) => i18n.setLocale((event.target as HTMLSelectElement).value)}>
61
+ {#each availableLocales as locale (locale)}<option value={locale}>{localeNames[locale] ?? locale}</option>{/each}
62
+ </select>
63
+ {/snippet}
64
+ </SettingsFieldRow>
65
+ <SettingsFieldRow label={i18n.t('settings.sidebarDensity')}>
66
+ {#snippet control()}
67
+ <div class="svadmin-u-60fbb7713999 svadmin-u-2cd02d11d1af svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22">
68
+ <button type="button" aria-pressed={density === 'compact'} class="svadmin-u-0e17f2bd9074 svadmin-u-ec0091ee009b svadmin-u-359090c2d529 svadmin-u-2689f3958069 {density === 'compact' ? 'svadmin-u-75b1bec3ea0e svadmin-u-30ca335ae9c2' : 'svadmin-u-e6f9e383a762 svadmin-u-bfa603190748 svadmin-u-0557b88819cd'}" onclick={() => setDensity('compact')}>{i18n.t('settings.compact')}</button>
69
+ <button type="button" aria-pressed={density === 'standard'} class="svadmin-u-0e17f2bd9074 svadmin-u-ec0091ee009b svadmin-u-359090c2d529 svadmin-u-2689f3958069 {density === 'standard' ? 'svadmin-u-75b1bec3ea0e svadmin-u-30ca335ae9c2' : 'svadmin-u-e6f9e383a762 svadmin-u-bfa603190748 svadmin-u-0557b88819cd'}" onclick={() => setDensity('standard')}>{i18n.t('settings.standard')}</button>
70
+ </div>
71
+ {/snippet}
72
+ </SettingsFieldRow>
73
+ <SettingsFieldRow label={i18n.t('settings.defaultPageSize')}>
74
+ {#snippet control()}
75
+ <div class="svadmin-u-60fbb7713999 svadmin-u-2cd02d11d1af svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e5795dad4d22">
76
+ {#each pageSizeOptions as size (size)}
77
+ <button type="button" aria-pressed={pageSize === size} class="svadmin-u-0e17f2bd9074 svadmin-u-ec0091ee009b svadmin-u-0e65706bcccd svadmin-u-359090c2d529 svadmin-u-2689f3958069 {pageSize === size ? 'svadmin-u-75b1bec3ea0e svadmin-u-30ca335ae9c2' : 'svadmin-u-e6f9e383a762 svadmin-u-bfa603190748 svadmin-u-0557b88819cd'}" onclick={() => setPageSize(size)}>{size}</button>
78
+ {/each}
79
+ </div>
80
+ {/snippet}
81
+ </SettingsFieldRow>
56
82
  </div>
57
83
  </SettingsGroup>
58
84
  </div>
@@ -1,3 +1,6 @@
1
- declare const AppearanceSettings: import("svelte").Component<Record<string, never>, {}, "">;
1
+ type $$ComponentProps = {
2
+ headingLevel?: 'h1' | 'h2';
3
+ };
4
+ declare const AppearanceSettings: import("svelte").Component<$$ComponentProps, {}, "">;
2
5
  type AppearanceSettings = ReturnType<typeof AppearanceSettings>;
3
6
  export default AppearanceSettings;
@@ -123,6 +123,8 @@
123
123
  // ─── Props with Snippet composability ─────────────────────────
124
124
  interface Props {
125
125
  resourceName: string;
126
+ /** 嵌入仪表盘的多个表格应关闭 URL 同步,避免互相覆盖筛选与分页。 */
127
+ syncWithLocation?: boolean;
126
128
  rendering?: ResourceRendering | undefined;
127
129
  /** 可选主体渲染器:不替换资源、权限、工具栏和持久化逻辑。 */
128
130
  gridBody?: Snippet<[AutoTableGridState]>;
@@ -170,6 +172,7 @@
170
172
 
171
173
  let {
172
174
  resourceName,
175
+ syncWithLocation = true,
173
176
  rendering,
174
177
  gridBody,
175
178
  selectable = true,
@@ -254,7 +257,7 @@
254
257
  }
255
258
 
256
259
  // ─── URL state + server-side state ────────────────────────────
257
- const urlState = readURLState(adminContext);
260
+ const urlState: ReturnType<typeof readURLState> = untrack(() => syncWithLocation ? readURLState(adminContext) : {});
258
261
 
259
262
  const savedViewColumnIds = $derived(new Set([
260
263
  ...resource.fields.map((field) => field.key),
@@ -504,9 +507,8 @@
504
507
  }
505
508
  return result;
506
509
  });
507
-
508
- // URL sync
509
510
  $effect(() => {
511
+ if (!syncWithLocation) return;
510
512
  writeURLState({
511
513
  page: pagination.current,
512
514
  pageSize: pagination.pageSize,
@@ -1448,7 +1450,9 @@
1448
1450
  && left.canDelete === right.canDelete
1449
1451
  && left.variables === right.variables;
1450
1452
  }
1453
+ let localDetailId = $state<string | number>();
1451
1454
  const detailState = $derived.by(() => {
1455
+ if (!syncWithLocation) return { id: canRead ? localDetailId : undefined, invalid: false };
1452
1456
  const route = parsed.params['detail'];
1453
1457
  if (route === undefined || !canRead) return { id: undefined, invalid: false };
1454
1458
  try { return { id: parseContractRouteId(binding.resource, route), invalid: false }; }
@@ -1470,6 +1474,7 @@
1470
1474
  rowSelection = {};
1471
1475
  expandedAtom.set({});
1472
1476
  detailOpenedInHistory = false;
1477
+ localDetailId = undefined;
1473
1478
  quickEditId = undefined;
1474
1479
  if (searchDebounceTimer) clearTimeout(searchDebounceTimer);
1475
1480
  searchDebounceTimer = undefined;
@@ -1563,14 +1568,14 @@
1563
1568
  await tick();
1564
1569
  }
1565
1570
  }
1566
-
1567
1571
  function openDetail(id: string | number): void {
1568
1572
  if (!canRead || !canShow) return;
1573
+ if (!syncWithLocation) { localDetailId = id; return; }
1569
1574
  detailOpenedInHistory = true;
1570
1575
  writeURLState({ detailId: formatContractRouteId(binding.resource, id) }, adminContext, 'push');
1571
1576
  }
1572
-
1573
1577
  function closeDetail(): void {
1578
+ if (!syncWithLocation) { localDetailId = undefined; return; }
1574
1579
  if (detailOpenedInHistory && adminContext.routerProvider) {
1575
1580
  detailOpenedInHistory = false;
1576
1581
  adminContext.routerProvider.back();
@@ -1579,7 +1584,6 @@
1579
1584
  detailOpenedInHistory = false;
1580
1585
  writeURLState({ detailId: undefined }, adminContext);
1581
1586
  }
1582
-
1583
1587
  // ─── CSV Export ───────────────────────────────────────────────
1584
1588
  function exportCSV() {
1585
1589
  if (!canRead || !canExport || !pageRecords.ok || query.isError || query.isFetching || !pageRecords.data.length) return;
@@ -7,6 +7,8 @@ import { type SavedListViewProvider } from './saved-list-views.js';
7
7
  import type { Snippet } from 'svelte';
8
8
  interface Props {
9
9
  resourceName: string;
10
+ /** 嵌入仪表盘的多个表格应关闭 URL 同步,避免互相覆盖筛选与分页。 */
11
+ syncWithLocation?: boolean;
10
12
  rendering?: ResourceRendering | undefined;
11
13
  /** 可选主体渲染器:不替换资源、权限、工具栏和持久化逻辑。 */
12
14
  gridBody?: Snippet<[AutoTableGridState]>;
@@ -6,6 +6,7 @@
6
6
  import SettingsGroup from './content/SettingsGroup.svelte';
7
7
  import SettingsFieldRow from './content/SettingsFieldRow.svelte';
8
8
  import StatusBadge from './content/StatusBadge.svelte';
9
+ import ContentPageHeader from './content/ContentPageHeader.svelte';
9
10
 
10
11
  const i18n = useTranslation();
11
12
 
@@ -18,11 +19,12 @@
18
19
  }
19
20
 
20
21
  interface Props {
22
+ headingLevel?: 'h1' | 'h2';
21
23
  integrations?: Integration[];
22
24
  onConnectionChange?: (id: string, connected: boolean) => void | Promise<void>;
23
25
  }
24
26
 
25
- let { integrations, onConnectionChange }: Props = $props();
27
+ let { integrations, onConnectionChange, headingLevel = 'h1' }: Props = $props();
26
28
  const defaultIntegrations = $derived<Integration[]>([
27
29
  { id: 'source-control', name: i18n.t('integrations.sourceControl'), description: i18n.t('integrations.sourceControlDescription'), Icon: FolderGit },
28
30
  { id: 'chatops', name: 'ChatOps', description: i18n.t('integrations.chatOpsDescription'), Icon: MessageCircle },
@@ -33,8 +35,8 @@
33
35
  </script>
34
36
 
35
37
  <div class="svadmin-u-b3542e058833">
36
- <div><h2 class="svadmin-u-d5c9b0001e7e svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{i18n.t('settings.integrations')}</h2><p class="svadmin-u-b6b02c0ebef6 svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('settings.integrationsDescription')}</p></div>
37
- <SettingsGroup title={i18n.t('settings.integrations')} description={i18n.t('integrations.addMoreHint')}>
38
+ <ContentPageHeader title={i18n.t('settings.integrations')} description={i18n.t('settings.integrationsDescription')} {headingLevel} />
39
+ <SettingsGroup title={i18n.t('settings.integrations')} description={i18n.t('integrations.addMoreHint')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
38
40
  <div class="svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3">
39
41
  {#each displayedIntegrations as integration (integration.id)}
40
42
  <SettingsFieldRow label={integration.name} description={integration.description}>
@@ -7,6 +7,7 @@ interface Integration {
7
7
  Icon: Component;
8
8
  }
9
9
  interface Props {
10
+ headingLevel?: 'h1' | 'h2';
10
11
  integrations?: Integration[];
11
12
  onConnectionChange?: (id: string, connected: boolean) => void | Promise<void>;
12
13
  }
@@ -20,8 +20,10 @@
20
20
  import SettingsFieldRow from './content/SettingsFieldRow.svelte';
21
21
  import DataState from './content/DataState.svelte';
22
22
  import FeedbackNotice from './content/FeedbackNotice.svelte';
23
+ import ContentPageHeader from './content/ContentPageHeader.svelte';
23
24
 
24
- let { preferencesProvider }: { preferencesProvider?: NotificationPreferencesProvider } = $props();
25
+ let { preferencesProvider, headingLevel = 'h1' }: { preferencesProvider?: NotificationPreferencesProvider; headingLevel?: 'h1' | 'h2' } = $props();
26
+ const controlId = $props.id();
25
27
  const i18n = useTranslation();
26
28
  const context = captureAdminContext();
27
29
  const notification = useNotification();
@@ -72,6 +74,11 @@
72
74
  </script>
73
75
 
74
76
  <div class="svadmin-u-b3542e058833">
77
+ <ContentPageHeader title={i18n.t('settings.notifications')} description={i18n.t('settings.notificationsDescription')} {headingLevel}>
78
+ {#snippet actions()}
79
+ <Button disabled={!loaded || loading || saving} onclick={saveSettings}>{i18n.t('common.save')}</Button>
80
+ {/snippet}
81
+ </ContentPageHeader>
75
82
  {#if preferencesProvider?.description}
76
83
  <p role="note">{preferencesProvider.description}</p>
77
84
  {/if}
@@ -81,23 +88,22 @@
81
88
  {#if loading}<DataState state="loading" />{/if}
82
89
  {#if error}<p role="alert">{error}</p>{#if !loaded}<Button onclick={load}>{i18n.t('common.retry')}</Button>{/if}{/if}
83
90
  <fieldset disabled={!loaded || loading || saving} class="svadmin-u-b3542e058833">
84
- <div class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-8ef2268efbbc svadmin-u-1004c0c3954c svadmin-u-020ba687fa12 svadmin-u-64cac80d2e9a"><div><h2 class="svadmin-u-d5c9b0001e7e svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{i18n.t('settings.notifications')}</h2><p class="svadmin-u-b6b02c0ebef6 svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('settings.notificationsDescription')}</p></div><Button onclick={saveSettings}>{i18n.t('common.save')}</Button></div>
85
- <SettingsGroup title={i18n.t('notifications.emailTitle')} description={i18n.t('notifications.emailDescription')}>
91
+ <SettingsGroup title={i18n.t('notifications.emailTitle')} description={i18n.t('notifications.emailDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
86
92
  <div class="svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3">
87
- <SettingsFieldRow label={i18n.t('notifications.securityAlerts')} description={i18n.t('notifications.securityAlertsDesc')}>{#snippet control()}<Switch id="email-security" bind:checked={emailAlerts.security} />{/snippet}</SettingsFieldRow>
88
- <SettingsFieldRow label={i18n.t('notifications.activityLogs')} description={i18n.t('notifications.activityLogsDesc')}>{#snippet control()}<Switch id="email-activity" bind:checked={emailAlerts.activity} />{/snippet}</SettingsFieldRow>
89
- <SettingsFieldRow label={i18n.t('notifications.systemAlerts')} description={i18n.t('notifications.systemAlertsDesc')}>{#snippet control()}<Switch id="email-reports" bind:checked={emailAlerts.reports} />{/snippet}</SettingsFieldRow>
93
+ <SettingsFieldRow controlId={`${controlId}-email-security`} label={i18n.t('notifications.securityAlerts')} description={i18n.t('notifications.securityAlertsDesc')}>{#snippet control()}<Switch id={`${controlId}-email-security`} bind:checked={emailAlerts.security} />{/snippet}</SettingsFieldRow>
94
+ <SettingsFieldRow controlId={`${controlId}-email-activity`} label={i18n.t('notifications.activityLogs')} description={i18n.t('notifications.activityLogsDesc')}>{#snippet control()}<Switch id={`${controlId}-email-activity`} bind:checked={emailAlerts.activity} />{/snippet}</SettingsFieldRow>
95
+ <SettingsFieldRow controlId={`${controlId}-email-reports`} label={i18n.t('notifications.systemAlerts')} description={i18n.t('notifications.systemAlertsDesc')}>{#snippet control()}<Switch id={`${controlId}-email-reports`} bind:checked={emailAlerts.reports} />{/snippet}</SettingsFieldRow>
90
96
  </div>
91
97
  </SettingsGroup>
92
- <SettingsGroup title={i18n.t('notifications.pushTitle')} description={i18n.t('notifications.pushDescription')}>
98
+ <SettingsGroup title={i18n.t('notifications.pushTitle')} description={i18n.t('notifications.pushDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
93
99
  <div class="svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3">
94
- <SettingsFieldRow label={i18n.t('notifications.securityAlerts')} description={i18n.t('notifications.securityPushDesc')}>{#snippet control()}<Switch id="push-security" bind:checked={pushAlerts.security} />{/snippet}</SettingsFieldRow>
95
- <SettingsFieldRow label={i18n.t('notifications.activityLogs')} description={i18n.t('notifications.activityPushDesc')}>{#snippet control()}<Switch id="push-activity" bind:checked={pushAlerts.activity} />{/snippet}</SettingsFieldRow>
96
- <SettingsFieldRow label={i18n.t('notifications.systemAlerts')} description={i18n.t('notifications.systemPushDesc')}>{#snippet control()}<Switch id="push-reports" bind:checked={pushAlerts.reports} />{/snippet}</SettingsFieldRow>
100
+ <SettingsFieldRow controlId={`${controlId}-push-security`} label={i18n.t('notifications.securityAlerts')} description={i18n.t('notifications.securityPushDesc')}>{#snippet control()}<Switch id={`${controlId}-push-security`} bind:checked={pushAlerts.security} />{/snippet}</SettingsFieldRow>
101
+ <SettingsFieldRow controlId={`${controlId}-push-activity`} label={i18n.t('notifications.activityLogs')} description={i18n.t('notifications.activityPushDesc')}>{#snippet control()}<Switch id={`${controlId}-push-activity`} bind:checked={pushAlerts.activity} />{/snippet}</SettingsFieldRow>
102
+ <SettingsFieldRow controlId={`${controlId}-push-reports`} label={i18n.t('notifications.systemAlerts')} description={i18n.t('notifications.systemPushDesc')}>{#snippet control()}<Switch id={`${controlId}-push-reports`} bind:checked={pushAlerts.reports} />{/snippet}</SettingsFieldRow>
97
103
  </div>
98
104
  </SettingsGroup>
99
- <SettingsGroup title={i18n.t('notifications.smsTitle')} description={i18n.t('notifications.smsDescription')}>
100
- <SettingsFieldRow label={i18n.t('notifications.criticalSecurity')} description={i18n.t('notifications.criticalSecurityDesc')}>{#snippet control()}<Switch id="sms-security" bind:checked={smsAlerts.security} />{/snippet}</SettingsFieldRow>
105
+ <SettingsGroup title={i18n.t('notifications.smsTitle')} description={i18n.t('notifications.smsDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
106
+ <SettingsFieldRow controlId={`${controlId}-sms-security`} label={i18n.t('notifications.criticalSecurity')} description={i18n.t('notifications.criticalSecurityDesc')}>{#snippet control()}<Switch id={`${controlId}-sms-security`} bind:checked={smsAlerts.security} />{/snippet}</SettingsFieldRow>
101
107
  </SettingsGroup>
102
108
  </fieldset>
103
109
  </div>
@@ -21,6 +21,7 @@ export interface NotificationPreferencesProvider {
21
21
  }
22
22
  type $$ComponentProps = {
23
23
  preferencesProvider?: NotificationPreferencesProvider;
24
+ headingLevel?: 'h1' | 'h2';
24
25
  };
25
26
  declare const NotificationsSettings: import("svelte").Component<$$ComponentProps, {}, "">;
26
27
  type NotificationsSettings = ReturnType<typeof NotificationsSettings>;
@@ -13,8 +13,10 @@
13
13
  import * as Card from './ui/card/index.js';
14
14
  import * as Alert from './ui/alert/index.js';
15
15
  import PasswordInput from './PasswordInput.svelte';
16
+ import ContentPageHeader from './content/ContentPageHeader.svelte';
16
17
  import { User, Mail, Lock, Loader2, AlertCircle, Camera } from '@lucide/svelte';
17
18
 
19
+ let { headingLevel = 'h1' }: { headingLevel?: 'h1' | 'h2' } = $props();
18
20
  const i18n = useTranslation();
19
21
  const adminContext = captureAdminContext();
20
22
 
@@ -150,9 +152,7 @@
150
152
  </script>
151
153
 
152
154
  <div class="svadmin-u-b3542e058833">
153
- <div>
154
- <h2 class="svadmin-u-d5c9b0001e7e svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{i18n.t('profile.title')}</h2>
155
- </div>
155
+ <ContentPageHeader title={i18n.t('profile.title')} {headingLevel} />
156
156
 
157
157
  <!-- Profile Info -->
158
158
  <Card.Card>
@@ -1,3 +1,6 @@
1
- declare const ProfilePage: import("svelte").Component<Record<string, never>, {}, "">;
1
+ type $$ComponentProps = {
2
+ headingLevel?: 'h1' | 'h2';
3
+ };
4
+ declare const ProfilePage: import("svelte").Component<$$ComponentProps, {}, "">;
2
5
  type ProfilePage = ReturnType<typeof ProfilePage>;
3
6
  export default ProfilePage;
@@ -0,0 +1,36 @@
1
+ <script lang="ts">
2
+ import { captureAdminContext } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+ import ContentPageShell from './content/ContentPageShell.svelte';
5
+ import DataState from './content/DataState.svelte';
6
+ import ResourceOverviewCard from './ResourceOverviewCard.svelte';
7
+
8
+ let { title }: { title?: string } = $props();
9
+ const context = captureAdminContext();
10
+ const i18n = useTranslation();
11
+ const resources = $derived(context.resources
12
+ .filter(resource => resource.showInMenu !== false && !resource.parentName)
13
+ .sort((left, right) => (left.menuOrder ?? 0) - (right.menuOrder ?? 0)));
14
+ </script>
15
+
16
+ <ContentPageShell title={title ?? i18n.t('common.dashboard')} pageId="resource-overview" width="wide">
17
+ {#if resources.length}
18
+ <div class="svadmin-resource-overview-grid">
19
+ {#each resources as resource (resource.identifier ?? resource.name)}
20
+ <ResourceOverviewCard {resource} />
21
+ {/each}
22
+ </div>
23
+ {:else}
24
+ <DataState state="empty" />
25
+ {/if}
26
+ </ContentPageShell>
27
+
28
+ <style>
29
+ .svadmin-resource-overview-grid {
30
+ display: grid;
31
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr));
32
+ align-items: start;
33
+ gap: 1rem;
34
+ min-width: 0;
35
+ }
36
+ </style>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ title?: string;
3
+ };
4
+ declare const ResourceOverview: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type ResourceOverview = ReturnType<typeof ResourceOverview>;
6
+ export default ResourceOverview;
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import { useCan, useNavigation, type ResourceDefinition } from '@svadmin/core';
3
+ import { useTranslation } from '@svadmin/core/i18n';
4
+ import { ArrowRight, Plus } from '@lucide/svelte';
5
+ import { Button } from './ui/button/index.js';
6
+ import DataState from './content/DataState.svelte';
7
+
8
+ let { resource }: { resource: ResourceDefinition } = $props();
9
+ const i18n = useTranslation();
10
+ const navigation = useNavigation();
11
+ const read = useCan(() => ({ resource: resource.name, action: 'list' }));
12
+ const create = useCan(() => ({ resource: resource.name, action: 'create' }));
13
+ </script>
14
+
15
+ <section class="svadmin-resource-overview-card" aria-label={resource.label}>
16
+ <h2>{resource.label}</h2>
17
+ {#if read.isLoading}
18
+ <DataState state="loading" />
19
+ {:else if !read.allowed}
20
+ <DataState state="forbidden" />
21
+ {:else}
22
+ <div class="svadmin-resource-overview-actions">
23
+ <Button variant="outline" onclick={() => navigation.list(resource.name)}>
24
+ {i18n.t('profileSections.viewAll')}<ArrowRight aria-hidden="true" />
25
+ </Button>
26
+ {#if resource.canCreate !== false && !create.isLoading && create.allowed}
27
+ <Button onclick={() => navigation.create(resource.name)}>
28
+ <Plus aria-hidden="true" />{i18n.t('common.create')}
29
+ </Button>
30
+ {/if}
31
+ </div>
32
+ {/if}
33
+ </section>
34
+
35
+ <style>
36
+ .svadmin-resource-overview-card {
37
+ display: grid;
38
+ align-content: start;
39
+ gap: 1rem;
40
+ min-width: 0;
41
+ padding: 1rem;
42
+ border: 1px solid var(--border);
43
+ border-radius: var(--radius);
44
+ background: var(--card);
45
+ color: var(--card-foreground);
46
+ box-shadow: var(--shadow-surface);
47
+ }
48
+ h2 { margin: 0; font-size: 1rem; font-weight: 600; overflow-wrap: anywhere; }
49
+ .svadmin-resource-overview-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; }
50
+ </style>
@@ -0,0 +1,7 @@
1
+ import { type ResourceDefinition } from '@svadmin/core';
2
+ type $$ComponentProps = {
3
+ resource: ResourceDefinition;
4
+ };
5
+ declare const ResourceOverviewCard: import("svelte").Component<$$ComponentProps, {}, "">;
6
+ type ResourceOverviewCard = ReturnType<typeof ResourceOverviewCard>;
7
+ export default ResourceOverviewCard;