/**
* Showcase · table-sticky-columns — 固定列 (V13)
*
* Sticky/pinned first (識別子 = 従業員) + last (操作) columns over a
* horizontal-scroll 勤怠 matrix (week grid, `min-w` on the table). The pinned
* edges carry an inset shadow that reveals there is more content to scroll.
*
* GAP: @godxjp/ui `DataTable` / `ColumnDef` has NO column-pinning capability —
* it only sticks the HEADER row (top), not LEFT/RIGHT columns. So the pinned-
* column matrix is COMPOSED from the real `Table` family primitives
* (Table/TableHeader/TableBody/TableRow/TableHead/TableCell) with
* `sticky left-0` / `sticky right-0` + a background + edge shadow — no hand-
* rolled
, no raw HTML controls. Status/識別子 cells reuse Badge/Button.
*
* DNA applied: compact density, small headings, tabular-nums on the matrix,
* fixed color signaling (出勤 若竹/success · 遅刻 朱/attention via warning ·
* 欠勤 茜/destructive · 有休 群青/info), quiet JP copy, no emoji.
*/
import * as React from "react";
import { Check } from "lucide-react";
import { Button, Text } from "@godxjp/ui/general";
import {
Badge,
Card,
CardAction,
CardContent,
CardHeader,
CardTitle,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
type BadgeProps,
} from "@godxjp/ui/data-display";
import { Flex, PageContainer } from "@godxjp/ui/layout";
type BadgeTone = NonNullable;
// ── Domain: a one-week 勤怠 matrix per 従業員 ───────────────────────────────
/** Per-day attendance mark. Tone follows the fixed attendance signaling map. */
type Mark = "present" | "late" | "absent" | "paid" | "off";
const MARK: Record = {
present: { label: "出勤", tone: "success" }, // 若竹
late: { label: "遅刻", tone: "warning" }, // 朱/山吹 — non-destructive attention
absent: { label: "欠勤", tone: "destructive" }, // 茜
paid: { label: "有休", tone: "info" }, // 群青
off: { label: "—", tone: "muted" }, // 公休 / 非番
};
const DAYS = [
{ key: "mon", label: "月", date: "6/1" },
{ key: "tue", label: "火", date: "6/2" },
{ key: "wed", label: "水", date: "6/3" },
{ key: "thu", label: "木", date: "6/4" },
{ key: "fri", label: "金", date: "6/5" },
{ key: "sat", label: "土", date: "6/6" },
{ key: "sun", label: "日", date: "6/7" },
] as const;
type DayKey = (typeof DAYS)[number]["key"];
type Employee = {
id: string;
name: string;
dept: string;
marks: Record;
/** 実働合計(時間) for the week. */
total: number;
/** 遅刻回数 for the week. */
lateCount: number;
};
const EMPLOYEES: Employee[] = [
{
id: "E-1042",
name: "佐藤 美咲",
dept: "受付",
marks: {
mon: "present",
tue: "present",
wed: "late",
thu: "present",
fri: "present",
sat: "off",
sun: "off",
},
total: 38.5,
lateCount: 1,
},
{
id: "E-1043",
name: "鈴木 健一",
dept: "倉庫",
marks: {
mon: "present",
tue: "late",
wed: "late",
thu: "present",
fri: "absent",
sat: "present",
sun: "off",
},
total: 40.0,
lateCount: 2,
},
{
id: "E-1044",
name: "高橋 結衣",
dept: "受付",
marks: {
mon: "paid",
tue: "paid",
wed: "present",
thu: "present",
fri: "present",
sat: "off",
sun: "off",
},
total: 24.0,
lateCount: 0,
},
{
id: "E-1045",
name: "田中 大輔",
dept: "配送",
marks: {
mon: "present",
tue: "present",
wed: "present",
thu: "present",
fri: "present",
sat: "present",
sun: "off",
},
total: 46.0,
lateCount: 0,
},
{
id: "E-1046",
name: "渡辺 さくら",
dept: "配送",
marks: {
mon: "absent",
tue: "present",
wed: "present",
thu: "late",
fri: "present",
sat: "off",
sun: "off",
},
total: 31.5,
lateCount: 1,
},
{
id: "E-1047",
name: "伊藤 翔太",
dept: "倉庫",
marks: {
mon: "present",
tue: "present",
wed: "present",
thu: "paid",
fri: "paid",
sat: "off",
sun: "off",
},
total: 23.0,
lateCount: 0,
},
];
// ── Sticky-edge styling (composed — DataTable cannot pin columns) ───────────
/**
* Pinned columns must paint an opaque background (the scrolling body slides
* UNDER them) and a hard edge so the seam reads as "more to scroll". The edge
* is the `--border` token (`border-border`) — a token-only inset cue; the
* `bg-inherit` lets each row's tint (selected/hover) bleed into the pinned cell.
* Header cells inherit `bg-secondary`; body cells inherit the row background.
*/
const PIN_LEFT = "sticky start-0 z-20 bg-inherit border-e border-border";
const PIN_RIGHT = "sticky end-0 z-20 bg-inherit border-s border-border";
function MarkCell({ mark }: { mark: Mark }) {
const def = MARK[mark];
if (mark === "off") {
return (
—
);
}
return (
{def.label}
);
}
export default function Demo() {
const [approved, setApproved] = React.useState>(new Set(["E-1044"]));
const toggleApprove = (id: string) => {
setApproved((prev) => {
const next = new Set(prev);
if (next.has(id)) next.delete(id);
else next.add(id);
return next;
});
};
return (
2026年6月 第1週
従業員 {EMPLOYEES.length} 名 · 横スクロールで全日表示
{/* The scroll container. `overflow-x-auto` clips; the table holds a
min width wider than the viewport so the matrix must scroll. */}
{/* Pinned-left identifier header */}
従業員
{DAYS.map((d) => (
{/* Weekday over its date reads as ONE label — no seam between the lines. */}
{d.label}
{d.date}
))}
実働
{/* Pinned-right action header */}
操作
{EMPLOYEES.map((emp) => {
const isApproved = approved.has(emp.id);
return (
{/* Pinned-left: 識別子 (従業員 + 部署 + ID) */}
{emp.name}
{emp.dept} ·{" "}
{emp.id}
{/* Scrolling day grid */}
{DAYS.map((d) => (
))}
{/* 実働合計 — numeric, tabular */}
{emp.total.toFixed(1)}
h
{/* Pinned-right: 操作 */}
);
})}