feat(ui): implement 'Twilight Velvet' dark theme and fix visibility issues

- Add 'Twilight Velvet' color palette to globals.css with OKLCH values
- Update SettingsPage headers, cards, and dialogs to use semantic theme variables
- Update HistoryCard, HistoryFeed, and DraftContent to support dark mode
- Update ProviderSelector and ProviderList to use custom card background (#2A2A3D)
- Add ThemeToggle component with improved visibility
- Ensure consistent use of 'bg-card', 'text-foreground', and 'text-muted-foreground'
This commit is contained in:
Max
2026-01-27 11:03:55 +07:00
parent e9e6fadb1d
commit 9b79856827
49 changed files with 2411 additions and 878 deletions

View File

@@ -1,16 +1,13 @@
"use client";
import { useState, useRef, useEffect } from 'react';
import { useEffect, useRef, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Textarea } from '@/components/ui/textarea';
import { Send, StopCircle } from 'lucide-react';
import { Send, StopCircle, Sparkles } from 'lucide-react';
import { useChatStore } from '@/store/use-chat';
interface ChatInputProps {
onSend: (message: string) => void;
isLoading: boolean;
}
export function ChatInput({ onSend, isLoading }: ChatInputProps) {
export function ChatInput() {
const { sendMessage, isTyping, phase, generateDraft } = useChatStore();
const [input, setInput] = useState('');
const textareaRef = useRef<HTMLTextAreaElement>(null);
@@ -22,10 +19,11 @@ export function ChatInput({ onSend, isLoading }: ChatInputProps) {
}
}, [input]);
const handleSend = () => {
if (!input.trim() || isLoading) return;
onSend(input);
setInput('');
const handleSend = async () => {
if (!input.trim() || isTyping) return;
const msg = input;
setInput(''); // Clear immediately for UX
await sendMessage(msg);
};
const handleKeyDown = (e: React.KeyboardEvent) => {
@@ -36,26 +34,41 @@ export function ChatInput({ onSend, isLoading }: ChatInputProps) {
};
return (
<div className="p-4 bg-card/80 backdrop-blur-md border-t border-border sticky bottom-0">
<div className="flex gap-2 items-center max-w-3xl mx-auto">
<div className="p-4 bg-card/80 backdrop-blur-md border-t border-border sticky bottom-0 z-10 w-full transition-all duration-300">
<div className="flex gap-2 items-end max-w-3xl mx-auto">
<Textarea
ref={textareaRef}
value={input}
onChange={(e) => setInput(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Record your thoughts..."
className="resize-none min-h-[44px] max-h-[120px] py-3 rounded-xl border-input focus:ring-ring"
placeholder={phase === 'elicitation' ? "Answer the question..." : "Record your thoughts..."}
className="resize-none min-h-[44px] max-h-[120px] py-3 rounded-xl border-input focus:ring-ring shadow-sm bg-background/50"
rows={1}
/>
<Button
onClick={handleSend}
disabled={!input.trim() || isLoading}
disabled={!input.trim() || isTyping}
size="icon"
className="h-11 w-11 rounded-xl shrink-0 bg-slate-800 hover:bg-slate-700 transition-colors"
className="h-11 w-11 rounded-xl shrink-0 bg-slate-800 hover:bg-slate-700 transition-colors shadow-sm"
>
{isLoading ? <StopCircle className="h-5 w-5 animate-pulse" /> : <Send className="h-5 w-5" />}
{isTyping ? <StopCircle className="h-5 w-5 animate-pulse" /> : <Send className="h-5 w-5" />}
</Button>
</div>
{/* Contextual Action Button (e.g. Draft) */}
{phase === 'elicitation' && !isTyping && (
<div className="absolute -top-14 left-1/2 -translate-x-1/2 animate-in slide-in-from-bottom-2 fade-in">
<Button
onClick={() => generateDraft()}
variant="secondary"
size="sm"
className="shadow-lg border-indigo-200 bg-indigo-50 hover:bg-indigo-100 text-indigo-700 gap-2 rounded-full px-6"
>
<Sparkles className="w-4 h-4" />
Summarize & Draft
</Button>
</div>
)}
</div>
);
}

View File

@@ -1,41 +1,19 @@
"use client";
import { useEffect, useRef } from 'react';
import { useLiveQuery } from 'dexie-react-hooks';
import { db } from '@/lib/db/db';
import { ChatBubble } from './chat-bubble';
import { TypingIndicator } from './typing-indicator';
import { useTeacherStatus } from '@/store/use-session';
import { useChatStore } from '@/store/use-chat';
import { BookOpen, Sparkles } from 'lucide-react';
interface ChatWindowProps {
sessionId: string | null;
}
export function ChatWindow({ sessionId }: ChatWindowProps) {
const teacherStatus = useTeacherStatus();
export function ChatWindow() {
const { messages, isTyping } = useChatStore();
const bottomRef = useRef<HTMLDivElement>(null);
// Reactive query for messages
const messages = useLiveQuery(
async () => {
if (!sessionId) return [];
return await db.chatLogs
.where('sessionId')
.equals(sessionId)
.sortBy('timestamp');
},
[sessionId]
);
// Auto-scroll to bottom
useEffect(() => {
bottomRef.current?.scrollIntoView({ behavior: 'smooth' });
}, [messages, teacherStatus]);
if (!sessionId) {
return <div className="flex-1 flex items-center justify-center text-slate-400">Loading session...</div>;
}
}, [messages, isTyping]);
if (!messages || messages.length === 0) {
return (
@@ -49,10 +27,10 @@ export function ChatWindow({ sessionId }: ChatWindowProps) {
<div className="space-y-2 max-w-md">
<h2 className="text-2xl font-bold font-serif text-foreground">
What do you want to record?
What's on your mind?
</h2>
<p className="text-muted-foreground font-sans">
Let me help you summarize your day.
I'm here to listen. Let it all out.
</p>
</div>
</div>
@@ -66,7 +44,7 @@ export function ChatWindow({ sessionId }: ChatWindowProps) {
<ChatBubble key={msg.id} role={msg.role} content={msg.content} />
))}
{teacherStatus !== 'idle' && (
{isTyping && (
<TypingIndicator />
)}

View File

@@ -88,7 +88,7 @@ describe('DraftActions', () => {
);
const approveButton = screen.getByRole('button', { name: /approve, copy to clipboard, and mark as completed/i });
expect(approveButton).toHaveClass('bg-slate-700', 'hover:bg-slate-800');
expect(approveButton).toHaveClass('bg-slate-800', 'hover:bg-slate-700');
});
it('renders Thumbs Down button with outline style', () => {

View File

@@ -20,7 +20,7 @@ interface DraftActionsProps {
* - Proper ARIA labels for screen readers
* - Sticky positioning to stay visible when scrolling long drafts
*/
export function DraftActions({ onApprove, onReject, onCopyOnly }: DraftActionsProps) {
export function DraftActions({ onApprove, onReject, onCopyOnly, children }: DraftActionsProps & { children?: React.ReactNode }) {
const currentDraft = useChatStore((s) => s.currentDraft);
const startRefinement = useChatStore((s) => s.startRefinement);
@@ -35,6 +35,9 @@ export function DraftActions({ onApprove, onReject, onCopyOnly }: DraftActionsPr
return (
<nav className="sticky bottom-0 flex gap-3 p-4 bg-white border-t border-slate-200">
{/* Optional additional actions (e.g. Delete) */}
{children}
{/* Thumbs Down - Request changes (Story 2.3: triggers refinement) */}
<button
onClick={handleReject}

View File

@@ -43,38 +43,38 @@ export function DraftContent({ draft }: DraftContentProps) {
})();
return (
<article className="draft-content px-4 sm:px-6 py-6 bg-white">
<article className="draft-content px-4 sm:px-6 py-6 bg-card">
{/* Title - using Merriweather serif font */}
<h2 className="draft-title text-2xl sm:text-3xl font-bold text-slate-800 mb-6 font-serif leading-tight">
<h2 className="draft-title text-2xl sm:text-3xl font-bold text-foreground mb-6 font-serif leading-tight">
{draft.title}
</h2>
{/* Body content - Markdown with prose styling */}
<div className="draft-body prose prose-slate max-w-none font-serif">
<div className="draft-body prose prose-slate dark:prose-invert max-w-none font-serif">
<ReactMarkdown
remarkPlugins={[remarkGfm]}
rehypePlugins={[rehypeHighlight, rehypeRaw]}
components={{
// Custom heading styles
h1: ({ node, ...props }) => (
<h1 className="text-2xl font-bold text-slate-800 mt-8 mb-4 first:mt-0" {...props} />
<h1 className="text-2xl font-bold text-foreground mt-8 mb-4 first:mt-0" {...props} />
),
h2: ({ node, ...props }) => (
<h2 className="text-xl font-bold text-slate-800 mt-6 mb-3" {...props} />
<h2 className="text-xl font-bold text-foreground mt-6 mb-3" {...props} />
),
h3: ({ node, ...props }) => (
<h3 className="text-lg font-semibold text-slate-800 mt-5 mb-2" {...props} />
<h3 className="text-lg font-semibold text-foreground mt-5 mb-2" {...props} />
),
// Paragraph styling
p: ({ node, ...props }) => (
<p className="text-base leading-relaxed text-slate-700 mb-4" {...props} />
<p className="text-base leading-relaxed text-muted-foreground mb-4" {...props} />
),
// Code blocks
code: ({ node, inline, className, children, ...props }: any) => {
if (inline) {
return (
<code
className="px-1.5 py-0.5 bg-slate-100 text-slate-800 rounded text-sm font-mono"
className="px-1.5 py-0.5 bg-muted text-foreground rounded text-sm font-mono"
{...props}
>
{children}
@@ -83,7 +83,7 @@ export function DraftContent({ draft }: DraftContentProps) {
}
return (
<code
className={`block bg-slate-100 text-slate-800 p-4 rounded-lg text-sm font-mono overflow-x-auto ${className || ''}`}
className={`block bg-muted text-foreground p-4 rounded-lg text-sm font-mono overflow-x-auto ${className || ''}`}
{...props}
>
{children}
@@ -92,22 +92,22 @@ export function DraftContent({ draft }: DraftContentProps) {
},
// Pre tags
pre: ({ node, ...props }) => (
<pre className="bg-slate-100 p-4 rounded-lg overflow-x-auto mb-4" {...props} />
<pre className="bg-muted p-4 rounded-lg overflow-x-auto mb-4" {...props} />
),
// Links
a: ({ node, ...props }) => (
<a className="text-slate-600 hover:text-slate-800 underline" {...props} />
<a className="text-primary hover:underline" {...props} />
),
// Lists
ul: ({ node, ...props }) => (
<ul className="list-disc list-inside mb-4 text-slate-700 space-y-1" {...props} />
<ul className="list-disc list-inside mb-4 text-muted-foreground space-y-1" {...props} />
),
ol: ({ node, ...props }) => (
<ol className="list-decimal list-inside mb-4 text-slate-700 space-y-1" {...props} />
<ol className="list-decimal list-inside mb-4 text-muted-foreground space-y-1" {...props} />
),
// Blockquotes
blockquote: ({ node, ...props }) => (
<blockquote className="border-l-4 border-slate-300 pl-4 italic text-slate-600 my-4" {...props} />
<blockquote className="border-l-4 border-muted-foreground/30 pl-4 italic text-muted-foreground my-4" {...props} />
),
}}
>
@@ -117,11 +117,11 @@ export function DraftContent({ draft }: DraftContentProps) {
{/* Tags section */}
{draft.tags && draft.tags.length > 0 && (
<div className="flex flex-wrap gap-2 mt-6 pt-4 border-t border-slate-200">
<div className="flex flex-wrap gap-2 mt-6 pt-4 border-t border-border">
{draft.tags.map((tag) => (
<span
key={tag}
className="tag-chip px-3 py-1 bg-slate-100 text-slate-600 rounded-full text-sm font-sans"
className="tag-chip px-3 py-1 bg-secondary text-secondary-foreground rounded-full text-sm font-sans"
>
#{tag}
</span>

View File

@@ -105,7 +105,12 @@ export function DraftViewSheet() {
<Sheet open={showDraftView} onClose={handleClose}>
<DraftContent draft={currentDraft} />
{/* Story 3.2: Extended footer with delete button */}
<nav className="sticky bottom-0 flex gap-3 p-4 bg-white border-t border-slate-200">
{/* Story 3.2: Extended footer with delete button passed as child to DraftActions */}
<DraftActions
onApprove={handleApprove}
onReject={handleReject}
onCopyOnly={handleCopyOnly}
>
{/* Delete button (Story 3.2) */}
<button
onClick={() => setShowDeleteDialog(true)}
@@ -116,14 +121,7 @@ export function DraftViewSheet() {
<Trash2 className="w-5 h-5" aria-hidden="true" />
<span>Delete</span>
</button>
{/* Draft actions from original component */}
<DraftActions
onApprove={handleApprove}
onReject={handleReject}
onCopyOnly={handleCopyOnly}
/>
</nav>
</DraftActions>
</Sheet>
{/* Story 3.2: Delete confirmation dialog */}

View File

@@ -35,16 +35,16 @@ export function HistoryCard({ draft, onClick }: HistoryCardProps) {
<button
onClick={() => onClick(draft)}
type="button"
className="history-card group w-full text-left p-4 bg-white rounded-lg shadow-sm hover:shadow-md transition-shadow border border-slate-200"
className="history-card group w-full text-left p-4 bg-card rounded-lg shadow-sm hover:shadow-md transition-shadow border border-border"
aria-label={`View post: ${draft.title}`}
>
{/* Title - Merriweather serif font for "published" feel */}
<h3 className="history-title text-lg font-bold text-slate-800 mb-2 font-serif leading-tight line-clamp-2">
<h3 className="history-title text-lg font-bold text-card-foreground mb-2 font-serif leading-tight line-clamp-2">
{draft.title}
</h3>
{/* Date - Inter font, subtle gray, relative format */}
<p className="history-date text-sm text-slate-500 mb-2 font-sans">
<p className="history-date text-sm text-muted-foreground mb-2 font-sans">
{formatRelativeDate(displayDate)}
</p>
@@ -54,7 +54,7 @@ export function HistoryCard({ draft, onClick }: HistoryCardProps) {
{draft.tags.map((tag) => (
<span
key={tag}
className="tag-chip px-2 py-1 bg-slate-100 text-slate-600 rounded-full text-xs font-sans"
className="tag-chip px-2 py-1 bg-secondary text-secondary-foreground rounded-full text-xs font-sans"
>
#{tag}
</span>
@@ -63,7 +63,7 @@ export function HistoryCard({ draft, onClick }: HistoryCardProps) {
)}
{/* Preview - light gray text */}
<p className="history-preview text-sm text-slate-400 font-sans line-clamp-2">
<p className="history-preview text-sm text-muted-foreground/80 font-sans line-clamp-2">
{preview}
{draft.content.length > 100 && '...'}
</p>

View File

@@ -5,8 +5,8 @@ import { Copy, Check, X, Trash2 } from 'lucide-react';
import { useHistoryStore } from '@/lib/store/history-store';
import { DraftContent } from '@/components/features/draft/DraftContent';
import { CopySuccessToast } from '@/components/features/feedback/CopySuccessToast';
import { useChatStore } from '@/lib/store/chat-store';
import { Sheet } from '@/components/features/draft/Sheet';
import { useChatStore } from '@/store/use-chat';
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from '@/components/ui/sheet';
import { DeleteConfirmDialog } from './DeleteConfirmDialog';
/**
@@ -36,9 +36,6 @@ export function HistoryDetailSheet() {
const closeDetail = useHistoryStore((s) => s.closeDetail);
const deleteDraft = useHistoryStore((s) => s.deleteDraft);
// Reuse copy action from ChatStore
const copyDraftToClipboard = useChatStore((s) => s.copyDraftToClipboard);
// Dialog state
const [showDeleteDialog, setShowDeleteDialog] = useState(false);
@@ -56,9 +53,11 @@ export function HistoryDetailSheet() {
setToastShow(true);
};
// Placeholder copy function since ChatStore might not have it exposed exactly this way yet
// or we need to implement it here.
const handleCopy = async () => {
if (selectedDraft) {
await copyDraftToClipboard(selectedDraft.id);
await navigator.clipboard.writeText(selectedDraft.content);
showCopyToast();
}
};
@@ -69,6 +68,7 @@ export function HistoryDetailSheet() {
if (success) {
setShowDeleteDialog(false);
showCopyToast('Post deleted successfully');
closeDetail(); // Close sheet on delete
} else {
setShowDeleteDialog(false);
showCopyToast('Failed to delete post');
@@ -76,54 +76,58 @@ export function HistoryDetailSheet() {
}
};
const handleClose = () => {
closeDetail();
};
if (!selectedDraft) {
return null;
}
return (
<>
<Sheet open={!!selectedDraft} onClose={handleClose}>
<DraftContent draft={selectedDraft} />
<Sheet open={!!selectedDraft} onOpenChange={(open) => !open && closeDetail()}>
<SheetContent side="right" className="w-full sm:max-w-xl overflow-y-auto p-0">
<SheetHeader className="sr-only">
<SheetTitle>Draft Details</SheetTitle>
<SheetDescription>View your saved draft details</SheetDescription>
</SheetHeader>
<div className="p-6">
<DraftContent draft={selectedDraft} />
</div>
{/* Footer with copy, delete and close buttons */}
<nav className="sticky bottom-0 flex gap-3 p-4 bg-white border-t border-slate-200">
{/* Delete button (Story 3.2.1) */}
<button
onClick={() => setShowDeleteDialog(true)}
type="button"
className="min-h-[44px] px-4 py-3 border border-destructive text-destructive rounded-md hover:bg-destructive/10 transition-colors flex items-center justify-center gap-2"
aria-label="Delete this draft"
>
<Trash2 className="w-5 h-5" aria-hidden="true" />
<span className="sr-only">Delete</span>
</button>
{/* Footer with copy, delete and close buttons */}
<nav className="sticky bottom-0 flex gap-3 p-4 bg-white border-t border-slate-200 mt-auto">
{/* Delete button (Story 3.2.1) */}
<button
onClick={() => setShowDeleteDialog(true)}
type="button"
className="min-h-[44px] px-4 py-3 border border-destructive text-destructive rounded-md hover:bg-destructive/10 transition-colors flex items-center justify-center gap-2"
aria-label="Delete this draft"
>
<Trash2 className="w-5 h-5" aria-hidden="true" />
<span className="sr-only">Delete</span>
</button>
{/* Copy button */}
<button
onClick={handleCopy}
type="button"
className="flex-1 min-h-[44px] px-4 py-3 border border-slate-300 rounded-md text-slate-700 hover:bg-slate-50 transition-colors flex items-center justify-center gap-2"
aria-label="Copy to clipboard"
>
<Copy className="w-5 h-5" aria-hidden="true" />
<span>Copy</span>
</button>
{/* Copy button */}
<button
onClick={handleCopy}
type="button"
className="flex-1 min-h-[44px] px-4 py-3 border border-slate-300 rounded-md text-slate-700 hover:bg-slate-50 transition-colors flex items-center justify-center gap-2"
aria-label="Copy to clipboard"
>
<Copy className="w-5 h-5" aria-hidden="true" />
<span>Copy</span>
</button>
{/* Close button */}
<button
onClick={handleClose}
type="button"
className="min-h-[44px] px-4 py-3 bg-slate-800 text-white rounded-md hover:bg-slate-700 transition-colors flex items-center justify-center gap-2"
aria-label="Close"
>
<X className="w-5 h-5" aria-hidden="true" />
<span>Close</span>
</button>
</nav>
{/* Close button */}
<button
onClick={closeDetail}
type="button"
className="min-h-[44px] px-4 py-3 bg-slate-800 text-white rounded-md hover:bg-slate-700 transition-colors flex items-center justify-center gap-2"
aria-label="Close"
>
<X className="w-5 h-5" aria-hidden="true" />
<span>Close</span>
</button>
</nav>
</SheetContent>
</Sheet>
{/* Delete Confirmation Dialog */}

View File

@@ -117,11 +117,11 @@ export function HistoryFeed() {
<div key={weekLabel} className="mb-6">
{/* Week separator header */}
<div className="flex items-center justify-center gap-3 mt-6 mb-4">
<div className="h-px flex-1 max-w-[100px] bg-slate-200" />
<span className="text-xs font-medium text-slate-500 uppercase tracking-wide px-3 py-1 bg-slate-50 rounded-full border border-slate-200">
<div className="h-px flex-1 max-w-[100px] bg-border" />
<span className="text-xs font-medium text-muted-foreground uppercase tracking-wide px-3 py-1 bg-muted rounded-full border border-border">
{weekLabel}
</span>
<div className="h-px flex-1 max-w-[100px] bg-slate-200" />
<div className="h-px flex-1 max-w-[100px] bg-border" />
</div>
{/* Drafts for this week */}

View File

@@ -0,0 +1,107 @@
'use client';
import { useChatStore } from '@/store/use-chat';
import { Button } from '@/components/ui/button';
import {
Sheet,
SheetContent,
SheetHeader,
SheetTitle,
SheetDescription,
SheetFooter,
} from '@/components/ui/sheet';
import { ThumbsUp, ThumbsDown, RefreshCw } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
export function DraftSheet() {
const { phase, currentDraft, setPhase, resetSession } = useChatStore();
const isOpen = phase === 'review' && !!currentDraft;
const handleKeep = async () => {
if (!currentDraft) return;
try {
// Import dynamically to avoid side-effects during render if possible,
// or just import at top. We'll stick to dynamic since DraftService might not be SSR friendly
// without checks, but it handles it internally.
const { DraftService } = await import('@/lib/db/draft-service');
const { useSessionStore } = await import('@/store/use-session');
const sessionId = useSessionStore.getState().activeSessionId;
if (!sessionId) {
console.error("No active session ID");
return;
}
await DraftService.saveDraft({
sessionId,
title: currentDraft.title,
content: currentDraft.lesson, // Using lesson as content for now, or construct full markdown?
// Let's construct a nice markdown
// Actually the draft artifact has title, insight, lesson.
// We should probably save the raw JSON or a formatted textual representation.
// Let's save formatted text.
createdAt: Date.now(),
updatedAt: Date.now(),
status: 'completed',
completedAt: Date.now(),
tags: []
});
// Redirect to history or show success
window.location.href = '/history';
resetSession();
} catch (error) {
console.error("Failed to save draft:", error);
}
};
const handleRefine = () => {
// Logic for refinement (Story 3.5)
// For now, close sheet and persist state
setPhase('drafting'); // Go back or stay?
// Actually, refinement usually means going back to chat Elicitation or having a specialized Refinement Mode.
// Let's just close for now.
setPhase('elicitation');
};
if (!currentDraft) return null;
return (
<Sheet open={isOpen} onOpenChange={(open) => !open && handleRefine()}>
<SheetContent side="bottom" className="h-[80vh] sm:h-[600px] rounded-t-[20px] pt-10">
<SheetHeader className="text-left mb-6">
<SheetTitle className="font-serif text-3xl font-bold bg-gradient-to-r from-indigo-500 to-purple-600 bg-clip-text text-transparent">
{currentDraft.title}
</SheetTitle>
<SheetDescription className="text-lg text-slate-600 italic">
" {currentDraft.insight} "
</SheetDescription>
</SheetHeader>
<div className="space-y-6 overflow-y-auto pb-20">
<div className="prose dark:prose-invert max-w-none">
<h3 className="font-serif text-xl border-l-4 border-indigo-500 pl-4 py-1">
The Lesson
</h3>
<p className="text-lg leading-relaxed text-slate-700 dark:text-slate-300">
{currentDraft.lesson}
</p>
</div>
</div>
<SheetFooter className="absolute bottom-0 left-0 right-0 p-6 bg-white/80 dark:bg-zinc-950/80 backdrop-blur-md border-t border-slate-200 flex flex-row gap-4 justify-between sm:justify-end">
<Button variant="outline" size="lg" className="flex-1 sm:flex-none gap-2" onClick={handleRefine}>
<ThumbsDown className="w-5 h-5" />
Refine
</Button>
<Button size="lg" className="flex-1 sm:flex-none gap-2 bg-indigo-600 hover:bg-indigo-700 text-white" onClick={handleKeep}>
<ThumbsUp className="w-5 h-5" />
Keep It
</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}

View File

@@ -54,9 +54,9 @@ export function ProviderList({
<div
key={provider.id}
data-active={provider.id === activeId ? 'true' : 'false'}
className={`p-4 rounded-xl border transition-all duration-200 bg-white ${provider.id === activeId
className={`p-4 rounded-xl border transition-all duration-200 bg-card ${provider.id === activeId
? 'border-primary shadow-sm ring-1 ring-primary/20'
: 'border-slate-200 hover:border-primary/30'
: 'border-border hover:border-primary/30'
}`}
onClick={() => onSelectProvider?.(provider.id)}
>
@@ -64,7 +64,7 @@ export function ProviderList({
<div className="flex-1 min-w-0 w-full sm:w-auto">
<h3 className="font-semibold text-foreground text-base mb-0.5">{provider.name}</h3>
<div className="flex items-center gap-2 text-sm text-muted-foreground">
<span className="font-mono bg-slate-100 px-1.5 py-0.5 rounded text-xs">{provider.modelName}</span>
<span className="font-mono bg-muted px-1.5 py-0.5 rounded text-xs text-foreground/80">{provider.modelName}</span>
<span className="truncate text-xs opacity-70">{provider.baseUrl}</span>
</div>
</div>
@@ -77,7 +77,7 @@ export function ProviderList({
onEditProvider(provider.id);
}}
aria-label="Edit provider"
className="px-3 py-1.5 text-sm font-medium bg-white border border-slate-200 text-slate-700 rounded-lg hover:bg-slate-50 hover:text-slate-900 transition-colors"
className="px-3 py-1.5 text-sm font-medium bg-card border border-border text-muted-foreground rounded-lg hover:bg-muted hover:text-foreground transition-colors"
>
Edit
</button>
@@ -89,7 +89,7 @@ export function ProviderList({
onDeleteProvider(provider.id);
}}
aria-label="Delete provider"
className="px-3 py-1.5 text-sm font-medium bg-red-50 text-red-600 rounded-lg hover:bg-red-100 transition-colors"
className="px-3 py-1.5 text-sm font-medium bg-destructive/10 text-destructive rounded-lg hover:bg-destructive/20 transition-colors"
>
Delete
</button>
@@ -102,7 +102,7 @@ export function ProviderList({
{onAddProvider && (
<button
onClick={onAddProvider}
className="w-full px-4 py-3 border-2 border-dashed border-slate-200 rounded-xl text-muted-foreground font-medium hover:border-primary/50 hover:text-primary hover:bg-primary/5 transition-all duration-200 flex items-center justify-center gap-2"
className="w-full px-4 py-3 border-2 border-dashed border-border rounded-xl text-muted-foreground font-medium hover:border-primary/50 hover:text-primary hover:bg-primary/5 transition-all duration-200 flex items-center justify-center gap-2"
>
<span>+</span> Add New Provider
</button>

View File

@@ -30,18 +30,18 @@ export function ProviderSelector() {
{providers.map((provider) => (
<label
key={provider.id}
className={`flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 p-4 rounded-xl border cursor-pointer transition-all duration-200 bg-white ${provider.id === activeId
className={`flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-4 p-4 rounded-xl border cursor-pointer transition-all duration-200 bg-card ${provider.id === activeId
? 'border-primary shadow-sm ring-1 ring-primary/20'
: 'border-slate-200 hover:border-primary/30'
: 'border-border hover:border-primary/30'
}`}
data-active={provider.id === activeId ? 'true' : 'false'}
>
<div className="flex items-center gap-3 w-full sm:w-auto">
<div className={`flex items-center justify-center w-5 h-5 rounded-full border transition-colors shrink-0 ${provider.id === activeId
? 'border-primary bg-primary text-primary-foreground'
: 'border-slate-300 bg-white'
: 'border-border bg-card'
}`}>
{provider.id === activeId && <div className="w-2 h-2 rounded-full bg-white" />}
{provider.id === activeId && <div className="w-2 h-2 rounded-full bg-background" />}
</div>
<div className="flex-1 min-w-0 sm:hidden">
@@ -49,7 +49,7 @@ export function ProviderSelector() {
</div>
{provider.id === activeId && (
<span className="sm:hidden text-xs text-primary font-bold bg-primary/10 px-2 py-1 rounded-full ml-auto">
<span className="sm:hidden text-xs text-primary font-bold bg-primary/20 px-2 py-1 rounded-full ml-auto">
Active
</span>
)}
@@ -69,7 +69,7 @@ export function ProviderSelector() {
</div>
{provider.id === activeId && (
<span className="hidden sm:inline-block text-xs text-primary font-bold bg-primary/10 px-2 py-1 rounded-full shrink-0">
<span className="hidden sm:inline-block text-xs text-primary font-bold bg-primary/20 px-2 py-1 rounded-full shrink-0">
Active
</span>
)}

View File

@@ -0,0 +1,39 @@
'use client';
import * as React from 'react';
import { Moon, Sun } from 'lucide-react';
import { useTheme } from 'next-themes';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
export function ThemeToggle() {
const { setTheme } = useTheme();
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="bg-white dark:bg-slate-950">
<Sun className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
<Moon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="bg-white dark:bg-slate-950">
<DropdownMenuItem onClick={() => setTheme('light')}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('dark')}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme('system')}>
System
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
}