Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 13 additions & 3 deletions app/web/navigation.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"""

from dataclasses import dataclass
from typing import Literal
from urllib.parse import urlencode

HITS_PATH = "/"
Expand All @@ -22,12 +23,12 @@

HITS_LABEL = "Hits"
STRIKEOUTS_LABEL = "Batting Strikeouts"
RUNS_LABEL = "Runs"
RUNS_LABEL = "Runs Scored"
BASERUNNERS_LABEL = "Baserunners"
RUN_DIFFERENTIAL_LABEL = "Run Differential"
PITCHING_LABEL = "Pitching"
PITCHING_LABEL = "Pitching Trends"
HITS_ALLOWED_LABEL = "Hits Allowed"
COMPARISON_LABEL = "Comparison"
COMPARISON_LABEL = "Hits vs Batting Strikeouts"


@dataclass(frozen=True)
Expand All @@ -37,6 +38,7 @@ class NavLink:
label: str
href: str
is_current: bool
group: Literal["Offense", "Pitching", "Results"]


def build_nav_links(
Expand All @@ -60,41 +62,49 @@ def build_nav_links(
return [
NavLink(
label=HITS_LABEL,
group="Offense",
href=f"{HITS_PATH}{suffix}",
is_current=current_path == HITS_PATH,
),
NavLink(
label=STRIKEOUTS_LABEL,
group="Offense",
href=f"{STRIKEOUTS_PATH}{suffix}",
is_current=current_path == STRIKEOUTS_PATH,
),
NavLink(
label=RUNS_LABEL,
group="Offense",
href=f"{RUNS_PATH}{suffix}",
is_current=current_path == RUNS_PATH,
),
NavLink(
label=BASERUNNERS_LABEL,
group="Offense",
href=f"{BASERUNNERS_PATH}{suffix}",
is_current=current_path == BASERUNNERS_PATH,
),
NavLink(
label=RUN_DIFFERENTIAL_LABEL,
group="Results",
href=f"{RUN_DIFFERENTIAL_PATH}{suffix}",
is_current=current_path == RUN_DIFFERENTIAL_PATH,
),
NavLink(
label=PITCHING_LABEL,
group="Pitching",
href=f"{PITCHING_PATH}{suffix}",
is_current=current_path == PITCHING_PATH,
),
NavLink(
label=HITS_ALLOWED_LABEL,
group="Pitching",
href=f"{HITS_ALLOWED_PATH}{suffix}",
is_current=current_path == HITS_ALLOWED_PATH,
),
NavLink(
label=COMPARISON_LABEL,
group="Offense",
href=f"{COMPARISON_PATH}{suffix}",
is_current=current_path == COMPARISON_PATH,
),
Expand Down
127 changes: 111 additions & 16 deletions app/web/static/css/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -86,41 +86,131 @@ body {
color: #d8e6f2;
}

/* Navigation */
/* Primary/domain navigation */

.site-nav {
.primary-nav {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
max-width: 100%;
}

.site-nav__link {
.primary-nav__link {
display: inline-block;
padding: 0.4rem 0.85rem;
border-radius: 999px;
color: #c8d6e5;
font-size: 0.95rem;
padding: 0.5rem 0.85rem;
border-radius: 8px;
color: #ffffff;
font-weight: 600;
text-decoration: none;
}

.site-nav__link:hover {
color: #ffffff;
background: rgba(255, 255, 255, 0.08);
.primary-nav__link:hover,
.primary-nav__link[aria-current="location"] {
background: rgba(255, 255, 255, 0.12);
}

.site-nav__link--current {
color: #ffffff;
background: rgba(255, 255, 255, 0.12);
.primary-nav__link[aria-current="location"] {
text-decoration: underline;
text-underline-offset: 0.3em;
}

.brand:focus-visible,
.site-nav__link:focus-visible {
.primary-nav__link:focus-visible {
outline: 2px solid #7fd3d4;
outline-offset: 2px;
}

/* Team analytics navigation */

.team-nav {
background: var(--card-bg);
border-bottom: 1px solid var(--border);
}

.team-nav__groups {
display: flex;
gap: 1rem 2rem;
padding-top: 1rem;
padding-bottom: 1rem;
}

.team-nav__group {
min-width: 0;
flex: 1 1 0;
}

.team-nav__group:first-child {
flex-grow: 2;
}

.team-nav__heading {
margin: 0 0 0.4rem;
color: var(--muted);
font-size: 0.8rem;
font-weight: 700;
}

.team-nav__links {
display: flex;
flex-wrap: wrap;
gap: 0.25rem;
margin: 0;
padding: 0;
list-style: none;
}

.team-nav__links li {
max-width: 100%;
}

.team-nav__link {
display: block;
padding: 0.55rem 0.75rem;
border-radius: 6px;
color: var(--navy);
font-size: 0.95rem;
font-weight: 600;
text-decoration: none;
}

.team-nav__link:hover {
background: var(--page-bg);
}

.team-nav__link--current {
background: var(--navy);
color: #ffffff;
text-decoration: underline;
text-underline-offset: 0.25em;
}

.team-nav__link--current:hover {
background: var(--navy-deep);
}

.team-nav__link:focus-visible,
.skip-link:focus-visible,
#main-content:focus-visible {
outline: 3px solid var(--teal-dark);
outline-offset: 3px;
}

/* Keyboard shortcut past both navigation landmarks. */
.skip-link {
position: absolute;
top: 0.5rem;
left: 0.5rem;
z-index: 10;
transform: translateY(-200%);
padding: 0.75rem 1rem;
background: #ffffff;
color: var(--navy);
font-weight: 700;
}

.skip-link:focus {
transform: translateY(0);
}

/* Page */

.page {
Expand Down Expand Up @@ -488,6 +578,11 @@ body {
}

@media (max-width: 48rem) {
.team-nav__groups {
flex-direction: column;
gap: 0.75rem;
}

/* Only the inline padding changes with width. A `padding` shorthand here
would also reset the vertical padding `.page` sets. */
.shell {
Expand Down Expand Up @@ -524,7 +619,7 @@ body {
gap: 0.5rem;
}

.site-nav {
.primary-nav {
margin-left: -0.85rem;
}

Expand Down
18 changes: 18 additions & 0 deletions app/web/templates/_team_navigation.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<nav class="team-nav" aria-label="Team analytics">
<div class="shell team-nav__groups">
{% for group in ("Offense", "Pitching", "Results") %}
<div class="team-nav__group">
<p class="team-nav__heading">{{ group }}</p>
<ul class="team-nav__links">
{% for link in nav_links if link.group == group %}
<li>
<a class="team-nav__link{% if link.is_current %} team-nav__link--current{% endif %}"
href="{{ link.href }}"
{% if link.is_current %}aria-current="page"{% endif %}>{{ link.label }}</a>
</li>
{% endfor %}
</ul>
</div>
{% endfor %}
</div>
</nav>
22 changes: 9 additions & 13 deletions app/web/templates/base.html
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,25 @@
{% block head %}{% endblock %}
</head>
<body>
{# One header bar: branding on the left, the real metric navigation on the
right. The navigation is the only site-wide control the application
actually has, so it fills the space rather than a second stacked bar. #}
<a class="skip-link" href="#main-content">Skip to main content</a>
<header class="site-header">
<div class="shell site-header__inner">
<a class="brand" href="/">
{# Decorative: the wordmark beside it already names the application. #}
<img class="brand__logo js-logo" src="{{ mlb_logo_url }}" alt="" width="34" height="34" />
MLB Stats Visualizer
</a>
{% if nav_links %}
<nav class="site-nav" aria-label="Metrics">
{%- for link in nav_links %}
<a class="site-nav__link{% if link.is_current %} site-nav__link--current{% endif %}"
href="{{ link.href }}"
{% if link.is_current %}aria-current="page"{% endif %}>{{ link.label }}</a>
{%- endfor %}
</nav>
{% endif %}
<nav class="primary-nav" aria-label="Primary">
{% block primary_navigation %}
<a class="primary-nav__link" href="/">Teams</a>
{% endblock %}
</nav>
</div>
</header>

<main class="shell page">{% block content %}{% endblock %}</main>
{% block domain_navigation %}{% endblock %}

<main id="main-content" class="shell page" tabindex="-1">{% block content %}{% endblock %}</main>

<footer class="site-footer">
<div class="shell site-footer__inner">
Expand Down
2 changes: 1 addition & 1 deletion app/web/templates/baserunners.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "team_base.html" %}

{% block title %}
{%- if state == "ok" -%}
Expand Down
2 changes: 1 addition & 1 deletion app/web/templates/comparison.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "team_base.html" %}

{% block title %}
{%- if state == "ok" -%}
Expand Down
2 changes: 1 addition & 1 deletion app/web/templates/hits_allowed.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "team_base.html" %}

{% block title %}
{%- if state == "ok" -%}
Expand Down
2 changes: 1 addition & 1 deletion app/web/templates/index.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "team_base.html" %}

{% block title %}
{%- if state == "ok" -%}
Expand Down
2 changes: 1 addition & 1 deletion app/web/templates/pitching.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "team_base.html" %}

{% block title %}
{%- if state == "ok" -%}
Expand Down
2 changes: 1 addition & 1 deletion app/web/templates/run_differential.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "team_base.html" %}

{% block title %}
{%- if state == "ok" -%}
Expand Down
2 changes: 1 addition & 1 deletion app/web/templates/runs.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "team_base.html" %}

{% block title %}
{%- if state == "ok" -%}
Expand Down
2 changes: 1 addition & 1 deletion app/web/templates/strikeouts.html
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "team_base.html" %}

{% block title %}
{%- if state == "ok" -%}
Expand Down
10 changes: 10 additions & 0 deletions app/web/templates/team_base.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% extends "base.html" %}

{% block primary_navigation %}
{# Hits is the Team entry point; reuse the scaffold's resolved/requested URL. #}
<a class="primary-nav__link" href="{{ nav_links[0].href }}" aria-current="location">Teams</a>
{% endblock %}

{% block domain_navigation %}
{% include "_team_navigation.html" %}
{% endblock %}
31 changes: 31 additions & 0 deletions docs/ui-information-architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# UI information architecture

Issue #30, implementation Slice A separates application domains from Team metrics.
The primary navigation currently contains only **Teams**. Players stays hidden
until a real destination exists; no placeholder links or pages are rendered.

Team analytics uses ordinary document links under noninteractive headings:

- **Offense:** Hits (`/`), Batting Strikeouts (`/strikeouts`), Runs Scored
(`/runs`), Baserunners (`/baserunners`), Hits vs Batting Strikeouts (`/comparison`).
- **Pitching:** Pitching Trends (`/pitching`), Hits Allowed (`/hits-allowed`).
- **Results:** Run Differential (`/run-differential`).

Offense includes broader outcomes than hitting alone. Comparison belongs there
because it compares hits and batting strikeouts, not arbitrary teams or seasons.
All existing URLs remain canonical, with no aliases or redirects.

`base.html` owns the entity-neutral shell, primary navigation, skip link, main
landmark, and footer. `team_base.html` supplies the active Team domain and grouped
navigation through `_team_navigation.html`. Individual metric templates retain
selectors, charts, interpretation, and recovery states. A future Player UI can
extend the same shell without copying Team assumptions.

The brand links to bare `/`. On Team pages, Teams links to the selection-aware
Hits URL. Metric links preserve `team_id`, `season`, and `window`, including the
existing requested-versus-resolved distinction in terminal states. Generic
validation and schema errors do not require Team context.

Teams uses `aria-current="location"`; only the current metric uses
`aria-current="page"`. Groups stack on mobile and links wrap, without navigation
JavaScript or tab semantics. Selectors and chart-local scrolling are unchanged.
Loading
Loading