Spaces:
Running
Running
| library(bslib) | |
| library(glue) | |
| HAWKS_ORANGE <- "#F25B00" | |
| HAWKS_BLUE <- "#164EA7" | |
| HAWKS_DARK <- "#0D1B2A" | |
| HAWKS_LIGHT <- "#F5F7FA" | |
| HAWKS_GRAY <- "#6B7280" | |
| HAWKS_BORDER <- "#E2E8F0" | |
| hawks_theme <- bs_theme( | |
| version = 5, | |
| bg = "#FFFFFF", | |
| fg = HAWKS_DARK, | |
| primary = HAWKS_BLUE, | |
| secondary = HAWKS_ORANGE, | |
| success = "#16a34a", | |
| warning = "#d97706", | |
| danger = "#dc2626", | |
| base_font = font_google("Inter"), | |
| heading_font = font_google("Inter"), | |
| `border-radius` = "0.5rem", | |
| `font-size-base` = "0.925rem" | |
| ) | |
| # ββ Shared CSS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| hawks_css <- tags$style(HTML(glue(" | |
| * {{ box-sizing: border-box; }} | |
| body {{ | |
| background: {HAWKS_LIGHT}; | |
| font-family: 'Inter', system-ui, sans-serif; | |
| }} | |
| /* ββ Sidebar shell ββ */ | |
| .hawks-shell {{ | |
| display: flex; | |
| height: 100vh; | |
| overflow: hidden; | |
| }} | |
| .hawks-sidebar {{ | |
| width: 220px; | |
| min-width: 220px; | |
| background: {HAWKS_DARK}; | |
| display: flex; | |
| flex-direction: column; | |
| transition: width 0.2s ease, min-width 0.2s ease; | |
| overflow: hidden; | |
| }} | |
| .hawks-sidebar.collapsed {{ | |
| width: 52px; | |
| min-width: 52px; | |
| }} | |
| .sb-brand {{ | |
| padding: 16px 14px; | |
| border-bottom: 1px solid #ffffff18; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| min-height: 56px; | |
| }} | |
| .sb-brand-text {{ overflow: hidden; white-space: nowrap; }} | |
| .sb-brand-text .t1 {{ color: #fff; font-size: 13px; font-weight: 700; display: block; }} | |
| .sb-brand-text .t2 {{ color: #ffffff55; font-size: 10px; display: block; letter-spacing: .03em; }} | |
| .hawks-sidebar.collapsed .sb-brand-text {{ display: none; }} | |
| .sb-section {{ | |
| padding: 12px 14px 4px; | |
| color: #ffffff33; | |
| font-size: 9px; | |
| font-weight: 700; | |
| letter-spacing: .1em; | |
| text-transform: uppercase; | |
| white-space: nowrap; | |
| }} | |
| .hawks-sidebar.collapsed .sb-section {{ visibility: hidden; }} | |
| .sb-item {{ | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| padding: 9px 14px; | |
| margin: 1px 6px; | |
| border-radius: 6px; | |
| cursor: pointer; | |
| color: #ffffffaa; | |
| font-size: 13px; | |
| font-weight: 500; | |
| white-space: nowrap; | |
| text-decoration: none; | |
| transition: background 0.15s, color 0.15s; | |
| }} | |
| .sb-item:hover {{ background: #ffffff12; color: #fff; }} | |
| .sb-item.active {{ background: {HAWKS_BLUE}; color: #fff; }} | |
| .sb-icon {{ font-size: 16px; width: 18px; text-align: center; flex-shrink: 0; }} | |
| .sb-label {{ overflow: hidden; }} | |
| .hawks-sidebar.collapsed .sb-label {{ display: none; }} | |
| .sb-footer {{ | |
| margin-top: auto; | |
| padding: 12px 6px; | |
| border-top: 1px solid #ffffff18; | |
| display: flex; | |
| justify-content: center; | |
| }} | |
| .sb-toggle {{ | |
| width: 32px; height: 32px; | |
| border-radius: 6px; | |
| display: flex; align-items: center; justify-content: center; | |
| cursor: pointer; | |
| color: #ffffff66; | |
| transition: background 0.15s, color 0.15s; | |
| }} | |
| .sb-toggle:hover {{ background: #ffffff15; color: #fff; }} | |
| /* ββ Main area ββ */ | |
| .hawks-main {{ | |
| flex: 1; | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| min-width: 0; | |
| }} | |
| .hawks-topbar {{ | |
| background: #fff; | |
| border-bottom: 1px solid {HAWKS_BORDER}; | |
| padding: 0 20px; | |
| height: 54px; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| flex-shrink: 0; | |
| }} | |
| .topbar-title {{ font-size: 15px; font-weight: 700; color: {HAWKS_DARK}; }} | |
| .topbar-sub {{ font-size: 11px; color: #9CA3AF; margin-left: 8px; }} | |
| /* ββ Content scroll area ββ */ | |
| .hawks-content {{ | |
| flex: 1; | |
| overflow-y: auto; | |
| padding: 24px; | |
| }} | |
| /* ββ Filter panel ββ */ | |
| .hawks-body {{ display: flex; flex: 1; overflow: hidden; }} | |
| .filter-panel {{ | |
| width: 210px; | |
| min-width: 210px; | |
| background: #fff; | |
| border-right: 1px solid {HAWKS_BORDER}; | |
| padding: 16px 13px; | |
| overflow-y: auto; | |
| flex-shrink: 0; | |
| }} | |
| .fp-section {{ | |
| font-size: 10px; font-weight: 700; text-transform: uppercase; | |
| letter-spacing: .08em; color: #9CA3AF; margin-bottom: 10px; | |
| }} | |
| .fp-group {{ margin-bottom: 16px; }} | |
| .fp-label {{ | |
| font-size: 11px; font-weight: 600; color: #374151; | |
| margin-bottom: 5px; display: block; | |
| }} | |
| .fp-input {{ | |
| width: 100%; background: {HAWKS_LIGHT}; | |
| border: 1px solid {HAWKS_BORDER}; border-radius: 5px; | |
| padding: 5px 8px; font-size: 11px; color: #374151; | |
| }} | |
| .fp-pills {{ display: flex; flex-wrap: wrap; gap: 4px; margin-top: 4px; }} | |
| .fp-pill {{ | |
| background: {HAWKS_LIGHT}; border: 1px solid {HAWKS_BORDER}; | |
| border-radius: 4px; padding: 3px 8px; | |
| font-size: 10px; color: #374151; cursor: pointer; | |
| }} | |
| .fp-pill.active {{ | |
| background: #164EA714; border-color: #164EA755; | |
| color: {HAWKS_BLUE}; font-weight: 600; | |
| }} | |
| .fp-apply {{ | |
| width: 100%; background: {HAWKS_BLUE}; color: #fff; | |
| border: none; border-radius: 6px; padding: 7px; | |
| font-size: 11px; font-weight: 600; cursor: pointer; margin-top: 4px; | |
| }} | |
| .fp-reset {{ | |
| text-align: center; font-size: 10px; color: #9CA3AF; | |
| margin-top: 6px; cursor: pointer; | |
| }} | |
| /* ββ Tab strip ββ */ | |
| .hawks-tabs {{ | |
| background: #fff; | |
| border-bottom: 1px solid {HAWKS_BORDER}; | |
| padding: 0 20px; | |
| display: flex; | |
| flex-shrink: 0; | |
| }} | |
| .h-tab {{ | |
| padding: 11px 14px; | |
| font-size: 12px; font-weight: 500; | |
| color: #6B7280; | |
| border-bottom: 2px solid transparent; | |
| cursor: pointer; | |
| white-space: nowrap; | |
| transition: color .15s, border-color .15s; | |
| }} | |
| .h-tab:hover {{ color: #374151; }} | |
| .h-tab.active {{ color: {HAWKS_BLUE}; border-bottom-color: {HAWKS_BLUE}; font-weight: 600; }} | |
| /* ββ Stat cards ββ */ | |
| .stat-grid {{ | |
| display: grid; | |
| grid-template-columns: repeat(4, 1fr); | |
| gap: 12px; | |
| margin-bottom: 18px; | |
| }} | |
| .stat-box {{ | |
| background: #fff; border: 1px solid {HAWKS_BORDER}; | |
| border-radius: 8px; padding: 14px 16px; | |
| }} | |
| .stat-val {{ font-size: 22px; font-weight: 700; color: {HAWKS_DARK}; line-height: 1; }} | |
| .stat-lbl {{ | |
| font-size: 10px; color: {HAWKS_GRAY}; | |
| text-transform: uppercase; letter-spacing: .07em; margin-top: 4px; | |
| }} | |
| .stat-delta {{ font-size: 10px; margin-top: 3px; }} | |
| .stat-delta.up {{ color: #16a34a; }} | |
| .stat-delta.dn {{ color: #dc2626; }} | |
| .stat-bar {{ height: 3px; background: {HAWKS_BORDER}; border-radius: 2px; margin-top: 10px; }} | |
| .stat-bar-fill {{ height: 3px; border-radius: 2px; background: {HAWKS_BLUE}; }} | |
| /* ββ Cards ββ */ | |
| .data-card {{ | |
| background: #fff; border: 1px solid {HAWKS_BORDER}; | |
| border-radius: 8px; overflow: hidden; | |
| }} | |
| .data-card-hdr {{ | |
| padding: 10px 14px; border-bottom: 1px solid {HAWKS_BORDER}; | |
| display: flex; align-items: center; justify-content: space-between; | |
| }} | |
| .data-card-title {{ | |
| font-size: 11px; font-weight: 700; color: {HAWKS_DARK}; | |
| text-transform: uppercase; letter-spacing: .05em; | |
| }} | |
| .data-card-sub {{ font-size: 10px; color: #9CA3AF; }} | |
| .data-card-body {{ padding: 12px 14px; }} | |
| /* ββ Player selector ββ */ | |
| .player-select {{ | |
| background: {HAWKS_LIGHT}; border: 1px solid {HAWKS_BORDER}; | |
| border-radius: 6px; padding: 6px 10px; | |
| display: flex; align-items: center; gap: 6px; cursor: pointer; | |
| }} | |
| .player-select .ps-name {{ font-size: 12px; font-weight: 600; color: #374151; }} | |
| /* ββ Placeholder ββ */ | |
| .placeholder-panel {{ | |
| background: {HAWKS_LIGHT}; | |
| border: 1.5px dashed {HAWKS_BORDER}; | |
| border-radius: 8px; | |
| padding: 3rem 1rem; | |
| text-align: center; color: {HAWKS_GRAY}; | |
| }} | |
| .placeholder-panel .ph-label {{ font-weight: 500; font-size: 0.9rem; }} | |
| .placeholder-panel .ph-sub {{ font-size: 0.8rem; margin-top: 0.25rem; opacity: .7; }} | |
| "))) | |
| # ββ Sidebar builder βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| hawks_sidebar <- function(items, app_title = "Analytics") { | |
| nav_items <- lapply(items, function(item) { | |
| tags$div( | |
| class = paste("sb-item", if (isTRUE(item$active)) "active" else ""), | |
| id = paste0("sb_", item$id), | |
| onclick = if (!is.null(item$tab)) { | |
| sprintf("Shiny.setInputValue('active_tab','%s',{priority:'event'})", item$tab) | |
| }, | |
| tags$i(class = paste("ti", item$icon, "sb-icon")), | |
| tags$span(class = "sb-label", item$label) | |
| ) | |
| }) | |
| tags$div( | |
| class = "hawks-sidebar", | |
| id = "hawksSidebar", | |
| tags$div( | |
| class = "sb-brand", | |
| tags$svg( | |
| width = "28", height = "28", viewBox = "0 0 30 30", fill = "none", | |
| tags$polygon(points = "15,2 28,26 15,21 2,26", fill = HAWKS_ORANGE, opacity = "0.9"), | |
| tags$polygon(points = "15,2 15,21 2,26", fill = "#FFFFFF", opacity = "0.18") | |
| ), | |
| tags$div( | |
| class = "sb-brand-text", | |
| tags$span(class = "t1", "Harbor Hawks"), | |
| tags$span(class = "t2", app_title) | |
| ) | |
| ), | |
| tags$div(class = "sb-section", "Navigation"), | |
| tagList(nav_items), | |
| tags$div( | |
| class = "sb-footer", | |
| tags$div( | |
| class = "sb-toggle", | |
| id = "sbToggle", | |
| onclick = "toggleSidebar()", | |
| tags$i(class = "ti ti-chevrons-left", id = "sbToggleIcon") | |
| ) | |
| ) | |
| ) | |
| } | |
| # ββ Topbar ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| hawks_topbar <- function(title, subtitle = "Cape Cod League \u00b7 2025", right = NULL) { | |
| tags$div( | |
| class = "hawks-topbar", | |
| tags$div( | |
| style = "display:flex; align-items:baseline; gap:6px;", | |
| tags$span(class = "topbar-title", title), | |
| tags$span(class = "topbar-sub", subtitle) | |
| ), | |
| if (!is.null(right)) right | |
| ) | |
| } | |
| # ββ Sidebar JS ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| hawks_sidebar_js <- tags$script(HTML(" | |
| function toggleSidebar() { | |
| var sb = document.getElementById('hawksSidebar'); | |
| var icon = document.getElementById('sbToggleIcon'); | |
| sb.classList.toggle('collapsed'); | |
| var isCollapsed = sb.classList.contains('collapsed'); | |
| icon.className = isCollapsed ? 'ti ti-chevrons-right' : 'ti ti-chevrons-left'; | |
| } | |
| ")) | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| placeholder_panel <- function(label, sub = "Coming soon") { | |
| tags$div( | |
| class = "placeholder-panel", | |
| tags$div(class = "ph-label", label), | |
| tags$div(class = "ph-sub", sub) | |
| ) | |
| } | |