@svadmin/ui 0.88.1 → 0.92.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 (172) hide show
  1. package/dist/app.css +4 -0
  2. package/dist/app.theme.css +4 -0
  3. package/dist/component-registry.svelte.d.ts +2 -2
  4. package/dist/components/AboutSettings.svelte +23 -4
  5. package/dist/components/AdminApp.svelte +6 -4
  6. package/dist/components/AdminApp.svelte.d.ts +3 -3
  7. package/dist/components/ApiSettings.svelte +43 -27
  8. package/dist/components/ApprovalActionCard.svelte +8 -9
  9. package/dist/components/ApprovalCenter.svelte +24 -5
  10. package/dist/components/AuditLogViewer.svelte +2 -2
  11. package/dist/components/AutoComplete.svelte +610 -0
  12. package/dist/components/AutoComplete.svelte.d.ts +25 -0
  13. package/dist/components/AutoForm.svelte +265 -18
  14. package/dist/components/AutoForm.svelte.d.ts +19 -2
  15. package/dist/components/AutoTable.svelte +141 -36
  16. package/dist/components/AutoTable.svelte.d.ts +8 -0
  17. package/dist/components/BatchActionBar.svelte +42 -14
  18. package/dist/components/BatchActionBar.svelte.d.ts +5 -0
  19. package/dist/components/BoundRecordDetailDrawer.svelte +5 -3
  20. package/dist/components/CancelTaskButton.svelte +14 -1
  21. package/dist/components/Cascader.svelte +412 -53
  22. package/dist/components/Cascader.svelte.d.ts +7 -1
  23. package/dist/components/ColorPicker.svelte +1 -1
  24. package/dist/components/ColorPicker.svelte.d.ts +1 -1
  25. package/dist/components/ComboboxField.svelte +270 -34
  26. package/dist/components/ComboboxField.svelte.d.ts +4 -0
  27. package/dist/components/CommandPalette.svelte +4 -4
  28. package/dist/components/DashboardView.svelte +73 -7
  29. package/dist/components/DashboardView.svelte.d.ts +3 -1
  30. package/dist/components/DateRangeInput.svelte +8 -1
  31. package/dist/components/DetailWorkspaceAsyncSection.svelte +107 -0
  32. package/dist/components/DetailWorkspaceAsyncSection.svelte.d.ts +36 -0
  33. package/dist/components/DetailWorkspaceSections.svelte +233 -0
  34. package/dist/components/DetailWorkspaceSections.svelte.d.ts +9 -0
  35. package/dist/components/DrawerForm.svelte +78 -9
  36. package/dist/components/DrawerForm.svelte.d.ts +3 -3
  37. package/dist/components/ErrorBoundary.svelte +1 -1
  38. package/dist/components/ErrorComponent.svelte +19 -6
  39. package/dist/components/ErrorComponent.svelte.d.ts +1 -0
  40. package/dist/components/ErrorSummary.svelte +9 -4
  41. package/dist/components/FieldRenderer.svelte +203 -90
  42. package/dist/components/FieldRenderer.svelte.d.ts +16 -1
  43. package/dist/components/FileUpload.svelte +116 -51
  44. package/dist/components/FileUpload.svelte.d.ts +7 -27
  45. package/dist/components/FileUploadField.svelte +170 -0
  46. package/dist/components/FileUploadField.svelte.d.ts +21 -0
  47. package/dist/components/FilterBuilder.svelte +13 -7
  48. package/dist/components/FilterCollectionInput.svelte +9 -7
  49. package/dist/components/FilterCollectionInput.svelte.d.ts +3 -0
  50. package/dist/components/ForgotPasswordPage.svelte +1 -1
  51. package/dist/components/Header.svelte +1 -1
  52. package/dist/components/ImportWizard.svelte +197 -28
  53. package/dist/components/ImportWizard.svelte.d.ts +5 -1
  54. package/dist/components/InferencerPanel.svelte +4 -2
  55. package/dist/components/InfiniteList.svelte +52 -9
  56. package/dist/components/JsonSchemaForm.svelte +237 -42
  57. package/dist/components/JsonSchemaForm.svelte.d.ts +10 -3
  58. package/dist/components/KanbanBoard.svelte +43 -7
  59. package/dist/components/KanbanBoard.svelte.d.ts +2 -2
  60. package/dist/components/Layout.svelte +1 -1
  61. package/dist/components/LoginPage.svelte +1 -1
  62. package/dist/components/MetricStrip.svelte +8 -3
  63. package/dist/components/MetricStrip.svelte.d.ts +1 -0
  64. package/dist/components/ModalForm.svelte +85 -9
  65. package/dist/components/ModalForm.svelte.d.ts +3 -3
  66. package/dist/components/MoneyInput.svelte +5 -3
  67. package/dist/components/MoneyInput.svelte.d.ts +4 -2
  68. package/dist/components/MultiTabKeepAlive.svelte +502 -43
  69. package/dist/components/MultiTabKeepAlive.svelte.d.ts +40 -6
  70. package/dist/components/NotificationsSettings.svelte +10 -10
  71. package/dist/components/NumberInput.svelte +5 -3
  72. package/dist/components/NumberInput.svelte.d.ts +4 -2
  73. package/dist/components/NumericInput.svelte +277 -25
  74. package/dist/components/NumericInput.svelte.d.ts +3 -1
  75. package/dist/components/PercentInput.svelte +5 -3
  76. package/dist/components/PercentInput.svelte.d.ts +4 -2
  77. package/dist/components/PermissionMatrix.svelte +44 -8
  78. package/dist/components/PermissionMatrix.svelte.d.ts +1 -0
  79. package/dist/components/ProfilePage.svelte +38 -13
  80. package/dist/components/RadioGroup.svelte +203 -4
  81. package/dist/components/RadioGroup.svelte.d.ts +1 -1
  82. package/dist/components/Rate.svelte +3 -3
  83. package/dist/components/Rate.svelte.d.ts +2 -2
  84. package/dist/components/RecordDetailDrawer.svelte +1 -1
  85. package/dist/components/RecordDetailDrawer.svelte.d.ts +1 -1
  86. package/dist/components/RegisterPage.svelte +1 -1
  87. package/dist/components/RelationPicker.svelte +39 -11
  88. package/dist/components/ResourceAccessGuard.svelte +7 -2
  89. package/dist/components/RetryTaskButton.svelte +14 -1
  90. package/dist/components/RolesSettings.svelte +74 -28
  91. package/dist/components/SecuritySettings.svelte +66 -51
  92. package/dist/components/SegmentedControl.svelte +4 -2
  93. package/dist/components/SegmentedControl.svelte.d.ts +1 -1
  94. package/dist/components/Sidebar.svelte +1 -1
  95. package/dist/components/StepsForm.svelte +62 -10
  96. package/dist/components/StepsForm.svelte.d.ts +4 -2
  97. package/dist/components/SvadminLogo.svelte +1 -0
  98. package/dist/components/SvarDataGrid.svelte +42 -24
  99. package/dist/components/SvarDataGrid.svelte.d.ts +10 -10
  100. package/dist/components/SvarResourceRead.svelte +2 -0
  101. package/dist/components/SvarResourceTableView.svelte +4 -2
  102. package/dist/components/TagsInput.svelte +3 -3
  103. package/dist/components/TagsInput.svelte.d.ts +2 -2
  104. package/dist/components/TaskDetails.svelte +22 -16
  105. package/dist/components/TaskDetails.svelte.d.ts +1 -1
  106. package/dist/components/TaskList.svelte +57 -15
  107. package/dist/components/TaskQueueDrawer.svelte +54 -34
  108. package/dist/components/Toast.svelte +13 -2
  109. package/dist/components/Transfer.svelte +67 -5
  110. package/dist/components/Transfer.svelte.d.ts +4 -0
  111. package/dist/components/TreeSelect.svelte +337 -77
  112. package/dist/components/TreeSelect.svelte.d.ts +4 -0
  113. package/dist/components/TreeTable.svelte +6 -1
  114. package/dist/components/TwoFactorAuthPage.svelte +8 -4
  115. package/dist/components/UpdatePasswordPage.svelte +1 -1
  116. package/dist/components/VersionDiffViewer.svelte +9 -7
  117. package/dist/components/account/CompanyProfilePage.svelte +13 -12
  118. package/dist/components/account/MemberDirectory.svelte +14 -10
  119. package/dist/components/account/SettingsEnterprisePage.svelte +18 -18
  120. package/dist/components/content/AuditTimeline.svelte +8 -6
  121. package/dist/components/content/DataState.svelte +1 -1
  122. package/dist/components/content/FileList.svelte +44 -13
  123. package/dist/components/content/FileList.svelte.d.ts +4 -2
  124. package/dist/components/content/FileListRow.svelte +87 -0
  125. package/dist/components/content/FileListRow.svelte.d.ts +11 -0
  126. package/dist/components/content/ImportDropzone.svelte +109 -12
  127. package/dist/components/content/ImportDropzone.svelte.d.ts +1 -0
  128. package/dist/components/content/IntegrationCard.svelte +3 -3
  129. package/dist/components/content/IntegrationCard.svelte.d.ts +2 -0
  130. package/dist/components/content/MediaThumbnail.svelte +6 -2
  131. package/dist/components/detail-workspace.d.ts +60 -0
  132. package/dist/components/detail-workspace.js +3 -0
  133. package/dist/components/enterprise/json-schema-form.d.ts +1 -1
  134. package/dist/components/enterprise/recursive-schema-form.d.ts +15 -1
  135. package/dist/components/enterprise/recursive-schema-form.js +288 -14
  136. package/dist/components/enterprise/schema-form-references.d.ts +14 -0
  137. package/dist/components/enterprise/schema-form-references.js +316 -0
  138. package/dist/components/fields/ArrayField.svelte +178 -14
  139. package/dist/components/fields/ArrayField.svelte.d.ts +17 -3
  140. package/dist/components/fields/array-field-metadata.d.ts +4 -0
  141. package/dist/components/fields/array-field-metadata.js +36 -0
  142. package/dist/components/file-upload-reference.d.ts +11 -0
  143. package/dist/components/file-upload-reference.js +50 -0
  144. package/dist/components/file-upload-types.d.ts +32 -0
  145. package/dist/components/file-upload-types.js +1 -0
  146. package/dist/components/form-contract.upload-type-fixture.svelte +108 -0
  147. package/dist/components/form-contract.upload-type-fixture.svelte.d.ts +18 -0
  148. package/dist/components/json-schema-form-state.d.ts +19 -0
  149. package/dist/components/list-deletion.svelte.js +6 -1
  150. package/dist/components/list-state.svelte.d.ts +12 -1
  151. package/dist/components/list-state.svelte.js +56 -16
  152. package/dist/components/numeric-format.d.ts +44 -0
  153. package/dist/components/numeric-format.js +198 -0
  154. package/dist/components/record-detail.svelte.js +2 -0
  155. package/dist/components/saved-list-views.d.ts +9 -2
  156. package/dist/components/saved-list-views.js +42 -11
  157. package/dist/components/saved-list-views.svelte.d.ts +3 -0
  158. package/dist/components/saved-list-views.svelte.js +56 -7
  159. package/dist/components/task-action.svelte.d.ts +1 -0
  160. package/dist/components/task-action.svelte.js +55 -3
  161. package/dist/components/task-utils.d.ts +1 -0
  162. package/dist/components/task-utils.js +18 -0
  163. package/dist/components/ui/input/input.svelte +1 -1
  164. package/dist/components/ui/input/input.svelte.d.ts +1 -1
  165. package/dist/components/ui/sheet/sheet.svelte +4 -1
  166. package/dist/components/version-history.d.ts +14 -0
  167. package/dist/components/version-history.js +32 -0
  168. package/dist/components/workspace-tab-state.d.ts +78 -0
  169. package/dist/components/workspace-tab-state.js +587 -0
  170. package/dist/index.d.ts +13 -1
  171. package/dist/index.js +4 -0
  172. package/package.json +5 -5
