@windstream/react-shared-components 0.2.67 → 0.2.68

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.
@@ -30,6 +30,7 @@ export const ProductCard: React.FC<ProductCardProps> = ({
30
30
  planSubtext,
31
31
  speed,
32
32
  price,
33
+ priceUnit = "/mo",
33
34
  description,
34
35
  bestValue = false,
35
36
  bestValueText = "best value",
@@ -69,9 +70,9 @@ export const ProductCard: React.FC<ProductCardProps> = ({
69
70
  // stacks keep the standard sizes at every breakpoint.
70
71
  const planNameClass = twoColumn ? "label3 md:subheading2" : "subheading2";
71
72
  const priceIntegerClass = twoColumn
72
- ? "heading2 md:subheading6"
73
- : "subheading6";
74
- const priceUnitClass = twoColumn ? "label3 md:label2" : "label2";
73
+ ? "heading2 md:subheading5"
74
+ : "subheading5";
75
+ const priceUnitClass = twoColumn ? "label3" : "label1";
75
76
 
76
77
  return (
77
78
  <article className="relative flex h-full w-full max-w-[392px] flex-col">
@@ -115,15 +116,12 @@ export const ProductCard: React.FC<ProductCardProps> = ({
115
116
  )}
116
117
  >
117
118
  {planName}
118
- </Text>
119
- <Text
120
- as="p"
121
- className={cx(
122
- "label3 text-wrap lowercase",
123
- isDark ? "text-text-accent-on-bg-inverse" : "text-text-brand"
124
- )}
125
- >
126
- {planSubtext}
119
+ {planSubtext ? (
120
+ <>
121
+ <br />
122
+ {planSubtext}
123
+ </>
124
+ ) : null}
127
125
  </Text>
128
126
  </div>
129
127
  <div className="flex flex-col items-start md:items-end">
@@ -155,14 +153,14 @@ export const ProductCard: React.FC<ProductCardProps> = ({
155
153
  isDark ? "text-text-inverse" : "text-text"
156
154
  )}
157
155
  >
158
- {price.split(".")[1]}/mo
156
+ {`${price.split(".")[1]}${priceUnit}`}
159
157
  </Text>
160
158
  </div>
161
159
  {priceSuffixBottom ? (
162
160
  <Text
163
161
  as="span"
164
162
  className={cx(
165
- "footnote text-nowrap",
163
+ "micro text-nowrap",
166
164
  isDark ? "text-text-inverse" : "text-text"
167
165
  )}
168
166
  >
@@ -176,7 +174,8 @@ export const ProductCard: React.FC<ProductCardProps> = ({
176
174
  <section data-pc-description>
177
175
  <Text
178
176
  className={cx(
179
- "footnote md:body2 md:h-[56px]",
177
+ twoColumn ? "footnote md:body2" : "body2",
178
+ "md:h-[56px]",
180
179
  isDark ? "text-text-inverse" : "text-text"
181
180
  )}
182
181
  >
@@ -189,18 +188,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
189
188
  <div data-pc-gift className="min-h-[76px]">
190
189
  <div
191
190
  className={cx(
192
- "flex-col items-center rounded-surface-sm p-2",
191
+ "flex w-full flex-col gap-3 rounded-surface-sm p-2",
193
192
  isDark ? "bg-bg-surface-inverse" : "bg-bg-surface-info"
194
193
  )}
195
194
  >
196
195
  {giftBadge?.map((badge: any) => {
197
196
  return (
198
- <div key={badge.title} className="flex items-center gap-2">
197
+ <div
198
+ key={badge.title}
199
+ className="flex w-full items-center gap-2"
200
+ >
199
201
  <Image
200
202
  src={badge.icon}
201
203
  alt={badge.title}
202
- width={24}
203
- height={24}
204
+ width={20}
205
+ height={20}
204
206
  />
205
207
  <Text
206
208
  as="div"
@@ -290,20 +292,21 @@ export const ProductCard: React.FC<ProductCardProps> = ({
290
292
  iconClassName="mt-0"
291
293
  iconColor={isDark ? "yellow" : "green"}
292
294
  listContainerClassName="mt-0 space-y-0 flex flex-col gap-3"
293
- listItemClassName={`${isDark ? "text-text-inverse" : "text-text"}`}
295
+ listItemClassName={`body3 ${isDark ? "text-text-inverse" : "text-text"}`}
294
296
  />
295
297
  </div>
296
298
  {innerBadge?.badgeIcon ? (
297
- <div className="mt-3 flex items-center gap-2 text-left">
299
+ <div className="mt-3 flex items-center gap-1 text-left">
298
300
  <Image
299
301
  src={innerBadge.badgeIcon}
300
302
  alt="Inner Badge"
301
303
  width={24}
302
304
  height={24}
305
+ className="ml-1 h-auto pr-2"
303
306
  />
304
307
  <span
305
308
  className={cx(
306
- "footnote",
309
+ "body3",
307
310
  isDark ? "text-text-inverse" : "text-text"
308
311
  )}
309
312
  >
@@ -10,6 +10,9 @@ export interface ProductCardProps {
10
10
  techType?: string;
11
11
  ismaxSpeed?: boolean;
12
12
  price: string;
13
+ /** The price unit rendered after the cents, e.g. "/mo" or "/mo*". Defaults
14
+ * to "/mo". Lets Contentful control a trailing disclaimer star. */
15
+ priceUnit?: string;
13
16
  priceSuffixBottom?: string;
14
17
  description: string;
15
18
  bestValue?: boolean;
@@ -263,6 +263,11 @@ function ProductCardPanel({
263
263
  const planSubtext = planContent?.[1] || "";
264
264
  const priceCents = item?.priceSuffix?.split("/")?.[0] || "00";
265
265
  const formattedPrice = `${item?.price || "0"}.${priceCents}`;
266
+ // Preserve the full unit after the cents (e.g. "/mo*") so a Contentful
267
+ // disclaimer star renders; fall back to "/mo" when no unit is present.
268
+ const suffixParts = item?.priceSuffix?.split("/") || [];
269
+ const priceUnit =
270
+ suffixParts.length > 1 ? `/${suffixParts.slice(1).join("/")}` : "/mo";
266
271
  const features = item?.benefits?.items || [];
267
272
  const bestValue = item?.highlighted || false;
268
273
  const rawBadge = item?.giftRewards?.list?.items || [];
@@ -308,6 +313,7 @@ function ProductCardPanel({
308
313
  techType={item?.techType || ""}
309
314
  ismaxSpeed={item?.ismaxSpeed || ""}
310
315
  price={formattedPrice}
316
+ priceUnit={priceUnit}
311
317
  description={item?.productCardDescription || ""}
312
318
  giftBadge={rawBadge}
313
319
  innerBadge={innerBadge}
@@ -54,6 +54,9 @@ const fields: DynamicFormItem[] = [
54
54
  requiredField: true,
55
55
  errorMessage: "Enter a valid phone number",
56
56
  },
57
+ {
58
+ itemType: "divider",
59
+ },
57
60
  {
58
61
  itemType: "formField",
59
62
  fieldId: "topic",
@@ -57,6 +57,31 @@ describe("dynamicRenderingWithGroupings", () => {
57
57
  expect(screen.getByText("Custom Content")).toBeInTheDocument();
58
58
  });
59
59
 
60
+ it("renders a full-width divider by default", () => {
61
+ const { container } = renderGroups([{ itemType: "divider" }]);
62
+ const divider = container.querySelector<HTMLElement>('[role="separator"]');
63
+ expect(divider).toBeInTheDocument();
64
+ expect(divider).toHaveClass("w-full");
65
+ expect(divider?.style.height).toBe("1px");
66
+ });
67
+
68
+ it("renders a half-width divider with a custom thickness", () => {
69
+ const { container } = renderGroups([
70
+ { itemType: "divider", dividerLength: false, dividerThickness: 4 },
71
+ ]);
72
+ const divider = container.querySelector<HTMLElement>('[role="separator"]');
73
+ expect(divider).toHaveClass("w-1/2");
74
+ expect(divider?.style.height).toBe("4px");
75
+ });
76
+
77
+ it("clamps a divider thickness below 1 to 1px", () => {
78
+ const { container } = renderGroups([
79
+ { itemType: "divider", dividerThickness: 0 },
80
+ ]);
81
+ const divider = container.querySelector<HTMLElement>('[role="separator"]');
82
+ expect(divider?.style.height).toBe("1px");
83
+ });
84
+
60
85
  it("renders a conditional item", () => {
61
86
  renderGroups([
62
87
  {
@@ -7,6 +7,7 @@ import FormFieldElement from "./form-field";
7
7
  import { DynamicFormItem } from "./types";
8
8
  import { groupFieldsForLayout } from "./utils";
9
9
 
10
+ import { Divider } from "@shared/components/divider";
10
11
  import { Text } from "@shared/components/text";
11
12
 
12
13
  type FormValues = Record<string, any>;
@@ -46,6 +47,20 @@ const renderItem = (
46
47
  />
47
48
  );
48
49
  }
50
+ if (item.itemType === "divider") {
51
+ const strokeWidth = Math.max(item.dividerThickness || 1, 1);
52
+ const widthClass = item.dividerLength === false ? "w-1/2" : "w-full";
53
+ return (
54
+ <div key={key} className="flex w-full justify-center">
55
+ <Divider
56
+ role="separator"
57
+ aria-label="Content divider"
58
+ className={widthClass}
59
+ style={{ height: `${strokeWidth}px` }}
60
+ />
61
+ </div>
62
+ );
63
+ }
49
64
  return <React.Fragment key={key}>{item.content}</React.Fragment>;
50
65
  };
51
66
 
@@ -69,14 +69,19 @@ export interface ConditionalFieldConfig {
69
69
  grouped?: boolean;
70
70
  }
71
71
 
72
+ export interface DynamicFormDividerConfig {
73
+ dividerLength?: boolean;
74
+ dividerThickness?: number;
75
+ }
76
+
72
77
  /**
73
- * A flat list item rendered by the form. Either a form field, a conditional
74
- * field group, or an arbitrary pre-rendered node (e.g. a nested CMS block that
75
- * the app rendered itself and passes through).
78
+ * A flat list item rendered by the form: a form field, conditional field,
79
+ * divider, or custom React node.
76
80
  */
77
81
  export type DynamicFormItem =
78
82
  | ({ itemType: "formField" } & DynamicFormFieldConfig)
79
83
  | ({ itemType: "conditional" } & ConditionalFieldConfig)
84
+ | ({ itemType: "divider"; fieldId?: string } & DynamicFormDividerConfig)
80
85
  | { itemType: "custom"; fieldId?: string; content: React.ReactNode };
81
86
 
82
87
  export interface DynamicFormPayload {