Skip to content

desktop: make sidebar filter transparent with circular hover - #2850

Merged
janicduplessis merged 2 commits into
mainfrom
codex/sidebar-filter-hover
Oct 20, 2026
Merged

janicduplessis merged 2 commits into
mainfrom
codex/sidebar-filter-hover

Conversation

@NicooleT

@NicooleT NicooleT commented Oct 7, 2026 •

Copy link
Copy Markdown
Contributor

Description

The filter control beside the sidebar logo has a persistent native background and a rectangular frame. It should be transparent at rest and have a circular light-grey background on hover.

Solution

Use a plain button with a 28-point square frame, a circle filled with 15% grey while hovered, and the existing icon tint. Preserve the active-filter indicator, accessibility label, tooltip, and popover.

Fixes #2846.

Test plan

  • Passed desktop swift build -Xswiftc -warnings-as-errors, Swift format, ASCII and diff checks.

  • Passed root format, lint, build, typecheck and runtime-floor checks.

  • Built and launched the merged branch (on current main) with stim macos from apps/desktop. Resting state is transparent; the options popover opens from the button. The hover capture comes from a local build with isHovering forced to true, since the cursor cannot be moved without taking over the screen; the committed code is unchanged.

    Rest Hover
  • Root tests: 7,726 passed, 87 failed, 20 skipped across 275 files; failures are in CLI process/native-workflow tests. No CLI source changed.

  • Desktop tests: the full suite had one NodeLauncherTests.recoversTheSameLauncherAfterTheInitialNodeProbeFails failure with Unavailable(); it passed when rerun alone after the parallel load finished.

  • Knip passed when the generated Swift .build folder was temporarily moved out of the source scan and then restored. The initial scan reported markdownlint from the generated sentry-cocoa dependency.

  • Reran the 14 failing CLI test files with two workers: 13 files passed; packages/server/tests/server.test.ts still had 18 failures (1,674 tests passed). No server source changed.

  • Fresh independent review of the issue, diff, tests and guidance found no actionable code findings.

No behavioral tests were added for this appearance-only change.

Nicoole and others added 2 commits October 7, 2026 17:32
…ilter-hover

# Conflicts:
#	apps/desktop/Sources/StimDesktop/Views/ViewOptionsMenu.swift
@janicduplessis

Copy link
Copy Markdown
Collaborator

Fresh independent review of b7cd87df4cd027070250e766f22f0e4dd3746ec6 against main:

No actionable findings. Two non-blocking notes and one geometry note below.

