Skip to content

Commit 2d07e45

Browse files
graycreateclaude
andcommitted
refactor: redesign filter menu with Reddit-style dropdown
Changes: - Change title display: Show "V2EX" for default "all" tab, show tab name for other selections - Redesign menu as left-aligned dropdown (like Reddit) instead of centered modal - Add icons for each tab category (house, flame, briefcase, etc.) - Replace grid layout with vertical list layout - Add checkmark indicator for selected item - Improve visual hierarchy with better spacing and typography - Add subtle shadow and spring animation for smoother transitions UI improvements: - Menu now anchors to top-left below title - Width: 200pt, max height: 450pt - Each item shows icon + label + checkmark (when selected) - Selected items have blue accent color with light background - Better touch targets with full-width clickable areas 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 80aefac commit 2d07e45

File tree

2 files changed

+80
-48
lines changed

2 files changed

+80
-48
lines changed

V2er/View/Feed/FilterMenuView.swift

Lines changed: 65 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ struct FilterMenuView: View {
2222
]
2323

2424
var body: some View {
25-
ZStack {
25+
ZStack(alignment: .topLeading) {
2626
if isShowing {
2727
// Background overlay
2828
Color.black.opacity(0.3)
@@ -32,12 +32,12 @@ struct FilterMenuView: View {
3232
}
3333
.transition(.opacity)
3434

35-
// Menu content
36-
VStack(spacing: 0) {
35+
// Menu content - positioned at top left
36+
VStack(alignment: .leading, spacing: 0) {
3737
ScrollView {
38-
LazyVGrid(columns: columns, spacing: 12) {
38+
VStack(alignment: .leading, spacing: 4) {
3939
ForEach(Tab.allTabs, id: \.self) { tab in
40-
TabFilterButton(
40+
TabFilterMenuItem(
4141
tab: tab,
4242
isSelected: tab == selectedTab,
4343
needsLogin: tab.needsLogin() && !AccountState.hasSignIn()
@@ -50,43 +50,77 @@ struct FilterMenuView: View {
5050
}
5151
}
5252
}
53-
.padding()
53+
.padding(.vertical, 8)
5454
}
55+
.frame(width: 200)
5556
.background(Color.itemBg)
5657
.cornerRadius(12)
57-
.padding()
58-
.frame(maxHeight: 400)
58+
.shadow(color: Color.black.opacity(0.15), radius: 8, x: 0, y: 4)
59+
.frame(maxHeight: 450)
60+
.padding(.top, topSafeAreaInset().top + 50)
61+
.padding(.leading, 16)
5962
}
60-
.transition(.move(edge: .top).combined(with: .opacity))
63+
.transition(.asymmetric(
64+
insertion: .move(edge: .top).combined(with: .opacity),
65+
removal: .move(edge: .top).combined(with: .opacity)
66+
))
6167
}
6268
}
63-
.animation(.easeInOut(duration: 0.25), value: isShowing)
69+
.animation(.spring(response: 0.3, dampingFraction: 0.8), value: isShowing)
6470
}
6571
}
6672

67-
struct TabFilterButton: View {
73+
struct TabFilterMenuItem: View {
6874
let tab: Tab
6975
let isSelected: Bool
7076
let needsLogin: Bool
7177
let action: () -> Void
7278

7379
var body: some View {
7480
Button(action: action) {
75-
Text(tab.displayName())
76-
.font(.system(size: 14))
77-
.foregroundColor(textColor)
78-
.frame(maxWidth: .infinity)
79-
.padding(.vertical, 12)
80-
.background(backgroundColor)
81-
.cornerRadius(8)
82-
.overlay(
83-
RoundedRectangle(cornerRadius: 8)
84-
.stroke(borderColor, lineWidth: isSelected ? 1.5 : 0)
85-
)
81+
HStack(spacing: 12) {
82+
Image(systemName: iconName)
83+
.font(.system(size: 16))
84+
.foregroundColor(iconColor)
85+
.frame(width: 24)
86+
87+
Text(tab.displayName())
88+
.font(.system(size: 15, weight: isSelected ? .semibold : .regular))
89+
.foregroundColor(textColor)
90+
91+
Spacer()
92+
93+
if isSelected {
94+
Image(systemName: "checkmark")
95+
.font(.system(size: 14, weight: .semibold))
96+
.foregroundColor(iconColor)
97+
}
98+
}
99+
.padding(.horizontal, 16)
100+
.padding(.vertical, 12)
101+
.background(backgroundColor)
86102
}
87103
.opacity(needsLogin ? 0.5 : 1.0)
88104
}
89105

106+
private var iconName: String {
107+
switch tab {
108+
case .all: return "house.fill"
109+
case .tech: return "desktopcomputer"
110+
case .creative: return "lightbulb.fill"
111+
case .play: return "gamecontroller.fill"
112+
case .apple: return "apple.logo"
113+
case .jobs: return "briefcase.fill"
114+
case .deals: return "cart.fill"
115+
case .city: return "building.2.fill"
116+
case .qna: return "questionmark.circle.fill"
117+
case .hot: return "flame.fill"
118+
case .r2: return "arrow.clockwise"
119+
case .nodes: return "square.grid.3x3.fill"
120+
case .members: return "person.2.fill"
121+
}
122+
}
123+
90124
private var textColor: Color {
91125
if isSelected {
92126
return Color.dynamic(light: .hex(0x2E7EF3), dark: .hex(0x5E9EFF))
@@ -95,16 +129,20 @@ struct TabFilterButton: View {
95129
}
96130
}
97131

98-
private var backgroundColor: Color {
132+
private var iconColor: Color {
99133
if isSelected {
100-
return Color.dynamic(light: .hex(0xE8F2FF), dark: .hex(0x1A3A52))
134+
return Color.dynamic(light: .hex(0x2E7EF3), dark: .hex(0x5E9EFF))
101135
} else {
102-
return Color.dynamic(light: .hex(0xF5F5F5), dark: .hex(0x2C2C2E))
136+
return Color.dynamic(light: .hex(0x666666), dark: .hex(0x999999))
103137
}
104138
}
105139

106-
private var borderColor: Color {
107-
return Color.dynamic(light: .hex(0x2E7EF3), dark: .hex(0x5E9EFF))
140+
private var backgroundColor: Color {
141+
if isSelected {
142+
return Color.dynamic(light: .hex(0xF0F7FF), dark: .hex(0x1A2533))
143+
} else {
144+
return Color.clear
145+
}
108146
}
109147
}
110148

V2er/View/Widget/TopBar.swift

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ struct TopBar: View {
1919
private var title: String {
2020
switch selectedTab {
2121
case .feed:
22-
return store.appState.feedState.selectedTab.displayName()
22+
let selectedTab = store.appState.feedState.selectedTab
23+
return selectedTab == .all ? "V2EX" : selectedTab.displayName()
2324
case .explore:
2425
return "发现"
2526
case .message:
@@ -33,26 +34,7 @@ struct TopBar: View {
3334

3435
var body: some View {
3536
VStack(spacing: 0) {
36-
ZStack {
37-
HStack {
38-
Image(systemName: "square.grid.2x2")
39-
.foregroundColor(.primary)
40-
.font(.system(size: 22))
41-
.padding(6)
42-
.forceClickable()
43-
.hide()
44-
// .to { TestView() }
45-
Spacer()
46-
Image(systemName: "magnifyingglass")
47-
.foregroundColor(.primary)
48-
.font(.system(size: 22))
49-
.padding(6)
50-
.forceClickable()
51-
.to { SearchPage() }
52-
}
53-
.padding(.horizontal, 10)
54-
.padding(.vertical, 8)
55-
37+
HStack {
5638
if isHomePage {
5739
HStack(spacing: 4) {
5840
Text(title)
@@ -71,8 +53,20 @@ struct TopBar: View {
7153
.font(.headline)
7254
.foregroundColor(.primary)
7355
.fontWeight(.bold)
56+
.padding(.leading, 10)
7457
}
58+
59+
Spacer()
60+
61+
Image(systemName: "magnifyingglass")
62+
.foregroundColor(.primary)
63+
.font(.system(size: 22))
64+
.padding(6)
65+
.forceClickable()
66+
.to { SearchPage() }
7567
}
68+
.padding(.horizontal, 10)
69+
.padding(.vertical, 8)
7670
.padding(.top, topSafeAreaInset().top)
7771
.background(VEBlur())
7872

0 commit comments

Comments
 (0)