@juspay/svelte-ui-components 2.61.0 → 2.62.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.
@@ -1,18 +1,77 @@
1
1
  <script lang="ts">
2
2
  import type { StepProperties } from './properties';
3
3
 
4
- let { stepIndex, label, icon, classes, onclick, onkeydown }: StepProperties = $props();
4
+ let {
5
+ stepIndex,
6
+ label,
7
+ icon,
8
+ status,
9
+ badge,
10
+ orientation = 'horizontal',
11
+ classes,
12
+ ariaLabel,
13
+ onclick,
14
+ onkeydown
15
+ }: StepProperties = $props();
5
16
 
6
- function handleStepClick() {
17
+ const handleStepClick = (): void => {
7
18
  onclick?.({ selectedIndex: stepIndex });
8
- }
19
+ };
20
+
21
+ const handleKeydown = (event: KeyboardEvent): void => {
22
+ if (event.key === 'Enter' || event.key === ' ') {
23
+ event.preventDefault();
24
+ handleStepClick();
25
+ }
26
+ onkeydown?.(event);
27
+ };
28
+
29
+ let isVertical = $derived(orientation === 'vertical');
30
+
31
+ let stepClass = $derived(
32
+ ['step', isVertical ? 'step-vertical' : '', classes ?? ''].filter((c) => c.length > 0).join(' ')
33
+ );
9
34
  </script>
10
35
 
