feat(react-email): add prop controls to the props panel#3678
feat(react-email): add prop controls to the props panel#3678gazjones00 wants to merge 9 commits into
Conversation
Move the input and label styling duplicated across the send popover into FieldLabel/TextInput and migrate the send form onto them. Also removes a stray unlabeled checkbox left behind in the send form.
…le inference inferEmailTitle's word-splitting core becomes a reusable humanizeIdentifier so other surfaces can render code identifiers as readable labels.
…ling Add a textareaRef so callers can track focus on the underlying control, and require either an id or an aria-label at the type level so the editor always ends up with an accessible name.
…rides usePreviewPropsCommit owns how props-panel edits become a props override: single-key edits merge into one pending object so edits within a debounce window don't drop each other, raw JSON drafts are parsed after a debounce so half-typed JSON doesn't flash errors, and both paths share a single debounced apply since every applied value costs a server render.
Templates can declare per-prop controls for the preview props panel through
a static PreviewControls property, mirroring PreviewProps:
MyEmail.PreviewControls = {
username: { type: 'text', label: 'Username' },
plan: { type: 'select', options: ['hobby', 'pro', 'enterprise'] },
teamSize: { type: 'number', min: 1, max: 500 },
};
The declaration is validated with zod on the server (strict, so typos fail
loudly), and a pure resolver merges it with controls inferred from prop
values: declared controls first in declaration order, inference as the
fallback so undeclared props keep working.
The props panel gains a Controls/JSON mode switch. Controls resolve from the template's PreviewControls declaration first, then from inference: switches for booleans (via Radix Switch), bounded number inputs, selects, text fields, and a raw JSON field as the fallback for everything else. Labels humanize the prop key unless the declaration provides one. The declaration is validated when the template renders, travels to the client through the rendering metadata, and resolves against the applied props so controls stay in sync with edits; an invalid declaration warns in the terminal and falls back to inference. Edits merge into a single debounced props override, and the JSON tab remains the escape hatch for values no control can express.
|
@gazjones00 is attempting to deploy a commit to the resend Team on Vercel. A member of the Team first needs to authorize it. |
🦋 Changeset detectedLatest commit: 4372de7 The changes in this PR will be included in the next version bump. This PR includes changesets to release 3 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f75f5dcf27
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Under React 18 a plain function component drops the ref prop, leaving useFieldDraft's focus check against a null ref and letting text/number controls snap back to applied props mid-typing.
Summary
The preview props panel gains a Controls tab alongside the raw JSON editor: per-prop form controls for editing a template's preview props without hand-writing JSON.
Templates can declare how each prop should be edited through a static
PreviewControlsproperty, mirroring howPreviewPropsis already declared (inspired by Storybook's argTypes):Props without a declared control get one inferred from their value (boolean → switch, finite number → number input, string → text input, everything else → raw JSON field), so existing templates get working controls with zero configuration.
Cap.2026-07-23.at.18.15.36.mp4
How it works
text,numberwith min/max/step,boolean,select,json) lives in one module,packages/ui/src/utils/preview-controls/: a zod schema validates the declaration and a pure resolver merges it with inference.inviterName→ “Inviter Name”).Commits
The branch is structured as three preparatory refactors (shared form-field primitives, identifier humanization, JsonEditor labelling/ref API) followed by the feature: the debounced override-commit hook, the declaration schema + resolver, and the panel itself.
Testing
packages/uisuite passes (129 tests).role="switch"semantics via Radix Switch.biome checkclean;tscclean apart from pre-existing baseline errors in untouched files.