@carefully-built/cli 0.1.0 → 0.1.2

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 (212) hide show
  1. package/README.md +148 -7
  2. package/dist/index.mjs +71 -11
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +4 -3
  5. package/registry/ui/avatar/manifest.json +33 -0
  6. package/registry/ui/avatar/primitives/avatar.tsx +64 -0
  7. package/registry/ui/avatar/utils/cn.ts +6 -0
  8. package/registry/ui/button/manifest.json +24 -5
  9. package/registry/ui/button/utils/cn.ts +6 -0
  10. package/registry/ui/calendar/manifest.json +35 -0
  11. package/registry/ui/calendar/primitives/button.tsx +89 -0
  12. package/registry/ui/calendar/primitives/calendar.tsx +68 -0
  13. package/registry/ui/calendar/utils/cn.ts +6 -0
  14. package/registry/ui/card/manifest.json +36 -0
  15. package/registry/ui/card/primitives/card.tsx +80 -0
  16. package/registry/ui/card/utils/cn.ts +6 -0
  17. package/registry/ui/chip/manifest.json +36 -0
  18. package/registry/ui/chip/primitives/chip-utils.ts +10 -0
  19. package/registry/ui/chip/primitives/chip.tsx +74 -0
  20. package/registry/ui/chip/utils/cn.ts +6 -0
  21. package/registry/ui/chip-utils/manifest.json +33 -0
  22. package/registry/ui/chip-utils/primitives/chip-utils.ts +10 -0
  23. package/registry/ui/chip-utils/utils/cn.ts +6 -0
  24. package/registry/ui/date-display/manifest.json +33 -0
  25. package/registry/ui/date-display/utils/cn.ts +6 -0
  26. package/registry/ui/date-display/utils/date-display.ts +61 -0
  27. package/registry/ui/dialog/manifest.json +43 -0
  28. package/registry/ui/dialog/primitives/button.tsx +89 -0
  29. package/registry/ui/dialog/primitives/dialog.tsx +147 -0
  30. package/registry/ui/dialog/utils/cn.ts +6 -0
  31. package/registry/ui/display-date/manifest.json +36 -0
  32. package/registry/ui/display-date/primitives/display-date.tsx +20 -0
  33. package/registry/ui/display-date/utils/cn.ts +6 -0
  34. package/registry/ui/display-date/utils/date-display.ts +61 -0
  35. package/registry/ui/drawer/manifest.json +37 -0
  36. package/registry/ui/drawer/primitives/drawer.tsx +99 -0
  37. package/registry/ui/drawer/utils/cn.ts +6 -0
  38. package/registry/ui/dropdown-menu/manifest.json +37 -0
  39. package/registry/ui/dropdown-menu/primitives/dropdown-menu.tsx +140 -0
  40. package/registry/ui/dropdown-menu/utils/cn.ts +6 -0
  41. package/registry/ui/empty-state/empty-state/collection-empty-state.ts +29 -0
  42. package/registry/ui/empty-state/empty-state/empty-state-card.tsx +72 -0
  43. package/registry/ui/empty-state/empty-state/index.ts +8 -0
  44. package/registry/ui/empty-state/empty-state/initial-empty-state.tsx +36 -0
  45. package/registry/ui/empty-state/empty-state/no-results-state.tsx +20 -0
  46. package/registry/ui/empty-state/manifest.json +63 -0
  47. package/registry/ui/empty-state/primitives/button.tsx +89 -0
  48. package/registry/ui/empty-state/primitives/card.tsx +80 -0
  49. package/registry/ui/empty-state/utils/cn.ts +6 -0
  50. package/registry/ui/error-page/error-page/error-code.tsx +16 -0
  51. package/registry/ui/error-page/error-page/error-page-content.ts +75 -0
  52. package/registry/ui/error-page/error-page/index.ts +19 -0
  53. package/registry/ui/error-page/error-page/posthog-error-capture.ts +83 -0
  54. package/registry/ui/error-page/error-page/saas-error-page.tsx +146 -0
  55. package/registry/ui/error-page/manifest.json +64 -0
  56. package/registry/ui/error-page/primitives/button.tsx +89 -0
  57. package/registry/ui/error-page/utils/cn.ts +6 -0
  58. package/registry/ui/field-detail-row/manifest.json +32 -0
  59. package/registry/ui/field-detail-row/primitives/field-detail-row.tsx +28 -0
  60. package/registry/ui/field-detail-row/utils/cn.ts +6 -0
  61. package/registry/ui/file-dropzone/manifest.json +35 -0
  62. package/registry/ui/file-dropzone/primitives/button.tsx +89 -0
  63. package/registry/ui/file-dropzone/primitives/file-dropzone.tsx +236 -0
  64. package/registry/ui/file-dropzone/utils/cn.ts +6 -0
  65. package/registry/ui/help-info-button/manifest.json +72 -0
  66. package/registry/ui/help-info-button/overlays/responsive-sheet.footer.tsx +88 -0
  67. package/registry/ui/help-info-button/overlays/responsive-sheet.layouts.tsx +207 -0
  68. package/registry/ui/help-info-button/overlays/responsive-sheet.shortcuts.ts +103 -0
  69. package/registry/ui/help-info-button/overlays/responsive-sheet.tsx +132 -0
  70. package/registry/ui/help-info-button/primitives/button.tsx +89 -0
  71. package/registry/ui/help-info-button/primitives/drawer.tsx +99 -0
  72. package/registry/ui/help-info-button/primitives/help-info-button.tsx +63 -0
  73. package/registry/ui/help-info-button/primitives/keyboard-shortcut-hint.tsx +40 -0
  74. package/registry/ui/help-info-button/primitives/sheet.tsx +103 -0
  75. package/registry/ui/help-info-button/primitives/tooltip.tsx +57 -0
  76. package/registry/ui/help-info-button/utils/cn.ts +6 -0
  77. package/registry/ui/help-info-button/utils/use-media-query.ts +28 -0
  78. package/registry/ui/input/manifest.json +31 -0
  79. package/registry/ui/input/primitives/input.tsx +19 -0
  80. package/registry/ui/input/utils/cn.ts +6 -0
  81. package/registry/ui/keyboard-shortcut-hint/manifest.json +32 -0
  82. package/registry/ui/keyboard-shortcut-hint/primitives/keyboard-shortcut-hint.tsx +40 -0
  83. package/registry/ui/keyboard-shortcut-hint/utils/cn.ts +6 -0
  84. package/registry/ui/label/manifest.json +31 -0
  85. package/registry/ui/label/primitives/label.tsx +21 -0
  86. package/registry/ui/label/utils/cn.ts +6 -0
  87. package/registry/ui/pagination/manifest.json +36 -0
  88. package/registry/ui/pagination/primitives/button.tsx +89 -0
  89. package/registry/ui/pagination/primitives/pagination.tsx +143 -0
  90. package/registry/ui/pagination/utils/cn.ts +6 -0
  91. package/registry/ui/popover/manifest.json +33 -0
  92. package/registry/ui/popover/primitives/popover.tsx +46 -0
  93. package/registry/ui/popover/utils/cn.ts +6 -0
  94. package/registry/ui/responsive-sheet/manifest.json +66 -0
  95. package/registry/ui/responsive-sheet/overlays/responsive-sheet.footer.tsx +88 -0
  96. package/registry/ui/responsive-sheet/overlays/responsive-sheet.layouts.tsx +207 -0
  97. package/registry/ui/responsive-sheet/overlays/responsive-sheet.shortcuts.ts +103 -0
  98. package/registry/ui/responsive-sheet/overlays/responsive-sheet.tsx +132 -0
  99. package/registry/ui/responsive-sheet/primitives/button.tsx +89 -0
  100. package/registry/ui/responsive-sheet/primitives/drawer.tsx +99 -0
  101. package/registry/ui/responsive-sheet/primitives/keyboard-shortcut-hint.tsx +40 -0
  102. package/registry/ui/responsive-sheet/primitives/sheet.tsx +103 -0
  103. package/registry/ui/responsive-sheet/utils/cn.ts +6 -0
  104. package/registry/ui/responsive-sheet/utils/use-media-query.ts +28 -0
  105. package/registry/ui/responsive-sheet.footer/manifest.json +40 -0
  106. package/registry/ui/responsive-sheet.footer/overlays/responsive-sheet.footer.tsx +88 -0
  107. package/registry/ui/responsive-sheet.footer/primitives/button.tsx +89 -0
  108. package/registry/ui/responsive-sheet.footer/primitives/keyboard-shortcut-hint.tsx +40 -0
  109. package/registry/ui/responsive-sheet.footer/utils/cn.ts +6 -0
  110. package/registry/ui/responsive-sheet.shortcuts/manifest.json +34 -0
  111. package/registry/ui/responsive-sheet.shortcuts/overlays/responsive-sheet.shortcuts.ts +103 -0
  112. package/registry/ui/responsive-sheet.shortcuts/utils/cn.ts +6 -0
  113. package/registry/ui/scroll-fade-area/manifest.json +31 -0
  114. package/registry/ui/scroll-fade-area/primitives/scroll-fade-area.tsx +295 -0
  115. package/registry/ui/scroll-fade-area/utils/cn.ts +6 -0
  116. package/registry/ui/search/manifest.json +35 -0
  117. package/registry/ui/search/utils/cn.ts +6 -0
  118. package/registry/ui/search/utils/search.ts +227 -0
  119. package/registry/ui/searchable-select/manifest.json +48 -0
  120. package/registry/ui/searchable-select/primitives/input.tsx +19 -0
  121. package/registry/ui/searchable-select/search/searchable-select-position.ts +95 -0
  122. package/registry/ui/searchable-select/search/searchable-select.tsx +431 -0
  123. package/registry/ui/searchable-select/utils/cn.ts +6 -0
  124. package/registry/ui/searchable-select/utils/search.ts +227 -0
  125. package/registry/ui/searchable-select-position/manifest.json +32 -0
  126. package/registry/ui/searchable-select-position/search/searchable-select-position.ts +95 -0
  127. package/registry/ui/searchable-select-position/utils/cn.ts +6 -0
  128. package/registry/ui/segmented-toggle/manifest.json +41 -0
  129. package/registry/ui/segmented-toggle/primitives/scroll-fade-area.tsx +295 -0
  130. package/registry/ui/segmented-toggle/primitives/segmented-toggle.tsx +106 -0
  131. package/registry/ui/segmented-toggle/primitives/tabs.tsx +97 -0
  132. package/registry/ui/segmented-toggle/utils/cn.ts +6 -0
  133. package/registry/ui/select/manifest.json +37 -0
  134. package/registry/ui/select/primitives/select.tsx +142 -0
  135. package/registry/ui/select/utils/cn.ts +6 -0
  136. package/registry/ui/sheet/manifest.json +39 -0
  137. package/registry/ui/sheet/primitives/button.tsx +89 -0
  138. package/registry/ui/sheet/primitives/sheet.tsx +103 -0
  139. package/registry/ui/sheet/utils/cn.ts +6 -0
  140. package/registry/ui/skeleton/manifest.json +31 -0
  141. package/registry/ui/skeleton/primitives/skeleton.tsx +13 -0
  142. package/registry/ui/skeleton/utils/cn.ts +6 -0
  143. package/registry/ui/smart-table/manifest.json +115 -0
  144. package/registry/ui/smart-table/primitives/button.tsx +89 -0
  145. package/registry/ui/smart-table/primitives/card.tsx +80 -0
  146. package/registry/ui/smart-table/primitives/display-date.tsx +20 -0
  147. package/registry/ui/smart-table/primitives/pagination.tsx +143 -0
  148. package/registry/ui/smart-table/primitives/skeleton.tsx +13 -0
  149. package/registry/ui/smart-table/primitives/table.tsx +92 -0
  150. package/registry/ui/smart-table/primitives/tooltip.tsx +57 -0
  151. package/registry/ui/smart-table/smart-table/DesktopView.tsx +343 -0
  152. package/registry/ui/smart-table/smart-table/MobileView.tsx +170 -0
  153. package/registry/ui/smart-table/smart-table/SmartTable.tsx +85 -0
  154. package/registry/ui/smart-table/smart-table/SmartTableActions.tsx +71 -0
  155. package/registry/ui/smart-table/smart-table/TruncatedContent.tsx +147 -0
  156. package/registry/ui/smart-table/smart-table/index.ts +15 -0
  157. package/registry/ui/smart-table/smart-table/sorting.ts +148 -0
  158. package/registry/ui/smart-table/smart-table/truncated-content.utils.ts +22 -0
  159. package/registry/ui/smart-table/smart-table/types.ts +95 -0
  160. package/registry/ui/smart-table/smart-table/utils.ts +150 -0
  161. package/registry/ui/smart-table/utils/cn.ts +6 -0
  162. package/registry/ui/smart-table/utils/date-display.ts +61 -0
  163. package/registry/ui/smart-table/utils/use-media-query.ts +28 -0
  164. package/registry/ui/switch/manifest.json +31 -0
  165. package/registry/ui/switch/primitives/switch.tsx +31 -0
  166. package/registry/ui/switch/utils/cn.ts +6 -0
  167. package/registry/ui/table/manifest.json +38 -0
  168. package/registry/ui/table/primitives/table.tsx +92 -0
  169. package/registry/ui/table/utils/cn.ts +6 -0
  170. package/registry/ui/table-toolbar/manifest.json +93 -0
  171. package/registry/ui/table-toolbar/overlays/responsive-sheet.footer.tsx +88 -0
  172. package/registry/ui/table-toolbar/overlays/responsive-sheet.layouts.tsx +207 -0
  173. package/registry/ui/table-toolbar/overlays/responsive-sheet.shortcuts.ts +103 -0
  174. package/registry/ui/table-toolbar/overlays/responsive-sheet.tsx +132 -0
  175. package/registry/ui/table-toolbar/primitives/button.tsx +89 -0
  176. package/registry/ui/table-toolbar/primitives/drawer.tsx +99 -0
  177. package/registry/ui/table-toolbar/primitives/input.tsx +19 -0
  178. package/registry/ui/table-toolbar/primitives/keyboard-shortcut-hint.tsx +40 -0
  179. package/registry/ui/table-toolbar/primitives/sheet.tsx +103 -0
  180. package/registry/ui/table-toolbar/search/searchable-select-position.ts +95 -0
  181. package/registry/ui/table-toolbar/search/searchable-select.tsx +431 -0
  182. package/registry/ui/table-toolbar/table-toolbar/index.ts +9 -0
  183. package/registry/ui/table-toolbar/table-toolbar/table-toolbar.tsx +552 -0
  184. package/registry/ui/table-toolbar/utils/cn.ts +6 -0
  185. package/registry/ui/table-toolbar/utils/search.ts +227 -0
  186. package/registry/ui/table-toolbar/utils/use-media-query.ts +28 -0
  187. package/registry/ui/tabs/manifest.json +40 -0
  188. package/registry/ui/tabs/primitives/scroll-fade-area.tsx +295 -0
  189. package/registry/ui/tabs/primitives/tabs.tsx +97 -0
  190. package/registry/ui/tabs/utils/cn.ts +6 -0
  191. package/registry/ui/textarea/manifest.json +31 -0
  192. package/registry/ui/textarea/primitives/textarea.tsx +18 -0
  193. package/registry/ui/textarea/utils/cn.ts +6 -0
  194. package/registry/ui/tooltip/manifest.json +34 -0
  195. package/registry/ui/tooltip/primitives/tooltip.tsx +57 -0
  196. package/registry/ui/tooltip/utils/cn.ts +6 -0
  197. package/registry/ui/use-media-query/manifest.json +32 -0
  198. package/registry/ui/use-media-query/utils/cn.ts +6 -0
  199. package/registry/ui/use-media-query/utils/use-media-query.ts +28 -0
  200. package/registry/ui/user-picker/manifest.json +52 -0
  201. package/registry/ui/user-picker/primitives/avatar.tsx +64 -0
  202. package/registry/ui/user-picker/primitives/button.tsx +89 -0
  203. package/registry/ui/user-picker/primitives/input.tsx +19 -0
  204. package/registry/ui/user-picker/primitives/popover.tsx +46 -0
  205. package/registry/ui/user-picker/primitives/user-picker-utils.ts +113 -0
  206. package/registry/ui/user-picker/primitives/user-picker.tsx +226 -0
  207. package/registry/ui/user-picker/utils/cn.ts +6 -0
  208. package/registry/ui/user-picker-utils/manifest.json +38 -0
  209. package/registry/ui/user-picker-utils/primitives/user-picker-utils.ts +113 -0
  210. package/registry/ui/user-picker-utils/utils/cn.ts +6 -0
  211. package/registry/ui/button/cn.ts +0 -6
  212. /package/registry/ui/button/{button.tsx → primitives/button.tsx} +0 -0
