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>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||||
|
<meta name="color-scheme" content="light dark">
|
||||||
<style>
|
<style>
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body {
|
body {
|
||||||
@@ -225,6 +226,13 @@ struct ArticleView: View {
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
-webkit-text-size-adjust: 100%;
|
-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 {
|
img {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
|
|||||||
@@ -38,9 +38,19 @@ struct EntryListView: View {
|
|||||||
LazyVStack(spacing: 12) {
|
LazyVStack(spacing: 12) {
|
||||||
ForEach(vm.entries) { entry in
|
ForEach(vm.entries) { entry in
|
||||||
NavigationLink(value: entry) {
|
NavigationLink(value: entry) {
|
||||||
EntryCardView(entry: entry, vm: vm)
|
EntryCardView(entry: entry)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.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
|
loadMoreTrigger
|
||||||
@@ -57,9 +67,18 @@ struct EntryListView: View {
|
|||||||
LazyVStack(spacing: 0) {
|
LazyVStack(spacing: 0) {
|
||||||
ForEach(vm.entries) { entry in
|
ForEach(vm.entries) { entry in
|
||||||
NavigationLink(value: entry) {
|
NavigationLink(value: entry) {
|
||||||
EntryRowView(entry: entry, vm: vm)
|
EntryRowView(entry: entry)
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onAppear {
|
||||||
|
if !entry.isRead {
|
||||||
|
Task { await vm.markAsRead(entry) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
entryContextMenu(entry: entry, vm: vm)
|
||||||
|
}
|
||||||
|
|
||||||
Divider()
|
Divider()
|
||||||
.padding(.leading, 36)
|
.padding(.leading, 36)
|
||||||
@@ -91,11 +110,10 @@ struct EntryListView: View {
|
|||||||
|
|
||||||
struct EntryCardView: View {
|
struct EntryCardView: View {
|
||||||
let entry: ReaderEntry
|
let entry: ReaderEntry
|
||||||
let vm: ReaderViewModel
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
// Thumbnail — only show if available
|
// Thumbnail
|
||||||
if let thumbURL = entry.thumbnailURL {
|
if let thumbURL = entry.thumbnailURL {
|
||||||
AsyncImage(url: thumbURL) { phase in
|
AsyncImage(url: thumbURL) { phase in
|
||||||
switch phase {
|
switch phase {
|
||||||
@@ -113,7 +131,6 @@ struct EntryCardView: View {
|
|||||||
|
|
||||||
// Content
|
// Content
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
// Feed + time
|
|
||||||
HStack(spacing: 6) {
|
HStack(spacing: 6) {
|
||||||
if !entry.isRead {
|
if !entry.isRead {
|
||||||
Circle()
|
Circle()
|
||||||
@@ -135,13 +152,11 @@ struct EntryCardView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Title
|
|
||||||
Text(entry.displayTitle)
|
Text(entry.displayTitle)
|
||||||
.font(.subheadline.weight(entry.isRead ? .medium : .bold))
|
.font(.subheadline.weight(entry.isRead ? .medium : .bold))
|
||||||
.foregroundStyle(entry.isRead ? Color.textSecondary : Color.textPrimary)
|
.foregroundStyle(entry.isRead ? Color.textSecondary : Color.textPrimary)
|
||||||
.lineLimit(3)
|
.lineLimit(3)
|
||||||
|
|
||||||
// Bottom row
|
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
if let author = entry.author, !author.isEmpty {
|
if let author = entry.author, !author.isEmpty {
|
||||||
Text(author)
|
Text(author)
|
||||||
@@ -168,18 +183,13 @@ struct EntryCardView: View {
|
|||||||
.background(Color.surfaceCard)
|
.background(Color.surfaceCard)
|
||||||
.clipShape(RoundedRectangle(cornerRadius: 14))
|
.clipShape(RoundedRectangle(cornerRadius: 14))
|
||||||
.shadow(color: .black.opacity(0.04), radius: 6, y: 2)
|
.shadow(color: .black.opacity(0.04), radius: 6, y: 2)
|
||||||
.contextMenu {
|
|
||||||
entryContextMenu(entry: entry, vm: vm)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - List Row View
|
// MARK: - List Row View
|
||||||
|
|
||||||
struct EntryRowView: View {
|
struct EntryRowView: View {
|
||||||
let entry: ReaderEntry
|
let entry: ReaderEntry
|
||||||
let vm: ReaderViewModel
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .top, spacing: 12) {
|
HStack(alignment: .top, spacing: 12) {
|
||||||
@@ -227,7 +237,6 @@ struct EntryRowView: View {
|
|||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
// Thumbnail mini
|
|
||||||
if let thumbURL = entry.thumbnailURL {
|
if let thumbURL = entry.thumbnailURL {
|
||||||
AsyncImage(url: thumbURL) { phase in
|
AsyncImage(url: thumbURL) { phase in
|
||||||
switch phase {
|
switch phase {
|
||||||
@@ -250,10 +259,6 @@ struct EntryRowView: View {
|
|||||||
}
|
}
|
||||||
.padding(.horizontal, 16)
|
.padding(.horizontal, 16)
|
||||||
.padding(.vertical, 12)
|
.padding(.vertical, 12)
|
||||||
.background(Color.canvas)
|
|
||||||
.contextMenu {
|
|
||||||
entryContextMenu(entry: entry, vm: vm)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +1,49 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
extension Color {
|
extension Color {
|
||||||
// MARK: - Canvas / Background
|
// MARK: - Canvas / Background (adaptive light/dark)
|
||||||
static let canvas = Color(red: 0.96, green: 0.94, blue: 0.90) // #F5EFE6
|
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
|
// 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
|
static let emerald = Color(red: 0.020, green: 0.588, blue: 0.412) // #059669
|
||||||
|
|
||||||
// MARK: - Surfaces
|
// MARK: - Surfaces (adaptive)
|
||||||
static let surfaceCard = Color(red: 255/255, green: 252/255, blue: 248/255) // warm white
|
static let surfaceCard = Color(UIColor { traits in
|
||||||
static let surfaceSheet = Color(red: 0.98, green: 0.97, blue: 0.95)
|
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
|
// MARK: - Text (adaptive)
|
||||||
static let textPrimary = Color(red: 0.12, green: 0.12, blue: 0.12)
|
static let textPrimary = Color(UIColor { traits in
|
||||||
static let textSecondary = Color(red: 0.45, green: 0.45, blue: 0.45)
|
traits.userInterfaceStyle == .dark
|
||||||
static let textTertiary = Color(red: 0.65, green: 0.65, blue: 0.65)
|
? 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
|
// MARK: - Meal Colors
|
||||||
static let mealBreakfast = Color(red: 1.0, green: 0.72, blue: 0.27) // warm orange
|
static let mealBreakfast = Color(red: 1.0, green: 0.72, blue: 0.27) // warm orange
|
||||||
|
|||||||
Reference in New Issue
Block a user