diff --git a/src/components/UploadButton/UploadButton.tsx b/src/components/UploadButton/UploadButton.tsx
index 70994f98b..0823bf2ae 100644
--- a/src/components/UploadButton/UploadButton.tsx
+++ b/src/components/UploadButton/UploadButton.tsx
@@ -159,15 +159,15 @@ const FileUploadButton = ({
// Try parsing with full options first
workbook = window.XLSX.read(arrayBuffer, {
type: 'array',
- cellFormula: false, // Disable formula parsing to avoid potential issues
- cellNF: false, // Disable number format parsing
- cellHTML: false, // Disable HTML parsing
- cellText: true, // Force text output
- cellDates: false, // Disable date parsing to avoid errors
+ cellFormula: true,
+ cellNF: true,
+ cellHTML: true,
+ cellText: true,
+ cellDates: true,
error: (e: any) => {
console.warn('Non-fatal XLSX error:', e);
- }, // Log non-fatal errors
- cellStyles: false, // Disable style parsing
+ },
+ cellStyles: true,
});
} catch (initialError) {
console.warn(
@@ -179,15 +179,15 @@ const FileUploadButton = ({
try {
workbook = window.XLSX.read(arrayBuffer, {
type: 'array',
- sheetRows: 1000, // Limit number of rows to parse
- cellFormula: false,
- cellStyles: false,
- bookDeps: false, // Don't parse external dependencies
- bookFiles: false, // Don't parse embedded files
- bookProps: false, // Don't parse document properties
- bookSheets: false, // Don't parse sheet properties
- bookVBA: false, // Don't parse VBA
- WTF: true, // "What the Formula" mode - ignores errors when possible
+ sheetRows: 1000,
+ cellFormula: true,
+ cellStyles: true,
+ bookDeps: true,
+ bookFiles: true,
+ bookProps: true,
+ bookSheets: true,
+ bookVBA: true,
+ WTF: true,
});
} catch (recoveryError) {
setErrors(prev => [
@@ -247,27 +247,62 @@ const FileUploadButton = ({
);
}
- // Try to convert sheet to CSV
- let csv;
+ // Try to convert sheet to formatted text with columns
+ let formattedText;
try {
- csv = window.XLSX.utils.sheet_to_csv(worksheet);
- } catch (csvError) {
- // If CSV conversion fails, try a more basic cell-by-cell approach
- csv = '';
+ // Get array of arrays representation
+ const data = window.XLSX.utils.sheet_to_json(worksheet, {
+ header: 1,
+ raw: false,
+ });
+
+ // Find the maximum width for each column
+ const colWidths = data.reduce((widths: number[], row: any[]) => {
+ row.forEach((cell, i) => {
+ const cellWidth = (cell || '').toString().length;
+ widths[i] = Math.max(widths[i] || 0, cellWidth);
+ });
+ return widths;
+ }, []);
+
+ // Format each row with proper column spacing
+ formattedText = data.map((row: any[]) => {
+ return row
+ .map((cell, i) => {
+ const cellStr = (cell || '').toString();
+ return cellStr.padEnd(colWidths[i] + 2); // Add 2 spaces padding
+ })
+ .join('|')
+ .trim();
+ });
+
+ // Add separator line after header
+ if (formattedText.length > 0) {
+ const separator = colWidths
+ .map((w: number) => '-'.repeat(w + 2))
+ .join('+');
+ formattedText.splice(1, 0, separator);
+ }
+
+ formattedText = formattedText.join('\n');
+
+ } catch (formatError) {
+ // Fallback to basic formatting if advanced fails
+ formattedText = '';
for (let r = range.s.r; r <= Math.min(range.e.r, 1000); ++r) {
let row = '';
for (let c = range.s.c; c <= Math.min(range.e.c, 100); ++c) {
const cell =
worksheet[window.XLSX.utils.encode_cell({ r: r, c: c })];
- row += (cell ? String(cell.v || '') : '') + ',';
+ row += (cell ? String(cell.v || '').padEnd(15) : ' '.repeat(15)) + '|';
}
- csv += row + '\n';
+ formattedText += row + '\n';
}
- csv += '...(truncated due to potential corruption)';
+ formattedText += '...(truncated due to potential corruption)';
}
- // Add sheet name and content to final text
- text += `Sheet: ${sheetName}\n${csv}\n\n`;
+ // Add sheet name and formatted content to final text
+ text += `Sheet: ${sheetName}\n${formattedText}\n\n`;
successfulSheets++;
} catch (sheetError) {
// Log sheet-specific error but continue with other sheets
@@ -291,7 +326,7 @@ const FileUploadButton = ({
text;
}
- return text; // Return the extracted text from all sheets
+ return text;
} catch (error) {
setErrors(prev => [
...prev,
@@ -303,7 +338,6 @@ const FileUploadButton = ({
fileId: file.name,
},
]);
- // If any error occurs during processing, throw with descriptive message
throw new Error(
`XLSX extraction failed: ${
error instanceof Error ? error.message : 'Unknown error'
diff --git a/src/components/ui/Tabs.css b/src/components/ui/Tabs.css
new file mode 100644
index 000000000..227445d2f
--- /dev/null
+++ b/src/components/ui/Tabs.css
@@ -0,0 +1,264 @@
+.memori-tabs {
+ display: flex;
+ width: 100%;
+ flex-direction: column;
+}
+
+/* Tab list styles */
+.memori-tabs--list {
+ position: relative;
+ display: flex;
+ border-bottom: 1px solid #f0f0f0;
+ margin-bottom: 16px;
+}
+
+.memori-tabs--list-vertical {
+ width: max-content;
+ flex-direction: column;
+ border-right: 1px solid #f0f0f0;
+ border-bottom: none;
+ margin-right: 16px;
+ margin-bottom: 0;
+}
+
+/* Tab position variants */
+.memori-tabs--bottom {
+ flex-direction: column-reverse;
+}
+
+.memori-tabs--bottom .memori-tabs--list {
+ border-top: 1px solid #f0f0f0;
+ border-bottom: none;
+ margin-top: 16px;
+ margin-bottom: 0;
+}
+
+.memori-tabs--left {
+ flex-direction: row;
+}
+
+.memori-tabs--right {
+ flex-direction: row-reverse;
+}
+
+.memori-tabs--right .memori-tabs--list {
+ border-right: none;
+ border-left: 1px solid #f0f0f0;
+ margin-right: 0;
+ margin-left: 16px;
+}
+
+/* Centered tabs */
+.memori-tabs--centered .memori-tabs--list {
+ justify-content: center;
+}
+
+/* Individual tab styles */
+.memori-tabs--tab {
+ position: relative;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 12px 16px;
+ border: none;
+ background-color: transparent;
+ color: rgba(0, 0, 0, 0.65);
+ cursor: pointer;
+ font-size: 14px;
+ outline: none;
+ transition: color 0.3s;
+}
+
+.memori-tabs--list-vertical .memori-tabs--tab {
+ justify-content: flex-start;
+ padding: 8px 24px 8px 16px;
+}
+
+.memori-tabs--tab:hover {
+ color: #40a9ff;
+}
+
+.memori-tabs--tab:focus-visible {
+ box-shadow: 0 0 0 2px rgba(24, 144, 255, 0.2);
+}
+
+.memori-tabs--tab-selected {
+ color: #1890ff;
+ font-weight: 500;
+}
+
+.memori-tabs--tab-disabled {
+ color: rgba(0, 0, 0, 0.25);
+ cursor: not-allowed;
+}
+
+.memori-tabs--tab-disabled:hover {
+ color: rgba(0, 0, 0, 0.25);
+}
+
+/* Tab line indicator */
+.memori-tabs--tab-line {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ height: 2px;
+ background-color: transparent;
+ transition: all 0.3s;
+}
+
+.memori-tabs--tab-line-active {
+ background-color: #1890ff;
+}
+
+.memori-tabs--list-vertical .memori-tabs--tab-line {
+ top: 0;
+ right: 0;
+ bottom: auto;
+ width: 2px;
+ height: 100%;
+}
+
+/* Card style tabs */
+.memori-tabs--card .memori-tabs--list {
+ border-bottom: 1px solid #f0f0f0;
+}
+
+.memori-tabs--card .memori-tabs--tab {
+ border: 1px solid transparent;
+ border-radius: 4px 4px 0 0;
+ border-bottom: none;
+ margin-right: 2px;
+ background-color: #fafafa;
+}
+
+/* Editable card style */
+.memori-tabs--editable-card .memori-tabs--tab {
+ position: relative;
+ padding-right: 32px;
+}
+
+/* Size variants */
+.memori-tabs--small .memori-tabs--tab {
+ padding: 8px 12px;
+ font-size: 12px;
+}
+
+.memori-tabs--large .memori-tabs--tab {
+ padding: 16px 20px;
+ font-size: 16px;
+}
+
+.memori-tabs--small.memori-tabs--left .memori-tabs--tab,
+.memori-tabs--small.memori-tabs--right .memori-tabs--tab {
+ padding: 6px 16px 6px 12px;
+}
+
+.memori-tabs--large.memori-tabs--left .memori-tabs--tab,
+.memori-tabs--large.memori-tabs--right .memori-tabs--tab {
+ padding: 12px 32px 12px 20px;
+}
+
+/* Tab with icon */
+.memori-tabs--tab-with-icon {
+ display: inline-flex;
+ align-items: center;
+}
+
+.memori-tabs--tab-icon {
+ display: flex;
+ align-items: center;
+ margin-right: 8px;
+}
+
+/* Tab content panel */
+.memori-tabs--panels {
+ flex: 1;
+}
+
+.memori-tabs--panel {
+ outline: none;
+}
+
+.memori-tabs--card .memori-tabs--tab-selected {
+ border-color: #f0f0f0;
+ border-bottom: 1px solid #fff;
+ margin-bottom: -1px;
+ background-color: #fff;
+}
+
+.memori-tabs--card.memori-tabs--bottom .memori-tabs--tab {
+ border-radius: 0 0 4px 4px;
+ border-top: none;
+ border-bottom: 1px solid transparent;
+}
+
+.memori-tabs--card.memori-tabs--bottom .memori-tabs--tab-selected {
+ border-color: #f0f0f0;
+ border-top: 1px solid #fff;
+ margin-top: -1px;
+}
+
+.memori-tabs--card.memori-tabs--left .memori-tabs--tab {
+ border-radius: 4px 0 0 4px;
+ border-right: none;
+ margin-bottom: 2px;
+}
+
+.memori-tabs--card.memori-tabs--left .memori-tabs--tab-selected {
+ border-color: #f0f0f0;
+ border-right: 1px solid #fff;
+ margin-right: -1px;
+}
+
+.memori-tabs--card.memori-tabs--right .memori-tabs--tab {
+ border-radius: 0 4px 4px 0;
+ border-left: none;
+ margin-bottom: 2px;
+}
+
+.memori-tabs--card.memori-tabs--right .memori-tabs--tab-selected {
+ border-color: #f0f0f0;
+ border-left: 1px solid #fff;
+ margin-left: -1px;
+}
+
+.memori-tabs--editable-card .memori-tabs--tab-close {
+ position: absolute;
+ top: 50%;
+ right: 12px;
+ display: flex;
+ width: 16px;
+ height: 16px;
+ align-items: center;
+ justify-content: center;
+ border-radius: 50%;
+ color: rgba(0, 0, 0, 0.45);
+ font-size: 12px;
+ opacity: 0;
+ transform: translateY(-50%);
+ transition: opacity 0.3s;
+}
+
+.memori-tabs--editable-card .memori-tabs--tab-close:hover {
+ background-color: rgba(0, 0, 0, 0.1);
+ color: rgba(0, 0, 0, 0.65);
+}
+
+.memori-tabs--editable-card .memori-tabs--tab:hover .memori-tabs--tab-close {
+ opacity: 1;
+}
+
+/* Animation */
+.memori-tabs--panel-animated {
+ animation: fadeIn 0.3s;
+}
+
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
diff --git a/src/components/ui/Tabs.stories.tsx b/src/components/ui/Tabs.stories.tsx
new file mode 100644
index 000000000..a19573d54
--- /dev/null
+++ b/src/components/ui/Tabs.stories.tsx
@@ -0,0 +1,389 @@
+import React, { useState } from 'react';
+import { Meta, Story } from '@storybook/react';
+import Tabs, { Props, TabItem } from './Tabs';
+import './Tabs.css';
+
+// Mock icons
+const HomeIcon = () => (
+
+);
+
+const UserIcon = () => (
+
+);
+
+const SettingsIcon = () => (
+
+);
+
+const defaultItems: TabItem[] = [
+ {
+ key: 'tab1',
+ label: 'Tab 1',
+ children:
Content of Tab 1
,
+ },
+ {
+ key: 'tab2',
+ label: 'Tab 2',
+ children: Content of Tab 2
,
+ },
+ {
+ key: 'tab3',
+ label: 'Tab 3',
+ children: Content of Tab 3
,
+ },
+];
+
+const meta: Meta = {
+ title: 'UI/Tabs',
+ component: Tabs,
+ argTypes: {
+ activeKey: {
+ control: {
+ type: 'text',
+ },
+ },
+ defaultActiveKey: {
+ control: {
+ type: 'text',
+ },
+ },
+ tabPosition: {
+ control: {
+ type: 'select',
+ options: ['top', 'bottom', 'left', 'right'],
+ },
+ },
+ centered: {
+ control: {
+ type: 'boolean',
+ },
+ },
+ size: {
+ control: {
+ type: 'select',
+ options: ['small', 'default', 'large'],
+ },
+ },
+ type: {
+ control: {
+ type: 'select',
+ options: ['line', 'card', 'editable-card'],
+ },
+ },
+ animated: {
+ control: {
+ type: 'boolean',
+ },
+ },
+ className: {
+ control: {
+ type: 'text',
+ },
+ },
+ },
+ parameters: {
+ controls: { expanded: true },
+ },
+};
+
+export default meta;
+
+const Template: Story = args => {
+ const [activeKey, setActiveKey] = useState(args.defaultActiveKey || 'tab1');
+
+ const handleChange = (key: string) => {
+ setActiveKey(key);
+ if (args.onChange) {
+ args.onChange(key);
+ }
+ };
+
+ return ;
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ 'data-testid': 'default-tabs',
+};
+
+export const WithIcons = Template.bind({});
+WithIcons.args = {
+ items: [
+ {
+ key: 'home',
+ label: 'Home',
+ children: Home content
,
+ icon: ,
+ },
+ {
+ key: 'profile',
+ label: 'Profile',
+ children: Profile content
,
+ icon: ,
+ },
+ {
+ key: 'settings',
+ label: 'Settings',
+ children: Settings content
,
+ icon: ,
+ },
+ ],
+ defaultActiveKey: 'home',
+ 'data-testid': 'icon-tabs',
+};
+
+export const WithDisabledTab = Template.bind({});
+WithDisabledTab.args = {
+ items: [
+ {
+ key: 'tab1',
+ label: 'Tab 1',
+ children: Content of Tab 1
,
+ },
+ {
+ key: 'tab2',
+ label: 'Tab 2 (Disabled)',
+ children: Content of Tab 2
,
+ disabled: true,
+ },
+ {
+ key: 'tab3',
+ label: 'Tab 3',
+ children: Content of Tab 3
,
+ },
+ ],
+ defaultActiveKey: 'tab1',
+ 'data-testid': 'disabled-tabs',
+};
+
+export const SmallSize = Template.bind({});
+SmallSize.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ size: 'small',
+ 'data-testid': 'small-tabs',
+};
+
+export const LargeSize = Template.bind({});
+LargeSize.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ size: 'large',
+ 'data-testid': 'large-tabs',
+};
+
+export const CardStyle = Template.bind({});
+CardStyle.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ type: 'card',
+ 'data-testid': 'card-tabs',
+};
+
+export const CenteredTabs = Template.bind({});
+CenteredTabs.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ centered: true,
+ 'data-testid': 'centered-tabs',
+};
+
+export const BottomPosition = Template.bind({});
+BottomPosition.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ tabPosition: 'bottom',
+ 'data-testid': 'bottom-tabs',
+};
+
+export const LeftPosition = Template.bind({});
+LeftPosition.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ tabPosition: 'left',
+ 'data-testid': 'left-tabs',
+};
+
+export const RightPosition = Template.bind({});
+RightPosition.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ tabPosition: 'right',
+ 'data-testid': 'right-tabs',
+};
+
+export const WithoutAnimation = Template.bind({});
+WithoutAnimation.args = {
+ items: defaultItems,
+ defaultActiveKey: 'tab1',
+ animated: false,
+ 'data-testid': 'no-animation-tabs',
+};
+
+// Interactive example with form fields in tabs
+export const InteractiveFormTabs: Story = () => {
+ const [formData, setFormData] = useState({
+ name: '',
+ email: '',
+ address: '',
+ preferences: {
+ notifications: true,
+ darkMode: false
+ }
+ });
+
+ const handleInputChange = (field: string) => (e: React.ChangeEvent) => {
+ setFormData({
+ ...formData,
+ [field]: e.target.value
+ });
+ };
+
+ const handleCheckboxChange = (field: string) => (e: React.ChangeEvent) => {
+ setFormData({
+ ...formData,
+ preferences: {
+ ...formData.preferences,
+ [field]: e.target.checked
+ }
+ });
+ };
+
+ return (
+ ,
+ children: (
+
+ ),
+ },
+ {
+ key: 'address',
+ label: 'Address',
+ icon: ,
+ children: (
+
+ ),
+ },
+ {
+ key: 'settings',
+ label: 'Settings',
+ icon: ,
+ children: (
+
+ ),
+ },
+ ]}
+ defaultActiveKey="personal"
+ data-testid="form-tabs"
+ type="card"
+ />
+ );
+};
+
+// Add some basic form styling for the interactive example
+InteractiveFormTabs.decorators = [
+ (Story) => (
+
+
+
+
+ ),
+];
\ No newline at end of file
diff --git a/src/components/ui/Tabs.test.tsx b/src/components/ui/Tabs.test.tsx
new file mode 100644
index 000000000..6f877042b
--- /dev/null
+++ b/src/components/ui/Tabs.test.tsx
@@ -0,0 +1,303 @@
+import React from 'react';
+import { render, screen, fireEvent } from '@testing-library/react';
+import Tabs from './Tabs';
+
+const defaultItems = [
+ {
+ key: 'tab1',
+ label: 'Tab 1',
+ children: Content of Tab 1
,
+ },
+ {
+ key: 'tab2',
+ label: 'Tab 2',
+ children: Content of Tab 2
,
+ },
+ {
+ key: 'tab3',
+ label: 'Tab 3',
+ children: Content of Tab 3
,
+ },
+];
+
+describe('Tabs Component', () => {
+ it('renders Tabs unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with defaultActiveKey unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with activeKey unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with disabled tab unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with tab position bottom unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with tab position left unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with tab position right unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs centered unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with size small unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with size large unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with type card unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs with type editable-card unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('renders Tabs without animation unchanged', () => {
+ const { container } = render(
+
+ );
+ expect(container).toMatchSnapshot();
+ });
+
+ it('calls onChange when tab is clicked', () => {
+ const handleChange = jest.fn();
+ render(
+
+ );
+
+ // Click on the second tab
+ const tab2 = screen.getByTestId('test-tabs-tab-tab2');
+ fireEvent.click(tab2);
+
+ expect(handleChange).toHaveBeenCalledWith('tab2');
+ });
+
+ it('does not call onChange when disabled tab is clicked', () => {
+ const handleChange = jest.fn();
+ render(
+
+ );
+
+ // Click on the disabled tab
+ const disabledTab = screen.getByTestId('test-tabs-tab-tab2');
+ fireEvent.click(disabledTab);
+
+ expect(handleChange).not.toHaveBeenCalled();
+ });
+
+ it('displays the content of the selected tab', () => {
+ render(
+
+ );
+
+ // Check if content of tab2 is visible
+ const content = screen.getByText('Content of Tab 2');
+ expect(content).toBeTruthy();
+ });
+
+ it('changes content when a new tab is selected', () => {
+ render(
+
+ );
+
+ // Initially tab1 content should be visible
+ expect(screen.getByText('Content of Tab 1')).toBeTruthy();
+
+ // Click on tab3
+ const tab3 = screen.getByTestId('test-tabs-tab-tab3');
+ fireEvent.click(tab3);
+
+ // Now tab3 content should be visible
+ expect(screen.getByText('Content of Tab 3')).toBeTruthy();
+ });
+
+ it('updates when activeKey prop changes', () => {
+ const { rerender } = render(
+
+ );
+
+ // Initially tab1 content should be visible
+ expect(screen.getByText('Content of Tab 1')).toBeTruthy();
+
+ // Update activeKey prop
+ rerender(
+
+ );
+
+ // Now tab3 content should be visible
+ expect(screen.getByText('Content of Tab 3')).toBeTruthy();
+ });
+
+ it('renders tabs with icons', () => {
+ const { container } = render(
+ Content of Tab 1,
+ icon: Icon
+ },
+ ...defaultItems.slice(1)
+ ]}
+ data-testid="test-tabs"
+ onChange={jest.fn()}
+ />
+ );
+
+ expect(screen.getByTestId('tab-icon')).toBeTruthy();
+ expect(container).toMatchSnapshot();
+ });
+});
\ No newline at end of file
diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx
new file mode 100644
index 000000000..c2a959983
--- /dev/null
+++ b/src/components/ui/Tabs.tsx
@@ -0,0 +1,149 @@
+import React, { useState, useEffect, Fragment } from 'react';
+import { Tab } from '@headlessui/react';
+import cx from 'classnames';
+
+export interface TabItem {
+ key: string;
+ label: React.ReactNode;
+ children: React.ReactNode;
+ disabled?: boolean;
+ icon?: React.ReactNode;
+}
+
+export interface Props {
+ className?: string;
+ defaultActiveKey?: string;
+ activeKey?: string;
+ onChange?: (activeKey: string) => void;
+ items: TabItem[];
+ tabPosition?: 'top' | 'bottom' | 'left' | 'right';
+ centered?: boolean;
+ size?: 'small' | 'default' | 'large';
+ type?: 'line' | 'card' | 'editable-card';
+ animated?: boolean;
+ 'data-testid'?: string;
+}
+
+const Tabs = ({
+ className,
+ defaultActiveKey,
+ activeKey,
+ onChange,
+ items,
+ tabPosition = 'top',
+ centered = false,
+ size = 'default',
+ type = 'line',
+ animated = true,
+ 'data-testid': testId,
+}: Props) => {
+ // Find the initial selected index based on defaultActiveKey or activeKey
+ const getInitialIndex = (): number => {
+ if (activeKey !== undefined) {
+ const index = items.findIndex(item => item.key === activeKey);
+ return index >= 0 ? index : 0;
+ }
+ if (defaultActiveKey !== undefined) {
+ const index = items.findIndex(item => item.key === defaultActiveKey);
+ return index >= 0 ? index : 0;
+ }
+ return 0;
+ };
+
+ const [selectedIndex, setSelectedIndex] = useState(getInitialIndex());
+
+ // Update selectedIndex when activeKey changes (for controlled component)
+ useEffect(() => {
+ if (activeKey !== undefined) {
+ const index = items.findIndex(item => item.key === activeKey);
+ if (index >= 0) {
+ setSelectedIndex(index);
+ }
+ }
+ }, [activeKey, items]);
+
+ const handleChange = (index: number) => {
+ if (!items[index]?.disabled) {
+ setSelectedIndex(index);
+
+ if (onChange && items[index]) {
+ onChange(items[index].key);
+ }
+ }
+ };
+
+ const isVertical = tabPosition === 'left' || tabPosition === 'right';
+
+ return (
+
+
+
+ {items.map((item) => (
+ cx(
+ 'memori-tabs--tab',
+ {
+ 'memori-tabs--tab-selected': selected,
+ 'memori-tabs--tab-disabled': item.disabled,
+ 'memori-tabs--tab-with-icon': !!item.icon
+ }
+ )}
+ disabled={item.disabled}
+ data-testid={`${testId}-tab-${item.key}`}
+ >
+ {({ selected }) => (
+
+ {item.icon && (
+ {item.icon}
+ )}
+ {item.label}
+ {type === 'line' && (
+
+ )}
+
+ )}
+
+ ))}
+
+
+
+ {items.map((item) => (
+
+ {item.children}
+
+ ))}
+
+
+
+ );
+};
+
+export default Tabs;
\ No newline at end of file
diff --git a/src/components/ui/__snapshots__/Tabs.test.tsx.snap b/src/components/ui/__snapshots__/Tabs.test.tsx.snap
new file mode 100644
index 000000000..ab88cde4c
--- /dev/null
+++ b/src/components/ui/__snapshots__/Tabs.test.tsx.snap
@@ -0,0 +1,1491 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Tabs Component renders Tabs centered unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with activeKey unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with defaultActiveKey unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with disabled tab unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with size large unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with size small unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with tab position bottom unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with tab position left unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with tab position right unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with type card unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs with type editable-card unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders Tabs without animation unchanged 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
+
+exports[`Tabs Component renders tabs with icons 1`] = `
+
+
+
+
+
+
+
+
+
+
+`;
diff --git a/src/styles.css b/src/styles.css
index 2fc0dc73d..15509e886 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -12,6 +12,7 @@
@import url('./components/ui/Expandable.css');
@import url('./components/ui/Slider.css');
@import url('./components/ui/Alert.css');
+@import url('./components/ui/Tabs.css');
@import url('./components/CompletionProviderStatus/CompletionProviderStatus.css');
@import url('./components/PoweredBy/PoweredBy.css');