@zentauri-ui/zentauri-components 1.3.1 → 1.4.0

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 (307) hide show
  1. package/README.md +78 -0
  2. package/cli/cli.integration.test.ts +51 -0
  3. package/cli/index.mjs +664 -0
  4. package/cli/registry.json +36 -0
  5. package/cli/rewrite-imports.mjs +57 -0
  6. package/cli/rewrite-imports.test.ts +71 -0
  7. package/dist/ui/slider/slider.d.ts +18 -0
  8. package/dist/ui/slider/slider.d.ts.map +1 -1
  9. package/dist/ui/slider.js +21 -25
  10. package/dist/ui/slider.js.map +1 -1
  11. package/dist/ui/slider.mjs +21 -25
  12. package/dist/ui/slider.mjs.map +1 -1
  13. package/package.json +8 -2
  14. package/src/hooks/index.ts +48 -0
  15. package/src/hooks/useBodyScrollLock/index.ts +1 -0
  16. package/src/hooks/useBodyScrollLock/useBodyScrollLock.test.ts +51 -0
  17. package/src/hooks/useBodyScrollLock/useBodyScrollLock.ts +48 -0
  18. package/src/hooks/useClickOutside/index.ts +5 -0
  19. package/src/hooks/useClickOutside/useClickOutside.test.tsx +60 -0
  20. package/src/hooks/useClickOutside/useClickOutside.ts +52 -0
  21. package/src/hooks/useClipboard/index.ts +1 -0
  22. package/src/hooks/useClipboard/useClipboard.test.ts +101 -0
  23. package/src/hooks/useClipboard/useClipboard.ts +69 -0
  24. package/src/hooks/useControllableState/index.ts +4 -0
  25. package/src/hooks/useControllableState/useControllableState.test.ts +59 -0
  26. package/src/hooks/useControllableState/useControllableState.ts +49 -0
  27. package/src/hooks/useDebouncedValue/index.ts +1 -0
  28. package/src/hooks/useDebouncedValue/useDebouncedValue.test.ts +74 -0
  29. package/src/hooks/useDebouncedValue/useDebouncedValue.ts +29 -0
  30. package/src/hooks/useDisclosure/index.ts +5 -0
  31. package/src/hooks/useDisclosure/useDisclosure.test.ts +64 -0
  32. package/src/hooks/useDisclosure/useDisclosure.ts +62 -0
  33. package/src/hooks/useDocumentTitle/index.ts +4 -0
  34. package/src/hooks/useDocumentTitle/useDocumentTitle.test.ts +40 -0
  35. package/src/hooks/useDocumentTitle/useDocumentTitle.ts +58 -0
  36. package/src/hooks/useFocusManagement/index.ts +1 -0
  37. package/src/hooks/useFocusManagement/useFocusManagement.test.tsx +45 -0
  38. package/src/hooks/useFocusManagement/useFocusManagement.ts +77 -0
  39. package/src/hooks/useHover/index.ts +1 -0
  40. package/src/hooks/useHover/useHover.test.ts +45 -0
  41. package/src/hooks/useHover/useHover.ts +45 -0
  42. package/src/hooks/useInView/index.ts +1 -0
  43. package/src/hooks/useInView/useInView.test.ts +43 -0
  44. package/src/hooks/useInView/useInView.ts +28 -0
  45. package/src/hooks/useIntersectionObserver/index.ts +4 -0
  46. package/src/hooks/useIntersectionObserver/useIntersectionObserver.test.ts +75 -0
  47. package/src/hooks/useIntersectionObserver/useIntersectionObserver.ts +54 -0
  48. package/src/hooks/useIsMounted/index.ts +1 -0
  49. package/src/hooks/useIsMounted/useIsMounted.test.ts +25 -0
  50. package/src/hooks/useIsMounted/useIsMounted.ts +22 -0
  51. package/src/hooks/useIsomorphicLayoutEffect/index.ts +1 -0
  52. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.test.ts +19 -0
  53. package/src/hooks/useIsomorphicLayoutEffect/useIsomorphicLayoutEffect.ts +12 -0
  54. package/src/hooks/useLocalStorage/index.ts +4 -0
  55. package/src/hooks/useLocalStorage/useLocalStorage.test.ts +99 -0
  56. package/src/hooks/useLocalStorage/useLocalStorage.ts +109 -0
  57. package/src/hooks/useMediaQuery/index.ts +1 -0
  58. package/src/hooks/useMediaQuery/useMediaQuery.test.ts +63 -0
  59. package/src/hooks/useMediaQuery/useMediaQuery.ts +37 -0
  60. package/src/hooks/useNetworkStatus/index.ts +1 -0
  61. package/src/hooks/useNetworkStatus/useNetworkStatus.test.ts +53 -0
  62. package/src/hooks/useNetworkStatus/useNetworkStatus.ts +33 -0
  63. package/src/hooks/usePageVisibility/index.ts +1 -0
  64. package/src/hooks/usePageVisibility/usePageVisibility.test.ts +21 -0
  65. package/src/hooks/usePageVisibility/usePageVisibility.ts +31 -0
  66. package/src/hooks/usePagination/index.ts +6 -0
  67. package/src/hooks/usePagination/usePagination.test.ts +139 -0
  68. package/src/hooks/usePagination/usePagination.ts +153 -0
  69. package/src/hooks/usePrefersColorScheme/index.ts +4 -0
  70. package/src/hooks/usePrefersColorScheme/usePrefersColorScheme.test.ts +53 -0
  71. package/src/hooks/usePrefersColorScheme/usePrefersColorScheme.ts +21 -0
  72. package/src/hooks/usePrefersReducedMotion/index.ts +1 -0
  73. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.test.ts +27 -0
  74. package/src/hooks/usePrefersReducedMotion/usePrefersReducedMotion.ts +14 -0
  75. package/src/hooks/useResizeObserver/index.ts +4 -0
  76. package/src/hooks/useResizeObserver/useResizeObserver.test.ts +68 -0
  77. package/src/hooks/useResizeObserver/useResizeObserver.ts +58 -0
  78. package/src/hooks/useSessionStorage/index.ts +4 -0
  79. package/src/hooks/useSessionStorage/useSessionStorage.test.ts +54 -0
  80. package/src/hooks/useSessionStorage/useSessionStorage.ts +84 -0
  81. package/src/hooks/useThrottledCallback/index.ts +1 -0
  82. package/src/hooks/useThrottledCallback/useThrottledCallback.test.ts +75 -0
  83. package/src/hooks/useThrottledCallback/useThrottledCallback.ts +36 -0
  84. package/src/hooks/useToggle/index.ts +1 -0
  85. package/src/hooks/useToggle/useToggle.test.ts +40 -0
  86. package/src/hooks/useToggle/useToggle.ts +22 -0
  87. package/src/hooks/useWindowSize/index.ts +1 -0
  88. package/src/hooks/useWindowSize/useWindowSize.test.ts +23 -0
  89. package/src/hooks/useWindowSize/useWindowSize.ts +39 -0
  90. package/src/lib/utils.ts +25 -0
  91. package/src/ui/accordion/accordion-base.tsx +223 -0
  92. package/src/ui/accordion/accordion.test.tsx +146 -0
  93. package/src/ui/accordion/accordion.tsx +11 -0
  94. package/src/ui/accordion/animated/accordion-content-animated.tsx +46 -0
  95. package/src/ui/accordion/animated/accordion-root-animated.tsx +10 -0
  96. package/src/ui/accordion/animated/animations.ts +16 -0
  97. package/src/ui/accordion/animated/index.ts +7 -0
  98. package/src/ui/accordion/animated/types.ts +7 -0
  99. package/src/ui/accordion/index.ts +23 -0
  100. package/src/ui/accordion/types.ts +48 -0
  101. package/src/ui/accordion/variants.ts +115 -0
  102. package/src/ui/alert/alert-base.tsx +157 -0
  103. package/src/ui/alert/alert.test.tsx +150 -0
  104. package/src/ui/alert/alert.tsx +9 -0
  105. package/src/ui/alert/animated/alert-animated.tsx +20 -0
  106. package/src/ui/alert/animated/animations.ts +20 -0
  107. package/src/ui/alert/animated/index.ts +3 -0
  108. package/src/ui/alert/animated/types.ts +16 -0
  109. package/src/ui/alert/index.ts +22 -0
  110. package/src/ui/alert/types.ts +28 -0
  111. package/src/ui/alert/variants.ts +74 -0
  112. package/src/ui/avatar/animated/animations.ts +11 -0
  113. package/src/ui/avatar/animated/avatar-animated.tsx +25 -0
  114. package/src/ui/avatar/animated/index.ts +6 -0
  115. package/src/ui/avatar/animated/types.ts +16 -0
  116. package/src/ui/avatar/avatar-base.tsx +184 -0
  117. package/src/ui/avatar/avatar.test.tsx +51 -0
  118. package/src/ui/avatar/avatar.tsx +11 -0
  119. package/src/ui/avatar/index.ts +16 -0
  120. package/src/ui/avatar/types.ts +36 -0
  121. package/src/ui/avatar/variants.ts +52 -0
  122. package/src/ui/badge/animated/animations.ts +20 -0
  123. package/src/ui/badge/animated/badge-animated.tsx +28 -0
  124. package/src/ui/badge/animated/index.ts +5 -0
  125. package/src/ui/badge/animated/types.ts +18 -0
  126. package/src/ui/badge/badge-base.tsx +53 -0
  127. package/src/ui/badge/badge.test.tsx +48 -0
  128. package/src/ui/badge/badge.tsx +9 -0
  129. package/src/ui/badge/index.ts +5 -0
  130. package/src/ui/badge/types.ts +25 -0
  131. package/src/ui/badge/variants.ts +85 -0
  132. package/src/ui/breadcrumb/breadcrumb.test.tsx +62 -0
  133. package/src/ui/breadcrumb/breadcrumb.tsx +135 -0
  134. package/src/ui/breadcrumb/index.ts +28 -0
  135. package/src/ui/breadcrumb/types.ts +29 -0
  136. package/src/ui/breadcrumb/variants.ts +53 -0
  137. package/src/ui/buttons/animated/animations.ts +34 -0
  138. package/src/ui/buttons/animated/button-animated.tsx +70 -0
  139. package/src/ui/buttons/animated/index.ts +5 -0
  140. package/src/ui/buttons/animated/types.ts +29 -0
  141. package/src/ui/buttons/button-base.tsx +59 -0
  142. package/src/ui/buttons/button.test.tsx +480 -0
  143. package/src/ui/buttons/button.tsx +9 -0
  144. package/src/ui/buttons/index.ts +5 -0
  145. package/src/ui/buttons/types.ts +14 -0
  146. package/src/ui/buttons/variants.ts +77 -0
  147. package/src/ui/card/animated/animations.ts +32 -0
  148. package/src/ui/card/animated/card-animated.tsx +28 -0
  149. package/src/ui/card/animated/index.ts +12 -0
  150. package/src/ui/card/animated/types.ts +8 -0
  151. package/src/ui/card/card-base.tsx +146 -0
  152. package/src/ui/card/card.test.tsx +79 -0
  153. package/src/ui/card/card.tsx +11 -0
  154. package/src/ui/card/index.ts +21 -0
  155. package/src/ui/card/types.ts +42 -0
  156. package/src/ui/card/variants.ts +122 -0
  157. package/src/ui/divider/animated/animations.ts +27 -0
  158. package/src/ui/divider/animated/divider-animated.tsx +24 -0
  159. package/src/ui/divider/animated/index.ts +4 -0
  160. package/src/ui/divider/animated/types.ts +18 -0
  161. package/src/ui/divider/divider-base.tsx +80 -0
  162. package/src/ui/divider/divider.tsx +9 -0
  163. package/src/ui/divider/index.ts +14 -0
  164. package/src/ui/divider/types.ts +18 -0
  165. package/src/ui/divider/variants.ts +98 -0
  166. package/src/ui/drawer/animated/animations.ts +39 -0
  167. package/src/ui/drawer/animated/drawer-content-animated.tsx +101 -0
  168. package/src/ui/drawer/animated/index.ts +14 -0
  169. package/src/ui/drawer/animated/types.ts +18 -0
  170. package/src/ui/drawer/drawer-base.tsx +259 -0
  171. package/src/ui/drawer/drawer.test.tsx +132 -0
  172. package/src/ui/drawer/drawer.tsx +11 -0
  173. package/src/ui/drawer/index.ts +21 -0
  174. package/src/ui/drawer/types.ts +39 -0
  175. package/src/ui/drawer/variants.ts +122 -0
  176. package/src/ui/dropdown/dropdown.test.tsx +114 -0
  177. package/src/ui/dropdown/dropdown.tsx +179 -0
  178. package/src/ui/dropdown/index.ts +15 -0
  179. package/src/ui/dropdown/types.ts +68 -0
  180. package/src/ui/dropdown/variants.ts +138 -0
  181. package/src/ui/empty-state/animated/animations.ts +19 -0
  182. package/src/ui/empty-state/animated/empty-state-animated.tsx +23 -0
  183. package/src/ui/empty-state/animated/index.ts +7 -0
  184. package/src/ui/empty-state/animated/types.ts +26 -0
  185. package/src/ui/empty-state/empty-state-base.tsx +114 -0
  186. package/src/ui/empty-state/empty-state.tsx +9 -0
  187. package/src/ui/empty-state/index.ts +10 -0
  188. package/src/ui/empty-state/types.ts +19 -0
  189. package/src/ui/empty-state/variants.ts +51 -0
  190. package/src/ui/file-upload/file-upload.test.tsx +36 -0
  191. package/src/ui/file-upload/file-upload.tsx +119 -0
  192. package/src/ui/file-upload/index.ts +5 -0
  193. package/src/ui/file-upload/types.ts +21 -0
  194. package/src/ui/file-upload/variants.ts +29 -0
  195. package/src/ui/inputs/animated/animations.ts +36 -0
  196. package/src/ui/inputs/animated/index.ts +5 -0
  197. package/src/ui/inputs/animated/input-animated.tsx +124 -0
  198. package/src/ui/inputs/animated/types.ts +40 -0
  199. package/src/ui/inputs/index.ts +5 -0
  200. package/src/ui/inputs/input-base.tsx +114 -0
  201. package/src/ui/inputs/input.test.tsx +414 -0
  202. package/src/ui/inputs/input.tsx +8 -0
  203. package/src/ui/inputs/types.ts +18 -0
  204. package/src/ui/inputs/variants.ts +316 -0
  205. package/src/ui/modal/animated/animations.ts +29 -0
  206. package/src/ui/modal/animated/index.ts +5 -0
  207. package/src/ui/modal/animated/modal-content-animated.tsx +96 -0
  208. package/src/ui/modal/animated/types.ts +23 -0
  209. package/src/ui/modal/index.ts +21 -0
  210. package/src/ui/modal/modal-base.tsx +279 -0
  211. package/src/ui/modal/modal.test.tsx +129 -0
  212. package/src/ui/modal/modal.tsx +8 -0
  213. package/src/ui/modal/types.ts +31 -0
  214. package/src/ui/modal/variants.ts +109 -0
  215. package/src/ui/pagination/index.ts +13 -0
  216. package/src/ui/pagination/pagination.test.tsx +165 -0
  217. package/src/ui/pagination/pagination.tsx +237 -0
  218. package/src/ui/pagination/types.ts +66 -0
  219. package/src/ui/pagination/variants.ts +97 -0
  220. package/src/ui/progress/animated/animations.ts +9 -0
  221. package/src/ui/progress/animated/index.ts +17 -0
  222. package/src/ui/progress/animated/progress-animated.tsx +133 -0
  223. package/src/ui/progress/animated/types.ts +35 -0
  224. package/src/ui/progress/index.ts +10 -0
  225. package/src/ui/progress/progress-base.tsx +151 -0
  226. package/src/ui/progress/progress.test.tsx +84 -0
  227. package/src/ui/progress/progress.tsx +12 -0
  228. package/src/ui/progress/types.ts +33 -0
  229. package/src/ui/progress/variants.ts +105 -0
  230. package/src/ui/select/index.ts +25 -0
  231. package/src/ui/select/select.test.tsx +128 -0
  232. package/src/ui/select/select.tsx +221 -0
  233. package/src/ui/select/types.ts +77 -0
  234. package/src/ui/select/variants.ts +163 -0
  235. package/src/ui/skeleton/animated/animations.ts +15 -0
  236. package/src/ui/skeleton/animated/index.ts +20 -0
  237. package/src/ui/skeleton/animated/skeleton-animated.tsx +119 -0
  238. package/src/ui/skeleton/animated/types.ts +49 -0
  239. package/src/ui/skeleton/index.ts +24 -0
  240. package/src/ui/skeleton/skeleton-base.tsx +288 -0
  241. package/src/ui/skeleton/skeleton.tsx +8 -0
  242. package/src/ui/skeleton/types.ts +31 -0
  243. package/src/ui/skeleton/variants.ts +254 -0
  244. package/src/ui/slider/index.ts +22 -0
  245. package/src/ui/slider/slider.test.tsx +94 -0
  246. package/src/ui/slider/slider.tsx +728 -0
  247. package/src/ui/slider/types.ts +66 -0
  248. package/src/ui/slider/variants.ts +81 -0
  249. package/src/ui/spinner/animated/index.ts +5 -0
  250. package/src/ui/spinner/animated/spinner.test.tsx +41 -0
  251. package/src/ui/spinner/animated/spinner.tsx +143 -0
  252. package/src/ui/spinner/animated/types.ts +11 -0
  253. package/src/ui/spinner/animated/variants.ts +50 -0
  254. package/src/ui/stepper/index.ts +22 -0
  255. package/src/ui/stepper/stepper.test.tsx +183 -0
  256. package/src/ui/stepper/stepper.tsx +172 -0
  257. package/src/ui/stepper/types.ts +32 -0
  258. package/src/ui/stepper/variants.ts +69 -0
  259. package/src/ui/table/animated/animations.ts +9 -0
  260. package/src/ui/table/animated/index.ts +15 -0
  261. package/src/ui/table/animated/table-animated.tsx +15 -0
  262. package/src/ui/table/animated/types.ts +16 -0
  263. package/src/ui/table/index.ts +22 -0
  264. package/src/ui/table/table-base.tsx +197 -0
  265. package/src/ui/table/table.tsx +13 -0
  266. package/src/ui/table/types.ts +47 -0
  267. package/src/ui/table/variants.ts +105 -0
  268. package/src/ui/tabs/animated/animations.ts +48 -0
  269. package/src/ui/tabs/animated/index.ts +8 -0
  270. package/src/ui/tabs/animated/tabs-content-animated.tsx +46 -0
  271. package/src/ui/tabs/animated/types.ts +24 -0
  272. package/src/ui/tabs/index.ts +10 -0
  273. package/src/ui/tabs/tabs-base.tsx +185 -0
  274. package/src/ui/tabs/tabs.test.tsx +53 -0
  275. package/src/ui/tabs/tabs.tsx +2 -0
  276. package/src/ui/tabs/types.ts +88 -0
  277. package/src/ui/tabs/variants.ts +70 -0
  278. package/src/ui/toast/animated/animations.ts +17 -0
  279. package/src/ui/toast/animated/index.ts +9 -0
  280. package/src/ui/toast/animated/toast-animated.tsx +96 -0
  281. package/src/ui/toast/animated/types.ts +13 -0
  282. package/src/ui/toast/index.ts +26 -0
  283. package/src/ui/toast/toast-base.tsx +231 -0
  284. package/src/ui/toast/toast.test.tsx +102 -0
  285. package/src/ui/toast/toast.tsx +13 -0
  286. package/src/ui/toast/types.ts +57 -0
  287. package/src/ui/toast/variants.ts +73 -0
  288. package/src/ui/toggle/animated/animations.ts +9 -0
  289. package/src/ui/toggle/animated/index.ts +7 -0
  290. package/src/ui/toggle/animated/toggle-animated.tsx +76 -0
  291. package/src/ui/toggle/animated/types.ts +13 -0
  292. package/src/ui/toggle/index.ts +5 -0
  293. package/src/ui/toggle/toggle-base.tsx +70 -0
  294. package/src/ui/toggle/toggle.test.tsx +44 -0
  295. package/src/ui/toggle/toggle.tsx +9 -0
  296. package/src/ui/toggle/types.ts +18 -0
  297. package/src/ui/toggle/variants.ts +84 -0
  298. package/src/ui/tooltip/animated/animations.ts +16 -0
  299. package/src/ui/tooltip/animated/index.ts +10 -0
  300. package/src/ui/tooltip/animated/tooltip-content-animated.tsx +47 -0
  301. package/src/ui/tooltip/animated/types.ts +19 -0
  302. package/src/ui/tooltip/index.ts +17 -0
  303. package/src/ui/tooltip/tooltip-base.tsx +152 -0
  304. package/src/ui/tooltip/tooltip.test.tsx +84 -0
  305. package/src/ui/tooltip/tooltip.tsx +8 -0
  306. package/src/ui/tooltip/types.ts +57 -0
  307. package/src/ui/tooltip/variants.ts +61 -0
