rip email: a preview CLI for rip/ui/email templates - #301
Merged
Merged
Conversation
rip email dev runs the preview as a Rip Site under the edge: a transient project whose app is the package's preview page and whose entry is the package's API, managed by the Sites manager so the page gets the hub and hot reload for free. The launcher owns the one watcher on the template directory and publishes a stamp on the site's hub when a save settles; the page subscribes and refetches the catalog and the open message. Ready means the edge holds the site's registration. --host takes another name in a family that resolves here (.local, .via.rip, .localhost). rip email export writes html, and with --text the text twin, per template. A template file exports one component and is addressed by its path without the extension; underscore-led files are shared. Component.previewProps chooses the props, Component.subject and Component.from answer the header, and the Preview block's text is the inbox line. The preview page follows react-email's chrome: HTML, text, and source views, desktop and mobile viewports, and a dark mode that inverts colors the way Gmail does. The inbox row above the sheet shows the sender's name, subject, and preview line. Email components: Section and Container split padding onto the cell, Body paints its background on the body element, and toEmail answers the preview text beside html and text. bin/rip dispatches email through the sub-command runner so signals reach the launcher; rip/sites exports edge for the control socket.
The sender is the mailer's, not a template's, so the from static is gone: a template answers its subject, and the inbox row shows the subject over the preview line.
A client's default body margin is not zero, and only a template that happened to set one had it zeroed. The body element now carries margin:0 always, and padding:0 whenever the template's style sets a padding, which the cell inside carries.
Ready waited for a registration named email on the host, which another run's registration matches exactly, so a second launcher greeted the first one's site and then watched its own manager refuse the claim. It now looks for a registration whose dist root lies under its own transient project, which no other run can carry.
The chrome's tokens are Tailwind neutral steps in a stylesheet shaped like medlabs's: the palette on :root, mapped into Tailwind's text, background, and border namespaces in @theme, with the body's defaults in the base layer and Inter loaded from Google Fonts. The sidebar and header follow shadcn's sidebar, with bordered segmented controls and no colored accent. The sheet is as tall as the email, centered on a neutral canvas, 800 wide for desktop and 390 for mobile, and follows its document's height through a resize observer. The Source view indents the html for reading; what is sent stays the serializer's one line.
The Sites manager announces its host on stdout once registered, and the launcher inherited that stream, so the URL printed twice and the announcement's newline pushed Ready off the spinner's line. The launcher now pipes the manager's stdout and forwards everything but that one line.
The Sites manager now speaks as rip-sites, so the line the launcher drops from its stdout is spelled that way.
philiplindberg
force-pushed
the
email-preview
branch
from
September 11, 2026 17:02
7aa5678 to
d9b5535
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A small CLI for the rip ui/email package.
rip email dev, run from an emails directory or pointed at one (medlabs/api/emails, say), serves a live preview of every template as a Rip Site under the edge at https://email.local/, with desktop and mobile widths, a Gmail-style dark mode, and the text and source views. A template is one component per file with asubjectstatic andpreviewProps.Three changes touch rendered email itself. Body carries its background on the body element and zeroes that element's margin always, and its padding whenever the style sets one. Container and Section move any padding in their style from the table onto the cell, where Outlook and Klaviyo honor it; everything else stays on the table. A template that styles padding on Container or Section renders that padding on the td. The README's "Where a style lands" section says so, with the split shown.
How it works: the launcher writes a transient project whose app is the package's preview page and whose entry is the package's API, and hands it to the Sites manager, so the page gets the hub and hot reload for free. The launcher owns the one watcher on the template directory and publishes a stamp on the site's hub when a save settles; the page subscribes and refetches. Ready means the edge holds the site's registration.
--hosttakes another name in a family that resolves here (.local, .via.rip, .localhost).rip email exportwrites html, and with--textthe text twin, per template.bin/ripdispatchesemailthrough the sub-command runner so signals reach the launcher, andrip/sitesexportsedgefor the control socket.