@windstream/react-shared-components 0.2.85 → 0.2.87

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 (54) hide show
  1. package/dist/contentful/index.d.ts +19 -3
  2. package/dist/contentful/index.esm.js +4 -4
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +4 -4
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +3 -3
  7. package/dist/index.d.ts +3 -3
  8. package/package.json +1 -1
  9. package/src/contentful/blocks/accordion/index.test.tsx +53 -0
  10. package/src/contentful/blocks/accordion/index.tsx +7 -2
  11. package/src/contentful/blocks/accordion/types.ts +4 -0
  12. package/src/contentful/blocks/blogs-grid/{BlogGrid.stories.tsx → blog-grid.stories.tsx} +1 -1
  13. package/src/contentful/blocks/callout/field-props.test.tsx +184 -0
  14. package/src/contentful/blocks/callout/index.tsx +26 -4
  15. package/src/contentful/blocks/callout/types.ts +17 -1
  16. package/src/contentful/blocks/cards/blog-card/index.tsx +6 -2
  17. package/src/contentful/blocks/cards/blog-card/types.ts +4 -0
  18. package/src/contentful/blocks/cards/floating-image-card/index.tsx +4 -2
  19. package/src/contentful/blocks/cards/floating-image-card/types.ts +4 -0
  20. package/src/contentful/blocks/cards/full-image-card/index.tsx +24 -5
  21. package/src/contentful/blocks/cards/full-image-card/types.ts +4 -0
  22. package/src/contentful/blocks/cards/simple-card/index.tsx +21 -3
  23. package/src/contentful/blocks/cards/simple-card/types.ts +4 -0
  24. package/src/contentful/blocks/comparison-table/index.test.tsx +75 -0
  25. package/src/contentful/blocks/comparison-table/index.tsx +15 -3
  26. package/src/contentful/blocks/comparison-table/types.ts +8 -2
  27. package/src/contentful/blocks/cta-callout/index.test.tsx +99 -7
  28. package/src/contentful/blocks/cta-callout/index.tsx +15 -8
  29. package/src/contentful/blocks/cta-callout/types.ts +3 -0
  30. package/src/contentful/blocks/find-kinetic/{FindKinetic.stories.tsx → find-kinetic.stories.tsx} +1 -1
  31. package/src/contentful/blocks/modal/index.test.tsx +75 -2
  32. package/src/contentful/blocks/modal/index.tsx +11 -2
  33. package/src/contentful/blocks/modal/types.ts +4 -0
  34. /package/src/contentful/blocks/address-input-banner/{AddressInputBanner.stories.tsx → address-input-banner.stories.tsx} +0 -0
  35. /package/src/contentful/blocks/anchored-bottom-banner/{AnchoredBottomBanner.stories.tsx → anchored-bottom-banner.stories.tsx} +0 -0
  36. /package/src/contentful/blocks/award-marquee/{AwardMarquee.stories.tsx → award-marquee.stories.tsx} +0 -0
  37. /package/src/contentful/blocks/blogs-grid/{BlogGrid.stories.mocks.tsx → blog-grid.stories.mocks.tsx} +0 -0
  38. /package/src/contentful/blocks/breadcrumbs/{BreadcrumbNavigation.stories.tsx → breadcrumb-navigation.stories.tsx} +0 -0
  39. /package/src/contentful/blocks/button/{Button.stories.tsx → button.stories.tsx} +0 -0
  40. /package/src/contentful/blocks/callout/{Callout.stories.mocks.ts → callout.stories.mocks.ts} +0 -0
  41. /package/src/contentful/blocks/cards/{Cards.stories.tsx → cards.stories.tsx} +0 -0
  42. /package/src/contentful/blocks/carousel/{Carousel.stories.tsx → carousel.stories.tsx} +0 -0
  43. /package/src/contentful/blocks/cart-retention-banner/{CartRetentionBanner.stories.tsx → cart-retention-banner.stories.tsx} +0 -0
  44. /package/src/contentful/blocks/cta-callout/{CtaCallout.stories.tsx → cta-callout.stories.tsx} +0 -0
  45. /package/src/contentful/blocks/dynamic-form/{DynamicForm.stories.tsx → dynamic-form.stories.tsx} +0 -0
  46. /package/src/contentful/blocks/email-input-block/{EmailInputBlock.stories.tsx → email-input-block.stories.tsx} +0 -0
  47. /package/src/contentful/blocks/find-kinetic/{FindKinetic.stories.mocks.ts → find-kinetic.stories.mocks.ts} +0 -0
  48. /package/src/contentful/blocks/floating-banner/{FloatingBanner.stories.tsx → floating-banner.stories.tsx} +0 -0
  49. /package/src/contentful/blocks/footer/{Footer.stories.tsx → footer.stories.tsx} +0 -0
  50. /package/src/contentful/blocks/image-promo-bar/{ImagePromoBar.stories.tsx → image-promo-bar.stories.tsx} +0 -0
  51. /package/src/contentful/blocks/navigation/{Navigation.stories.mocks.tsx → navigation.stories.mocks.tsx} +0 -0
  52. /package/src/contentful/blocks/primary-hero/{PrimaryHero.stories.tsx → primary-hero.stories.tsx} +0 -0
  53. /package/src/contentful/blocks/shape-background-wrapper/{ShapeBackgroundWrapper.stories.tsx → shape-background-wrapper.stories.tsx} +0 -0
  54. /package/src/contentful/blocks/text/{Text.stories.tsx → text.stories.tsx} +0 -0
