From 69e838459887266481e127ea7f3589cbf54f94c1 Mon Sep 17 00:00:00 2001 From: limiteinductive Date: Sun, 29 Mar 2026 19:35:44 +0200 Subject: [PATCH] =?UTF-8?q?mobile-first=20UI=20redesign=20=E2=80=94=20touc?= =?UTF-8?q?h=20targets,=20responsive=20layout,=20safe=20areas?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MessageItem: 44px touch targets, relative time on mobile, tap-expand metadata - ComposeBox: safe-area-inset-bottom, compact type labels (T/C/R/E/P) - ChannelSidebar: wider on mobile, 44px channel buttons - All components: mobile-first with md: breakpoint for desktop - viewport: cover, no-scale, apple-mobile-web-app-capable - Pure Tailwind, no custom CSS Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/colony/index.html | 5 +- ui/colony/src/components/ChannelSidebar.tsx | 28 ++-- ui/colony/src/components/ComposeBox.tsx | 43 +++--- ui/colony/src/components/MessageItem.tsx | 156 +++++++++++--------- 4 files changed, 124 insertions(+), 108 deletions(-) diff --git a/ui/colony/index.html b/ui/colony/index.html index 43ba67c..00472c3 100644 --- a/ui/colony/index.html +++ b/ui/colony/index.html @@ -3,7 +3,10 @@ - + + + + colony diff --git a/ui/colony/src/components/ChannelSidebar.tsx b/ui/colony/src/components/ChannelSidebar.tsx index 6dfc28d..f79eedc 100644 --- a/ui/colony/src/components/ChannelSidebar.tsx +++ b/ui/colony/src/components/ChannelSidebar.tsx @@ -31,20 +31,20 @@ export function ChannelSidebar({ } return ( -
+
{/* Header */} -
-
+
+
COLONY
-
+
apes.unslope.com
{/* Channel list */}
-
+
CHANNELS
{channels.map((ch) => ( @@ -52,26 +52,26 @@ export function ChannelSidebar({ type="button" key={ch.id} onClick={() => onSelect(ch.id)} - className={`w-full text-left px-3 py-1 text-[12px] transition-colors ${ + 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 ${ ch.id === activeId - ? "bg-sidebar-accent text-sidebar-accent-foreground font-medium" - : "text-sidebar-foreground hover:bg-sidebar-accent/50" + ? "bg-white/[0.06] text-foreground font-medium" + : "hover:bg-white/[0.03]" }`} > - # + # {ch.name} ))}
{/* Current user */} -
- logged in as +
+ as {getCurrentUsername()}
- {/* New channel input */} -
+ {/* New channel */} +
setNewName(e.target.value)} onKeyDown={(e) => e.key === "Enter" && handleCreate()} disabled={creating} - className="w-full bg-sidebar-accent text-[11px] text-sidebar-foreground placeholder:text-muted-foreground px-2 py-1 rounded-sm border border-sidebar-border focus:outline-none focus:border-[var(--color-agent-glow)]" + 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" />
diff --git a/ui/colony/src/components/ComposeBox.tsx b/ui/colony/src/components/ComposeBox.tsx index bd19bd8..395c996 100644 --- a/ui/colony/src/components/ComposeBox.tsx +++ b/ui/colony/src/components/ComposeBox.tsx @@ -9,12 +9,12 @@ interface Props { onMessageSent: () => void; } -const TYPES: { value: MessageType; label: string; key: string }[] = [ - { value: "text", label: "TXT", key: "1" }, - { value: "code", label: "CODE", key: "2" }, - { value: "result", label: "RES", key: "3" }, - { value: "error", label: "ERR", key: "4" }, - { value: "plan", label: "PLAN", key: "5" }, +const TYPES: { value: MessageType; label: string; shortLabel: string }[] = [ + { value: "text", label: "TXT", shortLabel: "T" }, + { value: "code", label: "CODE", shortLabel: "C" }, + { value: "result", label: "RES", shortLabel: "R" }, + { value: "error", label: "ERR", shortLabel: "E" }, + { value: "plan", label: "PLAN", shortLabel: "P" }, ]; export function ComposeBox({ @@ -46,40 +46,42 @@ export function ComposeBox({ } return ( -
+
{replyTo && ( -
- replying to #{replyTo.slice(0, 8)} +
+ ^ #{replyTo.slice(0, 8)}
)} -
- {/* Type selector */} + +
+ {/* Type selector — compact on mobile */}
{TYPES.map((t) => ( ))}
- {/* Input */} + {/* Input — larger touch target on mobile */} = "1" && e.key <= "5") { setMsgType(TYPES[parseInt(e.key) - 1].value); } }} - placeholder={`message #${channelId.slice(0, 8)}...`} + placeholder="message..." disabled={sending} - className="flex-1 bg-input text-[13px] text-foreground placeholder:text-muted-foreground px-3 py-1.5 rounded-sm border border-border focus:outline-none focus:border-[var(--color-agent-glow)]" + className="flex-1 bg-input text-[13px] md:text-[13px] text-foreground placeholder:text-muted-foreground px-3 py-2 md:py-1.5 rounded-sm border border-border focus:outline-none focus:border-blue-500/30" />
diff --git a/ui/colony/src/components/MessageItem.tsx b/ui/colony/src/components/MessageItem.tsx index b5900e0..e7ee205 100644 --- a/ui/colony/src/components/MessageItem.tsx +++ b/ui/colony/src/components/MessageItem.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import type { Message } from "@/types/Message"; interface Props { @@ -6,67 +7,65 @@ interface Props { onReply: (id: string) => void; } -const TYPE_CONFIG: Record< - string, - { border: string; label: string; labelColor: string } -> = { - text: { border: "", label: "", labelColor: "" }, - code: { - border: "border-l-2 border-[var(--color-msg-code)]", - label: "CODE", - labelColor: "text-[var(--color-msg-code)]", - }, - result: { - border: "border-l-2 border-[var(--color-msg-result)]", - label: "RESULT", - labelColor: "text-[var(--color-msg-result)]", - }, - error: { - border: "border-l-2 border-[var(--color-msg-error)]", - label: "ERROR", - labelColor: "text-[var(--color-msg-error)]", - }, - plan: { - border: "border-l-2 border-[var(--color-msg-plan)]", - label: "PLAN", - labelColor: "text-[var(--color-msg-plan)]", - }, +const TYPE_BORDER: Record = { + 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 = { + 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 { + const diff = Date.now() - new Date(dateStr).getTime(); + const mins = Math.floor(diff / 60000); + if (mins < 1) return "now"; + if (mins < 60) return `${mins}m`; + const hrs = Math.floor(mins / 60); + if (hrs < 24) return `${hrs}h`; + return `${Math.floor(hrs / 24)}d`; +} + export function MessageItem({ message, replyTarget, onReply }: Props) { + const [metaOpen, setMetaOpen] = useState(false); const isAgent = message.user.role === "agent"; const isDeleted = !!message.deleted_at; - const cfg = TYPE_CONFIG[message.type] || TYPE_CONFIG.text; + const border = TYPE_BORDER[message.type] || ""; + const label = TYPE_LABEL[message.type]; const meta = message.metadata as Record | null; return (
- {/* Agent glow line */} + {/* Agent glow — left edge */} {isAgent && ( -
+
)} {/* Reply context */} {replyTarget && ( -
- ^ +
+ ^ {replyTarget.user.display_name} - - {replyTarget.content} - + {replyTarget.content}
)} -
- {/* Avatar */} +
+ {/* Avatar — small on mobile */}
@@ -74,68 +73,81 @@ export function MessageItem({ message, replyTarget, onReply }: Props) {
- {/* Header line */} -
- + {/* Header */} +
+ {message.user.display_name} {isAgent && ( - - AGENT + + AGT )} - {cfg.label && ( - - {cfg.label} + {label && ( + + {label.text} )} - + {/* Mobile: relative time. Desktop: full time */} + + {timeAgo(message.created_at)} + + {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", })} - + #{Number(message.seq)} + {/* Reply — always visible on mobile (no hover), hover on desktop */}
{/* Content */} -
+
{isDeleted ? ( - - [deleted] - + [deleted] ) : ( message.content )}
- {/* Agent metadata strip */} + {/* Agent metadata — tap to expand on mobile, always compact on desktop */} {meta && isAgent && ( -
- {meta.model && {meta.model}} - {meta.hostname && {meta.hostname}} - {meta.cwd && {meta.cwd}} - {meta.skill && ( - - {meta.skill} - + <> + {/* Desktop: inline strip */} +
+ {meta.model && {meta.model}} + {meta.hostname && {meta.hostname}} + {meta.cwd && {meta.cwd}} + {meta.skill && {meta.skill}} +
+ {/* Mobile: tap to expand */} + + {metaOpen && ( +
+ {meta.model &&
model: {meta.model}
} + {meta.hostname &&
host: {meta.hostname}
} + {meta.cwd &&
cwd: {meta.cwd}
} + {meta.skill &&
skill: {meta.skill}
} +
)} -
+ )}