@one-million-lines/email-builder 0.1.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/App.d.ts +1 -0
  5. package/dist/core/aiActions.d.ts +52 -0
  6. package/dist/core/plugins.d.ts +26 -0
  7. package/dist/core/renderer.d.ts +2 -0
  8. package/dist/core/theme.d.ts +3 -0
  9. package/dist/core/types.d.ts +150 -0
  10. package/dist/core/utils.d.ts +3 -0
  11. package/dist/core/validation.d.ts +810 -0
  12. package/dist/editor/Canvas.d.ts +1 -0
  13. package/dist/editor/LeftSidebar.d.ts +1 -0
  14. package/dist/editor/RecommendationsPanel.d.ts +6 -0
  15. package/dist/editor/RightSidebar.d.ts +1 -0
  16. package/dist/editor/TemplatesModal.d.ts +6 -0
  17. package/dist/editor/TopBar.d.ts +1 -0
  18. package/dist/email-builder.cjs +80 -0
  19. package/dist/email-builder.cjs.map +1 -0
  20. package/dist/email-builder.js +8766 -0
  21. package/dist/email-builder.js.map +1 -0
  22. package/dist/index.d.ts +35 -0
  23. package/dist/modules/basic.d.ts +2 -0
  24. package/dist/modules/content.d.ts +2 -0
  25. package/dist/modules/cta.d.ts +2 -0
  26. package/dist/modules/defaultModules.d.ts +14 -0
  27. package/dist/modules/ecommerce.d.ts +2 -0
  28. package/dist/modules/feature.d.ts +2 -0
  29. package/dist/modules/footer.d.ts +2 -0
  30. package/dist/modules/header.d.ts +2 -0
  31. package/dist/modules/helpers.d.ts +89 -0
  32. package/dist/modules/menu.d.ts +2 -0
  33. package/dist/modules/registry.d.ts +24 -0
  34. package/dist/modules/social.d.ts +2 -0
  35. package/dist/modules/transactional.d.ts +2 -0
  36. package/dist/plugins/imageUploader/index.d.ts +36 -0
  37. package/dist/recommendations/logic.d.ts +99 -0
  38. package/dist/store/emailStore.d.ts +42 -0
  39. package/dist/styles.css +1 -0
  40. package/dist/templates/abandonedCart.d.ts +3 -0
  41. package/dist/templates/blogDigest.d.ts +3 -0
  42. package/dist/templates/eventInvite.d.ts +3 -0
  43. package/dist/templates/flashSale.d.ts +3 -0
  44. package/dist/templates/index.d.ts +2 -0
  45. package/dist/templates/orderReceipt.d.ts +3 -0
  46. package/dist/templates/productLaunch.d.ts +3 -0
  47. package/dist/templates/registry.d.ts +25 -0
  48. package/dist/templates/weeklyNewsletter.d.ts +3 -0
  49. package/dist/templates/welcomeOnboarding.d.ts +3 -0
  50. package/dist/themes/defaultThemes.d.ts +7 -0
  51. package/package.json +91 -0
