Compare commits

...

4 Commits

18 changed files with 650 additions and 624 deletions

View File

@ -263,72 +263,6 @@ public enum Design {
public static let badge: CGFloat = IconSize.small
}
// MARK: - Typography
/// Semantic font styles for consistent text appearance.
///
/// Use these semantic styles instead of inline `.font()` modifiers.
/// All fonts use SwiftUI system fonts that scale with Dynamic Type.
///
/// ## Example
///
/// ```swift
/// Text("Settings")
/// .font(Design.Typography.settingsTitle)
/// ```
public enum Typography {
// MARK: - Display
/// Large display text for splash screens and heroes.
public static let displayLarge = Font.largeTitle.weight(.bold)
/// Medium display text for major headings.
public static let displayMedium = Font.title.weight(.bold)
/// Small display text for section titles.
public static let displaySmall = Font.title2.weight(.bold)
// MARK: - Headlines
/// Standard headline for card titles and section headers.
public static let headline = Font.headline
/// Bold headline for emphasized titles.
public static let headlineBold = Font.headline.weight(.bold)
// MARK: - Body
/// Large body text.
public static let bodyLarge = Font.body
/// Standard body text for most content.
public static let body = Font.subheadline
/// Medium weight body text for row titles.
public static let bodyMedium = Font.subheadline.weight(.medium)
/// Bold body text for emphasis.
public static let bodyBold = Font.subheadline.weight(.bold)
// MARK: - Caption
/// Standard caption for metadata and descriptions.
public static let caption = Font.caption
/// Medium weight caption for labels.
public static let captionMedium = Font.caption.weight(.medium)
/// Bold caption for section headers and badges.
public static let captionBold = Font.caption.weight(.semibold)
/// Small caption for fine print.
public static let captionSmall = Font.caption2
// MARK: - Semantic Styles
/// Settings row title text.
public static let settingsTitle = Font.subheadline.weight(.medium)
/// Uppercase section header text.
public static let sectionHeader = Font.caption.weight(.semibold)
/// Badge and pill text with rounded design.
public static let badge = Font.subheadline.weight(.bold)
/// Callout text for emphasized rows.
public static let callout = Font.callout
/// Semibold callout for selectable row titles.
public static let calloutBold = Font.callout.weight(.semibold)
}
// MARK: - Scale
/// Scale factors for pressed states, selections, and animations.

View File

