feat(journal): add JournalContext to allow closing panel on mobile

This commit is contained in:
hypercross 2026-07-08 00:11:16 +08:00
parent 57ffb35f2b
commit 3d957706e9
4 changed files with 68 additions and 39 deletions

View File

@ -0,0 +1,18 @@
/**
* Journal context provides `onClose` to message renderers so that
* clicking a link message can close the panel on mobile.
*/
import { createContext, useContext } from "solid-js";
export interface JournalContextValue {
onClose: () => void;
}
const JournalContext = createContext<JournalContextValue>();
export function useJournalContext(): JournalContextValue | undefined {
return useContext(JournalContext);
}
export { JournalContext };

View File

@ -26,6 +26,7 @@ import {
} from "../stores/journalStream"; } from "../stores/journalStream";
import { StreamView } from "./StreamView"; import { StreamView } from "./StreamView";
import { JournalInput } from "./JournalInput"; import { JournalInput } from "./JournalInput";
import { JournalContext } from "./JournalContext";
export interface JournalPanelProps { export interface JournalPanelProps {
open: boolean; open: boolean;
@ -57,49 +58,51 @@ export const JournalPanel: Component<JournalPanelProps> = (props) => {
transition-transform duration-300 ease-in-out ${ transition-transform duration-300 ease-in-out ${
props.open ? "translate-x-0" : "translate-x-full" props.open ? "translate-x-0" : "translate-x-full"
}`} }`}
aria-hidden={!props.open} inert={!props.open}
> >
{/* Header */} <JournalContext.Provider value={{ onClose: props.onClose }}>
<JournalHeader onClose={props.onClose} /> {/* Header */}
<JournalHeader onClose={props.onClose} />
{/* Player list tabs */} {/* Player list tabs */}
<Show when={stream.connected}> <Show when={stream.connected}>
<div class="flex items-center gap-1 px-3 py-1.5 border-b border-gray-100 bg-gray-50 overflow-x-auto shrink-0"> <div class="flex items-center gap-1 px-3 py-1.5 border-b border-gray-100 bg-gray-50 overflow-x-auto shrink-0">
<For each={playerEntries()}> <For each={playerEntries()}>
{([name]) => ( {([name]) => (
<span class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full whitespace-nowrap"> <span class="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded-full whitespace-nowrap">
{name} {name}
</span> </span>
)} )}
</For> </For>
<button <button
onClick={() => setShowInvite(true)} onClick={() => setShowInvite(true)}
class="text-xs text-gray-400 hover:text-blue-600 px-1.5 py-0.5 rounded border border-dashed border-gray-300 hover:border-blue-400 whitespace-nowrap shrink-0" class="text-xs text-gray-400 hover:text-blue-600 px-1.5 py-0.5 rounded border border-dashed border-gray-300 hover:border-blue-400 whitespace-nowrap shrink-0"
title="邀请玩家" title="邀请玩家"
> >
+ +
</button> </button>
</div>
</Show>
<Show
when={stream.connected}
fallback={
<div class="flex-1 flex items-center justify-center p-4 overflow-y-auto">
<ConnectDialog />
</div> </div>
} </Show>
>
<div class="flex-1 min-h-0">
<StreamView />
</div>
<JournalInput />
</Show>
{/* Invite modal */} <Show
<Show when={showInvite()}> when={stream.connected}
<InviteDialog onClose={() => setShowInvite(false)} /> fallback={
</Show> <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>
<JournalInput />
</Show>
{/* Invite modal */}
<Show when={showInvite()}>
<InviteDialog onClose={() => setShowInvite(false)} />
</Show>
</JournalContext.Provider>
</aside> </aside>
</> </>
); );

View File

@ -44,3 +44,5 @@ export { ComposePanel } from "./ComposePanel";
export { JournalInput } from "./JournalInput"; export { JournalInput } from "./JournalInput";
export { DynamicForm } from "./DynamicForm"; export { DynamicForm } from "./DynamicForm";
export { useJournalCompletions, invalidateCompletions } from "./completions"; export { useJournalCompletions, invalidateCompletions } from "./completions";
export { JournalContext, useJournalContext } from "./JournalContext";
export type { JournalContextValue } from "./JournalContext";

View File

@ -11,6 +11,7 @@ import { z } from "zod";
import { registerMessageType } from "../registry"; import { registerMessageType } from "../registry";
import { journalSetState } from "../../stores/journalStream"; import { journalSetState } from "../../stores/journalStream";
import { produce } from "solid-js/store"; import { produce } from "solid-js/store";
import { useJournalContext } from "../JournalContext";
const schema = z.object({ const schema = z.object({
path: z.string().min(1), path: z.string().min(1),
@ -44,6 +45,7 @@ function slugToTitle(slug: string): string {
const RevealLink: Component<LinkPayload> = (p) => { const RevealLink: Component<LinkPayload> = (p) => {
const navigate = useNavigateWithParams(); const navigate = useNavigateWithParams();
const ctx = useJournalContext();
const target = () => { const target = () => {
let t = encodePath(p.path); let t = encodePath(p.path);
@ -59,6 +61,10 @@ const RevealLink: Component<LinkPayload> = (p) => {
const handleClick = (e: MouseEvent) => { const handleClick = (e: MouseEvent) => {
e.preventDefault(); e.preventDefault();
navigate(target()); navigate(target());
// On mobile, close the journal panel to reveal the article
if (window.innerWidth < 768) {
ctx?.onClose();
}
}; };
return ( return (