package/README.md CHANGED
@@ -1,23 +1,164 @@
1
1
  # @carefully-built/cli
2
2
 
3
- CLI for adding Carefully Built components to apps as editable source code.
3
+ Carefully Built SaaS Kit is meant to be used in two complementary ways:
4
4
 
5
- ## Usage
5
+ 1. **Managed package imports** from packages such as `@carefully-built/ui` when you want shared, upgradeable components.
6
+ 2. **Editable source ejection** with this CLI when a component needs to become local app code.
7
+
8
+ The CLI does not replace the package imports. It gives you the shadcn-style path for the same Carefully Built components.
9
+
10
+ ## Component Previews
11
+
12
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/smarttable.png" alt="Smart Table" width="360" />
13
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/responsivesheet.png" alt="Responsive Sheet" width="360" />
14
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/kanban.png" alt="Kanban" width="360" />
15
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/charts.png" alt="Charts" width="360" />
16
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/notifications.png" alt="Notifications" width="360" />
17
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/search.png" alt="Search" width="360" />
18
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/calendar.png" alt="Calendar" width="360" />
19
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/maps.png" alt="Maps" width="360" />
20
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/superadmin.png" alt="Superadmin" width="360" />
21
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/widgets.png" alt="Widgets" width="360" />
22
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/file-dropper.png" alt="Files" width="360" />
23
+ <img src="https://raw.githubusercontent.com/AlessandroDodi/carefully-built-saas-kit/main/docs/saas-kit/images/theme-switcher.png" alt="Theme" width="360" />
24
+
25
+ ## Managed Package Imports
26
+
27
+ Use package imports when you want the kit to stay shared and receive fixes through npm updates.
28
+
29
+ ```bash
30
+ bun add @carefully-built/ui
31
+ ```
32
+
33
+ ```tsx
34
+ import { Button, SmartTable, TableToolbar, ResponsiveSheet } from "@carefully-built/ui";
35
+ ```
36
+
37
+ This mode is the default for stable primitives, CRUD surfaces, tables, overlays, hooks, and utilities that should remain consistent across apps.
38
+
39
+ ## Editable Source With The CLI
40
+
41
+ Use the CLI when a component should become local code that you can edit directly.
6
42
 
