@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.
- package/README.md +148 -7
- package/dist/index.mjs +71 -11
- package/dist/index.mjs.map +1 -1
- package/package.json +4 -3
- package/registry/ui/avatar/manifest.json +33 -0
- package/registry/ui/avatar/primitives/avatar.tsx +64 -0
- package/registry/ui/avatar/utils/cn.ts +6 -0
- package/registry/ui/button/manifest.json +24 -5
- package/registry/ui/button/utils/cn.ts +6 -0
- package/registry/ui/calendar/manifest.json +35 -0
- package/registry/ui/calendar/primitives/button.tsx +89 -0
- package/registry/ui/calendar/primitives/calendar.tsx +68 -0
- package/registry/ui/calendar/utils/cn.ts +6 -0
- package/registry/ui/card/manifest.json +36 -0
- package/registry/ui/card/primitives/card.tsx +80 -0
- package/registry/ui/card/utils/cn.ts +6 -0
- package/registry/ui/chip/manifest.json +36 -0
- package/registry/ui/chip/primitives/chip-utils.ts +10 -0
- package/registry/ui/chip/primitives/chip.tsx +74 -0
- package/registry/ui/chip/utils/cn.ts +6 -0
- package/registry/ui/chip-utils/manifest.json +33 -0
- package/registry/ui/chip-utils/primitives/chip-utils.ts +10 -0
- package/registry/ui/chip-utils/utils/cn.ts +6 -0
- package/registry/ui/date-display/manifest.json +33 -0
- package/registry/ui/date-display/utils/cn.ts +6 -0
- package/registry/ui/date-display/utils/date-display.ts +61 -0
- package/registry/ui/dialog/manifest.json +43 -0
- package/registry/ui/dialog/primitives/button.tsx +89 -0
- package/registry/ui/dialog/primitives/dialog.tsx +147 -0
- package/registry/ui/dialog/utils/cn.ts +6 -0
- package/registry/ui/display-date/manifest.json +36 -0
- package/registry/ui/display-date/primitives/display-date.tsx +20 -0
- package/registry/ui/display-date/utils/cn.ts +6 -0
- package/registry/ui/display-date/utils/date-display.ts +61 -0
- package/registry/ui/drawer/manifest.json +37 -0
- package/registry/ui/drawer/primitives/drawer.tsx +99 -0
- package/registry/ui/drawer/utils/cn.ts +6 -0
- package/registry/ui/dropdown-menu/manifest.json +37 -0
- package/registry/ui/dropdown-menu/primitives/dropdown-menu.tsx +140 -0
- package/registry/ui/dropdown-menu/utils/cn.ts +6 -0
- package/registry/ui/empty-state/empty-state/collection-empty-state.ts +29 -0
- package/registry/ui/empty-state/empty-state/empty-state-card.tsx +72 -0
- package/registry/ui/empty-state/empty-state/index.ts +8 -0
- package/registry/ui/empty-state/empty-state/initial-empty-state.tsx +36 -0
- package/registry/ui/empty-state/empty-state/no-results-state.tsx +20 -0
- package/registry/ui/empty-state/manifest.json +63 -0
- package/registry/ui/empty-state/primitives/button.tsx +89 -0
- package/registry/ui/empty-state/primitives/card.tsx +80 -0
- package/registry/ui/empty-state/utils/cn.ts +6 -0
- package/registry/ui/error-page/error-page/error-code.tsx +16 -0
- package/registry/ui/error-page/error-page/error-page-content.ts +75 -0
- package/registry/ui/error-page/error-page/index.ts +19 -0
- package/registry/ui/error-page/error-page/posthog-error-capture.ts +83 -0
- package/registry/ui/error-page/error-page/saas-error-page.tsx +146 -0
- package/registry/ui/error-page/manifest.json +64 -0
- package/registry/ui/error-page/primitives/button.tsx +89 -0
- package/registry/ui/error-page/utils/cn.ts +6 -0
- package/registry/ui/field-detail-row/manifest.json +32 -0
- package/registry/ui/field-detail-row/primitives/field-detail-row.tsx +28 -0
- package/registry/ui/field-detail-row/utils/cn.ts +6 -0
- package/registry/ui/file-dropzone/manifest.json +35 -0
- package/registry/ui/file-dropzone/primitives/button.tsx +89 -0
- package/registry/ui/file-dropzone/primitives/file-dropzone.tsx +236 -0
- package/registry/ui/file-dropzone/utils/cn.ts +6 -0
- package/registry/ui/help-info-button/manifest.json +72 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/help-info-button/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/help-info-button/primitives/button.tsx +89 -0
- package/registry/ui/help-info-button/primitives/drawer.tsx +99 -0
- package/registry/ui/help-info-button/primitives/help-info-button.tsx +63 -0
- package/registry/ui/help-info-button/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/help-info-button/primitives/sheet.tsx +103 -0
- package/registry/ui/help-info-button/primitives/tooltip.tsx +57 -0
- package/registry/ui/help-info-button/utils/cn.ts +6 -0
- package/registry/ui/help-info-button/utils/use-media-query.ts +28 -0
- package/registry/ui/input/manifest.json +31 -0
- package/registry/ui/input/primitives/input.tsx +19 -0
- package/registry/ui/input/utils/cn.ts +6 -0
- package/registry/ui/keyboard-shortcut-hint/manifest.json +32 -0
- package/registry/ui/keyboard-shortcut-hint/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/keyboard-shortcut-hint/utils/cn.ts +6 -0
- package/registry/ui/label/manifest.json +31 -0
- package/registry/ui/label/primitives/label.tsx +21 -0
- package/registry/ui/label/utils/cn.ts +6 -0
- package/registry/ui/pagination/manifest.json +36 -0
- package/registry/ui/pagination/primitives/button.tsx +89 -0
- package/registry/ui/pagination/primitives/pagination.tsx +143 -0
- package/registry/ui/pagination/utils/cn.ts +6 -0
- package/registry/ui/popover/manifest.json +33 -0
- package/registry/ui/popover/primitives/popover.tsx +46 -0
- package/registry/ui/popover/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet/manifest.json +66 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/responsive-sheet/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/responsive-sheet/primitives/button.tsx +89 -0
- package/registry/ui/responsive-sheet/primitives/drawer.tsx +99 -0
- package/registry/ui/responsive-sheet/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/responsive-sheet/primitives/sheet.tsx +103 -0
- package/registry/ui/responsive-sheet/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet/utils/use-media-query.ts +28 -0
- package/registry/ui/responsive-sheet.footer/manifest.json +40 -0
- package/registry/ui/responsive-sheet.footer/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/responsive-sheet.footer/primitives/button.tsx +89 -0
- package/registry/ui/responsive-sheet.footer/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/responsive-sheet.footer/utils/cn.ts +6 -0
- package/registry/ui/responsive-sheet.shortcuts/manifest.json +34 -0
- package/registry/ui/responsive-sheet.shortcuts/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/responsive-sheet.shortcuts/utils/cn.ts +6 -0
- package/registry/ui/scroll-fade-area/manifest.json +31 -0
- package/registry/ui/scroll-fade-area/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/scroll-fade-area/utils/cn.ts +6 -0
- package/registry/ui/search/manifest.json +35 -0
- package/registry/ui/search/utils/cn.ts +6 -0
- package/registry/ui/search/utils/search.ts +227 -0
- package/registry/ui/searchable-select/manifest.json +48 -0
- package/registry/ui/searchable-select/primitives/input.tsx +19 -0
- package/registry/ui/searchable-select/search/searchable-select-position.ts +95 -0
- package/registry/ui/searchable-select/search/searchable-select.tsx +431 -0
- package/registry/ui/searchable-select/utils/cn.ts +6 -0
- package/registry/ui/searchable-select/utils/search.ts +227 -0
- package/registry/ui/searchable-select-position/manifest.json +32 -0
- package/registry/ui/searchable-select-position/search/searchable-select-position.ts +95 -0
- package/registry/ui/searchable-select-position/utils/cn.ts +6 -0
- package/registry/ui/segmented-toggle/manifest.json +41 -0
- package/registry/ui/segmented-toggle/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/segmented-toggle/primitives/segmented-toggle.tsx +106 -0
- package/registry/ui/segmented-toggle/primitives/tabs.tsx +97 -0
- package/registry/ui/segmented-toggle/utils/cn.ts +6 -0
- package/registry/ui/select/manifest.json +37 -0
- package/registry/ui/select/primitives/select.tsx +142 -0
- package/registry/ui/select/utils/cn.ts +6 -0
- package/registry/ui/sheet/manifest.json +39 -0
- package/registry/ui/sheet/primitives/button.tsx +89 -0
- package/registry/ui/sheet/primitives/sheet.tsx +103 -0
- package/registry/ui/sheet/utils/cn.ts +6 -0
- package/registry/ui/skeleton/manifest.json +31 -0
- package/registry/ui/skeleton/primitives/skeleton.tsx +13 -0
- package/registry/ui/skeleton/utils/cn.ts +6 -0
- package/registry/ui/smart-table/manifest.json +115 -0
- package/registry/ui/smart-table/primitives/button.tsx +89 -0
- package/registry/ui/smart-table/primitives/card.tsx +80 -0
- package/registry/ui/smart-table/primitives/display-date.tsx +20 -0
- package/registry/ui/smart-table/primitives/pagination.tsx +143 -0
- package/registry/ui/smart-table/primitives/skeleton.tsx +13 -0
- package/registry/ui/smart-table/primitives/table.tsx +92 -0
- package/registry/ui/smart-table/primitives/tooltip.tsx +57 -0
- package/registry/ui/smart-table/smart-table/DesktopView.tsx +343 -0
- package/registry/ui/smart-table/smart-table/MobileView.tsx +170 -0
- package/registry/ui/smart-table/smart-table/SmartTable.tsx +85 -0
- package/registry/ui/smart-table/smart-table/SmartTableActions.tsx +71 -0
- package/registry/ui/smart-table/smart-table/TruncatedContent.tsx +147 -0
- package/registry/ui/smart-table/smart-table/index.ts +15 -0
- package/registry/ui/smart-table/smart-table/sorting.ts +148 -0
- package/registry/ui/smart-table/smart-table/truncated-content.utils.ts +22 -0
- package/registry/ui/smart-table/smart-table/types.ts +95 -0
- package/registry/ui/smart-table/smart-table/utils.ts +150 -0
- package/registry/ui/smart-table/utils/cn.ts +6 -0
- package/registry/ui/smart-table/utils/date-display.ts +61 -0
- package/registry/ui/smart-table/utils/use-media-query.ts +28 -0
- package/registry/ui/switch/manifest.json +31 -0
- package/registry/ui/switch/primitives/switch.tsx +31 -0
- package/registry/ui/switch/utils/cn.ts +6 -0
- package/registry/ui/table/manifest.json +38 -0
- package/registry/ui/table/primitives/table.tsx +92 -0
- package/registry/ui/table/utils/cn.ts +6 -0
- package/registry/ui/table-toolbar/manifest.json +93 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.footer.tsx +88 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.layouts.tsx +207 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.shortcuts.ts +103 -0
- package/registry/ui/table-toolbar/overlays/responsive-sheet.tsx +132 -0
- package/registry/ui/table-toolbar/primitives/button.tsx +89 -0
- package/registry/ui/table-toolbar/primitives/drawer.tsx +99 -0
- package/registry/ui/table-toolbar/primitives/input.tsx +19 -0
- package/registry/ui/table-toolbar/primitives/keyboard-shortcut-hint.tsx +40 -0
- package/registry/ui/table-toolbar/primitives/sheet.tsx +103 -0
- package/registry/ui/table-toolbar/search/searchable-select-position.ts +95 -0
- package/registry/ui/table-toolbar/search/searchable-select.tsx +431 -0
- package/registry/ui/table-toolbar/table-toolbar/index.ts +9 -0
- package/registry/ui/table-toolbar/table-toolbar/table-toolbar.tsx +552 -0
- package/registry/ui/table-toolbar/utils/cn.ts +6 -0
- package/registry/ui/table-toolbar/utils/search.ts +227 -0
- package/registry/ui/table-toolbar/utils/use-media-query.ts +28 -0
- package/registry/ui/tabs/manifest.json +40 -0
- package/registry/ui/tabs/primitives/scroll-fade-area.tsx +295 -0
- package/registry/ui/tabs/primitives/tabs.tsx +97 -0
- package/registry/ui/tabs/utils/cn.ts +6 -0
- package/registry/ui/textarea/manifest.json +31 -0
- package/registry/ui/textarea/primitives/textarea.tsx +18 -0
- package/registry/ui/textarea/utils/cn.ts +6 -0
- package/registry/ui/tooltip/manifest.json +34 -0
- package/registry/ui/tooltip/primitives/tooltip.tsx +57 -0
- package/registry/ui/tooltip/utils/cn.ts +6 -0
- package/registry/ui/use-media-query/manifest.json +32 -0
- package/registry/ui/use-media-query/utils/cn.ts +6 -0
- package/registry/ui/use-media-query/utils/use-media-query.ts +28 -0
- package/registry/ui/user-picker/manifest.json +52 -0
- package/registry/ui/user-picker/primitives/avatar.tsx +64 -0
- package/registry/ui/user-picker/primitives/button.tsx +89 -0
- package/registry/ui/user-picker/primitives/input.tsx +19 -0
- package/registry/ui/user-picker/primitives/popover.tsx +46 -0
- package/registry/ui/user-picker/primitives/user-picker-utils.ts +113 -0
- package/registry/ui/user-picker/primitives/user-picker.tsx +226 -0
- package/registry/ui/user-picker/utils/cn.ts +6 -0
- package/registry/ui/user-picker-utils/manifest.json +38 -0
- package/registry/ui/user-picker-utils/primitives/user-picker-utils.ts +113 -0
- package/registry/ui/user-picker-utils/utils/cn.ts +6 -0
- package/registry/ui/button/cn.ts +0 -6
- /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,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
|
+
}
|
package/registry/ui/button/cn.ts
DELETED
|
File without changes
|