Skip to content

Commit 103c1cb

Browse files
committed
Enh(viewer): Left/right buttons
1 parent c004bf2 commit 103c1cb

8 files changed

Lines changed: 438 additions & 40 deletions

File tree

.cursor/rules/viewer.mdc

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,14 +261,37 @@ The application has two main pages:
261261

262262
### Keyboard Shortcuts
263263

264+
#### Navigation Shortcuts
265+
* `h` or `←` (Left Arrow): Navigate to previous game in the folder structure
266+
* `l` or `→` (Right Arrow): Navigate to next game in the folder structure
264267
* `p` (lowercase): Open game picker in same tab (from viewer)
265268
* `P` (uppercase): Open game picker in new tab (from viewer)
269+
270+
#### Interface Shortcuts
271+
* `?`: Show keyboard shortcuts help modal (available on both viewer and picker pages)
266272
* `t` (lowercase): Toggle floating table of contents
267273
* `Ctrl/Cmd + D`: Toggle dark/light mode
268274
* `Ctrl/Cmd + E`: Expand all sections
269275
* `Ctrl/Cmd + Shift + E`: Collapse all sections
270276
* `Escape`: Close all open sections
271277

278+
### Keyboard Shortcuts Help Modal
279+
280+
Both the viewer and picker pages include a help modal accessible via the `?` button or `?` keyboard shortcut:
281+
282+
#### Modal Features
283+
* **Trigger**: `?` button in header or `?` keyboard shortcut
284+
* **Content**: Displays all available keyboard shortcuts organized by category
285+
* **Styling**: Bootstrap modal with consistent dark theme styling
286+
* **Dismissal**: Click outside modal, press Escape, or click close button
287+
* **Responsive**: Adapts to mobile screens appropriately
288+
289+
#### Modal Content Structure
290+
* **Navigation Shortcuts**: Game navigation (h/←, l/→, p/P)
291+
* **Interface Shortcuts**: UI controls (?/t, Ctrl+E combinations, Escape)
292+
* **Mouse Shortcuts**: Click behaviors and multi-click actions
293+
* **Page-Specific Shortcuts**: Different content for viewer vs picker pages
294+
272295
### Floating Navigation
273296

274297
* **Floating Table of Contents**: A floating navigation panel that provides quick navigation (always visible by default)
@@ -296,6 +319,12 @@ The application has two main pages:
296319

297320
## Game Picker Design (`/picker`)
298321

322+
### Header
323+
324+
* **Help button**: `?` button that shows keyboard shortcuts modal (same functionality as viewer page)
325+
* **Title**: "Pick a Game" with game controller icon
326+
* **Description**: Brief explanation of the picker functionality
327+
299328
### Features
300329

301330
* **Hierarchical Tree Structure**: Shows all folders recursively with proper indentation
@@ -341,10 +370,18 @@ The application has two main pages:
341370

342371
### Header
343372

344-
* **"Pick Game" button**: Navigate to picker (supports middle-click for new tab)
373+
#### Navigation Controls
374+
* **Previous/Next Game Buttons**: Navigate between games in alphabetical order within the folder structure
375+
- **Previous button**: Shows `h` and `←` keyboard shortcut indicators, disabled when no previous game available
376+
- **Next button**: Shows `l` and `→` keyboard shortcut indicators, disabled when no next game available
377+
- **Button styling**: Uses Bootstrap secondary button styling, similar to Pick Game button
378+
* **"Pick Game" button**: Navigate to picker (supports middle-click for new tab) - positioned in the center between navigation buttons
345379
- Shows `p` keyboard shortcut indicator
346380
- Tooltip explains middle-click functionality
347381
* **"↗" button**: Always open picker in new tab
382+
* **Help button**: `?` button that shows keyboard shortcuts modal (available on both viewer and picker pages)
383+
384+
#### Other Header Elements
348385
* **Dark/light mode toggle button**
349386
* **Current folder path display** with copy functionality, move/rename button, and delete button
350387
- **Move/Rename button**: 📁 icon that opens a dialog for moving or renaming the experiment folder (exact same functionality as in the game picker)
@@ -568,6 +605,25 @@ Both the game picker and trajectory viewer include move/rename functionality for
568605
- After successful move/rename in the trajectory viewer, the user is automatically redirected to the new folder path
569606
- The move/rename button appears in the folder path section alongside copy and delete buttons
570607

