Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
7859b22927
commit
492e088cef
@ -9,9 +9,7 @@ The typography system consists of:
|
||||
1. **`Theme`** - Global theme registration for all color providers
|
||||
2. **`Typography` enum** - All font definitions with explicit naming
|
||||
3. **`TextEmphasis` enum** - Semantic text colors
|
||||
4. **`StyledLabel`** - Simple text component combining typography + emphasis
|
||||
5. **`IconLabel`** - Icon + text component
|
||||
6. **`.typography()` modifier** - View extension for applying fonts
|
||||
4. **`Text().styled()` / `Label().styled()`** - View extensions for semantic styling
|
||||
|
||||
---
|
||||
|
||||
@ -37,8 +35,8 @@ This enables Bedrock components to use your app's colors automatically.
|
||||
After registration, use `Theme.*` to access your registered colors:
|
||||
|
||||
```swift
|
||||
// In Bedrock components (automatic via TextEmphasis)
|
||||
StyledLabel("Title", .heading) // Uses Theme.Text.primary
|
||||
// In views (automatic via TextEmphasis)
|
||||
Text("Title").styled(.heading) // Uses Theme.Text.primary
|
||||
|
||||
// Direct access when needed
|
||||
let bgColor = Theme.Surface.card
|
||||
@ -88,7 +86,7 @@ All fonts are defined in the `Typography` enum. Each case name explicitly descri
|
||||
|
||||
## TextEmphasis Enum
|
||||
|
||||
Semantic text colors resolved from the registered `TextTheme`:
|
||||
Semantic text colors resolved from the registered Theme:
|
||||
|
||||
| Emphasis | Description |
|
||||
|----------|-------------|
|
||||
@ -101,52 +99,67 @@ Semantic text colors resolved from the registered `TextTheme`:
|
||||
|
||||
### When to Use `.custom()`
|
||||
|
||||
Only use `.custom()` for colors that aren't text colors:
|
||||
Only use `.custom()` for colors that aren't semantic text colors:
|
||||
|
||||
```swift
|
||||
// ✅ Semantic text colors - use the emphasis value directly
|
||||
StyledLabel("Title", .heading) // .primary is default
|
||||
StyledLabel("Subtitle", .subheading, emphasis: .secondary)
|
||||
StyledLabel("Hint", .caption, emphasis: .tertiary)
|
||||
Text("Title").styled(.heading) // .primary is default
|
||||
Text("Subtitle").styled(.subheading, emphasis: .secondary)
|
||||
Text("Hint").styled(.caption, emphasis: .tertiary)
|
||||
|
||||
// ✅ Use .custom() for non-text colors
|
||||
StyledLabel("Success!", .heading, emphasis: .custom(AppStatus.success))
|
||||
StyledLabel("Arc 3", .caption, emphasis: .custom(AppAccent.primary))
|
||||
Text("Success!").styled(.heading, emphasis: .custom(AppStatus.success))
|
||||
Text("Arc 3").styled(.caption, emphasis: .custom(AppAccent.primary))
|
||||
|
||||
// ✅ Use .custom() for conditional colors
|
||||
StyledLabel(text, .body, emphasis: .custom(isActive ? AppStatus.success : AppTextColors.tertiary))
|
||||
Text(text).styled(.body, emphasis: .custom(isActive ? AppStatus.success : AppTextColors.tertiary))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## StyledLabel Component
|
||||
## Text().styled() Extension
|
||||
|
||||
A single component for all styled text:
|
||||
The primary way to style text:
|
||||
|
||||
```swift
|
||||
StyledLabel(
|
||||
_ text: String,
|
||||
_ typography: Typography = .body,
|
||||
emphasis: TextEmphasis = .primary,
|
||||
alignment: TextAlignment? = nil,
|
||||
lineLimit: Int? = nil
|
||||
)
|
||||
Text(_ text: String)
|
||||
.styled(_ typography: Typography = .body, emphasis: TextEmphasis = .primary)
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```swift
|
||||
// Simple - uses registered theme's primary color
|
||||
StyledLabel("Morning Ritual", .heading)
|
||||
StyledLabel("Day 6 of 28", .caption, emphasis: .secondary)
|
||||
Text("Morning Ritual").styled(.heading)
|
||||
Text("Day 6 of 28").styled(.caption, emphasis: .secondary)
|
||||
|
||||
// With alignment and line limit
|
||||
StyledLabel("Centered text", .body, alignment: .center)
|
||||
StyledLabel("One line", .caption, emphasis: .tertiary, lineLimit: 1)
|
||||
// String interpolation
|
||||
Text("Count: \(count)").styled(.bodyEmphasis)
|
||||
|
||||
// With additional modifiers
|
||||
Text("Centered text")
|
||||
.styled(.body)
|
||||
.multilineTextAlignment(.center)
|
||||
|
||||
Text("One line")
|
||||
.styled(.caption, emphasis: .tertiary)
|
||||
.lineLimit(1)
|
||||
|
||||
// Font design variants
|
||||
Text("$9.99")
|
||||
.styled(.subheadingEmphasis)
|
||||
.fontDesign(.rounded)
|
||||
|
||||
// Uppercase headers
|
||||
Text("SECTION")
|
||||
.styled(.captionEmphasis, emphasis: .secondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
|
||||
// In buttons
|
||||
Button(action: onContinue) {
|
||||
StyledLabel("Continue", .heading, emphasis: .inverse)
|
||||
Text("Continue")
|
||||
.styled(.heading, emphasis: .inverse)
|
||||
.frame(maxWidth: .infinity)
|
||||
.frame(height: 50)
|
||||
.background(AppAccent.primary)
|
||||
@ -155,66 +168,48 @@ Button(action: onContinue) {
|
||||
|
||||
---
|
||||
|
||||
## IconLabel Component
|
||||
## Label().styled() Extension
|
||||
|
||||
For icon + text combinations:
|
||||
|
||||
```swift
|
||||
IconLabel(_ icon: String, _ text: String, _ typography: Typography = .body, emphasis: TextEmphasis = .primary)
|
||||
Label(_ text: String, systemImage: String)
|
||||
.styled(_ typography: Typography = .body, emphasis: TextEmphasis = .primary)
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
```swift
|
||||
IconLabel("bell.fill", "Notifications", .subheading)
|
||||
IconLabel("star.fill", "Favorites", .body, emphasis: .secondary)
|
||||
IconLabel("flame.fill", "5-day streak", .caption, emphasis: .custom(AppStatus.success))
|
||||
Label("Notifications", systemImage: "bell.fill")
|
||||
.styled(.subheading)
|
||||
|
||||
Label("Favorites", systemImage: "star.fill")
|
||||
.styled(.body, emphasis: .secondary)
|
||||
|
||||
Label("5-day streak", systemImage: "flame.fill")
|
||||
.styled(.caption, emphasis: .custom(AppStatus.success))
|
||||
|
||||
// In buttons
|
||||
Button(action: addItem) {
|
||||
Label("Add to Rituals", systemImage: "plus.circle.fill")
|
||||
.styled(.heading, emphasis: .inverse)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(AppAccent.primary)
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## When to Use StyledLabel vs Text()
|
||||
|
||||
### Use StyledLabel (Preferred)
|
||||
|
||||
Use `StyledLabel` for 95% of text:
|
||||
|
||||
```swift
|
||||
StyledLabel("Settings", .subheadingEmphasis)
|
||||
StyledLabel("Description", .subheading, emphasis: .secondary)
|
||||
StyledLabel("Centered", .body, alignment: .center)
|
||||
```
|
||||
|
||||
### Use Text() with .typography() (Rare Exceptions)
|
||||
|
||||
Only when you need modifiers `StyledLabel` doesn't support:
|
||||
|
||||
```swift
|
||||
// Font design variants
|
||||
Text(formattedValue)
|
||||
.typography(.subheadingEmphasis)
|
||||
.fontDesign(.rounded)
|
||||
|
||||
// TextField styling
|
||||
TextField("Placeholder", text: $value)
|
||||
.typography(.heading)
|
||||
|
||||
// Inside special view builders (like Charts AxisValueLabel)
|
||||
AxisValueLabel {
|
||||
Text("\(value)%")
|
||||
.font(Typography.caption2.font)
|
||||
.foregroundStyle(color)
|
||||
}
|
||||
```
|
||||
|
||||
### Never Use Raw `.font()` with System Fonts
|
||||
## Never Use Raw `.font()` with System Fonts
|
||||
|
||||
```swift
|
||||
// ❌ BAD
|
||||
Text("Title").font(.headline)
|
||||
Text("Body").font(.body).foregroundStyle(.white)
|
||||
|
||||
// ✅ GOOD
|
||||
StyledLabel("Title", .heading)
|
||||
Text("Title").styled(.heading)
|
||||
Text("Body").styled(.body, emphasis: .inverse)
|
||||
```
|
||||
|
||||
---
|
||||
@ -227,16 +222,21 @@ StyledLabel("Title", .heading)
|
||||
// OLD: Design.Typography
|
||||
Text("Title").font(Design.Typography.headline)
|
||||
|
||||
// NEW: StyledLabel
|
||||
StyledLabel("Title", .heading)
|
||||
// NEW: Text().styled()
|
||||
Text("Title").styled(.heading)
|
||||
|
||||
// OLD: Multiple Label components
|
||||
TitleLabel("Title", style: .headline, color: .white)
|
||||
BodyLabel("Body", emphasis: .secondary)
|
||||
|
||||
// NEW: Single StyledLabel
|
||||
// OLD: StyledLabel component
|
||||
StyledLabel("Title", .heading, emphasis: .inverse)
|
||||
StyledLabel("Body", .subheading, emphasis: .secondary)
|
||||
|
||||
// NEW: Text().styled()
|
||||
Text("Title").styled(.heading, emphasis: .inverse)
|
||||
|
||||
// OLD: IconLabel component
|
||||
IconLabel("bell.fill", "Notifications", .subheading)
|
||||
|
||||
// NEW: Label().styled()
|
||||
Label("Notifications", systemImage: "bell.fill")
|
||||
.styled(.subheading)
|
||||
```
|
||||
|
||||
### Mapping Table
|
||||
|
||||
@ -26,9 +26,9 @@ import SwiftUI
|
||||
/// )
|
||||
///
|
||||
/// // Then use emphasis levels directly
|
||||
/// StyledLabel("Primary text", .body) // uses .primary
|
||||
/// StyledLabel("Secondary text", .caption, emphasis: .secondary)
|
||||
/// StyledLabel("Custom color", .heading, emphasis: .custom(.red))
|
||||
/// Text("Primary text").styled(.body) // uses .primary
|
||||
/// Text("Secondary text").styled(.caption, emphasis: .secondary)
|
||||
/// Text("Custom color").styled(.heading, emphasis: .custom(.red))
|
||||
/// ```
|
||||
public enum TextEmphasis: Sendable {
|
||||
/// Primary text color (highest emphasis). Default.
|
||||
|
||||
@ -12,7 +12,7 @@ import SwiftUI
|
||||
/// Global theme provider for Bedrock components.
|
||||
///
|
||||
/// Register your app's theme once at launch to enable semantic colors
|
||||
/// in `StyledLabel`, `IconLabel`, and other Bedrock components.
|
||||
/// in `Text().styled()`, `Label().styled()`, and other Bedrock components.
|
||||
///
|
||||
/// ## Usage
|
||||
///
|
||||
@ -26,8 +26,8 @@ import SwiftUI
|
||||
/// )
|
||||
///
|
||||
/// // Then use semantic emphasis directly
|
||||
/// StyledLabel("Title", .heading) // uses Theme.Text.primary
|
||||
/// StyledLabel("Subtitle", .subheading, emphasis: .secondary) // uses Theme.Text.secondary
|
||||
/// Text("Title").styled(.heading) // uses Theme.Text.primary
|
||||
/// Text("Subtitle").styled(.subheading, emphasis: .secondary) // uses Theme.Text.secondary
|
||||
/// ```
|
||||
public enum Theme {
|
||||
// MARK: - Registered Providers
|
||||
|
||||
@ -73,7 +73,7 @@ public extension View {
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
|
||||
.pulsing(isActive: true)
|
||||
|
||||
StyledLabel("Pulsing highlights interactive areas", .caption, emphasis: .secondary)
|
||||
Text("Pulsing highlights interactive areas").styled(.caption, emphasis: .secondary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -75,18 +75,20 @@ public struct LicensesView: View {
|
||||
private func licenseCard(_ license: License) -> some View {
|
||||
SettingsCard(backgroundColor: cardBackgroundColor, borderColor: cardBorderColor) {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
StyledLabel(license.name, .bodyEmphasis, emphasis: .inverse)
|
||||
Text(license.name).styled(.bodyEmphasis, emphasis: .inverse)
|
||||
|
||||
StyledLabel(license.description, .caption, emphasis: .secondary)
|
||||
Text(license.description).styled(.caption, emphasis: .secondary)
|
||||
|
||||
HStack {
|
||||
IconLabel("doc.text", license.licenseType, .caption2, emphasis: .custom(accentColor))
|
||||
Label(license.licenseType, systemImage: "doc.text")
|
||||
.styled(.caption2, emphasis: .custom(accentColor))
|
||||
|
||||
Spacer()
|
||||
|
||||
if let linkURL = URL(string: license.url) {
|
||||
Link(destination: linkURL) {
|
||||
IconLabel("arrow.up.right.square", String(localized: "View on GitHub"), .caption2, emphasis: .custom(accentColor))
|
||||
Label(String(localized: "View on GitHub"), systemImage: "arrow.up.right.square")
|
||||
.styled(.caption2, emphasis: .custom(accentColor))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,7 +51,7 @@ public struct SegmentedPicker<T: Equatable>: View {
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
StyledLabel(title, .subheadingEmphasis, emphasis: .inverse)
|
||||
Text(title).styled(.subheadingEmphasis, emphasis: .inverse)
|
||||
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
ForEach(options.indices, id: \.self) { index in
|
||||
@ -59,7 +59,8 @@ public struct SegmentedPicker<T: Equatable>: View {
|
||||
Button {
|
||||
selection = option.1
|
||||
} label: {
|
||||
StyledLabel(option.0, .subheadingEmphasis, emphasis: .custom(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong)))
|
||||
Text(option.0)
|
||||
.styled(.subheadingEmphasis, emphasis: .custom(selection == option.1 ? .black : .white.opacity(Design.Opacity.strong)))
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
|
||||
@ -67,9 +67,9 @@ public struct SelectableRow<Badge: View>: View {
|
||||
Button(action: action) {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
StyledLabel(title, .calloutEmphasis, emphasis: .inverse)
|
||||
Text(title).styled(.calloutEmphasis, emphasis: .inverse)
|
||||
|
||||
StyledLabel(subtitle, .subheading, emphasis: .tertiary)
|
||||
Text(subtitle).styled(.subheading, emphasis: .tertiary)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
@ -67,10 +67,10 @@ public struct SettingsNavigationRow<Destination: View>: View {
|
||||
} label: {
|
||||
HStack {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
StyledLabel(title, .subheadingEmphasis)
|
||||
Text(title).styled(.subheadingEmphasis)
|
||||
|
||||
if let subtitle {
|
||||
StyledLabel(subtitle, .caption, emphasis: .secondary)
|
||||
Text(subtitle).styled(.caption, emphasis: .secondary)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -59,12 +59,12 @@ public struct SettingsRow<Accessory: View>: View {
|
||||
.background(iconColor.opacity(Design.Opacity.heavy))
|
||||
.clipShape(.rect(cornerRadius: Design.CornerRadius.xSmall))
|
||||
|
||||
StyledLabel(title, .subheading)
|
||||
Text(title).styled(.subheading)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let value {
|
||||
StyledLabel(value, .subheading, emphasis: .secondary)
|
||||
Text(value).styled(.subheading, emphasis: .secondary)
|
||||
}
|
||||
|
||||
if let accessory {
|
||||
|
||||
@ -40,8 +40,7 @@ public struct SettingsSectionHeader: View {
|
||||
}
|
||||
|
||||
Text(title)
|
||||
.font(Typography.captionEmphasis.font)
|
||||
.foregroundStyle(Theme.Text.secondary)
|
||||
.styled(.captionEmphasis, emphasis: .secondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
|
||||
|
||||
@ -79,7 +79,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
// Title row with optional accessory
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
StyledLabel(title, .subheadingEmphasis)
|
||||
Text(title).styled(.subheadingEmphasis)
|
||||
|
||||
if let titleAccessory {
|
||||
titleAccessory
|
||||
@ -88,7 +88,7 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
||||
}
|
||||
|
||||
// Subtitle
|
||||
StyledLabel(subtitle, .caption, emphasis: .secondary)
|
||||
Text(subtitle).styled(.caption, emphasis: .secondary)
|
||||
|
||||
// Segmented buttons
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
@ -97,7 +97,8 @@ public struct SettingsSegmentedPicker<T: Equatable, Accessory: View>: View {
|
||||
Button {
|
||||
selection = option.1
|
||||
} label: {
|
||||
StyledLabel(option.0, .subheadingEmphasis, emphasis: .custom(selection == option.1 ? Color.white : Theme.Text.primary))
|
||||
Text(option.0)
|
||||
.styled(.subheadingEmphasis, emphasis: .custom(selection == option.1 ? Color.white : Theme.Text.primary))
|
||||
.padding(.vertical, Design.Spacing.small)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(
|
||||
|
||||
@ -110,18 +110,17 @@ public struct SettingsSlider<Value: BinaryFloatingPoint & Sendable>: View where
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.small) {
|
||||
HStack {
|
||||
StyledLabel(title, .subheadingEmphasis)
|
||||
Text(title).styled(.subheadingEmphasis)
|
||||
|
||||
Spacer()
|
||||
|
||||
// Exception: Needs .fontDesign(.rounded) modifier
|
||||
Text(format(value))
|
||||
.font(Typography.subheadingEmphasis.font)
|
||||
.styled(.subheadingEmphasis, emphasis: .secondary)
|
||||
.fontDesign(.rounded)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
|
||||
StyledLabel(subtitle, .caption, emphasis: .secondary)
|
||||
Text(subtitle).styled(.caption, emphasis: .secondary)
|
||||
|
||||
HStack(spacing: Design.Spacing.medium) {
|
||||
if let leadingIcon {
|
||||
|
||||
@ -78,7 +78,7 @@ public struct SettingsToggle<Accessory: View>: View {
|
||||
Toggle(isOn: $isOn) {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
|
||||
HStack(spacing: Design.Spacing.xSmall) {
|
||||
StyledLabel(title, .subheadingEmphasis)
|
||||
Text(title).styled(.subheadingEmphasis)
|
||||
|
||||
if let titleAccessory {
|
||||
titleAccessory
|
||||
@ -86,7 +86,7 @@ public struct SettingsToggle<Accessory: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
StyledLabel(subtitle, .subheading, emphasis: .secondary)
|
||||
Text(subtitle).styled(.subheading, emphasis: .secondary)
|
||||
}
|
||||
}
|
||||
.tint(accentColor)
|
||||
|
||||
@ -117,14 +117,14 @@ public struct iCloudSyncSettingsView<ViewModel: CloudSyncable>: View {
|
||||
HStack(spacing: Design.Spacing.small) {
|
||||
SymbolIcon(syncStatusIcon, size: .inline, color: syncStatusColor)
|
||||
|
||||
StyledLabel(syncStatusText, .caption, emphasis: .tertiary)
|
||||
Text(syncStatusText).styled(.caption, emphasis: .tertiary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Button {
|
||||
viewModel.forceSync()
|
||||
} label: {
|
||||
StyledLabel(String(localized: "Sync Now"), .captionEmphasis, emphasis: .custom(accentColor))
|
||||
Text(String(localized: "Sync Now")).styled(.captionEmphasis, emphasis: .custom(accentColor))
|
||||
}
|
||||
}
|
||||
.padding(.top, Design.Spacing.xSmall)
|
||||
|
||||
@ -2,100 +2,70 @@
|
||||
// StyledText.swift
|
||||
// Bedrock
|
||||
//
|
||||
// Semantic text components for consistent typography.
|
||||
// Semantic text styling extensions for consistent typography.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
// MARK: - Styled Label
|
||||
// MARK: - Text Extension
|
||||
|
||||
/// A text label with semantic typography and color.
|
||||
///
|
||||
/// Use `StyledLabel` for all text that follows the typography system.
|
||||
/// It combines a `Typography` style with a `TextEmphasis` color.
|
||||
public extension Text {
|
||||
/// Applies semantic typography and emphasis styling to a Text view.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```swift
|
||||
/// StyledLabel("Morning Ritual", .heading)
|
||||
/// StyledLabel("Day 6 of 28", .caption, emphasis: .secondary)
|
||||
/// StyledLabel("Warning", .bodyEmphasis, emphasis: .custom(.red))
|
||||
/// Text("Hello, \(name)!")
|
||||
/// .styled(.bodyEmphasis, emphasis: .secondary)
|
||||
///
|
||||
/// Text(attributedString)
|
||||
/// .styled(.caption)
|
||||
///
|
||||
/// // With additional modifiers
|
||||
/// Text("HEADER")
|
||||
/// .styled(.captionEmphasis, emphasis: .secondary)
|
||||
/// .textCase(.uppercase)
|
||||
/// .tracking(0.5)
|
||||
/// ```
|
||||
public struct StyledLabel: View {
|
||||
private let text: String
|
||||
private let typography: Typography
|
||||
private let emphasis: TextEmphasis
|
||||
private let alignment: TextAlignment?
|
||||
private let lineLimit: Int?
|
||||
|
||||
/// Creates a styled label.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - text: The text to display.
|
||||
/// - typography: The typography style (default: `.body`).
|
||||
/// - emphasis: The text emphasis/color (default: `.primary`).
|
||||
/// - alignment: Optional multiline text alignment.
|
||||
/// - lineLimit: Optional line limit.
|
||||
public init(
|
||||
_ text: String,
|
||||
_ typography: Typography = .body,
|
||||
emphasis: TextEmphasis = .primary,
|
||||
alignment: TextAlignment? = nil,
|
||||
lineLimit: Int? = nil
|
||||
) {
|
||||
self.text = text
|
||||
self.typography = typography
|
||||
self.emphasis = emphasis
|
||||
self.alignment = alignment
|
||||
self.lineLimit = lineLimit
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Text(text)
|
||||
.font(typography.font)
|
||||
.foregroundStyle(emphasis.color)
|
||||
.multilineTextAlignment(alignment ?? .leading)
|
||||
.lineLimit(lineLimit)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Icon Label
|
||||
|
||||
/// A label with an icon and text, styled with semantic typography.
|
||||
///
|
||||
/// Use `IconLabel` for icon + text combinations like menu items or list rows.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```swift
|
||||
/// IconLabel("bell.fill", "Notifications", .subheading)
|
||||
/// IconLabel("star.fill", "Favorites", .body, emphasis: .secondary)
|
||||
/// ```
|
||||
public struct IconLabel: View {
|
||||
private let icon: String
|
||||
private let text: String
|
||||
private let typography: Typography
|
||||
private let emphasis: TextEmphasis
|
||||
|
||||
/// Creates an icon label.
|
||||
/// - Parameters:
|
||||
/// - icon: The SF Symbol name.
|
||||
/// - text: The text to display.
|
||||
/// - typography: The typography style (default: `.body`).
|
||||
/// - emphasis: The text emphasis/color (default: `.primary`).
|
||||
public init(
|
||||
_ icon: String,
|
||||
_ text: String,
|
||||
/// - Returns: A styled Text view.
|
||||
func styled(
|
||||
_ typography: Typography = .body,
|
||||
emphasis: TextEmphasis = .primary
|
||||
) {
|
||||
self.icon = icon
|
||||
self.text = text
|
||||
self.typography = typography
|
||||
self.emphasis = emphasis
|
||||
) -> some View {
|
||||
self
|
||||
.font(typography.font)
|
||||
.foregroundStyle(emphasis.color)
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
Label(text, systemImage: icon)
|
||||
// MARK: - Label Extension
|
||||
|
||||
public extension Label where Title == Text, Icon == Image {
|
||||
/// Applies semantic typography and emphasis styling to a Label view.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```swift
|
||||
/// Label("Notifications", systemImage: "bell.fill")
|
||||
/// .styled(.subheading, emphasis: .secondary)
|
||||
///
|
||||
/// Label("Settings", systemImage: "gear")
|
||||
/// .styled(.body)
|
||||
/// ```
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - typography: The typography style (default: `.body`).
|
||||
/// - emphasis: The text emphasis/color (default: `.primary`).
|
||||
/// - Returns: A styled Label view.
|
||||
func styled(
|
||||
_ typography: Typography = .body,
|
||||
emphasis: TextEmphasis = .primary
|
||||
) -> some View {
|
||||
self
|
||||
.font(typography.font)
|
||||
.foregroundStyle(emphasis.color)
|
||||
}
|
||||
@ -137,8 +107,7 @@ public struct SectionHeader: View {
|
||||
}
|
||||
|
||||
Text(title)
|
||||
.font(Typography.caption.font)
|
||||
.foregroundStyle(Theme.Text.secondary)
|
||||
.styled(.caption, emphasis: .secondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
}
|
||||
@ -147,41 +116,57 @@ public struct SectionHeader: View {
|
||||
|
||||
// MARK: - Previews
|
||||
|
||||
#Preview("Styled Labels") {
|
||||
#Preview("Text.styled()") {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
Group {
|
||||
StyledLabel("Hero Bold", .heroBold)
|
||||
StyledLabel("Title Bold", .titleBold)
|
||||
StyledLabel("Title 2", .title2)
|
||||
StyledLabel("Heading", .heading)
|
||||
StyledLabel("Heading Emphasis", .headingEmphasis)
|
||||
Text("Hero Bold").styled(.heroBold)
|
||||
Text("Title Bold").styled(.titleBold)
|
||||
Text("Title 2").styled(.title2)
|
||||
Text("Heading").styled(.heading)
|
||||
Text("Heading Emphasis").styled(.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)
|
||||
Text("Body").styled(.body)
|
||||
Text("Body Emphasis").styled(.bodyEmphasis)
|
||||
Text("Subheading").styled(.subheading, emphasis: .secondary)
|
||||
Text("Caption").styled(.caption, emphasis: .secondary)
|
||||
Text("Caption 2").styled(.caption2, emphasis: .tertiary)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Group {
|
||||
StyledLabel("Custom Color", .body, emphasis: .custom(.orange))
|
||||
StyledLabel("Disabled", .body, emphasis: .disabled)
|
||||
Text("Custom Color").styled(.body, emphasis: .custom(.orange))
|
||||
Text("Disabled").styled(.body, emphasis: .disabled)
|
||||
}
|
||||
|
||||
Divider()
|
||||
|
||||
Group {
|
||||
Text("Count: \(42)").styled(.heading)
|
||||
Text("UPPERCASE")
|
||||
.styled(.captionEmphasis, emphasis: .secondary)
|
||||
.textCase(.uppercase)
|
||||
.tracking(0.5)
|
||||
Text("$9.99")
|
||||
.styled(.subheadingEmphasis)
|
||||
.fontDesign(.rounded)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
#Preview("Icon Labels") {
|
||||
#Preview("Label.styled()") {
|
||||
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
|
||||
IconLabel("bell.fill", "Notifications", .subheading)
|
||||
IconLabel("star.fill", "Favorites", .body, emphasis: .secondary)
|
||||
IconLabel("gear", "Settings", .caption)
|
||||
Label("Notifications", systemImage: "bell.fill")
|
||||
.styled(.subheading)
|
||||
Label("Favorites", systemImage: "star.fill")
|
||||
.styled(.body, emphasis: .secondary)
|
||||
Label("Settings", systemImage: "gear")
|
||||
.styled(.caption)
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user