Add initial styles for Excel-like application layout and components
Some checks are pending
GBCI / build (push) Waiting to run

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2025-06-22 23:01:03 -03:00
parent 6e87c5b449
commit 9b04053cf0
11 changed files with 4381 additions and 528 deletions

View file

@ -11,7 +11,7 @@ const examples = [
{ name: "Mail", href: "/mail", color: "#FFD700" }, // Outlook yellow
{ name: "Drive", href: "/drive", color: "#10B981" }, // Emerald green
{ name: "Editor", href: "/editor", color: "#2563EB" }, // Word blue
{ name: "Tables", href: "/table", color: "#8B5CF6" }, // Purple
{ name: "Tables", href: "/tables", color: "#8B5CF6" }, // Purple
{ name: "Meet", href: "/meet", color: "#059669" }, // Google Meet green
{ name: "Videos", href: "/videos", color: "#DC2626" }, // YouTube red
{ name: "Music", href: "/music", color: "#1DB954" }, // Spotify green

View file

@ -1,69 +0,0 @@
"use client";
import React, { useState } from 'react';
import { Task } from '../data/schema';
import { labels, priorities, statuses } from '../data/data';
import { DataTableToolbar } from './DataTableToolbar';
import { DataTablePagination } from './DataTablePagination';
interface DataTableProps {
data: Task[];
}
export const DataTable: React.FC<DataTableProps> = ({ data }) => {
const [filteredData, setFilteredData] = useState(data);
const [page, setPage] = useState(0);
const [rowsPerPage, setRowsPerPage] = useState(10);
const renderItem = (item: Task) => {
const label = labels.find(l => l.value === item.label);
const status = statuses.find(s => s.value === item.status);
const priority = priorities.find(p => p.value === item.priority);
return (
<tr key={item.id} className="border-b">
<td className="p-2">{item.id}</td>
<td className="p-2">
<span className="bg-gray-100 rounded-full px-2 py-1 text-xs mr-2">
{label?.label}
</span>
{item.title}
</td>
<td className="p-2">{status?.label}</td>
<td className="p-2">{priority?.label}</td>
<td className="p-2">
<button className="text-blue-500 hover:text-blue-700">Actions</button>
</td>
</tr>
);
};
return (
<div className="flex flex-col h-full">
<DataTableToolbar onFilter={setFilteredData} data={data} />
<div className="overflow-auto flex-grow">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">ID</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Title</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Priority</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Actions</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredData.slice(page * rowsPerPage, (page + 1) * rowsPerPage).map(renderItem)}
</tbody>
</table>
</div>
<DataTablePagination
page={page}
setPage={setPage}
rowsPerPage={rowsPerPage}
setRowsPerPage={setRowsPerPage}
totalItems={filteredData.length}
/>
</div>
);
};

View file

@ -1,101 +0,0 @@
import React from 'react';
interface DataTablePaginationProps {
page: number;
setPage: (page: number) => void;
rowsPerPage: number;
setRowsPerPage: (rowsPerPage: number) => void;
totalItems: number;
}
export const DataTablePagination: React.FC<DataTablePaginationProps> = ({
page,
setPage,
rowsPerPage,
setRowsPerPage,
totalItems,
}) => {
const totalPages = Math.ceil(totalItems / rowsPerPage);
return (
<div className="flex items-center justify-between px-6 py-3 border-t border-gray-200">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => setPage(page - 1)}
disabled={page === 0}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Previous
</button>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages - 1}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50"
>
Next
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
Showing <span className="font-medium">{page * rowsPerPage + 1}</span> to{' '}
<span className="font-medium">
{Math.min((page + 1) * rowsPerPage, totalItems)}
</span>{' '}
of <span className="font-medium">{totalItems}</span> results
</p>
</div>
<div>
<select
value={rowsPerPage}
onChange={(e) => setRowsPerPage(Number(e.target.value))}
className="mr-4 border-gray-300 rounded-md shadow-sm focus:border-indigo-300 focus:ring focus:ring-indigo-200 focus:ring-opacity-50"
>
{[5, 10, 25, 50].map((size) => (
<option key={size} value={size}>
Show {size}
</option>
))}
</select>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button
onClick={() => setPage(0)}
disabled={page === 0}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
<span className="sr-only">First</span>
&laquo;
</button>
<button
onClick={() => setPage(page - 1)}
disabled={page === 0}
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
<span className="sr-only">Previous</span>
&lsaquo;
</button>
<span className="relative inline-flex items-center px-4 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-700">
Page {page + 1} of {totalPages}
</span>
<button
onClick={() => setPage(page + 1)}
disabled={page === totalPages - 1}
className="relative inline-flex items-center px-2 py-2 border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
<span className="sr-only">Next</span>
&rsaquo;
</button>
<button
onClick={() => setPage(totalPages - 1)}
disabled={page === totalPages - 1}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50"
>
<span className="sr-only">Last</span>
&raquo;
</button>
</nav>
</div>
</div>
</div>
);
};

