2584c45644
The confirm dialog painted its panel with `var(--bg-surface, #1e1e2e)`, but `--bg-surface` is not defined by any stylesheet in the project. Every theme therefore fell through to the hardcoded dark `#1e1e2e`. That went unnoticed under the six dark themes, whose `--text` is light and so still contrasted against the dark panel. The paper theme is the only light palette: its `--text` is `#1e293b`, which put dark text on the dark panel at roughly 1.05:1 contrast and made the title and body invisible. Point the panel at `--bg-elevated` so it tracks the active palette, and give the title and body explicit `--text` / `--text-secondary` colors instead of relying on inherited color plus `opacity: 0.7`, which is equally unreliable over a light surface. Add a `--border` outline so the now-white panel still reads as a distinct layer above the scrim. Verified by hand across all seven themes.
122 lines
4.1 KiB
TypeScript
122 lines
4.1 KiB
TypeScript
import { useState, useCallback } from 'react'
|
|
import type { Project } from '../types/kanban'
|
|
import { useI18n } from '../i18n'
|
|
|
|
interface ProjectSelectorProps {
|
|
projects: Project[]
|
|
activeId: string
|
|
onSelect: (id: string) => void
|
|
onCreate: (id: string) => void
|
|
onDelete?: (id: string) => void
|
|
}
|
|
|
|
export function ProjectSelector({ projects, activeId, onSelect, onCreate, onDelete }: ProjectSelectorProps) {
|
|
const { t } = useI18n()
|
|
const [creating, setCreating] = useState(false)
|
|
const [newName, setNewName] = useState('')
|
|
const [confirmDelete, setConfirmDelete] = useState<string | null>(null)
|
|
|
|
const handleCreate = useCallback(() => {
|
|
const id = newName.trim().toLowerCase().replace(/[^a-z0-9_-]+/g, '-').replace(/^-|-$/g, '')
|
|
if (!id) return
|
|
onCreate(id)
|
|
setNewName('')
|
|
setCreating(false)
|
|
}, [newName, onCreate])
|
|
|
|
const handleDelete = useCallback(() => {
|
|
if (!confirmDelete || !onDelete) return
|
|
onDelete(confirmDelete)
|
|
setConfirmDelete(null)
|
|
}, [confirmDelete, onDelete])
|
|
|
|
return (
|
|
<div className="project-selector" style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
<select
|
|
className="theme-select"
|
|
value={activeId}
|
|
onChange={e => onSelect(e.target.value)}
|
|
title={t('project.switch')}
|
|
aria-label={t('project.switch')}
|
|
>
|
|
{projects.map(p => (
|
|
<option key={p.id} value={p.id}>{p.name}</option>
|
|
))}
|
|
</select>
|
|
{creating ? (
|
|
<form
|
|
onSubmit={e => { e.preventDefault(); handleCreate() }}
|
|
style={{ display: 'flex', gap: 4 }}
|
|
>
|
|
<input
|
|
autoFocus
|
|
className="theme-select"
|
|
value={newName}
|
|
placeholder={t('project.placeholder')}
|
|
onChange={e => setNewName(e.target.value)}
|
|
onKeyDown={e => { if (e.key === 'Escape') setCreating(false) }}
|
|
style={{ width: 120 }}
|
|
/>
|
|
<button type="submit" className="pill-btn" style={{ fontSize: 11, padding: '2px 8px' }}>+</button>
|
|
</form>
|
|
) : (
|
|
<button
|
|
className="pill-btn"
|
|
onClick={() => setCreating(true)}
|
|
title={t('project.new')}
|
|
style={{ fontSize: 11, padding: '2px 8px' }}
|
|
>
|
|
+
|
|
</button>
|
|
)}
|
|
{onDelete && activeId !== 'default' && !confirmDelete && (
|
|
<button
|
|
className="pill-btn"
|
|
onClick={() => setConfirmDelete(activeId)}
|
|
title={t('project.delete')}
|
|
style={{ fontSize: 11, padding: '2px 8px', color: '#ef4444' }}
|
|
>
|
|
{t('project.deleteButton')}
|
|
</button>
|
|
)}
|
|
|
|
{confirmDelete && (
|
|
<div className="project-delete-confirm" style={{
|
|
position: 'fixed', inset: 0, zIndex: 9999,
|
|
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
|
background: 'rgba(0,0,0,0.5)',
|
|
}}>
|
|
<div style={{
|
|
background: 'var(--bg-elevated)', borderRadius: 12, padding: '24px 32px',
|
|
border: '1px solid var(--border)', color: 'var(--text)',
|
|
maxWidth: 400, boxShadow: '0 8px 32px rgba(0,0,0,0.4)', textAlign: 'center',
|
|
}}>
|
|
<p style={{ margin: '0 0 8px', fontWeight: 600, fontSize: 15, color: 'var(--text)' }}>
|
|
{t('project.confirmTitle', { name: confirmDelete })}
|
|
</p>
|
|
<p style={{ margin: '0 0 20px', fontSize: 13, color: 'var(--text-secondary)' }}>
|
|
{t('project.confirmBody')}
|
|
</p>
|
|
<div style={{ display: 'flex', gap: 8, justifyContent: 'center' }}>
|
|
<button
|
|
className="pill-btn"
|
|
onClick={() => setConfirmDelete(null)}
|
|
style={{ padding: '6px 18px', fontSize: 13 }}
|
|
>
|
|
{t('common.cancel')}
|
|
</button>
|
|
<button
|
|
className="pill-btn"
|
|
onClick={handleDelete}
|
|
style={{ padding: '6px 18px', fontSize: 13, background: '#ef4444', color: '#fff' }}
|
|
>
|
|
{t('common.delete')}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|