@svadmin/ui 0.85.0 → 0.88.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 (89) hide show
  1. package/README.md +31 -0
  2. package/dist/app-shell.css +216 -0
  3. package/dist/app.css +659 -165
  4. package/dist/app.theme.css +659 -165
  5. package/dist/component-registry.d.ts +7292 -529
  6. package/dist/component-registry.js +32 -3
  7. package/dist/components/AppShellPage.svelte +172 -0
  8. package/dist/components/AppShellPage.svelte.d.ts +30 -0
  9. package/dist/components/ApprovalActionCard.svelte +54 -31
  10. package/dist/components/ApprovalCenter.svelte +73 -31
  11. package/dist/components/AuthPageShell.svelte +13 -2
  12. package/dist/components/AutoForm.svelte +7 -12
  13. package/dist/components/AutoForm.svelte.d.ts +0 -1
  14. package/dist/components/AutoTable.svelte +15 -3
  15. package/dist/components/AutoTable.svelte.d.ts +2 -0
  16. package/dist/components/ConfigurationPage.svelte +0 -3
  17. package/dist/components/ConfigurationPage.svelte.d.ts +0 -1
  18. package/dist/components/CreatePage.svelte +22 -19
  19. package/dist/components/CreatePage.svelte.d.ts +4 -1
  20. package/dist/components/EditPage.svelte +23 -20
  21. package/dist/components/EditPage.svelte.d.ts +4 -1
  22. package/dist/components/FieldRenderer.svelte +2 -10
  23. package/dist/components/FieldRenderer.svelte.d.ts +0 -1
  24. package/dist/components/ListPage.svelte +21 -18
  25. package/dist/components/ListPage.svelte.d.ts +2 -1
  26. package/dist/components/LoginPage.svelte +27 -11
  27. package/dist/components/LoginPage.svelte.d.ts +4 -0
  28. package/dist/components/NotificationsSettings.svelte +16 -8
  29. package/dist/components/PageHeader.svelte +1 -3
  30. package/dist/components/PageHeader.svelte.d.ts +0 -1
  31. package/dist/components/PagePattern.svelte +36 -13
  32. package/dist/components/PagePattern.svelte.d.ts +7 -0
  33. package/dist/components/PasswordInput.svelte +36 -17
  34. package/dist/components/PasswordInput.svelte.d.ts +3 -0
  35. package/dist/components/QuickEditDrawer.svelte +0 -1
  36. package/dist/components/ShowPage.svelte +24 -29
  37. package/dist/components/ShowPage.svelte.d.ts +0 -1
  38. package/dist/components/SvarAutoTable.svelte +2 -1
  39. package/dist/components/SvarDataGrid.svelte +8 -4
  40. package/dist/components/SvarDataGrid.svelte.d.ts +3 -0
  41. package/dist/components/SvarResourceTable.svelte +0 -1
  42. package/dist/components/SvarResourceTable.svelte.d.ts +0 -1
  43. package/dist/components/SvarResourceTableView.svelte +2 -2
  44. package/dist/components/account/MemberDirectory.svelte +17 -4
  45. package/dist/components/account/SecurityLogPage.svelte +8 -1
  46. package/dist/components/auto-table-grid.d.ts +1 -0
  47. package/dist/components/content/AuditTimeline.svelte +32 -4
  48. package/dist/components/content/AuditTimeline.svelte.d.ts +9 -0
  49. package/dist/components/content/ContentPageHeader.svelte +2 -1
  50. package/dist/components/content/ContentPageHeader.svelte.d.ts +1 -0
  51. package/dist/components/content/ContentPageShell.svelte +1 -1
  52. package/dist/components/content/ContentPageShell.svelte.d.ts +1 -0
  53. package/dist/components/content/DataPanel.svelte +58 -0
  54. package/dist/components/content/DataPanel.svelte.d.ts +16 -0
  55. package/dist/components/content/DataState.svelte +5 -4
  56. package/dist/components/content/DataState.svelte.d.ts +1 -1
  57. package/dist/components/content/DescriptionList.svelte +5 -8
  58. package/dist/components/content/DescriptionList.svelte.d.ts +0 -1
  59. package/dist/components/content/FilterToolbar.svelte +9 -6
  60. package/dist/components/content/FilterToolbar.svelte.d.ts +0 -1
  61. package/dist/components/content/NetworkTable.svelte +3 -3
  62. package/dist/components/content/NetworkTable.svelte.d.ts +0 -1
  63. package/dist/components/content/PageSection.svelte +1 -1
  64. package/dist/components/content/StatusTabs.svelte +2 -7
  65. package/dist/components/content/StatusTabs.svelte.d.ts +0 -1
  66. package/dist/components/content/WorkspaceLayout.svelte +3 -1
  67. package/dist/components/content/WorkspaceLayout.svelte.d.ts +1 -0
  68. package/dist/components/content/index.d.ts +1 -0
  69. package/dist/components/content/index.js +1 -0
  70. package/dist/components/network/TeamCrewTablePage.svelte +8 -1
  71. package/dist/components/network/UserCardsNFTPage.svelte +3 -1
  72. package/dist/components/profile/ProjectsGrid.svelte +1 -1
  73. package/dist/components/profile/TeamsShowcase.svelte +1 -1
  74. package/dist/components/ui/table/index.d.ts +0 -1
  75. package/dist/components/ui/table/table.svelte +1 -0
  76. package/dist/components/ui/table/table.svelte.d.ts +1 -0
  77. package/dist/components.css +1 -11
  78. package/dist/data-panel.css +88 -0
  79. package/dist/design-contract.d.ts +11 -0
  80. package/dist/design-contract.js +8 -0
  81. package/dist/design-system.d.ts +3 -2
  82. package/dist/design-system.js +3 -2
  83. package/dist/index.d.ts +4 -2
  84. package/dist/index.js +2 -0
  85. package/dist/page-chrome.css +42 -64
  86. package/dist/recipes.d.ts +12 -66
  87. package/dist/recipes.js +52 -50
  88. package/dist/styles/recipes.css +15 -48
  89. package/package.json +4 -4
