import { useEffect, useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, Popover, PopoverContent, PopoverTrigger, } from "@godxjp/ui/data-display"; import { Checkbox, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, } from "@godxjp/ui/data-entry"; import { Skeleton } from "@godxjp/ui/feedback"; import { Button } from "@godxjp/ui/general"; import { Flex, PageContainer } from "@godxjp/ui/layout"; import { ChevronDown } from "lucide-react"; /** * Command — キーボードナビゲーション対応のコマンドパレット。 * 必ずツリー構造を守る: Command > CommandInput + CommandList > CommandGroup > CommandItem。 * CommandItem は必ず CommandList の内側に配置する。 * CommandEmpty は CommandList の内側に置くと自動で表示/非表示が切り替わる。 * Composed only from real @godxjp/ui components. */ type Suggestion = { value: string; label: string }; const VENDOR_DIRECTORY: Suggestion[] = [ { value: "v-001", label: "株式会社山田商事" }, { value: "v-002", label: "山下電機株式会社" }, { value: "v-003", label: "やまと運輸サービス" }, { value: "v-004", label: "佐藤工業株式会社" }, { value: "v-005", label: "田中物産" }, ]; const MEMBERS: Suggestion[] = [ { value: "m-001", label: "山田 太郎" }, { value: "m-002", label: "佐藤 花子" }, { value: "m-003", label: "鈴木 一郎" }, { value: "m-004", label: "高橋 美咲" }, { value: "m-005", label: "PHAM THAI DUONG(プロジェクト外)" }, ]; export default function Demo() { // Card 1: grouped quick-select. Controlled `value` pre-highlights an item so the // [aria-selected=true] accent is visible at rest, without keyboard interaction. const [account, setAccount] = useState("1020"); // Card 2: action palette — its own independent state (Rule #6). const [action, setAction] = useState(""); // Card 3: async server-side search. `shouldFilter={false}` + controlled input + // an externally-fetched result list, staged mid-flight so the loading row renders. const [query, setQuery] = useState("やま"); const [loading, setLoading] = useState(true); const [results, setResults] = useState([]); // Card 4: filter facet — a checklist, so the list is `split` (ruled rows, 0 list padding). const [assignees, setAssignees] = useState(["m-002"]); const toggleAssignee = (member: string) => setAssignees((current) => current.includes(member) ? current.filter((m) => m !== member) : [...current, member], ); useEffect(() => { let active = true; setLoading(true); // Simulated server round-trip; the delay keeps the loading state observable at rest. const timer = setTimeout(() => { if (!active) return; const q = query.trim(); setResults(q ? VENDOR_DIRECTORY.filter((v) => v.label.includes(q)) : []); setLoading(false); }, 1500); return () => { active = false; clearTimeout(timer); }; }, [query]); return ( 勘定科目クイック選択 グループ化された科目リストをキーボードで素早く選択。 ハイライト中の科目を value で制御(現在: {account || "未選択"})。 該当する科目がありません。 1010 · 現金 1020 · 普通預金 1030 · 売掛金 1040 · 前払費用 2010 · 買掛金 2020 · 未払費用 2030 · 廃止済科目(選択不可) 4010 · 売上高 4020 · 受取利息 5010 · 売上原価 5020 · 給料手当 5030 · 旅費交通費 5040 · 通信費 担当者フィルター(split) Popover + Command に split を付けると、行間に区切り線・リスト余白 0・行は popover の端まで届く一つの箱として描画されます。チェックボックスで複数選択(選択中:{" "} {assignees.length} 名)。 該当する担当者がいません。 {MEMBERS.map((member) => { const checked = assignees.includes(member.value); return ( toggleAssignee(member.value)} > {/* The ROW is the option and owns the click; the box only mirrors it, so it leaves the tab ring (TreeSelect does the same). */} toggleAssignee(member.value)} aria-labelledby={`assignee-${member.value}`} /> {member.label} ); })} 勤怠アクションパレット(Cmd+K 想定) ページ横断のクイックアクション。キーワードで絞り込み、Enter で実行。 実行:{" "} {action || "なし"} 一致するアクションがありません。 出勤打刻 退勤打刻 休憩開始 有給休暇申請 時間外勤務申請 テレワーク申請 ダッシュボードへ移動 勤怠一覧へ移動 取引先サーバー検索(非同期) shouldFilter={"{false}"} で内蔵フィルタを無効化し、入力値(value / onValueChange)でサーバー検索した結果だけを表示。 取得中はローディング行、0 件は CommandEmpty を表示。 {loading ? ( ) : ( <> 該当する取引先が見つかりません。 {results.length > 0 ? ( {results.map((v) => ( undefined}> {v.label} ))} ) : null} )} ); }