diff --git a/Jarvis/Views/Home/SignalFeedView.swift b/Jarvis/Views/Home/SignalFeedView.swift index 61c27a3..9438f62 100644 --- a/Jarvis/Views/Home/SignalFeedView.swift +++ b/Jarvis/Views/Home/SignalFeedView.swift @@ -292,7 +292,10 @@ struct SignalFeedView: View { @ViewBuilder private var digestContent: some View { let d = digest sectionHeader("TOP STORIES", pill: nil) - ForEach(d.top) { mainRow($0) } + if let lead = d.top.first { + mainRow(lead, hero: true) + ForEach(d.top.dropFirst()) { mainRow($0) } + } ForEach(d.sections, id: \.0.id) { section, stories in sectionHeader(section.label.uppercased(), pill: section.pill) ForEach(stories) { mainRow($0) } @@ -323,11 +326,17 @@ struct SignalFeedView: View { } @ViewBuilder - private func mainRow(_ story: StorySummary) -> some View { + private func mainRow(_ story: StorySummary, hero: Bool = false) -> some View { NavigationLink(value: story) { - StoryRowView(story: story, isCached: cachedStoryIds.contains(story.id)) + if hero { + heroCard(story) + } else { + StoryRowView(story: story, isCached: cachedStoryIds.contains(story.id)) + } } - .plainBlackRow() + .listRowInsets(hero ? EdgeInsets(top: 2, leading: 16, bottom: 12, trailing: 16) : EdgeInsets()) + .listRowBackground(Color.black) + .listRowSeparator(.hidden) .onAppear { markSeen(story) if story.id == mainStories.last?.id { Task { await store.loadMore() } } @@ -353,6 +362,47 @@ struct SignalFeedView: View { } } + /// Apple-News-style lead card for the #1 Top Story โ€” flat surface, big type. + private func heroCard(_ story: StorySummary) -> some View { + HStack(alignment: .top, spacing: 0) { + SignalStripe(score: story.signalScore, width: 4) + VStack(alignment: .leading, spacing: 11) { + HStack(alignment: .firstTextBaseline) { + Text(Topic.label(story.topic).uppercased()) + .font(.system(size: 10, weight: .bold, design: .monospaced)).kerning(0.8) + .foregroundStyle(Color(hex: "8A8A8A")) + Spacer() + Text("\(story.signalScore)") + .font(.system(size: 21, weight: .heavy, design: .monospaced)) + .foregroundStyle(Signal.scoreColor(story.signalScore)) + } + Text(story.headline) + .font(.system(size: 24, weight: .heavy)) + .foregroundStyle(.white) + .lineLimit(4) + .fixedSize(horizontal: false, vertical: true) + if !story.summary.isEmpty { + Text(story.summary) + .font(.system(size: 14)) + .foregroundStyle(Color(hex: "9A9A9A")) + .lineLimit(3).lineSpacing(2) + .fixedSize(horizontal: false, vertical: true) + } + if !story.sources.isEmpty { + SourceChips(names: story.sources.map(\.name), total: story.sourceCount) + } + Text("\(story.sourceCount) sources ยท \(story.updatedAt.timeAgoShort()) ago") + .font(.system(size: 11, weight: .regular, design: .monospaced)) + .foregroundStyle(Color(hex: "666666")) + } + .padding(16) + Spacer(minLength: 0) + } + .background(Palette.surface) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .overlay(RoundedRectangle(cornerRadius: 16).stroke(Palette.surface2, lineWidth: 0.5)) + } + private var caughtUpRow: some View { VStack(spacing: 10) { Image(systemName: "checkmark.circle") diff --git a/Jarvis/Views/Shared/Theme.swift b/Jarvis/Views/Shared/Theme.swift index 3424edc..100bf78 100644 --- a/Jarvis/Views/Shared/Theme.swift +++ b/Jarvis/Views/Shared/Theme.swift @@ -106,11 +106,15 @@ enum Signal { enum Topic { static func label(_ slug: String) -> String { switch slug.lowercased() { - case "finance": return "Finance" - case "tech": return "Tech" - case "politics": return "Politics" - case "africa": return "Africa" - default: return slug.prefix(1).uppercased() + slug.dropFirst() + case "ai", "artificial-intelligence": return "AI" + case "us", "united-states": return "US" + case "formula-1": return "Formula 1" + case "ui-ux", "web-design-and-ui-ux": return "Web Design" + default: + // Slug โ†’ Title Case, e.g. "cloud-computing" โ†’ "Cloud Computing". + return slug.split(separator: "-") + .map { $0.prefix(1).uppercased() + $0.dropFirst() } + .joined(separator: " ") } }