@@ -7,6 +7,10 @@ export interface TreeSelectOption {
7
7
  expanded?: boolean;
8
8
  }
9
9
  interface Props {
10
+ id?: string;
11
+ ariaLabel?: string;
12
+ describedby?: string;
13
+ invalid?: boolean;
10
14
  options?: TreeSelectOption[];
11
15
  value?: string | number | (string | number)[];
12
16
  multiple?: boolean;
@@ -151,7 +151,12 @@
151
151
  && new Set(columns.map(column => column.key)).size === columns.length
152
152
  && (!ordering || (columns.some(column => column.key === ordering.key) && ['asc', 'desc'].includes(ordering.direction)));
153
153
  });
154
- const displayError = $derived(error || (!fullIndex.valid || !index.valid || !columnsValid ? i18n.t('treeTable.invalidData') : ''));
154
+ const hasInvalidData = $derived(!fullIndex.valid || !index.valid || !columnsValid);
155
+ // 宿主传入的 error 可能包含 provider/internal diagnostic,只映射为稳定本地化文案;
156
+ // 本地数据/列校验失败优先保留更具体、可操作的 invalidData 说明。
157
+ const displayError = $derived(hasInvalidData
158
+ ? i18n.t('treeTable.invalidData')
159
+ : (error ? i18n.t('common.operationFailed') : ''));
155
160
  const available = $derived(!loading && !displayError);
156
161
  const selection = $derived(treeTableSelection(fullIndex.nodes, new Set(selectedKeys)));
157
162
  function isExpanded(id: TreeTableId): boolean {
@@ -18,6 +18,7 @@
18
18
  let { enrollmentProvider }: { enrollmentProvider?: MfaEnrollmentProvider } = $props();
19
19
  const i18n = useTranslation();
20
20
  const context = captureAdminContext();
21
+ const controlId = $props.id();
21
22
  const isZh = $derived(i18n.locale === 'zh-CN');
22
23
  let secret = $state('');
23
24
  let code = $state('');
@@ -34,6 +35,9 @@
34
35
  secret = ''; code = ''; recoveryCodes = null; pending = false; error = ''; copied = false;
35
36
  return () => { epoch += 1; };
36
37
  });
38
+ function failureMessage(): string {
39
+ return i18n.t('common.operationFailed');
40
+ }
37
41
  async function enroll(verify: boolean) {
38
42
  const provider = enrollmentProvider;
39
43
  if (!provider || pending) return;
@@ -55,8 +59,8 @@
55
59
  if (!result.secret) throw new Error(isZh ? '未返回登记密钥' : 'No enrollment secret returned');
56
60
  secret = result.secret;
57
61
  }
58
- } catch (caught) {
59
- if (request === epoch) error = caught instanceof Error ? caught.message : String(caught);
62
+ } catch {
63
+ if (request === epoch) error = failureMessage();
60
64
  } finally {
61
65
  if (request === epoch) pending = false;
62
66
  }
@@ -87,8 +91,8 @@
87
91
  <p>{isZh ? '将此登记密钥添加到身份验证器,然后输入验证码。' : 'Add this enrollment secret to your authenticator, then enter its verification code.'}</p>
88
92
  <code>{secret}</code>
89
93
  <form onsubmit={(event) => { event.preventDefault(); void enroll(true); }}>
90
- <label for="mfa-code">{i18n.t('auth.twoFactorEnterCode')}</label>
91
- <Input id="mfa-code" bind:value={code} inputmode="numeric" autocomplete="one-time-code" maxlength={6} disabled={pending} />
94
+ <label for={`${controlId}-mfa-code`}>{i18n.t('auth.twoFactorEnterCode')}</label>
95
+ <Input id={`${controlId}-mfa-code`} bind:value={code} inputmode="numeric" autocomplete="one-time-code" maxlength={6} disabled={pending} />
92
96
  <Button type="submit" disabled={pending}>{pending ? i18n.t('common.loading') : i18n.t('common.confirm')}</Button>
93
97
  </form>
94
98
  {:else}
@@ -30,7 +30,7 @@
30
30
 
31
31
  const result = await updatePw.mutate({ password, confirmPassword });
32
32
  if (!result.success) {
33
- error = result.error?.message ?? i18n.t('common.operationFailed');
33
+ error = i18n.t('common.operationFailed');
34
34
  }
35
35
  }
36
36
  </script>
@@ -82,31 +82,33 @@
82
82
 
83
83
  <div class={cn('svadmin-u-a217b4eaa918 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 svadmin-u-cef5b893cf23 svadmin-u-2cd02d11d1af', className)}>
84
84
  <!-- Header & Toolbar -->
85
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-56796a90bec5 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff svadmin-u-967d113a1451">
86
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
85
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-56796a90bec5 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff svadmin-u-967d113a1451 svadmin-u-1eb5c6df38c1">
86
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4 svadmin-u-7e0b7cdf1a94">
87
87
  <h4 class="svadmin-u-359090c2d529 svadmin-u-e83a7042bc91 svadmin-u-d4108abe6359">Record Comparison</h4>
88
88
  <Badge variant="outline" class="svadmin-u-d058ca6de60f">
89
89
  {modifiedCount} change{modifiedCount === 1 ? '' : 's'}
90
90
  </Badge>
91
91
  </div>
92
92
 
93
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421">
93
+ <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-44ee8ba0a421 svadmin-u-1eb5c6df38c1">
94
94
  <Button
95
95
  variant={mode === 'split' ? 'secondary' : 'ghost'}
96
96
  size="sm"
97
- class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421 svadmin-u-d5eab218aa34"
97
+ class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421 svadmin-u-d5eab218aa34 svadmin-u-012fbd121f37"
98
+ aria-pressed={mode === 'split'}
98
99
  onclick={() => { mode = 'split'; }}
99
100
  >
100
- <Columns2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
101
+ <Columns2 class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" aria-hidden="true" />
101
102
  Side by Side
102
103
  </Button>
103
104
  <Button
104
105
  variant={mode === 'unified' ? 'secondary' : 'ghost'}
105
106
  size="sm"
106
- class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421 svadmin-u-d5eab218aa34"
107
+ class="svadmin-u-d0a52b312f7d svadmin-u-359090c2d529 svadmin-u-44ee8ba0a421 svadmin-u-d5eab218aa34 svadmin-u-012fbd121f37"
108
+ aria-pressed={mode === 'unified'}
107
109
  onclick={() => { mode = 'unified'; }}
108
110
  >
109
- <AlignJustify class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
111
+ <AlignJustify class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" aria-hidden="true" />
110
112
  Unified
111
113
  </Button>
112
114
  </div>
@@ -10,28 +10,29 @@
10
10
 
11
11
  const context = captureAdminContext();
12
12
  const i18n = useTranslation();
