Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
113660a7f9
commit
92e6de53c5
@ -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` |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
124
Sources/Bedrock/Theme/Theme.swift
Normal file
124
Sources/Bedrock/Theme/Theme.swift
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user