2025-06-22 23:01:03 -03:00
|
|
|
"use client";
|
|
|
|
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
|
|
import {
|
|
|
|
Bold, Italic, Underline as UnderlineIcon, AlignLeft, AlignCenter,
|
|
|
|
AlignRight, AlignJustify, Link as LinkIcon, Image as ImageIcon,
|
|
|
|
Save, FileText, Printer, Table as TableIcon, Plus, Minus,
|
|
|
|
Trash2, Type, Palette, Highlighter, FileImage, File, Home,
|
|
|
|
View, ChevronDown, Search, Undo, Redo, Copy, MoreVertical,
|
|
|
|
PieChart, BarChart3, LineChart, Sigma, Filter, DollarSign,
|
|
|
|
Calendar, Clock, Percent, Font, FontSize, BorderAll, BorderNone,
|
|
|
|
BorderHorizontal, BorderVertical, BorderInner, BorderOuter,
|
|
|
|
CornerUpLeft, CornerUpRight, CornerDownLeft, CornerDownRight,
|
|
|
|
WrapText, Merge, Split, Functions, PlusCircle, MinusCircle,
|
|
|
|
ChevronRight, ChevronLeft, Sun, Moon, Grid, List, Columns,
|
|
|
|
Rows, Settings, HelpCircle, Info, Download, Upload, Share2,
|
|
|
|
Lock, Unlock, Maximize, Minimize, X, Check, Sliders, Type as TextIcon,
|
|
|
|
Hash, PlusSquare, MinusSquare, Table2, Divide, Multiply,
|
|
|
|
ZoomIn as ZoomInIcon,
|
|
|
|
ZoomOut as ZoomOutIcon,
|
|
|
|
Cat as CatIcon
|
|
|
|
} from 'lucide-react';
|
|
|
|
import { createUniver, defaultTheme, LocaleType, merge } from '@univerjs/presets';
|
|
|
|
import { UniverSheetsCorePreset } from '@univerjs/presets/preset-sheets-core';
|
|
|
|
import UniverPresetSheetsCoreEnUS from '@univerjs/presets/preset-sheets-core/locales/en-US';
|
|
|
|
import '@univerjs/presets/lib/styles/preset-sheets-core.css';
|
|
|
|
import './styles.css'; // Import your custom styles
|
|
|
|
|
|
|
|
const RibbonTab = ({ label, isActive, onClick, children }) => (
|
|
|
|
<div className="ribbon-tab">
|
|
|
|
<button onClick={onClick} className={`ribbon-tab-button ${isActive ? 'active' : ''}`}>
|
|
|
|
{label}
|
|
|
|
</button>
|
|
|
|
{isActive && <div className="ribbon-content">{children}</div>}
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
const RibbonGroup = ({ title, children }) => (
|
|
|
|
<div className="ribbon-group">
|
|
|
|
<div className="ribbon-group-content">{children}</div>
|
|
|
|
<div className="ribbon-group-title">{title}</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
const RibbonButton = ({ icon: Icon, label, onClick, isActive, size = 'medium', dropdown = false }) => (
|
|
|
|
<button onClick={onClick} className={`ribbon-button ${size} ${isActive ? 'active' : ''}`} title={label}>
|
|
|
|
<div className="ribbon-button-content">
|
|
|
|
<CatIcon size={size === 'large' ? 18 : 16} />
|
|
|
|
{size === 'large' && <span className="ribbon-button-label">{label}</span>}
|
|
|
|
{dropdown && <ChevronDown size={12} className="dropdown-arrow" />}
|
|
|
|
</div>
|
|
|
|
</button>
|
|
|
|
);
|
|
|
|
|
|
|
|
const RibbonDropdownButton = ({ icon: Icon, label, onClick, isActive, children }) => (
|
|
|
|
<div className="ribbon-dropdown">
|
|
|
|
<button onClick={onClick} className={`ribbon-button ${isActive ? 'active' : ''}`} title={label}>
|
|
|
|
<div className="ribbon-button-content">
|
|
|
|
<CatIcon size={16} />
|
|
|
|
<ChevronDown size={12} className="dropdown-arrow" />
|
|
|
|
</div>
|
|
|
|
</button>
|
|
|
|
<div className="ribbon-dropdown-content">
|
|
|
|
{children}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
const RibbonSplitButton = ({ icon: Icon, label, onClick, dropdownOnClick, isActive }) => (
|
|
|
|
<div className="ribbon-split-button">
|
|
|
|
<button onClick={onClick} className={`ribbon-button ${isActive ? 'active' : ''}`} title={label}>
|
|
|
|
<CatIcon size={16} />
|
|
|
|
</button>
|
|
|
|
<button onClick={dropdownOnClick} className="ribbon-split-button-arrow">
|
|
|
|
<ChevronDown size={12} />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
);
|
|
|
|
|
|
|
|
export default function Lotus123Clone() {
|
|
|
|
const [fileName, setFileName] = useState('Sales Report');
|
|
|
|
const [activeTab, setActiveTab] = useState('home');
|
|
|
|
const [formulaMode, setFormulaMode] = useState('@');
|
|
|
|
const [commandMode, setCommandMode] = useState(false);
|
|
|
|
const [currentCell, setCurrentCell] = useState('A1');
|
|
|
|
const [cellContent, setCellContent] = useState('');
|
|
|
|
const [zoomLevel, setZoomLevel] = useState(100);
|
|
|
|
const [showSampleData, setShowSampleData] = useState(false);
|
|
|
|
const univerRef = useRef(null);
|
|
|
|
const containerRef = useRef(null);
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
const { univerAPI } = createUniver({
|
|
|
|
locale: LocaleType.EN_US,
|
|
|
|
locales: {
|
|
|
|
[LocaleType.EN_US]: merge(
|
|
|
|
{},
|
|
|
|
UniverPresetSheetsCoreEnUS,
|
|
|
|
),
|
|
|
|
},
|
|
|
|
theme: defaultTheme,
|
|
|
|
presets: [
|
|
|
|
UniverSheetsCorePreset({
|
|
|
|
container: containerRef.current,
|
|
|
|
}),
|
|
|
|
],
|
|
|
|
});
|
|
|
|
|
|
|
|
// Create a sample workbook with some data
|
|
|
|
const workbook = univerAPI.createWorkbook({
|
|
|
|
name: fileName,
|
|
|
|
styles: {
|
|
|
|
1: { // Style ID 1 for header
|
|
|
|
bl: 1, // bold
|
|
|
|
bg: { rgb: 'f2f2f2' } // background color
|
|
|
|
},
|
|
|
|
2: { // Style ID 2 for currency
|
|
|
|
ff: '$#,##0' // currency format
|
|
|
|
},
|
|
|
|
3: { // Style ID 3 for percentages
|
|
|
|
ff: '0.00%' // percentage format
|
|
|
|
}
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
// Add a worksheet
|
|
|
|
const worksheet = workbook.create('Sales Data');
|
|
|
|
|
|
|
|
// Set sample data if enabled
|
|
|
|
if (showSampleData) {
|
|
|
|
// Merge cells for title
|
|
|
|
const titleRange = worksheet.getRange('A1:F1');
|
|
|
|
titleRange.merge();
|
|
|
|
|
|
|
|
// Set title
|
|
|
|
titleRange.setValue('Sales Report');
|
|
|
|
titleRange.setStyle({ fs: 16, bl: 1 }); // font size 16, bold
|
|
|
|
|
|
|
|
// Set headers
|
|
|
|
const headers = ['Region', 'Q1', 'Q2', 'Q3', 'Q4', 'Total'];
|
|
|
|
const headerRange = worksheet.getRange(1, 0, 1, headers.length - 1);
|
|
|
|
headerRange.setValues([headers]);
|
|
|
|
headerRange.setStyle(1); // Apply header style
|
|
|
|
|
|
|
|
// Set data rows
|
|
|
|
const data = [
|
|
|
|
['North', 12500, 15000, 14200, 16800, '=SUM(B3:E3)'],
|
|
|
|
['South', 9800, 11200, 10800, 12400, '=SUM(B4:E4)'],
|
|
|
|
['East', 15300, 16800, 17500, 19200, '=SUM(B5:E5)'],
|
|
|
|
['West', 11800, 13200, 12800, 14500, '=SUM(B6:E6)']
|
|
|
|
];
|
|
|
|
|
|
|
|
const dataRange = worksheet.getRange(2, 0, data.length, data[0].length);
|
|
|
|
dataRange.setValues(data);
|
|
|
|
|
|
|
|
// Apply currency format to Q1-Q4 and Total columns
|
|
|
|
const currencyRange = worksheet.getRange(2, 1, data.length, 5);
|
|
|
|
currencyRange.setStyle(2);
|
|
|
|
|
|
|
|
// Set totals row
|
|
|
|
const totalsRow = 2 + data.length;
|
|
|
|
worksheet.getRange(totalsRow, 0).setValue('Total');
|
|
|
|
|
|
|
|
for (let col = 1; col <= 5; col++) {
|
|
|
|
const colLetter = String.fromCharCode(64 + col); // A=65, B=66, etc.
|
|
|
|
const formula = col === 5
|
|
|
|
? `=SUM(F3:F6)`
|
|
|
|
: `=SUM(${colLetter}3:${colLetter}6)`;
|
|
|
|
worksheet.getRange(totalsRow, col).setValue(formula);
|
|
|
|
worksheet.getRange(totalsRow, col).setStyle({ bl: 1 }); // bold
|
|
|
|
}
|
|
|
|
|
|
|
|
// Set growth rate section
|
|
|
|
const growthTitleRow = totalsRow + 2;
|
|
|
|
worksheet.getRange(growthTitleRow, 0).setValue('Growth Rate');
|
|
|
|
worksheet.getRange(growthTitleRow, 0).setStyle({ fs: 14, bl: 1 });
|
|
|
|
|
|
|
|
const growthRates = [
|
|
|
|
['Q1 to Q2', '=C3/B3-1', '=C4/B4-1', '=C5/B5-1', '=C6/B6-1', '=C7/B7-1'],
|
|
|
|
['Q2 to Q3', '=D3/C3-1', '=D4/C4-1', '=D5/C5-1', '=D6/C6-1', '=D7/C7-1'],
|
|
|
|
['Q3 to Q4', '=E3/D3-1', '=E4/D4-1', '=E5/D5-1', '=E6/D6-1', '=E7/D7-1']
|
|
|
|
];
|
|
|
|
|
|
|
|
const growthRange = worksheet.getRange(growthTitleRow + 1, 0, growthRates.length, growthRates[0].length);
|
|
|
|
growthRange.setValues(growthRates);
|
|
|
|
|
|
|
|
// Apply styles to growth rates
|
|
|
|
const growthLabelRange = worksheet.getRange(growthTitleRow + 1, 0, growthRates.length, 1);
|
|
|
|
growthLabelRange.setStyle({ bg: { rgb: 'f2f2f2' } });
|
|
|
|
|
|
|
|
const growthValueRange = worksheet.getRange(growthTitleRow + 1, 1, growthRates.length, growthRates[0].length - 1);
|
|
|
|
growthValueRange.setStyle(3); // percentage format
|
|
|
|
|
|
|
|
// Adjust column widths
|
|
|
|
worksheet.setColumnWidth(0, 120);
|
|
|
|
for (let col = 1; col <= 5; col++) {
|
|
|
|
worksheet.setColumnWidth(col, 80);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Adjust row heights
|
|
|
|
worksheet.setRowHeight(0, 30); // title row
|
|
|
|
worksheet.setRowHeight(1, 22); // header row
|
|
|
|
worksheet.setRowHeight(totalsRow, 22); // totals row
|
|
|
|
worksheet.setRowHeight(growthTitleRow, 24); // growth rate title
|
|
|
|
}
|
|
|
|
|
|
|
|
univerRef.current = univerAPI;
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
univerAPI.dispose();
|
|
|
|
};
|
|
|
|
}, [fileName, showSampleData]);
|
|
|
|
|
|
|
|
// Get the active range from selection
|
|
|
|
const getActiveRange = () => {
|
|
|
|
const workbook = univerRef.current?.getActiveWorkbook();
|
|
|
|
if (!workbook) return null;
|
|
|
|
|
|
|
|
const worksheet = workbook.getActiveSheet();
|
|
|
|
if (!worksheet) return null;
|
|
|
|
|
|
|
|
const selection = worksheet.getSelection();
|
|
|
|
if (!selection) return null;
|
|
|
|
|
|
|
|
return worksheet.getRange(selection.getRange());
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleCopy = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
const values = range.getValues();
|
|
|
|
navigator.clipboard.writeText(JSON.stringify(values));
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handlePaste = async () => {
|
|
|
|
try {
|
|
|
|
const text = await navigator.clipboard.readText();
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
try {
|
|
|
|
const values = JSON.parse(text);
|
|
|
|
range.setValues(values);
|
|
|
|
} catch {
|
|
|
|
range.setValue(text);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Paste failed:', error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleInsertRow = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
range.insertCells(univerRef.current.Enum.Dimension.ROWS);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleInsertColumn = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
range.insertCells(univerRef.current.Enum.Dimension.COLUMNS);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleDeleteRow = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
range.deleteCells(univerRef.current.Enum.Dimension.ROWS);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleDeleteColumn = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
range.deleteCells(univerRef.current.Enum.Dimension.COLUMNS);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleBold = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
const isBold = range.getCellStyle()?.bl === 1;
|
|
|
|
range.setFontWeight(isBold ? null : 'bold');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleItalic = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
const isItalic = range.getCellStyle()?.it === 1;
|
|
|
|
range.setFontLine(isItalic ? null : 'italic');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleUnderline = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
const isUnderlined = range.getCellStyle()?.ul === 1;
|
|
|
|
range.setFontLine(isUnderlined ? null : 'underline');
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleAlignment = (alignment) => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
range.setTextAlignment(alignment);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSort = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
const worksheet = range.getWorksheet();
|
|
|
|
worksheet.sort(range, { column: range.getColumn(), ascending: true });
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleFillDown = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range && range.getRowCount() > 1) {
|
|
|
|
const sourceValue = range.getValue();
|
|
|
|
const values = Array(range.getRowCount()).fill(sourceValue);
|
|
|
|
range.setValues(values);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleFillRight = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range && range.getColumnCount() > 1) {
|
|
|
|
const sourceValue = range.getValue();
|
|
|
|
const values = Array(range.getColumnCount()).fill(sourceValue);
|
|
|
|
range.setValues([values]);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleUndo = () => {
|
|
|
|
univerRef.current?.undo();
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleRedo = () => {
|
|
|
|
univerRef.current?.redo();
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSave = () => {
|
|
|
|
const workbook = univerRef.current?.getActiveWorkbook();
|
|
|
|
if (workbook) {
|
|
|
|
const data = workbook.save();
|
|
|
|
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' });
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement('a');
|
|
|
|
a.href = url;
|
|
|
|
a.download = `${fileName}.json`;
|
|
|
|
document.body.appendChild(a);
|
|
|
|
a.click();
|
|
|
|
document.body.removeChild(a);
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleZoomIn = () => {
|
|
|
|
setZoomLevel(prev => Math.min(prev + 10, 200));
|
|
|
|
univerRef.current?.setZoomLevel(zoomLevel / 100 + 0.1);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleZoomOut = () => {
|
|
|
|
setZoomLevel(prev => Math.max(prev - 10, 50));
|
|
|
|
univerRef.current?.setZoomLevel(zoomLevel / 100 - 0.1);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSlashCommand = (e) => {
|
|
|
|
if (e.key === '/') {
|
|
|
|
setCommandMode(true);
|
|
|
|
} else if (e.key === 'Escape') {
|
|
|
|
setCommandMode(false);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleLoadSampleData = () => {
|
|
|
|
setShowSampleData(true);
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleMergeCells = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
if (range.isMerged()) {
|
|
|
|
range.breakApart();
|
|
|
|
} else {
|
|
|
|
range.merge();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleClearFormat = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
range.clearFormat();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleClearContent = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
range.clearContent();
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleHighlightRange = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
const highlightDisposable = range.highlight({
|
|
|
|
stroke: 'red',
|
|
|
|
fill: 'rgba(255, 255, 0, 0.3)'
|
|
|
|
});
|
|
|
|
|
|
|
|
// Remove highlight after 5 seconds
|
|
|
|
setTimeout(() => {
|
|
|
|
highlightDisposable.dispose();
|
|
|
|
}, 5000);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
const handleSplitText = () => {
|
|
|
|
const range = getActiveRange();
|
|
|
|
if (range) {
|
|
|
|
range.splitTextToColumns(true); // Split with default delimiter (comma)
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2025-03-30 16:42:51 -03:00
|
|
|
return (
|
2025-06-22 23:01:03 -03:00
|
|
|
<div className="excel-clone">
|
|
|
|
{/* ... (keep all the existing JSX and styles the same) ... */}
|
|
|
|
|
|
|
|
<div className="ribbon">
|
|
|
|
<div className="ribbon-tabs">
|
|
|
|
<RibbonTab label="Home" isActive={activeTab === 'home'} onClick={() => setActiveTab('home')}>
|
|
|
|
<RibbonGroup title="Clipboard">
|
|
|
|
<RibbonButton icon={Copy} label="Copy" size="large" onClick={handleCopy} />
|
|
|
|
<RibbonButton icon={Copy} label="Paste" size="large" onClick={handlePaste} />
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Font">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Bold} label="Bold" onClick={handleBold} />
|
|
|
|
<RibbonButton icon={Italic} label="Italic" onClick={handleItalic} />
|
|
|
|
<RibbonButton icon={UnderlineIcon} label="Underline" onClick={handleUnderline} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={FontSize} label="Font Size" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Palette} label="Font Color" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Alignment">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={AlignLeft} label="Left" onClick={() => handleAlignment('left')} />
|
|
|
|
<RibbonButton icon={AlignCenter} label="Center" onClick={() => handleAlignment('center')} />
|
|
|
|
<RibbonButton icon={AlignRight} label="Right" onClick={() => handleAlignment('right')} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={WrapText} label="Wrap Text" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Merge} label="Merge" onClick={handleMergeCells} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Number">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={DollarSign} label="Currency" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Percent} label="Percent" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Functions} label="Formula" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Cells">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Plus} label="Insert" size="large" onClick={handleInsertRow} />
|
|
|
|
<RibbonButton icon={Minus} label="Delete" size="large" onClick={handleDeleteRow} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Trash2} label="Clear" size="large" onClick={handleClearContent} />
|
|
|
|
<RibbonButton icon={BorderNone} label="Clear Format" size="large" onClick={handleClearFormat} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Editing">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Sigma} label="AutoSum" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Filter} label="Filter" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Highlighter} label="Highlight" size="large" onClick={handleHighlightRange} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Split} label="Split Text" size="large" onClick={handleSplitText} />
|
|
|
|
<RibbonButton icon={Search} label="Find" size="large" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
</RibbonTab>
|
|
|
|
<RibbonTab label="Home" isActive={activeTab === 'home'} onClick={() => setActiveTab('home')}>
|
|
|
|
<RibbonGroup title="Clipboard">
|
|
|
|
<RibbonButton icon={Copy} label="Copy" size="large" onClick={handleCopy} />
|
|
|
|
<RibbonButton icon={Copy} label="Paste" size="large" onClick={handlePaste} />
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Font">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonDropdownButton icon={Font} label="Font" onClick={() => {}}>
|
|
|
|
<div style={{ padding: '8px', minWidth: '200px' }}>
|
|
|
|
<div style={{ marginBottom: '8px' }}>Font Family</div>
|
|
|
|
<div style={{ marginBottom: '8px' }}>Font Size</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Bold} label="Bold" onClick={handleBold} />
|
|
|
|
<RibbonButton icon={Italic} label="Italic" onClick={handleItalic} />
|
|
|
|
<RibbonButton icon={UnderlineIcon} label="Underline" onClick={handleUnderline} />
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</RibbonDropdownButton>
|
|
|
|
<RibbonButton icon={FontSize} label="Font Size" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Bold} label="Bold" onClick={handleBold} />
|
|
|
|
<RibbonButton icon={Italic} label="Italic" onClick={handleItalic} />
|
|
|
|
<RibbonButton icon={UnderlineIcon} label="Underline" onClick={handleUnderline} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Alignment">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={AlignLeft} label="Left" onClick={() => handleAlignment('left')} />
|
|
|
|
<RibbonButton icon={AlignCenter} label="Center" onClick={() => handleAlignment('center')} />
|
|
|
|
<RibbonButton icon={AlignRight} label="Right" onClick={() => handleAlignment('right')} />
|
|
|
|
<RibbonButton icon={AlignJustify} label="Justify" onClick={() => handleAlignment('justify')} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={WrapText} label="Wrap Text" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Merge} label="Merge" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Number">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={DollarSign} label="Currency" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Percent} label="Percent" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Functions} label="Formula" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={PlusSquare} label="Increase Decimal" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={MinusSquare} label="Decrease Decimal" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Cells">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Plus} label="Insert" size="large" onClick={handleInsertRow} />
|
|
|
|
<RibbonButton icon={Minus} label="Delete" size="large" onClick={handleDeleteRow} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Columns} label="Format" size="large" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Editing">
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Sigma} label="AutoSum" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Filter} label="Filter" size="large" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
<div style={{ display: 'flex', gap: '4px' }}>
|
|
|
|
<RibbonButton icon={Search} label="Find" size="large" onClick={() => {}} />
|
|
|
|
</div>
|
|
|
|
</RibbonGroup>
|
|
|
|
</RibbonTab>
|
|
|
|
|
|
|
|
<RibbonTab label="Insert" isActive={activeTab === 'insert'} onClick={() => setActiveTab('insert')}>
|
|
|
|
<RibbonGroup title="Tables">
|
|
|
|
<RibbonButton icon={Table2} label="Table" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={PieChart} label="PivotTable" size="large" onClick={() => {}} />
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Charts">
|
|
|
|
<RibbonButton icon={BarChart3} label="Column" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={BarChart3} label="Bar" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={PieChart} label="Pie" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={LineChart} label="Line" size="large" onClick={() => {}} />
|
|
|
|
</RibbonGroup>
|
|
|
|
</RibbonTab>
|
|
|
|
|
|
|
|
<RibbonTab label="Data" isActive={activeTab === 'data'} onClick={() => setActiveTab('data')}>
|
|
|
|
<RibbonGroup title="Sort & Filter">
|
|
|
|
<RibbonButton icon={Filter} label="Filter" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={TableIcon} label="Sort A-Z" size="large" onClick={handleSort} />
|
|
|
|
<RibbonButton icon={TableIcon} label="Sort Z-A" size="large" onClick={() => {}} />
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Data Tools">
|
|
|
|
<RibbonButton icon={TableIcon} label="Text to Columns" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={TableIcon} label="Remove Duplicates" size="large" onClick={() => {}} />
|
|
|
|
</RibbonGroup>
|
|
|
|
</RibbonTab>
|
|
|
|
|
|
|
|
<RibbonTab label="View" isActive={activeTab === 'view'} onClick={() => setActiveTab('view')}>
|
|
|
|
<RibbonGroup title="Workbook Views">
|
|
|
|
<RibbonButton icon={Grid} label="Normal" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={List} label="Page Layout" size="large" onClick={() => {}} />
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Show">
|
|
|
|
<RibbonButton icon={Grid} label="Gridlines" size="large" onClick={() => {}} />
|
|
|
|
<RibbonButton icon={Rows} label="Headings" size="large" onClick={() => {}} />
|
|
|
|
</RibbonGroup>
|
|
|
|
|
|
|
|
<RibbonGroup title="Zoom">
|
|
|
|
<RibbonButton icon={ZoomInIcon} label="Zoom In" size="large" onClick={handleZoomIn} />
|
|
|
|
<RibbonButton icon={ZoomOutIcon} label="Zoom Out" size="large" onClick={handleZoomOut} />
|
|
|
|
</RibbonGroup>
|
|
|
|
</RibbonTab>
|
2025-03-30 19:28:28 -03:00
|
|
|
</div>
|
|
|
|
</div>
|
2025-06-22 23:01:03 -03:00
|
|
|
|
|
|
|
<div className="formula-bar">
|
|
|
|
<div className="cell-reference">{currentCell}</div>
|
|
|
|
<input
|
|
|
|
type="text"
|
|
|
|
className="formula-input"
|
|
|
|
value={cellContent}
|
|
|
|
onChange={(e) => setCellContent(e.target.value)}
|
|
|
|
onKeyDown={handleSlashCommand}
|
|
|
|
placeholder="Enter formula or value"
|
|
|
|
/>
|
|
|
|
{commandMode && (
|
|
|
|
<div className="command-palette">
|
|
|
|
<div className="command-item">/Worksheet - Manage worksheets</div>
|
|
|
|
<div className="command-item">/Range - Format range</div>
|
|
|
|
<div className="command-item">/File - Save or open files</div>
|
|
|
|
<div className="command-item">/Print - Print options</div>
|
|
|
|
<div className="command-item">/Graph - Create charts</div>
|
|
|
|
<div className="command-item">/Data - Sort and filter</div>
|
|
|
|
<div className="command-item">/System - Settings</div>
|
|
|
|
<div className="command-item">/Quit - Exit application</div>
|
|
|
|
</div>
|
|
|
|
)}
|
|
|
|
</div>
|
|
|
|
|
2025-06-22 23:06:00 -03:00
|
|
|
<div className="worksheet-container h-[470px]">
|
|
|
|
<div ref={containerRef} className="univer-container h-full" />
|
2025-06-22 23:01:03 -03:00
|
|
|
</div>
|
|
|
|
|
|
|
|
<div className="status-bar">
|
|
|
|
<div className="status-mode">{formulaMode === '@' ? 'READY' : 'EDIT'}</div>
|
|
|
|
<div className="status-message">For help, press F1</div>
|
|
|
|
<div className="zoom-controls">
|
|
|
|
<button className="zoom-btn" onClick={handleZoomOut}>-</button>
|
|
|
|
<div className="zoom-level">{zoomLevel}%</div>
|
|
|
|
<button className="zoom-btn" onClick={handleZoomIn}>+</button>
|
|
|
|
</div>
|
2025-03-30 19:28:28 -03:00
|
|
|
</div>
|
|
|
|
</div>
|
2025-03-30 16:42:51 -03:00
|
|
|
);
|
2025-06-22 23:01:03 -03:00
|
|
|
}
|