@boxcustodia/library 2.0.0-alpha.19 → 2.0.0-alpha.20

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 (265) hide show
  1. package/dist/components/button/button.cjs.js +1 -1
  2. package/dist/components/button/button.es.js +19 -18
  3. package/dist/components/button/components/base-button.cjs.js +1 -1
  4. package/dist/components/button/components/base-button.es.js +20 -20
  5. package/dist/components/calendar/calendar.cjs.js +1 -1
  6. package/dist/components/calendar/calendar.es.js +1 -0
  7. package/dist/components/date-picker/date-input.cjs.js +1 -1
  8. package/dist/components/date-picker/date-input.es.js +92 -75
  9. package/dist/components/date-picker/date-picker.cjs.js +1 -1
  10. package/dist/components/date-picker/date-picker.es.js +104 -95
  11. package/dist/components/date-picker/date-picker.utils.cjs.js +1 -1
  12. package/dist/components/date-picker/date-picker.utils.es.js +51 -43
  13. package/dist/components/date-picker/use-hidden-field-value.cjs.js +1 -0
  14. package/dist/components/date-picker/use-hidden-field-value.es.js +11 -0
  15. package/dist/components/menu/menu.es.js +1 -9
  16. package/dist/components/otp/otp.cjs.js +2 -0
  17. package/dist/components/otp/otp.es.js +93 -0
  18. package/dist/components/password/password.cjs.js +1 -1
  19. package/dist/components/password/password.es.js +2 -2
  20. package/dist/components/select/select.cjs.js +1 -1
  21. package/dist/components/select/select.es.js +68 -60
  22. package/dist/hooks/internal/is-apple-device.cjs.js +1 -0
  23. package/dist/hooks/internal/is-apple-device.es.js +9 -0
  24. package/dist/hooks/internal/use-latest-ref.cjs.js +1 -0
  25. package/dist/hooks/internal/use-latest-ref.es.js +11 -0
  26. package/dist/hooks/use-array/use-array.cjs.js +1 -1
  27. package/dist/hooks/use-array/use-array.es.js +54 -42
  28. package/dist/hooks/use-async/use-async.cjs.js +1 -1
  29. package/dist/hooks/use-async/use-async.es.js +53 -20
  30. package/dist/hooks/use-boolean/use-boolean.cjs.js +1 -0
  31. package/dist/hooks/use-boolean/use-boolean.es.js +25 -0
  32. package/dist/hooks/use-click-outside/use-click-outside.cjs.js +1 -1
  33. package/dist/hooks/use-click-outside/use-click-outside.es.js +26 -12
  34. package/dist/hooks/use-debounce-callback/use-debounced-callback.cjs.js +1 -1
  35. package/dist/hooks/use-debounce-callback/use-debounced-callback.es.js +27 -10
  36. package/dist/hooks/use-debounce-value/use-debounced-value.cjs.js +1 -1
  37. package/dist/hooks/use-debounce-value/use-debounced-value.es.js +7 -9
  38. package/dist/hooks/use-disclosure/use-disclosure.cjs.js +1 -1
  39. package/dist/hooks/use-disclosure/use-disclosure.es.js +21 -11
  40. package/dist/hooks/use-document-title/use-document-title.cjs.js +1 -1
  41. package/dist/hooks/use-document-title/use-document-title.es.js +14 -12
  42. package/dist/hooks/use-event-listener/use-event-listener.cjs.js +1 -1
  43. package/dist/hooks/use-event-listener/use-event-listener.es.js +17 -9
  44. package/dist/hooks/use-hotkey/use-hotkey.cjs.js +1 -1
  45. package/dist/hooks/use-hotkey/use-hotkey.es.js +30 -14
  46. package/dist/hooks/use-hotkey/utils/is-input-field.cjs.js +1 -1
  47. package/dist/hooks/use-hotkey/utils/is-input-field.es.js +4 -2
  48. package/dist/hooks/use-hotkey/utils/match-and-run.cjs.js +1 -0
  49. package/dist/hooks/use-hotkey/utils/match-and-run.es.js +12 -0
  50. package/dist/hooks/use-hotkey/utils/match-key-modifiers.cjs.js +1 -1
  51. package/dist/hooks/use-hotkey/utils/match-key-modifiers.es.js +13 -12
  52. package/dist/hooks/use-hover/use-hover.cjs.js +1 -1
  53. package/dist/hooks/use-hover/use-hover.es.js +32 -17
  54. package/dist/hooks/use-is-visible/use-is-visible.cjs.js +1 -1
  55. package/dist/hooks/use-is-visible/use-is-visible.es.js +31 -27
  56. package/dist/hooks/use-local-storage/use-local-storage.cjs.js +1 -1
  57. package/dist/hooks/use-local-storage/use-local-storage.es.js +52 -20
  58. package/dist/hooks/use-media-query/use-media-query.cjs.js +1 -1
  59. package/dist/hooks/use-media-query/use-media-query.es.js +21 -11
  60. package/dist/hooks/use-mutation/use-mutation.cjs.js +1 -1
  61. package/dist/hooks/use-mutation/use-mutation.es.js +36 -22
  62. package/dist/hooks/use-object/use-object.cjs.js +1 -1
  63. package/dist/hooks/use-object/use-object.es.js +26 -22
  64. package/dist/hooks/use-prevent-page-close/use-prevent-page-close.cjs.js +1 -0
  65. package/dist/hooks/use-prevent-page-close/use-prevent-page-close.es.js +14 -0
  66. package/dist/hooks/use-step/use-step.cjs.js +1 -1
  67. package/dist/hooks/use-step/use-step.es.js +25 -24
  68. package/dist/index.cjs.js +1 -1
  69. package/dist/index.es.js +308 -300
  70. package/dist/src/components/date-picker/date-picker.utils.d.ts +17 -0
  71. package/dist/src/components/date-picker/use-hidden-field-value.d.ts +12 -0
  72. package/dist/src/components/index.d.ts +1 -0
  73. package/dist/src/hooks/index.d.ts +2 -2
  74. package/dist/src/hooks/internal/index.d.ts +2 -0
  75. package/dist/src/hooks/internal/is-apple-device.d.ts +12 -0
  76. package/dist/src/hooks/internal/use-latest-ref.d.ts +12 -0
  77. package/dist/src/hooks/use-array/use-array.d.ts +24 -11
  78. package/dist/src/hooks/use-async/use-async.d.ts +16 -13
  79. package/dist/src/hooks/use-boolean/index.d.ts +1 -0
  80. package/dist/src/hooks/use-boolean/use-boolean.d.ts +15 -0
  81. package/dist/src/hooks/use-boolean/use-boolean.test.d.ts +1 -0
  82. package/dist/src/hooks/use-click-outside/use-click-outside.d.ts +23 -1
  83. package/dist/src/hooks/use-debounce-callback/use-debounced-callback.d.ts +19 -1
  84. package/dist/src/hooks/use-debounce-value/use-debounced-value.d.ts +10 -1
  85. package/dist/src/hooks/use-disclosure/use-disclosure.d.ts +17 -8
  86. package/dist/src/hooks/use-document-title/use-document-title.d.ts +11 -0
  87. package/dist/src/hooks/use-event-listener/use-event-listener.d.ts +18 -1
  88. package/dist/src/hooks/use-hotkey/index.d.ts +2 -1
  89. package/dist/src/hooks/use-hotkey/use-hotkey.d.ts +62 -5
  90. package/dist/src/hooks/use-hotkey/utils/index.d.ts +4 -3
  91. package/dist/src/hooks/use-hotkey/utils/is-input-field.d.ts +12 -2
  92. package/dist/src/hooks/use-hotkey/utils/is-input-field.test.d.ts +1 -0
  93. package/dist/src/hooks/use-hotkey/utils/match-and-run.d.ts +36 -0
  94. package/dist/src/hooks/use-hotkey/utils/match-and-run.test.d.ts +1 -0
  95. package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.d.ts +20 -6
  96. package/dist/src/hooks/use-hotkey/utils/match-key-modifiers.test.d.ts +1 -0
  97. package/dist/src/hooks/use-hover/use-hover.d.ts +8 -4
  98. package/dist/src/hooks/use-is-visible/use-is-visible.d.ts +28 -4
  99. package/dist/src/hooks/use-local-storage/use-local-storage.d.ts +13 -2
  100. package/dist/src/hooks/use-media-query/use-media-query.d.ts +10 -1
  101. package/dist/src/hooks/use-media-query/use-media-query.test.d.ts +1 -0
  102. package/dist/src/hooks/use-mutation/use-mutation.d.ts +18 -11
  103. package/dist/src/hooks/use-object/use-object.d.ts +15 -6
  104. package/dist/src/hooks/use-prevent-page-close/index.d.ts +1 -0
  105. package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.d.ts +10 -0
  106. package/dist/src/hooks/use-prevent-page-close/use-prevent-page-close.test.d.ts +1 -0
  107. package/dist/src/hooks/use-step/use-step.d.ts +18 -11
  108. package/dist/src/utils/form.d.ts +10 -0
  109. package/package.json +1 -1
  110. package/src/components/alert-dialog/alert-dialog.test.tsx +13 -9
  111. package/src/components/auto-complete/auto-complete.test.tsx +4 -14
  112. package/src/components/avatar/avatar.test.tsx +7 -12
  113. package/src/components/button/button.test.tsx +10 -15
  114. package/src/components/button/button.tsx +14 -9
  115. package/src/components/button/components/base-button.tsx +2 -4
  116. package/src/components/calendar/calendar.test.tsx +12 -19
  117. package/src/components/calendar/calendar.tsx +4 -0
  118. package/src/components/card/card.test.tsx +4 -6
  119. package/src/components/checkbox/checkbox.test.tsx +12 -8
  120. package/src/components/checkbox-group/checkbox-group.test.tsx +7 -8
  121. package/src/components/combobox/combobox.test.tsx +24 -21
  122. package/src/components/date-picker/date-input-form.test.tsx +77 -0
  123. package/src/components/date-picker/date-input.stories.tsx +30 -18
  124. package/src/components/date-picker/date-input.tsx +77 -44
  125. package/src/components/date-picker/date-picker.stories.tsx +31 -1
  126. package/src/components/date-picker/date-picker.test.tsx +3 -13
  127. package/src/components/date-picker/date-picker.tsx +35 -16
  128. package/src/components/date-picker/date-picker.utils.test.ts +32 -14
  129. package/src/components/date-picker/date-picker.utils.ts +33 -0
  130. package/src/components/date-picker/use-date-input-popover.test.ts +3 -1
  131. package/src/components/date-picker/use-hidden-field-value.ts +23 -0
  132. package/src/components/dialog/dialog.test.tsx +10 -8
  133. package/src/components/dropzone/dropzone.test.tsx +11 -13
  134. package/src/components/empty/empty.test.tsx +4 -3
  135. package/src/components/field/field.test.tsx +12 -13
  136. package/src/components/form/form.stories.tsx +16 -1
  137. package/src/components/index.ts +1 -0
  138. package/src/components/label/label.test.tsx +3 -3
  139. package/src/components/menu/menu.tsx +1 -5
  140. package/src/components/number-input/number-input.test.tsx +6 -2
  141. package/src/components/password/password.test.tsx +20 -6
  142. package/src/components/password/password.tsx +2 -2
  143. package/src/components/popover/popover.test.tsx +4 -4
  144. package/src/components/progress/progress.test.tsx +7 -8
  145. package/src/components/radio-group/radio-group.test.tsx +17 -11
  146. package/src/components/select/select.test.tsx +10 -10
  147. package/src/components/select/select.tsx +9 -1
  148. package/src/components/stepper/stepper.stories.tsx +11 -15
  149. package/src/components/stepper/stepper.test.tsx +6 -4
  150. package/src/components/switch/switch.test.tsx +3 -3
  151. package/src/components/table/table.test.tsx +9 -3
  152. package/src/components/tabs/tabs.test.tsx +6 -2
  153. package/src/components/tag/tag.test.tsx +1 -3
  154. package/src/components/textarea/textarea.test.tsx +4 -1
  155. package/src/components/timeline/timeline.test.tsx +10 -5
  156. package/src/components/toast/toast.test.tsx +11 -14
  157. package/src/components/tooltip/tooltip.test.tsx +1 -5
  158. package/src/components/tree/tree.test.tsx +3 -1
  159. package/src/hooks/index.ts +2 -2
  160. package/src/hooks/internal/index.ts +2 -0
  161. package/src/hooks/internal/is-apple-device.test.ts +41 -0
  162. package/src/hooks/internal/is-apple-device.ts +33 -0
  163. package/src/hooks/internal/use-isomorphic-layout-effect.ts +3 -1
  164. package/src/hooks/internal/use-latest-ref.ts +21 -0
  165. package/src/hooks/use-array/use-array.stories.tsx +435 -64
  166. package/src/hooks/use-array/use-array.test.tsx +398 -15
  167. package/src/hooks/use-array/use-array.ts +105 -66
  168. package/src/hooks/use-async/use-async.stories.tsx +255 -131
  169. package/src/hooks/use-async/use-async.test.ts +397 -0
  170. package/src/hooks/use-async/use-async.ts +117 -39
  171. package/src/hooks/use-boolean/index.ts +1 -0
  172. package/src/hooks/use-boolean/use-boolean.stories.tsx +377 -0
  173. package/src/hooks/use-boolean/use-boolean.test.tsx +177 -0
  174. package/src/hooks/use-boolean/use-boolean.ts +50 -0
  175. package/src/hooks/use-click-outside/use-click-outside.stories.tsx +188 -18
  176. package/src/hooks/use-click-outside/use-click-outside.test.tsx +89 -10
  177. package/src/hooks/use-click-outside/use-click-outside.ts +62 -16
  178. package/src/hooks/use-debounce-callback/use-debounced-callback.stories.tsx +141 -41
  179. package/src/hooks/use-debounce-callback/use-debounced-callback.test.ts +217 -9
  180. package/src/hooks/use-debounce-callback/use-debounced-callback.ts +71 -11
  181. package/src/hooks/use-debounce-value/use-debounced-value.stories.tsx +247 -47
  182. package/src/hooks/use-debounce-value/use-debounced-value.test.ts +105 -10
  183. package/src/hooks/use-debounce-value/use-debounced-value.ts +19 -10
  184. package/src/hooks/use-disclosure/use-disclosure.stories.tsx +305 -14
  185. package/src/hooks/use-disclosure/use-disclosure.test.ts +198 -50
  186. package/src/hooks/use-disclosure/use-disclosure.ts +49 -29
  187. package/src/hooks/use-document-title/use-document-title.stories.tsx +54 -0
  188. package/src/hooks/use-document-title/use-document-title.test.tsx +26 -0
  189. package/src/hooks/use-document-title/{use-document-title.tsx → use-document-title.ts} +17 -3
  190. package/src/hooks/use-event-listener/use-event-listener.stories.tsx +105 -9
  191. package/src/hooks/use-event-listener/use-event-listener.test.tsx +77 -10
  192. package/src/hooks/use-event-listener/use-event-listener.ts +71 -11
  193. package/src/hooks/use-focus-trap/use-focus-trap.test.ts +31 -6
  194. package/src/hooks/use-focus-trap/use-focus-trap.ts +3 -2
  195. package/src/hooks/use-hotkey/index.ts +9 -1
  196. package/src/hooks/use-hotkey/use-hotkey.stories.tsx +279 -74
  197. package/src/hooks/use-hotkey/use-hotkey.test.tsx +286 -34
  198. package/src/hooks/use-hotkey/use-hotkey.ts +141 -17
  199. package/src/hooks/use-hotkey/utils/index.ts +8 -3
  200. package/src/hooks/use-hotkey/utils/is-input-field.test.ts +78 -0
  201. package/src/hooks/use-hotkey/utils/is-input-field.ts +31 -10
  202. package/src/hooks/use-hotkey/utils/match-and-run.test.ts +203 -0
  203. package/src/hooks/use-hotkey/utils/match-and-run.ts +62 -0
  204. package/src/hooks/use-hotkey/utils/match-key-modifiers.test.ts +65 -0
  205. package/src/hooks/use-hotkey/utils/match-key-modifiers.ts +39 -12
  206. package/src/hooks/use-hover/use-hover.stories.tsx +258 -80
  207. package/src/hooks/use-hover/use-hover.test.tsx +266 -26
  208. package/src/hooks/use-hover/use-hover.tsx +93 -28
  209. package/src/hooks/use-is-visible/use-is-visible.stories.tsx +193 -46
  210. package/src/hooks/use-is-visible/use-is-visible.test.tsx +235 -7
  211. package/src/hooks/use-is-visible/use-is-visible.ts +114 -0
  212. package/src/hooks/use-local-storage/use-local-storage.stories.tsx +129 -29
  213. package/src/hooks/use-local-storage/use-local-storage.test.ts +106 -41
  214. package/src/hooks/use-local-storage/use-local-storage.ts +100 -31
  215. package/src/hooks/use-media-query/use-media-query.stories.tsx +86 -26
  216. package/src/hooks/use-media-query/use-media-query.test.ts +132 -0
  217. package/src/hooks/use-media-query/use-media-query.ts +39 -14
  218. package/src/hooks/use-memoized-fn/use-memoized-fn.ts +0 -1
  219. package/src/hooks/use-mutation/use-mutation.stories.tsx +260 -94
  220. package/src/hooks/use-mutation/use-mutation.test.ts +359 -0
  221. package/src/hooks/use-mutation/use-mutation.ts +97 -0
  222. package/src/hooks/use-object/use-object.stories.tsx +310 -79
  223. package/src/hooks/use-object/use-object.test.tsx +235 -56
  224. package/src/hooks/use-object/use-object.ts +59 -0
  225. package/src/hooks/use-pagination/use-pagination.tsx +0 -1
  226. package/src/hooks/use-prevent-page-close/index.ts +1 -0
  227. package/src/hooks/use-prevent-page-close/use-prevent-page-close.stories.tsx +39 -0
  228. package/src/hooks/use-prevent-page-close/use-prevent-page-close.test.ts +89 -0
  229. package/src/hooks/use-prevent-page-close/use-prevent-page-close.ts +27 -0
  230. package/src/hooks/use-range-pagination/use-range-pagination.test.tsx +1 -1
  231. package/src/hooks/use-range-pagination/use-range-pagination.tsx +1 -1
  232. package/src/hooks/use-selection/use-selection.ts +0 -1
  233. package/src/hooks/use-step/use-step.stories.tsx +178 -65
  234. package/src/hooks/use-step/use-step.test.ts +178 -53
  235. package/src/hooks/use-step/use-step.ts +57 -49
  236. package/src/utils/form.test.tsx +13 -8
  237. package/src/utils/form.tsx +10 -0
  238. package/src/utils/functions/getFormData.test.ts +1 -1
  239. package/dist/hooks/use-hotkey/utils/create-hotkey-listener.cjs.js +0 -1
  240. package/dist/hooks/use-hotkey/utils/create-hotkey-listener.es.js +0 -10
  241. package/dist/hooks/use-prevent-close-window/use-prevent-close-window.cjs.js +0 -1
  242. package/dist/hooks/use-prevent-close-window/use-prevent-close-window.es.js +0 -15
  243. package/dist/hooks/use-toggle/use-toggle.cjs.js +0 -1
  244. package/dist/hooks/use-toggle/use-toggle.es.js +0 -10
  245. package/dist/src/hooks/use-hotkey/utils/create-hotkey-listener.d.ts +0 -1
  246. package/dist/src/hooks/use-prevent-close-window/index.d.ts +0 -1
  247. package/dist/src/hooks/use-prevent-close-window/use-prevent-close-window.d.ts +0 -13
  248. package/dist/src/hooks/use-toggle/index.d.ts +0 -1
  249. package/dist/src/hooks/use-toggle/use-toggle.d.ts +0 -3
  250. package/src/hooks/use-async/use-async.test.tsx +0 -68
  251. package/src/hooks/use-hotkey/utils/create-hotkey-listener.ts +0 -25
  252. package/src/hooks/use-is-visible/use-is-visible.tsx +0 -49
  253. package/src/hooks/use-mutation/use-mutation.test.tsx +0 -83
  254. package/src/hooks/use-mutation/use-mutation.tsx +0 -59
  255. package/src/hooks/use-object/use-object.tsx +0 -46
  256. package/src/hooks/use-prevent-close-window/index.ts +0 -1
  257. package/src/hooks/use-prevent-close-window/use-prevent-close-window.stories.tsx +0 -32
  258. package/src/hooks/use-prevent-close-window/use-prevent-close-window.test.ts +0 -79
  259. package/src/hooks/use-prevent-close-window/use-prevent-close-window.ts +0 -33
  260. package/src/hooks/use-toggle/index.ts +0 -1
  261. package/src/hooks/use-toggle/use-toggle.stories.tsx +0 -25
  262. package/src/hooks/use-toggle/use-toggle.test.tsx +0 -64
  263. package/src/hooks/use-toggle/use-toggle.ts +0 -14
  264. /package/dist/src/{hooks/use-prevent-close-window/use-prevent-close-window.test.d.ts → components/date-picker/date-input-form.test.d.ts} +0 -0
  265. /package/dist/src/hooks/{use-toggle/use-toggle.test.d.ts → internal/is-apple-device.test.d.ts} +0 -0
