@svadmin/ui 0.72.0 → 0.74.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 (276) hide show
  1. package/README.md +62 -22
  2. package/THIRD_PARTY_NOTICES.md +52 -0
  3. package/dist/app.css +8488 -6297
  4. package/dist/app.theme.css +8514 -6374
  5. package/dist/class-properties.generated.d.ts +1 -0
  6. package/dist/class-properties.generated.js +2 -0
  7. package/dist/classnames.d.ts +8 -0
  8. package/dist/classnames.js +26 -0
  9. package/dist/component-registry.d.ts +14977 -0
  10. package/dist/component-registry.js +216 -0
  11. package/dist/components/AboutSettings.svelte +7 -1
  12. package/dist/components/AboutSettings.svelte.d.ts +4 -1
  13. package/dist/components/ActivityFeed.svelte +9 -3
  14. package/dist/components/ActivityFeed.svelte.d.ts +3 -0
  15. package/dist/components/AdminApp.svelte +19 -10
  16. package/dist/components/AdminApp.svelte.d.ts +6 -0
  17. package/dist/components/ApiSettings.svelte +41 -5
  18. package/dist/components/ApprovalCenter.svelte +283 -0
  19. package/dist/components/ApprovalCenter.svelte.d.ts +10 -0
  20. package/dist/components/AuditLogViewer.svelte +3 -1
  21. package/dist/components/AutoForm.svelte +53 -9
  22. package/dist/components/AutoForm.svelte.d.ts +3 -0
  23. package/dist/components/AutoTable.svelte +846 -43
  24. package/dist/components/AutoTable.svelte.d.ts +22 -1
  25. package/dist/components/BoundRecordDetailDrawer.svelte +17 -2
  26. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +4 -0
  27. package/dist/components/Breadcrumbs.svelte +11 -1
  28. package/dist/components/CancelTaskButton.svelte +12 -42
  29. package/dist/components/ColorPicker.svelte +73 -0
  30. package/dist/components/ColorPicker.svelte.d.ts +15 -0
  31. package/dist/components/ComboboxField.svelte +143 -34
  32. package/dist/components/ComboboxField.svelte.d.ts +4 -2
  33. package/dist/components/CreatePage.svelte +4 -0
  34. package/dist/components/CreatePage.svelte.d.ts +2 -0
  35. package/dist/components/DashboardView.svelte +99 -0
  36. package/dist/components/DashboardView.svelte.d.ts +14 -0
  37. package/dist/components/DatePicker.svelte +10 -0
  38. package/dist/components/DatePicker.svelte.d.ts +6 -0
  39. package/dist/components/DateRangeInput.svelte +155 -0
  40. package/dist/components/DateRangeInput.svelte.d.ts +35 -0
  41. package/dist/components/DateRangePicker.svelte +10 -0
  42. package/dist/components/DateRangePicker.svelte.d.ts +6 -0
  43. package/dist/components/DateTimeInput.svelte +134 -0
  44. package/dist/components/DateTimeInput.svelte.d.ts +27 -0
  45. package/dist/components/DateTimePicker.svelte +10 -0
  46. package/dist/components/DateTimePicker.svelte.d.ts +6 -0
  47. package/dist/components/DevTools.svelte +36 -28
  48. package/dist/components/DevframePanel.svelte +123 -0
  49. package/dist/components/DevframePanel.svelte.d.ts +3 -0
  50. package/dist/components/EditPage.svelte +4 -0
  51. package/dist/components/EditPage.svelte.d.ts +2 -0
  52. package/dist/components/ErrorSummary.svelte +52 -0
  53. package/dist/components/ErrorSummary.svelte.d.ts +14 -0
  54. package/dist/components/FieldRenderer.svelte +172 -99
  55. package/dist/components/FileUpload.svelte +412 -0
  56. package/dist/components/FileUpload.svelte.d.ts +43 -0
  57. package/dist/components/FilterBuilder.svelte +244 -265
  58. package/dist/components/FilterBuilder.svelte.d.ts +3 -0
  59. package/dist/components/FilterCollectionInput.svelte +80 -0
  60. package/dist/components/FilterCollectionInput.svelte.d.ts +10 -0
  61. package/dist/components/GanttChart.svelte +66 -14
  62. package/dist/components/GanttChart.svelte.d.ts +6 -0
  63. package/dist/components/Header.svelte +18 -2
  64. package/dist/components/ImportWizard.svelte +509 -100
  65. package/dist/components/ImportWizard.svelte.d.ts +11 -0
  66. package/dist/components/JsonSchemaForm.svelte +322 -98
  67. package/dist/components/JsonSchemaForm.svelte.d.ts +18 -1
  68. package/dist/components/KanbanBoard.svelte +195 -40
  69. package/dist/components/KanbanBoard.svelte.d.ts +9 -2
  70. package/dist/components/Layout.svelte +4 -3
  71. package/dist/components/LazyAutoForm.svelte +28 -0
  72. package/dist/components/LazyAutoForm.svelte.d.ts +8 -0
  73. package/dist/components/LazyAutoTable.svelte +28 -0
  74. package/dist/components/LazyAutoTable.svelte.d.ts +8 -0
  75. package/dist/components/LazySettingsPage.svelte +28 -0
  76. package/dist/components/LazySettingsPage.svelte.d.ts +8 -0
  77. package/dist/components/LazyShowPage.svelte +28 -0
  78. package/dist/components/LazyShowPage.svelte.d.ts +8 -0
  79. package/dist/components/LazyTaskQueueDrawer.svelte.d.ts +1 -1
  80. package/dist/components/ListPage.svelte +9 -1
  81. package/dist/components/ListPage.svelte.d.ts +5 -0
  82. package/dist/components/LoginPage.svelte +2 -2
  83. package/dist/components/MoneyInput.svelte +25 -0
  84. package/dist/components/MoneyInput.svelte.d.ts +20 -0
  85. package/dist/components/MultiTabKeepAlive.svelte +11 -6
  86. package/dist/components/NotificationsSettings.svelte +69 -2
  87. package/dist/components/NotificationsSettings.svelte.d.ts +25 -1
  88. package/dist/components/NumberInput.svelte +25 -0
  89. package/dist/components/NumberInput.svelte.d.ts +19 -0
  90. package/dist/components/NumericInput.svelte +110 -0
  91. package/dist/components/NumericInput.svelte.d.ts +23 -0
  92. package/dist/components/PercentInput.svelte +25 -0
  93. package/dist/components/PercentInput.svelte.d.ts +20 -0
  94. package/dist/components/PermissionMatrix.svelte +20 -1
  95. package/dist/components/PivotExportButton.svelte +157 -0
  96. package/dist/components/PivotExportButton.svelte.d.ts +16 -0
  97. package/dist/components/PivotTable.svelte +229 -74
  98. package/dist/components/PivotTable.svelte.d.ts +19 -2
  99. package/dist/components/QuickEditDrawer.svelte +4 -1
  100. package/dist/components/QuickEditDrawer.svelte.d.ts +2 -0
  101. package/dist/components/RadioGroup.svelte +121 -0
  102. package/dist/components/RadioGroup.svelte.d.ts +43 -0
  103. package/dist/components/RangeSlider.svelte +127 -0
  104. package/dist/components/RangeSlider.svelte.d.ts +22 -0
  105. package/dist/components/Rate.svelte +121 -0
  106. package/dist/components/Rate.svelte.d.ts +16 -0
  107. package/dist/components/RecordDetailDrawer.svelte +6 -2
  108. package/dist/components/RecordDetailDrawer.svelte.d.ts +4 -0
  109. package/dist/components/RelationPicker.svelte +341 -0
  110. package/dist/components/RelationPicker.svelte.d.ts +26 -0
  111. package/dist/components/ResourceOperationsPage.svelte +25 -19
  112. package/dist/components/ResourceOperationsPage.svelte.d.ts +5 -1
  113. package/dist/components/RetryTaskButton.svelte +12 -42
  114. package/dist/components/SecuritySettings.svelte +29 -5
  115. package/dist/components/SegmentedControl.svelte +145 -0
  116. package/dist/components/SegmentedControl.svelte.d.ts +38 -0
  117. package/dist/components/SettingsPage.svelte +3 -1
  118. package/dist/components/SettingsPage.svelte.d.ts +1 -0
  119. package/dist/components/ShowInferencer.svelte +4 -2
  120. package/dist/components/ShowInferencer.svelte.d.ts +2 -0
  121. package/dist/components/ShowPage.svelte +25 -6
  122. package/dist/components/ShowPage.svelte.d.ts +5 -0
  123. package/dist/components/Sidebar.svelte +5 -5
  124. package/dist/components/Slider.svelte +100 -0
  125. package/dist/components/Slider.svelte.d.ts +19 -0
  126. package/dist/components/SpreadsheetView.svelte +1037 -124
  127. package/dist/components/SpreadsheetView.svelte.d.ts +19 -0
  128. package/dist/components/StepsForm.svelte +5 -5
  129. package/dist/components/SvarAutoTable.svelte +39 -0
  130. package/dist/components/SvarAutoTable.svelte.d.ts +13 -0
  131. package/dist/components/SvarDataGrid.svelte +362 -0
  132. package/dist/components/SvarDataGrid.svelte.d.ts +57 -0
  133. package/dist/components/SvarGridCell.svelte +10 -0
  134. package/dist/components/SvarGridCell.svelte.d.ts +7 -0
  135. package/dist/components/SvarResourceRead.svelte +50 -0
  136. package/dist/components/SvarResourceRead.svelte.d.ts +23 -0
  137. package/dist/components/SvarResourceTable.svelte +63 -0
  138. package/dist/components/SvarResourceTable.svelte.d.ts +42 -0
  139. package/dist/components/SvarResourceTableView.svelte +364 -0
  140. package/dist/components/SvarResourceTableView.svelte.d.ts +4 -0
  141. package/dist/components/TagsInput.svelte +178 -0
  142. package/dist/components/TagsInput.svelte.d.ts +17 -0
  143. package/dist/components/TaskQueueDrawer.svelte +72 -16
  144. package/dist/components/TimePicker.svelte +10 -0
  145. package/dist/components/TimePicker.svelte.d.ts +6 -0
  146. package/dist/components/TreeSelect.svelte +440 -46
  147. package/dist/components/TreeSelect.svelte.d.ts +6 -1
  148. package/dist/components/TreeTable.svelte +484 -82
  149. package/dist/components/TreeTable.svelte.d.ts +24 -1
  150. package/dist/components/TwoFactorAuthPage.svelte +85 -48
  151. package/dist/components/TwoFactorAuthPage.svelte.d.ts +12 -1
  152. package/dist/components/VirtualTable.svelte +116 -20
  153. package/dist/components/VirtualTable.svelte.d.ts +5 -0
  154. package/dist/components/account/CompanyProfilePage.svelte +74 -38
  155. package/dist/components/account/GetStartedPage.svelte +2 -1
  156. package/dist/components/account/ImportMembersPage.svelte +22 -167
  157. package/dist/components/account/ImportMembersPage.svelte.d.ts +4 -1
  158. package/dist/components/account/MemberDirectory.svelte +156 -0
  159. package/dist/components/account/MemberDirectory.svelte.d.ts +19 -0
  160. package/dist/components/account/MembersStarterPage.svelte +9 -55
  161. package/dist/components/account/MembersStarterPage.svelte.d.ts +5 -1
  162. package/dist/components/account/SecurityLogPage.svelte +4 -1
  163. package/dist/components/account/SettingsPlainPage.svelte +7 -38
  164. package/dist/components/account/SettingsPlainPage.svelte.d.ts +4 -1
  165. package/dist/components/account/SettingsSidebarPage.svelte +7 -20
  166. package/dist/components/account/TeamMembersPage.svelte +4 -20
  167. package/dist/components/account/TeamMembersPage.svelte.d.ts +5 -1
  168. package/dist/components/account/UserProfilePage.svelte +3 -25
  169. package/dist/components/account/UserProfilePage.svelte.d.ts +17 -2
  170. package/dist/components/auto-table-grid.d.ts +26 -0
  171. package/dist/components/auto-table-grid.js +1 -0
  172. package/dist/components/buttons/ExportButton.svelte +56 -12
  173. package/dist/components/buttons/ExportButton.svelte.d.ts +11 -0
  174. package/dist/components/content/ContentPageHeader.svelte +12 -9
  175. package/dist/components/content/ContentPageShell.svelte +6 -9
  176. package/dist/components/content/FileBrowser.svelte +35 -0
  177. package/dist/components/content/FileBrowser.svelte.d.ts +14 -0
  178. package/dist/components/content/MetricBlock.svelte +10 -13
  179. package/dist/components/content/NetworkTable.svelte +3 -3
  180. package/dist/components/content/NetworkTable.svelte.d.ts +1 -0
  181. package/dist/components/content/PageToolbar.svelte +6 -10
  182. package/dist/components/content/ReferenceNotice.svelte +5 -0
  183. package/dist/components/content/ReferenceNotice.svelte.d.ts +18 -0
  184. package/dist/components/content/SectionHeader.svelte +8 -13
  185. package/dist/components/content/SettingsFieldRow.svelte +9 -23
  186. package/dist/components/content/SettingsGroup.svelte +11 -27
  187. package/dist/components/content/StatusBadge.svelte +3 -8
  188. package/dist/components/content/WorkspaceLayout.svelte +8 -12
  189. package/dist/components/enterprise/filter-tree.d.ts +48 -0
  190. package/dist/components/enterprise/filter-tree.js +224 -0
  191. package/dist/components/enterprise/json-schema-form.d.ts +39 -0
  192. package/dist/components/enterprise/json-schema-form.js +195 -0
  193. package/dist/components/enterprise/recursive-schema-form.d.ts +30 -0
  194. package/dist/components/enterprise/recursive-schema-form.js +196 -0
  195. package/dist/components/enterprise/spreadsheet-formula.d.ts +12 -0
  196. package/dist/components/enterprise/spreadsheet-formula.js +245 -0
  197. package/dist/components/export-reference.d.ts +2 -0
  198. package/dist/components/export-reference.js +9 -0
  199. package/dist/components/fieldComponentMap.js +2 -0
  200. package/dist/components/fields/DateField.svelte +12 -12
  201. package/dist/components/fields/date-display.d.ts +6 -0
  202. package/dist/components/fields/date-display.js +24 -0
  203. package/dist/components/fields/date-field-value.d.ts +2 -0
  204. package/dist/components/fields/date-field-value.js +13 -0
  205. package/dist/components/json-schema-form-state.d.ts +37 -0
  206. package/dist/components/json-schema-form-state.js +168 -0
  207. package/dist/components/network/TeamCrewTablePage.svelte +6 -1
  208. package/dist/components/network/UserCardsNFTPage.svelte +2 -0
  209. package/dist/components/profile/PublicProfilePage.svelte +2 -0
  210. package/dist/components/record-detail.svelte.d.ts +3 -0
  211. package/dist/components/record-detail.svelte.js +10 -4
  212. package/dist/components/saved-list-views.d.ts +74 -1
  213. package/dist/components/saved-list-views.js +191 -3
  214. package/dist/components/svar-grid-cells.d.ts +14 -0
  215. package/dist/components/svar-grid-cells.js +2 -0
  216. package/dist/components/svar-grid-contract.d.ts +80 -0
  217. package/dist/components/svar-grid-contract.js +159 -0
  218. package/dist/components/svar-grid-interactions.d.ts +54 -0
  219. package/dist/components/svar-grid-interactions.js +174 -0
  220. package/dist/components/svar-grid-loading.d.ts +40 -0
  221. package/dist/components/svar-grid-loading.js +180 -0
  222. package/dist/components/svar-grid-operations.d.ts +37 -0
  223. package/dist/components/svar-grid-operations.js +186 -0
  224. package/dist/components/svar-grid-panes.d.ts +34 -0
  225. package/dist/components/svar-grid-panes.js +66 -0
  226. package/dist/components/svar-grid-resource-loading.d.ts +18 -0
  227. package/dist/components/svar-grid-resource-loading.js +68 -0
  228. package/dist/components/svar-grid-resource-session.svelte.d.ts +29 -0
  229. package/dist/components/svar-grid-resource-session.svelte.js +61 -0
  230. package/dist/components/table-contract.d.ts +12 -1
  231. package/dist/components/task-action.svelte.d.ts +13 -0
  232. package/dist/components/task-action.svelte.js +73 -0
  233. package/dist/components/ui/DesignSystemShowcase.svelte +164 -0
  234. package/dist/components/ui/DesignSystemShowcase.svelte.d.ts +3 -0
  235. package/dist/components/ui/badge/badge-variants.d.ts +3 -2
  236. package/dist/components/ui/badge/badge-variants.js +2 -1
  237. package/dist/components/ui/badge/badge.svelte +3 -0
  238. package/dist/components/ui/badge/badge.svelte.d.ts +1 -0
  239. package/dist/components/ui/button/button-variants.d.ts +5 -4
  240. package/dist/components/ui/button/button-variants.js +7 -2
  241. package/dist/components/ui/checkbox/checkbox.svelte +5 -1
  242. package/dist/components/ui/checkbox/checkbox.svelte.d.ts +5 -1
  243. package/dist/components/ui/input/input.svelte +8 -6
  244. package/dist/components/ui/input/input.svelte.d.ts +4 -1
  245. package/dist/components/ui/select/select.svelte +4 -1
  246. package/dist/components/ui/select/select.svelte.d.ts +2 -1
  247. package/dist/components/ui/textarea/textarea.svelte +2 -1
  248. package/dist/components.css +3110 -0
  249. package/dist/date-input-policy.d.ts +4 -0
  250. package/dist/date-input-policy.js +35 -0
  251. package/dist/design-contract.d.ts +13 -0
  252. package/dist/design-contract.js +11 -0
  253. package/dist/design-system-components.d.ts +100 -0
  254. package/dist/design-system-components.js +101 -0
  255. package/dist/design-system.d.ts +15 -0
  256. package/dist/design-system.js +7 -0
  257. package/dist/devtools-bridge.d.ts +80 -0
  258. package/dist/devtools-bridge.js +92 -0
  259. package/dist/index.d.ts +50 -1
  260. package/dist/index.js +32 -2
  261. package/dist/page-chrome.css +12 -49
  262. package/dist/recipes.d.ts +960 -0
  263. package/dist/recipes.js +264 -0
  264. package/dist/rendering/index.d.ts +87 -0
  265. package/dist/rendering/index.js +117 -0
  266. package/dist/styles/aliases.css +49 -0
  267. package/dist/styles/compatibility.epilogue.css +384 -0
  268. package/dist/styles/compatibility.prelude.css +5513 -0
  269. package/dist/styles/conditional-states.css +28 -0
  270. package/dist/styles/conditional-utilities.css +22 -0
  271. package/dist/tree-table-model.d.ts +24 -0
  272. package/dist/tree-table-model.js +111 -0
  273. package/dist/utils.d.ts +1 -1
  274. package/dist/utils.js +1 -1
  275. package/package.json +40 -10
  276. package/styles-compatibility.json +12 -0
