diff --git a/sl/SL_Menu.py b/sl/SL_Menu.py index 91b780f..b10f1fe 100644 --- a/sl/SL_Menu.py +++ b/sl/SL_Menu.py @@ -20,6 +20,17 @@ except (ImportError, ModuleNotFoundError): UPDATE_MODULE_AVAILABLE = False +try: + # Internal (not officially public) API, but it's the only way to tell a + # fragment's own run_every tick apart from an ordinary rerun - see the + # usage in _auto_refresh_on_external_changes() for why that distinction + # matters. Guarded so a future Streamlit release that moves/removes this + # degrades to the older, less precise timing-based heuristic instead of + # crashing the app outright. + from streamlit.runtime.scriptrunner_utils.script_run_context import get_script_run_ctx +except ImportError: + get_script_run_ctx = None + # --- Configuration & Setup --- CONFIG_FILE = 'config.json' SL_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -245,14 +256,6 @@ def navigate_to(menu_name): :param menu_name: The key of the menu to navigate to (must exist in menu_map). """ st.session_state.menu = menu_name - # Counts as "just refreshed" so the auto-refresh fragment (see - # _auto_refresh_on_external_changes below) doesn't fire its own - # competing st.rerun() while this navigation's rerun is still being - # processed - otherwise that second rerun can win the race and abort - # this one before the newly selected menu ever gets rendered, making the - # click look like it did nothing (or, if the user then clicks again out - # of impatience, like the view jumped to the wrong place). - st.session_state["_last_auto_refresh"] = time.monotonic() st.rerun() def set_feedback(message, type='success'): @@ -305,13 +308,36 @@ def _auto_refresh_on_external_changes(): since an MCP client can be spawning that process independently of what this app's own config says, so there's no reliable way to know it isn't. - This function is invoked twice per cycle: once inline as a normal part - of every full script run (including the one it itself triggers below), - and once on its own every `run_every` seconds via the fragment's timer. - The timestamp check tells those two cases apart - without it, calling - st.rerun() unconditionally here would re-trigger itself instantly on - every single run and the app would never finish rendering. - """ + This function is invoked two different ways: inline, as a normal part of + every full script run (a click anywhere in the app, navigate_to(), ...), + and separately, on its own, every `run_every` seconds via the fragment's + own timer. Only the second case should trigger a rerun here - Streamlit + runs a timer-triggered tick as a fragment-scoped rerun that calls only + this function's body and nothing else in the script (see + streamlit/runtime/scriptrunner/script_runner.py: a rerun with a non-empty + fragment_id_queue calls just the stored fragment closure instead of + exec()-ing the rest of the script), so escalating to a full st.rerun() + is genuinely needed there for the data reload and current view to + actually refresh. An inline call, by contrast, is already running as + part of a full script execution - calling st.rerun() there would abort + that very run before it reaches whatever code (a button's own handler, + say) triggered it in the first place, silently discarding that + interaction. That was a real, user-reported bug: clicks made after a few + seconds of idling (long enough to make this look like a timer tick by + elapsed time alone, which is what an earlier version of this function + compared against) would occasionally do nothing on the first try. + ctx.fragment_ids_this_run tells the two cases apart directly instead of + guessing from elapsed time. + """ + ctx = get_script_run_ctx() if get_script_run_ctx else None + if ctx is not None: + if ctx.fragment_ids_this_run: + st.rerun() + return + + # Fallback if a future Streamlit release moves/removes the internal hook + # above: less precise (can still occasionally preempt a click that lands + # in the same ~4s window), but keeps the periodic refresh itself working. now = time.monotonic() last_refresh = st.session_state.get("_last_auto_refresh") st.session_state["_last_auto_refresh"] = now diff --git a/sl/style_blue.css b/sl/style_blue.css new file mode 100644 index 0000000..55de807 --- /dev/null +++ b/sl/style_blue.css @@ -0,0 +1,292 @@ +/* ========================================================================== + TimeControl - Blue Style + ========================================================================== + Clean, professional palette inspired by Dlubal's own software (RFEM / + Dlubal CALC): a near-white page background, soft blue-gray card borders, + and a corporate blue accent for buttons, links and selection states. + + Custom properties live on :root (not .stApp) because several Streamlit + components (popover panels, select/date dropdowns) render into a portal + appended directly to , outside .stApp's own subtree - a variable + defined only on .stApp would never reach them. + ========================================================================== */ + +:root { + --tc-bg: #F7F9FC; + --tc-bg-elevated: #FFFFFF; + --tc-bg-input: #FFFFFF; + --tc-bg-hover: #EAF1FC; + --tc-text: #1E2A3A; + --tc-text-muted: #6B7686; + --tc-border: #E1E7F0; + --tc-accent: #2E6FE3; + --tc-accent-hover: #1E56C4; + --tc-icon-color: #55627A; + --tc-code-text: #2E6FE3; +} + +/* Global Font & Background */ +.stApp { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + background-color: var(--tc-bg); + color: var(--tc-text); +} + +/* Native Streamlit chrome: the fixed header bar (Deploy/hamburger menu) and + the toolbar/decoration strip sit outside .stApp's own background, so they + otherwise stay on Streamlit's default white/gray regardless of the rest + of the page. */ +header[data-testid="stHeader"], +[data-testid="stToolbar"], +[data-testid="stDecoration"] { + background-color: var(--tc-bg) !important; + color: var(--tc-text); +} + +header[data-testid="stHeader"] svg { + fill: var(--tc-icon-color) !important; +} + +/* Headings */ +h1, h2, h3 { + font-weight: 700; + color: var(--tc-text); + letter-spacing: -0.02em; +} + +h1 { + font-size: 2.0rem; + padding-bottom: 0.5rem; + border-bottom: 1px solid var(--tc-border); + margin-bottom: 1.5rem; + padding-top: 0.1rem; +} + +h1::before { + content: "⏱️"; + margin-right: 0.5rem; +} + +h2 { + font-size: 1.6rem; +} + +h3 { + font-size: 1.35rem; +} + +h4 { + font-size: 1.15rem; +} + +h5, h6 { + font-size: 1.0rem; +} + +/* Widget labels, captions and help tooltips */ +[data-testid="stWidgetLabel"] p, +[data-testid="stCaptionContainer"], +[data-testid="stCaptionContainer"] p, +[data-testid="stMarkdownContainer"] small { + color: var(--tc-text-muted) !important; +} + +[data-testid="stTooltipIcon"] { + color: var(--tc-text-muted) !important; +} + +/* Buttons - regular, form-submit and popover triggers all need the same + treatment so none of them fall back to Streamlit's own default styling. */ +.stButton button, +[data-testid="stFormSubmitButton"] button, +[data-testid="stPopoverButton"] { + border-radius: 10px; + font-weight: 600; + border: 1px solid var(--tc-border); + background-color: var(--tc-bg-elevated); + color: var(--tc-text); + transition: all 0.2s ease-in-out; + box-shadow: 0 1px 2px rgba(30, 42, 58, 0.06); +} + +.stButton button:hover, +[data-testid="stFormSubmitButton"] button:hover, +[data-testid="stPopoverButton"]:hover { + border-color: var(--tc-accent); + color: var(--tc-accent-hover); + background-color: var(--tc-bg-hover); + transform: translateY(-1px); + box-shadow: 0 4px 10px rgba(30, 42, 58, 0.10); +} + +.stButton button:active, +[data-testid="stFormSubmitButton"] button:active { + transform: translateY(0); + box-shadow: none; +} + +.stButton button:disabled, +.stButton button:disabled:hover { + opacity: 0.5; + color: var(--tc-text-muted) !important; + border-color: var(--tc-border) !important; + background-color: var(--tc-bg-elevated) !important; + transform: none; + box-shadow: none; +} + +/* Primary-type buttons (e.g. "Add Task") get a solid accent fill, matching + Dlubal CALC's own "New chat" / "Sign up" call-to-action buttons. */ +[data-testid="stBaseButton-primary"] { + background-color: var(--tc-accent) !important; + border-color: var(--tc-accent) !important; + color: #FFFFFF !important; +} + +[data-testid="stBaseButton-primary"]:hover { + background-color: var(--tc-accent-hover) !important; + border-color: var(--tc-accent-hover) !important; + color: #FFFFFF !important; +} + +/* The dropdown card a popover opens (toolbar's New/Management/Reporting + menus) - rendered in a body-level portal, stayed on Streamlit's default + white/gray otherwise. */ +[data-testid="stPopoverBody"], +[data-testid="stPopoverBody"] > div { + background-color: var(--tc-bg-elevated) !important; + border: 1px solid var(--tc-border) !important; + color: var(--tc-text); +} + +/* Input Fields */ +.stTextInput input, +.stNumberInput input, +.stTextArea textarea, +.stDateInput input, +.stSelectbox div[data-baseweb="select"] > div { + border-radius: 8px; + border: 1px solid var(--tc-border); + background-color: var(--tc-bg-input); + color: var(--tc-text); +} + +.stTextInput input:focus, +.stNumberInput input:focus, +.stTextArea textarea:focus { + border-color: var(--tc-accent) !important; + box-shadow: 0 0 0 1px var(--tc-accent) !important; +} + +.stTextInput input::placeholder, +.stTextArea textarea::placeholder { + color: var(--tc-text-muted); + opacity: 1; +} + +[data-testid="stNumberInputStepDown"], +[data-testid="stNumberInputStepUp"] { + background-color: var(--tc-bg-hover) !important; + border-color: var(--tc-border) !important; + color: var(--tc-text) !important; +} + +/* Select / date-input dropdown popups also render in a body-level portal + via BaseWeb, so they need the same background rendered explicitly. The + plain [data-baseweb="popover"] wrapper (and the unlabeled div directly + inside it) is what shows a card behind the calendar/menu content - the + inner listbox/menu/calendar elements need their own override too. */ +[data-baseweb="popover"], +[data-baseweb="popover"] > div, +[data-baseweb="popover"] [role="listbox"], +[data-baseweb="menu"], +[data-baseweb="calendar"] { + background-color: var(--tc-bg-elevated) !important; + color: var(--tc-text) !important; +} + +[data-baseweb="popover"] li[role="option"], +[data-baseweb="menu"] li { + color: var(--tc-text) !important; +} + +[data-baseweb="popover"] li[role="option"]:hover, +[data-baseweb="menu"] li:hover { + background-color: var(--tc-bg-hover) !important; +} + +/* The date-input calendar's month/year header bar and weekday-initials row + keep Streamlit's default secondary-background grey regardless of theme; + [role="presentation"] reaches the weekday row, .st-c7 is the shared class + between it and the header bar just above it. Out-of-month filler day + cells render their fill via a ::after pseudo-element rather than their + own background, so they need a separate override. */ +[data-baseweb="calendar"] [role="presentation"], +[data-baseweb="calendar"] .st-c7 { + background-color: transparent !important; +} + +[data-baseweb="calendar"] [role="gridcell"]::after { + background-color: transparent !important; +} + +/* The selected day's own indicator (a circular ::after ring BaseWeb pairs + with white text) shares that same pseudo-element with the out-of-month + filler cells above, so it lost its fill too - leaving white-on-transparent + text that's invisible against this theme's light background. Restore a + solid, on-brand circle behind just the selected day. */ +[data-baseweb="calendar"] .st-cj::after { + background-color: var(--tc-accent) !important; +} + +/* Checkbox */ +[data-testid="stCheckbox"] label p { + color: var(--tc-text) !important; +} + +/* Expander */ +[data-testid="stExpander"] { + border: 1px solid var(--tc-border); + border-radius: 10px; + background-color: var(--tc-bg-elevated); + overflow: hidden; +} + +[data-testid="stExpander"] summary { + color: var(--tc-text) !important; +} + +[data-testid="stExpander"] summary:hover { + background-color: var(--tc-bg-hover); +} + +/* Divider */ +hr { + border-color: var(--tc-border) !important; +} + +/* Alerts (st.info/success/warning/error) */ +[data-testid="stAlert"] { + background-color: var(--tc-bg-elevated); + border-radius: 8px; + border: 1px solid var(--tc-border); +} + +/* Inline code and code blocks */ +code { + background-color: var(--tc-bg-hover) !important; + color: var(--tc-code-text) !important; +} + +/* Report Box */ +.report-box { + background-color: #FFFFFF; + padding: 20px; + border-radius: 12px; + font-family: 'SF Mono', 'Menlo', 'Monaco', 'Courier New', monospace; + font-size: 0.9em; + white-space: pre-wrap; + border: 1px solid var(--tc-border); + box-shadow: inset 0 1px 3px rgba(30, 42, 58, 0.06); + color: var(--tc-text); +} diff --git a/sl/style_claude.css b/sl/style_claude.css index 567f263..03930db 100644 --- a/sl/style_claude.css +++ b/sl/style_claude.css @@ -231,6 +231,15 @@ h5, h6 { background-color: transparent !important; } +/* The selected day's own indicator (a circular ::after ring BaseWeb pairs + with white text) shares that same pseudo-element with the out-of-month + filler cells above, so it lost its fill too - leaving white-on-transparent + text that's invisible against this theme's light background. Restore a + solid, on-brand circle behind just the selected day. */ +[data-baseweb="calendar"] .st-cj::after { + background-color: var(--tc-accent) !important; +} + /* Checkbox */ [data-testid="stCheckbox"] label p { color: var(--tc-text) !important; diff --git a/sl/style_dark.css b/sl/style_dark.css index 1229943..57c8b89 100644 --- a/sl/style_dark.css +++ b/sl/style_dark.css @@ -202,6 +202,16 @@ h5, h6 { background-color: transparent !important; } +/* The selected day's own indicator (a circular ::after ring BaseWeb pairs + with white text) shares that same pseudo-element with the out-of-month + filler cells above, so it lost its fill too. Text still happens to read + fine here since this theme's regular day text is already light-on-dark, + but give the selected day its own solid, on-brand circle too rather than + relying on that coincidence. */ +[data-baseweb="calendar"] .st-cj::after { + background-color: var(--tc-accent) !important; +} + [data-baseweb="popover"] li[role="option"]:hover, [data-baseweb="menu"] li:hover { background-color: var(--tc-bg-hover) !important;