/** * 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: 操作 */} ); })}
{/* Legend — the fixed color-signaling map, shown at rest. */} 凡例 出勤 通常勤務 遅刻 非破壊的な注意 欠勤 未承認の不在 有休 有給休暇 公休 / 非番 ); }