Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 3 additions & 2 deletions src/app/about/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@

import Link from 'next/link';
import Image from 'next/image';
import { m, LazyMotion, domAnimation, useReducedMotion } from 'framer-motion';
import { m, LazyMotion, useReducedMotion } from 'framer-motion';
import { loadFeatures } from '@/lib/motion/load-features';
import { Navigation } from '@/components/landing/Navigation';
import { Footer } from '@/components/landing/Footer';
import { Button } from '@/components/ui/button';
Expand Down Expand Up @@ -61,7 +62,7 @@ const staggerContainer = {
export default function AboutPage() {
const prefersReducedMotion = useReducedMotion();
return (
<LazyMotion features={domAnimation}>
<LazyMotion features={loadFeatures}>
<main className="min-h-dvh bg-background overflow-x-hidden">
<a href="#main-content" className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:px-4 focus:py-2 focus:bg-primary-600 focus:text-white focus:rounded-lg focus:shadow-lg">
Skip to main content
Expand Down
67 changes: 40 additions & 27 deletions src/app/admin/_components/AdminShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,43 +205,56 @@ export function AdminShell({
[],
);

// Stable element/object identities (perf packet [shell-render-hygiene]):
// inline literals here are fresh objects every render (e.g. each ⌘K
// open/close), defeating the React.memo on FairwaySidebar/FairwayTopBar.
const brand = useMemo(
() => (
<span className="flex min-w-0 items-center gap-2">
<span className="flex h-7 w-7 items-center justify-center rounded-lg border border-white/[0.08] bg-nav-surface font-fw-mono text-caption text-accent-400">
HB
</span>
<span className="min-w-0 text-sm font-semibold tracking-wide text-white">
Helm <span className="text-accent-400">Bridge</span>
</span>
</span>
),
[],
);
const shellUser = useMemo(() => ({ name: 'Super admin', teamName: email }), [email]);
const topBarActions = useMemo(
() => (
<div className="hidden items-center gap-2 lg:flex">
<span className="rounded-full border border-warm-200 bg-surface px-2.5 py-1 font-fw-mono text-caption uppercase text-warm-600">
prod
</span>
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => router.refresh()}
className="min-h-0 rounded-full px-2.5 py-1 text-caption"
>
Refresh
</Button>
</div>
),
[router],
);

return (
<SessionActivityProvider>
<div className="fairway-ds min-h-screen bg-canvas-gradient">
<AppShell
sections={sections}
brand={
<span className="flex min-w-0 items-center gap-2">
<span className="flex h-7 w-7 items-center justify-center rounded-lg border border-white/[0.08] bg-nav-surface font-fw-mono text-caption text-accent-400">
HB
</span>
<span className="min-w-0 text-sm font-semibold tracking-wide text-white">
Helm <span className="text-accent-400">Bridge</span>
</span>
</span>
}
user={{ name: 'Super admin', teamName: email }}
brand={brand}
user={shellUser}
pathname={pathname}
linkComponent={Link}
breadcrumbs={breadcrumbs}
onSearchOpen={() => setCommandOpen(true)}
searchPlaceholder="Jump to command, incident, user…"
topBarActions={
<div className="hidden items-center gap-2 lg:flex">
<span className="rounded-full border border-warm-200 bg-surface px-2.5 py-1 font-fw-mono text-caption uppercase text-warm-600">
prod
</span>
<Button
type="button"
variant="secondary"
size="sm"
onClick={() => router.refresh()}
className="min-h-0 rounded-full px-2.5 py-1 text-caption"
>
Refresh
</Button>
</div>
}
topBarActions={topBarActions}
>
{children}
</AppShell>
Expand Down
4 changes: 2 additions & 2 deletions src/app/admin/_components/SessionsPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ export function SessionsPanel({ sessions }: { sessions: SessionRow[] }) {
<ul className="space-y-2">
{sessions.map((s) => (
<li key={s.session_id}>
<Inset padding="sm" className="flex items-center gap-3">
<div className="min-w-0 flex-1">
<Inset padding="sm" className="flex flex-wrap items-center gap-x-3 gap-y-2">
<div className="min-w-0 flex-1 basis-full sm:basis-auto">
<p className="truncate text-sm font-medium text-warm-900">{s.email}</p>
<p className="font-fw-mono text-xs tabular-nums text-warm-500">
started <LocalTime iso={s.created_at} variant="datetime" /> · refreshed{' '}
Expand Down
4 changes: 3 additions & 1 deletion src/app/admin/_components/TeamHealthTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,10 @@ export function TeamHealthTable({ teams }: { teams: TeamHealthEntry[] }) {
{teams.map((t) => {
const isLeader = t.health === 'active' && t.errors7d === 0;
return (
<tr key={t.teamId} className={cn(isLeader && 'border-l-2 border-l-accent-500 bg-accent-50')}>
<tr key={t.teamId} className={cn(isLeader && 'bg-accent-50')}>
<td className={cn('sticky left-0 z-10 py-2 pr-3', isLeader ? 'bg-accent-50' : 'bg-surface')}>
{/* Dateline rule — replaces the retired border-l-2 leader stripe. */}
{isLeader && <span aria-hidden className="mb-1 block h-[2px] w-7 rounded-full bg-accent-500" />}
<Link
href={t.href ?? `/admin/teams/${t.teamId}`}
className="font-medium text-warm-900 underline-offset-2 hover:underline"
Expand Down
5 changes: 3 additions & 2 deletions src/app/admin/_motion-provider.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use client';

import { LazyMotion, domAnimation } from 'framer-motion';
import { LazyMotion } from 'framer-motion';
import { loadFeatures } from '@/lib/motion/load-features';
import type { ReactNode } from 'react';

/**
Expand All @@ -9,5 +10,5 @@ import type { ReactNode } from 'react';
* bit the golf-admin Tracer KPI tiles before (see golf/admin/layout.tsx).
*/
export function AdminMotionProvider({ children }: { children: ReactNode }) {
return <LazyMotion features={domAnimation}>{children}</LazyMotion>;
return <LazyMotion features={loadFeatures}>{children}</LazyMotion>;
}
9 changes: 6 additions & 3 deletions src/app/admin/activity/ActivityFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ function ActivityRow({ item }: { item: ActivityItem }) {
<Icon size={16} className={chip.icon} />
</span>
<div className="min-w-0 flex-1">
{/* Dateline rule — replaces the retired border-l-2 leader stripe. */}
{isDemo && <span aria-hidden className="mb-1 block h-[2px] w-7 rounded-full bg-accent-500" />}
<p className="break-words text-sm font-medium text-warm-900 [overflow-wrap:anywhere]">{item.title}</p>
{isDemo ? (
<DemoDetail detail={item.detail} />
Expand All @@ -64,11 +66,12 @@ function ActivityRow({ item }: { item: ActivityItem }) {
</>
);

// Leader row: soft green wash + 2px green left bar (GREEN CONTRACT — the
// one deliberately-highlighted kind).
// Leader row: soft green wash + a dateline rule above the title (GREEN
// CONTRACT — the one deliberately-highlighted kind). Replaces the retired
// border-l-2 left bar.
const rowClass = cn(
'flex min-w-0 items-start gap-3 rounded-fw-md px-3 py-3',
isDemo && 'border-l-2 border-accent-500 bg-accent-50/60',
isDemo && 'bg-accent-50/60',
);

// demo_session rows carry a mailto: <a> inside the detail — nesting that
Expand Down
13 changes: 11 additions & 2 deletions src/app/admin/baseball/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ function SectionLabel({ children }: { children: React.ReactNode }) {
);
}

// Dateline rule — replaces the retired border-l-2 "key panel" left-edge
// stripe. Chrome, not a status signal: a helm-green h-[2px] w-7 rounded-full
// rule above the card title.
function KeyPanelRule() {
return <span aria-hidden className="mb-3 block h-[2px] w-7 rounded-full bg-accent-500" />;
}

function lastSeenLabel(value: string | null) {
if (!value) return 'never';
return new Date(value).toLocaleDateString();
Expand All @@ -37,9 +44,10 @@ function TeamCommandCard({ team }: { team: TeamRosterInsight }) {
const topPlayers = team.players.slice(0, 5);

return (
<Surface padding="sm" className="h-full border-l-2 border-l-accent-500">
<Surface padding="sm" className="h-full">
<div className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<KeyPanelRule />
<Link
href={`/admin/users?team=${team.teamId}`}
className="block truncate text-base font-semibold text-warm-900 underline-offset-2 hover:underline"
Expand Down Expand Up @@ -162,9 +170,10 @@ async function BaseballBody() {
return (
<div className="space-y-6">
<section className="grid gap-4 xl:grid-cols-[minmax(0,1.35fr)_minmax(360px,0.65fr)]">
<Surface padding="sm" className="border-l-2 border-l-accent-500">
<Surface padding="sm">
<div className="flex flex-wrap items-start justify-between gap-3">
<div>
<KeyPanelRule />
<p className="text-xs font-semibold uppercase tracking-widest text-warm-500">Baseball command center</p>
<h2 className="mt-2 text-2xl font-semibold tracking-normal text-warm-900">
Team-by-team, player-by-player visibility
Expand Down
10 changes: 9 additions & 1 deletion src/app/admin/golf/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ function SectionLabel({ children }: { children: React.ReactNode }) {
);
}

// Dateline rule — replaces the retired border-l-2 "key panel" left-edge
// stripe. Chrome, not a status signal: a helm-green h-[2px] w-7 rounded-full
// rule above the card title.
function KeyPanelRule() {
return <span aria-hidden className="mb-3 block h-[2px] w-7 rounded-full bg-accent-500" />;
}

async function GolfBody() {
const tab = await fetchGolfTab();
const r = tab.rollup.rounds;
Expand Down Expand Up @@ -91,7 +98,8 @@ async function GolfBody() {
{/* CoachHelm engine health — key panel: the AI engine that makes
CoachHelm worth paying for, so it earns the same 2px green left
edge as the overview's Feature health rollup. */}
<Surface padding="sm" className="border-l-2 border-l-accent-500">
<Surface padding="sm">
<KeyPanelRule />
<SectionLabel>CoachHelm engine health</SectionLabel>
<p className="mt-2 text-sm text-warm-700">
Last insight generated:{' '}
Expand Down
14 changes: 9 additions & 5 deletions src/app/admin/golf/tracer/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,14 +73,18 @@ async function TracerBody() {
className={
i === 0
// Leader row — the single most-active player by rounds
// logged, sorted just above: soft green wash + 2px green
// left bar, the same "leader" treatment used everywhere
// else in the console a list is genuinely rank-ordered.
? 'flex flex-wrap items-center gap-x-3 gap-y-1 border-l-2 border-l-accent-500 bg-accent-50/60 py-2 pl-2 text-sm'
// logged, sorted just above: soft green wash + a dateline
// rule above the name, the same "leader" treatment used
// everywhere else in the console a list is genuinely
// rank-ordered. Replaces the retired border-l-2 left bar.
? 'flex flex-wrap items-center gap-x-3 gap-y-1 bg-accent-50/60 py-2 pl-2 text-sm'
: 'flex flex-wrap items-center gap-x-3 gap-y-1 py-2 text-sm'
}
>
<span className="min-w-0 flex-1 basis-full truncate text-warm-900 sm:basis-auto">{playerName(p)}</span>
<span className="min-w-0 flex-1 basis-full sm:basis-auto">
{i === 0 && <span aria-hidden className="mb-1 block h-[2px] w-7 rounded-full bg-accent-500" />}
<span className="block truncate text-warm-900">{playerName(p)}</span>
</span>
<span className="font-fw-mono text-xs tabular-nums text-warm-600">
{p.completed_rounds}/{p.total_rounds} completed
</span>
Expand Down
10 changes: 9 additions & 1 deletion src/app/admin/jobs/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,13 @@ function SectionLabel({ children }: { children: React.ReactNode }) {
);
}

// Dateline rule — replaces the retired border-l-2 "key panel" left-edge
// stripe. Chrome, not a status signal: a helm-green h-[2px] w-7 rounded-full
// rule above the card title.
function KeyPanelRule() {
return <span aria-hidden className="mb-3 block h-[2px] w-7 rounded-full bg-accent-500" />;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

function formatDuration(ms: number | null): string {
if (ms == null) return '—';
if (ms < 1000) return `${ms}ms`;
Expand Down Expand Up @@ -130,7 +137,8 @@ async function JobsBody() {
{/* Key panel: the board that answers "is anything actually running on
schedule" — a 2px green left edge, same signal as the overview's
Feature health rollup. */}
<Surface padding="sm" className="border-l-2 border-l-accent-500">
<Surface padding="sm">
<KeyPanelRule />
<SectionLabel>Cron board — expected vs actual</SectionLabel>
<p className="mt-1 text-xs text-warm-500">
A job with no row yet reads &ldquo;awaiting first run&rdquo; (neutral) — never a red alarm until it has
Expand Down
4 changes: 3 additions & 1 deletion src/app/admin/teams/[id]/RosterTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,10 @@ export function RosterTable({ roster }: { roster: RosterDisplayRow[] }) {
const isLeader = maxRounds30d > 0 && r.activityStatus === 'active' && r.rounds30d === maxRounds30d;
const isDormant = r.activityStatus === 'dormant';
return (
<tr key={r.playerId} className={cn(isLeader && 'border-l-2 border-l-accent-500 bg-accent-50')}>
<tr key={r.playerId} className={cn(isLeader && 'bg-accent-50')}>
<td className={cn('sticky left-0 z-10 py-2 pr-3', isLeader ? 'bg-accent-50' : 'bg-surface')}>
{/* Dateline rule — replaces the retired border-l-2 leader stripe. */}
{isLeader && <span aria-hidden className="mb-1 block h-[2px] w-7 rounded-full bg-accent-500" />}
<Link
href={r.href}
className={cn(
Expand Down
19 changes: 12 additions & 7 deletions src/app/admin/teams/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,14 @@ function GraphiteStat({
);
}

// "Key panel" chrome: a 2px helm-green left edge on the structural cards of
// this page (header, activity, coachhelm) — a chrome constant, not a status
// signal, so it applies regardless of whether the panel's content is good or
// bad news.
const KEY_PANEL = 'border-l-2 border-l-accent-500';
// "Key panel" chrome: a dateline rule (h-[2px] w-7 rounded-full, helm-green)
// above the title of the structural cards of this page (header, activity,
// coachhelm) — a chrome constant, not a status signal, so it applies
// regardless of whether the panel's content is good or bad news. Replaces
// the retired border-l-2 left-edge stripe.
function KeyPanelRule() {
return <span aria-hidden className="mb-3 block h-[2px] w-7 rounded-full bg-accent-500" />;
}

async function TeamDetailBody({ teamId }: { teamId: string }) {
const detail = await fetchTeamDetail(teamId);
Expand Down Expand Up @@ -115,9 +118,10 @@ async function TeamDetailBody({ teamId }: { teamId: string }) {

return (
<div className="space-y-6">
<Surface padding="sm" className={KEY_PANEL}>
<Surface padding="sm">
<div className="flex flex-wrap items-start justify-between gap-4">
<div className="min-w-0">
<KeyPanelRule />
<div className="flex flex-wrap items-center gap-2">
<h1 className="truncate text-xl font-semibold text-warm-900">{team.name}</h1>
<StatusPill tone={HEALTH_TONE[health]} dot size="sm">
Expand Down Expand Up @@ -224,7 +228,8 @@ async function TeamDetailBody({ teamId }: { teamId: string }) {
</div>
</Surface>

<Surface padding="sm" className={KEY_PANEL}>
<Surface padding="sm">
<KeyPanelRule />
<SectionLabel>CoachHelm</SectionLabel>
<div className="mt-3">
{coaches.length === 0 || extras.coachhelmInsights === null ? (
Expand Down
4 changes: 3 additions & 1 deletion src/app/admin/users/[id]/EngagementPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,9 @@ export async function EngagementPanel({ userId }: { userId: string }) {
const engagement = await fetchUserEngagement(userId);

return (
<Surface padding="sm" className="border-l-2 border-l-accent-500">
<Surface padding="sm">
{/* Dateline rule — replaces the retired border-l-2 "key panel" stripe. */}
<span aria-hidden className="mb-3 block h-[2px] w-7 rounded-full bg-accent-500" />
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
<h2 className="text-xs font-semibold uppercase tracking-widest text-warm-500">Engagement</h2>
<div className="mt-3">
{engagement === null ? (
Expand Down
30 changes: 26 additions & 4 deletions src/app/baseball/(dashboard)/BaseballFairwayShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,21 @@ function BaseballFairwayContent({
const name = coach?.full_name || (player ? `${player.first_name} ${player.last_name}` : 'User');
const avatarUrl = coach?.avatar_url || player?.avatar_url || undefined;
const teamName = selectedTeam?.name || coach?.organization?.name;
// Stable identity across pathname-only re-renders (perf packet
// [shell-render-hygiene]) — was a fresh object literal every render,
// defeating FairwaySidebar's React.memo (it reads `user` from AppShell).
const shellUser = useMemo(
() => ({ name, teamName: teamName ?? undefined, avatarUrl: avatarUrl ?? undefined }),
[name, teamName, avatarUrl],
);

// Same packet, element props: inline JSX literals are fresh objects every
// render, so passing them straight into AppShell defeats the React.memo on
// FairwaySidebar/FairwayTopBar (which receive them verbatim) — and AppShell's
// own sidebarProps useMemo, which lists `brand` as a dependency.
const brand = useMemo(() => <Brand homeHref={homeHref} />, [homeHref]);
const sidebarFooter = useMemo(() => <ShellFooter />, []);
const topBarActions = useMemo(() => <NotificationBell />, []);

// Imperative open (mirrors GolfHelm's FairwayDashboardShell): the shell's
// own ⌘K entry point dispatches the same global event CommandPalette listens
Expand Down Expand Up @@ -631,14 +646,21 @@ function BaseballFairwayContent({

<AppShell
sections={sections}
user={{ name, teamName: teamName ?? undefined, avatarUrl: avatarUrl ?? undefined }}
brand={<Brand homeHref={homeHref} />}
sidebarFooter={<ShellFooter />}
topBarActions={<NotificationBell />}
user={shellUser}
brand={brand}
sidebarFooter={sidebarFooter}
topBarActions={topBarActions}
pathname={pathname}
linkComponent={shellLink}
breadcrumbs={breadcrumbs}
collapsible
// The dashboard route `template.tsx` already owns the route-reveal
// fade (one keyed motion div). Disabling the shell's own
// RouteTransition here prevents BOTH from fading on navigation —
// that compounded the opacity and read as a heavy, laggy
// double-fade. One fade, one source of truth (mirrors golf's
// FairwayDashboardShell.tsx).
disableRouteTransition
mobileOpen={mobileOpen}
onMobileOpenChange={setMobileOpen}
onSearchOpen={openCommandPalette}
Expand Down
Loading
Loading