@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
@@ -1,14 +1,23 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from 'svelte';
3
- import { ChevronRight, ChevronDown, Folder, File } from '@lucide/svelte';
3
+ import { onDestroy, tick, untrack } from 'svelte';
4
+ import { snapshotPlainData } from '@svadmin/core/schema';
5
+ import { useTranslation } from '@svadmin/core/i18n';
6
+ import { ArrowDown, ArrowUp, ArrowUpDown, ChevronLeft, ChevronRight, ChevronDown, Folder, File, RotateCw } from '@lucide/svelte';
7
+ import {
8
+ filterTreeTableData, indexTreeTable, sortTreeTableData, treeTableSelection,
9
+ type TreeTableId, type TreeTableNode, type TreeTableSort,
10
+ } from '../tree-table-model.js';
4
11
  import { Button } from './ui/button/index.js';
5
12
  import { Checkbox } from './ui/checkbox/index.js';
13
+ import { Input } from './ui/input/index.js';
6
14
  import { cn } from '../utils.js';
7
15
 
8
16
  export interface TreeTableColumn {
9
17
  key: string;
10
18
  label: string;
11
19
  width?: string;
20
+ sortable?: boolean;
12
21
  }
13
22
 
14
23
  interface Props {
@@ -21,6 +30,27 @@
21
30
  onselect?: (selectedKeys: (string | number)[]) => void;
22
31
  rowActions?: Snippet<[{ record: Record<string, unknown>; id: string | number }]>;
23
32
  customCell?: Snippet<[{ column: TreeTableColumn; record: Record<string, unknown>; value: unknown }]>;
33
+ scopeKey?: string | number;
34
+ ariaLabel?: string;
35
+ loading?: boolean;
36
+ disabled?: boolean;
37
+ error?: string;
38
+ onRetry?: () => void;
39
+ filterText?: string;
40
+ filterable?: boolean;
41
+ filterPlaceholder?: string;
42
+ filterKeys?: string[];
43
+ sort?: TreeTableSort | undefined;
44
+ onsortchange?: (sort: TreeTableSort | undefined) => void;
45
+ page?: number;
46
+ pageSize?: number;
47
+ onpagechange?: (page: number) => void;
48
+ loadChildren?: (record: Record<string, unknown>, id: TreeTableId) => Promise<Record<string, unknown>[]>;
49
+ canLoadChildren?: (record: Record<string, unknown>) => boolean;
50
+ virtualized?: boolean;
51
+ height?: number;
52
+ rowHeight?: number;
53
+ overscan?: number;
24
54
  class?: string;
25
55
  }
26
56
 
@@ -34,46 +64,221 @@
34
64
  onselect,
35
65
  rowActions,
36
66
  customCell,
67
+ scopeKey,
68
+ ariaLabel,
69
+ loading = false,
70
+ disabled = false,
71
+ error,
72
+ onRetry,
73
+ filterText = $bindable(''),
74
+ filterable = false,
75
+ filterPlaceholder,
76
+ filterKeys,
77
+ sort = $bindable(),
78
+ onsortchange,
79
+ page = $bindable(1),
80
+ pageSize = 0,
81
+ onpagechange,
82
+ loadChildren,
83
+ canLoadChildren,
84
+ virtualized = false,
85
+ height = 360,
86
+ rowHeight = 40,
87
+ overscan = 4,
37
88
  class: className = '',
38
89
  }: Props = $props();
39
90
 
40
91
  let expandedKeys = $state<Set<string | number>>(new Set());
