refactor: extract JournalHeader from JournalPanel
Extract the header logic into a standalone `JournalHeader` component. This new component manages the connection status display, session dropdown for switching between sessions, and the ability to create new sessions or disconnect.
This commit is contained in:
parent
13f454de9c
commit
307a3c8320
|
|
@ -5,15 +5,17 @@
|
|||
* Auto-connects to ws://{current host}:{current port} — no URL input needed.
|
||||
*/
|
||||
|
||||
import { Component, Show, createSignal, onMount } from "solid-js";
|
||||
import { Component, Show, createSignal, onMount, For } from "solid-js";
|
||||
import {
|
||||
connectStream,
|
||||
hydrateFromServer,
|
||||
useJournalStream,
|
||||
setMyName,
|
||||
setSessionId,
|
||||
sessions,
|
||||
createSession,
|
||||
disconnectStream,
|
||||
} from "../stores/journalStream";
|
||||
import { ConnectionBar } from "./ConnectionBar";
|
||||
import { StreamView } from "./StreamView";
|
||||
import { ComposePanel } from "./ComposePanel";
|
||||
|
||||
|
|
@ -43,45 +45,7 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
|||
aria-hidden={!props.open}
|
||||
>
|
||||
{/* Header */}
|
||||
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 shrink-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
classList={{
|
||||
"bg-gray-400": stream.connectionStatus === "disconnected",
|
||||
"bg-yellow-400 animate-pulse":
|
||||
stream.connectionStatus === "connecting",
|
||||
"bg-green-500": stream.connectionStatus === "connected",
|
||||
"bg-red-500": stream.connectionStatus === "error",
|
||||
}}
|
||||
title={stream.connectionStatus}
|
||||
/>
|
||||
<Show
|
||||
when={stream.connected && stream.sessionId}
|
||||
fallback={
|
||||
<h2 class="text-sm font-semibold text-gray-700 truncate">
|
||||
Disconnected
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-700 truncate leading-tight">
|
||||
{stream.myName}
|
||||
</p>
|
||||
<p class="text-[10px] text-gray-400 truncate leading-tight">
|
||||
{stream.sessionName || stream.sessionId}
|
||||
</p>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
class="text-gray-400 hover:text-gray-600 text-sm px-1 shrink-0"
|
||||
title="Hide panel"
|
||||
>
|
||||
▸
|
||||
</button>
|
||||
</div>
|
||||
<JournalHeader onClose={props.onClose} />
|
||||
|
||||
<Show
|
||||
when={stream.connected}
|
||||
|
|
@ -91,7 +55,6 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<ConnectionBar />
|
||||
<div class="flex-1 min-h-0">
|
||||
<StreamView />
|
||||
</div>
|
||||
|
|
@ -102,6 +65,169 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
|||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Journal Header — status dot, player name, session dropdown, disconnect & close
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface JournalHeaderProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const JournalHeader: Component<JournalHeaderProps> = (props) => {
|
||||
const stream = useJournalStream();
|
||||
const manifest = sessions;
|
||||
const [dropdownOpen, setDropdownOpen] = createSignal(false);
|
||||
const [newSessionName, setNewSessionName] = createSignal("");
|
||||
const [showNewInput, setShowNewInput] = createSignal(false);
|
||||
let dropdownRef!: HTMLDivElement;
|
||||
|
||||
const handleSessionSelect = async (id: string) => {
|
||||
try {
|
||||
await hydrateFromServer(id);
|
||||
setSessionId(id);
|
||||
} catch {
|
||||
/* */
|
||||
}
|
||||
setDropdownOpen(false);
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
const name = newSessionName().trim();
|
||||
if (!name) return;
|
||||
createSession(name);
|
||||
setNewSessionName("");
|
||||
setShowNewInput(false);
|
||||
};
|
||||
|
||||
const handleDisconnect = () => disconnectStream();
|
||||
|
||||
// Close dropdown on outside click
|
||||
if (typeof document !== "undefined") {
|
||||
document.addEventListener("click", (e) => {
|
||||
if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
|
||||
setDropdownOpen(false);
|
||||
setShowNewInput(false);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex items-center justify-between px-3 py-2 border-b border-gray-200 shrink-0">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<span
|
||||
class="w-2 h-2 rounded-full shrink-0"
|
||||
classList={{
|
||||
"bg-gray-400": stream.connectionStatus === "disconnected",
|
||||
"bg-yellow-400 animate-pulse":
|
||||
stream.connectionStatus === "connecting",
|
||||
"bg-green-500": stream.connectionStatus === "connected",
|
||||
"bg-red-500": stream.connectionStatus === "error",
|
||||
}}
|
||||
title={stream.connectionStatus}
|
||||
/>
|
||||
<Show
|
||||
when={stream.connected && stream.sessionId}
|
||||
fallback={
|
||||
<h2 class="text-sm font-semibold text-gray-700 truncate">
|
||||
Disconnected
|
||||
</h2>
|
||||
}
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm font-semibold text-gray-700 truncate leading-tight">
|
||||
{stream.myName}
|
||||
</p>
|
||||
{/* Clickable subtitle — session dropdown */}
|
||||
<div ref={dropdownRef} class="relative">
|
||||
<button
|
||||
onClick={() => setDropdownOpen((v) => !v)}
|
||||
class="text-[10px] text-gray-400 hover:text-gray-600 truncate leading-tight cursor-pointer text-left"
|
||||
>
|
||||
{stream.sessionName || stream.sessionId}{" "}
|
||||
<span class="text-gray-300">▾</span>
|
||||
</button>
|
||||
<Show when={dropdownOpen()}>
|
||||
<div class="absolute top-full left-0 mt-1 w-48 bg-white border border-gray-200 rounded shadow-lg z-50 py-1 max-h-48 overflow-y-auto">
|
||||
<For each={Object.entries(manifest().sessions)}>
|
||||
{([id, meta]) => (
|
||||
<button
|
||||
onClick={() => handleSessionSelect(id)}
|
||||
class={`w-full text-left px-3 py-1 text-xs hover:bg-gray-100 flex items-center gap-1 ${
|
||||
id === stream.sessionId
|
||||
? "bg-blue-50 text-blue-700"
|
||||
: "text-gray-700"
|
||||
}`}
|
||||
>
|
||||
<span class="truncate flex-1">{meta.name || id}</span>
|
||||
<Show when={id === stream.sessionId}>
|
||||
<span class="text-blue-500 shrink-0">✓</span>
|
||||
</Show>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
<div class="border-t border-gray-100 my-0.5" />
|
||||
<Show
|
||||
when={!showNewInput()}
|
||||
fallback={
|
||||
<div class="px-3 py-1 flex gap-1">
|
||||
<input
|
||||
type="text"
|
||||
value={newSessionName()}
|
||||
onInput={(e) =>
|
||||
setNewSessionName(e.currentTarget.value)
|
||||
}
|
||||
placeholder="Session name"
|
||||
class="flex-1 border border-gray-300 rounded px-1.5 py-0.5 text-xs"
|
||||
autofocus
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") handleCreate();
|
||||
if (e.key === "Escape") setShowNewInput(false);
|
||||
}}
|
||||
/>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
class="bg-blue-600 text-white rounded px-2 py-0.5 text-xs hover:bg-blue-700"
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<button
|
||||
onClick={() => setShowNewInput(true)}
|
||||
class="w-full text-left px-3 py-1 text-xs text-blue-600 hover:bg-gray-100"
|
||||
>
|
||||
+ New session
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Show when={stream.connected}>
|
||||
<button
|
||||
onClick={handleDisconnect}
|
||||
class="text-xs text-gray-400 hover:text-red-500 px-1"
|
||||
title="Disconnect"
|
||||
>
|
||||
⏻
|
||||
</button>
|
||||
</Show>
|
||||
<button
|
||||
onClick={props.onClose}
|
||||
class="text-gray-400 hover:text-gray-600 text-sm px-1"
|
||||
title="Hide panel"
|
||||
>
|
||||
▸
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Connect dialog — one input: player name. Broker URL is always current host.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Reference in New Issue