Add initial styles for Excel-like application layout and components
Some checks are pending
GBCI / build (push) Waiting to run
Some checks are pending
GBCI / build (push) Waiting to run
This commit is contained in:
parent
6e87c5b449
commit
9b04053cf0
11 changed files with 4381 additions and 528 deletions
|
@ -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
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
«
|
||||
</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>
|
||||
‹
|
||||
</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>
|
||||
›
|
||||
</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>
|
||||
»
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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',
|
||||
},
|
||||
]
|
|
@ -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>
|
|
@ -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
411
app/tables/styles.css
Normal 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;
|
||||
}
|
||||
|
11
package.json
11
package.json
|
@ -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
3445
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue