Skip to content

feat: seamless inline editing of generated prose - #8

Merged
AhmedFr merged 1 commit into
mainfrom
feat/inline-editing
Jun 21, 2026
Merged

AhmedFr merged 1 commit into
mainfrom
feat/inline-editing

Conversation

@AhmedFr

@AhmedFr AhmedFr commented Jun 21, 2026 •

Copy link
Copy Markdown
Owner

What & why

The Edit tab rendered generated copy as read-only cards. This makes the main prose directly editable in place — click the text and type. No input boxes, no edit mode, no save/edit buttons; edits persist automatically and Preview reflects them.

Design spec was brainstormed and approved first (kept locally at docs/superpowers/specs/2026-06-21-inline-editing-design.md, which is gitignored).

How it works

  • EditableText (components/common/EditableText) — the seamless primitive. Uncontrolled contentEditable (React renders value once and never mid-edit, so the caret never jumps), commits the trimmed text on blur. Single-line: Enter commits + blurs; multiline: Enter = newline. Paste is forced to plain text. Cleared-to-empty reverts to the previous value. Affordance: subtle hover tint + cursor-text and a soft focus ring — no buttons, no boxes. Renders read-only when no onCommit is passed.
  • State: new immutable setIn deep-set (lib/wizard/set-in.ts) + an EDIT_FIELD { platform, path, value } reducer action that updates the active platform's content by path (supports array indices, e.g. ['thread','tweets',2]). Persistence is free — useRun already autosaves state to localStorage, so edits survive reload and flow into Export. Preview reads the same state.
  • Wiring: PlatformSectionsProps gains onEdit; PostKit takes onEditField(platform, path, value) and binds the active platform; the run page dispatches EDIT_FIELD.

Scope (per agreed design — main prose only)

Editable: Product Hunt tagline / description / first comment; Hacker News title / post body / first comment; Reddit title / body; AppSumo deal headline / pitch; each Social thread tweet. Read-only: lists, topics, checklists, gallery/video, outreach hints, the Launch ops tab, and the Launch Plan. No add/remove of list items.

How it was tested

Verified the real round-trip in the browser (throwaway harness, since deleted): typed into the tagline, clicked away → the value committed and the live (38/60) counter updated, with no caret disruption. The edited text looks identical to static text.

  • pnpm test — 100 passed (18 files)
  • pnpm typecheck
  • pnpm lint
  • pnpm build

Checklist

  • Follows the single-responsibility / per-component folder structure (EditableText, set-in)
  • No secrets, API keys, or .env values committed
  • README / docs updated if behavior or configuration changed (n/a — internal UX; no config change)
  • Screenshots included for UI changes

Screenshots / notes

  • Screenshots not committed (captured via the deleted verification harness). The change is invisible at rest by design; on hover an editable field gets a faint tint + I-beam cursor, on focus a soft ring.
  • Tests: setIn, EDIT_FIELD reducer, EditableText behavior, and a SocialSections edit-path test (asserts the correct ['thread','tweets',i] path).
  • Possible fast-follow if desired: live char counter while typing (currently updates on commit), and extending editing to lists/Launch ops/Plan.

Summary by CodeRabbit

  • New Features
    • Added inline editing capabilities for post content across multiple platforms: edit Reddit titles and body text, Hacker News posts and comments, AppSumo headlines and pitches, Product Hunt fields, social media posts (including individual tweets), and copy sections directly from the post kit interface.

Click any main prose field on the Edit tab and type in place — no input boxes, no
edit/save buttons. Edits commit on blur and persist automatically via the run's
existing autosave; Preview reflects them with no extra wiring.

- New EditableText primitive: uncontrolled contentEditable, commit-on-blur, Enter
  commits single-line / inserts newline multiline, paste-as-plain-text, reverts to
  the previous value if cleared. Subtle hover tint + focus ring, no chrome.
- New immutable setIn deep-set + EDIT_FIELD reducer action update the active
  platform's content by path (supports array indices, e.g. social tweets).
- Editable fields: PH tagline/description/first-comment, HN title/post/first-comment,
  Reddit title/body, AppSumo headline/pitch, each Social thread tweet. Lists, hints,
  Launch ops, and the Launch Plan stay read-only (per agreed scope).

Tests: setIn, EDIT_FIELD reducer, EditableText behavior, and a section edit-path test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
launch-kit Ready Ready Preview, Comment Jun 21, 2026 2:25pm

@coderabbitai

coderabbitai Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds inline field editing to the post-kit wizard. It introduces a setIn immutable deep-set utility, a new EDIT_FIELD action in the wizard reducer, and a reusable EditableText component. All five platform section components and two shared section components replace static text renders with EditableText, wired through a new onEditField prop that propagates from RunPage through PostKit to each section.

