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:
@@ -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>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user