// ProposeModal.jsx — §9.1. // // Title (required) and pitch (required textarea), with a slug field // that auto-fills from the title via the same deterministic kebab-case // the backend uses. Tags are chip-input (free-form for slice 1; the // AI-suggested chips of §9.1 are deferred to Slice 2 when the AI surface // is wired up). // // The submit button drives the §17 POST /api/rfcs/propose endpoint; // success navigates the proposer to the pending-idea view per §9.3. import { useEffect, useState } from 'react' import { proposeRFC } from '../api' function slugify(title) { return title .toLowerCase() .trim() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') } export default function ProposeModal({ viewer, onClose, onSubmitted }) { const [title, setTitle] = useState('') const [slug, setSlug] = useState('') const [slugEdited, setSlugEdited] = useState(false) const [pitch, setPitch] = useState('') const [tagInput, setTagInput] = useState('') const [tags, setTags] = useState([]) const [submitting, setSubmitting] = useState(false) const [error, setError] = useState(null) useEffect(() => { if (!slugEdited) setSlug(slugify(title)) }, [title, slugEdited]) function addTag() { const t = tagInput.trim() if (t && !tags.includes(t)) setTags([...tags, t]) setTagInput('') } async function handleSubmit(e) { e.preventDefault() if (!title.trim() || !slug || !pitch.trim()) return setSubmitting(true) setError(null) try { const result = await proposeRFC({ title: title.trim(), slug, pitch: pitch.trim(), tags, }) onSubmitted?.(result) } catch (err) { setError(err.message || 'Submission failed.') } finally { setSubmitting(false) } } return (
{ if (e.target === e.currentTarget) onClose() }}>

Propose a New RFC

setTitle(e.target.value)} placeholder="e.g. Open Human Model" autoFocus required />

The word or topic this RFC will define.

{ setSlug(slugify(e.target.value)); setSlugEdited(true) }} placeholder="open-human-model" required />

Auto-derived from the title; edit if needed.