@pega/cosmos-react-work 9.12.0 → 9.14.0

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 (27) hide show
  1. package/lib/components/CaseView/CaseView.styles.d.ts.map +1 -1
  2. package/lib/components/Details/Details.d.ts.map +1 -1
  3. package/lib/components/Details/Details.js +8 -3
  4. package/lib/components/Details/Details.js.map +1 -1
  5. package/lib/components/Details/DetailsList.d.ts.map +1 -1
  6. package/lib/components/Details/DetailsList.js +2 -2
  7. package/lib/components/Details/DetailsList.js.map +1 -1
  8. package/lib/components/GenAICoach/GenAICoach.types.d.ts +2 -0
  9. package/lib/components/GenAICoach/GenAICoach.types.d.ts.map +1 -1
  10. package/lib/components/GenAICoach/GenAICoach.types.js.map +1 -1
  11. package/lib/components/GenAICoach/GenAIMessage.d.ts.map +1 -1
  12. package/lib/components/GenAICoach/GenAIMessage.js +4 -4
  13. package/lib/components/GenAICoach/GenAIMessage.js.map +1 -1
  14. package/lib/components/Stages/Stages.d.ts.map +1 -1
  15. package/lib/components/Stages/Stages.js +55 -36
  16. package/lib/components/Stages/Stages.js.map +1 -1
  17. package/lib/components/Stages/Stages.styles.d.ts +25 -3
  18. package/lib/components/Stages/Stages.styles.d.ts.map +1 -1
  19. package/lib/components/Stages/Stages.styles.js +594 -212
  20. package/lib/components/Stages/Stages.styles.js.map +1 -1
  21. package/lib/components/Stages/Stages.types.d.ts +6 -0
  22. package/lib/components/Stages/Stages.types.d.ts.map +1 -1
  23. package/lib/components/Stages/Stages.types.js.map +1 -1
  24. package/lib/components/Stages/index.d.ts +1 -1
  25. package/lib/components/Stages/index.d.ts.map +1 -1
  26. package/lib/components/Stages/index.js.map +1 -1
  27. package/package.json +3 -3
