mirror of
https://github.com/wu736139669/hapi.git
synced 2026-10-08 19:19:42 +00:00
Use unified 16pt Dynamic Type body text, clearer Markdown spacing, and a shared reading column. Preserve transcript anchors across typography changes and bound tool command previews to two lines. Add layout and typography regression tests.
92 lines
3.5 KiB
Swift
92 lines
3.5 KiB
Swift
import SwiftUI
|
|
|
|
/// Resolved once at the presentation root; never scale these values again.
|
|
/// Separate from color palettes so light, dark and OLED have identical metrics.
|
|
public struct HapiTypography: Equatable, Sendable {
|
|
public var bodyScale: CGFloat
|
|
public var codeScale: CGFloat
|
|
public var captionScale: CGFloat
|
|
public var boldText: Bool
|
|
|
|
public init(bodyScale: CGFloat = 1, codeScale: CGFloat = 1,
|
|
captionScale: CGFloat = 1, boldText: Bool = false) {
|
|
self.bodyScale = bodyScale
|
|
self.codeScale = codeScale
|
|
self.captionScale = captionScale
|
|
self.boldText = boldText
|
|
}
|
|
|
|
public var bodySize: CGFloat { 16 * bodyScale }
|
|
public var codeSize: CGFloat { 14 * codeScale }
|
|
public var captionSize: CGFloat { 12 * captionScale }
|
|
public var inlineCodeSize: CGFloat { 15 * bodyScale }
|
|
public var bodyLineSpacing: CGFloat { 3 * bodyScale }
|
|
public var codeLineSpacing: CGFloat { 2 * codeScale }
|
|
public var bodyFont: Font { .system(size: bodySize, weight: regularWeight) }
|
|
public var codeFont: Font { .system(size: codeSize, weight: regularWeight, design: .monospaced) }
|
|
public var captionFont: Font { .system(size: captionSize, weight: regularWeight) }
|
|
public var inlineCodeFont: Font { .system(size: inlineCodeSize, weight: regularWeight, design: .monospaced) }
|
|
public var captionMonoFont: Font { .system(size: captionSize, weight: regularWeight, design: .monospaced) }
|
|
|
|
private var regularWeight: Font.Weight { boldText ? .semibold : .regular }
|
|
|
|
public func headingSize(_ level: Int) -> CGFloat {
|
|
let base: CGFloat = switch level {
|
|
case 1: 24
|
|
case 2: 20
|
|
case 3: 18
|
|
default: 17
|
|
}
|
|
return base * bodyScale
|
|
}
|
|
|
|
public func headingFont(_ level: Int) -> Font {
|
|
.system(size: headingSize(level), weight: boldText ? .bold : .semibold)
|
|
}
|
|
}
|
|
|
|
private struct HapiTypographyKey: EnvironmentKey {
|
|
static let defaultValue = HapiTypography()
|
|
}
|
|
|
|
public extension EnvironmentValues {
|
|
var hapiTypography: HapiTypography {
|
|
get { self[HapiTypographyKey.self] }
|
|
set { self[HapiTypographyKey.self] = newValue }
|
|
}
|
|
}
|
|
|
|
private struct HapiTypographyModifier: ViewModifier {
|
|
@ScaledMetric(relativeTo: .body) private var bodyScale: CGFloat = 1
|
|
@ScaledMetric(relativeTo: .footnote) private var codeScale: CGFloat = 1
|
|
@ScaledMetric(relativeTo: .caption) private var captionScale: CGFloat = 1
|
|
@Environment(\.legibilityWeight) private var legibilityWeight
|
|
|
|
func body(content: Content) -> some View {
|
|
content.environment(\.hapiTypography, HapiTypography(
|
|
bodyScale: bodyScale, codeScale: codeScale, captionScale: captionScale,
|
|
boldText: legibilityWeight == .bold
|
|
))
|
|
}
|
|
}
|
|
|
|
public enum HapiReadingLayout {
|
|
public static let horizontalInset: CGFloat = 16
|
|
public static let maximumWidth: CGFloat = 720
|
|
|
|
public static func contentWidth(in availableWidth: CGFloat) -> CGFloat {
|
|
max(1, min(maximumWidth, availableWidth - 2 * horizontalInset))
|
|
}
|
|
}
|
|
|
|
public extension View {
|
|
/// Install outside the chat list so hosted rows inherit resolved metrics.
|
|
func hapiTypography() -> some View { modifier(HapiTypographyModifier()) }
|
|
|
|
func hapiReadingColumn(horizontalInset: CGFloat = HapiReadingLayout.horizontalInset) -> some View {
|
|
frame(maxWidth: HapiReadingLayout.maximumWidth, alignment: .leading)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.horizontal, horizontalInset)
|
|
}
|
|
}
|