@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,27 @@
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
+ import { Spreadsheet } from './Spreadsheet';
3
+
4
+ const meta: Meta<typeof Spreadsheet> = {
5
+ title: 'Components/Spreadsheet/Simple Test',
6
+ component: Spreadsheet,
7
+ parameters: {
8
+ docs: {
9
+ description: {
10
+ component: 'Simple test story for Spreadsheet component debugging.',
11
+ },
12
+ },
13
+ },
14
+ };
15
+
16
+ export default meta;
17
+ type Story = StoryObj<typeof Spreadsheet>;
18
+
19
+ /**
20
+ * Minimal test - just render with defaults
21
+ */
22
+ export const MinimalTest: Story = {
23
+ args: {
24
+ rows: 5,
25
+ columns: 3,
26
+ },
27
+ };
@@ -249,3 +249,34 @@ export const Complete: Story = {
249
249
  return <Tabs tabs={tabs} activeTab={activeTab} onChange={setActiveTab} variant="underline" />;
250
250
  },
251
251
  };
252
+
253
+ export const ControlledMode: Story = {
254
+ render: () => {
255
+ const [activeTab, setActiveTab] = useState('profile');
256
+ return (
257
+ <div>
258
+ <div style={{ marginBottom: '1rem', display: 'flex', gap: '0.5rem' }}>
259
+ <button
260
+ onClick={() => setActiveTab('profile')}
261
+ style={{ padding: '0.5rem 1rem', background: activeTab === 'profile' ? '#334155' : '#f1f5f9', color: activeTab === 'profile' ? 'white' : '#334155', border: 'none', borderRadius: '0.375rem', cursor: 'pointer' }}
262
+ >
263
+ Go to Profile
264
+ </button>
265
+ <button
266
+ onClick={() => setActiveTab('settings')}
267
+ style={{ padding: '0.5rem 1rem', background: activeTab === 'settings' ? '#334155' : '#f1f5f9', color: activeTab === 'settings' ? 'white' : '#334155', border: 'none', borderRadius: '0.375rem', cursor: 'pointer' }}
268
+ >
269
+ Go to Settings
270
+ </button>
271
+ <button
272
+ onClick={() => setActiveTab('notifications')}
273
+ style={{ padding: '0.5rem 1rem', background: activeTab === 'notifications' ? '#334155' : '#f1f5f9', color: activeTab === 'notifications' ? 'white' : '#334155', border: 'none', borderRadius: '0.375rem', cursor: 'pointer' }}
274
+ >
275
+ Go to Notifications
276
+ </button>
277
+ </div>
278
+ <Tabs tabs={basicTabs} activeTab={activeTab} onChange={setActiveTab} />
279
+ </div>
280
+ );
281
+ },
282
+ };
@@ -1,4 +1,4 @@
1
- import React, { useState } from 'react';
1
+ import React, { useState, useEffect } from 'react';
2
2
 
