@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/form/form.ts CHANGED
@@ -1,46 +1,326 @@
1
- import { Directive, computed, inject, input } from '@angular/core';
2
- import { KJ_FORM_FIELD, KjFormFieldContext } from './form.context';
1
+ import {
2
+ Directive,
3
+ ElementRef,
4
+ Injector,
5
+ afterNextRender,
6
+ booleanAttribute,
7
+ computed,
8
+ inject,
9
+ input,
10
+ model,
11
+ output,
12
+ signal,
13
+ } from '@angular/core';
14
+ import { AbstractControl, FormArray, FormGroup, FormGroupDirective, NgForm } from '@angular/forms';
15
+ import { Observable } from 'rxjs';
16
+ import {
17
+ InvalidControlInfo,
18
+ KJ_FORM,
19
+ KjFormContext,
20
+ KjFormControlRegistration,
21
+ } from './form.context';
3
22
 
4
23
  /**
5
- * Container for a form field. Groups label, input, and error message.
24
+ * Async submit handler signature accepted by `[kjAsyncSubmit]`. The directive
25
+ * awaits the returned promise/observable and toggles `submitting` for its
26
+ * lifetime.
27
+ */
28
+ export type KjAsyncSubmitHandler<T = unknown> = (
29
+ value: T,
30
+ ) => void | Promise<unknown> | Observable<unknown>;
31
+
32
+ /**
33
+ * Coordinator directive applied to a `<form>` element. Owns the submit
34
+ * pipeline (intercept, validity check, focus-on-first-error, scroll-into-view,
35
+ * async submitting state, mark-all-as-touched, optional error summary). Defers
36
+ * to Angular Forms (`FormGroup` / `NgForm`) for validation; this directive is
37
+ * a coordinator, not a state holder.
38
+ *
39
+ * - Emits `(kjSubmit)` only when the form is valid (replaces hand-rolled
40
+ * `(ngSubmit)` + `if (form.invalid) return` patterns).
41
+ * - Emits `(kjInvalidSubmit)` when the user submitted but validity failed,
42
+ * useful for analytics.
43
+ * - When `[kjAsyncSubmit]` is set, awaits its returned `Promise`/`Observable`
44
+ * and reflects `aria-busy="true"` / `data-submitting=""` for the lifetime.
45
+ *
6
46
  * @example
7
47
  * ```html
8
- * <div kjFormField [kjFieldInvalid]="ctrl.invalid && ctrl.touched">
9
- * <label kjFormLabel for="name">Name</label>
10
- * <input id="name" kjInput />
11
- * <span kjFormError>Name is required</span>
12
- * </div>
48
+ * <form kjForm [formGroup]="form" (kjSubmit)="onSubmit($event)">
49
+ * <input formControlName="email" kjInput />
50
+ * <button type="submit">Sign in</button>
51
+ * </form>
13
52
  * ```
14
- * @category Core/Inputs
53
+ * @doc-category Core/Inputs
54
+ * @doc
55
+ * @doc-name form
56
+ * @doc-description Coordinates form submit with validation, first-error focus, and async submitting state.
57
+ * @doc-is-main
15
58
  */
16
59
  @Directive({
17
- selector: '[kjFormField]', standalone: true,
18
- providers: [{ provide: KJ_FORM_FIELD, useExisting: KjFormField }],
19
- host: { '[attr.data-invalid]': 'kjFieldInvalid() ? "" : null' },
60
+ selector: 'form[kjForm]',
61
+ standalone: true,
62
+ exportAs: 'kjForm',
63
+ providers: [{ provide: KJ_FORM, useExisting: KjForm }],
64
+ host: {
65
+ '[attr.aria-busy]': 'submitting() ? "true" : null',
66
+ '[attr.aria-describedby]': 'errorSummaryId() ?? null',
67
+ '[attr.data-submitting]': 'submitting() ? "" : null',
68
+ '[attr.data-invalid]': 'invalid() ? "" : null',
69
+ '(submit)': 'onNativeSubmit($event)',
70
+ },
20
71
  })
