Skip to content

Commit c1c0420

Browse files
akhil-gautamclaude
andcommitted
design(bloatmac): rewrite Settings to match macOS System Settings
The previous Settings used decorative `metallic` panels with chunky section cards and a hardcoded "v 2.4.1 — design preview" string. Reads playful rather than utilitarian. Replace with an Apple-System-Settings-style layout: * Single 640 pt-wide column, generous breathing room (32 pt horizontal, 24 pt top, 28 pt between sections) * Small-caps section headers with 0.6 letter-tracking in Tokens.text3 * Each section is one `Tokens.bgPanel` rounded rect containing rows separated by hairline dividers (16 pt left inset) * Rows use a label-plus-sublabel pattern with the control flush right so settings remain glanceable Section structure: 1. Appearance — Theme segmented picker, Accent color row with check mark on the active swatch instead of a stroked ring 2. General — Menu bar widget toggle + Onboarding replay 3. Permissions — Full Disk Access status with Re-check + Open settings buttons (new — surfaces what was previously only reachable through the gate overlay) 4. About — BrandMark + dynamic CFBundleShortVersionString + a "View on GitHub" link in the active accent colour Also drops the prototype version string. The `appVersionLabel` helper introduced for the sidebar already does the right thing — reuses it here, with the build number appended in parens only when it differs from the marketing version. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 143ca06 commit c1c0420

1 file changed

Lines changed: 153 additions & 40 deletions

File tree

bloatmac/bloatmac/Screens/Screens.swift

