CoolFace
Apppublic

Kelvin-programmer/pulsecommerce

sourceHugging Faceupdated 2mo agoView on Hugging Face
0likes
theme.py666 linesDownload Raw Back to dashboard
1"""Shared design system for PulseCommerce - a professional BI platform.2 3Light, card-based UI inspired by Stripe / Shopify analytics. Every page imports4`apply_theme()` at the top and uses the helpers below for consistent visuals.5"""6 7from __future__ import annotations8 9from typing import Any, Sequence10 11import plotly.graph_objects as go12import streamlit as st13 14COLORS = {15    # Brand16    "primary": "#4F46E5",          # Indigo17    "primary_dark": "#3730A3",18    "primary_light": "#EEF2FF",19    "accent": "#0EA5E9",            # Sky20    # Neutrals21    "bg": "#F8FAFC",22    "surface": "#FFFFFF",23    "border": "#E5E7EB",24    "border_strong": "#D1D5DB",25    "text": "#0F172A",26    "text_muted": "#475569",27    "text_subtle": "#64748B",28    # Status29    "success": "#10B981",30    "success_bg": "#ECFDF5",31    "warning": "#F59E0B",32    "warning_bg": "#FFFBEB",33    "danger": "#EF4444",34    "danger_bg": "#FEF2F2",35    "info": "#3B82F6",36    "info_bg": "#EFF6FF",37}38 39SEQ_INDIGO = ["#EEF2FF", "#C7D2FE", "#A5B4FC", "#818CF8", "#6366F1", "#4F46E5", "#4338CA", "#3730A3"]40SEQ_TEAL = ["#F0FDFA", "#CCFBF1", "#99F6E4", "#5EEAD4", "#2DD4BF", "#14B8A6", "#0D9488", "#0F766E"]41 42CATEGORICAL = ["#4F46E5", "#0EA5E9", "#10B981", "#F59E0B", "#EC4899", "#8B5CF6", "#14B8A6", "#F97316"]43 44 45def apply_theme(page_title: str, page_icon: str) -> None:46    """Call once at the top of every page."""47    st.set_page_config(48        page_title=f"{page_title} · PulseCommerce",49        page_icon=page_icon,50        layout="wide",51        initial_sidebar_state="auto",52    )53    st.markdown(_GLOBAL_CSS, unsafe_allow_html=True)54    _sync_sidebar_for_viewport()55 56 57def _sync_sidebar_for_viewport() -> None:58    """Keep desktop navigation discoverable without covering mobile content."""59    from streamlit.components.v1 import html as components_html60 61    components_html(62        """63        <script>64          (function() {65            const parentWin = window.parent;66            const parentDoc = parentWin.document;67 68            const isMobile = () => parentWin.innerWidth <= 900;69            const isExpanded = () => {70              const sb = parentDoc.querySelector('section[data-testid="stSidebar"]');71              return sb && sb.getAttribute('aria-expanded') === 'true';72            };73            const collapseSidebar = () => {74              if (!isMobile() || !isExpanded()) return true;75              const btn = parentDoc.querySelector(76                '[data-testid="stSidebarCollapseButton"] button, '77                + '[data-testid="stSidebarCollapseButton"], '78                + 'button[aria-label="Close sidebar"]'79              );80              if (btn) { btn.click(); return true; }81              return false;82            };83 84            let tries = 0;85            const attempt = () => {86              if (collapseSidebar()) return;87              if (++tries < 40) requestAnimationFrame(attempt);88            };89            attempt();90            parentWin.addEventListener('resize', attempt);91            parentWin.addEventListener('load', attempt);92          })();93        </script>94        """,95        height=0,96    )97 98 99_GLOBAL_CSS = f"""100<style>101  @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap');102 103  html, body, [class*="css"]  {{104    font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;105    color: {COLORS['text']};106  }}107 108  /* Page background */109  .stApp {{110    background: {COLORS['bg']};111  }}112 113  /* Main block - fluid width so content expands when the sidebar is collapsed */114  .main .block-container {{115    padding-top: 1.5rem;116    padding-bottom: 4rem;117    padding-left: 2.5rem;118    padding-right: 2.5rem;119    max-width: 100%;120  }}121  @media (min-width: 1600px) {{122    .main .block-container {{123      padding-left: 4rem;124      padding-right: 4rem;125    }}126  }}127 128  /* Hide only the three-dot menu, Deploy button, and footer. */129  #MainMenu {{ display: none !important; }}130  footer {{ display: none !important; }}131  .stDeployButton {{ display: none !important; }}132  header[data-testid="stHeader"] {{133    background: transparent;134    z-index: 100;135  }}136  /* Hide only the toolbar actions (deploy/menu), NOT the sidebar toggle */137  [data-testid="stToolbarActions"] {{ display: none !important; }}138 139  /* Force the "re-open sidebar" button to always be visible and prominent140     so users are never stranded after collapsing the sidebar. */141  [data-testid="stSidebarCollapsedControl"],142  [data-testid="collapsedControl"],143  button[kind="header"][data-testid="baseButton-headerNoPadding"] {{144    visibility: visible !important;145    display: flex !important;146    opacity: 1 !important;147    z-index: 999 !important;148  }}149  [data-testid="stSidebarCollapsedControl"] button,150  [data-testid="collapsedControl"] button {{151    background: {COLORS['primary']} !important;152    color: white !important;153    border-radius: 8px !important;154    box-shadow: 0 2px 8px rgba(79,70,229,0.35) !important;155  }}156  [data-testid="stSidebarCollapsedControl"] button svg,157  [data-testid="collapsedControl"] button svg {{158    color: white !important;159    fill: white !important;160  }}161  /* Sidebar's own collapse chevron - keep obvious */162  [data-testid="stSidebarCollapseButton"] {{163    visibility: visible !important;164    display: flex !important;165  }}166 167  /* Sidebar */168  section[data-testid="stSidebar"] {{169    background: #FFFFFF;170    border-right: 1px solid {COLORS['border']};171  }}172  /* Only pin width when the sidebar is actually expanded, so it can173     collapse to 0 and let main content flush left. */174  section[data-testid="stSidebar"][aria-expanded="true"] {{175    min-width: 280px !important;176    width: 280px !important;177  }}178  /* Ensure the collapsed sidebar has no visible footprint */179  section[data-testid="stSidebar"][aria-expanded="false"] {{180    min-width: 0 !important;181    width: 0 !important;182    border-right: none !important;183  }}184  /* Streamlit's auto page-navigation list */185  [data-testid="stSidebarNav"] {{186    padding-top: 8px;187  }}188  [data-testid="stSidebarNav"] ul {{ padding: 0 8px; }}189  [data-testid="stSidebarNav"] a {{190    color: {COLORS['text']} !important;191    font-weight: 500;192    border-radius: 8px;193    padding: 6px 10px;194  }}195  [data-testid="stSidebarNav"] a:hover {{196    background: {COLORS['primary_light']};197  }}198 199  section[data-testid="stSidebar"] .stRadio label,200  section[data-testid="stSidebar"] p {{201    color: {COLORS['text']} !important;202  }}203 204  /* Typography */205  h1 {{206    font-weight: 700 !important;207    font-size: 1.75rem !important;208    letter-spacing: -0.02em;209    color: {COLORS['text']} !important;210    margin-bottom: 0.25rem !important;211  }}212  h2 {{213    font-weight: 600 !important;214    font-size: 1.25rem !important;215    letter-spacing: -0.01em;216    color: {COLORS['text']} !important;217  }}218  h3, h4 {{219    font-weight: 600 !important;220    color: {COLORS['text']} !important;221  }}222 223  /* KPI metric override */224  [data-testid="stMetric"] {{225    background: {COLORS['surface']};226    border: 1px solid {COLORS['border']};227    border-radius: 12px;228    padding: 18px 20px;229    box-shadow: 0 1px 2px rgba(15,23,42,0.04);230  }}231  [data-testid="stMetricLabel"] {{232    font-size: 0.8rem !important;233    color: {COLORS['text_muted']} !important;234    font-weight: 500;235    text-transform: uppercase;236    letter-spacing: 0.04em;237    white-space: normal !important;238    overflow: visible !important;239    text-overflow: clip !important;240  }}241  [data-testid="stMetricValue"] {{242    font-size: 1.5rem !important;243    font-weight: 700 !important;244    color: {COLORS['text']} !important;245    line-height: 1.1;246    white-space: normal !important;247    overflow: visible !important;248    text-overflow: clip !important;249    overflow-wrap: anywhere;250  }}251  [data-testid="stMetricDelta"] {{252    font-size: 0.8rem !important;253    font-weight: 500 !important;254  }}255 256  /* Divider */257  hr {{ border-top: 1px solid {COLORS['border']} !important; }}258 259  /* Tabs */260  .stTabs [data-baseweb="tab-list"] {{261    gap: 4px;262    border-bottom: 1px solid {COLORS['border']};263  }}264  .stTabs [data-baseweb="tab"] {{265    background: transparent;266    border: none;267    color: {COLORS['text_muted']};268    font-weight: 500;269    padding: 10px 16px;270  }}271  .stTabs [aria-selected="true"] {{272    color: {COLORS['primary']} !important;273    border-bottom: 2px solid {COLORS['primary']} !important;274  }}275 276  /* DataFrame */277  .stDataFrame {{278    border: 1px solid {COLORS['border']};279    border-radius: 10px;280    overflow: hidden;281    background: {COLORS['surface']} !important;282  }}283 284  /* Plotly chart containers */285  .js-plotly-plot {{ border-radius: 8px; }}286 287  /* Buttons */288  .stButton > button {{289    background: {COLORS['primary']};290    color: white;291    border: none;292    border-radius: 8px;293    font-weight: 500;294    padding: 8px 16px;295    transition: all 0.15s;296  }}297  .stButton > button:hover {{298    background: {COLORS['primary_dark']};299    transform: translateY(-1px);300  }}301 302  /* Selectbox / slider labels */303  .stSelectbox label, .stSlider label {{304    color: {COLORS['text_muted']} !important;305    font-weight: 500;306    font-size: 0.85rem;307  }}308 309  /* Caption */310  .stCaption, [data-testid="stCaptionContainer"] {{311    color: {COLORS['text_muted']} !important;312  }}313 314  /* Card container helper */315  .pc-card {{316    background: {COLORS['surface']};317    border: 1px solid {COLORS['border']};318    border-radius: 12px;319    padding: 20px 22px;320    box-shadow: 0 1px 2px rgba(15,23,42,0.04);321    margin-bottom: 16px;322  }}323  .pc-card-title {{324    font-size: 0.95rem;325    font-weight: 600;326    color: {COLORS['text']};327    margin-bottom: 4px;328  }}329  .pc-card-sub {{330    font-size: 0.8rem;331    color: {COLORS['text_muted']};332    margin-bottom: 14px;333  }}334 335  /* Page hero */336  .pc-hero {{337    background: linear-gradient(135deg, #EEF2FF 0%, #F5F3FF 100%);338    border: 1px solid {COLORS['border']};339    border-radius: 14px;340    padding: 22px 26px;341    margin-bottom: 22px;342  }}343  .pc-hero-eyebrow {{344    font-size: 0.72rem;345    font-weight: 600;346    letter-spacing: 0.12em;347    color: {COLORS['primary']};348    text-transform: uppercase;349  }}350  .pc-hero-title {{351    font-size: 1.65rem;352    font-weight: 700;353    color: {COLORS['text']};354    margin: 4px 0 6px 0;355    letter-spacing: -0.02em;356  }}357  .pc-hero-sub {{358    font-size: 0.95rem;359    color: {COLORS['text_muted']};360    max-width: 780px;361  }}362 363  @media (max-width: 900px) {{364    .main .block-container {{365      padding-top: 1.1rem !important;366      padding-left: 1rem !important;367      padding-right: 1rem !important;368      padding-bottom: 2.5rem !important;369    }}370    section[data-testid="stSidebar"][aria-expanded="true"] {{371      max-width: min(82vw, 300px) !important;372      min-width: min(82vw, 300px) !important;373      width: min(82vw, 300px) !important;374    }}375    .pc-hero {{376      padding: 18px 20px;377      margin-bottom: 18px;378    }}379    .pc-hero-title {{380      font-size: 1.45rem;381      line-height: 1.18;382    }}383    .pc-hero-sub {{384      font-size: 0.9rem;385      line-height: 1.55;386    }}387    [data-testid="stMetric"] {{388      padding: 14px 16px;389      min-height: 112px;390    }}391    [data-testid="stMetricLabel"] {{392      font-size: 0.74rem !important;393      line-height: 1.25 !important;394    }}395    [data-testid="stMetricValue"] {{396      font-size: 1.35rem !important;397      line-height: 1.12 !important;398    }}399    .stTabs [data-baseweb="tab-list"] {{400      overflow-x: auto;401      flex-wrap: nowrap;402    }}403    .stTabs [data-baseweb="tab"] {{404      white-space: nowrap;405      padding: 9px 12px;406    }}407  }}408 409  @media (max-width: 520px) {{410    .main .block-container {{411      padding-left: 0.85rem !important;412      padding-right: 0.85rem !important;413    }}414    .pc-card,415    .pc-hero,416    .pc-insight {{417      border-radius: 10px;418    }}419  }}420 421  /* Insight card */422  .pc-insight {{423    background: {COLORS['surface']};424    border: 1px solid {COLORS['border']};425    border-left: 3px solid {COLORS['primary']};426    border-radius: 10px;427    padding: 14px 18px;428    margin-bottom: 10px;429  }}430  .pc-insight.success {{ border-left-color: {COLORS['success']}; }}431  .pc-insight.warning {{ border-left-color: {COLORS['warning']}; }}432  .pc-insight.danger  {{ border-left-color: {COLORS['danger']}; }}433  .pc-insight-title {{434    font-weight: 600;435    font-size: 0.9rem;436    color: {COLORS['text']};437    margin-bottom: 2px;438  }}439  .pc-insight-body {{440    color: {COLORS['text_muted']};441    font-size: 0.88rem;442    line-height: 1.5;443  }}444 445  /* Badges */446  .pc-badge {{447    display: inline-block;448    padding: 2px 10px;449    border-radius: 999px;450    font-size: 0.72rem;451    font-weight: 600;452    letter-spacing: 0.02em;453  }}454  .pc-badge.success {{ background: {COLORS['success_bg']}; color: #065F46; }}455  .pc-badge.warning {{ background: {COLORS['warning_bg']}; color: #92400E; }}456  .pc-badge.danger  {{ background: {COLORS['danger_bg']};  color: #991B1B; }}457  .pc-badge.info    {{ background: {COLORS['info_bg']};    color: #1E40AF; }}458  .pc-badge.neutral {{ background: #F1F5F9; color: {COLORS['text_muted']}; }}459 460  /* Verdict banner */461  .pc-verdict {{462    border-radius: 14px;463    padding: 22px 26px;464    margin-bottom: 22px;465    border: 1px solid transparent;466  }}467  .pc-verdict .label {{468    font-size: 0.72rem;469    letter-spacing: 0.14em;470    text-transform: uppercase;471    font-weight: 700;472    opacity: 0.8;473  }}474  .pc-verdict .title {{475    font-size: 1.9rem;476    font-weight: 700;477    letter-spacing: -0.02em;478    margin: 2px 0 6px 0;479  }}480  .pc-verdict .sub {{481    font-size: 0.95rem;482    opacity: 0.85;483  }}484  .pc-verdict.ship    {{ background: {COLORS['success_bg']}; border-color: #A7F3D0; color: #064E3B; }}485  .pc-verdict.iterate {{ background: {COLORS['warning_bg']}; border-color: #FDE68A; color: #78350F; }}486  .pc-verdict.reject  {{ background: {COLORS['danger_bg']};  border-color: #FECACA; color: #7F1D1D; }}487 488  /* Nav pills (Home) */489  .pc-navgrid {{490    display: grid;491    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));492    gap: 14px;493    margin-top: 10px;494  }}495  .pc-navcard {{496    background: {COLORS['surface']};497    border: 1px solid {COLORS['border']};498    border-radius: 12px;499    padding: 16px 18px;500    transition: all 0.15s;501  }}502  .pc-navcard:hover {{503    border-color: {COLORS['primary']};504    transform: translateY(-1px);505    box-shadow: 0 4px 10px rgba(79,70,229,0.08);506  }}507  .pc-navcard .n {{508    font-size: 0.7rem;509    font-weight: 700;510    color: {COLORS['primary']};511    letter-spacing: 0.12em;512  }}513  .pc-navcard .t {{514    font-weight: 600;515    font-size: 1rem;516    margin: 2px 0 4px 0;517    color: {COLORS['text']};518  }}519  .pc-navcard .d {{520    font-size: 0.82rem;521    color: {COLORS['text_muted']};522    line-height: 1.4;523  }}524 525  /* Section header (title + subtitle) */526  .pc-section-title {{527    font-size: 1.05rem;528    font-weight: 600;529    color: {COLORS['text']};530    margin-top: 18px;531    margin-bottom: 2px;532  }}533  .pc-section-sub {{534    font-size: 0.85rem;535    color: {COLORS['text_muted']};536    margin-bottom: 14px;537  }}538</style>539"""540 541 542def plotly_layout(**overrides: Any) -> dict[str, Any]:543    """Return a clean white-theme layout dict for Plotly figures."""544    base: dict[str, Any] = dict(545        template="plotly_white",546        font=dict(family="Inter, -apple-system, sans-serif", size=12, color=COLORS["text"]),547        paper_bgcolor=COLORS["surface"],548        plot_bgcolor=COLORS["surface"],549        margin=dict(l=20, r=20, t=40, b=20),550        xaxis=dict(551            showgrid=False,552            showline=False,553            linecolor=COLORS["border"],554            tickfont=dict(color=COLORS["text_muted"], size=11),555        ),556        yaxis=dict(557            showgrid=True,558            gridcolor=COLORS["border"],559            gridwidth=1,560            showline=False,561            tickfont=dict(color=COLORS["text_muted"], size=11),562        ),563        colorway=CATEGORICAL,564        hoverlabel=dict(bgcolor="white", font_size=12, font_family="Inter",565                        bordercolor=COLORS["border"]),566        legend=dict(bgcolor="rgba(255,255,255,0)", bordercolor=COLORS["border"],567                    font=dict(color=COLORS["text_muted"], size=11)),568    )569    base.update(overrides)570    return base571 572 573def style_fig(fig: go.Figure, **overrides: Any) -> go.Figure:574    fig.update_layout(**plotly_layout(**overrides))575    return fig576 577 578def hero(eyebrow: str, title: str, subtitle: str) -> None:579    st.markdown(580        f"""581        <div class="pc-hero">582          <div class="pc-hero-eyebrow">{eyebrow}</div>583          <div class="pc-hero-title">{title}</div>584          <div class="pc-hero-sub">{subtitle}</div>585        </div>586        """,587        unsafe_allow_html=True,588    )589 590 591def section(title: str, subtitle: str = "") -> None:592    sub = f'<div class="pc-section-sub">{subtitle}</div>' if subtitle else ""593    st.markdown(594        f'<div class="pc-section-title">{title}</div>{sub}',595        unsafe_allow_html=True,596    )597 598 599def insight_card(title: str, body: str, tone: str = "") -> None:600    cls = f"pc-insight {tone}" if tone else "pc-insight"601    st.markdown(602        f'<div class="{cls}"><div class="pc-insight-title">{title}</div>'603        f'<div class="pc-insight-body">{body}</div></div>',604        unsafe_allow_html=True,605    )606 607 608def badge(text: str, tone: str = "neutral") -> str:609    return f'<span class="pc-badge {tone}">{text}</span>'610 611 612def verdict_banner(recommendation: str, rationale: str) -> None:613    rec = (recommendation or "").lower()614    mapping = {615        "ship": ("ship", "✓ SHIP", "Clear win on primary metric with guardrails intact."),616        "iterate": ("iterate", "◐ ITERATE", "Directional but not conclusive. Needs more data or refinement."),617        "reject": ("reject", "✕ REJECT", "Primary metric did not move, or a guardrail was breached."),618    }619    tone, title, default_sub = mapping.get(rec, ("iterate", rec.upper() or "-", ""))620    st.markdown(621        f"""622        <div class="pc-verdict {tone}">623          <div class="label">Experiment Verdict</div>624          <div class="title">{title}</div>625          <div class="sub">{rationale or default_sub}</div>626        </div>627        """,628        unsafe_allow_html=True,629    )630 631 632def kpi_row(items: Sequence[dict[str, Any]]) -> None:633    """Render a row of KPI cards.634 635    Each item: {label, value, delta (optional), delta_tone (optional: 'success'/'danger'/'neutral')}.636    """637    cols = st.columns(len(items))638    for col, item in zip(cols, items, strict=False):639        delta = item.get("delta")640        if delta is None:641            col.metric(item["label"], item["value"])642        else:643            col.metric(item["label"], item["value"], delta)644 645 646def sidebar_brand() -> None:647    """Brand mark at the top of the sidebar."""648    with st.sidebar:649        st.markdown(650            f"""651            <div style="padding: 8px 4px 16px 4px; border-bottom: 1px solid {COLORS['border']}; margin-bottom: 12px;">652              <div style="display: flex; align-items: center; gap: 10px;">653                <div style="width: 34px; height: 34px; border-radius: 9px;654                            background: linear-gradient(135deg, #4F46E5, #7C3AED);655                            display:flex; align-items:center; justify-content:center;656                            color:white; font-weight:700; font-size:18px;">P</div>657                <div>658                  <div style="font-weight:700; font-size:1.02rem; color:{COLORS['text']}; line-height:1.1;">PulseCommerce</div>659                  <div style="font-size:0.72rem; color:{COLORS['text_muted']};">Commerce Intelligence</div>660                </div>661              </div>662            </div>663            """,664            unsafe_allow_html=True,665        )666