@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
@@ -11,7 +11,10 @@
11
11
  import DataState from './content/DataState.svelte';
12
12
  import FeedbackNotice from './content/FeedbackNotice.svelte';
13
13
  import ConfirmDialog from './ConfirmDialog.svelte';
14
+ import ContentPageHeader from './content/ContentPageHeader.svelte';
14
15
 
16
+ let { headingLevel = 'h1' }: { headingLevel?: 'h1' | 'h2' } = $props();
17
+ const controlId = $props.id();
15
18
  const i18n = useTranslation();
16
19
  const adminContext = captureAdminContext();
17
20
  const authProvider = $derived(adminContext.authProvider);
@@ -179,16 +182,16 @@
179
182
  </script>
180
183
 
181
184
  <div class="svadmin-u-b3542e058833">
182
- <div><h2 class="svadmin-u-d5c9b0001e7e svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">{i18n.t('settings.security')}</h2><p class="svadmin-u-b6b02c0ebef6 svadmin-u-fc7473ca09eb svadmin-u-bfa603190748">{i18n.t('settings.securityDescription')}</p></div>
185
+ <ContentPageHeader title={i18n.t('settings.security')} description={i18n.t('settings.securityDescription')} {headingLevel} />
183
186
  {#if !sessionProvider}
184
187
  <FeedbackNotice tone="warning" message={isZh ? '未配置 SessionProvider。会话和 MFA 设置不会使用模拟数据。' : 'SessionProvider is not configured. Session and MFA controls do not use simulated data.'} />
185
188
  {/if}
186
- <SettingsGroup title={i18n.t('security.twoFactorAuth')} description={i18n.t('security.twoFactorDescription')}>
187
- <SettingsFieldRow label={i18n.t('security.enable2fa')} description={is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}>
188
- {#snippet control()}<Switch bind:checked={() => is2faEnabled, (enabled) => { void handleMfaChange(enabled); }} disabled={!sessionProvider?.setMfaEnabled || updatingMfa} />{/snippet}
189
+ <SettingsGroup title={i18n.t('security.twoFactorAuth')} description={i18n.t('security.twoFactorDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
190
+ <SettingsFieldRow controlId={`${controlId}-mfa`} label={i18n.t('security.enable2fa')} description={is2faEnabled ? i18n.t('security.twoFactorActive') : i18n.t('security.twoFactorInactive')}>
191
+ {#snippet control()}<Switch id={`${controlId}-mfa`} bind:checked={() => is2faEnabled, (enabled) => { void handleMfaChange(enabled); }} disabled={!sessionProvider?.setMfaEnabled || updatingMfa} />{/snippet}
189
192
  </SettingsFieldRow>
190
193
  </SettingsGroup>
191
- <SettingsGroup title={i18n.t('profile.changePassword')} description={isZh ? '密码修改由 AuthProvider 持久化。' : 'Password changes are persisted by AuthProvider.'}>
194
+ <SettingsGroup title={i18n.t('profile.changePassword')} description={isZh ? '密码修改由 AuthProvider 持久化。' : 'Password changes are persisted by AuthProvider.'} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
192
195
  {#if !authProvider?.updatePassword}<FeedbackNotice tone="warning" message={isZh ? '当前 AuthProvider 不支持修改密码。' : 'The current AuthProvider does not support password updates.'} />{/if}
193
196
  <form onsubmit={handlePasswordChange} class="svadmin-u-6199866f612f svadmin-u-3e7ce58d64fa">
194
197
  {#if passwordErrorMessage}<Alert.Root variant="destructive"><AlertTriangle class="svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3" /><Alert.Description>{passwordErrorMessage}</Alert.Description></Alert.Root>{/if}
@@ -198,7 +201,7 @@
198
201
  <Button type="submit" disabled={!authProvider?.updatePassword || changingPassword}>{changingPassword ? (isZh ? '更新中...' : 'Updating...') : i18n.t('profile.updatePassword')}</Button>
199
202
  </form>
200
203
  </SettingsGroup>
201
- <SettingsGroup title={i18n.t('security.activeSessions')} description={i18n.t('security.sessionsDescription')}>
204
+ <SettingsGroup title={i18n.t('security.activeSessions')} description={i18n.t('security.sessionsDescription')} headingLevel={headingLevel === 'h1' ? 'h2' : 'h3'}>
202
205
  {#snippet actions()}{#if sessions.some((session) => !session.current)}<Button variant="outline" size="sm" disabled={revoking !== null} onclick={() => revokeAllOthers()}>{i18n.t('security.revokeOthers')}</Button>{/if}{/snippet}
203
206
  {#if loadingSessions}
204
207
  <DataState state="loading" />
@@ -1,3 +1,6 @@
1
- declare const SecuritySettings: import("svelte").Component<Record<string, never>, {}, "">;
1
+ type $$ComponentProps = {
2
+ headingLevel?: 'h1' | 'h2';
3
+ };
4
+ declare const SecuritySettings: import("svelte").Component<$$ComponentProps, {}, "">;
2
5
  type SecuritySettings = ReturnType<typeof SecuritySettings>;
3
6
  export default SecuritySettings;
@@ -25,6 +25,7 @@
25
25
  import FeedbackNotice from '../content/FeedbackNotice.svelte';
26
26
 
27
27
  const i18n = useTranslation();
28
+ const controlId = $props.id();
28
29
  const adminContext = captureAdminContext();
29
30
  const identityProvider = $derived(adminContext.identityGovernanceProvider);
30
31
  const organizationProvider = $derived(adminContext.organizationProvider);
@@ -50,16 +51,12 @@
50
51
  && organizationName.trim()
51
52
  && organizationName.trim() !== organization.name,
52
53
  ));
53
- const policyNumbersValid = $derived.by(() => {
54
- const sessionTimeout = Number(sessionTimeoutValue);
55
- const auditRetention = Number(auditRetentionValue);
56
- return /^\d+$/.test(sessionTimeoutValue)
57
- && /^\d+$/.test(auditRetentionValue)
58
- && Number.isSafeInteger(sessionTimeout)
59
- && Number.isSafeInteger(auditRetention)
60
- && sessionTimeout > 0
61
- && auditRetention > 0;
62
- });
54
+ function isPositiveInteger(value: string): boolean {
55
+ return /^\d+$/.test(value) && Number.isSafeInteger(Number(value)) && Number(value) > 0;
56
+ }
57
+ const sessionTimeoutValid = $derived(isPositiveInteger(sessionTimeoutValue));
58
+ const auditRetentionValid = $derived(isPositiveInteger(auditRetentionValue));
59
+ const policyNumbersValid = $derived(sessionTimeoutValid && auditRetentionValid);
63
60
 
64
61
  function message(error: unknown): string {
65
62
  return error instanceof Error ? error.message : String(error);
@@ -202,11 +199,11 @@
202
199
  </script>
203
200
 
204
201
  {#snippet auditControl()}
205
- <Switch checked={policy?.auditLoggingEnabled ?? false} disabled={!policy || saving} aria-label={i18n.t('account.auditLogging')} onCheckedChange={(checked) => { if (policy) policy = { ...policy, auditLoggingEnabled: checked }; }} />
202
+ <Switch id={`${controlId}-audit`} checked={policy?.auditLoggingEnabled ?? false} disabled={!policy || saving} onCheckedChange={(checked) => { if (policy) policy = { ...policy, auditLoggingEnabled: checked }; }} />
206
203
  {/snippet}
207
204
 
208
205
  {#snippet ssoControl()}
209
- <Switch checked={policy?.requireSso ?? false} disabled={!policy || saving} aria-label={i18n.t('account.enableSso')} onCheckedChange={(checked) => { if (policy) policy = { ...policy, requireSso: checked }; }} />
206
+ <Switch id={`${controlId}-sso`} checked={policy?.requireSso ?? false} disabled={!policy || saving} onCheckedChange={(checked) => { if (policy) policy = { ...policy, requireSso: checked }; }} />
210
207
  {/snippet}
211
208
 
212
209
  <ContentPageShell pageId="account-settings-enterprise" width="wide">
@@ -222,19 +219,22 @@
222
219
  <div class="svadmin-u-f3c543ad5fe9 svadmin-u-60541e1e26f8 svadmin-u-0c3bc98565dd svadmin-u-2f27a80ed92b">
223
220
  <SettingsGroup title={i18n.t('account.securityPolicy')} description={isZh ? '配置组织级身份、会话与审计策略。' : 'Configure organization-wide identity, session, and audit controls.'} bodyClass="space-y-4">
224
221
  {#if organizationProvider}
225
- <div class="svadmin-u-6f7e013d6499"><Label for="organization-name">{isZh ? '组织名称' : 'Organization name'}</Label><div class="svadmin-u-60fbb7713999 svadmin-u-77a2a20e90d4"><Input id="organization-name" bind:value={organizationName} disabled={!organizationProvider.updateCurrentOrganization || savingOrganization} /><Button variant="outline" size="sm" disabled={!organizationChanged || savingOrganization} onclick={saveOrganization}>{savingOrganization ? (isZh ? '保存中...' : 'Saving...') : i18n.t('common.save')}</Button></div></div>
222
+ <div class="svadmin-u-6f7e013d6499"><Label for={`${controlId}-organization-name`}>{isZh ? '组织名称' : 'Organization name'}</Label><div class="svadmin-u-60fbb7713999 svadmin-u-77a2a20e90d4"><Input id={`${controlId}-organization-name`} bind:value={organizationName} disabled={!organizationProvider.updateCurrentOrganization || savingOrganization} /><Button variant="outline" size="sm" disabled={!organizationChanged || savingOrganization} onclick={saveOrganization}>{savingOrganization ? (isZh ? '保存中...' : 'Saving...') : i18n.t('common.save')}</Button></div></div>
226
223
  {/if}
227
224
  <div class="svadmin-u-f3c543ad5fe9 svadmin-u-0c3bc98565dd svadmin-u-e00ad81645a2">
228
- <div class="svadmin-u-6f7e013d6499"><Label for="session-timeout">{i18n.t('account.sessionTimeout')}</Label><Input id="session-timeout" type="number" min="1" step="1" bind:value={sessionTimeoutValue} disabled={!policy || saving} /></div>
229
- <div class="svadmin-u-6f7e013d6499"><Label for="retention">{i18n.t('account.dataRetention')}</Label><Input id="retention" type="number" min="1" step="1" bind:value={auditRetentionValue} disabled={!policy || saving} /></div>
225
+ <div class="svadmin-u-6f7e013d6499"><Label for={`${controlId}-session-timeout`}>{i18n.t('account.sessionTimeout')}</Label><Input id={`${controlId}-session-timeout`} type="number" min="1" step="1" bind:value={sessionTimeoutValue} disabled={!policy || saving} aria-invalid={policy ? !sessionTimeoutValid : undefined} aria-describedby={policy && !sessionTimeoutValid ? `${controlId}-policy-error` : undefined} /></div>
226
+ <div class="svadmin-u-6f7e013d6499"><Label for={`${controlId}-retention`}>{i18n.t('account.dataRetention')}</Label><Input id={`${controlId}-retention`} type="number" min="1" step="1" bind:value={auditRetentionValue} disabled={!policy || saving} aria-invalid={policy ? !auditRetentionValid : undefined} aria-describedby={policy && !auditRetentionValid ? `${controlId}-policy-error` : undefined} /></div>
230
227
  </div>
231
- {#if policy && !policyNumbersValid}<FeedbackNotice tone="warning" message={isZh ? '会话超时和审计保留天数必须为正整数。' : 'Session timeout and audit retention must be positive integers.'} />{/if}
232
- <SettingsFieldRow label={i18n.t('account.auditLogging')} description={isZh ? '由后端持久化组织级审计策略。' : 'Persist the organization audit policy through the backend provider.'} control={auditControl} separated />
228
+ {#if policy && !policyNumbersValid}<div id={`${controlId}-policy-error`}><FeedbackNotice tone="warning" message={isZh ? '会话超时和审计保留天数必须为正整数。' : 'Session timeout and audit retention must be positive integers.'} /></div>{/if}
229
+ <SettingsFieldRow label={i18n.t('account.auditLogging')} controlId={`${controlId}-audit`} description={isZh ? '由后端持久化组织级审计策略。' : 'Persist the organization audit policy through the backend provider.'} control={auditControl} separated />
233
230
  <Button size="sm" disabled={!identityProvider || !policy || !policyNumbersValid || saving} onclick={saveSettings}>{saving ? (isZh ? '保存中...' : 'Saving...') : i18n.t('common.save')}</Button>
234
231
  </SettingsGroup>
235
232
  <SettingsGroup title={i18n.t('account.singleSignOn')} description={i18n.t('account.ssoDescription')} bodyClass="space-y-4">
236
- <SettingsFieldRow label={i18n.t('account.enableSso')} description={isZh ? '启用后由已配置身份源执行组织登录策略。' : 'Require the configured identity provider for organization members.'} control={ssoControl} />
237
- <Input bind:value={metadataUrl} placeholder="https://idp.example.com/.well-known/openid-configuration" aria-label={i18n.t('account.ssoProvider')} disabled={!identityProvider || testing} />
233
+ <SettingsFieldRow label={i18n.t('account.enableSso')} controlId={`${controlId}-sso`} description={isZh ? '启用后由已配置身份源执行组织登录策略。' : 'Require the configured identity provider for organization members.'} control={ssoControl} />
234
+ <div class="svadmin-u-6f7e013d6499">
235
+ <Label for={`${controlId}-metadata-url`}>{i18n.t('account.ssoProvider')}</Label>
236
+ <Input id={`${controlId}-metadata-url`} type="url" bind:value={metadataUrl} placeholder="https://idp.example.com/.well-known/openid-configuration" disabled={!identityProvider || testing} />
237
+ </div>
238
238
  <Button variant="outline" size="sm" disabled={!identityProvider || testing} onclick={() => testConnection()}>{testing ? (isZh ? '测试中...' : 'Testing...') : i18n.t('common.test')}</Button>
239
239
  </SettingsGroup>
240
240
  </div>
@@ -3,11 +3,15 @@
3
3
  import NotificationsSettings from '../NotificationsSettings.svelte';
4
4
  import SecuritySettings from '../SecuritySettings.svelte';
5
5
  import ContentPageShell from '../content/ContentPageShell.svelte';
6
+ import ContentPageHeader from '../content/ContentPageHeader.svelte';
7
+ import { useTranslation } from '@svadmin/core/i18n';
8
+ const i18n = useTranslation();
6
9
  let { preferencesProvider }: { preferencesProvider?: import('../NotificationsSettings.svelte').NotificationPreferencesProvider } = $props();
7
10
  </script>
8
11
 
9
12
  <ContentPageShell pageId="account-settings-plain" width="narrow">
10
- <ProfilePage />
11
- <NotificationsSettings {...preferencesProvider ? { preferencesProvider } : {}} />
12
- <SecuritySettings />
13
+ <ContentPageHeader title={i18n.t('account.settingsPlain')} />
14
+ <ProfilePage headingLevel="h2" />
15
+ <NotificationsSettings headingLevel="h2" {...preferencesProvider ? { preferencesProvider } : {}} />
16
+ <SecuritySettings headingLevel="h2" />
13
17
  </ContentPageShell>
@@ -27,11 +27,11 @@
27
27
  </nav>
28
28
  <div class="svadmin-u-7e0b7cdf1a94">
29
29
  {#if active === 'profile'}
30
- <ProfilePage />
30
+ <ProfilePage headingLevel="h2" />
31
31
  {:else if active === 'signin'}
32
- <IntegrationsSettings />
32
+ <IntegrationsSettings headingLevel="h2" />
33
33
  {:else if active === 'preferences'}
34
- <AppearanceSettings />
34
+ <AppearanceSettings headingLevel="h2" />
35
35
  {:else}
36
36
  <SettingsGroup title={i18n.t('account.manageApi')} description={isZh ? '使用有作用域的真实凭据,并在权限变更时轮换。' : 'Use scoped credentials and rotate them when access changes.'} bodyClass="space-y-3">
37
37
  {#if credentialProvider}
@@ -9,6 +9,7 @@
9
9
  breadcrumbs?: string[];
10
10
  actions?: Snippet;
11
11
  density?: 'compact' | 'comfortable';
12
+ headingLevel?: 'h1' | 'h2';
12
13
  class?: string;
13
14
  }
14
15
  let {
@@ -18,6 +19,7 @@
18
19
  breadcrumbs = [],
19
20
  actions,
20
21
  density = 'comfortable',
22
+ headingLevel = 'h1',
21
23
  class: className = '',
22
24
  }: Props = $props();
23
25
  const styles = $derived(contentHeader({ density }));
@@ -40,7 +42,7 @@
40
42
  <div class={styles.row}>
41
43
  <div class={styles.heading}>
42
44
  {#if eyebrow}<p class={styles.eyebrow}>{eyebrow}</p>{/if}
43
- <h1 class={styles.title}>{title}</h1>
45
+ <svelte:element this={headingLevel} class={styles.title}>{title}</svelte:element>
44
46
  {#if description}<p class={styles.description}>{description}</p>{/if}
45
47
  </div>
46
48
  {#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
@@ -6,6 +6,7 @@ interface Props {
6
6
  breadcrumbs?: string[];
7
7
  actions?: Snippet;
8
8
  density?: 'compact' | 'comfortable';
9
+ headingLevel?: 'h1' | 'h2';
9
10
  class?: string;
10
11
  }
11
12
  declare const ContentPageHeader: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,35 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { definedOptions } from '@svadmin/core/options';
4
+ import ContentPageShell from './ContentPageShell.svelte';
5
+ import WorkspaceLayout from './WorkspaceLayout.svelte';
6
+
7
+ let {
8
+ title, description, actions, metrics, secondary, children,
9
+ pageId = 'dashboard',
10
+ }: {
11
+ title: string;
12
+ description?: string;
13
+ actions?: Snippet;
14
+ metrics?: Snippet;
15
+ secondary?: Snippet;
16
+ children: Snippet;
17
+ pageId?: string;
18
+ } = $props();
19
+ </script>
20
+
21
+ <ContentPageShell {title} {pageId} width="wide" {...definedOptions({ description, actions })}>
22
+ {#if metrics}
23
+ <div class="svadmin-dashboard-metrics" data-svadmin-dashboard-metrics>{@render metrics()}</div>
24
+ {/if}
25
+ <WorkspaceLayout primary={children} {...definedOptions({ secondary })} />
26
+ </ContentPageShell>
27
+
28
+ <style>
29
+ .svadmin-dashboard-metrics {
30
+ display: grid;
31
+ grid-template-columns: repeat(auto-fit, minmax(min(100%, 14rem), 1fr));
32
+ gap: 1rem;
33
+ min-width: 0;
34
+ }
35
+ </style>
@@ -0,0 +1,13 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ title: string;
4
+ description?: string;
5
+ actions?: Snippet;
6
+ metrics?: Snippet;
7
+ secondary?: Snippet;
8
+ children: Snippet;
9
+ pageId?: string;
10
+ };
11
+ declare const DashboardPage: import("svelte").Component<$$ComponentProps, {}, "">;
12
+ type DashboardPage = ReturnType<typeof DashboardPage>;
13
+ export default DashboardPage;
@@ -27,7 +27,7 @@
27
27
  tone === 'danger'
28
28
  ? 'svadmin-u-26e4f7bdbd56 svadmin-u-7a0854fdbc30 svadmin-u-811148b13d1e'
29
29
  : tone === 'warning'
30
- ? 'svadmin-u-d008dee27eaa svadmin-u-283481e780bb svadmin-u-3a4ff758c2ab'
30
+ ? 'svadmin-u-d008dee27eaa svadmin-u-283481e780bb svadmin-u-d4108abe6359'
31
31
  : 'svadmin-u-18049387f0af svadmin-u-b00f43c30c2b svadmin-u-d4108abe6359',
32
32
  );
33
33
  </script>
@@ -0,0 +1,28 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { definedOptions } from '@svadmin/core/options';
4
+ import SectionHeader from './SectionHeader.svelte';
5
+
6
+ let { title, description, actions, children }: {
7
+ title: string;
8
+ description?: string;
9
+ actions?: Snippet;
10
+ children: Snippet;
11
+ } = $props();
12
+ const id = $props.id();
13
+ </script>
14
+
15
+ <section class="svadmin-page-section" aria-labelledby={id} data-svadmin-page-section>
16
+ <SectionHeader {id} {title} {...definedOptions({ description, actions })} />
17
+ {@render children()}
18
+ </section>
19
+
20
+ <style>
21
+ .svadmin-page-section {
22
+ display: grid;
23
+ grid-template-columns: minmax(0, 1fr);
24
+ align-content: start;
25
+ gap: 1rem;
26
+ min-width: 0;
27
+ }
28
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { Snippet } from 'svelte';
2
+ type $$ComponentProps = {
3
+ title: string;
4
+ description?: string;
5
+ actions?: Snippet;
6
+ children: Snippet;
7
+ };
8
+ declare const PageSection: import("svelte").Component<$$ComponentProps, {}, "">;
9
+ type PageSection = ReturnType<typeof PageSection>;
10
+ export default PageSection;
@@ -1,14 +1,19 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import { productSettingsRowRecipe as productSettingsRow } from '../../recipes.js';
4
- interface Props { label: string; description?: string; control: Snippet; separated?: boolean; class?: string; }
5
- let { label, description, control, separated = false, class: className = '' }: Props = $props();
4
+ interface Props { label: string; description?: string; control: Snippet; controlId?: string; separated?: boolean; class?: string; }
5
+ let { label, description, control, controlId, separated = false, class: className = '' }: Props = $props();
6
+ const labelId = $props.id();
6
7
  const styles = $derived(productSettingsRow({ separated }));
7
8
  </script>
8
9
  <div class={styles.root + ' ' + className} data-svadmin-settings-field-row>
9
10
  <div class={styles.heading}>
10
- <p class={styles.label}>{label}</p>
11
- {#if description}<p class={styles.description}>{description}</p>{/if}
11
+ {#if controlId}
12
+ <label id={labelId} for={controlId} class={styles.label}>{label}</label>
13
+ {:else}
14
+ <p id={labelId} class={styles.label}>{label}</p>
15
+ {/if}
16
+ {#if description}<p id={`${labelId}-description`} class={styles.description}>{description}</p>{/if}
12
17
  </div>
13
- <div class={styles.control}>{@render control()}</div>
18
+ <div class={styles.control} role="group" aria-labelledby={labelId} aria-describedby={description ? `${labelId}-description` : undefined}>{@render control()}</div>
14
19
  </div>
@@ -3,6 +3,7 @@ interface Props {
3
3
  label: string;
4
4
  description?: string;
5
5
  control: Snippet;
6
+ controlId?: string;
6
7
  separated?: boolean;
7
8
  class?: string;
8
9
  }
@@ -1,14 +1,14 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
3
  import { productSettingsRecipe as productSettings } from '../../recipes.js';
4
- interface Props { title: string; description?: string; actions?: Snippet; children: Snippet; class?: string; bodyClass?: string; }
5
- let { title, description, actions, children, class: className = '', bodyClass = '' }: Props = $props();
4
+ interface Props { title: string; description?: string; actions?: Snippet; children: Snippet; headingLevel?: 'h2' | 'h3'; class?: string; bodyClass?: string; }
5
+ let { title, description, actions, children, headingLevel = 'h2', class: className = '', bodyClass = '' }: Props = $props();
6
6
  const styles = productSettings();
7
7
  </script>
8
8
  <section class={styles.root + ' ' + className} data-svadmin-settings-group>
9
9
  <header class={styles.header}>
10
10
  <div class={styles.heading}>
11
- <h2 class={styles.title}>{title}</h2>
11
+ <svelte:element this={headingLevel} class={styles.title}>{title}</svelte:element>
12
12
  {#if description}<p class={styles.description}>{description}</p>{/if}
13
13
  </div>
14
14
  {#if actions}<div class={styles.actions}>{@render actions()}</div>{/if}
@@ -4,6 +4,7 @@ interface Props {
4
4
  description?: string;
5
5
  actions?: Snippet;
6
6
  children: Snippet;
7
+ headingLevel?: 'h2' | 'h3';
7
8
  class?: string;
8
9
  bodyClass?: string;
9
10
  }
@@ -1,4 +1,6 @@
1
1
  export { default as ContentPageShell } from './ContentPageShell.svelte';
2
+ export { default as DashboardPage } from './DashboardPage.svelte';
3
+ export { default as PageSection } from './PageSection.svelte';
2
4
  export { default as ContentPageHeader } from './ContentPageHeader.svelte';
3
5
  export { default as SectionHeader } from './SectionHeader.svelte';
4
6
  export { default as PageToolbar } from './PageToolbar.svelte';
@@ -1,4 +1,6 @@
1
1
  export { default as ContentPageShell } from './ContentPageShell.svelte';
2
+ export { default as DashboardPage } from './DashboardPage.svelte';
3
+ export { default as PageSection } from './PageSection.svelte';
2
4
  export { default as ContentPageHeader } from './ContentPageHeader.svelte';
3
5
  export { default as SectionHeader } from './SectionHeader.svelte';
4
6
  export { default as PageToolbar } from './PageToolbar.svelte';
@@ -16,6 +16,7 @@
16
16
  variant?: ButtonVariantValue;
17
17
  size?: ButtonSizeValue;
18
18
  disabledReason?: string;
19
+ disabledReasonDisplay?: "tooltip" | "inline";
19
20
  restrictionClass?: string;
20
21
  restrictionStyle?: string;
21
22
  };
@@ -34,6 +35,7 @@
34
35
  type = "button",
35
36
  disabled,
36
37
  disabledReason = "",
38
+ disabledReasonDisplay = "tooltip",
37
39
  restrictionClass,
38
40
  restrictionStyle,
39
41
  children,
@@ -100,7 +102,21 @@
100
102
  {/if}
101
103
  {/snippet}
102
104
 
103
- {#if restriction}
105
+ {#if restriction && disabledReasonDisplay === "inline"}
106
+ <span
107
+ data-slot="button-restriction"
108
+ data-disabled-reason={restriction}
109
+ role="group"
110
+ aria-label={restProps["aria-label"]}
111
+ aria-labelledby={restProps["aria-labelledby"]}
112
+ aria-describedby={describedBy}
113
+ class={restrictionClass}
114
+ style={`display: inline-flex; flex-direction: column; align-items: flex-start; gap: .375rem; min-width: 0; max-width: 100%; ${restrictionStyle ?? ""}`}
115
+ >
116
+ {@render control()}
117
+ <span id={restrictionId} data-slot="button-restriction-description" style="font-size: .875rem; line-height: 1.4; overflow-wrap: anywhere;">{restriction}</span>
118
+ </span>
119
+ {:else if restriction}
104
120
  <Tooltip.Root>
105
121
  <Tooltip.Trigger>
106
122
  {#snippet child({ props })}
@@ -6,6 +6,7 @@ export type ButtonProps = WithElementRef<HTMLButtonAttributes & HTMLAnchorAttrib
6
6
  variant?: ButtonVariantValue;
7
7
  size?: ButtonSizeValue;
8
8
  disabledReason?: string;
9
+ disabledReasonDisplay?: "tooltip" | "inline";
9
10
  restrictionClass?: string;
10
11
  restrictionStyle?: string;
11
12
  };
@@ -33,6 +33,7 @@
33
33
  <script lang="ts">
34
34
  /* eslint-disable svelte/no-unused-svelte-ignore */
35
35
  import { tick } from "svelte";
36
+ import { Portal } from "bits-ui";
36
37
  import { cn, type WithElementRef } from "../../../utils.js";
37
38
  import type { HTMLAttributes } from "svelte/elements";
38
39
 
@@ -159,6 +160,7 @@
159
160
  <svelte:document onkeydown={handleKeydown} />
160
161
 
161
162
  {#if open}
163
+ <Portal>
162
164
  <!-- Overlay -->
163
165
  <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
164
166
  <div
@@ -191,4 +193,5 @@
191
193
  <span class="svadmin-sr-only">{closeLabel}</span>
192
194
  </button>
193
195
  </div>
196
+ </Portal>
194
197
  {/if}
@@ -0,0 +1,3 @@
1
+ import type { ThemeConfig } from '@svadmin/core';
2
+ /** UI 默认值不改变 headless core 的主题归属规则,也不覆盖已保存的配色。 */
3
+ export declare function resolveAdminThemeConfig(config?: ThemeConfig): ThemeConfig;
@@ -0,0 +1,4 @@
1
+ /** UI 默认值不改变 headless core 的主题归属规则,也不覆盖已保存的配色。 */
2
+ export function resolveAdminThemeConfig(config) {
3
+ return { ...config, layoutPreset: config?.layoutPreset ?? 'clean-flat' };
4
+ }
package/dist/index.d.ts CHANGED
@@ -98,6 +98,7 @@ export { default as ApprovalActionCard } from './components/ApprovalActionCard.s
98
98
  export type { ApprovalStatus } from './components/ApprovalActionCard.svelte';
99
99
  export { default as ApprovalCenter } from './components/ApprovalCenter.svelte';
100
100
  export { default as DashboardView } from './components/DashboardView.svelte';
101
+ export { default as ResourceOverview } from './components/ResourceOverview.svelte';
101
102
  export { default as StepForm } from './components/StepForm.svelte';
102
103
  export type { FormStep } from './components/StepForm.svelte';
103
104
  export { default as ModalForm } from './components/ModalForm.svelte';
@@ -271,6 +272,8 @@ export { default as UserCardsNFTPage } from './components/network/UserCardsNFTPa
271
272
  export { default as TeamCrewTablePage } from './components/network/TeamCrewTablePage.svelte';
272
273
  export { default as TwoFactorAuthPage } from './components/TwoFactorAuthPage.svelte';
273
274
  export { default as ContentPageShell } from './components/content/ContentPageShell.svelte';
275
+ export { default as DashboardPage } from './components/content/DashboardPage.svelte';
276
+ export { default as PageSection } from './components/content/PageSection.svelte';
274
277
  export { default as ContentPageHeader } from './components/content/ContentPageHeader.svelte';
275
278
  export { default as SectionHeader } from './components/content/SectionHeader.svelte';
276
279
  export { default as PageToolbar } from './components/content/PageToolbar.svelte';
package/dist/index.js CHANGED
@@ -82,6 +82,7 @@ export { default as SensitiveDataMask } from './components/SensitiveDataMask.sve
82
82
  export { default as ApprovalActionCard } from './components/ApprovalActionCard.svelte';
83
83
  export { default as ApprovalCenter } from './components/ApprovalCenter.svelte';
84
84
  export { default as DashboardView } from './components/DashboardView.svelte';
85
+ export { default as ResourceOverview } from './components/ResourceOverview.svelte';
85
86
  export { default as StepForm } from './components/StepForm.svelte';
86
87
  export { default as ModalForm } from './components/ModalForm.svelte';
87
88
  export { default as DrawerForm } from './components/DrawerForm.svelte';
@@ -241,6 +242,8 @@ export { default as TeamCrewTablePage } from './components/network/TeamCrewTable
241
242
  export { default as TwoFactorAuthPage } from './components/TwoFactorAuthPage.svelte';
242
243
  // Admin UI content page and domain primitives
243
244
  export { default as ContentPageShell } from './components/content/ContentPageShell.svelte';
245
+ export { default as DashboardPage } from './components/content/DashboardPage.svelte';
246
+ export { default as PageSection } from './components/content/PageSection.svelte';
244
247
  export { default as ContentPageHeader } from './components/content/ContentPageHeader.svelte';
245
248
  export { default as SectionHeader } from './components/content/SectionHeader.svelte';
246
249
  export { default as PageToolbar } from './components/content/PageToolbar.svelte';
@@ -106,7 +106,7 @@
106
106
  .svadmin-filter-toolbar__search-icon {
107
107
  position: absolute;
108
108
  top: 50%;
109
- left: 0.75rem;
109
+ inset-inline-start: 0.75rem;
110
110
  width: var(--svadmin-filter-search-icon-size);
111
111
  height: var(--svadmin-filter-search-icon-size);
112
112
  color: var(--muted-foreground);
@@ -122,7 +122,7 @@
122
122
  .svadmin-filter-toolbar[data-density] .svadmin-filter-toolbar__clear {
123
123
  position: absolute;
124
124
  top: 50%;
125
- right: 0.25rem;
125
+ inset-inline-end: 0.25rem;
126
126
  transform: translateY(-50%);
127
127
  }
128
128
  .svadmin-filter-toolbar[data-density] .svadmin-filter-toolbar__toggle {
@@ -144,14 +144,14 @@
144
144
  .svadmin-filter-toolbar .svadmin-filter-toolbar__chevron {
145
145
  width: 0.75rem;
146
146
  height: 0.75rem;
147
- margin-left: 0.125rem;
147
+ margin-inline-start: 0.125rem;
148
148
  color: var(--muted-foreground);
149
149
  }
150
150
  .svadmin-filter-toolbar[data-density] .svadmin-filter-toolbar__count {
151
151
  height: 1rem;
152
152
  min-height: 1rem;
153
153
  min-width: 1rem;
154
- margin-left: 0.125rem;
154
+ margin-inline-start: 0.125rem;
155
155
  padding: 0 0.25rem;
156
156
  font-size: 0.625rem;
157
157
  font-weight: 600;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@svadmin/ui",
3
- "version": "0.77.0",
3
+ "version": "0.78.0",
4
4
  "description": "Pre-built admin UI components — AdminApp, AutoTable, AutoForm, Sidebar, Layout",
5
5
  "type": "module",
6
6
  "sideEffects": [