@papernote/ui 1.0.0 → 1.2.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 -445
- package/dist/components/CurrencyInput.d.ts +52 -0
- package/dist/components/CurrencyInput.d.ts.map +1 -0
- package/dist/components/DataTable.d.ts +3 -1
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/Modal.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/Spreadsheet.d.ts +129 -0
- package/dist/components/Spreadsheet.d.ts.map +1 -0
- package/dist/components/Tabs.d.ts +5 -1
- package/dist/components/Tabs.d.ts.map +1 -1
- package/dist/components/index.d.ts +6 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +336 -5
- package/dist/index.esm.js +51152 -174
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +51145 -143
- package/dist/index.js.map +1 -1
- package/dist/styles.css +1187 -11
- 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 +13 -3
- package/src/components/AdminModal.css +49 -49
- package/src/components/CurrencyInput.stories.tsx +290 -0
- package/src/components/CurrencyInput.tsx +193 -0
- package/src/components/DataTable.stories.tsx +87 -0
- package/src/components/DataTable.tsx +149 -37
- package/src/components/Modal.stories.tsx +64 -0
- package/src/components/Modal.tsx +15 -2
- 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 -0
- package/src/components/Spreadsheet.stories.tsx +362 -0
- package/src/components/Spreadsheet.tsx +351 -0
- package/src/components/SpreadsheetSimple.stories.tsx +27 -0
- package/src/components/Tabs.stories.tsx +31 -0
- package/src/components/Tabs.tsx +28 -4
- package/src/components/TimePicker.tsx +1 -1
- package/src/components/Toast.tsx +9 -9
- package/src/components/__tests__/Input.test.tsx +22 -26
- package/src/components/index.ts +11 -2
- package/src/styles/index.css +44 -6
- package/src/utils/excelExport.stories.tsx +535 -0
- package/src/utils/excelExport.ts +225 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/sqlToNaturalLanguage.ts +1 -1
- 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/__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
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { Spreadsheet, SpreadsheetReport } from './Spreadsheet';
|
|
3
|
+
import type { SpreadsheetCell, Matrix } from './Spreadsheet';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import Button from './Button';
|
|
6
|
+
import { Calculator } from 'lucide-react';
|
|
7
|
+
|
|
8
|
+
const meta: Meta<typeof Spreadsheet> = {
|
|
9
|
+
title: 'Components/Spreadsheet',
|
|
10
|
+
component: Spreadsheet,
|
|
11
|
+
parameters: {
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
'Interactive spreadsheet component with Excel formula support (280+ formulas via Fast Formula Parser), import/export functionality, and save capabilities. Perfect for report designers and data editing interfaces.',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
tags: ['autodocs'],
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export default meta;
|
|
23
|
+
type Story = StoryObj<typeof Spreadsheet>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Basic spreadsheet with default settings
|
|
27
|
+
*/
|
|
28
|
+
export const Basic: Story = {
|
|
29
|
+
args: {
|
|
30
|
+
rows: 10,
|
|
31
|
+
columns: 5,
|
|
32
|
+
},
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Spreadsheet with toolbar showing import/export/save actions
|
|
37
|
+
*/
|
|
38
|
+
export const WithToolbar: Story = {
|
|
39
|
+
args: {
|
|
40
|
+
rows: 15,
|
|
41
|
+
columns: 8,
|
|
42
|
+
showToolbar: true,
|
|
43
|
+
enableImport: true,
|
|
44
|
+
enableExport: true,
|
|
45
|
+
enableSave: true,
|
|
46
|
+
title: 'Data Editor',
|
|
47
|
+
exportFileName: 'my-data.xlsx',
|
|
48
|
+
},
|
|
49
|
+
render: (args) => {
|
|
50
|
+
const [data, setData] = useState<Matrix<SpreadsheetCell>>();
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<Spreadsheet
|
|
54
|
+
{...args}
|
|
55
|
+
data={data}
|
|
56
|
+
onChange={setData}
|
|
57
|
+
onSave={async (data) => {
|
|
58
|
+
console.log('Saving data:', data);
|
|
59
|
+
// Simulate API call
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
61
|
+
}}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Spreadsheet wrapped in Card component for better presentation
|
|
69
|
+
*/
|
|
70
|
+
export const InCard: Story = {
|
|
71
|
+
args: {
|
|
72
|
+
rows: 12,
|
|
73
|
+
columns: 6,
|
|
74
|
+
showToolbar: true,
|
|
75
|
+
enableImport: true,
|
|
76
|
+
enableExport: true,
|
|
77
|
+
enableSave: true,
|
|
78
|
+
title: 'Financial Report',
|
|
79
|
+
wrapInCard: true,
|
|
80
|
+
},
|
|
81
|
+
render: (args) => {
|
|
82
|
+
const [data, setData] = useState<Matrix<SpreadsheetCell>>();
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<Spreadsheet
|
|
86
|
+
{...args}
|
|
87
|
+
data={data}
|
|
88
|
+
onChange={setData}
|
|
89
|
+
onSave={async (data) => {
|
|
90
|
+
console.log('Saving:', data);
|
|
91
|
+
await new Promise((resolve) => setTimeout(resolve, 800));
|
|
92
|
+
}}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Pre-populated spreadsheet with formula examples
|
|
100
|
+
*/
|
|
101
|
+
export const WithFormulas: Story = {
|
|
102
|
+
render: () => {
|
|
103
|
+
const initialData: Matrix<SpreadsheetCell> = [
|
|
104
|
+
[
|
|
105
|
+
{ value: 'Product', readOnly: true, className: 'font-bold' },
|
|
106
|
+
{ value: 'Q1', readOnly: true, className: 'font-bold' },
|
|
107
|
+
{ value: 'Q2', readOnly: true, className: 'font-bold' },
|
|
108
|
+
{ value: 'Q3', readOnly: true, className: 'font-bold' },
|
|
109
|
+
{ value: 'Q4', readOnly: true, className: 'font-bold' },
|
|
110
|
+
{ value: 'Total', readOnly: true, className: 'font-bold' },
|
|
111
|
+
],
|
|
112
|
+
[
|
|
113
|
+
{ value: 'Widget A' },
|
|
114
|
+
{ value: 15000 },
|
|
115
|
+
{ value: 18000 },
|
|
116
|
+
{ value: 22000 },
|
|
117
|
+
{ value: 19000 },
|
|
118
|
+
{ formula: '=SUM(B2:E2)' },
|
|
119
|
+
],
|
|
120
|
+
[
|
|
121
|
+
{ value: 'Widget B' },
|
|
122
|
+
{ value: 12000 },
|
|
123
|
+
{ value: 13500 },
|
|
124
|
+
{ value: 14200 },
|
|
125
|
+
{ value: 15800 },
|
|
126
|
+
{ formula: '=SUM(B3:E3)' },
|
|
127
|
+
],
|
|
128
|
+
[
|
|
129
|
+
{ value: 'Widget C' },
|
|
130
|
+
{ value: 8500 },
|
|
131
|
+
{ value: 9200 },
|
|
132
|
+
{ value: 11000 },
|
|
133
|
+
{ value: 12300 },
|
|
134
|
+
{ formula: '=SUM(B4:E4)' },
|
|
135
|
+
],
|
|
136
|
+
[],
|
|
137
|
+
[
|
|
138
|
+
{ value: 'Quarterly Total', readOnly: true, className: 'font-bold' },
|
|
139
|
+
{ formula: '=SUM(B2:B4)' },
|
|
140
|
+
{ formula: '=SUM(C2:C4)' },
|
|
141
|
+
{ formula: '=SUM(D2:D4)' },
|
|
142
|
+
{ formula: '=SUM(E2:E4)' },
|
|
143
|
+
{ formula: '=SUM(F2:F4)' },
|
|
144
|
+
],
|
|
145
|
+
[
|
|
146
|
+
{ value: 'Average per Product', readOnly: true, className: 'font-bold' },
|
|
147
|
+
{ formula: '=AVERAGE(B2:B4)' },
|
|
148
|
+
{ formula: '=AVERAGE(C2:C4)' },
|
|
149
|
+
{ formula: '=AVERAGE(D2:D4)' },
|
|
150
|
+
{ formula: '=AVERAGE(E2:E4)' },
|
|
151
|
+
{ formula: '=AVERAGE(F2:F4)' },
|
|
152
|
+
],
|
|
153
|
+
];
|
|
154
|
+
|
|
155
|
+
const [data, setData] = useState<Matrix<SpreadsheetCell>>(initialData);
|
|
156
|
+
|
|
157
|
+
return (
|
|
158
|
+
<div className="p-4">
|
|
159
|
+
<h3 className="text-lg font-semibold mb-4">Sales Report with Formulas</h3>
|
|
160
|
+
<p className="text-sm text-ink-600 mb-4">
|
|
161
|
+
This example demonstrates SUM and AVERAGE formulas. Try editing the values in Q1-Q4 columns
|
|
162
|
+
and watch the totals update automatically!
|
|
163
|
+
</p>
|
|
164
|
+
<Spreadsheet
|
|
165
|
+
data={data}
|
|
166
|
+
onChange={setData}
|
|
167
|
+
showToolbar
|
|
168
|
+
enableExport
|
|
169
|
+
enableSave
|
|
170
|
+
title="Quarterly Sales Report"
|
|
171
|
+
wrapInCard
|
|
172
|
+
exportFileName="sales-report.xlsx"
|
|
173
|
+
onSave={async (data) => {
|
|
174
|
+
console.log('Saving report:', data);
|
|
175
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
176
|
+
}}
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
);
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Read-only spreadsheet for viewing reports
|
|
185
|
+
*/
|
|
186
|
+
export const ReadOnly: Story = {
|
|
187
|
+
render: () => {
|
|
188
|
+
const reportData: Matrix<SpreadsheetCell> = [
|
|
189
|
+
[
|
|
190
|
+
{ value: 'Metric', readOnly: true },
|
|
191
|
+
{ value: 'Value', readOnly: true },
|
|
192
|
+
{ value: 'Target', readOnly: true },
|
|
193
|
+
{ value: 'Status', readOnly: true },
|
|
194
|
+
],
|
|
195
|
+
[{ value: 'Revenue' }, { value: 125000 }, { value: 120000 }, { value: '✓ Met' }],
|
|
196
|
+
[{ value: 'Expenses' }, { value: 85000 }, { value: 90000 }, { value: '✓ Under' }],
|
|
197
|
+
[{ value: 'Profit' }, { formula: '=B2-B3' }, { formula: '=C2-C3' }, { value: '✓ Above' }],
|
|
198
|
+
[{ value: 'Margin %' }, { formula: '=B4/B2*100' }, { formula: '=C4/C2*100' }, { value: '' }],
|
|
199
|
+
];
|
|
200
|
+
|
|
201
|
+
return (
|
|
202
|
+
<Spreadsheet
|
|
203
|
+
data={reportData}
|
|
204
|
+
readOnly
|
|
205
|
+
title="Financial Summary (Read-Only)"
|
|
206
|
+
showToolbar
|
|
207
|
+
enableExport
|
|
208
|
+
wrapInCard
|
|
209
|
+
exportFileName="financial-summary.xlsx"
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
212
|
+
},
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* SpreadsheetReport component - pre-configured for report designer use
|
|
217
|
+
*/
|
|
218
|
+
export const ReportDesigner: Story = {
|
|
219
|
+
render: () => {
|
|
220
|
+
const [reportData, setReportData] = useState<Matrix<SpreadsheetCell>>([
|
|
221
|
+
[
|
|
222
|
+
{ value: 'Month', readOnly: true },
|
|
223
|
+
{ value: 'Sales', readOnly: true },
|
|
224
|
+
{ value: 'Costs', readOnly: true },
|
|
225
|
+
{ value: 'Profit', readOnly: true },
|
|
226
|
+
],
|
|
227
|
+
[{ value: 'January' }, { value: 50000 }, { value: 30000 }, { formula: '=B2-C2' }],
|
|
228
|
+
[{ value: 'February' }, { value: 55000 }, { value: 32000 }, { formula: '=B3-C3' }],
|
|
229
|
+
[{ value: 'March' }, { value: 62000 }, { value: 35000 }, { formula: '=B4-C4' }],
|
|
230
|
+
[],
|
|
231
|
+
[
|
|
232
|
+
{ value: 'Total', readOnly: true },
|
|
233
|
+
{ formula: '=SUM(B2:B4)' },
|
|
234
|
+
{ formula: '=SUM(C2:C4)' },
|
|
235
|
+
{ formula: '=SUM(D2:D4)' },
|
|
236
|
+
],
|
|
237
|
+
]);
|
|
238
|
+
|
|
239
|
+
return (
|
|
240
|
+
<div className="max-w-6xl mx-auto p-6">
|
|
241
|
+
<h2 className="text-2xl font-bold mb-4">Report Designer</h2>
|
|
242
|
+
<p className="text-ink-600 mb-6">
|
|
243
|
+
The SpreadsheetReport component comes pre-configured with toolbar, import/export, and save
|
|
244
|
+
functionality. Perfect for building interactive reports!
|
|
245
|
+
</p>
|
|
246
|
+
|
|
247
|
+
<SpreadsheetReport
|
|
248
|
+
data={reportData}
|
|
249
|
+
onChange={setReportData}
|
|
250
|
+
title="Monthly Financial Report"
|
|
251
|
+
exportFileName="monthly-report.xlsx"
|
|
252
|
+
onSave={async (data) => {
|
|
253
|
+
console.log('Saving report:', data);
|
|
254
|
+
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
255
|
+
}}
|
|
256
|
+
/>
|
|
257
|
+
|
|
258
|
+
<div className="mt-6 p-4 bg-paper-50 border border-stone-200 rounded-lg">
|
|
259
|
+
<h3 className="font-semibold mb-2">Formula Support</h3>
|
|
260
|
+
<p className="text-sm text-ink-600 mb-2">
|
|
261
|
+
Fast Formula Parser provides 280+ Excel formulas including:
|
|
262
|
+
</p>
|
|
263
|
+
<ul className="text-sm text-ink-600 list-disc list-inside space-y-1">
|
|
264
|
+
<li>Math: SUM, AVERAGE, ROUND, ABS, POWER, SQRT</li>
|
|
265
|
+
<li>Logical: IF, AND, OR, NOT, IFERROR</li>
|
|
266
|
+
<li>Lookup: VLOOKUP, HLOOKUP, INDEX</li>
|
|
267
|
+
<li>Text: CONCATENATE, LEFT, RIGHT, TRIM, UPPER, LOWER</li>
|
|
268
|
+
<li>Date/Time: DATE, TODAY, YEAR, MONTH, DAY</li>
|
|
269
|
+
<li>Statistical: COUNT, COUNTIF, MAX, MIN, STDEV</li>
|
|
270
|
+
</ul>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
},
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Spreadsheet with custom actions in toolbar
|
|
279
|
+
*/
|
|
280
|
+
export const WithCustomActions: Story = {
|
|
281
|
+
render: () => {
|
|
282
|
+
const [data, setData] = useState<Matrix<SpreadsheetCell>>();
|
|
283
|
+
|
|
284
|
+
return (
|
|
285
|
+
<Spreadsheet
|
|
286
|
+
data={data}
|
|
287
|
+
onChange={setData}
|
|
288
|
+
rows={10}
|
|
289
|
+
columns={6}
|
|
290
|
+
showToolbar
|
|
291
|
+
enableImport
|
|
292
|
+
enableExport
|
|
293
|
+
title="Custom Actions Demo"
|
|
294
|
+
actions={
|
|
295
|
+
<>
|
|
296
|
+
<Button
|
|
297
|
+
variant="ghost"
|
|
298
|
+
size="sm"
|
|
299
|
+
icon={<Calculator className="h-4 w-4" />}
|
|
300
|
+
onClick={() => console.log('Calculate clicked')}
|
|
301
|
+
>
|
|
302
|
+
Calculate
|
|
303
|
+
</Button>
|
|
304
|
+
<Button
|
|
305
|
+
variant="secondary"
|
|
306
|
+
size="sm"
|
|
307
|
+
onClick={() => console.log('Clear clicked')}
|
|
308
|
+
>
|
|
309
|
+
Clear
|
|
310
|
+
</Button>
|
|
311
|
+
</>
|
|
312
|
+
}
|
|
313
|
+
wrapInCard
|
|
314
|
+
/>
|
|
315
|
+
);
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Compact spreadsheet for smaller spaces
|
|
321
|
+
*/
|
|
322
|
+
export const Compact: Story = {
|
|
323
|
+
args: {
|
|
324
|
+
rows: 8,
|
|
325
|
+
columns: 4,
|
|
326
|
+
showToolbar: false,
|
|
327
|
+
className: 'text-sm',
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Large spreadsheet for complex data entry
|
|
333
|
+
*/
|
|
334
|
+
export const Large: Story = {
|
|
335
|
+
args: {
|
|
336
|
+
rows: 50,
|
|
337
|
+
columns: 20,
|
|
338
|
+
showToolbar: true,
|
|
339
|
+
enableImport: true,
|
|
340
|
+
enableExport: true,
|
|
341
|
+
enableSave: true,
|
|
342
|
+
title: 'Large Dataset Editor',
|
|
343
|
+
wrapInCard: true,
|
|
344
|
+
},
|
|
345
|
+
render: (args) => {
|
|
346
|
+
const [data, setData] = useState<Matrix<SpreadsheetCell>>();
|
|
347
|
+
|
|
348
|
+
return (
|
|
349
|
+
<div style={{ height: '600px' }}>
|
|
350
|
+
<Spreadsheet
|
|
351
|
+
{...args}
|
|
352
|
+
data={data}
|
|
353
|
+
onChange={setData}
|
|
354
|
+
onSave={async (data) => {
|
|
355
|
+
console.log('Saving large dataset');
|
|
356
|
+
await new Promise((resolve) => setTimeout(resolve, 1500));
|
|
357
|
+
}}
|
|
358
|
+
/>
|
|
359
|
+
</div>
|
|
360
|
+
);
|
|
361
|
+
},
|
|
362
|
+
};
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import React, { useState, useCallback } from 'react';
|
|
2
|
+
import BaseSpreadsheet, { CellBase, Matrix } from 'react-spreadsheet';
|
|
3
|
+
import { read, utils, writeFile, WorkBook } from 'xlsx';
|
|
4
|
+
import Button from './Button';
|
|
5
|
+
import Card, { CardHeader, CardTitle, CardContent } from './Card';
|
|
6
|
+
import Stack from './Stack';
|
|
7
|
+
import { Download, Upload, Save } from 'lucide-react';
|
|
8
|
+
import { addSuccessMessage, addErrorMessage } from './StatusBar';
|
|
9
|
+
import './Spreadsheet.css';
|
|
10
|
+
|
|
11
|
+
// Re-export types for external use
|
|
12
|
+
export type { CellBase, Matrix } from 'react-spreadsheet';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enhanced cell type with formula support
|
|
16
|
+
*/
|
|
17
|
+
export interface SpreadsheetCell extends CellBase {
|
|
18
|
+
value: string | number | boolean;
|
|
19
|
+
formula?: string;
|
|
20
|
+
readOnly?: boolean;
|
|
21
|
+
className?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Spreadsheet component props
|
|
26
|
+
*/
|
|
27
|
+
export interface SpreadsheetProps {
|
|
28
|
+
/** Initial data matrix */
|
|
29
|
+
data?: Matrix<SpreadsheetCell>;
|
|
30
|
+
/** Callback when data changes */
|
|
31
|
+
onChange?: (data: Matrix<SpreadsheetCell>) => void;
|
|
32
|
+
/** Number of rows to display */
|
|
33
|
+
rows?: number;
|
|
34
|
+
/** Number of columns to display */
|
|
35
|
+
columns?: number;
|
|
36
|
+
/** Column labels (A, B, C... if not provided) */
|
|
37
|
+
columnLabels?: string[];
|
|
38
|
+
/** Row labels (1, 2, 3... if not provided) */
|
|
39
|
+
rowLabels?: string[];
|
|
40
|
+
/** Show toolbar with actions */
|
|
41
|
+
showToolbar?: boolean;
|
|
42
|
+
/** Enable Excel import */
|
|
43
|
+
enableImport?: boolean;
|
|
44
|
+
/** Enable Excel export */
|
|
45
|
+
enableExport?: boolean;
|
|
46
|
+
/** Enable save button */
|
|
47
|
+
enableSave?: boolean;
|
|
48
|
+
/** Save handler */
|
|
49
|
+
onSave?: (data: Matrix<SpreadsheetCell>) => Promise<void> | void;
|
|
50
|
+
/** Title to display in toolbar */
|
|
51
|
+
title?: string;
|
|
52
|
+
/** Additional toolbar actions */
|
|
53
|
+
actions?: React.ReactNode;
|
|
54
|
+
/** Wrap in Card component */
|
|
55
|
+
wrapInCard?: boolean;
|
|
56
|
+
/** Custom className for the spreadsheet container */
|
|
57
|
+
className?: string;
|
|
58
|
+
/** Make entire spreadsheet read-only */
|
|
59
|
+
readOnly?: boolean;
|
|
60
|
+
/** Default export filename */
|
|
61
|
+
exportFileName?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Spreadsheet - Interactive spreadsheet component with formula support
|
|
66
|
+
*
|
|
67
|
+
* A full-featured spreadsheet component for report designers and data editing.
|
|
68
|
+
* Built on react-spreadsheet with Fast Formula Parser (280+ Excel formulas).
|
|
69
|
+
*
|
|
70
|
+
* **Features:**
|
|
71
|
+
* - Excel-like formula support (SUM, AVERAGE, VLOOKUP, IF, etc.)
|
|
72
|
+
* - Excel import/export with SheetJS
|
|
73
|
+
* - Save/load functionality
|
|
74
|
+
* - Keyboard navigation
|
|
75
|
+
* - Copy/paste support
|
|
76
|
+
* - Customizable styling to match notebook-ui aesthetic
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* ```tsx
|
|
80
|
+
* // Basic spreadsheet
|
|
81
|
+
* <Spreadsheet
|
|
82
|
+
* rows={10}
|
|
83
|
+
* columns={5}
|
|
84
|
+
* showToolbar
|
|
85
|
+
* />
|
|
86
|
+
*
|
|
87
|
+
* // Report designer with formulas
|
|
88
|
+
* const [reportData, setReportData] = useState<Matrix<SpreadsheetCell>>([
|
|
89
|
+
* [{ value: 'Q1' }, { value: 100 }],
|
|
90
|
+
* [{ value: 'Q2' }, { value: 200 }],
|
|
91
|
+
* [{ value: 'Total' }, { formula: '=SUM(B1:B2)' }],
|
|
92
|
+
* ]);
|
|
93
|
+
*
|
|
94
|
+
* <Spreadsheet
|
|
95
|
+
* data={reportData}
|
|
96
|
+
* onChange={setReportData}
|
|
97
|
+
* title="Sales Report"
|
|
98
|
+
* showToolbar
|
|
99
|
+
* enableImport
|
|
100
|
+
* enableExport
|
|
101
|
+
* enableSave
|
|
102
|
+
* onSave={async (data) => {
|
|
103
|
+
* await saveReport(data);
|
|
104
|
+
* }}
|
|
105
|
+
* />
|
|
106
|
+
*
|
|
107
|
+
* // With custom actions
|
|
108
|
+
* <Spreadsheet
|
|
109
|
+
* data={data}
|
|
110
|
+
* onChange={setData}
|
|
111
|
+
* showToolbar
|
|
112
|
+
* actions={
|
|
113
|
+
* <Button onClick={handleCustomAction}>Custom Action</Button>
|
|
114
|
+
* }
|
|
115
|
+
* />
|
|
116
|
+
* ```
|
|
117
|
+
*/
|
|
118
|
+
export const Spreadsheet: React.FC<SpreadsheetProps> = ({
|
|
119
|
+
data: initialData,
|
|
120
|
+
onChange,
|
|
121
|
+
rows = 20,
|
|
122
|
+
columns = 10,
|
|
123
|
+
columnLabels,
|
|
124
|
+
rowLabels,
|
|
125
|
+
showToolbar = false,
|
|
126
|
+
enableImport = false,
|
|
127
|
+
enableExport = false,
|
|
128
|
+
enableSave = false,
|
|
129
|
+
onSave,
|
|
130
|
+
title,
|
|
131
|
+
actions,
|
|
132
|
+
wrapInCard = false,
|
|
133
|
+
className = '',
|
|
134
|
+
readOnly = false,
|
|
135
|
+
exportFileName = 'spreadsheet.xlsx',
|
|
136
|
+
}) => {
|
|
137
|
+
// Initialize data if not provided
|
|
138
|
+
const [data, setData] = useState<Matrix<SpreadsheetCell>>(() => {
|
|
139
|
+
if (initialData) return initialData;
|
|
140
|
+
return Array(rows)
|
|
141
|
+
.fill(null)
|
|
142
|
+
.map(() => Array(columns).fill(null).map(() => ({ value: '' })));
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
146
|
+
|
|
147
|
+
// Handle data changes
|
|
148
|
+
const handleChange = useCallback(
|
|
149
|
+
(newData: Matrix<SpreadsheetCell>) => {
|
|
150
|
+
setData(newData);
|
|
151
|
+
onChange?.(newData);
|
|
152
|
+
},
|
|
153
|
+
[onChange]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Handle Excel import
|
|
157
|
+
const handleImport = useCallback(
|
|
158
|
+
(event: React.ChangeEvent<HTMLInputElement>) => {
|
|
159
|
+
const file = event.target.files?.[0];
|
|
160
|
+
if (!file) return;
|
|
161
|
+
|
|
162
|
+
const reader = new FileReader();
|
|
163
|
+
reader.onload = (e) => {
|
|
164
|
+
try {
|
|
165
|
+
const workbook: WorkBook = read(e.target?.result, { type: 'binary' });
|
|
166
|
+
const sheetName = workbook.SheetNames[0];
|
|
167
|
+
const worksheet = workbook.Sheets[sheetName];
|
|
168
|
+
|
|
169
|
+
// Convert to array of arrays
|
|
170
|
+
const jsonData: any[][] = utils.sheet_to_json(worksheet, { header: 1 });
|
|
171
|
+
|
|
172
|
+
// Convert to spreadsheet format
|
|
173
|
+
const spreadsheetData: Matrix<SpreadsheetCell> = jsonData.map(row =>
|
|
174
|
+
row.map(cell => ({
|
|
175
|
+
value: cell,
|
|
176
|
+
}))
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
handleChange(spreadsheetData);
|
|
180
|
+
addSuccessMessage('Excel file imported successfully');
|
|
181
|
+
} catch (error) {
|
|
182
|
+
console.error('Error importing Excel file:', error);
|
|
183
|
+
addErrorMessage('Failed to import Excel file');
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
reader.readAsBinaryString(file);
|
|
187
|
+
|
|
188
|
+
// Reset input
|
|
189
|
+
event.target.value = '';
|
|
190
|
+
},
|
|
191
|
+
[handleChange]
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
// Handle Excel export
|
|
195
|
+
const handleExport = useCallback(() => {
|
|
196
|
+
try {
|
|
197
|
+
// Convert spreadsheet data to worksheet format
|
|
198
|
+
const worksheetData = data.map(row =>
|
|
199
|
+
row.map(cell => {
|
|
200
|
+
// If cell has a formula, export the calculated value
|
|
201
|
+
if (cell?.formula) {
|
|
202
|
+
return cell.value ?? cell.formula;
|
|
203
|
+
}
|
|
204
|
+
return cell?.value ?? '';
|
|
205
|
+
})
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const worksheet = utils.aoa_to_sheet(worksheetData);
|
|
209
|
+
const workbook = utils.book_new();
|
|
210
|
+
utils.book_append_sheet(workbook, worksheet, 'Sheet1');
|
|
211
|
+
|
|
212
|
+
writeFile(workbook, exportFileName);
|
|
213
|
+
addSuccessMessage('Excel file exported successfully');
|
|
214
|
+
} catch (error) {
|
|
215
|
+
console.error('Error exporting Excel file:', error);
|
|
216
|
+
addErrorMessage('Failed to export Excel file');
|
|
217
|
+
}
|
|
218
|
+
}, [data, exportFileName]);
|
|
219
|
+
|
|
220
|
+
// Handle save
|
|
221
|
+
const handleSave = useCallback(async () => {
|
|
222
|
+
if (!onSave) return;
|
|
223
|
+
|
|
224
|
+
setIsSaving(true);
|
|
225
|
+
try {
|
|
226
|
+
await onSave(data);
|
|
227
|
+
addSuccessMessage('Spreadsheet saved successfully');
|
|
228
|
+
} catch (error) {
|
|
229
|
+
console.error('Error saving spreadsheet:', error);
|
|
230
|
+
addErrorMessage('Failed to save spreadsheet');
|
|
231
|
+
} finally {
|
|
232
|
+
setIsSaving(false);
|
|
233
|
+
}
|
|
234
|
+
}, [onSave, data]);
|
|
235
|
+
|
|
236
|
+
// Build toolbar
|
|
237
|
+
const toolbar = showToolbar && (
|
|
238
|
+
<Stack direction="horizontal" spacing="md" align="center" className="mb-4">
|
|
239
|
+
{title && <div className="text-lg font-medium text-ink-900 flex-1">{title}</div>}
|
|
240
|
+
|
|
241
|
+
{enableImport && (
|
|
242
|
+
<label>
|
|
243
|
+
<input
|
|
244
|
+
type="file"
|
|
245
|
+
accept=".xlsx,.xls,.csv"
|
|
246
|
+
onChange={handleImport}
|
|
247
|
+
className="hidden"
|
|
248
|
+
/>
|
|
249
|
+
<Button variant="ghost" size="sm" icon={<Upload className="h-4 w-4" />}>
|
|
250
|
+
Import
|
|
251
|
+
</Button>
|
|
252
|
+
</label>
|
|
253
|
+
)}
|
|
254
|
+
|
|
255
|
+
{enableExport && (
|
|
256
|
+
<Button
|
|
257
|
+
variant="ghost"
|
|
258
|
+
size="sm"
|
|
259
|
+
icon={<Download className="h-4 w-4" />}
|
|
260
|
+
onClick={handleExport}
|
|
261
|
+
>
|
|
262
|
+
Export
|
|
263
|
+
</Button>
|
|
264
|
+
)}
|
|
265
|
+
|
|
266
|
+
{enableSave && onSave && (
|
|
267
|
+
<Button
|
|
268
|
+
variant="primary"
|
|
269
|
+
size="sm"
|
|
270
|
+
icon={<Save className="h-4 w-4" />}
|
|
271
|
+
onClick={handleSave}
|
|
272
|
+
loading={isSaving}
|
|
273
|
+
>
|
|
274
|
+
Save
|
|
275
|
+
</Button>
|
|
276
|
+
)}
|
|
277
|
+
|
|
278
|
+
{actions}
|
|
279
|
+
</Stack>
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
// Spreadsheet component
|
|
283
|
+
const spreadsheet = (
|
|
284
|
+
<div className={`spreadsheet-container ${className}`}>
|
|
285
|
+
<BaseSpreadsheet
|
|
286
|
+
data={data}
|
|
287
|
+
onChange={readOnly ? undefined : handleChange}
|
|
288
|
+
columnLabels={columnLabels}
|
|
289
|
+
rowLabels={rowLabels}
|
|
290
|
+
className="notebook-spreadsheet"
|
|
291
|
+
/>
|
|
292
|
+
</div>
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
// Wrap in card if requested
|
|
296
|
+
if (wrapInCard) {
|
|
297
|
+
return (
|
|
298
|
+
<Card>
|
|
299
|
+
{(showToolbar || title) && (
|
|
300
|
+
<CardHeader>
|
|
301
|
+
{title && !showToolbar && <CardTitle>{title}</CardTitle>}
|
|
302
|
+
{toolbar}
|
|
303
|
+
</CardHeader>
|
|
304
|
+
)}
|
|
305
|
+
<CardContent>{spreadsheet}</CardContent>
|
|
306
|
+
</Card>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return (
|
|
311
|
+
<>
|
|
312
|
+
{toolbar}
|
|
313
|
+
{spreadsheet}
|
|
314
|
+
</>
|
|
315
|
+
);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* SpreadsheetReport - Pre-configured spreadsheet for report designer
|
|
320
|
+
*
|
|
321
|
+
* A ready-to-use spreadsheet component specifically designed for building
|
|
322
|
+
* and editing reports with formulas, import/export, and save functionality.
|
|
323
|
+
*
|
|
324
|
+
* @example
|
|
325
|
+
* ```tsx
|
|
326
|
+
* const [reportData, setReportData] = useState<Matrix<SpreadsheetCell>>();
|
|
327
|
+
*
|
|
328
|
+
* <SpreadsheetReport
|
|
329
|
+
* data={reportData}
|
|
330
|
+
* onChange={setReportData}
|
|
331
|
+
* title="Monthly Sales Report"
|
|
332
|
+
* onSave={async (data) => {
|
|
333
|
+
* await api.saveReport(reportId, data);
|
|
334
|
+
* }}
|
|
335
|
+
* />
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
export const SpreadsheetReport: React.FC<
|
|
339
|
+
Omit<SpreadsheetProps, 'showToolbar' | 'enableImport' | 'enableExport' | 'enableSave' | 'wrapInCard'>
|
|
340
|
+
> = (props) => {
|
|
341
|
+
return (
|
|
342
|
+
<Spreadsheet
|
|
343
|
+
{...props}
|
|
344
|
+
showToolbar
|
|
345
|
+
enableImport
|
|
346
|
+
enableExport
|
|
347
|
+
enableSave
|
|
348
|
+
wrapInCard
|
|
349
|
+
/>
|
|
350
|
+
);
|
|
351
|
+
};
|