package/cli/index.mjs ADDED
@@ -0,0 +1,664 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @file Zentauri UI Components CLI — `zentauri-components` / `zentauri-ui`
4
+ *
5
+ * ## Purpose
6
+ *
7
+ * This executable copies **selected** UI component source from the
8
+ * `@zentauri-ui/zentauri-components` package into a consumer app’s tree, so you own the files, paths are configurable, and
9
+ * imports are rewritten to match your `components.json` aliases.
10
+ *
11
+ * ## How it fits together
12
+ *
13
+ * ```
14
+ * [Your app repo]
15
+ * │
16
+ * ├── components.json ← `init` creates this; `add` reads it
17
+ * └── src/...
18
+ *
19
+ * [This package @ packages/components]
20
+ * ├── cli/index.mjs ← this file (entry when run via bin)
21
+ * ├── cli/registry.json ← list of installable component folder names
22
+ * └── src/ui/<name>/ ← source copied by `add`
23
+ * └── src/hooks/<name>/ ← hooks pulled in as dependencies
24
+ * └── src/lib/utils.ts ← template for `cn()` etc. if missing in app
25
+ * ```
26
+ *
27
+ * - **packageRoot**: directory of the published `components` package (parent of
28
+ * `cli/`). Used to read `registry.json`, `src/ui`, `src/hooks`, `src/lib`.
29
+ * - **configDir**: directory containing `components.json` (may differ from
30
+ * `--cwd` when the config is found by walking up from `cwd`).
31
+ *
32
+ * ## Commands (see also `printHelp()`)
33
+ *
34
+ * | Command | Role |
35
+ * |-----------|------|
36
+ * | `init` | Writes `components.json` with default `aliases` + `resolvedPaths` if none exists. |
37
+ * | `add` … | Resolves names via registry, copies UI folders, discovers hooks, copies hook transitive closure, ensures `utils` file. |
38
+ * | `--help` | Prints usage text. |
39
+ * | `--version` | Prints `version` from this package’s `package.json`. |
40
+ *
41
+ * ## Example sessions (terminal)
42
+ *
43
+ * ```bash
44
+ * # From your Next.js app root (after init once):
45
+ * npx zentauri-components init
46
+ * ```
47
+ *
48
+ * Example stdout:
49
+ * ```
50
+ * Wrote /path/to/your-app/components.json
51
+ * ```
52
+ *
53
+ * ```bash
54
+ * npx zentauri-components add button
55
+ * ```
56
+ *
57
+ * Example stdout (paths depend on your `resolvedPaths`):
58
+ * ```
59
+ * Created src/lib/utils.ts
60
+ * Adding button…
61
+ * Adding hook useMediaQuery…
62
+ * Done.
63
+ * ```
64
+ *
65
+ * ```bash
66
+ * npx zentauri-components add accordion buttons inputs
67
+ * ```
68
+ *
69
+ * Multiple components in one run: each line `Adding <name>…`, then hooks, then `Done.`
70
+ *
71
+ * ## Import rewriting
72
+ *
73
+ * TypeScript/JSX files are passed through `rewrite-imports.mjs` so internal
74
+ * package paths become the consumer’s `@/…` (or whatever you set in
75
+ * `components.json`). Non-code assets are copied byte-for-byte.
76
+ *
77
+ * ## Exit codes
78
+ *
79
+ * - Sets `process.exitCode = 1` on validation errors, missing config, unknown
80
+ * command, or refused `init` overwrite. Successful runs leave default exit 0.
81
+ */
82
+
83
+ import { existsSync, readFileSync } from "node:fs";
84
+ import {
85
+ readFile,
86
+ writeFile,
87
+ mkdir,
88
+ copyFile,
89
+ readdir,
90
+ } from "node:fs/promises";
91
+ import { dirname, join, relative } from "node:path";
92
+ import { fileURLToPath } from "node:url";
93
+ import { parseArgs } from "node:util";
94
+
95
+ import {
96
+ extractSiblingHookImports,
97
+ rewriteImports,
98
+ } from "./rewrite-imports.mjs";
99
+
100
+ /** Directory containing this script (`…/packages/components/cli`). */
101
+ const __dirname = dirname(fileURLToPath(import.meta.url));
102
+
103
+ /**
104
+ * Root of the components package (`…/packages/components`), i.e. parent of `cli/`.
105
+ * All reads from `src/ui`, `src/hooks`, `registry.json`, and `package.json` are
106
+ * relative to this path — not the consumer’s project root.
107
+ */
108
+ const packageRoot = join(__dirname, "..");
109
+
110
+ /**
111
+ * Loads the static registry of component folder names and optional aliases.
112
+ *
113
+ * @returns {object} Parsed `cli/registry.json` — expect at least
114
+ * `{ components: string[], nameAliases?: Record<string, string> }`.
115
+ *
116
+ * @example
117
+ * // registry.json (conceptual)
118
+ * // { "components": ["buttons", "inputs"], "nameAliases": { "btn": "buttons" } }
119
+ */
120
+ function loadRegistry() {
121
+ const path = join(packageRoot, "cli", "registry.json");
122
+ return JSON.parse(readFileSync(path, "utf8"));
123
+ }
124
+
125
+ /**
126
+ * Prints human-readable help to **stdout** (not stderr), for `-h` / `--help`.
127
+ * Side effect only; no return value.
128
+ *
129
+ * @example
130
+ * // User runs: zentauri-components --help
131
+ * // Terminal shows multi-line Usage / Options / examples (see string below).
132
+ */
133
+ function printHelp() {
134
+ console.log(`Zentauri UI — copy component source into your app (shadcn-style)
135
+
136
+ Usage:
137
+ zentauri-components init [options] Create components.json with defaults
138
+ zentauri-components add <names...> Copy one or more components (and deps)
139
+
140
+ (The zentauri-ui binary name works the same if your PATH exposes it.)
141
+
142
+ Options:
143
+ --cwd <dir> Working directory (default: process.cwd())
144
+ -h, --help Show help
145
+ -v, --version Show package version
146
+
147
+ Local (no npm publish): cd to your app root, then run Node with a path to this CLI (yours may differ):
148
+ node ../../packages/components/cli/index.mjs init
149
+ node ../../packages/components/cli/index.mjs add button
150
+ Only copies that component’s folder (here: ui/buttons) and creates lib/utils.ts if missing—not the whole monorepo.
151
+
152
+ Published package:
153
+ npx @zentauri-ui/zentauri-components init
154
+ npx @zentauri-ui/zentauri-components add accordion buttons inputs
155
+
156
+ If npx does not pick the right binary:
157
+ npx --yes --package=@zentauri-ui/zentauri-components zentauri-components init
158
+ npx --yes --package=@zentauri-ui/zentauri-components zentauri-ui init
159
+ `);
160
+ }
161
+
162
+ /**
163
+ * Whether a path segment should be skipped when copying into a consumer app.
164
+ * Test files are not shipped so apps do not inherit package test suites.
165
+ *
166
+ * @param {string} name — file name or relative path (e.g. `Button.test.tsx`)
167
+ * @returns {boolean}
168
+ *
169
+ * @example
170
+ * isTestFile("Button.spec.tsx"); // true
171
+ * @example
172
+ * isTestFile("index.ts"); // false
173
+ * @example
174
+ * isTestFile("foo.test.utils.ts"); // true (substring ".test.")
175
+ */
176
+ function isTestFile(name) {
177
+ return /\.(?:test|spec)\.(?:tsx?|jsx?)$/.test(name) || name.includes(".test.");
178
+ }
179
+
180
+ /**
181
+ * Recursively lists every **file** path under `dir` (directories are traversed,
182
+ * not included in the result).
183
+ *
184
+ * @param {string} dir — absolute path to a directory under the package
185
+ * @returns {Promise<string[]>} — flat list of absolute file paths
186
+ *
187
+ * @example
188
+ * // Given src/ui/buttons/index.ts and src/ui/buttons/Button.tsx
189
+ * // await walkFiles("/pkg/src/ui/buttons")
190
+ * // => ["/pkg/src/ui/buttons/Button.tsx", "/pkg/src/ui/buttons/index.ts"]
191
+ * // (order depends on filesystem; not sorted)
192
+ */
193
+ async function walkFiles(dir) {
194
+ const entries = await readdir(dir, { withFileTypes: true });
195
+ const out = [];
196
+ for (const e of entries) {
197
+ const p = join(dir, e.name);
198
+ if (e.isDirectory()) {
199
+ out.push(...(await walkFiles(p)));
200
+ } else {
201
+ out.push(p);
202
+ }
203
+ }
204
+ return out;
205
+ }
206
+
207
+ /**
208
+ * Walks upward from `startDir` toward the filesystem root until `components.json`
209
+ * exists, or returns `undefined` if none found (e.g. user forgot `init`).
210
+ *
211
+ * @param {string} startDir — typically `process.cwd()` or `--cwd` resolved path
212
+ * @returns {string | undefined} — absolute path to `components.json` if found
213
+ *
214
+ * @example
215
+ * // startDir = "/app/apps/web", file at "/app/components.json"
216
+ * // => "/app/components.json"
217
+ * @example
218
+ * // No components.json in startDir or any parent
219
+ * // => undefined
220
+ */
221
+ async function findComponentsJson(startDir) {
222
+ let d = startDir;
223
+ for (;;) {
224
+ const p = join(d, "components.json");
225
+ if (existsSync(p)) {
226
+ return p;
227
+ }
228
+ const parent = dirname(d);
229
+ if (parent === d) {
230
+ return undefined;
231
+ }
232
+ d = parent;
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Default shape written by `init`. Consumers may edit paths to match their
238
+ * bundler/tsconfig path aliases (`@/…`).
239
+ *
240
+ * @returns {{ aliases: Record<string, string>, resolvedPaths: Record<string, string> }}
241
+ *
242
+ * @example
243
+ * // JSON written to disk (pretty-printed):
244
+ * // {
245
+ * // "aliases": {
246
+ * // "ui": "@/components/ui",
247
+ * // "utils": "@/lib/utils",
248
+ * // "hooks": "@/hooks"
249
+ * // },
250
+ * // "resolvedPaths": {
251
+ * // "ui": "src/components/ui",
252
+ * // "utils": "src/lib/utils.ts",
253
+ * // "hooks": "src/hooks"
254
+ * // }
255
+ * // }
256
+ */
257
+ function defaultConfig() {
258
+ return {
259
+ aliases: {
260
+ ui: "@/components/ui",
261
+ utils: "@/lib/utils",
262
+ hooks: "@/hooks",
263
+ },
264
+ resolvedPaths: {
265
+ ui: "src/components/ui",
266
+ utils: "src/lib/utils.ts",
267
+ hooks: "src/hooks",
268
+ },
269
+ };
270
+ }
271
+
272
+ /**
273
+ * Ensures `add` has everything it needs to compute destination paths and rewrite
274
+ * imports. Throws a single clear error if the config is incomplete.
275
+ *
276
+ * @param {object} cfg — parsed `components.json`
277
+ * @throws {Error} if any of `aliases.{utils,hooks,ui}` or
278
+ * `resolvedPaths.{ui,utils,hooks}` is missing
279
+ *
280
+ * @example
281
+ * // validateConfig({ aliases: {}, resolvedPaths: {} });
282
+ * // throws:
283
+ * // Error: components.json must define aliases.utils, aliases.hooks, and aliases.ui
284
+ */
285
+ function validateConfig(cfg) {
286
+ if (!cfg.aliases?.utils || !cfg.aliases?.hooks || !cfg.aliases?.ui) {
287
+ throw new Error(
288
+ "components.json must define aliases.utils, aliases.hooks, and aliases.ui",
289
+ );
290
+ }
291
+ if (!cfg.resolvedPaths?.ui || !cfg.resolvedPaths?.utils || !cfg.resolvedPaths?.hooks) {
292
+ throw new Error(
293
+ "components.json must define resolvedPaths.ui, resolvedPaths.utils, and resolvedPaths.hooks",
294
+ );
295
+ }
296
+ }
297
+
298
+ /**
299
+ * Maps CLI input (any casing, optional registry alias) to a canonical folder name
300
+ * under `src/ui/<name>` in the package.
301
+ *
302
+ * Resolution order:
303
+ * 1. Exact key in `registry.nameAliases`
304
+ * 2. Case-insensitive key match in `nameAliases`
305
+ * 3. Exact match in `registry.components`
306
+ * 4. Case-insensitive match in `registry.components`
307
+ * 5. Otherwise throws
308
+ *
309
+ * @param {string} input — argv token from user, e.g. `"Btn"` or `"buttons"`
310
+ * @param {object} registry — from `loadRegistry()`
311
+ * @returns {string} — canonical component directory name
312
+ * @throws {Error} when the name is unknown
313
+ *
314
+ * @example
315
+ * // registry.components = ["buttons"], nameAliases = { "btn": "buttons" }
316
+ * resolveComponentName("btn", registry); // "buttons"
317
+ * resolveComponentName("BUTTONS", registry); // "buttons"
318
+ * @example
319
+ * // resolveComponentName("typo", registry);
320
+ * // throws: Unknown component "typo". Valid names include: ...
321
+ */
322
+ function resolveComponentName(input, registry) {
323
+ const aliases = registry.nameAliases ?? {};
324
+ if (aliases[input]) {
325
+ return aliases[input];
326
+ }
327
+ const lower = input.toLowerCase();
328
+ const aliasKey = Object.keys(aliases).find((k) => k.toLowerCase() === lower);
329
+ if (aliasKey) {
330
+ return aliases[aliasKey];
331
+ }
332
+ if (registry.components.includes(input)) {
333
+ return input;
334
+ }
335
+ const found = registry.components.find((c) => c.toLowerCase() === lower);
336
+ if (found) {
337
+ return found;
338
+ }
339
+ throw new Error(
340
+ `Unknown component "${input}". Valid names include: ${registry.components.join(", ")}`,
341
+ );
342
+ }
343
+
344
+ /**
345
+ * Starting from hook folder names discovered in UI code, expands the set to
346
+ * include every hook that those hooks import from **sibling** hook packages
347
+ * (parsed via `extractSiblingHookImports`). Ensures `add` copies a consistent
348
+ * dependency graph, not only direct imports.
349
+ *
350
+ * @param {string} packageRoot — components package root
351
+ * @param {string[]} seedHooks — e.g. `["useToggle", "useMediaQuery"]`
352
+ * @returns {Promise<string[]>} — deduplicated list of hook folder names
353
+ *
354
+ * @example
355
+ * // seedHooks = ["useA"]; useA imports useB; useB imports useC
356
+ * // => ["useA", "useB", "useC"] (order depends on BFS/stack pop order)
357
+ */
358
+ async function collectHookTransitiveClosure(packageRoot, seedHooks) {
359
+ const closure = new Set(seedHooks);
360
+ const queue = [...seedHooks];
361
+ while (queue.length > 0) {
362
+ const h = queue.pop();
363
+ const hookDir = join(packageRoot, "src", "hooks", h);
364
+ if (!existsSync(hookDir)) {
365
+ continue;
366
+ }
367
+ const files = await walkFiles(hookDir);
368
+ for (const file of files) {
369
+ if (!/\.(tsx?|jsx?)$/.test(file)) {
370
+ continue;
371
+ }
372
+ const source = await readFile(file, "utf8");
373
+ for (const dep of extractSiblingHookImports(source)) {
374
+ if (!closure.has(dep)) {
375
+ closure.add(dep);
376
+ queue.push(dep);
377
+ }
378
+ }
379
+ }
380
+ }
381
+ return [...closure];
382
+ }
383
+
384
+ /**
385
+ * Copies `packageRoot/src/ui/<componentName>` into
386
+ * `<configDir>/<resolvedPaths.ui>/<componentName>`, skipping tests, rewriting
387
+ * imports in TS/JS files, and collecting hook folder names referenced by those
388
+ * files for later copying.
389
+ *
390
+ * @param {string} componentName — resolved registry name (directory under `src/ui`)
391
+ * @param {object} config — validated `components.json`
392
+ * @param {string} configDir — dirname(components.json)
393
+ * @param {string} packageRoot — package containing source
394
+ * @returns {Promise<string[]>} — hook names used by copied UI files (not yet transitive)
395
+ * @throws {Error} if source folder is missing in the package
396
+ *
397
+ * @example
398
+ * // After copy, consumer might have:
399
+ * // src/components/ui/buttons/Button.tsx
400
+ * // with imports pointing at @/lib/utils, @/hooks/useX, etc.
401
+ */
402
+ async function copyUiComponent(
403
+ componentName,
404
+ config,
405
+ configDir,
406
+ packageRoot,
407
+ ) {
408
+ const srcRoot = join(packageRoot, "src", "ui", componentName);
409
+ if (!existsSync(srcRoot)) {
410
+ throw new Error(`Missing package source: ${relative(packageRoot, srcRoot)}`);
411
+ }
412
+ const destRoot = join(configDir, config.resolvedPaths.ui, componentName);
413
+ const files = await walkFiles(srcRoot);
414
+ const usedHooks = new Set();
415
+
416
+ for (const absSrc of files) {
417
+ const rel = relative(srcRoot, absSrc);
418
+ if (isTestFile(rel)) {
419
+ continue;
420
+ }
421
+ const absDest = join(destRoot, rel);
422
+ await mkdir(dirname(absDest), { recursive: true });
423
+ if (/\.(tsx?|jsx?)$/.test(absSrc)) {
424
+ const raw = await readFile(absSrc, "utf8");
425
+ const { code, usedHooks: uh } = rewriteImports(raw, {
426
+ utilsAlias: config.aliases.utils,
427
+ hooksAlias: config.aliases.hooks,
428
+ uiAlias: config.aliases.ui,
429
+ });
430
+ for (const h of uh) {
431
+ usedHooks.add(h);
432
+ }
433
+ await writeFile(absDest, code, "utf8");
434
+ } else {
435
+ await copyFile(absSrc, absDest);
436
+ }
437
+ }
438
+ return [...usedHooks];
439
+ }
440
+
441
+ /**
442
+ * Same traversal rules as {@link copyUiComponent}, but for `src/hooks/<hookName>`.
443
+ * Used after UI copy so shared hooks land under the consumer’s `resolvedPaths.hooks`.
444
+ *
445
+ * @param {string} hookName — folder name under `src/hooks`
446
+ *
447
+ * @example
448
+ * // Source: package src/hooks/useToggle/useToggle.ts
449
+ * // Dest: <configDir>/src/hooks/useToggle/useToggle.ts
450
+ */
451
+ async function copyHookFolder(hookName, config, configDir, packageRoot) {
452
+ const srcRoot = join(packageRoot, "src", "hooks", hookName);
453
+ if (!existsSync(srcRoot)) {
454
+ throw new Error(`Missing hook source: hooks/${hookName}`);
455
+ }
456
+ const destRoot = join(configDir, config.resolvedPaths.hooks, hookName);
457
+ const files = await walkFiles(srcRoot);
458
+ for (const absSrc of files) {
459
+ const rel = relative(srcRoot, absSrc);
460
+ if (isTestFile(rel)) {
461
+ continue;
462
+ }
463
+ const absDest = join(destRoot, rel);
464
+ await mkdir(dirname(absDest), { recursive: true });
465
+ if (/\.(tsx?|jsx?)$/.test(absSrc)) {
466
+ const raw = await readFile(absSrc, "utf8");
467
+ const { code } = rewriteImports(raw, {
468
+ utilsAlias: config.aliases.utils,
469
+ hooksAlias: config.aliases.hooks,
470
+ uiAlias: config.aliases.ui,
471
+ });
472
+ await writeFile(absDest, code, "utf8");
473
+ } else {
474
+ await copyFile(absSrc, absDest);
475
+ }
476
+ }
477
+ }
478
+
479
+ /**
480
+ * If the target utils file from config does not exist, copies the package default
481
+ * `src/lib/utils.ts` (with import paths rewritten) and logs creation.
482
+ *
483
+ * @example
484
+ * // First `add` in a fresh app prints:
485
+ * // Created src/lib/utils.ts
486
+ * // Second `add` prints nothing here (file already exists).
487
+ */
488
+ async function ensureUtilsFile(config, configDir, packageRoot) {
489
+ const dest = join(configDir, config.resolvedPaths.utils);
490
+ if (existsSync(dest)) {
491
+ return;
492
+ }
493
+ await mkdir(dirname(dest), { recursive: true });
494
+ const src = join(packageRoot, "src", "lib", "utils.ts");
495
+ const raw = await readFile(src, "utf8");
496
+ const { code } = rewriteImports(raw, {
497
+ utilsAlias: config.aliases.utils,
498
+ hooksAlias: config.aliases.hooks,
499
+ uiAlias: config.aliases.ui,
500
+ });
501
+ await writeFile(dest, code, "utf8");
502
+ console.log(`Created ${relative(configDir, dest)}`);
503
+ }
504
+
505
+ /**
506
+ * `zentauri-components init` — writes default `components.json` next to `--cwd`.
507
+ *
508
+ * @param {string} cwd — resolved working directory
509
+ *
510
+ * @example
511
+ * // Success stdout:
512
+ * // Wrote /Users/me/my-app/components.json
513
+ *
514
+ * @example
515
+ * // If file exists — stderr + exitCode 1, no overwrite:
516
+ * // Refusing to overwrite existing /Users/me/my-app/components.json
517
+ */
518
+ async function cmdInit(cwd) {
519
+ const target = join(cwd, "components.json");
520
+ if (existsSync(target)) {
521
+ console.error(`Refusing to overwrite existing ${target}`);
522
+ process.exitCode = 1;
523
+ return;
524
+ }
525
+ const body = `${JSON.stringify(defaultConfig(), null, 2)}\n`;
526
+ await writeFile(target, body, "utf8");
527
+ console.log(`Wrote ${target}`);
528
+ }
529
+
530
+ /**
531
+ * `zentauri-components add a b c` — full pipeline: find config, validate, utils,
532
+ * copy each UI component, compute hook closure, copy each hook, finish.
533
+ *
534
+ * @param {string[]} names — positional args after `add`
535
+ * @param {string} cwd — resolved working directory (starting point for config search)
536
+ *
537
+ * @example
538
+ * // Typical stdout:
539
+ * // Created src/lib/utils.ts
540
+ * // Adding buttons…
541
+ * // Adding inputs…
542
+ * // Adding hook useMediaQuery…
543
+ * // Done.
544
+ *
545
+ * @example
546
+ * // No components.json in cwd or parents — stderr:
547
+ * // No components.json found. Run: zentauri-components init (or: zentauri-ui init)
548
+ */
549
+ async function cmdAdd(names, cwd) {
550
+ const configPath = await findComponentsJson(cwd);
551
+ if (!configPath) {
552
+ console.error(
553
+ "No components.json found. Run: zentauri-components init (or: zentauri-ui init)",
554
+ );
555
+ process.exitCode = 1;
556
+ return;
557
+ }
558
+ const configDir = dirname(configPath);
559
+ const config = JSON.parse(await readFile(configPath, "utf8"));
560
+ validateConfig(config);
561
+
562
+ const registry = loadRegistry();
563
+ const resolvedNames = names.map((n) => resolveComponentName(n, registry));
564
+
565
+ await ensureUtilsFile(config, configDir, packageRoot);
566
+
567
+ const allHooks = new Set();
568
+ for (const name of resolvedNames) {
569
+ console.log(`Adding ${name}…`);
570
+ const uh = await copyUiComponent(name, config, configDir, packageRoot);
571
+ for (const h of uh) {
572
+ allHooks.add(h);
573
+ }
574
+ }
575
+
576
+ const finalHooks = await collectHookTransitiveClosure(
577
+ packageRoot,
578
+ [...allHooks],
579
+ );
580
+
581
+ for (const h of finalHooks) {
582
+ console.log(`Adding hook ${h}…`);
583
+ await copyHookFolder(h, config, configDir, packageRoot);
584
+ }
585
+
586
+ console.log("Done.");
587
+ }
588
+
589
+ /**
590
+ * Parses `process.argv` with `node:util.parseArgs`, dispatches `init` / `add`,
591
+ * or prints help / version. Sets `process.exitCode` for recoverable CLI errors
592
+ * instead of always calling `process.exit()` (except unhandled rejections
593
+ * caught in the outer `.catch`).
594
+ *
595
+ * @example
596
+ * // argv: ["node", "index.mjs", "add", "button"]
597
+ * // => cmdAdd(["button"], cwd)
598
+ *
599
+ * @example
600
+ * // argv: ["node", "index.mjs"]
601
+ * // => printHelp(), exitCode 1
602
+ *
603
+ * @example
604
+ * // argv: ["node", "index.mjs", "-v"]
605
+ * // => "1.2.3" (whatever version is in package.json)
606
+ */
607
+ async function main() {
608
+ const { values, positionals } = parseArgs({
609
+ args: process.argv.slice(2),
610
+ allowPositionals: true,
611
+ options: {
612
+ help: { type: "boolean", short: "h" },
613
+ version: { type: "boolean", short: "v" },
614
+ cwd: { type: "string" },
615
+ },
616
+ });
617
+
618
+ if (values.help) {
619
+ printHelp();
620
+ return;
621
+ }
622
+ if (values.version) {
623
+ const pkg = JSON.parse(
624
+ readFileSync(join(packageRoot, "package.json"), "utf8"),
625
+ );
626
+ console.log(pkg.version);
627
+ return;
628
+ }
629
+
630
+ const cwd = values.cwd ? join(process.cwd(), values.cwd) : process.cwd();
631
+ const cmd = positionals[0];
632
+ const rest = positionals.slice(1);
633
+
634
+ if (!cmd) {
635
+ printHelp();
636
+ process.exitCode = 1;
637
+ return;
638
+ }
639
+
640
+ if (cmd === "init") {
641
+ await cmdInit(cwd);
642
+ return;
643
+ }
644
+ if (cmd === "add") {
645
+ if (rest.length === 0) {
646
+ console.error(
647
+ "Usage: zentauri-components add <component> [more...] (or: zentauri-ui add …)",
648
+ );
649
+ process.exitCode = 1;
650
+ return;
651
+ }
652
+ await cmdAdd(rest, cwd);
653
+ return;
654
+ }
655
+
656
+ console.error(`Unknown command: ${cmd}`);
657
+ printHelp();
658
+ process.exitCode = 1;
659
+ }
660
+
661
+ main().catch((err) => {
662
+ console.error(err instanceof Error ? err.message : err);
663
+ process.exitCode = 1;
664
+ });
@@ -0,0 +1,36 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "description": "Addable UI component folder names under src/ui. Generated by scripts/generate-registry.mjs.",
4
+ "components": [
5
+ "accordion",
6
+ "alert",
7
+ "avatar",
8
+ "badge",
9
+ "breadcrumb",
10
+ "buttons",
11
+ "card",
12
+ "divider",
13
+ "drawer",
14
+ "dropdown",
15
+ "empty-state",
16
+ "file-upload",
17
+ "inputs",
18
+ "modal",
19
+ "pagination",
20
+ "progress",
21
+ "select",
22
+ "skeleton",
23
+ "slider",
24
+ "spinner",
25
+ "stepper",
26
+ "table",
27
+ "tabs",
28
+ "toast",
29
+ "toggle",
30
+ "tooltip"
31
+ ],
32
+ "nameAliases": {
33
+ "button": "buttons",
34
+ "input": "inputs"
35
+ }
36
+ }