Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion frontend/src/components/AssistantAttachments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,7 @@ export function AttachControls() {
}

const item =
'flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs hover:bg-gray-50 dark:hover:bg-gray-700'
'flex w-full items-center gap-2 px-2 py-1.5 text-left text-xs text-gray-800 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700'

return (
<div className="relative">
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/components/RenderPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,10 @@ export default function RenderPanel({ projectId, dirty, version }: Props) {
{renders.length > 0 && (
<ul className="space-y-1" aria-label="Renders">
{renders.map((render) => (
<li key={render.id} className="card flex items-center gap-3 p-2 text-xs">
<li
key={render.id}
className="card flex items-center gap-3 p-2 text-xs text-gray-700 dark:text-gray-200"
>
<span className="w-24 font-medium text-gray-900 dark:text-gray-100">
{render.preview ? 'Preview' : 'Render'} v{render.version}
</span>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/spec/edits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ describe('narration as text', () => {
expect(scene.narration?.beats.map((b) => b.id)).toEqual(['b1', 'b2', 'b3'])
})

it('keeps a space typed at the end of a paragraph', () => {
const scene = withNarrationText(spec().segments[1], 'One two ')
expect(narrationText(scene)).toBe('One two ')
})

it('keeps one empty beat when the box is cleared', () => {
const scene = withNarrationText(spec().segments[1], '')
expect(scene.narration?.beats).toEqual([{ id: 'b1', text: '' }])
Expand Down
5 changes: 4 additions & 1 deletion frontend/src/spec/edits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,10 @@ export function narrationText(segment: Segment): string {

export function withNarrationText(segment: Segment, text: string): Segment {
const existing = segment.narration?.beats ?? []
const paragraphs = text.split(/\n\s*\n/).map((p) => p.trim())
// Not trimmed: this runs on every keystroke and the box shows the result back, so
// trimming would eat the space just typed at the end of a paragraph. Only a
// paragraph of nothing but whitespace is normalised, to empty.
const paragraphs = text.split(/\n\s*\n/).map((p) => (p.trim() === '' ? '' : p))
// An emptied box is one empty beat, not none, so the scene keeps a place to type.
const kept = paragraphs.filter((p, i) => p !== '' || i === 0)
const taken = new Set(existing.map((b) => b.id))
Expand Down
110 changes: 57 additions & 53 deletions frontend/src/views/ProjectView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -115,41 +115,67 @@ export default function ProjectView() {
}

return (
<div className="mx-auto max-w-4xl space-y-4 p-6">
<div className="flex items-center gap-3">
<Link to="/projects" className="btn btn-ghost p-2" aria-label="All videos">
<ArrowLeft className="h-4 w-4" />
</Link>
<input
className="input flex-1 text-lg font-semibold"
aria-label="Video title"
value={draft.title}
onChange={(e) => edit({ ...draft, title: e.target.value })}
/>
{dirty && (
<button type="button" className="btn btn-ghost" onClick={discard}>
<Undo2 className="mr-1 inline h-4 w-4" />
Discard
<div className="mx-auto max-w-4xl space-y-4 px-6 pb-6">
{/* Pinned so Save stays reachable however far down the scenes you are. The
opaque background stops the scenes showing through as they scroll under it. */}
<div className="sticky top-0 z-20 -mx-6 space-y-3 bg-gray-50 px-6 pb-0 pt-6 dark:bg-gray-900">
<div className="flex items-center gap-3">
<Link to="/projects" className="btn btn-ghost p-2" aria-label="All videos">
<ArrowLeft className="h-4 w-4" />
</Link>
<input
className="input flex-1 text-lg font-semibold"
aria-label="Video title"
value={draft.title}
onChange={(e) => edit({ ...draft, title: e.target.value })}
/>
{dirty && (
<button type="button" className="btn btn-ghost" onClick={discard}>
<Undo2 className="mr-1 inline h-4 w-4" />
Discard
</button>
)}
<button
type="button"
className="btn btn-primary"
disabled={!dirty || saving}
onClick={() => void save()}
>
<Save className="mr-1 inline h-4 w-4" />
{saving ? 'Saving…' : dirty ? 'Save' : 'Saved'}
</button>
)}
<button
type="button"
className="btn btn-primary"
disabled={!dirty || saving}
onClick={() => void save()}
</div>

<p className="text-xs text-gray-500">
Version {project.version}
{source?.kind === 'note' &&
' · pushed from Gecko Notes; edited only here from now on'}
{source?.kind === 'brief' && ' · drafted from a prompt'}
</p>

<div
role="tablist"
className="flex gap-1 border-b border-gray-200 dark:border-gray-700"
>
<Save className="mr-1 inline h-4 w-4" />
{saving ? 'Saving…' : dirty ? 'Save' : 'Saved'}
</button>
{TABS.map((t) => (
<button
key={t.id}
type="button"
role="tab"
aria-selected={tab === t.id}
onClick={() => setTab(t.id)}
className={`px-3 py-2 text-sm ${
tab === t.id
? 'border-b-2 border-primary-600 font-medium text-gray-900 dark:text-gray-100'
: 'text-gray-500'
}`}
>
{t.label}
</button>
))}
</div>
</div>

<p className="text-xs text-gray-500">
Version {project.version}
{source?.kind === 'note' &&
' · pushed from Gecko Notes; edited only here from now on'}
{source?.kind === 'brief' && ' · drafted from a prompt'}
</p>

{project.pending_note_media > 0 && (
<div className="space-y-2 rounded border border-amber-200 bg-amber-50 p-3 text-sm text-amber-800 dark:border-amber-800 dark:bg-amber-950 dark:text-amber-200">
<p>
Expand Down Expand Up @@ -193,28 +219,6 @@ export default function ProjectView() {
)}
<IssueList issues={issues} />

<div
role="tablist"
className="flex gap-1 border-b border-gray-200 dark:border-gray-700"
>
{TABS.map((t) => (
<button
key={t.id}
type="button"
role="tab"
aria-selected={tab === t.id}
onClick={() => setTab(t.id)}
className={`px-3 py-2 text-sm ${
tab === t.id
? 'border-b-2 border-primary-600 font-medium text-gray-900 dark:text-gray-100'
: 'text-gray-500'
}`}
>
{t.label}
</button>
))}
</div>

{tab === 'scenes' && toFind.length > 0 && (
<section aria-label="To find" className="card space-y-2 p-3">
<h2 className="text-sm font-medium text-gray-900 dark:text-gray-100">
Expand Down
Loading