@inglorious/ui 0.1.0 → 1.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 (283) hide show
  1. package/README.md +45 -0
  2. package/package.json +190 -15
  3. package/src/all.css +7 -0
  4. package/src/controls/button/button.stories.js +35 -15
  5. package/src/controls/button/index.js +1 -3
  6. package/src/controls/button/style.css +80 -37
  7. package/src/controls/button/template.js +23 -26
  8. package/src/controls/button-group/button-group.stories.js +19 -15
  9. package/src/controls/button-group/index.js +1 -2
  10. package/src/controls/button-group/style.css +0 -7
  11. package/src/controls/button-group/template.js +46 -56
  12. package/src/controls/checkbox/checkbox.stories.js +16 -13
  13. package/src/controls/checkbox/index.js +1 -2
  14. package/src/controls/checkbox/style.css +23 -2
  15. package/src/controls/checkbox/template.js +20 -18
  16. package/src/controls/combobox/combobox.stories.js +92 -0
  17. package/src/controls/{select → combobox}/handlers.js +8 -5
  18. package/src/controls/combobox/helpers.js +91 -0
  19. package/src/controls/combobox/index.js +43 -0
  20. package/src/controls/combobox/style.css +203 -0
  21. package/src/controls/combobox/template.js +340 -0
  22. package/src/controls/controls.css +12 -0
  23. package/src/controls/fab/fab.stories.js +11 -11
  24. package/src/controls/fab/index.js +1 -2
  25. package/src/controls/fab/template.js +21 -19
  26. package/src/controls/icon-button/icon-button.stories.js +8 -10
  27. package/src/controls/icon-button/index.js +1 -2
  28. package/src/controls/icon-button/template.js +13 -19
  29. package/src/controls/input/index.js +1 -2
  30. package/src/controls/input/input.stories.js +24 -23
  31. package/src/controls/input/style.css +9 -7
  32. package/src/controls/input/template.js +28 -31
  33. package/src/controls/radio-group/index.js +1 -2
  34. package/src/controls/radio-group/radio-group.stories.js +11 -8
  35. package/src/controls/radio-group/style.css +22 -1
  36. package/src/controls/radio-group/template.js +20 -15
  37. package/src/controls/rating/index.js +1 -2
  38. package/src/controls/rating/rating.stories.js +7 -10
  39. package/src/controls/rating/style.css +1 -2
  40. package/src/controls/rating/template.js +20 -17
  41. package/src/controls/select/helpers.js +2 -21
  42. package/src/controls/select/index.js +1 -13
  43. package/src/controls/select/select.stories.js +68 -64
  44. package/src/controls/select/style.css +66 -189
  45. package/src/controls/select/template.js +77 -406
  46. package/src/controls/slider/index.js +1 -2
  47. package/src/controls/slider/slider.stories.js +15 -12
  48. package/src/controls/slider/style.css +24 -3
  49. package/src/controls/slider/template.js +30 -23
  50. package/src/controls/switch/index.js +1 -2
  51. package/src/controls/switch/style.css +23 -2
  52. package/src/controls/switch/switch.stories.js +14 -11
  53. package/src/controls/switch/template.js +18 -16
  54. package/src/data-display/avatar/avatar.stories.js +106 -0
  55. package/src/data-display/avatar/index.js +3 -0
  56. package/src/data-display/avatar/style.css +64 -0
  57. package/src/data-display/avatar/template.js +81 -0
  58. package/src/data-display/badge/badge.stories.js +64 -0
  59. package/src/data-display/badge/index.js +3 -0
  60. package/src/data-display/badge/style.css +84 -0
  61. package/src/data-display/badge/template.js +46 -0
  62. package/src/data-display/chip/chip.stories.js +57 -0
  63. package/src/data-display/chip/index.js +3 -0
  64. package/src/data-display/chip/style.css +84 -0
  65. package/src/data-display/chip/template.js +32 -0
  66. package/src/data-display/data-display.css +11 -0
  67. package/src/data-display/data-grid/base.css +58 -0
  68. package/src/data-display/data-grid/data-grid.stories.js +158 -0
  69. package/src/data-display/data-grid/filters/helpers.js +8 -0
  70. package/src/data-display/data-grid/filters/index.js +21 -0
  71. package/src/data-display/data-grid/filters/input.js +24 -0
  72. package/src/data-display/data-grid/filters/range.js +48 -0
  73. package/src/data-display/data-grid/filters/select.js +17 -0
  74. package/src/data-display/data-grid/handlers.js +523 -0
  75. package/src/data-display/data-grid/helpers.js +248 -0
  76. package/src/data-display/data-grid/index.js +64 -0
  77. package/src/data-display/data-grid/style.css +14 -0
  78. package/src/data-display/data-grid/template.js +458 -0
  79. package/src/data-display/data-grid/theme.css +148 -0
  80. package/src/data-display/divider/divider.stories.js +48 -0
  81. package/src/data-display/divider/index.js +3 -0
  82. package/src/data-display/divider/style.css +18 -0
  83. package/src/data-display/divider/template.js +17 -0
  84. package/src/data-display/icon/icon-implementations.mdx +160 -0
  85. package/src/data-display/icon/icon-utf8.stories.js +156 -0
  86. package/src/data-display/icon/icon.stories.js +38 -0
  87. package/src/data-display/icon/index.js +3 -0
  88. package/src/data-display/icon/style.css +17 -0
  89. package/src/data-display/icon/template.js +19 -0
  90. package/src/data-display/list/handlers.js +25 -0
  91. package/src/data-display/list/index.js +4 -0
  92. package/src/data-display/list/list.stories.js +187 -0
  93. package/src/data-display/list/style.css +119 -0
  94. package/src/data-display/list/template.js +302 -0
  95. package/src/data-display/material-icon/index.js +3 -0
  96. package/src/data-display/material-icon/material-icon.stories.js +99 -0
  97. package/src/data-display/material-icon/style.css +27 -0
  98. package/src/data-display/material-icon/template.js +21 -0
  99. package/src/data-display/table/index.js +1 -0
  100. package/src/data-display/table/style.css +42 -0
  101. package/src/data-display/table/table.stories.js +40 -0
  102. package/src/data-display/table/template.js +114 -0
  103. package/src/data-display/tooltip/index.js +3 -0
  104. package/src/data-display/tooltip/style.css +57 -0
  105. package/src/data-display/tooltip/template.js +58 -0
  106. package/src/data-display/tooltip/tooltip.stories.js +44 -0
  107. package/src/data-display/typography/index.js +3 -0
  108. package/src/data-display/typography/style.css +49 -0
  109. package/src/data-display/typography/template.js +110 -0
  110. package/src/data-display/typography/typography.stories.js +48 -0
  111. package/src/data-display/virtual-list/handlers.js +68 -0
  112. package/src/data-display/virtual-list/index.js +4 -0
  113. package/src/data-display/virtual-list/template.js +101 -0
  114. package/src/examples/dashboard/app-drawer.js +255 -0
  115. package/src/examples/dashboard/app-header.js +149 -0
  116. package/src/examples/dashboard/dashboard.js +85 -0
  117. package/src/examples/dashboard/dashboard.stories.js +47 -0
  118. package/src/examples/dashboard/data.js +205 -0
  119. package/src/examples/dashboard/mode.js +27 -0
  120. package/src/examples/dashboard/primitive-content.js +1325 -0
  121. package/src/examples/dashboard/primitive-section.js +158 -0
  122. package/src/examples/dashboard/router.js +13 -0
  123. package/src/examples/dashboard/social-card.js +73 -0
  124. package/src/examples/dashboard/stat-card.js +84 -0
  125. package/src/examples/dashboard/style.css +339 -0
  126. package/src/examples/dashboard/theme-utils.js +95 -0
  127. package/src/examples/dashboard/theme.js +27 -0
  128. package/src/examples/dashboard/traffic-card.js +108 -0
  129. package/src/examples/dashboard/traffic-sales-card.js +267 -0
  130. package/src/feedback/alert/alert.stories.js +70 -0
  131. package/src/feedback/alert/index.js +3 -0
  132. package/src/feedback/alert/style.css +98 -0
  133. package/src/feedback/alert/template.js +74 -0
  134. package/src/feedback/backdrop/backdrop.stories.js +21 -0
  135. package/src/feedback/backdrop/index.js +3 -0
  136. package/src/feedback/backdrop/style.css +25 -0
  137. package/src/feedback/backdrop/template.js +31 -0
  138. package/src/feedback/dialog/dialog.stories.js +27 -0
  139. package/src/feedback/dialog/index.js +3 -0
  140. package/src/feedback/dialog/style.css +68 -0
  141. package/src/feedback/dialog/template.js +64 -0
  142. package/src/feedback/feedback.css +6 -0
  143. package/src/feedback/progress/index.js +3 -0
  144. package/src/feedback/progress/progress.stories.js +54 -0
  145. package/src/feedback/progress/style.css +95 -0
  146. package/src/feedback/progress/template.js +88 -0
  147. package/src/feedback/skeleton/index.js +3 -0
  148. package/src/feedback/skeleton/skeleton.stories.js +44 -0
  149. package/src/feedback/skeleton/style.css +41 -0
  150. package/src/feedback/skeleton/template.js +48 -0
  151. package/src/feedback/snackbar/index.js +3 -0
  152. package/src/feedback/snackbar/snackbar.stories.js +25 -0
  153. package/src/feedback/snackbar/style.css +64 -0
  154. package/src/feedback/snackbar/template.js +55 -0
  155. package/src/index.js +32 -1
  156. package/src/layout/container/container.stories.js +105 -0
  157. package/src/layout/container/index.js +3 -0
  158. package/src/layout/container/style.css +34 -0
  159. package/src/layout/container/template.js +119 -0
  160. package/src/layout/flex/flex.stories.js +28 -10
  161. package/src/layout/flex/index.js +1 -2
  162. package/src/layout/flex/style.css +20 -0
  163. package/src/layout/flex/template.js +40 -13
  164. package/src/layout/grid/grid.stories.js +46 -30
  165. package/src/layout/grid/index.js +1 -2
  166. package/src/layout/grid/style.css +20 -0
  167. package/src/layout/grid/template.js +100 -20
  168. package/src/layout/layout.css +3 -0
  169. package/src/navigation/bottom-navigation/bottom-navigation.stories.js +33 -0
  170. package/src/navigation/bottom-navigation/index.js +1 -0
  171. package/src/navigation/bottom-navigation/style.css +32 -0
  172. package/src/navigation/bottom-navigation/template.js +74 -0
  173. package/src/navigation/breadcrumbs/breadcrumbs.stories.js +33 -0
  174. package/src/navigation/breadcrumbs/index.js +1 -0
  175. package/src/navigation/breadcrumbs/style.css +27 -0
  176. package/src/navigation/breadcrumbs/template.js +79 -0
  177. package/src/navigation/drawer/drawer.stories.js +60 -0
  178. package/src/navigation/drawer/index.js +3 -0
  179. package/src/navigation/drawer/style.css +125 -0
  180. package/src/navigation/drawer/template.js +131 -0
  181. package/src/navigation/link/index.js +3 -0
  182. package/src/navigation/link/link.stories.js +53 -0
  183. package/src/navigation/link/style.css +40 -0
  184. package/src/navigation/link/template.js +53 -0
  185. package/src/navigation/menu/index.js +1 -0
  186. package/src/navigation/menu/menu.stories.js +36 -0
  187. package/src/navigation/menu/style.css +44 -0
  188. package/src/navigation/menu/template.js +81 -0
  189. package/src/navigation/navigation.css +9 -0
  190. package/src/navigation/pagination/index.js +1 -0
  191. package/src/navigation/pagination/pagination.stories.js +61 -0
  192. package/src/navigation/pagination/style.css +29 -0
  193. package/src/navigation/pagination/template.js +140 -0
  194. package/src/navigation/speed-dial/index.js +1 -0
  195. package/src/navigation/speed-dial/speed-dial.stories.js +40 -0
  196. package/src/navigation/speed-dial/style.css +38 -0
  197. package/src/navigation/speed-dial/template.js +68 -0
  198. package/src/navigation/stepper/index.js +1 -0
  199. package/src/navigation/stepper/stepper.stories.js +35 -0
  200. package/src/navigation/stepper/style.css +49 -0
  201. package/src/navigation/stepper/template.js +61 -0
  202. package/src/navigation/tabs/index.js +1 -0
  203. package/src/navigation/tabs/style.css +35 -0
  204. package/src/navigation/tabs/tabs.stories.js +40 -0
  205. package/src/navigation/tabs/template.js +79 -0
  206. package/src/overview.mdx +39 -0
  207. package/src/stories/notifyStory.js +22 -25
  208. package/src/styles/base.css +2 -0
  209. package/src/styles/reset.css +15 -0
  210. package/src/{themes → styles/themes}/bootstrap.css +2 -0
  211. package/src/{themes → styles/themes}/inglorious.css +9 -1
  212. package/src/{themes → styles/themes}/material.css +2 -0
  213. package/src/{tokens/index.css → styles/tokens.css} +9 -0
  214. package/src/surfaces/accordion/accordion.stories.js +60 -0
  215. package/src/surfaces/accordion/index.js +1 -0
  216. package/src/surfaces/accordion/style.css +64 -0
  217. package/src/surfaces/accordion/template.js +85 -0
  218. package/src/surfaces/app-bar/app-bar.stories.js +109 -0
  219. package/src/surfaces/app-bar/index.js +3 -0
  220. package/src/surfaces/app-bar/style.css +109 -0
  221. package/src/surfaces/app-bar/template.js +78 -0
  222. package/src/surfaces/card/card.stories.js +108 -0
  223. package/src/surfaces/card/index.js +1 -0
  224. package/src/surfaces/card/style.css +107 -0
  225. package/src/surfaces/card/template.js +163 -0
  226. package/src/surfaces/paper/index.js +3 -0
  227. package/src/surfaces/paper/paper.stories.js +97 -0
  228. package/src/surfaces/paper/style.css +73 -0
  229. package/src/surfaces/paper/template.js +76 -0
  230. package/src/surfaces/surfaces.css +4 -0
  231. package/types/controls/button-group.d.ts +8 -7
  232. package/types/controls/button.d.ts +10 -7
  233. package/types/controls/checkbox.d.ts +8 -6
  234. package/types/controls/combobox.d.ts +66 -0
  235. package/types/controls/fab.d.ts +6 -5
  236. package/types/controls/icon-button.d.ts +8 -7
  237. package/types/controls/input.d.ts +18 -11
  238. package/types/controls/radio-group.d.ts +7 -5
  239. package/types/controls/rating.d.ts +6 -5
  240. package/types/controls/select.d.ts +11 -34
  241. package/types/controls/slider.d.ts +8 -6
  242. package/types/controls/switch.d.ts +7 -5
  243. package/types/data-display/avatar.d.ts +30 -0
  244. package/types/data-display/badge.d.ts +20 -0
  245. package/types/data-display/chip.d.ts +25 -0
  246. package/types/data-display/data-grid.d.ts +151 -0
  247. package/types/data-display/divider.d.ts +14 -0
  248. package/types/data-display/icon.d.ts +16 -0
  249. package/types/data-display/list.d.ts +66 -0
  250. package/types/data-display/material-icon.d.ts +15 -0
  251. package/types/data-display/table.d.ts +45 -0
  252. package/types/data-display/tooltip.d.ts +20 -0
  253. package/types/data-display/typography.d.ts +28 -0
  254. package/types/data-display/virtual-list.d.ts +94 -0
  255. package/types/feedback/alert.d.ts +22 -0
  256. package/types/feedback/backdrop.d.ts +17 -0
  257. package/types/feedback/dialog.d.ts +21 -0
  258. package/types/feedback/progress.d.ts +18 -0
  259. package/types/feedback/skeleton.d.ts +18 -0
  260. package/types/feedback/snackbar.d.ts +19 -0
  261. package/types/index.d.ts +92 -14
  262. package/types/layout/container.d.ts +28 -0
  263. package/types/layout/flex.d.ts +16 -5
  264. package/types/layout/grid.d.ts +15 -4
  265. package/types/navigation/bottom-navigation.d.ts +33 -0
  266. package/types/navigation/breadcrumbs.d.ts +29 -0
  267. package/types/navigation/drawer.d.ts +23 -0
  268. package/types/navigation/link.d.ts +22 -0
  269. package/types/navigation/menu.d.ts +32 -0
  270. package/types/navigation/pagination.d.ts +50 -0
  271. package/types/navigation/speed-dial.d.ts +35 -0
  272. package/types/navigation/stepper.d.ts +26 -0
  273. package/types/navigation/tabs.d.ts +31 -0
  274. package/types/surfaces/accordion.d.ts +32 -0
  275. package/types/surfaces/app-bar.d.ts +25 -0
  276. package/types/surfaces/card.d.ts +39 -0
  277. package/types/surfaces/paper.d.ts +28 -0
  278. package/src/shared/withRenderValidation.js +0 -19
  279. package/src/widgets/card/card.stories.js +0 -135
  280. package/src/widgets/card/index.js +0 -4
  281. package/src/widgets/card/style.css +0 -53
  282. package/src/widgets/card/template.js +0 -97
  283. package/types/widgets/card.d.ts +0 -21
@@ -0,0 +1,523 @@
1
+ /**
2
+ * @typedef {import('../../../types/data-display/data-grid').DataGridProps} DataGridEntity
3
+ * @typedef {import('../../../types/data-display/data-grid').Column} Column
4
+ */
5
+
6
+ import { getRowKeyValue, getRows, getTotalRows } from "./helpers.js"
7
+
8
+ const MIN_COLUMN_WIDTH = 72
9
+ const VIRTUAL_START = 0
10
+ const VIRTUAL_DEFAULT_END = 20
11
+ const DEFAULT_VIEWPORT_HEIGHT = 600
12
+ const DEFAULT_BUFFER_SIZE = 5
13
+ const DEFAULT_ESTIMATED_HEIGHT = 50
14
+
15
+ /**
16
+ * Resets the table entity with default state.
17
+ * @param {DataGridEntity} entity
18
+ */
19
+ export function create(entity) {
20
+ initDataGrid(entity)
21
+ }
22
+
23
+ /**
24
+ * Toggles sorting for a column.
25
+ * @param {DataGridEntity} entity
26
+ * @param {string} columnId
27
+ */
28
+ export function sortChange(entity, columnId) {
29
+ const column = entity.columns.find((c) => c.id === columnId)
30
+ if (!column?.isSortable) return
31
+
32
+ const existingIndex = entity.sorts.findIndex((s) => s.column === columnId)
33
+
34
+ if (existingIndex !== -1) {
35
+ // Toggle direction
36
+ const existing = entity.sorts[existingIndex]
37
+ if (existing.direction === "asc") {
38
+ existing.direction = "desc"
39
+ } else {
40
+ // Remove from sort if going from desc back to nothing
41
+ entity.sorts.splice(existingIndex, 1)
42
+ }
43
+ } else {
44
+ // Add new sort
45
+ entity.sorts.push({ column: columnId, direction: "asc" })
46
+ }
47
+
48
+ if (entity.pagination) {
49
+ entity.pagination.page = 0
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Clears all sorts.
55
+ * @param {DataGridEntity} entity
56
+ */
57
+ export function sortsClear(entity) {
58
+ entity.sorts = []
59
+ }
60
+
61
+ /**
62
+ * Updates a filter value for a column.
63
+ * @param {DataGridEntity} entity
64
+ * @param {{ columnId: string, value: any }} payload
65
+ */
66
+ export function filterChange(entity, { columnId, value }) {
67
+ if (value == null || value === "") {
68
+ delete entity.filters[columnId]
69
+ } else {
70
+ entity.filters[columnId] = value
71
+ }
72
+
73
+ if (entity.pagination) {
74
+ entity.pagination.page = 0
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Clears all filters.
80
+ * @param {DataGridEntity} entity
81
+ */
82
+ export function filtersClear(entity) {
83
+ entity.filters = {}
84
+
85
+ if (entity.pagination) {
86
+ entity.pagination.page = 0
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Updates the search term.
92
+ * @param {DataGridEntity} entity
93
+ * @param {string} search
94
+ */
95
+ export function searchChange(entity, search) {
96
+ entity.search.value = search
97
+
98
+ if (entity.pagination) {
99
+ entity.pagination.page = 0
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Changes the current page.
105
+ * @param {DataGridEntity} entity
106
+ * @param {number} page
107
+ */
108
+ export function pageChange(entity, page) {
109
+ if (!entity.pagination) return
110
+
111
+ const totalPages = Math.ceil(
112
+ getTotalRows(entity) / entity.pagination.pageSize,
113
+ )
114
+ entity.pagination.page = Math.max(0, Math.min(page, totalPages - 1))
115
+ }
116
+
117
+ /**
118
+ * Moves to the next page.
119
+ * @param {DataGridEntity} entity
120
+ */
121
+ export function pageNext(entity) {
122
+ if (!entity.pagination) return
123
+ const totalPages = Math.ceil(
124
+ getTotalRows(entity) / entity.pagination.pageSize,
125
+ )
126
+ entity.pagination.page = Math.min(entity.pagination.page + 1, totalPages - 1)
127
+ }
128
+
129
+ /**
130
+ * Moves to the previous page.
131
+ * @param {DataGridEntity} entity
132
+ */
133
+ export function pagePrev(entity) {
134
+ if (!entity.pagination) return
135
+ entity.pagination.page = Math.max(entity.pagination.page - 1, 0)
136
+ }
137
+
138
+ /**
139
+ * Changes the page size.
140
+ * @param {DataGridEntity} entity
141
+ * @param {number} pageSize
142
+ */
143
+ export function pageSizeChange(entity, pageSize) {
144
+ if (!entity.pagination) return
145
+
146
+ entity.pagination.pageSize = pageSize
147
+ entity.pagination.page = 0
148
+ }
149
+
150
+ /**
151
+ * Changes the width of a column.
152
+ * @param {DataGridEntity} entity
153
+ * @param {{ columnId: string, width: number }} payload
154
+ */
155
+ export function columnResize(entity, { columnId, width }) {
156
+ const column = entity.columns.find((item) => item.id === columnId)
157
+ if (!column || Number.isNaN(width)) return
158
+
159
+ column.width = Math.max(MIN_COLUMN_WIDTH, Math.round(width))
160
+ }
161
+
162
+ /**
163
+ * Selects a row.
164
+ * @param {DataGridEntity} entity
165
+ * @param {string|number} rowId
166
+ */
167
+ export function rowSelect(entity, rowId) {
168
+ if (!entity.isMultiSelect) {
169
+ entity.selection = []
170
+ }
171
+
172
+ if (!entity.selection.includes(rowId)) {
173
+ entity.selection.push(rowId)
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Deselects a row.
179
+ * @param {DataGridEntity} entity
180
+ * @param {string|number} rowId
181
+ */
182
+ export function rowDeselect(entity, rowId) {
183
+ const index = entity.selection.indexOf(rowId)
184
+ if (index !== -1) {
185
+ entity.selection.splice(index, 1)
186
+ }
187
+ }
188
+
189
+ /**
190
+ * Toggles row selection.
191
+ * @param {DataGridEntity} entity
192
+ * @param {string|number} rowId
193
+ */
194
+ export function rowToggle(entity, rowId) {
195
+ const index = entity.selection.indexOf(rowId)
196
+
197
+ if (index === -1) {
198
+ if (!entity.isMultiSelect) {
199
+ entity.selection = [rowId] // Replace entirely
200
+ } else {
201
+ entity.selection.push(rowId)
202
+ }
203
+ } else {
204
+ entity.selection.splice(index, 1)
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Handles row selection from click interactions, including modifier keys.
210
+ * @param {DataGridEntity} entity
211
+ * @param {{ rowId: string|number, shiftKey?: boolean, metaKey?: boolean, ctrlKey?: boolean }} payload
212
+ */
213
+ export function rowClick(
214
+ entity,
215
+ { rowId, shiftKey = false, metaKey = false, ctrlKey = false },
216
+ ) {
217
+ if (!entity.isMultiSelect) {
218
+ entity.selection = [rowId]
219
+ entity.selectionAnchor = rowId
220
+ return
221
+ }
222
+
223
+ if (shiftKey && entity.selectionAnchor != null) {
224
+ selectRowRange(entity, entity.selectionAnchor, rowId)
225
+ return
226
+ }
227
+
228
+ if (metaKey || ctrlKey) {
229
+ rowToggle(entity, rowId)
230
+ entity.selectionAnchor = rowId
231
+ return
232
+ }
233
+
234
+ entity.selection = [rowId]
235
+ entity.selectionAnchor = rowId
236
+ }
237
+
238
+ /**
239
+ * Toggles selection of all currently visible rows.
240
+ * @param {DataGridEntity} entity
241
+ */
242
+ export function rowsToggleAll(entity) {
243
+ const rows = getRows(entity)
244
+ const allSelected = rows.every((row) =>
245
+ entity.selection.includes(getRowKeyValue(entity, row)),
246
+ )
247
+
248
+ if (allSelected) {
249
+ // Deselect all visible
250
+ rows.forEach((row) => {
251
+ const rowId = getRowKeyValue(entity, row)
252
+ const index = entity.selection.indexOf(rowId)
253
+ if (index !== -1) entity.selection.splice(index, 1)
254
+ })
255
+ } else {
256
+ // Select all visible
257
+ rows.forEach((row) => {
258
+ const rowId = getRowKeyValue(entity, row)
259
+ if (!entity.selection.includes(rowId)) {
260
+ entity.selection.push(rowId)
261
+ }
262
+ })
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Selects all currently visible rows.
268
+ * @param {DataGridEntity} entity
269
+ */
270
+ export function rowsSelectAll(entity) {
271
+ const rows = getRows(entity)
272
+ rows.forEach((row) => {
273
+ const rowId = getRowKeyValue(entity, row)
274
+ if (!entity.selection.includes(rowId)) {
275
+ entity.selection.push(rowId)
276
+ }
277
+ })
278
+ }
279
+
280
+ /**
281
+ * Clears all row selections.
282
+ * @param {DataGridEntity} entity
283
+ */
284
+ export function selectionClear(entity) {
285
+ entity.selection.length = 0
286
+ }
287
+
288
+ /**
289
+ * Handles the scroll event to update the virtualized visible range.
290
+ * @param {DataGridEntity} entity
291
+ * @param {HTMLElement} containerEl
292
+ */
293
+ export function virtualScroll(entity, containerEl) {
294
+ if (!entity.isVirtualized) return
295
+ initVirtualization(entity)
296
+
297
+ const rows = getRows(entity)
298
+ const totalItems = rows.length
299
+ const scrollTop = containerEl.scrollTop
300
+ const { bufferSize, itemHeight, estimatedHeight, viewportHeight } =
301
+ entity.virtualization
302
+ const height = itemHeight || estimatedHeight
303
+
304
+ const start = Math.max(
305
+ VIRTUAL_START,
306
+ Math.floor(scrollTop / height) - bufferSize,
307
+ )
308
+ const visibleCount = Math.ceil(viewportHeight / height)
309
+ const end = Math.min(start + visibleCount + bufferSize, totalItems)
310
+
311
+ if (
312
+ entity.virtualization.visibleRange.start === start &&
313
+ entity.virtualization.visibleRange.end === end
314
+ ) {
315
+ return
316
+ }
317
+
318
+ entity.virtualization.scrollTop = scrollTop
319
+ entity.virtualization.visibleRange = { start, end }
320
+ }
321
+
322
+ /**
323
+ * Measures the virtualized rows container to update item and viewport height.
324
+ * @param {DataGridEntity} entity
325
+ * @param {HTMLElement} containerEl
326
+ */
327
+ export function virtualMount(entity, containerEl) {
328
+ if (!entity.isVirtualized) return
329
+ initVirtualization(entity)
330
+
331
+ const viewportHeight = containerEl.clientHeight || containerEl.offsetHeight
332
+ if (viewportHeight) {
333
+ entity.virtualization.viewportHeight = viewportHeight
334
+ }
335
+
336
+ const firstRow = containerEl.querySelector("[data-index]")
337
+ if (!firstRow) return
338
+
339
+ entity.virtualization.itemHeight = firstRow.offsetHeight
340
+ const height =
341
+ entity.virtualization.itemHeight || entity.virtualization.estimatedHeight
342
+ const totalItems = getRows(entity).length
343
+ const end = Math.min(
344
+ Math.ceil(entity.virtualization.viewportHeight / height),
345
+ totalItems,
346
+ )
347
+
348
+ entity.virtualization.visibleRange = { start: 0, end }
349
+ }
350
+
351
+ /**
352
+ * Initializes the table state.
353
+ * @param {DataGridEntity} entity
354
+ */
355
+ function initDataGrid(entity) {
356
+ entity.rows ??= []
357
+ entity.rowId ??= "id"
358
+
359
+ // Auto-generate columns from first data item if not provided
360
+ if (!entity.columns && entity.rows.length) {
361
+ const [firstRow] = entity.rows
362
+
363
+ entity.columns = Object.keys(firstRow).map((key) => {
364
+ const value = firstRow[key]
365
+ const type = getDefaultColumnType(value)
366
+ const filter = getDefaultColumnFilter(type)
367
+
368
+ return {
369
+ id: key,
370
+ title: capitalize(key),
371
+ type,
372
+ isSortable: false,
373
+ isFilterable: false,
374
+ filter,
375
+ width: getDefaultColumnWidth(filter.type),
376
+ }
377
+ })
378
+ } else {
379
+ entity.columns ??= []
380
+ entity.columns.forEach((column) => {
381
+ column.title ??= capitalize(column.id)
382
+ column.type ??= getDefaultColumnType()
383
+ column.filter ??= getDefaultColumnFilter(column.type)
384
+ column.width ??= getDefaultColumnWidth(column.filter.type)
385
+ })
386
+ }
387
+
388
+ // State
389
+ entity.sorts ??= []
390
+ entity.filters ??= {}
391
+ entity.search ??= null
392
+ if (entity.search) {
393
+ entity.search.value ??= ""
394
+ }
395
+ entity.selection ??= []
396
+ entity.selectionAnchor ??= null
397
+ entity.isMultiSelect ??= true
398
+ entity.isVirtualized ??= false
399
+
400
+ entity.pagination ??= null
401
+ if (entity.pagination) {
402
+ entity.pagination.page ??= 0
403
+ }
404
+
405
+ if (entity.isVirtualized) {
406
+ initVirtualization(entity)
407
+ }
408
+
409
+ assertRowKeys(entity)
410
+ }
411
+
412
+ /**
413
+ * Ensures virtualization defaults are set.
414
+ * @param {DataGridEntity} entity
415
+ */
416
+ function initVirtualization(entity) {
417
+ entity.virtualization ??= {}
418
+ entity.virtualization.scrollTop ??= 0
419
+ entity.virtualization.visibleRange ??= {
420
+ start: VIRTUAL_START,
421
+ end: VIRTUAL_DEFAULT_END,
422
+ }
423
+ entity.virtualization.viewportHeight ??= DEFAULT_VIEWPORT_HEIGHT
424
+ entity.virtualization.bufferSize ??= DEFAULT_BUFFER_SIZE
425
+ entity.virtualization.itemHeight ??= null
426
+ entity.virtualization.estimatedHeight ??= DEFAULT_ESTIMATED_HEIGHT
427
+ }
428
+
429
+ /**
430
+ * Infers the column type from a value.
431
+ * @param {any} value
432
+ * @returns {string}
433
+ */
434
+ function getDefaultColumnType(value) {
435
+ if (typeof value === "number") return "number"
436
+ if (typeof value === "boolean") return "boolean"
437
+ if (value instanceof Date) return "date"
438
+ return "string"
439
+ }
440
+
441
+ /**
442
+ * Gets the default filter configuration for a column type.
443
+ * @param {string} type
444
+ * @returns {object}
445
+ */
446
+ function getDefaultColumnFilter(type) {
447
+ if (type === "number") return { type: "range" }
448
+ if (type === "boolean") return { type: "select", options: ["", true, false] }
449
+ if (type === "date") return { type: "date" }
450
+ return { type: "text" }
451
+ }
452
+
453
+ /**
454
+ * Gets the default column width based on filter type.
455
+ * @param {string} filterType
456
+ * @returns {number}
457
+ */
458
+ function getDefaultColumnWidth(filterType) {
459
+ if (filterType === "number") return 70
460
+ if (filterType === "range") return 120
461
+ if (filterType === "select") return 70
462
+ if (filterType === "date") return 120
463
+ if (filterType === "time") return 120
464
+ if (filterType === "datetime") return 170
465
+ return 200
466
+ }
467
+
468
+ /**
469
+ * Capitalizes the first letter of a string.
470
+ * @param {string} str
471
+ * @returns {string}
472
+ */
473
+ function capitalize(str) {
474
+ const [firstChar, ...rest] = str
475
+ return [firstChar.toUpperCase(), ...rest].join("")
476
+ }
477
+
478
+ /**
479
+ * Selects a contiguous range of visible rows.
480
+ * @param {DataGridEntity} entity
481
+ * @param {string|number} fromRowId
482
+ * @param {string|number} toRowId
483
+ */
484
+ function selectRowRange(entity, fromRowId, toRowId) {
485
+ const visibleRows = getRows(entity)
486
+ const visibleRowIds = visibleRows.map((row) => getRowKeyValue(entity, row))
487
+ const start = visibleRowIds.indexOf(fromRowId)
488
+ const end = visibleRowIds.indexOf(toRowId)
489
+
490
+ if (start === -1 || end === -1) {
491
+ entity.selection = [toRowId]
492
+ entity.selectionAnchor = toRowId
493
+ return
494
+ }
495
+
496
+ const [rangeStart, rangeEnd] = start < end ? [start, end] : [end, start]
497
+ entity.selection = visibleRowIds.slice(rangeStart, rangeEnd + 1)
498
+ }
499
+
500
+ /**
501
+ * Ensures all rows have unique and stable keys.
502
+ * @param {DataGridEntity} entity
503
+ */
504
+ function assertRowKeys(entity) {
505
+ if (!entity.rows.length) return
506
+
507
+ const seen = new Set()
508
+ entity.rows.forEach((row, index) => {
509
+ const rowKey = getRowKeyValue(entity, row)
510
+ const isValidType = typeof rowKey === "string" || typeof rowKey === "number"
511
+ if (!isValidType) {
512
+ throw new Error(
513
+ `[table] Invalid row key at row ${index} for field "${entity.rowId}". Keys must be string or number. Set entity.rowId to a stable key field.`,
514
+ )
515
+ }
516
+ if (seen.has(rowKey)) {
517
+ throw new Error(
518
+ `[table] Duplicate row key "${rowKey}" for field "${entity.rowId}". Keys must be unique.`,
519
+ )
520
+ }
521
+ seen.add(rowKey)
522
+ })
523
+ }