1
0
Fork 0

Move menu to the main tabs area

This commit is contained in:
Pabloader 2026-03-27 16:08:55 +00:00
parent 9803512c0b
commit d8b1893739
5 changed files with 54 additions and 72 deletions

View File

@ -1,4 +1,3 @@
import { MenuSidebar } from "./menu-sidebar";
import { Editor } from "./editor";
import { ChatSidebar } from "./chat-sidebar";
import { Title } from "@common/components/Title";
@ -13,7 +12,6 @@ export const App = () => {
{currentStory
? <Title>{currentStory.title} - Storywriter</Title>
: <Title>Storywriter</Title>}
<MenuSidebar />
<Editor />
<ChatSidebar />
</div>

View File

@ -8,10 +8,12 @@ import { CharacterEditor } from "./character-editor";
import { LocationEditor } from "./location-editor";
import { ChaptersEditor } from "./chapters-editor";
import { LoreEditor } from "./lore-editor";
import { Menu } from "./menu";
import { useInputCallback } from "@common/hooks/useInputCallback";
import Prompt from "../utils/prompt";
const TABS: { id: Tab; label: string; right?: boolean }[] = [
{ id: "menu", label: "Menu" },
{ id: "story", label: "Story" },
{ id: "chapters", label: "Chapters" },
{ id: "lore", label: "Lore" },
@ -23,7 +25,7 @@ const TABS: { id: Tab; label: string; right?: boolean }[] = [
export const Editor = () => {
const appState = useAppState();
const { currentStory, dispatch } = appState;
const { currentStory, currentTab, dispatch } = appState;
const handleInput = useInputCallback((text: string) => {
if (!currentStory) return;
@ -36,12 +38,7 @@ export const Editor = () => {
}, [currentStory?.id]);
const handleTabChange = (tab: Tab) => {
if (!currentStory) return;
dispatch({
type: 'SET_CURRENT_TAB',
id: currentStory.id,
tab,
});
dispatch({ type: 'SET_CURRENT_TAB', tab });
};
const contentRef = useRef<HTMLDivElement>(null);
@ -65,10 +62,10 @@ export const Editor = () => {
}, [currentStory?.scratchpad]);
const promptPreview = useMemo(() => {
if (currentStory?.currentTab !== 'prompt') return '';
if (currentTab !== 'prompt') return '';
const text = Prompt.formatSystemPrompt(appState);
return highlight(text, false);
}, [currentStory?.currentTab, appState]);
}, [currentTab, appState]);
useEffect(() => {
if (currentStory?.lastEditedText) {
@ -91,19 +88,16 @@ export const Editor = () => {
}
});
return () => cancelAnimationFrame(raf);
}, [currentStory?.id, currentStory?.currentTab]);
if (!currentStory) {
return <div class={styles.editor} />;
}
}, [currentStory?.id, currentTab]);
return (
<div class={styles.editor}>
<div class={styles.title}>
{currentStory.title}
</div>
{currentStory && <div class={styles.title}>{currentStory.title}</div>}
<div class={styles.content} ref={contentRef}>
{currentStory.currentTab === "story" && (
{currentTab === "menu" && (
<Menu />
)}
{currentTab === "story" && currentStory && (
<ContentEditable
class={styles.editable}
value={storyValue}
@ -111,19 +105,19 @@ export const Editor = () => {
placeholder="Start writing your story..."
/>
)}
{currentStory.currentTab === "lore" && (
{currentTab === "lore" && currentStory && (
<LoreEditor />
)}
{currentStory.currentTab === "characters" && (
{currentTab === "characters" && currentStory && (
<CharacterEditor />
)}
{currentStory.currentTab === "locations" && (
{currentTab === "locations" && currentStory && (
<LocationEditor />
)}
{currentStory.currentTab === "chapters" && (
{currentTab === "chapters" && currentStory && (
<ChaptersEditor />
)}
{currentStory.currentTab === "scratchpad" && (
{currentTab === "scratchpad" && currentStory && (
<ContentEditable
class={styles.editable}
value={scratchpadValue}
@ -131,15 +125,15 @@ export const Editor = () => {
placeholder="Notes, ideas, outlines — anything you don't want in the story..."
/>
)}
{currentStory.currentTab === "prompt" && (
{currentTab === "prompt" && currentStory && (
<div class={styles.promptPreview} dangerouslySetInnerHTML={{ __html: promptPreview }} />
)}
</div>
<div class={styles.tabs}>
{TABS.map((tab) => (
{TABS.filter(tab => currentStory || tab.id === 'menu').map((tab) => (
<button
key={tab.id}
class={clsx(styles.tab, currentStory.currentTab === tab.id && styles.active, tab.right && styles.tabRight)}
class={clsx(styles.tab, currentTab === tab.id && styles.active, tab.right && styles.tabRight)}
onClick={() => handleTabChange(tab.id)}
>
{tab.label}

View File

@ -1,12 +1,11 @@
import clsx from "clsx";
import { Sidebar } from "./sidebar";
import { ConnectionSettingsModal } from "./connection-settings-modal";
import { SettingsModal } from "./settings-modal";
import { useAppState } from "../contexts/state";
import { useBool } from "@common/hooks/useBool";
import { useInputState } from "@common/hooks/useInputState";
import type { Story } from "../contexts/state";
import styles from '../assets/menu-sidebar.module.css';
import styles from '../assets/menu.module.css';
import { Pencil, X, Plus, Plug, Settings, Copy } from "lucide-preact";
// ─── Story Item ───────────────────────────────────────────────────────────────
@ -85,7 +84,7 @@ const StoryItem = ({ story, active, onSelect, onRename, onDelete, onDuplicate }:
// ─── Menu Sidebar ─────────────────────────────────────────────────────────────
export const MenuSidebar = () => {
export const Menu = () => {
const { stories, currentStory, dispatch } = useAppState();
const isConnectionSettingsOpen = useBool(false);
const isSettingsOpen = useBool(false);
@ -115,7 +114,6 @@ export const MenuSidebar = () => {
};
return (
<Sidebar side="left">
<div class={styles.menu}>
<button class={styles.newButton} onClick={handleCreate}>
<Plus size={16} /> New Story
@ -141,13 +139,12 @@ export const MenuSidebar = () => {
<Plug size={16} /> Connection Settings
</button>
</div>
</div>
{isSettingsOpen.value && (
<SettingsModal onClose={isSettingsOpen.toggle} />
)}
{isConnectionSettingsOpen.value && (
<ConnectionSettingsModal onClose={isConnectionSettingsOpen.toggle} />
)}
</Sidebar>
</div>
);
};

View File

@ -11,7 +11,7 @@ export type ChatMessage = LLM.ChatMessage & {
id: string;
}
export type Tab = "story" | "lore" | "characters" | "locations" | "chapters" | "scratchpad" | "prompt";
export type Tab = "story" | "lore" | "characters" | "locations" | "chapters" | "scratchpad" | "prompt" | "menu";
export enum CharacterRole {
Protagonist = 'protagonist',
@ -72,7 +72,6 @@ export interface Story {
lore: LoreEntry[];
characters: Character[];
locations: Location[];
currentTab: Tab;
chatMessages: ChatMessage[];
chapters: Chapters.Chapter[];
lastEditedText?: string;
@ -83,6 +82,7 @@ export interface Story {
interface IState {
stories: Story[];
currentStoryId: string | null;
currentTab: Tab;
connection: LLM.Connection | null;
model: LLM.ModelInfo | null;
enableThinking: boolean;
@ -102,7 +102,7 @@ type Action =
| { type: 'DELETE_LORE_ENTRY'; storyId: string; entryId: string }
| { type: 'REORDER_LORE_ENTRIES'; storyId: string; entryIds: string[] }
| { type: 'SET_SYSTEM_INSTRUCTION'; systemInstruction: string }
| { type: 'SET_CURRENT_TAB'; id: string; tab: Tab }
| { type: 'SET_CURRENT_TAB'; tab: Tab }
| { type: 'DELETE_STORY'; id: string }
| { type: 'SELECT_STORY'; id: string }
| { type: 'DUPLICATE_STORY'; id: string }
@ -129,6 +129,7 @@ type Action =
const DEFAULT_STATE: IState = {
stories: [],
currentStoryId: null,
currentTab: 'menu',
connection: null,
model: null,
enableThinking: false,
@ -170,7 +171,6 @@ function reducer(state: IState, action: Action): IState {
lore: [],
characters: [],
locations: [],
currentTab: 'story',
chatMessages: [],
chapters: [],
};
@ -261,12 +261,7 @@ function reducer(state: IState, action: Action): IState {
};
}
case 'SET_CURRENT_TAB': {
return {
...state,
stories: state.stories.map(s =>
s.id === action.id ? { ...s, currentTab: action.tab } : s
),
};
return { ...state, currentTab: action.tab };
}
case 'DELETE_STORY': {
const remaining = state.stories.filter(s => s.id !== action.id);
@ -288,7 +283,6 @@ function reducer(state: IState, action: Action): IState {
lore: [...original.lore],
characters: original.characters,
locations: original.locations,
currentTab: 'story',
chatMessages: [],
chapters: [],
};
@ -299,10 +293,7 @@ function reducer(state: IState, action: Action): IState {
};
}
case 'SELECT_STORY': {
return {
...state,
currentStoryId: action.id,
};
return { ...state, currentStoryId: action.id, currentTab: 'story' };
}
case 'ADD_CHAT_MESSAGE': {
return {
@ -547,6 +538,7 @@ function reducer(state: IState, action: Action): IState {
export interface AppState {
stories: Story[];
currentStory: Story | null;
currentTab: Tab;
connection: LLM.Connection | null;
model: LLM.ModelInfo | null;
enableThinking: boolean;
@ -567,6 +559,7 @@ export const StateContextProvider = ({ children }: { children?: any }) => {
const value = useMemo<AppState>(() => ({
stories: state.stories,
currentStory: state.stories.find(s => s.id === state.currentStoryId) ?? null,
currentTab: state.currentTab,
connection: state.connection,
model: state.model,
enableThinking: state.enableThinking,