feat(desktop): add new navigation links to index.html
Added new navigation links for Dashboard, Editor, Player, Paper, Settings, Tables, and News sections. Each link includes click handlers to switch sections and active state styling. This expands the application's navigation options for better user access to different features.
This commit is contained in:
parent
01e89c9358
commit
b9395cb0d7
71 changed files with 3301 additions and 1891 deletions
|
|
@ -1,14 +0,0 @@
|
|||
<app>
|
||||
|
||||
<client-nav/>
|
||||
|
||||
<script src="https://unpkg.com/riot@10/riot+compiler.min.js"></script>
|
||||
|
||||
<!-- Load the component definition -->
|
||||
<script type="riot" src="client-nav.html"></script>
|
||||
|
||||
<script>
|
||||
riot.mount('client-nav');
|
||||
|
||||
</script>
|
||||
</app>
|
||||
|
|
@ -1,298 +0,0 @@
|
|||
<!-- Riot.js component for the chat page (converted from app/chat/page.tsx) -->
|
||||
<template>
|
||||
<div class="flex min-h-[calc(100vh-43px)] bg-background text-foreground">
|
||||
<!-- Sidebar -->
|
||||
<div class="{sidebarOpen ? 'w-80' : 'w-0'} transition-all duration-300 ease-in-out bg-card border-r border-border flex flex-col overflow-hidden">
|
||||
{sidebarOpen && (
|
||||
<>
|
||||
<!-- Sidebar Header -->
|
||||
<div class="p-4 border-b border-border flex-shrink-0">
|
||||
<button @click={newChat} class="flex items-center gap-3 w-full p-3 rounded-xl border-2 border-dashed border-muted hover:border-accent hover:bg-accent/10 transition-all duration-200 group">
|
||||
<Plus class="w-5 h-5 text-muted-foreground group-hover:text-accent" />
|
||||
<span class="font-medium text-foreground group-hover:text-accent">New Chat</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Conversations List -->
|
||||
<div class="flex-1 overflow-hidden">
|
||||
<div class="h-full overflow-y-auto px-3 py-2 space-y-1">
|
||||
{conversations.map(conv => (
|
||||
<div
|
||||
key={conv.id}
|
||||
class="group relative flex items-center gap-3 p-3 rounded-xl cursor-pointer transition-all duration-200 {conv.active ? 'bg-primary/10 border border-primary' : 'hover:bg-secondary'}"
|
||||
@click={() => setActiveConversation(conv.id)}
|
||||
>
|
||||
<MessageSquare class="w-4 h-4 flex-shrink-0 {conv.active ? 'text-primary' : 'text-muted-foreground'}" />
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium truncate {conv.active ? 'text-primary' : 'text-foreground'}">{conv.title}</div>
|
||||
<div class="text-xs text-muted-foreground truncate">{formatTimestamp(conv.timestamp)}</div>
|
||||
</div>
|
||||
{conv.active && <div class="w-2 h-2 rounded-full bg-primary flex-shrink-0"></div>}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Sidebar Footer -->
|
||||
<div class="p-4 border-t border-border flex-shrink-0">
|
||||
<div class="flex items-center gap-3 p-3 rounded-xl hover:bg-secondary cursor-pointer transition-colors duration-200">
|
||||
<User class="w-5 h-5 text-muted-foreground" />
|
||||
<User class="w-5 h-5 text-muted-foreground" />
|
||||
<User class="w-5 h-5 text-muted-foreground" />
|
||||
<User class="w-5 h-5 text-muted-foreground" />
|
||||
<User class="w-5 h-5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<!-- Main Chat Area -->
|
||||
<div class="flex-1 flex flex-col min-w-0 bg-background">
|
||||
<!-- Header -->
|
||||
<div class="flex-shrink-0 p-4 border-b border-border bg-card">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-4">
|
||||
<button @click={() => sidebarOpen = !sidebarOpen} class="p-2 hover:bg-secondary rounded-lg transition-colors duration-200">
|
||||
<Menu class="w-5 h-5" />
|
||||
</button>
|
||||
<h1 class="text-xl font-semibold text-foreground">{activeConversation?.title || 'New Chat'}</h1>
|
||||
</div>
|
||||
<button class="p-2 hover:bg-secondary rounded-lg transition-colors duration-200">
|
||||
<Search class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Messages Container -->
|
||||
<div class="flex-1 overflow-hidden flex flex-col">
|
||||
<div class="flex-1 overflow-y-auto px-4 py-6 space-y-6">
|
||||
{messages.map(message => (
|
||||
<div key={message.id} class="group flex {message.type === 'user' ? 'justify-end' : 'justify-start'}">
|
||||
<div class="max-w-[85%] md:max-w-[75%] {message.type === 'user' ? 'bg-primary text-primary-foreground' : 'bg-secondary text-secondary-foreground'} rounded-2xl px-4 py-3 shadow-sm">
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 mt-0.5">
|
||||
{message.type === 'user' ? <User class="w-4 h-4" /> : <Bot class="w-4 h-4" />}
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="whitespace-pre-wrap break-words leading-relaxed">{message.content}</div>
|
||||
<div class="mt-3 flex items-center justify-between text-xs {message.type === 'user' ? 'text-primary-foreground/80' : 'text-muted-foreground'}">
|
||||
<span>{formatTimestamp(message.timestamp)}</span>
|
||||
{message.type === 'assistant' && (
|
||||
<div class="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity duration-200">
|
||||
<button class="p-1.5 hover:bg-secondary rounded-md transition-colors">
|
||||
<Copy class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-secondary rounded-md transition-colors">
|
||||
<ThumbsUp class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-secondary rounded-md transition-colors">
|
||||
<ThumbsDown class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
<button class="p-1.5 hover:bg-secondary rounded-md transition-colors">
|
||||
<Share class="w-3.5 h-3.5 text-muted-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isTyping && (
|
||||
<div class="flex justify-start">
|
||||
<div class="max-w-[85%] md:max-w-[75%] bg-secondary rounded-2xl px-4 py-3 shadow-sm">
|
||||
<div class="flex items-center gap-3">
|
||||
<Bot class="w-4 h-4 text-muted-foreground" />
|
||||
<div class="flex gap-1">
|
||||
<div class="w-2 h-2 rounded-full bg-muted-foreground animate-bounce"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-muted-foreground animate-bounce" style="animation-delay:0.2s"></div>
|
||||
<div class="w-2 h-2 rounded-full bg-muted-foreground animate-bounce" style="animation-delay:0.4s"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div ref={messagesEndRef}></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mode Carousel -->
|
||||
<div class="flex-shrink-0 border-t border-border bg-card">
|
||||
<div class="overflow-x-auto px-4 py-3">
|
||||
<div class="flex gap-2 min-w-max">
|
||||
{modeButtons.map(button => (
|
||||
<button
|
||||
key={button.id}
|
||||
@click={() => activeMode = button.id}
|
||||
class="flex items-center gap-2 px-4 py-2 rounded-full text-sm font-medium transition-all duration-200 whitespace-nowrap {activeMode === button.id ? 'bg-primary/10 text-primary border border-primary' : 'bg-secondary text-secondary-foreground hover:bg-secondary/80'}"
|
||||
>
|
||||
{button.icon}
|
||||
<span>{button.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="flex-shrink-0 p-4 border-t border-border bg-card">
|
||||
<div class="relative max-w-4xl mx-auto">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={input}
|
||||
@input={e => input = e.target.value}
|
||||
@keydown={handleKeyDown}
|
||||
placeholder="Type your message..."
|
||||
class="w-full p-4 pr-14 rounded-2xl border border-input bg-background text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:border-transparent resize-none transition-all duration-200"
|
||||
rows="1"
|
||||
style="min-height:56px;max-height:120px"
|
||||
></textarea>
|
||||
<button
|
||||
@click={handleSubmit}
|
||||
disabled={!input.trim()}
|
||||
class="absolute right-6 bottom-3 p-2.5 rounded-xl transition-all duration-200 {input.trim() ? 'bg-primary hover:bg-primary/90 text-primary-foreground shadow-lg hover:shadow-xl transform hover:scale-105' : 'bg-muted text-muted-foreground cursor-not-allowed'}"
|
||||
>
|
||||
<Send class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script >
|
||||
import { useState, useRef } from 'riot';
|
||||
import {
|
||||
Send, Plus, Menu, Search,
|
||||
MessageSquare, User, Bot, Copy, ThumbsUp, ThumbsDown,
|
||||
Share, Image, Video,
|
||||
Brain, Globe
|
||||
} from 'lucide-react';
|
||||
import './style.css';
|
||||
|
||||
export default {
|
||||
// Reactive state
|
||||
messages: [],
|
||||
input: '',
|
||||
isTyping: false,
|
||||
sidebarOpen: true,
|
||||
conversations: [],
|
||||
activeMode: 'assistant',
|
||||
modeButtons: [],
|
||||
activeConversation: null,
|
||||
textareaRef: null,
|
||||
messagesEndRef: null,
|
||||
|
||||
// Lifecycle
|
||||
async mounted() {
|
||||
// Initialize state (mirroring the original React defaults)
|
||||
this.messages = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'assistant',
|
||||
content: "Hello! I'm General Bots, a large language model by Pragmatismo. How can I help you today?",
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
this.input = '';
|
||||
this.isTyping = false;
|
||||
this.sidebarOpen = true;
|
||||
this.conversations = [
|
||||
{ id: 1, title: 'Current Chat', timestamp: new Date(), active: true },
|
||||
{ id: 2, title: 'Previous Conversation', timestamp: new Date(Date.now() - 86400000), active: false },
|
||||
{ id: 3, title: 'Code Review Discussion', timestamp: new Date(Date.now() - 172800000), active: false },
|
||||
{ id: 4, title: 'Project Planning', timestamp: new Date(Date.now() - 259200000), active: false },
|
||||
];
|
||||
this.activeMode = 'assistant';
|
||||
this.modeButtons = [
|
||||
{ id: 'deep-think', icon: <Brain size={16} />, label: 'Deep Think' },
|
||||
{ id: 'web', icon: <Globe size={16} />, label: 'Web' },
|
||||
{ id: 'image', icon: <Image size={16} />, label: 'Image' },
|
||||
{ id: 'video', icon: <Video size={16} />, label: 'Video' },
|
||||
];
|
||||
this.setActiveConversation(1);
|
||||
this.scrollToBottom();
|
||||
},
|
||||
|
||||
// Helpers
|
||||
formatTimestamp(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffInHours = (now - date) / (1000 * 60 * 60);
|
||||
if (diffInHours < 24) {
|
||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
||||
} else if (diffInHours < 48) {
|
||||
return 'Yesterday';
|
||||
} else {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
this.messagesEndRef?.scrollIntoView({ behavior: 'smooth' });
|
||||
},
|
||||
|
||||
// Event handlers
|
||||
async handleSubmit() {
|
||||
if (!this.input.trim()) return;
|
||||
const userMessage = {
|
||||
id: Date.now(),
|
||||
type: 'user',
|
||||
content: this.input.trim(),
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
this.messages = [...this.messages, userMessage];
|
||||
this.input = '';
|
||||
this.isTyping = true;
|
||||
this.update();
|
||||
|
||||
// Simulate assistant response
|
||||
setTimeout(() => {
|
||||
const assistantMessage = {
|
||||
id: Date.now() + 1,
|
||||
type: 'assistant',
|
||||
content: `I understand you're asking about "${userMessage.content}". This is a simulated response to demonstrate the chat interface. The actual implementation would connect to your chat provider and send real responses.`,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
this.messages = [...this.messages, assistantMessage];
|
||||
this.isTyping = false;
|
||||
this.update();
|
||||
this.scrollToBottom();
|
||||
}, 1500);
|
||||
},
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.handleSubmit();
|
||||
}
|
||||
},
|
||||
|
||||
newChat() {
|
||||
const newConv = {
|
||||
id: Date.now(),
|
||||
title: 'New Chat',
|
||||
timestamp: new Date(),
|
||||
active: true
|
||||
};
|
||||
this.conversations = [newConv, ...this.conversations.map(c => ({ ...c, active: false }))];
|
||||
this.messages = [
|
||||
{
|
||||
id: Date.now(),
|
||||
type: 'assistant',
|
||||
content: "Hello! I'm General Bots, a large language model by Pragmatismo. How can I help you today?",
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
];
|
||||
this.setActiveConversation(newConv.id);
|
||||
},
|
||||
|
||||
setActiveConversation(id) {
|
||||
this.conversations = this.conversations.map(c => ({ ...c, active: c.id === id }));
|
||||
this.activeConversation = this.conversations.find(c => c.id === id);
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1,414 +0,0 @@
|
|||
.nav-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 50;
|
||||
background: hsl(var(--background));
|
||||
height: auto;
|
||||
min-height: 40px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
|
||||
border-bottom: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.nav-inner {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 0 16px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.auth-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-container,
|
||||
.theme-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.login-button,
|
||||
.theme-toggle {
|
||||
background: hsl(var(--accent));
|
||||
border: 1px solid hsl(var(--border));
|
||||
color: hsl(var(--accent-foreground));
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.login-button:hover,
|
||||
.theme-toggle:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 10px hsla(var(--primary), 0.5);
|
||||
}
|
||||
|
||||
.login-menu,
|
||||
.theme-menu {
|
||||
position: absolute;
|
||||
top: calc(100% + 8px);
|
||||
right: 0;
|
||||
background: hsl(var(--popover));
|
||||
border: 1px solid hsl(var(--border));
|
||||
border-radius: 6px;
|
||||
min-width: 120px;
|
||||
z-index: 100;
|
||||
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2);
|
||||
padding: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.menu-item,
|
||||
.theme-menu-item {
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
text-align: left;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.menu-item:hover,
|
||||
.theme-menu-item:hover {
|
||||
background: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
}
|
||||
|
||||
.active-theme {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.scroll-btn {
|
||||
background: hsl(var(--accent));
|
||||
border: 1px solid hsl(var(--border));
|
||||
color: hsl(var(--accent-foreground));
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
transition: all 0.3s ease;
|
||||
flex-shrink: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.scroll-btn:hover {
|
||||
transform: scale(1.1);
|
||||
box-shadow: 0 0 10px hsla(var(--primary), 0.5);
|
||||
}
|
||||
|
||||
.scroll-btn:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
.nav-scroll {
|
||||
flex: 1;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
height: 100%;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
scroll-behavior: smooth;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
white-space: nowrap;
|
||||
gap: 3px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
position: relative;
|
||||
background: hsl(var(--card));
|
||||
border: 1px solid hsl(var(--border));
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
padding: 6px 14px;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
height: 32px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
transition: all 0.3s ease;
|
||||
overflow: hidden;
|
||||
min-width: 70px;
|
||||
}
|
||||
|
||||
.nav-item::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(var(--neon-color-rgb, 0, 255, 255), 0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.nav-item:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
border-color: var(--neon-color, hsl(var(--primary)));
|
||||
color: var(--neon-color, hsl(var(--primary)));
|
||||
box-shadow: 0 0 15px rgba(var(--neon-color-rgb, 0, 255, 255), 0.3);
|
||||
text-shadow: 0 0 6px rgba(var(--neon-color-rgb, 0, 255, 255), 0.4);
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
border-color: var(--neon-color, hsl(var(--primary)));
|
||||
color: var(--neon-color, hsl(var(--primary)));
|
||||
box-shadow: 0 0 20px rgba(var(--neon-color-rgb, 0, 255, 255), 0.4);
|
||||
text-shadow: 0 0 8px rgba(var(--neon-color-rgb, 0, 255, 255), 0.6);
|
||||
}
|
||||
|
||||
.nav-item.active:hover {
|
||||
box-shadow: 0 0 25px rgba(var(--neon-color-rgb, 0, 255, 255), 0.6);
|
||||
}
|
||||
|
||||
.neon-glow {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg, transparent, rgba(var(--neon-color-rgb, 0, 255, 255), 0.3), transparent);
|
||||
border-radius: 8px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.nav-item:hover .neon-glow,
|
||||
.nav-item.active .neon-glow {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.nav-spacer {
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
/* Set CSS custom properties for each neon color */
|
||||
.nav-item[style*="--neon-color: #25D366"] {
|
||||
--neon-color-rgb: 37, 211, 102;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #6366F1"] {
|
||||
--neon-color-rgb: 99, 102, 241;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #FFD700"] {
|
||||
--neon-color-rgb: 255, 215, 0;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #10B981"] {
|
||||
--neon-color-rgb: 16, 185, 129;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #2563EB"] {
|
||||
--neon-color-rgb: 37, 99, 235;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #8B5CF6"] {
|
||||
--neon-color-rgb: 139, 92, 246;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #059669"] {
|
||||
--neon-color-rgb: 5, 150, 105;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #DC2626"] {
|
||||
--neon-color-rgb: 220, 38, 38;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #1DB954"] {
|
||||
--neon-color-rgb: 29, 185, 84;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #F59E0B"] {
|
||||
--neon-color-rgb: 245, 158, 11;
|
||||
}
|
||||
|
||||
.nav-item[style*="--neon-color: #6B7280"] {
|
||||
--neon-color-rgb: 107, 114, 128;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.nav-container {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.nav-spacer {
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.nav-inner {
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.scroll-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.theme-toggle,
|
||||
.login-button {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 13px;
|
||||
padding: 8px 16px;
|
||||
height: 36px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
gap: 6px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.auth-controls {
|
||||
gap: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.nav-container {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.nav-spacer {
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.nav-inner {
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.scroll-btn {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.theme-toggle,
|
||||
.login-button {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
font-size: 12px;
|
||||
padding: 10px 14px;
|
||||
height: 34px;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
gap: 4px;
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.auth-controls {
|
||||
gap: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 320px) {
|
||||
.nav-inner {
|
||||
padding: 0 6px;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 8px 12px;
|
||||
height: 32px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
gap: 3px;
|
||||
padding: 0 4px;
|
||||
}
|
||||
|
||||
.theme-toggle,
|
||||
.login-button {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.scroll-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Touch-friendly scrolling for mobile */
|
||||
@media (hover: none) and (pointer: coarse) {
|
||||
.nav-scroll {
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scroll-snap-type: x mandatory;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
scroll-snap-align: start;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,493 +0,0 @@
|
|||
<client-nav>
|
||||
<script>
|
||||
// Import icons from Lucide (using CDN)
|
||||
import { HardDrive, Terminal, ChevronLeft, ChevronRight } from 'https://cdn.jsdelivr.net/npm/lucide@0.331.0/+esm';
|
||||
|
||||
export default {
|
||||
// Component state
|
||||
data() {
|
||||
return {
|
||||
examples: [
|
||||
{ name: "Chat", href: "/chat", color: "#25D366" },
|
||||
{ name: "Paper", href: "/paper", color: "#6366F1" },
|
||||
{ name: "Mail", href: "/mail", color: "#FFD700" },
|
||||
{ name: "Calendar", href: "/calendar", color: "#1DB954" },
|
||||
{ name: "Meet", href: "/meet", color: "#059669" },
|
||||
{ name: "Drive", href: "/drive", color: "#10B981" },
|
||||
{ name: "Editor", href: "/editor", color: "#2563EB" },
|
||||
{ name: "Player", href: "/player", color: "Yellow" },
|
||||
{ name: "Tables", href: "/tables", color: "#8B5CF6" },
|
||||
{ name: "Dashboard", href: "/dashboard", color: "#6366F1" },
|
||||
{ name: "Sources", href: "/sources", color: "#F59E0B" },
|
||||
{ name: "Settings", href: "/settings", color: "#6B7280" },
|
||||
],
|
||||
pathname: window.location.pathname,
|
||||
scrollContainer: null,
|
||||
navItems: [],
|
||||
isLoggedIn: false,
|
||||
showLoginMenu: false,
|
||||
showScrollButtons: false,
|
||||
loginMenu: null,
|
||||
showThemeMenu: false,
|
||||
themeMenu: null,
|
||||
currentTime: new Date(),
|
||||
theme: { name: "default", label: "Default", icon: "🎨" },
|
||||
themes: [
|
||||
{ name: "retrowave", label: "Retrowave", icon: "🌌" },
|
||||
{ name: "vapordream", label: "Vapordream", icon: "🌀" },
|
||||
{ name: "y2kglow", label: "Y2K Glow", icon: "💿" },
|
||||
{ name: "mellowgold", label: "Mellow Gold", icon: "☮️" },
|
||||
{ name: "arcadeflash", label: "Arcade Flash", icon: "🕹️" },
|
||||
{ name: "polaroidmemories", label: "Polaroid Memories", icon: "📸" },
|
||||
{ name: "midcenturymod", label: "Mid‑Century Mod", icon: "🪑" },
|
||||
{ name: "grungeera", label: "Grunge Era", icon: "🎸" },
|
||||
{ name: "discofever", label: "Disco Fever", icon: "🪩" },
|
||||
{ name: "saturdaycartoons", label: "Saturday Cartoons", icon: "📺" },
|
||||
{ name: "oldhollywood", label: "Old Hollywood", icon: "🎬" },
|
||||
{ name: "cyberpunk", label: "Cyberpunk", icon: "🤖" },
|
||||
{ name: "seasidepostcard", label: "Seaside Postcard", icon: "🏖️" },
|
||||
{ name: "typewriter", label: "Typewriter", icon: "⌨️" },
|
||||
{ name: "jazzage", label: "Jazz Age", icon: "🎷" },
|
||||
{ name: "xtreegold", label: "X‑Tree Gold", icon: "X" },
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
// Lifecycle: component mounted
|
||||
mounted() {
|
||||
// References
|
||||
this.scrollContainer = this.root.querySelector('.nav-scroll');
|
||||
this.loginMenu = this.root.querySelector('.login-menu');
|
||||
this.themeMenu = this.root.querySelector('.theme-menu');
|
||||
|
||||
// Initialize nav item refs
|
||||
this.navItems = Array.from(this.root.querySelectorAll('.nav-item'));
|
||||
|
||||
// Time update interval
|
||||
this.timeInterval = setInterval(() => {
|
||||
this.currentTime = new Date();
|
||||
this.update();
|
||||
}, 1000);
|
||||
|
||||
// Scroll button visibility
|
||||
this.checkScrollNeeded();
|
||||
|
||||
// Resize listener
|
||||
window.addEventListener('resize', this.checkScrollNeeded);
|
||||
|
||||
// Click‑outside handling
|
||||
document.addEventListener('mousedown', this.handleClickOutside);
|
||||
},
|
||||
|
||||
// Cleanup
|
||||
unmounted() {
|
||||
clearInterval(this.timeInterval);
|
||||
window.removeEventListener('resize', this.checkScrollNeeded);
|
||||
document.removeEventListener('mousedown', this.handleClickOutside);
|
||||
},
|
||||
|
||||
// Methods
|
||||
formatTime(date) {
|
||||
return date.toLocaleTimeString('en-US', {
|
||||
hour12: false,
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
});
|
||||
},
|
||||
|
||||
formatDate(date) {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
});
|
||||
},
|
||||
|
||||
isActive(href) {
|
||||
if (href === '/') return this.pathname === href;
|
||||
return this.pathname.startsWith(href);
|
||||
},
|
||||
|
||||
handleLogin() {
|
||||
this.isLoggedIn = true;
|
||||
this.showLoginMenu = false;
|
||||
this.update();
|
||||
},
|
||||
|
||||
handleLogout() {
|
||||
this.isLoggedIn = false;
|
||||
this.showLoginMenu = false;
|
||||
this.update();
|
||||
},
|
||||
|
||||
checkScrollNeeded() {
|
||||
if (this.scrollContainer) {
|
||||
const container = this.scrollContainer;
|
||||
const isScrollable = container.scrollWidth > container.clientWidth;
|
||||
this.showScrollButtons = isScrollable;
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
|
||||
handleClickOutside(event) {
|
||||
if (this.loginMenu && !this.loginMenu.contains(event.target)) {
|
||||
this.showLoginMenu = false;
|
||||
}
|
||||
if (this.themeMenu && !this.themeMenu.contains(event.target)) {
|
||||
this.showThemeMenu = false;
|
||||
}
|
||||
this.update();
|
||||
},
|
||||
|
||||
scrollLeft() {
|
||||
if (this.scrollContainer) {
|
||||
this.scrollContainer.scrollBy({ left: -150, behavior: 'smooth' });
|
||||
}
|
||||
},
|
||||
|
||||
scrollRight() {
|
||||
if (this.scrollContainer) {
|
||||
this.scrollContainer.scrollBy({ left: 150, behavior: 'smooth' });
|
||||
}
|
||||
},
|
||||
|
||||
getThemeIcon() {
|
||||
const found = this.themes.find(t => t.name === this.theme.name);
|
||||
return found ? found.icon : '🎨';
|
||||
},
|
||||
|
||||
setTheme(name) {
|
||||
const found = this.themes.find(t => t.name === name);
|
||||
if (found) {
|
||||
this.theme = found;
|
||||
this.showThemeMenu = false;
|
||||
this.update();
|
||||
}
|
||||
},
|
||||
|
||||
navigate(href) {
|
||||
window.location.href = href;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
/* Basic styles - the original Tailwind classes are kept as comments for reference */
|
||||
.fixed {
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.top-0 {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.left-0 {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right-0 {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.z-50 {
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.bg-gray-800 {
|
||||
background-color: #2d3748;
|
||||
}
|
||||
|
||||
.text-green-400 {
|
||||
color: #68d391;
|
||||
}
|
||||
|
||||
.font-mono {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.border-b {
|
||||
border-bottom: 1px solid transparent;
|
||||
}
|
||||
|
||||
.border-green-600 {
|
||||
border-color: #38a169;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: .75rem;
|
||||
}
|
||||
|
||||
.flex {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.items-center {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.justify-between {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.px-4 {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.py-1 {
|
||||
padding-top: .25rem;
|
||||
padding-bottom: .25rem;
|
||||
}
|
||||
|
||||
.gap-4>*+* {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
.gap-2>*+* {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.w-3 {
|
||||
width: .75rem;
|
||||
}
|
||||
|
||||
.h-3 {
|
||||
height: .75rem;
|
||||
}
|
||||
|
||||
.text-green-300 {
|
||||
color: #9ae6b4;
|
||||
}
|
||||
|
||||
.w-2 {
|
||||
width: .5rem;
|
||||
}
|
||||
|
||||
.h-2 {
|
||||
height: .5rem;
|
||||
}
|
||||
|
||||
.bg-green-500 {
|
||||
background-color: #48bb78;
|
||||
}
|
||||
|
||||
.rounded-full {
|
||||
border-radius: 9999px;
|
||||
}
|
||||
|
||||
.animate-pulse {
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
.nav-container {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav-inner {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.logo-container img {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav-scroll {
|
||||
overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
-ms-overflow-style: none;
|
||||
}
|
||||
|
||||
.nav-scroll::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav-items {
|
||||
display: flex;
|
||||
gap: .25rem;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: .25rem .5rem;
|
||||
border: 1px solid transparent;
|
||||
border-radius: .25rem;
|
||||
cursor: pointer;
|
||||
transition: all .2s;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background-color: #2d3748;
|
||||
border-color: #68d391;
|
||||
color: #68d391;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
.auth-controls {
|
||||
display: flex;
|
||||
gap: .5rem;
|
||||
}
|
||||
|
||||
.login-button,
|
||||
.theme-toggle {
|
||||
background: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.login-menu,
|
||||
.theme-menu {
|
||||
position: absolute;
|
||||
background: #1a202c;
|
||||
border: 1px solid #4a5568;
|
||||
padding: .5rem;
|
||||
margin-top: .25rem;
|
||||
border-radius: .25rem;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: block;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
padding: .25rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #a0aec0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background: #2d3748;
|
||||
}
|
||||
|
||||
.active-theme {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- Markup -->
|
||||
<div class="fixed top-0 left-0 right-0 z-50 bg-gray-800 text-green-400 font-mono border-b border-green-600 text-xs">
|
||||
<div class="flex items-center justify-between px-4 py-1">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<HardDrive class="w-3 h-3 text-green-400" />
|
||||
<span class="text-green-300">RETRO NAVIGATOR v4.0</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-2 h-2 bg-green-500 rounded-full animate-pulse"></div>
|
||||
<span class="text-green-400">READY</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="text-green-300">THEME:</span>
|
||||
<span class="text-yellow-400">{theme.label}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-4">
|
||||
<span class="text-green-300">{formatDate(currentTime)}</span>
|
||||
<span class="text-green-300">{formatTime(currentTime)}</span>
|
||||
<div class="flex items-center gap-1">
|
||||
<Terminal class="w-3 h-3 text-green-400" />
|
||||
<span class="text-green-400">SYS</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-container" style="top:24px;">
|
||||
<div class="nav-inner">
|
||||
<div class="nav-content">
|
||||
<div class="logo-container">
|
||||
<img src="/images/generalbots-logo.svg" alt="Logo" width="64" height="24" />
|
||||
</div>
|
||||
|
||||
{#if showScrollButtons}
|
||||
<button
|
||||
class="w-8 h-8 bg-gray-800 border border-green-600 text-green-400 rounded hover:bg-green-900/30 hover:border-green-500 hover:text-green-300 transition-all flex items-center justify-center flex-shrink-0 mx-1"
|
||||
@click="scrollLeft" aria-label="Scroll left">
|
||||
<ChevronLeft class="w-4 h-4" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="nav-scroll">
|
||||
<div class="nav-items">
|
||||
{#each examples as example, index}
|
||||
<button class="nav-item {isActive(example.href) ? 'active' : ''}" @click="{() => navigate(example.href)}"
|
||||
style="--neon-color:{example.color}" @mouseenter="{(e) => {
|
||||
e.target.style.boxShadow = `0 0 15px ${example.color}60`;
|
||||
e.target.style.borderColor = example.color;
|
||||
e.target.style.color = example.color;
|
||||
e.target.style.textShadow = `0 0 8px ${example.color}80`;
|
||||
}}" @mouseleave="{(e) => {
|
||||
if (!isActive(example.href)) {
|
||||
e.target.style.boxShadow = 'none';
|
||||
e.target.style.borderColor = '';
|
||||
e.target.style.color = '';
|
||||
e.target.style.textShadow = 'none';
|
||||
}
|
||||
}}">
|
||||
{example.name}
|
||||
<div class="neon-glow"></div>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if showScrollButtons}
|
||||
<button
|
||||
class="w-8 h-8 bg-gray-800 border border-green-600 text-green-400 rounded hover:bg-green-900/30 hover:border-green-500 hover:text-green-300 transition-all flex items-center justify-center flex-shrink-0 mx-1"
|
||||
@click="scrollRight" aria-label="Scroll right">
|
||||
<ChevronRight class="w-4 h-4" />
|
||||
</button>
|
||||
{/if}
|
||||
|
||||
<div class="auth-controls">
|
||||
<div class="login-container" bind:this="{loginMenu}">
|
||||
<button @click="{() => showLoginMenu = !showLoginMenu}" class="login-button"
|
||||
aria-label="{isLoggedIn ? 'User menu' : 'Login'}">
|
||||
{isLoggedIn ? '👤' : '🔐'}
|
||||
</button>
|
||||
|
||||
{#if showLoginMenu}
|
||||
<div class="login-menu">
|
||||
{#if !isLoggedIn}
|
||||
<button @click="handleLogin" class="menu-item">Login</button>
|
||||
{:else}
|
||||
<button @click="handleLogout" class="menu-item">Logout</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="theme-container" bind:this="{themeMenu}">
|
||||
<button @click="{() => showThemeMenu = !showThemeMenu}" class="theme-toggle" aria-label="Change theme">
|
||||
{getThemeIcon()}
|
||||
</button>
|
||||
|
||||
{#if showThemeMenu}
|
||||
<div class="theme-menu">
|
||||
{#each themes as t}
|
||||
<button @click="{() => setTheme(t.name)}"
|
||||
class="theme-menu-item {theme.name === t.name ? 'active-theme' : ''}">
|
||||
{t.label}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav-spacer" style="height:88px;"></div>
|
||||
</div>
|
||||
</client-nav>
|
||||
|
|
@ -1,227 +0,0 @@
|
|||
<!-- Riot.js component for the drive page (converted from app/drive/page.tsx) -->
|
||||
<template>
|
||||
<div class="flex flex-col h-[calc(100vh-40px)] bg-background">
|
||||
<resizable-panel-group direction="horizontal" class="flex-1 min-h-0">
|
||||
<!-- Folder Tree Panel -->
|
||||
<resizable-panel
|
||||
default-size="20"
|
||||
collapsed-size="4"
|
||||
collapsible="true"
|
||||
min-size="15"
|
||||
max-size="30"
|
||||
@collapse="{() => isCollapsed = true}"
|
||||
@resize="{() => isCollapsed = false}"
|
||||
class="{isCollapsed && 'min-w-[50px] transition-all duration-300'}"
|
||||
>
|
||||
<folder-tree
|
||||
on-select="{setCurrentPath}"
|
||||
selected-path="{currentPath}"
|
||||
is-collapsed="{isCollapsed}"
|
||||
/>
|
||||
</resizable-panel>
|
||||
|
||||
<resizable-handle with-handle class="bg-border" />
|
||||
|
||||
<!-- File List Panel -->
|
||||
<resizable-panel default-size="50" min-size="30" class="bg-background border-r border-border">
|
||||
<tabs default-value="all" class="flex flex-col h-full">
|
||||
<div class="flex items-center px-4 py-2 bg-secondary border-b border-border">
|
||||
<h1 class="text-xl font-bold">{currentItem?.name || 'My Drive'}</h1>
|
||||
<tabs-list class="ml-auto bg-background">
|
||||
<tabs-trigger value="all" class="data-[state=active]:bg-accent">All</tabs-trigger>
|
||||
<tabs-trigger value="starred" class="data-[state=active]:bg-accent">Starred</tabs-trigger>
|
||||
</tabs-list>
|
||||
</div>
|
||||
<separator class="bg-border" />
|
||||
<div class="bg-secondary p-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="relative flex-1">
|
||||
<search class="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<input placeholder="Search files"
|
||||
class="pl-8 bg-background border border-border"
|
||||
bind="{searchTerm}"
|
||||
@input="{e => searchTerm = e.target.value}" />
|
||||
</div>
|
||||
<select value="{filterType}" @change="{e => filterType = e.target.value}">
|
||||
<option value="all">All items</option>
|
||||
<option value="folders">Folders</option>
|
||||
<option value="files">Files</option>
|
||||
<option value="starred">Starred</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<tabs-content value="all" class="m-0 flex-1">
|
||||
<file-list
|
||||
path="{currentPath}"
|
||||
search-term="{searchTerm}"
|
||||
filter-type="{filterType}"
|
||||
selected-file="{selectedFile}"
|
||||
set-selected-file="{file => selectedFile = file}"
|
||||
on-context-action="{handleContextAction}"
|
||||
/>
|
||||
</tabs-content>
|
||||
<tabs-content value="starred" class="m-0 flex-1">
|
||||
<file-list
|
||||
path="{currentPath}"
|
||||
search-term="{searchTerm}"
|
||||
filter-type="starred"
|
||||
selected-file="{selectedFile}"
|
||||
set-selected-file="{file => selectedFile = file}"
|
||||
on-context-action="{handleContextAction}"
|
||||
/>
|
||||
</tabs-content>
|
||||
</tabs>
|
||||
</resizable-panel>
|
||||
|
||||
<resizable-handle with-handle class="bg-border" />
|
||||
|
||||
<!-- File Details Panel -->
|
||||
<resizable-panel default-size="30" min-size="25" class="bg-background">
|
||||
<file-display file="{selectedFile}" />
|
||||
</resizable-panel>
|
||||
</resizable-panel-group>
|
||||
<footer-component shortcuts="{shortcuts}" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script >
|
||||
import { useState, useEffect } from 'riot';
|
||||
import {
|
||||
Search, Download, Trash2, Share, Star,
|
||||
MoreVertical, Home, ChevronRight, ChevronLeft,
|
||||
Folder, File, Image, Video, Music, FileText, Code, Database,
|
||||
Clock, Users, Eye, Edit3, Copy, Scissors,
|
||||
FolderPlus, Info, Lock, Menu,
|
||||
ExternalLink, History, X
|
||||
} from 'lucide-react';
|
||||
import { cn } from "@/lib/utils";
|
||||
import Footer from '../footer';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { ContextMenu, ContextMenuContent, ContextMenuItem, ContextMenuSeparator, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger } from '@/components/ui/context-menu';
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
ResizableHandle,
|
||||
ResizablePanel,
|
||||
ResizablePanelGroup,
|
||||
} from "@/components/ui/resizable";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||
import './style.css';
|
||||
|
||||
export default {
|
||||
// Reactive state
|
||||
isCollapsed: false,
|
||||
currentPath: '',
|
||||
searchTerm: '',
|
||||
filterType: 'all',
|
||||
selectedFile: null,
|
||||
isMobile: false,
|
||||
showMobileMenu: false,
|
||||
activePanel: 'files',
|
||||
shortcuts: [],
|
||||
|
||||
// Lifecycle
|
||||
async mounted() {
|
||||
// Initialize shortcuts (same as original component)
|
||||
this.shortcuts = [
|
||||
[
|
||||
{ key: 'Q', label: 'Rename', action: () => console.log('Rename') },
|
||||
{ key: 'W', label: 'View', action: () => console.log('View') },
|
||||
{ key: 'E', label: 'Edit', action: () => console.log('Edit') },
|
||||
{ key: 'R', label: 'Move', action: () => console.log('Move') },
|
||||
{ key: 'T', label: 'MkDir', action: () => console.log('Make Directory') },
|
||||
{ key: 'Y', label: 'Delete', action: () => console.log('Delete') },
|
||||
{ key: 'U', label: 'Copy', action: () => console.log('Copy') },
|
||||
{ key: 'I', label: 'Cut', action: () => console.log('Cut') },
|
||||
{ key: 'O', label: 'Paste', action: () => console.log('Paste') },
|
||||
{ key: 'P', label: 'Duplicate', action: () => console.log('Duplicate') },
|
||||
],
|
||||
[
|
||||
{ key: 'A', label: 'Select', action: () => console.log('Select') },
|
||||
{ key: 'S', label: 'Select All', action: () => console.log('Select All') },
|
||||
{ key: 'D', label: 'Deselect', action: () => console.log('Deselect') },
|
||||
{ key: 'G', label: 'Details', action: () => console.log('Details') },
|
||||
{ key: 'H', label: 'History', action: () => console.log('History') },
|
||||
{ key: 'J', label: 'Share', action: () => console.log('Share') },
|
||||
{ key: 'K', label: 'Star', action: () => console.log('Star') },
|
||||
{ key: 'L', label: 'Download', action: () => console.log('Download') },
|
||||
{ key: 'Z', label: 'Upload', action: () => console.log('Upload') },
|
||||
{ key: 'X', label: 'Refresh', action: () => console.log('Refresh') },
|
||||
]
|
||||
];
|
||||
},
|
||||
|
||||
// Helpers
|
||||
formatDate(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric'
|
||||
});
|
||||
},
|
||||
|
||||
formatDateTime(dateString) {
|
||||
const date = new Date(dateString);
|
||||
return date.toLocaleString('en-US', {
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: '2-digit',
|
||||
hour12: true
|
||||
});
|
||||
},
|
||||
|
||||
formatDistanceToNow(date) {
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - new Date(date).getTime();
|
||||
const diffMinutes = Math.floor(diffMs / (1000 * 60));
|
||||
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
|
||||
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
||||
|
||||
if (diffMinutes < 1) return 'now';
|
||||
if (diffMinutes < 60) return `${diffMinutes}m ago`;
|
||||
if (diffHours < 24) return `${diffHours}h ago`;
|
||||
if (diffDays < 7) return `${diffDays}d ago`;
|
||||
return this.formatDate(date);
|
||||
},
|
||||
|
||||
formatFileSize(bytes) {
|
||||
if (!bytes) return '';
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||
return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${sizes[i]}`;
|
||||
},
|
||||
|
||||
getFileIcon(item) {
|
||||
if (item.is_dir) {
|
||||
return <Folder className="w-4 h-4 text-yellow-500" />;
|
||||
}
|
||||
const iconMap = {
|
||||
pdf: <FileText className="w-4 h-4 text-red-500" />,
|
||||
xlsx: <Database className="w-4 h-4 text-green-600" />,
|
||||
json: <Code className="w-4 h-4 text-yellow-600" />,
|
||||
markdown: <Edit3 className="w-4 h-4 text-purple-500" />,
|
||||
md: <Edit3 className="w-4 h-4 text-purple-500" />,
|
||||
jpg: <Image className="w-4 h-4 text-pink-500" />,
|
||||
jpeg: <Image className="w-4 h-4 text-pink-500" />,
|
||||
png: <Image className="w-4 h-4 text-pink-500" />,
|
||||
mp4: <Video className="w-4 h-4 text-red-600" />,
|
||||
mp3: <Music className="w-4 h-4 text-green-600" />
|
||||
};
|
||||
return iconMap[item.type] || <File className="w-4 h-4 text-muted-foreground" />;
|
||||
},
|
||||
|
||||
// Context actions
|
||||
handleContextAction(action, file) {
|
||||
console.log(`Context action: ${action}`, file);
|
||||
// Implement actions as needed
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
|
@ -1 +0,0 @@
|
|||
- The UI shoule look exactly xtree gold but using shadcn with keyborad shortcut well explicit.
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>General Bots</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<app></app>
|
||||
|
||||
<script src="https://unpkg.com/riot@10/riot+compiler.min.js"></script>
|
||||
<script src="app.html" type="riot"></script>
|
||||
|
||||
<script>
|
||||
riot.compile().then(() => {
|
||||
riot.mount('app');
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
@ -1,15 +0,0 @@
|
|||
import { atom, useAtom } from "jotai"
|
||||
|
||||
import { Mail, mails } from "./data"
|
||||
|
||||
type Config = {
|
||||
selected: Mail["id"] | null
|
||||
}
|
||||
|
||||
const configAtom = atom<Config>({
|
||||
selected: mails[0].id,
|
||||
})
|
||||
|
||||
export function useMail() {
|
||||
return useAtom(configAtom)
|
||||
}
|
||||
|
|
@ -1,408 +0,0 @@
|
|||
` .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;
|
||||
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;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
102
web/desktop/css/global.css
Normal file
102
web/desktop/css/global.css
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
||||
background: #0f172a;
|
||||
color: #e2e8f0;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Navbar */
|
||||
nav {
|
||||
background: #1e293b;
|
||||
border-bottom: 2px solid #334155;
|
||||
padding: 0 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 60px;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
nav .logo {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
background: linear-gradient(135deg, #3b82f6, #8b5cf6);
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #94a3b8;
|
||||
text-decoration: none;
|
||||
padding: 0.75rem 1.25rem;
|
||||
border-radius: 0.5rem;
|
||||
transition: all 0.2s;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
nav a:hover {
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
nav a.active {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
}
|
||||
|
||||
/* Main Content */
|
||||
#main-content {
|
||||
height: calc(100vh - 60px);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.content-section {
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.content-section.active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Panel Styles */
|
||||
.panel {
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
/* Buttons */
|
||||
button {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Utility */
|
||||
h1, h2, h3 {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.text-sm {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.text-xs {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
|
||||
.text-gray {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
[x-cloak] {
|
||||
display: none !important;
|
||||
}
|
||||
66
web/desktop/css/modules/drive.css
Normal file
66
web/desktop/css/modules/drive.css
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/* Drive Styles */
|
||||
.drive-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 250px 1fr 300px;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.drive-sidebar, .drive-details {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.drive-main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
margin: 0.25rem 0.5rem;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
.file-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
cursor: pointer;
|
||||
border-radius: 0.375rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.file-item:hover {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.file-item.selected {
|
||||
background: #1e40af;
|
||||
}
|
||||
|
||||
.file-icon {
|
||||
font-size: 2rem;
|
||||
}
|
||||
45
web/desktop/css/modules/mail.css
Normal file
45
web/desktop/css/modules/mail.css
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/* Mail Styles */
|
||||
.mail-layout {
|
||||
display: grid;
|
||||
grid-template-columns: 250px 350px 1fr;
|
||||
gap: 1rem;
|
||||
padding: 1rem;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mail-sidebar, .mail-list, .mail-content {
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.mail-item {
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #334155;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.mail-item:hover {
|
||||
background: #334155;
|
||||
}
|
||||
|
||||
.mail-item.unread {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mail-item.selected {
|
||||
background: #1e40af;
|
||||
}
|
||||
|
||||
.mail-content-view {
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.mail-header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid #334155;
|
||||
}
|
||||
|
||||
.mail-body {
|
||||
line-height: 1.6;
|
||||
}
|
||||
108
web/desktop/css/modules/tasks.css
Normal file
108
web/desktop/css/modules/tasks.css
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/* Tasks Styles */
|
||||
.tasks-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.task-input {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.task-input input {
|
||||
flex: 1;
|
||||
padding: 0.75rem;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
color: #e2e8f0;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.task-input input:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
|
||||
.task-input button {
|
||||
padding: 0.75rem 1.5rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 0.5rem;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.task-input button:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
|
||||
.task-list {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.task-item {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
background: #1e293b;
|
||||
border: 1px solid #334155;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.task-item.completed span {
|
||||
text-decoration: line-through;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.task-item input[type="checkbox"] {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.task-item span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.task-item button {
|
||||
background: #ef4444;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
|
||||
.task-item button:hover {
|
||||
background: #dc2626;
|
||||
}
|
||||
|
||||
.task-filters {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
margin-top: 2rem;
|
||||
padding-top: 2rem;
|
||||
border-top: 1px solid #334155;
|
||||
}
|
||||
|
||||
.task-filters button {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #334155;
|
||||
color: #e2e8f0;
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.task-filters button.active {
|
||||
background: #3b82f6;
|
||||
}
|
||||
73
web/desktop/dashboard/components/date-range-picker.js
Normal file
73
web/desktop/dashboard/components/date-range-picker.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
// DateRangePicker component
|
||||
class DateRangePicker {
|
||||
constructor() {
|
||||
this.state = {
|
||||
startDate: new Date(),
|
||||
endDate: new Date()
|
||||
};
|
||||
this.element = document.createElement('div');
|
||||
this.element.className = 'date-range-picker';
|
||||
this.render();
|
||||
this.bindEvents();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.element.innerHTML = `
|
||||
<div class="flex items-center gap-2">
|
||||
<button class="start-date-btn">
|
||||
Start: ${this.formatDate(this.state.startDate)}
|
||||
</button>
|
||||
<span>to</span>
|
||||
<button class="end-date-btn">
|
||||
End: ${this.formatDate(this.state.endDate)}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.element.querySelector('.start-date-btn').addEventListener('click', () => {
|
||||
this.setStartDate();
|
||||
});
|
||||
|
||||
this.element.querySelector('.end-date-btn').addEventListener('click', () => {
|
||||
this.setEndDate();
|
||||
});
|
||||
}
|
||||
|
||||
setStartDate() {
|
||||
const input = prompt("Enter start date (YYYY-MM-DD)");
|
||||
if (input) {
|
||||
this.state.startDate = new Date(input);
|
||||
this.render();
|
||||
this.onDateChange();
|
||||
}
|
||||
}
|
||||
|
||||
setEndDate() {
|
||||
const input = prompt("Enter end date (YYYY-MM-DD)");
|
||||
if (input) {
|
||||
this.state.endDate = new Date(input);
|
||||
this.render();
|
||||
this.onDateChange();
|
||||
}
|
||||
}
|
||||
|
||||
formatDate(date) {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
onDateChange() {
|
||||
// To be implemented by parent
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and mount the component
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const picker = new DateRangePicker();
|
||||
document.querySelector('.date-range-picker').replaceWith(picker.element);
|
||||
});
|
||||
28
web/desktop/dashboard/components/overview.js
Normal file
28
web/desktop/dashboard/components/overview.js
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
// Overview component
|
||||
class Overview {
|
||||
constructor() {
|
||||
this.element = document.createElement('div');
|
||||
this.element.className = 'overview-chart';
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.element.innerHTML = `
|
||||
<div class="chart-container">
|
||||
<div class="flex justify-between items-end h-40">
|
||||
${[100, 80, 60, 40, 20].map((h, i) => `
|
||||
<div class="chart-bar"
|
||||
style="height:${h}px;background-color:hsl(var(--chart-${(i%5)+1}))">
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and mount the component
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const overview = new Overview();
|
||||
document.querySelector('.overview-chart').replaceWith(overview.element);
|
||||
});
|
||||
40
web/desktop/dashboard/components/recent-sales.js
Normal file
40
web/desktop/dashboard/components/recent-sales.js
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
// RecentSales component
|
||||
class RecentSales {
|
||||
constructor() {
|
||||
this.salesData = [
|
||||
{ name: "Olivia Martin", email: "olivia.martin@email.com", amount: "+$1,999.00" },
|
||||
{ name: "Jackson Lee", email: "jackson.lee@email.com", amount: "+$39.00" },
|
||||
{ name: "Isabella Nguyen", email: "isabella.nguyen@email.com", amount: "+$299.00" },
|
||||
{ name: "William Kim", email: "will@email.com", amount: "+$99.00" },
|
||||
{ name: "Sofia Davis", email: "sofia.davis@email.com", amount: "+$39.00" }
|
||||
];
|
||||
this.element = document.createElement('div');
|
||||
this.element.className = 'recent-sales-list';
|
||||
this.render();
|
||||
}
|
||||
|
||||
render() {
|
||||
this.element.innerHTML = `
|
||||
<div class="sales-list">
|
||||
${this.salesData.map(sale => `
|
||||
<div class="sale-item">
|
||||
<div class="sale-info">
|
||||
<div class="avatar">${sale.name[0]}</div>
|
||||
<div>
|
||||
<div class="name">${sale.name}</div>
|
||||
<div class="email">${sale.email}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="amount">${sale.amount}</div>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize and mount the component
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const recentSales = new RecentSales();
|
||||
document.querySelector('.recent-sales-list').replaceWith(recentSales.element);
|
||||
});
|
||||
73
web/desktop/dashboard/dashboard.css
Normal file
73
web/desktop/dashboard/dashboard.css
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
/* Dashboard specific styles */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.cards-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 1.5rem;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
font-size: 0.875rem;
|
||||
color: var(--muted-foreground);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card .value {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: var(--card-foreground);
|
||||
}
|
||||
|
||||
.card .subtext {
|
||||
font-size: 0.75rem;
|
||||
color: var(--muted-foreground);
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
|
||||
.dashboard-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
}
|
||||
|
||||
.overview-container,
|
||||
.recent-sales-container {
|
||||
padding: 1.5rem;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
|
||||
.overview-container h3,
|
||||
.recent-sales-container h3 {
|
||||
font-size: 1.125rem;
|
||||
font-weight: 500;
|
||||
color: var(--card-foreground);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.download-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: var(--primary);
|
||||
color: var(--primary-foreground);
|
||||
border: none;
|
||||
border-radius: 0.375rem;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.download-btn:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
58
web/desktop/dashboard/dashboard.js
Normal file
58
web/desktop/dashboard/dashboard.js
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
// Dashboard module JavaScript
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Dashboard state
|
||||
const state = {
|
||||
dateRange: {
|
||||
startDate: new Date(),
|
||||
endDate: new Date()
|
||||
},
|
||||
salesData: [
|
||||
{ name: "Olivia Martin", email: "olivia.martin@email.com", amount: "+$1,999.00" },
|
||||
{ name: "Jackson Lee", email: "jackson.lee@email.com", amount: "+$39.00" },
|
||||
{ name: "Isabella Nguyen", email: "isabella.nguyen@email.com", amount: "+$299.00" },
|
||||
{ name: "William Kim", email: "will@email.com", amount: "+$99.00" },
|
||||
{ name: "Sofia Davis", email: "sofia.davis@email.com", amount: "+$39.00" },
|
||||
],
|
||||
cards: [
|
||||
{ title: "Total Revenue", value: "$45,231.89", subtext: "+20.1% from last month" },
|
||||
{ title: "Subscriptions", value: "+2350", subtext: "+180.1% from last month" },
|
||||
{ title: "Sales", value: "+12,234", subtext: "+19% from last month" },
|
||||
{ title: "Active Now", value: "+573", subtext: "+201 since last hour" },
|
||||
]
|
||||
};
|
||||
|
||||
// Initialize dashboard
|
||||
function init() {
|
||||
renderCards();
|
||||
document.querySelector('.download-btn').addEventListener('click', handleDownload);
|
||||
}
|
||||
|
||||
// Render dashboard cards
|
||||
function renderCards() {
|
||||
const container = document.querySelector('.cards-grid');
|
||||
container.innerHTML = state.cards.map(card => `
|
||||
<div class="card">
|
||||
<h3>${card.title}</h3>
|
||||
<p class="value">${card.value}</p>
|
||||
<p class="subtext">${card.subtext}</p>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// Handle download button click
|
||||
function handleDownload() {
|
||||
console.log('Downloading dashboard data...');
|
||||
}
|
||||
|
||||
// Format date helper
|
||||
function formatDate(date) {
|
||||
return date.toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: '2-digit',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
// Initialize dashboard
|
||||
init();
|
||||
});
|
||||
41
web/desktop/dashboard/index.html
Normal file
41
web/desktop/dashboard/index.html
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Dashboard</title>
|
||||
<link rel="stylesheet" href="../css/global.css">
|
||||
<link rel="stylesheet" href="dashboard.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-content">
|
||||
<div class="content-section active">
|
||||
<main class="container p-4 space-y-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="text-2xl font-bold text-foreground">Dashboard</h1>
|
||||
<div class="date-range-picker"></div>
|
||||
<button class="download-btn">Download</button>
|
||||
</div>
|
||||
|
||||
<div class="cards-grid">
|
||||
<!-- Cards will be populated by JavaScript -->
|
||||
</div>
|
||||
|
||||
<div class="dashboard-grid">
|
||||
<div class="overview-container">
|
||||
<h3>Overview</h3>
|
||||
<div class="overview-chart"></div>
|
||||
</div>
|
||||
<div class="recent-sales-container">
|
||||
<h3>Recent Sales</h3>
|
||||
<p>You made 265 sales this month.</p>
|
||||
<div class="recent-sales-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
<script src="dashboard.js"></script>
|
||||
<script src="components/date-range-picker.js"></script>
|
||||
<script src="components/overview.js"></script>
|
||||
<script src="components/recent-sales.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
66
web/desktop/editor/editor.css
Normal file
66
web/desktop/editor/editor.css
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/* Editor specific styles */
|
||||
#editor-content {
|
||||
min-height: 100%;
|
||||
outline: none;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
#editor-content h1,
|
||||
#editor-content h2,
|
||||
#editor-content h3,
|
||||
#editor-content p {
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
|
||||
#editor-content img {
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Ribbon button active state */
|
||||
.ribbon-button.active {
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
/* Ribbon group styling */
|
||||
.ribbon-group {
|
||||
padding: 0.5rem;
|
||||
border-right: 1px solid hsl(var(--border));
|
||||
&:last-child {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.ribbon-group-title {
|
||||
font-size: 0.8rem;
|
||||
text-align: center;
|
||||
margin-bottom: 0.5rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.ribbon-group-content {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* Page simulation */
|
||||
.page {
|
||||
width: 210mm;
|
||||
min-height: 297mm;
|
||||
background: white;
|
||||
box-shadow:
|
||||
0 0 0 1px hsl(var(--border)),
|
||||
0 4px 8px rgba(0, 0, 0, 0.1);
|
||||
position: relative;
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.page-number {
|
||||
position: absolute;
|
||||
top: -1.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
font-size: 0.8rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
77
web/desktop/editor/editor.js
Normal file
77
web/desktop/editor/editor.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Editor module JavaScript using Alpine.js
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('editor', () => ({
|
||||
fileName: 'Document 1',
|
||||
fontSize: '12',
|
||||
fontFamily: 'Calibri',
|
||||
textColor: '#000000',
|
||||
highlightColor: '#ffff00',
|
||||
activeTab: 'home',
|
||||
zoom: 100,
|
||||
pages: [1],
|
||||
content: '',
|
||||
|
||||
init() {
|
||||
// Initialize with default content
|
||||
this.content = `
|
||||
<h1 style="text-align: center; font-size: 24px; margin-bottom: 20px;">${this.fileName}</h1>
|
||||
<p><br></p>
|
||||
<p>Start typing your document here...</p>
|
||||
<p><br></p>
|
||||
`;
|
||||
},
|
||||
|
||||
// Ribbon tab switching
|
||||
setActiveTab(tab) {
|
||||
this.activeTab = tab;
|
||||
},
|
||||
|
||||
// Formatting methods
|
||||
formatBold() {
|
||||
document.execCommand('bold', false);
|
||||
},
|
||||
formatItalic() {
|
||||
document.execCommand('italic', false);
|
||||
},
|
||||
formatUnderline() {
|
||||
document.execCommand('underline', false);
|
||||
},
|
||||
alignLeft() {
|
||||
document.execCommand('justifyLeft', false);
|
||||
},
|
||||
alignCenter() {
|
||||
document.execCommand('justifyCenter', false);
|
||||
},
|
||||
alignRight() {
|
||||
document.execCommand('justifyRight', false);
|
||||
},
|
||||
alignJustify() {
|
||||
document.execCommand('justifyFull', false);
|
||||
},
|
||||
|
||||
// Zoom controls
|
||||
zoomOut() {
|
||||
this.zoom = Math.max(50, this.zoom - 10);
|
||||
this.updateZoom();
|
||||
},
|
||||
zoomIn() {
|
||||
this.zoom = Math.min(200, this.zoom + 10);
|
||||
this.updateZoom();
|
||||
},
|
||||
updateZoom() {
|
||||
document.querySelector('.pages-container').style.transform = `scale(${this.zoom / 100})`;
|
||||
},
|
||||
|
||||
// Save document
|
||||
saveDocument() {
|
||||
const content = document.getElementById('editor-content').innerHTML;
|
||||
const blob = new Blob([content], { type: 'text/html' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${this.fileName}.html`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
}));
|
||||
});
|
||||
89
web/desktop/editor/index.html
Normal file
89
web/desktop/editor/index.html
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Editor</title>
|
||||
<link rel="stylesheet" href="../css/global.css">
|
||||
<link rel="stylesheet" href="editor.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body x-data="editor" x-init="init()">
|
||||
<div id="main-content">
|
||||
<div class="content-section active">
|
||||
<!-- Quick Access Toolbar -->
|
||||
<div class="quick-access">
|
||||
<button class="quick-access-btn" @click="document.execCommand('undo', false)">
|
||||
<svg><!-- Undo icon --></svg>
|
||||
</button>
|
||||
<button class="quick-access-btn" @click="document.execCommand('redo', false)">
|
||||
<svg><!-- Redo icon --></svg>
|
||||
</button>
|
||||
<div class="title-controls">
|
||||
<input type="text" class="title-input" x-model="fileName" placeholder="Document name">
|
||||
<button class="quick-access-btn" @click="saveDocument">
|
||||
<svg><!-- Save icon --></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ribbon -->
|
||||
<div class="ribbon">
|
||||
<div class="ribbon-tabs">
|
||||
<button class="ribbon-tab-button"
|
||||
:class="{ 'active': activeTab === 'home' }"
|
||||
@click="setActiveTab('home')">Home</button>
|
||||
<button class="ribbon-tab-button"
|
||||
:class="{ 'active': activeTab === 'insert' }"
|
||||
@click="setActiveTab('insert')">Insert</button>
|
||||
<button class="ribbon-tab-button"
|
||||
:class="{ 'active': activeTab === 'view' }"
|
||||
@click="setActiveTab('view')">View</button>
|
||||
</div>
|
||||
|
||||
<div class="ribbon-content" x-show="activeTab === 'home'">
|
||||
<div class="ribbon-group">
|
||||
<div class="ribbon-group-title">Format</div>
|
||||
<div class="ribbon-group-content">
|
||||
<button class="ribbon-button" @click="formatBold">B</button>
|
||||
<button class="ribbon-button" @click="formatItalic">I</button>
|
||||
<button class="ribbon-button" @click="formatUnderline">U</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ribbon-group">
|
||||
<div class="ribbon-group-title">Alignment</div>
|
||||
<div class="ribbon-group-content">
|
||||
<button class="ribbon-button" @click="alignLeft">Left</button>
|
||||
<button class="ribbon-button" @click="alignCenter">Center</button>
|
||||
<button class="ribbon-button" @click="alignRight">Right</button>
|
||||
<button class="ribbon-button" @click="alignJustify">Justify</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Editor Area -->
|
||||
<div class="editor-container">
|
||||
<div class="editor-main">
|
||||
<div class="pages-container">
|
||||
<div class="page">
|
||||
<div class="page-number">Page 1</div>
|
||||
<div class="page-content" id="editor-content" contenteditable="true" x-html="content"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Status Bar -->
|
||||
<div class="status-bar">
|
||||
<div>Page <span x-text="pages.length"></span> of <span x-text="pages.length"></span></div>
|
||||
<div class="zoom-controls">
|
||||
<button @click="zoomOut">-</button>
|
||||
<span x-text="zoom + '%'"></span>
|
||||
<button @click="zoomIn">+</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="editor.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -16,6 +16,20 @@
|
|||
:class="{ active: current === 'tasks' }">✓ Tasks</a>
|
||||
<a href="#mail" @click.prevent="current = 'mail'; window.switchSection('mail')"
|
||||
:class="{ active: current === 'mail' }">✉ Mail</a>
|
||||
<a href="#dashboard" @click.prevent="current = 'dashboard'; window.switchSection('dashboard')"
|
||||
:class="{ active: current === 'dashboard' }">📊 Dashboard</a>
|
||||
<a href="#editor" @click.prevent="current = 'editor'; window.switchSection('editor')"
|
||||
:class="{ active: current === 'editor' }">📝 Editor</a>
|
||||
<a href="#player" @click.prevent="current = 'player'; window.switchSection('player')"
|
||||
:class="{ active: current === 'player' }">🎬 Player</a>
|
||||
<a href="#paper" @click.prevent="current = 'paper'; window.switchSection('paper')"
|
||||
:class="{ active: current === 'paper' }">📄 Paper</a>
|
||||
<a href="#settings" @click.prevent="current = 'settings'; window.switchSection('settings')"
|
||||
:class="{ active: current === 'settings' }">⚙️ Settings</a>
|
||||
<a href="#tables" @click.prevent="current = 'tables'; window.switchSection('tables')"
|
||||
:class="{ active: current === 'tables' }">📋 Tables</a>
|
||||
<a href="#news" @click.prevent="current = 'news'; window.switchSection('news')"
|
||||
:class="{ active: current === 'news' }">📰 News</a>
|
||||
</nav>
|
||||
|
||||
<div id="main-content">
|
||||
|
|
|
|||
19
web/desktop/news/index.html
Normal file
19
web/desktop/news/index.html
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>News</title>
|
||||
<link rel="stylesheet" href="../css/global.css">
|
||||
<link rel="stylesheet" href="news.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-content">
|
||||
<div class="content-section active">
|
||||
<div class="panel">
|
||||
<!-- News content will go here -->
|
||||
<h1>News</h1>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="news.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
web/desktop/news/news.css
Normal file
1
web/desktop/news/news.css
Normal file
|
|
@ -0,0 +1 @@
|
|||
/* News module specific styles */
|
||||
2
web/desktop/news/news.js
Normal file
2
web/desktop/news/news.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// News module JavaScript
|
||||
// Will be added as needed
|
||||
77
web/desktop/paper/index.html
Normal file
77
web/desktop/paper/index.html
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Paper</title>
|
||||
<link rel="stylesheet" href="../css/global.css">
|
||||
<link rel="stylesheet" href="paper.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body x-data="paper">
|
||||
<div id="main-content">
|
||||
<div class="content-section active">
|
||||
<div class="max-w-4xl mx-auto">
|
||||
<!-- Paper Shadow Effect -->
|
||||
<div class="mx-4 my-8 bg-card rounded-lg shadow-2xl shadow-black/20 border border-border">
|
||||
<div class="editor-content"
|
||||
x-ref="editor"
|
||||
contenteditable="true"
|
||||
x-init="initEditor()"
|
||||
class="min-h-[calc(100vh-12rem)] p-8 prose max-w-none focus:outline-none">
|
||||
Start writing your thoughts here...
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Floating Toolbar -->
|
||||
<div class="floating-toolbar" x-show="showToolbar" x-transition>
|
||||
<div class="flex items-center bg-card border border-border rounded-lg shadow-lg p-1">
|
||||
<!-- Text Formatting -->
|
||||
<button @click="formatText('bold')"
|
||||
class="p-2 rounded hover:bg-accent transition-colors"
|
||||
:class="{'bg-primary text-primary-foreground': isActive('bold')}">
|
||||
B
|
||||
</button>
|
||||
<button @click="formatText('italic')"
|
||||
class="p-2 rounded hover:bg-accent transition-colors"
|
||||
:class="{'bg-primary text-primary-foreground': isActive('italic')}">
|
||||
I
|
||||
</button>
|
||||
<button @click="formatText('underline')"
|
||||
class="p-2 rounded hover:bg-accent transition-colors"
|
||||
:class="{'bg-primary text-primary-foreground': isActive('underline')}">
|
||||
U
|
||||
</button>
|
||||
|
||||
<div class="w-px h-6 bg-border mx-1"></div>
|
||||
|
||||
<!-- Text Alignment -->
|
||||
<button @click="alignText('left')"
|
||||
class="p-2 rounded hover:bg-accent transition-colors"
|
||||
:class="{'bg-primary text-primary-foreground': isAligned('left')}">
|
||||
Left
|
||||
</button>
|
||||
<button @click="alignText('center')"
|
||||
class="p-2 rounded hover:bg-accent transition-colors"
|
||||
:class="{'bg-primary text-primary-foreground': isAligned('center')}">
|
||||
Center
|
||||
</button>
|
||||
<button @click="alignText('right')"
|
||||
class="p-2 rounded hover:bg-accent transition-colors"
|
||||
:class="{'bg-primary text-primary-foreground': isAligned('right')}">
|
||||
Right
|
||||
</button>
|
||||
|
||||
<div class="w-px h-6 bg-border mx-1"></div>
|
||||
|
||||
<!-- Link -->
|
||||
<button @click="addLink"
|
||||
class="p-2 rounded hover:bg-accent transition-colors">
|
||||
Link
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="paper.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
39
web/desktop/paper/paper.css
Normal file
39
web/desktop/paper/paper.css
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/* Paper specific styles */
|
||||
.editor-content {
|
||||
outline: none;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.prose {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.floating-toolbar {
|
||||
position: fixed;
|
||||
bottom: 2rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 100;
|
||||
/* Smooth transition */
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Paper shadow effect */
|
||||
.bg-card {
|
||||
background-color: hsl(var(--card));
|
||||
color: hsl(var(--card-foreground));
|
||||
}
|
||||
|
||||
.border-border {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
/* Toolbar button styles */
|
||||
button {
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
}
|
||||
52
web/desktop/paper/paper.js
Normal file
52
web/desktop/paper/paper.js
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
// Paper module JavaScript
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('paper', () => ({
|
||||
showToolbar: false,
|
||||
selection: null,
|
||||
|
||||
initEditor() {
|
||||
const editor = this.$refs.editor;
|
||||
|
||||
// Track selection for floating toolbar
|
||||
editor.addEventListener('mouseup', this.updateSelection.bind(this));
|
||||
editor.addEventListener('keyup', this.updateSelection.bind(this));
|
||||
|
||||
// Show/hide toolbar based on selection
|
||||
document.addEventListener('selectionchange', () => {
|
||||
const selection = window.getSelection();
|
||||
this.showToolbar = !selection.isCollapsed &&
|
||||
editor.contains(selection.anchorNode);
|
||||
});
|
||||
},
|
||||
|
||||
updateSelection() {
|
||||
this.selection = window.getSelection();
|
||||
},
|
||||
|
||||
formatText(format) {
|
||||
document.execCommand(format, false);
|
||||
this.updateSelection();
|
||||
},
|
||||
|
||||
alignText(align) {
|
||||
document.execCommand('justify' + align, false);
|
||||
this.updateSelection();
|
||||
},
|
||||
|
||||
isActive(format) {
|
||||
return document.queryCommandState(format);
|
||||
},
|
||||
|
||||
isAligned(align) {
|
||||
return document.queryCommandValue('justify' + align) === align;
|
||||
},
|
||||
|
||||
addLink() {
|
||||
const url = prompt('Enter URL:');
|
||||
if (url) {
|
||||
document.execCommand('createLink', false, url);
|
||||
}
|
||||
this.updateSelection();
|
||||
}
|
||||
}));
|
||||
});
|
||||
26
web/desktop/player/index.html
Normal file
26
web/desktop/player/index.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Player</title>
|
||||
<link rel="stylesheet" href="../css/global.css">
|
||||
<link rel="stylesheet" href="player.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="main-content">
|
||||
<div class="content-section active">
|
||||
<div class="player-container">
|
||||
<div class="video-container">
|
||||
<!-- Video element will be inserted here -->
|
||||
</div>
|
||||
<div class="player-controls">
|
||||
<button class="play-btn">Play</button>
|
||||
<input type="range" class="slider progress-slider" min="0" max="100" value="0">
|
||||
<span class="time-display">0:00 / 0:00</span>
|
||||
<input type="range" class="slider volume-slider" min="0" max="100" value="80">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="player.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
77
web/desktop/player/player.css
Normal file
77
web/desktop/player/player.css
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/* Player specific styles */
|
||||
.player-container {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.video-container {
|
||||
background: #000;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.video-container video {
|
||||
width: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.player-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.play-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.time-display {
|
||||
font-family: monospace;
|
||||
font-size: 0.9rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* Slider styles */
|
||||
.slider {
|
||||
flex: 1;
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
appearance: none;
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--primary));
|
||||
cursor: pointer;
|
||||
border: 2px solid hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
height: 12px;
|
||||
width: 12px;
|
||||
border-radius: 50%;
|
||||
background: hsl(var(--primary));
|
||||
cursor: pointer;
|
||||
border: 2px solid hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-track {
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.slider::-moz-range-track {
|
||||
height: 4px;
|
||||
cursor: pointer;
|
||||
background: hsl(var(--muted));
|
||||
border-radius: 2px;
|
||||
}
|
||||
61
web/desktop/player/player.js
Normal file
61
web/desktop/player/player.js
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
// Player module JavaScript
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const playerContainer = document.querySelector('.player-container');
|
||||
const videoContainer = document.querySelector('.video-container');
|
||||
const playBtn = document.querySelector('.play-btn');
|
||||
const progressSlider = document.querySelector('.progress-slider');
|
||||
const volumeSlider = document.querySelector('.volume-slider');
|
||||
const timeDisplay = document.querySelector('.time-display');
|
||||
|
||||
// Create video element
|
||||
const video = document.createElement('video');
|
||||
video.src = ''; // Will be set when loading media
|
||||
video.controls = false;
|
||||
videoContainer.appendChild(video);
|
||||
|
||||
// Play/Pause toggle
|
||||
playBtn.addEventListener('click', () => {
|
||||
if (video.paused) {
|
||||
video.play();
|
||||
playBtn.textContent = 'Pause';
|
||||
} else {
|
||||
video.pause();
|
||||
playBtn.textContent = 'Play';
|
||||
}
|
||||
});
|
||||
|
||||
// Update progress slider
|
||||
video.addEventListener('timeupdate', () => {
|
||||
const progress = (video.currentTime / video.duration) * 100;
|
||||
progressSlider.value = progress || 0;
|
||||
updateTimeDisplay();
|
||||
});
|
||||
|
||||
// Seek video
|
||||
progressSlider.addEventListener('input', () => {
|
||||
const seekTime = (progressSlider.value / 100) * video.duration;
|
||||
video.currentTime = seekTime;
|
||||
});
|
||||
|
||||
// Volume control
|
||||
volumeSlider.addEventListener('input', () => {
|
||||
video.volume = volumeSlider.value / 100;
|
||||
});
|
||||
|
||||
// Format time display
|
||||
function updateTimeDisplay() {
|
||||
const formatTime = (seconds) => {
|
||||
const mins = Math.floor(seconds / 60);
|
||||
const secs = Math.floor(seconds % 60);
|
||||
return `${mins}:${secs < 10 ? '0' : ''}${secs}`;
|
||||
};
|
||||
|
||||
timeDisplay.textContent = `${formatTime(video.currentTime)} / ${formatTime(video.duration)}`;
|
||||
}
|
||||
|
||||
// Load media (to be called externally)
|
||||
window.loadMedia = (src) => {
|
||||
video.src = src;
|
||||
video.load();
|
||||
};
|
||||
});
|
||||
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
74
web/desktop/settings/index.html
Normal file
74
web/desktop/settings/index.html
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Settings</title>
|
||||
<link rel="stylesheet" href="../css/global.css">
|
||||
<link rel="stylesheet" href="settings.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body x-data="settings">
|
||||
<div id="main-content">
|
||||
<div class="content-section active">
|
||||
<div class="space-y-6 p-4">
|
||||
<div>
|
||||
<h2 class="text-lg font-medium">Profile</h2>
|
||||
<p class="text-sm text-gray-500"></p>
|
||||
</div>
|
||||
<div class="border-t border-gray-200 my-4"></div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Username Field -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1">Username</label>
|
||||
<input class="w-full p-2 border rounded"
|
||||
x-model="username"
|
||||
placeholder="Enter username" />
|
||||
<template x-if="errors.username">
|
||||
<p class="text-red-500 text-xs mt-1" x-text="errors.username"></p>
|
||||
</template>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
This is your public display name. It can be your real name or a pseudonym.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Email Field -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1">Email</label>
|
||||
<input type="email"
|
||||
class="w-full p-2 border rounded"
|
||||
x-model="email"
|
||||
placeholder="Enter email" />
|
||||
<template x-if="errors.email">
|
||||
<p class="text-red-500 text-xs mt-1" x-text="errors.email"></p>
|
||||
</template>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
You can manage verified email addresses in your email settings.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Bio Field -->
|
||||
<div class="mb-4">
|
||||
<label class="block text-sm font-medium mb-1">Bio</label>
|
||||
<textarea class="w-full p-2 border rounded"
|
||||
x-model="bio"
|
||||
rows="4"
|
||||
placeholder="Tell us a little bit about yourself"></textarea>
|
||||
<template x-if="errors.bio">
|
||||
<p class="text-red-500 text-xs mt-1" x-text="errors.bio"></p>
|
||||
</template>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
You can @mention other users and organizations to link to them.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<button class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"
|
||||
@click="submitForm">
|
||||
Update profile
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="settings.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
33
web/desktop/settings/settings.css
Normal file
33
web/desktop/settings/settings.css
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
/* Settings specific styles */
|
||||
input, textarea {
|
||||
border-color: hsl(var(--border));
|
||||
background-color: hsl(var(--input));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
input:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--ring));
|
||||
box-shadow: 0 0 0 2px hsl(var(--ring)/0.2);
|
||||
}
|
||||
|
||||
button {
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.text-gray-500 {
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.border-gray-200 {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
.bg-blue-500 {
|
||||
background-color: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
}
|
||||
|
||||
.hover\:bg-blue-600:hover {
|
||||
background-color: hsl(var(--primary)/0.9);
|
||||
}
|
||||
53
web/desktop/settings/settings.js
Normal file
53
web/desktop/settings/settings.js
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
// Settings module JavaScript
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('settings', () => ({
|
||||
username: '',
|
||||
email: '',
|
||||
bio: '',
|
||||
errors: {},
|
||||
|
||||
submitForm() {
|
||||
this.errors = {};
|
||||
let isValid = true;
|
||||
|
||||
// Validate username
|
||||
if (this.username.length < 2) {
|
||||
this.errors.username = "Username must be at least 2 characters.";
|
||||
isValid = false;
|
||||
} else if (this.username.length > 30) {
|
||||
this.errors.username = "Username must not be longer than 30 characters.";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate email
|
||||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||||
if (!emailRegex.test(this.email)) {
|
||||
this.errors.email = "Please enter a valid email address.";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
// Validate bio
|
||||
if (this.bio.length < 4) {
|
||||
this.errors.bio = "Bio must be at least 4 characters.";
|
||||
isValid = false;
|
||||
} else if (this.bio.length > 160) {
|
||||
this.errors.bio = "Bio must not be longer than 160 characters.";
|
||||
isValid = false;
|
||||
}
|
||||
|
||||
if (isValid) {
|
||||
this.saveSettings();
|
||||
}
|
||||
},
|
||||
|
||||
saveSettings() {
|
||||
const settings = {
|
||||
username: this.username,
|
||||
email: this.email,
|
||||
bio: this.bio
|
||||
};
|
||||
console.log('Saving settings:', settings);
|
||||
// Here you would typically send the data to a server
|
||||
}
|
||||
}));
|
||||
});
|
||||
1
web/desktop/sources/README.md
Normal file
1
web/desktop/sources/README.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Prompts come from: https://github.com/0xeb/TheBigPromptLibrary
|
||||
1567
web/desktop/sources/prompts.csv
Normal file
1567
web/desktop/sources/prompts.csv
Normal file
File diff suppressed because it is too large
Load diff
67
web/desktop/tablesv2/index.html
Normal file
67
web/desktop/tablesv2/index.html
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Tables</title>
|
||||
<link rel="stylesheet" href="../css/global.css">
|
||||
<link rel="stylesheet" href="tables.css">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
</head>
|
||||
<body x-data="tablesApp()" x-init="init()">
|
||||
<div id="main-content">
|
||||
<div class="content-section active">
|
||||
<div class="app-container">
|
||||
<div class="content-container">
|
||||
<div class="header">
|
||||
<h1>📊 Tables</h1>
|
||||
<div class="subtitle">Excel Clone - Celebrating Lotus 1-2-3 Legacy 🎉</div>
|
||||
</div>
|
||||
|
||||
<div class="resizable-container">
|
||||
<div class="resizable-panel left" style="width: 30%">
|
||||
<!-- Left panel content -->
|
||||
</div>
|
||||
<div class="resizable-handle"></div>
|
||||
<div class="resizable-panel right" style="width: 70%">
|
||||
<div class="spreadsheet-content">
|
||||
<table>
|
||||
<thead id="tableHead"></thead>
|
||||
<tbody id="tableBody"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Formula bar -->
|
||||
<div class="formula-bar">
|
||||
<span id="cellRef">A1</span>
|
||||
<input type="text"
|
||||
id="formulaInput"
|
||||
placeholder="Enter formula..."
|
||||
@keypress.enter="updateCellValue($event.target.value)"
|
||||
x-model="formulaInputValue">
|
||||
</div>
|
||||
|
||||
<!-- Status bar -->
|
||||
<div class="status-bar">
|
||||
<span>Rows: <span id="rowCount" x-text="rows"></span></span>
|
||||
<span>Columns: <span id="colCount" x-text="cols"></span></span>
|
||||
</div>
|
||||
|
||||
<!-- Toolbar -->
|
||||
<div class="toolbar">
|
||||
<button @click="addRow()">Add Row</button>
|
||||
<button @click="addColumn()">Add Column</button>
|
||||
<button @click="deleteRow()">Delete Row</button>
|
||||
<button @click="deleteColumn()">Delete Column</button>
|
||||
<button @click="sort()">Sort</button>
|
||||
<button @click="sum()">Sum</button>
|
||||
<button @click="average()">Average</button>
|
||||
<button @click="exportData()">Export</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script src="tables.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
108
web/desktop/tablesv2/tables.css
Normal file
108
web/desktop/tablesv2/tables.css
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
/* Tables specific styles */
|
||||
.app-container {
|
||||
display: 100vh;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
flex: 1;
|
||||
overflow: 1rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: hsl(var(--muted-foreground));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.resizable-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.resizable-panel {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.resizable-handle {
|
||||
width: 4px;
|
||||
background: hsl(var(--border));
|
||||
cursor: col-resize;
|
||||
}
|
||||
|
||||
.spreadsheet-content {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
th, td {
|
||||
border: 1px solid hsl(var(--border));
|
||||
padding: 0.5rem;
|
||||
min-width: 100px;
|
||||
}
|
||||
|
||||
th {
|
||||
background: hsl(var(--muted));
|
||||
position: sticky;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
td.selected {
|
||||
background: hsl(var(--accent));
|
||||
color: hsl(var(--accent-foreground));
|
||||
}
|
||||
|
||||
.formula-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.formula-bar span {
|
||||
margin-right: 0.5rem;
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.formula-bar input {
|
||||
flex: 1;
|
||||
padding: 0.25rem;
|
||||
}
|
||||
|
||||
.status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.5rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem;
|
||||
border-top: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
.toolbar button {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: hsl(var(--primary));
|
||||
color: hsl(var(--primary-foreground));
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.toolbar button:hover {
|
||||
background: hsl(var(--primary)/0.9);
|
||||
}
|
||||
164
web/desktop/tablesv2/tables.js
Normal file
164
web/desktop/tablesv2/tables.js
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
// Tables module JavaScript
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('tablesApp', () => ({
|
||||
data: [],
|
||||
selectedCell: null,
|
||||
cols: 26,
|
||||
rows: 100,
|
||||
init() {
|
||||
this.data = this.generateMockData(this.rows, this.cols);
|
||||
this.renderTable();
|
||||
},
|
||||
|
||||
generateMockData(rows, cols) {
|
||||
const data = [];
|
||||
const products = ['Laptop', 'Mouse', 'Keyboard', 'Monitor', 'Headphones', 'Webcam', 'Desk', 'Chair'];
|
||||
const regions = ['North', 'South', 'East', 'West'];
|
||||
|
||||
for (let i = 0; i < rows; i++) {
|
||||
const row = {};
|
||||
for (let j = 0; j < cols; j++) {
|
||||
const col = this.getColumnName(j);
|
||||
if (i === 0) {
|
||||
if (j === 0) row[col] = 'Product';
|
||||
else if (j === 1) row[col] = 'Region';
|
||||
else if (j === 2) row[col] = 'Q1';
|
||||
else if (j === 3) row[col] = 'Q2';
|
||||
else if (j === 4) row[col] = 'Q3';
|
||||
else if (j === 5) row[col] = 'Q4';
|
||||
else if (j === 6) row[col] = 'Total';
|
||||
else row[col] = `Col ${col}`;
|
||||
} else {
|
||||
if (j === 0) row[col] = products[i % products.length];
|
||||
else if (j === 1) row[col] = regions[i % regions.length];
|
||||
else if (j >= 2 && j <= 5) row[col] = Math.floor(Math.random() * 10000) + 1000;
|
||||
else if (j === 6) {
|
||||
row[col] = `=C${i+1}+D${i+1}+E${i+1}+F${i+1}`;
|
||||
}
|
||||
else row[col] = Math.random() > 0.5 ? Math.floor(Math.random() * 1000) : '';
|
||||
}
|
||||
}
|
||||
data.push(row);
|
||||
}
|
||||
return data;
|
||||
},
|
||||
|
||||
getColumnName(index) {
|
||||
let name = '';
|
||||
while (index >= 0) {
|
||||
name = String.fromCharCode(65 + (index % 26)) + name;
|
||||
index = Math.floor(index / 26) - 1;
|
||||
}
|
||||
return name;
|
||||
},
|
||||
|
||||
selectCell(cell) {
|
||||
if (this.selectedCell) {
|
||||
this.selectedCell.classList.remove('selected');
|
||||
}
|
||||
this.selectedCell = cell;
|
||||
cell.classList.add('selected');
|
||||
|
||||
const cellRef = cell.dataset.cell;
|
||||
document.getElementById('cellRef').textContent = cellRef;
|
||||
|
||||
const row = parseInt(cell.dataset.row);
|
||||
const col = this.getColumnName(parseInt(cell.dataset.col));
|
||||
const value = this.data[row][col] || '';
|
||||
document.getElementById('formulaInput').value = value;
|
||||
},
|
||||
|
||||
updateCellValue(value) {
|
||||
if (!this.selectedCell) return;
|
||||
|
||||
const row = parseInt(this.selectedCell.dataset.row);
|
||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
||||
|
||||
this.data[row][col] = value;
|
||||
this.renderTable();
|
||||
|
||||
const newCell = document.querySelector(`td[data-row="${row}"][data-col="${this.selectedCell.dataset.col}"]`);
|
||||
if (newCell) this.selectCell(newCell);
|
||||
},
|
||||
|
||||
renderTable() {
|
||||
const thead = document.getElementById('tableHead');
|
||||
const tbody = document.getElementById('tableBody');
|
||||
|
||||
let headerHTML = '<tr><th></th>';
|
||||
for (let i = 0; i < this.cols; i++) {
|
||||
headerHTML += `<th>${this.getColumnName(i)}</th>`;
|
||||
}
|
||||
headerHTML += '</tr>';
|
||||
thead.innerHTML = headerHTML;
|
||||
|
||||
let bodyHTML = '';
|
||||
for (let i = 0; i < this.rows; i++) {
|
||||
bodyHTML += `<tr><th>${i + 1}</th>`;
|
||||
for (let j = 0; j < this.cols; j++) {
|
||||
const col = this.getColumnName(j);
|
||||
const value = this.data[i][col] || '';
|
||||
const displayValue = this.calculateCell(value, i, j);
|
||||
bodyHTML += `<td @click="selectCell($el)"
|
||||
data-row="${i}"
|
||||
data-col="${j}"
|
||||
data-cell="${col}${i+1}"
|
||||
:class="{ 'selected': selectedCell === $el }">
|
||||
${displayValue}
|
||||
</td>`;
|
||||
}
|
||||
bodyHTML += '</tr>';
|
||||
}
|
||||
tbody.innerHTML = bodyHTML;
|
||||
},
|
||||
|
||||
// Toolbar actions
|
||||
addRow() {
|
||||
const newRow = {};
|
||||
for (let i = 0; i < this.cols; i++) {
|
||||
newRow[this.getColumnName(i)] = '';
|
||||
}
|
||||
this.data.push(newRow);
|
||||
this.rows++;
|
||||
this.renderTable();
|
||||
},
|
||||
|
||||
addColumn() {
|
||||
const newCol = this.getColumnName(this.cols);
|
||||
this.data.forEach(row => row[newCol] = '');
|
||||
this.cols++;
|
||||
this.renderTable();
|
||||
},
|
||||
|
||||
deleteRow() {
|
||||
if (this.selectedCell && this.rows > 1) {
|
||||
const row = parseInt(this.selectedCell.dataset.row);
|
||||
this.data.splice(row, 1);
|
||||
this.rows--;
|
||||
this.renderTable();
|
||||
}
|
||||
},
|
||||
|
||||
deleteColumn() {
|
||||
if (this.selectedCell && this.cols > 1) {
|
||||
const col = this.getColumnName(parseInt(this.selectedCell.dataset.col));
|
||||
this.data.forEach(row => delete row[col]);
|
||||
this.cols--;
|
||||
this.renderTable();
|
||||
}
|
||||
},
|
||||
|
||||
exportData() {
|
||||
const csv = this.data.map(row => {
|
||||
return Object.values(row).join(',');
|
||||
}).join('\n');
|
||||
|
||||
const blob = new Blob([csv], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'tables_export.csv';
|
||||
a.click();
|
||||
}
|
||||
}));
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue