feat: hero card layout in all pills, cache-while-loading, logo splash screen
- Every pill (F1, Tech, AI, Cloud, HomeLab…) now shows the same hero card layout as "All" — first story is the big featured card, rest follow as rows - "All" retains its full multi-section front page (US / Tech / AI / Sport…) - Topic pill headers use the pill name instead of "TOP STORIES" - filteredStories now falls back to cached SwiftData rows while loading, so switching to F1 or Tech shows cached cards instantly instead of a blank list - Logo splash screen (3 pill bars + jarvis wordmark) shows on launch and fades out after setup completes; minimum 1.3 s so it's always readable - JarvisWordmark moved to Theme.swift so SplashView and feed can share it Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,21 +5,6 @@
|
||||
import SwiftUI
|
||||
import SwiftData
|
||||
|
||||
// Reusable wordmark: typewriter font, lowercase j in orange, orange dot on i.
|
||||
struct JarvisWordmark: View {
|
||||
var size: CGFloat = 30
|
||||
|
||||
private var font: Font { .custom("Courier New", size: size).bold() }
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Text("j").font(font).foregroundStyle(Palette.orange)
|
||||
Text("arv").font(font).foregroundStyle(.white)
|
||||
Text("is").font(font).foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SignalFeedView: View {
|
||||
@EnvironmentObject var store: StoryStore
|
||||
@EnvironmentObject var ws: WebSocketManager
|
||||
@@ -46,16 +31,14 @@ struct SignalFeedView: View {
|
||||
private var readStoryIds: Set<String> { Set(readStories.map(\.id)) }
|
||||
private var savedStoryIds: Set<String> { Set(savedStories.map(\.id)) }
|
||||
|
||||
/// Live stories when online; cached fallback when offline. Filtered by the
|
||||
/// selected pill via backend tags — no headline keyword matching.
|
||||
/// Live stories when connected; cached fallback while loading or offline so
|
||||
/// switching to Tech / AI / F1 shows cached cards instantly.
|
||||
private var filteredStories: [StorySummary] {
|
||||
let base: [StorySummary]
|
||||
if !store.stories.isEmpty {
|
||||
base = store.stories.sorted(by: StorySummary.feedOrder)
|
||||
} else if !ws.connectionState.isLive {
|
||||
base = cachedStories.map(StorySummary.init(cached:)).sorted(by: StorySummary.feedOrder)
|
||||
} else {
|
||||
base = []
|
||||
base = cachedStories.map(StorySummary.init(cached:)).sorted(by: StorySummary.feedOrder)
|
||||
}
|
||||
return base.filter { $0.matches(selectedPill) }
|
||||
}
|
||||
@@ -73,8 +56,8 @@ struct SignalFeedView: View {
|
||||
filteredStories.filter { readStoryIds.contains($0.id) }
|
||||
}
|
||||
|
||||
/// "All" shows a sectioned front page; specific pills show a flat list.
|
||||
private var isDigest: Bool { selectedPill == .all }
|
||||
/// Every pill shows the card layout — sections are scoped to "All".
|
||||
private var isDigest: Bool { true }
|
||||
|
||||
/// Front-page digest: Top Stories + up to 3 per category section, deduped.
|
||||
private var digest: (top: [StorySummary], sections: [(NewsSection, [StorySummary])]) {
|
||||
@@ -125,7 +108,6 @@ struct SignalFeedView: View {
|
||||
header
|
||||
syncBar
|
||||
topicPills
|
||||
if !isDigest { columnHeaders } // sections carry their own headers
|
||||
Divider().overlay(Palette.hairline)
|
||||
feedList
|
||||
}
|
||||
@@ -260,13 +242,7 @@ struct SignalFeedView: View {
|
||||
|
||||
private var feedList: some View {
|
||||
List {
|
||||
if mainStories.isEmpty && readItems.isEmpty {
|
||||
emptyState.plainBlackRow()
|
||||
} else if isDigest {
|
||||
digestContent
|
||||
} else {
|
||||
flatContent
|
||||
}
|
||||
digestContent
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
@@ -283,28 +259,25 @@ struct SignalFeedView: View {
|
||||
}
|
||||
}
|
||||
|
||||
/// Flat list for a specific pill.
|
||||
@ViewBuilder private var flatContent: some View {
|
||||
ForEach(mainStories) { mainRow($0) }
|
||||
if mainStories.isEmpty { caughtUpRow.plainBlackRow() }
|
||||
if store.isLoadingMore {
|
||||
ProgressView().tint(Palette.orange)
|
||||
.frame(maxWidth: .infinity).padding(.vertical, 20).plainBlackRow()
|
||||
}
|
||||
readShelf
|
||||
}
|
||||
|
||||
/// Sectioned front page for "All".
|
||||
/// Card layout for every pill. "All" gets full multi-section front page;
|
||||
/// topic pills get a hero card + flat list within that topic.
|
||||
@ViewBuilder private var digestContent: some View {
|
||||
let d = digest
|
||||
sectionHeader("TOP STORIES", pill: nil)
|
||||
let headerLabel = selectedPill == .all
|
||||
? "TOP STORIES"
|
||||
: selectedPill.label.uppercased()
|
||||
sectionHeader(headerLabel, pill: nil)
|
||||
if let lead = d.top.first {
|
||||
mainRow(lead, hero: true)
|
||||
ForEach(d.top.dropFirst()) { mainRow($0) }
|
||||
} else {
|
||||
emptyState.plainBlackRow()
|
||||
}
|
||||
ForEach(d.sections, id: \.0.id) { section, stories in
|
||||
sectionHeader(section.label.uppercased(), pill: section.pill)
|
||||
ForEach(stories) { mainRow($0) }
|
||||
if selectedPill == .all {
|
||||
ForEach(d.sections, id: \.0.id) { section, stories in
|
||||
sectionHeader(section.label.uppercased(), pill: section.pill)
|
||||
ForEach(stories) { mainRow($0) }
|
||||
}
|
||||
}
|
||||
readShelf
|
||||
}
|
||||
|
||||
@@ -264,6 +264,19 @@ extension StorySummary {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Wordmark
|
||||
|
||||
struct JarvisWordmark: View {
|
||||
var size: CGFloat = 30
|
||||
private var font: Font { .custom("Courier New", size: size).bold() }
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
Text("j").font(font).foregroundStyle(Palette.orange)
|
||||
Text("arvis").font(font).foregroundStyle(.white)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - News sections (the "All" front page)
|
||||
//
|
||||
// Groups the firehose into Apple/Google-News-style sections. A story lands in
|
||||
|
||||
Reference in New Issue
Block a user