chore(mac): rely on status item disable for dimming

main
Peter Steinberger 2025-12-06 01:27:20 +01:00
parent f290b9a145
commit c545ec727c
1 changed files with 54 additions and 58 deletions

View File

@ -383,7 +383,6 @@ struct ClawdisApp: App {
private func applyStatusItemAppearance(paused: Bool) { private func applyStatusItemAppearance(paused: Bool) {
statusItem?.button?.appearsDisabled = paused statusItem?.button?.appearsDisabled = paused
statusItem?.button?.alphaValue = paused ? 0.45 : 1.0
} }
} }
@ -433,14 +432,11 @@ private struct CritterStatusLabel: View {
Image(nsImage: CritterIconRenderer.makeIcon( Image(nsImage: CritterIconRenderer.makeIcon(
blink: blinkAmount, blink: blinkAmount,
legWiggle: legWiggle, legWiggle: legWiggle,
earWiggle: earWiggle, earWiggle: earWiggle
isPaused: isPaused
)) ))
.frame(width: 18, height: 16) .frame(width: 18, height: 16)
.rotationEffect(.degrees(wiggleAngle), anchor: .center) .rotationEffect(.degrees(wiggleAngle), anchor: .center)
.offset(x: wiggleOffset) .offset(x: wiggleOffset)
.foregroundStyle(isPaused ? .secondary : .primary)
.opacity(isPaused ? 0.45 : 1.0)
.onReceive(ticker) { now in .onReceive(ticker) { now in
guard !isPaused else { guard !isPaused else {
resetMotion() resetMotion()
@ -533,7 +529,7 @@ private struct CritterStatusLabel: View {
enum CritterIconRenderer { enum CritterIconRenderer {
private static let size = NSSize(width: 18, height: 16) private static let size = NSSize(width: 18, height: 16)
static func makeIcon(blink: CGFloat, legWiggle: CGFloat = 0, earWiggle: CGFloat = 0, isPaused: Bool = false) -> NSImage { static func makeIcon(blink: CGFloat, legWiggle: CGFloat = 0, earWiggle: CGFloat = 0) -> NSImage {
let image = NSImage(size: size) let image = NSImage(size: size)
image.lockFocus() image.lockFocus()
defer { image.unlockFocus() } defer { image.unlockFocus() }
@ -567,8 +563,7 @@ enum CritterIconRenderer {
let eyeY = bodyY + bodyH * 0.56 let eyeY = bodyY + bodyH * 0.56
let eyeOffset = bodyW * 0.24 let eyeOffset = bodyW * 0.24
let baseAlpha: CGFloat = isPaused ? 0.38 : 1.0 ctx.setFillColor(NSColor.labelColor.cgColor)
ctx.setFillColor(NSColor.labelColor.withAlphaComponent(baseAlpha).cgColor)
// Body // Body
ctx.addPath(CGPath(roundedRect: CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH), cornerWidth: bodyCorner, cornerHeight: bodyCorner, transform: nil)) ctx.addPath(CGPath(roundedRect: CGRect(x: bodyX, y: bodyY, width: bodyW, height: bodyH), cornerWidth: bodyCorner, cornerHeight: bodyCorner, transform: nil))
@ -622,7 +617,7 @@ enum CritterIconRenderer {
ctx.fillPath() ctx.fillPath()
ctx.restoreGState() ctx.restoreGState()
image.isTemplate = false image.isTemplate = true
return image return image
} }
} }
@ -834,13 +829,12 @@ struct GeneralSettings: View {
.frame(width: 140) .frame(width: 140)
} }
} }
.padding(14)
.background(RoundedRectangle(cornerRadius: 12).fill(Color(NSColor.controlBackgroundColor)))
GroupBox("CLI helper") { VStack(alignment: .leading, spacing: 6) {
Text("CLI helper")
.font(.callout.weight(.semibold))
cliInstaller cliInstaller
} }
.groupBoxStyle(.automatic)
Spacer() Spacer()
HStack { HStack {
@ -1201,7 +1195,7 @@ private struct PermissionRow: View {
} }
} }
// MARK: - Onboarding (VibeTunnel-aligned) // MARK: - Onboarding
@MainActor @MainActor
final class OnboardingController { final class OnboardingController {
@ -1217,7 +1211,7 @@ final class OnboardingController {
let hosting = NSHostingController(rootView: OnboardingView()) let hosting = NSHostingController(rootView: OnboardingView())
let window = NSWindow(contentViewController: hosting) let window = NSWindow(contentViewController: hosting)
window.title = "Welcome to Clawdis" window.title = "Welcome to Clawdis"
window.setContentSize(NSSize(width: 640, height: 600)) window.setContentSize(NSSize(width: 640, height: 560))
window.styleMask = [.titled, .closable] window.styleMask = [.titled, .closable]
window.center() window.center()
window.makeKeyAndOrderFront(nil) window.makeKeyAndOrderFront(nil)
@ -1235,50 +1229,47 @@ struct OnboardingView: View {
@State private var currentPage = 0 @State private var currentPage = 0
@State private var permStatus: [Capability: Bool] = [:] @State private var permStatus: [Capability: Bool] = [:]
@State private var isRequesting = false @State private var isRequesting = false
@State private var installingCLI = false @State private var installingCLI = false
@State private var cliStatus: String? @State private var cliStatus: String?
@State private var copied = false @State private var copied = false
@ObservedObject private var state = AppStateStore.shared @ObservedObject private var state = AppStateStore.shared
private let pageWidth: CGFloat = 640 private let pageWidth: CGFloat = 640
private let contentHeight: CGFloat = 260 private let contentHeight: CGFloat = 260
private var pageCount: Int { 6 } private var pageCount: Int { 6 }
private var buttonTitle: String { currentPage == pageCount - 1 ? "Finish" : "Next" } private var buttonTitle: String { currentPage == pageCount - 1 ? "Finish" : "Next" }
private let devLinkCommand = "ln -sf $(pwd)/apps/macos/.build/debug/ClawdisCLI /usr/local/bin/clawdis-mac" private let devLinkCommand = "ln -sf $(pwd)/apps/macos/.build/debug/ClawdisCLI /usr/local/bin/clawdis-mac"
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
GlowingClawdisIcon(size: 148) GlowingClawdisIcon(size: 148)
.padding(.top, 22) .padding(.top, 22)
.padding(.bottom, 12) .padding(.bottom, 12)
.frame(height: 200) .frame(maxWidth: .infinity, minHeight: 200, maxHeight: 220)
GeometryReader { _ in HStack(spacing: 0) {
HStack(spacing: 0) { welcomePage
welcomePage focusPage
focusPage permissionsPage
permissionsPage cliPage
cliPage launchPage
launchPage readyPage
readyPage
}
.frame(height: contentHeight)
.offset(x: CGFloat(-currentPage) * pageWidth)
.animation(
.interactiveSpring(response: 0.5, dampingFraction: 0.86, blendDuration: 0.25),
value: currentPage
)
}
.frame(height: contentHeight)
.clipped()
navigationBar
} }
.frame(width: pageWidth) .frame(width: pageWidth, height: contentHeight)
.background(Color(NSColor.windowBackgroundColor)) .offset(x: CGFloat(-currentPage) * pageWidth)
.onAppear { currentPage = 0 } .animation(
.task { await refreshPerms() } .interactiveSpring(response: 0.5, dampingFraction: 0.86, blendDuration: 0.25),
value: currentPage
)
.clipped()
navigationBar
} }
.frame(width: pageWidth, height: 560, alignment: .top)
.background(Color(NSColor.windowBackgroundColor))
.onAppear { currentPage = 0 }
.task { await refreshPerms() }
}
private var welcomePage: some View { private var welcomePage: some View {
onboardingPage { onboardingPage {
@ -1289,11 +1280,13 @@ struct OnboardingView: View {
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.frame(maxWidth: 520) .frame(maxWidth: 520)
Text("We'll guide you through the same flow as VibeTunnel: quick steps, live permission checks, and the helper CLI.") .fixedSize(horizontal: false, vertical: true)
Text("Quick steps with live permission checks and the helper CLI so you can finish setup in minutes.")
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.frame(maxWidth: 520) .frame(maxWidth: 520)
.fixedSize(horizontal: false, vertical: true)
} }
} }
@ -1325,11 +1318,12 @@ struct OnboardingView: View {
onboardingPage { onboardingPage {
Text("Grant permissions") Text("Grant permissions")
.font(.largeTitle.weight(.semibold)) .font(.largeTitle.weight(.semibold))
Text("Match VibeTunnel's checklist: approve these once and the CLI reuses the same grants.") Text("Approve these once and the helper CLI reuses the same grants.")
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.frame(maxWidth: 520) .frame(maxWidth: 520)
.fixedSize(horizontal: false, vertical: true)
onboardingCard { onboardingCard {
ForEach(Capability.allCases, id: \.self) { cap in ForEach(Capability.allCases, id: \.self) { cap in
@ -1355,11 +1349,12 @@ struct OnboardingView: View {
onboardingPage { onboardingPage {
Text("Install the helper CLI") Text("Install the helper CLI")
.font(.largeTitle.weight(.semibold)) .font(.largeTitle.weight(.semibold))
Text("Link `clawdis-mac` like VibeTunnel's `vt` so scripts and the agent can talk to this app.") Text("Link `clawdis-mac` so scripts and the agent can talk to this app.")
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.frame(maxWidth: 520) .frame(maxWidth: 520)
.fixedSize(horizontal: false, vertical: true)
onboardingCard { onboardingCard {
HStack(spacing: 12) { HStack(spacing: 12) {
@ -1398,11 +1393,12 @@ struct OnboardingView: View {
onboardingPage { onboardingPage {
Text("Keep it running") Text("Keep it running")
.font(.largeTitle.weight(.semibold)) .font(.largeTitle.weight(.semibold))
Text("Match VibeTunnel's stay-on guidance: let Clawdis launch with macOS so permissions and notifications are ready.") Text("Let Clawdis launch with macOS so permissions and notifications are ready when automations start.")
.font(.body) .font(.body)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.frame(maxWidth: 520) .frame(maxWidth: 520)
.fixedSize(horizontal: false, vertical: true)
onboardingCard { onboardingCard {
Toggle("Launch at login", isOn: $state.launchAtLogin) Toggle("Launch at login", isOn: $state.launchAtLogin)