@svadmin/ui 0.73.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 (295) hide show
  1. package/README.md +53 -28
  2. package/dist/app.css +3733 -4119
  3. package/dist/app.theme.css +3733 -4119
  4. package/dist/component-registry.d.ts +14977 -0
  5. package/dist/component-registry.js +216 -0
  6. package/dist/components/AboutSettings.svelte +7 -1
  7. package/dist/components/AboutSettings.svelte.d.ts +4 -1
  8. package/dist/components/ActivityFeed.svelte +9 -3
  9. package/dist/components/ActivityFeed.svelte.d.ts +3 -0
  10. package/dist/components/AdminApp.svelte +19 -10
  11. package/dist/components/AdminApp.svelte.d.ts +6 -0
  12. package/dist/components/ApiSettings.svelte +41 -5
  13. package/dist/components/ApprovalCenter.svelte +283 -0
  14. package/dist/components/ApprovalCenter.svelte.d.ts +10 -0
  15. package/dist/components/AuditLogViewer.svelte +3 -1
  16. package/dist/components/AutoForm.svelte +33 -3
  17. package/dist/components/AutoForm.svelte.d.ts +1 -0
  18. package/dist/components/AutoTable.svelte +610 -47
  19. package/dist/components/AutoTable.svelte.d.ts +17 -1
  20. package/dist/components/BoundRecordDetailDrawer.svelte +9 -0
  21. package/dist/components/BoundRecordDetailDrawer.svelte.d.ts +2 -0
  22. package/dist/components/Breadcrumbs.svelte +11 -1
  23. package/dist/components/CancelTaskButton.svelte +12 -42
  24. package/dist/components/ColorPicker.svelte +73 -0
  25. package/dist/components/ColorPicker.svelte.d.ts +15 -0
  26. package/dist/components/ComboboxField.svelte +114 -27
  27. package/dist/components/ComboboxField.svelte.d.ts +1 -0
  28. package/dist/components/DashboardView.svelte +99 -0
  29. package/dist/components/DashboardView.svelte.d.ts +14 -0
  30. package/dist/components/DatePicker.svelte +10 -0
  31. package/dist/components/DatePicker.svelte.d.ts +6 -0
  32. package/dist/components/DateRangeInput.svelte +85 -5
  33. package/dist/components/DateRangeInput.svelte.d.ts +10 -0
  34. package/dist/components/DateRangePicker.svelte +10 -0
  35. package/dist/components/DateRangePicker.svelte.d.ts +6 -0
  36. package/dist/components/DateTimeInput.svelte +74 -5
  37. package/dist/components/DateTimeInput.svelte.d.ts +8 -0
  38. package/dist/components/DateTimePicker.svelte +10 -0
  39. package/dist/components/DateTimePicker.svelte.d.ts +6 -0
  40. package/dist/components/DevTools.svelte +34 -26
  41. package/dist/components/DevframePanel.svelte +123 -0
  42. package/dist/components/DevframePanel.svelte.d.ts +3 -0
  43. package/dist/components/ErrorSummary.svelte +52 -0
  44. package/dist/components/ErrorSummary.svelte.d.ts +14 -0
  45. package/dist/components/FieldRenderer.svelte +82 -81
  46. package/dist/components/FileUpload.svelte +214 -52
  47. package/dist/components/FileUpload.svelte.d.ts +11 -0
  48. package/dist/components/FilterBuilder.svelte +88 -66
  49. package/dist/components/FilterCollectionInput.svelte +80 -0
  50. package/dist/components/FilterCollectionInput.svelte.d.ts +10 -0
  51. package/dist/components/GanttChart.svelte +66 -14
  52. package/dist/components/GanttChart.svelte.d.ts +6 -0
  53. package/dist/components/Header.svelte +18 -2
  54. package/dist/components/ImportWizard.svelte +339 -17
  55. package/dist/components/ImportWizard.svelte.d.ts +11 -0
  56. package/dist/components/JsonSchemaForm.svelte +120 -52
  57. package/dist/components/JsonSchemaForm.svelte.d.ts +7 -1
  58. package/dist/components/KanbanBoard.svelte +195 -40
  59. package/dist/components/KanbanBoard.svelte.d.ts +9 -2
  60. package/dist/components/Layout.svelte +4 -2
  61. package/dist/components/LazyAutoForm.svelte +28 -0
  62. package/dist/components/LazyAutoForm.svelte.d.ts +8 -0
  63. package/dist/components/LazyAutoTable.svelte +28 -0
  64. package/dist/components/LazyAutoTable.svelte.d.ts +8 -0
  65. package/dist/components/LazySettingsPage.svelte +28 -0
  66. package/dist/components/LazySettingsPage.svelte.d.ts +8 -0
  67. package/dist/components/LazyShowPage.svelte +28 -0
  68. package/dist/components/LazyShowPage.svelte.d.ts +8 -0
  69. package/dist/components/ListPage.svelte +5 -1
  70. package/dist/components/ListPage.svelte.d.ts +3 -0
  71. package/dist/components/LoginPage.svelte +1 -1
  72. package/dist/components/MultiTabKeepAlive.svelte +11 -6
  73. package/dist/components/NotificationsSettings.svelte +69 -2
  74. package/dist/components/NotificationsSettings.svelte.d.ts +25 -1
  75. package/dist/components/PermissionMatrix.svelte +20 -1
  76. package/dist/components/PivotExportButton.svelte +157 -0
  77. package/dist/components/PivotExportButton.svelte.d.ts +16 -0
  78. package/dist/components/PivotTable.svelte +229 -74
  79. package/dist/components/PivotTable.svelte.d.ts +19 -2
  80. package/dist/components/RadioGroup.svelte +121 -0
  81. package/dist/components/RadioGroup.svelte.d.ts +43 -0
  82. package/dist/components/RangeSlider.svelte +127 -0
  83. package/dist/components/RangeSlider.svelte.d.ts +22 -0
  84. package/dist/components/Rate.svelte +121 -0
  85. package/dist/components/Rate.svelte.d.ts +16 -0
  86. package/dist/components/RecordDetailDrawer.svelte +4 -2
  87. package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
  88. package/dist/components/RelationPicker.svelte +341 -0
  89. package/dist/components/RelationPicker.svelte.d.ts +26 -0
  90. package/dist/components/ResourceOperationsPage.svelte +18 -18
  91. package/dist/components/ResourceOperationsPage.svelte.d.ts +2 -2
  92. package/dist/components/RetryTaskButton.svelte +12 -42
  93. package/dist/components/SecuritySettings.svelte +29 -5
  94. package/dist/components/SegmentedControl.svelte +145 -0
  95. package/dist/components/SegmentedControl.svelte.d.ts +38 -0
  96. package/dist/components/SettingsPage.svelte +3 -1
  97. package/dist/components/SettingsPage.svelte.d.ts +1 -0
  98. package/dist/components/ShowPage.svelte +18 -3
  99. package/dist/components/ShowPage.svelte.d.ts +3 -0
  100. package/dist/components/Sidebar.svelte +2 -2
  101. package/dist/components/Slider.svelte +100 -0
  102. package/dist/components/Slider.svelte.d.ts +19 -0
  103. package/dist/components/SpreadsheetView.svelte +1213 -95
  104. package/dist/components/SpreadsheetView.svelte.d.ts +19 -0
  105. package/dist/components/TagsInput.svelte +178 -0
  106. package/dist/components/TagsInput.svelte.d.ts +17 -0
  107. package/dist/components/TimePicker.svelte +10 -0
  108. package/dist/components/TimePicker.svelte.d.ts +6 -0
  109. package/dist/components/TreeSelect.svelte +267 -53
  110. package/dist/components/TreeSelect.svelte.d.ts +3 -0
  111. package/dist/components/TreeTable.svelte +484 -82
  112. package/dist/components/TreeTable.svelte.d.ts +24 -1
  113. package/dist/components/TwoFactorAuthPage.svelte +85 -48
  114. package/dist/components/TwoFactorAuthPage.svelte.d.ts +12 -1
  115. package/dist/components/VirtualTable.svelte +116 -20
  116. package/dist/components/VirtualTable.svelte.d.ts +5 -0
  117. package/dist/components/account/CompanyProfilePage.svelte +74 -38
  118. package/dist/components/account/GetStartedPage.svelte +2 -1
  119. package/dist/components/account/ImportMembersPage.svelte +22 -167
  120. package/dist/components/account/ImportMembersPage.svelte.d.ts +4 -1
  121. package/dist/components/account/MemberDirectory.svelte +156 -0
  122. package/dist/components/account/MemberDirectory.svelte.d.ts +19 -0
  123. package/dist/components/account/MembersStarterPage.svelte +9 -55
  124. package/dist/components/account/MembersStarterPage.svelte.d.ts +5 -1
  125. package/dist/components/account/SecurityLogPage.svelte +4 -1
  126. package/dist/components/account/SettingsPlainPage.svelte +7 -38
  127. package/dist/components/account/SettingsPlainPage.svelte.d.ts +4 -1
  128. package/dist/components/account/SettingsSidebarPage.svelte +7 -20
  129. package/dist/components/account/TeamMembersPage.svelte +4 -20
  130. package/dist/components/account/TeamMembersPage.svelte.d.ts +5 -1
  131. package/dist/components/account/UserProfilePage.svelte +3 -25
  132. package/dist/components/account/UserProfilePage.svelte.d.ts +17 -2
  133. package/dist/components/buttons/ExportButton.svelte +56 -12
  134. package/dist/components/buttons/ExportButton.svelte.d.ts +11 -0
  135. package/dist/components/content/ContentPageHeader.svelte +1 -1
  136. package/dist/components/content/ContentPageShell.svelte +1 -1
  137. package/dist/components/content/MetricBlock.svelte +1 -1
  138. package/dist/components/content/PageToolbar.svelte +1 -1
  139. package/dist/components/content/ReferenceNotice.svelte +5 -0
  140. package/dist/components/content/ReferenceNotice.svelte.d.ts +18 -0
  141. package/dist/components/content/SectionHeader.svelte +1 -1
  142. package/dist/components/content/SettingsFieldRow.svelte +1 -1
  143. package/dist/components/content/SettingsGroup.svelte +1 -1
  144. package/dist/components/content/StatusBadge.svelte +1 -1
  145. package/dist/components/content/WorkspaceLayout.svelte +1 -1
  146. package/dist/components/export-reference.d.ts +2 -0
  147. package/dist/components/export-reference.js +9 -0
  148. package/dist/components/fieldComponentMap.js +2 -5
  149. package/dist/components/fields/DateField.svelte +12 -13
  150. package/dist/components/fields/date-display.d.ts +6 -0
  151. package/dist/components/fields/date-display.js +24 -0
  152. package/dist/components/network/TeamCrewTablePage.svelte +6 -1
  153. package/dist/components/network/UserCardsNFTPage.svelte +2 -0
  154. package/dist/components/profile/PublicProfilePage.svelte +2 -0
  155. package/dist/components/saved-list-views.d.ts +74 -1
  156. package/dist/components/saved-list-views.js +191 -3
  157. package/dist/components/table-contract.d.ts +12 -1
  158. package/dist/components/task-action.svelte.d.ts +13 -0
  159. package/dist/components/task-action.svelte.js +73 -0
  160. package/dist/components/ui/DesignSystemShowcase.svelte +164 -0
  161. package/dist/components/ui/DesignSystemShowcase.svelte.d.ts +3 -0
  162. package/dist/components/ui/badge/badge-variants.d.ts +3 -2
  163. package/dist/components/ui/badge/badge-variants.js +2 -3
  164. package/dist/components/ui/badge/badge.svelte +3 -0
  165. package/dist/components/ui/badge/badge.svelte.d.ts +1 -0
  166. package/dist/components/ui/button/button-variants.d.ts +5 -4
  167. package/dist/components/ui/button/button-variants.js +7 -5
  168. package/dist/components/ui/checkbox/checkbox.svelte +5 -1
  169. package/dist/components/ui/checkbox/checkbox.svelte.d.ts +5 -1
  170. package/dist/components/ui/input/input.svelte +7 -2
  171. package/dist/components/ui/input/input.svelte.d.ts +4 -1
  172. package/dist/components/ui/select/select.svelte +4 -1
  173. package/dist/components/ui/select/select.svelte.d.ts +2 -1
  174. package/dist/components/ui/textarea/textarea.svelte +2 -2
  175. package/dist/components.css +33 -2
  176. package/dist/date-input-policy.d.ts +4 -0
  177. package/dist/date-input-policy.js +35 -0
  178. package/dist/design-system-components.d.ts +100 -0
  179. package/dist/design-system-components.js +101 -0
  180. package/dist/design-system.js +1 -1
  181. package/dist/devtools-bridge.d.ts +80 -0
  182. package/dist/devtools-bridge.js +92 -0
  183. package/dist/index.d.ts +37 -2
  184. package/dist/index.js +23 -2
  185. package/dist/recipes.d.ts +960 -4
  186. package/dist/recipes.js +264 -4
  187. package/dist/tree-table-model.d.ts +24 -0
  188. package/dist/tree-table-model.js +111 -0
  189. package/package.json +22 -10
  190. package/dist/styled-system/css/conditions.js +0 -36
  191. package/dist/styled-system/css/css.d.ts +0 -22
  192. package/dist/styled-system/css/css.js +0 -45
  193. package/dist/styled-system/css/cva.d.ts +0 -6
  194. package/dist/styled-system/css/cva.js +0 -87
  195. package/dist/styled-system/css/cx.d.ts +0 -5
  196. package/dist/styled-system/css/cx.js +0 -15
  197. package/dist/styled-system/css/index.d.ts +0 -5
  198. package/dist/styled-system/css/index.js +0 -4
  199. package/dist/styled-system/css/sva.d.ts +0 -4
  200. package/dist/styled-system/css/sva.js +0 -46
  201. package/dist/styled-system/helpers.js +0 -316
  202. package/dist/styled-system/patterns/aspect-ratio.d.ts +0 -20
  203. package/dist/styled-system/patterns/aspect-ratio.js +0 -38
  204. package/dist/styled-system/patterns/bleed.d.ts +0 -21
  205. package/dist/styled-system/patterns/bleed.js +0 -24
  206. package/dist/styled-system/patterns/box.d.ts +0 -20
  207. package/dist/styled-system/patterns/box.js +0 -15
  208. package/dist/styled-system/patterns/center.d.ts +0 -20
  209. package/dist/styled-system/patterns/center.js +0 -21
  210. package/dist/styled-system/patterns/circle.d.ts +0 -20
  211. package/dist/styled-system/patterns/circle.js +0 -25
  212. package/dist/styled-system/patterns/container.d.ts +0 -20
  213. package/dist/styled-system/patterns/container.js +0 -21
  214. package/dist/styled-system/patterns/cq.d.ts +0 -21
  215. package/dist/styled-system/patterns/cq.js +0 -21
  216. package/dist/styled-system/patterns/divider.d.ts +0 -22
  217. package/dist/styled-system/patterns/divider.js +0 -25
  218. package/dist/styled-system/patterns/flex.d.ts +0 -26
  219. package/dist/styled-system/patterns/flex.js +0 -26
  220. package/dist/styled-system/patterns/float.d.ts +0 -23
  221. package/dist/styled-system/patterns/float.js +0 -52
  222. package/dist/styled-system/patterns/grid-item.d.ts +0 -25
  223. package/dist/styled-system/patterns/grid-item.js +0 -25
  224. package/dist/styled-system/patterns/grid.d.ts +0 -24
  225. package/dist/styled-system/patterns/grid.js +0 -27
  226. package/dist/styled-system/patterns/hstack.d.ts +0 -21
  227. package/dist/styled-system/patterns/hstack.js +0 -24
  228. package/dist/styled-system/patterns/index.d.ts +0 -21
  229. package/dist/styled-system/patterns/index.js +0 -20
  230. package/dist/styled-system/patterns/link-overlay.d.ts +0 -20
  231. package/dist/styled-system/patterns/link-overlay.js +0 -24
  232. package/dist/styled-system/patterns/spacer.d.ts +0 -20
  233. package/dist/styled-system/patterns/spacer.js +0 -25
  234. package/dist/styled-system/patterns/square.d.ts +0 -20
  235. package/dist/styled-system/patterns/square.js +0 -24
  236. package/dist/styled-system/patterns/stack.d.ts +0 -23
  237. package/dist/styled-system/patterns/stack.js +0 -24
  238. package/dist/styled-system/patterns/visually-hidden.d.ts +0 -20
  239. package/dist/styled-system/patterns/visually-hidden.js +0 -18
  240. package/dist/styled-system/patterns/vstack.d.ts +0 -21
  241. package/dist/styled-system/patterns/vstack.js +0 -24
  242. package/dist/styled-system/patterns/wrap.d.ts +0 -24
  243. package/dist/styled-system/patterns/wrap.js +0 -25
  244. package/dist/styled-system/recipes/content-header.d.ts +0 -31
  245. package/dist/styled-system/recipes/content-header.js +0 -65
  246. package/dist/styled-system/recipes/content-page.d.ts +0 -34
  247. package/dist/styled-system/recipes/content-page.js +0 -43
  248. package/dist/styled-system/recipes/create-recipe.js +0 -82
  249. package/dist/styled-system/recipes/index.d.ts +0 -17
  250. package/dist/styled-system/recipes/index.js +0 -16
  251. package/dist/styled-system/recipes/metric-block.d.ts +0 -34
  252. package/dist/styled-system/recipes/metric-block.js +0 -76
  253. package/dist/styled-system/recipes/product-list.d.ts +0 -31
  254. package/dist/styled-system/recipes/product-list.js +0 -73
  255. package/dist/styled-system/recipes/product-section.d.ts +0 -31
  256. package/dist/styled-system/recipes/product-section.js +0 -49
  257. package/dist/styled-system/recipes/product-settings-row.d.ts +0 -34
  258. package/dist/styled-system/recipes/product-settings-row.js +0 -58
  259. package/dist/styled-system/recipes/product-settings.d.ts +0 -31
  260. package/dist/styled-system/recipes/product-settings.js +0 -57
  261. package/dist/styled-system/recipes/product-status.d.ts +0 -34
  262. package/dist/styled-system/recipes/product-status.js +0 -45
  263. package/dist/styled-system/recipes/product-toolbar.d.ts +0 -31
  264. package/dist/styled-system/recipes/product-toolbar.js +0 -41
  265. package/dist/styled-system/recipes/product-workspace.d.ts +0 -34
  266. package/dist/styled-system/recipes/product-workspace.js +0 -58
  267. package/dist/styled-system/recipes/surface-metric.d.ts +0 -38
  268. package/dist/styled-system/recipes/surface-metric.js +0 -63
  269. package/dist/styled-system/recipes/surface-table.d.ts +0 -34
  270. package/dist/styled-system/recipes/surface-table.js +0 -62
  271. package/dist/styled-system/recipes/ui-badge.d.ts +0 -31
  272. package/dist/styled-system/recipes/ui-badge.js +0 -38
  273. package/dist/styled-system/recipes/ui-button.d.ts +0 -32
  274. package/dist/styled-system/recipes/ui-button.js +0 -43
  275. package/dist/styled-system/recipes/ui-input.d.ts +0 -31
  276. package/dist/styled-system/recipes/ui-input.js +0 -49
  277. package/dist/styled-system/recipes/ui-textarea.d.ts +0 -31
  278. package/dist/styled-system/recipes/ui-textarea.js +0 -24
  279. package/dist/styled-system/styles.css +0 -1257
  280. package/dist/styled-system/tokens/index.d.ts +0 -9
  281. package/dist/styled-system/tokens/index.js +0 -268
  282. package/dist/styled-system/tokens/tokens.d.ts +0 -36
  283. package/dist/styled-system/types/composition.d.ts +0 -227
  284. package/dist/styled-system/types/conditions.d.ts +0 -236
  285. package/dist/styled-system/types/csstype.d.ts +0 -22570
  286. package/dist/styled-system/types/index.d.ts +0 -6
  287. package/dist/styled-system/types/parts.d.ts +0 -8
  288. package/dist/styled-system/types/pattern.d.ts +0 -78
  289. package/dist/styled-system/types/prop-type.d.ts +0 -223
  290. package/dist/styled-system/types/recipe.d.ts +0 -181
  291. package/dist/styled-system/types/selectors.d.ts +0 -59
  292. package/dist/styled-system/types/static-css.d.ts +0 -56
  293. package/dist/styled-system/types/style-props.d.ts +0 -8088
  294. package/dist/styled-system/types/system-types.d.ts +0 -151
  295. package/dist/styles/recipes.css +0 -1262
@@ -0,0 +1,283 @@
1
+ <script lang="ts">
2
+ import { ChevronLeft, ChevronRight, Loader2, RefreshCw, Search } from '@lucide/svelte';
3
+ import { onDestroy, untrack } from 'svelte';
4
+ import { captureAdminContext, captureAuthSession, createEnterpriseRequestContext } from '@svadmin/core';
5
+ import {
6
+ ApprovalContractError, decodeApprovalList, decodeApprovalReceipt, decodeApprovalTransition,
7
+ } from '@svadmin/core';
8
+ import type {
9
+ ApprovalProvider, ApprovalRecord, ApprovalTransition, EnterpriseProviderRequestContext,
10
+ } from '@svadmin/core';
11
+ import { useTranslation } from '@svadmin/core/i18n';
12
+ import { Badge } from './ui/badge/index.js';
13
+ import { Button } from './ui/button/index.js';
14
+ import { Input } from './ui/input/index.js';
15
+
16
+ type ApprovalView = 'pending' | 'history';
17
+ interface Props {
18
+ provider: ApprovalProvider;
19
+ requestContext?: EnterpriseProviderRequestContext;
20
+ pageSize?: number;
21
+ title?: string;
22
+ }
23
+
24
+ let { provider, requestContext, pageSize = 20, title }: Props = $props();
25
+ const context = captureAdminContext();
26
+ const i18n = useTranslation();
27
+ let view = $state<ApprovalView>('pending');
28
+ let search = $state('');
29
+ let appliedSearch = $state('');
30
+ let page = $state(1);
31
+ let records = $state<ApprovalRecord[]>([]);
32
+ let total = $state(0);
33
+ let selectedId = $state<string>();
34
+ let loading = $state(false);
35
+ let error = $state<'load' | 'invalid' | 'conflict' | 'action'>();
36
+ let actionPending = $state(false);
37
+ let comment = $state('');
38
+ let targetId = $state('');
39
+ let actionDialog = $state<string>();
40
+ let timer: ReturnType<typeof setTimeout> | undefined;
41
+ let mounted = true;
42
+ let epoch = 0;
43
+ let requestScope: EnterpriseProviderRequestContext | undefined;
44
+ let generatedRequestContext: EnterpriseProviderRequestContext | undefined;
45
+ onDestroy(() => { mounted = false; clearTimeout(timer); });
46
+
47
+ const activeRecord = $derived(records.find(record => record.id === selectedId));
48
+ const activeAction = $derived(
49
+ activeRecord?.allowedActions.find(action => action.id === actionDialog),
50
+ );
51
+ const normalizedPageSize = $derived(Number.isSafeInteger(pageSize) && pageSize > 0 && pageSize <= 200 ? pageSize : 20);
52
+ const totalPages = $derived(Math.max(1, Math.ceil(total / normalizedPageSize)));
53
+
54
+ function resolveRequestContext(): EnterpriseProviderRequestContext {
55
+ if (requestContext) return requestContext;
56
+ const tenant = context.tenantCacheKey?.__svadminTenant;
57
+ if (typeof tenant !== 'string' && typeof tenant !== 'number') throw new Error('Missing tenant context');
58
+ if (generatedRequestContext?.tenantId === tenant) return generatedRequestContext;
59
+ generatedRequestContext = createEnterpriseRequestContext({ tenantId: tenant });
60
+ return generatedRequestContext;
61
+ }
62
+
63
+ async function load(): Promise<void> {
64
+ const currentEpoch = ++epoch;
65
+ loading = true;
66
+ error = undefined;
67
+ try {
68
+ const scope = resolveRequestContext();
69
+ requestScope = scope;
70
+ const result = decodeApprovalList(await provider.list(scope, {
71
+ view, page, pageSize: normalizedPageSize, search: appliedSearch,
72
+ }));
73
+ if (!mounted || currentEpoch !== epoch || requestScope !== scope) return;
74
+ records = result.data;
75
+ total = result.total;
76
+ selectedId = result.data.some(record => record.id === selectedId) ? selectedId : result.data[0]?.id;
77
+ } catch (cause) {
78
+ if (!mounted || currentEpoch !== epoch) return;
79
+ error = cause instanceof ApprovalContractError ? 'invalid' : 'load';
80
+ records = [];
81
+ total = 0;
82
+ selectedId = undefined;
83
+ } finally {
84
+ if (mounted && currentEpoch === epoch) loading = false;
85
+ }
86
+ }
87
+
88
+ function scheduleSearch(event: Event): void {
89
+ if (!(event.currentTarget instanceof HTMLInputElement)) return;
90
+ search = event.currentTarget.value;
91
+ clearTimeout(timer);
92
+ timer = setTimeout(() => {
93
+ appliedSearch = search.trim().slice(0, 200);
94
+ page = 1;
95
+ }, 250);
96
+ }
97
+
98
+ function switchView(next: ApprovalView): void {
99
+ if (view === next) return;
100
+ view = next;
101
+ page = 1;
102
+ selectedId = undefined;
103
+ }
104
+
105
+ function openAction(action: string): void {
106
+ if (!activeRecord?.allowedActions.some(candidate => candidate.id === action)) return;
107
+ actionDialog = action;
108
+ comment = '';
109
+ targetId = '';
110
+ }
111
+
112
+ async function submitAction(): Promise<void> {
113
+ const record = activeRecord;
114
+ const action = activeAction;
115
+ const scope = requestScope;
116
+ if (!record || !action || !scope || !provider.transition || actionPending) return;
117
+ let input: ApprovalTransition;
118
+ try {
119
+ input = decodeApprovalTransition({
120
+ id: record.id,
121
+ expectedVersion: record.version,
122
+ action: action.id,
123
+ comment,
124
+ ...(targetId.trim() ? { targetId: targetId.trim() } : {}),
125
+ idempotencyKey: `${record.id}:${record.version}:${action.id}:${crypto.randomUUID()}`,
126
+ }, record);
127
+ } catch {
128
+ error = 'action';
129
+ return;
130
+ }
131
+ actionPending = true;
132
+ const actionEpoch = epoch;
133
+ error = undefined;
134
+ try {
135
+ const receipt = decodeApprovalReceipt(await provider.transition(scope, input), input);
136
+ if (!mounted || actionEpoch !== epoch) return;
137
+ if (!receipt.ok) {
138
+ error = 'conflict';
139
+ return;
140
+ }
141
+ actionDialog = undefined;
142
+ records = records.map(candidate => candidate.id === receipt.record.id ? receipt.record : candidate);
143
+ selectedId = receipt.record.id;
144
+ } catch (cause) {
145
+ if (!mounted || actionEpoch !== epoch) return;
146
+ error = cause instanceof ApprovalContractError ? 'invalid' : 'action';
147
+ } finally {
148
+ if (mounted && actionEpoch === epoch) actionPending = false;
149
+ }
150
+ }
151
+
152
+ function canSubmit(action: ApprovalRecord['allowedActions'][number]): boolean {
153
+ return (!action.commentRequired || !!comment.trim()) && (!action.targetRequired || !!targetId.trim());
154
+ }
155
+
156
+ $effect(() => {
157
+ void provider;
158
+ void view;
159
+ void page;
160
+ void appliedSearch;
161
+ void requestContext;
162
+ void normalizedPageSize;
163
+ void context.tenantCacheKey?.__svadminTenant;
164
+ void captureAuthSession(context.authProvider).cacheKey;
165
+ untrack(() => {
166
+ records = [];
167
+ selectedId = undefined;
168
+ actionDialog = undefined;
169
+ actionPending = false;
170
+ void load();
171
+ });
172
+ });
173
+ </script>
174
+
175
+ <section class="svadmin-approval-center" aria-label={title ?? i18n.t('approval.title')}>
176
+ <header class="svadmin-approval-center__header">
177
+ <div>
178
+ <h2>{title ?? i18n.t('approval.title')}</h2>
179
+ </div>
180
+ <Button type="button" variant="outline" size="sm" aria-label={i18n.t('common.refresh')} onclick={() => void load()}>
181
+ <RefreshCw class={loading ? 'svadmin-approval-center__spin' : ''} />
182
+ </Button>
183
+ </header>
184
+ <div class="svadmin-approval-center__toolbar">
185
+ <Button type="button" aria-pressed={view === 'pending'} variant={view === 'pending' ? 'default' : 'outline'} onclick={() => switchView('pending')}>{i18n.t('approval.pending')}</Button>
186
+ <Button type="button" aria-pressed={view === 'history'} variant={view === 'history' ? 'default' : 'outline'} onclick={() => switchView('history')}>{i18n.t('approval.history')}</Button>
187
+ <label class="svadmin-approval-center__search">
188
+ <Search aria-hidden="true" />
189
+ <Input value={search} oninput={scheduleSearch} placeholder={i18n.t('approval.search')} aria-label={i18n.t('approval.search')} />
190
+ </label>
191
+ </div>
192
+ {#if error}
193
+ <div role="alert">{i18n.t(`approval.${error}`)}</div>
194
+ {/if}
195
+ <div class="svadmin-approval-center__body">
196
+ <div class="svadmin-approval-center__list">
197
+ {#if loading}
198
+ <div role="status"><Loader2 class="svadmin-approval-center__spin" /> {i18n.t('common.loading')}</div>
199
+ {:else if records.length === 0}
200
+ <div role="status">{i18n.t('approval.empty')}</div>
201
+ {:else}
202
+ {#each records as record (record.id)}
203
+ <button type="button" aria-pressed={record.id === selectedId} class:selected={record.id === selectedId} onclick={() => selectedId = record.id}>
204
+ <strong>{record.title}</strong>
205
+ <span>{record.applicant} · {record.status}</span>
206
+ </button>
207
+ {/each}
208
+ {/if}
209
+ </div>
210
+ <div class="svadmin-approval-center__detail">
211
+ {#if activeRecord}
212
+ <h3>{activeRecord.title}</h3>
213
+ <Badge variant="outline">{activeRecord.status}</Badge>
214
+ <p>{i18n.t('approval.applicant', { name: activeRecord.applicant })}</p>
215
+ {#if activeRecord.attachments.length}
216
+ <h4>{i18n.t('approval.attachments')}</h4>
217
+ <ul>{#each activeRecord.attachments as attachment (attachment.id)}<li>{attachment.name}</li>{/each}</ul>
218
+ {/if}
219
+ <h4>{i18n.t('approval.history')}</h4>
220
+ <ol>{#each activeRecord.history as event (event.id)}<li>{event.actor} · {event.action} · {event.comment || '—'}</li>{/each}</ol>
221
+ {#if provider.transition && activeRecord.allowedActions.length}
222
+ <div class="svadmin-approval-center__actions">
223
+ {#each activeRecord.allowedActions as action (action.id)}
224
+ <Button type="button" disabled={actionPending} onclick={() => openAction(action.id)}>{action.label}</Button>
225
+ {/each}
226
+ </div>
227
+ {/if}
228
+ {:else}
229
+ <p>{i18n.t('approval.select')}</p>
230
+ {/if}
231
+ </div>
232
+ </div>
233
+ <footer class="svadmin-approval-center__footer">
234
+ <Button type="button" aria-label={i18n.t('common.prev')} title={i18n.t('common.prev')} variant="outline" disabled={page <= 1 || loading} onclick={() => { page -= 1; }}>
235
+ <ChevronLeft />
236
+ </Button>
237
+ <span>{page} / {totalPages}</span>
238
+ <Button type="button" aria-label={i18n.t('common.next')} title={i18n.t('common.next')} variant="outline" disabled={page >= totalPages || loading} onclick={() => { page += 1; }}>
239
+ <ChevronRight />
240
+ </Button>
241
+ </footer>
242
+ </section>
243
+
244
+ {#if actionDialog && activeRecord}
245
+ <div class="svadmin-approval-center__dialog" role="dialog" aria-modal="true" aria-label={activeAction?.label ?? i18n.t('common.confirmAction')}>
246
+ <h3>{activeAction?.label}</h3>
247
+ <label>{i18n.t('approval.comment')}
248
+ <textarea bind:value={comment} required={activeAction?.commentRequired} maxlength={10000}></textarea>
249
+ </label>
250
+ {#if activeAction?.targetRequired}
251
+ <label>{i18n.t('approval.target')}
252
+ <Input bind:value={targetId} required maxlength={200} />
253
+ </label>
254
+ {/if}
255
+ <div>
256
+ <Button type="button" variant="outline" onclick={() => actionDialog = undefined}>{i18n.t('common.cancel')}</Button>
257
+ <Button type="button" disabled={actionPending || !activeAction || !canSubmit(activeAction)} onclick={() => void submitAction()}>
258
+ {actionPending ? i18n.t('common.processing') : i18n.t('common.confirm')}
259
+ </Button>
260
+ </div>
261
+ </div>
262
+ {/if}
263
+
264
+ <style>
265
+ .svadmin-approval-center { display: grid; gap: 1rem; min-width: 0; }
266
+ .svadmin-approval-center__header, .svadmin-approval-center__toolbar, .svadmin-approval-center__footer,
267
+ .svadmin-approval-center__actions { display: flex; align-items: center; gap: 0.5rem; }
268
+ .svadmin-approval-center__header, .svadmin-approval-center__footer { justify-content: space-between; }
269
+ .svadmin-approval-center__toolbar { flex-wrap: wrap; }
270
+ .svadmin-approval-center__search { display: flex; align-items: center; gap: 0.5rem; min-width: min(100%, 20rem); }
271
+ .svadmin-approval-center__body { display: grid; grid-template-columns: minmax(12rem, 0.8fr) minmax(0, 1.6fr); border: 1px solid var(--border); }
272
+ .svadmin-approval-center__list { border-inline-end: 1px solid var(--border); }
273
+ .svadmin-approval-center__list button { display: grid; gap: 0.25rem; width: 100%; padding: 0.75rem; text-align: start; border-block-end: 1px solid var(--border); }
274
+ .svadmin-approval-center__list button.selected { background: var(--muted); }
275
+ .svadmin-approval-center__list span { color: var(--muted-foreground); font-size: 0.875rem; }
276
+ .svadmin-approval-center__detail { display: grid; gap: 0.75rem; padding: 1rem; min-width: 0; }
277
+ .svadmin-approval-center__detail ol, .svadmin-approval-center__detail ul { margin: 0; padding-inline-start: 1.25rem; }
278
+ .svadmin-approval-center__dialog { position: fixed; inset: 20% auto auto 50%; z-index: 20; display: grid; gap: 1rem; width: min(28rem, calc(100vw - 2rem)); transform: translateX(-50%); padding: 1rem; background: var(--card); border: 1px solid var(--border); box-shadow: var(--shadow-lg); }
279
+ .svadmin-approval-center__dialog textarea { min-height: 6rem; width: 100%; }
280
+ :global(.svadmin-approval-center__spin) { animation: svadmin-approval-spin 1s linear infinite; }
281
+ @keyframes svadmin-approval-spin { to { transform: rotate(360deg); } }
282
+ @media (max-width: 640px) { .svadmin-approval-center__body { grid-template-columns: 1fr; } .svadmin-approval-center__list { border-inline-end: 0; border-block-end: 1px solid var(--border); max-height: 15rem; overflow: auto; } }
283
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { ApprovalProvider, EnterpriseProviderRequestContext } from '@svadmin/core';
2
+ interface Props {
3
+ provider: ApprovalProvider;
4
+ requestContext?: EnterpriseProviderRequestContext;
5
+ pageSize?: number;
6
+ title?: string;
7
+ }
8
+ declare const ApprovalCenter: import("svelte").Component<Props, {}, "">;
9
+ type ApprovalCenter = ReturnType<typeof ApprovalCenter>;
10
+ export default ApprovalCenter;
@@ -58,7 +58,8 @@
58
58
  total = result.total;
59
59
  } catch (e) {
60
60
  if (currentId !== requestId) return;
61
- toast.error((e as Error).message);
61
+ error = e instanceof Error ? e.message : String(e);
62
+ toast.error(error);
62
63
  } finally {
63
64
  if (currentId === requestId) loading = false;
64
65
  }
@@ -118,6 +119,7 @@
118
119
  {#if error}
119
120
  <div class="svadmin-u-0478c89a150f svadmin-u-ca6bcd4b6f3f svadmin-u-f0c1e65bd6f2 svadmin-u-7a0854fdbc30 svadmin-u-811148b13d1e svadmin-u-5f22e64f2282 svadmin-u-ca6bf63030aa">
120
121
  <p>{error}</p>
122
+ <button type="button" onclick={() => void loadLogs(authProvider, page)}>{i18n.t('common.retry')}</button>
121
123
  </div>
122
124
  {:else}
123
125
  <!-- Search Bar -->
@@ -18,6 +18,7 @@
18
18
  import * as Alert from './ui/alert/index.js';
19
19
  import { Skeleton } from './ui/skeleton/index.js';
20
20
  import ConfirmDialog from './ConfirmDialog.svelte';
21
+ import ErrorSummary from './ErrorSummary.svelte';
21
22
  import { cn } from '../utils.js';
22
23
  import type { Snippet } from 'svelte';
23
24
 
@@ -38,6 +39,7 @@
38
39
  redirect?: 'list' | false;
39
40
  onCancel?: () => void;
40
41
  onNavigationGuardReady?: (guard: (fn: () => void) => void) => void;
42
+ showErrorSummary?: boolean;
41
43
  }
42
44
 
43
45
  let {
@@ -55,6 +57,7 @@
55
57
  redirect = 'list',
56
58
  onCancel,
57
59
  onNavigationGuardReady,
60
+ showErrorSummary = true,
58
61
  }: Props = $props();
59
62
  const navigation = useNavigation();
60
63
  const context = captureAdminContext();
@@ -62,6 +65,7 @@
62
65
  const activeRendering = $derived(bindResourceRendering(rendering, binding.resource));
63
66
  const isReadonly = $derived(mode === 'show');
64
67
  const isCompact = $derived(density === 'compact');
68
+ const instanceId = $props.id();
65
69
 
66
70
  // ─── Resource metadata ────────────────────────────────────────────
67
71
  const resource = $derived(context.getResource(resourceName));
@@ -132,6 +136,7 @@
132
136
  case 'boolean': return false;
133
137
  case 'tags': case 'images': case 'multiselect': return [];
134
138
  case 'select': return field.options?.[0]?.value ?? '';
139
+ case 'relation': return field.multiple ? [] : null;
135
140
  case 'json': return {};
136
141
  default: return '';
137
142
  }
@@ -161,6 +166,11 @@
161
166
  return callback ? () => callback() : undefined;
162
167
  },
163
168
  }));
169
+ const errorEntries = $derived(formFields.flatMap(field => {
170
+ const message = form.errors[field.key];
171
+ return typeof message === 'string' && message.length > 0
172
+ ? [{ fieldKey: field.key, label: field.label || field.key, message }] : [];
173
+ }));
164
174
 
165
175
  const renderedDraft = $derived.by(() => {
166
176
  try {
@@ -192,7 +202,7 @@
192
202
  $effect(() => () => { mounted = false; submission = undefined; });
193
203
 
194
204
  function fieldErrorId(fieldKey: string): string {
195
- return `${resourceName}-${fieldKey}-error`;
205
+ return `${instanceId}-${resourceName}-${fieldKey}-error`;
196
206
  }
197
207
 
198
208
  function focusFirstInvalidField(): void {
@@ -200,6 +210,15 @@
200
210
  firstInvalid?.focus();
201
211
  }
202
212
 
213
+ function focusField(fieldKey: string): void {
214
+ const field = formElement?.querySelector<HTMLElement>(
215
+ `[data-svadmin-field-key="${CSS.escape(fieldKey)}"]`,
216
+ );
217
+ field?.querySelector<HTMLElement>(
218
+ 'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled]), button:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])',
219
+ )?.focus();
220
+ }
221
+
203
222
  const handleSubmit = $derived.by(() => {
204
223
  const current = scope;
205
224
  return async () => {
@@ -240,6 +259,8 @@
240
259
  const pageTitle = $derived(
241
260
  mode === 'create'
242
261
  ? `${i18n.t('common.create')}${resource.label}`
262
+ : mode === 'clone'
263
+ ? `${i18n.t('common.clone')} ${resource.label}`
243
264
  : mode === 'show'
244
265
  ? `${i18n.t('common.detail')}${resource.label}`
245
266
  : `${i18n.t('common.edit')}${resource.label}`
@@ -344,6 +365,15 @@
344
365
  </div>
345
366
  {/if}
346
367
 
368
+ {#if showErrorSummary && errorEntries.length > 0}
369
+ <ErrorSummary
370
+ errors={errorEntries}
371
+ title={i18n.t('common.formErrors')}
372
+ id={`${instanceId}-error-summary`}
373
+ onfocusfield={focusField}
374
+ />
375
+ {/if}
376
+
347
377
  {#if hasGroups}
348
378
  {#each groups as group, _i (_i)}
349
379
  <Card.Root class="svadmin-u-6ee2d41e2d2d svadmin-u-438b2237b8d6">
@@ -355,7 +385,7 @@
355
385
  <Card.Content class={isCompact ? 'svadmin-u-f0faeb26d656 svadmin-u-9fcd8a13827e svadmin-u-9335c39f6eff' : 'svadmin-u-f0faeb26d656 svadmin-u-7a9aabfcd059 svadmin-u-9fcd8a13827e svadmin-u-050494726fba svadmin-u-9335c39f6eff'}>
356
386
  <div class={gridClass}>
357
387
  {#each group.fields as field (field.key)}
358
- <div class={cn(columns > 1 && isFullWidthField(field) && 'svadmin-u-2c955d1b45df', !!form.errors[field.key] && 'svadmin-u-ee1a5af3aa10')}>
388
+ <div data-svadmin-field-key={field.key} class={cn(columns > 1 && isFullWidthField(field) && 'svadmin-u-2c955d1b45df', !!form.errors[field.key] && 'svadmin-u-ee1a5af3aa10')}>
359
389
  {#if fieldRenderer}
360
390
  {@render fieldRenderer({ field, value: renderedDraft.values[field.key], onchange: fieldChange(field.key) })}
361
391
  {:else}
@@ -383,7 +413,7 @@
383
413
  <Card.Content class={isCompact ? 'svadmin-u-8e63407b5ceb' : 'svadmin-u-f0faeb26d656 svadmin-u-52be28846b5f svadmin-u-9fcd8a13827e svadmin-u-7a9aabfcd059 svadmin-u-0a58453f3755 svadmin-u-050494726fba'}>
384
414
  <div class={gridClass} data-svadmin-form-grid data-columns={columns} data-density={density}>
385
415
  {#each formFields as field (field.key)}
386
- <div class={cn(columns > 1 && isFullWidthField(field) && 'svadmin-u-2c955d1b45df', !!form.errors[field.key] && 'svadmin-u-ee1a5af3aa10')}>
416
+ <div data-svadmin-field-key={field.key} class={cn(columns > 1 && isFullWidthField(field) && 'svadmin-u-2c955d1b45df', !!form.errors[field.key] && 'svadmin-u-ee1a5af3aa10')}>
387
417
  {#if fieldRenderer}
388
418
  {@render fieldRenderer({ field, value: renderedDraft.values[field.key], onchange: fieldChange(field.key) })}
389
419
  {:else}
@@ -23,6 +23,7 @@ interface Props {
23
23
  redirect?: 'list' | false;
24
24
  onCancel?: () => void;
25
25
  onNavigationGuardReady?: (guard: (fn: () => void) => void) => void;
26
+ showErrorSummary?: boolean;
26
27
  }
27
28
  declare const AutoForm: import("svelte").Component<Props, {}, "">;
28
29
  type AutoForm = ReturnType<typeof AutoForm>;