redesign: Concrete Brutalism — warm concrete palette, Inconsolata + Instrument Sans

Kill the AI slop. New design language:
- Warm concrete grays (#1a1917 base) with hot orange (#F26522) accent
- Inconsolata mono for body, Instrument Sans for headings
- Zero border-radius everywhere — brutalist, no rounded corners
- Thick 4px type slabs on messages (green/red/blue/yellow)
- Thick 2px borders on all structural elements
- Agent messages in warm card bg, names in hot orange
- Ape emoji logo in sidebar
- Command terminal compose box with > prompt
- Blocky type selector buttons

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-29 20:52:00 +02:00
parent 9e375fd953
commit 0ab3d64daa
7 changed files with 234 additions and 207 deletions

View File

@@ -93,31 +93,31 @@ export default function App() {
</div>
<div className="flex-1 flex flex-col min-w-0">
<div className="px-3 py-2 md:px-4 border-b border-border flex items-center gap-2">
<div className="px-3 py-2 md:px-4 border-b-2 border-border flex items-center gap-2">
<Sheet open={sheetOpen} onOpenChange={setSheetOpen}>
<SheetTrigger className="md:hidden p-1 h-8 w-8 text-muted-foreground hover:text-foreground rounded-sm">
<SheetTrigger className="md:hidden p-1 h-8 w-8 text-muted-foreground hover:text-primary font-mono font-bold text-lg">
=
</SheetTrigger>
<SheetContent side="left" className="p-0 w-64 bg-background">
<SheetContent side="left" className="p-0 w-56 bg-sidebar border-r-2 border-border">
{sidebar}
</SheetContent>
</Sheet>
{activeChannel ? (
<>
<span className="text-muted-foreground">#</span>
<span className="font-bold text-sm">{activeChannel.name}</span>
<span className="text-primary font-bold font-mono">#</span>
<span className="font-sans font-bold text-sm uppercase tracking-wide">{activeChannel.name}</span>
{activeChannel.description && (
<span className="text-xs text-muted-foreground ml-2 hidden md:inline">
<span className="text-xs font-mono text-muted-foreground ml-2 hidden md:inline">
{activeChannel.description}
</span>
)}
<span className="ml-auto text-xs text-muted-foreground tabular-nums">
<span className="ml-auto text-[10px] font-mono text-muted-foreground tabular-nums">
{messages.length} msg
</span>
</>
) : (
<span className="text-muted-foreground text-xs">select a channel</span>
<span className="text-muted-foreground text-xs font-mono">no channel selected</span>
)}
</div>

View File

@@ -31,55 +31,60 @@ export function ChannelSidebar({
}
return (
<div className="flex flex-col h-full w-64 md:w-52 border-r border-white/[0.06] bg-[oklch(0.1_0.005_260)] text-muted-foreground">
{/* Header */}
<div className="px-4 py-3 md:px-3 border-b border-white/[0.06]">
<div className="text-[16px] md:text-[15px] font-bold tracking-tight text-foreground">
COLONY
</div>
<div className="text-[11px] md:text-[10px] text-muted-foreground/60 mt-0.5">
apes.unslope.com
<div className="flex flex-col h-full w-56 md:w-48 border-r-2 border-border bg-sidebar text-sidebar-foreground">
{/* Header — ape logo */}
<div className="px-3 py-3 border-b-2 border-border">
<div className="flex items-center gap-2">
<span className="text-2xl" role="img" aria-label="ape">🐒</span>
<div>
<div className="font-sans text-sm font-bold tracking-wide uppercase text-foreground">
Colony
</div>
<div className="font-mono text-[10px] text-muted-foreground tracking-wider">
apes.unslope.com
</div>
</div>
</div>
</div>
{/* Channel list */}
<div className="flex-1 overflow-y-auto py-2">
<div className="px-3 md:px-2 mb-1 text-[10px] font-bold text-muted-foreground/50 tracking-widest">
CHANNELS
<div className="flex-1 overflow-y-auto">
<div className="px-3 pt-3 pb-1 font-sans text-[10px] font-bold text-muted-foreground uppercase tracking-[0.2em]">
Channels
</div>
{channels.map((ch) => (
<button
type="button"
key={ch.id}
onClick={() => onSelect(ch.id)}
className={`w-full text-left px-4 md:px-3 py-2 md:py-1 text-[13px] md:text-[12px] transition-colors min-h-[44px] md:min-h-0 flex items-center ${
className={`w-full text-left px-3 py-2 md:py-1.5 text-xs font-mono border-l-4 transition-colors min-h-[44px] md:min-h-0 flex items-center ${
ch.id === activeId
? "bg-white/[0.06] text-foreground font-medium"
: "hover:bg-white/[0.03]"
? "border-primary bg-sidebar-accent text-foreground font-bold"
: "border-transparent hover:border-muted-foreground/30 hover:bg-sidebar-accent/50"
}`}
>
<span className="text-muted-foreground/50 mr-1.5">#</span>
<span className="text-muted-foreground mr-1.5 font-bold">#</span>
{ch.name}
</button>
))}
</div>
{/* Current user */}
<div className="px-4 md:px-3 py-2 border-t border-white/[0.06] text-[11px]">
<span className="text-muted-foreground/50">as </span>
<span className="font-bold text-foreground">{getCurrentUsername()}</span>
{/* User strip */}
<div className="px-3 py-2 border-t-2 border-border font-mono text-[11px]">
<span className="text-muted-foreground">usr:</span>
<span className="text-primary font-bold ml-1">{getCurrentUsername()}</span>
</div>
{/* New channel */}
<div className="p-2 border-t border-white/[0.06]">
<div className="px-2 py-2 border-t border-border">
<input
type="text"
placeholder="+ new channel"
placeholder="+ channel"
value={newName}
onChange={(e) => setNewName(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
disabled={creating}
className="w-full bg-white/[0.04] text-[12px] md:text-[11px] text-foreground placeholder:text-muted-foreground/40 px-3 py-2 md:py-1 rounded-sm border border-white/[0.06] focus:outline-none focus:border-blue-500/30 min-h-[40px] md:min-h-0"
className="w-full bg-input text-xs font-mono text-foreground placeholder:text-muted-foreground px-2 py-1.5 border-2 border-border focus:outline-none focus:border-primary min-h-[36px] md:min-h-0"
/>
</div>
</div>

View File

@@ -1,8 +1,6 @@
import { useState } from "react";
import type { MessageType } from "@/types/MessageType";
import { postMessage } from "@/api";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
interface Props {
@@ -12,12 +10,12 @@ interface Props {
onMessageSent: () => void;
}
const MSG_TYPES: { value: MessageType; label: string }[] = [
{ value: "text", label: "T" },
{ value: "code", label: "C" },
{ value: "result", label: "R" },
{ value: "error", label: "E" },
{ value: "plan", label: "P" },
const MSG_TYPES: { value: MessageType; label: string; color: string }[] = [
{ value: "text", label: "TXT", color: "" },
{ value: "code", label: "COD", color: "text-[var(--color-msg-code)]" },
{ value: "result", label: "RES", color: "text-[var(--color-msg-result)]" },
{ value: "error", label: "ERR", color: "text-[var(--color-msg-error)]" },
{ value: "plan", label: "PLN", color: "text-[var(--color-msg-plan)]" },
];
export function ComposeBox({
@@ -49,29 +47,34 @@ export function ComposeBox({
}
return (
<div className="border-t border-border bg-card px-3 py-2 md:px-4 md:py-3 pb-[env(safe-area-inset-bottom,8px)]">
<div className="border-t-2 border-border bg-card px-3 py-2.5 md:px-4 pb-[env(safe-area-inset-bottom,10px)]">
{replyTo && (
<div className="flex items-center gap-2 mb-1.5 text-xs text-muted-foreground">
<span>^ #{replyTo.slice(0, 8)}</span>
<Button variant="ghost" size="sm" onClick={onClearReply} className="h-6 px-1 text-xs">
<div className="flex items-center gap-2 mb-2 text-[11px] font-mono text-muted-foreground">
<span className="text-primary font-bold">^</span>
<span>replying to #{replyTo.slice(0, 8)}</span>
<button
type="button"
onClick={onClearReply}
className="text-muted-foreground hover:text-primary font-bold min-w-[32px] min-h-[32px] md:min-w-0 md:min-h-0 flex items-center"
>
[x]
</Button>
</button>
</div>
)}
<div className="flex items-center gap-1.5 md:gap-2">
{/* Type selector */}
<div className="flex gap-0.5 rounded-md border border-border p-0.5">
<div className="flex items-center gap-2">
{/* Type selector — blocky, no-radius buttons */}
<div className="flex border-2 border-border">
{MSG_TYPES.map((t) => (
<button
type="button"
key={t.value}
onClick={() => setMsgType(t.value)}
className={cn(
"h-7 w-7 md:h-6 md:w-6 rounded-sm text-xs font-bold transition-colors",
"h-8 w-8 md:h-7 md:w-7 text-[9px] font-mono font-bold uppercase transition-colors border-r border-border last:border-r-0",
msgType === t.value
? "bg-primary text-primary-foreground"
: "text-muted-foreground hover:text-foreground"
: cn("text-muted-foreground hover:text-foreground hover:bg-muted/50", t.color),
)}
>
{t.label}
@@ -79,7 +82,9 @@ export function ComposeBox({
))}
</div>
<Input
{/* Input — thick border, no radius */}
<input
type="text"
value={content}
onChange={(e) => setContent(e.target.value)}
onKeyDown={(e) => {
@@ -88,19 +93,20 @@ export function ComposeBox({
handleSend();
}
}}
placeholder="message..."
placeholder="> message..."
disabled={sending}
className="flex-1 h-9 md:h-8 text-sm"
className="flex-1 bg-input text-sm font-mono text-foreground placeholder:text-muted-foreground/50 px-3 py-1.5 h-9 md:h-8 border-2 border-border focus:outline-none focus:border-primary"
/>
<Button
{/* Send — hot orange */}
<button
type="button"
onClick={handleSend}
disabled={sending || !content.trim()}
size="sm"
className="h-9 md:h-8 px-3 text-xs font-bold"
className="h-9 md:h-8 px-4 font-sans text-xs font-bold uppercase tracking-wider bg-primary text-primary-foreground border-2 border-primary hover:bg-primary/80 disabled:opacity-20 transition-opacity"
>
{sending ? "..." : "SEND"}
</Button>
{sending ? "..." : "Send"}
</button>
</div>
</div>
);

View File

@@ -1,5 +1,6 @@
import { useState } from "react";
import type { Message } from "@/types/Message";
import { cn } from "@/lib/utils";
interface Props {
message: Message;
@@ -7,19 +8,12 @@ interface Props {
onReply: (id: string) => void;
}
const TYPE_BORDER: Record<string, string> = {
text: "",
code: "border-l-2 border-amber-500/70",
result: "border-l-2 border-emerald-500/70",
error: "border-l-2 border-red-500/70",
plan: "border-l-2 border-blue-500/70",
};
const TYPE_LABEL: Record<string, { text: string; color: string }> = {
code: { text: "CODE", color: "text-amber-500" },
result: { text: "RES", color: "text-emerald-500" },
error: { text: "ERR", color: "text-red-500" },
plan: { text: "PLAN", color: "text-blue-500" },
const TYPE_CONFIG: Record<string, { border: string; label: string; labelBg: string }> = {
text: { border: "border-l-transparent", label: "", labelBg: "" },
code: { border: "border-l-[var(--color-msg-code)]", label: "CODE", labelBg: "bg-[var(--color-msg-code)]/15 text-[var(--color-msg-code)]" },
result: { border: "border-l-[var(--color-msg-result)]", label: "RESULT", labelBg: "bg-[var(--color-msg-result)]/15 text-[var(--color-msg-result)]" },
error: { border: "border-l-[var(--color-msg-error)]", label: "ERROR", labelBg: "bg-[var(--color-msg-error)]/15 text-[var(--color-msg-error)]" },
plan: { border: "border-l-[var(--color-msg-plan)]", label: "PLAN", labelBg: "bg-[var(--color-msg-plan)]/15 text-[var(--color-msg-plan)]" },
};
function timeAgo(dateStr: string): string {
@@ -36,120 +30,116 @@ export function MessageItem({ message, replyTarget, onReply }: Props) {
const [metaOpen, setMetaOpen] = useState(false);
const isAgent = message.user.role === "agent";
const isDeleted = !!message.deleted_at;
const border = TYPE_BORDER[message.type] || "";
const label = TYPE_LABEL[message.type];
const cfg = TYPE_CONFIG[message.type] || TYPE_CONFIG.text;
const meta = message.metadata as Record<string, string> | null;
return (
<div
className={`group relative px-3 py-2 md:px-4 transition-colors hover:bg-white/[0.03] ${
isAgent ? "bg-white/[0.02]" : ""
}`}
>
{/* Agent glow — left edge */}
{isAgent && (
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-blue-500/30" />
className={cn(
"group border-b border-border/50 border-l-4 transition-colors",
cfg.border,
isAgent ? "bg-card" : "bg-background",
"hover:bg-muted/30",
)}
>
{/* Reply context */}
{replyTarget && (
<div className="mb-1 text-[10px] md:text-[11px] text-muted-foreground flex items-center gap-1 opacity-60 pl-7 md:pl-9">
<span>^</span>
<span className="font-medium">{replyTarget.user.display_name}</span>
<span className="truncate max-w-40 md:max-w-80">{replyTarget.content}</span>
<div className="px-4 pt-1.5 text-[10px] text-muted-foreground flex items-center gap-1">
<span className="text-primary font-bold">^</span>
<span className="font-bold">{replyTarget.user.display_name}</span>
<span className="truncate max-w-40 md:max-w-80 opacity-60">{replyTarget.content}</span>
</div>
)}
<div className={`flex items-start gap-2 md:gap-3 ${border} ${border ? "pl-2 md:pl-3" : ""}`}>
{/* Avatar — small on mobile */}
<div
className={`w-5 h-5 md:w-6 md:h-6 flex-shrink-0 flex items-center justify-center text-[9px] md:text-[10px] font-bold rounded-sm ${
isAgent
? "bg-blue-500/20 text-blue-400"
: "bg-secondary text-muted-foreground"
}`}
>
{message.user.display_name[0]}
<div className="px-4 py-2">
{/* Header */}
<div className="flex items-center gap-2 text-[11px] flex-wrap">
{/* Name */}
<span className={cn(
"font-sans font-bold text-xs",
isAgent ? "text-primary" : "text-foreground"
)}>
{message.user.display_name}
</span>
{/* Agent badge */}
{isAgent && (
<span className="font-mono text-[9px] font-bold px-1.5 py-0.5 bg-primary/15 text-primary uppercase tracking-wider">
AGT
</span>
)}
{/* Type badge */}
{cfg.label && (
<span className={cn("font-mono text-[9px] font-bold px-1.5 py-0.5 uppercase tracking-wider", cfg.labelBg)}>
{cfg.label}
</span>
)}
{/* Time */}
<span className="text-muted-foreground font-mono tabular-nums md:hidden text-[10px]">
{timeAgo(message.created_at)}
</span>
<span className="text-muted-foreground font-mono tabular-nums hidden md:inline text-[10px]">
{new Date(message.created_at).toLocaleTimeString("en-US", {
hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit",
})}
</span>
{/* Seq */}
<span className="text-muted-foreground/30 font-mono tabular-nums text-[10px] hidden md:inline">
#{Number(message.seq)}
</span>
{/* Reply button */}
<button
type="button"
onClick={() => onReply(message.id)}
className="font-sans text-[10px] font-bold uppercase tracking-wider text-muted-foreground md:opacity-0 md:group-hover:opacity-60 hover:!text-primary transition-all ml-auto min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-end"
>
Reply
</button>
</div>
<div className="flex-1 min-w-0">
{/* Header */}
<div className="flex items-center gap-1.5 md:gap-2 text-[10px] md:text-[11px] flex-wrap">
<span className={`font-bold ${isAgent ? "text-blue-400" : "text-foreground"}`}>
{message.user.display_name}
</span>
{isAgent && (
<span className="text-[9px] px-1 bg-blue-500/20 rounded-sm text-blue-400">
AGT
</span>
)}
{label && (
<span className={`text-[9px] md:text-[10px] font-bold ${label.color}`}>
{label.text}
</span>
)}
{/* Mobile: relative time. Desktop: full time */}
<span className="text-muted-foreground tabular-nums md:hidden">
{timeAgo(message.created_at)}
</span>
<span className="text-muted-foreground tabular-nums hidden md:inline">
{new Date(message.created_at).toLocaleTimeString("en-US", {
hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit",
})}
</span>
<span className="text-muted-foreground/40 tabular-nums hidden md:inline">
#{Number(message.seq)}
</span>
{/* Reply — always visible on mobile (no hover), hover on desktop */}
<button
type="button"
onClick={() => onReply(message.id)}
className="text-[9px] md:text-[10px] text-muted-foreground md:opacity-0 md:group-hover:opacity-60 hover:!opacity-100 transition-opacity ml-auto min-w-[44px] min-h-[44px] md:min-w-0 md:min-h-0 flex items-center justify-end"
>
REPLY
</button>
</div>
{/* Content */}
<div className={`mt-0.5 text-[12px] md:text-[13px] leading-relaxed break-words ${
message.type === "code" ? "bg-black/20 px-2 py-1 rounded-sm whitespace-pre-wrap overflow-x-auto" : ""
} ${message.type === "error" ? "text-red-400" : ""}`}>
{isDeleted ? (
<span className="italic text-muted-foreground/40">[deleted]</span>
) : (
message.content
)}
</div>
{/* Agent metadata — tap to expand on mobile, always compact on desktop */}
{meta && isAgent && (
<>
{/* Desktop: inline strip */}
<div className="hidden md:flex mt-1 gap-3 text-[10px] text-muted-foreground/50">
{meta.model && <span>{meta.model}</span>}
{meta.hostname && <span>{meta.hostname}</span>}
{meta.cwd && <span>{meta.cwd}</span>}
{meta.skill && <span className="text-blue-400/50">{meta.skill}</span>}
</div>
{/* Mobile: tap to expand */}
<button
type="button"
onClick={() => setMetaOpen(!metaOpen)}
className="md:hidden mt-1 text-[9px] text-muted-foreground/40 min-h-[32px] flex items-center"
>
{metaOpen ? "hide meta" : `${meta.model || "agent"} ...`}
</button>
{metaOpen && (
<div className="md:hidden mt-0.5 text-[10px] text-muted-foreground/50 space-y-0.5">
{meta.model && <div>model: {meta.model}</div>}
{meta.hostname && <div>host: {meta.hostname}</div>}
{meta.cwd && <div>cwd: {meta.cwd}</div>}
{meta.skill && <div className="text-blue-400/50">skill: {meta.skill}</div>}
</div>
)}
</>
{/* Content */}
<div className={cn(
"mt-1 text-[13px] leading-relaxed break-words font-mono",
message.type === "code" && "bg-muted px-3 py-2 border-2 border-border whitespace-pre-wrap overflow-x-auto",
message.type === "error" && "text-[var(--color-msg-error)]",
)}>
{isDeleted ? (
<span className="italic text-muted-foreground/40">[deleted]</span>
) : (
message.content
)}
</div>
{/* Agent metadata */}
{meta && isAgent && (
<>
<div className="hidden md:flex mt-1.5 gap-3 text-[10px] font-mono text-muted-foreground/50">
{meta.model && <span>{meta.model}</span>}
{meta.hostname && <span>{meta.hostname}</span>}
{meta.cwd && <span className="text-muted-foreground/30">{meta.cwd}</span>}
{meta.skill && <span className="text-primary/50">{meta.skill}</span>}
</div>
<button
type="button"
onClick={() => setMetaOpen(!metaOpen)}
className="md:hidden mt-1 text-[9px] font-mono text-muted-foreground/40 min-h-[32px] flex items-center"
>
{metaOpen ? "[-] hide" : `[+] ${meta.model || "meta"}`}
</button>
{metaOpen && (
<div className="md:hidden mt-0.5 text-[10px] font-mono text-muted-foreground/50 space-y-0.5 pl-2 border-l-2 border-border">
{meta.model && <div>{meta.model}</div>}
{meta.hostname && <div>{meta.hostname}</div>}
{meta.cwd && <div>{meta.cwd}</div>}
{meta.skill && <div className="text-primary/50">{meta.skill}</div>}
</div>
)}
</>
)}
</div>
</div>
);

View File

@@ -1,13 +1,13 @@
@import "tailwindcss";
@import "@fontsource/jetbrains-mono/400.css";
@import "@fontsource/jetbrains-mono/500.css";
@import "@fontsource/jetbrains-mono/700.css";
@import "@fontsource/inconsolata/400.css";
@import "@fontsource/inconsolata/700.css";
@import "@fontsource-variable/instrument-sans";
@custom-variant dark (&:is(.dark *));
@theme inline {
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
--font-sans: 'JetBrains Mono', ui-monospace, monospace;
--font-mono: 'Inconsolata', ui-monospace, monospace;
--font-sans: 'Instrument Sans Variable', system-ui, sans-serif;
--color-ring: var(--ring);
--color-input: var(--input);
@@ -32,42 +32,46 @@
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--radius-sm: 2px;
--radius-md: 4px;
--radius-lg: 6px;
/* message type accents */
--color-msg-result: oklch(0.72 0.19 145);
--color-msg-error: oklch(0.63 0.24 25);
--color-msg-plan: oklch(0.68 0.16 250);
--color-msg-code: oklch(0.75 0.12 80);
--color-agent-glow: oklch(0.55 0.15 250 / 0.3);
/* Zero radii — brutalist, no rounded corners */
--radius-sm: 0;
--radius-md: 0;
--radius-lg: 0;
--radius-xl: 0;
/* Message type accents */
--color-hot: #F26522;
--color-msg-result: #22C55E;
--color-msg-error: #EF4444;
--color-msg-plan: #3B82F6;
--color-msg-code: #EAB308;
}
:root {
--background: oklch(0.12 0.005 260);
--foreground: oklch(0.85 0 0);
--card: oklch(0.15 0.005 260);
--card-foreground: oklch(0.85 0 0);
--popover: oklch(0.15 0.005 260);
--popover-foreground: oklch(0.85 0 0);
--primary: oklch(0.85 0 0);
--primary-foreground: oklch(0.12 0.005 260);
--secondary: oklch(0.2 0.005 260);
--secondary-foreground: oklch(0.75 0 0);
--muted: oklch(0.2 0.005 260);
--muted-foreground: oklch(0.55 0 0);
--accent: oklch(0.22 0.01 260);
--accent-foreground: oklch(0.85 0 0);
--destructive: oklch(0.63 0.24 25);
--border: oklch(0.22 0.01 260);
--input: oklch(0.2 0.005 260);
--ring: oklch(0.55 0.15 250);
--sidebar: oklch(0.1 0.005 260);
--sidebar-foreground: oklch(0.7 0 0);
--sidebar-accent: oklch(0.18 0.01 260);
--sidebar-accent-foreground: oklch(0.9 0 0);
--sidebar-border: oklch(0.2 0.01 260);
/* Warm concrete palette */
--background: #1a1917;
--foreground: #d4d0c8;
--card: #1f1e1b;
--card-foreground: #d4d0c8;
--popover: #1f1e1b;
--popover-foreground: #d4d0c8;
--primary: #F26522;
--primary-foreground: #1a1917;
--secondary: #2a2825;
--secondary-foreground: #a8a49c;
--muted: #252320;
--muted-foreground: #7a756c;
--accent: #2a2825;
--accent-foreground: #d4d0c8;
--destructive: #EF4444;
--border: #3a3632;
--input: #252320;
--ring: #F26522;
--sidebar: #151413;
--sidebar-foreground: #8a857c;
--sidebar-accent: #252320;
--sidebar-accent-foreground: #d4d0c8;
--sidebar-border: #2a2825;
}
@layer base {
@@ -77,7 +81,7 @@
body {
@apply bg-background text-foreground font-mono;
font-size: 13px;
line-height: 1.5;
line-height: 1.6;
}
html, body, #root {
height: 100%;