feat: dark mode, mark-as-read on scroll, fix card tap targets
All checks were successful
Security Checks / dependency-audit (push) Successful in 13s
Security Checks / secret-scanning (push) Successful in 4s
Security Checks / dockerfile-lint (push) Successful in 4s

Dark mode:
- All colors in Color+Extensions now adaptive (UIColor with traits)
- Warm dark palette: dark brown canvas, brighter gold accent, warm cards
- Article HTML CSS supports prefers-color-scheme: dark
- Meal/macro colors unchanged (vivid on both themes)

Reader fixes:
- .contentShape(Rectangle()) on cards/rows — fixes tap target issues
  where small cards couldn't be clicked
- Context menu moved from card/row to the NavigationLink wrapper
  so it doesn't interfere with taps
- Mark as read on scroll via .onAppear on each entry
- Cards no longer pass vm (cleaner, context menu handled at list level)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yusuf Suleman
2026-04-03 20:15:26 -05:00
parent aad9b92342
commit 917a2c4621
3 changed files with 68 additions and 27 deletions

View File

@@ -213,6 +213,7 @@ struct ArticleView: View {
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
<meta name="color-scheme" content="light dark">
<style>
* { box-sizing: border-box; }
body {
@@ -225,6 +226,13 @@ struct ArticleView: View {
background: transparent;
-webkit-text-size-adjust: 100%;
}
@media (prefers-color-scheme: dark) {
body { color: #ede8e1; }
a { color: #c79e40 !important; }
pre, code { background: #1e1c1a !important; }
blockquote { border-left-color: #c79e40; color: #9a9590; }
td, th { border-color: #333; }
}
img {
max-width: 100%;
height: auto;

View File

@@ -38,9 +38,19 @@ struct EntryListView: View {
LazyVStack(spacing: 12) {
ForEach(vm.entries) { entry in
NavigationLink(value: entry) {
EntryCardView(entry: entry, vm: vm)
EntryCardView(entry: entry)
}
.buttonStyle(.plain)
.contentShape(Rectangle())
.onAppear {
// Mark as read when scrolled into view (card mode)
if !entry.isRead {
Task { await vm.markAsRead(entry) }
}
}
.contextMenu {
entryContextMenu(entry: entry, vm: vm)
}
}
loadMoreTrigger
@@ -57,9 +67,18 @@ struct EntryListView: View {
LazyVStack(spacing: 0) {
ForEach(vm.entries) { entry in
NavigationLink(value: entry) {
EntryRowView(entry: entry, vm: vm)
EntryRowView(entry: entry)
}
.buttonStyle(.plain)
.contentShape(Rectangle())
.onAppear {
if !entry.isRead {
Task { await vm.markAsRead(entry) }
}
}
.contextMenu {
entryContextMenu(entry: entry, vm: vm)
}
Divider()
.padding(.leading, 36)
@@ -91,11 +110,10 @@ struct EntryListView: View {
struct EntryCardView: View {
let entry: ReaderEntry
let vm: ReaderViewModel
var body: some View {
VStack(alignment: .leading, spacing: 0) {
// Thumbnail only show if available
// Thumbnail
if let thumbURL = entry.thumbnailURL {
AsyncImage(url: thumbURL) { phase in
switch phase {
@@ -113,7 +131,6 @@ struct EntryCardView: View {
// Content
VStack(alignment: .leading, spacing: 8) {
// Feed + time
HStack(spacing: 6) {
if !entry.isRead {
Circle()
@@ -135,13 +152,11 @@ struct EntryCardView: View {
}
}
// Title
Text(entry.displayTitle)
.font(.subheadline.weight(entry.isRead ? .medium : .bold))
.foregroundStyle(entry.isRead ? Color.textSecondary : Color.textPrimary)
.lineLimit(3)
// Bottom row
HStack(spacing: 8) {
if let author = entry.author, !author.isEmpty {
Text(author)
@@ -168,18 +183,13 @@ struct EntryCardView: View {
.background(Color.surfaceCard)
.clipShape(RoundedRectangle(cornerRadius: 14))
.shadow(color: .black.opacity(0.04), radius: 6, y: 2)
.contextMenu {
entryContextMenu(entry: entry, vm: vm)
}
}
}
// MARK: - List Row View
struct EntryRowView: View {
let entry: ReaderEntry
let vm: ReaderViewModel
var body: some View {
HStack(alignment: .top, spacing: 12) {
@@ -227,7 +237,6 @@ struct EntryRowView: View {
Spacer()
// Thumbnail mini
if let thumbURL = entry.thumbnailURL {
AsyncImage(url: thumbURL) { phase in
switch phase {
@@ -250,10 +259,6 @@ struct EntryRowView: View {
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
.background(Color.canvas)
.contextMenu {
entryContextMenu(entry: entry, vm: vm)
}
}
}

View File

@@ -1,21 +1,49 @@
import SwiftUI
extension Color {
// MARK: - Canvas / Background
static let canvas = Color(red: 0.96, green: 0.94, blue: 0.90) // #F5EFE6
// MARK: - Canvas / Background (adaptive light/dark)
static let canvas = Color(UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.10, green: 0.09, blue: 0.08, alpha: 1) // warm dark
: UIColor(red: 0.96, green: 0.94, blue: 0.90, alpha: 1) // #F5EFE6
})
// MARK: - Accent
static let accentWarm = Color(red: 0.545, green: 0.412, blue: 0.078) // #8B6914
static let accentWarm = Color(UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.78, green: 0.62, blue: 0.25, alpha: 1) // brighter gold for dark
: UIColor(red: 0.545, green: 0.412, blue: 0.078, alpha: 1) // #8B6914
})
static let emerald = Color(red: 0.020, green: 0.588, blue: 0.412) // #059669
// MARK: - Surfaces
static let surfaceCard = Color(red: 255/255, green: 252/255, blue: 248/255) // warm white
static let surfaceSheet = Color(red: 0.98, green: 0.97, blue: 0.95)
// MARK: - Surfaces (adaptive)
static let surfaceCard = Color(UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.15, green: 0.14, blue: 0.13, alpha: 1) // warm dark card
: UIColor(red: 1.0, green: 0.988, blue: 0.973, alpha: 1) // warm white
})
static let surfaceSheet = Color(UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.13, green: 0.12, blue: 0.11, alpha: 1)
: UIColor(red: 0.98, green: 0.97, blue: 0.95, alpha: 1)
})
// MARK: - Text
static let textPrimary = Color(red: 0.12, green: 0.12, blue: 0.12)
static let textSecondary = Color(red: 0.45, green: 0.45, blue: 0.45)
static let textTertiary = Color(red: 0.65, green: 0.65, blue: 0.65)
// MARK: - Text (adaptive)
static let textPrimary = Color(UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.93, green: 0.91, blue: 0.88, alpha: 1)
: UIColor(red: 0.12, green: 0.12, blue: 0.12, alpha: 1)
})
static let textSecondary = Color(UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.62, green: 0.60, blue: 0.57, alpha: 1)
: UIColor(red: 0.45, green: 0.45, blue: 0.45, alpha: 1)
})
static let textTertiary = Color(UIColor { traits in
traits.userInterfaceStyle == .dark
? UIColor(red: 0.45, green: 0.43, blue: 0.40, alpha: 1)
: UIColor(red: 0.65, green: 0.65, blue: 0.65, alpha: 1)
})
// MARK: - Meal Colors
static let mealBreakfast = Color(red: 1.0, green: 0.72, blue: 0.27) // warm orange