@k3-tech/react-kit 0.0.60 → 0.0.62

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 (34) hide show
  1. package/dist/index.d.ts +1 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +483 -10
  4. package/dist/kit/builder/data-table/components/DataTable.d.ts +3 -1
  5. package/dist/kit/builder/data-table/components/DataTable.d.ts.map +1 -1
  6. package/dist/kit/components/data-table-headers/DataTableHeader.d.ts +25 -0
  7. package/dist/kit/components/data-table-headers/DataTableHeader.d.ts.map +1 -0
  8. package/dist/kit/components/data-table-headers/DebouncedFilterField.d.ts +23 -0
  9. package/dist/kit/components/data-table-headers/DebouncedFilterField.d.ts.map +1 -0
  10. package/dist/kit/components/data-table-headers/FilterFieldRenderer.d.ts +19 -0
  11. package/dist/kit/components/data-table-headers/FilterFieldRenderer.d.ts.map +1 -0
  12. package/dist/kit/components/data-table-headers/index.d.ts +7 -0
  13. package/dist/kit/components/data-table-headers/index.d.ts.map +1 -0
  14. package/dist/kit/components/data-table-headers/types.d.ts +76 -0
  15. package/dist/kit/components/data-table-headers/types.d.ts.map +1 -0
  16. package/dist/kit/components/data-table-headers/urlSerialization.d.ts +12 -0
  17. package/dist/kit/components/data-table-headers/urlSerialization.d.ts.map +1 -0
  18. package/dist/kit/components/data-table-headers/useDataTableHeaderUrl.d.ts +36 -0
  19. package/dist/kit/components/data-table-headers/useDataTableHeaderUrl.d.ts.map +1 -0
  20. package/dist/kit/themes/clean-slate.css +8 -0
  21. package/dist/kit/themes/default.css +8 -0
  22. package/dist/kit/themes/minimal-modern.css +8 -0
  23. package/dist/kit/themes/spotify.css +8 -0
  24. package/package.json +1 -1
  25. package/src/index.ts +1 -0
  26. package/src/kit/builder/data-table/components/DataTable.tsx +5 -2
  27. package/src/kit/builder/form/components/fields/NumberField.tsx +1 -1
  28. package/src/kit/components/data-table-headers/DataTableHeader.tsx +158 -0
  29. package/src/kit/components/data-table-headers/DebouncedFilterField.tsx +83 -0
  30. package/src/kit/components/data-table-headers/FilterFieldRenderer.tsx +194 -0
  31. package/src/kit/components/data-table-headers/index.ts +6 -0
  32. package/src/kit/components/data-table-headers/types.ts +89 -0
  33. package/src/kit/components/data-table-headers/urlSerialization.ts +173 -0
  34. package/src/kit/components/data-table-headers/useDataTableHeaderUrl.ts +107 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DataTableHeader.d.ts","sourceRoot":"","sources":["../../../../src/kit/components/data-table-headers/DataTableHeader.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAyB,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAO3E;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,MAAM,EACN,MAAM,EACN,QAAQ,EACR,WAAW,EACX,UAAgB,EAChB,GAAW,EACX,OAAY,EACZ,SAAS,EACT,QAAQ,EACR,SAAS,EACT,gBAAgB,EAChB,gBAAgB,GACjB,EAAE,oBAAoB,2CA4GtB;AAED,eAAe,eAAe,CAAC"}
