@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,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 -->
@@ -1,4 +1,5 @@
1
1
  <script lang="ts">
2
+ import { bindResourceRendering, type ResourceRendering } from '../rendering/index.js';
2
3
  import { definedReactiveOptions } from '@svadmin/core/options';
3
4
 
4
5
  import { definedOptions } from '@svadmin/core/options';
@@ -17,6 +18,7 @@
17
18
  import * as Alert from './ui/alert/index.js';
18
19
  import { Skeleton } from './ui/skeleton/index.js';
19
20
  import ConfirmDialog from './ConfirmDialog.svelte';
21
+ import ErrorSummary from './ErrorSummary.svelte';
20
22
  import { cn } from '../utils.js';
21
23
  import type { Snippet } from 'svelte';
22
24
 
@@ -24,6 +26,7 @@
24
26
 
25
27
  interface Props {
26
28
  resourceName: string;
29
+ rendering?: ResourceRendering | undefined;
27
30
  id?: string | number;
28
31
  mode?: 'create' | 'edit' | 'clone' | 'show';
29
32
  density?: 'compact' | 'comfortable';
@@ -36,10 +39,12 @@
36
39
  redirect?: 'list' | false;
37
40
  onCancel?: () => void;
38
41
  onNavigationGuardReady?: (guard: (fn: () => void) => void) => void;
42
+ showErrorSummary?: boolean;
39
43
  }
40
44
 
41
45
  let {
42
46
  resourceName,
47
+ rendering,
43
48
  id,
44
49
  mode = 'create',
45
50
  density = 'comfortable',
@@ -52,12 +57,15 @@
52
57
  redirect = 'list',
53
58
  onCancel,
54
59
  onNavigationGuardReady,
60
+ showErrorSummary = true,
55
61
  }: Props = $props();
56
62
  const navigation = useNavigation();
57
63
  const context = captureAdminContext();
58
64
  const binding = useResourceContract(() => resourceName);
65
+ const activeRendering = $derived(bindResourceRendering(rendering, binding.resource));
59
66
  const isReadonly = $derived(mode === 'show');
60
67
  const isCompact = $derived(density === 'compact');
68
+ const instanceId = $props.id();
61
69
 
62
70
  // ─── Resource metadata ────────────────────────────────────────────
63
71
  const resource = $derived(context.getResource(resourceName));
@@ -128,6 +136,7 @@
128
136
  case 'boolean': return false;
129
137
  case 'tags': case 'images': case 'multiselect': return [];
130
138
  case 'select': return field.options?.[0]?.value ?? '';
139
+ case 'relation': return field.multiple ? [] : null;
131
140
  case 'json': return {};
132
141
  default: return '';
133
142
  }
@@ -143,7 +152,7 @@
143
152
 
144
153
  // ─── useForm: single source of truth for values, errors, tainted ──
145
154
  const form = useForm(definedReactiveOptions({
146
- get resource() { return binding.resource; },
155
+ get resource() { void activeRendering; return binding.resource; },
147
156
  get action() { return mode; },
148
157
  get id() { return id; },
149
158
  get defaultValues() { return defaults; },
@@ -157,6 +166,19 @@
157
166
  return callback ? () => callback() : undefined;
158
167
  },
159
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
+ }));
174
+
175
+ const renderedDraft = $derived.by(() => {
176
+ try {
177
+ return { ok: true, values: form.ready && activeRendering ? activeRendering.draft(mode, form.values) : form.values };
178
+ } catch {
179
+ return { ok: false, values: {} };
180
+ }
181
+ });
160
182
 
161
183
  // ─── Submission error (non-field, e.g. network error) ─────────────
162
184
  let submitError = $state<string | null>(null);
@@ -180,7 +202,7 @@
180
202
  $effect(() => () => { mounted = false; submission = undefined; });
181
203
 
182
204
  function fieldErrorId(fieldKey: string): string {
183
- return `${resourceName}-${fieldKey}-error`;
205
+ return `${instanceId}-${resourceName}-${fieldKey}-error`;
184
206
  }
185
207
 
186
208
  function focusFirstInvalidField(): void {
@@ -188,6 +210,15 @@
188
210
  firstInvalid?.focus();
189
211
  }
190
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
+
191
222
  const handleSubmit = $derived.by(() => {
192
223
  const current = scope;
193
224
  return async () => {
@@ -228,6 +259,8 @@
228
259
  const pageTitle = $derived(
229
260
  mode === 'create'
230
261
  ? `${i18n.t('common.create')}${resource.label}`
262
+ : mode === 'clone'
263
+ ? `${i18n.t('common.clone')} ${resource.label}`
231
264
  : mode === 'show'
232
265
  ? `${i18n.t('common.detail')}${resource.label}`
233
266
  : `${i18n.t('common.edit')}${resource.label}`
@@ -282,7 +315,7 @@
282
315
  });
283
316
  </script>
284
317
 
285
- <div class={isCompact ? 'svadmin-u-3e7ce58d64fa' : 'svadmin-u-b3542e058833'}>
318
+ <div class={isCompact ? 'svadmin-u-3e7ce58d64fa' : 'svadmin-u-b3542e058833'} data-svadmin-rendering-resource={activeRendering?.resource.name} data-svadmin-rendering-kind={activeRendering ? 'form' : undefined}>
286
319
  {#if showHeader}
287
320
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-0c3bc98565dd">
288
321
  <TooltipButton
@@ -319,6 +352,8 @@
319
352
  <Button type="button" variant="outline" onclick={retry}>
320
353
  {i18n.t('common.retry')}
321
354
  </Button>
355
+ {:else if !renderedDraft.ok}
356
+ <p role="alert">{i18n.t('common.operationFailed')}</p>
322
357
  {:else if form.ready}
323
358
  <form bind:this={formElement} onsubmit={submitEvent} class="svadmin-u-cf3893e36c22 svadmin-u-b3542e058833" novalidate>
324
359
  {#if submitError}
@@ -330,6 +365,15 @@
330
365
  </div>
331
366
  {/if}
332
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
+
333
377
  {#if hasGroups}
334
378
  {#each groups as group, _i (_i)}
335
379
  <Card.Root class="svadmin-u-6ee2d41e2d2d svadmin-u-438b2237b8d6">
@@ -341,13 +385,13 @@
341
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'}>
342
386
  <div class={gridClass}>
343
387
  {#each group.fields as field (field.key)}
344
- <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')}>
345
389
  {#if fieldRenderer}
346
- {@render fieldRenderer({ field, value: form.values[field.key], onchange: fieldChange(field.key) })}
390
+ {@render fieldRenderer({ field, value: renderedDraft.values[field.key], onchange: fieldChange(field.key) })}
347
391
  {:else}
348
392
  <FieldRenderer
349
393
  {field}
350
- value={form.values[field.key]}
394
+ value={renderedDraft.values[field.key]}
351
395
  onchange={fieldChange(field.key)}
352
396
  {density}
353
397
  invalid={!!form.errors[field.key]}
@@ -369,13 +413,13 @@
369
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'}>
370
414
  <div class={gridClass} data-svadmin-form-grid data-columns={columns} data-density={density}>
371
415
  {#each formFields as field (field.key)}
372
- <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')}>
373
417
  {#if fieldRenderer}
374
- {@render fieldRenderer({ field, value: form.values[field.key], onchange: fieldChange(field.key) })}
418
+ {@render fieldRenderer({ field, value: renderedDraft.values[field.key], onchange: fieldChange(field.key) })}
375
419
  {:else}
376
420
  <FieldRenderer
377
421
  {field}
378
- value={form.values[field.key]}
422
+ value={renderedDraft.values[field.key]}
379
423
  onchange={fieldChange(field.key)}
380
424
  {density}
381
425
  invalid={!!form.errors[field.key]}
@@ -1,7 +1,9 @@
1
+ import { type ResourceRendering } from '../rendering/index.js';
1
2
  import type { FieldDefinition } from '@svadmin/core';
2
3
  import type { Snippet } from 'svelte';
3
4
  interface Props {
4
5
  resourceName: string;
6
+ rendering?: ResourceRendering | undefined;
5
7
  id?: string | number;
6
8
  mode?: 'create' | 'edit' | 'clone' | 'show';
7
9
  density?: 'compact' | 'comfortable';
@@ -21,6 +23,7 @@ interface Props {
21
23
  redirect?: 'list' | false;
22
24
  onCancel?: () => void;
23
25
  onNavigationGuardReady?: (guard: (fn: () => void) => void) => void;
26
+ showErrorSummary?: boolean;
24
27
  }
25
28
  declare const AutoForm: import("svelte").Component<Props, {}, "">;
26
29
  type AutoForm = ReturnType<typeof AutoForm>;