feat(journal): implement session management UI components

Add a suite of dialogs and header components to manage journal
sessions, including:

- ConnectDialog for joining sessions with name and role selection
- CreateSessionDialog for starting new sessions
- InviteDialog for generating and copying player invite links
- SessionDropdown for switching between sessions (GM only)
- JournalHeader for displaying connection status and user info
This commit is contained in:
2026-07-08 00:31:35 +08:00
parent 3d957706e9
commit 2c2b7b781d
7 changed files with 456 additions and 398 deletions
@@ -0,0 +1,92 @@
/**
* SessionDropdown — session switcher for the journal header (GM only)
*/
import { Component, createSignal, For, Show } from "solid-js";
import {
sessions,
setSessionId,
hydrateFromServer,
useJournalStream,
} from "../stores/journalStream";
import { CreateSessionDialog } from "./CreateSessionDialog";
export const SessionDropdown: Component = () => {
const stream = useJournalStream();
const manifest = sessions;
const [dropdownOpen, setDropdownOpen] = createSignal(false);
const [showCreateDialog, setShowCreateDialog] = createSignal(false);
let dropdownRef!: HTMLDivElement;
const handleSessionSelect = (id: string) => {
if (id === stream.sessionId) {
setDropdownOpen(false);
return;
}
setDropdownOpen(false);
setSessionId(id);
hydrateFromServer(id).catch(() => {});
};
// Close dropdown on outside click
if (typeof document !== "undefined") {
document.addEventListener("click", (e) => {
if (dropdownRef && !dropdownRef.contains(e.target as Node)) {
setDropdownOpen(false);
}
});
}
return (
<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"
onClick={(e) => e.stopPropagation()}
>
<For each={Object.entries(manifest().sessions)}>
{([id, meta]) => (
<button
onClick={(e) => {
e.stopPropagation();
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" />
<button
onClick={(e) => {
e.stopPropagation();
e.preventDefault();
setShowCreateDialog(true);
}}
class="w-full text-left px-3 py-1 text-xs text-blue-600 hover:bg-gray-100"
>
+
</button>
</div>
</Show>
<CreateSessionDialog
open={showCreateDialog()}
onClose={() => setShowCreateDialog(false)}
/>
</div>
);
};