funda-ui 4.6.399 → 4.7.103

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.
@@ -0,0 +1,312 @@
1
+ import React, { useRef, useEffect, forwardRef, useImperativeHandle, useState } from 'react';
2
+
3
+ import { clsWrite, combinedCls } from 'funda-utils/dist/cjs/cls';
4
+
5
+
6
+ interface StepperPanelProps {
7
+ header: React.ReactNode;
8
+ children?: React.ReactNode;
9
+ style?: React.CSSProperties;
10
+ }
11
+
12
+ interface StepperProps {
13
+ wrapperClassName?: string;
14
+ indicatorClickAllowed?: boolean;
15
+ style?: React.CSSProperties;
16
+ initialStep?: number;
17
+ layout?: 'horizontal' | 'vertical';
18
+ completeIcon?: React.ReactNode;
19
+ disableCompleteIcon?: boolean;
20
+ onChange?: (index: number, isLastStepComplete: boolean) => void;
21
+ children: React.ReactElement<StepperPanelProps>[];
22
+ }
23
+
24
+ interface StepperRef {
25
+ goto: (index: number) => void;
26
+ next: () => void;
27
+ prev: () => void;
28
+ }
29
+
30
+ const StepperPanel: React.FC<StepperPanelProps> = (props) => {
31
+ const {
32
+ header,
33
+ children,
34
+ style
35
+ } = props;
36
+
37
+ return (
38
+ <div className="stepper-panel" style={style}>
39
+ <div className="stepper-panel-header">{header}</div>
40
+ <div className="stepper-panel-content">{children}</div>
41
+ </div>
42
+ );
43
+ };
44
+
45
+ const Stepper = forwardRef<StepperRef, StepperProps>((props, ref) => {
46
+ const {
47
+ wrapperClassName,
48
+ indicatorClickAllowed = false,
49
+ style,
50
+ initialStep = 0,
51
+ layout = 'horizontal',
52
+ completeIcon = <><svg width="20px" height="20px" viewBox="0 0 20 20" fill="none"><path d="M15.3742 5.98559L10.3742 14.9856C9.72664 16.1511 7.97832 15.1798 8.62585 14.0143L13.6258 5.01431C14.2734 3.84876 16.0217 4.82005 15.3742 5.98559Z" fill="#fff"/><path d="M5.1247 9.71907L10.1247 13.7191C11.1659 14.552 9.91646 16.1137 8.87531 15.2808L3.87531 11.2808C2.83415 10.4479 4.08354 8.88615 5.1247 9.71907Z" fill="#fff"/></svg></>,
53
+ disableCompleteIcon = true,
54
+ onChange,
55
+ children
56
+ } = props;
57
+
58
+
59
+ const rootRef = useRef<HTMLDivElement>(null);
60
+ const [isLastStepComplete, setIsLastStepComplete] = useState<boolean>(false);
61
+ const [activeIndex, setActiveIndex] = useState<number>(initialStep);
62
+ const panels = React.Children.toArray(children) as React.ReactElement<StepperPanelProps>[];
63
+ const isVertical: boolean = layout === 'vertical';
64
+ const prevActiveIndexRef = useRef<number>(activeIndex);
65
+ const prevIsLastStepCompleteRef = useRef<boolean>(isLastStepComplete);
66
+
67
+ useImperativeHandle(
68
+ ref,
69
+ () => ({
70
+ goto: (index: number) => {
71
+ if (index >= 0 && index < panels.length) {
72
+ setActiveIndex(index);
73
+ }
74
+ },
75
+ next: () => {
76
+ setActiveIndex((prevState) => {
77
+ const _val = Math.min(prevState + 1, panels.length - 1);
78
+ return _val;
79
+ });
80
+ },
81
+ prev: () => {
82
+ setActiveIndex((prevState) => {
83
+ const _val = Math.max(prevState - 1, 0);
84
+ return _val;
85
+ });
86
+
87
+ setIsLastStepComplete(false);
88
+ },
89
+ setLastStepComplete: (val: boolean) => {
90
+ setIsLastStepComplete(val);
91
+ },
92
+ }),
93
+ [panels.length],
94
+ );
95
+
96
+ const horizontalPanelsGenerator = (): JSX.Element => {
97
+ return <>
98
+ <div className="stepper-header">
99
+ {panels.map((panel, index) => {
100
+ const { header } = panel.props;
101
+ const isActive = index === activeIndex;
102
+ const isCompleted = index < activeIndex || (index === panels.length - 1 && isLastStepComplete);
103
+
104
+ return (
105
+ <React.Fragment key={index}>
106
+ <div
107
+ data-step-index={index}
108
+ className={combinedCls(
109
+ 'step-item',
110
+ {
111
+ 'step-item--clickable': indicatorClickAllowed && !isLastStepComplete
112
+ }
113
+ )}
114
+ onClick={indicatorClickAllowed && !isLastStepComplete ? () => {
115
+ setActiveIndex(index);
116
+ setIsLastStepComplete(false);
117
+ } : undefined}
118
+ >
119
+ <div
120
+ className={combinedCls(
121
+ 'step-indicator',
122
+ {
123
+ 'step-indicator--active': isActive,
124
+ 'step-indicator--complete': isCompleted
125
+ }
126
+ )}
127
+ >
128
+ {isCompleted ? <>{disableCompleteIcon ? index + 1 : completeIcon}</> : index + 1}
129
+ </div>
130
+ <div
131
+ className={combinedCls(
132
+ 'step-title',
133
+ {
134
+ 'step-title--active': isActive
135
+ }
136
+ )}
137
+ >
138
+ {header}
139
+ </div>
140
+ </div>
141
+ {index < panels.length - 1 && (
142
+ <div
143
+ className={combinedCls(
144
+ 'step-line',
145
+ {
146
+ 'step-line--active': index === activeIndex - 1,
147
+ 'step-line--complete': index < activeIndex - 1
148
+ }
149
+ )}
150
+ />
151
+ )}
152
+ </React.Fragment>
153
+ );
154
+ })}
155
+ </div>
156
+
157
+ <div className="stepper-panels">
158
+ {panels[activeIndex]}
159
+ </div>
160
+ </>;
161
+ };
162
+
163
+ const verticalPanelsGenerator = (): JSX.Element => {
164
+ return <>
165
+ {panels.map((panel, index) => {
166
+ const { header } = panel.props;
167
+ const isActive = index === activeIndex;
168
+ const isCompleted = index < activeIndex || (index === panels.length - 1 && isLastStepComplete);
169
+
170
+ return (
171
+ <div key={index} className="vertical-step-row">
172
+ {/* Left */}
173
+ <div className="vertical-step-left">
174
+ <div
175
+ data-step-index={index}
176
+ className={combinedCls(
177
+ 'step-item',
178
+ {
179
+ 'step-item--clickable': indicatorClickAllowed && !isLastStepComplete
180
+ }
181
+ )}
182
+ onClick={indicatorClickAllowed && !isLastStepComplete ? () => {
183
+ setActiveIndex(index);
184
+ setIsLastStepComplete(false);
185
+ } : undefined}
186
+ >
187
+ <div
188
+ className={combinedCls(
189
+ 'step-indicator',
190
+ {
191
+ 'step-indicator--active': isActive,
192
+ 'step-indicator--complete': isCompleted
193
+ }
194
+ )}
195
+ >
196
+ {isCompleted ? <>{disableCompleteIcon ? index + 1 : completeIcon}</> : index + 1}
197
+ </div>
198
+ <div
199
+ className={combinedCls(
200
+ 'step-title',
201
+ {
202
+ 'step-title--active': isActive
203
+ }
204
+ )}
205
+ >
206
+ {header}
207
+ </div>
208
+ </div>
209
+
210
+ {index < panels.length - 1 && (
211
+ <div
212
+ className={combinedCls(
213
+ 'step-line',
214
+ {
215
+ 'step-line--active': index === activeIndex - 1,
216
+ 'step-line--complete': index < activeIndex - 1
217
+ }
218
+ )}
219
+ />
220
+ )}
221
+ </div>
222
+
223
+ {/* Right */}
224
+ {isActive && (
225
+ <div className="vertical-step-right">
226
+ {panels[activeIndex]}
227
+ </div>
228
+ )}
229
+ </div>
230
+ );
231
+ })}
232
+ </>;
233
+ };
234
+
235
+ // Calculate the width/height of the progress line
236
+ const calculateProgressStyle = () => {
237
+ if (!panels.length || rootRef.current === null) return {};
238
+
239
+ const stepItems = rootRef.current.querySelectorAll('.step-item');
240
+ if (!stepItems.length) return {};
241
+
242
+ if (isVertical) {
243
+ const totalHeight = stepItems[0].clientHeight * (panels.length - 1);
244
+ const progress = (activeIndex / (panels.length - 1)) * 100;
245
+ return {
246
+ '--stepper-progress-height': `${progress}%`
247
+ } as React.CSSProperties;
248
+ } else {
249
+ const firstItem = stepItems[0] as HTMLDivElement;
250
+ const lastItem = stepItems[stepItems.length - 1] as HTMLDivElement;
251
+ if (!firstItem || !lastItem) return {};
252
+
253
+ const firstCenter = firstItem.offsetLeft + (firstItem.clientWidth / 2);
254
+ const lastCenter = lastItem.offsetLeft + (lastItem.clientWidth / 2);
255
+ const totalWidth = lastCenter - firstCenter;
256
+
257
+ const currentItem = stepItems[activeIndex] as HTMLDivElement;
258
+ if (!currentItem) return {};
259
+
260
+ const currentCenter = currentItem.offsetLeft + (currentItem.clientWidth / 2);
261
+ const progress = ((currentCenter - firstCenter) / totalWidth) * 100;
262
+
263
+ return {
264
+ '--stepper-progress-width': `${progress}%`
265
+ } as React.CSSProperties;
266
+ }
267
+ };
268
+
269
+ useEffect(() => {
270
+ // Only trigger onChange if values actually changed from previous values
271
+ if (prevActiveIndexRef.current !== activeIndex ||
272
+ prevIsLastStepCompleteRef.current !== isLastStepComplete) {
273
+
274
+ prevActiveIndexRef.current = activeIndex;
275
+ prevIsLastStepCompleteRef.current = isLastStepComplete;
276
+
277
+ onChange?.(activeIndex, isLastStepComplete);
278
+ }
279
+ }, [activeIndex, isLastStepComplete]);
280
+
281
+
282
+ useEffect(() => {
283
+ // Force a recalculation of the progress line
284
+ const timer = setTimeout(() => {
285
+ setActiveIndex(prev => prev);
286
+ }, 0);
287
+ return () => clearTimeout(timer);
288
+ }, []);
289
+
290
+ return (
291
+ <div
292
+ ref={rootRef}
293
+ className={combinedCls(
294
+ 'stepper-container',
295
+ clsWrite(wrapperClassName, ''),
296
+ {
297
+ 'stepper-container--vertical': isVertical
298
+ }
299
+ )}
300
+ style={{
301
+ ...style,
302
+ ...calculateProgressStyle()
303
+ }}
304
+ >
305
+ {!isVertical && horizontalPanelsGenerator()}
306
+ {isVertical && verticalPanelsGenerator()}
307
+ </div>
308
+ );
309
+ });
310
+
311
+ export { Stepper, StepperPanel };
312
+ export type { StepperProps, StepperPanelProps, StepperRef };
@@ -330,7 +330,7 @@
330
330
  min-width: auto !important;
331
331
  width: auto !important;
332
332
 
333
- &:not(last-child) {
333
+ &:not(:last-child) {
334
334
  border-bottom: 0;
335
335
  }
336
336
 
@@ -390,7 +390,7 @@
390
390
  border-top: 0;
391
391
  }
392
392
 
393
- &:not(last-child) {
393
+ &:not(:last-child) {
394
394
  border-bottom: 0;
395
395
  border-right: 0;
396
396
  }
package/lib/esm/index.js CHANGED
@@ -33,6 +33,7 @@ export { default as Scrollbar } from './Scrollbar';
33
33
  export { default as SearchBar } from './SearchBar';
34
34
  export { default as Select } from './Select';
35
35
  export { default as ShowMoreLess } from './ShowMoreLess';
36
+ export { default as Stepper } from './Stepper';
36
37
  export { default as Switch } from './Switch';
37
38
  export { default as Table } from './Table';
38
39
  export { default as Tabs } from './Tabs';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "UIUX Lab",
3
3
  "email": "uiuxlab@gmail.com",
4
4
  "name": "funda-ui",
5
- "version": "4.6.399",
5
+ "version": "4.7.103",
6
6
  "description": "React components using pure Bootstrap 5+ which does not contain any external style and script libraries.",
7
7
  "repository": {
8
8
  "type": "git",