@@ -1,6 +1,6 @@
1
- import styled, { css } from 'styled-components';
2
- import { transparentize } from 'polished';
3
- import { calculateFontSize, DateTimeDisplay, defaultThemeProp, Icon, Text, StyledIcon, calculateForegroundColor } from '@pega/cosmos-react-core';
1
+ import styled, { css, keyframes } from 'styled-components';
2
+ import { transparentize, darken } from 'polished';
3
+ import { calculateFontSize, DateTimeDisplay, defaultThemeProp, Icon, Text, StyledIcon, calculateForegroundColor, tryCatch } from '@pega/cosmos-react-core';
4
4
  const stageClipPath = (leftNotch, rightPoint, mirrored = false) => {
5
5
  if (mirrored) {
6
6
  // RTL: mirror the LTR clip-path polygon
@@ -30,6 +30,20 @@ const stageClipPath = (leftNotch, rightPoint, mirrored = false) => {
30
30
  }
31
31
  return `clip-path: polygon(${points.join(', ')});`;
32
32
  };
33
+ const segmentedConfig = {
34
+ width: 'calc(0.8 * var(--chevron-spacing))',
35
+ coarseWidth: 'calc(1.0 * var(--chevron-spacing))'
36
+ };
37
+ const segmentedClipPath = (leftCut, rightCut, mirrored = false) => {
38
+ if (mirrored) {
39
+ const topRight = rightCut ? 'calc(100% - var(--segmented-width)) 0%' : '100% 0%';
40
+ const bottomLeft = leftCut ? 'var(--segmented-width) 100%' : '0% 100%';
41
+ return `clip-path: polygon(0% 0%, ${topRight}, 100% 100%, ${bottomLeft});`;
42
+ }
43
+ const topLeft = leftCut ? 'var(--segmented-width) 0%' : '0% 0%';
44
+ const bottomRight = rightCut ? 'calc(100% - var(--segmented-width)) 100%' : '100% 100%';
45
+ return `clip-path: polygon(${topLeft}, 100% 0%, ${bottomRight}, 0% 100%);`;
46
+ };
33
47
  // === Chevron Configuration ===
34
48
  // Centralizes spacing and border thickness via CSS custom properties set at StyledStages
35
49
  const chevronConfig = {
@@ -137,14 +151,6 @@ export const StyledStagesWrapper = styled.div(({ theme }) => {
137
151
  --stages-wrapper-bottom-radius,
138
152
  ${theme.components.card['border-radius']}
139
153
  );
140
- -webkit-mask-image: linear-gradient(black, black),
141
- linear-gradient(
142
- to right,
143
- transparent calc(-1 * var(--mask-edge-overhang)),
144
- black var(--stages-fade-start),
145
- black calc(100% - var(--stages-fade-end)),
146
- transparent calc(100% + var(--mask-edge-overhang))
147
- );
148
154
  mask-image: linear-gradient(black, black),
149
155
  linear-gradient(
150
156
  to right,
@@ -162,14 +168,6 @@ export const StyledStagesWrapper = styled.div(({ theme }) => {
162
168
  mask-repeat: no-repeat, no-repeat;
163
169
 
164
170
  [dir='rtl'] & {
165
- -webkit-mask-image: linear-gradient(black, black),
166
- linear-gradient(
167
- to right,
168
- transparent calc(-1 * var(--mask-edge-overhang)),
169
- black var(--stages-fade-end),
170
- black calc(100% - var(--stages-fade-start)),
171
- transparent calc(100% + var(--mask-edge-overhang))
172
- );
173
171
  mask-image: linear-gradient(black, black),
174
172
  linear-gradient(
175
173
  to right,
@@ -186,13 +184,6 @@ export const StyledStagesWrapper = styled.div(({ theme }) => {
186
184
 
187
185
  @media (pointer: coarse) {
188
186
  --stages-fade-width: calc(${theme.base.spacing} * 3);
189
- -webkit-mask-image: linear-gradient(
190
- to right,
191
- transparent calc(-1 * var(--mask-edge-overhang)),
192
- black var(--stages-fade-start),
193
- black calc(100% - var(--stages-fade-end)),
194
- transparent calc(100% + var(--mask-edge-overhang))
195
- );
196
187
  mask-image: linear-gradient(
197
188
  to right,
198
189
  transparent calc(-1 * var(--mask-edge-overhang)),
@@ -205,13 +196,6 @@ export const StyledStagesWrapper = styled.div(({ theme }) => {
205
196
  mask-repeat: no-repeat;
206
197
 
207
198
  [dir='rtl'] & {
208
- -webkit-mask-image: linear-gradient(
209
- to right,
210
- transparent calc(-1 * var(--mask-edge-overhang)),
211
- black var(--stages-fade-end),
212
- black calc(100% - var(--stages-fade-start)),
213
- transparent calc(100% + var(--mask-edge-overhang))
214
- );
215
199
  mask-image: linear-gradient(
216
200
  to right,
217
201
  transparent calc(-1 * var(--mask-edge-overhang)),
@@ -225,14 +209,17 @@ export const StyledStagesWrapper = styled.div(({ theme }) => {
225
209
  });
226
210
  StyledStagesWrapper.defaultProps = defaultThemeProp;
227
211
  /**
228
- * Scrollable `<ol>` containing all stage chevron items. Sets the `--chevron-spacing`
229
- * and `--chevron-border-thickness` custom properties consumed by child elements.
212
+ * Scrollable `<ol>` containing all stage chevron items. Sets the `--chevron-spacing`,
213
+ * `--chevron-border-thickness`, and `--segmented-border-thickness` custom properties
214
+ * consumed by child elements.
230
215
  */
231
- export const StyledStages = styled.ol(({ theme }) => {
216
+ export const StyledStages = styled.ol(({ theme, variant }) => {
217
+ const isMilestone = variant === 'milestone';
232
218
  return css `
233
219
  --chevron-spacing: ${theme.base.spacing};
234
220
  --chevron-border-thickness: ${theme.components.button['border-width']};
235
- background: ${theme.components.card.background};
221
+ --segmented-border-thickness: calc(2 * ${theme.components.button['border-width']});
222
+ --segmented-width: ${segmentedConfig.width};
236
223
  border-radius: ${theme.components.card['border-radius']};
237
224
  display: flex;
238
225
  padding: 0;
@@ -243,6 +230,17 @@ export const StyledStages = styled.ol(({ theme }) => {
243
230
  scrollbar-width: thin;
244
231
  scrollbar-color: transparent transparent;
245
232
 
233
+ ${isMilestone &&
234
+ css `
235
+ --milestone-size: calc(4 * ${theme.base.spacing});
236
+ --milestone-pulse-min: calc(var(--milestone-size) * 0.1);
237
+ --milestone-pulse-start: ${transparentize(0.5, theme.base.palette.interactive)};
238
+ --milestone-pulse-spread: calc(var(--milestone-size) * 0.25);
239
+ --milestone-pulse-end: ${transparentize(0.75, theme.base.palette.interactive)};
240
+ padding-block: ${theme.base.spacing};
241
+ padding-inline: calc(var(--milestone-size) * 0.25);
242
+ `}
243
+
246
244
  &:hover,
247
245
  &:focus-within {
248
246
  scrollbar-color: var(
@@ -330,11 +328,58 @@ export const StyledInnerStage = styled.span(({ theme, ellipsis }) => {
330
328
  `;
331
329
  });
332
330
  StyledInnerStage.defaultProps = defaultThemeProp;
331
+ const stageButtonChevronStyles = (theme) => css `
332
+ padding-inline-start: calc(2 * var(--chevron-spacing));
333
+ padding-inline-end: var(--chevron-spacing);
334
+ width: calc(100% + ${chevronConfig.width});
335
+ --stage-chevron-width: ${chevronConfig.width};
336
+
337
+ ${StyledInnerStage} {
338
+ transform: translateX(calc(-0.25 * var(--chevron-spacing)));
339
+ opacity: ${theme.base.transparency['transparent-2']};
340
+ }
341
+
342
+ ${stageClipPath(true, true)}
343
+
344
+ [dir='rtl'] & ${StyledInnerStage} {
345
+ transform: translateX(calc(0.25 * var(--chevron-spacing)));
346
+ }
347
+
348
+ [dir='rtl'] & {
349
+ ${stageClipPath(true, true, true)}
350
+ }
351
+
352
+ @media (pointer: coarse) {
353
+ padding-inline-start: calc(2.25 * var(--chevron-spacing));
354
+ width: calc(100% + ${chevronConfig.coarseWidth});
355
+ --stage-chevron-width: ${chevronConfig.coarseWidth};
356
+ }
357
+ `;
358
+ const stageButtonSegmentedStyles = (theme) => css `
359
+ width: 100%;
360
+ padding-inline-start: calc(var(--segmented-width) + var(--chevron-spacing));
361
+ padding-inline-end: calc(var(--segmented-width) + var(--chevron-spacing));
362
+
363
+ ${StyledInnerStage} {
364
+ opacity: ${theme.base.transparency['transparent-2']};
365
+ }
366
+
367
+ ${segmentedClipPath(true, true)}
368
+
369
+ [dir='rtl'] & {
370
+ ${segmentedClipPath(true, true, true)}
371
+ }
372
+ `;
373
+ const stageButtonVariants = {
374
+ chevron: stageButtonChevronStyles,
375
+ segmented: stageButtonSegmentedStyles,
376
+ milestone: () => css ``
377
+ };
333
378
  /**
334
379
  * Clickable `<button>` for a single stage. Renders the chevron clip-path shape and
335
380
  * applies status-based background and foreground colours.
336
381
  */
337
- export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area': hitArea, transparency, 'font-weight': fontWeight }, components: { 'case-view': { stages } } }, status, readOnly }) => {
382
+ export const StyledStage = styled.button(({ theme, theme: { base: { palette, 'hit-area': hitArea, transparency, 'font-weight': fontWeight }, components: { 'case-view': { stages } } }, status, readOnly, variant: shape }) => {
338
383
  const stageForeground = calculateForegroundColor(stages.status[status].background, stages.status[status]['foreground-color']);
339
384
  return css `
340
385
  background: ${stages.status[status].background};
@@ -349,26 +394,10 @@ export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area'
349
394
  * Adding 0.0625rem (1px at default font size) to min-height compensates so the button fills its <li> container,
350
395
  * keeping the clipped chevron background flush with the top and bottom borders. */
351
396
  min-height: calc(5 * var(--chevron-spacing) + 0.0625rem);
352
- padding-inline-start: calc(2 * var(--chevron-spacing));
353
- padding-inline-end: var(--chevron-spacing);
354
397
  margin-inline-start: 0;
355
398
  margin-inline-end: 0;
356
- width: calc(100% + ${chevronConfig.width});
357
- --stage-chevron-width: ${chevronConfig.width};
358
-
359
- ${StyledInnerStage} {
360
- transform: translateX(calc(-0.25 * var(--chevron-spacing)));
361
- opacity: ${transparency['transparent-2']};
362
- }
363
- ${stageClipPath(true, true)}
364
-
365
- [dir='rtl'] & ${StyledInnerStage} {
366
- transform: translateX(calc(0.25 * var(--chevron-spacing)));
367
- }
368
399
 
369
- [dir='rtl'] & {
370
- ${stageClipPath(true, true, true)}
371
- }
400
+ ${stageButtonVariants[shape](theme)}
372
401
 
373
402
  ${!readOnly &&
374
403
  css `
@@ -407,9 +436,6 @@ export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area'
407
436
 
408
437
  @media (pointer: coarse) {
409
438
  min-height: ${hitArea['finger-min']};
410
- padding-inline-start: calc(2.25 * var(--chevron-spacing));
411
- width: calc(100% + ${chevronConfig.coarseWidth});
412
- --stage-chevron-width: ${chevronConfig.coarseWidth};
413
439
  }
414
440
  `;
415
441
  });
@@ -431,217 +457,384 @@ export const StyledStageText = styled(Text)(({ theme, isCurrent }) => css `
431
457
  font-weight: ${isCurrent ? theme.base['font-weight'].bold : theme.base['font-weight'].normal};
432
458
  `);
433
459
  StyledStageText.defaultProps = defaultThemeProp;
434
- /**
435
- * `<li>` container for a single stage item. Manages negative-margin chevron overlap,
436
- * pseudo-element positioning, and z-index stacking for interactive states.
437
- */
438
- export const StyledStageContainer = styled.li(({ theme: { base: { palette }, components: { card: { 'border-radius': borderRadius } } }, readOnly }) => {
460
+ const stageContainerChevronStyles = (theme, readOnly) => {
461
+ const includeHover = !readOnly;
439
462
  return css `
440
- display: flex;
441
- position: relative;
442
- margin-inline-start: calc(-1 * ${chevronConfig.width});
463
+ /* Chevron: negative-margin overlap so consecutive stages share a boundary */
464
+ margin-inline-start: calc(-1 * ${chevronConfig.width});
443
465
 
444
- &:nth-of-type(2) {
445
- margin-inline-start: 0;
446
- }
466
+ &:nth-of-type(2) {
467
+ margin-inline-start: 0;
468
+ }
447
469
 
448
- /* Resting z-index: for same-z siblings, DOM order determines paint order.
449
- * All non-last <li>s have z-index:1 so the later (right) <li>'s ::before paints on top
450
- * at internal boundaries; the last <li> has no z-index so its left neighbor's ::after
451
- * wins at the final boundary. On hover/focus/active, the interacting <li> gains z:2/3
452
- * and its own pseudo-element surfaces for that boundary, regardless of which side.
470
+ /* Chevron boundary indicators via ::before (left notch) and ::after (right point) pseudo-elements */
471
+ &:not(:nth-of-type(-n + 2))::before {
472
+ ${createChevronPseudo('calc(2 * var(--chevron-border-thickness))', theme.base.palette['border-line'], chevronConfig.squareSize)}
473
+ /* Elevate ::before above the button so it renders via the same pathway as ::after.
474
+ * Without z-index, ::before (first in DOM) is painted first and then covered by the
475
+ * button, making it only partially visible through the clip-path notch window. This
476
+ * causes thicker interactive-state borders (3×) to look thinner on the left boundary
477
+ * than on the right (::after, which paints last and is always fully visible).
478
+ * The ::before tip's rightmost extent is at --stage-chevron-width from the <li> start,
479
+ * which is before the stage text (padding-inline-start: 2 × spacing), so no content overlap.
453
480
  */
454
- &:not(:nth-last-of-type(-n + 2)) {
455
- z-index: 1;
456
- }
481
+ z-index: 1;
482
+ }
457
483
 
458
- &:has(button:focus-visible),
459
- &:has(button:active) {
460
- z-index: 2;
484
+ &:not(:nth-last-of-type(-n + 2))::after {
485
+ ${createChevronPseudo('calc(2 * var(--chevron-border-thickness))', theme.base.palette['border-line'], chevronConfig.squareSize)}
486
+ }
487
+
488
+ [dir='rtl'] &::before,
489
+ [dir='rtl'] &::after {
490
+ transform: rotateZ(-45deg) skew(-15deg, -15deg);
491
+ }
492
+
493
+ /* Chevron positioning: 45° rotated squares create visual tips via offsets:
494
+ * left ≈ 0.86 × spacing, right ≈ 1.445 × spacing (accounts for skew correction)
495
+ */
496
+ ${createChevronBaseStyles(chevronConfig, 'var(--chevron-border-thickness)')}
497
+ ${createChevronInteractiveStates(chevronConfig, 'var(--chevron-border-thickness)', includeHover)}
498
+
499
+ /* Thicken chevrons to visually match button top/bottom border on any colored state.
500
+ * At 45° rotation, border appears 1/sin(45°) ≈ 0.707× thinner, so 2× border ≈ 1.4× horizontal border weight.
501
+ */
502
+ ${includeHover &&
503
+ css `
504
+ &:has(button:hover)::before,
505
+ &:has(button:hover)::after {
506
+ border-color: ${theme.base.palette['border-line']};
461
507
  }
508
+ `}
462
509
 
463
- ${!readOnly &&
510
+ &:has(button:focus-visible)::before,
511
+ &:has(button:focus-visible)::after {
512
+ border-color: ${theme.base.palette.interactive};
513
+ }
514
+
515
+ &:has(button:active)::before,
516
+ &:has(button:active)::after {
517
+ border-color: ${theme.base.palette.interactive};
518
+ }
519
+
520
+ /* First stage: no left notch, extended width to cover the full left edge */
521
+ &:nth-of-type(2) > ${StyledStage} {
522
+ --stage-chevron-width: ${chevronConfig.width};
523
+ width: calc(100% + (2 * var(--stage-chevron-width)));
524
+ padding-inline-start: calc(var(--chevron-spacing));
525
+ ${stageClipPath(false, true)}
526
+
527
+ ${includeHover &&
464
528
  css `
465
- &:has(button:hover) {
466
- z-index: 3;
529
+ /* Inline-start interactive indicator via inset shadow — avoids border-color bleeding through gradient backgrounds */
530
+ &:hover {
531
+ box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
532
+ ${theme.base.palette['border-line']};
533
+
534
+ [dir='rtl'] & {
535
+ box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
536
+ ${theme.base.palette['border-line']};
537
+ }
467
538
  }
468
539
  `}
469
540
 
470
- @media (pointer: coarse) {
471
- margin-inline-start: calc(-1 * ${chevronConfig.coarseWidth});
472
- }
541
+ &:focus-visible,
542
+ &:active {
543
+ box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
544
+ ${theme.base.palette.interactive};
473
545
 
474
- &:not(:nth-of-type(-n + 2))::before {
475
- ${createChevronPseudo('calc(2 * var(--chevron-border-thickness))', palette['border-line'], chevronConfig.squareSize)}
476
- /* Elevate ::before above the button so it renders via the same pathway as ::after.
477
- * Without z-index, ::before (first in DOM) is painted first and then covered by the
478
- * button, making it only partially visible through the clip-path notch window. This
479
- * causes thicker interactive-state borders (3×) to look thinner on the left boundary
480
- * than on the right (::after, which paints last and is always fully visible).
481
- * The ::before tip's rightmost extent is at --stage-chevron-width from the <li> start,
482
- * which is before the stage text (padding-inline-start: 2 × spacing), so no content overlap.
483
- */
484
- z-index: 1;
546
+ [dir='rtl'] & {
547
+ box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
548
+ ${theme.base.palette.interactive};
549
+ }
485
550
  }
486
551
 
487
- &:not(:nth-last-of-type(-n + 2))::after {
488
- ${createChevronPseudo('calc(2 * var(--chevron-border-thickness))', palette['border-line'], chevronConfig.squareSize)}
552
+ @media (pointer: coarse) {
553
+ --stage-chevron-width: ${chevronConfig.coarseWidth};
554
+ width: calc(100% + (2 * var(--stage-chevron-width)));
489
555
  }
556
+ }
490
557
 
491
- /* Chevron positioning: 45° rotated squares create visual tips via offsets:
492
- * left ≈ 0.86 × spacing, right ≈ 1.445 × spacing (accounts for skew correction)
493
- */
494
-
495
- [dir='rtl'] &::before,
496
- [dir='rtl'] &::after {
497
- transform: rotateZ(-45deg) skew(-15deg, -15deg);
498
- }
558
+ /* Last stage: no right point */
559
+ &:nth-last-of-type(2) > ${StyledStage} {
560
+ --stage-chevron-width: ${chevronConfig.width};
561
+ padding-inline-end: 0;
562
+ width: 100%;
563
+ ${stageClipPath(true, false)}
499
564
 
500
- /* Thicken chevrons to visually match button top/bottom border on any colored state.
501
- * At 45° rotation, border appears 1/sin(45°) ≈ 0.707× thinner, so 2× border ≈ 1.4× horizontal border weight.
502
- */
503
- ${createChevronBaseStyles(chevronConfig, 'var(--chevron-border-thickness)')}
504
- ${createChevronInteractiveStates(chevronConfig, 'var(--chevron-border-thickness)', !readOnly)}
505
- ${!readOnly &&
565
+ ${includeHover &&
506
566
  css `
507
- &:has(button:hover)::before,
508
- &:has(button:hover)::after {
509
- border-color: ${palette['border-line']};
567
+ /* Inline-end interactive indicator via inset shadow — avoids border-color bleeding through gradient backgrounds */
568
+ &:hover {
569
+ box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
570
+ ${theme.base.palette['border-line']};
571
+
572
+ [dir='rtl'] & {
573
+ box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
574
+ ${theme.base.palette['border-line']};
575
+ }
510
576
  }
511
577
  `}
512
- &:has(button:focus-visible)::before,
513
- &:has(button:focus-visible)::after {
514
- border-color: ${palette.interactive};
515
- }
516
578
 
517
- &:has(button:active)::before,
518
- &:has(button:active)::after {
519
- border-color: ${palette.interactive};
579
+ &:focus-visible,
580
+ &:active {
581
+ box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
582
+ ${theme.base.palette.interactive};
583
+
584
+ [dir='rtl'] & {
585
+ box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
586
+ ${theme.base.palette.interactive};
587
+ }
520
588
  }
521
589
 
522
590
  @media (pointer: coarse) {
523
- &:not(:nth-of-type(-n + 2))::before,
524
- &:not(:nth-last-of-type(-n + 2))::after {
525
- inset-block-start: calc(50% - ${chevronConfig.coarseSquareSize} / 2);
526
- width: ${chevronConfig.coarseSquareSize};
527
- height: ${chevronConfig.coarseSquareSize};
528
- }
591
+ --stage-chevron-width: ${chevronConfig.coarseWidth};
592
+ }
593
+ }
529
594
 
530
- &:not(:nth-of-type(-n + 2))::before {
531
- inset-inline-start: calc(
532
- -1 * ${chevronConfig.coarseWidth} - ${chevronConfig.coarseOffsetLeft}
533
- );
534
- }
595
+ /* RTL: flip clip-paths for first and last stages */
596
+ [dir='rtl'] &:nth-of-type(2) > ${StyledStage} {
597
+ ${stageClipPath(true, false, true)}
598
+ }
535
599
 
536
- &:not(:nth-last-of-type(-n + 2))::after {
537
- inset-inline-end: calc(
538
- -1 * ${chevronConfig.coarseWidth} + ${chevronConfig.coarseOffsetRight}
539
- );
540
- }
600
+ [dir='rtl'] &:nth-last-of-type(2) > ${StyledStage} {
601
+ ${stageClipPath(false, true, true)}
602
+ }
603
+
604
+ @media (pointer: coarse) {
605
+ margin-inline-start: calc(-1 * ${chevronConfig.coarseWidth});
606
+
607
+ &:not(:nth-of-type(-n + 2))::before,
608
+ &:not(:nth-last-of-type(-n + 2))::after {
609
+ inset-block-start: calc(50% - ${chevronConfig.coarseSquareSize} / 2);
610
+ width: ${chevronConfig.coarseSquareSize};
611
+ height: ${chevronConfig.coarseSquareSize};
541
612
  }
542
613
 
543
- &:nth-of-type(2):not(:nth-last-of-type(2)) > ${StyledStage} {
544
- --stage-chevron-width: ${chevronConfig.width};
545
- border-start-start-radius: ${borderRadius};
546
- border-end-start-radius: ${borderRadius};
547
- width: calc(100% + (2 * var(--stage-chevron-width)));
548
- padding-inline-start: calc(var(--chevron-spacing));
549
- ${stageClipPath(false, true)}
614
+ &:not(:nth-of-type(-n + 2))::before {
615
+ inset-inline-start: calc(
616
+ -1 * ${chevronConfig.coarseWidth} - ${chevronConfig.coarseOffsetLeft}
617
+ );
618
+ }
550
619
 
551
- ${!readOnly &&
620
+ &:not(:nth-last-of-type(-n + 2))::after {
621
+ inset-inline-end: calc(
622
+ -1 * ${chevronConfig.coarseWidth} + ${chevronConfig.coarseOffsetRight}
623
+ );
624
+ }
625
+ }
626
+ `;
627
+ };
628
+ const stageContainerSegmentedStyles = (theme, readOnly) => {
629
+ const includeHover = !readOnly;
630
+ return css `
631
+ --segmented-edge-thickness: var(--chevron-border-thickness);
632
+
633
+ /* Segmented: stages overlap by --segmented-width so parallelogram edges meet flush */
634
+ margin-inline-start: calc(-1 * var(--segmented-width));
635
+
636
+ &:nth-of-type(2) {
637
+ margin-inline-start: 0;
638
+ }
639
+
640
+ /* Left segmented border strip: diagonal clip-path on each stage's inline-start edge (stages 2..N).
641
+ * The strip goes from (segmented-width, 0%) at top to (0%, 100%) at bottom of the pseudo,
642
+ * matching the parallelogram's inline-start clip edge exactly.
643
+ */
644
+ &:not(:nth-of-type(-n + 2))::before {
645
+ content: '';
646
+ position: absolute;
647
+ inset-block: 0;
648
+ inset-inline-start: 0;
649
+ width: calc(var(--segmented-width) + var(--segmented-edge-thickness));
650
+ clip-path: polygon(
651
+ var(--segmented-width) 0%,
652
+ calc(var(--segmented-width) + var(--segmented-edge-thickness)) 0%,
653
+ var(--segmented-edge-thickness) 100%,
654
+ 0% 100%
655
+ );
656
+ background: ${theme.base.palette['border-line']};
657
+ pointer-events: none;
658
+ z-index: 1;
659
+ }
660
+
661
+ /* Right segmented border strip: diagonal clip-path on each stage's inline-end edge (stages 1..N-1). */
662
+ &:not(:nth-last-of-type(-n + 2))::after {
663
+ content: '';
664
+ position: absolute;
665
+ inset-block: 0;
666
+ inset-inline-end: 0;
667
+ width: calc(var(--segmented-width) + var(--segmented-edge-thickness));
668
+ clip-path: polygon(
669
+ var(--segmented-width) 0%,
670
+ calc(var(--segmented-width) + var(--segmented-edge-thickness)) 0%,
671
+ var(--segmented-edge-thickness) 100%,
672
+ 0% 100%
673
+ );
674
+ background: ${theme.base.palette['border-line']};
675
+ pointer-events: none;
676
+ z-index: 2;
677
+ }
678
+
679
+ /* Interactive states: update pseudo background to signal hover/focus/active */
680
+ ${includeHover &&
552
681
  css `
553
- /* Inline-start interactive indicator via inset shadow — avoids border-color bleeding through gradient backgrounds */
554
- &:hover {
555
- box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
556
- ${palette['border-line']};
682
+ &:has(button:hover) {
683
+ --segmented-edge-thickness: var(--segmented-border-thickness);
684
+ }
557
685
 
558
- [dir='rtl'] & {
559
- box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
560
- ${palette['border-line']};
561
- }
562
- }
563
- `}
686
+ &:has(button:hover)::before,
687
+ &:has(button:hover)::after {
688
+ background: ${theme.base.palette['border-line']};
689
+ }
690
+ `}
691
+
692
+ &:has(button:focus-visible),
693
+ &:has(button:active) {
694
+ --segmented-edge-thickness: var(--segmented-border-thickness);
695
+ }
696
+
697
+ &:has(button:focus-visible)::before,
698
+ &:has(button:focus-visible)::after,
699
+ &:has(button:active)::before,
700
+ &:has(button:active)::after {
701
+ background: ${theme.base.palette.interactive};
702
+ }
564
703
 
565
- &:focus-visible,
566
- &:active {
567
- box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive};
704
+ /* First stage (no inline-start cut): flat left edge + inset shadow edge indicator */
705
+ &:nth-of-type(2) > ${StyledStage} {
706
+ ${segmentedClipPath(false, true)}
707
+
708
+ ${includeHover &&
709
+ css `
710
+ &:hover {
711
+ box-shadow: inset var(--segmented-border-thickness) 0 0
712
+ ${theme.base.palette['border-line']};
568
713
 
569
714
  [dir='rtl'] & {
570
- box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive};
715
+ box-shadow: inset calc(-1 * var(--segmented-border-thickness)) 0 0
716
+ ${theme.base.palette['border-line']};
571
717
  }
572
718
  }
719
+ `}
720
+
721
+ &:focus-visible,
722
+ &:active {
723
+ box-shadow: inset var(--segmented-border-thickness) 0 0 ${theme.base.palette.interactive};
573
724
 
574
- @media (pointer: coarse) {
575
- --stage-chevron-width: ${chevronConfig.coarseWidth};
576
- width: calc(100% + (2 * var(--stage-chevron-width)));
725
+ [dir='rtl'] & {
726
+ box-shadow: inset calc(-1 * var(--segmented-border-thickness)) 0 0
727
+ ${theme.base.palette.interactive};
577
728
  }
578
729
  }
730
+ }
579
731
 
580
- &:nth-last-of-type(2):not(:nth-of-type(2)) > ${StyledStage} {
581
- --stage-chevron-width: ${chevronConfig.width};
582
- border-start-end-radius: ${borderRadius};
583
- border-end-end-radius: ${borderRadius};
584
- padding-inline-end: 0;
585
- width: 100%;
586
- ${stageClipPath(true, false)}
732
+ /* Last stage (no inline-end cut): flat right edge + inset shadow edge indicator */
733
+ &:nth-last-of-type(2) > ${StyledStage} {
734
+ ${segmentedClipPath(true, false)}
587
735
 
588
- ${!readOnly &&
736
+ ${includeHover &&
589
737
  css `
590
- /* Inline-end interactive indicator via inset shadow — avoids border-color bleeding through gradient backgrounds */
591
- &:hover {
592
- box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
593
- ${palette['border-line']};
594
-
595
- [dir='rtl'] & {
596
- box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
597
- ${palette['border-line']};
598
- }
599
- }
600
- `}
601
-
602
- &:focus-visible,
603
- &:active {
604
- box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive};
738
+ &:hover {
739
+ box-shadow: inset calc(-1 * var(--segmented-border-thickness)) 0 0
740
+ ${theme.base.palette['border-line']};
605
741
 
606
742
  [dir='rtl'] & {
607
- box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive};
743
+ box-shadow: inset var(--segmented-border-thickness) 0 0
744
+ ${theme.base.palette['border-line']};
608
745
  }
609
746
  }
747
+ `}
610
748
 
611
- @media (pointer: coarse) {
612
- --stage-chevron-width: ${chevronConfig.coarseWidth};
749
+ &:focus-visible,
750
+ &:active {
751
+ box-shadow: inset calc(-1 * var(--segmented-border-thickness)) 0 0
752
+ ${theme.base.palette.interactive};
753
+
754
+ [dir='rtl'] & {
755
+ box-shadow: inset var(--segmented-border-thickness) 0 0 ${theme.base.palette.interactive};
613
756
  }
614
757
  }
758
+ }
615
759
 
616
- [dir='rtl'] &:nth-of-type(2):not(:nth-last-of-type(2)) > ${StyledStage} {
617
- ${stageClipPath(true, false, true)}
618
- }
760
+ /* RTL: flip clip-paths for first and last stage buttons */
761
+ [dir='rtl'] &:nth-of-type(2) > ${StyledStage} {
762
+ ${segmentedClipPath(true, false, true)}
763
+ }
764
+
765
+ [dir='rtl'] &:nth-last-of-type(2) > ${StyledStage} {
766
+ ${segmentedClipPath(false, true, true)}
767
+ }
768
+
769
+ /* RTL: mirror the pseudo clip-paths.
770
+ * In LTR the strip goes top-right → bottom-left (polygon A 0%, A+B 0%, B 100%, 0% 100%).
771
+ * In RTL the pseudo is positioned at the opposite physical edge via logical inset properties,
772
+ * so the strip must go top-left → bottom-right to stay aligned with the flipped slant.
773
+ */
774
+ [dir='rtl'] &:not(:nth-of-type(-n + 2))::before,
775
+ [dir='rtl'] &:not(:nth-last-of-type(-n + 2))::after {
776
+ clip-path: polygon(
777
+ 0% 0%,
778
+ var(--segmented-edge-thickness) 0%,
779
+ calc(var(--segmented-width) + var(--segmented-edge-thickness)) 100%,
780
+ var(--segmented-width) 100%
781
+ );
782
+ }
783
+
784
+ @media (pointer: coarse) {
785
+ /* A single CSS custom property update propagates to all margin and clip-path consumers */
786
+ --segmented-width: ${segmentedConfig.coarseWidth};
787
+ }
788
+ `;
789
+ };
790
+ const stageContainerVariants = {
791
+ chevron: stageContainerChevronStyles,
792
+ segmented: stageContainerSegmentedStyles,
793
+ milestone: () => css ``
794
+ };
795
+ /**
796
+ * `<li>` container for a single stage item. Manages negative-margin chevron overlap,
797
+ * pseudo-element positioning, and z-index stacking for interactive states.
798
+ */
799
+ export const StyledStageContainer = styled.li(({ theme, theme: { components: { card: { 'border-radius': borderRadius } } }, readOnly, variant: shape }) => {
800
+ return css `
801
+ display: flex;
802
+ position: relative;
619
803
 
620
- [dir='rtl'] &:nth-last-of-type(2):not(:nth-of-type(2)) > ${StyledStage} {
621
- ${stageClipPath(false, true, true)}
804
+ /* Resting z-index: for same-z siblings, DOM order determines paint order.
805
+ * All non-last <li>s have z-index:1 so the later (right) <li>'s ::before paints on top
806
+ * at internal boundaries; the last <li> has no z-index so its left neighbor's ::after
807
+ * wins at the final boundary. On hover/focus/active, the interacting <li> gains z:2/3
808
+ * and its own pseudo-element surfaces for that boundary, regardless of which side.
809
+ */
810
+ &:not(:nth-last-of-type(-n + 2)) {
811
+ z-index: 1;
622
812
  }
623
813
 
624
- &:nth-of-type(2):nth-last-of-type(2) > ${StyledStage} {
625
- border-radius: ${borderRadius};
626
- width: 100%;
627
- ${stageClipPath(false, false)}
814
+ &:has(button:focus-visible),
815
+ &:has(button:active) {
816
+ z-index: 2;
817
+ }
628
818
 
629
- ${!readOnly &&
819
+ ${!readOnly &&
630
820
  css `
631
- &:hover {
632
- box-shadow:
633
- inset calc(2 * var(--chevron-border-thickness)) 0 0 ${palette['border-line']},
634
- inset calc(-2 * var(--chevron-border-thickness)) 0 0 ${palette['border-line']};
635
- }
636
- `}
637
-
638
- &:focus-visible,
639
- &:active {
640
- box-shadow:
641
- inset calc(2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive},
642
- inset calc(-2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive};
821
+ &:has(button:hover) {
822
+ z-index: 3;
643
823
  }
824
+ `}
825
+
826
+ /* Corner radii are shared across all shape variants */
827
+ &:nth-of-type(2) > ${StyledStage} {
828
+ border-start-start-radius: ${borderRadius};
829
+ border-end-start-radius: ${borderRadius};
830
+ }
831
+
832
+ &:nth-last-of-type(2) > ${StyledStage} {
833
+ border-start-end-radius: ${borderRadius};
834
+ border-end-end-radius: ${borderRadius};
644
835
  }
836
+
837
+ ${stageContainerVariants[shape](theme, readOnly)}
645
838
  `;
646
839
  });
647
840
  StyledStageContainer.defaultProps = defaultThemeProp;
@@ -704,6 +897,195 @@ export const StyledStageGlimpse = styled.div(({ theme }) => {
704
897
  `;
705
898
  });
706
899
  StyledStageGlimpse.defaultProps = defaultThemeProp;
900
+ const milestonePulse = keyframes `
901
+ 0%, 100% {
902
+ box-shadow: 0 0 0 var(--milestone-pulse-min) var(--milestone-pulse-start);
903
+ }
904
+ 60% {
905
+ box-shadow: 0 0 0 var(--milestone-pulse-spread) var(--milestone-pulse-end);
906
+ }
907
+ `;
908
+ export const StyledMilestoneLabel = styled.span(({ theme }) => {
909
+ return css `
910
+ color: ${theme.base.palette['foreground-color']};
911
+ line-height: ${theme.base['line-height']};
912
+ text-align: center;
913
+ align-self: stretch;
914
+ overflow-wrap: break-word;
915
+ padding-inline: calc(0.25 * ${theme.base.spacing});
916
+ `;
917
+ });
918
+ StyledMilestoneLabel.defaultProps = defaultThemeProp;
919
+ /**
920
+ * `<li>` container for a single stage item in the 'milestone' shape variant.
921
+ * Renders column-flex layout (milestone above, label below) and draws the
922
+ * horizontal connector as a single line from this milestone's centre to the next
923
+ * milestone's centre (::after), sized so every connector has an identical length.
924
+ */
925
+ export const StyledMilestoneStageContainer = styled.li(({ theme, nextCompleted, status }) => {
926
+ const { base: { palette: { interactive, 'border-line': borderLine }, spacing } } = theme;
927
+ const fontSize = calculateFontSize(theme.base['font-size'], theme.base['font-scale']);
928
+ const connectorFilled = nextCompleted;
929
+ return css `
930
+ --connector-height: ${connectorFilled ? '0.25rem' : '0.125rem'};
931
+ --connector-display: block;
932
+ --connector-start: 50%;
933
+ --connector-width: 100%;
934
+ --connector-bg: ${connectorFilled ? interactive : borderLine};
935
+ --connector-forced-bg: ${connectorFilled ? 'Highlight' : 'CanvasText'};
936
+ display: flex;
937
+ flex-direction: column;
938
+ align-items: center;
939
+ flex: 1 0 0;
940
+ font-size: ${fontSize.xs};
941
+ font-weight: ${status === 'current'
942
+ ? theme.base['font-weight']['semi-bold']
943
+ : theme.base['font-weight'].normal};
944
+ min-width: 15ch;
945
+ position: relative;
946
+ gap: ${spacing};
947
+ z-index: 1;
948
+
949
+ &:nth-of-type(2):not(:nth-last-of-type(2)) {
950
+ --connector-start: calc(var(--milestone-size) / 2);
951
+ --connector-width: calc(200% - var(--milestone-size));
952
+ align-items: flex-start;
953
+ flex: 0.5 0 calc(var(--milestone-size) / 2);
954
+ min-width: calc(7.5ch + var(--milestone-size) / 2);
955
+
956
+ ${StyledMilestoneLabel} {
957
+ text-align: start;
958
+ }
959
+ }
960
+
961
+ &:nth-last-of-type(2):not(:nth-of-type(2)) {
962
+ --connector-display: none;
963
+ align-items: flex-end;
964
+ flex: 0.5 0 calc(var(--milestone-size) / 2);
965
+ min-width: calc(7.5ch + var(--milestone-size) / 2);
966
+
967
+ ${StyledMilestoneLabel} {
968
+ text-align: end;
969
+ }
970
+ }
971
+
972
+ &:nth-of-type(2):nth-last-of-type(2) {
973
+ --connector-display: none;
974
+ align-items: center;
975
+ flex: 1 0 0;
976
+ }
977
+
978
+ &::after {
979
+ content: '';
980
+ display: var(--connector-display);
981
+ position: absolute;
982
+ inset-block-start: calc(var(--milestone-size) / 2 - var(--connector-height) / 2);
983
+ inset-inline-start: var(--connector-start);
984
+ width: var(--connector-width);
985
+ height: var(--connector-height);
986
+ background: var(--connector-bg);
987
+ pointer-events: none;
988
+
989
+ @media (forced-colors: active) {
990
+ background: var(--connector-forced-bg);
991
+ }
992
+ }
993
+ `;
994
+ });
995
+ StyledMilestoneStageContainer.defaultProps = defaultThemeProp;
996
+ export const StyledMilestoneButton = styled.button(({ theme, status, readOnly }) => {
997
+ const { base: { 'hit-area': hitArea, animation: { speed: animationSpeed, timing: { ease: animationTiming } }, shadow: { focus: focusShadow } }, components: { button: { 'border-width': borderWidth }, 'case-view': { stages } } } = theme;
998
+ const stageForeground = calculateForegroundColor(stages.status[status].background, stages.status[status]['foreground-color']);
999
+ const fontSize = calculateFontSize(theme.base['font-size'], theme.base['font-scale']);
1000
+ const completedBorderColor = status === 'completed'
1001
+ ? tryCatch(() => darken(0.12, stages.status.completed.background), () => 'transparent')
1002
+ : '';
1003
+ return css `
1004
+ position: relative;
1005
+ z-index: 1;
1006
+ display: flex;
1007
+ align-items: center;
1008
+ justify-content: center;
1009
+ flex-shrink: 0;
1010
+ border-radius: 50%;
1011
+ width: var(--milestone-size);
1012
+ height: var(--milestone-size);
1013
+ background: ${stages.status[status].background};
1014
+ color: ${stageForeground};
1015
+ font-size: ${fontSize.s};
1016
+ font-weight: ${status === 'current'
1017
+ ? theme.base['font-weight']['semi-bold']
1018
+ : theme.base['font-weight'].normal};
1019
+ outline: none;
1020
+ border: none;
1021
+ cursor: ${readOnly ? 'default' : 'pointer'};
1022
+ transition: box-shadow calc(2 * ${animationSpeed}) ${animationTiming};
1023
+
1024
+ ${status === 'completed' &&
1025
+ css `
1026
+ box-shadow: inset 0 0 0 calc(2 * ${borderWidth}) ${completedBorderColor};
1027
+ `}
1028
+
1029
+ &:focus-visible {
1030
+ box-shadow: ${focusShadow};
1031
+
1032
+ @media (forced-colors: active) {
1033
+ outline: 0.125rem solid Highlight;
1034
+ outline-offset: 0.125rem;
1035
+ }
1036
+ }
1037
+
1038
+ ${!readOnly &&
1039
+ css `
1040
+ &:hover {
1041
+ filter: brightness(0.85);
1042
+ }
1043
+
1044
+ &:active {
1045
+ filter: brightness(0.85);
1046
+ }
1047
+ `}
1048
+
1049
+ ${status === 'current' &&
1050
+ !readOnly &&
1051
+ css `
1052
+ &::before {
1053
+ content: '';
1054
+ position: absolute;
1055
+ inset: 0;
1056
+ border-radius: 50%;
1057
+ pointer-events: none;
1058
+ animation: ${milestonePulse} 2s ease-out infinite;
1059
+
1060
+ @media (prefers-reduced-motion: reduce) {
1061
+ animation: none;
1062
+ }
1063
+ }
1064
+ `}
1065
+
1066
+ /* Expanded hit area */
1067
+ &::after {
1068
+ content: '';
1069
+ position: absolute;
1070
+ width: ${hitArea['mouse-min']};
1071
+ height: ${hitArea['mouse-min']};
1072
+ inset-block-start: calc((${hitArea['mouse-min']} - var(--milestone-size)) / -2);
1073
+ inset-inline-start: calc((${hitArea['mouse-min']} - var(--milestone-size)) / -2);
1074
+
1075
+ @media (pointer: coarse) {
1076
+ width: ${hitArea['finger-min']};
1077
+ height: ${hitArea['finger-min']};
1078
+ inset-block-start: calc((${hitArea['finger-min']} - var(--milestone-size)) / -2);
1079
+ inset-inline-start: calc((${hitArea['finger-min']} - var(--milestone-size)) / -2);
1080
+ }
1081
+ }
1082
+
1083
+ @media (forced-colors: active) {
1084
+ border: 0.125rem solid ButtonText;
1085
+ }
1086
+ `;
1087
+ });
1088
+ StyledMilestoneButton.defaultProps = defaultThemeProp;
707
1089
  /**
708
1090
  * Visually hidden `<p>` providing an accessible description for the stages list,
709
1091
  * announced by screen readers but not displayed on screen.