diff --git a/CHANGELOG.md b/CHANGELOG.md index b0dcc65..06b374d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,11 @@ 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 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 its current value ("1000 Hz", "Static · 80%", the active profile). Click a row to open its @@ -98,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/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/CardComponents.swift b/Sources/MacRazer/CardComponents.swift index 6bb4f9e..e142498 100644 --- a/Sources/MacRazer/CardComponents.swift +++ b/Sources/MacRazer/CardComponents.swift @@ -71,6 +71,46 @@ 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, with a hover wash and a focus ring. +/// `keyboardShortcut(.defaultAction)` still works on it. +struct RazerProminentButtonStyle: ButtonStyle { + func makeBody(configuration: Configuration) -> some View { + StyledButton(configuration: configuration) + } + + // 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 + @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(Color.onRazerGreen) + .padding(.horizontal, small ? 8 : 10) + .padding(.vertical, small ? 2 : 3) + .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) + } + } +} + /// 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 65a0d33..271b1e7 100644 --- a/Sources/MacRazer/CrashReportView.swift +++ b/Sources/MacRazer/CrashReportView.swift @@ -107,17 +107,20 @@ struct CrashReportView: View { if model.state == .sent { Button("Done", action: onDone) .keyboardShortcut(.defaultAction) + .buttonStyle(RazerProminentButtonStyle()) } else { Button("Don't Send", action: onDone) .keyboardShortcut(.cancelAction) 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(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) .disabled(!model.canSend) } } 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/PermissionsView.swift b/Sources/MacRazer/PermissionsView.swift index cb365db..aa68c30 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.") @@ -192,10 +192,16 @@ struct PermissionsView: View { } } Spacer() - Button(model.allRequiredGranted ? "Done" : "Close") { onDone?() } - .buttonStyle(.borderedProminent) - .tint(model.allRequiredGranted ? .razerGreen : .gray) - .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)) + } } } } diff --git a/Sources/MacRazer/PopoverView.swift b/Sources/MacRazer/PopoverView.swift index 66114a2..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) @@ -585,8 +588,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 } @@ -689,8 +691,7 @@ struct PopoverView: View { .frame(maxWidth: .infinity) .padding(.vertical, 5) } - .buttonStyle(.borderedProminent) - .tint(.razerGreen) + .buttonStyle(RazerProminentButtonStyle()) .controlSize(.small) } @@ -787,8 +788,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 369918a..304c344 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) } @@ -262,6 +261,7 @@ struct SettingsView: View { Spacer() Button("Done") { onDone() } .keyboardShortcut(.defaultAction) + .buttonStyle(RazerProminentButtonStyle()) } } } 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) }