Changes

Inline Field Editing for Post-Kit

Layer / File(s) Summary
setIn utility and EDIT_FIELD reducer action
lib/wizard/set-in.ts, lib/wizard/set-in.test.ts, lib/wizard/wizard-reducer.ts, lib/wizard/wizard-reducer.test.ts
setIn performs immutable deep updates on objects/arrays; EDIT_FIELD action added to WizardAction union and handled in wizardReducer to update generation.platforms at [platform, ...path] via setIn. Three new reducer tests and three setIn tests verify immutability and array/object update correctness.
EditableText component
components/common/EditableText/EditableText.types.ts, components/common/EditableText/EditableText.tsx, components/common/EditableText/index.ts, components/common/EditableText/EditableText.test.tsx
New EditableText client component using contentEditable; commits trimmed text on blur, reverts on blank, forces plain-text paste, and blurs on Enter for single-line mode. Renders a read-only div when onCommit is omitted. Four tests cover commit, no-op, revert, and read-only behaviors.
PlatformSectionsProps, section types, and per-platform wiring
components/post-kit/platforms/platform-sections.types.ts, components/post-kit/sections/CopySection/CopySection.types.ts, components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.types.ts, components/post-kit/sections/CopySection/CopySection.tsx, components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.tsx, components/post-kit/platforms/AppSumoSections/AppSumoSections.tsx, components/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsx, components/post-kit/platforms/ProductHuntSections/ProductHuntSections.tsx, components/post-kit/platforms/RedditSections/RedditSections.tsx, components/post-kit/platforms/SocialSections/SocialSections.tsx, components/post-kit/platforms/SocialSections/SocialSections.test.tsx
EditFieldFn type added; PlatformSectionsProps, CopySectionProps, and TopicsCommentSectionProps gain optional onEdit. All platform section components replace static <p> renders with EditableText wired to onEdit at their respective field paths. Two new SocialSections tests verify onEdit invocation and read-only mode.
PostKit prop and RunPage wiring
components/post-kit/PostKit/PostKit.types.ts, components/post-kit/PostKit/PostKit.tsx, components/post-kit/PostKit/PostKit.test.tsx, app/runs/[id]/page.tsx
PostKitProps gains onEditField; PostKit forwards it as onEdit into the active platform Sections in the edit view. RunPage defines an editField helper that dispatches EDIT_FIELD and passes it as onEditField to PostKit.

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant EditableText
  participant PlatformSections
  participant PostKit
  participant RunPage
  participant wizardReducer

  User->>EditableText: blur after editing text
  EditableText->>EditableText: normalize & validate (trim, revert if blank)
  EditableText->>PlatformSections: onCommit(newValue)
  PlatformSections->>PostKit: onEdit(fieldPath, newValue)
  PostKit->>RunPage: onEditField(platform, fieldPath, newValue)
  RunPage->>wizardReducer: dispatch EDIT_FIELD { platform, path, value }
  wizardReducer->>wizardReducer: setIn(generation.platforms, [platform, ...path], value)
  wizardReducer-->>RunPage: updated state with new generation.platforms
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • AhmedFr/launch-kit#1: The main PR extends lib/wizard/wizard-reducer.ts with a new EDIT_FIELD action branch, directly building on the wizard reducer structure introduced in this PR.
  • AhmedFr/launch-kit#2: This PR reshaped WizardAction and the wizard state machine; the main PR adds EDIT_FIELD to that same WizardAction union and handles it in the same reducer.

Poem

