Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>

This commit is contained in:
Matt Bruce 2026-01-27 11:16:09 -06:00
parent 76d484a1b3
commit 90366720c0
12 changed files with 138 additions and 435 deletions

View File

@ -3,27 +3,11 @@
"strings" : { "strings" : {
"" : { "" : {
},
" : " : {
"comment" : "A separator between the time of day and its range in the RitualDetailView.",
"isCommentAutoGenerated" : true
}, },
"-%lld%% vs last week" : { "-%lld%% vs last week" : {
"comment" : "A description of how a user's usage has changed compared to the previous week. The argument is the percentage by which the usage has increased or decreased.", "comment" : "A description of how a user's usage has changed compared to the previous week. The argument is the percentage by which the usage has increased or decreased.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"%@ %@" : {
"comment" : "A sublabel that shows the start and end dates of an arc. The date format used is \"MMM d, yyyy\".",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ %2$@"
}
}
}
},
"%@, Day %lld" : { "%@, Day %lld" : {
"comment" : "A view representing a milestone achievement in a ritual journey. The first argument is the title of the milestone. The second argument is the day on which the milestone was achieved.", "comment" : "A view representing a milestone achievement in a ritual journey. The first argument is the title of the milestone. The second argument is the day on which the milestone was achieved.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@ -40,18 +24,6 @@
"comment" : "A text view displaying the day number in a history calendar cell. The text is centered and has a small font size.", "comment" : "A text view displaying the day number in a history calendar cell. The text is centered and has a small font size.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"%lld arc%@" : {
"comment" : "A badge displaying the number of completed arcs for a ritual. The argument is the count of completed arcs.",
"isCommentAutoGenerated" : true,
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$lld arc%2$@"
}
}
}
},
"%lld days" : { "%lld days" : {
"comment" : "A label displaying the duration of a preset in days. The argument is the number of days the preset is active.", "comment" : "A label displaying the duration of a preset in days. The argument is the number of days the preset is active.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
@ -154,10 +126,6 @@
} }
} }
}, },
"%lld%% complete" : {
"comment" : "A text label showing the percentage of the ritual that has been completed.",
"isCommentAutoGenerated" : true
},
"%lld%% completion over %lld days" : { "%lld%% completion over %lld days" : {
"comment" : "A string summarizing the completion rate of a ritual arc. The first argument is the completion rate, expressed as a percentage. The second argument is the duration of the arc in days.", "comment" : "A string summarizing the completion rate of a ritual arc. The first argument is the completion rate, expressed as a percentage. The second argument is the duration of the arc in days.",
"isCommentAutoGenerated" : true, "isCommentAutoGenerated" : true,
@ -733,10 +701,6 @@
"comment" : "Label for the x-axis in the mini sparkline chart within an `InsightCardView`.", "comment" : "Label for the x-axis in the mini sparkline chart within an `InsightCardView`.",
"isCommentAutoGenerated" : true "isCommentAutoGenerated" : true
}, },
"Day %lld" : {
"comment" : "A small label displaying the day and title of a milestone. The first argument is the day of the milestone. The second argument is the title of the milestone.",
"isCommentAutoGenerated" : true
},
"Day %lld of %lld" : { "Day %lld of %lld" : {
"localizations" : { "localizations" : {
"en" : { "en" : {

View File

@ -101,18 +101,11 @@ struct InsightDetailSheet: View {
private var trendIndicatorSection: some View { private var trendIndicatorSection: some View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
// Trend direction badge // Trend direction badge
HStack(spacing: Design.Spacing.xSmall) { IconLabel(trendDirection.symbolName, trendDirection.accessibilityLabel, .caption, emphasis: .custom(trendDirection.color))
Image(systemName: trendDirection.symbolName) .padding(.horizontal, Design.Spacing.medium)
.font(.caption) .padding(.vertical, Design.Spacing.small)
.background(trendDirection.color.opacity(Design.Opacity.subtle))
Text(trendDirection.accessibilityLabel) .clipShape(.capsule)
.font(.caption)
}
.foregroundStyle(trendDirection.color)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(trendDirection.color.opacity(Design.Opacity.subtle))
.clipShape(.capsule)
// Week-over-week change // Week-over-week change
if abs(weekOverWeekChange) > 0.01 { if abs(weekOverWeekChange) > 0.01 {
@ -121,9 +114,7 @@ struct InsightDetailSheet: View {
? String(localized: "+\(changePercent)% vs last week") ? String(localized: "+\(changePercent)% vs last week")
: String(localized: "-\(changePercent)% vs last week") : String(localized: "-\(changePercent)% vs last week")
Text(changeText) StyledLabel(changeText, .caption, emphasis: .custom(AppTextColors.secondary))
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
} }
Spacer() Spacer()
@ -135,22 +126,15 @@ struct InsightDetailSheet: View {
private var tipsSection: some View { private var tipsSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Label(String(localized: "Tips"), systemImage: "lightbulb.fill") IconLabel("lightbulb.fill", String(localized: "Tips"), .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
ForEach(tips, id: \.self) { tip in ForEach(tips, id: \.self) { tip in
HStack(alignment: .top, spacing: Design.Spacing.small) { HStack(alignment: .top, spacing: Design.Spacing.small) {
Image(systemName: "chevron.right") SymbolIcon("chevron.right", size: .badge, color: AppAccent.primary)
.font(.caption2)
.foregroundStyle(AppAccent.primary)
.padding(.top, Design.Spacing.xSmall) .padding(.top, Design.Spacing.xSmall)
Text(tip) StyledLabel(tip, .subheading, emphasis: .custom(AppTextColors.secondary))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
.fixedSize(horizontal: false, vertical: true)
} }
} }
} }
@ -165,14 +149,9 @@ struct InsightDetailSheet: View {
private var explanationSection: some View { private var explanationSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "What this means")) StyledLabel(String(localized: "What this means"), .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Text(card.explanation) StyledLabel(card.explanation, .body, emphasis: .custom(AppTextColors.secondary))
.font(.body)
.foregroundStyle(AppTextColors.secondary)
.fixedSize(horizontal: false, vertical: true)
} }
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@ -184,9 +163,7 @@ struct InsightDetailSheet: View {
private func chartSection(_ data: [TrendDataPoint]) -> some View { private func chartSection(_ data: [TrendDataPoint]) -> some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "7-Day Trend")) StyledLabel(String(localized: "7-Day Trend"), .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Chart(data) { point in Chart(data) { point in
BarMark( BarMark(
@ -205,8 +182,9 @@ struct InsightDetailSheet: View {
AxisMarks(values: [0, 0.5, 1.0]) { value in AxisMarks(values: [0, 0.5, 1.0]) { value in
AxisValueLabel { AxisValueLabel {
if let v = value.as(Double.self) { if let v = value.as(Double.self) {
// Exception: Inside AxisValueLabel closure
Text("\(Int(v * 100))%") Text("\(Int(v * 100))%")
.font(.caption2) .font(Typography.caption2.font)
.foregroundStyle(AppTextColors.tertiary) .foregroundStyle(AppTextColors.tertiary)
} }
} }
@ -215,9 +193,10 @@ struct InsightDetailSheet: View {
} }
} }
.chartXAxis { .chartXAxis {
AxisMarks { value in AxisMarks { _ in
// Exception: AxisValueLabel doesn't support StyledLabel
AxisValueLabel() AxisValueLabel()
.font(.caption2) .font(Typography.caption2.font)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
} }
@ -235,22 +214,16 @@ struct InsightDetailSheet: View {
private func breakdownSection(_ breakdown: [BreakdownItem]) -> some View { private func breakdownSection(_ breakdown: [BreakdownItem]) -> some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "Breakdown")) StyledLabel(String(localized: "Breakdown"), .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
VStack(spacing: Design.Spacing.xSmall) { VStack(spacing: Design.Spacing.xSmall) {
ForEach(breakdown) { item in ForEach(breakdown) { item in
HStack { HStack {
Text(item.label) StyledLabel(item.label, .subheading, emphasis: .custom(AppTextColors.primary))
.font(.subheadline)
.foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text(item.value) StyledLabel(item.value, .subheading, emphasis: .custom(AppTextColors.secondary))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
} }
.padding(.vertical, Design.Spacing.small) .padding(.vertical, Design.Spacing.small)

View File

@ -55,29 +55,19 @@ struct FirstCheckInStepView: View {
private var tutorialView: some View { private var tutorialView: some View {
VStack(spacing: Design.Spacing.xxLarge) { VStack(spacing: Design.Spacing.xxLarge) {
// Header // Header
Text(String(localized: "Let's try it out!")) StyledLabel(String(localized: "Let's try it out!"), .title2Bold, emphasis: .custom(AppTextColors.primary), alignment: .center)
.font(.title2)
.fontWeight(.bold)
.foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center)
.opacity(animateContent ? 1 : 0) .opacity(animateContent ? 1 : 0)
// Ritual card with habits // Ritual card with habits
VStack(alignment: .leading, spacing: Design.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
// Header // Header
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: ritual.iconName) SymbolIcon(ritual.iconName, size: .row, color: AppAccent.primary)
.font(.headline)
.foregroundStyle(AppAccent.primary)
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text(String(localized: "Day 1 of \(ritual.durationDays)")) StyledLabel(String(localized: "Day 1 of \(ritual.durationDays)"), .caption, emphasis: .custom(AppTextColors.secondary))
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
Text(ritual.title) StyledLabel(ritual.title, .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
} }
Spacer() Spacer()
@ -103,18 +93,14 @@ struct FirstCheckInStepView: View {
.offset(y: animateContent ? 0 : 20) .offset(y: animateContent ? 0 : 20)
// Instruction // Instruction
Text(String(localized: "Tap a habit to check in")) StyledLabel(String(localized: "Tap a habit to check in"), .subheading, emphasis: .custom(AppTextColors.secondary))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
.opacity(animateContent ? 1 : 0) .opacity(animateContent ? 1 : 0)
Spacer() Spacer()
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
Button(action: onComplete) { Button(action: onComplete) {
Text(String(localized: "Continue")) StyledLabel(String(localized: "Continue"), .heading, emphasis: .custom(AppTextColors.inverse))
.font(.headline)
.foregroundStyle(AppTextColors.inverse)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: AppMetrics.Size.buttonHeight) .frame(height: AppMetrics.Size.buttonHeight)
.background(AppAccent.primary) .background(AppAccent.primary)
@ -122,9 +108,7 @@ struct FirstCheckInStepView: View {
} }
Button(action: onComplete) { Button(action: onComplete) {
Text(String(localized: "Skip for now")) StyledLabel(String(localized: "Skip for now"), .subheading, emphasis: .custom(AppTextColors.secondary))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
} }
} }
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)
@ -151,15 +135,9 @@ struct FirstCheckInStepView: View {
.opacity(showCelebration ? 1 : 0) .opacity(showCelebration ? 1 : 0)
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
Text(String(localized: "Nice work!")) StyledLabel(String(localized: "Nice work!"), .heroBold, emphasis: .custom(AppTextColors.primary))
.font(.largeTitle)
.fontWeight(.bold)
.foregroundStyle(AppTextColors.primary)
Text(String(localized: "You completed your first check-in")) StyledLabel(String(localized: "You completed your first check-in"), .title3, emphasis: .custom(AppTextColors.secondary), alignment: .center)
.font(.title3)
.foregroundStyle(AppTextColors.secondary)
.multilineTextAlignment(.center)
} }
Spacer() Spacer()
@ -167,9 +145,7 @@ struct FirstCheckInStepView: View {
// Continue button // Continue button
if showContinueButton { if showContinueButton {
Button(action: onComplete) { Button(action: onComplete) {
Text(String(localized: "Continue to Rituals")) StyledLabel(String(localized: "Continue to Rituals"), .heading, emphasis: .custom(AppTextColors.inverse))
.font(.headline)
.foregroundStyle(AppTextColors.inverse)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: AppMetrics.Size.buttonHeight) .frame(height: AppMetrics.Size.buttonHeight)
.background(AppAccent.primary) .background(AppAccent.primary)
@ -215,20 +191,14 @@ private struct OnboardingHabitRowView: View {
var body: some View { var body: some View {
Button(action: action) { Button(action: action) {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Image(systemName: symbolName) SymbolIcon(symbolName, size: .row, color: isCompleted ? AppStatus.success : AppAccent.primary)
.font(.title3)
.foregroundStyle(isCompleted ? AppStatus.success : AppAccent.primary)
.frame(width: AppMetrics.Size.iconLarge) .frame(width: AppMetrics.Size.iconLarge)
Text(title) StyledLabel(title, .body, emphasis: .custom(AppTextColors.primary))
.font(.body)
.foregroundStyle(AppTextColors.primary)
Spacer(minLength: Design.Spacing.medium) Spacer(minLength: Design.Spacing.medium)
Image(systemName: isCompleted ? "checkmark.circle.fill" : "circle") SymbolIcon(isCompleted ? "checkmark.circle.fill" : "circle", size: .row, color: isCompleted ? AppStatus.success : AppBorder.subtle)
.font(.title3)
.foregroundStyle(isCompleted ? AppStatus.success : AppBorder.subtle)
} }
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.medium) .padding(.vertical, Design.Spacing.medium)

View File

@ -26,30 +26,19 @@ struct RitualPreviewStepView: View {
.frame(height: Design.Spacing.large) .frame(height: Design.Spacing.large)
// Header // Header
Text(headerText) StyledLabel(headerText, .title2Bold, emphasis: .custom(AppTextColors.primary), alignment: .center)
.font(.title2)
.fontWeight(.bold)
.foregroundStyle(AppTextColors.primary)
.multilineTextAlignment(.center)
.opacity(animateContent ? 1 : 0) .opacity(animateContent ? 1 : 0)
// Ritual preview card // Ritual preview card
VStack(alignment: .leading, spacing: Design.Spacing.large) { VStack(alignment: .leading, spacing: Design.Spacing.large) {
// Title and theme // Title and theme
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Image(systemName: preset.iconName) SymbolIcon(preset.iconName, size: .card, color: AppAccent.primary)
.font(.title)
.foregroundStyle(AppAccent.primary)
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(preset.title) StyledLabel(preset.title, .title3Bold, emphasis: .custom(AppTextColors.primary))
.font(.title3)
.fontWeight(.semibold)
.foregroundStyle(AppTextColors.primary)
Text(preset.theme) StyledLabel(preset.theme, .subheading, emphasis: .custom(AppTextColors.secondary))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
} }
} }
@ -60,14 +49,10 @@ struct RitualPreviewStepView: View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
ForEach(preset.habits) { habit in ForEach(preset.habits) { habit in
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: habit.symbolName) SymbolIcon(habit.symbolName, size: .inline, color: AppTextColors.secondary)
.font(.body)
.foregroundStyle(AppTextColors.secondary)
.frame(width: 24) .frame(width: 24)
Text(habit.title) StyledLabel(habit.title, .body, emphasis: .custom(AppTextColors.primary))
.font(.body)
.foregroundStyle(AppTextColors.primary)
} }
} }
} }
@ -77,23 +62,9 @@ struct RitualPreviewStepView: View {
// Duration and time // Duration and time
HStack(spacing: Design.Spacing.large) { HStack(spacing: Design.Spacing.large) {
Label { IconLabel("calendar", String(localized: "\(preset.durationDays) days"), .caption, emphasis: .custom(AppTextColors.secondary))
Text(String(localized: "\(preset.durationDays) days"))
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
} icon: {
Image(systemName: "calendar")
.foregroundStyle(AppTextColors.tertiary)
}
Label { IconLabel(preset.timeOfDay.symbolName, preset.timeOfDay.displayName, .caption, emphasis: .custom(AppTextColors.secondary))
Text(preset.timeOfDay.displayName)
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
} icon: {
Image(systemName: preset.timeOfDay.symbolName)
.foregroundStyle(AppTextColors.tertiary)
}
} }
} }
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
@ -109,9 +80,7 @@ struct RitualPreviewStepView: View {
VStack(spacing: Design.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
// Primary CTA // Primary CTA
Button(action: onStartRitual) { Button(action: onStartRitual) {
Text(String(localized: "Start This Ritual")) StyledLabel(String(localized: "Start This Ritual"), .heading, emphasis: .custom(AppTextColors.inverse))
.font(.headline)
.foregroundStyle(AppTextColors.inverse)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: AppMetrics.Size.buttonHeight) .frame(height: AppMetrics.Size.buttonHeight)
.background(AppAccent.primary) .background(AppAccent.primary)
@ -120,9 +89,7 @@ struct RitualPreviewStepView: View {
// Skip option // Skip option
Button(action: onSkip) { Button(action: onSkip) {
Text(String(localized: "Skip for now")) StyledLabel(String(localized: "Skip for now"), .subheading, emphasis: .custom(AppTextColors.secondary))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
} }
} }
.padding(.horizontal, Design.Spacing.xxLarge) .padding(.horizontal, Design.Spacing.xxLarge)

View File

@ -62,9 +62,7 @@ struct WhatsNextStepView: View {
// CTA button // CTA button
Button(action: onComplete) { Button(action: onComplete) {
Text(String(localized: "Let's Go")) StyledLabel(String(localized: "Let's Go"), .heading, emphasis: .custom(AppTextColors.inverse))
.font(.headline)
.foregroundStyle(AppTextColors.inverse)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.frame(height: AppMetrics.Size.buttonHeight) .frame(height: AppMetrics.Size.buttonHeight)
.background(AppAccent.primary) .background(AppAccent.primary)
@ -94,23 +92,16 @@ private struct FeatureHelpCard: View {
var body: some View { var body: some View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
// Icon // Icon
Image(systemName: icon) SymbolIcon(icon, size: .row, color: AppAccent.primary)
.font(.title2)
.foregroundStyle(AppAccent.primary)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background(AppAccent.primary.opacity(0.15)) .background(AppAccent.primary.opacity(0.15))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) .clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
// Text // Text
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(title) StyledLabel(title, .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Text(description) StyledLabel(description, .caption, emphasis: .custom(AppTextColors.secondary))
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
.fixedSize(horizontal: false, vertical: true)
} }
Spacer() Spacer()

View File

@ -29,9 +29,7 @@ struct HabitPerformanceView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "Habit Performance")) StyledLabel(String(localized: "Habit Performance"), .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
LazyVGrid(columns: habitColumns, alignment: .leading, spacing: Design.Spacing.xSmall) { LazyVGrid(columns: habitColumns, alignment: .leading, spacing: Design.Spacing.xSmall) {
ForEach(sortedByRate, id: \.habit.id) { item in ForEach(sortedByRate, id: \.habit.id) { item in
@ -49,20 +47,14 @@ struct HabitPerformanceView: View {
private func habitRow(_ habit: ArcHabit, rate: Double) -> some View { private func habitRow(_ habit: ArcHabit, rate: Double) -> some View {
VStack(spacing: Design.Spacing.xSmall) { VStack(spacing: Design.Spacing.xSmall) {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Image(systemName: habit.symbolName) SymbolIcon(habit.symbolName, size: .inline, color: colorForRate(rate))
.foregroundStyle(colorForRate(rate))
.frame(width: AppMetrics.Size.iconMedium) .frame(width: AppMetrics.Size.iconMedium)
Text(habit.title) StyledLabel(habit.title, .subheading, emphasis: .custom(AppTextColors.primary))
.font(.subheadline)
.foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
Text("\(Int(rate * 100))%") StyledLabel("\(Int(rate * 100))%", .subheadingEmphasis, emphasis: .custom(colorForRate(rate)))
.font(.subheadline)
.bold()
.foregroundStyle(colorForRate(rate))
} }
// Progress bar // Progress bar

View File

@ -14,9 +14,7 @@ struct RitualMilestonesView: View {
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "Milestones")) StyledLabel(String(localized: "Milestones"), .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
ForEach(milestones) { milestone in ForEach(milestones) { milestone in
@ -37,20 +35,12 @@ struct RitualMilestonesView: View {
.fill(milestone.isAchieved ? AppStatus.success.opacity(Design.Opacity.medium) : AppSurface.secondary) .fill(milestone.isAchieved ? AppStatus.success.opacity(Design.Opacity.medium) : AppSurface.secondary)
.frame(width: AppMetrics.Size.milestoneIcon, height: AppMetrics.Size.milestoneIcon) .frame(width: AppMetrics.Size.milestoneIcon, height: AppMetrics.Size.milestoneIcon)
Image(systemName: milestone.symbolName) SymbolIcon(milestone.symbolName, size: .badge, color: milestone.isAchieved ? AppStatus.success : AppTextColors.tertiary)
.font(.caption)
.foregroundStyle(milestone.isAchieved ? AppStatus.success : AppTextColors.tertiary)
} }
Text("Day \(milestone.day)") StyledLabel("Day \(milestone.day)", .caption2, emphasis: .custom(milestone.isAchieved ? AppTextColors.primary : AppTextColors.tertiary))
.font(.caption2)
.foregroundStyle(milestone.isAchieved ? AppTextColors.primary : AppTextColors.tertiary)
Text(milestone.title) StyledLabel(milestone.title, .caption2, emphasis: .custom(milestone.isAchieved ? AppTextColors.secondary : AppTextColors.tertiary), lineLimit: 1)
.font(.caption2)
.foregroundStyle(milestone.isAchieved ? AppTextColors.secondary : AppTextColors.tertiary)
.lineLimit(1)
.minimumScaleFactor(0.8)
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.accessibilityElement(children: .combine) .accessibilityElement(children: .combine)

View File

@ -40,9 +40,7 @@ struct RitualProgressStatsView: View {
ProgressView(value: progress) ProgressView(value: progress)
.tint(AppAccent.primary) .tint(AppAccent.primary)
Text("\(Int(progress * 100))% complete") StyledLabel("\(Int(progress * 100))% complete", .caption, emphasis: .custom(AppTextColors.secondary))
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
} }
} }
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
@ -53,19 +51,12 @@ struct RitualProgressStatsView: View {
private func statColumn(value: String, secondary: String?, label: String) -> some View { private func statColumn(value: String, secondary: String?, label: String) -> some View {
VStack(spacing: Design.Spacing.xSmall) { VStack(spacing: Design.Spacing.xSmall) {
Text(value) StyledLabel(value, .titleBold, emphasis: .custom(AppTextColors.primary))
.font(.title)
.fontWeight(.bold)
.foregroundStyle(AppTextColors.primary)
// Always show secondary row for consistent height // Always show secondary row for consistent height
Text(secondary ?? " ") StyledLabel(secondary ?? " ", .caption, emphasis: .custom(secondary != nil ? AppTextColors.tertiary : .clear))
.font(.caption)
.foregroundStyle(secondary != nil ? AppTextColors.tertiary : .clear)
Text(label) StyledLabel(label, .caption2, emphasis: .custom(AppTextColors.secondary))
.font(.caption2)
.foregroundStyle(AppTextColors.secondary)
} }
} }
} }

View File

@ -108,9 +108,7 @@ struct RitualDetailView: View {
if !ritual.notes.isEmpty { if !ritual.notes.isEmpty {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
SectionHeaderView(title: String(localized: "Notes")) SectionHeaderView(title: String(localized: "Notes"))
Text(ritual.notes) StyledLabel(ritual.notes, .body, emphasis: .custom(AppTextColors.secondary))
.font(.body)
.foregroundStyle(AppTextColors.secondary)
} }
} }
@ -258,11 +256,8 @@ struct RitualDetailView: View {
// Summary card // Summary card
VStack(alignment: .leading, spacing: Design.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
HStack { HStack {
Image(systemName: "clock.arrow.circlepath") SymbolIcon("clock.arrow.circlepath", size: .inline, color: AppTextColors.secondary)
.foregroundStyle(AppTextColors.secondary) StyledLabel(String(localized: "This ritual is not currently active"), .subheading, emphasis: .custom(AppTextColors.secondary))
Text(String(localized: "This ritual is not currently active"))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
} }
if let lastArc = ritual.latestArc { if let lastArc = ritual.latestArc {
@ -271,9 +266,7 @@ struct RitualDetailView: View {
let possibleCheckIns = habits.count * lastArc.durationDays let possibleCheckIns = habits.count * lastArc.durationDays
let completionRate = possibleCheckIns > 0 ? Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100) : 0 let completionRate = possibleCheckIns > 0 ? Int(Double(totalCheckIns) / Double(possibleCheckIns) * 100) : 0
Text(String(localized: "Last arc completed with \(completionRate)% habit completion over \(lastArc.durationDays) days.")) StyledLabel(String(localized: "Last arc completed with \(completionRate)% habit completion over \(lastArc.durationDays) days."), .caption, emphasis: .custom(AppTextColors.tertiary))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} }
Button { Button {
@ -319,21 +312,17 @@ struct RitualDetailView: View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
// Current arc indicator // Current arc indicator
if let arc = ritual.currentArc { if let arc = ritual.currentArc {
Text(String(localized: "Arc \(arc.arcNumber)")) StyledLabel(String(localized: "Arc \(arc.arcNumber)"), .captionEmphasis, emphasis: .custom(AppAccent.primary))
.font(.caption.bold())
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background(AppAccent.primary.opacity(0.2)) .background(AppAccent.primary.opacity(0.2))
.clipShape(.capsule) .clipShape(.capsule)
.foregroundStyle(AppAccent.primary)
} else { } else {
Text(String(localized: "No active arc")) StyledLabel(String(localized: "No active arc"), .caption, emphasis: .custom(AppTextColors.tertiary))
.font(.caption)
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background(AppTextColors.tertiary.opacity(0.2)) .background(AppTextColors.tertiary.opacity(0.2))
.clipShape(.capsule) .clipShape(.capsule)
.foregroundStyle(AppTextColors.tertiary)
} }
// Time of day badge // Time of day badge
@ -342,13 +331,11 @@ struct RitualDetailView: View {
// Category badge (if set) // Category badge (if set)
if !ritual.category.isEmpty { if !ritual.category.isEmpty {
let badgeColor = categoryStore.color(for: ritual.category) let badgeColor = categoryStore.color(for: ritual.category)
Text(ritual.category) StyledLabel(ritual.category, .caption, emphasis: .custom(badgeColor))
.font(.caption)
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background(badgeColor.opacity(0.15)) .background(badgeColor.opacity(0.15))
.clipShape(.capsule) .clipShape(.capsule)
.foregroundStyle(badgeColor)
} }
Spacer() Spacer()
@ -357,15 +344,10 @@ struct RitualDetailView: View {
private var timeOfDayBadge: some View { private var timeOfDayBadge: some View {
HStack(spacing: 2) { HStack(spacing: 2) {
HStack(spacing: Design.Spacing.xSmall) { IconLabel(ritual.timeOfDay.symbolName, ritual.timeOfDay.displayName, .caption2, emphasis: .custom(timeOfDayColor))
Image(systemName: ritual.timeOfDay.symbolName) StyledLabel(" : ", .caption2, emphasis: .custom(timeOfDayColor))
Text(ritual.timeOfDay.displayName) StyledLabel(ritual.timeOfDay.timeRange, .caption2, emphasis: .custom(timeOfDayColor))
}
Text(" : ")
Text(ritual.timeOfDay.timeRange)
} }
.font(.caption2)
.foregroundStyle(timeOfDayColor)
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background(timeOfDayColor.opacity(0.15)) .background(timeOfDayColor.opacity(0.15))
@ -399,9 +381,7 @@ struct RitualDetailView: View {
) )
if completedArcs.isEmpty { if completedArcs.isEmpty {
Text(String(localized: "No completed arcs yet.")) StyledLabel(String(localized: "No completed arcs yet."), .caption, emphasis: .custom(AppTextColors.tertiary))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} else { } else {
LazyVGrid(columns: arcColumns, alignment: .leading, spacing: Design.Spacing.small) { LazyVGrid(columns: arcColumns, alignment: .leading, spacing: Design.Spacing.small) {
ForEach(completedArcs) { arc in ForEach(completedArcs) { arc in
@ -427,30 +407,20 @@ struct RitualDetailView: View {
} label: { } label: {
HStack { HStack {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(String(localized: "Arc \(arc.arcNumber)")) StyledLabel(String(localized: "Arc \(arc.arcNumber)"), .subheadingEmphasis, emphasis: .custom(AppTextColors.primary))
.font(.subheadline.bold())
.foregroundStyle(AppTextColors.primary)
Text("\(dateFormatter.string(from: arc.startDate)) \(dateFormatter.string(from: arc.endDate))") StyledLabel("\(dateFormatter.string(from: arc.startDate)) \(dateFormatter.string(from: arc.endDate))", .caption, emphasis: .custom(AppTextColors.tertiary))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} }
Spacer() Spacer()
VStack(alignment: .trailing, spacing: Design.Spacing.xSmall) { VStack(alignment: .trailing, spacing: Design.Spacing.xSmall) {
Text("\(completionRate)%") StyledLabel("\(completionRate)%", .subheadingEmphasis, emphasis: .custom(completionRate >= 70 ? AppStatus.success : AppTextColors.secondary))
.font(.subheadline.bold())
.foregroundStyle(completionRate >= 70 ? AppStatus.success : AppTextColors.secondary)
Text(String(localized: "\(arc.durationDays) days")) StyledLabel(String(localized: "\(arc.durationDays) days"), .caption, emphasis: .custom(AppTextColors.tertiary))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} }
Image(systemName: "chevron.right") SymbolIcon.chevron(color: AppTextColors.tertiary)
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} }
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background(AppSurface.card) .background(AppSurface.card)
@ -466,44 +436,26 @@ struct RitualDetailView: View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
// Days remaining // Days remaining
if daysRemaining > 0 { if daysRemaining > 0 {
HStack(spacing: Design.Spacing.xSmall) { IconLabel("hourglass", String(localized: "\(daysRemaining) days remaining"), .caption, emphasis: .custom(AppTextColors.secondary))
Image(systemName: "hourglass") .padding(.horizontal, Design.Spacing.medium)
.font(.caption) .padding(.vertical, Design.Spacing.small)
Text(String(localized: "\(daysRemaining) days remaining")) .background(AppSurface.card)
.font(.caption) .clipShape(.capsule)
}
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(AppSurface.card)
.clipShape(.capsule)
} else { } else {
HStack(spacing: Design.Spacing.xSmall) { IconLabel("checkmark.seal.fill", String(localized: "Arc complete!"), .caption, emphasis: .custom(AppStatus.success))
Image(systemName: "checkmark.seal.fill") .padding(.horizontal, Design.Spacing.medium)
.font(.caption) .padding(.vertical, Design.Spacing.small)
Text(String(localized: "Arc complete!")) .background(AppStatus.success.opacity(Design.Opacity.subtle))
.font(.caption) .clipShape(.capsule)
}
.foregroundStyle(AppStatus.success)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(AppStatus.success.opacity(Design.Opacity.subtle))
.clipShape(.capsule)
} }
// Ritual streak // Ritual streak
if ritualStreak > 0 { if ritualStreak > 0 {
HStack(spacing: Design.Spacing.xSmall) { IconLabel("flame.fill", String(localized: "\(ritualStreak)-day streak"), .caption, emphasis: .custom(AppStatus.success))
Image(systemName: "flame.fill") .padding(.horizontal, Design.Spacing.medium)
.font(.caption) .padding(.vertical, Design.Spacing.small)
Text(String(localized: "\(ritualStreak)-day streak")) .background(AppStatus.success.opacity(Design.Opacity.subtle))
.font(.caption) .clipShape(.capsule)
}
.foregroundStyle(AppStatus.success)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(AppStatus.success.opacity(Design.Opacity.subtle))
.clipShape(.capsule)
} }
Spacer() Spacer()
@ -511,15 +463,8 @@ struct RitualDetailView: View {
// Arc comparison (only shown if there's a previous arc) // Arc comparison (only shown if there's a previous arc)
if let comparison = arcComparisonInfo { if let comparison = arcComparisonInfo {
HStack(spacing: Design.Spacing.xSmall) { let comparisonColor = comparison.isAhead ? AppStatus.success : comparison.isBehind ? AppStatus.warning : AppTextColors.secondary
Image(systemName: comparison.isAhead ? "arrow.up.right" : IconLabel(comparison.isAhead ? "arrow.up.right" : comparison.isBehind ? "arrow.down.right" : "equal", comparison.text, .caption, emphasis: .custom(comparisonColor))
comparison.isBehind ? "arrow.down.right" : "equal")
.font(.caption)
Text(comparison.text)
.font(.caption)
}
.foregroundStyle(comparison.isAhead ? AppStatus.success :
comparison.isBehind ? AppStatus.warning : AppTextColors.secondary)
} }
} }
} }

View File

@ -140,12 +140,8 @@ struct RitualsView: View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
// Time of day header // Time of day header
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: group.timeOfDay.symbolName) SymbolIcon(group.timeOfDay.symbolName, size: .inline, color: AppAccent.primary)
.foregroundStyle(AppAccent.primary) StyledLabel(group.timeOfDay.displayName, .subheadingEmphasis, emphasis: .custom(AppTextColors.secondary))
Text(group.timeOfDay.displayName)
.font(.subheadline)
.bold()
.foregroundStyle(AppTextColors.secondary)
} }
.padding(.top, Design.Spacing.small) .padding(.top, Design.Spacing.small)
@ -322,29 +318,18 @@ struct RitualsView: View {
private func pastRitualCardView(for ritual: Ritual) -> some View { private func pastRitualCardView(for ritual: Ritual) -> some View {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
// Icon // Icon
Image(systemName: ritual.iconName) SymbolIcon(ritual.iconName, size: .row, color: AppTextColors.secondary)
.font(.title2)
.foregroundStyle(AppTextColors.secondary)
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.background(AppSurface.secondary) .background(AppSurface.secondary)
.clipShape(.rect(cornerRadius: Design.CornerRadius.small)) .clipShape(.rect(cornerRadius: Design.CornerRadius.small))
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(ritual.title) StyledLabel(ritual.title, .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Text(ritual.theme) StyledLabel(ritual.theme, .caption, emphasis: .custom(AppTextColors.secondary))
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
if let lastArc = ritual.latestArc { if let lastArc = ritual.latestArc {
HStack(spacing: Design.Spacing.xSmall) { IconLabel("calendar", formattedEndDate(lastArc.endDate), .caption2, emphasis: .custom(AppTextColors.tertiary))
Image(systemName: "calendar")
Text(formattedEndDate(lastArc.endDate))
}
.font(.caption2)
.foregroundStyle(AppTextColors.tertiary)
} }
} }
@ -352,18 +337,14 @@ struct RitualsView: View {
// Arc count badge // Arc count badge
if ritual.completedArcCount > 0 { if ritual.completedArcCount > 0 {
Text("\(ritual.completedArcCount) arc\(ritual.completedArcCount == 1 ? "" : "s")") StyledLabel("\(ritual.completedArcCount) arc\(ritual.completedArcCount == 1 ? "" : "s")", .caption2, emphasis: .custom(AppTextColors.secondary))
.font(.caption2)
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background(AppSurface.secondary) .background(AppSurface.secondary)
.clipShape(.capsule) .clipShape(.capsule)
} }
Image(systemName: "chevron.right") SymbolIcon.chevron(color: AppTextColors.tertiary)
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} }
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background(AppSurface.card) .background(AppSurface.card)

View File

@ -60,16 +60,11 @@ struct PresetLibrarySheet: View {
selectedCategory = category selectedCategory = category
} }
} label: { } label: {
HStack(spacing: Design.Spacing.xSmall) { IconLabel(category.symbolName, category.displayName, .subheading, emphasis: .custom(selectedCategory == category ? .white : AppTextColors.primary))
Image(systemName: category.symbolName) .padding(.horizontal, Design.Spacing.medium)
Text(category.displayName) .padding(.vertical, Design.Spacing.small)
} .background(selectedCategory == category ? AppAccent.primary : AppSurface.card)
.font(.subheadline) .clipShape(.capsule)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small)
.background(selectedCategory == category ? AppAccent.primary : AppSurface.card)
.foregroundStyle(selectedCategory == category ? .white : AppTextColors.primary)
.clipShape(.capsule)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
} }
@ -90,50 +85,36 @@ struct PresetLibrarySheet: View {
} label: { } label: {
VStack(alignment: .leading, spacing: Design.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Image(systemName: preset.iconName) SymbolIcon(preset.iconName, size: .row, color: AppAccent.primary)
.font(.title2)
.foregroundStyle(AppAccent.primary)
.frame(width: 40, height: 40) .frame(width: 40, height: 40)
.background(AppAccent.primary.opacity(0.1)) .background(AppAccent.primary.opacity(0.1))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) .clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
HStack { HStack {
Text(preset.title) StyledLabel(preset.title, .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
if isAdded { if isAdded {
Image(systemName: "checkmark.circle.fill") SymbolIcon("checkmark.circle.fill", size: .badge, color: AppStatus.success)
.foregroundStyle(AppStatus.success)
.font(.caption)
} }
} }
Text(preset.theme) StyledLabel(preset.theme, .subheading, emphasis: .custom(AppTextColors.secondary))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
} }
Spacer() Spacer()
VStack(alignment: .trailing, spacing: Design.Spacing.xSmall) { VStack(alignment: .trailing, spacing: Design.Spacing.xSmall) {
Image(systemName: preset.timeOfDay.symbolName) SymbolIcon(preset.timeOfDay.symbolName, size: .badge, color: AppTextColors.tertiary)
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
Text(String(localized: "\(preset.habits.count) habits")) StyledLabel(String(localized: "\(preset.habits.count) habits"), .caption, emphasis: .custom(AppTextColors.tertiary))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} }
} }
// Habit preview // Habit preview
HStack(spacing: Design.Spacing.small) { HStack(spacing: Design.Spacing.small) {
ForEach(preset.habits.prefix(4)) { habit in ForEach(preset.habits.prefix(4)) { habit in
Image(systemName: habit.symbolName) SymbolIcon(habit.symbolName, size: .badge, color: AppTextColors.tertiary)
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} }
if preset.habits.count > 4 { if preset.habits.count > 4 {
@ -218,13 +199,9 @@ struct PresetDetailSheet: View {
private var descriptionSection: some View { private var descriptionSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "About")) StyledLabel(String(localized: "About"), .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
Text(preset.notes) StyledLabel(preset.notes, .body, emphasis: .custom(AppTextColors.secondary))
.font(.body)
.foregroundStyle(AppTextColors.secondary)
} }
.padding(Design.Spacing.large) .padding(Design.Spacing.large)
.frame(maxWidth: .infinity, alignment: .leading) .frame(maxWidth: .infinity, alignment: .leading)
@ -234,20 +211,15 @@ struct PresetDetailSheet: View {
private var habitsSection: some View { private var habitsSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(String(localized: "Habits")) StyledLabel(String(localized: "Habits"), .heading, emphasis: .custom(AppTextColors.primary))
.font(.headline)
.foregroundStyle(AppTextColors.primary)
VStack(spacing: Design.Spacing.xSmall) { VStack(spacing: Design.Spacing.xSmall) {
ForEach(preset.habits) { habit in ForEach(preset.habits) { habit in
HStack(spacing: Design.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
Image(systemName: habit.symbolName) SymbolIcon(habit.symbolName, size: .inline, color: AppAccent.primary)
.foregroundStyle(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text(habit.title) StyledLabel(habit.title, .subheading, emphasis: .custom(AppTextColors.primary))
.font(.subheadline)
.foregroundStyle(AppTextColors.primary)
Spacer() Spacer()
} }
@ -271,20 +243,15 @@ struct PresetDetailSheet: View {
store.createRitualFromPreset(preset) store.createRitualFromPreset(preset)
hasBeenAdded = true hasBeenAdded = true
} label: { } label: {
HStack { IconLabel(
if isAlreadyAdded || hasBeenAdded { isAlreadyAdded || hasBeenAdded ? "checkmark.circle.fill" : "plus.circle.fill",
Image(systemName: "checkmark.circle.fill") isAlreadyAdded || hasBeenAdded ? String(localized: "Added to My Rituals") : String(localized: "Add to My Rituals"),
Text(String(localized: "Added to My Rituals")) .heading,
} else { emphasis: .custom(.white)
Image(systemName: "plus.circle.fill") )
Text(String(localized: "Add to My Rituals"))
}
}
.font(.headline)
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(Design.Spacing.medium) .padding(Design.Spacing.medium)
.background(isAlreadyAdded || hasBeenAdded ? AppStatus.success : AppAccent.primary) .background(isAlreadyAdded || hasBeenAdded ? AppStatus.success : AppAccent.primary)
.foregroundStyle(.white)
.clipShape(.rect(cornerRadius: Design.CornerRadius.large)) .clipShape(.rect(cornerRadius: Design.CornerRadius.large))
} }
.buttonStyle(.plain) .buttonStyle(.plain)

View File

@ -103,9 +103,7 @@ struct RitualEditSheet: View {
Button { Button {
showingIconPicker = true showingIconPicker = true
} label: { } label: {
Image(systemName: iconName) SymbolIcon(iconName, size: .card, color: AppAccent.primary)
.font(.title)
.foregroundStyle(AppAccent.primary)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background(AppSurface.card) .background(AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) .clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
@ -113,10 +111,10 @@ struct RitualEditSheet: View {
VStack(alignment: .leading, spacing: Design.Spacing.xSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
TextField(String(localized: "Ritual name"), text: $title) TextField(String(localized: "Ritual name"), text: $title)
.font(.headline) .typography(.heading)
TextField(String(localized: "Theme or tagline"), text: $theme) TextField(String(localized: "Theme or tagline"), text: $theme)
.font(.subheadline) .typography(.subheading)
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
} }
} }
@ -124,9 +122,7 @@ struct RitualEditSheet: View {
// Category selection - simple picker from CategoryStore // Category selection - simple picker from CategoryStore
VStack(alignment: .leading, spacing: Design.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text(String(localized: "Category")) StyledLabel(String(localized: "Category"), .subheading, emphasis: .custom(AppTextColors.secondary))
.font(.subheadline)
.foregroundStyle(AppTextColors.secondary)
// Horizontal scrollable category chips // Horizontal scrollable category chips
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
@ -173,13 +169,7 @@ struct RitualEditSheet: View {
} }
// Show the time range for the selected time of day // Show the time range for the selected time of day
HStack(spacing: Design.Spacing.xSmall) { IconLabel(timeOfDay.symbolName, timeOfDay.timeRange, .caption, emphasis: .custom(AppTextColors.tertiary))
Image(systemName: timeOfDay.symbolName)
.font(.caption)
Text(timeOfDay.timeRange)
.font(.caption)
}
.foregroundStyle(AppTextColors.tertiary)
} }
.listRowBackground(AppSurface.card) .listRowBackground(AppSurface.card)
@ -218,9 +208,7 @@ struct RitualEditSheet: View {
HStack(spacing: Design.Spacing.xSmall) { HStack(spacing: Design.Spacing.xSmall) {
Text(String(localized: "\(Int(durationDays)) days")) Text(String(localized: "\(Int(durationDays)) days"))
.foregroundStyle(AppTextColors.secondary) .foregroundStyle(AppTextColors.secondary)
Image(systemName: "pencil.circle") SymbolIcon("pencil.circle", size: .badge, color: AppAccent.primary)
.font(.caption)
.foregroundStyle(AppAccent.primary)
} }
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -237,9 +225,7 @@ struct RitualEditSheet: View {
Button { Button {
durationDays = Double(days) durationDays = Double(days)
} label: { } label: {
Text("\(days)") StyledLabel("\(days)", .caption, emphasis: .custom(Int(durationDays) == days ? AppAccent.primary : AppTextColors.tertiary))
.font(.caption)
.foregroundStyle(Int(durationDays) == days ? AppAccent.primary : AppTextColors.tertiary)
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
.padding(.vertical, Design.Spacing.xSmall) .padding(.vertical, Design.Spacing.xSmall)
.background(Int(durationDays) == days ? AppAccent.primary.opacity(0.2) : AppSurface.secondary) .background(Int(durationDays) == days ? AppAccent.primary.opacity(0.2) : AppSurface.secondary)
@ -255,8 +241,7 @@ struct RitualEditSheet: View {
} header: { } header: {
Text(String(localized: "Schedule")) Text(String(localized: "Schedule"))
} footer: { } footer: {
Text(String(localized: "Tap the duration to enter a custom number of days (up to 365).")) StyledLabel(String(localized: "Tap the duration to enter a custom number of days (up to 365)."), .caption, emphasis: .secondary)
.font(.caption)
} }
} }
@ -329,14 +314,11 @@ struct RitualEditSheet: View {
HStack { HStack {
Text(String(localized: "Habits")) Text(String(localized: "Habits"))
Spacer() Spacer()
Text(String(localized: "\(habits.count) habits")) StyledLabel(String(localized: "\(habits.count) habits"), .caption, emphasis: .custom(AppTextColors.tertiary))
.font(.caption)
.foregroundStyle(AppTextColors.tertiary)
} }
} footer: { } footer: {
if habits.count > 1 { if habits.count > 1 {
Text(String(localized: "Drag the handle to reorder habits.")) StyledLabel(String(localized: "Drag the handle to reorder habits."), .caption, emphasis: .secondary)
.font(.caption)
} }
} }
} }
@ -395,13 +377,11 @@ struct RitualEditSheet: View {
.fill(color) .fill(color)
.frame(width: 10, height: 10) .frame(width: 10, height: 10)
} }
Text(label) StyledLabel(label, .subheading, emphasis: .custom(isSelected ? .white : AppTextColors.primary))
.font(.subheadline)
} }
.padding(.horizontal, Design.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.small) .padding(.vertical, Design.Spacing.small)
.background(isSelected ? (color ?? AppAccent.primary) : AppSurface.secondary) .background(isSelected ? (color ?? AppAccent.primary) : AppSurface.secondary)
.foregroundStyle(isSelected ? .white : AppTextColors.primary)
.clipShape(.capsule) .clipShape(.capsule)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -513,9 +493,7 @@ struct IconPickerSheet: View {
LazyVStack(alignment: .leading, spacing: Design.Spacing.large) { LazyVStack(alignment: .leading, spacing: Design.Spacing.large) {
ForEach(iconGroups, id: \.name) { group in ForEach(iconGroups, id: \.name) { group in
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(group.name) StyledLabel(group.name, .caption, emphasis: .custom(AppTextColors.secondary))
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: Design.Spacing.small) { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: Design.Spacing.small) {
@ -559,9 +537,7 @@ struct IconPickerSheet: View {
selectedIcon = icon selectedIcon = icon
dismiss() dismiss()
} label: { } label: {
Image(systemName: icon) SymbolIcon(icon, size: .row, color: selectedIcon == icon ? AppAccent.primary : AppTextColors.secondary)
.font(.title3)
.foregroundStyle(selectedIcon == icon ? AppAccent.primary : AppTextColors.secondary)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background(selectedIcon == icon ? AppAccent.primary.opacity(0.2) : AppSurface.card) .background(selectedIcon == icon ? AppAccent.primary.opacity(0.2) : AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) .clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
@ -621,9 +597,7 @@ struct HabitIconPickerSheet: View {
LazyVStack(alignment: .leading, spacing: Design.Spacing.large) { LazyVStack(alignment: .leading, spacing: Design.Spacing.large) {
ForEach(iconGroups, id: \.name) { group in ForEach(iconGroups, id: \.name) { group in
VStack(alignment: .leading, spacing: Design.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text(group.name) StyledLabel(group.name, .caption, emphasis: .custom(AppTextColors.secondary))
.font(.caption)
.foregroundStyle(AppTextColors.secondary)
.padding(.horizontal, Design.Spacing.small) .padding(.horizontal, Design.Spacing.small)
LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: Design.Spacing.small) { LazyVGrid(columns: Array(repeating: GridItem(.flexible()), count: 6), spacing: Design.Spacing.small) {
@ -675,9 +649,7 @@ struct HabitIconPickerSheet: View {
selectedIcon = icon selectedIcon = icon
dismiss() dismiss()
} label: { } label: {
Image(systemName: icon) SymbolIcon(icon, size: .row, color: selectedIcon == icon ? AppAccent.primary : AppTextColors.secondary)
.font(.title3)
.foregroundStyle(selectedIcon == icon ? AppAccent.primary : AppTextColors.secondary)
.frame(width: 44, height: 44) .frame(width: 44, height: 44)
.background(selectedIcon == icon ? AppAccent.primary.opacity(0.2) : AppSurface.card) .background(selectedIcon == icon ? AppAccent.primary.opacity(0.2) : AppSurface.card)
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium)) .clipShape(.rect(cornerRadius: Design.CornerRadius.medium))