View file

@ -1,93 +0,0 @@
import React, { useState } from 'react';
import { Task } from '../data/schema';
import { priorities, statuses } from '../data/data';
interface DataTableToolbarProps {
onFilter: (filteredData: Task[]) => void;
data: Task[];
}
export const DataTableToolbar: React.FC<DataTableToolbarProps> = ({ onFilter, data }) => {
const [searchText, setSearchText] = useState('');
const [statusFilter, setStatusFilter] = useState<string[]>([]);
const [priorityFilter, setPriorityFilter] = useState<string[]>([]);
const applyFilters = () => {
const filteredData = data.filter(task => {
const matchesSearch = task.title.toLowerCase().includes(searchText.toLowerCase());
const matchesStatus = statusFilter.length === 0 || statusFilter.includes(task.status);
const matchesPriority = priorityFilter.length === 0 || priorityFilter.includes(task.priority);
return matchesSearch && matchesStatus && matchesPriority;
});
onFilter(filteredData);
};
const toggleFilter = (filter: string[], value: string, setFilter: (filter: string[]) => void) => {
const newFilter = filter.includes(value)
? filter.filter(f => f !== value)
: [...filter, value];
setFilter(newFilter);
applyFilters();
};
return (
<div className="px-6 py-4 bg-white border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex-1 max-w-lg">
<input
type="text"
placeholder="Filter tasks..."
className="block w-full px-4 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-indigo-500 focus:border-indigo-500"
value={searchText}
onChange={(e) => {
setSearchText(e.target.value);
applyFilters();
}}
/>
</div>
<div className="ml-4 flex space-x-4">
<div className="relative">
<button className="inline-flex justify-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Status
</button>
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
<div className="py-1">
{statuses.map((status) => (
<label key={status.value} className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<input
type="checkbox"
checked={statusFilter.includes(status.value)}
onChange={() => toggleFilter(statusFilter, status.value, setStatusFilter)}
className="mr-2"
/>
{status.label}
</label>
))}
</div>
</div>
</div>
<div className="relative">
<button className="inline-flex justify-center px-4 py-2 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
Priority
</button>
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-white ring-1 ring-black ring-opacity-5 z-10">
<div className="py-1">
{priorities.map((priority) => (
<label key={priority.value} className="flex items-center px-4 py-2 text-sm text-gray-700 hover:bg-gray-100">
<input
type="checkbox"
checked={priorityFilter.includes(priority.value)}
onChange={() => toggleFilter(priorityFilter, priority.value, setPriorityFilter)}
className="mr-2"
/>
{priority.label}
</label>
))}
</div>
</div>
</div>
</div>
</div>
</div>
);
};

View file

@ -1,15 +0,0 @@
import React from 'react';
export const UserNav: React.FC = () => {
return (
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<img className="h-10 w-10 rounded-full" src="https://via.placeholder.com/40" alt="User avatar" />
</div>
<div>
<div className="font-medium text-gray-900">John Doe</div>
<div className="text-sm text-gray-500">john@example.com</div>
</div>
</div>
);
};

View file

@ -1,62 +0,0 @@
export const labels = [
{
value: "bug",
label: "Bug",
},
{
value: "feature",
label: "Feature",
},
{
value: "documentation",
label: "Documentation",
},
]
export const statuses = [
{
value: "backlog",
label: "Backlog",
icon: 'help-circle-outline',
},
{
value: "todo",
label: "Todo",
icon: 'ellipse-outline',
},
{
value: "in progress",
label: "In Progress",
icon: 'timer-outline',
},
{
value: "done",
label: "Done",
icon: 'checkmark-circle-outline',
},
{
value: "canceled",
label: "Canceled",
icon: 'close-circle-outline',
},
]
export const priorities = [
{
label: "Low",
value: "low",
icon: 'arrow-down-outline',
},
{
label: "Medium",
value: "medium",
icon: 'arrow-forward-outline',
},
{
label: "High",
value: "high",
icon: 'arrow-up-outline',
},
]