@@ -0,0 +1,23 @@
1
+ import { Control, FieldValues } from 'react-hook-form';
2
+ import { FilterFieldConfig } from './types';
3
+ export interface DebouncedFilterFieldProps {
4
+ field: FilterFieldConfig;
5
+ /** External value (source of truth). */
6
+ value: unknown;
7
+ /** Push the (debounced) value upstream. */
8
+ onChange: (value: unknown) => void;
9
+ debounceMs: number;
10
+ control: Control<FieldValues>;
11
+ className?: string;
12
+ }
13
+ /**
14
+ * Wraps a free-typing field (text/email/password/number/textarea) with a local
15
+ * draft so keystrokes feel instant while the upstream `onChange` is debounced.
16
+ *
17
+ * The external `value` stays the single source of truth: the draft is only a
18
+ * typing buffer. When the external value changes for reasons other than this
19
+ * field's own push (URL hydration, programmatic clear), the draft resyncs and
20
+ * any in-flight debounced push is cancelled.
21
+ */
22
+ export declare function DebouncedFilterField({ field, value, onChange, debounceMs, control, className, }: DebouncedFilterFieldProps): import("react/jsx-runtime").JSX.Element;
23
+ //# sourceMappingURL=DebouncedFilterField.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DebouncedFilterField.d.ts","sourceRoot":"","sources":["../../../../src/kit/components/data-table-headers/DebouncedFilterField.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAM5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,yBAAyB;IACxC,KAAK,EAAE,iBAAiB,CAAC;IACzB,wCAAwC;IACxC,KAAK,EAAE,OAAO,CAAC;IACf,2CAA2C;IAC3C,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,EACnC,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,OAAO,EACP,SAAS,GACV,EAAE,yBAAyB,2CA6C3B"}
@@ -0,0 +1,19 @@
1
+ import { Control, FieldValues } from 'react-hook-form';
2
+ import { FilterFieldConfig } from './types';
3
+ export interface FilterFieldRendererProps {
4
+ field: FilterFieldConfig;
5
+ /** Current value for this field (from the external values map). */
6
+ value: unknown;
7
+ /** Push a new value for this field upstream. */
8
+ onChange: (value: unknown) => void;
9
+ debounceMs: number;
10
+ /** Throwaway control shim so date fields' `useWatch` does not throw. */
11
+ control: Control<FieldValues>;
12
+ }
13
+ /**
14
+ * Renders a single filter field by mirroring FormBuilderField's type → component
15
+ * mapping, but WITHOUT react-hook-form's `useController`. Values flow in via
16
+ * props and out via `onChange`; the header owns no field state.
17
+ */
18
+ export declare function FilterFieldRenderer({ field, value, onChange, debounceMs, control, }: FilterFieldRendererProps): import("react/jsx-runtime").JSX.Element | null;
19
+ //# sourceMappingURL=FilterFieldRenderer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"FilterFieldRenderer.d.ts","sourceRoot":"","sources":["../../../../src/kit/components/data-table-headers/FilterFieldRenderer.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAsB5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AA8BjD,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,iBAAiB,CAAC;IACzB,mEAAmE;IACnE,KAAK,EAAE,OAAO,CAAC;IACf,gDAAgD;IAChD,QAAQ,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,UAAU,EAAE,MAAM,CAAC;IACnB,wEAAwE;IACxE,OAAO,EAAE,OAAO,CAAC,WAAW,CAAC,CAAC;CAC/B;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,KAAK,EACL,KAAK,EACL,QAAQ,EACR,UAAU,EACV,OAAO,GACR,EAAE,wBAAwB,kDAsH1B"}
@@ -0,0 +1,7 @@
1
+ export * from './DataTableHeader';
2
+ export * from './FilterFieldRenderer';
3
+ export * from './DebouncedFilterField';
4
+ export * from './useDataTableHeaderUrl';
5
+ export * from './urlSerialization';
6
+ export * from './types';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/kit/components/data-table-headers/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAC;AAClC,cAAc,uBAAuB,CAAC;AACtC,cAAc,wBAAwB,CAAC;AACvC,cAAc,yBAAyB,CAAC;AACxC,cAAc,oBAAoB,CAAC;AACnC,cAAc,SAAS,CAAC"}
@@ -0,0 +1,76 @@
1
+ import { ReactNode } from 'react';
2
+ import { DataTableAction } from '../../builder/data-table/types';
3
+ import { FormBuilderFieldConfig } from '../../builder/form/types';
4
+ /**
5
+ * A single filter field definition. Reuses {@link FormBuilderFieldConfig} so
6
+ * field definitions for the header work exactly like FormBuilder fields, but
7
+ * the header renders them WITHOUT react-hook-form and holds no value state.
8
+ *
9
+ * Supported `type`s: text, email, password, number (plain), textarea, select,
10
+ * autocomplete, checkbox, switch, radio, month, month_range, time, time_range,
11
+ * and the date family (date, date_picker, date_range, date_time,
12
+ * date_time_range).
13
+ *
14
+ * Unsupported in a filter header: object, array, custom_field, hidden — these
15
+ * are skipped with a dev warning.
16
+ */
17
+ export type FilterFieldConfig = FormBuilderFieldConfig;
18
+ /**
19
+ * Action shown on the right side of the header. Reuses the DataTable action
20
+ * shape and rendering, mirroring DataTable's `renderActionButton`.
21
+ */
22
+ export type DataTableHeaderAction = DataTableAction;
23
+ /**
24
+ * Adapter that lets the URL sync work against an arbitrary URL source instead
25
+ * of the browser History API. Provide this in environments with their own
26
+ * router (e.g. TanStack Router) so the header does not fight the router for
27
+ * control of the URL.
28
+ */
29
+ export interface UrlSyncAdapter {
30
+ /** Read the current query params as a flat map (repeated keys -> string[]). */
31
+ read: () => Record<string, string | string[]>;
32
+ /** Write the given query params back to the URL. */
33
+ write: (params: Record<string, string | string[]>) => void;
34
+ }
35
+ /**
36
+ * URL sync option.
37
+ * - `false` / `undefined`: disabled.
38
+ * - `true`: use the native History API (URLSearchParams + replaceState).
39
+ * - `UrlSyncAdapter`: use the provided adapter (recommended when a router owns
40
+ * the URL).
41
+ */
42
+ export type UrlSyncOption = boolean | UrlSyncAdapter;
43
+ export interface DataTableHeaderProps {
44
+ /** Filter field definitions (reuses FormBuilder field config). */
45
+ fields: FilterFieldConfig[];
46
+ /**
47
+ * Externally-owned filter values, keyed by `field.name`. This is the single
48
+ * source of truth — the header never holds its own copy.
49
+ */
50
+ values: Record<string, unknown>;
51
+ /** Called whenever a single field changes (debounced for free-typing inputs). */
52
+ onChange: (name: string, value: unknown) => void;
53
+ /**
54
+ * Bulk setter used to hydrate values from the URL on mount. If omitted, the
55
+ * header falls back to issuing one `onChange` per hydrated field.
56
+ */
57
+ onChangeAll?: (next: Record<string, unknown>) => void;
58
+ /** Debounce (ms) applied to free-typing inputs. Default 400. */
59
+ debounceMs?: number;
60
+ /** Enable URL sync. See {@link UrlSyncOption}. Default disabled. */
61
+ url?: UrlSyncOption;
62
+ /** Right-side action buttons. */
63
+ actions?: DataTableHeaderAction[];
64
+ /**
65
+ * Refresh handler. When provided, a default refresh button (mirroring the
66
+ * DataTable's standard refresh button) is rendered before the actions. Omit
67
+ * to hide it.
68
+ */
69
+ onRefresh?: () => void | Promise<void>;
70
+ /** Optional content rendered between filters and actions. */
71
+ children?: ReactNode;
72
+ className?: string;
73
+ filtersClassName?: string;
74
+ actionsClassName?: string;
75
+ }
76
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/kit/components/data-table-headers/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,gCAAgC,CAAC;AACtE,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,0BAA0B,CAAC;AAEvE;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,iBAAiB,GAAG,sBAAsB,CAAC;AAEvD;;;GAGG;AACH,MAAM,MAAM,qBAAqB,GAAG,eAAe,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,WAAW,cAAc;IAC7B,+EAA+E;IAC/E,IAAI,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAAC;IAC9C,oDAAoD;IACpD,KAAK,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;CAC5D;AAED;;;;;;GAMG;AACH,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,cAAc,CAAC;AAErD,MAAM,WAAW,oBAAoB;IACnC,kEAAkE;IAClE,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAE5B;;;OAGG;IACH,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAEhC,iFAAiF;IACjF,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAEjD;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAEtD,gEAAgE;IAChE,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,oEAAoE;IACpE,GAAG,CAAC,EAAE,aAAa,CAAC;IAEpB,iCAAiC;IACjC,OAAO,CAAC,EAAE,qBAAqB,EAAE,CAAC;IAElC;;;;OAIG;IACH,SAAS,CAAC,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAEvC,6DAA6D;IAC7D,QAAQ,CAAC,EAAE,SAAS,CAAC;IAErB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B"}
@@ -0,0 +1,12 @@
1
+ import { FilterFieldConfig } from './types';
2
+ /**
3
+ * Serialize a full values object into a query-param map. Repeated keys are
4
+ * represented as string[]; everything else as string.
5
+ */
6
+ export declare function serializeAll(fields: FilterFieldConfig[], values: Record<string, unknown>): Record<string, string | string[]>;
7
+ /**
8
+ * Deserialize a query-param map back into typed filter values, guided by field
9
+ * definitions. Only keys present in the params are included in the result.
10
+ */
11
+ export declare function deserializeAll(fields: FilterFieldConfig[], params: Record<string, string | string[]>): Record<string, unknown>;
12
+ //# sourceMappingURL=urlSerialization.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"urlSerialization.d.ts","sourceRoot":"","sources":["../../../../src/kit/components/data-table-headers/urlSerialization.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,SAAS,CAAC;AAwDjD;;;GAGG;AACH,wBAAgB,YAAY,CAC1B,MAAM,EAAE,iBAAiB,EAAE,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC9B,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,CAqCnC;AAED;;;GAGG;AACH,wBAAgB,cAAc,CAC5B,MAAM,EAAE,iBAAiB,EAAE,EAC3B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC,GACxC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CA+DzB"}
@@ -0,0 +1,36 @@
1
+ import { Control, FieldValues } from 'react-hook-form';
2
+ import { FilterFieldConfig, UrlSyncAdapter } from './types';
3
+ /**
4
+ * Default URL adapter backed by the browser History API. Suitable for
5
+ * router-less contexts (plain apps, Storybook). In an app whose router owns the
6
+ * URL (e.g. TanStack Router), pass a custom {@link UrlSyncAdapter} instead so
7
+ * the header does not fight the router.
8
+ */
9
+ export declare const nativeAdapter: UrlSyncAdapter;
10
+ export interface UseDataTableHeaderUrlOptions {
11
+ enabled: boolean;
12
+ fields: FilterFieldConfig[];
13
+ values: Record<string, unknown>;
14
+ adapter: UrlSyncAdapter;
15
+ /** Bulk hydrate used on mount. */
16
+ onHydrate: (next: Record<string, unknown>) => void;
17
+ }
18
+ /**
19
+ * Two-way URL sync for the filter header:
20
+ * - On mount, reads params from the adapter and hydrates the external values.
21
+ * - On every values change, writes the serialized params back to the adapter.
22
+ *
23
+ * Holds no filter state of its own; the external `values` remain the single
24
+ * source of truth.
25
+ */
26
+ export declare function useDataTableHeaderUrl({ enabled, fields, values, adapter, onHydrate, }: UseDataTableHeaderUrlOptions): void;
27
+ /**
28
+ * A throwaway react-hook-form control used solely to satisfy date field
29
+ * components that call `useWatch({ control })`. It holds NO filter data —
30
+ * nothing ever writes to it — so it does not violate the "no internal state"
31
+ * rule. Cross-field date constraints (`minDateFromField`/`maxDateFromField`)
32
+ * resolve against this empty form and therefore won't apply in filters; static
33
+ * `minDate`/`maxDate` still work.
34
+ */
35
+ export declare function useFilterControl(): Control<FieldValues>;
36
+ //# sourceMappingURL=useDataTableHeaderUrl.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useDataTableHeaderUrl.d.ts","sourceRoot":"","sources":["../../../../src/kit/components/data-table-headers/useDataTableHeaderUrl.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAGjE;;;;;GAKG;AACH,eAAO,MAAM,aAAa,EAAE,cA4B3B,CAAC;AAEF,MAAM,WAAW,4BAA4B;IAC3C,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,iBAAiB,EAAE,CAAC;IAC5B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,OAAO,EAAE,cAAc,CAAC;IACxB,kCAAkC;IAClC,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CACpD;AAED;;;;;;;GAOG;AACH,wBAAgB,qBAAqB,CAAC,EACpC,OAAO,EACP,MAAM,EACN,MAAM,EACN,OAAO,EACP,SAAS,GACV,EAAE,4BAA4B,GAAG,IAAI,CA4BrC;AAED;;;;;;;GAOG;AACH,wBAAgB,gBAAgB,IAAI,OAAO,CAAC,WAAW,CAAC,CAGvD"}
@@ -4604,6 +4604,10 @@
4604
4604
  display: flex;
4605
4605
  }
4606
4606
 
4607
+ .sm\:w-auto {
4608
+ width: auto;
4609
+ }
4610
+
4607
4611
  .sm\:max-w-lg {
4608
4612
  max-width: var(--container-lg);
4609
4613
  }
@@ -4612,6 +4616,10 @@
4612
4616
  max-width: var(--container-sm);
4613
4617
  }
4614
4618
 
4619
+ .sm\:min-w-\[180px\] {
4620
+ min-width: 180px;
4621
+ }
4622
+
4615
4623
  .sm\:grid-cols-1 {
4616
4624
  grid-template-columns: repeat(1, minmax(0, 1fr));
4617
4625
  }
@@ -4611,6 +4611,10 @@
4611
4611
  display: flex;
4612
4612
  }
4613
4613
 
4614
+ .sm\:w-auto {
4615
+ width: auto;
4616
+ }
4617
+
4614
4618
  .sm\:max-w-lg {
4615
4619
  max-width: var(--container-lg);
4616
4620
  }
@@ -4619,6 +4623,10 @@
4619
4623
  max-width: var(--container-sm);
4620
4624
  }
4621
4625
 
4626
+ .sm\:min-w-\[180px\] {
4627
+ min-width: 180px;
4628
+ }
4629
+
4622
4630
  .sm\:grid-cols-1 {
4623
4631
  grid-template-columns: repeat(1, minmax(0, 1fr));
4624
4632
  }
