@akinon/akifilter 1.7.1 → 1.8.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 (35) hide show
  1. package/dist/cjs/akifilter.d.ts +0 -10
  2. package/dist/cjs/akifilter.d.ts.map +1 -1
  3. package/dist/cjs/akifilter.js +14 -108
  4. package/dist/cjs/common/storage.d.ts +1 -0
  5. package/dist/cjs/common/storage.d.ts.map +1 -1
  6. package/dist/cjs/common/storage.js +3 -1
  7. package/dist/cjs/components/section-fieldset.d.ts +29 -0
  8. package/dist/cjs/components/section-fieldset.d.ts.map +1 -0
  9. package/dist/cjs/components/section-fieldset.js +97 -0
  10. package/dist/cjs/hooks/use-field-visibility.d.ts +45 -0
  11. package/dist/cjs/hooks/use-field-visibility.d.ts.map +1 -0
  12. package/dist/cjs/hooks/use-field-visibility.js +129 -0
  13. package/dist/cjs/styles.css +23 -0
  14. package/dist/cjs/types.d.ts +8 -1
  15. package/dist/cjs/types.d.ts.map +1 -1
  16. package/dist/cjs/utils/schema.d.ts +3 -13
  17. package/dist/cjs/utils/schema.d.ts.map +1 -1
  18. package/dist/esm/akifilter.d.ts +0 -10
  19. package/dist/esm/akifilter.d.ts.map +1 -1
  20. package/dist/esm/akifilter.js +16 -109
  21. package/dist/esm/common/storage.d.ts +1 -0
  22. package/dist/esm/common/storage.d.ts.map +1 -1
  23. package/dist/esm/common/storage.js +1 -0
  24. package/dist/esm/components/section-fieldset.d.ts +29 -0
  25. package/dist/esm/components/section-fieldset.d.ts.map +1 -0
  26. package/dist/esm/components/section-fieldset.js +93 -0
  27. package/dist/esm/hooks/use-field-visibility.d.ts +45 -0
  28. package/dist/esm/hooks/use-field-visibility.d.ts.map +1 -0
  29. package/dist/esm/hooks/use-field-visibility.js +125 -0
  30. package/dist/esm/styles.css +23 -0
  31. package/dist/esm/types.d.ts +8 -1
  32. package/dist/esm/types.d.ts.map +1 -1
  33. package/dist/esm/utils/schema.d.ts +3 -13
  34. package/dist/esm/utils/schema.d.ts.map +1 -1
  35. package/package.json +4 -4
@@ -1,6 +1,5 @@
1
1
  import type { FieldValues } from '@akinon/akiform';
2
- import { FIELD_TYPES } from '../constants';
3
- import type { AkifilterField, AkifilterSchema } from '../types';
2
+ import type { AkifilterField, AkifilterSchema, AkifilterSectionField } from '../types';
4
3
  /**
5
4
  * Flattens schema by extracting all nested fields from section fields.
6
5
  * This is used for storage operations and form value management.
@@ -11,18 +10,9 @@ export declare const flattenSchema: <TFieldValues extends FieldValues = FieldVal
11
10
  */
12
11
  export declare const partitionSchema: <TFieldValues extends FieldValues = FieldValues>(schema: AkifilterSchema<TFieldValues>) => {
13
12
  regularFields: AkifilterSchema<TFieldValues>;
14
- sectionFields: Array<AkifilterField<TFieldValues> & {
15
- type: typeof FIELD_TYPES.SECTION;
16
- fields: AkifilterSchema<TFieldValues>;
17
- defaultExpanded?: boolean;
18
- isExcludeSection?: boolean;
19
- }>;
13
+ sectionFields: Array<AkifilterSectionField<TFieldValues>>;
20
14
  };
21
- export declare const deriveExcludeSectionKeys: <TFieldValues extends FieldValues = FieldValues>(sectionFields: Array<AkifilterField<TFieldValues> & {
22
- type: typeof FIELD_TYPES.SECTION;
23
- fields: AkifilterSchema<TFieldValues>;
24
- isExcludeSection?: boolean;
25
- }>) => Set<string>;
15
+ export declare const deriveExcludeSectionKeys: <TFieldValues extends FieldValues = FieldValues>(sectionFields: Array<AkifilterSectionField<TFieldValues>>) => Set<string>;
26
16
  export declare const getDisplayLabel: <TFieldValues extends FieldValues = FieldValues>(field: AkifilterField<TFieldValues>) => string;
27
17
  export declare const getFieldAriaLabel: <TFieldValues extends FieldValues = FieldValues>(field: AkifilterField<TFieldValues>) => string;
