From 3d94a298b8c1a844d3edddf4ca788403745aa36f Mon Sep 17 00:00:00 2001 From: Pabloader Date: Thu, 9 Apr 2026 14:03:41 +0000 Subject: [PATCH] Full tab chat on mobile --- src/common/hooks/useMediaQuery.ts | 14 +++++++++++ src/games/storywriter/assets/breakpoints.ts | 1 + .../storywriter/components/chat-sidebar.tsx | 7 ++++-- src/games/storywriter/components/editor.tsx | 25 +++++++++++++------ 4 files changed, 37 insertions(+), 10 deletions(-) create mode 100644 src/common/hooks/useMediaQuery.ts create mode 100644 src/games/storywriter/assets/breakpoints.ts diff --git a/src/common/hooks/useMediaQuery.ts b/src/common/hooks/useMediaQuery.ts new file mode 100644 index 0000000..c77149c --- /dev/null +++ b/src/common/hooks/useMediaQuery.ts @@ -0,0 +1,14 @@ +import { useEffect, useState } from "preact/hooks"; + +export function useMediaQuery(query: string): boolean { + const [matches, setMatches] = useState(() => window.matchMedia(query).matches); + + useEffect(() => { + const mq = window.matchMedia(query); + const handler = (e: MediaQueryListEvent) => setMatches(e.matches); + mq.addEventListener('change', handler); + return () => mq.removeEventListener('change', handler); + }, [query]); + + return matches; +} diff --git a/src/games/storywriter/assets/breakpoints.ts b/src/games/storywriter/assets/breakpoints.ts new file mode 100644 index 0000000..346974c --- /dev/null +++ b/src/games/storywriter/assets/breakpoints.ts @@ -0,0 +1 @@ +export const MOBILE_BREAKPOINT = 1000; diff --git a/src/games/storywriter/components/chat-sidebar.tsx b/src/games/storywriter/components/chat-sidebar.tsx index 2e6086e..a8f757f 100644 --- a/src/games/storywriter/components/chat-sidebar.tsx +++ b/src/games/storywriter/components/chat-sidebar.tsx @@ -1,9 +1,11 @@ import { ContentEditable } from "@common/components/ContentEditable"; import { highlight } from "@common/highlight"; import { useInputState } from "@common/hooks/useInputState"; +import { useMediaQuery } from "@common/hooks/useMediaQuery"; import clsx from "clsx"; import { Check, ChevronsRight, Edit2, GitFork, RefreshCw, Sparkles, Trash2, X } from "lucide-preact"; import { useCallback, useEffect, useMemo, useRef, useState } from "preact/hooks"; +import { MOBILE_BREAKPOINT } from '../assets/breakpoints'; import styles from '../assets/chat-sidebar.module.css'; import sidebarStyles from '../assets/sidebar.module.css'; import { useAppState, type ChatMessage } from "../contexts/state"; @@ -635,9 +637,10 @@ export const ChatPanel = ({ visible }: { visible: boolean }) => { export const ChatSidebar = () => { const { currentWorld, chatOpen, dispatch } = useAppState(); + const isMobile = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}px)`); - // In chat-only worlds, chat is a full editor tab — no sidebar needed - if (currentWorld?.chatOnly) return null; + // In chat-only worlds or on mobile, chat is a full editor tab — no sidebar needed + if (currentWorld?.chatOnly || isMobile) return null; return ( { return () => cancelAnimationFrame(raf); }, [currentStory?.id, currentWorld?.id, currentTab]); + const isMobile = useMediaQuery(`(max-width: ${MOBILE_BREAKPOINT}px)`); + const hasSelection = currentWorld !== null; const isChatOnly = currentWorld?.chatOnly ?? false; - const tabs = currentStory - ? (isChatOnly ? CHAT_STORY_TABS : STORY_TABS) - : currentWorld - ? (isChatOnly ? CHAT_WORLD_TABS : WORLD_TABS) - : [{ id: "menu" as Tab, label: "Menu", icon: List }]; + const tabs = useMemo(() => { + if (currentStory) { + if (isChatOnly) return CHAT_STORY_TABS; + if (isMobile) return [...STORY_TABS, { ...CHAT_TAB, right: true }]; + return STORY_TABS; + } + if (currentWorld) return isChatOnly ? CHAT_WORLD_TABS : WORLD_TABS; + return [{ id: "menu" as Tab, label: "Menu", icon: List }]; + }, [currentStory, currentWorld, isChatOnly, isMobile]); // Title bar: use MessagesSquare icon for chat-only worlds const WorldIcon = isChatOnly ? MessagesSquare : Globe; @@ -124,7 +133,7 @@ export const Editor = () => { class={clsx(styles.promptPreview, currentTab !== "prompt" && styles.tabHidden)} dangerouslySetInnerHTML={{ __html: Prompt.substituteVars(appState, promptPreview) }} /> - {isChatOnly && } + {(isChatOnly || isMobile) && } )} {(currentStory || currentWorld) && (<> @@ -145,7 +154,7 @@ export const Editor = () => { {tab.label} ))} - {currentStory && !isChatOnly && ( + {currentStory && !isChatOnly && !isMobile && (