Ctrl K
ComunidadGuías…

src/components/dives/chart-period-controls.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import { ChevronLeft, ChevronRight } from "lucide-react";
import { CARD_TITLE_ACTION } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { IconTooltip } from "@/components/ui/tooltip";
import { NativeSelect } from "@/components/ui/native-select";
import {
  CHART_SCOPES,
  CHART_SCOPE_LABELS,
  periodLabel,
  periodRange,
} from "@/lib/chart-period";
import type { ChartPeriodState } from "@/hooks/useChartPeriodView";
import { cn } from "@/lib/utils";

export interface ChartPeriodControlsProps {
  // The card's name as every control's accessible name leads with it -
  // "Dive activity", "Gas consumption".
  name: string;
  view: ChartPeriodState<unknown>;
}

// A Home trend card's period stepper and All/Year/Month toggle. Two siblings
// rather than one box, laid out by the `CARD_TITLE_ROW` they sit in after the
// card's title, which is an `@container`: below its `2xl` the stepper wraps onto
// a line of its own.
//
// Every control here names its own card, because the two cards draw the same row
// and `Card` is a plain `div` - so nothing scopes them to each other. Read in
// place the heading is all the context you need, but a screen reader's controls
// list is flat names and nothing else, and four arrows reading "Previous period
// with dives" in it are four coin flips. The card name leads rather than trails
// so the list groups by chart when it is scanned or sorted. Same ambiguity
// `screenshots.mjs` hit from the automation side, where the fix was to scope by
// the card's own heading, which is exactly the context a controls list drops.
export function ChartPeriodControls({ name, view }: ChartPeriodControlsProps) {
  const { scope, anchor, setAnchor, setScope, previous, next, periods } = view;

  return (
    <>
      {/* Only the bounded scopes have a period to navigate. "All" already shows
          everything there is, so arrows on it would have nowhere to go. */}
      {scope !== "all" && (
        <div
          className={cn(
            "flex items-center gap-1 @max-2xl:order-last @max-2xl:min-w-0 @max-2xl:basis-full @max-2xl:justify-center",
            CARD_TITLE_ACTION,
          )}
        >
          <IconTooltip label={`${name}: previous period with dives`}>
            <Button
              variant="ghost"
              size="icon"
              className="h-9 w-9"
              disabled={previous === null}
              onClick={() => setAnchor(previous)}
            >
              <ChevronLeft className="h-4 w-4" />
            </Button>
          </IconTooltip>
          {/* The label is also the jump-to control - stepping one period at a
              time is fine for "the trip before this one", and useless for
              reaching one several seasons back.

              Its value is the *period's* start, never the anchor itself: the
              anchor is whichever dive or day you happened to land on, which
              usually matches no option, and a `<select>` whose value matches no
              option shows the first one instead. Fixed width so the chart
              doesn't shift sideways between "May 2026" and "September 2026",
              and wider once it wraps onto a line of its own - down to what
              the arrows leave of that line on a 320px phone. */}
          <div className="w-40 shrink-0 @max-2xl:w-56 @max-2xl:min-w-0 @max-2xl:shrink">
            <NativeSelect
              aria-label={`${name}: period`}
              className="h-9 pl-2 font-medium"
              value={String(periodRange(anchor, scope).start)}
              onChange={(event) => {
                const picked = periods.find(
                  (period) => String(period.start) === event.target.value,
                );
                if (picked) setAnchor(picked.anchor);
              }}
            >
              {periods.map((period) => (
                <option key={period.start} value={String(period.start)}>
                  {periodLabel(period.start, scope, "long")}
                </option>
              ))}
            </NativeSelect>
          </div>
          <IconTooltip label={`${name}: next period with dives`}>
            <Button
              variant="ghost"
              size="icon"
              className="h-9 w-9"
              disabled={next === null}
              onClick={() => setAnchor(next)}
            >
              <ChevronRight className="h-4 w-4" />
            </Button>
          </IconTooltip>
        </div>
      )}

      {/* A segmented control built from plain buttons - the app has no
          tabs/toggle-group primitive, and three buttons in a bordered row is the
          whole of it. */}
      <div
        className={cn(
          "flex h-9 items-stretch rounded-md border p-1 touch:h-11",
          CARD_TITLE_ACTION,
        )}
        role="group"
        aria-label={`${name}: time range`}
      >
        {CHART_SCOPES.map((option) => (
          <button
            key={option}
            type="button"
            onClick={() => setScope(option)}
            aria-pressed={scope === option}
            className={cn(
              "relative rounded px-2.5 text-xs font-medium transition-colors touch:min-w-11 touch:tap-target",
              scope === option
                ? "bg-secondary text-secondary-foreground"
                : "text-muted-foreground hover:text-foreground",
            )}
          >
            {CHART_SCOPE_LABELS[option]}
          </button>
        ))}
      </div>
    </>
  );
}