@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.
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +483 -10
- package/dist/kit/builder/data-table/components/DataTable.d.ts +3 -1
- package/dist/kit/builder/data-table/components/DataTable.d.ts.map +1 -1
- package/dist/kit/components/data-table-headers/DataTableHeader.d.ts +25 -0
- package/dist/kit/components/data-table-headers/DataTableHeader.d.ts.map +1 -0
- package/dist/kit/components/data-table-headers/DebouncedFilterField.d.ts +23 -0
- package/dist/kit/components/data-table-headers/DebouncedFilterField.d.ts.map +1 -0
- package/dist/kit/components/data-table-headers/FilterFieldRenderer.d.ts +19 -0
- package/dist/kit/components/data-table-headers/FilterFieldRenderer.d.ts.map +1 -0
- package/dist/kit/components/data-table-headers/index.d.ts +7 -0
- package/dist/kit/components/data-table-headers/index.d.ts.map +1 -0
- package/dist/kit/components/data-table-headers/types.d.ts +76 -0
- package/dist/kit/components/data-table-headers/types.d.ts.map +1 -0
- package/dist/kit/components/data-table-headers/urlSerialization.d.ts +12 -0
- package/dist/kit/components/data-table-headers/urlSerialization.d.ts.map +1 -0
- package/dist/kit/components/data-table-headers/useDataTableHeaderUrl.d.ts +36 -0
- package/dist/kit/components/data-table-headers/useDataTableHeaderUrl.d.ts.map +1 -0
- package/dist/kit/themes/clean-slate.css +8 -0
- package/dist/kit/themes/default.css +8 -0
- package/dist/kit/themes/minimal-modern.css +8 -0
- package/dist/kit/themes/spotify.css +8 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/kit/builder/data-table/components/DataTable.tsx +5 -2
- package/src/kit/builder/form/components/fields/NumberField.tsx +1 -1
- package/src/kit/components/data-table-headers/DataTableHeader.tsx +158 -0
- package/src/kit/components/data-table-headers/DebouncedFilterField.tsx +83 -0
- package/src/kit/components/data-table-headers/FilterFieldRenderer.tsx +194 -0
- package/src/kit/components/data-table-headers/index.ts +6 -0
- package/src/kit/components/data-table-headers/types.ts +89 -0
- package/src/kit/components/data-table-headers/urlSerialization.ts +173 -0
- 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 @@
|
|
|
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
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
|
-
|
|
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
|
+
}
|