@svadmin/ui 0.73.0 → 0.75.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 (303) 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 +24 -15
  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 +85 -16
  37. package/dist/components/DateTimeInput.svelte.d.ts +19 -11
  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/LazyPage.svelte +3 -3
  66. package/dist/components/LazyPage.svelte.d.ts +2 -2
  67. package/dist/components/LazySettingsPage.svelte +28 -0
  68. package/dist/components/LazySettingsPage.svelte.d.ts +8 -0
  69. package/dist/components/LazyShowPage.svelte +28 -0
  70. package/dist/components/LazyShowPage.svelte.d.ts +8 -0
  71. package/dist/components/ListPage.svelte +5 -1
  72. package/dist/components/ListPage.svelte.d.ts +3 -0
  73. package/dist/components/LoginPage.svelte +1 -1
  74. package/dist/components/MultiTabKeepAlive.svelte +11 -6
  75. package/dist/components/NotificationsSettings.svelte +69 -2
  76. package/dist/components/NotificationsSettings.svelte.d.ts +25 -1
  77. package/dist/components/PasswordInput.svelte +3 -3
  78. package/dist/components/PasswordInput.svelte.d.ts +2 -1
  79. package/dist/components/PermissionMatrix.svelte +20 -1
  80. package/dist/components/PivotExportButton.svelte +157 -0
  81. package/dist/components/PivotExportButton.svelte.d.ts +16 -0
  82. package/dist/components/PivotTable.svelte +229 -74
  83. package/dist/components/PivotTable.svelte.d.ts +19 -2
  84. package/dist/components/RadioGroup.svelte +121 -0
  85. package/dist/components/RadioGroup.svelte.d.ts +43 -0
  86. package/dist/components/RangeSlider.svelte +127 -0
  87. package/dist/components/RangeSlider.svelte.d.ts +22 -0
  88. package/dist/components/Rate.svelte +121 -0
  89. package/dist/components/Rate.svelte.d.ts +16 -0
  90. package/dist/components/RecordDetailDrawer.svelte +4 -2
  91. package/dist/components/RecordDetailDrawer.svelte.d.ts +2 -0
  92. package/dist/components/RelationPicker.svelte +341 -0
  93. package/dist/components/RelationPicker.svelte.d.ts +26 -0
  94. package/dist/components/ResourceOperationsPage.svelte +23 -19
  95. package/dist/components/ResourceOperationsPage.svelte.d.ts +2 -2
  96. package/dist/components/RetryTaskButton.svelte +12 -42
  97. package/dist/components/SecuritySettings.svelte +29 -5
  98. package/dist/components/SegmentedControl.svelte +145 -0
  99. package/dist/components/SegmentedControl.svelte.d.ts +38 -0
  100. package/dist/components/SettingsPage.svelte +3 -1
  101. package/dist/components/SettingsPage.svelte.d.ts +1 -0
  102. package/dist/components/ShowPage.svelte +18 -3
  103. package/dist/components/ShowPage.svelte.d.ts +3 -0
  104. package/dist/components/Sidebar.svelte +2 -2
  105. package/dist/components/Slider.svelte +100 -0
  106. package/dist/components/Slider.svelte.d.ts +19 -0
  107. package/dist/components/SpreadsheetView.svelte +1213 -95
  108. package/dist/components/SpreadsheetView.svelte.d.ts +19 -0
  109. package/dist/components/TagsInput.svelte +178 -0
  110. package/dist/components/TagsInput.svelte.d.ts +17 -0
  111. package/dist/components/TimePicker.svelte +10 -0
  112. package/dist/components/TimePicker.svelte.d.ts +6 -0
  113. package/dist/components/TooltipButton.svelte +3 -4
  114. package/dist/components/TreeSelect.svelte +267 -53
  115. package/dist/components/TreeSelect.svelte.d.ts +3 -0
  116. package/dist/components/TreeTable.svelte +484 -82
  117. package/dist/components/TreeTable.svelte.d.ts +24 -1
  118. package/dist/components/TwoFactorAuthPage.svelte +85 -48
  119. package/dist/components/TwoFactorAuthPage.svelte.d.ts +12 -1
  120. package/dist/components/VirtualTable.svelte +116 -20
  121. package/dist/components/VirtualTable.svelte.d.ts +5 -0
  122. package/dist/components/account/CompanyProfilePage.svelte +74 -38
  123. package/dist/components/account/GetStartedPage.svelte +2 -1
  124. package/dist/components/account/ImportMembersPage.svelte +22 -167
  125. package/dist/components/account/ImportMembersPage.svelte.d.ts +4 -1
  126. package/dist/components/account/MemberDirectory.svelte +156 -0
  127. package/dist/components/account/MemberDirectory.svelte.d.ts +19 -0
  128. package/dist/components/account/MembersStarterPage.svelte +9 -55
  129. package/dist/components/account/MembersStarterPage.svelte.d.ts +5 -1
  130. package/dist/components/account/SecurityLogPage.svelte +4 -1
  131. package/dist/components/account/SettingsPlainPage.svelte +7 -38
  132. package/dist/components/account/SettingsPlainPage.svelte.d.ts +4 -1
  133. package/dist/components/account/SettingsSidebarPage.svelte +7 -20
  134. package/dist/components/account/TeamMembersPage.svelte +4 -20
  135. package/dist/components/account/TeamMembersPage.svelte.d.ts +5 -1
  136. package/dist/components/account/UserProfilePage.svelte +3 -25
  137. package/dist/components/account/UserProfilePage.svelte.d.ts +17 -2
  138. package/dist/components/buttons/ExportButton.svelte +56 -12
  139. package/dist/components/buttons/ExportButton.svelte.d.ts +11 -0
  140. package/dist/components/content/ContentPageHeader.svelte +1 -1
  141. package/dist/components/content/ContentPageShell.svelte +1 -1
  142. package/dist/components/content/MetricBlock.svelte +1 -1
  143. package/dist/components/content/PageToolbar.svelte +1 -1
  144. package/dist/components/content/ReferenceNotice.svelte +5 -0
  145. package/dist/components/content/ReferenceNotice.svelte.d.ts +18 -0
  146. package/dist/components/content/SectionHeader.svelte +1 -1
  147. package/dist/components/content/SettingsFieldRow.svelte +1 -1
  148. package/dist/components/content/SettingsGroup.svelte +1 -1
  149. package/dist/components/content/StatusBadge.svelte +1 -1
  150. package/dist/components/content/WorkspaceLayout.svelte +1 -1
  151. package/dist/components/export-reference.d.ts +2 -0
  152. package/dist/components/export-reference.js +9 -0
  153. package/dist/components/fieldComponentMap.js +2 -5
  154. package/dist/components/fields/DateField.svelte +12 -13
  155. package/dist/components/fields/MarkdownField.svelte +14 -5
  156. package/dist/components/fields/date-display.d.ts +6 -0
  157. package/dist/components/fields/date-display.js +24 -0
  158. package/dist/components/network/TeamCrewTablePage.svelte +6 -1
  159. package/dist/components/network/UserCardsNFTPage.svelte +2 -0
  160. package/dist/components/profile/PublicProfilePage.svelte +2 -0
  161. package/dist/components/saved-list-views.d.ts +74 -1
  162. package/dist/components/saved-list-views.js +191 -3
  163. package/dist/components/table-contract.d.ts +12 -1
  164. package/dist/components/task-action.svelte.d.ts +13 -0
  165. package/dist/components/task-action.svelte.js +73 -0
  166. package/dist/components/ui/DesignSystemShowcase.svelte +164 -0
  167. package/dist/components/ui/DesignSystemShowcase.svelte.d.ts +3 -0
  168. package/dist/components/ui/badge/badge-variants.d.ts +3 -2
  169. package/dist/components/ui/badge/badge-variants.js +2 -3
  170. package/dist/components/ui/badge/badge.svelte +3 -0
  171. package/dist/components/ui/badge/badge.svelte.d.ts +1 -0
  172. package/dist/components/ui/button/button-variants.d.ts +5 -4
  173. package/dist/components/ui/button/button-variants.js +7 -5
  174. package/dist/components/ui/checkbox/checkbox.svelte +5 -1
  175. package/dist/components/ui/checkbox/checkbox.svelte.d.ts +5 -1
  176. package/dist/components/ui/dropdown-menu/dropdown-menu-trigger.svelte +1 -2
  177. package/dist/components/ui/input/input.svelte +7 -2
  178. package/dist/components/ui/input/input.svelte.d.ts +4 -1
  179. package/dist/components/ui/select/select.svelte +4 -1
  180. package/dist/components/ui/select/select.svelte.d.ts +2 -1
  181. package/dist/components/ui/textarea/textarea.svelte +2 -2
  182. package/dist/components.css +33 -2
  183. package/dist/date-input-policy.d.ts +4 -0
  184. package/dist/date-input-policy.js +35 -0
  185. package/dist/design-system-components.d.ts +100 -0
  186. package/dist/design-system-components.js +101 -0
  187. package/dist/design-system.js +1 -1
  188. package/dist/devtools-bridge.d.ts +80 -0
  189. package/dist/devtools-bridge.js +92 -0
  190. package/dist/editor-config.svelte.d.ts +4 -2
  191. package/dist/index.d.ts +37 -2
  192. package/dist/index.js +23 -2
  193. package/dist/recipes.d.ts +960 -4
  194. package/dist/recipes.js +264 -4
  195. package/dist/tree-table-model.d.ts +24 -0
  196. package/dist/tree-table-model.js +111 -0
  197. package/dist/types.d.ts +4 -4
  198. package/package.json +22 -10
  199. package/dist/styled-system/css/conditions.js +0 -36
  200. package/dist/styled-system/css/css.d.ts +0 -22
  201. package/dist/styled-system/css/css.js +0 -45
  202. package/dist/styled-system/css/cva.d.ts +0 -6
  203. package/dist/styled-system/css/cva.js +0 -87
  204. package/dist/styled-system/css/cx.d.ts +0 -5
  205. package/dist/styled-system/css/cx.js +0 -15
  206. package/dist/styled-system/css/index.d.ts +0 -5
  207. package/dist/styled-system/css/index.js +0 -4
  208. package/dist/styled-system/css/sva.d.ts +0 -4
  209. package/dist/styled-system/css/sva.js +0 -46
  210. package/dist/styled-system/helpers.js +0 -316
  211. package/dist/styled-system/patterns/aspect-ratio.d.ts +0 -20
  212. package/dist/styled-system/patterns/aspect-ratio.js +0 -38
  213. package/dist/styled-system/patterns/bleed.d.ts +0 -21
  214. package/dist/styled-system/patterns/bleed.js +0 -24
  215. package/dist/styled-system/patterns/box.d.ts +0 -20
  216. package/dist/styled-system/patterns/box.js +0 -15
  217. package/dist/styled-system/patterns/center.d.ts +0 -20
  218. package/dist/styled-system/patterns/center.js +0 -21
  219. package/dist/styled-system/patterns/circle.d.ts +0 -20
  220. package/dist/styled-system/patterns/circle.js +0 -25
  221. package/dist/styled-system/patterns/container.d.ts +0 -20
  222. package/dist/styled-system/patterns/container.js +0 -21
  223. package/dist/styled-system/patterns/cq.d.ts +0 -21
  224. package/dist/styled-system/patterns/cq.js +0 -21
  225. package/dist/styled-system/patterns/divider.d.ts +0 -22
  226. package/dist/styled-system/patterns/divider.js +0 -25
  227. package/dist/styled-system/patterns/flex.d.ts +0 -26
  228. package/dist/styled-system/patterns/flex.js +0 -26
  229. package/dist/styled-system/patterns/float.d.ts +0 -23
  230. package/dist/styled-system/patterns/float.js +0 -52
  231. package/dist/styled-system/patterns/grid-item.d.ts +0 -25
  232. package/dist/styled-system/patterns/grid-item.js +0 -25
  233. package/dist/styled-system/patterns/grid.d.ts +0 -24
  234. package/dist/styled-system/patterns/grid.js +0 -27
  235. package/dist/styled-system/patterns/hstack.d.ts +0 -21
  236. package/dist/styled-system/patterns/hstack.js +0 -24
  237. package/dist/styled-system/patterns/index.d.ts +0 -21
  238. package/dist/styled-system/patterns/index.js +0 -20
  239. package/dist/styled-system/patterns/link-overlay.d.ts +0 -20
  240. package/dist/styled-system/patterns/link-overlay.js +0 -24
  241. package/dist/styled-system/patterns/spacer.d.ts +0 -20
  242. package/dist/styled-system/patterns/spacer.js +0 -25
  243. package/dist/styled-system/patterns/square.d.ts +0 -20
  244. package/dist/styled-system/patterns/square.js +0 -24
  245. package/dist/styled-system/patterns/stack.d.ts +0 -23
  246. package/dist/styled-system/patterns/stack.js +0 -24
  247. package/dist/styled-system/patterns/visually-hidden.d.ts +0 -20
  248. package/dist/styled-system/patterns/visually-hidden.js +0 -18
  249. package/dist/styled-system/patterns/vstack.d.ts +0 -21
  250. package/dist/styled-system/patterns/vstack.js +0 -24
  251. package/dist/styled-system/patterns/wrap.d.ts +0 -24
  252. package/dist/styled-system/patterns/wrap.js +0 -25
  253. package/dist/styled-system/recipes/content-header.d.ts +0 -31
  254. package/dist/styled-system/recipes/content-header.js +0 -65
  255. package/dist/styled-system/recipes/content-page.d.ts +0 -34
  256. package/dist/styled-system/recipes/content-page.js +0 -43
  257. package/dist/styled-system/recipes/create-recipe.js +0 -82
  258. package/dist/styled-system/recipes/index.d.ts +0 -17
  259. package/dist/styled-system/recipes/index.js +0 -16
  260. package/dist/styled-system/recipes/metric-block.d.ts +0 -34
  261. package/dist/styled-system/recipes/metric-block.js +0 -76
  262. package/dist/styled-system/recipes/product-list.d.ts +0 -31
  263. package/dist/styled-system/recipes/product-list.js +0 -73
  264. package/dist/styled-system/recipes/product-section.d.ts +0 -31
  265. package/dist/styled-system/recipes/product-section.js +0 -49
  266. package/dist/styled-system/recipes/product-settings-row.d.ts +0 -34
  267. package/dist/styled-system/recipes/product-settings-row.js +0 -58
  268. package/dist/styled-system/recipes/product-settings.d.ts +0 -31
  269. package/dist/styled-system/recipes/product-settings.js +0 -57
  270. package/dist/styled-system/recipes/product-status.d.ts +0 -34
  271. package/dist/styled-system/recipes/product-status.js +0 -45
  272. package/dist/styled-system/recipes/product-toolbar.d.ts +0 -31
  273. package/dist/styled-system/recipes/product-toolbar.js +0 -41
  274. package/dist/styled-system/recipes/product-workspace.d.ts +0 -34
  275. package/dist/styled-system/recipes/product-workspace.js +0 -58
  276. package/dist/styled-system/recipes/surface-metric.d.ts +0 -38
  277. package/dist/styled-system/recipes/surface-metric.js +0 -63
  278. package/dist/styled-system/recipes/surface-table.d.ts +0 -34
  279. package/dist/styled-system/recipes/surface-table.js +0 -62
  280. package/dist/styled-system/recipes/ui-badge.d.ts +0 -31
  281. package/dist/styled-system/recipes/ui-badge.js +0 -38
  282. package/dist/styled-system/recipes/ui-button.d.ts +0 -32
  283. package/dist/styled-system/recipes/ui-button.js +0 -43
  284. package/dist/styled-system/recipes/ui-input.d.ts +0 -31
  285. package/dist/styled-system/recipes/ui-input.js +0 -49
  286. package/dist/styled-system/recipes/ui-textarea.d.ts +0 -31
  287. package/dist/styled-system/recipes/ui-textarea.js +0 -24
  288. package/dist/styled-system/styles.css +0 -1257
  289. package/dist/styled-system/tokens/index.d.ts +0 -9
  290. package/dist/styled-system/tokens/index.js +0 -268
  291. package/dist/styled-system/tokens/tokens.d.ts +0 -36
  292. package/dist/styled-system/types/composition.d.ts +0 -227
  293. package/dist/styled-system/types/conditions.d.ts +0 -236
  294. package/dist/styled-system/types/csstype.d.ts +0 -22570
  295. package/dist/styled-system/types/index.d.ts +0 -6
  296. package/dist/styled-system/types/parts.d.ts +0 -8
  297. package/dist/styled-system/types/pattern.d.ts +0 -78
  298. package/dist/styled-system/types/prop-type.d.ts +0 -223
  299. package/dist/styled-system/types/recipe.d.ts +0 -181
  300. package/dist/styled-system/types/selectors.d.ts +0 -59
  301. package/dist/styled-system/types/static-css.d.ts +0 -56
  302. package/dist/styled-system/types/style-props.d.ts +0 -8088
  303. package/dist/styled-system/types/system-types.d.ts +0 -151
