@sabrenski/spire-ui 0.0.1

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 (237) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +233 -0
  3. package/dist/index.d.ts +4981 -0
  4. package/dist/spire-ui.css +1 -0
  5. package/dist/spire-ui.es.js +18403 -0
  6. package/dist/spire-ui.umd.js +45 -0
  7. package/package.json +83 -0
  8. package/src/components/Accordion/Accordion.test.ts +218 -0
  9. package/src/components/Accordion/AccordionContent.vue +112 -0
  10. package/src/components/Accordion/AccordionItem.vue +87 -0
  11. package/src/components/Accordion/AccordionRoot.vue +111 -0
  12. package/src/components/Accordion/AccordionTrigger.vue +125 -0
  13. package/src/components/Accordion/index.ts +11 -0
  14. package/src/components/Accordion/keys.ts +23 -0
  15. package/src/components/Avatar/Avatar.test.ts +181 -0
  16. package/src/components/Avatar/Avatar.vue +150 -0
  17. package/src/components/Avatar/index.ts +2 -0
  18. package/src/components/Badge/Badge.test.ts +141 -0
  19. package/src/components/Badge/Badge.vue +133 -0
  20. package/src/components/Badge/index.ts +2 -0
  21. package/src/components/BadgeContainer/BadgeContainer.test.ts +150 -0
  22. package/src/components/BadgeContainer/BadgeContainer.vue +90 -0
  23. package/src/components/BadgeContainer/index.ts +2 -0
  24. package/src/components/Breadcrumb/Breadcrumb.test.ts +342 -0
  25. package/src/components/Breadcrumb/BreadcrumbEllipsis.vue +96 -0
  26. package/src/components/Breadcrumb/BreadcrumbItem.vue +16 -0
  27. package/src/components/Breadcrumb/BreadcrumbLink.vue +67 -0
  28. package/src/components/Breadcrumb/BreadcrumbList.vue +20 -0
  29. package/src/components/Breadcrumb/BreadcrumbPage.vue +25 -0
  30. package/src/components/Breadcrumb/BreadcrumbRoot.vue +41 -0
  31. package/src/components/Breadcrumb/BreadcrumbSeparator.vue +63 -0
  32. package/src/components/Breadcrumb/index.ts +13 -0
  33. package/src/components/Breadcrumb/keys.ts +7 -0
  34. package/src/components/Button/Button.test.ts +231 -0
  35. package/src/components/Button/Button.vue +349 -0
  36. package/src/components/Button/index.ts +2 -0
  37. package/src/components/Callout/Callout.test.ts +260 -0
  38. package/src/components/Callout/Callout.vue +341 -0
  39. package/src/components/Callout/index.ts +2 -0
  40. package/src/components/Card/Card.test.ts +565 -0
  41. package/src/components/Card/Card.vue +209 -0
  42. package/src/components/Card/CardContent.vue +57 -0
  43. package/src/components/Card/CardFooter.vue +72 -0
  44. package/src/components/Card/CardHeader.vue +111 -0
  45. package/src/components/Card/CardImage.vue +124 -0
  46. package/src/components/Card/index.ts +14 -0
  47. package/src/components/Chart/BarChart.vue +208 -0
  48. package/src/components/Chart/BaseChart.vue +444 -0
  49. package/src/components/Chart/Chart.test.ts +359 -0
  50. package/src/components/Chart/DonutChart.vue +283 -0
  51. package/src/components/Chart/LineChart.vue +211 -0
  52. package/src/components/Chart/index.ts +20 -0
  53. package/src/components/Chart/useChartTheme.ts +192 -0
  54. package/src/components/Checkbox/Checkbox.test.ts +209 -0
  55. package/src/components/Checkbox/Checkbox.vue +285 -0
  56. package/src/components/Checkbox/index.ts +2 -0
  57. package/src/components/ChoiceChip/ChoiceChip.test.ts +142 -0
  58. package/src/components/ChoiceChip/ChoiceChip.vue +218 -0
  59. package/src/components/ChoiceChip/index.ts +2 -0
  60. package/src/components/ChoiceChipGroup/ChoiceChipGroup.test.ts +151 -0
  61. package/src/components/ChoiceChipGroup/ChoiceChipGroup.vue +70 -0
  62. package/src/components/ChoiceChipGroup/index.ts +2 -0
  63. package/src/components/ColorPicker/ColorArea.vue +159 -0
  64. package/src/components/ColorPicker/ColorPicker.test.ts +250 -0
  65. package/src/components/ColorPicker/ColorPicker.vue +339 -0
  66. package/src/components/ColorPicker/ColorSlider.vue +191 -0
  67. package/src/components/ColorPicker/index.ts +7 -0
  68. package/src/components/Combobox/Combobox.test.ts +891 -0
  69. package/src/components/Combobox/Combobox.vue +934 -0
  70. package/src/components/Combobox/index.ts +2 -0
  71. package/src/components/DataTable/DataTable.test.ts +1221 -0
  72. package/src/components/DataTable/DataTable.vue +1415 -0
  73. package/src/components/DataTable/index.ts +10 -0
  74. package/src/components/DatePicker/DatePicker.test.ts +625 -0
  75. package/src/components/DatePicker/DatePicker.vue +1586 -0
  76. package/src/components/DatePicker/index.ts +2 -0
  77. package/src/components/Drawer/Drawer.test.ts +336 -0
  78. package/src/components/Drawer/Drawer.vue +466 -0
  79. package/src/components/Drawer/index.ts +2 -0
  80. package/src/components/Dropdown/Dropdown.test.ts +607 -0
  81. package/src/components/Dropdown/Dropdown.vue +807 -0
  82. package/src/components/Dropdown/DropdownItem.vue +227 -0
  83. package/src/components/Dropdown/DropdownSeparator.vue +14 -0
  84. package/src/components/Dropdown/DropdownSub.vue +104 -0
  85. package/src/components/Dropdown/DropdownSubContent.vue +187 -0
  86. package/src/components/Dropdown/DropdownSubTrigger.vue +151 -0
  87. package/src/components/Dropdown/index.ts +14 -0
  88. package/src/components/EmptyState/EmptyState.test.ts +180 -0
  89. package/src/components/EmptyState/EmptyState.vue +137 -0
  90. package/src/components/EmptyState/index.ts +2 -0
  91. package/src/components/FileUpload/FileUpload.test.ts +1151 -0
  92. package/src/components/FileUpload/FileUpload.vue +1042 -0
  93. package/src/components/FileUpload/index.ts +2 -0
  94. package/src/components/Heading/Heading.test.ts +107 -0
  95. package/src/components/Heading/Heading.vue +67 -0
  96. package/src/components/Heading/index.ts +2 -0
  97. package/src/components/Icon/Icon.test.ts +157 -0
  98. package/src/components/Icon/Icon.vue +86 -0
  99. package/src/components/Icon/index.ts +2 -0
  100. package/src/components/Input/Input.test.ts +273 -0
  101. package/src/components/Input/Input.vue +388 -0
  102. package/src/components/Input/index.ts +2 -0
  103. package/src/components/Layout/Container.vue +67 -0
  104. package/src/components/Layout/Grid.vue +159 -0
  105. package/src/components/Layout/GridItem.vue +154 -0
  106. package/src/components/Layout/Layout.test.ts +202 -0
  107. package/src/components/Layout/Stack.vue +128 -0
  108. package/src/components/Layout/index.ts +9 -0
  109. package/src/components/Layout/keys.ts +7 -0
  110. package/src/components/Modal/Modal.test.ts +311 -0
  111. package/src/components/Modal/Modal.vue +336 -0
  112. package/src/components/Modal/index.ts +2 -0
  113. package/src/components/Pagination/Pagination.test.ts +303 -0
  114. package/src/components/Pagination/Pagination.vue +212 -0
  115. package/src/components/Pagination/index.ts +3 -0
  116. package/src/components/Pagination/utils.ts +86 -0
  117. package/src/components/Popover/Popover.test.ts +285 -0
  118. package/src/components/Popover/Popover.vue +441 -0
  119. package/src/components/Popover/index.ts +2 -0
  120. package/src/components/Progress/Progress.test.ts +361 -0
  121. package/src/components/Progress/Progress.vue +363 -0
  122. package/src/components/Progress/index.ts +7 -0
  123. package/src/components/Radio/Radio.test.ts +216 -0
  124. package/src/components/Radio/Radio.vue +214 -0
  125. package/src/components/Radio/index.ts +2 -0
  126. package/src/components/Rating/Rating.test.ts +319 -0
  127. package/src/components/Rating/Rating.vue +247 -0
  128. package/src/components/Rating/index.ts +2 -0
  129. package/src/components/SegmentedControl/SegmentedControl.test.ts +292 -0
  130. package/src/components/SegmentedControl/SegmentedControl.vue +288 -0
  131. package/src/components/SegmentedControl/index.ts +2 -0
  132. package/src/components/Select/Select.test.ts +589 -0
  133. package/src/components/Select/Select.vue +666 -0
  134. package/src/components/Select/index.ts +2 -0
  135. package/src/components/Sidebar/Sidebar.test.ts +301 -0
  136. package/src/components/Sidebar/SidebarGroup.vue +103 -0
  137. package/src/components/Sidebar/SidebarItem.vue +196 -0
  138. package/src/components/Sidebar/SidebarLayout.vue +42 -0
  139. package/src/components/Sidebar/SidebarRoot.vue +122 -0
  140. package/src/components/Sidebar/index.ts +11 -0
  141. package/src/components/Sidebar/keys.ts +14 -0
  142. package/src/components/Skeleton/Skeleton.test.ts +130 -0
  143. package/src/components/Skeleton/Skeleton.vue +104 -0
  144. package/src/components/Skeleton/index.ts +2 -0
  145. package/src/components/Slider/Slider.test.ts +416 -0
  146. package/src/components/Slider/Slider.vue +435 -0
  147. package/src/components/Slider/index.ts +2 -0
  148. package/src/components/Slider/utils.ts +91 -0
  149. package/src/components/Spinner/Spinner.test.ts +79 -0
  150. package/src/components/Spinner/Spinner.vue +159 -0
  151. package/src/components/Spinner/index.ts +2 -0
  152. package/src/components/SpireProvider/SpireProvider.vue +71 -0
  153. package/src/components/SpireProvider/index.ts +11 -0
  154. package/src/components/Stepper/Stepper.test.ts +221 -0
  155. package/src/components/Stepper/StepperContent.vue +51 -0
  156. package/src/components/Stepper/StepperItem.vue +89 -0
  157. package/src/components/Stepper/StepperRoot.vue +101 -0
  158. package/src/components/Stepper/StepperSeparator.vue +52 -0
  159. package/src/components/Stepper/StepperTrigger.vue +144 -0
  160. package/src/components/Stepper/index.ts +11 -0
  161. package/src/components/Stepper/keys.ts +27 -0
  162. package/src/components/Switch/Switch.test.ts +214 -0
  163. package/src/components/Switch/Switch.vue +235 -0
  164. package/src/components/Switch/index.ts +2 -0
  165. package/src/components/Tabs/Tabs.test.ts +363 -0
  166. package/src/components/Tabs/Tabs.vue +318 -0
  167. package/src/components/Tabs/index.ts +2 -0
  168. package/src/components/Text/Text.test.ts +154 -0
  169. package/src/components/Text/Text.vue +100 -0
  170. package/src/components/Text/index.ts +2 -0
  171. package/src/components/Textarea/Textarea.test.ts +432 -0
  172. package/src/components/Textarea/Textarea.vue +411 -0
  173. package/src/components/Textarea/index.ts +2 -0
  174. package/src/components/TimePicker/TimePicker.test.ts +352 -0
  175. package/src/components/TimePicker/TimePicker.vue +569 -0
  176. package/src/components/TimePicker/index.ts +2 -0
  177. package/src/components/Timeline/Timeline.test.ts +193 -0
  178. package/src/components/Timeline/Timeline.vue +111 -0
  179. package/src/components/Timeline/TimelineItem.vue +167 -0
  180. package/src/components/Timeline/index.ts +13 -0
  181. package/src/components/Timeline/keys.ts +21 -0
  182. package/src/components/Toast/ToastItem.test.ts +289 -0
  183. package/src/components/Toast/ToastItem.vue +370 -0
  184. package/src/components/Toast/ToastProvider.test.ts +158 -0
  185. package/src/components/Toast/ToastProvider.vue +181 -0
  186. package/src/components/Toast/index.ts +83 -0
  187. package/src/components/Toast/toastState.test.ts +165 -0
  188. package/src/components/Toast/toastState.ts +161 -0
  189. package/src/components/ToggleButton/ToggleButton.test.ts +166 -0
  190. package/src/components/ToggleButton/ToggleButton.vue +197 -0
  191. package/src/components/ToggleButton/index.ts +2 -0
  192. package/src/components/ToggleGroup/ToggleGroup.test.ts +181 -0
  193. package/src/components/ToggleGroup/ToggleGroup.vue +130 -0
  194. package/src/components/ToggleGroup/index.ts +2 -0
  195. package/src/components/Tooltip/Tooltip.test.ts +238 -0
  196. package/src/components/Tooltip/Tooltip.vue +217 -0
  197. package/src/components/Tooltip/index.ts +2 -0
  198. package/src/components/TreeView/TreeView.test.ts +357 -0
  199. package/src/components/TreeView/TreeView.vue +251 -0
  200. package/src/components/TreeView/TreeViewItem.vue +288 -0
  201. package/src/components/TreeView/index.ts +11 -0
  202. package/src/components/TreeView/keys.ts +35 -0
  203. package/src/composables/index.ts +12 -0
  204. package/src/composables/useClickOutside.ts +36 -0
  205. package/src/composables/useClipboard.ts +35 -0
  206. package/src/composables/useEventListener.ts +48 -0
  207. package/src/composables/useFocusTrap.ts +58 -0
  208. package/src/composables/useHoverReveal.ts +98 -0
  209. package/src/composables/useId.ts +10 -0
  210. package/src/composables/useMagnetic.ts +171 -0
  211. package/src/composables/useRelativePosition.ts +127 -0
  212. package/src/composables/useRipple.ts +146 -0
  213. package/src/composables/useScrollLock.ts +25 -0
  214. package/src/composables/useSpireConfig.ts +27 -0
  215. package/src/composables/useStagger.ts +224 -0
  216. package/src/config/icons.test.ts +115 -0
  217. package/src/config/icons.ts +170 -0
  218. package/src/index.ts +361 -0
  219. package/src/styles/depth.css +129 -0
  220. package/src/styles/effects.css +169 -0
  221. package/src/styles/fallback.css +152 -0
  222. package/src/styles/main.css +25 -0
  223. package/src/styles/mood.css +211 -0
  224. package/src/styles/motion.css +159 -0
  225. package/src/styles/reset.css +97 -0
  226. package/src/styles/theme.css +708 -0
  227. package/src/styles/tokens.css +183 -0
  228. package/src/utils/.gitkeep +0 -0
  229. package/src/utils/color.ts +277 -0
  230. package/src/utils/date.test.ts +522 -0
  231. package/src/utils/date.ts +380 -0
  232. package/src/utils/index.ts +23 -0
  233. package/src/utils/object.test.ts +80 -0
  234. package/src/utils/object.ts +25 -0
  235. package/src/utils/string.test.ts +64 -0
  236. package/src/utils/string.ts +32 -0
  237. package/src/utils/time.ts +156 -0
