mirror of
https://github.com/PragmaticMachineLearning/probly.git
synced 2026-01-09 21:37:56 -05:00
hide chat window with expand/collapse button and key bindings
This commit is contained in:
@@ -57,5 +57,5 @@ npm run electron-start
|
|||||||
- [ ] Other kinds of operations (hide rows where, hide columns where, sort table by attribute, etc.)
|
- [ ] Other kinds of operations (hide rows where, hide columns where, sort table by attribute, etc.)
|
||||||
- [ ] Support for multiple sheets (or select sheet to use on import)
|
- [ ] Support for multiple sheets (or select sheet to use on import)
|
||||||
- [ ] Make more of the top toolbar buttons work (first check out of the box hands on table functionality)
|
- [ ] Make more of the top toolbar buttons work (first check out of the box hands on table functionality)
|
||||||
- [ ] Keybindings / hotkeys
|
- [] Keybindings / hotkeys
|
||||||
- [ ] Hide chat window with expand / collapse button
|
- [x] Hide chat window with expand / collapse button
|
||||||
|
|||||||
21
components.json
Normal file
21
components.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.js",
|
||||||
|
"css": "src/app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"iconLibrary": "lucide"
|
||||||
|
}
|
||||||
@@ -36,3 +36,14 @@ body {
|
|||||||
.chat-message-container::-webkit-scrollbar-thumb:hover {
|
.chat-message-container::-webkit-scrollbar-thumb:hover {
|
||||||
background: #555;
|
background: #555;
|
||||||
}
|
}
|
||||||
|
.handsontable {
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable .wtHolder {
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable .htCore {
|
||||||
|
z-index: 1 !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,11 +7,12 @@ import {
|
|||||||
SpreadsheetProvider,
|
SpreadsheetProvider,
|
||||||
useSpreadsheet,
|
useSpreadsheet,
|
||||||
} from "@/context/SpreadsheetContext";
|
} from "@/context/SpreadsheetContext";
|
||||||
|
import { MessageCircle } from "lucide-react";
|
||||||
|
|
||||||
const Spreadsheet = dynamic(() => import("@/components/Spreadsheet"), {
|
const Spreadsheet = dynamic(() => import("@/components/Spreadsheet"), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => (
|
loading: () => (
|
||||||
<div className="flex-1 h-[70vh] flex items-center justify-center bg-gray-50 border rounded-lg">
|
<div className="flex-1 h-full flex items-center justify-center bg-gray-50 border rounded-lg">
|
||||||
<div className="text-gray-500">Loading spreadsheet...</div>
|
<div className="text-gray-500">Loading spreadsheet...</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -30,8 +31,33 @@ const SpreadsheetApp = () => {
|
|||||||
const [isElectron, setIsElectron] = useState(false);
|
const [isElectron, setIsElectron] = useState(false);
|
||||||
const [electronAPIAvailable, setElectronAPIAvailable] = useState(false);
|
const [electronAPIAvailable, setElectronAPIAvailable] = useState(false);
|
||||||
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
const [chatHistory, setChatHistory] = useState<ChatMessage[]>([]);
|
||||||
|
const [isChatOpen, setIsChatOpen] = useState(false);
|
||||||
|
|
||||||
// Load chat history from localStorage on mount
|
// Keyboard shortcut handler
|
||||||
|
useEffect(() => {
|
||||||
|
const handleKeyPress = (e: KeyboardEvent) => {
|
||||||
|
if (e.ctrlKey && e.shiftKey && e.key === "?") {
|
||||||
|
setIsChatOpen((prev) => !prev);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("keydown", handleKeyPress);
|
||||||
|
return () => window.removeEventListener("keydown", handleKeyPress);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Load chat state
|
||||||
|
useEffect(() => {
|
||||||
|
const savedState = localStorage.getItem("chatOpen");
|
||||||
|
if (savedState) {
|
||||||
|
setIsChatOpen(JSON.parse(savedState));
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Save chat state
|
||||||
|
useEffect(() => {
|
||||||
|
localStorage.setItem("chatOpen", JSON.stringify(isChatOpen));
|
||||||
|
}, [isChatOpen]);
|
||||||
|
|
||||||
|
// Load chat history
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedHistory = localStorage.getItem("chatHistory");
|
const savedHistory = localStorage.getItem("chatHistory");
|
||||||
if (savedHistory) {
|
if (savedHistory) {
|
||||||
@@ -50,13 +76,13 @@ const SpreadsheetApp = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// Save chat history to localStorage when it changes
|
// Save chat history
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.setItem("chatHistory", JSON.stringify(chatHistory));
|
localStorage.setItem("chatHistory", JSON.stringify(chatHistory));
|
||||||
}, [chatHistory]);
|
}, [chatHistory]);
|
||||||
|
|
||||||
|
// Check Electron availability
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check for Electron API availability after component mounts
|
|
||||||
const checkElectronAvailability = () => {
|
const checkElectronAvailability = () => {
|
||||||
const hasElectronAPI = typeof window !== "undefined" && !!window.electron;
|
const hasElectronAPI = typeof window !== "undefined" && !!window.electron;
|
||||||
setElectronAPIAvailable(hasElectronAPI);
|
setElectronAPIAvailable(hasElectronAPI);
|
||||||
@@ -65,7 +91,7 @@ const SpreadsheetApp = () => {
|
|||||||
if (hasElectronAPI) {
|
if (hasElectronAPI) {
|
||||||
console.log("Electron environment detected!");
|
console.log("Electron environment detected!");
|
||||||
window.electron
|
window.electron
|
||||||
.invoke("test-connection", { test: true })
|
?.invoke("test-connection", { test: true })
|
||||||
.then((result) => console.log("Electron test successful:", result))
|
.then((result) => console.log("Electron test successful:", result))
|
||||||
.catch((err) => console.error("Electron test failed:", err));
|
.catch((err) => console.error("Electron test failed:", err));
|
||||||
} else {
|
} else {
|
||||||
@@ -81,7 +107,6 @@ const SpreadsheetApp = () => {
|
|||||||
isElectron,
|
isElectron,
|
||||||
hasElectronAPI: electronAPIAvailable,
|
hasElectronAPI: electronAPIAvailable,
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let updates;
|
let updates;
|
||||||
|
|
||||||
@@ -153,16 +178,26 @@ const SpreadsheetApp = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className="min-h-screen p-8 bg-gray-50">
|
<main className="h-screen w-screen bg-gray-50 overflow-hidden">
|
||||||
<div className="max-w-[1800px] mx-auto">
|
<div className="h-full p-4 flex flex-col">
|
||||||
<h1 className="text-2xl font-bold mb-4 text-gray-800">
|
<h1 className="text-2xl font-bold mb-4 text-gray-800">
|
||||||
Magic Spreadsheet
|
Magic Spreadsheet
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4 flex-1 relative">
|
||||||
<div className="flex-1 bg-white rounded-lg shadow-sm overflow-hidden">
|
<div className="flex-1 bg-white rounded-lg shadow-sm">
|
||||||
<Spreadsheet onDataChange={handleDataChange} />
|
<Spreadsheet onDataChange={handleDataChange} />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-96">
|
{/* Updated ChatBox container */}
|
||||||
|
<div
|
||||||
|
className={`fixed right-4 top-[5.5rem] bottom-4 w-96 transition-transform duration-300 transform ${
|
||||||
|
isChatOpen ? "translate-x-0" : "translate-x-full"
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
backgroundColor: "white",
|
||||||
|
boxShadow: "0 0 10px rgba(0, 0, 0, 0.1)",
|
||||||
|
zIndex: 9999,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<ChatBox
|
<ChatBox
|
||||||
onSend={handleSend}
|
onSend={handleSend}
|
||||||
chatHistory={chatHistory}
|
chatHistory={chatHistory}
|
||||||
@@ -171,6 +206,13 @@ const SpreadsheetApp = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsChatOpen((prev) => !prev)}
|
||||||
|
className="fixed bottom-4 right-4 p-3 rounded-full shadow-lg bg-blue-500 hover:bg-blue-600 text-white transition-colors z-50"
|
||||||
|
title="Toggle Chat (Ctrl+Shift+/)"
|
||||||
|
>
|
||||||
|
<MessageCircle size={24} />
|
||||||
|
</button>
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { CellUpdate } from "@/types/api";
|
import { CellUpdate } from "@/types/api";
|
||||||
|
import { Send, Trash2, Loader2 } from "lucide-react";
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -21,7 +22,6 @@ const ChatBox = ({ onSend, chatHistory, clearHistory }: ChatBoxProps) => {
|
|||||||
const handleSend = async () => {
|
const handleSend = async () => {
|
||||||
if (message.trim()) {
|
if (message.trim()) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await onSend(message);
|
await onSend(message);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -40,59 +40,81 @@ const ChatBox = ({ onSend, chatHistory, clearHistory }: ChatBoxProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-[calc(100vh-8rem)] bg-white rounded-lg shadow-sm border">
|
<div className="flex flex-col h-full bg-white rounded-lg shadow-lg border border-gray-200">
|
||||||
{/* Chat Header */}
|
{/* Header */}
|
||||||
<div className="p-4 border-b flex justify-between items-center">
|
<div className="px-4 py-3 border-b border-gray-200 flex justify-between items-center bg-white rounded-t-lg z-50">
|
||||||
<h2 className="font-semibold text-gray-800">Chat History</h2>
|
<div>
|
||||||
|
<h2 className="font-semibold text-gray-800">AI Assistant</h2>
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Ask me about spreadsheet formulas
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={clearHistory}
|
onClick={clearHistory}
|
||||||
className="text-sm px-2 py-1 text-red-500 hover:bg-red-50 rounded transition-colors"
|
className="p-2 text-gray-500 hover:text-red-500 hover:bg-red-50 rounded-full transition-colors"
|
||||||
|
title="Clear chat history"
|
||||||
>
|
>
|
||||||
Clear History
|
<Trash2 size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Chat History */}
|
{/* Chat History */}
|
||||||
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
<div className="flex-1 overflow-y-auto p-4 space-y-4 bg-gray-50">
|
||||||
{chatHistory.map((chat) => (
|
{chatHistory.length === 0 ? (
|
||||||
<div key={chat.id} className="space-y-2">
|
<div className="flex flex-col items-center justify-center h-full text-gray-500 space-y-2">
|
||||||
<div className="flex items-start gap-2">
|
<p className="text-sm">No messages yet</p>
|
||||||
<div className="bg-blue-50 rounded-lg p-3 max-w-[80%]">
|
<p className="text-xs text-center">
|
||||||
<p className="text-sm text-gray-800">{chat.text}</p>
|
Try asking me to create formulas or analyze your data
|
||||||
<span className="text-xs text-gray-500">
|
</p>
|
||||||
{new Date(chat.timestamp).toLocaleTimeString()}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-start gap-2 justify-end">
|
|
||||||
<div className="bg-gray-50 rounded-lg p-3 max-w-[80%]">
|
|
||||||
<pre className="text-sm text-gray-800 whitespace-pre-wrap">
|
|
||||||
{chat.response}
|
|
||||||
</pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
) : (
|
||||||
|
chatHistory.map((chat) => (
|
||||||
|
<div key={chat.id} className="space-y-2">
|
||||||
|
{/* User Message */}
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<div className="bg-blue-500 text-white rounded-2xl rounded-tr-sm px-4 py-2 max-w-[80%] shadow-sm">
|
||||||
|
<p className="text-sm">{chat.text}</p>
|
||||||
|
<span className="text-xs opacity-75 mt-1 block">
|
||||||
|
{new Date(chat.timestamp).toLocaleTimeString()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* AI Response */}
|
||||||
|
<div className="flex justify-start">
|
||||||
|
<div className="bg-white rounded-2xl rounded-tl-sm px-4 py-2 max-w-[80%] shadow-sm border border-gray-200">
|
||||||
|
<pre className="text-sm text-gray-800 whitespace-pre-wrap font-mono">
|
||||||
|
{chat.response}
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Input Area */}
|
{/* Input Area */}
|
||||||
<div className="p-4 border-t">
|
<div className="p-4 bg-white border-t border-gray-200 rounded-b-lg">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2 items-center">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={message}
|
value={message}
|
||||||
onChange={(e) => setMessage(e.target.value)}
|
onChange={(e) => setMessage(e.target.value)}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
className="flex-1 p-2 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="flex-1 px-4 py-2 border border-gray-200 rounded-full focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent text-sm"
|
||||||
placeholder="Ask the LLM to create a formula..."
|
placeholder="Type your message..."
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
className="px-4 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition-colors disabled:bg-gray-400"
|
disabled={isLoading || !message.trim()}
|
||||||
disabled={isLoading}
|
className="p-2 bg-blue-500 text-white rounded-full hover:bg-blue-600 transition-colors disabled:bg-gray-300 disabled:cursor-not-allowed"
|
||||||
|
title="Send message"
|
||||||
>
|
>
|
||||||
{isLoading ? "..." : "Send"}
|
{isLoading ? (
|
||||||
|
<Loader2 size={20} className="animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Send size={20} />
|
||||||
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
9
src/components/KeyboardShortcutHint.tsx
Normal file
9
src/components/KeyboardShortcutHint.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const KeyboardShortcutHint = () => {
|
||||||
|
return (
|
||||||
|
<div className="fixed bottom-20 right-4 bg-gray-800 text-white text-sm px-3 py-2 rounded-lg opacity-0 transition-opacity duration-200 group-hover:opacity-100">
|
||||||
|
Press Ctrl+Shift+/ to toggle chat
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default KeyboardShortcutHint;
|
||||||
@@ -128,6 +128,15 @@ const Spreadsheet = ({ onDataChange, initialData }: SpreadsheetProps) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (spreadsheetRef.current && !hotInstanceRef.current) {
|
if (spreadsheetRef.current && !hotInstanceRef.current) {
|
||||||
try {
|
try {
|
||||||
|
const config = getInitialConfig(currentData);
|
||||||
|
config.afterChange = (changes: any) => {
|
||||||
|
if (changes) {
|
||||||
|
const currentData = hotInstanceRef.current?.getData();
|
||||||
|
if (currentData && onDataChange) {
|
||||||
|
onDataChange(currentData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
hotInstanceRef.current = new Handsontable(
|
hotInstanceRef.current = new Handsontable(
|
||||||
spreadsheetRef.current,
|
spreadsheetRef.current,
|
||||||
getInitialConfig(currentData),
|
getInitialConfig(currentData),
|
||||||
@@ -152,7 +161,7 @@ const Spreadsheet = ({ onDataChange, initialData }: SpreadsheetProps) => {
|
|||||||
}, [currentData]);
|
}, [currentData]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full h-full bg-white">
|
<div className="h-full flex flex-col">
|
||||||
<SpreadsheetToolbar
|
<SpreadsheetToolbar
|
||||||
onImport={handleImport}
|
onImport={handleImport}
|
||||||
onExport={handleExport}
|
onExport={handleExport}
|
||||||
@@ -205,7 +214,7 @@ const Spreadsheet = ({ onDataChange, initialData }: SpreadsheetProps) => {
|
|||||||
onSearch={() => setShowSearch(true)}
|
onSearch={() => setShowSearch(true)}
|
||||||
onSort={() => dataHandlers.handleSort(hotInstanceRef.current)}
|
onSort={() => dataHandlers.handleSort(hotInstanceRef.current)}
|
||||||
/>
|
/>
|
||||||
<div className="relative flex-1 min-h-0">
|
<div className="relative flex-1">
|
||||||
{showSearch && (
|
{showSearch && (
|
||||||
<SearchBox
|
<SearchBox
|
||||||
onSearch={handleSearch}
|
onSearch={handleSearch}
|
||||||
|
|||||||
57
src/components/ui/button.tsx
Normal file
57
src/components/ui/button.tsx
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import { Slot } from "@radix-ui/react-slot"
|
||||||
|
import { cva, type VariantProps } from "class-variance-authority"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
const buttonVariants = cva(
|
||||||
|
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"bg-primary text-primary-foreground shadow hover:bg-primary/90",
|
||||||
|
destructive:
|
||||||
|
"bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90",
|
||||||
|
outline:
|
||||||
|
"border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground",
|
||||||
|
secondary:
|
||||||
|
"bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80",
|
||||||
|
ghost: "hover:bg-accent hover:text-accent-foreground",
|
||||||
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
default: "h-9 px-4 py-2",
|
||||||
|
sm: "h-8 rounded-md px-3 text-xs",
|
||||||
|
lg: "h-10 rounded-md px-8",
|
||||||
|
icon: "h-9 w-9",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
size: "default",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
export interface ButtonProps
|
||||||
|
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||||
|
VariantProps<typeof buttonVariants> {
|
||||||
|
asChild?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
|
({ className, variant, size, asChild = false, ...props }, ref) => {
|
||||||
|
const Comp = asChild ? Slot : "button"
|
||||||
|
return (
|
||||||
|
<Comp
|
||||||
|
className={cn(buttonVariants({ variant, size, className }))}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Button.displayName = "Button"
|
||||||
|
|
||||||
|
export { Button, buttonVariants }
|
||||||
@@ -11,7 +11,7 @@ export const getInitialConfig = (data: any[][]): GridSettings => {
|
|||||||
rowHeaders: true,
|
rowHeaders: true,
|
||||||
colHeaders: true,
|
colHeaders: true,
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: "70vh",
|
height: "100%",
|
||||||
licenseKey: "non-commercial-and-evaluation",
|
licenseKey: "non-commercial-and-evaluation",
|
||||||
formulas: {
|
formulas: {
|
||||||
engine: hyperformulaInstance,
|
engine: hyperformulaInstance,
|
||||||
@@ -36,5 +36,7 @@ export const getInitialConfig = (data: any[][]): GridSettings => {
|
|||||||
const cellProperties: any = {};
|
const cellProperties: any = {};
|
||||||
return cellProperties;
|
return cellProperties;
|
||||||
},
|
},
|
||||||
|
observeDomVisibility: true,
|
||||||
|
observeChanges: true,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
16
src/lib/spreadsheet/themes.ts
Normal file
16
src/lib/spreadsheet/themes.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export const themes: any = {
|
||||||
|
default: {
|
||||||
|
name: "default",
|
||||||
|
className: "default-theme",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
name: "dark",
|
||||||
|
className: "dark-theme",
|
||||||
|
},
|
||||||
|
custom: {
|
||||||
|
name: "custom",
|
||||||
|
className: "custom-theme",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type ThemeType = keyof typeof themes;
|
||||||
6
src/lib/utils.ts
Normal file
6
src/lib/utils.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { clsx, type ClassValue } from "clsx"
|
||||||
|
import { twMerge } from "tailwind-merge"
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs))
|
||||||
|
}
|
||||||
279
src/styles/spreadsheet-themes.css
Normal file
279
src/styles/spreadsheet-themes.css
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
.handsontable.default-theme {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.default-theme th {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
color: #374151;
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.default-theme td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #1f2937;
|
||||||
|
border-color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.default-theme .currentRow {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.default-theme .currentCol {
|
||||||
|
background-color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark Theme */
|
||||||
|
.handsontable.dark-theme {
|
||||||
|
background: #1f2937;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme th {
|
||||||
|
background-color: #374151;
|
||||||
|
color: #e5e7eb;
|
||||||
|
border-color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme td {
|
||||||
|
background-color: #1f2937;
|
||||||
|
color: #f9fafb;
|
||||||
|
border-color: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme .currentRow {
|
||||||
|
background-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme .currentCol {
|
||||||
|
background-color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme .handsontable-row-header,
|
||||||
|
.handsontable.dark-theme .handsontable-col-header {
|
||||||
|
background-color: #374151;
|
||||||
|
color: #e5e7eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Custom Theme (Blue) */
|
||||||
|
.handsontable.custom-theme {
|
||||||
|
background: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.custom-theme th {
|
||||||
|
background-color: #0ea5e9;
|
||||||
|
color: white;
|
||||||
|
border-color: #7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.custom-theme td {
|
||||||
|
background-color: #ffffff;
|
||||||
|
color: #0c4a6e;
|
||||||
|
border-color: #7dd3fc;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.custom-theme .currentRow {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.custom-theme .currentCol {
|
||||||
|
background-color: #f0f9ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Common Styles */
|
||||||
|
.handsontable {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection Styles */
|
||||||
|
.handsontable .area {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable .area.corner {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Text Formatting */
|
||||||
|
.handsontable td strong,
|
||||||
|
.handsontable td .bold {
|
||||||
|
font-weight: bold !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable td em,
|
||||||
|
.handsontable td .italic {
|
||||||
|
font-style: italic !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable td u,
|
||||||
|
.handsontable td .underline {
|
||||||
|
text-decoration: underline !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cell Alignment */
|
||||||
|
.handsontable td.htLeft {
|
||||||
|
text-align: left !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable td.htCenter {
|
||||||
|
text-align: center !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable td.htRight {
|
||||||
|
text-align: right !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Search Highlighting */
|
||||||
|
.handsontable td.search-highlight {
|
||||||
|
background-color: rgba(255, 255, 0, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header Highlighting */
|
||||||
|
.handsontable th.ht__highlight {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
color: white !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active Cell */
|
||||||
|
.handsontable td.current {
|
||||||
|
border: 2px solid #0ea5e9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Invalid Cell */
|
||||||
|
.handsontable td.htInvalid {
|
||||||
|
background-color: #fee2e2 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Read-only Cell */
|
||||||
|
.handsontable td.htReadOnly {
|
||||||
|
background-color: #f3f4f6 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Merged Cells */
|
||||||
|
.handsontable td.htMergedCell {
|
||||||
|
background-color: #f9fafb !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Context Menu */
|
||||||
|
.handsontable .htContextMenu {
|
||||||
|
background: #ffffff;
|
||||||
|
border: 1px solid #e5e7eb;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme .htContextMenu {
|
||||||
|
background: #374151;
|
||||||
|
border: 1px solid #4b5563;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable .htContextMenu .ht_master .htCore td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable .htContextMenu .ht_master .htCore td:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme .htContextMenu .ht_master .htCore td:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Scrollbars */
|
||||||
|
.handsontable ::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable ::-webkit-scrollbar-track {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme ::-webkit-scrollbar-track {
|
||||||
|
background: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable ::-webkit-scrollbar-thumb {
|
||||||
|
background: #cbd5e1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme ::-webkit-scrollbar-thumb {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme ::-webkit-scrollbar-thumb:hover {
|
||||||
|
background: #6b7280;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Comments */
|
||||||
|
.handsontable .htCommentCell:after {
|
||||||
|
border-color: #ef4444 transparent transparent #ef4444 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Column Resize Handle */
|
||||||
|
.handsontable .manualColumnResizer:hover,
|
||||||
|
.handsontable .manualColumnResizer.active {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Row Resize Handle */
|
||||||
|
.handsontable .manualRowResizer:hover,
|
||||||
|
.handsontable .manualRowResizer.active {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Focus Border */
|
||||||
|
.handsontable .wtBorder.current {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable .wtBorder.area {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selection Border */
|
||||||
|
.handsontable .wtBorder.selection {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fill Handle */
|
||||||
|
.handsontable .wtBorder.corner {
|
||||||
|
background-color: #0ea5e9 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Autocomplete */
|
||||||
|
.handsontableEditor.autocompleteEditor {
|
||||||
|
background: #ffffff;
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontableEditor.autocompleteEditor .ht_master .htCore td {
|
||||||
|
padding: 8px 12px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontableEditor.autocompleteEditor .ht_master .htCore td:hover {
|
||||||
|
background: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme .handsontableEditor.autocompleteEditor {
|
||||||
|
background: #374151;
|
||||||
|
color: #f9fafb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.handsontable.dark-theme
|
||||||
|
.handsontableEditor.autocompleteEditor
|
||||||
|
.ht_master
|
||||||
|
.htCore
|
||||||
|
td:hover {
|
||||||
|
background: #4b5563;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user