/**
* Showcase · table-states — 状態 (V16)
*
* The five states a 勤怠 (attendance) DataTable must render — each shown statically,
* side by side, so a reader learns every state at rest without clicking:
*
* 1. loading … SkeletonTable inside the real Card+toolbar chrome, preserving
* the column layout (no spinner overlay).
* 2. empty … two distinct wordings — filter-empty (絞り込み結果なし, offers
* 「条件をクリア」) vs truly-empty (データなし, offers a create CTA).
* 3. error … Alert tone="destructive" carrying an error code + request-id
* (copyable, tabular-nums) + a 再試行 retry action.
* 4. partial … rows that DID load, with a warning Alert that one segment
* failed / is stale, plus a 部分再取得 retry for the missing part.
* 5. ideal … the fully-populated DataTable (sorted, status Badges).
*
* Built ENTIRELY from real @godxjp/ui primitives — DataTable / Table chrome via
* Card + DataTable.Toolbar, Skeleton family, Alert family, EmptyState, Badge,
* Button. No hand-rolled
, no raw controls, tokens only.
*
* DNA: compact density · small headings · tabular-nums on 時刻/件数 · fixed color
* signaling (success 若竹 / warning 山吹 / attention 朱 / danger 茜 / info 群青 via
* tone) · quiet factual JP copy · no emoji.
*/
import * as React from "react";
import { Clock, Inbox, Plus, RefreshCw, SearchX } from "lucide-react";
import { Button, Heading, Text } from "@godxjp/ui/general";
import {
Badge,
Card,
CardAction,
CardContent,
CardHeader,
CardTitle,
DataTable,
EmptyState,
type ColumnDef,
} from "@godxjp/ui/data-display";
import {
Alert,
AlertActions,
AlertDescription,
AlertTitle,
SkeletonTable,
} from "@godxjp/ui/feedback";
import { Flex, PageContainer } from "@godxjp/ui/layout";
// ── Domain: 勤怠 (打刻) records ────────────────────────────────────────────────
type Punch = {
id: string;
employee: string;
dept: string;
clockIn: string;
clockOut: string;
workH: string;
status: "present" | "late" | "early" | "pending";
};
/** 勤怠ステータス → Badge tone (fixed color signaling). */
const STATUS_META: Record<
Punch["status"],
{ label: string; tone: "success" | "warning" | "info" | "neutral" }
> = {
// 出勤 = 若竹(success) · 遅刻 = 山吹(warning, non-destructive lateness) ·
// 早退 = 群青(info) · 承認待ち = neutral(quiet, not a warning yet).
present: { label: "出勤", tone: "success" },
late: { label: "遅刻", tone: "warning" },
early: { label: "早退", tone: "info" },
pending: { label: "承認待ち", tone: "neutral" },
};
const COLUMNS: ColumnDef[] = [
{ key: "employee", header: "従業員", width: "w-40" },
{ key: "dept", header: "部署", hiddenOnMobile: true },
{
key: "clockIn",
header: "出勤",
align: "right",
render: (r) => {r.clockIn},
},
{
key: "clockOut",
header: "退勤",
align: "right",
render: (r) => {r.clockOut},
},
{
key: "workH",
header: "実働",
align: "right",
hiddenOnMobile: true,
render: (r) => {r.workH},
},
{
key: "status",
header: "状態",
align: "center",
render: (r) => (
{STATUS_META[r.status].label}
),
},
];
const ROWS: Punch[] = [
{
id: "EMP-1042",
employee: "佐藤 美咲",
dept: "受付",
clockIn: "08:58",
clockOut: "18:02",
workH: "8.0h",
status: "present",
},
{
id: "EMP-1043",
employee: "鈴木 一郎",
dept: "倉庫",
clockIn: "09:14",
clockOut: "18:05",
workH: "7.8h",
status: "late",
},
{
id: "EMP-1044",
employee: "高橋 結衣",
dept: "経理",
clockIn: "08:55",
clockOut: "16:30",
workH: "6.6h",
status: "early",
},
{
id: "EMP-1045",
employee: "田中 健太",
dept: "配送",
clockIn: "08:49",
clockOut: "—",
workH: "—",
status: "pending",
},
{
id: "EMP-1046",
employee: "伊藤 さくら",
dept: "受付",
clockIn: "08:52",
clockOut: "18:00",
workH: "8.1h",
status: "present",
},
];
/** Only the segment that succeeded in the `partial` case (倉庫 fetch failed). */
const PARTIAL_ROWS = ROWS.filter((r) => r.dept !== "倉庫");
// ── Local chrome: a titled section that frames each state demo ─────────────────
function StateSection({
step,
title,
note,
children,
}: {
step: string;
title: string;
note: string;
children: React.ReactNode;
}) {
return (
{step}
{title}
{note}
{children}
);
}
/** Shared table-card shell: the toolbar chrome stays identical across states so
* the loading skeleton visibly preserves the table's column layout. */
function TableShell({
caption,
banner,
children,
}: {
caption: React.ReactNode;
/** Padded band between the header and the flush table body (an Alert). */
banner?: React.ReactNode;
children?: React.ReactNode;
}) {
return (
本日の打刻
{caption}
{banner ? {banner} : null}
{children ? {children} : null}
);
}
export default function Demo() {
return (
{/* 1 ─ loading: skeleton that preserves the column layout */}
{/* 2 ─ empty: filter-empty vs truly-empty wording */}
{/* filter-empty: data exists, the FILTER matched nothing → clear filters */}
r.id}
empty={
条件をクリア
}
/>
}
/>
{/* truly-empty: no data at all for the day → create / 打刻登録 CTA */}
r.id}
empty={
打刻を登録
}
/>
}
/>
{/* 3 ─ error: code + request-id + retry */}
勤怠データを取得できませんでした
サーバーが応答しませんでした。時間をおいて再試行してください。
コード ATTEND_FETCH_504 ・ リクエストID{" "}
req_7f3a91c0d2
}
/>
{/* 4 ─ partial: some rows loaded, one segment failed / stale */}
一部のデータを取得できませんでした
「倉庫」部署の打刻が取得できなかったため、最新ではない可能性があります。
コード ATTEND_PARTIAL_倉庫 ・ リクエストID{" "}
req_7f3a91c0e8
}
>
r.id} />
{/* 5 ─ ideal: fully populated */}
5 件 · 14:32 更新
}
>
r.id} />
);
}