Skip to content

Commit 8cbabe3

Browse files
feat(ui): create dedicated UI modules
Extract UI-related functionality into separate files: - Create components.py for reusable UI elements - Create board_view.py for board rendering - Create pages.py for page definitions and handlers - Create styling.py for style-related functions 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 04a9ff4 commit 8cbabe3

File tree

5 files changed

+297
-0
lines changed

5 files changed

+297
-0
lines changed

src/ui/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# UI components package initialization

src/ui/board_view.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Board UI construction
2+
from nicegui import ui
3+
import logging
4+
from typing import Dict, Callable, List, Tuple, Any
5+
6+
from src.config.constants import (
7+
FREE_SPACE_TEXT, FREE_SPACE_TEXT_COLOR,
8+
TILE_CLICKED_BG_COLOR, TILE_CLICKED_TEXT_COLOR,
9+
TILE_UNCLICKED_BG_COLOR, TILE_UNCLICKED_TEXT_COLOR
10+
)
11+
from src.config.styles import (
12+
GRID_CONTAINER_CLASS, GRID_CLASSES, CARD_CLASSES,
13+
LABEL_CLASSES, LABEL_SMALL_CLASSES
14+
)
15+
from src.utils.text_processing import split_phrase_into_lines
16+
from src.ui.styling import get_line_style_for_lines
17+
from src.utils.javascript import run_fitty_js
18+
19+
def build_board(parent, board: List[List[str]], clicked_tiles: set, tile_buttons_dict: dict, on_tile_click: Callable):
20+
"""Build the Bingo board UI."""
21+
with parent:
22+
with ui.element("div").classes(GRID_CONTAINER_CLASS):
23+
with ui.grid(columns=5).classes(GRID_CLASSES):
24+
for row_idx, row in enumerate(board):
25+
for col_idx, phrase in enumerate(row):
26+
card = ui.card().classes(CARD_CLASSES).style("cursor: pointer;")
27+
labels_list = [] # initialize list for storing label metadata
28+
29+
with card:
30+
with ui.column().classes("flex flex-col items-center justify-center gap-0 w-full"):
31+
default_text_color = FREE_SPACE_TEXT_COLOR if phrase.upper() == FREE_SPACE_TEXT else TILE_UNCLICKED_TEXT_COLOR
32+
lines = split_phrase_into_lines(phrase)
33+
line_count = len(lines)
34+
35+
for line in lines:
36+
with ui.row().classes("w-full items-center justify-center"):
37+
base_class = LABEL_SMALL_CLASSES if len(line) <= 3 else LABEL_CLASSES
38+
lbl = ui.label(line).classes(base_class).style(
39+
get_line_style_for_lines(line_count, default_text_color)
40+
)
41+
labels_list.append({
42+
"ref": lbl,
43+
"base_classes": base_class,
44+
"base_style": get_line_style_for_lines(line_count, default_text_color)
45+
})
46+
47+
tile_buttons_dict[(row_idx, col_idx)] = {"card": card, "labels": labels_list}
48+
49+
if phrase.upper() == FREE_SPACE_TEXT:
50+
clicked_tiles.add((row_idx, col_idx))
51+
card.style(f"color: {FREE_SPACE_TEXT_COLOR}; border: none; outline: 3px solid {TILE_CLICKED_TEXT_COLOR};")
52+
else:
53+
card.on("click", lambda e, r=row_idx, c=col_idx: on_tile_click(r, c))
54+
55+
return tile_buttons_dict
56+
57+
def update_tile_styles(board: List[List[str]], clicked_tiles: set, tile_buttons_dict: dict):
58+
"""Update styles for each tile based on clicked state."""
59+
for (r, c), tile in tile_buttons_dict.items():
60+
phrase = board[r][c]
61+
62+
if (r, c) in clicked_tiles:
63+
new_card_style = f"background-color: {TILE_CLICKED_BG_COLOR}; color: {TILE_CLICKED_TEXT_COLOR}; border: none; outline: 3px solid {TILE_CLICKED_TEXT_COLOR};"
64+
new_label_color = TILE_CLICKED_TEXT_COLOR
65+
else:
66+
new_card_style = f"background-color: {TILE_UNCLICKED_BG_COLOR}; color: {TILE_UNCLICKED_TEXT_COLOR}; border: none;"
67+
new_label_color = TILE_UNCLICKED_TEXT_COLOR
68+
69+
# Update the card style
70+
tile["card"].style(new_card_style)
71+
tile["card"].update()
72+
73+
# Recalculate the styles for labels
74+
lines = split_phrase_into_lines(phrase)
75+
line_count = len(lines)
76+
new_label_style = get_line_style_for_lines(line_count, new_label_color)
77+
78+
# Update all label elements for this tile
79+
for label_info in tile["labels"]:
80+
lbl = label_info["ref"]
81+
lbl.classes(label_info["base_classes"])
82+
lbl.style(new_label_style)
83+
lbl.update()
84+
85+
# Run fitty JavaScript to resize text
86+
run_fitty_js()