View file

@ -1,11 +0,0 @@
import { z } from "zod"
export const taskSchema = z.object({
id: z.string(),
title: z.string(),
status: z.string(),
label: z.string(),
priority: z.string(),
})
export type Task = z.infer<typeof taskSchema>

View file

@ -1,44 +1,657 @@
import React from 'react';
import { DataTable } from './components/DataTable';
import { UserNav } from './components/UserNav';
"use client";
const tasks = [
{
id: "TASK-8782",
title: "You can't compress the program without quantifying the open-source SSD pixel!",
status: "in progress",
label: "documentation",
priority: "medium"
},
{
id: "TASK-7878",
title: "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!",
status: "backlog",
label: "documentation",
priority: "medium"
},
{
id: "TASK-7839",
title: "We need to bypass the neural TCP card!",
status: "todo",
label: "bug",
priority: "high"
},
];
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
export default function TaskPage() {
return (
<div className="min-h-screen bg-white">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900">Welcome back!</h1>
<p className="text-sm text-gray-500">Here's a list of your tasks for this month!</p>
</div>
<UserNav />
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>
<div className="h-[calc(100vh-80px)]">
<DataTable data={tasks} />
</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)
}
};
return (
<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>
</div>
</div>
<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>
<div className="worksheet-container">
<div ref={containerRef} className="univer-container" />
</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>
</div>
</div>
);
}
}

411
app/tables/styles.css Normal file
View file

