@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
package/src/kbd/kbd.ts ADDED
@@ -0,0 +1,132 @@
1
+ import {
2
+ Directive,
3
+ ElementRef,
4
+ afterNextRender,
5
+ inject,
6
+ isDevMode,
7
+ } from '@angular/core';
8
+ import { KjSize, type KjSizePreset } from '../presets';
9
+
10
+ /**
11
+ * Default size preset used by Kbd consumers (`xs / sm / md / lg`, default `md`).
12
+ *
13
+ * Re-exported so core-package consumers applying `[kjKbd]` directly to their
14
+ * own `<kbd>` elements can opt into the same size scale the eventual styled
15
+ * wrapper will provide, via:
16
+ *
17
+ * ```ts
18
+ * providers: [{ provide: KJ_SIZE_PRESET, useValue: KJ_KBD_SIZE_PRESET }]
19
+ * ```
20
+ *
21
+ * Avoids the silent-drift case where a consumer picks `'huge'` and the
22
+ * directive's dev-mode validator passes (because the consumer's preset didn't
23
+ * list it) but the CSS doesn't recognise the value. See
24
+ * `docs/component-analyses/data-display/kbd.md` (Open question 13).
25
+ */
26
+ export const KJ_KBD_SIZE_PRESET: KjSizePreset = {
27
+ values: ['xs', 'sm', 'md', 'lg'],
28
+ default: 'md',
29
+ };
30
+
31
+ /**
32
+ * Marks an element as a kouji keyboard-key visual (`<kbd>`). The directive is
33
+ * a pure visual primitive: it composes `KjSize` (the only configurable axis)
34
+ * and otherwise relies on the native `<kbd>` element's a11y semantics.
35
+ *
36
+ * The host element is consumer-supplied. The recommended host is `<kbd>`
37
+ * (native semantic — implicit `role="generic"` per HTML AAM, but commonly
38
+ * announced with a "key" prefix in verbose AT modes); applying `[kjKbd]` to
39
+ * any other element is dev-mode warned but not enforced (consumers wanting a
40
+ * `<span>` host for inline-baseline reasons get an escape hatch).
41
+ *
42
+ * **No variant.** Kbd is tonally neutral — there is no "destructive kbd" or
43
+ * "primary kbd". A future design language wanting e.g. a "deprecated shortcut"
44
+ * tone can fold in via a CSS-only `[data-deprecated="true"]` attribute without
45
+ * API churn.
46
+ *
47
+ * **No keyboard contract.** Kbd is non-focusable, non-clickable. Consumers
48
+ * wanting a clickable shortcut chip wrap a `<button kjButton>` around the
49
+ * `<kbd kjKbd>` and the button owns the click / focus / a11y story. Embedding
50
+ * a focusable descendant inside `[kjKbd]` is dev-mode warned because most AT
51
+ * will re-parent or strip the interactive child out of the kbd's accessible
52
+ * name.
53
+ *
54
+ * **No `aria-label` injection.** When the visible text is a unicode glyph
55
+ * (`⌘`, `⌥`, `↵`) the consumer sets `aria-label` directly on the host —
56
+ * exactly the same idiom as setting `aria-label` on any HTML element. The
57
+ * directive does not declare a duplicate input for it. (The eventual
58
+ * `<kj-kbd>` wrapper exposes a template-driven `kjKbdAriaLabel` input that
59
+ * forwards to `[attr.aria-label]`; that is the wrapper's call, not the
60
+ * directive's.)
61
+ *
62
+ * @example
63
+ * ```html
64
+ * <kbd kjKbd>Enter</kbd>
65
+ * <kbd kjKbd kjSize="xs">⌘</kbd>
66
+ * <kbd kjKbd aria-label="Command">⌘</kbd>
67
+ * ```
68
+ * @doc-category Core/Data display
69
+ * @doc
70
+ * @doc-name kbd
71
+ * @doc-description Marks a kbd element as a keyboard-key visual with a configurable size.
72
+ * @doc-is-main
73
+ */
74
+ @Directive({
75
+ selector: '[kjKbd]',
76
+ standalone: true,
77
+ hostDirectives: [{ directive: KjSize, inputs: ['kjSize'] }],
78
+ })
79
+ export class KjKbd {
80
+ private readonly el = inject<ElementRef<HTMLElement>>(ElementRef);
81
+
82
+ constructor() {
83
+ if (isDevMode()) {
84
+ afterNextRender(() => {
85
+ const host = this.el.nativeElement;
86
+
87
+ if (host.tagName?.toLowerCase() !== 'kbd') {
88
+ console.warn(
89
+ `[kj] kjKbd applied to <${host.tagName?.toLowerCase()}>. ` +
90
+ `Recommended host element is <kbd> for native semantics ` +
91
+ `(WCAG 1.3.1 / 4.1.2). Apply only to non-kbd elements when ` +
92
+ `you have a specific styling reason.`,
93
+ );
94
+ }
95
+
96
+ if (hasFocusableDescendant(host)) {
97
+ console.warn(
98
+ `[kj] kjKbd contains a focusable descendant. Kbd is a ` +
99
+ `non-interactive visual primitive; most AT will re-parent or ` +
100
+ `strip interactive children out of the kbd's accessible name. ` +
101
+ `Wrap the <kbd kjKbd> inside a <button kjButton> instead of ` +
102
+ `nesting an interactive element inside it.`,
103
+ );
104
+ }
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ /**
111
+ * Selector matching elements that are focusable by default. Intentionally
112
+ * narrow: this is a dev-mode advisory, not an a11y enforcement gate, so we
113
+ * accept the small risk of missing exotic focusable descendants
114
+ * (`contenteditable`, embedded `<object>` with focusable controls, etc.) in
115
+ * favour of a fast first-pass check.
116
+ */
117
+ const FOCUSABLE_SELECTOR = [
118
+ 'a[href]',
119
+ 'button',
120
+ 'input',
121
+ 'select',
122
+ 'textarea',
123
+ 'audio[controls]',
124
+ 'video[controls]',
125
+ '[tabindex]',
126
+ '[contenteditable=""]',
127
+ '[contenteditable="true"]',
128
+ ].join(',');
129
+
130
+ function hasFocusableDescendant(host: HTMLElement): boolean {
131
+ return host.querySelector(FOCUSABLE_SELECTOR) !== null;
132
+ }
@@ -0,0 +1,44 @@
1
+ import { InjectionToken, Provider } from '@angular/core';
2
+
3
+ export interface KjLinkConfig {
4
+ variants: string[];
5
+ sizes: string[];
6
+ defaults: { variant: string; size: string };
7
+ }
8
+
9
+ /**
10
+ * Default Link presets shipped by kouji-ui. Exported so consumers can spread
11
+ * them when extending: `[...KJ_LINK_DEFAULTS.variants, 'brand']`.
12
+ */
13
+ export const KJ_LINK_DEFAULTS: KjLinkConfig = {
14
+ variants: ['primary', 'secondary', 'muted', 'destructive'],
15
+ sizes: ['sm', 'md', 'lg', 'inherit'],
16
+ defaults: { variant: 'primary', size: 'inherit' },
17
+ };
18
+
19
+ /**
20
+ * DI token for the active Link presets. Default factory yields
21
+ * `KJ_LINK_DEFAULTS`. Override via `provideKjLink(…)` at the application
22
+ * scope (e.g. `bootstrapApplication`'s `providers` or a route's `providers`)
23
+ * or at the component scope (a component's own `providers: […]`).
24
+ */
25
+ export const KJ_LINK_CONFIG = new InjectionToken<KjLinkConfig>('kj.link.config', {
26
+ factory: () => KJ_LINK_DEFAULTS,
27
+ });
28
+
29
+ /**
30
+ * Configures the Link presets for the enclosing injector. Replaces (does not
31
+ * merge) `variants` and `sizes`; spread `KJ_LINK_DEFAULTS.variants` to extend.
32
+ *
33
+ * Returns a `Provider[]` so it can be spread into either an environment
34
+ * `providers` (`bootstrapApplication`, route config) or a component-level
35
+ * `providers` array.
36
+ */
37
+ export function provideKjLink(config: Partial<KjLinkConfig>): Provider[] {
38
+ return [
39
+ {
40
+ provide: KJ_LINK_CONFIG,
41
+ useValue: { ...KJ_LINK_DEFAULTS, ...config },
42
+ },
43
+ ];
44
+ }
@@ -0,0 +1,2 @@
1
+ export * from './link';
2
+ export * from './config';
@@ -0,0 +1,246 @@
1
+ import { Component, ChangeDetectionStrategy } from '@angular/core';
2
+ import { TestBed } from '@angular/core/testing';
3
+ import { render } from '@testing-library/angular';
4
+ import { describe, expect, it } from 'vitest';
5
+ import { KjLink } from './link';
6
+
7
+ @Component({
8
+ standalone: true,
9
+ imports: [KjLink],
10
+ changeDetection: ChangeDetectionStrategy.Eager,
11
+ template: `<a kjLink href="/x" [kjDisabled]="d" (click)="onClick()">x</a>`,
12
+ })
13
+ class DisabledClickHost {
14
+ d = true;
15
+ fired = 0;
16
+ onClick() {
17
+ this.fired++;
18
+ }
19
+ }
20
+
21
+ @Component({
22
+ standalone: true,
23
+ imports: [KjLink],
24
+ changeDetection: ChangeDetectionStrategy.Eager,
25
+ template: `<a kjLink href="/x" [kjDisabled]="d" (keydown)="onKeydown($event)">x</a>`,
26
+ })
27
+ class DisabledKeydownHost {
28
+ d = true;
29
+ navigated = 0;
30
+ onKeydown(_event: KeyboardEvent) {
31
+ this.navigated++;
32
+ }
33
+ }
34
+
35
+ describe('KjLink', () => {
36
+ it('renders an anchor with role=link via native <a>', async () => {
37
+ const { getByRole } = await render(`<a kjLink href="/foo">Foo</a>`, { imports: [KjLink] });
38
+ expect(getByRole('link')).toBeInTheDocument();
39
+ });
40
+
41
+ it('default variant comes from KJ_LINK_DEFAULTS', async () => {
42
+ const { getByRole } = await render(`<a kjLink href="/x">x</a>`, { imports: [KjLink] });
43
+ expect(getByRole('link')).toHaveAttribute('data-variant', 'primary');
44
+ });
45
+
46
+ it('default size comes from KJ_LINK_DEFAULTS', async () => {
47
+ const { getByRole } = await render(`<a kjLink href="/x">x</a>`, { imports: [KjLink] });
48
+ expect(getByRole('link')).toHaveAttribute('data-size', 'inherit');
49
+ });
50
+
51
+ it('reflects kjVariant to data-variant', async () => {
52
+ const { getByRole } = await render(`<a kjLink href="/x" [kjVariant]="'destructive'">x</a>`, {
53
+ imports: [KjLink],
54
+ });
55
+ expect(getByRole('link')).toHaveAttribute('data-variant', 'destructive');
56
+ });
57
+
58
+ it('reflects kjSize to data-size', async () => {
59
+ const { getByRole } = await render(`<a kjLink href="/x" [kjSize]="'sm'">x</a>`, {
60
+ imports: [KjLink],
61
+ });
62
+ expect(getByRole('link')).toHaveAttribute('data-size', 'sm');
63
+ });
64
+
65
+ it('reflects kjUnderline to data-underline (default hover)', async () => {
66
+ const { getByRole } = await render(`<a kjLink href="/x">x</a>`, { imports: [KjLink] });
67
+ expect(getByRole('link')).toHaveAttribute('data-underline', 'hover');
68
+ });
69
+
70
+ it('reflects kjUnderline=always', async () => {
71
+ const { getByRole } = await render(`<a kjLink href="/x" [kjUnderline]="'always'">x</a>`, {
72
+ imports: [KjLink],
73
+ });
74
+ expect(getByRole('link')).toHaveAttribute('data-underline', 'always');
75
+ });
76
+
77
+ it('reflects kjUnderline=none', async () => {
78
+ const { getByRole } = await render(`<a kjLink href="/x" [kjUnderline]="'none'">x</a>`, {
79
+ imports: [KjLink],
80
+ });
81
+ expect(getByRole('link')).toHaveAttribute('data-underline', 'none');
82
+ });
83
+
84
+ it('auto-detects external from target="_blank" and reflects data-external', async () => {
85
+ const { getByRole } = await render(
86
+ `<a kjLink href="https://example.com" target="_blank">Docs</a>`,
87
+ { imports: [KjLink] },
88
+ );
89
+ expect(getByRole('link')).toHaveAttribute('data-external', 'true');
90
+ });
91
+
92
+ it('does not mark as external when no target="_blank" and kjExternal unset', async () => {
93
+ const { getByRole } = await render(`<a kjLink href="https://example.com">Docs</a>`, {
94
+ imports: [KjLink],
95
+ });
96
+ expect(getByRole('link')).not.toHaveAttribute('data-external');
97
+ });
98
+
99
+ it('explicit kjExternal=true overrides target detection', async () => {
100
+ const { getByRole } = await render(`<a kjLink href="/internal" [kjExternal]="true">Docs</a>`, {
101
+ imports: [KjLink],
102
+ });
103
+ expect(getByRole('link')).toHaveAttribute('data-external', 'true');
104
+ });
105
+
106
+ it('explicit kjExternal=false suppresses external treatment even with target=_blank', async () => {
107
+ const { getByRole } = await render(
108
+ `<a kjLink href="/x" target="_blank" [kjExternal]="false">x</a>`,
109
+ { imports: [KjLink] },
110
+ );
111
+ expect(getByRole('link')).not.toHaveAttribute('data-external');
112
+ });
113
+
114
+ it('injects rel="noopener noreferrer" when external', async () => {
115
+ const { getByRole } = await render(
116
+ `<a kjLink href="https://example.com" target="_blank">Docs</a>`,
117
+ { imports: [KjLink] },
118
+ );
119
+ const rel = getByRole('link').getAttribute('rel') ?? '';
120
+ const tokens = rel.split(/\s+/u).filter(Boolean);
121
+ expect(tokens).toContain('noopener');
122
+ expect(tokens).toContain('noreferrer');
123
+ });
124
+
125
+ it('rel injection is additive — preserves consumer-supplied tokens', async () => {
126
+ const { getByRole } = await render(
127
+ `<a kjLink href="https://example.com" target="_blank" rel="nofollow me">Docs</a>`,
128
+ { imports: [KjLink] },
129
+ );
130
+ const rel = getByRole('link').getAttribute('rel') ?? '';
131
+ const tokens = rel.split(/\s+/u).filter(Boolean);
132
+ expect(tokens).toContain('nofollow');
133
+ expect(tokens).toContain('me');
134
+ expect(tokens).toContain('noopener');
135
+ expect(tokens).toContain('noreferrer');
136
+ });
137
+
138
+ it('does not add noopener/noreferrer when not external', async () => {
139
+ const { getByRole } = await render(`<a kjLink href="/internal" rel="nofollow">x</a>`, {
140
+ imports: [KjLink],
141
+ });
142
+ const rel = getByRole('link').getAttribute('rel') ?? '';
143
+ expect(rel.split(/\s+/u)).not.toContain('noopener');
144
+ expect(rel.split(/\s+/u)).toContain('nofollow');
145
+ });
146
+
147
+ it('appends visually-hidden "(opens in new tab)" suffix span when external', async () => {
148
+ const { getByRole } = await render(
149
+ `<a kjLink href="https://example.com" target="_blank">Docs</a>`,
150
+ { imports: [KjLink] },
151
+ );
152
+ const link = getByRole('link');
153
+ const suffix = link.querySelector('.kj-link-external-suffix');
154
+ expect(suffix).not.toBeNull();
155
+ expect(suffix!.textContent).toContain('opens in new tab');
156
+ // Visually hidden inline style applied.
157
+ const style = (suffix as HTMLElement).getAttribute('style') ?? '';
158
+ expect(style).toContain('width:1px');
159
+ expect(style).toContain('height:1px');
160
+ });
161
+
162
+ it('does not inject AT suffix when consumer supplies aria-label', async () => {
163
+ const { getByRole } = await render(
164
+ `<a kjLink href="https://example.com" target="_blank" aria-label="Docs (new window)">Docs</a>`,
165
+ { imports: [KjLink] },
166
+ );
167
+ const link = getByRole('link');
168
+ expect(link.querySelector('.kj-link-external-suffix')).toBeNull();
169
+ });
170
+
171
+ it('does not inject AT suffix for non-external links', async () => {
172
+ const { getByRole } = await render(`<a kjLink href="/x">x</a>`, { imports: [KjLink] });
173
+ expect(getByRole('link').querySelector('.kj-link-external-suffix')).toBeNull();
174
+ });
175
+
176
+ it('disabled bundle: aria-disabled, data-disabled, tabindex=-1', async () => {
177
+ const { getByRole } = await render(`<a kjLink href="/x" [kjDisabled]="true">x</a>`, {
178
+ imports: [KjLink],
179
+ });
180
+ const link = getByRole('link');
181
+ expect(link).toHaveAttribute('aria-disabled', 'true');
182
+ expect(link).toHaveAttribute('data-disabled', '');
183
+ expect(link).toHaveAttribute('tabindex', '-1');
184
+ });
185
+
186
+ it('omits aria-disabled / tabindex when not disabled', async () => {
187
+ const { getByRole } = await render(`<a kjLink href="/x">x</a>`, { imports: [KjLink] });
188
+ const link = getByRole('link');
189
+ expect(link).not.toHaveAttribute('aria-disabled');
190
+ expect(link).not.toHaveAttribute('tabindex');
191
+ });
192
+
193
+ it('suppresses click events when disabled (capture-phase)', () => {
194
+ TestBed.configureTestingModule({ imports: [DisabledClickHost] });
195
+ const fixture = TestBed.createComponent(DisabledClickHost);
196
+ fixture.detectChanges();
197
+ const link = fixture.nativeElement.querySelector('a') as HTMLAnchorElement;
198
+ link.click();
199
+ expect(fixture.componentInstance.fired).toBe(0);
200
+ });
201
+
202
+ it('allows click events when not disabled', () => {
203
+ TestBed.configureTestingModule({ imports: [DisabledClickHost] });
204
+ const fixture = TestBed.createComponent(DisabledClickHost);
205
+ fixture.componentInstance.d = false;
206
+ fixture.detectChanges();
207
+ const link = fixture.nativeElement.querySelector('a') as HTMLAnchorElement;
208
+ link.addEventListener('click', (e) => e.preventDefault(), { once: true });
209
+ link.click();
210
+ expect(fixture.componentInstance.fired).toBe(1);
211
+ });
212
+
213
+ it('suppresses Enter keydown events when disabled', () => {
214
+ TestBed.configureTestingModule({ imports: [DisabledKeydownHost] });
215
+ const fixture = TestBed.createComponent(DisabledKeydownHost);
216
+ fixture.detectChanges();
217
+ const link = fixture.nativeElement.querySelector('a') as HTMLAnchorElement;
218
+ const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true });
219
+ link.dispatchEvent(event);
220
+ expect(event.defaultPrevented).toBe(true);
221
+ expect(fixture.componentInstance.navigated).toBe(0);
222
+ });
223
+
224
+ it('does not suppress non-Enter keydown when disabled', () => {
225
+ TestBed.configureTestingModule({ imports: [DisabledKeydownHost] });
226
+ const fixture = TestBed.createComponent(DisabledKeydownHost);
227
+ fixture.detectChanges();
228
+ const link = fixture.nativeElement.querySelector('a') as HTMLAnchorElement;
229
+ const event = new KeyboardEvent('keydown', { key: 'Tab', bubbles: true, cancelable: true });
230
+ link.dispatchEvent(event);
231
+ expect(event.defaultPrevented).toBe(false);
232
+ expect(fixture.componentInstance.navigated).toBe(1);
233
+ });
234
+
235
+ it('does not suppress Enter when not disabled', () => {
236
+ TestBed.configureTestingModule({ imports: [DisabledKeydownHost] });
237
+ const fixture = TestBed.createComponent(DisabledKeydownHost);
238
+ fixture.componentInstance.d = false;
239
+ fixture.detectChanges();
240
+ const link = fixture.nativeElement.querySelector('a') as HTMLAnchorElement;
241
+ const event = new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true });
242
+ link.dispatchEvent(event);
243
+ expect(event.defaultPrevented).toBe(false);
244
+ expect(fixture.componentInstance.navigated).toBe(1);
245
+ });
246
+ });
@@ -0,0 +1,226 @@
1
+ import { DOCUMENT } from '@angular/common';
2
+ import {
3
+ Directive,
4
+ ElementRef,
5
+ afterNextRender,
6
+ computed,
7
+ effect,
8
+ inject,
9
+ input,
10
+ } from '@angular/core';
11
+ import { KjFocusRing, KjDisabled } from '../primitives';
12
+ import { KjVariant, KjSize, bindPresets } from '../presets';
13
+ import { KJ_LINK_CONFIG } from './config';
14
+
15
+ /** CSS class applied to the visually-hidden "(opens in new tab)" suffix span. */
16
+ const KJ_LINK_EXTERNAL_SUFFIX_CLASS = 'kj-link-external-suffix';
17
+
18
+ /** AT suffix copy. English-only for v1; i18n is a v1.1 concern (see analysis §15). */
19
+ const KJ_LINK_EXTERNAL_SUFFIX_TEXT = '(opens in new tab)';
20
+
21
+ /** Inline style string equivalent to `KjVisuallyHidden` — applied to the AT suffix span. */
22
+ const KJ_LINK_VISUALLY_HIDDEN_STYLE =
23
+ 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0';
24
+
25
+ /**
26
+ * Enhances a native `<a>` (or any element used as a link) with kouji-ui
27
+ * presets, focus-ring and disabled-link plumbing, and external-link
28
+ * `rel` + AT suffix discipline.
29
+ *
30
+ * Composes `KjVariant`, `KjSize`, `KjFocusRing`, and `KjDisabled` via
31
+ * `hostDirectives`. Variants and sizes are configurable via
32
+ * `provideKjLink(…)`.
33
+ *
34
+ * Owns three behaviours that a CSS-only solution cannot deliver:
35
+ *
36
+ * 1. **External-link plumbing.** When `kjExternal` is `true` (or `undefined`
37
+ * and the host carries `target="_blank"`), the directive ensures `rel`
38
+ * contains both `noopener` and `noreferrer` (additive — preserves any
39
+ * consumer-supplied `rel` tokens like `nofollow`, `me`, `bookmark`) and
40
+ * appends a visually-hidden `(opens in new tab)` `<span>` to the host's
41
+ * accessible name. The suffix is suppressed when the consumer has set
42
+ * their own `aria-label` (the consumer is presumed to have authored the
43
+ * complete accessible name).
44
+ *
45
+ * 2. **Disabled-link bundle.** When `kjDisabled` is `true`, the directive
46
+ * sets `aria-disabled="true"`, `tabindex="-1"`, and capture-phase
47
+ * intercepts both `click` and `keydown.enter` to call `preventDefault()`
48
+ * and `stopImmediatePropagation()`. Capture phase ensures the suppression
49
+ * runs before any consumer-bound `(click)` handler and before Angular
50
+ * Router's bubble-phase click handler — so `<a kjLink routerLink="/foo"
51
+ * [kjDisabled]="true">` does not navigate either.
52
+ *
53
+ * 3. **Underline-mode reflection.** `kjUnderline: 'always' | 'hover' |
54
+ * 'none'` reflects to `[attr.data-underline]` for the components-package
55
+ * CSS to key off. The `kj-prose a` selector overrides only when the
56
+ * directive's input is left at its default — explicit consumer values
57
+ * win over the prose container's defaults.
58
+ *
59
+ * @example
60
+ * ```html
61
+ * <a kjLink href="/about">Inline link</a>
62
+ * <a kjLink href="https://example.com" target="_blank">External</a>
63
+ * <a kjLink [kjDisabled]="!ready()" routerLink="/billing">Manage billing</a>
64
+ * ```
65
+ *
66
+ * @doc-category Core/Navigation
67
+ * @doc
68
+ * @doc-name link
69
+ * @doc-description Adds disabled-link, external-link, and underline-mode handling to a native anchor.
70
+ * @doc-is-main
71
+ */
72
+ @Directive({
73
+ selector: 'a[kjLink], [kjLink]',
74
+ standalone: true,
75
+ hostDirectives: [
76
+ { directive: KjVariant, inputs: ['kjVariant'] },
77
+ { directive: KjSize, inputs: ['kjSize'] },
78
+ KjFocusRing,
79
+ { directive: KjDisabled, inputs: ['kjDisabled'] },
80
+ ],
81
+ providers: [...bindPresets(KJ_LINK_CONFIG)],
82
+ host: {
83
+ '[attr.data-underline]': 'kjUnderline()',
84
+ '[attr.data-external]': 'isExternal() ? "true" : null',
85
+ '[attr.tabindex]': 'kjDisabled() ? "-1" : null',
86
+ '[attr.rel]': 'relAttr()',
87
+ },
88
+ })
89
+ export class KjLink {
90
+ private readonly el = inject<ElementRef<HTMLElement>>(ElementRef);
91
+ private readonly document = inject(DOCUMENT);
92
+
93
+ /**
94
+ * Disables the link. Forwarded to `KjDisabled` (which reflects
95
+ * `aria-disabled` and `data-disabled`); the directive additionally sets
96
+ * `tabindex="-1"` and intercepts `click` + `keydown.enter` in the capture
97
+ * phase.
98
+ */
99
+ readonly kjDisabled = input(false);
100
+
101
+ /**
102
+ * Underline policy. `'always'` for in-content links (WCAG 1.4.1 — colour
103
+ * is not the only means); `'hover'` for standalone links sitting in a
104
+ * group of links that already signals "this is a link" via layout;
105
+ * `'none'` for the rare cases where the consumer wants to suppress the
106
+ * underline entirely (e.g. icon-text links inside breadcrumb separators).
107
+ * Reflects `[attr.data-underline]`.
108
+ */
109
+ readonly kjUnderline = input<'always' | 'hover' | 'none'>('hover');
110
+
111
+ /**
112
+ * External-link tri-state. `undefined` (default) auto-detects from the
113
+ * host's `target` attribute (`target="_blank"` → external). `true` forces
114
+ * external treatment regardless of `target` (e.g. for cross-origin URLs
115
+ * served through the app's router that still warrant the indicator).
116
+ * `false` forces internal treatment even when `target="_blank"` is
117
+ * present (suppresses the icon and AT suffix).
118
+ */
119
+ readonly kjExternal = input<boolean | undefined>(undefined);
120
+
121
+ /** Whether the host element currently carries `target="_blank"`. */
122
+ protected readonly hasBlankTarget = computed(() => this.targetSignal() === '_blank');
123
+
124
+ /** Computed external state — explicit input wins, else target-based auto-detect. */
125
+ protected readonly isExternal = computed(() => {
126
+ const explicit = this.kjExternal();
127
+ if (explicit !== undefined) return explicit;
128
+ return this.hasBlankTarget();
129
+ });
130
+
131
+ /**
132
+ * `rel` attribute computation. Additive: existing tokens (`nofollow`,
133
+ * `me`, `bookmark`, …) are preserved; the directive only adds the missing
134
+ * `noopener` / `noreferrer` security tokens when external. When not
135
+ * external, the directive returns `null` so the consumer's literal `rel`
136
+ * attribute (read at construction) is preserved as-is.
137
+ */
138
+ protected readonly relAttr = computed(() => {
139
+ if (!this.isExternal()) return this.initialRel || null;
140
+ const tokens = new Set<string>();
141
+ if (this.initialRel) {
142
+ for (const token of this.initialRel.split(/\s+/u)) {
143
+ if (token) tokens.add(token);
144
+ }
145
+ }
146
+ tokens.add('noopener');
147
+ tokens.add('noreferrer');
148
+ return [...tokens].join(' ');
149
+ });
150
+
151
+ /**
152
+ * Snapshot of the host's `target` attribute. Re-read after the host
153
+ * renders — host bindings are evaluated synchronously on creation, so
154
+ * the SSR HTML carries the right `[data-external]` / `[rel]` values
155
+ * provided the consumer's `target` is set declaratively in the template
156
+ * (the common case).
157
+ */
158
+ private readonly targetSignal = (() => {
159
+ const initial = this.el.nativeElement.getAttribute('target');
160
+ return computed(() => initial);
161
+ })();
162
+
163
+ /** Snapshot of the host's `rel` attribute at construction time. */
164
+ private readonly initialRel: string | null = this.el.nativeElement.getAttribute('rel');
165
+
166
+ /** Reference to the injected AT suffix span, if any. Cleaned up on toggle. */
167
+ private suffixSpan: HTMLSpanElement | null = null;
168
+
169
+ constructor() {
170
+ // Capture-phase native listeners: fire BEFORE Angular's template-bound
171
+ // bubble-phase (click) handlers and before Angular Router's bubble-phase
172
+ // click handler on `<a routerLink>`, so `stopImmediatePropagation()`
173
+ // actually prevents both navigation and any consumer (click) handlers.
174
+ afterNextRender(() => {
175
+ const node = this.el.nativeElement;
176
+
177
+ const onClick = (event: Event) => {
178
+ if (this.kjDisabled()) {
179
+ event.preventDefault();
180
+ event.stopImmediatePropagation();
181
+ }
182
+ };
183
+
184
+ const onKeydown = (event: KeyboardEvent) => {
185
+ if (this.kjDisabled() && event.key === 'Enter') {
186
+ event.preventDefault();
187
+ event.stopImmediatePropagation();
188
+ }
189
+ };
190
+
191
+ node.addEventListener('click', onClick, { capture: true });
192
+ node.addEventListener('keydown', onKeydown, { capture: true });
193
+ });
194
+
195
+ // External-link AT suffix injection. Runs reactively so the suffix
196
+ // appears / disappears as `kjExternal` (or the host's `target`) changes.
197
+ // The effect itself is created in injection context (constructor); its
198
+ // first run happens after the first change-detection pass, by which
199
+ // point the host element has been attached to the DOM in browser
200
+ // contexts. Use injected DOCUMENT — prerender VMs may have no global `document`.
201
+ effect(() => {
202
+ const external = this.isExternal();
203
+ const doc = this.document;
204
+ if (!doc.createElement) return;
205
+
206
+ const node = this.el.nativeElement;
207
+ const consumerOwnsName = node.hasAttribute('aria-label');
208
+
209
+ if (external && !consumerOwnsName) {
210
+ if (!this.suffixSpan) {
211
+ const span = doc.createElement('span');
212
+ span.className = KJ_LINK_EXTERNAL_SUFFIX_CLASS;
213
+ span.setAttribute('style', KJ_LINK_VISUALLY_HIDDEN_STYLE);
214
+ // Leading space so AT reads "Documentation (opens in new tab)"
215
+ // not "Documentation(opens in new tab)".
216
+ span.textContent = ` ${KJ_LINK_EXTERNAL_SUFFIX_TEXT}`;
217
+ node.appendChild(span);
218
+ this.suffixSpan = span;
219
+ }
220
+ } else if (this.suffixSpan) {
221
+ this.suffixSpan.remove();
222
+ this.suffixSpan = null;
223
+ }
224
+ });
225
+ }
226
+ }
@@ -0,0 +1,11 @@
1
+ export {
2
+ KjList,
3
+ KjListRow,
4
+ KJ_LIST,
5
+ KJ_LIST_ROW,
6
+ } from './list';
7
+ export type {
8
+ KjListAs,
9
+ KjListContext,
10
+ KjListRowContext,
11
+ } from './list';