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:
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` |
---

View File

@ -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<T: TextColorProvider>(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

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