@@ -0,0 +1,35 @@
1
+ import { EmailDocument, Theme } from './core/types';
2
+ import { documentSchema } from './core/validation';
3
+ import { renderEmailHtml } from './core/renderer';
4
+ import { ModuleDefinition } from './modules/registry';
5
+ import { AIProvider } from './core/aiActions';
6
+ import { registerPlugin, Plugin } from './core/plugins';
7
+ export interface EmailBuilderProps {
8
+ initialDocument?: EmailDocument;
9
+ modules?: ModuleDefinition[];
10
+ themes?: Theme[];
11
+ aiProvider?: AIProvider;
12
+ onChange?: (doc: EmailDocument) => void;
13
+ onExportHtml?: (html: string) => void;
14
+ }
15
+ /** React component wrapper. */
16
+ export declare function EmailBuilder(props: EmailBuilderProps): import('react').FunctionComponentElement<{}>;
17
+ /** Vanilla JS factory. */
18
+ export interface VanillaOptions extends EmailBuilderProps {
19
+ container: HTMLElement;
20
+ }
21
+ export interface VanillaInstance {
22
+ destroy: () => void;
23
+ getDocument: () => EmailDocument;
24
+ exportHtml: () => string;
25
+ exportJson: () => string;
26
+ }
27
+ export declare function createEmailBuilder(opts: VanillaOptions): VanillaInstance;
28
+ export type { Plugin, ModuleDefinition, AIProvider, EmailDocument, Theme };
29
+ export { registerPlugin, renderEmailHtml, documentSchema };
30
+ export { imageUploaderPlugin } from './plugins/imageUploader';
31
+ export type { ImageUploaderOptions } from './plugins/imageUploader';
32
+ export { templateRegistry, TEMPLATE_CATEGORY_LABELS } from './templates';
33
+ export type { TemplateDefinition, TemplateCategory } from './templates';
34
+ export { ALGORITHMS, ALGORITHM_BY_ID, FALLBACK_OPTIONS, defaultLogic as defaultRecommendationsLogic, readLogic as readRecommendationsLogic, toLegacyShape as recommendationsToLegacyShape, } from './recommendations/logic';
35
+ export type { RecommendationsLogic, RecommendationFilters, StackEntry, AlgorithmDefinition, AlgorithmParamSpec, FallbackId, RecommendationMode, } from './recommendations/logic';
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const basicModules: ModuleDefinition[];
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const contentModules: ModuleDefinition[];
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const ctaModules: ModuleDefinition[];
@@ -0,0 +1,14 @@
1
+ import { ModuleDefinition } from './registry';
2
+ import { basicModules } from './basic';
3
+ import { headerModules } from './header';
4
+ import { menuModules } from './menu';
5
+ import { contentModules } from './content';
6
+ import { featureModules } from './feature';
7
+ import { ctaModules } from './cta';
8
+ import { ecommerceModules } from './ecommerce';
9
+ import { transactionalModules } from './transactional';
10
+ import { socialModules } from './social';
11
+ import { footerModules } from './footer';
12
+ export declare const defaultModules: ModuleDefinition[];
13
+ export declare function registerDefaultModules(): void;
14
+ export { basicModules, headerModules, menuModules, contentModules, featureModules, ctaModules, ecommerceModules, transactionalModules, socialModules, footerModules, };
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const ecommerceModules: ModuleDefinition[];
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const featureModules: ModuleDefinition[];
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const footerModules: ModuleDefinition[];
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const headerModules: ModuleDefinition[];
@@ -0,0 +1,89 @@
1
+ import { EmailElement, EmailModule, Product, ProductGridElement } from '../core/types';
2
+ export declare const text: (content: string, opts?: Partial<{
3
+ role: "headline" | "subheadline" | "body" | "caption";
4
+ fontSize: number;
5
+ fontFamily: string;
6
+ fontWeight: number | string;
7
+ color: string;
8
+ align: "left" | "center" | "right";
9
+ paddingTop: number;
10
+ paddingBottom: number;
11
+ paddingLeft: number;
12
+ paddingRight: number;
13
+ lineHeight: number;
14
+ letterSpacing: number;
15
+ link: string;
16
+ mobile: Record<string, unknown>;
17
+ }>) => EmailElement;
18
+ export declare const image: (src: string, alt: string, opts?: Partial<{
19
+ width: number;
20
+ height: number;
21
+ align: "left" | "center" | "right";
22
+ link: string;
23
+ paddingTop: number;
24
+ paddingBottom: number;
25
+ borderRadius: number;
26
+ mobile: Record<string, unknown>;
27
+ }>) => EmailElement;
28
+ export declare const button: (label: string, link?: string, opts?: Partial<{
29
+ align: "left" | "center" | "right";
30
+ backgroundColor: string;
31
+ color: string;
32
+ fontSize: number;
33
+ borderRadius: number;
34
+ paddingTop: number;
35
+ paddingBottom: number;
36
+ mobile: Record<string, unknown>;
37
+ }>) => EmailElement;
38
+ export declare const spacer: (height?: number) => EmailElement;
39
+ export declare const divider: (opts?: Partial<{
40
+ color: string;
41
+ thickness: number;
42
+ paddingTop: number;
43
+ paddingBottom: number;
44
+ }>) => EmailElement;
45
+ export declare const mod: (type: string, name: string, children: EmailElement[], style?: EmailModule["style"]) => EmailModule;
46
+ export declare const heading: (content: string, opts?: {
47
+ level?: 1 | 2 | 3;
48
+ align?: "left" | "center" | "right";
49
+ paddingTop?: number;
50
+ paddingBottom?: number;
51
+ fontSize?: number;
52
+ }) => EmailElement;
53
+ export declare const eyebrow: (content: string, opts?: {
54
+ align?: "left" | "center" | "right";
55
+ paddingTop?: number;
56
+ paddingBottom?: number;
57
+ }) => EmailElement;
58
+ export declare const muted: (content: string, opts?: {
59
+ align?: "left" | "center" | "right";
60
+ fontSize?: number;
61
+ paddingTop?: number;
62
+ paddingBottom?: number;
63
+ lineHeight?: number;
64
+ link?: string;
65
+ letterSpacing?: number;
66
+ }) => EmailElement;
67
+ export declare const PLACEHOLDER: (w: number, h: number, label?: string) => string;
68
+ export declare const product: (p: Partial<Product> & {
69
+ name: string;
70
+ finalPrice: string;
71
+ }) => Product;
72
+ export declare const productGrid: (products: Product[], opts?: Partial<{
73
+ columns: 1 | 2 | 3;
74
+ showOldPrice: boolean;
75
+ showButton: boolean;
76
+ showDescription: boolean;
77
+ buttonLabel: string;
78
+ cardBackgroundColor: string;
79
+ nameColor: string;
80
+ finalPriceColor: string;
81
+ oldPriceColor: string;
82
+ buttonBackgroundColor: string;
83
+ buttonColor: string;
84
+ borderRadius: number;
85
+ paddingTop: number;
86
+ paddingBottom: number;
87
+ paddingLeft: number;
88
+ paddingRight: number;
89
+ }>) => ProductGridElement;
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const menuModules: ModuleDefinition[];
@@ -0,0 +1,24 @@
1
+ import { EmailModule } from '../core/types';
2
+ export type ModuleCategory = "basic" | "menu" | "header" | "content" | "feature" | "call_to_action" | "ecommerce" | "transactional" | "social" | "footer";
3
+ export interface ModuleDefinition {
4
+ type: string;
5
+ category: ModuleCategory;
6
+ name: string;
7
+ /** Human + AI readable summary of the module's structure, intent and content slots. Used by AI to choose modules when assembling templates. */
8
+ description: string;
9
+ /** Free-form tags to help AI semantic search: e.g. ["product", "grid", "ecommerce", "3-column"]. */
10
+ tags?: string[];
11
+ thumbnail?: string;
12
+ /** Returns a fresh module JSON with new ids. */
13
+ create: () => EmailModule;
14
+ }
15
+ declare class ModuleRegistry {
16
+ private modules;
17
+ register(def: ModuleDefinition): void;
18
+ get(type: string): ModuleDefinition | undefined;
19
+ list(): ModuleDefinition[];
20
+ byCategory(category: ModuleCategory): ModuleDefinition[];
21
+ }
22
+ export declare const moduleRegistry: ModuleRegistry;
23
+ export declare const CATEGORY_LABELS: Record<ModuleCategory, string>;
24
+ export {};
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const socialModules: ModuleDefinition[];
@@ -0,0 +1,2 @@
1
+ import { ModuleDefinition } from './registry';
2
+ export declare const transactionalModules: ModuleDefinition[];
@@ -0,0 +1,36 @@
1
+ import { Plugin, AssetProvider } from '../../core/plugins';
2
+ export interface ImageUploaderOptions {
3
+ /** Absolute or same-origin URL the file is POSTed to. Required. */
4
+ endpoint: string;
5
+ /** Form field name for the file. Default: "file". */
6
+ fieldName?: string;
7
+ /** Extra headers (e.g. Authorization). Cookies/credentials are sent if `withCredentials` is true. */
8
+ headers?: Record<string, string>;
9
+ /** Send credentials (cookies). Default: false. */
10
+ withCredentials?: boolean;
11
+ /** Max file size in MB. Default: 10. */
12
+ maxSizeMB?: number;
13
+ /** Comma-separated MIME types accepted by the file picker. Default: "image/*". */
14
+ accept?: string;
15
+ /**
16
+ * Map a successful server JSON body to `{ url, alt? }`.
17
+ * Default expects `{ url: string, alt?: string }`.
18
+ */
19
+ transformResponse?: (body: unknown) => {
20
+ url: string;
21
+ alt?: string;
22
+ };
23
+ /** Optional progress callback for the active upload. */
24
+ onProgress?: (percent: number) => void;
25
+ }
26
+ export declare function createImageUploaderProvider(opts: ImageUploaderOptions): AssetProvider;
27
+ /**
28
+ * Plugin factory. Pass to `registerPlugin()` exported by the builder.
29
+ *
30
+ * @example
31
+ * import { registerPlugin } from "openpostcards-builder";
32
+ * import { imageUploaderPlugin } from "openpostcards-builder/plugins/imageUploader";
33
+ * registerPlugin(imageUploaderPlugin({ endpoint: "/api/upload" }));
34
+ */
35
+ export declare function imageUploaderPlugin(opts: ImageUploaderOptions): Plugin;
36
+ export declare const ACCEPT_DEFAULT = "image/*";
@@ -0,0 +1,99 @@
1
+ import { EmailModule } from '../core/types';
2
+ export type RecommendationMode = "manual" | "recommender";
3
+ export type FallbackId = "smart-reccs" | "random" | "ignore" | "stop";
4
+ export interface AlgorithmParamSpec {
5
+ key: string;
6
+ label: string;
7
+ type: "select" | "text" | "number";
8
+ options?: Array<{
9
+ value: string | number;
10
+ label: string;
11
+ }>;
12
+ default?: string | number;
13
+ }
14
+ export interface AlgorithmDefinition {
15
+ id: string;
16
+ label: string;
17
+ group?: "behavioral" | "catalog" | "personal" | "contextual";
18
+ params?: AlgorithmParamSpec[];
19
+ }
20
+ export interface StackEntry {
21
+ algorithm: string;
22
+ params?: Record<string, string | number | boolean>;
23
+ }
24
+ export interface RecommendationFilters {
25
+ /** Product IDs / SKUs to include (manual or boost). */
26
+ includeProducts: string[];
27
+ /** Product IDs / SKUs to exclude. */
28
+ excludeProducts: string[];
29
+ /** Categories to include. */
30
+ includeCategories: string[];
31
+ /** Categories to exclude. */
32
+ excludeCategories: string[];
33
+ minStock?: number;
34
+ minPrice?: number;
35
+ mainOnly?: boolean;
36
+ higherPrice?: boolean;
37
+ salesPrice?: boolean;
38
+ sameCategory?: boolean;
39
+ matchTitle?: boolean;
40
+ /** Comma-separated property keys to match on, e.g. "brand,color". */
41
+ sameField?: string;
42
+ /** Free-form advanced JSON overrides. */
43
+ advanced?: Record<string, unknown>;
44
+ }
45
+ export interface RecommendationsLogic {
46
+ mode: RecommendationMode;
47
+ noProducts: number;
48
+ /** Source feed id (for manual mode). */
49
+ sourceFeed?: string;
50
+ /** Manually-picked product ids (manual mode). */
51
+ manualProducts: string[];
52
+ /** Algorithm stack — tried in order. Max 3. */
53
+ stack: StackEntry[];
54
+ /** Last-resort algorithm. */
55
+ fallback: FallbackId;
56
+ filters: RecommendationFilters;
57
+ }
58
+ export declare const ALGORITHMS: AlgorithmDefinition[];
59
+ export declare const ALGORITHM_BY_ID: Record<string, AlgorithmDefinition>;
60
+ export declare const FALLBACK_OPTIONS: Array<{
61
+ value: FallbackId;
62
+ label: string;
63
+ description: string;
64
+ }>;
65
+ export declare const MAX_STACK = 3;
66
+ export declare function defaultLogic(noProducts?: number): RecommendationsLogic;
67
+ /** Read recommendations logic from a module, returning defaults if absent. */
68
+ export declare function readLogic(data: Record<string, unknown> | undefined): RecommendationsLogic | null;
69
+ /** A module is "product-aware" if any of its children renders products. */
70
+ export declare function isProductAware(m: EmailModule): boolean;
71
+ /** Count of product slots the module currently shows (used as default noProducts). */
72
+ export declare function productSlotCount(m: EmailModule): number;
73
+ /** Serialize back to legacy Vibetrace-shaped JSON for backend interop. */
74
+ export declare function toLegacyShape(l: RecommendationsLogic): {
75
+ params: {
76
+ selection: string;
77
+ include: {
78
+ item: string[];
79
+ category: string[];
80
+ };
81
+ exclude: {
82
+ item: string[];
83
+ category: string[];
84
+ };
85
+ higherPrice: boolean;
86
+ salesPrice: boolean;
87
+ sameCategory: boolean;
88
+ matchTitle: boolean;
89
+ sameField: string;
90
+ mainOnly: boolean;
91
+ };
92
+ noProducts: string;
93
+ fallback: FallbackId;
94
+ stack: StackEntry[];
95
+ minStock: string;
96
+ minPrice: string;
97
+ sourceFeed: string;
98
+ manualProducts: string[];
99
+ };
@@ -0,0 +1,42 @@
1
+ import { EmailDocument, EmailModule, EmailElement, Theme, Selection } from '../core/types';
2
+ import { TemplateDefinition } from '../templates/registry';
3
+ export type ViewMode = "desktop" | "mobile";
4
+ interface State {
5
+ doc: EmailDocument;
6
+ selection: Selection;
7
+ past: EmailDocument[];
8
+ future: EmailDocument[];
9
+ themes: Theme[];
10
+ viewMode: ViewMode;
11
+ setSelection: (sel: Selection) => void;
12
+ setViewMode: (mode: ViewMode) => void;
13
+ applyDoc: (next: EmailDocument, push?: boolean) => void;
14
+ undo: () => void;
15
+ redo: () => void;
16
+ addModule: (mod: EmailModule, index?: number) => void;
17
+ deleteModule: (moduleId: string) => void;
18
+ duplicateModule: (moduleId: string) => void;
19
+ reorderModules: (fromIndex: number, toIndex: number) => void;
20
+ updateModule: (moduleId: string, patch: Partial<EmailModule>) => void;
21
+ updateElement: (moduleId: string, elementId: string, patch: Partial<EmailElement>) => void;
22
+ deleteElement: (moduleId: string, elementId: string) => void;
23
+ reorderElement: (moduleId: string, fromIndex: number, toIndex: number) => void;
24
+ moveElement: (moduleId: string, elementId: string, dir: -1 | 1) => void;
25
+ applyTheme: (theme: Theme) => void;
26
+ updateMeta: (patch: Partial<EmailDocument["meta"]>) => void;
27
+ updateSettings: (patch: Partial<EmailDocument["settings"]>) => void;
28
+ loadTemplate: (def: TemplateDefinition) => void;
29
+ resetDocument: () => void;
30
+ exportJson: () => string;
31
+ importJson: (json: string) => {
32
+ ok: true;
33
+ } | {
34
+ ok: false;
35
+ error: string;
36
+ };
37
+ exportHtml: () => string;
38
+ loadFromLocalStorage: () => void;
39
+ saveToLocalStorage: () => void;
40
+ }
41
+ export declare const useEmailStore: import('zustand').UseBoundStore<import('zustand').StoreApi<State>>;
42
+ export {};
@@ -0,0 +1 @@
1
+ /*! tailwindcss v4.2.4 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-600:oklch(57.7% .245 27.325);--color-amber-50:oklch(98.7% .022 95.277);--color-amber-100:oklch(96.2% .059 95.617);--color-amber-200:oklch(92.4% .12 95.746);--color-amber-500:oklch(76.9% .188 70.08);--color-amber-800:oklch(47.3% .137 46.201);--color-teal-50:oklch(98.4% .014 180.72);--color-teal-600:oklch(60% .118 184.704);--color-teal-700:oklch(51.1% .096 186.391);--color-blue-50:oklch(97% .014 254.604);--color-blue-100:oklch(93.2% .032 255.585);--color-blue-200:oklch(88.2% .059 254.128);--color-blue-400:oklch(70.7% .165 254.624);--color-blue-500:oklch(62.3% .214 259.815);--color-blue-600:oklch(54.6% .245 262.881);--color-blue-700:oklch(48.8% .243 264.376);--color-blue-800:oklch(42.4% .199 265.638);--color-blue-900:oklch(37.9% .146 265.522);--color-purple-50:oklch(97.7% .014 308.299);--color-purple-100:oklch(94.6% .033 307.174);--color-purple-200:oklch(90.2% .063 306.703);--color-purple-700:oklch(49.6% .265 301.924);--color-gray-50:oklch(98.5% .002 247.839);--color-gray-100:oklch(96.7% .003 264.542);--color-gray-200:oklch(92.8% .006 264.531);--color-gray-300:oklch(87.2% .01 258.338);--color-gray-400:oklch(70.7% .022 261.325);--color-gray-500:oklch(55.1% .027 264.364);--color-gray-600:oklch(44.6% .03 256.802);--color-gray-700:oklch(37.3% .034 259.733);--color-gray-800:oklch(27.8% .033 256.848);--color-gray-900:oklch(21% .034 264.665);--color-neutral-50:oklch(98.5% 0 0);--color-neutral-100:oklch(97% 0 0);--color-neutral-200:oklch(92.2% 0 0);--color-neutral-400:oklch(70.8% 0 0);--color-neutral-500:oklch(55.6% 0 0);--color-neutral-700:oklch(37.1% 0 0);--color-neutral-900:oklch(20.5% 0 0);--color-black:#000;--color-white:#fff;--spacing:.25rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--font-weight-medium:500;--font-weight-semibold:600;--tracking-wide:.025em;--tracking-wider:.05em;--leading-tight:1.25;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--radius-2xl:1rem;--animate-spin:spin 1s linear infinite;--blur-sm:8px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.inset-0{inset:calc(var(--spacing) * 0)}.start{inset-inline-start:var(--spacing)}.end{inset-inline-end:var(--spacing)}.-top-3{top:calc(var(--spacing) * -3)}.top-0{top:calc(var(--spacing) * 0)}.top-1\/2{top:50%}.top-full{top:100%}.right-0{right:calc(var(--spacing) * 0)}.right-2{right:calc(var(--spacing) * 2)}.left-2{left:calc(var(--spacing) * 2)}.left-2\.5{left:calc(var(--spacing) * 2.5)}.z-10{z-index:10}.z-20{z-index:20}.z-50{z-index:50}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-auto{margin:auto}.mx-1{margin-inline:calc(var(--spacing) * 1)}.my-1{margin-block:calc(var(--spacing) * 1)}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-auto{margin-top:auto}.mr-2{margin-right:calc(var(--spacing) * 2)}.mb-1{margin-bottom:calc(var(--spacing) * 1)}.mb-1\.5{margin-bottom:calc(var(--spacing) * 1.5)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-auto{margin-left:auto}.line-clamp-1{-webkit-line-clamp:1;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.line-clamp-2{-webkit-line-clamp:2;-webkit-box-orient:vertical;display:-webkit-box;overflow:hidden}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.aspect-3\/4{aspect-ratio:3/4}.h-3{height:calc(var(--spacing) * 3)}.h-4{height:calc(var(--spacing) * 4)}.h-5{height:calc(var(--spacing) * 5)}.h-8{height:calc(var(--spacing) * 8)}.h-12{height:calc(var(--spacing) * 12)}.h-\[92vh\]{height:92vh}.h-full{height:100%}.h-px{height:1px}.h-screen{height:100vh}.max-h-60{max-height:calc(var(--spacing) * 60)}.max-h-80{max-height:calc(var(--spacing) * 80)}.w-3{width:calc(var(--spacing) * 3)}.w-4{width:calc(var(--spacing) * 4)}.w-9{width:calc(var(--spacing) * 9)}.w-10{width:calc(var(--spacing) * 10)}.w-14{width:calc(var(--spacing) * 14)}.w-16{width:calc(var(--spacing) * 16)}.w-20{width:calc(var(--spacing) * 20)}.w-56{width:calc(var(--spacing) * 56)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-\[96vw\]{width:96vw}.w-full{width:100%}.w-px{width:1px}.w-screen{width:100vw}.max-w-350{max-width:calc(var(--spacing) * 350)}.min-w-20{min-width:calc(var(--spacing) * 20)}.flex-1{flex:1}.shrink-0{flex-shrink:0}.translate-x-0\.5{--tw-translate-x:calc(var(--spacing) * .5);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-x-4{--tw-translate-x:calc(var(--spacing) * 4);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y: -50% ;translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.animate-spin{animation:var(--animate-spin)}.cursor-grab{cursor:grab}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-5{gap:calc(var(--spacing) * 5)}:where(.space-y-0\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * .5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * .5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-1\.5>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1.5) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1.5) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-2xl{border-radius:var(--radius-2xl)}.rounded-\[28px\]{border-radius:28px}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-\[10px\]{border-style:var(--tw-border-style);border-width:10px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-amber-200{border-color:var(--color-amber-200)}.border-blue-100{border-color:var(--color-blue-100)}.border-blue-200{border-color:var(--color-blue-200)}.border-blue-500{border-color:var(--color-blue-500)}.border-gray-100{border-color:var(--color-gray-100)}.border-gray-200{border-color:var(--color-gray-200)}.border-gray-300{border-color:var(--color-gray-300)}.border-gray-800{border-color:var(--color-gray-800)}.border-neutral-200{border-color:var(--color-neutral-200)}.border-purple-200{border-color:var(--color-purple-200)}.border-teal-600{border-color:var(--color-teal-600)}.bg-amber-50{background-color:var(--color-amber-50)}.bg-amber-100{background-color:var(--color-amber-100)}.bg-amber-500{background-color:var(--color-amber-500)}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black) 60%,transparent)}}.bg-blue-50{background-color:var(--color-blue-50)}.bg-blue-50\/40{background-color:#eff6ff66}@supports (color:color-mix(in lab,red,red)){.bg-blue-50\/40{background-color:color-mix(in oklab,var(--color-blue-50) 40%,transparent)}}.bg-blue-600{background-color:var(--color-blue-600)}.bg-gray-50{background-color:var(--color-gray-50)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-200{background-color:var(--color-gray-200)}.bg-gray-300{background-color:var(--color-gray-300)}.bg-gray-900{background-color:var(--color-gray-900)}.bg-neutral-50{background-color:var(--color-neutral-50)}.bg-neutral-100{background-color:var(--color-neutral-100)}.bg-neutral-900{background-color:var(--color-neutral-900)}.bg-purple-50{background-color:var(--color-purple-50)}.bg-red-600{background-color:var(--color-red-600)}.bg-teal-50{background-color:var(--color-teal-50)}.bg-teal-600{background-color:var(--color-teal-600)}.bg-transparent{background-color:#0000}.bg-white{background-color:var(--color-white)}.object-cover{object-fit:cover}.p-0\.5{padding:calc(var(--spacing) * .5)}.p-1{padding:calc(var(--spacing) * 1)}.p-1\.5{padding:calc(var(--spacing) * 1.5)}.p-2{padding:calc(var(--spacing) * 2)}.p-2\.5{padding:calc(var(--spacing) * 2.5)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-6{padding:calc(var(--spacing) * 6)}.p-12{padding:calc(var(--spacing) * 12)}.px-1{padding-inline:calc(var(--spacing) * 1)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-8{padding-block:calc(var(--spacing) * 8)}.pt-3{padding-top:calc(var(--spacing) * 3)}.pt-4{padding-top:calc(var(--spacing) * 4)}.pr-2{padding-right:calc(var(--spacing) * 2)}.pl-7{padding-left:calc(var(--spacing) * 7)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11px\]{font-size:11px}.leading-tight{--tw-leading:var(--leading-tight);line-height:var(--leading-tight)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.text-amber-800{color:var(--color-amber-800)}.text-blue-600{color:var(--color-blue-600)}.text-blue-700{color:var(--color-blue-700)}.text-blue-800{color:var(--color-blue-800)}.text-blue-900{color:var(--color-blue-900)}.text-gray-100{color:var(--color-gray-100)}.text-gray-400{color:var(--color-gray-400)}.text-gray-500{color:var(--color-gray-500)}.text-gray-600{color:var(--color-gray-600)}.text-gray-700{color:var(--color-gray-700)}.text-gray-800{color:var(--color-gray-800)}.text-gray-900{color:var(--color-gray-900)}.text-neutral-400{color:var(--color-neutral-400)}.text-neutral-500{color:var(--color-neutral-500)}.text-neutral-700{color:var(--color-neutral-700)}.text-neutral-900{color:var(--color-neutral-900)}.text-purple-700{color:var(--color-purple-700)}.text-red-600{color:var(--color-red-600)}.text-teal-700{color:var(--color-teal-700)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.italic{font-style:italic}.line-through{text-decoration-line:line-through}.accent-teal-600{accent-color:var(--color-teal-600)}.opacity-0{opacity:0}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-teal-600{--tw-ring-color:var(--color-teal-600)}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.duration-200{--tw-duration:.2s;transition-duration:.2s}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.group-hover\:scale-\[1\.02\]:is(:where(.group):hover *){scale:1.02}}.focus-within\:border-blue-500:focus-within{border-color:var(--color-blue-500)}@media (hover:hover){.hover\:-translate-y-0\.5:hover{--tw-translate-y:calc(var(--spacing) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.hover\:border-blue-400:hover{border-color:var(--color-blue-400)}.hover\:border-gray-300:hover{border-color:var(--color-gray-300)}.hover\:border-neutral-400:hover{border-color:var(--color-neutral-400)}.hover\:bg-blue-50:hover{background-color:var(--color-blue-50)}.hover\:bg-blue-700:hover{background-color:var(--color-blue-700)}.hover\:bg-gray-50:hover{background-color:var(--color-gray-50)}.hover\:bg-gray-100:hover{background-color:var(--color-gray-100)}.hover\:bg-gray-200:hover{background-color:var(--color-gray-200)}.hover\:bg-neutral-100:hover{background-color:var(--color-neutral-100)}.hover\:bg-neutral-200:hover{background-color:var(--color-neutral-200)}.hover\:bg-purple-100:hover{background-color:var(--color-purple-100)}.hover\:bg-teal-50:hover{background-color:var(--color-teal-50)}.hover\:bg-teal-700:hover{background-color:var(--color-teal-700)}.hover\:text-blue-600:hover{color:var(--color-blue-600)}.hover\:text-gray-700:hover{color:var(--color-gray-700)}.hover\:text-neutral-900:hover{color:var(--color-neutral-900)}.hover\:text-red-600:hover{color:var(--color-red-600)}.hover\:text-teal-700:hover{color:var(--color-teal-700)}.hover\:shadow-lg:hover{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.hover\:shadow-sm:hover{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}}.focus\:border-blue-500:focus{border-color:var(--color-blue-500)}.focus\:border-neutral-400:focus{border-color:var(--color-neutral-400)}.focus\:bg-gray-50:focus{background-color:var(--color-gray-50)}.focus\:outline-none:focus{--tw-outline-style:none;outline-style:none}.active\:cursor-grabbing:active{cursor:grabbing}.disabled\:bg-gray-300:disabled{background-color:var(--color-gray-300)}.disabled\:text-gray-300:disabled{color:var(--color-gray-300)}.disabled\:opacity-30:disabled{opacity:.3}.disabled\:opacity-40:disabled{opacity:.4}.disabled\:opacity-50:disabled{opacity:.5}@media (hover:hover){.disabled\:hover\:bg-transparent:disabled:hover{background-color:#0000}}@media (min-width:40rem){.sm\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}}@media (min-width:64rem){.lg\:grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}}@media (min-width:80rem){.xl\:grid-cols-5{grid-template-columns:repeat(5,minmax(0,1fr))}}}[contenteditable=true]:focus{outline-offset:2px;border-radius:2px;outline:2px solid #3b82f6}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@keyframes spin{to{transform:rotate(360deg)}}
@@ -0,0 +1,3 @@
1
+ import { TemplateDefinition } from './registry';
2
+ declare const def: TemplateDefinition;
3
+ export default def;
@@ -0,0 +1,3 @@
1
+ import { TemplateDefinition } from './registry';
2
+ declare const def: TemplateDefinition;
3
+ export default def;
@@ -0,0 +1,3 @@
1
+ import { TemplateDefinition } from './registry';
2
+ declare const def: TemplateDefinition;
3
+ export default def;
@@ -0,0 +1,3 @@
1
+ import { TemplateDefinition } from './registry';
2
+ declare const def: TemplateDefinition;
3
+ export default def;
@@ -0,0 +1,2 @@
1
+ export { templateRegistry, TEMPLATE_CATEGORY_LABELS, } from './registry';
2
+ export type { TemplateDefinition, TemplateCategory } from './registry';
@@ -0,0 +1,3 @@
1
+ import { TemplateDefinition } from './registry';
2
+ declare const def: TemplateDefinition;
3
+ export default def;
@@ -0,0 +1,3 @@
1
+ import { TemplateDefinition } from './registry';
2
+ declare const def: TemplateDefinition;
3
+ export default def;
@@ -0,0 +1,25 @@
1
+ import { EmailDocument } from '../core/types';
2
+ export type TemplateCategory = "newsletter" | "ecommerce" | "abandoned_cart" | "product_launch" | "onboarding" | "event" | "transactional" | "publishing";
3
+ export interface TemplateDefinition {
4
+ id: string;
5
+ name: string;
6
+ category: TemplateCategory;
7
+ /** Human + AI readable description. */
8
+ description: string;
9
+ /** Free-form tags for search. */
10
+ tags?: string[];
11
+ /** Optional preview image URL (https or data: URI). */
12
+ thumbnail?: string;
13
+ /** Returns a fresh `EmailDocument` JSON with new ids every call. */
14
+ build: () => EmailDocument;
15
+ }
16
+ declare class TemplateRegistry {
17
+ private templates;
18
+ register(def: TemplateDefinition): void;
19
+ get(id: string): TemplateDefinition | undefined;
20
+ list(): TemplateDefinition[];
21
+ byCategory(cat: TemplateCategory): TemplateDefinition[];
22
+ }
23
+ export declare const templateRegistry: TemplateRegistry;
24
+ export declare const TEMPLATE_CATEGORY_LABELS: Record<TemplateCategory, string>;
25
+ export {};
@@ -0,0 +1,3 @@
1
+ import { TemplateDefinition } from './registry';
2
+ declare const def: TemplateDefinition;
3
+ export default def;
@@ -0,0 +1,3 @@
1
+ import { TemplateDefinition } from './registry';
2
+ declare const def: TemplateDefinition;
3
+ export default def;
@@ -0,0 +1,7 @@
1
+ import { Theme } from '../core/types';
2
+ export declare const minimalSaaS: Theme;
3
+ export declare const ecommercePromo: Theme;
4
+ export declare const luxuryBlack: Theme;
5
+ export declare const greenEco: Theme;
6
+ export declare const newsletterEditorial: Theme;
7
+ export declare const defaultThemes: Theme[];
package/package.json ADDED
@@ -0,0 +1,91 @@
1
+ {
2
+ "name": "@one-million-lines/email-builder",
3
+ "version": "0.1.0",
4
+ "description": "Embeddable visual email builder (React + vanilla JS) that composes modular marketing emails from a JSON model and exports table-based HTML.",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Alex Rada (https://linkedin.com/in/alexrada)",
8
+ "homepage": "https://github.com/one-million-lines/email-builder#readme",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/one-million-lines/email-builder.git"
12
+ },
13
+ "bugs": {
14
+ "url": "https://github.com/one-million-lines/email-builder/issues"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ },
19
+ "keywords": [
20
+ "email-builder",
21
+ "email-editor",
22
+ "email-template",
23
+ "drag-and-drop",
24
+ "react",
25
+ "html-email",
26
+ "newsletter",
27
+ "marketing",
28
+ "no-code",
29
+ "visual-editor"
30
+ ],
31
+ "engines": {
32
+ "node": ">=18"
33
+ },
34
+ "sideEffects": [
35
+ "**/*.css",
36
+ "**/templates/**"
37
+ ],
38
+ "main": "./dist/email-builder.cjs",
39
+ "module": "./dist/email-builder.js",
40
+ "types": "./dist/index.d.ts",
41
+ "exports": {
42
+ ".": {
43
+ "types": "./dist/index.d.ts",
44
+ "import": "./dist/email-builder.js",
45
+ "require": "./dist/email-builder.cjs"
46
+ },
47
+ "./styles.css": "./dist/styles.css",
48
+ "./package.json": "./package.json"
49
+ },
50
+ "files": [
51
+ "dist",
52
+ "README.md",
53
+ "LICENSE",
54
+ "CHANGELOG.md"
55
+ ],
56
+ "scripts": {
57
+ "dev": "vite",
58
+ "build": "vite build --config vite.lib.config.ts",
59
+ "build:demo": "tsc -b && vite build",
60
+ "preview": "vite preview",
61
+ "lint": "eslint .",
62
+ "test": "node --test test/*.test.mjs",
63
+ "prepublishOnly": "npm run build",
64
+ "validate:pack": "npm run build && npm pack --dry-run"
65
+ },
66
+ "peerDependencies": {
67
+ "react": "^18.0.0 || ^19.0.0",
68
+ "react-dom": "^18.0.0 || ^19.0.0"
69
+ },
70
+ "dependencies": {
71
+ "@dnd-kit/core": "^6.1.0",
72
+ "@dnd-kit/sortable": "^8.0.0",
73
+ "@dnd-kit/utilities": "^3.2.2",
74
+ "lucide-react": "^0.460.0",
75
+ "zod": "^3.23.8",
76
+ "zustand": "^5.0.2"
77
+ },
78
+ "devDependencies": {
79
+ "@tailwindcss/vite": "^4.1.0",
80
+ "@types/react": "^19.0.0",
81
+ "@types/react-dom": "^19.0.0",
82
+ "@vitejs/plugin-react-swc": "^3.5.0",
83
+ "jsdom": "^25.0.1",
84
+ "react": "^19.0.0",
85
+ "react-dom": "^19.0.0",
86
+ "tailwindcss": "^4.1.0",
87
+ "typescript": "^5.5.3",
88
+ "vite": "^5.4.10",
89
+ "vite-plugin-dts": "^4.0.0"
90
+ }
91
+ }