@kouji-ui/core 0.0.4 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (534) hide show
  1. package/.turbo/turbo-build.log +7 -4
  2. package/CHANGELOG.md +169 -0
  3. package/ng-package.json +5 -1
  4. package/package.json +18 -6
  5. package/src/a11y/aria-describedby.ts +7 -3
  6. package/src/a11y/aria-labelledby.spec.ts +55 -0
  7. package/src/a11y/aria-labelledby.ts +38 -0
  8. package/src/a11y/focus-trap.ts +4 -2
  9. package/src/a11y/index.ts +1 -0
  10. package/src/a11y/live-region.ts +4 -2
  11. package/src/a11y/roving-tabindex.spec.ts +139 -0
  12. package/src/a11y/roving-tabindex.ts +56 -7
  13. package/src/a11y/visually-hidden.ts +4 -2
  14. package/src/accordion/accordion.context.ts +22 -0
  15. package/src/accordion/accordion.spec.ts +233 -23
  16. package/src/accordion/accordion.ts +311 -39
  17. package/src/accordion/index.ts +8 -2
  18. package/src/alert/alert.context.ts +28 -0
  19. package/src/alert/alert.spec.ts +217 -0
  20. package/src/alert/alert.ts +417 -0
  21. package/src/alert/config.ts +46 -0
  22. package/src/alert/index.ts +19 -0
  23. package/src/avatar/avatar-group.context.ts +45 -0
  24. package/src/avatar/avatar-group.spec.ts +358 -0
  25. package/src/avatar/avatar-group.ts +259 -0
  26. package/src/avatar/avatar.ts +17 -4
  27. package/src/avatar/index.ts +11 -1
  28. package/src/badge/badge.spec.ts +8 -0
  29. package/src/badge/badge.ts +24 -3
  30. package/src/breadcrumb/breadcrumb-current.ts +38 -0
  31. package/src/breadcrumb/breadcrumb-ellipsis.ts +71 -0
  32. package/src/breadcrumb/breadcrumb-item.ts +41 -0
  33. package/src/breadcrumb/breadcrumb-link.ts +44 -0
  34. package/src/breadcrumb/breadcrumb-list.ts +42 -0
  35. package/src/breadcrumb/breadcrumb-separator.ts +45 -0
  36. package/src/breadcrumb/breadcrumb.context.ts +45 -0
  37. package/src/breadcrumb/breadcrumb.spec.ts +354 -0
  38. package/src/breadcrumb/breadcrumb.ts +239 -0
  39. package/src/breadcrumb/config.ts +84 -0
  40. package/src/breadcrumb/index.ts +18 -0
  41. package/src/button/_examples/button.example.ts +71 -0
  42. package/src/button/_examples/button.finance.example.ts +83 -0
  43. package/src/button/_examples/button.retro.example.ts +91 -0
  44. package/src/button/_examples/button.sizes.example.ts +60 -0
  45. package/src/button/_examples/index.ts +6 -0
  46. package/src/button/button.spec.ts +139 -8
  47. package/src/button/button.ts +114 -16
  48. package/src/button/config.spec.ts +33 -0
  49. package/src/button/config.ts +45 -0
  50. package/src/button/index.ts +2 -1
  51. package/src/button-group/button-group.context.ts +28 -0
  52. package/src/button-group/button-group.spec.ts +99 -0
  53. package/src/button-group/button-group.ts +99 -0
  54. package/src/button-group/index.ts +2 -0
  55. package/src/calendar/calendar-day.ts +102 -0
  56. package/src/calendar/calendar-grid.ts +90 -0
  57. package/src/calendar/calendar-header.ts +65 -0
  58. package/src/calendar/calendar.context.ts +53 -0
  59. package/src/calendar/calendar.spec.ts +225 -0
  60. package/src/calendar/calendar.ts +233 -0
  61. package/src/calendar/date-utils.ts +225 -0
  62. package/src/calendar/index.ts +28 -0
  63. package/src/carousel/carousel.context.ts +81 -0
  64. package/src/carousel/carousel.spec.ts +337 -0
  65. package/src/carousel/carousel.ts +919 -0
  66. package/src/carousel/index.ts +22 -0
  67. package/src/cascade-select/cascade-select-option.ts +205 -0
  68. package/src/cascade-select/cascade-select-panel.ts +106 -0
  69. package/src/cascade-select/cascade-select-root.ts +265 -0
  70. package/src/cascade-select/cascade-select-sub-panel.ts +168 -0
  71. package/src/cascade-select/cascade-select-trigger.ts +44 -0
  72. package/src/cascade-select/cascade-select.context.ts +71 -0
  73. package/src/cascade-select/cascade-select.spec.ts +241 -0
  74. package/src/cascade-select/cascade-select.ts +5 -0
  75. package/src/cascade-select/index.ts +11 -0
  76. package/src/chart/chart.ts +6 -2
  77. package/src/chat/chat-avatar.ts +31 -0
  78. package/src/chat/chat-bubble.ts +49 -0
  79. package/src/chat/chat-footer.ts +67 -0
  80. package/src/chat/chat-header.ts +41 -0
  81. package/src/chat/chat-log.ts +43 -0
  82. package/src/chat/chat.context.ts +64 -0
  83. package/src/chat/chat.spec.ts +341 -0
  84. package/src/chat/chat.ts +191 -0
  85. package/src/chat/config.ts +53 -0
  86. package/src/chat/index.ts +21 -0
  87. package/src/checkbox/checkbox.ts +9 -2
  88. package/src/color-picker/color-picker.context.ts +74 -0
  89. package/src/color-picker/color-picker.spec.ts +241 -0
  90. package/src/color-picker/color-picker.ts +698 -0
  91. package/src/color-picker/color-picker.utils.ts +140 -0
  92. package/src/color-picker/index.ts +28 -0
  93. package/src/combobox/combobox-input.ts +143 -0
  94. package/src/combobox/combobox-listbox.ts +49 -0
  95. package/src/combobox/combobox-option.ts +43 -0
  96. package/src/combobox/combobox-root.ts +281 -0
  97. package/src/combobox/combobox.spec.ts +75 -0
  98. package/src/combobox/combobox.ts +4 -0
  99. package/src/combobox/index.ts +4 -0
  100. package/src/command-palette/command-empty.ts +36 -0
  101. package/src/command-palette/command-group.ts +21 -0
  102. package/src/command-palette/command-input.ts +70 -0
  103. package/src/command-palette/command-item.ts +60 -0
  104. package/src/command-palette/command-list.ts +25 -0
  105. package/src/command-palette/command-palette-dialog.ts +74 -0
  106. package/src/command-palette/command-palette-trigger.ts +64 -0
  107. package/src/command-palette/command-palette.filters.ts +53 -0
  108. package/src/command-palette/command-palette.spec.ts +349 -0
  109. package/src/command-palette/command-palette.ts +204 -0
  110. package/src/command-palette/command-separator.ts +18 -0
  111. package/src/command-palette/index.ts +15 -0
  112. package/src/confirm-popup/confirm-popup-action.ts +39 -0
  113. package/src/confirm-popup/confirm-popup-cancel.ts +38 -0
  114. package/src/confirm-popup/confirm-popup-content.ts +113 -0
  115. package/src/confirm-popup/confirm-popup-message.ts +36 -0
  116. package/src/confirm-popup/confirm-popup-trigger.ts +31 -0
  117. package/src/confirm-popup/confirm-popup.context.ts +41 -0
  118. package/src/confirm-popup/confirm-popup.spec.ts +337 -0
  119. package/src/confirm-popup/confirm-popup.ts +109 -0
  120. package/src/confirm-popup/index.ts +12 -0
  121. package/src/date-picker/date-picker-calendar.ts +65 -0
  122. package/src/date-picker/date-picker-trigger.ts +215 -0
  123. package/src/date-picker/date-picker.context.ts +50 -0
  124. package/src/date-picker/date-picker.spec.ts +117 -0
  125. package/src/date-picker/date-picker.ts +127 -0
  126. package/src/date-picker/index.ts +7 -0
  127. package/src/dialog/_examples/dialog.confirm.example.ts +145 -0
  128. package/src/dialog/_examples/dialog.example.ts +106 -0
  129. package/src/dialog/_examples/dialog.finance.example.ts +121 -0
  130. package/src/dialog/_examples/dialog.retro.example.ts +126 -0
  131. package/src/dialog/_examples/index.ts +6 -0
  132. package/src/dialog/dialog.ref.ts +42 -0
  133. package/src/dialog/dialog.service.ts +57 -167
  134. package/src/dialog/dialog.spec.ts +35 -120
  135. package/src/dialog/dialog.ts +17 -192
  136. package/src/dialog/index.ts +3 -9
  137. package/src/divider/divider.spec.ts +223 -0
  138. package/src/divider/divider.ts +137 -0
  139. package/src/divider/index.ts +5 -0
  140. package/src/drawer/drawer.ref.ts +63 -0
  141. package/src/drawer/drawer.service.ts +106 -0
  142. package/src/drawer/drawer.spec.ts +117 -0
  143. package/src/drawer/drawer.ts +122 -0
  144. package/src/drawer/index.ts +10 -0
  145. package/src/dropdown-menu/dropdown-menu-content.ts +258 -0
  146. package/src/dropdown-menu/dropdown-menu-group.ts +31 -0
  147. package/src/dropdown-menu/dropdown-menu-item.ts +51 -0
  148. package/src/dropdown-menu/dropdown-menu-label.ts +41 -0
  149. package/src/dropdown-menu/dropdown-menu-separator.ts +27 -0
  150. package/src/dropdown-menu/dropdown-menu-trigger.ts +140 -0
  151. package/src/dropdown-menu/dropdown-menu.spec.ts +265 -0
  152. package/src/dropdown-menu/dropdown-menu.ts +60 -0
  153. package/src/dropdown-menu/index.ts +15 -0
  154. package/src/example-components.ts +39 -30
  155. package/src/field/field-error.ts +49 -0
  156. package/src/field/field-group.ts +39 -0
  157. package/src/field/field-help.ts +47 -0
  158. package/src/field/field-label.ts +30 -0
  159. package/src/field/field.context.ts +28 -0
  160. package/src/field/field.spec.ts +179 -0
  161. package/src/field/field.ts +121 -0
  162. package/src/field/index.ts +6 -0
  163. package/src/file-upload/file-upload.context.ts +89 -0
  164. package/src/file-upload/file-upload.spec.ts +290 -0
  165. package/src/file-upload/file-upload.ts +639 -0
  166. package/src/file-upload/file-upload.types.ts +59 -0
  167. package/src/file-upload/index.ts +23 -0
  168. package/src/form/form-error-summary.ts +87 -0
  169. package/src/form/form-field.context.ts +3 -0
  170. package/src/form/form-field.spec.ts +25 -0
  171. package/src/form/form-field.ts +52 -0
  172. package/src/form/form.context.ts +52 -2
  173. package/src/form/form.spec.ts +155 -18
  174. package/src/form/form.ts +315 -35
  175. package/src/form/index.ts +11 -2
  176. package/src/icon/E2E.md +14 -0
  177. package/src/icon/icon.css +40 -0
  178. package/src/icon/icon.directive.spec.ts +198 -0
  179. package/src/icon/icon.directive.ts +113 -0
  180. package/src/icon/icon.mode.spec.ts +29 -0
  181. package/src/icon/icon.mode.ts +23 -0
  182. package/src/icon/icon.providers.spec.ts +63 -0
  183. package/src/icon/icon.providers.ts +47 -0
  184. package/src/icon/icon.resolver.spec.ts +96 -0
  185. package/src/icon/icon.resolver.ts +49 -0
  186. package/src/icon/icon.tokens.ts +88 -0
  187. package/src/icon/icon.types.ts +48 -0
  188. package/src/icon/index.ts +21 -0
  189. package/src/input/input.ts +12 -3
  190. package/src/input-group/index.ts +3 -0
  191. package/src/input-group/input-group-addon.ts +92 -0
  192. package/src/input-group/input-group.context.ts +27 -0
  193. package/src/input-group/input-group.spec.ts +186 -0
  194. package/src/input-group/input-group.ts +136 -0
  195. package/src/input-mask/index.ts +11 -0
  196. package/src/input-mask/input-mask.compile.ts +69 -0
  197. package/src/input-mask/input-mask.engine.ts +376 -0
  198. package/src/input-mask/input-mask.presets.ts +24 -0
  199. package/src/input-mask/input-mask.spec.ts +304 -0
  200. package/src/input-mask/input-mask.tokens.ts +49 -0
  201. package/src/input-mask/input-mask.ts +280 -0
  202. package/src/input-otp/index.ts +2 -0
  203. package/src/input-otp/input-otp.context.ts +41 -0
  204. package/src/input-otp/input-otp.spec.ts +308 -0
  205. package/src/input-otp/input-otp.ts +470 -0
  206. package/src/kbd/index.ts +1 -0
  207. package/src/kbd/kbd.spec.ts +127 -0
  208. package/src/kbd/kbd.ts +132 -0
  209. package/src/link/config.ts +44 -0
  210. package/src/link/index.ts +2 -0
  211. package/src/link/link.spec.ts +246 -0
  212. package/src/link/link.ts +226 -0
  213. package/src/list/index.ts +11 -0
  214. package/src/list/list.spec.ts +237 -0
  215. package/src/list/list.ts +277 -0
  216. package/src/menubar/index.ts +7 -0
  217. package/src/menubar/menubar-item.ts +204 -0
  218. package/src/menubar/menubar.context.ts +43 -0
  219. package/src/menubar/menubar.spec.ts +433 -0
  220. package/src/menubar/menubar.ts +309 -0
  221. package/src/number-input/index.ts +4 -0
  222. package/src/number-input/number-input-group.ts +64 -0
  223. package/src/number-input/number-input.context.ts +31 -0
  224. package/src/number-input/number-input.format.ts +148 -0
  225. package/src/number-input/number-input.spec.ts +170 -0
  226. package/src/number-input/number-input.ts +457 -0
  227. package/src/number-input/number-stepper.ts +193 -0
  228. package/src/overlay-badge/index.ts +7 -0
  229. package/src/overlay-badge/overlay-badge-content.ts +73 -0
  230. package/src/overlay-badge/overlay-badge.context.ts +37 -0
  231. package/src/overlay-badge/overlay-badge.spec.ts +220 -0
  232. package/src/overlay-badge/overlay-badge.ts +128 -0
  233. package/src/pagination/config.ts +82 -0
  234. package/src/pagination/index.ts +19 -0
  235. package/src/pagination/page-tokens.ts +78 -0
  236. package/src/pagination/pagination-ellipsis.ts +69 -0
  237. package/src/pagination/pagination-first.ts +84 -0
  238. package/src/pagination/pagination-info.ts +70 -0
  239. package/src/pagination/pagination-item.ts +81 -0
  240. package/src/pagination/pagination-last.ts +85 -0
  241. package/src/pagination/pagination-next.ts +86 -0
  242. package/src/pagination/pagination-previous.ts +90 -0
  243. package/src/pagination/pagination.context.ts +52 -0
  244. package/src/pagination/pagination.spec.ts +388 -0
  245. package/src/pagination/pagination.ts +288 -0
  246. package/src/password-input/index.ts +15 -0
  247. package/src/password-input/password-input.context.ts +65 -0
  248. package/src/password-input/password-input.scorer.ts +34 -0
  249. package/src/password-input/password-input.spec.ts +274 -0
  250. package/src/password-input/password-input.ts +488 -0
  251. package/src/popover/_examples/index.ts +5 -0
  252. package/src/popover/_examples/popover.example.ts +92 -0
  253. package/src/popover/_examples/popover.finance.example.ts +106 -0
  254. package/src/popover/_examples/popover.retro.example.ts +103 -0
  255. package/src/popover/index.ts +5 -12
  256. package/src/popover/popover-arrow.ts +30 -0
  257. package/src/popover/popover-close.ts +36 -0
  258. package/src/popover/popover-content.ts +41 -0
  259. package/src/popover/popover-title.ts +39 -0
  260. package/src/popover/popover-trigger.ts +40 -0
  261. package/src/popover/popover.spec.ts +51 -44
  262. package/src/popover/popover.ts +5 -124
  263. package/src/presets/bind-presets.spec.ts +50 -0
  264. package/src/presets/bind-presets.ts +43 -0
  265. package/src/presets/index.ts +3 -0
  266. package/src/presets/size.spec.ts +67 -0
  267. package/src/presets/size.ts +59 -0
  268. package/src/presets/variant.spec.ts +72 -0
  269. package/src/presets/variant.ts +62 -0
  270. package/src/primitives/directionality/directionality.spec.ts +112 -0
  271. package/src/primitives/directionality/directionality.ts +89 -0
  272. package/src/primitives/directionality/index.ts +1 -0
  273. package/src/primitives/forms/form-control.spec.ts +11 -3
  274. package/src/primitives/forms/form-control.ts +6 -2
  275. package/src/primitives/index.ts +3 -0
  276. package/src/primitives/interaction/disabled.ts +12 -5
  277. package/src/primitives/interaction/focus-ring.ts +4 -2
  278. package/src/primitives/list/filterable-list.spec.ts +114 -0
  279. package/src/primitives/list/filterable-list.ts +126 -0
  280. package/src/primitives/list/group.spec.ts +99 -0
  281. package/src/primitives/list/group.ts +139 -0
  282. package/src/primitives/list/index.ts +8 -0
  283. package/src/primitives/list/inject-helpers.ts +8 -0
  284. package/src/primitives/list/item.spec.ts +173 -0
  285. package/src/primitives/list/item.ts +222 -0
  286. package/src/primitives/list/navigator.spec.ts +286 -0
  287. package/src/primitives/list/navigator.ts +238 -0
  288. package/src/primitives/list/selection.spec.ts +214 -0
  289. package/src/primitives/list/selection.ts +339 -0
  290. package/src/primitives/list/tokens.ts +106 -0
  291. package/src/primitives/list/type-ahead.spec.ts +58 -0
  292. package/src/primitives/list/type-ahead.ts +46 -0
  293. package/src/primitives/overlay/backdrop.spec.ts +54 -0
  294. package/src/primitives/overlay/backdrop.ts +31 -0
  295. package/src/primitives/overlay/builder.spec.ts +38 -0
  296. package/src/primitives/overlay/builder.ts +151 -0
  297. package/src/primitives/overlay/container.ts +42 -0
  298. package/src/primitives/overlay/context.ts +14 -0
  299. package/src/primitives/overlay/controller.spec.ts +86 -0
  300. package/src/primitives/overlay/controller.ts +190 -0
  301. package/src/primitives/overlay/id.spec.ts +24 -0
  302. package/src/primitives/overlay/id.ts +17 -0
  303. package/src/primitives/overlay/index.ts +18 -1
  304. package/src/primitives/overlay/overlay.css +42 -0
  305. package/src/primitives/overlay/panel.spec.ts +52 -0
  306. package/src/primitives/overlay/panel.ts +102 -0
  307. package/src/primitives/overlay/stack.spec.ts +55 -0
  308. package/src/primitives/overlay/stack.ts +113 -0
  309. package/src/primitives/overlay/strategies/backdrop/blurred.spec.ts +11 -0
  310. package/src/primitives/overlay/strategies/backdrop/blurred.ts +5 -0
  311. package/src/primitives/overlay/strategies/backdrop/none.spec.ts +10 -0
  312. package/src/primitives/overlay/strategies/backdrop/none.ts +9 -0
  313. package/src/primitives/overlay/strategies/backdrop/solid.spec.ts +18 -0
  314. package/src/primitives/overlay/strategies/backdrop/solid.ts +23 -0
  315. package/src/primitives/overlay/strategies/focus-trap/inert-based.spec.ts +28 -0
  316. package/src/primitives/overlay/strategies/focus-trap/inert-based.ts +38 -0
  317. package/src/primitives/overlay/strategies/focus-trap/no-trap.spec.ts +9 -0
  318. package/src/primitives/overlay/strategies/focus-trap/no-trap.ts +8 -0
  319. package/src/primitives/overlay/strategies/focus-trap/tab-cycle.spec.ts +49 -0
  320. package/src/primitives/overlay/strategies/focus-trap/tab-cycle.ts +85 -0
  321. package/src/primitives/overlay/strategies/index.ts +34 -0
  322. package/src/primitives/overlay/strategies/live-announcer/_announce.ts +35 -0
  323. package/src/primitives/overlay/strategies/live-announcer/assertive.spec.ts +21 -0
  324. package/src/primitives/overlay/strategies/live-announcer/assertive.ts +11 -0
  325. package/src/primitives/overlay/strategies/live-announcer/polite.spec.ts +21 -0
  326. package/src/primitives/overlay/strategies/live-announcer/polite.ts +11 -0
  327. package/src/primitives/overlay/strategies/live-announcer/silent.spec.ts +12 -0
  328. package/src/primitives/overlay/strategies/live-announcer/silent.ts +5 -0
  329. package/src/primitives/overlay/strategies/mount/body-portal.spec.ts +40 -0
  330. package/src/primitives/overlay/strategies/mount/body-portal.ts +91 -0
  331. package/src/primitives/overlay/strategies/mount/in-container.spec.ts +43 -0
  332. package/src/primitives/overlay/strategies/mount/in-container.ts +39 -0
  333. package/src/primitives/overlay/strategies/mount/in-place.spec.ts +40 -0
  334. package/src/primitives/overlay/strategies/mount/in-place.ts +20 -0
  335. package/src/primitives/overlay/strategies/position/anchored-to.spec.ts +56 -0
  336. package/src/primitives/overlay/strategies/position/anchored-to.ts +248 -0
  337. package/src/primitives/overlay/strategies/position/corner.spec.ts +36 -0
  338. package/src/primitives/overlay/strategies/position/corner.ts +33 -0
  339. package/src/primitives/overlay/strategies/position/edge-sheet.spec.ts +32 -0
  340. package/src/primitives/overlay/strategies/position/edge-sheet.ts +36 -0
  341. package/src/primitives/overlay/strategies/position/in-place-sibling.spec.ts +25 -0
  342. package/src/primitives/overlay/strategies/position/in-place-sibling.ts +13 -0
  343. package/src/primitives/overlay/strategies/position/point-at.spec.ts +40 -0
  344. package/src/primitives/overlay/strategies/position/point-at.ts +34 -0
  345. package/src/primitives/overlay/strategies/position/viewport-centered.spec.ts +31 -0
  346. package/src/primitives/overlay/strategies/position/viewport-centered.ts +31 -0
  347. package/src/primitives/overlay/strategies/scroll-lock/css-clip.spec.ts +15 -0
  348. package/src/primitives/overlay/strategies/scroll-lock/css-clip.ts +39 -0
  349. package/src/primitives/overlay/strategies/scroll-lock/html-overflow.spec.ts +21 -0
  350. package/src/primitives/overlay/strategies/scroll-lock/html-overflow.ts +44 -0
  351. package/src/primitives/overlay/strategies/scroll-lock/none.spec.ts +9 -0
  352. package/src/primitives/overlay/strategies/scroll-lock/none.ts +5 -0
  353. package/src/primitives/overlay/strategies/trigger-event/on-click.spec.ts +32 -0
  354. package/src/primitives/overlay/strategies/trigger-event/on-click.ts +28 -0
  355. package/src/primitives/overlay/strategies/trigger-event/on-context-menu.spec.ts +45 -0
  356. package/src/primitives/overlay/strategies/trigger-event/on-context-menu.ts +70 -0
  357. package/src/primitives/overlay/strategies/trigger-event/on-focus-or-input.spec.ts +35 -0
  358. package/src/primitives/overlay/strategies/trigger-event/on-focus-or-input.ts +34 -0
  359. package/src/primitives/overlay/strategies/trigger-event/on-focus.spec.ts +64 -0
  360. package/src/primitives/overlay/strategies/trigger-event/on-focus.ts +37 -0
  361. package/src/primitives/overlay/strategies/trigger-event/on-hotkey.spec.ts +37 -0
  362. package/src/primitives/overlay/strategies/trigger-event/on-hotkey.ts +59 -0
  363. package/src/primitives/overlay/strategies/trigger-event/on-hover.spec.ts +34 -0
  364. package/src/primitives/overlay/strategies/trigger-event/on-hover.ts +86 -0
  365. package/src/primitives/overlay/strategies/trigger-event/programmatic.spec.ts +16 -0
  366. package/src/primitives/overlay/strategies/trigger-event/programmatic.ts +12 -0
  367. package/src/primitives/overlay/tokens.ts +54 -0
  368. package/src/primitives/overlay/trigger.spec.ts +103 -0
  369. package/src/primitives/overlay/trigger.ts +75 -0
  370. package/src/primitives/overlay/types.ts +24 -0
  371. package/src/primitives/overlay/wrapper.ts +50 -0
  372. package/src/primitives/signals/deep-signal.ts +109 -0
  373. package/src/primitives/signals/index.ts +1 -0
  374. package/src/progress-bar/config.ts +48 -0
  375. package/src/progress-bar/index.ts +12 -0
  376. package/src/progress-bar/progress-bar-fill.ts +37 -0
  377. package/src/progress-bar/progress-bar.context.ts +34 -0
  378. package/src/progress-bar/progress-bar.spec.ts +423 -0
  379. package/src/progress-bar/progress-bar.ts +217 -0
  380. package/src/public-api.ts +58 -9
  381. package/src/radio/radio.ts +14 -3
  382. package/src/select/index.ts +4 -2
  383. package/src/select/select-content.ts +69 -0
  384. package/src/select/select-option.ts +35 -0
  385. package/src/select/select-root.ts +129 -0
  386. package/src/select/select-trigger.ts +93 -0
  387. package/src/select/select.spec.ts +133 -140
  388. package/src/select/select.ts +4 -250
  389. package/src/skeleton/index.ts +5 -0
  390. package/src/skeleton/skeleton.spec.ts +115 -0
  391. package/src/skeleton/skeleton.ts +56 -0
  392. package/src/slider/index.ts +10 -0
  393. package/src/slider/slider-range.ts +49 -0
  394. package/src/slider/slider-thumb.ts +335 -0
  395. package/src/slider/slider-track.ts +70 -0
  396. package/src/slider/slider.context.ts +79 -0
  397. package/src/slider/slider.geometry.ts +73 -0
  398. package/src/slider/slider.spec.ts +216 -0
  399. package/src/slider/slider.ts +443 -0
  400. package/src/speed-dial/index.ts +10 -0
  401. package/src/speed-dial/speed-dial-action.ts +40 -0
  402. package/src/speed-dial/speed-dial-actions.ts +31 -0
  403. package/src/speed-dial/speed-dial-trigger.ts +55 -0
  404. package/src/speed-dial/speed-dial.context.ts +36 -0
  405. package/src/speed-dial/speed-dial.spec.ts +143 -0
  406. package/src/speed-dial/speed-dial.ts +127 -0
  407. package/src/spinner/config.ts +48 -0
  408. package/src/spinner/index.ts +8 -0
  409. package/src/spinner/spinner.spec.ts +262 -0
  410. package/src/spinner/spinner.ts +129 -0
  411. package/src/stepper/index.ts +15 -0
  412. package/src/stepper/stepper.context.ts +63 -0
  413. package/src/stepper/stepper.spec.ts +403 -0
  414. package/src/stepper/stepper.ts +544 -0
  415. package/src/styles.css +17 -0
  416. package/src/table/column-helpers.spec.ts +74 -0
  417. package/src/table/column-helpers.ts +68 -0
  418. package/src/table/filter-fns.ts +230 -0
  419. package/src/table/filter-models.spec.ts +240 -0
  420. package/src/table/filter-models.ts +124 -0
  421. package/src/table/filter-params.ts +90 -0
  422. package/src/table/grid-api-impl.ts +321 -0
  423. package/src/table/grid-api.spec.ts +178 -0
  424. package/src/table/grid-api.ts +285 -0
  425. package/src/table/index.ts +73 -1
  426. package/src/table/table-cell.spec.ts +43 -0
  427. package/src/table/table-cell.ts +38 -0
  428. package/src/table/table-filter-outlet.ts +120 -0
  429. package/src/table/table-header.spec.ts +54 -0
  430. package/src/table/table-header.ts +63 -0
  431. package/src/table/table-keyboard.spec.ts +68 -0
  432. package/src/table/table-keyboard.ts +107 -0
  433. package/src/table/table-resource.spec.ts +37 -0
  434. package/src/table/table-resource.ts +36 -0
  435. package/src/table/table-row.spec.ts +54 -0
  436. package/src/table/table-row.ts +31 -0
  437. package/src/table/table-storage.spec.ts +67 -0
  438. package/src/table/table-storage.ts +76 -0
  439. package/src/table/table.spec.ts +101 -42
  440. package/src/table/table.ts +228 -73
  441. package/src/table/table.types.ts +83 -0
  442. package/src/tabs/index.ts +7 -2
  443. package/src/tabs/tabs.context.ts +27 -4
  444. package/src/tabs/tabs.spec.ts +336 -24
  445. package/src/tabs/tabs.ts +313 -79
  446. package/src/tag/config.ts +53 -0
  447. package/src/tag/index.ts +7 -0
  448. package/src/tag/tag-list.ts +76 -0
  449. package/src/tag/tag-remove.ts +61 -0
  450. package/src/tag/tag.context.ts +36 -0
  451. package/src/tag/tag.spec.ts +240 -0
  452. package/src/tag/tag.ts +203 -0
  453. package/src/textarea/config.ts +42 -0
  454. package/src/textarea/index.ts +8 -0
  455. package/src/textarea/textarea.spec.ts +176 -0
  456. package/src/textarea/textarea.ts +235 -0
  457. package/src/time-picker/index.ts +15 -0
  458. package/src/time-picker/time-picker-meridiem.ts +123 -0
  459. package/src/time-picker/time-picker-segment.ts +318 -0
  460. package/src/time-picker/time-picker.context.ts +38 -0
  461. package/src/time-picker/time-picker.format.ts +181 -0
  462. package/src/time-picker/time-picker.spec.ts +189 -0
  463. package/src/time-picker/time-picker.ts +274 -0
  464. package/src/toast/_examples/index.ts +5 -0
  465. package/src/toast/_examples/toast.example.ts +282 -0
  466. package/src/toast/_examples/toast.finance.example.ts +180 -0
  467. package/src/toast/_examples/toast.retro.example.ts +161 -0
  468. package/src/toast/index.ts +8 -1
  469. package/src/toast/toast.ref.ts +26 -0
  470. package/src/toast/toast.service.ts +227 -41
  471. package/src/toast/toast.spec.ts +166 -14
  472. package/src/toast/toast.strategy.ts +10 -0
  473. package/src/toast/toast.ts +177 -21
  474. package/src/toggle/toggle.ts +10 -2
  475. package/src/tooltip/_examples/index.ts +6 -0
  476. package/src/tooltip/_examples/tooltip.example.ts +82 -0
  477. package/src/tooltip/_examples/tooltip.finance.example.ts +97 -0
  478. package/src/tooltip/_examples/tooltip.placements.example.ts +83 -0
  479. package/src/tooltip/_examples/tooltip.retro.example.ts +107 -0
  480. package/src/tooltip/index.ts +4 -5
  481. package/src/tooltip/tooltip-arrow.ts +31 -0
  482. package/src/tooltip/tooltip-content.ts +38 -0
  483. package/src/tooltip/tooltip-group.ts +32 -0
  484. package/src/tooltip/tooltip-trigger.ts +47 -0
  485. package/src/tooltip/tooltip.spec.ts +38 -102
  486. package/src/tooltip/tooltip.ts +5 -129
  487. package/src/tree-select/index.ts +12 -0
  488. package/src/tree-select/tree-select-content.ts +199 -0
  489. package/src/tree-select/tree-select-node.ts +132 -0
  490. package/src/tree-select/tree-select-root.ts +272 -0
  491. package/src/tree-select/tree-select-trigger.ts +90 -0
  492. package/src/tree-select/tree-select.context.ts +63 -0
  493. package/src/tree-select/tree-select.spec.ts +433 -0
  494. package/src/tree-select/tree-select.ts +2 -0
  495. package/src/typography/blockquote.ts +57 -0
  496. package/src/typography/code.ts +53 -0
  497. package/src/typography/index.ts +22 -0
  498. package/src/typography/lead.ts +49 -0
  499. package/src/typography/muted.ts +29 -0
  500. package/src/typography/prose.css +312 -0
  501. package/src/typography/truncate.ts +99 -0
  502. package/src/typography/typography.spec.ts +203 -0
  503. package/tsconfig.lib.json +9 -1
  504. package/tsconfig.lib.prod.json +7 -1
  505. package/tsconfig.spec.json +9 -1
  506. package/vite.config.ts +8 -0
  507. package/src/button/button.example.ts +0 -30
  508. package/src/button/button.finance.example.ts +0 -37
  509. package/src/button/button.retro.example.ts +0 -47
  510. package/src/button/button.sizes.example.ts +0 -25
  511. package/src/dialog/dialog.confirm.example.ts +0 -57
  512. package/src/dialog/dialog.context.ts +0 -11
  513. package/src/dialog/dialog.example.ts +0 -40
  514. package/src/dialog/dialog.finance.example.ts +0 -47
  515. package/src/dialog/dialog.retro.example.ts +0 -46
  516. package/src/menu/index.ts +0 -2
  517. package/src/menu/menu.context.ts +0 -10
  518. package/src/menu/menu.example.ts +0 -39
  519. package/src/menu/menu.spec.ts +0 -168
  520. package/src/menu/menu.ts +0 -176
  521. package/src/popover/popover.context.ts +0 -18
  522. package/src/popover/popover.example.ts +0 -50
  523. package/src/popover/popover.finance.example.ts +0 -52
  524. package/src/popover/popover.retro.example.ts +0 -53
  525. package/src/primitives/overlay/overlay.spec.ts +0 -90
  526. package/src/primitives/overlay/overlay.ts +0 -106
  527. package/src/select/select.context.ts +0 -3
  528. package/src/toast/toast.example.ts +0 -134
  529. package/src/toast/toast.finance.example.ts +0 -68
  530. package/src/toast/toast.retro.example.ts +0 -69
  531. package/src/tooltip/tooltip.example.ts +0 -46
  532. package/src/tooltip/tooltip.finance.example.ts +0 -54
  533. package/src/tooltip/tooltip.placements.example.ts +0 -56
  534. package/src/tooltip/tooltip.retro.example.ts +0 -65
