Publisher pack

This commit is contained in:
TC
2026-06-14 18:34:43 +10:00
parent 32c57e02f1
commit 839f61cffe
7 changed files with 59 additions and 4 deletions

View File

@@ -9,6 +9,7 @@ const sessionFile = (): string => join(hohoffDir(), 'session.json')
const telemetryFile = (): string => join(hohoffDir(), 'telemetry.json')
const revisionsDir = (): string => join(hohoffDir(), 'revisions')
export const getStoryBiblePath = (): string => join(hohoffDir(), 'Story Bible.md')
export const getPublisherPackPath = (): string => join(hohoffDir(), 'Publisher Pack.md')
const submissionsFile = (): string => join(hohoffDir(), 'submissions.json')
const STORY_BIBLE_TEMPLATE = `# Story Bible
@@ -257,6 +258,27 @@ export async function openStoryBibleFile(): Promise<{ path: string; content: str
return { path: getStoryBiblePath(), content }
}
const PUBLISHER_PACK_TEMPLATE = `# Synopsis
<!-- A concise summary of the novel's plot, themes, and arc -->
# Author Bio
<!-- A short professional biography in third person -->
`
export async function openPublisherPackFile(): Promise<{ path: string; content: string }> {
await mkdir(hohoffDir(), { recursive: true })
let content: string
try {
content = await readFile(getPublisherPackPath(), 'utf-8')
} catch {
content = PUBLISHER_PACK_TEMPLATE
await writeFile(getPublisherPackPath(), content, 'utf-8')
}
return { path: getPublisherPackPath(), content }
}
// Parse a markdown document into a preamble (text before first ## heading) and
// an ordered list of { header, body } sections delimited by ## headings.
function parseSections(content: string): { preamble: string; sections: { header: string; body: string }[] } {

View File

@@ -2,7 +2,7 @@ import { ipcMain, dialog, BrowserWindow } from 'electron'
import { readFileSync, writeFileSync, unlinkSync } from 'fs'
import { extname, basename, join } from 'path'
import { tmpdir } from 'os'
import { listDraftFiles, readMarkdownFile, writeMarkdownFile, getProjectWordCount, saveOrderFile, readSession, writeSession, saveRevision, listRevisions, loadRevision, deleteRevision, renameFileOrDir, deleteFileOrDir, createMarkdownFile, createSubdirectory, moveFileOrDir, readStoryBibleFile, openStoryBibleFile, writeStoryBibleFile, searchAcrossFiles, replaceInFiles, readAllDraftFiles, readProjectConfig, writeProjectConfig, PROJECT_CONFIG_FIELDS, readTelemetry, readSubmissions, writeSubmissions } from './fileSystem'
import { listDraftFiles, readMarkdownFile, writeMarkdownFile, getProjectWordCount, saveOrderFile, readSession, writeSession, saveRevision, listRevisions, loadRevision, deleteRevision, renameFileOrDir, deleteFileOrDir, createMarkdownFile, createSubdirectory, moveFileOrDir, readStoryBibleFile, openStoryBibleFile, writeStoryBibleFile, openPublisherPackFile, searchAcrossFiles, replaceInFiles, readAllDraftFiles, readProjectConfig, writeProjectConfig, PROJECT_CONFIG_FIELDS, readTelemetry, readSubmissions, writeSubmissions } from './fileSystem'
import type { SearchOptions, ProjectConfig } from './fileSystem'
import { streamMessage, resetClient } from './aiService'
import { onWordSnapshot, flushTelemetry } from './telemetry'
@@ -91,6 +91,10 @@ export function registerIpcHandlers(): void {
return await writeStoryBibleFile(content)
})
ipcMain.handle('fs:openPublisherPack', async () => {
return await openPublisherPackFile()
})
ipcMain.handle('fs:pickAttachments', async (event): Promise<Attachment[]> => {
const win = BrowserWindow.fromWebContents(event.sender)
const result = await dialog.showOpenDialog(win!, {

View File

@@ -95,6 +95,9 @@ contextBridge.exposeInMainWorld('api', {
openStoryBible: (): Promise<{ path: string; content: string }> =>
ipcRenderer.invoke('fs:openStoryBible'),
openPublisherPack: (): Promise<{ path: string; content: string }> =>
ipcRenderer.invoke('fs:openPublisherPack'),
writeStoryBible: (content: string): Promise<string> =>
ipcRenderer.invoke('fs:writeStoryBible', content),

View File

@@ -38,6 +38,15 @@ export default function App(): JSX.Element {
console.error('[App] openStoryBible failed:', err)
}
}
const handleOpenPublisherPack = async (): Promise<void> => {
try {
const { path, content } = await window.api.openPublisherPack()
setActiveFile(path, content)
} catch (err) {
console.error('[App] openPublisherPack failed:', err)
}
}
const [sidebarOpen, setSidebarOpen] = useState(
() => localStorage.getItem('sidebarOpen') !== 'false'
)
@@ -212,16 +221,23 @@ export default function App(): JSX.Element {
<button
className="sidebar-nav-btn"
onClick={handleOpenStoryBible}
title="Open Story Bible"
title="Story Bible"
>
<span className="sidebar-nav-icon">📖</span>
</button>
<button
className="sidebar-nav-btn"
onClick={handleOpenPublisherPack}
title="Publisher Pack"
>
<span className="sidebar-nav-label">Pack</span>
</button>
<button
className={`sidebar-nav-btn${showSubmissions ? ' active' : ''}`}
onClick={goSubmissions}
title="Submissions"
>
<span className="sidebar-nav-icon"></span>
<span className="sidebar-nav-label">Sub</span>
</button>
</div>
<FileTree />

View File

@@ -207,6 +207,15 @@
line-height: 1;
}
.sidebar-nav-label {
font-family: var(--font-sans);
font-size: 9px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
line-height: 1;
}
/* ─── Editor area ──────────────────────────────────────────────── */
.editor-area {
grid-row: 2;

View File

@@ -32,6 +32,7 @@ declare global {
moveFile: (sourcePath: string, targetDirPath: string) => Promise<string>
openStoryBible: () => Promise<{ path: string; content: string }>
writeStoryBible: (content: string) => Promise<string>
openPublisherPack: () => Promise<{ path: string; content: string }>
onMenuAction: (handler: (action: string) => void) => () => void
searchFiles: (query: string, options: SearchOptions) => Promise<SearchFileResult[]>
replaceInFiles: (query: string, replacement: string, options: SearchOptions, filePaths: string[]) => Promise<string[]>

File diff suppressed because one or more lines are too long