From d502d33601591095a5737b1bcba83850568bdf24 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 15 Jul 2026 00:52:38 +0000 Subject: [PATCH] Introduce LumenColors design-token layer for the semantic palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The app had no color design system: the same three semantic colors were repeated as raw Color(hex:) literals across ~14 view files (gold accent 49×, dark background 20×, success green 11×). This promotes them to named tokens so the brand stays consistent and a palette change is a one-line edit. - New Lumen/Support/LumenColors.swift defines Color.lumenAccent / .lumenBackground / .lumenSuccess, each equal to the exact hex it replaced — a behavior-preserving rename, not a visual change. - Mechanically replaced all 80 exact-string occurrences with the tokens, including the #0E0819 base stop of the time-of-day gradients (it is the background token). Contextual one-off colors (per-mood gradient tints, away- mode backgrounds, #D4825A) intentionally stay inline — only repeated semantic colors are tokenized. - LumenTests/LumenColorsTests.swift guards that each token still equals its source hex. Note: Linux environment — the iOS target cannot be compiled or run here, and CI builds only the web app. Change is a mechanical exact-string substitution (verified 0 raw literals remain outside the token file; introduced token counts match the originals 49/20/11). Build in Xcode to confirm. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01PE4HFdLKjK4er6BBAgbgrb --- Lumen/Components/DarkEmptyStateView.swift | 8 +++---- Lumen/Components/NowNextCard.swift | 2 +- Lumen/Components/RoomCard.swift | 2 +- Lumen/Features/Devices/DeviceDetailView.swift | 24 +++++++++---------- Lumen/Features/Devices/DeviceListView.swift | 14 +++++------ Lumen/Features/Home/DashboardSetupViews.swift | 18 +++++++------- Lumen/Features/Home/HomeDashboardView.swift | 16 ++++++------- Lumen/Features/Home/LumenActionView.swift | 6 ++--- Lumen/Features/Home/LumenReasoningView.swift | 8 +++---- Lumen/Features/Home/OnboardingView.swift | 6 ++--- Lumen/Features/Rooms/RoomDetailView.swift | 4 ++-- Lumen/Features/Rooms/RoomListView.swift | 2 +- .../Features/Scenes/SceneApprovalSheet.swift | 6 ++--- Lumen/Features/Scenes/SceneDetailView.swift | 14 +++++------ Lumen/Features/Scenes/SceneListView.swift | 10 ++++---- Lumen/Features/Settings/SettingsView.swift | 10 ++++---- Lumen/Models/TimeOfDay.swift | 10 ++++---- Lumen/Support/LumenColors.swift | 24 +++++++++++++++++++ LumenTests/LumenColorsTests.swift | 22 +++++++++++++++++ 19 files changed, 126 insertions(+), 80 deletions(-) create mode 100644 Lumen/Support/LumenColors.swift create mode 100644 LumenTests/LumenColorsTests.swift diff --git a/Lumen/Components/DarkEmptyStateView.swift b/Lumen/Components/DarkEmptyStateView.swift index 6228deb..1bb47dc 100644 --- a/Lumen/Components/DarkEmptyStateView.swift +++ b/Lumen/Components/DarkEmptyStateView.swift @@ -33,7 +33,7 @@ struct DarkEmptyStateView: View { .frame(width: 58, height: 58) Image(systemName: icon) .font(.system(size: 24, weight: .semibold)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } VStack(spacing: 7) { @@ -66,7 +66,7 @@ struct DarkEmptyStateView: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 13) - .background(Color(hex: "#C49A6C"), in: RoundedRectangle(cornerRadius: 16)) + .background(Color.lumenAccent, in: RoundedRectangle(cornerRadius: 16)) } .padding(.top, 2) } @@ -85,9 +85,9 @@ struct DarkEmptyStateView: View { HStack(alignment: .top, spacing: 10) { Text("\(index + 1)") .font(.system(size: 11, weight: .bold)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) .frame(width: 22, height: 22) - .background(Color(hex: "#C49A6C").opacity(0.13), in: Circle()) + .background(Color.lumenAccent.opacity(0.13), in: Circle()) Text(text) .font(.system(size: 12)) diff --git a/Lumen/Components/NowNextCard.swift b/Lumen/Components/NowNextCard.swift index 2fe624a..c995177 100644 --- a/Lumen/Components/NowNextCard.swift +++ b/Lumen/Components/NowNextCard.swift @@ -185,7 +185,7 @@ extension TimeOfDay { } .padding(20) } - .background(Color(hex: "#0E0819")) + .background(Color.lumenBackground) } private func makeDate(hour: Int, minute: Int) -> Date { diff --git a/Lumen/Components/RoomCard.swift b/Lumen/Components/RoomCard.swift index fe9c6cb..6633c80 100644 --- a/Lumen/Components/RoomCard.swift +++ b/Lumen/Components/RoomCard.swift @@ -12,7 +12,7 @@ struct RoomCard: View { .frame(width: 40, height: 40) Image(systemName: room.type.iconName) .font(.system(size: 17)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } Spacer() diff --git a/Lumen/Features/Devices/DeviceDetailView.swift b/Lumen/Features/Devices/DeviceDetailView.swift index 65111c6..e90d0cb 100644 --- a/Lumen/Features/Devices/DeviceDetailView.swift +++ b/Lumen/Features/Devices/DeviceDetailView.swift @@ -15,7 +15,7 @@ struct DeviceDetailView: View { let state = viewModel.controlState(for: currentDevice) ZStack { - Color(hex: "#0E0819").ignoresSafeArea() + Color.lumenBackground.ignoresSafeArea() content(device: currentDevice, state: state) } .toolbar(.hidden, for: .navigationBar) @@ -70,7 +70,7 @@ struct DeviceDetailView: View { .frame(width: 46, height: 46) Image(systemName: device.category.systemImage) .font(.system(size: 20)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } } } @@ -83,7 +83,7 @@ struct DeviceDetailView: View { DarkRow(label: "Status") { HStack(spacing: 6) { Circle() - .fill(device.reachability == .reachable ? Color(hex: "#6FDBA8") : Color.white.opacity(0.25)) + .fill(device.reachability == .reachable ? Color.lumenSuccess : Color.white.opacity(0.25)) .frame(width: 7, height: 7) Text(device.reachability == .reachable ? "Online" : "Offline") .font(.system(size: 14)) @@ -102,7 +102,7 @@ struct DeviceDetailView: View { DarkRow(label: "Power", isLast: true) { Text(state.isPowered ? "On" : "Off") .font(.system(size: 14)) - .foregroundStyle(state.isPowered ? Color(hex: "#6FDBA8") : Color.white.opacity(0.4)) + .foregroundStyle(state.isPowered ? Color.lumenSuccess : Color.white.opacity(0.4)) } } } @@ -117,8 +117,8 @@ struct DeviceDetailView: View { if let linked = viewModel.linkedPlannedDevice(for: device) { DarkRow(label: "Linked to") { HStack(spacing: 4) { - Image(systemName: "link").font(.caption).foregroundStyle(Color(hex: "#6FDBA8")) - Text(linked.displayName).font(.system(size: 14)).foregroundStyle(Color(hex: "#6FDBA8")) + Image(systemName: "link").font(.caption).foregroundStyle(Color.lumenSuccess) + Text(linked.displayName).font(.system(size: 14)).foregroundStyle(Color.lumenSuccess) } } if let room = linked.room?.name { @@ -138,7 +138,7 @@ struct DeviceDetailView: View { Button { isShowingCommissionSheet = true } label: { Label("Link to Planned Device", systemImage: "link") .font(.system(size: 15, weight: .medium)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) .frame(maxWidth: .infinity, alignment: .leading) .padding(.horizontal, 16) .padding(.vertical, 16) @@ -161,7 +161,7 @@ struct DeviceDetailView: View { get: { state.isPowered }, set: { viewModel.setPower($0, deviceID: device.id) } )) - .tint(Color(hex: "#6FDBA8")) + .tint(Color.lumenSuccess) .labelsHidden() } .padding(.horizontal, 16) @@ -181,7 +181,7 @@ struct DeviceDetailView: View { ), in: 0...1 ) - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) Image(systemName: "sun.max").foregroundStyle(Color.white.opacity(0.8)) } Text("\(Int(state.brightness * 100))%") @@ -206,7 +206,7 @@ struct DeviceDetailView: View { ), in: 1800...6500 ) - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) Image(systemName: "snowflake").foregroundStyle(.blue.opacity(0.8)) } Text("\(state.colorTemperature)K") @@ -257,7 +257,7 @@ struct DeviceDetailView: View { DarkRow(label: "Detected", isLast: true) { Text(state.motionDetected ? "Yes" : "No") .font(.system(size: 14)) - .foregroundStyle(state.motionDetected ? Color(hex: "#C49A6C") : Color.white.opacity(0.4)) + .foregroundStyle(state.motionDetected ? Color.lumenAccent : Color.white.opacity(0.4)) } } } @@ -283,7 +283,7 @@ struct DeviceDetailView: View { systemImage: state.lockState == .locked ? "lock.open.fill" : "lock.fill" ) .font(.system(size: 15, weight: .medium)) - .foregroundStyle(state.lockState == .locked ? Color(hex: "#C49A6C") : Color.white.opacity(0.6)) + .foregroundStyle(state.lockState == .locked ? Color.lumenAccent : Color.white.opacity(0.6)) Spacer() } .padding(.horizontal, 16) diff --git a/Lumen/Features/Devices/DeviceListView.swift b/Lumen/Features/Devices/DeviceListView.swift index 6731e40..219612e 100644 --- a/Lumen/Features/Devices/DeviceListView.swift +++ b/Lumen/Features/Devices/DeviceListView.swift @@ -9,7 +9,7 @@ struct DeviceListView: View { var body: some View { ZStack { - Color(hex: "#0E0819").ignoresSafeArea() + Color.lumenBackground.ignoresSafeArea() scrollContent } .toolbar(.hidden, for: .navigationBar) @@ -78,7 +78,7 @@ struct DeviceListView: View { case .connecting: DiscoveryCard( icon: "antenna.radiowaves.left.and.right", - iconColor: Color(hex: "#C49A6C"), + iconColor: Color.lumenAccent, title: "Looking for devices…", message: "Scanning your HomeKit home for smart devices.", actionLabel: nil, @@ -90,11 +90,11 @@ struct DeviceListView: View { case .authorizationRequired: DiscoveryCard( icon: "house.fill", - iconColor: Color(hex: "#C49A6C"), + iconColor: Color.lumenAccent, title: "Connect to HomeKit", message: "Allow Lumen to access your HomeKit home to discover devices automatically.", actionLabel: "Open Settings", - actionColor: Color(hex: "#C49A6C"), + actionColor: Color.lumenAccent, action: { if let url = URL(string: UIApplication.openSettingsURLString) { UIApplication.shared.open(url) @@ -122,14 +122,14 @@ struct DeviceListView: View { case .authorized: if viewModel.totalCount > 0 { HStack(spacing: 8) { - Circle().fill(Color(hex: "#6FDBA8")).frame(width: 6, height: 6) + Circle().fill(Color.lumenSuccess).frame(width: 6, height: 6) Text("HomeKit · \(viewModel.totalCount) device\(viewModel.totalCount == 1 ? "" : "s") discovered") .font(.system(size: 12)) .foregroundStyle(Color.white.opacity(0.45)) Spacer() Text("\(viewModel.reachableCount) online") .font(.system(size: 12)) - .foregroundStyle(Color(hex: "#6FDBA8")) + .foregroundStyle(Color.lumenSuccess) } .padding(.horizontal, 16) .padding(.vertical, 12) @@ -277,7 +277,7 @@ private struct DeviceDarkRow: View { Spacer() Circle() - .fill(device.reachability == .reachable ? Color(hex: "#6FDBA8") : Color.white.opacity(0.2)) + .fill(device.reachability == .reachable ? Color.lumenSuccess : Color.white.opacity(0.2)) .frame(width: 8, height: 8) Image(systemName: "chevron.right") diff --git a/Lumen/Features/Home/DashboardSetupViews.swift b/Lumen/Features/Home/DashboardSetupViews.swift index e71a5ea..94fb18e 100644 --- a/Lumen/Features/Home/DashboardSetupViews.swift +++ b/Lumen/Features/Home/DashboardSetupViews.swift @@ -9,11 +9,11 @@ struct FirstRunSetupCard: View { HStack(alignment: .top, spacing: 12) { ZStack { Circle() - .fill(Color(hex: "#C49A6C").opacity(0.14)) + .fill(Color.lumenAccent.opacity(0.14)) .frame(width: 44, height: 44) Image(systemName: "door.left.hand.open") .font(.system(size: 18, weight: .semibold)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } VStack(alignment: .leading, spacing: 5) { @@ -45,7 +45,7 @@ struct FirstRunSetupCard: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 13) - .background(Color(hex: "#C49A6C"), in: RoundedRectangle(cornerRadius: 16)) + .background(Color.lumenAccent, in: RoundedRectangle(cornerRadius: 16)) } .accessibilityHint("Opens the room setup sheet.") } @@ -89,9 +89,9 @@ private struct DashboardSetupStepRow: View { private var indicatorFill: Color { switch step.state { case .complete: - return Color(hex: "#6FDBA8").opacity(0.18) + return Color.lumenSuccess.opacity(0.18) case .current: - return Color(hex: "#C49A6C").opacity(0.18) + return Color.lumenAccent.opacity(0.18) case .pending: return Color.white.opacity(0.06) } @@ -100,9 +100,9 @@ private struct DashboardSetupStepRow: View { private var indicatorForeground: Color { switch step.state { case .complete: - return Color(hex: "#6FDBA8") + return Color.lumenSuccess case .current: - return Color(hex: "#C49A6C") + return Color.lumenAccent case .pending: return Color.white.opacity(0.32) } @@ -146,10 +146,10 @@ struct LocationPermissionCard: View { Button(action: action) { Text(presentation.actionTitle) .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) .padding(.horizontal, 12) .padding(.vertical, 7) - .background(Color(hex: "#C49A6C").opacity(0.12), in: RoundedRectangle(cornerRadius: 14)) + .background(Color.lumenAccent.opacity(0.12), in: RoundedRectangle(cornerRadius: 14)) } .padding(.top, 4) } diff --git a/Lumen/Features/Home/HomeDashboardView.swift b/Lumen/Features/Home/HomeDashboardView.swift index 1c8f2d1..a4dda2b 100644 --- a/Lumen/Features/Home/HomeDashboardView.swift +++ b/Lumen/Features/Home/HomeDashboardView.swift @@ -102,7 +102,7 @@ struct HomeDashboardView: View { private var ambientBackground: some View { ZStack { if locationService.isAtHome { - Color(hex: "#0E0819") + Color.lumenBackground LinearGradient( colors: [timeOfDay.backgroundColors.first ?? .clear, .clear], startPoint: .top, @@ -207,10 +207,10 @@ struct HomeDashboardView: View { Text(locationService.isAtHome ? "HOME MODE" : "AWAY MODE") .font(.system(size: 10, weight: .semibold)) .tracking(2) - .foregroundStyle(locationService.isAtHome ? Color(hex: "#C49A6C") : Color.white.opacity(0.35)) + .foregroundStyle(locationService.isAtHome ? Color.lumenAccent : Color.white.opacity(0.35)) Image(systemName: locationService.isAtHome ? "house.fill" : "location.fill") .font(.system(size: 11)) - .foregroundStyle(locationService.isAtHome ? Color(hex: "#C49A6C") : Color.white.opacity(0.35)) + .foregroundStyle(locationService.isAtHome ? Color.lumenAccent : Color.white.opacity(0.35)) } } .padding(.top, 4) @@ -400,10 +400,10 @@ struct HomeDashboardView: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 12) - .background(Color(hex: "#C49A6C"), in: RoundedRectangle(cornerRadius: 18)) + .background(Color.lumenAccent, in: RoundedRectangle(cornerRadius: 18)) } .padding(24) - .background(Color(hex: "#0E0819").ignoresSafeArea()) + .background(Color.lumenBackground.ignoresSafeArea()) } } @@ -628,11 +628,11 @@ private struct LumenNoticedCard: View { HStack(spacing: 8) { Image(systemName: icon) .font(.system(size: 12)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) Text("Lumen noticed") .font(.system(size: 11, weight: .semibold)) .tracking(1) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } Text(message) @@ -690,7 +690,7 @@ private struct LumenNoticedCard: View { private var footerIcon: some View { Image(systemName: isActionable ? "chevron.right" : "info.circle") .font(.system(size: 12, weight: .semibold)) - .foregroundStyle(isActionable ? Color(hex: "#C49A6C") : Color.white.opacity(0.35)) + .foregroundStyle(isActionable ? Color.lumenAccent : Color.white.opacity(0.35)) .frame(width: 24, height: 24) } } diff --git a/Lumen/Features/Home/LumenActionView.swift b/Lumen/Features/Home/LumenActionView.swift index 330c681..f42f8e9 100644 --- a/Lumen/Features/Home/LumenActionView.swift +++ b/Lumen/Features/Home/LumenActionView.swift @@ -28,7 +28,7 @@ struct LumenActionView: View { .padding(.horizontal, 24) .padding(.top, 12) .padding(.bottom, 28) - .background(Color(hex: "#0E0819").ignoresSafeArea()) + .background(Color.lumenBackground.ignoresSafeArea()) .presentationDetents([.medium, .large]) .presentationDragIndicator(.hidden) } @@ -50,7 +50,7 @@ struct LumenActionView: View { .frame(width: 64, height: 64) Image(systemName: scene.iconName) .font(.system(size: 26)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } VStack(spacing: 6) { @@ -95,7 +95,7 @@ struct LumenActionView: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 16) - .background(Color(hex: "#C49A6C"), in: RoundedRectangle(cornerRadius: 18)) + .background(Color.lumenAccent, in: RoundedRectangle(cornerRadius: 18)) } .buttonStyle(.pressable) .padding(.bottom, 10) diff --git a/Lumen/Features/Home/LumenReasoningView.swift b/Lumen/Features/Home/LumenReasoningView.swift index e928074..698d28c 100644 --- a/Lumen/Features/Home/LumenReasoningView.swift +++ b/Lumen/Features/Home/LumenReasoningView.swift @@ -27,7 +27,7 @@ struct LumenReasoningView: View { .padding(.horizontal, 24) .padding(.top, 12) .padding(.bottom, 28) - .background(Color(hex: "#0E0819").ignoresSafeArea()) + .background(Color.lumenBackground.ignoresSafeArea()) .presentationDetents([.medium, .large]) .presentationDragIndicator(.hidden) } @@ -49,7 +49,7 @@ struct LumenReasoningView: View { .frame(width: 64, height: 64) Image(systemName: "sparkles") .font(.system(size: 26)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } VStack(spacing: 6) { Text("WHY LUMEN NOTICED") @@ -133,7 +133,7 @@ struct LumenReasoningView: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 16) - .background(Color(hex: "#C49A6C"), in: RoundedRectangle(cornerRadius: 18)) + .background(Color.lumenAccent, in: RoundedRectangle(cornerRadius: 18)) } .padding(.bottom, 10) } @@ -169,7 +169,7 @@ enum SignalWeight: Equatable { var tint: Color { switch self { - case .high: return Color(hex: "#C49A6C") + case .high: return Color.lumenAccent case .medium: return Color.white.opacity(0.55) case .low: return Color.white.opacity(0.3) } diff --git a/Lumen/Features/Home/OnboardingView.swift b/Lumen/Features/Home/OnboardingView.swift index 34f6d26..91957c9 100644 --- a/Lumen/Features/Home/OnboardingView.swift +++ b/Lumen/Features/Home/OnboardingView.swift @@ -11,7 +11,7 @@ struct OnboardingView: View { var body: some View { ZStack { - Color(hex: "#0E0819").ignoresSafeArea() + Color.lumenBackground.ignoresSafeArea() VStack(spacing: 0) { Spacer() @@ -23,7 +23,7 @@ struct OnboardingView: View { .frame(width: 80, height: 80) Image(systemName: "house.fill") .font(.system(size: 34)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } VStack(spacing: 8) { @@ -56,7 +56,7 @@ struct OnboardingView: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 16) - .background(Color(hex: "#C49A6C"), in: RoundedRectangle(cornerRadius: 16)) + .background(Color.lumenAccent, in: RoundedRectangle(cornerRadius: 16)) } .disabled(homeName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) .opacity(homeName.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? 0.5 : 1) diff --git a/Lumen/Features/Rooms/RoomDetailView.swift b/Lumen/Features/Rooms/RoomDetailView.swift index 0c23ed9..d40e67c 100644 --- a/Lumen/Features/Rooms/RoomDetailView.swift +++ b/Lumen/Features/Rooms/RoomDetailView.swift @@ -12,7 +12,7 @@ struct RoomDetailView: View { var body: some View { ZStack { - Color(hex: "#0E0819").ignoresSafeArea() + Color.lumenBackground.ignoresSafeArea() scrollContent } .toolbar(.hidden, for: .navigationBar) @@ -171,7 +171,7 @@ private struct LiveDeviceDarkRow: View { Spacer() Circle() - .fill(device.reachability == .reachable ? Color(hex: "#6FDBA8") : Color.white.opacity(0.2)) + .fill(device.reachability == .reachable ? Color.lumenSuccess : Color.white.opacity(0.2)) .frame(width: 8, height: 8) Image(systemName: "chevron.right") diff --git a/Lumen/Features/Rooms/RoomListView.swift b/Lumen/Features/Rooms/RoomListView.swift index bc76b25..b5ed336 100644 --- a/Lumen/Features/Rooms/RoomListView.swift +++ b/Lumen/Features/Rooms/RoomListView.swift @@ -9,7 +9,7 @@ struct RoomListView: View { var body: some View { ZStack { - Color(hex: "#0E0819").ignoresSafeArea() + Color.lumenBackground.ignoresSafeArea() if viewModel.rooms.isEmpty { emptyState diff --git a/Lumen/Features/Scenes/SceneApprovalSheet.swift b/Lumen/Features/Scenes/SceneApprovalSheet.swift index 31609a1..b6b216e 100644 --- a/Lumen/Features/Scenes/SceneApprovalSheet.swift +++ b/Lumen/Features/Scenes/SceneApprovalSheet.swift @@ -28,7 +28,7 @@ struct SceneApprovalSheet: View { .padding(.horizontal, 24) .padding(.top, 12) .padding(.bottom, 28) - .background(Color(hex: "#0E0819").ignoresSafeArea()) + .background(Color.lumenBackground.ignoresSafeArea()) .presentationDetents([.medium, .large]) .presentationDragIndicator(.hidden) } @@ -50,7 +50,7 @@ struct SceneApprovalSheet: View { .frame(width: 64, height: 64) Image(systemName: scene.iconName) .font(.system(size: 26)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } VStack(spacing: 6) { @@ -95,7 +95,7 @@ struct SceneApprovalSheet: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 16) - .background(Color(hex: "#C49A6C"), in: RoundedRectangle(cornerRadius: 18)) + .background(Color.lumenAccent, in: RoundedRectangle(cornerRadius: 18)) } .buttonStyle(.pressable) .padding(.bottom, 10) diff --git a/Lumen/Features/Scenes/SceneDetailView.swift b/Lumen/Features/Scenes/SceneDetailView.swift index 2e71b91..e1dc4ee 100644 --- a/Lumen/Features/Scenes/SceneDetailView.swift +++ b/Lumen/Features/Scenes/SceneDetailView.swift @@ -20,7 +20,7 @@ struct SceneDetailView: View { var body: some View { ZStack { - Color(hex: "#0E0819").ignoresSafeArea() + Color.lumenBackground.ignoresSafeArea() ScrollView { VStack(alignment: .leading, spacing: 28) { header @@ -73,7 +73,7 @@ struct SceneDetailView: View { .frame(width: 52, height: 52) Image(systemName: scene.iconName) .font(.system(size: 22)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } VStack(alignment: .leading, spacing: 4) { @@ -101,7 +101,7 @@ struct SceneDetailView: View { .foregroundStyle(.white) .frame(maxWidth: .infinity) .padding(.vertical, 14) - .background(Color(hex: "#C49A6C"), in: RoundedRectangle(cornerRadius: 16)) + .background(Color.lumenAccent, in: RoundedRectangle(cornerRadius: 16)) } } } @@ -127,7 +127,7 @@ struct SceneDetailView: View { } } .pickerStyle(.menu) - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) .onChange(of: selectedGeofenceTrigger) { _, newValue in viewModel.updateScene(scene, geofenceTrigger: newValue) } @@ -155,7 +155,7 @@ struct SceneDetailView: View { .font(.system(size: 15, weight: .medium)) .foregroundStyle(.white) } - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) .onChange(of: isScheduled) { _, enabled in // Ignore the toggle flip that comes from onAppear syncing to the model. guard enabled != (scene.scheduleMinutesSinceMidnight != nil) else { return } @@ -174,7 +174,7 @@ struct SceneDetailView: View { displayedComponents: .hourAndMinute ) .datePickerStyle(.compact) - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) .foregroundStyle(.white) .onChange(of: scheduleTime) { _, newValue in let mins = Self.minutes(from: newValue) @@ -229,7 +229,7 @@ struct SceneDetailView: View { } label: { Image(systemName: "plus.circle.fill") .font(.system(size: 20)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } } diff --git a/Lumen/Features/Scenes/SceneListView.swift b/Lumen/Features/Scenes/SceneListView.swift index 42de681..a0f2b9e 100644 --- a/Lumen/Features/Scenes/SceneListView.swift +++ b/Lumen/Features/Scenes/SceneListView.swift @@ -18,7 +18,7 @@ struct SceneListView: View { var body: some View { ZStack { - Color(hex: "#0E0819").ignoresSafeArea() + Color.lumenBackground.ignoresSafeArea() if scenes.isEmpty { emptyState @@ -212,7 +212,7 @@ private struct ActiveSceneCard: View { RoundedRectangle(cornerRadius: 20) .fill( LinearGradient( - colors: [Color(hex: "#1E1A3A"), Color(hex: "#0E0819")], + colors: [Color(hex: "#1E1A3A"), Color.lumenBackground], startPoint: .topLeading, endPoint: .bottomTrailing ) @@ -223,16 +223,16 @@ private struct ActiveSceneCard: View { if isExecuting { ProgressView() .scaleEffect(0.6) - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) } else { Circle() - .fill(Color(hex: "#C49A6C")) + .fill(Color.lumenAccent) .frame(width: 6, height: 6) } Text("ACTIVE NOW") .font(.system(size: 10, weight: .semibold)) .tracking(2) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) } Text(scene.name) diff --git a/Lumen/Features/Settings/SettingsView.swift b/Lumen/Features/Settings/SettingsView.swift index b8dcc6c..b73b0eb 100644 --- a/Lumen/Features/Settings/SettingsView.swift +++ b/Lumen/Features/Settings/SettingsView.swift @@ -15,7 +15,7 @@ struct SettingsView: View { var body: some View { ZStack { - Color(hex: "#0E0819").ignoresSafeArea() + Color.lumenBackground.ignoresSafeArea() scrollContent } .toolbar(.hidden, for: .navigationBar) @@ -77,7 +77,7 @@ struct SettingsView: View { HStack { Text("Use current location") .font(.system(size: 15, weight: .medium)) - .foregroundStyle(Color(hex: "#C49A6C")) + .foregroundStyle(Color.lumenAccent) Spacer() } .padding(.horizontal, 16) @@ -309,7 +309,7 @@ private struct SettingsOptionRow Text(option[keyPath: title]).tag(option) } } - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) .labelsHidden() } .padding(.horizontal, 16) @@ -344,7 +344,7 @@ private struct SettingsStepperRow: View { .foregroundStyle(Color.white.opacity(0.45)) } } - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) .padding(.horizontal, 16) .padding(.vertical, 12) .overlay(alignment: .bottom) { @@ -414,7 +414,7 @@ private struct SettingsToggleRow: View { .foregroundStyle(.white) Spacer() Toggle("", isOn: $isOn) - .tint(Color(hex: "#C49A6C")) + .tint(Color.lumenAccent) .labelsHidden() } .padding(.horizontal, 16) diff --git a/Lumen/Models/TimeOfDay.swift b/Lumen/Models/TimeOfDay.swift index 298c7e5..b9158d5 100644 --- a/Lumen/Models/TimeOfDay.swift +++ b/Lumen/Models/TimeOfDay.swift @@ -35,11 +35,11 @@ enum TimeOfDay: Equatable { var backgroundColors: [Color] { switch self { - case .dawn: return [Color(hex: "#1A0F35"), Color(hex: "#0E0819")] - case .morning: return [Color(hex: "#0F1530"), Color(hex: "#0E0819")] - case .afternoon: return [Color(hex: "#0D1228"), Color(hex: "#0E0819")] - case .evening: return [Color(hex: "#1E0F38"), Color(hex: "#0E0819")] - case .night: return [Color(hex: "#080618"), Color(hex: "#0E0819")] + case .dawn: return [Color(hex: "#1A0F35"), Color.lumenBackground] + case .morning: return [Color(hex: "#0F1530"), Color.lumenBackground] + case .afternoon: return [Color(hex: "#0D1228"), Color.lumenBackground] + case .evening: return [Color(hex: "#1E0F38"), Color.lumenBackground] + case .night: return [Color(hex: "#080618"), Color.lumenBackground] } } diff --git a/Lumen/Support/LumenColors.swift b/Lumen/Support/LumenColors.swift new file mode 100644 index 0000000..b511af0 --- /dev/null +++ b/Lumen/Support/LumenColors.swift @@ -0,0 +1,24 @@ +import SwiftUI + +// MARK: - Lumen Colors +// A single, named source of truth for Lumen's semantic palette. These three +// colors were previously repeated as raw `Color(hex:)` literals across ~14 view +// files; naming them here keeps the brand consistent and makes a palette change +// a one-line edit instead of a find-and-replace. +// +// Each token is deliberately equal to the exact hex it replaced, so this is a +// behavior-preserving rename. Contextual one-off colors (gradient tints, per-mood +// backgrounds) intentionally stay as inline `Color(hex:)` — only the repeated, +// semantic colors live here. + +extension Color { + + /// Warm gold — Lumen's primary accent (highlights, primary CTAs, "Lumen noticed"). + static let lumenAccent = Color(hex: "#C49A6C") + + /// The primary dark app background, and the base stop of the time-of-day gradients. + static let lumenBackground = Color(hex: "#0E0819") + + /// Success / reachable-device green (online indicators, confirmations). + static let lumenSuccess = Color(hex: "#6FDBA8") +} diff --git a/LumenTests/LumenColorsTests.swift b/LumenTests/LumenColorsTests.swift new file mode 100644 index 0000000..5cc0193 --- /dev/null +++ b/LumenTests/LumenColorsTests.swift @@ -0,0 +1,22 @@ +import XCTest +import SwiftUI +@testable import Lumen + +// MARK: - LumenColors Tests +// Guards the semantic palette: each token must equal the exact hex it replaced, +// so the token layer stays a behavior-preserving source of truth. + +final class LumenColorsTests: XCTestCase { + + func testAccentMatchesSourceHex() { + XCTAssertEqual(Color.lumenAccent, Color(hex: "#C49A6C")) + } + + func testBackgroundMatchesSourceHex() { + XCTAssertEqual(Color.lumenBackground, Color(hex: "#0E0819")) + } + + func testSuccessMatchesSourceHex() { + XCTAssertEqual(Color.lumenSuccess, Color(hex: "#6FDBA8")) + } +}