608+
## Navigation Implementation Details
609+
610+
### Game Navigation Logic
611+
* **Navigation Order**: Games are sorted alphabetically by their folder path within the logs directory
612+
* **Backend Function**: `get_navigation_info(selected_folder)` determines previous/next games
613+
* **Button States**: Navigation buttons are automatically disabled when no previous/next game is available
614+
* **URL Generation**: Supports both dynamic Flask routes (`/?folder=`) and static frozen routes (`/game/`)
615+
616+
### Keyboard Shortcut Implementation
617+
* **Event Handling**: Single consolidated keyboard event listener to prevent conflicts
618+
* **Input Field Detection**: Shortcuts are disabled when typing in input fields or textareas
619+
* **Modal Priority**: Dialog shortcuts (Escape/Enter) take precedence when modals are open
620+
* **Arrow Key Support**: Both letter keys (h/l) and arrow keys (←/→) work for navigation
621+
622+
### Button Layout
623+
* **Header Organization**: Navigation buttons (Previous, Pick Game, Next, Help) in logical left-to-right order
624+
* **Consistent Styling**: All buttons use Bootstrap classes with consistent kbd shortcut indicators
625+
* **Responsive Design**: Button layout adapts appropriately on mobile devices
626+
571627
### Important
572628

573629
* Always use the player name from the metadata.json file, do not assume players are called p1, p2, etc.

codeclash/viewer/app.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -670,6 +670,29 @@ def unescape_content(value):
670670
return value.replace("\\n", "\n")
671671

672672

673+
def get_navigation_info(selected_folder: str) -> dict[str, str | None]:
674+
"""Get previous and next game folders for navigation"""
675+
# Get all game folders
676+
game_folders = find_all_game_folders(LOG_BASE_DIR)
677+
678+
# Filter to only actual game folders and sort them
679+
game_names = [folder["name"] for folder in game_folders if folder["is_game"]]
680+
game_names.sort()
681+
682+
# Find current game index
683+
try:
684+
current_index = game_names.index(selected_folder)
685+
except ValueError:
686+
# Current folder not found in the list
687+
return {"previous": None, "next": None}
688+
689+
# Determine previous and next
690+
previous_game = game_names[current_index - 1] if current_index > 0 else None
691+
next_game = game_names[current_index + 1] if current_index < len(game_names) - 1 else None
692+
693+
return {"previous": previous_game, "next": next_game}
694+
695+
673696
def render_game_viewer(folder_path: Path, selected_folder: str) -> str:
674697
"""Common logic for rendering game viewer pages"""
675698
# Parse the selected game
@@ -690,6 +713,9 @@ def render_game_viewer(folder_path: Path, selected_folder: str) -> str:
690713
sim_wins_data = parser.analyze_sim_wins_per_round()
691714
matrix_data = parser.load_matrix_analysis()
692715

716+
# Get navigation info
717+
navigation_info = get_navigation_info(selected_folder)
718+
693719
return render_template(
694720
"index.html",
695721
selected_folder=selected_folder,
@@ -699,6 +725,7 @@ def render_game_viewer(folder_path: Path, selected_folder: str) -> str:
699725
analysis_data=analysis_data,
700726
sim_wins_data=sim_wins_data,
701727
matrix_data=matrix_data,
728+
navigation=navigation_info,
702729
is_static=STATIC_MODE,
703730
)
704731

codeclash/viewer/static/css/picker.css

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,21 @@
88
}
99

1010
.picker-header {
11-
text-align: center;
11+
display: flex;
12+
justify-content: space-between;
13+
align-items: center;
1214
margin-bottom: 3rem;
1315
}
1416

17+
.picker-title-section {
18+
text-align: left;
19+
}
20+
21+
.picker-controls {
22+
display: flex;
23+
align-items: center;
24+
}
25+
1526
.picker-header h1 {
1627
color: var(--accent-color);
1728
font-size: 2rem;

codeclash/viewer/static/css/style.css

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,27 @@ body {
116116
min-width: 120px;
117117
}
118118

119+
/* Navigation buttons */
120+
.nav-button {
121+
min-width: 60px;
122+
}
123+
124+
.nav-button kbd {
125+
background-color: rgba(255, 255, 255, 0.2);
126+
border: 1px solid rgba(255, 255, 255, 0.3);
127+
border-radius: 0.25rem;
128+
padding: 0.125rem 0.25rem;
129+
font-size: 0.75rem;
130+
color: #ffffff;
131+
font-weight: 600;
132+
}
133+
134+
.nav-button:disabled kbd {
135+
background-color: rgba(255, 255, 255, 0.1);
136+
border: 1px solid rgba(255, 255, 255, 0.2);
137+
color: #888888;
138+
}
139+
119140
/* Let Bootstrap handle hover states */
120141

121142
.pick-game-button kbd {
@@ -143,6 +164,91 @@ body {
143164
min-width: 40px;
144165
}
145166

167+
/* Help button */
168+
.help-button {
169+
min-width: 60px;
170+
}
171+
172+
.help-button kbd {
173+
background-color: rgba(255, 255, 255, 0.2);
174+
border: 1px solid rgba(255, 255, 255, 0.3);
175+
border-radius: 0.25rem;
176+
padding: 0.125rem 0.25rem;
177+
font-size: 0.75rem;
178+
color: #ffffff;
179+
font-weight: 600;
180+
}
181+
182+
/* Help modal styling */
183+
.shortcut-section {
184+
margin-bottom: 1.5rem;
185+
}
186+
187+
.shortcut-section h6 {
188+
color: var(--accent-color);
189+
font-weight: 600;
190+
margin-bottom: 0.75rem;
191+
font-size: 1rem;
192+
}
193+
194+
.shortcut-item {
195+
display: flex;
196+
align-items: center;
197+
justify-content: space-between;
198+
padding: 0.5rem 0;
199+
border-bottom: 1px solid var(--border-color);
200+
}
201+
202+
.shortcut-item:last-child {
203+
border-bottom: none;
204+
}
205+
206+
.shortcut-item kbd {
207+
background-color: var(--bg-tertiary);
208+
border: 1px solid var(--border-color);
209+
border-radius: 0.25rem;
210+
padding: 0.25rem 0.5rem;
211+
font-size: 0.75rem;
212+
color: var(--accent-color);
213+
font-weight: 600;
214+
margin-right: 0.5rem;
215+
}
216+
217+
.shortcut-item .mouse-action {
218+
background-color: var(--bg-tertiary);
219+
border: 1px solid var(--border-color);
220+
border-radius: 0.25rem;
221+
padding: 0.25rem 0.5rem;
222+
font-size: 0.75rem;
223+
color: var(--text-secondary);
224+
font-weight: 500;
225+
margin-right: 0.5rem;
226+
}
227+
228+
.shortcut-item span:last-child {
229+
flex: 1;
230+
text-align: left;
231+
color: var(--text-primary) !important;
232+
}
233+
234+
/* Bootstrap modal overrides for dark theme */
235+
.modal-content {
236+
background-color: var(--bg-secondary) !important;
237+
border: 1px solid var(--border-color) !important;
238+
}
239+
240+
.modal-header {
241+
border-bottom: 1px solid var(--border-color) !important;
242+
}
243+
244+
.modal-title {
245+
color: var(--text-primary) !important;
246+
}
247+
248+
.modal-body {
249+
color: var(--text-primary) !important;
250+
}
251+
146252
/* Let Bootstrap handle hover states */
147253

148254
/* Bootstrap handles most styling for theme toggle */

0 commit comments

Comments
 (0)