|
| 1 | +import CoreGraphics |
| 2 | + |
| 3 | +/// Centralised card sizing tokens. Every card surface in the app derives its |
| 4 | +/// corner radii, internal padding, and spacing from here so changes propagate |
| 5 | +/// everywhere at once and the visual system stays coherent. |
| 6 | +/// |
| 7 | +/// Inspired by Plozz's shared-metrics approach: a single source of truth that |
| 8 | +/// any card view can reference, with the concentric-radius pattern (outer glass |
| 9 | +/// radius = inner media radius + card inset) guaranteeing an even-width border |
| 10 | +/// at every size. |
| 11 | +enum CardMetrics { |
| 12 | + |
| 13 | + // MARK: - Card Inset (glass edge → content artwork) |
| 14 | + |
| 15 | + /// The visual gap between the outer glass surface and the inner content/media. |
| 16 | + /// Corner radii are concentric: outerRadius = innerRadius + cardInset, so the |
| 17 | + /// glass border renders at a constant width around the artwork. |
| 18 | + static let cardInset: CGFloat = 12 |
| 19 | + |
| 20 | + // MARK: - Corner Radii (inner media radii; outer derived concentrically) |
| 21 | + |
| 22 | + /// Standard media/artwork corner radius used for rail cards, category art, |
| 23 | + /// content cards, and most surfaces. |
| 24 | + static let mediaCornerRadius: CGFloat = 18 |
| 25 | + |
| 26 | + /// Tighter media corner radius for compact grid cards. |
| 27 | + static let gridMediaCornerRadius: CGFloat = 14 |
| 28 | + |
| 29 | + /// Outer card corner radius: concentric with the standard media radius. |
| 30 | + static var cardCornerRadius: CGFloat { mediaCornerRadius + cardInset } // 30 |
| 31 | + |
| 32 | + /// Outer card corner radius for compact grid cards. |
| 33 | + static var gridCardCornerRadius: CGFloat { gridMediaCornerRadius + cardInset } // 26 |
| 34 | + |
| 35 | + /// Outer corner radius for hero/featured surfaces (slightly smaller than a |
| 36 | + /// full card, matching the larger single-item presentation). |
| 37 | + static let heroCornerRadius: CGFloat = 28 |
| 38 | + |
| 39 | + // MARK: - Focus Insets (breathing room for scale-up + internal padding) |
| 40 | + |
| 41 | + /// Horizontal/vertical inset between the glass edge and content for standard |
| 42 | + /// cards. Doubles as the focus breathing room for the scale-up animation. |
| 43 | + static let focusInset: CGFloat = 18 |
| 44 | + |
| 45 | + /// Internal content padding for compact grid cards. |
| 46 | + static let gridContentInset: CGFloat = 14 |
| 47 | + |
| 48 | + /// Internal padding for category (box-art) cards. |
| 49 | + static let categoryInset: CGFloat = 10 |
| 50 | + |
| 51 | + // MARK: - Spacing Between Cards |
| 52 | + |
| 53 | + /// Base horizontal gap between cards in rails and grids. Subtly tightens |
| 54 | + /// at higher card counts via ``spacingScale(forVisibleCardCount:)``. |
| 55 | + static let cardSpacing: CGFloat = 24 |
| 56 | + |
| 57 | + /// Vertical padding above/below a card rail row. |
| 58 | + static let railVerticalPadding: CGFloat = 24 |
| 59 | + |
| 60 | + /// Gap between the media thumbnail and the text/metadata below it. |
| 61 | + static let captionSpacing: CGFloat = 10 |
| 62 | + |
| 63 | + /// Gap between text lines within a card's caption area (e.g. title → game). |
| 64 | + static let captionLineSpacing: CGFloat = 4 |
| 65 | + |
| 66 | + /// Gap between the avatar and the text block in cards with a profile image. |
| 67 | + static let avatarTextSpacing: CGFloat = 10 |
| 68 | + |
| 69 | + /// Vertical gap between full rail sections on the Home tab. |
| 70 | + static let sectionSpacing: CGFloat = 72 |
| 71 | + |
| 72 | + // MARK: - Screen Layout |
| 73 | + |
| 74 | + /// Horizontal page gutter (matches ``AppLayout.horizontalPadding``). |
| 75 | + static let screenPadding: CGFloat = 24 |
| 76 | + |
| 77 | + // MARK: - Focus & Animation |
| 78 | + |
| 79 | + /// Uniform scale applied to a focused card. |
| 80 | + static let focusedScale: CGFloat = 1.07 |
| 81 | + |
| 82 | + /// Focus shadow opacity for dark/OLED themes. |
| 83 | + static let focusShadowOpacity: CGFloat = 0.36 |
| 84 | + |
| 85 | + /// Focus shadow opacity for light theme. |
| 86 | + static let focusShadowOpacityLight: CGFloat = 0.12 |
| 87 | + |
| 88 | + /// Focus shadow blur radius. |
| 89 | + static let focusShadowRadius: CGFloat = 20 |
| 90 | + |
| 91 | + /// Focus shadow vertical offset. |
| 92 | + static let focusShadowY: CGFloat = 10 |
| 93 | + |
| 94 | + // MARK: - Avatars |
| 95 | + |
| 96 | + /// Channel avatar diameter in rail cards. |
| 97 | + static let railAvatarSize: CGFloat = 62 |
| 98 | + |
| 99 | + /// Channel avatar diameter in grid cards. |
| 100 | + static let gridAvatarSize: CGFloat = 68 |
| 101 | + |
| 102 | + // MARK: - Adaptive Spacing |
| 103 | + |
| 104 | + /// Subtly tightens the gap between cards as more fit across the screen. |
| 105 | + /// Full base spacing at 2-across, easing to ~68 % at 6-across. |
| 106 | + static func spacingScale(forVisibleCardCount count: Int) -> CGFloat { |
| 107 | + let clamped = CGFloat(min(max(count, 2), 6)) |
| 108 | + return 1.0 - (clamped - 2) * 0.08 |
| 109 | + } |
| 110 | +} |
0 commit comments