Source code for klea_utils.ui.web.nicegui.components.theme
#!/usr/bin/env python3
"""
Page theme component: CSS overrides and persistent dark mode.
File: klea_utils/ui/web/nicegui/components/theme.py
Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""
import logging
from nicegui import ui
from klea_utils.ui.web.nicegui.components.context import PageContext
from klea_utils.ui.web.nicegui.components.storage import user_storage_or_none
logger = logging.getLogger(__name__)
def _add_css_overrides() -> None:
"""Install the shared page CSS overrides (flex layout, alerts, panels)."""
# Make q-page a flex container so the nicegui-content can flex-fill
# the available page height, which in turn lets the center column
# grow and pin the input row to the bottom.
ui.add_css(".q-page { display: flex; flex-direction: column; }")
ui.add_css(
".nicegui-content { display: flex; flex-direction: column; flex: 1; min-height: 0; }"
)
# GitHub-style alerts (rendered from ``> [!WARNING]`` etc. by the markdown2
# 'alerts' extra) used for the fallback / best-effort warnings in bubbles.
# No default style defined by nicegui for these extras
ui.add_css(
".nicegui-markdown div.alert { "
"padding: 0.4rem 0.75rem; "
"border-left: 4px solid #d29922; "
"border-radius: 0.25rem; "
"background: rgba(210, 153, 34, 0.12); "
"margin: 0.5rem 0; "
"}"
)
ui.add_css(
".nicegui-markdown div.alert em { font-style: normal; font-weight: 600; }"
)
# Collapse long bot messages to 4 lines with an expand / collapse toggle.
ui.add_css(".msg-collapsed { max-height: 6em; overflow: hidden; }")
ui.add_css(".msg-expanded { max-height: none; }")
ui.add_css(
".inspector-entry > summary { list-style: none; display: flex; align-items: center; gap: 0.25rem; }"
)
ui.add_css(
".inspector-entry > summary::before { content: '\\25B6'; font-size: 0.65rem; margin-right: 0.35rem; transition: transform 0.15s; }"
)
ui.add_css(".inspector-entry[open] > summary::before { content: '\\25BC'; }")
ui.add_css(
".inspector-details summary { list-style: none; display: flex; align-items: center; gap: 0.25rem; }"
)
ui.add_css(
".inspector-details summary::before { content: '\\25B6'; font-size: 0.6rem; margin-right: 0.35rem; }"
)
ui.add_css(".inspector-details[open] summary::before { content: '\\25BC'; }")
ui.add_css(
".inspector-details .md-div { overflow: hidden !important; height: auto !important; }"
)
ui.add_css(
".inspector-details code { white-space: pre-wrap !important; word-break: break-all !important; }"
)
ui.add_css(
".q-tooltip { max-width: 350px !important; overflow: visible !important; white-space: nowrap !important; padding: 4px 8px !important; }"
)
ui.add_css(
".model-tooltip { white-space: pre-wrap !important; max-width: none !important; }"
)
# The chat input must stay pinned to the bottom of the central pane.
# Quasar wraps each tab panel in a `.q-panel` container between
# `.q-tab-panels` and `.q-tab-panel`; it must also flex-fill so the chat
# panel's scroll area can grow and push the input row down.
ui.add_css(
".center-tab-panels > .q-panel { flex: 1; min-height: 0; display: flex; flex-direction: column; }"
)
# Status pane styling --- uses disclosure triangles (same pattern as inspector)
ui.add_css(
".status-entry > summary { list-style: none; display: flex; align-items: center; gap: 0.25rem; }"
)
ui.add_css(
".status-entry > summary::before { content: '\\25B6'; font-size: 0.65rem; margin-right: 0.35rem; transition: transform 0.15s; }"
)
ui.add_css(".status-entry[open] > summary::before { content: '\\25BC'; }")
ui.add_css(
".status-details summary { list-style: none; display: flex; align-items: center; gap: 0.25rem; }"
)
ui.add_css(
".status-details summary::before { content: '\\25B6'; font-size: 0.6rem; margin-right: 0.35rem; }"
)
ui.add_css(".status-details[open] summary::before { content: '\\25BC'; }")
ui.add_css(
".status-details code { white-space: pre-wrap !important; word-break: break-all !important; }"
)
ui.add_css(
".status-entry .nicegui-markdown { overflow: hidden !important; height: auto !important; overflow-wrap: break-word !important; word-break: break-word !important; }"
)
# Keep heading sizes in status pane small so they don't compete with
# the section summary label. Nodes can use # freely without worrying
# about hierarchy.
ui.add_css(
".status-entry .nicegui-markdown h1, .status-entry .nicegui-markdown h2, "
".status-entry .nicegui-markdown h3, .status-entry .nicegui-markdown h4, "
".status-entry .nicegui-markdown h5, .status-entry .nicegui-markdown h6 { "
"font-size: 0.7rem !important; "
"font-weight: 600; "
"margin: 0.15rem 0; "
"line-height: 1.2; }"
)
# Reduce default padding on lists in the status pane (40px is too wide
# at text-xs scale).
ui.add_css(
".status-entry .nicegui-markdown ul, "
".status-entry .nicegui-markdown ol { "
"padding-inline-start: 1rem; }"
)
[docs]
def install_theme(ctx: PageContext) -> None:
"""Install the page CSS overrides and persistent dark mode.
Binds the dark-mode flag to ``app.storage.user["dark_mode"]`` when
storage is available, and stores the resulting ``ui.dark_mode``
element on ``ctx.dark`` for the header toggle.
:param ctx: The shared page context.
"""
_add_css_overrides()
dark = ui.dark_mode()
ctx.dark = dark
store = user_storage_or_none()
if store is not None:
if "dark_mode" not in store:
store["dark_mode"] = False
dark.bind_value(store, "dark_mode")