@unlayer/react-elements 0.1.9 → 0.1.10

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
@@ -51,14 +51,17 @@ function WelcomeEmail() {
51
51
 
52
52
  These props have non-obvious shapes that **must** be followed exactly:
53
53
 
54
- - **fontFamily**: Must be `{ label: string, value: string }`, NOT a plain string.
54
+ - **fontFamily**: Accepts a plain family-name string (`fontFamily="Georgia"`) or, for a full stack, `{ label, value }` (recommended).
55
55
  ```tsx
56
56
  fontFamily={{ label: "Arial", value: "arial, sans-serif" }}
57
57
  ```
58
- - **fontWeight**: Must be a number (`400`, `700`), NOT a string (`"400"`).
58
+ - **fontWeight**: Accepts a number (`700`), a numeric string (`"700"`), or a CSS keyword (`"bold"`).
59
+ - **fontSize / padding**: Accept a CSS string (`"28px"`, `"20px 40px"`) or a bare number (treated as px: `fontSize={28}` → `28px`).
60
+ - **lineHeight**: Accepts a CSS string (`"1.4"`, `"140%"`) or a bare number (kept **unitless**: `lineHeight={1.4}` → `"1.4"`).
59
61
  - **Wrapper component**: Use `<Email>`, `<Page>`, or `<Document>` as root — they set the rendering mode automatically.
60
62
  - **href**: Can be a plain string URL (auto-wrapped) or `{ name: "web", values: { href, target } }`.
61
- - **Image src**: Can be a plain string URL (auto-wrapped) or `{ url, width?, autoWidth?, maxWidth? }`.
63
+ - **Image sizing**: `src` is a plain URL string or `{ url, width?, height?, ... }`, where `width`/`height` are the image's **natural** size. By default an image is **responsive** — it fills its container, capped at its natural size. For a **fixed** display size, use a **percent**: `width="50%"` or `maxWidth="50%"`. A px/number `width` is treated as the natural size, so `width="300px"` shows the image at up to 300px (responsive).
64
+ - **Heading level**: `headingType` (or its alias `level`) accepts `h1`–`h6`.
62
65
  - **children**: Text components accept children as shorthand. `<Heading>Hello</Heading>` sets the heading text. `<Paragraph>` supports children for plain text.
63
66
  - **Paragraph text**: Use `html` prop for text content (supports inline formatting like `<b>`, `<a>`). Use children for plain text.
64
67
 
@@ -154,6 +157,7 @@ Must be child of Row. Count must match layout.
154
157
  - `fontFamily?: { label: string, value: string }`
155
158
  - `padding?: string` — `"10px 20px"`
156
159
  - `borderRadius?: string` — `"4px"`
160
+ - `width?: number | string` — display width; `width="100%"` makes the button full-width, `width="200px"` pins it
157
161
  - `textAlign?: "left" | "center" | "right"` — `"center"`
158
162
 
159
163
  ### Paragraph
@@ -439,13 +443,12 @@ const monoFont = { label: "Monospace", value: "'SF Mono', 'Fira Code', 'Roboto M
439
443
 
440
444
  ## Common Mistakes
441
445
 
442
- 1. **fontFamily as string** — `fontFamily="Arial"` Must be `fontFamily={{ label: "Arial", value: "arial, sans-serif" }}`
443
- 2. **fontWeight as string** — `fontWeight="700"` Must be `fontWeight={700}`
444
- 3. **Column count mismatch** — `TwoEqual` layout requires exactly 2 `<Column>` children
445
- 4. **Missing Column** — Items must be inside `<Column>`, never directly in `<Row>`
446
- 5. **Missing Row** — Columns must be inside `<Row>`, never directly in `<Email>`/`<Page>`/`<Document>`
447
- 6. **Paragraph text prop**Use `html` prop or children, not `text` (which is not typed for Paragraph)
448
- 7. **padding without units** — Use `padding="0px"` not `padding="0"` — the type requires the `px` suffix for consistency
446
+ 1. **Column count mismatch** — `TwoEqual` layout requires exactly 2 `<Column>` children
447
+ 2. **Missing Column** — Items must be inside `<Column>`, never directly in `<Row>`
448
+ 3. **Missing Row** — Columns must be inside `<Row>`, never directly in `<Email>`/`<Page>`/`<Document>`
449
+ 4. **JSX formatting in children** — `<Heading>Hi <b>x</b></Heading>` is flattened to plain text (the formatting is **not** preserved). For inline formatting use `<Paragraph html="Hi <b>x</b>" />`.
450
+
451
+ > Note: the CSS-idiom forms that used to be mistakes now work a string `fontFamily`, a string/number `fontWeight`, a numeric `fontSize`, `padding="0"`, and `<Paragraph text="..." />` are all accepted and normalized. The object/numeric forms above are still recommended for clarity.
449
452
 
450
453
  ## Development
451
454
 
package/dist/index.cjs CHANGED
@@ -208,16 +208,53 @@ function analyzeNestedStructure(defaultValues) {
208
208
  nestedStructureCache.set(defaultValues, nestedGroups);
209
209
  return nestedGroups;
210
210
  }
211
+ var PX_SIZE_KEYS = [
212
+ "fontSize",
213
+ "padding",
214
+ "containerPadding",
215
+ "borderRadius",
216
+ "letterSpacing"
217
+ ];
218
+ function normalizeCssProps(props) {
219
+ if (typeof props.fontFamily === "string") {
220
+ const v = props.fontFamily;
221
+ props.fontFamily = { label: v, value: v };
222
+ }
223
+ if (typeof props.fontWeight === "string" && /^\d+$/.test(props.fontWeight.trim())) {
224
+ props.fontWeight = Number(props.fontWeight.trim());
225
+ }
226
+ if (typeof props.lineHeight === "number") {
227
+ props.lineHeight = String(props.lineHeight);
228
+ }
229
+ for (const key of PX_SIZE_KEYS) {
230
+ const v = props[key];
231
+ if (typeof v === "number") {
232
+ props[key] = `${v}px`;
233
+ } else if (typeof v === "string" && /^\d+$/.test(v.trim())) {
234
+ props[key] = `${v.trim()}px`;
235
+ }
236
+ }
237
+ }
238
+ function flattenChildrenText(node) {
239
+ if (node == null || typeof node === "boolean") return "";
240
+ if (typeof node === "string") return node;
241
+ if (typeof node === "number") return String(node);
242
+ if (Array.isArray(node)) return node.map(flattenChildrenText).join("");
243
+ if (typeof node === "object" && "props" in node) {
244
+ return flattenChildrenText(node.props?.children);
245
+ }
246
+ return "";
247
+ }
211
248
  function mapSemanticProps(props, defaultValues, componentType) {
212
249
  const { children, values, ...restProps } = props;
213
250
  const userProps = { ...restProps };
214
251
  const result = values ? { ...values } : {};
215
252
  if (children !== void 0 && !result.text && !result.textJson) {
253
+ const textContent = typeof children === "string" ? children : flattenChildrenText(children);
216
254
  if (componentType === "Paragraph") {
217
- const textContent = typeof children === "string" ? children : String(children);
218
255
  result.textJson = textToTextJson(textContent);
219
256
  } else {
220
- result.text = children;
257
+ result.text = textContent;
221
258
  }
222
259
  }
223
260
  const textFromEscapeHatch = result.text;
@@ -243,6 +280,7 @@ function mapSemanticProps(props, defaultValues, componentType) {
243
280
  };
244
281
  }
245
282
  }
283
+ normalizeCssProps(userProps);
246
284
  const nestedGroups = analyzeNestedStructure(defaultValues);
247
285
  const nested = {};
248
286
  const flat = {};
@@ -277,6 +315,19 @@ function mapSemanticProps(props, defaultValues, componentType) {
277
315
  };
278
316
  }
