diff --git a/app/runs/[id]/page.tsx b/app/runs/[id]/page.tsx
index 51fe54b..74fd22e 100644
--- a/app/runs/[id]/page.tsx
+++ b/app/runs/[id]/page.tsx
@@ -105,6 +105,10 @@ function RunWizard({ id }: { id: string }) {
toast.success('Launch plan copied')
}
+ function editField(platform: PlatformId, path: (string | number)[], value: string) {
+ dispatch({ type: 'EDIT_FIELD', platform, path, value })
+ }
+
if (status === 'loading') {
return
Loading run…
}
@@ -161,6 +165,7 @@ function RunWizard({ id }: { id: string }) {
onExportMarkdown={exportMarkdown}
onCopyPlan={copyPlan}
onExportPlan={exportPlan}
+ onEditField={editField}
onStartOver={() => router.push('/runs')}
/>
)}
diff --git a/components/common/EditableText/EditableText.test.tsx b/components/common/EditableText/EditableText.test.tsx
new file mode 100644
index 0000000..42db264
--- /dev/null
+++ b/components/common/EditableText/EditableText.test.tsx
@@ -0,0 +1,38 @@
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
+import { EditableText } from './EditableText'
+
+describe('EditableText', () => {
+ it('commits trimmed text on blur when changed', () => {
+ const onCommit = vi.fn()
+ render()
+ const el = screen.getByRole('textbox', { name: 'Field' })
+ el.textContent = ' world '
+ fireEvent.blur(el)
+ expect(onCommit).toHaveBeenCalledWith('world')
+ })
+
+ it('does not commit when the value is unchanged', () => {
+ const onCommit = vi.fn()
+ render()
+ const el = screen.getByRole('textbox', { name: 'Field' })
+ fireEvent.blur(el)
+ expect(onCommit).not.toHaveBeenCalled()
+ })
+
+ it('reverts to the previous value when cleared to empty', () => {
+ const onCommit = vi.fn()
+ render()
+ const el = screen.getByRole('textbox', { name: 'Field' })
+ el.textContent = ' '
+ fireEvent.blur(el)
+ expect(onCommit).not.toHaveBeenCalled()
+ expect(el.textContent).toBe('hello')
+ })
+
+ it('renders read-only text (no textbox) when onCommit is omitted', () => {
+ render()
+ expect(screen.queryByRole('textbox')).toBeNull()
+ expect(screen.getByText('static')).toBeInTheDocument()
+ })
+})
diff --git a/components/common/EditableText/EditableText.tsx b/components/common/EditableText/EditableText.tsx
new file mode 100644
index 0000000..6e17f3a
--- /dev/null
+++ b/components/common/EditableText/EditableText.tsx
@@ -0,0 +1,65 @@
+'use client'
+import { useRef, type KeyboardEvent, type ClipboardEvent } from 'react'
+import type { EditableTextProps } from './EditableText.types'
+
+// Seamless inline editing: the text itself is contentEditable — no input box, no
+// edit/save buttons. Edits commit on blur and persist via the run's autosave.
+// React renders `value` as the initial child and never re-renders it mid-edit
+// (commits happen on blur, so the value prop only changes when unfocused), so the
+// caret never jumps.
+export function EditableText({ value, onCommit, multiline = false, ariaLabel, className = '' }: EditableTextProps) {
+ const ref = useRef(null)
+
+ if (!onCommit) {
+ return (
+
+ {value}
+
+ )
+ }
+
+ function commit() {
+ const el = ref.current
+ if (!el) return
+ // jsdom lacks innerText; fall back to textContent so tests still exercise commit.
+ const raw = (multiline ? el.innerText || el.textContent : el.textContent) || ''
+ const next = raw.replace(/ /g, ' ').replace(/\n+$/, '').trim()
+ if (!next) {
+ el.textContent = value // never persist blank prose — revert
+ return
+ }
+ if (next !== value) onCommit!(next)
+ }
+
+ function handleKeyDown(e: KeyboardEvent) {
+ if (!multiline && e.key === 'Enter') {
+ e.preventDefault()
+ ref.current?.blur()
+ }
+ }
+
+ function handlePaste(e: ClipboardEvent) {
+ e.preventDefault()
+ const text = e.clipboardData.getData('text/plain')
+ document.execCommand('insertText', false, text)
+ }
+
+ return (
+
+ {value}
+
+ )
+}
diff --git a/components/common/EditableText/EditableText.types.ts b/components/common/EditableText/EditableText.types.ts
new file mode 100644
index 0000000..354abd7
--- /dev/null
+++ b/components/common/EditableText/EditableText.types.ts
@@ -0,0 +1,9 @@
+export type EditableTextProps = {
+ value: string
+ /** When omitted, the text renders read-only (no contentEditable). */
+ onCommit?: (next: string) => void
+ /** Multiline keeps line breaks and lets Enter insert a newline. */
+ multiline?: boolean
+ ariaLabel: string
+ className?: string
+}
diff --git a/components/common/EditableText/index.ts b/components/common/EditableText/index.ts
new file mode 100644
index 0000000..da5c4f2
--- /dev/null
+++ b/components/common/EditableText/index.ts
@@ -0,0 +1 @@
+export { EditableText } from './EditableText'
diff --git a/components/post-kit/PostKit/PostKit.test.tsx b/components/post-kit/PostKit/PostKit.test.tsx
index a7101e4..51f6f64 100644
--- a/components/post-kit/PostKit/PostKit.test.tsx
+++ b/components/post-kit/PostKit/PostKit.test.tsx
@@ -39,6 +39,7 @@ function renderKit() {
onExportMarkdown={vi.fn()}
onCopyPlan={vi.fn()}
onExportPlan={vi.fn()}
+ onEditField={vi.fn()}
onStartOver={vi.fn()}
/>
,
diff --git a/components/post-kit/PostKit/PostKit.tsx b/components/post-kit/PostKit/PostKit.tsx
index afda6c7..62654bc 100644
--- a/components/post-kit/PostKit/PostKit.tsx
+++ b/components/post-kit/PostKit/PostKit.tsx
@@ -15,7 +15,7 @@ import type { PostKitProps } from './PostKit.types'
type View = 'edit' | 'launch-ops' | 'preview'
const VIEW_LABELS: Record = { edit: 'Edit', 'launch-ops': 'Launch ops', preview: 'Preview' }
-export function PostKit({ generation, productName, onExportMarkdown, onCopyPlan, onExportPlan, onStartOver }: PostKitProps) {
+export function PostKit({ generation, productName, onExportMarkdown, onCopyPlan, onExportPlan, onEditField, onStartOver }: PostKitProps) {
const { core, platforms, plan } = generation
const firstReady = PLATFORMS.find((p) => platforms[p.id])?.id ?? 'product-hunt'
const [platform, setPlatform] = useState(firstReady)
@@ -117,7 +117,9 @@ export function PostKit({ generation, productName, onExportMarkdown, onCopyPlan,
This platform didn't generate. Pick another, or start over to try again.
) : activeView === 'edit' ? (
- Sections ? : null
+ Sections ? (
+ onEditField(platform, path, value)} />
+ ) : null
) : activeView === 'launch-ops' ? (
) : Preview ? (
diff --git a/components/post-kit/PostKit/PostKit.types.ts b/components/post-kit/PostKit/PostKit.types.ts
index 99892dd..531b2c0 100644
--- a/components/post-kit/PostKit/PostKit.types.ts
+++ b/components/post-kit/PostKit/PostKit.types.ts
@@ -7,5 +7,6 @@ export type PostKitProps = {
onExportMarkdown: (platform: PlatformId) => void
onCopyPlan: () => void
onExportPlan: () => void
+ onEditField: (platform: PlatformId, path: (string | number)[], value: string) => void
onStartOver: () => void
}
diff --git a/components/post-kit/platforms/AppSumoSections/AppSumoSections.tsx b/components/post-kit/platforms/AppSumoSections/AppSumoSections.tsx
index 74db1e4..a220b43 100644
--- a/components/post-kit/platforms/AppSumoSections/AppSumoSections.tsx
+++ b/components/post-kit/platforms/AppSumoSections/AppSumoSections.tsx
@@ -1,16 +1,22 @@
import { SectionCard } from '@/components/common/SectionCard'
import { CopyButton } from '@/components/common/CopyButton'
+import { EditableText } from '@/components/common/EditableText'
import { Badge } from '@/components/ui/badge'
import type { AppSumoContent } from '@/lib/types'
import type { PlatformSectionsProps } from '../platform-sections.types'
-export function AppSumoSections({ content, loading = false }: PlatformSectionsProps) {
+export function AppSumoSections({ content, loading = false, onEdit }: PlatformSectionsProps) {
const deal = content as AppSumoContent
return (
-
{deal.dealHeadline}
+
onEdit(['dealHeadline'], v))}
+ ariaLabel="Deal headline"
+ className="flex-1 font-medium"
+ />
@@ -20,7 +26,12 @@ export function AppSumoSections({ content, loading = false }: PlatformSectionsPr
Overview
- {deal.pitch}
+ onEdit(['pitch'], v))}
+ multiline
+ ariaLabel="Pitch"
+ />
diff --git a/components/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsx b/components/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsx
index 5f8936c..12d0ca2 100644
--- a/components/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsx
+++ b/components/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsx
@@ -1,15 +1,21 @@
import { SectionCard } from '@/components/common/SectionCard'
import { CopyButton } from '@/components/common/CopyButton'
+import { EditableText } from '@/components/common/EditableText'
import type { HackerNewsContent } from '@/lib/types'
import type { PlatformSectionsProps } from '../platform-sections.types'
-export function HackerNewsSections({ content, loading = false }: PlatformSectionsProps) {
+export function HackerNewsSections({ content, loading = false, onEdit }: PlatformSectionsProps) {
const hn = content as HackerNewsContent
return (
-
{hn.title}
+
onEdit(['title'], v))}
+ ariaLabel="Show HN title"
+ className="flex-1 font-medium"
+ />
@@ -19,7 +25,12 @@ export function HackerNewsSections({ content, loading = false }: PlatformSection
Body
- {hn.postBody}
+ onEdit(['postBody'], v))}
+ multiline
+ ariaLabel="Post body"
+ />
@@ -27,7 +38,12 @@ export function HackerNewsSections({ content, loading = false }: PlatformSection
Comment
- {hn.firstComment}
+ onEdit(['firstComment'], v))}
+ multiline
+ ariaLabel="First comment"
+ />
diff --git a/components/post-kit/platforms/ProductHuntSections/ProductHuntSections.tsx b/components/post-kit/platforms/ProductHuntSections/ProductHuntSections.tsx
index 87ff157..8ee09d9 100644
--- a/components/post-kit/platforms/ProductHuntSections/ProductHuntSections.tsx
+++ b/components/post-kit/platforms/ProductHuntSections/ProductHuntSections.tsx
@@ -6,12 +6,12 @@ import type { ProductHuntContent } from '@/lib/types'
import type { PlatformSectionsProps } from '../platform-sections.types'
// Launch-day ops live in their own "Launch ops" tab in PostKit, not in this edit view.
-export function ProductHuntSections({ content, loading }: PlatformSectionsProps) {
+export function ProductHuntSections({ content, loading, onEdit }: PlatformSectionsProps) {
const kit = content as ProductHuntContent
return (
-
-
+
+
diff --git a/components/post-kit/platforms/RedditSections/RedditSections.tsx b/components/post-kit/platforms/RedditSections/RedditSections.tsx
index f53b4b2..424a2a4 100644
--- a/components/post-kit/platforms/RedditSections/RedditSections.tsx
+++ b/components/post-kit/platforms/RedditSections/RedditSections.tsx
@@ -1,9 +1,10 @@
import { SectionCard } from '@/components/common/SectionCard'
import { CopyButton } from '@/components/common/CopyButton'
+import { EditableText } from '@/components/common/EditableText'
import type { RedditContent } from '@/lib/types'
import type { PlatformSectionsProps } from '../platform-sections.types'
-export function RedditSections({ content, loading = false }: PlatformSectionsProps) {
+export function RedditSections({ content, loading = false, onEdit }: PlatformSectionsProps) {
const reddit = content as RedditContent
return (
@@ -33,14 +34,24 @@ export function RedditSections({ content, loading = false }: PlatformSectionsPro
Title
- {reddit.title}
+ onEdit(['title'], v))}
+ ariaLabel="Reddit post title"
+ className="font-medium"
+ />
Body
-
{reddit.body}
+
onEdit(['body'], v))}
+ multiline
+ ariaLabel="Reddit post body"
+ />
diff --git a/components/post-kit/platforms/SocialSections/SocialSections.test.tsx b/components/post-kit/platforms/SocialSections/SocialSections.test.tsx
index a21a85b..a0a1d35 100644
--- a/components/post-kit/platforms/SocialSections/SocialSections.test.tsx
+++ b/components/post-kit/platforms/SocialSections/SocialSections.test.tsx
@@ -1,5 +1,5 @@
-import { describe, it, expect } from 'vitest'
-import { render, screen } from '@testing-library/react'
+import { describe, it, expect, vi } from 'vitest'
+import { render, screen, fireEvent } from '@testing-library/react'
import { SocialSections } from './SocialSections'
import type { LaunchCore, SocialContent } from '@/lib/types'
@@ -25,4 +25,18 @@ describe('SocialSections', () => {
expect(screen.getByText('Would you share how you use Acme?')).toBeInTheDocument()
expect(screen.getByText('#buildinpublic')).toBeInTheDocument()
})
+
+ it('commits a tweet edit with the correct array path', () => {
+ const onEdit = vi.fn()
+ render()
+ const tweet = screen.getByRole('textbox', { name: 'Tweet 2' })
+ tweet.textContent = 'Edited second tweet'
+ fireEvent.blur(tweet)
+ expect(onEdit).toHaveBeenCalledWith(['thread', 'tweets', 1], 'Edited second tweet')
+ })
+
+ it('renders tweets read-only when onEdit is omitted', () => {
+ render()
+ expect(screen.queryByRole('textbox', { name: 'Tweet 1' })).toBeNull()
+ })
})
diff --git a/components/post-kit/platforms/SocialSections/SocialSections.tsx b/components/post-kit/platforms/SocialSections/SocialSections.tsx
index 4056013..e6332f6 100644
--- a/components/post-kit/platforms/SocialSections/SocialSections.tsx
+++ b/components/post-kit/platforms/SocialSections/SocialSections.tsx
@@ -1,9 +1,10 @@
import { SectionCard } from '@/components/common/SectionCard'
import { CopyButton } from '@/components/common/CopyButton'
+import { EditableText } from '@/components/common/EditableText'
import type { SocialContent } from '@/lib/types'
import type { PlatformSectionsProps } from '../platform-sections.types'
-export function SocialSections({ content, loading = false }: PlatformSectionsProps) {
+export function SocialSections({ content, loading = false, onEdit }: PlatformSectionsProps) {
const social = content as SocialContent
const fullThread = social.thread.tweets.join('\n\n')
const channels: { label: string; value: string }[] = [
@@ -21,12 +22,17 @@ export function SocialSections({ content, loading = false }: PlatformSectionsPro
{social.thread.tweets.map((tweet, i) => (
- -
+
-
{i + 1}/{social.thread.tweets.length}
- {tweet}
+ onEdit(['thread', 'tweets', i], v))}
+ multiline
+ ariaLabel={`Tweet ${i + 1}`}
+ />
))}
diff --git a/components/post-kit/platforms/platform-sections.types.ts b/components/post-kit/platforms/platform-sections.types.ts
index 06b5e65..5d20325 100644
--- a/components/post-kit/platforms/platform-sections.types.ts
+++ b/components/post-kit/platforms/platform-sections.types.ts
@@ -1,5 +1,14 @@
import type { LaunchCore, PlatformContent } from '@/lib/types'
+// Commits an inline edit of one prose field. `path` is relative to the active
+// platform's content object (e.g. ['copy','tagline'], ['thread','tweets',2]).
+export type EditFieldFn = (path: (string | number)[], value: string) => void
+
// Every platform's edit-view section group takes the shared core plus its own
-// content (narrowed internally) and a loading flag for the regenerate shimmer.
-export type PlatformSectionsProps = { core: LaunchCore; content: PlatformContent; loading?: boolean }
+// content (narrowed internally), a loading flag, and an optional inline-edit hook.
+export type PlatformSectionsProps = {
+ core: LaunchCore
+ content: PlatformContent
+ loading?: boolean
+ onEdit?: EditFieldFn
+}
diff --git a/components/post-kit/sections/CopySection/CopySection.tsx b/components/post-kit/sections/CopySection/CopySection.tsx
index 177f0cf..04b9ed3 100644
--- a/components/post-kit/sections/CopySection/CopySection.tsx
+++ b/components/post-kit/sections/CopySection/CopySection.tsx
@@ -1,16 +1,22 @@
import { SectionCard } from '@/components/common/SectionCard'
import { CopyButton } from '@/components/common/CopyButton'
+import { EditableText } from '@/components/common/EditableText'
import { Badge } from '@/components/ui/badge'
import type { CopySectionProps } from './CopySection.types'
-export function CopySection({ kit, loading = false }: CopySectionProps) {
+export function CopySection({ kit, loading = false, onEdit }: CopySectionProps) {
const { copy } = kit
return (
Tagline ({copy.tagline.length}/60)
-
{copy.tagline}
+
onEdit(['copy', 'tagline'], v))}
+ ariaLabel="Tagline"
+ className="flex-1 font-medium"
+ />
@@ -26,7 +32,12 @@ export function CopySection({ kit, loading = false }: CopySectionProps) {
Description
-
{copy.description}
+
onEdit(['copy', 'description'], v))}
+ multiline
+ ariaLabel="Description"
+ />
)
diff --git a/components/post-kit/sections/CopySection/CopySection.types.ts b/components/post-kit/sections/CopySection/CopySection.types.ts
index 937e619..16dcb06 100644
--- a/components/post-kit/sections/CopySection/CopySection.types.ts
+++ b/components/post-kit/sections/CopySection/CopySection.types.ts
@@ -1,3 +1,4 @@
import type { LaunchKit } from '@/lib/types'
+import type { EditFieldFn } from '@/components/post-kit/platforms/platform-sections.types'
-export type CopySectionProps = { kit: LaunchKit; loading?: boolean }
+export type CopySectionProps = { kit: LaunchKit; loading?: boolean; onEdit?: EditFieldFn }
diff --git a/components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.tsx b/components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.tsx
index d645d10..9962668 100644
--- a/components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.tsx
+++ b/components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.tsx
@@ -1,9 +1,10 @@
import { SectionCard } from '@/components/common/SectionCard'
import { CopyButton } from '@/components/common/CopyButton'
+import { EditableText } from '@/components/common/EditableText'
import { Badge } from '@/components/ui/badge'
import type { TopicsCommentSectionProps } from './TopicsCommentSection.types'
-export function TopicsCommentSection({ kit, loading = false }: TopicsCommentSectionProps) {
+export function TopicsCommentSection({ kit, loading = false, onEdit }: TopicsCommentSectionProps) {
return (
@@ -14,7 +15,12 @@ export function TopicsCommentSection({ kit, loading = false }: TopicsCommentSect
Maker's first comment
- {kit.firstComment}
+ onEdit(['firstComment'], v))}
+ multiline
+ ariaLabel="Maker's first comment"
+ />
)
diff --git a/components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.types.ts b/components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.types.ts
index ae903c6..8957f63 100644
--- a/components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.types.ts
+++ b/components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.types.ts
@@ -1,3 +1,4 @@
import type { LaunchKit } from '@/lib/types'
+import type { EditFieldFn } from '@/components/post-kit/platforms/platform-sections.types'
-export type TopicsCommentSectionProps = { kit: LaunchKit; loading?: boolean }
+export type TopicsCommentSectionProps = { kit: LaunchKit; loading?: boolean; onEdit?: EditFieldFn }
diff --git a/lib/wizard/set-in.test.ts b/lib/wizard/set-in.test.ts
new file mode 100644
index 0000000..daeaf61
--- /dev/null
+++ b/lib/wizard/set-in.test.ts
@@ -0,0 +1,25 @@
+import { describe, it, expect } from 'vitest'
+import { setIn } from './set-in'
+
+describe('setIn', () => {
+ it('sets a nested object key immutably', () => {
+ const o = { copy: { tagline: 'a', description: 'd' } }
+ const next = setIn(o, ['copy', 'tagline'], 'b')
+ expect(next).toEqual({ copy: { tagline: 'b', description: 'd' } })
+ expect(o.copy.tagline).toBe('a')
+ expect(next).not.toBe(o)
+ expect(next.copy).not.toBe(o.copy)
+ })
+
+ it('sets an array index immutably', () => {
+ const o = { thread: { tweets: ['x', 'y', 'z'] } }
+ const next = setIn(o, ['thread', 'tweets', 1], 'Y')
+ expect(next.thread.tweets).toEqual(['x', 'Y', 'z'])
+ expect(o.thread.tweets[1]).toBe('y')
+ expect(next.thread.tweets).not.toBe(o.thread.tweets)
+ })
+
+ it('replaces the whole value at an empty path', () => {
+ expect(setIn({ a: 1 }, [], 'whole')).toBe('whole')
+ })
+})
diff --git a/lib/wizard/set-in.ts b/lib/wizard/set-in.ts
new file mode 100644
index 0000000..4f6a939
--- /dev/null
+++ b/lib/wizard/set-in.ts
@@ -0,0 +1,13 @@
+// Immutable deep-set: returns a copy of `obj` with `value` written at `path`,
+// cloning only the nodes along the path. Supports object keys and array indices.
+export function setIn(obj: T, path: ReadonlyArray, value: unknown): T {
+ if (path.length === 0) return value as T
+ const [key, ...rest] = path
+ if (typeof key === 'number') {
+ const arr = Array.isArray(obj) ? obj.slice() : []
+ arr[key] = setIn(arr[key], rest, value)
+ return arr as T
+ }
+ const src = obj && typeof obj === 'object' ? (obj as Record) : {}
+ return { ...src, [key]: setIn(src[key], rest, value) } as T
+}
diff --git a/lib/wizard/wizard-reducer.test.ts b/lib/wizard/wizard-reducer.test.ts
index 0a3a9a3..c5cc911 100644
--- a/lib/wizard/wizard-reducer.test.ts
+++ b/lib/wizard/wizard-reducer.test.ts
@@ -61,4 +61,27 @@ describe('wizardReducer', () => {
expect(re.generation).toBeNull()
expect(re.step).toBe('review')
})
+
+ it('EDIT_FIELD deep-sets a nested prose field immutably', () => {
+ const editable = {
+ core: gen.core,
+ platforms: { 'product-hunt': { copy: { tagline: 'old' } }, social: { thread: { tweets: ['a', 'b'] } } },
+ } as unknown as Generation
+ const atKit = { ...initialState, step: 'kit' as const, context: ctx, generation: editable }
+ const s = wizardReducer(atKit, { type: 'EDIT_FIELD', platform: 'product-hunt', path: ['copy', 'tagline'], value: 'new' })
+ expect((s.generation!.platforms['product-hunt'] as { copy: { tagline: string } }).copy.tagline).toBe('new')
+ expect((editable.platforms['product-hunt'] as { copy: { tagline: string } }).copy.tagline).toBe('old')
+ })
+
+ it('EDIT_FIELD edits a social tweet by array index', () => {
+ const editable = { core: gen.core, platforms: { social: { thread: { tweets: ['a', 'b'] } } } } as unknown as Generation
+ const atKit = { ...initialState, step: 'kit' as const, context: ctx, generation: editable }
+ const s = wizardReducer(atKit, { type: 'EDIT_FIELD', platform: 'social', path: ['thread', 'tweets', 0], value: 'A' })
+ expect((s.generation!.platforms.social as { thread: { tweets: string[] } }).thread.tweets).toEqual(['A', 'b'])
+ })
+
+ it('EDIT_FIELD is a no-op when there is no generation', () => {
+ const s = wizardReducer(initialState, { type: 'EDIT_FIELD', platform: 'reddit', path: ['title'], value: 'x' })
+ expect(s.generation).toBeNull()
+ })
})
diff --git a/lib/wizard/wizard-reducer.ts b/lib/wizard/wizard-reducer.ts
index d68487f..5a86d74 100644
--- a/lib/wizard/wizard-reducer.ts
+++ b/lib/wizard/wizard-reducer.ts
@@ -1,5 +1,6 @@
import type { ProjectContext, Refinements, Generation, PlatformContent } from '@/lib/types'
import type { PlatformId } from '@/lib/platforms'
+import { setIn } from './set-in'
export type WizardStep = 'folder' | 'review' | 'kit'
@@ -18,6 +19,7 @@ export type WizardAction =
| { type: 'EDIT_REFINEMENTS'; refinements: Refinements }
| { type: 'GENERATED'; generation: Generation }
| { type: 'REGEN_PLATFORM'; platform: PlatformId; content: PlatformContent }
+ | { type: 'EDIT_FIELD'; platform: PlatformId; path: (string | number)[]; value: string }
| { type: 'GO'; step: WizardStep }
| { type: 'HYDRATE'; state: WizardState }
| { type: 'RESET' }
@@ -43,6 +45,17 @@ export function wizardReducer(state: WizardState, action: WizardAction): WizardS
},
}
: state
+ case 'EDIT_FIELD':
+ // Inline edit: deep-set a single field inside the active platform's content.
+ return state.generation
+ ? {
+ ...state,
+ generation: {
+ ...state.generation,
+ platforms: setIn(state.generation.platforms, [action.platform, ...action.path], action.value),
+ },
+ }
+ : state
case 'GO': return { ...state, step: action.step }
case 'HYDRATE': return action.state
case 'RESET': return initialState