diff --git a/Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md b/Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md index 00fbb8c..24206f3 100644 --- a/Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md +++ b/Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md @@ -6,11 +6,45 @@ This guide documents the typography and text styling system in Bedrock. The typography system consists of: -1. **`Typography` enum** - All font definitions with explicit naming -2. **`TextEmphasis` enum** - Semantic text colors -3. **`StyledLabel`** - Simple text component combining typography + emphasis -4. **`IconLabel`** - Icon + text component -5. **`.typography()` modifier** - View extension for applying fonts +1. **`Theme`** - Global theme registration for all color providers +2. **`Typography` enum** - All font definitions with explicit naming +3. **`TextEmphasis` enum** - Semantic text colors +4. **`StyledLabel`** - Simple text component combining typography + emphasis +5. **`IconLabel`** - Icon + text component +6. **`.typography()` modifier** - View extension for applying fonts + +--- + +## App Setup + +Register your app's theme once at launch: + +```swift +// In App.init() +Theme.register( + text: AppTextColors.self, + surface: AppSurface.self, + accent: AppAccent.self, + status: AppStatus.self +) +Theme.register(border: AppBorder.self) // Optional +``` + +This enables Bedrock components to use your app's colors automatically. + +### Using Theme Colors + +After registration, use `Theme.*` to access your registered colors: + +```swift +// In Bedrock components (automatic via TextEmphasis) +StyledLabel("Title", .heading) // Uses Theme.Text.primary + +// Direct access when needed +let bgColor = Theme.Surface.card +let accentColor = Theme.Accent.primary +let successColor = Theme.Status.success +``` --- @@ -50,65 +84,73 @@ All fonts are defined in the `Typography` enum. Each case name explicitly descri | | `.caption2` | caption2 | | | `.caption2Emphasis` | caption2.weight(.semibold) | -### Usage - -```swift -// Using the .typography() modifier (preferred) -Text("Hello") - .typography(.heading) - -// Using .font() directly -Text("Hello") - .font(Typography.heading.font) -``` - --- ## TextEmphasis Enum -Semantic text colors that work with any theme: +Semantic text colors resolved from the registered `TextTheme`: | Emphasis | Description | |----------|-------------| -| `.primary` | Main text color | +| `.primary` | Main text color (default) | | `.secondary` | Supporting text | | `.tertiary` | Subtle/hint text | | `.disabled` | Disabled state | | `.inverse` | Contrasting backgrounds | | `.custom(Color)` | Custom color override | -### Usage +### When to Use `.custom()` + +Only use `.custom()` for colors that aren't text colors: ```swift -// With StyledLabel -StyledLabel("Title", .heading, emphasis: .primary) +// ✅ Semantic text colors - use the emphasis value directly +StyledLabel("Title", .heading) // .primary is default StyledLabel("Subtitle", .subheading, emphasis: .secondary) -StyledLabel("Custom", .body, emphasis: .custom(.red)) +StyledLabel("Hint", .caption, emphasis: .tertiary) -// With custom theme colors -StyledLabel("Title", .heading, emphasis: .custom(AppTextColors.primary)) +// ✅ Use .custom() for non-text colors +StyledLabel("Success!", .heading, emphasis: .custom(AppStatus.success)) +StyledLabel("Arc 3", .caption, emphasis: .custom(AppAccent.primary)) + +// ✅ Use .custom() for conditional colors +StyledLabel(text, .body, emphasis: .custom(isActive ? AppStatus.success : AppTextColors.tertiary)) ``` --- ## StyledLabel Component -A single component for all styled text. Replaces the old `TitleLabel`, `BodyLabel`, `CaptionLabel`. +A single component for all styled text: ```swift -StyledLabel(_ text: String, _ typography: Typography = .body, emphasis: TextEmphasis = .primary) +StyledLabel( + _ text: String, + _ typography: Typography = .body, + emphasis: TextEmphasis = .primary, + alignment: TextAlignment? = nil, + lineLimit: Int? = nil +) ``` ### Examples ```swift -// Simple usage +// Simple - uses registered theme's primary color StyledLabel("Morning Ritual", .heading) StyledLabel("Day 6 of 28", .caption, emphasis: .secondary) -// With custom theme colors -StyledLabel("Title", .heading, emphasis: .custom(AppTextColors.primary)) -StyledLabel("Subtitle", .subheading, emphasis: .custom(AppTextColors.secondary)) +// With alignment and line limit +StyledLabel("Centered text", .body, alignment: .center) +StyledLabel("One line", .caption, emphasis: .tertiary, lineLimit: 1) + +// In buttons +Button(action: onContinue) { + StyledLabel("Continue", .heading, emphasis: .inverse) + .frame(maxWidth: .infinity) + .frame(height: 50) + .background(AppAccent.primary) +} ``` --- @@ -126,6 +168,7 @@ IconLabel(_ icon: String, _ text: String, _ typography: Typography = .body, emph ```swift IconLabel("bell.fill", "Notifications", .subheading) IconLabel("star.fill", "Favorites", .body, emphasis: .secondary) +IconLabel("flame.fill", "5-day streak", .caption, emphasis: .custom(AppStatus.success)) ``` --- @@ -134,34 +177,20 @@ IconLabel("star.fill", "Favorites", .body, emphasis: .secondary) ### Use StyledLabel (Preferred) -Use `StyledLabel` for the vast majority of text: +Use `StyledLabel` for 95% of text: ```swift -// Simple text StyledLabel("Settings", .subheadingEmphasis) StyledLabel("Description", .subheading, emphasis: .secondary) - -// With multiline alignment -StyledLabel("Centered text", .body, emphasis: .primary, alignment: .center) - -// With line limit -StyledLabel("One line only", .caption, emphasis: .secondary, lineLimit: 1) - -// In buttons (put frame modifiers outside) -Button(action: onContinue) { - StyledLabel("Continue", .heading, emphasis: .custom(AppTextColors.inverse)) - .frame(maxWidth: .infinity) - .frame(height: 50) - .background(AppAccent.primary) -} +StyledLabel("Centered", .body, alignment: .center) ``` ### Use Text() with .typography() (Rare Exceptions) -Only use `Text()` when you genuinely need modifiers that `StyledLabel` doesn't support: +Only when you need modifiers `StyledLabel` doesn't support: ```swift -// Font design variants (rounded, etc.) +// Font design variants Text(formattedValue) .typography(.subheadingEmphasis) .fontDesign(.rounded) @@ -173,24 +202,18 @@ TextField("Placeholder", text: $value) // Inside special view builders (like Charts AxisValueLabel) AxisValueLabel { Text("\(value)%") - .font(Typography.caption2.font) // .typography() won't work here + .font(Typography.caption2.font) .foregroundStyle(color) } ``` ### Never Use Raw `.font()` with System Fonts -Instead of: - ```swift -// BAD +// ❌ BAD Text("Title").font(.headline) -``` -Use: - -```swift -// GOOD +// ✅ GOOD StyledLabel("Title", .heading) ``` @@ -203,23 +226,17 @@ StyledLabel("Title", .heading) ```swift // OLD: Design.Typography Text("Title").font(Design.Typography.headline) -Text("Body").font(Design.Typography.body) -Text("Caption").font(Design.Typography.caption) -// NEW: Typography enum -Text("Title").typography(.heading) -Text("Body").typography(.subheading) -Text("Caption").typography(.caption) +// NEW: StyledLabel +StyledLabel("Title", .heading) // OLD: Multiple Label components TitleLabel("Title", style: .headline, color: .white) BodyLabel("Body", emphasis: .secondary) -CaptionLabel("Caption") // NEW: Single StyledLabel -StyledLabel("Title", .heading, emphasis: .custom(.white)) +StyledLabel("Title", .heading, emphasis: .inverse) StyledLabel("Body", .subheading, emphasis: .secondary) -StyledLabel("Caption", .caption, emphasis: .secondary) ``` ### Mapping Table @@ -237,8 +254,6 @@ StyledLabel("Caption", .caption, emphasis: .secondary) | `.caption` | `.caption` | | `.captionMedium` | `.captionEmphasis` | | `.captionSmall` | `.caption2` | -| `.settingsTitle` | `.subheadingEmphasis` | -| `.sectionHeader` | `.captionEmphasis` | --- diff --git a/Sources/Bedrock/Theme/TextEmphasis.swift b/Sources/Bedrock/Theme/TextEmphasis.swift index 53f4c3c..c53928c 100644 --- a/Sources/Bedrock/Theme/TextEmphasis.swift +++ b/Sources/Bedrock/Theme/TextEmphasis.swift @@ -2,25 +2,36 @@ // TextEmphasis.swift // Bedrock // -// Semantic text color emphasis levels that work with any TextColorProvider theme. +// Semantic text color emphasis levels. // import SwiftUI +// MARK: - Text Emphasis + /// Semantic text color emphasis levels. /// -/// Use `TextEmphasis` to specify text color semantically rather than with explicit colors. -/// The actual color is resolved from the app's registered `TextColorProvider`. +/// Use `TextEmphasis` to specify text color semantically. Colors are resolved +/// from the globally registered `Theme`. /// /// ## Example /// /// ```swift -/// StyledLabel("Primary text", .body, emphasis: .primary) +/// // Register your theme once at app launch +/// Theme.register( +/// text: AppTextColors.self, +/// surface: AppSurface.self, +/// accent: AppAccent.self, +/// status: AppStatus.self +/// ) +/// +/// // Then use emphasis levels directly +/// StyledLabel("Primary text", .body) // uses .primary /// StyledLabel("Secondary text", .caption, emphasis: .secondary) /// StyledLabel("Custom color", .heading, emphasis: .custom(.red)) /// ``` public enum TextEmphasis: Sendable { - /// Primary text color (highest emphasis). + /// Primary text color (highest emphasis). Default. case primary /// Secondary text color (medium emphasis). case secondary @@ -30,31 +41,24 @@ public enum TextEmphasis: Sendable { case disabled /// Inverse text color (for contrasting backgrounds). case inverse - /// Custom color override. + /// Custom color override for one-off cases. case custom(Color) - /// Resolves the color from a specific TextColorProvider. - /// - /// Use this when you need to explicitly specify which theme to use: - /// ```swift - /// emphasis.color(from: MyAppTextColors.self) - /// ``` - public func color(from provider: T.Type) -> Color { + /// Resolves the color from the globally registered Theme. + public var color: Color { switch self { - case .primary: provider.primary - case .secondary: provider.secondary - case .tertiary: provider.tertiary - case .disabled: provider.disabled - case .inverse: provider.inverse + case .primary: Theme.Text.primary + case .secondary: Theme.Text.secondary + case .tertiary: Theme.Text.tertiary + case .disabled: Theme.Text.disabled + case .inverse: Theme.Text.inverse case .custom(let color): color } } - - /// Resolves the color from the default TextColorProvider. - /// - /// This uses `Color.TextColors` (the default Bedrock text colors). - /// Apps with custom themes should use `color(from:)` instead. - public var color: Color { - color(from: DefaultTextColors.self) - } } + +// MARK: - Deprecated TextTheme + +/// Deprecated: Use `Theme` instead. +@available(*, deprecated, renamed: "Theme", message: "Use Theme.register() instead") +public typealias TextTheme = Theme diff --git a/Sources/Bedrock/Theme/Theme.swift b/Sources/Bedrock/Theme/Theme.swift new file mode 100644 index 0000000..7d27a1b --- /dev/null +++ b/Sources/Bedrock/Theme/Theme.swift @@ -0,0 +1,124 @@ +// +// Theme.swift +// Bedrock +// +// Global theme registration for Bedrock components. +// + +import SwiftUI + +// MARK: - Global Theme Registration + +/// Global theme provider for Bedrock components. +/// +/// Register your app's theme once at launch to enable semantic colors +/// in `StyledLabel`, `IconLabel`, and other Bedrock components. +/// +/// ## Usage +/// +/// ```swift +/// // In App.init() +/// Theme.register( +/// text: AppTextColors.self, +/// surface: AppSurface.self, +/// accent: AppAccent.self, +/// status: AppStatus.self +/// ) +/// +/// // Then use semantic emphasis directly +/// StyledLabel("Title", .heading) // uses Theme.Text.primary +/// StyledLabel("Subtitle", .subheading, emphasis: .secondary) // uses Theme.Text.secondary +/// ``` +public enum Theme { + // MARK: - Registered Providers + + // Set once at app launch, read-only thereafter + nonisolated(unsafe) private static var _text: any TextColorProvider.Type = DefaultTextColors.self + nonisolated(unsafe) private static var _surface: any SurfaceColorProvider.Type = DefaultSurfaceColors.self + nonisolated(unsafe) private static var _accent: any AccentColorProvider.Type = DefaultAccentColors.self + nonisolated(unsafe) private static var _status: any StatusColorProvider.Type = DefaultStatusColors.self + nonisolated(unsafe) private static var _border: any BorderColorProvider.Type = DefaultBorderColors.self + + // MARK: - Registration + + /// Registers custom color providers for the app. + /// + /// Call this once at app launch: + /// ```swift + /// Theme.register( + /// text: AppTextColors.self, + /// surface: AppSurface.self, + /// accent: AppAccent.self, + /// status: AppStatus.self + /// ) + /// ``` + public static func register( + text: T.Type, + surface: S.Type, + accent: A.Type, + status: St.Type + ) { + _text = text + _surface = surface + _accent = accent + _status = status + } + + /// Registers a border color provider (optional). + public static func register(border: B.Type) { + _border = border + } + + // MARK: - Text Colors + + /// Registered text color provider. + public enum Text { + public static var primary: Color { _text.primary } + public static var secondary: Color { _text.secondary } + public static var tertiary: Color { _text.tertiary } + public static var disabled: Color { _text.disabled } + public static var inverse: Color { _text.inverse } + } + + // MARK: - Surface Colors + + /// Registered surface color provider. + public enum Surface { + public static var primary: Color { _surface.primary } + public static var secondary: Color { _surface.secondary } + public static var tertiary: Color { _surface.tertiary } + public static var card: Color { _surface.card } + public static var overlay: Color { _surface.overlay } + } + + // MARK: - Accent Colors + + /// Registered accent color provider. + public enum Accent { + public static var primary: Color { _accent.primary } + public static var light: Color { _accent.light } + public static var dark: Color { _accent.dark } + public static var secondary: Color { _accent.secondary } + } + + // MARK: - Status Colors + + /// Registered status color provider. + public enum Status { + public static var success: Color { _status.success } + public static var warning: Color { _status.warning } + public static var error: Color { _status.error } + public static var info: Color { _status.info } + } + + // MARK: - Border Colors + + /// Registered border color provider. + public enum Border { + public static var subtle: Color { _border.subtle } + public static var standard: Color { _border.standard } + public static var emphasized: Color { _border.emphasized } + } +} + +// Note: Default providers are defined in Colors.swift