279
317
  }
318
+ if (defaultValues && typeof defaultValues === "object" && "border" in defaultValues) {
319
+ const borderSideRe = /^border(Top|Right|Bottom|Left)(Width|Style|Color)$/;
320
+ const collected = {};
321
+ for (const key of Object.keys(final)) {
322
+ if (borderSideRe.test(key)) {
323
+ collected[key] = final[key];
324
+ delete final[key];
325
+ }
326
+ }
327
+ if (Object.keys(collected).length > 0) {
328
+ final.border = { ...final.border || {}, ...collected };
329
+ }
330
+ }
280
331
  return final;
281
332
  }
282
333
  function normalizeLinkValue(value) {
@@ -545,7 +596,26 @@ var DEFAULT_VALUES = {
545
596
  var Button = createItemComponent({
546
597
  name: "Button",
547
598
  defaultValues: DEFAULT_VALUES,
548
- propMapper: (props) => mapSemanticProps(props, DEFAULT_VALUES, "Button"),
599
+ propMapper: (props) => {
600
+ const mapped = mapSemanticProps(
601
+ props,
602
+ DEFAULT_VALUES,
603
+ "Button"
604
+ );
605
+ const size = mapped.size;
606
+ if (size && typeof size === "object" && !Array.isArray(size)) {
607
+ const s = size;
608
+ if (typeof s.width === "number") {
609
+ s.width = `${s.width}px`;
610
+ } else if (typeof s.width === "string" && /^\d+(?:\.\d+)?$/.test(s.width.trim())) {
611
+ s.width = `${s.width.trim()}px`;
612
+ }
613
+ if (s.width !== void 0 && s.autoWidth === void 0) {
614
+ s.autoWidth = false;
615
+ }
616
+ }
617
+ return mapped;
618
+ },
549
619
  displayName: "Button",
550
620
  exporters: exporters.ButtonExporters
551
621
  });
@@ -610,18 +680,46 @@ var Image = createItemComponent({
610
680
  defaultValues: DEFAULT_VALUES5,
611
681
  propMapper: (props) => {
612
682
  const { alt, src, ...rest } = props;
683
+ const restValues = rest.values;
684
+ const normalizedRest = restValues && typeof restValues.src === "string" ? { ...rest, values: { ...restValues, src: { url: restValues.src } } } : rest;
613
685
  const base = mapSemanticProps(
614
- rest,
686
+ normalizedRest,
615
687
  DEFAULT_VALUES5,
616
688
  "Image"
617
689
  );
618
690
  if (alt !== void 0) {
619
691
  base.altText = alt;
620
692
  }
621
- if (typeof src === "string") {
622
- base.src = { url: src, autoWidth: true, maxWidth: "100%" };
623
- } else if (src !== void 0) {
624
- base.src = { ...DEFAULT_VALUES5.src, ...src };
693
+ const baseSrc = base.src;
694
+ const fromValues = baseSrc && typeof baseSrc === "object" && !Array.isArray(baseSrc) ? baseSrc : typeof baseSrc === "string" ? { url: baseSrc } : {};
695
+ const fromProp = typeof src === "string" ? { url: src } : src ?? {};
696
+ const userSrc = { ...fromValues, ...fromProp };
697
+ if (src !== void 0 || baseSrc !== void 0) {
698
+ const isStringUrl = typeof src === "string" || src === void 0 && typeof baseSrc === "string";
699
+ const start = isStringUrl ? { autoWidth: true, maxWidth: "100%" } : { ...DEFAULT_VALUES5.src };
700
+ const merged = { ...start, ...userSrc };
701
+ const pctRe = /^\d+(?:\.\d+)?%$/;
702
+ if (typeof merged.width === "string") {
703
+ const t = merged.width.trim();
704
+ if (pctRe.test(t)) {
705
+ if (userSrc.maxWidth === void 0) merged.maxWidth = t;
706
+ delete merged.width;
707
+ } else {
708
+ const px = /^(\d+(?:\.\d+)?)(?:px)?$/.exec(t);
709
+ if (px) merged.width = parseFloat(px[1]);
710
+ }
711
+ }
712
+ const displayPct = typeof merged.maxWidth === "string" && pctRe.test(merged.maxWidth.trim()) ? merged.maxWidth.trim() : void 0;
713
+ if (userSrc.autoWidth === void 0) {
714
+ if (displayPct && displayPct !== "100%") {
715
+ merged.autoWidth = false;
716
+ merged.maxWidth = displayPct;
717
+ } else {
718
+ merged.autoWidth = true;
719
+ merged.maxWidth = "100%";
720
+ }
721
+ }
722
+ base.src = merged;
625
723
  }
626
724
  return base;
627
725
  },
@@ -732,25 +830,37 @@ var Table = createItemComponent({
732
830
  name: "Table",
733
831
  defaultValues: DEFAULT_VALUES9,
734
832
  propMapper: (props) => {
735
- const { headers, data, ...rest } = props;
736
- if (headers || data) {
833
+ const { headers, data, columns, rows, ...rest } = props;
834
+ if (headers || data || typeof columns === "number" || typeof rows === "number") {
737
835
  const base = mapSemanticProps(
738
836
  rest,
739
837
  DEFAULT_VALUES9,
740
838
  "Table"
741
839
  );
840
+ const colCount = headers ? headers.length : typeof columns === "number" ? columns : data?.[0]?.length ?? 0;
841
+ const blankCells = (n) => Array.from({ length: n }, () => ({ text: "", width: 0 }));
742
842
  const tableHeaders = headers ? [{ cells: headers.map((text) => ({ text, width: 0 })), height: 0 }] : [];
743
843
  const tableRows = data ? data.map((row) => ({
744
844
  cells: row.map((text) => ({ text, width: 0 })),
745
845
  height: 0
746
- })) : [];
846
+ })) : typeof rows === "number" ? (
847
+ // No data: build an empty grid sized by `columns` × `rows`.
848
+ Array.from({ length: rows }, () => ({
849
+ cells: blankCells(colCount),
850
+ height: 0
851
+ }))
852
+ ) : [];
747
853
  base.table = { headers: tableHeaders, rows: tableRows, footers: [] };
854
+ if (headers || typeof columns === "number") {
855
+ base.columns = colCount;
856
+ }
748
857
  if (headers) {
749
- base.columns = headers.length;
750
858
  base.enableHeader = true;
751
859
  }
752
860
  if (data) {
753
861
  base.rows = data.length;
862
+ } else if (typeof rows === "number") {
863
+ base.rows = rows;
754
864
  }
755
865
  return base;
756
866
  }
@@ -1619,6 +1729,10 @@ function processBody(element, counters) {
1619
1729
  const semanticProps = extractSemanticProps2(element.props);
1620
1730
  const mapped = mapSemanticProps(semanticProps, BODY_DEFAULTS, "Body");
1621
1731
  const values = mergeValues(BODY_DEFAULTS, mapped);
1732
+ const previewText = element.props.previewText;
1733
+ if (previewText !== void 0) {
1734
+ values.preheaderText = previewText;
1735
+ }
1622
1736
  const valuesWithMeta = {
1623
1737
  ...values,
1624
1738
  _meta: {