Phase 1 - Focus & Typography: New TVFocusButtonStyle with 1.04x scale +
white glow border on focus, 0.97x press. Enforced 22px minimum text on
tvOS across DesignSystem (tvCaption 22px, tvBody 24px, tvDataValue 24px).
DataLabelStyle uses tvOS caption with reduced kerning.
Phase 2 - Today Tab: FeaturedGameCard redesigned as full-bleed hero with
away team left, home team right, 96pt score centered, DiamondView for
live count/outs. Removed side panel, replaced with single subtitle row.
GameCardView redesigned as horizontal score-bug style (~120px tall vs
320px) with team color accent bar, stacked logos, centered score, inline
mini linescore. Both show record + streak on every card.
Phase 3 - Intel Tab: Side-by-side layout on tvOS with standings
horizontal scroll on left (60%) and leaders vertical column on right
(40%). Both visible without scrolling past each other. iOS keeps the
stacked layout.
Phase 4 - Feed: Cards now horizontal with thumbnail left (300px tvOS),
info right. Added timestamp ("2h ago") to every card. All text meets
22px minimum on tvOS. Condensed game badge uses larger font.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
107 lines
4.0 KiB
Swift
107 lines
4.0 KiB
Swift
import SwiftUI
|
|
|
|
// MARK: - The Dugout Design System
|
|
|
|
enum DS {
|
|
// MARK: - Colors
|
|
|
|
enum Colors {
|
|
static let background = Color(red: 0.02, green: 0.03, blue: 0.05)
|
|
static let panelFill = Color.white.opacity(0.04)
|
|
static let panelStroke = Color.white.opacity(0.06)
|
|
|
|
static let live = Color(red: 0.95, green: 0.22, blue: 0.22)
|
|
static let positive = Color(red: 0.20, green: 0.78, blue: 0.35)
|
|
static let warning = Color(red: 0.95, green: 0.55, blue: 0.15)
|
|
static let interactive = Color(red: 0.25, green: 0.52, blue: 0.95)
|
|
static let media = Color(red: 0.55, green: 0.35, blue: 0.85)
|
|
|
|
static let textPrimary = Color.white
|
|
static let textSecondary = Color.white.opacity(0.7)
|
|
static let textTertiary = Color.white.opacity(0.45)
|
|
static let textQuaternary = Color.white.opacity(0.2)
|
|
}
|
|
|
|
// MARK: - Typography
|
|
|
|
enum Fonts {
|
|
static let heroScore = Font.system(size: 72, weight: .black, design: .rounded).monospacedDigit()
|
|
static let largeScore = Font.system(size: 42, weight: .black, design: .rounded).monospacedDigit()
|
|
static let score = Font.system(size: 28, weight: .black, design: .rounded).monospacedDigit()
|
|
static let scoreCompact = Font.system(size: 22, weight: .bold, design: .rounded).monospacedDigit()
|
|
|
|
static let sectionTitle = Font.system(size: 28, weight: .bold, design: .rounded)
|
|
static let cardTitle = Font.system(size: 20, weight: .bold, design: .rounded)
|
|
static let cardTitleCompact = Font.system(size: 17, weight: .bold, design: .rounded)
|
|
|
|
static let dataValue = Font.system(size: 18, weight: .bold, design: .rounded).monospacedDigit()
|
|
static let dataValueCompact = Font.system(size: 15, weight: .semibold, design: .rounded).monospacedDigit()
|
|
static let body = Font.system(size: 15, weight: .medium)
|
|
static let bodySmall = Font.system(size: 13, weight: .medium)
|
|
static let caption = Font.system(size: 11, weight: .bold, design: .rounded)
|
|
|
|
// tvOS scaled variants — 22px minimum for readability at 10ft
|
|
#if os(tvOS)
|
|
static let tvSectionTitle = Font.system(size: 38, weight: .bold, design: .rounded)
|
|
static let tvCardTitle = Font.system(size: 28, weight: .bold, design: .rounded)
|
|
static let tvScore = Font.system(size: 36, weight: .black, design: .rounded).monospacedDigit()
|
|
static let tvDataValue = Font.system(size: 24, weight: .bold, design: .rounded).monospacedDigit()
|
|
static let tvBody = Font.system(size: 24, weight: .medium)
|
|
static let tvCaption = Font.system(size: 22, weight: .bold, design: .rounded)
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Spacing
|
|
|
|
enum Spacing {
|
|
#if os(tvOS)
|
|
static let panelPadCompact: CGFloat = 18
|
|
static let panelPadStandard: CGFloat = 24
|
|
static let panelPadFeatured: CGFloat = 32
|
|
static let sectionGap: CGFloat = 40
|
|
static let cardGap: CGFloat = 20
|
|
static let itemGap: CGFloat = 12
|
|
static let edgeInset: CGFloat = 50
|
|
#else
|
|
static let panelPadCompact: CGFloat = 12
|
|
static let panelPadStandard: CGFloat = 16
|
|
static let panelPadFeatured: CGFloat = 24
|
|
static let sectionGap: CGFloat = 28
|
|
static let cardGap: CGFloat = 14
|
|
static let itemGap: CGFloat = 8
|
|
static let edgeInset: CGFloat = 20
|
|
#endif
|
|
}
|
|
|
|
// MARK: - Radii
|
|
|
|
enum Radii {
|
|
static let compact: CGFloat = 14
|
|
static let standard: CGFloat = 18
|
|
static let featured: CGFloat = 22
|
|
}
|
|
}
|
|
|
|
// MARK: - Data Label Style
|
|
|
|
struct DataLabelStyle: ViewModifier {
|
|
func body(content: Content) -> some View {
|
|
content
|
|
#if os(tvOS)
|
|
.font(DS.Fonts.tvCaption)
|
|
.kerning(1.0)
|
|
#else
|
|
.font(DS.Fonts.caption)
|
|
.kerning(1.5)
|
|
#endif
|
|
.foregroundStyle(DS.Colors.textQuaternary)
|
|
.textCase(.uppercase)
|
|
}
|
|
}
|
|
|
|
extension View {
|
|
func dataLabelStyle() -> some View {
|
|
modifier(DataLabelStyle())
|
|
}
|
|
}
|