Carry the library design across the rest of the app
Every screen now speaks the paper vocabulary rather than only the bookmarks screen: Search, Sources, Podcasts and the player, Settings, Add/Edit, Smart Collections, Ask, Onboarding, the browser toolbar, the Siri snippets, and the share extension's save card. The change is mostly a design system rather than per-screen tweaks. LibraryKit gains: - Semantic colors — secondary/tertiary/faint text, `raised` surfaces, and accent/alarm/affirm, so screens stop reaching for .secondary, systemGray, .blue, .red and .green. The three status colors come out of the palette (the swatch blue, vermilion and forest) rather than from the system set, so the design keeps spending one set of inks. - PaperType — the two voices made explicit. Prose (titles, summaries, anything a person wrote) is serif; anything the machine contributes (domains, dates, counts, tags, labels, buttons) is monospaced. Keeping that split strict is what makes the design read as archival rather than as decoration. - paperSurface() / paperField() / paperCard() for grounds, inputs and raised blocks. - PaperEmptyState, because ContentUnavailableView can't be restyled — it draws its own bold system type and grey, which was the loudest remaining system voice once the screens moved onto the sheet. All nine usages are converted. The app also sets one accent for the system chrome it doesn't draw — tab bar, search fields, switches, swipe actions — which otherwise stayed system blue around paper screens. BookmarkRow is deleted. Once Search moved to the library row and TagBookmarksView was gone, nothing passed .classic, so the style fork in BookmarkListRow went with it. There is one bookmark row now. The share extension compiles LibraryKit directly, since its save card is a user-facing surface and should not be the one place still using system styling. Verified on device in both color schemes; screens toured with a temporary UI test harness (removed). Suite passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JgLHztZGaEHvS3KNeGQmRM
This commit is contained in:
co-authored by
Claude Opus 5
parent
8a777ffc83
commit
fffb3999bf
@@ -510,6 +510,7 @@ struct PodcastPlayerView: View {
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 32)
|
||||
.paperSurface()
|
||||
.navigationTitle("Podcast")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
@@ -520,7 +521,7 @@ struct PodcastPlayerView: View {
|
||||
ToolbarItem(placement: .topBarTrailing) {
|
||||
Button { vm.stop(); dismiss() } label: {
|
||||
Image(systemName: "stop.circle")
|
||||
.foregroundStyle(.red)
|
||||
.foregroundStyle(Paper.alarm)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -556,16 +557,16 @@ struct PodcastPlayerView: View {
|
||||
VStack(spacing: 28) {
|
||||
Image(systemName: "waveform")
|
||||
.font(.system(size: 52))
|
||||
.foregroundStyle(.secondary)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
.symbolEffect(.variableColor.iterative, isActive: true)
|
||||
|
||||
VStack(spacing: 10) {
|
||||
ProgressView(value: progress)
|
||||
.progressViewStyle(.linear)
|
||||
.tint(.blue)
|
||||
.tint(Paper.accent)
|
||||
Text(label)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(PaperType.meta)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -573,16 +574,16 @@ struct PodcastPlayerView: View {
|
||||
private func playerView(title: String) -> some View {
|
||||
VStack(spacing: 28) {
|
||||
RoundedRectangle(cornerRadius: 20)
|
||||
.fill(Color(.systemGray6))
|
||||
.fill(Paper.raised)
|
||||
.frame(width: 220, height: 220)
|
||||
.overlay {
|
||||
Image(systemName: "waveform.circle.fill")
|
||||
.font(.system(size: 80))
|
||||
.foregroundStyle(.blue)
|
||||
.foregroundStyle(Paper.accent)
|
||||
}
|
||||
|
||||
Text(title)
|
||||
.font(.system(size: 18, weight: .semibold))
|
||||
.font(PaperType.heading)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineLimit(3)
|
||||
|
||||
@@ -591,32 +592,32 @@ struct PodcastPlayerView: View {
|
||||
value: Binding(get: { vm.currentTime }, set: { vm.seek(to: $0) }),
|
||||
in: 0...vm.duration
|
||||
)
|
||||
.tint(.primary)
|
||||
.tint(Paper.ink)
|
||||
|
||||
HStack {
|
||||
Text(formatTime(vm.currentTime))
|
||||
Spacer()
|
||||
Text(formatTime(vm.duration))
|
||||
}
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundStyle(.tertiary)
|
||||
.font(PaperType.micro)
|
||||
.foregroundStyle(Paper.tertiary)
|
||||
}
|
||||
|
||||
HStack(spacing: 36) {
|
||||
Button { vm.skipBackward15() } label: {
|
||||
Image(systemName: "gobackward.15")
|
||||
.font(.system(size: 32))
|
||||
.foregroundStyle(.primary)
|
||||
.foregroundStyle(Paper.ink)
|
||||
}
|
||||
Button { vm.togglePlayPause() } label: {
|
||||
Image(systemName: vm.isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
||||
.font(.system(size: 72))
|
||||
.foregroundStyle(.primary)
|
||||
.foregroundStyle(Paper.ink)
|
||||
}
|
||||
Button { vm.skipForward15() } label: {
|
||||
Image(systemName: "goforward.15")
|
||||
.font(.system(size: 32))
|
||||
.foregroundStyle(.primary)
|
||||
.foregroundStyle(Paper.ink)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -636,8 +637,8 @@ struct PodcastPlayerView: View {
|
||||
}
|
||||
} label: {
|
||||
Text(vm.playbackSpeed == 1.0 ? "1× Speed" : "\(String(format: "%g", vm.playbackSpeed))×")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(PaperType.meta)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 7)
|
||||
.glassEffect(in: Capsule())
|
||||
@@ -652,7 +653,7 @@ struct PodcastPlayerView: View {
|
||||
} label: {
|
||||
Label(sleepLabel, systemImage: sleepActive ? "moon.fill" : "moon")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(sleepActive ? Color.blue : .secondary)
|
||||
.foregroundStyle(sleepActive ? Paper.accent : .secondary)
|
||||
.padding(.horizontal, 14)
|
||||
.padding(.vertical, 7)
|
||||
.glassEffect(in: Capsule())
|
||||
@@ -676,7 +677,7 @@ struct PodcastPlayerView: View {
|
||||
ShareLink(item: url, subject: Text(vm.currentArticleTitle.isEmpty ? "Marks Podcast" : vm.currentArticleTitle)) {
|
||||
Label("Share Episode", systemImage: "square.and.arrow.up")
|
||||
.font(.system(size: 14, weight: .medium))
|
||||
.foregroundStyle(.secondary)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -687,12 +688,12 @@ struct PodcastPlayerView: View {
|
||||
VStack(spacing: 16) {
|
||||
Image(systemName: "exclamationmark.triangle")
|
||||
.font(.system(size: 44))
|
||||
.foregroundStyle(.red)
|
||||
.foregroundStyle(Paper.alarm)
|
||||
Text("Generation Failed")
|
||||
.font(.headline)
|
||||
.font(PaperType.heading)
|
||||
Text(message)
|
||||
.font(.system(size: 14))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(PaperType.meta)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
Button {
|
||||
vm.start(articleUrl: articleUrl, articleTitle: articleTitle, claude: claude)
|
||||
@@ -701,7 +702,7 @@ struct PodcastPlayerView: View {
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
}
|
||||
.buttonStyle(.glassProminent)
|
||||
.tint(.blue)
|
||||
.tint(Paper.accent)
|
||||
.buttonBorderShape(.capsule)
|
||||
.padding(.top, 4)
|
||||
}
|
||||
@@ -724,18 +725,18 @@ struct MiniPlayerView: View {
|
||||
HStack(spacing: 14) {
|
||||
Image(systemName: "waveform")
|
||||
.font(.system(size: 16, weight: .semibold))
|
||||
.foregroundStyle(.blue)
|
||||
.foregroundStyle(Paper.accent)
|
||||
.symbolEffect(.variableColor.iterative, isActive: vm.isPlaying || vm.isGenerating)
|
||||
.frame(width: 22)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2) {
|
||||
Text(vm.currentArticleTitle.isEmpty ? "Podcast" : vm.currentArticleTitle)
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundStyle(.primary)
|
||||
.font(PaperType.meta)
|
||||
.foregroundStyle(Paper.ink)
|
||||
.lineLimit(1)
|
||||
Text(progressLabel)
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(PaperType.micro)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
@@ -749,7 +750,7 @@ struct MiniPlayerView: View {
|
||||
} label: {
|
||||
Image(systemName: vm.isPlaying ? "pause.fill" : "play.fill")
|
||||
.font(.system(size: 18))
|
||||
.foregroundStyle(.primary)
|
||||
.foregroundStyle(Paper.ink)
|
||||
.frame(width: 36, height: 36)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -760,20 +761,20 @@ struct MiniPlayerView: View {
|
||||
} label: {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
.foregroundStyle(.secondary)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
.frame(width: 28, height: 28)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(.regularMaterial)
|
||||
.background(Paper.raised)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16))
|
||||
.overlay(alignment: .bottom) {
|
||||
// Progress bar along bottom edge
|
||||
GeometryReader { geo in
|
||||
Rectangle()
|
||||
.fill(Color.blue.opacity(0.6))
|
||||
.fill(Paper.accent.opacity(0.6))
|
||||
.frame(width: geo.size.width * progressFraction, height: 3)
|
||||
}
|
||||
.frame(height: 3)
|
||||
@@ -819,10 +820,10 @@ struct PodcastLibraryView: View {
|
||||
NavigationStack {
|
||||
Group {
|
||||
if library.entries.isEmpty {
|
||||
ContentUnavailableView(
|
||||
"No Podcasts",
|
||||
PaperEmptyState(
|
||||
title: "No Podcasts",
|
||||
systemImage: "headphones",
|
||||
description: Text("Podcasts you generate from bookmarks will appear here.")
|
||||
message: "Podcasts you generate from bookmarks will appear here."
|
||||
)
|
||||
} else {
|
||||
List {
|
||||
@@ -840,6 +841,7 @@ struct PodcastLibraryView: View {
|
||||
.listStyle(.insetGrouped)
|
||||
}
|
||||
}
|
||||
.paperSurface()
|
||||
.navigationTitle("Podcasts")
|
||||
.toolbar {
|
||||
if !unplayed.isEmpty {
|
||||
@@ -888,18 +890,18 @@ struct PodcastLibraryView: View {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(entry.title ?? entry.articleUrl)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.font(PaperType.quote)
|
||||
.lineLimit(2)
|
||||
.foregroundStyle(entry.isPlayed ? .secondary : .primary)
|
||||
Text(entry.createdAt.formatted(date: .abbreviated, time: .omitted))
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(PaperType.micro)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
}
|
||||
Spacer()
|
||||
if entry.isPlayed {
|
||||
Image(systemName: "checkmark.circle.fill")
|
||||
.font(.system(size: 15))
|
||||
.foregroundStyle(.green)
|
||||
.foregroundStyle(Paper.affirm)
|
||||
.accessibilityLabel("Played")
|
||||
}
|
||||
Button {
|
||||
@@ -907,7 +909,7 @@ struct PodcastLibraryView: View {
|
||||
} label: {
|
||||
Image(systemName: isCurrent(entry) && vm.isPlaying ? "pause.circle.fill" : "play.circle.fill")
|
||||
.font(.system(size: 36))
|
||||
.foregroundStyle(.blue)
|
||||
.foregroundStyle(Paper.accent)
|
||||
.contentTransition(.symbolEffect(.replace))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
@@ -1005,17 +1007,17 @@ struct EpisodePickerView: View {
|
||||
HStack(spacing: 12) {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(entry.title ?? entry.articleUrl)
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.font(PaperType.quote)
|
||||
.lineLimit(2)
|
||||
if entry.articleUrl != bookmark.url {
|
||||
Text(entry.articleUrl)
|
||||
.font(.system(size: 11))
|
||||
.foregroundStyle(.tertiary)
|
||||
.font(PaperType.micro)
|
||||
.foregroundStyle(Paper.tertiary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Text(entry.createdAt.formatted(date: .abbreviated, time: .omitted))
|
||||
.font(.system(size: 12))
|
||||
.foregroundStyle(.secondary)
|
||||
.font(PaperType.micro)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
}
|
||||
Spacer()
|
||||
Button {
|
||||
@@ -1026,7 +1028,7 @@ struct EpisodePickerView: View {
|
||||
} label: {
|
||||
Image(systemName: "play.circle.fill")
|
||||
.font(.system(size: 36))
|
||||
.foregroundStyle(.blue)
|
||||
.foregroundStyle(Paper.accent)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
@@ -1095,21 +1097,21 @@ struct EpisodeDetailView: View {
|
||||
Section {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(entry.title ?? entry.articleUrl)
|
||||
.font(.system(size: 20, weight: .semibold))
|
||||
.font(PaperType.display)
|
||||
Label(domain, systemImage: "link")
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
.lineLimit(1)
|
||||
HStack(spacing: 12) {
|
||||
Label(entry.createdAt.formatted(date: .abbreviated, time: .omitted),
|
||||
systemImage: "calendar")
|
||||
if isPlayed {
|
||||
Label("Played", systemImage: "checkmark.circle.fill")
|
||||
.foregroundStyle(.green)
|
||||
.foregroundStyle(Paper.affirm)
|
||||
}
|
||||
}
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
@@ -1118,7 +1120,7 @@ struct EpisodeDetailView: View {
|
||||
Section("Summary") {
|
||||
Text(summary)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(.secondary)
|
||||
.foregroundStyle(Paper.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user