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
35 changes: 35 additions & 0 deletions app/web/components/PreviewPanel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,39 @@ describe("PreviewPanel — Genesis workflow-coach cut actions (#429)", () => {
fireEvent.click(screen.getByTestId("genesis-edit-mode-cuts"));
expect(await screen.findByTestId("cut-list-panel")).toBeInTheDocument();
});

it("keeps the opening-text save bar and episode footer outside the editor body after repeated tab/subview switches (#504)", async () => {
render(
<PreviewPanel
storyName="god-cell"
fileName="genesis.md"
authFetch={makeAuthFetch()}
contentType="cartoon"
hasGenesis
/>,
);

await screen.findByTestId("workflow-coach");

for (let i = 0; i < 2; i++) {
fireEvent.click(screen.getByRole("button", { name: /^Edit/ }));
expect(await screen.findByTestId("prose-editor-shell")).toBeInTheDocument();
expect(screen.getByTestId("prose-editor-savebar")).toHaveTextContent(
"No changes",
);
expect(screen.getByTestId("preview-panel-footer")).toBeInTheDocument();
expect(screen.getByTestId("cartoon-review-publish")).toBeInTheDocument();

fireEvent.click(screen.getByTestId("genesis-edit-mode-cuts"));
expect(await screen.findByTestId("cut-list-panel")).toBeInTheDocument();

fireEvent.click(screen.getByTestId("genesis-edit-mode-text"));
expect(await screen.findByTestId("prose-editor-textarea")).toBeInTheDocument();
expect(screen.getByTestId("prose-editor-savebar")).toBeInTheDocument();
expect(screen.getByTestId("preview-panel-footer")).toBeInTheDocument();

fireEvent.click(screen.getByRole("button", { name: /^Preview/ }));
expect(await screen.findByText("The story begins.")).toBeInTheDocument();
}
});
});
16 changes: 12 additions & 4 deletions app/web/components/PreviewPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1213,7 +1213,8 @@
// Plain prose editor (fiction files + the Genesis "Opening text" sub-view).
const proseEditor = (
<div
className="flex-1 min-h-0 flex flex-col"
className="flex-1 min-h-0 flex flex-col overflow-hidden"
data-testid="prose-editor-shell"
style={{ background: "var(--paper-bg)" }}
>
<textarea
Expand All @@ -1231,8 +1232,12 @@
color: "var(--text)",
}}
spellCheck={false}
data-testid="prose-editor-textarea"
/>
<div className="px-3 py-1.5 border-t border-border flex items-center justify-between">
<div
className="shrink-0 px-3 py-1.5 border-t border-border flex items-center justify-between bg-surface/95"
data-testid="prose-editor-savebar"
>
<span className="text-xs text-muted">
{dirty ? "Unsaved changes" : "No changes"}
</span>
Expand All @@ -1248,7 +1253,7 @@
);