src/ui/components.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Reusable UI components
2+
from nicegui import ui
3+
from typing import Dict, Callable, List, Tuple, Any
4+
5+
from src.config.constants import (
6+
HEADER_TEXT, HEADER_TEXT_COLOR, HEADER_FONT_FAMILY,
7+
BOARD_TILE_FONT, FREE_SPACE_TEXT, FREE_SPACE_TEXT_COLOR,
8+
TILE_CLICKED_BG_COLOR, TILE_CLICKED_TEXT_COLOR,
9+
TILE_UNCLICKED_BG_COLOR, TILE_UNCLICKED_TEXT_COLOR
10+
)
11+
from src.config.styles import (
12+
GRID_CONTAINER_CLASS, GRID_CLASSES,
13+
CARD_CLASSES, LABEL_CLASSES, LABEL_SMALL_CLASSES
14+
)
15+
from src.utils.text_processing import split_phrase_into_lines
16+
from src.ui.styling import get_line_style_for_lines
17+
18+
def create_header():
19+
"""Create the application header."""
20+
with ui.element("div").classes("w-full"):
21+
ui.label(f"{HEADER_TEXT}").classes("fit-header text-center").style(
22+
f"font-family: {HEADER_FONT_FAMILY}; color: {HEADER_TEXT_COLOR};"
23+
)
24+
25+
def create_board_controls(on_reset: Callable, on_new_board: Callable, seed_text: str):
26+
"""Create board control buttons."""
27+
with ui.row().classes("w-full mt-4 items-center justify-center gap-4"):
28+
with ui.button("", icon="refresh", on_click=on_reset).classes("rounded-full w-12 h-12") as reset_btn:
29+
ui.tooltip("Reset Board")
30+
with ui.button("", icon="autorenew", on_click=on_new_board).classes("rounded-full w-12 h-12") as new_board_btn:
31+
ui.tooltip("New Board")
32+
seed_label = ui.label(f"Seed: {seed_text}").classes("text-sm text-center").style(
33+
f"font-family: '{BOARD_TILE_FONT}', sans-serif; color: {TILE_UNCLICKED_BG_COLOR};"
34+
)
35+
return seed_label

src/ui/pages.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# Page definitions
2+
from nicegui import ui
3+
import logging
4+
from typing import Dict, Tuple, List, Set, Any
5+
6+
from src.config.constants import (
7+
HOME_BG_COLOR, STREAM_BG_COLOR
8+
)
9+
from src.core.board import (
10+
board, clicked_tiles, board_views,
11+
generate_board, reset_board, generate_new_board,
12+
toggle_tile, today_seed
13+
)
14+
from src.core.win_patterns import check_winner, process_win_notifications
15+
from src.core.phrases import check_phrases_file_change
16+
from src.ui.styling import setup_head
17+
from src.ui.components import create_header, create_board_controls
18+
from src.ui.board_view import build_board, update_tile_styles
19+
from src.utils.javascript import setup_javascript, run_fitty_js
20+
21+
# Global variable for the seed label
22+
seed_label = None
23+
24+
def toggle_tile_handler(row: int, col: int):
25+
"""Handle tile click events."""
26+
toggle_tile(row, col)
27+
28+
# Check for win conditions
29+
new_patterns = check_winner(clicked_tiles)
30+
process_win_notifications(new_patterns)
31+
32+
# Update all board views
33+
sync_board_state()
34+
35+
def sync_board_state():
36+
"""Synchronize the board state across all views."""
37+
try:
38+
# Update tile styles in every board view
39+
for view_key, (container, tile_buttons_local) in board_views.items():
40+
update_tile_styles(board, clicked_tiles, tile_buttons_local)
41+
container.update()
42+
43+
# Run fitty to resize text
44+
run_fitty_js()
45+
except Exception as e:
46+
logging.debug(f"Error in sync_board_state: {e}")
47+
48+
def create_board_view(background_color: str, is_global: bool):
49+
"""Create a board view (home or stream)."""
50+
# Setup page head elements
51+
setup_head(background_color)
52+
setup_javascript()
53+
54+
# Create header
55+
create_header()
56+
57+
# Create board container
58+
if is_global:
59+
container = ui.element("div").classes("home-board-container flex justify-center items-center w-full")
60+
try:
61+
ui.run_javascript("document.querySelector('.home-board-container').id = 'board-container'")
62+
except Exception as e:
63+
logging.debug(f"Setting board container ID failed: {e}")
64+
else:
65+
container = ui.element("div").classes("stream-board-container flex justify-center items-center w-full")
66+
try:
67+
ui.run_javascript("document.querySelector('.stream-board-container').id = 'board-container-stream'")
68+
except Exception as e:
69+
logging.debug(f"Setting stream container ID failed: {e}")
70+
71+
if is_global:
72+
# For home view, use global state
73+
global seed_label
74+
tile_buttons_dict = {}
75+
build_board(container, board, clicked_tiles, tile_buttons_dict, toggle_tile_handler)
76+
board_views["home"] = (container, tile_buttons_dict)
77+
78+
# Add phrase file watcher
79+
try:
80+
check_timer = ui.timer(1, check_phrases_file_change)
81+
except Exception as e:
82+
logging.warning(f"Error setting up timer: {e}")
83+
84+
# Add board controls
85+
seed_label = create_board_controls(reset_board, generate_new_board, today_seed)
86+
else:
87+
# For stream view, create local state
88+
local_tile_buttons = {}
89+
build_board(container, board, clicked_tiles, local_tile_buttons, toggle_tile_handler)
90+
board_views["stream"] = (container, local_tile_buttons)
91+
92+
@ui.page("/")
93+
def home_page():
94+
"""Home page with interactive board."""
95+
create_board_view(HOME_BG_COLOR, True)
96+
try:
97+
# Create a timer that deactivates when the client disconnects
98+
timer = ui.timer(0.1, sync_board_state)
99+
except Exception as e:
100+
logging.warning(f"Error creating timer: {e}")
101+
102+
@ui.page("/stream")
103+
def stream_page():
104+
"""Stream overlay page."""
105+
create_board_view(STREAM_BG_COLOR, False)
106+
try:
107+
# Create a timer that deactivates when the client disconnects
108+
timer = ui.timer(0.1, sync_board_state)
109+
except Exception as e:
110+
logging.warning(f"Error creating timer: {e}")
111+
112+
def setup_pages():
113+
"""Initialize all pages."""
114+
# Make sure the page routes are registered
115+
# The decorators should handle registration, but we include the functions here
116+
# to ensure they're imported properly
117+
home_page
118+
stream_page