13
+ const controlId = $props.id();
13
14
  const isZh = $derived(i18n.locale === 'zh-CN');
14
15
  const provider = $derived(context.organizationProvider);
15
16
  let organization = $state<Organization | null>(null);
16
17
  let name = $state('');
17
18
  let loading = $state(false);
18
19
  let saving = $state(false);
19
- let error = $state('');
20
- let loadError = $state('');
20
+ let error = $state(false);
21
+ let loadError = $state(false);
21
22
  let status = $state('');
22
23
  let epoch = 0;
23
24
  async function load() {
24
25
  const current = provider;
25
26
  const request = ++epoch;
26
- organization = null; name = ''; error = ''; loadError = ''; status = ''; saving = false;
27
+ organization = null; name = ''; error = false; loadError = false; status = ''; saving = false;
27
28
  loading = Boolean(current);
28
29
  if (!current) return;
29
30
  try {
30
31
  const result = await current.getCurrentOrganization(context.enterpriseRequestContext);
31
32
  if (request !== epoch) return;
32
33
  organization = result; name = result?.name ?? '';
33
- } catch (caught) {
34
- if (request === epoch) loadError = caught instanceof Error ? caught.message : String(caught);
34
+ } catch {
35
+ if (request === epoch) loadError = true;
35
36
  } finally {
36
37
  if (request === epoch) loading = false;
37
38
  }
@@ -46,14 +47,14 @@
46
47
  const current = provider;
47
48
  if (!current?.updateCurrentOrganization || !organization || !name.trim() || saving) return;
48
49
  const request = epoch;
49
- saving = true; error = ''; status = '';
50
+ saving = true; error = false; status = '';
50
51
  try {
51
52
  const result = await current.updateCurrentOrganization({ name: name.trim() }, context.enterpriseRequestContext);
52
53
  if (request !== epoch) return;
53
54
  organization = result; name = result.name;
54
55
  status = isZh ? '组织信息已保存' : 'Organization saved';
55
- } catch (caught) {
56
- if (request === epoch) error = caught instanceof Error ? caught.message : String(caught);
56
+ } catch {
57
+ if (request === epoch) error = true;
57
58
  } finally {
58
59
  if (request === epoch) saving = false;
59
60
  }
@@ -67,16 +68,16 @@
67
68
  {:else if loading}
68
69
  <DataState state="loading" />
69
70
  {:else if loadError}
70
- <DataState state="error" description={loadError} retry={load} />
71
+ <DataState state="error" description={i18n.t('common.operationFailed')} retry={load} />
71
72
  {:else if !organization}
72
73
  <DataState state="empty" title={isZh ? '暂无组织资料' : 'No organization details'} />
73
74
  {:else}
74
75
  <form onsubmit={save}>
75
- <label for="company-name">{isZh ? '组织名称' : 'Organization name'}</label>
76
- <Input id="company-name" bind:value={name} required disabled={saving || !provider.updateCurrentOrganization} />
76
+ <label for={`${controlId}-company-name`}>{isZh ? '组织名称' : 'Organization name'}</label>
77
+ <Input id={`${controlId}-company-name`} bind:value={name} required disabled={saving || !provider.updateCurrentOrganization} />
77
78
  {#if !provider.updateCurrentOrganization}<p>{isZh ? '当前组织资料为只读。' : 'Organization details are read-only.'}</p>{/if}
78
79
  <Button type="submit" disabled={saving || !provider.updateCurrentOrganization || !name.trim() || name.trim() === organization.name}>{saving ? i18n.t('common.loading') : i18n.t('common.save')}</Button>
79
- {#if error}<p role="alert">{error}</p>{/if}
80
+ {#if error}<p role="alert">{i18n.t('common.operationFailed')}</p>{/if}
80
81
  {#if status}<p role="status">{status}</p>{/if}
81
82
  </form>
82
83
  {/if}
@@ -14,6 +14,7 @@
14
14
  }
15
15
  </script>
16
16
  <script lang="ts">
17
+ import { untrack } from 'svelte';
17
18
  import { captureAdminContext } from '@svadmin/core';
18
19
  import { useTranslation } from '@svadmin/core/i18n';
19
20
  import { Button } from '../ui/button/index.js';
@@ -34,7 +35,8 @@
34
35
  let loading = $state(false);
35
36
  let sending = $state(false);
36
37
  let error = $state('');
37
- let loadError = $state('');
38
+ let requestError = $state(false);
39
+ let loadError = $state(false);
38
40
  let status = $state('');
39
41
  let epoch = 0;
40
42
  const filtered = $derived(rows.filter(row => `${row.name} ${row.email}`.toLowerCase().includes(query.toLowerCase())));
@@ -43,22 +45,23 @@
43
45
  searchInput?.focus();
44
46
  }
45
47
  async function load() {
48
+ if (untrack(() => sending)) return;
46
49
  const current = provider;
47
50
  const request = ++epoch;
48
- rows = []; loadError = ''; loading = Boolean(current);
51
+ rows = []; loadError = false; requestError = false; loading = Boolean(current);
49
52
  if (!current) return;
50
53
  try {
51
54
  const result = await current.list();
52
55
  if (request === epoch) rows = result;
53
- } catch (caught) {
54
- if (request === epoch) loadError = caught instanceof Error ? caught.message : String(caught);
56
+ } catch {
57
+ if (request === epoch) loadError = true;
55
58
  } finally {
56
59
  if (request === epoch) loading = false;
57
60
  }
58
61
  }
59
62
  $effect(() => {
60
63
  void provider; void context.authProvider; void context.tenantCacheKey?.__svadminTenant;
61
- email = ''; query = ''; error = ''; status = ''; sending = false;
64
+ email = ''; query = ''; error = ''; requestError = false; status = ''; sending = false;
62
65
  void load();
63
66
  return () => { epoch += 1; };
64
67
  });
@@ -66,6 +69,7 @@
66
69
  event.preventDefault();
67
70
  const current = provider;
68
71
  if (!current?.invite || sending || loading) return;
72
+ requestError = false;
69
73
  if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email.trim())) {
70
74
  error = isZh ? '请输入有效邮箱' : 'Enter a valid email address';
71
75
  return;
@@ -77,8 +81,8 @@
77
81
  if (request !== epoch) return;
78
82
  email = '';
79
83
  status = isZh ? '邀请已提交' : 'Invitation submitted';
80
- } catch (caught) {
81
- if (request === epoch) error = caught instanceof Error ? caught.message : String(caught);
84
+ } catch {
85
+ if (request === epoch) requestError = true;
82
86
  } finally {
83
87
  if (request === epoch) sending = false;
84
88
  }
@@ -86,7 +90,7 @@
86
90
  async function copyLink() {
87
91
  if (!provider?.invitationUrl) return;
88
92
  const request = epoch;
89
- status = ''; error = '';
93
+ status = ''; error = ''; requestError = false;
90
94
  try {
91
95
  await navigator.clipboard.writeText(provider.invitationUrl);
92
96
  if (request === epoch) status = isZh ? '链接已复制' : 'Link copied';
@@ -113,12 +117,12 @@
113
117
  <p class="text-sm text-muted-foreground">{isZh ? '当前目录为只读,未配置邀请发送能力。' : 'This directory is read-only. Invitation delivery is not configured.'}</p>
114
118
  {/if}
115
119
  {#if provider.invitationUrl}<div><Button variant="outline" onclick={copyLink}><Copy size={16} />{i18n.t('account.copyLink')}</Button></div>{/if}
116
- {#if error}<p role="alert" class="text-sm text-destructive">{error}</p>{/if}
120
+ {#if requestError || error}<p role="alert" class="text-sm text-destructive">{requestError ? i18n.t('common.operationFailed') : error}</p>{/if}
117
121
  {#if status}<p role="status" class="text-sm text-foreground">{status}</p>{/if}
118
122
  {#if loading}
119
123
  <DataState state="loading" />
120
124
  {:else if loadError}
121
- <DataState state="error" description={loadError} retry={load} />
125
+ <DataState state="error" description={i18n.t('common.operationFailed')} {...(sending ? {} : { retry: load })} />
122
126
  {:else}
123
127
  <div class="w-full max-w-sm space-y-2">
124
128
  <label for={`${controlId}-search`} class="block text-sm font-medium text-foreground">{isZh ? '搜索成员' : 'Search members'}</label>
@@ -43,7 +43,7 @@
43
43
  let saving = $state(false);
44
44
  let savingOrganization = $state(false);
45
45
  let testing = $state(false);
46
- let error = $state<string | null>(null);
46
+ let error = $state(false);
47
47
  let requestId = 0;
48
48
  const organizationChanged = $derived(Boolean(
49
49
  organizationProvider?.updateCurrentOrganization
@@ -58,8 +58,8 @@
58
58
  const auditRetentionValid = $derived(isPositiveInteger(auditRetentionValue));
59
59
  const policyNumbersValid = $derived(sessionTimeoutValid && auditRetentionValid);
60
60
 
61
- function message(error: unknown): string {
62
- return error instanceof Error ? error.message : String(error);
61
+ function failureMessage(): string {
62
+ return i18n.t('common.operationFailed');
63
63
  }
64
64
 
65
65
  async function loadSettings(): Promise<void> {
@@ -74,7 +74,7 @@
74
74
  auditRetentionValue = '';
75
75
  identityProviders = [];
76
76
  events = [];
77
- error = null;
77
+ error = false;
78
78
  loading = false;
79
79
  saving = false;
80
80
  savingOrganization = false;
@@ -97,8 +97,8 @@
97
97
  organization = nextOrganization;
98
98
  organizationName = nextOrganization?.name ?? '';
99
99
  metadataUrl = nextIdentityProviders[0]?.metadataUrl ?? '';
100
- } catch (caught) {
101
- if (currentRequest === requestId) error = message(caught);
100
+ } catch {
101
+ if (currentRequest === requestId) error = true;
102
102
  } finally {
103
103
  if (currentRequest === requestId) loading = false;
104
104
  }
@@ -116,7 +116,7 @@
116
116
  const provider = identityProvider;
117
117
  const currentRequest = requestId;
118
118
  const context = adminContext.enterpriseRequestContext;
119
- if (!provider || !policy || !policyNumbersValid) return;
119
+ if (!provider || !policy || !policyNumbersValid || saving) return;
120
120
  saving = true;
121
121
  try {
122
122
  const updatedPolicy = await provider.updateSecurityPolicy({
@@ -129,9 +129,9 @@
129
129
  sessionTimeoutValue = String(updatedPolicy.sessionTimeoutMinutes);
130
130
  auditRetentionValue = String(updatedPolicy.auditRetentionDays);
131
131
  notifyWithProvider({ type: 'success', message: isZh ? '企业策略已保存' : 'Enterprise policy saved' }, adminContext.notificationProvider);
132
- } catch (caught) {
132
+ } catch {
133
133
  if (currentRequest !== requestId) return;
134
- notifyWithProvider({ type: 'error', message: message(caught) }, adminContext.notificationProvider);
134
+ notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
135
135
  } finally {
136
136
  if (currentRequest === requestId) saving = false;
137
137
  }
@@ -142,7 +142,7 @@
142
142
  const currentRequest = requestId;
143
143
  const context = adminContext.enterpriseRequestContext;
144
144
  const name = organizationName.trim();
145
- if (!provider?.updateCurrentOrganization || !organization || !name || name === organization.name) return;
145
+ if (!provider?.updateCurrentOrganization || !organization || !name || name === organization.name || savingOrganization) return;
146
146
  savingOrganization = true;
147
147
  try {
148
148
  const updatedOrganization = await provider.updateCurrentOrganization({ name }, context);
@@ -150,9 +150,9 @@
150
150
  organization = updatedOrganization;
151
151
  organizationName = updatedOrganization.name;
152
152
  notifyWithProvider({ type: 'success', message: isZh ? '组织信息已保存' : 'Organization saved' }, adminContext.notificationProvider);
153
- } catch (caught) {
153
+ } catch {
154
154
  if (currentRequest !== requestId) return;
155
- notifyWithProvider({ type: 'error', message: message(caught) }, adminContext.notificationProvider);
155
+ notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
156
156
  } finally {
157
157
  if (currentRequest === requestId) savingOrganization = false;
158
158
  }
@@ -162,7 +162,7 @@
162
162
  const provider = identityProvider;
163
163
  const currentRequest = requestId;
164
164
  const context = adminContext.enterpriseRequestContext;
165
- if (!provider) return;
165
+ if (!provider || testing || loading || error) return;
166
166
  testing = true;
167
167
  try {
168
168
  const selectedProvider = target ?? identityProviders[0];
@@ -178,11 +178,11 @@
178
178
  type: result.success ? 'success' : 'error',
179
179
  message: result.success
180
180
  ? (isZh ? '身份源连接测试通过' : 'Identity provider connection succeeded')
181
- : (result.error?.message ?? (isZh ? '身份源连接测试失败' : 'Identity provider connection failed')),
181
+ : failureMessage(),
182
182
  }, adminContext.notificationProvider);
183
- } catch (caught) {
183
+ } catch {
184
184
  if (currentRequest !== requestId) return;
185
- notifyWithProvider({ type: 'error', message: message(caught) }, adminContext.notificationProvider);
185
+ notifyWithProvider({ type: 'error', message: failureMessage() }, adminContext.notificationProvider);
186
186
  } finally {
187
187
  if (currentRequest === requestId) testing = false;
188
188
  }
@@ -214,7 +214,7 @@
214
214
  {#if loading}
215
215
  <DataState state="loading" />
216
216
  {:else if error}
217
- <DataState state="error" title={isZh ? '企业设置加载失败' : 'Enterprise settings failed to load'} description={error} retry={loadSettings} />
217
+ <DataState state="error" title={isZh ? '企业设置加载失败' : 'Enterprise settings failed to load'} description={failureMessage()} retry={loadSettings} />
218
218
  {:else}
219
219
  <div class="svadmin-u-f3c543ad5fe9 svadmin-u-60541e1e26f8 svadmin-u-0c3bc98565dd svadmin-u-2f27a80ed92b">
220
220
  <SettingsGroup title={i18n.t('account.securityPolicy')} description={isZh ? '配置组织级身份、会话与审计策略。' : 'Configure organization-wide identity, session, and audit controls.'} bodyClass="space-y-4">
@@ -244,7 +244,7 @@
244
244
  <DataState state="empty" title={isZh ? '尚未配置身份源' : 'No identity provider configured'} description={isZh ? '请在后端 IdentityGovernanceProvider 中创建并验证身份源。' : 'Create and verify an identity provider through the backend IdentityGovernanceProvider.'} />
245
245
  {:else}
246
246
  {#each identityProviders as connection (connection.id)}
247
- <IntegrationCard integration={definedOptions({ id: connection.id, name: connection.name, account: connection.protocol.toUpperCase(), connected: connection.status === 'connected', description: connection.domain ?? connection.metadataUrl })} onconnect={() => testConnection(connection)} />
247
+ <IntegrationCard integration={definedOptions({ id: connection.id, name: connection.name, account: connection.protocol.toUpperCase(), connected: connection.status === 'connected', description: connection.domain ?? connection.metadataUrl })} actionLabel={i18n.t('common.test')} disabled={testing} onconnect={() => testConnection(connection)} />
248
248
  {/each}
249
249
  {/if}
250
250
  </section>
@@ -16,6 +16,7 @@
16
16
  <script lang="ts">
17
17
  import type { Snippet } from 'svelte';
18
18
  import { definedOptions } from '@svadmin/core/options';
19
+ import { useTranslation } from '@svadmin/core/i18n';
19
20
  import { cn } from '../../utils.js';
20
21
  import StatusBadge from './StatusBadge.svelte';
21
22
  import DataState from './DataState.svelte';
@@ -37,8 +38,8 @@
37
38
 
38
39
  let {
39
40
  items = [],
40
- emptyTitle = 'No timeline events',
41
- emptyDescription = 'Activity events and status transitions will appear here.',
41
+ emptyTitle,
42
+ emptyDescription,
42
43
  state,
43
44
  stateTitle,
44
45
  stateDescription,
@@ -49,6 +50,7 @@
49
50
  class: className = '',
50
51
  }: Props = $props();
51
52
 
53
+ const i18n = useTranslation();
52
54
  const dotClass: Record<Status, string> = {
53
55
  success: 'svadmin-u-3355648fe22b svadmin-u-18a6e7a36f29 svadmin-u-76747e5e02ff',
54
56
  warning: 'svadmin-u-a486cacb3a15 svadmin-u-2f960aa0c478 svadmin-u-3a4ff758c2ab',
@@ -64,8 +66,8 @@
64
66
  <DataState
65
67
  state={resolvedState}
66
68
  {...definedOptions({
67
- title: stateTitle ?? (resolvedState === 'empty' ? emptyTitle : undefined),
68
- description: stateDescription ?? (resolvedState === 'empty' ? emptyDescription : undefined),
69
+ title: stateTitle ?? (resolvedState === 'empty' ? emptyTitle ?? i18n.t('audit.emptyTitle') : undefined),
70
+ description: stateDescription ?? (resolvedState === 'empty' ? emptyDescription ?? i18n.t('audit.emptyDescription') : undefined),
69
71
  retry,
70
72
  retryLabel,
71
73
  loadingLabel,
@@ -89,10 +91,10 @@
89
91
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
90
92
  <span class="svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{item.title}</span>
91
93
  {#if item.actor}
92
- <span class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">by {item.actor}</span>
94
+ <span class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">{i18n.t('audit.actorPrefix')}{item.actor}</span>
93
95
  {/if}
94
96
  {#if item.status}
95
- <StatusBadge status={item.status} label={item.tag ?? item.status} class="svadmin-u-d058ca6de60f svadmin-u-68ecb30dbec6 svadmin-u-11e59c6d5f6b" />
97
+ <StatusBadge status={item.status} label={item.tag ?? i18n.t(`audit.status.${item.status}`)} class="svadmin-u-d058ca6de60f svadmin-u-68ecb30dbec6 svadmin-u-11e59c6d5f6b" />
96
98
  {:else if item.tag}
97
99
  <span class="svadmin-u-52083e7da442 svadmin-u-3960ffc248d9 svadmin-u-07389a777c1f svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-45d828117213 svadmin-u-465609a240a8 svadmin-u-d058ca6de60f svadmin-u-2689f3958069 svadmin-u-bfa603190748 svadmin-u-2ef11f1cb219">
98
100
  {item.tag}
@@ -32,7 +32,7 @@
32
32
  loading: [i18n.t('common.loading'), i18n.t('common.loading')],
33
33
  empty: [i18n.t('empty.title'), i18n.t('empty.description')],
34
34
  'filtered-empty': [isZh ? '没有匹配结果' : 'No matching results', isZh ? '请调整或清除筛选条件。' : 'Adjust or clear your filters.'],
35
- error: [i18n.t('common.error'), i18n.t('common.retry')],
35
+ error: [i18n.t('common.error'), i18n.t('common.operationFailed')],
36
36
  forbidden: [isZh ? '无权访问' : 'Access restricted', isZh ? '你没有查看此内容的权限。' : 'You do not have permission to view this content.'],
37
37
  } as const);
38
38
  const resolvedTitle = $derived(title ?? defaults[state][0]);
@@ -1,23 +1,54 @@
1
1
  <script lang="ts">
2
2
  import { definedOptions } from '@svadmin/core/options';
3
-
4
- import { Download, File, Trash2 } from '@lucide/svelte';
5
- import { useTranslation } from '@svadmin/core/i18n';
6
- import { Button } from '../ui/button/index.js';
7
3
  import DataState from './DataState.svelte';
8
4
  import type { DataStateKind } from './DataState.svelte';
5
+ import FileListRow from './FileListRow.svelte';
6
+
9
7
  export interface FileItem { id?: string; name: string; size?: string; type?: string; updatedAt?: string; }
10
- interface Props { files?: FileItem[]; state?: DataStateKind; stateTitle?: string; stateDescription?: string; emptyTitle?: string; emptyDescription?: string; retry?: () => void; retryLabel?: string; loadingLabel?: string; ondownload?: (file: FileItem) => void; onremove?: (file: FileItem) => void; class?: string; }
11
- const i18n = useTranslation();
12
- let { files = [], state, stateTitle, stateDescription, emptyTitle, emptyDescription, retry, retryLabel, loadingLabel, ondownload, onremove, class: className = '' }: Props = $props();
8
+ type FileActionHandler = (file: FileItem) => void;
9
+ interface Props {
10
+ files?: FileItem[];
11
+ state?: DataStateKind;
12
+ stateTitle?: string;
13
+ stateDescription?: string;
14
+ emptyTitle?: string;
15
+ emptyDescription?: string;
16
+ retry?: () => void;
17
+ retryLabel?: string;
18
+ loadingLabel?: string;
19
+ ondownload?: FileActionHandler;
20
+ onpreview?: FileActionHandler;
21
+ onremove?: FileActionHandler;
22
+ class?: string;
23
+ }
24
+ let {
25
+ files = [], state, stateTitle, stateDescription, emptyTitle, emptyDescription,
26
+ retry, retryLabel, loadingLabel, ondownload, onpreview, onremove, class: className = '',
27
+ }: Props = $props();
13
28
  const resolvedState = $derived(state ?? (files.length === 0 ? 'empty' : undefined));
29
+ const isEmptyState = $derived(resolvedState === 'empty' || resolvedState === 'filtered-empty');
30
+
31
+ const rows = $derived.by(() => {
32
+ const occurrences = new Map<string, number>();
33
+ return files.map((file, index) => {
34
+ if (file.id === undefined) return { file, key: JSON.stringify(['index', index, file.name]) };
35
+ const occurrence = occurrences.get(file.id) ?? 0;
36
+ occurrences.set(file.id, occurrence + 1);
37
+ return { file, key: JSON.stringify(['id', file.id, occurrence]) };
38
+ });
39
+ });
14
40
  </script>
41
+
15
42
  {#if resolvedState}
16
- <DataState state={resolvedState} {...definedOptions({ title: stateTitle ?? emptyTitle, description: stateDescription ?? emptyDescription, retry, retryLabel, loadingLabel })} class={className} />
43
+ <DataState state={resolvedState} {...definedOptions({
44
+ title: stateTitle ?? (isEmptyState ? emptyTitle : undefined),
45
+ description: stateDescription ?? (isEmptyState ? emptyDescription : undefined),
46
+ retry, retryLabel, loadingLabel,
47
+ })} class={className} />
17
48
  {:else}
18
- <div class={'svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3 svadmin-u-2cd02d11d1af svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 ' + className}>
19
- {#each files as file, index (file.id ?? file.name + '-' + index)}
20
- <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-1004c0c3954c svadmin-u-f0faeb26d656 svadmin-u-1b2d54a3fd12"><span class="svadmin-u-60fbb7713999 svadmin-u-665f07fe73cc svadmin-u-012fbd121f37 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-421ac2be5045 svadmin-u-2ef11f1cb219 svadmin-u-bfa603190748"><File class="svadmin-u-f7b5fa971871" /></span><div class="svadmin-u-7e0b7cdf1a94 svadmin-u-36e579c0b41c"><p class="svadmin-u-f283ea9bea0e svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359">{file.name}</p><p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">{file.size ?? file.type ?? (i18n.locale === 'zh-CN' ? '文件' : 'File')}{file.updatedAt ? ' / ' + file.updatedAt : ''}</p></div>{#if ondownload}<Button variant="ghost" size="icon-sm" aria-label={(i18n.locale === 'zh-CN' ? '下载 ' : 'Download ') + file.name} onclick={() => ondownload?.(file)}><Download class="svadmin-u-f7b5fa971871" /></Button>{/if}{#if onremove}<Button variant="ghost" size="icon-sm" aria-label={(i18n.locale === 'zh-CN' ? '移除 ' : 'Remove ') + file.name} onclick={() => onremove?.(file)}><Trash2 class="svadmin-u-f7b5fa971871" /></Button>{/if}</div>
21
- {/each}
22
- </div>
49
+ <div class={'svadmin-u-fa6acbf81d74 svadmin-u-e783642739e3 svadmin-u-2cd02d11d1af svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-18049387f0af svadmin-u-cd0ad9a56558 ' + className} role="list" data-svadmin-file-list>
50
+ {#each rows as { file, key } (key)}
51
+ <FileListRow {file} {...definedOptions({ ondownload, onpreview, onremove })} />
52
+ {/each}
53
+ </div>
23
54
  {/if}
@@ -6,6 +6,7 @@ export interface FileItem {
6
6
  type?: string;
7
7
  updatedAt?: string;
8
8
  }
9
+ type FileActionHandler = (file: FileItem) => void;
9
10
  interface Props {
10
11
  files?: FileItem[];
11
12
  state?: DataStateKind;
@@ -16,8 +17,9 @@ interface Props {
16
17
  retry?: () => void;
17
18
  retryLabel?: string;
18
19
  loadingLabel?: string;
19
- ondownload?: (file: FileItem) => void;
20
- onremove?: (file: FileItem) => void;
20
+ ondownload?: FileActionHandler;
21
+ onpreview?: FileActionHandler;
22
+ onremove?: FileActionHandler;
21
23
  class?: string;
22
24
  }
23
25
  declare const FileList: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,87 @@
1
+ <script lang="ts">
2
+ import { onDestroy, untrack } from 'svelte';
3
+ import { Download, Eye, File, Trash2 } from '@lucide/svelte';
4
+ import { useTranslation } from '@svadmin/core/i18n';
5
+ import { Button } from '../ui/button/index.js';
6
+ import type { FileItem } from './FileList.svelte';
7
+
8
+ type Action = 'download' | 'preview' | 'remove';
9
+ type Handler = (file: FileItem) => void;
10
+ interface Props { file: FileItem; ondownload?: Handler; onpreview?: Handler; onremove?: Handler; }
11
+ let { file, ondownload, onpreview, onremove }: Props = $props();
12
+ const i18n = useTranslation();
13
+ const instanceId = $props.id();
14
+ const errorId = `${instanceId}-error`;
15
+ let pending = $state<Action | undefined>();
16
+ let failed = $state<Action | undefined>();
17
+ let generation = 0;
18
+ let destroyed = false;
19
+ let observed = untrack(() => ({ file, ondownload, onpreview, onremove }));
20
+
21
+ $effect.pre(() => {
22
+ if (file === observed.file && ondownload === observed.ondownload
23
+ && onpreview === observed.onpreview && onremove === observed.onremove) return;
24
+ observed = { file, ondownload, onpreview, onremove };
25
+ generation += 1;
26
+ pending = undefined;
27
+ failed = undefined;
28
+ });
29
+
30
+ onDestroy(() => { destroyed = true; generation += 1; });
31
+
32
+ function label(action: Action): string {
33
+ return i18n.t(action === 'download' ? 'common.download'
34
+ : action === 'preview' ? 'common.preview' : 'common.remove');
35
+ }
36
+
37
+ async function run(action: Action): Promise<void> {
38
+ if (destroyed || pending) return;
39
+ const handler = action === 'download' ? ondownload : action === 'preview' ? onpreview : onremove;
40
+ if (!handler) return;
41
+ const owner = { file, ondownload, onpreview, onremove };
42
+ const current = ++generation;
43
+ const isCurrent = () => !destroyed && generation === current && file === owner.file
44
+ && ondownload === owner.ondownload && onpreview === owner.onpreview && onremove === owner.onremove;
45
+ pending = action;
46
+ failed = undefined;
47
+ try {
48
+ await handler(file);
49
+ } catch {
50
+ if (isCurrent()) failed = action;
51
+ } finally {
52
+ if (isCurrent()) pending = undefined;
53
+ }
54
+ }
55
+ </script>
56
+
57
+ <div class="file-list-row svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-1004c0c3954c svadmin-u-f0faeb26d656 svadmin-u-1b2d54a3fd12"
58
+ role="listitem" aria-label={file.name} aria-busy={Boolean(pending)}>
59
+ <span class="svadmin-u-60fbb7713999 svadmin-u-665f07fe73cc svadmin-u-012fbd121f37 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-421ac2be5045 svadmin-u-2ef11f1cb219 svadmin-u-bfa603190748" aria-hidden="true"><File class="svadmin-u-f7b5fa971871" /></span>
60
+ <div class="svadmin-u-7e0b7cdf1a94 svadmin-u-36e579c0b41c">
61
+ <p class="svadmin-u-f283ea9bea0e svadmin-u-fc7473ca09eb svadmin-u-2689f3958069 svadmin-u-d4108abe6359" title={file.name}>{file.name}</p>
62
+ {#if pending}
63
+ <p class="svadmin-u-359090c2d529 svadmin-u-bfa603190748" role="status">{i18n.t('common.loading')}</p>
64
+ {:else}
65
+ <p class="file-list-row-meta svadmin-u-359090c2d529 svadmin-u-bfa603190748">{file.size ?? file.type ?? i18n.t('common.file')}{file.updatedAt ? ' / ' + file.updatedAt : ''}</p>
66
+ {/if}
67
+ {#if failed}<p class="file-list-row-error" role="alert" id={errorId}>{label(failed)}: {i18n.t('common.error')}</p>{/if}
68
+ </div>
69
+ {#if onpreview}<Button type="button" variant="ghost" size="icon-sm"
70
+ aria-label={`${label('preview')} ${file.name}`} title={`${label('preview')} ${file.name}`}
71
+ aria-describedby={failed ? errorId : undefined} disabled={Boolean(pending)}
72
+ onclick={() => { void run('preview'); }}><Eye aria-hidden="true" class="svadmin-u-f7b5fa971871" /></Button>{/if}
73
+ {#if ondownload}<Button type="button" variant="ghost" size="icon-sm"
74
+ aria-label={`${label('download')} ${file.name}`} title={`${label('download')} ${file.name}`}
75
+ aria-describedby={failed ? errorId : undefined} disabled={Boolean(pending)}
76
+ onclick={() => { void run('download'); }}><Download aria-hidden="true" class="svadmin-u-f7b5fa971871" /></Button>{/if}
77
+ {#if onremove}<Button type="button" variant="ghost" size="icon-sm"
78
+ aria-label={`${label('remove')} ${file.name}`} title={`${label('remove')} ${file.name}`}
79
+ aria-describedby={failed ? errorId : undefined} disabled={Boolean(pending)}
80
+ onclick={() => { void run('remove'); }}><Trash2 aria-hidden="true" class="svadmin-u-f7b5fa971871" /></Button>{/if}
81
+ </div>
82
+
83
+ <style>
84
+ .file-list-row-error { margin: 4px 0 0; font-size: 14px; line-height: 20px; color: var(--destructive); overflow-wrap: anywhere; }
85
+ .file-list-row-meta { overflow-wrap: anywhere; }
86
+ .file-list-row :global(button) { flex-shrink: 0; }
87
+ </style>