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: ( +
+
+ +