🐇 A field once frozen, locked in stone,
Now blinks and waits for text you've typed alone.
A setIn tunnel digs the path just right,
EditableText glows with contentEditable light.
On blur the rabbit thumps — commit complete!
No blank reversions, only trimmed and neat. ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 16.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat: seamless inline editing of generated prose' accurately summarizes the main feature addition and aligns with the primary change across all modified files.
Description check ✅ Passed The description comprehensively covers all required template sections: it explains what changed and why, details the testing performed with checkboxes verified, confirms the checklist items, and provides technical implementation details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/inline-editing

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/common/EditableText/EditableText.tsx`:
- Around line 41-45: In the handlePaste function, replace the deprecated
document.execCommand('insertText') call with a modern approach that prioritizes
the Selection and Range APIs. First attempt to retrieve the current selection
using window.getSelection(), create a range, delete the current content, and
insert the new text using the Range API. If the selection is unavailable or the
modern approach fails, fall back to document.execCommand('insertText') as a
compatibility measure to ensure reliable paste handling across all browsers.

In `@lib/wizard/set-in.ts`:
- Around line 6-9: The setIn function currently accepts any numeric key without
validation, allowing negative numbers and non-integer values like -1 or 1.5 to
be set as array properties instead of proper indices. Add a guard condition
after the typeof check for 'number' in the setIn function to validate that the
key is a non-negative integer (e.g., using Number.isInteger and checking that
key >= 0). If the key fails validation, throw an error or handle it
appropriately to prevent invalid array shapes from being created silently.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: aff72f65-5491-4568-8c7a-458954f848eb

📥 Commits

Reviewing files that changed from the base of the PR and between 8356325 and 607d4ed.

📒 Files selected for processing (23)
  • app/runs/[id]/page.tsx
  • components/common/EditableText/EditableText.test.tsx
  • components/common/EditableText/EditableText.tsx
  • components/common/EditableText/EditableText.types.ts
  • components/common/EditableText/index.ts
  • components/post-kit/PostKit/PostKit.test.tsx
  • components/post-kit/PostKit/PostKit.tsx
  • components/post-kit/PostKit/PostKit.types.ts
  • components/post-kit/platforms/AppSumoSections/AppSumoSections.tsx
  • components/post-kit/platforms/HackerNewsSections/HackerNewsSections.tsx
  • components/post-kit/platforms/ProductHuntSections/ProductHuntSections.tsx
  • components/post-kit/platforms/RedditSections/RedditSections.tsx
  • components/post-kit/platforms/SocialSections/SocialSections.test.tsx
  • components/post-kit/platforms/SocialSections/SocialSections.tsx
  • components/post-kit/platforms/platform-sections.types.ts
  • components/post-kit/sections/CopySection/CopySection.tsx
  • components/post-kit/sections/CopySection/CopySection.types.ts
  • components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.tsx
  • components/post-kit/sections/TopicsCommentSection/TopicsCommentSection.types.ts
  • lib/wizard/set-in.test.ts
  • lib/wizard/set-in.ts
  • lib/wizard/wizard-reducer.test.ts
  • lib/wizard/wizard-reducer.ts

Comment on lines +41 to +45
function handlePaste(e: ClipboardEvent) {
e.preventDefault()
const text = e.clipboardData.getData('text/plain')
document.execCommand('insertText', false, text)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Current browser support and deprecation status for Document.execCommand('insertText') in contentEditable elements (Chrome, Firefox, Safari, iOS Safari), and recommended modern replacement API.

💡 Result:

As of June 2026, document.execCommand('insertText') is officially deprecated and non-standard [1][2][3]. It is no longer on a standards track, and its use is discouraged by browser vendors and documentation platforms [1][4]. Browser Support Status Despite being deprecated, the method remains widely implemented across major browsers for compatibility reasons [1][5]. - Chrome, Edge, and Opera: Full support [6]. - Firefox: Supported, though with some known historical caveats regarding nested calls and input manipulation in specific older versions [6][7][2]. - Safari (including iOS Safari): Supported [6]. Because it is not standardized, browser vendors have no obligation to maintain it, and it may cease to function at any time [1][4]. Developers should treat it as an unstable feature [1]. Modern Replacement and Recommendations There is no single direct, drop-in replacement API for document.execCommand('insertText') that provides the same behavior, particularly regarding the preservation of the browser's native undo/redo history [1][4][5]. - For general text editing: Developers are encouraged to use the Selection and Range APIs to manually manipulate the DOM or, for complex editors, leverage established open-source rich-text editing libraries (e.g., CKEditor, TinyMCE, ProseMirror, Slate) which abstract away the inconsistencies of native browser editing [4][8][5]. - For advanced custom editing: The EditContext API is the modern, standardized alternative designed to give developers granular control over text input, composition, and selection, without relying on the DOM-heavy contentEditable attribute [9][10][11]. However, EditContext is primarily for building custom editing experiences rather than a direct replacement for simple text insertion [10]. - For undo/redo preservation: If you must maintain native undo/redo behavior, many developers are currently forced to continue using execCommand as a legacy fallback due to the lack of a standardized modern API that satisfies this specific requirement [12][13][5]. Some implementers build custom undo/redo stacks using MutationObserver or application-level state management, though this significantly increases implementation complexity [4][5].

Citations:


🏁 Script executed:

# First, let's examine the EditableText component to understand the context
fd -t f "EditableText.tsx" --type file

Repository: AhmedFr/launch-kit

Length of output: 110


🏁 Script executed:

# Once found, examine the file around lines 41-45
cat -n components/common/EditableText/EditableText.tsx | head -60 | tail -30

Repository: AhmedFr/launch-kit

Length of output: 1172


🏁 Script executed:

# Check if there are any tests for this component
fd -t f "EditableText" --type file | grep -i test

Repository: AhmedFr/launch-kit

Length of output: 115


🏁 Script executed:

# Check the test file to understand the expected behavior
cat -n components/common/EditableText/EditableText.test.tsx

Repository: AhmedFr/launch-kit

Length of output: 1813


🏁 Script executed:

# Get more context about the component - check the beginning and full structure
head -40 components/common/EditableText/EditableText.tsx | cat -n

Repository: AhmedFr/launch-kit

Length of output: 1766


🏁 Script executed:

# Check if there's any type information about the ref
rg -A 5 -B 5 "useRef" components/common/EditableText/EditableText.tsx

Repository: AhmedFr/launch-kit

Length of output: 857


Replace deprecated paste insertion API with a fallback-safe approach.

Line 44 uses document.execCommand('insertText', ...) after preventing default paste at Line 42. This API is officially deprecated and non-standard as of June 2026; if the browser blocks or removes support for it, pasted text will be lost. Switch to the Selection and Range APIs with execCommand as a fallback to ensure reliable paste handling.

Suggested patch
 function handlePaste(e: ClipboardEvent) {
   e.preventDefault()
   const text = e.clipboardData.getData('text/plain')
-  document.execCommand('insertText', false, text)
+  const selection = window.getSelection()
+  if (selection && selection.rangeCount > 0) {
+    const range = selection.getRangeAt(0)
+    range.deleteContents()
+    range.insertNode(document.createTextNode(text))
+    range.collapse(false)
+    selection.removeAllRanges()
+    selection.addRange(range)
+    return
+  }
+  // Fallback for engines that still support it
+  document.execCommand('insertText', false, text)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function handlePaste(e: ClipboardEvent) {
e.preventDefault()
const text = e.clipboardData.getData('text/plain')
document.execCommand('insertText', false, text)
}
function handlePaste(e: ClipboardEvent) {
e.preventDefault()
const text = e.clipboardData.getData('text/plain')
const selection = window.getSelection()
if (selection && selection.rangeCount > 0) {
const range = selection.getRangeAt(0)
range.deleteContents()
range.insertNode(document.createTextNode(text))
range.collapse(false)
selection.removeAllRanges()
selection.addRange(range)
return
}
// Fallback for engines that still support it
document.execCommand('insertText', false, text)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/common/EditableText/EditableText.tsx` around lines 41 - 45, In the
handlePaste function, replace the deprecated document.execCommand('insertText')
call with a modern approach that prioritizes the Selection and Range APIs. First
attempt to retrieve the current selection using window.getSelection(), create a
range, delete the current content, and insert the new text using the Range API.
If the selection is unavailable or the modern approach fails, fall back to
document.execCommand('insertText') as a compatibility measure to ensure reliable
paste handling across all browsers.