@ -1,378 +1,270 @@
# Typography and Iconography Guide
# Typography System Guide
This guide documents the typography and iconography systems in Bedrock for building consistent UI components.
This guide documents the typography and text styling system in Bedrock.
## Quick Reference
## Overview
| What you need | Use this |
|--------------|----------|
| Font for text | `Design.Typography.*` |
| Icon sizing | `SymbolIcon` component or `Design.SymbolSize.*` |
| Icon container background | `Design.Size.iconContainerSmall/Medium/Large` |
| Avatar/profile image | `Design.Size.avatarSmall/Medium/Large` |
| Badge container | `Design.Size.badgeSmall/Medium/Large` |
The typography system consists of:
**Deprecated:** `Design.IconSize.*` - use `SymbolSize` instead (kept for backwards compatibility).
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
---
## Typography
## App Setup
All fonts are defined in `Design.Typography` and scale automatically with Dynamic Type.
### Semantic Font Styles
Use these semantic styles instead of inline `.font()` modifiers:
| Style | Usage | SwiftUI Equivalent |
|-------|-------|-------------------|
| `displayLarge` | Splash screens, heroes | `.largeTitle.weight(.bold)` |
| `displayMedium` | Major headings | `.title.weight(.bold)` |
| `displaySmall` | Section titles | `.title2.weight(.bold)` |
| `headline` | Card titles, section headers | `.headline` |
| `headlineBold` | Emphasized titles | `.headline.weight(.bold)` |
| `bodyLarge` | Large body text | `.body` |
| `body` | Standard content | `.subheadline` |
| `bodyMedium` | Row titles | `.subheadline.weight(.medium)` |
| `bodyBold` | Emphasized text | `.subheadline.weight(.bold)` |
| `caption` | Metadata, descriptions | `.caption` |
| `captionMedium` | Labels | `.caption.weight(.medium)` |
| `captionBold` | Section headers, badges | `.caption.weight(.semibold)` |
| `captionSmall` | Fine print | `.caption2` |
| `settingsTitle` | Settings row titles | `.subheadline.weight(.medium)` |
| `sectionHeader` | Uppercase section headers | `.caption.weight(.semibold)` |
| `badge` | Badge/pill text | `.subheadline.weight(.bold)` |
| `callout` | Callout text | `.callout` |
| `calloutBold` | Selectable row titles | `.callout.weight(.semibold)` |
### Two Approaches
There are two ways to apply typography. Choose based on your needs:
| Approach | Best For |
|----------|----------|
| `*Label` components | Simple text with default styling |
| `Text().font(Design.Typography.*)` | Complex layouts, additional modifiers |
### Preferred: StyledText Components
**Use these for most cases.** They combine font + color with sensible defaults:
Register your app's theme once at launch:
```swift
// Titles
TitleLabel("Settings", style: .settings)
TitleLabel("Morning Ritual", style: .headline)
TitleLabel("Welcome", style: .displayLarge)
// Body text
BodyLabel("Description text", emphasis: .secondary)
BodyLabel("Important note", emphasis: .primary, weight: .medium)
// Captions
CaptionLabel("Day 3 of 28")
CaptionLabel("PRO", style: .badge)
// Section headers
SectionHeader("General")
SectionHeader("Notifications", icon: "bell.fill")
// In App.init()
Theme.register(
text: AppTextColors.self,
surface: AppSurface.self,
accent: AppAccent.self,
status: AppStatus.self
)
Theme.register(border: AppBorder.self) // Optional
```
### Fallback: Direct Typography Usage
This enables Bedrock components to use your app's colors automatically.
**Use `Design.Typography.*` when you need more control:**
### Using Theme Colors
After registration, use `Theme.*` to access your registered colors:
```swift
// Need .multilineTextAlignment or .lineLimit
Text("A longer description that might wrap")
.font(Design.Typography.body)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.lineLimit(3)
// In Bedrock components (automatic via TextEmphasis)
StyledLabel("Title", .heading) // Uses Theme.Text.primary
// Localized strings with interpolation
Text("Day \(dayNumber) of \(totalDays)")
.font(Design.Typography.caption)
.foregroundStyle(AppTextColors.tertiary)
// Custom colors not in the component
Text("Warning message")
.font(Design.Typography.bodyMedium)
.foregroundStyle(AppStatus.warning)
// Animated text
Text(isExpanded ? "Collapse" : "Expand")
.font(Design.Typography.callout)
.animation(.easeInOut, value: isExpanded)
```
### Migration Examples
```swift
// Before (inconsistent)
Text("Settings")
.font(.subheadline.weight(.medium))
.foregroundStyle(.primary)
// After (using component)
TitleLabel("Settings", style: .settings)
// After (using direct typography, if more control needed)
Text("Settings")
.font(Design.Typography.settingsTitle)
.foregroundStyle(.primary)
// Direct access when needed
let bgColor = Theme.Surface.card
let accentColor = Theme.Accent.primary
let successColor = Theme.Status.success
```
---
## Iconography
## Typography Enum
All SF Symbol sizing is defined in `Design.SymbolSize` with semantic names.
All fonts are defined in the `Typography` enum. Each case name explicitly describes the font:
### Semantic Icon Sizes
- Base name = regular weight (e.g., `.title2` = Font.title2)
- `*Bold` = bold weight (e.g., `.title2Bold` = Font.title2.bold())
- `*Emphasis` = semibold weight (e.g., `.bodyEmphasis` = Font.body.weight(.semibold))
| Size | Points | Usage |
|------|--------|-------|
| `inline` | 16pt | Inline with body text |
| `row` | 22pt | List row icons |
| `rowContainer` | 28pt | Row icons with background |
| `card` | 36pt | Card/button icons |
| `feature` | 48pt | Feature callout icons |
| `section` | 64pt | Section header icons |
| `hero` | 80pt | Empty state/hero icons |
| `chevron` | 12pt | Navigation chevrons |
| `indicator` | 16pt | Status indicators |
| `badge` | 12pt | Badge icons |
### Available Styles
### Using SymbolIcon
Use `SymbolIcon` for consistent SF Symbol styling:
```swift
// Before (inconsistent)
Image(systemName: "star.fill")
.font(.subheadline)
.foregroundStyle(.secondary)
// After (consistent)
SymbolIcon("star.fill", size: .row, color: .secondary)
```
### Size Examples
```swift
// Inline with text
SymbolIcon("checkmark", size: .inline, color: .green)
// Row icon in a list
SymbolIcon("star.fill", size: .row, color: .yellow)
// Card icon
SymbolIcon("sparkles", size: .card, color: .accent)
// Hero/empty state icon
SymbolIcon("moon.stars.fill", size: .hero, color: .accent)
// Navigation chevron
SymbolIcon.chevron() // Convenience method
// Checkmark indicator
SymbolIcon.checkmark(color: .green) // Convenience method
```
### Icon + Container Pattern
For icons with backgrounds, combine `SymbolIcon` with `Design.Size.iconContainer*`:
```swift
SymbolIcon("star.fill", size: .inline, color: .white)
.frame(width: Design.Size.iconContainerSmall, height: Design.Size.iconContainerSmall)
.background(AppAccent.primary.opacity(Design.Opacity.heavy))
.clipShape(.rect(cornerRadius: Design.CornerRadius.xSmall))
```
Container sizes:
- `iconContainerSmall`: 28pt
- `iconContainerMedium`: 40pt
- `iconContainerLarge`: 56pt
| Category | Style | Font |
|----------|-------|------|
| **Hero/Titles** | `.hero` | largeTitle |
| | `.heroBold` | largeTitle.bold() |
| | `.title` | title |
| | `.titleBold` | title.bold() |
| | `.title2` | title2 |
| | `.title2Bold` | title2.bold() |
| | `.title3` | title3 |
| | `.title3Bold` | title3.bold() |
| **Headings** | `.heading` | headline |
| | `.headingEmphasis` | headline.weight(.semibold) |
| | `.headingBold` | headline.bold() |
| | `.subheading` | subheadline |
| | `.subheadingEmphasis` | subheadline.weight(.semibold) |
| **Body** | `.body` | body |
| | `.bodyEmphasis` | body.weight(.semibold) |
| | `.callout` | callout |
| | `.calloutEmphasis` | callout.weight(.semibold) |
| **Micro** | `.footnote` | footnote |
| | `.footnoteEmphasis` | footnote.weight(.semibold) |
| | `.caption` | caption |
| | `.captionEmphasis` | caption.weight(.semibold) |
| | `.caption2` | caption2 |
| | `.caption2Emphasis` | caption2.weight(.semibold) |
---
## When to Use Fallback Approach
## TextEmphasis Enum
The `*Label` components work for ~80% of cases. Use `Text().font(Design.Typography.*)` for these exceptions:
Semantic text colors resolved from the registered `TextTheme`:
### 1. String Interpolation
| Emphasis | Description |
|----------|-------------|
| `.primary` | Main text color (default) |
| `.secondary` | Supporting text |
| `.tertiary` | Subtle/hint text |
| `.disabled` | Disabled state |
| `.inverse` | Contrasting backgrounds |
| `.custom(Color)` | Custom color override |
### When to Use `.custom()`
Only use `.custom()` for colors that aren't text colors:
```swift
// Label components only accept String, not interpolated strings
Text(String(localized: "Arc \(arcNumber) Complete"))
.font(Design.Typography.body)
.foregroundStyle(AppTextColors.secondary)
// ✅ Semantic text colors - use the emphasis value directly
StyledLabel("Title", .heading) // .primary is default
StyledLabel("Subtitle", .subheading, emphasis: .secondary)
StyledLabel("Hint", .caption, emphasis: .tertiary)
// ✅ 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))
```
### 2. Additional Modifiers
---
## StyledLabel Component
A single component for all styled text:
```swift
// Need .multilineTextAlignment(), .lineLimit(), or .frame()
Text(description)
.font(Design.Typography.caption)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.lineLimit(2)
StyledLabel(
_ text: String,
_ typography: Typography = .body,
emphasis: TextEmphasis = .primary,
alignment: TextAlignment? = nil,
lineLimit: Int? = nil
)
```
### 3. Font Design Variants
### Examples
```swift
// Need .fontDesign(.rounded) or similar
// Simple - uses registered theme's primary color
StyledLabel("Morning Ritual", .heading)
StyledLabel("Day 6 of 28", .caption, emphasis: .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)
}
```
---
## IconLabel Component
For icon + text combinations:
```swift
IconLabel(_ icon: String, _ text: String, _ typography: Typography = .body, emphasis: TextEmphasis = .primary)
```
### Examples
```swift
IconLabel("bell.fill", "Notifications", .subheading)
IconLabel("star.fill", "Favorites", .body, emphasis: .secondary)
IconLabel("flame.fill", "5-day streak", .caption, emphasis: .custom(AppStatus.success))
```
---
## When to Use StyledLabel vs Text()
### Use StyledLabel (Preferred)
Use `StyledLabel` for 95% of text:
```swift
StyledLabel("Settings", .subheadingEmphasis)
StyledLabel("Description", .subheading, emphasis: .secondary)
StyledLabel("Centered", .body, alignment: .center)
```
### Use Text() with .typography() (Rare Exceptions)
Only when you need modifiers `StyledLabel` doesn't support:
```swift
// Font design variants
Text(formattedValue)
.font(Design.Typography.settingsTitle)
.typography(.subheadingEmphasis)
.fontDesign(.rounded)
.foregroundStyle(.secondary)
// TextField styling
TextField("Placeholder", text: $value)
.typography(.heading)
// Inside special view builders (like Charts AxisValueLabel)
AxisValueLabel {
Text("\(value)%")
.font(Typography.caption2.font)
.foregroundStyle(color)
}
```
### 4. Conditional Colors
### Never Use Raw `.font()` with System Fonts
```swift
// Color changes based on state
Text(title)
.font(Design.Typography.headline)
.foregroundStyle(isActive ? AppTextColors.primary : AppTextColors.tertiary)
```
// ❌ BAD
Text("Title").font(.headline)
### 5. Button Labels with Layout
```swift
// Need .frame() for button sizing
Text(actionTitle)
.font(Design.Typography.headline)
.foregroundStyle(AppTextColors.primary)
.frame(maxWidth: .infinity)
.frame(height: 50)
```
### 6. Custom Theme Colors (Bedrock)
```swift
// Bedrock uses .white for dark backgrounds, not semantic colors
Text(title)
.font(Design.Typography.settingsTitle)
.foregroundStyle(.white)
// ✅ GOOD
StyledLabel("Title", .heading)
```
---
## Migration Guide
## Migration from Old System
### From Inline Fonts
Replace inline font modifiers with `Design.Typography`:
### Old vs New
```swift
// Old
.font(.subheadline.weight(.medium))
// New
.font(Design.Typography.settingsTitle)
// OLD: Design.Typography
Text("Title").font(Design.Typography.headline)
// Old
.font(.caption.weight(.semibold))
// New
.font(Design.Typography.sectionHeader)
// NEW: StyledLabel
StyledLabel("Title", .heading)
// Old
.font(.headline)
// New
.font(Design.Typography.headline)
// OLD: Multiple Label components
TitleLabel("Title", style: .headline, color: .white)
BodyLabel("Body", emphasis: .secondary)
// NEW: Single StyledLabel
StyledLabel("Title", .heading, emphasis: .inverse)
StyledLabel("Body", .subheading, emphasis: .secondary)
```
### From Inline Icons
### Mapping Table
Replace inline Image styling with `SymbolIcon`:
| Old (Design.Typography) | New (Typography) |
|------------------------|------------------|
| `.displayLarge` | `.heroBold` |
| `.displayMedium` | `.titleBold` |
| `.displaySmall` | `.title2Bold` |
| `.headline` | `.heading` |
| `.headlineBold` | `.headingBold` |
| `.body` | `.subheading` |
| `.bodyMedium` | `.subheadingEmphasis` |
| `.bodyLarge` | `.body` |
| `.caption` | `.caption` |
| `.captionMedium` | `.captionEmphasis` |
| `.captionSmall` | `.caption2` |
---
## SymbolIcon (Unchanged)
The `SymbolIcon` component remains the same for consistent icon styling:
```swift
// Old
Image(systemName: "star.fill")
.font(.body)
.foregroundStyle(.accent)
// New
SymbolIcon("star.fill", size: .row, color: .accent)
```
### From Hero Icons
Replace hero icon patterns with semantic sizes:
```swift
// Old
Image(systemName: "sparkles")
.font(.system(size: Design.IconSize.hero))
.foregroundStyle(AppAccent.primary)
// New
SymbolIcon("sparkles", size: .hero, color: AppAccent.primary)
SymbolIcon.chevron()
```
---
## Best Practices
1. **Always use semantic styles** - Use `Design.Typography.settingsTitle` instead of `.subheadline.weight(.medium)`
2. **Consistent icon sizing** - Use `SymbolIcon` sizes that match the context (`.row` for lists, `.hero` for empty states)
3. **Preserve Dynamic Type** - All typography scales automatically; avoid fixed sizes
4. **Color consistency** - Pair typography with semantic colors (`AppTextColors.primary`, `.secondary`, `.tertiary`)
5. **Container sizing** - Use `Design.Size.iconContainer*` for icon backgrounds, `Design.SymbolSize.*` for the icon itself
6. **Audit regularly** - Search for inline `.font()` and `Image(systemName:)` to find inconsistencies
---
## What NOT to Use
These patterns are deprecated or should be avoided:
| Avoid | Use Instead |
|-------|-------------|
| `Design.IconSize.*` | `SymbolIcon` or `Design.SymbolSize.*` |
| `.font(.subheadline.weight(.medium))` | `Design.Typography.settingsTitle` |
| `.font(.system(size: 80))` | `SymbolIcon("icon", size: .hero)` |
| Hardcoded frame sizes like `44x44` | `Design.Size.actionRowMinHeight` or `Design.Size.iconContainer*` |
---
## Size Reference
### Design.Size (UI Elements)
| Constant | Value | Use For |
|----------|-------|---------|
| `actionRowMinHeight` | 44pt | Minimum tappable row height |
| `buttonHeight` | 50pt | Standard button height |
| `iconContainerSmall` | 28pt | Small icon backgrounds |
| `iconContainerMedium` | 40pt | Medium icon backgrounds |
| `iconContainerLarge` | 56pt | Large icon backgrounds |
| `avatarSmall` | 32pt | Small profile images |
| `avatarMedium` | 44pt | Medium profile images |
| `avatarLarge` | 64pt | Large profile images |
| `badgeSmall` | 20pt | Small badge containers |
| `badgeMedium` | 26pt | Medium badge containers |
| `badgeLarge` | 32pt | Large badge containers |
### Design.SymbolSize (Icon Point Sizes)
| Constant | Value | Use For |
|----------|-------|---------|
| `inline` | 16pt | Inline with body text |
| `row` | 22pt | List row icons |
| `rowContainer` | 28pt | Row icons with background |
| `card` | 36pt | Card/button icons |
| `feature` | 48pt | Feature callouts |
| `section` | 64pt | Section headers |
| `hero` | 80pt | Empty states |
| `chevron` | 12pt | Navigation chevrons |
| `indicator` | 16pt | Status indicators |
| `badge` | 12pt | Badge icons |
See the existing documentation for `SymbolIcon` sizes and usage.