@@ -1,8 +1,54 @@
1
- import { Meta, StoryObj } from "@storybook/react-vite";
2
- import { HTMLProps, ReactNode, useState } from "react";
1
+ import type { Meta, StoryObj } from "@storybook/react-vite";
2
+ import { type ReactNode, useState } from "react";
3
+ import { Kbd, KbdGroup } from "../../components";
3
4
  import { cn } from "../../lib";
4
- import { useHotkey } from "../use-hotkey";
5
+ import { getHotkeyHandler, useHotkey } from "../use-hotkey";
5
6
 
7
+ /**
8
+ * Bind keyboard shortcuts to handlers. `useHotkey` registers on `document`, so
9
+ * shortcuts fire regardless of which element has focus; `getHotkeyHandler`
10
+ * builds an element-scoped `onKeyDown` handler instead.
11
+ *
12
+ * **Two forms of `useHotkey`:**
13
+ * - Single binding — `useHotkey(keys, handler, options?)`. `keys` is a string
14
+ * or string array sharing one handler (the handler fires if any matches).
15
+ * - Multiple bindings — `useHotkey(bindings)` where `bindings` is a list of
16
+ * `[key, handler, options?]` tuples, each dispatched independently with its
17
+ * own options.
18
+ *
19
+ * ```tsx
20
+ * useHotkey("mod+k", openPalette);
21
+ * useHotkey(["w", "ArrowUp"], moveUp);
22
+ * useHotkey([
23
+ * ["mod+k", openPalette],
24
+ * ["mod+s", save, { preventDefault: false }],
25
+ * ]);
26
+ * ```
27
+ *
28
+ * **Key syntax** — zero or more modifiers followed by the main key, joined with
29
+ * `+` (e.g. `"ctrl+shift+k"`). Literal modifiers are `shift`, `ctrl`, `alt`,
30
+ * `meta`. `mod` is platform-aware: it resolves to `meta` (⌘) on Apple devices
31
+ * and `ctrl` everywhere else — write the shortcut once and it adapts.
32
+ *
33
+ * **Matching is strict and exact.** A binding fires only when every required
34
+ * modifier is held and no other modifier is. `"a"` will not fire for `Ctrl+A`;
35
+ * `"ctrl+k"` will not fire for `Ctrl+Shift+K`. Strict `mod` also rejects the
36
+ * opposite platform's modifier (on Apple, `mod+k` matches ⌘+K but not Ctrl+K).
37
+ *
38
+ * **`getHotkeyHandler(bindings)`** returns an `onKeyDown` handler for a single
39
+ * element. It shares the same strict-modifier and `mod` behavior but applies no
40
+ * tag suppression — it has no `tagsToIgnore` and matches regardless of the
41
+ * target tag, including the `<input>` it is attached to. Use it for shortcuts
42
+ * that must work while a specific field is focused (e.g. ⌘/Ctrl+Enter to
43
+ * submit).
44
+ *
45
+ * ```tsx
46
+ * <input onKeyDown={getHotkeyHandler("mod+enter", submit)} />
47
+ * ```
48
+ *
49
+ * Handlers are read through a ref, so passing an inline function never
50
+ * re-attaches the listener. SSR-safe: no DOM access on the server.
51
+ */
6
52
  const meta: Meta = {
7
53
  title: "hooks/useHotkey",
8
54
  };
