@fuf-stack/megapixels 0.8.0 → 0.9.1

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.
@@ -0,0 +1,751 @@
1
+ import createDebug from "debug";
2
+ import { tv, variantsToClassNames } from "@fuf-stack/pixel-utils";
3
+ import Form from "@fuf-stack/uniform/Form";
4
+ import { Suspense, createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";
5
+ import { any, array, boolean, object, refineArray, string, stringToJSON, veto } from "@fuf-stack/veto";
6
+ import Label from "@fuf-stack/pixels/Label";
7
+ import { useFormContext } from "@fuf-stack/uniform/hooks/useFormContext";
8
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
9
+ import { FaSliders } from "react-icons/fa6";
10
+ import Menu from "@fuf-stack/pixels/Menu";
11
+ import { PiSlidersHorizontalBold } from "react-icons/pi";
12
+ import Button from "@fuf-stack/pixels/Button";
13
+ import Modal from "@fuf-stack/pixels/Modal";
14
+ import SubmitButton from "@fuf-stack/uniform/SubmitButton";
15
+ import { FaSearch } from "react-icons/fa";
16
+ import { motion } from "@fuf-stack/pixel-motion";
17
+ import Input from "@fuf-stack/uniform/Input";
18
+ import Switch from "@fuf-stack/uniform/Switch";
19
+ import Checkboxes from "@fuf-stack/uniform/Checkboxes";
20
+
21
+ //#region src/Filter/hooks/useFilterValidation.ts
22
+ /**
23
+ * useFilterValidation
24
+ *
25
+ * Builds a composite validation schema from all provided filter definitions
26
+ * under "filter" and optionally includes a "search" string field.
27
+ * Memoized by inputs.
28
+ */
29
+ const useFilterValidation = (filters$1, withSearch) => {
30
+ return useMemo(() => {
31
+ let filterSchema = {};
32
+ filters$1.forEach((f) => {
33
+ filterSchema = {
34
+ ...filterSchema,
35
+ [f.name]: f.validation(f.config)
36
+ };
37
+ });
38
+ return veto({
39
+ filter: stringToJSON().pipe(object(filterSchema)).or(object(filterSchema)).optional().nullable().transform((val) => {
40
+ return val ?? void 0;
41
+ }),
42
+ ...withSearch ? { search: string({ min: 0 }).nullable().optional() } : {}
43
+ });
44
+ }, [filters$1, withSearch]);
45
+ };
46
+
47
+ //#endregion
48
+ //#region src/Filter/Subcomponents/FiltersContext.tsx
49
+ /**
50
+ * FiltersContext
51
+ *
52
+ * Central state for the filter UI with a clear boundary:
53
+ * - The parent component controls committed filter values (via value/onChange)
54
+ * - The form acts as an edit buffer (used by the modal)
55
+ *
56
+ * Design:
57
+ * - activeFilters/unusedFilters are names-only and derived from the controlled
58
+ * form state
59
+ * - getFilterInstanceByName gives access to the concrete registry entry to
60
+ * retrieve the correct Form/Display components
61
+ * - add seeds defaults in the form and opens the modal
62
+ * - remove un-registers the form field; if the removed filter is currently
63
+ * open in the modal, the modal is closed without rollback
64
+ * - on a new successful form submit (Apply), the modal closes without rollback
65
+ * by subscribing to ex-forms submit state
66
+ */
67
+ const FiltersContext = createContext(void 0);
68
+ const FiltersContextProvider = ({ children, config: config$2 }) => {
69
+ const { formState, getFieldState, setValue, triggerSubmit, unregister, watch } = useFormContext();
70
+ /**
71
+ * currentModalFilter
72
+ *
73
+ * Single source of truth for the filter edit modal and its rollback snapshot.
74
+ * - name: which filter's modal is currently open (null when closed)
75
+ * - hadValue/previousValue: snapshot of the controlled value taken when the
76
+ * modal is opened; used to restore state if the user cancels/closes without
77
+ * applying.
78
+ *
79
+ * Lifecycle semantics:
80
+ * - showFilterModal(name): capture snapshot (current controlled value) and open
81
+ * the modal for that filter.
82
+ * - closeFilterModal(): if a snapshot exists, roll back un-applied edits by
83
+ * restoring the previous value (setValue) or removing the field (unregister)
84
+ * when it did not exist before; then clear currentModalFilter.
85
+ * - On successful submit (Apply): close and clear currentModalFilter WITHOUT rollback
86
+ * so edits remain committed.
87
+ * - removeFilter(name): unregisters the field; when removing the filter that is
88
+ * currently open, close the modal WITHOUT rollback (since removal is explicit).
89
+ */
90
+ const [currentModalFilter, setCurrentModalFilter] = useState(null);
91
+ const filterValue = watch("filter", {});
92
+ /**
93
+ * getFilterFormFieldName
94
+ *
95
+ * Returns the fully-qualified field path for a given filter name,
96
+ * e.g., `${filterUrlParam}.status`.
97
+ */
98
+ const getFilterFormFieldName = useCallback((name) => {
99
+ return `filter.${name}`;
100
+ }, []);
101
+ /**
102
+ * getFilterValueByName
103
+ *
104
+ * Returns the committed value for a filter from the controlled state.
105
+ */
106
+ const getFilterValueByName = useCallback((name) => {
107
+ return filterValue[name];
108
+ }, [filterValue]);
109
+ /** Open the filter edit modal for the given filter name. */
110
+ const showFilterModal = useCallback((name) => {
111
+ const prev = getFilterValueByName(name);
112
+ setCurrentModalFilter({
113
+ name,
114
+ hadValue: typeof prev !== "undefined",
115
+ previousValue: prev
116
+ });
117
+ }, [getFilterValueByName]);
118
+ /** Close the filter edit modal. Rollback un-applied edits to controlled state. */
119
+ const closeFilterModal = useCallback(() => {
120
+ if (currentModalFilter?.name) {
121
+ const fieldName = getFilterFormFieldName(currentModalFilter.name);
122
+ if (currentModalFilter.hadValue) setValue(fieldName, currentModalFilter.previousValue);
123
+ else unregister(fieldName);
124
+ }
125
+ setCurrentModalFilter(null);
126
+ }, [
127
+ getFilterFormFieldName,
128
+ currentModalFilter,
129
+ setValue,
130
+ unregister
131
+ ]);
132
+ /**
133
+ * Auto-close on submit success
134
+ *
135
+ * Close the modal only on new successful submissions. We track the last
136
+ * submitCount and only react when it changes AND the form reports a
137
+ * successful submit. This prevents closing when `isSubmitSuccessful` remains
138
+ * true without a new submit event.
139
+ */
140
+ const lastSubmitCountRef = useRef(0);
141
+ useEffect(() => {
142
+ if (formState.submitCount !== lastSubmitCountRef.current && formState.isSubmitSuccessful) setCurrentModalFilter(null);
143
+ lastSubmitCountRef.current = formState.submitCount;
144
+ }, [
145
+ formState.submitCount,
146
+ formState.isSubmitSuccessful,
147
+ setCurrentModalFilter
148
+ ]);
149
+ /**
150
+ * activeFilters
151
+ *
152
+ * Filter names derived from the controlled form state. A filter is considered
153
+ * active when a field exists at `filter.<name>`. Newly added filters become
154
+ * active immediately (seeded default), and will be rolled back on cancel.
155
+ */
156
+ const activeFilters = useMemo(() => {
157
+ return config$2.filter((f) => {
158
+ return Object.hasOwn(filterValue ?? {}, f.name);
159
+ }).map((f) => {
160
+ return f.name;
161
+ });
162
+ }, [config$2, filterValue]);
163
+ /**
164
+ * unusedFilters
165
+ *
166
+ * Complement of activeFilters (names without a corresponding `filter.<name>`
167
+ * field in the controlled form state).
168
+ */
169
+ const unusedFilters = useMemo(() => {
170
+ return config$2.filter((f) => {
171
+ return !Object.hasOwn(filterValue ?? {}, f.name);
172
+ }).map((f) => {
173
+ return f.name;
174
+ });
175
+ }, [config$2, filterValue]);
176
+ /**
177
+ * getRegistryFilterByName
178
+ *
179
+ * Looks up the concrete registry entry for a filter by name, enabling access
180
+ * to typed Form/Display components and other registry-level metadata.
181
+ */
182
+ const getFilterInstanceByName = useCallback((name) => {
183
+ return config$2.find((f) => {
184
+ return f.name === name;
185
+ });
186
+ }, [config$2]);
187
+ /**
188
+ * addFilter
189
+ *
190
+ * Seeds the filter with its registry default value inside the form and opens
191
+ * the modal for immediate editing. No URL writes happen here.
192
+ */
193
+ const addFilter = useCallback((name) => {
194
+ const inst = getFilterInstanceByName(name);
195
+ showFilterModal(name);
196
+ setValue(getFilterFormFieldName(name), inst.defaultValue);
197
+ }, [
198
+ getFilterFormFieldName,
199
+ getFilterInstanceByName,
200
+ setValue,
201
+ showFilterModal
202
+ ]);
203
+ /**
204
+ * removeFilter
205
+ *
206
+ * Unregisters the filter field from the form. This immediately removes the
207
+ * filter from the active list since derived state watches the form. It
208
+ * closes the modal without rollback if the removed filter is currently open.
209
+ */
210
+ const removeFilter = useCallback((name) => {
211
+ unregister(getFilterFormFieldName(name));
212
+ if (currentModalFilter?.name === name) setCurrentModalFilter(null);
213
+ triggerSubmit();
214
+ }, [
215
+ getFilterFormFieldName,
216
+ currentModalFilter,
217
+ setCurrentModalFilter,
218
+ triggerSubmit,
219
+ unregister
220
+ ]);
221
+ /**
222
+ * hasError
223
+ *
224
+ * Helper that checks the ex-forms field state for a specific filter and
225
+ * reports whether the field is currently invalid.
226
+ */
227
+ const hasError = useCallback((name) => {
228
+ return getFieldState(getFilterFormFieldName(name)).invalid;
229
+ }, [getFieldState, getFilterFormFieldName]);
230
+ const contextValue = useMemo(() => {
231
+ return {
232
+ activeFilters,
233
+ addFilter,
234
+ closeFilterModal,
235
+ getFilterFormFieldName,
236
+ getFilterValueByName,
237
+ getFilterInstanceByName,
238
+ hasError,
239
+ modalFilterName: currentModalFilter?.name,
240
+ removeFilter,
241
+ showFilterModal,
242
+ unusedFilters
243
+ };
244
+ }, [
245
+ activeFilters,
246
+ addFilter,
247
+ closeFilterModal,
248
+ getFilterFormFieldName,
249
+ getFilterValueByName,
250
+ getFilterInstanceByName,
251
+ hasError,
252
+ currentModalFilter,
253
+ removeFilter,
254
+ showFilterModal,
255
+ unusedFilters
256
+ ]);
257
+ return /* @__PURE__ */ jsx(FiltersContext.Provider, {
258
+ value: contextValue,
259
+ children
260
+ });
261
+ };
262
+ /**
263
+ * useFilters
264
+ *
265
+ * Convenience hook to consume the FiltersContext. Throws a descriptive error
266
+ * when used outside of a FiltersContextProvider to make integration mistakes
267
+ * obvious during development.
268
+ */
269
+ const useFilters = () => {
270
+ const ctx = useContext(FiltersContext);
271
+ if (!ctx) throw new Error("useFilters must be used within FiltersContextProvider");
272
+ return ctx;
273
+ };
274
+
275
+ //#endregion
276
+ //#region src/Filter/Subcomponents/ActiveFilters.tsx
277
+ const ActiveFilters = ({ className = void 0 }) => {
278
+ const { activeFilters, getFilterValueByName, getFilterInstanceByName, hasError, removeFilter, showFilterModal } = useFilters();
279
+ return /* @__PURE__ */ jsx(Fragment, { children: activeFilters.map((name) => {
280
+ const instance = getFilterInstanceByName(name);
281
+ const value = getFilterValueByName(name);
282
+ const DisplayComponent = instance.components.Display;
283
+ return /* @__PURE__ */ jsx("button", {
284
+ "aria-label": `Open ${name} filter`,
285
+ type: "button",
286
+ onClick: () => {
287
+ showFilterModal(name);
288
+ },
289
+ children: /* @__PURE__ */ jsxs(Label, {
290
+ className,
291
+ color: hasError(name) ? "danger" : "primary",
292
+ variant: "flat",
293
+ onClose: () => {
294
+ removeFilter(name);
295
+ },
296
+ children: [instance.icon, /* @__PURE__ */ jsx(DisplayComponent, {
297
+ config: instance.config,
298
+ value
299
+ })]
300
+ })
301
+ }, name);
302
+ }) });
303
+ };
304
+ var ActiveFilters_default = ActiveFilters;
305
+
306
+ //#endregion
307
+ //#region src/Filter/Subcomponents/AddFilterMenu.tsx
308
+ /**
309
+ * AddFilterMenu
310
+ *
311
+ * Renders a menu trigger that opens a list of addable filters. Selecting an
312
+ * item triggers the parent to seed a default value and open the modal.
313
+ */
314
+ const AddFilterMenu = ({ classNames = {} }) => {
315
+ const { unusedFilters, addFilter, getFilterInstanceByName } = useFilters();
316
+ const menuItems = unusedFilters.map((name) => {
317
+ const instance = getFilterInstanceByName(name);
318
+ const label = instance.config?.text ?? name;
319
+ return {
320
+ key: name,
321
+ icon: instance.icon,
322
+ label,
323
+ onClick: () => {
324
+ addFilter(name);
325
+ }
326
+ };
327
+ });
328
+ return /* @__PURE__ */ jsxs(Menu, {
329
+ isDisabled: !menuItems.length,
330
+ items: menuItems,
331
+ placement: "bottom-start",
332
+ className: {
333
+ item: classNames.addFilterMenuItem,
334
+ trigger: classNames.addFilterMenuButton
335
+ },
336
+ triggerButtonProps: {
337
+ "aria-label": "Add Filter",
338
+ "data-testid": "add_filter_button",
339
+ disableRipple: true,
340
+ size: "sm",
341
+ variant: "bordered"
342
+ },
343
+ children: [/* @__PURE__ */ jsx(FaSliders, {}), "Filter"]
344
+ });
345
+ };
346
+ var AddFilterMenu_default = AddFilterMenu;
347
+
348
+ //#endregion
349
+ //#region src/Filter/Subcomponents/FilterModal.tsx
350
+ const FilterModal = ({ classNames = {} }) => {
351
+ const { closeFilterModal, getFilterFormFieldName, getFilterInstanceByName, modalFilterName, removeFilter } = useFilters();
352
+ if (!modalFilterName) return null;
353
+ const instance = getFilterInstanceByName(modalFilterName);
354
+ const config$2 = instance.config;
355
+ const FormComponent = instance.components.Form;
356
+ return /* @__PURE__ */ jsx(Modal, {
357
+ isOpen: true,
358
+ onClose: closeFilterModal,
359
+ className: {
360
+ body: classNames.body,
361
+ footer: classNames.footer,
362
+ header: classNames.header
363
+ },
364
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(Button, {
365
+ ariaLabel: "Remove filter",
366
+ color: "danger",
367
+ testId: "remove_filter_button",
368
+ variant: "flat",
369
+ onClick: () => {
370
+ removeFilter(modalFilterName);
371
+ },
372
+ children: "Remove"
373
+ }), /* @__PURE__ */ jsx(SubmitButton, {
374
+ ariaLabel: "Apply filter",
375
+ testId: "apply_filter_button",
376
+ children: "Apply Filter"
377
+ })] }),
378
+ header: /* @__PURE__ */ jsxs(Fragment, { children: [instance.icon ?? /* @__PURE__ */ jsx(PiSlidersHorizontalBold, {}), /* @__PURE__ */ jsx("div", { children: `${config$2?.text ?? modalFilterName} Filter` })] }),
379
+ children: /* @__PURE__ */ jsx(Suspense, { children: /* @__PURE__ */ jsx(FormComponent, {
380
+ config: config$2,
381
+ fieldName: getFilterFormFieldName(modalFilterName)
382
+ }) })
383
+ });
384
+ };
385
+ var FilterModal_default = FilterModal;
386
+
387
+ //#endregion
388
+ //#region src/Filter/Subcomponents/SearchInput.tsx
389
+ /**
390
+ * SearchInput
391
+ *
392
+ * By default renders only a search button. When clicked, the text input animates in
393
+ * and a trailing submit button is shown.
394
+ */
395
+ const SearchInput = ({ classNames = {}, config: config$2 }) => {
396
+ const { formState, setFocus, triggerSubmit } = useFormContext();
397
+ const isInitiallyVisible = !!formState?.defaultValues?.search;
398
+ const [isVisible, setIsVisible] = useState(isInitiallyVisible);
399
+ const placeholder = typeof config$2 === "object" ? config$2.placeholder : void 0;
400
+ return /* @__PURE__ */ jsxs("div", {
401
+ className: classNames.searchWrapper,
402
+ children: [!isVisible && /* @__PURE__ */ jsx(Button, {
403
+ ariaLabel: "Show search input",
404
+ className: classNames.searchShowButton,
405
+ icon: /* @__PURE__ */ jsx(FaSearch, {}),
406
+ size: "sm",
407
+ testId: "show_search_input_button",
408
+ variant: "bordered",
409
+ onClick: () => {
410
+ setIsVisible(true);
411
+ }
412
+ }), isVisible ? /* @__PURE__ */ jsxs(motion.div, {
413
+ animate: { opacity: 1 },
414
+ className: classNames.searchMotionDiv,
415
+ initial: !isInitiallyVisible ? { opacity: .5 } : false,
416
+ onAnimationComplete: () => {
417
+ if (!isInitiallyVisible) setFocus("search");
418
+ },
419
+ transition: {
420
+ duration: .3,
421
+ ease: "circOut"
422
+ },
423
+ children: [/* @__PURE__ */ jsx(Input, {
424
+ clearable: true,
425
+ debounceDelay: 0,
426
+ name: "search",
427
+ placeholder,
428
+ size: "sm",
429
+ className: {
430
+ input: classNames.searchInput,
431
+ inputWrapper: classNames.searchInputWrapper
432
+ },
433
+ onClear: () => {
434
+ triggerSubmit();
435
+ }
436
+ }), /* @__PURE__ */ jsx(SubmitButton, {
437
+ ariaLabel: "Submit search",
438
+ children: null,
439
+ className: classNames.searchSubmitButton,
440
+ color: "primary",
441
+ icon: /* @__PURE__ */ jsx(FaSearch, {}),
442
+ size: "sm",
443
+ testId: "submit_search_button"
444
+ })]
445
+ }, "search-input") : null]
446
+ });
447
+ };
448
+ var SearchInput_default = SearchInput;
449
+
450
+ //#endregion
451
+ //#region src/Filter/Filter.tsx
452
+ const debug = createDebug("megapixels:Filter");
453
+ const filterVariants = tv({ slots: {
454
+ base: "flex flex-auto flex-col",
455
+ addFilterMenuButton: "",
456
+ addFilterMenuItem: "",
457
+ activeFilterLabel: "h-8 cursor-pointer rounded-md dark:text-foreground",
458
+ filterModalBody: "",
459
+ filterModalHeader: "flex items-center gap-3 text-default-700",
460
+ filterModalFooter: "justify-between",
461
+ form: "mb-3 flex flex-wrap gap-3",
462
+ searchInput: "",
463
+ searchInputWrapper: "",
464
+ searchMotionDiv: "flex w-72 gap-2",
465
+ searchShowButton: "",
466
+ searchSubmitButton: "",
467
+ searchWrapper: "flex items-center"
468
+ } });
469
+ /**
470
+ * Renders the filter UI bound to a single ex-forms `Form`.
471
+ * The form is the source of truth during user interaction; the committed
472
+ * state is controlled by the parent via `values`/`onChange`.
473
+ */
474
+ const Filter = ({ children = void 0, className = void 0, config: config$2, formName = "filterComponentForm", onChange, values }) => {
475
+ const handleSubmit = (nextValues) => {
476
+ debug("handleSubmit", { nextValues });
477
+ onChange(nextValues);
478
+ };
479
+ const validation = useFilterValidation(config$2.filters, Boolean(config$2.search));
480
+ const { data: valuesValidated, errors, success } = validation.validate(values);
481
+ const classNames = variantsToClassNames(filterVariants(), className, "base");
482
+ debug("render", {
483
+ props: {
484
+ config: config$2,
485
+ values
486
+ },
487
+ validation: {
488
+ errors,
489
+ success,
490
+ valuesValidated
491
+ }
492
+ });
493
+ return /* @__PURE__ */ jsxs("div", {
494
+ className: classNames.base,
495
+ children: [/* @__PURE__ */ jsxs(Form, {
496
+ className: classNames.form,
497
+ debug: { disable: true },
498
+ initialValues: valuesValidated ?? {},
499
+ name: formName,
500
+ onSubmit: handleSubmit,
501
+ validation,
502
+ children: [config$2.search ? /* @__PURE__ */ jsx(SearchInput_default, {
503
+ config: config$2.search,
504
+ classNames: {
505
+ searchInput: classNames.searchInput,
506
+ searchInputWrapper: classNames.searchInputWrapper,
507
+ searchMotionDiv: classNames.searchMotionDiv,
508
+ searchShowButton: classNames.searchShowButton,
509
+ searchSubmitButton: classNames.searchSubmitButton,
510
+ searchWrapper: classNames.searchWrapper
511
+ }
512
+ }) : null, /* @__PURE__ */ jsxs(FiltersContextProvider, {
513
+ config: config$2.filters,
514
+ children: [
515
+ /* @__PURE__ */ jsx(ActiveFilters_default, { className: classNames.activeFilterLabel }),
516
+ /* @__PURE__ */ jsx(AddFilterMenu_default, { classNames: {
517
+ addFilterMenuButton: classNames.addFilterMenuButton,
518
+ addFilterMenuItem: classNames.addFilterMenuItem
519
+ } }),
520
+ /* @__PURE__ */ jsx(FilterModal_default, { classNames: {
521
+ body: classNames.filterModalBody,
522
+ footer: classNames.filterModalFooter,
523
+ header: classNames.filterModalHeader
524
+ } })
525
+ ]
526
+ })]
527
+ }), children?.(valuesValidated ?? {})]
528
+ });
529
+ };
530
+ var Filter_default = Filter;
531
+
532
+ //#endregion
533
+ //#region src/Filter/filters/createFilter.ts
534
+ /**
535
+ * createFilter
536
+ *
537
+ * Builds a filter factory from a static FilterDefinition. The returned factory
538
+ * accepts a usage descriptor (name/icon and optional partial config) and
539
+ * produces a concrete FilterInstance with:
540
+ * - merged config (shallow: definition.defaults.config overlaid by overrides)
541
+ * - Form/Display components
542
+ * - validate function (forwarded from the definition)
543
+ * - defaultValue (forwarded from the definition)
544
+ * - name and icon for UI integration
545
+ *
546
+ * @typeParam Config - Configuration object shape for the filter
547
+ * @typeParam Value - Runtime value type for the filter
548
+ * @param definition - Static description of the filter (components, defaults, validate)
549
+ * @returns FilterFactory that creates FilterInstance<Config, Value>
550
+ */
551
+ const createFilter = (definition) => {
552
+ return ({ name, icon, config: config$2 }) => {
553
+ return {
554
+ components: definition.components,
555
+ config: {
556
+ ...definition.defaults.config,
557
+ ...config$2 ?? {}
558
+ },
559
+ defaultValue: definition.defaults.value,
560
+ icon,
561
+ name,
562
+ validation: definition.validation
563
+ };
564
+ };
565
+ };
566
+ var createFilter_default = createFilter;
567
+
568
+ //#endregion
569
+ //#region src/Filter/filters/boolean/Display.tsx
570
+ /**
571
+ * Read-only presentation for the boolean filter.
572
+ * Displays human-readable text based on the current boolean `value`
573
+ * and the provided `config` strings.
574
+ */
575
+ const Display$1 = ({ value, config: { text, textPrefix, textNoWord } }) => {
576
+ if (typeof value === "boolean") return /* @__PURE__ */ jsx(Fragment, { children: `${value ? textPrefix : `${textPrefix} ${textNoWord ?? "no"}`} ${text}` });
577
+ return /* @__PURE__ */ jsx(Fragment, { children: `${text}...` });
578
+ };
579
+ var Display_default$1 = Display$1;
580
+
581
+ //#endregion
582
+ //#region src/Filter/filters/boolean/Form.tsx
583
+ /**
584
+ * Renders the form control for the boolean filter.
585
+ * Uses a `Switch` to toggle the boolean value and composes
586
+ * the label from the provided `config` and `fieldName`.
587
+ */
588
+ const Form$2 = ({ fieldName, config: { text, textPrefix } }) => {
589
+ return /* @__PURE__ */ jsx(Switch, {
590
+ label: `${textPrefix} ${text}`,
591
+ name: fieldName
592
+ });
593
+ };
594
+ var Form_default$1 = Form$2;
595
+
596
+ //#endregion
597
+ //#region src/Filter/filters/boolean/schema.ts
598
+ /** configuration of the filter */
599
+ const config$1 = object({
600
+ text: string(),
601
+ textPrefix: string().optional(),
602
+ textNoWord: string().optional()
603
+ });
604
+ /** validate the filter value */
605
+ const validate$1 = (_config) => {
606
+ return boolean().optional();
607
+ };
608
+
609
+ //#endregion
610
+ //#region src/Filter/filters/boolean/boolean.ts
611
+ /**
612
+ * Boolean filter definition for the Filter system.
613
+ * Provides Display and Form components, default value/config, and validation.
614
+ *
615
+ * Defaults:
616
+ * - value: true
617
+ * - config: { text: 'Active', textPrefix: 'is', textNoWord: 'no' }
618
+ *
619
+ * @see Display
620
+ * @see Form
621
+ * @see validate
622
+ */
623
+ const boolean$1 = createFilter_default({
624
+ components: {
625
+ Display: Display_default$1,
626
+ Form: Form_default$1
627
+ },
628
+ defaults: {
629
+ value: true,
630
+ config: {
631
+ text: "Active",
632
+ textPrefix: "is",
633
+ textNoWord: "no"
634
+ }
635
+ },
636
+ validation: validate$1
637
+ });
638
+
639
+ //#endregion
640
+ //#region src/Filter/filters/checkboxes/Display.tsx
641
+ /**
642
+ * Read-only presentation for the checkboxes filter.
643
+ * Resolves and displays selected option labels based on `value` and `config`.
644
+ * Supports string, ReactNode, and function labels (mode: 'display').
645
+ */
646
+ const Display = ({ value, config: { text, options } }) => {
647
+ if (value && value.length > 0) return /* @__PURE__ */ jsxs("span", {
648
+ className: "flex items-center gap-1",
649
+ children: [
650
+ text,
651
+ " is",
652
+ value.map((val) => {
653
+ const label = options.find((op) => {
654
+ return op.value === val;
655
+ })?.label ?? val;
656
+ return /* @__PURE__ */ jsx("span", { children: typeof label === "function" ? label("display") : label }, val);
657
+ })
658
+ ]
659
+ });
660
+ return `${text} is ...`;
661
+ };
662
+ var Display_default = Display;
663
+
664
+ //#endregion
665
+ //#region src/Filter/filters/checkboxes/Form.tsx
666
+ /**
667
+ * Renders the form control for the checkboxes filter.
668
+ * Uses a `Checkboxes` to select multiple options.
669
+ * Resolves function labels with 'form' mode.
670
+ */
671
+ const Form$1 = ({ fieldName, config: config$2 }) => {
672
+ return /* @__PURE__ */ jsx(Checkboxes, {
673
+ name: fieldName,
674
+ options: config$2.options.map((option) => {
675
+ return {
676
+ ...option,
677
+ label: typeof option.label === "function" ? option.label("form") : option.label
678
+ };
679
+ })
680
+ });
681
+ };
682
+ var Form_default = Form$1;
683
+
684
+ //#endregion
685
+ //#region src/Filter/filters/checkboxes/schema.ts
686
+ /** configuration of the filter */
687
+ const config = object({
688
+ text: string(),
689
+ options: array(object({
690
+ label: any(),
691
+ value: string()
692
+ }))
693
+ });
694
+ /** validate the filter value */
695
+ const validate = (cfg) => {
696
+ return refineArray(array(string()).optional())({
697
+ unique: true,
698
+ custom: (values, ctx) => {
699
+ if (!cfg) return;
700
+ values.forEach((value) => {
701
+ if (!cfg.options.find((option) => {
702
+ return option?.value === value;
703
+ })) ctx.addIssue({
704
+ code: "custom",
705
+ message: `Invalid value: ${value}`
706
+ });
707
+ });
708
+ }
709
+ });
710
+ };
711
+
712
+ //#endregion
713
+ //#region src/Filter/filters/checkboxes/checkboxes.ts
714
+ /**
715
+ * Checkboxes filter definition for the Filter system.
716
+ * Provides Display and Form components, default value/config, and validation.
717
+ *
718
+ * Defaults:
719
+ * - value: []
720
+ * - config: { text: 'Options', options: [] }
721
+ *
722
+ * @see Display
723
+ * @see Form
724
+ * @see validate
725
+ */
726
+ const checkboxes = createFilter_default({
727
+ components: {
728
+ Display: Display_default,
729
+ Form: Form_default
730
+ },
731
+ defaults: {
732
+ value: [],
733
+ config: {
734
+ text: "Options",
735
+ options: []
736
+ }
737
+ },
738
+ validation: validate
739
+ });
740
+
741
+ //#endregion
742
+ //#region src/Filter/index.ts
743
+ const filters = {
744
+ boolean: boolean$1,
745
+ checkboxes
746
+ };
747
+ var Filter_default$1 = Filter_default;
748
+
749
+ //#endregion
750
+ export { filterVariants as i, filters as n, createFilter_default as r, Filter_default$1 as t };
751
+ //# sourceMappingURL=Filter-nSuQco2t.js.map