@pega/cosmos-react-work 10.0.0-build.4.7 → 10.0.0-build.4.8

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 (35) hide show
  1. package/lib/components/CaseView/CaseView.styles.d.ts.map +1 -1
  2. package/lib/components/CaseView/CaseView.styles.js +2 -0
  3. package/lib/components/CaseView/CaseView.styles.js.map +1 -1
  4. package/lib/components/Details/DetailsList.d.ts.map +1 -1
  5. package/lib/components/Details/DetailsList.js +2 -2
  6. package/lib/components/Details/DetailsList.js.map +1 -1
  7. package/lib/components/GenAICoach/ActiveCases.d.ts.map +1 -1
  8. package/lib/components/GenAICoach/ActiveCases.js +2 -1
  9. package/lib/components/GenAICoach/ActiveCases.js.map +1 -1
  10. package/lib/components/Shortcuts/Shortcuts.d.ts.map +1 -1
  11. package/lib/components/Shortcuts/Shortcuts.js +13 -6
  12. package/lib/components/Shortcuts/Shortcuts.js.map +1 -1
  13. package/lib/components/Shortcuts/Shortcuts.styles.d.ts +13 -2
  14. package/lib/components/Shortcuts/Shortcuts.styles.d.ts.map +1 -1
  15. package/lib/components/Shortcuts/Shortcuts.styles.js +71 -29
  16. package/lib/components/Shortcuts/Shortcuts.styles.js.map +1 -1
  17. package/lib/components/Shortcuts/Shortcuts.types.d.ts +4 -0
  18. package/lib/components/Shortcuts/Shortcuts.types.d.ts.map +1 -1
  19. package/lib/components/Shortcuts/Shortcuts.types.js.map +1 -1
  20. package/lib/components/Stages/Stages.d.ts.map +1 -1
  21. package/lib/components/Stages/Stages.js +55 -36
  22. package/lib/components/Stages/Stages.js.map +1 -1
  23. package/lib/components/Stages/Stages.styles.d.ts +25 -3
  24. package/lib/components/Stages/Stages.styles.d.ts.map +1 -1
  25. package/lib/components/Stages/Stages.styles.js +597 -162
  26. package/lib/components/Stages/Stages.styles.js.map +1 -1
  27. package/lib/components/Stages/Stages.types.d.ts +6 -0
  28. package/lib/components/Stages/Stages.types.d.ts.map +1 -1
  29. package/lib/components/Stages/Stages.types.js.map +1 -1
  30. package/lib/components/Stages/index.d.ts +1 -1
  31. package/lib/components/Stages/index.d.ts.map +1 -1
  32. package/lib/components/Stages/index.js.map +1 -1
  33. package/lib/components/Tasks/TaskList.js +1 -1
  34. package/lib/components/Tasks/TaskList.js.map +1 -1
  35. 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 = {
@@ -195,13 +209,17 @@ export const StyledStagesWrapper = styled.div(({ theme }) => {
195
209
  });
196
210
  StyledStagesWrapper.defaultProps = defaultThemeProp;
197
211
  /**
198
- * Scrollable `<ol>` containing all stage chevron items. Sets the `--chevron-spacing`
199
- * 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.
200
215
  */
201
- export const StyledStages = styled.ol(({ theme }) => {
216
+ export const StyledStages = styled.ol(({ theme, variant }) => {
217
+ const isMilestone = variant === 'milestone';
202
218
  return css `
203
219
  --chevron-spacing: ${theme.base.spacing};
204
220
  --chevron-border-thickness: ${theme.components.button['border-width']};
221
+ --segmented-border-thickness: calc(2 * ${theme.components.button['border-width']});
222
+ --segmented-width: ${segmentedConfig.width};
205
223
  border-radius: ${theme.components.card['border-radius']};
206
224
  display: flex;
207
225
  padding: 0;
@@ -212,6 +230,17 @@ export const StyledStages = styled.ol(({ theme }) => {
212
230
  scrollbar-width: thin;
213
231
  scrollbar-color: transparent transparent;
214
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
+
215
244
  &:hover,
216
245
  &:focus-within {
217
246
  scrollbar-color: var(
@@ -299,11 +328,58 @@ export const StyledInnerStage = styled.span(({ theme, ellipsis }) => {
299
328
  `;
300
329
  });
301
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
+ };
302
378
  /**
303
379
  * Clickable `<button>` for a single stage. Renders the chevron clip-path shape and
304
380
  * applies status-based background and foreground colours.
305
381
  */
306
- 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 }) => {
307
383
  const stageForeground = calculateForegroundColor(stages.status[status].background, stages.status[status]['foreground-color']);
308
384
  return css `
309
385
  background: ${stages.status[status].background};
@@ -318,26 +394,10 @@ export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area'
318
394
  * Adding 0.0625rem (1px at default font size) to min-height compensates so the button fills its <li> container,
319
395
  * keeping the clipped chevron background flush with the top and bottom borders. */
320
396
  min-height: calc(5 * var(--chevron-spacing) + 0.0625rem);
321
- padding-inline-start: calc(2 * var(--chevron-spacing));
322
- padding-inline-end: var(--chevron-spacing);
323
397
  margin-inline-start: 0;
324
398
  margin-inline-end: 0;
325
- width: calc(100% + ${chevronConfig.width});
326
- --stage-chevron-width: ${chevronConfig.width};
327
399
 
328
- ${StyledInnerStage} {
329
- transform: translateX(calc(-0.25 * var(--chevron-spacing)));
330
- opacity: ${transparency['transparent-2']};
331
- }
332
- ${stageClipPath(true, true)}
333
-
334
- [dir='rtl'] & ${StyledInnerStage} {
335
- transform: translateX(calc(0.25 * var(--chevron-spacing)));
336
- }
337
-
338
- [dir='rtl'] & {
339
- ${stageClipPath(true, true, true)}
340
- }
400
+ ${stageButtonVariants[shape](theme)}
341
401
 
342
402
  ${!readOnly &&
343
403
  css `
@@ -376,9 +436,6 @@ export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area'
376
436
 
377
437
  @media (pointer: coarse) {
378
438
  min-height: ${hitArea['finger-min']};
379
- padding-inline-start: calc(2.25 * var(--chevron-spacing));
380
- width: calc(100% + ${chevronConfig.coarseWidth});
381
- --stage-chevron-width: ${chevronConfig.coarseWidth};
382
439
  }
383
440
  `;
384
441
  });
@@ -400,195 +457,384 @@ export const StyledStageText = styled(Text)(({ theme, isCurrent }) => css `
400
457
  font-weight: ${isCurrent ? theme.base['font-weight'].bold : theme.base['font-weight'].normal};
401
458
  `);
402
459
  StyledStageText.defaultProps = defaultThemeProp;
403
- /**
404
- * `<li>` container for a single stage item. Manages negative-margin chevron overlap,
405
- * pseudo-element positioning, and z-index stacking for interactive states.
406
- */
407
- export const StyledStageContainer = styled.li(({ theme: { base: { palette }, components: { card: { 'border-radius': borderRadius } } }, readOnly }) => {
460
+ const stageContainerChevronStyles = (theme, readOnly) => {
461
+ const includeHover = !readOnly;
408
462
  return css `
409
- display: flex;
410
- position: relative;
411
- 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});
412
465
 