7
43
  ```bash
8
44
  bunx @carefully-built/cli list
9
45
  bunx @carefully-built/cli add button
46
+ bunx @carefully-built/cli add smart-table
47
+ bunx @carefully-built/cli add responsive-sheet
10
48
  ```
11
49
 
12
- Use `--overwrite` to replace existing files:
50
+ The CLI copies the component source and its local dependency closure into your app. It preserves normal app imports such as `@/components/ui/button` and `@/lib/utils`.
51
+
52
+ It reads common shadcn project conventions:
53
+
54
+ - `components.json` aliases such as `ui: "@/components/ui"` and `utils: "@/lib/utils"`
55
+ - `tsconfig.json` paths such as `@/* -> ./src/*`
56
+
57
+ So a `src` app receives files under `src/components/ui` and `src/lib`; a root-style app receives files under `components/ui` and `lib`.
58
+
59
+ Use `--overwrite` only when replacing local files intentionally:
13
60
 
14
61
  ```bash
15
62
  bunx @carefully-built/cli add button --overwrite
16
63
  ```
17
64
 
18
- The first registry entry copies:
65
+ ## Full Registry
66
+
67
+ Every public UI module exported by `@carefully-built/ui` is available through the CLI registry.
19
68
 
20
- - `components/ui/button.tsx`
21
- - `lib/utils.ts`
69
+ | Registry entry | Managed import | Editable source |
70
+ |---|---|---|
71
+ | `avatar` | `import { Avatar, AvatarFallback, AvatarImage } from "@carefully-built/ui"` | `bunx @carefully-built/cli add avatar` |
72
+ | `button` | `import { Button, ButtonSize, ButtonVariant, buttonVariants } from "@carefully-built/ui"` | `bunx @carefully-built/cli add button` |
73
+ | `calendar` | `import { Calendar } from "@carefully-built/ui"` | `bunx @carefully-built/cli add calendar` |
74
+ | `card` | `import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@carefully-built/ui"` | `bunx @carefully-built/cli add card` |
75
+ | `chip` | `import { Chip, ChipButton } from "@carefully-built/ui"` | `bunx @carefully-built/cli add chip` |
76
+ | `chip-utils` | `import { CHIP_CLASS_NAMES, ChipSize, getChipClassName } from "@carefully-built/ui"` | `bunx @carefully-built/cli add chip-utils` |
77
+ | `date-display` | `import { DateDisplayValue, formatAbsoluteDate, formatDisplayDate } from "@carefully-built/ui"` | `bunx @carefully-built/cli add date-display` |
78
+ | `dialog` | `import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay } from "@carefully-built/ui"` | `bunx @carefully-built/cli add dialog` |
79
+ | `display-date` | `import { DisplayDate } from "@carefully-built/ui"` | `bunx @carefully-built/cli add display-date` |
80
+ | `drawer` | `import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerOverlay, DrawerPortal } from "@carefully-built/ui"` | `bunx @carefully-built/cli add drawer` |
81
+ | `dropdown-menu` | `import { DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator } from "@carefully-built/ui"` | `bunx @carefully-built/cli add dropdown-menu` |
82
+ | `empty-state` | `import { CollectionEmptyState, EmptyStateCard, InitialEmptyState, NoResultsState, ResolveCollectionEmptyStateOptions, resolveCollectionEmptyState } from "@carefully-built/ui"` | `bunx @carefully-built/cli add empty-state` |
83
+ | `error-page` | `import { ErrorCode, ErrorPageContent, ErrorPageKind, PostHogErrorCapturePayload, ResolveErrorPageContentOptions, SaasErrorPage } from "@carefully-built/ui"` | `bunx @carefully-built/cli add error-page` |
84
+ | `field-detail-row` | `import { FieldDetailRow } from "@carefully-built/ui"` | `bunx @carefully-built/cli add field-detail-row` |
85
+ | `file-dropzone` | `import { FileDropzone } from "@carefully-built/ui"` | `bunx @carefully-built/cli add file-dropzone` |
86
+ | `help-info-button` | `import { HelpInfoButton } from "@carefully-built/ui"` | `bunx @carefully-built/cli add help-info-button` |
87
+ | `input` | `import { Input } from "@carefully-built/ui"` | `bunx @carefully-built/cli add input` |
88
+ | `keyboard-shortcut-hint` | `import { KeyboardKeycap, ShortcutModifierKeycap } from "@carefully-built/ui"` | `bunx @carefully-built/cli add keyboard-shortcut-hint` |
89
+ | `label` | `import { Label } from "@carefully-built/ui"` | `bunx @carefully-built/cli add label` |
90
+ | `pagination` | `import { Pagination } from "@carefully-built/ui"` | `bunx @carefully-built/cli add pagination` |
91
+ | `popover` | `import { Popover, PopoverContent, PopoverTrigger } from "@carefully-built/ui"` | `bunx @carefully-built/cli add popover` |
92
+ | `responsive-sheet` | `import { ResponsiveSheet, SheetOutsideInteractionGuard } from "@carefully-built/ui"` | `bunx @carefully-built/cli add responsive-sheet` |
93
+ | `responsive-sheet.footer` | `import { DesktopConfirmShortcutHint, SheetActionFooter } from "@carefully-built/ui"` | `bunx @carefully-built/cli add responsive-sheet.footer` |
94
+ | `responsive-sheet.shortcuts` | `import { getDesktopShortcutModifierLabel, isAllowedConfirmShortcutEvent, useDesktopConfirmShortcut, useDesktopShortcutModifierLabel } from "@carefully-built/ui"` | `bunx @carefully-built/cli add responsive-sheet.shortcuts` |
95
+ | `scroll-fade-area` | `import { ScrollFadeArea } from "@carefully-built/ui"` | `bunx @carefully-built/cli add scroll-fade-area` |
96
+ | `search` | `import { SearchTextPart, buildSearchText, filterAndRankBySearch, rankBySearch, scoreFuzzyMatch } from "@carefully-built/ui"` | `bunx @carefully-built/cli add search` |
97
+ | `searchable-select` | `import { AUTO_SEARCHABLE_SELECT_THRESHOLD, SearchableSelect, SearchableSelectOption, getSearchableSelectPortalContainer, isSearchableSelectPointerInside } from "@carefully-built/ui"` | `bunx @carefully-built/cli add searchable-select` |
98
+ | `searchable-select-position` | `import { SearchableSelectRect, resolveSearchableSelectDropdownPosition } from "@carefully-built/ui"` | `bunx @carefully-built/cli add searchable-select-position` |
99
+ | `segmented-toggle` | `import { SegmentedToggle, SegmentedToggleOption } from "@carefully-built/ui"` | `bunx @carefully-built/cli add segmented-toggle` |
100
+ | `select` | `import { Select, SelectContent, SelectItem, SelectScrollDownButton, SelectScrollUpButton, SelectTrigger } from "@carefully-built/ui"` | `bunx @carefully-built/cli add select` |
101
+ | `sheet` | `import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@carefully-built/ui"` | `bunx @carefully-built/cli add sheet` |
102
+ | `skeleton` | `import { Skeleton } from "@carefully-built/ui"` | `bunx @carefully-built/cli add skeleton` |
103
+ | `smart-table` | `import { ActionHandlers, ActionType, Column, ColumnAlign, PaginationConfig, SmartTable } from "@carefully-built/ui"` | `bunx @carefully-built/cli add smart-table` |
104
+ | `switch` | `import { Switch } from "@carefully-built/ui"` | `bunx @carefully-built/cli add switch` |
105
+ | `table` | `import { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead } from "@carefully-built/ui"` | `bunx @carefully-built/cli add table` |
106
+ | `table-toolbar` | `import { CustomTableToolbarFilter, FilterConfig, FilterDropdown, FilterOption, SearchInput, TableToolbar } from "@carefully-built/ui"` | `bunx @carefully-built/cli add table-toolbar` |
107
+ | `tabs` | `import { Tabs, TabsContent, TabsList, TabsScrollArea, TabsTrigger, tabsListVariants } from "@carefully-built/ui"` | `bunx @carefully-built/cli add tabs` |
108
+ | `textarea` | `import { Textarea } from "@carefully-built/ui"` | `bunx @carefully-built/cli add textarea` |
109
+ | `tooltip` | `import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@carefully-built/ui"` | `bunx @carefully-built/cli add tooltip` |
110
+ | `use-media-query` | `import { useIsMobile, useMediaQuery } from "@carefully-built/ui"` | `bunx @carefully-built/cli add use-media-query` |
111
+ | `user-picker` | `import { UserPicker } from "@carefully-built/ui"` | `bunx @carefully-built/cli add user-picker` |
112
+ | `user-picker-utils` | `import { UserPickerCopy, UserPickerOption, buildUserInitials, filterSelectableUsers, filterUsersBySearch, formatSelectedUserSummary } from "@carefully-built/ui"` | `bunx @carefully-built/cli add user-picker-utils` |
22
113
 
