Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
e998661ce1
commit
d8b45af52f
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
60
Sources/Bedrock/Theme/TextEmphasis.swift
Normal file
60
Sources/Bedrock/Theme/TextEmphasis.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
138
Sources/Bedrock/Theme/Typography.swift
Normal file
138
Sources/Bedrock/Theme/Typography.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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))
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -40,7 +40,7 @@ public struct SettingsSectionHeader: View {
|
||||
}
|
||||
|
||||
Text(title)
|
||||
.font(Design.Typography.sectionHeader)
|
||||
.typography(.captionEmphasis)
|
||||
.foregroundStyle(.secondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user