413
- &:nth-of-type(2) {
414
- margin-inline-start: 0;
415
- }
466
+ &:nth-of-type(2) {
467
+ margin-inline-start: 0;
468
+ }
416
469
 
417
- /* Resting z-index: for same-z siblings, DOM order determines paint order.
418
- * All non-last <li>s have z-index:1 so the later (right) <li>'s ::before paints on top
419
- * at internal boundaries; the last <li> has no z-index so its left neighbor's ::after
420
- * wins at the final boundary. On hover/focus/active, the interacting <li> gains z:2/3
421
- * 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.
422
480
  */
423
- &:not(:nth-last-of-type(-n + 2)) {
424
- z-index: 1;
481
+ z-index: 1;
482
+ }
483
+
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']};
425
507
  }
508
+ `}
426
509
 
427
- &:has(button:focus-visible),
428
- &:has(button:active) {
429
- z-index: 2;
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 &&
528
+ css `
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
+ }
538
+ }
539
+ `}
540
+
541
+ &:focus-visible,
542
+ &:active {
543
+ box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
544
+ ${theme.base.palette.interactive};
545
+
546
+ [dir='rtl'] & {
547
+ box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
548
+ ${theme.base.palette.interactive};
549
+ }
430
550
  }
431
551
 
432
- ${!readOnly &&
552
+ @media (pointer: coarse) {
553
+ --stage-chevron-width: ${chevronConfig.coarseWidth};
554
+ width: calc(100% + (2 * var(--stage-chevron-width)));
555
+ }
556
+ }
557
+
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)}
564
+
565
+ ${includeHover &&
433
566
  css `
434
- &:has(button:hover) {
435
- z-index: 3;
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
+ }
436
576
  }
