feat(BookmarkRow): adopt Dynamic Type, move cache stat off render path, safer delete
Apply WWDC26 design-brief fixes to the bookmark list reading surface: - Dynamic Type: replace fixed .system(size:) points with text styles (title .headline, domain/date .footnote, summary .subheadline, tags .caption, podcast glyph .title3). Rows now scale and wrap with the user's text-size setting instead of staying fixed. - Perf: podcastCached was a FileManager.fileExists stat evaluated inside body — i.e. per row, per scroll frame. Move it to @State populated by a .task(id:) that stats off the main actor, once per appearance. - Forgiveness: destructive trailing swipe is now allowsFullSwipe: false so a single fling can't permanently delete; requires tapping the button. Adds docs/design-wwdc26.md (consolidated brief + codebase audit). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,7 +23,9 @@ struct BookmarkListRow: View {
|
||||
.onTapGesture { onOpen() }
|
||||
.listRowInsets(EdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 16))
|
||||
.listRowSeparator(.visible)
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
// Delete is destructive and not undoable — require an explicit tap on the
|
||||
// revealed button rather than letting a single full swipe delete instantly.
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
|
||||
Button(role: .destructive) {
|
||||
Task { await viewModel.delete(bookmark) }
|
||||
} label: {
|
||||
|
||||
@@ -6,6 +6,7 @@ struct BookmarkRow: View {
|
||||
var onPodcast: (() -> Void)? = nil
|
||||
|
||||
@State private var podcastTapCount = 0
|
||||
@State private var podcastCached = false
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 6) {
|
||||
@@ -23,7 +24,7 @@ struct BookmarkRow: View {
|
||||
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(bookmark.displayTitle)
|
||||
.font(.system(size: 17, weight: .semibold))
|
||||
.font(.headline)
|
||||
.foregroundStyle(.primary)
|
||||
.lineLimit(2)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
@@ -33,7 +34,7 @@ struct BookmarkRow: View {
|
||||
Text("·")
|
||||
Text(bookmark.dateAdded, style: .relative)
|
||||
}
|
||||
.font(.system(size: 13))
|
||||
.font(.footnote)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
@@ -45,7 +46,7 @@ struct BookmarkRow: View {
|
||||
onPodcast()
|
||||
} label: {
|
||||
Image(systemName: podcastCached ? "headphones.circle.fill" : "headphones.circle")
|
||||
.font(.system(size: 20))
|
||||
.font(.title3)
|
||||
.foregroundStyle(podcastCached ? .blue : Color(.systemGray3))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -56,7 +57,7 @@ struct BookmarkRow: View {
|
||||
|
||||
if let summary = bookmark.aiSummary {
|
||||
Text(summary)
|
||||
.font(.system(size: 14))
|
||||
.font(.subheadline)
|
||||
.italic()
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(2)
|
||||
@@ -68,7 +69,7 @@ struct BookmarkRow: View {
|
||||
HStack(spacing: 6) {
|
||||
ForEach(effectiveTags, id: \.self) { tag in
|
||||
Text(tag)
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.font(.caption.weight(.medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 3)
|
||||
@@ -97,10 +98,12 @@ struct BookmarkRow: View {
|
||||
.frame(height: 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var podcastCached: Bool {
|
||||
FileManager.default.fileExists(atPath: ClaudeService.cachedPodcastURL(for: bookmark.url).path)
|
||||
.task(id: bookmark.url) {
|
||||
// Stat the podcast cache off the render path: once per appearance
|
||||
// (and when the URL changes), not on every `body` recomputation.
|
||||
let path = ClaudeService.cachedPodcastURL(for: bookmark.url).path
|
||||
podcastCached = await Task.detached { FileManager.default.fileExists(atPath: path) }.value
|
||||
}
|
||||
}
|
||||
|
||||
private var effectiveTags: [String] {
|
||||
|
||||
Reference in New Issue
Block a user