funda-ui 4.7.108 → 4.7.115

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,5 +1,4 @@
1
1
 
2
-
3
2
  /* ======================================================
4
3
  <!-- Stepper -->
5
4
  /* ====================================================== */
@@ -18,9 +17,11 @@
18
17
  --stepper-line-default: #dfdfdf;
19
18
  --stepper-line-active: #2563eb;
20
19
  --stepper-line-complete: #22c55e;
21
- --stepper-indicator-size: 0.875rem;
22
- --stepper-indicator-offset: 100px;
20
+ --stepper-indicator-font-size: 0.875rem;
21
+ --stepper-indicator-offset: 60px;
23
22
  --stepper-title-size: 0.875rem;
23
+ --stepper-indicator-size: 32px;
24
+
24
25
 
25
26
 
26
27
  position: relative;
@@ -31,25 +32,27 @@
31
32
  align-items: flex-start;
32
33
  flex-wrap: nowrap;
33
34
  position: relative;
35
+ max-width: calc(100% - var(--stepper-indicator-size));
34
36
 
35
37
  /* background line */
36
38
  &::before {
37
39
  content: '';
38
40
  position: absolute;
39
41
  top: 16px; /* Subtract the height of the title */
40
- left: 14px;
42
+ left: 0;
41
43
  right: 0;
42
44
  height: 2px;
43
45
  background-color: var(--stepper-line-default);
44
46
  z-index: 1;
45
- width: calc(100% - 32px);
47
+ width: 100%;
48
+
46
49
  }
47
50
 
48
51
  &::after {
49
52
  content: '';
50
53
  position: absolute;
51
54
  top: 16px; /* Subtract the height of the title */
52
- left: 14px;
55
+ left: 0;
53
56
  height: 2px;
54
57
  background-color: var(--stepper-line-complete);
55
58
  z-index: 2;
@@ -59,7 +62,7 @@
59
62
 
60
63
  &::after {
61
64
  width: var(--stepper-progress-width, 0%);
62
- max-width: calc(100% - 32px);
65
+ max-width: 100%;
63
66
  }
64
67
  }
65
68
 
@@ -69,7 +72,7 @@
69
72
  display: flex;
70
73
  flex-direction: column;
71
74
  align-items: center;
72
- max-width: var(--stepper-indicator-offset);
75
+ max-width: 0;
73
76
  position: relative;
74
77
  z-index: 3;
75
78
 
@@ -123,15 +126,15 @@
123
126
 
124
127
  /* Indicator */
