@windstream/react-shared-components 0.2.81 → 0.2.82

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.
package/dist/core.d.ts CHANGED
@@ -423,7 +423,7 @@ type ButtonAsAnchor = ButtonCustomProps & Omit<AnchorHTMLAttributes<HTMLAnchorEl
423
423
  };
424
424
  type ButtonProps = ButtonAsButton | ButtonAsAnchor;
425
425
 
426
- declare const BrandButton: React__default.ForwardRefExoticComponent<ButtonProps$2 & React__default.RefAttributes<HTMLButtonElement | HTMLAnchorElement>>;
426
+ declare const BrandButton: React__default.ForwardRefExoticComponent<ButtonProps$2 & React__default.RefAttributes<HTMLAnchorElement | HTMLButtonElement>>;
427
427
 
428
428
  declare const Checklist: React__default.FC<ChecklistProps>;
429
429
 
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?: "default" | "error" | "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.81",
3
+ "version": "0.2.82",
4
4
  "type": "module",
5
5
  "description": "Shared React components for Kinetic applications",
6
6
  "main": "dist/index.js",
@@ -141,6 +141,17 @@ describe("AnchoredBottomBanner", () => {
141
141
  expect(container.querySelector(".text-white")).toBeInTheDocument();
142
142
  });
143
143
 
144
+ it("falls back to the accent background with dark text for an unmapped color", () => {
145
+ const { container } = render(
146
+ <AnchoredBottomBanner backgroundColor="orange" ctaButtonLabel="Test" />
147
+ );
148
+ expect(
149
+ container.querySelector(".bg-bg-fill-brand-accent")
150
+ ).toBeInTheDocument();
151
+ expect(container.querySelector(".text-text-primary")).toBeInTheDocument();
152
+ expect(container.querySelector(".text-white")).not.toBeInTheDocument();
153
+ });
154
+
144
155
  // Box shadow
145
156
  it("uses default shadow when boxShadow is not provided", () => {
146
157
  const { container } = render(<AnchoredBottomBanner />);
@@ -568,6 +579,30 @@ describe("AnchoredBottomBanner scheduledMessages", () => {
568
579
  container.querySelector(".bg-bg-fill-brand-accent")
569
580
  ).toBeInTheDocument();
570
581
  });
582
+
583
+ it("falls back to the accent background with dark text for an unmapped active-message color", () => {
584
+ const { container } = render(
585
+ <AnchoredBottomBanner
586
+ backgroundColor="navy"
587
+ scheduledMessages={[
588
+ {
589
+ scheduledStart: "2026-05-18",
590
+ scheduleEnd: "2026-05-20",
591
+ backgroundColor: "orange",
592
+ ctaButtonLabel: "Active message",
593
+ },
594
+ ]}
595
+ />
596
+ );
597
+ expect(
598
+ container.querySelector(".bg-bg-fill-brand-accent")
599
+ ).toBeInTheDocument();
600
+ expect(
601
+ container.querySelector(".bg-bg-fill-inverse")
602
+ ).not.toBeInTheDocument();
603
+ expect(container.querySelector(".text-text-primary")).toBeInTheDocument();
604
+ expect(container.querySelector(".text-white")).not.toBeInTheDocument();
605
+ });
571
606
  });
572
607
 
