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