Spaces:
Running
Running
Smoothing transitions
Browse files- battlewords/ui.py +71 -53
- battlewords/ui_helpers.py +113 -7
battlewords/ui.py
CHANGED
|
@@ -28,24 +28,26 @@ from .audio import (
|
|
| 28 |
)
|
| 29 |
from .game_storage import load_game_from_sid, save_game_to_hf, get_shareable_url, add_user_result_to_game
|
| 30 |
from .settings_page import render_settings_page
|
| 31 |
-
from .ui_helpers import render_footer, inject_styles, fig_to_pil_rgba, ocean_background_css, inject_ocean_layers, pwa_service_worker, show_spinner
|
| 32 |
|
| 33 |
# --- Spinner context manager for custom spinner ---
|
| 34 |
class CustomSpinner:
|
| 35 |
"""Context manager for showing custom spinner with wrdler.gif"""
|
| 36 |
-
def __init__(self, placeholder, message: str = "Loading..."):
|
| 37 |
self.placeholder = placeholder
|
| 38 |
self.message = message
|
|
|
|
| 39 |
|
| 40 |
def __enter__(self):
|
| 41 |
with self.placeholder.container():
|
| 42 |
show_spinner(self.message)
|
| 43 |
#wait 1 sec to ensure spinner is visible
|
| 44 |
-
time.sleep(0.
|
| 45 |
-
|
| 46 |
return self
|
| 47 |
|
| 48 |
-
def __exit__(self, *args):
|
|
|
|
|
|
|
| 49 |
self.placeholder.empty()
|
| 50 |
|
| 51 |
CoordLike = Tuple[int, int]
|
|
@@ -160,44 +162,46 @@ def _init_session() -> None:
|
|
| 160 |
st.session_state.enable_sound_effects = False
|
| 161 |
|
| 162 |
def _new_game() -> None:
|
| 163 |
-
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
|
|
|
|
|
|
| 201 |
|
| 202 |
|
| 203 |
def _to_state() -> GameState:
|
|
@@ -765,6 +769,17 @@ def _render_guess_form(state: GameState):
|
|
| 765 |
st.markdown(
|
| 766 |
"""
|
| 767 |
<style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 768 |
.bw-incorrect-guesses {
|
| 769 |
font-size: 0.7rem;
|
| 770 |
color: #ff9999;
|
|
@@ -1412,7 +1427,10 @@ def _on_game_option_change() -> None:
|
|
| 1412 |
|
| 1413 |
def run_app():
|
| 1414 |
# Render PWA service worker registration (meta tags in <head> via Docker)
|
| 1415 |
-
|
|
|
|
|
|
|
|
|
|
| 1416 |
|
| 1417 |
# try:
|
| 1418 |
# st.set_page_config(initial_sidebar_state="collapsed")
|
|
@@ -1442,26 +1460,23 @@ def run_app():
|
|
| 1442 |
spinner_placeholder = st.empty()
|
| 1443 |
with CustomSpinner(spinner_placeholder, "Initial Load..."):
|
| 1444 |
st.session_state["initial_page_loaded"] = True
|
| 1445 |
-
st.session_state["last_nav_page"] = page
|
| 1446 |
-
|
| 1447 |
-
st.rerun()
|
| 1448 |
|
| 1449 |
# Show spinner when navigating via menu (query param page change)
|
| 1450 |
last_nav_page = st.session_state.get("last_nav_page")
|
| 1451 |
if last_nav_page is not None and last_nav_page != page:
|
| 1452 |
spinner_placeholder = st.empty()
|
| 1453 |
with CustomSpinner(spinner_placeholder, "Loading..."):
|
| 1454 |
-
st.session_state["last_nav_page"] = page
|
| 1455 |
-
|
| 1456 |
-
st.rerun()
|
| 1457 |
|
| 1458 |
# One-time spinner on initial display of the main UI.
|
| 1459 |
if page == "play" and not st.session_state.get("initial_page_loaded", False):
|
| 1460 |
spinner_placeholder = st.empty()
|
| 1461 |
with CustomSpinner(spinner_placeholder, "Loading Game..."):
|
| 1462 |
-
st.session_state["initial_page_loaded"] = True
|
| 1463 |
-
|
| 1464 |
-
st.rerun()
|
| 1465 |
|
| 1466 |
if page == "settings":
|
| 1467 |
spinner_placeholder = st.empty()
|
|
@@ -1537,6 +1552,7 @@ def _render_game_tab():
|
|
| 1537 |
"""
|
| 1538 |
Render the main game tab layout.
|
| 1539 |
"""
|
|
|
|
| 1540 |
_init_session()
|
| 1541 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 1542 |
#inject_ocean_layers() # <-- add the animated layers
|
|
@@ -1569,3 +1585,5 @@ def _render_game_tab():
|
|
| 1569 |
st.session_state.letter_map,
|
| 1570 |
show_grid_ticks=st.session_state.get("show_grid_ticks", True),
|
| 1571 |
)
|
|
|
|
|
|
|
|
|
| 28 |
)
|
| 29 |
from .game_storage import load_game_from_sid, save_game_to_hf, get_shareable_url, add_user_result_to_game
|
| 30 |
from .settings_page import render_settings_page
|
| 31 |
+
from .ui_helpers import render_footer, inject_styles, fig_to_pil_rgba, ocean_background_css, inject_ocean_layers, pwa_service_worker, show_spinner, fade_out_spinner, start_root_fade_in, finish_root_fade_in
|
| 32 |
|
| 33 |
# --- Spinner context manager for custom spinner ---
|
| 34 |
class CustomSpinner:
|
| 35 |
"""Context manager for showing custom spinner with wrdler.gif"""
|
| 36 |
+
def __init__(self, placeholder, message: str = "Loading...", duration: float = 1.0):
|
| 37 |
self.placeholder = placeholder
|
| 38 |
self.message = message
|
| 39 |
+
self.duration = duration
|
| 40 |
|
| 41 |
def __enter__(self):
|
| 42 |
with self.placeholder.container():
|
| 43 |
show_spinner(self.message)
|
| 44 |
#wait 1 sec to ensure spinner is visible
|
| 45 |
+
time.sleep(0.7)
|
|
|
|
| 46 |
return self
|
| 47 |
|
| 48 |
+
def __exit__(self, *args):
|
| 49 |
+
# fade_out_spinner(self.duration)
|
| 50 |
+
# time.sleep(self.duration)
|
| 51 |
self.placeholder.empty()
|
| 52 |
|
| 53 |
CoordLike = Tuple[int, int]
|
|
|
|
| 162 |
st.session_state.enable_sound_effects = False
|
| 163 |
|
| 164 |
def _new_game() -> None:
|
| 165 |
+
spinner_placeholder = st.empty()
|
| 166 |
+
with CustomSpinner(spinner_placeholder, "Initializing New Game..."):
|
| 167 |
+
selected = st.session_state.get("selected_wordlist")
|
| 168 |
+
mode = st.session_state.get("game_mode")
|
| 169 |
+
show_grid_ticks = st.session_state.get("show_grid_ticks", False)
|
| 170 |
+
spacer = st.session_state.get("spacer",1)
|
| 171 |
+
show_incorrect_guesses = st.session_state.get("show_incorrect_guesses", False)
|
| 172 |
+
# --- Preserve music and effects settings ---
|
| 173 |
+
music_enabled = st.session_state.get("music_enabled", False)
|
| 174 |
+
music_track_path = st.session_state.get("music_track_path")
|
| 175 |
+
music_volume = st.session_state.get("music_volume",15)
|
| 176 |
+
effects_volume = st.session_state.get("effects_volume",25)
|
| 177 |
+
enable_sound_effects = st.session_state.get("enable_sound_effects", True)
|
| 178 |
+
# NEW: Preserve Show Challenge Share Links
|
| 179 |
+
show_challenge_share_links = st.session_state.get("show_challenge_share_links", False)
|
| 180 |
+
|
| 181 |
+
st.session_state.clear()
|
| 182 |
+
if selected:
|
| 183 |
+
st.session_state.selected_wordlist = selected
|
| 184 |
+
if mode:
|
| 185 |
+
st.session_state.game_mode = mode
|
| 186 |
+
st.session_state.show_grid_ticks = show_grid_ticks
|
| 187 |
+
st.session_state.spacer = spacer
|
| 188 |
+
st.session_state.show_incorrect_guesses = show_incorrect_guesses
|
| 189 |
+
# --- Restore music/effects settings ---
|
| 190 |
+
st.session_state.music_enabled = music_enabled
|
| 191 |
+
if music_track_path:
|
| 192 |
+
st.session_state.music_track_path = music_track_path
|
| 193 |
+
st.session_state.music_volume = music_volume
|
| 194 |
+
st.session_state.effects_volume = effects_volume
|
| 195 |
+
st.session_state.enable_sound_effects = enable_sound_effects
|
| 196 |
+
# NEW: Restore Show Challenge Share Links
|
| 197 |
+
st.session_state.show_challenge_share_links = show_challenge_share_links
|
| 198 |
+
|
| 199 |
+
st.session_state.radar_gif_path = None
|
| 200 |
+
st.session_state.radar_gif_signature = None
|
| 201 |
+
st.session_state.start_time = datetime.now() # Reset timer on new game
|
| 202 |
+
st.session_state.end_time = None
|
| 203 |
+
st.session_state.incorrect_guesses = [] # Clear incorrect guesses for new game
|
| 204 |
+
_init_session()
|
| 205 |
|
| 206 |
|
| 207 |
def _to_state() -> GameState:
|
|
|
|
| 769 |
st.markdown(
|
| 770 |
"""
|
| 771 |
<style>
|
| 772 |
+
/* Grey-out the guess textbox when disabled */
|
| 773 |
+
.st-key-guess_input input:disabled {
|
| 774 |
+
background-color: #ffffff66 !important;
|
| 775 |
+
color: #ffffff !important;
|
| 776 |
+
-webkit-text-fill-color: #ffffff !important;
|
| 777 |
+
opacity: 1 !important;
|
| 778 |
+
}
|
| 779 |
+
/* Match the disabled textbox border to the label color */
|
| 780 |
+
.st-key-guess_input input:disabled {
|
| 781 |
+
border-color: #ffffff !important;
|
| 782 |
+
}
|
| 783 |
.bw-incorrect-guesses {
|
| 784 |
font-size: 0.7rem;
|
| 785 |
color: #ff9999;
|
|
|
|
| 1427 |
|
| 1428 |
def run_app():
|
| 1429 |
# Render PWA service worker registration (meta tags in <head> via Docker)
|
| 1430 |
+
# Streamlit reruns the script frequently; inject this only once per session.
|
| 1431 |
+
if not st.session_state.get("pwa_injected", False):
|
| 1432 |
+
st.markdown(pwa_service_worker, unsafe_allow_html=True)
|
| 1433 |
+
st.session_state["pwa_injected"] = True
|
| 1434 |
|
| 1435 |
# try:
|
| 1436 |
# st.set_page_config(initial_sidebar_state="collapsed")
|
|
|
|
| 1460 |
spinner_placeholder = st.empty()
|
| 1461 |
with CustomSpinner(spinner_placeholder, "Initial Load..."):
|
| 1462 |
st.session_state["initial_page_loaded"] = True
|
| 1463 |
+
st.session_state["last_nav_page"] = page
|
| 1464 |
+
st.rerun()
|
|
|
|
| 1465 |
|
| 1466 |
# Show spinner when navigating via menu (query param page change)
|
| 1467 |
last_nav_page = st.session_state.get("last_nav_page")
|
| 1468 |
if last_nav_page is not None and last_nav_page != page:
|
| 1469 |
spinner_placeholder = st.empty()
|
| 1470 |
with CustomSpinner(spinner_placeholder, "Loading..."):
|
| 1471 |
+
st.session_state["last_nav_page"] = page
|
| 1472 |
+
st.rerun()
|
|
|
|
| 1473 |
|
| 1474 |
# One-time spinner on initial display of the main UI.
|
| 1475 |
if page == "play" and not st.session_state.get("initial_page_loaded", False):
|
| 1476 |
spinner_placeholder = st.empty()
|
| 1477 |
with CustomSpinner(spinner_placeholder, "Loading Game..."):
|
| 1478 |
+
st.session_state["initial_page_loaded"] = True
|
| 1479 |
+
st.rerun()
|
|
|
|
| 1480 |
|
| 1481 |
if page == "settings":
|
| 1482 |
spinner_placeholder = st.empty()
|
|
|
|
| 1552 |
"""
|
| 1553 |
Render the main game tab layout.
|
| 1554 |
"""
|
| 1555 |
+
start_root_fade_in(0.3)
|
| 1556 |
_init_session()
|
| 1557 |
st.markdown(ocean_background_css, unsafe_allow_html=True)
|
| 1558 |
#inject_ocean_layers() # <-- add the animated layers
|
|
|
|
| 1585 |
st.session_state.letter_map,
|
| 1586 |
show_grid_ticks=st.session_state.get("show_grid_ticks", True),
|
| 1587 |
)
|
| 1588 |
+
|
| 1589 |
+
finish_root_fade_in(0.3)
|
battlewords/ui_helpers.py
CHANGED
|
@@ -10,6 +10,7 @@ from . import __version__ as version
|
|
| 10 |
from .modules.constants import APP_SETTINGS
|
| 11 |
import base64
|
| 12 |
import os
|
|
|
|
| 13 |
|
| 14 |
def fig_to_pil_rgba(fig):
|
| 15 |
canvas = FigureCanvas(fig)
|
|
@@ -312,6 +313,86 @@ def inject_ocean_layers() -> None:
|
|
| 312 |
)
|
| 313 |
|
| 314 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 315 |
def inject_styles() -> None:
|
| 316 |
st.markdown(
|
| 317 |
"""
|
|
@@ -379,7 +460,10 @@ def inject_styles() -> None:
|
|
| 379 |
/* div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; border-radius: 0; background: #1d64c8; color: #ffffff; font-weight: 700; padding: 0.5rem 0.75rem; min-height: 2.5rem; min-width: 2.5rem;} */
|
| 380 |
.st-key-new_game_btn, .st-key-sort_wordlist_btn, .st-key-filter_wordlist_btn, .st-key-settings_save_apply_btn, .st-key-settings_discard_btn { margin: 0 auto; aspect-ratio: unset; z-index:9999;}
|
| 381 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button, .st-key-filter_wordlist_btn > div[data-testid="stButton"] button, .st-key-settings_save_apply_btn > div[data-testid="stButton"] button, .st-key-settings_discard_btn > div[data-testid="stButton"] button { aspect-ratio: unset; text-align:center; height: auto; width: 160px; border-radius:8px;}
|
| 382 |
-
|
|
|
|
|
|
|
|
|
|
| 383 |
|
| 384 |
div[data-testid="column"], .st-emotion-cache-zh2fnc, .st-emotion-cache-1tj828o, .st-emotion-cache-1anq8dj { width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important; border-radius:0; background-color: #1e69d2; border: 1px solid transparent;}
|
| 385 |
.st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc { gap:0.15rem !important; min-height: 2.5rem; min-width: 2.5rem;}
|
|
@@ -465,10 +549,19 @@ def inject_styles() -> None:
|
|
| 465 |
}
|
| 466 |
}
|
| 467 |
|
| 468 |
-
|
| 469 |
-
|
| 470 |
-
|
| 471 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 472 |
</style>
|
| 473 |
""",
|
| 474 |
unsafe_allow_html=True,
|
|
@@ -582,9 +675,14 @@ def show_spinner(message: str = "Loading..."):
|
|
| 582 |
<style>
|
| 583 |
.bw-spinner-overlay {{
|
| 584 |
position: fixed; z-index: 99999; top: 0; left: 0; width: 100vw; height: 100vh;
|
| 585 |
-
|
|
|
|
| 586 |
background: rgba(11,42,74,1.0); display: flex; align-items: center; justify-content: center;
|
| 587 |
}}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 588 |
.bw-spinner-img {{
|
| 589 |
width: 150px; height: 150px; border-radius: 16px; box-shadow: 0 0 8px #1d64c8;
|
| 590 |
background: #fff; display: flex; align-items: center; justify-content: center;
|
|
@@ -598,7 +696,15 @@ def show_spinner(message: str = "Loading..."):
|
|
| 598 |
color: #1d64c8; font-size: 1.2rem; margin-top: 1.5rem; text-align: center; font-weight: 600;
|
| 599 |
}}
|
| 600 |
</style>
|
| 601 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 602 |
<div class="modal-spinner-inner" style="display:flex; flex-direction:column; align-items:center;">
|
| 603 |
<div class="bw-spinner-img stImage"><img src="/app/static/battlewords.gif" alt="Loading..." width="192" height="192" /></div>
|
| 604 |
<div class="bw-spinner-msg">{message}</div>
|
|
|
|
| 10 |
from .modules.constants import APP_SETTINGS
|
| 11 |
import base64
|
| 12 |
import os
|
| 13 |
+
import streamlit.components.v1 as components
|
| 14 |
|
| 15 |
def fig_to_pil_rgba(fig):
|
| 16 |
canvas = FigureCanvas(fig)
|
|
|
|
| 313 |
)
|
| 314 |
|
| 315 |
|
| 316 |
+
def start_root_fade_in(duration_s: float = 0.35) -> None:
|
| 317 |
+
"""Hide the app root and prepare a fade-in transition."""
|
| 318 |
+
components.html(
|
| 319 |
+
f"""
|
| 320 |
+
<script>
|
| 321 |
+
(function() {{
|
| 322 |
+
try {{
|
| 323 |
+
var parentDoc = (window.parent && window.parent.document) ? window.parent.document : document;
|
| 324 |
+
var root = parentDoc.getElementById('root');
|
| 325 |
+
if (!root) return;
|
| 326 |
+
root.classList.add('bw-fadein-prep');
|
| 327 |
+
// Immediately hide before any Streamlit render updates become visible.
|
| 328 |
+
root.style.setProperty('opacity', '0', 'important');
|
| 329 |
+
// Force style/layout flush so the opacity=0 takes effect before transition is applied.
|
| 330 |
+
void root.offsetHeight;
|
| 331 |
+
root.style.setProperty('transition', 'opacity {duration_s:.3f}s ease', 'important');
|
| 332 |
+
}} catch (e) {{ /* no-op */ }}
|
| 333 |
+
}})();
|
| 334 |
+
</script>
|
| 335 |
+
""",
|
| 336 |
+
height=0,
|
| 337 |
+
)
|
| 338 |
+
|
| 339 |
+
|
| 340 |
+
def finish_root_fade_in(duration_s: float = 0.35) -> None:
|
| 341 |
+
"""Trigger the fade-in transition for the app root."""
|
| 342 |
+
components.html(
|
| 343 |
+
f"""
|
| 344 |
+
<script>
|
| 345 |
+
(function() {{
|
| 346 |
+
try {{
|
| 347 |
+
var parentDoc = (window.parent && window.parent.document) ? window.parent.document : document;
|
| 348 |
+
var root = parentDoc.getElementById('root');
|
| 349 |
+
if (!root) return;
|
| 350 |
+
root.style.transition = 'opacity {duration_s:.3f}s ease';
|
| 351 |
+
root.style.opacity = '1';
|
| 352 |
+
root.classList.remove('bw-fadein-prep');
|
| 353 |
+
}} catch (e) {{ /* no-op */ }}
|
| 354 |
+
}})();
|
| 355 |
+
</script>
|
| 356 |
+
""",
|
| 357 |
+
height=0,
|
| 358 |
+
)
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
def fade_out_spinner(duration_s: float = 0.5) -> None:
|
| 362 |
+
"""Trigger a client-side fade-out of the spinner overlay (if present)."""
|
| 363 |
+
ms = max(0, int(duration_s * 1000))
|
| 364 |
+
# Use a component iframe so JS actually executes reliably.
|
| 365 |
+
# This script reaches up to the parent document to find the overlay.
|
| 366 |
+
components.html(
|
| 367 |
+
f"""
|
| 368 |
+
<script>
|
| 369 |
+
(function() {{
|
| 370 |
+
try {{
|
| 371 |
+
var parentDoc = (window.parent && window.parent.document) ? window.parent.document : document;
|
| 372 |
+
// Ensure the app stays hidden while we perform the fade-out.
|
| 373 |
+
try {{
|
| 374 |
+
var root = parentDoc.getElementById('root');
|
| 375 |
+
if (root) root.classList.add('bw-spinner-active');
|
| 376 |
+
}} catch (e) {{ /* no-op */ }}
|
| 377 |
+
var el = parentDoc.getElementById('bw-spinner-overlay');
|
| 378 |
+
if (!el) return;
|
| 379 |
+
el.style.transition = 'opacity {duration_s:.3f}s ease';
|
| 380 |
+
el.classList.add('bw-spinner-fadeout');
|
| 381 |
+
window.setTimeout(function() {{
|
| 382 |
+
try {{ el.remove(); }} catch (e) {{ /* no-op */ }}
|
| 383 |
+
try {{
|
| 384 |
+
var root = parentDoc.getElementById('root');
|
| 385 |
+
if (root) root.classList.remove('bw-spinner-active');
|
| 386 |
+
}} catch (e) {{ /* no-op */ }}
|
| 387 |
+
}}, {ms});
|
| 388 |
+
}} catch (e) {{ /* no-op */ }}
|
| 389 |
+
}})();
|
| 390 |
+
</script>
|
| 391 |
+
""",
|
| 392 |
+
height=0,
|
| 393 |
+
)
|
| 394 |
+
|
| 395 |
+
|
| 396 |
def inject_styles() -> None:
|
| 397 |
st.markdown(
|
| 398 |
"""
|
|
|
|
| 460 |
/* div[data-testid="stButton"] button { max-width: 100%; aspect-ratio: 1 / 1; border-radius: 0; background: #1d64c8; color: #ffffff; font-weight: 700; padding: 0.5rem 0.75rem; min-height: 2.5rem; min-width: 2.5rem;} */
|
| 461 |
.st-key-new_game_btn, .st-key-sort_wordlist_btn, .st-key-filter_wordlist_btn, .st-key-settings_save_apply_btn, .st-key-settings_discard_btn { margin: 0 auto; aspect-ratio: unset; z-index:9999;}
|
| 462 |
.st-key-new_game_btn > div[data-testid="stButton"] button, .st-key-sort_wordlist_btn > div[data-testid="stButton"] button, .st-key-filter_wordlist_btn > div[data-testid="stButton"] button, .st-key-settings_save_apply_btn > div[data-testid="stButton"] button, .st-key-settings_discard_btn > div[data-testid="stButton"] button { aspect-ratio: unset; text-align:center; height: auto; width: 160px; border-radius:8px;}
|
| 463 |
+
.stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:first-child { display: none; }
|
| 464 |
+
.stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:nth-child(2) { display: none; }
|
| 465 |
+
.stVerticalBlock .st-emotion-cache-tn0cau.e1wguzas3 div:nth-child(3) { display: none; }
|
| 466 |
+
.st-emotion-cache-18kf3ut.e1wguzas41 { display: none; }
|
| 467 |
|
| 468 |
div[data-testid="column"], .st-emotion-cache-zh2fnc, .st-emotion-cache-1tj828o, .st-emotion-cache-1anq8dj { width: auto !important; flex: 1 1 auto !important; min-width: 100% !important; max-width: 100% !important; border-radius:0; background-color: #1e69d2; border: 1px solid transparent;}
|
| 469 |
.st-emotion-cache-1permvm, .st-emotion-cache-1n6tfoc { gap:0.15rem !important; min-height: 2.5rem; min-width: 2.5rem;}
|
|
|
|
| 549 |
}
|
| 550 |
}
|
| 551 |
|
| 552 |
+
/* Helper to absolutely/fixed position Streamlit component wrapper when showing modal */
|
| 553 |
+
.bw-component-abs { position: fixed !important; inset: 0 !important; z-index: 99999 !important; width: 100vw !important; height: 100vh !important; margin: 0 !important; padding: 0 !important; }
|
| 554 |
+
/* Generic hide utility */
|
| 555 |
+
.hide { display: none !important; pointer-events: none !important; }
|
| 556 |
+
.invisible { visibility: hidden !important; }
|
| 557 |
+
|
| 558 |
+
/* While spinner is active, hide the underlying app to prevent flashes. */
|
| 559 |
+
#root.bw-spinner-active .stApp {
|
| 560 |
+
visibility: hidden;
|
| 561 |
+
}
|
| 562 |
+
#root.bw-spinner-active #bw-spinner-overlay {
|
| 563 |
+
visibility: visible;
|
| 564 |
+
}
|
| 565 |
</style>
|
| 566 |
""",
|
| 567 |
unsafe_allow_html=True,
|
|
|
|
| 675 |
<style>
|
| 676 |
.bw-spinner-overlay {{
|
| 677 |
position: fixed; z-index: 99999; top: 0; left: 0; width: 100vw; height: 100vh;
|
| 678 |
+
opacity: 1;
|
| 679 |
+
transition: opacity 0.5s ease;
|
| 680 |
background: rgba(11,42,74,1.0); display: flex; align-items: center; justify-content: center;
|
| 681 |
}}
|
| 682 |
+
.bw-spinner-overlay.bw-spinner-fadeout {{
|
| 683 |
+
opacity: 0;
|
| 684 |
+
pointer-events: none;
|
| 685 |
+
}}
|
| 686 |
.bw-spinner-img {{
|
| 687 |
width: 150px; height: 150px; border-radius: 16px; box-shadow: 0 0 8px #1d64c8;
|
| 688 |
background: #fff; display: flex; align-items: center; justify-content: center;
|
|
|
|
| 696 |
color: #1d64c8; font-size: 1.2rem; margin-top: 1.5rem; text-align: center; font-weight: 600;
|
| 697 |
}}
|
| 698 |
</style>
|
| 699 |
+
<script>
|
| 700 |
+
(function() {{
|
| 701 |
+
try {{
|
| 702 |
+
var root = document.getElementById('root');
|
| 703 |
+
if (root) root.classList.add('bw-spinner-active');
|
| 704 |
+
}} catch (e) {{ /* no-op */ }}
|
| 705 |
+
}})();
|
| 706 |
+
</script>
|
| 707 |
+
<div class="bw-spinner-overlay" id="bw-spinner-overlay">
|
| 708 |
<div class="modal-spinner-inner" style="display:flex; flex-direction:column; align-items:center;">
|
| 709 |
<div class="bw-spinner-img stImage"><img src="/app/static/battlewords.gif" alt="Loading..." width="192" height="192" /></div>
|
| 710 |
<div class="bw-spinner-msg">{message}</div>
|