@one-million-lines/email-builder 0.2.2 → 0.3.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
@@ -155,6 +155,49 @@ response is validated with Zod before it is applied. See
155
155
  [`src/ai/README.md`](./src/ai/README.md) and
156
156
  [`backend/README.md`](./backend/README.md).
157
157
 
158
+ ### Product search
159
+
160
+ Connect product cards to your catalog. When a product provider is configured, a
161
+ **Find** button appears on any product grid (and a search icon on each card).
162
+ Searching opens a modal, previews the returned product, and — on **Save** —
163
+ populates the card's fields (which stay fully editable afterwards).
164
+
165
+ ```tsx
166
+ <EmailBuilder productEndpoint="http://localhost:3001/products/search" />
167
+ ```
168
+
169
+ ```ts
170
+ import { registerPlugin, productSearchPlugin } from "@one-million-lines/email-builder";
171
+ registerPlugin(productSearchPlugin({ endpoint: "http://localhost:3001/products/search" }));
172
+ ```
173
+
174
+ The demo backend in [`backend/`](./backend) ships a `/products/search` endpoint
175
+ over a small in-memory catalog. Product fields are `title`, `final_price`,
176
+ `old_price` (optional), `description`, `link`, `image` and `stars` (optional).
177
+ See [`src/plugins/productSearch/README.md`](./src/plugins/productSearch/README.md)
178
+ for the wire protocol and response mapping.
179
+
180
+ ### Voucher select
181
+
182
+ Let users pick a discount code from your backend instead of typing it. When a
183
+ voucher provider is configured, selecting a **voucher block** (the built-in
184
+ **Voucher Code** module, or any module with a `voucherCode` text element) shows a
185
+ **Select voucher** dropdown that fills the code. The list is fetched once and
186
+ cached (lazily, or eagerly with `preload`).
187
+
188
+ ```tsx
189
+ <EmailBuilder voucherEndpoint="http://localhost:3001/vouchers" />
190
+ ```
191
+
192
+ ```ts
193
+ import { registerPlugin, voucherPlugin } from "@one-million-lines/email-builder";
194
+ registerPlugin(voucherPlugin({ endpoint: "http://localhost:3001/vouchers", preload: true }));
195
+ ```
196
+
197
+ The demo backend ships a `GET /vouchers` endpoint. See
198
+ [`src/plugins/voucherSelect/README.md`](./src/plugins/voucherSelect/README.md)
199
+ for the wire protocol and response mapping.
200
+
158
201
  ### Styling & isolation
159
202
 
160
203
  The stylesheet at `@one-million-lines/email-builder/styles.css` is designed to
@@ -7,13 +7,50 @@ export interface AssetProvider {
7
7
  alt?: string;
8
8
  }>;
9
9
  }
10
+ /**
11
+ * A single product returned by a {@link ProductProvider} search. Field names
12
+ * match the builder's `Product` model so results drop straight into a card.
13
+ */
14
+ export interface ProductSearchResult {
15
+ name: string;
16
+ finalPrice: string;
17
+ oldPrice?: string;
18
+ description?: string;
19
+ link?: string;
20
+ image?: string;
21
+ imageAlt?: string;
22
+ stars?: number;
23
+ /** Optional external identifier echoed back from the backend. */
24
+ sku?: string;
25
+ }
26
+ export interface ProductProvider {
27
+ /** Look up a single product for a free-text query. Resolves null if none. */
28
+ search: (query: string) => Promise<ProductSearchResult | null>;
29
+ }
30
+ /** A discount/voucher entry returned by a {@link VoucherProvider}. */
31
+ export interface Voucher {
32
+ /** Stable identifier (used to remember the selection). */
33
+ id: string;
34
+ /** Human label shown in the select dropdown. */
35
+ title: string;
36
+ /** The code (or merge tag) inserted into the voucher block. */
37
+ code: string;
38
+ /** Optional longer description. */
39
+ description?: string;
40
+ }
41
+ export interface VoucherProvider {
42
+ /** Load the list of vouchers to choose from. */
43
+ list: () => Promise<Voucher[]>;
44
+ }
10
45
  export interface BuilderHandle {
11
46
  registerModule: (def: ModuleDefinition) => void;
12
47
  registerTheme: (theme: Theme) => void;
13
48
  registerAssetProvider: (provider: AssetProvider) => void;
49
+ registerProductProvider: (provider: ProductProvider) => void;
50
+ registerVoucherProvider: (provider: VoucherProvider) => void;
14
51
  setAIProvider: (provider: AIProvider) => void;
15
52
  }
