Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions Sources/MacRazer/AboutView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ struct AboutView: View {
Spacer(minLength: 0)
Button("Done") { onDone() }
.keyboardShortcut(.defaultAction)
.buttonStyle(RazerProminentButtonStyle())
}
.controlSize(.small)
.font(.system(size: 11.5))
Expand Down
40 changes: 40 additions & 0 deletions Sources/MacRazer/CardComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,46 @@ extension View {
func hoverHighlight<S: Shape>(_ 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)
Expand Down
9 changes: 6 additions & 3 deletions Sources/MacRazer/CrashReportView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
18 changes: 9 additions & 9 deletions Sources/MacRazer/DeviceTestView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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())
}
}

Expand Down
18 changes: 12 additions & 6 deletions Sources/MacRazer/PermissionsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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.")
Expand Down Expand Up @@ -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))
}
}
}
}
20 changes: 10 additions & 10 deletions Sources/MacRazer/PopoverView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 }
Expand Down Expand Up @@ -689,8 +691,7 @@ struct PopoverView: View {
.frame(maxWidth: .infinity)
.padding(.vertical, 5)
}
.buttonStyle(.borderedProminent)
.tint(.razerGreen)
.buttonStyle(RazerProminentButtonStyle())
.controlSize(.small)
}

Expand Down Expand Up @@ -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))
}
Expand Down
4 changes: 2 additions & 2 deletions Sources/MacRazer/SettingsView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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) }
Expand Down Expand Up @@ -262,6 +261,7 @@ struct SettingsView: View {
Spacer()
Button("Done") { onDone() }
.keyboardShortcut(.defaultAction)
.buttonStyle(RazerProminentButtonStyle())
}
}
}
3 changes: 1 addition & 2 deletions Sources/MacRazer/UpdatedView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,7 @@ struct UpdatedView: View {
Spacer()
Button("Done", action: onDone)
.keyboardShortcut(.defaultAction)
.buttonStyle(.borderedProminent)
.tint(.razerGreen)
.buttonStyle(RazerProminentButtonStyle())
}
}
.padding(22)
Expand Down
3 changes: 1 addition & 2 deletions Sources/MacRazer/WhatsNewPage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,7 @@ struct WhatsNewPage: View {
.frame(maxWidth: .infinity)
.padding(.vertical, 5)
}
.buttonStyle(.borderedProminent)
.tint(.razerGreen)
.buttonStyle(RazerProminentButtonStyle())
.controlSize(.small)
.padding(12)
}
Expand Down
Loading