fragment-headless-sdk 2.4.3 → 2.5.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.
@@ -2,7 +2,7 @@
2
2
  import { XMarkIcon } from "@heroicons/react/24/outline";
3
3
  import React, { useEffect, useRef } from "react";
4
4
  import { AnnouncementType, ButtonType, SectionType } from "../../constants";
5
- import { fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, resolveAnnouncementColors, } from "../../utils";
5
+ import { fireImpressionWhenVisible, getThemeClasses, mergeSlotAttributes, mergeSlotClasses, mergeSlotStyles, normalizeRichTextHtml, resolveAnnouncementColors, resolveAnnouncementTypography, } from "../../utils";
6
6
  import AnnouncementButton from "./AnnouncementButton";
7
7
  import { AnnouncementStyles } from "./AnnouncementStyles";
8
8
  import CountdownTimer from "./CountdownTimer";
@@ -18,14 +18,16 @@ export default function Announcement({ content, type, handleClose, }) {
18
18
  return null;
19
19
  const styling = content.styling;
20
20
  const colors = resolveAnnouncementColors(content);
21
+ const typography = resolveAnnouncementTypography(content);
22
+ const announcementHtml = normalizeRichTextHtml(content.announcementHtml);
21
23
  const themeClasses = getThemeClasses(styling);
22
- const rootClass = mergeSlotClasses(`relative w-full ${themeClasses}`, styling, "root");
24
+ const rootClass = mergeSlotClasses(`fragment-announcement relative w-full ${themeClasses}`, styling, "root");
23
25
  const rootStyle = mergeSlotStyles({
24
26
  backgroundColor: colors.background,
25
27
  color: colors.text,
26
28
  }, styling, "root");
27
29
  const rootAttributes = mergeSlotAttributes(styling, "root");
28
- const innerClass = mergeSlotClasses("relative mx-auto flex max-w-screen-xl flex-col md:flex-row items-center justify-center gap-4 px-10 md:px-4 py-2 md:py-0 text-center md:text-left min-h-[50px]", styling, "inner", "wrapper");
30
+ const innerClass = mergeSlotClasses("relative mx-auto flex max-w-screen-xl flex-col md:flex-row items-center justify-center gap-4 px-10 md:pl-4 md:pr-10 py-2 md:py-0 text-center min-h-[50px]", styling, "inner", "wrapper");
29
31
  const innerStyle = mergeSlotStyles(undefined, styling, "inner", "wrapper");
30
32
  const innerAttributes = mergeSlotAttributes(styling, "inner", "wrapper");
31
33
  const marqueeContainerClass = mergeSlotClasses("flex w-full flex-col md:flex-row items-center justify-between gap-2 md:gap-4 overflow-hidden md:pr-8", styling, "marqueeContainer");
@@ -37,14 +39,14 @@ export default function Announcement({ content, type, handleClose, }) {
37
39
  const marqueeTextClass = mergeSlotClasses("whitespace-nowrap animate-marquee", styling, "marqueeText");
38
40
  const marqueeTextStyle = mergeSlotStyles(undefined, styling, "marqueeText");
39
41
  const marqueeTextAttributes = mergeSlotAttributes(styling, "marqueeText");
40
- const marqueeContentClass = mergeSlotClasses("inline-block max-w-none text-base", styling, "marqueeContent");
41
- const marqueeContentStyle = mergeSlotStyles(undefined, styling, "marqueeContent");
42
+ const marqueeContentClass = mergeSlotClasses(`fragment-announcement-text inline-block max-w-none font-semibold ${typography.fontSize} ${typography.lineHeight}`, styling, "marqueeContent");
43
+ const marqueeContentStyle = mergeSlotStyles({ fontFamily: typography.fontFamily }, styling, "marqueeContent");
42
44
  const marqueeContentAttributes = mergeSlotAttributes(styling, "marqueeContent");
43
45
  const contentRowClass = mergeSlotClasses("flex flex-col md:flex-row items-center justify-center gap-2 md:gap-4 w-full", styling, "contentRow");
44
46
  const contentRowStyle = mergeSlotStyles(undefined, styling, "contentRow");
45
47
  const contentRowAttributes = mergeSlotAttributes(styling, "contentRow");
46
- const announcementTextClass = mergeSlotClasses("max-w-none text-based", styling, "announcementText");
47
- const announcementTextStyle = mergeSlotStyles(undefined, styling, "announcementText");
48
+ const announcementTextClass = mergeSlotClasses(`fragment-announcement-text max-w-none font-semibold ${typography.fontSize} ${typography.lineHeight}`, styling, "announcementText");
49
+ const announcementTextStyle = mergeSlotStyles({ fontFamily: typography.fontFamily }, styling, "announcementText");
48
50
  const announcementTextAttributes = mergeSlotAttributes(styling, "announcementText");
49
51
  const closeButtonClass = mergeSlotClasses("absolute right-4 top-1/2 -translate-y-1/2 cursor-pointer", styling, "closeButton");
50
52
  const closeButtonStyle = mergeSlotStyles({ color: colors.closeButton }, styling, "closeButton");
@@ -56,12 +58,12 @@ export default function Announcement({ content, type, handleClose, }) {
56
58
  React.createElement("div", { className: marqueeTextWrapperClass, style: marqueeTextWrapperStyle, ...(marqueeTextWrapperAttributes ?? {}) },
57
59
  React.createElement("div", { className: marqueeTextClass, style: marqueeTextStyle, ...(marqueeTextAttributes ?? {}) },
58
60
  React.createElement("div", { className: marqueeContentClass, style: marqueeContentStyle, ...(marqueeContentAttributes ?? {}), dangerouslySetInnerHTML: {
59
- __html: content.announcementHtml || "",
61
+ __html: announcementHtml,
60
62
  } }))),
61
63
  content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref })))) : (React.createElement("div", { className: contentRowClass, style: contentRowStyle, ...(contentRowAttributes ?? {}) },
62
64
  React.createElement("div", { className: announcementTextClass, style: announcementTextStyle, ...(announcementTextAttributes ?? {}) },
63
65
  React.createElement("div", { dangerouslySetInnerHTML: {
64
- __html: content.announcementHtml || "",
66
+ __html: announcementHtml,
65
67
  } })),
66
68
  type === AnnouncementType.Countdown && (React.createElement(CountdownTimer, { content: content })),
67
69
  content.buttonText && content.buttonType !== ButtonType.None && (React.createElement(AnnouncementButton, { content: content, buttonHref: buttonHref })))),
