@kouji-ui/core 0.0.3 → 0.1.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 (534) hide show
  1. package/.turbo/turbo-build.log +6 -3
  2. package/CHANGELOG.md +177 -0
  3. package/ng-package.json +5 -1
  4. package/package.json +18 -6
  5. package/src/a11y/aria-describedby.ts +7 -3
  6. package/src/a11y/aria-labelledby.spec.ts +55 -0
  7. package/src/a11y/aria-labelledby.ts +38 -0
  8. package/src/a11y/focus-trap.ts +4 -2
  9. package/src/a11y/index.ts +1 -0
  10. package/src/a11y/live-region.ts +4 -2
  11. package/src/a11y/roving-tabindex.spec.ts +139 -0
  12. package/src/a11y/roving-tabindex.ts +56 -7
  13. package/src/a11y/visually-hidden.ts +4 -2
  14. package/src/accordion/accordion.context.ts +22 -0
  15. package/src/accordion/accordion.spec.ts +233 -23
  16. package/src/accordion/accordion.ts +311 -39
  17. package/src/accordion/index.ts +8 -2
  18. package/src/alert/alert.context.ts +28 -0
  19. package/src/alert/alert.spec.ts +217 -0
  20. package/src/alert/alert.ts +417 -0
  21. package/src/alert/config.ts +46 -0
  22. package/src/alert/index.ts +19 -0
  23. package/src/avatar/avatar-group.context.ts +45 -0
  24. package/src/avatar/avatar-group.spec.ts +358 -0
  25. package/src/avatar/avatar-group.ts +259 -0
  26. package/src/avatar/avatar.ts +17 -4
  27. package/src/avatar/index.ts +11 -1
  28. package/src/badge/badge.spec.ts +8 -0
  29. package/src/badge/badge.ts +24 -3
  30. package/src/breadcrumb/breadcrumb-current.ts +38 -0
  31. package/src/breadcrumb/breadcrumb-ellipsis.ts +71 -0
  32. package/src/breadcrumb/breadcrumb-item.ts +41 -0
  33. package/src/breadcrumb/breadcrumb-link.ts +44 -0
  34. package/src/breadcrumb/breadcrumb-list.ts +42 -0
  35. package/src/breadcrumb/breadcrumb-separator.ts +45 -0
  36. package/src/breadcrumb/breadcrumb.context.ts +45 -0
  37. package/src/breadcrumb/breadcrumb.spec.ts +354 -0
  38. package/src/breadcrumb/breadcrumb.ts +239 -0
  39. package/src/breadcrumb/config.ts +84 -0
  40. package/src/breadcrumb/index.ts +18 -0
  41. package/src/button/_examples/button.example.ts +71 -0
  42. package/src/button/_examples/button.finance.example.ts +83 -0
  43. package/src/button/_examples/button.retro.example.ts +91 -0
  44. package/src/button/_examples/button.sizes.example.ts +60 -0
  45. package/src/button/_examples/index.ts +6 -0
  46. package/src/button/button.spec.ts +139 -8
  47. package/src/button/button.ts +114 -16
  48. package/src/button/config.spec.ts +33 -0
  49. package/src/button/config.ts +45 -0
  50. package/src/button/index.ts +2 -1
  51. package/src/button-group/button-group.context.ts +28 -0
  52. package/src/button-group/button-group.spec.ts +99 -0
  53. package/src/button-group/button-group.ts +99 -0
  54. package/src/button-group/index.ts +2 -0
  55. package/src/calendar/calendar-day.ts +102 -0
  56. package/src/calendar/calendar-grid.ts +90 -0
  57. package/src/calendar/calendar-header.ts +65 -0
  58. package/src/calendar/calendar.context.ts +53 -0
  59. package/src/calendar/calendar.spec.ts +225 -0
  60. package/src/calendar/calendar.ts +233 -0
  61. package/src/calendar/date-utils.ts +225 -0
  62. package/src/calendar/index.ts +28 -0
  63. package/src/carousel/carousel.context.ts +81 -0
  64. package/src/carousel/carousel.spec.ts +337 -0
  65. package/src/carousel/carousel.ts +919 -0
  66. package/src/carousel/index.ts +22 -0
  67. package/src/cascade-select/cascade-select-option.ts +205 -0
  68. package/src/cascade-select/cascade-select-panel.ts +106 -0
  69. package/src/cascade-select/cascade-select-root.ts +265 -0
  70. package/src/cascade-select/cascade-select-sub-panel.ts +168 -0
  71. package/src/cascade-select/cascade-select-trigger.ts +44 -0
  72. package/src/cascade-select/cascade-select.context.ts +71 -0
  73. package/src/cascade-select/cascade-select.spec.ts +241 -0
  74. package/src/cascade-select/cascade-select.ts +5 -0
  75. package/src/cascade-select/index.ts +11 -0
  76. package/src/chart/chart.ts +6 -2
  77. package/src/chat/chat-avatar.ts +31 -0
  78. package/src/chat/chat-bubble.ts +49 -0
  79. package/src/chat/chat-footer.ts +67 -0
  80. package/src/chat/chat-header.ts +41 -0
  81. package/src/chat/chat-log.ts +43 -0
  82. package/src/chat/chat.context.ts +64 -0
  83. package/src/chat/chat.spec.ts +341 -0
  84. package/src/chat/chat.ts +191 -0
  85. package/src/chat/config.ts +53 -0
  86. package/src/chat/index.ts +21 -0
  87. package/src/checkbox/checkbox.ts +9 -2
  88. package/src/color-picker/color-picker.context.ts +74 -0
  89. package/src/color-picker/color-picker.spec.ts +241 -0
  90. package/src/color-picker/color-picker.ts +698 -0
  91. package/src/color-picker/color-picker.utils.ts +140 -0
  92. package/src/color-picker/index.ts +28 -0
  93. package/src/combobox/combobox-input.ts +143 -0
  94. package/src/combobox/combobox-listbox.ts +49 -0
  95. package/src/combobox/combobox-option.ts +43 -0
  96. package/src/combobox/combobox-root.ts +281 -0
  97. package/src/combobox/combobox.spec.ts +75 -0
  98. package/src/combobox/combobox.ts +4 -0
  99. package/src/combobox/index.ts +4 -0
  100. package/src/command-palette/command-empty.ts +36 -0
  101. package/src/command-palette/command-group.ts +21 -0
  102. package/src/command-palette/command-input.ts +70 -0
  103. package/src/command-palette/command-item.ts +60 -0
  104. package/src/command-palette/command-list.ts +25 -0
  105. package/src/command-palette/command-palette-dialog.ts +74 -0
  106. package/src/command-palette/command-palette-trigger.ts +64 -0
  107. package/src/command-palette/command-palette.filters.ts +53 -0
  108. package/src/command-palette/command-palette.spec.ts +349 -0
  109. package/src/command-palette/command-palette.ts +204 -0
  110. package/src/command-palette/command-separator.ts +18 -0
  111. package/src/command-palette/index.ts +15 -0
  112. package/src/confirm-popup/confirm-popup-action.ts +39 -0
  113. package/src/confirm-popup/confirm-popup-cancel.ts +38 -0
  114. package/src/confirm-popup/confirm-popup-content.ts +113 -0
  115. package/src/confirm-popup/confirm-popup-message.ts +36 -0
  116. package/src/confirm-popup/confirm-popup-trigger.ts +31 -0
  117. package/src/confirm-popup/confirm-popup.context.ts +41 -0
  118. package/src/confirm-popup/confirm-popup.spec.ts +337 -0
  119. package/src/confirm-popup/confirm-popup.ts +109 -0
  120. package/src/confirm-popup/index.ts +12 -0
  121. package/src/date-picker/date-picker-calendar.ts +65 -0
  122. package/src/date-picker/date-picker-trigger.ts +215 -0
  123. package/src/date-picker/date-picker.context.ts +50 -0
  124. package/src/date-picker/date-picker.spec.ts +117 -0
  125. package/src/date-picker/date-picker.ts +127 -0
  126. package/src/date-picker/index.ts +7 -0
  127. package/src/dialog/_examples/dialog.confirm.example.ts +145 -0
  128. package/src/dialog/_examples/dialog.example.ts +106 -0
  129. package/src/dialog/_examples/dialog.finance.example.ts +121 -0
  130. package/src/dialog/_examples/dialog.retro.example.ts +126 -0
  131. package/src/dialog/_examples/index.ts +6 -0
  132. package/src/dialog/dialog.ref.ts +42 -0
  133. package/src/dialog/dialog.service.ts +57 -167
  134. package/src/dialog/dialog.spec.ts +35 -120
  135. package/src/dialog/dialog.ts +17 -192
  136. package/src/dialog/index.ts +3 -9
  137. package/src/divider/divider.spec.ts +223 -0
  138. package/src/divider/divider.ts +137 -0
  139. package/src/divider/index.ts +5 -0
  140. package/src/drawer/drawer.ref.ts +63 -0
  141. package/src/drawer/drawer.service.ts +106 -0
  142. package/src/drawer/drawer.spec.ts +117 -0
  143. package/src/drawer/drawer.ts +122 -0
  144. package/src/drawer/index.ts +10 -0
  145. package/src/dropdown-menu/dropdown-menu-content.ts +258 -0
  146. package/src/dropdown-menu/dropdown-menu-group.ts +31 -0
  147. package/src/dropdown-menu/dropdown-menu-item.ts +51 -0
  148. package/src/dropdown-menu/dropdown-menu-label.ts +41 -0
  149. package/src/dropdown-menu/dropdown-menu-separator.ts +27 -0
  150. package/src/dropdown-menu/dropdown-menu-trigger.ts +140 -0
  151. package/src/dropdown-menu/dropdown-menu.spec.ts +265 -0
  152. package/src/dropdown-menu/dropdown-menu.ts +60 -0
  153. package/src/dropdown-menu/index.ts +15 -0
  154. package/src/example-components.ts +39 -30
  155. package/src/field/field-error.ts +49 -0
  156. package/src/field/field-group.ts +39 -0
  157. package/src/field/field-help.ts +47 -0
  158. package/src/field/field-label.ts +30 -0
  159. package/src/field/field.context.ts +28 -0
  160. package/src/field/field.spec.ts +179 -0
  161. package/src/field/field.ts +121 -0
  162. package/src/field/index.ts +6 -0
  163. package/src/file-upload/file-upload.context.ts +89 -0
  164. package/src/file-upload/file-upload.spec.ts +290 -0
  165. package/src/file-upload/file-upload.ts +639 -0
  166. package/src/file-upload/file-upload.types.ts +59 -0
  167. package/src/file-upload/index.ts +23 -0
  168. package/src/form/form-error-summary.ts +87 -0
  169. package/src/form/form-field.context.ts +3 -0
  170. package/src/form/form-field.spec.ts +25 -0
  171. package/src/form/form-field.ts +52 -0
  172. package/src/form/form.context.ts +52 -2
  173. package/src/form/form.spec.ts +155 -18
  174. package/src/form/form.ts +315 -35
  175. package/src/form/index.ts +11 -2
  176. package/src/icon/E2E.md +14 -0
  177. package/src/icon/icon.css +40 -0
  178. package/src/icon/icon.directive.spec.ts +198 -0
  179. package/src/icon/icon.directive.ts +113 -0
  180. package/src/icon/icon.mode.spec.ts +29 -0
  181. package/src/icon/icon.mode.ts +23 -0
  182. package/src/icon/icon.providers.spec.ts +63 -0
  183. package/src/icon/icon.providers.ts +47 -0
  184. package/src/icon/icon.resolver.spec.ts +96 -0
  185. package/src/icon/icon.resolver.ts +49 -0
  186. package/src/icon/icon.tokens.ts +88 -0
  187. package/src/icon/icon.types.ts +48 -0
  188. package/src/icon/index.ts +21 -0
  189. package/src/input/input.ts +12 -3
  190. package/src/input-group/index.ts +3 -0
  191. package/src/input-group/input-group-addon.ts +92 -0
  192. package/src/input-group/input-group.context.ts +27 -0
  193. package/src/input-group/input-group.spec.ts +186 -0
  194. package/src/input-group/input-group.ts +136 -0
  195. package/src/input-mask/index.ts +11 -0
  196. package/src/input-mask/input-mask.compile.ts +69 -0
  197. package/src/input-mask/input-mask.engine.ts +376 -0
  198. package/src/input-mask/input-mask.presets.ts +24 -0
  199. package/src/input-mask/input-mask.spec.ts +304 -0
  200. package/src/input-mask/input-mask.tokens.ts +49 -0
  201. package/src/input-mask/input-mask.ts +280 -0
  202. package/src/input-otp/index.ts +2 -0
  203. package/src/input-otp/input-otp.context.ts +41 -0
  204. package/src/input-otp/input-otp.spec.ts +308 -0
  205. package/src/input-otp/input-otp.ts +470 -0
  206. package/src/kbd/index.ts +1 -0
  207. package/src/kbd/kbd.spec.ts +127 -0
  208. package/src/kbd/kbd.ts +132 -0
  209. package/src/link/config.ts +44 -0
  210. package/src/link/index.ts +2 -0
  211. package/src/link/link.spec.ts +246 -0
  212. package/src/link/link.ts +226 -0
  213. package/src/list/index.ts +11 -0
  214. package/src/list/list.spec.ts +237 -0
  215. package/src/list/list.ts +277 -0
  216. package/src/menubar/index.ts +7 -0
  217. package/src/menubar/menubar-item.ts +204 -0
  218. package/src/menubar/menubar.context.ts +43 -0
  219. package/src/menubar/menubar.spec.ts +433 -0
  220. package/src/menubar/menubar.ts +309 -0
  221. package/src/number-input/index.ts +4 -0
  222. package/src/number-input/number-input-group.ts +64 -0
  223. package/src/number-input/number-input.context.ts +31 -0
  224. package/src/number-input/number-input.format.ts +148 -0
  225. package/src/number-input/number-input.spec.ts +170 -0
  226. package/src/number-input/number-input.ts +457 -0
  227. package/src/number-input/number-stepper.ts +193 -0
  228. package/src/overlay-badge/index.ts +7 -0
  229. package/src/overlay-badge/overlay-badge-content.ts +73 -0
  230. package/src/overlay-badge/overlay-badge.context.ts +37 -0
  231. package/src/overlay-badge/overlay-badge.spec.ts +220 -0
  232. package/src/overlay-badge/overlay-badge.ts +128 -0
  233. package/src/pagination/config.ts +82 -0
  234. package/src/pagination/index.ts +19 -0
  235. package/src/pagination/page-tokens.ts +78 -0
  236. package/src/pagination/pagination-ellipsis.ts +69 -0
  237. package/src/pagination/pagination-first.ts +84 -0
  238. package/src/pagination/pagination-info.ts +70 -0
  239. package/src/pagination/pagination-item.ts +81 -0
  240. package/src/pagination/pagination-last.ts +85 -0
  241. package/src/pagination/pagination-next.ts +86 -0
  242. package/src/pagination/pagination-previous.ts +90 -0
  243. package/src/pagination/pagination.context.ts +52 -0
  244. package/src/pagination/pagination.spec.ts +388 -0
  245. package/src/pagination/pagination.ts +288 -0
  246. package/src/password-input/index.ts +15 -0
  247. package/src/password-input/password-input.context.ts +65 -0
  248. package/src/password-input/password-input.scorer.ts +34 -0
  249. package/src/password-input/password-input.spec.ts +274 -0
  250. package/src/password-input/password-input.ts +488 -0
  251. package/src/popover/_examples/index.ts +5 -0
  252. package/src/popover/_examples/popover.example.ts +92 -0
  253. package/src/popover/_examples/popover.finance.example.ts +106 -0
  254. package/src/popover/_examples/popover.retro.example.ts +103 -0
  255. package/src/popover/index.ts +5 -12
  256. package/src/popover/popover-arrow.ts +30 -0
  257. package/src/popover/popover-close.ts +36 -0
  258. package/src/popover/popover-content.ts +41 -0
  259. package/src/popover/popover-title.ts +39 -0
  260. package/src/popover/popover-trigger.ts +40 -0
  261. package/src/popover/popover.spec.ts +51 -44
  262. package/src/popover/popover.ts +5 -124
  263. package/src/presets/bind-presets.spec.ts +50 -0
  264. package/src/presets/bind-presets.ts +43 -0
  265. package/src/presets/index.ts +3 -0
  266. package/src/presets/size.spec.ts +67 -0
  267. package/src/presets/size.ts +59 -0
  268. package/src/presets/variant.spec.ts +72 -0
  269. package/src/presets/variant.ts +62 -0
  270. package/src/primitives/directionality/directionality.spec.ts +112 -0
  271. package/src/primitives/directionality/directionality.ts +89 -0
  272. package/src/primitives/directionality/index.ts +1 -0
  273. package/src/primitives/forms/form-control.spec.ts +14 -6
  274. package/src/primitives/forms/form-control.ts +6 -2
  275. package/src/primitives/index.ts +3 -0
  276. package/src/primitives/interaction/disabled.ts +12 -5
  277. package/src/primitives/interaction/focus-ring.ts +4 -2
  278. package/src/primitives/list/filterable-list.spec.ts +114 -0
  279. package/src/primitives/list/filterable-list.ts +126 -0
  280. package/src/primitives/list/group.spec.ts +99 -0
  281. package/src/primitives/list/group.ts +139 -0
  282. package/src/primitives/list/index.ts +8 -0
  283. package/src/primitives/list/inject-helpers.ts +8 -0
  284. package/src/primitives/list/item.spec.ts +173 -0
  285. package/src/primitives/list/item.ts +222 -0
  286. package/src/primitives/list/navigator.spec.ts +286 -0
  287. package/src/primitives/list/navigator.ts +238 -0
  288. package/src/primitives/list/selection.spec.ts +214 -0
  289. package/src/primitives/list/selection.ts +339 -0
  290. package/src/primitives/list/tokens.ts +106 -0
  291. package/src/primitives/list/type-ahead.spec.ts +58 -0
  292. package/src/primitives/list/type-ahead.ts +46 -0
  293. package/src/primitives/overlay/backdrop.spec.ts +54 -0
  294. package/src/primitives/overlay/backdrop.ts +31 -0
  295. package/src/primitives/overlay/builder.spec.ts +38 -0
  296. package/src/primitives/overlay/builder.ts +151 -0
  297. package/src/primitives/overlay/container.ts +42 -0
  298. package/src/primitives/overlay/context.ts +14 -0
  299. package/src/primitives/overlay/controller.spec.ts +86 -0
  300. package/src/primitives/overlay/controller.ts +190 -0
  301. package/src/primitives/overlay/id.spec.ts +24 -0
  302. package/src/primitives/overlay/id.ts +17 -0
  303. package/src/primitives/overlay/index.ts +18 -1
  304. package/src/primitives/overlay/overlay.css +42 -0
  305. package/src/primitives/overlay/panel.spec.ts +52 -0
  306. package/src/primitives/overlay/panel.ts +102 -0
  307. package/src/primitives/overlay/stack.spec.ts +55 -0
  308. package/src/primitives/overlay/stack.ts +113 -0
  309. package/src/primitives/overlay/strategies/backdrop/blurred.spec.ts +11 -0
  310. package/src/primitives/overlay/strategies/backdrop/blurred.ts +5 -0
  311. package/src/primitives/overlay/strategies/backdrop/none.spec.ts +10 -0
  312. package/src/primitives/overlay/strategies/backdrop/none.ts +9 -0
  313. package/src/primitives/overlay/strategies/backdrop/solid.spec.ts +18 -0
  314. package/src/primitives/overlay/strategies/backdrop/solid.ts +23 -0
  315. package/src/primitives/overlay/strategies/focus-trap/inert-based.spec.ts +28 -0
  316. package/src/primitives/overlay/strategies/focus-trap/inert-based.ts +38 -0
  317. package/src/primitives/overlay/strategies/focus-trap/no-trap.spec.ts +9 -0
  318. package/src/primitives/overlay/strategies/focus-trap/no-trap.ts +8 -0
  319. package/src/primitives/overlay/strategies/focus-trap/tab-cycle.spec.ts +49 -0
  320. package/src/primitives/overlay/strategies/focus-trap/tab-cycle.ts +85 -0
  321. package/src/primitives/overlay/strategies/index.ts +34 -0
  322. package/src/primitives/overlay/strategies/live-announcer/_announce.ts +35 -0
  323. package/src/primitives/overlay/strategies/live-announcer/assertive.spec.ts +21 -0
  324. package/src/primitives/overlay/strategies/live-announcer/assertive.ts +11 -0
  325. package/src/primitives/overlay/strategies/live-announcer/polite.spec.ts +21 -0
  326. package/src/primitives/overlay/strategies/live-announcer/polite.ts +11 -0
  327. package/src/primitives/overlay/strategies/live-announcer/silent.spec.ts +12 -0
  328. package/src/primitives/overlay/strategies/live-announcer/silent.ts +5 -0
  329. package/src/primitives/overlay/strategies/mount/body-portal.spec.ts +40 -0
  330. package/src/primitives/overlay/strategies/mount/body-portal.ts +91 -0
  331. package/src/primitives/overlay/strategies/mount/in-container.spec.ts +43 -0
  332. package/src/primitives/overlay/strategies/mount/in-container.ts +39 -0
  333. package/src/primitives/overlay/strategies/mount/in-place.spec.ts +40 -0
  334. package/src/primitives/overlay/strategies/mount/in-place.ts +20 -0
  335. package/src/primitives/overlay/strategies/position/anchored-to.spec.ts +56 -0
  336. package/src/primitives/overlay/strategies/position/anchored-to.ts +248 -0
  337. package/src/primitives/overlay/strategies/position/corner.spec.ts +36 -0
  338. package/src/primitives/overlay/strategies/position/corner.ts +33 -0
  339. package/src/primitives/overlay/strategies/position/edge-sheet.spec.ts +32 -0
  340. package/src/primitives/overlay/strategies/position/edge-sheet.ts +36 -0
  341. package/src/primitives/overlay/strategies/position/in-place-sibling.spec.ts +25 -0
  342. package/src/primitives/overlay/strategies/position/in-place-sibling.ts +13 -0
  343. package/src/primitives/overlay/strategies/position/point-at.spec.ts +40 -0
  344. package/src/primitives/overlay/strategies/position/point-at.ts +34 -0
  345. package/src/primitives/overlay/strategies/position/viewport-centered.spec.ts +31 -0
  346. package/src/primitives/overlay/strategies/position/viewport-centered.ts +31 -0
  347. package/src/primitives/overlay/strategies/scroll-lock/css-clip.spec.ts +15 -0
  348. package/src/primitives/overlay/strategies/scroll-lock/css-clip.ts +39 -0
  349. package/src/primitives/overlay/strategies/scroll-lock/html-overflow.spec.ts +21 -0
  350. package/src/primitives/overlay/strategies/scroll-lock/html-overflow.ts +44 -0
  351. package/src/primitives/overlay/strategies/scroll-lock/none.spec.ts +9 -0
  352. package/src/primitives/overlay/strategies/scroll-lock/none.ts +5 -0
  353. package/src/primitives/overlay/strategies/trigger-event/on-click.spec.ts +32 -0
  354. package/src/primitives/overlay/strategies/trigger-event/on-click.ts +28 -0
  355. package/src/primitives/overlay/strategies/trigger-event/on-context-menu.spec.ts +45 -0
  356. package/src/primitives/overlay/strategies/trigger-event/on-context-menu.ts +70 -0
  357. package/src/primitives/overlay/strategies/trigger-event/on-focus-or-input.spec.ts +35 -0
  358. package/src/primitives/overlay/strategies/trigger-event/on-focus-or-input.ts +34 -0
  359. package/src/primitives/overlay/strategies/trigger-event/on-focus.spec.ts +64 -0
  360. package/src/primitives/overlay/strategies/trigger-event/on-focus.ts +37 -0
  361. package/src/primitives/overlay/strategies/trigger-event/on-hotkey.spec.ts +37 -0
  362. package/src/primitives/overlay/strategies/trigger-event/on-hotkey.ts +59 -0
  363. package/src/primitives/overlay/strategies/trigger-event/on-hover.spec.ts +34 -0
  364. package/src/primitives/overlay/strategies/trigger-event/on-hover.ts +86 -0
  365. package/src/primitives/overlay/strategies/trigger-event/programmatic.spec.ts +16 -0
  366. package/src/primitives/overlay/strategies/trigger-event/programmatic.ts +12 -0
  367. package/src/primitives/overlay/tokens.ts +54 -0
  368. package/src/primitives/overlay/trigger.spec.ts +103 -0
  369. package/src/primitives/overlay/trigger.ts +75 -0
  370. package/src/primitives/overlay/types.ts +24 -0
  371. package/src/primitives/overlay/wrapper.ts +50 -0
  372. package/src/primitives/signals/deep-signal.ts +109 -0
  373. package/src/primitives/signals/index.ts +1 -0
  374. package/src/progress-bar/config.ts +48 -0
  375. package/src/progress-bar/index.ts +12 -0
  376. package/src/progress-bar/progress-bar-fill.ts +37 -0
  377. package/src/progress-bar/progress-bar.context.ts +34 -0
  378. package/src/progress-bar/progress-bar.spec.ts +423 -0
  379. package/src/progress-bar/progress-bar.ts +217 -0
  380. package/src/public-api.ts +58 -9
  381. package/src/radio/radio.ts +14 -3
  382. package/src/select/index.ts +4 -2
  383. package/src/select/select-content.ts +69 -0
  384. package/src/select/select-option.ts +35 -0
  385. package/src/select/select-root.ts +129 -0
  386. package/src/select/select-trigger.ts +93 -0
  387. package/src/select/select.spec.ts +133 -140
  388. package/src/select/select.ts +4 -250
  389. package/src/skeleton/index.ts +5 -0
  390. package/src/skeleton/skeleton.spec.ts +115 -0
  391. package/src/skeleton/skeleton.ts +56 -0
  392. package/src/slider/index.ts +10 -0
  393. package/src/slider/slider-range.ts +49 -0
  394. package/src/slider/slider-thumb.ts +335 -0
  395. package/src/slider/slider-track.ts +70 -0
  396. package/src/slider/slider.context.ts +79 -0
  397. package/src/slider/slider.geometry.ts +73 -0
  398. package/src/slider/slider.spec.ts +216 -0
  399. package/src/slider/slider.ts +443 -0
  400. package/src/speed-dial/index.ts +10 -0
  401. package/src/speed-dial/speed-dial-action.ts +40 -0
  402. package/src/speed-dial/speed-dial-actions.ts +31 -0
  403. package/src/speed-dial/speed-dial-trigger.ts +55 -0
  404. package/src/speed-dial/speed-dial.context.ts +36 -0
  405. package/src/speed-dial/speed-dial.spec.ts +143 -0
  406. package/src/speed-dial/speed-dial.ts +127 -0
  407. package/src/spinner/config.ts +48 -0
  408. package/src/spinner/index.ts +8 -0
  409. package/src/spinner/spinner.spec.ts +262 -0
  410. package/src/spinner/spinner.ts +129 -0
  411. package/src/stepper/index.ts +15 -0
  412. package/src/stepper/stepper.context.ts +63 -0
  413. package/src/stepper/stepper.spec.ts +403 -0
  414. package/src/stepper/stepper.ts +544 -0
  415. package/src/styles.css +17 -0
  416. package/src/table/column-helpers.spec.ts +74 -0
  417. package/src/table/column-helpers.ts +68 -0
  418. package/src/table/filter-fns.ts +230 -0
  419. package/src/table/filter-models.spec.ts +240 -0
  420. package/src/table/filter-models.ts +124 -0
  421. package/src/table/filter-params.ts +90 -0
  422. package/src/table/grid-api-impl.ts +321 -0
  423. package/src/table/grid-api.spec.ts +178 -0
  424. package/src/table/grid-api.ts +285 -0
  425. package/src/table/index.ts +73 -1
  426. package/src/table/table-cell.spec.ts +43 -0
  427. package/src/table/table-cell.ts +38 -0
  428. package/src/table/table-filter-outlet.ts +120 -0
  429. package/src/table/table-header.spec.ts +54 -0
  430. package/src/table/table-header.ts +63 -0
  431. package/src/table/table-keyboard.spec.ts +68 -0
  432. package/src/table/table-keyboard.ts +107 -0
  433. package/src/table/table-resource.spec.ts +37 -0
  434. package/src/table/table-resource.ts +36 -0
  435. package/src/table/table-row.spec.ts +54 -0
  436. package/src/table/table-row.ts +31 -0
  437. package/src/table/table-storage.spec.ts +67 -0
  438. package/src/table/table-storage.ts +76 -0
  439. package/src/table/table.spec.ts +102 -43
  440. package/src/table/table.ts +228 -73
  441. package/src/table/table.types.ts +83 -0
  442. package/src/tabs/index.ts +7 -2
  443. package/src/tabs/tabs.context.ts +27 -4
  444. package/src/tabs/tabs.spec.ts +336 -24
  445. package/src/tabs/tabs.ts +313 -79
  446. package/src/tag/config.ts +53 -0
  447. package/src/tag/index.ts +7 -0
  448. package/src/tag/tag-list.ts +76 -0
  449. package/src/tag/tag-remove.ts +61 -0
  450. package/src/tag/tag.context.ts +36 -0
  451. package/src/tag/tag.spec.ts +240 -0
  452. package/src/tag/tag.ts +203 -0
  453. package/src/textarea/config.ts +42 -0
  454. package/src/textarea/index.ts +8 -0
  455. package/src/textarea/textarea.spec.ts +176 -0
  456. package/src/textarea/textarea.ts +235 -0
  457. package/src/time-picker/index.ts +15 -0
  458. package/src/time-picker/time-picker-meridiem.ts +123 -0
  459. package/src/time-picker/time-picker-segment.ts +318 -0
  460. package/src/time-picker/time-picker.context.ts +38 -0
  461. package/src/time-picker/time-picker.format.ts +181 -0
  462. package/src/time-picker/time-picker.spec.ts +189 -0
  463. package/src/time-picker/time-picker.ts +274 -0
  464. package/src/toast/_examples/index.ts +5 -0
  465. package/src/toast/_examples/toast.example.ts +282 -0
  466. package/src/toast/_examples/toast.finance.example.ts +180 -0
  467. package/src/toast/_examples/toast.retro.example.ts +161 -0
  468. package/src/toast/index.ts +8 -1
  469. package/src/toast/toast.ref.ts +26 -0
  470. package/src/toast/toast.service.ts +227 -41
  471. package/src/toast/toast.spec.ts +178 -25
  472. package/src/toast/toast.strategy.ts +10 -0
  473. package/src/toast/toast.ts +178 -22
  474. package/src/toggle/toggle.ts +10 -2
  475. package/src/tooltip/_examples/index.ts +6 -0
  476. package/src/tooltip/_examples/tooltip.example.ts +82 -0
  477. package/src/tooltip/_examples/tooltip.finance.example.ts +97 -0
  478. package/src/tooltip/_examples/tooltip.placements.example.ts +83 -0
  479. package/src/tooltip/_examples/tooltip.retro.example.ts +107 -0
  480. package/src/tooltip/index.ts +4 -5
  481. package/src/tooltip/tooltip-arrow.ts +31 -0
  482. package/src/tooltip/tooltip-content.ts +38 -0
  483. package/src/tooltip/tooltip-group.ts +32 -0
  484. package/src/tooltip/tooltip-trigger.ts +47 -0
  485. package/src/tooltip/tooltip.spec.ts +38 -102
  486. package/src/tooltip/tooltip.ts +5 -129
  487. package/src/tree-select/index.ts +12 -0
  488. package/src/tree-select/tree-select-content.ts +199 -0
  489. package/src/tree-select/tree-select-node.ts +132 -0
  490. package/src/tree-select/tree-select-root.ts +272 -0
  491. package/src/tree-select/tree-select-trigger.ts +90 -0
  492. package/src/tree-select/tree-select.context.ts +63 -0
  493. package/src/tree-select/tree-select.spec.ts +433 -0
  494. package/src/tree-select/tree-select.ts +2 -0
  495. package/src/typography/blockquote.ts +57 -0
  496. package/src/typography/code.ts +53 -0
  497. package/src/typography/index.ts +22 -0
  498. package/src/typography/lead.ts +49 -0
  499. package/src/typography/muted.ts +29 -0
  500. package/src/typography/prose.css +312 -0
  501. package/src/typography/truncate.ts +99 -0
  502. package/src/typography/typography.spec.ts +203 -0
  503. package/tsconfig.lib.json +9 -1
  504. package/tsconfig.lib.prod.json +7 -1
  505. package/tsconfig.spec.json +9 -1
  506. package/vite.config.ts +8 -0
  507. package/src/button/button.example.ts +0 -30
  508. package/src/button/button.finance.example.ts +0 -37
  509. package/src/button/button.retro.example.ts +0 -47
  510. package/src/button/button.sizes.example.ts +0 -25
  511. package/src/dialog/dialog.confirm.example.ts +0 -57
  512. package/src/dialog/dialog.context.ts +0 -11
  513. package/src/dialog/dialog.example.ts +0 -40
  514. package/src/dialog/dialog.finance.example.ts +0 -47
  515. package/src/dialog/dialog.retro.example.ts +0 -46
  516. package/src/menu/index.ts +0 -2
  517. package/src/menu/menu.context.ts +0 -10
  518. package/src/menu/menu.example.ts +0 -39
  519. package/src/menu/menu.spec.ts +0 -168
  520. package/src/menu/menu.ts +0 -176
  521. package/src/popover/popover.context.ts +0 -18
  522. package/src/popover/popover.example.ts +0 -50
  523. package/src/popover/popover.finance.example.ts +0 -52
  524. package/src/popover/popover.retro.example.ts +0 -53
  525. package/src/primitives/overlay/overlay.spec.ts +0 -90
  526. package/src/primitives/overlay/overlay.ts +0 -102
  527. package/src/select/select.context.ts +0 -3
  528. package/src/toast/toast.example.ts +0 -134
  529. package/src/toast/toast.finance.example.ts +0 -68
  530. package/src/toast/toast.retro.example.ts +0 -69
  531. package/src/tooltip/tooltip.example.ts +0 -46
  532. package/src/tooltip/tooltip.finance.example.ts +0 -54
  533. package/src/tooltip/tooltip.placements.example.ts +0 -56
  534. package/src/tooltip/tooltip.retro.example.ts +0 -65