Verified:

  • Conflict resolution is correct. isPresented stays a @Binding and the popover still uses $isPresented, so the tutorial path (showsViewOptions = true for .hideWorkspaces / .statusFilter, Sidebar.swift line 76) still opens it. isHovering is a separate @State.
    struct ViewOptionsButton: View {
    var projects: [Project]
    var counts: [StatusFilter: Int]
    var title: (Project) -> String
    @Binding var isPresented: Bool
    @State private var isHovering = false
    let prefs = SidebarPreferences()
    var body: some View {
    let differs = prefs.options.differsFromDefaults(projects: projects)
    Button {
    isPresented.toggle()
    } label: {
    Image(systemName: "slider.horizontal.3")
    .iconFont(IconSize.regular, weight: .medium)
    .frame(width: 28, height: 28)
    .overlay(alignment: .topTrailing) {
    if differs {
    Circle().fill(Palette.primary).frame(width: 6, height: 6).offset(x: -2, y: 2)
    }
    }
    }
    .buttonStyle(.plain)
    .foregroundStyle(differs ? Palette.primary : Palette.secondary)
    .background {
    Circle().fill(isHovering ? Color.gray.opacity(0.15) : Color.clear)
    }
    .contentShape(Circle())
    .onHover { isHovering = $0 }
    .tutorialAnchor(.archivedFilter)
    .accessibilityLabel("View options")
    .help(differs ? "View options (filtered)" : "View options")
    .popover(isPresented: $isPresented, arrowEdge: .bottom) {
    ViewOptionsMenu(projects: projects, counts: counts, title: title)
    }
    }
  • Preserved: icon, the 6pt filter indicator (differs), .tutorialAnchor(.archivedFilter), .accessibilityLabel("View options"), the filtered/unfiltered .help text, and the popover. The tint still switches on differs.
  • Qualifies as a polish change under CLAUDE.md: one file in apps/desktop, styling only. No command, data, decoding, navigation, dependency, native module or config change. The PR has rest and hover screenshots on the only affected platform (macOS); the hover one is a forced-state build, which the PR body says.
  • The diff matches issue desktop: make sidebar filter button transparent with circular hover #2846 (transparent rest, 28 by 28 circular light-grey hover, existing icon/indicator/tooltip/label/popover).

Notes (non-blocking):

  1. P3, note. The pressed/active state of nativeIconStyle is gone. Before, nativeIconStyle(..., active: isPresented) kept the button visibly active while the popover was open (glass tint on macOS 26, hoverHighlight(selected: active) on the fallback). Now the fill depends only on isHovering. Once the pointer moves into the popover, or when the tutorial opens the popover with no hover, the button looks idle. A plain button also has no pressed feedback. The issue lists what to preserve and does not name this state, so I am not blocking on it. If the maintainer wants it kept, .fill(isHovering || isPresented ? ... : .clear) is the smallest change.

    .buttonStyle(.plain)
    .foregroundStyle(differs ? Palette.primary : Palette.secondary)
    .background {
    Circle().fill(isHovering ? Color.gray.opacity(0.15) : Color.clear)
    }
    .contentShape(Circle())
    .onHover { isHovering = $0 }

  2. P3, note. Color.gray.opacity(0.15) is the only use of .gray in apps/desktop/Sources. The repo does have a shared hover token: .buttonStyle(.hoverRow(radius: Radius.round, selected: isPresented)) (Design/Hover.swift, already used with Radius.round for the CPU resource chip in RootView.swift near line 1044). It fills Palette.accent at Opacity.subtle, animates over 0.1s, adds a pressed tint, shows Palette.selection while selected, and so would also cover note 1. I am not calling this actionable because desktop: make sidebar filter button transparent with circular hover #2846 explicitly asks for a "light-grey" hover and says the maintainer approved that styling, and the accent tint is purple, not grey. Raising it so the maintainer can choose between the issue's grey and consistency with the other hover backgrounds.

    /// A plain button that shows the shared hover tint: the style for rows, chips and text links. The label carries
    /// its own padding so the tint covers it; `outset` grows the tint past a tight label without changing layout.
    /// A selected button shows `Palette.selection` and no hover; a disabled one shows no hover.
    struct HoverRowStyle: ButtonStyle {
    var radius: CGFloat = Radius.chip
    var outset: CGFloat = 0
    var selected = false
    func makeBody(configuration: Configuration) -> some View {
    HoverRowBody(configuration: configuration, radius: radius, outset: outset, selected: selected)
    }
    }
    extension ButtonStyle where Self == HoverRowStyle {
    static func hoverRow(radius: CGFloat = Radius.chip, outset: CGFloat = 0, selected: Bool = false) -> HoverRowStyle {
    HoverRowStyle(radius: radius, outset: outset, selected: selected)
    }
    }
    private struct HoverRowBody: View {
    var configuration: ButtonStyleConfiguration
    var radius: CGFloat
    var outset: CGFloat
    var selected: Bool
    @Environment(\.isEnabled) private var isEnabled
    @State private var hovering = false
    var body: some View {
    configuration.label
    .modifier(
    HoverLayers(
    radius: radius, outset: outset, hovering: hovering && isEnabled,
    pressed: configuration.isPressed && isEnabled, selected: selected)
    )
    .contentShape(Rectangle())
    .opacity(isEnabled ? 1 : Opacity.disabled)
    .onHover { hovering = $0 }
    }
    }

  3. P3, geometry, computed and not observed. The indicator dot sits at .topTrailing with offset(x: -2, y: 2) on the 28 by 28 frame, so its center is about 12.7pt from the circle's center with a 3pt radius. The 14pt hover circle's edge is exceeded by about 1.7pt, so the dot may touch or slightly overhang the circle's edge while hovered in the filtered state. The PR's hover screenshot is the unfiltered state. Worth a look at the filtered hover; no change needed if it looks fine.

Not verified (no build run in this review): that the outer .contentShape(Circle()) makes the transparent corners of the plain button's label un-hittable or hittable as intended, and keyboard focus ring appearance. Neither is expected to change from the previous behavior in a way the issue cares about.

@janicduplessis
janicduplessis marked this pull request as ready for review October 20, 2026 05:05
@janicduplessis
janicduplessis added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit c132bb1 Oct 20, 2026
4 checks passed
@janicduplessis
janicduplessis deleted the codex/sidebar-filter-hover branch October 20, 2026 05:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

desktop: make sidebar filter button transparent with circular hover

2 participants