@carefully-built/cli 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (213) hide show
  1. package/README.md +101 -80
  2. package/dist/index.mjs +8 -5
  3. package/dist/index.mjs.map +1 -1
  4. package/package.json +3 -3
  5. package/registry/ui/avatar/manifest.json +33 -0
  6. package/registry/ui/avatar/primitives/avatar.tsx +64 -0
  7. package/registry/ui/avatar/utils/cn.ts +6 -0
  8. package/registry/ui/button/manifest.json +24 -5
  9. package/registry/ui/button/utils/cn.ts +6 -0
  10. package/registry/ui/calendar/manifest.json +35 -0
  11. package/registry/ui/calendar/primitives/button.tsx +89 -0
  12. package/registry/ui/calendar/primitives/calendar.tsx +68 -0
  13. package/registry/ui/calendar/utils/cn.ts +6 -0
  14. package/registry/ui/card/manifest.json +36 -0
  15. package/registry/ui/card/primitives/card.tsx +80 -0
  16. package/registry/ui/card/utils/cn.ts +6 -0
  17. package/registry/ui/chip/manifest.json +36 -0
  18. package/registry/ui/chip/primitives/chip-utils.ts +10 -0
  19. package/registry/ui/chip/primitives/chip.tsx +74 -0
  20. package/registry/ui/chip/utils/cn.ts +6 -0
  21. package/registry/ui/chip-utils/manifest.json +33 -0
  22. package/registry/ui/chip-utils/primitives/chip-utils.ts +10 -0
  23. package/registry/ui/chip-utils/utils/cn.ts +6 -0
  24. package/registry/ui/date-display/manifest.json +33 -0
  25. package/registry/ui/date-display/utils/cn.ts +6 -0
  26. package/registry/ui/date-display/utils/date-display.ts +61 -0
  27. package/registry/ui/dialog/manifest.json +43 -0
  28. package/registry/ui/dialog/primitives/button.tsx +89 -0
  29. package/registry/ui/dialog/primitives/dialog.tsx +147 -0
  30. package/registry/ui/dialog/utils/cn.ts +6 -0
  31. package/registry/ui/display-date/manifest.json +36 -0
  32. package/registry/ui/display-date/primitives/display-date.tsx +20 -0
  33. package/registry/ui/display-date/utils/cn.ts +6 -0
  34. package/registry/ui/display-date/utils/date-display.ts +61 -0
  35. package/registry/ui/drawer/manifest.json +37 -0
  36. package/registry/ui/drawer/primitives/drawer.tsx +99 -0
  37. package/registry/ui/drawer/utils/cn.ts +6 -0
  38. package/registry/ui/dropdown-menu/manifest.json +37 -0
  39. package/registry/ui/dropdown-menu/primitives/dropdown-menu.tsx +140 -0
  40. package/registry/ui/dropdown-menu/utils/cn.ts +6 -0
  41. package/registry/ui/empty-state/empty-state/collection-empty-state.ts +29 -0
  42. package/registry/ui/empty-state/empty-state/empty-state-card.tsx +72 -0
  43. package/registry/ui/empty-state/empty-state/index.ts +8 -0
  44. package/registry/ui/empty-state/empty-state/initial-empty-state.tsx +36 -0
  45. package/registry/ui/empty-state/empty-state/no-results-state.tsx +20 -0
  46. package/registry/ui/empty-state/manifest.json +63 -0
  47. package/registry/ui/empty-state/primitives/button.tsx +89 -0
  48. package/registry/ui/empty-state/primitives/card.tsx +80 -0
  49. package/registry/ui/empty-state/utils/cn.ts +6 -0
  50. package/registry/ui/error-page/error-page/error-code.tsx +16 -0
  51. package/registry/ui/error-page/error-page/error-page-content.ts +75 -0
  52. package/registry/ui/error-page/error-page/index.ts +19 -0
  53. package/registry/ui/error-page/error-page/posthog-error-capture.ts +83 -0
  54. package/registry/ui/error-page/error-page/saas-error-page.tsx +146 -0
  55. package/registry/ui/error-page/manifest.json +64 -0
  56. package/registry/ui/error-page/primitives/button.tsx +89 -0
  57. package/registry/ui/error-page/utils/cn.ts +6 -0
  58. package/registry/ui/field-detail-row/manifest.json +32 -0
  59. package/registry/ui/field-detail-row/primitives/field-detail-row.tsx +28 -0
  60. package/registry/ui/field-detail-row/utils/cn.ts +6 -0
  61. package/registry/ui/file-dropzone/manifest.json +35 -0
  62. package/registry/ui/file-dropzone/primitives/button.tsx +89 -0
  63. package/registry/ui/file-dropzone/primitives/file-dropzone.tsx +236 -0
  64. package/registry/ui/file-dropzone/utils/cn.ts +6 -0
  65. package/registry/ui/help-info-button/manifest.json +72 -0
  66. package/registry/ui/help-info-button/overlays/responsive-sheet.footer.tsx +88 -0
  67. package/registry/ui/help-info-button/overlays/responsive-sheet.layouts.tsx +207 -0
  68. package/registry/ui/help-info-button/overlays/responsive-sheet.shortcuts.ts +103 -0
  69. package/registry/ui/help-info-button/overlays/responsive-sheet.tsx +132 -0
  70. package/registry/ui/help-info-button/primitives/button.tsx +89 -0
  71. package/registry/ui/help-info-button/primitives/drawer.tsx +99 -0
  72. package/registry/ui/help-info-button/primitives/help-info-button.tsx +63 -0
  73. package/registry/ui/help-info-button/primitives/keyboard-shortcut-hint.tsx +40 -0
  74. package/registry/ui/help-info-button/primitives/sheet.tsx +103 -0
  75. package/registry/ui/help-info-button/primitives/tooltip.tsx +57 -0
  76. package/registry/ui/help-info-button/utils/cn.ts +6 -0
  77. package/registry/ui/help-info-button/utils/use-media-query.ts +28 -0
  78. package/registry/ui/input/manifest.json +31 -0
  79. package/registry/ui/input/primitives/input.tsx +19 -0
  80. package/registry/ui/input/utils/cn.ts +6 -0
  81. package/registry/ui/keyboard-shortcut-hint/manifest.json +32 -0
  82. package/registry/ui/keyboard-shortcut-hint/primitives/keyboard-shortcut-hint.tsx +40 -0
  83. package/registry/ui/keyboard-shortcut-hint/utils/cn.ts +6 -0
  84. package/registry/ui/label/manifest.json +31 -0
  85. package/registry/ui/label/primitives/label.tsx +21 -0
  86. package/registry/ui/label/utils/cn.ts +6 -0
  87. package/registry/ui/pagination/manifest.json +36 -0
  88. package/registry/ui/pagination/primitives/button.tsx +89 -0
  89. package/registry/ui/pagination/primitives/pagination.tsx +143 -0
  90. package/registry/ui/pagination/utils/cn.ts +6 -0
  91. package/registry/ui/popover/manifest.json +33 -0
  92. package/registry/ui/popover/primitives/popover.tsx +46 -0
  93. package/registry/ui/popover/utils/cn.ts +6 -0
  94. package/registry/ui/responsive-sheet/manifest.json +66 -0
  95. package/registry/ui/responsive-sheet/overlays/responsive-sheet.footer.tsx +88 -0
  96. package/registry/ui/responsive-sheet/overlays/responsive-sheet.layouts.tsx +207 -0
  97. package/registry/ui/responsive-sheet/overlays/responsive-sheet.shortcuts.ts +103 -0
  98. package/registry/ui/responsive-sheet/overlays/responsive-sheet.tsx +132 -0
  99. package/registry/ui/responsive-sheet/primitives/button.tsx +89 -0
  100. package/registry/ui/responsive-sheet/primitives/drawer.tsx +99 -0
  101. package/registry/ui/responsive-sheet/primitives/keyboard-shortcut-hint.tsx +40 -0
  102. package/registry/ui/responsive-sheet/primitives/sheet.tsx +103 -0
  103. package/registry/ui/responsive-sheet/utils/cn.ts +6 -0
  104. package/registry/ui/responsive-sheet/utils/use-media-query.ts +28 -0
  105. package/registry/ui/responsive-sheet.footer/manifest.json +40 -0
  106. package/registry/ui/responsive-sheet.footer/overlays/responsive-sheet.footer.tsx +88 -0
  107. package/registry/ui/responsive-sheet.footer/primitives/button.tsx +89 -0
  108. package/registry/ui/responsive-sheet.footer/primitives/keyboard-shortcut-hint.tsx +40 -0
  109. package/registry/ui/responsive-sheet.footer/utils/cn.ts +6 -0
  110. package/registry/ui/responsive-sheet.shortcuts/manifest.json +34 -0
  111. package/registry/ui/responsive-sheet.shortcuts/overlays/responsive-sheet.shortcuts.ts +103 -0
  112. package/registry/ui/responsive-sheet.shortcuts/utils/cn.ts +6 -0
  113. package/registry/ui/scroll-fade-area/manifest.json +31 -0
  114. package/registry/ui/scroll-fade-area/primitives/scroll-fade-area.tsx +295 -0
  115. package/registry/ui/scroll-fade-area/utils/cn.ts +6 -0
  116. package/registry/ui/search/manifest.json +35 -0
  117. package/registry/ui/search/utils/cn.ts +6 -0
  118. package/registry/ui/search/utils/search.ts +227 -0
  119. package/registry/ui/searchable-select/manifest.json +48 -0
  120. package/registry/ui/searchable-select/primitives/input.tsx +19 -0
  121. package/registry/ui/searchable-select/search/searchable-select-position.ts +95 -0
  122. package/registry/ui/searchable-select/search/searchable-select.tsx +431 -0
  123. package/registry/ui/searchable-select/utils/cn.ts +6 -0
  124. package/registry/ui/searchable-select/utils/search.ts +227 -0
  125. package/registry/ui/searchable-select-position/manifest.json +32 -0
  126. package/registry/ui/searchable-select-position/search/searchable-select-position.ts +95 -0
  127. package/registry/ui/searchable-select-position/utils/cn.ts +6 -0
  128. package/registry/ui/segmented-toggle/manifest.json +41 -0
  129. package/registry/ui/segmented-toggle/primitives/scroll-fade-area.tsx +295 -0
  130. package/registry/ui/segmented-toggle/primitives/segmented-toggle.tsx +106 -0
  131. package/registry/ui/segmented-toggle/primitives/tabs.tsx +97 -0
  132. package/registry/ui/segmented-toggle/utils/cn.ts +6 -0
  133. package/registry/ui/select/manifest.json +37 -0
  134. package/registry/ui/select/primitives/select.tsx +142 -0
  135. package/registry/ui/select/utils/cn.ts +6 -0
  136. package/registry/ui/sheet/manifest.json +39 -0
  137. package/registry/ui/sheet/primitives/button.tsx +89 -0
  138. package/registry/ui/sheet/primitives/sheet.tsx +103 -0
  139. package/registry/ui/sheet/utils/cn.ts +6 -0
  140. package/registry/ui/skeleton/manifest.json +31 -0
  141. package/registry/ui/skeleton/primitives/skeleton.tsx +13 -0
  142. package/registry/ui/skeleton/utils/cn.ts +6 -0
  143. package/registry/ui/smart-table/manifest.json +115 -0
  144. package/registry/ui/smart-table/primitives/button.tsx +89 -0
  145. package/registry/ui/smart-table/primitives/card.tsx +80 -0
  146. package/registry/ui/smart-table/primitives/display-date.tsx +20 -0
  147. package/registry/ui/smart-table/primitives/pagination.tsx +143 -0
  148. package/registry/ui/smart-table/primitives/skeleton.tsx +13 -0
  149. package/registry/ui/smart-table/primitives/table.tsx +92 -0
  150. package/registry/ui/smart-table/primitives/tooltip.tsx +57 -0
  151. package/registry/ui/smart-table/smart-table/DesktopView.tsx +343 -0
  152. package/registry/ui/smart-table/smart-table/MobileView.tsx +170 -0
  153. package/registry/ui/smart-table/smart-table/SmartTable.tsx +85 -0
  154. package/registry/ui/smart-table/smart-table/SmartTableActions.tsx +71 -0
  155. package/registry/ui/smart-table/smart-table/TruncatedContent.tsx +147 -0
  156. package/registry/ui/smart-table/smart-table/index.ts +15 -0
  157. package/registry/ui/smart-table/smart-table/sorting.ts +148 -0
  158. package/registry/ui/smart-table/smart-table/truncated-content.utils.ts +22 -0
  159. package/registry/ui/smart-table/smart-table/types.ts +95 -0
  160. package/registry/ui/smart-table/smart-table/utils.ts +150 -0
  161. package/registry/ui/smart-table/utils/cn.ts +6 -0
  162. package/registry/ui/smart-table/utils/date-display.ts +61 -0
  163. package/registry/ui/smart-table/utils/use-media-query.ts +28 -0
  164. package/registry/ui/switch/manifest.json +31 -0
  165. package/registry/ui/switch/primitives/switch.tsx +31 -0
  166. package/registry/ui/switch/utils/cn.ts +6 -0
  167. package/registry/ui/table/manifest.json +38 -0
  168. package/registry/ui/table/primitives/table.tsx +92 -0
  169. package/registry/ui/table/utils/cn.ts +6 -0
  170. package/registry/ui/table-toolbar/manifest.json +93 -0
  171. package/registry/ui/table-toolbar/overlays/responsive-sheet.footer.tsx +88 -0
  172. package/registry/ui/table-toolbar/overlays/responsive-sheet.layouts.tsx +207 -0
  173. package/registry/ui/table-toolbar/overlays/responsive-sheet.shortcuts.ts +103 -0
  174. package/registry/ui/table-toolbar/overlays/responsive-sheet.tsx +132 -0
  175. package/registry/ui/table-toolbar/primitives/button.tsx +89 -0
  176. package/registry/ui/table-toolbar/primitives/drawer.tsx +99 -0
  177. package/registry/ui/table-toolbar/primitives/input.tsx +19 -0
  178. package/registry/ui/table-toolbar/primitives/keyboard-shortcut-hint.tsx +40 -0
  179. package/registry/ui/table-toolbar/primitives/sheet.tsx +103 -0
  180. package/registry/ui/table-toolbar/search/searchable-select-position.ts +95 -0
  181. package/registry/ui/table-toolbar/search/searchable-select.tsx +431 -0
  182. package/registry/ui/table-toolbar/table-toolbar/index.ts +9 -0
  183. package/registry/ui/table-toolbar/table-toolbar/table-toolbar.tsx +552 -0
  184. package/registry/ui/table-toolbar/utils/cn.ts +6 -0
  185. package/registry/ui/table-toolbar/utils/search.ts +227 -0
  186. package/registry/ui/table-toolbar/utils/use-media-query.ts +28 -0
  187. package/registry/ui/tabs/manifest.json +40 -0
  188. package/registry/ui/tabs/primitives/scroll-fade-area.tsx +295 -0
  189. package/registry/ui/tabs/primitives/tabs.tsx +97 -0
  190. package/registry/ui/tabs/utils/cn.ts +6 -0
  191. package/registry/ui/textarea/manifest.json +31 -0
  192. package/registry/ui/textarea/primitives/textarea.tsx +18 -0
  193. package/registry/ui/textarea/utils/cn.ts +6 -0
  194. package/registry/ui/tooltip/manifest.json +34 -0
  195. package/registry/ui/tooltip/primitives/tooltip.tsx +57 -0
  196. package/registry/ui/tooltip/utils/cn.ts +6 -0
  197. package/registry/ui/use-media-query/manifest.json +32 -0
  198. package/registry/ui/use-media-query/utils/cn.ts +6 -0
  199. package/registry/ui/use-media-query/utils/use-media-query.ts +28 -0
  200. package/registry/ui/user-picker/manifest.json +52 -0
  201. package/registry/ui/user-picker/primitives/avatar.tsx +64 -0
  202. package/registry/ui/user-picker/primitives/button.tsx +89 -0
  203. package/registry/ui/user-picker/primitives/input.tsx +19 -0
  204. package/registry/ui/user-picker/primitives/popover.tsx +46 -0
  205. package/registry/ui/user-picker/primitives/user-picker-utils.ts +113 -0
  206. package/registry/ui/user-picker/primitives/user-picker.tsx +226 -0
  207. package/registry/ui/user-picker/utils/cn.ts +6 -0
  208. package/registry/ui/user-picker-utils/manifest.json +38 -0
  209. package/registry/ui/user-picker-utils/primitives/user-picker-utils.ts +113 -0
  210. package/registry/ui/user-picker-utils/utils/cn.ts +6 -0
  211. package/assets/hero.png +0 -0
  212. package/registry/ui/button/cn.ts +0 -6
  213. /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
+ }
@@ -0,0 +1,6 @@
1
+ import { clsx, type ClassValue } from "clsx"
2
+ import { twMerge } from "tailwind-merge"
3
+
4
+ export function cn(...inputs: ClassValue[]): string {
5
+ return twMerge(clsx(inputs))
6
+ }