Source code for klea_utils.ui.web.nicegui.components.choice

#!/usr/bin/env python3
"""
Compact single-choice control for the NiceGUI status pane.

A ``q-btn-group`` (connected buttons on one line) is used rather than
``ui.toggle`` because each option can carry its own active colour (for
example destructive = red, scientific = green), which a ``q-btn-toggle``
cannot express.  Labels and button groups use fixed widths so that stacked
controls line up regardless of their text lengths.

File: klea_utils/ui/web/nicegui/components/choice.py

Copyright 2026 Ankur Sinha
Author: Ankur Sinha <sanjay DOT ankur AT gmail DOT com>
"""

from collections.abc import Callable

from nicegui import ui

#: Fixed width for the row label (fits the longest expected label, e.g.
#: ``"Project root:"``), so labels of different lengths stay aligned.
DEFAULT_LABEL_WIDTH = "13ch"

#: Fixed width for the button group, so controls with different option
#: lengths (e.g. ``General | Scientific`` vs ``Full | Read-only``) match.
DEFAULT_GROUP_WIDTH = "10rem"


[docs] def choice_buttons( label: str, options: dict[str, str], selected: str, on_select: Callable[[str], None], *, colors: dict[str, str] | None = None, tooltips: dict[str, str] | None = None, info: str = "", label_width: str = DEFAULT_LABEL_WIDTH, group_width: str = DEFAULT_GROUP_WIDTH, ) -> None: """Render a compact, aligned segmented single-choice control. The selected option is filled with its colour (or ``primary``) and white text; the others are drawn as outlines. Buttons are ``dense``/``size=sm`` and share the group width equally, so the control stays close to the surrounding label text and two stacked controls align. The ``choice-row`` / ``choice-label`` / ``choice-btn`` classes are styled in :mod:`klea_utils.ui.web.nicegui.components.theme` so the inactive option stays legible in dark mode (its outline follows its text colour). :param label: Row label (e.g. ``"Mode:"``). :param options: ``{value: label}`` mapping; iteration order is display order. :param selected: The currently selected value. :param on_select: Callback invoked with the newly selected value. :param colors: Optional ``{value: quasar-colour}`` for the active state (e.g. ``{"full": "red-5", "read_only": "green-5"}``). :param tooltips: Optional ``{value: text}`` tooltips per option. :param info: Optional explanatory tooltip attached to the whole group (e.g. an operating-mode downgrade note). :param label_width: CSS width for the label (default :data:`DEFAULT_LABEL_WIDTH`); pass the same value for stacked controls to align them. :param group_width: CSS width for the button group (default :data:`DEFAULT_GROUP_WIDTH`); pass the same value for stacked controls to align them. """ colors = colors or {} tooltips = tooltips or {} with ui.row().classes("items-center w-full gap-2 choice-row"): ui.label(label).classes("text-xs font-bold choice-label").style( f"min-width: {label_width}" ) group = ( ui.button_group() .props("dense") .classes("text-xs") .style(f"width: {group_width}") ) with group: for value, text in options.items(): active = value == selected props = ( f"dense size=sm no-caps color={colors.get(value, 'primary')} " "text-color=white" if active else "dense size=sm no-caps outline" ) classes = "flex-1 choice-btn" + ( " choice-btn--active" if active else "" ) with ( ui.button(text, on_click=lambda v=value: on_select(v)) .props(props) .classes(classes) ): tip = tooltips.get(value) if tip: ui.tooltip(tip) if info: with group: ui.tooltip(info)