Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
4d84e2ec14
commit
e998661ce1
@ -153,6 +153,9 @@ public enum Design {
|
|||||||
// MARK: - Sizes
|
// MARK: - Sizes
|
||||||
|
|
||||||
/// Common size values for UI elements.
|
/// Common size values for UI elements.
|
||||||
|
///
|
||||||
|
/// For icon sizes, use `SymbolSize` instead.
|
||||||
|
/// For icon container backgrounds, use `iconContainerSmall/Medium/Large`.
|
||||||
public enum Size {
|
public enum Size {
|
||||||
/// Minimum height for tappable rows (Apple HIG: 44pt minimum touch target).
|
/// Minimum height for tappable rows (Apple HIG: 44pt minimum touch target).
|
||||||
public static let actionRowMinHeight: CGFloat = 44
|
public static let actionRowMinHeight: CGFloat = 44
|
||||||
@ -161,21 +164,18 @@ public enum Design {
|
|||||||
public static let buttonHeight: CGFloat = 50
|
public static let buttonHeight: CGFloat = 50
|
||||||
public static let buttonMinWidth: CGFloat = 80
|
public static let buttonMinWidth: CGFloat = 80
|
||||||
|
|
||||||
/// Checkmark and indicator sizes.
|
/// Badge container sizes (for badge UI elements, not icon sizes).
|
||||||
public static let checkmark: CGFloat = 22
|
|
||||||
public static let indicator: CGFloat = 24
|
|
||||||
|
|
||||||
/// Badge sizes.
|
|
||||||
public static let badgeSmall: CGFloat = 20
|
public static let badgeSmall: CGFloat = 20
|
||||||
public static let badgeMedium: CGFloat = 26
|
public static let badgeMedium: CGFloat = 26
|
||||||
public static let badgeLarge: CGFloat = 32
|
public static let badgeLarge: CGFloat = 32
|
||||||
|
|
||||||
/// Avatar sizes.
|
/// Avatar/profile image sizes (not icons - use for user photos).
|
||||||
public static let avatarSmall: CGFloat = 32
|
public static let avatarSmall: CGFloat = 32
|
||||||
public static let avatarMedium: CGFloat = 44
|
public static let avatarMedium: CGFloat = 44
|
||||||
public static let avatarLarge: CGFloat = 64
|
public static let avatarLarge: CGFloat = 64
|
||||||
|
|
||||||
/// Icon container sizes.
|
/// Icon container sizes (background frames for icons, not the icon itself).
|
||||||
|
/// Use with `SymbolSize` for the icon inside.
|
||||||
public static let iconContainerSmall: CGFloat = 28
|
public static let iconContainerSmall: CGFloat = 28
|
||||||
public static let iconContainerMedium: CGFloat = 40
|
public static let iconContainerMedium: CGFloat = 40
|
||||||
public static let iconContainerLarge: CGFloat = 56
|
public static let iconContainerLarge: CGFloat = 56
|
||||||
@ -186,30 +186,149 @@ public enum Design {
|
|||||||
public static let maxModalWidth: CGFloat = 450
|
public static let maxModalWidth: CGFloat = 450
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Icon Sizes
|
// MARK: - Icon Sizes (Raw Values)
|
||||||
|
|
||||||
/// Standard icon sizes for SF Symbols and custom icons.
|
/// Raw icon point sizes for SF Symbols and custom icons.
|
||||||
|
///
|
||||||
|
/// **Prefer using `SymbolSize` or `SymbolIcon` instead** for semantic naming.
|
||||||
|
/// This enum provides the underlying values that `SymbolSize` references.
|
||||||
|
///
|
||||||
|
/// ## Migration
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// // Old (raw sizes)
|
||||||
|
/// Image(systemName: "star").font(.system(size: Design.IconSize.hero))
|
||||||
|
///
|
||||||
|
/// // New (semantic)
|
||||||
|
/// SymbolIcon("star", size: .hero, color: .accent)
|
||||||
|
/// ```
|
||||||
public enum IconSize {
|
public enum IconSize {
|
||||||
/// Extra small icon (10pt) - tiny indicators.
|
/// 10pt - tiny indicators.
|
||||||
public static let xSmall: CGFloat = 10
|
public static let xSmall: CGFloat = 10
|
||||||
/// Small icon (12pt) - inline with caption text.
|
/// 12pt - inline with caption text, chevrons.
|
||||||
public static let small: CGFloat = 12
|
public static let small: CGFloat = 12
|
||||||
/// Medium icon (16pt) - inline with body text.
|
/// 16pt - inline with body text, indicators.
|
||||||
public static let medium: CGFloat = 16
|
public static let medium: CGFloat = 16
|
||||||
/// Large icon (22pt) - standalone icons.
|
/// 22pt - row icons, checkmarks.
|
||||||
public static let large: CGFloat = 22
|
public static let large: CGFloat = 22
|
||||||
/// Extra large icon (28pt) - row icons, list items.
|
/// 28pt - row icons with containers.
|
||||||
public static let xLarge: CGFloat = 28
|
public static let xLarge: CGFloat = 28
|
||||||
/// Double extra large icon (36pt) - card icons, buttons.
|
/// 36pt - card and button icons.
|
||||||
public static let xxLarge: CGFloat = 36
|
public static let xxLarge: CGFloat = 36
|
||||||
/// Triple extra large icon (48pt) - feature icons.
|
/// 48pt - feature callout icons.
|
||||||
public static let xxxLarge: CGFloat = 48
|
public static let xxxLarge: CGFloat = 48
|
||||||
/// Display icon (64pt) - section headers, prominent features.
|
/// 64pt - section headers, prominent features.
|
||||||
public static let display: CGFloat = 64
|
public static let display: CGFloat = 64
|
||||||
/// Hero icon (80pt) - empty states, splash screens.
|
/// 80pt - empty states, splash screens.
|
||||||
public static let hero: CGFloat = 80
|
public static let hero: CGFloat = 80
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Symbol Sizes (Semantic)
|
||||||
|
|
||||||
|
/// Semantic icon sizes for common use cases.
|
||||||
|
///
|
||||||
|
/// Use these semantic sizes instead of raw `IconSize` values for clearer intent.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// Image(systemName: "star.fill")
|
||||||
|
/// .font(.system(size: Design.SymbolSize.row))
|
||||||
|
/// ```
|
||||||
|
public enum SymbolSize {
|
||||||
|
// MARK: - Content Sizes
|
||||||
|
|
||||||
|
/// Inline with body text (16pt).
|
||||||
|
public static let inline: CGFloat = IconSize.medium
|
||||||
|
/// List row icons (22pt).
|
||||||
|
public static let row: CGFloat = IconSize.large
|
||||||
|
/// Row icons with background container (28pt).
|
||||||
|
public static let rowContainer: CGFloat = IconSize.xLarge
|
||||||
|
/// Card and button icons (36pt).
|
||||||
|
public static let card: CGFloat = IconSize.xxLarge
|
||||||
|
/// Feature callout icons (48pt).
|
||||||
|
public static let feature: CGFloat = IconSize.xxxLarge
|
||||||
|
/// Section header icons (64pt).
|
||||||
|
public static let section: CGFloat = IconSize.display
|
||||||
|
/// Empty state and hero icons (80pt).
|
||||||
|
public static let hero: CGFloat = IconSize.hero
|
||||||
|
|
||||||
|
// MARK: - Accessory Sizes
|
||||||
|
|
||||||
|
/// Navigation chevrons (12pt).
|
||||||
|
public static let chevron: CGFloat = IconSize.small
|
||||||
|
/// Status indicators (16pt).
|
||||||
|
public static let indicator: CGFloat = IconSize.medium
|
||||||
|
/// Badge icons (12pt).
|
||||||
|
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
|
// MARK: - Scale
|
||||||
|
|
||||||
/// Scale factors for pressed states, selections, and animations.
|
/// Scale factors for pressed states, selections, and animations.
|
||||||
|
|||||||
378
Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md
Normal file
378
Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md
Normal file
@ -0,0 +1,378 @@
|
|||||||
|
# Typography and Iconography Guide
|
||||||
|
|
||||||
|
This guide documents the typography and iconography systems in Bedrock for building consistent UI components.
|
||||||
|
|
||||||
|
## Quick Reference
|
||||||
|
|
||||||
|
| 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` |
|
||||||
|
|
||||||
|
**Deprecated:** `Design.IconSize.*` - use `SymbolSize` instead (kept for backwards compatibility).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Typography
|
||||||
|
|
||||||
|
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:
|
||||||
|
|
||||||
|
```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")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fallback: Direct Typography Usage
|
||||||
|
|
||||||
|
**Use `Design.Typography.*` when you need more control:**
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Need .multilineTextAlignment or .lineLimit
|
||||||
|
Text("A longer description that might wrap")
|
||||||
|
.font(Design.Typography.body)
|
||||||
|
.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
|
||||||
|
Text(title)
|
||||||
|
.font(Design.Typography.headline)
|
||||||
|
.foregroundStyle(isActive ? AppTextColors.primary : AppTextColors.tertiary)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Guide
|
||||||
|
|
||||||
|
### From Inline Fonts
|
||||||
|
|
||||||
|
Replace inline font modifiers with `Design.Typography`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Old
|
||||||
|
.font(.subheadline.weight(.medium))
|
||||||
|
// New
|
||||||
|
.font(Design.Typography.settingsTitle)
|
||||||
|
|
||||||
|
// Old
|
||||||
|
.font(.caption.weight(.semibold))
|
||||||
|
// New
|
||||||
|
.font(Design.Typography.sectionHeader)
|
||||||
|
|
||||||
|
// Old
|
||||||
|
.font(.headline)
|
||||||
|
// New
|
||||||
|
.font(Design.Typography.headline)
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Inline Icons
|
||||||
|
|
||||||
|
Replace inline Image styling with `SymbolIcon`:
|
||||||
|
|
||||||
|
```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)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 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 |
|
||||||
182
Sources/Bedrock/Views/Components/SymbolIcon.swift
Normal file
182
Sources/Bedrock/Views/Components/SymbolIcon.swift
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
//
|
||||||
|
// SymbolIcon.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// A consistent SF Symbol icon component with semantic sizing.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
/// A consistent SF Symbol icon with semantic sizing.
|
||||||
|
///
|
||||||
|
/// Use `SymbolIcon` instead of inline `Image(systemName:)` with manual font
|
||||||
|
/// and color modifiers to ensure visual consistency across your app.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// // Before
|
||||||
|
/// Image(systemName: "star.fill")
|
||||||
|
/// .font(.subheadline)
|
||||||
|
/// .foregroundStyle(.secondary)
|
||||||
|
///
|
||||||
|
/// // After
|
||||||
|
/// SymbolIcon("star.fill", size: .row, color: .secondary)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// ## Sizes
|
||||||
|
///
|
||||||
|
/// - `inline`: 16pt - inline with body text
|
||||||
|
/// - `row`: 22pt - list row icons
|
||||||
|
/// - `rowContainer`: 28pt - row icons with background
|
||||||
|
/// - `card`: 36pt - card and button icons
|
||||||
|
/// - `feature`: 48pt - feature callout icons
|
||||||
|
/// - `section`: 64pt - section header icons
|
||||||
|
/// - `hero`: 80pt - empty state and hero icons
|
||||||
|
/// - `chevron`: 12pt - navigation chevrons
|
||||||
|
/// - `indicator`: 16pt - status indicators
|
||||||
|
/// - `badge`: 12pt - badge icons
|
||||||
|
public struct SymbolIcon: View {
|
||||||
|
private let systemName: String
|
||||||
|
private let size: Size
|
||||||
|
private let color: Color
|
||||||
|
private let weight: Font.Weight
|
||||||
|
|
||||||
|
/// Semantic icon sizes.
|
||||||
|
public enum Size {
|
||||||
|
/// Inline with body text (16pt).
|
||||||
|
case inline
|
||||||
|
/// List row icons (22pt).
|
||||||
|
case row
|
||||||
|
/// Row icons with background container (28pt).
|
||||||
|
case rowContainer
|
||||||
|
/// Card and button icons (36pt).
|
||||||
|
case card
|
||||||
|
/// Feature callout icons (48pt).
|
||||||
|
case feature
|
||||||
|
/// Section header icons (64pt).
|
||||||
|
case section
|
||||||
|
/// Empty state and hero icons (80pt).
|
||||||
|
case hero
|
||||||
|
/// Navigation chevrons (12pt).
|
||||||
|
case chevron
|
||||||
|
/// Status indicators (16pt).
|
||||||
|
case indicator
|
||||||
|
/// Badge icons (12pt).
|
||||||
|
case badge
|
||||||
|
|
||||||
|
/// The point size for this semantic size.
|
||||||
|
var pointSize: CGFloat {
|
||||||
|
switch self {
|
||||||
|
case .inline: Design.SymbolSize.inline
|
||||||
|
case .row: Design.SymbolSize.row
|
||||||
|
case .rowContainer: Design.SymbolSize.rowContainer
|
||||||
|
case .card: Design.SymbolSize.card
|
||||||
|
case .feature: Design.SymbolSize.feature
|
||||||
|
case .section: Design.SymbolSize.section
|
||||||
|
case .hero: Design.SymbolSize.hero
|
||||||
|
case .chevron: Design.SymbolSize.chevron
|
||||||
|
case .indicator: Design.SymbolSize.indicator
|
||||||
|
case .badge: Design.SymbolSize.badge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a symbol icon with semantic sizing.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - systemName: The SF Symbol name.
|
||||||
|
/// - size: The semantic size (default: `.row`).
|
||||||
|
/// - color: The icon color (default: `.primary`).
|
||||||
|
/// - weight: The font weight (default: `.regular`).
|
||||||
|
public init(
|
||||||
|
_ systemName: String,
|
||||||
|
size: Size = .row,
|
||||||
|
color: Color = .primary,
|
||||||
|
weight: Font.Weight = .regular
|
||||||
|
) {
|
||||||
|
self.systemName = systemName
|
||||||
|
self.size = size
|
||||||
|
self.color = color
|
||||||
|
self.weight = weight
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Image(systemName: systemName)
|
||||||
|
.font(.system(size: size.pointSize, weight: weight))
|
||||||
|
.foregroundStyle(color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Convenience Initializers
|
||||||
|
|
||||||
|
extension SymbolIcon {
|
||||||
|
/// Creates a chevron icon for navigation rows.
|
||||||
|
/// - Parameter color: The chevron color (default: `.secondary`).
|
||||||
|
public static func chevron(color: Color = .secondary) -> SymbolIcon {
|
||||||
|
SymbolIcon("chevron.right", size: .chevron, color: color, weight: .medium)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a checkmark icon for selection indicators.
|
||||||
|
/// - Parameter color: The checkmark color.
|
||||||
|
public static func checkmark(color: Color) -> SymbolIcon {
|
||||||
|
SymbolIcon("checkmark.circle.fill", size: .row, color: color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||||
|
Group {
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .badge, color: .yellow)
|
||||||
|
Text("Badge (12pt)")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .chevron, color: .yellow)
|
||||||
|
Text("Chevron (12pt)")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .inline, color: .yellow)
|
||||||
|
Text("Inline (16pt)")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .row, color: .yellow)
|
||||||
|
Text("Row (22pt)")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .rowContainer, color: .yellow)
|
||||||
|
Text("Row Container (28pt)")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .card, color: .yellow)
|
||||||
|
Text("Card (36pt)")
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .feature, color: .yellow)
|
||||||
|
Text("Feature (48pt)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .section, color: .yellow)
|
||||||
|
Text("Section (64pt)")
|
||||||
|
}
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
SymbolIcon("star.fill", size: .hero, color: .yellow)
|
||||||
|
Text("Hero (80pt)")
|
||||||
|
}
|
||||||
|
|
||||||
|
Divider()
|
||||||
|
|
||||||
|
HStack {
|
||||||
|
Text("Settings Row")
|
||||||
|
Spacer()
|
||||||
|
SymbolIcon.chevron()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
@ -37,7 +37,7 @@ public struct BadgePill: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
Text(text)
|
Text(text)
|
||||||
.font(.subheadline.weight(.bold))
|
.font(Design.Typography.badge)
|
||||||
.fontDesign(.rounded)
|
.fontDesign(.rounded)
|
||||||
.foregroundStyle(isSelected ? .black : accentColor)
|
.foregroundStyle(isSelected ? .black : accentColor)
|
||||||
.padding(.horizontal, Design.Spacing.small)
|
.padding(.horizontal, Design.Spacing.small)
|
||||||
|
|||||||
@ -76,16 +76,16 @@ public struct LicensesView: View {
|
|||||||
SettingsCard(backgroundColor: cardBackgroundColor, borderColor: cardBorderColor) {
|
SettingsCard(backgroundColor: cardBackgroundColor, borderColor: cardBorderColor) {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
Text(license.name)
|
Text(license.name)
|
||||||
.font(.subheadline.weight(.bold))
|
.font(Design.Typography.bodyBold)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
Text(license.description)
|
Text(license.description)
|
||||||
.font(.caption)
|
.font(Design.Typography.caption)
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
.foregroundStyle(.white.opacity(Design.Opacity.strong))
|
||||||
|
|
||||||
HStack {
|
HStack {
|
||||||
Label(license.licenseType, systemImage: "doc.text")
|
Label(license.licenseType, systemImage: "doc.text")
|
||||||
.font(.caption2)
|
.font(Design.Typography.captionSmall)
|
||||||
.foregroundStyle(accentColor)
|
.foregroundStyle(accentColor)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -93,7 +93,7 @@ public struct LicensesView: View {
|
|||||||
if let linkURL = URL(string: license.url) {
|
if let linkURL = URL(string: license.url) {
|
||||||
Link(destination: linkURL) {
|
Link(destination: linkURL) {
|
||||||
Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square")
|
Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square")
|
||||||
.font(.caption2)
|
.font(Design.Typography.captionSmall)
|
||||||
.foregroundStyle(accentColor)
|
.foregroundStyle(accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ public struct SegmentedPicker<T: Equatable>: View {
|
|||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.subheadline.weight(.medium))
|
.font(Design.Typography.settingsTitle)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
@ -62,7 +62,7 @@ public struct SegmentedPicker<T: Equatable>: View {
|
|||||||
selection = option.1
|
selection = option.1
|
||||||
} label: {
|
} label: {
|
||||||
Text(option.0)
|
Text(option.0)
|
||||||
.font(.subheadline.weight(.medium))
|
.font(Design.Typography.settingsTitle)
|
||||||
.foregroundStyle(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong))
|
.foregroundStyle(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong))
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|||||||
@ -68,11 +68,11 @@ public struct SelectableRow<Badge: View>: View {
|
|||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.callout.weight(.semibold))
|
.font(Design.Typography.calloutBold)
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
Text(subtitle)
|
Text(subtitle)
|
||||||
.font(.subheadline)
|
.font(Design.Typography.body)
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,11 +22,11 @@ public struct SelectionIndicator: View {
|
|||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
/// - isSelected: Whether selected.
|
/// - isSelected: Whether selected.
|
||||||
/// - accentColor: Color for checkmark (default: primary accent).
|
/// - accentColor: Color for checkmark (default: primary accent).
|
||||||
/// - size: Size of the indicator (default: checkmark size from design).
|
/// - size: Size of the indicator (default: row symbol size).
|
||||||
public init(
|
public init(
|
||||||
isSelected: Bool,
|
isSelected: Bool,
|
||||||
accentColor: Color = .Accent.primary,
|
accentColor: Color = .Accent.primary,
|
||||||
size: CGFloat = Design.Size.checkmark
|
size: CGFloat = Design.SymbolSize.row
|
||||||
) {
|
) {
|
||||||
self.isSelected = isSelected
|
self.isSelected = isSelected
|
||||||
self.accentColor = accentColor
|
self.accentColor = accentColor
|
||||||
@ -35,9 +35,7 @@ public struct SelectionIndicator: View {
|
|||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
if isSelected {
|
if isSelected {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
SymbolIcon("checkmark.circle.fill", size: .row, color: accentColor)
|
||||||
.font(.system(size: size))
|
|
||||||
.foregroundStyle(accentColor)
|
|
||||||
} else {
|
} else {
|
||||||
Circle()
|
Circle()
|
||||||
.strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.medium)
|
.strokeBorder(Color.white.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.medium)
|
||||||
|
|||||||
@ -67,22 +67,16 @@ public struct SettingsNavigationRow<Destination: View>: View {
|
|||||||
} label: {
|
} label: {
|
||||||
HStack {
|
HStack {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
Text(title)
|
TitleLabel(title, style: .settings)
|
||||||
.font(.subheadline.weight(.medium))
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
|
|
||||||
if let subtitle {
|
if let subtitle {
|
||||||
Text(subtitle)
|
CaptionLabel(subtitle)
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Image(systemName: "chevron.right")
|
SymbolIcon.chevron(color: .secondary)
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
}
|
||||||
.padding(Design.Spacing.medium)
|
.padding(Design.Spacing.medium)
|
||||||
.background(backgroundColor, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
.background(backgroundColor, in: RoundedRectangle(cornerRadius: Design.CornerRadius.medium))
|
||||||
|
|||||||
@ -54,31 +54,23 @@ public struct SettingsRow<Accessory: View>: View {
|
|||||||
public var body: some View {
|
public var body: some View {
|
||||||
Button(action: action) {
|
Button(action: action) {
|
||||||
HStack(spacing: Design.Spacing.medium) {
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
Image(systemName: systemImage)
|
SymbolIcon(systemImage, size: .inline, color: .white)
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.white)
|
|
||||||
.frame(width: Design.Size.iconContainerSmall, height: Design.Size.iconContainerSmall)
|
.frame(width: Design.Size.iconContainerSmall, height: Design.Size.iconContainerSmall)
|
||||||
.background(iconColor.opacity(Design.Opacity.heavy))
|
.background(iconColor.opacity(Design.Opacity.heavy))
|
||||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.xSmall))
|
.clipShape(.rect(cornerRadius: Design.CornerRadius.xSmall))
|
||||||
|
|
||||||
Text(title)
|
BodyLabel(title)
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if let value {
|
if let value {
|
||||||
Text(value)
|
BodyLabel(value, emphasis: .secondary)
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if let accessory {
|
if let accessory {
|
||||||
accessory
|
accessory
|
||||||
} else {
|
} else {
|
||||||
Image(systemName: "chevron.right")
|
SymbolIcon.chevron()
|
||||||
.font(.subheadline.weight(.medium))
|
|
||||||
.foregroundStyle(.tertiary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.vertical, Design.Spacing.medium)
|
.padding(.vertical, Design.Spacing.medium)
|
||||||
|
|||||||
@ -36,13 +36,11 @@ public struct SettingsSectionHeader: View {
|
|||||||
public var body: some View {
|
public var body: some View {
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
if let systemImage {
|
if let systemImage {
|
||||||
Image(systemName: systemImage)
|
SymbolIcon(systemImage, size: .inline, color: accentColor.opacity(Design.Opacity.strong))
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(accentColor.opacity(Design.Opacity.strong))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(title)
|
Text(title)
|
||||||
.font(.caption.weight(.semibold))
|
.font(Design.Typography.sectionHeader)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.textCase(.uppercase)
|
.textCase(.uppercase)
|
||||||
.tracking(0.5)
|
.tracking(0.5)
|
||||||
|
|||||||
@ -79,20 +79,16 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
|||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
// Title row with optional accessory
|
// Title row with optional accessory
|
||||||
HStack(spacing: Design.Spacing.xSmall) {
|
HStack(spacing: Design.Spacing.xSmall) {
|
||||||
Text(title)
|
TitleLabel(title, style: .settings)
|
||||||
.font(.subheadline.weight(.medium))
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
|
|
||||||
if let titleAccessory {
|
if let titleAccessory {
|
||||||
titleAccessory
|
titleAccessory
|
||||||
.font(.caption2)
|
.font(Design.Typography.captionSmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Subtitle
|
// Subtitle
|
||||||
Text(subtitle)
|
CaptionLabel(subtitle)
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
// Segmented buttons
|
// Segmented buttons
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
@ -102,7 +98,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
|||||||
selection = option.1
|
selection = option.1
|
||||||
} label: {
|
} label: {
|
||||||
Text(option.0)
|
Text(option.0)
|
||||||
.font(.subheadline.weight(.medium))
|
.font(Design.Typography.settingsTitle)
|
||||||
.foregroundStyle(selection == option.1 ? Color.white : .primary)
|
.foregroundStyle(selection == option.1 ? Color.white : .primary)
|
||||||
.padding(.vertical, Design.Spacing.small)
|
.padding(.vertical, Design.Spacing.small)
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|||||||
@ -110,26 +110,23 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
|
|||||||
public var body: some View {
|
public var body: some View {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||||
HStack {
|
HStack {
|
||||||
Text(title)
|
TitleLabel(title, style: .settings)
|
||||||
.font(.subheadline.weight(.medium))
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
|
// Exception: Needs .fontDesign(.rounded) modifier
|
||||||
Text(format(value))
|
Text(format(value))
|
||||||
.font(.subheadline.weight(.medium))
|
.font(Design.Typography.settingsTitle)
|
||||||
.fontDesign(.rounded)
|
.fontDesign(.rounded)
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(subtitle)
|
CaptionLabel(subtitle)
|
||||||
.font(.caption)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
|
|
||||||
HStack(spacing: Design.Spacing.medium) {
|
HStack(spacing: Design.Spacing.medium) {
|
||||||
if let leadingIcon {
|
if let leadingIcon {
|
||||||
leadingIcon
|
leadingIcon
|
||||||
.font(.caption2)
|
.font(.system(size: Design.SymbolSize.badge))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -138,7 +135,7 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
|
|||||||
|
|
||||||
if let trailingIcon {
|
if let trailingIcon {
|
||||||
trailingIcon
|
trailingIcon
|
||||||
.font(.callout)
|
.font(.system(size: Design.SymbolSize.inline))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -78,19 +78,15 @@ public struct SettingsToggle<Accessory: View>: View {
|
|||||||
Toggle(isOn: $isOn) {
|
Toggle(isOn: $isOn) {
|
||||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||||
HStack(spacing: Design.Spacing.xSmall) {
|
HStack(spacing: Design.Spacing.xSmall) {
|
||||||
Text(title)
|
TitleLabel(title, style: .settings)
|
||||||
.font(.subheadline.weight(.medium))
|
|
||||||
.foregroundStyle(.primary)
|
|
||||||
|
|
||||||
if let titleAccessory {
|
if let titleAccessory {
|
||||||
titleAccessory
|
titleAccessory
|
||||||
.font(.caption2)
|
.font(Design.Typography.captionSmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(subtitle)
|
BodyLabel(subtitle, emphasis: .secondary)
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(.secondary)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tint(accentColor)
|
.tint(accentColor)
|
||||||
|
|||||||
@ -115,12 +115,10 @@ public struct iCloudSyncSettingsView<ViewModel: CloudSyncable>: View {
|
|||||||
// Sync status (show when enabled and available)
|
// Sync status (show when enabled and available)
|
||||||
if viewModel.iCloudEnabled && viewModel.iCloudAvailable {
|
if viewModel.iCloudEnabled && viewModel.iCloudAvailable {
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
Image(systemName: syncStatusIcon)
|
SymbolIcon(syncStatusIcon, size: .inline, color: syncStatusColor)
|
||||||
.font(.subheadline)
|
|
||||||
.foregroundStyle(syncStatusColor)
|
|
||||||
|
|
||||||
Text(syncStatusText)
|
Text(syncStatusText)
|
||||||
.font(.caption)
|
.font(Design.Typography.caption)
|
||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
@ -129,7 +127,7 @@ public struct iCloudSyncSettingsView<ViewModel: CloudSyncable>: View {
|
|||||||
viewModel.forceSync()
|
viewModel.forceSync()
|
||||||
} label: {
|
} label: {
|
||||||
Text(String(localized: "Sync Now"))
|
Text(String(localized: "Sync Now"))
|
||||||
.font(.caption.weight(.medium))
|
.font(Design.Typography.captionMedium)
|
||||||
.foregroundStyle(accentColor)
|
.foregroundStyle(accentColor)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
298
Sources/Bedrock/Views/Text/StyledText.swift
Normal file
298
Sources/Bedrock/Views/Text/StyledText.swift
Normal file
@ -0,0 +1,298 @@
|
|||||||
|
//
|
||||||
|
// StyledText.swift
|
||||||
|
// Bedrock
|
||||||
|
//
|
||||||
|
// Semantic text components for consistent typography.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
// MARK: - Title Label
|
||||||
|
|
||||||
|
/// A title label with semantic styling.
|
||||||
|
///
|
||||||
|
/// Use `TitleLabel` for card titles, section headers, and row titles
|
||||||
|
/// to ensure consistent typography across your app.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// TitleLabel("Settings", style: .settings)
|
||||||
|
/// TitleLabel("Morning Ritual", style: .card)
|
||||||
|
/// ```
|
||||||
|
public struct TitleLabel: View {
|
||||||
|
private let text: String
|
||||||
|
private let style: Style
|
||||||
|
private let color: Color?
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
/// - 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) {
|
||||||
|
self.text = text
|
||||||
|
self.emphasis = emphasis
|
||||||
|
self.weight = weight
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Text(text)
|
||||||
|
.font(weight.font)
|
||||||
|
.foregroundStyle(color ?? emphasis.color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Caption Label
|
||||||
|
|
||||||
|
/// A caption label for metadata and small text.
|
||||||
|
///
|
||||||
|
/// Use `CaptionLabel` for timestamps, metadata, badges, and fine print
|
||||||
|
/// to ensure consistent typography across your app.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// CaptionLabel("Day 6 of 28", style: .badge)
|
||||||
|
/// CaptionLabel("Last updated 2 hours ago", style: .standard)
|
||||||
|
/// ```
|
||||||
|
public struct CaptionLabel: View {
|
||||||
|
private let text: String
|
||||||
|
private let style: Style
|
||||||
|
private let color: Color?
|
||||||
|
|
||||||
|
/// 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.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - 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) {
|
||||||
|
self.text = text
|
||||||
|
self.style = style
|
||||||
|
self.color = color
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
Text(text)
|
||||||
|
.font(style.font)
|
||||||
|
.foregroundStyle(color ?? .secondary)
|
||||||
|
.textCase(style.isUppercase ? .uppercase : nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Section Header
|
||||||
|
|
||||||
|
/// A section header label with uppercase styling.
|
||||||
|
///
|
||||||
|
/// Use `SectionHeader` for grouped list section titles
|
||||||
|
/// to match iOS Settings app conventions.
|
||||||
|
///
|
||||||
|
/// ## Example
|
||||||
|
///
|
||||||
|
/// ```swift
|
||||||
|
/// SectionHeader("GENERAL")
|
||||||
|
/// SectionHeader("Notifications", icon: "bell.fill")
|
||||||
|
/// ```
|
||||||
|
public struct SectionHeader: View {
|
||||||
|
private let title: String
|
||||||
|
private let icon: String?
|
||||||
|
private let accentColor: Color
|
||||||
|
|
||||||
|
/// Creates a section header.
|
||||||
|
/// - Parameters:
|
||||||
|
/// - title: The header text (will be uppercased).
|
||||||
|
/// - icon: Optional SF Symbol name.
|
||||||
|
/// - accentColor: Icon accent color (default: `.Accent.primary`).
|
||||||
|
public init(_ title: String, icon: String? = nil, accentColor: Color = .Accent.primary) {
|
||||||
|
self.title = title
|
||||||
|
self.icon = icon
|
||||||
|
self.accentColor = accentColor
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack(spacing: Design.Spacing.small) {
|
||||||
|
if let icon {
|
||||||
|
SymbolIcon(icon, size: .inline, color: accentColor.opacity(Design.Opacity.strong))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(title)
|
||||||
|
.font(Design.Typography.sectionHeader)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.textCase(.uppercase)
|
||||||
|
.tracking(0.5)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Preview
|
||||||
|
|
||||||
|
#Preview("Title 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)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Body 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)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview("Section Headers") {
|
||||||
|
VStack(alignment: .leading, spacing: Design.Spacing.large) {
|
||||||
|
SectionHeader("General")
|
||||||
|
SectionHeader("Notifications", icon: "bell.fill")
|
||||||
|
SectionHeader("Privacy", icon: "lock.fill", accentColor: .blue)
|
||||||
|
}
|
||||||
|
.padding()
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user