return (
<div className="h-full flex flex-col">
<div className="h-full min-h-0 flex flex-col">
{/* Header with file path + tabs */}
{!hideFocusedEditorChrome && (
<div className="border-b border-border">
Expand Down Expand Up @@ -1475,7 +1480,10 @@

{/* Action bar */}
{!hideFocusedEditorChrome && (
<div className="px-3 py-2 border-t border-border flex items-center justify-between">
<div
className="shrink-0 px-3 py-2 border-t border-border flex items-center justify-between bg-surface/95"
data-testid="preview-panel-footer"
>
{fileName === "structure.md" ? (
<p
className="text-muted text-xs italic"
Expand Down Expand Up @@ -1657,7 +1665,7 @@
<div className="flex items-start gap-3">
{coverPreview && (
<div className="relative">
<img

Check warning on line 1668 in app/web/components/PreviewPanel.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={coverPreview}
alt="Cover preview"
className="w-16 h-24 object-cover rounded border border-border"
Expand Down Expand Up @@ -1962,7 +1970,7 @@
<div className="flex items-start gap-3">
{coverPreview && (
<div className="relative">
<img

Check warning on line 1973 in app/web/components/PreviewPanel.tsx

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Using `<img>` could result in slower LCP and higher bandwidth. Consider using `<Image />` from `next/image` or a custom image loader to automatically optimize images. This may incur additional usage or cost from your provider. See: https://nextjs.org/docs/messages/no-img-element
src={coverPreview}
alt="Cover preview"
className="w-16 h-24 object-cover rounded border border-border"
Expand Down
140 changes: 71 additions & 69 deletions app/web/components/StoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1196,7 +1196,7 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
{/* Preview — takes remaining space. With a story but no file selected, show
the story-level progress overview (#418) instead of the empty state. */}
<div
className="min-w-0 flex flex-col"
className="min-w-0 min-h-0 flex flex-col"
style={
hideFocusedLetteringWorkspace
? { flex: "1 0 0" }
Expand Down Expand Up @@ -1228,74 +1228,76 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
/>
</div>
)}
{isCartoonStory && cartoonView === "story-info" && selectedStory ? (
<StoryInfoPage
storyName={selectedStory}
authFetch={authFetch}
onSaved={handleStoryInfoSaved}
/>
) : isCartoonStory && cartoonView === "episodes" && selectedStory ? (
<EpisodesPage
storyName={selectedStory}
authFetch={authFetch}
onOpenFile={handleSelectFile}
/>
) : isCartoonStory && cartoonView === "publish" && selectedStory ? (
<CartoonPublishPage
storyName={selectedStory}
authFetch={authFetch}
onOpenFile={handleSelectFile}
onOpenStoryInfo={() => setCartoonView("story-info")}
onPublish={handlePublish}
publishingFile={publishingFile}
genre={storyGenres[selectedStory]}
language={storyLanguages[selectedStory]}
isNsfw={storyNsfw[selectedStory]}
refreshKey={cartoonPublishRefresh}
/>
) : selectedStory && !selectedFile ? (
<StoryProgressPanel
storyName={selectedStory}
authFetch={authFetch}
onOpenFile={handleSelectFile}
onOpenStoryInfo={() => setCartoonView("story-info")}
/>
) : (
<PreviewPanel
storyName={selectedStory}
fileName={selectedFile}
authFetch={authFetch}
onPublish={handlePublish}
publishingFile={publishingFile}
walletAddress={walletAddress}
contentType={
resolveSelectedContentType(
selectedStory,
storyContentTypes,
contentTypeMap.current,
) || "fiction"
}
language={selectedStory ? storyLanguages[selectedStory] : undefined}
genre={selectedStory ? storyGenres[selectedStory] : undefined}
isNsfw={selectedStory ? storyNsfw[selectedStory] : undefined}
hasGenesis={
selectedStory ? genesisStories.has(selectedStory) : false
}
onViewProgress={() => setSelectedFile(null)}
onOpenFile={(file) =>
selectedStory && handleSelectFile(selectedStory, file)
}
onViewPublish={() => setCartoonView("publish")}
focusedLetteringMode={focusedLetteringMode}
focusedLetteringWorkspaceVisible={focusedLetteringWorkspaceVisible}
onFocusedLetteringModeChange={handleFocusedLetteringModeChange}
onFocusedLetteringWorkspaceVisibleChange={
setFocusedLetteringWorkspaceVisible
}
/>
)}
<div className="flex-1 min-h-0 flex flex-col">
{isCartoonStory && cartoonView === "story-info" && selectedStory ? (
<StoryInfoPage
storyName={selectedStory}
authFetch={authFetch}
onSaved={handleStoryInfoSaved}
/>
) : isCartoonStory && cartoonView === "episodes" && selectedStory ? (
<EpisodesPage
storyName={selectedStory}
authFetch={authFetch}
onOpenFile={handleSelectFile}
/>
) : isCartoonStory && cartoonView === "publish" && selectedStory ? (
<CartoonPublishPage
storyName={selectedStory}
authFetch={authFetch}
onOpenFile={handleSelectFile}
onOpenStoryInfo={() => setCartoonView("story-info")}
onPublish={handlePublish}
publishingFile={publishingFile}
genre={storyGenres[selectedStory]}
language={storyLanguages[selectedStory]}
isNsfw={storyNsfw[selectedStory]}
refreshKey={cartoonPublishRefresh}
/>
) : selectedStory && !selectedFile ? (
<StoryProgressPanel
storyName={selectedStory}
authFetch={authFetch}
onOpenFile={handleSelectFile}
onOpenStoryInfo={() => setCartoonView("story-info")}
/>
) : (
<PreviewPanel
storyName={selectedStory}
fileName={selectedFile}
authFetch={authFetch}
onPublish={handlePublish}
publishingFile={publishingFile}
walletAddress={walletAddress}
contentType={
resolveSelectedContentType(
selectedStory,
storyContentTypes,
contentTypeMap.current,
) || "fiction"
}
language={selectedStory ? storyLanguages[selectedStory] : undefined}
genre={selectedStory ? storyGenres[selectedStory] : undefined}
isNsfw={selectedStory ? storyNsfw[selectedStory] : undefined}
hasGenesis={
selectedStory ? genesisStories.has(selectedStory) : false
}
onViewProgress={() => setSelectedFile(null)}
onOpenFile={(file) =>
selectedStory && handleSelectFile(selectedStory, file)
}
onViewPublish={() => setCartoonView("publish")}
focusedLetteringMode={focusedLetteringMode}
focusedLetteringWorkspaceVisible={focusedLetteringWorkspaceVisible}
onFocusedLetteringModeChange={handleFocusedLetteringModeChange}
onFocusedLetteringWorkspaceVisibleChange={
setFocusedLetteringWorkspaceVisible
}
/>
)}
</div>
{publishProgress && (
<div className="px-3 py-1.5 bg-surface border-t border-border text-xs text-muted">
<div className="shrink-0 px-3 py-1.5 bg-surface border-t border-border text-xs text-muted">
{publishProgress}
</div>
)}
Expand All @@ -1304,7 +1306,7 @@ export function StoriesPage({ token, authFetch }: StoriesPageProps) {
doesn't disappear on a timer. */}
{publishError && (
<div
className="px-3 py-2 bg-error/10 border-t border-error/40 text-xs text-error flex items-start justify-between gap-3"
className="shrink-0 px-3 py-2 bg-error/10 border-t border-error/40 text-xs text-error flex items-start justify-between gap-3"
data-testid="publish-block-error"
role="alert"
>
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading