mirror of
https://github.com/Dvorinka/Trackeep.git
synced 2026-07-29 22:13:51 +00:00
first test
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
import { createSignal, For, Show } from 'solid-js'
|
||||
import { IconSend, IconX, IconBrain, IconUser, IconChevronDown } from '@tabler/icons-solidjs'
|
||||
import { AIProviderIcon } from '../AIProviderIcon'
|
||||
|
||||
interface AIChatPanelProps {
|
||||
isOpen: boolean
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
interface AIModel {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
provider: string
|
||||
category: string
|
||||
iconId?: string
|
||||
}
|
||||
|
||||
export function AIChatPanel(props: AIChatPanelProps) {
|
||||
const [messages, setMessages] = createSignal<Message[]>([
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
content: 'Hello! I\'m your AI assistant. How can I help you today?',
|
||||
timestamp: new Date()
|
||||
}
|
||||
])
|
||||
const [inputValue, setInputValue] = createSignal('')
|
||||
const [selectedModel, setSelectedModel] = createSignal('longcat-flash-chat')
|
||||
const [showModelPicker, setShowModelPicker] = createSignal(false)
|
||||
|
||||
const aiModels: AIModel[] = [
|
||||
{ id: 'longcat-flash-chat', name: 'LongCat Flash Chat', description: 'Fast and efficient', provider: 'longcat', category: 'fast', iconId: 'longcat' },
|
||||
{ id: 'mistral-standard', name: 'Mistral Standard', description: 'Mistral default model', provider: 'mistral', category: 'standard', iconId: 'mistral' },
|
||||
{ id: 'grok-standard', name: 'Grok Standard', description: 'Grok from X', provider: 'grok', category: 'standard', iconId: 'grok' },
|
||||
{ id: 'deepseek-chat', name: 'DeepSeek Chat', description: 'DeepSeek chat model', provider: 'deepseek', category: 'standard', iconId: 'deepseek' },
|
||||
{ id: 'ollama-local', name: 'Ollama Local', description: 'Local Ollama model', provider: 'ollama', category: 'local', iconId: 'ollama' },
|
||||
{ id: 'openrouter-auto', name: 'OpenRouter Auto', description: 'Router over many models', provider: 'openrouter', category: 'standard', iconId: 'openrouter' },
|
||||
]
|
||||
|
||||
const handleSendMessage = () => {
|
||||
const value = inputValue().trim()
|
||||
if (!value) return
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: value,
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, userMessage])
|
||||
setInputValue('')
|
||||
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: 'I understand your question. Let me help you with that...',
|
||||
timestamp: new Date()
|
||||
}
|
||||
setMessages(prev => [...prev, aiMessage])
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Chat Panel */}
|
||||
<div class={`fixed right-0 top-0 h-full bg-card border-l border-border shadow-xl transition-transform duration-300 z-50 ${
|
||||
props.isOpen ? 'translate-x-0' : 'translate-x-full'
|
||||
}`} style="width: min(420px, 100vw); max-width: 100vw;">
|
||||
{/* Header */}
|
||||
<div class="flex items-center justify-between p-4 border-b border-border">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="flex items-center justify-center p-2 rounded-lg bg-primary/10">
|
||||
<IconBrain class="size-5 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold">AI Assistant</h3>
|
||||
<p class="text-xs text-muted-foreground">Always here to help</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
{/* Close Button */}
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-inherit hover:bg-accent/50 hover:text-accent-foreground h-8 w-8"
|
||||
>
|
||||
<IconX class="size-4 text-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4" style="height: calc(100vh - 200px); max-height: calc(100vh - 200px);">
|
||||
<For each={messages()}>
|
||||
{(message) => (
|
||||
<div class={`flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'}`}>
|
||||
{message.role === 'assistant' && (
|
||||
<div class="flex items-center justify-center p-2 rounded-lg bg-muted flex-shrink-0">
|
||||
<IconBrain class="size-4 text-primary" />
|
||||
</div>
|
||||
)}
|
||||
<div class={`max-w-[280px] rounded-2xl p-3 ${
|
||||
message.role === 'user'
|
||||
? 'bg-primary text-primary-foreground rounded-br-sm'
|
||||
: 'bg-muted rounded-bl-sm'
|
||||
}`}>
|
||||
<p class="text-sm leading-relaxed">{message.content}</p>
|
||||
<p class="text-xs opacity-70 mt-2">
|
||||
{message.timestamp.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
|
||||
</p>
|
||||
</div>
|
||||
{message.role === 'user' && (
|
||||
<div class="flex items-center justify-center p-2 rounded-lg bg-primary flex-shrink-0">
|
||||
<IconUser class="size-4 text-primary-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div class="p-4 border-t border-border bg-card">
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue()}
|
||||
onInput={(e) => setInputValue(e.currentTarget.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type your message..."
|
||||
class="flex-1 h-10 w-full rounded-full border border-input bg-transparent px-4 py-2 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputValue().trim()}
|
||||
class="inline-flex items-center justify-center rounded-full text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-10 w-10"
|
||||
>
|
||||
<IconSend class="size-4 text-primary-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Model Picker at Bottom */}
|
||||
<div class="mt-3 pt-3 border-t border-border">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="relative">
|
||||
<button
|
||||
onClick={() => setShowModelPicker(!showModelPicker())}
|
||||
class="flex items-center gap-2 px-3 py-1.5 bg-muted hover:bg-muted/80 rounded-full text-xs transition-colors"
|
||||
>
|
||||
<Show when={aiModels.find(m => m.id === selectedModel())?.iconId}>
|
||||
<AIProviderIcon
|
||||
providerId={aiModels.find(m => m.id === selectedModel())?.iconId || 'longcat'}
|
||||
size="1rem"
|
||||
class="rounded-full"
|
||||
/>
|
||||
</Show>
|
||||
<Show when={!aiModels.find(m => m.id === selectedModel())?.iconId}>
|
||||
<div class="w-4 h-4 rounded-full bg-gradient-to-r from-blue-500 to-purple-500"></div>
|
||||
</Show>
|
||||
<span class="text-muted-foreground">
|
||||
{aiModels.find(m => m.id === selectedModel())?.name?.split(' ')[0] || 'AI'}
|
||||
</span>
|
||||
<IconChevronDown class={`size-3 transition-transform ${showModelPicker() ? 'rotate-180' : ''}`} />
|
||||
</button>
|
||||
|
||||
<Show when={showModelPicker()}>
|
||||
<div class="absolute bottom-full left-0 mb-2 w-64 bg-background border rounded-lg shadow-lg z-50 p-1 max-h-48 overflow-y-auto">
|
||||
<For each={aiModels}>
|
||||
{model => (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedModel(model.id)
|
||||
setShowModelPicker(false)
|
||||
}}
|
||||
class={`w-full text-left p-2 rounded text-xs transition-colors ${
|
||||
selectedModel() === model.id
|
||||
? 'bg-primary/10 border border-primary/20'
|
||||
: 'hover:bg-muted'
|
||||
}`}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Show when={model.iconId}>
|
||||
<AIProviderIcon
|
||||
providerId={model.iconId!}
|
||||
size="0.75rem"
|
||||
class="rounded-full flex-shrink-0"
|
||||
/>
|
||||
</Show>
|
||||
<Show when={!model.iconId}>
|
||||
<div class={`w-3 h-3 rounded-full flex-shrink-0 ${
|
||||
model.provider === 'LongCat' ? 'bg-gradient-to-r from-orange-500 to-red-500' :
|
||||
model.provider === 'OpenAI' ? 'bg-gradient-to-r from-green-500 to-emerald-500' :
|
||||
'bg-gradient-to-r from-purple-500 to-pink-500'
|
||||
}`}></div>
|
||||
</Show>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="font-medium truncate">{model.name}</div>
|
||||
<div class="text-muted-foreground text-xs truncate">{model.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 text-xs text-muted-foreground">
|
||||
<span>{aiModels.find(m => m.id === selectedModel())?.provider || 'LongCat'}</span>
|
||||
<a href="/app/settings" class="text-primary hover:underline">
|
||||
AI settings
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { IconX, IconSend, IconUser, IconChevronDown } from '@tabler/icons-solidjs'
|
||||
import longcatIcon from '@/assets/longcat-color.svg'
|
||||
|
||||
interface FloatingAIProps {
|
||||
onToggleChat: () => void
|
||||
isChatOpen: boolean
|
||||
}
|
||||
|
||||
interface Message {
|
||||
id: string
|
||||
role: 'user' | 'assistant'
|
||||
content: string
|
||||
timestamp: Date
|
||||
}
|
||||
|
||||
export function FloatingAI(props: FloatingAIProps) {
|
||||
const [messages, setMessages] = createSignal<Message[]>([
|
||||
{
|
||||
id: '1',
|
||||
role: 'assistant',
|
||||
content: 'Hello! I\'m your AI assistant. How can I help you today?',
|
||||
timestamp: new Date()
|
||||
}
|
||||
])
|
||||
const [inputValue, setInputValue] = createSignal('')
|
||||
const [selectedModel, setSelectedModel] = createSignal('longcat-flash-chat')
|
||||
const [showModelSelector, setShowModelSelector] = createSignal(false)
|
||||
|
||||
const aiModels = [
|
||||
{ id: 'longcat-flash-chat', name: 'LongCat Flash', description: 'Fast and efficient' },
|
||||
{ id: 'gpt-4', name: 'GPT-4', description: 'Most capable' },
|
||||
{ id: 'claude-3', name: 'Claude 3', description: 'Balanced performance' }
|
||||
]
|
||||
|
||||
const handleSendMessage = () => {
|
||||
const value = inputValue().trim()
|
||||
if (!value) return
|
||||
|
||||
const userMessage: Message = {
|
||||
id: Date.now().toString(),
|
||||
role: 'user',
|
||||
content: value,
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
setMessages(prev => [...prev, userMessage])
|
||||
setInputValue('')
|
||||
|
||||
// Simulate AI response
|
||||
setTimeout(() => {
|
||||
const aiMessage: Message = {
|
||||
id: (Date.now() + 1).toString(),
|
||||
role: 'assistant',
|
||||
content: 'I understand your question. Let me help you with that...',
|
||||
timestamp: new Date()
|
||||
}
|
||||
setMessages(prev => [...prev, aiMessage])
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const handleKeyPress = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault()
|
||||
handleSendMessage()
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Floating AI Button */}
|
||||
<button
|
||||
onClick={props.onToggleChat}
|
||||
class="fixed bottom-6 right-8 z-40 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-all duration-200 hover:scale-110 w-14 h-14"
|
||||
title="AI Assistant"
|
||||
>
|
||||
<img src={longcatIcon} alt="AI Assistant" class="size-6" />
|
||||
</button>
|
||||
|
||||
{/* AI Chat Modal */}
|
||||
<Show when={props.isChatOpen}>
|
||||
<div class="fixed inset-0 bg-black/50 z-50 flex items-center justify-center p-4">
|
||||
<div class="bg-card border border-border rounded-lg shadow-xl max-w-md w-full max-h-[600px] flex flex-col" style="width: 420px;">
|
||||
{/* Header */}
|
||||
<div class="flex items-center justify-between p-4 border-b border-border bg-gradient-to-r from-primary/10 to-primary/5">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="flex items-center justify-center p-3 rounded-lg bg-primary/20">
|
||||
<img src={longcatIcon} alt="AI Assistant" class="size-5" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-semibold text-foreground">AI Assistant</h3>
|
||||
<div class="flex items-center gap-2">
|
||||
<p class="text-xs text-muted-foreground">Always here to help</p>
|
||||
<div class="relative">
|
||||
<button
|
||||
onClick={() => setShowModelSelector(!showModelSelector())}
|
||||
class="flex items-center gap-1 text-xs text-primary hover:text-primary/80 transition-colors"
|
||||
>
|
||||
{aiModels.find(m => m.id === selectedModel())?.name || 'LongCat Flash'}
|
||||
<IconChevronDown class="size-3" />
|
||||
</button>
|
||||
|
||||
{/* Model Selector Dropdown */}
|
||||
<Show when={showModelSelector()}>
|
||||
<div class="absolute bottom-full left-0 mb-2 w-48 bg-popover border border-border rounded-md shadow-lg z-10">
|
||||
{aiModels.map((model) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedModel(model.id)
|
||||
setShowModelSelector(false)
|
||||
}}
|
||||
class="w-full text-left px-3 py-2 text-sm hover:bg-accent transition-colors first:rounded-t-md last:rounded-b-md"
|
||||
>
|
||||
<div class="font-medium">{model.name}</div>
|
||||
<div class="text-xs text-muted-foreground">{model.description}</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={props.onToggleChat}
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-inherit hover:bg-accent/50 hover:text-accent-foreground h-8 w-8"
|
||||
>
|
||||
<IconX class="size-4 text-foreground" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
<div class="flex-1 overflow-y-auto p-4 space-y-4 bg-gradient-to-b from-background to-muted/20" style="max-height: 400px;">
|
||||
{messages().map((message) => (
|
||||
<div class={`flex gap-3 ${message.role === 'user' ? 'justify-end' : 'justify-start'} animate-in slide-in-from-bottom-2 duration-200`}>
|
||||
{message.role === 'assistant' && (
|
||||
<div class="flex items-center justify-center p-2 rounded-lg bg-primary/10 flex-shrink-0">
|
||||
<img src={longcatIcon} alt="AI Assistant" class="size-4" />
|
||||
</div>
|
||||
)}
|
||||
<div class={`max-w-[300px] rounded-lg p-3 shadow-sm ${
|
||||
message.role === 'user'
|
||||
? 'bg-primary text-primary-foreground ml-auto'
|
||||
: 'bg-muted border border-border'
|
||||
}`}>
|
||||
<p class="text-sm leading-relaxed">{message.content}</p>
|
||||
<p class="text-xs opacity-70 mt-2">
|
||||
{message.timestamp.toLocaleTimeString()}
|
||||
</p>
|
||||
</div>
|
||||
{message.role === 'user' && (
|
||||
<div class="flex items-center justify-center p-2 rounded-lg bg-primary flex-shrink-0">
|
||||
<IconUser class="size-4 text-primary-foreground" />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Input */}
|
||||
<div class="p-4 border-t border-border bg-muted/30">
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={inputValue()}
|
||||
onInput={(e) => setInputValue(e.currentTarget.value)}
|
||||
onKeyPress={handleKeyPress}
|
||||
placeholder="Type your message..."
|
||||
class="flex-1 h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSendMessage}
|
||||
disabled={!inputValue().trim()}
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 hover:shadow-md h-10 px-4"
|
||||
>
|
||||
<IconSend class="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,15 @@
|
||||
import {
|
||||
IconBell,
|
||||
IconSearch,
|
||||
IconPlus,
|
||||
IconUpload,
|
||||
IconMoon,
|
||||
IconLogout,
|
||||
IconUser
|
||||
IconSettings,
|
||||
IconMenu2
|
||||
} from '@tabler/icons-solidjs'
|
||||
import { Button } from '@/components/ui/Button'
|
||||
import { Input } from '@/components/ui/Input'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { QuickSearch } from '@/components/search/QuickSearch'
|
||||
import { ColorSwitcherDropdown } from '@/components/ui/ColorSwitcherDropdown'
|
||||
import { UploadModal } from '@/components/ui/UploadModal'
|
||||
import { UserProfileDropdown } from '@/components/ui/UserProfileDropdown'
|
||||
import { createSignal } from 'solid-js'
|
||||
import { useAuth } from '@/lib/auth'
|
||||
|
||||
export interface HeaderProps {
|
||||
@@ -16,70 +17,102 @@ export interface HeaderProps {
|
||||
title?: string
|
||||
}
|
||||
|
||||
export function Header(props: HeaderProps) {
|
||||
const { authState, logout } = useAuth();
|
||||
export function Header(_props: HeaderProps) {
|
||||
const [showUploadModal, setShowUploadModal] = createSignal(false);
|
||||
const { authState, updateProfile } = useAuth();
|
||||
|
||||
const handleLogout = async () => {
|
||||
await logout();
|
||||
const handleThemeToggle = async () => {
|
||||
const currentTheme = document.documentElement.getAttribute('data-kb-theme');
|
||||
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
||||
|
||||
// Apply theme immediately
|
||||
if (newTheme === 'dark') {
|
||||
document.documentElement.setAttribute('data-kb-theme', 'dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-kb-theme');
|
||||
}
|
||||
|
||||
// Save to localStorage
|
||||
localStorage.setItem('theme', newTheme);
|
||||
|
||||
// Update user profile if authenticated
|
||||
if (authState.isAuthenticated && authState.user) {
|
||||
try {
|
||||
await updateProfile({ theme: newTheme });
|
||||
} catch (error) {
|
||||
console.error('Failed to update theme in profile:', error);
|
||||
// Still keep the local theme change even if profile update fails
|
||||
}
|
||||
}
|
||||
|
||||
// Reload the page so all Papra CSS and color schemes re-initialize
|
||||
// and the theme change is fully applied without manual refresh
|
||||
window.location.reload();
|
||||
};
|
||||
|
||||
return (
|
||||
<header class={cn('flex h-16 items-center justify-between border-b border-[#262626] bg-[#141415] px-6', props.class)}>
|
||||
{/* Page Title */}
|
||||
<div class="flex items-center space-x-4">
|
||||
<h1 class="text-xl font-semibold text-[#fafafa]">
|
||||
{props.title || 'Dashboard'}
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Search and Actions */}
|
||||
<div class="flex items-center space-x-4">
|
||||
{/* Search */}
|
||||
<div class="relative">
|
||||
<IconSearch class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-[#a3a3a3]" />
|
||||
<Input
|
||||
type="search"
|
||||
placeholder="Search bookmarks, tasks, files..."
|
||||
class="w-80 pl-10 bg-[#141415] border-[#262626] text-[#fafafa] placeholder-[#a3a3a3]"
|
||||
/>
|
||||
<>
|
||||
<div class="flex justify-between px-6 pt-4 pb-4">
|
||||
{/* Left side */}
|
||||
<div class="flex items-center">
|
||||
{/* Mobile menu button */}
|
||||
<button type="button" aria-haspopup="dialog" aria-expanded="false" data-closed="" class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-inherit hover:bg-accent/50 hover:text-accent-foreground h-9 w-9 md:hidden mr-2">
|
||||
<IconMenu2 class="size-6" />
|
||||
</button>
|
||||
|
||||
{/* Quick Search */}
|
||||
<QuickSearch />
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div class="flex items-center space-x-2">
|
||||
<Button variant="ghost" size="icon" class="text-[#a3a3a3] hover:text-[#fafafa]">
|
||||
<IconPlus class="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="icon" class="text-[#a3a3a3] hover:text-[#fafafa]">
|
||||
<IconBell class="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
<Button variant="ghost" size="icon" class="text-[#a3a3a3] hover:text-[#fafafa]">
|
||||
<IconMoon class="h-5 w-5" />
|
||||
</Button>
|
||||
|
||||
{/* User Menu */}
|
||||
<div class="flex items-center space-x-2 pl-4 border-l border-[#262626]">
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="text-right">
|
||||
<p class="text-sm font-medium text-[#fafafa]">{authState.user?.full_name || authState.user?.username}</p>
|
||||
<p class="text-xs text-[#a3a3a3]">{authState.user?.email}</p>
|
||||
</div>
|
||||
<Button variant="ghost" size="icon" class="text-[#a3a3a3] hover:text-[#fafafa]">
|
||||
<IconUser class="h-5 w-5" />
|
||||
</Button>
|
||||
{/* Right side */}
|
||||
<div class="flex items-center gap-2">
|
||||
{/* Drop zone overlay */}
|
||||
<div class="fixed top-0 left-0 w-screen h-screen z-80 bg-background bg-opacity-50 backdrop-blur transition-colors hidden">
|
||||
<div class="flex items-center justify-center h-full text-center flex-col">
|
||||
<IconPlus class="text-6xl text-muted-foreground mx-auto" />
|
||||
<div class="text-xl my-2 font-semibold text-muted-foreground">Drop files here</div>
|
||||
<div class="text-base text-muted-foreground">Drag and drop files here to import them</div>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="text-[#a3a3a3] hover:text-[#fafafa]"
|
||||
onClick={handleLogout}
|
||||
>
|
||||
<IconLogout class="h-5 w-5" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Import button */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowUploadModal(true)}
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2"
|
||||
>
|
||||
<IconUpload class="size-4" />
|
||||
<span class="hidden sm:inline ml-2">Import a document</span>
|
||||
</button>
|
||||
|
||||
{/* Color switcher dropdown */}
|
||||
<ColorSwitcherDropdown />
|
||||
|
||||
{/* Theme switcher */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleThemeToggle}
|
||||
class="items-center justify-center rounded-md font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-8 px-3 py-1 text-base hidden sm:flex"
|
||||
>
|
||||
<IconMoon class="size-4" />
|
||||
</button>
|
||||
|
||||
{/* Admin link */}
|
||||
<a href="/app/admin-settings" class="items-center justify-center rounded-md text-sm font-medium transition-shadow focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground h-9 px-4 py-2 hidden sm:flex gap-2">
|
||||
<IconSettings class="size-4" />
|
||||
Admin
|
||||
</a>
|
||||
|
||||
{/* User menu */}
|
||||
<UserProfileDropdown />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Upload Modal */}
|
||||
<UploadModal
|
||||
isOpen={showUploadModal()}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,31 +1,180 @@
|
||||
import { children } from 'solid-js'
|
||||
import { children, createSignal, onMount } from 'solid-js'
|
||||
import { Sidebar } from './Sidebar'
|
||||
import { Header } from './Header'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { AIChatPanel } from './AIChatPanel'
|
||||
import { UpdateNotification } from '../ui/UpdateNotification'
|
||||
import { IconBrain } from '@tabler/icons-solidjs'
|
||||
|
||||
export interface LayoutProps {
|
||||
children: any
|
||||
title?: string
|
||||
class?: string
|
||||
fullBleed?: boolean
|
||||
}
|
||||
|
||||
export function Layout(props: LayoutProps) {
|
||||
const resolved = children(() => props.children)
|
||||
const [isChatOpen, setIsChatOpen] = createSignal(false)
|
||||
|
||||
onMount(() => {
|
||||
// Initialize dark mode from localStorage or system preference
|
||||
const savedTheme = localStorage.getItem('theme')
|
||||
const systemPrefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches
|
||||
|
||||
if (savedTheme === 'dark' || (!savedTheme && systemPrefersDark)) {
|
||||
document.documentElement.setAttribute('data-kb-theme', 'dark')
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-kb-theme')
|
||||
}
|
||||
|
||||
// Initialize color scheme from localStorage
|
||||
const savedColorScheme = localStorage.getItem('colorScheme');
|
||||
const savedCustomColors = localStorage.getItem('customColors');
|
||||
|
||||
if (savedColorScheme === 'custom' && savedCustomColors) {
|
||||
try {
|
||||
const colors = JSON.parse(savedCustomColors);
|
||||
|
||||
// Apply custom colors
|
||||
const hexToHsl = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (!result) return '0 0% 100%';
|
||||
|
||||
let r = parseInt(result[1], 16) / 255;
|
||||
let g = parseInt(result[2], 16) / 255;
|
||||
let b = parseInt(result[3], 16) / 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0, s = 0, l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
||||
};
|
||||
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--primary', hexToHsl(colors.primary));
|
||||
root.style.setProperty('--background', hexToHsl(colors.background));
|
||||
root.style.setProperty('--foreground', hexToHsl(colors.foreground));
|
||||
root.style.setProperty('--muted', hexToHsl(colors.muted));
|
||||
root.style.setProperty('--border', colors.border);
|
||||
|
||||
// Also set as CSS custom properties for direct use
|
||||
root.style.setProperty('--colors-primary', hexToHsl(colors.primary));
|
||||
root.style.setProperty('--colors-background', hexToHsl(colors.background));
|
||||
root.style.setProperty('--colors-foreground', hexToHsl(colors.foreground));
|
||||
root.style.setProperty('--colors-muted', hexToHsl(colors.muted));
|
||||
root.style.setProperty('--colors-border', colors.border);
|
||||
} catch (e) {
|
||||
console.error('Failed to load custom colors:', e);
|
||||
}
|
||||
} else if (savedColorScheme) {
|
||||
// Apply predefined scheme
|
||||
const predefinedSchemes: Record<string, any> = {
|
||||
'default': { primary: '#5ab9ff', background: savedTheme === 'dark' ? '#1a1a1a' : '#ffffff', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#262727' : '#f5f5f5', border: '#262626' },
|
||||
'ocean': { primary: '#0077be', background: savedTheme === 'dark' ? '#001f3f' : '#e6f3ff', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#003366' : '#cce7ff', border: '#004080' },
|
||||
'forest': { primary: '#228b22', background: savedTheme === 'dark' ? '#0d2818' : '#f0f8f0', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#1a431a' : '#d4edd4', border: '#2d5a2d' },
|
||||
'sunset': { primary: '#ff6b35', background: savedTheme === 'dark' ? '#2c1810' : '#fff5f0', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#5c2e00' : '#ffe4d6', border: '#8b4513' },
|
||||
'purple': { primary: '#8b5cf6', background: savedTheme === 'dark' ? '#1a0033' : '#f8f5ff', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#330066' : '#ede9fe', border: '#4d0099' },
|
||||
'rose': { primary: '#f43f5e', background: savedTheme === 'dark' ? '#2d1111' : '#fff1f2', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#5a1a1a' : '#ffe4e6', border: '#881337' },
|
||||
'amber': { primary: '#f59e0b', background: savedTheme === 'dark' ? '#2d1a00' : '#fffbeb', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#5c4a00' : '#fef3c7', border: '#78350f' },
|
||||
'emerald': { primary: '#10b981', background: savedTheme === 'dark' ? '#022c22' : '#ecfdf5', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#064e3b' : '#d1fae5', border: '#047857' },
|
||||
'cyan': { primary: '#06b6d4', background: savedTheme === 'dark' ? '#022c3a' : '#ecfeff', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#164e63' : '#cffafe', border: '#0891b2' },
|
||||
'indigo': { primary: '#6366f1', background: savedTheme === 'dark' ? '#1e1b4b' : '#eef2ff', foreground: savedTheme === 'dark' ? '#ffffff' : '#000000', muted: savedTheme === 'dark' ? '#312e81' : '#e0e7ff', border: '#4338ca' }
|
||||
};
|
||||
|
||||
const scheme = predefinedSchemes[savedColorScheme];
|
||||
if (scheme) {
|
||||
const hexToHsl = (hex: string) => {
|
||||
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
||||
if (!result) return '0 0% 100%';
|
||||
|
||||
let r = parseInt(result[1], 16) / 255;
|
||||
let g = parseInt(result[2], 16) / 255;
|
||||
let b = parseInt(result[3], 16) / 255;
|
||||
|
||||
const max = Math.max(r, g, b);
|
||||
const min = Math.min(r, g, b);
|
||||
let h = 0, s = 0, l = (max + min) / 2;
|
||||
|
||||
if (max !== min) {
|
||||
const d = max - min;
|
||||
s = l > 0.5 ? d / (2 - max - min) : d / (max + min);
|
||||
|
||||
switch (max) {
|
||||
case r: h = ((g - b) / d + (g < b ? 6 : 0)) / 6; break;
|
||||
case g: h = ((b - r) / d + 2) / 6; break;
|
||||
case b: h = ((r - g) / d + 4) / 6; break;
|
||||
}
|
||||
}
|
||||
|
||||
return `${Math.round(h * 360)} ${Math.round(s * 100)}% ${Math.round(l * 100)}%`;
|
||||
};
|
||||
|
||||
const root = document.documentElement;
|
||||
root.style.setProperty('--primary', hexToHsl(scheme.primary));
|
||||
root.style.setProperty('--background', hexToHsl(scheme.background));
|
||||
root.style.setProperty('--foreground', hexToHsl(scheme.foreground));
|
||||
root.style.setProperty('--muted', hexToHsl(scheme.muted));
|
||||
root.style.setProperty('--border', scheme.border);
|
||||
|
||||
// Also set as CSS custom properties for direct use
|
||||
root.style.setProperty('--colors-primary', hexToHsl(scheme.primary));
|
||||
root.style.setProperty('--colors-background', hexToHsl(scheme.background));
|
||||
root.style.setProperty('--colors-foreground', hexToHsl(scheme.foreground));
|
||||
root.style.setProperty('--colors-muted', hexToHsl(scheme.muted));
|
||||
root.style.setProperty('--colors-border', hexToHsl(scheme.border));
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const toggleChat = () => {
|
||||
setIsChatOpen(!isChatOpen())
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={cn('flex h-screen bg-[#18181b]', props.class)}>
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
<div class="min-h-screen font-sans text-sm font-400 bg-background text-foreground">
|
||||
{/* Update Notification - Above everything */}
|
||||
<UpdateNotification />
|
||||
|
||||
{/* Main Content */}
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<Header title={props.title} />
|
||||
<div class="flex flex-row h-screen min-h-0">
|
||||
{/* Sidebar */}
|
||||
<Sidebar />
|
||||
|
||||
{/* Page Content */}
|
||||
<main class="flex-1 overflow-y-auto bg-[#18181b] p-6">
|
||||
{resolved()}
|
||||
</main>
|
||||
{/* Main Content */}
|
||||
<div class="flex-1 min-h-0 flex flex-col">
|
||||
{/* Header */}
|
||||
<Header title={props.title} />
|
||||
|
||||
{/* Page Content */}
|
||||
<main class="flex-1 overflow-auto max-w-screen">
|
||||
<div class={props.fullBleed ? "h-full" : "p-2 max-w-7xl mx-auto"}>
|
||||
{resolved()}
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Floating AI Button */}
|
||||
<button
|
||||
onClick={toggleChat}
|
||||
class="fixed bottom-6 right-8 z-40 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground shadow-lg hover:bg-primary/90 transition-all duration-200 hover:scale-110 w-14 h-14"
|
||||
title="AI Assistant"
|
||||
>
|
||||
<IconBrain class="size-6 text-primary-foreground" />
|
||||
</button>
|
||||
|
||||
{/* AI Chat Panel */}
|
||||
<AIChatPanel isOpen={isChatOpen()} onClose={() => setIsChatOpen(false)} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -1,70 +1,263 @@
|
||||
import { For } from 'solid-js'
|
||||
import { A } from '@solidjs/router'
|
||||
import { For, createSignal, onMount, Show } from 'solid-js'
|
||||
import { A, useLocation } from '@solidjs/router'
|
||||
import {
|
||||
IconBookmark,
|
||||
IconChecklist,
|
||||
IconFolder,
|
||||
IconHome,
|
||||
IconNotebook,
|
||||
IconSettings
|
||||
IconSettings,
|
||||
IconVideo,
|
||||
IconFileText,
|
||||
IconChevronDown,
|
||||
IconTrash,
|
||||
IconUsers,
|
||||
IconBrain,
|
||||
IconSchool,
|
||||
IconChartLine,
|
||||
IconBrandGithub,
|
||||
IconClock,
|
||||
IconCalendar,
|
||||
IconLogout,
|
||||
IconBuilding,
|
||||
IconPlus
|
||||
} from '@tabler/icons-solidjs'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { UpdateChecker } from '../ui/UpdateChecker'
|
||||
|
||||
const navigation = [
|
||||
{ name: 'Dashboard', href: '/app', icon: IconHome },
|
||||
{ name: 'Home', href: '/app', icon: IconHome },
|
||||
{ name: 'Bookmarks', href: '/app/bookmarks', icon: IconBookmark },
|
||||
{ name: 'Tasks', href: '/app/tasks', icon: IconChecklist },
|
||||
{ name: 'Time Tracking', href: '/app/time-tracking', icon: IconClock },
|
||||
{ name: 'Calendar', href: '/app/calendar', icon: IconCalendar },
|
||||
{ name: 'Files', href: '/app/files', icon: IconFolder },
|
||||
{ name: 'Notes', href: '/app/notes', icon: IconNotebook },
|
||||
{ name: 'Settings', href: '/app/settings', icon: IconSettings },
|
||||
{ name: 'YouTube', href: '/app/youtube', icon: IconVideo },
|
||||
{ name: 'Members', href: '/app/members', icon: IconUsers },
|
||||
{ name: 'Learning', href: '/app/learning-paths', icon: IconSchool },
|
||||
{ name: 'Stats', href: '/app/stats', icon: IconChartLine },
|
||||
{ name: 'GitHub', href: '/app/github', icon: IconBrandGithub },
|
||||
{ name: 'AI Assistant', href: '/app/chat', icon: IconBrain },
|
||||
]
|
||||
|
||||
const mockWorkspaces = [
|
||||
{ id: '1', name: 'Trackeep Workspace', icon: IconFileText },
|
||||
{ id: '2', name: 'Personal Projects', icon: IconBuilding },
|
||||
{ id: '3', name: 'Team Collaboration', icon: IconUsers },
|
||||
]
|
||||
|
||||
export interface SidebarProps {
|
||||
class?: string
|
||||
}
|
||||
|
||||
export function Sidebar(props: SidebarProps) {
|
||||
export function Sidebar(_props: SidebarProps) {
|
||||
const location = useLocation()
|
||||
const [isWorkspaceDropdownOpen, setIsWorkspaceDropdownOpen] = createSignal(false)
|
||||
const [selectedWorkspace, setSelectedWorkspace] = createSignal(mockWorkspaces[0])
|
||||
|
||||
const isActive = (href: string) => {
|
||||
const currentPath = location.pathname
|
||||
if (href === '/app' && currentPath === '/app') return true
|
||||
return currentPath === href
|
||||
}
|
||||
|
||||
const handleWorkspaceSelect = (workspace: typeof mockWorkspaces[0]) => {
|
||||
setSelectedWorkspace(workspace)
|
||||
setIsWorkspaceDropdownOpen(false)
|
||||
}
|
||||
|
||||
const toggleWorkspaceDropdown = () => {
|
||||
setIsWorkspaceDropdownOpen(!isWorkspaceDropdownOpen())
|
||||
}
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
onMount(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
const target = event.target
|
||||
if (!(target instanceof HTMLElement)) return
|
||||
if (!target.closest('#workspace-selector')) {
|
||||
setIsWorkspaceDropdownOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', handleClickOutside)
|
||||
return () => document.removeEventListener('click', handleClickOutside)
|
||||
})
|
||||
|
||||
return (
|
||||
<div class={cn('flex h-full w-64 flex-col bg-[#141415] border-r border-[#262626]', props.class)}>
|
||||
{/* Logo */}
|
||||
<div class="flex h-16 items-center px-6 border-b border-[#262626]">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-lg bg-primary-500">
|
||||
<span class="text-sm font-bold text-white">T</span>
|
||||
</div>
|
||||
<span class="text-xl font-semibold text-[#fafafa]">Trackeep</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav class="flex-1 space-y-1 px-3 py-4">
|
||||
<For each={navigation}>
|
||||
{(item) => {
|
||||
const Icon = item.icon
|
||||
return (
|
||||
<A
|
||||
href={item.href}
|
||||
class="flex items-center px-3 py-2 text-sm font-medium rounded-md text-[#a3a3a3] hover:text-[#fafafa] hover:bg-[#262626] transition-colors group"
|
||||
activeClass="bg-primary-600 text-white hover:bg-primary-700"
|
||||
<div class="w-280px border-r border-r-border flex-shrink-0 hidden md:block bg-card">
|
||||
<div class="flex h-full">
|
||||
<div class="h-full flex flex-col pb-6 flex-1 min-w-0">
|
||||
{/* Organization Selector */}
|
||||
<div class="p-4 pb-0 min-w-0 max-w-full" id="workspace-selector">
|
||||
<div role="group" class="w-full relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleWorkspaceDropdown}
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isWorkspaceDropdownOpen()}
|
||||
class="flex w-full items-center justify-between border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 hover:bg-accent/50 transition rounded-lg h-auto pl-2"
|
||||
>
|
||||
<Icon class="mr-3 h-5 w-5" />
|
||||
{item.name}
|
||||
</A>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</nav>
|
||||
<span class="flex items-center gap-2 min-w-0">
|
||||
<span class="p-1.5 rounded text-lg font-bold flex items-center bg-muted light:border dark:bg-primary/10 transition flex-shrink-0">
|
||||
{(() => {
|
||||
const workspace = selectedWorkspace()
|
||||
return <workspace.icon class="size-5.5" style="color: hsl(var(--primary))" />
|
||||
})()}
|
||||
</span>
|
||||
<span class="truncate text-base font-medium">{selectedWorkspace().name}</span>
|
||||
</span>
|
||||
<div class="size-4 opacity-50 ml-2 flex-shrink-0 transition-transform duration-200" classList={{ "rotate-180": isWorkspaceDropdownOpen() }}>
|
||||
<IconChevronDown class="size-4" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{/* User Section */}
|
||||
<div class="border-t border-[#262626] p-4">
|
||||
<div class="flex items-center space-x-3">
|
||||
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-[#262626]">
|
||||
<span class="text-sm font-medium text-[#a3a3a3]">U</span>
|
||||
{/* Dropdown Menu */}
|
||||
<Show when={isWorkspaceDropdownOpen()}>
|
||||
<div class="absolute top-full left-0 right-0 mt-1 bg-popover border border-border rounded-md shadow-lg z-50 max-h-60 overflow-auto">
|
||||
<div class="p-1" role="listbox">
|
||||
<For each={mockWorkspaces}>
|
||||
{(workspace) => (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleWorkspaceSelect(workspace)}
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-sm rounded-sm hover:bg-accent/50 transition-colors focus:bg-accent/50 focus:outline-none"
|
||||
role="option"
|
||||
classList={{ "bg-accent/30": workspace.id === selectedWorkspace().id }}
|
||||
>
|
||||
{(() => {
|
||||
const Icon = workspace.icon
|
||||
return <Icon class="size-4 text-muted-foreground" />
|
||||
})()}
|
||||
<span class="flex-1 text-left truncate">{workspace.name}</span>
|
||||
<Show when={workspace.id === selectedWorkspace().id}>
|
||||
<div class="w-2 h-2 bg-primary rounded-full"></div>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<div class="border-t border-border mt-1 pt-1">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center gap-2 px-3 py-2 text-sm rounded-sm hover:bg-accent/50 transition-colors focus:bg-accent/50 focus:outline-none text-muted-foreground"
|
||||
>
|
||||
<IconPlus class="size-4" />
|
||||
<span>Create Workspace</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="text-sm font-medium text-[#fafafa] truncate">User</p>
|
||||
<p class="text-xs text-[#a3a3a3] truncate">user@trackeep.local</p>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav class="flex flex-col gap-0.5 mt-4 px-4">
|
||||
<For each={navigation}>
|
||||
{(item) => {
|
||||
const Icon = item.icon
|
||||
const active = isActive(item.href)
|
||||
return (
|
||||
<A
|
||||
href={item.href}
|
||||
class="group inline-flex rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 justify-start items-center gap-2 truncate relative overflow-hidden"
|
||||
classList={{
|
||||
"bg-[#262626] text-white font-medium shadow-sm": active,
|
||||
"hover:bg-[#262626] hover:text-white text-[#a3a3a3]": !active
|
||||
}}
|
||||
>
|
||||
<div class="relative z-10 flex items-center gap-2">
|
||||
<Icon class={`size-5 transition-colors ${
|
||||
active
|
||||
? 'text-primary'
|
||||
: 'text-[#a3a3a3] group-hover:text-primary'
|
||||
}`} />
|
||||
<div class={`transition-colors ${
|
||||
active
|
||||
? 'text-white font-medium'
|
||||
: 'text-[#a3a3a3] group-hover:text-white'
|
||||
}`}>{item.name}</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200" classList={{
|
||||
"bg-[#262626]": !active
|
||||
}}></div>
|
||||
</A>
|
||||
)
|
||||
}}
|
||||
</For>
|
||||
</nav>
|
||||
|
||||
{/* Bottom Navigation */}
|
||||
<div class="flex-1"></div>
|
||||
|
||||
{/* Update Checker */}
|
||||
<div class="px-4 mb-2">
|
||||
<UpdateChecker />
|
||||
</div>
|
||||
|
||||
<nav class="flex flex-col gap-0.5 px-4">
|
||||
<A
|
||||
href="/app/removed-stuff"
|
||||
class="group inline-flex rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 justify-start items-center gap-2 truncate relative overflow-hidden"
|
||||
classList={{
|
||||
"bg-[#262626] text-white font-medium shadow-sm": isActive('/app/removed-stuff'),
|
||||
"hover:bg-[#262626] hover:text-white text-[#a3a3a3]": !isActive('/app/removed-stuff')
|
||||
}}
|
||||
>
|
||||
<div class="relative z-10 flex items-center gap-2">
|
||||
<IconTrash class={`size-5 transition-colors ${
|
||||
isActive('/app/removed-stuff')
|
||||
? 'text-white'
|
||||
: 'text-[#a3a3a3] group-hover:text-primary'
|
||||
}`} />
|
||||
<div class={`transition-colors ${
|
||||
isActive('/app/removed-stuff')
|
||||
? 'text-white font-medium'
|
||||
: 'text-[#a3a3a3] group-hover:text-white'
|
||||
}`}>Removed stuff</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200" classList={{
|
||||
"bg-[#262626]": !isActive('/app/removed-stuff')
|
||||
}}></div>
|
||||
</A>
|
||||
<A
|
||||
href="/app/settings"
|
||||
class="group inline-flex rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 justify-start items-center gap-2 truncate relative overflow-hidden"
|
||||
classList={{
|
||||
"bg-[#262626] text-white font-medium shadow-sm": isActive('/app/settings'),
|
||||
"hover:bg-[#262626] hover:text-white text-[#a3a3a3]": !isActive('/app/settings')
|
||||
}}
|
||||
>
|
||||
<div class="relative z-10 flex items-center gap-2">
|
||||
<IconSettings class={`size-5 transition-colors ${
|
||||
isActive('/app/settings')
|
||||
? 'text-white'
|
||||
: 'text-[#a3a3a3] group-hover:text-primary'
|
||||
}`} />
|
||||
<div class={`transition-colors ${
|
||||
isActive('/app/settings')
|
||||
? 'text-white font-medium'
|
||||
: 'text-[#a3a3a3] group-hover:text-white'
|
||||
}`}>Settings</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 opacity-0 group-hover:opacity-100 transition-opacity duration-200" classList={{
|
||||
"bg-[#262626]": !isActive('/app/settings')
|
||||
}}></div>
|
||||
</A>
|
||||
<button
|
||||
onClick={() => {
|
||||
// Handle logout logic here
|
||||
localStorage.removeItem('auth_token')
|
||||
window.location.href = '/login'
|
||||
}}
|
||||
class="group inline-flex rounded-md text-sm font-medium transition-all duration-200 focus-visible:outline-none focus-visible:ring-1.5 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 h-9 px-4 py-2 justify-start items-center gap-2 truncate w-full relative overflow-hidden hover:bg-destructive/10 hover:text-destructive dark:text-muted-foreground"
|
||||
>
|
||||
<div class="relative z-10 flex items-center gap-2">
|
||||
<IconLogout class={`size-5 transition-colors text-[#a3a3a3]`} />
|
||||
<div class="transition-colors">Logout</div>
|
||||
</div>
|
||||
<div class="absolute inset-0 bg-destructive/10 opacity-0 group-hover:opacity-100 transition-opacity duration-200"></div>
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user