@sondor-technology/sondor-components 1.0.8 → 1.0.10

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 (437) hide show
  1. package/dist/components/accordion/accordion.constants.d.ts +5 -0
  2. package/dist/components/accordion/accordion.constants.d.ts.map +1 -0
  3. package/dist/components/accordion/accordion.constants.js +3 -0
  4. package/dist/components/accordion/accordion.constants.js.map +1 -0
  5. package/dist/components/accordion/accordion.d.ts +29 -0
  6. package/dist/components/accordion/accordion.d.ts.map +1 -0
  7. package/dist/components/accordion/accordion.js +226 -0
  8. package/dist/components/accordion/accordion.js.map +1 -0
  9. package/dist/components/alert/alert.constants.d.ts +2 -0
  10. package/dist/components/alert/alert.constants.d.ts.map +1 -1
  11. package/dist/components/alert/alert.d.ts +50 -2
  12. package/dist/components/alert/alert.d.ts.map +1 -1
  13. package/dist/components/alert/alert.js +285 -16
  14. package/dist/components/alert/alert.js.map +1 -1
  15. package/dist/components/avatar/avatar.d.ts +20 -0
  16. package/dist/components/avatar/avatar.d.ts.map +1 -0
  17. package/dist/components/avatar/avatar.js +100 -0
  18. package/dist/components/avatar/avatar.js.map +1 -0
  19. package/dist/components/back-to-top/back-to-top.d.ts +29 -0
  20. package/dist/components/back-to-top/back-to-top.d.ts.map +1 -0
  21. package/dist/components/back-to-top/back-to-top.js +149 -0
  22. package/dist/components/back-to-top/back-to-top.js.map +1 -0
  23. package/dist/components/bar-chart/bar-chart.d.ts +42 -0
  24. package/dist/components/bar-chart/bar-chart.d.ts.map +1 -0
  25. package/dist/components/bar-chart/bar-chart.js +298 -0
  26. package/dist/components/bar-chart/bar-chart.js.map +1 -0
  27. package/dist/components/breadcrumb/breadcrumb.js +2 -2
  28. package/dist/components/button/button.d.ts +1 -0
  29. package/dist/components/button/button.d.ts.map +1 -1
  30. package/dist/components/button/button.js +8 -8
  31. package/dist/components/button/button.js.map +1 -1
  32. package/dist/components/button-group/button-group.js +4 -4
  33. package/dist/components/calendar/calendar.d.ts.map +1 -1
  34. package/dist/components/calendar/calendar.js +21 -9
  35. package/dist/components/calendar/calendar.js.map +1 -1
  36. package/dist/components/chart/chart-base.d.ts +59 -0
  37. package/dist/components/chart/chart-base.d.ts.map +1 -0
  38. package/dist/components/chart/chart-base.js +261 -0
  39. package/dist/components/chart/chart-base.js.map +1 -0
  40. package/dist/components/chart/chart.utils.d.ts +27 -0
  41. package/dist/components/chart/chart.utils.d.ts.map +1 -0
  42. package/dist/components/chart/chart.utils.js +117 -0
  43. package/dist/components/chart/chart.utils.js.map +1 -0
  44. package/dist/components/code-block/code-block.d.ts +5 -0
  45. package/dist/components/code-block/code-block.d.ts.map +1 -1
  46. package/dist/components/code-block/code-block.js +34 -24
  47. package/dist/components/code-block/code-block.js.map +1 -1
  48. package/dist/components/consent/consent.d.ts +63 -0
  49. package/dist/components/consent/consent.d.ts.map +1 -0
  50. package/dist/components/consent/consent.js +327 -0
  51. package/dist/components/consent/consent.js.map +1 -0
  52. package/dist/components/consent/consent.utils.d.ts +36 -0
  53. package/dist/components/consent/consent.utils.d.ts.map +1 -0
  54. package/dist/components/consent/consent.utils.js +82 -0
  55. package/dist/components/consent/consent.utils.js.map +1 -0
  56. package/dist/components/contact/contact.d.ts +22 -0
  57. package/dist/components/contact/contact.d.ts.map +1 -0
  58. package/dist/components/contact/contact.js +83 -0
  59. package/dist/components/contact/contact.js.map +1 -0
  60. package/dist/components/container/container.d.ts +20 -0
  61. package/dist/components/container/container.d.ts.map +1 -0
  62. package/dist/components/container/container.js +68 -0
  63. package/dist/components/container/container.js.map +1 -0
  64. package/dist/components/content-card/content-card.js +4 -4
  65. package/dist/components/context-menu/context-menu.d.ts +89 -0
  66. package/dist/components/context-menu/context-menu.d.ts.map +1 -0
  67. package/dist/components/context-menu/context-menu.js +414 -0
  68. package/dist/components/context-menu/context-menu.js.map +1 -0
  69. package/dist/components/cron-builder/cron-builder.d.ts +5 -2
  70. package/dist/components/cron-builder/cron-builder.d.ts.map +1 -1
  71. package/dist/components/cron-builder/cron-builder.js +83 -15
  72. package/dist/components/cron-builder/cron-builder.js.map +1 -1
  73. package/dist/components/cta/cta.d.ts +20 -0
  74. package/dist/components/cta/cta.d.ts.map +1 -0
  75. package/dist/components/cta/cta.js +64 -0
  76. package/dist/components/cta/cta.js.map +1 -0
  77. package/dist/components/drawer/drawer.js +6 -6
  78. package/dist/components/faq/faq-item.d.ts +22 -0
  79. package/dist/components/faq/faq-item.d.ts.map +1 -0
  80. package/dist/components/faq/faq-item.js +117 -0
  81. package/dist/components/faq/faq-item.js.map +1 -0
  82. package/dist/components/faq/faq.d.ts +18 -0
  83. package/dist/components/faq/faq.d.ts.map +1 -0
  84. package/dist/components/faq/faq.js +38 -0
  85. package/dist/components/faq/faq.js.map +1 -0
  86. package/dist/components/feature-grid/feature-grid.d.ts +22 -0
  87. package/dist/components/feature-grid/feature-grid.d.ts.map +1 -0
  88. package/dist/components/feature-grid/feature-grid.js +55 -0
  89. package/dist/components/feature-grid/feature-grid.js.map +1 -0
  90. package/dist/components/feature-grid/feature.d.ts +18 -0
  91. package/dist/components/feature-grid/feature.d.ts.map +1 -0
  92. package/dist/components/feature-grid/feature.js +91 -0
  93. package/dist/components/feature-grid/feature.js.map +1 -0
  94. package/dist/components/field/field.js +2 -2
  95. package/dist/components/footer/footer.js +4 -4
  96. package/dist/components/form/form-reset.d.ts +7 -0
  97. package/dist/components/form/form-reset.d.ts.map +1 -0
  98. package/dist/components/form/form-reset.js +47 -0
  99. package/dist/components/form/form-reset.js.map +1 -0
  100. package/dist/components/form/form.d.ts +5 -0
  101. package/dist/components/form/form.d.ts.map +1 -1
  102. package/dist/components/form/form.js +17 -3
  103. package/dist/components/form/form.js.map +1 -1
  104. package/dist/components/grid/grid.d.ts +5 -0
  105. package/dist/components/grid/grid.d.ts.map +1 -1
  106. package/dist/components/grid/grid.js +86 -4
  107. package/dist/components/grid/grid.js.map +1 -1
  108. package/dist/components/heat-map/heat-map.d.ts +39 -0
  109. package/dist/components/heat-map/heat-map.d.ts.map +1 -0
  110. package/dist/components/heat-map/heat-map.js +306 -0
  111. package/dist/components/heat-map/heat-map.js.map +1 -0
  112. package/dist/components/hero/hero.d.ts +26 -0
  113. package/dist/components/hero/hero.d.ts.map +1 -0
  114. package/dist/components/hero/hero.js +132 -0
  115. package/dist/components/hero/hero.js.map +1 -0
  116. package/dist/components/icon/icon-paths.d.ts +84 -0
  117. package/dist/components/icon/icon-paths.d.ts.map +1 -1
  118. package/dist/components/icon/icon-paths.js +89 -0
  119. package/dist/components/icon/icon-paths.js.map +1 -1
  120. package/dist/components/icon/icon.d.ts +2 -0
  121. package/dist/components/icon/icon.d.ts.map +1 -1
  122. package/dist/components/icon/icon.js +10 -1
  123. package/dist/components/icon/icon.js.map +1 -1
  124. package/dist/components/image/image.d.ts +31 -0
  125. package/dist/components/image/image.d.ts.map +1 -0
  126. package/dist/components/image/image.js +179 -0
  127. package/dist/components/image/image.js.map +1 -0
  128. package/dist/components/input/input.js +4 -4
  129. package/dist/components/lazy/lazy.d.ts +34 -0
  130. package/dist/components/lazy/lazy.d.ts.map +1 -0
  131. package/dist/components/lazy/lazy.js +104 -0
  132. package/dist/components/lazy/lazy.js.map +1 -0
  133. package/dist/components/line-chart/line-chart.d.ts +37 -0
  134. package/dist/components/line-chart/line-chart.d.ts.map +1 -0
  135. package/dist/components/line-chart/line-chart.js +271 -0
  136. package/dist/components/line-chart/line-chart.js.map +1 -0
  137. package/dist/components/list/list-item.d.ts +45 -0
  138. package/dist/components/list/list-item.d.ts.map +1 -0
  139. package/dist/components/list/list-item.js +263 -0
  140. package/dist/components/list/list-item.js.map +1 -0
  141. package/dist/components/list/list.d.ts +50 -0
  142. package/dist/components/list/list.d.ts.map +1 -0
  143. package/dist/components/list/list.js +161 -0
  144. package/dist/components/list/list.js.map +1 -0
  145. package/dist/components/logo-cloud/logo-cloud.d.ts +19 -0
  146. package/dist/components/logo-cloud/logo-cloud.d.ts.map +1 -0
  147. package/dist/components/logo-cloud/logo-cloud.js +66 -0
  148. package/dist/components/logo-cloud/logo-cloud.js.map +1 -0
  149. package/dist/components/modal/modal.d.ts +12 -0
  150. package/dist/components/modal/modal.d.ts.map +1 -1
  151. package/dist/components/modal/modal.js +40 -11
  152. package/dist/components/modal/modal.js.map +1 -1
  153. package/dist/components/page-layout/page-layout.js +8 -8
  154. package/dist/components/pagination/pagination.d.ts +15 -0
  155. package/dist/components/pagination/pagination.d.ts.map +1 -0
  156. package/dist/components/pagination/pagination.js +126 -0
  157. package/dist/components/pagination/pagination.js.map +1 -0
  158. package/dist/components/pagination/pagination.utils.d.ts +3 -0
  159. package/dist/components/pagination/pagination.utils.d.ts.map +1 -0
  160. package/dist/components/pagination/pagination.utils.js +19 -0
  161. package/dist/components/pagination/pagination.utils.js.map +1 -0
  162. package/dist/components/pie-chart/pie-chart.d.ts +33 -0
  163. package/dist/components/pie-chart/pie-chart.d.ts.map +1 -0
  164. package/dist/components/pie-chart/pie-chart.js +233 -0
  165. package/dist/components/pie-chart/pie-chart.js.map +1 -0
  166. package/dist/components/pin-input/pin-input.js +4 -4
  167. package/dist/components/popover/popover.d.ts.map +1 -1
  168. package/dist/components/popover/popover.js +1 -3
  169. package/dist/components/popover/popover.js.map +1 -1
  170. package/dist/components/post-list/post-card.d.ts +16 -0
  171. package/dist/components/post-list/post-card.d.ts.map +1 -0
  172. package/dist/components/post-list/post-card.js +22 -0
  173. package/dist/components/post-list/post-card.js.map +1 -0
  174. package/dist/components/post-list/post-list.d.ts +17 -0
  175. package/dist/components/post-list/post-list.d.ts.map +1 -0
  176. package/dist/components/post-list/post-list.js +23 -0
  177. package/dist/components/post-list/post-list.js.map +1 -0
  178. package/dist/components/pricing/pricing-tier.d.ts +23 -0
  179. package/dist/components/pricing/pricing-tier.d.ts.map +1 -0
  180. package/dist/components/pricing/pricing-tier.js +144 -0
  181. package/dist/components/pricing/pricing-tier.js.map +1 -0
  182. package/dist/components/pricing/pricing.d.ts +18 -0
  183. package/dist/components/pricing/pricing.d.ts.map +1 -0
  184. package/dist/components/pricing/pricing.js +38 -0
  185. package/dist/components/pricing/pricing.js.map +1 -0
  186. package/dist/components/progress/progress.d.ts +17 -0
  187. package/dist/components/progress/progress.d.ts.map +1 -0
  188. package/dist/components/progress/progress.js +109 -0
  189. package/dist/components/progress/progress.js.map +1 -0
  190. package/dist/components/project-grid/project-card.d.ts +17 -0
  191. package/dist/components/project-grid/project-card.d.ts.map +1 -0
  192. package/dist/components/project-grid/project-card.js +23 -0
  193. package/dist/components/project-grid/project-card.js.map +1 -0
  194. package/dist/components/project-grid/project-grid.d.ts +17 -0
  195. package/dist/components/project-grid/project-grid.d.ts.map +1 -0
  196. package/dist/components/project-grid/project-grid.js +23 -0
  197. package/dist/components/project-grid/project-grid.js.map +1 -0
  198. package/dist/components/radio/radio.js +1 -1
  199. package/dist/components/reveal/reveal.constants.d.ts +10 -0
  200. package/dist/components/reveal/reveal.constants.d.ts.map +1 -0
  201. package/dist/components/reveal/reveal.constants.js +11 -0
  202. package/dist/components/reveal/reveal.constants.js.map +1 -0
  203. package/dist/components/reveal/reveal.d.ts +64 -0
  204. package/dist/components/reveal/reveal.d.ts.map +1 -0
  205. package/dist/components/reveal/reveal.js +352 -0
  206. package/dist/components/reveal/reveal.js.map +1 -0
  207. package/dist/components/scroll-progress/scroll-progress.d.ts +32 -0
  208. package/dist/components/scroll-progress/scroll-progress.d.ts.map +1 -0
  209. package/dist/components/scroll-progress/scroll-progress.js +120 -0
  210. package/dist/components/scroll-progress/scroll-progress.js.map +1 -0
  211. package/dist/components/section/card-row.d.ts +30 -0
  212. package/dist/components/section/card-row.d.ts.map +1 -0
  213. package/dist/components/section/card-row.js +174 -0
  214. package/dist/components/section/card-row.js.map +1 -0
  215. package/dist/components/section/card.d.ts +27 -0
  216. package/dist/components/section/card.d.ts.map +1 -0
  217. package/dist/components/section/card.js +205 -0
  218. package/dist/components/section/card.js.map +1 -0
  219. package/dist/components/section/row-item.d.ts +22 -0
  220. package/dist/components/section/row-item.d.ts.map +1 -0
  221. package/dist/components/section/row-item.js +53 -0
  222. package/dist/components/section/row-item.js.map +1 -0
  223. package/dist/components/section/section.d.ts +70 -0
  224. package/dist/components/section/section.d.ts.map +1 -0
  225. package/dist/components/section/section.js +240 -0
  226. package/dist/components/section/section.js.map +1 -0
  227. package/dist/components/select/select.js +3 -3
  228. package/dist/components/series-nav/series-nav.d.ts +29 -0
  229. package/dist/components/series-nav/series-nav.d.ts.map +1 -0
  230. package/dist/components/series-nav/series-nav.js +178 -0
  231. package/dist/components/series-nav/series-nav.js.map +1 -0
  232. package/dist/components/site-header/site-header.d.ts +51 -0
  233. package/dist/components/site-header/site-header.d.ts.map +1 -0
  234. package/dist/components/site-header/site-header.js +296 -0
  235. package/dist/components/site-header/site-header.js.map +1 -0
  236. package/dist/components/skeleton/skeleton.js +7 -7
  237. package/dist/components/sparkline/sparkline.d.ts +19 -0
  238. package/dist/components/sparkline/sparkline.d.ts.map +1 -0
  239. package/dist/components/sparkline/sparkline.js +99 -0
  240. package/dist/components/sparkline/sparkline.js.map +1 -0
  241. package/dist/components/spinner/spinner.d.ts +13 -0
  242. package/dist/components/spinner/spinner.d.ts.map +1 -0
  243. package/dist/components/spinner/spinner.js +65 -0
  244. package/dist/components/spinner/spinner.js.map +1 -0
  245. package/dist/components/split/split.d.ts +23 -0
  246. package/dist/components/split/split.d.ts.map +1 -0
  247. package/dist/components/split/split.js +104 -0
  248. package/dist/components/split/split.js.map +1 -0
  249. package/dist/components/stack/stack.d.ts +21 -0
  250. package/dist/components/stack/stack.d.ts.map +1 -0
  251. package/dist/components/stack/stack.js +102 -0
  252. package/dist/components/stack/stack.js.map +1 -0
  253. package/dist/components/stat-card/stat-card.d.ts +35 -0
  254. package/dist/components/stat-card/stat-card.d.ts.map +1 -0
  255. package/dist/components/stat-card/stat-card.js +199 -0
  256. package/dist/components/stat-card/stat-card.js.map +1 -0
  257. package/dist/components/stats/stat.d.ts +16 -0
  258. package/dist/components/stats/stat.d.ts.map +1 -0
  259. package/dist/components/stats/stat.js +51 -0
  260. package/dist/components/stats/stat.js.map +1 -0
  261. package/dist/components/stats/stats.d.ts +19 -0
  262. package/dist/components/stats/stats.d.ts.map +1 -0
  263. package/dist/components/stats/stats.js +45 -0
  264. package/dist/components/stats/stats.js.map +1 -0
  265. package/dist/components/switch/switch.js +2 -2
  266. package/dist/components/table/data-table.d.ts +115 -27
  267. package/dist/components/table/data-table.d.ts.map +1 -1
  268. package/dist/components/table/data-table.js +1012 -411
  269. package/dist/components/table/data-table.js.map +1 -1
  270. package/dist/components/table/data-table.utils.d.ts +81 -0
  271. package/dist/components/table/data-table.utils.d.ts.map +1 -0
  272. package/dist/components/table/data-table.utils.js +208 -0
  273. package/dist/components/table/data-table.utils.js.map +1 -0
  274. package/dist/components/table/table.d.ts +28 -0
  275. package/dist/components/table/table.d.ts.map +1 -1
  276. package/dist/components/table/table.js +161 -33
  277. package/dist/components/table/table.js.map +1 -1
  278. package/dist/components/team/person.d.ts +19 -0
  279. package/dist/components/team/person.d.ts.map +1 -0
  280. package/dist/components/team/person.js +123 -0
  281. package/dist/components/team/person.js.map +1 -0
  282. package/dist/components/team/team.d.ts +18 -0
  283. package/dist/components/team/team.d.ts.map +1 -0
  284. package/dist/components/team/team.js +36 -0
  285. package/dist/components/team/team.js.map +1 -0
  286. package/dist/components/testimonials/testimonial.d.ts +18 -0
  287. package/dist/components/testimonials/testimonial.d.ts.map +1 -0
  288. package/dist/components/testimonials/testimonial.js +115 -0
  289. package/dist/components/testimonials/testimonial.js.map +1 -0
  290. package/dist/components/testimonials/testimonials.d.ts +18 -0
  291. package/dist/components/testimonials/testimonials.d.ts.map +1 -0
  292. package/dist/components/testimonials/testimonials.js +36 -0
  293. package/dist/components/testimonials/testimonials.js.map +1 -0
  294. package/dist/components/textarea/textarea.js +3 -3
  295. package/dist/components/theme-toggle/theme-toggle.js +1 -1
  296. package/dist/components/time-picker/time-picker.d.ts.map +1 -1
  297. package/dist/components/time-picker/time-picker.js +13 -3
  298. package/dist/components/time-picker/time-picker.js.map +1 -1
  299. package/dist/components/timeline/timeline-item.d.ts +18 -0
  300. package/dist/components/timeline/timeline-item.d.ts.map +1 -0
  301. package/dist/components/timeline/timeline-item.js +102 -0
  302. package/dist/components/timeline/timeline-item.js.map +1 -0
  303. package/dist/components/timeline/timeline.d.ts +18 -0
  304. package/dist/components/timeline/timeline.d.ts.map +1 -0
  305. package/dist/components/timeline/timeline.js +30 -0
  306. package/dist/components/timeline/timeline.js.map +1 -0
  307. package/dist/components/toast/toast.d.ts +33 -0
  308. package/dist/components/toast/toast.d.ts.map +1 -0
  309. package/dist/components/toast/toast.js +147 -0
  310. package/dist/components/toast/toast.js.map +1 -0
  311. package/dist/components/toggle-group/toggle-button.d.ts +2 -0
  312. package/dist/components/toggle-group/toggle-button.d.ts.map +1 -1
  313. package/dist/components/toggle-group/toggle-button.js +12 -5
  314. package/dist/components/toggle-group/toggle-button.js.map +1 -1
  315. package/dist/components/toggle-group/toggle-group.d.ts +6 -0
  316. package/dist/components/toggle-group/toggle-group.d.ts.map +1 -1
  317. package/dist/components/toggle-group/toggle-group.js +141 -21
  318. package/dist/components/toggle-group/toggle-group.js.map +1 -1
  319. package/dist/components/tooltip/tooltip.js +21 -21
  320. package/dist/components/vertical-nav/vertical-nav-group.js +3 -3
  321. package/dist/components/vertical-nav/vertical-nav-item.js +4 -4
  322. package/dist/components/video-embed/video-embed.d.ts +37 -0
  323. package/dist/components/video-embed/video-embed.d.ts.map +1 -0
  324. package/dist/components/video-embed/video-embed.js +183 -0
  325. package/dist/components/video-embed/video-embed.js.map +1 -0
  326. package/dist/index.d.ts +107 -0
  327. package/dist/index.d.ts.map +1 -0
  328. package/dist/index.js +104 -0
  329. package/dist/index.js.map +1 -0
  330. package/dist/styles/index.css +205 -1
  331. package/dist/styles/reset.d.ts.map +1 -1
  332. package/dist/styles/reset.js +5 -0
  333. package/dist/styles/reset.js.map +1 -1
  334. package/package.json +214 -7
  335. package/src/components/accordion/accordion.constants.ts +5 -0
  336. package/src/components/accordion/accordion.ts +209 -0
  337. package/src/components/alert/alert.constants.ts +3 -0
  338. package/src/components/alert/alert.ts +284 -17
  339. package/src/components/avatar/avatar.ts +84 -0
  340. package/src/components/back-to-top/back-to-top.ts +140 -0
  341. package/src/components/bar-chart/bar-chart.ts +295 -0
  342. package/src/components/breadcrumb/breadcrumb.ts +2 -2
  343. package/src/components/button/button.ts +9 -8
  344. package/src/components/button-group/button-group.ts +4 -4
  345. package/src/components/calendar/calendar.ts +26 -9
  346. package/src/components/chart/chart-base.ts +275 -0
  347. package/src/components/chart/chart.utils.ts +140 -0
  348. package/src/components/code-block/code-block.ts +35 -24
  349. package/src/components/consent/consent.ts +312 -0
  350. package/src/components/consent/consent.utils.ts +110 -0
  351. package/src/components/contact/contact.ts +83 -0
  352. package/src/components/container/container.ts +60 -0
  353. package/src/components/content-card/content-card.ts +4 -4
  354. package/src/components/context-menu/context-menu.ts +426 -0
  355. package/src/components/cron-builder/cron-builder.ts +102 -22
  356. package/src/components/cta/cta.ts +64 -0
  357. package/src/components/drawer/drawer.ts +6 -6
  358. package/src/components/faq/faq-item.ts +110 -0
  359. package/src/components/faq/faq.ts +40 -0
  360. package/src/components/feature-grid/feature-grid.ts +52 -0
  361. package/src/components/feature-grid/feature.ts +84 -0
  362. package/src/components/field/field.ts +2 -2
  363. package/src/components/footer/footer.ts +4 -4
  364. package/src/components/form/form-reset.ts +45 -0
  365. package/src/components/form/form.ts +17 -3
  366. package/src/components/grid/grid.ts +73 -4
  367. package/src/components/heat-map/heat-map.ts +292 -0
  368. package/src/components/hero/hero.ts +132 -0
  369. package/src/components/icon/icon-paths.ts +92 -1
  370. package/src/components/icon/icon.ts +8 -1
  371. package/src/components/image/image.ts +165 -0
  372. package/src/components/input/input.ts +4 -4
  373. package/src/components/lazy/lazy.ts +94 -0
  374. package/src/components/line-chart/line-chart.ts +271 -0
  375. package/src/components/list/list-item.ts +225 -0
  376. package/src/components/list/list.ts +157 -0
  377. package/src/components/logo-cloud/logo-cloud.ts +65 -0
  378. package/src/components/modal/modal.ts +37 -11
  379. package/src/components/page-layout/page-layout.ts +8 -8
  380. package/src/components/pagination/pagination.ts +120 -0
  381. package/src/components/pagination/pagination.utils.ts +14 -0
  382. package/src/components/pie-chart/pie-chart.ts +230 -0
  383. package/src/components/pin-input/pin-input.ts +4 -4
  384. package/src/components/popover/popover.ts +1 -3
  385. package/src/components/post-list/post-card.ts +18 -0
  386. package/src/components/post-list/post-list.ts +19 -0
  387. package/src/components/pricing/pricing-tier.ts +132 -0
  388. package/src/components/pricing/pricing.ts +39 -0
  389. package/src/components/progress/progress.ts +95 -0
  390. package/src/components/project-grid/project-card.ts +19 -0
  391. package/src/components/project-grid/project-grid.ts +19 -0
  392. package/src/components/radio/radio.ts +1 -1
  393. package/src/components/reveal/reveal.constants.ts +20 -0
  394. package/src/components/reveal/reveal.ts +344 -0
  395. package/src/components/scroll-progress/scroll-progress.ts +119 -0
  396. package/src/components/section/card-row.ts +152 -0
  397. package/src/components/section/card.ts +197 -0
  398. package/src/components/section/row-item.ts +38 -0
  399. package/src/components/section/section.ts +232 -0
  400. package/src/components/select/select.ts +3 -3
  401. package/src/components/series-nav/series-nav.ts +164 -0
  402. package/src/components/site-header/site-header.ts +276 -0
  403. package/src/components/skeleton/skeleton.ts +7 -7
  404. package/src/components/sparkline/sparkline.ts +83 -0
  405. package/src/components/spinner/spinner.ts +55 -0
  406. package/src/components/split/split.ts +106 -0
  407. package/src/components/stack/stack.ts +88 -0
  408. package/src/components/stat-card/stat-card.ts +179 -0
  409. package/src/components/stats/stat.ts +49 -0
  410. package/src/components/stats/stats.ts +47 -0
  411. package/src/components/switch/switch.ts +2 -2
  412. package/src/components/table/data-table.ts +1015 -425
  413. package/src/components/table/data-table.utils.ts +276 -0
  414. package/src/components/table/table.ts +168 -38
  415. package/src/components/team/person.ts +113 -0
  416. package/src/components/team/team.ts +37 -0
  417. package/src/components/testimonials/testimonial.ts +108 -0
  418. package/src/components/testimonials/testimonials.ts +37 -0
  419. package/src/components/textarea/textarea.ts +3 -3
  420. package/src/components/theme-toggle/theme-toggle.ts +1 -1
  421. package/src/components/time-picker/time-picker.ts +17 -3
  422. package/src/components/timeline/timeline-item.ts +100 -0
  423. package/src/components/timeline/timeline.ts +31 -0
  424. package/src/components/toast/toast.ts +153 -0
  425. package/src/components/toggle-group/toggle-button.ts +9 -5
  426. package/src/components/toggle-group/toggle-group.ts +158 -42
  427. package/src/components/tooltip/tooltip.ts +21 -21
  428. package/src/components/vertical-nav/vertical-nav-group.ts +3 -3
  429. package/src/components/vertical-nav/vertical-nav-item.ts +4 -4
  430. package/src/components/video-embed/video-embed.ts +162 -0
  431. package/src/index.ts +108 -0
  432. package/src/styles/globals.css +63 -0
  433. package/src/styles/index.css +3 -1
  434. package/src/styles/prose.css +154 -0
  435. package/src/styles/reset.ts +5 -0
  436. package/src/styles/theme.css +53 -0
  437. package/src/styles/typography.css +1 -0
