import { useEffect, useRef, useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@godxjp/ui/data-display"; import { CommandPalette, type CommandPaletteItem, type CommandPaletteLabels, } from "@godxjp/ui/data-entry"; import { Button, Text } from "@godxjp/ui/general"; import { Flex, PageContainer } from "@godxjp/ui/layout"; const labels: CommandPaletteLabels = { open: "画面を検索", title: "コマンドパレット", description: "移動先または実行する操作を検索します。", placeholder: "画面や操作を検索…", empty: "一致する項目はありません。", loading: "候補を読み込み中…", move: "移動", select: "開く", close: "閉じる", }; const groups = [ { id: "screens", label: "画面", items: [ { id: "dashboard", label: "ダッシュボード", meta: "/dashboard" }, { id: "members", label: "メンバー管理", meta: "/organization/members" }, { id: "billing", label: "請求とサブスクリプション", meta: "権限が必要", disabled: true, }, ], }, { id: "actions", label: "操作", items: [{ id: "invite", label: "メンバーを招待", meta: "⌘ I" }], }, ]; const directory = [ { id: "tanaka", label: "田中 太郎", meta: "経理部" }, { id: "suzuki", label: "鈴木 花子", meta: "情報システム部" }, { id: "sato", label: "佐藤 健", meta: "営業部" }, { id: "takahashi", label: "高橋 美咲", meta: "人事部" }, ]; const serverLabels: CommandPaletteLabels = { ...labels, open: "メンバーを検索", title: "メンバー検索", description: "サーバー検索で該当するメンバーを表示します。", placeholder: "氏名または部署で検索…", empty: "該当するメンバーはいません。", loading: "検索中…", }; /** * Server-backed search-as-you-type. The consumer owns the query (`onSearchChange`) and the result * set, so `shouldFilter={false}` stops the palette from scoring the same rows a second time — and * with it off the palette derives the empty node from `groups`, never from cmdk's scheduled count. * `loading` is held for the whole in-flight window: a request that has not answered is not an * empty result, so "該当なし" can only appear on a query the server actually answered. */ function ServerSearchDemo() { const [query, setQuery] = useState(""); const [results, setResults] = useState(directory); const [loading, setLoading] = useState(false); const requestRef = useRef(0); useEffect(() => { const request = (requestRef.current += 1); setLoading(true); const timer = setTimeout(() => { if (request !== requestRef.current) return; const trimmed = query.trim(); setResults( trimmed === "" ? directory : directory.filter((entry) => `${entry.label}${entry.meta}`.includes(trimmed)), ); setLoading(false); }, 300); return () => clearTimeout(timer); }, [query]); return ( setQuery(String(item.label))} /> {`クエリ: ${query === "" ? "(未入力)" : query} · 結果 ${results.length} 件`} ); } /** * CommandPalette — searchable keyboard navigation with controlled async/error states and a durable * selected-item readout. Press Command/Ctrl+K or use the trigger. */ export default function Demo() { const [selected, setSelected] = useState(null); const [loading, setLoading] = useState(false); const [error, setError] = useState(); return ( 組織コンソールの移動先 Command/Ctrl+K で開き、矢印キーで移動し、Enter で選択します。 setSelected(item)} /> {selected ? `選択: ${String(selected.label)}` : "まだ項目を選択していません。"} サーバー検索(search-as-you-type) `onSearchChange` でクエリを受け取り、結果を `groups` として返します。`shouldFilter= false` でクライアント側の再フィルタを止め、空状態は `groups` から決まります。 ); }