@@ -0,0 +1,639 @@
1
+ import {
2
+ Directive,
3
+ ElementRef,
4
+ EventEmitter,
5
+ Output,
6
+ Signal,
7
+ afterNextRender,
8
+ booleanAttribute,
9
+ computed,
10
+ inject,
11
+ input,
12
+ signal,
13
+ } from '@angular/core';
14
+ import {
15
+ KJ_FILE_UPLOAD,
16
+ KJ_FILE_UPLOAD_ITEM,
17
+ KjFileUploadAggregateStatus,
18
+ KjFileUploadContext,
19
+ KjFileUploadItemContext,
20
+ } from './file-upload.context';
21
+ import {
22
+ KJ_FILE_UPLOAD_DEFAULT_MESSAGES,
23
+ KjFileRejection,
24
+ KjFileStatus,
25
+ KjFileUploadValidationMessages,
26
+ KjUploadableFile,
27
+ } from './file-upload.types';
28
+
29
+ let kjFileUploadIdCounter = 0;
30
+ function nextFileId(): string {
31
+ const cryptoLike = (globalThis as { crypto?: { randomUUID?: () => string } }).crypto;
32
+ if (cryptoLike?.randomUUID) return `kjf-${cryptoLike.randomUUID().slice(0, 8)}`;
33
+ return `kjf-${(++kjFileUploadIdCounter).toString(36)}`;
34
+ }
35
+
36
+ /**
37
+ * Validates a single `File` against the configured `accept` / `maxSize`
38
+ * constraints. Returns the rejection reason, or `null` if the file passes.
39
+ *
40
+ * `accept` matching follows the WHATWG file picker rules: a comma-separated
41
+ * list of MIME types (`image/*`, `application/pdf`) or extensions
42
+ * (`.pdf`, `.heic`). When `file.type` is empty (browsers don't report a MIME
43
+ * type for some extensions, e.g. `.heic`), we fall back to extension
44
+ * matching against `file.name`.
45
+ *
46
+ * @internal
47
+ */
48
+ export function kjFileMatchesAccept(file: File, accept: string | undefined): boolean {
49
+ if (!accept) return true;
50
+ const tokens = accept
51
+ .split(',')
52
+ .map((t) => t.trim().toLowerCase())
53
+ .filter(Boolean);
54
+ if (!tokens.length) return true;
55
+ const type = file.type.toLowerCase();
56
+ const name = file.name.toLowerCase();
57
+ for (const tok of tokens) {
58
+ if (tok.startsWith('.')) {
59
+ if (name.endsWith(tok)) return true;
60
+ } else if (tok.endsWith('/*')) {
61
+ const prefix = tok.slice(0, -1); // keep the trailing slash
62
+ if (type.startsWith(prefix)) return true;
63
+ } else if (type && tok === type) {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+
70
+ /**
71
+ * Root file-upload directive. Owns the file list, drag state, and selection
72
+ * validation (`accept` / `maxSize` / `maxFiles`). Provides `KJ_FILE_UPLOAD`
73
+ * for child directives (trigger, drop-zone, list, item).
74
+ *
75
+ * The transport is the consumer's job — `KjFileUpload` ships the *state
76
+ * machine* and emits structured events. Examples call `setFileStatus` from
77
+ * a simulated upload to drive `KjProgressBar` per row.
78
+ *
79
+ * The directive owns a visually-hidden `aria-live="polite"` span (mounted
80
+ * as a child of the host on first render) that announces file additions,
81
+ * removals, completions, and errors to screen readers (WCAG 4.1.3). The
82
+ * announcement node is intentionally NOT the host element itself — clearing
83
+ * `host.textContent` to retrigger SR readout would also wipe out the
84
+ * projected drop-zone / list children, so we keep the live region as a
85
+ * dedicated sibling.
86
+ *
87
+ * @example
88
+ * ```html
89
+ * <div kjFileUpload kjAccept="image/*" [kjMaxSize]="5_000_000">
90
+ * <button kjFileUploadTrigger>Choose files</button>
91
+ * <div kjFileUploadDropzone>Drop files here</div>
92
+ * <ul kjFileUploadList>
93
+ * @for (f of upload.files(); track f.id) {
94
+ * <li kjFileUploadItem [kjFile]="f">{{ f.file.name }}</li>
95
+ * }
96
+ * </ul>
97
+ * </div>
98
+ * ```
99
+ * @doc-category Core/Data input
100
+ * @doc
101
+ * @doc-name file-upload
102
+ * @doc-description Manages file selection, drag state, and validation while you control the upload transport.
103
+ * @doc-is-main
104
+ */
105
+ @Directive({
106
+ selector: '[kjFileUpload]',
107
+ standalone: true,
108
+ exportAs: 'kjFileUpload',
109
+ providers: [{ provide: KJ_FILE_UPLOAD, useExisting: KjFileUpload }],
110
+ host: {
111
+ '[attr.data-disabled]': 'kjDisabled() ? "" : null',
112
+ '[attr.data-drag-active]': 'dragActive() ? "" : null',
113
+ '[attr.data-status]': 'aggregateStatus()',
114
+ },
115
+ })
116
+ export class KjFileUpload implements KjFileUploadContext {
117
+ private readonly hostEl = inject<ElementRef<HTMLElement>>(ElementRef);
118
+ private liveRegionEl: HTMLElement | null = null;
119
+
120
+ /** MIME / extension list for the file picker and drop validation. */
121
+ readonly kjAccept = input<string | undefined>(undefined);
122
+
123
+ /** Whether multiple files may be selected. Defaults to `true` (advanced flavour). */
124
+ readonly kjMultiple = input(true, { transform: booleanAttribute });
125
+
126
+ /** Maximum bytes per file. Files over the cap are rejected at selection. */
127
+ readonly kjMaxSize = input<number | undefined>(undefined);
128
+
129
+ /** Maximum total file count across the list. Excess files are rejected. */
130
+ readonly kjMaxFiles = input<number | undefined>(undefined);
131
+
132
+ /** Disables the upload — drop-zone is `aria-disabled`, the trigger is inert. */
133
+ readonly kjDisabled = input(false, { transform: booleanAttribute });
134
+
135
+ /** Per-rejection-reason override messages. */
136
+ readonly kjValidationMessages = input<Partial<KjFileUploadValidationMessages>>({});
137
+
138
+ /** Emits the survivors after validation each time the user adds files. */
139
+ @Output() readonly kjSelect = new EventEmitter<File[]>();
140
+
141
+ /** Emits the rejection list whenever validation filters files out. */
142
+ @Output() readonly kjReject = new EventEmitter<KjFileRejection[]>();
143
+
144
+ /** Emits the `KjUploadableFile` for each row the user removes. */
145
+ @Output() readonly kjRemove = new EventEmitter<KjUploadableFile>();
146
+
147
+ private readonly _files = signal<readonly KjUploadableFile[]>([]);
148
+ /** Current file list (in selection order). */
149
+ readonly files: Signal<readonly KjUploadableFile[]> = this._files.asReadonly();
150
+
151
+ private readonly _dragActive = signal(false);
152
+ /** True while at least one drag operation is currently over the drop-zone host. */
153
+ readonly dragActive: Signal<boolean> = this._dragActive.asReadonly();
154
+
155
+ /** Read-only mirror of the disabled flag for context consumers. */
156
+ readonly disabled: Signal<boolean> = this.kjDisabled;
157
+ readonly multiple: Signal<boolean> = this.kjMultiple;
158
+ readonly accept: Signal<string | undefined> = this.kjAccept;
159
+ readonly maxSize: Signal<number | undefined> = this.kjMaxSize;
160
+ readonly maxFiles: Signal<number | undefined> = this.kjMaxFiles;
161
+
162
+ /** Aggregate status across the file list. */
163
+ readonly aggregateStatus = computed<KjFileUploadAggregateStatus>(() => {
164
+ const list = this._files();
165
+ if (!list.length) return 'idle';
166
+ let hasUploading = false;
167
+ let hasPending = false;
168
+ let hasError = false;
169
+ let allDone = true;
170
+ for (const f of list) {
171
+ if (f.status === 'uploading') hasUploading = true;
172
+ if (f.status === 'pending' || f.status === 'preparing') hasPending = true;
173
+ if (f.status === 'error') hasError = true;
174
+ if (f.status !== 'done') allDone = false;
175
+ }
176
+ if (hasUploading) return 'uploading';
177
+ if (allDone) return 'complete';
178
+ if (hasError && !hasPending) return 'partial-error';
179
+ return 'pending';
180
+ });
181
+
182
+ /** @internal Drag-enter / drag-leave counter — flips `_dragActive` cleanly across nested children. */
183
+ private dragDepth = 0;
184
+
185
+ /** @internal Set by the trigger / drop-zone so `openPicker()` can click the right input. */
186
+ private pickerInput: HTMLInputElement | null = null;
187
+
188
+ /** @internal Trigger / drop-zone register their hidden file input here. */
189
+ registerPickerInput(el: HTMLInputElement): void {
190
+ this.pickerInput = el;
191
+ }
192
+
193
+ /** Opens the native file picker by clicking the registered hidden input. */
194
+ openPicker(): void {
195
+ if (this.kjDisabled()) return;
196
+ this.pickerInput?.click();
197
+ }
198
+
199
+ /**
200
+ * Adds candidate files (from drop, programmatic add, or the picker's
201
+ * `change` event). Validates each against `accept` / `maxSize`, then
202
+ * applies the `kjMaxFiles` cap to the survivors. Mints ids, appends to
203
+ * the list, fires `kjSelect` / `kjReject`, and announces.
204
+ */
205
+ addFiles(incoming: readonly File[] | FileList): void {
206
+ if (this.kjDisabled()) return;
207
+ const candidates: File[] = Array.from(incoming);
208
+ if (!candidates.length) return;
209
+
210
+ const accept = this.kjAccept();
211
+ const maxSize = this.kjMaxSize();
212
+ const maxFiles = this.kjMaxFiles();
213
+ const messages = this.resolvedMessages();
214
+
215
+ const survivors: File[] = [];
216
+ const rejections: KjFileRejection[] = [];
217
+
218
+ for (const file of candidates) {
219
+ if (accept && !kjFileMatchesAccept(file, accept)) {
220
+ rejections.push({ file, reason: 'type' });
221
+ continue;
222
+ }
223
+ if (maxSize !== undefined && file.size > maxSize) {
224
+ rejections.push({ file, reason: 'size' });
225
+ continue;
226
+ }
227
+ survivors.push(file);
228
+ }
229
+
230
+ // In single mode, the most recent pick replaces any existing file. Cap
231
+ // the survivors to 1 (rejecting any overflow with reason 'count'), then
232
+ // clear the existing list so the survivor takes its place.
233
+ if (!this.kjMultiple()) {
234
+ if (survivors.length > 1) {
235
+ const overflow = survivors.splice(1);
236
+ for (const f of overflow) rejections.push({ file: f, reason: 'count' });
237
+ }
238
+ if (survivors.length) this._files.set([]);
239
+ } else if (maxFiles !== undefined) {
240
+ // Multi mode: the explicit `kjMaxFiles` cap applies to current + new.
241
+ const current = this._files().length;
242
+ if (current + survivors.length > maxFiles) {
243
+ const allowed = Math.max(0, maxFiles - current);
244
+ const overflow = survivors.splice(allowed);
245
+ for (const f of overflow) rejections.push({ file: f, reason: 'count' });
246
+ }
247
+ }
248
+
249
+ if (survivors.length) {
250
+ const next: KjUploadableFile[] = survivors.map((f) => ({
251
+ id: nextFileId(),
252
+ file: f,
253
+ status: 'pending' as const,
254
+ progress: null,
255
+ }));
256
+ this._files.update((list) => [...list, ...next]);
257
+ this.kjSelect.emit(survivors);
258
+ this.announce(
259
+ survivors.length === 1
260
+ ? `${survivors[0].name} added`
261
+ : `${survivors.length} files added`,
262
+ );
263
+ }
264
+
265
+ if (rejections.length) {
266
+ this.kjReject.emit(rejections);
267
+ const reason = rejections[0].reason;
268
+ const msg = messages[reason];
269
+ this.announce(
270
+ rejections.length === 1
271
+ ? `${rejections[0].file.name} rejected: ${msg}`
272
+ : `${rejections.length} files rejected: ${msg}`,
273
+ );
274
+ }
275
+ }
276
+
277
+ /** Remove a file by id. Cancels in-flight uploads, emits `kjRemove`. */
278
+ remove(id: string): void {
279
+ const target = this._files().find((f) => f.id === id);
280
+ if (!target) return;
281
+ this._files.update((list) => list.filter((f) => f.id !== id));
282
+ this.kjRemove.emit(target);
283
+ this.announce(`${target.file.name} removed`);
284
+ }
285
+
286
+ /** Clear the entire file list. */
287
+ clear(): void {
288
+ if (!this._files().length) return;
289
+ this._files.set([]);
290
+ this.announce('File list cleared');
291
+ }
292
+
293
+ /** Reset a row to `pending` so the consumer (or simulator) can re-run it. */
294
+ retry(id: string): void {
295
+ this.patchFile(id, { status: 'pending', progress: null, error: undefined });
296
+ }
297
+
298
+ /** Cancel an in-flight upload — sets `status: 'cancelled'`, clears progress. */
299
+ cancel(id: string): void {
300
+ this.patchFile(id, { status: 'cancelled', progress: null });
301
+ }
302
+
303
+ /**
304
+ * Update a row's status / progress / error. Examples drive this from a
305
+ * simulated upload; consumers wiring `HttpClient` call it from their
306
+ * progress / success / error subscriptions.
307
+ */
308
+ setFileStatus(id: string, status: KjFileStatus, progress?: number | null, error?: string): void {
309
+ this.patchFile(id, {
310
+ status,
311
+ progress: progress === undefined ? null : progress,
312
+ error,
313
+ });
314
+ if (status === 'done') {
315
+ const f = this._files().find((x) => x.id === id);
316
+ if (f) this.announce(`${f.file.name} uploaded`);
317
+ } else if (status === 'error') {
318
+ const f = this._files().find((x) => x.id === id);
319
+ if (f) this.announce(`${f.file.name} failed: ${error ?? 'upload error'}`);
320
+ }
321
+ }
322
+
323
+ // ─── Drag state plumbing (called by KjFileUploadDropzone) ───
324
+
325
+ /** @internal */
326
+ onDragEnter(event: Event): void {
327
+ if (this.kjDisabled()) return;
328
+ if (!this.eventCarriesFiles(event)) return;
329
+ event.preventDefault();
330
+ this.dragDepth++;
331
+ this._dragActive.set(true);
332
+ }
333
+
334
+ /** @internal */
335
+ onDragOver(event: Event): void {
336
+ if (this.kjDisabled()) return;
337
+ if (!this.eventCarriesFiles(event)) return;
338
+ event.preventDefault();
339
+ }
340
+
341
+ /** @internal */
342
+ onDragLeave(event: Event): void {
343
+ if (this.kjDisabled()) return;
344
+ event.preventDefault();
345
+ this.dragDepth = Math.max(0, this.dragDepth - 1);
346
+ if (this.dragDepth === 0) this._dragActive.set(false);
347
+ }
348
+
349
+ /** @internal */
350
+ onDrop(event: Event): void {
351
+ event.preventDefault();
352
+ this.dragDepth = 0;
353
+ this._dragActive.set(false);
354
+ if (this.kjDisabled()) return;
355
+ const files = (event as DragEvent).dataTransfer?.files;
356
+ if (!files || files.length === 0) return;
357
+ this.addFiles(files);
358
+ }
359
+
360
+ /** @internal */
361
+ onPickerChange(event: Event): void {
362
+ const target = event.target as HTMLInputElement;
363
+ if (target.files) this.addFiles(target.files);
364
+ // Reset so picking the same file twice fires another change event.
365
+ target.value = '';
366
+ }
367
+
368
+ private eventCarriesFiles(event: Event): boolean {
369
+ const types = (event as DragEvent).dataTransfer?.types;
370
+ if (!types) return false;
371
+ for (let i = 0; i < types.length; i++) {
372
+ if (types[i] === 'Files') return true;
373
+ }
374
+ return false;
375
+ }
376
+
377
+ private patchFile(id: string, patch: Partial<KjUploadableFile>): void {
378
+ this._files.update((list) =>
379
+ list.map((f) => (f.id === id ? { ...f, ...patch } : f)),
380
+ );
381
+ }
382
+
383
+ private resolvedMessages(): KjFileUploadValidationMessages {
384
+ return { ...KJ_FILE_UPLOAD_DEFAULT_MESSAGES, ...this.kjValidationMessages() };
385
+ }
386
+
387
+ private announce(message: string): void {
388
+ const el = this.liveRegionEl;
389
+ if (!el) return;
390
+ // Clear-then-set with a microtask gap so screen readers detect the change
391
+ // even when consecutive announcements share a prefix.
392
+ el.textContent = '';
393
+ setTimeout(() => {
394
+ if (this.liveRegionEl) this.liveRegionEl.textContent = message;
395
+ }, 50);
396
+ }
397
+
398
+ constructor() {
399
+ afterNextRender(() => {
400
+ if (typeof document === 'undefined') return;
401
+ const span = document.createElement('span');
402
+ span.setAttribute('aria-live', 'polite');
403
+ span.setAttribute('aria-atomic', 'true');
404
+ span.style.cssText =
405
+ 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
406
+ this.hostEl.nativeElement.appendChild(span);
407
+ this.liveRegionEl = span;
408
+ });
409
+ }
410
+ }
411
+
412
+ /**
413
+ * The "browse" button — when clicked, opens the native file picker. Owns the
414
+ * hidden `<input type="file">` so consumers don't have to render one
415
+ * themselves. Mirrors the `KjButton` keyboard contract (Enter / Space click
416
+ * the host element).
417
+ *
418
+ * Use on a `<button>` (recommended, native keyboard semantics) or any
419
+ * focusable element. The directive does **not** force `role="button"` — wrap
420
+ * a real `<button>` and let the browser handle it.
421
+ *
422
+ * @example
423
+ * ```html
424
+ * <button kjFileUploadTrigger>Choose files</button>
425
+ * ```
426
+ * @doc-category Core/Data input
427
+ * @doc
428
+ * @doc-name file-upload
429
+ */
430
+ @Directive({
431
+ selector: '[kjFileUploadTrigger]',
432
+ standalone: true,
433
+ host: {
434
+ 'type': 'button',
435
+ '[attr.aria-disabled]': 'ctx.disabled() ? "true" : null',
436
+ '[attr.data-disabled]': 'ctx.disabled() ? "" : null',
437
+ '(click)': 'onClick($event)',
438
+ },
439
+ })
440
+ export class KjFileUploadTrigger {
441
+ /** @internal */
442
+ readonly ctx = inject(KJ_FILE_UPLOAD);
443
+ private readonly el = inject<ElementRef<HTMLElement>>(ElementRef);
444
+ private hidden!: HTMLInputElement;
445
+
446
+ constructor() {
447
+ // Mint and own a hidden `<input type="file">` parked in the trigger's
448
+ // host element. Visually hidden (not [hidden] attribute) so it stays in
449
+ // the a11y tree and programmatic .click() opens the picker reliably
450
+ // across browsers (analysis open-question #2).
451
+ if (typeof document !== 'undefined') {
452
+ this.hidden = document.createElement('input');
453
+ this.hidden.type = 'file';
454
+ this.hidden.tabIndex = -1;
455
+ this.hidden.setAttribute('aria-hidden', 'true');
456
+ this.hidden.style.cssText =
457
+ 'position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0;';
458
+ this.hidden.addEventListener('change', (event) => this.ctx.onPickerChange(event));
459
+ this.syncAttrs();
460
+ // Mirror dynamic accept / multiple onto the hidden input.
461
+ // computed-via-effect would also work; for a deliberately small surface
462
+ // we re-sync on every click (cheap).
463
+ this.el.nativeElement.appendChild(this.hidden);
464
+ this.ctx.registerPickerInput(this.hidden);
465
+ }
466
+ }
467
+
468
+ /** @internal */
469
+ onClick(event: Event): void {
470
+ if (this.ctx.disabled()) {
471
+ event.preventDefault();
472
+ event.stopImmediatePropagation();
473
+ return;
474
+ }
475
+ this.syncAttrs();
476
+ this.hidden?.click();
477
+ }
478
+
479
+ private syncAttrs(): void {
480
+ if (!this.hidden) return;
481
+ const accept = this.ctx.accept();
482
+ if (accept) this.hidden.setAttribute('accept', accept);
483
+ else this.hidden.removeAttribute('accept');
484
+ if (this.ctx.multiple()) this.hidden.setAttribute('multiple', '');
485
+ else this.hidden.removeAttribute('multiple');
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Drag-and-drop surface. `role="button"`, `tabindex="0"`, Enter/Space open
491
+ * the picker. Reflects `data-drag-active` while files are over the host so
492
+ * theme CSS can style the highlighted state.
493
+ *
494
+ * The drop-zone wires drag enter / over / leave / drop into the root's
495
+ * counter (so nested children don't churn `data-drag-active`). The host is
496
+ * a generic `<div>`-shaped element so consumers can project arbitrary
497
+ * content (thumbnail grid, instructional text, etc.) — analysis decision
498
+ * #3 in `file-upload.md`.
499
+ *
500
+ * @example
501
+ * ```html
502
+ * <div kjFileUploadDropzone>
503
+ * Drag files here, or click to browse
504
+ * </div>
505
+ * ```
506
+ * @doc-category Core/Data input
507
+ * @doc
508
+ * @doc-name file-upload
509
+ */
510
+ @Directive({
511
+ selector: '[kjFileUploadDropzone]',
512
+ standalone: true,
513
+ host: {
514
+ 'role': 'button',
515
+ 'tabindex': '0',
516
+ '[attr.aria-label]': 'kjLabel()',
517
+ '[attr.aria-disabled]': 'ctx.disabled() ? "true" : null',
518
+ '[attr.data-drag-active]': 'ctx.dragActive() ? "" : null',
519
+ '[attr.data-disabled]': 'ctx.disabled() ? "" : null',
520
+ '(dragenter)': 'ctx.onDragEnter($event)',
521
+ '(dragover)': 'ctx.onDragOver($event)',
522
+ '(dragleave)': 'ctx.onDragLeave($event)',
523
+ '(drop)': 'ctx.onDrop($event)',
524
+ '(click)': 'onClick($event)',
525
+ '(keydown.enter)': 'onActivate($event)',
526
+ '(keydown.space)': 'onActivate($event)',
527
+ },
528
+ })
529
+ export class KjFileUploadDropzone {
530
+ /** @internal */
531
+ readonly ctx = inject(KJ_FILE_UPLOAD);
532
+
533
+ /** Accessible name for the drop-zone. */
534
+ readonly kjLabel = input<string>('Drag files here, or click to browse');
535
+
536
+ /** @internal */
537
+ onClick(event: Event): void {
538
+ if (this.ctx.disabled()) {
539
+ event.preventDefault();
540
+ return;
541
+ }
542
+ this.ctx.openPicker();
543
+ }
544
+
545
+ /** @internal */
546
+ onActivate(event: Event): void {
547
+ event.preventDefault();
548
+ if (this.ctx.disabled()) return;
549
+ this.ctx.openPicker();
550
+ }
551
+ }
552
+
553
+ /**
554
+ * File list container. Sets `role="list"` (idempotent on a `<ul>`, required
555
+ * on a `<div>`) so the per-row `role="listitem"` semantics chain correctly
556
+ * for screen readers (WCAG 1.3.1).
557
+ *
558
+ * The directive is structural-light — the consumer iterates `ctx.files()`
559
+ * with `@for` and renders rows via `[kjFileUploadItem]` — so list-shape
560
+ * (table / definition list / virtualised) stays the consumer's call.
561
+ *
562
+ * @example
563
+ * ```html
564
+ * <ul kjFileUploadList>
565
+ * @for (f of upload.files(); track f.id) {
566
+ * <li kjFileUploadItem [kjFile]="f">{{ f.file.name }}</li>
567
+ * }
568
+ * </ul>
569
+ * ```
570
+ * @doc-category Core/Data input
571
+ * @doc
572
+ * @doc-name file-upload
573
+ */
574
+ @Directive({
575
+ selector: '[kjFileUploadList]',
576
+ standalone: true,
577
+ host: {
578
+ 'role': 'list',
579
+ },
580
+ })
581
+ export class KjFileUploadList {
582
+ /** @internal */
583
+ readonly ctx = inject(KJ_FILE_UPLOAD);
584
+ }
585
+
586
+ /**
587
+ * Per-file row directive. Provides `KJ_FILE_UPLOAD_ITEM` so per-row buttons
588
+ * (retry / cancel / remove) and a per-row `KjProgressBar` can read
589
+ * `file()` / `progress()` / `status()` from context.
590
+ *
591
+ * Reflects `data-status` so theme CSS can flip the row's tint (red border
592
+ * for `error`, dimmed for `done`, etc.).
593
+ *
594
+ * @example
595
+ * ```html
596
+ * <li kjFileUploadItem [kjFile]="f">
597
+ * <span>{{ f.file.name }}</span>
598
+ * <button (click)="item.remove()">Remove</button>
599
+ * </li>
600
+ * ```
601
+ * @doc-category Core/Data input
602
+ * @doc
603
+ * @doc-name file-upload
604
+ */
605
+ @Directive({
606
+ selector: '[kjFileUploadItem]',
607
+ standalone: true,
608
+ exportAs: 'kjFileUploadItem',
609
+ providers: [{ provide: KJ_FILE_UPLOAD_ITEM, useExisting: KjFileUploadItem }],
610
+ host: {
611
+ 'role': 'listitem',
612
+ '[attr.data-status]': 'status()',
613
+ },
614
+ })
615
+ export class KjFileUploadItem implements KjFileUploadItemContext {
616
+ /** @internal */
617
+ readonly ctx = inject(KJ_FILE_UPLOAD);
618
+
619
+ /** The `KjUploadableFile` for this row. Required. */
620
+ readonly kjFile = input.required<KjUploadableFile>();
621
+
622
+ /** Read-only mirror for context consumers. */
623
+ readonly file: Signal<KjUploadableFile> = this.kjFile;
624
+
625
+ readonly status: Signal<KjFileStatus> = computed(() => this.kjFile().status);
626
+ readonly progress: Signal<number | null> = computed(() => this.kjFile().progress);
627
+
628
+ retry(): void {
629
+ this.ctx.retry(this.kjFile().id);
630
+ }
631
+
632
+ cancel(): void {
633
+ this.ctx.cancel(this.kjFile().id);
634
+ }
635
+
636
+ remove(): void {
637
+ this.ctx.remove(this.kjFile().id);
638
+ }
639
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Per-file lifecycle status. Set by the directive as files move through the
3
+ * state machine; read by templates and per-row controls (retry / cancel /
4
+ * remove gate on this).
5
+ */
6
+ export type KjFileStatus =
7
+ | 'preparing'
8
+ | 'pending'
9
+ | 'uploading'
10
+ | 'done'
11
+ | 'error'
12
+ | 'cancelled';
13
+
14
+ /**
15
+ * The kouji envelope around a native `File`. `KjFileUpload` mints `id` (a
16
+ * stable string) on selection so consumers can `track` per-row by identity.
17
+ *
18
+ * `progress` is `null` when the file hasn't started yet *or* the upload is
19
+ * indeterminate; once the consumer reports `progress` events the directive
20
+ * fills in `0..100`.
21
+ */
22
+ export interface KjUploadableFile {
23
+ /** Stable id, minted by `KjFileUpload`. Survives retry. */
24
+ readonly id: string;
25
+ /** The native `File`. */
26
+ readonly file: File;
27
+ /** Current per-file status. */
28
+ readonly status: KjFileStatus;
29
+ /** `0..100` once the upload reports progress; `null` otherwise. */
30
+ readonly progress: number | null;
31
+ /** Human-readable error message (set when `status === 'error'`). */
32
+ readonly error?: string;
33
+ }
34
+
35
+ /**
36
+ * Reason a candidate file was rejected at selection time. Drives both the
37
+ * built-in validation messages and the structured `kjReject` output the
38
+ * consumer can map to a hint / toast.
39
+ */
40
+ export type KjFileRejectReason = 'size' | 'type' | 'count';
41
+
42
+ /** A file that was filtered out at selection. */
43
+ export interface KjFileRejection {
44
+ readonly file: File;
45
+ readonly reason: KjFileRejectReason;
46
+ }
47
+
48
+ /** Built-in validation messages (English, locale-neutral). Override per-instance via `kjValidationMessages`. */
49
+ export interface KjFileUploadValidationMessages {
50
+ size: string;
51
+ type: string;
52
+ count: string;
53
+ }
54
+
55
+ export const KJ_FILE_UPLOAD_DEFAULT_MESSAGES: KjFileUploadValidationMessages = {
56
+ size: 'File too large',
57
+ type: 'File type not allowed',
58
+ count: 'Too many files',
59
+ };