sync: update from private repo (6c4d482)
CI / build-and-test (push) Has been cancelled

This commit is contained in:
oss-sync
2026-06-08 01:01:47 +00:00
parent caa0d03900
commit 03be80f036
21 changed files with 1140 additions and 15 deletions
@@ -0,0 +1,121 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import {
buildCommands, filterCommands, groupCommands,
type CommandContext, type CommandItem,
} from '../../lib/command-palette';
interface Props {
open: boolean;
onClose: () => void;
ctx: CommandContext;
}
export function CommandPalette({ open, onClose, ctx }: Props) {
const dialogRef = useRef<HTMLDialogElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const openerRef = useRef<Element | null>(null);
const [query, setQuery] = useState('');
const [highlight, setHighlight] = useState(0);
const allCommands = useMemo(() => buildCommands(ctx), [ctx]);
const results = useMemo(() => filterCommands(allCommands, query), [allCommands, query]);
const groups = useMemo(() => groupCommands(results), [results]);
useEffect(() => {
const dlg = dialogRef.current;
if (!dlg) return;
if (open && !dlg.open) {
openerRef.current = document.activeElement;
setQuery('');
setHighlight(0);
dlg.showModal();
inputRef.current?.focus();
} else if (!open && dlg.open) {
dlg.close();
const o = openerRef.current;
if (o instanceof HTMLElement && o.isConnected) o.focus();
else document.body.focus();
}
}, [open]);
useEffect(() => {
const dlg = dialogRef.current;
if (!dlg) return;
const onCancel = (e: Event) => { e.preventDefault(); onClose(); };
const onClick = (e: MouseEvent) => { if (e.target === dlg) onClose(); };
dlg.addEventListener('cancel', onCancel);
dlg.addEventListener('click', onClick);
return () => { dlg.removeEventListener('cancel', onCancel); dlg.removeEventListener('click', onClick); };
}, [onClose]);
useEffect(() => { setHighlight(0); }, [query]);
const flat = results;
const highlightedId = flat[highlight]?.id;
const onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'ArrowDown') { e.preventDefault(); setHighlight((h) => Math.min(h + 1, flat.length - 1)); }
else if (e.key === 'ArrowUp') { e.preventDefault(); setHighlight((h) => Math.max(h - 1, 0)); }
else if (e.key === 'Enter') {
e.preventDefault();
const item = flat[highlight];
if (item) { item.run(); onClose(); }
}
};
useEffect(() => {
if (!highlightedId) return;
document.getElementById(`cmdk-opt-${highlightedId}`)?.scrollIntoView({ block: 'nearest' });
}, [highlightedId]);
return (
<dialog
ref={dialogRef}
aria-label="コマンドパレット"
className="m-0 mt-[12vh] mx-auto w-[min(560px,92vw)] rounded-xl border border-hairline bg-surface text-ink shadow-2xl p-0 backdrop:bg-black/40"
>
<div className="p-2 border-b border-hairline">
<input
ref={inputRef}
role="combobox"
aria-expanded="true"
aria-controls="cmdk-listbox"
aria-label="コマンド・タスクを検索"
aria-activedescendant={highlightedId ? `cmdk-opt-${highlightedId}` : undefined}
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={onKeyDown}
placeholder="コマンド・タスクを検索…"
className="w-full h-9 px-2 bg-transparent outline-none text-sm"
/>
</div>
<div id="cmdk-listbox" role="listbox" className="max-h-[52vh] overflow-y-auto p-1">
{flat.length === 0 && <div role="status" className="px-3 py-6 text-center text-sm text-muted"></div>}
{groups.map((g) => (
<div key={g.group} role="group" aria-label={g.label}>
<div className="section-label px-2 pt-2 pb-1">{g.label}</div>
{g.items.map((item: CommandItem) => {
const active = item.id === highlightedId;
return (
<div
key={item.id}
id={`cmdk-opt-${item.id}`}
role="option"
aria-selected={active}
onMouseMove={() => setHighlight(flat.indexOf(item))}
onClick={() => { item.run(); onClose(); }}
className={`flex items-center justify-between gap-2 px-2.5 h-9 rounded-md cursor-pointer text-sm ${
active ? 'bg-surface-2 text-ink' : 'text-slate-600'
}`}
>
<span className="truncate">{item.label}</span>
{item.hint && <span className="text-2xs text-muted flex-shrink-0">{item.hint}</span>}
</div>
);
})}
</div>
))}
</div>
</dialog>
);
}