28
18
  export declare const ensureSchemaOrder: <TFieldValues extends FieldValues = FieldValues>(schema: AkifilterSchema<TFieldValues>, keys: string[]) => string[];
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/utils/schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,EAAsC,WAAW,EAAE,MAAM,cAAc,CAAC;AAC/E,OAAO,KAAK,EAAE,cAAc,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAEhE;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC1E,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,eAAe,CAAC,YAAY,CAW9B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC;IACD,aAAa,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC7C,aAAa,EAAE,KAAK,CAClB,cAAc,CAAC,YAAY,CAAC,GAAG;QAC7B,IAAI,EAAE,OAAO,WAAW,CAAC,OAAO,CAAC;QACjC,MAAM,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;QACtC,eAAe,CAAC,EAAE,OAAO,CAAC;QAC1B,gBAAgB,CAAC,EAAE,OAAO,CAAC;KAC5B,CACF,CAAC;CAwBH,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,eAAe,KAAK,CAClB,cAAc,CAAC,YAAY,CAAC,GAAG;IAC7B,IAAI,EAAE,OAAO,WAAW,CAAC,OAAO,CAAC;IACjC,MAAM,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IACtC,gBAAgB,CAAC,EAAE,OAAO,CAAC;CAC5B,CACF,KACA,GAAG,CAAC,MAAM,CAKV,CAAC;AAEJ,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,MAEF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,MAEF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,EACrC,MAAM,MAAM,EAAE,KACb,MAAM,EAGR,CAAC;AAkBF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,OAAO,cAAc,CAAC,YAAY,CAAC,EACnC,aAAa,YAAY,EACzB,iBAAiB,OAAO,KACvB,OAAO,GAAG,SAC8D,CAAC;AAE5E,eAAO,MAAM,wBAAwB,GACnC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,MAAM,EAUR,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,OAAO,CAAC,YAAY,CAQtB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,YAAY,SAAS,WAAW,EACnE,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,OAA4C,CAAC;AAEhD,eAAO,MAAM,uBAAuB,GAClC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,EACrC,SAAS,OAAO,CAAC,YAAY,CAAC,KAC7B,OAAO,CAAC,YAAY,CA0CtB,CAAC"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../../src/utils/schema.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAInD,OAAO,KAAK,EACV,cAAc,EACd,eAAe,EACf,qBAAqB,EACtB,MAAM,UAAU,CAAC;AAElB;;;GAGG;AACH,eAAO,MAAM,aAAa,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC1E,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,eAAe,CAAC,YAAY,CAW9B,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC;IACD,aAAa,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC7C,aAAa,EAAE,KAAK,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC,CAAC;CAc3D,CAAC;AAEF,eAAO,MAAM,wBAAwB,GACnC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,eAAe,KAAK,CAAC,qBAAqB,CAAC,YAAY,CAAC,CAAC,KACxD,GAAG,CAAC,MAAM,CAKV,CAAC;AAEJ,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,MAEF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,MAEF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAC5B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,EACrC,MAAM,MAAM,EAAE,KACb,MAAM,EAGR,CAAC;AAkBF;;GAEG;AACH,eAAO,MAAM,sBAAsB,GACjC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,OAAO,cAAc,CAAC,YAAY,CAAC,EACnC,aAAa,YAAY,EACzB,iBAAiB,OAAO,KACvB,OAAO,GAAG,SAC8D,CAAC;AAE5E,eAAO,MAAM,wBAAwB,GACnC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,MAAM,EAUR,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAC/B,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,KACpC,OAAO,CAAC,YAAY,CAQtB,CAAC;AAEF,eAAO,MAAM,oBAAoB,GAAI,YAAY,SAAS,WAAW,EACnE,OAAO,cAAc,CAAC,YAAY,CAAC,KAClC,OAA4C,CAAC;AAEhD,eAAO,MAAM,uBAAuB,GAClC,YAAY,SAAS,WAAW,GAAG,WAAW,EAE9C,QAAQ,eAAe,CAAC,YAAY,CAAC,EACrC,SAAS,OAAO,CAAC,YAAY,CAAC,KAC7B,OAAO,CAAC,YAAY,CA0CtB,CAAC"}
@@ -1,6 +1,5 @@
1
1
  import './styles.css';
2
2
  import { FieldValues, Path } from '@akinon/akiform';
3
- import { type PanelProps } from '@akinon/ui-collapse';
4
3
  import React from 'react';
5
4
  import { type AppliedFilter } from './components/applied-filters';
6
5
  import type { AkifilterActionsRef, AkifilterButton, AkifilterSchema } from './types';
@@ -73,15 +72,6 @@ export type AkifilterProps<TFieldValues extends AkifilterFieldValues = Akifilter
73
72
  */
74
73
  onFilterButtonsChange?: (activeButtons: AkifilterButton[]) => void;
75
74
  };
