fix: improve text contrast throughout app

Replace near-invisible grays (#2a2a2a, #333, #3a3a3a, #444, #555) with
readable values (#555–#777) on the dark background.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
nmemmertandClaude Sonnet 4.6 committed 2026-08-18 10:30:03 -04:00
1 parent e14919fe17
commit dd6fa803eb
8 files changed
+27 -27

No files matched your search

+4 -4
View File
@@ -120,7 +120,7 @@ export default function App() {
<Tab active={view === 'library'} onClick={() => setView('library')}> <Tab active={view === 'library'} onClick={() => setView('library')}>
Library Library
{total !== null && ( {total !== null && (
<span style={{ marginLeft: 6, fontSize: 12, color: view === 'library' ? '#555' : '#3a3a3a' }}> <span style={{ marginLeft: 6, fontSize: 12, color: view === 'library' ? '#777' : '#666' }}>
{total.toLocaleString()} {total.toLocaleString()}
</span> </span>
)} )}
@@ -192,7 +192,7 @@ export default function App() {
</p> </p>
)} )}
{photos.length === 0 && !loading && !error && ( {photos.length === 0 && !loading && !error && (
<div style={{ textAlign: 'center', padding: '80px 24px', color: '#555' }}> <div style={{ textAlign: 'center', padding: '80px 24px', color: '#777' }}>
{hasFilter ? ( {hasFilter ? (
<p style={{ fontSize: 15 }}>No photos match these filters.</p> <p style={{ fontSize: 15 }}>No photos match these filters.</p>
) : ( ) : (
@@ -207,7 +207,7 @@ export default function App() {
<div ref={sentinelRef} style={{ height: 1 }} /> <div ref={sentinelRef} style={{ height: 1 }} />
{loading && <p style={{ textAlign: 'center', padding: 24, color: '#444', fontSize: 13 }}>Loading…</p>} {loading && <p style={{ textAlign: 'center', padding: 24, color: '#444', fontSize: 13 }}>Loading…</p>}
{nextCursor === null && photos.length > 0 && !loading && ( {nextCursor === null && photos.length > 0 && !loading && (
<p style={{ textAlign: 'center', padding: 24, color: '#2a2a2a', fontSize: 12 }}> <p style={{ textAlign: 'center', padding: 24, color: '#555', fontSize: 12 }}>
— {total?.toLocaleString()} photo{total === 1 ? '' : 's'} — — {total?.toLocaleString()} photo{total === 1 ? '' : 's'} —
</p> </p>
)} )}
@@ -216,7 +216,7 @@ export default function App() {
{/* Map */} {/* Map */}
{view === 'map' && ( {view === 'map' && (
<Suspense fallback={<p style={{ padding: 24, color: '#555', fontSize: 13 }}>Loading map…</p>}> <Suspense fallback={<p style={{ padding: 24, color: '#777', fontSize: 13 }}>Loading map…</p>}>
<MapView onOpenPhoto={(photo) => openLightbox([photo], 0)} /> <MapView onOpenPhoto={(photo) => openLightbox([photo], 0)} />
</Suspense> </Suspense>
)} )}
+2 -2
View File
@@ -77,7 +77,7 @@ export default function DetailPanel({ photoId }) {
function Section({ label, children }) { function Section({ label, children }) {
return ( return (
<div style={{ marginBottom: 24 }}> <div style={{ marginBottom: 24 }}>
<div style={{ fontSize: 10, fontWeight: 700, color: '#3a3a3a', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 10 }}> <div style={{ fontSize: 10, fontWeight: 700, color: '#666', textTransform: 'uppercase', letterSpacing: 1, marginBottom: 10 }}>
{label} {label}
</div> </div>
{children} {children}
@@ -88,7 +88,7 @@ function Section({ label, children }) {
function Row({ k, v, mono }) { function Row({ k, v, mono }) {
return ( return (
<div style={{ display: 'flex', gap: 8, marginBottom: 5 }}> <div style={{ display: 'flex', gap: 8, marginBottom: 5 }}>
<span style={{ color: '#555', flexShrink: 0, width: 60, fontSize: 12 }}>{k}</span> <span style={{ color: '#777', flexShrink: 0, width: 60, fontSize: 12 }}>{k}</span>
<span style={{ <span style={{
color: '#aaa', color: '#aaa',
fontFamily: mono ? "'SF Mono', 'Fira Code', monospace" : undefined, fontFamily: mono ? "'SF Mono', 'Fira Code', monospace" : undefined,
+3 -3
View File
@@ -56,13 +56,13 @@ export default function DuplicateGroup({ photos, onDismiss, onOpen }) {
</div> </div>
{/* Meta */} {/* Meta */}
<div style={{ padding: '7px 10px 8px', fontSize: 12, color: '#555', display: 'flex', flexDirection: 'column', gap: 2 }}> <div style={{ padding: '7px 10px 8px', fontSize: 12, color: '#777', display: 'flex', flexDirection: 'column', gap: 2 }}>
<span style={{ color: '#888' }}>{fmtDate(photo.date_taken)}</span> <span style={{ color: '#888' }}>{fmtDate(photo.date_taken)}</span>
<span> <span>
{[fmtSize(photo.filesize), photo.width && `${photo.width}×${photo.height}`].filter(Boolean).join(' · ')} {[fmtSize(photo.filesize), photo.width && `${photo.width}×${photo.height}`].filter(Boolean).join(' · ')}
</span> </span>
{photo.camera_model && ( {photo.camera_model && (
<span style={{ color: '#3a3a3a', fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}> <span style={{ color: '#666', fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
{photo.camera_model} {photo.camera_model}
</span> </span>
)} )}
@@ -77,7 +77,7 @@ export default function DuplicateGroup({ photos, onDismiss, onOpen }) {
borderTop: '1px solid #1a1a1a', borderTop: '1px solid #1a1a1a',
display: 'flex', alignItems: 'center', gap: 12, display: 'flex', alignItems: 'center', gap: 12,
}}> }}>
<span style={{ fontSize: 12, color: '#333', flex: 1 }}> <span style={{ fontSize: 12, color: '#666', flex: 1 }}>
{photos.length} similar photos {photos.length} similar photos
</span> </span>
<button <button
+4 -4
View File
@@ -66,7 +66,7 @@ export default function DuplicatesView({ onOpenPhoto, onGroupCountChange }) {
</button> </button>
{scanResult && ( {scanResult && (
<span style={{ fontSize: 12, color: '#555' }}> <span style={{ fontSize: 12, color: '#777' }}>
{scanResult.newPairs === 0 {scanResult.newPairs === 0
? 'No new pairs' ? 'No new pairs'
: `${scanResult.newPairs} new pair${scanResult.newPairs !== 1 ? 's' : ''} found`} : `${scanResult.newPairs} new pair${scanResult.newPairs !== 1 ? 's' : ''} found`}
@@ -80,11 +80,11 @@ export default function DuplicatesView({ onOpenPhoto, onGroupCountChange }) {
)} )}
{groups === null && ( {groups === null && (
<p style={{ color: '#555', fontSize: 13 }}>Loading…</p> <p style={{ color: '#777', fontSize: 13 }}>Loading…</p>
)} )}
{groups !== null && groups.length === 0 && ( {groups !== null && groups.length === 0 && (
<div style={{ textAlign: 'center', paddingTop: 60, color: '#555' }}> <div style={{ textAlign: 'center', paddingTop: 60, color: '#777' }}>
<p style={{ fontSize: 15, marginBottom: 8 }}>No similar photos to review.</p> <p style={{ fontSize: 15, marginBottom: 8 }}>No similar photos to review.</p>
<p style={{ fontSize: 13 }}>Click "Scan library" to compare perceptual hashes across your library.</p> <p style={{ fontSize: 13 }}>Click "Scan library" to compare perceptual hashes across your library.</p>
</div> </div>
@@ -92,7 +92,7 @@ export default function DuplicatesView({ onOpenPhoto, onGroupCountChange }) {
{groups !== null && groups.length > 0 && ( {groups !== null && groups.length > 0 && (
<div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}> <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
<p style={{ fontSize: 12, color: '#444', marginBottom: 4 }}> <p style={{ fontSize: 12, color: '#666', marginBottom: 4 }}>
{groups.length} group{groups.length !== 1 ? 's' : ''} — click a photo to inspect it, "Not duplicates" to dismiss {groups.length} group{groups.length !== 1 ? 's' : ''} — click a photo to inspect it, "Not duplicates" to dismiss
</p> </p>
{groups.map((group, i) => ( {groups.map((group, i) => (
+1 -1
View File
@@ -156,7 +156,7 @@ function Section({ title, children }) {
<div style={{ marginTop: 16 }}> <div style={{ marginTop: 16 }}>
<div style={{ <div style={{
fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', fontSize: 10, fontWeight: 700, letterSpacing: '0.08em',
color: '#3a3a3a', textTransform: 'uppercase', color: '#666', textTransform: 'uppercase',
padding: '0 16px 4px', padding: '0 16px 4px',
}}> }}>
{title} {title}
+2 -2
View File
@@ -47,11 +47,11 @@ export default function MapView({ onOpenPhoto }) {
); );
if (points === null) return ( if (points === null) return (
<p style={{ color: '#555', padding: 24, fontSize: 13 }}>Loading map…</p> <p style={{ color: '#777', padding: 24, fontSize: 13 }}>Loading map…</p>
); );
if (points.length === 0) return ( if (points.length === 0) return (
<div style={{ textAlign: 'center', paddingTop: 60, color: '#555' }}> <div style={{ textAlign: 'center', paddingTop: 60, color: '#777' }}>
<p style={{ fontSize: 15, marginBottom: 8 }}>No geotagged photos.</p> <p style={{ fontSize: 15, marginBottom: 8 }}>No geotagged photos.</p>
<p style={{ fontSize: 13 }}>Photos with GPS metadata will appear here once indexed.</p> <p style={{ fontSize: 13 }}>Photos with GPS metadata will appear here once indexed.</p>
</div> </div>
+1 -1
View File
@@ -13,7 +13,7 @@ export default function MonthGroup({ label, photos, startIdx, onSelect }) {
userSelect: 'none', userSelect: 'none',
}}> }}>
{label} {label}
<span style={{ marginLeft: 8, color: '#3a3a3a', fontWeight: 400 }}> <span style={{ marginLeft: 8, color: '#666', fontWeight: 400 }}>
{photos.length.toLocaleString()} {photos.length.toLocaleString()}
</span> </span>
</h2> </h2>
+10 -10
View File
@@ -190,7 +190,7 @@ export default function SettingsPanel({ onClose }) {
onClick={() => handleRemoveFolder(f.id)} onClick={() => handleRemoveFolder(f.id)}
style={{ style={{
background: 'none', border: 'none', background: 'none', border: 'none',
color: '#3a3a3a', cursor: 'pointer', fontSize: 14, color: '#666', cursor: 'pointer', fontSize: 14,
flexShrink: 0, lineHeight: 1, flexShrink: 0, lineHeight: 1,
}} }}
>×</button> >×</button>
@@ -333,7 +333,7 @@ export default function SettingsPanel({ onClose }) {
<NumSetting label="Preview quality" unit="%" min={1} max={100} <NumSetting label="Preview quality" unit="%" min={1} max={100}
value={settings.preview_quality} value={settings.preview_quality}
onChange={v => handleSetting('preview_quality', v)} /> onChange={v => handleSetting('preview_quality', v)} />
<p style={{ fontSize: 11, color: '#3a3a3a', padding: '4px 18px 0', lineHeight: 1.5 }}> <p style={{ fontSize: 11, color: '#666', padding: '4px 18px 0', lineHeight: 1.5 }}>
Changes apply to newly ingested photos. Existing thumbnails are not regenerated. Changes apply to newly ingested photos. Existing thumbnails are not regenerated.
</p> </p>
</Section> </Section>
@@ -343,7 +343,7 @@ export default function SettingsPanel({ onClose }) {
<NumSetting label="Similarity threshold" unit="" min={1} max={20} <NumSetting label="Similarity threshold" unit="" min={1} max={20}
value={settings.dedupe_threshold} value={settings.dedupe_threshold}
onChange={v => handleSetting('dedupe_threshold', v)} /> onChange={v => handleSetting('dedupe_threshold', v)} />
<p style={{ fontSize: 11, color: '#3a3a3a', padding: '4px 18px 0', lineHeight: 1.5 }}> <p style={{ fontSize: 11, color: '#666', padding: '4px 18px 0', lineHeight: 1.5 }}>
Hamming distance — lower = stricter (fewer, more exact matches). Hamming distance — lower = stricter (fewer, more exact matches).
</p> </p>
</Section> </Section>
@@ -390,7 +390,7 @@ export default function SettingsPanel({ onClose }) {
error={syncError.icloud} error={syncError.icloud}
onSync={() => handleSync('icloud')} onSync={() => handleSync('icloud')}
/> />
<p style={{ fontSize: 11, color: '#3a3a3a', padding: '2px 18px 4px', lineHeight: 1.5 }}> <p style={{ fontSize: 11, color: '#666', padding: '2px 18px 4px', lineHeight: 1.5 }}>
First run: <code style={{ color: '#555' }}>docker exec &lt;container&gt; icloudpd --username &lt;id&gt; --auth-only --cookie-directory /data/icloud-cookies</code> First run: <code style={{ color: '#555' }}>docker exec &lt;container&gt; icloudpd --username &lt;id&gt; --auth-only --cookie-directory /data/icloud-cookies</code>
</p> </p>
</> </>
@@ -398,7 +398,7 @@ export default function SettingsPanel({ onClose }) {
{/* Amazon Photos */} {/* Amazon Photos */}
<SyncSourceHeader label="Amazon Photos — one-time import" /> <SyncSourceHeader label="Amazon Photos — one-time import" />
<p style={{ fontSize: 11, color: '#3a3a3a', padding: '0 18px 8px', lineHeight: 1.6 }}> <p style={{ fontSize: 11, color: '#666', padding: '0 18px 8px', lineHeight: 1.6 }}>
Request your library at amazon.com → Account → Request Your Data → Amazon Photos. Request your library at amazon.com → Account → Request Your Data → Amazon Photos.
Extract the ZIP, copy to your server, then add the folder path here. Extract the ZIP, copy to your server, then add the folder path here.
</p> </p>
@@ -430,7 +430,7 @@ export default function SettingsPanel({ onClose }) {
</button> </button>
</div> </div>
<p style={{ fontSize: 11, color: '#3a3a3a', padding: '4px 18px 0', lineHeight: 1.5 }}> <p style={{ fontSize: 11, color: '#666', padding: '4px 18px 0', lineHeight: 1.5 }}>
Also add the iCloud download folder to Watch Folders once sync runs. Also add the iCloud download folder to Watch Folders once sync runs.
</p> </p>
</Section> </Section>
@@ -441,7 +441,7 @@ export default function SettingsPanel({ onClose }) {
<DangerBtn onClick={handleClearDismissals}> <DangerBtn onClick={handleClearDismissals}>
{cleared ? '✓ Done' : 'Clear dismissed duplicates'} {cleared ? '✓ Done' : 'Clear dismissed duplicates'}
</DangerBtn> </DangerBtn>
<p style={{ fontSize: 11, color: '#3a3a3a', marginTop: 5, lineHeight: 1.5 }}> <p style={{ fontSize: 11, color: '#666', marginTop: 5, lineHeight: 1.5 }}>
Resets all "Not duplicates" decisions so groups reappear in the review list. Resets all "Not duplicates" decisions so groups reappear in the review list.
</p> </p>
</div> </div>
@@ -459,7 +459,7 @@ function Section({ title, children }) {
<div style={{ marginTop: 20 }}> <div style={{ marginTop: 20 }}>
<div style={{ <div style={{
fontSize: 10, fontWeight: 700, letterSpacing: '0.08em', fontSize: 10, fontWeight: 700, letterSpacing: '0.08em',
color: '#3a3a3a', textTransform: 'uppercase', color: '#666', textTransform: 'uppercase',
padding: '0 18px 8px', padding: '0 18px 8px',
}}> }}>
{title} {title}
@@ -610,7 +610,7 @@ function SyncControls({ job, error, onSync }) {
)} )}
{lastRunLabel && !job.running && ( {lastRunLabel && !job.running && (
<span style={{ fontSize: 11, color: '#3a3a3a' }}> <span style={{ fontSize: 11, color: '#666' }}>
Last: {lastRunLabel} Last: {lastRunLabel}
{job.exitCode !== null && ( {job.exitCode !== null && (
<span style={{ color: job.exitCode === 0 ? '#4a7a4a' : '#7a3a3a', marginLeft: 6 }}> <span style={{ color: job.exitCode === 0 ? '#4a7a4a' : '#7a3a3a', marginLeft: 6 }}>
@@ -624,7 +624,7 @@ function SyncControls({ job, error, onSync }) {
<button <button
onClick={() => setShowLog(s => !s)} onClick={() => setShowLog(s => !s)}
style={{ style={{
background: 'none', border: 'none', color: '#3a3a3a', background: 'none', border: 'none', color: '#666',
cursor: 'pointer', fontSize: 11, marginLeft: 'auto', padding: 0, cursor: 'pointer', fontSize: 11, marginLeft: 'auto', padding: 0,
}} }}
> >