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) ) }