437
577
  `}
438
578
 
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
+ }
588
+ }
589
+
439
590
  @media (pointer: coarse) {
440
- margin-inline-start: calc(-1 * ${chevronConfig.coarseWidth});
591
+ --stage-chevron-width: ${chevronConfig.coarseWidth};
592
+ }
593
+ }
594
+
595
+ /* RTL: flip clip-paths for first and last stages */
596
+ [dir='rtl'] &:nth-of-type(2) > ${StyledStage} {
597
+ ${stageClipPath(true, false, true)}
598
+ }
599
+
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};
441
612
  }
442
613
 
443
614
  &:not(:nth-of-type(-n + 2))::before {
444
- ${createChevronPseudo('calc(2 * var(--chevron-border-thickness))', palette['border-line'], chevronConfig.squareSize)}
445
- /* Elevate ::before above the button so it renders via the same pathway as ::after.
446
- * Without z-index, ::before (first in DOM) is painted first and then covered by the
447
- * button, making it only partially visible through the clip-path notch window. This
448
- * causes thicker interactive-state borders (3×) to look thinner on the left boundary
449
- * than on the right (::after, which paints last and is always fully visible).
450
- * The ::before tip's rightmost extent is at --stage-chevron-width from the <li> start,
451
- * which is before the stage text (padding-inline-start: 2 × spacing), so no content overlap.
452
- */
453
- z-index: 1;
615
+ inset-inline-start: calc(
616
+ -1 * ${chevronConfig.coarseWidth} - ${chevronConfig.coarseOffsetLeft}
617
+ );
454
618
  }
455
619
 
456
620
  &:not(:nth-last-of-type(-n + 2))::after {
457
- ${createChevronPseudo('calc(2 * var(--chevron-border-thickness))', palette['border-line'], chevronConfig.squareSize)}
621
+ inset-inline-end: calc(
622
+ -1 * ${chevronConfig.coarseWidth} + ${chevronConfig.coarseOffsetRight}
623
+ );
458
624
  }
625
+ }
626
+ `;
627
+ };
628
+ const stageContainerSegmentedStyles = (theme, readOnly) => {
629
+ const includeHover = !readOnly;
630
+ return css `
631
+ --segmented-edge-thickness: var(--chevron-border-thickness);
459
632
 
460
- /* Chevron positioning: 45° rotated squares create visual tips via offsets:
461
- * left ≈ 0.86 × spacing, right ≈ 1.445 × spacing (accounts for skew correction)
462
- */
633
+ /* Segmented: stages overlap by --segmented-width so parallelogram edges meet flush */
634
+ margin-inline-start: calc(-1 * var(--segmented-width));
463
635
 
464
- [dir='rtl'] &::before,
465
- [dir='rtl'] &::after {
466
- transform: rotateZ(-45deg) skew(-15deg, -15deg);
467
- }
636
+ &:nth-of-type(2) {
637
+ margin-inline-start: 0;
638
+ }
468
639
 
469
- /* Thicken chevrons to visually match button top/bottom border on any colored state.
470
- * At 45° rotation, border appears 1/sin(45°) 0.707× thinner, so border 1.4× horizontal border weight.
471
- */
472
- ${createChevronBaseStyles(chevronConfig, 'var(--chevron-border-thickness)')}
473
- ${createChevronInteractiveStates(chevronConfig, 'var(--chevron-border-thickness)', !readOnly)}
474
- ${!readOnly &&
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 &&
475
681
  css `
476
- &:has(button:hover)::before,
477
- &:has(button:hover)::after {
478
- border-color: ${palette['border-line']};
479
- }
480
- `}
481
- &:has(button:focus-visible)::before,
482
- &:has(button:focus-visible)::after {
483
- border-color: ${palette.interactive};
682
+ &:has(button:hover) {
683
+ --segmented-edge-thickness: var(--segmented-border-thickness);
484
684
  }
485
685
 
486
- &:has(button:active)::before,
487
- &:has(button:active)::after {
488
- border-color: ${palette.interactive};
686
+ &:has(button:hover)::before,
687
+ &:has(button:hover)::after {
688
+ background: ${theme.base.palette['border-line']};
489
689
  }
690
+ `}
490
691
 
491
- @media (pointer: coarse) {
492
- &:not(:nth-of-type(-n + 2))::before,
493
- &:not(:nth-last-of-type(-n + 2))::after {
494
- inset-block-start: calc(50% - ${chevronConfig.coarseSquareSize} / 2);
495
- width: ${chevronConfig.coarseSquareSize};
496
- height: ${chevronConfig.coarseSquareSize};
497
- }
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
+ }
703
+
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']};
498
713
 
499
- &:not(:nth-of-type(-n + 2))::before {
500
- inset-inline-start: calc(
501
- -1 * ${chevronConfig.coarseWidth} - ${chevronConfig.coarseOffsetLeft}
502
- );
714
+ [dir='rtl'] & {
715
+ box-shadow: inset calc(-1 * var(--segmented-border-thickness)) 0 0
716
+ ${theme.base.palette['border-line']};
717
+ }
503
718
  }
