@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,236 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Check, FileText, Upload, X } from 'lucide-react';
|
|
4
|
+
import { useRef, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { cn } from '@/lib/utils';
|
|
8
|
+
|
|
9
|
+
interface FileDropzoneProps {
|
|
10
|
+
readonly accept?: string;
|
|
11
|
+
readonly browseLabel?: string;
|
|
12
|
+
readonly browseButtonClassName?: string;
|
|
13
|
+
readonly className?: string;
|
|
14
|
+
readonly currentPreviewUrl?: string | null;
|
|
15
|
+
readonly disabled?: boolean;
|
|
16
|
+
readonly emptyIcon?: React.ReactNode;
|
|
17
|
+
readonly helperText?: string;
|
|
18
|
+
readonly footerText?: string | null;
|
|
19
|
+
readonly inputClassName?: string;
|
|
20
|
+
readonly multiple?: boolean;
|
|
21
|
+
readonly onFileSelect: (file: File) => void;
|
|
22
|
+
readonly onFilesSelect?: (files: File[]) => void;
|
|
23
|
+
readonly onRemove?: () => void;
|
|
24
|
+
readonly previewAlt: string;
|
|
25
|
+
readonly title?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function formatFileSize(size: number): string {
|
|
29
|
+
if (size < 1024) {
|
|
30
|
+
return `${size} B`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (size < 1024 * 1024) {
|
|
34
|
+
return `${Math.round(size / 1024)} KB`;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `${(size / (1024 * 1024)).toFixed(1)} MB`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function getFileTypeLabel(file: File): string {
|
|
41
|
+
return file.type || file.name.split('.').pop()?.toUpperCase() || 'File';
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function FileDropzone({
|
|
45
|
+
accept,
|
|
46
|
+
browseLabel = 'Esplora',
|
|
47
|
+
browseButtonClassName,
|
|
48
|
+
className,
|
|
49
|
+
currentPreviewUrl = null,
|
|
50
|
+
disabled = false,
|
|
51
|
+
emptyIcon,
|
|
52
|
+
helperText,
|
|
53
|
+
footerText = 'Drag a file here or click the box to select one.',
|
|
54
|
+
inputClassName,
|
|
55
|
+
multiple = false,
|
|
56
|
+
onFileSelect,
|
|
57
|
+
onFilesSelect,
|
|
58
|
+
onRemove,
|
|
59
|
+
previewAlt,
|
|
60
|
+
title = 'Lascia qui o esplora file',
|
|
61
|
+
}: FileDropzoneProps): React.ReactElement {
|
|
62
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
63
|
+
const [isDragging, setIsDragging] = useState(false);
|
|
64
|
+
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
|
65
|
+
const hasSelectedFiles = selectedFiles.length > 0;
|
|
66
|
+
|
|
67
|
+
const openFilePicker = (): void => {
|
|
68
|
+
if (disabled) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
inputRef.current?.click();
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const handleFileSelection = (files: FileList | File[] | null | undefined): void => {
|
|
76
|
+
const selectedFiles = Array.from(files ?? []);
|
|
77
|
+
if (selectedFiles.length === 0 || disabled) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
setSelectedFiles(selectedFiles);
|
|
82
|
+
|
|
83
|
+
if (multiple && onFilesSelect) {
|
|
84
|
+
onFilesSelect(selectedFiles);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const firstFile = selectedFiles[0];
|
|
89
|
+
if (firstFile) {
|
|
90
|
+
onFileSelect(firstFile);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const handleInputChange = (event: React.ChangeEvent<HTMLInputElement>): void => {
|
|
95
|
+
handleFileSelection(event.target.files);
|
|
96
|
+
event.target.value = '';
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const handleDrop = (event: React.DragEvent<HTMLDivElement>): void => {
|
|
100
|
+
event.preventDefault();
|
|
101
|
+
event.stopPropagation();
|
|
102
|
+
setIsDragging(false);
|
|
103
|
+
handleFileSelection(event.dataTransfer.files);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const handleRemove = (event: React.MouseEvent<HTMLButtonElement>): void => {
|
|
107
|
+
event.stopPropagation();
|
|
108
|
+
setSelectedFiles([]);
|
|
109
|
+
onRemove?.();
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<>
|
|
114
|
+
<input
|
|
115
|
+
ref={inputRef}
|
|
116
|
+
type="file"
|
|
117
|
+
accept={accept}
|
|
118
|
+
multiple={multiple}
|
|
119
|
+
className="hidden"
|
|
120
|
+
onChange={handleInputChange}
|
|
121
|
+
disabled={disabled}
|
|
122
|
+
/>
|
|
123
|
+
<div
|
|
124
|
+
role="button"
|
|
125
|
+
tabIndex={disabled ? -1 : 0}
|
|
126
|
+
aria-disabled={disabled}
|
|
127
|
+
onClick={openFilePicker}
|
|
128
|
+
onKeyDown={(event): void => {
|
|
129
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
130
|
+
event.preventDefault();
|
|
131
|
+
openFilePicker();
|
|
132
|
+
}
|
|
133
|
+
}}
|
|
134
|
+
onDragEnter={(event): void => {
|
|
135
|
+
event.preventDefault();
|
|
136
|
+
event.stopPropagation();
|
|
137
|
+
if (!disabled) {
|
|
138
|
+
setIsDragging(true);
|
|
139
|
+
}
|
|
140
|
+
}}
|
|
141
|
+
onDragOver={(event): void => {
|
|
142
|
+
event.preventDefault();
|
|
143
|
+
event.stopPropagation();
|
|
144
|
+
if (!disabled) {
|
|
145
|
+
setIsDragging(true);
|
|
146
|
+
}
|
|
147
|
+
}}
|
|
148
|
+
onDragLeave={(event): void => {
|
|
149
|
+
event.preventDefault();
|
|
150
|
+
event.stopPropagation();
|
|
151
|
+
setIsDragging(false);
|
|
152
|
+
}}
|
|
153
|
+
onDrop={handleDrop}
|
|
154
|
+
className={cn(
|
|
155
|
+
'bg-muted/30 relative flex w-full cursor-pointer flex-col items-center justify-center rounded-lg border border-dashed px-6 py-5 text-center transition-colors outline-none',
|
|
156
|
+
'border-primary/60 hover:bg-muted/50 focus-visible:ring-primary/20 focus-visible:ring-2',
|
|
157
|
+
isDragging && 'bg-muted/60',
|
|
158
|
+
disabled && 'cursor-not-allowed opacity-60',
|
|
159
|
+
className,
|
|
160
|
+
)}
|
|
161
|
+
>
|
|
162
|
+
{currentPreviewUrl ? (
|
|
163
|
+
<div className="bg-background relative flex min-h-40 w-full items-center justify-center overflow-hidden rounded-md">
|
|
164
|
+
<img src={currentPreviewUrl} alt={previewAlt} className="size-full object-contain" />
|
|
165
|
+
{onRemove ? (
|
|
166
|
+
<Button
|
|
167
|
+
type="button"
|
|
168
|
+
variant="destructive"
|
|
169
|
+
size="icon-xs"
|
|
170
|
+
className="absolute top-2 right-2 z-10"
|
|
171
|
+
onClick={handleRemove}
|
|
172
|
+
disabled={disabled}
|
|
173
|
+
>
|
|
174
|
+
<X className="size-3.5" />
|
|
175
|
+
</Button>
|
|
176
|
+
) : null}
|
|
177
|
+
{hasSelectedFiles ? (
|
|
178
|
+
<div className="bg-background/95 absolute right-2 bottom-2 left-2 rounded-md border px-3 py-2 text-left shadow-sm backdrop-blur">
|
|
179
|
+
<p className="truncate text-sm font-medium">{selectedFiles[0]?.name}</p>
|
|
180
|
+
{selectedFiles[0] ? (
|
|
181
|
+
<p className="text-muted-foreground text-xs">
|
|
182
|
+
{getFileTypeLabel(selectedFiles[0])} | {formatFileSize(selectedFiles[0].size)}
|
|
183
|
+
</p>
|
|
184
|
+
) : null}
|
|
185
|
+
</div>
|
|
186
|
+
) : null}
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
<>
|
|
190
|
+
<div className="text-primary mb-2 flex items-center justify-center">
|
|
191
|
+
{emptyIcon ?? <Upload className="size-5" />}
|
|
192
|
+
</div>
|
|
193
|
+
<div className="space-y-0.5">
|
|
194
|
+
<p className="text-foreground text-base font-medium">{title}</p>
|
|
195
|
+
{helperText ? <p className="text-muted-foreground text-sm">{helperText}</p> : null}
|
|
196
|
+
</div>
|
|
197
|
+
<Button
|
|
198
|
+
type="button"
|
|
199
|
+
size="sm"
|
|
200
|
+
className={cn('mt-4 shadow-[0px_1px_1px_rgba(5,32,81,0.05)]', browseButtonClassName)}
|
|
201
|
+
onClick={(event): void => {
|
|
202
|
+
event.stopPropagation();
|
|
203
|
+
openFilePicker();
|
|
204
|
+
}}
|
|
205
|
+
disabled={disabled}
|
|
206
|
+
>
|
|
207
|
+
{browseLabel}
|
|
208
|
+
</Button>
|
|
209
|
+
{hasSelectedFiles ? (
|
|
210
|
+
<div className="mt-4 w-full space-y-2">
|
|
211
|
+
{selectedFiles.map((file) => (
|
|
212
|
+
<div
|
|
213
|
+
key={`${file.name}-${file.size}-${file.lastModified}`}
|
|
214
|
+
className="bg-background flex min-w-0 items-center gap-3 rounded-md border px-3 py-2 text-left"
|
|
215
|
+
>
|
|
216
|
+
<FileText className="text-muted-foreground size-4 shrink-0" />
|
|
217
|
+
<div className="min-w-0 flex-1">
|
|
218
|
+
<p className="truncate text-sm font-medium">{file.name}</p>
|
|
219
|
+
<p className="text-muted-foreground text-xs">
|
|
220
|
+
{getFileTypeLabel(file)} | {formatFileSize(file.size)}
|
|
221
|
+
</p>
|
|
222
|
+
</div>
|
|
223
|
+
<Check className="text-primary size-4 shrink-0" />
|
|
224
|
+
</div>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
) : null}
|
|
228
|
+
</>
|
|
229
|
+
)}
|
|
230
|
+
</div>
|
|
231
|
+
{footerText ? (
|
|
232
|
+
<p className={cn('text-muted-foreground mt-2 text-xs', inputClassName)}>{footerText}</p>
|
|
233
|
+
) : null}
|
|
234
|
+
</>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "help-info-button",
|
|
3
|
+
"description": "Editable source registry entry for help-info-button.",
|
|
4
|
+
"importPath": "@carefully-built/ui",
|
|
5
|
+
"exports": [
|
|
6
|
+
"HelpInfoButton",
|
|
7
|
+
"HelpInfoButtonProps"
|
|
8
|
+
],
|
|
9
|
+
"dependencies": [
|
|
10
|
+
"class-variance-authority",
|
|
11
|
+
"clsx",
|
|
12
|
+
"tailwind-merge"
|
|
13
|
+
],
|
|
14
|
+
"peerDependencies": [
|
|
15
|
+
"react",
|
|
16
|
+
"react-dom",
|
|
17
|
+
"radix-ui",
|
|
18
|
+
"lucide-react",
|
|
19
|
+
"react-day-picker",
|
|
20
|
+
"vaul"
|
|
21
|
+
],
|
|
22
|
+
"files": [
|
|
23
|
+
{
|
|
24
|
+
"source": "overlays/responsive-sheet.footer.tsx",
|
|
25
|
+
"target": "components/ui/responsive-sheet.footer.tsx"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
"source": "overlays/responsive-sheet.layouts.tsx",
|
|
29
|
+
"target": "components/ui/responsive-sheet.layouts.tsx"
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
"source": "overlays/responsive-sheet.shortcuts.ts",
|
|
33
|
+
"target": "components/ui/responsive-sheet.shortcuts.ts"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"source": "overlays/responsive-sheet.tsx",
|
|
37
|
+
"target": "components/ui/responsive-sheet.tsx"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"source": "primitives/button.tsx",
|
|
41
|
+
"target": "components/ui/button.tsx"
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"source": "primitives/drawer.tsx",
|
|
45
|
+
"target": "components/ui/drawer.tsx"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"source": "primitives/help-info-button.tsx",
|
|
49
|
+
"target": "components/ui/help-info-button.tsx"
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"source": "primitives/keyboard-shortcut-hint.tsx",
|
|
53
|
+
"target": "components/ui/keyboard-shortcut-hint.tsx"
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
"source": "primitives/sheet.tsx",
|
|
57
|
+
"target": "components/ui/sheet.tsx"
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
"source": "primitives/tooltip.tsx",
|
|
61
|
+
"target": "components/ui/tooltip.tsx"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"source": "utils/cn.ts",
|
|
65
|
+
"target": "lib/utils.ts"
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
"source": "utils/use-media-query.ts",
|
|
69
|
+
"target": "components/ui/use-media-query.ts"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { CornerDownLeft } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
import type { ReactNode } from 'react';
|
|
6
|
+
|
|
7
|
+
import { Button } from '@/components/ui/button';
|
|
8
|
+
import { KeyboardKeycap, ShortcutModifierKeycap } from '@/components/ui/keyboard-shortcut-hint';
|
|
9
|
+
|
|
10
|
+
export function DesktopConfirmShortcutHint({
|
|
11
|
+
desktopModifierLabel,
|
|
12
|
+
}: {
|
|
13
|
+
readonly desktopModifierLabel: string;
|
|
14
|
+
}): React.ReactElement {
|
|
15
|
+
const keycapClassName = 'border-primary-foreground/20 bg-primary-foreground/10';
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<span className="text-primary-foreground/70 inline-flex items-center gap-1">
|
|
19
|
+
<ShortcutModifierKeycap modifierLabel={desktopModifierLabel} className={keycapClassName} />
|
|
20
|
+
<KeyboardKeycap className={keycapClassName}>
|
|
21
|
+
<CornerDownLeft className="size-[10px]" />
|
|
22
|
+
</KeyboardKeycap>
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface SheetActionFooterProps {
|
|
28
|
+
readonly footer?: ReactNode;
|
|
29
|
+
readonly onCancel?: () => void;
|
|
30
|
+
readonly cancelLabel: ReactNode;
|
|
31
|
+
readonly onConfirm?: () => void;
|
|
32
|
+
readonly confirmLabel: ReactNode;
|
|
33
|
+
readonly confirmDisabled: boolean;
|
|
34
|
+
readonly confirmLoading: boolean;
|
|
35
|
+
readonly desktopConfirmShortcutEnabled?: boolean;
|
|
36
|
+
readonly desktopModifierLabel?: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function SheetActionFooter({
|
|
40
|
+
footer,
|
|
41
|
+
onCancel,
|
|
42
|
+
cancelLabel,
|
|
43
|
+
onConfirm,
|
|
44
|
+
confirmLabel,
|
|
45
|
+
confirmDisabled,
|
|
46
|
+
confirmLoading,
|
|
47
|
+
desktopConfirmShortcutEnabled = false,
|
|
48
|
+
desktopModifierLabel = null,
|
|
49
|
+
}: SheetActionFooterProps): React.ReactNode {
|
|
50
|
+
if (footer) return footer;
|
|
51
|
+
|
|
52
|
+
if (!onCancel && !onConfirm) return null;
|
|
53
|
+
|
|
54
|
+
const actionCount = Number(Boolean(onCancel)) + Number(Boolean(onConfirm));
|
|
55
|
+
const footerButtonClassName = actionCount === 1 ? 'w-full' : 'min-w-0 flex-1';
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="flex w-full flex-nowrap items-center gap-2">
|
|
59
|
+
{onCancel ? (
|
|
60
|
+
<Button
|
|
61
|
+
type="button"
|
|
62
|
+
variant="outline"
|
|
63
|
+
onClick={onCancel}
|
|
64
|
+
className={footerButtonClassName}
|
|
65
|
+
>
|
|
66
|
+
{cancelLabel}
|
|
67
|
+
</Button>
|
|
68
|
+
) : null}
|
|
69
|
+
{onConfirm ? (
|
|
70
|
+
<Button
|
|
71
|
+
type="button"
|
|
72
|
+
onClick={onConfirm}
|
|
73
|
+
disabled={confirmDisabled}
|
|
74
|
+
className={`${footerButtonClassName} relative`}
|
|
75
|
+
>
|
|
76
|
+
<span className="inline-flex w-full items-center justify-center">
|
|
77
|
+
<span>{confirmLoading ? 'Saving...' : confirmLabel}</span>
|
|
78
|
+
{desktopConfirmShortcutEnabled && desktopModifierLabel ? (
|
|
79
|
+
<span className="absolute top-1/2 right-2 -translate-y-1/2">
|
|
80
|
+
<DesktopConfirmShortcutHint desktopModifierLabel={desktopModifierLabel} />
|
|
81
|
+
</span>
|
|
82
|
+
) : null}
|
|
83
|
+
</span>
|
|
84
|
+
</Button>
|
|
85
|
+
) : null}
|
|
86
|
+
</div>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type { ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import type { SheetOutsideInteractionGuard } from '@/components/ui/responsive-sheet';
|
|
6
|
+
import type { ResponsiveSheetClassNames } from '@/components/ui/responsive-sheet';
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Drawer,
|
|
10
|
+
DrawerContent,
|
|
11
|
+
DrawerDescription,
|
|
12
|
+
DrawerHeader,
|
|
13
|
+
DrawerTitle,
|
|
14
|
+
} from '@/components/ui/drawer';
|
|
15
|
+
import {
|
|
16
|
+
Sheet,
|
|
17
|
+
SheetContent,
|
|
18
|
+
SheetDescription,
|
|
19
|
+
SheetHeader,
|
|
20
|
+
SheetTitle,
|
|
21
|
+
} from '@/components/ui/sheet';
|
|
22
|
+
import { cn } from '@/lib/utils';
|
|
23
|
+
|
|
24
|
+
interface SheetDescriptionBlockProps {
|
|
25
|
+
readonly title: ReactNode;
|
|
26
|
+
readonly description?: ReactNode;
|
|
27
|
+
readonly classes?: ResponsiveSheetClassNames;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function SheetDescriptionBlock({
|
|
31
|
+
title,
|
|
32
|
+
description,
|
|
33
|
+
classes,
|
|
34
|
+
}: SheetDescriptionBlockProps): React.ReactElement {
|
|
35
|
+
return (
|
|
36
|
+
<>
|
|
37
|
+
<SheetTitle className={classes?.title}>{title}</SheetTitle>
|
|
38
|
+
{description ? (
|
|
39
|
+
<SheetDescription className={classes?.description}>{description}</SheetDescription>
|
|
40
|
+
) : null}
|
|
41
|
+
</>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
interface SharedSheetLayoutProps {
|
|
46
|
+
readonly open: boolean;
|
|
47
|
+
readonly onOpenChange: (open: boolean) => void;
|
|
48
|
+
readonly modal: boolean;
|
|
49
|
+
readonly outsideInteractionGuard?: SheetOutsideInteractionGuard;
|
|
50
|
+
readonly title: ReactNode;
|
|
51
|
+
readonly description?: ReactNode;
|
|
52
|
+
readonly children: ReactNode;
|
|
53
|
+
readonly footer: ReactNode;
|
|
54
|
+
readonly mobileDrawerContentClassName?: string;
|
|
55
|
+
readonly contentClassName?: string;
|
|
56
|
+
readonly footerClassName?: string;
|
|
57
|
+
readonly classes?: ResponsiveSheetClassNames;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function shouldPreventOutsideInteraction(
|
|
61
|
+
target: EventTarget | null,
|
|
62
|
+
guard?: SheetOutsideInteractionGuard,
|
|
63
|
+
): boolean {
|
|
64
|
+
const element =
|
|
65
|
+
target instanceof Element ? target : target instanceof Node ? target.parentElement : null;
|
|
66
|
+
|
|
67
|
+
if (!element) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (element.closest('[data-searchable-select-content]')) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return guard?.selectors.some((selector) => element.closest(selector)) ?? false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
type MobileSheetLayoutProps = SharedSheetLayoutProps;
|
|
79
|
+
|
|
80
|
+
export function MobileSheetLayout({
|
|
81
|
+
open,
|
|
82
|
+
onOpenChange,
|
|
83
|
+
modal,
|
|
84
|
+
outsideInteractionGuard,
|
|
85
|
+
title,
|
|
86
|
+
description,
|
|
87
|
+
children,
|
|
88
|
+
footer,
|
|
89
|
+
mobileDrawerContentClassName,
|
|
90
|
+
contentClassName,
|
|
91
|
+
footerClassName,
|
|
92
|
+
classes,
|
|
93
|
+
}: MobileSheetLayoutProps): React.ReactElement {
|
|
94
|
+
return (
|
|
95
|
+
<Drawer open={open} onOpenChange={onOpenChange} modal={modal}>
|
|
96
|
+
<DrawerContent
|
|
97
|
+
aria-describedby={description ? undefined : 'responsive-sheet-description-empty'}
|
|
98
|
+
className={cn(
|
|
99
|
+
'px-4 pb-[calc(env(safe-area-inset-bottom)+20px)]',
|
|
100
|
+
mobileDrawerContentClassName,
|
|
101
|
+
classes?.mobileContent,
|
|
102
|
+
)}
|
|
103
|
+
onInteractOutside={(event) => {
|
|
104
|
+
if (shouldPreventOutsideInteraction(event.target, outsideInteractionGuard)) {
|
|
105
|
+
event.preventDefault();
|
|
106
|
+
}
|
|
107
|
+
}}
|
|
108
|
+
onPointerDownOutside={(event) => {
|
|
109
|
+
if (shouldPreventOutsideInteraction(event.target, outsideInteractionGuard)) {
|
|
110
|
+
event.preventDefault();
|
|
111
|
+
}
|
|
112
|
+
}}
|
|
113
|
+
>
|
|
114
|
+
<DrawerHeader className={cn('px-0 pb-4', classes?.header)}>
|
|
115
|
+
<DrawerTitle className={classes?.title}>{title}</DrawerTitle>
|
|
116
|
+
{description ? (
|
|
117
|
+
<DrawerDescription className={classes?.description}>{description}</DrawerDescription>
|
|
118
|
+
) : (
|
|
119
|
+
<DrawerDescription id="responsive-sheet-description-empty" className="sr-only">
|
|
120
|
+
Dialog
|
|
121
|
+
</DrawerDescription>
|
|
122
|
+
)}
|
|
123
|
+
</DrawerHeader>
|
|
124
|
+
<div className="flex min-h-0 flex-1 flex-col gap-4">
|
|
125
|
+
<div
|
|
126
|
+
className={cn(
|
|
127
|
+
'-mx-1 flex-1 overflow-y-auto px-1 pb-3 [scrollbar-gutter:stable]',
|
|
128
|
+
contentClassName,
|
|
129
|
+
classes?.body,
|
|
130
|
+
)}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</div>
|
|
134
|
+
{footer ? (
|
|
135
|
+
<div className={cn('shrink-0 border-t pt-4 pb-3', footerClassName, classes?.footer)}>
|
|
136
|
+
{footer}
|
|
137
|
+
</div>
|
|
138
|
+
) : null}
|
|
139
|
+
</div>
|
|
140
|
+
</DrawerContent>
|
|
141
|
+
</Drawer>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
interface DesktopSheetLayoutProps extends SharedSheetLayoutProps {
|
|
146
|
+
readonly width: number;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export function DesktopSheetLayout({
|
|
150
|
+
open,
|
|
151
|
+
onOpenChange,
|
|
152
|
+
modal,
|
|
153
|
+
outsideInteractionGuard,
|
|
154
|
+
title,
|
|
155
|
+
description,
|
|
156
|
+
children,
|
|
157
|
+
footer,
|
|
158
|
+
width,
|
|
159
|
+
contentClassName,
|
|
160
|
+
footerClassName,
|
|
161
|
+
classes,
|
|
162
|
+
}: DesktopSheetLayoutProps): React.ReactElement {
|
|
163
|
+
return (
|
|
164
|
+
<Sheet open={open} onOpenChange={onOpenChange} modal={modal}>
|
|
165
|
+
<SheetContent
|
|
166
|
+
aria-describedby={description ? undefined : 'responsive-sheet-description-empty'}
|
|
167
|
+
style={{ width: `${String(width)}px`, maxWidth: '85vw' }}
|
|
168
|
+
className={cn('flex flex-col gap-0 p-0', classes?.desktopContent)}
|
|
169
|
+
onInteractOutside={(event) => {
|
|
170
|
+
if (shouldPreventOutsideInteraction(event.target, outsideInteractionGuard)) {
|
|
171
|
+
event.preventDefault();
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
onPointerDownOutside={(event) => {
|
|
175
|
+
if (shouldPreventOutsideInteraction(event.target, outsideInteractionGuard)) {
|
|
176
|
+
event.preventDefault();
|
|
177
|
+
}
|
|
178
|
+
}}
|
|
179
|
+
>
|
|
180
|
+
<SheetHeader className={cn('border-b px-4 py-4', classes?.header)}>
|
|
181
|
+
<SheetDescriptionBlock title={title} description={description} classes={classes} />
|
|
182
|
+
</SheetHeader>
|
|
183
|
+
{description ? null : (
|
|
184
|
+
<SheetDescription id="responsive-sheet-description-empty" className="sr-only">
|
|
185
|
+
Dialog
|
|
186
|
+
</SheetDescription>
|
|
187
|
+
)}
|
|
188
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
189
|
+
<div
|
|
190
|
+
className={cn(
|
|
191
|
+
'flex-1 overflow-x-visible overflow-y-auto px-4 pt-4 pb-6 [scrollbar-gutter:stable]',
|
|
192
|
+
contentClassName,
|
|
193
|
+
classes?.body,
|
|
194
|
+
)}
|
|
195
|
+
>
|
|
196
|
+
{children}
|
|
197
|
+
</div>
|
|
198
|
+
{footer ? (
|
|
199
|
+
<div className={cn('border-t px-4 py-4', footerClassName, classes?.footer)}>
|
|
200
|
+
{footer}
|
|
201
|
+
</div>
|
|
202
|
+
) : null}
|
|
203
|
+
</div>
|
|
204
|
+
</SheetContent>
|
|
205
|
+
</Sheet>
|
|
206
|
+
);
|
|
207
|
+
}
|