From 5453b0513fe1a4e1461f442393bd7c8f3dad0e8a Mon Sep 17 00:00:00 2001 From: Pabloader Date: Fri, 27 Mar 2026 19:33:23 +0000 Subject: [PATCH] Mobile adaptation --- .../assets/chat-sidebar.module.css | 1 + .../storywriter/assets/editor.module.css | 29 ++++++++++++++++ .../storywriter/assets/sidebar.module.css | 32 ++++++++++++++++++ .../storywriter/components/chat-sidebar.tsx | 8 ++--- src/games/storywriter/components/editor.tsx | 33 ++++++++++++------- src/games/storywriter/components/sidebar.tsx | 27 ++++++++++----- src/games/storywriter/contexts/state.tsx | 8 +++++ 7 files changed, 114 insertions(+), 24 deletions(-) diff --git a/src/games/storywriter/assets/chat-sidebar.module.css b/src/games/storywriter/assets/chat-sidebar.module.css index 0018325..f3770f7 100644 --- a/src/games/storywriter/assets/chat-sidebar.module.css +++ b/src/games/storywriter/assets/chat-sidebar.module.css @@ -5,6 +5,7 @@ padding: 8px; } + .placeholder { display: flex; align-items: center; diff --git a/src/games/storywriter/assets/editor.module.css b/src/games/storywriter/assets/editor.module.css index aa5c147..70c8d82 100644 --- a/src/games/storywriter/assets/editor.module.css +++ b/src/games/storywriter/assets/editor.module.css @@ -62,6 +62,13 @@ border-top: 1px solid var(--border); padding: 0 12px; gap: 6px; + overflow-x: auto; + flex-shrink: 0; + scrollbar-width: none; + + &::-webkit-scrollbar { + display: none; + } } .tabRight { @@ -81,7 +88,11 @@ } .tab { + display: flex; + align-items: center; + gap: 6px; padding: 12px 16px; + flex-shrink: 0; background: transparent; border: none; border-top: 2px solid transparent; @@ -101,3 +112,21 @@ border-top-color: var(--accent); } } + +@media (max-width: 1000px) { + .tabLabel { + display: none; + } + + .tab { + padding: 12px; + } + + .content { + padding: 0 16px 32px; + } + + .title { + padding: 0 16px 16px; + } +} diff --git a/src/games/storywriter/assets/sidebar.module.css b/src/games/storywriter/assets/sidebar.module.css index ebccc68..7eb3fa6 100644 --- a/src/games/storywriter/assets/sidebar.module.css +++ b/src/games/storywriter/assets/sidebar.module.css @@ -33,6 +33,38 @@ min-width: 32px; } +.closed[data-controlled] { + width: 0; + min-width: 0; + border: none; +} + +.toggleMobile { + display: none; +} + +@media (max-width: 1000px) { + .toggleMobile { + display: flex; + } +} + +@media (max-width: 1000px) { + .mobileOverlay { + position: fixed; + top: 0; + right: 0; + height: 100dvh; + z-index: 100; + border-left: none; + } + + .mobileOverlay.open { + width: 100dvw; + min-width: 0; + } +} + .toggle { display: flex; align-items: center; diff --git a/src/games/storywriter/components/chat-sidebar.tsx b/src/games/storywriter/components/chat-sidebar.tsx index 970dc7d..78bca3a 100644 --- a/src/games/storywriter/components/chat-sidebar.tsx +++ b/src/games/storywriter/components/chat-sidebar.tsx @@ -4,6 +4,7 @@ import { Sidebar } from "./sidebar"; import { useAppState, type ChatMessage } from "../contexts/state"; import { useChapterSummarization } from "../utils/useChapterSummarization"; import styles from '../assets/chat-sidebar.module.css'; +import sidebarStyles from '../assets/sidebar.module.css'; import { useState, useRef, useEffect, useMemo, useCallback } from "preact/hooks"; import LLM from "../utils/llm"; import Prompt from "../utils/prompt"; @@ -43,11 +44,10 @@ const RoleHeader = ({ message, chatMessages }: RoleHeaderProps) => { export const ChatSidebar = () => { const appState = useAppState(); - const { currentStory, dispatch, connection, model, enableThinking } = appState; + const { currentStory, dispatch, connection, model, enableThinking, chatOpen } = appState; const { summarizeAll, isSummarizing } = useChapterSummarization(); const [input, setInput] = useInputState(''); const [isLoading, setIsLoading] = useState(false); - const [isCollapsed, setCollapsed] = useState(false); const [tokenCount, setTokenCount] = useState<{ taken: number; total: number } | null>(null); const [error, setError] = useState(null); @@ -76,7 +76,7 @@ export const ChatSidebar = () => { if (messagesRef.current) { messagesRef.current.scrollTop = messagesRef.current.scrollHeight; } - }, [isCollapsed]); + }, [chatOpen]); useEffect(() => { return () => { @@ -301,7 +301,7 @@ export const ChatSidebar = () => { const isDisabled = !currentStory || !connection || !model || isLoading; return ( - + dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })} class={sidebarStyles.mobileOverlay}>
{!currentStory ? (
diff --git a/src/games/storywriter/components/editor.tsx b/src/games/storywriter/components/editor.tsx index c19b5ef..ce4fb6a 100644 --- a/src/games/storywriter/components/editor.tsx +++ b/src/games/storywriter/components/editor.tsx @@ -11,21 +11,22 @@ import { LoreEditor } from "./lore-editor"; import { Menu } from "./menu"; import { useInputCallback } from "@common/hooks/useInputCallback"; import Prompt from "../utils/prompt"; +import { BookOpen, List, Users, MapPin, BookMarked, FileText, Code, Layers, MessageSquare, type LucideIcon } from "lucide-preact"; -const TABS: { id: Tab; label: string; right?: boolean }[] = [ - { id: "menu", label: "Menu" }, - { id: "story", label: "Story" }, - { id: "chapters", label: "Chapters" }, - { id: "lore", label: "Lore" }, - { id: "characters", label: "Characters" }, - { id: "locations", label: "Locations" }, - { id: "scratchpad", label: "Scratchpad", right: true }, - { id: "prompt", label: "Prompt" }, +const TABS: { id: Tab; label: string; icon: LucideIcon; right?: boolean }[] = [ + { id: "menu", label: "Menu", icon: List }, + { id: "story", label: "Story", icon: BookOpen }, + { id: "chapters", label: "Chapters", icon: Layers }, + { id: "lore", label: "Lore", icon: BookMarked }, + { id: "characters", label: "Characters", icon: Users }, + { id: "locations", label: "Locations", icon: MapPin }, + { id: "scratchpad", label: "Scratchpad", icon: FileText, right: true }, + { id: "prompt", label: "Prompt", icon: Code }, ]; export const Editor = () => { const appState = useAppState(); - const { currentStory, currentTab, dispatch } = appState; + const { currentStory, currentTab, chatOpen, dispatch } = appState; const handleInput = useInputCallback((text: string) => { if (!currentStory) return; @@ -135,10 +136,20 @@ export const Editor = () => { key={tab.id} class={clsx(styles.tab, currentTab === tab.id && styles.active, tab.right && styles.tabRight)} onClick={() => handleTabChange(tab.id)} + title={tab.label} > - {tab.label} + + {tab.label} ))} +
); diff --git a/src/games/storywriter/components/sidebar.tsx b/src/games/storywriter/components/sidebar.tsx index c6cb826..f57dd60 100644 --- a/src/games/storywriter/components/sidebar.tsx +++ b/src/games/storywriter/components/sidebar.tsx @@ -7,28 +7,37 @@ import { PanelLeftClose, PanelLeftOpen, PanelRightClose, PanelRightOpen } from " interface Props { side: 'left' | 'right'; children?: ComponentChildren; + open?: boolean; + onToggle?: () => void; onCollapseChanged?: (collapsed: boolean) => void; + class?: string; } -export const Sidebar = ({ side, children, onCollapseChanged }: Props) => { - const open = useBool(true); +export const Sidebar = ({ side, children, open: controlledOpen, onToggle, onCollapseChanged, class: className }: Props) => { + const internalOpen = useBool(true); + const isControlled = controlledOpen !== undefined; + const isOpen = isControlled ? controlledOpen : internalOpen.value; const handleToggle = () => { - open.toggle(); - onCollapseChanged?.(!open.value); + if (isControlled) { + onToggle?.(); + } else { + internalOpen.toggle(); + } + onCollapseChanged?.(!isOpen); }; const isLeft = side === 'left'; const IconComponent = isLeft - ? (open.value ? PanelLeftClose : PanelLeftOpen) - : (open.value ? PanelRightClose : PanelRightOpen); + ? (isOpen ? PanelLeftClose : PanelLeftOpen) + : (isOpen ? PanelRightClose : PanelRightOpen); return ( -
- - {open.value && ( + {isOpen && (
{children}
diff --git a/src/games/storywriter/contexts/state.tsx b/src/games/storywriter/contexts/state.tsx index 2988494..b7040fc 100644 --- a/src/games/storywriter/contexts/state.tsx +++ b/src/games/storywriter/contexts/state.tsx @@ -83,6 +83,7 @@ interface IState { stories: Story[]; currentStoryId: string | null; currentTab: Tab; + chatOpen: boolean; connection: LLM.Connection | null; model: LLM.ModelInfo | null; enableThinking: boolean; @@ -103,6 +104,7 @@ type Action = | { type: 'REORDER_LORE_ENTRIES'; storyId: string; entryIds: string[] } | { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string } | { type: 'SET_CURRENT_TAB'; tab: Tab } + | { type: 'SET_CHAT_OPEN'; open: boolean } | { type: 'DELETE_STORY'; id: string } | { type: 'SELECT_STORY'; id: string } | { type: 'DUPLICATE_STORY'; id: string } @@ -130,6 +132,7 @@ const DEFAULT_STATE: IState = { stories: [], currentStoryId: null, currentTab: 'menu', + chatOpen: false, connection: null, model: null, enableThinking: false, @@ -263,6 +266,9 @@ function reducer(state: IState, action: Action): IState { case 'SET_CURRENT_TAB': { return { ...state, currentTab: action.tab }; } + case 'SET_CHAT_OPEN': { + return { ...state, chatOpen: action.open }; + } case 'DELETE_STORY': { const remaining = state.stories.filter(s => s.id !== action.id); const deletingCurrent = state.currentStoryId === action.id; @@ -539,6 +545,7 @@ export interface AppState { stories: Story[]; currentStory: Story | null; currentTab: Tab; + chatOpen: boolean; connection: LLM.Connection | null; model: LLM.ModelInfo | null; enableThinking: boolean; @@ -560,6 +567,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => { stories: state.stories, currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null, currentTab: state.currentTab, + chatOpen: state.chatOpen, connection: state.connection, model: state.model, enableThinking: state.enableThinking,