Redesign the Coach mode shell: ice-blue header, flat sheet, Squad/Schedule tabs

Reworks the coach shell per feedback that the previous dark-ink header
didn't read as visually distinct from Member mode's own navy header, and
that the rounded "sheet overlaps header" treatment wasn't landing:

- .coach-header is now solid ice-blue (--color-ice, never club-themed --
  Coach mode's own signature colour) with --color-ice-ink foreground text,
  instead of dark ink with white text. Every header_extra block across the
  coach templates (attendance, lineup) is recoloured to match.
- .coach-sheet drops the 20px rounded-top/negative-margin overlap -- flush
  edge-to-edge below the header now, same join the member shell already uses.
- The header's "Head coach" label was hardcoded regardless of the account's
  actual role -- CoachScopeMixin now resolves active_team_role from the
  person's own current-season StaffAssignment.position on the active team,
  so a team manager or physio sees their real title, not someone else's.

Tab bar is now Today / Squad / [+] / Schedule -- no Me tab (the account's
own settings already live in Member mode via the role switcher, no need
for a second one). Squad (CoachSquadView) is a new roster+staff screen for
the active team; Schedule (CoachScheduleView) is a new full upcoming-events
list for the team, each row routed straight to the coach-relevant action
(Attendance for a practice, Line-up for a game) rather than the Member-
shell RSVP page. The "+" is a raised ice-blue circle opening a small popup
with New event/New post/Add player -- three genuinely different actions,
so the button opens a menu rather than committing to one destination.
Today's own inline New event/New post/Add player buttons are gone now that
the tab bar covers the same ground.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
This commit is contained in:
2026-08-21 23:21:27 +02:00
parent 64d0fec547
commit 1d159ca5de
13 changed files with 421 additions and 45 deletions

View File

@@ -13,10 +13,10 @@
{% block header_extra %}
<div class="mt-3">
<div class="flex items-center justify-between">
<span class="font-display text-xl leading-none font-extrabold text-white uppercase">{% trans "Attendance" %}</span>
<span class="font-mono text-sm text-on-dark">{{ checked_in_count }}/{{ total_count }}</span>
<span class="font-display text-xl leading-none font-extrabold text-ice-ink uppercase">{% trans "Attendance" %}</span>
<span class="font-mono text-sm text-ice-ink/70">{{ checked_in_count }}/{{ total_count }}</span>
</div>
<div class="mt-1 text-xs text-on-dark-dim">{{ event.title }} &middot; {{ event.start|date:"D d M H:i" }}</div>
<div class="mt-1 text-xs text-ice-ink/65">{{ event.title }} &middot; {{ event.start|date:"D d M H:i" }}</div>
<div class="mt-2 h-1.5 overflow-hidden rounded-full bg-steel">
<div class="h-full bg-ice" style="width: {% widthratio checked_in_count total_count|default:1 100 %}%"></div>
</div>

View File

@@ -4,19 +4,21 @@
App shell for Coach mode (C1-C6) -- design_handoff_rosterchief_platform/README.md's
"Coach mode (mobile, dark chrome)" section. Standalone from mobile/templates/mobile/
base.html (Member mode's shell) rather than a shared parent: the two have almost no
markup in common beyond the outer <html>/<body> skeleton -- dark ink header instead
of navy, a light .coach-sheet body that overlaps the header by 20px (the mode's own
signature, see assets/mobile.css's own comment), and a dark .coach-tab-bar instead of
the white one.
markup in common beyond the outer <html>/<body> skeleton -- an ice-blue header
(never club-themed, the mode's own signature colour, see assets/mobile.css's own
comment) instead of the member shell's club-themed navy, and a dark .coach-tab-bar
instead of the white one.
Reuses the SAME stylesheet (static/css/mobile.css) and the same mobile:manifest/
mobile:icon/mobile:service_worker PWA plumbing as Member mode -- one app, one
manifest, two modes, not two separate PWAs.
The tab bar deliberately stays minimal (Today + Me) even with all six coach screens
built -- C2/C3/C4/C5/C6 are reached from Today's own action buttons and "needs you"
list, not separate tab items; Me reuses the *existing* member mobile:me page rather
than a separate coach-Me screen (see CoachTodayView's own docstring).
Tab bar: Today / Squad / Schedule, plus a raised "+" action in the middle that opens
a small popup menu (New event / New post / Add player) rather than committing to one
destination -- coach mode has three genuinely different "add" actions and picking a
single one for the button would just hide the other two. No Me tab here -- the
account's own settings live in Member mode's mobile:me, reached via the role
switcher above, not duplicated as a second Me screen.
hx-boost="true" on <body>: every same-shell link/form becomes an AJAX navigation
instead of a full browser reload -- see mobile/templates/mobile/base.html's own
@@ -29,7 +31,7 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
<meta name="theme-color" content="#0b1220">
<meta name="theme-color" content="#14b8e8">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="mobile-web-app-capable" content="yes">
@@ -66,15 +68,15 @@
<span class="app-crest app-crest-fallback">{{ club.initials }}</span>
{% endif %}
<span class="min-w-0 flex-1">
<span class="block truncate font-display text-[19px] leading-none font-extrabold text-white uppercase">{{ active_team.name|default:club.name }}</span>
{% if me %}<span class="block font-mono text-xs text-on-dark">{% trans "Head coach" %} &middot; {{ me.first_name }}</span>{% endif %}
<span class="block truncate font-display text-[19px] leading-none font-extrabold text-ice-ink uppercase">{{ active_team.name|default:club.name }}</span>
{% if me and active_team_role %}<span class="block font-mono text-xs text-ice-ink/70">{{ active_team_role }} &middot; {{ me.first_name }}</span>{% endif %}
</span>
</div>
{% if staffed_teams|length > 1 %}
<div class="mt-3 flex gap-2 overflow-x-auto scrollbar-hide">
{% for team in staffed_teams %}
<a class="shrink-0 rounded-full px-3 py-1.5 font-display text-xs font-extrabold tracking-wide uppercase {% if team == active_team %}bg-ice text-ice-ink{% else %}bg-steel text-on-dark{% endif %}" href="?team={{ team.pk }}">
<a class="shrink-0 rounded-full px-3 py-1.5 font-display text-xs font-extrabold tracking-wide uppercase {% if team == active_team %}bg-ink text-ice{% else %}bg-white/40 text-ice-ink{% endif %}" href="?team={{ team.pk }}">
{{ team.short_name }}
</a>
{% endfor %}
@@ -108,14 +110,35 @@
{% lucide "house" size=21 %}
<span class="tab-bar-label">{% trans "Today" %}</span>
</a>
{% comment %}
hx-boost="false" -- mobile:me is a Member-shell view (bg-paper, not
this page's bg-ink); see base.html's own comment for why a boosted
cross-shell swap would leave the wrong <body> class in place.
{% endcomment %}
<a class="coach-tab-bar-item" href="{% url "mobile:me" %}" hx-boost="false">
{% lucide "user" size=21 %}
<span class="tab-bar-label">{% trans "Me" %}</span>
<a class="coach-tab-bar-item {% if active_tab == "coach_squad" %}coach-tab-bar-item-active{% endif %}" href="{% url "mobile:coach_squad" %}">
{% lucide "users" size=21 %}
<span class="tab-bar-label">{% trans "Squad" %}</span>
</a>
{% if can_manage_active_team %}
<div class="coach-tab-bar-add" x-data="{ open: false }">
<button class="coach-tab-bar-add-button" type="button" @click="open = !open" aria-label="{% trans "Add" %}">
{% lucide "plus" size=24 %}
</button>
<div class="absolute inset-x-3 bottom-full z-20 mb-3 flex flex-col gap-1 rounded-xl border border-line bg-white p-2 shadow-lg" x-show="open" x-cloak @click.outside="open = false">
<a class="flex items-center gap-3 rounded-lg p-3 text-sm font-semibold text-ink" href="{% url "mobile:coach_create_event" %}">
{% lucide "calendar-plus" size=18 class="text-club" %} {% trans "New event" %}
</a>
<a class="flex items-center gap-3 rounded-lg p-3 text-sm font-semibold text-ink" href="{% url "mobile:coach_create_news" %}">
{% lucide "newspaper" size=18 class="text-club" %} {% trans "New post" %}
</a>
<a class="flex items-center gap-3 rounded-lg p-3 text-sm font-semibold text-ink" href="{% url "mobile:coach_add_player" %}">
{% lucide "user-plus" size=18 class="text-club" %} {% trans "Add player" %}
</a>
</div>
</div>
{% else %}
<div class="coach-tab-bar-add"></div>
{% endif %}
<a class="coach-tab-bar-item {% if active_tab == "coach_schedule" %}coach-tab-bar-item-active{% endif %}" href="{% url "mobile:coach_schedule" %}">
{% lucide "calendar" size=21 %}
<span class="tab-bar-label">{% trans "Schedule" %}</span>
</a>
</nav>

View File

@@ -15,10 +15,10 @@
{% block header_extra %}
<div class="mt-3 flex items-center justify-between">
<span class="font-display text-xl leading-none font-extrabold text-white uppercase">{% trans "Line-up" %}</span>
<span class="font-display text-xl leading-none font-extrabold text-ice-ink uppercase">{% trans "Line-up" %}</span>
{% if lineup.published_at %}<span class="pill pill-info">{% trans "Published" %}</span>{% endif %}
</div>
<div class="mt-1 text-xs text-on-dark-dim">{{ event.title }} &middot; {{ event.start|date:"D d M H:i" }}</div>
<div class="mt-1 text-xs text-ice-ink/65">{{ event.title }} &middot; {{ event.start|date:"D d M H:i" }}</div>
{% endblock header_extra %}
{% block content %}

View File

@@ -0,0 +1,39 @@
{% extends "mobile/coach/base.html" %}
{% load i18n %}
{% comment %}
Bottom-tab "Schedule" -- every upcoming event for the active team (not
just the next one, that's Today's job). Each row links straight into the
coach-relevant action for its kind rather than mobile:event_detail (the
Member-shell RSVP page) -- see CoachScheduleView's own docstring.
{% endcomment %}
{% block content %}
{% if not active_team %}
<div class="m-card p-6 text-center">
<p class="font-display text-lg font-extrabold text-ink uppercase">{% trans "Not staffing a team yet" %}</p>
</div>
{% elif not events %}
<div class="m-card p-6 text-center">
<p class="text-sm text-muted">{% trans "Nothing scheduled." %}</p>
</div>
{% else %}
<div class="m-card flex flex-col overflow-hidden">
{% for event in events %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
<a class="flex items-center gap-3 p-3.5" href="{% if event.kind == "game" %}{% url "mobile:coach_lineup" event.pk %}{% else %}{% url "mobile:coach_attendance" event.pk %}{% endif %}">
<div class="w-9.5 shrink-0 text-center">
<div class="font-mono text-[10px] tracking-wide text-muted uppercase">{{ event.start|date:"D" }}</div>
<div class="font-display text-2xl leading-none font-extrabold text-ink">{{ event.start|date:"d" }}</div>
</div>
<div class="w-[3px] shrink-0 self-stretch rounded-full {% if event.kind == "game" %}bg-club{% elif event.kind == "training" %}bg-info{% else %}bg-warn{% endif %}"></div>
<div class="min-w-0 flex-1">
<div class="text-sm font-semibold text-ink">{{ event.title }}</div>
<div class="truncate text-xs text-muted">{{ event.start|date:"H:i" }}{% if event.location %} &middot; {{ event.location.name }}{% endif %}</div>
</div>
<span class="pill {% if event.kind == "game" %}pill-danger{% elif event.kind == "training" %}pill-info{% else %}pill-neutral{% endif %} shrink-0">{{ event.get_kind_display }}</span>
</a>
{% endfor %}
</div>
{% endif %}
{% endblock content %}

View File

@@ -0,0 +1,62 @@
{% extends "mobile/coach/base.html" %}
{% load i18n lucide %}
{% comment %}
Bottom-tab "Squad" -- roster + staff for the active team, current season.
See CoachSquadView's own docstring for why there's no per-row edit here
(jersey number/position/captaincy stays a desktop-only action for now).
{% endcomment %}
{% block content %}
{% if not active_team %}
<div class="m-card p-6 text-center">
<p class="font-display text-lg font-extrabold text-ink uppercase">{% trans "Not staffing a team yet" %}</p>
</div>
{% else %}
<div>
<div class="mb-2 flex items-center justify-between">
<span class="font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Roster" %}</span>
{% if can_manage_active_team %}
<a class="flex items-center gap-1 font-display text-xs font-extrabold tracking-wide text-club uppercase" href="{% url "mobile:coach_add_player" %}">
{% lucide "user-plus" size=14 %} {% trans "Add" %}
</a>
{% endif %}
</div>
<div class="m-card overflow-hidden">
{% for membership in roster %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
<div class="flex items-center gap-3 p-3.5">
<div class="w-8 shrink-0 text-center font-display text-lg font-extrabold text-ink tabular-nums">{{ membership.jersey_number|default:"—" }}</div>
<div class="min-w-0 flex-1">
<div class="text-[15px] font-semibold text-ink">
{{ membership.member.get_full_name }}
{% if membership.is_captain %}<span class="ml-1 font-display text-xs font-extrabold text-club-dark">C</span>{% endif %}
{% if membership.is_alternate_captain %}<span class="ml-1 font-display text-xs font-extrabold text-club-dark">A</span>{% endif %}
</div>
<div class="text-xs text-muted">{{ membership.position|default:"—" }}</div>
</div>
</div>
{% empty %}
<div class="p-6 text-center text-sm text-muted">{% trans "No one on the roster for this season yet." %}</div>
{% endfor %}
</div>
</div>
<div>
<div class="mb-2 font-display text-xs font-extrabold tracking-wide text-muted uppercase">{% trans "Staff" %}</div>
<div class="m-card overflow-hidden">
{% for assignment in staff %}
{% if not forloop.first %}<div class="h-px bg-rule"></div>{% endif %}
<div class="flex items-center gap-3 p-3.5">
<div class="min-w-0 flex-1">
<div class="text-[15px] font-semibold text-ink">{{ assignment.member.get_full_name }}</div>
<div class="text-xs text-muted">{{ assignment.position }}</div>
</div>
</div>
{% empty %}
<div class="p-6 text-center text-sm text-muted">{% trans "No staff assigned for this season yet." %}</div>
{% endfor %}
</div>
</div>
{% endif %}
{% endblock content %}

View File

@@ -6,7 +6,9 @@
stat tiles, a tonight's-session card (only rendered when one exists),
a "needs you" list, and an "Also yours" card for the coach's own
member-side RSVP. See CoachTodayView's own docstring for what's scoped
down from the mock and why.
down from the mock and why. The mock's own New event/New post/Add
player actions live on the shell's own "+" tab-bar button now (coach/
base.html) rather than repeated here as inline buttons.
{% endcomment %}
{% block content %}
@@ -16,14 +18,6 @@
<p class="mt-1 text-sm text-muted">{% trans "Once you're assigned to a team's staff, its schedule and roster will show up here." %}</p>
</div>
{% else %}
{% if can_manage_active_team %}
<div class="flex gap-2">
<a class="btn btn-dark flex-1" href="{% url "mobile:coach_create_event" %}">{% trans "New event" %}</a>
<a class="btn btn-secondary flex-1" href="{% url "mobile:coach_create_news" %}">{% trans "New post" %}</a>
<a class="btn btn-secondary flex-1" href="{% url "mobile:coach_add_player" %}">{% trans "Add player" %}</a>
</div>
{% endif %}
<div class="grid grid-cols-3 gap-2.5">
<div class="m-card p-3 text-center">
<div class="font-display text-2xl leading-none font-extrabold text-ink">{{ squad_count }}</div>