feat: implement session creation flow

Refactor `CreateSessionDialog` to be controlled by its parent instead
of using an internal `open` prop. Lift the state up to `JournalPanel`
and pass the creation callback through `JournalHeader` to
`SessionDropdown`.
This commit is contained in:
hypercross 2026-07-08 00:39:29 +08:00
parent 376dde44b5
commit c13f66153a
4 changed files with 67 additions and 58 deletions

View File

@ -1,11 +1,10 @@
/** /**
* CreateSessionDialog modal for naming a new session * CreateSessionDialog modal for naming a new session
*/ */
import { Component, createSignal, Show } from "solid-js"; import { Component, createSignal, onMount } from "solid-js";
import { createSession } from "../stores/journalStream"; import { createSession } from "../stores/journalStream";
interface CreateSessionDialogProps { interface CreateSessionDialogProps {
open: boolean;
onClose: () => void; onClose: () => void;
} }
@ -23,56 +22,56 @@ export const CreateSessionDialog: Component<CreateSessionDialogProps> = (
props.onClose(); props.onClose();
}; };
onMount(() => inputRef?.focus());
return ( return (
<Show when={props.open}> <div
class="absolute inset-0 z-50 flex items-center justify-center bg-black/20"
onClick={props.onClose}
>
<div <div
class="absolute inset-0 z-50 flex items-center justify-center bg-black/20" class="bg-white rounded-lg border border-gray-200 shadow-xl w-[280px] p-4 space-y-3"
onClick={props.onClose} onClick={(e) => e.stopPropagation()}
> >
<div <div class="flex items-center justify-between">
class="bg-white rounded-lg border border-gray-200 shadow-xl w-[280px] p-4 space-y-3" <h3 class="text-sm font-semibold text-gray-800"></h3>
onClick={(e) => e.stopPropagation()} <button
> onClick={props.onClose}
<div class="flex items-center justify-between"> class="text-gray-400 hover:text-gray-600 text-sm"
<h3 class="text-sm font-semibold text-gray-800"></h3> >
<button
onClick={props.onClose} </button>
class="text-gray-400 hover:text-gray-600 text-sm" </div>
> <p class="text-xs text-gray-500"></p>
<input
</button> ref={inputRef!}
</div> type="text"
<p class="text-xs text-gray-500"></p> value={name()}
<input onInput={(e) => setName(e.currentTarget.value)}
ref={inputRef!} onKeyDown={(e) => {
type="text" if (e.key === "Enter") handleCreate();
value={name()} if (e.key === "Escape") props.onClose();
onInput={(e) => setName(e.currentTarget.value)} }}
onKeyDown={(e) => { placeholder="会话名称"
if (e.key === "Enter") handleCreate(); class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm"
if (e.key === "Escape") props.onClose(); autofocus
}} />
placeholder="会话名称" <div class="flex justify-end gap-1">
class="w-full border border-gray-300 rounded px-2 py-1.5 text-sm" <button
autofocus onClick={props.onClose}
/> class="px-3 py-1 text-xs text-gray-600 hover:text-gray-800"
<div class="flex justify-end gap-1"> >
<button
onClick={props.onClose} </button>
class="px-3 py-1 text-xs text-gray-600 hover:text-gray-800" <button
> onClick={handleCreate}
disabled={!name().trim()}
</button> class="bg-blue-600 text-white rounded px-3 py-1 text-xs font-medium hover:bg-blue-700 disabled:opacity-50"
<button >
onClick={handleCreate}
disabled={!name().trim()} </button>
class="bg-blue-600 text-white rounded px-3 py-1 text-xs font-medium hover:bg-blue-700 disabled:opacity-50"
>
</button>
</div>
</div> </div>
</div> </div>
</Show> </div>
); );
}; };

View File

