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
+84
View File
@@ -0,0 +1,84 @@
/**
* InviteDialog — input player name, copy invite link
*/
import { Component, createSignal } from "solid-js";
import { useJournalStream } from "../stores/journalStream";
interface InviteDialogProps {
onClose: () => void;
}
export const InviteDialog: Component<InviteDialogProps> = (props) => {
const stream = useJournalStream();
const [playerName, setPlayerName] = createSignal("");
const [copied, setCopied] = createSignal(false);
const inviteLink = () => {
const url = new URL(window.location.href);
url.searchParams.set("session", stream.sessionId || "default");
if (playerName().trim()) {
url.searchParams.set("player", playerName().trim());
}
url.searchParams.set("autojoin", "1");
return url.toString();
};
const handleCopy = async () => {
try {
await navigator.clipboard.writeText(inviteLink());
setCopied(true);
setTimeout(() => setCopied(false), 2000);
} catch {
const input = document.getElementById(
"invite-link-input",
) as HTMLInputElement;
if (input) input.select();
}
};
return (
<div class="absolute inset-0 z-50 flex items-center justify-center bg-black/20">
<div class="bg-white rounded-lg border border-gray-200 shadow-xl w-[280px] p-4 space-y-3">
<div class="flex items-center justify-between">
<h3 class="text-sm font-semibold text-gray-800"></h3>
<button
onClick={props.onClose}
class="text-gray-400 hover:text-gray-600 text-sm"
>
</button>
</div>
<p class="text-xs text-gray-500">
</p>
<input
type="text"
value={playerName()}
onInput={(e) => setPlayerName(e.currentTarget.value)}
placeholder="玩家名字"
class="w-full border border-gray-300 rounded px-2 py-1 text-xs"
autofocus
/>
<div class="flex gap-1">
<input
id="invite-link-input"
type="text"
value={inviteLink()}
readonly
class="flex-1 border border-gray-200 rounded px-2 py-1 text-xs bg-gray-50 text-gray-600 font-mono truncate"
/>
<button
onClick={handleCopy}
class={`shrink-0 rounded px-2 py-1 text-xs font-medium transition-colors ${
copied()
? "bg-green-100 text-green-700"
: "bg-blue-600 text-white hover:bg-blue-700"
}`}
>
{copied() ? "已复制!" : "复制"}
</button>
</div>
</div>
</div>
);
};