@papernote/ui 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +455 -455
- package/dist/components/Box.d.ts +2 -1
- package/dist/components/Box.d.ts.map +1 -1
- package/dist/components/Button.d.ts +10 -1
- package/dist/components/Button.d.ts.map +1 -1
- package/dist/components/Card.d.ts +11 -2
- package/dist/components/Card.d.ts.map +1 -1
- package/dist/components/CurrencyInput.d.ts +52 -0
- package/dist/components/CurrencyInput.d.ts.map +1 -0
- package/dist/components/DataTable.d.ts +19 -3
- package/dist/components/DataTable.d.ts.map +1 -1
- package/dist/components/EmptyState.d.ts +3 -1
- package/dist/components/EmptyState.d.ts.map +1 -1
- package/dist/components/Grid.d.ts +4 -2
- package/dist/components/Grid.d.ts.map +1 -1
- package/dist/components/Input.d.ts +2 -0
- package/dist/components/Input.d.ts.map +1 -1
- package/dist/components/Modal.d.ts.map +1 -1
- package/dist/components/MultiSelect.d.ts +13 -1
- package/dist/components/MultiSelect.d.ts.map +1 -1
- package/dist/components/Page.d.ts +2 -0
- package/dist/components/Page.d.ts.map +1 -1
- package/dist/components/PageLayout.d.ts +5 -1
- package/dist/components/PageLayout.d.ts.map +1 -1
- package/dist/components/Stack.d.ts +25 -5
- package/dist/components/Stack.d.ts.map +1 -1
- package/dist/components/Text.d.ts +20 -4
- package/dist/components/Text.d.ts.map +1 -1
- package/dist/components/Textarea.d.ts +2 -0
- package/dist/components/Textarea.d.ts.map +1 -1
- package/dist/components/index.d.ts +5 -3
- package/dist/components/index.d.ts.map +1 -1
- package/dist/index.d.ts +311 -49
- package/dist/index.esm.js +557 -224
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +555 -219
- package/dist/index.js.map +1 -1
- package/dist/styles.css +2838 -2679
- package/dist/utils/excelExport.d.ts +143 -0
- package/dist/utils/excelExport.d.ts.map +1 -0
- package/dist/utils/index.d.ts +2 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/AdminModal.css +49 -49
- package/src/components/Box.stories.tsx +377 -0
- package/src/components/Box.tsx +8 -4
- package/src/components/Button.tsx +23 -10
- package/src/components/Card.tsx +20 -5
- package/src/components/CurrencyInput.stories.tsx +290 -0
- package/src/components/CurrencyInput.tsx +193 -0
- package/src/components/DataTable.stories.tsx +36 -25
- package/src/components/DataTable.tsx +170 -16
- package/src/components/EmptyState.stories.tsx +124 -72
- package/src/components/EmptyState.tsx +10 -0
- package/src/components/Grid.stories.tsx +348 -0
- package/src/components/Grid.tsx +12 -5
- package/src/components/Input.tsx +12 -2
- package/src/components/Modal.stories.tsx +64 -0
- package/src/components/Modal.tsx +15 -2
- package/src/components/MultiSelect.tsx +41 -10
- package/src/components/Page.stories.tsx +76 -0
- package/src/components/Page.tsx +35 -3
- package/src/components/PageLayout.stories.tsx +75 -0
- package/src/components/PageLayout.tsx +28 -9
- package/src/components/RoleManager.css +10 -10
- package/src/components/Spreadsheet.css +216 -216
- package/src/components/Spreadsheet.stories.tsx +362 -362
- package/src/components/Spreadsheet.tsx +351 -351
- package/src/components/SpreadsheetSimple.stories.tsx +27 -27
- package/src/components/Stack.stories.tsx +24 -1
- package/src/components/Stack.tsx +40 -10
- package/src/components/Tabs.tsx +152 -152
- package/src/components/Text.stories.tsx +273 -0
- package/src/components/Text.tsx +33 -8
- package/src/components/Textarea.tsx +32 -21
- package/src/components/index.ts +6 -4
- package/src/styles/index.css +41 -4
- package/src/utils/excelExport.stories.tsx +535 -0
- package/src/utils/excelExport.ts +225 -0
- package/src/utils/index.ts +3 -0
- package/tailwind.config.js +253 -253
- package/dist/components/Button.stories.d.ts +0 -51
- package/dist/components/Button.stories.d.ts.map +0 -1
- package/dist/components/ChartVisualizationUI.d.ts +0 -21
- package/dist/components/ChartVisualizationUI.d.ts.map +0 -1
- package/dist/components/ChatUI.d.ts +0 -23
- package/dist/components/ChatUI.d.ts.map +0 -1
- package/dist/components/CommissionDashboardUI.d.ts +0 -25
- package/dist/components/CommissionDashboardUI.d.ts.map +0 -1
- package/dist/components/DataTable.stories.d.ts +0 -23
- package/dist/components/DataTable.stories.d.ts.map +0 -1
- package/dist/components/FormField.d.ts +0 -35
- package/dist/components/FormField.d.ts.map +0 -1
- package/dist/components/Input.stories.d.ts +0 -366
- package/dist/components/Input.stories.d.ts.map +0 -1
- package/dist/components/InsightsPanelUI.d.ts +0 -21
- package/dist/components/InsightsPanelUI.d.ts.map +0 -1
- package/dist/components/PaymentHistoryTimeline.d.ts +0 -34
- package/dist/components/PaymentHistoryTimeline.d.ts.map +0 -1
- package/dist/components/RelationshipManagerUI.d.ts +0 -60
- package/dist/components/RelationshipManagerUI.d.ts.map +0 -1
- package/dist/components/RoleManager.d.ts +0 -19
- package/dist/components/RoleManager.d.ts.map +0 -1
- package/dist/components/SplitCommissionBadge.d.ts +0 -18
- package/dist/components/SplitCommissionBadge.d.ts.map +0 -1
- package/dist/components/Spreadsheet.css +0 -216
- package/dist/components/Table.d.ts +0 -26
- package/dist/components/Table.d.ts.map +0 -1
- package/dist/components/__tests__/Button.test.d.ts +0 -2
- package/dist/components/__tests__/Button.test.d.ts.map +0 -1
- package/dist/components/__tests__/Input.test.d.ts +0 -2
- package/dist/components/__tests__/Input.test.d.ts.map +0 -1
- package/src/components/Table.tsx +0 -239
|
@@ -1,27 +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
|
-
};
|
|
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
|
+
};
|
|
@@ -49,7 +49,16 @@ import { Stack, Button } from 'notebook-ui';
|
|
|
49
49
|
spacing: {
|
|
50
50
|
control: 'select',
|
|
51
51
|
options: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
|
|
52
|
-
description: 'Gap spacing between children (
|
|
52
|
+
description: 'Gap spacing between children (alias: gap)',
|
|
53
|
+
table: {
|
|
54
|
+
type: { summary: 'none | xs | sm | md | lg | xl' },
|
|
55
|
+
defaultValue: { summary: 'md' },
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
gap: {
|
|
59
|
+
control: 'select',
|
|
60
|
+
options: ['none', 'xs', 'sm', 'md', 'lg', 'xl'],
|
|
61
|
+
description: 'Gap spacing between children (alias for spacing)',
|
|
53
62
|
table: {
|
|
54
63
|
type: { summary: 'none | xs | sm | md | lg | xl' },
|
|
55
64
|
defaultValue: { summary: 'md' },
|
|
@@ -339,3 +348,17 @@ export const NestedStacks: Story = {
|
|
|
339
348
|
</Stack>
|
|
340
349
|
),
|
|
341
350
|
};
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* The `gap` prop is an alias for `spacing` - they are interchangeable.
|
|
354
|
+
* This provides flexibility for developers who prefer the `gap` terminology.
|
|
355
|
+
*/
|
|
356
|
+
export const GapAlias: Story = {
|
|
357
|
+
render: () => (
|
|
358
|
+
<Stack direction="horizontal" gap="md">
|
|
359
|
+
<Box>Using</Box>
|
|
360
|
+
<Box color="#8b5cf6">gap</Box>
|
|
361
|
+
<Box color="#10b981">prop</Box>
|
|
362
|
+
</Stack>
|
|
363
|
+
),
|
|
364
|
+
};
|
package/src/components/Stack.tsx
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
// Stack Component - Vertical or horizontal stacking layout
|
|
2
2
|
// Provides consistent spacing between child elements
|
|
3
3
|
|
|
4
|
-
import React from 'react';
|
|
4
|
+
import React, { forwardRef } from 'react';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
type SpacingValue = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl';
|
|
7
|
+
|
|
8
|
+
export interface StackProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
7
9
|
/** Content to stack */
|
|
8
10
|
children: React.ReactNode;
|
|
9
11
|
/** Direction of stack */
|
|
10
12
|
direction?: 'vertical' | 'horizontal';
|
|
11
|
-
/** Spacing between items */
|
|
12
|
-
spacing?:
|
|
13
|
+
/** Spacing between items (alias: gap) */
|
|
14
|
+
spacing?: SpacingValue;
|
|
15
|
+
/** Spacing between items (alias for spacing - for developer convenience) */
|
|
16
|
+
gap?: SpacingValue;
|
|
13
17
|
/** Alignment of items */
|
|
14
18
|
align?: 'start' | 'center' | 'end' | 'stretch';
|
|
15
19
|
/** Justify content */
|
|
@@ -23,23 +27,45 @@ export interface StackProps {
|
|
|
23
27
|
/**
|
|
24
28
|
* Stack component for arranging children vertically or horizontally with consistent spacing.
|
|
25
29
|
*
|
|
26
|
-
*
|
|
30
|
+
* Supports ref forwarding for DOM access.
|
|
31
|
+
*
|
|
32
|
+
* Spacing scale (use either `spacing` or `gap` prop - they're aliases):
|
|
27
33
|
* - none: 0
|
|
28
34
|
* - xs: 0.5rem (2)
|
|
29
35
|
* - sm: 0.75rem (3)
|
|
30
36
|
* - md: 1.5rem (6)
|
|
31
37
|
* - lg: 2rem (8)
|
|
32
38
|
* - xl: 3rem (12)
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```tsx
|
|
42
|
+
* // Using spacing prop
|
|
43
|
+
* <Stack spacing="md">
|
|
44
|
+
* <Card>Item 1</Card>
|
|
45
|
+
* <Card>Item 2</Card>
|
|
46
|
+
* </Stack>
|
|
47
|
+
*
|
|
48
|
+
* // Using gap prop (alias)
|
|
49
|
+
* <Stack gap="md">
|
|
50
|
+
* <Card>Item 1</Card>
|
|
51
|
+
* <Card>Item 2</Card>
|
|
52
|
+
* </Stack>
|
|
53
|
+
* ```
|
|
33
54
|
*/
|
|
34
|
-
export const Stack
|
|
55
|
+
export const Stack = forwardRef<HTMLDivElement, StackProps>(({
|
|
35
56
|
children,
|
|
36
57
|
direction = 'vertical',
|
|
37
|
-
spacing
|
|
58
|
+
spacing,
|
|
59
|
+
gap,
|
|
38
60
|
align = 'stretch',
|
|
39
61
|
justify = 'start',
|
|
40
62
|
wrap = false,
|
|
41
63
|
className = '',
|
|
42
|
-
|
|
64
|
+
...htmlProps
|
|
65
|
+
}, ref) => {
|
|
66
|
+
// Use gap as alias for spacing (spacing takes precedence if both provided)
|
|
67
|
+
const effectiveSpacing = spacing ?? gap ?? 'md';
|
|
68
|
+
|
|
43
69
|
const spacingClasses = {
|
|
44
70
|
vertical: {
|
|
45
71
|
none: '',
|
|
@@ -76,11 +102,13 @@ export const Stack: React.FC<StackProps> = ({
|
|
|
76
102
|
|
|
77
103
|
return (
|
|
78
104
|
<div
|
|
105
|
+
ref={ref}
|
|
106
|
+
{...htmlProps}
|
|
79
107
|
className={`
|
|
80
108
|
flex
|
|
81
109
|
${direction === 'vertical' ? 'flex-col' : 'flex-row'}
|
|
82
110
|
${wrap ? 'flex-wrap' : ''}
|
|
83
|
-
${spacingClasses[direction][
|
|
111
|
+
${spacingClasses[direction][effectiveSpacing]}
|
|
84
112
|
${alignClasses[align]}
|
|
85
113
|
${justifyClasses[justify]}
|
|
86
114
|
${className}
|
|
@@ -89,6 +117,8 @@ export const Stack: React.FC<StackProps> = ({
|
|
|
89
117
|
{children}
|
|
90
118
|
</div>
|
|
91
119
|
);
|
|
92
|
-
};
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
Stack.displayName = 'Stack';
|
|
93
123
|
|
|
94
124
|
export default Stack;
|
package/src/components/Tabs.tsx
CHANGED
|
@@ -1,152 +1,152 @@
|
|
|
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
|
-
}
|
|
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
|
+
}
|