From ffb476a81a9b1fb724503e1134880abb05ffbb76 Mon Sep 17 00:00:00 2001 From: James Wampler Date: Wed, 12 Aug 2026 17:20:56 -0700 Subject: [PATCH] Add dashboard back-link to chapters list; add character via text selection Chapters page now links back to the project dashboard. Right-clicking highlighted text in the chapter summary or a beat's What happened / What's next fields offers "Add Character", creating a character from the selection and attaching it to the beat when applicable. --- .../src/components/CharacterContextMenu.tsx | 59 +++++++++++++++++++ src/Novelly.Web/src/components/ui.tsx | 5 +- src/Novelly.Web/src/pages/ChapterPage.tsx | 23 +++++++- src/Novelly.Web/src/pages/ChaptersPage.tsx | 6 ++ 4 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/Novelly.Web/src/components/CharacterContextMenu.tsx diff --git a/src/Novelly.Web/src/components/CharacterContextMenu.tsx b/src/Novelly.Web/src/components/CharacterContextMenu.tsx new file mode 100644 index 0000000..78410b4 --- /dev/null +++ b/src/Novelly.Web/src/components/CharacterContextMenu.tsx @@ -0,0 +1,59 @@ +import { useState, type MouseEvent } from 'react' +import { useCreateCharacter } from '../api/hooks' + +type MenuState = { + x: number + y: number + text: string + onCreated: (characterId: string) => void +} + +export function useCharacterContextMenu(projectId: string) { + const [menu, setMenu] = useState(null) + const createCharacter = useCreateCharacter(projectId) + + const handleContextMenu = ( + e: MouseEvent, + onCreated: (characterId: string) => void, + ) => { + const target = e.currentTarget + const text = target.value.slice(target.selectionStart, target.selectionEnd).trim() + if (!text) return + e.preventDefault() + setMenu({ x: e.clientX, y: e.clientY, text, onCreated }) + } + + const close = () => setMenu(null) + + const menuElement = menu && ( + <> +
{ + e.preventDefault() + close() + }} + /> +
+ +
+ + ) + + return { handleContextMenu, menuElement } +} diff --git a/src/Novelly.Web/src/components/ui.tsx b/src/Novelly.Web/src/components/ui.tsx index dd5ced8..e8058c5 100644 --- a/src/Novelly.Web/src/components/ui.tsx +++ b/src/Novelly.Web/src/components/ui.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState, type ReactNode } from 'react' +import { useEffect, useRef, useState, type MouseEvent, type ReactNode } from 'react' import type { DraftStatus } from '../api/types' export function Spinner({ label = 'Loading' }: { label?: string }) { @@ -67,6 +67,7 @@ export function AutoField({ rows = 3, placeholder, serif, + onContextMenu, }: { label?: string value: string | null | undefined @@ -75,6 +76,7 @@ export function AutoField({ rows?: number placeholder?: string serif?: boolean + onContextMenu?: (e: MouseEvent) => void }) { const [draft, setDraft] = useState(value ?? '') const committed = useRef(value ?? '') @@ -109,6 +111,7 @@ export function AutoField({ placeholder={placeholder} onChange={(e) => setDraft(e.target.value)} onBlur={commit} + onContextMenu={onContextMenu} /> ) : ( ('outline') + const { handleContextMenu, menuElement } = useCharacterContextMenu(projectId) useHotkey('b', 'Add beat', () => createBeat.mutate({ title: 'New beat' }), { group: 'Chapter' }) @@ -166,6 +168,7 @@ export default function ChapterPage() { serif placeholder="What this chapter is for: where it starts, what shifts, where it leaves the reader." onCommit={(summary) => patch({ summary })} + onContextMenu={(e) => handleContextMenu(e, () => {})} />
@@ -174,6 +177,7 @@ export default function ChapterPage() { projectId={projectId} characters={characters?.map((c) => ({ id: c.id, name: c.name })) ?? []} suggestions={suggestions} + onCharacterContextMenu={handleContextMenu} />