diff --git a/.github/workflows/publish-npm-package.yml b/.github/workflows/publish-npm-package.yml index c5d557c22..75576c4b6 100644 --- a/.github/workflows/publish-npm-package.yml +++ b/.github/workflows/publish-npm-package.yml @@ -8,13 +8,14 @@ on: workflow_dispatch: inputs: package: - description: "Package to publish (react-ui, react-headless, openui-cli or react-lang)" + description: "Package to publish (react-ui, react-headless, react-lang, react-email or openui-cli)" required: true type: choice options: - react-ui - react-headless - react-lang + - react-email - openui-cli jobs: diff --git a/docs/content/docs/openui-lang/examples/react-email.mdx b/docs/content/docs/openui-lang/examples/react-email.mdx new file mode 100644 index 000000000..5b8307c47 --- /dev/null +++ b/docs/content/docs/openui-lang/examples/react-email.mdx @@ -0,0 +1,121 @@ +--- +title: React Email +description: An AI-powered email generator that uses OpenUI Lang to render 44 React Email components from natural language descriptions. +--- + +OpenUI Lang isn't limited to chat interfaces — it can power any domain-specific UI generator. This example uses [React Email](https://react.email) components as the rendering target, letting users describe emails in plain English and see them rendered live. The `@openuidev/react-email` package ships 44 email components built with `defineComponent`, a ready-to-use `emailLibrary`, and a system prompt with examples and design rules — so the LLM generates well-structured, email-client-compatible HTML out of the box. + +[View source on GitHub →](https://github.com/thesysdev/openui/tree/main/examples/react-email) + +
+
+ +## Building an email library with OpenUI Lang + +Each email component wraps a React Email primitive with inline styles (required by email clients that strip ` +
+ + ); + } + + return ( +
+ +
+ + Generating... + +
+ ); +} diff --git a/examples/react-email/src/components/loadingDots.tsx b/examples/react-email/src/components/loadingDots.tsx new file mode 100644 index 000000000..ab5960d79 --- /dev/null +++ b/examples/react-email/src/components/loadingDots.tsx @@ -0,0 +1,28 @@ +"use client"; + +export function LoadingDots({ color }: { color: string }) { + return ( + <> + +
+ {[0, 1, 2].map((i) => ( +
+ ))} +
+ + ); +} diff --git a/examples/react-email/src/components/session.ts b/examples/react-email/src/components/session.ts new file mode 100644 index 000000000..826de3abe --- /dev/null +++ b/examples/react-email/src/components/session.ts @@ -0,0 +1,40 @@ +const VIEW_KEY = "email-chat-view"; +const MESSAGES_KEY = "email-chat-messages"; + +export function saveView(view: "compose" | "chat") { + try { + sessionStorage.setItem(VIEW_KEY, view); + } catch { /* ignore */ } +} + +export function loadView(): "compose" | "chat" { + try { + const v = sessionStorage.getItem(VIEW_KEY); + if (v === "chat") return "chat"; + } catch { /* ignore */ } + return "compose"; +} + +export function saveMessages(messages: unknown[]) { + try { + sessionStorage.setItem(MESSAGES_KEY, JSON.stringify(messages)); + } catch { /* ignore */ } +} + +export function loadMessages(): unknown[] | null { + try { + const raw = sessionStorage.getItem(MESSAGES_KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (Array.isArray(parsed) && parsed.length > 0) return parsed; + } + } catch { /* ignore */ } + return null; +} + +export function clearSession() { + try { + sessionStorage.removeItem(VIEW_KEY); + sessionStorage.removeItem(MESSAGES_KEY); + } catch { /* ignore */ } +} diff --git a/examples/react-email/src/generated/system-prompt.txt b/examples/react-email/src/generated/system-prompt.txt new file mode 100644 index 000000000..01539c412 --- /dev/null +++ b/examples/react-email/src/generated/system-prompt.txt @@ -0,0 +1,378 @@ +You are an AI assistant that responds using openui-lang, a declarative UI language. Your ENTIRE response must be valid openui-lang code — no markdown, no explanations, just openui-lang. + +## Syntax Rules + +1. Each statement is on its own line: `identifier = Expression` +2. `root` is the entry point — every program must define `root = EmailTemplate(...)` +3. Expressions are: strings ("..."), numbers, booleans (true/false), arrays ([...]), objects ({...}), or component calls TypeName(arg1, arg2, ...) +4. Use references for readability: define `name = ...` on one line, then use `name` later +5. EVERY variable (except root) MUST be referenced by at least one other variable. Unreferenced variables are silently dropped and will NOT render. Always include defined variables in their parent's children/items array. +6. Arguments are POSITIONAL (order matters, not names) +7. Optional arguments can be omitted from the end +8. No operators, no logic, no variables — only declarations +9. Strings use double quotes with backslash escaping + +## Component Signatures + +Arguments marked with ? are optional. Sub-components can be inline or referenced; prefer references for better streaming. +The `action` prop type accepts: ContinueConversation (sends message to LLM), OpenUrl (navigates to URL), or Custom (app-defined). + +### Email Structure +EmailTemplate(subject: string, previewText?: string, children: (EmailHeading | EmailText | EmailButton | EmailImage | EmailDivider | EmailLink | EmailCodeBlock | EmailCodeInline | EmailMarkdown | EmailArticle | EmailProductCard | EmailFeatureGrid | EmailFeatureList | EmailNumberedSteps | EmailCheckoutTable | EmailPricingCard | EmailTestimonial | EmailSurveyRating | EmailStats | EmailImageGrid | EmailAvatarGroup | EmailAvatarWithText | EmailList | EmailCustomerReview | EmailBentoGrid | EmailSection | EmailColumns | EmailColumn | EmailHeaderSideNav | EmailHeaderCenteredNav | EmailHeaderSocial | EmailFooterCentered | EmailFooterTwoColumn)[]) — Root email template. Renders a live email preview with Copy HTML export. Always provide a subject line. +EmailSection(children: (EmailHeading | EmailText | EmailButton | EmailImage | EmailDivider | EmailLink | EmailCodeBlock | EmailCodeInline | EmailMarkdown | EmailArticle | EmailProductCard | EmailFeatureGrid | EmailFeatureList | EmailNumberedSteps | EmailCheckoutTable | EmailPricingCard | EmailTestimonial | EmailSurveyRating | EmailStats | EmailImageGrid | EmailAvatarGroup | EmailAvatarWithText | EmailList | EmailCustomerReview | EmailBentoGrid)[]) — Groups email content into a section. Use to organize header, body, and footer areas. +EmailColumns(children: EmailColumn[]) — Multi-column row layout. Contains EmailColumn children. +EmailColumn(children: (EmailHeading | EmailText | EmailButton | EmailImage | EmailDivider | EmailLink | EmailCodeBlock | EmailCodeInline | EmailMarkdown | EmailArticle | EmailProductCard | EmailFeatureGrid | EmailFeatureList | EmailNumberedSteps | EmailCheckoutTable | EmailPricingCard | EmailTestimonial | EmailSurveyRating | EmailStats | EmailImageGrid | EmailAvatarGroup | EmailAvatarWithText | EmailList | EmailCustomerReview | EmailBentoGrid)[]) — A single column within an EmailColumns row. +- EmailTemplate is the root email wrapper. Always use it for email content. +- Use EmailSection to group related content (e.g. header area, body, footer). +- Use EmailColumns + EmailColumn for multi-column layouts (e.g. two features side by side). + +### Email Headers +EmailHeaderSideNav(logoSrc: string, logoAlt: string, logoHeight?: number, links: EmailNavLink[]) — Header with logo on the left and navigation links on the right. Uses EmailNavLink items. +EmailHeaderCenteredNav(logoSrc: string, logoAlt: string, logoHeight?: number, links: EmailNavLink[]) — Header with logo centered on top and navigation links centered below. Uses EmailNavLink items. +EmailHeaderSocial(logoSrc: string, logoAlt: string, logoHeight?: number, icons: EmailSocialIcon[]) — Header with logo on the left and social media icon links on the right. Uses EmailSocialIcon items. +EmailNavLink(text: string, href: string) — Navigation link item for email headers. +EmailSocialIcon(src: string, alt: string, href: string) — Social media icon link for email headers. src should be a square icon image URL. +- EmailHeaderSideNav: Logo on the left, text navigation links on the right. Use EmailNavLink for each link. +- EmailHeaderCenteredNav: Logo centered on top, text navigation links centered below. Use EmailNavLink for each link. +- EmailHeaderSocial: Logo on the left, social media icon links on the right. Use EmailSocialIcon for each icon. +- Place a header as the FIRST child of EmailTemplate for a professional branded look. +- Always follow the header with an EmailDivider to separate it from the body content. + +### Email Footers +EmailFooterCentered(logoSrc: string, logoAlt: string, companyName: string, tagline?: string, address: string, contact?: string, icons?: EmailSocialIcon[]) — Centered footer with logo, company name, tagline, social icons, address, and contact info. Uses EmailSocialIcon items. +EmailFooterTwoColumn(logoSrc: string, logoAlt: string, companyName: string, tagline?: string, address: string, contact?: string, icons?: EmailSocialIcon[]) — Two-column footer with logo and company info on the left, social icons and address on the right. Uses EmailSocialIcon items. +- EmailFooterCentered: Centered footer with logo, company name, tagline, social icons, address, and contact info. +- EmailFooterTwoColumn: Two-column footer with logo and company info on the left, social icons and address on the right. +- Both footer components accept EmailSocialIcon items for social media icons. +- Place a footer as the LAST child of EmailTemplate, after an EmailDivider. + +### Email Content +EmailHeading(text: string, level?: number) — Email heading (h1-h6). Use level 1 for main title, 2 for section headers. +EmailText(text: string) — Email text paragraph for body content. +EmailButton(label: string, href: string, backgroundColor?: string) — Email call-to-action button with link. +EmailImage(src: string, alt: string, width?: number) — Email image. Use real, publicly accessible URLs. +EmailDivider() — Horizontal divider line to separate email sections. +EmailLink(text: string, href: string) — Inline hyperlink in email content. +EmailCodeBlock(code: string, language?: string) — Syntax-highlighted code block for displaying code snippets in emails. +EmailCodeInline(code: string) — Inline code snippet for embedding code within text. +EmailMarkdown(content: string) — Renders markdown content as styled email HTML. Supports headings, bold, italic, links, lists, and code. +- EmailHeading level 1 for main title, level 2 for section headers. +- EmailButton always needs an href URL. +- EmailImage should use real, publicly accessible image URLs. +- EmailDivider takes no arguments: EmailDivider() +- EmailCodeBlock for multi-line code snippets. Optionally set language (e.g. 'javascript', 'python'). +- EmailCodeInline for inline code within text (e.g. variable names, commands). +- EmailMarkdown for rendering markdown content (headings, bold, italic, links, lists). + +### Email Articles & Products +EmailArticle(imageSrc: string, imageAlt: string, category?: string, title: string, description: string, buttonLabel: string, buttonHref: string, buttonColor?: string) — Article block with hero image, optional category label, title, description, and CTA button. Great for blog posts and newsletter articles. +EmailProductCard(imageSrc: string, imageAlt: string, category?: string, title: string, description: string, price: string, buttonLabel: string, buttonHref: string, buttonColor?: string) — Product showcase card with hero image, optional category, title, description, price, and buy button. +- EmailArticle: Hero image + category + title + description + CTA button. Great for blog posts and newsletters. +- EmailProductCard: Hero image + category + title + description + price + buy button. Great for product showcases. +- Both support an optional buttonColor prop for CTA button customization. + +### Email Features & Steps +EmailFeatureItem(iconSrc: string, iconAlt: string, title: string, description: string) — Single feature item with icon, title, and description. Used inside EmailFeatureGrid or EmailFeatureList. +EmailFeatureGrid(title: string, description: string, items: EmailFeatureItem[]) — 2x2 feature grid with header title, description, and four feature items each with icon, title, and description. +EmailFeatureList(title: string, description: string, items: EmailFeatureItem[]) — Vertical feature list with header title, description, and feature items separated by dividers. Each item has an icon, title, and description. +EmailStepItem(title: string, description: string) — Single numbered step with title and description. Used inside EmailNumberedSteps. +EmailNumberedSteps(title: string, description: string, steps: EmailStepItem[]) — Numbered steps list with header title, description, and sequential step items. Each step shows a numbered badge, title, and description. +- EmailFeatureItem: Child component with iconSrc, iconAlt, title, description. Used inside EmailFeatureGrid or EmailFeatureList. +- EmailFeatureGrid: 2x2 grid of features with header title and description. Takes EmailFeatureItem items. +- EmailFeatureList: Vertical list of features separated by dividers. Takes EmailFeatureItem items. +- EmailStepItem: Child component with title and description. Used inside EmailNumberedSteps. +- EmailNumberedSteps: Numbered steps list with auto-numbered badges. Takes EmailStepItem items. +- For icon URLs, use https://picsum.photos/seed/KEYWORD/48/48 or similar. + +### Email Commerce +EmailCheckoutItem(imageSrc?: string, imageAlt?: string, name: string, quantity?: number, price: string) — Single checkout/cart item with optional image, name, quantity, and price. Used inside EmailCheckoutTable. +EmailCheckoutTable(title?: string, items: EmailCheckoutItem[], buttonLabel: string, buttonHref: string, buttonColor?: string) — Checkout/cart table with product items (image, name, quantity, price) and a checkout button. Great for abandoned cart and order summary emails. +EmailPricingFeature(text: string) — Single pricing feature line item. Used inside EmailPricingCard. +EmailPricingCard(badge?: string, price: string, period?: string, description: string, features: EmailPricingFeature[], buttonLabel: string, buttonHref: string, buttonColor?: string, note?: string, subNote?: string) — Pricing card with badge, price, description, feature list, CTA button, and optional note. Great for upgrade and promotional emails. +- EmailCheckoutItem: Cart item with optional image, name, quantity, price. Used inside EmailCheckoutTable. +- EmailCheckoutTable: Cart table with items and checkout button. Great for abandoned cart and order summary emails. +- EmailPricingFeature: Single feature line item text. Used inside EmailPricingCard. +- EmailPricingCard: Pricing card with badge, price, period, description, features list, CTA button, and optional note. + +### Email Social Proof & Surveys +EmailTestimonial(quote: string, avatarSrc: string, avatarAlt: string, name: string, role: string) — Testimonial quote with avatar, name, and role. Centered layout for social proof in emails. +EmailSurveyRating(question: string, description?: string, buttonColor?: string) — Rating survey section with a question and 1-5 numbered buttons. Great for feedback and NPS emails. +EmailStatItem(value: string, label: string) — Single stat with a value and label. Used inside EmailStats. +EmailStats(items: EmailStatItem[]) — Horizontal stats row displaying key metrics. Each stat has a large value and a label below it. +- EmailTestimonial: Centered testimonial quote with avatar, name, and role. +- EmailSurveyRating: Rating survey with question and 1-5 numbered buttons. Great for feedback/NPS emails. +- EmailStatItem: Child component with value and label. Used inside EmailStats. +- EmailStats: Horizontal row of key metrics/stats. + +### Email Image Layouts +EmailImageGrid(title?: string, description?: string, images: EmailImage[]) — 2x2 image grid layout with optional header title and description. Pass up to 4 EmailImage items. Great for product galleries and portfolios. +- EmailImageGrid: 2x2 image grid with optional title and description. Pass up to 4 EmailImage items. +- Great for product galleries, portfolios, and visual showcases. + +### Email Avatars +EmailAvatar(src: string, alt: string, size?: number, rounded?: "full" | "md") — Avatar image component. Supports circular (rounded='full') or rounded-square (rounded='md') shapes, and configurable size. +EmailAvatarGroup(avatars: EmailAvatar[]) — Overlapping stacked avatar group. Pass EmailAvatar items to display them in a horizontal row with negative offset overlap. +EmailAvatarWithText(avatarSrc: string, avatarAlt: string, name: string, role: string, href?: string) — Avatar with name and role text beside it. Optionally wraps in a link. Great for author attribution or team member display. +- EmailAvatar: Single avatar image. Supports circular (rounded='full') or rounded-square (rounded='md') shapes, and configurable size. +- EmailAvatarGroup: Overlapping stacked avatars. Pass EmailAvatar items. Great for showing team members or participants. +- EmailAvatarWithText: Avatar with name and role text beside it. Optionally wraps in a link. Great for author attribution. + +### Email Lists +EmailListItem(title: string, description: string) — Data-only list item with title and description. Used as a child inside EmailList. +EmailList(title?: string, items: EmailListItem[]) — Numbered list with circular number badges and item title + description. Pass EmailListItem children. Great for feature lists, how-it-works sections, and top-N lists. +- EmailListItem: Data-only component with title and description. Used as a child inside EmailList. +- EmailList: Numbered list with circular badges. Pass EmailListItem children. Great for top-N lists, how-it-works, and feature lists. + +### Email Reviews +EmailCustomerReview(title?: string, totalReviews: number, rating5: number, rating4: number, rating3: number, rating2: number, rating1: number, buttonLabel?: string, buttonHref?: string, buttonColor?: string) — Customer review summary with star rating distribution bars showing percentages for each rating (1-5). Includes total review count and optional CTA button to write a review. +- EmailCustomerReview: Star rating distribution with bars and percentages. Shows total review count and optional CTA button. +- Provide rating counts for each star level (rating1 through rating5) and totalReviews. + +### Email Marketing +EmailBentoItem(imageSrc: string, imageAlt: string, title: string, description: string) — Data-only bento grid item with image, title, and description. Used as a child inside EmailBentoGrid. +EmailBentoGrid(heroTitle: string, heroDescription: string, heroLinkText?: string, heroLinkHref?: string, heroImageSrc: string, heroImageAlt: string, items: EmailBentoItem[]) — Bento grid layout with a dark hero section (title, description, link, image) on top and a row of product cards below. Pass EmailBentoItem children for the bottom row. Great for marketing and product showcase emails. +- EmailBentoItem: Data-only component with imageSrc, imageAlt, title, description. Used inside EmailBentoGrid. +- EmailBentoGrid: Bento-style layout with a dark hero section on top and product cards below. Pass EmailBentoItem children. +- Great for product showcase and marketing emails. + +## Hoisting & Streaming (CRITICAL) + +openui-lang supports hoisting: a reference can be used BEFORE it is defined. The parser resolves all references after the full input is parsed. + +During streaming, the output is re-parsed on every chunk. Undefined references are temporarily unresolved and appear once their definitions stream in. This creates a progressive top-down reveal — structure first, then data fills in. + +**Recommended statement order for optimal streaming:** +1. `root = EmailTemplate(...)` — UI shell appears immediately +2. Component definitions — fill in as they stream +3. Data values — leaf content last + +Always write the root = EmailTemplate(...) statement first so the UI shell appears immediately, even before child data has streamed in. + +## Examples + +Example 1 — Welcome email: +root = EmailTemplate("Welcome to Acme!", "You're in! Here's how to get started.", [heading, intro, btn, divider, footer]) +heading = EmailHeading("Welcome aboard!", 1) +intro = EmailText("Thanks for signing up for Acme. We're thrilled to have you on board. Here's everything you need to get started.") +btn = EmailButton("Get Started", "https://example.com/start", "#5F51E8") +divider = EmailDivider() +footer = EmailText("If you have any questions, reply to this email. We're here to help.") + +Example 2 — Newsletter with header and footer: +root = EmailTemplate("Acme Weekly", "This week: new features and tips", [header, divider1, section1, divider2, section2, divider3, footer]) +header = EmailHeaderSideNav("https://picsum.photos/seed/acme-logo/150/42", "Acme", 42, [nav1, nav2, nav3]) +nav1 = EmailNavLink("About", "https://example.com/about") +nav2 = EmailNavLink("Blog", "https://example.com/blog") +nav3 = EmailNavLink("Docs", "https://example.com/docs") +divider1 = EmailDivider() +section1 = EmailSection([s1title, s1text, s1btn]) +s1title = EmailHeading("New Feature: Dark Mode", 2) +s1text = EmailText("We just launched dark mode across all platforms. Your eyes will thank you.") +s1btn = EmailButton("Try It Now", "https://example.com/dark-mode", "#1a1a2e") +divider2 = EmailDivider() +section2 = EmailSection([s2title, s2text]) +s2title = EmailHeading("Tip of the Week", 2) +s2text = EmailText("Use keyboard shortcuts to navigate 3x faster.") +divider3 = EmailDivider() +footer = EmailFooterCentered("https://picsum.photos/seed/acme-icon/42/42", "Acme", "Acme Corporation", "Think different", "123 Main Street, Anytown, CA 12345", "hello@acme.com", [fi1, fi2, fi3]) +fi1 = EmailSocialIcon("https://react.email/static/facebook-logo.png", "Facebook", "https://facebook.com/acme") +fi2 = EmailSocialIcon("https://react.email/static/x-logo.png", "X", "https://x.com/acme") +fi3 = EmailSocialIcon("https://react.email/static/instagram-logo.png", "Instagram", "https://instagram.com/acme") + +Example 3 — Order confirmation with columns: +root = EmailTemplate("Order Confirmed #12345", "Your order has been placed!", [heading, thanks, divider1, cols, divider2, total, btn, divider3, footer]) +heading = EmailHeading("Order Confirmed", 1) +thanks = EmailText("Thank you for your purchase! Here's a summary of your order.") +divider1 = EmailDivider() +cols = EmailColumns([col1, col2]) +col1 = EmailColumn([itemTitle, itemDesc]) +col2 = EmailColumn([priceTitle, priceVal]) +itemTitle = EmailHeading("Item", 2) +itemDesc = EmailText("Premium Plan - Annual") +priceTitle = EmailHeading("Price", 2) +priceVal = EmailText("$99.00/year") +divider2 = EmailDivider() +total = EmailText("Total: $99.00") +btn = EmailButton("View Order", "https://example.com/orders/12345", "#16a34a") +divider3 = EmailDivider() +footer = EmailText("Need help? Contact us at support@example.com") + +Example 4 — Developer onboarding with code: +root = EmailTemplate("Getting Started with Acme API", "Your API key is ready", [heading, intro, section1, divider, section2, divider2, footer]) +heading = EmailHeading("Welcome to the Acme API", 1) +intro = EmailMarkdown("You're all set! Below you'll find everything you need to **get started** with our API.") +section1 = EmailSection([s1title, s1text, codeblock]) +s1title = EmailHeading("Quick Start", 2) +s1text = EmailText("Install the SDK and make your first request:") +codeblock = EmailCodeBlock("npm install @acme/sdk\n\nimport { Acme } from '@acme/sdk';\nconst client = new Acme({ apiKey: 'your-key' });\nconst res = await client.ping();", "javascript") +divider = EmailDivider() +section2 = EmailSection([s2title, s2text]) +s2title = EmailHeading("Need Help?", 2) +s2text = EmailMarkdown("Check our [documentation](https://docs.example.com) or reply to this email.") +divider2 = EmailDivider() +footer = EmailText("Happy coding!") + +Example 5 — Password reset email: +root = EmailTemplate("Reset your password", "Someone requested a password change", [logo, heading, text1, btn, text2, divider, linkText, divider2, footer]) +logo = EmailImage("https://picsum.photos/seed/app-logo/100/30", "App", 100) +heading = EmailHeading("Reset your password", 1) +text1 = EmailText("Someone recently requested a password change for your account. If this was you, click below:") +btn = EmailButton("Reset Password", "https://example.com/reset?token=abc123", "#0061fe") +text2 = EmailText("Or copy and paste this link:") +divider = EmailDivider() +linkText = EmailLink("https://example.com/reset?token=abc123", "https://example.com/reset?token=abc123") +divider2 = EmailDivider() +footer = EmailText("If you didn't request this, just ignore this email. This link expires in 24 hours.") + +Example 6 — Promotional sale email: +root = EmailTemplate("Summer Sale — Up to 50% Off!", "Don't miss our biggest sale", [hero, heading, subhead, divider1, featCols, divider2, ctaBtn, divider3, footer]) +hero = EmailImage("https://picsum.photos/seed/summer-sale/600/250", "Summer Sale Banner", 600) +heading = EmailHeading("Summer Sale is Here!", 1) +subhead = EmailText("For a limited time, enjoy up to 50% off on select items.") +divider1 = EmailDivider() +featCols = EmailColumns([deal1Col, deal2Col, deal3Col]) +deal1Col = EmailColumn([deal1Img, deal1Name, deal1Price]) +deal2Col = EmailColumn([deal2Img, deal2Name, deal2Price]) +deal3Col = EmailColumn([deal3Img, deal3Name, deal3Price]) +deal1Img = EmailImage("https://picsum.photos/seed/product1/180/180", "Sunglasses", 180) +deal1Name = EmailText("Designer Sunglasses") +deal1Price = EmailText("$49.99 (was $99.99)") +deal2Img = EmailImage("https://picsum.photos/seed/product2/180/180", "Beach Bag", 180) +deal2Name = EmailText("Canvas Beach Bag") +deal2Price = EmailText("$29.99 (was $59.99)") +deal3Img = EmailImage("https://picsum.photos/seed/product3/180/180", "Sandals", 180) +deal3Name = EmailText("Leather Sandals") +deal3Price = EmailText("$39.99 (was $79.99)") +divider2 = EmailDivider() +ctaBtn = EmailButton("Shop the Sale", "https://example.com/summer-sale", "#e11d48") +divider3 = EmailDivider() +footer = EmailText("StyleShop · 500 Fashion Ave · New York, NY 10018") + +Example 7 — Feature showcase with grid and steps: +root = EmailTemplate("Welcome to Acme!", "Discover what you can do", [heading, intro, divider1, featureGrid, divider2, steps, divider3, footer]) +heading = EmailHeading("Welcome to Acme!", 1) +intro = EmailText("Here's what you can do with your new account:") +divider1 = EmailDivider() +featureGrid = EmailFeatureGrid("Key Features", "Powerful features to help you succeed.", [feat1, feat2, feat3, feat4]) +feat1 = EmailFeatureItem("https://picsum.photos/seed/heart/48/48", "Heart", "Easy to Use", "Get started in minutes.") +feat2 = EmailFeatureItem("https://picsum.photos/seed/rocket/48/48", "Rocket", "Lightning Fast", "Blazing fast performance.") +feat3 = EmailFeatureItem("https://picsum.photos/seed/shield/48/48", "Shield", "Secure", "Enterprise-grade security.") +feat4 = EmailFeatureItem("https://picsum.photos/seed/chart/48/48", "Chart", "Analytics", "Deep data insights.") +divider2 = EmailDivider() +steps = EmailNumberedSteps("Getting Started", "Follow these steps:", [step1, step2, step3]) +step1 = EmailStepItem("Create Profile", "Set up your profile with a photo and bio.") +step2 = EmailStepItem("Connect Tools", "Integrate with Slack, GitHub, and Jira.") +step3 = EmailStepItem("Invite Team", "Add team members and start collaborating.") +divider3 = EmailDivider() +footer = EmailText("Acme, Inc. · San Francisco, CA 94107") + +Example 8 — Abandoned cart email: +root = EmailTemplate("You left something behind!", "Complete your purchase", [heading, text, checkout, divider, footer]) +heading = EmailHeading("Don't forget your items!", 1) +text = EmailText("Complete your purchase before these items sell out.") +checkout = EmailCheckoutTable("Your Cart", [item1, item2], "Complete Purchase", "https://example.com/checkout", "#4F46E5") +item1 = EmailCheckoutItem("https://picsum.photos/seed/watch/100/100", "Classic Watch", "Classic Watch", 1, "$210.00") +item2 = EmailCheckoutItem("https://picsum.photos/seed/clock/100/100", "Wall Clock", "Analogue Clock", 2, "$40.00") +divider = EmailDivider() +footer = EmailText("Acme Store · 123 Commerce Way · San Francisco, CA 94107") + +Example 9 — Pricing with testimonial and survey: +root = EmailTemplate("Upgrade to Pro", "Unlock premium features", [heading, text, divider1, pricing, divider2, testimonial, divider3, survey, divider4, footer]) +heading = EmailHeading("Upgrade Your Plan", 1) +text = EmailText("You've been on the free plan for 30 days. Unlock everything with Pro.") +divider1 = EmailDivider() +pricing = EmailPricingCard("Pro", "$12", "/ month", "Everything you need to grow.", [pf1, pf2, pf3], "Upgrade Now", "https://example.com/upgrade", "#4F46E5") +pf1 = EmailPricingFeature("Unlimited projects") +pf2 = EmailPricingFeature("Advanced analytics") +pf3 = EmailPricingFeature("Priority support") +divider2 = EmailDivider() +testimonial = EmailTestimonial("Acme Pro transformed our workflow. Can't imagine going back.", "https://picsum.photos/seed/ceo/100/100", "Jane Smith", "Jane Smith", "CEO, TechCorp") +divider3 = EmailDivider() +survey = EmailSurveyRating("How would you rate your experience?", "Your feedback helps us improve.", "#4F46E5") +divider4 = EmailDivider() +footer = EmailText("Acme, Inc. · San Francisco, CA 94107") + +Example 10 — Stats, gallery, avatars, list, and bento grid: +root = EmailTemplate("Your Monthly Report", "Key metrics and highlights", [header, divider1, stats, divider2, gallery, divider3, team, divider4, list, divider5, bento, divider6, reviews, divider7, footer]) +header = EmailHeaderCenteredNav("https://picsum.photos/seed/acme/150/42", "Acme", 42, [nav1, nav2]) +nav1 = EmailNavLink("Dashboard", "https://example.com/dashboard") +nav2 = EmailNavLink("Reports", "https://example.com/reports") +divider1 = EmailDivider() +stats = EmailStats([stat1, stat2, stat3]) +stat1 = EmailStatItem("12,847", "Users") +stat2 = EmailStatItem("94.2%", "Uptime") +stat3 = EmailStatItem("$48.5K", "Revenue") +divider2 = EmailDivider() +gallery = EmailImageGrid("New Products", "Our latest arrivals.", [img1, img2, img3, img4]) +img1 = EmailImage("https://picsum.photos/seed/prod-a/300/288", "Product A") +img2 = EmailImage("https://picsum.photos/seed/prod-b/300/288", "Product B") +img3 = EmailImage("https://picsum.photos/seed/prod-c/300/288", "Product C") +img4 = EmailImage("https://picsum.photos/seed/prod-d/300/288", "Product D") +divider3 = EmailDivider() +team = EmailAvatarGroup([av1, av2, av3]) +av1 = EmailAvatar("https://picsum.photos/seed/person1/100/100", "Alice", 44) +av2 = EmailAvatar("https://picsum.photos/seed/person2/100/100", "Bob", 44) +av3 = EmailAvatar("https://picsum.photos/seed/person3/100/100", "Carol", 44) +divider4 = EmailDivider() +list = EmailList("Top 3 Updates", [li1, li2, li3]) +li1 = EmailListItem("New Dashboard", "Redesigned analytics dashboard with real-time data.") +li2 = EmailListItem("Mobile App", "Now available on iOS and Android.") +li3 = EmailListItem("API v2", "Faster, more reliable API with new endpoints.") +divider5 = EmailDivider() +bento = EmailBentoGrid("Featured Collection", "Handpicked products for you.", "Shop now", "https://example.com/shop", "https://picsum.photos/seed/hero/400/250", "Collection", [bi1, bi2]) +bi1 = EmailBentoItem("https://picsum.photos/seed/item-x/300/200", "Item X", "Premium Widget", "High-quality craftsmanship.") +bi2 = EmailBentoItem("https://picsum.photos/seed/item-y/300/200", "Item Y", "Deluxe Gadget", "Next-gen technology.") +divider6 = EmailDivider() +reviews = EmailCustomerReview("Product Ratings", 500, 300, 100, 50, 30, 20, "Write a Review", "https://example.com/review", "#4F46E5") +divider7 = EmailDivider() +footer = EmailFooterTwoColumn("https://picsum.photos/seed/acme-icon/42/42", "Acme", "Acme Corp", "Innovation first", "123 Main St, CA 12345", "hello@acme.com", [si1, si2]) +si1 = EmailSocialIcon("https://react.email/static/x-logo.png", "X", "https://x.com/acme") +si2 = EmailSocialIcon("https://react.email/static/instagram-logo.png", "Instagram", "https://instagram.com/acme") + +## Important Rules +- ALWAYS start with root = EmailTemplate(...) +- Write statements in TOP-DOWN order: root → components → data (leverages hoisting for progressive streaming) +- Each statement on its own line +- No trailing text or explanations — output ONLY openui-lang code +- When asked about data, generate realistic/plausible data +- Choose components that best represent the content (tables for comparisons, charts for trends, forms for input, etc.) +- NEVER define a variable without referencing it from the tree. Every variable must be reachable from root, otherwise it will not render. + +- You are an expert email designer using react-email components. +- The 10 supported email types are: Welcome/Onboarding, Newsletter, Order Confirmation, Password Reset, Promotional/Sale, Event Invitation, Feedback Request, Shipping/Delivery Update, Account Verification, Onboarding Tutorial. +- Use realistic, professional placeholder text — never use lorem ipsum. +- Always provide both subject and previewText for EmailTemplate. +- Use EmailSection to group related content areas (header, body, footer sections). +- Use EmailDivider between major sections for visual separation. +- For multi-column layouts (features, pricing comparisons), use EmailColumns with EmailColumn children. +- Keep email designs clean and focused — avoid too many colors or fonts. +- Use EmailButton with descriptive labels and realistic href URLs. +- For images, use publicly accessible URLs like https://picsum.photos/seed/KEYWORD/600/300. +- When the user asks to modify an existing email, regenerate the full EmailTemplate with the requested changes applied. +- Use EmailCodeBlock for multi-line code (API examples, install commands). Set the language prop for syntax context. +- Use EmailCodeInline for short inline code references within EmailText (e.g. variable names, CLI commands). +- Use EmailMarkdown when the content includes rich formatting like bold, italic, links, or lists — it's more flexible than plain EmailText. +- Use EmailHeaderSideNav for a professional header with logo left and nav links right. +- Use EmailHeaderCenteredNav for a centered brand header with logo on top and nav links below. +- Use EmailHeaderSocial for a header with logo left and social media icons right. +- Place the header as the FIRST child of EmailTemplate, followed by an EmailDivider. +- Use EmailFooterCentered for a centered footer with logo, company name, social icons, and contact info. +- Use EmailFooterTwoColumn for a two-column footer with logo and info on the left, social icons and address on the right. +- Place the footer as the LAST child of EmailTemplate, after an EmailDivider. +- Use EmailArticle for blog post or newsletter article blocks with hero image, category, title, description, and CTA. +- Use EmailProductCard for product showcases with image, title, description, price, and buy button. +- Use EmailFeatureGrid for a 2x2 grid of features with icons. Provide exactly 4 EmailFeatureItem children. +- Use EmailFeatureList for a vertical list of features with icons and dividers. Any number of EmailFeatureItem children. +- Use EmailNumberedSteps for step-by-step guides. Steps are auto-numbered. Provide EmailStepItem children. +- Use EmailCheckoutTable for cart/order summary tables. Provide EmailCheckoutItem children with product details. +- Use EmailPricingCard for pricing plans with feature lists. Provide EmailPricingFeature children for each feature line. +- Use EmailTestimonial for customer quotes with avatar, name, and role. +- Use EmailSurveyRating for feedback/NPS emails with a 1-5 rating scale. +- Use EmailStats for displaying key metrics. Provide EmailStatItem children with value and label. +- Use EmailImageGrid for a 2x2 image gallery. Provide up to 4 EmailImage children. +- Use EmailAvatar for a single avatar image. Set rounded='full' for circular or rounded='md' for rounded-square. Default size is 42px. +- Use EmailAvatarGroup for overlapping stacked avatars. Provide EmailAvatar children. Great for showing team members or participants. +- Use EmailAvatarWithText for an avatar with name and role text. Optionally wrap in a link with href. Great for author attribution in articles. +- Use EmailList for numbered lists with circular badges. Provide EmailListItem children with title and description. +- Use EmailCustomerReview for a star rating distribution summary. Provide counts for each rating level (rating1-rating5) and totalReviews. +- Use EmailBentoGrid for a bento-style marketing layout. Provide a dark hero section and EmailBentoItem children for product cards below. \ No newline at end of file diff --git a/examples/react-email/src/hooks/useAutoScroll.ts b/examples/react-email/src/hooks/useAutoScroll.ts new file mode 100644 index 000000000..bf4b1da28 --- /dev/null +++ b/examples/react-email/src/hooks/useAutoScroll.ts @@ -0,0 +1,57 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +export function useAutoScroll(deps: unknown[], isRunning: boolean) { + const ref = useRef(null); + const userScrolledRef = useRef(false); + const isUserScrollingRef = useRef(false); + + useEffect(() => { + const el = ref.current; + if (!el) return; + + const onUserStart = () => { + isUserScrollingRef.current = true; + }; + const onUserEnd = () => { + isUserScrollingRef.current = false; + }; + const onScroll = () => { + if (isUserScrollingRef.current) { + userScrolledRef.current = true; + } + }; + + el.addEventListener("mousedown", onUserStart); + el.addEventListener("touchstart", onUserStart); + el.addEventListener("wheel", onUserStart); + el.addEventListener("click", onUserEnd); + el.addEventListener("scroll", onScroll); + + return () => { + el.removeEventListener("mousedown", onUserStart); + el.removeEventListener("touchstart", onUserStart); + el.removeEventListener("wheel", onUserStart); + el.removeEventListener("click", onUserEnd); + el.removeEventListener("scroll", onScroll); + }; + }, []); + + // Auto-scroll to bottom when content changes + // eslint-disable-next-line react-hooks/exhaustive-deps + useEffect(() => { + if (ref.current && !userScrolledRef.current) { + ref.current.scrollTop = ref.current.scrollHeight; + } + }, deps); + + // Reset scroll lock when a new generation starts + useEffect(() => { + if (isRunning) { + userScrolledRef.current = false; + } + }, [isRunning]); + + return ref; +} diff --git a/examples/react-email/src/hooks/useClipboard.ts b/examples/react-email/src/hooks/useClipboard.ts new file mode 100644 index 000000000..28d443867 --- /dev/null +++ b/examples/react-email/src/hooks/useClipboard.ts @@ -0,0 +1,19 @@ +"use client"; + +import { useCallback, useState } from "react"; + +export function useClipboard(timeout = 2000) { + const [copied, setCopied] = useState(false); + + const copy = useCallback( + (text: string) => { + navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), timeout); + }); + }, + [timeout], + ); + + return { copied, copy }; +} diff --git a/examples/react-email/src/hooks/useEmailRendering.tsx b/examples/react-email/src/hooks/useEmailRendering.tsx new file mode 100644 index 000000000..ea5c9a478 --- /dev/null +++ b/examples/react-email/src/hooks/useEmailRendering.tsx @@ -0,0 +1,104 @@ +"use client"; + +import { emailLibrary } from "@openuidev/react-email"; +import type { ParseResult } from "@openuidev/react-lang"; +import { Renderer } from "@openuidev/react-lang"; +import { render } from "@react-email/render"; +import { useCallback, useEffect, useRef, useState } from "react"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function findTemplate(node: any): any { + if (node?.typeName === "EmailTemplate") return node; + const children = node?.props?.children; + if (Array.isArray(children)) { + for (const child of children) { + const found = findTemplate(child); + if (found) return found; + } + } + return null; +} + +function renderToHtml(openuiCode: string) { + return render(, { + pretty: true, + }); +} + +export function useEmailRendering(openuiCode: string | null, isStreaming: boolean, isRunning: boolean) { + const [renderedHtml, setRenderedHtml] = useState(null); + const [htmlLoading, setHtmlLoading] = useState(false); + const [emailSubject, setEmailSubject] = useState(null); + const lastParsedRef = useRef(null); + const renderingRef = useRef(false); + const wasStreamingRef = useRef(false); + const wasRunningRef = useRef(false); + + const triggerRender = useCallback( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (template: any, code: string) => { + renderingRef.current = true; + setHtmlLoading(true); + setEmailSubject(String(template.props?.subject ?? "")); + + renderToHtml(code) + .then((html) => setRenderedHtml(html)) + .catch(() => setRenderedHtml(null)) + .finally(() => { + setHtmlLoading(false); + renderingRef.current = false; + }); + }, + [], + ); + + const handleParseResult = useCallback( + (result: ParseResult | null) => { + lastParsedRef.current = result; + + // On refresh (not streaming, no HTML yet), render as soon as parse completes + if (result?.root && openuiCode && !isStreaming && !renderingRef.current) { + const template = findTemplate(result.root); + if (template && !renderedHtml) { + requestAnimationFrame(() => triggerRender(template, openuiCode)); + } + } + }, + [openuiCode, isStreaming, renderedHtml, triggerRender], + ); + + const onStreamingEnd = useCallback(() => { + if (renderingRef.current || !openuiCode) return; + const result = lastParsedRef.current; + if (!result?.root) return; + + const template = findTemplate(result.root); + if (!template) return; + + triggerRender(template, openuiCode); + }, [openuiCode, triggerRender]); + + const onGenerationStart = useCallback(() => { + setRenderedHtml(null); + setEmailSubject(null); + lastParsedRef.current = null; + renderingRef.current = false; + }, []); + + // Detect streaming/running transitions + useEffect(() => { + const id = requestAnimationFrame(() => { + if (wasStreamingRef.current && !isStreaming) { + onStreamingEnd(); + } + if (!wasRunningRef.current && isRunning) { + onGenerationStart(); + } + wasStreamingRef.current = isStreaming; + wasRunningRef.current = isRunning; + }); + return () => cancelAnimationFrame(id); + }, [isStreaming, isRunning, openuiCode, onStreamingEnd, onGenerationStart]); + + return { renderedHtml, htmlLoading, emailSubject, handleParseResult }; +} diff --git a/examples/react-email/src/hooks/useIsMobile.ts b/examples/react-email/src/hooks/useIsMobile.ts new file mode 100644 index 000000000..f0fcdf868 --- /dev/null +++ b/examples/react-email/src/hooks/useIsMobile.ts @@ -0,0 +1,14 @@ +"use client"; + +import { useEffect, useState } from "react"; + +export function useIsMobile(breakpoint = 768) { + const [isMobile, setIsMobile] = useState(false); + useEffect(() => { + const check = () => setIsMobile(window.innerWidth < breakpoint); + check(); + window.addEventListener("resize", check); + return () => window.removeEventListener("resize", check); + }, [breakpoint]); + return isMobile; +} diff --git a/examples/react-email/tsconfig.json b/examples/react-email/tsconfig.json new file mode 100644 index 000000000..cf9c65d3e --- /dev/null +++ b/examples/react-email/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "incremental": true, + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": ["./src/*"] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + ".next/dev/types/**/*.ts", + "**/*.mts" + ], + "exclude": ["node_modules"] +} diff --git a/packages/react-email/package.json b/packages/react-email/package.json new file mode 100644 index 000000000..13d5a8725 --- /dev/null +++ b/packages/react-email/package.json @@ -0,0 +1,40 @@ +{ + "name": "@openuidev/react-email", + "version": "0.1.0", + "description": "React Email components for OpenUI \u2014 44 email building blocks with defineComponent", + "type": "module", + "main": "src/index.ts", + "types": "src/index.ts", + "exports": { + ".": { + "types": "./src/index.ts", + "default": "./src/index.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc -p tsconfig.build.json", + "dev": "tsc -p tsconfig.build.json --watch", + "lint": "eslint", + "lint:check": "eslint ./src", + "lint:fix": "eslint ./src --fix", + "format:fix": "prettier --write ./src", + "format:check": "prettier --check ./src", + "ci": "pnpm run lint:check && pnpm run format:check" + }, + "dependencies": { + "@react-email/components": "^0.0.41", + "zod": "^4.3.6" + }, + "peerDependencies": { + "@openuidev/react-lang": "workspace:*", + "react": ">=19.0.0", + "react-dom": ">=19.0.0" + }, + "devDependencies": { + "@types/react": "^19", + "typescript": "^5" + } +} diff --git a/packages/react-email/src/components/Article.tsx b/packages/react-email/src/components/Article.tsx new file mode 100644 index 000000000..3a83e9806 --- /dev/null +++ b/packages/react-email/src/components/Article.tsx @@ -0,0 +1,82 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Button, Heading, Img, Section, Text } from "@react-email/components"; +import { z } from "zod"; + +export const EmailArticle = defineComponent({ + name: "EmailArticle", + props: z.object({ + imageSrc: z.string(), + imageAlt: z.string(), + category: z.string().optional(), + title: z.string(), + description: z.string(), + buttonLabel: z.string(), + buttonHref: z.string(), + buttonColor: z.string().optional(), + }), + description: + "Article block with hero image, optional category label, title, description, and CTA button. Great for blog posts and newsletter articles.", + component: ({ props }) => { + const bg = (props.buttonColor as string) ?? "#4F46E5"; + return ( +
+ {props.imageAlt +
+ {props.category && ( + + {props.category as string} + + )} + + {props.title as string} + + + {props.description as string} + + +
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/Avatar.tsx b/packages/react-email/src/components/Avatar.tsx new file mode 100644 index 000000000..bbbd93496 --- /dev/null +++ b/packages/react-email/src/components/Avatar.tsx @@ -0,0 +1,48 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Img } from "@react-email/components"; +import { z } from "zod"; + +export const EmailAvatar = defineComponent({ + name: "EmailAvatar", + props: z.object({ + src: z.string(), + alt: z.string(), + size: z.number().optional(), + rounded: z.enum(["full", "md"]).optional(), + }), + description: + "Avatar image component. Supports circular (rounded='full') or rounded-square (rounded='md') shapes, and configurable size.", + component: ({ props }) => { + const size = (props.size as number) ?? 42; + const rounded = (props.rounded as string) ?? "full"; + const borderRadius = rounded === "full" ? "9999px" : "8px"; + + return ( +
+ {props.alt +
+ ); + }, +}); diff --git a/packages/react-email/src/components/AvatarGroup.tsx b/packages/react-email/src/components/AvatarGroup.tsx new file mode 100644 index 000000000..0ced068d0 --- /dev/null +++ b/packages/react-email/src/components/AvatarGroup.tsx @@ -0,0 +1,77 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Row } from "@react-email/components"; +import { z } from "zod"; +import { EmailAvatar } from "./Avatar"; + +export const EmailAvatarGroup = defineComponent({ + name: "EmailAvatarGroup", + props: z.object({ + avatars: z.array(EmailAvatar.ref), + }), + description: + "Overlapping stacked avatar group. Pass EmailAvatar items to display them in a horizontal row with negative offset overlap.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const avatars = (props.avatars ?? []) as any[]; + + return ( + + {avatars.map((avatar, i) => { + const src = String(avatar?.props?.src ?? ""); + const alt = String(avatar?.props?.alt ?? ""); + const size = Number(avatar?.props?.size ?? 44); + + return ( + 0 ? -(i * 12) : 0, + }} + > +
+ {alt} +
+
+ ); + })} +
+ ); + }, +}); diff --git a/packages/react-email/src/components/AvatarWithText.tsx b/packages/react-email/src/components/AvatarWithText.tsx new file mode 100644 index 000000000..914ea17ac --- /dev/null +++ b/packages/react-email/src/components/AvatarWithText.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Link, Row } from "@react-email/components"; +import { z } from "zod"; + +export const EmailAvatarWithText = defineComponent({ + name: "EmailAvatarWithText", + props: z.object({ + avatarSrc: z.string(), + avatarAlt: z.string(), + name: z.string(), + role: z.string(), + href: z.string().optional(), + }), + description: + "Avatar with name and role text beside it. Optionally wraps in a link. Great for author attribution or team member display.", + component: ({ props }) => { + const content = ( + + + {props.avatarAlt + + +

+ {props.name as string} +

+

+ {props.role as string} +

+
+
+ ); + + if (props.href) { + return ( + + + + {content} + + + + ); + } + + return ( + + {content} + + ); + }, +}); diff --git a/packages/react-email/src/components/BentoGrid.tsx b/packages/react-email/src/components/BentoGrid.tsx new file mode 100644 index 000000000..4cf36ce0f --- /dev/null +++ b/packages/react-email/src/components/BentoGrid.tsx @@ -0,0 +1,157 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Heading, Img, Link, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailBentoItem } from "./BentoItem"; + +export const EmailBentoGrid = defineComponent({ + name: "EmailBentoGrid", + props: z.object({ + heroTitle: z.string(), + heroDescription: z.string(), + heroLinkText: z.string().optional(), + heroLinkHref: z.string().optional(), + heroImageSrc: z.string(), + heroImageAlt: z.string(), + items: z.array(EmailBentoItem.ref), + }), + description: + "Bento grid layout with a dark hero section (title, description, link, image) on top and a row of product cards below. Pass EmailBentoItem children for the bottom row. Great for marketing and product showcase emails.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = (props.items ?? []) as any[]; + + return ( +
+ {/* Hero section */} +
+ + + + {props.heroTitle as string} + + + {props.heroDescription as string} + + {props.heroLinkText && props.heroLinkHref && ( + + {props.heroLinkText as string} → + + )} + + + {props.heroImageAlt + + +
+ + {/* Items row */} + {items.length > 0 && ( +
+ + {items.map((item, i) => { + const imageSrc = String(item?.props?.imageSrc ?? ""); + const imageAlt = String(item?.props?.imageAlt ?? ""); + const title = String(item?.props?.title ?? ""); + const description = String(item?.props?.description ?? ""); + + return ( + + {imageAlt} + + {title} + + + {description} + + + ); + })} + +
+ )} +
+ ); + }, +}); diff --git a/packages/react-email/src/components/BentoItem.tsx b/packages/react-email/src/components/BentoItem.tsx new file mode 100644 index 000000000..ad05ce4b0 --- /dev/null +++ b/packages/react-email/src/components/BentoItem.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailBentoItem = defineComponent({ + name: "EmailBentoItem", + props: z.object({ + imageSrc: z.string(), + imageAlt: z.string(), + title: z.string(), + description: z.string(), + }), + description: + "Data-only bento grid item with image, title, and description. Used as a child inside EmailBentoGrid.", + component: () => null, +}); diff --git a/packages/react-email/src/components/Button.tsx b/packages/react-email/src/components/Button.tsx new file mode 100644 index 000000000..87a1041d1 --- /dev/null +++ b/packages/react-email/src/components/Button.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Button } from "@react-email/components"; +import { z } from "zod"; + +export const EmailButton = defineComponent({ + name: "EmailButton", + props: z.object({ + label: z.string(), + href: z.string(), + backgroundColor: z.string().optional(), + }), + description: "Email call-to-action button with link.", + component: ({ props }) => ( + + ), +}); diff --git a/packages/react-email/src/components/CheckoutItem.tsx b/packages/react-email/src/components/CheckoutItem.tsx new file mode 100644 index 000000000..5b569a301 --- /dev/null +++ b/packages/react-email/src/components/CheckoutItem.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailCheckoutItem = defineComponent({ + name: "EmailCheckoutItem", + props: z.object({ + imageSrc: z.string().optional(), + imageAlt: z.string().optional(), + name: z.string(), + quantity: z.number().optional(), + price: z.string(), + }), + description: + "Single checkout/cart item with optional image, name, quantity, and price. Used inside EmailCheckoutTable.", + component: () => null, +}); diff --git a/packages/react-email/src/components/CheckoutTable.tsx b/packages/react-email/src/components/CheckoutTable.tsx new file mode 100644 index 000000000..4423c314a --- /dev/null +++ b/packages/react-email/src/components/CheckoutTable.tsx @@ -0,0 +1,202 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Button, Column, Heading, Img, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailCheckoutItem } from "./CheckoutItem"; + +export const EmailCheckoutTable = defineComponent({ + name: "EmailCheckoutTable", + props: z.object({ + title: z.string().optional(), + items: z.array(EmailCheckoutItem.ref), + buttonLabel: z.string(), + buttonHref: z.string(), + buttonColor: z.string().optional(), + }), + description: + "Checkout/cart table with product items (image, name, quantity, price) and a checkout button. Great for abandoned cart and order summary emails.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = (props.items ?? []) as any[]; + const bg = (props.buttonColor as string) ?? "#4F46E5"; + const title = (props.title as string) ?? "You left something in your cart"; + + return ( +
+ + {title} + +
+ + + + + + + + + + + {items.map((item, i) => ( + + + + + + + ))} + +
+   + + Product + + Qty + + Price +
+ {item?.props?.imageSrc ? ( + {String(item?.props?.imageAlt + ) : null} + + {String(item?.props?.name ?? "")} + + {String(item?.props?.quantity ?? 1)} + + {String(item?.props?.price ?? "")} +
+ + + + + +
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/CodeBlock.tsx b/packages/react-email/src/components/CodeBlock.tsx new file mode 100644 index 000000000..eb8c6992f --- /dev/null +++ b/packages/react-email/src/components/CodeBlock.tsx @@ -0,0 +1,35 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { CodeBlock } from "@react-email/components"; +import { z } from "zod"; + +type PrismLanguage = Parameters[0]["language"]; + +export const EmailCodeBlock = defineComponent({ + name: "EmailCodeBlock", + props: z.object({ + code: z.string(), + language: z.string().optional(), + }), + description: "Syntax-highlighted code block for displaying code snippets in emails.", + component: ({ props }) => ( + + ), +}); diff --git a/packages/react-email/src/components/CodeInline.tsx b/packages/react-email/src/components/CodeInline.tsx new file mode 100644 index 000000000..767bfec3a --- /dev/null +++ b/packages/react-email/src/components/CodeInline.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { CodeInline } from "@react-email/components"; +import { z } from "zod"; + +export const EmailCodeInline = defineComponent({ + name: "EmailCodeInline", + props: z.object({ + code: z.string(), + }), + description: "Inline code snippet for embedding code within text.", + component: ({ props }) => ( + + {props.code as string} + + ), +}); diff --git a/packages/react-email/src/components/Column.tsx b/packages/react-email/src/components/Column.tsx new file mode 100644 index 000000000..9c6f23011 --- /dev/null +++ b/packages/react-email/src/components/Column.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column } from "@react-email/components"; +import { z } from "zod"; +import { EmailLeafChildUnion } from "../unions"; + +export const EmailColumn = defineComponent({ + name: "EmailColumn", + props: z.object({ + children: z.array(EmailLeafChildUnion), + }), + description: "A single column within an EmailColumns row.", + component: ({ props, renderNode }) => ( + {renderNode(props.children)} + ), +}); diff --git a/packages/react-email/src/components/Columns.tsx b/packages/react-email/src/components/Columns.tsx new file mode 100644 index 000000000..22c0684a5 --- /dev/null +++ b/packages/react-email/src/components/Columns.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Row } from "@react-email/components"; +import { z } from "zod"; +import { EmailColumn } from "./Column"; + +export const EmailColumns = defineComponent({ + name: "EmailColumns", + props: z.object({ + children: z.array(EmailColumn.ref), + }), + description: "Multi-column row layout. Contains EmailColumn children.", + component: ({ props, renderNode }) => ( + {renderNode(props.children)} + ), +}); diff --git a/packages/react-email/src/components/CustomerReview.tsx b/packages/react-email/src/components/CustomerReview.tsx new file mode 100644 index 000000000..dd733eed6 --- /dev/null +++ b/packages/react-email/src/components/CustomerReview.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Button, Column, Heading, Hr, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; + +export const EmailCustomerReview = defineComponent({ + name: "EmailCustomerReview", + props: z.object({ + title: z.string().optional(), + totalReviews: z.number(), + rating5: z.number(), + rating4: z.number(), + rating3: z.number(), + rating2: z.number(), + rating1: z.number(), + buttonLabel: z.string().optional(), + buttonHref: z.string().optional(), + buttonColor: z.string().optional(), + }), + description: + "Customer review summary with star rating distribution bars showing percentages for each rating (1-5). Includes total review count and optional CTA button to write a review.", + component: ({ props }) => { + const title = (props.title as string) ?? "Customer Reviews"; + const totalReviews = props.totalReviews as number; + const ratings = [ + { rating: 5, count: props.rating5 as number }, + { rating: 4, count: props.rating4 as number }, + { rating: 3, count: props.rating3 as number }, + { rating: 2, count: props.rating2 as number }, + { rating: 1, count: props.rating1 as number }, + ]; + const buttonLabel = (props.buttonLabel as string) ?? "Write a review"; + const buttonHref = (props.buttonHref as string) ?? "#"; + const buttonColor = (props.buttonColor as string) ?? "#4F46E5"; + + return ( +
+ + {title} + +
+ {ratings.map(({ rating, count }) => { + const pct = totalReviews > 0 ? Math.round((count / totalReviews) * 100) : 0; + const filledPct = + totalReviews > 0 ? `${Math.round((count / totalReviews) * 100)}%` : "0%"; + + return ( + + + {rating} + + +
+
+
+ + + {pct}% + + + ); + })} + + Based on {totalReviews} Reviews + +
+
+
+ + Share your thoughts + + + If you've used this product, share your thoughts with other customers + + +
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/Divider.tsx b/packages/react-email/src/components/Divider.tsx new file mode 100644 index 000000000..19362bbfb --- /dev/null +++ b/packages/react-email/src/components/Divider.tsx @@ -0,0 +1,12 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Hr } from "@react-email/components"; +import { z } from "zod"; + +export const EmailDivider = defineComponent({ + name: "EmailDivider", + props: z.object({}), + description: "Horizontal divider line to separate email sections.", + component: () =>
, +}); diff --git a/packages/react-email/src/components/FeatureGrid.tsx b/packages/react-email/src/components/FeatureGrid.tsx new file mode 100644 index 000000000..f790b8197 --- /dev/null +++ b/packages/react-email/src/components/FeatureGrid.tsx @@ -0,0 +1,95 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailFeatureItem } from "./FeatureItem"; + +export const EmailFeatureGrid = defineComponent({ + name: "EmailFeatureGrid", + props: z.object({ + title: z.string(), + description: z.string(), + items: z.array(EmailFeatureItem.ref), + }), + description: + "2x2 feature grid with header title, description, and four feature items each with icon, title, and description.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = (props.items ?? []) as any[]; + + return ( +
+ + + {props.title as string} + + + {props.description as string} + + + {[0, 2].map((startIdx) => ( + + {items.slice(startIdx, startIdx + 2).map((item, i) => ( + + {String(item?.props?.iconAlt + + {String(item?.props?.title ?? "")} + + + {String(item?.props?.description ?? "")} + + + ))} + + ))} +
+ ); + }, +}); diff --git a/packages/react-email/src/components/FeatureItem.tsx b/packages/react-email/src/components/FeatureItem.tsx new file mode 100644 index 000000000..7e15b8149 --- /dev/null +++ b/packages/react-email/src/components/FeatureItem.tsx @@ -0,0 +1,17 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailFeatureItem = defineComponent({ + name: "EmailFeatureItem", + props: z.object({ + iconSrc: z.string(), + iconAlt: z.string(), + title: z.string(), + description: z.string(), + }), + description: + "Single feature item with icon, title, and description. Used inside EmailFeatureGrid or EmailFeatureList.", + component: () => null, +}); diff --git a/packages/react-email/src/components/FeatureList.tsx b/packages/react-email/src/components/FeatureList.tsx new file mode 100644 index 000000000..83227b0b3 --- /dev/null +++ b/packages/react-email/src/components/FeatureList.tsx @@ -0,0 +1,105 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Hr, Img, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailFeatureItem } from "./FeatureItem"; + +export const EmailFeatureList = defineComponent({ + name: "EmailFeatureList", + props: z.object({ + title: z.string(), + description: z.string(), + items: z.array(EmailFeatureItem.ref), + }), + description: + "Vertical feature list with header title, description, and feature items separated by dividers. Each item has an icon, title, and description.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = (props.items ?? []) as any[]; + + return ( +
+
+ + + {props.title as string} + + + {props.description as string} + + +
+
+ {items.map((item, i) => ( +
+
+
+ + + {String(item?.props?.iconAlt + + + + {String(item?.props?.title ?? "")} + + + {String(item?.props?.description ?? "")} + + + +
+
+ ))} +
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/FooterCentered.tsx b/packages/react-email/src/components/FooterCentered.tsx new file mode 100644 index 000000000..e40cb70ca --- /dev/null +++ b/packages/react-email/src/components/FooterCentered.tsx @@ -0,0 +1,128 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Link, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailSocialIcon } from "./SocialIcon"; + +export const EmailFooterCentered = defineComponent({ + name: "EmailFooterCentered", + props: z.object({ + logoSrc: z.string(), + logoAlt: z.string(), + companyName: z.string(), + tagline: z.string().optional(), + address: z.string(), + contact: z.string().optional(), + icons: z.array(EmailSocialIcon.ref).optional(), + }), + description: + "Centered footer with logo, company name, tagline, social icons, address, and contact info. Uses EmailSocialIcon items.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const icons = (props.icons ?? []) as any[]; + + return ( +
+ + + + + + + + + {icons.length > 0 && ( + + + + )} + + + + +
+ {props.logoAlt +
+ + {props.companyName as string} + + {props.tagline && ( + + {props.tagline as string} + + )} +
+ + {icons.map((icon, i) => ( + + + {String(icon?.props?.alt + + + ))} + +
+ + {props.address as string} + + {props.contact && ( + + {props.contact as string} + + )} +
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/FooterTwoColumn.tsx b/packages/react-email/src/components/FooterTwoColumn.tsx new file mode 100644 index 000000000..e596f5df0 --- /dev/null +++ b/packages/react-email/src/components/FooterTwoColumn.tsx @@ -0,0 +1,111 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Link, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailSocialIcon } from "./SocialIcon"; + +export const EmailFooterTwoColumn = defineComponent({ + name: "EmailFooterTwoColumn", + props: z.object({ + logoSrc: z.string(), + logoAlt: z.string(), + companyName: z.string(), + tagline: z.string().optional(), + address: z.string(), + contact: z.string().optional(), + icons: z.array(EmailSocialIcon.ref).optional(), + }), + description: + "Two-column footer with logo and company info on the left, social icons and address on the right. Uses EmailSocialIcon items.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const icons = (props.icons ?? []) as any[]; + + return ( +
+ + + {props.logoAlt + + {props.companyName as string} + + {props.tagline && ( + + {props.tagline as string} + + )} + + + {icons.length > 0 && ( + + {icons.map((icon, i) => ( + + + {String(icon?.props?.alt + + + ))} + + )} + + + {props.address as string} + + {props.contact && ( + + {props.contact as string} + + )} + + + +
+ ); + }, +}); diff --git a/packages/react-email/src/components/HeaderCenteredNav.tsx b/packages/react-email/src/components/HeaderCenteredNav.tsx new file mode 100644 index 000000000..99983b9da --- /dev/null +++ b/packages/react-email/src/components/HeaderCenteredNav.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Link, Row, Section } from "@react-email/components"; +import { z } from "zod"; +import { EmailNavLink } from "./NavLink"; + +export const EmailHeaderCenteredNav = defineComponent({ + name: "EmailHeaderCenteredNav", + props: z.object({ + logoSrc: z.string(), + logoAlt: z.string(), + logoHeight: z.number().optional(), + links: z.array(EmailNavLink.ref), + }), + description: + "Header with logo centered on top and navigation links centered below. Uses EmailNavLink items.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const links = (props.links ?? []) as any[]; + + return ( +
+ + + {props.logoAlt + + + + + + + + {links.map((link, i) => ( + + ))} + + +
+ + {String(link?.props?.text ?? "")} + +
+
+
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/HeaderSideNav.tsx b/packages/react-email/src/components/HeaderSideNav.tsx new file mode 100644 index 000000000..7f57867e7 --- /dev/null +++ b/packages/react-email/src/components/HeaderSideNav.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Link, Row, Section } from "@react-email/components"; +import { z } from "zod"; +import { EmailNavLink } from "./NavLink"; + +export const EmailHeaderSideNav = defineComponent({ + name: "EmailHeaderSideNav", + props: z.object({ + logoSrc: z.string(), + logoAlt: z.string(), + logoHeight: z.number().optional(), + links: z.array(EmailNavLink.ref), + }), + description: + "Header with logo on the left and navigation links on the right. Uses EmailNavLink items.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const links = (props.links ?? []) as any[]; + + return ( +
+ + + {props.logoAlt + + + + {links.map((link, i) => ( + + + {String(link?.props?.text ?? "")} + + + ))} + + + +
+ ); + }, +}); diff --git a/packages/react-email/src/components/HeaderSocial.tsx b/packages/react-email/src/components/HeaderSocial.tsx new file mode 100644 index 000000000..6a5b06a4e --- /dev/null +++ b/packages/react-email/src/components/HeaderSocial.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Link, Row, Section } from "@react-email/components"; +import { z } from "zod"; +import { EmailSocialIcon } from "./SocialIcon"; + +export const EmailHeaderSocial = defineComponent({ + name: "EmailHeaderSocial", + props: z.object({ + logoSrc: z.string(), + logoAlt: z.string(), + logoHeight: z.number().optional(), + icons: z.array(EmailSocialIcon.ref), + }), + description: + "Header with logo on the left and social media icon links on the right. Uses EmailSocialIcon items.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const icons = (props.icons ?? []) as any[]; + + return ( +
+ + + {props.logoAlt + + + + {icons.map((icon, i) => ( + + + {String(icon?.props?.alt + + + ))} + + + +
+ ); + }, +}); diff --git a/packages/react-email/src/components/Heading.tsx b/packages/react-email/src/components/Heading.tsx new file mode 100644 index 000000000..5f3ede1fb --- /dev/null +++ b/packages/react-email/src/components/Heading.tsx @@ -0,0 +1,31 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Heading } from "@react-email/components"; +import { z } from "zod"; + +export const EmailHeading = defineComponent({ + name: "EmailHeading", + props: z.object({ + text: z.string(), + level: z.number().optional(), + }), + description: "Email heading (h1-h6). Use level 1 for main title, 2 for section headers.", + component: ({ props }) => { + const level = Math.min(Math.max((props.level as number) ?? 1, 1), 6); + const tag = `h${level}` as "h1" | "h2" | "h3" | "h4" | "h5" | "h6"; + return ( + + {props.text as string} + + ); + }, +}); diff --git a/packages/react-email/src/components/Image.tsx b/packages/react-email/src/components/Image.tsx new file mode 100644 index 000000000..822b9c402 --- /dev/null +++ b/packages/react-email/src/components/Image.tsx @@ -0,0 +1,28 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Img } from "@react-email/components"; +import { z } from "zod"; + +export const EmailImage = defineComponent({ + name: "EmailImage", + props: z.object({ + src: z.string(), + alt: z.string(), + width: z.number().optional(), + }), + description: "Email image. Use real, publicly accessible URLs.", + component: ({ props }) => ( + {props.alt + ), +}); diff --git a/packages/react-email/src/components/ImageGrid.tsx b/packages/react-email/src/components/ImageGrid.tsx new file mode 100644 index 000000000..d2fc88b58 --- /dev/null +++ b/packages/react-email/src/components/ImageGrid.tsx @@ -0,0 +1,89 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailImage } from "./Image"; + +export const EmailImageGrid = defineComponent({ + name: "EmailImageGrid", + props: z.object({ + title: z.string().optional(), + description: z.string().optional(), + images: z.array(EmailImage.ref), + }), + description: + "2x2 image grid layout with optional header title and description. Pass up to 4 EmailImage items. Great for product galleries and portfolios.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const images = (props.images ?? []) as any[]; + + return ( +
+ {(props.title || props.description) && ( +
+ + {props.title && ( + + {props.title as string} + + )} + {props.description && ( + + {props.description as string} + + )} + +
+ )} +
+ {[0, 2].map((startIdx) => { + const rowImages = images.slice(startIdx, startIdx + 2); + if (rowImages.length === 0) return null; + return ( + 0 ? 16 : 0 }}> + {rowImages.map((img, i) => ( + + {String(img?.props?.alt + + ))} + + ); + })} +
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/Link.tsx b/packages/react-email/src/components/Link.tsx new file mode 100644 index 000000000..a6675c97c --- /dev/null +++ b/packages/react-email/src/components/Link.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Link } from "@react-email/components"; +import { z } from "zod"; + +export const EmailLink = defineComponent({ + name: "EmailLink", + props: z.object({ + text: z.string(), + href: z.string(), + }), + description: "Inline hyperlink in email content.", + component: ({ props }) => ( + + {props.text as string} + + ), +}); diff --git a/packages/react-email/src/components/List.tsx b/packages/react-email/src/components/List.tsx new file mode 100644 index 000000000..ffed3d96d --- /dev/null +++ b/packages/react-email/src/components/List.tsx @@ -0,0 +1,99 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Heading, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailListItem } from "./ListItem"; + +export const EmailList = defineComponent({ + name: "EmailList", + props: z.object({ + title: z.string().optional(), + items: z.array(EmailListItem.ref), + }), + description: + "Numbered list with circular number badges and item title + description. Pass EmailListItem children. Great for feature lists, how-it-works sections, and top-N lists.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = (props.items ?? []) as any[]; + + return ( +
+ {props.title && ( + + {props.title as string} + + )} + {items.map((item, i) => { + const title = String(item?.props?.title ?? ""); + const description = String(item?.props?.description ?? ""); + const number = i + 1; + + return ( +
+ + +
+ {number} +
+
+ + + {title} + + + {description} + + +
+
+ ); + })} +
+ ); + }, +}); diff --git a/packages/react-email/src/components/ListItem.tsx b/packages/react-email/src/components/ListItem.tsx new file mode 100644 index 000000000..e70b88831 --- /dev/null +++ b/packages/react-email/src/components/ListItem.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailListItem = defineComponent({ + name: "EmailListItem", + props: z.object({ + title: z.string(), + description: z.string(), + }), + description: "Data-only list item with title and description. Used as a child inside EmailList.", + component: () => null, +}); diff --git a/packages/react-email/src/components/Markdown.tsx b/packages/react-email/src/components/Markdown.tsx new file mode 100644 index 000000000..0dcfff8b5 --- /dev/null +++ b/packages/react-email/src/components/Markdown.tsx @@ -0,0 +1,27 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Markdown } from "@react-email/components"; +import { z } from "zod"; + +export const EmailMarkdown = defineComponent({ + name: "EmailMarkdown", + props: z.object({ + content: z.string(), + }), + description: + "Renders markdown content as styled email HTML. Supports headings, bold, italic, links, lists, and code.", + component: ({ props }) => ( + + {props.content as string} + + ), +}); diff --git a/packages/react-email/src/components/NavLink.tsx b/packages/react-email/src/components/NavLink.tsx new file mode 100644 index 000000000..9861cc75c --- /dev/null +++ b/packages/react-email/src/components/NavLink.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailNavLink = defineComponent({ + name: "EmailNavLink", + props: z.object({ + text: z.string(), + href: z.string(), + }), + description: "Navigation link item for email headers.", + component: () => null, +}); diff --git a/packages/react-email/src/components/NumberedSteps.tsx b/packages/react-email/src/components/NumberedSteps.tsx new file mode 100644 index 000000000..cab76ce56 --- /dev/null +++ b/packages/react-email/src/components/NumberedSteps.tsx @@ -0,0 +1,106 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Hr, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailStepItem } from "./StepItem"; + +export const EmailNumberedSteps = defineComponent({ + name: "EmailNumberedSteps", + props: z.object({ + title: z.string(), + description: z.string(), + steps: z.array(EmailStepItem.ref), + }), + description: + "Numbered steps list with header title, description, and sequential step items. Each step shows a numbered badge, title, and description.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const steps = (props.steps ?? []) as any[]; + + return ( +
+
+ + {props.title as string} + + + {props.description as string} + +
+ {steps.map((step, index) => ( +
+
+
+ + +
+ {index + 1} +
+
+ + + {String(step?.props?.title ?? "")} + + + {String(step?.props?.description ?? "")} + + +
+
+
+ ))} +
+ ); + }, +}); diff --git a/packages/react-email/src/components/PricingCard.tsx b/packages/react-email/src/components/PricingCard.tsx new file mode 100644 index 000000000..207bf52f3 --- /dev/null +++ b/packages/react-email/src/components/PricingCard.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Button, Hr, Text } from "@react-email/components"; +import { z } from "zod"; +import { EmailPricingFeature } from "./PricingFeature"; + +export const EmailPricingCard = defineComponent({ + name: "EmailPricingCard", + props: z.object({ + badge: z.string().optional(), + price: z.string(), + period: z.string().optional(), + description: z.string(), + features: z.array(EmailPricingFeature.ref), + buttonLabel: z.string(), + buttonHref: z.string(), + buttonColor: z.string().optional(), + note: z.string().optional(), + subNote: z.string().optional(), + }), + description: + "Pricing card with badge, price, description, feature list, CTA button, and optional note. Great for upgrade and promotional emails.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const features = (props.features ?? []) as any[]; + const bg = (props.buttonColor as string) ?? "#4F46E5"; + const period = (props.period as string) ?? "/ month"; + + return ( +
+ {props.badge && ( + + {props.badge as string} + + )} + + {props.price as string}{" "} + {period} + + + {props.description as string} + +
    + {features.map((feat, i) => ( +
  • + {String(feat?.props?.text ?? "")} +
  • + ))} +
+ + {(props.note || props.subNote) && ( + <> +
+ {props.note && ( + + {props.note as string} + + )} + {props.subNote && ( + + {props.subNote as string} + + )} + + )} +
+ ); + }, +}); diff --git a/packages/react-email/src/components/PricingFeature.tsx b/packages/react-email/src/components/PricingFeature.tsx new file mode 100644 index 000000000..30bf3d003 --- /dev/null +++ b/packages/react-email/src/components/PricingFeature.tsx @@ -0,0 +1,13 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailPricingFeature = defineComponent({ + name: "EmailPricingFeature", + props: z.object({ + text: z.string(), + }), + description: "Single pricing feature line item. Used inside EmailPricingCard.", + component: () => null, +}); diff --git a/packages/react-email/src/components/ProductCard.tsx b/packages/react-email/src/components/ProductCard.tsx new file mode 100644 index 000000000..6643e2e0e --- /dev/null +++ b/packages/react-email/src/components/ProductCard.tsx @@ -0,0 +1,97 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Button, Heading, Img, Section, Text } from "@react-email/components"; +import { z } from "zod"; + +export const EmailProductCard = defineComponent({ + name: "EmailProductCard", + props: z.object({ + imageSrc: z.string(), + imageAlt: z.string(), + category: z.string().optional(), + title: z.string(), + description: z.string(), + price: z.string(), + buttonLabel: z.string(), + buttonHref: z.string(), + buttonColor: z.string().optional(), + }), + description: + "Product showcase card with hero image, optional category, title, description, price, and buy button.", + component: ({ props }) => { + const bg = (props.buttonColor as string) ?? "#4F46E5"; + return ( +
+ {props.imageAlt +
+ {props.category && ( + + {props.category as string} + + )} + + {props.title as string} + + + {props.description as string} + + + {props.price as string} + + +
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/Section.tsx b/packages/react-email/src/components/Section.tsx new file mode 100644 index 000000000..2e9f906b0 --- /dev/null +++ b/packages/react-email/src/components/Section.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Section } from "@react-email/components"; +import { z } from "zod"; +import { EmailLeafChildUnion } from "../unions"; + +export const EmailSection = defineComponent({ + name: "EmailSection", + props: z.object({ + children: z.array(EmailLeafChildUnion), + }), + description: + "Groups email content into a section. Use to organize header, body, and footer areas.", + component: ({ props, renderNode }) => ( +
{renderNode(props.children)}
+ ), +}); diff --git a/packages/react-email/src/components/SocialIcon.tsx b/packages/react-email/src/components/SocialIcon.tsx new file mode 100644 index 000000000..d54d2de67 --- /dev/null +++ b/packages/react-email/src/components/SocialIcon.tsx @@ -0,0 +1,15 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailSocialIcon = defineComponent({ + name: "EmailSocialIcon", + props: z.object({ + src: z.string(), + alt: z.string(), + href: z.string(), + }), + description: "Social media icon link for email headers. src should be a square icon image URL.", + component: () => null, +}); diff --git a/packages/react-email/src/components/StatItem.tsx b/packages/react-email/src/components/StatItem.tsx new file mode 100644 index 000000000..7d8c42512 --- /dev/null +++ b/packages/react-email/src/components/StatItem.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailStatItem = defineComponent({ + name: "EmailStatItem", + props: z.object({ + value: z.string(), + label: z.string(), + }), + description: "Single stat with a value and label. Used inside EmailStats.", + component: () => null, +}); diff --git a/packages/react-email/src/components/Stats.tsx b/packages/react-email/src/components/Stats.tsx new file mode 100644 index 000000000..06cb53cd4 --- /dev/null +++ b/packages/react-email/src/components/Stats.tsx @@ -0,0 +1,53 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Row } from "@react-email/components"; +import { z } from "zod"; +import { EmailStatItem } from "./StatItem"; + +export const EmailStats = defineComponent({ + name: "EmailStats", + props: z.object({ + items: z.array(EmailStatItem.ref), + }), + description: + "Horizontal stats row displaying key metrics. Each stat has a large value and a label below it.", + component: ({ props }) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const items = (props.items ?? []) as any[]; + + return ( + + {items.map((item, i) => ( + +

+ {String(item?.props?.value ?? "")} +

+

+ {String(item?.props?.label ?? "")} +

+
+ ))} +
+ ); + }, +}); diff --git a/packages/react-email/src/components/StepItem.tsx b/packages/react-email/src/components/StepItem.tsx new file mode 100644 index 000000000..45dd36f9a --- /dev/null +++ b/packages/react-email/src/components/StepItem.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; + +export const EmailStepItem = defineComponent({ + name: "EmailStepItem", + props: z.object({ + title: z.string(), + description: z.string(), + }), + description: "Single numbered step with title and description. Used inside EmailNumberedSteps.", + component: () => null, +}); diff --git a/packages/react-email/src/components/SurveyRating.tsx b/packages/react-email/src/components/SurveyRating.tsx new file mode 100644 index 000000000..06b549146 --- /dev/null +++ b/packages/react-email/src/components/SurveyRating.tsx @@ -0,0 +1,86 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Heading, Row, Section, Text } from "@react-email/components"; +import { z } from "zod"; + +export const EmailSurveyRating = defineComponent({ + name: "EmailSurveyRating", + props: z.object({ + question: z.string(), + description: z.string().optional(), + buttonColor: z.string().optional(), + }), + description: + "Rating survey section with a question and 1-5 numbered buttons. Great for feedback and NPS emails.", + component: ({ props }) => { + const bg = (props.buttonColor as string) ?? "#4F46E5"; + + return ( +
+ + Your opinion matters + + + {props.question as string} + + {props.description && ( + + {props.description as string} + + )} + + + + + + {[1, 2, 3, 4, 5].map((number) => ( + + ))} + + +
+ + {number} + +
+
+
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/Template.tsx b/packages/react-email/src/components/Template.tsx new file mode 100644 index 000000000..102eb018a --- /dev/null +++ b/packages/react-email/src/components/Template.tsx @@ -0,0 +1,50 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { z } from "zod"; +import { EmailLeafChildUnion } from "../unions"; +import { EmailColumn } from "./Column"; +import { EmailColumns } from "./Columns"; +import { EmailFooterCentered } from "./FooterCentered"; +import { EmailFooterTwoColumn } from "./FooterTwoColumn"; +import { EmailHeaderCenteredNav } from "./HeaderCenteredNav"; +import { EmailHeaderSideNav } from "./HeaderSideNav"; +import { EmailHeaderSocial } from "./HeaderSocial"; +import { EmailSection } from "./Section"; + +const EmailTemplateChildUnion = z.union([ + ...EmailLeafChildUnion.options, + EmailSection.ref, + EmailColumns.ref, + EmailColumn.ref, + EmailHeaderSideNav.ref, + EmailHeaderCenteredNav.ref, + EmailHeaderSocial.ref, + EmailFooterCentered.ref, + EmailFooterTwoColumn.ref, +]); + +export const EmailTemplate = defineComponent({ + name: "EmailTemplate", + props: z.object({ + subject: z.string(), + previewText: z.string().optional(), + children: z.array(EmailTemplateChildUnion), + }), + description: + "Root email template. Renders a live email preview with Copy HTML export. Always provide a subject line.", + component: function EmailTemplateView({ props, renderNode }) { + return ( +
+ {renderNode(props.children)} +
+ ); + }, +}); diff --git a/packages/react-email/src/components/Testimonial.tsx b/packages/react-email/src/components/Testimonial.tsx new file mode 100644 index 000000000..846eeb9b9 --- /dev/null +++ b/packages/react-email/src/components/Testimonial.tsx @@ -0,0 +1,84 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Column, Img, Row, Section } from "@react-email/components"; +import { z } from "zod"; + +export const EmailTestimonial = defineComponent({ + name: "EmailTestimonial", + props: z.object({ + quote: z.string(), + avatarSrc: z.string(), + avatarAlt: z.string(), + name: z.string(), + role: z.string(), + }), + description: + "Testimonial quote with avatar, name, and role. Centered layout for social proof in emails.", + component: ({ props }) => { + return ( +
+

+ {props.quote as string} +

+ + +
+ {props.avatarAlt +
+
+ +

+ {props.name as string} +

+
+ + + + +

{props.role as string}

+
+
+
+ ); + }, +}); diff --git a/packages/react-email/src/components/Text.tsx b/packages/react-email/src/components/Text.tsx new file mode 100644 index 000000000..4eb4c3301 --- /dev/null +++ b/packages/react-email/src/components/Text.tsx @@ -0,0 +1,26 @@ +"use client"; + +import { defineComponent } from "@openuidev/react-lang"; +import { Text } from "@react-email/components"; +import { z } from "zod"; + +export const EmailText = defineComponent({ + name: "EmailText", + props: z.object({ + text: z.string(), + }), + description: "Email text paragraph for body content.", + component: ({ props }) => ( + + {props.text as string} + + ), +}); diff --git a/packages/react-email/src/index.ts b/packages/react-email/src/index.ts new file mode 100644 index 000000000..204566d78 --- /dev/null +++ b/packages/react-email/src/index.ts @@ -0,0 +1 @@ +export { emailLibrary, emailPromptOptions } from "./library"; diff --git a/packages/react-email/src/library.ts b/packages/react-email/src/library.ts new file mode 100644 index 000000000..bc73998c9 --- /dev/null +++ b/packages/react-email/src/library.ts @@ -0,0 +1,494 @@ +import type { ComponentGroup, PromptOptions } from "@openuidev/react-lang"; +import { createLibrary } from "@openuidev/react-lang"; + +// ── Email components ── + +import { EmailArticle } from "./components/Article"; +import { EmailAvatar } from "./components/Avatar"; +import { EmailAvatarGroup } from "./components/AvatarGroup"; +import { EmailAvatarWithText } from "./components/AvatarWithText"; +import { EmailBentoGrid } from "./components/BentoGrid"; +import { EmailBentoItem } from "./components/BentoItem"; +import { EmailButton } from "./components/Button"; +import { EmailCheckoutItem } from "./components/CheckoutItem"; +import { EmailCheckoutTable } from "./components/CheckoutTable"; +import { EmailCodeBlock } from "./components/CodeBlock"; +import { EmailCodeInline } from "./components/CodeInline"; +import { EmailColumn } from "./components/Column"; +import { EmailColumns } from "./components/Columns"; +import { EmailCustomerReview } from "./components/CustomerReview"; +import { EmailDivider } from "./components/Divider"; +import { EmailFeatureGrid } from "./components/FeatureGrid"; +import { EmailFeatureItem } from "./components/FeatureItem"; +import { EmailFeatureList } from "./components/FeatureList"; +import { EmailFooterCentered } from "./components/FooterCentered"; +import { EmailFooterTwoColumn } from "./components/FooterTwoColumn"; +import { EmailHeaderCenteredNav } from "./components/HeaderCenteredNav"; +import { EmailHeaderSideNav } from "./components/HeaderSideNav"; +import { EmailHeaderSocial } from "./components/HeaderSocial"; +import { EmailHeading } from "./components/Heading"; +import { EmailImage } from "./components/Image"; +import { EmailImageGrid } from "./components/ImageGrid"; +import { EmailLink } from "./components/Link"; +import { EmailList } from "./components/List"; +import { EmailListItem } from "./components/ListItem"; +import { EmailMarkdown } from "./components/Markdown"; +import { EmailNavLink } from "./components/NavLink"; +import { EmailNumberedSteps } from "./components/NumberedSteps"; +import { EmailPricingCard } from "./components/PricingCard"; +import { EmailPricingFeature } from "./components/PricingFeature"; +import { EmailProductCard } from "./components/ProductCard"; +import { EmailSection } from "./components/Section"; +import { EmailSocialIcon } from "./components/SocialIcon"; +import { EmailStatItem } from "./components/StatItem"; +import { EmailStats } from "./components/Stats"; +import { EmailStepItem } from "./components/StepItem"; +import { EmailSurveyRating } from "./components/SurveyRating"; +import { EmailTemplate } from "./components/Template"; +import { EmailTestimonial } from "./components/Testimonial"; +import { EmailText } from "./components/Text"; + +// ── Component groups (email only) ── + +export const emailComponentGroups: ComponentGroup[] = [ + { + name: "Email Structure", + components: ["EmailTemplate", "EmailSection", "EmailColumns", "EmailColumn"], + notes: [ + "- EmailTemplate is the root email wrapper. Always use it for email content.", + "- Use EmailSection to group related content (e.g. header area, body, footer).", + "- Use EmailColumns + EmailColumn for multi-column layouts (e.g. two features side by side).", + ], + }, + { + name: "Email Headers", + components: [ + "EmailHeaderSideNav", + "EmailHeaderCenteredNav", + "EmailHeaderSocial", + "EmailNavLink", + "EmailSocialIcon", + ], + notes: [ + "- EmailHeaderSideNav: Logo on the left, text navigation links on the right. Use EmailNavLink for each link.", + "- EmailHeaderCenteredNav: Logo centered on top, text navigation links centered below. Use EmailNavLink for each link.", + "- EmailHeaderSocial: Logo on the left, social media icon links on the right. Use EmailSocialIcon for each icon.", + "- Place a header as the FIRST child of EmailTemplate for a professional branded look.", + "- Always follow the header with an EmailDivider to separate it from the body content.", + ], + }, + { + name: "Email Footers", + components: ["EmailFooterCentered", "EmailFooterTwoColumn"], + notes: [ + "- EmailFooterCentered: Centered footer with logo, company name, tagline, social icons, address, and contact info.", + "- EmailFooterTwoColumn: Two-column footer with logo and company info on the left, social icons and address on the right.", + "- Both footer components accept EmailSocialIcon items for social media icons.", + "- Place a footer as the LAST child of EmailTemplate, after an EmailDivider.", + ], + }, + { + name: "Email Content", + components: [ + "EmailHeading", + "EmailText", + "EmailButton", + "EmailImage", + "EmailDivider", + "EmailLink", + "EmailCodeBlock", + "EmailCodeInline", + "EmailMarkdown", + ], + notes: [ + "- EmailHeading level 1 for main title, level 2 for section headers.", + "- EmailButton always needs an href URL.", + "- EmailImage should use real, publicly accessible image URLs.", + "- EmailDivider takes no arguments: EmailDivider()", + "- EmailCodeBlock for multi-line code snippets. Optionally set language (e.g. 'javascript', 'python').", + "- EmailCodeInline for inline code within text (e.g. variable names, commands).", + "- EmailMarkdown for rendering markdown content (headings, bold, italic, links, lists).", + ], + }, + { + name: "Email Articles & Products", + components: ["EmailArticle", "EmailProductCard"], + notes: [ + "- EmailArticle: Hero image + category + title + description + CTA button. Great for blog posts and newsletters.", + "- EmailProductCard: Hero image + category + title + description + price + buy button. Great for product showcases.", + "- Both support an optional buttonColor prop for CTA button customization.", + ], + }, + { + name: "Email Features & Steps", + components: [ + "EmailFeatureItem", + "EmailFeatureGrid", + "EmailFeatureList", + "EmailStepItem", + "EmailNumberedSteps", + ], + notes: [ + "- EmailFeatureItem: Child component with iconSrc, iconAlt, title, description. Used inside EmailFeatureGrid or EmailFeatureList.", + "- EmailFeatureGrid: 2x2 grid of features with header title and description. Takes EmailFeatureItem items.", + "- EmailFeatureList: Vertical list of features separated by dividers. Takes EmailFeatureItem items.", + "- EmailStepItem: Child component with title and description. Used inside EmailNumberedSteps.", + "- EmailNumberedSteps: Numbered steps list with auto-numbered badges. Takes EmailStepItem items.", + "- For icon URLs, use https://picsum.photos/seed/KEYWORD/48/48 or similar.", + ], + }, + { + name: "Email Commerce", + components: [ + "EmailCheckoutItem", + "EmailCheckoutTable", + "EmailPricingFeature", + "EmailPricingCard", + ], + notes: [ + "- EmailCheckoutItem: Cart item with optional image, name, quantity, price. Used inside EmailCheckoutTable.", + "- EmailCheckoutTable: Cart table with items and checkout button. Great for abandoned cart and order summary emails.", + "- EmailPricingFeature: Single feature line item text. Used inside EmailPricingCard.", + "- EmailPricingCard: Pricing card with badge, price, period, description, features list, CTA button, and optional note.", + ], + }, + { + name: "Email Social Proof & Surveys", + components: ["EmailTestimonial", "EmailSurveyRating", "EmailStatItem", "EmailStats"], + notes: [ + "- EmailTestimonial: Centered testimonial quote with avatar, name, and role.", + "- EmailSurveyRating: Rating survey with question and 1-5 numbered buttons. Great for feedback/NPS emails.", + "- EmailStatItem: Child component with value and label. Used inside EmailStats.", + "- EmailStats: Horizontal row of key metrics/stats.", + ], + }, + { + name: "Email Image Layouts", + components: ["EmailImageGrid"], + notes: [ + "- EmailImageGrid: 2x2 image grid with optional title and description. Pass up to 4 EmailImage items.", + "- Great for product galleries, portfolios, and visual showcases.", + ], + }, + { + name: "Email Avatars", + components: ["EmailAvatar", "EmailAvatarGroup", "EmailAvatarWithText"], + notes: [ + "- EmailAvatar: Single avatar image. Supports circular (rounded='full') or rounded-square (rounded='md') shapes, and configurable size.", + "- EmailAvatarGroup: Overlapping stacked avatars. Pass EmailAvatar items. Great for showing team members or participants.", + "- EmailAvatarWithText: Avatar with name and role text beside it. Optionally wraps in a link. Great for author attribution.", + ], + }, + { + name: "Email Lists", + components: ["EmailListItem", "EmailList"], + notes: [ + "- EmailListItem: Data-only component with title and description. Used as a child inside EmailList.", + "- EmailList: Numbered list with circular badges. Pass EmailListItem children. Great for top-N lists, how-it-works, and feature lists.", + ], + }, + { + name: "Email Reviews", + components: ["EmailCustomerReview"], + notes: [ + "- EmailCustomerReview: Star rating distribution with bars and percentages. Shows total review count and optional CTA button.", + "- Provide rating counts for each star level (rating1 through rating5) and totalReviews.", + ], + }, + { + name: "Email Marketing", + components: ["EmailBentoItem", "EmailBentoGrid"], + notes: [ + "- EmailBentoItem: Data-only component with imageSrc, imageAlt, title, description. Used inside EmailBentoGrid.", + "- EmailBentoGrid: Bento-style layout with a dark hero section on top and product cards below. Pass EmailBentoItem children.", + "- Great for product showcase and marketing emails.", + ], + }, +]; + +// ── Examples (email only, Examples 1-10) ── + +export const emailExamples: string[] = [ + `Example 1 — Welcome email: +root = EmailTemplate("Welcome to Acme!", "You're in! Here's how to get started.", [heading, intro, btn, divider, footer]) +heading = EmailHeading("Welcome aboard!", 1) +intro = EmailText("Thanks for signing up for Acme. We're thrilled to have you on board. Here's everything you need to get started.") +btn = EmailButton("Get Started", "https://example.com/start", "#5F51E8") +divider = EmailDivider() +footer = EmailText("If you have any questions, reply to this email. We're here to help.")`, + + `Example 2 — Newsletter with header and footer: +root = EmailTemplate("Acme Weekly", "This week: new features and tips", [header, divider1, section1, divider2, section2, divider3, footer]) +header = EmailHeaderSideNav("https://picsum.photos/seed/acme-logo/150/42", "Acme", 42, [nav1, nav2, nav3]) +nav1 = EmailNavLink("About", "https://example.com/about") +nav2 = EmailNavLink("Blog", "https://example.com/blog") +nav3 = EmailNavLink("Docs", "https://example.com/docs") +divider1 = EmailDivider() +section1 = EmailSection([s1title, s1text, s1btn]) +s1title = EmailHeading("New Feature: Dark Mode", 2) +s1text = EmailText("We just launched dark mode across all platforms. Your eyes will thank you.") +s1btn = EmailButton("Try It Now", "https://example.com/dark-mode", "#1a1a2e") +divider2 = EmailDivider() +section2 = EmailSection([s2title, s2text]) +s2title = EmailHeading("Tip of the Week", 2) +s2text = EmailText("Use keyboard shortcuts to navigate 3x faster.") +divider3 = EmailDivider() +footer = EmailFooterCentered("https://picsum.photos/seed/acme-icon/42/42", "Acme", "Acme Corporation", "Think different", "123 Main Street, Anytown, CA 12345", "hello@acme.com", [fi1, fi2, fi3]) +fi1 = EmailSocialIcon("https://react.email/static/facebook-logo.png", "Facebook", "https://facebook.com/acme") +fi2 = EmailSocialIcon("https://react.email/static/x-logo.png", "X", "https://x.com/acme") +fi3 = EmailSocialIcon("https://react.email/static/instagram-logo.png", "Instagram", "https://instagram.com/acme")`, + + `Example 3 — Order confirmation with columns: +root = EmailTemplate("Order Confirmed #12345", "Your order has been placed!", [heading, thanks, divider1, cols, divider2, total, btn, divider3, footer]) +heading = EmailHeading("Order Confirmed", 1) +thanks = EmailText("Thank you for your purchase! Here's a summary of your order.") +divider1 = EmailDivider() +cols = EmailColumns([col1, col2]) +col1 = EmailColumn([itemTitle, itemDesc]) +col2 = EmailColumn([priceTitle, priceVal]) +itemTitle = EmailHeading("Item", 2) +itemDesc = EmailText("Premium Plan - Annual") +priceTitle = EmailHeading("Price", 2) +priceVal = EmailText("$99.00/year") +divider2 = EmailDivider() +total = EmailText("Total: $99.00") +btn = EmailButton("View Order", "https://example.com/orders/12345", "#16a34a") +divider3 = EmailDivider() +footer = EmailText("Need help? Contact us at support@example.com")`, + + `Example 4 — Developer onboarding with code: +root = EmailTemplate("Getting Started with Acme API", "Your API key is ready", [heading, intro, section1, divider, section2, divider2, footer]) +heading = EmailHeading("Welcome to the Acme API", 1) +intro = EmailMarkdown("You're all set! Below you'll find everything you need to **get started** with our API.") +section1 = EmailSection([s1title, s1text, codeblock]) +s1title = EmailHeading("Quick Start", 2) +s1text = EmailText("Install the SDK and make your first request:") +codeblock = EmailCodeBlock("npm install @acme/sdk\\n\\nimport { Acme } from '@acme/sdk';\\nconst client = new Acme({ apiKey: 'your-key' });\\nconst res = await client.ping();", "javascript") +divider = EmailDivider() +section2 = EmailSection([s2title, s2text]) +s2title = EmailHeading("Need Help?", 2) +s2text = EmailMarkdown("Check our [documentation](https://docs.example.com) or reply to this email.") +divider2 = EmailDivider() +footer = EmailText("Happy coding!")`, + + `Example 5 — Password reset email: +root = EmailTemplate("Reset your password", "Someone requested a password change", [logo, heading, text1, btn, text2, divider, linkText, divider2, footer]) +logo = EmailImage("https://picsum.photos/seed/app-logo/100/30", "App", 100) +heading = EmailHeading("Reset your password", 1) +text1 = EmailText("Someone recently requested a password change for your account. If this was you, click below:") +btn = EmailButton("Reset Password", "https://example.com/reset?token=abc123", "#0061fe") +text2 = EmailText("Or copy and paste this link:") +divider = EmailDivider() +linkText = EmailLink("https://example.com/reset?token=abc123", "https://example.com/reset?token=abc123") +divider2 = EmailDivider() +footer = EmailText("If you didn't request this, just ignore this email. This link expires in 24 hours.")`, + + `Example 6 — Promotional sale email: +root = EmailTemplate("Summer Sale — Up to 50% Off!", "Don't miss our biggest sale", [hero, heading, subhead, divider1, featCols, divider2, ctaBtn, divider3, footer]) +hero = EmailImage("https://picsum.photos/seed/summer-sale/600/250", "Summer Sale Banner", 600) +heading = EmailHeading("Summer Sale is Here!", 1) +subhead = EmailText("For a limited time, enjoy up to 50% off on select items.") +divider1 = EmailDivider() +featCols = EmailColumns([deal1Col, deal2Col, deal3Col]) +deal1Col = EmailColumn([deal1Img, deal1Name, deal1Price]) +deal2Col = EmailColumn([deal2Img, deal2Name, deal2Price]) +deal3Col = EmailColumn([deal3Img, deal3Name, deal3Price]) +deal1Img = EmailImage("https://picsum.photos/seed/product1/180/180", "Sunglasses", 180) +deal1Name = EmailText("Designer Sunglasses") +deal1Price = EmailText("$49.99 (was $99.99)") +deal2Img = EmailImage("https://picsum.photos/seed/product2/180/180", "Beach Bag", 180) +deal2Name = EmailText("Canvas Beach Bag") +deal2Price = EmailText("$29.99 (was $59.99)") +deal3Img = EmailImage("https://picsum.photos/seed/product3/180/180", "Sandals", 180) +deal3Name = EmailText("Leather Sandals") +deal3Price = EmailText("$39.99 (was $79.99)") +divider2 = EmailDivider() +ctaBtn = EmailButton("Shop the Sale", "https://example.com/summer-sale", "#e11d48") +divider3 = EmailDivider() +footer = EmailText("StyleShop · 500 Fashion Ave · New York, NY 10018")`, + + `Example 7 — Feature showcase with grid and steps: +root = EmailTemplate("Welcome to Acme!", "Discover what you can do", [heading, intro, divider1, featureGrid, divider2, steps, divider3, footer]) +heading = EmailHeading("Welcome to Acme!", 1) +intro = EmailText("Here's what you can do with your new account:") +divider1 = EmailDivider() +featureGrid = EmailFeatureGrid("Key Features", "Powerful features to help you succeed.", [feat1, feat2, feat3, feat4]) +feat1 = EmailFeatureItem("https://picsum.photos/seed/heart/48/48", "Heart", "Easy to Use", "Get started in minutes.") +feat2 = EmailFeatureItem("https://picsum.photos/seed/rocket/48/48", "Rocket", "Lightning Fast", "Blazing fast performance.") +feat3 = EmailFeatureItem("https://picsum.photos/seed/shield/48/48", "Shield", "Secure", "Enterprise-grade security.") +feat4 = EmailFeatureItem("https://picsum.photos/seed/chart/48/48", "Chart", "Analytics", "Deep data insights.") +divider2 = EmailDivider() +steps = EmailNumberedSteps("Getting Started", "Follow these steps:", [step1, step2, step3]) +step1 = EmailStepItem("Create Profile", "Set up your profile with a photo and bio.") +step2 = EmailStepItem("Connect Tools", "Integrate with Slack, GitHub, and Jira.") +step3 = EmailStepItem("Invite Team", "Add team members and start collaborating.") +divider3 = EmailDivider() +footer = EmailText("Acme, Inc. · San Francisco, CA 94107")`, + + `Example 8 — Abandoned cart email: +root = EmailTemplate("You left something behind!", "Complete your purchase", [heading, text, checkout, divider, footer]) +heading = EmailHeading("Don't forget your items!", 1) +text = EmailText("Complete your purchase before these items sell out.") +checkout = EmailCheckoutTable("Your Cart", [item1, item2], "Complete Purchase", "https://example.com/checkout", "#4F46E5") +item1 = EmailCheckoutItem("https://picsum.photos/seed/watch/100/100", "Classic Watch", "Classic Watch", 1, "$210.00") +item2 = EmailCheckoutItem("https://picsum.photos/seed/clock/100/100", "Wall Clock", "Analogue Clock", 2, "$40.00") +divider = EmailDivider() +footer = EmailText("Acme Store · 123 Commerce Way · San Francisco, CA 94107")`, + + `Example 9 — Pricing with testimonial and survey: +root = EmailTemplate("Upgrade to Pro", "Unlock premium features", [heading, text, divider1, pricing, divider2, testimonial, divider3, survey, divider4, footer]) +heading = EmailHeading("Upgrade Your Plan", 1) +text = EmailText("You've been on the free plan for 30 days. Unlock everything with Pro.") +divider1 = EmailDivider() +pricing = EmailPricingCard("Pro", "$12", "/ month", "Everything you need to grow.", [pf1, pf2, pf3], "Upgrade Now", "https://example.com/upgrade", "#4F46E5") +pf1 = EmailPricingFeature("Unlimited projects") +pf2 = EmailPricingFeature("Advanced analytics") +pf3 = EmailPricingFeature("Priority support") +divider2 = EmailDivider() +testimonial = EmailTestimonial("Acme Pro transformed our workflow. Can't imagine going back.", "https://picsum.photos/seed/ceo/100/100", "Jane Smith", "Jane Smith", "CEO, TechCorp") +divider3 = EmailDivider() +survey = EmailSurveyRating("How would you rate your experience?", "Your feedback helps us improve.", "#4F46E5") +divider4 = EmailDivider() +footer = EmailText("Acme, Inc. · San Francisco, CA 94107")`, + + `Example 10 — Stats, gallery, avatars, list, and bento grid: +root = EmailTemplate("Your Monthly Report", "Key metrics and highlights", [header, divider1, stats, divider2, gallery, divider3, team, divider4, list, divider5, bento, divider6, reviews, divider7, footer]) +header = EmailHeaderCenteredNav("https://picsum.photos/seed/acme/150/42", "Acme", 42, [nav1, nav2]) +nav1 = EmailNavLink("Dashboard", "https://example.com/dashboard") +nav2 = EmailNavLink("Reports", "https://example.com/reports") +divider1 = EmailDivider() +stats = EmailStats([stat1, stat2, stat3]) +stat1 = EmailStatItem("12,847", "Users") +stat2 = EmailStatItem("94.2%", "Uptime") +stat3 = EmailStatItem("$48.5K", "Revenue") +divider2 = EmailDivider() +gallery = EmailImageGrid("New Products", "Our latest arrivals.", [img1, img2, img3, img4]) +img1 = EmailImage("https://picsum.photos/seed/prod-a/300/288", "Product A") +img2 = EmailImage("https://picsum.photos/seed/prod-b/300/288", "Product B") +img3 = EmailImage("https://picsum.photos/seed/prod-c/300/288", "Product C") +img4 = EmailImage("https://picsum.photos/seed/prod-d/300/288", "Product D") +divider3 = EmailDivider() +team = EmailAvatarGroup([av1, av2, av3]) +av1 = EmailAvatar("https://picsum.photos/seed/person1/100/100", "Alice", 44) +av2 = EmailAvatar("https://picsum.photos/seed/person2/100/100", "Bob", 44) +av3 = EmailAvatar("https://picsum.photos/seed/person3/100/100", "Carol", 44) +divider4 = EmailDivider() +list = EmailList("Top 3 Updates", [li1, li2, li3]) +li1 = EmailListItem("New Dashboard", "Redesigned analytics dashboard with real-time data.") +li2 = EmailListItem("Mobile App", "Now available on iOS and Android.") +li3 = EmailListItem("API v2", "Faster, more reliable API with new endpoints.") +divider5 = EmailDivider() +bento = EmailBentoGrid("Featured Collection", "Handpicked products for you.", "Shop now", "https://example.com/shop", "https://picsum.photos/seed/hero/400/250", "Collection", [bi1, bi2]) +bi1 = EmailBentoItem("https://picsum.photos/seed/item-x/300/200", "Item X", "Premium Widget", "High-quality craftsmanship.") +bi2 = EmailBentoItem("https://picsum.photos/seed/item-y/300/200", "Item Y", "Deluxe Gadget", "Next-gen technology.") +divider6 = EmailDivider() +reviews = EmailCustomerReview("Product Ratings", 500, 300, 100, 50, 30, 20, "Write a Review", "https://example.com/review", "#4F46E5") +divider7 = EmailDivider() +footer = EmailFooterTwoColumn("https://picsum.photos/seed/acme-icon/42/42", "Acme", "Acme Corp", "Innovation first", "123 Main St, CA 12345", "hello@acme.com", [si1, si2]) +si1 = EmailSocialIcon("https://react.email/static/x-logo.png", "X", "https://x.com/acme") +si2 = EmailSocialIcon("https://react.email/static/instagram-logo.png", "Instagram", "https://instagram.com/acme")`, +]; + +// ── Additional rules (email only) ── + +export const emailAdditionalRules: string[] = [ + "You are an expert email designer using react-email components.", + "The 10 supported email types are: Welcome/Onboarding, Newsletter, Order Confirmation, Password Reset, Promotional/Sale, Event Invitation, Feedback Request, Shipping/Delivery Update, Account Verification, Onboarding Tutorial.", + "Use realistic, professional placeholder text — never use lorem ipsum.", + "Always provide both subject and previewText for EmailTemplate.", + "Use EmailSection to group related content areas (header, body, footer sections).", + "Use EmailDivider between major sections for visual separation.", + "For multi-column layouts (features, pricing comparisons), use EmailColumns with EmailColumn children.", + "Keep email designs clean and focused — avoid too many colors or fonts.", + "Use EmailButton with descriptive labels and realistic href URLs.", + "For images, use publicly accessible URLs like https://picsum.photos/seed/KEYWORD/600/300.", + "When the user asks to modify an existing email, regenerate the full EmailTemplate with the requested changes applied.", + "Use EmailCodeBlock for multi-line code (API examples, install commands). Set the language prop for syntax context.", + "Use EmailCodeInline for short inline code references within EmailText (e.g. variable names, CLI commands).", + "Use EmailMarkdown when the content includes rich formatting like bold, italic, links, or lists — it's more flexible than plain EmailText.", + "Use EmailHeaderSideNav for a professional header with logo left and nav links right.", + "Use EmailHeaderCenteredNav for a centered brand header with logo on top and nav links below.", + "Use EmailHeaderSocial for a header with logo left and social media icons right.", + "Place the header as the FIRST child of EmailTemplate, followed by an EmailDivider.", + "Use EmailFooterCentered for a centered footer with logo, company name, social icons, and contact info.", + "Use EmailFooterTwoColumn for a two-column footer with logo and info on the left, social icons and address on the right.", + "Place the footer as the LAST child of EmailTemplate, after an EmailDivider.", + "Use EmailArticle for blog post or newsletter article blocks with hero image, category, title, description, and CTA.", + "Use EmailProductCard for product showcases with image, title, description, price, and buy button.", + "Use EmailFeatureGrid for a 2x2 grid of features with icons. Provide exactly 4 EmailFeatureItem children.", + "Use EmailFeatureList for a vertical list of features with icons and dividers. Any number of EmailFeatureItem children.", + "Use EmailNumberedSteps for step-by-step guides. Steps are auto-numbered. Provide EmailStepItem children.", + "Use EmailCheckoutTable for cart/order summary tables. Provide EmailCheckoutItem children with product details.", + "Use EmailPricingCard for pricing plans with feature lists. Provide EmailPricingFeature children for each feature line.", + "Use EmailTestimonial for customer quotes with avatar, name, and role.", + "Use EmailSurveyRating for feedback/NPS emails with a 1-5 rating scale.", + "Use EmailStats for displaying key metrics. Provide EmailStatItem children with value and label.", + "Use EmailImageGrid for a 2x2 image gallery. Provide up to 4 EmailImage children.", + "Use EmailAvatar for a single avatar image. Set rounded='full' for circular or rounded='md' for rounded-square. Default size is 42px.", + "Use EmailAvatarGroup for overlapping stacked avatars. Provide EmailAvatar children. Great for showing team members or participants.", + "Use EmailAvatarWithText for an avatar with name and role text. Optionally wrap in a link with href. Great for author attribution in articles.", + "Use EmailList for numbered lists with circular badges. Provide EmailListItem children with title and description.", + "Use EmailCustomerReview for a star rating distribution summary. Provide counts for each rating level (rating1-rating5) and totalReviews.", + "Use EmailBentoGrid for a bento-style marketing layout. Provide a dark hero section and EmailBentoItem children for product cards below.", +]; + +// ── Prompt options (email only) ── + +export const emailPromptOptions: PromptOptions = { + examples: emailExamples, + additionalRules: emailAdditionalRules, +}; + +// ── Ready-to-use library ── + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const emailLibrary = createLibrary({ + root: "EmailTemplate", + componentGroups: emailComponentGroups, + components: [ + EmailTemplate, + EmailSection, + EmailColumns, + EmailColumn, + EmailHeaderSideNav, + EmailHeaderCenteredNav, + EmailHeaderSocial, + EmailNavLink, + EmailSocialIcon, + EmailFooterCentered, + EmailFooterTwoColumn, + EmailHeading, + EmailText, + EmailButton, + EmailImage, + EmailDivider, + EmailLink, + EmailCodeBlock, + EmailCodeInline, + EmailMarkdown, + EmailArticle, + EmailProductCard, + EmailFeatureItem, + EmailFeatureGrid, + EmailFeatureList, + EmailStepItem, + EmailNumberedSteps, + EmailCheckoutItem, + EmailCheckoutTable, + EmailPricingFeature, + EmailPricingCard, + EmailTestimonial, + EmailSurveyRating, + EmailStatItem, + EmailStats, + EmailImageGrid, + EmailAvatar, + EmailAvatarGroup, + EmailAvatarWithText, + EmailListItem, + EmailList, + EmailCustomerReview, + EmailBentoItem, + EmailBentoGrid, + ] as any[], +}); diff --git a/packages/react-email/src/unions.ts b/packages/react-email/src/unions.ts new file mode 100644 index 000000000..96d8068e5 --- /dev/null +++ b/packages/react-email/src/unions.ts @@ -0,0 +1,54 @@ +import { z } from "zod"; +import { EmailArticle } from "./components/Article"; +import { EmailAvatarGroup } from "./components/AvatarGroup"; +import { EmailAvatarWithText } from "./components/AvatarWithText"; +import { EmailBentoGrid } from "./components/BentoGrid"; +import { EmailButton } from "./components/Button"; +import { EmailCheckoutTable } from "./components/CheckoutTable"; +import { EmailCodeBlock } from "./components/CodeBlock"; +import { EmailCodeInline } from "./components/CodeInline"; +import { EmailCustomerReview } from "./components/CustomerReview"; +import { EmailDivider } from "./components/Divider"; +import { EmailFeatureGrid } from "./components/FeatureGrid"; +import { EmailFeatureList } from "./components/FeatureList"; +import { EmailHeading } from "./components/Heading"; +import { EmailImage } from "./components/Image"; +import { EmailImageGrid } from "./components/ImageGrid"; +import { EmailLink } from "./components/Link"; +import { EmailList } from "./components/List"; +import { EmailMarkdown } from "./components/Markdown"; +import { EmailNumberedSteps } from "./components/NumberedSteps"; +import { EmailPricingCard } from "./components/PricingCard"; +import { EmailProductCard } from "./components/ProductCard"; +import { EmailStats } from "./components/Stats"; +import { EmailSurveyRating } from "./components/SurveyRating"; +import { EmailTestimonial } from "./components/Testimonial"; +import { EmailText } from "./components/Text"; + +export const EmailLeafChildUnion = z.union([ + EmailHeading.ref, + EmailText.ref, + EmailButton.ref, + EmailImage.ref, + EmailDivider.ref, + EmailLink.ref, + EmailCodeBlock.ref, + EmailCodeInline.ref, + EmailMarkdown.ref, + EmailArticle.ref, + EmailProductCard.ref, + EmailFeatureGrid.ref, + EmailFeatureList.ref, + EmailNumberedSteps.ref, + EmailCheckoutTable.ref, + EmailPricingCard.ref, + EmailTestimonial.ref, + EmailSurveyRating.ref, + EmailStats.ref, + EmailImageGrid.ref, + EmailAvatarGroup.ref, + EmailAvatarWithText.ref, + EmailList.ref, + EmailCustomerReview.ref, + EmailBentoGrid.ref, +]); diff --git a/packages/react-email/tsconfig.build.json b/packages/react-email/tsconfig.build.json new file mode 100644 index 000000000..78e7883b6 --- /dev/null +++ b/packages/react-email/tsconfig.build.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/react-email/tsconfig.json b/packages/react-email/tsconfig.json new file mode 100644 index 000000000..6021a9fde --- /dev/null +++ b/packages/react-email/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2017", + "lib": ["dom", "dom.iterable", "esnext"], + "module": "esnext", + "moduleResolution": "bundler", + "declaration": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + "skipLibCheck": true + }, + "include": ["src"], + "exclude": ["node_modules", "dist"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c6eddd5f3..72c1993ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -64,7 +64,7 @@ importers: version: 16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6) fumadocs-mdx: specifier: 14.2.8 - version: 14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0)) + version: 14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(sass@1.89.2)) fumadocs-ui: specifier: 16.6.5 version: 16.6.5(@takumi-rs/image-response@0.68.17)(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(tailwindcss@4.2.1) @@ -261,6 +261,70 @@ importers: specifier: ~5.9.2 version: 5.9.3 + examples/react-email: + dependencies: + '@openuidev/cli': + specifier: workspace:* + version: link:../../packages/openui-cli + '@openuidev/react-email': + specifier: workspace:* + version: link:../../packages/react-email + '@openuidev/react-headless': + specifier: workspace:* + version: link:../../packages/react-headless + '@openuidev/react-lang': + specifier: workspace:* + version: link:../../packages/react-lang + '@react-email/components': + specifier: ^0.0.41 + version: 0.0.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-email/render': + specifier: ^1.0.6 + version: 1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + lucide-react: + specifier: ^0.562.0 + version: 0.562.0(react@19.2.3) + next: + specifier: 16.1.6 + version: 16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(sass@1.89.2) + openai: + specifier: ^6.22.0 + version: 6.22.0(ws@8.18.2)(zod@4.3.6) + react: + specifier: 19.2.3 + version: 19.2.3 + react-dom: + specifier: 19.2.3 + version: 19.2.3(react@19.2.3) + devDependencies: + '@tailwindcss/postcss': + specifier: ^4 + version: 4.2.1 + '@types/node': + specifier: ^20 + version: 20.19.35 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + eslint: + specifier: ^9 + version: 9.29.0(jiti@2.6.1) + eslint-config-next: + specifier: 16.1.6 + version: 16.1.6(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3) + tailwindcss: + specifier: ^4 + version: 4.2.1 + tsx: + specifier: ^4.19.2 + version: 4.20.3 + typescript: + specifier: ^5 + version: 5.9.3 + examples/shadcn-chat: dependencies: '@openuidev/react-headless': @@ -501,6 +565,31 @@ importers: specifier: ^22.15.32 version: 22.15.32 + packages/react-email: + dependencies: + '@openuidev/react-lang': + specifier: workspace:* + version: link:../react-lang + '@react-email/components': + specifier: ^0.0.41 + version: 0.0.41(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react: + specifier: '>=19.0.0' + version: 19.2.4 + react-dom: + specifier: '>=19.0.0' + version: 19.2.4(react@19.2.4) + zod: + specifier: ^4.3.6 + version: 4.3.6 + devDependencies: + '@types/react': + specifier: ^19 + version: 19.2.14 + typescript: + specifier: ^5 + version: 5.9.3 + packages/react-headless: dependencies: '@ag-ui/core': @@ -3777,6 +3866,138 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-email/body@0.0.11': + resolution: {integrity: sha512-ZSD2SxVSgUjHGrB0Wi+4tu3MEpB4fYSbezsFNEJk2xCWDBkFiOeEsjTmR5dvi+CxTK691hQTQlHv0XWuP7ENTg==} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/button@0.0.19': + resolution: {integrity: sha512-HYHrhyVGt7rdM/ls6FuuD6XE7fa7bjZTJqB2byn6/oGsfiEZaogY77OtoLL/mrQHjHjZiJadtAMSik9XLcm7+A==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/code-block@0.0.13': + resolution: {integrity: sha512-4DE4yPSgKEOnZMzcrDvRuD6mxsNxOex0hCYEG9F9q23geYgb2WCCeGBvIUXVzK69l703Dg4Vzrd5qUjl+JfcwA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/code-inline@0.0.5': + resolution: {integrity: sha512-MmAsOzdJpzsnY2cZoPHFPk6uDO/Ncpb4Kh1hAt9UZc1xOW3fIzpe1Pi9y9p6wwUmpaeeDalJxAxH6/fnTquinA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/column@0.0.13': + resolution: {integrity: sha512-Lqq17l7ShzJG/d3b1w/+lVO+gp2FM05ZUo/nW0rjxB8xBICXOVv6PqjDnn3FXKssvhO5qAV20lHM6S+spRhEwQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/components@0.0.41': + resolution: {integrity: sha512-WUI3wHwra3QS0pwrovSU6b0I0f3TvY33ph0y44LuhSYDSQlMRyeOzgoT6HRDY5FXMDF57cHYq9WoKwpwP0yd7Q==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/container@0.0.15': + resolution: {integrity: sha512-Qo2IQo0ru2kZq47REmHW3iXjAQaKu4tpeq/M8m1zHIVwKduL2vYOBQWbC2oDnMtWPmkBjej6XxgtZByxM6cCFg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/font@0.0.9': + resolution: {integrity: sha512-4zjq23oT9APXkerqeslPH3OZWuh5X4crHK6nx82mVHV2SrLba8+8dPEnWbaACWTNjOCbcLIzaC9unk7Wq2MIXw==} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/head@0.0.12': + resolution: {integrity: sha512-X2Ii6dDFMF+D4niNwMAHbTkeCjlYYnMsd7edXOsi0JByxt9wNyZ9EnhFiBoQdqkE+SMDcu8TlNNttMrf5sJeMA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/heading@0.0.15': + resolution: {integrity: sha512-xF2GqsvBrp/HbRHWEfOgSfRFX+Q8I5KBEIG5+Lv3Vb2R/NYr0s8A5JhHHGf2pWBMJdbP4B2WHgj/VUrhy8dkIg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/hr@0.0.11': + resolution: {integrity: sha512-S1gZHVhwOsd1Iad5IFhpfICwNPMGPJidG/Uysy1AwmspyoAP5a4Iw3OWEpINFdgh9MHladbxcLKO2AJO+cA9Lw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/html@0.0.11': + resolution: {integrity: sha512-qJhbOQy5VW5qzU74AimjAR9FRFQfrMa7dn4gkEXKMB/S9xZN8e1yC1uA9C15jkXI/PzmJ0muDIWmFwatm5/+VA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/img@0.0.11': + resolution: {integrity: sha512-aGc8Y6U5C3igoMaqAJKsCpkbm1XjguQ09Acd+YcTKwjnC2+0w3yGUJkjWB2vTx4tN8dCqQCXO8FmdJpMfOA9EQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/link@0.0.12': + resolution: {integrity: sha512-vF+xxQk2fGS1CN7UPQDbzvcBGfffr+GjTPNiWM38fhBfsLv6A/YUfaqxWlmL7zLzVmo0K2cvvV9wxlSyNba1aQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/markdown@0.0.15': + resolution: {integrity: sha512-UQA9pVm5sbflgtg3EX3FquUP4aMBzmLReLbGJ6DZQZnAskBF36aI56cRykDq1o+1jT+CKIK1CducPYziaXliag==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/preview@0.0.13': + resolution: {integrity: sha512-F7j9FJ0JN/A4d7yr+aw28p4uX7VLWs7hTHtLo7WRyw4G+Lit6Zucq4UWKRxJC8lpsUdzVmG7aBJnKOT+urqs/w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/render@1.1.2': + resolution: {integrity: sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/render@1.4.0': + resolution: {integrity: sha512-ZtJ3noggIvW1ZAryoui95KJENKdCzLmN5F7hyZY1F/17B1vwzuxHB7YkuCg0QqHjDivc5axqYEYdIOw4JIQdUw==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/row@0.0.12': + resolution: {integrity: sha512-HkCdnEjvK3o+n0y0tZKXYhIXUNPDx+2vq1dJTmqappVHXS5tXS6W5JOPZr5j+eoZ8gY3PShI2LWj5rWF7ZEtIQ==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/section@0.0.16': + resolution: {integrity: sha512-FjqF9xQ8FoeUZYKSdt8sMIKvoT9XF8BrzhT3xiFKdEMwYNbsDflcjfErJe3jb7Wj/es/lKTbV5QR1dnLzGpL3w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/tailwind@1.0.5': + resolution: {integrity: sha512-BH00cZSeFfP9HiDASl+sPHi7Hh77W5nzDgdnxtsVr/m3uQD9g180UwxcE3PhOfx0vRdLzQUU8PtmvvDfbztKQg==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + + '@react-email/text@0.1.4': + resolution: {integrity: sha512-cMNE02y8172DocpNGh97uV5HSTawaS4CKG/zOku8Pu+m6ehBKbAjgtQZDIxhgstw8+TWraFB8ltS1DPjfG8nLA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^18.0 || ^19.0 || ^19.0.0-rc + '@react-native/assets-registry@0.83.2': resolution: {integrity: sha512-9I5l3pGAKnlpQ15uVkeB9Mgjvt3cZEaEc8EDtdexvdtZvLSjtwBzgourrOW4yZUijbjJr8h3YO2Y0q+THwUHTA==} engines: {node: '>= 20.19.4'} @@ -3975,6 +4196,9 @@ packages: '@rtsao/scc@1.1.0': resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} + '@selderee/plugin-htmlparser2@0.11.0': + resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} + '@shikijs/core@3.23.0': resolution: {integrity: sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==} @@ -6123,6 +6347,9 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + fast-deep-equal@2.0.1: + resolution: {integrity: sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -6606,12 +6833,19 @@ packages: resolution: {integrity: sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} + html-to-text@9.0.5: + resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==} + engines: {node: '>=14'} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} html-void-elements@3.0.0: resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + http-errors@2.0.1: resolution: {integrity: sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==} engines: {node: '>= 0.8'} @@ -7022,6 +7256,9 @@ packages: resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} engines: {node: '>=0.10'} + leac@0.6.0: + resolution: {integrity: sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==} + leven@3.1.0: resolution: {integrity: sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==} engines: {node: '>=6'} @@ -7209,6 +7446,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@7.0.4: + resolution: {integrity: sha512-t8eP0dXRJMtMvBojtkcsA7n48BkauktUKzfkPSCq85ZMTJ0v76Rke4DYz01omYpPTUh4p/f7HePgRo3ebG8+QQ==} + engines: {node: '>= 16'} + hasBin: true + marky@1.3.0: resolution: {integrity: sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==} @@ -7216,6 +7458,11 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} + md-to-react-email@5.0.5: + resolution: {integrity: sha512-OvAXqwq57uOk+WZqFFNCMZz8yDp8BD3WazW1wAKHUrPbbdr89K9DWS6JXY09vd9xNdPNeurI8DU/X4flcfaD8A==} + peerDependencies: + react: ^18.0 || ^19.0 + mdast-util-find-and-replace@3.0.2: resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} @@ -7907,6 +8154,9 @@ packages: parse5@8.0.0: resolution: {integrity: sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==} + parseley@0.12.1: + resolution: {integrity: sha512-e6qHKe3a9HWr0oMRVDTRhKce+bRO8VGQR3NyVwcjwrbhMmFCX9KszEV35+rn4AdilFAq9VPxP/Fe1wC9Qjd2lw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -7944,6 +8194,9 @@ packages: resolution: {integrity: sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==} engines: {node: '>= 14.16'} + peberminta@0.9.0: + resolution: {integrity: sha512-XIxfHpEuSJbITd1H3EeQwpcZbTLHc+VVr8ANI9t5sit565tsI4/xK3KWTUFE2e6QiangUkh3B0jihzmGnNrRsQ==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -8274,6 +8527,9 @@ packages: '@types/react': optional: true + react-promise-suspense@0.3.4: + resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==} + react-refresh@0.14.2: resolution: {integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==} engines: {node: '>=0.10.0'} @@ -8588,6 +8844,9 @@ packages: scroll-into-view-if-needed@3.1.0: resolution: {integrity: sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==} + selderee@0.11.0: + resolution: {integrity: sha512-5TF+l7p4+OsnP8BCCvSyZiSPc4x4//p5uPwK8TCnVPJYRmU2aYKMpOXvw8zM5a5JvuuCGN1jmsMwuU2W02ukfA==} + semver@6.3.1: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true @@ -13390,6 +13649,238 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-email/body@0.0.11(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/body@0.0.11(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/button@0.0.19(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/button@0.0.19(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/code-block@0.0.13(react@19.2.3)': + dependencies: + prismjs: 1.30.0 + react: 19.2.3 + + '@react-email/code-block@0.0.13(react@19.2.4)': + dependencies: + prismjs: 1.30.0 + react: 19.2.4 + + '@react-email/code-inline@0.0.5(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/code-inline@0.0.5(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/column@0.0.13(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/column@0.0.13(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/components@0.0.41(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@react-email/body': 0.0.11(react@19.2.3) + '@react-email/button': 0.0.19(react@19.2.3) + '@react-email/code-block': 0.0.13(react@19.2.3) + '@react-email/code-inline': 0.0.5(react@19.2.3) + '@react-email/column': 0.0.13(react@19.2.3) + '@react-email/container': 0.0.15(react@19.2.3) + '@react-email/font': 0.0.9(react@19.2.3) + '@react-email/head': 0.0.12(react@19.2.3) + '@react-email/heading': 0.0.15(react@19.2.3) + '@react-email/hr': 0.0.11(react@19.2.3) + '@react-email/html': 0.0.11(react@19.2.3) + '@react-email/img': 0.0.11(react@19.2.3) + '@react-email/link': 0.0.12(react@19.2.3) + '@react-email/markdown': 0.0.15(react@19.2.3) + '@react-email/preview': 0.0.13(react@19.2.3) + '@react-email/render': 1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@react-email/row': 0.0.12(react@19.2.3) + '@react-email/section': 0.0.16(react@19.2.3) + '@react-email/tailwind': 1.0.5(react@19.2.3) + '@react-email/text': 0.1.4(react@19.2.3) + react: 19.2.3 + transitivePeerDependencies: + - react-dom + + '@react-email/components@0.0.41(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@react-email/body': 0.0.11(react@19.2.4) + '@react-email/button': 0.0.19(react@19.2.4) + '@react-email/code-block': 0.0.13(react@19.2.4) + '@react-email/code-inline': 0.0.5(react@19.2.4) + '@react-email/column': 0.0.13(react@19.2.4) + '@react-email/container': 0.0.15(react@19.2.4) + '@react-email/font': 0.0.9(react@19.2.4) + '@react-email/head': 0.0.12(react@19.2.4) + '@react-email/heading': 0.0.15(react@19.2.4) + '@react-email/hr': 0.0.11(react@19.2.4) + '@react-email/html': 0.0.11(react@19.2.4) + '@react-email/img': 0.0.11(react@19.2.4) + '@react-email/link': 0.0.12(react@19.2.4) + '@react-email/markdown': 0.0.15(react@19.2.4) + '@react-email/preview': 0.0.13(react@19.2.4) + '@react-email/render': 1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@react-email/row': 0.0.12(react@19.2.4) + '@react-email/section': 0.0.16(react@19.2.4) + '@react-email/tailwind': 1.0.5(react@19.2.4) + '@react-email/text': 0.1.4(react@19.2.4) + react: 19.2.4 + transitivePeerDependencies: + - react-dom + + '@react-email/container@0.0.15(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/container@0.0.15(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/font@0.0.9(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/font@0.0.9(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/head@0.0.12(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/head@0.0.12(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/heading@0.0.15(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/heading@0.0.15(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/hr@0.0.11(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/hr@0.0.11(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/html@0.0.11(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/html@0.0.11(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/img@0.0.11(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/img@0.0.11(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/link@0.0.12(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/link@0.0.12(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/markdown@0.0.15(react@19.2.3)': + dependencies: + md-to-react-email: 5.0.5(react@19.2.3) + react: 19.2.3 + + '@react-email/markdown@0.0.15(react@19.2.4)': + dependencies: + md-to-react-email: 5.0.5(react@19.2.4) + react: 19.2.4 + + '@react-email/preview@0.0.13(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/preview@0.0.13(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/render@1.1.2(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.5.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-promise-suspense: 0.3.4 + + '@react-email/render@1.1.2(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.5.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + react-promise-suspense: 0.3.4 + + '@react-email/render@1.4.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + html-to-text: 9.0.5 + prettier: 3.5.3 + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + react-promise-suspense: 0.3.4 + + '@react-email/row@0.0.12(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/row@0.0.12(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/section@0.0.16(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/section@0.0.16(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/tailwind@1.0.5(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/tailwind@1.0.5(react@19.2.4)': + dependencies: + react: 19.2.4 + + '@react-email/text@0.1.4(react@19.2.3)': + dependencies: + react: 19.2.3 + + '@react-email/text@0.1.4(react@19.2.4)': + dependencies: + react: 19.2.4 + '@react-native/assets-registry@0.83.2': {} '@react-native/babel-plugin-codegen@0.81.5(@babel/core@7.29.0)': @@ -13619,6 +14110,11 @@ snapshots: '@rtsao/scc@1.1.0': {} + '@selderee/plugin-htmlparser2@0.11.0': + dependencies: + domhandler: 5.0.3 + selderee: 0.11.0 + '@shikijs/core@3.23.0': dependencies: '@shikijs/types': 3.23.0 @@ -15757,7 +16253,7 @@ snapshots: dependencies: debug: 3.2.7 is-core-module: 2.16.1 - resolve: 1.22.10 + resolve: 1.22.11 transitivePeerDependencies: - supports-color @@ -15776,7 +16272,7 @@ snapshots: transitivePeerDependencies: - supports-color - eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)): + eslint-module-utils@2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)): dependencies: debug: 3.2.7 optionalDependencies: @@ -15798,7 +16294,7 @@ snapshots: doctrine: 2.1.0 eslint: 9.29.0(jiti@2.6.1) eslint-import-resolver-node: 0.3.9 - eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.29.0(jiti@2.6.1)) + eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.56.1(eslint@9.29.0(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.29.0(jiti@2.6.1)))(eslint@9.29.0(jiti@2.6.1)) hasown: 2.0.2 is-core-module: 2.16.1 is-glob: 4.0.3 @@ -16123,6 +16619,8 @@ snapshots: extend@3.0.2: {} + fast-deep-equal@2.0.1: {} + fast-deep-equal@3.1.3: {} fast-diff@1.3.0: {} @@ -16321,7 +16819,7 @@ snapshots: transitivePeerDependencies: - supports-color - fumadocs-mdx@14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(lightningcss@1.31.1)(sass@1.89.2)(terser@5.43.0)(tsx@4.20.3)(yaml@2.8.0)): + fumadocs-mdx@14.2.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.14)(fumadocs-core@16.6.5(@mdx-js/mdx@3.1.1)(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.14)(lucide-react@0.570.0(react@19.2.4))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(zod@4.3.6))(next@16.1.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(babel-plugin-react-compiler@1.0.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)(sass@1.89.2))(react@19.2.4)(vite@7.3.1(@types/node@25.3.2)(jiti@2.6.1)(sass@1.89.2)): dependencies: '@mdx-js/mdx': 3.1.1 '@standard-schema/spec': 1.1.0 @@ -16710,10 +17208,25 @@ snapshots: - '@noble/hashes' optional: true + html-to-text@9.0.5: + dependencies: + '@selderee/plugin-htmlparser2': 0.11.0 + deepmerge: 4.3.1 + dom-serializer: 2.0.0 + htmlparser2: 8.0.2 + selderee: 0.11.0 + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} + htmlparser2@8.0.2: + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.2.2 + entities: 4.5.0 + http-errors@2.0.1: dependencies: depd: 2.0.0 @@ -17170,6 +17683,8 @@ snapshots: dependencies: language-subtag-registry: 0.3.23 + leac@0.6.0: {} + leven@3.1.0: {} levn@0.4.1: @@ -17284,6 +17799,10 @@ snapshots: dependencies: yallist: 3.1.1 + lucide-react@0.562.0(react@19.2.3): + dependencies: + react: 19.2.3 + lucide-react@0.562.0(react@19.2.4): dependencies: react: 19.2.4 @@ -17320,10 +17839,22 @@ snapshots: markdown-table@3.0.4: {} + marked@7.0.4: {} + marky@1.3.0: {} math-intrinsics@1.1.0: {} + md-to-react-email@5.0.5(react@19.2.3): + dependencies: + marked: 7.0.4 + react: 19.2.3 + + md-to-react-email@5.0.5(react@19.2.4): + dependencies: + marked: 7.0.4 + react: 19.2.4 + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 @@ -18557,6 +19088,11 @@ snapshots: entities: 6.0.1 optional: true + parseley@0.12.1: + dependencies: + leac: 0.6.0 + peberminta: 0.9.0 + parseurl@1.3.3: {} path-exists@4.0.0: {} @@ -18583,6 +19119,8 @@ snapshots: pathval@2.0.0: {} + peberminta@0.9.0: {} + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -19048,6 +19586,10 @@ snapshots: - supports-color - utf-8-validate + react-promise-suspense@0.3.4: + dependencies: + fast-deep-equal: 2.0.1 + react-refresh@0.14.2: {} react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.3): @@ -19561,6 +20103,10 @@ snapshots: dependencies: compute-scroll-into-view: 3.1.1 + selderee@0.11.0: + dependencies: + parseley: 0.12.1 + semver@6.3.1: {} semver@7.7.2: {}