@classytic/fluid 0.4.1 → 0.5.0

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 (51) hide show
  1. package/README.md +21 -1
  2. package/dist/client/calendar.d.mts +1 -2
  3. package/dist/client/calendar.mjs +4 -4
  4. package/dist/client/color-picker.d.mts +94 -0
  5. package/dist/client/color-picker.mjs +392 -0
  6. package/dist/client/core.d.mts +243 -557
  7. package/dist/client/core.mjs +351 -1462
  8. package/dist/client/error.d.mts +41 -41
  9. package/dist/client/error.mjs +35 -35
  10. package/dist/client/gallery.d.mts +175 -0
  11. package/dist/client/gallery.mjs +546 -0
  12. package/dist/client/hooks.d.mts +57 -39
  13. package/dist/client/hooks.mjs +29 -7
  14. package/dist/client/spreadsheet.d.mts +30 -27
  15. package/dist/client/spreadsheet.mjs +80 -80
  16. package/dist/client/table.d.mts +66 -33
  17. package/dist/client/table.mjs +87 -54
  18. package/dist/client/theme.mjs +1 -1
  19. package/dist/command.d.mts +6 -4
  20. package/dist/command.mjs +3 -3
  21. package/dist/compact.d.mts +97 -95
  22. package/dist/compact.mjs +336 -322
  23. package/dist/dashboard.d.mts +614 -422
  24. package/dist/dashboard.mjs +1051 -762
  25. package/dist/{dropdown-wrapper-B86u9Fri.mjs → dropdown-wrapper-B9nRDUlz.mjs} +25 -35
  26. package/dist/forms.d.mts +1037 -972
  27. package/dist/forms.mjs +2849 -2721
  28. package/dist/index.d.mts +218 -152
  29. package/dist/index.mjs +357 -264
  30. package/dist/layouts.d.mts +94 -94
  31. package/dist/layouts.mjs +115 -110
  32. package/dist/phone-input-B9_XPNvv.mjs +429 -0
  33. package/dist/phone-input-CLH_UjQZ.d.mts +31 -0
  34. package/dist/{search-context-DR7DBs7S.mjs → search-context-1g3ZmOvx.mjs} +1 -1
  35. package/dist/search.d.mts +168 -164
  36. package/dist/search.mjs +305 -301
  37. package/dist/{sheet-wrapper-C13Y-Q6w.mjs → sheet-wrapper-B2uxookb.mjs} +1 -1
  38. package/dist/timeline-Bgu1mIe9.d.mts +373 -0
  39. package/dist/timeline-HJtWf4Op.mjs +804 -0
  40. package/dist/{use-base-search-BGgWnWaF.d.mts → use-base-search-DFC4QKYU.d.mts} +1 -1
  41. package/dist/{use-media-query-BnVNIKT4.mjs → use-media-query-ChLfFChU.mjs} +6 -7
  42. package/package.json +10 -2
  43. /package/dist/{api-pagination-CJ0vR_w6.d.mts → api-pagination-C30ser2L.d.mts} +0 -0
  44. /package/dist/{filter-utils-DqMmy_v-.mjs → filter-utils-BGIvtq1R.mjs} +0 -0
  45. /package/dist/{filter-utils-IZ0GtuPo.d.mts → filter-utils-DOFTBWm1.d.mts} +0 -0
  46. /package/dist/{use-debounce-xmZucz5e.mjs → use-debounce-BNoNiEon.mjs} +0 -0
  47. /package/dist/{use-keyboard-shortcut-Bl6YM5Q7.mjs → use-keyboard-shortcut-C_Vk-36P.mjs} +0 -0
  48. /package/dist/{use-keyboard-shortcut-_mRCh3QO.d.mts → use-keyboard-shortcut-Q4CSPzSI.d.mts} +0 -0
  49. /package/dist/{use-mobile-BX3SQVo2.mjs → use-mobile-CnEmFiQx.mjs} +0 -0
  50. /package/dist/{use-scroll-detection-CsgsQYvy.mjs → use-scroll-detection-BKfqkmEC.mjs} +0 -0
  51. /package/dist/{utils-CDue7cEt.d.mts → utils-rqvYP1by.d.mts} +0 -0
