@kouji-ui/core 0.0.4 → 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 +7 -4
  2. package/CHANGELOG.md +169 -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 +11 -3
  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 +101 -42
  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 +166 -14
  472. package/src/toast/toast.strategy.ts +10 -0
  473. package/src/toast/toast.ts +177 -21
  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 -106
  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,90 @@
1
+ import {
2
+ Directive,
3
+ ElementRef,
4
+ afterNextRender,
5
+ computed,
6
+ inject,
7
+ } from '@angular/core';
8
+ import { KjFocusRing } from '../primitives';
9
+ import { KjVariant, KjSize } from '../presets';
10
+ import { KJ_PAGINATION } from './pagination.context';
11
+ import { KJ_PAGINATION_CONFIG } from './config';
12
+
13
+ /**
14
+ * Boundary control that retreats one page. Disabled (via `aria-disabled`,
15
+ * not the native `disabled` attribute) when the parent is on the first
16
+ * page or the dataset is empty. Stays in the focus ring even when
17
+ * disabled (`tabindex="0"`) — matches Button's `disabledInteractive`
18
+ * stance so AT users discover the control and learn it is currently
19
+ * inactive (kouji's WCAG 2.1 AAA stance).
20
+ *
21
+ * @example
22
+ * ```html
23
+ * <button kjButton kjPaginationPrevious>Previous</button>
24
+ * ```
25
+ *
26
+ * @doc-category Core/Navigation
27
+ * @doc
28
+ * @doc-name pagination
29
+ */
30
+ @Directive({
31
+ selector: '[kjPaginationPrevious]',
32
+ standalone: true,
33
+ hostDirectives: [
34
+ { directive: KjVariant, inputs: ['kjVariant'] },
35
+ { directive: KjSize, inputs: ['kjSize'] },
36
+ KjFocusRing,
37
+ ],
38
+ host: {
39
+ '[attr.aria-label]': 'config.previousLabel',
40
+ '[attr.aria-disabled]': 'isDisabled() ? "true" : null',
41
+ '[attr.data-disabled]': 'isDisabled() ? "" : null',
42
+ '[attr.tabindex]': '"0"',
43
+ '[attr.data-pagination-action]': '"previous"',
44
+ '(click)': 'onClick($event)',
45
+ },
46
+ })
47
+ export class KjPaginationPrevious {
48
+ /** @internal */
49
+ readonly pagination = inject(KJ_PAGINATION);
50
+ /** @internal */
51
+ readonly config = inject(KJ_PAGINATION_CONFIG);
52
+ private readonly el = inject<ElementRef<HTMLElement>>(ElementRef);
53
+
54
+ /** True when the parent is on page 1 or the dataset is empty. */
55
+ readonly isDisabled = computed(() => this.pagination.isFirstPage());
56
+
57
+ constructor() {
58
+ // Capture-phase listeners fire before any consumer template-bound
59
+ // (click) / (keydown) handler and before native button activation,
60
+ // so the suppression actually prevents downstream navigation.
61
+ afterNextRender(() => {
62
+ const node = this.el.nativeElement;
63
+ const block = (event: Event) => {
64
+ if (this.isDisabled()) {
65
+ event.preventDefault();
66
+ event.stopImmediatePropagation();
67
+ }
68
+ };
69
+ const blockKey = (event: KeyboardEvent) => {
70
+ if (!this.isDisabled()) return;
71
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'Spacebar') {
72
+ event.preventDefault();
73
+ event.stopImmediatePropagation();
74
+ }
75
+ };
76
+ node.addEventListener('click', block, { capture: true });
77
+ node.addEventListener('keydown', blockKey, { capture: true });
78
+ });
79
+ }
80
+
81
+ /** @internal */
82
+ onClick(event: Event): void {
83
+ if (this.isDisabled()) {
84
+ event.preventDefault();
85
+ event.stopImmediatePropagation();
86
+ return;
87
+ }
88
+ this.pagination.goToPrevious();
89
+ }
90
+ }
@@ -0,0 +1,52 @@
1
+ import { InjectionToken, Signal } from '@angular/core';
2
+
3
+ /**
4
+ * A single token in the windowed page list produced by the page-token
5
+ * algorithm. Numeric tokens render as page buttons (`KjPaginationItem`);
6
+ * the two `'ellipsis-*'` variants are rendered as a `KjPaginationEllipsis`
7
+ * gap indicator. The two ellipsis variants are kept distinct so Angular's
8
+ * `@for (… ; track token)` can build stable, collision-free tracks.
9
+ */
10
+ export type KjPageToken = number | 'ellipsis-left' | 'ellipsis-right';
11
+
12
+ /**
13
+ * Context exposed by `KjPagination` to its child directives
14
+ * (`KjPaginationItem`, `KjPaginationPrevious`, `KjPaginationNext`,
15
+ * `KjPaginationFirst`, `KjPaginationLast`, `KjPaginationEllipsis`,
16
+ * `KjPaginationInfo`). Holds the page state, the algorithm output, the
17
+ * boundary computeds, and the imperative navigation methods.
18
+ *
19
+ * Children are stateless reflectors of the root's state — every
20
+ * derivation lives here so a single source of truth drives ARIA wiring,
21
+ * `data-current` / `data-disabled` reflection, and the live-region
22
+ * announcement.
23
+ */
24
+ export interface KjPaginationContext {
25
+ /** Current page (1-indexed). */
26
+ readonly page: Signal<number>;
27
+ /** Total number of pages. May be `0` (empty dataset). */
28
+ readonly totalPages: Signal<number>;
29
+ /** Output of the windowed page-token algorithm. */
30
+ readonly pages: Signal<readonly KjPageToken[]>;
31
+ /** True when the current page is `1` or the dataset is empty. */
32
+ readonly isFirstPage: Signal<boolean>;
33
+ /** True when the current page is `totalPages` or the dataset is empty. */
34
+ readonly isLastPage: Signal<boolean>;
35
+ /** Cascaded variant for children that did not set their own. */
36
+ readonly variant: Signal<string>;
37
+ /** Cascaded size for children that did not set their own. */
38
+ readonly size: Signal<string>;
39
+ /** Jump to a specific page (1-indexed); clamps to `[1, totalPages]`. */
40
+ goToPage(page: number): void;
41
+ /** Jump to page 1. No-op when already on page 1 / empty dataset. */
42
+ goToFirst(): void;
43
+ /** Jump to the last page. No-op when already there / empty dataset. */
44
+ goToLast(): void;
45
+ /** Advance one page. No-op when on the last page. */
46
+ goToNext(): void;
47
+ /** Retreat one page. No-op when on the first page. */
48
+ goToPrevious(): void;
49
+ }
50
+
51
+ /** Injection token for the root `KjPagination` directive context. */
52
+ export const KJ_PAGINATION = new InjectionToken<KjPaginationContext>('KjPagination');
@@ -0,0 +1,388 @@
1
+ import { Component, signal, ChangeDetectionStrategy } from '@angular/core';
2
+ import { fireEvent, render } from '@testing-library/angular';
3
+ import { describe, expect, it, vi } from 'vitest';
4
+ import { KjPagination } from './pagination';
5
+ import { KjPaginationItem } from './pagination-item';
6
+ import { KjPaginationPrevious } from './pagination-previous';
7
+ import { KjPaginationNext } from './pagination-next';
8
+ import { KjPaginationFirst } from './pagination-first';
9
+ import { KjPaginationLast } from './pagination-last';
10
+ import { KjPaginationEllipsis } from './pagination-ellipsis';
11
+ import { KjPaginationInfo } from './pagination-info';
12
+ import { computePageTokens } from './page-tokens';
13
+
14
+ const imports = [
15
+ KjPagination,
16
+ KjPaginationItem,
17
+ KjPaginationPrevious,
18
+ KjPaginationNext,
19
+ KjPaginationFirst,
20
+ KjPaginationLast,
21
+ KjPaginationEllipsis,
22
+ KjPaginationInfo,
23
+ ];
24
+
25
+ @Component({
26
+ standalone: true,
27
+ imports,
28
+ changeDetection: ChangeDetectionStrategy.Eager,
29
+ template: `
30
+ <nav kjPagination [(kjPage)]="page" [kjTotalPages]="totalPages()" #p="kjPagination">
31
+ <button kjPaginationFirst>«</button>
32
+ <button kjPaginationPrevious>‹</button>
33
+ @for (token of p.pages(); track token) {
34
+ @if (token === 'ellipsis-left' || token === 'ellipsis-right') {
35
+ <span kjPaginationEllipsis>…</span>
36
+ } @else {
37
+ <button kjPaginationItem [kjPage]="token">{{ token }}</button>
38
+ }
39
+ }
40
+ <button kjPaginationNext>›</button>
41
+ <button kjPaginationLast>»</button>
42
+ <span kjPaginationInfo></span>
43
+ </nav>
44
+ `,
45
+ })
46
+ class PaginationHost {
47
+ readonly page = signal(1);
48
+ readonly totalPages = signal(10);
49
+ }
50
+
51
+ @Component({
52
+ standalone: true,
53
+ imports,
54
+ changeDetection: ChangeDetectionStrategy.Eager,
55
+ template: `
56
+ <nav kjPagination [(kjPage)]="page" [kjTotalPages]="totalPages()" #p="kjPagination">
57
+ @for (token of p.pages(); track token) {
58
+ @if (token !== 'ellipsis-left' && token !== 'ellipsis-right') {
59
+ <button kjPaginationItem [kjPage]="token">{{ token }}</button>
60
+ }
61
+ }
62
+ </nav>
63
+ `,
64
+ })
65
+ class SmallPaginationHost {
66
+ readonly page = signal(1);
67
+ readonly totalPages = signal(3);
68
+ }
69
+
70
+ describe('computePageTokens (algorithm)', () => {
71
+ describe('edge cases', () => {
72
+ it('returns [] when total <= 0', () => {
73
+ expect(computePageTokens(1, 0, 1, 1)).toEqual([]);
74
+ expect(computePageTokens(1, -5, 1, 1)).toEqual([]);
75
+ });
76
+
77
+ it('renders all pages with no ellipses when total fits in the window', () => {
78
+ // boundary*2 + siblings*2 + 3 = 1*2 + 1*2 + 3 = 7 — total <= 7 fits.
79
+ expect(computePageTokens(1, 1, 1, 1)).toEqual([1]);
80
+ expect(computePageTokens(2, 5, 1, 1)).toEqual([1, 2, 3, 4, 5]);
81
+ expect(computePageTokens(4, 7, 1, 1)).toEqual([1, 2, 3, 4, 5, 6, 7]);
82
+ });
83
+ });
84
+
85
+ describe('large datasets — current near start', () => {
86
+ it('total=10, current=1: only right ellipsis (sibling window collapses to filler)', () => {
87
+ // With current=1 and siblings=1, the sibling window would span [0,2]
88
+ // but is clamped at the boundary anchor (page 1) — only `2` is added
89
+ // by the !showLeftEllipsis filler branch. The right ellipsis bridges
90
+ // to the right boundary.
91
+ expect(computePageTokens(1, 10, 1, 1)).toEqual([1, 2, 'ellipsis-right', 10]);
92
+ });
93
+
94
+ it('total=10, current=2: only right ellipsis', () => {
95
+ expect(computePageTokens(2, 10, 1, 1)).toEqual([1, 2, 3, 'ellipsis-right', 10]);
96
+ });
97
+
98
+ it('total=10, current=3: only right ellipsis', () => {
99
+ expect(computePageTokens(3, 10, 1, 1)).toEqual([1, 2, 3, 4, 'ellipsis-right', 10]);
100
+ });
101
+ });
102
+
103
+ describe('large datasets — current near end', () => {
104
+ it('total=10, current=10: only left ellipsis (sibling window clamps at boundary)', () => {
105
+ // Mirror of the current=1 case — the sibling window is clamped at
106
+ // the right boundary anchor, leaving the !showRightEllipsis filler
107
+ // branch to push `9` and the left ellipsis to bridge.
108
+ expect(computePageTokens(10, 10, 1, 1)).toEqual([1, 'ellipsis-left', 9, 10]);
109
+ });
110
+
111
+ it('total=10, current=9: only left ellipsis', () => {
112
+ expect(computePageTokens(9, 10, 1, 1)).toEqual([1, 'ellipsis-left', 8, 9, 10]);
113
+ });
114
+
115
+ it('total=10, current=8: only left ellipsis', () => {
116
+ expect(computePageTokens(8, 10, 1, 1)).toEqual([1, 'ellipsis-left', 7, 8, 9, 10]);
117
+ });
118
+ });
119
+
120
+ describe('large datasets — current in the middle', () => {
121
+ it('total=10, current=5: both ellipses', () => {
122
+ expect(computePageTokens(5, 10, 1, 1)).toEqual([
123
+ 1,
124
+ 'ellipsis-left',
125
+ 4,
126
+ 5,
127
+ 6,
128
+ 'ellipsis-right',
129
+ 10,
130
+ ]);
131
+ });
132
+
133
+ it('total=20, current=10, siblings=2, boundary=2: both ellipses', () => {
134
+ expect(computePageTokens(10, 20, 2, 2)).toEqual([
135
+ 1,
136
+ 2,
137
+ 'ellipsis-left',
138
+ 8,
139
+ 9,
140
+ 10,
141
+ 11,
142
+ 12,
143
+ 'ellipsis-right',
144
+ 19,
145
+ 20,
146
+ ]);
147
+ });
148
+ });
149
+
150
+ describe('off-by-one sweep across totals 7–11', () => {
151
+ it.each([7, 8, 9, 10, 11])(
152
+ 'total=%i: every current 1..total produces a stable, well-formed token list',
153
+ (total) => {
154
+ for (let current = 1; current <= total; current++) {
155
+ const tokens = computePageTokens(current, total, 1, 1);
156
+ // Always starts with 1 when boundary>=1.
157
+ expect(tokens[0]).toBe(1);
158
+ // Always ends with total when boundary>=1.
159
+ expect(tokens[tokens.length - 1]).toBe(total);
160
+ // The current page number always appears as a numeric token.
161
+ expect(tokens.includes(current)).toBe(true);
162
+ // Numeric tokens are strictly ascending.
163
+ const numerics = tokens.filter((t): t is number => typeof t === 'number');
164
+ for (let i = 1; i < numerics.length; i++) {
165
+ expect(numerics[i]).toBeGreaterThan(numerics[i - 1]);
166
+ }
167
+ }
168
+ },
169
+ );
170
+ });
171
+
172
+ describe('degenerate inputs', () => {
173
+ it('siblings=0, boundary=0, total=10, current=5: collapses to current only', () => {
174
+ // No boundary anchors, no sibling neighbors — just the current with
175
+ // ellipses bridging on each side.
176
+ const tokens = computePageTokens(5, 10, 0, 0);
177
+ expect(tokens).toContain(5);
178
+ // No `1` or `10` because boundary=0.
179
+ expect(tokens.includes(1)).toBe(false);
180
+ expect(tokens.includes(10)).toBe(false);
181
+ });
182
+ });
183
+ });
184
+
185
+ describe('KjPagination — directive integration', () => {
186
+ describe('host wiring', () => {
187
+ it('hosts <nav> with aria-label="Pagination" by default', async () => {
188
+ const { container } = await render(PaginationHost);
189
+ const nav = container.querySelector('[kjPagination]')!;
190
+ expect(nav.tagName).toBe('NAV');
191
+ expect(nav).toHaveAttribute('aria-label', 'Pagination');
192
+ });
193
+
194
+ it('reflects current page and total via data attributes', async () => {
195
+ const { container } = await render(PaginationHost);
196
+ const nav = container.querySelector('[kjPagination]')!;
197
+ expect(nav).toHaveAttribute('data-page', '1');
198
+ expect(nav).toHaveAttribute('data-total-pages', '10');
199
+ });
200
+ });
201
+
202
+ describe('aria-current on the active item', () => {
203
+ it('marks the current page item with aria-current="page" and data-current="true"', async () => {
204
+ const { container, fixture } = await render(PaginationHost);
205
+ // Initial page = 1.
206
+ const items = container.querySelectorAll<HTMLElement>('[kjPaginationItem]');
207
+ const first = Array.from(items).find((b) => b.textContent?.trim() === '1')!;
208
+ expect(first).toHaveAttribute('aria-current', 'page');
209
+ expect(first).toHaveAttribute('data-current', 'true');
210
+
211
+ // Move to page 3. With totalPages=10, siblings=1, boundary=1, current=3
212
+ // the window is [1, 2, 3, 4, 'ellipsis-right', 10] — page 3 is rendered.
213
+ (fixture.componentInstance as PaginationHost).page.set(3);
214
+ fixture.detectChanges();
215
+ const updated = container.querySelectorAll<HTMLElement>('[kjPaginationItem]');
216
+ const three = Array.from(updated).find((b) => b.textContent?.trim() === '3')!;
217
+ expect(three).toHaveAttribute('aria-current', 'page');
218
+ expect(three).toHaveAttribute('data-current', 'true');
219
+ // The previously-current item is no longer aria-current.
220
+ const one = Array.from(updated).find((b) => b.textContent?.trim() === '1')!;
221
+ expect(one).not.toHaveAttribute('aria-current');
222
+ expect(one).toHaveAttribute('data-current', 'false');
223
+ });
224
+
225
+ it('stamps a configured aria-label on each item', async () => {
226
+ const { container } = await render(PaginationHost);
227
+ const items = container.querySelectorAll<HTMLElement>('[kjPaginationItem]');
228
+ const first = Array.from(items).find((b) => b.textContent?.trim() === '1')!;
229
+ expect(first).toHaveAttribute('aria-label', 'Page 1');
230
+ });
231
+ });
232
+
233
+ describe('boundary controls — disabled state at edges, focus-preserving', () => {
234
+ it('Previous and First are aria-disabled on page 1, but stay focusable (tabindex="0")', async () => {
235
+ const { container } = await render(PaginationHost);
236
+ const prev = container.querySelector<HTMLElement>('[kjPaginationPrevious]')!;
237
+ const first = container.querySelector<HTMLElement>('[kjPaginationFirst]')!;
238
+ expect(prev).toHaveAttribute('aria-disabled', 'true');
239
+ expect(prev).toHaveAttribute('tabindex', '0');
240
+ expect(first).toHaveAttribute('aria-disabled', 'true');
241
+ expect(first).toHaveAttribute('tabindex', '0');
242
+ // Native disabled is NOT used.
243
+ expect(prev).not.toHaveAttribute('disabled');
244
+ expect(first).not.toHaveAttribute('disabled');
245
+ });
246
+
247
+ it('Next and Last are aria-disabled on the last page, but stay focusable', async () => {
248
+ const { fixture, container } = await render(PaginationHost);
249
+ (fixture.componentInstance as PaginationHost).page.set(10);
250
+ fixture.detectChanges();
251
+ const next = container.querySelector<HTMLElement>('[kjPaginationNext]')!;
252
+ const last = container.querySelector<HTMLElement>('[kjPaginationLast]')!;
253
+ expect(next).toHaveAttribute('aria-disabled', 'true');
254
+ expect(next).toHaveAttribute('tabindex', '0');
255
+ expect(last).toHaveAttribute('aria-disabled', 'true');
256
+ expect(last).toHaveAttribute('tabindex', '0');
257
+ });
258
+
259
+ it('Previous click is suppressed at the first-page boundary (capture-phase)', async () => {
260
+ const { fixture, container } = await render(PaginationHost);
261
+ const host = fixture.componentInstance as PaginationHost;
262
+ const prev = container.querySelector<HTMLElement>('[kjPaginationPrevious]')!;
263
+ fireEvent.click(prev);
264
+ fixture.detectChanges();
265
+ expect(host.page()).toBe(1);
266
+ });
267
+
268
+ it('Next click is suppressed at the last-page boundary', async () => {
269
+ const { fixture, container } = await render(PaginationHost);
270
+ const host = fixture.componentInstance as PaginationHost;
271
+ host.page.set(10);
272
+ fixture.detectChanges();
273
+ const next = container.querySelector<HTMLElement>('[kjPaginationNext]')!;
274
+ fireEvent.click(next);
275
+ fixture.detectChanges();
276
+ expect(host.page()).toBe(10);
277
+ });
278
+
279
+ it('Next click advances the page when not at the boundary', async () => {
280
+ const { fixture, container } = await render(PaginationHost);
281
+ const host = fixture.componentInstance as PaginationHost;
282
+ host.page.set(3);
283
+ fixture.detectChanges();
284
+ const next = container.querySelector<HTMLElement>('[kjPaginationNext]')!;
285
+ fireEvent.click(next);
286
+ fixture.detectChanges();
287
+ expect(host.page()).toBe(4);
288
+ });
289
+
290
+ it('First / Last jump to the corresponding boundary', async () => {
291
+ const { fixture, container } = await render(PaginationHost);
292
+ const host = fixture.componentInstance as PaginationHost;
293
+ host.page.set(5);
294
+ fixture.detectChanges();
295
+ const last = container.querySelector<HTMLElement>('[kjPaginationLast]')!;
296
+ fireEvent.click(last);
297
+ fixture.detectChanges();
298
+ expect(host.page()).toBe(10);
299
+ const first = container.querySelector<HTMLElement>('[kjPaginationFirst]')!;
300
+ fireEvent.click(first);
301
+ fixture.detectChanges();
302
+ expect(host.page()).toBe(1);
303
+ });
304
+ });
305
+
306
+ describe('kjPage two-way model', () => {
307
+ it('clicking a page item updates the consumer signal', async () => {
308
+ const { fixture, container } = await render(PaginationHost);
309
+ const host = fixture.componentInstance as PaginationHost;
310
+ const items = container.querySelectorAll<HTMLElement>('[kjPaginationItem]');
311
+ const two = Array.from(items).find((b) => b.textContent?.trim() === '2')!;
312
+ fireEvent.click(two);
313
+ fixture.detectChanges();
314
+ expect(host.page()).toBe(2);
315
+ });
316
+
317
+ it('writing the consumer signal flows to the directive (current item updates)', async () => {
318
+ const { fixture, container } = await render(PaginationHost);
319
+ (fixture.componentInstance as PaginationHost).page.set(2);
320
+ fixture.detectChanges();
321
+ const items = container.querySelectorAll<HTMLElement>('[kjPaginationItem]');
322
+ const two = Array.from(items).find((b) => b.textContent?.trim() === '2')!;
323
+ expect(two).toHaveAttribute('aria-current', 'page');
324
+ });
325
+
326
+ it('out-of-range writes are clamped and a warning is issued', async () => {
327
+ const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
328
+ const { fixture } = await render(PaginationHost);
329
+ const host = fixture.componentInstance as PaginationHost;
330
+ host.page.set(99);
331
+ fixture.detectChanges();
332
+ // Two ticks — the clamp effect re-runs on the second.
333
+ await Promise.resolve();
334
+ fixture.detectChanges();
335
+ expect(host.page()).toBe(10);
336
+ expect(warn).toHaveBeenCalled();
337
+ warn.mockRestore();
338
+ });
339
+ });
340
+
341
+ describe('live-region announcement', () => {
342
+ it('appends a polite, atomic, visually-hidden live region inside the nav', async () => {
343
+ const { container } = await render(PaginationHost);
344
+ // afterNextRender is asynchronous in jsdom — wait a tick.
345
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
346
+ const nav = container.querySelector('[kjPagination]')!;
347
+ const region = nav.querySelector('[data-kj-pagination-live]') as HTMLElement | null;
348
+ expect(region).toBeTruthy();
349
+ expect(region!).toHaveAttribute('aria-live', 'polite');
350
+ expect(region!).toHaveAttribute('aria-atomic', 'true');
351
+ expect(region!.getAttribute('style') ?? '').toContain('clip:rect(0,0,0,0)');
352
+ });
353
+
354
+ it('writes the configured announcement template on page change', async () => {
355
+ const { fixture, container } = await render(PaginationHost);
356
+ const host = fixture.componentInstance as PaginationHost;
357
+ // Wait for the live region to attach.
358
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
359
+ host.page.set(4);
360
+ fixture.detectChanges();
361
+ // The directive uses queueMicrotask to clear-then-set; flush it.
362
+ await Promise.resolve();
363
+ await new Promise<void>((resolve) => queueMicrotask(() => resolve()));
364
+ const region = container.querySelector('[data-kj-pagination-live]') as HTMLElement;
365
+ expect(region.textContent).toBe('Page 4 of 10');
366
+ });
367
+ });
368
+
369
+ describe('small dataset', () => {
370
+ it('renders every page with no ellipses', async () => {
371
+ const { container } = await render(SmallPaginationHost);
372
+ const items = container.querySelectorAll<HTMLElement>('[kjPaginationItem]');
373
+ expect(items).toHaveLength(3);
374
+ const ellipses = container.querySelectorAll('[kjPaginationEllipsis]');
375
+ expect(ellipses).toHaveLength(0);
376
+ });
377
+ });
378
+
379
+ describe('info directive', () => {
380
+ it('renders the "Page N of M" string into an empty host', async () => {
381
+ const { container, fixture } = await render(PaginationHost);
382
+ await new Promise<void>((resolve) => setTimeout(resolve, 0));
383
+ fixture.detectChanges();
384
+ const info = container.querySelector('[kjPaginationInfo]')!;
385
+ expect(info.textContent).toContain('Page 1 of 10');
386
+ });
387
+ });
388
+ });