125
128
  .step-indicator {
126
- width: 32px;
127
- height: 32px;
129
+ width: var(--stepper-indicator-size);
130
+ height: var(--stepper-indicator-size);
128
131
  margin: 0 auto 0.25rem;
129
132
  border-radius: 9999px;
130
133
  display: flex;
131
134
  align-items: center;
132
135
  justify-content: center;
133
136
  border: 2px solid #ccc;
134
- font-size: var(--stepper-indicator-size);
137
+ font-size: var(--stepper-indicator-font-size);
135
138
 
136
139
  /* default */
137
140
  background-color: var(--stepper-indicator-default);
@@ -155,13 +158,19 @@
155
158
 
156
159
  /* Title */
157
160
  .step-title {
161
+ white-space: nowrap;
158
162
  font-size: var(--stepper-title-size);
163
+ margin-left: calc(var(--stepper-indicator-offset) / 2);
159
164
 
160
165
  /* default */
161
166
  color: var(--stepper-color-default);
162
167
 
163
168
  &--active {
164
- font-weight: bold;
169
+ color: var(--stepper-indicator-active);
170
+ }
171
+
172
+ &--complete {
173
+ color: var(--stepper-indicator-complete);
165
174
  }
166
175
  }
167
176
 
@@ -201,20 +210,20 @@
201
210
  /*------ Verticle ------*/
202
211
  .stepper-container.stepper-container--vertical {
203
212
 
204
- --stepper-indicator-offset: 50px;
205
213
 
206
214
  display: flex;
207
215
  flex-direction: column;
208
- gap: 1rem; /* line length */
216
+ gap: 2.5rem; /* line length */
217
+ max-height: calc(100% - var(--stepper-indicator-size));
209
218
 
210
219
  /* background line */
211
220
  &::before {
212
221
  content: '';
213
222
  position: absolute;
214
- top: 20px;
215
- left: 24px;
223
+ top: 0;
224
+ left: calc(var(--stepper-indicator-size) / 2);
216
225
  width: 2px;
217
- height: calc(100% - 32px);
226
+ height: 100%;
218
227
  background-color: var(--stepper-line-default);
219
228
  z-index: 1;
220
229
  }
@@ -222,8 +231,8 @@
222
231
  &::after {
223
232
  content: '';
224
233
  position: absolute;
225
- top: 20px;
226
- left: 24px;
234
+ top: 0;
235
+ left: calc(var(--stepper-indicator-size) / 2);
227
236
  width: 2px;
228
237
  background-color: var(--stepper-line-complete);
229
238
  z-index: 2;
@@ -233,7 +242,7 @@
233
242
 
234
243
  &::after {
235
244
  height: var(--stepper-progress-height, 0%);
236
- max-height: calc(100% - 32px);
245
+ max-height: 100%;
237
246
  }
238
247
 
239
248
 
@@ -244,12 +253,13 @@
244
253
  }
245
254
  .vertical-step-left {
246
255
  flex-shrink: 0;
247
- width: var(--stepper-indicator-offset);
256
+ width: var(--stepper-indicator-size);
257
+ height: var(--stepper-indicator-size);
248
258
  position: relative;
249
259
 
250
260
  /* Main Navigation - Each step item (with circle + title) */
251
261
  .step-item {
252
- margin-top: 20px;
262
+ margin-top: 0;
253
263
  }
254
264
 
255
265
  /* Line */
@@ -266,7 +276,7 @@
266
276
  left: 70px;
267
277
  width: calc(100% - 70px);
268
278
  position: absolute;
269
- top: 1.5rem;
279
+ top: 0; /* required */
270
280
  }
271
281
 
272
282
  /* Title */
@@ -131,7 +131,8 @@ const Stepper = forwardRef<StepperRef, StepperProps>((props, ref) => {
131
131
  className={combinedCls(
132
132
  'step-title',
133
133
  {
134
- 'step-title--active': isActive
134
+ 'step-title--active': isActive,
135
+ 'step-title--complete': isCompleted
135
136
  }
136
137
  )}
137
138
  >
@@ -168,7 +169,16 @@ const Stepper = forwardRef<StepperRef, StepperProps>((props, ref) => {
168
169
  const isCompleted = index < activeIndex || (index === panels.length - 1 && isLastStepComplete);
169
170
 
170
171
  return (
171
- <div key={index} className="vertical-step-row">
172
+ <div
173
+ key={index}
174
+ className={combinedCls(
175
+ 'vertical-step-row',
176
+ {
177
+ 'vertical-step-row--active': isActive,
178
+ 'vertical-step-row--complete': isCompleted
179
+ }
180
+ )}
181
+ >
172
182
  {/* Left */}
173
183
  <div className="vertical-step-left">
174
184
  <div
@@ -199,7 +209,8 @@ const Stepper = forwardRef<StepperRef, StepperProps>((props, ref) => {
199
209
  className={combinedCls(
200
210
  'step-title',
201
211
  {
202
- 'step-title--active': isActive
212
+ 'step-title--active': isActive,
213
+ 'step-title--complete': isCompleted
203
214
  }
204
215
  )}
205
216
  >
@@ -246,6 +257,8 @@ const Stepper = forwardRef<StepperRef, StepperProps>((props, ref) => {
246
257
  '--stepper-progress-height': `${progress}%`
247
258
  } as React.CSSProperties;
248
259
  } else {
260
+
261
+ const defaultProgress = (activeIndex / (panels.length - 1)) * 100;
249
262
  const firstItem = stepItems[0] as HTMLDivElement;
250
263
  const lastItem = stepItems[stepItems.length - 1] as HTMLDivElement;
251
264
  if (!firstItem || !lastItem) return {};
@@ -258,10 +271,10 @@ const Stepper = forwardRef<StepperRef, StepperProps>((props, ref) => {
258
271
  if (!currentItem) return {};
259
272
 
260
273
  const currentCenter = currentItem.offsetLeft + (currentItem.clientWidth / 2);
261
- const progress = ((currentCenter - firstCenter) / totalWidth) * 100;
274
+ const progress = defaultProgress !== 0 && activeIndex > 0 ? defaultProgress : ((currentCenter - firstCenter) / totalWidth) * 100;
262
275
 
263
276
  return {
264
- '--stepper-progress-width': `${progress}%`
277
+ '--stepper-progress-width': `${isNaN(progress) ? 0 : progress}%`
265
278
  } as React.CSSProperties;
266
279
  }
267
280
  };
@@ -0,0 +1,403 @@
1
+ /**
2
+ * History Tracker
3
+ *
4
+ * @usage:
5
+
6
+ const App = () => {
7
+ const {
8
+ history,
9
+ forwardHistory,
10
+ currentUrl,
11
+ firstUrl,
12
+ clearHistory,
13
+ goBack
14
+ } = useHistoryTracker({
15
+ onChange: ({
16
+ isReady,
17
+ history,
18
+ forwardHistory,
19
+ currentUrl,
20
+ firstUrl,
21
+ canGoBack,
22
+ canGoForward
23
+ } : {
24
+ isReady: boolean;
25
+ history: string[];
26
+ forwardHistory: string[];
27
+ currentUrl: string;
28
+ firstUrl: string;
29
+ canGoBack: boolean;
30
+ canGoForward: boolean;
31
+ }) => {
32
+ console.log('--> onChange: ',
33
+ isReady,
34
+ history,
35
+ forwardHistory,
36
+ currentUrl,
37
+ firstUrl,
38
+ canGoBack,
39
+ canGoForward
40
+ );
41
+ }
42
+ });
43
+
44
+ return (
45
+ <div>
46
+
47
+ <div>
48
+ <h3>First URL:</h3>
49
+ <p>{firstUrl}</p>
50
+ </div>
51
+
52
+ <div>
53
+ <h3>Current URL:</h3>
54
+ <p>{currentUrl}</p>
55
+ </div>
56
+
57
+ <div>
58
+ <h3>History ({history.length}):</h3>
59
+ <ul>
60
+ {history.map((url, index) => (
61
+ <li key={index}>{url}</li>
62
+ ))}
63
+ </ul>
64
+ </div>
65
+
66
+ <div>
67
+ <h3>Forward History ({forwardHistory.length}):</h3>
68
+ <ul>
69
+ {forwardHistory.map((url, index) => (
70
+ <li key={index}>{url}</li>
71
+ ))}
72
+ </ul>
73
+ </div>
74
+
75
+
76
+
77
+ <button onClick={clearHistory}>
78
+ Clear History
79
+ </button>
80
+
81
+ <button onClick={async () => {
82
+ try {
83
+ const {
84
+ isReady,
85
+ history,
86
+ forwardHistory,
87
+ canGoBack,
88
+ canGoForward
89
+ } : {
90
+ isReady: boolean;
91
+ history: string[];
92
+ forwardHistory: string[];
93
+ canGoBack: boolean;
94
+ canGoForward: boolean;
95
+ } = await goBack();
96
+
97
+ console.log('--> goBack: ',
98
+ isReady,
99
+ history,
100
+ forwardHistory,
101
+ currentUrl,
102
+ firstUrl,
103
+ canGoBack,
104
+ canGoForward
105
+ );
106
+ } catch (error) {
107
+ console.error('Navigation failed', error);
108
+ }
109
+
110
+ }}>
111
+ Back
112
+ </button>
113
+
114
+ </div>
115
+ );
116
+ };
117
+
118
+ */
119
+ import { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
120
+
121
+ export type UseHistoryTrackerChangeFnType = (args: {
122
+ isReady: boolean;
123
+ history: string[];
124
+ forwardHistory: string[],
125
+ currentUrl: string;
126
+ firstUrl: string;
127
+ canGoBack: boolean;
128
+ canGoForward: boolean;
129
+ }) => void;
130
+
131
+
132
+ export type UseHistoryTrackerProps = {
133
+ onChange?: UseHistoryTrackerChangeFnType | null;
134
+ };
135
+
136
+ // Create a secure version of useLayoutEffect that is downgraded to useEffect when SSR
137
+ const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
138
+
139
+ const useHistoryTracker = (props: UseHistoryTrackerProps) => {
140
+ const {
141
+ onChange
142
+ } = props;
143
+
144
+ const [isReady, setIsReady] = useState<boolean>(false);
145
+ const historyRef = useRef<string[]>([]);
146
+ const forwardHistoryRef = useRef<string[]>([]);
147
+ const firstUrlRef = useRef<string>('');
148
+ const [currentUrl, setCurrentUrl] = useState<string>('');
149
+
150
+ const initialize = useCallback(() => {
151
+ if (typeof window === 'undefined') return;
152
+
153
+ const currentLocation = window.location.href as string;
154
+
155
+ // If the history is empty, set the first record
156
+ if (historyRef.current.length === 0) {
157
+ firstUrlRef.current = currentLocation;
158
+ historyRef.current = [currentLocation];
159
+ setCurrentUrl(currentLocation);
160
+
161
+ onChange?.({
162
+ isReady: false,
163
+ history: [currentLocation],
164
+ forwardHistory: [],
165
+ currentUrl: currentLocation,
166
+ firstUrl: currentLocation,
167
+ canGoBack: false,
168
+ canGoForward: false
169
+ });
170
+
171
+ }
172
+
173
+ setIsReady(true);
174
+ }, []);
175
+
176
+ useIsomorphicLayoutEffect(() => {
177
+ initialize();
178
+ }, [initialize]);
179
+
180
+
181
+ const clearHistory = useCallback(() => {
182
+ if (typeof window === 'undefined') return;
183
+
184
+ historyRef.current = [];
185
+ forwardHistoryRef.current = [];
186
+ firstUrlRef.current = '';
187
+ setCurrentUrl('');
188
+
189
+ onChange?.({
190
+ isReady: true,
191
+ history: [],
192
+ forwardHistory: [],
193
+ currentUrl: '',
194
+ firstUrl: '',
195
+ canGoBack: false,
196
+ canGoForward: false
197
+
198
+ });
199
+ }, [onChange]); // only "onChange"
200
+
201
+ const goToHistory = useCallback((index: number) => {
202
+ if (typeof window === 'undefined') return;
203
+ if (index < 0 || index >= historyRef.current.length) return;
204
+
205
+ const targetUrl = historyRef.current[index];
206
+ if (targetUrl && targetUrl !== window.location.href) {
207
+ window.location.href = targetUrl;
208
+ }
209
+ }, []);
210
+
211
+ const goBack = useCallback(() => {
212
+ if (typeof window === 'undefined') return Promise.reject('Window is undefined');
213
+ if (historyRef.current.length <= 1) return Promise.reject('Cannot go back');
214
+
215
+ return new Promise((resolve) => {
216
+ // Moves the current URL into the forward history
217
+ const removedUrl = historyRef.current.pop() as string;
218
+ forwardHistoryRef.current.push(removedUrl);
219
+
220
+ const newCurrentUrl = historyRef.current[historyRef.current.length - 1];
221
+ setCurrentUrl(newCurrentUrl);
222
+
223
+ // Create initial data object
224
+ const data = {
225
+ isReady: true,
226
+ history: [...historyRef.current],
227
+ forwardHistory: [...forwardHistoryRef.current],
228
+ currentUrl: newCurrentUrl,
229
+ firstUrl: firstUrlRef.current,
230
+ canGoBack: canGoBack(),
231
+ canGoForward: canGoForward()
232
+ };
233
+
234
+ // Notify about the history change
235
+ onChange?.(data);
236
+
237
+ // Create one-time listener for popstate
238
+ const handlePopState = () => {
239
+ // Remove the listener after it's called
240
+ window.removeEventListener('popstate', handlePopState);
241
+
242
+ // Get the final data after URL has changed
243
+ const finalData = {
244
+ isReady: true,
245
+ history: [...historyRef.current],
246
+ forwardHistory: [...forwardHistoryRef.current],
247
+ currentUrl: window.location.href,
248
+ firstUrl: firstUrlRef.current,
249
+ canGoBack: canGoBack(),
250
+ canGoForward: canGoForward()
251
+ };
252
+
253
+ resolve(finalData);
254
+ };
255
+
256
+ // Add the listener
257
+ window.addEventListener('popstate', handlePopState);
258
+
259
+ // Trigger the navigation
260
+ window.history.go(-1);
261
+ });
262
+ }, [onChange]);
263
+
264
+ const goForward = useCallback(() => {
265
+ if (typeof window === 'undefined') return Promise.reject('Window is undefined');
266
+ if (forwardHistoryRef.current.length === 0) return Promise.reject('Cannot go forward');
267
+
268
+ return new Promise((resolve) => {
269
+ // Take the URL from the forward history and add it to the main history
270
+ const nextUrl = forwardHistoryRef.current.pop() as string;
271
+ historyRef.current.push(nextUrl);
272
+ setCurrentUrl(nextUrl);
273
+
274
+ // Create initial data object
275
+ const data = {
276
+ isReady: true,
277
+ history: [...historyRef.current],
278
+ forwardHistory: [...forwardHistoryRef.current],
279
+ currentUrl: nextUrl,
280
+ firstUrl: firstUrlRef.current,
281
+ canGoBack: canGoBack(),
282
+ canGoForward: canGoForward()
283
+ };
284
+
285
+ onChange?.(data);
286
+
287
+ // Create one-time listener for popstate
288
+ const handlePopState = () => {
289
+ // Remove the listener after it's called
290
+ window.removeEventListener('popstate', handlePopState);
291
+
292
+ // Get the final data after URL has changed
293
+ const finalData = {
294
+ isReady: true,
295
+ history: [...historyRef.current],
296
+ forwardHistory: [...forwardHistoryRef.current],
297
+ currentUrl: window.location.href,
298
+ firstUrl: firstUrlRef.current,
299
+ canGoBack: canGoBack(),
300
+ canGoForward: canGoForward()
301
+ };
302
+
303
+ resolve(finalData);
304
+ };
305
+
306
+ // Add the listener
307
+ window.addEventListener('popstate', handlePopState);
308
+
309
+ // Trigger the navigation
310
+ window.history.go(1);
311
+ });
312
+ }, [onChange]);
313
+
314
+ const canGoBack = useCallback(() => {
315
+ return historyRef.current.length > 1;
316
+ }, []);
317
+
318
+ const canGoForward = useCallback(() => {
319
+ return forwardHistoryRef.current.length > 0;
320
+ }, []);
321
+
322
+
323
+ const handleUrlChange = useCallback(() => {
324
+ if (typeof window === 'undefined') return;
325
+
326
+ const newUrl = window.location.href;
327
+
328
+ // If the history is empty, set to the first URL
329
+ if (historyRef.current.length === 0) {
330
+ firstUrlRef.current = newUrl;
331
+ }
332
+
333
+ // Avoid recording the same URL
334
+ if (historyRef.current[historyRef.current.length - 1] !== newUrl) {
335
+ historyRef.current.push(newUrl);
336
+
337
+ // Clear the advance history, as new navigation invalidates the advance history
338
+ forwardHistoryRef.current = [];
339
+ setCurrentUrl(newUrl);
340
+
341
+ onChange?.({
342
+ isReady: true,
343
+ history: [...historyRef.current],
344
+ forwardHistory: [...forwardHistoryRef.current],
345
+ currentUrl: newUrl,
346
+ firstUrl: firstUrlRef.current || newUrl, // Make sure there is always a value
347
+ canGoBack: canGoBack(),
348
+ canGoForward: canGoForward()
349
+ });
350
+ }
351
+ }, [onChange]); // only "onChange"
352
+
353
+
354
+ useEffect(() => {
355
+ if (typeof window === 'undefined') return;
356
+
357
+ // Listen for popstate events (browser forward/back)
358
+ window.addEventListener('popstate', handleUrlChange);
359
+
360
+ // Listen for hashchange events
361
+ window.addEventListener('hashchange', handleUrlChange);
362
+
363
+ // Listen for DOM and property changes
364
+ const observer = new MutationObserver((mutations) => {
365
+ mutations.forEach((mutation) => {
366
+ if (mutation.type === 'childList' || mutation.type === 'attributes') {
367
+ handleUrlChange();
368
+ }
369
+ });
370
+ });
371
+
372
+
373
+ observer.observe(document.body, {
374
+ childList: true, // monitor the addition and deletion of child nodes
375
+ subtree: true, // monitor all descendant nodes
376
+ attributes: true, // monitor attribute changes
377
+ attributeFilter: ['href'] // only monitor changes in the href attribute
378
+ });
379
+
380
+ return () => {
381
+ window.removeEventListener('popstate', handleUrlChange);
382
+ window.removeEventListener('hashchange', handleUrlChange);
383
+ observer.disconnect();
384
+ };
385
+ }, [handleUrlChange]);
386
+
387
+
388
+ return {
389
+ isReady,
390
+ history: historyRef.current,
391
+ forwardHistory: forwardHistoryRef.current,
392
+ currentUrl,
393
+ firstUrl: firstUrlRef.current,
394
+ clearHistory,
395
+ goToHistory,
396
+ goBack,
397
+ goForward,
398
+ canGoBack,
399
+ canGoForward
400
+ };
401
+ };
402
+
403
+ export default useHistoryTracker;
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.7.108",
5
+ "version": "4.7.115",
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",
@@ -1,2 +0,0 @@
1
- declare const useGlobalUrlListener: () => string;
2
- export default useGlobalUrlListener;