@windstream/react-shared-components 0.1.77 → 0.1.79

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.
@@ -0,0 +1,156 @@
1
+ import {
2
+ ALL_ARTICLES,
3
+ ARTICLES_WITH_COVERS,
4
+ ARTICLES_WITHOUT_COVERS,
5
+ CATEGORY_OPTIONS,
6
+ } from "./BlogGrid.stories.mocks";
7
+ import { BlogGrid } from "./index";
8
+ import type { BlogGridProps } from "./types";
9
+
10
+ import { DocsPage } from "@shared/stories/DocsTemplate";
11
+ import type { ArgTypes, Meta, StoryObj } from "@storybook/react";
12
+
13
+ /* ------------
14
+ ArgTypes
15
+ --------------- */
16
+
17
+ const argTypes: ArgTypes<BlogGridProps> = {
18
+ paginatedArticles: {
19
+ control: { type: "object" as const },
20
+ description: "Array of blog articles to display on the current page",
21
+ },
22
+ totalArticles: {
23
+ control: { type: "number" as const },
24
+ description: "Total number of articles across all pages",
25
+ },
26
+ currentPage: {
27
+ control: { type: "number" as const },
28
+ description: "Current active page number",
29
+ },
30
+ totalPages: {
31
+ control: { type: "number" as const },
32
+ description: "Total number of pages",
33
+ },
34
+ selectedCategory: {
35
+ control: { type: "object" as const },
36
+ description: "Currently selected category option",
37
+ },
38
+ categoryOptions: {
39
+ control: { type: "object" as const },
40
+ description: "Available category filter options",
41
+ },
42
+ };
43
+
44
+ /* --------
45
+ Meta
46
+ ----------- */
47
+
48
+ const meta: Meta<typeof BlogGrid> = {
49
+ title: "Contentful Blocks/BlogGrid",
50
+ component: BlogGrid,
51
+ tags: ["autodocs"],
52
+ argTypes,
53
+ parameters: {
54
+ layout: "fullscreen",
55
+ docs: {
56
+ page: DocsPage,
57
+ description: {
58
+ component:
59
+ "A paginated, filterable grid of blog article cards with a category dropdown, article count summary, and pagination controls.",
60
+ },
61
+ },
62
+ },
63
+ args: {
64
+ paginatedArticles: ARTICLES_WITH_COVERS,
65
+ totalArticles: ALL_ARTICLES.length,
66
+ currentPage: 1,
67
+ totalPages: 2,
68
+ selectedCategory: CATEGORY_OPTIONS[0],
69
+ categoryOptions: CATEGORY_OPTIONS,
70
+ },
71
+ };
72
+
73
+ export default meta;
74
+ type Story = StoryObj<typeof meta>;
75
+
76
+ /* --------
77
+ Stories
78
+ ----------- */
79
+
80
+ export const Default: Story = {};
81
+
82
+ export const WithPagination: Story = {
83
+ args: {
84
+ paginatedArticles: ARTICLES_WITH_COVERS,
85
+ totalArticles: 18,
86
+ currentPage: 2,
87
+ totalPages: 3,
88
+ },
89
+ };
90
+
91
+ export const SinglePage: Story = {
92
+ args: {
93
+ paginatedArticles: ARTICLES_WITH_COVERS,
94
+ totalArticles: 6,
95
+ currentPage: 1,
96
+ totalPages: 1,
97
+ },
98
+ };
99
+
100
+ export const EmptyState: Story = {
101
+ args: {
102
+ paginatedArticles: [],
103
+ totalArticles: 9,
104
+ currentPage: 1,
105
+ totalPages: 1,
106
+ selectedCategory: CATEGORY_OPTIONS[1],
107
+ },
108
+ };
109
+
110
+ export const CategoryFilterActive: Story = {
111
+ args: {
112
+ paginatedArticles: ALL_ARTICLES.filter(
113
+ (a) => a.category === "Technology",
114
+ ),
115
+ totalArticles: 4,
116
+ currentPage: 1,
117
+ totalPages: 1,
118
+ selectedCategory: CATEGORY_OPTIONS[1],
119
+ },
120
+ };
121
+
122
+ export const WithCoverImages: Story = {
123
+ args: {
124
+ paginatedArticles: ARTICLES_WITH_COVERS,
125
+ totalArticles: 6,
126
+ currentPage: 1,
127
+ totalPages: 1,
128
+ },
129
+ };
130
+
131
+ export const WithoutCoverImages: Story = {
132
+ args: {
133
+ paginatedArticles: ARTICLES_WITHOUT_COVERS,
134
+ totalArticles: 3,
135
+ currentPage: 1,
136
+ totalPages: 1,
137
+ },
138
+ };
139
+
140
+ export const MixedCoverImages: Story = {
141
+ args: {
142
+ paginatedArticles: [...ARTICLES_WITH_COVERS.slice(4), ...ARTICLES_WITHOUT_COVERS],
143
+ totalArticles: 5,
144
+ currentPage: 1,
145
+ totalPages: 1,
146
+ },
147
+ };
148
+
149
+ export const SingleArticle: Story = {
150
+ args: {
151
+ paginatedArticles: [ARTICLES_WITH_COVERS[0]],
152
+ totalArticles: 1,
153
+ currentPage: 1,
154
+ totalPages: 1,
155
+ },
156
+ };
@@ -0,0 +1,147 @@
1
+ import { BreadcrumbNavigation } from "./index";
2
+
3
+ import { DocsPage } from "@shared/stories/DocsTemplate";
4
+ import type { Meta, StoryObj } from "@storybook/react";
5
+
6
+ const meta: Meta<typeof BreadcrumbNavigation> = {
7
+ title: "Contentful Blocks/Breadcrumb",
8
+ component: BreadcrumbNavigation,
9
+ tags: ["autodocs"],
10
+ parameters: {
11
+ layout: "fullscreen",
12
+ docs: {
13
+ page: DocsPage,
14
+ description: {
15
+ component:
16
+ "Breadcrumb navigation component used to display the user's location within the site hierarchy.",
17
+ },
18
+ },
19
+ },
20
+ args: {
21
+ textColor: "dark",
22
+ mobileTextColor: undefined,
23
+ desktopTextColor: undefined,
24
+ maxWidth: true,
25
+ float: "desktop",
26
+ links: [
27
+ { buttonLabel: "Home", href: "/" },
28
+ { buttonLabel: "Category", href: "/category" },
29
+ { buttonLabel: "Current Page" },
30
+ ],
31
+ },
32
+ argTypes: {
33
+ links: { control: "object" },
34
+ textColor: {
35
+ control: "radio",
36
+ options: ["dark", "light"],
37
+ },
38
+ mobileTextColor: {
39
+ control: "radio",
40
+ options: ["dark", "light"],
41
+ },
42
+ desktopTextColor: {
43
+ control: "radio",
44
+ options: ["dark", "light"],
45
+ },
46
+ maxWidth: { control: "boolean" },
47
+ float: {
48
+ control: "radio",
49
+ options: ["none", "mobile", "desktop", "always"],
50
+ },
51
+ },
52
+ };
53
+
54
+ export default meta;
55
+ type Story = StoryObj<typeof meta>;
56
+
57
+ /* ---------------- Stories ---------------- */
58
+
59
+ export const Default: Story = {};
60
+
61
+ export const LightText: Story = {
62
+ args: {
63
+ textColor: "light",
64
+ },
65
+ parameters: {
66
+ backgrounds: {
67
+ default: "dark",
68
+ },
69
+ },
70
+ };
71
+
72
+ export const WithImages: Story = {
73
+ args: {
74
+ links: [
75
+ {
76
+ buttonLabel: "Home",
77
+ href: "/",
78
+
79
+ image: {
80
+ url: "https://images.ctfassets.net/8d4yn2ywtegc/50Aef1sWZhzXKR0smBjFi8/e552eb0193b203822381f508babd51b3/Alabama.svg",
81
+ alt: "Home",
82
+ },
83
+ },
84
+ {
85
+ buttonLabel: "Category",
86
+ href: "/category",
87
+
88
+ image: {
89
+ url: "https://images.ctfassets.net/8d4yn2ywtegc/50Aef1sWZhzXKR0smBjFi8/e552eb0193b203822381f508babd51b3/Alabama.svg",
90
+ alt: "Category",
91
+ },
92
+ },
93
+ {
94
+ buttonLabel: "Current Page",
95
+ },
96
+ ],
97
+ },
98
+ };
99
+
100
+ export const SingleLink: Story = {
101
+ args: {
102
+ links: [{ buttonLabel: "Current Page" }],
103
+ },
104
+ };
105
+
106
+ export const EmptyLinks: Story = {
107
+ args: {
108
+ links: [],
109
+ },
110
+ };
111
+
112
+ export const FloatOnMobile: Story = {
113
+ args: {
114
+ float: "mobile",
115
+ },
116
+ };
117
+
118
+ export const FloatOnDesktop: Story = {
119
+ args: {
120
+ float: "desktop",
121
+ },
122
+ };
123
+
124
+ export const FloatAlways: Story = {
125
+ args: {
126
+ float: "always",
127
+ },
128
+ };
129
+
130
+ export const MaxWidthDisabled: Story = {
131
+ args: {
132
+ maxWidth: false,
133
+ },
134
+ };
135
+
136
+ export const MixedTextColors: Story = {
137
+ args: {
138
+ mobileTextColor: "light",
139
+ desktopTextColor: "dark",
140
+ float: "mobile",
141
+ },
142
+ parameters: {
143
+ backgrounds: {
144
+ default: "dark",
145
+ },
146
+ },
147
+ };
@@ -1,5 +1,4 @@
1
1
  import React from "react";
2
- import Button from "../button";
3
2
  import { BreadcrumbNavigationProps } from "./types";
4
3
 
5
4
  import { MaterialIcon } from "@shared/components/material-icon";
@@ -11,24 +10,37 @@ export const BreadcrumbNavigation: React.FC<
11
10
  const {
12
11
  links = [],
13
12
  textColor = "dark",
13
+ mobileTextColor,
14
+ desktopTextColor,
14
15
  maxWidth = true,
15
- floatOnMobile = false,
16
+ float = "desktop",
16
17
  } = props;
17
- const color = floatOnMobile
18
- ? textColor === "dark"
19
- ? "text-text"
20
- : "text-white"
21
- : textColor === "dark"
22
- ? "text-white xl:text-text"
23
- : "text-text xl:text-white";
18
+ const resolvedMobile =
19
+ mobileTextColor ?? (float === "desktop" ? "dark" : textColor);
20
+ const resolvedDesktop =
21
+ desktopTextColor ?? (float === "mobile" ? "dark" : textColor);
22
+ const mobileColor = resolvedMobile === "dark" ? "text-text" : "text-white";
23
+ const desktopColor =
24
+ resolvedDesktop === "dark" ? "xl:text-text" : "xl:text-white";
25
+ const color = `${mobileColor} ${desktopColor}`;
26
+
27
+ const olPosition =
28
+ float === "always"
29
+ ? "absolute"
30
+ : float === "mobile"
31
+ ? "absolute xl:relative"
32
+ : float === "desktop"
33
+ ? "relative xl:absolute"
34
+ : "relative";
35
+
24
36
  if (!links.length) return null;
25
37
  return (
26
38
  <nav
27
39
  aria-label="Breadcrumb"
28
- className={`${maxWidth ? `${!floatOnMobile && "mx-5"} max-w-120 xl:mx-auto` : "mx-auto"} relative`}
40
+ className={`${maxWidth ? `${float === "none" && "mx-5"} max-w-120 xl:mx-auto` : "mx-auto"} relative`}
29
41
  >
30
42
  <ol
31
- className={`right-0 z-10 mx-0 flex w-full flex-nowrap items-center gap-2 px-7 pb-0 pt-8 md:px-14 ${floatOnMobile ? "absolute" : "relative xl:absolute"} xl:mx-auto xl:flex-wrap xl:px-3`}
43
+ className={`right-0 z-10 mx-0 flex w-full flex-nowrap items-center gap-2 px-7 pb-0 pt-8 md:px-14 ${olPosition} xl:mx-auto xl:flex-wrap xl:px-3`}
32
44
  >
33
45
  {links.map((link, index) => {
34
46
  const { image, ...linkProps } = link;
@@ -49,11 +61,13 @@ export const BreadcrumbNavigation: React.FC<
49
61
  className="mr-2 h-10 w-10"
50
62
  />
51
63
  )}
52
- <Button
53
- {...linkProps}
54
- linkClassName={`label3 mr-2 whitespace-nowrap ${color}`}
55
- linkVariant="unstyled"
56
- />
64
+ <a
65
+ href={linkProps.href}
66
+ className={`label3 mr-2 whitespace-nowrap ${color} hover:underline`}
67
+ >
68
+ {linkProps.buttonLabel}
69
+ </a>
70
+
57
71
  <MaterialIcon name="chevron_right" className={`${color} `} />
58
72
  </li>
59
73
  ) : (
@@ -1,6 +1,8 @@
1
1
  export type BreadcrumbNavigationProps = {
2
2
  links?: Array<any>;
3
3
  textColor?: "dark" | "light";
4
+ mobileTextColor?: "dark" | "light";
5
+ desktopTextColor?: "dark" | "light";
4
6
  maxWidth?: boolean;
5
- floatOnMobile?: boolean;
7
+ float?: "none" | "mobile" | "desktop" | "always";
6
8
  };
@@ -23,29 +23,20 @@ const backgroundClassMap: Record<string, string> = {
23
23
 
24
24
  // Literal class strings (Tailwind JIT only picks up literal tokens; do not
25
25
  // build these by concatenation at runtime).
26
- const baseColMap: Record<number, string> = {
27
- 1: "grid-cols-1",
28
- 2: "grid-cols-2",
29
- 3: "grid-cols-3",
30
- 4: "grid-cols-4",
31
- 5: "grid-cols-5",
32
- 6: "grid-cols-6",
26
+ // Per-card responsive width classes for the flex-wrap layout. Mobile is
27
+ // always full-width; md renders 1 or 2 columns; lg renders up to 6.
28
+ // The calc() values subtract a portion of the gap so cards fit cleanly.
29
+ const mdWidthMap: Record<number, string> = {
30
+ 1: "md:w-full",
31
+ 2: "md:w-[calc(50%-1rem)]",
33
32
  };
34
- const lgColMap: Record<number, string> = {
35
- 1: "lg:grid-cols-1",
36
- 2: "lg:grid-cols-2",
37
- 3: "lg:grid-cols-3",
38
- 4: "lg:grid-cols-4",
39
- 5: "lg:grid-cols-5",
40
- 6: "lg:grid-cols-6",
41
- };
42
- const xlColMap: Record<number, string> = {
43
- 1: "xl:grid-cols-1",
44
- 2: "xl:grid-cols-2",
45
- 3: "xl:grid-cols-3",
46
- 4: "xl:grid-cols-4",
47
- 5: "xl:grid-cols-5",
48
- 6: "xl:grid-cols-6",
33
+ const lgWidthMap: Record<number, string> = {
34
+ 1: "lg:w-full",
35
+ 2: "lg:w-[calc(50%-0.75rem)]",
36
+ 3: "lg:w-[calc(33.333%-1rem)]",
37
+ 4: "lg:w-[calc(25%-1.125rem)]",
38
+ 5: "lg:w-[calc(20%-1.2rem)]",
39
+ 6: "lg:w-[calc(16.666%-1.25rem)]",
49
40
  };
50
41
 
51
42
  /**
@@ -95,32 +86,43 @@ export const Callout: React.FC<CalloutProps> = ({
95
86
  );
96
87
  const lgCols = clampCol(Math.min(desktopCols, itemCount || desktopCols));
97
88
 
98
- // Mobile / md: 1 col when stacking flag is on, else 2 (or 1 when single).
99
- const mobileCols = clampCol(cardStackingMobile ? 1 : itemCount === 1 ? 1 : 2);
100
-
101
- // When cardsWidth is false: full-width stacked layout (no grid).
102
- const gridClass = cardsWidth
89
+ // Layout selection:
90
+ // - cardsWidth === false → explicit vertical stack (single column)
91
+ // - any other value (true / undefined / string / etc.) → legacy
92
+ // flex-wrap layout that always renders multi-column on md+ and
93
+ // full-width on mobile. This matches the pre-0.1.79 DOM and keeps
94
+ // consumers like SMB-browse rendering correctly without changes.
95
+ const isStackMode = cardsWidth === false;
96
+ const gridClass = isStackMode
103
97
  ? cx(
104
- "grid items-stretch self-stretch",
105
- noGutter ? "gap-0" : "gap-6",
106
- baseColMap[mobileCols],
107
- lgColMap[lgCols],
108
- xlColMap[desktopCols]
109
- )
110
- : cx(
111
98
  "flex flex-col items-stretch self-stretch",
112
99
  noGutter ? "gap-0" : "gap-6"
100
+ )
101
+ : cx(
102
+ "flex flex-wrap items-stretch justify-center self-stretch",
103
+ noGutter ? "gap-0" : "gap-6",
104
+ noGutter ? "md:gap-0" : "md:gap-6"
105
+ );
106
+
107
+ // Per-card width classes — only used for flex-wrap layout. Mobile is
108
+ // full-width; md respects `cardStackingMobile` (1 col when true, 2 cols
109
+ // when false); lg uses the optimal column count. Stack mode skips this.
110
+ const cardWidthClass = isStackMode
111
+ ? ""
112
+ : cx(
113
+ "w-full",
114
+ mdWidthMap[clampCol(cardStackingMobile ? 1 : Math.min(2, desktopCols))],
115
+ lgWidthMap[lgCols]
113
116
  );
114
117
 
115
118
  const renderCard = (item: CalloutItem, index: number) => {
116
119
  const itemCardType: CalloutCardType = item.cardType ?? cardType;
117
120
 
118
- // When cardsWidth is true we control widths via grid columns, so do
119
- // NOT pass legacy lgWidth/mdWidth (which would force fixed pixel
120
- // widths and break the responsive grid).
121
- const widthProps = cardsWidth
122
- ? {}
123
- : { lgWidth: undefined, mdWidth: undefined };
121
+ // Stack mode preserves any legacy lgWidth/mdWidth on the card itself;
122
+ // flex-wrap mode controls width via the wrapper div, so strip them.
123
+ const widthProps = isStackMode
124
+ ? { lgWidth: undefined, mdWidth: undefined }
125
+ : {};
124
126
 
125
127
  switch (itemCardType) {
126
128
  case "blog": {
@@ -223,7 +225,18 @@ export const Callout: React.FC<CalloutProps> = ({
223
225
  )}
224
226
  </div>
225
227
  <div className={cx("card-holder", gridClass)}>
226
- {items.map((item, index: number) => renderCard(item, index))}
228
+ {items.map((item, index: number) =>
229
+ isStackMode ? (
230
+ renderCard(item, index)
231
+ ) : (
232
+ <div
233
+ key={`callout-card-${index}`}
234
+ className={cx("callout-card", cardWidthClass)}
235
+ >
236
+ {renderCard(item, index)}
237
+ </div>
238
+ )
239
+ )}
227
240
  </div>
228
241
  {(cta || finePrint) && (
229
242
  <div className="flex flex-col items-center gap-4">
@@ -35,8 +35,12 @@ export type CalloutProps = {
35
35
  * When `true` (default) cards are laid out in a responsive Tailwind
36
36
  * grid sized by `maxCardsPerRow`. When `false` the cards stretch
37
37
  * full-width and stack vertically (no inner widths).
38
+ *
39
+ * Accepts a string for backward compatibility with legacy consumers
40
+ * that pass a Tailwind width class (e.g. "w-1/2"); any truthy value
41
+ * enables grid mode — the string itself is not applied.
38
42
  */
39
- cardsWidth?: boolean;
43
+ cardsWidth?: boolean | string;
40
44
  maxCardsPerRow?: number;
41
45
  noGutter?: boolean;
42
46
  items: CalloutItem[];
@@ -258,7 +258,8 @@ export const Navigation: React.FC<NavigationProps> = props => {
258
258
  </div>
259
259
  </div>
260
260
  <div className="flex h-full items-center gap-10">
261
- {showCallNowCtaInMainNav
261
+ {showCallNowCtaInMainNav &&
262
+ !(displayUtilityNavigation && displayCallNowCta)
262
263
  ? renderMainNavCallCta(onCallClickDesktop)
263
264
  : null}
264
265
  {displaySearchBar ? (