feat(messaging-ui): rendered Messenger components — the drop-in UI

Turns the SDK from hooks-only into a real UI SDK: <Messenger> (conversation
list + open thread + composer), plus <ConversationList> and <Thread>, all
driven by the existing useConversations/useMessages hooks over the injected
adapter — zero transport imports (boundary intact).

- themeable via --miu-* CSS variables; default theme ships as ./styles.css
- optimistic send, typing indicator, seen ticks, reactions display, unread badges
- render smoke tests (jsdom + MockAdapter): mounts, auto-selects first thread,
  sends a message, switches threads (3 tests) — 48 total green
- tsup: css bundled to dist/styles.css; dts scoped to TS entries

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-21 17:47:30 +05:30
parent c5237a237a
commit 3f1f89dfbe
7 changed files with 433 additions and 2 deletions
@@ -0,0 +1,32 @@
import { useEffect, useState } from 'react';
import { useConversations } from '../hooks/use-conversations';
import { ConversationList } from './conversation-list';
import { Thread } from './thread';
/**
* The drop-in messenger: conversation list + open thread. Owns only selection state;
* all data flows through the injected adapter via the hooks. Auto-selects the first
* conversation so the pane is never empty on load.
*/
export function Messenger() {
const { conversations, loading, error } = useConversations();
const [selected, setSelected] = useState<string | null>(null);
useEffect(() => {
if (selected && conversations.some((c) => c.threadId === selected)) return;
setSelected(conversations[0]?.threadId ?? null);
}, [conversations, selected]);
return (
<div className="miu-messenger">
<aside className="miu-sidebar">
{loading && conversations.length === 0 ? <div className="miu-empty">Loading</div> : null}
{error ? <div className="miu-empty miu-error">{error}</div> : null}
<ConversationList conversations={conversations} selectedId={selected} onSelect={setSelected} />
</aside>
<section className="miu-main">
<Thread threadId={selected} />
</section>
</div>
);
}