feat: fold in the portable Claude Code, zsh and git config; add a drift check
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.
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
// 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;
|
||||
@@ -0,0 +1,85 @@
|
||||
// Editor — tabs + gutter + syntax-highlighted code.
|
||||
|
||||
const Editor = ({ openFiles, activeFile, onCloseFile, onSelectFile, fileContent }) => {
|
||||
return (
|
||||
<div style={edStyles.root}>
|
||||
<div style={edStyles.tabs}>
|
||||
{openFiles.map(f => {
|
||||
const active = f === activeFile;
|
||||
return (
|
||||
<div key={f}
|
||||
style={{ ...edStyles.tab, ...(active ? edStyles.tabActive : {}) }}
|
||||
onClick={() => onSelectFile(f)}
|
||||
>
|
||||
<Icon name="file" size={12}
|
||||
style={{ color: active ? '#42dcd1' : 'var(--fg-3)' }}/>
|
||||
<span>{f.split('/').pop()}</span>
|
||||
<span
|
||||
style={edStyles.close}
|
||||
onClick={(e) => { e.stopPropagation(); onCloseFile(f); }}
|
||||
>×</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div style={edStyles.tabSpacer}/>
|
||||
</div>
|
||||
|
||||
<div style={edStyles.body}>
|
||||
<div style={edStyles.gutter}>
|
||||
{(fileContent[activeFile] || []).map((_, i) => (
|
||||
<div key={i}>{i + 1}</div>
|
||||
))}
|
||||
</div>
|
||||
<div style={edStyles.code}>
|
||||
{(fileContent[activeFile] || []).map((line, i) => (
|
||||
<div key={i} style={edStyles.line}
|
||||
dangerouslySetInnerHTML={{ __html: line || ' ' }}/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const edStyles = {
|
||||
root: { flex: 1, display: 'flex', flexDirection: 'column',
|
||||
background: '#1b1e28', minWidth: 0 },
|
||||
tabs: {
|
||||
display: 'flex', background: '#222531',
|
||||
borderBottom: '1px solid #373b46', overflow: 'hidden',
|
||||
},
|
||||
tab: {
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '8px 14px',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 12,
|
||||
color: 'var(--fg-3)',
|
||||
borderRight: '1px solid #373b46',
|
||||
cursor: 'pointer',
|
||||
},
|
||||
tabActive: {
|
||||
background: '#1b1e28',
|
||||
color: 'var(--fg-0)',
|
||||
boxShadow: 'inset 0 2px 0 #6388d8',
|
||||
},
|
||||
tabSpacer: { flex: 1, borderBottom: '1px solid #373b46' },
|
||||
close: {
|
||||
fontSize: 14, color: 'var(--fg-3)',
|
||||
width: 14, height: 14, lineHeight: '12px', textAlign: 'center',
|
||||
borderRadius: 3,
|
||||
},
|
||||
body: { flex: 1, display: 'flex', overflow: 'auto',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13.5, lineHeight: 1.7 },
|
||||
gutter: {
|
||||
padding: '14px 12px 14px 18px',
|
||||
color: '#414751',
|
||||
textAlign: 'right',
|
||||
userSelect: 'none',
|
||||
background: '#1b1e28',
|
||||
borderRight: '1px solid #2a2e38',
|
||||
fontSize: 12.5,
|
||||
},
|
||||
code: { flex: 1, padding: '14px 20px', whiteSpace: 'pre' },
|
||||
line: {},
|
||||
};
|
||||
|
||||
window.Editor = Editor;
|
||||
@@ -0,0 +1,45 @@
|
||||
// Inline Lucide-style icons. 24×24 viewBox, 1.75 stroke, currentColor, no fill.
|
||||
// Keep the set small — add icons as the kit grows.
|
||||
|
||||
const Icon = ({ name, size = 18, style }) => {
|
||||
const props = {
|
||||
width: size, height: size, viewBox: "0 0 24 24",
|
||||
fill: "none", stroke: "currentColor",
|
||||
strokeWidth: 1.75, strokeLinecap: "round", strokeLinejoin: "round",
|
||||
style,
|
||||
};
|
||||
switch (name) {
|
||||
case "search": return <svg {...props}><circle cx="11" cy="11" r="7"/><path d="M21 21l-5-5"/></svg>;
|
||||
case "chevron-r": return <svg {...props}><path d="M9 6l6 6-6 6"/></svg>;
|
||||
case "chevron-d": return <svg {...props}><path d="M6 9l6 6 6-6"/></svg>;
|
||||
case "file": return <svg {...props}><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><path d="M14 2v6h6"/></svg>;
|
||||
case "folder": return <svg {...props}><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>;
|
||||
case "terminal": return <svg {...props}><path d="M4 17l6-6-6-6M12 19h8"/></svg>;
|
||||
case "settings": return <svg {...props}><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.8l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.8-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1.1-1.5 1.7 1.7 0 0 0-1.8.3l-.1.1A2 2 0 1 1 4.3 17l.1-.1a1.7 1.7 0 0 0 .3-1.8 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1A1.7 1.7 0 0 0 4.6 9a1.7 1.7 0 0 0-.3-1.8l-.1-.1A2 2 0 1 1 7 4.3l.1.1a1.7 1.7 0 0 0 1.8.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.8-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.8V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/></svg>;
|
||||
case "git-branch": return <svg {...props}><line x1="6" y1="3" x2="6" y2="15"/><circle cx="18" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><path d="M18 9a9 9 0 0 1-9 9"/></svg>;
|
||||
case "play": return <svg {...props}><polygon points="5 3 19 12 5 21 5 3"/></svg>;
|
||||
case "x": return <svg {...props}><path d="M18 6L6 18M6 6l12 12"/></svg>;
|
||||
case "check": return <svg {...props}><path d="M3 12l6 6 12-12"/></svg>;
|
||||
case "alert": return <svg {...props}><path d="M10.3 3.86a2 2 0 0 1 3.4 0l8.5 14a2 2 0 0 1-1.7 3H3.5a2 2 0 0 1-1.7-3z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>;
|
||||
case "command": return <svg {...props}><path d="M18 3a3 3 0 0 0-3 3v12a3 3 0 0 0 3 3 3 3 0 0 0 3-3 3 3 0 0 0-3-3H6a3 3 0 0 0-3 3 3 3 0 0 0 3 3 3 3 0 0 0 3-3V6a3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3h12a3 3 0 0 0 3-3 3 3 0 0 0-3-3z"/></svg>;
|
||||
case "circle": return <svg {...props}><circle cx="12" cy="12" r="4" fill="currentColor"/></svg>;
|
||||
case "plus": return <svg {...props}><path d="M3 12h18M12 3v18"/></svg>;
|
||||
case "logo": return (
|
||||
<svg width={size} height={size} viewBox="0 0 64 64" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="ig" x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="0%" stopColor="#a4c4ff"/>
|
||||
<stop offset="45%" stopColor="#42dcd1"/>
|
||||
<stop offset="100%" stopColor="#16b866"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="M10 48 C 14 22, 22 12, 28 8 L 30 8 C 26 18, 22 32, 22 48 Z" fill="url(#ig)" opacity="0.95"/>
|
||||
<path d="M24 48 C 30 28, 36 18, 42 12 L 44 12 C 38 22, 34 34, 34 48 Z" fill="url(#ig)" opacity="0.7"/>
|
||||
<path d="M38 48 C 44 32, 50 24, 56 20 L 58 20 C 52 28, 48 38, 48 48 Z" fill="url(#ig)" opacity="0.5"/>
|
||||
</svg>
|
||||
);
|
||||
default: return null;
|
||||
}
|
||||
};
|
||||
|
||||
window.Icon = Icon;
|
||||
@@ -0,0 +1,26 @@
|
||||
# Australis — Terminal UI Kit
|
||||
|
||||
The signature surface for Australis: a developer tool combining a terminal, a code editor, a file sidebar, and a status bar. This is what the system was built for.
|
||||
|
||||
## Files
|
||||
|
||||
- `index.html` — interactive demo wiring everything together
|
||||
- `Sidebar.jsx` — file tree + workspace switcher
|
||||
- `Editor.jsx` — tabbed code editor with syntax tokens
|
||||
- `Terminal.jsx` — bottom panel terminal with prompt + output
|
||||
- `StatusBar.jsx` — bottom rail (branch, syntax, line/col)
|
||||
- `CommandPalette.jsx` — ⌘K palette overlay
|
||||
- `Icon.jsx` — Lucide-style inline icons (1.75 stroke, currentColor)
|
||||
|
||||
## What's interactive
|
||||
|
||||
- Click sidebar files to switch editor tabs
|
||||
- Click the workspace pill in the top-left to cycle workspaces
|
||||
- Type into the terminal prompt and hit `Enter` — outputs are faked but feel right
|
||||
- Press `⌘K` (or click the search field) to open the command palette
|
||||
- Hover/focus states honor the system
|
||||
|
||||
## Visual sources
|
||||
|
||||
- `colors_and_type.css` — all tokens
|
||||
- `README.md` (root) — visual foundations and content rules
|
||||
@@ -0,0 +1,131 @@
|
||||
// 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;
|
||||
@@ -0,0 +1,44 @@
|
||||
// StatusBar — bottom rail. Mono, dense, 24px tall.
|
||||
|
||||
const StatusBar = ({ branch, problems, position, syntax }) => (
|
||||
<div style={sbStyles.root}>
|
||||
<div style={sbStyles.left}>
|
||||
<span style={sbStyles.cell}>
|
||||
<Icon name="git-branch" size={12}/> {branch}
|
||||
</span>
|
||||
<span style={{ ...sbStyles.cell, color: problems.errors ? '#ff854f' : 'var(--fg-2)' }}>
|
||||
<Icon name="x" size={12}/> {problems.errors}
|
||||
</span>
|
||||
<span style={{ ...sbStyles.cell, color: problems.warnings ? '#ffe14e' : 'var(--fg-2)' }}>
|
||||
<Icon name="alert" size={12}/> {problems.warnings}
|
||||
</span>
|
||||
</div>
|
||||
<div style={sbStyles.right}>
|
||||
<span style={sbStyles.cell}>{syntax}</span>
|
||||
<span style={sbStyles.cell}>UTF-8</span>
|
||||
<span style={sbStyles.cell}>LF</span>
|
||||
<span style={sbStyles.cell}>{position}</span>
|
||||
<span style={{ ...sbStyles.cell, color: '#51e08a' }}>
|
||||
<Icon name="circle" size={8}/> live
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const sbStyles = {
|
||||
root: {
|
||||
height: 26, flex: '0 0 26px',
|
||||
display: 'flex', justifyContent: 'space-between',
|
||||
padding: '0 14px',
|
||||
background: '#222531',
|
||||
borderTop: '1px solid #373b46',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11,
|
||||
color: 'var(--fg-2)',
|
||||
letterSpacing: '0.03em',
|
||||
},
|
||||
left: { display:'flex', alignItems:'center', gap: 14 },
|
||||
right: { display:'flex', alignItems:'center', gap: 14 },
|
||||
cell: { display:'flex', alignItems:'center', gap: 5 },
|
||||
};
|
||||
|
||||
window.StatusBar = StatusBar;
|
||||
@@ -0,0 +1,88 @@
|
||||
// Terminal — bottom panel, prompt + scrollback.
|
||||
|
||||
const Terminal = ({ lines, onCommand }) => {
|
||||
const [input, setInput] = React.useState('');
|
||||
const scrollRef = React.useRef(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (scrollRef.current) {
|
||||
scrollRef.current.scrollTop = scrollRef.current.scrollHeight;
|
||||
}
|
||||
}, [lines]);
|
||||
|
||||
const submit = (e) => {
|
||||
e.preventDefault();
|
||||
if (!input.trim()) return;
|
||||
onCommand(input);
|
||||
setInput('');
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={tmStyles.root}>
|
||||
<div style={tmStyles.head}>
|
||||
<Icon name="terminal" size={13} style={{ color:'#42dcd1' }}/>
|
||||
<span style={tmStyles.title}>terminal · zsh</span>
|
||||
<span style={tmStyles.spacer}/>
|
||||
<span style={tmStyles.host}>aurora ~/projects/australis</span>
|
||||
</div>
|
||||
<div ref={scrollRef} style={tmStyles.body}>
|
||||
{lines.map((ln, i) => (
|
||||
<div key={i} style={tmStyles.line}
|
||||
dangerouslySetInnerHTML={{ __html: ln }}/>
|
||||
))}
|
||||
<form onSubmit={submit} style={tmStyles.promptRow}>
|
||||
<span style={tmStyles.prompt}>➜</span>
|
||||
<input
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
placeholder="try: build, deploy, theme, help"
|
||||
style={tmStyles.input}
|
||||
autoFocus
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const tmStyles = {
|
||||
root: {
|
||||
height: 220, flex: '0 0 220px',
|
||||
background: '#1b1e28',
|
||||
borderTop: '1px solid #373b46',
|
||||
display: 'flex', flexDirection: 'column',
|
||||
},
|
||||
head: {
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
padding: '6px 14px',
|
||||
background: '#222531',
|
||||
borderBottom: '1px solid #373b46',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 11,
|
||||
color: 'var(--fg-3)',
|
||||
letterSpacing: '0.04em',
|
||||
},
|
||||
title: { color: 'var(--fg-2)' },
|
||||
spacer: { flex: 1 },
|
||||
host: { color: 'var(--fg-3)' },
|
||||
body: {
|
||||
flex: 1, overflowY: 'auto',
|
||||
padding: '10px 18px 14px',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13,
|
||||
lineHeight: 1.65,
|
||||
color: 'var(--fg-1)',
|
||||
},
|
||||
line: { whiteSpace: 'pre-wrap' },
|
||||
promptRow: {
|
||||
display: 'flex', alignItems: 'center', gap: 8,
|
||||
marginTop: 2,
|
||||
},
|
||||
prompt: { color: '#42dcd1' },
|
||||
input: {
|
||||
flex: 1, background: 'transparent', border: 0, outline: 'none',
|
||||
fontFamily: 'var(--font-mono)', fontSize: 13,
|
||||
color: 'var(--fg-0)',
|
||||
caretColor: '#42dcd1',
|
||||
},
|
||||
};
|
||||
|
||||
window.Terminal = Terminal;
|
||||
@@ -0,0 +1,285 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1"/>
|
||||
<title>Australis — Terminal UI Kit</title>
|
||||
<link rel="stylesheet" href="../../colors_and_type.css"/>
|
||||
<style>
|
||||
html, body { height: 100%; margin: 0; }
|
||||
body {
|
||||
background: var(--bg-0);
|
||||
color: var(--fg-1);
|
||||
font-family: var(--font-sans);
|
||||
overflow: hidden;
|
||||
}
|
||||
.shell {
|
||||
height: 100vh;
|
||||
display: flex; flex-direction: column;
|
||||
position: relative;
|
||||
}
|
||||
.titlebar {
|
||||
height: 36px; flex: 0 0 36px;
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 0 14px;
|
||||
background: #1b1e28;
|
||||
border-bottom: 1px solid #373b46;
|
||||
}
|
||||
.dots { display: flex; gap: 7px; }
|
||||
.dot { width: 11px; height: 11px; border-radius: 50%; }
|
||||
.titlename {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 11px;
|
||||
color: var(--fg-3);
|
||||
letter-spacing: 0.05em;
|
||||
flex: 1; text-align: center;
|
||||
}
|
||||
.pal-trigger {
|
||||
display:flex; align-items:center; gap:6px;
|
||||
background: #222531; border: 1px solid #414751;
|
||||
color: var(--fg-2);
|
||||
font-family: var(--font-sans); font-size: 11px;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.pal-trigger:hover { background:#373b46; color: var(--fg-0); }
|
||||
.body-row { flex: 1; display: flex; min-height: 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
||||
<script src="https://unpkg.com/react@18.3.1/umd/react.development.js" integrity="sha384-hD6/rw4ppMLGNu3tX5cjIb+uRZ7UkRJ6BPkLpg4hAu/6onKUg4lLsHAs9EBPT82L" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/react-dom@18.3.1/umd/react-dom.development.js" integrity="sha384-u6aeetuaXnQ38mYT8rp6sbXaQe3NL9t+IBXmnYxwkUI2Hw4bsp2Wvmx4yRQF1uAm" crossorigin="anonymous"></script>
|
||||
<script src="https://unpkg.com/@babel/standalone@7.29.0/babel.min.js" integrity="sha384-m08KidiNqLdpJqLq95G/LEi8Qvjl/xUYll3QILypMoQ65QorJ9Lvtp2RXYGBFj1y" crossorigin="anonymous"></script>
|
||||
|
||||
<script type="text/babel" src="Icon.jsx"></script>
|
||||
<script type="text/babel" src="Sidebar.jsx"></script>
|
||||
<script type="text/babel" src="Editor.jsx"></script>
|
||||
<script type="text/babel" src="Terminal.jsx"></script>
|
||||
<script type="text/babel" src="StatusBar.jsx"></script>
|
||||
<script type="text/babel" src="CommandPalette.jsx"></script>
|
||||
|
||||
<script type="text/babel">
|
||||
const FILE_CONTENT = {
|
||||
'src/aurora.ts': [
|
||||
`<span style="color:#6e7882;">// Render the southern sky.</span>`,
|
||||
`<span style="color:#9d78ff;">import</span> { <span style="color:#6388d8;">paint</span> } <span style="color:#9d78ff;">from</span> <span style="color:#16b866;">'./paint'</span>;`,
|
||||
`<span style="color:#9d78ff;">import</span> <span style="color:#a9bcc3;">type</span> { <span style="color:#42dcd1;">Sky</span> } <span style="color:#9d78ff;">from</span> <span style="color:#16b866;">'./types'</span>;`,
|
||||
``,
|
||||
`<span style="color:#9d78ff;">export const</span> <span style="color:#ffe14e;">LATITUDE</span> = <span style="color:#42dcd1;">-77.85</span>;`,
|
||||
`<span style="color:#9d78ff;">export const</span> <span style="color:#ffe14e;">CURTAINS</span> = <span style="color:#42dcd1;">3</span>;`,
|
||||
``,
|
||||
`<span style="color:#9d78ff;">export function</span> <span style="color:#6388d8;">render</span>(<span style="color:#a9bcc3;">name</span>: <span style="color:#42dcd1;">string</span>): <span style="color:#42dcd1;">Sky</span> {`,
|
||||
` <span style="color:#9d78ff;">if</span> (!<span style="color:#a9bcc3;">name</span>) <span style="color:#9d78ff;">throw new</span> <span style="color:#6388d8;">Error</span>(<span style="color:#16b866;">'name required'</span>);`,
|
||||
` <span style="color:#9d78ff;">return</span> <span style="color:#6388d8;">paint</span>(<span style="color:#a9bcc3;">name</span>, <span style="color:#ffe14e;">LATITUDE</span>, <span style="color:#ffe14e;">CURTAINS</span>);`,
|
||||
`}`,
|
||||
``,
|
||||
`<span style="color:#6e7882;">// Watch the sky every 800ms.</span>`,
|
||||
`<span style="color:#6388d8;">setInterval</span>(() <span style="color:#9d78ff;">=></span> <span style="color:#6388d8;">render</span>(<span style="color:#16b866;">'australis'</span>), <span style="color:#42dcd1;">800</span>);`,
|
||||
],
|
||||
'src/tokens.css': [
|
||||
`<span style="color:#6e7882;">/* Australis tokens — abridged */</span>`,
|
||||
`<span style="color:#00b1a8;">:root</span> {`,
|
||||
` <span style="color:#a9bcc3;">--bg-0</span>: <span style="color:#42dcd1;">#222531</span>;`,
|
||||
` <span style="color:#a9bcc3;">--bg-1</span>: <span style="color:#42dcd1;">#373b46</span>;`,
|
||||
` <span style="color:#a9bcc3;">--fg-0</span>: <span style="color:#42dcd1;">#cce7ec</span>;`,
|
||||
` <span style="color:#a9bcc3;">--fg-1</span>: <span style="color:#42dcd1;">#a9bcc3</span>;`,
|
||||
` <span style="color:#a9bcc3;">--aus-blue</span>: <span style="color:#42dcd1;">#6388d8</span>;`,
|
||||
` <span style="color:#a9bcc3;">--aus-cyan</span>: <span style="color:#42dcd1;">#00b1a8</span>;`,
|
||||
` <span style="color:#a9bcc3;">--aus-green</span>: <span style="color:#42dcd1;">#16b866</span>;`,
|
||||
`}`,
|
||||
],
|
||||
'README.md': [
|
||||
`<span style="color:#9d78ff;"># australis</span>`,
|
||||
``,
|
||||
`<span style="color:#a9bcc3;">A cool, calm dark theme inspired by the southern lights.</span>`,
|
||||
``,
|
||||
`<span style="color:#9d78ff;">## install</span>`,
|
||||
``,
|
||||
`<span style="color:#16b866;"> npm i @australis/core</span>`,
|
||||
``,
|
||||
`<span style="color:#9d78ff;">## use</span>`,
|
||||
``,
|
||||
`<span style="color:#a9bcc3;">Import the stylesheet and you're done.</span>`,
|
||||
],
|
||||
};
|
||||
|
||||
const WORKSPACES = {
|
||||
prod: {
|
||||
name: 'Aurora prod',
|
||||
tree: [
|
||||
{ name: 'src', path: 'src', open: true, children: [
|
||||
{ name: 'aurora.ts', path: 'src/aurora.ts', modified: true },
|
||||
{ name: 'paint.ts', path: 'src/paint.ts' },
|
||||
{ name: 'tokens.css', path: 'src/tokens.css' },
|
||||
]},
|
||||
{ name: 'public', path: 'public', children: [
|
||||
{ name: 'logo.svg', path: 'public/logo.svg' },
|
||||
]},
|
||||
{ name: 'README.md', path: 'README.md' },
|
||||
{ name: 'package.json', path: 'package.json' },
|
||||
],
|
||||
},
|
||||
staging: {
|
||||
name: 'Staging',
|
||||
tree: [
|
||||
{ name: 'src', path: 'src', open: true, children: [
|
||||
{ name: 'aurora.ts', path: 'src/aurora.ts' },
|
||||
{ name: 'tokens.css', path: 'src/tokens.css' },
|
||||
]},
|
||||
{ name: 'README.md', path: 'README.md' },
|
||||
],
|
||||
},
|
||||
preview: {
|
||||
name: 'Preview',
|
||||
tree: [
|
||||
{ name: 'src', path: 'src', children: [
|
||||
{ name: 'aurora.ts', path: 'src/aurora.ts' },
|
||||
]},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const WS_KEYS = Object.keys(WORKSPACES);
|
||||
|
||||
const INITIAL_TERM = [
|
||||
`<span style="color:#42dcd1;">➜</span> <span style="color:#cce7ec;">australis</span> <span style="color:#9d78ff;">build</span> <span style="color:#a4c4ff;">--watch</span>`,
|
||||
`<span style="color:#6e7882;"># compiling 24 modules…</span>`,
|
||||
`<span style="color:#51e08a;">✓</span> tokens compiled <span style="color:#6e7882;">(184ms)</span>`,
|
||||
`<span style="color:#51e08a;">✓</span> ui-kit bundled <span style="color:#6e7882;">(412ms)</span>`,
|
||||
`<span style="color:#ffe14e;">!</span> 2 deprecated tokens — see migration.md`,
|
||||
``,
|
||||
];
|
||||
|
||||
function App() {
|
||||
const [workspaceIdx, setWorkspaceIdx] = React.useState(0);
|
||||
const workspace = WS_KEYS[workspaceIdx];
|
||||
const [openFiles, setOpenFiles] = React.useState(['src/aurora.ts', 'src/tokens.css']);
|
||||
const [activeFile, setActiveFile] = React.useState('src/aurora.ts');
|
||||
const [termLines, setTermLines] = React.useState(INITIAL_TERM);
|
||||
const [paletteOpen, setPaletteOpen] = React.useState(false);
|
||||
|
||||
const cycleWorkspace = () => setWorkspaceIdx(i => (i + 1) % WS_KEYS.length);
|
||||
|
||||
const openFile = (path) => {
|
||||
if (!openFiles.includes(path)) setOpenFiles(f => [...f, path]);
|
||||
setActiveFile(path);
|
||||
};
|
||||
const closeFile = (path) => {
|
||||
const next = openFiles.filter(f => f !== path);
|
||||
setOpenFiles(next);
|
||||
if (activeFile === path) setActiveFile(next[0] || null);
|
||||
};
|
||||
|
||||
const runCommand = (cmd) => {
|
||||
const c = cmd.trim().toLowerCase();
|
||||
const promptLine = `<span style="color:#42dcd1;">➜</span> <span style="color:#cce7ec;">${cmd}</span>`;
|
||||
let out = [];
|
||||
if (c === 'build' || c.startsWith('australis build')) {
|
||||
out = [
|
||||
`<span style="color:#6e7882;"># compiling…</span>`,
|
||||
`<span style="color:#51e08a;">✓</span> built in <span style="color:#42dcd1;">312ms</span>`,
|
||||
];
|
||||
} else if (c === 'deploy') {
|
||||
out = [
|
||||
`<span style="color:#a4c4ff;">→</span> deploying to ${WORKSPACES[workspace].name.toLowerCase()}…`,
|
||||
`<span style="color:#51e08a;">✓</span> live at <span style="color:#42dcd1;">https://australis.dev</span>`,
|
||||
];
|
||||
} else if (c === 'theme') {
|
||||
out = [`<span style="color:#a4c4ff;">→</span> theme: <span style="color:#cce7ec;">Australis Dark</span> (active)`];
|
||||
} else if (c === 'help') {
|
||||
out = [
|
||||
`<span style="color:#a9bcc3;">commands:</span>`,
|
||||
` <span style="color:#9d78ff;">build</span> — compile the project`,
|
||||
` <span style="color:#9d78ff;">deploy</span> — push to current workspace`,
|
||||
` <span style="color:#9d78ff;">theme</span> — show active theme`,
|
||||
` <span style="color:#9d78ff;">clear</span> — clear the terminal`,
|
||||
];
|
||||
} else if (c === 'clear') {
|
||||
setTermLines([]); return;
|
||||
} else {
|
||||
out = [`<span style="color:#ff854f;">✗</span> unknown command: <span style="color:#cce7ec;">${cmd}</span> — try <span style="color:#9d78ff;">help</span>`];
|
||||
}
|
||||
setTermLines(prev => [...prev, promptLine, ...out, '']);
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const onKey = (e) => {
|
||||
if ((e.metaKey || e.ctrlKey) && e.key.toLowerCase() === 'k') {
|
||||
e.preventDefault();
|
||||
setPaletteOpen(true);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, []);
|
||||
|
||||
const handlePaletteRun = (item) => {
|
||||
if (item.id === 'tokens') openFile('src/tokens.css');
|
||||
else if (item.id === 'aurora') openFile('src/aurora.ts');
|
||||
else runCommand(item.id);
|
||||
};
|
||||
|
||||
const errors = activeFile === 'src/aurora.ts' ? 0 : 0;
|
||||
const warnings = 2;
|
||||
|
||||
return (
|
||||
<div className="shell">
|
||||
<div className="titlebar">
|
||||
<div className="dots">
|
||||
<div className="dot" style={{ background:'#ff491a' }}/>
|
||||
<div className="dot" style={{ background:'#e1c631' }}/>
|
||||
<div className="dot" style={{ background:'#16b866' }}/>
|
||||
</div>
|
||||
<button className="pal-trigger" onClick={() => setPaletteOpen(true)}>
|
||||
<Icon name="command" size={11}/> command palette
|
||||
</button>
|
||||
<div className="titlename">australis · {WORKSPACES[workspace].name.toLowerCase()}</div>
|
||||
<div style={{ width: 80 }}/>
|
||||
</div>
|
||||
|
||||
<div className="body-row">
|
||||
<Sidebar
|
||||
workspace={workspace}
|
||||
workspaces={WORKSPACES}
|
||||
onCycleWorkspace={cycleWorkspace}
|
||||
openFile={openFile}
|
||||
activeFile={activeFile}
|
||||
onOpenFile={openFile}
|
||||
/>
|
||||
<div style={{ flex: 1, display: 'flex', flexDirection: 'column', minWidth: 0 }}>
|
||||
<Editor
|
||||
openFiles={openFiles}
|
||||
activeFile={activeFile}
|
||||
onCloseFile={closeFile}
|
||||
onSelectFile={setActiveFile}
|
||||
fileContent={FILE_CONTENT}
|
||||
/>
|
||||
<Terminal lines={termLines} onCommand={runCommand}/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<StatusBar
|
||||
branch="main"
|
||||
problems={{ errors, warnings }}
|
||||
position="ln 8 · col 24"
|
||||
syntax={activeFile && activeFile.endsWith('.ts') ? 'TypeScript'
|
||||
: activeFile && activeFile.endsWith('.css') ? 'CSS'
|
||||
: 'Markdown'}
|
||||
/>
|
||||
|
||||
<CommandPalette
|
||||
open={paletteOpen}
|
||||
onClose={() => setPaletteOpen(false)}
|
||||
onRun={handlePaletteRun}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')).render(<App/>);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user