@juspay/svelte-ui-components 2.134.1 → 2.136.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 (38) hide show
  1. package/dist/Chat/Chat.svelte +38 -2
  2. package/dist/Chat/properties.d.ts +7 -0
  3. package/dist/ChatMessageList/ChatMessageList.svelte +54 -1
  4. package/dist/CheckListItem/CheckListItem.svelte +4 -1
  5. package/dist/ChipInput/ChipInput.svelte +150 -10
  6. package/dist/ChipInput/properties.d.ts +18 -1
  7. package/dist/CommandMenu/CommandMenu.svelte +7 -3
  8. package/dist/EmptyState/EmptyState.svelte +17 -3
  9. package/dist/Input/Input.svelte +1 -0
  10. package/dist/ListItem/ListItem.svelte +14 -12
  11. package/dist/ListItem/properties.d.ts +7 -0
  12. package/dist/Menu/Menu.svelte +2 -1
  13. package/dist/Menu/properties.d.ts +2 -0
  14. package/dist/Modal/Modal.svelte +3 -3
  15. package/dist/Pill/Pill.svelte +2 -0
  16. package/dist/Pill/properties.d.ts +1 -0
  17. package/dist/Sheet/Sheet.svelte +3 -23
  18. package/dist/StatCard/StatCard.svelte +64 -1
  19. package/dist/StatCard/properties.d.ts +44 -2
  20. package/dist/Status/Status.svelte +2 -1
  21. package/dist/Status/properties.d.ts +10 -0
  22. package/dist/Stepper/Step.svelte +95 -2
  23. package/dist/Stepper/Stepper.svelte +52 -7
  24. package/dist/Stepper/properties.d.ts +34 -1
  25. package/dist/Table/BuiltinCell.svelte +46 -23
  26. package/dist/Table/Table.svelte +63 -40
  27. package/dist/Table/cellData.js +3 -0
  28. package/dist/Table/properties.d.ts +70 -0
  29. package/dist/ThinkingIndicator/ThinkingIndicator.svelte +93 -5
  30. package/dist/ThinkingIndicator/properties.d.ts +7 -3
  31. package/dist/TypewriterText/TypewriterText.svelte +67 -2
  32. package/dist/TypewriterText/properties.d.ts +97 -1
  33. package/dist/index.d.ts +2 -1
  34. package/dist/index.js +1 -1
  35. package/dist/utils.d.ts +2 -0
  36. package/dist/utils.js +30 -0
  37. package/dist-wc/index.js +2937 -2658
  38. package/package.json +1 -1
@@ -3,7 +3,7 @@
3
3
  import { onMount, onDestroy } from 'svelte';
4
4
  import ModalAnimation from '../Animations/ModalAnimation.svelte';
5
5
  import OverlayAnimation from '../Animations/OverlayAnimation.svelte';
6
- import { createDebouncer } from '../utils';
6
+ import { createDebouncer, lockBodyScroll, unlockBodyScroll } from '../utils';
7
7
  import Button from '../Button/Button.svelte';
8
8
  import Img from '../Img/Img.svelte';
9
9
 
@@ -122,7 +122,7 @@
122
122
 
