From e22616ff71098b808d47dcc6ae92447cb4fe6185 Mon Sep 17 00:00:00 2001 From: sorcrr Date: Thu, 15 Oct 2026 00:08:19 +0300 Subject: [PATCH 1/4] Use dark text on the green primary buttons The bordered-prominent buttons tinted Razer green drew the system's white label on #44D62C, about 1.9:1 contrast, and macOS gives no way to recolour it. A shared RazerProminentButtonStyle draws the green rounded rect with onRazerGreen text instead, matching the selected chips, with pressed and disabled states and sizing off the control size. --- CHANGELOG.md | 4 +++ Sources/MacRazer/CardComponents.swift | 41 ++++++++++++++++++++++++++ Sources/MacRazer/CrashReportView.swift | 3 +- Sources/MacRazer/PermissionsView.swift | 7 ++--- Sources/MacRazer/PopoverView.swift | 9 ++---- Sources/MacRazer/SettingsView.swift | 3 +- Sources/MacRazer/UpdatedView.swift | 3 +- Sources/MacRazer/WhatsNewPage.swift | 3 +- 8 files changed, 55 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f62731..c5867c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ expect rough edges until 1.0. instead of white, which was hard to read. If you liked it dark, pick Dark in Settings › Appearance. +- **Green buttons have dark text.** Install Now, Send Report, Grant, Relaunch, Done and the + update buttons used white on the green, which was hard to read. They now use the same dark + text as the selected chips. + - **The popover is shorter: settings below DPI collapse.** Polling rate, Lighting, Power Management, Buttons and Profiles are now compact cards of one row each, each showing its current value ("1000 Hz", "Static · 80%", the active profile). Click a row to open its diff --git a/Sources/MacRazer/CardComponents.swift b/Sources/MacRazer/CardComponents.swift index 6bb4f9e..9b01286 100644 --- a/Sources/MacRazer/CardComponents.swift +++ b/Sources/MacRazer/CardComponents.swift @@ -71,6 +71,47 @@ extension View { func hoverHighlight(_ shape: S) -> some View { modifier(HoverHighlight(shape: shape)) } } +/// The app's primary-action button: a Razer-green rounded rect with dark text, in place of +/// `.borderedProminent.tint(.razerGreen)`. That draws the system's white label on the green, +/// about 1.9:1 contrast, and macOS gives no way to recolour it; this matches the selected +/// chips' `onRazerGreen` instead. Sized off the control size (regular or small), dimmed when +/// disabled, darkened while pressed. `keyboardShortcut(.defaultAction)` still works on it. +/// +/// `prominent: false` is the same shape in a neutral fill, for a button that is only the +/// default action some of the time (the Permissions window's Close / Done). +struct RazerProminentButtonStyle: ButtonStyle { + var prominent = true + + func makeBody(configuration: Configuration) -> some View { + StyledButton(configuration: configuration, prominent: prominent) + } + + // A view rather than inline in `makeBody`, so it can read the control size, enabled state + // and font from the environment. A font the caller set wins; otherwise the system's + // button size for the control size. + private struct StyledButton: View { + let configuration: Configuration + let prominent: Bool + @Environment(\.controlSize) private var controlSize + @Environment(\.isEnabled) private var isEnabled + @Environment(\.font) private var font + + var body: some View { + let small = controlSize == .small || controlSize == .mini + let shape = RoundedRectangle(cornerRadius: small ? 5 : 6, style: .continuous) + configuration.label + .font(font ?? .system(size: small ? 11 : 13)) + .foregroundStyle(prominent ? Color.onRazerGreen : Color.primary) + .padding(.horizontal, small ? 8 : 10) + .padding(.vertical, small ? 2 : 3) + .background(prominent ? Color.razerGreen : Color.primary.opacity(0.12), in: shape) + .overlay(shape.fill(Color.black.opacity(configuration.isPressed ? 0.18 : 0))) + .opacity(isEnabled ? 1 : 0.45) + .contentShape(shape) + } + } +} + /// Small secondary-styled section heading used inside cards. func sectionLabel(_ text: String, _ symbol: String) -> some View { Label(text, systemImage: symbol) diff --git a/Sources/MacRazer/CrashReportView.swift b/Sources/MacRazer/CrashReportView.swift index 3ab16b6..3169371 100644 --- a/Sources/MacRazer/CrashReportView.swift +++ b/Sources/MacRazer/CrashReportView.swift @@ -112,8 +112,7 @@ struct CrashReportView: View { if model.state == .sending { ProgressView().controlSize(.small) } else { Text("Send Report") } } .keyboardShortcut(.defaultAction) - .buttonStyle(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) .disabled(!model.canSend) } } diff --git a/Sources/MacRazer/PermissionsView.swift b/Sources/MacRazer/PermissionsView.swift index cb365db..5397900 100644 --- a/Sources/MacRazer/PermissionsView.swift +++ b/Sources/MacRazer/PermissionsView.swift @@ -89,7 +89,7 @@ struct PermissionsView: View { Text("Grant…").font(.system(size: 12, weight: .medium)) .frame(maxWidth: .infinity) } - .buttonStyle(.borderedProminent).tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) Button("Open Settings", action: openSettings) .buttonStyle(.bordered) .font(.system(size: 12)) @@ -129,7 +129,7 @@ struct PermissionsView: View { Spacer(minLength: 6) VStack(alignment: .trailing, spacing: 4) { Button("Relaunch") { model.relaunch() } - .buttonStyle(.borderedProminent).tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) .font(.system(size: 12, weight: .medium)) if model.relaunchFailed { Text("Couldn't relaunch. Quit and reopen MacRazer yourself.") @@ -193,8 +193,7 @@ struct PermissionsView: View { } Spacer() Button(model.allRequiredGranted ? "Done" : "Close") { onDone?() } - .buttonStyle(.borderedProminent) - .tint(model.allRequiredGranted ? .razerGreen : .gray) + .buttonStyle(RazerProminentButtonStyle(prominent: model.allRequiredGranted)) .font(.system(size: 12, weight: .medium)) } } diff --git a/Sources/MacRazer/PopoverView.swift b/Sources/MacRazer/PopoverView.swift index 98cb782..3d4ffc9 100644 --- a/Sources/MacRazer/PopoverView.swift +++ b/Sources/MacRazer/PopoverView.swift @@ -590,8 +590,7 @@ struct PopoverView: View { .padding(.horizontal, 8).padding(.vertical, 4) .background(Color.primary.opacity(0.08), in: RoundedRectangle(cornerRadius: 6)) Button("Save", action: saveNewProfile) - .buttonStyle(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) .controlSize(.small) .disabled(newProfileName.trimmingCharacters(in: .whitespaces).isEmpty) Button("Cancel") { isAddingProfile = false } @@ -694,8 +693,7 @@ struct PopoverView: View { .frame(maxWidth: .infinity) .padding(.vertical, 5) } - .buttonStyle(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) .controlSize(.small) } @@ -792,8 +790,7 @@ struct PopoverView: View { .font(.system(size: 11)).foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) Button("Quit MacRazer") { NSApplication.shared.terminate(nil) } - .buttonStyle(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) .controlSize(.small) .font(.system(size: 11.5, weight: .medium)) } diff --git a/Sources/MacRazer/SettingsView.swift b/Sources/MacRazer/SettingsView.swift index e418da6..df50a20 100644 --- a/Sources/MacRazer/SettingsView.swift +++ b/Sources/MacRazer/SettingsView.swift @@ -190,8 +190,7 @@ struct SettingsView: View { Task { await updateChecker.downloadAndInstall() } } .controlSize(.small) - .buttonStyle(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) } Button { Task { await updateChecker.checkForUpdatesNow(userRequested: true) } diff --git a/Sources/MacRazer/UpdatedView.swift b/Sources/MacRazer/UpdatedView.swift index 1ef60c7..3d1f2f1 100644 --- a/Sources/MacRazer/UpdatedView.swift +++ b/Sources/MacRazer/UpdatedView.swift @@ -23,8 +23,7 @@ struct UpdatedView: View { Spacer() Button("Done", action: onDone) .keyboardShortcut(.defaultAction) - .buttonStyle(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) } } .padding(22) diff --git a/Sources/MacRazer/WhatsNewPage.swift b/Sources/MacRazer/WhatsNewPage.swift index a8265c6..7ce1f50 100644 --- a/Sources/MacRazer/WhatsNewPage.swift +++ b/Sources/MacRazer/WhatsNewPage.swift @@ -54,8 +54,7 @@ struct WhatsNewPage: View { .frame(maxWidth: .infinity) .padding(.vertical, 5) } - .buttonStyle(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) .controlSize(.small) .padding(12) } From 34bc2cdcb6f59f70b42dee2fa3d9600d152a0969 Mon Sep 17 00:00:00 2001 From: sorcrr Date: Thu, 15 Oct 2026 00:09:51 +0300 Subject: [PATCH 2/4] Add focus ring and hover to the green button; fix review findings Draw a focus ring for keyboard navigation and the hover wash the app's other custom buttons use. Permissions' Close stays a plain bordered button like its neighbour, and the Send Report spinner draws dark so it shows on the green. --- Sources/MacRazer/CardComponents.swift | 19 +++++++++---------- Sources/MacRazer/CrashReportView.swift | 5 ++++- Sources/MacRazer/PermissionsView.swift | 13 ++++++++++--- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/Sources/MacRazer/CardComponents.swift b/Sources/MacRazer/CardComponents.swift index 9b01286..e142498 100644 --- a/Sources/MacRazer/CardComponents.swift +++ b/Sources/MacRazer/CardComponents.swift @@ -75,15 +75,11 @@ extension View { /// `.borderedProminent.tint(.razerGreen)`. That draws the system's white label on the green, /// about 1.9:1 contrast, and macOS gives no way to recolour it; this matches the selected /// chips' `onRazerGreen` instead. Sized off the control size (regular or small), dimmed when -/// disabled, darkened while pressed. `keyboardShortcut(.defaultAction)` still works on it. -/// -/// `prominent: false` is the same shape in a neutral fill, for a button that is only the -/// default action some of the time (the Permissions window's Close / Done). +/// disabled, darkened while pressed, with a hover wash and a focus ring. +/// `keyboardShortcut(.defaultAction)` still works on it. struct RazerProminentButtonStyle: ButtonStyle { - var prominent = true - func makeBody(configuration: Configuration) -> some View { - StyledButton(configuration: configuration, prominent: prominent) + StyledButton(configuration: configuration) } // A view rather than inline in `makeBody`, so it can read the control size, enabled state @@ -91,22 +87,25 @@ struct RazerProminentButtonStyle: ButtonStyle { // button size for the control size. private struct StyledButton: View { let configuration: Configuration - let prominent: Bool @Environment(\.controlSize) private var controlSize @Environment(\.isEnabled) private var isEnabled @Environment(\.font) private var font + @Environment(\.isFocused) private var isFocused var body: some View { let small = controlSize == .small || controlSize == .mini let shape = RoundedRectangle(cornerRadius: small ? 5 : 6, style: .continuous) configuration.label .font(font ?? .system(size: small ? 11 : 13)) - .foregroundStyle(prominent ? Color.onRazerGreen : Color.primary) + .foregroundStyle(Color.onRazerGreen) .padding(.horizontal, small ? 8 : 10) .padding(.vertical, small ? 2 : 3) - .background(prominent ? Color.razerGreen : Color.primary.opacity(0.12), in: shape) + .background(Color.razerGreen, in: shape) .overlay(shape.fill(Color.black.opacity(configuration.isPressed ? 0.18 : 0))) + .hoverHighlight(shape) .opacity(isEnabled ? 1 : 0.45) + // A custom style loses the system focus ring; draw one for keyboard navigation. + .overlay(shape.inset(by: -2).stroke(Color.accentColor.opacity(isFocused ? 0.8 : 0), lineWidth: 2.5)) .contentShape(shape) } } diff --git a/Sources/MacRazer/CrashReportView.swift b/Sources/MacRazer/CrashReportView.swift index 3169371..56cf40d 100644 --- a/Sources/MacRazer/CrashReportView.swift +++ b/Sources/MacRazer/CrashReportView.swift @@ -109,7 +109,10 @@ struct CrashReportView: View { Button { Task { await model.send() } } label: { - if model.state == .sending { ProgressView().controlSize(.small) } else { Text("Send Report") } + if model.state == .sending { + // The light scheme draws the spinner dark, so it reads on the green. + ProgressView().controlSize(.small).environment(\.colorScheme, .light) + } else { Text("Send Report") } } .keyboardShortcut(.defaultAction) .buttonStyle(RazerProminentButtonStyle()) diff --git a/Sources/MacRazer/PermissionsView.swift b/Sources/MacRazer/PermissionsView.swift index 5397900..aa68c30 100644 --- a/Sources/MacRazer/PermissionsView.swift +++ b/Sources/MacRazer/PermissionsView.swift @@ -192,9 +192,16 @@ struct PermissionsView: View { } } Spacer() - Button(model.allRequiredGranted ? "Done" : "Close") { onDone?() } - .buttonStyle(RazerProminentButtonStyle(prominent: model.allRequiredGranted)) - .font(.system(size: 12, weight: .medium)) + // Green once there's nothing left to do; until then a plain button like its neighbour. + if model.allRequiredGranted { + Button("Done") { onDone?() } + .buttonStyle(RazerProminentButtonStyle()) + .font(.system(size: 12, weight: .medium)) + } else { + Button("Close") { onDone?() } + .buttonStyle(.bordered) + .font(.system(size: 12, weight: .medium)) + } } } } From c37859aaae0b4764c1ddce5fb9deaa42e8b82eea Mon Sep 17 00:00:00 2001 From: sorcrr Date: Thu, 15 Oct 2026 00:38:52 +0300 Subject: [PATCH 3/4] Make the remaining default buttons green Settings, About, Crash Report's post-send Done and the device test's Start/Run/Next/Done were plain default buttons, so they took the system accent colour (blue) instead of the app's green. --- CHANGELOG.md | 7 ++++--- Sources/MacRazer/AboutView.swift | 1 + Sources/MacRazer/CrashReportView.swift | 1 + Sources/MacRazer/DeviceTestView.swift | 18 +++++++++--------- Sources/MacRazer/SettingsView.swift | 1 + 5 files changed, 16 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11b7ac1..943671e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,9 +13,10 @@ expect rough edges until 1.0. instead of white, which was hard to read. If you liked it dark, pick Dark in Settings › Appearance. -- **Green buttons have dark text.** Install Now, Send Report, Grant, Relaunch, Done and the - update buttons used white on the green, which was hard to read. They now use the same dark - text as the selected chips. +- **Green buttons have dark text.** Install Now, Send Report, Grant, Relaunch and the update + buttons used white on the green, which was hard to read. They now use the same dark text as + the selected chips. The main button in Settings, About and the device test was system blue; + it is green now too. - **The popover is shorter: settings below DPI collapse.** Polling rate, Lighting, Power Management, Buttons and Profiles are now compact cards of one row each, each showing diff --git a/Sources/MacRazer/AboutView.swift b/Sources/MacRazer/AboutView.swift index a7d185c..5466b1f 100644 --- a/Sources/MacRazer/AboutView.swift +++ b/Sources/MacRazer/AboutView.swift @@ -181,6 +181,7 @@ struct AboutView: View { Spacer(minLength: 0) Button("Done") { onDone() } .keyboardShortcut(.defaultAction) + .buttonStyle(RazerProminentButtonStyle()) } .controlSize(.small) .font(.system(size: 11.5)) diff --git a/Sources/MacRazer/CrashReportView.swift b/Sources/MacRazer/CrashReportView.swift index 283ae42..271b1e7 100644 --- a/Sources/MacRazer/CrashReportView.swift +++ b/Sources/MacRazer/CrashReportView.swift @@ -107,6 +107,7 @@ struct CrashReportView: View { if model.state == .sent { Button("Done", action: onDone) .keyboardShortcut(.defaultAction) + .buttonStyle(RazerProminentButtonStyle()) } else { Button("Don't Send", action: onDone) .keyboardShortcut(.cancelAction) diff --git a/Sources/MacRazer/DeviceTestView.swift b/Sources/MacRazer/DeviceTestView.swift index 7ef92e6..8940e0b 100644 --- a/Sources/MacRazer/DeviceTestView.swift +++ b/Sources/MacRazer/DeviceTestView.swift @@ -312,20 +312,20 @@ struct DeviceTestView: View { case .intro: Button("Cancel", action: onClose) Spacer() - Button("Start") { model.start() }.keyboardShortcut(.defaultAction) + Button("Start") { model.start() }.keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) case .permission: Button("Continue without it") { model.continueWithoutAccess() } Spacer() if model.needsRelaunch { - Button("Quit and reopen") { model.relaunchAndResume() }.keyboardShortcut(.defaultAction) + Button("Quit and reopen") { model.relaunchAndResume() }.keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) } else { - Button("Grant access") { model.grantAccess() }.keyboardShortcut(.defaultAction) + Button("Grant access") { model.grantAccess() }.keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) } case .review: Button("Copy") { model.copyReport() } Button("Open GitHub issue") { model.openGitHubIssue() } Spacer() - Button("Done", action: onClose).keyboardShortcut(.defaultAction) + Button("Done", action: onClose).keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) default: Button("Back") { model.back() }.disabled(model.running || model.stage == .identify) Spacer() @@ -345,19 +345,19 @@ struct DeviceTestView: View { switch model.stage { case .dpi where model.report?.dpi.data == nil && model.report?.dpi.error == nil: Button("Skip") { model.next() }.disabled(model.running) - Button("Run") { Task { await model.runDPI() } }.disabled(model.running).keyboardShortcut(.defaultAction) + Button("Run") { Task { await model.runDPI() } }.disabled(model.running).keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) case .polling where model.report?.polling.data == nil && model.report?.polling.error == nil: Button("Skip") { model.next() }.disabled(model.running) - Button("Run") { Task { await model.runPolling() } }.disabled(model.running).keyboardShortcut(.defaultAction) + Button("Run") { Task { await model.runPolling() } }.disabled(model.running).keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) case .lighting where model.report?.lighting.data == nil: Button("Skip") { model.next() }.disabled(model.running) - Button("Run") { Task { await model.runLighting() } }.disabled(model.running).keyboardShortcut(.defaultAction) + Button("Run") { Task { await model.runLighting() } }.disabled(model.running).keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) case .buttons: - Button(model.buttonsSeen.isEmpty ? "Skip" : "Next") { model.next() }.keyboardShortcut(.defaultAction) + Button(model.buttonsSeen.isEmpty ? "Skip" : "Next") { model.next() }.keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) default: Button("Next") { model.next() } .disabled(model.running || model.report == nil || (model.stage == .lighting && !model.lightingAnswered)) - .keyboardShortcut(.defaultAction) + .keyboardShortcut(.defaultAction).buttonStyle(RazerProminentButtonStyle()) } } diff --git a/Sources/MacRazer/SettingsView.swift b/Sources/MacRazer/SettingsView.swift index e12ce8c..304c344 100644 --- a/Sources/MacRazer/SettingsView.swift +++ b/Sources/MacRazer/SettingsView.swift @@ -261,6 +261,7 @@ struct SettingsView: View { Spacer() Button("Done") { onDone() } .keyboardShortcut(.defaultAction) + .buttonStyle(RazerProminentButtonStyle()) } } } From a29e098065176ad81bd93ea7521e2ca850e00f3c Mon Sep 17 00:00:00 2001 From: sorcrr Date: Thu, 15 Oct 2026 04:32:04 +0300 Subject: [PATCH 4/4] Make the whole add-profile circle clickable The circle's frame, fill and stroke sat outside the plain button's label, so only the plus glyph was hit-tested. Move them into the label with a circular content shape. --- CHANGELOG.md | 4 ++++ Sources/MacRazer/PopoverView.swift | 11 +++++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 943671e..06b374d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -103,6 +103,10 @@ expect rough edges until 1.0. news isn't repeated. After an automatic install it comes to the front without taking your keyboard focus. +### Fixed +- **The add-profile plus button works anywhere in its circle.** Only the plus sign itself took + the click. The rest of the circle did nothing. + ## [0.5.0] — 2026-09-23 ### Changed diff --git a/Sources/MacRazer/PopoverView.swift b/Sources/MacRazer/PopoverView.swift index bc1fb11..813e12f 100644 --- a/Sources/MacRazer/PopoverView.swift +++ b/Sources/MacRazer/PopoverView.swift @@ -564,13 +564,16 @@ struct PopoverView: View { newProfileName = suggestedProfileName isAddingProfile = true } label: { + // The circle is part of the label, so the whole of it takes the click, not just + // the glyph a plain button would otherwise hit-test. Image(systemName: "plus").font(.system(size: 10.5, weight: .semibold)) + .foregroundStyle(Color.razerGreenText) + .frame(width: 24, height: 24) + .background(Color.razerGreen.opacity(0.12), in: Circle()) + .overlay(Circle().stroke(Color.razerGreen.opacity(0.55), lineWidth: 1)) + .contentShape(Circle()) } .buttonStyle(.plain) - .foregroundStyle(Color.razerGreenText) - .frame(width: 24, height: 24) - .background(Color.razerGreen.opacity(0.12), in: Circle()) - .overlay(Circle().stroke(Color.razerGreen.opacity(0.55), lineWidth: 1)) // dpi == 0 = the first settings read hasn't landed yet; a snapshot taken now // would save a profile that later applies as 100 DPI. .disabled(controller.dpi == 0)