| """Design system & reusable UI components for Hostel MS.""" | |
| import streamlit as st | |
| PALETTE = { | |
| "primary": "#6366F1", | |
| "primary_light": "#818CF8", | |
| "accent": "#A855F7", | |
| "success": "#10B981", | |
| "warning": "#F59E0B", | |
| "danger": "#EF4444", | |
| "surface": "#1E293B", | |
| "surface_alt": "#334155", | |
| "bg": "#0B1220", | |
| "text": "#F8FAFC", | |
| "muted": "#94A3B8", | |
| } | |
| def inject_theme(): | |
| st.markdown( | |
| f""" | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Plus+Jakarta+Sans:wght@400;500;600;700;800&display=swap'); | |
| :root {{ | |
| --primary: {PALETTE['primary']}; | |
| --accent: {PALETTE['accent']}; | |
| --surface: {PALETTE['surface']}; | |
| --bg: {PALETTE['bg']}; | |
| --text: {PALETTE['text']}; | |
| --muted: {PALETTE['muted']}; | |
| }} | |
| html, body, [class*="css"] {{ | |
| font-family: 'Plus Jakarta Sans', sans-serif; | |
| }} | |
| .stApp {{ | |
| background: radial-gradient(ellipse 120% 80% at 50% -20%, rgba(99,102,241,0.22), transparent), | |
| radial-gradient(ellipse 80% 50% at 100% 50%, rgba(168,85,247,0.1), transparent), | |
| radial-gradient(ellipse 60% 40% at 0% 80%, rgba(16,185,129,0.06), transparent), | |
| linear-gradient(180deg, #0B1220 0%, #0F172A 50%, #0B1220 100%); | |
| }} | |
| .block-container {{ | |
| padding-top: 1.25rem; | |
| max-width: 1320px; | |
| }} | |
| div[data-testid="stSidebar"] {{ | |
| background: linear-gradient(180deg, #0B1220 0%, #1E293B 100%); | |
| border-right: 1px solid rgba(255,255,255,0.06); | |
| }} | |
| div[data-testid="stSidebar"] > div:first-child {{ | |
| padding-top: 1.5rem; | |
| }} | |
| .sidebar-brand {{ | |
| text-align: center; | |
| padding: 0.5rem 0 1rem; | |
| }} | |
| .sidebar-brand .logo {{ | |
| font-size: 2.5rem; | |
| line-height: 1; | |
| }} | |
| .sidebar-brand h3 {{ | |
| margin: 0.35rem 0 0; | |
| color: #E2E8F0; | |
| font-weight: 800; | |
| font-size: 1.05rem; | |
| }} | |
| .sidebar-brand p {{ | |
| margin: 0.15rem 0 0; | |
| color: #64748B; | |
| font-size: 0.72rem; | |
| }} | |
| div[data-testid="stSidebar"] .stRadio label {{ | |
| background: rgba(255,255,255,0.03); | |
| border: 1px solid rgba(255,255,255,0.06); | |
| border-radius: 12px; | |
| padding: 0.55rem 0.75rem; | |
| margin-bottom: 0.35rem; | |
| transition: all 0.2s ease; | |
| }} | |
| div[data-testid="stSidebar"] .stRadio label:hover {{ | |
| border-color: rgba(99,102,241,0.45); | |
| background: rgba(99,102,241,0.08); | |
| }} | |
| .hero {{ | |
| background: linear-gradient(135deg, #4F46E5 0%, #7C3AED 45%, #A855F7 100%); | |
| padding: 2rem 2.5rem; | |
| border-radius: 24px; | |
| color: white; | |
| margin-bottom: 1.5rem; | |
| box-shadow: 0 24px 48px rgba(79,70,229,0.28); | |
| border: 1px solid rgba(255,255,255,0.12); | |
| position: relative; | |
| overflow: hidden; | |
| }} | |
| .hero::before {{ | |
| content: ''; | |
| position: absolute; | |
| inset: 0; | |
| background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.04'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); | |
| opacity: 0.5; | |
| }} | |
| .hero::after {{ | |
| content: ''; | |
| position: absolute; | |
| top: -50%; | |
| right: -10%; | |
| width: 280px; | |
| height: 280px; | |
| background: rgba(255,255,255,0.06); | |
| border-radius: 50%; | |
| }} | |
| .hero h1 {{ | |
| margin: 0; | |
| font-size: 2rem; | |
| font-weight: 800; | |
| letter-spacing: -0.02em; | |
| position: relative; | |
| z-index: 1; | |
| }} | |
| .hero p {{ | |
| margin: 0.5rem 0 0; | |
| opacity: 0.92; | |
| font-size: 1rem; | |
| position: relative; | |
| z-index: 1; | |
| }} | |
| .stat-card {{ | |
| background: linear-gradient(145deg, rgba(30,41,59,0.95), rgba(51,65,85,0.85)); | |
| border: 1px solid rgba(255,255,255,0.08); | |
| border-radius: 18px; | |
| padding: 1.35rem 1.5rem; | |
| text-align: left; | |
| box-shadow: 0 8px 32px rgba(0,0,0,0.25); | |
| backdrop-filter: blur(8px); | |
| height: 100%; | |
| transition: transform 0.2s ease, border-color 0.2s ease; | |
| }} | |
| .stat-card:hover {{ | |
| transform: translateY(-2px); | |
| border-color: rgba(99,102,241,0.35); | |
| }} | |
| .stat-card .value {{ | |
| margin: 0; | |
| font-size: 1.85rem; | |
| font-weight: 800; | |
| color: #C7D2FE; | |
| line-height: 1.2; | |
| }} | |
| .stat-card .label {{ | |
| margin: 0.35rem 0 0; | |
| color: #94A3B8; | |
| font-size: 0.82rem; | |
| font-weight: 500; | |
| text-transform: uppercase; | |
| letter-spacing: 0.04em; | |
| }} | |
| .stat-card .delta {{ | |
| margin-top: 0.5rem; | |
| font-size: 0.78rem; | |
| color: #6EE7B7; | |
| }} | |
| .feature-card {{ | |
| background: rgba(30,41,59,0.7); | |
| border: 1px solid rgba(255,255,255,0.08); | |
| border-radius: 16px; | |
| padding: 1.25rem; | |
| height: 100%; | |
| transition: border-color 0.2s ease; | |
| }} | |
| .feature-card:hover {{ | |
| border-color: rgba(168,85,247,0.35); | |
| }} | |
| .feature-card h4 {{ | |
| margin: 0 0 0.35rem; | |
| color: #E2E8F0; | |
| font-size: 1rem; | |
| }} | |
| .feature-card p {{ | |
| margin: 0; | |
| color: #94A3B8; | |
| font-size: 0.85rem; | |
| line-height: 1.5; | |
| }} | |
| .login-shell {{ | |
| background: rgba(30,41,59,0.65); | |
| border-radius: 24px; | |
| padding: 2rem; | |
| border: 1px solid rgba(99,102,241,0.25); | |
| box-shadow: 0 16px 48px rgba(0,0,0,0.35); | |
| backdrop-filter: blur(12px); | |
| }} | |
| .login-header {{ | |
| text-align: center; | |
| margin-bottom: 1.25rem; | |
| }} | |
| .login-header .icon {{ | |
| font-size: 3rem; | |
| }} | |
| .login-header h2 {{ | |
| margin: 0.5rem 0 0.25rem; | |
| color: #E2E8F0; | |
| font-weight: 800; | |
| }} | |
| .login-header p {{ | |
| margin: 0; | |
| color: #94A3B8; | |
| font-size: 0.85rem; | |
| }} | |
| .badge {{ | |
| display: inline-block; | |
| padding: 0.25rem 0.65rem; | |
| border-radius: 999px; | |
| font-size: 0.72rem; | |
| font-weight: 600; | |
| }} | |
| .badge-success {{ background: rgba(16,185,129,0.2); color: #6EE7B7; }} | |
| .badge-warning {{ background: rgba(245,158,11,0.2); color: #FCD34D; }} | |
| .badge-danger {{ background: rgba(239,68,68,0.2); color: #FCA5A5; }} | |
| .badge-info {{ background: rgba(99,102,241,0.2); color: #A5B4FC; }} | |
| .activity-item {{ | |
| background: rgba(30,41,59,0.55); | |
| border: 1px solid rgba(255,255,255,0.06); | |
| border-radius: 14px; | |
| padding: 0.85rem 1rem; | |
| margin-bottom: 0.5rem; | |
| display: flex; | |
| gap: 0.75rem; | |
| align-items: flex-start; | |
| }} | |
| .activity-item .icon {{ | |
| font-size: 1.25rem; | |
| line-height: 1; | |
| }} | |
| .activity-item .body {{ | |
| flex: 1; | |
| }} | |
| .activity-item .title {{ | |
| color: #E2E8F0; | |
| font-weight: 600; | |
| font-size: 0.88rem; | |
| margin: 0; | |
| }} | |
| .activity-item .meta {{ | |
| color: #64748B; | |
| font-size: 0.75rem; | |
| margin: 0.2rem 0 0; | |
| }} | |
| .room-tile {{ | |
| border-radius: 12px; | |
| padding: 0.65rem 0.5rem; | |
| text-align: center; | |
| font-size: 0.72rem; | |
| font-weight: 600; | |
| border: 1px solid rgba(255,255,255,0.08); | |
| }} | |
| .room-vacant {{ background: rgba(16,185,129,0.15); color: #6EE7B7; }} | |
| .room-partial {{ background: rgba(245,158,11,0.15); color: #FCD34D; }} | |
| .room-full {{ background: rgba(99,102,241,0.15); color: #A5B4FC; }} | |
| .panel {{ | |
| background: rgba(30,41,59,0.55); | |
| border: 1px solid rgba(255,255,255,0.06); | |
| border-radius: 18px; | |
| padding: 1.25rem; | |
| margin-bottom: 1rem; | |
| }} | |
| .panel-title {{ | |
| color: #E2E8F0; | |
| font-weight: 700; | |
| font-size: 0.95rem; | |
| margin: 0 0 0.75rem; | |
| }} | |
| .footer-note {{ | |
| text-align: center; | |
| color: #64748B; | |
| font-size: 0.75rem; | |
| padding: 1.5rem 0 0.5rem; | |
| border-top: 1px solid rgba(255,255,255,0.05); | |
| margin-top: 2rem; | |
| }} | |
| div[data-testid="stMetric"] {{ | |
| background: rgba(30,41,59,0.6); | |
| border: 1px solid rgba(255,255,255,0.06); | |
| border-radius: 14px; | |
| padding: 0.75rem 1rem; | |
| }} | |
| .stTabs [data-baseweb="tab-list"] {{ gap: 0.5rem; }} | |
| .stTabs [data-baseweb="tab"] {{ border-radius: 10px; padding: 0.5rem 1rem; }} | |
| div.stButton > button[kind="primary"] {{ | |
| background: linear-gradient(135deg, #6366F1, #8B5CF6); | |
| border: none; | |
| font-weight: 600; | |
| border-radius: 12px; | |
| transition: opacity 0.2s ease; | |
| }} | |
| div.stButton > button[kind="primary"]:hover {{ | |
| opacity: 0.92; | |
| border: none; | |
| }} | |
| </style> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def sidebar_brand(): | |
| st.markdown( | |
| """ | |
| <div class="sidebar-brand"> | |
| <div class="logo">馃彔</div> | |
| <h3>Hostel MS</h3> | |
| <p>UCP BSAI 路 PDC Lab</p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def hero(title: str, subtitle: str, badge: str = ""): | |
| badge_html = f'<span class="badge badge-info">{badge}</span> 路 ' if badge else "" | |
| st.markdown( | |
| f""" | |
| <div class="hero"> | |
| <h1>{title}</h1> | |
| <p>{badge_html}{subtitle}</p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def metrics_row(items: list[tuple[str, str, str | None]]): | |
| cols = st.columns(len(items)) | |
| for col, item in zip(cols, items): | |
| val, label = item[0], item[1] | |
| delta = item[2] if len(item) > 2 else None | |
| delta_html = f'<div class="delta">{delta}</div>' if delta else "" | |
| col.markdown( | |
| f""" | |
| <div class="stat-card"> | |
| <p class="value">{val}</p> | |
| <p class="label">{label}</p> | |
| {delta_html} | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def feature_grid(features: list[tuple[str, str, str]]): | |
| cols = st.columns(len(features)) | |
| for col, (icon, title, desc) in zip(cols, features): | |
| col.markdown( | |
| f""" | |
| <div class="feature-card"> | |
| <h4>{icon} {title}</h4> | |
| <p>{desc}</p> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def activity_feed(items: list[tuple[str, str, str]]): | |
| for icon, title, meta in items: | |
| st.markdown( | |
| f""" | |
| <div class="activity-item"> | |
| <span class="icon">{icon}</span> | |
| <div class="body"> | |
| <p class="title">{title}</p> | |
| <p class="meta">{meta}</p> | |
| </div> | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def room_grid(rooms: list[dict]): | |
| cols = st.columns(min(len(rooms), 5)) | |
| for i, r in enumerate(rooms): | |
| status = r.get("status", "Vacant") | |
| css = "room-vacant" if status == "Vacant" else "room-partial" if status == "Partial" else "room-full" | |
| cols[i % 5].markdown( | |
| f""" | |
| <div class="room-tile {css}"> | |
| {r['number']}<br> | |
| {r['occupied']}/{r['capacity']} | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def page_footer(): | |
| st.markdown( | |
| """ | |
| <div class="footer-note"> | |
| Hostel Management System 路 PDC Lab Project 路 UCP BSAI<br> | |
| Ch Hamza Younas & Muhammad Abdullah 路 Instructor: Ma'am Khadija | |
| </div> | |
| """, | |
| unsafe_allow_html=True, | |
| ) | |
| def status_badge(text: str, kind: str = "info") -> str: | |
| return f'<span class="badge badge-{kind}">{text}</span>' | |