@pega/cosmos-react-work 9.10.1 → 9.12.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 (40) hide show
  1. package/lib/components/CaseView/CaseView.styles.d.ts.map +1 -1
  2. package/lib/components/CaseView/CaseView.styles.js +7 -3
  3. package/lib/components/CaseView/CaseView.styles.js.map +1 -1
  4. package/lib/components/Shortcuts/Shortcuts.d.ts.map +1 -1
  5. package/lib/components/Shortcuts/Shortcuts.js +13 -6
  6. package/lib/components/Shortcuts/Shortcuts.js.map +1 -1
  7. package/lib/components/Shortcuts/Shortcuts.styles.d.ts +13 -2
  8. package/lib/components/Shortcuts/Shortcuts.styles.d.ts.map +1 -1
  9. package/lib/components/Shortcuts/Shortcuts.styles.js +71 -29
  10. package/lib/components/Shortcuts/Shortcuts.styles.js.map +1 -1
  11. package/lib/components/Shortcuts/Shortcuts.types.d.ts +4 -0
  12. package/lib/components/Shortcuts/Shortcuts.types.d.ts.map +1 -1
  13. package/lib/components/Shortcuts/Shortcuts.types.js.map +1 -1
  14. package/lib/components/Stages/Stages.d.ts +4 -2
  15. package/lib/components/Stages/Stages.d.ts.map +1 -1
  16. package/lib/components/Stages/Stages.js +100 -88
  17. package/lib/components/Stages/Stages.js.map +1 -1
  18. package/lib/components/Stages/Stages.styles.d.ts +57 -0
  19. package/lib/components/Stages/Stages.styles.d.ts.map +1 -1
  20. package/lib/components/Stages/Stages.styles.js +326 -53
  21. package/lib/components/Stages/Stages.styles.js.map +1 -1
  22. package/lib/components/Stages/Stages.test-ids.d.ts +2 -0
  23. package/lib/components/Stages/Stages.test-ids.d.ts.map +1 -0
  24. package/lib/components/Stages/Stages.test-ids.js +3 -0
  25. package/lib/components/Stages/Stages.test-ids.js.map +1 -0
  26. package/lib/components/Stages/Stages.types.d.ts +3 -3
  27. package/lib/components/Stages/Stages.types.d.ts.map +1 -1
  28. package/lib/components/Stages/Stages.types.js.map +1 -1
  29. package/lib/components/Stages/index.d.ts +1 -0
  30. package/lib/components/Stages/index.d.ts.map +1 -1
  31. package/lib/components/Stages/index.js +1 -0
  32. package/lib/components/Stages/index.js.map +1 -1
  33. package/lib/components/Tasks/TaskList.d.ts.map +1 -1
  34. package/lib/components/Tasks/TaskList.js +24 -16
  35. package/lib/components/Tasks/TaskList.js.map +1 -1
  36. package/lib/components/Tasks/Tasks.d.ts +5 -0
  37. package/lib/components/Tasks/Tasks.d.ts.map +1 -1
  38. package/lib/components/Tasks/Tasks.js +7 -5
  39. package/lib/components/Tasks/Tasks.js.map +1 -1
  40. package/package.json +3 -3
@@ -1,4 +1,5 @@
1
1
  import styled, { css } from 'styled-components';
2
+ import { transparentize } from 'polished';
2
3
  import { calculateFontSize, DateTimeDisplay, defaultThemeProp, Icon, Text, StyledIcon, calculateForegroundColor } from '@pega/cosmos-react-core';