View File

@ -0,0 +1,64 @@
//
// TextEmphasis.swift
// Bedrock
//
// Semantic text color emphasis levels.
//
import SwiftUI
// MARK: - Text Emphasis
/// Semantic text color emphasis levels.
///
/// Use `TextEmphasis` to specify text color semantically. Colors are resolved
/// from the globally registered `Theme`.
///
/// ## Example
///
/// ```swift
/// // 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). Default.
case primary
/// Secondary text color (medium emphasis).
case secondary
/// Tertiary text color (low emphasis).
case tertiary
/// Disabled text color.
case disabled
/// Inverse text color (for contrasting backgrounds).
case inverse
/// Custom color override for one-off cases.
case custom(Color)
/// Resolves the color from the globally registered Theme.
public var color: Color {
switch self {
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
}
}
}
// 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

View File

@ -0,0 +1,138 @@
//
// Typography.swift
// Bedrock
//
// Semantic typography system with explicit, self-describing font names.
//
import SwiftUI
/// Semantic typography with explicit naming.
///
/// Each case name describes exactly what font you get:
/// - Base name = regular weight (e.g., `.title2` = Font.title2)
/// - `*Bold` = bold weight (e.g., `.title2Bold` = Font.title2.bold())
/// - `*Emphasis` = semibold weight (e.g., `.bodyEmphasis` = Font.body.weight(.semibold))
///
/// ## Example
///
/// ```swift
/// Text("Hello")
/// .font(Typography.heading.font)
///
/// // Or with the View extension:
/// Text("Hello")
/// .typography(.heading)
/// ```
public enum Typography: CaseIterable, Sendable {
// MARK: - Hero / Large Titles
/// Large title (regular weight).
case hero
/// Large title (bold weight).
case heroBold
/// Title (regular weight).
case title
/// Title (bold weight).
case titleBold
/// Title 2 (regular weight).
case title2
/// Title 2 (bold weight).
case title2Bold
/// Title 3 (regular weight).
case title3
/// Title 3 (bold weight).
case title3Bold
// MARK: - Headings
/// Headline (regular weight).
case heading
/// Headline (semibold weight).
case headingEmphasis
/// Headline (bold weight).
case headingBold
/// Subheadline (regular weight).
case subheading
/// Subheadline (semibold weight).
case subheadingEmphasis
// MARK: - Body
/// Body text (regular weight).
case body
/// Body text (semibold weight).
case bodyEmphasis
/// Callout text (regular weight).
case callout
/// Callout text (semibold weight).
case calloutEmphasis
// MARK: - Micro
/// Footnote text (regular weight).
case footnote
/// Footnote text (semibold weight).
case footnoteEmphasis
/// Caption text (regular weight).
case caption
/// Caption text (semibold weight).
case captionEmphasis
/// Caption 2 text (regular weight).
case caption2
/// Caption 2 text (semibold weight).
case caption2Emphasis
// MARK: - Font
/// The SwiftUI Font for this typography style.
public var font: Font {
switch self {
case .hero: .largeTitle
case .heroBold: .largeTitle.bold()
case .title: .title
case .titleBold: .title.bold()
case .title2: .title2
case .title2Bold: .title2.bold()
case .title3: .title3
case .title3Bold: .title3.bold()
case .heading: .headline
case .headingEmphasis: .headline.weight(.semibold)
case .headingBold: .headline.bold()
case .subheading: .subheadline
case .subheadingEmphasis: .subheadline.weight(.semibold)
case .body: .body
case .bodyEmphasis: .body.weight(.semibold)
case .callout: .callout
case .calloutEmphasis: .callout.weight(.semibold)
case .footnote: .footnote
case .footnoteEmphasis: .footnote.weight(.semibold)
case .caption: .caption
case .captionEmphasis: .caption.weight(.semibold)
case .caption2: .caption2
case .caption2Emphasis: .caption2.weight(.semibold)
}
}
}
// MARK: - View Extension
extension View {
/// Applies a typography style to the view.
///
/// ```swift
/// Text("Hello")
/// .typography(.heading)
/// ```
public func typography(_ style: Typography) -> some View {
self.font(style.font)
}
}