@@ -0,0 +1,68 @@
1
+ import type { ColumnDef, FilterFn, RowData } from '@tanstack/angular-table';
2
+ import {
3
+ kjDateFilterFn,
4
+ kjNumberFilterFn,
5
+ kjSetFilterFn,
6
+ kjTextFilterFn,
7
+ } from './filter-fns';
8
+ import type { KjColumnDef, KjColumnMeta, KjColumnType } from './table.types';
9
+
10
+ const KJ_KEYS = [
11
+ 'kjType', 'kjEditable', 'kjFilterable', 'kjPin', 'kjGroup', 'kjAgg',
12
+ 'kjEditor', 'kjFilterUi', 'kjSelectOptions', 'kjExportable', 'kjPersist',
13
+ ] as const;
14
+
15
+ /** Auto-wired matcher per `kjType`. Picked up only when the column def
16
+ * doesn't already supply its own `filterFn`. */
17
+ const DEFAULT_FILTER_FNS: Readonly<Record<KjColumnType, FilterFn<unknown>>> = {
18
+ text: kjTextFilterFn as unknown as FilterFn<unknown>,
19
+ number: kjNumberFilterFn as unknown as FilterFn<unknown>,
20
+ date: kjDateFilterFn as unknown as FilterFn<unknown>,
21
+ select: kjSetFilterFn as unknown as FilterFn<unknown>,
22
+ boolean: kjSetFilterFn as unknown as FilterFn<unknown>,
23
+ };
24
+
25
+ /** Strip leading 'kj' and lowercase first char. */
26
+ function unprefix(k: string): keyof KjColumnMeta {
27
+ return (k.slice(2, 3).toLowerCase() + k.slice(3)) as keyof KjColumnMeta;
28
+ }
29
+
30
+ /**
31
+ * Define a column for `<kj-data-table>`. Pass-through for every TanStack
32
+ * `ColumnDef` field; kj-specific top-level knobs (`kjType`, `kjEditable`,
33
+ * `kjPin`, …) are hoisted into `meta.kj` for the wrapper to read.
34
+ */
35
+ export function kjColumn<TData extends RowData>(def: KjColumnDef<TData>): ColumnDef<TData> {
36
+ const kjMeta: Partial<KjColumnMeta<TData>> = {};
37
+ const rest: Record<string, unknown> = {};
38
+ for (const [k, v] of Object.entries(def)) {
39
+ if ((KJ_KEYS as readonly string[]).includes(k)) {
40
+ if (v !== undefined) kjMeta[unprefix(k)] = v as never;
41
+ } else {
42
+ rest[k] = v;
43
+ }
44
+ }
45
+ const existingMeta = (rest['meta'] ?? {}) as Record<string, unknown>;
46
+ const meta = Object.keys(kjMeta).length === 0
47
+ ? existingMeta
48
+ : { ...existingMeta, kj: kjMeta };
49
+
50
+ // Auto-wire `filterFn` from `kjType` so columns built with `kjColumn({ ... })`
51
+ // actually understand the structured `KjFilterModel` the built-in filter UIs
52
+ // write. User-supplied `filterFn` always wins.
53
+ if (rest['filterFn'] === undefined && kjMeta.type) {
54
+ const fn = DEFAULT_FILTER_FNS[kjMeta.type as KjColumnType];
55
+ if (fn) rest['filterFn'] = fn as unknown as FilterFn<TData>;
56
+ }
57
+
58
+ return { ...rest, meta } as unknown as ColumnDef<TData>;
59
+ }
60
+
61
+ /** Define a column group (header grouping). Children are `kjColumn()` defs. */
62
+ export function kjColumnGroup<TData extends RowData>(def: {
63
+ id?: string;
64
+ header: ColumnDef<TData>['header'];
65
+ columns: ColumnDef<TData>[];
66
+ }): ColumnDef<TData> {
67
+ return def as unknown as ColumnDef<TData>;
68
+ }
@@ -0,0 +1,230 @@
1
+ import type { Row } from '@tanstack/angular-table';
2
+ import {
3
+ isKjFilterModel,
4
+ type KjDateFilterModel,
5
+ type KjFilterModel,
6
+ type KjMultiConditionFilterModel,
7
+ type KjNumberFilterModel,
8
+ type KjSetFilterModel,
9
+ type KjTextFilterModel,
10
+ } from './filter-models';
11
+
12
+ /**
13
+ * TanStack `filterFn` for {@link KjTextFilterModel}. Accepts the model as
14
+ * the filter value and dispatches on `model.type`. Tolerates a bare
15
+ * string (legacy "contains" raw value) so users migrating filters one at
16
+ * a time don't have to flip every column simultaneously.
17
+ *
18
+ * Usage:
19
+ * ```ts
20
+ * kjColumn<User>({ accessorKey: 'name', filterFn: kjTextFilterFn })
21
+ * ```
22
+ */
23
+ export function kjTextFilterFn<TData>(
24
+ row: Row<TData>,
25
+ columnId: string,
26
+ filterValue: unknown,
27
+ ): boolean {
28
+ if (filterValue == null || filterValue === '') return true;
29
+
30
+ // Legacy raw-string compatibility — treated as `contains`.
31
+ if (typeof filterValue === 'string') {
32
+ return contains(read(row, columnId), filterValue, /* caseSensitive */ false);
33
+ }
34
+
35
+ if (!isKjFilterModel(filterValue) || filterValue.filterType !== 'text') return true;
36
+ const model = filterValue as KjTextFilterModel;
37
+ const cell = read(row, columnId);
38
+ const cs = model.caseSensitive === true;
39
+
40
+ switch (model.type) {
41
+ case 'blank': return cell == null || cell === '';
42
+ case 'notBlank': return cell != null && cell !== '';
43
+ case 'equals': return text(cell, cs) === text(model.filter ?? '', cs);
44
+ case 'notEquals': return text(cell, cs) !== text(model.filter ?? '', cs);
45
+ case 'contains': return contains(cell, model.filter ?? '', cs);
46
+ case 'notContains': return !contains(cell, model.filter ?? '', cs);
47
+ case 'startsWith': return text(cell, cs).startsWith(text(model.filter ?? '', cs));
48
+ case 'endsWith': return text(cell, cs).endsWith(text(model.filter ?? '', cs));
49
+ }
50
+ }
51
+
52
+ /**
53
+ * TanStack `filterFn` for {@link KjNumberFilterModel}. Tolerates a bare
54
+ * `[min, max]` tuple as legacy raw value (treated as `inRange`).
55
+ */
56
+ export function kjNumberFilterFn<TData>(
57
+ row: Row<TData>,
58
+ columnId: string,
59
+ filterValue: unknown,
60
+ ): boolean {
61
+ if (filterValue == null) return true;
62
+
63
+ // Legacy `[min, max]` tuple → inRange.
64
+ if (Array.isArray(filterValue)) {
65
+ const [min, max] = filterValue as [number | undefined, number | undefined];
66
+ return inNumberRange(Number(read(row, columnId)), min, max);
67
+ }
68
+
69
+ if (!isKjFilterModel(filterValue) || filterValue.filterType !== 'number') return true;
70
+ const model = filterValue as KjNumberFilterModel;
71
+ const cell = Number(read(row, columnId));
72
+
73
+ if (model.type === 'blank') return Number.isNaN(cell);
74
+ if (model.type === 'notBlank') return !Number.isNaN(cell);
75
+
76
+ // `inRange` is the one operator where a missing `filter` is still
77
+ // meaningful — it just leaves the lower bound open. Every other operator
78
+ // needs an explicit `filter`, so we treat its absence as "no filter".
79
+ if (model.type === 'inRange') {
80
+ if (model.filter == null && model.filterTo == null) return true;
81
+ return inNumberRange(cell, model.filter, model.filterTo);
82
+ }
83
+
84
+ const value = model.filter;
85
+ if (value == null) return true;
86
+
87
+ switch (model.type) {
88
+ case 'equals': return cell === value;
89
+ case 'notEquals': return cell !== value;
90
+ case 'greaterThan': return cell > value;
91
+ case 'greaterThanOrEqual': return cell >= value;
92
+ case 'lessThan': return cell < value;
93
+ case 'lessThanOrEqual': return cell <= value;
94
+ }
95
+ return true;
96
+ }
97
+
98
+ /**
99
+ * TanStack `filterFn` for {@link KjDateFilterModel}. Compares ISO date
100
+ * strings as calendar dates (no time component, no TZ math). Tolerates a
101
+ * `[from, to]` tuple of ISO strings as legacy raw value.
102
+ */
103
+ export function kjDateFilterFn<TData>(
104
+ row: Row<TData>,
105
+ columnId: string,
106
+ filterValue: unknown,
107
+ ): boolean {
108
+ if (filterValue == null) return true;
109
+
110
+ if (Array.isArray(filterValue)) {
111
+ const [from, to] = filterValue as [string | undefined, string | undefined];
112
+ return inIsoRange(isoOf(read(row, columnId)), from, to);
113
+ }
114
+
115
+ if (!isKjFilterModel(filterValue) || filterValue.filterType !== 'date') return true;
116
+ const model = filterValue as KjDateFilterModel;
117
+ const cell = isoOf(read(row, columnId));
118
+
119
+ if (model.type === 'blank') return !cell;
120
+ if (model.type === 'notBlank') return !!cell;
121
+ if (!cell) return false;
122
+
123
+ switch (model.type) {
124
+ case 'equals': return cell === model.dateFrom;
125
+ case 'notEquals': return cell !== model.dateFrom;
126
+ case 'greaterThan': return !!model.dateFrom && cell > model.dateFrom;
127
+ case 'lessThan': return !!model.dateFrom && cell < model.dateFrom;
128
+ case 'inRange': return inIsoRange(cell, model.dateFrom, model.dateTo);
129
+ }
130
+ }
131
+
132
+ /**
133
+ * TanStack `filterFn` for {@link KjSetFilterModel}. The row passes if the
134
+ * cell's stringified value matches any selected entry. Tolerates a bare
135
+ * (non-array) value as legacy raw equality.
136
+ */
137
+ export function kjSetFilterFn<TData>(
138
+ row: Row<TData>,
139
+ columnId: string,
140
+ filterValue: unknown,
141
+ ): boolean {
142
+ if (filterValue == null) return true;
143
+
144
+ if (!isKjFilterModel(filterValue)) {
145
+ return Object.is(read(row, columnId), filterValue);
146
+ }
147
+
148
+ if (filterValue.filterType !== 'set') return true;
149
+ const model = filterValue as KjSetFilterModel;
150
+ if (model.values.length === 0) return true;
151
+ const cell = read(row, columnId);
152
+ return model.values.some((v) => Object.is(v, cell));
153
+ }
154
+
155
+ /**
156
+ * TanStack `filterFn` for {@link KjMultiConditionFilterModel}.
157
+ * Recursively delegates each condition to the matching helper based on
158
+ * `condition.filterType`.
159
+ */
160
+ export function kjMultiFilterFn<TData>(
161
+ row: Row<TData>,
162
+ columnId: string,
163
+ filterValue: unknown,
164
+ ): boolean {
165
+ if (!isKjFilterModel(filterValue) || filterValue.filterType !== 'multi') return true;
166
+ const model = filterValue as KjMultiConditionFilterModel;
167
+ const reduce = model.operator === 'AND'
168
+ ? model.conditions.every.bind(model.conditions)
169
+ : model.conditions.some.bind(model.conditions);
170
+ return reduce((c) => evaluate(row, columnId, c));
171
+ }
172
+
173
+ /** Dispatch a single model against the correct typed helper. */
174
+ function evaluate<TData>(
175
+ row: Row<TData>,
176
+ columnId: string,
177
+ model: KjFilterModel,
178
+ ): boolean {
179
+ switch (model.filterType) {
180
+ case 'text': return kjTextFilterFn(row, columnId, model);
181
+ case 'number': return kjNumberFilterFn(row, columnId, model);
182
+ case 'date': return kjDateFilterFn(row, columnId, model);
183
+ case 'set': return kjSetFilterFn(row, columnId, model);
184
+ case 'multi': return kjMultiFilterFn(row, columnId, model);
185
+ }
186
+ }
187
+
188
+ // ── primitives ─────────────────────────────────────────────────────────────
189
+
190
+ function read<TData>(row: Row<TData>, columnId: string): unknown {
191
+ return row.getValue(columnId);
192
+ }
193
+ function text(value: unknown, caseSensitive: boolean): string {
194
+ const s = value == null ? '' : String(value);
195
+ return caseSensitive ? s : s.toLowerCase();
196
+ }
197
+ function contains(cell: unknown, needle: string, caseSensitive: boolean): boolean {
198
+ return text(cell, caseSensitive).includes(text(needle, caseSensitive));
199
+ }
200
+ function inNumberRange(
201
+ cell: number,
202
+ min: number | undefined,
203
+ max: number | undefined,
204
+ ): boolean {
205
+ if (Number.isNaN(cell)) return false;
206
+ if (min !== undefined && cell < min) return false;
207
+ if (max !== undefined && cell > max) return false;
208
+ return true;
209
+ }
210
+ function inIsoRange(
211
+ cell: string | undefined,
212
+ from: string | undefined,
213
+ to: string | undefined,
214
+ ): boolean {
215
+ if (!cell) return false;
216
+ if (from && cell < from) return false;
217
+ if (to && cell > to) return false;
218
+ return true;
219
+ }
220
+ function isoOf(value: unknown): string | undefined {
221
+ if (!value) return undefined;
222
+ if (value instanceof Date) {
223
+ const y = value.getFullYear();
224
+ const m = String(value.getMonth() + 1).padStart(2, '0');
225
+ const d = String(value.getDate()).padStart(2, '0');
226
+ return `${y}-${m}-${d}`;
227
+ }
228
+ if (typeof value === 'string') return value.slice(0, 10);
229
+ return undefined;
230
+ }
@@ -0,0 +1,240 @@
1
+ import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
2
+ import { render } from '@testing-library/angular';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { KjTable } from './table';
5
+ import { kjColumn } from './column-helpers';
6
+ import {
7
+ kjTextFilterFn,
8
+ kjNumberFilterFn,
9
+ kjDateFilterFn,
10
+ kjSetFilterFn,
11
+ kjMultiFilterFn,
12
+ } from './filter-fns';
13
+ import type {
14
+ KjDateFilterModel,
15
+ KjMultiConditionFilterModel,
16
+ KjNumberFilterModel,
17
+ KjSetFilterModel,
18
+ KjTextFilterModel,
19
+ } from './filter-models';
20
+
21
+ interface Row {
22
+ id: string;
23
+ name: string;
24
+ age: number;
25
+ joined: string;
26
+ role: string;
27
+ }
28
+
29
+ @Component({
30
+ standalone: true,
31
+ imports: [KjTable],
32
+ changeDetection: ChangeDetectionStrategy.Eager,
33
+ template: `<table
34
+ [kjTable]="cols"
35
+ [kjTableData]="data()"
36
+ [kjGetRowId]="getId"
37
+ #t="kjTable"
38
+ ></table>`,
39
+ })
40
+ class Host {
41
+ protected readonly cols = [
42
+ kjColumn<Row>({ accessorKey: 'name', header: 'Name', filterFn: kjTextFilterFn }),
43
+ kjColumn<Row>({ accessorKey: 'age', header: 'Age', filterFn: kjNumberFilterFn }),
44
+ kjColumn<Row>({ accessorKey: 'joined', header: 'Joined', filterFn: kjDateFilterFn }),
45
+ kjColumn<Row>({ accessorKey: 'role', header: 'Role', filterFn: kjSetFilterFn }),
46
+ ];
47
+ protected readonly data = signal<Row[]>([
48
+ { id: '1', name: 'Alice', age: 30, joined: '2024-01-15', role: 'admin' },
49
+ { id: '2', name: 'Bob', age: 25, joined: '2024-06-01', role: 'editor' },
50
+ { id: '3', name: 'Carol', age: 40, joined: '2025-02-10', role: 'viewer' },
51
+ { id: '4', name: 'Dave', age: 35, joined: '2025-08-22', role: 'editor' },
52
+ ]);
53
+ protected readonly getId = (row: Row) => row.id;
54
+ }
55
+
56
+ async function getDir() {
57
+ const r = await render(Host);
58
+ return r.fixture.debugElement.children[0].injector.get(KjTable<Row>);
59
+ }
60
+
61
+ describe('text filter model', () => {
62
+ it('contains', async () => {
63
+ const { gridApi } = await getDir();
64
+ gridApi.filter.set('name', {
65
+ filterType: 'text',
66
+ type: 'contains',
67
+ filter: 'al',
68
+ } as KjTextFilterModel);
69
+ expect(gridApi.rows.filteredCount()).toBe(1); // Alice
70
+ });
71
+
72
+ it('startsWith / endsWith', async () => {
73
+ const { gridApi } = await getDir();
74
+ gridApi.filter.set('name', { filterType: 'text', type: 'startsWith', filter: 'B' });
75
+ expect(gridApi.rows.filteredCount()).toBe(1);
76
+ gridApi.filter.set('name', { filterType: 'text', type: 'endsWith', filter: 'l' });
77
+ expect(gridApi.rows.filteredCount()).toBe(1); // Carol
78
+ });
79
+
80
+ it('blank / notBlank', async () => {
81
+ const { gridApi } = await getDir();
82
+ gridApi.filter.set('name', { filterType: 'text', type: 'notBlank' });
83
+ expect(gridApi.rows.filteredCount()).toBe(4);
84
+ gridApi.filter.set('name', { filterType: 'text', type: 'blank' });
85
+ expect(gridApi.rows.filteredCount()).toBe(0);
86
+ });
87
+ });
88
+
89
+ describe('number filter model', () => {
90
+ it('equals / notEquals', async () => {
91
+ const { gridApi } = await getDir();
92
+ gridApi.filter.set('age', { filterType: 'number', type: 'equals', filter: 30 });
93
+ expect(gridApi.rows.filteredCount()).toBe(1);
94
+ gridApi.filter.set('age', { filterType: 'number', type: 'notEquals', filter: 30 });
95
+ expect(gridApi.rows.filteredCount()).toBe(3);
96
+ });
97
+
98
+ it('inRange', async () => {
99
+ const { gridApi } = await getDir();
100
+ const model: KjNumberFilterModel = {
101
+ filterType: 'number',
102
+ type: 'inRange',
103
+ filter: 28,
104
+ filterTo: 36,
105
+ };
106
+ gridApi.filter.set('age', model);
107
+ expect(gridApi.rows.filteredCount()).toBe(2); // 30, 35
108
+ });
109
+
110
+ it('greaterThan / lessThanOrEqual', async () => {
111
+ const { gridApi } = await getDir();
112
+ gridApi.filter.set('age', { filterType: 'number', type: 'greaterThan', filter: 30 });
113
+ expect(gridApi.rows.filteredCount()).toBe(2);
114
+ gridApi.filter.set('age', { filterType: 'number', type: 'lessThanOrEqual', filter: 30 });
115
+ expect(gridApi.rows.filteredCount()).toBe(2);
116
+ });
117
+ });
118
+
119
+ describe('date filter model', () => {
120
+ it('inRange', async () => {
121
+ const { gridApi } = await getDir();
122
+ const model: KjDateFilterModel = {
123
+ filterType: 'date',
124
+ type: 'inRange',
125
+ dateFrom: '2024-01-01',
126
+ dateTo: '2024-12-31',
127
+ };
128
+ gridApi.filter.set('joined', model);
129
+ expect(gridApi.rows.filteredCount()).toBe(2);
130
+ });
131
+
132
+ it('greaterThan / lessThan', async () => {
133
+ const { gridApi } = await getDir();
134
+ gridApi.filter.set('joined', {
135
+ filterType: 'date',
136
+ type: 'greaterThan',
137
+ dateFrom: '2025-01-01',
138
+ });
139
+ expect(gridApi.rows.filteredCount()).toBe(2);
140
+ gridApi.filter.set('joined', { filterType: 'date', type: 'lessThan', dateFrom: '2024-06-01' });
141
+ expect(gridApi.rows.filteredCount()).toBe(1);
142
+ });
143
+ });
144
+
145
+ describe('set filter model', () => {
146
+ it('matches any of the listed values', async () => {
147
+ const { gridApi } = await getDir();
148
+ const model: KjSetFilterModel = { filterType: 'set', values: ['admin', 'editor'] };
149
+ gridApi.filter.set('role', model);
150
+ expect(gridApi.rows.filteredCount()).toBe(3);
151
+ });
152
+
153
+ it('empty values = no filter', async () => {
154
+ const { gridApi } = await getDir();
155
+ gridApi.filter.set('role', { filterType: 'set', values: [] });
156
+ expect(gridApi.rows.filteredCount()).toBe(4);
157
+ });
158
+ });
159
+
160
+ describe('multi-condition filter (AND/OR)', () => {
161
+ it('AND', async () => {
162
+ const { gridApi } = await getDir();
163
+ const model: KjMultiConditionFilterModel = {
164
+ filterType: 'multi',
165
+ operator: 'AND',
166
+ conditions: [
167
+ { filterType: 'number', type: 'greaterThan', filter: 28 },
168
+ { filterType: 'number', type: 'lessThan', filter: 36 },
169
+ ],
170
+ };
171
+ // Apply against age column — kjMultiFilterFn dispatches by inner filterType.
172
+ // Use direct fn invocation here because TanStack columns only have one filterFn at a time.
173
+ const passed = gridApi.rows
174
+ .all()
175
+ .filter((r) =>
176
+ kjMultiFilterFn(
177
+ { getValue: () => r.age } as unknown as Parameters<typeof kjMultiFilterFn>[0],
178
+ 'age',
179
+ model,
180
+ ),
181
+ );
182
+ expect(passed.length).toBe(2);
183
+ });
184
+
185
+ it('OR', async () => {
186
+ const { gridApi } = await getDir();
187
+ const model: KjMultiConditionFilterModel = {
188
+ filterType: 'multi',
189
+ operator: 'OR',
190
+ conditions: [
191
+ { filterType: 'number', type: 'lessThan', filter: 26 },
192
+ { filterType: 'number', type: 'greaterThan', filter: 38 },
193
+ ],
194
+ };
195
+ const passed = gridApi.rows
196
+ .all()
197
+ .filter((r) =>
198
+ kjMultiFilterFn(
199
+ { getValue: () => r.age } as unknown as Parameters<typeof kjMultiFilterFn>[0],
200
+ 'age',
201
+ model,
202
+ ),
203
+ );
204
+ expect(passed.length).toBe(2);
205
+ });
206
+ });
207
+
208
+ describe('gridApi.filter.getModel / setModel', () => {
209
+ it('getModel returns structured models keyed by column id', async () => {
210
+ const { gridApi } = await getDir();
211
+ gridApi.filter.set('name', { filterType: 'text', type: 'contains', filter: 'al' });
212
+ gridApi.filter.set('age', { filterType: 'number', type: 'equals', filter: 30 });
213
+ const model = gridApi.filter.getModel();
214
+ expect(model['name']).toEqual({ filterType: 'text', type: 'contains', filter: 'al' });
215
+ expect(model['age']).toEqual({ filterType: 'number', type: 'equals', filter: 30 });
216
+ });
217
+
218
+ it('getModel omits raw (non-model) entries', async () => {
219
+ const { gridApi } = await getDir();
220
+ gridApi.filter.set('name', 'alice'); // legacy raw value
221
+ expect(gridApi.filter.getModel()).toEqual({});
222
+ });
223
+
224
+ it('setModel replaces every column filter at once', async () => {
225
+ const { gridApi } = await getDir();
226
+ gridApi.filter.set('age', { filterType: 'number', type: 'equals', filter: 30 });
227
+ gridApi.filter.setModel({
228
+ role: { filterType: 'set', values: ['editor'] },
229
+ });
230
+ expect(gridApi.filter.getModelFor('age')).toBeUndefined();
231
+ expect(gridApi.filter.getModelFor('role')).toEqual({ filterType: 'set', values: ['editor'] });
232
+ });
233
+
234
+ it('setModel(null) clears everything', async () => {
235
+ const { gridApi } = await getDir();
236
+ gridApi.filter.set('age', { filterType: 'number', type: 'equals', filter: 30 });
237
+ gridApi.filter.setModel(null);
238
+ expect(gridApi.filter.all()).toEqual([]);
239
+ });
240
+ });
@@ -0,0 +1,124 @@
1
+ /**
2
+ * Structured filter models — a near-direct adaptation of AG-Grid's
3
+ * `FilterModel` shape. Each built-in filter writes one of these models
4
+ * to `column.setFilterValue(...)` instead of a raw value. The structured
5
+ * shape buys two things:
6
+ *
7
+ * 1. **Server-side rendering.** `gridApi.filter.getModel()` returns a
8
+ * `KjGridFilterModel` (`Record<columnId, KjFilterModel>`) that
9
+ * serialises cleanly into a JSON payload your backend can translate
10
+ * to SQL/Mongo/etc. without re-parsing free-form values. Pair with
11
+ * `kjTableResource({ loader })` to fan changes back to the server.
12
+ *
13
+ * 2. **Cross-filter introspection.** Code that needs to "what does the
14
+ * user currently have selected on column X" can discriminate on
15
+ * `filterType` and `type` instead of guessing from a `string | unknown`
16
+ * value.
17
+ *
18
+ * The companion `filter-fns` helpers (`kjTextFilterFn` etc.) implement
19
+ * client-side filtering against these models so client-mode tables work
20
+ * out of the box — point `kjColumn({ filterFn: kjTextFilterFn })` at one
21
+ * and TanStack will dispatch row matching for you.
22
+ */
23
+
24
+ /** Comparison operators for {@link KjTextFilterModel}. */
25
+ export type KjTextFilterType =
26
+ | 'contains'
27
+ | 'notContains'
28
+ | 'equals'
29
+ | 'notEquals'
30
+ | 'startsWith'
31
+ | 'endsWith'
32
+ | 'blank'
33
+ | 'notBlank';
34
+
35
+ export interface KjTextFilterModel {
36
+ readonly filterType: 'text';
37
+ readonly type: KjTextFilterType;
38
+ /** Match string. Omitted for `blank` / `notBlank`. */
39
+ readonly filter?: string;
40
+ /** Case-insensitive matching. Defaults to `true`. */
41
+ readonly caseSensitive?: boolean;
42
+ }
43
+
44
+ /** Comparison operators for {@link KjNumberFilterModel}. */
45
+ export type KjNumberFilterType =
46
+ | 'equals'
47
+ | 'notEquals'
48
+ | 'greaterThan'
49
+ | 'greaterThanOrEqual'
50
+ | 'lessThan'
51
+ | 'lessThanOrEqual'
52
+ | 'inRange'
53
+ | 'blank'
54
+ | 'notBlank';
55
+
56
+ export interface KjNumberFilterModel {
57
+ readonly filterType: 'number';
58
+ readonly type: KjNumberFilterType;
59
+ /** Primary number. Omitted for `blank` / `notBlank`. */
60
+ readonly filter?: number;
61
+ /** Range upper bound — used by `inRange`. */
62
+ readonly filterTo?: number;
63
+ }
64
+
65
+ /** Comparison operators for {@link KjDateFilterModel}. */
66
+ export type KjDateFilterType =
67
+ | 'equals'
68
+ | 'notEquals'
69
+ | 'greaterThan' // after
70
+ | 'lessThan' // before
71
+ | 'inRange'
72
+ | 'blank'
73
+ | 'notBlank';
74
+
75
+ export interface KjDateFilterModel {
76
+ readonly filterType: 'date';
77
+ readonly type: KjDateFilterType;
78
+ /** Primary date — ISO `YYYY-MM-DD`. */
79
+ readonly dateFrom?: string;
80
+ /** Range upper bound — used by `inRange`. */
81
+ readonly dateTo?: string;
82
+ }
83
+
84
+ /**
85
+ * Set / multi-select filter — the row passes if the cell value is in
86
+ * `values`. An empty `values` array means "no filter" (cleared); pair
87
+ * with `setFilterValue(undefined)` for the cleared state by preference.
88
+ */
89
+ export interface KjSetFilterModel {
90
+ readonly filterType: 'set';
91
+ readonly values: readonly unknown[];
92
+ }
93
+
94
+ /** Compound filter — combine two conditions with AND / OR. */
95
+ export interface KjMultiConditionFilterModel<TModel extends KjFilterModel = KjFilterModel> {
96
+ readonly filterType: 'multi';
97
+ readonly operator: 'AND' | 'OR';
98
+ readonly conditions: readonly TModel[];
99
+ }
100
+
101
+ /** Discriminated union of every built-in filter model. */
102
+ export type KjFilterModel =
103
+ | KjTextFilterModel
104
+ | KjNumberFilterModel
105
+ | KjDateFilterModel
106
+ | KjSetFilterModel
107
+ | KjMultiConditionFilterModel;
108
+
109
+ /**
110
+ * Bulk filter snapshot — keyed by column id. Shape mirrors AG-Grid's
111
+ * `api.getFilterModel()` return so SSR loaders that already understand
112
+ * AG-Grid payloads can consume it as-is.
113
+ */
114
+ export type KjGridFilterModel = Record<string, KjFilterModel>;
115
+
116
+ /** Narrowing helper. */
117
+ export function isKjFilterModel(value: unknown): value is KjFilterModel {
118
+ return (
119
+ !!value &&
120
+ typeof value === 'object' &&
121
+ 'filterType' in (value as object) &&
122
+ typeof (value as { filterType: unknown }).filterType === 'string'
123
+ );
124
+ }