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:
Rodrigo Rodriguez (Pragmatismo) 2025-11-15 19:52:24 -03:00
parent 01e89c9358
commit b9395cb0d7
71 changed files with 3301 additions and 1891 deletions

View file

@ -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>

View file

@ -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>

View file

@ -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;
}
}

View file

@ -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: "MidCentury 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: "XTree 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);
// Clickoutside 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>

View file

@ -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>

View file

@ -1 +0,0 @@
- The UI shoule look exactly xtree gold but using shadcn with keyborad shortcut well explicit.

View file

@ -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>

View file

@ -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)
}

View file

@ -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
View 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;
}

View 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;
}

View 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;
}

View 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;
}

View 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);
});

View 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);
});

View 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);
});

View 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;
}

View 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();
});

View 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>

View 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));
}

View 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);
}
}));
});

View 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>

View file

@ -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">

View 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>

View file

@ -0,0 +1 @@
/* News module specific styles */

2
web/desktop/news/news.js Normal file
View file

@ -0,0 +1,2 @@
// News module JavaScript
// Will be added as needed

View 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>

View 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));
}

View 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();
}
}));
});

View 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>

View 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;
}

View 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();
};
});

View file

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

View 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>

View 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);
}

View 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
}
}));
});

View file

@ -0,0 +1 @@
Prompts come from: https://github.com/0xeb/TheBigPromptLibrary

File diff suppressed because it is too large Load diff

View 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>

View 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);
}

View 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();
}
}));
});