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.
|
* 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 {
|
import {
|
||||||
connectStream,
|
connectStream,
|
||||||
hydrateFromServer,
|
hydrateFromServer,
|
||||||
useJournalStream,
|
useJournalStream,
|
||||||
setMyName,
|
setMyName,
|
||||||
setSessionId,
|
setSessionId,
|
||||||
|
sessions,
|
||||||
|
createSession,
|
||||||
|
disconnectStream,
|
||||||
} from "../stores/journalStream";
|
} from "../stores/journalStream";
|
||||||
import { ConnectionBar } from "./ConnectionBar";
|
|
||||||
import { StreamView } from "./StreamView";
|
import { StreamView } from "./StreamView";
|
||||||
import { ComposePanel } from "./ComposePanel";
|
import { ComposePanel } from "./ComposePanel";
|
||||||
|
|
||||||
|
|
@ -43,6 +45,73 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||||
aria-hidden={!props.open}
|
aria-hidden={!props.open}
|
||||||
>
|
>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
|
<JournalHeader onClose={props.onClose} />
|
||||||
|
|
||||||
|
<Show
|
||||||
|
when={stream.connected}
|
||||||
|
fallback={
|
||||||
|
<div class="flex-1 flex items-center justify-center p-4 overflow-y-auto">
|
||||||
|
<ConnectDialog />
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="flex-1 min-h-0">
|
||||||
|
<StreamView />
|
||||||
|
</div>
|
||||||
|
<ComposePanel />
|
||||||
|
</Show>
|
||||||
|
</aside>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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 justify-between px-3 py-2 border-b border-gray-200 shrink-0">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<div class="flex items-center gap-2 min-w-0">
|
||||||
<span
|
<span
|
||||||
|
|
@ -68,37 +137,94 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
|
||||||
<p class="text-sm font-semibold text-gray-700 truncate leading-tight">
|
<p class="text-sm font-semibold text-gray-700 truncate leading-tight">
|
||||||
{stream.myName}
|
{stream.myName}
|
||||||
</p>
|
</p>
|
||||||
<p class="text-[10px] text-gray-400 truncate leading-tight">
|
{/* Clickable subtitle — session dropdown */}
|
||||||
{stream.sessionName || stream.sessionId}
|
<div ref={dropdownRef} class="relative">
|
||||||
</p>
|
<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>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
</div>
|
</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
|
<button
|
||||||
onClick={props.onClose}
|
onClick={props.onClose}
|
||||||
class="text-gray-400 hover:text-gray-600 text-sm px-1 shrink-0"
|
class="text-gray-400 hover:text-gray-600 text-sm px-1"
|
||||||
title="Hide panel"
|
title="Hide panel"
|
||||||
>
|
>
|
||||||
▸
|
▸
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show
|
|
||||||
when={stream.connected}
|
|
||||||
fallback={
|
|
||||||
<div class="flex-1 flex items-center justify-center p-4 overflow-y-auto">
|
|
||||||
<ConnectDialog />
|
|
||||||
</div>
|
</div>
|
||||||
}
|
|
||||||
>
|
|
||||||
<ConnectionBar />
|
|
||||||
<div class="flex-1 min-h-0">
|
|
||||||
<StreamView />
|
|
||||||
</div>
|
|
||||||
<ComposePanel />
|
|
||||||
</Show>
|
|
||||||
</aside>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue