mirror of
https://github.com/zvx-echo6/navi.git
synced 2026-05-20 14:44:51 +02:00
fix(ui): Use 4-column grid for theme picker
8 themes no longer fit in a single row. Changed from flex row to a 4x2 grid layout so all themes are visible. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
948871b9e5
commit
66a35d9472
1 changed files with 167 additions and 167 deletions
|
|
@ -1,167 +1,167 @@
|
|||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Palette } from 'lucide-react'
|
||||
import { themeList } from '../themes/registry'
|
||||
import { useStore } from '../store'
|
||||
|
||||
/**
|
||||
* ThemeSwatch - Renders a circular swatch with 3 color segments
|
||||
*/
|
||||
function ThemeSwatch({ colors, size = 28, active = false }) {
|
||||
// Split circle into 3 segments using conic gradient
|
||||
const gradient = `conic-gradient(
|
||||
${colors[0]} 0deg 120deg,
|
||||
${colors[1]} 120deg 240deg,
|
||||
${colors[2]} 240deg 360deg
|
||||
)`
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '50%',
|
||||
background: gradient,
|
||||
border: active ? '2px solid var(--accent)' : '2px solid var(--border)',
|
||||
boxShadow: active ? '0 0 0 2px var(--accent-muted)' : 'none',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemePicker - Popover component for selecting themes
|
||||
*/
|
||||
export default function ThemePicker() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const theme = useStore((s) => s.theme)
|
||||
const setThemeOverride = useStore((s) => s.setThemeOverride)
|
||||
const triggerRef = useRef(null)
|
||||
const popoverRef = useRef(null)
|
||||
|
||||
const themes = themeList()
|
||||
const currentTheme = themes.find(t => t.id === theme) || themes[0]
|
||||
|
||||
// Handle click outside to close
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
function handleClickOutside(e) {
|
||||
if (
|
||||
popoverRef.current &&
|
||||
!popoverRef.current.contains(e.target) &&
|
||||
triggerRef.current &&
|
||||
!triggerRef.current.contains(e.target)
|
||||
) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscape(e) {
|
||||
if (e.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleThemeSelect = (themeId) => {
|
||||
setThemeOverride(themeId)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* Trigger button */}
|
||||
<button
|
||||
ref={triggerRef}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="p-1.5 rounded flex items-center justify-center"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
aria-label="Select theme"
|
||||
title="Select theme"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<Palette size={16} />
|
||||
</button>
|
||||
|
||||
{/* Popover */}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={popoverRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 'calc(100% + 8px)',
|
||||
right: 0,
|
||||
background: 'var(--bg-raised)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
zIndex: 100,
|
||||
minWidth: '140px',
|
||||
}}
|
||||
role="menu"
|
||||
aria-orientation="horizontal"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
gap: '16px',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{themes.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => handleThemeSelect(t.id)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: '4px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'var(--bg-overlay)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
role="menuitem"
|
||||
aria-current={t.id === theme ? 'true' : undefined}
|
||||
>
|
||||
<ThemeSwatch
|
||||
colors={t.swatch}
|
||||
size={32}
|
||||
active={t.id === theme}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 'var(--text-xs)',
|
||||
color: t.id === theme ? 'var(--accent)' : 'var(--text-secondary)',
|
||||
fontWeight: t.id === theme ? 500 : 400,
|
||||
}}
|
||||
>
|
||||
{t.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Palette } from 'lucide-react'
|
||||
import { themeList } from '../themes/registry'
|
||||
import { useStore } from '../store'
|
||||
|
||||
/**
|
||||
* ThemeSwatch - Renders a circular swatch with 3 color segments
|
||||
*/
|
||||
function ThemeSwatch({ colors, size = 28, active = false }) {
|
||||
// Split circle into 3 segments using conic gradient
|
||||
const gradient = `conic-gradient(
|
||||
${colors[0]} 0deg 120deg,
|
||||
${colors[1]} 120deg 240deg,
|
||||
${colors[2]} 240deg 360deg
|
||||
)`
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
borderRadius: '50%',
|
||||
background: gradient,
|
||||
border: active ? '2px solid var(--accent)' : '2px solid var(--border)',
|
||||
boxShadow: active ? '0 0 0 2px var(--accent-muted)' : 'none',
|
||||
transition: 'border-color 0.15s, box-shadow 0.15s',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* ThemePicker - Popover component for selecting themes
|
||||
*/
|
||||
export default function ThemePicker() {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const theme = useStore((s) => s.theme)
|
||||
const setThemeOverride = useStore((s) => s.setThemeOverride)
|
||||
const triggerRef = useRef(null)
|
||||
const popoverRef = useRef(null)
|
||||
|
||||
const themes = themeList()
|
||||
const currentTheme = themes.find(t => t.id === theme) || themes[0]
|
||||
|
||||
// Handle click outside to close
|
||||
useEffect(() => {
|
||||
if (!isOpen) return
|
||||
|
||||
function handleClickOutside(e) {
|
||||
if (
|
||||
popoverRef.current &&
|
||||
!popoverRef.current.contains(e.target) &&
|
||||
triggerRef.current &&
|
||||
!triggerRef.current.contains(e.target)
|
||||
) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
function handleEscape(e) {
|
||||
if (e.key === 'Escape') {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside)
|
||||
document.removeEventListener('keydown', handleEscape)
|
||||
}
|
||||
}, [isOpen])
|
||||
|
||||
const handleThemeSelect = (themeId) => {
|
||||
setThemeOverride(themeId)
|
||||
setIsOpen(false)
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
{/* Trigger button */}
|
||||
<button
|
||||
ref={triggerRef}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="p-1.5 rounded flex items-center justify-center"
|
||||
style={{ color: 'var(--text-secondary)' }}
|
||||
aria-label="Select theme"
|
||||
title="Select theme"
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<Palette size={16} />
|
||||
</button>
|
||||
|
||||
{/* Popover */}
|
||||
{isOpen && (
|
||||
<div
|
||||
ref={popoverRef}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: 'calc(100% + 8px)',
|
||||
right: 0,
|
||||
background: 'var(--bg-raised)',
|
||||
border: '1px solid var(--border)',
|
||||
borderRadius: '8px',
|
||||
padding: '12px',
|
||||
boxShadow: 'var(--shadow-lg)',
|
||||
zIndex: 100,
|
||||
minWidth: '140px',
|
||||
}}
|
||||
role="menu"
|
||||
aria-orientation="horizontal"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gap: '12px',
|
||||
}}
|
||||
>
|
||||
{themes.map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
onClick={() => handleThemeSelect(t.id)}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '6px',
|
||||
background: 'transparent',
|
||||
border: 'none',
|
||||
padding: '4px',
|
||||
borderRadius: '6px',
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s',
|
||||
}}
|
||||
onMouseEnter={(e) => {
|
||||
e.currentTarget.style.background = 'var(--bg-overlay)'
|
||||
}}
|
||||
onMouseLeave={(e) => {
|
||||
e.currentTarget.style.background = 'transparent'
|
||||
}}
|
||||
role="menuitem"
|
||||
aria-current={t.id === theme ? 'true' : undefined}
|
||||
>
|
||||
<ThemeSwatch
|
||||
colors={t.swatch}
|
||||
size={28}
|
||||
active={t.id === theme}
|
||||
/>
|
||||
<span
|
||||
style={{
|
||||
fontSize: 'var(--text-xs)',
|
||||
color: t.id === theme ? 'var(--accent)' : 'var(--text-secondary)',
|
||||
fontWeight: t.id === theme ? 500 : 400,
|
||||
}}
|
||||
>
|
||||
{t.name}
|
||||
</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue