From fddccff4ee92043ca9e53862a422a2e7726bdc54 Mon Sep 17 00:00:00 2001 From: Kaede Akino Date: Wed, 23 Sep 2026 12:19:32 +0800 Subject: [PATCH] Refine Sidebar glass and selection states --- TFM/Design/DesignTokens.swift | 19 ++-- TFM/Features/Assistant/AssistantView.swift | 103 ++++++++++--------- TFM/Features/Sidebar/WindowSidebarView.swift | 85 ++++++--------- TFM/UI/Components/SidebarRow.swift | 2 +- 4 files changed, 100 insertions(+), 109 deletions(-) diff --git a/TFM/Design/DesignTokens.swift b/TFM/Design/DesignTokens.swift index e448dad0..c671e737 100644 --- a/TFM/Design/DesignTokens.swift +++ b/TFM/Design/DesignTokens.swift @@ -481,9 +481,9 @@ struct LegacySidebarMaterialBackground: NSViewRepresentable { } } -/// A native-feeling emphasized selection: the key window uses an accent fill -/// with reversed content, while an inactive window keeps the same destination -/// legible with a quiet accent wash and accent-colored content. +/// A native selection: the key window uses the system selection color with +/// reversed content; an inactive window uses the system's neutral selection +/// background and unemphasized text color. /// /// The row's context menu is drawn here rather than by the caller, and it is a /// native menu rather than SwiftUI's: `contextMenu` derives its source rectangle @@ -554,9 +554,7 @@ struct TermindSidebarRowBackground: ViewModifier { private var fillColor: Color { if isSelected { - controlActiveState == .key - ? TermindColor.accent - : TermindColor.accent.opacity(0.14) + SidebarSelectionStyle.background(controlActiveState: controlActiveState) } else { Highlight.fill(isSelected: false, isHovering: isHovering) } @@ -564,6 +562,13 @@ struct TermindSidebarRowBackground: ViewModifier { } enum SidebarSelectionStyle { + static func background(controlActiveState: ControlActiveState) -> Color { + Color( + nsColor: controlActiveState == .key + ? .selectedContentBackgroundColor : .unemphasizedSelectedContentBackgroundColor + ) + } + static func foreground( isSelected: Bool, controlActiveState: ControlActiveState, @@ -572,7 +577,7 @@ enum SidebarSelectionStyle { guard isSelected else { return unselected } return controlActiveState == .key ? AnyShapeStyle(TermindColor.selectedForeground) - : AnyShapeStyle(.tint) + : AnyShapeStyle(Color(nsColor: .unemphasizedSelectedTextColor)) } } diff --git a/TFM/Features/Assistant/AssistantView.swift b/TFM/Features/Assistant/AssistantView.swift index 2b449510..9ef5f383 100644 --- a/TFM/Features/Assistant/AssistantView.swift +++ b/TFM/Features/Assistant/AssistantView.swift @@ -57,7 +57,7 @@ enum AssistantConversationMenuSurface { /// every mode of the column ends at the same line. Its two controls are shaped /// by what they are: New Chat is a capsule because it is a labelled action that /// takes the width, and Archived is a circle because it is one glyph that -/// toggles — the same distinction the mode rail at the top of the column makes. +/// toggles. Both share neutral glass, including the archive's selected state. struct AssistantSidebarView: View { @Environment(WindowModel.self) private var window @@ -71,54 +71,63 @@ struct AssistantSidebarView: View { onOpen: { window.selectAssistant($0) } ) - HStack(spacing: Metrics.Space.s) { - Button { - window.openNewAssistantChat() - } label: { - Label("New Chat", systemImage: "plus.bubble") - .frame( - maxWidth: .infinity, - maxHeight: .infinity, - alignment: .center - ) - .contentShape(.capsule) - } - .buttonStyle(.plain) - .termindGlassCapsule(isInteractive: true) + footer + .frame(height: Metrics.Sidebar.searchFieldHeight) + .padding(.horizontal, Metrics.Space.m) + .padding(.vertical, Metrics.Space.s) + } + } - Button { - window.showAssistantArchive() - } label: { - Image(systemName: "archivebox") - // Filled and reversed while it is the destination on - // screen, the same way a selected row states itself. - // An accent-on-glass glyph reads as "tinted", not as - // "this is where you are". - .foregroundStyle( - window.isAssistantArchivePresented - ? AnyShapeStyle(TermindColor.selectedForeground) - : AnyShapeStyle(.primary) - ) - .frame( - width: Metrics.Sidebar.searchFieldHeight, - height: Metrics.Sidebar.searchFieldHeight - ) - .background { - if window.isAssistantArchivePresented { - Circle().fill(Color.accentColor) - } - } - .contentShape(.circle) - } - .buttonStyle(.plain) - .termindGlassCircle(isInteractive: true) - .help(Text("Archived")) - .accessibilityLabel(Text("Archived")) - .accessibilityAddTraits(window.isAssistantArchivePresented ? .isSelected : []) + @ViewBuilder + private var footer: some View { + if #available(macOS 26.0, *) { + GlassEffectContainer(spacing: Metrics.Space.s) { + footerButtons } - .frame(height: Metrics.Sidebar.searchFieldHeight) - .padding(.horizontal, Metrics.Space.m) - .padding(.vertical, Metrics.Space.s) + } else { + footerButtons + } + } + + private var footerButtons: some View { + HStack(spacing: Metrics.Space.s) { + Button { + window.openNewAssistantChat() + } label: { + Label("New Chat", systemImage: "plus.bubble") + .foregroundStyle(.primary) + .frame( + maxWidth: .infinity, + maxHeight: .infinity, + alignment: .center + ) + .contentShape(.capsule) + } + .buttonStyle(.plain) + .termindGlassCapsule(isInteractive: true) + + Button { + window.showAssistantArchive() + } label: { + Image(systemName: "archivebox") + .foregroundStyle(.primary) + .fontWeight(window.isAssistantArchivePresented ? .semibold : .regular) + .frame( + width: Metrics.Sidebar.searchFieldHeight, + height: Metrics.Sidebar.searchFieldHeight + ) + .background { + if window.isAssistantArchivePresented { + Circle().fill(Color.primary.opacity(0.10)) + } + } + .contentShape(.circle) + } + .buttonStyle(.plain) + .termindGlassCircle(isInteractive: true) + .help(Text("Archived")) + .accessibilityLabel(Text("Archived")) + .accessibilityAddTraits(window.isAssistantArchivePresented ? .isSelected : []) } } } diff --git a/TFM/Features/Sidebar/WindowSidebarView.swift b/TFM/Features/Sidebar/WindowSidebarView.swift index c39c7e18..d436a9c5 100644 --- a/TFM/Features/Sidebar/WindowSidebarView.swift +++ b/TFM/Features/Sidebar/WindowSidebarView.swift @@ -7,13 +7,15 @@ import TermindPresentation import struct TermindDomain.Host -/// The window's one left-hand column. A glass mode rail switches the Navigator -/// and three Workspace tools in place without the square selection segments of -/// `NSSegmentedControl`. +/// The window's one left-hand column. Its mode buttons share one glass capsule +/// with a neutral selection highlight, keeping the symbols above the material. struct WindowSidebarView: View { @Environment(WindowModel.self) private var window @Environment(\.accessibilityReduceMotion) private var reduceMotion + @Environment(\.controlActiveState) private var controlActiveState + @FocusState private var focusedMode: WindowSidebarState.Mode? + @Namespace private var modeSelectionNamespace var body: some View { VStack(spacing: 0) { @@ -33,44 +35,20 @@ struct WindowSidebarView: View { private var focusedPaneID: Pane.ID? { window.selectedWorkspace?.focusedPaneID } - @ViewBuilder private var modeSelector: some View { - Group { - if #available(macOS 26.0, *) { - GlassEffectContainer(spacing: Metrics.Space.xs) { - modeButtons(usesSystemGlass: true) - } - .padding(Metrics.Space.xs) - .glassEffect(.regular, in: .capsule) - } else { - modeButtons(usesSystemGlass: false) - .padding(Metrics.Space.xs) - .termindGlassCapsule() + HStack(spacing: Metrics.Space.xs) { + ForEach(WindowSidebarState.Mode.allCases) { mode in + modeButton(mode) } } + .padding(Metrics.Space.xs) + .termindGlassCapsule() .animation(reduceMotion ? nil : Motion.panel, value: window.sidebarMode) .accessibilityElement(children: .contain) .accessibilityLabel(Text("Sidebar")) } - private func modeButtons(usesSystemGlass: Bool) -> some View { - HStack(spacing: Metrics.Space.xs) { - ForEach(Array(WindowSidebarState.Mode.allCases.enumerated()), id: \.element.id) { - index, mode in - if index > 0 { - Divider() - .frame(height: 18) - .opacity(0.55) - } - modeButton(mode, usesSystemGlass: usesSystemGlass) - } - } - } - - private func modeButton( - _ mode: WindowSidebarState.Mode, - usesSystemGlass: Bool - ) -> some View { + private func modeButton(_ mode: WindowSidebarState.Mode) -> some View { let isSelected = window.sidebarMode == mode let isEnabled = window.isSidebarModeEnabled(mode) @@ -78,23 +56,27 @@ struct WindowSidebarView: View { guard isEnabled else { return } window.activateSidebarMode(mode) } label: { - Group { - if #available(macOS 26.0, *), usesSystemGlass, isSelected { - modeIcon(mode, isSelected: true, isEnabled: isEnabled) - .background(Circle().fill(Color.accentColor)) - .glassEffect(.clear.interactive(), in: .circle) - } else { - modeIcon(mode, isSelected: isSelected, isEnabled: isEnabled) - .background { - if isSelected { - Circle().fill(Color.accentColor) - } - } + modeIcon(mode, isSelected: isSelected, isEnabled: isEnabled) + .frame(maxWidth: .infinity) + .background { + if isSelected { + Capsule() + .fill(Color.primary.opacity(0.10)) + .matchedGeometryEffect(id: "selection", in: modeSelectionNamespace) + } } - } - .frame(maxWidth: .infinity) + .contentShape(.capsule) } .buttonStyle(.plain) + .focused($focusedMode, equals: mode) + .focusEffectDisabled() + .overlay { + if focusedMode == mode && controlActiveState == .key { + Capsule() + .strokeBorder(Color.primary.opacity(0.35), lineWidth: 2) + .allowsHitTesting(false) + } + } .disabled(!isEnabled) .help(Text(mode.label)) .accessibilityLabel(Text(mode.label)) @@ -107,14 +89,9 @@ struct WindowSidebarView: View { isEnabled: Bool ) -> some View { Image(systemName: mode.symbol) - .font(.system(size: 15, weight: .medium)) - .foregroundStyle( - isSelected - ? AnyShapeStyle(TermindColor.selectedForeground) - : isEnabled ? AnyShapeStyle(.primary) : AnyShapeStyle(.tertiary) - ) + .font(.system(size: 15, weight: isSelected ? .semibold : .medium)) + .foregroundStyle(isEnabled ? AnyShapeStyle(.primary) : AnyShapeStyle(.tertiary)) .frame(width: 36, height: 36) - .contentShape(.circle) } @ViewBuilder diff --git a/TFM/UI/Components/SidebarRow.swift b/TFM/UI/Components/SidebarRow.swift index 273b05b6..3edb142a 100644 --- a/TFM/UI/Components/SidebarRow.swift +++ b/TFM/UI/Components/SidebarRow.swift @@ -65,7 +65,7 @@ struct SidebarInlineRenameField: NSViewRepresentable { guard isSelected else { return .labelColor } return controlActiveState == .key ? .alternateSelectedControlTextColor - : .controlAccentColor + : .unemphasizedSelectedTextColor } private var placeholderColor: NSColor {