@@ -0,0 +1,412 @@
1
+ <script lang="ts">
2
+ import { onDestroy, untrack } from 'svelte';
3
+ import { X, Upload, RotateCw, Ban } from '@lucide/svelte';
4
+ import { Button } from './ui/button/index.js';
5
+ import { useTranslation } from '@svadmin/core/i18n';
6
+
7
+ export type UploadItemStatus = 'queued' | 'uploading' | 'success' | 'error' | 'cancelled';
8
+
9
+ export interface UploadItem {
10
+ id: string;
11
+ file: File;
12
+ status: UploadItemStatus;
13
+ progress: number;
14
+ url?: string;
15
+ error?: string;
16
+ uploadId?: string;
17
+ cleanupStatus?: 'pending' | 'success' | 'error';
18
+ }
19
+
20
+ export interface UploadSession {
21
+ signal: AbortSignal;
22
+ idempotencyKey: string;
23
+ setUploadId: (uploadId: string) => void;
24
+ onProgress: (progress: number) => void;
25
+ }
26
+
27
+ export interface UploadCancellation {
28
+ uploadId: string;
29
+ idempotencyKey: string;
30
+ reason: 'cancel' | 'remove' | 'replace' | 'scope-change' | 'unmount';
31
+ }
32
+
33
+ interface Props {
34
+ id?: string;
35
+ name?: string;
36
+ accept?: string;
37
+ multiple?: boolean;
38
+ maxFiles?: number;
39
+ maxSize?: number;
40
+ disabled?: boolean;
41
+ required?: boolean;
42
+ upload?: (file: File, session: UploadSession) => Promise<{ url?: string; uploadId?: string } | undefined> | Promise<void>;
43
+ cancelUpload?: (file: File, cancellation: UploadCancellation) => Promise<void>;
44
+ onChange?: (items: UploadItem[]) => void;
45
+ onReject?: (file: File, reason: string) => void;
46
+ class?: string;
47
+ }
48
+
49
+ let {
50
+ id,
51
+ name,
52
+ accept = '',
53
+ multiple = false,
54
+ maxFiles = multiple ? 10 : 1,
55
+ maxSize,
56
+ disabled = false,
57
+ required = false,
58
+ upload,
59
+ cancelUpload,
60
+ onChange,
61
+ onReject,
62
+ class: className = '',
63
+ }: Props = $props();
64
+
65
+ const i18n = useTranslation();
66
+ let input: HTMLInputElement | undefined = $state();
67
+ let items = $state<UploadItem[]>([]);
68
+ let rejected = $state<{ name: string; reason: string }[]>([]);
69
+ let sequence = 0;
70
+ const controllers = new Map<string, AbortController>();
71
+ interface UploadAttempt {
72
+ controller: AbortController;
73
+ upload: NonNullable<Props['upload']>;
74
+ cancelUpload?: Props['cancelUpload'];
75
+ epoch: number;
76
+ file: File;
77
+ idempotencyKey: string;
78
+ uploadId?: string;
79
+ reason?: UploadCancellation['reason'];
80
+ cleanupStarted?: boolean;
81
+ settled?: boolean;
82
+ }
83
+ const attempts = new Map<string, UploadAttempt>();
84
+ const latestAttempts = new Map<string, UploadAttempt>();
85
+ let scopeEpoch = 0;
86
+ let observedUpload = untrack(() => upload);
87
+ let destroyed = false;
88
+
89
+ $effect.pre(() => {
90
+ if (upload === observedUpload) return;
91
+ observedUpload = upload;
92
+ scopeEpoch += 1;
93
+ untrack(() => {
94
+ const activeIds = [...controllers.keys()];
95
+ for (const id of activeIds) retire(id, 'scope-change');
96
+ const cancelledIds = new Set(activeIds);
97
+ if (cancelledIds.size > 0) {
98
+ items = items.map(item => cancelledIds.has(item.id) && item.status === 'uploading'
99
+ ? { ...item, status: 'cancelled', error: 'Upload cancelled.' }
100
+ : item);
101
+ emitChange();
102
+ }
103
+ });
104
+ });
105
+
106
+ function retire(id: string, reason: UploadCancellation['reason'] = 'cancel'): void {
107
+ const attempt = attempts.get(id);
108
+ const controller = controllers.get(id);
109
+ controllers.delete(id);
110
+ attempts.delete(id);
111
+ if (attempt) attempt.reason = reason;
112
+ controller?.abort();
113
+ if (attempt) cleanupAttempt(id, attempt);
114
+ }
115
+
116
+ function cleanupAttempt(id: string, attempt: UploadAttempt): void {
117
+ if (!attempt.reason || attempt.cleanupStarted) return;
118
+ if (attempt?.uploadId && attempt.cancelUpload) {
119
+ attempt.cleanupStarted = true;
120
+ const publish = (cleanupStatus: NonNullable<UploadItem['cleanupStatus']>) => {
121
+ if (!destroyed && scopeEpoch === attempt.epoch && upload === attempt.upload
122
+ && latestAttempts.get(id) === attempt && items.some(item => item.id === id)) {
123
+ updateItem(id, { cleanupStatus });
124
+ }
125
+ };
126
+ publish('pending');
127
+ const cancellation: UploadCancellation = {
128
+ uploadId: attempt.uploadId,
129
+ idempotencyKey: attempt.idempotencyKey,
130
+ reason: attempt.reason,
131
+ };
132
+ const cancel = attempt.cancelUpload;
133
+ void Promise.resolve().then(() => cancel(attempt.file, cancellation))
134
+ .then(() => publish('success'), () => publish('error'));
135
+ }
136
+ }
137
+
138
+ onDestroy(() => {
139
+ destroyed = true;
140
+ for (const id of controllers.keys()) retire(id, 'unmount');
141
+ latestAttempts.clear();
142
+ });
143
+
144
+ function emitChange(): void {
145
+ onChange?.(items.map(item => ({ ...item })));
146
+ }
147
+
148
+ function nextId(): string {
149
+ sequence += 1;
150
+ return `upload-${sequence}`;
151
+ }
152
+
153
+ function accepts(file: File): boolean {
154
+ if (!accept.trim()) return true;
155
+ return accept.split(',').map(token => token.trim().toLowerCase()).some(token =>
156
+ token === file.type.toLowerCase() ||
157
+ (token.endsWith('/*') && file.type.toLowerCase().startsWith(token.slice(0, -1))) ||
158
+ (token.startsWith('.') && file.name.toLowerCase().endsWith(token)),
159
+ );
160
+ }
161
+
162
+ function validate(file: File): string | undefined {
163
+ if (!accepts(file)) return 'File type is not accepted.';
164
+ if (maxSize !== undefined && (!Number.isSafeInteger(maxSize) || maxSize < 0 || file.size > maxSize)) {
165
+ return 'File is too large.';
166
+ }
167
+ return undefined;
168
+ }
169
+
170
+ const normalizedMaxFiles = $derived(
171
+ multiple ? (Number.isSafeInteger(maxFiles) && maxFiles > 0 ? maxFiles : 10) : 1,
172
+ );
173
+
174
+ type UploadItemUpdate = Partial<Omit<UploadItem, 'error' | 'url' | 'uploadId' | 'cleanupStatus'>> & {
175
+ [K in 'error' | 'url' | 'uploadId' | 'cleanupStatus']?: UploadItem[K] | undefined;
176
+ };
177
+
178
+ function updateItem(id: string, update: UploadItemUpdate): void {
179
+ items = items.map(item => {
180
+ if (item.id !== id) return item;
181
+ const { error, url, uploadId, cleanupStatus, ...required } = update;
182
+ const next: UploadItem = { ...item, ...required };
183
+ if ('error' in update && error === undefined) delete next.error;
184
+ if ('url' in update && url === undefined) delete next.url;
185
+ if ('uploadId' in update && uploadId === undefined) delete next.uploadId;
186
+ if ('cleanupStatus' in update && cleanupStatus === undefined) delete next.cleanupStatus;
187
+ if (error !== undefined) next.error = error;
188
+ if (url !== undefined) next.url = url;
189
+ if (uploadId !== undefined) next.uploadId = uploadId;
190
+ if (cleanupStatus !== undefined) next.cleanupStatus = cleanupStatus;
191
+ return next;
192
+ });
193
+ emitChange();
194
+ }
195
+
196
+ async function process(item: UploadItem): Promise<void> {
197
+ if (destroyed || !upload || disabled) return;
198
+ if (controllers.has(item.id) || !items.some(candidate => candidate.id === item.id)) return;
199
+ const controller = new AbortController();
200
+ controllers.set(item.id, controller);
201
+ const attempt: UploadAttempt = {
202
+ controller, upload, epoch: scopeEpoch, file: item.file,
203
+ cancelUpload,
204
+ idempotencyKey: crypto.randomUUID(),
205
+ };
206
+ attempts.set(item.id, attempt);
207
+ latestAttempts.set(item.id, attempt);
208
+ // 每次尝试由独立控制器持有;旧回执和 finally 不得影响新尝试。
209
+ const current = () => attempts.get(item.id) === attempt
210
+ && controllers.get(item.id) === controller
211
+ && scopeEpoch === attempt.epoch && upload === attempt.upload;
212
+ const updateCurrent = (update: UploadItemUpdate) => {
213
+ if (current() && !controller.signal.aborted) updateItem(item.id, update);
214
+ };
215
+ const register = (uploadId: string) => {
216
+ if (attempt.settled) return;
217
+ if (typeof uploadId !== 'string' || !uploadId.trim() || uploadId.length > 200
218
+ || (attempt.uploadId !== undefined && attempt.uploadId !== uploadId)) throw new Error('Invalid upload session.');
219
+ attempt.uploadId = uploadId;
220
+ updateCurrent({ uploadId });
221
+ cleanupAttempt(item.id, attempt);
222
+ };
223
+ updateItem(item.id, {
224
+ status: 'uploading',
225
+ progress: 0,
226
+ error: undefined,
227
+ uploadId: undefined,
228
+ cleanupStatus: undefined,
229
+ });
230
+ try {
231
+ // 宿主回调可以同步卸载或替换组件;通知返回后必须重新确认本次请求仍有效。
232
+ if (destroyed || !current() || controller.signal.aborted) return;
233
+ const result = await attempt.upload(item.file, {
234
+ signal: controller.signal,
235
+ idempotencyKey: attempt.idempotencyKey,
236
+ setUploadId: register,
237
+ onProgress: progress => updateCurrent({
238
+ progress: Number.isFinite(progress) ? Math.max(0, Math.min(100, Math.round(progress))) : 0,
239
+ }),
240
+ });
241
+ if (result?.uploadId !== undefined) register(result.uploadId);
242
+ const uploadId = result?.uploadId ?? attempt.uploadId;
243
+ updateCurrent({
244
+ status: 'success',
245
+ progress: 100,
246
+ ...(result?.url === undefined ? {} : { url: result.url }),
247
+ ...(uploadId === undefined ? {} : { uploadId }),
248
+ });
249
+ } catch (error) {
250
+ if (!current()) return;
251
+ updateItem(item.id, {
252
+ status: controller.signal.aborted ? 'cancelled' : 'error',
253
+ error: controller.signal.aborted ? 'Upload cancelled.' : error instanceof Error ? error.message : 'Upload failed.',
254
+ });
255
+ } finally {
256
+ attempt.settled = true;
257
+ if (current()) {
258
+ controllers.delete(item.id);
259
+ attempts.delete(item.id);
260
+ }
261
+ }
262
+ }
263
+
264
+ function addFiles(selected: File[]): void {
265
+ if (destroyed || disabled) return;
266
+ const available = multiple
267
+ ? Math.max(0, normalizedMaxFiles - items.length)
268
+ : 1;
269
+ rejected = [];
270
+ if (selected.length > available) {
271
+ for (const file of selected.slice(available)) {
272
+ if (destroyed) return;
273
+ rejected = [...rejected, { name: file.name, reason: 'Maximum file count exceeded.' }];
274
+ onReject?.(file, 'Maximum file count exceeded.');
275
+ }
276
+ }
277
+ for (const file of selected.slice(0, multiple ? available : Math.min(available, 1))) {
278
+ if (destroyed) return;
279
+ const reason = validate(file);
280
+ if (reason) {
281
+ rejected = [...rejected, { name: file.name, reason }];
282
+ onReject?.(file, reason);
283
+ continue;
284
+ }
285
+ const item: UploadItem = { id: nextId(), file, status: 'queued', progress: 0 };
286
+ if (!multiple) {
287
+ for (const previous of items) {
288
+ retire(previous.id, 'replace');
289
+ latestAttempts.delete(previous.id);
290
+ }
291
+ }
292
+ items = multiple ? [...items, item] : [item];
293
+ emitChange();
294
+ void process(item);
295
+ }
296
+ }
297
+
298
+ function handleInput(event: Event): void {
299
+ const target = event.currentTarget;
300
+ if (!(target instanceof HTMLInputElement)) return;
301
+ addFiles(Array.from(target.files ?? []));
302
+ // 异步模式的文件由队列持有,清空输入以允许再次选择同名文件。
303
+ if (upload) target.value = '';
304
+ }
305
+
306
+ function remove(id: string): void {
307
+ if (disabled) return;
308
+ retire(id, 'remove');
309
+ latestAttempts.delete(id);
310
+ items = items.filter(item => item.id !== id);
311
+ if (input) input.value = '';
312
+ emitChange();
313
+ }
314
+
315
+ function cancel(id: string): void {
316
+ const controller = controllers.get(id);
317
+ if (!controller) return;
318
+ retire(id, 'cancel');
319
+ updateItem(id, { status: 'cancelled', error: 'Upload cancelled.' });
320
+ }
321
+
322
+ function retry(item: UploadItem): void {
323
+ if (item.status === 'error' || item.status === 'cancelled') void process(item);
324
+ }
325
+
326
+ function handleKeydown(event: KeyboardEvent): void {
327
+ if (event.key === 'Enter' || event.key === ' ') {
328
+ event.preventDefault();
329
+ input?.click();
330
+ }
331
+ }
332
+ </script>
333
+
334
+ <div class={`svadmin-file-upload ${className}`}>
335
+ <input
336
+ bind:this={input}
337
+ type="file"
338
+ {id}
339
+ name={upload ? undefined : name}
340
+ {accept}
341
+ {multiple}
342
+ disabled={disabled}
343
+ required={upload ? false : required}
344
+ class="svadmin-file-upload-input"
345
+ onchange={handleInput}
346
+ />
347
+ <div
348
+ class="svadmin-file-upload-dropzone"
349
+ role="button"
350
+ tabindex={disabled ? -1 : 0}
351
+ aria-disabled={disabled}
352
+ onclick={() => input?.click()}
353
+ onkeydown={handleKeydown}
354
+ ondragover={(event) => event.preventDefault()}
355
+ ondrop={(event) => {
356
+ event.preventDefault();
357
+ addFiles(Array.from(event.dataTransfer?.files ?? []));
358
+ }}
359
+ >
360
+ <Upload aria-hidden="true" />
361
+ <span>{i18n.t('upload.selectOrDrop')}</span>
362
+ </div>
363
+
364
+ <div role="status" aria-live="polite">
365
+ {i18n.t('upload.selectedCount', { count: String(items.length), max: String(normalizedMaxFiles) })}
366
+ </div>
367
+ {#if rejected.length}
368
+ <ul aria-label={i18n.t('upload.rejectedFiles')}>
369
+ {#each rejected as rejection (rejection)}
370
+ <li>{rejection.name}: {rejection.reason}</li>
371
+ {/each}
372
+ </ul>
373
+ {/if}
374
+
375
+ {#if items.length}
376
+ <ul class="svadmin-file-upload-list" aria-label="Selected files">
377
+ {#each items as item (item.id)}
378
+ <li>
379
+ <span class="svadmin-file-upload-name">{item.file.name}</span>
380
+ <span class="svadmin-file-upload-status">
381
+ {#if item.status === 'uploading'}{item.progress}%{:else}{item.status}{/if}
382
+ </span>
383
+ {#if item.status === 'uploading'}
384
+ <Button type="button" variant="ghost" size="icon-sm" aria-label="Cancel upload" onclick={() => cancel(item.id)}>
385
+ <Ban aria-hidden="true" />
386
+ </Button>
387
+ {:else if item.status === 'error' || item.status === 'cancelled'}
388
+ <Button type="button" variant="ghost" size="icon-sm" aria-label="Retry upload" {disabled} onclick={() => retry(item)}>
389
+ <RotateCw aria-hidden="true" />
390
+ </Button>
391
+ {/if}
392
+ <Button type="button" variant="ghost" size="icon-sm" aria-label="Remove file" {disabled} onclick={() => remove(item.id)}>
393
+ <X aria-hidden="true" />
394
+ </Button>
395
+ {#if item.error}<span role="alert">{item.error}</span>{/if}
396
+ {#if item.cleanupStatus === 'pending'}<span role="status">{i18n.t('upload.cleanupPending')}</span>{/if}
397
+ {#if item.cleanupStatus === 'error'}<span role="alert">{i18n.t('upload.cleanupFailed')}</span>{/if}
398
+ </li>
399
+ {/each}
400
+ </ul>
401
+ {/if}
402
+ </div>
403
+
404
+ <style>
405
+ .svadmin-file-upload-input { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); }
406
+ .svadmin-file-upload-dropzone { display: flex; align-items: center; justify-content: center; gap: 8px; min-height: 88px; border: 1px dashed var(--border); border-radius: 6px; cursor: pointer; }
407
+ .svadmin-file-upload-dropzone[aria-disabled="true"] { cursor: not-allowed; opacity: 0.6; }
408
+ .svadmin-file-upload-list { display: grid; gap: 6px; margin: 8px 0 0; padding: 0; list-style: none; }
409
+ .svadmin-file-upload-list li { display: flex; align-items: center; gap: 8px; min-width: 0; }
410
+ .svadmin-file-upload-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
411
+ .svadmin-file-upload-status { color: var(--muted-foreground); font-size: 0.875rem; }
412
+ </style>
@@ -0,0 +1,43 @@
1
+ export type UploadItemStatus = 'queued' | 'uploading' | 'success' | 'error' | 'cancelled';
2
+ export interface UploadItem {
3
+ id: string;
4
+ file: File;
5
+ status: UploadItemStatus;
6
+ progress: number;
7
+ url?: string;
8
+ error?: string;
9
+ uploadId?: string;
10
+ cleanupStatus?: 'pending' | 'success' | 'error';
11
+ }
12
+ export interface UploadSession {
13
+ signal: AbortSignal;
14
+ idempotencyKey: string;
15
+ setUploadId: (uploadId: string) => void;
16
+ onProgress: (progress: number) => void;
17
+ }
18
+ export interface UploadCancellation {
19
+ uploadId: string;
20
+ idempotencyKey: string;
21
+ reason: 'cancel' | 'remove' | 'replace' | 'scope-change' | 'unmount';
22
+ }
23
+ interface Props {
24
+ id?: string;
25
+ name?: string;
26
+ accept?: string;
27
+ multiple?: boolean;
28
+ maxFiles?: number;
29
+ maxSize?: number;
30
+ disabled?: boolean;
31
+ required?: boolean;
32
+ upload?: (file: File, session: UploadSession) => Promise<{
33
+ url?: string;
34
+ uploadId?: string;
35
+ } | undefined> | Promise<void>;
36
+ cancelUpload?: (file: File, cancellation: UploadCancellation) => Promise<void>;
37
+ onChange?: (items: UploadItem[]) => void;
38
+ onReject?: (file: File, reason: string) => void;
39
+ class?: string;
40
+ }
41
+ declare const FileUpload: import("svelte").Component<Props, {}, "">;
42
+ type FileUpload = ReturnType<typeof FileUpload>;
43
+ export default FileUpload;