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: "Mail", href: "/mail", color: "#FFD700" }, // Outlook yellow
{ name: "Drive", href: "/drive", color: "#10B981" }, // Emerald green { name: "Drive", href: "/drive", color: "#10B981" }, // Emerald green
{ name: "Editor", href: "/editor", color: "#2563EB" }, // Word blue { 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: "Meet", href: "/meet", color: "#059669" }, // Google Meet green
{ name: "Videos", href: "/videos", color: "#DC2626" }, // YouTube red { name: "Videos", href: "/videos", color: "#DC2626" }, // YouTube red
{ name: "Music", href: "/music", color: "#1DB954" }, // Spotify green { 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,43 +1,656 @@
import React from 'react'; "use client";
import { DataTable } from './components/DataTable';
import { UserNav } from './components/UserNav';
const tasks = [ import { useState, useRef, useEffect } from 'react';
{ import {
id: "TASK-8782", Bold, Italic, Underline as UnderlineIcon, AlignLeft, AlignCenter,
title: "You can't compress the program without quantifying the open-source SSD pixel!", AlignRight, AlignJustify, Link as LinkIcon, Image as ImageIcon,
status: "in progress", Save, FileText, Printer, Table as TableIcon, Plus, Minus,
label: "documentation", Trash2, Type, Palette, Highlighter, FileImage, File, Home,
priority: "medium" View, ChevronDown, Search, Undo, Redo, Copy, MoreVertical,
}, PieChart, BarChart3, LineChart, Sigma, Filter, DollarSign,
{ Calendar, Clock, Percent, Font, FontSize, BorderAll, BorderNone,
id: "TASK-7878", BorderHorizontal, BorderVertical, BorderInner, BorderOuter,
title: "Try to calculate the EXE feed, maybe it will index the multi-byte pixel!", CornerUpLeft, CornerUpRight, CornerDownLeft, CornerDownRight,
status: "backlog", WrapText, Merge, Split, Functions, PlusCircle, MinusCircle,
label: "documentation", ChevronRight, ChevronLeft, Sun, Moon, Grid, List, Columns,
priority: "medium" Rows, Settings, HelpCircle, Info, Download, Upload, Share2,
}, Lock, Unlock, Maximize, Minimize, X, Check, Sliders, Type as TextIcon,
{ Hash, PlusSquare, MinusSquare, Table2, Divide, Multiply,
id: "TASK-7839", ZoomIn as ZoomInIcon,
title: "We need to bypass the neural TCP card!", ZoomOut as ZoomOutIcon,
status: "todo", Cat as CatIcon
label: "bug", } from 'lucide-react';
priority: "high" 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)
}
};
export default function TaskPage() {
return ( return (
<div className="min-h-screen bg-white"> <div className="excel-clone">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between"> {/* ... (keep all the existing JSX and styles the same) ... */}
<div>
<h1 className="text-2xl font-bold text-gray-900">Welcome back!</h1> <div className="ribbon">
<p className="text-sm text-gray-500">Here's a list of your tasks for this month!</p> <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>
<UserNav /> <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 className="h-[calc(100vh-80px)]">
<DataTable data={tasks} />
</div> </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/react": "^2.22.3",
"@tiptap/starter-kit": "^2.22.3", "@tiptap/starter-kit": "^2.22.3",
"@umoteam/editor": "^7.0.1", "@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", "@zitadel/react": "1.0.5",
"autoprefixer": "10.4.17", "autoprefixer": "10.4.17",
"botframework-directlinejs": "0.15.1", "botframework-directlinejs": "0.15.1",

3445
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff