@papernote/ui 1.0.0 → 1.1.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/README.md +13 -3
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/Spreadsheet.css +216 -0
- 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 +2 -0
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +134 -3
- package/dist/index.esm.js +50720 -69
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +50717 -46
- package/dist/index.js.map +1 -1
- package/dist/styles.css +3492 -2518
- package/package.json +13 -3
- package/src/components/DataTable.stories.tsx +87 -0
- package/src/components/DataTable.tsx +71 -23
- 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 +152 -128
- 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 +6 -2
- package/src/styles/index.css +3 -2
- package/src/utils/sqlToNaturalLanguage.ts +1 -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
|
+
};
|
package/src/components/Tabs.tsx
CHANGED
|
@@ -1,128 +1,152 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
2
|
-
|
|
3
|
-
export interface Tab {
|
|
4
|
-
id: string;
|
|
5
|
-
label: string;
|
|
6
|
-
icon?: React.ReactNode;
|
|
7
|
-
content: React.ReactNode;
|
|
8
|
-
disabled?: boolean;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface TabsProps {
|
|
12
|
-
tabs: Tab[];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
}
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface Tab {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
icon?: React.ReactNode;
|
|
7
|
+
content: React.ReactNode;
|
|
8
|
+
disabled?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface TabsProps {
|
|
12
|
+
tabs: Tab[];
|
|
13
|
+
/** Controlled mode: Currently active tab ID */
|
|
14
|
+
activeTab?: string;
|
|
15
|
+
/** Uncontrolled mode: Initial tab ID (ignored if activeTab is provided) */
|
|
16
|
+
defaultTab?: string;
|
|
17
|
+
variant?: 'underline' | 'pill';
|
|
18
|
+
/** Orientation of tabs (default: 'horizontal') */
|
|
19
|
+
orientation?: 'horizontal' | 'vertical';
|
|
20
|
+
/** Size of tabs (default: 'md') */
|
|
21
|
+
size?: 'sm' | 'md' | 'lg';
|
|
22
|
+
/** Called when tab changes (required for controlled mode) */
|
|
23
|
+
onChange?: (tabId: string) => void;
|
|
24
|
+
}
|
|
25
|
+
|
|
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]);
|
|
46
|
+
|
|
47
|
+
const handleTabChange = (tabId: string) => {
|
|
48
|
+
if (!isControlled) {
|
|
49
|
+
setInternalActiveTab(tabId);
|
|
50
|
+
}
|
|
51
|
+
onChange?.(tabId);
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Size-specific classes
|
|
55
|
+
const sizeClasses = {
|
|
56
|
+
sm: {
|
|
57
|
+
padding: 'px-3 py-1.5',
|
|
58
|
+
text: 'text-xs',
|
|
59
|
+
icon: 'h-3.5 w-3.5',
|
|
60
|
+
gap: orientation === 'vertical' ? 'gap-1.5' : 'gap-4',
|
|
61
|
+
minWidth: orientation === 'vertical' ? 'min-w-[150px]' : '',
|
|
62
|
+
spacing: orientation === 'vertical' ? 'mt-4' : 'mt-4',
|
|
63
|
+
},
|
|
64
|
+
md: {
|
|
65
|
+
padding: 'px-4 py-2.5',
|
|
66
|
+
text: 'text-sm',
|
|
67
|
+
icon: 'h-4 w-4',
|
|
68
|
+
gap: orientation === 'vertical' ? 'gap-2' : 'gap-6',
|
|
69
|
+
minWidth: orientation === 'vertical' ? 'min-w-[200px]' : '',
|
|
70
|
+
spacing: orientation === 'vertical' ? 'mt-6' : 'mt-6',
|
|
71
|
+
},
|
|
72
|
+
lg: {
|
|
73
|
+
padding: 'px-5 py-3',
|
|
74
|
+
text: 'text-base',
|
|
75
|
+
icon: 'h-5 w-5',
|
|
76
|
+
gap: orientation === 'vertical' ? 'gap-3' : 'gap-8',
|
|
77
|
+
minWidth: orientation === 'vertical' ? 'min-w-[250px]' : '',
|
|
78
|
+
spacing: orientation === 'vertical' ? 'mt-8' : 'mt-8',
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<div className={`w-full ${orientation === 'vertical' ? `flex ${sizeClasses[size].gap}` : ''}`}>
|
|
84
|
+
{/* Tab Headers */}
|
|
85
|
+
<div
|
|
86
|
+
className={`
|
|
87
|
+
flex ${orientation === 'vertical' ? 'flex-col' : ''}
|
|
88
|
+
${variant === 'underline'
|
|
89
|
+
? orientation === 'vertical'
|
|
90
|
+
? `border-r border-paper-200 ${sizeClasses[size].gap} pr-6`
|
|
91
|
+
: `border-b border-paper-200 ${sizeClasses[size].gap}`
|
|
92
|
+
: `${sizeClasses[size].gap} p-1 bg-paper-50 rounded-lg`
|
|
93
|
+
}
|
|
94
|
+
${sizeClasses[size].minWidth}
|
|
95
|
+
`}
|
|
96
|
+
role="tablist"
|
|
97
|
+
>
|
|
98
|
+
{tabs.map((tab) => {
|
|
99
|
+
const isActive = activeTab === tab.id;
|
|
100
|
+
|
|
101
|
+
return (
|
|
102
|
+
<button
|
|
103
|
+
key={tab.id}
|
|
104
|
+
role="tab"
|
|
105
|
+
aria-selected={isActive}
|
|
106
|
+
aria-controls={`panel-${tab.id}`}
|
|
107
|
+
disabled={tab.disabled}
|
|
108
|
+
onClick={() => !tab.disabled && handleTabChange(tab.id)}
|
|
109
|
+
className={`
|
|
110
|
+
flex items-center gap-2 ${sizeClasses[size].padding} ${sizeClasses[size].text} font-medium transition-all duration-200
|
|
111
|
+
${orientation === 'vertical' ? 'w-full justify-start' : ''}
|
|
112
|
+
${
|
|
113
|
+
variant === 'underline'
|
|
114
|
+
? isActive
|
|
115
|
+
? orientation === 'vertical'
|
|
116
|
+
? 'text-accent-900 border-r-2 border-accent-500 -mr-[1px]'
|
|
117
|
+
: 'text-accent-900 border-b-2 border-accent-500 -mb-[1px]'
|
|
118
|
+
: orientation === 'vertical'
|
|
119
|
+
? 'text-ink-600 hover:text-ink-900 border-r-2 border-transparent'
|
|
120
|
+
: 'text-ink-600 hover:text-ink-900 border-b-2 border-transparent'
|
|
121
|
+
: isActive
|
|
122
|
+
? 'bg-white text-accent-900 rounded-md shadow-xs'
|
|
123
|
+
: 'text-ink-600 hover:text-ink-900 hover:bg-white/50 rounded-md'
|
|
124
|
+
}
|
|
125
|
+
${tab.disabled ? 'opacity-40 cursor-not-allowed' : 'cursor-pointer'}
|
|
126
|
+
`}
|
|
127
|
+
>
|
|
128
|
+
{tab.icon && <span className={`flex-shrink-0 ${sizeClasses[size].icon}`}>{tab.icon}</span>}
|
|
129
|
+
<span>{tab.label}</span>
|
|
130
|
+
</button>
|
|
131
|
+
);
|
|
132
|
+
})}
|
|
133
|
+
</div>
|
|
134
|
+
|
|
135
|
+
{/* Tab Content */}
|
|
136
|
+
<div className={`${orientation === 'vertical' ? 'flex-1' : sizeClasses[size].spacing}`}>
|
|
137
|
+
{tabs.map((tab) => (
|
|
138
|
+
<div
|
|
139
|
+
key={tab.id}
|
|
140
|
+
id={`panel-${tab.id}`}
|
|
141
|
+
role="tabpanel"
|
|
142
|
+
aria-labelledby={tab.id}
|
|
143
|
+
hidden={activeTab !== tab.id}
|
|
144
|
+
className={activeTab === tab.id ? 'animate-fade-in' : ''}
|
|
145
|
+
>
|
|
146
|
+
{activeTab === tab.id && tab.content}
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
</div>
|
|
150
|
+
</div>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
@@ -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
|
-
|
|
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}`;
|
package/src/components/Toast.tsx
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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('
|
|
82
|
-
render(<Input label="Email"
|
|
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
|
-
|
|
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
|
});
|
package/src/components/index.ts
CHANGED
|
@@ -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,
|
package/src/styles/index.css
CHANGED
|
@@ -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;
|
|
@@ -427,8 +430,6 @@
|
|
|
427
430
|
|
|
428
431
|
.table-stable tbody tr td {
|
|
429
432
|
vertical-align: top;
|
|
430
|
-
overflow: hidden;
|
|
431
|
-
text-overflow: ellipsis;
|
|
432
433
|
}
|
|
433
434
|
|
|
434
435
|
/* Smooth loading overlay */
|
|
@@ -415,7 +415,7 @@ function parseCondition(condition: string, friendlyNames: FriendlyNameConfig): s
|
|
|
415
415
|
function extractFieldName(condition: string, friendlyNames: FriendlyNameConfig): string {
|
|
416
416
|
condition = condition.trim();
|
|
417
417
|
|
|
418
|
-
|
|
418
|
+
const match = condition.match(/^(?:["']?\w+["']?\.)?["']?(\w+)["']?/);
|
|
419
419
|
|
|
420
420
|
if (match && match[1]) {
|
|
421
421
|
const fieldName = match[1];
|