@classytic/fluid 0.2.4 → 0.3.3
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/LICENSE +21 -0
- package/README.md +149 -62
- package/dist/api-pagination-CJ0vR_w6.d.mts +34 -0
- package/dist/api-pagination-DBTE0yk4.mjs +190 -0
- package/dist/chunk-DQk6qfdC.mjs +18 -0
- package/dist/client/calendar.d.mts +105 -0
- package/dist/client/calendar.mjs +202 -0
- package/dist/client/core.d.mts +1614 -0
- package/dist/client/core.mjs +2779 -0
- package/dist/client/error.d.mts +125 -0
- package/dist/client/error.mjs +166 -0
- package/dist/client/hooks.d.mts +162 -0
- package/dist/client/hooks.mjs +447 -0
- package/dist/client/table.d.mts +84 -0
- package/dist/client/table.mjs +373 -0
- package/dist/client/theme.d.mts +6 -0
- package/dist/client/theme.mjs +65 -0
- package/dist/command.d.mts +134 -0
- package/dist/command.mjs +132 -0
- package/dist/compact.d.mts +359 -0
- package/dist/compact.mjs +892 -0
- package/dist/dashboard.d.mts +778 -0
- package/dist/dashboard.mjs +1617 -0
- package/dist/filter-utils-DqMmy_v-.mjs +72 -0
- package/dist/filter-utils-IZ0GtuPo.d.mts +40 -0
- package/dist/forms.d.mts +1549 -0
- package/dist/forms.mjs +3740 -0
- package/dist/index.d.mts +296 -0
- package/dist/index.mjs +432 -0
- package/dist/layouts.d.mts +215 -0
- package/dist/layouts.mjs +460 -0
- package/dist/search-context-DR7DBs7S.mjs +19 -0
- package/dist/search.d.mts +254 -0
- package/dist/search.mjs +523 -0
- package/dist/sheet-wrapper-C13Y-Q6w.mjs +211 -0
- package/dist/use-base-search-BGgWnWaF.d.mts +35 -0
- package/dist/use-debounce-xmZucz5e.mjs +53 -0
- package/dist/use-keyboard-shortcut-Bl6YM5Q7.mjs +82 -0
- package/dist/use-keyboard-shortcut-_mRCh3QO.d.mts +24 -0
- package/dist/use-media-query-BnVNIKT4.mjs +17 -0
- package/dist/use-mobile-BX3SQVo2.mjs +20 -0
- package/dist/use-scroll-detection-CsgsQYvy.mjs +43 -0
- package/dist/utils-CDue7cEt.d.mts +6 -0
- package/dist/utils-DQ5SCVoW.mjs +10 -0
- package/package.json +85 -45
- package/styles.css +2 -2
- package/dist/chunk-GUHK2DTW.js +0 -15
- package/dist/chunk-GUHK2DTW.js.map +0 -1
- package/dist/chunk-H3NFL3GJ.js +0 -57
- package/dist/chunk-H3NFL3GJ.js.map +0 -1
- package/dist/chunk-J2YRTQE4.js +0 -293
- package/dist/chunk-J2YRTQE4.js.map +0 -1
- package/dist/compact.d.ts +0 -217
- package/dist/compact.js +0 -986
- package/dist/compact.js.map +0 -1
- package/dist/dashboard.d.ts +0 -387
- package/dist/dashboard.js +0 -1032
- package/dist/dashboard.js.map +0 -1
- package/dist/index.d.ts +0 -2140
- package/dist/index.js +0 -6422
- package/dist/index.js.map +0 -1
- package/dist/layout.d.ts +0 -25
- package/dist/layout.js +0 -4
- package/dist/layout.js.map +0 -1
- package/dist/search.d.ts +0 -172
- package/dist/search.js +0 -341
- package/dist/search.js.map +0 -1
- package/dist/use-base-search-AS5Z3SAy.d.ts +0 -64
- package/dist/utils-Cbsgs0XP.d.ts +0 -5
package/dist/search.mjs
ADDED
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
|
|
4
|
+
import { t as cn } from "./utils-DQ5SCVoW.mjs";
|
|
5
|
+
import { t as useIsMobile } from "./use-mobile-BX3SQVo2.mjs";
|
|
6
|
+
import { r as SheetWrapper } from "./sheet-wrapper-C13Y-Q6w.mjs";
|
|
7
|
+
import { n as useSearch, t as SearchProvider } from "./search-context-DR7DBs7S.mjs";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { useEffect, useRef, useState } from "react";
|
|
10
|
+
import { Filter, FilterX, RotateCcw, Search, X } from "lucide-react";
|
|
11
|
+
import { Button } from "@/components/ui/button";
|
|
12
|
+
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
|
|
13
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
14
|
+
import { Badge } from "@/components/ui/badge";
|
|
15
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
16
|
+
|
|
17
|
+
//#region src/components/search/search-root.tsx
|
|
18
|
+
/**
|
|
19
|
+
* Root search component that provides context to all child components
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* // Manual mode (default)
|
|
23
|
+
* <Search.Root hook={useMySearch()}>
|
|
24
|
+
* <Search.Input />
|
|
25
|
+
* <Search.Actions />
|
|
26
|
+
* </Search.Root>
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Auto-search mode — no Search button needed
|
|
30
|
+
* <Search.Root hook={useMySearch()} autoSearch>
|
|
31
|
+
* <Search.Input />
|
|
32
|
+
* <Search.ActiveFilters />
|
|
33
|
+
* </Search.Root>
|
|
34
|
+
*/
|
|
35
|
+
function SearchRoot({ children, hook, className, autoSearch = false, autoSearchDelay = 300 }) {
|
|
36
|
+
const handleSearchRef = useRef(hook.handleSearch);
|
|
37
|
+
handleSearchRef.current = hook.handleSearch;
|
|
38
|
+
const timerRef = useRef(void 0);
|
|
39
|
+
const isInitialMount = useRef(true);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!autoSearch) return;
|
|
42
|
+
if (isInitialMount.current) {
|
|
43
|
+
isInitialMount.current = false;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
47
|
+
timerRef.current = setTimeout(() => {
|
|
48
|
+
handleSearchRef.current();
|
|
49
|
+
}, autoSearchDelay);
|
|
50
|
+
return () => {
|
|
51
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
52
|
+
};
|
|
53
|
+
}, [
|
|
54
|
+
autoSearch,
|
|
55
|
+
autoSearchDelay,
|
|
56
|
+
hook.searchValue,
|
|
57
|
+
hook.searchType,
|
|
58
|
+
hook.filters
|
|
59
|
+
]);
|
|
60
|
+
return /* @__PURE__ */ jsx(SearchProvider, {
|
|
61
|
+
value: hook,
|
|
62
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
63
|
+
className: cn("w-full", className),
|
|
64
|
+
children
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
//#endregion
|
|
70
|
+
//#region src/components/search/search-input.tsx
|
|
71
|
+
/**
|
|
72
|
+
* Search input component using modern shadcn InputGroup pattern
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* <Search.Input placeholder="Search..." />
|
|
76
|
+
*/
|
|
77
|
+
function SearchInput({ placeholder = "Search...", className, disabled, showIcon = true, showClearButton = false, onKeyDown, ...props }) {
|
|
78
|
+
const { searchValue, setSearchValue, handleSearch } = useSearch();
|
|
79
|
+
const handleKeyDown = (event) => {
|
|
80
|
+
onKeyDown?.(event);
|
|
81
|
+
if (!event.defaultPrevented && event.key === "Enter" && !disabled) handleSearch?.();
|
|
82
|
+
};
|
|
83
|
+
return /* @__PURE__ */ jsxs(InputGroup, {
|
|
84
|
+
className: cn("h-10 flex-1 shadow-sm", className),
|
|
85
|
+
"data-disabled": disabled || void 0,
|
|
86
|
+
children: [
|
|
87
|
+
showIcon && /* @__PURE__ */ jsx(InputGroupAddon, {
|
|
88
|
+
align: "inline-start",
|
|
89
|
+
children: /* @__PURE__ */ jsx(Search, { className: "size-4" })
|
|
90
|
+
}),
|
|
91
|
+
/* @__PURE__ */ jsx(InputGroupInput, {
|
|
92
|
+
...props,
|
|
93
|
+
type: "text",
|
|
94
|
+
placeholder,
|
|
95
|
+
value: searchValue || "",
|
|
96
|
+
onChange: (e) => setSearchValue?.(e.target.value),
|
|
97
|
+
onKeyDown: handleKeyDown,
|
|
98
|
+
disabled,
|
|
99
|
+
className: "text-base"
|
|
100
|
+
}),
|
|
101
|
+
showClearButton && searchValue && /* @__PURE__ */ jsx(InputGroupAddon, {
|
|
102
|
+
align: "inline-end",
|
|
103
|
+
children: /* @__PURE__ */ jsx(InputGroupButton, {
|
|
104
|
+
variant: "ghost",
|
|
105
|
+
size: "icon-sm",
|
|
106
|
+
onClick: () => setSearchValue?.(""),
|
|
107
|
+
disabled,
|
|
108
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
]
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/components/search/search-type-input.tsx
|
|
117
|
+
/**
|
|
118
|
+
* Search input component with type selector using InputGroup pattern
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* <Search.TypeInput
|
|
122
|
+
* placeholder="Search..."
|
|
123
|
+
* searchTypeOptions={[
|
|
124
|
+
* { value: "_id", label: "ID" },
|
|
125
|
+
* { value: "customerPhone", label: "Phone" },
|
|
126
|
+
* { value: "customerEmail", label: "Email" },
|
|
127
|
+
* ]}
|
|
128
|
+
* />
|
|
129
|
+
*/
|
|
130
|
+
function SearchTypeInput({ placeholder = "Search...", className, disabled, showIcon = true, showClearButton = false, searchTypeOptions = [], onKeyDown, ...props }) {
|
|
131
|
+
const { searchValue, setSearchValue, searchType, setSearchType, handleSearch } = useSearch();
|
|
132
|
+
const handleKeyDown = (event) => {
|
|
133
|
+
onKeyDown?.(event);
|
|
134
|
+
if (!event.defaultPrevented && event.key === "Enter" && !disabled) handleSearch?.();
|
|
135
|
+
};
|
|
136
|
+
const selectedOption = searchTypeOptions.find((opt) => opt.value === searchType);
|
|
137
|
+
return /* @__PURE__ */ jsxs(InputGroup, {
|
|
138
|
+
className: cn("h-10 flex-1 shadow-sm", className),
|
|
139
|
+
"data-disabled": disabled || void 0,
|
|
140
|
+
children: [
|
|
141
|
+
showIcon && /* @__PURE__ */ jsx(InputGroupAddon, {
|
|
142
|
+
align: "inline-start",
|
|
143
|
+
children: /* @__PURE__ */ jsx(Search, { className: "size-4" })
|
|
144
|
+
}),
|
|
145
|
+
searchTypeOptions.length > 0 && /* @__PURE__ */ jsxs(InputGroupAddon, {
|
|
146
|
+
align: "inline-start",
|
|
147
|
+
className: "pr-0",
|
|
148
|
+
children: [/* @__PURE__ */ jsxs(Select, {
|
|
149
|
+
value: searchType,
|
|
150
|
+
onValueChange: setSearchType,
|
|
151
|
+
disabled,
|
|
152
|
+
children: [/* @__PURE__ */ jsx(SelectTrigger, {
|
|
153
|
+
className: "h-7 w-auto border-0 bg-transparent px-2 text-sm font-medium shadow-none focus:ring-0 focus-visible:ring-0 focus-visible:border-0",
|
|
154
|
+
children: /* @__PURE__ */ jsx(SelectValue, { children: selectedOption?.label || searchTypeOptions[0]?.label })
|
|
155
|
+
}), /* @__PURE__ */ jsx(SelectContent, { children: searchTypeOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, {
|
|
156
|
+
value: option.value,
|
|
157
|
+
children: option.label
|
|
158
|
+
}, option.value)) })]
|
|
159
|
+
}), /* @__PURE__ */ jsx("div", { className: "h-6 w-px bg-border ml-1" })]
|
|
160
|
+
}),
|
|
161
|
+
/* @__PURE__ */ jsx(InputGroupInput, {
|
|
162
|
+
...props,
|
|
163
|
+
type: "text",
|
|
164
|
+
placeholder,
|
|
165
|
+
value: searchValue || "",
|
|
166
|
+
onChange: (e) => setSearchValue?.(e.target.value),
|
|
167
|
+
onKeyDown: handleKeyDown,
|
|
168
|
+
disabled,
|
|
169
|
+
className: "text-base"
|
|
170
|
+
}),
|
|
171
|
+
showClearButton && searchValue && /* @__PURE__ */ jsx(InputGroupAddon, {
|
|
172
|
+
align: "inline-end",
|
|
173
|
+
children: /* @__PURE__ */ jsx(InputGroupButton, {
|
|
174
|
+
variant: "ghost",
|
|
175
|
+
size: "icon-sm",
|
|
176
|
+
onClick: () => setSearchValue?.(""),
|
|
177
|
+
disabled,
|
|
178
|
+
children: /* @__PURE__ */ jsx(X, { className: "size-4" })
|
|
179
|
+
})
|
|
180
|
+
})
|
|
181
|
+
]
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region src/components/search/search-filter-actions.tsx
|
|
187
|
+
/**
|
|
188
|
+
* Filter actions component (Reset/Apply buttons)
|
|
189
|
+
* Used inside filter popovers/sheets
|
|
190
|
+
*/
|
|
191
|
+
function SearchFilterActions({ onClose, disabled }) {
|
|
192
|
+
const { handleSearch, clearFilters, hasActiveFilters } = useSearch();
|
|
193
|
+
const handleApply = () => {
|
|
194
|
+
handleSearch?.();
|
|
195
|
+
onClose?.();
|
|
196
|
+
};
|
|
197
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
198
|
+
className: "flex gap-2 border-t bg-muted/30 p-4 pt-3 -mx-4 -mb-4",
|
|
199
|
+
children: [/* @__PURE__ */ jsx(Button, {
|
|
200
|
+
variant: "outline",
|
|
201
|
+
size: "sm",
|
|
202
|
+
onClick: clearFilters,
|
|
203
|
+
className: "flex-1",
|
|
204
|
+
disabled: disabled || !hasActiveFilters,
|
|
205
|
+
children: "Reset"
|
|
206
|
+
}), /* @__PURE__ */ jsxs(Button, {
|
|
207
|
+
size: "sm",
|
|
208
|
+
onClick: handleApply,
|
|
209
|
+
className: "flex-1",
|
|
210
|
+
disabled,
|
|
211
|
+
children: [/* @__PURE__ */ jsx(Search, {
|
|
212
|
+
size: 16,
|
|
213
|
+
className: "mr-2"
|
|
214
|
+
}), "Apply"]
|
|
215
|
+
})]
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
//#endregion
|
|
220
|
+
//#region src/components/search/search-filters.tsx
|
|
221
|
+
/**
|
|
222
|
+
* Search filters component with mobile sheet support
|
|
223
|
+
*
|
|
224
|
+
* @example
|
|
225
|
+
* <Search.Filters>
|
|
226
|
+
* <TagChoiceInput label="Category" ... />
|
|
227
|
+
* <SelectInput label="Status" ... />
|
|
228
|
+
* </Search.Filters>
|
|
229
|
+
*/
|
|
230
|
+
function SearchFilters({ children, title = "Filters", description = "Refine your search results", disabled, className }) {
|
|
231
|
+
const { hasActiveFilters, filters = {} } = useSearch();
|
|
232
|
+
const isMobile = useIsMobile();
|
|
233
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
234
|
+
const activeFilterCount = Object.values(filters).filter((value) => {
|
|
235
|
+
if (Array.isArray(value)) return value.filter(Boolean).length > 0;
|
|
236
|
+
if (typeof value === "string") return value.trim().length > 0 && value !== "all";
|
|
237
|
+
if (typeof value === "number") return value !== void 0 && value !== null && !Number.isNaN(value);
|
|
238
|
+
if (typeof value === "boolean") return value;
|
|
239
|
+
return Boolean(value);
|
|
240
|
+
}).length;
|
|
241
|
+
const FilterContent = () => /* @__PURE__ */ jsxs("div", {
|
|
242
|
+
className: "space-y-4",
|
|
243
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
244
|
+
className: "space-y-4",
|
|
245
|
+
children
|
|
246
|
+
}), /* @__PURE__ */ jsx(SearchFilterActions, { onClose: () => setIsOpen(false) })]
|
|
247
|
+
});
|
|
248
|
+
const triggerButton = /* @__PURE__ */ jsxs(Button, {
|
|
249
|
+
variant: "outline",
|
|
250
|
+
size: "default",
|
|
251
|
+
className: cn("relative h-10 w-10 shrink-0 px-0 sm:w-auto sm:px-4", hasActiveFilters && "bg-primary/10 border-primary hover:bg-primary/15"),
|
|
252
|
+
disabled,
|
|
253
|
+
"aria-label": "Open filters",
|
|
254
|
+
children: [
|
|
255
|
+
/* @__PURE__ */ jsx(Filter, { className: "size-4" }),
|
|
256
|
+
/* @__PURE__ */ jsx("span", {
|
|
257
|
+
className: "hidden sm:inline ml-2",
|
|
258
|
+
children: "Filters"
|
|
259
|
+
}),
|
|
260
|
+
activeFilterCount > 0 && /* @__PURE__ */ jsx(Badge, {
|
|
261
|
+
variant: "secondary",
|
|
262
|
+
className: "absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-xs font-medium text-primary-foreground",
|
|
263
|
+
children: activeFilterCount
|
|
264
|
+
})
|
|
265
|
+
]
|
|
266
|
+
});
|
|
267
|
+
if (isMobile) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
|
|
268
|
+
variant: "outline",
|
|
269
|
+
size: "default",
|
|
270
|
+
className: cn("relative h-10 w-10 shrink-0 px-0", hasActiveFilters && "bg-primary/10 border-primary hover:bg-primary/15"),
|
|
271
|
+
disabled,
|
|
272
|
+
"aria-label": "Open filters",
|
|
273
|
+
onClick: () => setIsOpen(true),
|
|
274
|
+
children: [/* @__PURE__ */ jsx(Filter, { className: "size-4" }), activeFilterCount > 0 && /* @__PURE__ */ jsx(Badge, {
|
|
275
|
+
variant: "secondary",
|
|
276
|
+
className: "absolute -right-1 -top-1 flex h-5 min-w-5 items-center justify-center rounded-full bg-primary px-1 text-xs font-medium text-primary-foreground",
|
|
277
|
+
children: activeFilterCount
|
|
278
|
+
})]
|
|
279
|
+
}), /* @__PURE__ */ jsx(SheetWrapper, {
|
|
280
|
+
open: isOpen,
|
|
281
|
+
onOpenChange: setIsOpen,
|
|
282
|
+
title,
|
|
283
|
+
description,
|
|
284
|
+
side: "bottom",
|
|
285
|
+
size: "default",
|
|
286
|
+
children: /* @__PURE__ */ jsx(FilterContent, {})
|
|
287
|
+
})] });
|
|
288
|
+
return /* @__PURE__ */ jsxs(Popover, {
|
|
289
|
+
open: isOpen,
|
|
290
|
+
onOpenChange: setIsOpen,
|
|
291
|
+
children: [/* @__PURE__ */ jsx(PopoverTrigger, { render: triggerButton }), /* @__PURE__ */ jsx(PopoverContent, {
|
|
292
|
+
className: "w-80 p-4 sm:w-96",
|
|
293
|
+
align: "end",
|
|
294
|
+
sideOffset: 8,
|
|
295
|
+
children: /* @__PURE__ */ jsx(FilterContent, {})
|
|
296
|
+
})]
|
|
297
|
+
});
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/components/search/search-actions.tsx
|
|
302
|
+
/**
|
|
303
|
+
* Search action buttons
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* // Combined clear (default)
|
|
307
|
+
* <Search.Actions />
|
|
308
|
+
*
|
|
309
|
+
* @example
|
|
310
|
+
* // Split clear — separate buttons for search text vs filters
|
|
311
|
+
* <Search.Actions clearMode="split" />
|
|
312
|
+
*/
|
|
313
|
+
function SearchActions({ showSearchButton = true, showClearButton = true, searchButtonText, clearButtonText = "Clear", clearMode = "combined", disabled, className }) {
|
|
314
|
+
const { handleSearch, clearSearch, clearSearchValue, clearFilters, searchValue, hasActiveFilters, hasActiveSearch } = useSearch();
|
|
315
|
+
const isMobile = useIsMobile();
|
|
316
|
+
const canSearch = Boolean(searchValue?.trim() || hasActiveFilters);
|
|
317
|
+
const canClear = Boolean(searchValue?.trim() || hasActiveFilters || hasActiveSearch);
|
|
318
|
+
const canClearSearch = Boolean(searchValue?.trim() || hasActiveSearch);
|
|
319
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
320
|
+
className: cn("flex items-center gap-2", className),
|
|
321
|
+
children: [
|
|
322
|
+
showClearButton && clearMode === "combined" && canClear && (isMobile ? /* @__PURE__ */ jsx(Button, {
|
|
323
|
+
variant: "outline",
|
|
324
|
+
size: "icon",
|
|
325
|
+
onClick: clearSearch,
|
|
326
|
+
"aria-label": clearButtonText,
|
|
327
|
+
title: clearButtonText,
|
|
328
|
+
className: "shrink-0",
|
|
329
|
+
children: /* @__PURE__ */ jsx(RotateCcw, {})
|
|
330
|
+
}) : /* @__PURE__ */ jsx(Button, {
|
|
331
|
+
variant: "outline",
|
|
332
|
+
size: "default",
|
|
333
|
+
onClick: clearSearch,
|
|
334
|
+
className: "h-10 shrink-0",
|
|
335
|
+
children: clearButtonText
|
|
336
|
+
})),
|
|
337
|
+
showClearButton && clearMode === "split" && /* @__PURE__ */ jsxs(Fragment, { children: [canClearSearch && /* @__PURE__ */ jsx(Button, {
|
|
338
|
+
variant: "outline",
|
|
339
|
+
size: isMobile ? "icon" : "default",
|
|
340
|
+
onClick: clearSearchValue,
|
|
341
|
+
"aria-label": "Clear search",
|
|
342
|
+
title: "Clear search",
|
|
343
|
+
className: cn("shrink-0", !isMobile && "h-10"),
|
|
344
|
+
children: isMobile ? /* @__PURE__ */ jsx(RotateCcw, {}) : "Clear search"
|
|
345
|
+
}), hasActiveFilters && /* @__PURE__ */ jsx(Button, {
|
|
346
|
+
variant: "outline",
|
|
347
|
+
size: isMobile ? "icon" : "default",
|
|
348
|
+
onClick: clearFilters,
|
|
349
|
+
"aria-label": "Clear filters",
|
|
350
|
+
title: "Clear filters",
|
|
351
|
+
className: cn("shrink-0", !isMobile && "h-10"),
|
|
352
|
+
children: isMobile ? /* @__PURE__ */ jsx(FilterX, {}) : "Clear filters"
|
|
353
|
+
})] }),
|
|
354
|
+
showSearchButton && /* @__PURE__ */ jsxs(Button, {
|
|
355
|
+
size: "default",
|
|
356
|
+
onClick: handleSearch,
|
|
357
|
+
disabled: disabled || !canSearch,
|
|
358
|
+
className: "h-10 shrink-0 px-3 sm:px-4",
|
|
359
|
+
children: [/* @__PURE__ */ jsx(Search, {
|
|
360
|
+
size: 16,
|
|
361
|
+
className: cn(searchButtonText ? "mr-2" : "sm:mr-2")
|
|
362
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
363
|
+
className: "hidden sm:inline",
|
|
364
|
+
children: searchButtonText || "Search"
|
|
365
|
+
})]
|
|
366
|
+
})
|
|
367
|
+
]
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/components/search/search-container.tsx
|
|
373
|
+
/**
|
|
374
|
+
* Container for search input and action buttons
|
|
375
|
+
* Provides responsive layout
|
|
376
|
+
*
|
|
377
|
+
* @example
|
|
378
|
+
* <Search.Container>
|
|
379
|
+
* <Search.Input />
|
|
380
|
+
* <Search.Filters />
|
|
381
|
+
* <Search.Actions />
|
|
382
|
+
* </Search.Container>
|
|
383
|
+
*/
|
|
384
|
+
function SearchContainer({ children, className }) {
|
|
385
|
+
return /* @__PURE__ */ jsx("div", {
|
|
386
|
+
className: cn("flex items-center gap-2 flex-1", className),
|
|
387
|
+
children
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
//#endregion
|
|
392
|
+
//#region src/components/search/search-active-filters.tsx
|
|
393
|
+
function capitalize(s) {
|
|
394
|
+
return s.charAt(0).toUpperCase() + s.replace(/([A-Z])/g, " $1").slice(1);
|
|
395
|
+
}
|
|
396
|
+
function isFilterActive(value, defaultValue) {
|
|
397
|
+
if (value === defaultValue) return false;
|
|
398
|
+
if (Array.isArray(value)) return value.filter(Boolean).length > 0;
|
|
399
|
+
if (typeof value === "string") return value.trim().length > 0 && value !== "all";
|
|
400
|
+
if (typeof value === "boolean") return value;
|
|
401
|
+
return value != null;
|
|
402
|
+
}
|
|
403
|
+
function formatFilterValue(value, valueLabelMap) {
|
|
404
|
+
if (Array.isArray(value)) {
|
|
405
|
+
const mapped = value.map((v) => valueLabelMap?.[String(v)] ?? String(v));
|
|
406
|
+
return mapped.length <= 3 ? mapped.join(", ") : `${mapped.length} selected`;
|
|
407
|
+
}
|
|
408
|
+
if (typeof value === "boolean") return value ? "Yes" : "No";
|
|
409
|
+
const str = String(value);
|
|
410
|
+
return valueLabelMap?.[str] ?? str;
|
|
411
|
+
}
|
|
412
|
+
/**
|
|
413
|
+
* Renders active filters as removable pills below the search bar.
|
|
414
|
+
*
|
|
415
|
+
* @example
|
|
416
|
+
* ```tsx
|
|
417
|
+
* <Search.Root hook={searchHook}>
|
|
418
|
+
* <Search.Container>
|
|
419
|
+
* <Search.Input />
|
|
420
|
+
* <Search.Filters>{...}</Search.Filters>
|
|
421
|
+
* <Search.Actions />
|
|
422
|
+
* </Search.Container>
|
|
423
|
+
* <Search.ActiveFilters
|
|
424
|
+
* labels={{ status: "Status", role: "Role" }}
|
|
425
|
+
* valueLabels={{ status: { active: "Active", inactive: "Inactive" } }}
|
|
426
|
+
* />
|
|
427
|
+
* </Search.Root>
|
|
428
|
+
* ```
|
|
429
|
+
*/
|
|
430
|
+
function SearchActiveFilters({ labels, valueLabels, formatValue: customFormat, showClearAll = true, className }) {
|
|
431
|
+
const { filters, filterFields, removeFilter, clearFilters, hasActiveFilters } = useSearch();
|
|
432
|
+
if (!hasActiveFilters) return null;
|
|
433
|
+
const activePills = [];
|
|
434
|
+
for (const [key, value] of Object.entries(filters)) {
|
|
435
|
+
const config = filterFields[key];
|
|
436
|
+
if (!config) continue;
|
|
437
|
+
if (!isFilterActive(value, config.defaultValue ?? (config.type === "array" ? [] : ""))) continue;
|
|
438
|
+
const label = labels?.[key] ?? config.label ?? capitalize(key);
|
|
439
|
+
let display;
|
|
440
|
+
if (customFormat) {
|
|
441
|
+
display = customFormat(key, value);
|
|
442
|
+
if (display === null) continue;
|
|
443
|
+
} else display = formatFilterValue(value, valueLabels?.[key]);
|
|
444
|
+
activePills.push({
|
|
445
|
+
key,
|
|
446
|
+
label,
|
|
447
|
+
display
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
if (activePills.length === 0) return null;
|
|
451
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
452
|
+
className: cn("flex flex-wrap items-center gap-1.5", className),
|
|
453
|
+
children: [activePills.map(({ key, label, display }) => /* @__PURE__ */ jsxs(Badge, {
|
|
454
|
+
variant: "secondary",
|
|
455
|
+
className: "gap-1 pl-2 pr-1 py-0.5 text-xs font-normal",
|
|
456
|
+
children: [
|
|
457
|
+
/* @__PURE__ */ jsxs("span", {
|
|
458
|
+
className: "font-medium",
|
|
459
|
+
children: [label, ":"]
|
|
460
|
+
}),
|
|
461
|
+
/* @__PURE__ */ jsx("span", {
|
|
462
|
+
className: "max-w-[120px] truncate",
|
|
463
|
+
children: display
|
|
464
|
+
}),
|
|
465
|
+
/* @__PURE__ */ jsx("button", {
|
|
466
|
+
type: "button",
|
|
467
|
+
onClick: () => removeFilter(key),
|
|
468
|
+
className: "ml-0.5 rounded-sm p-0.5 hover:bg-muted-foreground/20",
|
|
469
|
+
"aria-label": `Remove ${label} filter`,
|
|
470
|
+
children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
|
|
471
|
+
})
|
|
472
|
+
]
|
|
473
|
+
}, key)), showClearAll && activePills.length > 1 && /* @__PURE__ */ jsx(Button, {
|
|
474
|
+
variant: "ghost",
|
|
475
|
+
size: "sm",
|
|
476
|
+
onClick: clearFilters,
|
|
477
|
+
className: "h-6 px-2 text-xs text-muted-foreground",
|
|
478
|
+
children: "Clear all"
|
|
479
|
+
})]
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
//#endregion
|
|
484
|
+
//#region src/components/search/index.ts
|
|
485
|
+
var search_exports = /* @__PURE__ */ __exportAll({
|
|
486
|
+
Actions: () => SearchActions,
|
|
487
|
+
ActiveFilters: () => SearchActiveFilters,
|
|
488
|
+
Container: () => SearchContainer,
|
|
489
|
+
FilterActions: () => SearchFilterActions,
|
|
490
|
+
Filters: () => SearchFilters,
|
|
491
|
+
Input: () => SearchInput,
|
|
492
|
+
Root: () => SearchRoot,
|
|
493
|
+
SearchProvider: () => SearchProvider,
|
|
494
|
+
TypeInput: () => SearchTypeInput,
|
|
495
|
+
useSearch: () => useSearch
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
//#endregion
|
|
499
|
+
//#region src/search.ts
|
|
500
|
+
/**
|
|
501
|
+
* @classytic/fluid/search
|
|
502
|
+
* Composable search component system with filters and actions
|
|
503
|
+
*
|
|
504
|
+
* @example
|
|
505
|
+
* ```tsx
|
|
506
|
+
* import { Search, SearchProvider, useSearch } from "@classytic/fluid/search";
|
|
507
|
+
*
|
|
508
|
+
* function MySearch() {
|
|
509
|
+
* return (
|
|
510
|
+
* <Search.Root hook={searchHook}>
|
|
511
|
+
* <Search.Container>
|
|
512
|
+
* <Search.Input placeholder="Search..." />
|
|
513
|
+
* <Search.Filters>{...}</Search.Filters>
|
|
514
|
+
* <Search.Actions />
|
|
515
|
+
* </Search.Container>
|
|
516
|
+
* </Search.Root>
|
|
517
|
+
* );
|
|
518
|
+
* }
|
|
519
|
+
* ```
|
|
520
|
+
*/
|
|
521
|
+
|
|
522
|
+
//#endregion
|
|
523
|
+
export { search_exports as Search, SearchProvider, useSearch };
|