View File

@ -73,9 +73,7 @@ public extension View {
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
.pulsing(isActive: true)
Text("Pulsing highlights interactive areas")
.foregroundStyle(Color.TextColors.secondary)
.font(.caption)
StyledLabel("Pulsing highlights interactive areas", .caption, emphasis: .secondary)
}
}
}

View File

@ -37,7 +37,7 @@ public struct BadgePill: View {
public var body: some View {
Text(text)
.font(Design.Typography.badge)
.typography(.subheadingEmphasis)
.fontDesign(.rounded)
.foregroundStyle(isSelected ? .black : accentColor)
.padding(.horizontal, Design.Spacing.small)

View File

@ -75,26 +75,18 @@ public struct LicensesView: View {
private func licenseCard(_ license: License) -> some View {
SettingsCard(backgroundColor: cardBackgroundColor, borderColor: cardBorderColor) {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(license.name)
.font(Design.Typography.bodyBold)
.foregroundStyle(.white)
StyledLabel(license.name, .bodyEmphasis, emphasis: .inverse)
Text(license.description)
.font(Design.Typography.caption)
.foregroundStyle(.white.opacity(Design.Opacity.strong))
StyledLabel(license.description, .caption, emphasis: .secondary)
HStack {
Label(license.licenseType, systemImage: "doc.text")
.font(Design.Typography.captionSmall)
.foregroundStyle(accentColor)
IconLabel("doc.text", license.licenseType, .caption2, emphasis: .custom(accentColor))
Spacer()
if let linkURL = URL(string: license.url) {
Link(destination: linkURL) {
Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square")
.font(Design.Typography.captionSmall)
.foregroundStyle(accentColor)
IconLabel("arrow.up.right.square", String(localized: "View on GitHub"), .caption2, emphasis: .custom(accentColor))
}
}
}

View File

@ -51,9 +51,7 @@ public struct SegmentedPicker<T: Equatable>: View {
public var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(title)
.font(Design.Typography.settingsTitle)
.foregroundStyle(.white)
StyledLabel(title, .subheadingEmphasis, emphasis: .inverse)
HStack(spacing: Design.Spacing.small) {
ForEach(options.indices, id: \.self) { index in
@ -61,9 +59,7 @@ public struct SegmentedPicker<T: Equatable>: View {
Button {
selection = option.1
} label: {
Text(option.0)
.font(Design.Typography.settingsTitle)
.foregroundStyle(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong))
StyledLabel(option.0, .subheadingEmphasis, emphasis: .custom(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong)))
.padding(.vertical, Design.Spacing.small)
.frame(maxWidth: .infinity)
.background(

View File

@ -67,13 +67,9 @@ public struct SelectableRow<Badge: View>: View {
Button(action: action) {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title)
.font(Design.Typography.calloutBold)
.foregroundStyle(.white)
StyledLabel(title, .calloutEmphasis, emphasis: .inverse)
Text(subtitle)
.font(Design.Typography.body)
.foregroundStyle(.white.opacity(Design.Opacity.medium))
StyledLabel(subtitle, .subheading, emphasis: .tertiary)
}
Spacer()

View File

@ -67,10 +67,10 @@ public struct SettingsNavigationRow<Destination: View>: View {
} label: {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
TitleLabel(title, style: .settings)
StyledLabel(title, .subheadingEmphasis)
if let subtitle {
CaptionLabel(subtitle)
StyledLabel(subtitle, .caption, emphasis: .secondary)
}
}

View File

@ -59,12 +59,12 @@ public struct SettingsRow<Accessory: View>: View {
.background(iconColor.opacity(Design.Opacity.heavy))
.clipShape(.rect(cornerRadius: Design.CornerRadius.xSmall))
BodyLabel(title)
StyledLabel(title, .subheading)
Spacer()
if let value {
BodyLabel(value, emphasis: .secondary)
StyledLabel(value, .subheading, emphasis: .secondary)
}
if let accessory {

View File

@ -40,8 +40,8 @@ public struct SettingsSectionHeader: View {
}
Text(title)
.font(Design.Typography.sectionHeader)
.foregroundStyle(.secondary)
.font(Typography.captionEmphasis.font)
.foregroundStyle(Theme.Text.secondary)
.textCase(.uppercase)
.tracking(0.5)

View File

@ -79,16 +79,16 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
// Title row with optional accessory
HStack(spacing: Design.Spacing.xSmall) {
TitleLabel(title, style: .settings)
StyledLabel(title, .subheadingEmphasis)
if let titleAccessory {
titleAccessory
.font(Design.Typography.captionSmall)
.font(Typography.caption2.font)
}
}
// Subtitle
CaptionLabel(subtitle)
StyledLabel(subtitle, .caption, emphasis: .secondary)
// Segmented buttons
HStack(spacing: Design.Spacing.small) {
@ -97,9 +97,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
Button {
selection = option.1
} label: {
Text(option.0)
.font(Design.Typography.settingsTitle)
.foregroundStyle(selection == option.1 ? Color.white : .primary)
StyledLabel(option.0, .subheadingEmphasis, emphasis: .custom(selection == option.1 ? Color.white : Theme.Text.primary))
.padding(.vertical, Design.Spacing.small)
.frame(maxWidth: .infinity)
.background(

View File

@ -110,18 +110,18 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
public var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
HStack {
TitleLabel(title, style: .settings)
StyledLabel(title, .subheadingEmphasis)
Spacer()
// Exception: Needs .fontDesign(.rounded) modifier
Text(format(value))
.font(Design.Typography.settingsTitle)
.font(Typography.subheadingEmphasis.font)
.fontDesign(.rounded)
.foregroundStyle(.secondary)
}
CaptionLabel(subtitle)
StyledLabel(subtitle, .caption, emphasis: .secondary)
HStack(spacing: Design.Spacing.medium) {
if let leadingIcon {

View File

@ -78,15 +78,15 @@ public struct SettingsToggle<Accessory: View>: View {
Toggle(isOn: $isOn) {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
HStack(spacing: Design.Spacing.xSmall) {
TitleLabel(title, style: .settings)
StyledLabel(title, .subheadingEmphasis)
if let titleAccessory {
titleAccessory
.font(Design.Typography.captionSmall)
.font(Typography.caption2.font)
}
}
BodyLabel(subtitle, emphasis: .secondary)
StyledLabel(subtitle, .subheading, emphasis: .secondary)
}
}
.tint(accentColor)

View File

@ -117,18 +117,14 @@ public struct iCloudSyncSettingsView<ViewModel: CloudSyncable>: View {
HStack(spacing: Design.Spacing.small) {
SymbolIcon(syncStatusIcon, size: .inline, color: syncStatusColor)
Text(syncStatusText)
.font(Design.Typography.caption)
.foregroundStyle(.white.opacity(Design.Opacity.medium))
StyledLabel(syncStatusText, .caption, emphasis: .tertiary)
Spacer()
Button {
viewModel.forceSync()
} label: {
Text(String(localized: "Sync Now"))
.font(Design.Typography.captionMedium)
.foregroundStyle(accentColor)
StyledLabel(String(localized: "Sync Now"), .captionEmphasis, emphasis: .custom(accentColor))
}
}
.padding(.top, Design.Spacing.xSmall)

View File

@ -7,209 +7,103 @@
import SwiftUI
// MARK: - Title Label
// MARK: - Styled Label
/// A title label with semantic styling.
/// A text label with semantic typography and color.
///
/// Use `TitleLabel` for card titles, section headers, and row titles
/// to ensure consistent typography across your app.
/// Use `StyledLabel` for all text that follows the typography system.
/// It combines a `Typography` style with a `TextEmphasis` color.
///
/// ## Example
///
/// ```swift
/// TitleLabel("Settings", style: .settings)
/// TitleLabel("Morning Ritual", style: .card)
/// StyledLabel("Morning Ritual", .heading)
/// StyledLabel("Day 6 of 28", .caption, emphasis: .secondary)
/// StyledLabel("Warning", .bodyEmphasis, emphasis: .custom(.red))
/// ```
public struct TitleLabel: View {
public struct StyledLabel: View {
private let text: String
private let style: Style
private let color: Color?
private let typography: Typography
private let emphasis: TextEmphasis
private let alignment: TextAlignment?
private let lineLimit: Int?
/// Title label styles.
public enum Style {
/// Large display title for splash screens.
case displayLarge
/// Medium display title for major headings.
case displayMedium
/// Small display title for section titles.
case displaySmall
/// Standard headline for cards and sections.
case headline
/// Settings row title.
case settings
/// Callout style for selectable rows.
case callout
var font: Font {
switch self {
case .displayLarge: Design.Typography.displayLarge
case .displayMedium: Design.Typography.displayMedium
case .displaySmall: Design.Typography.displaySmall
case .headline: Design.Typography.headline
case .settings: Design.Typography.settingsTitle
case .callout: Design.Typography.calloutBold
}
}
}
/// Creates a title label.
/// Creates a styled label.
/// - Parameters:
/// - text: The text to display.
/// - style: The title style (default: `.headline`).
/// - color: Optional color override (default: `.primary`).
public init(_ text: String, style: Style = .headline, color: Color? = nil) {
self.text = text
self.style = style
self.color = color
}
public var body: some View {
Text(text)
.font(style.font)
.foregroundStyle(color ?? .primary)
}
}
// MARK: - Body Label
/// A body text label with emphasis levels.
///
/// Use `BodyLabel` for descriptive text, row subtitles, and content
/// to ensure consistent typography across your app.
///
/// ## Example
///
/// ```swift
/// BodyLabel("Your daily rituals", emphasis: .secondary)
/// BodyLabel("Important note", emphasis: .primary, weight: .medium)
/// BodyLabel("Custom color", color: AppTextColors.primary)
/// ```
public struct BodyLabel: View {
private let text: String
private let emphasis: Emphasis
private let weight: Weight
private let color: Color?
/// Body text emphasis levels.
public enum Emphasis {
/// Primary text color.
case primary
/// Secondary text color.
case secondary
var color: Color {
switch self {
case .primary: .primary
case .secondary: .secondary
}
}
}
/// Body text weight.
public enum Weight {
case regular
case medium
case bold
var font: Font {
switch self {
case .regular: Design.Typography.body
case .medium: Design.Typography.bodyMedium
case .bold: Design.Typography.bodyBold
}
}
}
/// Creates a body label.
/// - Parameters:
/// - text: The text to display.
/// - emphasis: The emphasis level (default: `.primary`).
/// - weight: The font weight (default: `.regular`)one /// - color: Optional color override (takes precedence over emphasis).
public init(_ text: String, emphasis: Emphasis = .primary, weight: Weight = .regular, color: Color? = nil) {
/// - typography: The typography style (default: `.body`).
/// - emphasis: The text emphasis/color (default: `.primary`).
/// - alignment: Optional multiline text alignment.
/// - lineLimit: Optional line limit.
public init(
_ text: String,
_ typography: Typography = .body,
emphasis: TextEmphasis = .primary,
alignment: TextAlignment? = nil,
lineLimit: Int? = nil
) {
self.text = text
self.typography = typography
self.emphasis = emphasis
self.weight = weight
self.color = color
self.alignment = alignment
self.lineLimit = lineLimit
}
public var body: some View {
Text(text)
.font(weight.font)
.foregroundStyle(color ?? emphasis.color)
.font(typography.font)
.foregroundStyle(emphasis.color)
.multilineTextAlignment(alignment ?? .leading)
.lineLimit(lineLimit)
}
}
// MARK: - Caption Label
// MARK: - Icon Label
/// A caption label for metadata and small text.
/// A label with an icon and text, styled with semantic typography.
///
/// Use `CaptionLabel` for timestamps, metadata, badges, and fine print
/// to ensure consistent typography across your app.
/// Use `IconLabel` for icon + text combinations like menu items or list rows.
///
/// ## Example
///
/// ```swift
/// CaptionLabel("Day 6 of 28", style: .badge)
/// CaptionLabel("Last updated 2 hours ago", style: .standard)
/// IconLabel("bell.fill", "Notifications", .subheading)
/// IconLabel("star.fill", "Favorites", .body, emphasis: .secondary)
/// ```
public struct CaptionLabel: View {
public struct IconLabel: View {
private let icon: String
private let text: String
private let style: Style
private let color: Color?
private let typography: Typography
private let emphasis: TextEmphasis
/// Caption label styles.
public enum Style {
/// Standard caption for metadata.
case standard
/// Medium weight caption for labels.
case medium
/// Bold caption for section headers.
case bold
/// Small caption for fine print.
case small
/// Badge text with bold weight.
case badge
/// Uppercase section header.
case sectionHeader
var font: Font {
switch self {
case .standard: Design.Typography.caption
case .medium: Design.Typography.captionMedium
case .bold: Design.Typography.captionBold
case .small: Design.Typography.captionSmall
case .badge: Design.Typography.badge
case .sectionHeader: Design.Typography.sectionHeader
}
}
var isUppercase: Bool {
self == .sectionHeader
}
}
/// Creates a caption label.
/// Creates an icon label.
/// - Parameters:
/// - icon: The SF Symbol name.
/// - text: The text to display.
/// - style: The caption style (default: `.standard`).
/// - color: Optional color override (default: `.secondary`).
public init(_ text: String, style: Style = .standard, color: Color? = nil) {
/// - typography: The typography style (default: `.body`).
/// - emphasis: The text emphasis/color (default: `.primary`).
public init(
_ icon: String,
_ text: String,
_ typography: Typography = .body,
emphasis: TextEmphasis = .primary
) {
self.icon = icon
self.text = text
self.style = style
self.color = color
self.typography = typography
self.emphasis = emphasis
}
public var body: some View {
Text(text)
.font(style.font)
.foregroundStyle(color ?? .secondary)
.textCase(style.isUppercase ? .uppercase : nil)
Label(text, systemImage: icon)
.font(typography.font)
.foregroundStyle(emphasis.color)
}
}
// MARK: - Section Header
/// A section header label with uppercase styling.
/// A section header with uppercase styling.
///
/// Use `SectionHeader` for grouped list section titles
/// to match iOS Settings app conventions.
@ -217,7 +111,7 @@ public struct CaptionLabel: View {
/// ## Example
///
/// ```swift
/// SectionHeader("GENERAL")
/// SectionHeader("General")
/// SectionHeader("Notifications", icon: "bell.fill")
/// ```
public struct SectionHeader: View {
@ -243,47 +137,51 @@ public struct SectionHeader: View {
}
Text(title)
.font(Design.Typography.sectionHeader)
.foregroundStyle(.secondary)
.font(Typography.caption.font)
.foregroundStyle(Theme.Text.secondary)
.textCase(.uppercase)
.tracking(0.5)
}
}
}
// MARK: - Preview
// MARK: - Previews
#Preview("Title Labels") {
#Preview("Styled Labels") {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
TitleLabel("Display Large", style: .displayLarge)
TitleLabel("Display Medium", style: .displayMedium)
TitleLabel("Display Small", style: .displaySmall)
TitleLabel("Headline", style: .headline)
TitleLabel("Settings Title", style: .settings)
TitleLabel("Callout", style: .callout)
Group {
StyledLabel("Hero Bold", .heroBold)
StyledLabel("Title Bold", .titleBold)
StyledLabel("Title 2", .title2)
StyledLabel("Heading", .heading)
StyledLabel("Heading Emphasis", .headingEmphasis)
}
Divider()
Group {
StyledLabel("Body", .body)
StyledLabel("Body Emphasis", .bodyEmphasis)
StyledLabel("Subheading", .subheading, emphasis: .secondary)
StyledLabel("Caption", .caption, emphasis: .secondary)
StyledLabel("Caption 2", .caption2, emphasis: .tertiary)
}
Divider()
Group {
StyledLabel("Custom Color", .body, emphasis: .custom(.orange))
StyledLabel("Disabled", .body, emphasis: .disabled)
}
}
.padding()
}
#Preview("Body Labels") {
#Preview("Icon Labels") {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
BodyLabel("Primary body text")
BodyLabel("Secondary body text", emphasis: .secondary)
BodyLabel("Custom color body", color: .gray)
BodyLabel("Medium weight body", weight: .medium)
BodyLabel("Bold body text", weight: .bold)
}
.padding()
}
#Preview("Caption Labels") {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
CaptionLabel("Standard caption")
CaptionLabel("Medium caption", style: .medium)
CaptionLabel("Bold caption", style: .bold)
CaptionLabel("Small caption", style: .small)
CaptionLabel("Badge text", style: .badge)
CaptionLabel("Section Header", style: .sectionHeader)
IconLabel("bell.fill", "Notifications", .subheading)
IconLabel("star.fill", "Favorites", .body, emphasis: .secondary)
IconLabel("gear", "Settings", .caption)
}
.padding()
}