@@ -8,9 +8,9 @@ interface HeroViewProps {
8
8
  contentWidthClass: string;
9
9
  typography: ReturnType<typeof resolveHeroTypography>;
10
10
  position: "left" | "center" | "right";
11
- height: string;
11
+ heightCss: string;
12
12
  buttonSpacing?: string;
13
13
  contentSpacing?: string;
14
14
  }
15
- export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, buttonSpacing, contentSpacing, }: HeroViewProps): React.JSX.Element;
15
+ export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, heightCss, buttonSpacing, contentSpacing, }: HeroViewProps): React.JSX.Element;
16
16
  export {};
@@ -3,7 +3,7 @@ import { SectionType } from "../../constants";
3
3
  import { fireClickMetric } from "../../utils";
4
4
  import { DEFAULT_CONTENT_WIDTH_CLASS, joinClassNames, renderText, } from "../../utils/hero-resolvers";
5
5
  import HeroCountdownTimer from "./HeroCountdownTimer";
6
- export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, height, buttonSpacing, contentSpacing, }) {
6
+ export default function DesktopHero({ buttonHref, content, colors, contentWidthClass, typography, position, heightCss, buttonSpacing, contentSpacing, }) {
7
7
  const getPositionClasses = () => {
8
8
  switch (position) {
9
9
  case "center":
@@ -18,7 +18,7 @@ export default function DesktopHero({ buttonHref, content, colors, contentWidthC
18
18
  const handleClick = React.useCallback(() => {
19
19
  fireClickMetric(content.measurementId, content.sectionType || SectionType.HeroBanner, content.sectionId);
20
20
  }, [content.measurementId, content.sectionType, content.sectionId]);
21
- return (React.createElement("div", { className: `relative ${height} gap-4 w-full`, style: { backgroundColor: colors.background } },
21
+ return (React.createElement("div", { className: "relative gap-4 w-full", style: { backgroundColor: colors.background, height: heightCss } },
22
22
  content?.videoUrl ? (React.createElement("video", { src: content.videoUrl, autoPlay: true, muted: true, loop: true, playsInline: true, className: "absolute inset-0 z-0 object-cover w-full h-full" })) : (
23
23
  /* Image Background */
24
24
  content?.imageUrl && (React.createElement("img", { src: content.imageUrl, alt: content.title || "Hero", className: "absolute inset-0 z-0 object-cover w-full h-full" }))),
@@ -19,12 +19,12 @@ export default function Hero({ content }) {
19
19
  const contentWidthClass = resolveContentWidthClass(content);
20
20
  const typography = resolveHeroTypography(content);
21
21
  const position = resolvePosition(content);
22
- const height = resolveHeight(content);
22
+ const heightCss = resolveHeight(content);
23
23
  const buttonSpacing = resolveButtonSpacing(content);
24
24
  const contentSpacing = resolveContentSpacing(content);
25
25
  return (React.createElement("div", { className: "bg-black", ref: ref, style: { backgroundColor: colors.background } },
26
26
  React.createElement("div", { className: "hidden lg:block" },
27
- React.createElement(DesktopHero, { content: content, buttonHref: buttonHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, height: height, buttonSpacing: buttonSpacing, contentSpacing: contentSpacing })),
27
+ React.createElement(DesktopHero, { content: content, buttonHref: buttonHref, colors: colors, contentWidthClass: contentWidthClass, typography: typography, position: position, heightCss: heightCss, buttonSpacing: buttonSpacing, contentSpacing: contentSpacing })),
28
28
  React.createElement("div", { className: "block lg:hidden" },
29
29
  React.createElement(MobileHero, { content: content, buttonHref: buttonHref, colors: colors, typography: typography, buttonSpacing: buttonSpacing, contentSpacing: contentSpacing }))));
30
30
  }
@@ -29,3 +29,14 @@
29
29
  color: currentColor;
30
30
  text-decoration: underline;
31
31
  }
32
+
33
+ /* Announcement text links: always inherit the surrounding text color and stay underlined */
34
+ .fragment-announcement-text a {
35
+ color: currentColor;
36
+ text-decoration: underline;
37
+ }
38
+
39
+ .fragment-announcement-text a:hover {
40
+ color: currentColor;
41
+ text-decoration: underline;
42
+ }
@@ -1,4 +1,5 @@
1
1
  import { IAnnouncementContent } from "../types";
2
+ import { FontKey } from "./hero-resolvers";
2
3
  export interface AnnouncementResolvedColors {
3
4
  background: string;
4
5
  text: string;
@@ -11,5 +12,12 @@ export interface CountdownResolvedColors {
11
12
  digitBackgroundColor: string;
12
13
  textColor: string;
13
14
  }
15
+ export interface AnnouncementTypographySettings {
16
+ fontSize: string;
17
+ lineHeight: string;
18
+ font: FontKey;
19
+ fontFamily: string;
20
+ }
14
21
  export declare function resolveAnnouncementColors(content: IAnnouncementContent): AnnouncementResolvedColors;
22
+ export declare function resolveAnnouncementTypography(content: IAnnouncementContent): AnnouncementTypographySettings;
15
23
  export declare function resolveCountdownColors(content: IAnnouncementContent): CountdownResolvedColors;
@@ -1,3 +1,4 @@
1
+ import { FONT_FAMILY_MAP } from "./hero-resolvers";
1
2
  import { resolveToken, resolveTokenByCategory } from "./styling";
2
3
  const DEFAULT_ANNOUNCEMENT_COLORS = {
3
4
  background: "#000000",
@@ -6,6 +7,12 @@ const DEFAULT_ANNOUNCEMENT_COLORS = {
6
7
  buttonText: "#000000",
7
8
  closeButton: "#ffffff",
8
9
  };
10
+ const DEFAULT_ANNOUNCEMENT_TYPOGRAPHY = {
11
+ fontSize: "text-base",
12
+ lineHeight: "leading-relaxed",
13
+ font: "geist",
14
+ fontFamily: FONT_FAMILY_MAP.geist,
15
+ };
9
16
  const DEFAULT_COUNTDOWN_COLORS = {
10
17
  digitColor: "#ffffff",
11
18
  digitBackgroundColor: "#000000",
@@ -32,6 +39,23 @@ export function resolveAnnouncementColors(content) {
32
39
  closeButton: fallbackColor(closeButton, fallbackColor(text, DEFAULT_ANNOUNCEMENT_COLORS.closeButton)),
33
40
  };
34
41
  }
42
+ export function resolveAnnouncementTypography(content) {
43
+ const typography = content?.styling?.tokens?.typography;
44
+ const rawFont = typography?.announcementFont;
45
+ const isValidFont = typeof rawFont === "string" &&
46
+ Object.prototype.hasOwnProperty.call(FONT_FAMILY_MAP, rawFont);
47
+ const font = isValidFont
48
+ ? rawFont
49
+ : DEFAULT_ANNOUNCEMENT_TYPOGRAPHY.font;
50
+ return {
51
+ fontSize: typography?.announcementFontSize ??
52
+ DEFAULT_ANNOUNCEMENT_TYPOGRAPHY.fontSize,
53
+ lineHeight: typography?.announcementLineHeight ??
54
+ DEFAULT_ANNOUNCEMENT_TYPOGRAPHY.lineHeight,
55
+ font,
56
+ fontFamily: FONT_FAMILY_MAP[font],
57
+ };
58
+ }
35
59
  export function resolveCountdownColors(content) {
36
60
  const styling = content.styling;
37
61
  const digitColor = resolveToken(styling, "counterDigitColor");
@@ -1,6 +1,6 @@
1
1
  import React from "react";
2
2
  import { IHeroContent } from "../types";
3
- import { CountdownResolvedColors } from "./announcement-resolvers";
3
+ import type { CountdownResolvedColors } from "./announcement-resolvers";
4
4
  export interface HeroResolvedColors {
5
5
  title: string;
6
6
  text: string;
@@ -10,7 +10,16 @@ export interface HeroResolvedColors {
10
10
  }
11
11
  export declare const DEFAULT_COLORS: HeroResolvedColors;
12
12
  export declare const DEFAULT_CONTENT_WIDTH_CLASS = "w-2/5";
13
- export declare const DEFAULT_HEIGHT_CLASS = "h-[400px]";
13
+ export declare const DEFAULT_HEIGHT_CSS = "400px";
14
+ /**
15
+ * Converts a Tailwind height class to a CSS value for use in inline styles.
16
+ * Handles arbitrary values (`h-[400px]`), named tokens (`h-screen`), and
17
+ * raw CSS values passed through unchanged (`400px`, `50vh`).
18
+ *
19
+ * Inline style is required because height tokens are stored in the database
20
+ * at runtime — Tailwind's static scanner cannot include them in the CSS bundle.
21
+ */
22
+ export declare function parseTailwindHeight(cls: string): string;
14
23
  export declare const FONT_FAMILY_MAP: {
15
24
  readonly geist: "\"Geist\", -apple-system, BlinkMacSystemFont, sans-serif";
16
25
  readonly georgia: "\"Georgia\", \"Times New Roman\", serif";
@@ -1,4 +1,5 @@
1
1
  import React from "react";
2
+ import { normalizeRichTextHtml } from "./html";
2
3
  import { resolveToken } from "./styling";
3
4
  export const DEFAULT_COLORS = {
4
5
  title: "#ffffff",
@@ -8,7 +9,35 @@ export const DEFAULT_COLORS = {
8
9
  background: "#000000",
9
10
  };
10
11
  export const DEFAULT_CONTENT_WIDTH_CLASS = "w-2/5";
11
- export const DEFAULT_HEIGHT_CLASS = "h-[400px]";
12
+ export const DEFAULT_HEIGHT_CSS = "400px";
13
+ /**
14
+ * Converts a Tailwind height class to a CSS value for use in inline styles.
15
+ * Handles arbitrary values (`h-[400px]`), named tokens (`h-screen`), and
16
+ * raw CSS values passed through unchanged (`400px`, `50vh`).
17
+ *
18
+ * Inline style is required because height tokens are stored in the database
19
+ * at runtime — Tailwind's static scanner cannot include them in the CSS bundle.
20
+ */
21
+ export function parseTailwindHeight(cls) {
22
+ const trimmed = cls?.trim() ?? "";
23
+ if (trimmed.length === 0)
24
+ return DEFAULT_HEIGHT_CSS;
25
+ const match = trimmed.match(/^h-\[(.+)\]$/);
26
+ if (match)
27
+ return match[1];
28
+ const map = {
29
+ "h-screen": "100vh",
30
+ "h-full": "100%",
31
+ "h-auto": "auto",
32
+ "h-fit": "fit-content",
33
+ };
34
+ if (map[trimmed])
35
+ return map[trimmed];
36
+ // Pass through values that are already valid CSS (e.g. "500px", "50vh")
37
+ if (!trimmed.startsWith("h-"))
38
+ return trimmed;
39
+ return DEFAULT_HEIGHT_CSS;
40
+ }
12
41
  export const FONT_FAMILY_MAP = {
13
42
  geist: '"Geist", -apple-system, BlinkMacSystemFont, sans-serif',
14
43
  georgia: '"Georgia", "Times New Roman", serif',
@@ -67,9 +96,9 @@ export function resolveHeight(content) {
67
96
  const layout = content?.styling?.tokens?.layout ?? {};
68
97
  const rawHeight = layout.height;
69
98
  if (typeof rawHeight === "string" && rawHeight.trim().length > 0) {
70
- return rawHeight.trim();
99
+ return parseTailwindHeight(rawHeight.trim());
71
100
  }
72
- return DEFAULT_HEIGHT_CLASS;
101
+ return DEFAULT_HEIGHT_CSS;
73
102
  }
74
103
  const DEFAULT_BUTTON_SPACING = "mt-4";
75
104
  export function resolveButtonSpacing(content) {
@@ -127,16 +156,17 @@ export function renderText({ fontSize, lineHeight, text, className, color, font,
127
156
  if (!text || text.trim().length === 0) {
128
157
  return null;
129
158
  }
130
- const baseClasses = "drop-shadow-lg max-w-full break-words";
159
+ const baseClasses = "drop-shadow-lg max-w-full";
131
160
  const combinedClasses = className
132
161
  ? `${baseClasses} ${fontSize} ${lineHeight} ${className}`
133
162
  : `${baseClasses} ${fontSize} ${lineHeight}`;
163
+ const displayHtml = normalizeRichTextHtml(text);
134
164
  return React.createElement("div", {
135
165
  className: combinedClasses,
136
166
  style: {
137
167
  color,
138
168
  fontFamily: FONT_FAMILY_MAP[font] ?? FONT_FAMILY_MAP.geist,
139
169
  },
140
- dangerouslySetInnerHTML: { __html: text },
170
+ dangerouslySetInnerHTML: { __html: displayHtml },
141
171
  });
142
172
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Rich-text editors often emit `&nbsp;` between words to preserve spacing.
3
+ * That prevents normal word wrapping and causes overflow in constrained
4
+ * containers (marquees, single-line announcements, heros). Replace them with
5
+ * regular spaces so the browser can break on them as expected.
6
+ */
7
+ export declare const normalizeRichTextHtml: (html: string | null | undefined) => string;
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Rich-text editors often emit `&nbsp;` between words to preserve spacing.
3
+ * That prevents normal word wrapping and causes overflow in constrained
4
+ * containers (marquees, single-line announcements, heros). Replace them with
5
+ * regular spaces so the browser can break on them as expected.
6
+ */
7
+ export const normalizeRichTextHtml = (html) => (html ?? "").replace(/&nbsp;/g, "\u0020");
@@ -3,5 +3,6 @@ export * from "./attribution";
3
3
  export * from "./cache";
4
4
  export * from "./fetch-resource";
5
5
  export * from "./hero-resolvers";
6
+ export * from "./html";
6
7
  export * from "./metrics";
7
8
  export * from "./styling";
@@ -3,5 +3,6 @@ export * from "./attribution";
3
3
  export * from "./cache";
4
4
  export * from "./fetch-resource";
5
5
  export * from "./hero-resolvers";
6
+ export * from "./html";
6
7
  export * from "./metrics";
7
8
  export * from "./styling";
package/docs/CHANGELOG.md CHANGED
@@ -7,11 +7,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ### [Unreleased]
9
9
 
10
+ #### Added
11
+
12
+ - **`normalizeRichTextHtml` utility** – New shared helper in `utils/html.ts` that normalizes rich-text editor output (replaces `&nbsp;` with regular spaces so words wrap at natural boundaries). Exported from the SDK entrypoint and used by both `Announcement` and `renderText` in `hero-resolvers`, replacing previously duplicated inline implementations.
13
+ - **`parseTailwindHeight` is now public** – Previously an internal helper, now exported so preview/template consumers (e.g. `HeroBannerTemplate`) can share the same Tailwind-height → CSS conversion. Also accepts raw CSS values (`500px`, `50vh`) as pass-through (`hero-resolvers.ts`).
14
+ - **`AnnouncementTypographySettings.fontFamily`** – `resolveAnnouncementTypography` now returns a resolved `fontFamily` CSS value alongside `font`/`fontSize`/`lineHeight`, so callers no longer need to import `FONT_FAMILY_MAP` to translate the font key themselves.
15
+ - **Font key validation in `resolveAnnouncementTypography`** – Invalid/stale `announcementFont` values stored in the database now fall back to `geist` instead of being silently cast to a non-existent `FontKey` (`announcement-resolvers.ts`).
16
+
17
+ #### Changed
18
+
19
+ - **Announcement applies typography tokens** – `Announcement` now applies `announcementFontSize`, `announcementLineHeight`, and `announcementFont` (as `fontFamily`) to marquee and standard text elements. Previously these tokens were ignored and text always rendered at `text-base` with no font override (`components/Announcement/index.tsx`).
20
+ - **Announcement inner wrapper padding** – Reserves right-side padding on `md+` (`md:pl-4 md:pr-10`) so the close button no longer overlaps long announcement text, and drops `md:text-left` so copy stays centered across breakpoints (`components/Announcement/index.tsx`).
21
+ - **`DesktopHero` prop rename** – `height` → `heightCss` to reflect that the value is a CSS string, not a Tailwind class. Callers updated accordingly (`components/Hero/DesktopHero.tsx`, `components/Hero/index.tsx`).
22
+
23
+ #### Fixed
24
+
25
+ - **Announcement text breaking mid-word** – `Announcement` now normalizes `&nbsp;` entities to regular spaces before rendering so words wrap at natural boundaries. Applied to both marquee and standard/countdown layouts (`components/Announcement/index.tsx`).
26
+ - **Announcement text class typo** – Replaced `max-w-none text-based` (invalid Tailwind class) with the correct resolved `${fontSize} ${lineHeight}` tokens (`components/Announcement/index.tsx`).
27
+ - **Announcement text links** – Links inside `.fragment-announcement-text` now inherit surrounding text color and stay underlined on hover, matching the existing hero link behavior (`styles/fragment-sdk.css`).
28
+
29
+ ### [2.4.4] - 2026-04-14
30
+
31
+ #### Fixed
32
+
33
+ - **Hero height ignored for non-default values**: `resolveHeight` now converts stored Tailwind height classes (e.g. `h-[500px]`) to CSS values and applies them via inline `style`. Arbitrary-value classes from the database are never included in Tailwind's generated CSS bundle at build time, so any height other than the 400 px default was silently ignored (`hero-resolvers.ts`, `DesktopHero.tsx`).
34
+ - **Hero text breaking mid-word**: `renderText` now replaces `&nbsp;` entities with regular spaces before rendering so words wrap naturally at boundaries. Removed `break-words` which was masking the underlying cause rather than fixing it (`hero-resolvers.ts`).
35
+
10
36
  ### [2.4.3] - 2026-04-14
11
37
 
12
38
  #### Fixed
13
39
 
14
- - **Hero text overflow**: `renderText` base classes now include `max-w-full break-words` so title and description respect their content column width and wrap correctly when content contains non-breaking spaces.
15
40
  - **Desktop hero content alignment**: `DesktopHero` content column now uses `items-start` so child elements (button, countdown) no longer stretch to full column width.
16
41
 
17
42
  ### [2.4.2] - 2026-04-14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fragment-headless-sdk",
3
- "version": "2.4.3",
3
+ "version": "2.5.0",
4
4
  "description": "Official SDK for Fragment-Shopify CMS: React components, TypeScript types, and utilities for headless Shopify storefronts.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",