11
- <div class="step {classes ?? ''}" onclick={handleStepClick} {onkeydown} role="button" tabindex="0">
36
+ <div
37
+ class={stepClass}
38
+ onclick={handleStepClick}
39
+ onkeydown={handleKeydown}
40
+ role="button"
41
+ tabindex="0"
42
+ aria-label={ariaLabel ?? null}
43
+ aria-labelledby={ariaLabel ? null : `step-label-${stepIndex}`}
44
+ aria-current={status === 'active' ? 'step' : null}
45
+ >
12
46
  {#if typeof icon === 'string' && icon.length > 0}
13
47
  <div class="step-icon-container">
14
48
  <img class="step-icon" src={icon} alt="" />
15
49
  </div>
50
+ {:else if status === 'in-progress'}
51
+ <div class="step-index-container">
52
+ <svg
53
+ class="step-spinner"
54
+ viewBox="0 0 24 24"
55
+ fill="none"
56
+ xmlns="http://www.w3.org/2000/svg"
57
+ aria-hidden="true"
58
+ >
59
+ <circle
60
+ cx="12"
61
+ cy="12"
62
+ r="9"
63
+ stroke="currentColor"
64
+ stroke-opacity="0.25"
65
+ stroke-width="3"
66
+ />
67
+ <path
68
+ d="M12 3a9 9 0 0 1 9 9"
69
+ stroke="currentColor"
70
+ stroke-width="3"
71
+ stroke-linecap="round"
72
+ />
73
+ </svg>
74
+ </div>
16
75
  {:else}
17
76
  <div class="step-index-container">
18
77
  <div class="step-index-text">
@@ -20,9 +79,17 @@
20
79
  </div>
21
80
  </div>
22
81
  {/if}
23
- <div class="step-text">
82
+
83
+ <div class="step-text" id="step-label-{stepIndex}">
24
84
  {label}
25
85
  </div>
86
+
87
+ {#if typeof badge === 'function'}
88
+ <div class="step-badge">
89
+ {@render badge()}
90
+ </div>
91
+ {/if}
92
+
26
93
  <div class="separator"></div>
27
94
  </div>
28
95
 
@@ -33,6 +100,11 @@
33
100
  align-items: center;
34
101
  }
35
102
 
103
+ .step-vertical {
104
+ --step-flex-direction: column;
105
+ align-items: flex-start;
106
+ }
107
+
36
108
  .step-index-container {
37
109
  display: flex;
38
110
  justify-content: center;
@@ -41,33 +113,110 @@
41
113
  width: var(--step-index-container-width, 30px);
42
114
  border-radius: var(--step-index-container-radius, 50%);
43
115
  background-color: var(--step-index-container-background-color, #798fa5cc);
116
+ color: var(--step-index-color, white);
117
+ flex-shrink: 0;
118
+ }
119
+
120
+ .step-icon-container {
121
+ display: flex;
122
+ justify-content: center;
123
+ align-items: center;
124
+ height: var(--step-index-container-height, 30px);
125
+ width: var(--step-index-container-width, 30px);
126
+ border-radius: var(--step-index-container-radius, 50%);
127
+ background-color: var(--step-index-container-background-color, #798fa5cc);
128
+ flex-shrink: 0;
129
+ overflow: hidden;
130
+ }
131
+
132
+ .step-icon {
133
+ width: var(--step-icon-size, 18px);
134
+ height: var(--step-icon-size, 18px);
135
+ object-fit: contain;
136
+ }
137
+
138
+ .step-spinner {
139
+ width: var(--step-spinner-size, 18px);
140
+ height: var(--step-spinner-size, 18px);
141
+ animation: stepper-spin 0.8s linear infinite;
142
+ }
143
+
144
+ @keyframes stepper-spin {
145
+ from {
146
+ transform: rotate(0deg);
147
+ }
148
+ to {
149
+ transform: rotate(360deg);
150
+ }
44
151
  }
45
152
 
46
153
  .separator {
47
- display: var(--separator-display, block);
48
- height: var(--separator-height, 1px);
49
- width: var(--separator-width, 50px);
50
- margin: var(--separator-margin, 0px 12px 0px 12px);
154
+ display: var(--stepper-separator-display, var(--separator-display, block));
155
+ height: var(--stepper-separator-height, var(--separator-height, 1px));
156
+ width: var(--stepper-separator-width, var(--separator-width, 50px));
157
+ margin: var(--stepper-separator-margin, var(--separator-margin, 0px 12px 0px 12px));
51
158
  background-image: var(
52
- --separator-background-image,
53
- repeating-linear-gradient(
54
- to right,
55
- var(--separator-background-image-color, #798fa5cc),
56
- var(--separator-background-image-color, #798fa5cc) 6px,
57
- transparent 6px,
58
- transparent 10px
159
+ --stepper-separator-background-image,
160
+ var(
161
+ --separator-background-image,
162
+ repeating-linear-gradient(
163
+ to right,
164
+ var(
165
+ --stepper-separator-background-image-color,
166
+ var(--separator-background-image-color, #798fa5cc)
167
+ ),
168
+ var(
169
+ --stepper-separator-background-image-color,
170
+ var(--separator-background-image-color, #798fa5cc)
171
+ )
172
+ 6px,
173
+ transparent 6px,
174
+ transparent 10px
175
+ )
59
176
  )
60
177
  );
61
178
  }
62
179
 
180
+ .step-vertical .separator {
181
+ height: var(--stepper-separator-vertical-height, 32px);
182
+ width: var(--stepper-separator-vertical-width, 1px);
183
+ margin: var(--stepper-separator-vertical-margin, 4px 0px 4px 14px);
184
+ background-image: repeating-linear-gradient(
185
+ to bottom,
186
+ var(
187
+ --stepper-separator-background-image-color,
188
+ var(--separator-background-image-color, #798fa5cc)
189
+ ),
190
+ var(
191
+ --stepper-separator-background-image-color,
192
+ var(--separator-background-image-color, #798fa5cc)
193
+ )
194
+ 6px,
195
+ transparent 6px,
196
+ transparent 10px
197
+ );
198
+ }
199
+
63
200
  .step-text {
64
201
  margin: var(--step-text-margin, 0px 0px 0px 12px);
65
202
  font-size: var(--step-text-font-size, 12px);
66
203
  color: var(--step-text-color, #798fa5cc);
67
204
  }
68
205
 
206
+ .step-vertical .step-text {
207
+ margin: var(--step-text-vertical-margin, 4px 0px 0px 0px);
208
+ }
209
+
69
210
  .step-index-text {
70
211
  font-size: var(--step-index-font-size, 14px);
71
212
  color: var(--step-index-color, white);
72
213
  }
214
+
215
+ .step-badge {
216
+ margin: var(--step-badge-margin, 0 0 0 4px);
217
+ }
218
+
219
+ .step-vertical .step-badge {
220
+ margin: var(--step-badge-vertical-margin, 4px 0 0 0);
221
+ }
73
222
  </style>
@@ -1,22 +1,57 @@
1
1
  <script lang="ts">
2
- import type { StepperProperties } from './properties';
2
+ import type { StepperProperties, StepStatus } from './properties';
3
3
  import Step from './Step.svelte';
4
4
 
5
- let { steps, currentStepIndex, classes, onhandleStepClick }: StepperProperties = $props();
5
+ let {
6
+ steps,
7
+ currentStepIndex,
8
+ orientation = 'horizontal',
9
+ classes,
10
+ testId,
11
+ onstepclick,
12
+ onhandleStepClick
13
+ }: StepperProperties = $props();
14
+
15
+ const resolveStatus = (stepIndex: number, explicitStatus: StepStatus | null): StepStatus => {
16
+ if (explicitStatus !== null) {
17
+ return explicitStatus;
18
+ }
19
+ if (stepIndex < currentStepIndex) {
20
+ return 'completed';
21
+ }
22
+ if (stepIndex === currentStepIndex) {
23
+ return 'active';
24
+ }
25
+ return 'pending';
26
+ };
27
+
28
+ // Support the deprecated onhandleStepClick alias — onstepclick takes priority.
29
+ const effectiveStepClick = $derived(onstepclick ?? onhandleStepClick);
30
+
31
+ let containerClass = $derived(
32
+ ['container', orientation === 'vertical' ? 'container-vertical' : '', classes ?? '']
33
+ .filter((c) => c.length > 0)
34
+ .join(' ')
35
+ );
6
36
  </script>
7
37
 
8
- <div class="container {classes ?? ''}">
38
+ <div class={containerClass} data-pw={typeof testId === 'string' ? testId : null} role="list">
9
39
  {#each steps as currentStep, stepIndex (stepIndex)}
40
+ {@const effectiveStatus = resolveStatus(stepIndex, currentStep.status ?? null)}
10
41
  <div
11
- class:active-step={currentStepIndex === stepIndex}
12
- class:completed-step={currentStepIndex > stepIndex}
13
- class="step-container"
42
+ role="listitem"
43
+ class="step-container status-{effectiveStatus} {effectiveStatus === 'active'
44
+ ? 'active-step'
45
+ : ''} {effectiveStatus === 'completed' ? 'completed-step' : ''}"
14
46
  >
15
47
  <Step
16
- onclick={onhandleStepClick}
48
+ onclick={effectiveStepClick}
17
49
  label={currentStep.label}
18
50
  icon={currentStep.icon}
19
51
  stepIndex={stepIndex + 1}
52
+ status={effectiveStatus}
53
+ badge={currentStep.badge}
54
+ {orientation}
20
55
  />
21
56
  </div>
22
57
  {/each}
@@ -29,8 +64,13 @@
29
64
  align-items: center;
30
65
  }
31
66
 
67
+ .container-vertical {
68
+ --container-flex-direction: column;
69
+ align-items: flex-start;
70
+ }
71
+
32
72
  .step-container:last-child {
33
- --separator-display: none;
73
+ --stepper-separator-display: none;
34
74
  }
35
75
 
36
76
  .step-container {
@@ -38,21 +78,81 @@
38
78
  align-items: center;
39
79
  }
40
80
 
41
- .active-step {
81
+ /* status-completed */
82
+ .status-completed {
83
+ --step-text-color: var(--step-text-completed-color, #24aa5a);
84
+ --stepper-separator-background-image-color: var(
85
+ --stepper-separator-background-image-completed-color,
86
+ #24aa5a
87
+ );
88
+ --step-index-container-background-color: var(
89
+ --step-index-container-completed-background-color,
90
+ #24aa5a
91
+ );
92
+ }
93
+
94
+ /* status-active */
95
+ .status-active {
42
96
  --step-text-color: var(--step-text-active-color, #2f3841);
43
- --separator-background-image-color: var(--separator-background-image-active-color, #2f3841);
97
+ --stepper-separator-background-image-color: var(
98
+ --stepper-separator-background-image-active-color,
99
+ #2f3841
100
+ );
44
101
  --step-index-container-background-color: var(
45
102
  --step-index-container-active-background-color,
46
103
  #2f3841
47
104
  );
48
105
  }
49
106
 
50
- .completed-step {
51
- --step-text-color: var(--step-text-completed-color, #24aa5a);
52
- --separator-background-image-color: var(--separator-background-image-completed-color, #24aa5a);
107
+ /* status-pending: no override — falls through to #798fa5cc grey default in Step.svelte */
108
+
109
+ /* status-failure */
110
+ .status-failure {
111
+ --step-text-color: var(--step-text-failure-color, #e53935);
112
+ --stepper-separator-background-image-color: var(
113
+ --stepper-separator-background-image-failure-color,
114
+ #e53935
115
+ );
53
116
  --step-index-container-background-color: var(
54
- --step-index-container-completed-background-color,
55
- #24aa5a
117
+ --step-index-container-failure-background-color,
118
+ var(--stepper-status-failure-color, #e53935)
119
+ );
120
+ }
121
+
122
+ /* status-in-progress */
123
+ .status-in-progress {
124
+ --step-text-color: var(--step-text-in-progress-color, #f59e0b);
125
+ --stepper-separator-background-image-color: var(
126
+ --stepper-separator-background-image-in-progress-color,
127
+ #f59e0b
128
+ );
129
+ --step-index-container-background-color: var(
130
+ --step-index-container-in-progress-background-color,
131
+ var(--stepper-status-in-progress-color, #f59e0b)
132
+ );
133
+ }
134
+
135
+ :global([theme='dark']) .status-failure {
136
+ --step-text-color: var(--step-text-failure-color, #ef5350);
137
+ --stepper-separator-background-image-color: var(
138
+ --stepper-separator-background-image-failure-color,
139
+ #ef5350
140
+ );
141
+ --step-index-container-background-color: var(
142
+ --step-index-container-failure-background-color,
143
+ var(--stepper-status-failure-color, #ef5350)
144
+ );
145
+ }
146
+
147
+ :global([theme='dark']) .status-in-progress {
148
+ --step-text-color: var(--step-text-in-progress-color, #fbbf24);
149
+ --stepper-separator-background-image-color: var(
150
+ --stepper-separator-background-image-in-progress-color,
151
+ #fbbf24
152
+ );
153
+ --step-index-container-background-color: var(
154
+ --step-index-container-in-progress-background-color,
155
+ var(--stepper-status-in-progress-color, #fbbf24)
56
156
  );
57
157
  }
58
158
  </style>
@@ -1,21 +1,44 @@
1
- export type StepperProperties = {
1
+ import type { Snippet } from 'svelte';
2
+ export type StepStatus = 'completed' | 'active' | 'pending' | 'failure' | 'in-progress';
3
+ export type Step = {
4
+ label: string;
5
+ /**
6
+ * URL of a custom icon image. When provided, the icon takes precedence over
7
+ * any `status`-driven rendering (including the `in-progress` spinner).
8
+ */
9
+ icon?: string;
10
+ status?: StepStatus;
11
+ /**
12
+ * Optional Svelte snippet rendered inline after the step label (to its right
13
+ * in horizontal layout, below it in vertical layout). Use it for badges,
14
+ * tags, or other inline metadata — e.g. a count pill, a status chip, or a
15
+ * "New" label.
16
+ */
17
+ badge?: Snippet;
18
+ };
19
+ export type MandatoryStepperProperties = {
2
20
  steps: Array<Step>;
3
21
  currentStepIndex: number;
22
+ };
23
+ export type OptionalStepperProperties = {
24
+ orientation?: 'horizontal' | 'vertical';
4
25
  classes?: string;
26
+ testId?: string;
27
+ };
28
+ export type StepperEventProperties = {
29
+ onstepclick?: (event: {
30
+ selectedIndex: number;
31
+ }) => void;
32
+ /** @deprecated Use `onstepclick` instead. */
5
33
  onhandleStepClick?: (event: {
6
34
  selectedIndex: number;
7
35
  }) => void;
8
36
  };
9
- export type Step = {
10
- label: string;
11
- icon?: string;
12
- };
13
- export type StepProperties = OptionalStepProperties & StepEventProperties & {
14
- stepIndex: number;
15
- label: string;
16
- };
37
+ export type StepperProperties = MandatoryStepperProperties & OptionalStepperProperties & StepperEventProperties;
17
38
  export type OptionalStepProperties = {
18
39
  icon?: string;
40
+ status?: StepStatus;
41
+ badge?: Snippet;
19
42
  classes?: string;
20
43
  };
21
44
  export type StepEventProperties = {
@@ -24,3 +47,16 @@ export type StepEventProperties = {
24
47
  }) => void;
25
48
  onkeydown?: (event: KeyboardEvent) => void;
26
49
  };
50
+ export type StepProperties = OptionalStepProperties & StepEventProperties & {
51
+ /** 1-based display index — Stepper passes `stepIndex + 1` so that clicking step 1 returns `selectedIndex: 1`. */
52
+ stepIndex: number;
53
+ label: string;
54
+ orientation?: 'horizontal' | 'vertical';
55
+ /**
56
+ * Accessible label for the step button. When provided, set as `aria-label` on
57
+ * the `role="button"` element, overriding the default `aria-labelledby` association
58
+ * with the visible label text. Use when the visible label alone is insufficient
59
+ * context for screen reader users (e.g. "Step 1 — Cart (completed)").
60
+ */
61
+ ariaLabel?: string;
62
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/svelte-ui-components",
3
- "version": "2.61.0",
3
+ "version": "2.62.0",
4
4
  "description": "A themeable Svelte 5 UI component library with CSS custom property driven styling",
5
5
  "keywords": [
6
6
  "svelte",