next-data-kit 1.0.1 → 3.0.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.
package/README.md CHANGED
@@ -4,14 +4,14 @@ A powerful table utility for server-side pagination, filtering, and sorting with
4
4
 
5
5
  ## Features
6
6
 
7
- - 🚀 **Server-side pagination** - Efficient data fetching with page-based navigation
8
- - 🔍 **Flexible filtering** - Support for regex, exact match, and custom filters
9
- - 📊 **Multi-column sorting** - Sort by multiple columns with customizable order
10
- - ⚛️ **React hooks** - `useDataKit`, `useSelection`, `usePagination` for state management
11
- - 🎨 **Components** - `DataKitTable` for tables, `DataKit` for custom layouts
12
- - 📝 **TypeScript** - Fully typed with generics support
13
- - 🔌 **Framework agnostic** - Works with any database ORM/ODM (Mongoose, Prisma, etc.)
14
- - 📦 **Tree-shakeable** - Import only what you need
7
+ - 🚀 **Server-side pagination** - Efficient data fetching with page-based navigation
8
+ - 🔍 **Flexible filtering** - Support for regex, exact match, and custom filters
9
+ - 📊 **Multi-column sorting** - Sort by multiple columns with customizable order
10
+ - ⚛️ **React hooks** - `useDataKit`, `useSelection`, `usePagination` for state management
11
+ - 🎨 **Components** - `DataKitTable` for tables, `DataKit` for custom layouts
12
+ - 📝 **TypeScript** - Fully typed with generics support
13
+ - 🔌 **Framework agnostic** - Works with any database ORM/ODM (Mongoose, Prisma, etc.)
14
+ - 📦 **Tree-shakeable** - Import only what you need
15
15
 
16
16
  ## Installation
17
17
 
@@ -28,17 +28,17 @@ pnpm add next-data-kit
28
28
  ### Server-side (Next.js Server Action)
29
29
 
30
30
  ```typescript
31
- "use server";
31
+ 'use server';
32
32
 
33
- import { dataKitServerAction, createSearchFilter } from "next-data-kit/server";
34
- import type { TDataKitInput } from "next-data-kit/types";
35
- import UserModel from "@/models/User";
33
+ import { dataKitServerAction, createSearchFilter } from 'next-data-kit/server';
34
+ import type { TDataKitInput } from 'next-data-kit/types';
35
+ import UserModel from '@/models/User';
36
36
 
37
37
  export async function fetchUsers(input: TDataKitInput) {
38
38
  return dataKitServerAction({
39
39
  model: UserModel,
40
40
  input,
41
- item: async (user) => ({
41
+ item: async user => ({
42
42
  id: user._id.toString(),
43
43
  name: user.name,
44
44
  email: user.email,
@@ -47,36 +47,41 @@ export async function fetchUsers(input: TDataKitInput) {
47
47
  active: true,
48
48
  }),
49
49
  filterCustom: {
50
- search: createSearchFilter(["name", "email"]),
51
- age: (value) => ({
52
- age: {
53
- $gte: value,
54
- }
55
- })
50
+ search: createSearchFilter(['name', 'email']),
51
+ age: value => ({
52
+ age: {
53
+ $gte: value,
54
+ },
55
+ }),
56
56
  },
57
+ // Only 'search' and 'age' filters are allowed (auto-extracted from filterCustom keys)
57
58
  });
58
59
  }
59
60
  ```
60
61
 
61
-
62
62
  ### Input Validation (Optional)
63
63
 
64
64
  You can use the built-in Zod schema to validate inputs before processing:
65
65
 
66
66
  ```typescript
67
- "use server";
67
+ 'use server';
68
68
 
69
- import { dataKitServerAction, dataKitSchemaZod } from "next-data-kit/server";
69
+ import { dataKitServerAction, dataKitSchemaZod } from 'next-data-kit/server';
70
70
 
71
71
  export async function fetchUsers(input: unknown) {
72
- // Validate input
73
- const parsedInput = dataKitSchemaZod.parse(input);
74
-
75
- return dataKitServerAction({
76
- model: UserModel,
77
- input: parsedInput,
78
- // ...
79
- });
72
+ // Validate input
73
+ const parsedInput = dataKitSchemaZod.parse(input);
74
+
75
+ return dataKitServerAction({
76
+ model: UserModel,
77
+ input: parsedInput,
78
+ item: user => ({ id: user._id.toString(), name: user.name }),
79
+ filterCustom: {
80
+ search: value => ({ name: { $regex: value, $options: 'i' } }),
81
+ role: value => ({ role: value }),
82
+ },
83
+ // Only 'search' and 'role' filters are allowed
84
+ });
80
85
  }
81
86
  ```
82
87
 
@@ -85,10 +90,10 @@ export async function fetchUsers(input: unknown) {
85
90
  Ready-to-use table with built-in filtering, sorting, and selection:
86
91
 
87
92
  ```tsx
88
- "use client";
93
+ 'use client';
89
94
 
90
- import { DataKitTable } from "next-data-kit/client";
91
- import { fetchUsers } from "@/actions/users";
95
+ import { DataKitTable } from 'next-data-kit/client';
96
+ import { fetchUsers } from '@/actions/users';
92
97
 
93
98
  export function UsersTable() {
94
99
  return (
@@ -96,14 +101,14 @@ export function UsersTable() {
96
101
  action={fetchUsers}
97
102
  limit={{ default: 10 }}
98
103
  filters={[
99
- { id: "search", label: "Search", type: "TEXT", placeholder: "Search..." },
104
+ { id: 'search', label: 'Search', type: 'TEXT', placeholder: 'Search...' },
100
105
  {
101
- id: "role",
102
- label: "Role",
103
- type: "SELECT",
106
+ id: 'role',
107
+ label: 'Role',
108
+ type: 'SELECT',
104
109
  dataset: [
105
- { id: "admin", name: "admin", label: "Admin" },
106
- { id: "user", name: "user", label: "User" },
110
+ { id: 'admin', name: 'admin', label: 'Admin' },
111
+ { id: 'user', name: 'user', label: 'User' },
107
112
  ],
108
113
  },
109
114
  ]}
@@ -111,9 +116,9 @@ export function UsersTable() {
111
116
  enabled: true,
112
117
  actions: {
113
118
  delete: {
114
- name: "Delete Selected",
115
- function: async (items) => {
116
- await deleteUsers(items.map((i) => i.id));
119
+ name: 'Delete Selected',
120
+ function: async items => {
121
+ await deleteUsers(items.map(i => i.id));
117
122
  return [true, { deselectAll: true }];
118
123
  },
119
124
  },
@@ -123,7 +128,7 @@ export function UsersTable() {
123
128
  {
124
129
  head: <DataKitTable.Head>Name</DataKitTable.Head>,
125
130
  body: ({ item }) => <DataKitTable.Cell>{item.name}</DataKitTable.Cell>,
126
- sortable: { path: "name", default: 0 },
131
+ sortable: { path: 'name', default: 0 },
127
132
  },
128
133
  {
129
134
  head: <DataKitTable.Head>Email</DataKitTable.Head>,
@@ -140,22 +145,18 @@ export function UsersTable() {
140
145
  Use `DataKit` for grids, cards, or any custom layout. It provides toolbar/pagination but lets you render content:
141
146
 
142
147
  ```tsx
143
- "use client";
148
+ 'use client';
144
149
 
145
- import { DataKit } from "next-data-kit/client";
146
- import { fetchUsers } from "@/actions/users";
150
+ import { DataKit } from 'next-data-kit/client';
151
+ import { fetchUsers } from '@/actions/users';
147
152
 
148
153
  export function UsersGrid() {
149
154
  return (
150
- <DataKit
151
- action={fetchUsers}
152
- limit={{ default: 12 }}
153
- filters={[{ id: "search", label: "Search", type: "TEXT" }]}
154
- >
155
- {(dataKit) => (
156
- <div className="grid grid-cols-4 gap-4">
157
- {dataKit.items.map((user) => (
158
- <div key={user.id} className="rounded-lg border p-4">
155
+ <DataKit action={fetchUsers} limit={{ default: 12 }} filters={[{ id: 'search', label: 'Search', type: 'TEXT' }]}>
156
+ {dataKit => (
157
+ <div className='grid grid-cols-4 gap-4'>
158
+ {dataKit.items.map(user => (
159
+ <div key={user.id} className='rounded-lg border p-4'>
159
160
  <h3>{user.name}</h3>
160
161
  <p>{user.email}</p>
161
162
  </div>
@@ -171,10 +172,12 @@ export function UsersGrid() {
171
172
 
172
173
  ```tsx
173
174
  <DataKit action={fetchUsers} manual>
174
- {(dataKit) => (
175
+ {dataKit => (
175
176
  <>
176
177
  {dataKit.state.isLoading && <Spinner />}
177
- {dataKit.items.map((user) => <Card key={user.id} user={user} />)}
178
+ {dataKit.items.map(user => (
179
+ <Card key={user.id} user={user} />
180
+ ))}
178
181
  </>
179
182
  )}
180
183
  </DataKit>
@@ -185,10 +188,10 @@ export function UsersGrid() {
185
188
  For fully custom implementations:
186
189
 
187
190
  ```tsx
188
- "use client";
191
+ 'use client';
189
192
 
190
- import { useDataKit } from "next-data-kit/client";
191
- import { fetchUsers } from "@/actions/users";
193
+ import { useDataKit } from 'next-data-kit/client';
194
+ import { fetchUsers } from '@/actions/users';
192
195
 
193
196
  export function UsersTable() {
194
197
  const {
@@ -206,7 +209,7 @@ export function UsersTable() {
206
209
 
207
210
  return (
208
211
  <div>
209
- <input placeholder="Search..." onChange={(e) => setFilter("search", e.target.value)} />
212
+ <input placeholder='Search...' onChange={e => setFilter('search', e.target.value)} />
210
213
 
211
214
  {isLoading ? (
212
215
  <p>Loading...</p>
@@ -214,12 +217,12 @@ export function UsersTable() {
214
217
  <table>
215
218
  <thead>
216
219
  <tr>
217
- <th onClick={() => setSort("name", 1)}>Name</th>
218
- <th onClick={() => setSort("email", 1)}>Email</th>
220
+ <th onClick={() => setSort('name', 1)}>Name</th>
221
+ <th onClick={() => setSort('email', 1)}>Email</th>
219
222
  </tr>
220
223
  </thead>
221
224
  <tbody>
222
- {items.map((user) => (
225
+ {items.map(user => (
223
226
  <tr key={user.id}>
224
227
  <td>{user.name}</td>
225
228
  <td>{user.email}</td>
@@ -253,18 +256,101 @@ type TDataKitServerActionOptions<T, R> = {
253
256
  model: TMongoModel<T>;
254
257
  item: (item: T) => Promise<R> | R;
255
258
  filter?: (filterInput?: Record<string, unknown>) => TMongoFilterQuery<T>;
259
+ // ** Custom filter configuration (defines allowed filter keys)
256
260
  filterCustom?: TFilterCustomConfigWithFilter<T, TMongoFilterQuery<T>>;
257
261
  defaultSort?: TSortOptions<T>;
262
+ // ** Maximum limit per page (default: 100)
263
+ maxLimit?: number;
264
+ // ** Whitelist of allowed query fields
265
+ queryAllowed?: string[];
258
266
  };
259
267
  ```
260
268
 
269
+ ```
270
+
271
+ // ... inside dataKitServerAction options
272
+ });
273
+ }
274
+ ```
275
+
276
+ ### Understanding `filter` vs `query`
277
+
278
+ There are two ways data reaches your database:
279
+
280
+ 1. **`filter` (via `filterCustom`)** - For user-facing filters with transformations
281
+ - Client-side `filters` prop → sends values to `filter` parameter
282
+ - Server validates against `filterCustom` keys
283
+ - You define how values transform into database queries
284
+ - **Use for**: search boxes, dropdowns, date ranges, etc.
285
+
286
+ 2. **`query` (via `queryAllowed`)** - For direct field matching
287
+ - Direct database field equality checks
288
+ - Must explicitly whitelist with `queryAllowed`
289
+ - **Use for**: fixed filters like `{ active: true }`, user-specific queries
290
+
291
+ ```typescript
292
+ dataKitServerAction({
293
+ model: UserModel,
294
+ input,
295
+ item: u => u,
296
+ // Client filters go through filterCustom
297
+ filterCustom: {
298
+ search: createSearchFilter(['name', 'email']),
299
+ role: value => ({ role: value }),
300
+ },
301
+ // Direct queries need explicit whitelist
302
+ queryAllowed: ['organizationId', 'active'],
303
+ });
304
+ ```
305
+
306
+ ### Security Note: Strict Mode by Default
307
+
308
+ **Filter Security**: When you define `filterCustom`, ONLY those keys are allowed. Any other filter key from the client will **THROW AN ERROR**.
309
+
310
+ **Query Security**: When you provide `queryAllowed`, only those query fields are accepted. Any other query field will throw an error.
311
+
312
+ ````typescript
313
+ // Strict Security Example
314
+ dataKitServerAction({
315
+ model: UserModel,
316
+ input,
317
+ item: u => ({ id: u._id.toString(), name: u.name }),
318
+ filterCustom: {
319
+ name: value => ({ name: { $regex: value, $options: 'i' } }),
320
+ email: value => ({ email: { $regex: value, $options: 'i' } }),
321
+ role: value => ({ role: value }),
322
+ },
323
+ // ONLY 'name', 'email', and 'role' filters are allowed
324
+ // If client sends { filter: { secret: "true" } }, this WILL THROW an Error!
325
+
326
+ // Query params need explicit whitelist
327
+ queryAllowed: ['status'],
328
+ });
329
+ ```
330
+
331
+ ### Error Handling on Client
332
+
333
+ When the server action throws an error (e.g., security violation), the client automatically handles it:
334
+
335
+ **`DataKitTable`**: Displays error in red within the table body
336
+
337
+ **`useDataKit`**: Error available in `state.error`
338
+
339
+ ```tsx
340
+ const { state: { error } } = useDataKit({ action: fetchUsers });
341
+
342
+ if (error) {
343
+ return <div className="text-red-500">Error: {error.message}</div>;
344
+ }
345
+ ```
346
+
261
347
  #### `createSearchFilter(fields)`
262
348
 
263
- Create a search filter for multiple fields.
349
+ Create a search filter for multiple fields. Use this in `filterCustom`.
264
350
 
265
351
  ```typescript
266
352
  filterCustom: {
267
- search: createSearchFilter(["name", "email", "phone"]);
353
+ search: createSearchFilter(['name', 'email', 'phone']),
268
354
  }
269
355
  ```
270
356
 
@@ -296,38 +382,39 @@ filterCustom: {
296
382
  priceRange: (value: { min: number; max: number }) => ({
297
383
  price: { $gte: value.min, $lte: value.max },
298
384
  }),
299
- }
385
+ },
386
+ // Only 'search' and 'priceRange' filters are allowed from client
300
387
  ```
301
388
 
302
389
  #### Understanding `filterCustom` Flow
303
390
 
304
391
  To use custom filters effectively, you must match the **Key** on the client with the **Key** on the server.
305
392
 
306
- 1. **Client-side**: Define a filter with a specific `id` (e.g., `'priceRange'`).
307
- ```tsx
308
- // Client Component
309
- <DataKitTable
310
- filters={[
311
- { id: 'priceRange', label: 'Price Range', type: 'TEXT' }
312
- ]}
313
- // ...
314
- />
315
- ```
316
-
317
- _Note: When use interact with this filter, `DataKit` sends `{ filter: { priceRange: "value" } }` to the server._
318
-
319
- 2. **Server-side**: Handle that key in `filterCustom`.
320
- ```typescript
321
- // Server Action
322
- filterCustom: {
323
- // MATCHES 'priceRange' FROM CLIENT
324
- priceRange: (value) => ({
325
- price: { $lte: Number(value) }
326
- })
327
- }
328
- ```
329
-
330
- The `filterCustom` function intercepts the value sent from the client before it hits the database query builder, allowing you to transform simple values into complex queries.
393
+ 1. **Client-side**: Define a filter with a specific `id` (e.g., `'priceRange'`).
394
+
395
+ ```tsx
396
+ // Client Component
397
+ <DataKitTable
398
+ filters={[{ id: 'priceRange', label: 'Price Range', type: 'TEXT' }]}
399
+ // ...
400
+ />
401
+ ```
402
+
403
+ _Note: When use interact with this filter, `DataKit` sends `{ filter: { priceRange: "value" } }` to the server._
404
+
405
+ 2. **Server-side**: Handle that key in `filterCustom`.
406
+
407
+ ```typescript
408
+ // Server Action
409
+ filterCustom: {
410
+ // MATCHES 'priceRange' FROM CLIENT
411
+ priceRange: value => ({
412
+ price: { $lte: Number(value) },
413
+ });
414
+ }
415
+ ```
416
+
417
+ The `filterCustom` function intercepts the value sent from the client before it hits the database query builder, allowing you to transform simple values into complex queries.
331
418
 
332
419
  **Client Usage:**
333
420
 
@@ -339,10 +426,10 @@ const {
339
426
  });
340
427
 
341
428
  // Trigger the manual search
342
- setFilter("search", "query string");
429
+ setFilter('search', 'query string');
343
430
 
344
431
  // Trigger the range filter
345
- setFilter("priceRange", { min: 10, max: 100 });
432
+ setFilter('priceRange', { min: 10, max: 100 });
346
433
  ```
347
434
 
348
435
  ### Client
@@ -351,29 +438,29 @@ setFilter("priceRange", { min: 10, max: 100 });
351
438
 
352
439
  Full-featured table component with built-in UI.
353
440
 
354
- | Prop | Type | Description |
355
- |------|------|-------------|
356
- | `action` | `(input) => Promise<Result>` | Server action function |
357
- | `table` | `Column[]` | Column definitions |
358
- | `filters` | `FilterItem[]` | Filter configurations |
359
- | `selectable` | `{ enabled, actions? }` | Selection & bulk actions |
360
- | `limit` | `{ default: number }` | Items per page |
361
- | `controller` | `Ref<Controller>` | External control ref |
362
- | `className` | `string` | Container class |
363
- | `bordered` | `boolean \| 'rounded'` | Border style |
364
- | `refetchInterval` | `number` | Auto-refresh interval (ms) |
441
+ | Prop | Type | Description |
442
+ | ----------------- | ---------------------------- | -------------------------- |
443
+ | `action` | `(input) => Promise<Result>` | Server action function |
444
+ | `table` | `Column[]` | Column definitions |
445
+ | `filters` | `FilterItem[]` | Filter configurations |
446
+ | `selectable` | `{ enabled, actions? }` | Selection & bulk actions |
447
+ | `limit` | `{ default: number }` | Items per page |
448
+ | `controller` | `Ref<Controller>` | External control ref |
449
+ | `className` | `string` | Container class |
450
+ | `bordered` | `boolean \| 'rounded'` | Border style |
451
+ | `refetchInterval` | `number` | Auto-refresh interval (ms) |
365
452
 
366
453
  #### `<DataKit>` Component
367
454
 
368
455
  Headless component for custom layouts (grids, cards, etc).
369
456
 
370
- | Prop | Type | Description |
371
- |------|------|-------------|
372
- | `action` | `(input) => Promise<Result>` | Server action function |
373
- | `filters` | `FilterItem[]` | Filter configurations |
374
- | `limit` | `{ default: number }` | Items per page |
375
- | `manual` | `boolean` | Skip loading/empty state handling |
376
- | `children` | `(dataKit) => ReactNode` | Render function |
457
+ | Prop | Type | Description |
458
+ | ---------- | ---------------------------- | --------------------------------- |
459
+ | `action` | `(input) => Promise<Result>` | Server action function |
460
+ | `filters` | `FilterItem[]` | Filter configurations |
461
+ | `limit` | `{ default: number }` | Items per page |
462
+ | `manual` | `boolean` | Skip loading/empty state handling |
463
+ | `children` | `(dataKit) => ReactNode` | Render function |
377
464
 
378
465
  #### `useDataKit(options)`
379
466
 
@@ -398,23 +485,23 @@ interface TDataKitControllerOptions<T, R> {
398
485
 
399
486
  Returns:
400
487
 
401
- - `items` - Current page items
402
- - `page` - Current page number
403
- - `limit` - Items per page
404
- - `total` - Total document count
405
- - `sorts` - Current sort configuration
406
- - `filter` - Current filter values
407
- - `state`
408
- - `isLoading` - Loading state
409
- - `error` - Error state
410
- - `actions`
411
- - `setPage(page)` - Go to a specific page
412
- - `setLimit(limit)` - Set items per page
413
- - `setSort(path, value)` - Set sort for a column
414
- - `setFilter(key, value)` - Set a filter value
415
- - `clearFilters()` - Clear all filters
416
- - `refresh()` - Refresh the table data
417
- - `reset()` - Reset to initial state
488
+ - `items` - Current page items
489
+ - `page` - Current page number
490
+ - `limit` - Items per page
491
+ - `total` - Total document count
492
+ - `sorts` - Current sort configuration
493
+ - `filter` - Current filter values
494
+ - `state`
495
+ - `isLoading` - Loading state
496
+ - `error` - Error state
497
+ - `actions`
498
+ - `setPage(page)` - Go to a specific page
499
+ - `setLimit(limit)` - Set items per page
500
+ - `setSort(path, value)` - Set sort for a column
501
+ - `setFilter(key, value)` - Set a filter value
502
+ - `clearFilters()` - Clear all filters
503
+ - `refresh()` - Refresh the table data
504
+ - `reset()` - Reset to initial state
418
505
 
419
506
  #### `useSelection<T>()`
420
507
 
@@ -455,7 +542,7 @@ const { pages, hasNextPage, hasPrevPage, totalPages } = usePagination({
455
542
 
456
543
  ```typescript
457
544
  interface TDataKitInput<T = unknown> {
458
- action?: "FETCH";
545
+ action?: 'FETCH';
459
546
  page?: number;
460
547
  limit?: number;
461
548
  sort?: TSortOptions<T>;
@@ -470,7 +557,7 @@ interface TDataKitInput<T = unknown> {
470
557
 
471
558
  ```typescript
472
559
  interface TDataKitResult<R> {
473
- type: "ITEMS";
560
+ type: 'ITEMS';
474
561
  items: R[];
475
562
  documentTotal: number;
476
563
  }
@@ -481,7 +568,7 @@ interface TDataKitResult<R> {
481
568
  ```typescript
482
569
  interface TFilterConfig {
483
570
  [key: string]: {
484
- type: "regex" | "exact";
571
+ type: 'REGEX' | 'EXACT';
485
572
  field?: string;
486
573
  };
487
574
  }
@@ -492,7 +579,7 @@ interface TFilterConfig {
492
579
  The package provides generic database types that work with any ORM/ODM:
493
580
 
494
581
  ```typescript
495
- import type { TModel, TMongoFilterQuery, TQueryBuilder } from "next-data-kit/types";
582
+ import type { TModel, TMongoFilterQuery, TQueryBuilder } from 'next-data-kit/types';
496
583
 
497
584
  // Your model just needs to implement the Model interface
498
585
  interface MyModel extends TModel<MyDocument> {
@@ -509,8 +596,8 @@ MIT © muhgholy
509
596
 
510
597
  This repo includes a **dev-only** playground you can open in the browser to preview components and validate end-to-end behavior against a **temporary in-memory MongoDB**.
511
598
 
512
- - It is **not exported** in `package.json#exports`.
513
- - It is **not published** to npm because `package.json#files` only includes `dist`.
599
+ - It is **not exported** in `package.json#exports`.
600
+ - It is **not published** to npm because `package.json#files` only includes `dist`.
514
601
 
515
602
  ### Run
516
603
 
@@ -520,8 +607,8 @@ npm run playground:dev
520
607
 
521
608
  Then open:
522
609
 
523
- - Web UI: http://localhost:5173
524
- - API health: http://127.0.0.1:8787/api/health
610
+ - Web UI: http://localhost:5173
611
+ - API health: http://127.0.0.1:8787/api/health
525
612
 
526
613
  ### Reset seed data
527
614
 
@@ -1 +1 @@
1
- {"version":3,"file":"data-kit-table.d.ts","sourceRoot":"","sources":["../../../src/client/components/data-kit-table.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,EAkBH,SAAS,EACT,SAAS,EAGZ,MAAM,MAAM,CAAC;AACd,OAAO,KAAK,EACR,aAAa,EACb,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,sBAAsB,EACtB,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EAChB,MAAM,aAAa,CAAC;AAsXrB,eAAO,MAAM,YAAY,IA/WrB,OAAO,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,cAAc,CAAC,sBAAsB,CAAC,CAAC,EAClG,SAAS,mBACJ,QAAQ,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,uBAAuB,CAAC,uBAAuB,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,CAAC;IAC9E,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,UAAU,CAAC,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;KAClF,CAAC;IACF,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,UAAU,CAAC,EAAE,KAAK,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CAC7G,CAAC;;;CA6VA,CAAC"}
1
+ {"version":3,"file":"data-kit-table.d.ts","sourceRoot":"","sources":["../../../src/client/components/data-kit-table.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAKxE,OAAO,EAkBH,SAAS,EACT,SAAS,EAGZ,MAAM,MAAM,CAAC;AACd,OAAO,KAAK,EACR,aAAa,EACb,cAAc,EACd,uBAAuB,EACvB,kBAAkB,EAClB,kBAAkB,EAClB,2BAA2B,EAC3B,sBAAsB,EACtB,iBAAiB,EACjB,uBAAuB,EACvB,aAAa,EAChB,MAAM,aAAa,CAAC;AA4XrB,eAAO,MAAM,YAAY,IArXrB,OAAO,SAAS,CAAC,KAAK,EAAE,aAAa,CAAC,OAAO,CAAC,KAAK,OAAO,CAAC,cAAc,CAAC,sBAAsB,CAAC,CAAC,EAClG,SAAS,mBACJ,QAAQ,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,YAAY,CAAC,EAAE,aAAa,CAAC;IAC7B,KAAK,EAAE,uBAAuB,CAAC,uBAAuB,CAAC,OAAO,CAAC,EAAE,SAAS,CAAC,EAAE,CAAC;IAC9E,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,UAAU,CAAC,EAAE;QACT,OAAO,EAAE,OAAO,CAAC;QACjB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,kBAAkB,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;KAClF,CAAC;IACF,YAAY,CAAC,EAAE,SAAS,CAAC;IACzB,KAAK,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC5B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,KAAK,CAAC,EAAE,iBAAiB,CAAC;IAC1B,UAAU,CAAC,EAAE,KAAK,CAAC,gBAAgB,CAAC,2BAA2B,CAAC,uBAAuB,CAAC,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;CAC7G,CAAC;;;CAmWA,CAAC"}
@@ -118,7 +118,7 @@ const DataKitRoot = (props) => {
118
118
  }, [refetchInterval, isVisible, dataKit.actions]);
119
119
  useEffect(() => { selection.deselectAll(); }, [dataKit.items.length]);
120
120
  // ** Render
121
- return (_jsxs("div", { ref: tableRef, className: `space-y-3 ${className ?? ''}`, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("div", { className: "flex items-center gap-2", children: filters.length > 0 && (_jsxs(Popover, { open: isFilterOpen, onOpenChange: setIsFilterOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Filter, { className: "mr-1.5 size-4" }), "Filters"] }) }), _jsxs(PopoverContent, { align: "start", className: "w-80", container: overlayContainer, children: [_jsx("div", { className: "grid gap-3", children: filters.map((f) => (_jsxs("div", { className: "grid gap-1.5", children: [_jsx("label", { className: "text-sm font-medium", children: f.label }), f.type === 'TEXT' && (_jsx("input", { type: "text", className: "h-9 w-full rounded-md border bg-transparent px-3 text-sm outline-none focus:ring-2 focus:ring-ring", placeholder: f.placeholder, value: dataKit.filter[f.id] ?? '', onChange: (e) => dataKit.actions.setFilter(f.id, e.target.value) })), f.type === 'SELECT' && (_jsxs(Select, { value: String(dataKit.filter[f.id] || '__all__'), onValueChange: (v) => dataKit.actions.setFilter(f.id, v === '__all__' ? '' : v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { container: overlayContainer, children: [_jsx(SelectItem, { value: "__all__", children: "All" }), f.dataset?.map((d) => (_jsx(SelectItem, { value: d.id, children: d.label }, d.id)))] })] })), f.type === 'BOOLEAN' && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: f.placeholder ?? 'Enable' }), _jsx(Switch, { checked: Boolean(dataKit.filter[f.id]), onCheckedChange: (c) => dataKit.actions.setFilter(f.id, c) })] }))] }, f.id))) }), _jsxs("div", { className: "mt-4 flex justify-between border-t pt-3", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: handleResetFilters, children: "Reset" }), _jsx(Button, { size: "sm", onClick: () => setIsFilterOpen(false), children: "Done" })] })] })] })) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("span", { className: "mr-2 text-sm text-muted-foreground", children: [dataKit.items.length, " of ", dataKit.total] }), _jsxs(Select, { value: String(dataKit.limit), onValueChange: (v) => dataKit.actions.setLimit(Number(v)), disabled: dataKit.state.isLoading, children: [_jsx(SelectTrigger, { className: "w-16", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { container: overlayContainer, children: [10, 25, 50, 100].map((v) => (_jsx(SelectItem, { value: String(v), children: v }, v))) })] })] })] }), _jsx("div", { className: `overflow-hidden border border-gray-200 dark:border-gray-800 ${bordered === 'rounded' ? 'rounded-lg' : bordered ? '' : 'rounded-lg'}`, children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [selectable?.enabled && (_jsx(TableHead, { className: "w-12", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { checked: selection.isIndeterminate ? 'indeterminate' : selection.isAllSelected, onCheckedChange: () => selection.toggleAll() }), selectable.actions && Object.keys(selectable.actions).length > 0 && (_jsxs(DropdownMenu, { open: actionsMenuOpen, onOpenChange: setActionsMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "size-6", disabled: selectedCount === 0 || !!actionLoading, children: actionLoading ? _jsx(Loader2, { className: "size-4 animate-spin" }) : _jsx(MoreHorizontal, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { align: "start", container: overlayContainer, children: Object.entries(selectable.actions).map(([key, action]) => (_jsx(DropdownMenuItem, { disabled: !!actionLoading, onSelect: () => handleSelectionAction(key), children: actionLoading === key ? 'Working…' : action.name }, key))) })] }))] }) })), columns.map((col, idx) => (_jsx(React.Fragment, { children: col.sortable ? (_jsx(TableHead, { ...(React.isValidElement(col.head) ? col.head.props : {}), children: _jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-3", onClick: () => handleSort(col.sortable.path), children: [React.isValidElement(col.head) ? col.head.props.children : col.head, getSortFor(col.sortable.path) === 1 && _jsx(ArrowUp, { className: "ml-1 size-4" }), getSortFor(col.sortable.path) === -1 && _jsx(ArrowDown, { className: "ml-1 size-4" })] }) })) : (col.head) }, idx)))] }) }), _jsx(TableBody, { children: dataKit.state.isLoading ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: colSpan, className: "h-24 text-center", children: _jsx(Loader2, { className: "mx-auto size-5 animate-spin" }) }) })) : dataKit.items.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: colSpan, className: "h-24 text-center text-muted-foreground", children: "No results found." }) })) : (dataKit.items.map((item, idx) => (_jsxs(TableRow, { children: [selectable?.enabled && (_jsx(TableCell, { children: _jsx(Checkbox, { checked: selection.isSelected(item.id), onCheckedChange: () => selection.toggle(item.id) }) })), columns.map((col, colIdx) => (_jsx(React.Fragment, { children: col.body({
121
+ return (_jsxs("div", { ref: tableRef, className: `space-y-3 ${className ?? ''}`, children: [_jsxs("div", { className: "flex items-center justify-between gap-2", children: [_jsx("div", { className: "flex items-center gap-2", children: filters.length > 0 && (_jsxs(Popover, { open: isFilterOpen, onOpenChange: setIsFilterOpen, children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { variant: "outline", size: "sm", children: [_jsx(Filter, { className: "mr-1.5 size-4" }), "Filters"] }) }), _jsxs(PopoverContent, { align: "start", className: "w-80", container: overlayContainer, children: [_jsx("div", { className: "grid gap-3", children: filters.map((f) => (_jsxs("div", { className: "grid gap-1.5", children: [_jsx("label", { className: "text-sm font-medium", children: f.label }), f.type === 'TEXT' && (_jsx("input", { type: "text", className: "h-9 w-full rounded-md border bg-transparent px-3 text-sm outline-none focus:ring-2 focus:ring-ring", placeholder: f.placeholder, value: dataKit.filter[f.id] ?? '', onChange: (e) => dataKit.actions.setFilter(f.id, e.target.value) })), f.type === 'SELECT' && (_jsxs(Select, { value: String(dataKit.filter[f.id] || '__all__'), onValueChange: (v) => dataKit.actions.setFilter(f.id, v === '__all__' ? '' : v), children: [_jsx(SelectTrigger, { children: _jsx(SelectValue, {}) }), _jsxs(SelectContent, { container: overlayContainer, children: [_jsx(SelectItem, { value: "__all__", children: "All" }), f.dataset?.map((d) => (_jsx(SelectItem, { value: d.id, children: d.label }, d.id)))] })] })), f.type === 'BOOLEAN' && (_jsxs("div", { className: "flex items-center justify-between", children: [_jsx("span", { className: "text-sm text-muted-foreground", children: f.placeholder ?? 'Enable' }), _jsx(Switch, { checked: Boolean(dataKit.filter[f.id]), onCheckedChange: (c) => dataKit.actions.setFilter(f.id, c) })] }))] }, f.id))) }), _jsxs("div", { className: "mt-4 flex justify-between border-t pt-3", children: [_jsx(Button, { variant: "outline", size: "sm", onClick: handleResetFilters, children: "Reset" }), _jsx(Button, { size: "sm", onClick: () => setIsFilterOpen(false), children: "Done" })] })] })] })) }), _jsxs("div", { className: "flex items-center gap-1", children: [_jsxs("span", { className: "mr-2 text-sm text-muted-foreground", children: [dataKit.items.length, " of ", dataKit.total] }), _jsxs(Select, { value: String(dataKit.limit), onValueChange: (v) => dataKit.actions.setLimit(Number(v)), disabled: dataKit.state.isLoading, children: [_jsx(SelectTrigger, { className: "w-16", children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { container: overlayContainer, children: [10, 25, 50, 100].map((v) => (_jsx(SelectItem, { value: String(v), children: v }, v))) })] })] })] }), _jsx("div", { className: `overflow-hidden border border-gray-200 dark:border-gray-800 ${bordered === 'rounded' ? 'rounded-lg' : bordered ? '' : 'rounded-lg'}`, children: _jsxs(Table, { children: [_jsx(TableHeader, { children: _jsxs(TableRow, { children: [selectable?.enabled && (_jsx(TableHead, { className: "w-12", children: _jsxs("div", { className: "flex items-center gap-2", children: [_jsx(Checkbox, { checked: selection.isIndeterminate ? 'indeterminate' : selection.isAllSelected, onCheckedChange: () => selection.toggleAll() }), selectable.actions && Object.keys(selectable.actions).length > 0 && (_jsxs(DropdownMenu, { open: actionsMenuOpen, onOpenChange: setActionsMenuOpen, children: [_jsx(DropdownMenuTrigger, { asChild: true, children: _jsx(Button, { variant: "ghost", size: "icon", className: "size-6", disabled: selectedCount === 0 || !!actionLoading, children: actionLoading ? _jsx(Loader2, { className: "size-4 animate-spin" }) : _jsx(MoreHorizontal, { className: "size-4" }) }) }), _jsx(DropdownMenuContent, { align: "start", container: overlayContainer, children: Object.entries(selectable.actions).map(([key, action]) => (_jsx(DropdownMenuItem, { disabled: !!actionLoading, onSelect: () => handleSelectionAction(key), children: actionLoading === key ? 'Working…' : action.name }, key))) })] }))] }) })), columns.map((col, idx) => (_jsx(React.Fragment, { children: col.sortable ? (_jsx(TableHead, { ...(React.isValidElement(col.head) ? col.head.props : {}), children: _jsxs(Button, { variant: "ghost", size: "sm", className: "-ml-3", onClick: () => handleSort(col.sortable.path), children: [React.isValidElement(col.head) ? col.head.props.children : col.head, getSortFor(col.sortable.path) === 1 && _jsx(ArrowUp, { className: "ml-1 size-4" }), getSortFor(col.sortable.path) === -1 && _jsx(ArrowDown, { className: "ml-1 size-4" })] }) })) : (col.head) }, idx)))] }) }), _jsx(TableBody, { children: dataKit.state.isLoading ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: colSpan, className: "h-24 text-center", children: _jsx(Loader2, { className: "mx-auto size-5 animate-spin" }) }) })) : dataKit.state.error ? (_jsx(TableRow, { children: _jsxs(TableCell, { colSpan: colSpan, className: "h-24 text-center text-red-500", children: ["Error: ", dataKit.state.error.message] }) })) : dataKit.items.length === 0 ? (_jsx(TableRow, { children: _jsx(TableCell, { colSpan: colSpan, className: "h-24 text-center text-muted-foreground", children: "No results found." }) })) : (dataKit.items.map((item, idx) => (_jsxs(TableRow, { children: [selectable?.enabled && (_jsx(TableCell, { children: _jsx(Checkbox, { checked: selection.isSelected(item.id), onCheckedChange: () => selection.toggle(item.id) }) })), columns.map((col, colIdx) => (_jsx(React.Fragment, { children: col.body({
122
122
  item,
123
123
  index: idx,
124
124
  state: initialState,