@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
@@ -0,0 +1,226 @@
1
+ 'use client';
2
+
3
+ import { Check, ChevronDown, Search } from 'lucide-react';
4
+ import { useMemo, useState } from 'react';
5
+
6
+ import {
7
+ buildUserInitials,
8
+ filterUsersBySearch,
9
+ filterSelectableUsers,
10
+ formatUserDisplayName,
11
+ formatSelectedUserSummary,
12
+ toggleUserSelection,
13
+ type UserPickerCopy,
14
+ type UserPickerOption,
15
+ } from '@/components/ui/user-picker-utils';
16
+
17
+ import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
18
+ import { Button } from '@/components/ui/button';
19
+ import { Input } from '@/components/ui/input';
20
+ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
21
+ import { cn } from '@/lib/utils';
22
+
23
+ interface BaseUserPickerProps {
24
+ readonly options: readonly UserPickerOption[];
25
+ readonly placeholder?: string;
26
+ readonly searchPlaceholder?: string;
27
+ readonly emptyMessage?: string;
28
+ readonly disabled?: boolean;
29
+ readonly className?: string;
30
+ readonly triggerClassName?: string;
31
+ readonly copy?: UserPickerCopy;
32
+ }
33
+
34
+ interface SingleUserPickerProps extends BaseUserPickerProps {
35
+ readonly mode: 'single';
36
+ readonly value?: string;
37
+ readonly onValueChange: (value: string | undefined) => void;
38
+ }
39
+
40
+ interface MultipleUserPickerProps extends BaseUserPickerProps {
41
+ readonly mode: 'multiple';
42
+ readonly value: readonly string[];
43
+ readonly onValueChange: (value: string[]) => void;
44
+ }
45
+
46
+ export type UserPickerProps = SingleUserPickerProps | MultipleUserPickerProps;
47
+
48
+ function UserAvatar({ user, className, fallbackName }: {
49
+ readonly user: UserPickerOption;
50
+ readonly className?: string;
51
+ readonly fallbackName?: string;
52
+ }): React.ReactElement {
53
+ return (
54
+ <Avatar size="sm" className={className}>
55
+ {user.imageUrl ? <AvatarImage src={user.imageUrl} alt={formatUserDisplayName(user, fallbackName)} /> : null}
56
+ <AvatarFallback>{buildUserInitials(user, fallbackName)}</AvatarFallback>
57
+ </Avatar>
58
+ );
59
+ }
60
+
61
+ function UserRow({
62
+ user,
63
+ fallbackName,
64
+ }: {
65
+ readonly user: UserPickerOption;
66
+ readonly fallbackName?: string;
67
+ }): React.ReactElement {
68
+ return (
69
+ <span className="flex min-w-0 items-center gap-2">
70
+ <UserAvatar user={user} fallbackName={fallbackName} />
71
+ <span className="min-w-0">
72
+ <span className="block truncate font-medium">{formatUserDisplayName(user, fallbackName)}</span>
73
+ {user.email ? (
74
+ <span className="block truncate text-xs text-muted-foreground">{user.email}</span>
75
+ ) : null}
76
+ </span>
77
+ </span>
78
+ );
79
+ }
80
+
81
+ function MultipleUserValue({ selectedUsers, placeholder, copy }: {
82
+ readonly selectedUsers: readonly UserPickerOption[];
83
+ readonly placeholder: string;
84
+ readonly copy: UserPickerCopy;
85
+ }): React.ReactElement {
86
+ if (selectedUsers.length === 0) {
87
+ return <span className="text-muted-foreground">{placeholder}</span>;
88
+ }
89
+
90
+ return (
91
+ <span className="flex min-w-0 items-center gap-2">
92
+ <span className="flex -space-x-2">
93
+ {selectedUsers.slice(0, 3).map((user) => (
94
+ <UserAvatar
95
+ key={user.value}
96
+ user={user}
97
+ className="ring-2 ring-background"
98
+ fallbackName={copy.fallbackName}
99
+ />
100
+ ))}
101
+ </span>
102
+ <span className="truncate">
103
+ {copy.formatSelectedCount
104
+ ? copy.formatSelectedCount(selectedUsers.length)
105
+ : formatSelectedUserSummary(selectedUsers, copy)}
106
+ </span>
107
+ </span>
108
+ );
109
+ }
110
+
111
+ export function UserPicker(props: UserPickerProps): React.ReactElement {
112
+ const {
113
+ options,
114
+ copy = {},
115
+ placeholder = props.mode === 'single'
116
+ ? copy.singlePlaceholder ?? 'Select person'
117
+ : copy.multiplePlaceholder ?? 'Select people',
118
+ searchPlaceholder = copy.searchPlaceholder ?? 'Search people...',
119
+ emptyMessage = copy.emptyMessage ?? 'No people available',
120
+ disabled = false,
121
+ className,
122
+ triggerClassName,
123
+ } = props;
124
+ const [open, setOpen] = useState(false);
125
+ const [search, setSearch] = useState('');
126
+ const singleValue = props.mode === 'single' ? props.value : undefined;
127
+ const multipleValue = props.mode === 'multiple' ? props.value : undefined;
128
+ const selectedValues = useMemo(
129
+ () => (props.mode === 'single' ? (singleValue ? [singleValue] : []) : [...(multipleValue ?? [])]),
130
+ [multipleValue, props.mode, singleValue],
131
+ );
132
+ const selectedValueSet = useMemo(() => new Set(selectedValues), [selectedValues]);
133
+ const selectableUsers = useMemo(
134
+ () => filterSelectableUsers(options, selectedValues),
135
+ [options, selectedValues],
136
+ );
137
+ const filteredUsers = useMemo(
138
+ () => filterUsersBySearch(selectableUsers, search, copy.fallbackName),
139
+ [copy.fallbackName, search, selectableUsers],
140
+ );
141
+ const selectedUsers = selectedValues
142
+ .map((selectedValue) => options.find((option) => option.value === selectedValue))
143
+ .filter((user): user is UserPickerOption => Boolean(user));
144
+ const selectedUser = selectedUsers[0];
145
+
146
+ function selectUser(userValue: string): void {
147
+ if (props.mode === 'single') {
148
+ props.onValueChange(userValue);
149
+ setOpen(false);
150
+ return;
151
+ }
152
+
153
+ props.onValueChange(toggleUserSelection(props.value, userValue));
154
+ }
155
+
156
+ return (
157
+ <Popover open={open} onOpenChange={setOpen}>
158
+ <div className={cn('relative', className)}>
159
+ <PopoverTrigger asChild>
160
+ <Button
161
+ type="button"
162
+ variant="outline"
163
+ disabled={disabled}
164
+ className={cn(
165
+ 'h-9 w-full justify-between gap-2 px-2 text-left font-normal',
166
+ selectedValues.length === 0 && 'text-muted-foreground',
167
+ triggerClassName,
168
+ )}
169
+ >
170
+ <span className="min-w-0 flex-1">
171
+ {props.mode === 'single' ? (
172
+ selectedUser ? <UserRow user={selectedUser} fallbackName={copy.fallbackName} /> : <span>{placeholder}</span>
173
+ ) : (
174
+ <MultipleUserValue selectedUsers={selectedUsers} placeholder={placeholder} copy={copy} />
175
+ )}
176
+ </span>
177
+ <ChevronDown className="size-4 shrink-0 text-muted-foreground" />
178
+ </Button>
179
+ </PopoverTrigger>
180
+
181
+ <PopoverContent
182
+ align="start"
183
+ data-searchable-select-content=""
184
+ className="w-[var(--radix-popover-trigger-width)] min-w-72 p-2"
185
+ >
186
+ <div className="relative">
187
+ <Search className="absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
188
+ <Input
189
+ value={search}
190
+ onChange={(event) => {
191
+ setSearch(event.target.value);
192
+ }}
193
+ placeholder={searchPlaceholder}
194
+ className="h-8 pl-8"
195
+ />
196
+ </div>
197
+ <div className="max-h-72 space-y-1 overflow-y-auto">
198
+ {filteredUsers.map((user) => {
199
+ const selected = selectedValueSet.has(user.value);
200
+
201
+ return (
202
+ <button
203
+ key={user.value}
204
+ type="button"
205
+ className={cn(
206
+ 'flex w-full items-center justify-between gap-3 rounded-md px-2 py-2 text-left text-sm transition-colors hover:bg-accent hover:text-accent-foreground',
207
+ selected && 'bg-muted/70',
208
+ )}
209
+ onClick={() => {
210
+ selectUser(user.value);
211
+ }}
212
+ >
213
+ <UserRow user={user} fallbackName={copy.fallbackName} />
214
+ {selected ? <Check className="size-4 shrink-0" /> : null}
215
+ </button>
216
+ );
217
+ })}
218
+ {filteredUsers.length === 0 ? (
219
+ <p className="px-2 py-4 text-sm text-muted-foreground">{emptyMessage}</p>
220
+ ) : null}
221
+ </div>
222
+ </PopoverContent>
223
+ </div>
224
+ </Popover>
225
+ );
226
+ }
@@ -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,38 @@
1
+ {
2
+ "name": "user-picker-utils",
3
+ "description": "Editable source registry entry for user-picker-utils.",
4
+ "importPath": "@carefully-built/ui",
5
+ "exports": [
6
+ "UserPickerCopy",
7
+ "UserPickerOption",
8
+ "buildUserInitials",
9
+ "filterSelectableUsers",
10
+ "filterUsersBySearch",
11
+ "formatSelectedUserSummary",
12
+ "formatUserDisplayName",
13
+ "toggleUserSelection"
14
+ ],
15
+ "dependencies": [
16
+ "class-variance-authority",
17
+ "clsx",
18
+ "tailwind-merge"
19
+ ],
20
+ "peerDependencies": [
21
+ "react",
22
+ "react-dom",
23
+ "radix-ui",
24
+ "lucide-react",
25
+ "react-day-picker",
26
+ "vaul"
27
+ ],
28
+ "files": [
29
+ {
30
+ "source": "primitives/user-picker-utils.ts",
31
+ "target": "components/ui/user-picker-utils.ts"
32
+ },
33
+ {
34
+ "source": "utils/cn.ts",
35
+ "target": "lib/utils.ts"
36
+ }
37
+ ]
38
+ }
@@ -0,0 +1,113 @@
1
+ export interface UserPickerOption {
2
+ readonly value: string;
3
+ readonly label?: string | null;
4
+ readonly email?: string | null;
5
+ readonly imageUrl?: string | null;
6
+ readonly archived?: boolean;
7
+ readonly searchText?: string;
8
+ }
9
+
10
+ export interface UserPickerCopy {
11
+ readonly fallbackName?: string;
12
+ readonly singlePlaceholder?: string;
13
+ readonly multiplePlaceholder?: string;
14
+ readonly searchPlaceholder?: string;
15
+ readonly emptyMessage?: string;
16
+ readonly selectedSingleLabel?: string;
17
+ readonly selectedPluralLabel?: string;
18
+ readonly formatSelectedCount?: (count: number) => string;
19
+ }
20
+
21
+ export function formatUserDisplayName(
22
+ user: UserPickerOption,
23
+ fallbackName = 'User',
24
+ ): string {
25
+ const label = user.label?.trim();
26
+ if (label) {
27
+ return label;
28
+ }
29
+
30
+ const email = user.email?.trim();
31
+ if (email) {
32
+ return email;
33
+ }
34
+
35
+ return fallbackName;
36
+ }
37
+
38
+ export function formatSelectedUserSummary(
39
+ selectedUsers: readonly UserPickerOption[],
40
+ copy: UserPickerCopy = {},
41
+ ): string {
42
+ const selectedNames = selectedUsers
43
+ .slice(0, 2)
44
+ .map((user) => formatUserDisplayName(user, copy.fallbackName));
45
+ const remainingCount = selectedUsers.length - selectedNames.length;
46
+
47
+ if (remainingCount <= 0) {
48
+ return selectedNames.join(', ');
49
+ }
50
+
51
+ const remainingLabel =
52
+ remainingCount === 1
53
+ ? copy.selectedSingleLabel ?? 'person'
54
+ : copy.selectedPluralLabel ?? 'people';
55
+
56
+ return `${selectedNames.join(', ')} +${String(remainingCount)} ${remainingLabel}`;
57
+ }
58
+
59
+ export function buildUserInitials(user: UserPickerOption, fallbackName?: string): string {
60
+ const displayName = formatUserDisplayName(user, fallbackName);
61
+ const nameParts = displayName
62
+ .split(/\s+/)
63
+ .filter(Boolean)
64
+ .slice(0, 2);
65
+
66
+ if (nameParts.length > 1) {
67
+ return nameParts.map((part) => part[0]?.toUpperCase()).join('');
68
+ }
69
+
70
+ return displayName[0]?.toUpperCase() ?? 'U';
71
+ }
72
+
73
+ export function filterSelectableUsers(
74
+ users: readonly UserPickerOption[],
75
+ selectedValues: readonly string[],
76
+ ): UserPickerOption[] {
77
+ const selectedValueSet = new Set(selectedValues);
78
+
79
+ return users.filter((user) => !user.archived || selectedValueSet.has(user.value));
80
+ }
81
+
82
+ export function filterUsersBySearch(
83
+ users: readonly UserPickerOption[],
84
+ search: string,
85
+ fallbackName?: string,
86
+ ): UserPickerOption[] {
87
+ const normalizedSearch = search.trim().toLowerCase();
88
+
89
+ if (!normalizedSearch) {
90
+ return [...users];
91
+ }
92
+
93
+ return users.filter((user) => {
94
+ const searchableText = [
95
+ formatUserDisplayName(user, fallbackName),
96
+ user.email,
97
+ user.searchText,
98
+ ].filter(Boolean).join(' ').toLowerCase();
99
+
100
+ return searchableText.includes(normalizedSearch);
101
+ });
102
+ }
103
+
104
+ export function toggleUserSelection(
105
+ selectedValues: readonly string[],
106
+ toggledValue: string,
107
+ ): string[] {
108
+ if (selectedValues.includes(toggledValue)) {
109
+ return selectedValues.filter((value) => value !== toggledValue);
110
+ }
111
+
112
+ return [...selectedValues, toggledValue];
113
+ }
@@ -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,6 +0,0 @@
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
- }