@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.
- package/dist/components/Box.d.ts +2 -1
- package/dist/components/Box.d.ts.map +1 -1
- package/dist/components/Button.d.ts +10 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Card.d.ts +11 -2
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/DataTable.d.ts +17 -3
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/EmptyState.d.ts +3 -1
- package/dist/components/EmptyState.d.ts.map +1 -1
- package/dist/components/Grid.d.ts +4 -2
- package/dist/components/Grid.d.ts.map +1 -1
- package/dist/components/Input.d.ts +2 -0
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/MultiSelect.d.ts +13 -1
- package/dist/components/MultiSelect.d.ts.map +1 -1
- package/dist/components/Stack.d.ts +25 -5
- package/dist/components/Stack.d.ts.map +1 -1
- package/dist/components/Text.d.ts +20 -4
- package/dist/components/Text.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +2 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +1 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +110 -48
- package/dist/index.esm.js +144 -138
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +143 -138
- package/dist/index.js.map +1 -1
- package/dist/styles.css +8 -51
- package/package.json +1 -1
- package/src/components/Box.stories.tsx +377 -0
- package/src/components/Box.tsx +8 -4
- package/src/components/Button.tsx +23 -10
- package/src/components/Card.tsx +20 -5
- package/src/components/DataTable.stories.tsx +36 -25
- package/src/components/DataTable.tsx +95 -5
- package/src/components/EmptyState.stories.tsx +124 -72
- package/src/components/EmptyState.tsx +10 -0
- package/src/components/Grid.stories.tsx +348 -0
- package/src/components/Grid.tsx +12 -5
- package/src/components/Input.tsx +12 -2
- package/src/components/MultiSelect.tsx +41 -10
- package/src/components/Stack.stories.tsx +24 -1
- package/src/components/Stack.tsx +40 -10
- package/src/components/Text.stories.tsx +273 -0
- package/src/components/Text.tsx +33 -8
- package/src/components/Textarea.tsx +32 -21
- package/src/components/index.ts +1 -4
- package/dist/components/Table.d.ts +0 -26
- package/dist/components/Table.d.ts.map +0 -1
- 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
|
-
|
|
175
|
+
header: 'Name',
|
|
183
176
|
sortable: true,
|
|
184
|
-
filterable: true,
|
|
185
177
|
},
|
|
186
178
|
{
|
|
187
179
|
key: 'email',
|
|
188
|
-
|
|
180
|
+
header: 'Email',
|
|
189
181
|
sortable: true,
|
|
190
182
|
},
|
|
191
183
|
{
|
|
192
184
|
key: 'role',
|
|
193
|
-
|
|
194
|
-
filterable: true,
|
|
185
|
+
header: 'Role',
|
|
195
186
|
},
|
|
196
187
|
{
|
|
197
188
|
key: 'status',
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
239
|
+
onRowSelect: (selected: string[]) => console.log('Selected:', selected),
|
|
249
240
|
},
|
|
250
241
|
};
|
|
251
242
|
|
|
252
243
|
export const Paginated: Story = {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
115
|
-
action:
|
|
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
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
action={
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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">
|