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:
20
ui/colony/package-lock.json
generated
20
ui/colony/package-lock.json
generated
@@ -10,6 +10,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.3.0",
|
"@base-ui/react": "^1.3.0",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
|
"@fontsource-variable/instrument-sans": "^5.2.8",
|
||||||
|
"@fontsource/inconsolata": "^5.2.8",
|
||||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
@@ -907,6 +909,24 @@
|
|||||||
"url": "https://github.com/sponsors/ayuhito"
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@fontsource-variable/instrument-sans": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource-variable/instrument-sans/-/instrument-sans-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-mTCaukbdIjjoipj2E3Q5XoZM3ZxJWdzyHevf/LG/0PHlfF9Q85pxOM7B7A9MerFyxmRzz5kVlumgIvgDSG4CPg==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@fontsource/inconsolata": {
|
||||||
|
"version": "5.2.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/@fontsource/inconsolata/-/inconsolata-5.2.8.tgz",
|
||||||
|
"integrity": "sha512-lIZW+WOZYpUH91g9r6rYYhfTmptF3YPPM54ZOs8IYVeeL4SeiAu4tfj7mdr8llYEq31DLYgi6JtGIJa192gB0Q==",
|
||||||
|
"license": "OFL-1.1",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/ayuhito"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@fontsource/jetbrains-mono": {
|
"node_modules/@fontsource/jetbrains-mono": {
|
||||||
"version": "5.2.8",
|
"version": "5.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/@fontsource/jetbrains-mono/-/jetbrains-mono-5.2.8.tgz",
|
||||||
|
|||||||
@@ -12,6 +12,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@base-ui/react": "^1.3.0",
|
"@base-ui/react": "^1.3.0",
|
||||||
"@fontsource-variable/geist": "^5.2.8",
|
"@fontsource-variable/geist": "^5.2.8",
|
||||||
|
"@fontsource-variable/instrument-sans": "^5.2.8",
|
||||||
|
"@fontsource/inconsolata": "^5.2.8",
|
||||||
"@fontsource/jetbrains-mono": "^5.2.8",
|
"@fontsource/jetbrains-mono": "^5.2.8",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
|
|||||||
@@ -93,31 +93,31 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex-1 flex flex-col min-w-0">
|
<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}>
|
<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>
|
</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}
|
{sidebar}
|
||||||
</SheetContent>
|
</SheetContent>
|
||||||
</Sheet>
|
</Sheet>
|
||||||
|
|
||||||
{activeChannel ? (
|
{activeChannel ? (
|
||||||
<>
|
<>
|
||||||
<span className="text-muted-foreground">#</span>
|
<span className="text-primary font-bold font-mono">#</span>
|
||||||
<span className="font-bold text-sm">{activeChannel.name}</span>
|
<span className="font-sans font-bold text-sm uppercase tracking-wide">{activeChannel.name}</span>
|
||||||
{activeChannel.description && (
|
{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}
|
{activeChannel.description}
|
||||||
</span>
|
</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
|
{messages.length} msg
|
||||||
</span>
|
</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>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -31,55 +31,60 @@ export function ChannelSidebar({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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">
|
<div className="flex flex-col h-full w-56 md:w-48 border-r-2 border-border bg-sidebar text-sidebar-foreground">
|
||||||
{/* Header */}
|
{/* Header — ape logo */}
|
||||||
<div className="px-4 py-3 md:px-3 border-b border-white/[0.06]">
|
<div className="px-3 py-3 border-b-2 border-border">
|
||||||
<div className="text-[16px] md:text-[15px] font-bold tracking-tight text-foreground">
|
<div className="flex items-center gap-2">
|
||||||
COLONY
|
<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>
|
||||||
<div className="text-[11px] md:text-[10px] text-muted-foreground/60 mt-0.5">
|
<div className="font-mono text-[10px] text-muted-foreground tracking-wider">
|
||||||
apes.unslope.com
|
apes.unslope.com
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Channel list */}
|
{/* Channel list */}
|
||||||
<div className="flex-1 overflow-y-auto py-2">
|
<div className="flex-1 overflow-y-auto">
|
||||||
<div className="px-3 md:px-2 mb-1 text-[10px] font-bold text-muted-foreground/50 tracking-widest">
|
<div className="px-3 pt-3 pb-1 font-sans text-[10px] font-bold text-muted-foreground uppercase tracking-[0.2em]">
|
||||||
CHANNELS
|
Channels
|
||||||
</div>
|
</div>
|
||||||
{channels.map((ch) => (
|
{channels.map((ch) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={ch.id}
|
key={ch.id}
|
||||||
onClick={() => onSelect(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
|
ch.id === activeId
|
||||||
? "bg-white/[0.06] text-foreground font-medium"
|
? "border-primary bg-sidebar-accent text-foreground font-bold"
|
||||||
: "hover:bg-white/[0.03]"
|
: "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}
|
{ch.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Current user */}
|
{/* User strip */}
|
||||||
<div className="px-4 md:px-3 py-2 border-t border-white/[0.06] text-[11px]">
|
<div className="px-3 py-2 border-t-2 border-border font-mono text-[11px]">
|
||||||
<span className="text-muted-foreground/50">as </span>
|
<span className="text-muted-foreground">usr:</span>
|
||||||
<span className="font-bold text-foreground">{getCurrentUsername()}</span>
|
<span className="text-primary font-bold ml-1">{getCurrentUsername()}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* New channel */}
|
{/* New channel */}
|
||||||
<div className="p-2 border-t border-white/[0.06]">
|
<div className="px-2 py-2 border-t border-border">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="+ new channel"
|
placeholder="+ channel"
|
||||||
value={newName}
|
value={newName}
|
||||||
onChange={(e) => setNewName(e.target.value)}
|
onChange={(e) => setNewName(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
onKeyDown={(e) => e.key === "Enter" && handleCreate()}
|
||||||
disabled={creating}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { MessageType } from "@/types/MessageType";
|
import type { MessageType } from "@/types/MessageType";
|
||||||
import { postMessage } from "@/api";
|
import { postMessage } from "@/api";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -12,12 +10,12 @@ interface Props {
|
|||||||
onMessageSent: () => void;
|
onMessageSent: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const MSG_TYPES: { value: MessageType; label: string }[] = [
|
const MSG_TYPES: { value: MessageType; label: string; color: string }[] = [
|
||||||
{ value: "text", label: "T" },
|
{ value: "text", label: "TXT", color: "" },
|
||||||
{ value: "code", label: "C" },
|
{ value: "code", label: "COD", color: "text-[var(--color-msg-code)]" },
|
||||||
{ value: "result", label: "R" },
|
{ value: "result", label: "RES", color: "text-[var(--color-msg-result)]" },
|
||||||
{ value: "error", label: "E" },
|
{ value: "error", label: "ERR", color: "text-[var(--color-msg-error)]" },
|
||||||
{ value: "plan", label: "P" },
|
{ value: "plan", label: "PLN", color: "text-[var(--color-msg-plan)]" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export function ComposeBox({
|
export function ComposeBox({
|
||||||
@@ -49,29 +47,34 @@ export function ComposeBox({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
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 && (
|
{replyTo && (
|
||||||
<div className="flex items-center gap-2 mb-1.5 text-xs text-muted-foreground">
|
<div className="flex items-center gap-2 mb-2 text-[11px] font-mono text-muted-foreground">
|
||||||
<span>^ #{replyTo.slice(0, 8)}</span>
|
<span className="text-primary font-bold">^</span>
|
||||||
<Button variant="ghost" size="sm" onClick={onClearReply} className="h-6 px-1 text-xs">
|
<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]
|
[x]
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-1.5 md:gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Type selector */}
|
{/* Type selector — blocky, no-radius buttons */}
|
||||||
<div className="flex gap-0.5 rounded-md border border-border p-0.5">
|
<div className="flex border-2 border-border">
|
||||||
{MSG_TYPES.map((t) => (
|
{MSG_TYPES.map((t) => (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
key={t.value}
|
key={t.value}
|
||||||
onClick={() => setMsgType(t.value)}
|
onClick={() => setMsgType(t.value)}
|
||||||
className={cn(
|
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
|
msgType === t.value
|
||||||
? "bg-primary text-primary-foreground"
|
? "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}
|
{t.label}
|
||||||
@@ -79,7 +82,9 @@ export function ComposeBox({
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Input
|
{/* Input — thick border, no radius */}
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
value={content}
|
value={content}
|
||||||
onChange={(e) => setContent(e.target.value)}
|
onChange={(e) => setContent(e.target.value)}
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
@@ -88,19 +93,20 @@ export function ComposeBox({
|
|||||||
handleSend();
|
handleSend();
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
placeholder="message..."
|
placeholder="> message..."
|
||||||
disabled={sending}
|
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}
|
onClick={handleSend}
|
||||||
disabled={sending || !content.trim()}
|
disabled={sending || !content.trim()}
|
||||||
size="sm"
|
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"
|
||||||
className="h-9 md:h-8 px-3 text-xs font-bold"
|
|
||||||
>
|
>
|
||||||
{sending ? "..." : "SEND"}
|
{sending ? "..." : "Send"}
|
||||||
</Button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Message } from "@/types/Message";
|
import type { Message } from "@/types/Message";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: Message;
|
message: Message;
|
||||||
@@ -7,19 +8,12 @@ interface Props {
|
|||||||
onReply: (id: string) => void;
|
onReply: (id: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const TYPE_BORDER: Record<string, string> = {
|
const TYPE_CONFIG: Record<string, { border: string; label: string; labelBg: string }> = {
|
||||||
text: "",
|
text: { border: "border-l-transparent", label: "", labelBg: "" },
|
||||||
code: "border-l-2 border-amber-500/70",
|
code: { border: "border-l-[var(--color-msg-code)]", label: "CODE", labelBg: "bg-[var(--color-msg-code)]/15 text-[var(--color-msg-code)]" },
|
||||||
result: "border-l-2 border-emerald-500/70",
|
result: { border: "border-l-[var(--color-msg-result)]", label: "RESULT", labelBg: "bg-[var(--color-msg-result)]/15 text-[var(--color-msg-result)]" },
|
||||||
error: "border-l-2 border-red-500/70",
|
error: { border: "border-l-[var(--color-msg-error)]", label: "ERROR", labelBg: "bg-[var(--color-msg-error)]/15 text-[var(--color-msg-error)]" },
|
||||||
plan: "border-l-2 border-blue-500/70",
|
plan: { border: "border-l-[var(--color-msg-plan)]", label: "PLAN", labelBg: "bg-[var(--color-msg-plan)]/15 text-[var(--color-msg-plan)]" },
|
||||||
};
|
|
||||||
|
|
||||||
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" },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function timeAgo(dateStr: string): string {
|
function timeAgo(dateStr: string): string {
|
||||||
@@ -36,84 +30,83 @@ export function MessageItem({ message, replyTarget, onReply }: Props) {
|
|||||||
const [metaOpen, setMetaOpen] = useState(false);
|
const [metaOpen, setMetaOpen] = useState(false);
|
||||||
const isAgent = message.user.role === "agent";
|
const isAgent = message.user.role === "agent";
|
||||||
const isDeleted = !!message.deleted_at;
|
const isDeleted = !!message.deleted_at;
|
||||||
const border = TYPE_BORDER[message.type] || "";
|
const cfg = TYPE_CONFIG[message.type] || TYPE_CONFIG.text;
|
||||||
const label = TYPE_LABEL[message.type];
|
|
||||||
const meta = message.metadata as Record<string, string> | null;
|
const meta = message.metadata as Record<string, string> | null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={`group relative px-3 py-2 md:px-4 transition-colors hover:bg-white/[0.03] ${
|
className={cn(
|
||||||
isAgent ? "bg-white/[0.02]" : ""
|
"group border-b border-border/50 border-l-4 transition-colors",
|
||||||
}`}
|
cfg.border,
|
||||||
>
|
isAgent ? "bg-card" : "bg-background",
|
||||||
{/* Agent glow — left edge */}
|
"hover:bg-muted/30",
|
||||||
{isAgent && (
|
|
||||||
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-blue-500/30" />
|
|
||||||
)}
|
)}
|
||||||
|
>
|
||||||
{/* Reply context */}
|
{/* Reply context */}
|
||||||
{replyTarget && (
|
{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">
|
<div className="px-4 pt-1.5 text-[10px] text-muted-foreground flex items-center gap-1">
|
||||||
<span>^</span>
|
<span className="text-primary font-bold">^</span>
|
||||||
<span className="font-medium">{replyTarget.user.display_name}</span>
|
<span className="font-bold">{replyTarget.user.display_name}</span>
|
||||||
<span className="truncate max-w-40 md:max-w-80">{replyTarget.content}</span>
|
<span className="truncate max-w-40 md:max-w-80 opacity-60">{replyTarget.content}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={`flex items-start gap-2 md:gap-3 ${border} ${border ? "pl-2 md:pl-3" : ""}`}>
|
<div className="px-4 py-2">
|
||||||
{/* 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>
|
|
||||||
|
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="flex items-center gap-1.5 md:gap-2 text-[10px] md:text-[11px] flex-wrap">
|
<div className="flex items-center gap-2 text-[11px] flex-wrap">
|
||||||
<span className={`font-bold ${isAgent ? "text-blue-400" : "text-foreground"}`}>
|
{/* Name */}
|
||||||
|
<span className={cn(
|
||||||
|
"font-sans font-bold text-xs",
|
||||||
|
isAgent ? "text-primary" : "text-foreground"
|
||||||
|
)}>
|
||||||
{message.user.display_name}
|
{message.user.display_name}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
|
{/* Agent badge */}
|
||||||
{isAgent && (
|
{isAgent && (
|
||||||
<span className="text-[9px] px-1 bg-blue-500/20 rounded-sm text-blue-400">
|
<span className="font-mono text-[9px] font-bold px-1.5 py-0.5 bg-primary/15 text-primary uppercase tracking-wider">
|
||||||
AGT
|
AGT
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{label && (
|
|
||||||
<span className={`text-[9px] md:text-[10px] font-bold ${label.color}`}>
|
{/* Type badge */}
|
||||||
{label.text}
|
{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>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* Mobile: relative time. Desktop: full time */}
|
|
||||||
<span className="text-muted-foreground tabular-nums md:hidden">
|
{/* Time */}
|
||||||
|
<span className="text-muted-foreground font-mono tabular-nums md:hidden text-[10px]">
|
||||||
{timeAgo(message.created_at)}
|
{timeAgo(message.created_at)}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground tabular-nums hidden md:inline">
|
<span className="text-muted-foreground font-mono tabular-nums hidden md:inline text-[10px]">
|
||||||
{new Date(message.created_at).toLocaleTimeString("en-US", {
|
{new Date(message.created_at).toLocaleTimeString("en-US", {
|
||||||
hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit",
|
hour12: false, hour: "2-digit", minute: "2-digit", second: "2-digit",
|
||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
<span className="text-muted-foreground/40 tabular-nums hidden md:inline">
|
|
||||||
|
{/* Seq */}
|
||||||
|
<span className="text-muted-foreground/30 font-mono tabular-nums text-[10px] hidden md:inline">
|
||||||
#{Number(message.seq)}
|
#{Number(message.seq)}
|
||||||
</span>
|
</span>
|
||||||
{/* Reply — always visible on mobile (no hover), hover on desktop */}
|
|
||||||
|
{/* Reply button */}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => onReply(message.id)}
|
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"
|
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
|
Reply
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content */}
|
{/* Content */}
|
||||||
<div className={`mt-0.5 text-[12px] md:text-[13px] leading-relaxed break-words ${
|
<div className={cn(
|
||||||
message.type === "code" ? "bg-black/20 px-2 py-1 rounded-sm whitespace-pre-wrap overflow-x-auto" : ""
|
"mt-1 text-[13px] leading-relaxed break-words font-mono",
|
||||||
} ${message.type === "error" ? "text-red-400" : ""}`}>
|
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 ? (
|
{isDeleted ? (
|
||||||
<span className="italic text-muted-foreground/40">[deleted]</span>
|
<span className="italic text-muted-foreground/40">[deleted]</span>
|
||||||
) : (
|
) : (
|
||||||
@@ -121,36 +114,33 @@ export function MessageItem({ message, replyTarget, onReply }: Props) {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Agent metadata — tap to expand on mobile, always compact on desktop */}
|
{/* Agent metadata */}
|
||||||
{meta && isAgent && (
|
{meta && isAgent && (
|
||||||
<>
|
<>
|
||||||
{/* Desktop: inline strip */}
|
<div className="hidden md:flex mt-1.5 gap-3 text-[10px] font-mono text-muted-foreground/50">
|
||||||
<div className="hidden md:flex mt-1 gap-3 text-[10px] text-muted-foreground/50">
|
|
||||||
{meta.model && <span>{meta.model}</span>}
|
{meta.model && <span>{meta.model}</span>}
|
||||||
{meta.hostname && <span>{meta.hostname}</span>}
|
{meta.hostname && <span>{meta.hostname}</span>}
|
||||||
{meta.cwd && <span>{meta.cwd}</span>}
|
{meta.cwd && <span className="text-muted-foreground/30">{meta.cwd}</span>}
|
||||||
{meta.skill && <span className="text-blue-400/50">{meta.skill}</span>}
|
{meta.skill && <span className="text-primary/50">{meta.skill}</span>}
|
||||||
</div>
|
</div>
|
||||||
{/* Mobile: tap to expand */}
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setMetaOpen(!metaOpen)}
|
onClick={() => setMetaOpen(!metaOpen)}
|
||||||
className="md:hidden mt-1 text-[9px] text-muted-foreground/40 min-h-[32px] flex items-center"
|
className="md:hidden mt-1 text-[9px] font-mono text-muted-foreground/40 min-h-[32px] flex items-center"
|
||||||
>
|
>
|
||||||
{metaOpen ? "hide meta" : `${meta.model || "agent"} ...`}
|
{metaOpen ? "[-] hide" : `[+] ${meta.model || "meta"}`}
|
||||||
</button>
|
</button>
|
||||||
{metaOpen && (
|
{metaOpen && (
|
||||||
<div className="md:hidden mt-0.5 text-[10px] text-muted-foreground/50 space-y-0.5">
|
<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>model: {meta.model}</div>}
|
{meta.model && <div>{meta.model}</div>}
|
||||||
{meta.hostname && <div>host: {meta.hostname}</div>}
|
{meta.hostname && <div>{meta.hostname}</div>}
|
||||||
{meta.cwd && <div>cwd: {meta.cwd}</div>}
|
{meta.cwd && <div>{meta.cwd}</div>}
|
||||||
{meta.skill && <div className="text-blue-400/50">skill: {meta.skill}</div>}
|
{meta.skill && <div className="text-primary/50">{meta.skill}</div>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@import "@fontsource/jetbrains-mono/400.css";
|
@import "@fontsource/inconsolata/400.css";
|
||||||
@import "@fontsource/jetbrains-mono/500.css";
|
@import "@fontsource/inconsolata/700.css";
|
||||||
@import "@fontsource/jetbrains-mono/700.css";
|
@import "@fontsource-variable/instrument-sans";
|
||||||
|
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-mono: 'JetBrains Mono', ui-monospace, monospace;
|
--font-mono: 'Inconsolata', ui-monospace, monospace;
|
||||||
--font-sans: 'JetBrains Mono', ui-monospace, monospace;
|
--font-sans: 'Instrument Sans Variable', system-ui, sans-serif;
|
||||||
|
|
||||||
--color-ring: var(--ring);
|
--color-ring: var(--ring);
|
||||||
--color-input: var(--input);
|
--color-input: var(--input);
|
||||||
@@ -32,42 +32,46 @@
|
|||||||
--color-sidebar-accent: var(--sidebar-accent);
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
--color-sidebar-border: var(--sidebar-border);
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
--radius-sm: 2px;
|
|
||||||
--radius-md: 4px;
|
|
||||||
--radius-lg: 6px;
|
|
||||||
|
|
||||||
/* message type accents */
|
/* Zero radii — brutalist, no rounded corners */
|
||||||
--color-msg-result: oklch(0.72 0.19 145);
|
--radius-sm: 0;
|
||||||
--color-msg-error: oklch(0.63 0.24 25);
|
--radius-md: 0;
|
||||||
--color-msg-plan: oklch(0.68 0.16 250);
|
--radius-lg: 0;
|
||||||
--color-msg-code: oklch(0.75 0.12 80);
|
--radius-xl: 0;
|
||||||
--color-agent-glow: oklch(0.55 0.15 250 / 0.3);
|
|
||||||
|
/* Message type accents */
|
||||||
|
--color-hot: #F26522;
|
||||||
|
--color-msg-result: #22C55E;
|
||||||
|
--color-msg-error: #EF4444;
|
||||||
|
--color-msg-plan: #3B82F6;
|
||||||
|
--color-msg-code: #EAB308;
|
||||||
}
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: oklch(0.12 0.005 260);
|
/* Warm concrete palette */
|
||||||
--foreground: oklch(0.85 0 0);
|
--background: #1a1917;
|
||||||
--card: oklch(0.15 0.005 260);
|
--foreground: #d4d0c8;
|
||||||
--card-foreground: oklch(0.85 0 0);
|
--card: #1f1e1b;
|
||||||
--popover: oklch(0.15 0.005 260);
|
--card-foreground: #d4d0c8;
|
||||||
--popover-foreground: oklch(0.85 0 0);
|
--popover: #1f1e1b;
|
||||||
--primary: oklch(0.85 0 0);
|
--popover-foreground: #d4d0c8;
|
||||||
--primary-foreground: oklch(0.12 0.005 260);
|
--primary: #F26522;
|
||||||
--secondary: oklch(0.2 0.005 260);
|
--primary-foreground: #1a1917;
|
||||||
--secondary-foreground: oklch(0.75 0 0);
|
--secondary: #2a2825;
|
||||||
--muted: oklch(0.2 0.005 260);
|
--secondary-foreground: #a8a49c;
|
||||||
--muted-foreground: oklch(0.55 0 0);
|
--muted: #252320;
|
||||||
--accent: oklch(0.22 0.01 260);
|
--muted-foreground: #7a756c;
|
||||||
--accent-foreground: oklch(0.85 0 0);
|
--accent: #2a2825;
|
||||||
--destructive: oklch(0.63 0.24 25);
|
--accent-foreground: #d4d0c8;
|
||||||
--border: oklch(0.22 0.01 260);
|
--destructive: #EF4444;
|
||||||
--input: oklch(0.2 0.005 260);
|
--border: #3a3632;
|
||||||
--ring: oklch(0.55 0.15 250);
|
--input: #252320;
|
||||||
--sidebar: oklch(0.1 0.005 260);
|
--ring: #F26522;
|
||||||
--sidebar-foreground: oklch(0.7 0 0);
|
--sidebar: #151413;
|
||||||
--sidebar-accent: oklch(0.18 0.01 260);
|
--sidebar-foreground: #8a857c;
|
||||||
--sidebar-accent-foreground: oklch(0.9 0 0);
|
--sidebar-accent: #252320;
|
||||||
--sidebar-border: oklch(0.2 0.01 260);
|
--sidebar-accent-foreground: #d4d0c8;
|
||||||
|
--sidebar-border: #2a2825;
|
||||||
}
|
}
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
@@ -77,7 +81,7 @@
|
|||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground font-mono;
|
@apply bg-background text-foreground font-mono;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
line-height: 1.5;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
html, body, #root {
|
html, body, #root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user