mirror of
https://github.com/JohnRiceML/clawport-ui.git
synced 2026-08-14 00:47:50 +00:00
Design System: - Apple HIG typography scale (caption2 → large-title) - 4px spacing grid (space-1 → space-16) - 7 new CSS tokens across all 5 themes - Interactive state CSS classes (hover-lift, btn-primary, focus-ring, etc.) - Shimmer skeleton, slideDown, bounce-dot animations - Light theme polish: deeper shadows, better accent contrast Navigation: - Lucide icons replacing emoji in sidebar - Cmd+K global search command palette (agents, pages, crons) - Breadcrumbs component for page context - Sidebar wrapper orchestrating desktop + mobile - Mobile hamburger menu with spring-animated slide-out - Cron error dot badge on nav item Manor Map + Agent Detail: - Polished agent nodes: tinted squircle emoji, status dots with border ring - Smooth bezier edges, selected agent edge highlighting - Map legend (top-right), skeleton loading state - Detail panel: hierarchy navigation, cron status, dual CTAs - Agent detail: single-column scrollable layout, card sections - SOUL.md viewer with copy button, voice ID copy Chat: - iMessage-style agent list with gradient avatars, online dots - Desktop + mobile variants with responsive master/detail - Sticky header with agent info, profile link, clear button - Time gap timestamps between messages - Typing indicator (bouncing dots) before first stream token - Thin blinking cursor replacing block cursor - Code blocks with copy button, URL auto-linking - Rounded input capsule with SVG icons Cron + Memory: - Sticky header with summary counts, spinning refresh, auto-update timer - Filter pills with ARIA tablist, keyboard cycling - Expandable cron rows with colored borders, error tinting, copy - Memory: 260px sidebar with search, file icons, arrow key nav - Content header with breadcrumb path, copy + download buttons - Mobile master/detail pattern with back button Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
109 lines
3.2 KiB
TypeScript
109 lines
3.2 KiB
TypeScript
'use client';
|
|
|
|
import { useRef, useCallback } from 'react';
|
|
import { THEMES } from '@/lib/themes';
|
|
import { useTheme } from '@/app/providers';
|
|
|
|
export function ThemeToggle() {
|
|
const { theme, setTheme } = useTheme();
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
const handleKeyDown = useCallback(
|
|
(e: React.KeyboardEvent) => {
|
|
const buttons =
|
|
containerRef.current?.querySelectorAll<HTMLButtonElement>('button');
|
|
if (!buttons || buttons.length === 0) return;
|
|
|
|
const currentIndex = THEMES.findIndex((t) => t.id === theme);
|
|
|
|
if (e.key === 'ArrowRight' || e.key === 'ArrowDown') {
|
|
e.preventDefault();
|
|
const nextIndex = (currentIndex + 1) % THEMES.length;
|
|
setTheme(THEMES[nextIndex].id);
|
|
buttons[nextIndex].focus();
|
|
} else if (e.key === 'ArrowLeft' || e.key === 'ArrowUp') {
|
|
e.preventDefault();
|
|
const prevIndex =
|
|
(currentIndex - 1 + THEMES.length) % THEMES.length;
|
|
setTheme(THEMES[prevIndex].id);
|
|
buttons[prevIndex].focus();
|
|
}
|
|
},
|
|
[theme, setTheme]
|
|
);
|
|
|
|
return (
|
|
<div style={{ padding: '8px 16px 12px' }}>
|
|
<div
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
letterSpacing: '0.06em',
|
|
color: 'var(--text-tertiary)',
|
|
textTransform: 'uppercase',
|
|
marginBottom: '6px',
|
|
paddingLeft: '4px',
|
|
}}
|
|
>
|
|
Theme
|
|
</div>
|
|
<div
|
|
ref={containerRef}
|
|
className="flex flex-wrap gap-1.5"
|
|
role="radiogroup"
|
|
aria-label="Theme selection"
|
|
onKeyDown={handleKeyDown}
|
|
>
|
|
{THEMES.map((t) => {
|
|
const isActive = theme === t.id;
|
|
return (
|
|
<button
|
|
key={t.id}
|
|
onClick={() => setTheme(t.id)}
|
|
title={t.label}
|
|
role="radio"
|
|
aria-checked={isActive}
|
|
aria-label={`${t.label} theme`}
|
|
tabIndex={isActive ? 0 : -1}
|
|
className="focus-ring"
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
height: '28px',
|
|
padding: isActive ? '0 10px' : '0 6px',
|
|
borderRadius: '14px',
|
|
fontSize: '12px',
|
|
fontWeight: isActive ? 600 : 500,
|
|
border: 'none',
|
|
cursor: 'pointer',
|
|
transition: 'all 150ms var(--ease-spring)',
|
|
background: isActive
|
|
? 'var(--accent-fill)'
|
|
: 'var(--fill-quaternary)',
|
|
color: isActive ? 'var(--accent)' : 'var(--text-tertiary)',
|
|
outline: 'none',
|
|
}}
|
|
>
|
|
<span style={{ fontSize: '13px', lineHeight: 1 }}>
|
|
{t.emoji}
|
|
</span>
|
|
{isActive && (
|
|
<span
|
|
style={{
|
|
fontSize: '11px',
|
|
fontWeight: 600,
|
|
letterSpacing: '-0.01em',
|
|
}}
|
|
>
|
|
{t.label}
|
|
</span>
|
|
)}
|
|
</button>
|
|
);
|
|
})}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|