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.
132 lines
4.2 KiB
React
132 lines
4.2 KiB
React
// Sidebar — workspace switcher + file tree.
|
|
|
|
const Sidebar = ({ workspace, workspaces, onCycleWorkspace, openFile, activeFile, onOpenFile }) => {
|
|
const ws = workspaces[workspace];
|
|
return (
|
|
<aside style={ssStyles.root}>
|
|
<button style={ssStyles.ws} onClick={onCycleWorkspace} title="Switch workspace">
|
|
<span style={{ display:'flex', alignItems:'center', gap: 8 }}>
|
|
<Icon name="logo" size={20}/>
|
|
<span style={ssStyles.wsName}>{ws.name}</span>
|
|
</span>
|
|
<span style={ssStyles.wsHint}>⌘1</span>
|
|
</button>
|
|
|
|
<div style={ssStyles.search}>
|
|
<Icon name="search" size={14} style={{ color: 'var(--fg-3)' }}/>
|
|
<input placeholder="Search files…" style={ssStyles.searchInput}/>
|
|
<span style={ssStyles.kbd}>⌘K</span>
|
|
</div>
|
|
|
|
<div style={ssStyles.head}>EXPLORER</div>
|
|
<div style={ssStyles.tree}>
|
|
{ws.tree.map(node => (
|
|
<TreeNode key={node.path} node={node} depth={0}
|
|
activeFile={activeFile} onOpenFile={onOpenFile}/>
|
|
))}
|
|
</div>
|
|
|
|
<div style={ssStyles.foot}>
|
|
<Icon name="settings" size={14}/>
|
|
<span>settings</span>
|
|
</div>
|
|
</aside>
|
|
);
|
|
};
|
|
|
|
const TreeNode = ({ node, depth, activeFile, onOpenFile }) => {
|
|
const [open, setOpen] = React.useState(node.open !== false);
|
|
const isFile = !node.children;
|
|
const active = activeFile === node.path;
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
style={{
|
|
...ssStyles.row,
|
|
paddingLeft: 8 + depth * 14,
|
|
background: active ? 'rgba(99,136,216,0.18)' : undefined,
|
|
color: active ? '#a4c4ff' : 'var(--fg-1)',
|
|
}}
|
|
onClick={() => isFile ? onOpenFile(node.path) : setOpen(o => !o)}
|
|
>
|
|
{!isFile && <Icon name={open ? "chevron-d" : "chevron-r"} size={12} style={{ color:'var(--fg-3)' }}/>}
|
|
{isFile && <span style={{ width: 12 }}/>}
|
|
<Icon name={isFile ? "file" : "folder"} size={14}
|
|
style={{ color: isFile ? 'var(--fg-2)' : '#42dcd1' }}/>
|
|
<span style={ssStyles.name}>{node.name}</span>
|
|
{node.modified && <span style={ssStyles.modified}>●</span>}
|
|
</div>
|
|
{!isFile && open && node.children.map(c => (
|
|
<TreeNode key={c.path} node={c} depth={depth+1}
|
|
activeFile={activeFile} onOpenFile={onOpenFile}/>
|
|
))}
|
|
</>
|
|
);
|
|
};
|
|
|
|
const ssStyles = {
|
|
root: {
|
|
width: 260, flex: '0 0 260px',
|
|
background: '#1f222d',
|
|
borderRight: '1px solid #373b46',
|
|
display: 'flex', flexDirection: 'column',
|
|
fontFamily: 'var(--font-sans)',
|
|
color: 'var(--fg-1)',
|
|
},
|
|
ws: {
|
|
display: 'flex', alignItems:'center', justifyContent:'space-between',
|
|
padding: '12px 14px',
|
|
background: 'transparent', border: 0, borderBottom: '1px solid #373b46',
|
|
color: 'var(--fg-0)',
|
|
cursor: 'pointer', textAlign: 'left',
|
|
fontFamily: 'var(--font-sans)',
|
|
},
|
|
wsName: { fontSize: 14, fontWeight: 600, letterSpacing: '-0.005em' },
|
|
wsHint: {
|
|
fontFamily: 'var(--font-mono)', fontSize: 10,
|
|
color: 'var(--fg-3)', background: '#1b1e28',
|
|
padding: '2px 6px', borderRadius: 4, border: '1px solid #414751',
|
|
},
|
|
search: {
|
|
display: 'flex', alignItems:'center', gap: 8,
|
|
margin: '10px 12px',
|
|
padding: '6px 10px',
|
|
background: '#1b1e28',
|
|
border: '1px solid #414751',
|
|
borderRadius: 6,
|
|
},
|
|
searchInput: {
|
|
flex: 1, background: 'transparent', border: 0, outline: 'none',
|
|
color: 'var(--fg-1)', fontFamily: 'var(--font-sans)', fontSize: 12,
|
|
},
|
|
kbd: {
|
|
fontFamily: 'var(--font-mono)', fontSize: 10,
|
|
color: 'var(--fg-3)',
|
|
},
|
|
head: {
|
|
fontFamily: 'var(--font-mono)', fontSize: 10,
|
|
textTransform: 'uppercase', letterSpacing: '0.08em',
|
|
color: 'var(--fg-3)',
|
|
padding: '8px 14px 4px',
|
|
},
|
|
tree: { flex: 1, overflowY: 'auto', paddingBottom: 8 },
|
|
row: {
|
|
display: 'flex', alignItems: 'center', gap: 6,
|
|
height: 26, padding: '0 10px 0 8px',
|
|
fontSize: 13,
|
|
cursor: 'pointer',
|
|
userSelect: 'none',
|
|
},
|
|
name: { flex: 1, whiteSpace:'nowrap', overflow:'hidden', textOverflow:'ellipsis' },
|
|
modified: { color:'#42dcd1', fontSize: 12 },
|
|
foot: {
|
|
display:'flex', alignItems:'center', gap:8,
|
|
padding: '10px 14px',
|
|
borderTop: '1px solid #373b46',
|
|
fontSize: 12, color: 'var(--fg-2)',
|
|
},
|
|
};
|
|
|
|
window.Sidebar = Sidebar;
|