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