@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,552 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Filter, Search, X } from 'lucide-react';
|
|
4
|
+
import { useMemo, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
import type { LucideIcon } from 'lucide-react';
|
|
7
|
+
import type { ReactNode } from 'react';
|
|
8
|
+
|
|
9
|
+
import { Button } from '@/components/ui/button';
|
|
10
|
+
import { Input } from '@/components/ui/input';
|
|
11
|
+
import { DesktopConfirmShortcutHint } from '@/components/ui/responsive-sheet.footer';
|
|
12
|
+
import { ResponsiveSheet } from '@/components/ui/responsive-sheet';
|
|
13
|
+
import { useDesktopShortcutModifierLabel } from '@/components/ui/responsive-sheet.shortcuts';
|
|
14
|
+
import { SearchableSelect } from '@/components/ui/search/searchable-select';
|
|
15
|
+
import { cn } from '@/lib/utils';
|
|
16
|
+
import { useIsMobile } from '@/components/ui/use-media-query';
|
|
17
|
+
|
|
18
|
+
export interface FilterOption<T extends string = string> {
|
|
19
|
+
readonly value: T;
|
|
20
|
+
readonly label: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface FilterDropdownProps<T extends string> {
|
|
24
|
+
readonly label: string;
|
|
25
|
+
readonly value: T | 'all';
|
|
26
|
+
readonly options: readonly FilterOption<T>[];
|
|
27
|
+
readonly onChange: (value: T | 'all') => void;
|
|
28
|
+
readonly className?: string;
|
|
29
|
+
readonly icon?: LucideIcon;
|
|
30
|
+
readonly allowAll?: boolean;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function FilterDropdown<T extends string>({
|
|
34
|
+
label,
|
|
35
|
+
value,
|
|
36
|
+
options,
|
|
37
|
+
onChange,
|
|
38
|
+
className,
|
|
39
|
+
allowAll = true,
|
|
40
|
+
}: FilterDropdownProps<T>): React.ReactElement {
|
|
41
|
+
return (
|
|
42
|
+
<SearchableSelect
|
|
43
|
+
value={value}
|
|
44
|
+
onValueChange={(nextValue) => {
|
|
45
|
+
onChange(nextValue as T | 'all');
|
|
46
|
+
}}
|
|
47
|
+
placeholder={label}
|
|
48
|
+
className={className ?? 'w-full sm:w-[140px]'}
|
|
49
|
+
searchPlaceholder={`Search ${label.toLocaleLowerCase()}...`}
|
|
50
|
+
options={[...(allowAll ? [{ value: 'all', label: `Tutti: ${label}` }] : []), ...options]}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface SearchInputProps {
|
|
56
|
+
readonly value: string;
|
|
57
|
+
readonly onChange: (value: string) => void;
|
|
58
|
+
readonly placeholder?: string;
|
|
59
|
+
readonly className?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function SearchInput({
|
|
63
|
+
value,
|
|
64
|
+
onChange,
|
|
65
|
+
placeholder = 'Search...',
|
|
66
|
+
className,
|
|
67
|
+
}: SearchInputProps): React.ReactElement {
|
|
68
|
+
return (
|
|
69
|
+
<div className={cn('relative', className)}>
|
|
70
|
+
<Search className="text-muted-foreground absolute top-1/2 left-2.5 size-4 -translate-y-1/2" />
|
|
71
|
+
<Input
|
|
72
|
+
type="text"
|
|
73
|
+
placeholder={placeholder}
|
|
74
|
+
value={value}
|
|
75
|
+
onChange={(event) => {
|
|
76
|
+
onChange(event.target.value);
|
|
77
|
+
}}
|
|
78
|
+
className="pr-9 pl-9"
|
|
79
|
+
/>
|
|
80
|
+
{value ? (
|
|
81
|
+
<button
|
|
82
|
+
type="button"
|
|
83
|
+
onClick={() => {
|
|
84
|
+
onChange('');
|
|
85
|
+
}}
|
|
86
|
+
className="text-muted-foreground hover:text-foreground absolute top-1/2 right-2.5 -translate-y-1/2"
|
|
87
|
+
>
|
|
88
|
+
<X className="size-4" />
|
|
89
|
+
</button>
|
|
90
|
+
) : null}
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface FilterConfig<T extends string = string> {
|
|
96
|
+
readonly key: string;
|
|
97
|
+
readonly label: string;
|
|
98
|
+
readonly icon?: LucideIcon;
|
|
99
|
+
readonly options: readonly FilterOption<T>[];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
interface SelectFilter {
|
|
103
|
+
readonly config: FilterConfig;
|
|
104
|
+
readonly value: string;
|
|
105
|
+
readonly onChange: (value: string) => void;
|
|
106
|
+
readonly allowAll?: boolean;
|
|
107
|
+
readonly clearable?: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface TextFilter {
|
|
111
|
+
readonly key: string;
|
|
112
|
+
readonly label: string;
|
|
113
|
+
readonly icon?: LucideIcon;
|
|
114
|
+
readonly value: string;
|
|
115
|
+
readonly onChange: (value: string) => void;
|
|
116
|
+
readonly placeholder?: string;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
interface RangeFilter {
|
|
120
|
+
readonly key: string;
|
|
121
|
+
readonly label: string;
|
|
122
|
+
readonly icon?: LucideIcon;
|
|
123
|
+
readonly minValue: string;
|
|
124
|
+
readonly maxValue: string;
|
|
125
|
+
readonly onMinChange: (value: string) => void;
|
|
126
|
+
readonly onMaxChange: (value: string) => void;
|
|
127
|
+
readonly minPlaceholder?: string;
|
|
128
|
+
readonly maxPlaceholder?: string;
|
|
129
|
+
readonly inputType?: 'text' | 'date';
|
|
130
|
+
readonly inputMode?: React.HTMLAttributes<HTMLInputElement>['inputMode'];
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export interface CustomTableToolbarFilter {
|
|
134
|
+
readonly key: string;
|
|
135
|
+
readonly label: string;
|
|
136
|
+
readonly icon?: LucideIcon;
|
|
137
|
+
readonly value: string;
|
|
138
|
+
readonly clearValue?: string;
|
|
139
|
+
readonly clearable?: boolean;
|
|
140
|
+
readonly onChange: (value: string) => void;
|
|
141
|
+
readonly render: (args: {
|
|
142
|
+
readonly value: string;
|
|
143
|
+
readonly setValue: (value: string) => void;
|
|
144
|
+
}) => ReactNode;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface TableToolbarProps {
|
|
148
|
+
readonly search?: {
|
|
149
|
+
value: string;
|
|
150
|
+
onChange: (value: string) => void;
|
|
151
|
+
placeholder?: string;
|
|
152
|
+
};
|
|
153
|
+
readonly filters?: SelectFilter[];
|
|
154
|
+
readonly textFilters?: TextFilter[];
|
|
155
|
+
readonly customFilters?: CustomTableToolbarFilter[];
|
|
156
|
+
readonly rangeFilters?: RangeFilter[];
|
|
157
|
+
readonly renderRangeInput?: (args: {
|
|
158
|
+
readonly filter: RangeFilter;
|
|
159
|
+
readonly input: 'min' | 'max';
|
|
160
|
+
readonly value: string;
|
|
161
|
+
readonly onChange: (value: string) => void;
|
|
162
|
+
readonly placeholder: string;
|
|
163
|
+
}) => ReactNode;
|
|
164
|
+
readonly inlineControls?: ReactNode;
|
|
165
|
+
readonly onClearAll?: () => void;
|
|
166
|
+
readonly getDraftResultCount?: (draftValues: Record<string, string>) => number | undefined;
|
|
167
|
+
readonly children?: ReactNode;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function TableToolbar({
|
|
171
|
+
search,
|
|
172
|
+
filters,
|
|
173
|
+
textFilters,
|
|
174
|
+
customFilters,
|
|
175
|
+
rangeFilters,
|
|
176
|
+
renderRangeInput,
|
|
177
|
+
inlineControls,
|
|
178
|
+
onClearAll,
|
|
179
|
+
getDraftResultCount,
|
|
180
|
+
children,
|
|
181
|
+
}: TableToolbarProps): React.ReactElement {
|
|
182
|
+
const isMobile = useIsMobile();
|
|
183
|
+
const [filtersOpen, setFiltersOpen] = useState(false);
|
|
184
|
+
const [draftFilterValues, setDraftFilterValues] = useState<Record<string, string>>({});
|
|
185
|
+
const hasFilters = Boolean(
|
|
186
|
+
(filters?.length ?? 0) +
|
|
187
|
+
(textFilters?.length ?? 0) +
|
|
188
|
+
(customFilters?.length ?? 0) +
|
|
189
|
+
(rangeFilters?.length ?? 0),
|
|
190
|
+
);
|
|
191
|
+
const desktopConfirmShortcutEnabled = filtersOpen && !isMobile;
|
|
192
|
+
const desktopModifierLabel = useDesktopShortcutModifierLabel(desktopConfirmShortcutEnabled);
|
|
193
|
+
const clearableFilters = filters?.filter((filter) => filter.clearable !== false) ?? [];
|
|
194
|
+
const clearableCustomFilters =
|
|
195
|
+
customFilters?.filter((filter) => filter.clearable !== false) ?? [];
|
|
196
|
+
const filterValues = useMemo(
|
|
197
|
+
() => ({
|
|
198
|
+
...Object.fromEntries((filters ?? []).map((filter) => [filter.config.key, filter.value])),
|
|
199
|
+
...Object.fromEntries((textFilters ?? []).map((filter) => [filter.key, filter.value])),
|
|
200
|
+
...Object.fromEntries((customFilters ?? []).map((filter) => [filter.key, filter.value])),
|
|
201
|
+
...Object.fromEntries(
|
|
202
|
+
(rangeFilters ?? []).flatMap((filter) => [
|
|
203
|
+
[`${filter.key}Min`, filter.minValue],
|
|
204
|
+
[`${filter.key}Max`, filter.maxValue],
|
|
205
|
+
]),
|
|
206
|
+
),
|
|
207
|
+
}),
|
|
208
|
+
[customFilters, filters, rangeFilters, textFilters],
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
const activeFilterCount = clearableFilters.filter((filter) => filter.value !== 'all').length;
|
|
212
|
+
const activeCustomFilterCount = clearableCustomFilters.filter(
|
|
213
|
+
(filter) => filter.value !== (filter.clearValue ?? 'all'),
|
|
214
|
+
).length;
|
|
215
|
+
const activeRangeFilterCount =
|
|
216
|
+
rangeFilters?.filter((filter) => filter.minValue.trim() || filter.maxValue.trim()).length ?? 0;
|
|
217
|
+
const activeTextFilterCount =
|
|
218
|
+
textFilters?.filter((filter) => filter.value.trim().length > 0).length ?? 0;
|
|
219
|
+
const hasDraftFilters =
|
|
220
|
+
clearableFilters.some((filter) => {
|
|
221
|
+
const draftValue = draftFilterValues[filter.config.key] ?? filter.value;
|
|
222
|
+
return draftValue !== 'all';
|
|
223
|
+
}) ||
|
|
224
|
+
clearableCustomFilters.some((filter) => {
|
|
225
|
+
const draftValue = draftFilterValues[filter.key] ?? filter.value;
|
|
226
|
+
return draftValue !== (filter.clearValue ?? 'all');
|
|
227
|
+
}) ||
|
|
228
|
+
(rangeFilters ?? []).some((filter) => {
|
|
229
|
+
const minValue = draftFilterValues[`${filter.key}Min`] ?? filter.minValue;
|
|
230
|
+
const maxValue = draftFilterValues[`${filter.key}Max`] ?? filter.maxValue;
|
|
231
|
+
return Boolean(minValue.trim() || maxValue.trim());
|
|
232
|
+
}) ||
|
|
233
|
+
(textFilters ?? []).some((filter) => {
|
|
234
|
+
const draftValue = draftFilterValues[filter.key] ?? filter.value;
|
|
235
|
+
return draftValue.trim().length > 0;
|
|
236
|
+
});
|
|
237
|
+
const draftResultCount = useMemo(
|
|
238
|
+
() => (filtersOpen ? getDraftResultCount?.(draftFilterValues) : undefined),
|
|
239
|
+
[draftFilterValues, filtersOpen, getDraftResultCount],
|
|
240
|
+
);
|
|
241
|
+
const applyButtonLabel =
|
|
242
|
+
typeof draftResultCount === 'number'
|
|
243
|
+
? `Show ${draftResultCount.toLocaleString('en-US')} ${
|
|
244
|
+
draftResultCount === 1 ? 'result' : 'results'
|
|
245
|
+
}`
|
|
246
|
+
: 'Show results';
|
|
247
|
+
|
|
248
|
+
function openFiltersSheet(): void {
|
|
249
|
+
setDraftFilterValues(filterValues);
|
|
250
|
+
setFiltersOpen(true);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const activeFilterTotal =
|
|
254
|
+
activeFilterCount + activeCustomFilterCount + activeRangeFilterCount + activeTextFilterCount;
|
|
255
|
+
|
|
256
|
+
function updateDraftFilterValue(key: string, value: string): void {
|
|
257
|
+
setDraftFilterValues((currentValues) => ({
|
|
258
|
+
...currentValues,
|
|
259
|
+
[key]: value,
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function clearAndApplyFilters(): void {
|
|
264
|
+
const clearedValues = Object.fromEntries(
|
|
265
|
+
clearableFilters.map((filter) => [filter.config.key, 'all']),
|
|
266
|
+
);
|
|
267
|
+
const clearedRangeValues = Object.fromEntries(
|
|
268
|
+
(rangeFilters ?? []).flatMap((filter) => [
|
|
269
|
+
[`${filter.key}Min`, ''],
|
|
270
|
+
[`${filter.key}Max`, ''],
|
|
271
|
+
]),
|
|
272
|
+
);
|
|
273
|
+
const clearedTextValues = Object.fromEntries(
|
|
274
|
+
(textFilters ?? []).map((filter) => [filter.key, '']),
|
|
275
|
+
);
|
|
276
|
+
const clearedCustomValues = Object.fromEntries(
|
|
277
|
+
clearableCustomFilters.map((filter) => [filter.key, filter.clearValue ?? 'all']),
|
|
278
|
+
);
|
|
279
|
+
setDraftFilterValues((currentValues) => ({
|
|
280
|
+
...currentValues,
|
|
281
|
+
...clearedValues,
|
|
282
|
+
...clearedRangeValues,
|
|
283
|
+
...clearedTextValues,
|
|
284
|
+
...clearedCustomValues,
|
|
285
|
+
}));
|
|
286
|
+
filters?.forEach((filter) => {
|
|
287
|
+
const nextValue = filter.clearable === false ? filter.value : 'all';
|
|
288
|
+
if (nextValue !== filter.value) {
|
|
289
|
+
filter.onChange(nextValue);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
rangeFilters?.forEach((filter) => {
|
|
293
|
+
if (filter.minValue) {
|
|
294
|
+
filter.onMinChange('');
|
|
295
|
+
}
|
|
296
|
+
if (filter.maxValue) {
|
|
297
|
+
filter.onMaxChange('');
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
textFilters?.forEach((filter) => {
|
|
301
|
+
if (filter.value) {
|
|
302
|
+
filter.onChange('');
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
customFilters?.forEach((filter) => {
|
|
306
|
+
const nextValue = filter.clearable === false ? filter.value : (filter.clearValue ?? 'all');
|
|
307
|
+
if (nextValue !== filter.value) {
|
|
308
|
+
filter.onChange(nextValue);
|
|
309
|
+
}
|
|
310
|
+
});
|
|
311
|
+
setFiltersOpen(false);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
function applyDraftFilters(): void {
|
|
315
|
+
filters?.forEach((filter) => {
|
|
316
|
+
const nextValue = draftFilterValues[filter.config.key] ?? filter.value;
|
|
317
|
+
if (nextValue !== filter.value) {
|
|
318
|
+
filter.onChange(nextValue);
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
rangeFilters?.forEach((filter) => {
|
|
322
|
+
const nextMinValue = draftFilterValues[`${filter.key}Min`] ?? filter.minValue;
|
|
323
|
+
const nextMaxValue = draftFilterValues[`${filter.key}Max`] ?? filter.maxValue;
|
|
324
|
+
if (nextMinValue !== filter.minValue) {
|
|
325
|
+
filter.onMinChange(nextMinValue);
|
|
326
|
+
}
|
|
327
|
+
if (nextMaxValue !== filter.maxValue) {
|
|
328
|
+
filter.onMaxChange(nextMaxValue);
|
|
329
|
+
}
|
|
330
|
+
});
|
|
331
|
+
textFilters?.forEach((filter) => {
|
|
332
|
+
const nextValue = draftFilterValues[filter.key] ?? filter.value;
|
|
333
|
+
if (nextValue !== filter.value) {
|
|
334
|
+
filter.onChange(nextValue);
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
customFilters?.forEach((filter) => {
|
|
338
|
+
const nextValue = draftFilterValues[filter.key] ?? filter.value;
|
|
339
|
+
if (nextValue !== filter.value) {
|
|
340
|
+
filter.onChange(nextValue);
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
setFiltersOpen(false);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
return (
|
|
347
|
+
<div className="space-y-3">
|
|
348
|
+
<div className="flex flex-wrap items-center gap-2">
|
|
349
|
+
{search ? (
|
|
350
|
+
<SearchInput
|
|
351
|
+
value={search.value}
|
|
352
|
+
onChange={search.onChange}
|
|
353
|
+
placeholder={search.placeholder}
|
|
354
|
+
className="min-w-0 flex-1 sm:w-64 sm:flex-initial"
|
|
355
|
+
/>
|
|
356
|
+
) : null}
|
|
357
|
+
|
|
358
|
+
{inlineControls}
|
|
359
|
+
|
|
360
|
+
{hasFilters ? (
|
|
361
|
+
<Button variant="outline" className="relative gap-2" onClick={openFiltersSheet}>
|
|
362
|
+
<Filter className="size-4" />
|
|
363
|
+
Filters
|
|
364
|
+
{activeFilterTotal > 0 ? (
|
|
365
|
+
<span className="bg-primary text-primary-foreground absolute -top-1 -right-1 flex size-4 items-center justify-center rounded-full text-[10px]">
|
|
366
|
+
{activeFilterTotal}
|
|
367
|
+
</span>
|
|
368
|
+
) : null}
|
|
369
|
+
</Button>
|
|
370
|
+
) : null}
|
|
371
|
+
|
|
372
|
+
{children ? (
|
|
373
|
+
<div className="hidden sm:ml-auto sm:flex sm:items-center sm:gap-2">{children}</div>
|
|
374
|
+
) : null}
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
{hasFilters ? (
|
|
378
|
+
<ResponsiveSheet
|
|
379
|
+
open={filtersOpen}
|
|
380
|
+
onOpenChange={setFiltersOpen}
|
|
381
|
+
title="Filters"
|
|
382
|
+
description="Restringi l'elenco con i criteri disponibili."
|
|
383
|
+
onConfirm={applyDraftFilters}
|
|
384
|
+
footer={
|
|
385
|
+
<div className="flex w-full gap-2">
|
|
386
|
+
{onClearAll ? (
|
|
387
|
+
<Button
|
|
388
|
+
type="button"
|
|
389
|
+
variant="outline"
|
|
390
|
+
disabled={!hasDraftFilters}
|
|
391
|
+
onClick={clearAndApplyFilters}
|
|
392
|
+
className="min-w-0 flex-1"
|
|
393
|
+
>
|
|
394
|
+
<X className="mr-1 size-4" />
|
|
395
|
+
Azzera
|
|
396
|
+
</Button>
|
|
397
|
+
) : null}
|
|
398
|
+
<Button
|
|
399
|
+
type="button"
|
|
400
|
+
onClick={applyDraftFilters}
|
|
401
|
+
className={cn(
|
|
402
|
+
'relative min-w-0 flex-1',
|
|
403
|
+
desktopConfirmShortcutEnabled ? 'pr-12' : null,
|
|
404
|
+
)}
|
|
405
|
+
>
|
|
406
|
+
<span className="inline-flex w-full items-center justify-center">
|
|
407
|
+
<span className="min-w-0 truncate">{applyButtonLabel}</span>
|
|
408
|
+
{desktopConfirmShortcutEnabled && desktopModifierLabel ? (
|
|
409
|
+
<span className="absolute top-1/2 right-2 -translate-y-1/2">
|
|
410
|
+
<DesktopConfirmShortcutHint desktopModifierLabel={desktopModifierLabel} />
|
|
411
|
+
</span>
|
|
412
|
+
) : null}
|
|
413
|
+
</span>
|
|
414
|
+
</Button>
|
|
415
|
+
</div>
|
|
416
|
+
}
|
|
417
|
+
width={420}
|
|
418
|
+
>
|
|
419
|
+
<div className="space-y-4">
|
|
420
|
+
{filters?.map((filter) => {
|
|
421
|
+
const Icon = filter.config.icon;
|
|
422
|
+
|
|
423
|
+
return (
|
|
424
|
+
<div key={filter.config.key} className="flex flex-col gap-1.5">
|
|
425
|
+
<span className="flex items-center gap-2 text-sm font-medium text-[#1f2328]">
|
|
426
|
+
{Icon ? (
|
|
427
|
+
<Icon className="text-foreground/70 size-3.5 shrink-0" strokeWidth={1.8} />
|
|
428
|
+
) : null}
|
|
429
|
+
{filter.config.label}
|
|
430
|
+
</span>
|
|
431
|
+
<FilterDropdown
|
|
432
|
+
label={filter.config.label}
|
|
433
|
+
value={draftFilterValues[filter.config.key] ?? filter.value}
|
|
434
|
+
options={filter.config.options}
|
|
435
|
+
onChange={(value) => {
|
|
436
|
+
updateDraftFilterValue(filter.config.key, value);
|
|
437
|
+
}}
|
|
438
|
+
className="w-full"
|
|
439
|
+
allowAll={filter.allowAll}
|
|
440
|
+
/>
|
|
441
|
+
</div>
|
|
442
|
+
);
|
|
443
|
+
})}
|
|
444
|
+
{customFilters?.map((filter) => {
|
|
445
|
+
const Icon = filter.icon;
|
|
446
|
+
|
|
447
|
+
return (
|
|
448
|
+
<div key={filter.key} className="flex flex-col gap-1.5">
|
|
449
|
+
<span className="flex items-center gap-2 text-sm font-medium text-[#1f2328]">
|
|
450
|
+
{Icon ? (
|
|
451
|
+
<Icon className="text-foreground/70 size-3.5 shrink-0" strokeWidth={1.8} />
|
|
452
|
+
) : null}
|
|
453
|
+
{filter.label}
|
|
454
|
+
</span>
|
|
455
|
+
{filter.render({
|
|
456
|
+
value: draftFilterValues[filter.key] ?? filter.value,
|
|
457
|
+
setValue: (value) => {
|
|
458
|
+
updateDraftFilterValue(filter.key, value);
|
|
459
|
+
},
|
|
460
|
+
})}
|
|
461
|
+
</div>
|
|
462
|
+
);
|
|
463
|
+
})}
|
|
464
|
+
{textFilters?.map((filter) => {
|
|
465
|
+
const Icon = filter.icon;
|
|
466
|
+
|
|
467
|
+
return (
|
|
468
|
+
<div key={filter.key} className="flex flex-col gap-1.5">
|
|
469
|
+
<span className="flex items-center gap-2 text-sm font-medium text-[#1f2328]">
|
|
470
|
+
{Icon ? (
|
|
471
|
+
<Icon className="text-foreground/70 size-3.5 shrink-0" strokeWidth={1.8} />
|
|
472
|
+
) : null}
|
|
473
|
+
{filter.label}
|
|
474
|
+
</span>
|
|
475
|
+
<Input
|
|
476
|
+
type="text"
|
|
477
|
+
value={draftFilterValues[filter.key] ?? filter.value}
|
|
478
|
+
onChange={(event) => {
|
|
479
|
+
updateDraftFilterValue(filter.key, event.target.value);
|
|
480
|
+
}}
|
|
481
|
+
placeholder={filter.placeholder ?? filter.label}
|
|
482
|
+
/>
|
|
483
|
+
</div>
|
|
484
|
+
);
|
|
485
|
+
})}
|
|
486
|
+
{rangeFilters?.map((filter) => {
|
|
487
|
+
const Icon = filter.icon;
|
|
488
|
+
const minKey = `${filter.key}Min`;
|
|
489
|
+
const maxKey = `${filter.key}Max`;
|
|
490
|
+
|
|
491
|
+
return (
|
|
492
|
+
<div key={filter.key} className="flex flex-col gap-1.5">
|
|
493
|
+
<span className="flex items-center gap-2 text-sm font-medium text-[#1f2328]">
|
|
494
|
+
{Icon ? (
|
|
495
|
+
<Icon className="text-foreground/70 size-3.5 shrink-0" strokeWidth={1.8} />
|
|
496
|
+
) : null}
|
|
497
|
+
{filter.label}
|
|
498
|
+
</span>
|
|
499
|
+
<div className="grid grid-cols-2 gap-2">
|
|
500
|
+
{renderRangeInput ? (
|
|
501
|
+
<>
|
|
502
|
+
{renderRangeInput({
|
|
503
|
+
filter,
|
|
504
|
+
input: 'min',
|
|
505
|
+
value: draftFilterValues[minKey] ?? filter.minValue,
|
|
506
|
+
onChange: (value) => {
|
|
507
|
+
updateDraftFilterValue(minKey, value);
|
|
508
|
+
},
|
|
509
|
+
placeholder: filter.minPlaceholder ?? 'Da',
|
|
510
|
+
})}
|
|
511
|
+
{renderRangeInput({
|
|
512
|
+
filter,
|
|
513
|
+
input: 'max',
|
|
514
|
+
value: draftFilterValues[maxKey] ?? filter.maxValue,
|
|
515
|
+
onChange: (value) => {
|
|
516
|
+
updateDraftFilterValue(maxKey, value);
|
|
517
|
+
},
|
|
518
|
+
placeholder: filter.maxPlaceholder ?? 'A',
|
|
519
|
+
})}
|
|
520
|
+
</>
|
|
521
|
+
) : (
|
|
522
|
+
<>
|
|
523
|
+
<Input
|
|
524
|
+
type={filter.inputType ?? 'text'}
|
|
525
|
+
inputMode={filter.inputMode ?? 'decimal'}
|
|
526
|
+
value={draftFilterValues[minKey] ?? filter.minValue}
|
|
527
|
+
onChange={(event) => {
|
|
528
|
+
updateDraftFilterValue(minKey, event.target.value);
|
|
529
|
+
}}
|
|
530
|
+
placeholder={filter.minPlaceholder ?? 'Min'}
|
|
531
|
+
/>
|
|
532
|
+
<Input
|
|
533
|
+
type={filter.inputType ?? 'text'}
|
|
534
|
+
inputMode={filter.inputMode ?? 'decimal'}
|
|
535
|
+
value={draftFilterValues[maxKey] ?? filter.maxValue}
|
|
536
|
+
onChange={(event) => {
|
|
537
|
+
updateDraftFilterValue(maxKey, event.target.value);
|
|
538
|
+
}}
|
|
539
|
+
placeholder={filter.maxPlaceholder ?? 'Max'}
|
|
540
|
+
/>
|
|
541
|
+
</>
|
|
542
|
+
)}
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
);
|
|
546
|
+
})}
|
|
547
|
+
</div>
|
|
548
|
+
</ResponsiveSheet>
|
|
549
|
+
) : null}
|
|
550
|
+
</div>
|
|
551
|
+
);
|
|
552
|
+
}
|