diff --git a/Sources/Bedrock/Theme/Design.swift b/Sources/Bedrock/Theme/Design.swift index b23cb91..ecfd34c 100644 --- a/Sources/Bedrock/Theme/Design.swift +++ b/Sources/Bedrock/Theme/Design.swift @@ -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. diff --git a/Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md b/Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md index e127282..9cbd5cf 100644 --- a/Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md +++ b/Sources/Bedrock/Theme/TYPOGRAPHY_GUIDE.md @@ -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. diff --git a/Sources/Bedrock/Theme/TextEmphasis.swift b/Sources/Bedrock/Theme/TextEmphasis.swift new file mode 100644 index 0000000..53f4c3c --- /dev/null +++ b/Sources/Bedrock/Theme/TextEmphasis.swift @@ -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(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) + } +} diff --git a/Sources/Bedrock/Theme/Typography.swift b/Sources/Bedrock/Theme/Typography.swift new file mode 100644 index 0000000..07b4228 --- /dev/null +++ b/Sources/Bedrock/Theme/Typography.swift @@ -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) + } +} diff --git a/Sources/Bedrock/Views/Settings/BadgePill.swift b/Sources/Bedrock/Views/Settings/BadgePill.swift index c4f59e0..c5c14df 100644 --- a/Sources/Bedrock/Views/Settings/BadgePill.swift +++ b/Sources/Bedrock/Views/Settings/BadgePill.swift @@ -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) diff --git a/Sources/Bedrock/Views/Settings/LicensesView.swift b/Sources/Bedrock/Views/Settings/LicensesView.swift index 509928a..56db7f4 100644 --- a/Sources/Bedrock/Views/Settings/LicensesView.swift +++ b/Sources/Bedrock/Views/Settings/LicensesView.swift @@ -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) } } diff --git a/Sources/Bedrock/Views/Settings/SegmentedPicker.swift b/Sources/Bedrock/Views/Settings/SegmentedPicker.swift index 7dbd4b4..f34b72d 100644 --- a/Sources/Bedrock/Views/Settings/SegmentedPicker.swift +++ b/Sources/Bedrock/Views/Settings/SegmentedPicker.swift @@ -52,7 +52,7 @@ public struct SegmentedPicker: 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: 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) diff --git a/Sources/Bedrock/Views/Settings/SelectableRow.swift b/Sources/Bedrock/Views/Settings/SelectableRow.swift index 88e31eb..45417b6 100644 --- a/Sources/Bedrock/Views/Settings/SelectableRow.swift +++ b/Sources/Bedrock/Views/Settings/SelectableRow.swift @@ -68,11 +68,11 @@ public struct SelectableRow: 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)) } diff --git a/Sources/Bedrock/Views/Settings/SettingsNavigationRow.swift b/Sources/Bedrock/Views/Settings/SettingsNavigationRow.swift index 08bbc1b..e6657d7 100644 --- a/Sources/Bedrock/Views/Settings/SettingsNavigationRow.swift +++ b/Sources/Bedrock/Views/Settings/SettingsNavigationRow.swift @@ -67,10 +67,10 @@ public struct SettingsNavigationRow: 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) } } diff --git a/Sources/Bedrock/Views/Settings/SettingsRow.swift b/Sources/Bedrock/Views/Settings/SettingsRow.swift index 8699946..50b58ed 100644 --- a/Sources/Bedrock/Views/Settings/SettingsRow.swift +++ b/Sources/Bedrock/Views/Settings/SettingsRow.swift @@ -59,12 +59,12 @@ public struct SettingsRow: 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 { diff --git a/Sources/Bedrock/Views/Settings/SettingsSectionHeader.swift b/Sources/Bedrock/Views/Settings/SettingsSectionHeader.swift index 3883f21..5c72b2e 100644 --- a/Sources/Bedrock/Views/Settings/SettingsSectionHeader.swift +++ b/Sources/Bedrock/Views/Settings/SettingsSectionHeader.swift @@ -40,7 +40,7 @@ public struct SettingsSectionHeader: View { } Text(title) - .font(Design.Typography.sectionHeader) + .typography(.captionEmphasis) .foregroundStyle(.secondary) .textCase(.uppercase) .tracking(0.5) diff --git a/Sources/Bedrock/Views/Settings/SettingsSegmentedPicker.swift b/Sources/Bedrock/Views/Settings/SettingsSegmentedPicker.swift index eeb5fa4..faa6eee 100644 --- a/Sources/Bedrock/Views/Settings/SettingsSegmentedPicker.swift +++ b/Sources/Bedrock/Views/Settings/SettingsSegmentedPicker.swift @@ -79,16 +79,16 @@ public struct SettingsSegmentedPicker: 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: 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) diff --git a/Sources/Bedrock/Views/Settings/SettingsSlider.swift b/Sources/Bedrock/Views/Settings/SettingsSlider.swift index c93152f..5fbfcf7 100644 --- a/Sources/Bedrock/Views/Settings/SettingsSlider.swift +++ b/Sources/Bedrock/Views/Settings/SettingsSlider.swift @@ -110,18 +110,18 @@ public struct SettingsSlider: 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 { diff --git a/Sources/Bedrock/Views/Settings/SettingsToggle.swift b/Sources/Bedrock/Views/Settings/SettingsToggle.swift index 75fd7df..36b1d56 100644 --- a/Sources/Bedrock/Views/Settings/SettingsToggle.swift +++ b/Sources/Bedrock/Views/Settings/SettingsToggle.swift @@ -78,15 +78,15 @@ public struct SettingsToggle: 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) diff --git a/Sources/Bedrock/Views/Settings/iCloudSyncSettingsView.swift b/Sources/Bedrock/Views/Settings/iCloudSyncSettingsView.swift index 2faf190..4614e87 100644 --- a/Sources/Bedrock/Views/Settings/iCloudSyncSettingsView.swift +++ b/Sources/Bedrock/Views/Settings/iCloudSyncSettingsView.swift @@ -118,7 +118,7 @@ public struct iCloudSyncSettingsView: 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: View { viewModel.forceSync() } label: { Text(String(localized: "Sync Now")) - .font(Design.Typography.captionMedium) + .typography(.captionEmphasis) .foregroundStyle(accentColor) } } diff --git a/Sources/Bedrock/Views/Text/StyledText.swift b/Sources/Bedrock/Views/Text/StyledText.swift index 385a86e..103ef31 100644 --- a/Sources/Bedrock/Views/Text/StyledText.swift +++ b/Sources/Bedrock/Views/Text/StyledText.swift @@ -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() }