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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

All notable changes to cmux are documented here.

## [Unreleased]

### Added
- **Aurean appearance**: a golden-ratio (φ) based theme. cmux ships a cohesive dark palette across the terminal canvas and the window chrome, with a palette-temperature picker (Cool / Dune / Warm / Obsidian) in Settings → App that re-skins the app live. Light mode keeps the existing theme. ([#5050](https://github.com/manaflow-ai/cmux/pull/5050))

Comment on lines +5 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing required per-locale changelog localization coverage.

Line 8 adds new user-facing changelog copy, but there’s no corresponding evidence in this PR layer that docs-localized entries were added for every locale supported by web/i18n/routing.ts. This should block merge under the repo i18n policy for changelog updates.

As per coding guidelines: “CHANGELOG.md: Apply .github/review-bot-rules/full-internationalization.md during review. Verify changelog copy rendered on the docs site has matching localized message coverage for every locale listed in web/i18n/routing.ts.”

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 7-7: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@CHANGELOG.md` around lines 5 - 9, The changelog addition in CHANGELOG.md
introduces user-facing copy but lacks per-locale localized entries required by
the repo i18n policy; update the PR to add matching localized changelog messages
for every locale listed in web/i18n/routing.ts (follow the guidance in
.github/review-bot-rules/full-internationalization.md), e.g., add
locale-specific entries or translation keys corresponding to the new “Aurean
appearance” changelog line and ensure the docs rendering pipeline picks them up
so the docs site shows localized changelog entries for each supported locale.

## [0.64.10] - 2026-05-23

### Added
Expand Down
17 changes: 17 additions & 0 deletions Packages/CmuxAppearance/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// swift-tools-version: 6.0
import PackageDescription

let package = Package(
name: "CmuxAppearance",
platforms: [.macOS(.v14)],
products: [
.library(name: "CmuxAppearance", targets: ["CmuxAppearance"])
],
targets: [
.target(name: "CmuxAppearance"),
.testTarget(
name: "CmuxAppearanceTests",
dependencies: ["CmuxAppearance"]
)
]
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import Foundation

/// The semantic color surface every Aurean palette must provide.
///
/// Views consume colors through these eight semantic roles, never through raw hex,
/// so swapping the active palette (cool ⇄ dune ⇄ warm ⇄ obsidian) re-skins the whole
/// app without touching call sites. The four **signal** roles
/// (``accent``/``ok``/``warn``/``crit``) carry meaning that is stable across palettes —
/// only the negative-space temperature and the ``text`` tone shift — which preserves
/// the user's muscle memory.
///
/// Conform a value type to this protocol; ``AureanPalette`` is the canonical
/// implementation. Keep it `Sendable` so palettes can cross actor boundaries.
public protocol AppearancePalette: Sendable {
/// Primary pane background (the design's `surface.primary` / liminal).
var surfacePrimary: AureanColor { get }
/// Raised surfaces — sidebars, headers, tab bars (`surface.off` / liminal-off).
var surfaceOff: AureanColor { get }
/// Deepest chrome — titlebar and status bar (`surface.abyssal`).
var surfaceAbyssal: AureanColor { get }

/// Primary typography and grid lines (the design's "sand").
var text: AureanColor { get }

/// Signal: active selection, highlight, focus ring, search match (liminal-blue).
var accent: AureanColor { get }
/// Signal: running agent, input prompt, success, spend telemetry (flow-green).
var ok: AureanColor { get }
/// Signal: needs-input and dirty state (gold). Fixed across palettes.
var warn: AureanColor { get }
/// Signal: failure, deletions, forbidden actions (rust). Fixed across palettes.
var crit: AureanColor { get }
}

extension AppearancePalette {
/// The ``text`` color projected onto a φ-opacity stop.
///
/// - Parameter stop: The opacity ladder rung (border, faint, dust …).
/// - Returns: Sand at the requested opacity — the standard way to draw rules,
/// secondary labels, and washes without introducing new hues.
public func text(_ stop: AureanOpacity) -> AureanColor {
text.opacity(stop.value)
}

/// The ``accent`` color projected onto a φ-opacity stop (e.g. selection washes).
/// - Parameter stop: The opacity ladder rung.
/// - Returns: Accent at the requested opacity.
public func accent(_ stop: AureanOpacity) -> AureanColor {
accent.opacity(stop.value)
}

/// The ``ok`` color projected onto a φ-opacity stop (e.g. agent-pane phosphor wash).
/// - Parameter stop: The opacity ladder rung.
/// - Returns: Flow-green at the requested opacity.
public func ok(_ stop: AureanOpacity) -> AureanColor {
ok.opacity(stop.value)
}
}
94 changes: 94 additions & 0 deletions Packages/CmuxAppearance/Sources/CmuxAppearance/AureanColor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import SwiftUI

/// A resolution-independent, testable color value used throughout the Aurean theme layer.
///
/// ``AureanColor`` stores straight (non-premultiplied) sRGB components in the `0...1`
/// range plus an alpha channel. It deliberately avoids wrapping `SwiftUI.Color` or
/// `NSColor` as its storage so that the whole theme layer can be unit-tested without
/// launching AppKit or a rendering context — tests assert on ``red``/``green``/``blue``/
/// ``alpha`` directly.
///
/// Convert to platform colors only at the view boundary via ``color`` or ``nsColor``.
///
/// ```swift
/// let sand = AureanColor(hex: "#C4C7CC")
/// Text("hello").foregroundStyle(sand.color)
/// ```
public struct AureanColor: Sendable, Hashable, Codable {
/// The red component, in the sRGB `0...1` range.
public let red: Double
/// The green component, in the sRGB `0...1` range.
public let green: Double
/// The blue component, in the sRGB `0...1` range.
public let blue: Double
/// The opacity, in the `0...1` range, where `1` is fully opaque.
public let alpha: Double

/// Creates a color from straight sRGB components.
///
/// - Parameters:
/// - red: Red component, clamped to `0...1`.
/// - green: Green component, clamped to `0...1`.
/// - blue: Blue component, clamped to `0...1`.
/// - alpha: Opacity, clamped to `0...1`. Defaults to `1` (opaque).
public init(red: Double, green: Double, blue: Double, alpha: Double = 1) {
self.red = red.clamped01
self.green = green.clamped01
self.blue = blue.clamped01
self.alpha = alpha.clamped01
}

/// Creates a color from a `#RRGGBB` or `#RRGGBBAA` hex string.
///
/// The leading `#` is optional and parsing is case-insensitive. An unparseable
/// string yields opaque black, so this initializer never fails — the Aurean tokens
/// are compile-time constants validated by tests, not user input.
///
/// - Parameter hex: A 6- or 8-digit hex string, e.g. `"#161819"` or `"161819FF"`.
public init(hex: String) {
var s = Substring(hex)
if s.first == "#" { s = s.dropFirst() }
let digits = String(s)
func byte(_ start: Int) -> Double {
let lo = digits.index(digits.startIndex, offsetBy: start)
let hi = digits.index(lo, offsetBy: 2)
return Double(UInt8(digits[lo..<hi], radix: 16) ?? 0) / 255.0
}
guard digits.count == 6 || digits.count == 8,
digits.allSatisfy({ $0.isHexDigit }) else {
self.init(red: 0, green: 0, blue: 0, alpha: 1)
return
}
self.init(
red: byte(0),
green: byte(2),
blue: byte(4),
alpha: digits.count == 8 ? byte(6) : 1
)
}

/// Returns a copy of this color with its alpha replaced by `opacity`.
///
/// Used to project a solid token onto one of the Aurean φ-opacity stops
/// (see ``AureanOpacity``) — depth in this theme is opacity, never shadow.
///
/// - Parameter opacity: The new alpha, clamped to `0...1`.
/// - Returns: A color identical in hue but at the requested opacity.
public func opacity(_ opacity: Double) -> AureanColor {
AureanColor(red: red, green: green, blue: blue, alpha: opacity)
}

/// The SwiftUI representation, in the sRGB color space.
public var color: Color {
Color(.sRGB, red: red, green: green, blue: blue, opacity: alpha)
}

/// The AppKit representation, in the sRGB color space.
public var nsColor: NSColor {
NSColor(srgbRed: red, green: green, blue: blue, alpha: alpha)
}
}

private extension Double {
var clamped01: Double { Swift.min(1, Swift.max(0, self)) }
}
113 changes: 113 additions & 0 deletions Packages/CmuxAppearance/Sources/CmuxAppearance/AureanMetrics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import CoreGraphics
import Foundation

/// The dimensionless constants of the Aurean Protocol: the golden ratio, the Fibonacci
/// spacing ladder, the φ type scale, motion timings, and the fixed chrome geometry.
///
/// These are pure declarations transcribed from the design's `tokens.css`. They carry no
/// behavior — only the numbers every surface is measured against. Multiply spacing by a
/// runtime `density` (0.75–1.4) at the call site; the ladder here is at density `1.0`.
public struct AureanMetrics: Sendable {
/// The golden ratio, φ.
public static let phi: CGFloat = 1.618033988749
/// `1/φ` — the major share of a golden split (`0.618`).
public static let phiInverse: CGFloat = 0.618033988749
/// `1/φ²` (`0.382`) — the minor share of a golden split.
public static let phiInverse2: CGFloat = 0.381966011250
/// `1/φ³` (`0.236`).
public static let phiInverse3: CGFloat = 0.236067977499
/// `1/φ⁴` (`0.145`).
public static let phiInverse4: CGFloat = 0.145898033750

/// The Fibonacci spacing ladder in points, at density `1.0`.
public struct Spacing: Sendable {
public static let s1: CGFloat = 1
public static let s2: CGFloat = 2
public static let s3: CGFloat = 3
public static let s5: CGFloat = 5
public static let s8: CGFloat = 8
public static let s13: CGFloat = 13
public static let s21: CGFloat = 21
public static let s34: CGFloat = 34
public static let s55: CGFloat = 55
public static let s89: CGFloat = 89
public static let s144: CGFloat = 144
public static let s233: CGFloat = 233
}
Comment on lines +23 to +36

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Add DocC comments to the Spacing ladder constants.

The Spacing struct is documented, but each public static constant (s1…s233) is undocumented. As per coding guidelines, "Every public symbol in any new Swift package under Packages/ is documented with a Swift-DocC triple-slash comment (///) at the time of writing". A one-line summary per rung (e.g. the corresponding Fibonacci point value/role) satisfies this.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Packages/CmuxAppearance/Sources/CmuxAppearance/AureanMetrics.swift` around
lines 23 - 36, Add Swift-DocC triple-slash comments to each public static
constant in the Spacing struct (Spac­ing.s1 through Spacing.s233) in
AureanMetrics.swift: for every public static let (s1, s2, s3, s5, s8, s13, s21,
s34, s55, s89, s144, s233) add a one-line /// summary documenting the spacing
value or role (e.g., "/// 8pt spacing — small gap" or "/// Fibonacci spacing:
21pt") immediately above the declaration so every public symbol in Spacing is
documented per package guidelines.


/// The φ-anchored type scale (anchor 16px, ratio φ) and its line metrics.
public struct TypeScale: Sendable {
/// The anchor size all others derive from.
public static let anchor: CGFloat = 16
/// `anchor / φ` ≈ 10 — uppercase technical labels.
public static let micro: CGFloat = 10
/// The body size (`anchor`).
public static let base: CGFloat = 16
/// `anchor · φ` ≈ 26.
public static let h3: CGFloat = 26
/// `h3 · φ` ≈ 42.
public static let h2: CGFloat = 42
/// `h2 · φ` ≈ 68.
public static let h1: CGFloat = 68
/// Base line-height multiple (φ).
public static let lineHeight: CGFloat = 1.618
/// Default letter tracking, in em.
public static let tracking: CGFloat = 0.01
/// Wide tracking for uppercase labels, in em.
public static let trackingWide: CGFloat = 0.14
/// The primary mono family, with fallbacks resolved by the platform.
public static let monoFamily = "JetBrains Mono"
/// Ordered fallbacks when the primary face is unavailable.
public static let monoFallbacks = ["Fira Code", "Berkeley Mono", "SF Mono", "Menlo"]
}

/// Fibonacci-derived motion durations (seconds) and φ easing control points.
public struct Motion: Sendable {
/// Instant feedback (`160ms`).
public static let feedback: Double = 0.160
/// Micro interactions — selection, focus ring (`260ms`).
public static let micro: Double = 0.260
/// Panel and split transitions (`420ms`).
public static let panel: Double = 0.420
/// Context and workspace switches (`680ms`).
public static let context: Double = 0.680
/// The status-dot pulse cycle (`2618ms`, looped).
public static let pulse: Double = 2.618
/// Control points for the φ ease curve: `cubic-bezier(0.382, 0, 0.236, 1)`.
public static let ease: (CGFloat, CGFloat, CGFloat, CGFloat) = (0.382, 0, 0.236, 1)
/// Control points for the φ ease-out curve: `cubic-bezier(0.145, 0.618, 0.236, 1)`.
public static let easeOut: (CGFloat, CGFloat, CGFloat, CGFloat) = (0.145, 0.618, 0.236, 1)
}

/// Fixed chrome geometry in points.
public struct Geometry: Sendable {
/// Titlebar height.
public static let titlebarHeight: CGFloat = 38
/// Status bar height.
public static let statusBarHeight: CGFloat = 27
/// Pane header height.
public static let paneHeaderHeight: CGFloat = 28
/// Workspace rail width in the dense Cockpit direction.
public static let railCockpitWidth: CGFloat = 208
/// Workspace rail width in the calm Atelier direction (glyph-only).
public static let railAtelierWidth: CGFloat = 64
/// Right sidebar width in Cockpit.
public static let rightSidebarCockpitWidth: CGFloat = 296
/// Right sidebar width in Atelier.
public static let rightSidebarAtelierWidth: CGFloat = 264
/// Split gutter thickness.
public static let gutter: CGFloat = 1
/// Drag-grab tick length on the gutter.
public static let gutterGrabTick: CGFloat = 34
/// Inner focus-ring thickness (never a glow or shadow).
public static let focusRing: CGFloat = 1
}

/// The golden pane split: there is no 50/50, and equalize (`⌥⌘=`) returns to this.
public struct Split: Sendable {
/// The major pane's fraction (`0.618`).
public static let major: CGFloat = AureanMetrics.phiInverse
/// The minor pane's fraction (`0.382`).
public static let minor: CGFloat = AureanMetrics.phiInverse2
}
}
30 changes: 30 additions & 0 deletions Packages/CmuxAppearance/Sources/CmuxAppearance/AureanOpacity.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import Foundation

/// The φ-derived opacity ladder that encodes depth in the Aurean theme.
///
/// Depth is expressed **only** through opacity — never drop shadows or bevels (the
/// single exception is the window's own near-black cast shadow, which lives outside
/// this theme). Each stop is `1/φⁿ`, matching the design tokens.
///
/// ```swift
/// let border = palette.text.opacity(AureanOpacity.border.value) // sand @ 0.236
/// ```
public enum AureanOpacity: Double, Sendable, CaseIterable {
/// Fully opaque (`1.0`) — solid fills.
case solid = 1.0
/// `1/φ` (`0.618`) — organic primary detail.
case organic = 0.618
/// `1/φ²` (`0.382`) — secondary content.
case secondary = 0.382
/// `1/φ³` (`0.236`) — standard rules and borders.
case border = 0.236
/// `1/φ⁴` (`0.145`) — faint trails and wash gradients.
case faint = 0.145
/// `0.090` — telemetry "dust" and ghost rules.
case dust = 0.090
/// `0.045` — the faintest pane-header wash.
case whisper = 0.045

/// The raw opacity value in the `0...1` range.
public var value: Double { rawValue }
}
65 changes: 65 additions & 0 deletions Packages/CmuxAppearance/Sources/CmuxAppearance/AureanPalette.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import Foundation

/// The canonical ``AppearancePalette`` implementation, carrying the exact token values
/// for each ``AureanPaletteVariant``.
///
/// Hex values are transcribed verbatim from the design's `tokens.css` (the source of
/// truth). The `warn` (gold `#E5C07B`) and `crit` (rust `#FF8A66`) signals are identical
/// across every variant by design; only negative space, sand, and the cool signals shift.
///
/// ```swift
/// let palette = AureanPalette(variant: .cool) // cmux default
/// view.background(palette.surfacePrimary.color)
/// ```
public struct AureanPalette: AppearancePalette, Sendable, Hashable {
public let surfacePrimary: AureanColor
public let surfaceOff: AureanColor
public let surfaceAbyssal: AureanColor
public let text: AureanColor
public let accent: AureanColor
public let ok: AureanColor
public let warn: AureanColor
public let crit: AureanColor
Comment on lines +15 to +22

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Public stored properties are missing DocC comments.

surfacePrimary…crit are public symbols without /// comments (only variant is documented). Although they satisfy AppearancePalette requirements, the package documentation rule applies at the declaration site. As per coding guidelines, "Every public symbol in any new Swift package under Packages/ is documented with a Swift-DocC triple-slash comment (///) at the time of writing".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Packages/CmuxAppearance/Sources/CmuxAppearance/AureanPalette.swift` around
lines 15 - 22, The public stored properties surfacePrimary, surfaceOff,
surfaceAbyssal, text, accent, ok, warn, and crit on AureanPalette are missing
Swift-DocC triple-slash comments; add concise /// comments above each public
property in AureanPalette.swift (similar to the existing documentation for
variant) that describe the role or intended use of each AureanColor (e.g.,
primary surface, secondary/off surface, abyssal/background, primary text, accent
color, success/ok, warning, critical/error) so every public symbol is documented
per package guidelines.


/// The temperature this palette was built from.
public let variant: AureanPaletteVariant

/// Builds the concrete palette for a temperature.
/// - Parameter variant: The Aurean temperature; defaults to ``AureanPaletteVariant/cool``.
public init(variant: AureanPaletteVariant = .cool) {
self.variant = variant
// Signals shared across all palettes (muscle-memory invariants).
self.warn = AureanColor(hex: "#E5C07B") // gold
self.crit = AureanColor(hex: "#FF8A66") // rust
switch variant {
case .cool:
surfacePrimary = AureanColor(hex: "#161819")
surfaceOff = AureanColor(hex: "#121314")
surfaceAbyssal = AureanColor(hex: "#0E1011")
text = AureanColor(hex: "#C4C7CC")
accent = AureanColor(hex: "#B8D8E8")
ok = AureanColor(hex: "#B6D4B0")
case .dune:
surfacePrimary = AureanColor(hex: "#1A1817")
surfaceOff = AureanColor(hex: "#141312")
surfaceAbyssal = AureanColor(hex: "#100E0D")
text = AureanColor(hex: "#D2C0B0")
accent = AureanColor(hex: "#AEE2EA")
ok = AureanColor(hex: "#C5E1A5")
case .warm:
surfacePrimary = AureanColor(hex: "#1E1A15")
surfaceOff = AureanColor(hex: "#18140F")
surfaceAbyssal = AureanColor(hex: "#140F0A")
text = AureanColor(hex: "#E0CAB2")
accent = AureanColor(hex: "#B8DDDC")
ok = AureanColor(hex: "#D0DFA5")
case .obsidian:
surfacePrimary = AureanColor(hex: "#15171A")
surfaceOff = AureanColor(hex: "#101214")
surfaceAbyssal = AureanColor(hex: "#0B0D0F")
text = AureanColor(hex: "#C9D0D4")
accent = AureanColor(hex: "#A3C9D6")
ok = AureanColor(hex: "#B6D8B8")
}
}
}
Loading