719
+ `}
720
+
721
+ &:focus-visible,
722
+ &:active {
723
+ box-shadow: inset var(--segmented-border-thickness) 0 0 ${theme.base.palette.interactive};
504
724
 
505
- &:not(:nth-last-of-type(-n + 2))::after {
506
- inset-inline-end: calc(
507
- -1 * ${chevronConfig.coarseWidth} + ${chevronConfig.coarseOffsetRight}
508
- );
725
+ [dir='rtl'] & {
726
+ box-shadow: inset calc(-1 * var(--segmented-border-thickness)) 0 0
727
+ ${theme.base.palette.interactive};
509
728
  }
510
729
  }
730
+ }
511
731
 
512
- &:nth-of-type(2) > ${StyledStage} {
513
- --stage-chevron-width: ${chevronConfig.width};
514
- border-start-start-radius: ${borderRadius};
515
- border-end-start-radius: ${borderRadius};
516
- width: calc(100% + (2 * var(--stage-chevron-width)));
517
- padding-inline-start: calc(var(--chevron-spacing));
518
- ${stageClipPath(false, true)}
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)}
519
735
 
520
- ${!readOnly &&
736
+ ${includeHover &&
521
737
  css `
522
- /* Inline-start interactive indicator via inset shadow — avoids border-color bleeding through gradient backgrounds */
523
- &:hover {
524
- box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
525
- ${palette['border-line']};
526
-
527
- [dir='rtl'] & {
528
- box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
529
- ${palette['border-line']};
530
- }
531
- }
532
- `}
533
-
534
- &:focus-visible,
535
- &:active {
536
- 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']};
537
741
 
538
742
  [dir='rtl'] & {
539
- 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']};
540
745
  }
541
746
  }
747
+ `}
748
+
749
+ &:focus-visible,
750
+ &:active {
751
+ box-shadow: inset calc(-1 * var(--segmented-border-thickness)) 0 0
752
+ ${theme.base.palette.interactive};
542
753
 
543
- @media (pointer: coarse) {
544
- --stage-chevron-width: ${chevronConfig.coarseWidth};
545
- width: calc(100% + (2 * var(--stage-chevron-width)));
754
+ [dir='rtl'] & {
755
+ box-shadow: inset var(--segmented-border-thickness) 0 0 ${theme.base.palette.interactive};
546
756
  }
547
757
  }
758
+ }
548
759
 
549
- &:nth-last-of-type(2) > ${StyledStage} {
550
- --stage-chevron-width: ${chevronConfig.width};
551
- border-start-end-radius: ${borderRadius};
552
- border-end-end-radius: ${borderRadius};
553
- padding-inline-end: 0;
554
- width: 100%;
555
- ${stageClipPath(true, false)}
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
+ }
556
764
 
557
- ${!readOnly &&
558
- css `
559
- /* Inline-end interactive indicator via inset shadow — avoids border-color bleeding through gradient backgrounds */
560
- &:hover {
561
- box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0
562
- ${palette['border-line']};
765
+ [dir='rtl'] &:nth-last-of-type(2) > ${StyledStage} {
766
+ ${segmentedClipPath(false, true, true)}
767
+ }
563
768
 
564
- [dir='rtl'] & {
565
- box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0
566
- ${palette['border-line']};
567
- }
568
- }
569
- `}
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
+ }
570
783
 
571
- &:focus-visible,
572
- &:active {
573
- box-shadow: inset calc(-2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive};
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;
574
803
 
575
- [dir='rtl'] & {
576
- box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive};
577
- }
578
- }
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;
812
+ }
579
813
 
580
- @media (pointer: coarse) {
581
- --stage-chevron-width: ${chevronConfig.coarseWidth};
582
- }
814
+ &:has(button:focus-visible),
815
+ &:has(button:active) {
816
+ z-index: 2;
583
817
  }
584
818
 
585
- [dir='rtl'] &:nth-of-type(2) > ${StyledStage} {
586
- ${stageClipPath(true, false, true)}
819
+ ${!readOnly &&
820
+ css `
821
+ &:has(button:hover) {
822
+ z-index: 3;
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};
587
830
  }
588
831
 
589
- [dir='rtl'] &:nth-last-of-type(2) > ${StyledStage} {
590
- ${stageClipPath(false, true, true)}
832
+ &:nth-last-of-type(2) > ${StyledStage} {
833
+ border-start-end-radius: ${borderRadius};
834
+ border-end-end-radius: ${borderRadius};
591
835
  }
836
+
837
+ ${stageContainerVariants[shape](theme, readOnly)}
592
838
  `;
593
839
  });
594
840
  StyledStageContainer.defaultProps = defaultThemeProp;
@@ -651,6 +897,195 @@ export const StyledStageGlimpse = styled.div(({ theme }) => {
651
897
  `;
652
898
  });
653
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;
654
1089
  /**
655
1090
  * Visually hidden `<p>` providing an accessible description for the stages list,
656
1091
  * announced by screen readers but not displayed on screen.