feat: dark mode, mark-as-read on scroll, fix card tap targets
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:
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user