Revert "Dark theme for Forecast view controls and operation panel"

This reverts commit cda3943515.
This commit is contained in:
Paul Trowbridge 2026-04-25 21:23:43 -04:00
parent cda3943515
commit 742d4b4cc4

View File

@ -118,11 +118,8 @@ export default function Forecast() {
const unitsCol = colMetaRef.current.find(c => c.role === 'units')?.cname const unitsCol = colMetaRef.current.find(c => c.role === 'units')?.cname
if (!valueCol && !unitsCol) return if (!valueCol && !unitsCol) return
try { try {
const dimNames = new Set(colMetaRef.current.filter(c => c.role === 'dimension').map(c => c.cname))
const filters = [ const filters = [
...Object.entries(sliceObj) ...Object.entries(sliceObj).map(([col, val]) => [col, '==', val]),
.filter(([col]) => dimNames.has(col))
.map(([col, val]) => [col, '==', val]),
['pf_iter', '!=', 'reference'], ['pf_iter', '!=', 'reference'],
] ]
const view = await tableRef.current.view({ filter: filters }) const view = await tableRef.current.view({ filter: filters })
@ -381,123 +378,110 @@ export default function Forecast() {
const hasSlice = Object.keys(slice).length > 0 const hasSlice = Object.keys(slice).length > 0
return ( return (
<div className="h-full flex flex-col bg-gray-900"> <div className="h-full flex flex-col">
{/* Source / version bar */} {/* Source / version bar */}
<div className="px-3 py-2 border-b border-gray-700 bg-gray-900 flex items-center gap-3 shrink-0 flex-wrap"> <div className="px-3 py-2 border-b border-gray-200 bg-white flex items-center gap-3 shrink-0 flex-wrap">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Source</span> <span className="text-xs text-gray-500">Source</span>
<select value={sourceId} onChange={e => setSourceId(e.target.value)} className="border border-gray-600 rounded px-2 py-1 text-sm bg-gray-800 text-gray-200"> <select value={sourceId} onChange={e => setSourceId(e.target.value)} className="border border-gray-200 rounded px-2 py-1 text-sm bg-white">
{sources.map(s => <option key={s.id} value={s.id}>{s.tname}</option>)} {sources.map(s => <option key={s.id} value={s.id}>{s.tname}</option>)}
</select> </select>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-xs text-gray-500">Version</span> <span className="text-xs text-gray-500">Version</span>
<select value={versionId} onChange={e => setVersionId(e.target.value)} className="border border-gray-600 rounded px-2 py-1 text-sm bg-gray-800 text-gray-200" disabled={!versions.length}> <select value={versionId} onChange={e => setVersionId(e.target.value)} className="border border-gray-200 rounded px-2 py-1 text-sm bg-white" disabled={!versions.length}>
{versions.length === 0 {versions.length === 0
? <option value=""> no versions </option> ? <option value=""> no versions </option>
: versions.map(v => <option key={v.id} value={v.id}>{v.name}</option>)} : versions.map(v => <option key={v.id} value={v.id}>{v.name}</option>)}
</select> </select>
{selectedVersion && ( {selectedVersion && (
<span className={`text-xs font-medium ${selectedVersion.status === 'open' ? 'text-green-400' : 'text-gray-500'}`}> <span className={`text-xs font-medium ${selectedVersion.status === 'open' ? 'text-green-600' : 'text-gray-400'}`}>
{selectedVersion.status} {selectedVersion.status}
</span> </span>
)} )}
</div> </div>
{msg && (
<span className={`text-xs font-medium px-2 py-0.5 rounded ml-auto ${msg.type === 'error' ? 'bg-red-50 text-red-600' : 'bg-green-50 text-green-600'}`}>
{msg.text}
</span>
)}
</div> </div>
{/* Toolbar bar */} {/* Layout / depth bar */}
<div className="px-3 py-1.5 border-b border-gray-700 bg-gray-900 flex items-center gap-3 shrink-0 flex-wrap text-xs"> <div className="px-3 py-1.5 border-b border-gray-200 bg-white flex items-center gap-2 shrink-0 flex-wrap">
<span className="text-xs text-gray-400 uppercase tracking-wide">Layouts</span>
{/* Layouts group */}
<div className="flex items-center gap-1.5">
<span className="text-gray-600 uppercase tracking-wide" style={{fontSize:'10px'}}>Layout</span>
{layouts.map(l => ( {layouts.map(l => (
<div key={l.id} onClick={() => applyLayout(l)} <div key={l.id} onClick={() => applyLayout(l)}
className={`flex items-center gap-1 rounded px-2 py-0.5 cursor-pointer border transition-colors className={`flex items-center gap-1 text-xs rounded px-2 py-0.5 cursor-pointer border transition-colors
${activeLayoutId === l.id ? 'bg-blue-900 border-blue-600 text-blue-300' : 'bg-gray-800 border-gray-600 text-gray-400 hover:border-gray-400 hover:text-gray-200'}`}> ${activeLayoutId === l.id ? 'bg-blue-50 border-blue-300 text-blue-700' : 'bg-white border-gray-200 text-gray-600 hover:border-gray-400'}`}>
{l.name} {l.name}
<button onClick={e => deleteLayout(l.id, e)} className="text-gray-600 hover:text-red-400 text-sm leading-none ml-0.5">×</button> <button onClick={e => deleteLayout(l.id, e)} className="text-gray-300 hover:text-red-400 text-sm leading-none ml-0.5">×</button>
</div> </div>
))} ))}
{activeLayoutId !== null && !showSaveAs && (
<button onClick={handleSaveOver} className="text-xs border border-blue-200 text-blue-500 hover:text-blue-700 rounded px-2 py-0.5">Save</button>
)}
{showSaveAs ? ( {showSaveAs ? (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<input autoFocus value={saveAsName} onChange={e => setSaveAsName(e.target.value)} <input autoFocus value={saveAsName} onChange={e => setSaveAsName(e.target.value)}
onKeyDown={e => { if (e.key === 'Enter') handleSaveAs(); if (e.key === 'Escape') { setShowSaveAs(false); setSaveAsName('') } }} onKeyDown={e => { if (e.key === 'Enter') handleSaveAs(); if (e.key === 'Escape') { setShowSaveAs(false); setSaveAsName('') } }}
placeholder="Layout name…" className="border border-gray-600 rounded px-2 py-0.5 w-32 bg-gray-800 text-gray-200 focus:outline-none focus:border-blue-500 placeholder:text-gray-600" /> placeholder="Layout name…" className="text-xs border border-gray-300 rounded px-2 py-0.5 w-32 focus:outline-none focus:border-blue-400" />
<button onClick={handleSaveAs} className="text-blue-400 hover:text-blue-300 px-1">Save</button> <button onClick={handleSaveAs} className="text-xs text-blue-600 hover:text-blue-800 px-1">Save</button>
<button onClick={() => { setShowSaveAs(false); setSaveAsName('') }} className="text-gray-600 hover:text-gray-400 px-1">Cancel</button> <button onClick={() => { setShowSaveAs(false); setSaveAsName('') }} className="text-xs text-gray-400 px-1">Cancel</button>
</div> </div>
) : ( ) : (
<> <button onClick={() => setShowSaveAs(true)} className="text-xs border border-dashed border-gray-200 text-gray-400 hover:text-gray-600 rounded px-2 py-0.5">
{activeLayoutId !== null && ( + Save as
<button onClick={handleSaveOver} className="border border-blue-700 text-blue-400 hover:text-blue-300 hover:border-blue-500 rounded px-2 py-0.5">
Save
</button> </button>
)} )}
<button onClick={() => setShowSaveAs(true)} className="border border-dashed border-gray-700 text-gray-500 hover:text-gray-300 hover:border-gray-500 rounded px-2 py-0.5">
Save as
</button>
{activeLayoutId !== null && ( {activeLayoutId !== null && (
<button onClick={resetLayout} className="text-gray-600 hover:text-red-400">Reset</button> <button onClick={resetLayout} className="text-xs text-gray-300 hover:text-gray-500">reset</button>
)} )}
</>
)}
</div>
<div className="w-px h-4 bg-gray-700 shrink-0" /> <button onClick={openLog} disabled={!versionId}
className="text-xs border border-gray-200 rounded px-2 py-0.5 text-gray-500 hover:bg-gray-50 disabled:opacity-40">
History
</button>
{/* Expand depth group */} {/* Depth controls */}
<div className="flex items-center gap-1.5"> <div className="ml-auto flex items-center gap-1.5">
<span className="text-gray-600 uppercase tracking-wide" style={{fontSize:'10px'}}>Expand</span> <span className="text-xs text-gray-400">depth</span>
{[0, 1, 2, 3].map(d => ( {[0, 1, 2, 3].map(d => (
<button key={d} onClick={() => applyDepth(d)} <button key={d} onClick={() => applyDepth(d)}
className={`border rounded px-1.5 py-0.5 transition-colors className={`text-xs border rounded px-1.5 py-0.5 transition-colors
${expandDepthRef.current === d ? 'border-blue-600 text-blue-400 bg-blue-900' : 'border-gray-600 text-gray-400 hover:border-gray-400 hover:text-gray-200'}`}> ${expandDepthRef.current === d ? 'border-blue-300 text-blue-600 bg-blue-50' : 'border-gray-200 text-gray-500 hover:border-gray-400'}`}>
{d} {d}
</button> </button>
))} ))}
</div>
<div className="w-px h-4 bg-gray-700 shrink-0" />
{/* Data group */}
<div className="flex items-center gap-1.5">
<button onClick={() => initViewer(versionId, sourceId)} disabled={loading || !versionId} <button onClick={() => initViewer(versionId, sourceId)} disabled={loading || !versionId}
className="border border-gray-600 rounded px-2 py-0.5 text-gray-400 hover:text-gray-200 hover:border-gray-400 disabled:opacity-30"> className="text-xs border border-gray-200 rounded px-2 py-0.5 text-gray-500 hover:bg-gray-50 disabled:opacity-40 ml-2">
{loading ? 'Loading…' : 'Refresh data'} {loading ? 'Loading…' : 'Refresh'}
</button>
<button onClick={openLog} disabled={!versionId}
className="border border-gray-600 rounded px-2 py-0.5 text-gray-400 hover:text-gray-200 hover:border-gray-400 disabled:opacity-30">
Change log
</button> </button>
</div> </div>
{msg && (
<span className={`ml-2 font-medium px-2 py-0.5 rounded ${msg.type === 'error' ? 'bg-red-900 text-red-400' : 'bg-green-900 text-green-400'}`}>
{msg.text}
</span>
)}
</div> </div>
{/* History modal */} {/* History modal */}
{showLog && ( {showLog && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60" onClick={() => setShowLog(false)}> <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40" onClick={() => setShowLog(false)}>
<div className="bg-gray-900 border border-gray-700 rounded-lg shadow-2xl w-full max-w-4xl mx-4 flex flex-col max-h-[80vh]" onClick={e => e.stopPropagation()}> <div className="bg-white rounded-lg shadow-xl w-full max-w-4xl mx-4 flex flex-col max-h-[80vh]" onClick={e => e.stopPropagation()}>
<div className="flex items-center justify-between px-5 py-3 border-b border-gray-700 shrink-0"> <div className="flex items-center justify-between px-5 py-3 border-b border-gray-200 shrink-0">
<span className="font-medium text-gray-200 text-sm">Change log</span> <span className="font-medium text-gray-700 text-sm">Change History</span>
<button onClick={() => setShowLog(false)} className="text-gray-500 hover:text-gray-300 text-lg leading-none">×</button> <button onClick={() => setShowLog(false)} className="text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
</div> </div>
<div className="overflow-y-auto flex-1"> <div className="overflow-y-auto flex-1">
{logLoading ? ( {logLoading ? (
<div className="p-8 text-center text-sm text-gray-500">Loading</div> <div className="p-8 text-center text-sm text-gray-400">Loading</div>
) : logEntries.length === 0 ? ( ) : logEntries.length === 0 ? (
<div className="p-8 text-center text-sm text-gray-500">No log entries yet.</div> <div className="p-8 text-center text-sm text-gray-400">No log entries yet.</div>
) : ( ) : (
<table className="w-full text-xs border-collapse"> <table className="w-full text-xs border-collapse">
<thead className="sticky top-0 bg-gray-800 text-gray-500 uppercase tracking-wide" style={{fontSize:'10px'}}> <thead className="sticky top-0 bg-gray-50 text-gray-400 uppercase tracking-wide" style={{fontSize:'10px'}}>
<tr> <tr>
<th className="text-left px-4 py-2 font-medium w-32">Time</th> <th className="text-left px-4 py-2 font-medium w-32">Time</th>
<th className="text-left px-4 py-2 font-medium w-24">Op</th> <th className="text-left px-4 py-2 font-medium w-24">Op</th>
@ -509,15 +493,15 @@ export default function Forecast() {
</thead> </thead>
<tbody> <tbody>
{logEntries.map(entry => ( {logEntries.map(entry => (
<tr key={entry.id} className="border-t border-gray-800 hover:bg-gray-800/50"> <tr key={entry.id} className="border-t border-gray-100 hover:bg-gray-50">
<td className="px-4 py-2 text-gray-500 whitespace-nowrap">{fmtStamp(entry.stamp)}</td> <td className="px-4 py-2 text-gray-400 whitespace-nowrap">{fmtStamp(entry.stamp)}</td>
<td className="px-4 py-2"> <td className="px-4 py-2">
<span className={`px-1.5 py-0.5 rounded text-xs font-medium ${opBadge(entry.operation)}`}> <span className={`px-1.5 py-0.5 rounded text-xs font-medium ${opBadge(entry.operation)}`}>
{entry.operation} {entry.operation}
</span> </span>
</td> </td>
<td className="px-4 py-2 text-gray-400 font-mono">{fmtSlice(entry.slice)}</td> <td className="px-4 py-2 text-gray-600 font-mono">{fmtSlice(entry.slice)}</td>
<td className="px-4 py-2 text-gray-400 max-w-xs"> <td className="px-4 py-2 text-gray-600 max-w-xs">
{editingNote?.id === entry.id ? ( {editingNote?.id === entry.id ? (
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<input autoFocus value={editingNote.text} <input autoFocus value={editingNote.text}
@ -526,15 +510,15 @@ export default function Forecast() {
if (e.key === 'Enter') saveNote(entry.id, editingNote.text) if (e.key === 'Enter') saveNote(entry.id, editingNote.text)
if (e.key === 'Escape') setEditingNote(null) if (e.key === 'Escape') setEditingNote(null)
}} }}
className="border border-blue-600 rounded px-1.5 py-0.5 text-xs flex-1 bg-gray-800 text-gray-200 focus:outline-none" /> className="border border-blue-300 rounded px-1.5 py-0.5 text-xs flex-1 focus:outline-none" />
<button onClick={() => saveNote(entry.id, editingNote.text)} className="text-blue-400 hover:text-blue-300"></button> <button onClick={() => saveNote(entry.id, editingNote.text)} className="text-blue-600 hover:text-blue-800"></button>
<button onClick={() => setEditingNote(null)} className="text-gray-500 hover:text-gray-300"></button> <button onClick={() => setEditingNote(null)} className="text-gray-400 hover:text-gray-600"></button>
</div> </div>
) : ( ) : (
<span onClick={() => setEditingNote({ id: entry.id, text: entry.note || '' })} <span onClick={() => setEditingNote({ id: entry.id, text: entry.note || '' })}
className="cursor-text hover:bg-gray-700 rounded px-1 -mx-1 block truncate" className="cursor-text hover:bg-blue-50 rounded px-1 -mx-1 block truncate"
title={entry.note || 'Click to add note'}> title={entry.note || 'Click to add note'}>
{entry.note || <span className="text-gray-600 italic">add note</span>} {entry.note || <span className="text-gray-300 italic">add note</span>}
</span> </span>
)} )}
</td> </td>
@ -543,7 +527,7 @@ export default function Forecast() {
<button <button
onClick={() => undoEntry(entry.id)} onClick={() => undoEntry(entry.id)}
disabled={undoingId === entry.id} disabled={undoingId === entry.id}
className="text-xs border border-red-800 text-red-500 hover:text-red-400 hover:border-red-600 rounded px-2 py-0.5 disabled:opacity-40 whitespace-nowrap"> className="text-xs border border-red-200 text-red-400 hover:text-red-600 hover:border-red-400 rounded px-2 py-0.5 disabled:opacity-40 whitespace-nowrap">
{undoingId === entry.id ? '…' : 'Undo'} {undoingId === entry.id ? '…' : 'Undo'}
</button> </button>
</td> </td>
@ -562,40 +546,40 @@ export default function Forecast() {
{/* Perspective viewer */} {/* Perspective viewer */}
<div className="relative flex-1 min-w-0"> <div className="relative flex-1 min-w-0">
{loading && ( {loading && (
<div className="absolute inset-0 flex items-center justify-center bg-gray-900 z-10"> <div className="absolute inset-0 flex items-center justify-center bg-gray-50 z-10">
<span className="text-sm text-gray-500">Loading</span> <span className="text-sm text-gray-400">Loading</span>
</div> </div>
)} )}
<perspective-viewer ref={viewerRef} style={{ position: 'absolute', inset: 0 }} /> <perspective-viewer ref={viewerRef} style={{ position: 'absolute', inset: 0 }} />
</div> </div>
{/* Drag handle */} {/* Drag handle */}
<div onMouseDown={onDragStart} className="w-1 shrink-0 cursor-col-resize hover:bg-blue-500 bg-gray-800 transition-colors" /> <div onMouseDown={onDragStart} className="w-1 shrink-0 cursor-col-resize hover:bg-blue-400 bg-transparent transition-colors" />
{/* Operation panel */} {/* Operation panel */}
<div className="shrink-0 border-l border-gray-700 bg-gray-900 flex flex-col overflow-y-auto text-xs" style={{ width: panelWidth }}> <div className="shrink-0 border-l border-gray-200 bg-white flex flex-col overflow-y-auto text-xs" style={{ width: panelWidth }}>
<div className="p-3 border-b border-gray-800"> <div className="p-3 border-b border-gray-100">
<div className="font-medium text-gray-600 uppercase tracking-wide mb-2" style={{fontSize:'10px'}}>Slice</div> <div className="font-medium text-gray-400 uppercase tracking-wide mb-2" style={{fontSize:'10px'}}>Slice</div>
{!hasSlice ? ( {!hasSlice ? (
<div className="text-gray-600 italic">Click a pivot row to select a slice</div> <div className="text-gray-300 italic">Click a pivot row to select a slice</div>
) : ( ) : (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
{Object.entries(slice).map(([k, v]) => ( {Object.entries(slice).map(([k, v]) => (
<div key={k} className="text-gray-300"> <div key={k} className="text-gray-700">
<span className="text-gray-500">{k}</span> = <span className="font-medium">{v}</span> <span className="text-gray-400">{k}</span> = <span className="font-medium">{v}</span>
</div> </div>
))} ))}
<button onClick={() => setSlice({})} className="text-gray-600 hover:text-red-400 mt-1 text-left">Clear</button> <button onClick={() => setSlice({})} className="text-gray-300 hover:text-red-500 mt-1 text-left">Clear</button>
</div> </div>
)} )}
</div> </div>
{hasSlice && ( {hasSlice && (
<> <>
<div className="flex border-b border-gray-800"> <div className="flex border-b border-gray-100">
{['scale', 'recode', 'clone'].map(op => ( {['scale', 'recode', 'clone'].map(op => (
<button key={op} onClick={() => setActiveOp(op)} <button key={op} onClick={() => setActiveOp(op)}
className={`flex-1 py-2 capitalize ${activeOp === op ? 'border-b-2 border-blue-500 text-blue-400 font-medium' : 'text-gray-600 hover:text-gray-300'}`}> className={`flex-1 py-2 capitalize ${activeOp === op ? 'border-b-2 border-blue-500 text-blue-600 font-medium' : 'text-gray-400 hover:text-gray-600'}`}>
{op} {op}
</button> </button>
))} ))}
@ -604,10 +588,10 @@ export default function Forecast() {
<div className="p-3 flex flex-col gap-2.5"> <div className="p-3 flex flex-col gap-2.5">
{activeOp === 'scale' && <> {activeOp === 'scale' && <>
{/* Mode toggle */} {/* Mode toggle */}
<div className="flex rounded border border-gray-700 overflow-hidden"> <div className="flex rounded border border-gray-200 overflow-hidden">
{[['target','= Target'],['delta','Δ Increment']].map(([m, label]) => ( {[['target','= Target'],['delta','Δ Increment']].map(([m, label]) => (
<button key={m} onClick={() => { setScaleMode(m); setScaleValue(''); setScaleUnits('') }} <button key={m} onClick={() => { setScaleMode(m); setScaleValue(''); setScaleUnits('') }}
className={`flex-1 py-1 text-xs ${scaleMode === m ? 'bg-blue-600 text-white' : 'bg-gray-800 text-gray-400 hover:bg-gray-700'}`}> className={`flex-1 py-1 text-xs ${scaleMode === m ? 'bg-blue-600 text-white' : 'bg-white text-gray-500 hover:bg-gray-50'}`}>
{label} {label}
</button> </button>
))} ))}
@ -616,9 +600,9 @@ export default function Forecast() {
{/* Value row */} {/* Value row */}
{currentTotals?.valueCol && ( {currentTotals?.valueCol && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center justify-between text-gray-500"> <div className="flex items-center justify-between text-gray-400">
<span>{currentTotals.valueCol}</span> <span>{currentTotals.valueCol}</span>
<span className="font-mono text-gray-300">{currentTotals.value?.toLocaleString(undefined, {maximumFractionDigits:2})}</span> <span className="font-mono">{currentTotals.value?.toLocaleString(undefined, {maximumFractionDigits:2})}</span>
</div> </div>
<input type="number" step="any" value={scaleValue} <input type="number" step="any" value={scaleValue}
onChange={e => setScaleValue(e.target.value)} onChange={e => setScaleValue(e.target.value)}
@ -630,9 +614,9 @@ export default function Forecast() {
{/* Units row */} {/* Units row */}
{currentTotals?.unitsCol && ( {currentTotals?.unitsCol && (
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<div className="flex items-center justify-between text-gray-500"> <div className="flex items-center justify-between text-gray-400">
<span>{currentTotals.unitsCol}</span> <span>{currentTotals.unitsCol}</span>
<span className="font-mono text-gray-300">{currentTotals.units?.toLocaleString(undefined, {maximumFractionDigits:2})}</span> <span className="font-mono">{currentTotals.units?.toLocaleString(undefined, {maximumFractionDigits:2})}</span>
</div> </div>
<input type="number" step="any" value={scaleUnits} <input type="number" step="any" value={scaleUnits}
onChange={e => setScaleUnits(e.target.value)} onChange={e => setScaleUnits(e.target.value)}
@ -652,7 +636,7 @@ export default function Forecast() {
</>} </>}
{activeOp === 'recode' && <> {activeOp === 'recode' && <>
<p className="text-gray-500">New values for dimensions to replace. Leave blank to keep.</p> <p className="text-gray-400">New values for dimensions to replace. Leave blank to keep.</p>
{dimCols.map(c => ( {dimCols.map(c => (
<Row key={c.cname} label={c.cname}> <Row key={c.cname} label={c.cname}>
<input value={recodeSet[c.cname] || ''} onChange={e => setRecodeSet(s => ({ ...s, [c.cname]: e.target.value }))} <input value={recodeSet[c.cname] || ''} onChange={e => setRecodeSet(s => ({ ...s, [c.cname]: e.target.value }))}
@ -664,7 +648,7 @@ export default function Forecast() {
</>} </>}
{activeOp === 'clone' && <> {activeOp === 'clone' && <>
<p className="text-gray-500">Override dimensions on cloned rows. Leave blank to keep.</p> <p className="text-gray-400">Override dimensions on cloned rows. Leave blank to keep.</p>
{dimCols.map(c => ( {dimCols.map(c => (
<Row key={c.cname} label={c.cname}> <Row key={c.cname} label={c.cname}>
<input value={cloneSet[c.cname] || ''} onChange={e => setCloneSet(s => ({ ...s, [c.cname]: e.target.value }))} <input value={cloneSet[c.cname] || ''} onChange={e => setCloneSet(s => ({ ...s, [c.cname]: e.target.value }))}
@ -684,7 +668,7 @@ export default function Forecast() {
) )
} }
const inp = 'border border-gray-700 rounded px-2 py-1 text-xs flex-1 bg-gray-800 text-gray-200 placeholder:text-gray-600 min-w-0 focus:outline-none focus:border-blue-600' const inp = 'border border-gray-200 rounded px-2 py-1 text-xs flex-1 bg-white min-w-0'
function fmtStamp(stamp) { function fmtStamp(stamp) {
return new Date(stamp).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' }) return new Date(stamp).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit' })
@ -696,18 +680,18 @@ function fmtSlice(slice) {
} }
const OP_BADGE = { const OP_BADGE = {
baseline: 'bg-gray-700 text-gray-300', baseline: 'bg-gray-100 text-gray-600',
reference: 'bg-blue-900 text-blue-300', reference: 'bg-blue-50 text-blue-600',
scale: 'bg-green-900 text-green-400', scale: 'bg-green-50 text-green-700',
recode: 'bg-amber-900 text-amber-400', recode: 'bg-amber-50 text-amber-700',
clone: 'bg-purple-900 text-purple-400', clone: 'bg-purple-50 text-purple-700',
} }
function opBadge(op) { return OP_BADGE[op] || 'bg-gray-700 text-gray-400' } function opBadge(op) { return OP_BADGE[op] || 'bg-gray-100 text-gray-500' }
function Row({ label, children }) { function Row({ label, children }) {
return ( return (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-gray-500 w-14 shrink-0 truncate" title={label}>{label}</span> <span className="text-gray-400 w-14 shrink-0 truncate" title={label}>{label}</span>
{children} {children}
</div> </div>
) )
@ -715,7 +699,7 @@ function Row({ label, children }) {
function Submit({ onClick, children }) { function Submit({ onClick, children }) {
return ( return (
<button onClick={onClick} className="mt-1 bg-blue-600 text-white text-xs px-3 py-1.5 rounded hover:bg-blue-500 w-full"> <button onClick={onClick} className="mt-1 bg-blue-600 text-white text-xs px-3 py-1.5 rounded hover:bg-blue-700 w-full">
{children} {children}
</button> </button>
) )