@@ -1,6 +1,9 @@
1
1
  import { type ResourceRendering } from '../rendering/index.js';
2
2
  import type { AutoTableGridState } from './auto-table-grid.js';
3
+ import { type ExportFormat, type TaskProvider } from '@svadmin/core';
4
+ import { type BatchSelection } from './table-contract';
3
5
  import type { BaseRecord, FieldDefinition, Sort } from '@svadmin/core';
6
+ import { type SavedListViewProvider } from './saved-list-views.js';
4
7
  import type { Snippet } from 'svelte';
5
8
  interface Props {
6
9
  resourceName: string;
@@ -8,6 +11,8 @@ interface Props {
8
11
  /** 可选主体渲染器:不替换资源、权限、工具栏和持久化逻辑。 */
9
12
  gridBody?: Snippet<[AutoTableGridState]>;
10
13
  selectable?: boolean;
14
+ /** 仅在业务回调支持按查询执行服务端命令时启用。 */
15
+ allowSelectAllMatching?: boolean;
11
16
  density?: 'compact' | 'comfortable';
12
17
  showHeader?: boolean;
13
18
  title?: string;
@@ -45,9 +50,10 @@ interface Props {
45
50
  };
46
51
  /** Externally controlled sorters */
47
52
  sorters?: Sort[];
48
- /** Custom batch actions to render when rows are selected */
53
+ /** 批量操作必须按 selection.scope 区分明确 ID 与全部查询结果。 */
49
54
  batchActions?: Snippet<[{
50
55
  selectedIds: (string | number)[];
56
+ selection: BatchSelection;
51
57
  }]>;
52
58
  /** Summary row rendered at table footer */
53
59
  summary?: Snippet<[{
@@ -56,6 +62,16 @@ interface Props {
56
62
  visibleColumnsCount: number;
57
63
  }]>;
58
64
  deleteVariables?: unknown;
65
+ /** 团队/系统视图;写入需要 Provider 能力和显式可写标记。 */
66
+ savedViewProvider?: SavedListViewProvider;
67
+ /** 显式允许唯一的远程默认视图在无 URL/个人活动视图时初始化页面。 */
68
+ applyRemoteDefaultView?: boolean;
69
+ /** 提供任务名后按当前筛选/排序提交全量导出;未提供时仍导出当前已加载页。 */
70
+ exportTaskName?: string;
71
+ exportTaskProvider?: TaskProvider;
72
+ exportTaskIdempotencyKey?: string;
73
+ exportFormat?: ExportFormat;
74
+ exportMaxItemCount?: number;
59
75
  }
60
76
  declare const AutoTable: import("svelte").Component<Props, {}, "">;
61
77
  type AutoTable = ReturnType<typeof AutoTable>;
@@ -1,6 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { ResourceRendering } from '../rendering/index.js';
3
3
  import { definedOptions } from '@svadmin/core/options';
4
+ import type { Snippet } from 'svelte';
4
5
 
5
6
  import { useCan, useNavigation, type FieldDefinition } from '@svadmin/core';
6
7
  import { useRecordDetail } from './record-detail.svelte';
@@ -17,12 +18,14 @@
17
18
  open = $bindable(false),
18
19
  recordId,
19
20
  onClose,
21
+ extraSections,
20
22
  }: {
21
23
  resourceName: string;
22
24
  rendering?: ResourceRendering | undefined;
23
25
  open?: boolean;
24
26
  recordId: string | number;
25
27
  onClose?: () => void;
28
+ extraSections?: Snippet;
26
29
  } = $props();
27
30
 
28
31
  const i18n = useTranslation();
@@ -149,6 +152,12 @@
149
152
  </div>
150
153
  {/if}
151
154
 
155
+ {#if extraSections && canReadRecord && query.isSuccess}
156
+ <section data-svadmin-detail-extensions aria-label={i18n.t('common.additionalDetails')}>
157
+ {@render extraSections()}
158
+ </section>
159
+ {/if}
160
+
152
161
  {#snippet footer()}
153
162
  {#if canReadRecord && query.isSuccess}
154
163
  <Button type="button" variant="outline" onclick={() => navigateTo('show')}>
@@ -1,10 +1,12 @@
1
1
  import type { ResourceRendering } from '../rendering/index.js';
2
+ import type { Snippet } from 'svelte';
2
3
  type $$ComponentProps = {
3
4
  resourceName: string;
4
5
  rendering?: ResourceRendering | undefined;
5
6
  open?: boolean;
6
7
  recordId: string | number;
7
8
  onClose?: () => void;
9
+ extraSections?: Snippet;
8
10
  };
9
11
  declare const BoundRecordDetailDrawer: import("svelte").Component<$$ComponentProps, {}, "open">;
10
12
  type BoundRecordDetailDrawer = ReturnType<typeof BoundRecordDetailDrawer>;
@@ -17,6 +17,12 @@
17
17
 
18
18
  interface Crumb { label: string; href?: string; }
19
19
 
20
+ // 仅复用 AdminApp 明确支持的成员页别名,宿主精确菜单项始终优先。
21
+ const memberAliases: Readonly<Record<string, string>> = {
22
+ '/account/members-starter': '/account/members/members-starter',
23
+ '/account/team-members': '/account/members/team-members',
24
+ };
25
+
20
26
  function normalizePath(path: string): string {
21
27
  const normalized = path.replace(/^#/, '').replace(/[?#].*$/, '').replace(/\/$/, '');
22
28
  return normalized || '/';
@@ -38,7 +44,11 @@
38
44
  const currentPathname = normalizePath(getPath());
39
45
  if (currentPathname === '/') return result;
40
46
 
41
- const menuTrail = menu ? findMenuTrail(menu, currentPathname) : undefined;
47
+ const canonicalPath = memberAliases[currentPathname];
48
+ const menuTrail = menu
49
+ ? findMenuTrail(menu, currentPathname)
50
+ ?? (canonicalPath ? findMenuTrail(menu, canonicalPath) : undefined)
51
+ : undefined;
42
52
  if (menuTrail) {
43
53
  for (const item of menuTrail) {
44
54
  result.push(definedOptions({
@@ -1,17 +1,15 @@
1
1
  <script lang="ts">
2
- import { captureAdminContext, queryKeyMatches, withValidatedTaskProvider } from '@svadmin/core';
3
- import { useQueryClient } from '@tanstack/svelte-query';
4
2
  import { useTranslation } from '@svadmin/core/i18n';
5
3
 
6
4
  import { Button } from './ui/button/index.js';
7
5
  import { Loader2, Ban } from '@lucide/svelte';
6
+ import { createTaskAction } from './task-action.svelte.js';
8
7
 
9
8
  const i18n = useTranslation();
10
- const adminContext = captureAdminContext();
11
9
 
12
10
  let {
13
11
  taskId,
14
- taskProvider = adminContext.taskProvider ?? undefined,
12
+ taskProvider,
15
13
  onSuccess,
16
14
  onError,
17
15
  disabled = false,
@@ -29,46 +27,18 @@
29
27
  children?: import('svelte').Snippet;
30
28
  }>();
31
29
 
32
- const queryClient = useQueryClient();
33
- let pending = $state(false);
34
-
35
- async function handleCancel() {
36
- if (!taskProvider?.cancel || pending) return;
37
- const scopedTaskId = taskId;
38
- const scope = adminContext.queryKeyMatcher();
39
- pending = true;
40
- try {
41
- const provider = withValidatedTaskProvider(taskProvider);
42
- if (!provider.cancel) return;
43
- await provider.cancel(scopedTaskId);
44
- await Promise.all([
45
- queryClient.invalidateQueries({
46
- predicate: (query) => queryKeyMatches(query.queryKey, {
47
- ...scope,
48
- kind: 'task',
49
- action: 'list',
50
- }),
51
- }),
52
- queryClient.invalidateQueries({
53
- predicate: (query) => queryKeyMatches(query.queryKey, {
54
- ...scope,
55
- kind: 'task',
56
- action: 'one',
57
- id: scopedTaskId,
58
- }),
59
- }),
60
- ]);
61
- onSuccess?.();
62
- } catch (error) {
63
- onError?.(error);
64
- } finally {
65
- pending = false;
66
- }
67
- }
30
+ const action = createTaskAction({
31
+ action: 'cancel',
32
+ get taskId() { return taskId; },
33
+ get provider() { return taskProvider; },
34
+ get disabled() { return disabled; },
35
+ get onSuccess() { return onSuccess; },
36
+ get onError() { return onError; },
37
+ });
68
38
  </script>
69
39
 
70
- <Button {variant} {size} onclick={handleCancel} disabled={disabled || pending || !taskProvider?.cancel}>
71
- {#if pending}
40
+ <Button {variant} {size} onclick={action.run} disabled={!action.available || action.pending}>
41
+ {#if action.pending}
72
42
  <Loader2 class="svadmin-u-82cc6c6581cd svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-afbdd13a380e" />
73
43
  {:else}
74
44
  <Ban class="svadmin-u-82cc6c6581cd svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
@@ -0,0 +1,73 @@
1
+ <script lang="ts">
2
+ import { cn } from '../utils.js';
3
+
4
+ interface Props {
5
+ value?: string;
6
+ name?: string;
7
+ id?: string;
8
+ disabled?: boolean;
9
+ required?: boolean;
10
+ invalid?: boolean;
11
+ describedby?: string;
12
+ ariaLabel?: string;
13
+ class?: string;
14
+ onchange?: (value: string) => void;
15
+ }
16
+
17
+ let {
18
+ value = $bindable(''),
19
+ name,
20
+ id,
21
+ disabled = false,
22
+ required = false,
23
+ invalid = false,
24
+ describedby,
25
+ ariaLabel,
26
+ class: className = '',
27
+ onchange,
28
+ }: Props = $props();
29
+
30
+ function handleInput(event: Event): void {
31
+ const input = event.currentTarget;
32
+ if (!(input instanceof HTMLInputElement) || disabled) return;
33
+ value = input.value;
34
+ onchange?.(input.value);
35
+ }
36
+ </script>
37
+
38
+ <input
39
+ class={cn('svadmin-color-picker', className)}
40
+ {id}
41
+ {name}
42
+ type="color"
43
+ value={value}
44
+ {disabled}
45
+ {required}
46
+ aria-label={ariaLabel}
47
+ aria-invalid={invalid || undefined}
48
+ aria-describedby={describedby}
49
+ oninput={handleInput}
50
+ />
51
+
52
+ <style>
53
+ .svadmin-color-picker {
54
+ display: block;
55
+ inline-size: 40px;
56
+ block-size: 32px;
57
+ padding: 2px;
58
+ border: 1px solid var(--border);
59
+ border-radius: 6px;
60
+ background: var(--background);
61
+ cursor: pointer;
62
+ }
63
+
64
+ .svadmin-color-picker:focus-visible {
65
+ outline: 2px solid var(--ring);
66
+ outline-offset: 2px;
67
+ }
68
+
69
+ .svadmin-color-picker:disabled {
70
+ cursor: not-allowed;
71
+ opacity: 0.5;
72
+ }
73
+ </style>
@@ -0,0 +1,15 @@
1
+ interface Props {
2
+ value?: string;
3
+ name?: string;
4
+ id?: string;
5
+ disabled?: boolean;
6
+ required?: boolean;
7
+ invalid?: boolean;
8
+ describedby?: string;
9
+ ariaLabel?: string;
10
+ class?: string;
11
+ onchange?: (value: string) => void;
12
+ }
13
+ declare const ColorPicker: import("svelte").Component<Props, {}, "value">;
14
+ type ColorPicker = ReturnType<typeof ColorPicker>;
15
+ export default ColorPicker;
@@ -2,7 +2,8 @@
2
2
  import { definedReactiveOptions } from '@svadmin/core/options';
3
3
 
4
4
  import { useTranslation } from '@svadmin/core/i18n';
5
- import { useResourceContract, useSelect } from '@svadmin/core';
5
+ import { captureAdminContext, captureAuthSession, useResourceContract, useSelect } from '@svadmin/core';
6
+ import { onDestroy, untrack } from 'svelte';
6
7
  import type { BaseRecord, Filter } from '@svadmin/core';
7
8
  import type { HTMLButtonAttributes } from 'svelte/elements';
8
9
  import type { ButtonProps } from './ui/button/index.js';
@@ -31,6 +32,7 @@
31
32
  searchable?: boolean;
32
33
  multiple?: boolean;
33
34
  onSearch?: (value: string) => Filter[];
35
+ fetchSize?: number;
34
36
  id?: string;
35
37
  class?: string;
36
38
  }
@@ -41,21 +43,51 @@
41
43
  onchange,
42
44
  optionLabel = 'title',
43
45
  optionValue = 'id',
44
- placeholder = 'Select...',
46
+ placeholder,
45
47
  searchable = true,
46
48
  multiple = false,
47
49
  onSearch,
50
+ fetchSize = 50,
48
51
  disabled = false,
49
52
  ...restProps
50
53
  }: Props = $props();
51
54
 
52
55
  const buttonRestProps = $derived(restProps as Omit<ButtonProps, 'children' | 'variant' | 'size'>);
56
+ let open = $state(false);
57
+ let searchInputValue = $state('');
58
+ let page = $state(1);
59
+ let appliedSearch = $state('');
60
+ let timer: ReturnType<typeof setTimeout> | undefined;
61
+ onDestroy(() => clearTimeout(timer));
62
+ const normalizedFetchSize = $derived(
63
+ Number.isSafeInteger(fetchSize) && fetchSize >= 10 && fetchSize <= 200 ? fetchSize : 50,
64
+ );
53
65
 
54
66
  const binding = useResourceContract(() => resource);
67
+ const context = captureAdminContext();
68
+ const sourceScope = $derived({
69
+ contract: binding.resource, provider: context.providers?.[binding.dataProviderName],
70
+ meta: JSON.stringify(binding.meta), tenant: context.tenantCacheKey?.__svadminTenant,
71
+ auth: context.authProvider, session: captureAuthSession(context.authProvider).cacheKey,
72
+ access: context.accessControlProvider, router: context.routerProvider,
73
+ optionLabel, optionValue, onSearch, searchable, normalizedFetchSize,
74
+ });
75
+ type Option = { value: string | number; label: string };
76
+ let loaded = $state.raw<{ scope: typeof sourceScope; search: string; options: Option[] }>();
55
77
  const defaultSearch = $derived.by(() => {
56
78
  const field = optionLabel;
57
79
  return (v: string): Filter[] => v ? [{ field, operator: 'contains', value: v }] : [];
58
80
  });
81
+ const searchFilters = $derived.by(() => {
82
+ try {
83
+ return {
84
+ filters: appliedSearch && (onSearch || searchable)
85
+ ? (onSearch ?? defaultSearch)(appliedSearch)
86
+ : [],
87
+ invalid: false,
88
+ };
89
+ } catch { return { filters: [], invalid: true }; }
90
+ });
59
91
  const select = useSelect(definedReactiveOptions({
60
92
  get resource() { return binding.resource; },
61
93
  get dataProviderName() { return binding.dataProviderName; },
@@ -69,33 +101,62 @@
69
101
  };
70
102
  },
71
103
  get projectionKey() { return `combobox:${optionLabel}:${optionValue}`; },
104
+ get fetchSize() { return normalizedFetchSize; },
105
+ get pagination() { return { current: page, pageSize: normalizedFetchSize }; },
72
106
  get defaultValue() {
73
107
  if (Array.isArray(value)) return value;
74
- return value === null || value === undefined ? [] : [value];
108
+ return value === null || value === undefined || value === '' ? [] : [value];
75
109
  },
76
- get onSearch() { return onSearch ?? (searchable ? defaultSearch : undefined); },
110
+ get filters() { return searchFilters.filters; },
111
+ get queryOptions() { return { enabled: !searchFilters.invalid }; },
77
112
  }));
78
113
 
79
- let open = $state(false);
80
- let searchInputValue = $state('');
81
- $effect(() => {
82
- void binding.resource;
83
- void binding.dataProviderName;
84
- void binding.meta;
85
- searchInputValue = '';
114
+ $effect.pre(() => {
115
+ void sourceScope;
116
+ clearTimeout(timer);
117
+ searchInputValue = appliedSearch = '';
86
118
  open = false;
119
+ page = 1;
120
+ loaded = undefined;
121
+ });
122
+ $effect(() => {
123
+ const data = select.query.data;
124
+ if (!data || select.query.isFetching || select.isError || searchFilters.invalid) return;
125
+ const scope = sourceScope;
126
+ const search = appliedSearch;
127
+ const previous = untrack(() => loaded);
128
+ const options = new Map<string | number, Option>(
129
+ page > 1 && previous?.scope === scope && previous.search === search
130
+ ? previous.options.map(option => [option.value, option]) : [],
131
+ );
132
+ for (const option of data.options) options.set(option.value, option);
133
+ loaded = { scope, search, options: [...options.values()] };
87
134
  });
88
135
 
89
136
  const selectedValues = $derived(
90
137
  Array.isArray(value) ? value : value === null || value === undefined ? [] : [value],
91
138
  );
139
+ const availableOptions = $derived.by(() => {
140
+ if (select.isError || searchFilters.invalid) return [];
141
+ const options = new Map<string | number, Option>(
142
+ loaded?.scope === sourceScope && loaded.search === appliedSearch
143
+ ? loaded.options.map(option => [option.value, option]) : [],
144
+ );
145
+ for (const option of select.options) options.set(option.value, option);
146
+ return [...options.values()];
147
+ });
148
+ const totalOptions = $derived(select.query.data?.total ?? availableOptions.length);
149
+ const hasMore = $derived(totalOptions > page * normalizedFetchSize && !select.isError
150
+ && !searchFilters.invalid && searchInputValue === appliedSearch);
151
+ const invalidOptions = $derived(select.isError || searchFilters.invalid);
92
152
  const selectedLabels = $derived(selectedValues.flatMap(selectedValue => {
93
- const option = select.options.find(candidate => candidate.value === selectedValue);
153
+ const option = availableOptions.find(candidate => candidate.value === selectedValue);
94
154
  return option ? [option.label] : [];
95
155
  }));
96
156
  const selectedLabel = $derived(selectedLabels.join(', '));
97
157
 
98
158
  function handleSelect(optValue: string | number) {
159
+ if (disabled || invalidOptions || select.isFetching || searchInputValue !== appliedSearch) return;
99
160
  if (multiple) {
100
161
  const next = selectedValues.includes(optValue)
101
162
  ? selectedValues.filter(selectedValue => selectedValue !== optValue)
@@ -105,23 +166,43 @@
105
166
  }
106
167
  onchange?.(optValue === value ? null : optValue);
107
168
  open = false;
108
- searchInputValue = '';
109
- select.onSearchChange('');
169
+ resetSearch();
110
170
  }
111
171
 
112
172
  function handleClear(e: Event) {
113
173
  e.stopPropagation();
174
+ if (disabled) return;
114
175
  onchange?.(multiple ? [] : null);
115
176
  open = false;
116
- searchInputValue = '';
117
- select.onSearchChange('');
177
+ resetSearch();
178
+ }
179
+
180
+ function resetSearch(): void {
181
+ clearTimeout(timer);
182
+ searchInputValue = appliedSearch = '';
183
+ page = 1;
184
+ loaded = undefined;
118
185
  }
119
186
 
120
187
  function handleSearchInput(e: Event) {
121
188
  if (!(e.currentTarget instanceof HTMLInputElement)) return;
122
189
  const v = e.currentTarget.value;
123
190
  searchInputValue = v;
124
- select.onSearchChange(v);
191
+ clearTimeout(timer);
192
+ const scope = sourceScope;
193
+ const apply = () => {
194
+ if (scope !== sourceScope || disabled) return;
195
+ appliedSearch = v;
196
+ page = 1;
197
+ loaded = undefined;
198
+ };
199
+ if (!v) apply();
200
+ else timer = setTimeout(apply, 300);
201
+ }
202
+
203
+ function loadMore(): void {
204
+ if (disabled || select.isFetching || !hasMore) return;
205
+ page += 1;
125
206
  }
126
207
  </script>
127
208
 
@@ -138,12 +219,12 @@
138
219
  aria-expanded={open}
139
220
  aria-multiselectable={multiple || undefined}
140
221
  aria-busy={select.isFetching}
141
- aria-invalid={select.isError}
222
+ aria-invalid={invalidOptions || restProps['aria-invalid']}
142
223
  >
143
224
  {#if selectedLabel}
144
225
  <span class="svadmin-u-f283ea9bea0e">{selectedLabel}</span>
145
226
  {:else}
146
- <span class="svadmin-u-bfa603190748">{placeholder}</span>
227
+ <span class="svadmin-u-bfa603190748">{placeholder ?? i18n.t('field.selectPlaceholder')}</span>
147
228
  {/if}
148
229
  <ChevronsUpDown class="svadmin-u-fb56d9cff341 svadmin-u-11e59c6d5f6b svadmin-u-dc7972ebf3f3 svadmin-u-012fbd121f37 svadmin-u-0b8c506a0596" />
149
230
  </Button>
@@ -154,9 +235,9 @@
154
235
  </Button>
155
236
  {/if}
156
237
  </div>
157
- {#if select.isError}
238
+ {#if invalidOptions}
158
239
  <div role="alert" class="select-error">
159
- <span>Unable to load options.</span>
240
+ <span>{i18n.t('select.loadFailed')}</span>
160
241
  <Button type="button" variant="ghost" size="icon-sm" disabled={disabled || select.isFetching}
161
242
  title={i18n.t('common.retry')} aria-label={i18n.t('common.retry')} onclick={() => select.refetch()}>
162
243
  <RotateCw class="svadmin-u-6a60c09e6aaa svadmin-u-9cea05671a29" />
@@ -164,7 +245,7 @@
164
245
  </div>
165
246
  {/if}
166
247
 
167
- {#if open}
248
+ {#if open && !disabled}
168
249
  <div class="svadmin-u-da4dbfbc4fdc svadmin-u-181b286668b5 svadmin-u-b6b02c0ebef6 svadmin-u-6da6a3c3f741 svadmin-u-421ac2be5045 svadmin-u-ca6bcd4b6f3f svadmin-u-e541d86d1ec8 svadmin-u-06bbb43166db">
169
250
  <Command.Root shouldFilter={false} class="svadmin-u-60fbb7713999 svadmin-u-8dddea0773ed svadmin-u-2cd02d11d1af svadmin-u-421ac2be5045 svadmin-u-e541d86d1ec8 svadmin-u-9b13e8ae5c9c">
170
251
  {#if searchable}
@@ -173,7 +254,7 @@
173
254
  <Command.Input
174
255
  bind:value={searchInputValue}
175
256
  oninput={handleSearchInput}
176
- placeholder="Search..."
257
+ placeholder={i18n.t('common.search')}
177
258
  class="svadmin-u-60fbb7713999 svadmin-u-e7a768f922d2 svadmin-u-6da6a3c3f741 svadmin-u-7f19cdf4c5bb svadmin-u-03b4dd7f172b svadmin-u-fc7473ca09eb svadmin-u-df37b1fd9495 svadmin-u-9c24ab70af61"
178
259
  />
179
260
  </div>
@@ -187,14 +268,15 @@
187
268
  {#each Array(3) as _, _i (_i)}
188
269
  <div class="svadmin-u-d5eab218aa34 svadmin-u-ec0091ee009b"><Skeleton class="svadmin-u-cd0d9c512cdc svadmin-u-6da6a3c3f741" /></div>
189
270
  {/each}
190
- {:else if !select.isError}
191
- {#if !select.options.length}
271
+ {:else if !invalidOptions}
272
+ {#if !availableOptions.length}
192
273
  <Command.Empty class="svadmin-u-cb11fec3bb46 svadmin-u-ca6bf63030aa svadmin-u-359090c2d529 svadmin-u-bfa603190748">
193
- No results.
274
+ {i18n.t('select.noResults')}
194
275
  </Command.Empty>
195
276
  {/if}
196
- {#each select.options as opt (opt.value)}
277
+ {#each availableOptions as opt (`${typeof opt.value}:${opt.value}`)}
197
278
  <Command.Item
279
+ disabled={select.isFetching || searchInputValue !== appliedSearch}
198
280
  value={`${typeof opt.value}:${opt.value}`}
199
281
  onSelect={() => handleSelect(opt.value)}
200
282
  class="svadmin-u-d89972fe17d6 svadmin-u-60fbb7713999 svadmin-u-50ca6ba56aa3 svadmin-u-7f6912283f11 svadmin-u-3960ffc248d9 svadmin-u-36d4469299aa svadmin-u-d5eab218aa34 svadmin-u-ec0091ee009b svadmin-u-fc7473ca09eb svadmin-u-df37b1fd9495 svadmin-u-99afb1cc3b47 svadmin-u-529780e25268"
@@ -205,6 +287,11 @@
205
287
  {/each}
206
288
  {/if}
207
289
  </Command.List>
290
+ {#if hasMore}
291
+ <Button type="button" variant="ghost" size="sm" disabled={select.isFetching} onclick={loadMore}>
292
+ {i18n.t('select.loadMore')}
293
+ </Button>
294
+ {/if}
208
295
  </Command.Root>
209
296
  </div>
210
297
  {/if}
@@ -10,6 +10,7 @@ interface Props extends Omit<HTMLButtonAttributes, 'value' | 'onchange' | 'child
10
10
  searchable?: boolean;
11
11
  multiple?: boolean;
12
12
  onSearch?: (value: string) => Filter[];
13
+ fetchSize?: number;
13
14
  id?: string;
14
15
  class?: string;
15
16
  }
@@ -0,0 +1,99 @@
1
+ <script lang="ts">
2
+ import { Loader2, RefreshCw } from '@lucide/svelte';
3
+ import {
4
+ DashboardContractError, captureAdminContext, captureAuthSession, createEnterpriseRequestContext,
5
+ decodeDashboardQuery, decodeDashboardSnapshot,
6
+ } from '@svadmin/core';
7
+ import type { DashboardProvider, DashboardSnapshot, EnterpriseProviderRequestContext } from '@svadmin/core';
8
+ import { useTranslation } from '@svadmin/core/i18n';
9
+ import { onDestroy, untrack } from 'svelte';
10
+ import MetricStrip, { type MetricStripItem } from './MetricStrip.svelte';
11
+ import { Button } from './ui/button/index.js';
12
+
13
+ interface Props {
14
+ provider: DashboardProvider;
15
+ dashboardId: string;
16
+ requestContext?: EnterpriseProviderRequestContext;
17
+ filters?: readonly { field: string; value: string | number | boolean | null }[];
18
+ title?: string;
19
+ }
20
+ let { provider, dashboardId, requestContext, filters = [], title }: Props = $props();
21
+ const context = captureAdminContext();
22
+ const i18n = useTranslation();
23
+ let snapshot = $state<DashboardSnapshot>();
24
+ let loading = $state(false);
25
+ let error = $state<'load' | 'invalid'>();
26
+ let epoch = 0;
27
+ let mounted = true;
28
+ let generatedContext: EnterpriseProviderRequestContext | undefined;
29
+ onDestroy(() => { mounted = false; });
30
+
31
+ function resolveContext(): EnterpriseProviderRequestContext {
32
+ if (requestContext) return requestContext;
33
+ const tenant = context.tenantCacheKey?.__svadminTenant;
34
+ if (typeof tenant !== 'string' && typeof tenant !== 'number') throw new Error('Missing tenant context');
35
+ if (generatedContext?.tenantId === tenant) return generatedContext;
36
+ generatedContext = createEnterpriseRequestContext({ tenantId: tenant });
37
+ return generatedContext;
38
+ }
39
+
40
+ async function load(): Promise<void> {
41
+ const currentEpoch = ++epoch;
42
+ loading = true;
43
+ error = undefined;
44
+ try {
45
+ const request = decodeDashboardQuery({
46
+ dashboardId: dashboardId.trim(),
47
+ ...(filters.length ? { filters: filters.map(filter => ({ ...filter })) } : {}),
48
+ });
49
+ const result = decodeDashboardSnapshot(await provider.get(resolveContext(), request));
50
+ if (!mounted || currentEpoch !== epoch) return;
51
+ snapshot = result;
52
+ } catch (cause) {
53
+ if (!mounted || currentEpoch !== epoch) return;
54
+ snapshot = undefined;
55
+ error = cause instanceof DashboardContractError ? 'invalid' : 'load';
56
+ } finally {
57
+ if (mounted && currentEpoch === epoch) loading = false;
58
+ }
59
+ }
60
+
61
+ const metricItems = $derived<MetricStripItem[]>(snapshot?.widgets.flatMap(widget =>
62
+ widget.kind === 'metric' ? widget.metrics.map(metric => ({
63
+ id: `${widget.id}:${metric.id}`, label: metric.label,
64
+ value: typeof metric.value === 'boolean' ? String(metric.value) : metric.value ?? '—',
65
+ ...(metric.tone ? { tone: metric.tone } : {}),
66
+ ...(metric.href ? { href: metric.href } : {}),
67
+ })) : [],
68
+ ) ?? []);
69
+
70
+ $effect(() => {
71
+ void provider; void dashboardId; void requestContext; void filters;
72
+ void captureAuthSession(context.authProvider).cacheKey;
73
+ untrack(() => { snapshot = undefined; void load(); });
74
+ });
75
+ </script>
76
+
77
+ <section class="svadmin-dashboard-view" aria-label={title ?? i18n.t('dashboard.title')}>
78
+ <header class="svadmin-dashboard-view__header">
79
+ <h2>{title ?? i18n.t('dashboard.title')}</h2>
80
+ <Button type="button" variant="outline" size="sm" aria-label={i18n.t('common.refresh')} onclick={() => void load()}>
81
+ <RefreshCw aria-hidden="true" class={loading ? 'svadmin-dashboard-view__spin' : undefined} />
82
+ </Button>
83
+ </header>
84
+ {#if loading}<div role="status"><Loader2 aria-hidden="true" /> {i18n.t('common.loading')}</div>
85
+ {:else if error}<div role="alert">{i18n.t(`dashboard.${error}`)}</div>
86
+ {:else if snapshot}
87
+ {#each snapshot.widgets.filter(widget => widget.kind === 'text') as widget (widget.id)}
88
+ <section aria-labelledby={`dashboard-widget-${widget.id}`}><h3 id={`dashboard-widget-${widget.id}`}>{widget.title}</h3><p>{widget.text}</p></section>
89
+ {/each}
90
+ <MetricStrip items={metricItems} ariaLabel={title ?? i18n.t('dashboard.metrics')} />
91
+ {:else}<div role="status">{i18n.t('dashboard.empty')}</div>{/if}
92
+ </section>
93
+
94
+ <style>
95
+ .svadmin-dashboard-view { display: grid; gap: 1rem; min-width: 0; }
96
+ .svadmin-dashboard-view__header { display: flex; align-items: center; justify-content: space-between; gap: 0.5rem; }
97
+ :global(.svadmin-dashboard-view__spin) { animation: svadmin-dashboard-spin 1s linear infinite; }
98
+ @keyframes svadmin-dashboard-spin { to { transform: rotate(360deg); } }
99
+ </style>