react-email-locale-lab 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,50 @@
1
1
  # React Email Locale Lab
2
2
 
3
- React Email Locale Lab is a proof of concept for previewing React Email templates in multiple languages while editing them.
3
+ React Email Locale Lab is a development utility for previewing [React Email](https://react.email/) templates in multiple languages while editing them.
4
4
 
5
- The idea came from an internal need I encountered while working at one of the companies in my career. We needed a faster way to understand how email templates behaved across languages, especially when translated text changed the size, spacing or structure of a layout.
5
+ [Live demo](https://react-email-locale-lab-integration.vercel.app/) · [Integration example](https://github.com/hducati/react-email-locale-lab-integration-example)
6
6
 
7
- This project exists to test the technical and product viability of that workflow.
7
+ I developed this library after encountering this need while working at an organization that used React Email templates in multiple languages. The templates were usually created first in English, but we also needed to understand how the same layout behaved in languages such as German and Russian, where translated text could change line wrapping, spacing and element dimensions.
8
8
 
9
- ## Install
9
+ During local development, the library renders a React Email template and generates on-demand previews for the selected languages. When the source template changes, Vite refreshes it and the selected previews are generated again, allowing the versions to be inspected side by side in near real time.
10
+
11
+ The generated translations are intended only for visual inspection of the templates. The library does not replace the localization workflow or the reviewed translations used by the application.
12
+
13
+ ## Quick start
10
14
 
11
15
  ```bash
12
16
  pnpm add -D react-email-locale-lab
13
17
  ```
14
18
 
15
- The package expects React 19 and React DOM 19 as peer dependencies.
19
+ The package supports React and React DOM 18 or 19 and does not require Vite. The following quick start uses Vite as an optional development host:
16
20
 
17
- Create a configuration in the consuming project:
21
+ ```bash
22
+ pnpm add -D vite
23
+ ```
24
+
25
+ ```html
26
+ <!-- email-lab/index.html -->
27
+ <div id="root"></div>
28
+ <script type="module" src="/main.tsx"></script>
29
+ ```
18
30
 
19
31
  ```tsx
32
+ // email-lab/main.tsx
33
+ /// <reference types="vite/client" />
34
+
35
+ import { createRoot } from 'react-dom/client';
20
36
  import {
21
37
  browserTranslatorProvider,
22
38
  defineEmailLab,
23
39
  EmailLabApp,
24
40
  } from 'react-email-locale-lab';
41
+ import { viteSourceUpdates } from 'react-email-locale-lab/vite';
25
42
  import 'react-email-locale-lab/styles.css';
43
+ import { WelcomeEmail } from '../src/emails/welcome';
26
44
 
27
45
  const config = defineEmailLab({
28
46
  routeBasePath: '/preview',
47
+ sourceUpdates: viteSourceUpdates(import.meta.hot, { watchPaths: ['src/emails'] }),
29
48
  sourceLocale: { code: 'en', label: 'English' },
30
49
  locales: [{ code: 'de', label: 'Deutsch' }],
31
50
  provider: browserTranslatorProvider(),
@@ -34,27 +53,33 @@ const config = defineEmailLab({
34
53
  },
35
54
  });
36
55
 
37
- export const App = () => <EmailLabApp config={config} />;
56
+ createRoot(document.getElementById('root')!).render(<EmailLabApp config={config} />);
38
57
  ```
39
58
 
40
- Import `react-email-locale-lab/styles.css` once in the consuming application. During local development of this repository, `pnpm pack` can be used to create an installable tarball before publishing to npm.
59
+ Add a script and start the lab:
41
60
 
42
- ## Run
61
+ ```json
62
+ {
63
+ "scripts": {
64
+ "email:lab": "vite email-lab --port 4174"
65
+ }
66
+ }
67
+ ```
43
68
 
44
69
  ```bash
45
- pnpm install
46
- pnpm dev
70
+ pnpm email:lab
47
71
  ```
48
72
 
49
- Open `http://localhost:4173/preview/welcome`, select up to three languages, then edit `src/emails/welcome.tsx`. Vite refreshes the source and the selected locale cards transition through `stale → translating → ready`.
73
+ Open `http://localhost:4174/preview/welcome`, select up to three languages, then edit the template. Vite refreshes the selected previews automatically.
50
74
 
51
75
  ## Configuration interface
52
76
 
53
- `src/email-lab.config.tsx` defines only the source locale, available target locales, templates, and translation provider. There are no manually maintained translations. The user selects zero to three languages in the UI; translation is not initialized during app startup.
77
+ The lab configuration defines only the source locale, available target locales, templates, and translation provider. There are no manually maintained translations. The user selects zero to three languages in the UI; translation is not initialized during app startup.
54
78
 
55
79
  ```tsx
56
80
  export default defineEmailLab({
57
81
  routeBasePath: '/preview',
82
+ sourceUpdates: viteSourceUpdates(import.meta.hot, { watchPaths: ['src/emails'] }),
58
83
  sourceLocale: { code: 'en', label: 'English' },
59
84
  locales: [
60
85
  { code: 'de', label: 'Deutsch' },
@@ -79,6 +104,37 @@ templates: {
79
104
 
80
105
  Set `props` on the template entry to override `PreviewProps`. A custom `render` function remains available for templates that need bespoke setup.
81
106
 
107
+ ### Template requirements
108
+
109
+ The lab renders templates synchronously to static HTML. Prefer presentation-only email components whose output is determined by props. Pass preview data through `PreviewProps` or `props`; fetch production data before rendering the email.
110
+
111
+ Data fetching in `useEffect`, async/Server Components, application routes and components that require missing providers may not render as expected. Use `render` to add synchronous providers or wrappers:
112
+
113
+ ```tsx
114
+ templates: {
115
+ welcome: {
116
+ name: 'Welcome',
117
+ render: () => <WelcomeEmail />,
118
+ },
119
+ }
120
+ ```
121
+
122
+ ## Template locations and source updates
123
+
124
+ Templates can be imported from anywhere in the consuming project. They do not need to live in `src/emails` or follow a specific folder structure.
125
+
126
+ The core library is bundler-agnostic. Without `sourceUpdates`, previews still work and can be regenerated manually. Vite users can opt into HMR updates and restrict them to path fragments:
127
+
128
+ ```tsx
129
+ import { viteSourceUpdates } from 'react-email-locale-lab/vite';
130
+
131
+ sourceUpdates: viteSourceUpdates(import.meta.hot, {
132
+ watchPaths: ['src/message-templates', 'packages/notifications'],
133
+ }),
134
+ ```
135
+
136
+ Omit `watchPaths` to react to any JavaScript or TypeScript module update. Other tools can implement the small `SourceUpdates` subscription interface and pass it through `sourceUpdates`.
137
+
82
138
  ## Template routes
83
139
 
84
140
  Every configured template has a stable preview route:
@@ -103,10 +159,12 @@ nl no pl pt ro ru sk sl sv ta te th tr uk vi zh zh-Hant
103
159
 
104
160
  The list is provider-specific and may change. The provider checks every source/target pair with `Translator.availability()` at runtime. Chrome's API currently runs on desktop, downloads language packs on demand and is unavailable on mobile. See the [official Translator API language list](https://developer.chrome.com/docs/ai/translator-api#supported-languages).
105
161
 
106
- The provider receives extracted text nodes and translatable attributes (`alt`, `title`, `aria-label`). When source copy changes, every target preview is marked stale and new or changed messages are automatically translated again. Unchanged messages are reused from the in-memory cache.
162
+ The provider receives extracted text nodes and translatable attributes (`alt`, `title`, `aria-label`). When source copy changes, every target preview is marked stale and new or changed messages are automatically translated again. Unchanged messages are reused from the session cache.
163
+
164
+ The included provider uses the browser's built-in Translator API. Language packs are lazy-loaded per selected pair, and translated messages are cached by source text and locale in session storage so unchanged copy is reused across Vite reloads. The source template does not leave the browser. In a browser without this API, the preview reports an actionable error instead of downloading a large JavaScript model or freezing startup.
107
165
 
108
- The included provider uses the browser's built-in Translator API. Language packs are lazy-loaded per selected pair, and translated messages are cached by source text and locale. The source template does not leave the browser. In a browser without this API, the preview reports an actionable error instead of downloading a large JavaScript model or freezing startup.
166
+ Translated previews set both the BCP 47 `lang` attribute and the corresponding `dir` attribute on the rendered document. Right-to-left locales such as Arabic and Hebrew therefore render with `dir="rtl"`; other target locales explicitly use `dir="ltr"`.
109
167
 
110
168
  ## Current scope
111
169
 
112
- React Email Locale Lab is currently a POC for validating the developer experience, not a production localization system. It automatically translates rendered text for preview only. Future iterations may protect placeholders, expose language-pack progress and offer an explicitly configured server provider for teams whose browsers do not support on-device translation.
170
+ React Email Locale Lab is currently a development-time proof of concept. It automatically translates rendered React Email content for visual preview only; it is not a production localization system or a replacement for testing in real email clients. Future iterations may protect placeholders, improve RTL coverage, expose language-pack progress and support additional translation providers.
@@ -0,0 +1,14 @@
1
+ import type { SourceUpdates } from '../core/types';
2
+ type ViteUpdatePayload = {
3
+ updates: Array<{
4
+ path: string;
5
+ }>;
6
+ };
7
+ type ViteHotContext = {
8
+ on: (event: 'vite:afterUpdate', listener: (payload: ViteUpdatePayload) => void) => void;
9
+ off: (event: 'vite:afterUpdate', listener: (payload: ViteUpdatePayload) => void) => void;
10
+ };
11
+ export declare const viteSourceUpdates: (hot: ViteHotContext | undefined, options?: {
12
+ watchPaths?: string[];
13
+ }) => SourceUpdates;
14
+ export {};
@@ -1,3 +1,4 @@
1
+ export declare const directionForLocale: (locale: string) => "ltr" | "rtl";
1
2
  export type MessageSlot = {
2
3
  value: string;
3
4
  apply: (translated: string) => void;
@@ -27,11 +27,15 @@ export type TranslationProvider = {
27
27
  name: string;
28
28
  translate: (request: TranslationRequest) => Promise<string[]>;
29
29
  };
30
+ export type SourceUpdates = {
31
+ subscribe: (onUpdate: () => void) => () => void;
32
+ };
30
33
  export type EmailLabConfig = {
31
34
  sourceLocale: Locale;
32
35
  locales: Locale[];
33
36
  templates: Record<string, EmailTemplate>;
34
37
  provider: TranslationProvider;
35
38
  routeBasePath?: string;
39
+ sourceUpdates?: SourceUpdates;
36
40
  };
37
41
  export declare const defineEmailLab: (config: EmailLabConfig) => EmailLabConfig;
@@ -0,0 +1 @@
1
+ export declare const shouldReloadForUpdates: (updatePaths: string[], watchPaths?: string[]) => boolean;
package/dist/index.d.ts CHANGED
@@ -2,4 +2,4 @@ import './styles.css';
2
2
  export { EmailLabApp } from './App';
3
3
  export { browserTranslatorProvider } from './core/browser-translator-provider';
4
4
  export { defineEmailLab } from './core/types';
5
- export type { EmailLabConfig, EmailTemplate, Locale, TranslationProvider, TranslationRequest, } from './core/types';
5
+ export type { EmailLabConfig, EmailTemplate, Locale, SourceUpdates, TranslationProvider, TranslationRequest, } from './core/types';
package/dist/index.js CHANGED
@@ -1,47 +1,67 @@
1
- import { jsxs as d, jsx as c } from "react/jsx-runtime";
2
- import { createElement as I, useMemo as A, useState as b, useEffect as T } from "react";
3
- import { renderToStaticMarkup as M } from "react-dom/server";
4
- const k = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").test(e), j = (e) => {
1
+ import { jsxs as d, jsx as i } from "react/jsx-runtime";
2
+ import { createElement as B, useMemo as R, useState as L, useEffect as S } from "react";
3
+ import { renderToStaticMarkup as F } from "react-dom/server";
4
+ const _ = ["alt", "title", "aria-label"], O = /* @__PURE__ */ new Set([
5
+ "ar",
6
+ "arc",
7
+ "ckb",
8
+ "dv",
9
+ "fa",
10
+ "he",
11
+ "khw",
12
+ "ks",
13
+ "ku",
14
+ "nqo",
15
+ "ps",
16
+ "sd",
17
+ "syr",
18
+ "ug",
19
+ "ur",
20
+ "yi"
21
+ ]), H = /* @__PURE__ */ new Set(["adlm", "arab", "hebr", "mand", "nkoo", "rohg", "samr", "syrc", "thaa"]), j = (e) => {
22
+ const t = e.trim().split(/[-_]/), r = t[0].toLowerCase(), s = t.findIndex((o, c) => c > 0 && /^[a-z0-9]$/i.test(o)), a = t.slice(1, s === -1 ? void 0 : s).find((o) => /^[a-z]{4}$/i.test(o));
23
+ return a ? H.has(a.toLowerCase()) ? "rtl" : "ltr" : O.has(r) ? "rtl" : "ltr";
24
+ }, C = (e) => new RegExp("\\p{L}", "u").test(e), z = (e) => {
5
25
  const t = [], r = e.createTreeWalker(e.body, NodeFilter.SHOW_TEXT);
6
- let n = r.nextNode();
7
- for (; n; ) {
8
- const s = n, a = s.parentElement, l = s.data.trim();
9
- if (a && !["STYLE", "SCRIPT"].includes(a.tagName) && $(l)) {
10
- const o = s.data.match(/^\s*/)?.[0] ?? "", u = s.data.match(/\s*$/)?.[0] ?? "";
11
- t.push({ value: l, apply: (w) => {
12
- s.data = `${o}${w}${u}`;
26
+ let s = r.nextNode();
27
+ for (; s; ) {
28
+ const n = s, a = n.parentElement, o = n.data.trim();
29
+ if (a && !["STYLE", "SCRIPT"].includes(a.tagName) && C(o)) {
30
+ const c = n.data.match(/^\s*/)?.[0] ?? "", u = n.data.match(/\s*$/)?.[0] ?? "";
31
+ t.push({ value: o, apply: (w) => {
32
+ n.data = `${c}${w}${u}`;
13
33
  } });
14
34
  }
15
- n = r.nextNode();
35
+ s = r.nextNode();
16
36
  }
17
- for (const s of e.body.querySelectorAll("*"))
18
- for (const a of k) {
19
- const l = s.getAttribute(a)?.trim();
20
- l && $(l) && t.push({ value: l, apply: (o) => s.setAttribute(a, o) });
37
+ for (const n of e.body.querySelectorAll("*"))
38
+ for (const a of _) {
39
+ const o = n.getAttribute(a)?.trim();
40
+ o && C(o) && t.push({ value: o, apply: (c) => n.setAttribute(a, c) });
21
41
  }
22
42
  return t;
23
- }, H = async (e, t, r, n) => {
43
+ }, q = async (e, t, r, s) => {
24
44
  if (r === t) return e;
25
- const s = new DOMParser().parseFromString(e, "text/html");
26
- s.documentElement.lang = r;
27
- const a = j(s), l = await n(a.map((o) => o.value));
28
- return a.forEach((o, u) => o.apply(l[u] ?? o.value)), `<!doctype html>${s.documentElement.outerHTML}`;
45
+ const n = new DOMParser().parseFromString(e, "text/html");
46
+ n.documentElement.lang = r, n.documentElement.dir = j(r);
47
+ const a = z(n), o = await s(a.map((c) => c.value));
48
+ return a.forEach((c, u) => c.apply(o[u] ?? c.value)), `<!doctype html>${n.documentElement.outerHTML}`;
29
49
  }, U = async (e) => {
30
50
  const t = new TextEncoder().encode(e), r = await crypto.subtle.digest("SHA-256", t);
31
- return Array.from(new Uint8Array(r).slice(0, 6), (n) => n.toString(16).padStart(2, "0")).join("");
32
- }, O = (e) => {
51
+ return Array.from(new Uint8Array(r).slice(0, 6), (s) => s.toString(16).padStart(2, "0")).join("");
52
+ }, D = (e) => {
33
53
  const t = new DOMParser().parseFromString(e, "text/html");
34
54
  return Array.from(t.body.querySelectorAll("[style]")).find(
35
- (s) => s.style.display === "none" && s.textContent?.trim()
55
+ (n) => n.style.display === "none" && n.textContent?.trim()
36
56
  )?.textContent?.replace(/[\u00A0\u200B-\u200F\u202A-\u202E\u2066-\u2069\uFEFF]/g, "").trim() || void 0;
37
- }, _ = ({ locale: e, preview: t, sourceRevision: r, onRefresh: n }) => /* @__PURE__ */ d("article", { className: "preview-card", children: [
57
+ }, G = ({ locale: e, preview: t, sourceRevision: r, onRefresh: s }) => /* @__PURE__ */ d("article", { className: "preview-card", children: [
38
58
  /* @__PURE__ */ d("header", { className: "preview-header", children: [
39
59
  /* @__PURE__ */ d("div", { children: [
40
- /* @__PURE__ */ c("strong", { children: e.label }),
41
- /* @__PURE__ */ c("span", { children: e.code })
60
+ /* @__PURE__ */ i("strong", { children: e.label }),
61
+ /* @__PURE__ */ i("span", { children: e.code })
42
62
  ] }),
43
63
  /* @__PURE__ */ d("div", { className: `status status-${t.status}`, children: [
44
- /* @__PURE__ */ c("i", {}),
64
+ /* @__PURE__ */ i("i", {}),
45
65
  t.status
46
66
  ] })
47
67
  ] }),
@@ -52,105 +72,112 @@ const k = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
52
72
  t.revision ?? "pending"
53
73
  ] }),
54
74
  /* @__PURE__ */ d("div", { className: "preheader", children: [
55
- /* @__PURE__ */ c("span", { children: "Preheader" }),
56
- /* @__PURE__ */ c("strong", { children: O(t.html) ?? "Not set" })
75
+ /* @__PURE__ */ i("span", { children: "Preheader" }),
76
+ /* @__PURE__ */ i("strong", { children: D(t.html) ?? "Not set" })
57
77
  ] }),
58
- /* @__PURE__ */ c("iframe", { title: `${e.label} preview`, srcDoc: t.html, sandbox: "allow-popups allow-popups-to-escape-sandbox" }),
59
- t.error && /* @__PURE__ */ c("div", { className: "preview-error", children: t.error }),
60
- t.status !== "source" && /* @__PURE__ */ c("button", { className: "refresh", onClick: n, children: "Regenerate translation" })
61
- ] }), z = (e) => {
78
+ /* @__PURE__ */ i("iframe", { title: `${e.label} preview`, srcDoc: t.html, sandbox: "allow-popups allow-popups-to-escape-sandbox" }),
79
+ t.error && /* @__PURE__ */ i("div", { className: "preview-error", children: t.error }),
80
+ t.status !== "source" && /* @__PURE__ */ i("button", { className: "refresh", onClick: s, children: "Regenerate translation" })
81
+ ] }), W = (e) => {
62
82
  if ("render" in e && e.render) return e.render();
63
83
  const t = e.props ?? e.component.PreviewProps ?? {};
64
- return I(e.component, t);
65
- }, x = "/preview", F = (e = x) => `/${e}`.replace(/\/+/g, "/").replace(/\/$/, "") || x, R = (e, t, r) => {
66
- const n = F(r), a = (e.pathname.startsWith(`${n}/`) ? decodeURIComponent(e.pathname.slice(n.length + 1)) : void 0) || e.searchParams.get("template") || void 0;
84
+ return B(e.component, t);
85
+ }, $ = "/preview", k = (e = $) => `/${e}`.replace(/\/+/g, "/").replace(/\/$/, "") || $, E = (e, t, r) => {
86
+ const s = k(r), a = (e.pathname.startsWith(`${s}/`) ? decodeURIComponent(e.pathname.slice(s.length + 1)) : void 0) || e.searchParams.get("template") || void 0;
67
87
  return a && t.includes(a) ? a : t[0];
68
- }, C = (e, t = 3) => e.searchParams.get("langs")?.split(",").filter(Boolean).slice(0, t) ?? [], B = (e, t, r) => {
69
- const n = new URL(e);
70
- return n.pathname = `${F(r)}/${encodeURIComponent(t)}`, n.searchParams.delete("template"), n;
71
- }, q = (e, t) => {
88
+ }, x = (e, t = 3) => e.searchParams.get("langs")?.split(",").filter(Boolean).slice(0, t) ?? [], I = (e, t, r) => {
89
+ const s = new URL(e);
90
+ return s.pathname = `${k(r)}/${encodeURIComponent(t)}`, s.searchParams.delete("template"), s;
91
+ }, V = (e, t) => {
72
92
  const r = new URL(e);
73
93
  return t.length ? r.searchParams.set("langs", t.join(",")) : r.searchParams.delete("langs"), r;
74
- }, S = 3, D = (e) => {
75
- const t = A(() => Object.keys(e.templates), [e.templates]), [r, n] = b(() => R(new URL(window.location.href), t, e.routeBasePath)), [s, a] = b(() => C(new URL(window.location.href), S)), [l, o] = b({}), [u, w] = b(""), [g, y] = b(0), L = e.templates[r] ?? e.templates[t[0]], h = A(() => `<!doctype html>${M(z(L))}`, [L]), P = e.locales.filter((i) => s.includes(i.code)), N = [e.sourceLocale, ...P];
76
- return T(() => {
77
- const i = R(new URL(window.location.href), t, e.routeBasePath);
78
- window.history.replaceState({}, "", B(new URL(window.location.href), i, e.routeBasePath));
79
- const v = () => {
80
- n(R(new URL(window.location.href), t, e.routeBasePath)), a(C(new URL(window.location.href), S));
94
+ }, N = 3, J = (e) => {
95
+ const t = R(() => Object.keys(e.templates), [e.templates]), [r, s] = L(() => E(new URL(window.location.href), t, e.routeBasePath)), [n, a] = L(() => x(new URL(window.location.href), N)), [o, c] = L({}), [u, w] = L(""), [v, b] = L(0), f = e.templates[r] ?? e.templates[t[0]], p = R(
96
+ () => `<!doctype html>${F(W(f))}`,
97
+ [f, v]
98
+ ), g = e.locales.filter((l) => n.includes(l.code)), A = [e.sourceLocale, ...g];
99
+ return S(() => {
100
+ const l = E(new URL(window.location.href), t, e.routeBasePath);
101
+ window.history.replaceState({}, "", I(new URL(window.location.href), l, e.routeBasePath));
102
+ const y = () => {
103
+ s(E(new URL(window.location.href), t, e.routeBasePath)), a(x(new URL(window.location.href), N));
81
104
  };
82
- return window.addEventListener("popstate", v), () => window.removeEventListener("popstate", v);
83
- }, [e.routeBasePath, t]), T(() => () => {
84
- }, []), T(() => {
85
- let i = !1;
86
- return U(h).then((v) => {
87
- i || w(v);
105
+ return window.addEventListener("popstate", y), () => window.removeEventListener("popstate", y);
106
+ }, [e.routeBasePath, t]), S(() => e.sourceUpdates?.subscribe(() => {
107
+ b((l) => l + 1);
108
+ }), [e.sourceUpdates]), S(() => {
109
+ let l = !1;
110
+ return U(p).then((y) => {
111
+ l || w(y);
88
112
  }), () => {
89
- i = !0;
113
+ l = !0;
90
114
  };
91
- }, [h]), T(() => {
92
- let i = !1;
93
- return o((m) => Object.fromEntries(N.map((p) => [
94
- p.code,
95
- p.code === e.sourceLocale.code ? { html: h, status: "source" } : { html: m[p.code]?.html ?? h, status: "stale", revision: m[p.code]?.revision }
115
+ }, [p]), S(() => {
116
+ let l = !1;
117
+ return c((h) => Object.fromEntries(A.map((m) => [
118
+ m.code,
119
+ m.code === e.sourceLocale.code ? { html: p, status: "source" } : { html: h[m.code]?.html ?? p, status: "stale", revision: h[m.code]?.revision }
96
120
  ]))), (async () => {
97
- for (const m of P) {
98
- if (i) return;
99
- o((p) => ({
100
- ...p,
101
- [m.code]: { ...p[m.code], status: "translating", error: void 0 }
121
+ for (const h of g) {
122
+ if (l) return;
123
+ c((m) => ({
124
+ ...m,
125
+ [h.code]: { ...m[h.code], status: "translating", error: void 0 }
102
126
  }));
103
127
  try {
104
- const p = await H(h, e.sourceLocale.code, m.code, (E) => e.provider.translate({
105
- texts: E,
128
+ const m = await q(p, e.sourceLocale.code, h.code, (P) => e.provider.translate({
129
+ texts: P,
106
130
  sourceLocale: e.sourceLocale.translationCode ?? e.sourceLocale.code,
107
- targetLocale: m.translationCode ?? m.code
108
- })), f = await U(h);
109
- i || o((E) => ({ ...E, [m.code]: { html: p, status: "ready", revision: f } }));
110
- } catch (p) {
111
- i || o((f) => ({
112
- ...f,
113
- [m.code]: {
114
- ...f[m.code],
131
+ targetLocale: h.translationCode ?? h.code
132
+ })), T = await U(p);
133
+ l || c((P) => ({ ...P, [h.code]: { html: m, status: "ready", revision: T } }));
134
+ } catch (m) {
135
+ l || c((T) => ({
136
+ ...T,
137
+ [h.code]: {
138
+ ...T[h.code],
115
139
  status: "error",
116
- error: p instanceof Error ? p.message : String(p)
140
+ error: m instanceof Error ? m.message : String(m)
117
141
  }
118
142
  }));
119
143
  }
120
144
  }
121
145
  })(), () => {
122
- i = !0;
146
+ l = !0;
123
147
  };
124
- }, [h, g, s.join(",")]), {
125
- activeLocaleCodes: s,
126
- activeLocales: P,
127
- previewLocales: N,
128
- previews: l,
129
- refreshPreviews: () => y((i) => i + 1),
130
- selectTemplate: (i) => {
131
- n(i), window.history.pushState({}, "", B(new URL(window.location.href), i, e.routeBasePath));
148
+ }, [p, v, n.join(",")]), {
149
+ activeLocaleCodes: n,
150
+ activeLocales: g,
151
+ previewLocales: A,
152
+ previews: o,
153
+ refreshPreviews: () => b((l) => l + 1),
154
+ selectTemplate: (l) => {
155
+ s(l), window.history.pushState({}, "", I(new URL(window.location.href), l, e.routeBasePath));
132
156
  },
133
157
  selectedTemplateId: r,
134
- sourceHtml: h,
158
+ sourceHtml: p,
135
159
  sourceRevision: u,
136
160
  templateIds: t,
137
- toggleLocale: (i) => {
138
- const v = s.includes(i) ? s.filter((m) => m !== i) : [...s, i].slice(0, S);
139
- a(v), window.history.replaceState({}, "", q(new URL(window.location.href), v));
161
+ toggleLocale: (l) => {
162
+ const y = n.includes(l) ? n.filter((h) => h !== l) : [...n, l].slice(0, N);
163
+ a(y), window.history.replaceState({}, "", V(new URL(window.location.href), y));
140
164
  }
141
165
  };
142
- }, J = ({ config: e }) => {
143
- const t = D(e);
144
- return /* @__PURE__ */ d("main", { children: [
166
+ }, re = ({ config: e }) => {
167
+ const t = J(e);
168
+ return /* @__PURE__ */ d("main", { className: "email-lab", children: [
145
169
  /* @__PURE__ */ d("header", { className: "topbar", children: [
146
- /* @__PURE__ */ d("div", { children: [
147
- /* @__PURE__ */ c("p", { className: "eyebrow", children: "React Email Locale Lab" }),
148
- /* @__PURE__ */ c("h1", { children: "See every language while you build." })
170
+ /* @__PURE__ */ d("div", { className: "topbar-copy", children: [
171
+ /* @__PURE__ */ d("div", { className: "brand", children: [
172
+ /* @__PURE__ */ i("span", { className: "brand-mark" }),
173
+ /* @__PURE__ */ i("p", { className: "eyebrow", children: "React Email Locale Lab" })
174
+ ] }),
175
+ /* @__PURE__ */ i("h1", { children: "See every language while you build." })
149
176
  ] }),
150
177
  /* @__PURE__ */ d("div", { className: "controls", children: [
151
178
  /* @__PURE__ */ d("label", { children: [
152
179
  "Template",
153
- /* @__PURE__ */ c("select", { value: t.selectedTemplateId, onChange: (r) => t.selectTemplate(r.target.value), children: t.templateIds.map((r) => /* @__PURE__ */ c("option", { value: r, children: e.templates[r].name }, r)) })
180
+ /* @__PURE__ */ i("select", { value: t.selectedTemplateId, onChange: (r) => t.selectTemplate(r.target.value), children: t.templateIds.map((r) => /* @__PURE__ */ i("option", { value: r, children: e.templates[r].name }, r)) })
154
181
  ] }),
155
182
  /* @__PURE__ */ d("span", { className: "provider", children: [
156
183
  "Provider: ",
@@ -160,17 +187,17 @@ const k = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
160
187
  ] }),
161
188
  /* @__PURE__ */ d("section", { className: "locale-picker", children: [
162
189
  /* @__PURE__ */ d("div", { children: [
163
- /* @__PURE__ */ c("strong", { children: "Preview languages" }),
164
- /* @__PURE__ */ c("span", { children: "Select up to three. Language packs load only when selected." })
190
+ /* @__PURE__ */ i("strong", { children: "Preview languages" }),
191
+ /* @__PURE__ */ i("span", { children: "Select up to three. Language packs load only when selected." })
165
192
  ] }),
166
- /* @__PURE__ */ c("div", { className: "locale-options", children: e.locales.map((r) => /* @__PURE__ */ d(
193
+ /* @__PURE__ */ i("div", { className: "locale-options", children: e.locales.map((r) => /* @__PURE__ */ d(
167
194
  "button",
168
195
  {
169
196
  className: t.activeLocaleCodes.includes(r.code) ? "locale-active" : "",
170
197
  disabled: !t.activeLocaleCodes.includes(r.code) && t.activeLocaleCodes.length >= 3,
171
198
  onClick: () => t.toggleLocale(r.code),
172
199
  children: [
173
- /* @__PURE__ */ c("i", {}),
200
+ /* @__PURE__ */ i("i", {}),
174
201
  r.label
175
202
  ]
176
203
  },
@@ -178,15 +205,15 @@ const k = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
178
205
  )) })
179
206
  ] }),
180
207
  /* @__PURE__ */ d("section", { className: "notice", children: [
181
- /* @__PURE__ */ c("span", { className: "live-dot" }),
208
+ /* @__PURE__ */ i("span", { className: "live-dot" }),
182
209
  "Watching source changes through Vite HMR."
183
210
  ] }),
184
211
  t.activeLocales.length === 0 && /* @__PURE__ */ d("section", { className: "empty-state", children: [
185
- /* @__PURE__ */ c("strong", { children: "Select a language to generate a preview." }),
186
- /* @__PURE__ */ c("span", { children: "The source template renders immediately; translation starts only on demand." })
212
+ /* @__PURE__ */ i("strong", { children: "Select a language to generate a preview." }),
213
+ /* @__PURE__ */ i("span", { children: "The source template renders immediately; translation starts only on demand." })
187
214
  ] }),
188
- /* @__PURE__ */ c("section", { className: "grid", children: t.previewLocales.map((r) => /* @__PURE__ */ c(
189
- _,
215
+ /* @__PURE__ */ i("section", { className: "grid", children: t.previewLocales.map((r) => /* @__PURE__ */ i(
216
+ G,
190
217
  {
191
218
  locale: r,
192
219
  preview: t.previews[r.code] ?? { html: t.sourceHtml, status: "stale" },
@@ -196,52 +223,73 @@ const k = ["alt", "title", "aria-label"], $ = (e) => new RegExp("\\p{L}", "u").t
196
223
  r.code
197
224
  )) })
198
225
  ] });
199
- }, K = () => {
200
- const e = /* @__PURE__ */ new Map(), t = /* @__PURE__ */ new Map(), r = /* @__PURE__ */ new Map(), n = async (a, l) => {
201
- const o = r.get(a) ?? Promise.resolve();
226
+ }, M = "react-email-locale-lab:translations:v1", Y = () => {
227
+ try {
228
+ const e = window.sessionStorage?.getItem(M);
229
+ if (!e) return /* @__PURE__ */ new Map();
230
+ const t = JSON.parse(e);
231
+ return Array.isArray(t) ? new Map(t.filter(
232
+ (r) => Array.isArray(r) && r.length === 2 && r.every((s) => typeof s == "string")
233
+ )) : /* @__PURE__ */ new Map();
234
+ } catch {
235
+ return /* @__PURE__ */ new Map();
236
+ }
237
+ }, K = (e) => {
238
+ try {
239
+ window.sessionStorage?.setItem(M, JSON.stringify([...e]));
240
+ } catch {
241
+ }
242
+ }, ae = () => {
243
+ const e = /* @__PURE__ */ new Map(), t = Y(), r = /* @__PURE__ */ new Map(), s = async (a, o) => {
244
+ const c = r.get(a) ?? Promise.resolve();
202
245
  let u = () => {
203
246
  };
204
- const w = new Promise((y) => {
205
- u = y;
206
- }), g = o.catch(() => {
247
+ const w = new Promise((b) => {
248
+ u = b;
249
+ }), v = c.catch(() => {
207
250
  }).then(() => w);
208
- r.set(a, g), await o.catch(() => {
251
+ r.set(a, v), await c.catch(() => {
209
252
  });
210
253
  try {
211
- return await l();
254
+ return await o();
212
255
  } finally {
213
- u(), r.get(a) === g && r.delete(a);
256
+ u(), r.get(a) === v && r.delete(a);
214
257
  }
215
- }, s = async (a, l) => {
258
+ }, n = async (a, o) => {
216
259
  if (!window.Translator)
217
260
  throw new Error("This browser does not support the built-in Translator API. Use a current Chrome build or configure a remote provider.");
218
- const o = `${a}:${l}`;
219
- let u = e.get(o);
261
+ const c = `${a}:${o}`;
262
+ let u = e.get(c);
220
263
  if (!u) {
221
- if (await window.Translator.availability({ sourceLanguage: a, targetLanguage: l }) === "unavailable") throw new Error(`The browser cannot translate ${a} → ${l}.`);
222
- u = window.Translator.create({ sourceLanguage: a, targetLanguage: l }), e.set(o, u);
264
+ if (await window.Translator.availability({ sourceLanguage: a, targetLanguage: o }) === "unavailable") throw new Error(`The browser cannot translate ${a} → ${o}.`);
265
+ u = window.Translator.create({ sourceLanguage: a, targetLanguage: o }), e.set(c, u);
223
266
  }
224
267
  return u;
225
268
  };
226
269
  return {
227
270
  name: "Browser Translator · on-device",
228
- async translate({ texts: a, sourceLocale: l, targetLocale: o }) {
271
+ async translate({ texts: a, sourceLocale: o, targetLocale: c }) {
229
272
  if (a.length === 0) return [];
230
- const u = await s(l, o), w = `${l}:${o}`;
231
- return n(w, async () => {
232
- const g = [];
233
- for (const y of a) {
234
- const L = `${w}:${y}`;
235
- let h = t.get(L);
236
- h || (h = await u.translate(y), t.set(L, h)), g.push(h);
273
+ const u = await n(o, c), w = `${o}:${c}`;
274
+ return s(w, async () => {
275
+ const v = [];
276
+ let b = !1;
277
+ try {
278
+ for (const f of a) {
279
+ const p = `${w}:${f}`;
280
+ let g = t.get(p);
281
+ g || (g = await u.translate(f), t.set(p, g), b = !0), v.push(g);
282
+ }
283
+ return v;
284
+ } finally {
285
+ b && K(t);
237
286
  }
238
- return g;
239
287
  });
240
288
  }
241
289
  };
242
- }, Q = (e) => e;
290
+ }, se = (e) => e;
243
291
  export {
244
- J as EmailLabApp,
245
- K as browserTranslatorProvider,
246
- Q as defineEmailLab
292
+ re as EmailLabApp,
293
+ ae as browserTranslatorProvider,
294
+ se as defineEmailLab
247
295
  };
package/dist/styles.css CHANGED
@@ -1 +1 @@
1
- :root{color:#182033;background:#f4f5f9;font-family:Inter,ui-sans-serif,system-ui,sans-serif;font-synthesis:none}*{box-sizing:border-box}body{margin:0;min-width:320px}button,select{font:inherit}main{margin:0 auto;max-width:1640px;padding:42px clamp(18px,4vw,64px) 70px}.topbar{align-items:end;display:flex;justify-content:space-between;gap:32px}.eyebrow{color:#615ee8;font-size:12px;font-weight:800;letter-spacing:.12em;margin:0 0 8px;text-transform:uppercase}h1{font-size:clamp(30px,4vw,50px);letter-spacing:-.045em;line-height:1;margin:0}.controls{align-items:end;display:flex;gap:12px}label{color:#72788a;display:grid;font-size:12px;font-weight:700;gap:6px}select{background:#fff;border:1px solid #dfe1e8;border-radius:10px;color:#182033;min-width:210px;padding:11px 36px 11px 12px}.provider{background:#e8e7ff;border-radius:999px;color:#514dcf;font-size:12px;font-weight:700;padding:10px 13px}.notice{align-items:center;background:#fff;border:1px solid #e2e4ea;border-radius:12px;display:flex;font-size:13px;margin:30px 0 20px;padding:13px 16px}.locale-picker{align-items:center;background:#fff;border:1px solid #e2e4ea;border-radius:14px;display:flex;justify-content:space-between;margin-top:30px;padding:16px}.locale-picker strong,.locale-picker span{display:block}.locale-picker strong{font-size:13px}.locale-picker span{color:#8c91a0;font-size:11px;margin-top:3px}.locale-options{display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end}.locale-options button{align-items:center;background:#f6f7fa;border:1px solid #e2e4ea;border-radius:999px;color:#686e7e;cursor:pointer;display:flex;font-size:12px;font-weight:700;gap:7px;padding:8px 11px}.locale-options button i{border:1px solid #b7bbc7;border-radius:50%;height:8px;width:8px}.locale-options button.locale-active{background:#e8e7ff;border-color:#c7c5ff;color:#514dcf}.locale-options button.locale-active i{background:#615ee8;border-color:#615ee8}.locale-options button:disabled{cursor:not-allowed;opacity:.4}.notice code{background:#f0f1f5;border-radius:5px;margin-left:4px;padding:2px 5px}.live-dot{animation:pulse 1.7s infinite;background:#16a56d;border-radius:50%;height:8px;margin-right:10px;width:8px}.grid{display:grid;gap:18px;grid-template-columns:repeat(2,minmax(0,1fr))}.preview-card{background:#fff;border:1px solid #dfe1e8;border-radius:16px;box-shadow:0 8px 28px #1b233a0d;overflow:hidden;position:relative}.preview-header{align-items:center;border-bottom:1px solid #eceef3;display:flex;justify-content:space-between;padding:15px 17px 12px}.preview-header strong{display:block;font-size:14px}.preview-header span{color:#979cab;font-size:11px}.status{align-items:center;border-radius:999px;display:flex;font-size:11px;font-weight:800;gap:6px;padding:5px 9px;text-transform:uppercase}.status i{background:currentColor;border-radius:50%;height:6px;width:6px}.status-source,.status-ready{background:#e6f7ef;color:#168659}.status-stale{background:#fff2d7;color:#b17100}.status-translating{background:#e8e7ff;color:#5753d6}.status-translating i{animation:pulse .8s infinite}.status-error{background:#ffe8e8;color:#c53c3c}.revision{background:#fafbfc;color:#9ba0ad;font-family:ui-monospace,SFMono-Regular,monospace;font-size:10px;padding:7px 17px}.preheader{align-items:baseline;border-bottom:1px solid #eceef3;display:flex;gap:9px;padding:10px 17px}.preheader span{color:#979cab;font-size:10px;font-weight:800;letter-spacing:.08em;text-transform:uppercase}.preheader strong{color:#4f5565;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}iframe{background:#edf0f7;border:0;display:block;height:590px;width:100%}.refresh{background:#fff;border:1px solid #dfe1e8;border-radius:8px;bottom:12px;color:#595f70;cursor:pointer;font-size:11px;font-weight:700;padding:7px 10px;position:absolute;right:12px}.refresh:hover{border-color:#615ee8;color:#514dcf}.preview-error{background:#fff2f2;border-top:1px solid #ffd5d5;bottom:0;color:#a63333;font-size:12px;left:0;padding:11px 130px 11px 14px;position:absolute;right:0}.empty-state{align-items:center;background:#fff;border:1px dashed #cfd2dc;border-radius:16px;display:flex;flex-direction:column;justify-content:center;min-height:220px;text-align:center}.empty-state span{color:#8b90a0;font-size:13px;margin-top:7px}@keyframes pulse{50%{opacity:.3;transform:scale(.82)}}@media(max-width:900px){.grid{grid-template-columns:1fr}.topbar,.locale-picker{align-items:start;flex-direction:column}.controls{align-items:stretch;flex-direction:column;width:100%}.locale-options{justify-content:flex-start}}
1
+ .email-lab{--lab-bg: #050505;--lab-panel: #0a0a0a;--lab-panel-raised: #111111;--lab-border: #262626;--lab-border-strong: #3a3a3a;--lab-text: #ededed;--lab-muted: #8f8f8f;--lab-dim: #666666;--lab-accent: #ffffff;--lab-success: #45d483;--lab-warning: #f5a524;--lab-error: #ff6369;background:radial-gradient(circle at 50% -18%,rgba(255,255,255,.09),transparent 30rem),var(--lab-bg);color-scheme:dark;color:var(--lab-text);font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,sans-serif;font-synthesis:none;min-height:100vh;padding:40px clamp(18px,3vw,56px) 72px}.email-lab,.email-lab *{box-sizing:border-box}.email-lab button,.email-lab select{font:inherit}.email-lab .topbar{align-items:end;display:flex;gap:32px;justify-content:space-between;margin:0 auto;max-width:2200px}.email-lab .topbar-copy{max-width:760px}.email-lab .brand{align-items:center;display:flex;gap:9px;margin-bottom:14px}.email-lab .brand-mark{border-bottom:8px solid #fff;border-left:6px solid transparent;border-right:6px solid transparent;display:block;filter:drop-shadow(0 0 10px rgba(255,255,255,.35));height:0;width:0}.email-lab .eyebrow{color:#b8b8b8;font-size:12px;font-weight:600;letter-spacing:.08em;margin:0;text-transform:uppercase}.email-lab h1{font-size:clamp(32px,4vw,58px);font-weight:560;letter-spacing:-.055em;line-height:.98;margin:0;text-wrap:balance}.email-lab .controls{align-items:end;display:flex;flex:0 0 auto;gap:10px}.email-lab label{color:var(--lab-muted);display:grid;font-size:11px;font-weight:550;gap:7px}.email-lab select{background:#0d0d0d;border:1px solid var(--lab-border);border-radius:7px;color:var(--lab-text);min-width:220px;outline:none;padding:10px 34px 10px 12px;transition:border-color .16s ease,background-color .16s ease,box-shadow .16s ease}.email-lab select:hover,.email-lab select:focus-visible{background:#121212;border-color:var(--lab-border-strong);box-shadow:0 0 0 3px #ffffff0d}.email-lab .provider{background:#111;border:1px solid var(--lab-border);border-radius:999px;color:#b5b5b5;font-size:11px;font-weight:550;padding:10px 13px;white-space:nowrap}.email-lab .locale-picker,.email-lab .notice,.email-lab .grid,.email-lab .empty-state{margin-left:auto;margin-right:auto;max-width:2200px}.email-lab .locale-picker{align-items:start;background:#0c0c0ce0;border:1px solid var(--lab-border);border-radius:10px;display:grid;gap:24px;grid-template-columns:minmax(180px,.3fr) minmax(0,1fr);margin-top:34px;padding:16px}.email-lab .locale-picker strong,.email-lab .locale-picker span{display:block}.email-lab .locale-picker strong{font-size:13px;font-weight:580}.email-lab .locale-picker span{color:var(--lab-muted);font-size:11px;line-height:1.45;margin-top:4px}.email-lab .locale-options{display:flex;flex-wrap:wrap;gap:7px;justify-content:flex-end;max-height:118px;overflow:auto;padding:1px 3px 3px 1px;scrollbar-color:#333 transparent;scrollbar-width:thin}.email-lab .locale-options button{align-items:center;background:#111;border:1px solid var(--lab-border);border-radius:6px;color:#a8a8a8;cursor:pointer;display:flex;font-size:11px;font-weight:550;gap:7px;padding:7px 10px;transition:background-color .14s ease,border-color .14s ease,color .14s ease,transform .14s ease}.email-lab .locale-options button:hover:not(:disabled){background:#181818;border-color:#444;color:#fff;transform:translateY(-1px)}.email-lab .locale-options button i{border:1px solid #555;border-radius:50%;height:7px;transition:background-color .14s ease,border-color .14s ease,box-shadow .14s ease;width:7px}.email-lab .locale-options button.locale-active{background:#f2f2f2;border-color:#fff;color:#0a0a0a}.email-lab .locale-options button.locale-active i{background:#0a0a0a;border-color:#0a0a0a;box-shadow:0 0 0 2px #0000001f}.email-lab .locale-options button:disabled{cursor:not-allowed;opacity:.35}.email-lab .notice{align-items:center;border-bottom:1px solid var(--lab-border);color:var(--lab-muted);display:flex;font-size:11px;margin-bottom:18px;padding:14px 2px}.email-lab .live-dot{animation:lab-pulse 1.8s ease-in-out infinite;background:var(--lab-success);border-radius:50%;box-shadow:0 0 10px #45d4838c;height:7px;margin-right:9px;width:7px}.email-lab .grid{display:grid;gap:14px;grid-template-columns:repeat(auto-fit,minmax(min(100%,410px),1fr))}.email-lab .preview-card{animation:lab-enter .32s ease both;background:var(--lab-panel);border:1px solid var(--lab-border);border-radius:10px;box-shadow:0 16px 50px #0000003d;min-width:0;overflow:hidden;position:relative;transition:border-color .18s ease,box-shadow .18s ease,transform .18s ease}.email-lab .preview-card:hover{border-color:#393939;box-shadow:0 20px 65px #00000057;transform:translateY(-2px)}.email-lab .preview-header{align-items:center;border-bottom:1px solid var(--lab-border);display:flex;justify-content:space-between;padding:13px 14px 11px}.email-lab .preview-header strong{display:block;font-size:13px;font-weight:580}.email-lab .preview-header span{color:var(--lab-dim);font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:10px}.email-lab .status{align-items:center;border:1px solid currentColor;border-radius:999px;display:flex;font-size:9px;font-weight:650;gap:6px;letter-spacing:.045em;opacity:.88;padding:4px 7px;text-transform:uppercase}.email-lab .status i{background:currentColor;border-radius:50%;height:5px;width:5px}.email-lab .status-source,.email-lab .status-ready{color:var(--lab-success)}.email-lab .status-stale{color:var(--lab-warning)}.email-lab .status-translating{color:#8da4ff}.email-lab .status-error{color:var(--lab-error)}.email-lab .status-translating i{animation:lab-pulse .8s ease-in-out infinite}.email-lab .revision{background:#080808;border-bottom:1px solid #1b1b1b;color:#5f5f5f;font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:9px;padding:7px 14px}.email-lab .preheader{align-items:baseline;border-bottom:1px solid var(--lab-border);display:flex;gap:9px;min-width:0;padding:9px 14px}.email-lab .preheader span{color:#666;flex:0 0 auto;font-size:9px;font-weight:650;letter-spacing:.08em;text-transform:uppercase}.email-lab .preheader strong{color:#aaa;font-size:11px;font-weight:480;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.email-lab iframe{background:#e9e9e9;border:0;display:block;height:620px;width:100%}.email-lab .refresh{-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);background:#0a0a0ae0;border:1px solid #3a3a3a;border-radius:6px;bottom:11px;color:#d3d3d3;cursor:pointer;font-size:10px;font-weight:580;padding:7px 9px;position:absolute;right:11px;transition:background-color .14s ease,border-color .14s ease,color .14s ease}.email-lab .refresh:hover{background:#fff;border-color:#fff;color:#000}.email-lab .preview-error{background:#23080af5;border-top:1px solid #5c2227;bottom:0;color:#ff969a;font-size:11px;left:0;line-height:1.45;padding:11px 128px 11px 13px;position:absolute;right:0}.email-lab .empty-state{align-items:center;background:linear-gradient(180deg,#0d0d0d,#080808);border:1px dashed #303030;border-radius:10px;display:flex;flex-direction:column;justify-content:center;min-height:230px;text-align:center}.email-lab .empty-state strong{font-size:14px;font-weight:580}.email-lab .empty-state span{color:var(--lab-muted);font-size:12px;margin-top:7px}@keyframes lab-pulse{50%{opacity:.35;transform:scale(.82)}}@keyframes lab-enter{0%{opacity:0;transform:translateY(7px)}to{opacity:1;transform:translateY(0)}}@media(min-width:1500px){.email-lab .grid{grid-template-columns:repeat(3,minmax(0,1fr))}}@media(min-width:2050px){.email-lab .grid{grid-template-columns:repeat(4,minmax(0,1fr))}}@media(max-width:900px){.email-lab{padding-top:26px}.email-lab .topbar{align-items:start;flex-direction:column}.email-lab .controls{align-items:stretch;flex-direction:column;width:100%}.email-lab select{width:100%}.email-lab .locale-picker{grid-template-columns:1fr}.email-lab .locale-options{justify-content:flex-start}}@media(prefers-reduced-motion:reduce){.email-lab *,.email-lab *:before,.email-lab *:after{animation-duration:.01ms!important;animation-iteration-count:1!important;scroll-behavior:auto!important;transition-duration:.01ms!important}}
package/dist/vite.js ADDED
@@ -0,0 +1,18 @@
1
+ const c = /\.[cm]?[jt]sx?(?:\?|$)/, n = (e) => e.replaceAll("\\", "/").replace(/^\.\//, ""), p = (e, r) => {
2
+ const s = e.map(n), t = r?.map(n).filter(Boolean);
3
+ return t?.length ? s.some(
4
+ (a) => t.some((o) => a.includes(o))
5
+ ) : s.some((a) => c.test(a));
6
+ }, l = (e, r = {}) => ({
7
+ subscribe(s) {
8
+ if (!e) return () => {
9
+ };
10
+ const t = (a) => {
11
+ p(a.updates.map((o) => o.path), r.watchPaths) && s();
12
+ };
13
+ return e.on("vite:afterUpdate", t), () => e.off("vite:afterUpdate", t);
14
+ }
15
+ });
16
+ export {
17
+ l as viteSourceUpdates
18
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-email-locale-lab",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Real-time multi-language previews for React Email templates.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -15,6 +15,10 @@
15
15
  "types": "./dist/index.d.ts",
16
16
  "import": "./dist/index.js"
17
17
  },
18
+ "./vite": {
19
+ "types": "./dist/adapters/vite.d.ts",
20
+ "import": "./dist/vite.js"
21
+ },
18
22
  "./styles.css": "./dist/styles.css"
19
23
  },
20
24
  "sideEffects": ["./dist/styles.css"],