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

This commit is contained in:
Matt Bruce 2026-01-27 10:55:08 -06:00
parent e998661ce1
commit d8b45af52f
16 changed files with 494 additions and 614 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,238 @@
# 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. **`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
---
## Typography
## Typography Enum
All fonts are defined in `Design.Typography` and scale automatically with Dynamic Type.
All fonts are defined in the `Typography` enum. Each case name explicitly describes the font:
### Semantic Font Styles
- 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))
Use these semantic styles instead of inline `.font()` modifiers:
### Available Styles
| 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)` |
| 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) |
### 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:
### Usage
```swift
// Titles
TitleLabel("Settings", style: .settings)
TitleLabel("Morning Ritual", style: .headline)
TitleLabel("Welcome", style: .displayLarge)
// Using the .typography() modifier (preferred)
Text("Hello")
.typography(.heading)
// 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")
// Using .font() directly
Text("Hello")
.font(Typography.heading.font)
```
### Fallback: Direct Typography Usage
---
**Use `Design.Typography.*` when you need more control:**
## TextEmphasis Enum
Semantic text colors that work with any theme:
| Emphasis | Description |
|----------|-------------|
| `.primary` | Main text color |
| `.secondary` | Supporting text |
| `.tertiary` | Subtle/hint text |
| `.disabled` | Disabled state |
| `.inverse` | Contrasting backgrounds |
| `.custom(Color)` | Custom color override |
### Usage
```swift
// Need .multilineTextAlignment or .lineLimit
Text("A longer description that might wrap")
.font(Design.Typography.body)
// With StyledLabel
StyledLabel("Title", .heading, emphasis: .primary)
StyledLabel("Subtitle", .subheading, emphasis: .secondary)
StyledLabel("Custom", .body, emphasis: .custom(.red))
// With custom theme colors
StyledLabel("Title", .heading, emphasis: .custom(AppTextColors.primary))
```
---
## StyledLabel Component
A single component for all styled text. Replaces the old `TitleLabel`, `BodyLabel`, `CaptionLabel`.
```swift
StyledLabel(_ text: String, _ typography: Typography = .body, emphasis: TextEmphasis = .primary)
```
### Examples
```swift
// Simple usage
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))
```
---
## 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)
```
---
## When to Use StyledLabel vs Text()
### Use StyledLabel
For simple text with standard styling:
```swift
StyledLabel("Settings", .subheadingEmphasis)
StyledLabel("Description", .subheading, emphasis: .secondary)
```
### Use Text() with .typography()
When you need additional modifiers:
```swift
// Multiline text
Text("Long description that might wrap to multiple lines")
.typography(.subheading)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.lineLimit(3)
// 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)
```
---
## Iconography
All SF Symbol sizing is defined in `Design.SymbolSize` with semantic names.
### Semantic Icon Sizes
| 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 |
### 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
---
## When to Use Fallback Approach
The `*Label` components work for ~80% of cases. Use `Text().font(Design.Typography.*)` for these exceptions:
### 1. String Interpolation
```swift
// Label components only accept String, not interpolated strings
Text(String(localized: "Arc \(arcNumber) Complete"))
.font(Design.Typography.body)
.foregroundStyle(AppTextColors.secondary)
```
### 2. Additional Modifiers
```swift
// Need .multilineTextAlignment(), .lineLimit(), or .frame()
Text(description)
.font(Design.Typography.caption)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
.lineLimit(2)
```
### 3. Font Design Variants
```swift
// Need .fontDesign(.rounded) or similar
Text(formattedValue)
.font(Design.Typography.settingsTitle)
.fontDesign(.rounded)
.foregroundStyle(.secondary)
```
### 4. Conditional Colors
```swift
// Color changes based on state
// Conditional colors
Text(title)
.font(Design.Typography.headline)
.typography(.heading)
.foregroundStyle(isActive ? AppTextColors.primary : AppTextColors.tertiary)
```
### 5. Button Labels with Layout
// With padding/background
Text(dayLabel)
.typography(.caption)
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.small)
.background(AppAccent.light.opacity(0.3))
```swift
// Need .frame() for button sizing
Text(actionTitle)
.font(Design.Typography.headline)
.foregroundStyle(AppTextColors.primary)
// Button labels with frame
Text("Continue")
.typography(.heading)
.foregroundStyle(AppTextColors.inverse)
.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)
// Font design variants
Text(formattedValue)
.typography(.subheadingEmphasis)
.fontDesign(.rounded)
```
---
## 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)
Text("Body").font(Design.Typography.body)
Text("Caption").font(Design.Typography.caption)
// Old
.font(.caption.weight(.semibold))
// New
.font(Design.Typography.sectionHeader)
// NEW: Typography enum
Text("Title").typography(.heading)
Text("Body").typography(.subheading)
Text("Caption").typography(.caption)
// Old
.font(.headline)
// New
.font(Design.Typography.headline)
// 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("Body", .subheading, emphasis: .secondary)
StyledLabel("Caption", .caption, 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` |
| `.settingsTitle` | `.subheadingEmphasis` |
| `.sectionHeader` | `.captionEmphasis` |
---
## 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,60 @@
//
// TextEmphasis.swift
// Bedrock
//
// Semantic text color emphasis levels that work with any TextColorProvider theme.
//
import SwiftUI
/// 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`.
///
/// ## Example
///
/// ```swift
/// StyledLabel("Primary text", .body, emphasis: .primary)
/// StyledLabel("Secondary text", .caption, emphasis: .secondary)
/// StyledLabel("Custom color", .heading, emphasis: .custom(.red))
/// ```
public enum TextEmphasis: Sendable {
/// Primary text color (highest emphasis).
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.
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 {
switch self {
case .primary: provider.primary
case .secondary: provider.secondary
case .tertiary: provider.tertiary
case .disabled: provider.disabled
case .inverse: provider.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)
}
}

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

@ -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

@ -76,16 +76,16 @@ public struct LicensesView: View {
SettingsCard(backgroundColor: cardBackgroundColor, borderColor: cardBorderColor) {
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(license.name)
.font(Design.Typography.bodyBold)
.typography(.bodyEmphasis)
.foregroundStyle(.white)
Text(license.description)
.font(Design.Typography.caption)
.typography(.caption)
.foregroundStyle(.white.opacity(Design.Opacity.strong))
HStack {
Label(license.licenseType, systemImage: "doc.text")
.font(Design.Typography.captionSmall)
.typography(.caption2)
.foregroundStyle(accentColor)
Spacer()
@ -93,7 +93,7 @@ public struct LicensesView: View {
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)
.typography(.caption2)
.foregroundStyle(accentColor)
}
}

View File

@ -52,7 +52,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)
.typography(.subheadingEmphasis)
.foregroundStyle(.white)
HStack(spacing: Design.Spacing.small) {
@ -62,7 +62,7 @@ public struct SegmentedPicker<T: Equatable>: View {
selection = option.1
} label: {
Text(option.0)
.font(Design.Typography.settingsTitle)
.typography(.subheadingEmphasis)
.foregroundStyle(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong))
.padding(.vertical, Design.Spacing.small)
.frame(maxWidth: .infinity)

View File

@ -68,11 +68,11 @@ public struct SelectableRow<Badge: View>: View {
HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text(title)
.font(Design.Typography.calloutBold)
.typography(.calloutEmphasis)
.foregroundStyle(.white)
Text(subtitle)
.font(Design.Typography.body)
.typography(.subheading)
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}

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,7 +40,7 @@ public struct SettingsSectionHeader: View {
}
Text(title)
.font(Design.Typography.sectionHeader)
.typography(.captionEmphasis)
.foregroundStyle(.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) {
@ -98,7 +98,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
selection = option.1
} label: {
Text(option.0)
.font(Design.Typography.settingsTitle)
.typography(.subheadingEmphasis)
.foregroundStyle(selection == option.1 ? Color.white : .primary)
.padding(.vertical, Design.Spacing.small)
.frame(maxWidth: .infinity)

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

@ -118,7 +118,7 @@ public struct iCloudSyncSettingsView<ViewModel: CloudSyncable>: View {
SymbolIcon(syncStatusIcon, size: .inline, color: syncStatusColor)
Text(syncStatusText)
.font(Design.Typography.caption)
.typography(.caption)
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Spacer()
@ -127,7 +127,7 @@ public struct iCloudSyncSettingsView<ViewModel: CloudSyncable>: View {
viewModel.forceSync()
} label: {
Text(String(localized: "Sync Now"))
.font(Design.Typography.captionMedium)
.typography(.captionEmphasis)
.foregroundStyle(accentColor)
}
}

View File

@ -7,209 +7,93 @@
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
/// 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`).
public init(
_ text: String,
_ typography: Typography = .body,
emphasis: TextEmphasis = .primary
) {
self.text = text
self.typography = typography
self.emphasis = emphasis
self.weight = weight
self.color = color
}
public var body: some View {
Text(text)
.font(weight.font)
.foregroundStyle(color ?? emphasis.color)
.font(typography.font)
.foregroundStyle(emphasis.color)
}
}
// 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 +101,7 @@ public struct CaptionLabel: View {
/// ## Example
///
/// ```swift
/// SectionHeader("GENERAL")
/// SectionHeader("General")
/// SectionHeader("Notifications", icon: "bell.fill")
/// ```
public struct SectionHeader: View {
@ -243,47 +127,51 @@ public struct SectionHeader: View {
}
Text(title)
.font(Design.Typography.sectionHeader)
.foregroundStyle(.secondary)
.font(Typography.caption.font)
.foregroundStyle(Color.TextColors.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()
}