"use client";
import { useTheme } from "@/components/theme/ThemeProvider";
import {
buildPageItems,
pageRange,
totalPageCount,
} from "@/lib/pagination/page-items";
import styles from "./Pagination.module.css";
/**
* 一覧のページ送り(指示書AA 段階2)
*
* ★判断はこの部品に置かない★ — 何ページあるか / どの番号を描くかは
* `lib/pagination/page-items.ts` の純粋関数が決める(`components/**` は
* `vitest.config.ts` の include に入っておらずテストが走らないため)。
* この部品がやるのは「返ってきたものを描く」「押されたページを親に通知する」だけ。
*
* ★`page` / `perPage` を内部 state にしないこと★ — 親が持つ。
* 取得(fetch)も親の仕事で、この部品は fetch しない。
*
* ★`components/ui/` で `useTheme()` を使う最初の部品★
* (既存の Pill / FilterButton / GradeBadge / StatCard / ProgressBar はどれも使っていない)。
* 使うのは ★文言の対訳 `t` を取るためだけ★ で、色は CSS 変数から取る
* (`THEME_TOKENS` は import しない)。件数表示の文言を各画面に散らすと
* `.replace()` の連鎖が6画面に増えるため、ここ1箇所に閉じ込めている。
*
* ★モックに前例が無いUI★ — `d-watch-mock.jsx` にページ送りは1つも無い。
* `CLAUDE.md` §5(モックが画面の正)の例外として、ユーザーの明示の承認
* (2026-09-11「ページ送りは作成してください」)に基づき新規に作っている。
*/
interface PaginationProps {
/**
* 総件数。
*
* ★`meta.total` を渡すこと。`summary.total` を渡さないこと★(D-054 / 指示書AA 禁止事項4)。
* `summary` は対応状況フィルタの**前**に計算されているため、一覧の件数とは別物で、
* 総ページ数がずれる。
*/
total: number;
/** 現在ページ(1始まり) */
page: number;
/**
* 1ページあたりの件数。
*
* ★`PAGINATION.defaultPerPage` を渡すこと。`50` を直書きしないこと★(`CLAUDE.md` §7)。
*/
perPage: number;
onPageChange: (page: number) => void;
}
export function Pagination({
total,
page,
perPage,
onPageChange,
}: PaginationProps) {
const { t } = useTheme();
const totalPages = totalPageCount(total, perPage);
// ★0件のときは部品ごと出さない★ — 各画面が「該当する〜はありません」を
// 既に出しているので、「全0件中 0〜0件」と二重に出るのを避ける
if (totalPages === 0) return null;
const { from, to } = pageRange(total, page, perPage);
const current = Math.min(Math.max(page, 1), totalPages);
const items = buildPageItems(current, totalPages);
// ★渡す値は必ず実在するページに丸める★(押した先が「有りません」にならないように)
function go(next: number) {
const clamped = Math.min(Math.max(next, 1), totalPages);
if (clamped !== current) onPageChange(clamped);
}
return (
<div className={styles.footer}>
<div className={styles.count}>
{t("全{total}件中 {from}〜{to}件")
.replace("{total}", String(total))
.replace("{from}", String(from))
.replace("{to}", String(to))}
</div>
{/* ★総ページ数が1のときは件数表示だけ出す★(押せないボタンを並べない) */}
{totalPages > 1 && (
<nav className={styles.nav} aria-label={t("ページ送り")}>
<button
type="button"
className={styles.step}
onClick={() => go(current - 1)}
disabled={current <= 1}
>
{t("前へ")}
</button>
{items.map((item) =>
item.kind === "page" ? (
<button
key={`p-${item.page}`}
type="button"
className={
item.page === current
? `${styles.num} ${styles.numActive}`
: styles.num
}
onClick={() => go(item.page)}
disabled={item.page === current}
aria-current={item.page === current ? "page" : undefined}
>
{item.page}
</button>
) : (
// ★「…」は button にしないこと★ — 押せる見た目にすると
// 「何ページに飛ぶのか」という存在しない問いが生まれる
<span key={item.key} className={styles.gap} aria-hidden="true">
…
</span>
)
)}
<button
type="button"
className={styles.step}
onClick={() => go(current + 1)}
disabled={current >= totalPages}
>
{t("次へ")}
</button>
</nav>
)}
</div>
);
}