Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ extension ThemeTokens {
t.textSecondary = fade(textSecondary, s, floor: Self.subtleSecondaryFloor)
t.textTertiary = fade(textTertiary, s, floor: Self.subtleTertiaryFloor)
t.selectionFill = selectionFill.withAlpha(selectionFill.alpha * (1 - s))
t.hoverFill = hoverFill.withAlpha(hoverFill.alpha * (1 - s))
// Hover fades half as much: hovering a tab is how a person picks that pane next.
t.hoverFill = hoverFill.withAlpha(hoverFill.alpha * (1 - s * 0.5))
case .tonal:
t.textPrimary = textSecondary
t.textSecondary = textTertiary
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ public nonisolated enum FocusIndicatorTunables {
default: .fade, code: "FocusIndicatorTunables.inactiveTabStyle")
public static let inactiveTabStrength = Tunable<CGFloat>.number(
"focus.inactiveTabStrength", .focus, "Unfocused tabs strength",
help: "How much subtler an unfocused pane's tabs draw (0 is the same as the focused pane).",
default: 0.35, range: 0...1, step: 0.05, unit: .fraction, code: "FocusIndicatorTunables.inactiveTabStrength")
help: "How much subtler an unfocused pane's tabs draw (0 is the same as the focused pane). 0.6 since 2026-10-21 (was 0.35): with no ring by default, the tabs are the cue for which pane Cmd-W closes.",
default: 0.6, range: 0...1, step: 0.05, unit: .fraction, code: "FocusIndicatorTunables.inactiveTabStrength")

public static var all: [TunableDescriptor] {
[indicator.descriptor, inactiveTabStyle.descriptor, inactiveTabStrength.descriptor]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ public nonisolated struct LayoutStyle: Hashable, Sendable {
/// subtler when it marks tabs (`ChromeEmphasis.forPane`).
public var focusIndicator: FocusIndicator = .tabs
public var inactiveTabStyle: InactiveTabStyle = .fade
public var inactiveTabStrength: CGFloat = 0.35
public var inactiveTabStrength: CGFloat = 0.6
/// Fraction of a pane's extent that counts as an edge drop zone
/// (DESIGN.md (c)2: a quarter, half of the half a split gives).
public var dropEdgeFraction: CGFloat = 0.25
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import CmuxNextDesign
import CmuxTheme
import Testing

/// Leo, 2026-10-21: a link opened in a split beside his chat, and Cmd-W closed the chat, because
/// nothing showed which pane had focus. With the default `appearance.focusIndicator` (tabs, no
/// ring) the only cue is the other panes' subtler tabs, so they must read clearly as "not this
/// one": the selected tab's pill at no more than half its focused fill, while every tab label
/// stays readable.
@Suite struct FocusedPaneTabCueTests {
static let themes: [ThemeTokens] = [
ThemeTokens.derive(from: ThemeInput(background: ThemeRGB(hex: 0x1E1E1E), foreground: ThemeRGB(hex: 0xD4D4D4))),
ThemeTokens.derive(from: ThemeInput(background: ThemeRGB(hex: 0x101018), foreground: ThemeRGB(hex: 0xE8E8F0))),
ThemeTokens.derive(from: ThemeInput(background: ThemeRGB(hex: 0xFFFFFF), foreground: ThemeRGB(hex: 0x24292F))),
ThemeTokens.derive(from: ThemeInput(background: ThemeRGB(hex: 0xF5F5F0), foreground: ThemeRGB(hex: 0x333333))),
]

/// The emphasis an unfocused pane's tabs get by default.
static var unfocused: ChromeEmphasis {
ChromeEmphasis.forPane(isFocused: false, paneCount: 2, indicator: .tabs,
style: FocusIndicatorTunables.inactiveTabStyle.defaultValue,
strength: FocusIndicatorTunables.inactiveTabStrength.defaultValue)
}

@Test func anUnfocusedPanesSelectedTabIsClearlyNotTheFocusedOne() {
for tokens in Self.themes {
let subtle = tokens.emphasized(Self.unfocused)
#expect(subtle.selectionFill.alpha <= tokens.selectionFill.alpha * 0.5 + 0.0001,
"the unfocused pill is at most half the focused one (\(subtle.selectionFill.alpha) of \(tokens.selectionFill.alpha))")
let page = tokens.contentBackground.withAlpha(1)
#expect(subtle.textPrimary.contrast(with: page) < tokens.textPrimary.contrast(with: page) * 0.6,
"the unfocused selected label is plainly fainter")
}
}

@Test func unfocusedTabLabelsStayReadable() {
for tokens in Self.themes {
let subtle = tokens.emphasized(Self.unfocused)
let page = tokens.contentBackground.withAlpha(1)
#expect(subtle.textPrimary.contrast(with: page) >= min(ThemeTokens.subtlePrimaryFloor, tokens.textPrimary.contrast(with: page)) - 0.05)
#expect(subtle.textSecondary.contrast(with: page) >= min(ThemeTokens.subtleSecondaryFloor, tokens.textSecondary.contrast(with: page)) - 0.05)
}
}

/// The focused pane and a lone pane keep full strength.
@Test func theFocusedPaneKeepsFullStrength() {
#expect(ChromeEmphasis.forPane(isFocused: true, paneCount: 2, indicator: .tabs, style: .fade, strength: 0.6) == .full)
#expect(ChromeEmphasis.forPane(isFocused: false, paneCount: 1, indicator: .tabs, style: .fade, strength: 0.6) == .full)
}
}
2 changes: 1 addition & 1 deletion plans/cmux-next/focus.md
Original file line number Diff line number Diff line change
Expand Up @@ -658,7 +658,7 @@ the user."):
settings; `focus.inactiveTabStyle` overrides the cmux.json key of the same name (`fade` default: text, icons
and pills fade toward the background; `tonal`: every text tier steps down, the selected
pill takes the hover fill; `quiet`: no pill, the selection is the text tier);
`focus.inactiveTabStrength` (0.35). Subtle text never drops below 3.5:1 (selected),
`focus.inactiveTabStrength` (0.6; 0.35 until 2026-10-21, when Leo could not tell which pane Cmd-W would close). Subtle text never drops below 3.5:1 (selected),
2.5:1 and 2:1 against the page (`ThemeTokens.subtle*Floor`). Panels and hover cards
opened from a subtle strip adopt `ThemeScope.fullStrength`, so they draw at full strength.
- With `appearance.borders` none the unfocused panes' dim stands in for the ring only when
Expand Down
10 changes: 5 additions & 5 deletions plans/cmux-next/spec-proposals/visuals/components/tabs.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,15 +68,15 @@ Pressed screenshots are UNVERIFIED: a synthetic mouse-down enters AppKit's track

## Unfocused pane tabs (focus.inactiveTabStyle)

Applies when `appearance.focusIndicator` is `tabs` or `both`, the screen has more than one pane, and the pane is not focused (`CmuxNextDesign/ChromeEmphasis.swift:30-77 (ChromeEmphasis.forPane, ThemeTokens.emphasized)`). Style: cmux.json `focus.inactiveTabStyle`, `fade` (default), `tonal` or `quiet`; Settings > Appearance > Unfocused Pane Tabs; the Debug tunable of the same name overrides it. Strength 0.35 (Debug tunable `focus.inactiveTabStrength`). Contrast floors: primary 3.5, secondary 2.5, tertiary 2.0. Fade step: `fade` starts at the strength and steps the mix fraction down by 0.02 until the color holds min(floor, its own contrast) against the page; tonal and quiet fade the tertiary tier by strength / 2.
Applies when `appearance.focusIndicator` is `tabs` or `both`, the screen has more than one pane, and the pane is not focused (`CmuxNextDesign/ChromeEmphasis.swift:30-77 (ChromeEmphasis.forPane, ThemeTokens.emphasized)`). Style: cmux.json `focus.inactiveTabStyle`, `fade` (default), `tonal` or `quiet`; Settings > Appearance > Unfocused Pane Tabs; the Debug tunable of the same name overrides it. Strength 0.6 (Debug tunable `focus.inactiveTabStrength`; 0.35 until 2026-10-21). Contrast floors: primary 3.5, secondary 2.5, tertiary 2.0. Fade step: `fade` starts at the strength and steps the mix fraction down by 0.02 until the color holds min(floor, its own contrast) against the page; tonal and quiet fade the tertiary tier by strength / 2.

| style | textPrimary | textSecondary | selectionFill | hoverFill |
|---|---|---|---|---|
| fade (default) | mixed toward the page by 0.35, floor 3.5 | mixed, floor 2.5 | alpha × 0.65 | alpha × 0.65 |
| tonal | ← textSecondary | ← textTertiary | hoverFill alpha × 0.825 | unchanged |
| quiet | ← textSecondary | textTertiary faded by 0.175 | transparent | unchanged |
| fade (default) | mixed toward the page by 0.6, floor 3.5 | mixed, floor 2.5 | alpha × 0.4 | alpha × 0.7 (half the strength) |
| tonal | ← textSecondary | ← textTertiary | hoverFill alpha × 0.7 | unchanged |
| quiet | ← textSecondary | textTertiary faded by 0.3 | transparent | unchanged |

Resolved for Apple System dark: fade primary `#B0B0B0`, selection `#FFFFFF11`; tonal selection `#FFFFFF0D`; quiet selection none. All themes: `components["tabs.inactivePaneEmphasis"].resolved` in the JSON. Screenshot check (dark): selected tab in the unfocused pane is `#2D2D2D` (fade), `#292929` (tonal), `#1E1E1E` (quiet).
Resolved at the earlier strength 0.35, Apple System dark: fade primary `#B0B0B0`, selection `#FFFFFF11`; tonal selection `#FFFFFF0D`; quiet selection none. All themes: `components["tabs.inactivePaneEmphasis"].resolved` in the JSON. Screenshot check (dark): selected tab in the unfocused pane is `#2D2D2D` (fade), `#292929` (tonal), `#1E1E1E` (quiet).

![Unfocused pane tabs: fade](../images/tabs/dark-inactive-pane-fade.png)
![Unfocused pane tabs: tonal](../images/tabs/dark-inactive-pane-tonal.png)
Expand Down
6 changes: 3 additions & 3 deletions plans/cmux-next/spec-proposals/visuals/design-tokens.json
Original file line number Diff line number Diff line change
Expand Up @@ -2581,8 +2581,8 @@
"quiet"
],
"default": "fade",
"strength": 0.35,
"cmuxJson": "focus.inactiveTabStyle (Settings > Appearance > Unfocused Pane Tabs); the Debug Settings tunable of the same name overrides it. focus.inactiveTabStrength (0.35) stays a Debug tunable."
"strength": 0.6,
"cmuxJson": "focus.inactiveTabStyle (Settings > Appearance > Unfocused Pane Tabs); the Debug Settings tunable of the same name overrides it. focus.inactiveTabStrength (0.6) stays a Debug tunable."
},
"source": "Packages/macOS/CmuxNext/Sources/CmuxNextSettings/PaneFocusSettings.swift (PaneFocusSettings.inactiveTabStylePath); Packages/macOS/CmuxNext/Sources/CmuxNextDesign/DesignSettings.swift (DesignSettings.inactiveTabStyle, effectiveInactiveTabStyle); Packages/macOS/CmuxNext/Sources/CmuxNextDesign/FocusIndicatorTunables.swift:9-16 (FocusIndicatorTunables.inactiveTabStyle)"
},
Expand Down Expand Up @@ -3216,7 +3216,7 @@
"Packages/macOS/CmuxNext/Sources/CmuxNextDesign/FocusIndicatorTunables.swift:9-16 (FocusIndicatorTunables.inactiveTabStyle)"
],
"metrics": {
"strength": 0.35,
"strength": 0.6,
"floors": {
"primary": 3.5,
"secondary": 2.5,
Expand Down
2 changes: 1 addition & 1 deletion plans/cmux-next/spec-proposals/visuals/design-tokens.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ Reduce Motion: no movement springs and no loops; fades capped at 0.1 s; the atte
| appearance.tabBarBackground | retired | ignored: every surface draws the one window background |
| focusRing.contrast | subtle 0.20, standard 0.55, strong 0.85 | ring alpha |
| focusRing.{enabled,style,color,width,cornerRadius,showWhenSinglePane} | true, ring, nil, 1, nil, false | |
| focus.inactiveTabStyle (the Debug tunable of the same name overrides it) | fade, tonal, quiet; strength 0.35 (Debug tunable) | see tabs page (`CmuxNextSettings/PaneFocusSettings.swift (PaneFocusSettings.inactiveTabStylePath)`) |
| focus.inactiveTabStyle (the Debug tunable of the same name overrides it) | fade, tonal, quiet; strength 0.6 (Debug tunable) | see tabs page (`CmuxNextSettings/PaneFocusSettings.swift (PaneFocusSettings.inactiveTabStylePath)`) |
| sidebar.sectionLook (Debug tunable `sidebar.sections.look` overrides it) | quiet, card, tray, lines, linesIcons | see sidebar page |
| sidebar.topBandMaxShare / bottomBandMaxShare / pinnedBandsScroll | 1/3, 0.25 (range 0.1-0.9), true | share of the sidebar height a pinned band takes before it scrolls inside; false: bands never scroll and the list shrinks (three rows minimum) |
| notifications.attention.* | style blink, width 2, blinkCount 2, duration 3 s, persist true | pane attention ring. Blink: blinkCount blinks over 0.5 s each (1.0 s for 2), then holds. duration applies to the pulse style only |
Expand Down