3
4
  const stageClipPath = (leftNotch, rightPoint, mirrored = false) => {
4
5
  if (mirrored) {
@@ -32,14 +33,25 @@ const stageClipPath = (leftNotch, rightPoint, mirrored = false) => {
32
33
  // === Chevron Configuration ===
33
34
  // Centralizes spacing and border thickness via CSS custom properties set at StyledStages
34
35
  const chevronConfig = {
35
- width: 'calc(1.156 * var(--chevron-spacing))',
36
- coarseWidth: 'calc(1.4375 * var(--chevron-spacing))',
37
- squareSize: 'calc(3 * var(--chevron-spacing))',
38
- coarseSquareSize: 'calc(3.5 * var(--chevron-spacing))',
39
- offsetLeft: 'calc(0.688 * var(--chevron-spacing))',
40
- offsetRight: 'calc(1.156 * var(--chevron-spacing))',
41
- coarseOffsetLeft: 'calc(0.688 * var(--chevron-spacing))',
42
- coarseOffsetRight: 'calc(1.5 * var(--chevron-spacing))'
36
+ // Horizontal projection of the rotated border-square forming the chevron point (fine pointer).
37
+ // Derived from the border-square size × cos(45° + skew). Tuned to visually align with the stage height.
38
+ // All values scaled by 1.25× relative to the original 4× spacing design (now spacing height).
39
+ width: 'calc(1.445 * var(--chevron-spacing))',
40
+ // Same horizontal projection for coarse/touch pointer (larger square, proportionally wider point).
41
+ coarseWidth: 'calc(1.797 * var(--chevron-spacing))',
42
+ // Side length of the rotated border-square forming the chevron (fine pointer: 3.75× base spacing).
43
+ squareSize: 'calc(3.75 * var(--chevron-spacing))',
44
+ // Side length of the rotated border-square for coarse/touch pointer (5× base spacing).
45
+ coarseSquareSize: 'calc(5 * var(--chevron-spacing))',
46
+ // Distance to inset the ::before notch from the left edge of the stage (fine pointer).
47
+ // 0.86 ≈ width − half-border-overlap, keeps the notch flush with the preceding chevron point.
48
+ offsetLeft: 'calc(0.86 * var(--chevron-spacing))',
49
+ // Distance to inset the ::after point from the right edge of the stage (fine pointer).
50
+ // Equal to `width` so the point extends exactly one chevron-width beyond the stage boundary.
51
+ offsetRight: 'calc(1.445 * var(--chevron-spacing))',
52
+ // Coarse-pointer equivalents of offsetLeft/offsetRight, scaled to match coarseWidth/coarseSquareSize.
53
+ coarseOffsetLeft: 'calc(1.2875 * var(--chevron-spacing))',
54
+ coarseOffsetRight: 'calc(1.797 * var(--chevron-spacing))'
43
55
  };
44
56
  // === Chevron Helpers ===
45
57
  // After team discussion we decided to use logical properties throughout — `inset-block-start`
@@ -62,17 +74,17 @@ const createChevronPseudo = (borderWidth, borderColor, size) => `
62
74
  pointer-events: none;
63
75
  `;
64
76
  const createChevronBaseStyles = (config, borderThicknessVar) => css `
65
- &:not(:first-of-type)::before,
66
- &:not(:last-of-type)::after {
77
+ &:not(:nth-of-type(-n + 2))::before,
78
+ &:not(:nth-last-of-type(-n + 2))::after {
67
79
  border-inline-end-width: calc(1.5 * ${borderThicknessVar});
68
80
  border-block-start-width: calc(1.5 * ${borderThicknessVar});
69
81
  }
70
82
 
71
- &:not(:first-of-type)::before {
83
+ &:not(:nth-of-type(-n + 2))::before {
72
84
  inset-inline-start: calc(-1 * ${config.width} - ${config.offsetLeft});
73
85
  }
74
86
 
75
- &:not(:last-of-type)::after {
87
+ &:not(:nth-last-of-type(-n + 2))::after {
76
88
  inset-inline-end: calc(-1 * ${config.width} + ${config.offsetRight});
77
89
  }
78
90
  `;
@@ -93,30 +105,224 @@ const createChevronInteractiveStates = (config, borderThicknessVar, includeHover
93
105
  border-block-start-width: calc(2 * ${borderThicknessVar});
94
106
  }
95
107
  `;
108
+ /**
109
+ * Outer wrapper `<div>` for the stages bar. Applies layered scroll-fade masks and
110
+ * reserves scrollbar space so the fade gradient does not overlap the scrollbar track.
111
+ */
112
+ export const StyledStagesWrapper = styled.div(({ theme }) => {
113
+ return css `
114
+ --stages-fade-width: calc(${theme.base.spacing} * 2);
115
+ --stages-fade-start: 0;
116
+ --stages-fade-end: 0;
117
+ --stages-scrollbar-reserve: calc(${theme.base.spacing} * 0.25);
118
+
119
+ /* Places gradient stops one physical pixel outside the element boundary,
120
+ * preventing browser anti-aliasing from creating a semi-transparent seam
121
+ * at mask edges. 1px is the correct unit here: it must be exactly one CSS pixel
122
+ * (independent of font-size), so the mask boundary doesn't introduce a seam at the
123
+ * element edge due to anti-aliasing.
124
+ * See also StyledSentinel, which uses 1px for the same reason.
125
+ */
126
+ /* stylelint-disable-next-line unit-allowed-list -- 1px is intentional; see comment above */
127
+ --mask-edge-overhang: 1px;
128
+ position: relative;
129
+ width: 100%;
130
+ overflow: hidden;
131
+ border-radius: ${theme.components.card['border-radius']};
132
+ border-end-start-radius: var(
133
+ --stages-wrapper-bottom-radius,
134
+ ${theme.components.card['border-radius']}
135
+ );
136
+ border-end-end-radius: var(
137
+ --stages-wrapper-bottom-radius,
138
+ ${theme.components.card['border-radius']}
139
+ );
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
+ mask-image: linear-gradient(black, black),
149
+ linear-gradient(
150
+ to right,
151
+ transparent calc(-1 * var(--mask-edge-overhang)),
152
+ black var(--stages-fade-start),
153
+ black calc(100% - var(--stages-fade-end)),
154
+ transparent calc(100% + var(--mask-edge-overhang))
155
+ );
156
+ mask-size:
157
+ 100% var(--stages-scrollbar-reserve),
158
+ 100% calc(100% - var(--stages-scrollbar-reserve));
159
+ mask-position:
160
+ 0 100%,
161
+ 0 0;
162
+ mask-repeat: no-repeat, no-repeat;
163
+
164
+ [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
+ mask-image: linear-gradient(black, black),
174
+ linear-gradient(
175
+ to right,
176
+ transparent calc(-1 * var(--mask-edge-overhang)),
177
+ black var(--stages-fade-end),
178
+ black calc(100% - var(--stages-fade-start)),
179
+ transparent calc(100% + var(--mask-edge-overhang))
180
+ );
181
+ }
182
+
183
+ &:has(:focus-visible) {
184
+ box-shadow: ${theme.base.shadow['focus-group']};
185
+ }
186
+
187
+ @media (pointer: coarse) {
188
+ --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
+ mask-image: linear-gradient(
197
+ to right,
198
+ transparent calc(-1 * var(--mask-edge-overhang)),
199
+ black var(--stages-fade-start),
200
+ black calc(100% - var(--stages-fade-end)),
201
+ transparent calc(100% + var(--mask-edge-overhang))
202
+ );
203
+ mask-size: 100% 100%;
204
+ mask-position: 0 0;
205
+ mask-repeat: no-repeat;
206
+
207
+ [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
+ mask-image: linear-gradient(
216
+ to right,
217
+ transparent calc(-1 * var(--mask-edge-overhang)),
218
+ black var(--stages-fade-end),
219
+ black calc(100% - var(--stages-fade-start)),
220
+ transparent calc(100% + var(--mask-edge-overhang))
221
+ );
222
+ }
223
+ }
224
+ `;
225
+ });
226
+ StyledStagesWrapper.defaultProps = defaultThemeProp;
227
+ /**
228
+ * Scrollable `<ol>` containing all stage chevron items. Sets the `--chevron-spacing`
229
+ * and `--chevron-border-thickness` custom properties consumed by child elements.
230
+ */
96
231
  export const StyledStages = styled.ol(({ theme }) => {
97
232
  return css `
98
233
  --chevron-spacing: ${theme.base.spacing};
99
234
  --chevron-border-thickness: ${theme.components.button['border-width']};
100
- background-color: ${theme.base.palette['primary-background']};
235
+ background: ${theme.components.card.background};
101
236
  border-radius: ${theme.components.card['border-radius']};
102
237
  display: flex;
103
238
  padding: 0;
104
239
  width: 100%;
105
- overflow: hidden;
240
+ overflow-x: auto;
241
+ overflow-y: hidden;
106
242
  list-style: none;
243
+ scrollbar-width: thin;
244
+ scrollbar-color: transparent transparent;
245
+
246
+ &:hover,
247
+ &:focus-within {
248
+ scrollbar-color: var(
249
+ --stages-scrollbar-thumb,
250
+ ${transparentize(0.4, theme.base.palette['border-line'])}
251
+ )
252
+ var(--stages-scrollbar-track, transparent);
253
+ }
107
254
 
108
- &:has(:focus-visible) {
109
- box-shadow: ${theme.base.shadow['focus-group']};
255
+ &::-webkit-scrollbar {
256
+ height: 0.25rem;
257
+ }
258
+
259
+ @media (pointer: coarse) {
260
+ scrollbar-width: none;
261
+
262
+ &::-webkit-scrollbar {
263
+ display: none;
264
+ }
110
265
  }
111
266
  `;
112
267
  });
113
268
  StyledStages.defaultProps = defaultThemeProp;
114
- export const StyledInnerStage = styled.span(props => {
269
+ /**
270
+ * A sentinel is an invisible 1 px <li> placed at each end of the scrollable stage list.
271
+ * It is observed by an IntersectionObserver; when it scrolls out of view, the component sets
272
+ * --stages-fade-start or --stages-fade-end to show a gradient fade indicating more stages
273
+ * are reachable by scrolling. When it comes back into view the property is reset to 0.
274
+ *
275
+ * Sentinel elements carry aria-hidden="true" so they are valid list items semantically.
276
+ * Because styled-components scopes all CSS to a generated class, the chevron and layout rules
277
+ * in StyledStageContainer never apply to these elements — they carry a different class.
278
+ * CSS type-based selectors (:nth-of-type, :nth-last-of-type) in StyledStageContainer are
279
+ * offset by 1 at each end to skip the sentinels and target only real stage items.
280
+ */
281
+ export const StyledSentinel = styled.li `
282
+ flex-shrink: 0;
283
+
284
+ /* NOTE: the sentinel must occupy
285
+ * a single physical pixel so IntersectionObserver can detect it at threshold 0 while having
286
+ * negligible layout impact. A rem value could collapse to zero at small font sizes.
287
+ */
288
+ /* stylelint-disable-next-line unit-allowed-list -- 1px is intentional */
289
+ width: 1px;
290
+ /* stylelint-disable-next-line unit-allowed-list -- 1px is intentional */
291
+ height: 1px;
292
+ padding: 0;
293
+ margin: 0;
294
+ pointer-events: none;
295
+
296
+ /* Collapse the sentinel's layout footprint to zero on the side facing the stages.
297
+ * Without this, each 1px sentinel creates a visible gap between the stage buttons
298
+ * and the left/right edges of the wrapper. The sentinel remains in flow (required
299
+ * for IntersectionObserver scroll detection) but no longer displaces stage content.
300
+ */
301
+ &:first-of-type {
302
+ /* stylelint-disable-next-line unit-allowed-list -- 1px matches sentinel width */
303
+ margin-inline-end: -1px;
304
+ }
305
+
306
+ &:last-of-type {
307
+ /* stylelint-disable-next-line unit-allowed-list -- 1px matches sentinel width */
308
+ margin-inline-start: -1px;
309
+ }
310
+ `;
311
+ StyledSentinel.defaultProps = defaultThemeProp;
312
+ /**
313
+ * Inner `<span>` holding stage label content. Optionally applies `text-overflow: ellipsis`
314
+ * for stages with long labels.
315
+ */
316
+ export const StyledInnerStage = styled.span(({ theme, ellipsis }) => {
115
317
  return css `
116
- display: inline-block;
318
+ display: inline-flex;
319
+ align-items: center;
320
+ gap: calc(0.25 * ${theme.base.spacing});
117
321
  text-align: center;
118
322
  white-space: nowrap;
119
- ${props.ellipsis &&
323
+ padding-inline-start: calc(0.875 * ${theme.base.spacing});
324
+ padding-inline-end: calc(1.125 * ${theme.base.spacing});
325
+ ${ellipsis &&
120
326
  css `
121
327
  overflow: hidden;
122
328
  text-overflow: ellipsis;
@@ -124,6 +330,10 @@ export const StyledInnerStage = styled.span(props => {
124
330
  `;
125
331
  });
126
332
  StyledInnerStage.defaultProps = defaultThemeProp;
333
+ /**
334
+ * Clickable `<button>` for a single stage. Renders the chevron clip-path shape and
335
+ * applies status-based background and foreground colours.
336
+ */
127
337
  export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area': hitArea, transparency, 'font-weight': fontWeight }, components: { 'case-view': { stages } } }, status, readOnly }) => {
128
338
  const stageForeground = calculateForegroundColor(stages.status[status].background, stages.status[status]['foreground-color']);
129
339
  return css `
@@ -138,7 +348,7 @@ export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area'
138
348
  /* Browsers do not reliably apply flex align-items: stretch to <button> elements.
139
349
  * Adding 0.0625rem (1px at default font size) to min-height compensates so the button fills its <li> container,
140
350
  * keeping the clipped chevron background flush with the top and bottom borders. */
141
- min-height: calc(4 * var(--chevron-spacing) + 0.0625rem);
351
+ min-height: calc(5 * var(--chevron-spacing) + 0.0625rem);
142
352
  padding-inline-start: calc(2 * var(--chevron-spacing));
143
353
  padding-inline-end: var(--chevron-spacing);
144
354
  margin-inline-start: 0;
@@ -146,12 +356,12 @@ export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area'
146
356
  width: calc(100% + ${chevronConfig.width});
147
357
  --stage-chevron-width: ${chevronConfig.width};
148
358
 
149
- ${StyledIcon}, ${StyledInnerStage} {
359
+ ${StyledInnerStage} {
150
360
  transform: translateX(calc(-0.25 * var(--chevron-spacing)));
361
+ opacity: ${transparency['transparent-2']};
151
362
  }
152
363
  ${stageClipPath(true, true)}
153
364
 
154
- [dir='rtl'] & ${StyledIcon},
155
365
  [dir='rtl'] & ${StyledInnerStage} {
156
366
  transform: translateX(calc(0.25 * var(--chevron-spacing)));
157
367
  }
@@ -201,19 +411,12 @@ export const StyledStage = styled.button(({ theme: { base: { palette, 'hit-area'
201
411
  width: calc(100% + ${chevronConfig.coarseWidth});
202
412
  --stage-chevron-width: ${chevronConfig.coarseWidth};
203
413
  }
204
-
205
- ${StyledIcon} {
206
- & ~ ${StyledInnerStage} {
207
- margin-inline-start: 0.25rem;
208
- }
209
- }
210
-
211
- ${StyledInnerStage} {
212
- opacity: ${transparency['transparent-2']};
213
- }
214
414
  `;
215
415
  });
216
416
  StyledStage.defaultProps = defaultThemeProp;
417
+ /**
418
+ * Checkmark icon shown inside completed stages, tinted to match the stage foreground colour.
419
+ */
217
420
  export const StyledStageCompleteIcon = styled(Icon)(({ theme: { components: { 'case-view': { stages: { status: { completed: { 'foreground-color': foregroundColor, background } } } } } } }) => {
218
421
  const stageForeground = calculateForegroundColor(background, foregroundColor);
219
422
  return css `
@@ -221,17 +424,24 @@ export const StyledStageCompleteIcon = styled(Icon)(({ theme: { components: { 'c
221
424
  `;
222
425
  });
223
426
  StyledStageCompleteIcon.defaultProps = defaultThemeProp;
427
+ /**
428
+ * Stage label text. Renders in bold weight when the stage is the current active stage.
429
+ */
224
430
  export const StyledStageText = styled(Text)(({ theme, isCurrent }) => css `
225
431
  font-weight: ${isCurrent ? theme.base['font-weight'].bold : theme.base['font-weight'].normal};
226
432
  `);
227
433
  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
+ */
228
438
  export const StyledStageContainer = styled.li(({ theme: { base: { palette }, components: { card: { 'border-radius': borderRadius } } }, readOnly }) => {
229
439
  return css `
230
440
  display: flex;
231
441
  position: relative;
232
442
  margin-inline-start: calc(-1 * ${chevronConfig.width});
233
443
 
234
- &:first-of-type {
444
+ &:nth-of-type(2) {
235
445
  margin-inline-start: 0;
236
446
  }
237
447
 
@@ -239,8 +449,9 @@ export const StyledStageContainer = styled.li(({ theme: { base: { palette }, com
239
449
  * All non-last <li>s have z-index:1 so the later (right) <li>'s ::before paints on top
240
450
  * at internal boundaries; the last <li> has no z-index so its left neighbor's ::after
241
451
  * wins at the final boundary. On hover/focus/active, the interacting <li> gains z:2/3
242
- * and its own pseudo-element surfaces for that boundary, regardless of which side. */
243
- &:not(:last-of-type) {
452
+ * and its own pseudo-element surfaces for that boundary, regardless of which side.
453
+ */
454
+ &:not(:nth-last-of-type(-n + 2)) {
244
455
  z-index: 1;
245
456
  }
246
457
 
@@ -260,7 +471,7 @@ export const StyledStageContainer = styled.li(({ theme: { base: { palette }, com
260
471
  margin-inline-start: calc(-1 * ${chevronConfig.coarseWidth});
261
472
  }
262
473
 
263
- &:not(:first-of-type)::before {
474
+ &:not(:nth-of-type(-n + 2))::before {
264
475
  ${createChevronPseudo('calc(2 * var(--chevron-border-thickness))', palette['border-line'], chevronConfig.squareSize)}
265
476
  /* Elevate ::before above the button so it renders via the same pathway as ::after.
266
477
  * Without z-index, ::before (first in DOM) is painted first and then covered by the
@@ -268,16 +479,18 @@ export const StyledStageContainer = styled.li(({ theme: { base: { palette }, com
268
479
  * causes thicker interactive-state borders (3×) to look thinner on the left boundary
269
480
  * than on the right (::after, which paints last and is always fully visible).
270
481
  * The ::before tip's rightmost extent is at --stage-chevron-width from the <li> start,
271
- * which is before the stage text (padding-inline-start: 2 × spacing), so no content overlap. */
482
+ * which is before the stage text (padding-inline-start: 2 × spacing), so no content overlap.
483
+ */
272
484
  z-index: 1;
273
485
  }
274
486
 
275
- &:not(:last-of-type)::after {
487
+ &:not(:nth-last-of-type(-n + 2))::after {
276
488
  ${createChevronPseudo('calc(2 * var(--chevron-border-thickness))', palette['border-line'], chevronConfig.squareSize)}
277
489
  }
278
490
 
279
491
  /* Chevron positioning: 45° rotated squares create visual tips via offsets:
280
- * left ≈ 0.688 × spacing, right ≈ 1.15 × spacing (accounts for skew correction) */
492
+ * left ≈ 0.86 × spacing, right ≈ 1.445 × spacing (accounts for skew correction)
493
+ */
281
494
 
282
495
  [dir='rtl'] &::before,
283
496
  [dir='rtl'] &::after {
@@ -285,19 +498,18 @@ export const StyledStageContainer = styled.li(({ theme: { base: { palette }, com
285
498
  }
286
499
 
287
500
  /* Thicken chevrons to visually match button top/bottom border on any colored state.
288
- * At 45° rotation, border appears 1/sin(45°) ≈ 0.707× thinner, so 2× border ≈ 1.4× horizontal border weight. */
501
+ * At 45° rotation, border appears 1/sin(45°) ≈ 0.707× thinner, so 2× border ≈ 1.4× horizontal border weight.
502
+ */
289
503
  ${createChevronBaseStyles(chevronConfig, 'var(--chevron-border-thickness)')}
290
504
  ${createChevronInteractiveStates(chevronConfig, 'var(--chevron-border-thickness)', !readOnly)}
291
-
292
- ${!readOnly &&
505
+ ${!readOnly &&
293
506
  css `
294
507
  &:has(button:hover)::before,
295
508
  &:has(button:hover)::after {
296
509
  border-color: ${palette['border-line']};
297
510
  }
298
511
  `}
299
-
300
- &:has(button:focus-visible)::before,
512
+ &:has(button:focus-visible)::before,
301
513
  &:has(button:focus-visible)::after {
302
514
  border-color: ${palette.interactive};
303
515
  }
@@ -308,27 +520,27 @@ export const StyledStageContainer = styled.li(({ theme: { base: { palette }, com
308
520
  }
309
521
 
310
522
  @media (pointer: coarse) {
311
- &:not(:first-of-type)::before,
312
- &:not(:last-of-type)::after {
523
+ &:not(:nth-of-type(-n + 2))::before,
524
+ &:not(:nth-last-of-type(-n + 2))::after {
313
525
  inset-block-start: calc(50% - ${chevronConfig.coarseSquareSize} / 2);
314
526
  width: ${chevronConfig.coarseSquareSize};
315
527
  height: ${chevronConfig.coarseSquareSize};
316
528
  }
317
529
 
318
- &:not(:first-of-type)::before {
530
+ &:not(:nth-of-type(-n + 2))::before {
319
531
  inset-inline-start: calc(
320
532
  -1 * ${chevronConfig.coarseWidth} - ${chevronConfig.coarseOffsetLeft}
321
533
  );
322
534
  }
323
535
 
324
- &:not(:last-of-type)::after {
536
+ &:not(:nth-last-of-type(-n + 2))::after {
325
537
  inset-inline-end: calc(
326
538
  -1 * ${chevronConfig.coarseWidth} + ${chevronConfig.coarseOffsetRight}
327
539
  );
328
540
  }
329
541
  }
330
542
 
331
- &:first-of-type > ${StyledStage} {
543
+ &:nth-of-type(2):not(:nth-last-of-type(2)) > ${StyledStage} {
332
544
  --stage-chevron-width: ${chevronConfig.width};
333
545
  border-start-start-radius: ${borderRadius};
334
546
  border-end-start-radius: ${borderRadius};
@@ -365,30 +577,77 @@ export const StyledStageContainer = styled.li(({ theme: { base: { palette }, com
365
577
  }
366
578
  }
367
579
 
368
- &:last-of-type > ${StyledStage} {
580
+ &:nth-last-of-type(2):not(:nth-of-type(2)) > ${StyledStage} {
369
581
  --stage-chevron-width: ${chevronConfig.width};
370
582
  border-start-end-radius: ${borderRadius};
371
583
  border-end-end-radius: ${borderRadius};
372
- border-inline-end-width: calc(2 * var(--chevron-border-thickness));
373
584
  padding-inline-end: 0;
374
585
  width: 100%;
375
586
  ${stageClipPath(true, false)}
376
587
 
588
+ ${!readOnly &&
589
+ 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};
605
+
606
+ [dir='rtl'] & {
607
+ box-shadow: inset calc(2 * var(--chevron-border-thickness)) 0 0 ${palette.interactive};
608
+ }
609
+ }
610
+
377
611
  @media (pointer: coarse) {
378
612
  --stage-chevron-width: ${chevronConfig.coarseWidth};
379
613
  }
380
614
  }
381
615
 
382
- [dir='rtl'] &:first-of-type > ${StyledStage} {
616
+ [dir='rtl'] &:nth-of-type(2):not(:nth-last-of-type(2)) > ${StyledStage} {
383
617
  ${stageClipPath(true, false, true)}
384
618
  }
385
619
 
386
- [dir='rtl'] &:last-of-type > ${StyledStage} {
620
+ [dir='rtl'] &:nth-last-of-type(2):not(:nth-of-type(2)) > ${StyledStage} {
387
621
  ${stageClipPath(false, true, true)}
388
622
  }
623
+
624
+ &:nth-of-type(2):nth-last-of-type(2) > ${StyledStage} {
625
+ border-radius: ${borderRadius};
626
+ width: 100%;
627
+ ${stageClipPath(false, false)}
628
+
629
+ ${!readOnly &&
630
+ 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};
643
+ }
644
+ }
389
645
  `;
390
646
  });
391
647
  StyledStageContainer.defaultProps = defaultThemeProp;
648
+ /**
649
+ * `<ol>` list of sub-step items displayed below the stages bar when a stage is expanded.
650
+ */
392
651
  export const StyledStepsContainer = styled.ol(({ theme }) => {
393
652
  return css `
394
653
  list-style-type: none;
@@ -400,6 +659,9 @@ export const StyledStepsContainer = styled.ol(({ theme }) => {
400
659
  `;
401
660
  });
402
661
  StyledStepsContainer.defaultProps = defaultThemeProp;
662
+ /**
663
+ * Individual step `<li>` within a stage. Dims text and icon at reduced opacity when completed.
664
+ */
403
665
  export const StyledStep = styled.li(({ completed, theme }) => {
404
666
  const textOpacity = completed ? theme.base.transparency['transparent-2'] : 1;
405
667
  return css `
@@ -421,6 +683,10 @@ export const StyledStep = styled.li(({ completed, theme }) => {
421
683
  `;
422
684
  });
423
685
  StyledStep.defaultProps = defaultThemeProp;
686
+ /**
687
+ * Small datetime label rendered beneath a step. Displayed at the `xs` font size
688
+ * and reduced opacity.
689
+ */
424
690
  export const StyledDateTimeDisplay = styled(DateTimeDisplay)(({ theme }) => {
425
691
  const fontSize = calculateFontSize(theme.base['font-size'], theme.base['font-scale']);
426
692
  return css `
@@ -429,12 +695,19 @@ export const StyledDateTimeDisplay = styled(DateTimeDisplay)(({ theme }) => {
429
695
  `;
430
696
  });
431
697
  StyledDateTimeDisplay.defaultProps = defaultThemeProp;
698
+ /**
699
+ * Container `<div>` for the stage glimpse preview panel, constrained to a small max-width.
700
+ */
432
701
  export const StyledStageGlimpse = styled.div(({ theme }) => {
433
702
  return css `
434
703
  max-width: ${theme.base['content-width'].sm};
435
704
  `;
436
705
  });
437
706
  StyledStageGlimpse.defaultProps = defaultThemeProp;
707
+ /**
708
+ * Visually hidden `<p>` providing an accessible description for the stages list,
709
+ * announced by screen readers but not displayed on screen.
710
+ */
438
711
  export const StyledStagesDescription = styled.p `
439
712
  display: none;
440
713
  `;