@papernote/ui 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/components/Box.d.ts +2 -1
  2. package/dist/components/Box.d.ts.map +1 -1
  3. package/dist/components/Button.d.ts +10 -1
  4. package/dist/components/Button.d.ts.map +1 -1
  5. package/dist/components/Card.d.ts +11 -2
  6. package/dist/components/Card.d.ts.map +1 -1
  7. package/dist/components/DataTable.d.ts +17 -3
  8. package/dist/components/DataTable.d.ts.map +1 -1
  9. package/dist/components/EmptyState.d.ts +3 -1
  10. package/dist/components/EmptyState.d.ts.map +1 -1
  11. package/dist/components/Grid.d.ts +4 -2
  12. package/dist/components/Grid.d.ts.map +1 -1
  13. package/dist/components/Input.d.ts +2 -0
  14. package/dist/components/Input.d.ts.map +1 -1
  15. package/dist/components/MultiSelect.d.ts +13 -1
  16. package/dist/components/MultiSelect.d.ts.map +1 -1
  17. package/dist/components/Stack.d.ts +25 -5
  18. package/dist/components/Stack.d.ts.map +1 -1
  19. package/dist/components/Text.d.ts +20 -4
  20. package/dist/components/Text.d.ts.map +1 -1
  21. package/dist/components/Textarea.d.ts +2 -0
  22. package/dist/components/Textarea.d.ts.map +1 -1
  23. package/dist/components/index.d.ts +1 -3
  24. package/dist/components/index.d.ts.map +1 -1
  25. package/dist/index.d.ts +110 -48
  26. package/dist/index.esm.js +144 -138
  27. package/dist/index.esm.js.map +1 -1
  28. package/dist/index.js +143 -138
  29. package/dist/index.js.map +1 -1
  30. package/dist/styles.css +8 -51
  31. package/package.json +1 -1
  32. package/src/components/Box.stories.tsx +377 -0
  33. package/src/components/Box.tsx +8 -4
  34. package/src/components/Button.tsx +23 -10
  35. package/src/components/Card.tsx +20 -5
  36. package/src/components/DataTable.stories.tsx +36 -25
  37. package/src/components/DataTable.tsx +95 -5
  38. package/src/components/EmptyState.stories.tsx +124 -72
  39. package/src/components/EmptyState.tsx +10 -0
  40. package/src/components/Grid.stories.tsx +348 -0
  41. package/src/components/Grid.tsx +12 -5
  42. package/src/components/Input.tsx +12 -2
  43. package/src/components/MultiSelect.tsx +41 -10
  44. package/src/components/Stack.stories.tsx +24 -1
  45. package/src/components/Stack.tsx +40 -10
  46. package/src/components/Text.stories.tsx +273 -0
  47. package/src/components/Text.tsx +33 -8
  48. package/src/components/Textarea.tsx +32 -21
  49. package/src/components/index.ts +1 -4
  50. package/dist/components/Table.d.ts +0 -26
  51. package/dist/components/Table.d.ts.map +0 -1
  52. package/src/components/Table.tsx +0 -239
@@ -1,3 +1,4 @@
1
+ import React from 'react';
1
2
  import type { Meta, StoryObj } from '@storybook/react';
2
3
  import DataTable from './DataTable';
3
4
  import Badge from './Badge';
@@ -126,14 +127,6 @@ const actions = [
126
127
  defaultValue: { summary: 'false' },
127
128
  },
128
129
  },