package/dist/search.mjs CHANGED
@@ -2,183 +2,197 @@
2
2
 
3
3
  import { t as __exportAll } from "./chunk-DQk6qfdC.mjs";
4
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";
5
+ import { t as useIsMobile } from "./use-mobile-CnEmFiQx.mjs";
6
+ import { r as SheetWrapper } from "./sheet-wrapper-B2uxookb.mjs";
7
+ import { n as useSearch, t as SearchProvider } from "./search-context-1g3ZmOvx.mjs";
9
8
  import { useEffect, useRef, useState } from "react";
9
+ import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
10
10
  import { Filter, FilterX, RotateCcw, Search, X } from "lucide-react";
11
11
  import { Button } from "@/components/ui/button";
12
+ import { Badge } from "@/components/ui/badge";
12
13
  import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group";
13
14
  import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
14
- import { Badge } from "@/components/ui/badge";
15
15
  import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
16
16
 
17
- //#region src/components/search/search-root.tsx
17
+ //#region src/components/search/search-actions.tsx
18
18
  /**
19
- * Root search component that provides context to all child components
19
+ * Search action buttons
20
20
  *
21
21
  * @example
22
- * // Manual mode (default)
23
- * <Search.Root hook={useMySearch()}>
24
- * <Search.Input />
25
- * <Search.Actions />
26
- * </Search.Root>
22
+ * // Combined clear (default)
23
+ * <Search.Actions />
27
24
  *
28
25
  * @example
29
- * // Auto-search modeno Search button needed
30
- * <Search.Root hook={useMySearch()} autoSearch>
31
- * <Search.Input />
32
- * <Search.ActiveFilters />
33
- * </Search.Root>
26
+ * // Split clearseparate buttons for search text vs filters
27
+ * <Search.Actions clearMode="split" />
34
28
  */
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
- })
29
+ function SearchActions({ showSearchButton = true, showClearButton = true, searchButtonText, clearButtonText = "Clear", clearMode = "combined", disabled, className }) {
30
+ const { handleSearch, clearSearch, clearSearchValue, clearFilters, searchValue, hasActiveFilters, hasActiveSearch } = useSearch();
31
+ const isMobile = useIsMobile();
32
+ const canSearch = Boolean(searchValue?.trim() || hasActiveFilters);
33
+ const canClear = Boolean(searchValue?.trim() || hasActiveFilters || hasActiveSearch);
34
+ const canClearSearch = Boolean(searchValue?.trim() || hasActiveSearch);
35
+ return /* @__PURE__ */ jsxs("div", {
36
+ className: cn("flex items-center gap-2", className),
37
+ children: [
38
+ showClearButton && clearMode === "combined" && canClear && (isMobile ? /* @__PURE__ */ jsx(Button, {
39
+ variant: "outline",
40
+ size: "icon",
41
+ onClick: clearSearch,
42
+ "aria-label": clearButtonText,
43
+ title: clearButtonText,
44
+ className: "shrink-0",
45
+ children: /* @__PURE__ */ jsx(RotateCcw, {})
46
+ }) : /* @__PURE__ */ jsx(Button, {
47
+ variant: "outline",
48
+ size: "default",
49
+ onClick: clearSearch,
50
+ className: "h-10 shrink-0",
51
+ children: clearButtonText
52
+ })),
53
+ showClearButton && clearMode === "split" && /* @__PURE__ */ jsxs(Fragment$1, { children: [canClearSearch && /* @__PURE__ */ jsx(Button, {
54
+ variant: "outline",
55
+ size: isMobile ? "icon" : "default",
56
+ onClick: clearSearchValue,
57
+ "aria-label": "Clear search",
58
+ title: "Clear search",
59
+ className: cn("shrink-0", !isMobile && "h-10"),
60
+ children: isMobile ? /* @__PURE__ */ jsx(RotateCcw, {}) : "Clear search"
61
+ }), hasActiveFilters && /* @__PURE__ */ jsx(Button, {
62
+ variant: "outline",
63
+ size: isMobile ? "icon" : "default",
64
+ onClick: clearFilters,
65
+ "aria-label": "Clear filters",
66
+ title: "Clear filters",
67
+ className: cn("shrink-0", !isMobile && "h-10"),
68
+ children: isMobile ? /* @__PURE__ */ jsx(FilterX, {}) : "Clear filters"
69
+ })] }),
70
+ showSearchButton && /* @__PURE__ */ jsxs(Button, {
71
+ size: "default",
72
+ onClick: handleSearch,
73
+ disabled: disabled || !canSearch,
74
+ className: "h-10 shrink-0 px-3 sm:px-4",
75
+ children: [/* @__PURE__ */ jsx(Search, {
76
+ size: 16,
77
+ className: cn(searchButtonText ? "mr-2" : "sm:mr-2")
78
+ }), /* @__PURE__ */ jsx("span", {
79
+ className: "hidden sm:inline",
80
+ children: searchButtonText || "Search"
81
+ })]
82
+ })
83
+ ]
66
84
  });
67
85
  }
68
86
 
69
87
  //#endregion
70
- //#region src/components/search/search-input.tsx
88
+ //#region src/components/search/search-active-filters.tsx
89
+ function capitalize(s) {
90
+ return s.charAt(0).toUpperCase() + s.replace(/([A-Z])/g, " $1").slice(1);
91
+ }
92
+ function isFilterActive(value, defaultValue) {
93
+ if (value === defaultValue) return false;
94
+ if (Array.isArray(value)) return value.filter(Boolean).length > 0;
95
+ if (typeof value === "string") return value.trim().length > 0 && value !== "all";
96
+ if (typeof value === "boolean") return value;
97
+ return value != null;
98
+ }
99
+ function formatFilterValue(value, valueLabelMap) {
100
+ if (Array.isArray(value)) {
101
+ const mapped = value.map((v) => valueLabelMap?.[String(v)] ?? String(v));
102
+ return mapped.length <= 3 ? mapped.join(", ") : `${mapped.length} selected`;
103
+ }
104
+ if (typeof value === "boolean") return value ? "Yes" : "No";
105
+ const str = String(value);
106
+ return valueLabelMap?.[str] ?? str;
107
+ }
71
108
  /**
72
- * Search input component using modern shadcn InputGroup pattern
109
+ * Renders active filters as removable pills below the search bar.
73
110
  *
74
111
  * @example
75
- * <Search.Input placeholder="Search..." />
112
+ * ```tsx
113
+ * <Search.Root hook={searchHook}>
114
+ * <Search.Container>
115
+ * <Search.Input />
116
+ * <Search.Filters>{...}</Search.Filters>
117
+ * <Search.Actions />
118
+ * </Search.Container>
119
+ * <Search.ActiveFilters
120
+ * labels={{ status: "Status", role: "Role" }}
121
+ * valueLabels={{ status: { active: "Active", inactive: "Inactive" } }}
122
+ * />
123
+ * </Search.Root>
124
+ * ```
76
125
  */
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" })
126
+ function SearchActiveFilters({ labels, valueLabels, formatValue: customFormat, showClearAll = true, className }) {
127
+ const { filters, filterFields, removeFilter, clearFilters, hasActiveFilters } = useSearch();
128
+ if (!hasActiveFilters) return null;
129
+ const activePills = [];
130
+ for (const [key, value] of Object.entries(filters)) {
131
+ const config = filterFields[key];
132
+ if (!config) continue;
133
+ if (!isFilterActive(value, config.defaultValue ?? (config.type === "array" ? [] : ""))) continue;
134
+ const label = labels?.[key] ?? config.label ?? capitalize(key);
135
+ let display;
136
+ if (customFormat) {
137
+ display = customFormat(key, value);
138
+ if (display === null) continue;
139
+ } else display = formatFilterValue(value, valueLabels?.[key]);
140
+ activePills.push({
141
+ key,
142
+ label,
143
+ display
144
+ });
145
+ }
146
+ if (activePills.length === 0) return null;
147
+ return /* @__PURE__ */ jsxs("div", {
148
+ className: cn("flex flex-wrap items-center gap-1.5", className),
149
+ children: [activePills.map(({ key, label, display }) => /* @__PURE__ */ jsxs(Badge, {
150
+ variant: "secondary",
151
+ className: "gap-1 pl-2 pr-1 py-0.5 text-xs font-normal",
152
+ children: [
153
+ /* @__PURE__ */ jsxs("span", {
154
+ className: "font-medium",
155
+ children: [label, ":"]
156
+ }),
157
+ /* @__PURE__ */ jsx("span", {
158
+ className: "max-w-[120px] truncate",
159
+ children: display
160
+ }),
161
+ /* @__PURE__ */ jsx("button", {
162
+ type: "button",
163
+ onClick: () => removeFilter(key),
164
+ className: "ml-0.5 rounded-sm p-0.5 hover:bg-muted-foreground/20",
165
+ "aria-label": `Remove ${label} filter`,
166
+ children: /* @__PURE__ */ jsx(X, { className: "h-3 w-3" })
109
167
  })
110
- })
111
- ]
168
+ ]
169
+ }, key)), showClearAll && activePills.length > 1 && /* @__PURE__ */ jsx(Button, {
170
+ variant: "ghost",
171
+ size: "sm",
172
+ onClick: clearFilters,
173
+ className: "h-6 px-2 text-xs text-muted-foreground",
174
+ children: "Clear all"
175
+ })]
112
176
  });
