""" Registry sidebar component — clean light theme. """ from __future__ import annotations from typing import TYPE_CHECKING import panel as pn import param from core.models.sequence import mRNASequence if TYPE_CHECKING: from ui.state import AppState # ── Colours (mirror THEME from app.py without importing it to avoid circulars) _SB = "#FFFFFF" # white sidebar bg _SEP = "#E2E8F0" # slate-200 separator _TEXT = "#334155" # slate-700 — readable on white _HEAD = "#0F172A" # slate-950 heading _ACTIVE_BG = "#F0FDFA" # teal-50 selection _ACTIVE_TEXT = "#0F766E" # teal-600 active text _LOCAL_DOT = "#059669" # emerald-600 — local _DB_DOT = "#0F766E" # teal-600 — database _SECTION = "#64748B" # slate-500 — section labels class RegistrySidebar(param.Parameterized): def __init__(self, state: "AppState", **params: object) -> None: super().__init__(**params) self._state = state def _section_label(self, text: str) -> pn.pane.HTML: return pn.pane.HTML( f'
' f'{text}
', sizing_mode="stretch_width", margin=0, ) @param.depends("_state.worklist", "_state.db_connections", "_state.model_registry", "_state.backbone_library", "_state.worklists", "_state.active_worklist_index", "_state.active_tab") def panel(self) -> pn.Column: children = [] # ── Context label ──────────────────────────────────────────────── children.append(pn.pane.HTML( f'
REGISTRY
', sizing_mode="stretch_width", margin=0, )) # ── WORKLIST section ───────────────────────────────────────────── children.append(self._section_label("Worklists")) # Show all worklists if multiple exist if self._state.worklists: for i, wl in enumerate(self._state.worklists): is_active = (i == self._state.active_worklist_index) btn = pn.widgets.Button( name=f"{wl.name} ({wl.count})", button_type="light", sizing_mode="stretch_width", margin=(3, 10), stylesheets=[f""" :host .bk-btn {{ background: {_ACTIVE_BG if is_active else 'transparent'}; color: {_ACTIVE_TEXT if is_active else _TEXT}; border: {'1px solid #99F6E4' if is_active else '1px solid transparent'}; border-radius: 4px; font-size: 12px; text-align: left; padding: 7px 12px; font-weight: {'600' if is_active else '400'}; }} :host .bk-btn:hover {{ background: {_ACTIVE_BG}; }} """], ) def _switch_worklist(event, idx=i): self._state.active_worklist_index = idx self._state.worklist = self._state.worklists[idx] self._state.active_tab = "worklist" btn.on_click(_switch_worklist) children.append(btn) elif self._state.worklist and self._state.worklist.count > 0: children.append(self._worklist_button()) else: children.append(pn.pane.HTML( f'
' f'No worklist loaded
', sizing_mode="stretch_width", margin=0, )) # ── DATABASES section ──────────────────────────────────────────── children.append(self._section_label("Databases")) if self._state.db_connections: for db_name in self._state.db_connections.keys(): db_btn = pn.widgets.Button( name=f"{db_name}", button_type="light", sizing_mode="stretch_width", margin=(3, 10), stylesheets=[f""" :host .bk-btn {{ background: transparent; color: {_TEXT}; border: 1px solid transparent; border-radius: 4px; font-size: 12px; text-align: left; padding: 7px 12px; }} :host .bk-btn:hover {{ background: #F8FAFC; }} """], ) children.append(db_btn) else: children.append(pn.pane.HTML( f'
' f'No databases connected
', sizing_mode="stretch_width", margin=0, )) # ── MODELS section ─────────────────────────────────────────────── children.append(self._section_label("Models")) if self._state.model_registry and len(self._state.model_registry.all_models) > 0: for model_reg in self._state.model_registry.all_models: model_name = model_reg.model.name if model_reg.model_type == "scoring": type_color, type_label = "#0284C7", "ANALYTICAL" else: type_color, type_label = "#7C3AED", "GENERATIVE" if model_reg.source == "catalog": src_color, src_label = "#059669", "IMPORTED" elif model_reg.source == "api": src_color, src_label = "#D97706", "API" else: src_color, src_label = "#0F766E", "BUILTIN" repo_line = "" if model_reg.repository: repo_line = ( f'
via {model_reg.repository}
' ) children.append(pn.pane.HTML( f'
' f'
{model_name}
' f'
' f'{type_label} ' f'{src_label}' f'
{repo_line}
', sizing_mode="stretch_width", margin=0, )) else: children.append(pn.pane.HTML( f'
' f'No models loaded
', sizing_mode="stretch_width", margin=0, )) # ── BACKBONES section ──────────────────────────────────────────── children.append(self._section_label("Backbones")) if self._state.backbone_library: for bb in self._state.backbone_library: children.append(pn.pane.HTML( f'
' f'
{bb.name}
' f'
{bb.length:,} bp
' f'
', sizing_mode="stretch_width", margin=0, )) else: children.append(pn.pane.HTML( f'
' f'No backbones imported
', sizing_mode="stretch_width", margin=0, )) return pn.Column( *children, sizing_mode="stretch_width", styles={ "background": _SB, "min-height": "100vh", "overflow-y": "auto", "border-right": f"1px solid {_SEP}", "padding-bottom": "24px", }, margin=0, ) def _worklist_button(self) -> pn.widgets.Button: count = self._state.worklist.count name = self._state.worklist.name btn = pn.widgets.Button( name=f"{name} ({count} sequences)", button_type="light", sizing_mode="stretch_width", margin=(4, 10), stylesheets=[f""" :host .bk-btn-light .bk-btn {{ background: {_ACTIVE_BG}; color: {_ACTIVE_TEXT}; border: 1px solid #99F6E4; border-radius: 6px; font-size: 12px; font-weight: 600; padding: 8px 12px; text-align: left; }} :host .bk-btn-light .bk-btn:hover {{ background: #CCFBF1; }} """], ) def _go_to_worklist(event: object) -> None: self._state.active_tab = "worklist" btn.on_click(_go_to_worklist) return btn