From 6c8bd819a051d1c19b0f7fe5add56153db70140e Mon Sep 17 00:00:00 2001
From: blessdyb
Date: Wed, 30 Sep 2026 23:21:03 -0700
Subject: [PATCH 1/2] Refresh the app on one design-token system
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introduce DesignTokens: the brand palette (the website's colours, resolved for
light and dark since the app follows the system), plus Spacing/Radius and the
shared surfaces — flCard, FLPill, Sparkline, TrafficBar. StatTile moves to the
card with display figures; TrafficColors now point at the tokens so every call
site refreshes at once.
Then the screens: the AI Agents table gains real traffic bars, risk and
allowlist state become bordered pills, the always-monitor banner uses the
accent card, the sidebar selection takes the brand accent. A sweep across the
UI replaces the scattered colour literals (orange/green/red/purple/accent)
with semantic tokens — warning/good/critical/tool/accent — so the whole app
reads as one system in both appearances.
Co-Authored-By: Claude Opus 4.8
---
Flowlight/UI/AgentsView.swift | 61 ++++----
Flowlight/UI/AlertsView.swift | 2 +-
Flowlight/UI/AskView.swift | 12 +-
Flowlight/UI/CaptureView.swift | 34 ++---
Flowlight/UI/CaptureWarning.swift | 16 +--
Flowlight/UI/Components.swift | 32 +++--
Flowlight/UI/ContentView.swift | 5 +-
Flowlight/UI/CoverageView.swift | 6 +-
Flowlight/UI/DesignTokens.swift | 160 +++++++++++++++++++++
Flowlight/UI/DevicesView.swift | 8 +-
Flowlight/UI/FocusView.swift | 4 +-
Flowlight/UI/GuardrailsPane.swift | 2 +-
Flowlight/UI/Insights/BehaviourPanel.swift | 2 +-
Flowlight/UI/Insights/InsightsPanel.swift | 2 +-
Flowlight/UI/InspectView.swift | 30 ++--
Flowlight/UI/JSONTreeView.swift | 6 +-
Flowlight/UI/MCPDomainsSection.swift | 2 +-
Flowlight/UI/MockRulesView.swift | 2 +-
Flowlight/UI/ReportsView.swift | 4 +-
Flowlight/UI/RuleEditor.swift | 10 +-
Flowlight/UI/RuleSimulationView.swift | 6 +-
Flowlight/UI/RulesView.swift | 20 +--
Flowlight/UI/SettingsView.swift | 2 +-
Flowlight/UI/UpdateView.swift | 2 +-
24 files changed, 302 insertions(+), 128 deletions(-)
create mode 100644 Flowlight/UI/DesignTokens.swift
diff --git a/Flowlight/UI/AgentsView.swift b/Flowlight/UI/AgentsView.swift
index ecb349e..6599400 100644
--- a/Flowlight/UI/AgentsView.swift
+++ b/Flowlight/UI/AgentsView.swift
@@ -124,7 +124,8 @@ struct AgentsView: View {
}
private var table: some View {
- Table(agents.sorted(using: sortOrder), selection: $selection, sortOrder: $sortOrder) {
+ let maxAI = agents.map(\.ai.total).max() ?? 1
+ return Table(agents.sorted(using: sortOrder), selection: $selection, sortOrder: $sortOrder) {
TableColumn(L("Agent"), value: \.name) { agent in
HStack(spacing: 8) {
AppIconView(path: agent.appPath, size: 20)
@@ -140,7 +141,15 @@ struct AgentsView: View {
Text(agent.providers.map(\.name).joined(separator: ", ")).lineLimit(1).foregroundStyle(.secondary)
}
.width(min: 80, ideal: 130)
- TableColumn(L("AI traffic"), value: \.aiTotal) { Text(ByteFormat.string($0.ai.total)).monospacedDigit() }.width(80)
+ TableColumn(L("AI traffic"), value: \.aiTotal) { agent in
+ VStack(alignment: .leading, spacing: 3) {
+ Text(ByteFormat.string(agent.ai.total)).monospacedDigit()
+ if agent.ai.total > 0 {
+ TrafficBar(fraction: Double(agent.ai.total) / Double(max(maxAI, 1)), color: FL.received)
+ }
+ }
+ }
+ .width(min: 96, ideal: 120)
TableColumn(L("Other hosts"), value: \.otherCount) { Text("\($0.otherCount)").monospacedDigit() }.width(70)
TableColumn(L("Sent elsewhere"), value: \.egressOut) { agent in
Text(ByteFormat.string(agent.egress.bytesOut)).monospacedDigit()
@@ -258,11 +267,10 @@ struct AlwaysMonitorBanner: View {
}
private func card(@ViewBuilder _ content: () -> Content) -> some View {
- VStack(alignment: .leading, spacing: 12) { content() }
- .padding(12)
+ VStack(alignment: .leading, spacing: Spacing.m) { content() }
+ .padding(Spacing.m)
.frame(maxWidth: .infinity, alignment: .leading)
- .background(RoundedRectangle(cornerRadius: 10).fill(Color.accentColor.opacity(0.08)))
- .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.accentColor.opacity(0.25)))
+ .flCard(accent: true)
}
/// A representative banner for the marketing screenshot: one agent to offer, two already monitored (one not yet
@@ -272,7 +280,7 @@ struct AlwaysMonitorBanner: View {
suggestion("Windsurf")
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
- Image(systemName: "checkmark.shield.fill").foregroundStyle(.green)
+ Image(systemName: "checkmark.shield.fill").foregroundStyle(FL.good)
Text(L("Flowlight is keeping these agents routed through it on every launch."))
.font(.callout.weight(.medium)).fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 8)
@@ -289,7 +297,7 @@ struct AlwaysMonitorBanner: View {
private var monitoringTip: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 6) {
- Image(systemName: "checkmark.shield.fill").foregroundStyle(.green)
+ Image(systemName: "checkmark.shield.fill").foregroundStyle(FL.good)
Text(L("Flowlight is keeping these agents routed through it on every launch."))
.font(.callout.weight(.medium)).fixedSize(horizontal: false, vertical: true)
Spacer(minLength: 8)
@@ -311,7 +319,7 @@ struct AlwaysMonitorBanner: View {
Text(name)
if notRouted {
Text(L("· not routed yet — it may pin its certificates, or a shell setting may override the file"))
- .font(.caption).foregroundStyle(.orange).fixedSize(horizontal: false, vertical: true)
+ .font(.caption).foregroundStyle(FL.warning).fixedSize(horizontal: false, vertical: true)
}
Spacer(minLength: 8)
Button(L("Stop Monitoring"), action: stop).controlSize(.small)
@@ -320,7 +328,7 @@ struct AlwaysMonitorBanner: View {
private func suggestion(_ name: String) -> some View {
HStack(alignment: .top, spacing: 10) {
- Image(systemName: "bolt.horizontal.circle").foregroundStyle(Color.accentColor)
+ Image(systemName: "bolt.horizontal.circle").foregroundStyle(FL.accent)
VStack(alignment: .leading, spacing: 6) {
Text(L("%@ is reaching its model provider directly, so Flowlight can't see what it sends.", name))
.fixedSize(horizontal: false, vertical: true)
@@ -351,23 +359,18 @@ struct RiskBadges: View {
badge(proto.uppercased(), icon: icon(for: ProtocolCatalog.category(of: proto)), color: TrafficColors.anomaly)
}
if agent.hasLargeUpload {
- badge(L("Large upload"), icon: "arrow.up.doc", color: .orange)
+ badge(L("Large upload"), icon: "arrow.up.doc", color: FL.warning)
.help(L("%@ sent to hosts that aren't AI providers", ByteFormat.string(agent.egress.bytesOut)))
}
if agent.hasUnnamedHost {
- badge(L("Raw IP"), icon: "questionmark.circle", color: .orange)
+ badge(L("Raw IP"), icon: "questionmark.circle", color: FL.warning)
.help(L("Connected to an address with no hostname"))
}
}
}
private func badge(_ text: String, icon: String, color: Color) -> some View {
- Label(text, systemImage: icon)
- .font(.caption2.bold())
- .labelStyle(.titleAndIcon)
- .padding(.horizontal, 5).padding(.vertical, 1)
- .background(color.opacity(0.15), in: Capsule())
- .foregroundStyle(color)
+ FLPill(text: text, systemImage: icon, color: color)
}
private func icon(for category: ProtocolCategory) -> String {
@@ -405,10 +408,10 @@ struct AllowlistStatus: View {
Label(L("%lld not allowed", violations), systemImage: "xmark.octagon.fill")
.font(.caption.bold()).foregroundStyle(TrafficColors.anomaly)
} else {
- Label(L("All allowed"), systemImage: "checkmark.seal.fill").font(.caption).foregroundStyle(.green)
+ Label(L("All allowed"), systemImage: "checkmark.seal.fill").font(.caption).foregroundStyle(FL.good)
}
if policy.enforce {
- Image(systemName: "shield.lefthalf.filled").font(.caption2).foregroundStyle(.orange)
+ Image(systemName: "shield.lefthalf.filled").font(.caption2).foregroundStyle(FL.warning)
.help(L("Unlisted destinations are refused, not only reported"))
}
}
@@ -460,7 +463,7 @@ struct AllowlistEditor: View {
VStack(alignment: .leading, spacing: 2) {
ForEach(policy.patterns, id: \.self) { pattern in
HStack(spacing: 4) {
- Image(systemName: "checkmark").font(.caption2).foregroundStyle(.green)
+ Image(systemName: "checkmark").font(.caption2).foregroundStyle(FL.good)
Text(pattern).font(.caption.monospaced()).lineLimit(1)
Spacer()
Button { var p = policy; p.patterns.removeAll { $0 == pattern }; save(p) } label: {
@@ -554,7 +557,7 @@ struct AgentDetail: View {
ForEach(toolCalls.prefix(6)) { usage in
Button { tab = .calls } label: {
HStack {
- Image(systemName: usage.isMCP ? "puzzlepiece.extension" : "wrench.and.screwdriver").foregroundStyle(.purple)
+ Image(systemName: usage.isMCP ? "puzzlepiece.extension" : "wrench.and.screwdriver").foregroundStyle(FL.tool)
.frame(width: 16)
Text(usage.name).lineLimit(1)
Spacer()
@@ -815,7 +818,7 @@ struct AgentDetail: View {
private func destinationRow(_ d: AgentDestination) -> some View {
HStack(spacing: 6) {
Image(systemName: d.isSensitive ? "exclamationmark.shield.fill" : d.isUnnamed ? "questionmark.circle" : "globe")
- .foregroundStyle(d.isSensitive ? TrafficColors.anomaly : d.isUnnamed ? .orange : .secondary)
+ .foregroundStyle(d.isSensitive ? TrafficColors.anomaly : d.isUnnamed ? FL.warning : .secondary)
Text(d.label).lineLimit(1).truncationMode(.middle)
if d.isUnnamed && d.label != d.ip { Text(d.ip).foregroundStyle(.secondary) }
Text(d.protocols.joined(separator: ", ") + (d.ports.isEmpty ? "" : " · " + d.ports))
@@ -882,7 +885,7 @@ struct ToolActivityRow: View {
HStack(spacing: 6) {
outcomeIcon
Image(systemName: activity.call.mcpServer == nil ? "wrench.and.screwdriver" : "puzzlepiece.extension")
- .foregroundStyle(.purple)
+ .foregroundStyle(FL.tool)
Text(activity.call.displayName).bold()
Spacer()
Text(activity.at, format: .dateTime.hour().minute().second()).monospacedDigit().foregroundStyle(.secondary)
@@ -920,7 +923,7 @@ struct ToolActivityRow: View {
@ViewBuilder private var outcomeIcon: some View {
switch activity.outcome {
- case .ok: Image(systemName: "checkmark.circle.fill").foregroundStyle(.green).help(L("Completed"))
+ case .ok: Image(systemName: "checkmark.circle.fill").foregroundStyle(FL.good).help(L("Completed"))
case .error: Image(systemName: "xmark.octagon.fill").foregroundStyle(TrafficColors.anomaly).help(L("The tool reported an error"))
case .pending: Image(systemName: "circle.dotted").foregroundStyle(.secondary).help(L("No result seen yet"))
}
@@ -934,7 +937,7 @@ struct DeclaredToolRow: View {
var body: some View {
HStack(alignment: .firstTextBaseline, spacing: 6) {
- Image(systemName: icon).foregroundStyle(used > 0 ? .purple : .secondary).frame(width: 14)
+ Image(systemName: icon).foregroundStyle(used > 0 ? FL.tool : .secondary).frame(width: 14)
VStack(alignment: .leading, spacing: 1) {
HStack(spacing: 5) {
Text(tool.name).bold(used > 0)
@@ -974,7 +977,7 @@ struct MCPServerRow: View {
var body: some View {
VStack(alignment: .leading, spacing: 4) {
HStack(spacing: 6) {
- Image(systemName: "puzzlepiece.extension").foregroundStyle(.purple)
+ Image(systemName: "puzzlepiece.extension").foregroundStyle(FL.tool)
Text(server.name).bold()
if let version = server.version { Text(version).foregroundStyle(.secondary) }
Text(kindLabel)
@@ -1125,7 +1128,7 @@ struct AgentSetupBar: View {
HStack(spacing: 8) {
Text(summary).foregroundStyle(.secondary)
Spacer()
- if store.isStale { Text(L("out of date")).foregroundStyle(.orange) }
+ if store.isStale { Text(L("out of date")).foregroundStyle(FL.warning) }
Button(L("Rescan")) { store.refresh(force: true) }.controlSize(.small)
Button(L("Add project folder…")) { pickFolder() }.controlSize(.small)
}
@@ -1230,7 +1233,7 @@ struct InspectionHint: View {
.controlSize(.small)
}
if let relaunchError {
- Text(relaunchError).font(.caption).foregroundStyle(.orange)
+ Text(relaunchError).font(.caption).foregroundStyle(FL.warning)
.fixedSize(horizontal: false, vertical: true)
}
}
diff --git a/Flowlight/UI/AlertsView.swift b/Flowlight/UI/AlertsView.swift
index c36bf59..932d719 100644
--- a/Flowlight/UI/AlertsView.swift
+++ b/Flowlight/UI/AlertsView.swift
@@ -43,7 +43,7 @@ struct AlertsView: View {
Table(visible, selection: $selection, sortOrder: $sortOrder) {
TableColumn("", value: \.severity) { a in
Image(systemName: a.severity >= 3 ? "exclamationmark.octagon.fill" : a.severity == 2 ? "exclamationmark.triangle.fill" : "info.circle")
- .foregroundStyle(a.severity >= 3 ? .red : a.severity == 2 ? .orange : .secondary)
+ .foregroundStyle(a.severity >= 3 ? FL.critical : a.severity == 2 ? FL.warning : .secondary)
.opacity(a.acknowledged ? 0.4 : 1)
.accessibilityLabel(a.severity >= 3 ? L("Critical") : a.severity == 2 ? L("Warning") : L("Info"))
}
diff --git a/Flowlight/UI/AskView.swift b/Flowlight/UI/AskView.swift
index bd624df..442d64d 100644
--- a/Flowlight/UI/AskView.swift
+++ b/Flowlight/UI/AskView.swift
@@ -137,7 +137,7 @@ private struct AskContent: View {
if let reason = ask.blockedReason {
HStack(spacing: 6) {
Label(reason, systemImage: "exclamationmark.triangle")
- .font(.caption).foregroundStyle(.orange)
+ .font(.caption).foregroundStyle(FL.warning)
Button(L("Choose a provider…")) { showingSettings = true }
.buttonStyle(.link).font(.caption)
Spacer(minLength: 0)
@@ -201,7 +201,7 @@ private struct AskTurnView: View {
if let error = turn.error {
Label(error, systemImage: "exclamationmark.triangle")
- .foregroundStyle(.orange)
+ .foregroundStyle(FL.warning)
.fixedSize(horizontal: false, vertical: true)
} else if turn.answer.isEmpty && thinking {
HStack(spacing: 8) {
@@ -251,7 +251,7 @@ private struct AskTurnView: View {
HStack(spacing: 6) {
Text(evidenceSummary)
if turn.calls.contains(where: \.failed) {
- Image(systemName: "exclamationmark.circle").foregroundStyle(.orange)
+ Image(systemName: "exclamationmark.circle").foregroundStyle(FL.warning)
}
}
.font(.caption)
@@ -274,7 +274,7 @@ private struct AskTurnView: View {
HStack(spacing: 8) {
Image(systemName: call.failed ? "exclamationmark.circle" : "magnifyingglass")
.font(.caption)
- .foregroundStyle(call.failed ? Color.orange : .secondary)
+ .foregroundStyle(call.failed ? FL.warning : .secondary)
.frame(width: 14)
VStack(alignment: .leading, spacing: 1) {
Text(call.call.sentence).font(.caption.monospaced())
@@ -349,7 +349,7 @@ private struct AskSettingsSheet: View {
}
}
if let reason = OnDeviceAsk.readiness.explanation, ask.provider == .onDevice {
- Text(reason).font(.caption).foregroundStyle(.orange)
+ Text(reason).font(.caption).foregroundStyle(FL.warning)
}
if ask.provider != .onDevice {
TextField(L("Endpoint"), text: Binding(get: { ask.endpoint }, set: { ask.endpoint = $0 }))
@@ -367,7 +367,7 @@ private struct AskSettingsSheet: View {
: L("Nothing leaves this Mac. The question, the queries and the answer all stay here."),
systemImage: ask.sendsOffDevice ? "cloud" : "lock")
.font(.caption)
- .foregroundStyle(ask.sendsOffDevice ? .orange : .secondary)
+ .foregroundStyle(ask.sendsOffDevice ? FL.warning : .secondary)
}
}
.formStyle(.grouped)
diff --git a/Flowlight/UI/CaptureView.swift b/Flowlight/UI/CaptureView.swift
index cfa332e..aaa311f 100644
--- a/Flowlight/UI/CaptureView.swift
+++ b/Flowlight/UI/CaptureView.swift
@@ -17,7 +17,7 @@ struct CaptureView: View {
/// source that was chosen, and arriving from the one that stepped in when that source gave up.
private var statusColor: Color {
if monitor.extensionFellBack { return .yellow }
- return monitor.isReceiving ? .green : .orange
+ return monitor.isReceiving ? FL.good : FL.warning
}
private var extensionDetailsVisible: Bool {
@@ -98,7 +98,7 @@ struct CaptureView: View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: chosen ? "largecircle.fill.circle" : "circle")
.font(.body)
- .foregroundStyle(chosen ? AnyShapeStyle(Color.accentColor) : AnyShapeStyle(.tertiary))
+ .foregroundStyle(chosen ? AnyShapeStyle(FL.accent) : AnyShapeStyle(.tertiary))
.frame(width: 26, height: 26)
VStack(alignment: .leading, spacing: 3) {
Text(sourceName(mode)).font(.body.weight(.medium))
@@ -110,7 +110,7 @@ struct CaptureView: View {
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
- .background(chosen ? Color.accentColor.opacity(0.10) : .clear)
+ .background(chosen ? FL.accent.opacity(0.10) : .clear)
.contentShape(Rectangle())
}
.buttonStyle(.plain)
@@ -196,8 +196,8 @@ struct CaptureView: View {
}
.padding(12)
.frame(maxWidth: .infinity, alignment: .leading)
- .background(RoundedRectangle(cornerRadius: 10).fill(Color.orange.opacity(0.12)))
- .overlay(RoundedRectangle(cornerRadius: 10).stroke(Color.orange.opacity(0.35)))
+ .background(RoundedRectangle(cornerRadius: 10).fill(FL.warning.opacity(0.12)))
+ .overlay(RoundedRectangle(cornerRadius: 10).stroke(FL.warning.opacity(0.35)))
}
private var extensionButtons: some View {
@@ -226,9 +226,9 @@ struct CaptureView: View {
private var extensionTint: Color {
switch extensionManager.state {
- case .enabled: return .green
- case .failed: return .red
- case .awaitingApproval, .installing, .needsReboot: return .orange
+ case .enabled: return FL.good
+ case .failed: return FL.critical
+ case .awaitingApproval, .installing, .needsReboot: return FL.warning
case .unknown, .notInstalled, .disabled: return .secondary
}
}
@@ -261,11 +261,11 @@ struct CaptureView: View {
.font(.caption).foregroundStyle(.secondary)
.prose()
.frame(maxWidth: .infinity, alignment: .leading)
- CaptureFact(icon: "checkmark.seal", tint: .green, title: L("Captures short-lived eligible flows"),
+ CaptureFact(icon: "checkmark.seal", tint: FL.good, title: L("Captures short-lived eligible flows"),
detail: L("macOS calls the filter when an eligible connection opens, providing better coverage of short requests than periodic sampling. Traffic from before activation is not available, some system traffic is exempt from content filters, and byte counts come from the filter's statistics reports."))
CaptureFact(icon: "info.circle", tint: .secondary, title: L("It keeps filtering after you quit Flowlight"),
detail: L("The system extension runs independently and remains active until you select Disable or Uninstall. While the Flowlight app is closed, summaries are buffered by the extension for later delivery, subject to its retention limit."))
- CaptureFact(icon: "exclamationmark.triangle", tint: .orange,
+ CaptureFact(icon: "exclamationmark.triangle", tint: FL.warning,
title: L("macOS runs one content filter at a time"),
detail: L("If a VPN or security agent already has that slot — Palo Alto Networks GlobalProtect, CrowdStrike Falcon and similar all use it — Flowlight's filter installs and connects but is never asked to filter anything, so nothing appears. Use the sampler on those Macs."))
}
@@ -281,7 +281,7 @@ struct CaptureView: View {
.foregroundStyle(.secondary)
.prose()
.frame(maxWidth: .infinity, alignment: .leading)
- CaptureFact(icon: "exclamationmark.triangle", tint: .orange,
+ CaptureFact(icon: "exclamationmark.triangle", tint: FL.warning,
title: L("What it misses is whole connections, not bytes"),
detail: L("nettop reports running totals, and Flowlight records the difference between one second and the next — so a long transfer is counted exactly, however bursty it was. What never appears is anything that starts and finishes between two readings: a quick DNS lookup, a fast API call, a script that runs curl and exits. The extension sees those."))
}
@@ -398,7 +398,7 @@ struct HostnameSection: View {
.font(.caption)
Spacer(minLength: 0)
}
- .foregroundStyle(.orange)
+ .foregroundStyle(FL.warning)
HStack(spacing: 8) {
Button(CaptureAccess.isInstalled ? L("Run Setup Again…") : L("Enable Packet Capture…")) { run(.install) }
.controlSize(.small).disabled(working)
@@ -406,7 +406,7 @@ struct HostnameSection: View {
}
}
.padding(.horizontal, 8).padding(.vertical, 6)
- .background(Color.orange.opacity(0.12), in: RoundedRectangle(cornerRadius: 7))
+ .background(FL.warning.opacity(0.12), in: RoundedRectangle(cornerRadius: 7))
}
private var ownerCard: some View {
@@ -438,9 +438,9 @@ struct HostnameSection: View {
private var captureColor: Color {
switch monitor.captureState {
- case .running: return .green
- case .noPermission, .stopped: return .orange
- case .failed: return .red
+ case .running: return FL.good
+ case .noPermission, .stopped: return FL.warning
+ case .failed: return FL.critical
}
}
@@ -476,7 +476,7 @@ struct CaptureOnboardingView: View {
VStack(alignment: .leading, spacing: 16) {
HStack(spacing: 14) {
Image(systemName: "network.badge.shield.half.filled")
- .font(.system(size: 38)).foregroundStyle(Color.accentColor)
+ .font(.system(size: 38)).foregroundStyle(FL.accent)
VStack(alignment: .leading, spacing: 4) {
Text(L("See which sites your apps talk to")).font(.title2.bold())
Text(L("Without this, most traffic shows only an IP address or network owner."))
diff --git a/Flowlight/UI/CaptureWarning.swift b/Flowlight/UI/CaptureWarning.swift
index d687167..cb6e37c 100644
--- a/Flowlight/UI/CaptureWarning.swift
+++ b/Flowlight/UI/CaptureWarning.swift
@@ -45,7 +45,7 @@ struct CaptureGlyph: View {
struct CaptureNote: View {
var text: String
var icon = "exclamationmark.triangle.fill"
- var tint = Color.orange
+ var tint = FL.warning
var body: some View {
HStack(alignment: .top, spacing: 8) {
@@ -93,7 +93,7 @@ struct CaptureWarningBanner: View {
var body: some View {
if let warning = monitor.captureWarning {
HStack(alignment: .top, spacing: 12) {
- CaptureGlyph(symbol: "exclamationmark.triangle.fill", tint: .orange)
+ CaptureGlyph(symbol: "exclamationmark.triangle.fill", tint: FL.warning)
VStack(alignment: .leading, spacing: 4) {
Text(L("The Network Extension can't capture on this Mac"))
.font(.body.weight(.medium))
@@ -109,7 +109,7 @@ struct CaptureWarningBanner: View {
}
}
.padding(12)
- .background(Color.orange.opacity(0.12), in: RoundedRectangle(cornerRadius: 10))
+ .background(FL.warning.opacity(0.12), in: RoundedRectangle(cornerRadius: 10))
}
}
}
@@ -123,10 +123,10 @@ struct BlockingUnavailableNotice: View {
var body: some View {
HStack(alignment: .top, spacing: 8) {
- Image(systemName: "shield.slash").font(.caption).foregroundStyle(.orange)
+ Image(systemName: "shield.slash").font(.caption).foregroundStyle(FL.warning)
VStack(alignment: .leading, spacing: 3) {
Text(enforcing ? L("Blocking is on, but not in effect here") : L("Blocking needs the Network Extension"))
- .font(.caption.weight(.semibold)).foregroundStyle(.orange)
+ .font(.caption.weight(.semibold)).foregroundStyle(FL.warning)
.frame(maxWidth: .infinity, alignment: .leading)
Text(reason)
.font(.caption2).foregroundStyle(.secondary)
@@ -138,7 +138,7 @@ struct BlockingUnavailableNotice: View {
}
}
.padding(.horizontal, 8).padding(.vertical, 6)
- .background(Color.orange.opacity(0.12), in: RoundedRectangle(cornerRadius: 7))
+ .background(FL.warning.opacity(0.12), in: RoundedRectangle(cornerRadius: 7))
}
private var reason: String {
@@ -160,7 +160,7 @@ struct OtherFiltersNotice: View {
var body: some View {
HStack(alignment: .top, spacing: 12) {
- CaptureGlyph(symbol: "shield.lefthalf.filled", tint: .orange)
+ CaptureGlyph(symbol: "shield.lefthalf.filled", tint: FL.warning)
VStack(alignment: .leading, spacing: 6) {
Text(filters.count == 1 ? L("Another content filter is active on this Mac")
: L("%lld other content filters are active on this Mac", filters.count))
@@ -191,6 +191,6 @@ struct OtherFiltersNotice: View {
}
}
.padding(12)
- .background(Color.orange.opacity(0.12), in: RoundedRectangle(cornerRadius: 10))
+ .background(FL.warning.opacity(0.12), in: RoundedRectangle(cornerRadius: 10))
}
}
diff --git a/Flowlight/UI/Components.swift b/Flowlight/UI/Components.swift
index 9136cfa..eaaad1e 100644
--- a/Flowlight/UI/Components.swift
+++ b/Flowlight/UI/Components.swift
@@ -42,7 +42,7 @@ struct AppIconView: View {
.font(.system(size: size * 0.55, weight: .bold, design: .rounded))
.foregroundStyle(.white)
.frame(width: size, height: size)
- .background(LinearGradient(colors: [.indigo, .purple], startPoint: .top, endPoint: .bottom),
+ .background(LinearGradient(colors: [.indigo, FL.tool], startPoint: .top, endPoint: .bottom),
in: RoundedRectangle(cornerRadius: size * 0.22))
.accessibilityHidden(true)
}
@@ -63,26 +63,36 @@ struct StatTile: View {
var title: String
var value: String
var systemImage: String
- var tint: Color = .accentColor
+ var tint: Color = FL.accent
+ /// A real trend for this figure, when the screen has one. Drawn as a sparkline; omitted otherwise.
+ var trend: [Double]? = nil
var body: some View {
- VStack(alignment: .leading, spacing: 4) {
- Label(title, systemImage: systemImage).font(.caption).foregroundStyle(.secondary)
- Text(value).font(.title2.monospacedDigit().weight(.semibold)).foregroundStyle(tint)
- .lineLimit(1).minimumScaleFactor(0.6)
+ VStack(alignment: .leading, spacing: Spacing.s) {
+ Label(title, systemImage: systemImage)
+ .labelStyle(.titleAndIcon)
+ .font(.caption).foregroundStyle(FL.muted)
+ Text(value)
+ .font(.system(size: 25, weight: .semibold, design: .rounded).monospacedDigit())
+ .foregroundStyle(tint)
+ .lineLimit(1).minimumScaleFactor(0.5)
.contentTransition(.numericText())
.motion(Motion.value, value: value)
+ if let trend, trend.count > 1 {
+ Sparkline(points: trend, color: tint).frame(height: 20).opacity(0.9)
+ }
}
.frame(maxWidth: .infinity, alignment: .leading)
- .padding(12)
- .background(.quaternary.opacity(0.5), in: RoundedRectangle(cornerRadius: 10))
+ .padding(Spacing.m)
+ .flCard()
}
}
+/// Traffic direction and anomaly, pointed at the brand tokens so every call site (there are many) refreshes at once.
enum TrafficColors {
- static let inbound = Color.blue
- static let outbound = Color.orange
- static let anomaly = Color.red
+ static let inbound = FL.received
+ static let outbound = FL.sent
+ static let anomaly = FL.critical
}
/// Opens the first window at the size of the screen, so everything Flowlight shows has room. It runs once: after
diff --git a/Flowlight/UI/ContentView.swift b/Flowlight/UI/ContentView.swift
index 448402c..6d305c3 100644
--- a/Flowlight/UI/ContentView.swift
+++ b/Flowlight/UI/ContentView.swift
@@ -22,6 +22,7 @@ struct ContentView: View {
}
}
.navigationSplitViewColumnWidth(min: 170, ideal: 190)
+ .tint(FL.accent)
.safeAreaInset(edge: .bottom) {
VStack(spacing: 0) {
if let update = updater.pendingUpdate {
@@ -30,7 +31,7 @@ struct ContentView: View {
.font(.caption.bold())
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, 10).padding(.vertical, 7)
- .background(Color.accentColor.opacity(0.15), in: RoundedRectangle(cornerRadius: 8))
+ .background(FL.accent.opacity(0.14), in: RoundedRectangle(cornerRadius: Radius.chip))
.contentShape(Rectangle())
}
.buttonStyle(.plain)
@@ -40,7 +41,7 @@ struct ContentView: View {
FocusBar()
Button { nav.selection = .capture } label: {
HStack(alignment: .top, spacing: 8) {
- LiveDot(color: monitor.isReceiving ? .green : .orange, active: monitor.isReceiving)
+ LiveDot(color: monitor.isReceiving ? FL.good : FL.warning, active: monitor.isReceiving)
.padding(.top, 4)
VStack(alignment: .leading, spacing: 2) {
Text(DemoData.isEnabled ? L("Demo data")
diff --git a/Flowlight/UI/CoverageView.swift b/Flowlight/UI/CoverageView.swift
index e9b6fbe..f1f5e23 100644
--- a/Flowlight/UI/CoverageView.swift
+++ b/Flowlight/UI/CoverageView.swift
@@ -64,10 +64,10 @@ private struct CoverageContent: View {
VStack(alignment: .leading, spacing: 10) {
CaptureFact(icon: monitor.mode == .networkExtension && !monitor.extensionFellBack
? "checkmark.seal" : "exclamationmark.triangle",
- tint: monitor.mode == .networkExtension && !monitor.extensionFellBack ? .green : .orange,
+ tint: monitor.mode == .networkExtension && !monitor.extensionFellBack ? FL.good : FL.warning,
title: captureTitle, detail: captureDetail)
CaptureFact(icon: inspection.enabled ? "checkmark.seal" : "info.circle",
- tint: inspection.enabled ? .green : .secondary,
+ tint: inspection.enabled ? FL.good : .secondary,
title: inspection.enabled ? L("HTTPS inspection is on") : L("HTTPS inspection is off"),
detail: inspection.enabled
? L("What an app sent is readable only where the app was routed through the proxy. Anything else is counted and named, but its contents were never offered to Flowlight.")
@@ -143,7 +143,7 @@ private struct CoverageRow: View {
var body: some View {
HStack(alignment: .top, spacing: 12) {
Image(systemName: coverage.isComplete ? "checkmark.circle.fill" : "circle.dashed")
- .foregroundStyle(coverage.isComplete ? Color.green : .orange)
+ .foregroundStyle(coverage.isComplete ? FL.good : FL.warning)
.frame(width: 20)
VStack(alignment: .leading, spacing: 3) {
HStack(spacing: 8) {
diff --git a/Flowlight/UI/DesignTokens.swift b/Flowlight/UI/DesignTokens.swift
new file mode 100644
index 0000000..419ea4d
--- /dev/null
+++ b/Flowlight/UI/DesignTokens.swift
@@ -0,0 +1,160 @@
+import AppKit
+import SwiftUI
+
+/// Flowlight's design tokens — one place for the brand palette, spacing, radii and the shared surfaces, so the app
+/// reads as one considered system instead of scattered literals. The colours are the same ones the website uses,
+/// each resolved for light and dark, because the app follows the system appearance.
+///
+/// Semantic roles, not raw hues: `accent` is the one "light" colour, reserved for what needs a decision; `received`
+/// and `sent` carry traffic direction; `critical`/`warning`/`good` are status only and never stand in for the accent.
+enum FL {
+ // Surfaces, deepest to lightest ink.
+ static let ground = Color(light: 0xf4f5f8, dark: 0x0d0f16)
+ static let surface = Color(light: 0xffffff, dark: 0x151824)
+ static let surfaceDeep = Color(light: 0xf0f1f6, dark: 0x10131d)
+ static let surface2 = Color(light: 0xeceef4, dark: 0x1c2030)
+ static let line = Color(light: 0xdde0e8, dark: 0x262b3b)
+
+ // Text.
+ static let ink = Color(light: 0x11131a, dark: 0xeef0f6)
+ static let muted = Color(light: 0x565d70, dark: 0xa3a9bb)
+ static let faint = Color(light: 0x8a90a2, dark: 0x737a8f)
+
+ // Semantic.
+ static let accent = Color(light: 0x4a3aa7, dark: 0x9085e9)
+ static let received = Color(light: 0x2a78d6, dark: 0x4a93ea)
+ static let sent = Color(light: 0xd95926, dark: 0xe5723f)
+ static let critical = Color(light: 0xc93b33, dark: 0xec6a62)
+ static let warning = Color(light: 0xb86e00, dark: 0xe0a13a)
+ static let good = Color(light: 0x1f9d6b, dark: 0x4fb98a)
+ /// Tools and MCP servers — a violet kept distinct from the primary accent so "something ran" reads apart from
+ /// "something needs a decision".
+ static let tool = Color(light: 0x6f4bc7, dark: 0xb4a2f2)
+}
+
+/// Consistent spacing, so groups line up and gaps read as intentional rather than arbitrary.
+enum Spacing {
+ static let xs: CGFloat = 4
+ static let s: CGFloat = 8
+ static let m: CGFloat = 12
+ static let l: CGFloat = 16
+ static let xl: CGFloat = 24
+}
+
+/// Corner radii by role: a chip is tighter than a card, a card tighter than a panel.
+enum Radius {
+ static let chip: CGFloat = 8
+ static let card: CGFloat = 12
+ static let panel: CGFloat = 16
+}
+
+extension Color {
+ /// A colour that resolves differently in light and dark, like an asset-catalog colour set but defined in code so
+ /// the whole palette lives in one file. `0xRRGGBB`.
+ init(light: Int, dark: Int) {
+ self.init(nsColor: NSColor(name: nil) { appearance in
+ appearance.bestMatch(from: [.darkAqua, .aqua]) == .darkAqua ? NSColor(rgb: dark) : NSColor(rgb: light)
+ })
+ }
+}
+
+extension NSColor {
+ convenience init(rgb: Int) {
+ self.init(srgbRed: Double((rgb >> 16) & 0xff) / 255,
+ green: Double((rgb >> 8) & 0xff) / 255,
+ blue: Double(rgb & 0xff) / 255, alpha: 1)
+ }
+}
+
+/// The one card surface the app uses, replacing the three slightly different `.quaternary` rounded rectangles that
+/// grew up across the screens. A faint top-to-bottom gradient gives depth without a heavy shadow; `accent: true`
+/// tints it for the one card that asks the user to act (the always-monitor banner).
+struct FLCard: ViewModifier {
+ var radius: CGFloat = Radius.card
+ var accent: Bool = false
+
+ func body(content: Content) -> some View {
+ content
+ .background(
+ RoundedRectangle(cornerRadius: radius, style: .continuous)
+ .fill(accent
+ ? AnyShapeStyle(FL.accent.opacity(0.09))
+ : AnyShapeStyle(LinearGradient(colors: [FL.surface, FL.surfaceDeep],
+ startPoint: .top, endPoint: .bottom)))
+ )
+ .overlay(
+ RoundedRectangle(cornerRadius: radius, style: .continuous)
+ .strokeBorder(accent ? FL.accent.opacity(0.28) : FL.line, lineWidth: 1)
+ )
+ }
+}
+
+extension View {
+ func flCard(radius: CGFloat = Radius.card, accent: Bool = false) -> some View {
+ modifier(FLCard(radius: radius, accent: accent))
+ }
+}
+
+/// A small status pill — a label and icon on a tinted, bordered capsule, never colour alone. The one chip style for
+/// risk badges, allowlist state and anything else that marks a value's status at a glance.
+struct FLPill: View {
+ var text: String
+ var systemImage: String?
+ var color: Color
+
+ var body: some View {
+ Label {
+ Text(text)
+ } icon: {
+ if let systemImage { Image(systemName: systemImage) }
+ }
+ .labelStyle(.titleAndIcon)
+ .font(.caption2.bold())
+ .padding(.horizontal, 7)
+ .padding(.vertical, 2)
+ .background(color.opacity(0.14), in: Capsule())
+ .overlay(Capsule().strokeBorder(color.opacity(0.32), lineWidth: 1))
+ .foregroundStyle(color)
+ }
+}
+
+/// A single-line trend, drawn to its own min/max so the shape reads even for small ranges. Shown only with real
+/// data — never a decorative squiggle.
+struct Sparkline: View {
+ var points: [Double]
+ var color: Color = FL.accent
+
+ var body: some View {
+ GeometryReader { geo in
+ let w = geo.size.width, h = geo.size.height
+ let lo = points.min() ?? 0, hi = points.max() ?? 1
+ let span = max(hi - lo, 0.0001)
+ Path { path in
+ for (i, v) in points.enumerated() {
+ let x = points.count <= 1 ? 0 : w * CGFloat(i) / CGFloat(points.count - 1)
+ let y = h - CGFloat((v - lo) / span) * h
+ i == 0 ? path.move(to: CGPoint(x: x, y: y)) : path.addLine(to: CGPoint(x: x, y: y))
+ }
+ }
+ .stroke(color, style: StrokeStyle(lineWidth: 1.8, lineCap: .round, lineJoin: .round))
+ }
+ }
+}
+
+/// An inline magnitude bar — a value's share of the largest in its column, so a table reads as a chart too.
+struct TrafficBar: View {
+ var fraction: Double
+ var color: Color = FL.accent
+
+ var body: some View {
+ GeometryReader { geo in
+ ZStack(alignment: .leading) {
+ Capsule().fill(FL.surface2)
+ Capsule()
+ .fill(LinearGradient(colors: [color.opacity(0.65), color], startPoint: .leading, endPoint: .trailing))
+ .frame(width: max(0, min(1, fraction)) * geo.size.width)
+ }
+ }
+ .frame(height: 5)
+ }
+}
diff --git a/Flowlight/UI/DevicesView.swift b/Flowlight/UI/DevicesView.swift
index a3a26aa..4ec8446 100644
--- a/Flowlight/UI/DevicesView.swift
+++ b/Flowlight/UI/DevicesView.swift
@@ -261,7 +261,7 @@ private struct DeviceRow: View {
}
private var glyph: some View {
- let tint: Color = device.connected ? .green : .secondary
+ let tint: Color = device.connected ? FL.good : .secondary
return Image(systemName: device.kind.icon)
.font(.caption.weight(.semibold))
.foregroundStyle(tint)
@@ -272,7 +272,7 @@ private struct DeviceRow: View {
private var state: some View {
HStack(spacing: 5) {
- LiveDot(color: device.connected ? .green : .secondary, active: device.connected, size: 6)
+ LiveDot(color: device.connected ? FL.good : .secondary, active: device.connected, size: 6)
Text(stateWord)
.font(.caption)
.foregroundStyle(device.connected ? HierarchicalShapeStyle.secondary : .tertiary)
@@ -351,10 +351,10 @@ private struct DeviceEventRow: View {
private var tint: Color {
switch event.change {
- case .connected: return .green
+ case .connected: return FL.good
case .disconnected: return .secondary
case .appeared: return .blue
- case .removed: return .orange
+ case .removed: return FL.warning
}
}
}
diff --git a/Flowlight/UI/FocusView.swift b/Flowlight/UI/FocusView.swift
index cfb78dd..f9b2131 100644
--- a/Flowlight/UI/FocusView.swift
+++ b/Flowlight/UI/FocusView.swift
@@ -9,7 +9,7 @@ struct FocusBar: View {
Button { editing = true } label: {
HStack(alignment: .top, spacing: 8) {
Image(systemName: focus.isActive ? "scope" : "circle.dashed")
- .foregroundStyle(focus.isActive ? Color.accentColor : .secondary)
+ .foregroundStyle(focus.isActive ? FL.accent : .secondary)
.padding(.top, 1)
VStack(alignment: .leading, spacing: 2) {
Text(focus.isActive ? L("Focus on") : L("Focus off")).font(.caption.bold())
@@ -89,7 +89,7 @@ struct FocusEditor: View {
}
if invalid {
Text(L("Enter a hostname, a domain or an IP address. Ranges like 10.0.0.0/8 aren't supported here."))
- .font(.caption).foregroundStyle(.orange).fixedSize(horizontal: false, vertical: true)
+ .font(.caption).foregroundStyle(FL.warning).fixedSize(horizontal: false, vertical: true)
}
if adding == .app {
diff --git a/Flowlight/UI/GuardrailsPane.swift b/Flowlight/UI/GuardrailsPane.swift
index f98a3aa..e2918b3 100644
--- a/Flowlight/UI/GuardrailsPane.swift
+++ b/Flowlight/UI/GuardrailsPane.swift
@@ -22,7 +22,7 @@ struct GuardrailsPane: View {
if !monitor.inspection.enabled {
Label(L("HTTPS inspection is off, so Flowlight can't read the tool list — nothing here is being carried out yet."),
systemImage: "exclamationmark.triangle")
- .font(.caption).foregroundStyle(.orange).fixedSize(horizontal: false, vertical: true)
+ .font(.caption).foregroundStyle(FL.warning).fixedSize(horizontal: false, vertical: true)
}
presets
diff --git a/Flowlight/UI/Insights/BehaviourPanel.swift b/Flowlight/UI/Insights/BehaviourPanel.swift
index be03218..c6cb2c8 100644
--- a/Flowlight/UI/Insights/BehaviourPanel.swift
+++ b/Flowlight/UI/Insights/BehaviourPanel.swift
@@ -43,7 +43,7 @@ struct BehaviourPanel: View {
}
ForEach(finding.signals) { signal in
HStack(alignment: .firstTextBaseline, spacing: 6) {
- Image(systemName: icon(signal.kind)).font(.caption2).foregroundStyle(.orange)
+ Image(systemName: icon(signal.kind)).font(.caption2).foregroundStyle(FL.warning)
.frame(width: 12)
Text(signal.detail).font(.callout).foregroundStyle(.secondary)
.fixedSize(horizontal: false, vertical: true)
diff --git a/Flowlight/UI/Insights/InsightsPanel.swift b/Flowlight/UI/Insights/InsightsPanel.swift
index df40907..4369558 100644
--- a/Flowlight/UI/Insights/InsightsPanel.swift
+++ b/Flowlight/UI/Insights/InsightsPanel.swift
@@ -136,7 +136,7 @@ struct DonutCard: View {
EntityGlyph(entity: slice.entity)
Text(slice.entity.kind == .other ? L("%@ more…", insight.hiddenCount.formatted()) : slice.entity.label)
.lineLimit(1).truncationMode(.middle)
- .foregroundStyle(slice.entity.kind == .other ? Color.accentColor : .primary)
+ .foregroundStyle(slice.entity.kind == .other ? FL.accent : .primary)
Spacer(minLength: 4)
Text(ByteFormat.string(metric.value(slice.counters))).monospacedDigit().foregroundStyle(.secondary)
Text(ShareFormat.string(slice.share))
diff --git a/Flowlight/UI/InspectView.swift b/Flowlight/UI/InspectView.swift
index 0d992c7..53ada31 100644
--- a/Flowlight/UI/InspectView.swift
+++ b/Flowlight/UI/InspectView.swift
@@ -49,7 +49,7 @@ private struct InspectContent: View {
if let diagnosis = inspection.diagnosis {
Text(diagnosis)
.font(.callout)
- .foregroundStyle(inspection.diagnosisIsGood ? .secondary : Color.orange)
+ .foregroundStyle(inspection.diagnosisIsGood ? .secondary : FL.warning)
.fixedSize(horizontal: false, vertical: true)
}
}
@@ -123,7 +123,7 @@ private struct InspectContent: View {
private var statusBar: some View {
HStack(spacing: 10) {
- Circle().fill(DemoData.isEnabled || inspection.running && inspection.trusted ? Color.green : Color.orange).frame(width: 8, height: 8)
+ Circle().fill(DemoData.isEnabled || inspection.running && inspection.trusted ? FL.good : FL.warning).frame(width: 8, height: 8)
Text(statusText).font(.callout)
// A mock left on looks exactly like an agent misbehaving, so it says so on every screen that shows
// inspected traffic, not only in the setup page where it was switched on.
@@ -133,7 +133,7 @@ private struct InspectContent: View {
: L("%lld mock rules on", inspection.activeMockRules),
systemImage: "wand.and.stars")
}
- .buttonStyle(.plain).font(.callout.bold()).foregroundStyle(.purple)
+ .buttonStyle(.plain).font(.callout.bold()).foregroundStyle(FL.tool)
.help(L("Flowlight is answering some requests itself instead of forwarding them. Click to review the rules."))
}
Spacer()
@@ -200,12 +200,12 @@ private struct InspectContent: View {
TableColumn(L("Request")) { e in
VStack(alignment: .leading, spacing: 1) {
if e.note != nil {
- Label(L("Not inspected: %@", e.host), systemImage: "lock").foregroundStyle(.orange).lineLimit(1)
+ Label(L("Not inspected: %@", e.host), systemImage: "lock").foregroundStyle(FL.warning).lineLimit(1)
} else {
HStack(spacing: 5) {
Text("\(e.method) \(e.host)").lineLimit(1)
if e.mockRule != nil {
- Label(L("Mocked"), systemImage: "wand.and.stars").font(.caption2.bold()).foregroundStyle(.purple)
+ Label(L("Mocked"), systemImage: "wand.and.stars").font(.caption2.bold()).foregroundStyle(FL.tool)
.labelStyle(.titleAndIcon)
.help(L("Flowlight answered this itself; the request never reached %@", e.host))
}
@@ -222,7 +222,7 @@ private struct InspectContent: View {
.width(min: 150, ideal: 220)
TableColumn(L("Status")) { e in
Text(e.status.map(String.init) ?? "–").monospacedDigit()
- .foregroundStyle((e.status ?? 0) >= 400 ? .red : .primary)
+ .foregroundStyle((e.status ?? 0) >= 400 ? FL.critical : .primary)
}
.width(min: 40, ideal: 46)
TableColumn(L("Size")) { e in
@@ -231,7 +231,7 @@ private struct InspectContent: View {
.width(min: 60, ideal: 68)
TableColumn(L("Tool calls")) { e in
if !e.toolCalls.isEmpty {
- Text(e.toolCalls.map(\.displayName).joined(separator: ", ")).lineLimit(1).foregroundStyle(.purple)
+ Text(e.toolCalls.map(\.displayName).joined(separator: ", ")).lineLimit(1).foregroundStyle(FL.tool)
} else if let id = e.id, let link = links[id] {
Text("← \(link.call.displayName): \(link.call.summary ?? "")").lineLimit(1).foregroundStyle(.secondary)
.help(L("Made by the tool the model asked for: %@", link.call.summary ?? link.call.input))
@@ -283,7 +283,7 @@ private struct InspectionSetup: View {
var body: some View {
VStack(alignment: .leading, spacing: 18) {
HStack(alignment: .top, spacing: 14) {
- Image(systemName: "lock.open.display").font(.system(size: 34)).foregroundStyle(Color.accentColor)
+ Image(systemName: "lock.open.display").font(.system(size: 34)).foregroundStyle(FL.accent)
VStack(alignment: .leading, spacing: 6) {
Text(L("HTTPS inspection")).font(.title2.bold())
Text(L("Inspect HTTP requests and responses for applications routed through Flowlight, including supported model tool calls. Flowlight uses a local proxy and a certificate authority created on this Mac. Inspection is off by default, and recorded data is stored locally."))
@@ -331,7 +331,7 @@ private struct InspectionSetup: View {
}
if let error = inspection.lastError {
- Label(error, systemImage: "exclamationmark.triangle").foregroundStyle(.orange)
+ Label(error, systemImage: "exclamationmark.triangle").foregroundStyle(FL.warning)
.fixedSize(horizontal: false, vertical: true)
}
@@ -398,7 +398,7 @@ private struct InspectionSetup: View {
if inspection.activeMockRules > 0 {
Label(inspection.activeMockRules == 1 ? L("1 on") : L("%lld on", inspection.activeMockRules),
systemImage: "wand.and.stars")
- .font(.caption.bold()).foregroundStyle(.purple)
+ .font(.caption.bold()).foregroundStyle(FL.tool)
}
}
}
@@ -420,7 +420,7 @@ private struct InspectionSetup: View {
private func ready(_ text: String, ok: Bool) -> some View {
Label(text, systemImage: ok ? "checkmark.circle.fill" : "circle.dotted")
- .foregroundStyle(ok ? .green : .secondary)
+ .foregroundStyle(ok ? FL.good : .secondary)
}
private func add() {
@@ -459,13 +459,13 @@ private struct ExchangeDetail: View {
}
}
if let note = exchange.note {
- Label(note, systemImage: "lock").foregroundStyle(.orange).fixedSize(horizontal: false, vertical: true)
+ Label(note, systemImage: "lock").foregroundStyle(FL.warning).fixedSize(horizontal: false, vertical: true)
}
if let mock = exchange.mockRule {
// The whole point of the feature is that this answer is a fiction; say so before anything below is read.
Label(L("Answered by Flowlight, not by %@ — mock rule “%@”. The request was never sent.", exchange.host, mock),
systemImage: "wand.and.stars")
- .foregroundStyle(.purple).fixedSize(horizontal: false, vertical: true)
+ .foregroundStyle(FL.tool).fixedSize(horizontal: false, vertical: true)
}
if let guardrail = exchange.guardrail {
// The exchange really happened and really was sent — but not quite as the agent wrote it, and
@@ -526,7 +526,7 @@ private struct ExchangeDetail: View {
VStack(alignment: .leading, spacing: 2) {
HStack(spacing: 6) {
Image(systemName: m.isError ? "xmark.octagon.fill" : "puzzlepiece.extension")
- .foregroundStyle(m.isError ? TrafficColors.anomaly : .purple)
+ .foregroundStyle(m.isError ? TrafficColors.anomaly : FL.tool)
Text("\(m.server)\(m.version.map { " \($0)" } ?? "") · \(m.method)\(m.tool.map { " · \($0)" } ?? "")")
.font(.callout.bold())
}
@@ -591,7 +591,7 @@ private struct ExchangeDetail: View {
private func resultView(_ result: ToolResult) -> some View {
VStack(alignment: .leading, spacing: 1) {
Label(result.isError ? L("Error") : L("Result"), systemImage: result.isError ? "xmark.octagon.fill" : "checkmark.circle.fill")
- .font(.caption.bold()).foregroundStyle(result.isError ? TrafficColors.anomaly : .green)
+ .font(.caption.bold()).foregroundStyle(result.isError ? TrafficColors.anomaly : FL.good)
Text(result.output.isEmpty ? L("(empty)") : result.output.trimmingCharacters(in: .whitespacesAndNewlines))
.font(.caption.monospaced()).lineLimit(6).textSelection(.enabled).foregroundStyle(.secondary)
if result.outputSize > result.output.count {
diff --git a/Flowlight/UI/JSONTreeView.swift b/Flowlight/UI/JSONTreeView.swift
index 36d52e8..3458803 100644
--- a/Flowlight/UI/JSONTreeView.swift
+++ b/Flowlight/UI/JSONTreeView.swift
@@ -28,7 +28,7 @@ struct BodyView: View {
let count = matchingLines.count
Text(count == 0 ? L("no match")
: count == 1 ? L("%lld matching line", count) : L("%lld matching lines", count))
- .font(.caption).foregroundStyle(count == 0 ? .secondary : Color.accentColor)
+ .font(.caption).foregroundStyle(count == 0 ? .secondary : FL.accent)
if count > 0, mode == "tree", isStructured {
Button(L("Show")) { mode = "raw" }
.controlSize(.small)
@@ -93,7 +93,7 @@ struct BodyView: View {
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.vertical, 1)
.background(line.range(of: term, options: .caseInsensitive) != nil
- ? Color.accentColor.opacity(0.10) : Color.clear)
+ ? FL.accent.opacity(0.10) : Color.clear)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
@@ -219,7 +219,7 @@ struct JSONNodeView: View {
.frame(width: 12)
if let key {
(Text(keyStyle == .key ? key : keyStyle == .index ? "[\(key)]" : key)
- .foregroundStyle(keyStyle == .key ? Color.accentColor : .secondary)
+ .foregroundStyle(keyStyle == .key ? FL.accent : .secondary)
.fontWeight(keyStyle == .event ? .semibold : .regular)
+ Text(":").foregroundStyle(.tertiary))
}
diff --git a/Flowlight/UI/MCPDomainsSection.swift b/Flowlight/UI/MCPDomainsSection.swift
index 6c789fd..649b0a0 100644
--- a/Flowlight/UI/MCPDomainsSection.swift
+++ b/Flowlight/UI/MCPDomainsSection.swift
@@ -20,7 +20,7 @@ struct MCPDomainsSection: View {
// Said here as well as on the Rules screen: a button that writes a rule nothing can carry
// out should not look like a button that stops traffic.
Text(L("Rules are recorded but not enforced in this capture mode."))
- .font(.caption).foregroundStyle(.orange)
+ .font(.caption).foregroundStyle(FL.warning)
}
}
if entries.isEmpty {
diff --git a/Flowlight/UI/MockRulesView.swift b/Flowlight/UI/MockRulesView.swift
index c6c97d5..1af286e 100644
--- a/Flowlight/UI/MockRulesView.swift
+++ b/Flowlight/UI/MockRulesView.swift
@@ -19,7 +19,7 @@ struct MockRulesSection: View {
if !inspection.enabled {
Label(L("HTTPS inspection is off, so nothing is decrypted and no rule can answer anything yet."),
systemImage: "exclamationmark.triangle")
- .font(.caption).foregroundStyle(.orange).fixedSize(horizontal: false, vertical: true)
+ .font(.caption).foregroundStyle(FL.warning).fixedSize(horizontal: false, vertical: true)
}
if inspection.mockRules.isEmpty {
diff --git a/Flowlight/UI/ReportsView.swift b/Flowlight/UI/ReportsView.swift
index 7a91c8a..3cb54c1 100644
--- a/Flowlight/UI/ReportsView.swift
+++ b/Flowlight/UI/ReportsView.swift
@@ -289,7 +289,7 @@ struct ReportsView: View {
.buttonStyle(.plain).accessibilityLabel(L("Remove filter %@", text))
}
.padding(.horizontal, 8).padding(.vertical, 3)
- .background(Color.accentColor.opacity(0.15), in: Capsule())
+ .background(FL.accent.opacity(0.15), in: Capsule())
}
private var summary: some View {
@@ -751,7 +751,7 @@ struct ShareBar: View {
GeometryReader { geo in
ZStack(alignment: .leading) {
Capsule().fill(.quaternary)
- Capsule().fill(Color.accentColor.opacity(0.8)).frame(width: max(2, geo.size.width * min(1, fraction)))
+ Capsule().fill(FL.accent.opacity(0.8)).frame(width: max(2, geo.size.width * min(1, fraction)))
}
}
.frame(height: 5)
diff --git a/Flowlight/UI/RuleEditor.swift b/Flowlight/UI/RuleEditor.swift
index b3dc322..80e97de 100644
--- a/Flowlight/UI/RuleEditor.swift
+++ b/Flowlight/UI/RuleEditor.swift
@@ -119,7 +119,7 @@ struct RuleEditor: View {
if !rule.isComplete {
Label(L("Name an app or a destination — a rule that names neither would decide every connection on the Mac."),
systemImage: "exclamationmark.circle")
- .font(.caption).foregroundStyle(.orange).fixedSize(horizontal: false, vertical: true)
+ .font(.caption).foregroundStyle(FL.warning).fixedSize(horizontal: false, vertical: true)
}
Spacer()
Button(L("Cancel")) { dismiss() }.keyboardShortcut(.cancelAction)
@@ -144,9 +144,9 @@ struct RuleEditor: View {
HStack(spacing: 10) {
Image(systemName: rule.action == .block ? "hand.raised.fill" : "checkmark.shield.fill")
.font(.callout.weight(.semibold))
- .foregroundStyle(rule.action == .block ? Color.red : Color.green)
+ .foregroundStyle(rule.action == .block ? FL.critical : FL.good)
.frame(width: 28, height: 28)
- .background((rule.action == .block ? Color.red : Color.green).opacity(0.14),
+ .background((rule.action == .block ? FL.critical : FL.good).opacity(0.14),
in: RoundedRectangle(cornerRadius: 8))
VStack(alignment: .leading, spacing: 1) {
Text(isNew ? L("New Rule") : L("Edit Rule")).font(.headline)
@@ -246,9 +246,9 @@ struct RuleEditor: View {
}
.buttonStyle(.borderless)
.font(.caption.weight(on ? .semibold : .regular))
- .foregroundStyle(on ? Color.accentColor : Color.secondary)
+ .foregroundStyle(on ? FL.accent : Color.secondary)
.padding(.horizontal, 7).padding(.vertical, 4)
- .background(on ? Color.accentColor.opacity(0.18) : Color.clear, in: RoundedRectangle(cornerRadius: 6))
+ .background(on ? FL.accent.opacity(0.18) : Color.clear, in: RoundedRectangle(cornerRadius: 6))
}
}
}
diff --git a/Flowlight/UI/RuleSimulationView.swift b/Flowlight/UI/RuleSimulationView.swift
index b439be7..8f4fc57 100644
--- a/Flowlight/UI/RuleSimulationView.swift
+++ b/Flowlight/UI/RuleSimulationView.swift
@@ -57,7 +57,7 @@ struct RuleSimulationView: View {
result.connectionsConsidered),
systemImage: result.connectionsConsidered == 0 ? "questionmark.circle" : "checkmark.circle")
.font(.callout)
- .foregroundStyle(result.connectionsConsidered == 0 ? Color.secondary : Color.green)
+ .foregroundStyle(result.connectionsConsidered == 0 ? Color.secondary : FL.good)
} else if !result.refusals.isEmpty {
Label(L("Would have refused %lld of %lld connections, from %lld apps, %@ in all.",
result.connectionsRefused, result.connectionsConsidered, result.appsAffected,
@@ -67,7 +67,7 @@ struct RuleSimulationView: View {
} else {
Label(L("Would have allowed %lld connections that are refused today.", result.permits.reduce(0) { $0 + $1.connections }),
systemImage: "checkmark.shield")
- .font(.callout).foregroundStyle(.green)
+ .font(.callout).foregroundStyle(FL.good)
}
}
@@ -76,7 +76,7 @@ struct RuleSimulationView: View {
ForEach(result.changes.prefix(12)) { change in
HStack(spacing: 8) {
Image(systemName: change.refused ? "xmark.circle.fill" : "checkmark.circle.fill")
- .foregroundStyle(change.refused ? .orange : .green)
+ .foregroundStyle(change.refused ? FL.warning : FL.good)
.imageScale(.small)
Text(change.appName).frame(minWidth: 120, alignment: .leading)
Text(change.destination).font(.caption.monospaced()).foregroundStyle(.secondary)
diff --git a/Flowlight/UI/RulesView.swift b/Flowlight/UI/RulesView.swift
index 6f657da..50f7715 100644
--- a/Flowlight/UI/RulesView.swift
+++ b/Flowlight/UI/RulesView.swift
@@ -137,7 +137,7 @@ struct RulesView: View {
Label(L("Paused until %@", until.formatted(date: .omitted, time: .shortened)),
systemImage: "play.circle.fill")
}
- .tint(.orange)
+ .tint(FL.warning)
.help(L("Nothing is being refused. Click to start again now."))
} else {
Menu {
@@ -224,7 +224,7 @@ struct RulesView: View {
private var banner: some View {
if store.isPaused, let until = store.pausedUntil {
HStack(spacing: 8) {
- Image(systemName: "pause.circle.fill").foregroundStyle(.orange)
+ Image(systemName: "pause.circle.fill").foregroundStyle(FL.warning)
Text(L("Nothing is being refused until %@.", until.formatted(date: .omitted, time: .shortened)))
.font(.callout)
Spacer(minLength: 8)
@@ -235,7 +235,7 @@ struct RulesView: View {
.overlay(alignment: .bottom) { Divider() }
} else if let warning = engineWarning {
HStack(alignment: .top, spacing: 8) {
- Image(systemName: "eye").foregroundStyle(.orange)
+ Image(systemName: "eye").foregroundStyle(FL.warning)
Text(warning).font(.caption).foregroundStyle(.secondary)
Spacer(minLength: 0)
}
@@ -388,7 +388,7 @@ private struct RuleRow: View {
.accessibilityLabel(L("More for %@", RuleWords.title(rule)))
}
if let limitation {
- note(limitation, icon: "eye", tint: .orange)
+ note(limitation, icon: "eye", tint: FL.warning)
}
if !rule.isUsable {
note(L("Used up — a one-off allowance, already spent."), icon: "checkmark.circle", tint: .secondary)
@@ -411,8 +411,8 @@ private struct RuleRow: View {
/// this is the mark the eye lands on first and a red hand over a rule that refuses nothing would be a lie.
private var glyph: some View {
let tint: Color = !rule.enabled ? .secondary
- : limitation != nil ? .orange
- : rule.action == .block ? .red : .green
+ : limitation != nil ? FL.warning
+ : rule.action == .block ? FL.critical : FL.good
let symbol = limitation != nil && rule.enabled ? "eye.fill"
: rule.action == .block ? "hand.raised.fill" : "checkmark.shield.fill"
return Image(systemName: symbol)
@@ -504,7 +504,7 @@ private struct EventRow: View {
HStack(alignment: .top, spacing: 10) {
Image(systemName: event.action == .block ? "hand.raised.fill" : "checkmark.shield.fill")
.font(.caption)
- .foregroundStyle(event.action == .block ? Color.red : Color.green)
+ .foregroundStyle(event.action == .block ? FL.critical : FL.good)
.frame(width: 14)
.padding(.top, 2)
.help(event.action == .block ? L("Refused") : L("Allowed by an exception"))
@@ -790,7 +790,7 @@ struct RuleTemplateLibrary: View {
}
if template.engines(for: filled).contains(.request) {
note(L("These name an HTTP method, so only HTTPS inspection can carry them out. With it off they sit in the list watching."),
- icon: "eye", tint: .orange)
+ icon: "eye", tint: FL.warning)
}
if let caveat = template.caveat {
note(caveat, icon: "exclamationmark.circle", tint: .secondary)
@@ -806,7 +806,7 @@ struct RuleTemplateLibrary: View {
Spacer()
if added.contains(template.id) {
Label(L("Added"), systemImage: "checkmark.circle.fill")
- .font(.caption).foregroundStyle(.green)
+ .font(.caption).foregroundStyle(FL.good)
}
Button(rules.count > 1 ? L("Add %lld Rules", rules.count) : L("Add Rule")) {
add(rules)
@@ -851,7 +851,7 @@ struct RuleTemplateLibrary: View {
VStack(alignment: .leading, spacing: 3) {
ForEach(rules) { rule in
HStack(alignment: .firstTextBaseline, spacing: 6) {
- Image(systemName: "hand.raised.fill").font(.caption2).foregroundStyle(.red.opacity(0.8))
+ Image(systemName: "hand.raised.fill").font(.caption2).foregroundStyle(FL.critical.opacity(0.8))
Text(subjectLine(rule)).font(.caption.monospaced()).lineLimit(1).truncationMode(.middle)
Spacer(minLength: 0)
if rule.schedule.kind != .always {
diff --git a/Flowlight/UI/SettingsView.swift b/Flowlight/UI/SettingsView.swift
index 2bcf196..769f2ba 100644
--- a/Flowlight/UI/SettingsView.swift
+++ b/Flowlight/UI/SettingsView.swift
@@ -35,7 +35,7 @@ struct SettingsView: View {
Toggle(L("Launch at login"), isOn: $launchAtLogin)
.onChange(of: launchAtLogin) { _, enabled in setLaunchAtLogin(enabled) }
.task { launchAtLogin = SMAppService.mainApp.status == .enabled }
- if let loginItemError { Text(loginItemError).font(.caption).foregroundStyle(.red) }
+ if let loginItemError { Text(loginItemError).font(.caption).foregroundStyle(FL.critical) }
Toggle(isOn: $backgroundOnly) {
VStack(alignment: .leading, spacing: 2) {
Text(L("Run in the background"))
diff --git a/Flowlight/UI/UpdateView.swift b/Flowlight/UI/UpdateView.swift
index 37f444f..4c8687b 100644
--- a/Flowlight/UI/UpdateView.swift
+++ b/Flowlight/UI/UpdateView.swift
@@ -46,7 +46,7 @@ struct UpdateView: View {
ProgressView(L("Preparing the update. Flowlight will quit and reopen…")).controlSize(.small)
}
if case .failed(let message) = updater.state {
- Label(message, systemImage: "exclamationmark.triangle").foregroundStyle(.orange)
+ Label(message, systemImage: "exclamationmark.triangle").foregroundStyle(FL.warning)
}
HStack {
From 6ce4791cd7394a1991941b64c43405fec9ab0d21 Mon Sep 17 00:00:00 2001
From: blessdyb
Date: Wed, 30 Sep 2026 23:21:48 -0700
Subject: [PATCH 2/2] Flowlight 0.12.0
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Version bump, release notes for the interface refresh, and the rebuilt site.
Extension stays pinned at 0.10.1 — no reboot to upgrade.
Co-Authored-By: Claude Opus 4.8
---
docs/404.html | 2 +-
docs/about/index.html | 2 +-
docs/de/about/index.html | 2 +-
docs/de/docs/index.html | 4 ++--
docs/de/index.html | 6 +++---
docs/de/privacy/index.html | 2 +-
docs/de/threat-model/index.html | 2 +-
docs/docs/index.html | 4 ++--
docs/es/about/index.html | 2 +-
docs/es/docs/index.html | 4 ++--
docs/es/index.html | 6 +++---
docs/es/privacy/index.html | 2 +-
docs/es/threat-model/index.html | 2 +-
docs/fr/about/index.html | 2 +-
docs/fr/docs/index.html | 4 ++--
docs/fr/index.html | 6 +++---
docs/fr/privacy/index.html | 2 +-
docs/fr/threat-model/index.html | 2 +-
docs/index.html | 6 +++---
docs/it/about/index.html | 2 +-
docs/it/docs/index.html | 4 ++--
docs/it/index.html | 6 +++---
docs/it/privacy/index.html | 2 +-
docs/it/threat-model/index.html | 2 +-
docs/ja/about/index.html | 2 +-
docs/ja/docs/index.html | 4 ++--
docs/ja/index.html | 6 +++---
docs/ja/privacy/index.html | 2 +-
docs/ja/threat-model/index.html | 2 +-
docs/ko/about/index.html | 2 +-
docs/ko/docs/index.html | 4 ++--
docs/ko/index.html | 6 +++---
docs/ko/privacy/index.html | 2 +-
docs/ko/threat-model/index.html | 2 +-
docs/llms-full.txt | 18 +++++++++++++++---
docs/llms.txt | 2 +-
docs/privacy/index.html | 2 +-
docs/pt-PT/about/index.html | 2 +-
docs/pt-PT/docs/index.html | 4 ++--
docs/pt-PT/index.html | 6 +++---
docs/pt-PT/privacy/index.html | 2 +-
docs/pt-PT/threat-model/index.html | 2 +-
docs/releases/index.html | 17 +++++++++++++++--
docs/threat-model/index.html | 2 +-
docs/zh-Hans/about/index.html | 2 +-
docs/zh-Hans/docs/index.html | 4 ++--
docs/zh-Hans/index.html | 6 +++---
docs/zh-Hans/privacy/index.html | 2 +-
docs/zh-Hans/threat-model/index.html | 2 +-
docs/zh-Hant/about/index.html | 2 +-
docs/zh-Hant/docs/index.html | 4 ++--
docs/zh-Hant/index.html | 6 +++---
docs/zh-Hant/privacy/index.html | 2 +-
docs/zh-Hant/threat-model/index.html | 2 +-
project.yml | 2 +-
site/pages/releases.html | 15 ++++++++++++++-
56 files changed, 127 insertions(+), 89 deletions(-)
diff --git a/docs/404.html b/docs/404.html
index 9f03a54..a4cf886 100644
--- a/docs/404.html
+++ b/docs/404.html
@@ -80,7 +80,7 @@ That page isn't here Try the home page ,
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.11.3 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.12.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/about/index.html b/docs/about/index.html
index d02e8d9..f3d86c1 100644
--- a/docs/about/index.html
+++ b/docs/about/index.html
@@ -138,7 +138,7 @@ Thanks
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.11.3 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.12.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/de/about/index.html b/docs/de/about/index.html
index b247e1f..232501c 100644
--- a/docs/de/about/index.html
+++ b/docs/de/about/index.html
@@ -137,7 +137,7 @@ Dank
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.11.3 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.12.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/docs/index.html b/docs/de/docs/index.html
index 4a83e8e..bdda1dd 100644
--- a/docs/de/docs/index.html
+++ b/docs/de/docs/index.html
@@ -61,7 +61,7 @@
Dokumentation
Flowlight benutzen
-
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.11.3, macOS 15 oder neuer.
+
Alles vom ersten Start bis zum Feinschliff an den Agentenregeln. Flowlight 0.12.0, macOS 15 oder neuer.
@@ -738,7 +738,7 @@ Grenzen
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.11.3 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.12.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/index.html b/docs/de/index.html
index 00ed469..20a5406 100644
--- a/docs/de/index.html
+++ b/docs/de/index.html
@@ -34,7 +34,7 @@
-
+
Zum Inhalt springen
@@ -75,7 +75,7 @@ Sieh, was deine Apps im Netz tun.brew install --cask xinbetween/tap/flowlightKopieren
- v0.11.3 macOS 15+ Universal GPL-3.0 Keine Telemetrie
+ v0.12.0 macOS 15+ Universal GPL-3.0 Keine Telemetrie
@@ -616,7 +616,7 @@ Wisse, was deinen Mac verlässt.
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.11.3 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.12.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/privacy/index.html b/docs/de/privacy/index.html
index 74f936e..c0a785f 100644
--- a/docs/de/privacy/index.html
+++ b/docs/de/privacy/index.html
@@ -173,7 +173,7 @@
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.11.3 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.12.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/de/threat-model/index.html b/docs/de/threat-model/index.html
index 8870cad..760802e 100644
--- a/docs/de/threat-model/index.html
+++ b/docs/de/threat-model/index.html
@@ -213,7 +213,7 @@ Eine Schwachstelle melden
© 2026 Die Flowlight-Mitwirkenden. Flowlight ist freie Software, veröffentlicht unter der GNU General Public License v3.0 .
-
Version 0.11.3 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
+
Version 0.12.0 · Nicht verbunden mit Apple oder einem der hier genannten KI-Anbieter.
diff --git a/docs/docs/index.html b/docs/docs/index.html
index ab39eed..d672fe3 100644
--- a/docs/docs/index.html
+++ b/docs/docs/index.html
@@ -61,7 +61,7 @@
Documentation
Using Flowlight
-
Everything from the first launch to tuning the agent rules. Flowlight 0.11.3, macOS 15 or later.
+
Everything from the first launch to tuning the agent rules. Flowlight 0.12.0, macOS 15 or later.
@@ -732,7 +732,7 @@ Limitations
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.11.3 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.12.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/es/about/index.html b/docs/es/about/index.html
index 081198b..2b82374 100644
--- a/docs/es/about/index.html
+++ b/docs/es/about/index.html
@@ -137,7 +137,7 @@ Agradecimientos
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.11.3 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.12.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/docs/index.html b/docs/es/docs/index.html
index 18ecef2..eb9f599 100644
--- a/docs/es/docs/index.html
+++ b/docs/es/docs/index.html
@@ -61,7 +61,7 @@
Documentación
Usar Flowlight
-
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.11.3, macOS 15 o posterior.
+
Todo, desde el primer arranque hasta el ajuste de las reglas de agentes. Flowlight 0.12.0, macOS 15 o posterior.
@@ -732,7 +732,7 @@ Limitaciones
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.11.3 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.12.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/index.html b/docs/es/index.html
index 192e0ee..b2c7521 100644
--- a/docs/es/index.html
+++ b/docs/es/index.html
@@ -34,7 +34,7 @@
-
+
Ir al contenido
@@ -75,7 +75,7 @@ Observa la actividad de red de tus apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.11.3 macOS 15+ Universal GPL-3.0 Sin telemetría
+ v0.12.0 macOS 15+ Universal GPL-3.0 Sin telemetría
@@ -616,7 +616,7 @@ Ten claro qué sale de tu Mac.
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.11.3 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.12.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/privacy/index.html b/docs/es/privacy/index.html
index a0b7ac6..6b04367 100644
--- a/docs/es/privacy/index.html
+++ b/docs/es/privacy/index.html
@@ -173,7 +173,7 @@
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.11.3 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.12.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/es/threat-model/index.html b/docs/es/threat-model/index.html
index 20009e5..5be27b9 100644
--- a/docs/es/threat-model/index.html
+++ b/docs/es/threat-model/index.html
@@ -208,7 +208,7 @@ Informar de una vulnerabilidad
© 2026 Quienes contribuyen a Flowlight. Flowlight es software libre, publicado bajo la GNU General Public License v3.0 .
-
Versión 0.11.3 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
+
Versión 0.12.0 · Sin relación con Apple ni con ningún proveedor de IA mencionado en este sitio.
diff --git a/docs/fr/about/index.html b/docs/fr/about/index.html
index 9da3a3b..e4819a7 100644
--- a/docs/fr/about/index.html
+++ b/docs/fr/about/index.html
@@ -137,7 +137,7 @@ Remerciements
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.11.3 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.12.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/docs/index.html b/docs/fr/docs/index.html
index cbb95cf..87e1535 100644
--- a/docs/fr/docs/index.html
+++ b/docs/fr/docs/index.html
@@ -61,7 +61,7 @@
Documentation
Utiliser Flowlight
-
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.11.3, macOS 15 ou version ultérieure.
+
Tout, du premier lancement au réglage des règles des agents. Flowlight 0.12.0, macOS 15 ou version ultérieure.
@@ -738,7 +738,7 @@ Limites
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.11.3 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.12.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/index.html b/docs/fr/index.html
index 4727872..641c61e 100644
--- a/docs/fr/index.html
+++ b/docs/fr/index.html
@@ -34,7 +34,7 @@
-
+
Aller au contenu
@@ -75,7 +75,7 @@ Voyez l’activité réseau de vos apps.
brew install --cask xinbetween/tap/flowlightCopier
- v0.11.3 macOS 15+ Universel GPL-3.0 Sans télémétrie
+ v0.12.0 macOS 15+ Universel GPL-3.0 Sans télémétrie
@@ -616,7 +616,7 @@ Sachez ce qui quitte votre Mac.
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.11.3 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.12.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/privacy/index.html b/docs/fr/privacy/index.html
index a8dc5eb..5a7f9b8 100644
--- a/docs/fr/privacy/index.html
+++ b/docs/fr/privacy/index.html
@@ -173,7 +173,7 @@
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.11.3 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.12.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/fr/threat-model/index.html b/docs/fr/threat-model/index.html
index 721f285..e48f9d3 100644
--- a/docs/fr/threat-model/index.html
+++ b/docs/fr/threat-model/index.html
@@ -211,7 +211,7 @@ Signaler une vulnérabilité
© 2026 Les contributeurs de Flowlight. Flowlight est un logiciel libre, publié sous la GNU General Public License v3.0 .
-
Version 0.11.3 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
+
Version 0.12.0 · Sans lien avec Apple ni avec aucun fournisseur d’IA cité sur ce site.
diff --git a/docs/index.html b/docs/index.html
index 694b40a..d151de1 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -34,7 +34,7 @@
-
+
Skip to content
@@ -75,7 +75,7 @@ See your apps' network activity.brew install --cask xinbetween/tap/flowlightCopy
- v0.11.3 macOS 15+ Universal GPL-3.0 No telemetry
+ v0.12.0 macOS 15+ Universal GPL-3.0 No telemetry
@@ -625,7 +625,7 @@ Know what leaves your Mac.
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.11.3 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.12.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/it/about/index.html b/docs/it/about/index.html
index 1711aed..a2aac2d 100644
--- a/docs/it/about/index.html
+++ b/docs/it/about/index.html
@@ -137,7 +137,7 @@ Ringraziamenti
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.11.3 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.12.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/docs/index.html b/docs/it/docs/index.html
index 7084268..a8d3b8e 100644
--- a/docs/it/docs/index.html
+++ b/docs/it/docs/index.html
@@ -61,7 +61,7 @@
Documentazione
Usare Flowlight
-
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.11.3, macOS 15 o successivo.
+
Tutto, dal primo avvio alla messa a punto delle regole per gli agenti. Flowlight 0.12.0, macOS 15 o successivo.
@@ -736,7 +736,7 @@ Limitazioni
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.11.3 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.12.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/index.html b/docs/it/index.html
index bbc1a9f..0cf14e7 100644
--- a/docs/it/index.html
+++ b/docs/it/index.html
@@ -34,7 +34,7 @@
-
+
Vai al contenuto
@@ -75,7 +75,7 @@ Vedi la rete delle tue app.Ca
Metti una stella su GitHub
brew install --cask xinbetween/tap/flowlightCopia
- v0.11.3 macOS 15+ Universale GPL-3.0 Nessuna telemetria
+ v0.12.0 macOS 15+ Universale GPL-3.0 Nessuna telemetria
@@ -616,7 +616,7 @@ Sappi che cosa esce dal tuo Mac.
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.11.3 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.12.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/privacy/index.html b/docs/it/privacy/index.html
index 68b288b..1bcb9bf 100644
--- a/docs/it/privacy/index.html
+++ b/docs/it/privacy/index.html
@@ -173,7 +173,7 @@
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.11.3 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.12.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/it/threat-model/index.html b/docs/it/threat-model/index.html
index f281515..93a01c5 100644
--- a/docs/it/threat-model/index.html
+++ b/docs/it/threat-model/index.html
@@ -209,7 +209,7 @@ Segnalare una vulnerabilità
© 2026 Chi contribuisce a Flowlight. Flowlight è software libero, distribuito sotto la GNU General Public License v3.0 .
-
Versione 0.11.3 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
+
Versione 0.12.0 · Non affiliato ad Apple né ad alcun fornitore di IA citato in questo sito.
diff --git a/docs/ja/about/index.html b/docs/ja/about/index.html
index bdaf933..3c1bef7 100644
--- a/docs/ja/about/index.html
+++ b/docs/ja/about/index.html
@@ -138,7 +138,7 @@ 謝辞
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.11.3 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.12.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/docs/index.html b/docs/ja/docs/index.html
index 0d8570e..28d432c 100644
--- a/docs/ja/docs/index.html
+++ b/docs/ja/docs/index.html
@@ -61,7 +61,7 @@
ドキュメント
Flowlight の使い方
-
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.11.3、macOS 15 以降。
+
初回起動からエージェントのルールの調整まで、すべてここに。Flowlight 0.12.0、macOS 15 以降。
@@ -686,7 +686,7 @@ 制限事項
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.11.3 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.12.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/index.html b/docs/ja/index.html
index 0a1b939..b024b04 100644
--- a/docs/ja/index.html
+++ b/docs/ja/index.html
@@ -34,7 +34,7 @@
-
+
本文へスキップ
@@ -75,7 +75,7 @@ アプリの通信が見える。brew install --cask xinbetween/tap/flowlightコピー
- v0.11.3 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
+ v0.12.0 macOS 15+ ユニバーサル GPL-3.0 テレメトリなし
@@ -615,7 +615,7 @@ Mac から何が出ていくのかを知る。
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.11.3 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.12.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/privacy/index.html b/docs/ja/privacy/index.html
index c040189..00be2b5 100644
--- a/docs/ja/privacy/index.html
+++ b/docs/ja/privacy/index.html
@@ -171,7 +171,7 @@
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.11.3 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.12.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ja/threat-model/index.html b/docs/ja/threat-model/index.html
index bc088df..ac5e780 100644
--- a/docs/ja/threat-model/index.html
+++ b/docs/ja/threat-model/index.html
@@ -210,7 +210,7 @@ 脆弱性を報告する
© 2026 Flowlight コントリビューター。Flowlight は GNU General Public License v3.0 のもとで公開されている自由ソフトウェアです。
-
バージョン 0.11.3 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
+
バージョン 0.12.0 · Apple および本サイトに挙げた AI プロバイダとは関係ありません。
diff --git a/docs/ko/about/index.html b/docs/ko/about/index.html
index 2a31549..aa21453 100644
--- a/docs/ko/about/index.html
+++ b/docs/ko/about/index.html
@@ -138,7 +138,7 @@ 감사
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.11.3 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.12.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/docs/index.html b/docs/ko/docs/index.html
index 54e8797..48e97da 100644
--- a/docs/ko/docs/index.html
+++ b/docs/ko/docs/index.html
@@ -61,7 +61,7 @@
문서
Flowlight 사용하기
-
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.11.3, macOS 15 이상.
+
첫 실행부터 에이전트 규칙 조정까지 전부. Flowlight 0.12.0, macOS 15 이상.
@@ -728,7 +728,7 @@ 한계
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.11.3 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.12.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/index.html b/docs/ko/index.html
index 5de1813..bf59205 100644
--- a/docs/ko/index.html
+++ b/docs/ko/index.html
@@ -34,7 +34,7 @@
-
+
본문으로 이동
@@ -75,7 +75,7 @@ 앱이 무엇을 하는지 봅니다.
brew install --cask xinbetween/tap/flowlight복사
- v0.11.3 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
+ v0.12.0 macOS 15+ 유니버설 GPL-3.0 텔레메트리 없음
@@ -616,7 +616,7 @@ 내 Mac에서 무엇이 나가는지 아세요.
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.11.3 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.12.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/privacy/index.html b/docs/ko/privacy/index.html
index 04ce032..2992c1c 100644
--- a/docs/ko/privacy/index.html
+++ b/docs/ko/privacy/index.html
@@ -174,7 +174,7 @@
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.11.3 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.12.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/ko/threat-model/index.html b/docs/ko/threat-model/index.html
index 8bc22eb..e319b56 100644
--- a/docs/ko/threat-model/index.html
+++ b/docs/ko/threat-model/index.html
@@ -208,7 +208,7 @@ 취약점 신고하기
© 2026 Flowlight 기여자들. Flowlight는 GNU General Public License v3.0 아래 배포되는 자유 소프트웨어입니다.
-
버전 0.11.3 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
+
버전 0.12.0 · Apple 및 이 사이트에 언급된 어떤 AI 제공업체와도 무관합니다.
diff --git a/docs/llms-full.txt b/docs/llms-full.txt
index efcf448..7f3a4f0 100644
--- a/docs/llms-full.txt
+++ b/docs/llms-full.txt
@@ -62,7 +62,7 @@ Documentation
Using Flowlight
- Everything from the first launch to tuning the agent rules. Flowlight 0.11.3, macOS 15 or later.
+ Everything from the first launch to tuning the agent rules. Flowlight 0.12.0, macOS 15 or later.
On this page
@@ -839,7 +839,7 @@ Understand your AI agents.
brew install --cask xinbetween/tap/flowlightCopy
- v0.11.3macOS 15+UniversalGPL-3.0No telemetry
+ v0.12.0macOS 15+UniversalGPL-3.0No telemetry
connectionslive
@@ -1398,9 +1398,21 @@ Releases
Downloads, checksums and full notes for each version are on GitHub Releases.
- 0.11.3
+ 0.12.0
September 30, 2026Latest
+ A refreshed interface, on one design system. Flowlight now draws from a single set of
+ tokens — a brand palette that resolves for light and dark, consistent spacing, and shared surfaces — so
+ every screen reads as one app. Traffic direction, risk and status each have their own colour and never
+ borrow the accent, which is reserved for what asks you to act.
+
+ Clearer at a glance. The AI Agents table shows each agent's share of traffic as an
+ inline bar, risk and allowlist state are bordered pills, the always-monitor tip sits on an accent card,
+ and the sidebar selection takes the brand colour. The numbers that matter are set in a display face.
+
+ 0.11.3
+September 30, 2026
+
A demo run no longer touches your real monitoring. Quitting Flowlight strips the
proxy it wrote into a monitored agent's settings, so the agent works without it — but that strip was
also running when a throwaway demo or screenshot instance quit, which could quietly switch off the
diff --git a/docs/llms.txt b/docs/llms.txt
index d999009..d816b8a 100644
--- a/docs/llms.txt
+++ b/docs/llms.txt
@@ -1,6 +1,6 @@
# Flowlight
-> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.11.3.
+> Free, open-source (GPL-3.0) application-aware network monitor for macOS, with focused visibility into AI agents. It attributes observed TCP and UDP activity to the application that made it and records the destination, protocol and byte counts, keeping local history from second to year. Recognized AI agents are listed by name along with the tools and MCP servers they start, under per-agent allowlists. It can also refuse, once asked: a rule blocks an application, a destination or a URL for as long as you specify, and a guardrail withholds a tool from an agent before its model is offered it. Runs on macOS 15 or later; capture is by a sampler or a Network Extension. Current version: 0.12.0.
- [Download Flowlight.dmg](https://github.com/xinbetween/flowlight/releases/latest/download/Flowlight.dmg)
- [Source code](https://github.com/xinbetween/flowlight)
diff --git a/docs/privacy/index.html b/docs/privacy/index.html
index 8e6a211..d466c59 100644
--- a/docs/privacy/index.html
+++ b/docs/privacy/index.html
@@ -174,7 +174,7 @@
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.11.3 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.12.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/pt-PT/about/index.html b/docs/pt-PT/about/index.html
index 83a1fdd..811e669 100644
--- a/docs/pt-PT/about/index.html
+++ b/docs/pt-PT/about/index.html
@@ -137,7 +137,7 @@ Agradecimentos
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.11.3 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.12.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/docs/index.html b/docs/pt-PT/docs/index.html
index eee96b5..c7ea1a7 100644
--- a/docs/pt-PT/docs/index.html
+++ b/docs/pt-PT/docs/index.html
@@ -61,7 +61,7 @@
Documentação
Usar o Flowlight
-
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.11.3, macOS 15 ou posterior.
+
Tudo, da primeira abertura ao ajuste das regras dos agentes. Flowlight 0.12.0, macOS 15 ou posterior.
@@ -733,7 +733,7 @@ Limitações
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.11.3 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.12.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/index.html b/docs/pt-PT/index.html
index bb364dc..3e7ffb8 100644
--- a/docs/pt-PT/index.html
+++ b/docs/pt-PT/index.html
@@ -34,7 +34,7 @@
-
+
Ir para o conteúdo
@@ -75,7 +75,7 @@ Veja a atividade de rede das suas apps.
brew install --cask xinbetween/tap/flowlightCopiar
- v0.11.3 macOS 15+ Universal GPL-3.0 Sem telemetria
+ v0.12.0 macOS 15+ Universal GPL-3.0 Sem telemetria
@@ -616,7 +616,7 @@ Saiba o que sai do seu Mac.
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.11.3 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.12.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/privacy/index.html b/docs/pt-PT/privacy/index.html
index b4f050f..2b288b5 100644
--- a/docs/pt-PT/privacy/index.html
+++ b/docs/pt-PT/privacy/index.html
@@ -173,7 +173,7 @@
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.11.3 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.12.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/pt-PT/threat-model/index.html b/docs/pt-PT/threat-model/index.html
index 57b7d9d..381a09d 100644
--- a/docs/pt-PT/threat-model/index.html
+++ b/docs/pt-PT/threat-model/index.html
@@ -210,7 +210,7 @@ Comunicar uma vulnerabilidade
© 2026 Quem contribui para o Flowlight. O Flowlight é software livre, publicado sob a GNU General Public License v3.0 .
-
Versão 0.11.3 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
+
Versão 0.12.0 · Sem qualquer ligação à Apple ou a algum fornecedor de IA mencionado neste site.
diff --git a/docs/releases/index.html b/docs/releases/index.html
index 2457451..bac6941 100644
--- a/docs/releases/index.html
+++ b/docs/releases/index.html
@@ -55,7 +55,20 @@ What's new
- 0.11.3 September 30, 2026 Latest
+ 0.12.0 September 30, 2026 Latest
+
+ A refreshed interface, on one design system. Flowlight now draws from a single set of
+ tokens — a brand palette that resolves for light and dark, consistent spacing, and shared surfaces — so
+ every screen reads as one app. Traffic direction, risk and status each have their own colour and never
+ borrow the accent, which is reserved for what asks you to act.
+ Clearer at a glance. The AI Agents table shows each agent's share of traffic as an
+ inline bar, risk and allowlist state are bordered pills, the always-monitor tip sits on an accent card,
+ and the sidebar selection takes the brand colour. The numbers that matter are set in a display face.
+
+
+
+
+
A demo run no longer touches your real monitoring. Quitting Flowlight strips the
proxy it wrote into a monitored agent's settings, so the agent works without it — but that strip was
@@ -1000,7 +1013,7 @@ What's new
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.11.3 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.12.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/threat-model/index.html b/docs/threat-model/index.html
index dc2d01e..e45d453 100644
--- a/docs/threat-model/index.html
+++ b/docs/threat-model/index.html
@@ -209,7 +209,7 @@ Reporting a vulnerability
© 2026 The Flowlight contributors. Flowlight is free software, released under the
GNU General Public License v3.0 .
-
Version 0.11.3 · Not affiliated with Apple or any AI provider named on this site.
+
Version 0.12.0 · Not affiliated with Apple or any AI provider named on this site.
diff --git a/docs/zh-Hans/about/index.html b/docs/zh-Hans/about/index.html
index 71b19a7..0d7d860 100644
--- a/docs/zh-Hans/about/index.html
+++ b/docs/zh-Hans/about/index.html
@@ -137,7 +137,7 @@ 致谢
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.11.3 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.12.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/docs/index.html b/docs/zh-Hans/docs/index.html
index 7322a7e..8ed0020 100644
--- a/docs/zh-Hans/docs/index.html
+++ b/docs/zh-Hans/docs/index.html
@@ -61,7 +61,7 @@
文档
使用 Flowlight
-
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.11.3,macOS 15 或更高版本。
+
从第一次启动到调整代理规则,需要的都在这里。Flowlight 0.12.0,macOS 15 或更高版本。
@@ -689,7 +689,7 @@ 限制
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.11.3 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.12.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/index.html b/docs/zh-Hans/index.html
index 98fd21c..f2f6de4 100644
--- a/docs/zh-Hans/index.html
+++ b/docs/zh-Hans/index.html
@@ -34,7 +34,7 @@
-
+
跳到正文
@@ -75,7 +75,7 @@ 看清应用的网络活动。
brew install --cask xinbetween/tap/flowlight拷贝
- v0.11.3 macOS 15+ 通用架构 GPL-3.0 无遥测
+ v0.12.0 macOS 15+ 通用架构 GPL-3.0 无遥测
@@ -613,7 +613,7 @@ 知道什么离开了你的 Mac。
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.11.3 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.12.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/privacy/index.html b/docs/zh-Hans/privacy/index.html
index f47014b..68436f1 100644
--- a/docs/zh-Hans/privacy/index.html
+++ b/docs/zh-Hans/privacy/index.html
@@ -171,7 +171,7 @@
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.11.3 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.12.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hans/threat-model/index.html b/docs/zh-Hans/threat-model/index.html
index d271794..aab50f5 100644
--- a/docs/zh-Hans/threat-model/index.html
+++ b/docs/zh-Hans/threat-model/index.html
@@ -197,7 +197,7 @@ 报告漏洞
© 2026 Flowlight 贡献者。Flowlight 是自由软件,依 GNU General Public License v3.0 发布。
-
版本 0.11.3 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
+
版本 0.12.0 · 与 Apple 及本站提及的任何 AI 提供方均无关联。
diff --git a/docs/zh-Hant/about/index.html b/docs/zh-Hant/about/index.html
index b49309c..357a694 100644
--- a/docs/zh-Hant/about/index.html
+++ b/docs/zh-Hant/about/index.html
@@ -137,7 +137,7 @@ 致謝
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.11.3 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.12.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/docs/index.html b/docs/zh-Hant/docs/index.html
index b7a55c6..1c425c5 100644
--- a/docs/zh-Hant/docs/index.html
+++ b/docs/zh-Hant/docs/index.html
@@ -61,7 +61,7 @@
說明文件
使用 Flowlight
-
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.11.3,macOS 15 或以上版本。
+
從第一次啟動到調整代理規則,全都在這裡。Flowlight 0.12.0,macOS 15 或以上版本。
@@ -695,7 +695,7 @@ 限制
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.11.3 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.12.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/index.html b/docs/zh-Hant/index.html
index b70be83..b5d07a5 100644
--- a/docs/zh-Hant/index.html
+++ b/docs/zh-Hant/index.html
@@ -34,7 +34,7 @@
-
+
跳至內容
@@ -75,7 +75,7 @@ 看見每個 App 的網路活動。brew install --cask xinbetween/tap/flowlight拷貝
- v0.11.3 macOS 15+ 通用架構 GPL-3.0 無遙測
+ v0.12.0 macOS 15+ 通用架構 GPL-3.0 無遙測
@@ -614,7 +614,7 @@ 知道有什麼離開了你的 Mac。
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.11.3 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.12.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/privacy/index.html b/docs/zh-Hant/privacy/index.html
index 81e718e..a07830e 100644
--- a/docs/zh-Hant/privacy/index.html
+++ b/docs/zh-Hant/privacy/index.html
@@ -171,7 +171,7 @@
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.11.3 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.12.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/docs/zh-Hant/threat-model/index.html b/docs/zh-Hant/threat-model/index.html
index efb1607..51ccfaa 100644
--- a/docs/zh-Hant/threat-model/index.html
+++ b/docs/zh-Hant/threat-model/index.html
@@ -198,7 +198,7 @@ 回報漏洞
© 2026 Flowlight 貢獻者。Flowlight 是自由軟體,依 GNU General Public License v3.0 發布。
-
版本 0.11.3 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
+
版本 0.12.0 · 與 Apple 及本站提及的任何 AI 供應商均無關聯。
diff --git a/project.yml b/project.yml
index 1f31ee2..68c3fb4 100644
--- a/project.yml
+++ b/project.yml
@@ -11,7 +11,7 @@ settings:
DEVELOPMENT_TEAM: ""
CODE_SIGN_STYLE: Automatic
ENABLE_HARDENED_RUNTIME: YES
- MARKETING_VERSION: "0.11.3"
+ MARKETING_VERSION: "0.12.0"
CURRENT_PROJECT_VERSION: "22"
targets:
Flowlight:
diff --git a/site/pages/releases.html b/site/pages/releases.html
index 571e713..13005da 100644
--- a/site/pages/releases.html
+++ b/site/pages/releases.html
@@ -13,7 +13,20 @@ What's new
- 0.11.3 September 30, 2026 Latest
+ 0.12.0 September 30, 2026 Latest
+
+ A refreshed interface, on one design system. Flowlight now draws from a single set of
+ tokens — a brand palette that resolves for light and dark, consistent spacing, and shared surfaces — so
+ every screen reads as one app. Traffic direction, risk and status each have their own colour and never
+ borrow the accent, which is reserved for what asks you to act.
+ Clearer at a glance. The AI Agents table shows each agent's share of traffic as an
+ inline bar, risk and allowlist state are bordered pills, the always-monitor tip sits on an accent card,
+ and the sidebar selection takes the brand colour. The numbers that matter are set in a display face.
+
+
+
+
+
A demo run no longer touches your real monitoring. Quitting Flowlight strips the
proxy it wrote into a monitored agent's settings, so the agent works without it — but that strip was