@@ -10,6 +56,17 @@ const meta: Meta = {
10
56
  export default meta;
11
57
  type Story = StoryObj<typeof meta>;
12
58
 
59
+ const isApple =
60
+ typeof navigator !== "undefined" &&
61
+ /Mac|iP(hone|ad|od)/.test(navigator.platform);
62
+ const modLabel = isApple ? "⌘" : "Ctrl";
63
+
64
+ const Stage = ({ children }: { children: ReactNode }) => (
65
+ <div className="flex h-52 w-80 items-center justify-center overflow-hidden rounded-md border border-input">
66
+ {children}
67
+ </div>
68
+ );
69
+
13
70
  const generateRandomColor = () => {
14
71
  const letters = "0123456789ABCDEF";
15
72
  let color = "#";
@@ -19,78 +76,31 @@ const generateRandomColor = () => {
19
76
  return color;
20
77
  };
21
78
 
22
- const Kbd = (props: HTMLProps<HTMLDivElement>) => (
23
- <kbd
24
- {...props}
25
- className="inline-block px-3 py-1 text-sm font-semibold text-gray-800 bg-gray-200 border rounded-md shadow-inner border-input"
26
- />
27
- );
28
-
29
- const Wrapper = ({ children }: { children: ReactNode }) => (
30
- <div>
31
- <div className="space-y-2">
32
- <div className="flex gap-2">
33
- <span>Usa</span>
34
- <Kbd>C</Kbd>
35
- <span>+</span>
36
- <Kbd>R</Kbd>
37
- <span>para cambiar el color</span>
38
- </div>
39
- <div className="flex gap-2">
40
- <span>Usa</span>
41
- <Kbd>w</Kbd>
42
- <Kbd>a</Kbd>
43
- <Kbd>s</Kbd>
44
- <Kbd>d</Kbd>
45
- <span>o</span>
46
- <Kbd>&#8593;</Kbd>
47
- <Kbd>&#8592;</Kbd>
48
- <Kbd>&#8595;</Kbd>
49
- <Kbd>&#8594;</Kbd>
50
- <span>Para moverte</span>
51
- </div>
52
- </div>
53
- <div className="flex items-center justify-center w-80 h-52">{children}</div>
54
- </div>
55
- );
56
-
57
79
  /**
58
- * Opciones
59
- *
60
- * ```tsx
61
- * type HotkeyOptions = {
62
- * // Es el evento que se va a utilizar; Keydown, Keyup, Keypress
63
- * eventName?: HotkeyEventName;
64
- * @default "keydown"
65
- * // si se debe prevenir los comandos por defecto del navegador
66
- * preventDefault?: boolean;
67
- * @default true
68
- * // si los comandos se deberían ejecutar mientras el usuario está escribiendo en un input o textarea
69
- * ignoreInputFields?: boolean;
70
- * @default false
71
- * };
72
- * ```
73
- **/
74
- export const Default: Story = {
80
+ * Interactive demo combining several bindings:
81
+ * - `Ctrl+B` randomizes the color.
82
+ * - `Space` pulses a ring.
83
+ * - `WASD` or the arrow keys move the dot, using an array of keys on a single
84
+ * handler that branches on `event.key`.
85
+ */
86
+ export const Playground: Story = {
75
87
  render: () => {
76
88
  const [posX, setPosX] = useState(0);
77
89
  const [posY, setPosY] = useState(0);
78
90
  const [color, setColor] = useState("#333333");
79
- const [spacePressed, setSpacePressed] = useState(false);
91
+ const [pulse, setPulse] = useState(false);
80
92
 
81
- useHotkey(["ctrl+r"], () => setColor(generateRandomColor()));
93
+ useHotkey("ctrl+b", () => setColor(generateRandomColor()));
82
94
 
83
95
  useHotkey(" ", () => {
84
- setSpacePressed(true);
85
- setTimeout(() => {
86
- setSpacePressed(false);
87
- }, 300);
96
+ setPulse(true);
97
+ setTimeout(() => setPulse(false), 300);
88
98
  });
89
99
 
90
100
  useHotkey(
91
101
  ["w", "s", "a", "d", "ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"],
92
- (e) => {
93
- const { key } = e;
102
+ (event) => {
103
+ const { key } = event;
94
104
  if (["w", "ArrowUp"].includes(key)) return setPosY((y) => y - 20);
95
105
  if (["s", "ArrowDown"].includes(key)) return setPosY((y) => y + 20);
96
106
  if (["a", "ArrowLeft"].includes(key)) return setPosX((x) => x - 20);
@@ -99,18 +109,213 @@ export const Default: Story = {
99
109
  );
100
110
 
101
111
  return (
102
- <Wrapper>
103
- <div
104
- className={cn(
105
- "w-10 h-10 rounded-full",
106
- spacePressed && "ring ring-offset-2",
107
- )}
108
- style={{
109
- backgroundColor: color,
110
- transform: `translate(${posX}px, ${posY}px)`,
111
- }}
112
+ <div className="space-y-4">
113
+ <div className="flex flex-wrap items-center gap-2 text-sm">
114
+ <KbdGroup>
115
+ <Kbd>Ctrl</Kbd>
116
+ <Kbd>B</Kbd>
117
+ </KbdGroup>
118
+ color · <Kbd>Space</Kbd> pulse ·{" "}
119
+ <KbdGroup>
120
+ <Kbd>W</Kbd>
121
+ <Kbd>A</Kbd>
122
+ <Kbd>S</Kbd>
123
+ <Kbd>D</Kbd>
124
+ </KbdGroup>
125
+ move
126
+ </div>
127
+ <Stage>
128
+ <div
129
+ className={cn(
130
+ "h-10 w-10 rounded-full transition-transform",
131
+ pulse && "ring ring-offset-2",
132
+ )}
133
+ style={{
134
+ backgroundColor: color,
135
+ transform: `translate(${posX}px, ${posY}px)`,
136
+ }}
137
+ />
138
+ </Stage>
139
+ </div>
140
+ );
141
+ },
142
+ };
143
+
144
+ /**
145
+ * Modifier matching is exact: `"ctrl+m"` fires only when Ctrl is held and
146
+ * Shift/Alt/Meta are not. Try pressing `M` alone or `Ctrl+Shift+M` — neither
147
+ * matches.
148
+ */
149
+ export const KeyCombo: Story = {
150
+ render: () => {
151
+ const [count, setCount] = useState(0);
152
+ useHotkey("ctrl+m", () => setCount((c) => c + 1));
153
+ return (
154
+ <p className="text-sm">
155
+ Press <Kbd>Ctrl</Kbd> + <Kbd>M</Kbd> — matched {count} time(s)
156
+ </p>
157
+ );
158
+ },
159
+ };
160
+
161
+ /**
162
+ * `mod` is platform-aware: it resolves to `meta` (⌘) on Apple devices and
163
+ * `ctrl` everywhere else, so one binding works cross-platform. Matching stays
164
+ * strict — on a Mac, `mod+m` matches ⌘+M but not Ctrl+M.
165
+ */
166
+ export const Mod: Story = {
167
+ render: () => {
168
+ const [count, setCount] = useState(0);
169
+ useHotkey("mod+m", () => setCount((c) => c + 1));
170
+ return (
171
+ <p className="text-sm">
172
+ Press <Kbd>{modLabel}</Kbd> + <Kbd>M</Kbd> — matched {count} time(s)
173
+ </p>
174
+ );
175
+ },
176
+ };
177
+
178
+ /**
179
+ * The array-of-tuples form binds several independent shortcuts in one call.
180
+ * Each `[key, handler, options?]` tuple dispatches on its own and can override
181
+ * options per binding (here `mod+s` opts out of `preventDefault`).
182
+ */
183
+ export const MultipleBindings: Story = {
184
+ render: () => {
185
+ const [log, setLog] = useState<string[]>([]);
186
+ const push = (entry: string) => setLog((l) => [entry, ...l].slice(0, 5));
187
+
188
+ useHotkey([
189
+ ["mod+m", () => push("palette")],
190
+ ["mod+s", () => push("save"), { preventDefault: false }],
191
+ ]);
192
+
193
+ return (
194
+ <div className="space-y-3 text-sm">
195
+ <div className="flex flex-wrap items-center gap-2">
196
+ <KbdGroup>
197
+ <Kbd>{modLabel}</Kbd>
198
+ <Kbd>M</Kbd>
199
+ </KbdGroup>
200
+ palette ·
201
+ <KbdGroup>
202
+ <Kbd>{modLabel}</Kbd>
203
+ <Kbd>S</Kbd>
204
+ </KbdGroup>
205
+ save
206
+ </div>
207
+ <ul className="font-mono text-muted-foreground">
208
+ {log.map((entry, i) => (
209
+ <li key={`${entry}-${i}`}>{entry}</li>
210
+ ))}
211
+ </ul>
212
+ </div>
213
+ );
214
+ },
215
+ };
216
+
217
+ /**
218
+ * By default `useHotkey` skips events while a control in `tagsToIgnore`
219
+ * (`INPUT`, `TEXTAREA`, `SELECT`) is focused. Focus the input and press `S` —
220
+ * the counter stays put. Pass `tagsToIgnore: []` to let shortcuts fire during
221
+ * text entry, or `triggerOnContentEditable: true` for contentEditable regions.
222
+ */
223
+ export const TagsToIgnore: Story = {
224
+ render: () => {
225
+ const [ignored, setIgnored] = useState(0);
226
+ const [always, setAlways] = useState(0);
227
+ useHotkey("s", () => setIgnored((c) => c + 1));
228
+ useHotkey("d", () => setAlways((c) => c + 1), { tagsToIgnore: [] });
229
+ return (
230
+ <div className="space-y-3 text-sm">
231
+ <p>
232
+ <Kbd>S</Kbd> (default) — matched {ignored} · <Kbd>D</Kbd>{" "}
233
+ (tagsToIgnore: []) — matched {always}
234
+ </p>
235
+ <input
236
+ placeholder="type here — S won't fire, D will"
237
+ className="w-full rounded-md border border-input px-3 py-1"
238
+ />
239
+ </div>
240
+ );
241
+ },
242
+ };
243
+
244
+ /**
245
+ * `getHotkeyHandler` builds an element-scoped `onKeyDown` handler. Unlike
246
+ * `useHotkey`, it applies no tag suppression, so it fires even on the `<input>`
247
+ * it is attached to — the canonical case for a submit shortcut while a field is
248
+ * focused. Press ⌘/Ctrl+Enter inside the input to submit.
249
+ */
250
+ export const ElementScoped: Story = {
251
+ render: () => {
252
+ const [value, setValue] = useState("");
253
+ const [submitted, setSubmitted] = useState<string | null>(null);
254
+ return (
255
+ <div className="space-y-3 text-sm">
256
+ <input
257
+ value={value}
258
+ onChange={(e) => setValue(e.target.value)}
259
+ onKeyDown={getHotkeyHandler("mod+enter", () => {
260
+ setSubmitted(value);
261
+ setValue("");
262
+ })}
263
+ placeholder="type, then press mod+Enter"
264
+ className="w-full rounded-md border border-input px-3 py-1"
112
265
  />
113
- </Wrapper>
266
+ <p className="flex flex-wrap items-center gap-2">
267
+ <KbdGroup>
268
+ <Kbd>{modLabel}</Kbd>
269
+ <Kbd>Enter</Kbd>
270
+ </KbdGroup>
271
+ to submit — last: {submitted ?? "—"}
272
+ </p>
273
+ </div>
274
+ );
275
+ },
276
+ };
277
+
278
+ /**
279
+ * `watch: false` disables the binding without unmounting. Toggle it to start
280
+ * and stop listening at runtime.
281
+ */
282
+ export const Disabled: Story = {
283
+ render: () => {
284
+ const [enabled, setEnabled] = useState(false);
285
+ const [count, setCount] = useState(0);
286
+ useHotkey("e", () => setCount((c) => c + 1), {
287
+ watch: enabled,
288
+ });
289
+ return (
290
+ <div className="space-y-3 text-sm">
291
+ <label className="flex items-center gap-2">
292
+ <input
293
+ type="checkbox"
294
+ checked={enabled}
295
+ onChange={(e) => setEnabled(e.target.checked)}
296
+ />
297
+ binding enabled
298
+ </label>
299
+ <p>
300
+ Press <Kbd>e</Kbd> — matched {count} time(s)
301
+ </p>
302
+ </div>
303
+ );
304
+ },
305
+ };
306
+
307
+ /**
308
+ * Bind to `keyup` instead of the default `keydown` via the `eventName` option
309
+ * — the handler fires when the key is released.
310
+ */
311
+ export const OnKeyUp: Story = {
312
+ render: () => {
313
+ const [count, setCount] = useState(0);
314
+ useHotkey("u", () => setCount((c) => c + 1), { eventName: "keyup" });
315
+ return (
316
+ <p className="text-sm">
317
+ Release <Kbd>U</Kbd> — matched {count} time(s)
318
+ </p>
114
319
  );
115
320
  },
116
321
  };