@@ -12,6 +12,36 @@ function variant(recipe, name) {
12
12
  };
13
13
  }
14
14
  const registryEntries = [
15
+ {
16
+ id: "page-pattern",
17
+ component: "@svadmin/ui/components/PagePattern.svelte",
18
+ recipe: "contentPageRecipe",
19
+ props: {
20
+ kind: { kind: "enum", values: ["list", "form", "detail", "workspace", "dashboard"], required: true },
21
+ title: { kind: "string", required: true },
22
+ description: { kind: "string" },
23
+ eyebrow: { kind: "string" },
24
+ pageId: { kind: "string" },
25
+ width: variant("contentPageRecipe", "width"),
26
+ density: { kind: "enum", values: ["comfortable", "compact"], default: "comfortable", description: "Deprecated compatibility prop; use the shared baseline." },
27
+ state: { kind: "enum", values: ["ready", "loading", "empty", "filtered-empty", "error", "forbidden"], default: "ready" },
28
+ stateTitle: { kind: "string" },
29
+ stateDescription: { kind: "string" },
30
+ retry: { kind: "callback", description: "Use an error-state retry callback." },
31
+ class: { kind: "string" },
32
+ },
33
+ slots: {
34
+ children: { kind: "snippet", required: true },
35
+ actions: { kind: "snippet" },
36
+ toolbar: { kind: "snippet" },
37
+ metrics: { kind: "snippet" },
38
+ secondary: { kind: "snippet" },
39
+ footer: { kind: "snippet" },
40
+ stateAction: { kind: "snippet" },
41
+ },
42
+ tokens: semanticTokens,
43
+ states: ["ready", "loading", "empty", "filtered-empty", "error", "forbidden"],
44
+ },
15
45
  {
16
46
  id: "content-page-shell",
17
47
  component: "@svadmin/ui/components/content/ContentPageShell.svelte",
@@ -22,7 +52,7 @@ const registryEntries = [
22
52
  description: { kind: "string" },
23
53
  pageId: { kind: "string" },
24
54
  width: variant("contentPageRecipe", "width"),
25
- density: variant("contentPageRecipe", "density"),
55
+ density: { kind: "enum", values: ["comfortable", "compact"], default: "comfortable", description: "Deprecated compatibility prop; use the shared baseline." },
26
56
  class: { kind: "string" },
27
57
  },
28
58
  slots: { children: { kind: "snippet", required: true }, actions: { kind: "snippet" } },
@@ -38,7 +68,6 @@ const registryEntries = [
38
68
  eyebrow: { kind: "string" },
39
69
  description: { kind: "string" },
40
70
  breadcrumbs: { kind: "string[]" },
41
- density: variant("contentHeaderRecipe", "density"),
42
71
  class: { kind: "string" },
43
72
  },
44
73
  slots: { actions: { kind: "snippet" } },
@@ -208,7 +237,7 @@ export function createComponentRegistryManifest() {
208
237
  return {
209
238
  version: "svadmin/component-registry-v1",
210
239
  purpose: "authoring-only",
211
- coverage: "twelve-ui-components-and-fourteen-recipes",
240
+ coverage: "page-pattern-and-semantic-ui-components",
212
241
  runtimeCatalog: "@svadmin/surface: createSurfaceCatalogManifest",
213
242
  designContract: surfaceDesignContract,
214
243
  recipes: Object.fromEntries(Object.keys(allRecipes)
@@ -0,0 +1,172 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from 'svelte';
3
+ import { Menu, X } from '@lucide/svelte';
4
+ import { useTranslation } from '@svadmin/core/i18n';
5
+ import { cn } from '../utils.js';
6
+
7
+ type Width = 'narrow' | 'default' | 'wide' | 'full';
8
+
9
+ interface Props {
10
+ /** Page content; typically a `PagePattern` or `ContentPageShell`. */
11
+ children: Snippet;
12
+ /** Navigation region. The host owns links and routing; the shell owns layout. */
13
+ sidebar?: Snippet;
14
+ /** Leading topbar region, rendered before the mobile navigation trigger. */
15
+ topbarStart?: Snippet;
16
+ /** Topbar region between the leading and trailing slots (search, breadcrumbs…). */
17
+ topbar?: Snippet;
18
+ /** Trailing topbar actions (account menu, notifications…). */
19
+ topbarEnd?: Snippet;
20
+ /** Optional secondary column for detail/workspace pages. */
21
+ aside?: Snippet;
22
+ footer?: Snippet;
23
+ width?: Width;
24
+ /** Collapses the navigation rail to icons. Host-controlled, presentation only. */
25
+ collapsed?: boolean;
26
+ /** Open state of the below-breakpoint navigation drawer. */
27
+ mobileOpen?: boolean;
28
+ /** Accessible name for the primary landmark. */
29
+ label?: string;
30
+ skipLabel?: string;
31
+ contentId?: string;
32
+ class?: string;
33
+ }
34
+
35
+ let {
36
+ children,
37
+ sidebar,
38
+ topbarStart,
39
+ topbar,
40
+ topbarEnd,
41
+ aside,
42
+ footer,
43
+ width = 'default',
44
+ collapsed = false,
45
+ mobileOpen = $bindable(false),
46
+ label,
47
+ skipLabel,
48
+ contentId,
49
+ class: className = '',
50
+ }: Props = $props();
51
+
52
+ const generatedId = $props.id();
53
+ const i18n = useTranslation();
54
+ const mainId = $derived(contentId ?? generatedId);
55
+ const sidebarId = $derived(`${generatedId}-sidebar`);
56
+ // 未提供可读名称时退回通用文案,避免出现没有可访问名的 <main>。
57
+ const resolvedLabel = $derived(label ?? i18n.t('common.mainNavigation'));
58
+ const resolvedSkipLabel = $derived(skipLabel ?? i18n.t('common.skipToMainContent'));
59
+
60
+ function closeMobileNav() {
61
+ mobileOpen = false;
62
+ }
63
+
64
+ function onKeydown(event: KeyboardEvent) {
65
+ if (event.key === 'Escape' && mobileOpen) closeMobileNav();
66
+ }
67
+
68
+ // 宿主常用 hash 路由,不能让跳转链接把 #main 当成一次路由跳转。
69
+ function focusMain(event: MouseEvent) {
70
+ const target = document.getElementById(mainId);
71
+ if (!target) return;
72
+ event.preventDefault();
73
+ target.focus();
74
+ target.scrollIntoView?.({ block: 'start' });
75
+ }
76
+ </script>
77
+
78
+ <svelte:window onkeydown={onKeydown} />
79
+
80
+ <a class="svadmin-app-shell__skip" href="#{mainId}" onclick={focusMain}>{resolvedSkipLabel}</a>
81
+
82
+ <div
83
+ class={cn('svadmin-app-shell', className)}
84
+ data-sidebar={sidebar ? 'true' : undefined}
85
+ data-aside={aside ? 'true' : undefined}
86
+ data-collapsed={collapsed ? 'true' : undefined}
87
+ data-mobile-open={mobileOpen ? 'true' : undefined}
88
+ data-width={width}
89
+ >
90
+ <header class="svadmin-app-shell__topbar" data-slot="app-shell-topbar">
91
+ {#if sidebar}
92
+ <button
93
+ type="button"
94
+ class="svadmin-app-shell__mobile-toggle"
95
+ data-slot="app-shell-mobile-toggle"
96
+ aria-controls={sidebarId}
97
+ aria-expanded={mobileOpen ? 'true' : 'false'}
98
+ aria-label={mobileOpen ? i18n.t('common.close') : i18n.t('common.menu')}
99
+ onclick={() => (mobileOpen = !mobileOpen)}
100
+ >
101
+ {#if mobileOpen}
102
+ <X aria-hidden="true" size={18} />
103
+ {:else}
104
+ <Menu aria-hidden="true" size={18} />
105
+ {/if}
106
+ </button>
107
+ {/if}
108
+ {#if topbarStart}
109
+ <div class="svadmin-app-shell__topbar-start">{@render topbarStart()}</div>
110
+ {/if}
111
+ <div class="svadmin-app-shell__topbar-main">{@render topbar?.()}</div>
112
+ {#if topbarEnd}
113
+ <div class="svadmin-app-shell__topbar-end">{@render topbarEnd()}</div>
114
+ {/if}
115
+ </header>
116
+
117
+ {#if sidebar}
118
+ <nav id={sidebarId} class="svadmin-app-shell__sidebar" aria-label={resolvedLabel} data-slot="app-shell-sidebar">
119
+ <div class="svadmin-app-shell__sidebar-inner">{@render sidebar()}</div>
120
+ </nav>
121
+ <button
122
+ type="button"
123
+ class="svadmin-app-shell__scrim"
124
+ tabindex="-1"
125
+ aria-hidden="true"
126
+ data-slot="app-shell-scrim"
127
+ onclick={closeMobileNav}
128
+ ></button>
129
+ {/if}
130
+
131
+ <main id={mainId} class="svadmin-app-shell__main" tabindex="-1">
132
+ <div class="svadmin-app-shell__body" data-slot="app-shell-body">
133
+ <div class="svadmin-app-shell__content" data-slot="app-shell-content">
134
+ {@render children()}
135
+ </div>
136
+ {#if aside}
137
+ <aside class="svadmin-app-shell__aside" data-slot="app-shell-aside">{@render aside()}</aside>
138
+ {/if}
139
+ </div>
140
+ </main>
141
+
142
+ {#if footer}
143
+ <footer class="svadmin-app-shell__footer" data-slot="app-shell-footer">{@render footer()}</footer>
144
+ {/if}
145
+ </div>
146
+
147
+ <style>
148
+ /* Layout-only overrides. Sizes, colors and radii stay in app-shell.css so a
149
+ * host override of the token layer reaches this component too. */
150
+ .svadmin-app-shell__mobile-toggle {
151
+ align-items: center;
152
+ justify-content: center;
153
+ flex: 0 0 auto;
154
+ height: var(--svadmin-control-h);
155
+ width: var(--svadmin-control-h);
156
+ border: 1px solid var(--border);
157
+ border-radius: var(--radius-md);
158
+ background: var(--background);
159
+ color: var(--foreground);
160
+ cursor: pointer;
161
+ }
162
+ .svadmin-app-shell__mobile-toggle:hover {
163
+ background: var(--muted);
164
+ }
165
+ .svadmin-app-shell__mobile-toggle:focus-visible {
166
+ outline: var(--svadmin-focus-width) solid var(--ring);
167
+ outline-offset: var(--svadmin-focus-offset);
168
+ }
169
+ .svadmin-app-shell__main:focus-visible {
170
+ outline: none;
171
+ }
172
+ </style>
@@ -0,0 +1,30 @@
1
+ import type { Snippet } from 'svelte';
2
+ type Width = 'narrow' | 'default' | 'wide' | 'full';
3
+ interface Props {
4
+ /** Page content; typically a `PagePattern` or `ContentPageShell`. */
5
+ children: Snippet;
6
+ /** Navigation region. The host owns links and routing; the shell owns layout. */
7
+ sidebar?: Snippet;
8
+ /** Leading topbar region, rendered before the mobile navigation trigger. */
9
+ topbarStart?: Snippet;
10
+ /** Topbar region between the leading and trailing slots (search, breadcrumbs…). */
11
+ topbar?: Snippet;
12
+ /** Trailing topbar actions (account menu, notifications…). */
13
+ topbarEnd?: Snippet;
14
+ /** Optional secondary column for detail/workspace pages. */
15
+ aside?: Snippet;
16
+ footer?: Snippet;
17
+ width?: Width;
18
+ /** Collapses the navigation rail to icons. Host-controlled, presentation only. */
19
+ collapsed?: boolean;
20
+ /** Open state of the below-breakpoint navigation drawer. */
21
+ mobileOpen?: boolean;
22
+ /** Accessible name for the primary landmark. */
23
+ label?: string;
24
+ skipLabel?: string;
25
+ contentId?: string;
26
+ class?: string;
27
+ }
28
+ declare const AppShellPage: import("svelte").Component<Props, {}, "mobileOpen">;
29
+ type AppShellPage = ReturnType<typeof AppShellPage>;
30
+ export default AppShellPage;
@@ -4,6 +4,7 @@
4
4
  import { Button } from './ui/button/index.js';
5
5
  import { Badge } from './ui/badge/index.js';
6
6
  import * as Dialog from './ui/dialog/index.js';
7
+ import { useTranslation } from '@svadmin/core/i18n';
7
8
  import { cn } from '../utils.js';
8
9
 
9
10
  export type ApprovalStatus = 'pending' | 'approved' | 'rejected' | 'recalled';
@@ -28,7 +29,7 @@
28
29
  }
29
30
 
30
31
  let {
31
- title = 'Approval Request',
32
+ title,
32
33
  status = 'pending',
33
34
  applicant,
34
35
  onapprove,
@@ -39,6 +40,10 @@
39
40
  children,
40
41
  }: Props = $props();
41
42
 
43
+ const controlId = $props.id();
44
+ const i18n = useTranslation();
45
+ const isZh = $derived(i18n.locale === 'zh-CN');
46
+ const resolvedTitle = $derived(title ?? (isZh ? '审批申请' : 'Approval Request'));
42
47
  let approveDialogOpen = $state(false);
43
48
  let rejectDialogOpen = $state(false);
44
49
  let transferDialogOpen = $state(false);
@@ -47,26 +52,35 @@
47
52
  let rejectReason = $state('');
48
53
  let transferTarget = $state('');
49
54
  let isSubmitting = $state(false);
55
+ let errorMessage = $state('');
50
56
 
51
57
  const statusConfig = $derived.by(() => {
52
58
  switch (status) {
53
59
  case 'approved':
54
- return { label: 'Approved', icon: CheckCircle2 };
60
+ return { label: isZh ? '已通过' : 'Approved', icon: CheckCircle2 };
55
61
  case 'rejected':
56
- return { label: 'Rejected', icon: XCircle };
62
+ return { label: isZh ? '已驳回' : 'Rejected', icon: XCircle };
57
63
  case 'recalled':
58
- return { label: 'Recalled', icon: Undo2 };
64
+ return { label: isZh ? '已撤回' : 'Recalled', icon: Undo2 };
59
65
  default:
60
- return { label: 'Pending Approval', icon: Clock };
66
+ return { label: isZh ? '待审批' : 'Pending Approval', icon: Clock };
61
67
  }
62
68
  });
63
69
 
70
+ function failureMessage(caught: unknown): string {
71
+ if (caught instanceof Error && caught.message.trim()) return caught.message;
72
+ return isZh ? '操作失败,请检查输入后重试。' : 'Action failed. Check the input and retry.';
73
+ }
74
+
64
75
  async function handleApprove() {
65
76
  isSubmitting = true;
77
+ errorMessage = '';
66
78
  try {
67
79
  await onapprove?.(approveComment || undefined);
68
80
  approveDialogOpen = false;
69
81
  approveComment = '';
82
+ } catch (caught) {
83
+ errorMessage = failureMessage(caught);
70
84
  } finally {
71
85
  isSubmitting = false;
72
86
  }
@@ -75,10 +89,13 @@
75
89
  async function handleReject() {
76
90
  if (!rejectReason.trim()) return;
77
91
  isSubmitting = true;
92
+ errorMessage = '';
78
93
  try {
79
94
  await onreject?.(rejectReason);
80
95
  rejectDialogOpen = false;
81
96
  rejectReason = '';
97
+ } catch (caught) {
98
+ errorMessage = failureMessage(caught);
82
99
  } finally {
83
100
  isSubmitting = false;
84
101
  }
@@ -87,10 +104,13 @@
87
104
  async function handleTransfer() {
88
105
  if (!transferTarget.trim()) return;
89
106
  isSubmitting = true;
107
+ errorMessage = '';
90
108
  try {
91
109
  await ontransfer?.(transferTarget);
92
110
  transferDialogOpen = false;
93
111
  transferTarget = '';
112
+ } catch (caught) {
113
+ errorMessage = failureMessage(caught);
94
114
  } finally {
95
115
  isSubmitting = false;
96
116
  }
@@ -101,7 +121,7 @@
101
121
  <div class="svadmin-approval-card__header">
102
122
  <div class="svadmin-approval-card__heading">
103
123
  <div class="svadmin-approval-card__title-row">
104
- <h4 class="svadmin-approval-card__title">{title}</h4>
124
+ <h4 class="svadmin-approval-card__title">{resolvedTitle}</h4>
105
125
  <Badge variant="outline" data-status={status}>
106
126
  <statusConfig.icon class="svadmin-approval-card__status-icon" />
107
127
  {statusConfig.label}
@@ -109,7 +129,7 @@
109
129
  </div>
110
130
  {#if applicant}
111
131
  <p class="svadmin-approval-card__applicant">
112
- Submitted by <strong>{applicant.name}</strong>
132
+ {isZh ? '提交人:' : 'Submitted by '}<strong>{applicant.name}</strong>
113
133
  {#if applicant.department} ({applicant.department}){/if}
114
134
  {#if applicant.time} · {applicant.time}{/if}
115
135
  </p>
@@ -124,10 +144,10 @@
124
144
  size="sm"
125
145
  class="svadmin-approval-card__action"
126
146
  {disabled}
127
- onclick={() => { transferDialogOpen = true; }}
147
+ onclick={() => { transferDialogOpen = true; errorMessage = ''; }}
128
148
  >
129
149
  <ArrowRightLeft class="svadmin-approval-card__action-icon" />
130
- Transfer
150
+ {isZh ? '转交' : 'Transfer'}
131
151
  </Button>
132
152
  {/if}
133
153
 
@@ -137,10 +157,10 @@
137
157
  size="sm"
138
158
  class="svadmin-approval-card__action svadmin-approval-card__action--reject"
139
159
  {disabled}
140
- onclick={() => { rejectDialogOpen = true; }}
160
+ onclick={() => { rejectDialogOpen = true; errorMessage = ''; }}
141
161
  >
142
162
  <X class="svadmin-approval-card__action-icon" />
143
- Reject
163
+ {isZh ? '驳回' : 'Reject'}
144
164
  </Button>
145
165
  {/if}
146
166
 
@@ -149,10 +169,10 @@
149
169
  size="sm"
150
170
  class="svadmin-approval-card__action svadmin-approval-card__action--approve"
151
171
  {disabled}
152
- onclick={() => { approveDialogOpen = true; }}
172
+ onclick={() => { approveDialogOpen = true; errorMessage = ''; }}
153
173
  >
154
174
  <Check class="svadmin-approval-card__action-icon" />
155
- Approve
175
+ {isZh ? '通过' : 'Approve'}
156
176
  </Button>
157
177
  {/if}
158
178
  </div>
@@ -171,21 +191,22 @@
171
191
  <Dialog.Dialog bind:open={approveDialogOpen}>
172
192
  <Dialog.DialogContent class="svadmin-approval-card__dialog">
173
193
  <Dialog.DialogHeader>
174
- <Dialog.DialogTitle>Confirm Approval</Dialog.DialogTitle>
194
+ <Dialog.DialogTitle>{isZh ? '确认通过' : 'Confirm Approval'}</Dialog.DialogTitle>
175
195
  </Dialog.DialogHeader>
176
196
  <div class="svadmin-approval-card__dialog-form">
177
- <label class="svadmin-approval-card__dialog-label" for="approve_comment_input">Optional Approval Comment</label>
197
+ <label class="svadmin-approval-card__dialog-label" for={`${controlId}-approve-comment`}>{isZh ? '审批意见(可选)' : 'Optional Approval Comment'}</label>
178
198
  <textarea
179
- id="approve_comment_input"
199
+ id={`${controlId}-approve-comment`}
180
200
  bind:value={approveComment}
181
- placeholder="e.g. Verified and approved"
201
+ placeholder={isZh ? '例如:已核验,准予通过' : 'e.g. Verified and approved'}
182
202
  class="svadmin-approval-card__textarea"
183
203
  ></textarea>
204
+ {#if errorMessage}<p role="alert" class="svadmin-approval-card__error text-destructive">{errorMessage}</p>{/if}
184
205
  </div>
185
206
  <Dialog.DialogFooter>
186
- <Button variant="outline" size="sm" onclick={() => { approveDialogOpen = false; }}>Cancel</Button>
207
+ <Button variant="outline" size="sm" onclick={() => { approveDialogOpen = false; errorMessage = ''; }}>{isZh ? '取消' : 'Cancel'}</Button>
187
208
  <Button size="sm" class="svadmin-approval-card__confirm svadmin-approval-card__confirm--approve" disabled={isSubmitting} onclick={handleApprove}>
188
- Confirm Approval
209
+ {isSubmitting ? (isZh ? '处理中…' : 'Processing…') : (isZh ? '确认通过' : 'Confirm Approval')}
189
210
  </Button>
190
211
  </Dialog.DialogFooter>
191
212
  </Dialog.DialogContent>
@@ -197,22 +218,23 @@
197
218
  <Dialog.Dialog bind:open={rejectDialogOpen}>
198
219
  <Dialog.DialogContent class="svadmin-approval-card__dialog">
199
220
  <Dialog.DialogHeader>
200
- <Dialog.DialogTitle class="svadmin-approval-card__reject-title">Reject Request</Dialog.DialogTitle>
221
+ <Dialog.DialogTitle class="svadmin-approval-card__reject-title">{isZh ? '驳回申请' : 'Reject Request'}</Dialog.DialogTitle>
201
222
  </Dialog.DialogHeader>
202
223
  <div class="svadmin-approval-card__dialog-form">
203
- <label class="svadmin-approval-card__dialog-label" for="reject_reason_input">Rejection Reason (Required)</label>
224
+ <label class="svadmin-approval-card__dialog-label" for={`${controlId}-reject-reason`}>{isZh ? '驳回原因(必填)' : 'Rejection Reason (Required)'}</label>
204
225
  <textarea
205
- id="reject_reason_input"
226
+ id={`${controlId}-reject-reason`}
206
227
  bind:value={rejectReason}
207
- placeholder="Please explain why this request is being rejected..."
228
+ placeholder={isZh ? '请说明驳回原因…' : 'Please explain why this request is being rejected...'}
208
229
  class="svadmin-approval-card__textarea svadmin-approval-card__textarea--reject"
209
230
  required
210
231
  ></textarea>
232
+ {#if errorMessage}<p role="alert" class="svadmin-approval-card__error text-destructive">{errorMessage}</p>{/if}
211
233
  </div>
212
234
  <Dialog.DialogFooter>
213
- <Button variant="outline" size="sm" onclick={() => { rejectDialogOpen = false; }}>Cancel</Button>
235
+ <Button variant="outline" size="sm" onclick={() => { rejectDialogOpen = false; errorMessage = ''; }}>{isZh ? '取消' : 'Cancel'}</Button>
214
236
  <Button variant="destructive" size="sm" disabled={isSubmitting || !rejectReason.trim()} onclick={handleReject}>
215
- Confirm Rejection
237
+ {isSubmitting ? (isZh ? '处理中…' : 'Processing…') : (isZh ? '确认驳回' : 'Confirm Rejection')}
216
238
  </Button>
217
239
  </Dialog.DialogFooter>
218
240
  </Dialog.DialogContent>
@@ -224,23 +246,24 @@
224
246
  <Dialog.Dialog bind:open={transferDialogOpen}>
225
247
  <Dialog.DialogContent class="svadmin-approval-card__dialog">
226
248
  <Dialog.DialogHeader>
227
- <Dialog.DialogTitle>Transfer Approval</Dialog.DialogTitle>
249
+ <Dialog.DialogTitle>{isZh ? '转交审批' : 'Transfer Approval'}</Dialog.DialogTitle>
228
250
  </Dialog.DialogHeader>
229
251
  <div class="svadmin-approval-card__dialog-form">
230
- <label class="svadmin-approval-card__dialog-label" for="transfer_target_input">Target Approver Email / Username</label>
252
+ <label class="svadmin-approval-card__dialog-label" for={`${controlId}-transfer-target`}>{isZh ? '目标审批人邮箱或用户名' : 'Target Approver Email / Username'}</label>
231
253
  <input
232
- id="transfer_target_input"
254
+ id={`${controlId}-transfer-target`}
233
255
  type="text"
234
256
  bind:value={transferTarget}
235
- placeholder="e.g. manager@example.com"
257
+ placeholder={isZh ? '例如:manager@example.com' : 'e.g. manager@example.com'}
236
258
  class="svadmin-approval-card__input"
237
259
  required
238
260
  />
261
+ {#if errorMessage}<p role="alert" class="svadmin-approval-card__error text-destructive">{errorMessage}</p>{/if}
239
262
  </div>
240
263
  <Dialog.DialogFooter>
241
- <Button variant="outline" size="sm" onclick={() => { transferDialogOpen = false; }}>Cancel</Button>
264
+ <Button variant="outline" size="sm" onclick={() => { transferDialogOpen = false; errorMessage = ''; }}>{isZh ? '取消' : 'Cancel'}</Button>
242
265
  <Button size="sm" disabled={isSubmitting || !transferTarget.trim()} onclick={handleTransfer}>
243
- Transfer
266
+ {isSubmitting ? (isZh ? '处理中…' : 'Processing…') : (isZh ? '转交' : 'Transfer')}
244
267
  </Button>
245
268
  </Dialog.DialogFooter>
246
269
  </Dialog.DialogContent>