diff --git a/apps/admin/src/components/EditorShell.tsx b/apps/admin/src/components/EditorShell.tsx
index 19092a8..5afcf82 100644
--- a/apps/admin/src/components/EditorShell.tsx
+++ b/apps/admin/src/components/EditorShell.tsx
@@ -1,10 +1,14 @@
import { Box, Button, Stack, Typography, TextField, MenuItem, Snackbar, Alert, Stepper, Step, StepLabel, StepButton } from '@mui/material';
import AdminLayout from '../layout/AdminLayout';
-import Recorder from '../features/recorder/Recorder';
-import RichEditor from './RichEditor';
import type { RichEditorHandle } from './RichEditor';
-import MediaLibrary from './MediaLibrary';
-import MetadataPanel, { type Metadata } from './MetadataPanel';
+import { type Metadata } from './MetadataPanel';
+import StepAssets from './steps/StepAssets';
+import StepAiPrompt from './steps/StepAiPrompt';
+import StepGenerate from './steps/StepGenerate';
+import StepEdit from './steps/StepEdit';
+import StepMetadata from './steps/StepMetadata';
+import StepPublish from './steps/StepPublish';
+import StepContainer from './steps/StepContainer';
import { useEffect, useRef, useState } from 'react';
export default function EditorShell({ onLogout, initialPostId, onBack }: { onLogout?: () => void; initialPostId?: string | null; onBack?: () => void }) {
@@ -208,174 +212,69 @@ export default function EditorShell({ onLogout, initialPostId, onBack }: { onLog
{activeStep === 0 && (
-
- Assets (Audio & Images)
-
-
- editorRef.current?.insertHtmlAtCursor(html)}
- initialClips={postClips}
- />
-
-
- {
- if (editorRef.current) {
- editorRef.current.insertHtmlAtCursor(`
`);
- } else {
- setDraft((prev) => `${prev || ''}
`);
- }
- }}
- onSetFeature={(url) => setMeta(m => ({ ...m, featureImage: url }))}
- showSetFeature
- />
-
-
-
+
+ editorRef.current?.insertHtmlAtCursor(html)}
+ onInsertImage={(url: string) => {
+ if (editorRef.current) {
+ editorRef.current.insertHtmlAtCursor(`
`);
+ } else {
+ setDraft((prev) => `${prev || ''}
`);
+ }
+ }}
+ onSetFeature={(url: string) => setMeta(m => ({ ...m, featureImage: url }))}
+ />
+
)}
{activeStep === 1 && (
-
- AI Prompt
- setPromptText(e.target.value)}
- fullWidth
- multiline
- minRows={6}
- placeholder="Describe the goal, audience, tone, outline, and reference transcript/image context to guide AI content generation."
- />
-
+
+
+
)}
{activeStep === 2 && (
-
- Generate
-
- Select images as generation assets, review audio transcriptions, and set the prompt to guide AI.
-
-
- {/* Audio transcriptions in order */}
-
- Audio Transcriptions
-
- {[...postClips].sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()).map((clip, idx) => (
-
- #{idx + 1} · {new Date(clip.createdAt).toLocaleString()}
- {clip.transcript || '(no transcript yet)'}
-
- ))}
- {postClips.length === 0 && (
- (No audio clips)
- )}
-
-
-
- {/* Images selected for generation */}
-
- Selected Images
-
- {genImageKeys.map((k) => (
-
-
-
-
- ))}
- {genImageKeys.length === 0 && (
- (No images selected)
- )}
-
-
-
- {/* Media library for selecting images */}
-
+
-
- {/* AI prompt used for generation */}
-
- AI Prompt
- setPromptText(e.target.value)}
- fullWidth
- multiline
- minRows={4}
- />
-
-
-
+
)}
{activeStep === 3 && (
-
- Edit Content
-
- setDraft(html)} placeholder="Write your post..." />
-
- {draftId && (
- ID: {draftId}
- )}
-
+
+
+
)}
{activeStep === 4 && (
-
+
+
+
)}
{activeStep === 5 && (
-
- Publish
-
- Preview reflects Ghost media URL rewriting. Layout may differ from your Ghost theme.
-
-
-
-
-
- {previewLoading && (
- Generating preview…
- )}
- {previewError && (
- {previewError}
- )}
- {!previewLoading && !previewError && (
-
- )}
-
-
-
-
-
+
+
+
)}
{/* Sticky bottom nav so Back/Next don't move */}
diff --git a/apps/admin/src/components/steps/SelectedImages.tsx b/apps/admin/src/components/steps/SelectedImages.tsx
new file mode 100644
index 0000000..08a9cd2
--- /dev/null
+++ b/apps/admin/src/components/steps/SelectedImages.tsx
@@ -0,0 +1,26 @@
+import { Box, Button, Typography } from '@mui/material';
+
+export default function SelectedImages({
+ imageKeys,
+ onRemove,
+}: {
+ imageKeys: string[];
+ onRemove: (key: string) => void;
+}) {
+ return (
+
+ Selected Images
+
+ {imageKeys.map((k) => (
+
+
+
+
+ ))}
+ {imageKeys.length === 0 && (
+ (No images selected)
+ )}
+
+
+ );
+}
diff --git a/apps/admin/src/components/steps/StepAiPrompt.tsx b/apps/admin/src/components/steps/StepAiPrompt.tsx
new file mode 100644
index 0000000..7efa112
--- /dev/null
+++ b/apps/admin/src/components/steps/StepAiPrompt.tsx
@@ -0,0 +1,24 @@
+import { Box, TextField, Typography } from '@mui/material';
+
+export default function StepAiPrompt({
+ promptText,
+ onChangePrompt,
+}: {
+ promptText: string;
+ onChangePrompt: (v: string) => void;
+}) {
+ return (
+
+ AI Prompt
+ onChangePrompt(e.target.value)}
+ fullWidth
+ multiline
+ minRows={6}
+ placeholder="Describe the goal, audience, tone, outline, and reference transcript/image context to guide AI content generation."
+ />
+
+ );
+}
diff --git a/apps/admin/src/components/steps/StepAssets.tsx b/apps/admin/src/components/steps/StepAssets.tsx
new file mode 100644
index 0000000..b232e51
--- /dev/null
+++ b/apps/admin/src/components/steps/StepAssets.tsx
@@ -0,0 +1,41 @@
+import { Box, Stack, Typography } from '@mui/material';
+import Recorder from '../../features/recorder/Recorder';
+import MediaLibrary from '../MediaLibrary';
+
+export type Clip = { id: string; bucket: string; key: string; mime: string; transcript?: string; createdAt: string };
+
+export default function StepAssets({
+ draftId,
+ postClips,
+ onInsertAtCursor,
+ onInsertImage,
+ onSetFeature,
+}: {
+ draftId?: string | null;
+ postClips: Clip[];
+ onInsertAtCursor: (html: string) => void;
+ onInsertImage: (url: string) => void;
+ onSetFeature: (url: string) => void;
+}) {
+ return (
+
+ Assets (Audio & Images)
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/apps/admin/src/components/steps/StepContainer.tsx b/apps/admin/src/components/steps/StepContainer.tsx
new file mode 100644
index 0000000..fdb4ae8
--- /dev/null
+++ b/apps/admin/src/components/steps/StepContainer.tsx
@@ -0,0 +1,10 @@
+import { Box, type SxProps, type Theme } from '@mui/material';
+import type { PropsWithChildren } from 'react';
+
+export default function StepContainer({ children, sx }: PropsWithChildren<{ sx?: SxProps }>) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/apps/admin/src/components/steps/StepEdit.tsx b/apps/admin/src/components/steps/StepEdit.tsx
new file mode 100644
index 0000000..ea50551
--- /dev/null
+++ b/apps/admin/src/components/steps/StepEdit.tsx
@@ -0,0 +1,32 @@
+import { Box, Typography } from '@mui/material';
+import RichEditor, { type RichEditorHandle } from '../RichEditor';
+import type { ForwardedRef } from 'react';
+
+export default function StepEdit({
+ editorRef,
+ draftHtml,
+ onChangeDraft,
+ draftId,
+}: {
+ editorRef: ForwardedRef | any;
+ draftHtml: string;
+ onChangeDraft: (html: string) => void;
+ draftId?: string | null;
+}) {
+ return (
+
+ Edit Content
+
+
+
+ {draftId && (
+ ID: {draftId}
+ )}
+
+ );
+}
diff --git a/apps/admin/src/components/steps/StepGenerate.tsx b/apps/admin/src/components/steps/StepGenerate.tsx
new file mode 100644
index 0000000..7cf048f
--- /dev/null
+++ b/apps/admin/src/components/steps/StepGenerate.tsx
@@ -0,0 +1,64 @@
+import { Box, Stack, TextField, Typography } from '@mui/material';
+import MediaLibrary from '../MediaLibrary';
+import SelectedImages from './SelectedImages';
+import type { Clip } from './StepAssets';
+
+export default function StepGenerate({
+ postClips,
+ genImageKeys,
+ onToggleGenImage,
+ promptText,
+ onChangePrompt,
+}: {
+ postClips: Clip[];
+ genImageKeys: string[];
+ onToggleGenImage: (key: string) => void;
+ promptText: string;
+ onChangePrompt: (v: string) => void;
+}) {
+ return (
+
+ Generate
+
+ Select images as generation assets, review audio transcriptions, and set the prompt to guide AI.
+
+
+ {/* Audio transcriptions in order */}
+
+ Audio Transcriptions
+
+ {[...postClips]
+ .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime())
+ .map((clip, idx) => (
+
+ #{idx + 1} · {new Date(clip.createdAt).toLocaleString()}
+ {clip.transcript || '(no transcript yet)'}
+
+ ))}
+ {postClips.length === 0 && (
+ (No audio clips)
+ )}
+
+
+
+ {/* Selected images */}
+
+
+ {/* Media library */}
+
+
+ {/* Prompt */}
+
+ AI Prompt
+ onChangePrompt(e.target.value)}
+ fullWidth
+ multiline
+ minRows={4}
+ />
+
+
+ );
+}
diff --git a/apps/admin/src/components/steps/StepMetadata.tsx b/apps/admin/src/components/steps/StepMetadata.tsx
new file mode 100644
index 0000000..30e6bd2
--- /dev/null
+++ b/apps/admin/src/components/steps/StepMetadata.tsx
@@ -0,0 +1,10 @@
+import { Box } from '@mui/material';
+import MetadataPanel, { type Metadata } from '../MetadataPanel';
+
+export default function StepMetadata({ value, onChange }: { value: Metadata; onChange: (v: Metadata) => void }) {
+ return (
+
+
+
+ );
+}
diff --git a/apps/admin/src/components/steps/StepPublish.tsx b/apps/admin/src/components/steps/StepPublish.tsx
new file mode 100644
index 0000000..0652856
--- /dev/null
+++ b/apps/admin/src/components/steps/StepPublish.tsx
@@ -0,0 +1,57 @@
+import { Alert, Box, Button, Stack, Typography } from '@mui/material';
+
+export default function StepPublish({
+ previewLoading,
+ previewError,
+ previewHtml,
+ draftHtml,
+ onRefreshPreview,
+ onSaveDraft,
+ onGhostPublish,
+}: {
+ previewLoading: boolean;
+ previewError: string | null;
+ previewHtml: string;
+ draftHtml: string;
+ onRefreshPreview: () => void;
+ onSaveDraft: () => void;
+ onGhostPublish: (status: 'draft' | 'published') => void;
+}) {
+ return (
+
+ Publish
+
+ Preview reflects Ghost media URL rewriting. Layout may differ from your Ghost theme.
+
+
+
+
+
+ {previewLoading && (
+ Generating preview…
+ )}
+ {previewError && (
+ {previewError}
+ )}
+ {!previewLoading && !previewError && (
+
+ )}
+
+
+
+
+
+ );
+}