113
177
  }
114
178
 
115
179
  //#endregion
116
- //#region src/components/search/search-type-input.tsx
180
+ //#region src/components/search/search-container.tsx
117
181
  /**
118
- * Search input component with type selector using InputGroup pattern
182
+ * Container for search input and action buttons
183
+ * Provides responsive layout
119
184
  *
120
185
  * @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
- * />
186
+ * <Search.Container>
187
+ * <Search.Input />
188
+ * <Search.Filters />
189
+ * <Search.Actions />
190
+ * </Search.Container>
129
191
  */
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
- ]
192
+ function SearchContainer({ children, className }) {
193
+ return /* @__PURE__ */ jsx("div", {
194
+ className: cn("flex items-center gap-2 flex-1", className),
195
+ children
182
196
  });
183
197
  }
184
198
 
@@ -264,7 +278,7 @@ function SearchFilters({ children, title = "Filters", description = "Refine your
264
278
  })
265
279
  ]
266
280
  });
267
- if (isMobile) return /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsxs(Button, {
281
+ if (isMobile) return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsxs(Button, {
268
282
  variant: "outline",
269
283
  size: "default",
270
284
  className: cn("relative h-10 w-10 shrink-0 px-0", hasActiveFilters && "bg-primary/10 border-primary hover:bg-primary/15"),
@@ -298,185 +312,175 @@ function SearchFilters({ children, title = "Filters", description = "Refine your
298
312
  }
299
313
 
300
314
  //#endregion
301
- //#region src/components/search/search-actions.tsx
315
+ //#region src/components/search/search-input.tsx
302
316
  /**
303
- * Search action buttons
304
- *
305
- * @example
306
- * // Combined clear (default)
307
- * <Search.Actions />
317
+ * Search input component using modern shadcn InputGroup pattern
308
318
  *
309
319
  * @example
310
- * // Split clear — separate buttons for search text vs filters
311
- * <Search.Actions clearMode="split" />
320
+ * <Search.Input placeholder="Search..." />
312
321
  */
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),
322
+ function SearchInput({ placeholder = "Search...", className, inputClassName, disabled, showIcon = true, showClearButton = false, onKeyDown, ...props }) {
323
+ const { searchValue, setSearchValue, handleSearch } = useSearch();
324
+ const handleKeyDown = (event) => {
325
+ onKeyDown?.(event);
326
+ if (!event.defaultPrevented && event.key === "Enter" && !disabled) handleSearch?.();
327
+ };
328
+ return /* @__PURE__ */ jsxs(InputGroup, {
329
+ className: cn("h-10 flex-1 shadow-sm", className),
330
+ "data-disabled": disabled || void 0,
321
331
  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
- })]
332
+ showIcon && /* @__PURE__ */ jsx(InputGroupAddon, {
333
+ align: "inline-start",
334
+ children: /* @__PURE__ */ jsx(Search, { className: "size-4" })
335
+ }),
336
+ /* @__PURE__ */ jsx(InputGroupInput, {
337
+ ...props,
338
+ type: "text",
339
+ placeholder,
340
+ value: searchValue || "",
341
+ onChange: (e) => setSearchValue?.(e.target.value),
342
+ onKeyDown: handleKeyDown,
343
+ disabled,
344
+ className: cn("text-base", inputClassName)
345
+ }),
346
+ showClearButton && searchValue && /* @__PURE__ */ jsx(InputGroupAddon, {
347
+ align: "inline-end",
348
+ children: /* @__PURE__ */ jsx(InputGroupButton, {
349
+ variant: "ghost",
350
+ size: "icon-sm",
351
+ onClick: () => setSearchValue?.(""),
352
+ disabled,
353
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
354
+ })
366
355
  })
367
356
  ]
368
357
  });
369
358
  }
370
359
 
371
360
  //#endregion
372
- //#region src/components/search/search-container.tsx
361
+ //#region src/components/search/search-root.tsx
373
362
  /**
374
- * Container for search input and action buttons
375
- * Provides responsive layout
363
+ * Root search component that provides context to all child components
376
364
  *
377
365
  * @example
378
- * <Search.Container>
366
+ * // Manual mode (default)
367
+ * <Search.Root hook={useMySearch()}>
379
368
  * <Search.Input />
380
- * <Search.Filters />
381
369
  * <Search.Actions />
382
- * </Search.Container>
370
+ * </Search.Root>
371
+ *
372
+ * @example
373
+ * // Auto-search mode — no Search button needed
374
+ * <Search.Root hook={useMySearch()} autoSearch>
375
+ * <Search.Input />
376
+ * <Search.ActiveFilters />
377
+ * </Search.Root>
383
378
  */
384
- function SearchContainer({ children, className }) {
385
- return /* @__PURE__ */ jsx("div", {
386
- className: cn("flex items-center gap-2 flex-1", className),
387
- children
379
+ function SearchRoot({ children, hook, className, autoSearch = false, autoSearchDelay = 300 }) {
380
+ const handleSearchRef = useRef(hook.handleSearch);
381
+ handleSearchRef.current = hook.handleSearch;
382
+ const timerRef = useRef(void 0);
383
+ const isInitialMount = useRef(true);
384
+ useEffect(() => {
385
+ if (!autoSearch) return () => {
386
+ if (timerRef.current) clearTimeout(timerRef.current);
387
+ };
388
+ if (isInitialMount.current) {
389
+ isInitialMount.current = false;
390
+ return () => {
391
+ if (timerRef.current) clearTimeout(timerRef.current);
392
+ };
393
+ }
394
+ if (timerRef.current) clearTimeout(timerRef.current);
395
+ timerRef.current = setTimeout(() => {
396
+ handleSearchRef.current();
397
+ }, autoSearchDelay);
398
+ return () => {
399
+ if (timerRef.current) clearTimeout(timerRef.current);
400
+ };
401
+ }, [
402
+ autoSearch,
403
+ autoSearchDelay,
404
+ hook.searchValue,
405
+ hook.searchType,
406
+ hook.filters
407
+ ]);
408
+ return /* @__PURE__ */ jsx(SearchProvider, {
409
+ value: hook,
410
+ children: /* @__PURE__ */ jsx("div", {
411
+ className: cn("w-full", className),
412
+ children
413
+ })
388
414
  });
389
415
  }
390
416
 
391
417
  //#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
- }
418
+ //#region src/components/search/search-type-input.tsx
412
419
  /**
413
- * Renders active filters as removable pills below the search bar.
420
+ * Search input component with type selector using InputGroup pattern
414
421
  *
415
422
  * @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
- * ```
423
+ * <Search.TypeInput
424
+ * placeholder="Search..."
425
+ * searchTypeOptions={[
426
+ * { value: "_id", label: "ID" },
427
+ * { value: "customerPhone", label: "Phone" },
428
+ * { value: "customerEmail", label: "Email" },
429
+ * ]}
430
+ * />
429
431
  */
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" })
432
+ function SearchTypeInput({ placeholder = "Search...", className, inputClassName, disabled, showIcon = true, showClearButton = false, searchTypeOptions = [], onKeyDown, ...props }) {
433
+ const { searchValue, setSearchValue, searchType, setSearchType, handleSearch } = useSearch();
434
+ const handleKeyDown = (event) => {
435
+ onKeyDown?.(event);
436
+ if (!event.defaultPrevented && event.key === "Enter" && !disabled) handleSearch?.();
437
+ };
438
+ const selectedOption = searchTypeOptions.find((opt) => opt.value === searchType);
439
+ return /* @__PURE__ */ jsxs(InputGroup, {
440
+ className: cn("h-10 flex-1 shadow-sm", className),
441
+ "data-disabled": disabled || void 0,
442
+ children: [
443
+ showIcon && /* @__PURE__ */ jsx(InputGroupAddon, {
444
+ align: "inline-start",
445
+ children: /* @__PURE__ */ jsx(Search, { className: "size-4" })
446
+ }),
447
+ searchTypeOptions.length > 0 && /* @__PURE__ */ jsxs(InputGroupAddon, {
448
+ align: "inline-start",
449
+ className: "pr-0",
450
+ children: [/* @__PURE__ */ jsxs(Select, {
451
+ value: searchType,
452
+ onValueChange: setSearchType,
453
+ disabled,
454
+ children: [/* @__PURE__ */ jsx(SelectTrigger, {
455
+ 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 dark:bg-transparent dark:hover:bg-transparent",
456
+ children: /* @__PURE__ */ jsx(SelectValue, { children: selectedOption?.label || searchTypeOptions[0]?.label })
457
+ }), /* @__PURE__ */ jsx(SelectContent, { children: searchTypeOptions.map((option) => /* @__PURE__ */ jsx(SelectItem, {
458
+ value: option.value,
459
+ children: option.label
460
+ }, option.value)) })]
461
+ }), /* @__PURE__ */ jsx("div", { className: "h-6 w-px bg-border ml-1" })]
462
+ }),
463
+ /* @__PURE__ */ jsx(InputGroupInput, {
464
+ ...props,
465
+ type: "text",
466
+ placeholder,
467
+ value: searchValue || "",
468
+ onChange: (e) => setSearchValue?.(e.target.value),
469
+ onKeyDown: handleKeyDown,
470
+ disabled,
471
+ className: cn("text-base", inputClassName)
472
+ }),
473
+ showClearButton && searchValue && /* @__PURE__ */ jsx(InputGroupAddon, {
474
+ align: "inline-end",
475
+ children: /* @__PURE__ */ jsx(InputGroupButton, {
476
+ variant: "ghost",
477
+ size: "icon-sm",
478
+ onClick: () => setSearchValue?.(""),
479
+ disabled,
480
+ children: /* @__PURE__ */ jsx(X, { className: "size-4" })
471
481
  })
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
- })]
482
+ })
483
+ ]
480
484
  });
481
485
  }
482
486