129
- multiSelect: {
130
- control: 'boolean',
131
- description: 'Allow multiple row selection (requires selectable=true)',
132
- table: {
133
- type: { summary: 'boolean' },
134
- defaultValue: { summary: 'true' },
135
- },
136
- },
137
130
  actions: {
138
131
  description: 'Row actions displayed in sticky actions column',
139
132
  table: {
@@ -179,23 +172,21 @@ type Story = StoryObj<typeof meta>;
179
172
  const columns = [
180
173
  {
181
174
  key: 'name',
182
- label: 'Name',
175
+ header: 'Name',
183
176
  sortable: true,
184
- filterable: true,
185
177
  },
186
178
  {
187
179
  key: 'email',
188
- label: 'Email',
180
+ header: 'Email',
189
181
  sortable: true,
190
182
  },
191
183
  {
192
184
  key: 'role',
193
- label: 'Role',
194
- filterable: true,
185
+ header: 'Role',
195
186
  },
196
187
  {
197
188
  key: 'status',
198
- label: 'Status',
189
+ header: 'Status',
199
190
  render: (user: User) => {
200
191
  const variant = user.status === 'active' ? 'success' : user.status === 'inactive' ? 'error' : 'warning';
201
192
  return <Badge variant={variant}>{user.status}</Badge>;
@@ -203,7 +194,7 @@ const columns = [
203
194
  },
204
195
  {
205
196
  key: 'joinedAt',
206
- label: 'Joined',
197
+ header: 'Joined',
207
198
  sortable: true,
208
199
  },
209
200
  ];
@@ -234,7 +225,7 @@ export const WithActions: Story = {
234
225
  label: 'Delete',
235
226
  icon: <Trash className="h-4 w-4" />,
236
227
  onClick: (user: User) => alert(`Delete ${user.name}`),
237
- danger: true,
228
+ variant: 'danger',
238
229
  },
239
230
  ],
240
231
  },
@@ -245,16 +236,36 @@ export const Selectable: Story = {
245
236
  data: sampleUsers,
246
237
  columns,
247
238
  selectable: true,
248
- onSelectionChange: (selected: User[]) => console.log('Selected:', selected),
239
+ onRowSelect: (selected: string[]) => console.log('Selected:', selected),
249
240
  },
250
241
  };
251
242
 
252
243
  export const Paginated: Story = {
253
- args: {
254
- data: sampleUsers,
255
- columns,
256
- paginated: true,
257
- pageSize: 3,
244
+ render: () => {
245
+ const [currentPage, setCurrentPage] = React.useState(1);
246
+ const [pageSize, setPageSize] = React.useState(2);
247
+
248
+ // Simulate client-side pagination
249
+ const paginatedData = sampleUsers.slice(
250
+ (currentPage - 1) * pageSize,
251
+ currentPage * pageSize
252
+ );
253
+
254
+ return (
255
+ <DataTable
256
+ data={paginatedData}
257
+ columns={columns}
258
+ paginated
259
+ currentPage={currentPage}
260
+ pageSize={pageSize}
261
+ totalItems={sampleUsers.length}
262
+ onPageChange={setCurrentPage}
263
+ onPageSizeChange={(size) => {
264
+ setPageSize(size);
265
+ setCurrentPage(1);
266
+ }}
267
+ />
268
+ );
258
269
  },
259
270
  };
260
271
 
@@ -263,6 +274,7 @@ export const WithExpandedRows: Story = {
263
274
  data: sampleUsers,
264
275
  columns,
265
276
  expandable: true,
277
+ showExpandChevron: true,
266
278
  renderExpandedRow: (user: User) => (
267
279
  <div style={{ padding: '1rem', backgroundColor: '#f5f5f4' }}>
268
280
  <h4 style={{ marginBottom: '0.5rem', fontWeight: 600 }}>User Details</h4>
@@ -311,8 +323,6 @@ export const FullFeatured: Story = {
311
323
  data: sampleUsers,
312
324
  columns,
313
325
  selectable: true,
314
- paginated: true,
315
- pageSize: 3,
316
326
  actions: [
317
327
  {
318
328
  label: 'Edit',
@@ -323,10 +333,11 @@ export const FullFeatured: Story = {
323
333
  label: 'Delete',
324
334
  icon: <Trash className="h-4 w-4" />,
325
335
  onClick: (user: User) => alert(`Delete ${user.name}`),
326
- danger: true,
336
+ variant: 'danger',
327
337
  },
328
338
  ],
329
339
  expandable: true,
340
+ showExpandChevron: true,
330
341
  renderExpandedRow: (user: User) => (
331
342
  <div style={{ padding: '1rem' }}>
332
343
  <p>Additional details for {user.name}</p>
@@ -3,6 +3,8 @@ import React, { useState, useRef, useEffect } from 'react';
3
3
  import { createPortal } from 'react-dom';
4
4
  import { ChevronDown, ChevronRight, MoreVertical, Edit, Trash } from 'lucide-react';
5
5
  import Menu, { MenuItem } from './Menu';
6
+ import Pagination from './Pagination';
7
+ import Select from './Select';
6
8
 
7
9
  /**
8
10
  * Base data item interface - all data items must have an id
@@ -13,8 +15,6 @@ import Menu, { MenuItem } from './Menu';
13
15
  export interface BaseDataItem {
14
16
  /** Unique identifier for the data item */
15
17
  id: string | number;
16
- /** Additional properties specific to your data type */
17
- [key: string]: unknown;
18
18
  }
19
19
 
20
20
  /**
@@ -229,6 +229,24 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
229
229
  virtualHeight?: string;
230
230
  /** Row height for virtual scrolling (default: 60) */
231
231
  virtualRowHeight?: number;
232
+
233
+ // Pagination props
234
+ /** Enable built-in pagination (renders Pagination component above table) */
235
+ paginated?: boolean;
236
+ /** Current page number (1-indexed) */
237
+ currentPage?: number;
238
+ /** Number of items per page */
239
+ pageSize?: number;
240
+ /** Total number of items (for server-side pagination) */
241
+ totalItems?: number;
242
+ /** Callback when page changes */
243
+ onPageChange?: (page: number) => void;
244
+ /** Available page size options (default: [10, 25, 50, 100]) */
245
+ pageSizeOptions?: number[];
246
+ /** Callback when page size changes */
247
+ onPageSizeChange?: (pageSize: number) => void;
248
+ /** Show page size selector (default: true when paginated) */
249
+ showPageSizeSelector?: boolean;
232
250
  }
233
251
 
234
252
  /**
@@ -490,6 +508,15 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
490
508
  virtualized = false,
491
509
  virtualHeight = '600px',
492
510
  virtualRowHeight = 60,
511
+ // Pagination props
512
+ paginated = false,
513
+ currentPage = 1,
514
+ pageSize = 10,
515
+ totalItems,
516
+ onPageChange,
517
+ pageSizeOptions = [10, 25, 50, 100],
518
+ onPageSizeChange,
519
+ showPageSizeSelector = true,
493
520
  }: DataTableProps<T>) {
494
521
  // Column resizing state
495
522
  const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
@@ -761,17 +788,31 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
761
788
  });
762
789
  }
763
790
 
791
+ // Combine all actions: built-in first, then custom actions, then delete last
792
+ // Delete is stored separately to ensure it's always last
793
+ let deleteAction: DataTableAction<T> | null = null;
764
794
  if (onDelete) {
765
- builtInActions.push({
795
+ deleteAction = {
766
796
  label: 'Delete',
767
797
  icon: Trash,
768
798
  onClick: onDelete,
769
799
  variant: 'danger',
770
800
  tooltip: 'Delete item'
771
- });
801
+ };
772
802
  }
773
803
 
774
- const allActions = [...builtInActions, ...actions];
804
+ // Build final actions array with consistent ordering:
805
+ // 1. Edit (first - most common action)
806
+ // 2. View Details
807
+ // 3. Add Related actions
808
+ // 4. Manage Related actions
809
+ // 5. Custom actions (from actions prop)
810
+ // 6. Delete (always last - destructive action)
811
+ const allActions: DataTableAction<T>[] = [
812
+ ...builtInActions,
813
+ ...actions,
814
+ ...(deleteAction ? [deleteAction] : [])
815
+ ];
775
816
 
776
817
  // Convert actions to menu items for context menu
777
818
  const convertActionsToMenuItems = (item: T): MenuItem[] => {
@@ -1447,9 +1488,58 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
1447
1488
  </div>
1448
1489
  ) : tableContent;
1449
1490
 
1491
+ // Calculate pagination values
1492
+ const effectiveTotalItems = totalItems ?? data.length;
1493
+ const totalPages = Math.ceil(effectiveTotalItems / pageSize);
1494
+
1495
+ // Page size selector options
1496
+ const pageSizeSelectOptions = pageSizeOptions.map(size => ({
1497
+ value: String(size),
1498
+ label: `${size} per page`,
1499
+ }));
1500
+
1501
+ // Render pagination controls
1502
+ const renderPaginationControls = () => {
1503
+ if (!paginated) return null;
1504
+
1505
+ return (
1506
+ <div className="flex items-center justify-between mb-4 px-1">
1507
+ <div className="flex items-center gap-4">
1508
+ {showPageSizeSelector && onPageSizeChange && (
1509
+ <div className="flex items-center gap-2">
1510
+ <span className="text-sm text-ink-600">Show:</span>
1511
+ <Select
1512
+ options={pageSizeSelectOptions}
1513
+ value={String(pageSize)}
1514
+ onChange={(value) => onPageSizeChange?.(Number(value))}
1515
+ />
1516
+ </div>
1517
+ )}
1518
+ <span className="text-sm text-ink-600">
1519
+ {effectiveTotalItems > 0 ? (
1520
+ <>
1521
+ Showing {((currentPage - 1) * pageSize) + 1} - {Math.min(currentPage * pageSize, effectiveTotalItems)} of {effectiveTotalItems}
1522
+ </>
1523
+ ) : (
1524
+ 'No items'
1525
+ )}
1526
+ </span>
1527
+ </div>
1528
+ {totalPages > 1 && onPageChange && (
1529
+ <Pagination
1530
+ currentPage={currentPage}
1531
+ totalPages={totalPages}
1532
+ onPageChange={onPageChange}
1533
+ />
1534
+ )}
1535
+ </div>
1536
+ );
1537
+ };
1538
+
1450
1539
  // Render with context menu
1451
1540
  return (
1452
1541
  <>
1542
+ {renderPaginationControls()}
1453
1543
  {finalContent}
1454
1544
  {contextMenuState.isOpen && contextMenuState.item && (
1455
1545
  <Menu
@@ -1,7 +1,10 @@
1
1
  import type { Meta, StoryObj } from '@storybook/react';
2
2
  import EmptyState from './EmptyState';
3
3
  import Button from './Button';
4
- import { Inbox, Users, FileText, Search, Database, ShoppingCart } from 'lucide-react';
4
+ import Input from './Input';
5
+ import Stack from './Stack';
6
+ import Text from './Text';
7
+ import { Inbox, Users, FileText, Search, Database, ShoppingCart, Plus } from 'lucide-react';
5
8
 
6
9
  const meta = {
7
10
  title: 'Feedback/EmptyState',
@@ -18,6 +21,7 @@ Empty state component for displaying helpful messages when no content is availab
18
21
  - **Title and description**: Clear messaging about empty state
19
22
  - **Primary action**: Main CTA button
20
23
  - **Secondary action**: Optional additional action
24
+ - **Children support**: Custom content below description
21
25
  - **Centered layout**: Vertically and horizontally centered
22
26
  - **Responsive**: Adapts to container width
23
27
 
@@ -79,6 +83,12 @@ import { Inbox } from 'lucide-react';
79
83
  type: { summary: '{ label: string; onClick: () => void }' },
80
84
  },
81
85
  },
86
+ children: {
87
+ description: 'Optional custom content rendered below the description',
88
+ table: {
89
+ type: { summary: 'React.ReactNode' },
90
+ },
91
+ },
82
92
  },
83
93
  decorators: [
84
94
  (Story) => (
@@ -95,7 +105,7 @@ type Story = StoryObj<typeof meta>;
95
105
  export const Default: Story = {
96
106
  args: {
97
107
  title: 'No items found',
98
- message: 'There are no items to display at this time.',
108
+ description: 'There are no items to display at this time.',
99
109
  },
100
110
  };
101
111
 
@@ -103,7 +113,7 @@ export const WithIcon: Story = {
103
113
  args: {
104
114
  icon: <Inbox className="h-12 w-12" />,
105
115
  title: 'Your inbox is empty',
106
- message: 'No new messages at this time.',
116
+ description: 'No new messages at this time.',
107
117
  },
108
118
  };
109
119
 
@@ -111,32 +121,35 @@ export const WithAction: Story = {
111
121
  args: {
112
122
  icon: <Users className="h-12 w-12" />,
113
123
  title: 'No users yet',
114
- message: 'Get started by adding your first user.',
115
- action: <Button variant="primary">Add User</Button>,
124
+ description: 'Get started by adding your first user.',
125
+ action: {
126
+ label: 'Add User',
127
+ onClick: () => alert('Add user clicked'),
128
+ },
116
129
  },
117
130
  };
118
131
 
119
- export const WithMultipleActions: Story = {
120
- render: () => (
121
- <EmptyState
122
- icon={<FileText className="h-12 w-12" />}
123
- title="No documents"
124
- message="You haven't created any documents yet. Create a new one or upload existing files."
125
- action={
126
- <div style={{ display: 'flex', gap: '0.75rem' }}>
127
- <Button variant="primary">Create New</Button>
128
- <Button variant="outline">Upload Files</Button>
129
- </div>
130
- }
131
- />
132
- ),
132
+ export const WithBothActions: Story = {
133
+ args: {
134
+ icon: <FileText className="h-12 w-12" />,
135
+ title: 'No documents',
136
+ description: "You haven't created any documents yet. Create a new one or upload existing files.",
137
+ action: {
138
+ label: 'Create New',
139
+ onClick: () => alert('Create clicked'),
140
+ },
141
+ secondaryAction: {
142
+ label: 'Upload Files',
143
+ onClick: () => alert('Upload clicked'),
144
+ },
145
+ },
133
146
  };
134
147
 
135
148
  export const SearchResults: Story = {
136
149
  args: {
137
150
  icon: <Search className="h-12 w-12" />,
138
151
  title: 'No results found',
139
- message: 'Try adjusting your search or filter to find what you\'re looking for.',
152
+ description: "Try adjusting your search or filter to find what you're looking for.",
140
153
  },
141
154
  };
142
155
 
@@ -144,66 +157,103 @@ export const NoData: Story = {
144
157
  args: {
145
158
  icon: <Database className="h-12 w-12" />,
146
159
  title: 'No data available',
147
- message: 'There is no data to display for the selected time period.',
160
+ description: 'There is no data to display for the selected time period.',
148
161
  },
149
162
  };
150
163
 
151
164
  export const EmptyCart: Story = {
152
- render: () => (
153
- <EmptyState
154
- icon={<ShoppingCart className="h-12 w-12" />}
155
- title="Your cart is empty"
156
- message="Looks like you haven't added anything to your cart yet."
157
- action={<Button variant="primary">Continue Shopping</Button>}
158
- />
165
+ args: {
166
+ icon: <ShoppingCart className="h-12 w-12" />,
167
+ title: 'Your cart is empty',
168
+ description: "Looks like you haven't added anything to your cart yet.",
169
+ action: {
170
+ label: 'Continue Shopping',
171
+ onClick: () => alert('Continue shopping clicked'),
172
+ },
173
+ },
174
+ };
175
+
176
+ /**
177
+ * Use the `children` prop to add custom content below the description.
178
+ * This is useful for adding forms, additional context, or complex UI elements.
179
+ */
180
+ export const WithChildren: Story = {
181
+ args: {
182
+ icon: <Plus className="h-12 w-12" />,
183
+ title: 'Create your first project',
184
+ description: 'Projects help you organize your work and collaborate with team members.',
185
+ },
186
+ render: (args) => (
187
+ <EmptyState {...args}>
188
+ <Stack spacing="sm" align="stretch">
189
+ <Input
190
+ placeholder="Enter project name..."
191
+ label="Project Name"
192
+ />
193
+ <Button variant="primary" icon={<Plus className="h-4 w-4" />}>
194
+ Create Project
195
+ </Button>
196
+ </Stack>
197
+ </EmptyState>
159
198
  ),
160
199
  };
161
200
 
162
- export const WithDescription: Story = {
163
- render: () => (
164
- <EmptyState
165
- icon={<FileText className="h-12 w-12" />}
166
- title="No projects yet"
167
- message="Projects help you organize your work and collaborate with team members."
168
- action={
169
- <div style={{ display: 'flex', flexDirection: 'column', gap: '1rem', alignItems: 'center' }}>
170
- <Button variant="primary">Create Your First Project</Button>
171
- <a href="#" style={{ fontSize: '0.875rem', color: '#3b82f6', textDecoration: 'none' }}>
172
- Learn more about projects →
173
- </a>
174
- </div>
175
- }
176
- />
201
+ /**
202
+ * Children can contain any custom content like search inputs for filtering.
203
+ */
204
+ export const WithSearchInChildren: Story = {
205
+ args: {
206
+ icon: <Search className="h-12 w-12" />,
207
+ title: 'No results found',
208
+ description: 'Try a different search term or browse categories.',
209
+ },
210
+ render: (args) => (
211
+ <EmptyState {...args}>
212
+ <Stack spacing="md" align="center">
213
+ <Input
214
+ placeholder="Search again..."
215
+ prefixIcon={<Search className="h-4 w-4" />}
216
+ clearable
217
+ />
218
+ <Text size="sm" color="muted">
219
+ Popular searches: React, TypeScript, Components
220
+ </Text>
221
+ </Stack>
222
+ </EmptyState>
177
223
  ),
178
224
  };
179
225
 
180
- export const ErrorState: Story = {
181
- render: () => (
182
- <EmptyState
183
- icon={
184
- <div style={{
185
- width: '64px',
186
- height: '64px',
187
- borderRadius: '50%',
188
- backgroundColor: '#fef2f2',
189
- display: 'flex',
190
- alignItems: 'center',
191
- justifyContent: 'center',
192
- }}>
193
- <span style={{ fontSize: '2rem' }}>⚠️</span>
194
- </div>
195
- }
196
- title="Something went wrong"
197
- message="We encountered an error while loading your data. Please try again."
198
- action={<Button variant="primary">Retry</Button>}
199
- />
226
+ /**
227
+ * Combine children with action buttons for maximum flexibility.
228
+ */
229
+ export const WithChildrenAndActions: Story = {
230
+ args: {
231
+ icon: <Users className="h-12 w-12" />,
232
+ title: 'Invite team members',
233
+ description: 'Add team members to collaborate on this project.',
234
+ action: {
235
+ label: 'Send Invites',
236
+ onClick: () => alert('Send invites clicked'),
237
+ },
238
+ secondaryAction: {
239
+ label: 'Skip for now',
240
+ onClick: () => alert('Skip clicked'),
241
+ },
242
+ },
243
+ render: (args) => (
244
+ <EmptyState {...args}>
245
+ <Input
246
+ placeholder="Enter email addresses..."
247
+ helperText="Separate multiple emails with commas"
248
+ />
249
+ </EmptyState>
200
250
  ),
201
251
  };
202
252
 
203
253
  export const MinimalStyle: Story = {
204
254
  args: {
205
255
  title: 'No notifications',
206
- message: 'You\'re all caught up!',
256
+ description: "You're all caught up!",
207
257
  },
208
258
  };
209
259
 
@@ -221,7 +271,7 @@ export const InCard: Story = {
221
271
  <EmptyState
222
272
  icon={<Inbox className="h-10 w-10" />}
223
273
  title="No recent activity"
224
- message="Your recent activity will appear here."
274
+ description="Your recent activity will appear here."
225
275
  />
226
276
  </div>
227
277
  ),
@@ -240,13 +290,15 @@ export const FullPage: Story = {
240
290
  <EmptyState
241
291
  icon={<Users className="h-16 w-16" />}
242
292
  title="Welcome to Your Dashboard"
243
- message="Get started by inviting team members or creating your first project."
244
- action={
245
- <div style={{ display: 'flex', gap: '0.75rem' }}>
246
- <Button variant="primary">Invite Team</Button>
247
- <Button variant="outline">Create Project</Button>
248
- </div>
249
- }
293
+ description="Get started by inviting team members or creating your first project."
294
+ action={{
295
+ label: 'Invite Team',
296
+ onClick: () => alert('Invite clicked'),
297
+ }}
298
+ secondaryAction={{
299
+ label: 'Create Project',
300
+ onClick: () => alert('Create clicked'),
301
+ }}
250
302
  />
251
303
  </div>
252
304
  ),
@@ -13,6 +13,8 @@ export interface EmptyStateProps {
13
13
  label: string;
14
14
  onClick: () => void;
15
15
  };
16
+ /** Optional custom content rendered below the description */
17
+ children?: React.ReactNode;
16
18
  }
17
19
 
18
20
  export default function EmptyState({
@@ -21,6 +23,7 @@ export default function EmptyState({
21
23
  description,
22
24
  action,
23
25
  secondaryAction,
26
+ children,
24
27
  }: EmptyStateProps) {
25
28
  return (
26
29
  <div className="flex flex-col items-center justify-center py-16 px-6 text-center">
@@ -37,6 +40,13 @@ export default function EmptyState({
37
40
  {/* Description */}
38
41
  <p className="text-sm text-ink-600 max-w-md mb-8">{description}</p>
39
42
 
43
+ {/* Custom children content */}
44
+ {children && (
45
+ <div className="mb-8 w-full max-w-md">
46
+ {children}
47
+ </div>
48
+ )}
49
+
40
50
  {/* Actions */}
41
51
  {(action || secondaryAction) && (
42
52
  <div className="flex items-center gap-3">