1
0
Fork 0

Mobile adaptation

This commit is contained in:
Pabloader 2026-03-27 19:33:23 +00:00
parent d8b1893739
commit 5453b0513f
7 changed files with 114 additions and 24 deletions

View File

@ -5,6 +5,7 @@
padding: 8px;
}
.placeholder {
display: flex;
align-items: center;

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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<string | null>(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 (
<Sidebar side="right" onCollapseChanged={setCollapsed}>
<Sidebar side="right" open={chatOpen} onToggle={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })} class={sidebarStyles.mobileOverlay}>
<div class={styles.chat}>
{!currentStory ? (
<div class={styles.placeholder}>

View File

@ -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.icon size={15} />
<span class={styles.tabLabel}>{tab.label}</span>
</button>
))}
<button
class={clsx(styles.tab, styles.tabRight, chatOpen && styles.active)}
onClick={() => dispatch({ type: 'SET_CHAT_OPEN', open: !chatOpen })}
title="Chat"
>
<MessageSquare size={15} />
<span class={styles.tabLabel}>Chat</span>
</button>
</div>
</div>
);

View File

@ -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 (
<div class={clsx(styles.sidebar, open.value ? styles.open : styles.closed)} data-side={side}>
<button class={styles.toggle} onClick={handleToggle}>
<div class={clsx(styles.sidebar, isOpen ? styles.open : styles.closed, className)} data-side={side} data-controlled={isControlled || undefined}>
<button class={clsx(styles.toggle, isControlled && styles.toggleMobile)} onClick={handleToggle}>
<IconComponent size={16} />
</button>
{open.value && (
{isOpen && (
<div class={styles.content}>
{children}
</div>

View File

@ -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,