41
-
42
- interface FlatTreeItem {
43
- record: Record<string, unknown>;
44
- id: string | number;
45
- level: number;
46
- hasChildren: boolean;
47
- isExpanded: boolean;
48
- isLastChild?: boolean;
49
- }
50
-
51
- function flattenTree(items: Record<string, unknown>[], level = 0): FlatTreeItem[] {
52
- const flat: FlatTreeItem[] = [];
53
- for (const item of items) {
54
- const id = item[primaryKey] as string | number;
55
- const children = item[childrenKey] as Record<string, unknown>[] | undefined;
56
- const hasChildren = Array.isArray(children) && children.length > 0;
57
- const isExpanded = expandedKeys.has(id);
58
-
59
- flat.push({
60
- record: item,
61
- id,
62
- level,
63
- hasChildren,
64
- isExpanded,
92
+ let loadedChildren = $state<Map<TreeTableId, Record<string, unknown>[]>>(new Map());
93
+ let childLoadingKeys = $state<Set<TreeTableId>>(new Set());
94
+ let childErrors = $state<Map<TreeTableId, string>>(new Map());
95
+ let tableRoot: HTMLTableElement | undefined = $state();
96
+ let focusedKey = $state<TreeTableId>();
97
+ let scrollTop = $state(0);
98
+ let viewport: HTMLDivElement | undefined = $state();
99
+ let observedScope = untrack(() => scopeKey);
100
+ let observedPage = untrack(() => page);
101
+ let generation = 0;
102
+ let mounted = true;
103
+ const captureLoadScope = () => ({ data, scopeKey, loadChildren, canLoadChildren, primaryKey, childrenKey, disabled, loading, error });
104
+ let loadScope = $state.raw(untrack(captureLoadScope));
105
+ function sameLoadScope(scope: ReturnType<typeof captureLoadScope>): boolean {
106
+ return scope.data === data && scope.scopeKey === scopeKey && scope.loadChildren === loadChildren
107
+ && scope.canLoadChildren === canLoadChildren && scope.primaryKey === primaryKey
108
+ && scope.childrenKey === childrenKey && scope.disabled === disabled
109
+ && scope.loading === loading && scope.error === error;
110
+ }
111
+ onDestroy(() => { mounted = false; generation++; });
112
+ const i18n = useTranslation();
113
+ const baseIndex = $derived(indexTreeTable(data, primaryKey, childrenKey));
114
+ const fullIndex = $derived(baseIndex.valid
115
+ ? indexTreeTable(data, primaryKey, childrenKey, sameLoadScope(loadScope) ? loadedChildren : undefined)
116
+ : baseIndex);
117
+ const mergedData = $derived.by(() => {
118
+ if (!fullIndex.valid) return [];
119
+ // 完整索引先校验循环和数量;逆序重建后再进行筛选、排序和分页。
120
+ const records = new Map<TreeTableId, Record<string, unknown>>();
121
+ const roots: Record<string, unknown>[] = [];
122
+ for (const node of [...fullIndex.nodes.values()].reverse()) {
123
+ const children = node.children.map(id => records.get(id)).filter(
124
+ (record): record is Record<string, unknown> => record !== undefined,
125
+ );
126
+ records.set(node.id, { ...node.record, [childrenKey]: children });
127
+ }
128
+ for (const node of fullIndex.nodes.values()) {
129
+ const record = records.get(node.id);
130
+ if (node.parent === undefined && record) roots.push(record);
131
+ }
132
+ return roots;
133
+ });
134
+ const filteredData = $derived(filterTreeTableData(
135
+ mergedData, filterText, filterKeys ?? columns.map(column => column.key), childrenKey,
136
+ ));
137
+ const sortedData = $derived(sortTreeTableData(filteredData, sort, childrenKey));
138
+ const normalizedPageSize = $derived(
139
+ Number.isSafeInteger(pageSize) && pageSize > 0 ? Math.min(pageSize, 1000) : 0,
140
+ );
141
+ const pageCount = $derived(normalizedPageSize > 0 ? Math.max(1, Math.ceil(sortedData.length / normalizedPageSize)) : 1);
142
+ const normalizedPage = $derived(Math.min(pageCount, Number.isSafeInteger(page) && page > 0 ? page : 1));
143
+ const pagedData = $derived(normalizedPageSize > 0
144
+ ? sortedData.slice((normalizedPage - 1) * normalizedPageSize, normalizedPage * normalizedPageSize)
145
+ : sortedData);
146
+ const index = $derived(indexTreeTable(pagedData, primaryKey, childrenKey));
147
+ const columnsValid = $derived.by(() => {
148
+ const ordering = sort;
149
+ return columns.length > 0
150
+ && columns.every(column => typeof column.key === 'string' && column.key.length > 0)
151
+ && new Set(columns.map(column => column.key)).size === columns.length
152
+ && (!ordering || (columns.some(column => column.key === ordering.key) && ['asc', 'desc'].includes(ordering.direction)));
153
+ });
154
+ const displayError = $derived(error || (!fullIndex.valid || !index.valid || !columnsValid ? i18n.t('treeTable.invalidData') : ''));
155
+ const available = $derived(!loading && !displayError);
156
+ const selection = $derived(treeTableSelection(fullIndex.nodes, new Set(selectedKeys)));
157
+ function isExpanded(id: TreeTableId): boolean {
158
+ return expandedKeys.has(id) || filterText.trim().length > 0;
159
+ }
160
+ function needsChildren(id: TreeTableId): boolean {
161
+ const node = fullIndex.nodes.get(id);
162
+ return !!node && !!loadChildren && !loadedChildren.has(id)
163
+ && (canLoadChildren?.(node.record) ?? !Array.isArray(node.record[childrenKey]));
164
+ }
165
+ const flattenedRows = $derived.by(() => {
166
+ if (!available) return [];
167
+ const visible = new Set<TreeTableId>();
168
+ const rows: (TreeTableNode & { hasChildren: boolean; isExpanded: boolean })[] = [];
169
+ for (const node of index.nodes.values()) {
170
+ if (node.parent !== undefined && (!visible.has(node.parent) || !isExpanded(node.parent))) continue;
171
+ visible.add(node.id);
172
+ const canLoad = needsChildren(node.id);
173
+ rows.push({
174
+ ...node,
175
+ hasChildren: node.children.length > 0 || canLoad,
176
+ isExpanded: isExpanded(node.id),
65
177
  });
178
+ }
179
+ return rows;
180
+ });
181
+ const normalizedRowHeight = $derived(Number.isFinite(rowHeight) && rowHeight >= 24
182
+ ? Math.min(Math.floor(rowHeight), 500) : 40);
183
+ const normalizedHeight = $derived(Number.isFinite(height) && height >= normalizedRowHeight * 2
184
+ ? Math.min(Math.floor(height), 4000) : Math.max(360, normalizedRowHeight * 2));
185
+ const normalizedOverscan = $derived(Number.isSafeInteger(overscan) && overscan >= 0
186
+ ? Math.min(overscan, 100) : 4);
187
+ const maxScroll = $derived(Math.max(0, (flattenedRows.length + 1) * normalizedRowHeight - normalizedHeight));
188
+ const boundedScrollTop = $derived(Math.max(0, Math.min(Number.isFinite(scrollTop) ? scrollTop : 0, maxScroll)));
189
+ const windowStart = $derived(virtualized
190
+ ? Math.max(0, Math.floor(boundedScrollTop / normalizedRowHeight) - normalizedOverscan) : 0);
191
+ const windowEnd = $derived(virtualized
192
+ ? Math.min(flattenedRows.length, Math.ceil((boundedScrollTop + normalizedHeight) / normalizedRowHeight) + normalizedOverscan)
193
+ : flattenedRows.length);
194
+ const renderedRows = $derived(flattenedRows.slice(windowStart, windowEnd));
195
+ const focusTarget = $derived(renderedRows.some(row => row.id === focusedKey) ? focusedKey : renderedRows[0]?.id);
196
+ const cellCount = $derived(columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0));
197
+
198
+ function nodeKey(id: TreeTableId): string {
199
+ return `${typeof id}:${String(id)}`;
200
+ }
66
201
 
67
- if (hasChildren && isExpanded && children) {
68
- flat.push(...flattenTree(children, level + 1));
202
+ async function focusRow(id: TreeTableId | undefined): Promise<void> {
203
+ if (id === undefined) return;
204
+ const epoch = generation;
205
+ const position = flattenedRows.findIndex(item => item.id === id);
206
+ if (position < 0) return;
207
+ if (virtualized) {
208
+ const top = position * normalizedRowHeight;
209
+ const bottom = top + normalizedRowHeight;
210
+ if (top < boundedScrollTop) scrollTop = top;
211
+ else if (bottom > boundedScrollTop + normalizedHeight - normalizedRowHeight) {
212
+ scrollTop = Math.min(maxScroll, bottom - normalizedHeight + normalizedRowHeight);
69
213
  }
214
+ await tick();
70
215
  }
71
- return flat;
216
+ if (!mounted || epoch !== generation || !available) return;
217
+ tableRoot?.querySelector<HTMLElement>(`[data-tree-key="${CSS.escape(nodeKey(id))}"]`)?.focus();
72
218
  }
73
219
 
74
- const flattenedRows = $derived(flattenTree(data));
220
+ function handleKeydown(event: KeyboardEvent, row: TreeTableNode): void {
221
+ if (disabled || event.target !== event.currentTarget || event.altKey || event.ctrlKey || event.metaKey) return;
222
+ const position = flattenedRows.findIndex(item => item.id === row.id);
223
+ switch (event.key) {
224
+ case 'ArrowDown': focusRow(flattenedRows[position + 1]?.id); break;
225
+ case 'ArrowUp': focusRow(flattenedRows[position - 1]?.id); break;
226
+ case 'Home': focusRow(flattenedRows[0]?.id); break;
227
+ case 'End': focusRow(flattenedRows.at(-1)?.id); break;
228
+ case 'ArrowRight':
229
+ if ((!isExpanded(row.id) && row.children.length) || needsChildren(row.id)) toggleExpand(row.id);
230
+ else focusRow(row.children[0]);
231
+ break;
232
+ case 'ArrowLeft':
233
+ if (expandedKeys.has(row.id)) toggleExpand(row.id);
234
+ else focusRow(row.parent);
235
+ break;
236
+ case ' ':
237
+ case 'Enter':
238
+ if (selectable) toggleSelect(row.id);
239
+ else return;
240
+ break;
241
+ default: return;
242
+ }
243
+ event.preventDefault();
244
+ }
245
+
246
+ async function loadNodeChildren(row: TreeTableNode): Promise<void> {
247
+ const loader = loadChildren;
248
+ if (!loader || !available || disabled || childLoadingKeys.has(row.id) || !needsChildren(row.id)) return;
249
+ const scope = captureLoadScope();
250
+ const epoch = generation;
251
+ const current = () => mounted && generation === epoch && sameLoadScope(scope);
252
+ childLoadingKeys = new Set([...childLoadingKeys, row.id]);
253
+ const errors = new Map(childErrors);
254
+ errors.delete(row.id);
255
+ childErrors = errors;
256
+ try {
257
+ const rawChildren: unknown = snapshotPlainData(await loader(row.record, row.id));
258
+ if (!current()) return;
259
+ if (!Array.isArray(rawChildren) || !rawChildren.every((child): child is Record<string, unknown> =>
260
+ typeof child === 'object' && child !== null && !Array.isArray(child))) {
261
+ throw new Error('Invalid children');
262
+ }
263
+ const children: Record<string, unknown>[] = rawChildren;
264
+ const candidate = new Map(loadedChildren);
265
+ candidate.set(row.id, children);
266
+ if (!indexTreeTable(data, primaryKey, childrenKey, candidate).valid) throw new Error('Invalid children');
267
+ loadedChildren = candidate;
268
+ } catch {
269
+ if (current()) childErrors = new Map([...childErrors, [row.id, i18n.t('treeTable.loadFailed')]]);
270
+ } finally {
271
+ if (current()) {
272
+ const next = new Set(childLoadingKeys);
273
+ next.delete(row.id);
274
+ childLoadingKeys = next;
275
+ }
276
+ }
277
+ }
75
278
 
76
279
  function toggleExpand(id: string | number) {
280
+ if (!available || disabled || filterText.trim().length > 0) return;
281
+ const row = fullIndex.nodes.get(id);
77
282
  const next = new Set(expandedKeys);
78
283
  if (next.has(id)) {
79
284
  next.delete(id);
@@ -81,120 +286,236 @@
81
286
  next.add(id);
82
287
  }
83
288
  expandedKeys = next;
289
+ if (row && next.has(id)) void loadNodeChildren(row);
84
290
  }
85
291
 
86
- function expandAll() {
87
- const all = new Set<string | number>();
88
- function collect(items: Record<string, unknown>[]) {
89
- for (const item of items) {
90
- const id = item[primaryKey] as string | number;
91
- const children = item[childrenKey] as Record<string, unknown>[] | undefined;
92
- if (Array.isArray(children) && children.length > 0) {
93
- all.add(id);
94
- collect(children);
95
- }
96
- }
292
+ function cycleSort(column: TreeTableColumn): void {
293
+ if (!available || disabled || !column.sortable) return;
294
+ if (!sort || sort.key !== column.key) {
295
+ sort = { key: column.key, direction: 'asc' };
296
+ } else if (sort.direction === 'asc') {
297
+ sort = { key: column.key, direction: 'desc' };
298
+ } else {
299
+ sort = undefined;
97
300
  }
98
- collect(data);
99
- expandedKeys = all;
301
+ onsortchange?.(sort);
302
+ }
303
+
304
+ function expandAll() {
305
+ if (!available || disabled || filterText.trim().length > 0) return;
306
+ expandedKeys = new Set([...index.nodes.values()].filter(node => node.children.length).map(node => node.id));
100
307
  }
101
308
 
102
309
  function collapseAll() {
310
+ if (!available || disabled || filterText.trim().length > 0) return;
103
311
  expandedKeys = new Set();
104
312
  }
105
313
 
106
- function getAllDescendantIds(item: Record<string, unknown>): (string | number)[] {
107
- const ids: (string | number)[] = [item[primaryKey] as string | number];
108
- const children = item[childrenKey] as Record<string, unknown>[] | undefined;
109
- if (Array.isArray(children)) {
110
- for (const child of children) {
111
- ids.push(...getAllDescendantIds(child));
314
+ function toggleSelect(id: TreeTableId) {
315
+ if (!available || disabled || !selectable || fullIndex.nodes.get(id)?.disabled) return;
316
+ const ids = new Set<TreeTableId>();
317
+ const stack = [id];
318
+ while (stack.length) {
319
+ const key = stack.pop();
320
+ const node = key === undefined ? undefined : fullIndex.nodes.get(key);
321
+ if (!node) continue;
322
+ if (!node.disabled) ids.add(node.id);
323
+ for (let child = node.children.length - 1; child >= 0; child -= 1) {
324
+ const next = node.children[child];
325
+ if (next !== undefined) stack.push(next);
112
326
  }
113
327
  }
114
- return ids;
328
+ const count = selection.get(id);
329
+ const isSelected = count !== undefined && count.selected === count.total;
330
+ const next = isSelected ? selectedKeys.filter(key => !ids.has(key)) : [...new Set([...selectedKeys, ...ids])];
331
+ selectedKeys = next;
332
+ onselect?.(next);
115
333
  }
116
334
 
117
- function toggleSelect(item: Record<string, unknown>) {
118
- const descendantIds = getAllDescendantIds(item);
119
- const isSelected = selectedKeys.includes(item[primaryKey] as string | number);
120
-
121
- let next: (string | number)[];
122
- if (isSelected) {
123
- next = selectedKeys.filter((k) => !descendantIds.includes(k));
124
- } else {
125
- next = Array.from(new Set([...selectedKeys, ...descendantIds]));
335
+ $effect.pre(() => {
336
+ if (!sameLoadScope(loadScope)) {
337
+ generation++;
338
+ loadScope = captureLoadScope();
339
+ loadedChildren = new Map();
340
+ childErrors = new Map();
341
+ childLoadingKeys = new Set();
126
342
  }
343
+ });
127
344
 
128
- selectedKeys = next;
129
- onselect?.(next);
345
+ $effect.pre(() => {
346
+ const changedScope = scopeKey !== observedScope;
347
+ const changedPage = page !== observedPage;
348
+ observedScope = scopeKey;
349
+ observedPage = page;
350
+ const nodes = fullIndex.nodes;
351
+ untrack(() => {
352
+ if (changedScope) focusedKey = undefined;
353
+ if (changedScope || changedPage) scrollTop = 0;
354
+ expandedKeys = new Set(changedScope ? [] : [...expandedKeys].filter(key => nodes.has(key)));
355
+ const next = changedScope ? [] : [...new Set(selectedKeys)].filter(key => nodes.get(key)?.disabled === false);
356
+ if (next.length !== selectedKeys.length || next.some((key, i) => key !== selectedKeys[i])) {
357
+ selectedKeys = next;
358
+ onselect?.([...next]);
359
+ }
360
+ });
361
+ });
362
+
363
+ function setPage(next: number): void {
364
+ page = next;
365
+ scrollTop = 0;
366
+ onpagechange?.(next);
130
367
  }
368
+ let queryKey = untrack(() => JSON.stringify([filterText, filterKeys, sort, pageSize, scopeKey]));
369
+ $effect.pre(() => {
370
+ const next = JSON.stringify([filterText, filterKeys, sort, pageSize, scopeKey]);
371
+ if (next !== queryKey) {
372
+ queryKey = next;
373
+ scrollTop = 0;
374
+ untrack(() => { if (page !== 1) setPage(1); });
375
+ }
376
+ });
377
+ $effect(() => {
378
+ if (normalizedPageSize > 0 && page !== normalizedPage) setPage(normalizedPage);
379
+ });
380
+ $effect(() => {
381
+ const top = virtualized ? boundedScrollTop : 0;
382
+ if (viewport && viewport.scrollTop !== top) viewport.scrollTop = top;
383
+ if (virtualized && scrollTop !== top) scrollTop = top;
384
+ });
131
385
  </script>
132
386
 
133
387
  <div class={cn('svadmin-u-6da6a3c3f741 svadmin-u-6f7e013d6499', className)}>
134
388
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-8ef2268efbbc svadmin-u-d8e0e382c67b svadmin-u-660d2effb880 svadmin-u-359090c2d529">
135
389
  <div class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-77a2a20e90d4">
136
- <Button variant="ghost" size="sm" class="svadmin-u-f6fe902450dc svadmin-u-d5eab218aa34 svadmin-u-359090c2d529" onclick={expandAll}>
137
- Expand All
390
+ {#if filterable}
391
+ <Input
392
+ bind:value={filterText}
393
+ disabled={disabled || loading}
394
+ placeholder={filterPlaceholder ?? i18n.t('common.search')}
395
+ aria-label={filterPlaceholder ?? i18n.t('common.search')}
396
+ />
397
+ {/if}
398
+ <Button type="button" disabled={!available || disabled || filterText.trim().length > 0} variant="ghost" size="sm" class="svadmin-u-f6fe902450dc svadmin-u-d5eab218aa34 svadmin-u-359090c2d529" onclick={expandAll}>
399
+ {i18n.t('treeTable.expandAll')}
138
400
  </Button>
139
- <Button variant="ghost" size="sm" class="svadmin-u-f6fe902450dc svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-bfa603190748" onclick={collapseAll}>
140
- Collapse All
401
+ {#if normalizedPageSize > 0}
402
+ <span role="status">{normalizedPage} / {pageCount}</span>
403
+ <Button type="button" disabled={!available || disabled || normalizedPage <= 1} variant="ghost" size="sm"
404
+ title={i18n.t('treeTable.previousPage')} aria-label={i18n.t('treeTable.previousPage')}
405
+ onclick={() => setPage(normalizedPage - 1)}><ChevronLeft aria-hidden="true" /></Button>
406
+ <Button type="button" disabled={!available || disabled || normalizedPage >= pageCount} variant="ghost" size="sm"
407
+ title={i18n.t('treeTable.nextPage')} aria-label={i18n.t('treeTable.nextPage')}
408
+ onclick={() => setPage(normalizedPage + 1)}><ChevronRight aria-hidden="true" /></Button>
409
+ {/if}
410
+ <Button type="button" disabled={!available || disabled || filterText.trim().length > 0} variant="ghost" size="sm" class="svadmin-u-f6fe902450dc svadmin-u-d5eab218aa34 svadmin-u-359090c2d529 svadmin-u-bfa603190748" onclick={collapseAll}>
411
+ {i18n.t('treeTable.collapseAll')}
141
412
  </Button>
142
413
  </div>
143
414
  {#if selectable && selectedKeys.length > 0}
144
415
  <span class="svadmin-u-359090c2d529 svadmin-u-bfa603190748">
145
- Selected: <strong class="svadmin-u-d4108abe6359">{selectedKeys.length}</strong>
416
+ {i18n.t('common.selectedCount', { count: String(selectedKeys.length) })}
146
417
  </span>
147
418
  {/if}
148
419
  </div>
149
420
 
150
- <div class="svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-c9ed8c5f79ae svadmin-u-2cd02d11d1af svadmin-u-cd0ad9a56558">
151
- <table class="svadmin-u-6da6a3c3f741 svadmin-u-2eba0d65d059 svadmin-u-359090c2d529 svadmin-u-4583f90cd9bd">
421
+ {#if loading}
422
+ <div role="status">{i18n.t('common.loading')}</div>
423
+ {:else if displayError}
424
+ <div role="alert">{displayError}</div>
425
+ {#if error && onRetry}
426
+ <Button type="button" variant="ghost" size="sm" onclick={onRetry}><RotateCw aria-hidden="true" />{i18n.t('common.retry')}</Button>
427
+ {/if}
428
+ {:else}
429
+ <div bind:this={viewport} data-testid="tree-table-viewport" class:tree-table-virtual={virtualized}
430
+ onscroll={event => { scrollTop = event.currentTarget.scrollTop; }}
431
+ style={virtualized ? `height: ${normalizedHeight}px; overflow: auto; --tree-row-height: ${normalizedRowHeight}px;` : 'overflow-x: auto;'}
432
+ class="svadmin-u-5f22e64f2282 svadmin-u-ca6bcd4b6f3f svadmin-u-c9ed8c5f79ae svadmin-u-2cd02d11d1af svadmin-u-cd0ad9a56558">
433
+ <!-- 原生表格保留列布局,treegrid 为可聚焦的层级行提供交互语义。 -->
434
+ <!-- svelte-ignore a11y_no_noninteractive_element_to_interactive_role -->
435
+ <table bind:this={tableRoot} role="treegrid" aria-label={ariaLabel ?? i18n.t('treeTable.label')}
436
+ aria-rowcount={flattenedRows.length + 1} aria-colcount={cellCount}
437
+ aria-multiselectable={selectable ? 'true' : undefined} aria-disabled={disabled}
438
+ class="svadmin-u-6da6a3c3f741 svadmin-u-2eba0d65d059 svadmin-u-359090c2d529 svadmin-u-4583f90cd9bd">
152
439
  <thead class="svadmin-u-358af0b65a31 svadmin-u-65fdbade2025 svadmin-u-05faf5c801ff svadmin-u-e83a7042bc91 svadmin-u-bfa603190748">
153
440
  <tr>
154
441
  {#if selectable}
155
442
  <th class="svadmin-u-2bbcfc3b5179 svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b svadmin-u-ca6bf63030aa"></th>
156
443
  {/if}
157
444
  {#each columns as col (col.key)}
158
- <th class="svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b" style={col.width ? `width: ${col.width}` : ''}>
159
- {col.label}
445
+ <th class="svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b" style={col.width ? `width: ${col.width}` : ''}
446
+ aria-sort={sort?.key === col.key ? (sort.direction === 'asc' ? 'ascending' : 'descending') : col.sortable ? 'none' : undefined}>
447
+ {#if col.sortable}
448
+ <button class="tree-sort" type="button" disabled={!available || disabled} onclick={() => cycleSort(col)}
449
+ title={i18n.t('treeTable.sortColumn', { column: col.label })}
450
+ aria-label={i18n.t('treeTable.sortColumn', { column: col.label })}>
451
+ {col.label}
452
+ {#if sort?.key !== col.key}<ArrowUpDown size={14} aria-hidden="true" />
453
+ {:else if sort.direction === 'asc'}<ArrowUp size={14} aria-hidden="true" />
454
+ {:else}<ArrowDown size={14} aria-hidden="true" />{/if}
455
+ </button>
456
+ {:else}
457
+ {col.label}
458
+ {/if}
160
459
  </th>
161
460
  {/each}
162
461
  {#if rowActions}
163
- <th class="svadmin-u-ed831a4dff32 svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b svadmin-u-308fc069e46e">Actions</th>
462
+ <th class="svadmin-u-ed831a4dff32 svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b svadmin-u-308fc069e46e">{i18n.t('common.actions')}</th>
164
463
  {/if}
165
464
  </tr>
166
465
  </thead>
167
466
  <tbody class="svadmin-u-fa6acbf81d74 svadmin-u-08b5607c7258">
168
- {#each flattenedRows as row (row.id)}
169
- {@const isSelected = selectedKeys.includes(row.id)}
170
- <tr class="svadmin-u-12251b8f1749 svadmin-u-ceb69a6b0e5f {isSelected ? 'svadmin-u-989c466fdbe7' : ''}">
467
+ {#if virtualized && windowStart > 0}
468
+ <tr aria-hidden="true" class="tree-spacer"><td colspan={cellCount}
469
+ style={`height: ${windowStart * normalizedRowHeight}px;`}></td></tr>
470
+ {/if}
471
+ {#key scopeKey}
472
+ {#each renderedRows as row, renderedIndex (nodeKey(row.id))}
473
+ {@const count = selection.get(row.id)}
474
+ {@const isSelected = count !== undefined && count.total > 0 && count.selected === count.total}
475
+ {@const partial = count !== undefined && count.selected > 0 && count.selected < count.total}
476
+ <tr tabindex={focusTarget === row.id ? 0 : -1}
477
+ aria-rowindex={windowStart + renderedIndex + 2}
478
+ aria-level={row.level + 1} aria-expanded={row.hasChildren ? row.isExpanded : undefined}
479
+ aria-busy={childLoadingKeys.has(row.id)}
480
+ aria-disabled={disabled || row.disabled ? 'true' : undefined}
481
+ aria-selected={selectable ? isSelected : undefined} data-tree-key={nodeKey(row.id)}
482
+ onfocus={() => { focusedKey = row.id; }} onkeydown={event => handleKeydown(event, row)}
483
+ class="svadmin-u-12251b8f1749 svadmin-u-ceb69a6b0e5f {isSelected ? 'svadmin-u-989c466fdbe7' : ''}">
171
484
  {#if selectable}
172
485
  <td class="svadmin-u-2bbcfc3b5179 svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b svadmin-u-ca6bf63030aa">
486
+ <div class="tree-cell">
173
487
  <Checkbox
174
488
  checked={isSelected}
175
- onCheckedChange={() => toggleSelect(row.record)}
176
- aria-label="Select {row.id}"
489
+ indeterminate={partial}
490
+ disabled={disabled || row.disabled}
491
+ onCheckedChange={() => toggleSelect(row.id)}
492
+ aria-label={i18n.t('common.selectRow', { id: String(row.id) })}
177
493
  />
494
+ </div>
178
495
  </td>
179
496
  {/if}
180
497
 
181
498
  {#each columns as col, colIdx (col.key)}
182
499
  {@const val = row.record[col.key]}
183
500
  <td class="svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b">
501
+ <div class="tree-cell">
184
502
  {#if colIdx === 0}
185
503
  <!-- Primary tree node column with indent -->
186
504
  <div
187
- class="svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-58284b4ea568"
505
+ class="tree-node-content svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-58284b4ea568"
188
506
  style="padding-left: {row.level * 20}px;"
189
507
  >
190
508
  {#if row.hasChildren}
191
509
  <button
192
510
  type="button"
511
+ disabled={disabled || filterText.trim().length > 0}
193
512
  class="svadmin-u-cd0d9c512cdc svadmin-u-72470489ff4e svadmin-u-60fbb7713999 svadmin-u-3960ffc248d9 svadmin-u-86843cf1e227 svadmin-u-07389a777c1f svadmin-u-8e551981c8d7 svadmin-u-bfa603190748 svadmin-u-ea7b2e9e070e svadmin-u-ceb69a6b0e5f"
194
513
  onclick={() => toggleExpand(row.id)}
195
- aria-label={row.isExpanded ? 'Collapse' : 'Expand'}
514
+ aria-label={`${String(val ?? row.id)}: ${i18n.t(row.isExpanded ? 'treeTable.collapse' : 'treeTable.expand')}`}
196
515
  >
197
- {#if row.isExpanded}
516
+ {#if childLoadingKeys.has(row.id)}
517
+ <RotateCw class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" aria-label={i18n.t('common.loading')} />
518
+ {:else if row.isExpanded}
198
519
  <ChevronDown class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
199
520
  {:else}
200
521
  <ChevronRight class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c" />
@@ -206,32 +527,113 @@
206
527
  <File class="svadmin-u-7fc7f732bf7e svadmin-u-bf600f8e029c svadmin-u-3a33f11548cd svadmin-u-012fbd121f37" />
207
528
  {/if}
208
529
 
209
- <span class="svadmin-u-2689f3958069 svadmin-u-d4108abe6359 svadmin-u-f283ea9bea0e">
210
- {val ?? '—'}
211
- </span>
530
+ {#if customCell}
531
+ {@render customCell({ column: col, record: row.record, value: val })}
532
+ {:else}
533
+ <span class="svadmin-u-2689f3958069 svadmin-u-d4108abe6359 svadmin-u-f283ea9bea0e">{val ?? '—'}</span>
534
+ {/if}
212
535
  </div>
536
+ {#if childErrors.has(row.id)}
537
+ <span role="alert" title={childErrors.get(row.id)}>{childErrors.get(row.id)}</span>
538
+ <Button type="button" variant="ghost" size="sm" disabled={disabled || childLoadingKeys.has(row.id)}
539
+ onclick={() => {
540
+ const node = fullIndex.nodes.get(row.id);
541
+ if (node) {
542
+ expandedKeys = new Set([...expandedKeys, row.id]);
543
+ void loadNodeChildren(node);
544
+ }
545
+ }}><RotateCw aria-hidden="true" />{i18n.t('common.retry')}</Button>
546
+ {/if}
213
547
  {:else if customCell}
214
548
  {@render customCell({ column: col, record: row.record, value: val })}
215
549
  {:else}
216
550
  <span class="svadmin-u-d4108abe6359 svadmin-u-f283ea9bea0e">{val ?? '—'}</span>
217
551
  {/if}
552
+ </div>
218
553
  </td>
219
554
  {/each}
220
555
 
221
556
  {#if rowActions}
222
557
  <td class="svadmin-u-ed831a4dff32 svadmin-u-0e17f2bd9074 svadmin-u-03b4dd7f172b svadmin-u-308fc069e46e">
558
+ <div class="tree-cell">
223
559
  {@render rowActions({ record: row.record, id: row.id })}
560
+ </div>
224
561
  </td>
225
562
  {/if}
226
563
  </tr>
227
564
  {:else}
228
565
  <tr>
229
566
  <td colspan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} class="svadmin-u-a1f611f027dd svadmin-u-ca6bf63030aa svadmin-u-bfa603190748">
230
- No data available
567
+ {i18n.t('common.noData')}
231
568
  </td>
232
569
  </tr>
233
570
  {/each}
571
+ {/key}
572
+ {#if virtualized && windowEnd < flattenedRows.length}
573
+ <tr aria-hidden="true" class="tree-spacer"><td colspan={cellCount}
574
+ style={`height: ${(flattenedRows.length - windowEnd) * normalizedRowHeight}px;`}></td></tr>
575
+ {/if}
234
576
  </tbody>
235
577
  </table>
236
578
  </div>
579
+ {/if}
237
580
  </div>
581
+
582
+ <style>
583
+ .tree-sort {
584
+ display: inline-flex;
585
+ align-items: center;
586
+ gap: 4px;
587
+ }
588
+ .tree-table-virtual .tree-node-content {
589
+ display: flex;
590
+ align-items: center;
591
+ min-width: 0;
592
+ overflow: hidden;
593
+ }
594
+ .tree-table-virtual .tree-cell > [role='alert'] {
595
+ min-width: 0;
596
+ overflow: hidden;
597
+ text-overflow: ellipsis;
598
+ }
599
+ .tree-table-virtual .tree-cell > :global(button) {
600
+ flex-shrink: 0;
601
+ max-height: var(--tree-row-height);
602
+ }
603
+ .tree-table-virtual table {
604
+ table-layout: fixed;
605
+ border-collapse: separate;
606
+ border-spacing: 0;
607
+ }
608
+ .tree-table-virtual thead {
609
+ position: sticky;
610
+ top: 0;
611
+ z-index: 1;
612
+ background: var(--background, white);
613
+ }
614
+ .tree-table-virtual th {
615
+ height: var(--tree-row-height);
616
+ box-sizing: border-box;
617
+ padding-block: 0;
618
+ overflow: hidden;
619
+ white-space: nowrap;
620
+ }
621
+ .tree-table-virtual td {
622
+ padding: 0;
623
+ border: 0;
624
+ }
625
+ .tree-table-virtual .tree-cell {
626
+ height: var(--tree-row-height);
627
+ box-sizing: border-box;
628
+ overflow: hidden;
629
+ display: flex;
630
+ align-items: center;
631
+ gap: 4px;
632
+ padding-inline: 12px;
633
+ white-space: nowrap;
634
+ }
635
+ .tree-spacer td {
636
+ padding: 0;
637
+ border: 0;
638
+ }
639
+ </style>