Front-page polish: hero lead card + title-cased labels
Render the #1 Top Story as an Apple-News-style hero card (flat surface, 4pt stripe, 24pt headline, summary, sources, prominent score). Prettify category labels (slug -> Title Case, with AI/US/Formula 1 special-cases) across rows, hero, and detail. Per Emil's framework, no row entrance/press motion on a constantly-scrolled surface — invest in hierarchy and defaults instead. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -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")
|
||||
|
||||
@@ -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: " ")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user