Added (all now linked from their live locations): - home_root/.claude/settings.json. The statusLine command now uses $HOME instead of a Linux-only absolute path. - home_root/.claude/bin/ratecheck and home_root/.claude/skills/australis-design. - home_config/zsh/claude-config-dir.zsh and home_root/.local/bin/claude-config-dir-init, the per-repo Claude Code login helper and its seeding script. - home_root/.gitconfig and home_config/git/ignore. - home_root/.zshenv, with the cargo env line guarded so boxes without rust start clean. link-dotfiles --check reports drift without changing anything: tracked files whose live copy is no longer a link (stow dry-run conflicts) and links into this repo that dangle. Exits 1 on drift. Verified against a clean baseline and two planted drifts, a detached .nanorc and a dangling link. .mailmap maps every earlier identity to Vuong Hoang: the repo-local 'Your Name' placeholder config (now removed), host-generated emails, and aider suffixes. CLAUDE.md: the symlink warning now lists every linked file and points at --check. README: documents what is and isn't tracked under .claude, the edit rule, and --check.
133 lines
4.7 KiB
React
133 lines
4.7 KiB
React
// CommandPalette — ⌘K overlay. Glass surface, blue focus ring.
|
|
|
|
const CommandPalette = ({ open, onClose, onRun }) => {
|
|
const [q, setQ] = React.useState('');
|
|
const [sel, setSel] = React.useState(0);
|
|
const inputRef = React.useRef(null);
|
|
|
|
const items = React.useMemo(() => [
|
|
{ id: 'build', label: 'Build project', hint: '⌘B', kind: 'Action' },
|
|
{ id: 'deploy', label: 'Deploy to prod', hint: '⌘D', kind: 'Action' },
|
|
{ id: 'theme', label: 'Theme: Australis Dark', hint: '⌘T', kind: 'Setting' },
|
|
{ id: 'tokens', label: 'Open tokens.css', hint: 'go', kind: 'File' },
|
|
{ id: 'aurora', label: 'Open aurora.ts', hint: 'go', kind: 'File' },
|
|
{ id: 'sign', label: 'Sign out', hint: '', kind: 'Action', danger: true },
|
|
], []);
|
|
|
|
const filtered = items.filter(i => i.label.toLowerCase().includes(q.toLowerCase()));
|
|
|
|
React.useEffect(() => {
|
|
if (open && inputRef.current) inputRef.current.focus();
|
|
setQ(''); setSel(0);
|
|
}, [open]);
|
|
|
|
React.useEffect(() => {
|
|
if (!open) return;
|
|
const onKey = (e) => {
|
|
if (e.key === 'Escape') onClose();
|
|
if (e.key === 'ArrowDown') { e.preventDefault(); setSel(s => Math.min(s + 1, filtered.length - 1)); }
|
|
if (e.key === 'ArrowUp') { e.preventDefault(); setSel(s => Math.max(s - 1, 0)); }
|
|
if (e.key === 'Enter' && filtered[sel]) { onRun(filtered[sel]); onClose(); }
|
|
};
|
|
window.addEventListener('keydown', onKey);
|
|
return () => window.removeEventListener('keydown', onKey);
|
|
}, [open, sel, filtered, onClose, onRun]);
|
|
|
|
if (!open) return null;
|
|
|
|
return (
|
|
<div style={cpStyles.scrim} onClick={onClose}>
|
|
<div style={cpStyles.panel} onClick={e => e.stopPropagation()}>
|
|
<div style={cpStyles.head}>
|
|
<Icon name="search" size={16} style={{ color: 'var(--fg-3)' }}/>
|
|
<input
|
|
ref={inputRef}
|
|
value={q}
|
|
onChange={e => { setQ(e.target.value); setSel(0); }}
|
|
placeholder="Type a command…"
|
|
style={cpStyles.input}
|
|
/>
|
|
<span style={cpStyles.esc}>esc</span>
|
|
</div>
|
|
<div style={cpStyles.list}>
|
|
{filtered.length === 0 && (
|
|
<div style={cpStyles.empty}>No matches.</div>
|
|
)}
|
|
{filtered.map((it, i) => (
|
|
<div key={it.id}
|
|
onMouseEnter={() => setSel(i)}
|
|
onClick={() => { onRun(it); onClose(); }}
|
|
style={{
|
|
...cpStyles.item,
|
|
background: i === sel ? 'rgba(99,136,216,0.18)' : 'transparent',
|
|
color: i === sel ? '#a4c4ff' : (it.danger ? '#ff854f' : 'var(--fg-1)'),
|
|
}}>
|
|
<Icon name={it.kind === 'File' ? 'file' : it.kind === 'Setting' ? 'settings' : 'play'} size={14}
|
|
style={{ color: i === sel ? '#a4c4ff' : 'var(--fg-3)' }}/>
|
|
<span style={{ flex: 1 }}>{it.label}</span>
|
|
<span style={cpStyles.kind}>{it.kind}</span>
|
|
{it.hint && <span style={cpStyles.hint}>{it.hint}</span>}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
const cpStyles = {
|
|
scrim: {
|
|
position: 'absolute', inset: 0,
|
|
background: 'rgba(34,37,49,0.72)',
|
|
backdropFilter: 'blur(4px)',
|
|
display: 'flex', alignItems: 'flex-start', justifyContent: 'center',
|
|
paddingTop: '15vh',
|
|
zIndex: 100,
|
|
},
|
|
panel: {
|
|
width: 540,
|
|
background: 'rgba(55,59,70,0.85)',
|
|
backdropFilter: 'blur(16px)',
|
|
border: '1px solid #565f69',
|
|
borderRadius: 10,
|
|
boxShadow: '0 24px 64px rgba(10,12,18,0.6), 0 8px 16px rgba(10,12,18,0.45)',
|
|
overflow: 'hidden',
|
|
},
|
|
head: {
|
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
padding: '12px 14px',
|
|
borderBottom: '1px solid #414751',
|
|
},
|
|
input: {
|
|
flex: 1, background: 'transparent', border: 0, outline: 'none',
|
|
color: 'var(--fg-0)',
|
|
fontFamily: 'var(--font-sans)', fontSize: 14,
|
|
},
|
|
esc: {
|
|
fontFamily: 'var(--font-mono)', fontSize: 10,
|
|
color: 'var(--fg-3)', background: '#1b1e28',
|
|
padding: '2px 6px', borderRadius: 4, border: '1px solid #414751',
|
|
},
|
|
list: { padding: 6, maxHeight: 360, overflowY: 'auto' },
|
|
item: {
|
|
display: 'flex', alignItems: 'center', gap: 10,
|
|
padding: '8px 10px',
|
|
borderRadius: 5,
|
|
fontSize: 13,
|
|
cursor: 'pointer',
|
|
},
|
|
kind: {
|
|
fontFamily: 'var(--font-mono)', fontSize: 10,
|
|
color: 'var(--fg-3)',
|
|
textTransform: 'uppercase', letterSpacing: '0.06em',
|
|
},
|
|
hint: {
|
|
fontFamily: 'var(--font-mono)', fontSize: 10,
|
|
color: 'var(--fg-3)', background: '#1b1e28',
|
|
padding: '2px 6px', borderRadius: 4, border: '1px solid #414751',
|
|
},
|
|
empty: { padding: 16, fontSize: 13, color: 'var(--fg-3)' },
|
|
};
|
|
|
|
window.CommandPalette = CommandPalette;
|