76
- export declare const ExpandIcon: ({ isActive }: PanelProps) => {
77
- name: "chevron_down";
78
- size: number;
79
- style: {
80
- transform: string;
81
- transition: string;
82
- marginTop: number;
83
- };
84
- };
85
75
  export declare const Akifilter: {
86
76
  <TFieldValues extends AkifilterFieldValues = FieldValues>(props: AkifilterProps<TFieldValues>): React.JSX.Element;
87
77
  displayName: string;
@@ -1 +1 @@
1
- {"version":3,"file":"akifilter.d.ts","sourceRoot":"","sources":["../../src/akifilter.tsx"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAGtB,OAAO,EAIL,WAAW,EACX,IAAI,EAIL,MAAM,iBAAiB,CAAC;AAIzB,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAShE,OAAO,KAAK,MAAM,OAAO,CAAC;AAgB1B,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,8BAA8B,CAAC;AAetC,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EAEf,eAAe,EAChB,MAAM,SAAS,CAAC;AAcjB,KAAK,oBAAoB,GAAG,WAAW,CAAC;AA2KxC,MAAM,MAAM,cAAc,CACxB,YAAY,SAAS,oBAAoB,GAAG,oBAAoB,IAC9D;IACF;;OAEG;IACH,YAAY,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC7C;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACtC;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IACzD;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC;IAClE;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,gBAAgB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAClD;;;OAGG;IACH,sBAAsB,CAAC,EAAE,aAAa,EAAE,CAAC;IACzC;;OAEG;IACH,sBAAsB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC;IAClC;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,aAAa,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACpE,CAAC;AAq9BF,eAAO,MAAM,UAAU,GAAI,cAAc,UAAU;;;;;;;;CAQjD,CAAC;AA8CH,eAAO,MAAM,SAAS;KACpB,YAAY,SAAS,oBAAoB,uBAElC,cAAc,CAAC,YAAY,CAAC;;CAoBpC,CAAC"}
1
+ {"version":3,"file":"akifilter.d.ts","sourceRoot":"","sources":["../../src/akifilter.tsx"],"names":[],"mappings":"AAAA,OAAO,cAAc,CAAC;AAGtB,OAAO,EAIL,WAAW,EACX,IAAI,EAIL,MAAM,iBAAiB,CAAC;AAYzB,OAAO,KAAK,MAAM,OAAO,CAAC;AAc1B,OAAO,EACL,KAAK,aAAa,EAEnB,MAAM,8BAA8B,CAAC;AAgBtC,OAAO,KAAK,EACV,mBAAmB,EACnB,eAAe,EAEf,eAAe,EAChB,MAAM,SAAS,CAAC;AAWjB,KAAK,oBAAoB,GAAG,WAAW,CAAC;AA2KxC,MAAM,MAAM,cAAc,CACxB,YAAY,SAAS,oBAAoB,GAAG,oBAAoB,IAC9D;IACF;;OAEG;IACH,YAAY,CAAC,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IAC7C;;OAEG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;OAEG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC;IACtC;;OAEG;IACH,cAAc,CAAC,EAAE,CAAC,MAAM,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IACzD;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC,KAAK,IAAI,CAAC;IAClE;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,KAAK,IAAI,CAAC;IACnC;;OAEG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;OAEG;IACH,gBAAgB,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,mBAAmB,CAAC,CAAC;IAClD;;;OAGG;IACH,sBAAsB,CAAC,EAAE,aAAa,EAAE,CAAC;IACzC;;OAEG;IACH,sBAAsB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IAC/C;;;;;OAKG;IACH,aAAa,CAAC,EAAE,eAAe,EAAE,CAAC;IAClC;;OAEG;IACH,qBAAqB,CAAC,EAAE,CAAC,aAAa,EAAE,eAAe,EAAE,KAAK,IAAI,CAAC;CACpE,CAAC;AA+3BF,eAAO,MAAM,SAAS;KACpB,YAAY,SAAS,oBAAoB,uBAElC,cAAc,CAAC,YAAY,CAAC;;CAoBpC,CAAC"}
@@ -15,7 +15,6 @@ import { Akiform, useController, useForm, useWatch } from '@akinon/akiform';
15
15
  import { Button } from '@akinon/ui-button';
16
16
  import { Card } from '@akinon/ui-card';
17
17
  import { Checkbox } from '@akinon/ui-checkbox';
18
- import { Collapse } from '@akinon/ui-collapse';
19
18
  import { DatePicker } from '@akinon/ui-date-picker';
20
19
  import { Input, InputTextArea } from '@akinon/ui-input';
21
20
  import { InputNumber } from '@akinon/ui-input-number';
@@ -25,18 +24,20 @@ import { Upload } from '@akinon/ui-upload';
25
24
  import { ConfigProvider } from 'antd';
26
25
  import React from 'react';
27
26
  import { ErrorBoundary } from 'react-error-boundary';
28
- import { buildStorageKey, clearStoredButtonValues, clearStoredValues, readStoredButtonValues, readStoredValues, readVisibleKeys, writeStoredButtonValues, writeStoredValues, writeVisibleKeys } from './common/storage';
27
+ import { buildStorageKey, clearStoredButtonValues, clearStoredValues, readStoredButtonValues, readStoredValues, writeStoredButtonValues, writeStoredValues } from './common/storage';
29
28
  import { themeOverrides } from './common/theme-overrides';
30
29
  import { AppliedFilters } from './components/applied-filters';
31
30
  import { FilterButtons } from './components/filter-buttons';
32
31
  import { FilterToolbar } from './components/filter-toolbar';
32
+ import { SectionFieldset } from './components/section-fieldset';
33
33
  import { VisibilityModal } from './components/visibility-modal';
34
- import { BOOLEAN_STRING, DEFAULT_MODAL_PAGE_SIZE, FIELD_MODES, FIELD_TYPES, FILTER_DEBOUNCE_DELAY } from './constants';
34
+ import { BOOLEAN_STRING, FIELD_MODES, FIELD_TYPES, FILTER_DEBOUNCE_DELAY } from './constants';
35
35
  import { useDebouncedValue } from './hooks/use-debounced-value';
36
36
  import { useDynamicVisibility } from './hooks/use-dynamic-visibility';
37
+ import { useFieldVisibility } from './hooks/use-field-visibility';
37
38
  import { useVisibilityCleanup } from './hooks/use-visibility-cleanup';
38
39
  import { i18n } from './i18n';
39
- import { deriveDefaultVisibleKeys, deriveExcludeSectionKeys, ensureSchemaOrder, extractDefaultValues, flattenSchema, getFieldAriaLabel, normaliseValuesBySchema, partitionSchema, resolveFieldVisibility } from './utils/schema';
40
+ import { deriveExcludeSectionKeys, extractDefaultValues, flattenSchema, getFieldAriaLabel, normaliseValuesBySchema, partitionSchema } from './utils/schema';
40
41
  import { normaliseOutputValues } from './utils/values';
41
42
  /**
42
43
  * Prefix for applied-filter chip keys that originate from filter buttons.
@@ -149,10 +150,6 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
149
150
  defaultValues: mergedDefaultValues
150
151
  });
151
152
  const formValues = useWatch({ control: formMethods.control });
152
- const [isModalOpen, setIsModalOpen] = React.useState(false);
153
- const [searchTerm, setSearchTerm] = React.useState('');
154
- const [modalPage, setModalPage] = React.useState(1);
155
- const [modalPageSize, setModalPageSize] = React.useState(DEFAULT_MODAL_PAGE_SIZE);
156
153
  // --- Filter buttons (quick-filter toggles) ---------------------------------
157
154
  // A button is identified by its `value`, which must be unique across the
158
155
  // whole `filterButtons` array.
@@ -280,41 +277,14 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
280
277
  value: button.label
281
278
  }));
282
279
  }, [filterButtons, activeButtonValues]);
283
- const resolveInitialVisibleKeys = React.useCallback(() => {
284
- const storedKeys = readVisibleKeys(regularFields, storageKey);
285
- const defaultKeys = deriveDefaultVisibleKeys(regularFields);
286
- const baseKeys = storedKeys && storedKeys.length > 0
287
- ? ensureSchemaOrder(regularFields, storedKeys)
288
- : defaultKeys;
289
- return regularFields.reduce((acc, field) => {
290
- const key = String(field.key);
291
- const explicitVisibility = resolveFieldVisibility(field);
292
- if (explicitVisibility !== undefined) {
293
- if (explicitVisibility)
294
- acc.push(key);
295
- return acc;
296
- }
297
- // No explicit boolean visibility, use stored/default
298
- if (baseKeys.includes(key)) {
299
- acc.push(key);
300
- }
301
- return acc;
302
- }, []);
303
- }, [regularFields, storageKey]);
304
- const [visibleKeys, setVisibleKeys] = React.useState(resolveInitialVisibleKeys);
305
- React.useEffect(() => {
306
- setVisibleKeys(previous => {
307
- const ordered = ensureSchemaOrder(regularFields, previous);
308
- if (ordered.length) {
309
- return ordered;
310
- }
311
- return resolveInitialVisibleKeys();
312
- });
313
- }, [regularFields, resolveInitialVisibleKeys]);
314
- React.useEffect(() => {
315
- writeVisibleKeys(storageKey, visibleKeys);
316
- onVisibleFieldsChange === null || onVisibleFieldsChange === void 0 ? void 0 : onVisibleFieldsChange(visibleKeys);
317
- }, [visibleKeys, onVisibleFieldsChange, storageKey]);
280
+ const handleVisibleKeysChange = React.useCallback((keys) => {
281
+ onVisibleFieldsChange === null || onVisibleFieldsChange === void 0 ? void 0 : onVisibleFieldsChange(keys);
282
+ }, [onVisibleFieldsChange]);
283
+ const { visibleKeys, setVisibleKeys, visibleFields, openModal, modalProps } = useFieldVisibility({
284
+ fields: regularFields,
285
+ storageKey,
286
+ onVisibleKeysChange: handleVisibleKeysChange
287
+ });
318
288
  const normalisedValues = React.useMemo(() => normaliseOutputValues(flattenedSchema, formValues), [flattenedSchema, formValues]);
319
289
  const hasInitialValuesRef = React.useRef(false);
320
290
  React.useEffect(() => {
@@ -445,26 +415,6 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
445
415
  emitValues,
446
416
  persistValues
447
417
  ]);
448
- const handleOpenModal = React.useCallback(() => {
449
- setIsModalOpen(true);
450
- setSearchTerm('');
451
- setModalPage(1);
452
- }, []);
453
- const handleCloseModal = React.useCallback(() => {
454
- setIsModalOpen(false);
455
- }, []);
456
- const handleToggleVisibility = React.useCallback((key) => {
457
- setVisibleKeys(prev => {
458
- const exists = prev.includes(key);
459
- if (exists && prev.length === 1) {
460
- return prev;
461
- }
462
- const nextKeys = exists
463
- ? prev.filter(item => item !== key)
464
- : [...prev, key];
465
- return ensureSchemaOrder(regularFields, nextKeys);
466
- });
467
- }, [regularFields]);
468
418
  const handleRemoveFilter = React.useCallback((key) => {
469
419
  const schemaField = flattenedSchema.find(field => String(field.key) === key);
470
420
  if (!schemaField) {
@@ -545,23 +495,6 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
545
495
  keysArray.forEach(key => handleRemoveFilter(key));
546
496
  }
547
497
  }), [handleRemoveFilter]);
548
- const filteredFields = React.useMemo(() => {
549
- const normalisedTerm = searchTerm.trim().toLowerCase();
550
- if (!normalisedTerm) {
551
- return regularFields;
552
- }
553
- return regularFields.filter(field => {
554
- const searchable = [field.label, field.placeholder, String(field.key)]
555
- .filter(Boolean)
556
- .map(value => String(value).toLowerCase());
557
- return searchable.some(text => text.includes(normalisedTerm));
558
- });
559
- }, [regularFields, searchTerm]);
560
- const paginatedFields = React.useMemo(() => {
561
- const start = (modalPage - 1) * modalPageSize;
562
- return filteredFields.slice(start, start + modalPageSize);
563
- }, [filteredFields, modalPage, modalPageSize]);
564
- const visibleFields = React.useMemo(() => regularFields.filter(field => visibleKeys.includes(String(field.key))), [regularFields, visibleKeys]);
565
498
  // Dynamic visibility
566
499
  useDynamicVisibility({
567
500
  regularFields,
@@ -652,7 +585,7 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
652
585
  };
653
586
  return (React.createElement(Card, { size: "small", className: "akinon-filter shadow", "data-testid": "akifilter-root" },
654
587
  React.createElement(ConfigProvider, { theme: themeOverrides },
655
- React.createElement(FilterToolbar, { onOpenModal: handleOpenModal, enableImportCsv: enableImportCsv, enableImportXls: enableImportXls, onImportCsv: onImportCsv, onImportXls: onImportXls }),
588
+ React.createElement(FilterToolbar, { onOpenModal: openModal, enableImportCsv: enableImportCsv, enableImportXls: enableImportXls, onImportCsv: onImportCsv, onImportXls: onImportXls }),
656
589
  React.createElement(AppliedFilters, { filters: [
657
590
  ...appliedFilters,
658
591
  ...buttonAppliedFilters,
@@ -663,35 +596,9 @@ const AkifilterContent = ({ filterSchema, storageNamespace, defaultValues, onVal
663
596
  React.createElement("div", { className: "akinon-filter__form-grid", "data-testid": "akifilter-form-grid" },
664
597
  visibleFields.map(renderFormField),
665
598
  visibleFields.length === 0 && regularFields.length > 0 ? (React.createElement("div", { className: "akinon-filter__empty" }, i18n.t('form.noVisibleFields'))) : null),
666
- sectionFields.length > 0 && (React.createElement("div", { className: "akinon-filter__section-fields" }, sectionFields.map(section => {
667
- return (React.createElement(Collapse, { expandIconPosition: "end", key: section.key, expandIcon: ExpandIcon, defaultActiveKey: section.defaultExpanded === false ? [] : [section.key], ghost: true, items: [
668
- {
669
- key: section.key,
670
- label: (React.createElement(Title, { className: "akinon-filter__title", level: 4 }, section.label)),
671
- headerClass: 'akinon-filter__section-header',
672
- children: (React.createElement("div", { className: "akinon-filter__form-grid" }, section.fields.map(renderFormField)))
673
- }
674
- ] }));
675
- }))))),
676
- React.createElement(VisibilityModal, { open: isModalOpen, onClose: handleCloseModal, searchTerm: searchTerm, paginatedFields: paginatedFields, filteredCount: filteredFields.length, visibleKeys: visibleKeys, onSearchTermChange: value => {
677
- setSearchTerm(value);
678
- setModalPage(1);
679
- }, onToggleVisibility: handleToggleVisibility, onPaginationChange: (page, pageSize) => {
680
- setModalPage(page);
681
- if (pageSize !== modalPageSize) {
682
- setModalPageSize(pageSize);
683
- }
684
- }, page: modalPage, pageSize: modalPageSize }))));
599
+ sectionFields.length > 0 && (React.createElement("div", { className: "akinon-filter__section-fields" }, sectionFields.map(section => (React.createElement(SectionFieldset, { key: String(section.key), section: section, storageKey: storageKey, renderFormField: renderFormField, onRemoveField: handleRemoveFilter, formValues: normalisedValues }))))))),
600
+ React.createElement(VisibilityModal, Object.assign({}, modalProps)))));
685
601
  };
686
- export const ExpandIcon = ({ isActive }) => ({
687
- name: 'chevron_down',
688
- size: 10,
689
- style: {
690
- transform: isActive ? 'rotate(-180deg)' : 'rotate(0deg)',
691
- transition: 'transform 0.2s ease-in-out',
692
- marginTop: 8
693
- }
694
- });
695
602
  const AkifilterEmptyState = () => {
696
603
  const { t } = i18n;
697
604
  return (React.createElement(Card, { size: "small", className: "akinon-filter shadow", "data-testid": "akifilter-empty" },
@@ -3,6 +3,7 @@ import type { AkifilterField } from '../types';
3
3
  export declare const buildStorageKey: <TFieldValues extends FieldValues>(schema: AkifilterField<TFieldValues>[], namespace?: string) => string;
4
4
  export declare const readVisibleKeys: <TFieldValues extends FieldValues>(schema: AkifilterField<TFieldValues>[], storageKey: string) => string[] | null;
5
5
  export declare const writeVisibleKeys: (storageKey: string, keys: string[]) => void;
6
+ export declare const buildSectionVisibilityKey: (storageKey: string, sectionKey: string | number) => string;
6
7
  export declare const clearVisibleKeys: (storageKey: string) => void;
7
8
  export declare const readStoredValues: <TFieldValues extends FieldValues>(schema: AkifilterField<TFieldValues>[], storageKey: string) => Partial<TFieldValues> | null;
8
9
  export declare const writeStoredValues: <TFieldValues extends FieldValues>(storageKey: string, values: Partial<TFieldValues>) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/common/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AA4B/C,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,EAC9D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,MAKF,CAAC;AAIF,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,EAC9D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,MAAM,EAAE,GAAG,IAoBb,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,IAQrE,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,KAAG,IAQrD,CAAC;AAKF,eAAO,MAAM,gBAAgB,GAAI,YAAY,SAAS,WAAW,EAC/D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,OAAO,CAAC,YAAY,CAAC,GAAG,IA+B1B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,SAAS,WAAW,EAChE,YAAY,MAAM,EAClB,QAAQ,OAAO,CAAC,YAAY,CAAC,KAC5B,IAeF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,IAQtD,CAAC;AAKF,eAAO,MAAM,sBAAsB,GACjC,YAAY,MAAM,KACjB,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,IAwB3B,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,YAAY,MAAM,EAClB,QAAQ,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KAC7B,IAcF,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,YAAY,MAAM,KAAG,IAQ5D,CAAC"}
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../../../src/common/storage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AA4B/C,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,EAC9D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,MAKF,CAAC;AAIF,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,EAC9D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,MAAM,EAAE,GAAG,IAoBb,CAAC;AAEF,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,EAAE,MAAM,MAAM,EAAE,KAAG,IAQrE,CAAC;AAEF,eAAO,MAAM,yBAAyB,GACpC,YAAY,MAAM,EAClB,YAAY,MAAM,GAAG,MAAM,KAC1B,MAAuD,CAAC;AAE3D,eAAO,MAAM,gBAAgB,GAAI,YAAY,MAAM,KAAG,IAQrD,CAAC;AAKF,eAAO,MAAM,gBAAgB,GAAI,YAAY,SAAS,WAAW,EAC/D,QAAQ,cAAc,CAAC,YAAY,CAAC,EAAE,EACtC,YAAY,MAAM,KACjB,OAAO,CAAC,YAAY,CAAC,GAAG,IA+B1B,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,SAAS,WAAW,EAChE,YAAY,MAAM,EAClB,QAAQ,OAAO,CAAC,YAAY,CAAC,KAC5B,IAeF,CAAC;AAEF,eAAO,MAAM,iBAAiB,GAAI,YAAY,MAAM,KAAG,IAQtD,CAAC;AAKF,eAAO,MAAM,sBAAsB,GACjC,YAAY,MAAM,KACjB,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,GAAG,IAwB3B,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAClC,YAAY,MAAM,EAClB,QAAQ,KAAK,CAAC,MAAM,GAAG,MAAM,CAAC,KAC7B,IAcF,CAAC;AAEF,eAAO,MAAM,uBAAuB,GAAI,YAAY,MAAM,KAAG,IAQ5D,CAAC"}
@@ -56,6 +56,7 @@ export const writeVisibleKeys = (storageKey, keys) => {
56
56
  console.warn(i18n.t('errors.storage.writeVisibility'), error);
57
57
  }
58
58
  };
59
+ export const buildSectionVisibilityKey = (storageKey, sectionKey) => `${storageKey}:section:${String(sectionKey)}`;
59
60
  export const clearVisibleKeys = (storageKey) => {
60
61
  if (!isBrowser)
61
62
  return;
@@ -0,0 +1,29 @@
1
+ import type { FieldValues } from '@akinon/akiform';
2
+ import React from 'react';
3
+ import type { AkifilterField, AkifilterSectionField } from '../types';
4
+ type SectionFieldsetProps<TFieldValues extends FieldValues = FieldValues> = {
5
+ section: AkifilterSectionField<TFieldValues>;
6
+ /**
7
+ * Base storage key of the host Akifilter; combined with the section key to
8
+ * persist this section's visible-field selection.
9
+ */
10
+ storageKey: string;
11
+ renderFormField: (field: AkifilterField<TFieldValues>) => React.ReactNode;
12
+ /**
13
+ * Clears a field's form value; used to drop the value of a field that gets
14
+ * hidden from the section.
15
+ */
16
+ onRemoveField: (key: string) => void;
17
+ /**
18
+ * Current normalised form values, needed to resolve dynamic visibility.
19
+ */
20
+ formValues: Partial<TFieldValues> | undefined;
21
+ };
22
+ /**
23
+ * Renders a single section field. Sections flagged with `selectableFields`
24
+ * expose a funnel icon + visibility modal; all others render every field
25
+ * inline as before.
26
+ */
27
+ export declare const SectionFieldset: <TFieldValues extends FieldValues = FieldValues>(props: SectionFieldsetProps<TFieldValues>) => React.JSX.Element;
28
+ export {};
29
+ //# sourceMappingURL=section-fieldset.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"section-fieldset.d.ts","sourceRoot":"","sources":["../../../src/components/section-fieldset.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAKnD,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,OAAO,KAAK,EAAE,cAAc,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AA8CtE,KAAK,oBAAoB,CAAC,YAAY,SAAS,WAAW,GAAG,WAAW,IAAI;IAC1E,OAAO,EAAE,qBAAqB,CAAC,YAAY,CAAC,CAAC;IAC7C;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB,eAAe,EAAE,CAAC,KAAK,EAAE,cAAc,CAAC,YAAY,CAAC,KAAK,KAAK,CAAC,SAAS,CAAC;IAC1E;;;OAGG;IACH,aAAa,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;OAEG;IACH,UAAU,EAAE,OAAO,CAAC,YAAY,CAAC,GAAG,SAAS,CAAC;CAC/C,CAAC;AAwGF;;;;GAIG;AACH,eAAO,MAAM,eAAe,GAAI,YAAY,SAAS,WAAW,GAAG,WAAW,EAC5E,OAAO,oBAAoB,CAAC,YAAY,CAAC,sBAuB1C,CAAC"}
@@ -0,0 +1,93 @@
1
+ import { Icon } from '@akinon/icons';
2
+ import { Button } from '@akinon/ui-button';
3
+ import { Collapse } from '@akinon/ui-collapse';
4
+ import { Title } from '@akinon/ui-typography';
5
+ import React from 'react';
6
+ import { buildSectionVisibilityKey } from '../common/storage';
7
+ import { useDynamicVisibility } from '../hooks/use-dynamic-visibility';
8
+ import { useFieldVisibility } from '../hooks/use-field-visibility';
9
+ import { useVisibilityCleanup } from '../hooks/use-visibility-cleanup';
10
+ import { i18n } from '../i18n';
11
+ import { VisibilityModal } from './visibility-modal';
12
+ const ExpandIcon = ({ isActive }) => ({
13
+ name: 'chevron_down',
14
+ size: 10,
15
+ style: {
16
+ transform: isActive ? 'rotate(-180deg)' : 'rotate(0deg)',
17
+ transition: 'transform 0.2s ease-in-out',
18
+ marginTop: 8
19
+ }
20
+ });
21
+ /**
22
+ * Presentational wrapper around the collapsible panel used by every section,
23
+ * so the selectable and plain variants share one markup.
24
+ */
25
+ const SectionCollapse = ({ sectionKey, label, defaultExpanded, children }) => (React.createElement(Collapse, { expandIconPosition: "end", expandIcon: ExpandIcon, defaultActiveKey: defaultExpanded === false ? [] : [sectionKey], ghost: true, items: [
26
+ {
27
+ key: sectionKey,
28
+ label,
29
+ headerClass: 'akinon-filter__section-header',
30
+ children: React.createElement("div", { className: "akinon-filter__form-grid" }, children)
31
+ }
32
+ ] }));
33
+ /**
34
+ * A section whose header exposes a funnel icon that opens the visibility modal,
35
+ * mirroring the top-level filter field-selection behaviour.
36
+ */
37
+ const SelectableSectionFieldset = ({ section, storageKey, renderFormField, onRemoveField, formValues }) => {
38
+ const sectionStorageKey = React.useMemo(() => buildSectionVisibilityKey(storageKey, section.key), [storageKey, section.key]);
39
+ const { visibleKeys, setVisibleKeys, visibleFields, openModal, modalProps } = useFieldVisibility({
40
+ fields: section.fields,
41
+ storageKey: sectionStorageKey
42
+ });
43
+ useDynamicVisibility({
44
+ regularFields: section.fields,
45
+ formValues,
46
+ setVisibleKeys
47
+ });
48
+ useVisibilityCleanup({
49
+ visibleKeys,
50
+ regularFields: section.fields,
51
+ onRemove: onRemoveField
52
+ });
53
+ // Controlled so we can render our own chevron in the header and keep the
54
+ // funnel button pinned to the far right (after the arrow).
55
+ const sectionKey = String(section.key);
56
+ const [activeKeys, setActiveKeys] = React.useState(section.defaultExpanded === false ? [] : [sectionKey]);
57
+ const isExpanded = activeKeys.includes(sectionKey);
58
+ const label = (React.createElement("div", { className: "akinon-filter__section-header-content" },
59
+ React.createElement(Title, { className: "akinon-filter__title", level: 4 }, section.label),
60
+ React.createElement(Icon, { icon: "chevron_down", size: 10, className: "akinon-filter__section-arrow", style: {
61
+ transform: isExpanded ? 'rotate(-180deg)' : 'rotate(0deg)',
62
+ transition: 'transform 0.2s ease-in-out'
63
+ } }),
64
+ React.createElement(Button, { className: "akinon-filter__toolbar-item akinon-filter__section-funnel", icon: "filter", iconSize: 16, type: "primary", title: i18n.t('header.actions.selectFilters'), "aria-label": i18n.t('header.actions.selectFilters'), onClick: event => {
65
+ // Prevent the header click from toggling the collapse.
66
+ event.stopPropagation();
67
+ openModal();
68
+ } })));
69
+ return (React.createElement(React.Fragment, null,
70
+ React.createElement(Collapse, { activeKey: activeKeys, onChange: keys => setActiveKeys((Array.isArray(keys) ? keys : [keys]).map(String)), ghost: true, items: [
71
+ {
72
+ key: sectionKey,
73
+ label,
74
+ headerClass: 'akinon-filter__section-header akinon-filter__section-header--selectable',
75
+ showArrow: false,
76
+ children: (React.createElement("div", { className: "akinon-filter__form-grid" }, visibleFields.map(renderFormField)))
77
+ }
78
+ ] }),
79
+ React.createElement(VisibilityModal, Object.assign({}, modalProps))));
80
+ };
81
+ /**
82
+ * Renders a single section field. Sections flagged with `selectableFields`
83
+ * expose a funnel icon + visibility modal; all others render every field
84
+ * inline as before.
85
+ */
86
+ export const SectionFieldset = (props) => {
87
+ const { section, renderFormField } = props;
88
+ if (section.selectableFields) {
89
+ return React.createElement(SelectableSectionFieldset, Object.assign({}, props));
90
+ }
91
+ const label = (React.createElement(Title, { className: "akinon-filter__title", level: 4 }, section.label));
92
+ return (React.createElement(SectionCollapse, { sectionKey: String(section.key), label: label, defaultExpanded: section.defaultExpanded }, section.fields.map(renderFormField)));
93
+ };
@@ -0,0 +1,45 @@
1
+ import { type FieldValues } from '@akinon/akiform';
2
+ import React from 'react';
3
+ import type { AkifilterSchema } from '../types';
4
+ type UseFieldVisibilityParams<TFieldValues extends FieldValues> = {
5
+ /**
6
+ * Fields whose visibility this hook manages (top-level filters or the fields
7
+ * of a single section).
8
+ */
9
+ fields: AkifilterSchema<TFieldValues>;
10
+ /**
11
+ * Storage key under which the visible-field selection is persisted. Callers
12
+ * pass distinct keys per group so selections never collide.
13
+ */
14
+ storageKey: string;
15
+ /**
16
+ * Notified whenever the resolved visible keys change.
17
+ */
18
+ onVisibleKeysChange?: (keys: string[]) => void;
19
+ };
20
+ /**
21
+ * Encapsulates the visible-field selection and its accompanying modal state for
22
+ * a single group of fields. Used both for the top-level filters and for each
23
+ * selectable section so the two share one implementation.
24
+ */
25
+ export declare const useFieldVisibility: <TFieldValues extends FieldValues = FieldValues>({ fields, storageKey, onVisibleKeysChange }: UseFieldVisibilityParams<TFieldValues>) => {
26
+ visibleKeys: string[];
27
+ setVisibleKeys: React.Dispatch<React.SetStateAction<string[]>>;
28
+ visibleFields: import("../types").AkifilterField<TFieldValues>[];
29
+ openModal: () => void;
30
+ modalProps: {
31
+ open: boolean;
32
+ searchTerm: string;
33
+ paginatedFields: import("../types").AkifilterField<TFieldValues>[];
34
+ filteredCount: number;
35
+ visibleKeys: string[];
36
+ onClose: () => void;
37
+ onSearchTermChange: (value: string) => void;
38
+ onToggleVisibility: (key: string) => void;
39
+ onPaginationChange: (page: number, pageSize: number) => void;
40
+ page: number;
41
+ pageSize: number;
42
+ };
43
+ };
44
+ export {};
45
+ //# sourceMappingURL=use-field-visibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"use-field-visibility.d.ts","sourceRoot":"","sources":["../../../src/hooks/use-field-visibility.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,MAAM,OAAO,CAAC;AAI1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAC;AAOhD,KAAK,wBAAwB,CAAC,YAAY,SAAS,WAAW,IAAI;IAChE;;;OAGG;IACH,MAAM,EAAE,eAAe,CAAC,YAAY,CAAC,CAAC;IACtC;;;OAGG;IACH,UAAU,EAAE,MAAM,CAAC;IACnB;;OAEG;IACH,mBAAmB,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CAChD,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAC7B,YAAY,SAAS,WAAW,GAAG,WAAW,EAC9C,6CAIC,wBAAwB,CAAC,YAAY,CAAC;;;;;;;;;;;;oCAiHP,MAAM;kCAlD9B,MAAM;mCAuDiB,MAAM,YAAY,MAAM;;;;CA6BxD,CAAC"}
@@ -0,0 +1,125 @@
1
+ import React from 'react';
2
+ import { readVisibleKeys, writeVisibleKeys } from '../common/storage';
3
+ import { DEFAULT_MODAL_PAGE_SIZE } from '../constants';
4
+ import { deriveDefaultVisibleKeys, ensureSchemaOrder, resolveFieldVisibility } from '../utils/schema';
5
+ /**
6
+ * Encapsulates the visible-field selection and its accompanying modal state for
7
+ * a single group of fields. Used both for the top-level filters and for each
8
+ * selectable section so the two share one implementation.
9
+ */
10
+ export const useFieldVisibility = ({ fields, storageKey, onVisibleKeysChange }) => {
11
+ const [isModalOpen, setIsModalOpen] = React.useState(false);
12
+ const [searchTerm, setSearchTerm] = React.useState('');
13
+ const [modalPage, setModalPage] = React.useState(1);
14
+ const [modalPageSize, setModalPageSize] = React.useState(DEFAULT_MODAL_PAGE_SIZE);
15
+ const resolveInitialVisibleKeys = React.useCallback(() => {
16
+ const storedKeys = readVisibleKeys(fields, storageKey);
17
+ const defaultKeys = deriveDefaultVisibleKeys(fields);
18
+ const baseKeys = storedKeys && storedKeys.length > 0
19
+ ? ensureSchemaOrder(fields, storedKeys)
20
+ : defaultKeys;
21
+ return fields.reduce((acc, field) => {
22
+ const key = String(field.key);
23
+ const explicitVisibility = resolveFieldVisibility(field);
24
+ if (explicitVisibility !== undefined) {
25
+ if (explicitVisibility)
26
+ acc.push(key);
27
+ return acc;
28
+ }
29
+ // No explicit boolean visibility, use stored/default
30
+ if (baseKeys.includes(key)) {
31
+ acc.push(key);
32
+ }
33
+ return acc;
34
+ }, []);
35
+ }, [fields, storageKey]);
36
+ const [visibleKeys, setVisibleKeys] = React.useState(resolveInitialVisibleKeys);
37
+ React.useEffect(() => {
38
+ setVisibleKeys(previous => {
39
+ const ordered = ensureSchemaOrder(fields, previous);
40
+ if (ordered.length) {
41
+ return ordered;
42
+ }
43
+ return resolveInitialVisibleKeys();
44
+ });
45
+ }, [fields, resolveInitialVisibleKeys]);
46
+ React.useEffect(() => {
47
+ writeVisibleKeys(storageKey, visibleKeys);
48
+ onVisibleKeysChange === null || onVisibleKeysChange === void 0 ? void 0 : onVisibleKeysChange(visibleKeys);
49
+ }, [visibleKeys, onVisibleKeysChange, storageKey]);
50
+ const openModal = React.useCallback(() => {
51
+ setIsModalOpen(true);
52
+ setSearchTerm('');
53
+ setModalPage(1);
54
+ }, []);
55
+ const closeModal = React.useCallback(() => {
56
+ setIsModalOpen(false);
57
+ }, []);
58
+ const handleToggleVisibility = React.useCallback((key) => {
59
+ setVisibleKeys(prev => {
60
+ const exists = prev.includes(key);
61
+ if (exists && prev.length === 1) {
62
+ return prev;
63
+ }
64
+ const nextKeys = exists
65
+ ? prev.filter(item => item !== key)
66
+ : [...prev, key];
67
+ return ensureSchemaOrder(fields, nextKeys);
68
+ });
69
+ }, [fields]);
70
+ const filteredFields = React.useMemo(() => {
71
+ const normalisedTerm = searchTerm.trim().toLowerCase();
72
+ if (!normalisedTerm) {
73
+ return fields;
74
+ }
75
+ return fields.filter(field => {
76
+ const searchable = [field.label, field.placeholder, String(field.key)]
77
+ .filter(Boolean)
78
+ .map(value => String(value).toLowerCase());
79
+ return searchable.some(text => text.includes(normalisedTerm));
80
+ });
81
+ }, [fields, searchTerm]);
82
+ const paginatedFields = React.useMemo(() => {
83
+ const start = (modalPage - 1) * modalPageSize;
84
+ return filteredFields.slice(start, start + modalPageSize);
85
+ }, [filteredFields, modalPage, modalPageSize]);
86
+ const visibleFields = React.useMemo(() => fields.filter(field => visibleKeys.includes(String(field.key))), [fields, visibleKeys]);
87
+ const modalProps = React.useMemo(() => ({
88
+ open: isModalOpen,
89
+ searchTerm,
90
+ paginatedFields,
91
+ filteredCount: filteredFields.length,
92
+ visibleKeys,
93
+ onClose: closeModal,
94
+ onSearchTermChange: (value) => {
95
+ setSearchTerm(value);
96
+ setModalPage(1);
97
+ },
98
+ onToggleVisibility: handleToggleVisibility,
99
+ onPaginationChange: (page, pageSize) => {
100
+ setModalPage(page);
101
+ if (pageSize !== modalPageSize) {
102
+ setModalPageSize(pageSize);
103
+ }
104
+ },
105
+ page: modalPage,
106
+ pageSize: modalPageSize
107
+ }), [
108
+ isModalOpen,
109
+ searchTerm,
110
+ paginatedFields,
111
+ filteredFields.length,
112
+ visibleKeys,
113
+ closeModal,
114
+ handleToggleVisibility,
115
+ modalPage,
116
+ modalPageSize
117
+ ]);
118
+ return {
119
+ visibleKeys,
120
+ setVisibleKeys,
121
+ visibleFields,
122
+ openModal,
123
+ modalProps
124
+ };
125
+ };
@@ -70,6 +70,29 @@
70
70
  flex: unset !important;
71
71
  }
72
72
 
73
+ .akinon-filter__section-header-content {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: 0.5rem;
77
+ width: 100%;
78
+ }
79
+
80
+ /* Let the header text span fill the header so the funnel can sit at the far right. */
81
+ .akinon-filter__section-header--selectable .akinon-collapse-header-text {
82
+ flex: 1 1 auto !important;
83
+ }
84
+
85
+ .akinon-filter__section-arrow {
86
+ color: var(--color-gray-500);
87
+ }
88
+
89
+ /* Funnel pinned to the far right of the header, after the title and arrow. */
90
+ .akinon-filter__section-funnel.akinon-filter__toolbar-item {
91
+ width: 36px;
92
+ justify-content: center;
93
+ margin-left: auto;
94
+ }
95
+
73
96
  .akinon-filter__buttons {
74
97
  display: flex;
75
98
  align-items: center;
@@ -1,5 +1,5 @@
1
1
  import type { FieldValues, Path } from '@akinon/akiform';
2
- import type { FormField } from '@akinon/akiform-builder';
2
+ import type { FormField, SectionField } from '@akinon/akiform-builder';
3
3
  import type { IconName } from '@akinon/icons';
4
4
  export interface AkifilterFieldConfig<TFieldValues extends FieldValues = FieldValues> {
5
5
  /**
@@ -25,6 +25,13 @@ export type AkifilterField<TFieldValues extends FieldValues = FieldValues> = Dis
25
25
  */
26
26
  fileAccept?: string;
27
27
  };
28
+ /**
29
+ * Section field as consumed by Akifilter. Derived from the shared
30
+ * `SectionField` so flags like `selectableFields`/`isExcludeSection` stay
31
+ * single-sourced, but omits `collapsible` — Akifilter always renders sections
32
+ * as collapsible panels and does not honor that prop.
33
+ */
34
+ export type AkifilterSectionField<TFieldValues extends FieldValues = FieldValues> = Omit<SectionField<TFieldValues>, 'collapsible'>;
28
35
  export type AkifilterFieldKey<TFieldValues extends FieldValues = FieldValues> = Path<TFieldValues>;
29
36
  export type AkifilterSchema<TFieldValues extends FieldValues = FieldValues> = AkifilterField<TFieldValues>[];
30
37
  /**