Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-27 11:23:39 -06:00
parent 113660a7f9
commit 92e6de53c5
3 changed files with 238 additions and 95 deletions

View File

@ -6,11 +6,45 @@ This guide documents the typography and text styling system in Bedrock.
The typography system consists of: The typography system consists of:
1. **`Typography` enum** - All font definitions with explicit naming 1. **`Theme`** - Global theme registration for all color providers
2. **`TextEmphasis` enum** - Semantic text colors 2. **`Typography` enum** - All font definitions with explicit naming
3. **`StyledLabel`** - Simple text component combining typography + emphasis 3. **`TextEmphasis` enum** - Semantic text colors
4. **`IconLabel`** - Icon + text component 4. **`StyledLabel`** - Simple text component combining typography + emphasis
5. **`.typography()` modifier** - View extension for applying fonts 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 | | | `.caption2` | caption2 |
| | `.caption2Emphasis` | caption2.weight(.semibold) | | | `.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 ## TextEmphasis Enum
Semantic text colors that work with any theme: Semantic text colors resolved from the registered `TextTheme`:
| Emphasis | Description | | Emphasis | Description |
|----------|-------------| |----------|-------------|
| `.primary` | Main text color | | `.primary` | Main text color (default) |
| `.secondary` | Supporting text | | `.secondary` | Supporting text |
| `.tertiary` | Subtle/hint text | | `.tertiary` | Subtle/hint text |
| `.disabled` | Disabled state | | `.disabled` | Disabled state |
| `.inverse` | Contrasting backgrounds | | `.inverse` | Contrasting backgrounds |
| `.custom(Color)` | Custom color override | | `.custom(Color)` | Custom color override |
### Usage ### When to Use `.custom()`
Only use `.custom()` for colors that aren't text colors:
```swift ```swift
// With StyledLabel // ✅ Semantic text colors - use the emphasis value directly
StyledLabel("Title", .heading, emphasis: .primary) StyledLabel("Title", .heading) // .primary is default
StyledLabel("Subtitle", .subheading, emphasis: .secondary) StyledLabel("Subtitle", .subheading, emphasis: .secondary)
StyledLabel("Custom", .body, emphasis: .custom(.red)) StyledLabel("Hint", .caption, emphasis: .tertiary)
// With custom theme colors // ✅ Use .custom() for non-text colors
StyledLabel("Title", .heading, emphasis: .custom(AppTextColors.primary)) 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 ## StyledLabel Component
A single component for all styled text. Replaces the old `TitleLabel`, `BodyLabel`, `CaptionLabel`. A single component for all styled text:
```swift ```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 ### Examples
```swift ```swift
// Simple usage // Simple - uses registered theme's primary color
StyledLabel("Morning Ritual", .heading) StyledLabel("Morning Ritual", .heading)
StyledLabel("Day 6 of 28", .caption, emphasis: .secondary) StyledLabel("Day 6 of 28", .caption, emphasis: .secondary)
// With custom theme colors // With alignment and line limit
StyledLabel("Title", .heading, emphasis: .custom(AppTextColors.primary)) StyledLabel("Centered text", .body, alignment: .center)
StyledLabel("Subtitle", .subheading, emphasis: .custom(AppTextColors.secondary)) 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 ```swift
IconLabel("bell.fill", "Notifications", .subheading) IconLabel("bell.fill", "Notifications", .subheading)
IconLabel("star.fill", "Favorites", .body, emphasis: .secondary) 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 (Preferred)
Use `StyledLabel` for the vast majority of text: Use `StyledLabel` for 95% of text:
```swift ```swift
// Simple text
StyledLabel("Settings", .subheadingEmphasis) StyledLabel("Settings", .subheadingEmphasis)
StyledLabel("Description", .subheading, emphasis: .secondary) StyledLabel("Description", .subheading, emphasis: .secondary)
StyledLabel("Centered", .body, alignment: .center)
// 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)
}
``` ```
### Use Text() with .typography() (Rare Exceptions) ### 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 ```swift
// Font design variants (rounded, etc.) // Font design variants
Text(formattedValue) Text(formattedValue)
.typography(.subheadingEmphasis) .typography(.subheadingEmphasis)
.fontDesign(.rounded) .fontDesign(.rounded)
@ -173,24 +202,18 @@ TextField("Placeholder", text: $value)
// Inside special view builders (like Charts AxisValueLabel) // Inside special view builders (like Charts AxisValueLabel)
AxisValueLabel { AxisValueLabel {
Text("\(value)%") Text("\(value)%")
.font(Typography.caption2.font) // .typography() won't work here .font(Typography.caption2.font)
.foregroundStyle(color) .foregroundStyle(color)
} }
``` ```
### Never Use Raw `.font()` with System Fonts ### Never Use Raw `.font()` with System Fonts
Instead of:
```swift ```swift
// BAD // ❌ BAD
Text("Title").font(.headline) Text("Title").font(.headline)
```
Use: // ✅ GOOD
```swift
// GOOD
StyledLabel("Title", .heading) StyledLabel("Title", .heading)
``` ```
@ -203,23 +226,17 @@ StyledLabel("Title", .heading)
```swift ```swift
// OLD: Design.Typography // OLD: Design.Typography
Text("Title").font(Design.Typography.headline) Text("Title").font(Design.Typography.headline)
Text("Body").font(Design.Typography.body)
Text("Caption").font(Design.Typography.caption)
// NEW: Typography enum // NEW: StyledLabel
Text("Title").typography(.heading) StyledLabel("Title", .heading)
Text("Body").typography(.subheading)
Text("Caption").typography(.caption)
// OLD: Multiple Label components // OLD: Multiple Label components
TitleLabel("Title", style: .headline, color: .white) TitleLabel("Title", style: .headline, color: .white)
BodyLabel("Body", emphasis: .secondary) BodyLabel("Body", emphasis: .secondary)
CaptionLabel("Caption")
// NEW: Single StyledLabel // NEW: Single StyledLabel
StyledLabel("Title", .heading, emphasis: .custom(.white)) StyledLabel("Title", .heading, emphasis: .inverse)
StyledLabel("Body", .subheading, emphasis: .secondary) StyledLabel("Body", .subheading, emphasis: .secondary)
StyledLabel("Caption", .caption, emphasis: .secondary)
``` ```
### Mapping Table ### Mapping Table
@ -237,8 +254,6 @@ StyledLabel("Caption", .caption, emphasis: .secondary)
| `.caption` | `.caption` | | `.caption` | `.caption` |
| `.captionMedium` | `.captionEmphasis` | | `.captionMedium` | `.captionEmphasis` |
| `.captionSmall` | `.caption2` | | `.captionSmall` | `.caption2` |
| `.settingsTitle` | `.subheadingEmphasis` |
| `.sectionHeader` | `.captionEmphasis` |
--- ---