Comment thread lib/wizard/set-in.ts
Comment on lines +6 to +9
if (typeof key === 'number') {
const arr = Array.isArray(obj) ? obj.slice() : []
arr[key] = setIn(arr[key], rest, value)
return arr as T

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reject negative/non-integer array indices in setIn.

setIn currently accepts any numeric key; invalid indices (e.g., -1, 1.5) become non-index array properties and silently produce wrong shapes. Add an explicit guard before assignment.

Proposed fix
   if (typeof key === 'number') {
+    if (!Number.isInteger(key) || key < 0) {
+      throw new RangeError(`setIn path index must be a non-negative integer, got ${key}`)
+    }
     const arr = Array.isArray(obj) ? obj.slice() : []
     arr[key] = setIn(arr[key], rest, value)
     return arr as T
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (typeof key === 'number') {
const arr = Array.isArray(obj) ? obj.slice() : []
arr[key] = setIn(arr[key], rest, value)
return arr as T
if (typeof key === 'number') {
if (!Number.isInteger(key) || key < 0) {
throw new RangeError(`setIn path index must be a non-negative integer, got ${key}`)
}
const arr = Array.isArray(obj) ? obj.slice() : []
arr[key] = setIn(arr[key], rest, value)
return arr as T
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/wizard/set-in.ts` around lines 6 - 9, The setIn function currently
accepts any numeric key without validation, allowing negative numbers and
non-integer values like -1 or 1.5 to be set as array properties instead of
proper indices. Add a guard condition after the typeof check for 'number' in the
setIn function to validate that the key is a non-negative integer (e.g., using
Number.isInteger and checking that key >= 0). If the key fails validation, throw
an error or handle it appropriately to prevent invalid array shapes from being
created silently.

@AhmedFr
AhmedFr merged commit 5b77d1d into main Jun 21, 2026
4 checks passed
@AhmedFr
AhmedFr deleted the feat/inline-editing branch June 21, 2026 17:38

This branch was successfully deployed

1 active deployment
Preview — 607d4ed7 Deployed Jun 21, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant