Files
romanvandClaude Opus 5 cd53b6a1a1 Add CampusNAS menu bar agent
Native macOS agent that keeps the office VPN and the NAS shares working
without the shell wrapper it replaces.

- VPN: full port of the `ovpn` script — host-route pinning, gateway
  discovery via networksetup, tunnel liveness by address plus pushed
  route, startup grace, restart of a wedged daemon. Root steps are
  batched into one system authorization prompt.
- SMB: mounts via NetFS, discovers shares from the server, detects
  mounts event-driven through NSWorkspace, matches the host by resolved
  alias so externally created mounts are recognised.
- Credentials never pass through the process: the VPN auth file is
  written by `security` straight to disk, and the password is stored via
  SecItemAdd rather than a command line visible in ps.
- UI: status window, settings editor, ⌘+/⌘− zoom that re-lays out rather
  than scaling a bitmap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 13:19:31 +06:00

54 lines
2.1 KiB
Swift

import SwiftUI
/// Button whose chrome scales, not just its label.
///
/// The system `.bordered` style sizes its padding, height and corner radius
/// from `controlSize`, which has four fixed steps — setting a larger font on it
/// grows the text inside a button that stays the same size. This style derives
/// every dimension from the zoom multiplier instead.
struct ScaledButtonStyle: ButtonStyle {
let scale: CGFloat
/// Text-only variant, for secondary actions like "Открыть".
var borderless = false
func makeBody(configuration: Configuration) -> some View {
Label(configuration: configuration, scale: scale, borderless: borderless)
}
/// Not named `Body`: that collides with `ButtonStyle`'s own associated type.
private struct Label: View {
let configuration: ButtonStyleConfiguration
let scale: CGFloat
let borderless: Bool
/// A custom style has to dim itself — SwiftUI only does that for the
/// built-in ones.
@Environment(\.isEnabled) private var isEnabled
var body: some View {
configuration.label
.font(.system(size: 12 * scale))
.lineLimit(1)
.padding(.horizontal, borderless ? 2 * scale : 10 * scale)
.padding(.vertical, borderless ? 1 * scale : 5 * scale)
.background(background)
.foregroundStyle(borderless ? Color.accentColor : Color.primary)
.opacity(isEnabled ? (configuration.isPressed ? 0.7 : 1) : 0.4)
.contentShape(Rectangle())
}
@ViewBuilder
private var background: some View {
if borderless {
Color.clear
} else {
RoundedRectangle(cornerRadius: 6 * scale, style: .continuous)
.fill(Color.primary.opacity(configuration.isPressed ? 0.16 : 0.08))
.overlay(
RoundedRectangle(cornerRadius: 6 * scale, style: .continuous)
.strokeBorder(Color.primary.opacity(0.12), lineWidth: 1)
)
}
}
}
}