23
- Install the printed dependencies in the target app if they are not already present.
114
+ Some entries export several related components, hooks, helpers, and types from one module. The import column shows the main exported symbols; TypeScript autocomplete will show the full API after installing `@carefully-built/ui`.
115
+
116
+ ## Dependency Notes
117
+
118
+ The CLI prints dependency hints after copying. Depending on the component, your app may need packages such as:
119
+
120
+ - `class-variance-authority`
121
+ - `clsx`
122
+ - `tailwind-merge`
123
+ - `react`
124
+ - `react-dom`
125
+ - `radix-ui`
126
+ - `lucide-react`
127
+ - `react-day-picker`
128
+ - `vaul`
129
+
130
+ Most Carefully Built apps already have these through the shared kit stack.
131
+
132
+ ## Import vs Eject
133
+
134
+ Import from `@carefully-built/ui` when:
135
+
136
+ - you want package updates and bug fixes
137
+ - the component should stay shared across apps
138
+ - product-specific differences can be handled with props, slots, `className`, or `classes`
139
+
140
+ Use `@carefully-built/cli add` when:
141
+
142
+ - the component needs local product behavior
143
+ - design needs to diverge from the shared package
144
+ - you want to own and edit every line in the consuming app
145
+ - the component should follow the app's normal source-control workflow
146
+
147
+ ## Development
148
+
149
+ From the monorepo root:
150
+
151
+ ```bash
152
+ bun install
153
+ bun run --cwd packages/cli registry:build
154
+ bun run --cwd packages/cli test
155
+ bun run --cwd packages/cli typecheck
156
+ bun run --cwd packages/cli build
157
+ ```
158
+
159
+ Before publishing:
160
+
161
+ ```bash
162
+ cd packages/cli
163
+ npm publish --dry-run --access public
164
+ ```
package/dist/index.mjs CHANGED
@@ -1,19 +1,22 @@
1
1
  #!/usr/bin/env node
2
- import { constants, readFileSync } from "node:fs";
3
- import { access, copyFile, mkdir } from "node:fs/promises";
4
- import { dirname, join } from "node:path";
2
+ import { constants, readFileSync, readdirSync } from "node:fs";
3
+ import { access, copyFile, mkdir, readFile } from "node:fs/promises";
4
+ import { dirname, join, normalize } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  //#region src/registry.ts
8
8
  const registryRoot = join(join(dirname(fileURLToPath(import.meta.url)), ".."), "registry");
