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

This commit is contained in:
Matt Bruce 2026-01-31 11:16:38 -06:00
parent c9b1b3cf62
commit 3307ba5433
27 changed files with 130 additions and 204 deletions

8
PRD.md
View File

@ -376,17 +376,13 @@ TheNoiseClock/
│ │ ├── Design/ │ │ ├── Design/
│ │ │ ├── BrandingConfig.swift # Bedrock branding + launch config │ │ │ ├── BrandingConfig.swift # Bedrock branding + launch config
│ │ │ ├── AppConstants.swift # App-wide constants and configuration │ │ │ ├── AppConstants.swift # App-wide constants and configuration
│ │ │ ├── UIConstants.swift # UI constants namespace │ │ │ ├── NoiseClockTheme.swift # Bedrock app color theme
│ │ │ ├── UIConstants+Colors.swift # Theme-backed colors │ │ │ ├── Design+NoiseClock.swift # App-specific Design extensions
│ │ │ ├── UIConstants+Layout.swift # Spacing/radius/opacity
│ │ │ ├── UIConstants+Animation.swift # Animation curves
│ │ │ └── Fonts/ │ │ │ └── Fonts/
│ │ │ ├── Font.Design.swift │ │ │ ├── Font.Design.swift
│ │ │ ├── Font.Weight.swift │ │ │ ├── Font.Weight.swift
│ │ │ ├── FontFamily.swift │ │ │ ├── FontFamily.swift
│ │ │ └── FontUtils.swift │ │ │ └── FontUtils.swift
│ │ ├── Theme/
│ │ │ └── NoiseClockTheme.swift # Bedrock app color theme
│ │ ├── Extensions/ │ │ ├── Extensions/
│ │ │ ├── Color+Extensions.swift # Color utilities and extensions │ │ │ ├── Color+Extensions.swift # Color utilities and extensions
│ │ │ ├── Date+Extensions.swift # Date formatting and utilities │ │ │ ├── Date+Extensions.swift # Date formatting and utilities

View File

@ -86,7 +86,7 @@ xcodebuild -project TheNoiseClock/TheNoiseClock.xcodeproj -scheme TheNoiseClock
The app uses Bedrock for theme tokens, settings UI, and the launch experience. The app uses Bedrock for theme tokens, settings UI, and the launch experience.
- Theme: `TheNoiseClock/Shared/Theme/NoiseClockTheme.swift` - Theme: `TheNoiseClock/Shared/Design/NoiseClockTheme.swift`
- Branding config: `TheNoiseClock/Shared/Design/BrandingConfig.swift` - Branding config: `TheNoiseClock/Shared/Design/BrandingConfig.swift`
- Launch screen: `TheNoiseClock/Resources/LaunchScreen.storyboard` - Launch screen: `TheNoiseClock/Resources/LaunchScreen.storyboard`

View File