123
123
  onMount(() => {
124
124
  if (lockScroll) {
125
- document.body.style.overflow = 'hidden';
125
+ lockBodyScroll();
126
126
  }
127
127
  if (typeof autoDismissAfter === 'number') {
128
128
  dismissTimer = setTimeout(() => onclose?.(), autoDismissAfter);
@@ -139,7 +139,7 @@
139
139
  }
140
140
  if (typeof window !== 'undefined') {
141
141
  if (lockScroll) {
142
- document.body.style.overflow = '';
142
+ unlockBodyScroll();
143
143
  }
144
144
  if (supportHardwareBackPress) {
145
145
  if (!backPressed) {
@@ -8,6 +8,7 @@
8
8
  dismissible = false,
9
9
  disabled = false,
10
10
  testId,
11
+ title,
11
12
  dismissIcon,
12
13
  leadingIcon,
13
14
  onclick,
@@ -52,6 +53,7 @@
52
53
  tabindex={interactive ? 0 : null}
53
54
  aria-disabled={interactive && disabled ? true : null}
54
55
  data-pw={typeof testId === 'string' ? testId : null}
56
+ title={title ?? null}
55
57
  testID={typeof testId === 'string' ? testId : null}
56
58
  >
57
59
  {#if typeof leadingIcon === 'function'}
@@ -7,6 +7,7 @@ export type OptionalPillProperties = {
7
7
  dismissible?: boolean;
8
8
  disabled?: boolean;
9
9
  testId?: string;
10
+ title?: string;
10
11
  dismissIcon?: Snippet;
11
12
  /**
12
13
  * A Svelte snippet rendered immediately before the text label inside a
@@ -1,27 +1,7 @@
1
- <script lang="ts" module>
2
- // Reference-counted so two Sheets open at once (or a Sheet nested inside
3
- // another scroll-locking surface) don't have the second one's close clobber
4
- // the first one's still-needed lock.
5
- let scrollLockCount = 0;
6
-
7
- function lockScroll() {
8
- if (scrollLockCount === 0) {
9
- document.body.style.overflow = 'hidden';
10
- }
11
- scrollLockCount += 1;
12
- }
13
-
14
- function unlockScroll() {
15
- scrollLockCount = Math.max(0, scrollLockCount - 1);
16
- if (scrollLockCount === 0) {
17
- document.body.style.overflow = '';
18
- }
19
- }
20
- </script>
21
-
22
1
  <script lang="ts">
23
2
  import type { SheetProperties } from './properties';
24
3
  import { fly, fade } from 'svelte/transition';
4
+ import { lockBodyScroll, unlockBodyScroll } from '../utils';
25
5
  import { tick } from 'svelte';
26
6
  import Button from '../Button/Button.svelte';
27
7
 
@@ -97,7 +77,7 @@
97
77
  }
98
78
 
99
79
  function scrollLockAction(_node: HTMLElement) {
100
- lockScroll();
80
+ lockBodyScroll();
101
81
  tick().then(() => {
102
82
  if (sheetPanel !== null) {
103
83
  sheetPanel.focus();
@@ -105,7 +85,7 @@
105
85
  });
106
86
  return {
107
87
  destroy() {
108
- unlockScroll();
88
+ unlockBodyScroll();
109
89
  }
110
90
  };
111
91
  }
@@ -185,6 +185,8 @@
185
185
  <div class="statcard-row-value-line">
186
186
  <div
187
187
  class="statcard-value"
188
+ class:statcard-value-success={row.valueVariant === 'success'}
189
+ class:statcard-value-warning={row.valueVariant === 'warning'}
188
190
  data-pw={typeof testId === 'string' ? `${testId}-value-${rowIndex}` : null}
189
191
  testID={typeof testId === 'string' ? `${testId}-value-${rowIndex}` : null}
190
192
  >
@@ -217,9 +219,25 @@
217
219
  </div>
218
220
  {/if}
219
221
  {#if typeof row.additionalContent === 'string' && row.additionalContent.length > 0}
220
- <div class="statcard-row-additional">{row.additionalContent}</div>
222
+ <div
223
+ class="statcard-row-additional"
224
+ class:statcard-row-additional-break={row.additionalContentBreak === true}
225
+ data-pw={typeof testId === 'string' ? `${testId}-additional-${rowIndex}` : null}
226
+ testID={typeof testId === 'string' ? `${testId}-additional-${rowIndex}` : null}
227
+ >
228
+ {row.additionalContent}
229
+ </div>
221
230
  {/if}
222
231
  </div>
232
+ {#if typeof row.subtitle === 'string' && row.subtitle.length > 0}
233
+ <div
234
+ class="statcard-row-subtitle"
235
+ data-pw={typeof testId === 'string' ? `${testId}-subtitle-${rowIndex}` : null}
236
+ testID={typeof testId === 'string' ? `${testId}-subtitle-${rowIndex}` : null}
237
+ >
238
+ {row.subtitle}
239
+ </div>
240
+ {/if}
223
241
  {#if Array.isArray(row.breakdown) && row.breakdown.length > 0}
224
242
  {#if typeof row.breakdownHeading === 'string' && row.breakdownHeading.length > 0}
225
243
  <div class="statcard-breakdown-heading">{row.breakdownHeading}</div>
@@ -421,6 +439,24 @@
421
439
  line-height: var(--statcard-value-line-height, 1.2);
422
440
  }
423
441
 
442
+ .statcard-value-success {
443
+ color: var(--statcard-value-success-color, #16a34a);
444
+ }
445
+
446
+ .statcard-value-warning {
447
+ color: var(--statcard-value-warning-color, #f59e0b);
448
+ }
449
+
450
+ /* Per-row value typography override — falls back to the card-level value
451
+ tokens so an unset row matches today's shared appearance exactly, then to
452
+ the same literals as the last resort. Scoped to the row's value line so
453
+ the single (non-rows) card value is unaffected. Mirrors the fallback
454
+ shape of --statcard-row-subtitle-font-size below. */
455
+ .statcard-row-value-line .statcard-value {
456
+ font-size: var(--statcard-row-value-font-size, var(--statcard-value-font-size, 24px));
457
+ font-weight: var(--statcard-row-value-font-weight, var(--statcard-value-font-weight, 600));
458
+ }
459
+
424
460
  .statcard-delta {
425
461
  font-size: var(--statcard-delta-font-size, 13px);
426
462
  font-weight: var(--statcard-delta-font-weight, 500);
@@ -458,6 +494,23 @@
458
494
  line-height: var(--statcard-subtitle-line-height, 1.4);
459
495
  }
460
496
 
497
+ /* Per-row subtitle — falls back to the card-level subtitle tokens so a
498
+ consumer that already themes `.statcard-subtitle` gets a matching look
499
+ with no extra wiring, then to the same literals as the last resort. */
500
+ .statcard-row-subtitle {
501
+ order: var(--statcard-row-subtitle-order, 0);
502
+ font-size: var(--statcard-row-subtitle-font-size, var(--statcard-subtitle-font-size, 12px));
503
+ font-weight: var(
504
+ --statcard-row-subtitle-font-weight,
505
+ var(--statcard-subtitle-font-weight, 400)
506
+ );
507
+ color: var(--statcard-row-subtitle-color, var(--statcard-subtitle-color, #9ca3af));
508
+ line-height: var(
509
+ --statcard-row-subtitle-line-height,
510
+ var(--statcard-subtitle-line-height, 1.4)
511
+ );
512
+ }
513
+
461
514
  /* Multi-row layout */
462
515
  .statcard-rows {
463
516
  order: var(--statcard-rows-order, 0);
@@ -498,6 +551,7 @@
498
551
  }
499
552
 
500
553
  .statcard-row-heading-wrap {
554
+ order: var(--statcard-row-heading-order, 0);
501
555
  display: flex;
502
556
  align-items: center;
503
557
  }
@@ -516,6 +570,7 @@
516
570
  }
517
571
 
518
572
  .statcard-row-value-line {
573
+ order: var(--statcard-row-value-line-order, 0);
519
574
  display: flex;
520
575
  align-items: baseline;
521
576
  gap: var(--statcard-row-value-gap, 8px);
@@ -523,12 +578,20 @@
523
578
  }
524
579
 
525
580
  .statcard-row-additional {
581
+ /* Default: flows inline in the wrapping flex row and only breaks if it
582
+ does not fit — matches short unit suffixes like "%" or "ms". */
526
583
  font-size: var(--statcard-row-additional-font-size, 12px);
527
584
  font-weight: var(--statcard-row-additional-font-weight, 400);
528
585
  color: var(--statcard-row-additional-color, #9ca3af);
529
586
  line-height: 1.4;
530
587
  }
531
588
 
589
+ .statcard-row-additional-break {
590
+ /* Opt-in: forces its own line regardless of available width, for
591
+ descriptive text that should never sit beside the value/delta. */
592
+ flex-basis: var(--statcard-row-additional-flex-basis, 100%);
593
+ }
594
+
532
595
  /* Breakdown grid */
533
596
  .statcard-breakdown-heading {
534
597
  font-size: var(--statcard-breakdown-heading-font-size, 11px);
@@ -43,15 +43,57 @@ export type StatCardRow = {
43
43
  change?: number | null;
44
44
  /** Invert delta colors for lower-is-better metrics on this row. */
45
45
  invertChangeColors?: boolean;
46
- /** Additional descriptive text rendered after the delta. */
46
+ /**
47
+ * Additional descriptive text rendered after the value/delta. By default it
48
+ * flows inline and only wraps if it does not fit (e.g. a short unit suffix
49
+ * like "%" or "ms"). Set `additionalContentBreak` to force it onto its own
50
+ * line regardless of available width.
51
+ */
47
52
  additionalContent?: string;
53
+ /**
54
+ * Forces `additionalContent` onto its own line below the value/delta,
55
+ * regardless of available width. Omit to keep the default inline flow.
56
+ */
57
+ additionalContentBreak?: boolean;
58
+ /** Tints this row's value text for a warning or success state. Omit for the default color. */
59
+ valueVariant?: 'success' | 'warning';
60
+ /**
61
+ * Secondary label rendered below this row's value line (e.g. a per-row
62
+ * comparison-period caption). Independent of the card-level `subtitle` —
63
+ * both may be set at once, e.g. when different rows compare against
64
+ * different baselines.
65
+ *
66
+ * Renders after the value line by default (heading → value → subtitle in
67
+ * markup order). A consumer that needs title → subtitle → value can
68
+ * reorder without touching markup via `--statcard-row-subtitle-order` /
69
+ * `--statcard-row-value-line-order` — see `testId` below.
70
+ */
71
+ subtitle?: string;
48
72
  /** Tooltip shown on the row heading. */
49
73
  tooltip?: StatCardTooltip;
50
74
  /** Heading rendered above the breakdown grid. */
51
75
  breakdownHeading?: string;
52
76
  /** Breakdown items rendered in a grid below the row value. */
53
77
  breakdown?: StatCardBreakdownItem[];
54
- /** Test selector for the row root element. */
78
+ /**
79
+ * Test selector for the row root element, rendered as `data-pw`/`testID`.
80
+ * Also the CSS scoping anchor for per-row typography and layout: target
81
+ * `[data-pw="<testId>"]` to set `--statcard-row-value-font-size`,
82
+ * `--statcard-row-value-font-weight`, `--statcard-row-heading-font-size`,
83
+ * `--statcard-row-heading-font-weight`, or the row's flex `order` overrides
84
+ * (`--statcard-row-heading-order`, `--statcard-row-value-line-order`,
85
+ * `--statcard-row-subtitle-order`) for just this row, independent of its
86
+ * siblings — see the CSS Variables table in the component docs.
87
+ *
88
+ * The row's three sub-elements (heading wrap, value line, subtitle) render
89
+ * in that markup order and each default to `order: 0`, so an untouched row
90
+ * is byte-identical to today. Set the three order hooks to reorder them —
91
+ * e.g. subtitle before the value line for a title → subtitle → value
92
+ * layout — the same pattern the card-level `--statcard-subtitle-order` /
93
+ * `--statcard-value-row-order` hooks already use for the card's own
94
+ * sections. `.statcard-row` is a `display: flex; flex-direction: column`
95
+ * container, so `order` applies directly with no other CSS needed.
96
+ */
55
97
  testId?: string;
56
98
  };
57
99
  export type OptionalStatCardProperties = {
@@ -6,6 +6,7 @@
6
6
  let {
7
7
  statusIcon = 'icons/order-success-icon.svg',
8
8
  statusText = '',
9
+ statusTextTag = 'div',
9
10
  statusDescription = '',
10
11
  buttonProperties,
11
12
  classes,
@@ -30,7 +31,7 @@
30
31
  <Img inlineSvg src={statusIcon} alt="status" />
31
32
  {/if}
32
33
  </div>
33
- <div class="status-text">{statusText}</div>
34
+ <svelte:element this={statusTextTag} class="status-text">{statusText}</svelte:element>
34
35
  <div class="status-description">
35
36
  {#if typeof descriptionSnippet === 'function'}
36
37
  {@render descriptionSnippet()}
@@ -4,6 +4,16 @@ export type StatusProperties = StatusEventProperties & {
4
4
  statusIcon?: string;
5
5
  statusText: string;
6
6
  statusDescription: string;
7
+ /**
8
+ * Element `statusText` renders as. Defaults to `'div'`, matching every existing
9
+ * consumer's render output exactly. Reach for `'h1'`..`'h6'` when `statusText`
10
+ * carries a real page/section heading: an app that wires typography to semantic
11
+ * tags (rather than setting `font-size` in component styles) can only pick up
12
+ * that tag's size, weight and colour, and only a heading tag joins the
13
+ * document outline for assistive tech — a `div` does neither, no matter what
14
+ * CSS targets it.
15
+ */
16
+ statusTextTag?: 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6';
7
17
  buttonProperties?: ButtonProperties;
8
18
  classes?: string;
9
19
  /**
@@ -10,6 +10,8 @@
10
10
  orientation = 'horizontal',
11
11
  classes,
12
12
  ariaLabel,
13
+ testId,
14
+ suppressRoleAndTabindex,
13
15
  onclick,
14
16
  onkeydown
15
17
  }: StepProperties = $props();
@@ -33,12 +35,29 @@
33
35
  );
34
36
  </script>
35
37
 
38
+ <!-- role and tabindex are suppressed together by suppressRoleAndTabindex, so this
39
+ can never end up non-interactive with a tab stop; matches ListItem's precedent. -->
40
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
41
+ <!-- The per-status colour tokens are remapped by Stepper's own `.status-<name>` wrapper.
42
+ A Step used on its own — outside a Stepper — never gets that wrapper, so every status
43
+ rendered identically at the neutral default and the component could not express its
44
+ own state. Carrying the same classes here lets a bare Step theme itself; inside a
45
+ Stepper both set the same custom properties to the same values, so nothing about an
46
+ existing consumer changes. These are `class:` directives rather than an interpolated
47
+ string so the compiler can see the selectors are used and does not prune the rules. -->
36
48
  <div
37
49
  class={stepClass}
50
+ class:status-completed={status === 'completed'}
51
+ class:status-active={status === 'active'}
52
+ class:status-failure={status === 'failure'}
53
+ class:status-in-progress={status === 'in-progress'}
54
+ class:status-muted={status === 'muted'}
55
+ data-pw={typeof testId === 'string' ? testId : null}
56
+ testID={typeof testId === 'string' ? testId : null}
38
57
  onclick={handleStepClick}
39
58
  onkeydown={handleKeydown}
40
- role="button"
41
- tabindex="0"
59
+ role={suppressRoleAndTabindex ? null : 'button'}
60
+ tabindex={suppressRoleAndTabindex ? null : 0}
42
61
  aria-label={ariaLabel ?? null}
43
62
  aria-labelledby={ariaLabel ? null : `step-label-${stepIndex}`}
44
63
  aria-current={status === 'active' ? 'step' : null}
@@ -98,6 +117,10 @@
98
117
  display: flex;
99
118
  flex-direction: var(--step-flex-direction, row);
100
119
  align-items: center;
120
+ /* Lets a step's own box grow to fill a --step-container-flex share of the
121
+ Stepper row — a prerequisite for the separator below to have any free
122
+ space to grow into. Default 0 keeps a step sized to its content. */
123
+ flex-grow: var(--step-flex-grow, 0);
101
124
  }
102
125
 
103
126
  .step-vertical {
@@ -105,6 +128,63 @@
105
128
  align-items: flex-start;
106
129
  }
107
130
 
131
+ /* Mirrors Stepper.svelte's per-status remapping so a bare Step themes itself. Only the
132
+ tokens Step actually renders are set here; the separator is Stepper's to draw. Each
133
+ chain terminates in the same literal Stepper uses, so the two agree by construction. */
134
+ .status-completed {
135
+ --step-text-color: var(--step-text-completed-color, #24aa5a);
136
+ --step-index-container-background-color: var(
137
+ --step-index-container-completed-background-color,
138
+ #24aa5a
139
+ );
140
+ --step-index-container-border: var(--step-index-container-completed-border, none);
141
+ }
142
+
143
+ .status-active {
144
+ --step-text-color: var(--step-text-active-color, #2f3841);
145
+ --step-index-container-background-color: var(
146
+ --step-index-container-active-background-color,
147
+ #2f3841
148
+ );
149
+ --step-index-container-border: var(--step-index-container-active-border, none);
150
+ }
151
+
152
+ .status-failure {
153
+ --step-text-color: var(--step-text-failure-color, #e53935);
154
+ --step-index-container-background-color: var(
155
+ --step-index-container-failure-background-color,
156
+ var(--stepper-status-failure-color, #e53935)
157
+ );
158
+ --step-index-container-border: var(--step-index-container-failure-border, none);
159
+ }
160
+
161
+ .status-in-progress {
162
+ --step-text-color: var(--step-text-in-progress-color, #f59e0b);
163
+ --step-index-container-background-color: var(
164
+ --step-index-container-in-progress-background-color,
165
+ var(--stepper-status-in-progress-color, #f59e0b)
166
+ );
167
+ --step-index-container-border: var(--step-index-container-in-progress-border, none);
168
+ }
169
+
170
+ .status-muted {
171
+ --step-text-color: var(--step-text-muted-color, #667080);
172
+ --step-index-container-background-color: var(
173
+ --step-index-container-muted-background-color,
174
+ #c9d2db
175
+ );
176
+ /* The circle is pale, so the numeral cannot inherit the white used on every other
177
+ status — that measured 1.53:1. #2f3841 is the same literal the active status
178
+ already uses for text, and reads at 7.79:1 here. */
179
+ --step-index-color: var(--step-index-muted-color, #2f3841);
180
+ --step-index-container-border: var(--step-index-container-muted-border, none);
181
+ --step-index-container-height: var(--step-index-container-muted-height, 20px);
182
+ --step-index-container-width: var(--step-index-container-muted-width, 20px);
183
+ --step-index-font-size: var(--step-index-muted-font-size, 10px);
184
+ }
185
+
186
+ /* status-pending sets nothing — it is the neutral default already in .step-index-container. */
187
+
108
188
  .step-index-container {
109
189
  display: flex;
110
190
  justify-content: center;
@@ -113,6 +193,11 @@
113
193
  width: var(--step-index-container-width, 30px);
114
194
  border-radius: var(--step-index-container-radius, 50%);
115
195
  background-color: var(--step-index-container-background-color, #798fa5cc);
196
+ /* No border today, so the fallback is `none` — an existing consumer that sets
197
+ nothing renders byte-identically. Shared with .step-icon-container below so
198
+ a status override (see Stepper.svelte) themes the circle whichever of the
199
+ two mutually-exclusive renders is showing. */
200
+ border: var(--step-index-container-border, none);
116
201
  color: var(--step-index-color, white);
117
202
  flex-shrink: 0;
118
203
  }
@@ -125,6 +210,7 @@
125
210
  width: var(--step-index-container-width, 30px);
126
211
  border-radius: var(--step-index-container-radius, 50%);
127
212
  background-color: var(--step-index-container-background-color, #798fa5cc);
213
+ border: var(--step-index-container-border, none);
128
214
  flex-shrink: 0;
129
215
  overflow: hidden;
130
216
  }
@@ -154,6 +240,9 @@
154
240
  display: var(--stepper-separator-display, var(--separator-display, block));
155
241
  height: var(--stepper-separator-height, var(--separator-height, 1px));
156
242
  width: var(--stepper-separator-width, var(--separator-width, 50px));
243
+ /* 0 keeps the separator pinned to its width above; set alongside --step-flex-grow
244
+ and --step-container-flex to let it stretch into a card's remaining width. */
245
+ flex-grow: var(--stepper-separator-flex-grow, 0);
157
246
  margin: var(--stepper-separator-margin, var(--separator-margin, 0px 12px 0px 12px));
158
247
  background-image: var(
159
248
  --stepper-separator-background-image,
@@ -200,6 +289,10 @@
200
289
  .step-text {
201
290
  margin: var(--step-text-margin, 0px 0px 0px 12px);
202
291
  font-size: var(--step-text-font-size, 12px);
292
+ /* No font-weight is set today, so the div inherits the browser default
293
+ `normal` — that is the literal fallback, keeping an existing consumer
294
+ byte-identical. */
295
+ font-weight: var(--step-text-font-weight, normal);
203
296
  color: var(--step-text-color, #798fa5cc);
204
297
  }
205
298
 
@@ -8,6 +8,8 @@
8
8
  orientation = 'horizontal',
9
9
  classes,
10
10
  testId,
11
+ suppressRoleAndTabindex,
12
+ suppressContainerTestId,
11
13
  onstepclick,
12
14
  onhandleStepClick
13
15
  }: StepperProperties = $props();
@@ -28,6 +30,12 @@
28
30
  // Support the deprecated onhandleStepClick alias — onstepclick takes priority.
29
31
  const effectiveStepClick = $derived(onstepclick ?? onhandleStepClick);
30
32
 
33
+ // Per-step ids always derive from testId, even when the container itself is
34
+ // opted out of claiming it — only the container's own data-pw/testID is suppressed.
35
+ const containerTestId = $derived(
36
+ typeof testId === 'string' && !suppressContainerTestId ? testId : null
37
+ );
38
+
31
39
  let containerClass = $derived(
32
40
  ['container', orientation === 'vertical' ? 'container-vertical' : '', classes ?? '']
33
41
  .filter((c) => c.length > 0)
@@ -35,14 +43,11 @@
35
43
  );
36
44
  </script>
37
45
 
38
- <div
39
- class={containerClass}
40
- data-pw={typeof testId === 'string' ? testId : null}
41
- testID={typeof testId === 'string' ? testId : null}
42
- role="list"
43
- >
46
+ <div class={containerClass} data-pw={containerTestId} testID={containerTestId} role="list">
44
47
  {#each steps as currentStep, stepIndex (stepIndex)}
45
48
  {@const effectiveStatus = resolveStatus(stepIndex, currentStep.status ?? null)}
49
+ {@const stepTestId =
50
+ currentStep.testId ?? (typeof testId === 'string' ? `${testId}-step-${stepIndex + 1}` : null)}
46
51
  <div
47
52
  role="listitem"
48
53
  class="step-container status-{effectiveStatus} {effectiveStatus === 'active'
@@ -56,7 +61,9 @@
56
61
  stepIndex={stepIndex + 1}
57
62
  status={effectiveStatus}
58
63
  badge={currentStep.badge}
64
+ testId={stepTestId}
59
65
  {orientation}
66
+ {suppressRoleAndTabindex}
60
67
  />
61
68
  </div>
62
69
  {/each}
@@ -66,6 +73,7 @@
66
73
  .container {
67
74
  display: flex;
68
75
  flex-direction: var(--container-flex-direction, row);
76
+ flex-wrap: var(--container-flex-wrap, nowrap);
69
77
  align-items: center;
70
78
  }
71
79
 
@@ -81,6 +89,7 @@
81
89
  .step-container {
82
90
  display: flex;
83
91
  align-items: center;
92
+ flex: var(--step-container-flex, 0 1 auto);
84
93
  }
85
94
 
86
95
  /* status-completed */
@@ -94,6 +103,10 @@
94
103
  --step-index-container-completed-background-color,
95
104
  #24aa5a
96
105
  );
106
+ /* No border existed for any status before this, so every one of these
107
+ per-status hooks falls back to `none` — byte-identical for a consumer
108
+ that sets nothing. */
109
+ --step-index-container-border: var(--step-index-container-completed-border, none);
97
110
  }
98
111
 
99
112
  /* status-active */
@@ -107,9 +120,10 @@
107
120
  --step-index-container-active-background-color,
108
121
  #2f3841
109
122
  );
123
+ --step-index-container-border: var(--step-index-container-active-border, none);
110
124
  }
111
125
 
112
- /* status-pending: no override — falls through to #798fa5cc grey default in Step.svelte */
126
+ /* status-pending: no override — falls through to #798fa5cc grey (and no border) default in Step.svelte */
113
127
 
114
128
  /* status-failure */
115
129
  .status-failure {
@@ -122,6 +136,7 @@
122
136
  --step-index-container-failure-background-color,
123
137
  var(--stepper-status-failure-color, #e53935)
124
138
  );
139
+ --step-index-container-border: var(--step-index-container-failure-border, none);
125
140
  }
126
141
 
127
142
  /* status-in-progress */
@@ -135,6 +150,36 @@
135
150
  --step-index-container-in-progress-background-color,
136
151
  var(--stepper-status-in-progress-color, #f59e0b)
137
152
  );
153
+ --step-index-container-border: var(--step-index-container-in-progress-border, none);
154
+ }
155
+
156
+ /* status-muted: a smaller, subtly-tinted marker for a de-emphasized or
157
+ supplementary step (e.g. an informational marker riding alongside a
158
+ primary rail). New opt-in status value — none of the statuses above are
159
+ touched, and a step only reaches these rules by setting status:'muted'
160
+ explicitly (resolveStatus never derives it). Reuses the same
161
+ --step-index-container-height/-width/-background-color, --step-text-color,
162
+ --stepper-separator-background-image-color and --step-index-container-border
163
+ hooks the other statuses already theme through, so a call site can further
164
+ override any of them the same way. */
165
+ .status-muted {
166
+ --step-text-color: var(--step-text-muted-color, #667080);
167
+ --stepper-separator-background-image-color: var(
168
+ --stepper-separator-background-image-muted-color,
169
+ #c9d2db
170
+ );
171
+ --step-index-container-background-color: var(
172
+ --step-index-container-muted-background-color,
173
+ #c9d2db
174
+ );
175
+ /* The circle is pale, so the numeral cannot inherit the white used on every other
176
+ status — that measured 1.53:1. #2f3841 is the same literal the active status
177
+ already uses for text, and reads at 7.79:1 here. */
178
+ --step-index-color: var(--step-index-muted-color, #2f3841);
179
+ --step-index-container-border: var(--step-index-container-muted-border, none);
180
+ --step-index-container-height: var(--step-index-container-muted-height, 20px);
181
+ --step-index-container-width: var(--step-index-container-muted-width, 20px);
182
+ --step-index-font-size: var(--step-index-muted-font-size, 10px);
138
183
  }
139
184
 
140
185
  :global([theme='dark']) .status-failure {
@@ -1,5 +1,12 @@
1
1
  import type { Snippet } from 'svelte';
2
- export type StepStatus = 'completed' | 'active' | 'pending' | 'failure' | 'in-progress';
2
+ /**
3
+ * `muted` is an opt-in, purely additive status: a smaller, subtly-tinted circle
4
+ * for a de-emphasized or supplementary step (e.g. an informational marker
5
+ * riding alongside a primary rail). It is only ever reached by setting a
6
+ * step's `status` explicitly — `resolveStatus` in Stepper.svelte never derives
7
+ * it from `currentStepIndex` — so no existing step changes how it renders.
8
+ */
9
+ export type StepStatus = 'completed' | 'active' | 'pending' | 'failure' | 'in-progress' | 'muted';
3
10
  export type Step = {
4
11
  label: string;
5
12
  /**
@@ -15,6 +22,13 @@ export type Step = {
15
22
  * "New" label.
16
23
  */
17
24
  badge?: Snippet;
25
+ /**
26
+ * Test selector for this step's root element, rendered as `data-pw`. When
27
+ * omitted and the Stepper itself has a `testId`, falls back to
28
+ * `${stepperTestId}-step-${n}` (1-based). When neither is set, no attribute
29
+ * is rendered.
30
+ */
31
+ testId?: string;
18
32
  };
19
33
  export type MandatoryStepperProperties = {
20
34
  steps: Array<Step>;
@@ -24,6 +38,18 @@ export type OptionalStepperProperties = {
24
38
  orientation?: 'horizontal' | 'vertical';
25
39
  classes?: string;
26
40
  testId?: string;
41
+ /**
42
+ * Removes every Step's synthetic role and tab stop while retaining its mouse handlers.
43
+ * Opt in when an ancestor or consumer supplies the semantic interactive control.
44
+ */
45
+ suppressRoleAndTabindex?: boolean;
46
+ /**
47
+ * Stops the Stepper's own root element from rendering `testId` as `data-pw`/`testID`,
48
+ * while per-step ids still derive from `testId` (`${testId}-step-${n}`) exactly as
49
+ * before. Opt in when the element wrapping the Stepper already carries the same
50
+ * `data-pw` value, which would otherwise leave two elements matching that selector.
51
+ */
52
+ suppressContainerTestId?: boolean;
27
53
  };
28
54
  export type StepperEventProperties = {
29
55
  onstepclick?: (event: {
@@ -40,6 +66,13 @@ export type OptionalStepProperties = {
40
66
  status?: StepStatus;
41
67
  badge?: Snippet;
42
68
  classes?: string;
69
+ /** Test selector for the step's root element, rendered as `data-pw`. */
70
+ testId?: string | null;
71
+ /**
72
+ * Removes Step's synthetic role and tab stop while retaining its mouse handlers.
73
+ * Opt in when an ancestor or consumer supplies the semantic interactive control.
74
+ */
75
+ suppressRoleAndTabindex?: boolean;
43
76
  };
44
77
  export type StepEventProperties = {
45
78
  onclick?: (event: {