Lines changed: 153 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5038,61 +5038,174 @@ private struct SessionChart: View {
50385038

50395039
struct SettingsScreen: View {
50405040
@EnvironmentObject var state: AppState
5041+
50415042
var body: some View {
5042-
ScreenScroll {
5043-
settingsCard(title: "Appearance") {
5044-
VStack(alignment: .leading, spacing: 14) {
5045-
HStack {
5046-
Text("Theme").font(.system(size: 13, weight: .medium)).frame(width: 160, alignment: .leading)
5047-
Picker("", selection: $state.themeRaw) {
5048-
Text("Light").tag("light")
5049-
Text("Dark").tag("dark")
5050-
}.pickerStyle(.segmented).frame(width: 220)
5051-
}
5052-
HStack {
5053-
Text("Accent").font(.system(size: 13, weight: .medium)).frame(width: 160, alignment: .leading)
5054-
HStack(spacing: 8) {
5055-
ForEach(AccentKey.allCases) { k in
5056-
Button { state.accent = k } label: {
5057-
Circle().fill(k.value)
5058-
.frame(width: 22, height: 22)
5059-
.overlay(Circle().stroke(.white, lineWidth: state.accent == k ? 2 : 0))
5060-
.overlay(Circle().stroke(Tokens.border, lineWidth: 1))
5061-
}.buttonStyle(.plain)
5043+
ScrollView {
5044+
VStack(alignment: .leading, spacing: 28) {
5045+
title
5046+
appearanceSection
5047+
generalSection
5048+
permissionsSection
5049+
aboutSection
5050+
}
5051+
.padding(.horizontal, 32).padding(.top, 24).padding(.bottom, 40)
5052+
.frame(maxWidth: 640, alignment: .leading)
5053+
}
5054+
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
5055+
.background(Tokens.bgWindow)
5056+
}
5057+
5058+
// MARK: Title
5059+
5060+
private var title: some View {
5061+
Text("Settings").font(.system(size: 24, weight: .bold))
5062+
}
5063+
5064+
// MARK: Appearance
5065+
5066+
private var appearanceSection: some View {
5067+
section("Appearance") {
5068+
settingsRow(label: "Theme") {
5069+
Picker("", selection: $state.themeRaw) {
5070+
Text("Light").tag("light")
5071+
Text("Dark").tag("dark")
5072+
}
5073+
.pickerStyle(.segmented)
5074+
.labelsHidden()
5075+
.frame(width: 180)
5076+
}
5077+
divider
5078+
settingsRow(label: "Accent color") {
5079+
HStack(spacing: 10) {
5080+
ForEach(AccentKey.allCases) { k in
5081+
Button { state.accent = k } label: {
5082+
ZStack {
5083+
Circle().fill(k.value).frame(width: 20, height: 20)
5084+
if state.accent == k {
5085+
Image(systemName: "checkmark")
5086+
.font(.system(size: 9, weight: .heavy))
5087+
.foregroundStyle(.white)
5088+
}
50625089
}
50635090
}
5091+
.buttonStyle(.plain)
5092+
.help(k.rawValue.capitalized)
50645093
}
50655094
}
50665095
}
5067-
settingsCard(title: "Modes") {
5068-
VStack(spacing: 12) {
5069-
HStack {
5070-
Text("Menu bar widget").font(.system(size: 13, weight: .medium)).frame(maxWidth: .infinity, alignment: .leading)
5071-
AppSwitch(on: $state.menubarWidgetEnabled)
5072-
}
5073-
HStack {
5074-
Text("Replay first scan").font(.system(size: 13, weight: .medium)).frame(maxWidth: .infinity, alignment: .leading)
5075-
Btn(label: "Replay", style: .secondary) { state.replayOnboarding() }
5096+
}
5097+
}
5098+
5099+
// MARK: General
5100+
5101+
private var generalSection: some View {
5102+
section("General") {
5103+
settingsRow(label: "Menu bar widget",
5104+
sublabel: "Quick storage / RAM / network in your menu bar.") {
5105+
Toggle("", isOn: $state.menubarWidgetEnabled)
5106+
.labelsHidden().toggleStyle(.switch)
5107+
}
5108+
divider
5109+
settingsRow(label: "Onboarding",
5110+
sublabel: "Replay the welcome scan animation.") {
5111+
Button("Replay") { state.replayOnboarding() }
5112+
.buttonStyle(.bordered)
5113+
.controlSize(.small)
5114+
}
5115+
}
5116+
}
5117+
5118+
// MARK: Permissions
5119+
5120+
private var permissionsSection: some View {
5121+
section("Permissions") {
5122+
settingsRow(label: "Full Disk Access",
5123+
sublabel: state.needsFDA
5124+
? "Not granted — some scans run with reduced detail."
5125+
: "Granted. BloatMac can read protected user dirs.") {
5126+
HStack(spacing: 8) {
5127+
Button("Re-check") { state.refreshPermissions() }
5128+
.buttonStyle(.bordered).controlSize(.small)
5129+
Button("Open settings") {
5130+
state.reopenPermissionsGate()
50765131
}
5132+
.buttonStyle(.borderedProminent).controlSize(.small)
50775133
}
50785134
}
5079-
settingsCard(title: "About") {
5080-
VStack(alignment: .leading, spacing: 4) {
5081-
Text("BloatMac").font(.system(size: 14, weight: .bold))
5082-
Text("v 2.4.1 — design preview").font(.system(size: 11.5)).foregroundStyle(Tokens.text3)
5135+
}
5136+
}
5137+
5138+
// MARK: About
5139+
5140+
private var aboutSection: some View {
5141+
section("About") {
5142+
HStack(alignment: .center, spacing: 14) {
5143+
BrandMark(size: 44)
5144+
VStack(alignment: .leading, spacing: 2) {
5145+
Text("BloatMac").font(.system(size: 16, weight: .bold))
5146+
Text(appVersionLabel + buildSuffix)
5147+
.font(.system(size: 11.5)).foregroundStyle(Tokens.text3)
5148+
.monospacedDigit()
50835149
}
5150+
Spacer()
5151+
Link(destination: URL(string: "https://github.com/akhil-gautam/bloat")!) {
5152+
HStack(spacing: 4) {
5153+
Text("View on GitHub")
5154+
Image(systemName: "arrow.up.right.square")
5155+
}
5156+
.font(.system(size: 11.5, weight: .medium))
5157+
}
5158+
.buttonStyle(.plain)
5159+
.foregroundStyle(state.accent.value)
50845160
}
5161+
.padding(.horizontal, 16).padding(.vertical, 14)
5162+
}
5163+
}
5164+
5165+
private var buildSuffix: String {
5166+
// CFBundleVersion sometimes mirrors the marketing version; only show
5167+
// it when it adds information (i.e. it's different).
5168+
let info = Bundle.main.infoDictionary
5169+
let short = info?["CFBundleShortVersionString"] as? String ?? ""
5170+
let build = info?["CFBundleVersion"] as? String ?? ""
5171+
return (build.isEmpty || build == short) ? "" : " (\(build))"
5172+
}
5173+
5174+
// MARK: Building blocks
5175+
5176+
@ViewBuilder
5177+
private func section<C: View>(_ title: String, @ViewBuilder _ content: () -> C) -> some View {
5178+
VStack(alignment: .leading, spacing: 8) {
5179+
Text(title.uppercased())
5180+
.font(.system(size: 11, weight: .semibold))
5181+
.tracking(0.6)
5182+
.foregroundStyle(Tokens.text3)
5183+
VStack(spacing: 0) { content() }
5184+
.background(Tokens.bgPanel)
5185+
.overlay(RoundedRectangle(cornerRadius: 10).stroke(Tokens.border))
5186+
.clipShape(RoundedRectangle(cornerRadius: 10))
50855187
}
50865188
}
50875189

50885190
@ViewBuilder
5089-
private func settingsCard<C: View>(title: String, @ViewBuilder _ content: () -> C) -> some View {
5090-
VStack(alignment: .leading, spacing: 12) {
5091-
Text(title).font(.system(size: 14, weight: .bold))
5092-
content()
5191+
private func settingsRow<C: View>(label: String,
5192+
sublabel: String? = nil,
5193+
@ViewBuilder _ control: () -> C) -> some View {
5194+
HStack(alignment: .center, spacing: 16) {
5195+
VStack(alignment: .leading, spacing: 2) {
5196+
Text(label).font(.system(size: 13, weight: .regular)).foregroundStyle(Tokens.text)
5197+
if let sub = sublabel {
5198+
Text(sub).font(.system(size: 11)).foregroundStyle(Tokens.text3)
5199+
.fixedSize(horizontal: false, vertical: true)
5200+
}
5201+
}
5202+
Spacer()
5203+
control()
50935204
}
5094-
.padding(18)
5095-
.frame(maxWidth: .infinity, alignment: .leading)
5096-
.metallic(radius: Tokens.Radius.lg)
5205+
.padding(.horizontal, 16).padding(.vertical, 12)
5206+
}
5207+
5208+
private var divider: some View {
5209+
Divider().padding(.leading, 16).opacity(0.6)
50975210
}
50985211
}

0 commit comments

Comments
 (0)