9
- const componentNames = ["button"];
10
9
  function listRegistryComponents() {
11
- return [...componentNames];
10
+ return readdirSync(join(registryRoot, "ui"), { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => entry.name).sort();
12
11
  }
13
12
  function getRegistryComponent(componentName) {
14
- if (!componentNames.includes(componentName)) return;
15
13
  const manifestPath = join(registryRoot, "ui", componentName, "manifest.json");
16
- const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
14
+ let manifest;
15
+ try {
16
+ manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
17
+ } catch {
18
+ return;
19
+ }
17
20
  return {
18
21
  ...manifest,
19
22
  files: manifest.files.map((file) => ({
@@ -31,17 +34,19 @@ async function addComponent({ componentName, cwd, overwrite }) {
31
34
  const created = [];
32
35
  const overwritten = [];
33
36
  const skipped = [];
37
+ const projectConfig = await readProjectConfig(cwd);
34
38
  for (const file of component.files) {
35
- const targetPath = join(cwd, file.target);
39
+ const target = resolveTargetPath(file.target, projectConfig);
40
+ const targetPath = join(cwd, target);
36
41
  const exists = await fileExists(targetPath);
37
42
  if (exists && !overwrite) {
38
- skipped.push(file.target);
43
+ skipped.push(target);
39
44
  continue;
40
45
  }
41
46
  await mkdir(dirname(targetPath), { recursive: true });
42
47
  await copyFile(file.source, targetPath);
43
- if (exists) overwritten.push(file.target);
44
- else created.push(file.target);
48
+ if (exists) overwritten.push(target);
49
+ else created.push(target);
45
50
  }
46
51
  return {
47
52
  componentName: component.name,
@@ -60,6 +65,61 @@ async function fileExists(path) {
60
65
  return false;
61
66
  }
62
67
  }
68
+ async function readProjectConfig(cwd) {
69
+ const [componentsJson, tsconfigJson] = await Promise.all([readJsonFile(join(cwd, "components.json")), readJsonFile(join(cwd, "tsconfig.json"))]);
70
+ return {
71
+ uiAlias: readString(componentsJson, ["aliases", "ui"]),
72
+ utilsAlias: readString(componentsJson, ["aliases", "utils"]),
73
+ ...readPrimaryTsconfigAlias(tsconfigJson)
74
+ };
75
+ }
76
+ function resolveTargetPath(target, config) {
77
+ if (target === "lib/utils.ts" && config.utilsAlias) return resolveAliasPath(`${config.utilsAlias}.ts`, config);
78
+ if (target.startsWith("components/ui/") && config.uiAlias) {
79
+ const fileName = target.slice(14);
80
+ return resolveAliasPath(`${config.uiAlias}/${fileName}`, config);
81
+ }
82
+ return target;
83
+ }
84
+ function resolveAliasPath(path, config) {
85
+ if (config.aliasPrefix && config.aliasTarget && path.startsWith(config.aliasPrefix)) return cleanPath(path.replace(config.aliasPrefix, config.aliasTarget));
86
+ if (path.startsWith("@/")) return cleanPath(path.slice(2));
87
+ return cleanPath(path);
88
+ }
89
+ function readPrimaryTsconfigAlias(value) {
90
+ const aliasTarget = readArray(readRecord(readRecord(value, "compilerOptions"), "paths"), "@/*").find((entry) => typeof entry === "string");
91
+ if (typeof aliasTarget !== "string") return {};
92
+ return {
93
+ aliasPrefix: "@/",
94
+ aliasTarget: aliasTarget.replace(/\/\*$/, "/")
95
+ };
96
+ }
97
+ async function readJsonFile(path) {
98
+ try {
99
+ return JSON.parse(stripJsonComments(await readFile(path, "utf8")));
100
+ } catch {
101
+ return;
102
+ }
103
+ }
104
+ function stripJsonComments(source) {
105
+ return source.replace(/^\s*\/\/.*$/gm, "");
106
+ }
107
+ function readString(value, path) {
108
+ let current = value;
109
+ for (const key of path) current = readRecord(current, key);
110
+ return typeof current === "string" ? current : void 0;
111
+ }
112
+ function readRecord(value, key) {
113
+ if (!value || typeof value !== "object") return;
114
+ return value[key];
115
+ }
116
+ function readArray(value, key) {
117
+ const array = readRecord(value, key);
118
+ return Array.isArray(array) ? array : [];
119
+ }
120
+ function cleanPath(path) {
121
+ return normalize(path).replace(/^\.\//, "");
122
+ }
63
123
 
64
124
  //#endregion
65
125
  //#region src/index.ts
@@ -1 +1 @@
1
- {"version":3,"file":"index.mjs","names":["created: string[]","overwritten: string[]","skipped: string[]"],"sources":["../src/registry.ts","../src/add.ts","../src/index.ts"],"sourcesContent":["import { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport interface RegistryFile {\n readonly source: string;\n readonly target: string;\n}\n\nexport interface RegistryComponent {\n readonly name: string;\n readonly description: string;\n readonly dependencies: readonly string[];\n readonly peerDependencies: readonly string[];\n readonly files: readonly RegistryFile[];\n}\n\nconst packageRoot = join(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst registryRoot = join(packageRoot, \"registry\");\nconst componentNames = [\"button\"] as const;\n\nexport function listRegistryComponents(): string[] {\n return [...componentNames];\n}\n\nexport function getRegistryComponent(\n componentName: string,\n): RegistryComponent | undefined {\n if (!componentNames.includes(componentName as (typeof componentNames)[number])) {\n return undefined;\n }\n\n const manifestPath = join(registryRoot, \"ui\", componentName, \"manifest.json\");\n const manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as Omit<\n RegistryComponent,\n \"files\"\n > & {\n files: readonly RegistryFile[];\n };\n\n return {\n ...manifest,\n files: manifest.files.map((file) => ({\n ...file,\n source: join(registryRoot, \"ui\", componentName, file.source),\n })),\n };\n}\n","import { constants } from \"node:fs\";\nimport { access, copyFile, mkdir } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\n\nimport { getRegistryComponent } from \"./registry\";\n\nexport interface AddComponentOptions {\n readonly componentName: string;\n readonly cwd: string;\n readonly overwrite: boolean;\n}\n\nexport interface AddComponentResult {\n readonly componentName: string;\n readonly created: string[];\n readonly overwritten: string[];\n readonly skipped: string[];\n readonly dependencies: readonly string[];\n readonly peerDependencies: readonly string[];\n}\n\nexport async function addComponent({\n componentName,\n cwd,\n overwrite,\n}: AddComponentOptions): Promise<AddComponentResult> {\n const component = getRegistryComponent(componentName);\n\n if (!component) {\n throw new Error(`Unknown component \"${componentName}\"`);\n }\n\n const created: string[] = [];\n const overwritten: string[] = [];\n const skipped: string[] = [];\n\n for (const file of component.files) {\n const targetPath = join(cwd, file.target);\n const exists = await fileExists(targetPath);\n\n if (exists && !overwrite) {\n skipped.push(file.target);\n continue;\n }\n\n await mkdir(dirname(targetPath), { recursive: true });\n await copyFile(file.source, targetPath);\n\n if (exists) {\n overwritten.push(file.target);\n } else {\n created.push(file.target);\n }\n }\n\n return {\n componentName: component.name,\n created,\n overwritten,\n skipped,\n dependencies: component.dependencies,\n peerDependencies: component.peerDependencies,\n };\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n","#!/usr/bin/env node\nimport { addComponent } from \"./add\";\nimport { listRegistryComponents } from \"./registry\";\n\ninterface ParsedArgs {\n readonly command?: string;\n readonly componentName?: string;\n readonly overwrite: boolean;\n readonly help: boolean;\n}\n\nexport async function runCli(argv = process.argv.slice(2)): Promise<void> {\n const args = parseArgs(argv);\n\n if (args.help || !args.command) {\n printHelp();\n return;\n }\n\n if (args.command === \"list\") {\n for (const componentName of listRegistryComponents()) {\n console.log(componentName);\n }\n return;\n }\n\n if (args.command === \"add\") {\n if (!args.componentName) {\n throw new Error(\"Missing component name. Example: carefully-built add button\");\n }\n\n const result = await addComponent({\n componentName: args.componentName,\n cwd: process.cwd(),\n overwrite: args.overwrite,\n });\n\n printAddResult(result);\n return;\n }\n\n throw new Error(`Unknown command \"${args.command}\"`);\n}\n\nfunction parseArgs(argv: readonly string[]): ParsedArgs {\n return {\n command: argv[0],\n componentName: argv[1]?.startsWith(\"-\") ? undefined : argv[1],\n overwrite: argv.includes(\"--overwrite\"),\n help: argv.includes(\"--help\") || argv.includes(\"-h\"),\n };\n}\n\nfunction printHelp(): void {\n console.log(`carefully-built\n\nUsage:\n carefully-built list\n carefully-built add <component> [--overwrite]\n\nComponents:\n ${listRegistryComponents().join(\", \")}\n`);\n}\n\nfunction printAddResult(result: Awaited<ReturnType<typeof addComponent>>): void {\n for (const file of result.created) {\n console.log(`created ${file}`);\n }\n for (const file of result.overwritten) {\n console.log(`overwrote ${file}`);\n }\n for (const file of result.skipped) {\n console.log(`skipped ${file}`);\n }\n\n if (result.dependencies.length > 0) {\n console.log(`dependencies: ${result.dependencies.join(\", \")}`);\n }\n if (result.peerDependencies.length > 0) {\n console.log(`peer dependencies: ${result.peerDependencies.join(\", \")}`);\n }\n}\n\nrunCli().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exitCode = 1;\n});\n"],"mappings":";;;;;;;AAkBA,MAAM,eAAe,KADD,KAAK,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC,EAAE,KAAK,EAChC,WAAW;AAClD,MAAM,iBAAiB,CAAC,SAAS;AAEjC,SAAgB,yBAAmC;AACjD,QAAO,CAAC,GAAG,eAAe;;AAG5B,SAAgB,qBACd,eAC+B;AAC/B,KAAI,CAAC,eAAe,SAAS,cAAiD,CAC5E;CAGF,MAAM,eAAe,KAAK,cAAc,MAAM,eAAe,gBAAgB;CAC7E,MAAM,WAAW,KAAK,MAAM,aAAa,cAAc,OAAO,CAAC;AAO/D,QAAO;EACL,GAAG;EACH,OAAO,SAAS,MAAM,KAAK,UAAU;GACnC,GAAG;GACH,QAAQ,KAAK,cAAc,MAAM,eAAe,KAAK,OAAO;GAC7D,EAAE;EACJ;;;;;ACzBH,eAAsB,aAAa,EACjC,eACA,KACA,aACmD;CACnD,MAAM,YAAY,qBAAqB,cAAc;AAErD,KAAI,CAAC,UACH,OAAM,IAAI,MAAM,sBAAsB,cAAc,GAAG;CAGzD,MAAMA,UAAoB,EAAE;CAC5B,MAAMC,cAAwB,EAAE;CAChC,MAAMC,UAAoB,EAAE;AAE5B,MAAK,MAAM,QAAQ,UAAU,OAAO;EAClC,MAAM,aAAa,KAAK,KAAK,KAAK,OAAO;EACzC,MAAM,SAAS,MAAM,WAAW,WAAW;AAE3C,MAAI,UAAU,CAAC,WAAW;AACxB,WAAQ,KAAK,KAAK,OAAO;AACzB;;AAGF,QAAM,MAAM,QAAQ,WAAW,EAAE,EAAE,WAAW,MAAM,CAAC;AACrD,QAAM,SAAS,KAAK,QAAQ,WAAW;AAEvC,MAAI,OACF,aAAY,KAAK,KAAK,OAAO;MAE7B,SAAQ,KAAK,KAAK,OAAO;;AAI7B,QAAO;EACL,eAAe,UAAU;EACzB;EACA;EACA;EACA,cAAc,UAAU;EACxB,kBAAkB,UAAU;EAC7B;;AAGH,eAAe,WAAW,MAAgC;AACxD,KAAI;AACF,QAAM,OAAO,MAAM,UAAU,KAAK;AAClC,SAAO;SACD;AACN,SAAO;;;;;;AC3DX,eAAsB,OAAO,OAAO,QAAQ,KAAK,MAAM,EAAE,EAAiB;CACxE,MAAM,OAAO,UAAU,KAAK;AAE5B,KAAI,KAAK,QAAQ,CAAC,KAAK,SAAS;AAC9B,aAAW;AACX;;AAGF,KAAI,KAAK,YAAY,QAAQ;AAC3B,OAAK,MAAM,iBAAiB,wBAAwB,CAClD,SAAQ,IAAI,cAAc;AAE5B;;AAGF,KAAI,KAAK,YAAY,OAAO;AAC1B,MAAI,CAAC,KAAK,cACR,OAAM,IAAI,MAAM,8DAA8D;AAShF,iBANe,MAAM,aAAa;GAChC,eAAe,KAAK;GACpB,KAAK,QAAQ,KAAK;GAClB,WAAW,KAAK;GACjB,CAAC,CAEoB;AACtB;;AAGF,OAAM,IAAI,MAAM,oBAAoB,KAAK,QAAQ,GAAG;;AAGtD,SAAS,UAAU,MAAqC;AACtD,QAAO;EACL,SAAS,KAAK;EACd,eAAe,KAAK,IAAI,WAAW,IAAI,GAAG,SAAY,KAAK;EAC3D,WAAW,KAAK,SAAS,cAAc;EACvC,MAAM,KAAK,SAAS,SAAS,IAAI,KAAK,SAAS,KAAK;EACrD;;AAGH,SAAS,YAAkB;AACzB,SAAQ,IAAI;;;;;;;IAOV,wBAAwB,CAAC,KAAK,KAAK,CAAC;EACtC;;AAGF,SAAS,eAAe,QAAwD;AAC9E,MAAK,MAAM,QAAQ,OAAO,QACxB,SAAQ,IAAI,WAAW,OAAO;AAEhC,MAAK,MAAM,QAAQ,OAAO,YACxB,SAAQ,IAAI,aAAa,OAAO;AAElC,MAAK,MAAM,QAAQ,OAAO,QACxB,SAAQ,IAAI,WAAW,OAAO;AAGhC,KAAI,OAAO,aAAa,SAAS,EAC/B,SAAQ,IAAI,iBAAiB,OAAO,aAAa,KAAK,KAAK,GAAG;AAEhE,KAAI,OAAO,iBAAiB,SAAS,EACnC,SAAQ,IAAI,sBAAsB,OAAO,iBAAiB,KAAK,KAAK,GAAG;;AAI3E,QAAQ,CAAC,OAAO,UAAmB;CACjC,MAAM,UAAU,iBAAiB,QAAQ,MAAM,UAAU,OAAO,MAAM;AACtE,SAAQ,MAAM,QAAQ;AACtB,SAAQ,WAAW;EACnB"}
1
+ {"version":3,"file":"index.mjs","names":["manifest: Omit<RegistryComponent, \"files\"> & {\n files: readonly RegistryFile[];\n }","created: string[]","overwritten: string[]","skipped: string[]"],"sources":["../src/registry.ts","../src/add.ts","../src/index.ts"],"sourcesContent":["import { readdirSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\ninterface RegistryFile {\n readonly source: string;\n readonly target: string;\n}\n\nexport interface RegistryComponent {\n readonly name: string;\n readonly description: string;\n readonly importPath?: string;\n readonly exports?: readonly string[];\n readonly dependencies: readonly string[];\n readonly peerDependencies: readonly string[];\n readonly files: readonly RegistryFile[];\n}\n\nconst packageRoot = join(dirname(fileURLToPath(import.meta.url)), \"..\");\nconst registryRoot = join(packageRoot, \"registry\");\n\nexport function listRegistryComponents(): string[] {\n return readdirSync(join(registryRoot, \"ui\"), { withFileTypes: true })\n .filter((entry) => entry.isDirectory())\n .map((entry) => entry.name)\n .sort();\n}\n\nexport function getRegistryComponent(\n componentName: string,\n): RegistryComponent | undefined {\n const manifestPath = join(registryRoot, \"ui\", componentName, \"manifest.json\");\n\n let manifest: Omit<RegistryComponent, \"files\"> & {\n files: readonly RegistryFile[];\n };\n\n try {\n manifest = JSON.parse(readFileSync(manifestPath, \"utf8\")) as typeof manifest;\n } catch {\n return undefined;\n }\n\n return {\n ...manifest,\n files: manifest.files.map((file) => ({\n ...file,\n source: join(registryRoot, \"ui\", componentName, file.source),\n })),\n };\n}\n","import { constants } from \"node:fs\";\nimport { access, copyFile, mkdir, readFile } from \"node:fs/promises\";\nimport { dirname, join, normalize } from \"node:path\";\n\nimport { getRegistryComponent } from \"./registry\";\n\nexport interface AddComponentOptions {\n readonly componentName: string;\n readonly cwd: string;\n readonly overwrite: boolean;\n}\n\nexport interface AddComponentResult {\n readonly componentName: string;\n readonly created: string[];\n readonly overwritten: string[];\n readonly skipped: string[];\n readonly dependencies: readonly string[];\n readonly peerDependencies: readonly string[];\n}\n\nexport async function addComponent({\n componentName,\n cwd,\n overwrite,\n}: AddComponentOptions): Promise<AddComponentResult> {\n const component = getRegistryComponent(componentName);\n\n if (!component) {\n throw new Error(`Unknown component \"${componentName}\"`);\n }\n\n const created: string[] = [];\n const overwritten: string[] = [];\n const skipped: string[] = [];\n const projectConfig = await readProjectConfig(cwd);\n\n for (const file of component.files) {\n const target = resolveTargetPath(file.target, projectConfig);\n const targetPath = join(cwd, target);\n const exists = await fileExists(targetPath);\n\n if (exists && !overwrite) {\n skipped.push(target);\n continue;\n }\n\n await mkdir(dirname(targetPath), { recursive: true });\n await copyFile(file.source, targetPath);\n\n if (exists) {\n overwritten.push(target);\n } else {\n created.push(target);\n }\n }\n\n return {\n componentName: component.name,\n created,\n overwritten,\n skipped,\n dependencies: component.dependencies,\n peerDependencies: component.peerDependencies,\n };\n}\n\nasync function fileExists(path: string): Promise<boolean> {\n try {\n await access(path, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\ninterface ProjectConfig {\n readonly uiAlias?: string;\n readonly utilsAlias?: string;\n readonly aliasPrefix?: string;\n readonly aliasTarget?: string;\n}\n\nasync function readProjectConfig(cwd: string): Promise<ProjectConfig> {\n const [componentsJson, tsconfigJson] = await Promise.all([\n readJsonFile(join(cwd, \"components.json\")),\n readJsonFile(join(cwd, \"tsconfig.json\")),\n ]);\n\n return {\n uiAlias: readString(componentsJson, [\"aliases\", \"ui\"]),\n utilsAlias: readString(componentsJson, [\"aliases\", \"utils\"]),\n ...readPrimaryTsconfigAlias(tsconfigJson),\n };\n}\n\nfunction resolveTargetPath(target: string, config: ProjectConfig): string {\n if (target === \"lib/utils.ts\" && config.utilsAlias) {\n return resolveAliasPath(`${config.utilsAlias}.ts`, config);\n }\n\n if (target.startsWith(\"components/ui/\") && config.uiAlias) {\n const fileName = target.slice(\"components/ui/\".length);\n return resolveAliasPath(`${config.uiAlias}/${fileName}`, config);\n }\n\n return target;\n}\n\nfunction resolveAliasPath(path: string, config: ProjectConfig): string {\n if (\n config.aliasPrefix &&\n config.aliasTarget &&\n path.startsWith(config.aliasPrefix)\n ) {\n return cleanPath(path.replace(config.aliasPrefix, config.aliasTarget));\n }\n\n if (path.startsWith(\"@/\")) {\n return cleanPath(path.slice(2));\n }\n\n return cleanPath(path);\n}\n\nfunction readPrimaryTsconfigAlias(value: unknown): Pick<\n ProjectConfig,\n \"aliasPrefix\" | \"aliasTarget\"\n> {\n const paths = readRecord(readRecord(value, \"compilerOptions\"), \"paths\");\n const aliasTargets = readArray(paths, \"@/*\");\n const aliasTarget = aliasTargets.find((entry) => typeof entry === \"string\");\n\n if (typeof aliasTarget !== \"string\") {\n return {};\n }\n\n return {\n aliasPrefix: \"@/\",\n aliasTarget: aliasTarget.replace(/\\/\\*$/, \"/\"),\n };\n}\n\nasync function readJsonFile(path: string): Promise<unknown> {\n try {\n return JSON.parse(stripJsonComments(await readFile(path, \"utf8\")));\n } catch {\n return undefined;\n }\n}\n\nfunction stripJsonComments(source: string): string {\n return source.replace(/^\\s*\\/\\/.*$/gm, \"\");\n}\n\nfunction readString(value: unknown, path: readonly string[]): string | undefined {\n let current = value;\n\n for (const key of path) {\n current = readRecord(current, key);\n }\n\n return typeof current === \"string\" ? current : undefined;\n}\n\nfunction readRecord(value: unknown, key: string): unknown {\n if (!value || typeof value !== \"object\") {\n return undefined;\n }\n\n return (value as Record<string, unknown>)[key];\n}\n\nfunction readArray(value: unknown, key: string): unknown[] {\n const array = readRecord(value, key);\n return Array.isArray(array) ? array : [];\n}\n\nfunction cleanPath(path: string): string {\n return normalize(path).replace(/^\\.\\//, \"\");\n}\n","#!/usr/bin/env node\nimport { addComponent } from \"./add\";\nimport { listRegistryComponents } from \"./registry\";\n\ninterface ParsedArgs {\n readonly command?: string;\n readonly componentName?: string;\n readonly overwrite: boolean;\n readonly help: boolean;\n}\n\nexport async function runCli(argv = process.argv.slice(2)): Promise<void> {\n const args = parseArgs(argv);\n\n if (args.help || !args.command) {\n printHelp();\n return;\n }\n\n if (args.command === \"list\") {\n for (const componentName of listRegistryComponents()) {\n console.log(componentName);\n }\n return;\n }\n\n if (args.command === \"add\") {\n if (!args.componentName) {\n throw new Error(\"Missing component name. Example: carefully-built add button\");\n }\n\n const result = await addComponent({\n componentName: args.componentName,\n cwd: process.cwd(),\n overwrite: args.overwrite,\n });\n\n printAddResult(result);\n return;\n }\n\n throw new Error(`Unknown command \"${args.command}\"`);\n}\n\nfunction parseArgs(argv: readonly string[]): ParsedArgs {\n return {\n command: argv[0],\n componentName: argv[1]?.startsWith(\"-\") ? undefined : argv[1],\n overwrite: argv.includes(\"--overwrite\"),\n help: argv.includes(\"--help\") || argv.includes(\"-h\"),\n };\n}\n\nfunction printHelp(): void {\n console.log(`carefully-built\n\nUsage:\n carefully-built list\n carefully-built add <component> [--overwrite]\n\nComponents:\n ${listRegistryComponents().join(\", \")}\n`);\n}\n\nfunction printAddResult(result: Awaited<ReturnType<typeof addComponent>>): void {\n for (const file of result.created) {\n console.log(`created ${file}`);\n }\n for (const file of result.overwritten) {\n console.log(`overwrote ${file}`);\n }\n for (const file of result.skipped) {\n console.log(`skipped ${file}`);\n }\n\n if (result.dependencies.length > 0) {\n console.log(`dependencies: ${result.dependencies.join(\", \")}`);\n }\n if (result.peerDependencies.length > 0) {\n console.log(`peer dependencies: ${result.peerDependencies.join(\", \")}`);\n }\n}\n\nrunCli().catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(message);\n process.exitCode = 1;\n});\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@carefully-built/cli",
3
- "version": "0.1.0",
4
- "description": "CLI for adding Carefully Built components to apps.",
3
+ "version": "0.1.2",
4
+ "description": "Add Carefully Built SaaS components to apps as editable source, with package imports when you want managed upgrades.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Alessandro Dodi",
@@ -39,7 +39,8 @@
39
39
  },
40
40
  "scripts": {
41
41
  "build": "tsdown src/index.ts --format esm --dts",
42
- "prepublishOnly": "bun run typecheck && bun run test && bun run build",
42
+ "prepublishOnly": "bun run registry:build && bun run typecheck && bun run test && bun run build",
43
+ "registry:build": "node scripts/build-registry.mjs",
43
44
  "test": "bun test",
44
45
  "typecheck": "tsc --noEmit"
45
46
  },
@@ -0,0 +1,33 @@
1
+ {
2
+ "name": "avatar",
3
+ "description": "Editable source registry entry for avatar.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "Avatar",
7
+ "AvatarFallback",
8
+ "AvatarImage"
9
+ ],
10
+ "dependencies": [
11
+ "class-variance-authority",
12
+ "clsx",
13
+ "tailwind-merge"
14
+ ],
15
+ "peerDependencies": [
16
+ "react",
17
+ "react-dom",
18
+ "radix-ui",
19
+ "lucide-react",
20
+ "react-day-picker",
21
+ "vaul"
22
+ ],
23
+ "files": [
24
+ {
25
+ "source": "primitives/avatar.tsx",
26
+ "target": "components/ui/avatar.tsx"
27
+ },
28
+ {
29
+ "source": "utils/cn.ts",
30
+ "target": "lib/utils.ts"
31
+ }
32
+ ]
33
+ }
@@ -0,0 +1,64 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { Avatar as AvatarPrimitive } from "radix-ui"
5
+
6
+ import { cn } from "@/lib/utils"
7
+
8
+ function Avatar({
9
+ className,
10
+ size = "default",
11
+ ...props
12
+ }: React.ComponentProps<typeof AvatarPrimitive.Root> & {
13
+ size?: "default" | "sm" | "lg"
14
+ }) {
15
+ return (
16
+ <AvatarPrimitive.Root
17
+ data-slot="avatar"
18
+ data-size={size}
19
+ className={cn(
20
+ "size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten",
21
+ className
22
+ )}
23
+ {...props}
24
+ />
25
+ )
26
+ }
27
+
28
+ function AvatarImage({
29
+ className,
30
+ ...props
31
+ }: React.ComponentProps<typeof AvatarPrimitive.Image>) {
32
+ return (
33
+ <AvatarPrimitive.Image
34
+ data-slot="avatar-image"
35
+ className={cn(
36
+ "rounded-full aspect-square size-full object-cover",
37
+ className
38
+ )}
39
+ {...props}
40
+ />
41
+ )
42
+ }
43
+
44
+ function AvatarFallback({
45
+ className,
46
+ ...props
47
+ }: React.ComponentProps<typeof AvatarPrimitive.Fallback>) {
48
+ return (
49
+ <AvatarPrimitive.Fallback
50
+ data-slot="avatar-fallback"
51
+ className={cn(
52
+ "bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs",
53
+ className
54
+ )}
55
+ {...props}
56
+ />
57
+ )
58
+ }
59
+
60
+ export {
61
+ Avatar,
62
+ AvatarImage,
63
+ AvatarFallback
64
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -1,15 +1,34 @@
1
1
  {
2
2
  "name": "button",
3
- "description": "Shared button primitive with Carefully Built variants and sizes.",
4
- "dependencies": ["class-variance-authority", "clsx", "tailwind-merge"],
5
- "peerDependencies": ["react", "radix-ui"],
3
+ "description": "Editable source registry entry for button.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "Button",
7
+ "ButtonProps",
8
+ "ButtonSize",
9
+ "ButtonVariant",
10
+ "buttonVariants"
11
+ ],
12
+ "dependencies": [
13
+ "class-variance-authority",
14
+ "clsx",
15
+ "tailwind-merge"
16
+ ],
17
+ "peerDependencies": [
18
+ "react",
19
+ "react-dom",
20
+ "radix-ui",
21
+ "lucide-react",
22
+ "react-day-picker",
23
+ "vaul"
24
+ ],
6
25
  "files": [
7
26
  {
8
- "source": "button.tsx",
27
+ "source": "primitives/button.tsx",
9
28
  "target": "components/ui/button.tsx"
10
29
  },
11
30
  {
12
- "source": "cn.ts",
31
+ "source": "utils/cn.ts",
13
32
  "target": "lib/utils.ts"
14
33
  }
15
34
  ]
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "calendar",
3
+ "description": "Editable source registry entry for calendar.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "Calendar"
7
+ ],
8
+ "dependencies": [
9
+ "class-variance-authority",
10
+ "clsx",
11
+ "tailwind-merge"
12
+ ],
13
+ "peerDependencies": [
14
+ "react",
15
+ "react-dom",
16
+ "radix-ui",
17
+ "lucide-react",
18
+ "react-day-picker",
19
+ "vaul"
20
+ ],
21
+ "files": [
22
+ {
23
+ "source": "primitives/button.tsx",
24
+ "target": "components/ui/button.tsx"
25
+ },
26
+ {
27
+ "source": "primitives/calendar.tsx",
28
+ "target": "components/ui/calendar.tsx"
29
+ },
30
+ {
31
+ "source": "utils/cn.ts",
32
+ "target": "lib/utils.ts"
33
+ }
34
+ ]
35
+ }
@@ -0,0 +1,89 @@
1
+ import * as React from "react";
2
+ import { cva } from "class-variance-authority";
3
+ import { Slot } from "radix-ui";
4
+
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const buttonVariants = cva(
8
+ "group/button inline-flex shrink-0 cursor-pointer items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
9
+ {
10
+ variants: {
11
+ variant: {
12
+ default:
13
+ "bg-primary text-primary-foreground hover:brightness-90 [a]:hover:bg-primary/80",
14
+ outline:
15
+ "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
16
+ secondary:
17
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground",
18
+ ghost:
19
+ "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50",
20
+ destructive:
21
+ "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40",
22
+ link: "text-primary underline-offset-4 hover:underline",
23
+ },
24
+ size: {
25
+ default:
26
+ "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
27
+ xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
28
+ sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
29
+ lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2",
30
+ icon: "size-8",
31
+ "icon-xs":
32
+ "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
33
+ "icon-sm":
34
+ "size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg",
35
+ "icon-lg": "size-9",
36
+ },
37
+ },
38
+ defaultVariants: {
39
+ variant: "default",
40
+ size: "default",
41
+ },
42
+ },
43
+ );
44
+
45
+ type ButtonVariant =
46
+ | "default"
47
+ | "outline"
48
+ | "secondary"
49
+ | "ghost"
50
+ | "destructive"
51
+ | "link";
52
+ type ButtonSize =
53
+ | "default"
54
+ | "xs"
55
+ | "sm"
56
+ | "lg"
57
+ | "icon"
58
+ | "icon-xs"
59
+ | "icon-sm"
60
+ | "icon-lg";
61
+
62
+ interface ButtonProps extends React.ComponentProps<"button"> {
63
+ readonly asChild?: boolean;
64
+ readonly size?: ButtonSize;
65
+ readonly variant?: ButtonVariant;
66
+ }
67
+
68
+ function Button({
69
+ className,
70
+ variant = "default",
71
+ size = "default",
72
+ asChild = false,
73
+ ...props
74
+ }: ButtonProps) {
75
+ const Comp = asChild ? Slot.Root : "button";
76
+
77
+ return (
78
+ <Comp
79
+ data-slot="button"
80
+ data-variant={variant}
81
+ data-size={size}
82
+ className={cn(buttonVariants({ variant, size, className }))}
83
+ {...props}
84
+ />
85
+ );
86
+ }
87
+
88
+ export { Button, buttonVariants };
89
+ export type { ButtonProps, ButtonSize, ButtonVariant };