573
608
  describe("AnchoredBottomBanner scheduling timezone (America/New_York)", () => {
@@ -147,9 +147,10 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
147
147
  countdownStartDateTime,
148
148
  countdownEndDateTime,
149
149
  scheduledMessages,
150
+ initialNowMs,
150
151
  onResolved,
151
152
  }) => {
152
- const backGroundColorClasses = {
153
+ const backGroundColorClasses: Record<string, string> = {
153
154
  navy: "bg-bg-fill-inverse",
154
155
  green: "bg-bg-fill-success",
155
156
  blue: "bg-bg-fill-brand-info",
@@ -158,19 +159,27 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
158
159
  white: "bg-white",
159
160
  };
160
161
 
161
- const [nowMs, setNowMs] = useState(() => Date.now());
162
+ // Seed from the server-provided timestamp so SSR and the first client render
163
+ // agree; the mount effect below reconciles to the live clock.
164
+ const [nowMs, setNowMs] = useState(() => initialNowMs ?? Date.now());
162
165
  const intervalRef = useRef<number | null>(null);
163
166
 
164
167
  const [hasMounted, setHasMounted] = useState(false);
165
- useEffect(() => setHasMounted(true), []);
168
+ useEffect(() => {
169
+ setHasMounted(true);
170
+ // Reconcile the shared initial timestamp to the client's real time; any
171
+ // resulting boundary change is a normal post-hydration update, not a
172
+ // hydration mismatch.
173
+ setNowMs(Date.now());
174
+ }, []);
166
175
 
167
- // The message active for "now" (display window + daysToShow) wins.
176
+ // The message active for "now" (display window + daysToShow) wins. Seeded from
177
+ // the shared initialNowMs so SSR and the first client render resolve the same
178
+ // message (background + content paint immediately, no post-hydration flip and
179
+ // no structural mismatch); the mount effect then reconciles to the live clock.
168
180
  const activeMessage = useMemo(
169
- () =>
170
- hasMounted
171
- ? scheduledMessages?.find(message => isMessageActive(message, nowMs))
172
- : undefined,
173
- [scheduledMessages, nowMs, hasMounted]
181
+ () => scheduledMessages?.find(message => isMessageActive(message, nowMs)),
182
+ [scheduledMessages, nowMs]
174
183
  );
175
184
  const hasActiveMessage = activeMessage !== undefined;
176
185
 
@@ -311,13 +320,17 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
311
320
  const effectiveBackgroundColor = hasActiveMessage
312
321
  ? activeMessage?.backgroundColor
313
322
  : backgroundColor;
314
- const bgClass = effectiveBackgroundColor
323
+ // Unmapped colors fall back to the light accent background, so the contrast
324
+ // decision follows the RESOLVED background (fallback = light) rather than the
325
+ // raw value, keeping CTA/icon text readable.
326
+ const mappedBgClass = effectiveBackgroundColor
315
327
  ? backGroundColorClasses[effectiveBackgroundColor]
316
- : "bg-bg-fill-brand-accent";
328
+ : undefined;
329
+ const bgClass = mappedBgClass || "bg-bg-fill-brand-accent";
317
330
  const isLightBackground =
331
+ !mappedBgClass ||
318
332
  effectiveBackgroundColor === "yellow" ||
319
- effectiveBackgroundColor === "white" ||
320
- !effectiveBackgroundColor;
333
+ effectiveBackgroundColor === "white";
321
334
  const textColorClass = isLightBackground ? "text-text-primary" : "text-white";
322
335
 
323
336
  return (
@@ -348,12 +361,15 @@ export const AnchoredBottomBanner: React.FC<AnchoredBottomBannerProps> = ({
348
361
  className={`${textColorClass} align-text-bottom`}
349
362
  />
350
363
  )}
351
- {hasMounted && countdown.shouldShow && (
352
- <span className="inline-block whitespace-nowrap rounded-lg bg-white px-1 tabular-nums text-text">
364
+ {countdown.shouldShow && (
365
+ <span
366
+ className="inline-block whitespace-nowrap rounded-lg bg-white px-1 tabular-nums text-text"
367
+ suppressHydrationWarning={true}
368
+ >
353
369
  {countdown.text}
354
370
  </span>
355
371
  )}
356
- {hasMounted && countdown.shouldShow ? " " : null}
372
+ {countdown.shouldShow ? " " : null}
357
373
  {effectiveCtaButtonLabel && effectiveCtaButtonLabel}{" "}
358
374
  {effectiveCtaSuffixText && (
359
375
  <span className="ml-0.5">{effectiveCtaSuffixText}</span>
@@ -17,6 +17,18 @@ export type ScheduledMessageDay =
17
17
  | "FRI"
18
18
  | "SAT";
19
19
 
20
+ // Known banner colors keep editor autocomplete, while `(string & {})` lets any
21
+ // new color added in Contentful flow through without changing this type.
22
+ export type AnchoredBannerBackgroundColor =
23
+ | "navy"
24
+ | "yellow"
25
+ | "green"
26
+ | "purple"
27
+ | "blue"
28
+ | "white"
29
+ // eslint-disable-next-line @typescript-eslint/ban-types
30
+ | (string & {});
31
+
20
32
  export interface ScheduledMessage {
21
33
  // Stable id for analytics (Contentful entry id or title).
22
34
  id?: string;
@@ -41,7 +53,7 @@ export interface ScheduledMessage {
41
53
 
42
54
  export interface AnchoredBottomBannerProps {
43
55
  ctaSuffixText?: string;
44
- backgroundColor?: "navy" | "yellow" | "green" | "purple" | "blue" | "white";
56
+ backgroundColor?: AnchoredBannerBackgroundColor;
45
57
  iconName?: string;
46
58
  boxShadow?: boolean;
47
59
  ctaButtonLabel?: string;
@@ -54,6 +66,11 @@ export interface AnchoredBottomBannerProps {
54
66
  // When a message is active for today, it drives the countdown + CTA content;
55
67
  // otherwise the banner falls back to its own top-level values above.
56
68
  scheduledMessages?: ScheduledMessage[];
69
+ // Server-generated timestamp (epoch ms) so SSR and the first client render
70
+ // resolve the same schedule/countdown state (no structural hydration
71
+ // mismatch). Reconciled to the live clock after mount. Falls back to
72
+ // Date.now() when omitted.
73
+ initialNowMs?: number;
57
74
  // Fires with the resolved result so consumers (analytics) know what showed.
58
75
  onResolved?: (result: {
59
76
  hasActiveMessage: boolean;