@ -0,0 +1,411 @@
` .excel-clone {
height: 100vh;
display: flex;
flex-direction: column;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: #f5f5f5;
}
.quick-access {
display: flex;
align-items: center;
padding: 0 8px;
background: #f3f3f3;
border-bottom: 1px solid #d9d9d9;
height: 40px;
gap: 8px;
}
.quick-access-btn {
padding: 6px;
border: 1px solid transparent;
background: transparent;
border-radius: 4px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
height: 32px;
min-width: 32px;
color: #333;
}
.quick-access-btn:hover {
background: #e5e5e5;
border-color: #d9d9d9;
}
.quick-access-btn:active {
background: #d9d9d9;
}
.quick-access-separator {
width: 1px;
height: 24px;
background: #d9d9d9;
margin: 0 4px;
}
.title-input {
margin-left: 8px;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-size: 14px;
width: 200px;
height: 28px;
font-family: inherit;
background: white;
}
.title-input:focus {
outline: none;
border-color: #217346;
box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.2);
}
.ribbon {
background: #f3f3f3;
border-bottom: 1px solid #d9d9d9;
}
.ribbon-tabs {
display: flex;
background: #f3f3f3;
padding-left: 8px;
}
.ribbon-tab {
position: relative;
}
.ribbon-tab-button {
padding: 8px 16px;
border: none;
background: transparent;
cursor: pointer;
font-size: 14px;
font-weight: 400;
color: #333;
position: relative;
height: 40px;
}
.ribbon-tab-button:hover:not(.active) {
background: #e5e5e5;
}
.ribbon-tab-button.active {
background: white;
color: #217346;
font-weight: 600;
}
.ribbon-content {
display: flex;
padding: 8px;
background: white;
gap: 16px;
min-height: 80px;
align-items: flex-start;
border-bottom: 1px solid #d9d9d9;
}
.ribbon-group {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
padding: 0 8px;
}
.ribbon-group:not(:last-child)::after {
content: '';
position: absolute;
right: 0;
top: 8px;
bottom: 8px;
width: 1px;
background: #e5e5e5;
}
.ribbon-group-content {
display: flex;
gap: 4px;
margin-bottom: 4px;
flex-wrap: wrap;
justify-content: center;
}
.ribbon-group-title {
font-size: 11px;
color: #666;
text-align: center;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-top: 4px;
}
.ribbon-button {
border: 1px solid transparent;
background: transparent;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.1s ease;
color: #333;
}
.ribbon-button.medium {
padding: 6px;
min-width: 32px;
height: 32px;
}
.ribbon-button.large {
flex-direction: column;
padding: 8px;
min-width: 56px;
height: 56px;
gap: 4px;
}
.ribbon-button:hover {
background: #e5e5e5;
border-color: #d9d9d9;
}
.ribbon-button:active {
background: #d9d9d9;
}
.ribbon-button.active {
background: #e0f0e9;
border-color: #217346;
color: #217346;
}
.ribbon-button-content {
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
}
.ribbon-button-label {
font-size: 11px;
text-align: center;
line-height: 1.2;
font-weight: 400;
max-width: 52px;
word-wrap: break-word;
}
.dropdown-arrow {
margin-left: 4px;
opacity: 0.7;
}
.ribbon-dropdown {
position: relative;
display: inline-block;
}
.ribbon-dropdown-content {
display: none;
position: absolute;
background-color: white;
min-width: 160px;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
z-index: 1;
border-radius: 4px;
border: 1px solid #d9d9d9;
padding: 8px;
left: 0;
top: 100%;
}
.ribbon-dropdown:hover .ribbon-dropdown-content {
display: block;
}
.ribbon-split-button {
display: flex;
border-radius: 4px;
overflow: hidden;
border: 1px solid transparent;
}
.ribbon-split-button:hover {
border-color: #d9d9d9;
}
.ribbon-split-button .ribbon-button {
border-radius: 0;
border: none;
}
.ribbon-split-button-arrow {
padding: 0 4px;
border: none;
background: transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border-left: 1px solid #e5e5e5;
}
.ribbon-split-button-arrow:hover {
background: #e5e5e5;
}
.worksheet-container {
flex: 1;
height: 100%;
position: relative;
overflow: hidden;
background: white;
}
.formula-bar {
display: flex;
align-items: center;
padding: 4px 8px;
border-bottom: 1px solid #d9d9d9;
height: 32px;
background: #f3f3f3;
}
.cell-reference {
font-family: 'Consolas', monospace;
font-size: 14px;
padding: 4px 8px;
min-width: 60px;
text-align: center;
background: white;
border: 1px solid #d9d9d9;
border-radius: 4px;
margin-right: 8px;
}
.formula-input {
flex: 1;
padding: 4px 8px;
border: 1px solid #d9d9d9;
border-radius: 4px;
font-family: 'Consolas', monospace;
font-size: 14px;
height: 24px;
}
.formula-input:focus {
outline: none;
border-color: #217346;
box-shadow: 0 0 0 2px rgba(33, 115, 70, 0.2);
}
.command-palette {
position: absolute;
top: 100%;
left: 0;
right: 0;
background: white;
border: 1px solid #217346;
border-top: none;
max-height: 300px;
overflow-y: auto;
z-index: 20;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.command-item {
padding: 8px 16px;
cursor: pointer;
font-size: 14px;
}
.command-item:hover {
background: #e0f0e9;
}
.univer-container {
position: relative;
top: 0;
left: 0;
right: 0;
bottom: 0;
height: 100%;
width: 100%;
}
.status-bar {
display: flex;
align-items: center;
padding: 0 8px;
background: #f3f3f3;
border-top: 1px solid #d9d9d9;
font-size: 12px;
color: #333;
gap: 16px;
height: 24px;
}
.status-mode {
font-family: 'Consolas', monospace;
font-weight: bold;
color: #217346;
padding: 0 4px;
}
.status-message {
color: #666;
}
.zoom-controls {
display: flex;
align-items: center;
margin-left: auto;
gap: 4px;
}
.zoom-level {
min-width: 40px;
text-align: center;
}
.zoom-btn {
padding: 2px 4px;
border: 1px solid #d9d9d9;
border-radius: 2px;
background: white;
cursor: pointer;
}
.zoom-btn:hover {
background: #e5e5e5;
}
.sample-data-btn {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 12px 24px;
background: #217346;
color: white;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
z-index: 30;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.sample-data-btn:hover {
background: #1a5c3a;
}

View file

@ -58,6 +58,17 @@
"@tiptap/react": "^2.22.3",
"@tiptap/starter-kit": "^2.22.3",
"@umoteam/editor": "^7.0.1",
"@univerjs/core": "^0.8.2",
"@univerjs/design": "^0.8.2",
"@univerjs/docs": "^0.8.2",
"@univerjs/docs-ui": "^0.8.2",
"@univerjs/engine-formula": "^0.8.2",
"@univerjs/engine-render": "^0.8.2",
"@univerjs/presets": "^0.8.2",
"@univerjs/sheets": "^0.8.2",
"@univerjs/sheets-formula": "^0.8.2",
"@univerjs/sheets-ui": "^0.8.2",
"@univerjs/ui": "^0.8.2",
"@zitadel/react": "1.0.5",
"autoprefixer": "10.4.17",
"botframework-directlinejs": "0.15.1",

3445
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff