@windstream/react-shared-components 0.2.22 → 0.2.24

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 (40) hide show
  1. package/dist/contentful/index.d.ts +6 -3
  2. package/dist/contentful/index.esm.js +3 -3
  3. package/dist/contentful/index.esm.js.map +1 -1
  4. package/dist/contentful/index.js +2 -2
  5. package/dist/contentful/index.js.map +1 -1
  6. package/dist/core.d.ts +13 -1
  7. package/dist/index.d.ts +16 -4
  8. package/dist/index.esm.js +1 -1
  9. package/dist/index.esm.js.map +1 -1
  10. package/dist/index.js +6 -6
  11. package/dist/index.js.map +1 -1
  12. package/dist/next/index.esm.js +1 -1
  13. package/dist/next/index.esm.js.map +1 -1
  14. package/dist/next/index.js +1 -1
  15. package/dist/next/index.js.map +1 -1
  16. package/dist/styles.css +1 -1
  17. package/dist/utils/index.d.ts +3 -1
  18. package/dist/utils/index.esm.js +1 -1
  19. package/dist/utils/index.esm.js.map +1 -1
  20. package/dist/utils/index.js +1 -1
  21. package/dist/utils/index.js.map +1 -1
  22. package/package.json +1 -1
  23. package/src/components/link/index.test.tsx +103 -4
  24. package/src/components/link/index.tsx +9 -1
  25. package/src/components/link/types.ts +12 -0
  26. package/src/contentful/blocks/anchored-bottom-banner/AnchoredBottomBanner.stories.tsx +155 -0
  27. package/src/contentful/blocks/breadcrumbs/index.tsx +3 -2
  28. package/src/contentful/blocks/callout/Callout.stories.mocks.ts +327 -0
  29. package/src/contentful/blocks/callout/Callout.stories.tsx +315 -2
  30. package/src/contentful/blocks/callout/types.ts +1 -2
  31. package/src/contentful/blocks/email-input-block/index.tsx +41 -17
  32. package/src/contentful/blocks/email-input-block/types.ts +2 -1
  33. package/src/contentful/blocks/find-kinetic/index.test.tsx +35 -0
  34. package/src/contentful/blocks/find-kinetic/index.tsx +33 -9
  35. package/src/contentful/blocks/find-kinetic/types.ts +1 -0
  36. package/src/contentful/blocks/primary-hero/index.test.tsx +63 -0
  37. package/src/contentful/blocks/primary-hero/index.tsx +66 -9
  38. package/src/hooks/contentful/use-contentful-rich-text.test.tsx +29 -0
  39. package/src/hooks/contentful/use-contentful-rich-text.tsx +20 -9
  40. package/src/types/global.d.ts +5 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@windstream/react-shared-components",
3
- "version": "0.2.22",
3
+ "version": "0.2.24",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -7,10 +7,23 @@ import "@testing-library/jest-dom";
7
7
 
8
8
  // Mock next/link
9
9
  jest.mock("next/link", () => {
10
- const MockLink = ({ children, href, className, onClick, ...rest }: any) => (
11
- <a href={href} className={className} onClick={onClick} {...rest}>
12
- {children}
13
- </a>
10
+ const ReactModule = require("react");
11
+ const MockLink = ReactModule.forwardRef(
12
+ (
13
+ { children, href, className, onClick, ...rest }: any,
14
+ ref: React.Ref<HTMLAnchorElement>
15
+ ) => (
16
+ <a
17
+ ref={ref}
18
+ href={href}
19
+ className={className}
20
+ onClick={onClick}
21
+ data-mock="next-link"
22
+ {...rest}
23
+ >
24
+ {children}
25
+ </a>
26
+ )
14
27
  );
15
28
  MockLink.displayName = "MockNextLink";
16
29
  return MockLink;
@@ -50,6 +63,67 @@ describe("Link", () => {
50
63
  });
51
64
  });
52
65
 
66
+ describe("preserveTrailingSlash", () => {
67
+ it("renders a plain <a> (bypassing next/link) when preserveTrailingSlash is true and href ends with '/'", () => {
68
+ render(
69
+ <Link href="/local/al/" preserveTrailingSlash>
70
+ Alabama
71
+ </Link>
72
+ );
73
+ const anchor = screen.getByText("Alabama").closest("a");
74
+ expect(anchor).toHaveAttribute("href", "/local/al/");
75
+ // plain <a> path does not carry the next/link mock marker
76
+ expect(anchor).not.toHaveAttribute("data-mock", "next-link");
77
+ });
78
+
79
+ it("still renders via next/link when preserveTrailingSlash is false even if href ends with '/'", () => {
80
+ render(<Link href="/local/al/">Alabama</Link>);
81
+ const anchor = screen.getByText("Alabama").closest("a");
82
+ expect(anchor).toHaveAttribute("data-mock", "next-link");
83
+ expect(anchor).toHaveAttribute("href", "/local/al/");
84
+ });
85
+
86
+ it("renders via next/link when preserveTrailingSlash is true but href has no trailing slash", () => {
87
+ render(
88
+ <Link href="/local/al" preserveTrailingSlash>
89
+ Alabama
90
+ </Link>
91
+ );
92
+ const anchor = screen.getByText("Alabama").closest("a");
93
+ expect(anchor).toHaveAttribute("data-mock", "next-link");
94
+ });
95
+
96
+ it("does not bypass next/link for protocol-relative hrefs even with a trailing slash", () => {
97
+ render(
98
+ <Link href="//example.com/" preserveTrailingSlash>
99
+ Protocol Relative
100
+ </Link>
101
+ );
102
+ const anchor = screen.getByText("Protocol Relative").closest("a");
103
+ expect(anchor).toHaveAttribute("data-mock", "next-link");
104
+ });
105
+
106
+ it("applies style on both render paths (plain <a> and next/link)", () => {
107
+ const { rerender } = render(
108
+ <Link href="/local/al/" preserveTrailingSlash style={{ color: "red" }}>
109
+ Alabama
110
+ </Link>
111
+ );
112
+ let anchor = screen.getByText("Alabama").closest("a")!;
113
+ expect(anchor).not.toHaveAttribute("data-mock", "next-link");
114
+ expect(anchor.style.color).toBe("red");
115
+
116
+ rerender(
117
+ <Link href="/internal" style={{ color: "red" }}>
118
+ Internal
119
+ </Link>
120
+ );
121
+ anchor = screen.getByText("Internal").closest("a")!;
122
+ expect(anchor).toHaveAttribute("data-mock", "next-link");
123
+ expect(anchor.style.color).toBe("red");
124
+ });
125
+ });
126
+
53
127
  describe("Variants", () => {
54
128
  it("applies unstyled variant with no extra classes", () => {
55
129
  render(
@@ -104,6 +178,20 @@ describe("Link", () => {
104
178
  expect(el).toHaveAttribute("tabindex", "-1");
105
179
  });
106
180
 
181
+ it("applies disabled accessibility attributes on internal links", () => {
182
+ render(
183
+ <Link href="/test" disabled={true}>
184
+ Disabled
185
+ </Link>
186
+ );
187
+ const el = screen.getByText("Disabled").closest("a");
188
+ // disabled links render as a plain <a>, not via next/link
189
+ expect(el).not.toHaveAttribute("data-mock", "next-link");
190
+ expect(el).toHaveAttribute("aria-disabled", "true");
191
+ expect(el).toHaveAttribute("tabindex", "-1");
192
+ expect(el).not.toHaveAttribute("href");
193
+ });
194
+
107
195
  it("prevents click when disabled", () => {
108
196
  const onClick = jest.fn();
109
197
  render(
@@ -173,6 +261,17 @@ describe("Link", () => {
173
261
  );
174
262
  expect(ref.current).toBeInstanceOf(HTMLAnchorElement);
175
263
  });
264
+
265
+ it("forwards ref to the anchor element for internal (next/link) links", () => {
266
+ const ref = React.createRef<HTMLAnchorElement>();
267
+ render(
268
+ <Link ref={ref} href="/internal">
269
+ Internal Ref
270
+ </Link>
271
+ );
272
+ expect(ref.current).toBeInstanceOf(HTMLAnchorElement);
273
+ expect(ref.current).toHaveAttribute("data-mock", "next-link");
274
+ });
176
275
  });
177
276
 
178
277
  describe("Custom className and style", () => {
@@ -23,6 +23,7 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
23
23
  style,
24
24
  external = false,
25
25
  disabled = false,
26
+ preserveTrailingSlash = false,
26
27
  ...props
27
28
  },
28
29
  ref
@@ -95,13 +96,20 @@ export const Link = forwardRef<HTMLAnchorElement, LinkProps>(
95
96
  tabIndex: -1,
96
97
  }),
97
98
  };
98
- if (external || (typeof href === "string" && href.startsWith("http"))) {
99
+
100
+ if (
101
+ disabled ||
102
+ external ||
103
+ (typeof href === "string" && href.startsWith("http"))
104
+ ) {
99
105
  return <a {...linkProps}>{children}</a>;
100
106
  }
101
107
  return (
102
108
  <NextLink
103
109
  href={href || "#"}
110
+ ref={ref}
104
111
  className={combinedClassName}
112
+ style={style}
105
113
  onClick={handleClick}
106
114
  {...props}
107
115
  >
@@ -22,4 +22,16 @@ export interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
22
22
  external?: boolean;
23
23
  /** Disable the link */
24
24
  disabled?: boolean;
25
+ /**
26
+ * Preserve an intentional trailing slash on internal links.
27
+ * next/link normalizes the trailing slash based on the app's `trailingSlash`
28
+ * config (stripping it when `false`). When this is `true` and the href ends
29
+ * with a slash, the link renders as a plain anchor so the slash is kept.
30
+ *
31
+ * Tradeoff: because it renders a plain `<a>` instead of `next/link`, this
32
+ * link bypasses Next.js client-side navigation and prefetch — clicking it
33
+ * triggers a full page load. Only opt in when preserving the trailing slash
34
+ * matters more than client-side routing.
35
+ */
36
+ preserveTrailingSlash?: boolean;
25
37
  }
@@ -0,0 +1,155 @@
1
+ import React from "react";
2
+
3
+ import { AnchoredBottomBanner } from "./index";
4
+
5
+ import { DocsPage } from "@shared/stories/DocsTemplate";
6
+ import type { Meta, StoryObj } from "@storybook/react";
7
+
8
+ const meta: Meta<typeof AnchoredBottomBanner> = {
9
+ title: "Contentful Blocks/AnchoredBottomBanner",
10
+ component: AnchoredBottomBanner,
11
+ tags: ["autodocs"],
12
+ parameters: {
13
+ layout: "fullscreen",
14
+ docs: {
15
+ page: DocsPage,
16
+ description: {
17
+ component:
18
+ "A fixed-position bottom banner with an optional countdown timer, icon, and CTA link.",
19
+ },
20
+ },
21
+ },
22
+ decorators: [
23
+ Story => (
24
+ <>
25
+ <style>{`
26
+ #anchored-banner .fixed > a > div > div {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ gap: 0.5rem;
31
+ flex-wrap: nowrap;
32
+ }
33
+ #anchored-banner .material-symbols-rounded {
34
+ flex-shrink: 0;
35
+ }
36
+ `}</style>
37
+ <Story />
38
+ </>
39
+ ),
40
+ ],
41
+ argTypes: {
42
+ ctaSuffixText: { control: "text" },
43
+ backgroundColor: {
44
+ control: "select",
45
+ options: ["navy", "yellow", "green", "purple", "blue", "white"],
46
+ },
47
+ iconName: { control: "text" },
48
+ boxShadow: { control: "boolean" },
49
+ ctaButtonLabel: { control: "text" },
50
+ ctaButtonLink: { control: "text" },
51
+ ctaButtonTarget: {
52
+ control: "select",
53
+ options: ["_self", "_blank"],
54
+ },
55
+ anchorId: { control: "text" },
56
+ enableCountdownTimer: { control: "boolean" },
57
+ countdownStartDateTime: {
58
+ control: "date",
59
+ description: "Timer becomes visible after this time",
60
+ },
61
+ countdownEndDateTime: {
62
+ control: "date",
63
+ description: "Timer counts down to this time",
64
+ },
65
+ },
66
+ render: args => {
67
+ const { countdownStartDateTime, countdownEndDateTime, ...rest } = args;
68
+ return (
69
+ <AnchoredBottomBanner
70
+ {...rest}
71
+ countdownStartDateTime={
72
+ typeof countdownStartDateTime === "number" &&
73
+ Number.isFinite(countdownStartDateTime)
74
+ ? new Date(countdownStartDateTime).toISOString()
75
+ : (countdownStartDateTime as string | undefined)
76
+ }
77
+ countdownEndDateTime={
78
+ typeof countdownEndDateTime === "number" &&
79
+ Number.isFinite(countdownEndDateTime)
80
+ ? new Date(countdownEndDateTime).toISOString()
81
+ : (countdownEndDateTime as string | undefined)
82
+ }
83
+ />
84
+ );
85
+ },
86
+ args: {
87
+ ctaSuffixText: "to order Kinetic today",
88
+ backgroundColor: "yellow",
89
+ iconName: "call",
90
+ boxShadow: true,
91
+ ctaButtonLabel: "call (866) 445-8084",
92
+ ctaButtonLink: "tel:+1-866-445-8084",
93
+ ctaButtonTarget: "_blank",
94
+ anchorId: "anchored-banner",
95
+ enableCountdownTimer: false,
96
+ countdownStartDateTime: "2026-07-10T00:00:00.000Z",
97
+ countdownEndDateTime: "2026-12-31T23:59:59.000Z",
98
+ },
99
+ };
100
+
101
+ export default meta;
102
+ type Story = StoryObj<typeof meta>;
103
+
104
+ export const Default: Story = {};
105
+
106
+ export const WithCountdownTimer: Story = {
107
+ args: {
108
+ enableCountdownTimer: true,
109
+ countdownStartDateTime: new Date("2026-07-10T00:00:00.000Z").getTime() as unknown as string,
110
+ countdownEndDateTime: new Date("2026-12-31T23:59:59.000Z").getTime() as unknown as string,
111
+ },
112
+ };
113
+
114
+ export const WithIcon: Story = {
115
+ args: {
116
+ iconName: "call",
117
+ ctaButtonLabel: "call (866) 445-8084",
118
+ ctaButtonLink: "tel:+1-866-445-8084",
119
+ backgroundColor: "green",
120
+ enableCountdownTimer: false,
121
+ },
122
+ };
123
+
124
+ export const WithSuffixText: Story = {
125
+ args: {
126
+ ctaSuffixText: "to order Kinetic today",
127
+ ctaButtonLabel: "call (866) 445-8084",
128
+ ctaButtonLink: "tel:+1-866-445-8084",
129
+ backgroundColor: "navy",
130
+ },
131
+ };
132
+
133
+ export const BackgroundNavy: Story = {
134
+ args: { backgroundColor: "navy" },
135
+ };
136
+
137
+ export const BackgroundYellow: Story = {
138
+ args: { backgroundColor: "yellow" },
139
+ };
140
+
141
+ export const BackgroundGreen: Story = {
142
+ args: { backgroundColor: "green" },
143
+ };
144
+
145
+ export const BackgroundPurple: Story = {
146
+ args: { backgroundColor: "purple" },
147
+ };
148
+
149
+ export const BackgroundBlue: Story = {
150
+ args: { backgroundColor: "blue" },
151
+ };
152
+
153
+ export const BackgroundWhite: Story = {
154
+ args: { backgroundColor: "white", boxShadow: true },
155
+ };
@@ -1,6 +1,7 @@
1
1
  import React from "react";
2
2
  import { BreadcrumbNavigationProps } from "./types";
3
3
 
4
+ import { Link } from "@shared/components/link";
4
5
  import { MaterialIcon } from "@shared/components/material-icon";
5
6
  import { Text } from "@shared/components/text";
6
7
 
@@ -61,12 +62,12 @@ export const BreadcrumbNavigation: React.FC<
61
62
  className="mr-2 h-10 w-10"
62
63
  />
63
64
  )}
64
- <a
65
+ <Link
65
66
  href={linkProps.href}
66
67
  className={`label3 mr-2 whitespace-nowrap ${color} hover:underline`}
67
68
  >
68
69
  {linkProps.buttonLabel}
69
- </a>
70
+ </Link>
70
71
 
71
72
  <MaterialIcon name="chevron_right" className={`${color} `} />
72
73
  </li>
@@ -0,0 +1,327 @@
1
+ import type { CalloutProps } from "./types";
2
+
3
+ /* Card Items */
4
+
5
+ export const defaultSimpleItems: CalloutProps["items"] = [
6
+ {
7
+ title: "ultra-fast fiber internet",
8
+ body: "Reliable high-speed internet for your home on our fiber-backed network.",
9
+ image: "https://images.ctfassets.net/8d4yn2ywtegc/2tArpGyb6Pmog6tU972NRR/07761d8a230242e0e8e302dbc0345f5e/icon.svg",
10
+ imageAlt: "Purple speedometer",
11
+ imageWidth: 88,
12
+ imageHeight: 88,
13
+ iconAlignment: "left",
14
+ imageView: "standard",
15
+ cta: {
16
+ buttonLabel: "See options",
17
+ href: "/fiber-internet",
18
+ target: "_self",
19
+ showButtonAs: "text",
20
+ preserveQueryParameters: true,
21
+ },
22
+ ctaAlignment: "left",
23
+ showBackgroundImage: true,
24
+ // Blog-compatible fields
25
+ shortDescription: "Reliable high-speed internet for your home on our fiber-backed network.",
26
+ slug: "/fiber-internet",
27
+ blogCreationDate: "2026-01-15",
28
+ category: "Internet",
29
+ cover: { src: "https://images.ctfassets.net/8d4yn2ywtegc/17t6o3lIEE1OmyRtK2LSiN/7c00763e6afc39e97492c8aa3a3e5260/image_390.jpg", alt: "Fiber internet", width: 1920, height: 1281 },
30
+ // FullImage / FloatingImage-compatible fields
31
+ description: "Reliable high-speed internet for your home on our fiber-backed network.",
32
+ caption: "Fiber-backed network",
33
+ },
34
+ {
35
+ title: "tv and entertainment",
36
+ body: "Start streaming with best-in-class live entertainment.",
37
+ image: "https://images.ctfassets.net/8d4yn2ywtegc/2XHS5ndV3g8qq4GmDxdmai/911b3dcfe6ba2240dd09b4d4153390a5/icon-1.svg",
38
+ imageAlt: "Green streaming play",
39
+ imageWidth: 88,
40
+ imageHeight: 88,
41
+ iconAlignment: "left",
42
+ imageView: "standard",
43
+ cta: {
44
+ buttonLabel: "See options",
45
+ href: "/products",
46
+ target: "_self",
47
+ showButtonAs: "text",
48
+ preserveQueryParameters: true,
49
+ },
50
+ ctaAlignment: "left",
51
+ // Blog-compatible fields
52
+ shortDescription: "Start streaming with best-in-class live entertainment.",
53
+ slug: "/products",
54
+ blogCreationDate: "2026-02-20",
55
+ category: "Entertainment",
56
+ cover: { src: "https://images.ctfassets.net/8d4yn2ywtegc/2QoSoX9jUc6G8Tvg007MI0/7ae950e16d2ac83ff75b992dc9e24c37/image_388.jpg", alt: "TV entertainment", width: 1920, height: 1281 },
57
+ // FullImage / FloatingImage-compatible fields
58
+ description: "Start streaming with best-in-class live entertainment.",
59
+ caption: "Live entertainment",
60
+ },
61
+ {
62
+ title: "voice services",
63
+ body: "Unlimited nationwide voice calling with Kinetic phone service.",
64
+ image: "https://images.ctfassets.net/8d4yn2ywtegc/1JPpHXSe3WcQBOSTHhK3YD/9398907485bc13a90e46df73a4c6634c/icon-2.svg",
65
+ imageAlt: "Blue phone",
66
+ imageWidth: 88,
67
+ imageHeight: 88,
68
+ iconAlignment: "left",
69
+ imageView: "standard",
70
+ cta: {
71
+ buttonLabel: "See options",
72
+ href: "/home-phone",
73
+ target: "_self",
74
+ showButtonAs: "text",
75
+ preserveQueryParameters: true,
76
+ },
77
+ ctaAlignment: "left",
78
+ // Blog-compatible fields
79
+ shortDescription: "Unlimited nationwide voice calling with Kinetic phone service.",
80
+ slug: "/home-phone",
81
+ blogCreationDate: "2026-03-10",
82
+ category: "Phone",
83
+ cover: { src: "https://images.ctfassets.net/8d4yn2ywtegc/6CVKQrlrmcuNyefT6jM96d/8264de217e3822c0e7284c823c57314c/image_391.jpg", alt: "Voice services", width: 1920, height: 1280 },
84
+ // FullImage / FloatingImage-compatible fields
85
+ description: "Unlimited nationwide voice calling with Kinetic phone service.",
86
+ caption: "Nationwide calling",
87
+ },
88
+ ];
89
+
90
+ /* Floating Image Card Items */
91
+
92
+ export const floatingImageItems: CalloutProps["items"] = [
93
+ {
94
+ cardType: "floatingImage",
95
+ title: "pick your favorite video conferencing spot",
96
+ body: "Our technicians can ensure Wi-Fi reaches your home office with optimal connectivity.",
97
+ image: {
98
+ href: "https://images.ctfassets.net/8d4yn2ywtegc/17t6o3lIEE1OmyRtK2LSiN/7c00763e6afc39e97492c8aa3a3e5260/image_390.jpg",
99
+ title: "Pick your favorite spot",
100
+ width: 1920,
101
+ height: 1281,
102
+ },
103
+ },
104
+ {
105
+ cardType: "floatingImage",
106
+ title: "stream and game from anywhere",
107
+ body: "Strong Wi-Fi throughout your whole home means you can enjoy your favorite movies, sports, and video games from every room.",
108
+ image: {
109
+ href: "https://images.ctfassets.net/8d4yn2ywtegc/2QoSoX9jUc6G8Tvg007MI0/7ae950e16d2ac83ff75b992dc9e24c37/image_388.jpg",
110
+ title: "Stream and game from anywhere",
111
+ width: 1920,
112
+ height: 1281,
113
+ },
114
+ },
115
+ {
116
+ cardType: "floatingImage",
117
+ title: "manage your smart home",
118
+ body: "Let our technicians know what smart devices you need connected during your Wi-Fi installation for seamless coverage.",
119
+ image: {
120
+ href: "https://images.ctfassets.net/8d4yn2ywtegc/6CVKQrlrmcuNyefT6jM96d/8264de217e3822c0e7284c823c57314c/image_391.jpg",
121
+ title: "Brother and sister smart home",
122
+ width: 1920,
123
+ height: 1280,
124
+ },
125
+ },
126
+ ];
127
+
128
+ /* Full Image Card Items */
129
+
130
+ export const fullImageItems: CalloutProps["items"] = [
131
+ {
132
+ cardType: "fullImage",
133
+ title: "Stream in 4K",
134
+ description: "Crystal clear entertainment on every device.",
135
+ caption: "Starting at $49/mo",
136
+ image: {
137
+ href: "https://images.ctfassets.net/8d4yn2ywtegc/2QoSoX9jUc6G8Tvg007MI0/7ae950e16d2ac83ff75b992dc9e24c37/image_388.jpg",
138
+ title: "Streaming",
139
+ width: 1920,
140
+ height: 1281,
141
+ },
142
+ cta: {
143
+ buttonLabel: "Learn more",
144
+ href: "/streaming",
145
+ showButtonAs: "solid",
146
+ buttonVariant: "primary_brand",
147
+ },
148
+ },
149
+ {
150
+ cardType: "fullImage",
151
+ title: "Work From Home",
152
+ description: "Stay productive with reliable, fast internet.",
153
+ caption: "Starting at $59/mo",
154
+ image: {
155
+ href: "https://images.ctfassets.net/8d4yn2ywtegc/17t6o3lIEE1OmyRtK2LSiN/7c00763e6afc39e97492c8aa3a3e5260/image_390.jpg",
156
+ title: "Work from home",
157
+ width: 1920,
158
+ height: 1281,
159
+ },
160
+ cta: {
161
+ buttonLabel: "See plans",
162
+ href: "/plans",
163
+ showButtonAs: "solid",
164
+ buttonVariant: "primary_brand",
165
+ },
166
+ },
167
+ {
168
+ cardType: "fullImage",
169
+ title: "Game Online",
170
+ description: "Low latency for competitive gaming.",
171
+ caption: "Starting at $69/mo",
172
+ image: {
173
+ href: "https://images.ctfassets.net/8d4yn2ywtegc/6CVKQrlrmcuNyefT6jM96d/8264de217e3822c0e7284c823c57314c/image_391.jpg",
174
+ title: "Gaming",
175
+ width: 1920,
176
+ height: 1280,
177
+ },
178
+ cta: {
179
+ buttonLabel: "Get started",
180
+ href: "/gaming",
181
+ showButtonAs: "solid",
182
+ buttonVariant: "primary_brand",
183
+ },
184
+ },
185
+ ];
186
+
187
+ /* Blog Card Items */
188
+
189
+ export const blogItems: CalloutProps["items"] = [
190
+ {
191
+ cardType: "blog",
192
+ title: "How to Choose the Right Internet Plan",
193
+ shortDescription: "A guide to picking the best plan for your needs.",
194
+ blogCreationDate: "2026-01-15",
195
+ category: "Guides",
196
+ slug: "/blog/choose-internet-plan",
197
+ cover: {
198
+ src: "https://images.ctfassets.net/8d4yn2ywtegc/17t6o3lIEE1OmyRtK2LSiN/7c00763e6afc39e97492c8aa3a3e5260/image_390.jpg",
199
+ alt: "Internet plan guide",
200
+ width: 1920,
201
+ height: 1281,
202
+ },
203
+ },
204
+ {
205
+ cardType: "blog",
206
+ title: "Top 5 Tips for Faster Wi-Fi",
207
+ shortDescription: "Simple tricks to boost your home network speed.",
208
+ blogCreationDate: "2026-02-20",
209
+ category: "Tips",
210
+ slug: "/blog/faster-wifi-tips",
211
+ cover: {
212
+ src: "https://images.ctfassets.net/8d4yn2ywtegc/2QoSoX9jUc6G8Tvg007MI0/7ae950e16d2ac83ff75b992dc9e24c37/image_388.jpg",
213
+ alt: "Wi-Fi tips",
214
+ width: 1920,
215
+ height: 1281,
216
+ },
217
+ },
218
+ {
219
+ cardType: "blog",
220
+ title: "Understanding Fiber Optic Internet",
221
+ shortDescription: "Everything you need to know about fiber technology.",
222
+ blogCreationDate: "2026-03-10",
223
+ category: "Technology",
224
+ slug: "/blog/fiber-optic-internet",
225
+ cover: {
226
+ src: "https://images.ctfassets.net/8d4yn2ywtegc/6CVKQrlrmcuNyefT6jM96d/8264de217e3822c0e7284c823c57314c/image_391.jpg",
227
+ alt: "Fiber optic technology",
228
+ width: 1920,
229
+ height: 1280,
230
+ },
231
+ },
232
+ ];
233
+
234
+ /* Default Callout Args */
235
+
236
+ export const defaultCalloutArgs: Partial<CalloutProps> = {
237
+ title: "whatever you need, Kinetic can deliver",
238
+ subtitle:
239
+ "We're not just delivering high-speed internet. We're delivering it with local care, backed by people who live and work right here in your community. That means service you can count on and people you can trust.",
240
+ items: defaultSimpleItems,
241
+ cardType: "simple",
242
+ cardStackingMobile: true,
243
+ backgroundColor: "white",
244
+ textColor: "#00002d",
245
+ cardsWidth: true,
246
+ maxCardsPerRow: 3,
247
+ applyBoxShadow: false,
248
+ };
249
+
250
+ /* Floating Image Callout Args — mirrors martech */
251
+
252
+ export const withFloatingImageArgs: Partial<CalloutProps> = {
253
+ anchorId: "callout",
254
+ title: "we'll connect your devices, so you don't have to",
255
+ subtitle:
256
+ "With Kinetic's Whole Home Wi-Fi Set Up, our technicians ensure you can get online quickly from any device, anywhere in your home.",
257
+ items: floatingImageItems,
258
+ cardType: "floatingImage",
259
+ cardStackingMobile: true,
260
+ background: "#fff",
261
+ textColor: "#00002D",
262
+ maxCardsPerRow: 3,
263
+ };
264
+
265
+ /* Four Column Items */
266
+
267
+ export const fourColumnItems: CalloutProps["items"] = [
268
+ {
269
+ title: "1 Gig Internet",
270
+ body: "Download speeds up to 1 Gbps for the ultimate connected home.",
271
+ image: "https://images.ctfassets.net/8d4yn2ywtegc/2tArpGyb6Pmog6tU972NRR/07761d8a230242e0e8e302dbc0345f5e/icon.svg",
272
+ imageAlt: "Purple speedometer",
273
+ imageWidth: 88,
274
+ imageHeight: 88,
275
+ iconAlignment: "center",
276
+ imageView: "icon",
277
+ cta: { buttonLabel: "View plan", href: "/plans/1gig", showButtonAs: "text" },
278
+ ctaAlignment: "center",
279
+ },
280
+ {
281
+ title: "500 Mbps Internet",
282
+ body: "Fast enough for streaming, gaming, and video calls simultaneously.",
283
+ image: "https://images.ctfassets.net/8d4yn2ywtegc/2XHS5ndV3g8qq4GmDxdmai/911b3dcfe6ba2240dd09b4d4153390a5/icon-1.svg",
284
+ imageAlt: "Green streaming play",
285
+ imageWidth: 88,
286
+ imageHeight: 88,
287
+ iconAlignment: "center",
288
+ imageView: "icon",
289
+ cta: { buttonLabel: "View plan", href: "/plans/500", showButtonAs: "text" },
290
+ ctaAlignment: "center",
291
+ },
292
+ {
293
+ title: "300 Mbps Internet",
294
+ body: "Great for households with multiple connected devices.",
295
+ image: "https://images.ctfassets.net/8d4yn2ywtegc/1JPpHXSe3WcQBOSTHhK3YD/9398907485bc13a90e46df73a4c6634c/icon-2.svg",
296
+ imageAlt: "Blue phone",
297
+ imageWidth: 88,
298
+ imageHeight: 88,
299
+ iconAlignment: "center",
300
+ imageView: "icon",
301
+ cta: { buttonLabel: "View plan", href: "/plans/300", showButtonAs: "text" },
302
+ ctaAlignment: "center",
303
+ },
304
+ {
305
+ title: "100 Mbps Internet",
306
+ body: "Reliable everyday internet for browsing and email.",
307
+ image: "https://images.ctfassets.net/8d4yn2ywtegc/2tArpGyb6Pmog6tU972NRR/07761d8a230242e0e8e302dbc0345f5e/icon.svg",
308
+ imageAlt: "Purple speedometer",
309
+ imageWidth: 88,
310
+ imageHeight: 88,
311
+ iconAlignment: "center",
312
+ imageView: "icon",
313
+ cta: { buttonLabel: "View plan", href: "/plans/100", showButtonAs: "text" },
314
+ ctaAlignment: "center",
315
+ },
316
+ ];
317
+
318
+ /* Six Column Items */
319
+
320
+ export const sixColumnItems: CalloutProps["items"] = [
321
+ { title: "Speed", body: "Up to 1 Gbps fiber speeds." },
322
+ { title: "Reliability", body: "99.9% uptime guaranteed." },
323
+ { title: "Support", body: "24/7 local customer care." },
324
+ { title: "Value", body: "No hidden fees or contracts." },
325
+ { title: "Security", body: "Built-in network protection." },
326
+ { title: "Flexibility", body: "Upgrade anytime, no penalty." },
327
+ ];