From 29527a0e716c3bd558094117e168596c2bd52b65 Mon Sep 17 00:00:00 2001 From: limiteinductive Date: Sun, 29 Mar 2026 20:57:58 +0200 Subject: [PATCH] compose: kill Send button, post-singularity input design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Enter sends, Shift+Enter for newlines - Type prefix inline: > for text, // for code, => for result, !! for error, :: for plan - Tab cycles type, Ctrl+1-5 selects directly - Click prefix to cycle - Auto-growing textarea, no fixed height - Subtle type name indicator on right - Border pulses primary color on focus - No buttons, no chrome — pure terminal feel Co-Authored-By: Claude Opus 4.6 (1M context) --- ui/colony/src/components/ComposeBox.tsx | 134 +++++++++++++++--------- 1 file changed, 87 insertions(+), 47 deletions(-) diff --git a/ui/colony/src/components/ComposeBox.tsx b/ui/colony/src/components/ComposeBox.tsx index 5fb0f40..01c9bb2 100644 --- a/ui/colony/src/components/ComposeBox.tsx +++ b/ui/colony/src/components/ComposeBox.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useRef, useEffect } from "react"; import type { MessageType } from "@/types/MessageType"; import { postMessage } from "@/api"; import { cn } from "@/lib/utils"; @@ -10,13 +10,13 @@ interface Props { onMessageSent: () => void; } -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)]" }, -]; +const TYPE_META: Record = { + text: { prefix: ">", color: "text-muted-foreground", key: "1" }, + code: { prefix: "//", color: "text-[var(--color-msg-code)]", key: "2" }, + result: { prefix: "=>", color: "text-[var(--color-msg-result)]", key: "3" }, + error: { prefix: "!!", color: "text-[var(--color-msg-error)]", key: "4" }, + plan: { prefix: "::", color: "text-[var(--color-msg-plan)]", key: "5" }, +}; export function ComposeBox({ channelId, @@ -27,6 +27,22 @@ export function ComposeBox({ const [content, setContent] = useState(""); const [msgType, setMsgType] = useState("text"); const [sending, setSending] = useState(false); + const [focused, setFocused] = useState(false); + const inputRef = useRef(null); + const meta = TYPE_META[msgType]; + + // Auto-resize textarea + useEffect(() => { + if (inputRef.current) { + inputRef.current.style.height = "0"; + inputRef.current.style.height = `${Math.min(inputRef.current.scrollHeight, 120)}px`; + } + }, [content]); + + // Auto-focus on mount + useEffect(() => { + inputRef.current?.focus(); + }, []); async function handleSend() { if (!content.trim() || sending) return; @@ -46,67 +62,91 @@ export function ComposeBox({ } } + function cycleType(direction: 1 | -1) { + const types: MessageType[] = ["text", "code", "result", "error", "plan"]; + const idx = types.indexOf(msgType); + const next = (idx + direction + types.length) % types.length; + setMsgType(types[next]); + } + return ( -
+
+ {/* Reply chip */} {replyTo && ( -
- ^ - replying to #{replyTo.slice(0, 8)} +
+ ^ + #{replyTo.slice(0, 8)}
)} -
- {/* Type selector — blocky, no-radius buttons */} -
- {MSG_TYPES.map((t) => ( - - ))} -
+
+ {/* Type prefix — click to cycle */} + - {/* Input — thick border, no radius */} - setContent(e.target.value)} + onFocus={() => setFocused(true)} + onBlur={() => setFocused(false)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } + if (e.key === "Tab" && !e.shiftKey) { + e.preventDefault(); + cycleType(1); + } + if (e.key === "Tab" && e.shiftKey) { + e.preventDefault(); + cycleType(-1); + } + // Ctrl+1-5 for type + if (e.ctrlKey && e.key >= "1" && e.key <= "5") { + e.preventDefault(); + const types: MessageType[] = ["text", "code", "result", "error", "plan"]; + setMsgType(types[parseInt(e.key) - 1]); + } }} - placeholder="> message..." + placeholder="message" disabled={sending} - 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" + rows={1} + className={cn( + "flex-1 bg-transparent text-sm font-mono text-foreground placeholder:text-muted-foreground/30 resize-none focus:outline-none leading-relaxed py-0.5", + sending && "opacity-30", + )} /> - {/* Send — hot orange */} - + {/* Subtle type indicator — shows current type name */} + + {msgType} +
);