16
- export type PluginType = "modules" | "themes" | "asset-provider" | "ai-provider";
53
+ export type PluginType = "modules" | "themes" | "asset-provider" | "product-provider" | "voucher-provider" | "ai-provider";
17
54
  export interface Plugin {
18
55
  name: string;
19
56
  type: PluginType;
@@ -23,4 +60,6 @@ export declare const builder: BuilderHandle;
23
60
  export declare function registerPlugin(plugin: Plugin): void;
24
61
  export declare function getRegisteredThemes(): Theme[];
25
62
  export declare function getAssetProvider(): AssetProvider | null;
63
+ export declare function getProductProvider(): ProductProvider | null;
64
+ export declare function getVoucherProvider(): VoucherProvider | null;
26
65
  export declare function getAIProvider(): AIProvider | null;
@@ -18,7 +18,7 @@ export interface BaseStyle {
18
18
  export interface TextElement {
19
19
  id: string;
20
20
  type: "text";
21
- role?: "headline" | "subheadline" | "body" | "caption";
21
+ role?: "headline" | "subheadline" | "body" | "caption" | "voucherCode";
22
22
  content: string;
23
23
  style?: BaseStyle & {
24
24
  fontFamily?: string;
@@ -79,6 +79,8 @@ export interface Product {
79
79
  description?: string;
80
80
  link?: string;
81
81
  buttonLabel?: string;
82
+ /** Optional 0–5 rating. Rendered as star glyphs when the grid shows stars. */
83
+ stars?: number;
82
84
  }
83
85
  export interface ProductGridElement {
84
86
  id: string;
@@ -88,6 +90,8 @@ export interface ProductGridElement {
88
90
  showOldPrice: boolean;
89
91
  showButton: boolean;
90
92
  showDescription: boolean;
93
+ /** Show the star rating on each product card. Optional for back-compat. */
94
+ showStars?: boolean;
91
95
  buttonLabel?: string;
92
96
  style?: BaseStyle & {
93
97
  nameColor?: string;
@@ -1,3 +1,10 @@
1
1
  export declare function uid(prefix?: string): string;
2
2
  export declare function escapeHtml(s: string): string;
3
3
  export declare function safeUrl(url: string | undefined): string;
4
+ /** Default gold used for product star ratings. */
5
+ export declare const STAR_COLOR = "#F5A623";
6
+ /**
7
+ * Build a 5-glyph star string for a 0–5 rating (rounded to the nearest whole
8
+ * star). Uses ★ (full) and ☆ (empty) so it renders in any email client.
9
+ */
10
+ export declare function starGlyphs(rating: number): string;
@@ -44,11 +44,13 @@ export declare const moduleSchema: z.ZodObject<{
44
44
  description: z.ZodOptional<z.ZodString>;
45
45
  link: z.ZodOptional<z.ZodString>;
46
46
  buttonLabel: z.ZodOptional<z.ZodString>;
47
+ stars: z.ZodOptional<z.ZodNumber>;
47
48
  }, z.core.$strip>>;
48
49
  columns: z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>;
49
50
  showOldPrice: z.ZodBoolean;
50
51
  showButton: z.ZodBoolean;
51
52
  showDescription: z.ZodBoolean;
53
+ showStars: z.ZodOptional<z.ZodBoolean>;
52
54
  buttonLabel: z.ZodOptional<z.ZodString>;
53
55
  style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
54
56
  }, z.core.$strip>], "type">>;
@@ -130,11 +132,13 @@ export declare const documentSchema: z.ZodObject<{
130
132
  description: z.ZodOptional<z.ZodString>;
131
133
  link: z.ZodOptional<z.ZodString>;
132
134
  buttonLabel: z.ZodOptional<z.ZodString>;
135
+ stars: z.ZodOptional<z.ZodNumber>;
133
136
  }, z.core.$strip>>;
134
137
  columns: z.ZodUnion<readonly [z.ZodLiteral<1>, z.ZodLiteral<2>, z.ZodLiteral<3>]>;
135
138
  showOldPrice: z.ZodBoolean;
136
139
  showButton: z.ZodBoolean;
137
140
  showDescription: z.ZodBoolean;
141
+ showStars: z.ZodOptional<z.ZodBoolean>;
138
142
  buttonLabel: z.ZodOptional<z.ZodString>;
139
143
  style: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodNumber, z.ZodBoolean, z.ZodRecord<z.ZodString, z.ZodAny>]>>>;
140
144
  }, z.core.$strip>], "type">>;