Front-page polish: hero lead card + title-cased labels
Some checks failed
CI / Build · macos-14 · Debug (push) Has been cancelled
CI / Build · macos-15 · Debug (push) Has been cancelled
CI / Build · macos-14 · Release (push) Has been cancelled
CI / Build · macos-15 · Release (push) Has been cancelled

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:
Robin Kutesa
2026-06-20 18:42:09 +03:00
parent da43399ead
commit 3f0d8348b9
2 changed files with 63 additions and 9 deletions

View File

@@ -292,7 +292,10 @@ struct SignalFeedView: View {
@ViewBuilder private var digestContent: some View { @ViewBuilder private var digestContent: some View {
let d = digest let d = digest
sectionHeader("TOP STORIES", pill: nil) 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 ForEach(d.sections, id: \.0.id) { section, stories in
sectionHeader(section.label.uppercased(), pill: section.pill) sectionHeader(section.label.uppercased(), pill: section.pill)
ForEach(stories) { mainRow($0) } ForEach(stories) { mainRow($0) }
@@ -323,11 +326,17 @@ struct SignalFeedView: View {
} }
@ViewBuilder @ViewBuilder
private func mainRow(_ story: StorySummary) -> some View { private func mainRow(_ story: StorySummary, hero: Bool = false) -> some View {
NavigationLink(value: story) { 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 { .onAppear {
markSeen(story) markSeen(story)
if story.id == mainStories.last?.id { Task { await store.loadMore() } } 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 { private var caughtUpRow: some View {
VStack(spacing: 10) { VStack(spacing: 10) {
Image(systemName: "checkmark.circle") Image(systemName: "checkmark.circle")

View File

@@ -106,11 +106,15 @@ enum Signal {
enum Topic { enum Topic {
static func label(_ slug: String) -> String { static func label(_ slug: String) -> String {
switch slug.lowercased() { switch slug.lowercased() {
case "finance": return "Finance" case "ai", "artificial-intelligence": return "AI"
case "tech": return "Tech" case "us", "united-states": return "US"
case "politics": return "Politics" case "formula-1": return "Formula 1"
case "africa": return "Africa" case "ui-ux", "web-design-and-ui-ux": return "Web Design"
default: return slug.prefix(1).uppercased() + slug.dropFirst() default:
// Slug Title Case, e.g. "cloud-computing" "Cloud Computing".
return slug.split(separator: "-")
.map { $0.prefix(1).uppercased() + $0.dropFirst() }
.joined(separator: " ")
} }
} }