src/ui/styling.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
# UI styling functions
2+
from nicegui import ui
3+
from src.config.constants import (
4+
BOARD_TILE_FONT, BOARD_TILE_FONT_WEIGHT,
5+
BOARD_TILE_FONT_STYLE
6+
)
7+
8+
def get_line_style_for_lines(line_count: int, text_color: str) -> str:
9+
"""Return a style string with adjusted line-height based on line count."""
10+
if line_count == 1:
11+
lh = "1.5em" # More spacing for a single line
12+
elif line_count == 2:
13+
lh = "1.2em" # Slightly reduced spacing for two lines
14+
elif line_count == 3:
15+
lh = "0.9em" # Even tighter spacing for three lines
16+
else:
17+
lh = "0.7em" # For four or more lines
18+
19+
return f"font-family: '{BOARD_TILE_FONT}', sans-serif; font-weight: {BOARD_TILE_FONT_WEIGHT}; font-style: {BOARD_TILE_FONT_STYLE}; padding: 0; margin: 0; color: {text_color}; line-height: {lh};"
20+
21+
def get_google_font_css(font_name: str, weight: str, style: str, uniquifier: str) -> str:
22+
"""Generate CSS for the specified Google font."""
23+
return f"""
24+
<style>
25+
.{uniquifier} {{
26+
font-family: "{font_name}", sans-serif;
27+
font-optical-sizing: auto;
28+
font-weight: {weight};
29+
font-style: {style};
30+
}}
31+
</style>
32+
"""
33+
34+
def setup_head(background_color: str):
35+
"""Set up common page head elements."""
36+
# Add Super Carnival font
37+
ui.add_css("""
38+
@font-face {
39+
font-family: 'Super Carnival';
40+
font-style: normal;
41+
font-weight: 400;
42+
src: url('/static/Super%20Carnival.woff') format('woff');
43+
}
44+
""")
45+
46+
# Add Google Fonts
47+
ui.add_head_html(f"""
48+
<link rel="preconnect" href="https://fonts.googleapis.com">
49+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
50+
<link href="https://fonts.googleapis.com/css2?family={BOARD_TILE_FONT.replace(" ", "+")}&display=swap" rel="stylesheet">
51+
""")
52+
53+
# Add CSS class for board tile fonts
54+
ui.add_head_html(get_google_font_css(BOARD_TILE_FONT, BOARD_TILE_FONT_WEIGHT, BOARD_TILE_FONT_STYLE, "board_tile"))
55+
56+
# Set background color
57+
ui.add_head_html(f'<style>body {{ background-color: {background_color}; }}</style>')

0 commit comments

Comments
 (0)