@papernote/ui 1.1.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/LICENSE +21 -21
- package/README.md +455 -455
- 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/CurrencyInput.d.ts +52 -0
- package/dist/components/CurrencyInput.d.ts.map +1 -0
- package/dist/components/DataTable.d.ts +19 -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/Modal.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/Page.d.ts +2 -0
- package/dist/components/Page.d.ts.map +1 -1
- package/dist/components/PageLayout.d.ts +5 -1
- package/dist/components/PageLayout.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 +5 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +311 -49
- package/dist/index.esm.js +557 -224
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +555 -219
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2838 -2679
- package/dist/utils/excelExport.d.ts +143 -0
- package/dist/utils/excelExport.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AdminModal.css +49 -49
- 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/CurrencyInput.stories.tsx +290 -0
- package/src/components/CurrencyInput.tsx +193 -0
- package/src/components/DataTable.stories.tsx +36 -25
- package/src/components/DataTable.tsx +170 -16
- 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/Modal.stories.tsx +64 -0
- package/src/components/Modal.tsx +15 -2
- package/src/components/MultiSelect.tsx +41 -10
- package/src/components/Page.stories.tsx +76 -0
- package/src/components/Page.tsx +35 -3
- package/src/components/PageLayout.stories.tsx +75 -0
- package/src/components/PageLayout.tsx +28 -9
- package/src/components/RoleManager.css +10 -10
- package/src/components/Spreadsheet.css +216 -216
- package/src/components/Spreadsheet.stories.tsx +362 -362
- package/src/components/Spreadsheet.tsx +351 -351
- package/src/components/SpreadsheetSimple.stories.tsx +27 -27
- package/src/components/Stack.stories.tsx +24 -1
- package/src/components/Stack.tsx +40 -10
- package/src/components/Tabs.tsx +152 -152
- 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 +6 -4
- package/src/styles/index.css +41 -4
- package/src/utils/excelExport.stories.tsx +535 -0
- package/src/utils/excelExport.ts +225 -0
- package/src/utils/index.ts +3 -0
- package/tailwind.config.js +253 -253
- package/dist/components/Button.stories.d.ts +0 -51
- package/dist/components/Button.stories.d.ts.map +0 -1
- package/dist/components/ChartVisualizationUI.d.ts +0 -21
- package/dist/components/ChartVisualizationUI.d.ts.map +0 -1
- package/dist/components/ChatUI.d.ts +0 -23
- package/dist/components/ChatUI.d.ts.map +0 -1
- package/dist/components/CommissionDashboardUI.d.ts +0 -25
- package/dist/components/CommissionDashboardUI.d.ts.map +0 -1
- package/dist/components/DataTable.stories.d.ts +0 -23
- package/dist/components/DataTable.stories.d.ts.map +0 -1
- package/dist/components/FormField.d.ts +0 -35
- package/dist/components/FormField.d.ts.map +0 -1
- package/dist/components/Input.stories.d.ts +0 -366
- package/dist/components/Input.stories.d.ts.map +0 -1
- package/dist/components/InsightsPanelUI.d.ts +0 -21
- package/dist/components/InsightsPanelUI.d.ts.map +0 -1
- package/dist/components/PaymentHistoryTimeline.d.ts +0 -34
- package/dist/components/PaymentHistoryTimeline.d.ts.map +0 -1
- package/dist/components/RelationshipManagerUI.d.ts +0 -60
- package/dist/components/RelationshipManagerUI.d.ts.map +0 -1
- package/dist/components/RoleManager.d.ts +0 -19
- package/dist/components/RoleManager.d.ts.map +0 -1
- package/dist/components/SplitCommissionBadge.d.ts +0 -18
- package/dist/components/SplitCommissionBadge.d.ts.map +0 -1
- package/dist/components/Spreadsheet.css +0 -216
- package/dist/components/Table.d.ts +0 -26
- package/dist/components/Table.d.ts.map +0 -1
- package/dist/components/__tests__/Button.test.d.ts +0 -2
- package/dist/components/__tests__/Button.test.d.ts.map +0 -1
- package/dist/components/__tests__/Input.test.d.ts +0 -2
- package/dist/components/__tests__/Input.test.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>
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
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
|
+
import Menu, { MenuItem } from './Menu';
|
|
6
|
+
import Pagination from './Pagination';
|
|
7
|
+
import Select from './Select';
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Base data item interface - all data items must have an id
|
|
@@ -12,8 +15,6 @@ import { ChevronDown, ChevronRight, MoreVertical, Edit, Trash } from 'lucide-rea
|
|
|
12
15
|
export interface BaseDataItem {
|
|
13
16
|
/** Unique identifier for the data item */
|
|
14
17
|
id: string | number;
|
|
15
|
-
/** Additional properties specific to your data type */
|
|
16
|
-
[key: string]: unknown;
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
/**
|
|
@@ -164,6 +165,8 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
|
|
|
164
165
|
onDelete?: (item: T) => void | Promise<void>;
|
|
165
166
|
/** Optional custom row actions (in addition to edit/delete) */
|
|
166
167
|
actions?: DataTableAction<T>[];
|
|
168
|
+
/** Enable context menu (right-click) for row actions (default: true when actions exist) */
|
|
169
|
+
enableContextMenu?: boolean;
|
|
167
170
|
/** Optional click handler for rows */
|
|
168
171
|
onRowClick?: (item: T) => void;
|
|
169
172
|
/** Optional double-click handler for rows */
|
|
@@ -226,6 +229,24 @@ interface DataTableProps<T extends BaseDataItem = BaseDataItem> {
|
|
|
226
229
|
virtualHeight?: string;
|
|
227
230
|
/** Row height for virtual scrolling (default: 60) */
|
|
228
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;
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
/**
|
|
@@ -455,6 +476,7 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
455
476
|
onEdit,
|
|
456
477
|
onDelete,
|
|
457
478
|
actions = [],
|
|
479
|
+
enableContextMenu = true,
|
|
458
480
|
onRowClick,
|
|
459
481
|
onRowDoubleClick,
|
|
460
482
|
selectable = false,
|
|
@@ -486,6 +508,15 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
486
508
|
virtualized = false,
|
|
487
509
|
virtualHeight = '600px',
|
|
488
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,
|
|
489
520
|
}: DataTableProps<T>) {
|
|
490
521
|
// Column resizing state
|
|
491
522
|
const [columnWidths, setColumnWidths] = useState<Record<string, number>>({});
|
|
@@ -505,6 +536,17 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
505
536
|
// Row hover state (for coordinating primary + secondary row highlighting)
|
|
506
537
|
const [hoveredRowKey, setHoveredRowKey] = useState<string | null>(null);
|
|
507
538
|
|
|
539
|
+
// Context menu state
|
|
540
|
+
const [contextMenuState, setContextMenuState] = useState<{
|
|
541
|
+
isOpen: boolean;
|
|
542
|
+
position: { x: number; y: number };
|
|
543
|
+
item: T | null;
|
|
544
|
+
}>({
|
|
545
|
+
isOpen: false,
|
|
546
|
+
position: { x: 0, y: 0 },
|
|
547
|
+
item: null,
|
|
548
|
+
});
|
|
549
|
+
|
|
508
550
|
// Filter columns based on hiddenColumns
|
|
509
551
|
const baseVisibleColumns = columns.filter(
|
|
510
552
|
col => !hiddenColumns.includes(String(col.key))
|
|
@@ -746,18 +788,56 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
746
788
|
});
|
|
747
789
|
}
|
|
748
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;
|
|
749
794
|
if (onDelete) {
|
|
750
|
-
|
|
795
|
+
deleteAction = {
|
|
751
796
|
label: 'Delete',
|
|
752
797
|
icon: Trash,
|
|
753
798
|
onClick: onDelete,
|
|
754
799
|
variant: 'danger',
|
|
755
800
|
tooltip: 'Delete item'
|
|
756
|
-
}
|
|
801
|
+
};
|
|
757
802
|
}
|
|
758
803
|
|
|
759
|
-
|
|
760
|
-
|
|
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
|
+
];
|
|
816
|
+
|
|
817
|
+
// Convert actions to menu items for context menu
|
|
818
|
+
const convertActionsToMenuItems = (item: T): MenuItem[] => {
|
|
819
|
+
const visibleActions = allActions.filter(action => !action.show || action.show(item));
|
|
820
|
+
|
|
821
|
+
return visibleActions.map((action, idx) => {
|
|
822
|
+
let iconElement: React.ReactNode = null;
|
|
823
|
+
if (action.icon) {
|
|
824
|
+
if (React.isValidElement(action.icon)) {
|
|
825
|
+
iconElement = action.icon;
|
|
826
|
+
} else {
|
|
827
|
+
iconElement = React.createElement(action.icon as any, { className: 'h-4 w-4' });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return {
|
|
832
|
+
id: `action-${idx}`,
|
|
833
|
+
label: action.label,
|
|
834
|
+
icon: iconElement,
|
|
835
|
+
onClick: () => action.onClick(item),
|
|
836
|
+
danger: action.variant === 'danger',
|
|
837
|
+
};
|
|
838
|
+
});
|
|
839
|
+
};
|
|
840
|
+
|
|
761
841
|
// Selection state management
|
|
762
842
|
const [internalSelectedRows, setInternalSelectedRows] = useState<Set<string>>(new Set());
|
|
763
843
|
|
|
@@ -997,6 +1077,21 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
997
1077
|
onMouseEnter={() => !disableHover && setHoveredRowKey(rowKey)}
|
|
998
1078
|
onMouseLeave={() => !disableHover && setHoveredRowKey(null)}
|
|
999
1079
|
onClick={() => onRowClick?.(item)}
|
|
1080
|
+
onContextMenu={(e) => {
|
|
1081
|
+
if (enableContextMenu && allActions.length > 0) {
|
|
1082
|
+
e.preventDefault();
|
|
1083
|
+
e.stopPropagation();
|
|
1084
|
+
|
|
1085
|
+
const x = e.clientX;
|
|
1086
|
+
const y = e.clientY;
|
|
1087
|
+
|
|
1088
|
+
setContextMenuState({
|
|
1089
|
+
isOpen: true,
|
|
1090
|
+
position: { x, y },
|
|
1091
|
+
item,
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
}}
|
|
1000
1095
|
onDoubleClick={() => {
|
|
1001
1096
|
// Priority 1: If there's an onEdit handler (legacy), trigger it
|
|
1002
1097
|
if (onEdit) {
|
|
@@ -1382,18 +1477,77 @@ export default function DataTable<T extends BaseDataItem = BaseDataItem>({
|
|
|
1382
1477
|
);
|
|
1383
1478
|
|
|
1384
1479
|
// Wrap in scrollable container if virtualized
|
|
1385
|
-
|
|
1480
|
+
const finalContent = virtualized ? (
|
|
1481
|
+
<div
|
|
1482
|
+
ref={tableContainerRef}
|
|
1483
|
+
onScroll={handleScroll}
|
|
1484
|
+
style={{ height: virtualHeight, overflow: 'auto' }}
|
|
1485
|
+
className="rounded-lg"
|
|
1486
|
+
>
|
|
1487
|
+
{tableContent}
|
|
1488
|
+
</div>
|
|
1489
|
+
) : tableContent;
|
|
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
|
+
|
|
1386
1505
|
return (
|
|
1387
|
-
<div
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
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
|
+
)}
|
|
1394
1535
|
</div>
|
|
1395
1536
|
);
|
|
1396
|
-
}
|
|
1537
|
+
};
|
|
1397
1538
|
|
|
1398
|
-
|
|
1539
|
+
// Render with context menu
|
|
1540
|
+
return (
|
|
1541
|
+
<>
|
|
1542
|
+
{renderPaginationControls()}
|
|
1543
|
+
{finalContent}
|
|
1544
|
+
{contextMenuState.isOpen && contextMenuState.item && (
|
|
1545
|
+
<Menu
|
|
1546
|
+
items={convertActionsToMenuItems(contextMenuState.item)}
|
|
1547
|
+
position={contextMenuState.position}
|
|
1548
|
+
onClose={() => setContextMenuState({ isOpen: false, position: { x: 0, y: 0 }, item: null })}
|
|
1549
|
+
/>
|
|
1550
|
+
)}
|
|
1551
|
+
</>
|
|
1552
|
+
);
|
|
1399
1553
|
}
|
|
@@ -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
|
),
|