package/dist/core.d.ts CHANGED
@@ -67,11 +67,11 @@ type InputFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & {
67
67
 
68
68
  declare const InputField: ForwardRefRenderFunction<HTMLInputElement, InputFieldProps>;
69
69
  declare const Input: React$1.ForwardRefExoticComponent<Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "size"> & {
70
- state?: "default" | "focus" | "active" | "hover" | "filled" | "error" | undefined;
71
- size?: "slim" | "medium" | "large" | undefined;
70
+ state?: "error" | "default" | "focus" | "active" | "hover" | "filled" | undefined;
71
+ size?: "medium" | "slim" | "large" | undefined;
72
72
  label?: string | undefined;
73
73
  errorText?: string | undefined;
74
- prefixIconName?: "location_on" | "search" | undefined;
74
+ prefixIconName?: "search" | "location_on" | undefined;
75
75
  prefixIconSize?: 20 | 24 | 40 | 48 | undefined;
76
76
  prefixIconFill?: boolean | undefined;
77
77
  suffixIconFill?: boolean | undefined;
package/dist/index.d.ts CHANGED
@@ -67,11 +67,11 @@ type InputFieldProps = Omit<InputHTMLAttributes<HTMLInputElement>, "size"> & {
67
67
 
68
68
  declare const InputField: ForwardRefRenderFunction<HTMLInputElement, InputFieldProps>;
69
69
  declare const Input: React$1.ForwardRefExoticComponent<Omit<React$1.InputHTMLAttributes<HTMLInputElement>, "size"> & {
70
- state?: "default" | "focus" | "active" | "hover" | "filled" | "error" | undefined;
71
- size?: "slim" | "medium" | "large" | undefined;
70
+ state?: "error" | "default" | "focus" | "active" | "hover" | "filled" | undefined;
71
+ size?: "medium" | "slim" | "large" | undefined;
72
72
  label?: string | undefined;
73
73
  errorText?: string | undefined;
74
- prefixIconName?: "location_on" | "search" | undefined;
74
+ prefixIconName?: "search" | "location_on" | undefined;
75
75
  prefixIconSize?: 20 | 24 | 40 | 48 | undefined;
76
76
  prefixIconFill?: boolean | undefined;
77
77
  suffixIconFill?: boolean | undefined;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.85",
3
+ "version": "0.2.87",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -10,6 +10,59 @@ const items: AccordionItem[] = [
10
10
  ];
11
11
 
12
12
  describe("Contentful Accordion", () => {
13
+ describe("fieldProps", () => {
14
+ const tag = (field: string) => ({
15
+ "data-contentful-field-id": field,
16
+ });
17
+
18
+ it("spreads block-level title fieldProps onto the heading", () => {
19
+ render(
20
+ <Accordion
21
+ items={items}
22
+ title="FAQ"
23
+ fieldProps={{ title: tag("title") }}
24
+ />
25
+ );
26
+ expect(screen.getByText("FAQ")).toHaveAttribute(
27
+ "data-contentful-field-id",
28
+ "title"
29
+ );
30
+ });
31
+
32
+ it("spreads per-item title and description fieldProps", () => {
33
+ render(
34
+ <Accordion
35
+ items={[
36
+ {
37
+ ...items[0],
38
+ fieldProps: {
39
+ title: tag("itemTitle"),
40
+ description: tag("itemDescription"),
41
+ },
42
+ },
43
+ items[1],
44
+ ]}
45
+ title="FAQ"
46
+ />
47
+ );
48
+ expect(document.getElementById("item-1")).toHaveAttribute(
49
+ "data-contentful-field-id",
50
+ "itemTitle"
51
+ );
52
+ expect(screen.getByText("Answer one")).toHaveAttribute(
53
+ "data-contentful-field-id",
54
+ "itemDescription"
55
+ );
56
+ expect(document.getElementById("item-2")).not.toHaveAttribute(
57
+ "data-contentful-field-id"
58
+ );
59
+ });
60
+
61
+ it("renders no tagging attributes when fieldProps is omitted", () => {
62
+ const { container } = render(<Accordion items={items} title="FAQ" />);
63
+ expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
64
+ });
65
+ });
13
66
  beforeEach(() => {
14
67
  Object.defineProperty(window, "location", {
15
68
  writable: true,
@@ -18,6 +18,7 @@ export const Accordion: React.FC<AccordionProps> = ({
18
18
  hashAutoExpand = false,
19
19
  descriptionMaxWidth = "max-w-[980px]",
20
20
  itemContainerClassName,
21
+ fieldProps,
21
22
  }) => {
22
23
  const [expandedItems, setExpandedItems] = useState<string[]>(
23
24
  initialExpandedItemsProp
@@ -64,7 +65,8 @@ export const Accordion: React.FC<AccordionProps> = ({
64
65
  <Text
65
66
  as={enableHeading ? "h1" : "h2"}
66
67
  className="heading2 mb-10 pt-12 text-left text-text md:heading1 lg:text-center"
67
- >
68
+ {...fieldProps?.title}
69
+ >
68
70
  {title}
69
71
  </Text>
70
72
  <div className={`pt-6 md:pt-10 ${itemContainerClassName ?? ""}`}>
@@ -79,6 +81,7 @@ export const Accordion: React.FC<AccordionProps> = ({
79
81
  key={itemKey}
80
82
  id={item.anchorId}
81
83
  className="snap-start scroll-mt-48 lg:scroll-mt-60"
84
+ {...item.fieldProps?.title}
82
85
  >
83
86
  <AccordionComponent
84
87
  title={item.title}
@@ -96,7 +99,9 @@ export const Accordion: React.FC<AccordionProps> = ({
96
99
  })}
97
100
  >
98
101
  <div className={descriptionMaxWidth}>
99
- <Text as="div">{item.description}</Text>
102
+ <Text as="div" {...item.fieldProps?.description}>
103
+ {item.description}
104
+ </Text>
100
105
  </div>
101
106
  </AccordionComponent>
102
107
  {index < items.length - 1 && (
@@ -1,9 +1,12 @@
1
1
  import React from "react";
2
2
 
3
+ import type { FieldProps } from "@shared/types/field-props";
4
+
3
5
  export type AccordionItem = {
4
6
  anchorId?: string;
5
7
  title: string;
6
8
  description: React.ReactNode;
9
+ fieldProps?: FieldProps<"title" | "description">;
7
10
  };
8
11
 
9
12
  export type AccordionProps = {
@@ -22,6 +25,7 @@ export type AccordionProps = {
22
25
  descriptionMaxWidth?: string;
23
26
  /** Custom class for the accordion list wrapper */
24
27
  itemContainerClassName?: string;
28
+ fieldProps?: FieldProps<"title">;
25
29
  };
26
30
 
27
31
  export type ThemeKey =
@@ -3,7 +3,7 @@ import {
3
3
  ARTICLES_WITH_COVERS,
4
4
  ARTICLES_WITHOUT_COVERS,
5
5
  CATEGORY_OPTIONS,
6
- } from "./BlogGrid.stories.mocks";
6
+ } from "./blog-grid.stories.mocks";
7
7
  import { BlogGrid } from "./index";
8
8
  import type { BlogGridProps } from "./types";
9
9
 
@@ -0,0 +1,184 @@
1
+ import React from "react";
2
+ import { Callout } from "./index";
3
+ import type { CalloutItem } from "./types";
4
+
5
+ import { fireEvent, render, screen } from "@testing-library/react";
6
+
7
+ const tag = (entry: string, field: string) => ({
8
+ "data-contentful-entry-id": entry,
9
+ "data-contentful-field-id": field,
10
+ "data-contentful-locale": "en-US",
11
+ });
12
+
13
+ describe("Callout fieldProps", () => {
14
+ it("tags the block fields and retains CTA behavior", () => {
15
+ const onClick = jest.fn();
16
+ const { container } = render(
17
+ <Callout
18
+ items={[]}
19
+ title="Callout title"
20
+ subtitle="Callout subtitle"
21
+ description="Callout description"
22
+ finePrint="Callout fine print"
23
+ cta={{ buttonLabel: "Callout CTA", onClick }}
24
+ fieldProps={{
25
+ title: tag("callout", "title"),
26
+ subtitle: tag("callout", "subtitle"),
27
+ description: tag("callout", "description"),
28
+ finePrint: tag("callout", "finePrint"),
29
+ cta: tag("callout", "cta"),
30
+ }}
31
+ />
32
+ );
33
+ ["title", "subtitle", "description", "finePrint", "cta"].forEach(field => {
34
+ expect(
35
+ container.querySelector(`[data-contentful-field-id="${field}"]`)
36
+ ).toHaveAttribute("data-contentful-entry-id", "callout");
37
+ });
38
+ fireEvent.click(screen.getByRole("button", { name: "Callout CTA" }));
39
+ expect(onClick).toHaveBeenCalledTimes(1);
40
+ });
41
+
42
+ it("preserves CTA entry tags when both the block and CTA supply attributes", () => {
43
+ render(
44
+ <Callout
45
+ items={[]}
46
+ cta={{
47
+ buttonLabel: "Nested CTA",
48
+ fieldProps: tag("button", "buttonLabel"),
49
+ }}
50
+ fieldProps={{ cta: tag("callout", "cta") }}
51
+ />
52
+ );
53
+ expect(screen.getByRole("button", { name: "Nested CTA" })).toHaveAttribute(
54
+ "data-contentful-entry-id",
55
+ "button"
56
+ );
57
+ });
58
+
59
+ it("tags custom CTA renderers without requiring them to forward attributes", () => {
60
+ const onClick = jest.fn();
61
+ render(
62
+ <Callout
63
+ items={[]}
64
+ cta={{
65
+ preDefinedFunctionExecution: "check availability",
66
+ renderCheckPlans: () => <button onClick={onClick}>Custom CTA</button>,
67
+ }}
68
+ fieldProps={{ cta: tag("callout", "cta") }}
69
+ />
70
+ );
71
+ const button = screen.getByRole("button", { name: "Custom CTA" });
72
+ expect(button.parentElement).toHaveAttribute(
73
+ "data-contentful-field-id",
74
+ "cta"
75
+ );
76
+ fireEvent.click(button);
77
+ expect(onClick).toHaveBeenCalledTimes(1);
78
+ });
79
+
80
+ it.each([
81
+ { cardsWidth: true, disableAnimation: true },
82
+ { cardsWidth: true, disableAnimation: false },
83
+ { cardsWidth: false, disableAnimation: true },
84
+ { cardsWidth: false, disableAnimation: false },
85
+ ])("tags every card variant with layout/animation options %p", options => {
86
+ const items: CalloutItem[] = [
87
+ {
88
+ cardType: "simple",
89
+ title: "Simple title",
90
+ body: "Simple body",
91
+ richText: "Simple rich text",
92
+ cta: { buttonLabel: "Simple CTA" },
93
+ fieldProps: {
94
+ title: tag("simple", "title"),
95
+ body: tag("simple", "body"),
96
+ richText: tag("simple", "richText"),
97
+ cta: tag("simple", "cta"),
98
+ },
99
+ },
100
+ {
101
+ cardType: "blog",
102
+ title: "Blog title",
103
+ slug: "/blog",
104
+ shortDescription: "Blog excerpt",
105
+ fieldProps: {
106
+ title: tag("blog", "title"),
107
+ shortDescription: tag("blog", "shortDescription"),
108
+ },
109
+ },
110
+ {
111
+ cardType: "fullImage",
112
+ title: "Full image title",
113
+ description: "Full image description",
114
+ caption: "Full image caption",
115
+ cta: { buttonLabel: "Full image CTA" },
116
+ fieldProps: {
117
+ title: tag("full", "title"),
118
+ description: tag("full", "description"),
119
+ caption: tag("full", "caption"),
120
+ cta: tag("full", "cta"),
121
+ },
122
+ },
123
+ {
124
+ cardType: "floatingImage",
125
+ header: "Floating header",
126
+ body: "Floating body",
127
+ cta: { buttonLabel: "Floating CTA" },
128
+ fieldProps: {
129
+ header: tag("floating", "header"),
130
+ body: tag("floating", "body"),
131
+ cta: tag("floating", "cta"),
132
+ },
133
+ },
134
+ { title: "Untagged item" },
135
+ ];
136
+ const { container } = render(<Callout items={items} {...options} />);
137
+ items.forEach(item => {
138
+ Object.values(item.fieldProps ?? {}).forEach(attributes => {
139
+ const element = container.querySelector(
140
+ `[data-contentful-entry-id="${attributes["data-contentful-entry-id"]}"][data-contentful-field-id="${attributes["data-contentful-field-id"]}"]`
141
+ );
142
+ expect(element).toBeInTheDocument();
143
+ expect(element).toHaveAttribute("data-contentful-locale", "en-US");
144
+ });
145
+ });
146
+ expect(screen.getByText("Untagged item")).not.toHaveAttribute(
147
+ "data-contentful-field-id"
148
+ );
149
+ expect(screen.getByText("Blog title").closest("h2")).toHaveAttribute(
150
+ "data-contentful-entry-id",
151
+ "blog"
152
+ );
153
+ expect(screen.getByText("Simple body")).toHaveAttribute(
154
+ "data-contentful-field-id",
155
+ "body"
156
+ );
157
+ });
158
+
159
+ it("supports partial fieldProps without creating absent fields", () => {
160
+ const { container } = render(
161
+ <Callout
162
+ items={[]}
163
+ title="Only title"
164
+ fieldProps={{
165
+ title: tag("callout", "title"),
166
+ description: tag("callout", "description"),
167
+ }}
168
+ />
169
+ );
170
+ expect(
171
+ container.querySelectorAll("[data-contentful-field-id]")
172
+ ).toHaveLength(1);
173
+ });
174
+
175
+ it("does not add tagging attributes when fieldProps is omitted", () => {
176
+ const { container } = render(
177
+ <Callout
178
+ title="No tags"
179
+ items={[{ title: "Card", body: "Body", cta: { buttonLabel: "CTA" } }]}
180
+ />
181
+ );
182
+ expect(container.querySelector("[data-contentful-field-id]")).toBeNull();
183
+ });
184
+ });
@@ -82,6 +82,7 @@ export const Callout: React.FC<CalloutProps> = ({
82
82
  noGutter = false,
83
83
  disableAnimation,
84
84
  animationType = "lift",
85
+ fieldProps,
85
86
  }) => {
86
87
  const itemCount = items?.length ?? 0;
87
88
  const desktopCols = clampCol(
@@ -142,6 +143,12 @@ export const Callout: React.FC<CalloutProps> = ({
142
143
  image={blogItem.cover}
143
144
  imageAltText={blogItem.imageAltText}
144
145
  asGrid={false}
146
+ fieldProps={{
147
+ ...item.fieldProps,
148
+ description:
149
+ item.fieldProps?.shortDescription ??
150
+ item.fieldProps?.description,
151
+ }}
145
152
  {...widthProps}
146
153
  />
147
154
  );
@@ -197,7 +204,9 @@ export const Callout: React.FC<CalloutProps> = ({
197
204
  >
198
205
  <div
199
206
  className={cx(
200
- noGutter ? "p-0" : "mx-5 mb-5 mt-12 md:mx-0 md:mb-0 md:mt-0 md:px-6 md:pb-8 md:pt-16 lg:px-0 lg:py-20",
207
+ noGutter
208
+ ? "p-0"
209
+ : "mx-5 mb-5 mt-12 md:mx-0 md:mb-0 md:mt-0 md:px-6 md:pb-8 md:pt-16 lg:px-0 lg:py-20",
201
210
  maxWidth && "max-w-120 xl:mx-auto",
202
211
  color === "dark" ? "text-text" : "text-white",
203
212
  innerClassName
@@ -209,6 +218,7 @@ export const Callout: React.FC<CalloutProps> = ({
209
218
  <Text
210
219
  as={enableHeading ? "h1" : "h2"}
211
220
  className="heading2 md:heading1 md:text-center"
221
+ {...fieldProps?.title}
212
222
  >
213
223
  {title}
214
224
  </Text>
@@ -217,12 +227,17 @@ export const Callout: React.FC<CalloutProps> = ({
217
227
  <Text
218
228
  as={enableHeading ? "h2" : "h3"}
219
229
  className="subheading3 mt-2 md:subheading1 md:text-center lg:mt-3"
230
+ {...fieldProps?.subtitle}
220
231
  >
221
232
  {subtitle}
222
233
  </Text>
223
234
  )}
224
235
  {description && (
225
- <Text as="p" className="body1 mt-4 text-center md:mt-6">
236
+ <Text
237
+ as="p"
238
+ className="body1 mt-4 text-center md:mt-6"
239
+ {...fieldProps?.description}
240
+ >
226
241
  {description}
227
242
  </Text>
228
243
  )}
@@ -253,7 +268,10 @@ export const Callout: React.FC<CalloutProps> = ({
253
268
  )}
254
269
  </div>
255
270
  {(cta || finePrint) && (
256
- <div className="flex flex-col items-center gap-4">
271
+ <div
272
+ className="flex flex-col items-center gap-4"
273
+ {...(cta ? fieldProps?.cta : undefined)}
274
+ >
257
275
  {cta ? (
258
276
  <Button
259
277
  linkClassName="label1"
@@ -264,7 +282,11 @@ export const Callout: React.FC<CalloutProps> = ({
264
282
  </Button>
265
283
  ) : null}
266
284
  {finePrint ? (
267
- <Text as="div" className="footnote text-center text-text">
285
+ <Text
286
+ as="div"
287
+ className="footnote text-center text-text"
288
+ {...fieldProps?.finePrint}
289
+ >
268
290
  {finePrint}
269
291
  </Text>
270
292
  ) : null}
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
2
2
  import type { ButtonProps } from "../button/types";
3
3
 
4
4
  import type { AnimationType } from "@shared/components/animation-wrapper";
5
+ import type { FieldProps } from "@shared/types/field-props";
5
6
 
6
7
  export type CalloutCardType = "simple" | "blog" | "fullImage" | "floatingImage";
7
8
 
@@ -16,6 +17,17 @@ export type CalloutItem = {
16
17
  * Callout (e.g. one full-image + two simple cards).
17
18
  */
18
19
  cardType?: CalloutCardType;
20
+ /** Per-field DOM attributes for the selected card variant. */
21
+ fieldProps?: FieldProps<
22
+ | "title"
23
+ | "shortDescription"
24
+ | "description"
25
+ | "header"
26
+ | "body"
27
+ | "richText"
28
+ | "caption"
29
+ | "cta"
30
+ >;
19
31
  // Permissive shape — concrete card components validate their own subset.
20
32
  [key: string]: any;
21
33
  };
@@ -30,7 +42,7 @@ export type CalloutProps = {
30
42
  finePrint?: ReactNode;
31
43
  cta?: CalloutCtaProps;
32
44
  applyBoxShadow?: boolean;
33
- cardStackingMobile?: boolean;
45
+ cardStackingMobile?: boolean;
34
46
  color?: "dark" | "light";
35
47
  /**
36
48
  * When `true` (default) cards are laid out in a responsive Tailwind
@@ -74,4 +86,8 @@ export type CalloutProps = {
74
86
  disableAnimation?: boolean;
75
87
  /** Animation type(s) applied to each card. */
76
88
  animationType?: AnimationType | AnimationType[];
89
+ /** Consumer-supplied Inspector Mode attributes. `cta` tags the CTA/fine-print region; nested CTA fieldProps remain unchanged. */
90
+ fieldProps?: FieldProps<
91
+ "title" | "subtitle" | "description" | "finePrint" | "cta"
92
+ >;
77
93
  };
@@ -22,6 +22,7 @@ export const BlogCard: React.FC<BlogCardProps> = ({
22
22
  imageComponent: ImageComponent,
23
23
  asGrid = true,
24
24
  index,
25
+ fieldProps,
25
26
  }: BlogCardProps) => {
26
27
  const imageAlt =
27
28
  imageAltText ??
@@ -96,7 +97,7 @@ export const BlogCard: React.FC<BlogCardProps> = ({
96
97
 
97
98
  {/* Title */}
98
99
  {title && (
99
- <Text as="h2" className="heading6">
100
+ <Text as="h2" className="heading6" {...fieldProps?.title}>
100
101
  <Link
101
102
  href={href}
102
103
  className="m-0 line-clamp-3 font-black transition-colors duration-150 after:absolute after:inset-0 after:z-10 after:content-[''] hover:text-text-brand group-hover:text-text-brand"
@@ -108,7 +109,10 @@ export const BlogCard: React.FC<BlogCardProps> = ({
108
109
 
109
110
  {/* Excerpt */}
110
111
  {description && (
111
- <Text className="body1 m-0 line-clamp-3 flex-1 text-text">
112
+ <Text
113
+ className="body1 m-0 line-clamp-3 flex-1 text-text"
114
+ {...fieldProps?.description}
115
+ >
112
116
  {description}
113
117
  </Text>
114
118
  )}
@@ -1,5 +1,7 @@
1
1
  import React from "react";
2
2
 
3
+ import type { FieldProps } from "@shared/types/field-props";
4
+
3
5
  export interface BlogCardImageProps {
4
6
  src: string;
5
7
  alt: string;
@@ -32,4 +34,6 @@ export interface BlogCardProps {
32
34
  imageComponent?: React.ComponentType<BlogCardImageProps>;
33
35
  asGrid?: boolean;
34
36
  index?: number;
37
+ /** Per-field DOM attributes supplied by the consumer or parent Callout item. */
38
+ fieldProps?: FieldProps<"title" | "description">;
35
39
  }
@@ -33,6 +33,7 @@ export const FloatingImageCard: React.FC<FloatingImageCardProps> = ({
33
33
  isPromoBarImg = false,
34
34
  shadow = false,
35
35
  disableAnimation,
36
+ fieldProps,
36
37
  } = card;
37
38
 
38
39
  const ctaAlignmentClass = ctaAlignment
@@ -102,7 +103,7 @@ export const FloatingImageCard: React.FC<FloatingImageCardProps> = ({
102
103
  ) : null}
103
104
 
104
105
  {header ? (
105
- <Text as="h4" className="heading6 text-text">
106
+ <Text as="h4" className="heading6 text-text" {...fieldProps?.header}>
106
107
  {header}
107
108
  </Text>
108
109
  ) : null}
@@ -111,12 +112,13 @@ export const FloatingImageCard: React.FC<FloatingImageCardProps> = ({
111
112
  <Text
112
113
  as="div"
113
114
  className="body1 -mt-1 mb-0 flex-1 text-text md:-mt-2 lg:mt-2"
115
+ {...fieldProps?.body}
114
116
  >
115
117
  {body}
116
118
  </Text>
117
119
  ) : null}
118
120
 
119
- <div className={cx(ctaAlignmentClass, "mt-auto")}>
121
+ <div className={cx(ctaAlignmentClass, "mt-auto")} {...fieldProps?.cta}>
120
122
  {cta ? (
121
123
  <Button
122
124
  {...cta}
@@ -1,6 +1,8 @@
1
1
  import type { ReactNode } from "react";
2
2
  import type { ButtonProps } from "../../button/types";
3
3
 
4
+ import type { FieldProps } from "@shared/types/field-props";
5
+
4
6
  export type FloatingImageAsset = {
5
7
  href: string;
6
8
  title?: string;
@@ -27,6 +29,8 @@ export type FloatingImageCardItem = {
27
29
  shadow?: boolean;
28
30
  /** Disables the CTA hover/tap animation when true. */
29
31
  disableAnimation?: boolean;
32
+ /** Per-field DOM attributes supplied by the consumer or parent Callout item. */
33
+ fieldProps?: FieldProps<"header" | "body" | "cta">;
30
34
  };
31
35
 
32
36
  export type FloatingImageCardProps = {
@@ -25,6 +25,7 @@ export const FullImageCard: React.FC<FullImageCardProps> = ({
25
25
  contentClassName,
26
26
  imageClassName,
27
27
  shadow = true,
28
+ fieldProps,
28
29
  } = card;
29
30
 
30
31
  const target = cta || link;
@@ -36,7 +37,13 @@ export const FullImageCard: React.FC<FullImageCardProps> = ({
36
37
  const img = (
37
38
  <NextImage
38
39
  src={image.href}
39
- alt={imageAltText ?? image.description ?? image.title ?? title ?? "card-image"}
40
+ alt={
41
+ imageAltText ??
42
+ image.description ??
43
+ image.title ??
44
+ title ??
45
+ "card-image"
46
+ }
40
47
  width={image.width || 400}
41
48
  height={image.height || 280}
42
49
  className="w-full"
@@ -53,7 +60,11 @@ export const FullImageCard: React.FC<FullImageCardProps> = ({
53
60
  };
54
61
 
55
62
  const titleNode = title ? (
56
- <Text as="h3" className="body1 text-text xl:heading6">
63
+ <Text
64
+ as="h3"
65
+ className="body1 text-text xl:heading6"
66
+ {...fieldProps?.title}
67
+ >
57
68
  {title}
58
69
  </Text>
59
70
  ) : null;
@@ -101,19 +112,27 @@ export const FullImageCard: React.FC<FullImageCardProps> = ({
101
112
  titleNode
102
113
  )}
103
114
  {description ? (
104
- <Text as="p" className="body2 mt-4 text-text md:body1">
115
+ <Text
116
+ as="p"
117
+ className="body2 mt-4 text-text md:body1"
118
+ {...fieldProps?.description}
119
+ >
105
120
  {description}
106
121
  </Text>
107
122
  ) : null}
108
123
  </div>
109
124
  {caption ? (
110
- <Text as="p" className="footnote mt-5 text-text-secondary">
125
+ <Text
126
+ as="p"
127
+ className="footnote mt-5 text-text-secondary"
128
+ {...fieldProps?.caption}
129
+ >
111
130
  {caption}
112
131
  </Text>
113
132
  ) : null}
114
133
  </div>
115
134
  {cta ? (
116
- <div className="mt-2 flex items-end">
135
+ <div className="mt-2 flex items-end" {...fieldProps?.cta}>
117
136
  <Button
118
137
  linkVariant="unstyled"
119
138
  linkClassName="label1 flex items-center gap-2 text-text"
@@ -1,5 +1,7 @@
1
1
  import type { ButtonProps } from "../../button/types";
2
2
 
3
+ import type { FieldProps } from "@shared/types/field-props";
4
+
3
5
  export type FullImageAsset = {
4
6
  href: string;
5
7
  title?: string;
@@ -24,6 +26,8 @@ export type FullImageCardItem = {
24
26
  imageClassName?: string;
25
27
  /** Applies `shadow-card` to the outer card element. */
26
28
  shadow?: boolean;
29
+ /** Per-field DOM attributes supplied by the consumer or parent Callout item. */
30
+ fieldProps?: FieldProps<"title" | "description" | "caption" | "cta">;
27
31
  };
28
32
 
29
33
  export type FullImageCardProps = {