21
- export class KjFormField implements KjFormFieldContext {
22
- /** Whether the field is in an invalid state. */
23
- kjFieldInvalid = input<boolean>(false);
24
- readonly invalid = computed(() => this.kjFieldInvalid());
72
+ export class KjForm implements KjFormContext {
73
+ private readonly el = inject<ElementRef<HTMLFormElement>>(ElementRef);
74
+ private readonly injector = inject(Injector);
75
+ private readonly ngForm = inject(NgForm, { optional: true, self: true });
76
+ private readonly formGroupDir = inject(FormGroupDirective, { optional: true, self: true });
77
+
78
+ // ── Inputs ───────────────────────────────────────────────────────────
79
+ /** When `false`, skips moving focus to the first invalid control on invalid submit. */
80
+ readonly kjFocusOnError = input(true, { transform: booleanAttribute });
81
+ /** When `false`, does not call `scrollIntoView` on the focused control. */
82
+ readonly kjScrollOnError = input(true, { transform: booleanAttribute });
83
+ /** Forwarded to `scrollIntoView({ behavior })`. Auto-downgrades to `'auto'` under `prefers-reduced-motion`. */
84
+ readonly kjScrollBehavior = input<ScrollBehavior>('smooth');
85
+ /** Forwarded to `scrollIntoView({ block })`. */
86
+ readonly kjScrollBlock = input<ScrollLogicalPosition>('center');
87
+ /** When `true` (default), calls `markAllAsTouched()` before validity check so all errors show together. */
88
+ readonly kjMarkAllAsTouchedOnSubmit = input(true, { transform: booleanAttribute });
89
+ /** When `true`, calls `form.reset()` after a successful async submit resolves. */
90
+ readonly kjResetOnSuccess = input(false, { transform: booleanAttribute });
91
+ /** Optional async submit handler. When set, the directive awaits its result and toggles `submitting`. */
92
+ readonly kjAsyncSubmit = input<KjAsyncSubmitHandler | undefined>(undefined);
93
+
94
+ // ── Outputs ──────────────────────────────────────────────────────────
95
+ /** Emits the form value when the user submits and the form is valid. */
96
+ readonly kjSubmit = output<unknown>();
97
+ /** Emits the FormGroup when the user submits but validity fails. */
98
+ readonly kjInvalidSubmit = output<FormGroup>();
99
+
100
+ // ── Models / state ───────────────────────────────────────────────────
101
+ /** Two-way bindable submitting flag. When unbound, the directive owns it internally. */
102
+ readonly kjSubmitting = model(false);
103
+
104
+ private readonly _submitted = signal(false);
105
+ private readonly _invalid = signal(false);
106
+ private readonly _invalidControls = signal<readonly InvalidControlInfo[]>([]);
107
+ private readonly _errorSummaryId = signal<string | null>(null);
108
+ private readonly registry = new Map<string, KjFormControlRegistration>();
109
+ private prefersReducedMotion = false;
110
+
111
+ // ── Public surface (KjFormContext) ───────────────────────────────────
112
+ readonly form = computed<FormGroup | null>(() => {
113
+ if (this.formGroupDir) return this.formGroupDir.form;
114
+ if (this.ngForm) return this.ngForm.form;
115
+ return null;
116
+ });
117
+ readonly submitting = computed(() => this.kjSubmitting());
118
+ readonly submitted = this._submitted.asReadonly();
119
+ readonly invalid = this._invalid.asReadonly();
120
+ readonly invalidControls = this._invalidControls.asReadonly();
121
+ readonly errorSummaryId = this._errorSummaryId.asReadonly();
122
+
123
+ constructor() {
124
+ afterNextRender(() => {
125
+ try {
126
+ this.prefersReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches;
127
+ } catch {
128
+ this.prefersReducedMotion = false;
129
+ }
130
+ });
131
+ }
132
+
133
+ registerControl(reg: KjFormControlRegistration): () => void {
134
+ this.registry.set(reg.path, reg);
135
+ return () => {
136
+ const cur = this.registry.get(reg.path);
137
+ if (cur === reg) this.registry.delete(reg.path);
138
+ };
139
+ }
140
+
141
+ /** Internal: called by `KjFormErrorSummary` to advertise its `id` to the form. */
142
+ setErrorSummaryId(id: string | null): void {
143
+ this._errorSummaryId.set(id);
144
+ }
145
+
146
+ /** Imperative submit — equivalent to dispatching a `submit` event on the host. */
147
+ submit(): void {
148
+ this.el.nativeElement.requestSubmit();
149
+ }
150
+
151
+ /** @internal — host listener target. */
152
+ onNativeSubmit(event: Event): void {
153
+ event.preventDefault();
154
+ // Drop submit attempts while a previous submission is still in flight.
155
+ if (this.submitting()) return;
156
+
157
+ const form = this.form();
158
+ this._submitted.set(true);
159
+
160
+ if (!form) {
161
+ // No Angular forms binding — log a dev-mode warning and bail out.
162
+ if (typeof ngDevMode !== 'undefined' && ngDevMode) {
163
+ console.warn(
164
+ '[KjForm] <form kjForm> has no `[formGroup]` or `ngForm` binding. The directive is a coordinator over Angular Forms and needs one to function.',
165
+ );
166
+ }
167
+ return;
168
+ }
169
+
170
+ if (this.kjMarkAllAsTouchedOnSubmit()) {
171
+ form.markAllAsTouched();
172
+ }
173
+
174
+ if (form.invalid) {
175
+ this._invalid.set(true);
176
+ const invalidControls = collectInvalidControls(form);
177
+ this._invalidControls.set(this.attachLabels(invalidControls));
178
+ this.kjInvalidSubmit.emit(form);
179
+ this.handleInvalidSubmit(invalidControls);
180
+ return;
181
+ }
182
+
183
+ this._invalid.set(false);
184
+ this._invalidControls.set([]);
185
+
186
+ const handler = this.kjAsyncSubmit();
187
+ if (handler) {
188
+ this.runAsyncSubmit(handler, form);
189
+ } else {
190
+ this.kjSubmit.emit(form.value);
191
+ }
192
+ }
193
+
194
+ // ── Internals ────────────────────────────────────────────────────────
195
+
196
+ private attachLabels(invalid: ReadonlyArray<{ path: string; control: AbstractControl }>): InvalidControlInfo[] {
197
+ return invalid.map((entry) => {
198
+ const reg = this.registry.get(entry.path);
199
+ return { ...entry, label: reg?.label ?? humanizePath(entry.path) };
200
+ });
201
+ }
202
+
203
+ private handleInvalidSubmit(invalid: ReadonlyArray<{ path: string }>): void {
204
+ if (!this.kjFocusOnError() || invalid.length === 0) return;
205
+ const first = invalid[0];
206
+ const reg = this.registry.get(first.path);
207
+ const target = reg?.element ?? this.fallbackQuery(first.path);
208
+ if (!target) return;
209
+ try {
210
+ target.focus({ preventScroll: !this.kjScrollOnError() });
211
+ } catch {
212
+ // Some hosts (SSR/jsdom) may not implement focus options — ignore.
213
+ target.focus?.();
214
+ }
215
+ if (this.kjScrollOnError()) {
216
+ const behavior = this.prefersReducedMotion ? 'auto' : this.kjScrollBehavior();
217
+ target.scrollIntoView?.({ block: this.kjScrollBlock(), behavior });
218
+ }
219
+ }
220
+
221
+ private fallbackQuery(path: string): HTMLElement | null {
222
+ const root = this.el.nativeElement;
223
+ const escaped = cssEscape(path);
224
+ return (
225
+ root.querySelector<HTMLElement>(`[formcontrolname="${escaped}"]`) ??
226
+ root.querySelector<HTMLElement>(`[ng-reflect-name="${escaped}"]`) ??
227
+ null
228
+ );
229
+ }
230
+
231
+ private runAsyncSubmit(handler: KjAsyncSubmitHandler, form: FormGroup): void {
232
+ let result: ReturnType<KjAsyncSubmitHandler>;
233
+ this.kjSubmitting.set(true);
234
+ try {
235
+ result = handler(form.value);
236
+ } catch (err) {
237
+ this.kjSubmitting.set(false);
238
+ throw err;
239
+ }
240
+ if (result instanceof Promise) {
241
+ result.then(
242
+ () => this.afterAsync(form, true),
243
+ () => this.afterAsync(form, false),
244
+ );
245
+ // Also emit the synchronous (kjSubmit) for consumers that use both shapes.
246
+ this.kjSubmit.emit(form.value);
247
+ return;
248
+ }
249
+ if (isObservable(result)) {
250
+ const sub = result.subscribe({
251
+ complete: () => this.afterAsync(form, true),
252
+ error: () => this.afterAsync(form, false),
253
+ });
254
+ // Defensive: if the observable completes synchronously, the subscribe
255
+ // call already cleaned us up. Otherwise this no-op preserves cleanup
256
+ // behaviour (we don't track for unsubscription beyond completion).
257
+ void sub;
258
+ this.kjSubmit.emit(form.value);
259
+ return;
260
+ }
261
+ // Sync handler — release the flag immediately and emit.
262
+ this.kjSubmitting.set(false);
263
+ this.kjSubmit.emit(form.value);
264
+ }
265
+
266
+ private afterAsync(form: FormGroup, success: boolean): void {
267
+ this.kjSubmitting.set(false);
268
+ if (success && this.kjResetOnSuccess()) {
269
+ form.reset();
270
+ this._submitted.set(false);
271
+ this._invalid.set(false);
272
+ this._invalidControls.set([]);
273
+ }
274
+ }
25
275
  }
26
276
 
27
- /**
28
- * Label element within a form field.
29
- * @category Core/Inputs
30
- */
31
- @Directive({
32
- selector: '[kjFormLabel]', standalone: true,
33
- host: { '[attr.data-invalid]': 'ctx.invalid() ? "" : null' },
34
- })
35
- export class KjFormLabel { readonly ctx = inject(KJ_FORM_FIELD); }
277
+ // ─── Helpers ──────────────────────────────────────────────────────────
36
278
 
37
- /**
38
- * Error message element. Visible only when field is invalid.
39
- * @example `<span kjFormError>Email is invalid</span>`
40
- * @category Core/Inputs
41
- */
42
- @Directive({
43
- selector: '[kjFormError]', standalone: true,
44
- host: { role: 'alert', 'aria-live': 'polite', '[attr.hidden]': '!ctx.invalid() ? "" : null' },
45
- })
46
- export class KjFormError { readonly ctx = inject(KJ_FORM_FIELD); }
279
+ /** Walks a `FormGroup` / `FormArray` tree depth-first, returning invalid leaves. */
280
+ function collectInvalidControls(
281
+ root: AbstractControl,
282
+ ): ReadonlyArray<{ path: string; control: AbstractControl }> {
283
+ const out: { path: string; control: AbstractControl }[] = [];
284
+ const walk = (ctrl: AbstractControl, path: string): void => {
285
+ if (ctrl instanceof FormGroup) {
286
+ for (const [name, child] of Object.entries(ctrl.controls)) {
287
+ walk(child, path ? `${path}.${name}` : name);
288
+ }
289
+ return;
290
+ }
291
+ if (ctrl instanceof FormArray) {
292
+ ctrl.controls.forEach((child, i) => walk(child, `${path}.${i}`));
293
+ return;
294
+ }
295
+ if (ctrl.invalid) {
296
+ out.push({ path, control: ctrl });
297
+ }
298
+ };
299
+ walk(root, '');
300
+ return out;
301
+ }
302
+
303
+ /** Converts `'address.street'` → `'Address street'` for fallback labels. */
304
+ function humanizePath(path: string): string {
305
+ if (!path) return '';
306
+ const last = path.split('.').pop() ?? path;
307
+ const words = last.replace(/([A-Z])/g, ' $1').replace(/[_-]+/g, ' ').trim();
308
+ return words.charAt(0).toUpperCase() + words.slice(1);
309
+ }
310
+
311
+ /** Type guard for RxJS observable. Avoids a hard dep on `isObservable` in tests. */
312
+ function isObservable(value: unknown): value is Observable<unknown> {
313
+ return (
314
+ !!value &&
315
+ typeof value === 'object' &&
316
+ typeof (value as { subscribe?: unknown }).subscribe === 'function'
317
+ );
318
+ }
319
+
320
+ /** Minimal CSS.escape polyfill for `formcontrolname` attribute selectors. */
321
+ function cssEscape(value: string): string {
322
+ if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
323
+ return CSS.escape(value);
324
+ }
325
+ return value.replace(/[^a-zA-Z0-9_-]/g, (ch) => `\\${ch}`);
326
+ }
package/src/form/index.ts CHANGED
@@ -1,2 +1,11 @@
1
- export { KjFormField, KjFormLabel, KjFormError } from './form';
2
- export { KJ_FORM_FIELD, type KjFormFieldContext } from './form.context';
1
+ export { KjFormField, KjFormLabel, KjFormError } from './form-field';
2
+ export { KJ_FORM_FIELD, type KjFormFieldContext } from './form-field.context';
3
+
4
+ export { KjForm, type KjAsyncSubmitHandler } from './form';
5
+ export { KjFormErrorSummary } from './form-error-summary';
6
+ export {
7
+ KJ_FORM,
8
+ type KjFormContext,
9
+ type KjFormControlRegistration,
10
+ type InvalidControlInfo,
11
+ } from './form.context';
@@ -0,0 +1,14 @@
1
+ # Icon — E2E coverage notes
2
+
3
+ The core icon layer has no consumer-visible UI in this session (no Lucide
4
+ adapter, no example refactor). The directive-level integration tests in
5
+ `icon.directive.spec.ts` cover the end-to-end path through DI:
6
+
7
+ - Eager registration via `provideIcons` → directive renders the registered value.
8
+ - Async loader path → directive renders nothing while pending, then re-renders on success.
9
+ - Decorative vs meaningful a11y modes → axe-clean.
10
+ - Mode switching (svg vs font) → correct CSS variable wrapping and `data-kj-icon-mode`.
11
+ - Selector restriction → directive only attaches to `span` and `i` hosts; ignores other tags.
12
+
13
+ Browser-level E2E lands with the Lucide adapter follow-up, when there's a
14
+ visible component to drive.
@@ -0,0 +1,40 @@
1
+ /* packages/core/src/icon/icon.css
2
+ *
3
+ * Renders icons set up by `KjIconDirective`. The directive writes:
4
+ * --kj-icon → CSS-ready value (`url("...")` or `"glyph"`)
5
+ * data-kj-icon-mode → "svg" | "font"
6
+ *
7
+ * SVG / CSS-mask mode (default): uses `mask-image` + `currentColor`.
8
+ * Works for any monochrome SVG including data: URIs.
9
+ * Font mode: uses pseudo-element `content`. Consumers must load the icon
10
+ * font and (optionally) set `--kj-icon-font` on a parent.
11
+ */
12
+
13
+ .kj-icon {
14
+ display: inline-block;
15
+ width: 1em;
16
+ height: 1em;
17
+ flex-shrink: 0;
18
+ line-height: 1;
19
+ vertical-align: middle;
20
+ }
21
+
22
+ .kj-icon[data-kj-icon-mode='svg'] {
23
+ background-color: currentColor;
24
+ mask-image: var(--kj-icon);
25
+ -webkit-mask-image: var(--kj-icon);
26
+ mask-size: contain;
27
+ -webkit-mask-size: contain;
28
+ mask-repeat: no-repeat;
29
+ -webkit-mask-repeat: no-repeat;
30
+ mask-position: center;
31
+ -webkit-mask-position: center;
32
+ }
33
+
34
+ .kj-icon[data-kj-icon-mode='font'] {
35
+ font-family: var(--kj-icon-font, inherit);
36
+ }
37
+
38
+ .kj-icon[data-kj-icon-mode='font']::before {
39
+ content: var(--kj-icon);
40
+ }
@@ -0,0 +1,198 @@
1
+ import { render } from '@testing-library/angular';
2
+ import { axe, toHaveNoViolations } from 'jest-axe';
3
+ import { describe, expect, it } from 'vitest';
4
+ import { KjIconDirective } from './icon.directive';
5
+ import { provideIcons, provideIconLoader } from './icon.providers';
6
+
7
+ expect.extend(toHaveNoViolations);
8
+
9
+ describe('KjIconDirective — selector', () => {
10
+ it('attaches to <i> hosts', async () => {
11
+ const { container } = await render(`<i [kjIcon]="'settings'"></i>`, {
12
+ imports: [KjIconDirective],
13
+ providers: [provideIcons({ settings: 'url("a")' })],
14
+ });
15
+ const host = container.querySelector('i')!;
16
+ expect(host.classList.contains('kj-icon')).toBe(true);
17
+ expect(host.getAttribute('data-kj-icon-mode')).toBe('svg');
18
+ });
19
+
20
+ it('does NOT attach to disallowed hosts (e.g. <div>)', async () => {
21
+ // Static `kjIcon` attribute (no binding) — Angular only matches the
22
+ // directive against the host element's tag, not the attribute binding.
23
+ const { container } = await render(`<div kjIcon="settings"></div>`, {
24
+ imports: [KjIconDirective],
25
+ providers: [provideIcons({ settings: 'url("a")' })],
26
+ });
27
+ const host = container.querySelector('div')!;
28
+ expect(host.classList.contains('kj-icon')).toBe(false);
29
+ expect(host.hasAttribute('data-kj-icon-mode')).toBe(false);
30
+ });
31
+ });
32
+
33
+ describe('KjIconDirective — svg mode (decorative default)', () => {
34
+ it('writes --kj-icon and data-kj-icon-mode="svg"', async () => {
35
+ const { container } = await render(`<span [kjIcon]="'settings'"></span>`, {
36
+ imports: [KjIconDirective],
37
+ providers: [provideIcons({ settings: 'url("a")' })],
38
+ });
39
+ const host = container.querySelector('span')!;
40
+ expect(host.getAttribute('data-kj-icon-mode')).toBe('svg');
41
+ expect(host.style.getPropertyValue('--kj-icon')).toBe('url("a")');
42
+ });
43
+
44
+ it('applies the .kj-icon class', async () => {
45
+ const { container } = await render(`<span [kjIcon]="'settings'"></span>`, {
46
+ imports: [KjIconDirective],
47
+ providers: [provideIcons({ settings: 'url("a")' })],
48
+ });
49
+ expect(container.querySelector('span')!.classList.contains('kj-icon')).toBe(true);
50
+ });
51
+
52
+ it('default = decorative: aria-hidden="true", no role, no aria-label', async () => {
53
+ const { container } = await render(`<span [kjIcon]="'settings'"></span>`, {
54
+ imports: [KjIconDirective],
55
+ providers: [provideIcons({ settings: 'url("a")' })],
56
+ });
57
+ const host = container.querySelector('span')!;
58
+ expect(host.getAttribute('aria-hidden')).toBe('true');
59
+ expect(host.hasAttribute('role')).toBe(false);
60
+ expect(host.hasAttribute('aria-label')).toBe(false);
61
+ });
62
+ });
63
+
64
+ describe('KjIconDirective — meaningful mode (kjIconLabel)', () => {
65
+ it('removes aria-hidden, sets role="img" and aria-label', async () => {
66
+ const { container } = await render(
67
+ `<span [kjIcon]="'alert'" [kjIconLabel]="'Warning'"></span>`,
68
+ {
69
+ imports: [KjIconDirective],
70
+ providers: [provideIcons({ alert: 'url("a")' })],
71
+ },
72
+ );
73
+ const host = container.querySelector('span')!;
74
+ expect(host.hasAttribute('aria-hidden')).toBe(false);
75
+ expect(host.getAttribute('role')).toBe('img');
76
+ expect(host.getAttribute('aria-label')).toBe('Warning');
77
+ });
78
+ });
79
+
80
+ describe('KjIconDirective — color tokens', () => {
81
+ it('inherits color by default (no inline color style)', async () => {
82
+ const { container } = await render(`<span [kjIcon]="'x'"></span>`, {
83
+ imports: [KjIconDirective],
84
+ providers: [provideIcons({ x: 'url("a")' })],
85
+ });
86
+ expect(container.querySelector('span')!.getAttribute('style') ?? '').not.toContain('color:');
87
+ });
88
+
89
+ it('writes color: var(--kj-color-icon-{token}) for non-inherit', async () => {
90
+ const { container } = await render(
91
+ `<span [kjIcon]="'x'" [kjIconColor]="'danger'"></span>`,
92
+ {
93
+ imports: [KjIconDirective],
94
+ providers: [provideIcons({ x: 'url("a")' })],
95
+ },
96
+ );
97
+ const styleAttr = container.querySelector('span')!.getAttribute('style') ?? '';
98
+ expect(styleAttr).toContain('color: var(--kj-color-icon-danger)');
99
+ });
100
+
101
+ it('"inherit" color writes no inline color', async () => {
102
+ const { container } = await render(
103
+ `<span [kjIcon]="'x'" [kjIconColor]="'inherit'"></span>`,
104
+ {
105
+ imports: [KjIconDirective],
106
+ providers: [provideIcons({ x: 'url("a")' })],
107
+ },
108
+ );
109
+ expect(container.querySelector('span')!.getAttribute('style') ?? '').not.toContain('color:');
110
+ });
111
+ });
112
+
113
+ describe('KjIconDirective — size tokens', () => {
114
+ it('writes font-size: var(--kj-icon-size-{token})', async () => {
115
+ const { container } = await render(
116
+ `<span [kjIcon]="'x'" [kjIconSize]="'lg'"></span>`,
117
+ {
118
+ imports: [KjIconDirective],
119
+ providers: [provideIcons({ x: 'url("a")' })],
120
+ },
121
+ );
122
+ const styleAttr = container.querySelector('span')!.getAttribute('style') ?? '';
123
+ expect(styleAttr).toContain('font-size: var(--kj-icon-size-lg)');
124
+ });
125
+ });
126
+
127
+ describe('KjIconDirective — font mode', () => {
128
+ it('reflects data-kj-icon-mode="font" for @font.* names', async () => {
129
+ const { container } = await render(
130
+ `<span [kjIcon]="'@font.fa-cog'"></span>`,
131
+ {
132
+ imports: [KjIconDirective],
133
+ providers: [provideIcons({ '@font.fa-cog': '"\\f013"' })],
134
+ },
135
+ );
136
+ const host = container.querySelector('span')!;
137
+ expect(host.getAttribute('data-kj-icon-mode')).toBe('font');
138
+ expect(host.style.getPropertyValue('--kj-icon')).toBe('"\\f013"');
139
+ });
140
+ });
141
+
142
+ describe('KjIconDirective — loader pending', () => {
143
+ it('renders no --kj-icon value while async load is pending', async () => {
144
+ const neverResolves = () =>
145
+ new Promise<string>(() => {/* hangs forever */});
146
+ const { container } = await render(`<span [kjIcon]="'missing'"></span>`, {
147
+ imports: [KjIconDirective],
148
+ providers: [provideIconLoader(neverResolves)],
149
+ });
150
+ const host = container.querySelector('span')!;
151
+ expect(host.style.getPropertyValue('--kj-icon')).toBe('');
152
+ });
153
+
154
+ it('updates --kj-icon once loader resolves', async () => {
155
+ let resolveLoad!: (v: string) => void;
156
+ const loader = (_n: string) =>
157
+ new Promise<string>((res) => {
158
+ resolveLoad = res;
159
+ });
160
+ const { container, fixture } = await render(
161
+ `<span [kjIcon]="'settings'"></span>`,
162
+ {
163
+ imports: [KjIconDirective],
164
+ providers: [provideIconLoader(loader)],
165
+ },
166
+ );
167
+ const host = container.querySelector('span')!;
168
+ expect(host.style.getPropertyValue('--kj-icon')).toBe('');
169
+
170
+ resolveLoad('url("loaded")');
171
+ await Promise.resolve();
172
+ await Promise.resolve();
173
+ fixture.detectChanges();
174
+
175
+ expect(host.style.getPropertyValue('--kj-icon')).toBe('url("loaded")');
176
+ });
177
+ });
178
+
179
+ describe('KjIconDirective — a11y audits', () => {
180
+ it('decorative icon passes axe', async () => {
181
+ const { container } = await render(`<span [kjIcon]="'settings'"></span>`, {
182
+ imports: [KjIconDirective],
183
+ providers: [provideIcons({ settings: 'url("a")' })],
184
+ });
185
+ expect(await axe(container)).toHaveNoViolations();
186
+ });
187
+
188
+ it('meaningful icon passes axe', async () => {
189
+ const { container } = await render(
190
+ `<span [kjIcon]="'alert'" [kjIconLabel]="'Warning'"></span>`,
191
+ {
192
+ imports: [KjIconDirective],
193
+ providers: [provideIcons({ alert: 'url("a")' })],
194
+ },
195
+ );
196
+ expect(await axe(container)).toHaveNoViolations();
197
+ });
198
+ });