♿ display prompt separately from story text
This commit is contained in:
@@ -9,6 +9,7 @@ export interface StoryMeta {
|
||||
wordCountTarget?: number
|
||||
tags?: string[]
|
||||
notes?: string
|
||||
prompt?: string
|
||||
}
|
||||
|
||||
export interface Market {
|
||||
|
||||
@@ -72,13 +72,12 @@ export function PromptHero(): JSX.Element {
|
||||
}
|
||||
const name = `Story ${stories.length + 1}`
|
||||
const created = await window.api.createStory(name)
|
||||
await window.api.setStoryMeta(created.id, { prompt })
|
||||
const refreshed = await window.api.listStories()
|
||||
setStories(refreshed)
|
||||
const story = refreshed.find((s) => s.path === created.path)
|
||||
if (story) {
|
||||
const initial = `> ${prompt}\n\n`
|
||||
await window.api.writeStory(story.path, initial)
|
||||
setActiveStory(story.path, story.id, initial)
|
||||
setActiveStory(story.path, story.id, '')
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { EditorView, keymap, Decoration, type DecorationSet, ViewPlugin, type ViewUpdate } from '@codemirror/view'
|
||||
import { EditorState, StateField, StateEffect, RangeSetBuilder, Compartment } from '@codemirror/state'
|
||||
import { markdown } from '@codemirror/lang-markdown'
|
||||
@@ -53,11 +53,11 @@ const annPlugin = ViewPlugin.fromClass(class {
|
||||
const themeCompartment = new Compartment()
|
||||
const fontSizeCompartment = new Compartment()
|
||||
|
||||
function buildTheme(fontSize: number, isDark: boolean): ReturnType<typeof EditorView.theme> {
|
||||
function buildTheme(fontSize: number, isDark: boolean, hasPrompt = false): ReturnType<typeof EditorView.theme> {
|
||||
return EditorView.theme({
|
||||
'&': { height: '100%', background: 'transparent' },
|
||||
'.cm-scroller': { fontFamily: 'Georgia, "Times New Roman", serif', fontSize: `${fontSize}px`, lineHeight: '1.85', overflow: 'auto' },
|
||||
'.cm-content': { maxWidth: '680px', margin: '0 auto', padding: '48px 24px 24px', caretColor: 'var(--accent)' },
|
||||
'.cm-content': { maxWidth: '680px', margin: '0 auto', padding: `${hasPrompt ? '24px' : '48px'} 24px 24px`, caretColor: 'var(--accent)' },
|
||||
'.cm-line': { padding: '0' },
|
||||
'.cm-cursor': { borderLeftColor: 'var(--accent)', borderLeftWidth: '2px' },
|
||||
'.cm-selectionBackground': { background: 'rgba(200,169,110,0.25)' },
|
||||
@@ -85,6 +85,7 @@ export function MarkdownEditor(): JSX.Element {
|
||||
const viewRef = useRef<EditorView | null>(null)
|
||||
const lastPathRef = useRef<string | null>(null)
|
||||
const storeTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const [storyPrompt, setStoryPrompt] = useState<string | null>(null)
|
||||
|
||||
|
||||
// Initialize editor
|
||||
@@ -159,6 +160,14 @@ export function MarkdownEditor(): JSX.Element {
|
||||
}
|
||||
}, [activeStoryPath, activeStoryContent, setRevisions])
|
||||
|
||||
// Load prompt from meta when story changes
|
||||
useEffect(() => {
|
||||
if (!activeStoryId) { setStoryPrompt(null); return }
|
||||
window.api.getStoryMeta(activeStoryId).then((meta) => {
|
||||
setStoryPrompt(meta.prompt ?? null)
|
||||
}).catch(() => setStoryPrompt(null))
|
||||
}, [activeStoryId])
|
||||
|
||||
// Sync annotations into editor
|
||||
useEffect(() => {
|
||||
const view = viewRef.current
|
||||
@@ -168,8 +177,8 @@ export function MarkdownEditor(): JSX.Element {
|
||||
|
||||
// Update theme
|
||||
useEffect(() => {
|
||||
viewRef.current?.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark')) })
|
||||
}, [theme, fontSize])
|
||||
viewRef.current?.dispatch({ effects: themeCompartment.reconfigure(buildTheme(fontSize, theme === 'dark', !!storyPrompt)) })
|
||||
}, [theme, fontSize, storyPrompt])
|
||||
|
||||
if (!activeStoryId) {
|
||||
return <div className="no-story-placeholder">Select a story or create a new one</div>
|
||||
@@ -178,6 +187,14 @@ export function MarkdownEditor(): JSX.Element {
|
||||
return (
|
||||
<div style={{ display: 'flex', flexDirection: 'column', height: '100%', position: 'relative' }}>
|
||||
<WordCountBar />
|
||||
{storyPrompt && (
|
||||
<div className="story-prompt-banner">
|
||||
<div className="story-prompt-banner-inner">
|
||||
<span className="story-prompt-banner-label">Prompt</span>
|
||||
{storyPrompt}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{ flex: 1, overflow: 'hidden', position: 'relative' }}
|
||||
onMouseDown={(e) => {
|
||||
|
||||
@@ -799,3 +799,31 @@ textarea { resize: vertical; }
|
||||
/* Misc */
|
||||
.inline-edit { background: none; border: none; padding: 0; font: inherit; color: inherit; width: 100%; outline: none; border-bottom: 1px solid var(--accent); }
|
||||
.no-story-placeholder { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text3); font-size: 14px; }
|
||||
|
||||
/* ── Story prompt banner ─────────────────────────────────────────────────── */
|
||||
.story-prompt-banner {
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
}
|
||||
.story-prompt-banner-inner {
|
||||
max-width: 680px;
|
||||
margin: 0 auto;
|
||||
padding: 28px 24px 0;
|
||||
font-size: 13px;
|
||||
font-style: italic;
|
||||
color: var(--text3);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.story-prompt-banner-label {
|
||||
display: block;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-style: normal;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.07em;
|
||||
color: var(--text3);
|
||||
margin-bottom: 6px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ export interface StoryMeta {
|
||||
wordCountTarget?: number
|
||||
tags?: string[]
|
||||
notes?: string
|
||||
prompt?: string
|
||||
}
|
||||
|
||||
export interface StoryFile {
|
||||
|
||||
Reference in New Issue
Block a user