View File

@ -2,25 +2,36 @@
// TextEmphasis.swift // TextEmphasis.swift
// Bedrock // Bedrock
// //
// Semantic text color emphasis levels that work with any TextColorProvider theme. // Semantic text color emphasis levels.
// //
import SwiftUI import SwiftUI
// MARK: - Text Emphasis
/// Semantic text color emphasis levels. /// Semantic text color emphasis levels.
/// ///
/// Use `TextEmphasis` to specify text color semantically rather than with explicit colors. /// Use `TextEmphasis` to specify text color semantically. Colors are resolved
/// The actual color is resolved from the app's registered `TextColorProvider`. /// from the globally registered `Theme`.
/// ///
/// ## Example /// ## Example
/// ///
/// ```swift /// ```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("Secondary text", .caption, emphasis: .secondary)
/// StyledLabel("Custom color", .heading, emphasis: .custom(.red)) /// StyledLabel("Custom color", .heading, emphasis: .custom(.red))
/// ``` /// ```
public enum TextEmphasis: Sendable { public enum TextEmphasis: Sendable {
/// Primary text color (highest emphasis). /// Primary text color (highest emphasis). Default.
case primary case primary
/// Secondary text color (medium emphasis). /// Secondary text color (medium emphasis).
case secondary case secondary
@ -30,31 +41,24 @@ public enum TextEmphasis: Sendable {
case disabled case disabled
/// Inverse text color (for contrasting backgrounds). /// Inverse text color (for contrasting backgrounds).
case inverse case inverse
/// Custom color override. /// Custom color override for one-off cases.
case custom(Color) case custom(Color)
/// Resolves the color from a specific TextColorProvider. /// Resolves the color from the globally registered Theme.
/// public var color: Color {
/// Use this when you need to explicitly specify which theme to use:
/// ```swift
/// emphasis.color(from: MyAppTextColors.self)
/// ```
public func color<T: TextColorProvider>(from provider: T.Type) -> Color {
switch self { switch self {
case .primary: provider.primary case .primary: Theme.Text.primary
case .secondary: provider.secondary case .secondary: Theme.Text.secondary
case .tertiary: provider.tertiary case .tertiary: Theme.Text.tertiary
case .disabled: provider.disabled case .disabled: Theme.Text.disabled
case .inverse: provider.inverse case .inverse: Theme.Text.inverse
case .custom(let color): color 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

View File

@ -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<T: TextColorProvider, S: SurfaceColorProvider, A: AccentColorProvider, St: StatusColorProvider>(
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<B: BorderColorProvider>(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