@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.
Files changed (84) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +455 -445
  3. package/dist/components/CurrencyInput.d.ts +52 -0
  4. package/dist/components/CurrencyInput.d.ts.map +1 -0
  5. package/dist/components/DataTable.d.ts +3 -1
  6. package/dist/components/DataTable.d.ts.map +1 -1
  7. package/dist/components/Modal.d.ts.map +1 -1
  8. package/dist/components/Page.d.ts +2 -0
  9. package/dist/components/Page.d.ts.map +1 -1
  10. package/dist/components/PageLayout.d.ts +5 -1
  11. package/dist/components/PageLayout.d.ts.map +1 -1
  12. package/dist/components/Spreadsheet.d.ts +129 -0
  13. package/dist/components/Spreadsheet.d.ts.map +1 -0
  14. package/dist/components/Tabs.d.ts +5 -1
  15. package/dist/components/Tabs.d.ts.map +1 -1
  16. package/dist/components/index.d.ts +6 -0
  17. package/dist/components/index.d.ts.map +1 -1
  18. package/dist/index.d.ts +336 -5
  19. package/dist/index.esm.js +51152 -174
  20. package/dist/index.esm.js.map +1 -1
  21. package/dist/index.js +51145 -143
  22. package/dist/index.js.map +1 -1
  23. package/dist/styles.css +1187 -11
  24. package/dist/utils/excelExport.d.ts +143 -0
  25. package/dist/utils/excelExport.d.ts.map +1 -0
  26. package/dist/utils/index.d.ts +2 -0
  27. package/dist/utils/index.d.ts.map +1 -1
  28. package/package.json +13 -3
  29. package/src/components/AdminModal.css +49 -49
  30. package/src/components/CurrencyInput.stories.tsx +290 -0
  31. package/src/components/CurrencyInput.tsx +193 -0
  32. package/src/components/DataTable.stories.tsx +87 -0
  33. package/src/components/DataTable.tsx +149 -37
  34. package/src/components/Modal.stories.tsx +64 -0
  35. package/src/components/Modal.tsx +15 -2
  36. package/src/components/Page.stories.tsx +76 -0
  37. package/src/components/Page.tsx +35 -3
  38. package/src/components/PageLayout.stories.tsx +75 -0
  39. package/src/components/PageLayout.tsx +28 -9
  40. package/src/components/RoleManager.css +10 -10
  41. package/src/components/Spreadsheet.css +216 -0
  42. package/src/components/Spreadsheet.stories.tsx +362 -0
  43. package/src/components/Spreadsheet.tsx +351 -0
  44. package/src/components/SpreadsheetSimple.stories.tsx +27 -0
  45. package/src/components/Tabs.stories.tsx +31 -0
  46. package/src/components/Tabs.tsx +28 -4
  47. package/src/components/TimePicker.tsx +1 -1
  48. package/src/components/Toast.tsx +9 -9
  49. package/src/components/__tests__/Input.test.tsx +22 -26
  50. package/src/components/index.ts +11 -2
  51. package/src/styles/index.css +44 -6
  52. package/src/utils/excelExport.stories.tsx +535 -0
  53. package/src/utils/excelExport.ts +225 -0
  54. package/src/utils/index.ts +3 -0
  55. package/src/utils/sqlToNaturalLanguage.ts +1 -1
  56. package/tailwind.config.js +253 -253
  57. package/dist/components/Button.stories.d.ts +0 -51
  58. package/dist/components/Button.stories.d.ts.map +0 -1
  59. package/dist/components/ChartVisualizationUI.d.ts +0 -21
  60. package/dist/components/ChartVisualizationUI.d.ts.map +0 -1
  61. package/dist/components/ChatUI.d.ts +0 -23
  62. package/dist/components/ChatUI.d.ts.map +0 -1
  63. package/dist/components/CommissionDashboardUI.d.ts +0 -25
  64. package/dist/components/CommissionDashboardUI.d.ts.map +0 -1
  65. package/dist/components/DataTable.stories.d.ts +0 -23
  66. package/dist/components/DataTable.stories.d.ts.map +0 -1
  67. package/dist/components/FormField.d.ts +0 -35
  68. package/dist/components/FormField.d.ts.map +0 -1
  69. package/dist/components/Input.stories.d.ts +0 -366
  70. package/dist/components/Input.stories.d.ts.map +0 -1
  71. package/dist/components/InsightsPanelUI.d.ts +0 -21
  72. package/dist/components/InsightsPanelUI.d.ts.map +0 -1
  73. package/dist/components/PaymentHistoryTimeline.d.ts +0 -34
  74. package/dist/components/PaymentHistoryTimeline.d.ts.map +0 -1
  75. package/dist/components/RelationshipManagerUI.d.ts +0 -60
  76. package/dist/components/RelationshipManagerUI.d.ts.map +0 -1
  77. package/dist/components/RoleManager.d.ts +0 -19
  78. package/dist/components/RoleManager.d.ts.map +0 -1
  79. package/dist/components/SplitCommissionBadge.d.ts +0 -18
  80. package/dist/components/SplitCommissionBadge.d.ts.map +0 -1
  81. package/dist/components/__tests__/Button.test.d.ts +0 -2
  82. package/dist/components/__tests__/Button.test.d.ts.map +0 -1
  83. package/dist/components/__tests__/Input.test.d.ts +0 -2
  84. 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
+ };