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:
vh
2026-09-24 09:07:16 -07:00
parent 77225b01d6
commit 7b16be02bf
49 changed files with 2675 additions and 4 deletions
@@ -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 || '&nbsp;' }}/>
))}
</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> {`,
`&nbsp;&nbsp;<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>);`,
`&nbsp;&nbsp;<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;">=&gt;</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> {`,
`&nbsp;&nbsp;<span style="color:#a9bcc3;">--bg-0</span>: <span style="color:#42dcd1;">#222531</span>;`,
`&nbsp;&nbsp;<span style="color:#a9bcc3;">--bg-1</span>: <span style="color:#42dcd1;">#373b46</span>;`,
`&nbsp;&nbsp;<span style="color:#a9bcc3;">--fg-0</span>: <span style="color:#42dcd1;">#cce7ec</span>;`,
`&nbsp;&nbsp;<span style="color:#a9bcc3;">--fg-1</span>: <span style="color:#42dcd1;">#a9bcc3</span>;`,
`&nbsp;&nbsp;<span style="color:#a9bcc3;">--aus-blue</span>: <span style="color:#42dcd1;">#6388d8</span>;`,
`&nbsp;&nbsp;<span style="color:#a9bcc3;">--aus-cyan</span>: <span style="color:#42dcd1;">#00b1a8</span>;`,
`&nbsp;&nbsp;<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>