Add icon set selector to admin page with persistent localStorage storage
- Appearance card in admin grid shows all 4 icon styles (color, flat, emoji, ascii) with live previews of all 6 statuses and a description for each - Selected style persists across reloads via localStorage (LS_KEY alongside expandedMap) - Stats overview card moved before Appearance card in admin grid - Screenshot images added to README with titles before each image
This commit is contained in:
+47
-1
@@ -399,7 +399,50 @@ function Kpi({ label, value, delta, unit, accent, deltaInverse }) {
|
||||
);
|
||||
}
|
||||
|
||||
function AdminPage({ data, onClose, history, onForceSave, onForceBackup, onCompact, onRestore, onDownloadBackup }) {
|
||||
const ICON_STYLES = [
|
||||
{ value: 'color', label: 'Color', hint: 'Filled circles with semantic colors' },
|
||||
{ value: 'flat', label: 'Flat', hint: 'Minimal monochrome SVG squares' },
|
||||
{ value: 'emoji', label: 'Emoji', hint: 'Native emoji glyphs' },
|
||||
{ value: 'ascii', label: 'ASCII', hint: 'Plain text glyphs: [ ] [/] [!] …' },
|
||||
];
|
||||
|
||||
function AppearanceCard({ tweaks, setTweak }) {
|
||||
const current = tweaks?.status_style || 'color';
|
||||
return (
|
||||
<section className="card">
|
||||
<div className="card-head"><h3>Appearance</h3></div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{ICON_STYLES.map(opt => (
|
||||
<label key={opt.value}
|
||||
style={{
|
||||
display: 'flex', alignItems: 'center', gap: 10,
|
||||
padding: '7px 10px', borderRadius: 8, cursor: 'pointer',
|
||||
background: current === opt.value ? 'var(--bg-2)' : 'transparent',
|
||||
border: `1px solid ${current === opt.value ? 'var(--rule)' : 'transparent'}`,
|
||||
}}>
|
||||
<input type="radio" name="icon-style" value={opt.value}
|
||||
checked={current === opt.value}
|
||||
onChange={() => setTweak('status_style', opt.value)}
|
||||
style={{ accentColor: 'var(--accent)', flexShrink: 0 }}/>
|
||||
<div style={{ display: 'flex', gap: 5, flexShrink: 0 }}>
|
||||
{['open','in-progress','blocked','postponed','done','cancelled'].map(s => (
|
||||
<StatusStyleContext.Provider key={s} value={opt.value}>
|
||||
<StatusIcon status={s} size={14}/>
|
||||
</StatusStyleContext.Provider>
|
||||
))}
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
<span style={{ fontSize: 13, fontWeight: 500, color: 'var(--ink)' }}>{opt.label}</span>
|
||||
<span style={{ fontSize: 11.5, color: 'var(--ink-3)' }}>{opt.hint}</span>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
function AdminPage({ data, onClose, history, tweaks, setTweak, onForceSave, onForceBackup, onCompact, onRestore, onDownloadBackup }) {
|
||||
const [previewBackup, setPreviewBackup] = useState(null);
|
||||
|
||||
const counts = countByStatus(data.entries);
|
||||
@@ -490,6 +533,9 @@ function AdminPage({ data, onClose, history, onForceSave, onForceBackup, onCompa
|
||||
{/* Stats overview */}
|
||||
<StatsOverview dataStats={data.stats} mostActiveProject={data.stats.mostActiveProject} history={history}/>
|
||||
|
||||
{/* Appearance */}
|
||||
<AppearanceCard tweaks={tweaks} setTweak={setTweak}/>
|
||||
|
||||
{/* Status mix */}
|
||||
<section className="card">
|
||||
<div className="card-head"><h3>Status mix</h3></div>
|
||||
|
||||
+11
-15
@@ -12,10 +12,10 @@ function loadLocalState() {
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// Only UI state (which rows are expanded) is persisted locally.
|
||||
// Only UI state (expanded rows, tweaks) is persisted locally.
|
||||
// Backlog data always lives in backlog.md — never in localStorage.
|
||||
function saveLocalState(expandedMap) {
|
||||
try { localStorage.setItem(LS_KEY, JSON.stringify({ expandedMap })); } catch { /* quota / private mode */ }
|
||||
function saveLocalState(expandedMap, tweaks) {
|
||||
try { localStorage.setItem(LS_KEY, JSON.stringify({ expandedMap, tweaks })); } catch { /* quota / private mode */ }
|
||||
}
|
||||
|
||||
// Empty starting state — used when there is no saved data and no seed data loaded.
|
||||
@@ -57,14 +57,8 @@ function App() {
|
||||
const [confirm, setConfirm] = useStateMain(null);
|
||||
const [needsConnect, setNeedsConnect] = useStateMain(false);
|
||||
|
||||
const [tweaks, setTweak] = useTweaks({
|
||||
accent_hue: 35,
|
||||
density: 'comfortable',
|
||||
show_ids: false,
|
||||
paper_texture: true,
|
||||
status_style: 'color',
|
||||
sort_mode: 'priority',
|
||||
});
|
||||
const TWEAK_DEFAULTS = { accent_hue: 35, density: 'comfortable', show_ids: false, paper_texture: true, status_style: 'color', sort_mode: 'priority' };
|
||||
const [tweaks, setTweak] = useTweaks({ ...TWEAK_DEFAULTS, ...(loadLocalState()?.tweaks ?? {}) });
|
||||
|
||||
// Refs for async callbacks that need latest state without stale closures.
|
||||
const latestData = useRefMain(data);
|
||||
@@ -158,10 +152,10 @@ function App() {
|
||||
return () => { cancelled = true; SyncPoller.stop(); };
|
||||
}, []);
|
||||
|
||||
// Persist expanded/collapsed row state locally. Data itself lives in backlog.md.
|
||||
// Persist expanded/collapsed row state and tweaks locally. Data itself lives in backlog.md.
|
||||
useEffectMain(() => {
|
||||
saveLocalState(latestExpandedMap.current);
|
||||
}, [expandedMap]);
|
||||
saveLocalState(latestExpandedMap.current, tweaks);
|
||||
}, [expandedMap, tweaks]);
|
||||
|
||||
const showToast = (msg, kind = 'ok') => {
|
||||
setToast({ msg, kind, t: Date.now() });
|
||||
@@ -222,7 +216,7 @@ function App() {
|
||||
const cm = content.match(/checksum:\s*(sha256:[a-f0-9]+)/);
|
||||
if (cm) SyncPoller.lastChecksum = cm[1];
|
||||
}
|
||||
saveLocalState(em);
|
||||
saveLocalState(em, tweaks);
|
||||
isDirtyRef.current = false;
|
||||
const now = new Date().toISOString();
|
||||
setSaveState({ status: 'saved', lastSaved: now });
|
||||
@@ -618,6 +612,8 @@ function App() {
|
||||
<AdminPage
|
||||
data={data}
|
||||
history={data.history}
|
||||
tweaks={tweaks}
|
||||
setTweak={setTweak}
|
||||
onClose={() => setView('backlog')}
|
||||
onForceSave={() => triggerSave('Force-saved')}
|
||||
onForceBackup={() => triggerSave('Force-backed up')}
|
||||
|
||||
Reference in New Issue
Block a user