@@ -1,38 +1,115 @@
1
- import {LitElement, html, css, PropertyValues} from "lit";
2
- import {customElement, property, state} from "lit/decorators.js";
1
+ import {LitElement, html, css, nothing, PropertyValues} from "lit";
2
+ import {customElement, property, query, state} from "lit/decorators.js";
3
3
  import {classMap} from "lit/directives/class-map.js";
4
- import {resetStyles} from "../../styles/reset.js";
5
4
  import {ifDefined} from "lit/directives/if-defined.js";
6
-
7
- export interface DataTableColumn {
8
- header: string;
9
- key: string;
10
- sortable?: boolean;
11
- hidden?: boolean;
12
- }
13
-
14
- export interface SortConfig {
15
- key: string;
16
- direction: "asc" | "desc";
5
+ import {resetStyles} from "../../styles/reset.js";
6
+ import "../icon/icon.js";
7
+ import "../skeleton/skeleton.js";
8
+ import "../pagination/pagination.js";
9
+ import "../context-menu/context-menu.js";
10
+ import type {MenuItem, StContextMenu} from "../context-menu/context-menu.js";
11
+ import {
12
+ applyFilters,
13
+ cellValue,
14
+ defaultOperator,
15
+ deriveOptions,
16
+ describeFilter,
17
+ isFilterComplete,
18
+ nextSort,
19
+ operatorsFor,
20
+ paginate,
21
+ searchRows,
22
+ sortRows,
23
+ type DataFilter,
24
+ type DataTableColumn,
25
+ type SortConfig,
26
+ } from "./data-table.utils.js";
27
+
28
+ export type {DataFilter, DataTableColumn, FilterOperator, FilterType, SortConfig} from "./data-table.utils.js";
29
+ export type {MenuItem} from "../context-menu/context-menu.js";
30
+ export type RowActions = MenuItem[] | ((row: any) => MenuItem[]);
31
+
32
+ const DEBOUNCE_MS = 350;
33
+ const SKELETON_ROWS = 5;
34
+
35
+ interface View {
36
+ rows: any[];
37
+ total: number;
38
+ page: number;
39
+ totalPages: number;
40
+ from: number;
41
+ to: number;
17
42
  }
18
43
 
44
+ /**
45
+ * Data table with search, multi-column filtering, multi-column sorting, pagination, column
46
+ * visibility and ordering, and per-row action menus.
47
+ *
48
+ * Data handling: by default the table filters, sorts and pages `data` itself. Set `server-side`
49
+ * (or `total-rows`) to handle that on a server instead: the table then shows `data` as given
50
+ * and reports every change through `st-data-change`.
51
+ *
52
+ * Events (bubble, composed):
53
+ * - `st-data-change` `{ page, pageSize, search, sort, filters }` on any user change.
54
+ * - `st-row-click` the row object, when `clickable-rows` is set.
55
+ * - `st-row-action` `{ value, item, row }` when a row menu item is chosen.
56
+ */
19
57
  @customElement("st-data-table")
20
58
  export class StDataTable extends LitElement {
21
59
  @property({type: Array}) columns: DataTableColumn[] = [];
22
60
  @property({type: Array}) data: Record<string, any>[] = [];
61
+ /** Total rows on the server. Implies server-side handling. */
23
62
  @property({type: Number, attribute: "total-rows"}) totalRows = 0;
24
- @property({type: Number}) page = 1;
63
+ @property({type: Boolean, attribute: "server-side"}) serverSide = false;
64
+ @property({type: Number, reflect: true}) page = 1;
25
65
  @property({type: Number, attribute: "page-size"}) pageSize = 10;
26
- @property({type: Boolean, attribute: "multi-sort"}) multiSort = false;
66
+ @property({type: Array, attribute: false}) pageSizeOptions: number[] = [10, 25, 50, 100];
67
+
68
+ /** Free-text search across searchable columns. */
69
+ @property({type: String}) search = "";
70
+ @property({type: Array, attribute: false}) sort: SortConfig[] = [];
71
+ @property({type: Array, attribute: false}) filters: DataFilter[] = [];
72
+
73
+ /** Sort by several columns with Shift+click (on by default). */
74
+ @property({type: Boolean, attribute: "multi-sort"}) multiSort = true;
75
+ @property({type: Boolean, attribute: "single-sort"}) singleSort = false;
76
+
27
77
  @property({type: Boolean, reflect: true}) loading = false;
78
+ @property({type: Boolean, attribute: "clickable-rows"}) clickableRows = false;
79
+ @property({type: Boolean, reflect: true}) dense = false;
80
+ @property({type: Boolean, reflect: true}) striped = false;
81
+ @property({type: Boolean, reflect: true, attribute: "sticky-header"}) stickyHeader = false;
82
+ /**
83
+ * Reserves space for this many rows so the footer never moves between pages. Pages with fewer
84
+ * rows are padded with blank space; pages with more (a larger page size) scroll inside the
85
+ * table while the header and footer stay put. Cells stay on one line (long text is truncated).
86
+ */
87
+ @property({type: Number, attribute: "fixed-rows"}) fixedRows = 0;
88
+ @property({type: String}) caption = "";
89
+ @property({type: String, attribute: "empty-text"}) emptyText = "No data available.";
90
+ @property({type: String, attribute: "search-placeholder"}) searchPlaceholder = "Search…";
91
+ @property({type: Boolean, attribute: "hide-search"}) hideSearch = false;
92
+ @property({type: Boolean, attribute: "hide-column-menu"}) hideColumnMenu = false;
93
+ @property({type: Boolean, attribute: "fixed-columns"}) fixedColumns = false;
94
+
95
+ /** Menu items per row: an array, or a function returning items for a given row. */
96
+ @property({attribute: false}) rowActions?: RowActions;
97
+ /** Column used to name a row in assistive text, e.g. "Actions for Ada". Defaults to the first. */
98
+ @property({type: String, attribute: "row-label-key"}) rowLabelKey = "";
28
99
 
29
100
  @state() private internalColumns: DataTableColumn[] = [];
30
- @state() private sortState: SortConfig[] = [];
31
- @state() private searchQuery = "";
32
- @state() private showColumnMenu = false;
101
+ @state() private searchDraft = "";
102
+ @state() private filtersOpen = false;
103
+ @state() private announcement = "";
104
+ @state() private menuOpenFor: unknown = null;
33
105
 
34
- private searchTimeout?: number;
35
- private draggedColumnKey: string | null = null;
106
+ @query("st-context-menu") private menu!: StContextMenu;
107
+
108
+ private view: View = {rows: [], total: 0, page: 1, totalPages: 1, from: 0, to: 0};
109
+ private menuMode: "row" | "columns" = "row";
110
+ private debounce?: number;
111
+ private draggedKey: string | null = null;
112
+ private announceNext = false;
36
113
 
37
114
  static styles = [
38
115
  resetStyles,
@@ -41,231 +118,504 @@ export class StDataTable extends LitElement {
41
118
  display: block;
42
119
  width: 100%;
43
120
  font-family: var(--st-font-family, inherit);
121
+ color: var(--st-color-text);
44
122
  }
45
123
 
124
+ .sr-only {
125
+ position: absolute;
126
+ width: 1px;
127
+ height: 1px;
128
+ overflow: hidden;
129
+ clip: rect(0 0 0 0);
130
+ white-space: nowrap;
131
+ }
132
+
133
+ /* ---------- Toolbar ---------- */
46
134
  .toolbar {
47
135
  display: flex;
48
- justify-content: space-between;
136
+ flex-wrap: wrap;
49
137
  align-items: center;
50
- gap: var(--st-spacing-md);
138
+ gap: var(--st-spacing-sm);
51
139
  margin-bottom: var(--st-spacing-md);
52
- flex-wrap: wrap;
53
140
  }
54
141
 
55
- .search-wrapper {
56
- flex: 1;
57
- min-width: 250px;
142
+ .search {
58
143
  position: relative;
144
+ flex: 1;
145
+ min-width: 14rem;
146
+ }
147
+
148
+ .search st-icon {
149
+ position: absolute;
150
+ left: var(--st-spacing-sm);
151
+ top: 50%;
152
+ transform: translateY(-50%);
153
+ color: var(--st-color-text-muted);
154
+ pointer-events: none;
59
155
  }
60
156
 
61
157
  .search-input {
62
158
  width: 100%;
63
- padding: 0.5rem 0.75rem 0.5rem 2.25rem;
159
+ padding: var(--st-spacing-sm) var(--st-spacing-sm) var(--st-spacing-sm) 2.25rem;
64
160
  border: 1px solid var(--st-color-border);
65
161
  border-radius: var(--st-radius-md);
66
- background-color: var(--st-color-surface);
67
- color: var(--st-color-text);
162
+ background: var(--st-color-surface);
163
+ color: inherit;
164
+ font: inherit;
68
165
  font-size: var(--st-font-size-sm);
69
166
  }
70
167
 
71
- .search-input:disabled {
72
- opacity: 0.6;
73
- cursor: not-allowed;
168
+ .search-input:focus-visible,
169
+ select:focus-visible,
170
+ .filter-value:focus-visible {
171
+ outline: 2px solid var(--st-color-primary);
172
+ outline-offset: 1px;
74
173
  }
75
174
 
76
- .search-icon {
77
- position: absolute;
78
- left: 0.75rem;
79
- top: 50%;
80
- transform: translateY(-50%);
81
- width: 1rem;
82
- height: 1rem;
83
- color: var(--st-color-text-muted);
84
- }
85
-
86
- .columns-toggle-wrapper {
87
- position: relative;
175
+ .tool-btn,
176
+ .chip,
177
+ .link-btn,
178
+ .row-menu-btn,
179
+ .th-btn {
180
+ font: inherit;
181
+ color: inherit;
182
+ cursor: pointer;
88
183
  }
89
184
 
90
- .columns-menu {
91
- position: absolute;
92
- top: calc(100% + 0.5rem);
93
- right: 0;
94
- background: var(--st-color-surface);
185
+ .tool-btn {
186
+ display: inline-flex;
187
+ align-items: center;
188
+ gap: var(--st-spacing-xs);
189
+ padding: var(--st-spacing-sm) var(--st-spacing-md);
95
190
  border: 1px solid var(--st-color-border);
96
191
  border-radius: var(--st-radius-md);
97
- padding: var(--st-spacing-sm);
98
- min-width: 200px;
99
- box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
100
- z-index: 10;
192
+ background: var(--st-color-surface);
193
+ font-size: var(--st-font-size-sm);
194
+ }
195
+
196
+ .tool-btn:hover:not(:disabled) {
197
+ background: var(--st-color-surface-hover);
198
+ }
199
+
200
+ .tool-btn[aria-expanded="true"] {
201
+ border-color: var(--st-color-primary);
202
+ color: var(--st-color-primary-text);
203
+ }
204
+
205
+ .tool-btn:focus-visible,
206
+ .chip:focus-visible,
207
+ .link-btn:focus-visible,
208
+ .row-menu-btn:focus-visible,
209
+ .th-btn:focus-visible {
210
+ outline: 2px solid var(--st-color-primary);
211
+ outline-offset: 2px;
212
+ }
213
+
214
+ .count {
215
+ min-width: 1.25rem;
216
+ padding: 0 var(--st-spacing-xs);
217
+ border-radius: 999px;
218
+ background: var(--st-color-primary);
219
+ color: var(--st-color-on-primary);
220
+ font-size: var(--st-font-size-xs);
221
+ text-align: center;
222
+ }
223
+
224
+ /* ---------- Filter panel and chips ---------- */
225
+ .filter-panel {
101
226
  display: flex;
102
227
  flex-direction: column;
103
- gap: 0.25rem;
228
+ gap: var(--st-spacing-sm);
229
+ margin-bottom: var(--st-spacing-md);
230
+ padding: var(--st-spacing-md);
231
+ border: 1px solid var(--st-color-border);
232
+ border-radius: var(--st-radius-md);
233
+ background: var(--st-color-surface);
104
234
  }
105
235
 
106
- .column-option {
236
+ .filter-row {
107
237
  display: flex;
238
+ flex-wrap: wrap;
108
239
  align-items: center;
109
- gap: 0.5rem;
240
+ gap: var(--st-spacing-sm);
241
+ }
242
+
243
+ select,
244
+ .filter-value {
245
+ padding: var(--st-spacing-xs) var(--st-spacing-sm);
246
+ border: 1px solid var(--st-color-border);
247
+ border-radius: var(--st-radius-sm);
248
+ background: var(--st-color-bg);
249
+ color: inherit;
250
+ font: inherit;
110
251
  font-size: var(--st-font-size-sm);
111
- cursor: pointer;
112
- padding: 0.25rem;
113
252
  }
114
253
 
254
+ .filter-value {
255
+ min-width: 10rem;
256
+ flex: 1;
257
+ }
258
+
259
+ .panel-actions {
260
+ display: flex;
261
+ gap: var(--st-spacing-md);
262
+ }
263
+
264
+ .link-btn {
265
+ padding: 0;
266
+ border: 0;
267
+ background: none;
268
+ color: var(--st-color-primary-text);
269
+ font-size: var(--st-font-size-sm);
270
+ text-decoration: underline;
271
+ }
272
+
273
+ .chips {
274
+ display: flex;
275
+ flex-wrap: wrap;
276
+ align-items: center;
277
+ gap: var(--st-spacing-xs);
278
+ margin-bottom: var(--st-spacing-md);
279
+ }
280
+
281
+ .chip {
282
+ display: inline-flex;
283
+ align-items: center;
284
+ gap: var(--st-spacing-xs);
285
+ padding: 2px var(--st-spacing-sm);
286
+ border: 1px solid transparent;
287
+ border-radius: 999px;
288
+ background: var(--st-color-primary-bg);
289
+ color: var(--st-color-primary-text);
290
+ font-size: var(--st-font-size-xs);
291
+ }
292
+
293
+ .chip:hover {
294
+ border-color: var(--st-color-primary);
295
+ }
296
+
297
+ /* ---------- Table ---------- */
115
298
  .table-wrapper {
116
- width: 100%;
117
- overflow-x: auto;
299
+ overflow: auto;
118
300
  border: 1px solid var(--st-color-border);
119
301
  border-radius: var(--st-radius-md);
302
+ max-height: var(--st-table-max-height, none);
120
303
  }
121
304
 
122
305
  table {
123
306
  width: 100%;
124
307
  border-collapse: collapse;
125
- text-align: left;
126
308
  font-size: var(--st-font-size-sm);
127
- background-color: var(--st-color-surface);
309
+ }
310
+
311
+ caption {
312
+ padding: var(--st-spacing-sm) var(--st-spacing-md);
313
+ text-align: left;
314
+ font-weight: var(--st-font-weight-medium);
128
315
  }
129
316
 
130
317
  th {
131
- font-weight: 600;
132
318
  padding: var(--st-spacing-sm) var(--st-spacing-md);
133
319
  border-bottom: 2px solid var(--st-color-border);
320
+ background: var(--st-color-surface);
134
321
  color: var(--st-color-text-muted);
322
+ font-weight: var(--st-font-weight-semibold);
323
+ text-align: left;
324
+ white-space: nowrap;
325
+ }
326
+
327
+ :host([sticky-header]) th {
328
+ position: sticky;
329
+ top: 0;
330
+ z-index: 1;
331
+ }
332
+
333
+ /* ---------- Fixed row count ---------- */
334
+ .table-wrapper.fixed {
335
+ --_row-height: var(--st-data-table-row-height, 3.25rem);
336
+ }
337
+
338
+ :host([dense]) .table-wrapper.fixed {
339
+ --_row-height: var(--st-data-table-row-height, 2.5rem);
340
+ }
341
+
342
+ .fixed th,
343
+ .fixed td {
344
+ height: var(--_row-height);
345
+ padding-top: 0;
346
+ padding-bottom: 0;
347
+ overflow: hidden;
348
+ text-overflow: ellipsis;
135
349
  white-space: nowrap;
136
- background-color: var(--st-color-surface-muted, #f8fafc);
137
- user-select: none;
350
+ max-width: var(--st-data-table-cell-max-width, 24rem);
351
+ }
352
+
353
+ .fixed th {
354
+ position: sticky;
355
+ top: 0;
356
+ z-index: 1;
138
357
  }
139
358
 
140
- th[draggable="true"] {
141
- cursor: grab;
359
+ .fixed td.empty {
360
+ height: calc(var(--st-data-table-fixed-rows, 1) * var(--_row-height));
142
361
  }
143
362
 
144
- th[draggable="true"]:active {
145
- cursor: grabbing;
363
+ .fixed tr.spacer td {
364
+ border-bottom: 0;
146
365
  }
147
366
 
148
367
  th.drag-over {
149
- border-left: 2px solid var(--st-color-primary);
368
+ box-shadow: inset 3px 0 0 var(--st-color-primary);
150
369
  }
151
370
 
152
- .th-content {
153
- display: flex;
154
- align-items: center;
155
- gap: 0.5rem;
371
+ td {
372
+ padding: var(--st-spacing-md);
373
+ border-bottom: 1px solid var(--st-color-border);
156
374
  }
157
375
 
158
- .sortable-header {
159
- cursor: pointer;
160
- display: flex;
161
- align-items: center;
162
- gap: 0.25rem;
376
+ :host([dense]) td {
377
+ padding: var(--st-spacing-xs) var(--st-spacing-md);
163
378
  }
164
379
 
165
- .sortable-header:hover {
166
- color: var(--st-color-text);
380
+ tbody tr:last-child td {
381
+ border-bottom: 0;
167
382
  }
168
383
 
169
- .sort-icon {
170
- width: 1rem;
171
- height: 1rem;
172
- color: var(--st-color-text-muted);
384
+ :host([striped]) tbody tr:nth-of-type(even) {
385
+ background: var(--st-color-surface-muted);
173
386
  }
174
387
 
175
- .sort-icon.active {
176
- color: var(--st-color-primary);
388
+ .align-center {
389
+ text-align: center;
390
+ }
391
+ .align-end {
392
+ text-align: end;
177
393
  }
178
394
 
179
- .sort-priority {
180
- font-size: 0.7rem;
181
- background: var(--st-color-border);
182
- color: var(--st-color-text);
183
- border-radius: 50%;
184
- width: 1.1rem;
185
- height: 1.1rem;
395
+ tbody tr.clickable {
396
+ cursor: pointer;
397
+ }
398
+
399
+ tbody tr.clickable:hover,
400
+ tbody tr.menu-open {
401
+ background: var(--st-color-surface-hover);
402
+ }
403
+
404
+ tbody tr.clickable:focus-visible {
405
+ outline: 2px solid var(--st-color-primary);
406
+ outline-offset: -2px;
407
+ }
408
+
409
+ .th-btn {
186
410
  display: inline-flex;
187
411
  align-items: center;
188
- justify-content: center;
412
+ gap: var(--st-spacing-xs);
413
+ padding: 0;
414
+ border: 0;
415
+ background: none;
416
+ font-weight: inherit;
189
417
  }
190
418
 
191
- td {
192
- padding: var(--st-spacing-sm) var(--st-spacing-md);
193
- border-bottom: 1px solid var(--st-color-border);
194
- color: var(--st-color-text);
419
+ .th-btn.sorted {
420
+ color: var(--st-color-primary-text);
195
421
  }
196
422
 
197
- tbody tr:last-child td {
198
- border-bottom: none;
423
+ .sort-icon {
424
+ width: 0.875rem;
425
+ height: 0.875rem;
426
+ opacity: 0.45;
199
427
  }
200
428
 
201
- tbody tr:hover {
202
- background-color: var(--st-color-surface-hover, #f1f5f9);
429
+ .th-btn.sorted .sort-icon {
430
+ opacity: 1;
203
431
  }
204
432
 
205
- .pagination {
206
- display: flex;
207
- justify-content: space-between;
208
- align-items: center;
209
- padding: var(--st-spacing-md) 0;
210
- font-size: var(--st-font-size-sm);
433
+ .priority {
434
+ min-width: 1rem;
435
+ border-radius: 999px;
436
+ background: var(--st-color-primary-bg);
437
+ font-size: var(--st-font-size-xs);
438
+ text-align: center;
439
+ }
440
+
441
+ .actions-col {
442
+ width: 1%;
443
+ text-align: end;
444
+ }
445
+
446
+ .row-menu-btn {
447
+ display: inline-flex;
448
+ padding: var(--st-spacing-xs);
449
+ border: 0;
450
+ border-radius: var(--st-radius-sm);
451
+ background: none;
211
452
  color: var(--st-color-text-muted);
212
453
  }
213
454
 
214
- .pagination-controls {
215
- display: flex;
216
- gap: var(--st-spacing-sm);
455
+ .row-menu-btn:hover,
456
+ .row-menu-btn[aria-expanded="true"] {
457
+ background: var(--st-color-surface-hover);
458
+ color: var(--st-color-text);
217
459
  }
218
460
 
219
- .drag-handle {
220
- opacity: 0.3;
221
- cursor: grab;
461
+ .empty {
462
+ padding: var(--st-spacing-xl);
463
+ text-align: center;
464
+ color: var(--st-color-text-muted);
465
+ }
466
+
467
+ /* ---------- Footer ---------- */
468
+ .footer {
469
+ display: flex;
470
+ flex-wrap: wrap;
471
+ align-items: center;
472
+ justify-content: space-between;
473
+ gap: var(--st-spacing-md);
474
+ margin-top: var(--st-spacing-md);
475
+ color: var(--st-color-text-muted);
476
+ font-size: var(--st-font-size-sm);
222
477
  }
223
478
 
224
- .drag-handle:hover {
225
- opacity: 0.7;
479
+ .page-size {
480
+ display: inline-flex;
481
+ align-items: center;
482
+ gap: var(--st-spacing-sm);
226
483
  }
227
484
  `,
228
485
  ];
229
486
 
230
- override willUpdate(changedProperties: PropertyValues) {
231
- // 🚀 FIX: Properly sync dynamically updated columns while preserving user visibility states
232
- if (changedProperties.has("columns") && this.columns) {
233
- this.internalColumns = this.columns.map((newCol) => {
234
- const existing = this.internalColumns.find((ic) => ic.key === newCol.key);
235
- return {...newCol, hidden: existing ? existing.hidden : newCol.hidden};
487
+ // ---------------------------------------------------------------- lifecycle
488
+
489
+ protected willUpdate(changed: PropertyValues) {
490
+ if (changed.has("columns") && this.columns) {
491
+ // Keep the user's visibility and order when the columns array is replaced.
492
+ const previous = this.internalColumns;
493
+ const merged = this.columns.map((col) => {
494
+ const existing = previous.find((c) => c.key === col.key);
495
+ return {...col, hidden: existing ? existing.hidden : col.hidden};
236
496
  });
497
+ const order = previous.map((c) => c.key);
498
+ this.internalColumns = previous.length
499
+ ? [...merged].sort((a, b) => {
500
+ const ai = order.indexOf(a.key);
501
+ const bi = order.indexOf(b.key);
502
+ return (ai === -1 ? Infinity : ai) - (bi === -1 ? Infinity : bi);
503
+ })
504
+ : merged;
505
+ }
506
+ if (changed.has("search") && this.search !== this.searchDraft) this.searchDraft = this.search;
507
+ this.view = this.computeView();
508
+ if (!this.isServer && this.page !== this.view.page) this.page = this.view.page;
509
+ }
510
+
511
+ protected updated() {
512
+ this.applyFixedHeight();
513
+ if (this.announceNext) {
514
+ this.announceNext = false;
515
+ const n = this.view.total;
516
+ this.announcement = `${n} ${n === 1 ? "result" : "results"}`;
237
517
  }
238
518
  }
239
519
 
240
520
  connectedCallback() {
241
521
  super.connectedCallback();
242
- document.addEventListener("click", this.handleOutsideClick);
522
+ this.resizeObserver = new ResizeObserver(() => this.applyFixedHeight());
523
+ this.resizeObserver.observe(this);
524
+ }
525
+
526
+ /**
527
+ * Caps the scroll area at the header plus `fixedRows` real rows. Measuring (rather than
528
+ * calculating) keeps it exact whatever the font, density or borders.
529
+ */
530
+ private applyFixedHeight() {
531
+ const wrapper = this.renderRoot.querySelector<HTMLElement>(".table-wrapper");
532
+ if (!wrapper) return;
533
+ if (this.fixedRows <= 0) {
534
+ wrapper.style.maxHeight = "";
535
+ this.style.removeProperty("--st-data-table-fixed-rows");
536
+ return;
537
+ }
538
+ this.style.setProperty("--st-data-table-fixed-rows", String(this.fixedRows));
539
+ const head = wrapper.querySelector("thead")?.getBoundingClientRect().height ?? 0;
540
+ const rows = Array.from(wrapper.querySelectorAll<HTMLElement>("tbody tr:not(.spacer)"))
541
+ .filter((row) => !row.querySelector("td.empty"))
542
+ .slice(0, this.fixedRows);
543
+ if (!rows.length) return;
544
+ const rowHeight = rows[0].getBoundingClientRect().height;
545
+ wrapper.style.maxHeight = `${Math.ceil(head + rowHeight * this.fixedRows + 1)}px`;
243
546
  }
244
547
 
548
+ private resizeObserver?: ResizeObserver;
549
+
245
550
  disconnectedCallback() {
246
- document.removeEventListener("click", this.handleOutsideClick);
551
+ this.resizeObserver?.disconnect();
552
+ window.clearTimeout(this.debounce);
247
553
  super.disconnectedCallback();
248
554
  }
249
555
 
250
- private handleOutsideClick = (e: Event) => {
251
- const path = e.composedPath();
252
- const toggleWrapper = this.shadowRoot?.querySelector(".columns-toggle-wrapper");
556
+ // --------------------------------------------------------------- derivations
557
+
558
+ private get isServer() {
559
+ return this.serverSide || this.totalRows > 0;
560
+ }
561
+
562
+ private get canMultiSort() {
563
+ return this.multiSort && !this.singleSort;
564
+ }
565
+
566
+ private get visibleColumns() {
567
+ return this.internalColumns.filter((c) => !c.hidden);
568
+ }
569
+
570
+ private get filterColumns() {
571
+ return this.internalColumns.filter((c) => c.filter);
572
+ }
253
573
 
254
- // 🚀 THE FIX: Use composedPath() to pierce the Shadow DOM and check if
255
- // the click originated from inside the wrapper element.
256
- if (toggleWrapper && !path.includes(toggleWrapper)) {
257
- this.showColumnMenu = false;
574
+ private get activeFilters() {
575
+ return this.filters.filter((f) => isFilterComplete(f, this.internalColumns));
576
+ }
577
+
578
+ private get hasQuery() {
579
+ return this.activeFilters.length > 0 || this.search.trim() !== "";
580
+ }
581
+
582
+ private computeView(): View {
583
+ if (this.isServer) {
584
+ const total = this.totalRows > 0 ? this.totalRows : this.data.length;
585
+ const totalPages = Math.max(1, Math.ceil(total / Math.max(1, this.pageSize)));
586
+ const from = this.data.length ? (this.page - 1) * this.pageSize + 1 : 0;
587
+ return {
588
+ rows: this.data,
589
+ total,
590
+ page: this.page,
591
+ totalPages,
592
+ from,
593
+ to: this.data.length ? from + this.data.length - 1 : 0,
594
+ };
258
595
  }
259
- };
596
+ const columns = this.internalColumns;
597
+ let rows = searchRows(this.data, this.search, columns);
598
+ rows = applyFilters(rows, this.filters, columns);
599
+ rows = sortRows(rows, this.sort, columns);
600
+ return paginate(rows, this.page, this.pageSize);
601
+ }
260
602
 
261
- private emitChange() {
603
+ private optionsFor(column: DataTableColumn) {
604
+ return column.options ?? deriveOptions(this.data, column);
605
+ }
606
+
607
+ // ------------------------------------------------------------------- events
608
+
609
+ private emitChange(announce = true) {
610
+ this.announceNext = announce;
262
611
  this.dispatchEvent(
263
612
  new CustomEvent("st-data-change", {
264
613
  detail: {
265
614
  page: this.page,
266
615
  pageSize: this.pageSize,
267
- search: this.searchQuery,
268
- sort: this.sortState,
616
+ search: this.search,
617
+ sort: this.sort,
618
+ filters: this.activeFilters,
269
619
  },
270
620
  bubbles: true,
271
621
  composed: true,
@@ -273,350 +623,590 @@ export class StDataTable extends LitElement {
273
623
  );
274
624
  }
275
625
 
276
- private handleSearchInput(e: Event) {
277
- if (this.loading) return;
278
- const val = (e.target as HTMLInputElement).value;
279
- window.clearTimeout(this.searchTimeout);
626
+ private emitDebounced() {
627
+ window.clearTimeout(this.debounce);
628
+ this.debounce = window.setTimeout(() => this.emitChange(), DEBOUNCE_MS);
629
+ }
280
630
 
281
- this.searchTimeout = window.setTimeout(() => {
282
- this.searchQuery = val;
631
+ private onSearchInput(e: Event) {
632
+ if (this.loading) return;
633
+ this.searchDraft = (e.target as HTMLInputElement).value;
634
+ window.clearTimeout(this.debounce);
635
+ this.debounce = window.setTimeout(() => {
636
+ this.search = this.searchDraft;
283
637
  this.page = 1;
284
638
  this.emitChange();
285
- }, 350);
639
+ }, DEBOUNCE_MS);
286
640
  }
287
641
 
288
- private handleSort(key: string, e: MouseEvent) {
642
+ private clearSearch() {
643
+ this.searchDraft = "";
644
+ this.search = "";
645
+ this.page = 1;
646
+ this.emitChange();
647
+ }
648
+
649
+ private onSort(key: string, e: MouseEvent) {
289
650
  if (this.loading) return;
290
- const isShift = e.shiftKey && this.multiSort;
291
- const existingIndex = this.sortState.findIndex((s) => s.key === key);
292
- let newDirection: "asc" | "desc" | null = "asc";
651
+ this.sort = nextSort(this.sort, key, e.shiftKey && this.canMultiSort);
652
+ this.page = 1;
653
+ this.emitChange();
654
+ }
293
655
 
294
- if (existingIndex !== -1) {
295
- newDirection = this.sortState[existingIndex].direction === "asc" ? "desc" : null;
296
- }
656
+ private removeSort(key: string) {
657
+ this.sort = this.sort.filter((s) => s.key !== key);
658
+ this.page = 1;
659
+ this.emitChange();
660
+ }
297
661
 
298
- let newSortState = [...this.sortState];
662
+ private goToPage(page: number) {
663
+ if (this.loading || page === this.page) return;
664
+ this.page = page;
665
+ this.emitChange(false);
666
+ }
299
667
 
300
- if (isShift) {
301
- if (newDirection) {
302
- if (existingIndex !== -1) {
303
- newSortState[existingIndex].direction = newDirection;
304
- } else {
305
- newSortState.push({key, direction: newDirection});
306
- }
307
- } else {
308
- newSortState.splice(existingIndex, 1);
309
- }
310
- } else {
311
- if (newDirection) {
312
- newSortState = [{key, direction: newDirection}];
313
- } else {
314
- newSortState = [];
315
- }
316
- }
668
+ private onPageSize(e: Event) {
669
+ this.pageSize = Number((e.target as HTMLSelectElement).value);
670
+ this.page = 1;
671
+ this.emitChange(false);
672
+ }
317
673
 
318
- this.sortState = newSortState;
674
+ private clearAll() {
675
+ this.searchDraft = "";
676
+ this.search = "";
677
+ this.filters = [];
319
678
  this.page = 1;
320
679
  this.emitChange();
321
680
  }
322
681
 
323
- private get totalPages() {
324
- return Math.max(1, Math.ceil(this.totalRows / this.pageSize));
682
+ // ------------------------------------------------------------------ filters
683
+
684
+ private addFilter() {
685
+ const column = this.filterColumns[0];
686
+ if (!column) return;
687
+ this.filters = [...this.filters, {key: column.key, op: defaultOperator(column.filter!), value: ""}];
688
+ this.filtersOpen = true;
325
689
  }
326
690
 
327
- private prevPage() {
328
- if (!this.loading && this.page > 1) {
329
- this.page--;
330
- this.emitChange();
331
- }
691
+ private updateFilter(index: number, patch: Partial<DataFilter>, debounce = false) {
692
+ this.filters = this.filters.map((f, i) => (i === index ? {...f, ...patch} : f));
693
+ this.page = 1;
694
+ if (debounce) this.emitDebounced();
695
+ else this.emitChange();
332
696
  }
333
697
 
334
- private nextPage() {
335
- if (!this.loading && this.page < this.totalPages) {
336
- this.page++;
337
- this.emitChange();
338
- }
698
+ private onFilterColumn(index: number, key: string) {
699
+ const column = this.internalColumns.find((c) => c.key === key);
700
+ if (!column?.filter) return;
701
+ this.updateFilter(index, {key, op: defaultOperator(column.filter), value: ""});
339
702
  }
340
703
 
341
- private toggleColumnVisibility(key: string) {
342
- this.internalColumns = this.internalColumns.map((col) =>
343
- col.key === key ? {...col, hidden: !col.hidden} : col,
344
- );
704
+ private onFilterOperator(index: number, op: DataFilter["op"]) {
705
+ const column = this.internalColumns.find((c) => c.key === this.filters[index].key);
706
+ const info = operatorsFor(column!.filter!).find((o) => o.op === op);
707
+ this.updateFilter(index, {op, value: info?.needsValue ? this.filters[index].value : ""});
708
+ }
709
+
710
+ private removeFilter(index: number) {
711
+ this.filters = this.filters.filter((_, i) => i !== index);
712
+ this.page = 1;
713
+ this.emitChange();
714
+ }
715
+
716
+ // ------------------------------------------------------------------ columns
717
+
718
+ private columnItems(): MenuItem[] {
719
+ const visible = this.visibleColumns.length;
720
+ return this.internalColumns.map((c) => ({
721
+ label: c.header,
722
+ value: c.key,
723
+ checked: !c.hidden,
724
+ keepOpen: true,
725
+ // Always keep at least one column on screen.
726
+ disabled: !c.hidden && visible === 1,
727
+ }));
728
+ }
729
+
730
+ private openColumnMenu(e: MouseEvent) {
731
+ this.menuMode = "columns";
732
+ this.menuOpenFor = null;
733
+ this.menu.label = "Columns";
734
+ this.menu.items = this.columnItems();
735
+ this.menu.showAt(e.currentTarget as HTMLElement);
736
+ }
737
+
738
+ private toggleColumn(key: string) {
739
+ this.internalColumns = this.internalColumns.map((c) => (c.key === key ? {...c, hidden: !c.hidden} : c));
740
+ this.menu.items = this.columnItems();
741
+ }
742
+
743
+ private moveColumn(from: string, to: string) {
744
+ if (from === to) return;
745
+ const cols = [...this.internalColumns];
746
+ const fromIndex = cols.findIndex((c) => c.key === from);
747
+ const toIndex = cols.findIndex((c) => c.key === to);
748
+ if (fromIndex < 0 || toIndex < 0) return;
749
+ const [moved] = cols.splice(fromIndex, 1);
750
+ cols.splice(toIndex, 0, moved);
751
+ this.internalColumns = cols;
752
+ }
753
+
754
+ private onHeaderKeydown(e: KeyboardEvent, key: string) {
755
+ if (this.fixedColumns || !e.altKey || (e.key !== "ArrowLeft" && e.key !== "ArrowRight")) return;
756
+ e.preventDefault();
757
+ const visible = this.visibleColumns;
758
+ const index = visible.findIndex((c) => c.key === key);
759
+ const target = visible[index + (e.key === "ArrowLeft" ? -1 : 1)];
760
+ if (target) {
761
+ this.moveColumn(key, target.key);
762
+ this.updateComplete.then(() =>
763
+ this.renderRoot.querySelector<HTMLElement>(`[data-col="${key}"] .th-btn, [data-col="${key}"]`)?.focus(),
764
+ );
765
+ }
345
766
  }
346
767
 
347
- private handleDragStart(e: DragEvent, key: string) {
348
- this.draggedColumnKey = key;
768
+ private onDragStart(e: DragEvent, key: string) {
769
+ this.draggedKey = key;
349
770
  if (e.dataTransfer) {
350
771
  e.dataTransfer.effectAllowed = "move";
351
772
  e.dataTransfer.setData("text/plain", key);
352
773
  }
353
774
  }
354
775
 
355
- private handleDragOver(e: DragEvent) {
776
+ private onDragOver(e: DragEvent) {
356
777
  e.preventDefault();
357
- if (e.dataTransfer) e.dataTransfer.dropEffect = "move";
358
- const target = (e.target as HTMLElement).closest("th");
359
- if (target) target.classList.add("drag-over");
778
+ (e.currentTarget as HTMLElement).classList.add("drag-over");
360
779
  }
361
780
 
362
- private handleDragLeave(e: DragEvent) {
363
- const target = (e.target as HTMLElement).closest("th");
364
- if (target) target.classList.remove("drag-over");
781
+ private onDrop(e: DragEvent, key: string) {
782
+ e.preventDefault();
783
+ (e.currentTarget as HTMLElement).classList.remove("drag-over");
784
+ if (this.draggedKey) this.moveColumn(this.draggedKey, key);
785
+ this.draggedKey = null;
365
786
  }
366
787
 
367
- private handleDrop(e: DragEvent, targetKey: string) {
368
- e.preventDefault();
369
- const targetEl = (e.target as HTMLElement).closest("th");
370
- if (targetEl) targetEl.classList.remove("drag-over");
788
+ // --------------------------------------------------------------- row actions
371
789
 
372
- if (!this.draggedColumnKey || this.draggedColumnKey === targetKey) return;
790
+ private itemsFor(row: any): MenuItem[] {
791
+ if (!this.rowActions) return [];
792
+ return typeof this.rowActions === "function" ? this.rowActions(row) : this.rowActions;
793
+ }
373
794
 
374
- const newCols = [...this.internalColumns];
375
- const fromIndex = newCols.findIndex((c) => c.key === this.draggedColumnKey);
376
- const toIndex = newCols.findIndex((c) => c.key === targetKey);
795
+ private rowLabel(row: any) {
796
+ const column =
797
+ this.internalColumns.find((c) => c.key === this.rowLabelKey) ?? this.internalColumns[0];
798
+ return column ? String(cellValue(row, column) ?? "") : "";
799
+ }
377
800
 
378
- const [movedCol] = newCols.splice(fromIndex, 1);
379
- newCols.splice(toIndex, 0, movedCol);
801
+ private openRowMenu(row: any, target: {x: number; y: number} | HTMLElement) {
802
+ const items = this.itemsFor(row);
803
+ if (!items.length) return;
804
+ this.menuMode = "row";
805
+ this.menuOpenFor = row;
806
+ this.menu.label = `Actions for ${this.rowLabel(row)}`.trim();
807
+ this.menu.items = items;
808
+ if (target instanceof HTMLElement) this.menu.showAt(target, row);
809
+ else this.menu.show(target.x, target.y, row);
810
+ }
380
811
 
381
- this.internalColumns = newCols;
382
- this.draggedColumnKey = null;
812
+ private onRowContextMenu(e: MouseEvent, row: any) {
813
+ if (!this.rowActions || this.loading || !this.itemsFor(row).length) return;
814
+ e.preventDefault();
815
+ this.openRowMenu(row, {x: e.clientX, y: e.clientY});
383
816
  }
384
817
 
385
- private renderSortIcon(key: string) {
386
- const sortIndex = this.sortState.findIndex((s) => s.key === key);
387
- if (sortIndex === -1) {
388
- return html`<svg
389
- class="sort-icon"
390
- viewBox="0 0 24 24"
391
- fill="none"
392
- stroke="currentColor"
393
- stroke-width="2"
394
- stroke-linecap="round"
395
- stroke-linejoin="round"
396
- >
397
- <path d="m7 15 5 5 5-5" />
398
- <path d="m7 9 5-5 5 5" />
399
- </svg>`;
818
+ private onMenuSelect(e: CustomEvent<{item: MenuItem; value: string; context: unknown}>) {
819
+ e.stopPropagation();
820
+ if (this.menuMode === "columns") {
821
+ this.toggleColumn(e.detail.value);
822
+ return;
400
823
  }
824
+ this.dispatchEvent(
825
+ new CustomEvent("st-row-action", {
826
+ detail: {value: e.detail.value, item: e.detail.item, row: e.detail.context},
827
+ bubbles: true,
828
+ composed: true,
829
+ }),
830
+ );
831
+ }
401
832
 
402
- const sortConfig = this.sortState[sortIndex];
403
- const icon =
404
- sortConfig.direction === "asc"
405
- ? html`<svg
406
- class="sort-icon active"
407
- viewBox="0 0 24 24"
408
- fill="none"
409
- stroke="currentColor"
410
- stroke-width="2"
411
- stroke-linecap="round"
412
- stroke-linejoin="round"
413
- >
414
- <path d="m18 15-6-6-6 6" />
415
- </svg>`
416
- : html`<svg
417
- class="sort-icon active"
418
- viewBox="0 0 24 24"
419
- fill="none"
420
- stroke="currentColor"
421
- stroke-width="2"
422
- stroke-linecap="round"
423
- stroke-linejoin="round"
424
- >
425
- <path d="m6 9 6 6 6-6" />
426
- </svg>`;
427
-
428
- const priority =
429
- this.sortState.length > 1 && this.multiSort
430
- ? html`<span class="sort-priority">${sortIndex + 1}</span>`
431
- : "";
833
+ private onRowClick(row: any) {
834
+ if (!this.clickableRows || this.loading) return;
835
+ this.dispatchEvent(new CustomEvent("st-row-click", {detail: row, bubbles: true, composed: true}));
836
+ }
432
837
 
433
- return html`${icon}${priority}`;
838
+ private onRowKeydown(e: KeyboardEvent, row: any) {
839
+ if (e.target !== e.currentTarget || (e.key !== "Enter" && e.key !== " ")) return;
840
+ e.preventDefault();
841
+ this.onRowClick(row);
434
842
  }
435
843
 
436
- render() {
437
- const visibleColumns = this.internalColumns.filter((c) => !c.hidden);
438
- const skeletonRowsCount = Math.min(this.pageSize, 5);
844
+ // ------------------------------------------------------------------- render
439
845
 
846
+ private renderToolbar() {
847
+ const filterCount = this.activeFilters.length;
440
848
  return html`
441
849
  <div class="toolbar">
442
- <div class="search-wrapper">
443
- <svg
444
- class="search-icon"
445
- aria-hidden="true"
446
- viewBox="0 0 24 24"
447
- fill="none"
448
- stroke="currentColor"
449
- stroke-width="2"
450
- stroke-linecap="round"
451
- stroke-linejoin="round"
452
- >
453
- <circle cx="11" cy="11" r="8"></circle>
454
- <line x1="21" y1="21" x2="16.65" y2="16.65"></line>
455
- </svg>
456
- <input
457
- type="text"
458
- class="search-input"
459
- placeholder="Search..."
460
- aria-label="Search data table"
461
- spellcheck="false"
462
- autocapitalize="none"
463
- ?disabled=${this.loading}
464
- @input=${this.handleSearchInput}
465
- />
466
- </div>
467
-
468
- <div class="columns-toggle-wrapper">
469
- <st-button
470
- variant="secondary"
471
- size="sm"
472
- aria-haspopup="menu"
473
- aria-expanded=${this.showColumnMenu}
474
- @click=${() => (this.showColumnMenu = !this.showColumnMenu)}
475
- >
476
- Columns
477
- </st-button>
478
-
479
- ${this.showColumnMenu
480
- ? html`
481
- <div class="columns-menu" role="menu">
482
- ${this.internalColumns.map(
483
- (col) => html`
484
- <label
485
- class="column-option"
486
- role="menuitemcheckbox"
487
- aria-checked=${!col.hidden}
488
- >
489
- <input
490
- type="checkbox"
491
- .checked=${!col.hidden}
492
- @change=${() => this.toggleColumnVisibility(col.key)}
493
- />
494
- ${col.header}
495
- </label>
496
- `,
497
- )}
498
- </div>
499
- `
500
- : ""}
501
- </div>
850
+ ${this.hideSearch
851
+ ? nothing
852
+ : html`
853
+ <div class="search" role="search">
854
+ <st-icon name="search" size="sm" aria-hidden="true"></st-icon>
855
+ <input
856
+ class="search-input"
857
+ type="search"
858
+ spellcheck="false"
859
+ autocapitalize="none"
860
+ autocomplete="off"
861
+ aria-label="Search table"
862
+ placeholder=${this.searchPlaceholder}
863
+ .value=${this.searchDraft}
864
+ ?disabled=${this.loading}
865
+ @input=${this.onSearchInput}
866
+ />
867
+ </div>
868
+ `}
869
+ ${this.filterColumns.length
870
+ ? html`
871
+ <button
872
+ class="tool-btn filters-btn"
873
+ type="button"
874
+ aria-expanded=${this.filtersOpen ? "true" : "false"}
875
+ aria-controls="filter-panel"
876
+ @click=${() => {
877
+ this.filtersOpen = !this.filtersOpen;
878
+ if (this.filtersOpen && !this.filters.length) this.addFilter();
879
+ }}
880
+ >
881
+ <st-icon name="filter" size="sm" aria-hidden="true"></st-icon>
882
+ Filters
883
+ ${filterCount
884
+ ? html`<span class="count" aria-label="${filterCount} active">${filterCount}</span>`
885
+ : nothing}
886
+ </button>
887
+ `
888
+ : nothing}
889
+ ${this.hideColumnMenu
890
+ ? nothing
891
+ : html`
892
+ <button
893
+ class="tool-btn columns-btn"
894
+ type="button"
895
+ aria-haspopup="menu"
896
+ @click=${this.openColumnMenu}
897
+ >
898
+ <st-icon name="table" size="sm" aria-hidden="true"></st-icon>
899
+ Columns
900
+ </button>
901
+ `}
502
902
  </div>
903
+ `;
904
+ }
503
905
 
504
- <div class="table-wrapper">
505
- <table aria-busy=${this.loading}>
506
- <thead>
507
- <tr>
508
- ${visibleColumns.map((col) => {
509
- const sortItem = this.sortState.find((s) => s.key === col.key);
510
- const ariaSort = sortItem
511
- ? sortItem.direction === "asc"
512
- ? "ascending"
513
- : "descending"
514
- : "none";
515
-
516
- return html`
517
- <th
518
- scope="col"
519
- aria-sort=${ifDefined(col.sortable ? ariaSort : undefined)}
520
- draggable="true"
521
- @dragstart=${(e: DragEvent) => this.handleDragStart(e, col.key)}
522
- @dragover=${this.handleDragOver}
523
- @dragleave=${this.handleDragLeave}
524
- @drop=${(e: DragEvent) => this.handleDrop(e, col.key)}
525
- >
526
- <div class="th-content">
527
- <svg
528
- class="drag-handle"
529
- aria-hidden="true"
530
- width="12"
531
- height="12"
532
- viewBox="0 0 24 24"
533
- fill="none"
534
- stroke="currentColor"
535
- stroke-width="2"
536
- stroke-linecap="round"
537
- stroke-linejoin="round"
538
- >
539
- <circle cx="9" cy="12" r="1" />
540
- <circle cx="9" cy="5" r="1" />
541
- <circle cx="9" cy="19" r="1" />
542
- <circle cx="15" cy="12" r="1" />
543
- <circle cx="15" cy="5" r="1" />
544
- <circle cx="15" cy="19" r="1" />
545
- </svg>
546
-
547
- <div
548
- class=${classMap({"sortable-header": !!col.sortable})}
549
- @click=${(e: MouseEvent) => col.sortable && this.handleSort(col.key, e)}
906
+ private renderFilterPanel() {
907
+ if (!this.filtersOpen || !this.filterColumns.length) return nothing;
908
+ return html`
909
+ <div class="filter-panel" id="filter-panel" role="group" aria-label="Filters">
910
+ ${this.filters.map((filter, index) => {
911
+ const column = this.internalColumns.find((c) => c.key === filter.key);
912
+ const type = column?.filter;
913
+ if (!column || !type) return nothing;
914
+ const operator = operatorsFor(type).find((o) => o.op === filter.op);
915
+ return html`
916
+ <div class="filter-row" role="group" aria-label="Filter ${index + 1}">
917
+ <select
918
+ aria-label="Filter ${index + 1} column"
919
+ @change=${(e: Event) => this.onFilterColumn(index, (e.target as HTMLSelectElement).value)}
920
+ >
921
+ ${this.filterColumns.map(
922
+ (c) => html`<option value=${c.key} .selected=${c.key === filter.key}>${c.header}</option>`,
923
+ )}
924
+ </select>
925
+ <select
926
+ aria-label="Filter ${index + 1} condition"
927
+ @change=${(e: Event) =>
928
+ this.onFilterOperator(index, (e.target as HTMLSelectElement).value as DataFilter["op"])}
929
+ >
930
+ ${operatorsFor(type).map(
931
+ (o) => html`<option value=${o.op} .selected=${o.op === filter.op}>${o.label}</option>`,
932
+ )}
933
+ </select>
934
+ ${operator?.needsValue
935
+ ? type === "select"
936
+ ? html`
937
+ <select
938
+ class="filter-value"
939
+ aria-label="Filter ${index + 1} value"
940
+ @change=${(e: Event) =>
941
+ this.updateFilter(index, {value: (e.target as HTMLSelectElement).value})}
550
942
  >
551
- ${col.header} ${col.sortable ? this.renderSortIcon(col.key) : ""}
552
- </div>
553
- </div>
554
- </th>
555
- `;
556
- })}
557
- </tr>
558
- </thead>
559
-
560
- <tbody aria-live="polite">
561
- ${this.loading
562
- ? html`
563
- ${Array.from({length: skeletonRowsCount}).map(
564
- () => html`
565
- <tr>
566
- ${visibleColumns.map(
567
- () => html`
568
- <td><st-skeleton variant="text" width="75%"></st-skeleton></td>
569
- `,
943
+ <option value="" .selected=${!filter.value}>Choose…</option>
944
+ ${this.optionsFor(column).map(
945
+ (o) => html`<option value=${o.value} .selected=${o.value === filter.value}>${o.label}</option>`,
570
946
  )}
571
- </tr>
572
- `,
573
- )}
574
- `
575
- : this.data.length === 0
576
- ? html`
577
- <tr>
578
- <td
579
- colspan=${visibleColumns.length}
580
- style="text-align: center; padding: 2rem;"
581
- >
582
- No results found.
583
- </td>
584
- </tr>
585
- `
586
- : this.data.map(
587
- (row) => html`
588
- <tr>
589
- ${visibleColumns.map((col) => html`<td>${row[col.key] ?? ""}</td>`)}
590
- </tr>
591
- `,
592
- )}
593
- </tbody>
594
- </table>
947
+ </select>
948
+ `
949
+ : html`
950
+ <input
951
+ class="filter-value"
952
+ type=${type === "number" ? "number" : "text"}
953
+ aria-label="Filter ${index + 1} value"
954
+ placeholder="Value"
955
+ .value=${filter.value ?? ""}
956
+ @input=${(e: Event) =>
957
+ this.updateFilter(index, {value: (e.target as HTMLInputElement).value}, true)}
958
+ />
959
+ `
960
+ : nothing}
961
+ <button
962
+ class="row-menu-btn"
963
+ type="button"
964
+ aria-label="Remove filter ${index + 1}"
965
+ @click=${() => this.removeFilter(index)}
966
+ >
967
+ <st-icon name="x" size="sm" aria-hidden="true"></st-icon>
968
+ </button>
969
+ </div>
970
+ `;
971
+ })}
972
+ <div class="panel-actions">
973
+ <button class="link-btn add-filter" type="button" @click=${this.addFilter}>Add filter</button>
974
+ ${this.filters.length
975
+ ? html`<button class="link-btn" type="button" @click=${() => ((this.filters = []), this.emitChange())}>
976
+ Clear filters
977
+ </button>`
978
+ : nothing}
979
+ </div>
595
980
  </div>
981
+ `;
982
+ }
596
983
 
597
- <div class="pagination">
598
- <div class="pagination-info">
599
- Page ${this.page} of ${this.totalPages} (${this.totalRows} total items)
600
- </div>
601
- <div class="pagination-controls">
602
- <st-button
603
- variant="secondary"
604
- size="sm"
605
- ?disabled=${this.loading || this.page === 1}
606
- @click=${this.prevPage}
984
+ private renderChips() {
985
+ const filters = this.activeFilters;
986
+ const sorts = this.sort.filter((s) => this.internalColumns.some((c) => c.key === s.key));
987
+ if (!filters.length && !sorts.length && !this.search.trim()) return nothing;
988
+ return html`
989
+ <div class="chips" role="group" aria-label="Active filters and sorting">
990
+ ${this.search.trim()
991
+ ? html`<button class="chip" type="button" aria-label="Clear search: ${this.search}" @click=${this.clearSearch}>
992
+ Search: ${this.search} <st-icon name="x" size="sm" aria-hidden="true"></st-icon>
993
+ </button>`
994
+ : nothing}
995
+ ${filters.map((f) => {
996
+ const text = describeFilter(f, this.internalColumns);
997
+ return html`<button
998
+ class="chip filter-chip"
999
+ type="button"
1000
+ aria-label="Remove filter: ${text}"
1001
+ @click=${() => this.removeFilter(this.filters.indexOf(f))}
607
1002
  >
608
- Prev
609
- </st-button>
610
- <st-button
611
- variant="secondary"
612
- size="sm"
613
- ?disabled=${this.loading || this.page === this.totalPages}
614
- @click=${this.nextPage}
1003
+ ${text} <st-icon name="x" size="sm" aria-hidden="true"></st-icon>
1004
+ </button>`;
1005
+ })}
1006
+ ${sorts.map((s, i) => {
1007
+ const header = this.internalColumns.find((c) => c.key === s.key)!.header;
1008
+ const dir = s.direction === "asc" ? "ascending" : "descending";
1009
+ return html`<button
1010
+ class="chip sort-chip"
1011
+ type="button"
1012
+ aria-label="Remove sort: ${header} ${dir}"
1013
+ @click=${() => this.removeSort(s.key)}
615
1014
  >
616
- Next
617
- </st-button>
618
- </div>
1015
+ ${sorts.length > 1 ? `${i + 1}. ` : ""}${header} ${s.direction === "asc" ? "↑" : "↓"}
1016
+ <st-icon name="x" size="sm" aria-hidden="true"></st-icon>
1017
+ </button>`;
1018
+ })}
1019
+ ${filters.length + sorts.length + (this.search.trim() ? 1 : 0) > 1
1020
+ ? html`<button class="link-btn clear-all" type="button" @click=${() => ((this.sort = []), this.clearAll())}>
1021
+ Clear all
1022
+ </button>`
1023
+ : nothing}
1024
+ </div>
1025
+ `;
1026
+ }
1027
+
1028
+ private renderSortIcon(key: string) {
1029
+ const index = this.sort.findIndex((s) => s.key === key);
1030
+ if (index === -1) {
1031
+ return html`<svg class="sort-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1032
+ <path d="m7 15 5 5 5-5" /><path d="m7 9 5-5 5 5" />
1033
+ </svg>`;
1034
+ }
1035
+ const asc = this.sort[index].direction === "asc";
1036
+ return html`
1037
+ <svg class="sort-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1038
+ <path d=${asc ? "m18 15-6-6-6 6" : "m6 9 6 6 6-6"} />
1039
+ </svg>
1040
+ ${this.sort.length > 1 ? html`<span class="priority" aria-hidden="true">${index + 1}</span>` : nothing}
1041
+ `;
1042
+ }
1043
+
1044
+ private sortLabel(key: string) {
1045
+ const s = this.sort.find((x) => x.key === key);
1046
+ return s ? (s.direction === "asc" ? "ascending" : "descending") : "none";
1047
+ }
1048
+
1049
+ private renderHeader() {
1050
+ const hasActions = !!this.rowActions;
1051
+ return html`
1052
+ <thead>
1053
+ <tr>
1054
+ ${this.visibleColumns.map(
1055
+ (col) => html`
1056
+ <th
1057
+ scope="col"
1058
+ data-col=${col.key}
1059
+ class=${classMap({"align-center": col.align === "center", "align-end": col.align === "end"})}
1060
+ style=${ifDefined(col.width ? `width:${col.width}` : undefined)}
1061
+ aria-sort=${ifDefined(col.sortable ? this.sortLabel(col.key) : undefined)}
1062
+ draggable=${this.fixedColumns ? "false" : "true"}
1063
+ @dragstart=${(e: DragEvent) => this.onDragStart(e, col.key)}
1064
+ @dragover=${this.onDragOver}
1065
+ @dragleave=${(e: DragEvent) => (e.currentTarget as HTMLElement).classList.remove("drag-over")}
1066
+ @drop=${(e: DragEvent) => this.onDrop(e, col.key)}
1067
+ >
1068
+ ${col.sortable
1069
+ ? html`<button
1070
+ class=${classMap({"th-btn": true, sorted: this.sort.some((s) => s.key === col.key)})}
1071
+ type="button"
1072
+ title=${this.canMultiSort ? "Click to sort, Shift+click to add a column" : "Click to sort"}
1073
+ @click=${(e: MouseEvent) => this.onSort(col.key, e)}
1074
+ @keydown=${(e: KeyboardEvent) => this.onHeaderKeydown(e, col.key)}
1075
+ >
1076
+ ${col.header} ${this.renderSortIcon(col.key)}
1077
+ </button>`
1078
+ : col.header}
1079
+ </th>
1080
+ `,
1081
+ )}
1082
+ ${hasActions ? html`<th scope="col" class="actions-col"><span class="sr-only">Actions</span></th>` : nothing}
1083
+ </tr>
1084
+ </thead>
1085
+ `;
1086
+ }
1087
+
1088
+ private renderBody() {
1089
+ const colSpan = this.visibleColumns.length + (this.rowActions ? 1 : 0) || 1;
1090
+ if (this.loading) {
1091
+ return Array.from(
1092
+ {length: this.fixedRows > 0 ? this.fixedRows : Math.min(this.pageSize, SKELETON_ROWS)},
1093
+ () => html`<tr>
1094
+ ${Array.from({length: colSpan}, () => html`<td><st-skeleton variant="text" width="70%"></st-skeleton></td>`)}
1095
+ </tr>`,
1096
+ );
1097
+ }
1098
+ if (!this.view.rows.length) {
1099
+ return html`<tr>
1100
+ <td class="empty" colspan=${colSpan}>
1101
+ ${this.hasQuery ? "No results match your search or filters." : this.emptyText}
1102
+ ${this.hasQuery
1103
+ ? html` <button class="link-btn" type="button" @click=${() => ((this.sort = []), this.clearAll())}>Clear all</button>`
1104
+ : nothing}
1105
+ </td>
1106
+ </tr>`;
1107
+ }
1108
+ const spacers = this.renderSpacers(this.view.rows.length, colSpan);
1109
+ return [this.renderRows(colSpan), spacers];
1110
+ }
1111
+
1112
+ /** Blank rows that pad a short page up to `fixedRows` so the footer stays put. */
1113
+ private renderSpacers(count: number, colSpan: number) {
1114
+ if (this.fixedRows <= count) return nothing;
1115
+ return Array.from(
1116
+ {length: this.fixedRows - count},
1117
+ () => html`<tr class="spacer" aria-hidden="true"><td colspan=${colSpan}></td></tr>`,
1118
+ );
1119
+ }
1120
+
1121
+ private renderRows(_colSpan: number) {
1122
+ return this.view.rows.map((row) => {
1123
+ const hasMenu = !!this.rowActions && this.itemsFor(row).length > 0;
1124
+ return html`
1125
+ <tr
1126
+ class=${classMap({clickable: this.clickableRows, "menu-open": this.menuOpenFor === row})}
1127
+ tabindex=${ifDefined(this.clickableRows ? "0" : undefined)}
1128
+ @click=${() => this.onRowClick(row)}
1129
+ @keydown=${(e: KeyboardEvent) => this.onRowKeydown(e, row)}
1130
+ @contextmenu=${(e: MouseEvent) => this.onRowContextMenu(e, row)}
1131
+ >
1132
+ ${this.visibleColumns.map(
1133
+ (col) => html`
1134
+ <td class=${classMap({"align-center": col.align === "center", "align-end": col.align === "end"})}>
1135
+ ${col.render ? col.render(row) : (cellValue(row, col) ?? "")}
1136
+ </td>
1137
+ `,
1138
+ )}
1139
+ ${this.rowActions
1140
+ ? html`<td class="actions-col">
1141
+ ${hasMenu
1142
+ ? html`<button
1143
+ class="row-menu-btn"
1144
+ type="button"
1145
+ aria-label="Actions for ${this.rowLabel(row)}"
1146
+ aria-haspopup="menu"
1147
+ aria-expanded=${this.menuOpenFor === row ? "true" : "false"}
1148
+ @click=${(e: MouseEvent) => {
1149
+ e.stopPropagation();
1150
+ this.openRowMenu(row, e.currentTarget as HTMLElement);
1151
+ }}
1152
+ >
1153
+ <st-icon name="ellipsis" size="sm" aria-hidden="true"></st-icon>
1154
+ </button>`
1155
+ : nothing}
1156
+ </td>`
1157
+ : nothing}
1158
+ </tr>
1159
+ `;
1160
+ });
1161
+ }
1162
+
1163
+ private renderFooter() {
1164
+ const {total, from, to, totalPages, page} = this.view;
1165
+ const options = this.pageSizeOptions.includes(this.pageSize)
1166
+ ? this.pageSizeOptions
1167
+ : [...this.pageSizeOptions, this.pageSize].sort((a, b) => a - b);
1168
+ return html`
1169
+ <div class="footer">
1170
+ <span class="info">${this.loading ? "Loading…" : total ? `Showing ${from}–${to} of ${total}` : "No results"}</span>
1171
+ <label class="page-size">
1172
+ Rows per page
1173
+ <select class="page-size-select" ?disabled=${this.loading} @change=${this.onPageSize}>
1174
+ ${options.map((n) => html`<option value=${n} .selected=${n === this.pageSize}>${n}</option>`)}
1175
+ </select>
1176
+ </label>
1177
+ ${totalPages > 1
1178
+ ? html`<st-pagination
1179
+ page=${page}
1180
+ total-pages=${totalPages}
1181
+ @st-page-change=${(e: CustomEvent<{page: number}>) => {
1182
+ e.stopPropagation();
1183
+ this.goToPage(e.detail.page);
1184
+ }}
1185
+ ></st-pagination>`
1186
+ : nothing}
1187
+ </div>
1188
+ `;
1189
+ }
1190
+
1191
+ render() {
1192
+ return html`
1193
+ ${this.renderToolbar()} ${this.renderFilterPanel()} ${this.renderChips()}
1194
+ <div class=${classMap({"table-wrapper": true, fixed: this.fixedRows > 0})}>
1195
+ <table aria-busy=${this.loading ? "true" : "false"}>
1196
+ ${this.caption ? html`<caption>${this.caption}</caption>` : nothing}
1197
+ ${this.renderHeader()}
1198
+ <tbody>
1199
+ ${this.renderBody()}
1200
+ </tbody>
1201
+ </table>
619
1202
  </div>
1203
+ ${this.renderFooter()}
1204
+ <div class="sr-only" aria-live="polite" aria-atomic="true">${this.announcement}</div>
1205
+ <st-context-menu
1206
+ trigger="manual"
1207
+ @st-menu-select=${this.onMenuSelect}
1208
+ @st-menu-close=${() => (this.menuOpenFor = null)}
1209
+ ></st-context-menu>
620
1210
  `;
621
1211
  }
622
1212
  }