@@ -0,0 +1,1415 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
3
+ import Checkbox from '../Checkbox/Checkbox.vue'
4
+ import EmptyState from '../EmptyState/EmptyState.vue'
5
+ import Skeleton from '../Skeleton/Skeleton.vue'
6
+ import Pagination from '../Pagination/Pagination.vue'
7
+ import Select from '../Select/Select.vue'
8
+ import Text from '../Text/Text.vue'
9
+ import Input from '../Input/Input.vue'
10
+ import { getNestedValue } from '../../utils/object'
11
+ import { useInternalIcon } from '../../config/icons'
12
+ import { useId } from '../../composables'
13
+
14
+ const skeletonWidths = ['60%', '75%', '90%', '70%', '85%', '65%', '80%', '55%']
15
+ function getSkeletonWidth(rowIndex: number, colIndex: number): string {
16
+ return skeletonWidths[(rowIndex * 3 + colIndex) % skeletonWidths.length]
17
+ }
18
+
19
+ const SortAscIcon = useInternalIcon('sortAsc')
20
+ const SortDescIcon = useInternalIcon('sortDesc')
21
+ const SortNeutralIcon = useInternalIcon('sortNeutral')
22
+ const ExpandIcon = useInternalIcon('expand')
23
+ const CollapseIcon = useInternalIcon('collapse')
24
+ const SearchIcon = useInternalIcon('search')
25
+ const ClearIcon = useInternalIcon('close')
26
+
27
+ export type SortDirection = 'asc' | 'desc' | null
28
+ export type ColumnAlign = 'left' | 'center' | 'right'
29
+ export type FilterFn = (value: unknown, filterValue: string, row: Record<string, unknown>) => boolean
30
+
31
+ export interface DataTableColumn {
32
+ /** Key path (supports dot notation: 'user.email') */
33
+ key: string
34
+ /** Header label */
35
+ label: string
36
+ /** Text alignment */
37
+ align?: ColumnAlign
38
+ /** Fixed width (e.g., '80px') */
39
+ width?: string
40
+ /** Minimum width for resizable columns */
41
+ minWidth?: string
42
+ /** Enable sorting */
43
+ sortable?: boolean
44
+ /** Enable filtering for this column */
45
+ filterable?: boolean
46
+ /** Custom filter function */
47
+ filterFn?: FilterFn
48
+ /** Enable resizing for this column */
49
+ resizable?: boolean
50
+ }
51
+
52
+ export interface SortState {
53
+ key: string | null
54
+ direction: SortDirection
55
+ }
56
+
57
+ export interface PaginationConfig {
58
+ /** Default page size */
59
+ pageSize?: number
60
+ /** Available page size options */
61
+ pageSizeOptions?: number[]
62
+ /** Number of sibling pages to show */
63
+ siblingCount?: number
64
+ /** Show first/last page numbers */
65
+ showEdges?: boolean
66
+ }
67
+
68
+ export interface DataTableProps {
69
+ /** Data array */
70
+ data: Record<string, unknown>[]
71
+ /** Column definitions */
72
+ columns: DataTableColumn[]
73
+ /** Enable row selection */
74
+ selectable?: boolean
75
+ /** Selected row IDs (v-model:selectedIds) */
76
+ selectedIds?: (string | number)[]
77
+ /** Row identifier key (default: 'id') */
78
+ rowKey?: string
79
+ /** Loading state */
80
+ loading?: boolean
81
+ /** Skeleton rows count (default: 5) */
82
+ skeletonRows?: number
83
+ /** Striped rows */
84
+ striped?: boolean
85
+ /** Row hover effect */
86
+ hoverable?: boolean
87
+ /** Size variant */
88
+ size?: 'sm' | 'md' | 'lg'
89
+ /** Empty state message */
90
+ emptyMessage?: string
91
+ /** Show cell borders */
92
+ bordered?: boolean
93
+ /** Sticky header */
94
+ stickyHeader?: boolean
95
+ /** Enable pagination (boolean or config object) */
96
+ pagination?: boolean | PaginationConfig
97
+ /** Current page for controlled pagination (v-model:page) */
98
+ page?: number
99
+ /** Page size for controlled pagination (v-model:pageSize) */
100
+ pageSize?: number
101
+ /** Total items count (required for server-side pagination) */
102
+ total?: number
103
+ /** Server-side mode: disables client-side slicing, emits events for parent to handle */
104
+ serverSide?: boolean
105
+ /** Enable global search */
106
+ searchable?: boolean
107
+ /** Search query (v-model:search) */
108
+ search?: string
109
+ /** Placeholder for search input */
110
+ searchPlaceholder?: string
111
+ /** Enable column filters (shows filter inputs below headers) */
112
+ filterable?: boolean
113
+ /** Current filter values (v-model:filters) */
114
+ filters?: Record<string, string>
115
+ /** Enable column resizing */
116
+ resizable?: boolean
117
+ /** Enable expandable rows */
118
+ expandable?: boolean
119
+ /** Expanded row IDs (v-model:expandedIds) */
120
+ expandedIds?: (string | number)[]
121
+ }
122
+
123
+ const props = withDefaults(defineProps<DataTableProps>(), {
124
+ selectable: false,
125
+ selectedIds: () => [],
126
+ rowKey: 'id',
127
+ loading: false,
128
+ skeletonRows: 5,
129
+ striped: false,
130
+ hoverable: true,
131
+ size: 'md',
132
+ emptyMessage: 'No data available',
133
+ bordered: false,
134
+ stickyHeader: false,
135
+ pagination: false,
136
+ page: 1,
137
+ pageSize: 10,
138
+ serverSide: false,
139
+ searchable: false,
140
+ search: '',
141
+ searchPlaceholder: 'Search...',
142
+ filterable: false,
143
+ filters: () => ({}),
144
+ resizable: false,
145
+ expandable: false,
146
+ expandedIds: () => []
147
+ })
148
+
149
+ const emit = defineEmits<{
150
+ (e: 'update:selectedIds', value: (string | number)[]): void
151
+ (e: 'sort-change', value: SortState): void
152
+ (e: 'row-click', row: Record<string, unknown>, index: number): void
153
+ (e: 'update:page', value: number): void
154
+ (e: 'update:pageSize', value: number): void
155
+ (e: 'pagination-change', value: { page: number; pageSize: number }): void
156
+ (e: 'update:search', value: string): void
157
+ (e: 'update:filters', value: Record<string, string>): void
158
+ (e: 'filter-change', value: { search: string; filters: Record<string, string> }): void
159
+ (e: 'update:expandedIds', value: (string | number)[]): void
160
+ (e: 'row-expand', row: Record<string, unknown>, expanded: boolean): void
161
+ (e: 'column-resize', columnKey: string, width: number): void
162
+ }>()
163
+
164
+ const currentSort = ref<SortState>({ key: null, direction: null })
165
+ const uid = useId('datatable')
166
+
167
+ const isPaginationEnabled = computed(() => !!props.pagination)
168
+
169
+ const paginationConfig = computed<PaginationConfig>(() => {
170
+ if (typeof props.pagination === 'object') {
171
+ return props.pagination
172
+ }
173
+ return {}
174
+ })
175
+
176
+ const defaultPageSizeOptions = [10, 20, 50, 100]
177
+ const pageSizeOptions = computed(() => {
178
+ return paginationConfig.value.pageSizeOptions || defaultPageSizeOptions
179
+ })
180
+
181
+ const currentPage = ref(props.page)
182
+ const currentPageSize = ref(
183
+ paginationConfig.value.pageSize || props.pageSize
184
+ )
185
+
186
+ watch(() => props.page, (newPage) => {
187
+ currentPage.value = newPage
188
+ })
189
+
190
+ watch(() => props.pageSize, (newSize) => {
191
+ currentPageSize.value = newSize
192
+ })
193
+
194
+ const totalItems = computed(() => {
195
+ if (props.serverSide && props.total !== undefined) return props.total
196
+ return filteredData.value.length
197
+ })
198
+
199
+ const totalPages = computed(() => {
200
+ if (totalItems.value <= 0 || currentPageSize.value <= 0) return 0
201
+ return Math.ceil(totalItems.value / currentPageSize.value)
202
+ })
203
+
204
+ function handlePageChange(page: number) {
205
+ currentPage.value = page
206
+ emit('update:page', page)
207
+ emit('pagination-change', { page, pageSize: currentPageSize.value })
208
+ }
209
+
210
+ function handlePageSizeChange(size: string | number | null) {
211
+ if (size === null) return
212
+ const newSize = typeof size === 'string' ? parseInt(size, 10) : size
213
+ currentPageSize.value = newSize
214
+ currentPage.value = 1
215
+ emit('update:pageSize', newSize)
216
+ emit('update:page', 1)
217
+ emit('pagination-change', { page: 1, pageSize: newSize })
218
+ }
219
+
220
+ const pageSizeSelectOptions = computed(() => {
221
+ return pageSizeOptions.value.map(size => ({
222
+ label: String(size),
223
+ value: size
224
+ }))
225
+ })
226
+
227
+ const paginationStart = computed(() => {
228
+ if (totalItems.value === 0) return 0
229
+ return (currentPage.value - 1) * currentPageSize.value + 1
230
+ })
231
+
232
+ const paginationEnd = computed(() => {
233
+ return Math.min(currentPage.value * currentPageSize.value, totalItems.value)
234
+ })
235
+
236
+ function getRowId(row: Record<string, unknown>): string | number {
237
+ const id = row[props.rowKey]
238
+ if (typeof id === 'string' || typeof id === 'number') {
239
+ return id
240
+ }
241
+ console.warn(`DataTable: Row key "${props.rowKey}" must be string or number`)
242
+ return String(id)
243
+ }
244
+
245
+ function getCellValue(row: Record<string, unknown>, key: string): unknown {
246
+ return getNestedValue(row, key)
247
+ }
248
+
249
+ const allSelected = computed(() => {
250
+ if (props.data.length === 0) return false
251
+ return props.data.every(row => props.selectedIds.includes(getRowId(row)))
252
+ })
253
+
254
+ const someSelected = computed(() => {
255
+ if (props.data.length === 0) return false
256
+ return props.selectedIds.length > 0 && !allSelected.value
257
+ })
258
+
259
+ function toggleAll(checked: boolean) {
260
+ if (checked) {
261
+ const allIds = props.data.map(row => getRowId(row))
262
+ emit('update:selectedIds', allIds)
263
+ } else {
264
+ emit('update:selectedIds', [])
265
+ }
266
+ }
267
+
268
+ function toggleRow(row: Record<string, unknown>, checked: boolean) {
269
+ const id = getRowId(row)
270
+ if (checked) {
271
+ emit('update:selectedIds', [...props.selectedIds, id])
272
+ } else {
273
+ emit('update:selectedIds', props.selectedIds.filter(sid => sid !== id))
274
+ }
275
+ }
276
+
277
+ function isRowSelected(row: Record<string, unknown>): boolean {
278
+ return props.selectedIds.includes(getRowId(row))
279
+ }
280
+
281
+ function handleSort(column: DataTableColumn) {
282
+ if (!column.sortable) return
283
+
284
+ let newDirection: SortDirection
285
+
286
+ if (currentSort.value.key !== column.key) {
287
+ newDirection = 'asc'
288
+ } else {
289
+ const cycle: SortDirection[] = [null, 'asc', 'desc']
290
+ const currentIndex = cycle.indexOf(currentSort.value.direction)
291
+ newDirection = cycle[(currentIndex + 1) % 3]
292
+ }
293
+
294
+ currentSort.value = {
295
+ key: newDirection ? column.key : null,
296
+ direction: newDirection
297
+ }
298
+
299
+ emit('sort-change', currentSort.value)
300
+ }
301
+
302
+ function handleKeydownSort(event: KeyboardEvent, column: DataTableColumn) {
303
+ if (event.key === 'Enter' || event.key === ' ') {
304
+ event.preventDefault()
305
+ handleSort(column)
306
+ }
307
+ }
308
+
309
+ const currentSearch = ref(props.search)
310
+ const currentFilters = ref<Record<string, string>>({ ...props.filters })
311
+
312
+ watch(() => props.search, (newSearch) => {
313
+ currentSearch.value = newSearch
314
+ })
315
+
316
+ watch(() => props.filters, (newFilters) => {
317
+ currentFilters.value = { ...newFilters }
318
+ }, { deep: true })
319
+
320
+ function handleSearchChange(value: string | number | null) {
321
+ const searchValue = value === null ? '' : String(value)
322
+ currentSearch.value = searchValue
323
+ currentPage.value = 1
324
+ emit('update:search', searchValue)
325
+ emit('update:page', 1)
326
+ emit('filter-change', { search: searchValue, filters: currentFilters.value })
327
+ }
328
+
329
+ function clearSearch() {
330
+ handleSearchChange('')
331
+ }
332
+
333
+ function handleFilterChange(columnKey: string, value: string) {
334
+ const newFilters = { ...currentFilters.value, [columnKey]: value }
335
+ if (!value) {
336
+ delete newFilters[columnKey]
337
+ }
338
+ currentFilters.value = newFilters
339
+ currentPage.value = 1
340
+ emit('update:filters', newFilters)
341
+ emit('update:page', 1)
342
+ emit('filter-change', { search: currentSearch.value, filters: newFilters })
343
+ }
344
+
345
+ function clearFilters() {
346
+ currentFilters.value = {}
347
+ emit('update:filters', {})
348
+ emit('filter-change', { search: currentSearch.value, filters: {} })
349
+ }
350
+
351
+ function defaultFilterFn(value: unknown, filterValue: string): boolean {
352
+ if (!filterValue) return true
353
+ const stringValue = String(value ?? '').toLowerCase()
354
+ return stringValue.includes(filterValue.toLowerCase())
355
+ }
356
+
357
+ const hasActiveFilters = computed(() => {
358
+ return currentSearch.value !== '' || Object.keys(currentFilters.value).length > 0
359
+ })
360
+
361
+ const filterableColumns = computed(() => {
362
+ if (!props.filterable) return []
363
+ return props.columns.filter(col => col.filterable !== false)
364
+ })
365
+
366
+ const columnWidths = ref<Record<string, number>>({})
367
+ const resizingColumn = ref<string | null>(null)
368
+ const resizeStartX = ref(0)
369
+ const resizeStartWidth = ref(0)
370
+ const tableRef = ref<HTMLElement | null>(null)
371
+
372
+ function getColumnWidth(column: DataTableColumn): string | undefined {
373
+ if (columnWidths.value[column.key]) {
374
+ return `${columnWidths.value[column.key]}px`
375
+ }
376
+ return column.width
377
+ }
378
+
379
+ function startResize(event: MouseEvent, column: DataTableColumn) {
380
+ if (!props.resizable || column.resizable === false) return
381
+
382
+ event.preventDefault()
383
+ resizingColumn.value = column.key
384
+ resizeStartX.value = event.clientX
385
+
386
+ const th = (event.target as HTMLElement).closest('th')
387
+ if (th) {
388
+ resizeStartWidth.value = th.offsetWidth
389
+ }
390
+
391
+ document.addEventListener('mousemove', handleResize)
392
+ document.addEventListener('mouseup', stopResize)
393
+ document.body.style.cursor = 'col-resize'
394
+ document.body.style.userSelect = 'none'
395
+ }
396
+
397
+ function handleResize(event: MouseEvent) {
398
+ if (!resizingColumn.value) return
399
+
400
+ const column = props.columns.find(c => c.key === resizingColumn.value)
401
+ const minWidth = column?.minWidth ? parseInt(column.minWidth) : 50
402
+ const delta = event.clientX - resizeStartX.value
403
+ const newWidth = Math.max(minWidth, resizeStartWidth.value + delta)
404
+
405
+ columnWidths.value = {
406
+ ...columnWidths.value,
407
+ [resizingColumn.value]: newWidth
408
+ }
409
+ }
410
+
411
+ function stopResize() {
412
+ if (resizingColumn.value) {
413
+ emit('column-resize', resizingColumn.value, columnWidths.value[resizingColumn.value])
414
+ }
415
+ resizingColumn.value = null
416
+ document.removeEventListener('mousemove', handleResize)
417
+ document.removeEventListener('mouseup', stopResize)
418
+ document.body.style.cursor = ''
419
+ document.body.style.userSelect = ''
420
+ }
421
+
422
+ onUnmounted(() => {
423
+ document.removeEventListener('mousemove', handleResize)
424
+ document.removeEventListener('mouseup', stopResize)
425
+ })
426
+
427
+ function isRowExpanded(row: Record<string, unknown>): boolean {
428
+ return props.expandedIds.includes(getRowId(row))
429
+ }
430
+
431
+ function toggleRowExpand(row: Record<string, unknown>) {
432
+ const id = getRowId(row)
433
+ const isExpanded = props.expandedIds.includes(id)
434
+
435
+ if (isExpanded) {
436
+ emit('update:expandedIds', props.expandedIds.filter(eid => eid !== id))
437
+ } else {
438
+ emit('update:expandedIds', [...props.expandedIds, id])
439
+ }
440
+
441
+ emit('row-expand', row, !isExpanded)
442
+ }
443
+
444
+ function getTypePrecedence(val: unknown): number {
445
+ if (val == null) return 4
446
+ if (typeof val === 'number') return 1
447
+ if (typeof val === 'string') return 2
448
+ if (typeof val === 'boolean') return 3
449
+ return 5
450
+ }
451
+
452
+ const filteredData = computed(() => {
453
+ if (props.serverSide) return props.data
454
+
455
+ let result = [...props.data]
456
+
457
+ if (currentSearch.value) {
458
+ const searchLower = currentSearch.value.toLowerCase()
459
+ result = result.filter(row => {
460
+ return props.columns.some(column => {
461
+ const value = getCellValue(row, column.key)
462
+ return String(value ?? '').toLowerCase().includes(searchLower)
463
+ })
464
+ })
465
+ }
466
+
467
+ if (Object.keys(currentFilters.value).length > 0) {
468
+ result = result.filter(row => {
469
+ return Object.entries(currentFilters.value).every(([columnKey, filterValue]) => {
470
+ if (!filterValue) return true
471
+ const column = props.columns.find(c => c.key === columnKey)
472
+ const value = getCellValue(row, columnKey)
473
+ const filterFn = column?.filterFn || defaultFilterFn
474
+ return filterFn(value, filterValue, row)
475
+ })
476
+ })
477
+ }
478
+
479
+ return result
480
+ })
481
+
482
+ const sortedData = computed(() => {
483
+ if (!currentSort.value.key || !currentSort.value.direction) {
484
+ return filteredData.value
485
+ }
486
+
487
+ const key = currentSort.value.key
488
+ const dir = currentSort.value.direction === 'asc' ? 1 : -1
489
+
490
+ return [...filteredData.value].sort((a, b) => {
491
+ const aVal = getCellValue(a, key)
492
+ const bVal = getCellValue(b, key)
493
+
494
+ const aType = getTypePrecedence(aVal)
495
+ const bType = getTypePrecedence(bVal)
496
+
497
+ if (aType !== bType) {
498
+ return (aType - bType) * dir
499
+ }
500
+
501
+ if (aVal == null && bVal == null) return 0
502
+
503
+ if (typeof aVal === 'string' && typeof bVal === 'string') {
504
+ return aVal.localeCompare(bVal) * dir
505
+ }
506
+
507
+ if (typeof aVal === 'number' && typeof bVal === 'number') {
508
+ return (aVal - bVal) * dir
509
+ }
510
+
511
+ if (typeof aVal === 'boolean' && typeof bVal === 'boolean') {
512
+ return (aVal === bVal ? 0 : aVal ? -1 : 1) * dir
513
+ }
514
+
515
+ return (String(aVal) < String(bVal) ? -1 : String(aVal) > String(bVal) ? 1 : 0) * dir
516
+ })
517
+ })
518
+
519
+ const paginatedData = computed(() => {
520
+ if (!isPaginationEnabled.value || props.serverSide) {
521
+ return sortedData.value
522
+ }
523
+
524
+ const start = (currentPage.value - 1) * currentPageSize.value
525
+ const end = start + currentPageSize.value
526
+ return sortedData.value.slice(start, end)
527
+ })
528
+
529
+ function handleRowClick(row: Record<string, unknown>, index: number) {
530
+ emit('row-click', row, index)
531
+ }
532
+
533
+ const totalColumns = computed(() => {
534
+ let count = props.columns.length
535
+ if (props.selectable) count++
536
+ if (props.expandable) count++
537
+ return count
538
+ })
539
+
540
+ const filteredTotalItems = computed(() => {
541
+ if (props.serverSide && props.total !== undefined) return props.total
542
+ return filteredData.value.length
543
+ })
544
+
545
+ const skeletonArray = computed(() => Array.from({ length: props.skeletonRows }))
546
+ </script>
547
+
548
+ <template>
549
+ <div
550
+ ref="tableRef"
551
+ class="ui-table"
552
+ :class="[
553
+ `ui-table--${size}`,
554
+ {
555
+ 'ui-table--striped': striped,
556
+ 'ui-table--hoverable': hoverable,
557
+ 'ui-table--bordered': bordered,
558
+ 'ui-table--loading': loading,
559
+ 'ui-table--sticky-header': stickyHeader,
560
+ 'ui-table--resizable': resizable,
561
+ 'ui-table--resizing': resizingColumn !== null
562
+ }
563
+ ]"
564
+ >
565
+ <div v-if="searchable" class="ui-table__toolbar">
566
+ <div class="ui-table__search">
567
+ <Input
568
+ :model-value="currentSearch"
569
+ :placeholder="searchPlaceholder"
570
+ size="sm"
571
+ type="search"
572
+ @update:model-value="handleSearchChange"
573
+ >
574
+ <template #left>
575
+ <component :is="SearchIcon" class="ui-table__search-icon" />
576
+ </template>
577
+ <template v-if="currentSearch" #right>
578
+ <button
579
+ type="button"
580
+ class="ui-table__search-clear"
581
+ aria-label="Clear search"
582
+ @click="clearSearch"
583
+ >
584
+ <component :is="ClearIcon" class="ui-table__search-clear-icon" />
585
+ </button>
586
+ </template>
587
+ </Input>
588
+ </div>
589
+ <slot name="toolbar" />
590
+ </div>
591
+
592
+ <div class="ui-table__wrapper">
593
+ <table class="ui-table__table" :aria-busy="loading || undefined">
594
+ <thead class="ui-table__header">
595
+ <tr class="ui-table__header-row">
596
+ <th
597
+ v-if="expandable"
598
+ class="ui-table__header-cell ui-table__header-cell--expand"
599
+ >
600
+ <span class="ui-table__header-text sr-only">Expand</span>
601
+ </th>
602
+ <th
603
+ v-if="selectable"
604
+ class="ui-table__header-cell ui-table__header-cell--checkbox"
605
+ >
606
+ <Checkbox
607
+ :model-value="allSelected"
608
+ :indeterminate="someSelected"
609
+ size="sm"
610
+ aria-label="Select all rows"
611
+ @update:model-value="toggleAll"
612
+ />
613
+ </th>
614
+ <th
615
+ v-for="(column, colIndex) in columns"
616
+ :key="column.key"
617
+ class="ui-table__header-cell"
618
+ :class="[
619
+ `ui-table__header-cell--${column.align || 'left'}`,
620
+ {
621
+ 'ui-table__header-cell--sortable': column.sortable,
622
+ 'ui-table__header-cell--sorted': currentSort.key === column.key,
623
+ 'ui-table__header-cell--resizable': resizable && column.resizable !== false
624
+ }
625
+ ]"
626
+ :style="getColumnWidth(column) ? { width: getColumnWidth(column) } : undefined"
627
+ :aria-sort="
628
+ column.sortable
629
+ ? currentSort.key === column.key
630
+ ? currentSort.direction === 'asc'
631
+ ? 'ascending'
632
+ : currentSort.direction === 'desc'
633
+ ? 'descending'
634
+ : 'none'
635
+ : 'none'
636
+ : undefined
637
+ "
638
+ :tabindex="column.sortable ? 0 : undefined"
639
+ :role="column.sortable ? 'button' : undefined"
640
+ @click="handleSort(column)"
641
+ @keydown="handleKeydownSort($event, column)"
642
+ >
643
+ <div class="ui-table__header-content">
644
+ <slot :name="`header-${column.key}`" :column="column">
645
+ <span class="ui-table__header-text">{{ column.label }}</span>
646
+ </slot>
647
+ <span
648
+ v-if="column.sortable"
649
+ class="ui-table__sort-icon"
650
+ :class="{
651
+ 'ui-table__sort-icon--asc': currentSort.key === column.key && currentSort.direction === 'asc',
652
+ 'ui-table__sort-icon--desc': currentSort.key === column.key && currentSort.direction === 'desc'
653
+ }"
654
+ aria-hidden="true"
655
+ >
656
+ <component
657
+ v-if="currentSort.key !== column.key || !currentSort.direction"
658
+ :is="SortNeutralIcon"
659
+ class="ui-table__sort-svg ui-table__sort-svg--neutral"
660
+ />
661
+ <component
662
+ v-else-if="currentSort.direction === 'asc'"
663
+ :is="SortAscIcon"
664
+ class="ui-table__sort-svg"
665
+ />
666
+ <component
667
+ v-else
668
+ :is="SortDescIcon"
669
+ class="ui-table__sort-svg"
670
+ />
671
+ </span>
672
+ </div>
673
+ <div
674
+ v-if="resizable && column.resizable !== false && colIndex < columns.length - 1"
675
+ class="ui-table__resize-handle"
676
+ @mousedown="startResize($event, column)"
677
+ />
678
+ </th>
679
+ </tr>
680
+ <tr v-if="filterable && filterableColumns.length > 0" class="ui-table__filter-row">
681
+ <th v-if="expandable" class="ui-table__filter-cell" />
682
+ <th v-if="selectable" class="ui-table__filter-cell" />
683
+ <th
684
+ v-for="column in columns"
685
+ :key="`filter-${column.key}`"
686
+ class="ui-table__filter-cell"
687
+ >
688
+ <Input
689
+ v-if="column.filterable !== false"
690
+ :model-value="currentFilters[column.key] || ''"
691
+ :placeholder="`Filter ${column.label}...`"
692
+ size="sm"
693
+ @update:model-value="handleFilterChange(column.key, String($event ?? ''))"
694
+ />
695
+ </th>
696
+ </tr>
697
+ </thead>
698
+
699
+ <tbody class="ui-table__body">
700
+ <template v-if="loading">
701
+ <tr
702
+ v-for="(_, skeletonIndex) in skeletonArray"
703
+ :key="`skeleton-${skeletonIndex}`"
704
+ class="ui-table__row ui-table__row--skeleton"
705
+ >
706
+ <td v-if="expandable" class="ui-table__cell ui-table__cell--expand">
707
+ <Skeleton variant="rect" :width="16" :height="16" />
708
+ </td>
709
+ <td v-if="selectable" class="ui-table__cell ui-table__cell--checkbox">
710
+ <Skeleton variant="rect" :width="16" :height="16" />
711
+ </td>
712
+ <td
713
+ v-for="(column, colIndex) in columns"
714
+ :key="column.key"
715
+ class="ui-table__cell"
716
+ :class="[`ui-table__cell--${column.align || 'left'}`]"
717
+ >
718
+ <Skeleton
719
+ variant="text"
720
+ :width="getSkeletonWidth(skeletonIndex, colIndex)"
721
+ :height="16"
722
+ />
723
+ </td>
724
+ </tr>
725
+ </template>
726
+
727
+ <template v-else-if="paginatedData.length === 0">
728
+ <tr class="ui-table__row ui-table__row--empty">
729
+ <td :colspan="totalColumns" class="ui-table__cell ui-table__cell--empty">
730
+ <slot name="empty">
731
+ <EmptyState
732
+ :title="hasActiveFilters ? 'No matching results' : emptyMessage"
733
+ :description="hasActiveFilters ? 'Try adjusting your search or filters' : undefined"
734
+ compact
735
+ />
736
+ </slot>
737
+ </td>
738
+ </tr>
739
+ </template>
740
+
741
+ <template v-else>
742
+ <template v-for="(row, rowIndex) in paginatedData" :key="getRowId(row)">
743
+ <tr
744
+ class="ui-table__row"
745
+ :class="{
746
+ 'ui-table__row--selected': isRowSelected(row),
747
+ 'ui-table__row--clickable': $attrs.onRowClick,
748
+ 'ui-table__row--expanded': expandable && isRowExpanded(row)
749
+ }"
750
+ @click="handleRowClick(row, rowIndex)"
751
+ >
752
+ <td
753
+ v-if="expandable"
754
+ class="ui-table__cell ui-table__cell--expand"
755
+ @click.stop
756
+ >
757
+ <button
758
+ type="button"
759
+ class="ui-table__expand-button"
760
+ :class="{ 'ui-table__expand-button--expanded': isRowExpanded(row) }"
761
+ :aria-expanded="isRowExpanded(row)"
762
+ :aria-label="isRowExpanded(row) ? 'Collapse row' : 'Expand row'"
763
+ @click="toggleRowExpand(row)"
764
+ >
765
+ <component
766
+ :is="isRowExpanded(row) ? CollapseIcon : ExpandIcon"
767
+ class="ui-table__expand-icon"
768
+ />
769
+ </button>
770
+ </td>
771
+ <td
772
+ v-if="selectable"
773
+ class="ui-table__cell ui-table__cell--checkbox"
774
+ @click.stop
775
+ >
776
+ <Checkbox
777
+ :model-value="isRowSelected(row)"
778
+ size="sm"
779
+ :aria-label="`Select row ${rowIndex + 1}`"
780
+ @update:model-value="toggleRow(row, $event)"
781
+ />
782
+ </td>
783
+ <td
784
+ v-for="column in columns"
785
+ :key="column.key"
786
+ class="ui-table__cell"
787
+ :class="[`ui-table__cell--${column.align || 'left'}`]"
788
+ :data-label="column.label"
789
+ :style="getColumnWidth(column) ? { width: getColumnWidth(column) } : undefined"
790
+ >
791
+ <slot
792
+ :name="`cell-${column.key}`"
793
+ :value="getCellValue(row, column.key)"
794
+ :row="row"
795
+ :column="column"
796
+ :index="rowIndex"
797
+ >
798
+ {{ getCellValue(row, column.key) ?? '' }}
799
+ </slot>
800
+ </td>
801
+ </tr>
802
+ <tr
803
+ v-if="expandable && isRowExpanded(row)"
804
+ :key="`expanded-${getRowId(row)}`"
805
+ class="ui-table__row ui-table__row--expansion"
806
+ >
807
+ <td :colspan="totalColumns" class="ui-table__cell ui-table__cell--expansion">
808
+ <slot name="expanded" :row="row" :index="rowIndex">
809
+ <div class="ui-table__expansion-content">
810
+ <pre>{{ JSON.stringify(row, null, 2) }}</pre>
811
+ </div>
812
+ </slot>
813
+ </td>
814
+ </tr>
815
+ </template>
816
+ </template>
817
+ </tbody>
818
+ </table>
819
+ </div>
820
+
821
+ <div v-if="isPaginationEnabled && totalPages > 0" class="ui-table__footer">
822
+ <div class="ui-table__footer-summary">
823
+ <Text size="sm" muted>
824
+ Showing {{ paginationStart }} to {{ paginationEnd }} of {{ totalItems }} results
825
+ </Text>
826
+ </div>
827
+
828
+ <div class="ui-table__footer-controls">
829
+ <div class="ui-table__page-size">
830
+ <Text size="sm" muted as="label" :for="`${uid}-page-size`">
831
+ Rows per page
832
+ </Text>
833
+ <Select
834
+ :id="`${uid}-page-size`"
835
+ :model-value="currentPageSize"
836
+ :options="pageSizeSelectOptions"
837
+ size="sm"
838
+ @update:model-value="handlePageSizeChange"
839
+ />
840
+ </div>
841
+
842
+ <Pagination
843
+ :model-value="currentPage"
844
+ :total="totalItems"
845
+ :page-size="currentPageSize"
846
+ :sibling-count="paginationConfig.siblingCount ?? 1"
847
+ :show-edges="paginationConfig.showEdges ?? true"
848
+ :size="size === 'lg' ? 'lg' : size === 'sm' ? 'sm' : 'md'"
849
+ @update:model-value="handlePageChange"
850
+ />
851
+ </div>
852
+ </div>
853
+ </div>
854
+ </template>
855
+
856
+ <style scoped>
857
+ .ui-table {
858
+ font-family: var(--font-sans);
859
+ width: 100%;
860
+ }
861
+
862
+ .ui-table__wrapper {
863
+ overflow-x: auto;
864
+ border: 1px solid var(--table-border);
865
+ border-radius: var(--radius-lg);
866
+ background: var(--table-bg);
867
+ }
868
+
869
+ .ui-table__table {
870
+ width: 100%;
871
+ border-collapse: collapse;
872
+ table-layout: auto;
873
+ }
874
+
875
+ .ui-table__header {
876
+ background: var(--table-header-bg);
877
+ }
878
+
879
+ .ui-table__header-row {
880
+ border-bottom: 1px solid var(--table-border);
881
+ }
882
+
883
+ .ui-table__header-cell {
884
+ padding: var(--space-3) var(--space-4);
885
+ text-align: left;
886
+ font-size: var(--text-sm);
887
+ font-weight: var(--font-medium);
888
+ color: var(--table-header-text);
889
+ white-space: nowrap;
890
+ user-select: none;
891
+ }
892
+
893
+ .ui-table__header-cell--center {
894
+ text-align: center;
895
+ }
896
+
897
+ .ui-table__header-cell--right {
898
+ text-align: right;
899
+ }
900
+
901
+ .ui-table__header-cell--checkbox {
902
+ width: 48px;
903
+ text-align: center;
904
+ }
905
+
906
+ .ui-table__header-cell--sortable {
907
+ cursor: pointer;
908
+ transition: color var(--duration-fast) var(--ease-default);
909
+ }
910
+
911
+ .ui-table__header-cell--sortable:hover {
912
+ color: var(--table-sort-icon-active);
913
+ }
914
+
915
+ .ui-table__header-cell--sortable:focus-visible {
916
+ outline: 2px solid var(--ring-color);
917
+ outline-offset: -2px;
918
+ }
919
+
920
+ .ui-table__header-content {
921
+ display: inline-flex;
922
+ align-items: center;
923
+ gap: var(--space-1);
924
+ }
925
+
926
+ .ui-table__header-cell--center .ui-table__header-content {
927
+ justify-content: center;
928
+ }
929
+
930
+ .ui-table__header-cell--right .ui-table__header-content {
931
+ justify-content: flex-end;
932
+ }
933
+
934
+ .ui-table__sort-icon {
935
+ display: inline-flex;
936
+ align-items: center;
937
+ justify-content: center;
938
+ width: 12px;
939
+ height: 12px;
940
+ color: var(--table-sort-icon);
941
+ transition: color var(--duration-fast) var(--ease-default);
942
+ }
943
+
944
+ .ui-table__sort-svg {
945
+ width: 100%;
946
+ height: 100%;
947
+ }
948
+
949
+ .ui-table__sort-svg--neutral {
950
+ opacity: 0.5;
951
+ }
952
+
953
+ .ui-table__sort-icon--asc,
954
+ .ui-table__sort-icon--desc {
955
+ color: var(--table-sort-icon-active);
956
+ }
957
+
958
+ .ui-table__sort-icon--asc .ui-table__sort-svg--neutral,
959
+ .ui-table__sort-icon--desc .ui-table__sort-svg--neutral {
960
+ opacity: 1;
961
+ }
962
+
963
+ .ui-table__header-cell--sortable:hover .ui-table__sort-icon {
964
+ color: var(--table-sort-icon-active);
965
+ }
966
+
967
+ .ui-table__header-cell--sortable:hover .ui-table__sort-svg--neutral {
968
+ opacity: 1;
969
+ }
970
+
971
+ .ui-table__body {
972
+ background: var(--table-bg);
973
+ }
974
+
975
+ .ui-table__row {
976
+ border-bottom: 1px solid var(--table-border);
977
+ transition: background-color var(--duration-fast) var(--ease-default);
978
+ }
979
+
980
+ .ui-table__row:last-child {
981
+ border-bottom: none;
982
+ }
983
+
984
+ .ui-table--hoverable .ui-table__row:not(.ui-table__row--empty):not(.ui-table__row--skeleton):hover {
985
+ background-color: var(--table-row-hover);
986
+ }
987
+
988
+ .ui-table__row--selected {
989
+ background-color: var(--table-row-selected);
990
+ }
991
+
992
+ .ui-table--hoverable .ui-table__row--selected:hover {
993
+ background-color: var(--table-row-selected);
994
+ }
995
+
996
+ .ui-table__row--clickable {
997
+ cursor: pointer;
998
+ }
999
+
1000
+ .ui-table--striped .ui-table__row:nth-child(even):not(.ui-table__row--selected) {
1001
+ background-color: var(--table-row-striped);
1002
+ }
1003
+
1004
+ .ui-table__cell {
1005
+ padding: var(--space-3) var(--space-4);
1006
+ font-size: var(--text-sm);
1007
+ color: var(--table-cell-text);
1008
+ vertical-align: middle;
1009
+ }
1010
+
1011
+ .ui-table__cell--center {
1012
+ text-align: center;
1013
+ }
1014
+
1015
+ .ui-table__cell--right {
1016
+ text-align: right;
1017
+ }
1018
+
1019
+ .ui-table__cell--checkbox {
1020
+ width: 48px;
1021
+ text-align: center;
1022
+ }
1023
+
1024
+ .ui-table__cell--empty {
1025
+ padding: 0;
1026
+ text-align: center;
1027
+ }
1028
+
1029
+ .ui-table--bordered .ui-table__cell {
1030
+ border-right: 1px solid var(--table-border);
1031
+ }
1032
+
1033
+ .ui-table--bordered .ui-table__cell:last-child {
1034
+ border-right: none;
1035
+ }
1036
+
1037
+ .ui-table--bordered .ui-table__header-cell {
1038
+ border-right: 1px solid var(--table-border);
1039
+ }
1040
+
1041
+ .ui-table--bordered .ui-table__header-cell:last-child {
1042
+ border-right: none;
1043
+ }
1044
+
1045
+ .ui-table--sm .ui-table__header-cell,
1046
+ .ui-table--sm .ui-table__cell {
1047
+ padding: var(--space-2) var(--space-3);
1048
+ font-size: var(--text-xs);
1049
+ }
1050
+
1051
+ .ui-table--lg .ui-table__header-cell,
1052
+ .ui-table--lg .ui-table__cell {
1053
+ padding: var(--space-4) var(--space-5);
1054
+ font-size: var(--text-md);
1055
+ }
1056
+
1057
+ .ui-table--sticky-header .ui-table__wrapper {
1058
+ max-height: 400px;
1059
+ overflow-y: auto;
1060
+ }
1061
+
1062
+ .ui-table--sticky-header .ui-table__header {
1063
+ position: sticky;
1064
+ top: 0;
1065
+ z-index: 1;
1066
+ }
1067
+
1068
+ @media (max-width: 768px) {
1069
+ .ui-table__wrapper {
1070
+ border: none;
1071
+ border-radius: 0;
1072
+ background: transparent;
1073
+ }
1074
+
1075
+ .ui-table__header {
1076
+ display: none;
1077
+ }
1078
+
1079
+ .ui-table__body {
1080
+ display: flex;
1081
+ flex-direction: column;
1082
+ gap: var(--space-3);
1083
+ }
1084
+
1085
+ .ui-table__row {
1086
+ display: flex;
1087
+ flex-direction: column;
1088
+ border: 1px solid var(--table-border);
1089
+ border-radius: var(--radius-lg);
1090
+ background: var(--table-bg);
1091
+ padding: var(--space-3);
1092
+ }
1093
+
1094
+ .ui-table__row--empty {
1095
+ padding: var(--space-6);
1096
+ }
1097
+
1098
+ .ui-table__row--skeleton {
1099
+ min-height: 120px;
1100
+ }
1101
+
1102
+ .ui-table__cell {
1103
+ display: flex;
1104
+ justify-content: space-between;
1105
+ align-items: center;
1106
+ padding: var(--space-2) 0;
1107
+ border-bottom: 1px solid var(--table-border);
1108
+ }
1109
+
1110
+ .ui-table__cell:last-child {
1111
+ border-bottom: none;
1112
+ }
1113
+
1114
+ .ui-table__cell::before {
1115
+ content: attr(data-label);
1116
+ font-weight: var(--font-medium);
1117
+ color: var(--table-header-text);
1118
+ margin-right: var(--space-4);
1119
+ flex-shrink: 0;
1120
+ }
1121
+
1122
+ .ui-table__cell--checkbox {
1123
+ justify-content: flex-start;
1124
+ border-bottom: 1px solid var(--table-border);
1125
+ }
1126
+
1127
+ .ui-table__cell--checkbox::before {
1128
+ content: 'Select';
1129
+ }
1130
+
1131
+ .ui-table__cell--empty {
1132
+ border-bottom: none;
1133
+ }
1134
+
1135
+ .ui-table__cell--empty::before {
1136
+ display: none;
1137
+ }
1138
+
1139
+ .ui-table--bordered .ui-table__cell {
1140
+ border-right: none;
1141
+ }
1142
+
1143
+ .ui-table__footer {
1144
+ flex-direction: column;
1145
+ gap: var(--space-3);
1146
+ padding: var(--space-3);
1147
+ }
1148
+
1149
+ .ui-table__footer-controls {
1150
+ flex-direction: column;
1151
+ gap: var(--space-3);
1152
+ }
1153
+
1154
+ .ui-table__page-size {
1155
+ justify-content: space-between;
1156
+ width: 100%;
1157
+ }
1158
+ }
1159
+
1160
+ .ui-table__footer {
1161
+ display: flex;
1162
+ align-items: center;
1163
+ justify-content: space-between;
1164
+ gap: var(--space-4);
1165
+ padding: var(--space-3) var(--space-4);
1166
+ border: 1px solid var(--table-border);
1167
+ border-top: none;
1168
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
1169
+ background: var(--table-bg);
1170
+ flex-wrap: wrap;
1171
+ }
1172
+
1173
+ .ui-table__footer-summary {
1174
+ flex-shrink: 0;
1175
+ }
1176
+
1177
+ .ui-table__footer-controls {
1178
+ display: flex;
1179
+ align-items: center;
1180
+ gap: var(--space-4);
1181
+ flex-wrap: wrap;
1182
+ }
1183
+
1184
+ .ui-table__page-size {
1185
+ display: flex;
1186
+ align-items: center;
1187
+ gap: var(--space-2);
1188
+ }
1189
+
1190
+ .ui-table__page-size :deep(.ui-select) {
1191
+ min-width: 70px;
1192
+ }
1193
+
1194
+ .ui-table__toolbar {
1195
+ display: flex;
1196
+ align-items: center;
1197
+ gap: var(--space-3);
1198
+ padding: var(--space-3) var(--space-4);
1199
+ border: 1px solid var(--table-border);
1200
+ border-bottom: none;
1201
+ border-radius: var(--radius-lg) var(--radius-lg) 0 0;
1202
+ background: var(--table-bg);
1203
+ }
1204
+
1205
+ .ui-table__toolbar + .ui-table__wrapper {
1206
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
1207
+ }
1208
+
1209
+ .ui-table__toolbar + .ui-table__wrapper + .ui-table__footer {
1210
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
1211
+ }
1212
+
1213
+ .ui-table__search {
1214
+ flex: 1;
1215
+ max-width: 300px;
1216
+ }
1217
+
1218
+ .ui-table__search-icon {
1219
+ width: 16px;
1220
+ height: 16px;
1221
+ color: var(--foreground-muted);
1222
+ }
1223
+
1224
+ .ui-table__search-clear {
1225
+ display: flex;
1226
+ align-items: center;
1227
+ justify-content: center;
1228
+ padding: 0;
1229
+ border: none;
1230
+ background: none;
1231
+ cursor: pointer;
1232
+ color: var(--foreground-muted);
1233
+ transition: color var(--duration-fast) var(--ease-default);
1234
+ }
1235
+
1236
+ .ui-table__search-clear:hover {
1237
+ color: var(--foreground);
1238
+ }
1239
+
1240
+ .ui-table__search-clear-icon {
1241
+ width: 14px;
1242
+ height: 14px;
1243
+ }
1244
+
1245
+ .ui-table__filter-row {
1246
+ border-bottom: 1px solid var(--table-border);
1247
+ background: var(--table-header-bg);
1248
+ }
1249
+
1250
+ .ui-table__filter-cell {
1251
+ padding: var(--space-2) var(--space-4);
1252
+ vertical-align: middle;
1253
+ }
1254
+
1255
+ .ui-table__filter-cell :deep(.ui-input) {
1256
+ min-width: 0;
1257
+ }
1258
+
1259
+ .ui-table__header-cell--expand,
1260
+ .ui-table__cell--expand {
1261
+ width: 48px;
1262
+ text-align: center;
1263
+ }
1264
+
1265
+ .ui-table__expand-button {
1266
+ display: inline-flex;
1267
+ align-items: center;
1268
+ justify-content: center;
1269
+ width: 24px;
1270
+ height: 24px;
1271
+ padding: 0;
1272
+ border: none;
1273
+ border-radius: var(--radius-sm);
1274
+ background: transparent;
1275
+ color: var(--foreground-muted);
1276
+ cursor: pointer;
1277
+ transition: all var(--duration-fast) var(--ease-default);
1278
+ }
1279
+
1280
+ .ui-table__expand-button:hover {
1281
+ background: var(--ghost-hover);
1282
+ color: var(--foreground);
1283
+ }
1284
+
1285
+ .ui-table__expand-button:focus-visible {
1286
+ outline: 2px solid var(--ring-color);
1287
+ outline-offset: 2px;
1288
+ }
1289
+
1290
+ .ui-table__expand-icon {
1291
+ width: 16px;
1292
+ height: 16px;
1293
+ transition: transform var(--duration-fast) var(--ease-default);
1294
+ }
1295
+
1296
+ .ui-table__expand-button--expanded .ui-table__expand-icon {
1297
+ transform: rotate(90deg);
1298
+ }
1299
+
1300
+ .ui-table__row--expanded {
1301
+ background-color: var(--subtle);
1302
+ }
1303
+
1304
+ .ui-table__row--expansion {
1305
+ background-color: var(--subtle);
1306
+ }
1307
+
1308
+ .ui-table--hoverable .ui-table__row--expansion:hover {
1309
+ background-color: var(--subtle);
1310
+ }
1311
+
1312
+ .ui-table__cell--expansion {
1313
+ padding: var(--space-4);
1314
+ }
1315
+
1316
+ .ui-table__expansion-content {
1317
+ padding: var(--space-2);
1318
+ background: var(--surface);
1319
+ border-radius: var(--radius-md);
1320
+ font-size: var(--text-sm);
1321
+ }
1322
+
1323
+ .ui-table__expansion-content pre {
1324
+ margin: 0;
1325
+ font-family: var(--font-mono);
1326
+ font-size: var(--text-xs);
1327
+ white-space: pre-wrap;
1328
+ word-break: break-word;
1329
+ }
1330
+
1331
+ .ui-table__header-cell--resizable {
1332
+ position: relative;
1333
+ }
1334
+
1335
+ .ui-table__resize-handle {
1336
+ position: absolute;
1337
+ top: 0;
1338
+ right: 0;
1339
+ bottom: 0;
1340
+ width: 4px;
1341
+ cursor: col-resize;
1342
+ background: transparent;
1343
+ transition: background-color var(--duration-fast) var(--ease-default);
1344
+ }
1345
+
1346
+ .ui-table__resize-handle:hover {
1347
+ background: var(--primary);
1348
+ }
1349
+
1350
+ .ui-table--resizing .ui-table__resize-handle {
1351
+ background: var(--primary);
1352
+ }
1353
+
1354
+ .ui-table--resizable .ui-table__table {
1355
+ table-layout: fixed;
1356
+ }
1357
+
1358
+ .sr-only {
1359
+ position: absolute;
1360
+ width: 1px;
1361
+ height: 1px;
1362
+ padding: 0;
1363
+ margin: -1px;
1364
+ overflow: hidden;
1365
+ clip: rect(0, 0, 0, 0);
1366
+ white-space: nowrap;
1367
+ border-width: 0;
1368
+ }
1369
+
1370
+ @media (max-width: 768px) {
1371
+ .ui-table__toolbar {
1372
+ flex-direction: column;
1373
+ align-items: stretch;
1374
+ gap: var(--space-2);
1375
+ border-radius: var(--radius-lg);
1376
+ border: 1px solid var(--table-border);
1377
+ margin-bottom: var(--space-3);
1378
+ }
1379
+
1380
+ .ui-table__toolbar + .ui-table__wrapper {
1381
+ border-radius: 0;
1382
+ }
1383
+
1384
+ .ui-table__search {
1385
+ max-width: none;
1386
+ }
1387
+
1388
+ .ui-table__filter-row {
1389
+ display: none;
1390
+ }
1391
+
1392
+ .ui-table__cell--expand {
1393
+ justify-content: flex-start;
1394
+ }
1395
+
1396
+ .ui-table__cell--expand::before {
1397
+ content: 'Expand';
1398
+ }
1399
+
1400
+ .ui-table__row--expansion {
1401
+ margin-top: calc(-1 * var(--space-3));
1402
+ padding-top: 0;
1403
+ border-top: none;
1404
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
1405
+ }
1406
+
1407
+ .ui-table__cell--expansion {
1408
+ padding: var(--space-3);
1409
+ }
1410
+
1411
+ .ui-table__cell--expansion::before {
1412
+ display: none;
1413
+ }
1414
+ }
1415
+ </style>