| 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" |
| ) |
|
|
| |
| 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; }} |
| "))) |
|
|
| |
| 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") |
| ) |
| ) |
| ) |
| } |
|
|
| |
| 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 |
| ) |
| } |
|
|
| |
| 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'; |
| } |
| ")) |
|
|
| |
| placeholder_panel <- function(label, sub = "Coming soon") { |
| tags$div( |
| class = "placeholder-panel", |
| tags$div(class = "ph-label", label), |
| tags$div(class = "ph-sub", sub) |
| ) |
| } |
|
|