@ -37,7 +37,7 @@ struct AddAlarmView: View {
NavigationLink(destination: LabelEditView(label: $alarmLabel)) { NavigationLink(destination: LabelEditView(label: $alarmLabel)) {
HStack { HStack {
Image(systemName: "textformat") Image(systemName: "textformat")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Label") Text("Label")
Spacer() Spacer()
@ -50,7 +50,7 @@ struct AddAlarmView: View {
NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) { NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) {
HStack { HStack {
Image(systemName: "message") Image(systemName: "message")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Message") Text("Message")
Spacer() Spacer()
@ -64,7 +64,7 @@ struct AddAlarmView: View {
NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) {
HStack { HStack {
Image(systemName: "music.note") Image(systemName: "music.note")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Sound") Text("Sound")
Spacer() Spacer()
@ -77,7 +77,7 @@ struct AddAlarmView: View {
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) { NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
HStack { HStack {
Image(systemName: "clock.arrow.circlepath") Image(systemName: "clock.arrow.circlepath")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Snooze") Text("Snooze")
Spacer() Spacer()
@ -96,7 +96,7 @@ struct AddAlarmView: View {
Button("Cancel") { Button("Cancel") {
isPresented = false isPresented = false
} }
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
@ -116,7 +116,7 @@ struct AddAlarmView: View {
isPresented = false isPresented = false
} }
} }
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.fontWeight(.semibold) .fontWeight(.semibold)
} }
} }

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
/// Component for displaying individual alarm row /// Component for displaying individual alarm row
struct AlarmRowView: View { struct AlarmRowView: View {
@ -18,18 +19,18 @@ struct AlarmRowView: View {
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
HStack { HStack {
VStack(alignment: .leading, spacing: UIConstants.Spacing.extraSmall) { VStack(alignment: .leading, spacing: Design.Spacing.xSmall) {
Text(alarm.formattedTime()) Text(alarm.formattedTime())
.font(.headline) .font(.headline)
.foregroundColor(UIConstants.Colors.primaryText) .foregroundColor(AppTextColors.primary)
Text(alarm.label) Text(alarm.label)
.font(.subheadline) .font(.subheadline)
.foregroundColor(UIConstants.Colors.secondaryText) .foregroundColor(AppTextColors.secondary)
Text("\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))") Text("\(AlarmSoundService.shared.getSoundDisplayName(alarm.soundName))")
.font(.caption) .font(.caption)
.foregroundColor(UIConstants.Colors.secondaryText) .foregroundColor(AppTextColors.secondary)
} }
Spacer() Spacer()

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
/// Empty state view for when no alarms are configured /// Empty state view for when no alarms are configured
struct EmptyAlarmsView: View { struct EmptyAlarmsView: View {
@ -15,7 +16,7 @@ struct EmptyAlarmsView: View {
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
VStack(spacing: UIConstants.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
// Icon // Icon
Image(systemName: "alarm") Image(systemName: "alarm")
.font(.largeTitle) .font(.largeTitle)

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
/// View for editing alarm label /// View for editing alarm label
struct LabelEditView: View { struct LabelEditView: View {
@ -13,15 +14,15 @@ struct LabelEditView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var body: some View { var body: some View {
VStack(spacing: UIConstants.Spacing.large) { VStack(spacing: Design.Spacing.large) {
TextField("Alarm Label", text: $label) TextField("Alarm Label", text: $label)
.textFieldStyle(RoundedBorderTextFieldStyle()) .textFieldStyle(RoundedBorderTextFieldStyle())
.contentPadding(horizontal: UIConstants.Spacing.large) .contentPadding(horizontal: Design.Spacing.large)
Spacer() Spacer()
} }
.navigationTitle("Label") .navigationTitle("Label")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.contentPadding(vertical: UIConstants.Spacing.large) .contentPadding(vertical: Design.Spacing.large)
} }
} }

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
/// View for editing alarm notification message /// View for editing alarm notification message
struct NotificationMessageEditView: View { struct NotificationMessageEditView: View {
@ -13,13 +14,13 @@ struct NotificationMessageEditView: View {
@Environment(\.dismiss) private var dismiss @Environment(\.dismiss) private var dismiss
var body: some View { var body: some View {
VStack(spacing: UIConstants.Spacing.large) { VStack(spacing: Design.Spacing.large) {
TextField("Notification message", text: $message) TextField("Notification message", text: $message)
.textFieldStyle(RoundedBorderTextFieldStyle()) .textFieldStyle(RoundedBorderTextFieldStyle())
.contentPadding(horizontal: UIConstants.Spacing.large) .contentPadding(horizontal: Design.Spacing.large)
// Preview section // Preview section
VStack(alignment: .leading, spacing: UIConstants.Spacing.small) { VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Preview:") Text("Preview:")
.font(.headline) .font(.headline)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -37,13 +38,13 @@ struct NotificationMessageEditView: View {
.background(Color(.systemGray6)) .background(Color(.systemGray6))
.cornerRadius(8) .cornerRadius(8)
} }
.contentPadding(horizontal: UIConstants.Spacing.large) .contentPadding(horizontal: Design.Spacing.large)
Spacer() Spacer()
} }
.navigationTitle("Message") .navigationTitle("Message")
.navigationBarTitleDisplayMode(.inline) .navigationBarTitleDisplayMode(.inline)
.contentPadding(vertical: UIConstants.Spacing.large) .contentPadding(vertical: Design.Spacing.large)
} }
} }

View File

@ -23,7 +23,7 @@ struct SnoozeSelectionView: View {
Spacer() Spacer()
if snoozeDuration == duration { if snoozeDuration == duration {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
} }
} }
.contentShape(Rectangle()) .contentShape(Rectangle())

View File

@ -30,7 +30,7 @@ struct SoundSelectionView: View {
Spacer() Spacer()
if selectedSound == sound.fileName { if selectedSound == sound.fileName {
Image(systemName: "checkmark") Image(systemName: "checkmark")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
} }
} }
.contentShape(Rectangle()) .contentShape(Rectangle())
@ -56,7 +56,7 @@ struct SoundSelectionView: View {
} }
}) { }) {
Image(systemName: isPlaying && currentlyPlayingSound == selectedSound ? "stop.fill" : "play.fill") Image(systemName: isPlaying && currentlyPlayingSound == selectedSound ? "stop.fill" : "play.fill")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
} }
} }
} }

View File

@ -15,7 +15,7 @@ struct TimeUntilAlarmSection: View {
VStack(spacing: 4) { VStack(spacing: 4) {
HStack { HStack {
Image(systemName: "calendar") Image(systemName: "calendar")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
Text(timeUntilAlarm) Text(timeUntilAlarm)
.font(.subheadline) .font(.subheadline)
.foregroundColor(.secondary) .foregroundColor(.secondary)

View File

@ -55,7 +55,7 @@ struct EditAlarmView: View {
NavigationLink(destination: LabelEditView(label: $alarmLabel)) { NavigationLink(destination: LabelEditView(label: $alarmLabel)) {
HStack { HStack {
Image(systemName: "textformat") Image(systemName: "textformat")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Label") Text("Label")
Spacer() Spacer()
@ -68,7 +68,7 @@ struct EditAlarmView: View {
NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) { NavigationLink(destination: NotificationMessageEditView(message: $notificationMessage)) {
HStack { HStack {
Image(systemName: "message") Image(systemName: "message")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Message") Text("Message")
Spacer() Spacer()
@ -82,7 +82,7 @@ struct EditAlarmView: View {
NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) { NavigationLink(destination: SoundSelectionView(selectedSound: $selectedSoundName)) {
HStack { HStack {
Image(systemName: "music.note") Image(systemName: "music.note")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Sound") Text("Sound")
Spacer() Spacer()
@ -95,7 +95,7 @@ struct EditAlarmView: View {
NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) { NavigationLink(destination: SnoozeSelectionView(snoozeDuration: $snoozeDuration)) {
HStack { HStack {
Image(systemName: "clock.arrow.circlepath") Image(systemName: "clock.arrow.circlepath")
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.frame(width: 24) .frame(width: 24)
Text("Snooze") Text("Snooze")
Spacer() Spacer()
@ -114,7 +114,7 @@ struct EditAlarmView: View {
Button("Cancel") { Button("Cancel") {
dismiss() dismiss()
} }
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
} }
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
@ -136,7 +136,7 @@ struct EditAlarmView: View {
dismiss() dismiss()
} }
} }
.foregroundColor(UIConstants.Colors.accentColor) .foregroundColor(AppAccent.primary)
.fontWeight(.semibold) .fontWeight(.semibold)
} }
} }

View File

@ -10,6 +10,7 @@ import Combine
import Observation import Observation
import AudioPlaybackKit import AudioPlaybackKit
import SwiftUI import SwiftUI
import Bedrock
/// ViewModel for clock display and management /// ViewModel for clock display and management
@Observable @Observable
@ -60,7 +61,7 @@ class ClockViewModel {
// MARK: - Public Interface // MARK: - Public Interface
func toggleDisplayMode() { func toggleDisplayMode() {
withAnimation(UIConstants.AnimationCurves.bouncy) { withAnimation(Design.Animation.spring(bounce: Design.Animation.springBounce)) {
isDisplayMode.toggle() isDisplayMode.toggle()
} }

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
/// Container component that handles the main clock display layout and scaling /// Container component that handles the main clock display layout and scaling
struct ClockDisplayContainer: View { struct ClockDisplayContainer: View {
@ -20,7 +21,7 @@ struct ClockDisplayContainer: View {
return GeometryReader { geometry in return GeometryReader { geometry in
let isPortrait = geometry.size.height >= geometry.size.width let isPortrait = geometry.size.height >= geometry.size.width
let hasOverlay = style.showBattery || style.showDate let hasOverlay = style.showBattery || style.showDate
let topSpacing = hasOverlay ? (isPortrait ? UIConstants.Spacing.huge : UIConstants.Spacing.large) : 0 let topSpacing = hasOverlay ? (isPortrait ? Design.Spacing.xxLarge : Design.Spacing.large) : 0
VStack(spacing: 0) { VStack(spacing: 0) {
// Top spacing to account for overlay // Top spacing to account for overlay
@ -50,7 +51,7 @@ struct ClockDisplayContainer: View {
Spacer() Spacer()
} }
.animation(UIConstants.AnimationCurves.smooth, value: isDisplayMode) .animation(.smooth(duration: Design.Animation.standard), value: isDisplayMode)
} }
} }
} }

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
/// Container component that handles the positioning and display of top overlays /// Container component that handles the positioning and display of top overlays
struct ClockOverlayContainer: View { struct ClockOverlayContainer: View {
@ -20,12 +21,12 @@ struct ClockOverlayContainer: View {
TopOverlayView( TopOverlayView(
showBattery: style.showBattery, showBattery: style.showBattery,
showDate: style.showDate, showDate: style.showDate,
color: style.digitColor.opacity(UIConstants.Opacity.primary), color: style.digitColor.opacity(Design.Opacity.heavy),
opacity: style.overlayOpacity, opacity: style.overlayOpacity,
dateFormat: style.dateFormat dateFormat: style.dateFormat
) )
.padding(.top, UIConstants.Spacing.small) .padding(.top, Design.Spacing.small)
.padding(.horizontal, UIConstants.Spacing.large) .padding(.horizontal, Design.Spacing.large)
.transition(.opacity) .transition(.opacity)
} }
Spacer() Spacer()

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
/// Component for displaying segmented time with customizable formatting /// Component for displaying segmented time with customizable formatting
struct TimeDisplayView: View { struct TimeDisplayView: View {
@ -132,8 +133,8 @@ struct TimeDisplayView: View {
} }
.offset(y: portraitMode && forceHorizontalMode ? -containerSize.height * 0.10 : 0) // Push up in horizontal mode .offset(y: portraitMode && forceHorizontalMode ? -containerSize.height * 0.10 : 0) // Push up in horizontal mode
.scaleEffect(finalScale, anchor: .center) .scaleEffect(finalScale, anchor: .center)
.animation(UIConstants.AnimationCurves.smooth, value: finalScale) .animation(.smooth(duration: Design.Animation.standard), value: finalScale)
.animation(UIConstants.AnimationCurves.smooth, value: showSeconds) // Smooth animation for seconds toggle .animation(.smooth(duration: Design.Animation.standard), value: showSeconds) // Smooth animation for seconds toggle
.minimumScaleFactor(0.1) .minimumScaleFactor(0.1)
.clipped() // Prevent overflow beyond bounds .clipped() // Prevent overflow beyond bounds
} }

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
/// Component for displaying top overlay with battery and date information /// Component for displaying top overlay with battery and date information
struct TopOverlayView: View { struct TopOverlayView: View {
@ -37,8 +38,8 @@ struct TopOverlayView: View {
) )
} }
} }
.padding(.horizontal, UIConstants.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, UIConstants.Spacing.small) .padding(.vertical, Design.Spacing.small)
.transition(.opacity) .transition(.opacity)
.onAppear { .onAppear {
batteryService.startMonitoring() batteryService.startMonitoring()

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
import AudioPlaybackKit import AudioPlaybackKit
/// Category-based sound selection view with grid layout /// Category-based sound selection view with grid layout
@ -67,7 +68,7 @@ struct SoundCategoryView: View {
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
VStack(spacing: UIConstants.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
// Search Bar // Search Bar
searchBar searchBar
@ -79,7 +80,7 @@ struct SoundCategoryView: View {
soundGrid soundGrid
} }
} }
.padding(.top, UIConstants.Spacing.medium) .padding(.top, Design.Spacing.medium)
} }
// MARK: - Subviews // MARK: - Subviews
@ -94,15 +95,15 @@ struct SoundCategoryView: View {
.autocorrectionDisabled() .autocorrectionDisabled()
.textInputAutocapitalization(.never) .textInputAutocapitalization(.never)
} }
.padding(.horizontal, UIConstants.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, UIConstants.Spacing.small) .padding(.vertical, Design.Spacing.small)
.background(Color(.systemGray6)) .background(Color(.systemGray6))
.cornerRadius(10) .cornerRadius(10)
} }
private var categoryTabs: some View { private var categoryTabs: some View {
ScrollView(.horizontal, showsIndicators: false) { ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: UIConstants.Spacing.small) { HStack(spacing: Design.Spacing.small) {
ForEach(categories) { category in ForEach(categories) { category in
CategoryTab( CategoryTab(
title: category.displayName, title: category.displayName,
@ -113,12 +114,12 @@ struct SoundCategoryView: View {
} }
} }
} }
.padding(.horizontal, UIConstants.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
} }
} }
private var soundGrid: some View { private var soundGrid: some View {
LazyVStack(spacing: UIConstants.Spacing.small) { LazyVStack(spacing: Design.Spacing.small) {
ForEach(filteredSounds) { sound in ForEach(filteredSounds) { sound in
SoundCard( SoundCard(
sound: sound, sound: sound,
@ -134,7 +135,7 @@ struct SoundCategoryView: View {
) )
} }
} }
.padding(.horizontal, UIConstants.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
} }
} }
@ -157,10 +158,10 @@ struct CategoryTab: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
} }
.padding(.horizontal, UIConstants.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, UIConstants.Spacing.small) .padding(.vertical, Design.Spacing.small)
.background(isSelected ? UIConstants.Colors.accentColor : Color(.systemGray6)) .background(isSelected ? AppAccent.primary : Color(.systemGray6))
.foregroundColor(isSelected ? .white : UIConstants.Colors.primaryText) .foregroundColor(isSelected ? .white : AppTextColors.primary)
.cornerRadius(20) .cornerRadius(20)
} }
.buttonStyle(.plain) .buttonStyle(.plain)
@ -175,20 +176,20 @@ struct SoundCard: View {
let onPreview: () -> Void let onPreview: () -> Void
var body: some View { var body: some View {
HStack(spacing: UIConstants.Spacing.medium) { HStack(spacing: Design.Spacing.medium) {
// Sound Icon (Left) // Sound Icon (Left)
ZStack { ZStack {
Circle() Circle()
.fill(isSelected ? UIConstants.Colors.accentColor : Color(.systemGray5)) .fill(isSelected ? AppAccent.primary : Color(.systemGray5))
.frame(width: 50, height: 50) .frame(width: 50, height: 50)
Image(systemName: soundIcon) Image(systemName: soundIcon)
.font(.title3) .font(.title3)
.foregroundColor(isSelected ? .white : UIConstants.Colors.primaryText) .foregroundColor(isSelected ? .white : AppTextColors.primary)
if isPreviewing { if isPreviewing {
Circle() Circle()
.stroke(UIConstants.Colors.accentColor, lineWidth: 2) .stroke(AppAccent.primary, lineWidth: 2)
.frame(width: 58, height: 58) .frame(width: 58, height: 58)
.scaleEffect(1.02) .scaleEffect(1.02)
.animation(.easeInOut(duration: 0.5).repeatForever(autoreverses: true), value: isPreviewing) .animation(.easeInOut(duration: 0.5).repeatForever(autoreverses: true), value: isPreviewing)
@ -200,7 +201,7 @@ struct SoundCard: View {
// Sound Name // Sound Name
Text(sound.name) Text(sound.name)
.font(.subheadline.weight(.medium)) .font(.subheadline.weight(.medium))
.foregroundColor(UIConstants.Colors.primaryText) .foregroundColor(AppTextColors.primary)
.lineLimit(1) .lineLimit(1)
// Description // Description
@ -216,7 +217,7 @@ struct SoundCard: View {
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 6) .padding(.horizontal, 6)
.padding(.vertical, 2) .padding(.vertical, 2)
.background(UIConstants.Colors.accentColor, in: Capsule()) .background(AppAccent.primary, in: Capsule())
Spacer() Spacer()
} }
@ -224,14 +225,14 @@ struct SoundCard: View {
Spacer() Spacer()
} }
.padding(.horizontal, UIConstants.Spacing.medium) .padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, UIConstants.Spacing.small) .padding(.vertical, Design.Spacing.small)
.background( .background(
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.fill(isSelected ? UIConstants.Colors.accentColor.opacity(0.1) : Color(.systemBackground)) .fill(isSelected ? AppAccent.primary.opacity(0.1) : Color(.systemBackground))
.overlay( .overlay(
RoundedRectangle(cornerRadius: 12) RoundedRectangle(cornerRadius: 12)
.stroke(isSelected ? UIConstants.Colors.accentColor : Color.clear, lineWidth: 2) .stroke(isSelected ? AppAccent.primary : Color.clear, lineWidth: 2)
) )
) )
.onTapGesture { .onTapGesture {

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
import AudioPlaybackKit import AudioPlaybackKit
/// Component for audio playback controls /// Component for audio playback controls
@ -19,18 +20,18 @@ struct SoundControlView: View {
// MARK: - Body // MARK: - Body
var body: some View { var body: some View {
VStack(spacing: UIConstants.Spacing.medium) { VStack(spacing: Design.Spacing.medium) {
// Sound info header // Sound info header
if let sound = selectedSound { if let sound = selectedSound {
HStack { HStack {
VStack(alignment: .leading, spacing: 4) { VStack(alignment: .leading, spacing: 4) {
Text(sound.name) Text(sound.name)
.font(.headline.weight(.semibold)) .font(.headline.weight(.semibold))
.foregroundColor(UIConstants.Colors.primaryText) .foregroundColor(AppTextColors.primary)
Text(sound.description) Text(sound.description)
.font(.caption) .font(.caption)
.foregroundColor(UIConstants.Colors.secondaryText) .foregroundColor(AppTextColors.secondary)
.lineLimit(2) .lineLimit(2)
} }
@ -42,10 +43,10 @@ struct SoundControlView: View {
.foregroundColor(.white) .foregroundColor(.white)
.padding(.horizontal, 8) .padding(.horizontal, 8)
.padding(.vertical, 4) .padding(.vertical, 4)
.background(UIConstants.Colors.accentColor, in: Capsule()) .background(AppAccent.primary, in: Capsule())
} }
.contentPadding(horizontal: UIConstants.Spacing.medium, vertical: UIConstants.Spacing.small) .contentPadding(horizontal: Design.Spacing.medium, vertical: Design.Spacing.small)
.background(UIConstants.Colors.overlayBackground, in: RoundedRectangle(cornerRadius: UIConstants.CornerRadius.medium)) .background(AppSurface.overlay, in: RoundedRectangle(cornerRadius: Design.CornerRadius.large))
} }
// Main control button // Main control button
@ -56,7 +57,7 @@ struct SoundControlView: View {
onPlay(sound) onPlay(sound)
} }
}) { }) {
HStack(spacing: UIConstants.Spacing.small) { HStack(spacing: Design.Spacing.small) {
Image(systemName: isPlaying ? "stop.fill" : "play.fill") Image(systemName: isPlaying ? "stop.fill" : "play.fill")
.font(.title2.weight(.semibold)) .font(.title2.weight(.semibold))
.foregroundColor(.white) .foregroundColor(.white)
@ -65,11 +66,11 @@ struct SoundControlView: View {
.font(.headline.weight(.semibold)) .font(.headline.weight(.semibold))
.foregroundColor(.white) .foregroundColor(.white)
} }
.contentPadding(horizontal: UIConstants.Spacing.large, vertical: UIConstants.Spacing.medium) .contentPadding(horizontal: Design.Spacing.large, vertical: Design.Spacing.medium)
.background( .background(
RoundedRectangle(cornerRadius: UIConstants.CornerRadius.large) RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge)
.fill(isPlaying ? Color.red : UIConstants.Colors.accentColor) .fill(isPlaying ? Color.red : AppAccent.primary)
.shadow(color: (isPlaying ? Color.red : UIConstants.Colors.accentColor).opacity(0.3), radius: 8, x: 0, y: 4) .shadow(color: (isPlaying ? Color.red : AppAccent.primary).opacity(0.3), radius: 8, x: 0, y: 4)
) )
} }
.disabled(selectedSound == nil) .disabled(selectedSound == nil)
@ -79,11 +80,11 @@ struct SoundControlView: View {
.animation(.easeInOut(duration: 0.2), value: selectedSound) .animation(.easeInOut(duration: 0.2), value: selectedSound)
} }
.frame(maxWidth: 400) // Reasonable max width for iPad .frame(maxWidth: 400) // Reasonable max width for iPad
.padding(UIConstants.Spacing.medium) .padding(Design.Spacing.medium)
.background(UIConstants.Colors.overlayBackground, in: RoundedRectangle(cornerRadius: UIConstants.CornerRadius.large)) .background(AppSurface.overlay, in: RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge))
.overlay( .overlay(
RoundedRectangle(cornerRadius: UIConstants.CornerRadius.large) RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge)
.stroke(UIConstants.Colors.overlayBorder, lineWidth: UIConstants.BorderWidth.normal) .stroke(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
) )
} }
} }

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
import AudioPlaybackKit import AudioPlaybackKit
/// Main noise/audio player view /// Main noise/audio player view
@ -57,7 +58,7 @@ struct NoiseView: View {
private var portraitLayout: some View { private var portraitLayout: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Fixed header // Fixed header
VStack(alignment: .leading, spacing: UIConstants.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text("Ambient Sounds") Text("Ambient Sounds")
.sectionTitleStyle() .sectionTitleStyle()
@ -67,8 +68,8 @@ struct NoiseView: View {
.centered() .centered()
} }
} }
.contentPadding(horizontal: UIConstants.Spacing.large) .contentPadding(horizontal: Design.Spacing.large)
.padding(.top, UIConstants.Spacing.large) .padding(.top, Design.Spacing.large)
.background(Color(.systemBackground)) .background(Color(.systemBackground))
// Scrollable sound selection // Scrollable sound selection
@ -77,15 +78,15 @@ struct NoiseView: View {
sounds: viewModel.availableSounds, sounds: viewModel.availableSounds,
selectedSound: $selectedSound selectedSound: $selectedSound
) )
.contentPadding(horizontal: UIConstants.Spacing.large, vertical: UIConstants.Spacing.large) .contentPadding(horizontal: Design.Spacing.large, vertical: Design.Spacing.large)
} }
} }
} }
private var landscapeLayout: some View { private var landscapeLayout: some View {
HStack(spacing: UIConstants.Spacing.large) { HStack(spacing: Design.Spacing.large) {
// Left side: Player controls // Left side: Player controls
VStack(alignment: .leading, spacing: UIConstants.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text("Ambient Sounds") Text("Ambient Sounds")
.sectionTitleStyle() .sectionTitleStyle()
@ -93,7 +94,7 @@ struct NoiseView: View {
soundControlView soundControlView
} else { } else {
// Placeholder when no sound selected // Placeholder when no sound selected
VStack(spacing: UIConstants.Spacing.small) { VStack(spacing: Design.Spacing.small) {
Image(systemName: "music.note") Image(systemName: "music.note")
.font(.largeTitle) .font(.largeTitle)
.foregroundColor(.secondary) .foregroundColor(.secondary)
@ -103,14 +104,14 @@ struct NoiseView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }
.frame(maxWidth: .infinity) .frame(maxWidth: .infinity)
.padding(.vertical, UIConstants.Spacing.large) .padding(.vertical, Design.Spacing.large)
} }
Spacer() Spacer()
} }
.frame(maxWidth: 400) // Reasonable width for player section .frame(maxWidth: 400) // Reasonable width for player section
.contentPadding(horizontal: UIConstants.Spacing.large) .contentPadding(horizontal: Design.Spacing.large)
.padding(.top, UIConstants.Spacing.large) .padding(.top, Design.Spacing.large)
// Right side: Sound selection // Right side: Sound selection
VStack(spacing: 0) { VStack(spacing: 0) {
@ -119,11 +120,11 @@ struct NoiseView: View {
sounds: viewModel.availableSounds, sounds: viewModel.availableSounds,
selectedSound: $selectedSound selectedSound: $selectedSound
) )
.contentPadding(horizontal: UIConstants.Spacing.large, vertical: UIConstants.Spacing.large) .contentPadding(horizontal: Design.Spacing.large, vertical: Design.Spacing.large)
} }
} }
} }
.contentPadding(horizontal: UIConstants.Spacing.medium) .contentPadding(horizontal: Design.Spacing.medium)
} }
} }

View File

@ -12,9 +12,9 @@ import Bedrock
extension Color { extension Color {
enum Branding { enum Branding {
static let primary = Color(red: 0.08, green: 0.10, blue: 0.16) static let primary = AppSurface.primary
static let secondary = Color(red: 0.03, green: 0.05, blue: 0.10) static let secondary = AppSurface.overlay
static let accent = Color.white static let accent = AppTextColors.primary
} }
} }

View File

@ -0,0 +1,13 @@
//
// Design+NoiseClock.swift
// TheNoiseClock
//
// Created by Matt Bruce on 1/31/26.
//
import SwiftUI
import Bedrock
extension Design.CornerRadius {
static let appLarge: CGFloat = 16
}

View File

@ -1,19 +0,0 @@
//
// UIConstants+Animation.swift
// TheNoiseClock
//
// Created by Matt Bruce on 1/31/26.
//
import SwiftUI
import Bedrock
extension UIConstants {
// MARK: - Animation Curves
enum AnimationCurves {
static let smooth = Animation.smooth(duration: Design.Animation.standard)
static let bouncy = Animation.spring(duration: Design.Animation.springDuration, bounce: Design.Animation.springBounce)
static let quick = Animation.easeInOut(duration: Design.Animation.quick)
}
}

View File

@ -1,21 +0,0 @@
//
// UIConstants+Colors.swift
// TheNoiseClock
//
// Created by Matt Bruce on 1/31/26.
//
import SwiftUI
extension UIConstants {
// MARK: - Colors
enum Colors {
static let accentColor = AppAccent.primary
static let primaryText = AppTextColors.primary
static let secondaryText = AppTextColors.secondary
static let background = AppSurface.primary
static let overlayBackground = AppSurface.overlay
static let overlayBorder = AppBorder.subtle
}
}

View File

@ -1,45 +0,0 @@
//
// UIConstants+Layout.swift
// TheNoiseClock
//
// Created by Matt Bruce on 1/31/26.
//
import SwiftUI
import Bedrock
extension UIConstants {
// MARK: - Spacing
enum Spacing {
static let extraSmall: CGFloat = Design.Spacing.xSmall
static let small: CGFloat = Design.Spacing.small
static let medium: CGFloat = Design.Spacing.medium
static let large: CGFloat = Design.Spacing.large
static let extraLarge: CGFloat = Design.Spacing.xLarge
static let huge: CGFloat = Design.Spacing.xxLarge
}
// MARK: - Corner Radius
enum CornerRadius {
static let small: CGFloat = Design.CornerRadius.small
static let medium: CGFloat = Design.CornerRadius.large
static let large: CGFloat = 16
static let extraLarge: CGFloat = Design.CornerRadius.xxLarge
}
// MARK: - Border Width
enum BorderWidth {
static let thin: CGFloat = Design.LineWidth.hairline
static let normal: CGFloat = Design.LineWidth.thin
static let thick: CGFloat = Design.LineWidth.standard
}
// MARK: - Opacity
enum Opacity {
static let disabled: Double = Design.Opacity.light
static let secondary: Double = Design.Opacity.accent
static let primary: Double = Design.Opacity.heavy
static let full: Double = 1.0
}
}

View File

@ -1,11 +0,0 @@
//
// UIConstants.swift
// TheNoiseClock
//
// Created by Matt Bruce on 9/7/25.
//
import SwiftUI
/// UI-specific constants for styling and layout.
enum UIConstants {}

View File

@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Bedrock
extension View { extension View {
@ -13,10 +14,10 @@ extension View {
/// - Returns: View with card styling applied /// - Returns: View with card styling applied
func cardStyle() -> some View { func cardStyle() -> some View {
self self
.background(UIConstants.Colors.overlayBackground, in: RoundedRectangle(cornerRadius: UIConstants.CornerRadius.large)) .background(AppSurface.overlay, in: RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge))
.overlay( .overlay(
RoundedRectangle(cornerRadius: UIConstants.CornerRadius.large) RoundedRectangle(cornerRadius: Design.CornerRadius.appLarge)
.stroke(UIConstants.Colors.overlayBorder, lineWidth: UIConstants.BorderWidth.normal) .stroke(AppBorder.subtle, lineWidth: Design.LineWidth.thin)
) )
} }
@ -25,12 +26,12 @@ extension View {
/// - isEnabled: Whether the button is enabled /// - isEnabled: Whether the button is enabled
/// - color: Button color /// - color: Button color
/// - Returns: View with button styling applied /// - Returns: View with button styling applied
func buttonStyle(isEnabled: Bool = true, color: Color = UIConstants.Colors.accentColor) -> some View { func buttonStyle(isEnabled: Bool = true, color: Color = AppAccent.primary) -> some View {
self self
.padding(UIConstants.Spacing.medium) .padding(Design.Spacing.medium)
.background(isEnabled ? color : color.opacity(UIConstants.Opacity.disabled)) .background(isEnabled ? color : color.opacity(Design.Opacity.light))
.foregroundColor(.white) .foregroundColor(.white)
.cornerRadius(UIConstants.CornerRadius.small) .cornerRadius(Design.CornerRadius.small)
.disabled(!isEnabled) .disabled(!isEnabled)
} }
@ -67,7 +68,7 @@ extension View {
func sectionTitleStyle() -> some View { func sectionTitleStyle() -> some View {
self self
.font(.title2.weight(.bold)) .font(.title2.weight(.bold))
.foregroundColor(UIConstants.Colors.primaryText) .foregroundColor(AppTextColors.primary)
} }
/// Center content horizontally with spacers /// Center content horizontally with spacers
@ -84,7 +85,7 @@ extension View {
/// - Parameter title: The title text /// - Parameter title: The title text
/// - Returns: View with section header styling /// - Returns: View with section header styling
func sectionHeader(title: String) -> some View { func sectionHeader(title: String) -> some View {
VStack(alignment: .leading, spacing: UIConstants.Spacing.medium) { VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text(title) Text(title)
.sectionTitleStyle() .sectionTitleStyle()
self self
@ -96,7 +97,7 @@ extension View {
/// - horizontal: Horizontal padding amount /// - horizontal: Horizontal padding amount
/// - vertical: Vertical padding amount /// - vertical: Vertical padding amount
/// - Returns: View with standard padding applied /// - Returns: View with standard padding applied
func contentPadding(horizontal: CGFloat = UIConstants.Spacing.large, vertical: CGFloat? = nil) -> some View { func contentPadding(horizontal: CGFloat = Design.Spacing.large, vertical: CGFloat? = nil) -> some View {
self self
.padding(.horizontal, horizontal) .padding(.horizontal, horizontal)
.padding(.vertical, vertical ?? horizontal) .padding(.vertical, vertical ?? horizontal)