@@ -4604,6 +4604,10 @@
4604
4604
  display: flex;
4605
4605
  }
4606
4606
 
4607
+ .sm\:w-auto {
4608
+ width: auto;
4609
+ }
4610
+
4607
4611
  .sm\:max-w-lg {
4608
4612
  max-width: var(--container-lg);
4609
4613
  }
@@ -4612,6 +4616,10 @@
4612
4616
  max-width: var(--container-sm);
4613
4617
  }
4614
4618
 
4619
+ .sm\:min-w-\[180px\] {
4620
+ min-width: 180px;
4621
+ }
4622
+
4615
4623
  .sm\:grid-cols-1 {
4616
4624
  grid-template-columns: repeat(1, minmax(0, 1fr));
4617
4625
  }
@@ -4604,6 +4604,10 @@
4604
4604
  display: flex;
4605
4605
  }
4606
4606
 
4607
+ .sm\:w-auto {
4608
+ width: auto;
4609
+ }
4610
+
4607
4611
  .sm\:max-w-lg {
4608
4612
  max-width: var(--container-lg);
4609
4613
  }
@@ -4612,6 +4616,10 @@
4612
4616
  max-width: var(--container-sm);
4613
4617
  }
4614
4618
 
4619
+ .sm\:min-w-\[180px\] {
4620
+ min-width: 180px;
4621
+ }
4622
+
4615
4623
  .sm\:grid-cols-1 {
4616
4624
  grid-template-columns: repeat(1, minmax(0, 1fr));
4617
4625
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@k3-tech/react-kit",
3
- "version": "0.0.60",
3
+ "version": "0.0.62",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
package/src/index.ts CHANGED
@@ -25,6 +25,7 @@ export * from './kit/components/fileuploader';
25
25
  export * from './kit/components/numpad';
26
26
  export * from './kit/components/keyboard';
27
27
  export * from './kit/components/timepicker';
28
+ export * from './kit/components/data-table-headers';
28
29
 
29
30
  // -----------------------------
30
31
  // KIT: layouts (admin)
@@ -14,6 +14,7 @@ import {
14
14
  } from '@tanstack/react-table';
15
15
  import { Loader2, RefreshCw } from 'lucide-react';
16
16
  import * as React from 'react';
17
+ import type { UseFormReturn } from 'react-hook-form';
17
18
  import { cn } from '../../../../shadcn/lib/utils';
18
19
  import {
19
20
  Accordion,
@@ -65,10 +66,11 @@ export interface DataTableProps<TData, TValue> {
65
66
  // UI
66
67
  className?: string;
67
68
  emptyText?: string;
68
- // Filters UI (section-based or simple)
69
+ // Filters UI (section-based or simple) // what's so simple about this
69
70
  formFilters?: DataTableFiltersProp;
70
71
  formFilterValues?: Record<string, unknown> | null;
71
72
  onFormFilterChange?: (values: Record<string, unknown>) => void;
73
+ formFilterForm?: UseFormReturn<any>;
72
74
  // Selection & Actions
73
75
  selectable?: boolean;
74
76
  actions?: DataTableAction[];
@@ -111,6 +113,7 @@ export function DataTable<TData, TValue>({
111
113
  onTable,
112
114
  className,
113
115
  emptyText = 'No results.',
116
+ formFilterForm,
114
117
  formFilters,
115
118
  formFilterValues,
116
119
  onFormFilterChange,
@@ -385,7 +388,7 @@ export function DataTable<TData, TValue>({
385
388
  </AccordionTrigger>
386
389
  <AccordionContent className="px-4 pb-4 pt-5">
387
390
  <FormBuilder
388
- key={JSON.stringify(formFilterValues ?? {})}
391
+ form={formFilterForm}
389
392
  sections={formFilters as FormBuilderSectionConfig[]}
390
393
  defaultValues={formFilterValues}
391
394
  onSubmit={(data) =>
@@ -10,7 +10,7 @@ export function NumberField({
10
10
  }: FieldRenderProps) {
11
11
  const innerValue = value as number;
12
12
  const [displayValue, setDisplayValue] = useState(
13
- innerValue.toLocaleString('id-ID'),
13
+ (Number(innerValue) || "").toLocaleString('id-ID'),
14
14
  );
15
15
 
16
16
  if (field.numberMode === 'currency') {
@@ -0,0 +1,158 @@
1
+ import React, { useCallback } from 'react';
2
+ import { RefreshCw } from 'lucide-react';
3
+ import { cn } from '@/shadcn/lib/utils';
4
+ import { Button } from '@/shadcn/ui/button';
5
+ import { FilterFieldRenderer } from './FilterFieldRenderer';
6
+ import type { DataTableHeaderAction, DataTableHeaderProps } from './types';
7
+ import {
8
+ nativeAdapter,
9
+ useDataTableHeaderUrl,
10
+ useFilterControl,
11
+ } from './useDataTableHeaderUrl';
12
+
13
+ /**
14
+ * A plug-in replacement for the DataTable's default filter + actions header.
15
+ *
16
+ * Plug it into `DataTable`'s `CustomHeaderComponent`:
17
+ *```
18
+ * <DataTable
19
+ * CustomHeaderComponent={() => (
20
+ * <DataTableHeader
21
+ * fields={filterFields}
22
+ * values={filtersState} // owned by your list hook
23
+ * onChange={setFilter} // updates that same state
24
+ * actions={[addAction, refreshAction]}
25
+ * />
26
+ * )}
27
+ * />
28
+ *```
29
+ * Unlike the default FormBuilder-based filter, this header holds NO filter data
30
+ * of its own — `values` is the single source of truth, so the UI can never
31
+ * drift from what is sent to the API. Filters apply on every change (no
32
+ * Apply/Clear); free-typing inputs are debounced internally.
33
+ */
34
+ export function DataTableHeader({
35
+ fields,
36
+ values,
37
+ onChange,
38
+ onChangeAll,
39
+ debounceMs = 400,
40
+ url = false,
41
+ actions = [],
42
+ onRefresh,
43
+ children,
44
+ className,
45
+ filtersClassName,
46
+ actionsClassName,
47
+ }: DataTableHeaderProps) {
48
+ const control = useFilterControl();
49
+
50
+ const adapter = url === true ? nativeAdapter : url || nativeAdapter;
51
+
52
+ // biome-ignore lint: see below
53
+ const handleHydrate = useCallback(
54
+ (next: Record<string, unknown>) => {
55
+ if (onChangeAll) {
56
+ onChangeAll({ ...values, ...next });
57
+ return;
58
+ }
59
+ for (const [name, value] of Object.entries(next)) onChange(name, value);
60
+ },
61
+ // `values` intentionally omitted: hydration runs once on mount and we don't
62
+ // want to re-bind to every value change.
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ [onChange, onChangeAll],
65
+ );
66
+
67
+ useDataTableHeaderUrl({
68
+ enabled: url !== false,
69
+ fields,
70
+ values,
71
+ adapter,
72
+ onHydrate: handleHydrate,
73
+ });
74
+
75
+ const renderActionButton = (action: DataTableHeaderAction, key: React.Key) => {
76
+ if (action.element)
77
+ return <React.Fragment key={key}>{action.element}</React.Fragment>;
78
+
79
+ const content = (
80
+ <div className="flex items-center gap-x-2">
81
+ {action.icon && (
82
+ <span
83
+ className={cn(action.iconPosition === 'right' ? 'order-last' : '')}
84
+ >
85
+ {action.icon}
86
+ </span>
87
+ )}
88
+ {action.label}
89
+ </div>
90
+ );
91
+
92
+ return (
93
+ <Button
94
+ key={key}
95
+ size="sm"
96
+ variant={action.variant ?? 'default'}
97
+ disabled={action.disabled}
98
+ onClick={action.onClick}
99
+ className="h-8"
100
+ >
101
+ {content}
102
+ </Button>
103
+ );
104
+ };
105
+
106
+ return (
107
+ <div
108
+ className={cn('flex items-start justify-between gap-3', className)}
109
+ >
110
+ {/* Filters: grow + wrap */}
111
+ <div
112
+ className={cn(
113
+ 'flex min-w-0 flex-1 flex-wrap items-end gap-2',
114
+ filtersClassName,
115
+ )}
116
+ >
117
+ {fields.map((field) => (
118
+ <FilterFieldRenderer
119
+ key={field.name}
120
+ field={field}
121
+ value={values[field.name]}
122
+ onChange={(value) => onChange(field.name, value)}
123
+ debounceMs={debounceMs}
124
+ control={control}
125
+ />
126
+ ))}
127
+ </div>
128
+
129
+ {children}
130
+
131
+ {/* Actions: pinned right, no wrap */}
132
+ {(actions.length > 0 || onRefresh) && (
133
+ <div
134
+ className={cn(
135
+ 'flex shrink-0 flex-nowrap items-center gap-2',
136
+ actionsClassName,
137
+ )}
138
+ >
139
+ {onRefresh && (
140
+ <Button
141
+ variant="outline"
142
+ size="sm"
143
+ onClick={onRefresh}
144
+ className="h-8"
145
+ title="Refresh"
146
+ >
147
+ <RefreshCw className="h-4 w-4" />
148
+ <span>Refresh</span>
149
+ </Button>
150
+ )}
151
+ {actions.map((action) => renderActionButton(action, action.key))}
152
+ </div>
153
+ )}
154
+ </div>
155
+ );
156
+ }
157
+
158
+ export default DataTableHeader;
@@ -0,0 +1,83 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { useDebouncedCallback } from 'use-debounce';
3
+ import type { Control, FieldValues } from 'react-hook-form';
4
+ import {
5
+ NumberField,
6
+ TextField,
7
+ TextareaField,
8
+ } from '@/kit/builder/form/components/fields';
9
+ import type { FilterFieldConfig } from './types';
10
+
11
+ export interface DebouncedFilterFieldProps {
12
+ field: FilterFieldConfig;
13
+ /** External value (source of truth). */
14
+ value: unknown;
15
+ /** Push the (debounced) value upstream. */
16
+ onChange: (value: unknown) => void;
17
+ debounceMs: number;
18
+ control: Control<FieldValues>;
19
+ className?: string;
20
+ }
21
+
22
+ /**
23
+ * Wraps a free-typing field (text/email/password/number/textarea) with a local
24
+ * draft so keystrokes feel instant while the upstream `onChange` is debounced.
25
+ *
26
+ * The external `value` stays the single source of truth: the draft is only a
27
+ * typing buffer. When the external value changes for reasons other than this
28
+ * field's own push (URL hydration, programmatic clear), the draft resyncs and
29
+ * any in-flight debounced push is cancelled.
30
+ */
31
+ export function DebouncedFilterField({
32
+ field,
33
+ value,
34
+ onChange,
35
+ debounceMs,
36
+ control,
37
+ className,
38
+ }: DebouncedFilterFieldProps) {
39
+ const [draft, setDraft] = useState<unknown>(value);
40
+ const lastPushedRef = useRef<unknown>(value);
41
+
42
+ const debounced = useDebouncedCallback((v: unknown) => {
43
+ lastPushedRef.current = v;
44
+ onChange(v);
45
+ }, debounceMs);
46
+
47
+ const handleChange = (v: unknown) => {
48
+ setDraft(v);
49
+ debounced(v);
50
+ };
51
+
52
+ // Resync the draft when the external value changes for a reason other than
53
+ // this field's own debounced push (e.g. URL hydration or external clear).
54
+ useEffect(() => {
55
+ if (!Object.is(value, lastPushedRef.current)) {
56
+ debounced.cancel();
57
+ lastPushedRef.current = value;
58
+ setDraft(value);
59
+ }
60
+ // eslint-disable-next-line react-hooks/exhaustive-deps
61
+ }, [value]);
62
+
63
+ // Flush a pending value on unmount so a half-typed filter isn't lost.
64
+ useEffect(
65
+ () => () => {
66
+ debounced.flush();
67
+ },
68
+ [debounced],
69
+ );
70
+
71
+ const common = {
72
+ field,
73
+ control,
74
+ fieldPath: field.name,
75
+ value: draft,
76
+ onChange: handleChange,
77
+ className,
78
+ };
79
+
80
+ if (field.type === 'textarea') return <TextareaField {...common} />;
81
+ if (field.type === 'number') return <NumberField {...common} />;
82
+ return <TextField {...common} />;
83
+ }