@ -7,6 +7,7 @@ import { SessionDropdown } from "./SessionDropdown";
interface JournalHeaderProps { interface JournalHeaderProps {
onClose: () => void; onClose: () => void;
onCreateSession: () => void;
} }
export const JournalHeader: Component<JournalHeaderProps> = (props) => { export const JournalHeader: Component<JournalHeaderProps> = (props) => {
@ -51,7 +52,7 @@ export const JournalHeader: Component<JournalHeaderProps> = (props) => {
</p> </p>
{/* Clickable subtitle — session dropdown (GM only) */} {/* Clickable subtitle — session dropdown (GM only) */}
<Show when={stream.myRole === "gm"}> <Show when={stream.myRole === "gm"}>
<SessionDropdown /> <SessionDropdown onCreate={props.onCreateSession} />
</Show> </Show>
</div> </div>
</Show> </Show>

View File

@ -13,6 +13,7 @@ import { JournalContext } from "./JournalContext";
import { JournalHeader } from "./JournalHeader"; import { JournalHeader } from "./JournalHeader";
import { ConnectDialog } from "./ConnectDialog"; import { ConnectDialog } from "./ConnectDialog";
import { InviteDialog } from "./InviteDialog"; import { InviteDialog } from "./InviteDialog";
import { CreateSessionDialog } from "./CreateSessionDialog";
export interface JournalPanelProps { export interface JournalPanelProps {
open: boolean; open: boolean;
@ -22,6 +23,7 @@ export interface JournalPanelProps {
export const JournalPanel: Component<JournalPanelProps> = (props) => { export const JournalPanel: Component<JournalPanelProps> = (props) => {
const stream = useJournalStream(); const stream = useJournalStream();
const [showInvite, setShowInvite] = createSignal(false); const [showInvite, setShowInvite] = createSignal(false);
const [showCreateSession, setShowCreateSession] = createSignal(false);
// Player list (exclude gm and observer) // Player list (exclude gm and observer)
const playerEntries = () => const playerEntries = () =>
@ -48,7 +50,10 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
> >
<JournalContext.Provider value={{ onClose: props.onClose }}> <JournalContext.Provider value={{ onClose: props.onClose }}>
{/* Header */} {/* Header */}
<JournalHeader onClose={props.onClose} /> <JournalHeader
onClose={props.onClose}
onCreateSession={() => setShowCreateSession(true)}
/>
{/* Player list tabs */} {/* Player list tabs */}
<Show when={stream.connected}> <Show when={stream.connected}>
@ -88,6 +93,11 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
<Show when={showInvite()}> <Show when={showInvite()}>
<InviteDialog onClose={() => setShowInvite(false)} /> <InviteDialog onClose={() => setShowInvite(false)} />
</Show> </Show>
{/* Create session modal */}
<Show when={showCreateSession()}>
<CreateSessionDialog onClose={() => setShowCreateSession(false)} />
</Show>
</JournalContext.Provider> </JournalContext.Provider>
</aside> </aside>
</> </>

View File

@ -8,13 +8,15 @@ import {
hydrateFromServer, hydrateFromServer,
useJournalStream, useJournalStream,
} from "../stores/journalStream"; } from "../stores/journalStream";
import { CreateSessionDialog } from "./CreateSessionDialog";
export const SessionDropdown: Component = () => { interface SessionDropdownProps {
onCreate: () => void;
}
export const SessionDropdown: Component<SessionDropdownProps> = (props) => {
const stream = useJournalStream(); const stream = useJournalStream();
const manifest = sessions; const manifest = sessions;
const [dropdownOpen, setDropdownOpen] = createSignal(false); const [dropdownOpen, setDropdownOpen] = createSignal(false);
const [showCreateDialog, setShowCreateDialog] = createSignal(false);
let dropdownRef!: HTMLDivElement; let dropdownRef!: HTMLDivElement;
const handleSessionSelect = (id: string) => { const handleSessionSelect = (id: string) => {
@ -75,7 +77,8 @@ export const SessionDropdown: Component = () => {
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
setShowCreateDialog(true); setDropdownOpen(false);
props.onCreate();
}} }}
class="w-full text-left px-3 py-1 text-xs text-blue-600 hover:bg-gray-100" class="w-full text-left px-3 py-1 text-xs text-blue-600 hover:bg-gray-100"
> >
@ -83,10 +86,6 @@ export const SessionDropdown: Component = () => {
</button> </button>
</div> </div>
</Show> </Show>
<CreateSessionDialog
open={showCreateDialog()}
onClose={() => setShowCreateDialog(false)}
/>
</div> </div>
); );
}; };