128 lines
5.2 KiB
TypeScript
128 lines
5.2 KiB
TypeScript
import { useState, useEffect } from 'react'
|
|
import { X, Copy, Check } from 'lucide-react'
|
|
import { useDevFlags, DEV_FLAGS, type DevFlagKey } from '../contexts/DevFlagsContext'
|
|
|
|
// Master-detail feature-flag panel (Ctrl/Cmd+Shift+D). List on the left
|
|
// (green = enabled, ring = selected), detail + toggle on the right.
|
|
export function DevPanel() {
|
|
const { flags, setFlag, isPanelOpen, setPanelOpen } = useDevFlags()
|
|
const [selectedKey, setSelectedKey] = useState<DevFlagKey>(DEV_FLAGS[0]?.key)
|
|
const [copied, setCopied] = useState(false)
|
|
|
|
useEffect(() => {
|
|
if (!isPanelOpen) return
|
|
const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setPanelOpen(false) }
|
|
window.addEventListener('keydown', onKey)
|
|
return () => window.removeEventListener('keydown', onKey)
|
|
}, [isPanelOpen, setPanelOpen])
|
|
|
|
if (!isPanelOpen) return null
|
|
|
|
const selected = DEV_FLAGS.find(f => f.key === selectedKey) ?? DEV_FLAGS[0]
|
|
const isOn = selected ? flags[selected.key] : false
|
|
|
|
const copyKey = () => {
|
|
if (!selected) return
|
|
void navigator.clipboard?.writeText(selected.key).then(() => {
|
|
setCopied(true)
|
|
setTimeout(() => setCopied(false), 1200)
|
|
})
|
|
}
|
|
|
|
return (
|
|
<div
|
|
className="fixed inset-0 z-[70] flex items-center justify-center bg-black/60 backdrop-blur-sm"
|
|
onClick={() => setPanelOpen(false)}
|
|
>
|
|
<div
|
|
className="flex h-[440px] w-full max-w-3xl flex-col overflow-hidden rounded-xl border border-zinc-700 bg-zinc-900 shadow-2xl"
|
|
onClick={e => e.stopPropagation()}
|
|
>
|
|
{/* Header */}
|
|
<div className="flex items-center justify-between border-b border-zinc-800 px-5 py-3.5">
|
|
<div>
|
|
<h2 className="text-base font-semibold text-white">Dev Panel</h2>
|
|
<p className="text-[11px] text-zinc-500">Local-only feature toggles, saved on this machine.</p>
|
|
</div>
|
|
<button
|
|
onClick={() => setPanelOpen(false)}
|
|
className="rounded-md p-1.5 text-zinc-400 transition-colors hover:bg-zinc-800 hover:text-white"
|
|
title="Close (Esc)"
|
|
>
|
|
<X className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Body: list + detail */}
|
|
<div className="flex min-h-0 flex-1">
|
|
{/* List */}
|
|
<div className="w-64 shrink-0 overflow-y-auto border-r border-zinc-800 p-2">
|
|
{DEV_FLAGS.map(spec => {
|
|
const on = flags[spec.key]
|
|
const isSelected = spec.key === selectedKey
|
|
return (
|
|
<button
|
|
key={spec.key}
|
|
onClick={() => setSelectedKey(spec.key)}
|
|
className={`mb-1 w-full rounded-md px-3 py-2 text-left text-sm transition-colors ${
|
|
isSelected
|
|
? 'text-white ring-1 ring-blue-500/60 ' + (on ? 'bg-emerald-900/40' : 'bg-zinc-800')
|
|
: on
|
|
? 'bg-emerald-900/30 text-emerald-100 hover:bg-emerald-900/50'
|
|
: 'text-zinc-300 hover:bg-zinc-800'
|
|
}`}
|
|
>
|
|
{spec.label}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
|
|
{/* Detail */}
|
|
<div className="min-w-0 flex-1 overflow-y-auto p-5">
|
|
{selected && (
|
|
<>
|
|
<div className="flex items-start justify-between gap-3">
|
|
<h3 className="text-lg font-semibold text-white">{selected.label}</h3>
|
|
<button
|
|
onClick={copyKey}
|
|
className="flex shrink-0 items-center gap-1.5 font-mono text-[11px] text-zinc-500 transition-colors hover:text-zinc-300"
|
|
title="Copy flag key"
|
|
>
|
|
{selected.key}
|
|
{copied ? <Check className="h-3 w-3 text-green-400" /> : <Copy className="h-3 w-3" />}
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mt-4 flex items-center gap-3">
|
|
<span className="text-xs font-medium uppercase tracking-wide text-zinc-400">Enabled</span>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={isOn}
|
|
onClick={() => setFlag(selected.key, !isOn)}
|
|
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none ${
|
|
isOn ? 'bg-blue-500' : 'bg-zinc-600'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
isOn ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
<div className="mt-6">
|
|
<p className="mb-1 text-xs font-medium text-zinc-400">Description</p>
|
|
<p className="text-sm leading-relaxed text-zinc-300">{selected.description}</p>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|