3
3
  export interface Tab {
4
4
  id: string;
@@ -10,20 +10,44 @@ export interface Tab {
10
10
 
11
11
  export interface TabsProps {
12
12
  tabs: Tab[];
13
+ /** Controlled mode: Currently active tab ID */
14
+ activeTab?: string;
15
+ /** Uncontrolled mode: Initial tab ID (ignored if activeTab is provided) */
13
16
  defaultTab?: string;
14
17
  variant?: 'underline' | 'pill';
15
18
  /** Orientation of tabs (default: 'horizontal') */
16
19
  orientation?: 'horizontal' | 'vertical';
17
20
  /** Size of tabs (default: 'md') */
18
21
  size?: 'sm' | 'md' | 'lg';
22
+ /** Called when tab changes (required for controlled mode) */
19
23
  onChange?: (tabId: string) => void;
20
24
  }
21
25
 
22
- export default function Tabs({ tabs, defaultTab, variant = 'underline', orientation = 'horizontal', size = 'md', onChange }: TabsProps) {
23
- const [activeTab, setActiveTab] = useState(defaultTab || tabs[0]?.id);
26
+ export default function Tabs({ tabs, activeTab: controlledActiveTab, defaultTab, variant = 'underline', orientation = 'horizontal', size = 'md', onChange }: TabsProps) {
27
+ const [internalActiveTab, setInternalActiveTab] = useState(defaultTab || tabs[0]?.id);
28
+
29
+ // Controlled mode: use activeTab prop, Uncontrolled mode: use internal state
30
+ const isControlled = controlledActiveTab !== undefined;
31
+ const activeTab = isControlled ? controlledActiveTab : internalActiveTab;
32
+
33
+ // Ensure the activeTab exists in the current tabs array
34
+ // This handles the case where tabs array reference changes at the same time as activeTab
35
+ useEffect(() => {
36
+ const tabExists = tabs.some(tab => tab.id === activeTab);
37
+ if (!tabExists && tabs.length > 0) {
38
+ // If the activeTab doesn't exist in the new tabs array, use the first tab
39
+ if (isControlled) {
40
+ onChange?.(tabs[0].id);
41
+ } else {
42
+ setInternalActiveTab(tabs[0].id);
43
+ }
44
+ }
45
+ }, [tabs, activeTab, isControlled, onChange]);
24
46
 
25
47
  const handleTabChange = (tabId: string) => {
26
- setActiveTab(tabId);
48
+ if (!isControlled) {
49
+ setInternalActiveTab(tabId);
50
+ }
27
51
  onChange?.(tabId);
28
52
  };
29
53
 
@@ -142,7 +142,7 @@ const TimePicker = forwardRef<TimePickerHandle, TimePickerProps>(({
142
142
 
143
143
  // Format TimeValue to string
144
144
  function formatTimeValue(tv: TimeValue, is12Hour: boolean, includeSeconds: boolean): string {
145
- let hours = tv.hours;
145
+ const hours = tv.hours;
146
146
 
147
147
  if (is12Hour) {
148
148
  const formatted = `${hours.toString().padStart(2, '0')}:${tv.minutes.toString().padStart(2, '0')}${includeSeconds ? ':' + tv.seconds.toString().padStart(2, '0') : ''} ${tv.period}`;
@@ -1,4 +1,4 @@
1
- import React, { useEffect, useState } from 'react';
1
+ import React, { useEffect, useState, useCallback } from 'react';
2
2
  import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
3
3
 
4
4
  export type ToastType = 'success' | 'error' | 'warning' | 'info';
@@ -40,20 +40,20 @@ export default function Toast({ id, type, title, message, duration = 5000, onClo
40
40
  const [isExiting, setIsExiting] = useState(false);
41
41
  const styles = toastStyles[type];
42
42
 
43
+ const handleClose = useCallback(() => {
44
+ setIsExiting(true);
45
+ setTimeout(() => {
46
+ onClose(id);
47
+ }, 300); // Match animation duration
48
+ }, [id, onClose]);
49
+
43
50
  useEffect(() => {
44
51
  const timer = setTimeout(() => {
45
52
  handleClose();
46
53
  }, duration);
47
54
 
48
55
  return () => clearTimeout(timer);
49
- }, [duration]);
50
-
51
- const handleClose = () => {
52
- setIsExiting(true);
53
- setTimeout(() => {
54
- onClose(id);
55
- }, 300); // Match animation duration
56
- };
56
+ }, [duration, handleClose]);
57
57
 
58
58
  return (
59
59
  <div
@@ -1,4 +1,4 @@
1
- import { render, screen, fireEvent } from '@testing-library/react';
1
+ import { render, screen } from '@testing-library/react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import Input from '../Input';
4
4
 
@@ -19,8 +19,8 @@ describe('Input', () => {
19
19
  expect(handleChange).toHaveBeenCalled();
20
20
  });
21
21
 
22
- it('shows error message', () => {
23
- render(<Input label="Email" error="Invalid email" />);
22
+ it('shows validation message', () => {
23
+ render(<Input label="Email" validationState="error" validationMessage="Invalid email" />);
24
24
  expect(screen.getByText('Invalid email')).toBeInTheDocument();
25
25
  });
26
26
 
@@ -40,7 +40,7 @@ describe('Input', () => {
40
40
  });
41
41
 
42
42
  it('is read-only when readOnly prop is true', () => {
43
- render(<Input label="Email" readOnly value="test@example.com" />);
43
+ render(<Input label="Email" readOnly value="test@example.com" onChange={() => {}} />);
44
44
  const input = screen.getByLabelText('Email') as HTMLInputElement;
45
45
  expect(input.readOnly).toBe(true);
46
46
  });
@@ -55,39 +55,25 @@ describe('Input', () => {
55
55
  expect(screen.getByText('.com')).toBeInTheDocument();
56
56
  });
57
57
 
58
- it('applies different sizes', () => {
59
- const { rerender } = render(<Input label="Email" size="sm" />);
60
- let input = screen.getByLabelText('Email');
61
- expect(input).toHaveClass('text-sm');
62
-
63
- rerender(<Input label="Email" size="lg" />);
64
- input = screen.getByLabelText('Email');
65
- expect(input).toHaveClass('text-lg');
66
- });
67
-
68
58
  it('handles clear button click', async () => {
69
59
  const user = userEvent.setup();
70
60
  const handleClear = jest.fn();
61
+ const handleChange = jest.fn();
71
62
 
72
- render(<Input label="Search" clearable onClear={handleClear} value="test" />);
63
+ render(<Input label="Search" clearable onClear={handleClear} value="test" onChange={handleChange} />);
73
64
 
74
- // Find and click clear button (would need to verify selector)
75
- const clearButton = screen.getByLabelText('Clear');
65
+ // Find clear button by aria-label
66
+ const clearButton = screen.getByLabelText('Clear input');
76
67
  await user.click(clearButton);
77
68
 
78
69
  expect(handleClear).toHaveBeenCalled();
79
70
  });
80
71
 
81
- it('shows loading state', () => {
82
- render(<Input label="Email" loading />);
83
- // Loading indicator should be rendered
84
- expect(screen.getByLabelText('Email')).toBeInTheDocument();
85
- });
86
-
87
- it('applies error styling when error is present', () => {
88
- render(<Input label="Email" error="Invalid" />);
72
+ it('applies error styling when validation state is error', () => {
73
+ render(<Input label="Email" validationState="error" validationMessage="Invalid" />);
89
74
  const input = screen.getByLabelText('Email');
90
- expect(input).toHaveClass('border-error-500');
75
+ // Check for error border class
76
+ expect(input).toHaveClass('border-error-400');
91
77
  });
92
78
 
93
79
  it('supports different input types', () => {
@@ -99,4 +85,14 @@ describe('Input', () => {
99
85
  input = screen.getByLabelText('Password') as HTMLInputElement;
100
86
  expect(input.type).toBe('password');
101
87
  });
88
+
89
+ it('shows character count when enabled', () => {
90
+ render(<Input label="Bio" showCount maxLength={100} value="Hello" onChange={() => {}} />);
91
+ expect(screen.getByText(/5/)).toBeInTheDocument();
92
+ });
93
+
94
+ it('shows password toggle button for password input', () => {
95
+ render(<Input label="Password" type="password" showPasswordToggle />);
96
+ expect(screen.getByLabelText('Show password')).toBeInTheDocument();
97
+ });
102
98
  });
@@ -283,13 +283,17 @@ export type { NotificationIndicatorProps } from './NotificationIndicator';
283
283
 
284
284
  // Data Table
285
285
  export { default as DataTable } from './DataTable';
286
- export type {
287
- DataTableColumn,
286
+ export type {
287
+ DataTableColumn,
288
288
  DataTableAction,
289
289
  ExpandedRowConfig,
290
290
  ExpansionMode
291
291
  } from './DataTable';
292
292
 
293
+ // Spreadsheet
294
+ export { Spreadsheet, SpreadsheetReport } from './Spreadsheet';
295
+ export type { SpreadsheetProps, SpreadsheetCell, Matrix, CellBase } from './Spreadsheet';
296
+
293
297
  export { default as ExpandedRowEditForm } from './ExpandedRowEditForm';
294
298
  export type {
295
299
  ExpandedRowEditFormProps,
@@ -305,6 +309,8 @@ export type {
305
309
  // Display Components
306
310
  export { default as CurrencyDisplay } from './CurrencyDisplay';
307
311
  export type { CurrencyDisplayProps } from './CurrencyDisplay';
312
+ export { default as CurrencyInput } from './CurrencyInput';
313
+ export type { CurrencyInputProps } from './CurrencyInput';
308
314
 
309
315
  export { default as DateDisplay } from './DateDisplay';
310
316
  export type { DateDisplayProps } from './DateDisplay';
@@ -405,6 +411,9 @@ export {
405
411
  } from '../utils/tableEnhancements';
406
412
  export type { ColumnResize, ColumnOrder } from '../utils/tableEnhancements';
407
413
 
414
+ export { exportToExcel, exportDataTableToExcel, createMultiSheetExcel } from '../utils/excelExport';
415
+ export type { ExcelColumn, ExportToExcelOptions, DataTableExportOptions, MultiSheetExcelOptions } from '../utils/excelExport';
416
+
408
417
  // Hooks
409
418
  export { useColumnResize, useColumnReorder } from '../hooks/useTableEnhancements';
410
419
  export type { UseColumnResizeOptions, UseColumnReorderOptions } from '../hooks/useTableEnhancements';
@@ -1,5 +1,8 @@
1
1
  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
2
2
 
3
+ /* Component styles */
4
+ @import '../components/Spreadsheet.css';
5
+
3
6
  @tailwind base;
4
7
  @tailwind components;
5
8
  @tailwind utilities;
@@ -169,16 +172,53 @@
169
172
  @apply text-error-600;
170
173
  }
171
174
 
172
- /* Notebook Page Container - Creates bounded page effect */
175
+ /* Notebook Page Container - Creates bounded page effect
176
+ NOTE: This class is deprecated - Page component now handles responsive layout via props
177
+ Keeping for backward compatibility only */
173
178
  .notebook-page {
174
179
  @apply bg-white bg-subtle-grain rounded-sm shadow-lg border-l-4 border-paper-300;
175
180
  max-width: 1400px;
176
- margin: 1rem auto 1rem 1rem; /* Top margin, auto right for anchoring, bottom margin, small left margin */
177
- padding: 3rem 4rem 3rem 5rem; /* Extra left padding for binding margin */
178
- min-height: calc(100vh - 12rem);
181
+ /* Responsive margins - fixed left/top, responsive right/bottom */
182
+ margin-top: 1rem;
183
+ margin-left: 1rem;
184
+ margin-right: 1rem;
185
+ margin-bottom: 1rem;
186
+ /* Responsive padding - fixed left/top, responsive right/bottom */
187
+ padding-top: 3rem;
188
+ padding-left: 5rem; /* Extra left padding for binding margin */
189
+ padding-right: 1rem;
190
+ padding-bottom: 1rem;
191
+ min-height: calc(100vh - 2rem);
179
192
  position: relative;
180
193
  }
181
194
 
195
+ /* Responsive padding/margin increases on larger screens */
196
+ @media (min-width: 640px) {
197
+ .notebook-page {
198
+ margin-right: 1.5rem;
199
+ margin-bottom: 2rem;
200
+ padding-right: 2rem;
201
+ padding-bottom: 2rem;
202
+ }
203
+ }
204
+
205
+ @media (min-width: 768px) {
206
+ .notebook-page {
207
+ margin-right: 2rem;
208
+ margin-bottom: 2rem;
209
+ padding-right: 3rem;
210
+ padding-bottom: 3rem;
211
+ }
212
+ }
213
+
214
+ @media (min-width: 1024px) {
215
+ .notebook-page {
216
+ margin-right: auto; /* Center on large screens */
217
+ padding-right: 4rem;
218
+ padding-bottom: 4rem;
219
+ }
220
+ }
221
+
182
222
  /* Notebook binding effect on sidebar */
183
223
  .notebook-binding {
184
224
  position: relative;
@@ -427,8 +467,6 @@
427
467
 
428
468
  .table-stable tbody tr td {
429
469
  vertical-align: top;
430
- overflow: hidden;
431
- text-overflow: ellipsis;
432
470
  }
433
471
 
434
472
  /* Smooth loading overlay */