Spaces:
Running on Zero
Running on Zero
| """Visual identity for the QC operator console. | |
| DESIGN INTENT. This is instrument software, not a consumer app. The reference | |
| points are laboratory and process-control interfaces: dark neutral ground, | |
| high-contrast data, monospace for anything a reader might transcribe (cell IDs, | |
| cycle counts, costs), and a clean sans-serif for prose. | |
| THE COLOUR RULE, which is load-bearing rather than aesthetic. Green, amber and | |
| red carry DECISION meaning -- accept, continue, reject -- and are used for | |
| nothing else anywhere in the interface. A chart axis, a section header or a | |
| hover state never uses them. An operator must be able to glance at a panel and | |
| read its colour as a decision without checking what the colour refers to. | |
| Data series therefore use the neutral/blue/violet ramp, and the one exception is | |
| the campaign-shift alarm state, which is deliberately red because it IS a | |
| decision: do not trust this result. | |
| """ | |
| from __future__ import annotations | |
| import gradio as gr | |
| # --- ground and structure --------------------------------------------------- | |
| BG = "#0d1117" # page | |
| PANEL = "#161b22" # cards | |
| PANEL_RAISED = "#1c2430" # inputs, table headers | |
| BORDER = "#30363d" | |
| BORDER_STRONG = "#484f58" | |
| # --- type ------------------------------------------------------------------- | |
| TEXT = "#e6edf3" | |
| TEXT_MUTED = "#8b949e" | |
| TEXT_DIM = "#6e7681" | |
| # --- data (never used for decisions) ---------------------------------------- | |
| DATA = "#58a6ff" # primary series | |
| DATA_ALT = "#a371f7" # secondary series | |
| DATA_BAND = "#1f6feb" # uncertainty bands | |
| GRID = "#21262d" | |
| # --- decision semantics (used for NOTHING else) ----------------------------- | |
| ACCEPT = "#3fb950" | |
| CONTINUE = "#d29922" | |
| REJECT = "#f85149" | |
| ALARM = "#f85149" | |
| DECISION_COLOURS = {"A": ACCEPT, "B": CONTINUE, "C": REJECT, "CONTINUE": CONTINUE} | |
| MONO = "'JetBrains Mono', 'SF Mono', 'Cascadia Mono', Consolas, monospace" | |
| SANS = "'Inter', -apple-system, 'Segoe UI', Roboto, sans-serif" | |
| def build_theme() -> gr.Theme: | |
| """A restrained technical theme, not the Gradio default.""" | |
| return gr.themes.Base( | |
| primary_hue=gr.themes.colors.blue, | |
| secondary_hue=gr.themes.colors.slate, | |
| neutral_hue=gr.themes.colors.slate, | |
| font=[gr.themes.GoogleFont("Inter"), "system-ui", "sans-serif"], | |
| font_mono=[gr.themes.GoogleFont("JetBrains Mono"), "Consolas", "monospace"], | |
| ).set( | |
| body_background_fill=BG, | |
| body_text_color=TEXT, | |
| background_fill_primary=PANEL, | |
| background_fill_secondary=BG, | |
| block_background_fill=PANEL, | |
| block_border_color=BORDER, | |
| block_border_width="1px", | |
| block_label_background_fill=PANEL, | |
| block_label_text_color=TEXT_MUTED, | |
| block_title_text_color=TEXT, | |
| border_color_primary=BORDER, | |
| input_background_fill=PANEL_RAISED, | |
| input_border_color=BORDER, | |
| button_primary_background_fill=DATA_BAND, | |
| button_primary_text_color="#ffffff", | |
| button_secondary_background_fill=PANEL_RAISED, | |
| button_secondary_text_color=TEXT, | |
| panel_background_fill=PANEL, | |
| table_odd_background_fill=PANEL, | |
| table_even_background_fill=PANEL_RAISED, | |
| ) | |
| CSS = f""" | |
| :root {{ | |
| --ct-bg: {BG}; --ct-panel: {PANEL}; --ct-raised: {PANEL_RAISED}; | |
| --ct-border: {BORDER}; --ct-text: {TEXT}; --ct-muted: {TEXT_MUTED}; | |
| --ct-accept: {ACCEPT}; --ct-continue: {CONTINUE}; --ct-reject: {REJECT}; | |
| --ct-data: {DATA}; | |
| }} | |
| .gradio-container {{ max-width: 1400px !important; }} | |
| /* ---------- masthead ---------- */ | |
| .ct-masthead {{ | |
| border-bottom: 1px solid var(--ct-border); | |
| padding: 0 0 14px 0; margin-bottom: 6px; | |
| }} | |
| .ct-masthead h1 {{ | |
| font-family: {MONO}; font-size: 1.45rem; font-weight: 600; | |
| letter-spacing: 0.02em; color: var(--ct-text); margin: 0 0 4px 0; | |
| }} | |
| .ct-masthead .ct-sub {{ | |
| color: var(--ct-muted); font-size: 0.86rem; margin: 0; | |
| }} | |
| .ct-masthead .ct-build {{ | |
| font-family: {MONO}; font-size: 0.72rem; color: {TEXT_DIM}; margin-top: 6px; | |
| }} | |
| /* ---------- decision badge ---------- */ | |
| .ct-decision {{ | |
| border-radius: 6px; padding: 18px 22px; margin-bottom: 10px; | |
| border-left: 5px solid var(--ct-border); background: var(--ct-raised); | |
| }} | |
| .ct-decision .ct-label {{ | |
| font-size: 0.7rem; letter-spacing: 0.16em; text-transform: uppercase; | |
| color: var(--ct-muted); margin-bottom: 6px; | |
| }} | |
| .ct-decision .ct-verdict {{ | |
| font-family: {MONO}; font-size: 1.9rem; font-weight: 700; line-height: 1.1; | |
| }} | |
| .ct-decision .ct-tier {{ color: var(--ct-muted); font-size: 0.9rem; margin-top: 4px; }} | |
| .ct-accept {{ border-left-color: var(--ct-accept); }} | |
| .ct-accept .ct-verdict {{ color: var(--ct-accept); }} | |
| .ct-continue {{ border-left-color: var(--ct-continue); }} | |
| .ct-continue .ct-verdict {{ color: var(--ct-continue); }} | |
| .ct-reject {{ border-left-color: var(--ct-reject); }} | |
| .ct-reject .ct-verdict {{ color: var(--ct-reject); }} | |
| /* ---------- readouts ---------- */ | |
| .ct-readout {{ | |
| display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); | |
| gap: 1px; background: var(--ct-border); border: 1px solid var(--ct-border); | |
| border-radius: 6px; overflow: hidden; margin-bottom: 10px; | |
| }} | |
| .ct-readout .ct-cell {{ background: var(--ct-panel); padding: 12px 14px; }} | |
| .ct-readout .ct-k {{ | |
| font-size: 0.66rem; letter-spacing: 0.12em; text-transform: uppercase; | |
| color: var(--ct-muted); margin-bottom: 5px; | |
| }} | |
| .ct-readout .ct-v {{ | |
| font-family: {MONO}; font-size: 1.15rem; color: var(--ct-text); font-weight: 600; | |
| }} | |
| .ct-readout .ct-u {{ font-size: 0.72rem; color: {TEXT_DIM}; margin-left: 3px; }} | |
| /* ---------- campaign-shift alarm: a state change, not a footnote ---------- */ | |
| .ct-alarm {{ | |
| border: 1px solid var(--ct-reject); | |
| border-left: 5px solid var(--ct-reject); | |
| background: rgba(248, 81, 73, 0.09); | |
| border-radius: 6px; padding: 16px 20px; margin-bottom: 12px; | |
| }} | |
| .ct-alarm .ct-alarm-title {{ | |
| font-family: {MONO}; font-weight: 700; color: var(--ct-reject); | |
| font-size: 0.95rem; letter-spacing: 0.04em; margin-bottom: 8px; | |
| }} | |
| .ct-alarm p {{ color: var(--ct-text); font-size: 0.85rem; margin: 4px 0; line-height: 1.5; }} | |
| .ct-alarm code {{ font-family: {MONO}; color: var(--ct-reject); }} | |
| .ct-nominal {{ | |
| border: 1px solid var(--ct-border); border-left: 5px solid var(--ct-accept); | |
| background: rgba(63, 185, 80, 0.06); | |
| border-radius: 6px; padding: 12px 18px; margin-bottom: 12px; | |
| }} | |
| .ct-nominal .ct-alarm-title {{ | |
| font-family: {MONO}; font-weight: 600; color: var(--ct-accept); font-size: 0.85rem; | |
| }} | |
| .ct-nominal p {{ color: var(--ct-muted); font-size: 0.8rem; margin: 4px 0; }} | |
| /* ---------- takeaway strip under each figure ---------- */ | |
| .ct-takeaway {{ | |
| border-left: 3px solid var(--ct-data); background: var(--ct-raised); | |
| padding: 10px 14px; margin: 4px 0 16px 0; border-radius: 0 4px 4px 0; | |
| color: var(--ct-text); font-size: 0.84rem; line-height: 1.5; | |
| }} | |
| .ct-takeaway strong {{ color: var(--ct-data); }} | |
| /* ---------- limitations ---------- */ | |
| .ct-limit {{ | |
| border: 1px solid var(--ct-border); border-radius: 6px; | |
| padding: 16px 20px; background: var(--ct-panel); margin-bottom: 10px; | |
| }} | |
| .ct-limit h3 {{ | |
| font-family: {MONO}; font-size: 0.9rem; color: var(--ct-text); | |
| margin: 0 0 10px 0; letter-spacing: 0.03em; | |
| }} | |
| .ct-limit ul {{ margin: 0; padding-left: 18px; }} | |
| .ct-limit li {{ color: var(--ct-muted); font-size: 0.84rem; margin-bottom: 7px; line-height: 1.5; }} | |
| .ct-limit li strong {{ color: var(--ct-text); }} | |
| /* ---------- figure provenance line ---------- */ | |
| .ct-source {{ | |
| font-family: {MONO}; font-size: 0.7rem; color: {TEXT_DIM}; | |
| margin: -8px 0 22px 3px; letter-spacing: 0.02em; | |
| }} | |
| .ct-mono {{ font-family: {MONO}; }} | |
| footer {{ display: none !important; }} | |
| """ | |
| def matplotlib_style() -> dict: | |
| """rcParams matching the console, so figures look native rather than pasted in.""" | |
| return { | |
| "figure.facecolor": PANEL, | |
| "axes.facecolor": PANEL, | |
| "savefig.facecolor": PANEL, | |
| "text.color": TEXT, | |
| "axes.labelcolor": TEXT_MUTED, | |
| "axes.edgecolor": BORDER_STRONG, | |
| "xtick.color": TEXT_MUTED, | |
| "ytick.color": TEXT_MUTED, | |
| "grid.color": GRID, | |
| "axes.grid": True, | |
| "grid.alpha": 0.6, | |
| "axes.titlecolor": TEXT, | |
| "font.size": 9, | |
| "axes.titlesize": 10, | |
| "legend.frameon": False, | |
| "legend.labelcolor": TEXT_MUTED, | |
| "figure.dpi": 110, | |
| } | |