diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ChromeEmphasis.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ChromeEmphasis.swift index 546e6112ce63..0019738057ad 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ChromeEmphasis.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/ChromeEmphasis.swift @@ -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 diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/FocusIndicatorTunables.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/FocusIndicatorTunables.swift index 9afb54466433..d363a1f2f3ed 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/FocusIndicatorTunables.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextDesign/FocusIndicatorTunables.swift @@ -12,8 +12,8 @@ public nonisolated enum FocusIndicatorTunables { default: .fade, code: "FocusIndicatorTunables.inactiveTabStyle") public static let inactiveTabStrength = Tunable.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] diff --git a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/LayoutStyle.swift b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/LayoutStyle.swift index a7c7e8ca7994..dc84e3b01176 100644 --- a/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/LayoutStyle.swift +++ b/Packages/macOS/CmuxNext/Sources/CmuxNextLayout/Model/LayoutStyle.swift @@ -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 diff --git a/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/FocusedPaneTabCueTests.swift b/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/FocusedPaneTabCueTests.swift new file mode 100644 index 000000000000..3100a8775804 --- /dev/null +++ b/Packages/macOS/CmuxNext/Tests/CmuxNextDesignTests/FocusedPaneTabCueTests.swift @@ -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) + } +} diff --git a/plans/cmux-next/focus.md b/plans/cmux-next/focus.md index 331d3d14accd..8524a1356251 100644 --- a/plans/cmux-next/focus.md +++ b/plans/cmux-next/focus.md @@ -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 diff --git a/plans/cmux-next/spec-proposals/visuals/components/tabs.md b/plans/cmux-next/spec-proposals/visuals/components/tabs.md index 502ca8b86e40..743b6f69ed37 100644 --- a/plans/cmux-next/spec-proposals/visuals/components/tabs.md +++ b/plans/cmux-next/spec-proposals/visuals/components/tabs.md @@ -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) diff --git a/plans/cmux-next/spec-proposals/visuals/design-tokens.json b/plans/cmux-next/spec-proposals/visuals/design-tokens.json index a563b99b265f..57f37131b3e3 100644 --- a/plans/cmux-next/spec-proposals/visuals/design-tokens.json +++ b/plans/cmux-next/spec-proposals/visuals/design-tokens.json @@ -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)" }, @@ -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, diff --git a/plans/cmux-next/spec-proposals/visuals/design-tokens.md b/plans/cmux-next/spec-proposals/visuals/design-tokens.md index e126d289af9a..859532d10563 100644 --- a/plans/cmux-next/spec-proposals/visuals/design-tokens.md +++ b/plans/cmux-next/spec-proposals/visuals/design-tokens.md @@ -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 |