dce-reactkit 5.0.0 → 5.0.2

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,274 @@
1
+ /**
2
+ * Fake progress bar that approaches completion but never fully finishes.
3
+ * Built on top of the existing ProgressBar component.
4
+ *
5
+ * Two phases:
6
+ * 1. Fast phase (0–90%): random jumps of 1–5% at randomized intervals,
7
+ * paced to reach ~90% in roughly estimatedTimeSec seconds.
8
+ * 2. Slow phase (90–99%): crawls 1% at a time with longer random delays,
9
+ * never reaching 100% on its own.
10
+ *
11
+ * When isFinished becomes true, the bar immediately jumps to 100%.
12
+ * @author Yuen Ler Chow
13
+ */
14
+
15
+ // Import React
16
+ import React, { useReducer, useEffect } from 'react';
17
+
18
+ // Import helpers
19
+ import { waitMs } from 'dce-commonkit';
20
+
21
+ // Import types
22
+ import Variant from '../types/Variant';
23
+ import ProgressBarSize from '../types/ProgressBarSize';
24
+
25
+ // Import components
26
+ import ProgressBar from './ProgressBar';
27
+
28
+ /*------------------------------------------------------------------------*/
29
+ /* ------------------------------ Constants ----------------------------- */
30
+ /*------------------------------------------------------------------------*/
31
+
32
+ // Percent where the bar switches from fast random jumps to a slow crawl
33
+ const SLOW_PHASE_START_PERCENT = 90;
34
+
35
+ // Highest percent the bar will reach on its own (before isFinished is true)
36
+ const MAX_UNFINISHED_PERCENT = 99;
37
+
38
+ // Average step size during the fast phase (uniform random from 1–5)
39
+ const AVG_FAST_PHASE_STEP = 3;
40
+
41
+ // Minimum delay (ms) between steps during the slow phase
42
+ const SLOW_PHASE_MIN_DELAY_MS = 2000;
43
+
44
+ // Maximum delay (ms) between steps during the slow phase
45
+ const SLOW_PHASE_MAX_DELAY_MS = 5000;
46
+
47
+ /*------------------------------------------------------------------------*/
48
+ /* -------------------------------- Types ------------------------------- */
49
+ /*------------------------------------------------------------------------*/
50
+
51
+ // Props definition
52
+ type Props = {
53
+ // True if the task is finished (triggers animation to 100%)
54
+ isFinished?: boolean,
55
+ // Estimated time (in seconds) to finish the task (default 8)
56
+ estimatedTimeSec?: number,
57
+ // Whether to show striped effect (default false)
58
+ striped?: boolean,
59
+ // Variant of the progress bar (default warning)
60
+ variant?: Variant,
61
+ // Background variants (default secondary subtle)
62
+ bgVariant?: Variant,
63
+ // Hide outline (default false)
64
+ showOutline?: boolean,
65
+ // Size of the progress bar (default md)
66
+ size?: ProgressBarSize,
67
+ };
68
+
69
+ /*------------------------------------------------------------------------*/
70
+ /* -------------------------------- State ------------------------------- */
71
+ /*------------------------------------------------------------------------*/
72
+
73
+ /* -------- State Definition -------- */
74
+
75
+ type State = {
76
+ // Current progress percentage (0-99)
77
+ progress: number,
78
+ };
79
+
80
+ /* ------------- Actions ------------ */
81
+
82
+ // Types of actions
83
+ enum ActionType {
84
+ // Advance the progress by a given step
85
+ Advance = 'Advance',
86
+ // Set progress to 100% (task complete)
87
+ Complete = 'Complete',
88
+ }
89
+
90
+ // Action definitions
91
+ type Action = (
92
+ | {
93
+ // Action type
94
+ type: ActionType.Advance,
95
+ // Number of percent points to advance
96
+ step: number,
97
+ }
98
+ | {
99
+ // Action type
100
+ type: ActionType.Complete,
101
+ }
102
+ );
103
+
104
+ /**
105
+ * Reducer that executes actions
106
+ * @author Yuen Ler Chow
107
+ * @param state current state
108
+ * @param action action to execute
109
+ */
110
+ const reducer = (state: State, action: Action): State => {
111
+ switch (action.type) {
112
+ case ActionType.Advance: {
113
+ return {
114
+ ...state,
115
+ progress: Math.min(100, state.progress + action.step),
116
+ };
117
+ }
118
+ case ActionType.Complete: {
119
+ return {
120
+ ...state,
121
+ progress: 100,
122
+ };
123
+ }
124
+ default: {
125
+ return state;
126
+ }
127
+ }
128
+ };
129
+
130
+ /*------------------------------------------------------------------------*/
131
+ /* ------------------------------ Component ----------------------------- */
132
+ /*------------------------------------------------------------------------*/
133
+
134
+ const FakeProgressBar: React.FC<Props> = (props) => {
135
+ /*------------------------------------------------------------------------*/
136
+ /* -------------------------------- Setup ------------------------------- */
137
+ /*------------------------------------------------------------------------*/
138
+
139
+ /* -------------- Props ------------- */
140
+
141
+ // Destructure props
142
+ const {
143
+ isFinished,
144
+ estimatedTimeSec = 8,
145
+ striped,
146
+ variant,
147
+ bgVariant,
148
+ showOutline,
149
+ size,
150
+ } = props;
151
+
152
+ /* -------------- State ------------- */
153
+
154
+ // Initial state
155
+ const initialState: State = {
156
+ progress: 0,
157
+ };
158
+
159
+ // Initialize state
160
+ const [state, dispatch] = useReducer(reducer, initialState);
161
+
162
+ // Destructure common state
163
+ const {
164
+ progress,
165
+ } = state;
166
+
167
+ // Convert estimate to milliseconds
168
+ const estimatedMs = estimatedTimeSec * 1000;
169
+
170
+ // Average interval between updates to reach ~90% in the estimated time
171
+ const avgIntervalMs = (estimatedMs / SLOW_PHASE_START_PERCENT) * AVG_FAST_PHASE_STEP;
172
+
173
+ /*------------------------------------------------------------------------*/
174
+ /* ------------------------- Lifecycle Functions ------------------------ */
175
+ /*------------------------------------------------------------------------*/
176
+
177
+ /**
178
+ * Animate to 100% when isFinished becomes true
179
+ * @author Yuen Ler Chow
180
+ */
181
+ useEffect(
182
+ () => {
183
+ if (isFinished) {
184
+ dispatch({
185
+ type: ActionType.Complete,
186
+ });
187
+ }
188
+ },
189
+ [isFinished],
190
+ );
191
+
192
+ /**
193
+ * Advance the progress in two phases:
194
+ * 1) Random jumps up to 90%
195
+ * 2) Slow crawl from 90% to 99%
196
+ * @author Yuen Ler Chow
197
+ */
198
+ useEffect(
199
+ () => {
200
+ // Stop if already finished or at max
201
+ if (isFinished || progress >= MAX_UNFINISHED_PERCENT) {
202
+ return;
203
+ }
204
+
205
+ // Determine which phase we are in
206
+ const inFastPhase = progress < SLOW_PHASE_START_PERCENT;
207
+
208
+ // Default values
209
+ let delay = avgIntervalMs;
210
+ let step = 1;
211
+
212
+ if (inFastPhase) {
213
+ // Randomize delay around the average interval for variability
214
+ const randomnessMultiplier = 0.5 + (Math.random());
215
+ delay = avgIntervalMs * randomnessMultiplier;
216
+
217
+ // Randomly move forward by 1-5%, capped at 90%
218
+ step = Math.min(
219
+ 1 + Math.floor(Math.random() * 5),
220
+ SLOW_PHASE_START_PERCENT - progress,
221
+ );
222
+ } else {
223
+ // Crawl slowly 1% at a time, clamped so we don't exceed 99%
224
+ delay = SLOW_PHASE_MIN_DELAY_MS + (Math.random() * (SLOW_PHASE_MAX_DELAY_MS - SLOW_PHASE_MIN_DELAY_MS));
225
+ step = Math.min(1, MAX_UNFINISHED_PERCENT - progress);
226
+ }
227
+
228
+ // Track whether this effect invocation is still current
229
+ let cancelled = false;
230
+
231
+ // Wait then dispatch the next progress update
232
+ (async () => {
233
+ await waitMs(delay);
234
+ if (!cancelled) {
235
+ dispatch({
236
+ type: ActionType.Advance,
237
+ step,
238
+ });
239
+ }
240
+ })();
241
+
242
+ return () => {
243
+ cancelled = true;
244
+ };
245
+ },
246
+ [progress, avgIntervalMs, isFinished],
247
+ );
248
+
249
+ /*------------------------------------------------------------------------*/
250
+ /* ------------------------------- Render ------------------------------- */
251
+ /*------------------------------------------------------------------------*/
252
+
253
+ /*----------------------------------------*/
254
+ /* --------------- Main UI -------------- */
255
+ /*----------------------------------------*/
256
+
257
+ return (
258
+ <ProgressBar
259
+ percentProgress={progress}
260
+ striped={striped}
261
+ variant={variant}
262
+ bgVariant={bgVariant}
263
+ showOutline={showOutline}
264
+ size={size}
265
+ />
266
+ );
267
+ };
268
+
269
+ /*------------------------------------------------------------------------*/
270
+ /* ------------------------------- Wrap Up ------------------------------ */
271
+ /*------------------------------------------------------------------------*/
272
+
273
+ // Export component
274
+ export default FakeProgressBar;
@@ -10,6 +10,12 @@ import React from 'react';
10
10
  import Variant from '../types/Variant';
11
11
  import ProgressBarSize from '../types/ProgressBarSize';
12
12
 
13
+ // Import helpers
14
+ import {
15
+ roundToNumDecimals,
16
+ padDecimalZeros,
17
+ } from 'dce-commonkit';
18
+
13
19
  /*------------------------------------------------------------------------*/
14
20
  /* -------------------------------- Types ------------------------------- */
15
21
  /*------------------------------------------------------------------------*/
@@ -75,7 +81,7 @@ type ProgressStatus = (
75
81
  const ITEM_WIDTH_MULTIPLIER = 1.3;
76
82
 
77
83
  // Constant for percent width
78
- const PERCENT_WIDTH = 3;
84
+ const PERCENT_WIDTH = 2.7;
79
85
 
80
86
  // Constant for item width
81
87
  const ITEM_WIDTH = 2;
@@ -88,11 +94,9 @@ const ITEM_WIDTH = 2;
88
94
  let style = `
89
95
  .ProgressBar-number-of,
90
96
  .ProgressBar-percent {
91
- flex: 0 0 auto;
92
97
  white-space: nowrap;
93
- padding-right: 0.5em;
94
- padding-left: 0.25em;
95
- text-align: right;
98
+ padding-right: 0.3em;
99
+ text-align: left;
96
100
  transition: width .25s ease;
97
101
  }
98
102
 
@@ -164,10 +168,20 @@ const ProgressBar: React.FC<Props> = (props) => {
164
168
  /* ---------------- Sizes --------------- */
165
169
  /*----------------------------------------*/
166
170
 
171
+ // Add dynamic general style
172
+ style += `
173
+ .ProgressBar-percent {
174
+ min-width: ${(status.usePercent ? status.numDecimalPlaces ?? 0 : 0) + PERCENT_WIDTH}em;
175
+ }
176
+ .ProgressBar-number-of {
177
+ min-width: ${((!status.usePercent ? String(status.total ?? 0).length || 1 : 0) * ITEM_WIDTH_MULTIPLIER) + ITEM_WIDTH}em;
178
+ }
179
+ `;
180
+
167
181
  // Size styles
168
182
  switch (size) {
169
- case ProgressBarSize.Small:
170
- // Small size
183
+ case ProgressBarSize.Medium:
184
+ // Medium size
171
185
  style += `
172
186
  .ProgressBar-container .ProgressBar-number-of, .ProgressBar-container .ProgressBar-percent {
173
187
  font-size: 1em;
@@ -180,18 +194,10 @@ const ProgressBar: React.FC<Props> = (props) => {
180
194
  height: 1.5em;
181
195
  border-radius: 0.5em;
182
196
  }
183
- .ProgressBar-percent {
184
- min-width: ${((status.usePercent ? status.numDecimalPlaces ?? 0 : 0) * 1) + PERCENT_WIDTH}em;
185
- max-width: ${((status.usePercent ? status.numDecimalPlaces ?? 0 : 0) * 1) + PERCENT_WIDTH}em;
186
- }
187
- .ProgressBar-number-of {
188
- min-width: ${((!status.usePercent ? status.total?.toString().length || 1 : 0) * ITEM_WIDTH_MULTIPLIER) + ITEM_WIDTH}em;
189
- max-width: ${((!status.usePercent ? status.total?.toString().length || 1 : 0) * ITEM_WIDTH_MULTIPLIER) + ITEM_WIDTH}em;
190
- }
191
197
  `;
192
198
  break;
193
- case ProgressBarSize.Medium:
194
- // Medium size
199
+ case ProgressBarSize.Large:
200
+ // Large size
195
201
  style += `
196
202
  .ProgressBar-container .ProgressBar-number-of, .ProgressBar-container .ProgressBar-percent {
197
203
  font-size: 1.5em;
@@ -204,38 +210,6 @@ const ProgressBar: React.FC<Props> = (props) => {
204
210
  height: 2em;
205
211
  border-radius: 0.7em;
206
212
  }
207
- .ProgressBar-percent {
208
- min-width: ${((status.usePercent ? status.numDecimalPlaces ?? 0 : 0) * 1) + PERCENT_WIDTH}em;
209
- max-width: ${((status.usePercent ? status.numDecimalPlaces ?? 0 : 0) * 1) + PERCENT_WIDTH}em;
210
- }
211
- .ProgressBar-number-of {
212
- min-width: ${((!status.usePercent ? status.total?.toString().length || 1 : 0) * ITEM_WIDTH_MULTIPLIER) + ITEM_WIDTH}em;
213
- max-width: ${((!status.usePercent ? status.total?.toString().length || 1 : 0) * ITEM_WIDTH_MULTIPLIER) + ITEM_WIDTH}em;
214
- }
215
- `;
216
- break;
217
- case ProgressBarSize.Large:
218
- // Large size
219
- style += `
220
- .ProgressBar-container .ProgressBar-number-of, .ProgressBar-container .ProgressBar-percent {
221
- font-size: 2em;
222
- }
223
- .ProgressBar-container .ProgressBar-background {
224
- height: 3em;
225
- border-radius: 1em;
226
- }
227
- .ProgressBar-container .ProgressBar-bar {
228
- height: 3em;
229
- border-radius: 1em;
230
- }
231
- .ProgressBar-percent {
232
- min-width: ${((status.usePercent ? status.numDecimalPlaces ?? 0 : 0) * 1) + PERCENT_WIDTH}em;
233
- max-width: ${((status.usePercent ? status.numDecimalPlaces ?? 0 : 0) * 1) + PERCENT_WIDTH}em;
234
- }
235
- .ProgressBar-number-of {
236
- min-width: ${((!status.usePercent ? status.total?.toString().length || 1 : 0) * ITEM_WIDTH_MULTIPLIER) + ITEM_WIDTH}em;
237
- max-width: ${((!status.usePercent ? status.total?.toString().length || 1 : 0) * ITEM_WIDTH_MULTIPLIER) + ITEM_WIDTH}em;
238
- }
239
213
  `;
240
214
  break;
241
215
  default:
@@ -245,9 +219,8 @@ const ProgressBar: React.FC<Props> = (props) => {
245
219
  // Get the width of the outline based on size
246
220
  const outlineWidth = (() => {
247
221
  switch (size) {
248
- case ProgressBarSize.Small: return '0.05em';
249
- case ProgressBarSize.Medium: return '0.08em';
250
- case ProgressBarSize.Large: return '0.1em';
222
+ case ProgressBarSize.Medium: return '0.05em';
223
+ case ProgressBarSize.Large: return '0.08em';
251
224
  default: return '0.05em';
252
225
  }
253
226
  })();
@@ -275,25 +248,38 @@ const ProgressBar: React.FC<Props> = (props) => {
275
248
  `;
276
249
 
277
250
  // Stripes for striped effect
278
- const stripes: React.ReactNode = (
279
- <div>
280
- <style>{stripesStyle}</style>
281
- <div
282
- className="ProgressBar-stripes position-absolute "
283
- style={{
284
- width: '200%',
285
- height: '100%',
286
- }}
287
- >
288
- &nbsp;
251
+ let stripes: React.ReactNode = null;
252
+ if (striped) {
253
+ stripes = (
254
+ <div>
255
+ <style>{stripesStyle}</style>
256
+ <div
257
+ className="ProgressBar-stripes position-absolute"
258
+ style={{
259
+ width: '200%',
260
+ height: '100%',
261
+ }}
262
+ >
263
+ &nbsp;
264
+ </div>
289
265
  </div>
290
- </div>
291
- );
266
+ );
267
+ }
292
268
 
293
269
  /*----------------------------------------*/
294
270
  /* --------------- Main UI -------------- */
295
271
  /*----------------------------------------*/
296
272
 
273
+ // Calculate the width of the progress bar
274
+ let progressBarWidthPercent = 0;
275
+ if (status.usePercent) {
276
+ // Use percent progress directly
277
+ progressBarWidthPercent = status.percentProgress;
278
+ } else if (status.total > 0) {
279
+ // Calculate percent progress from items
280
+ progressBarWidthPercent = (status.numComplete / status.total) * 100;
281
+ }
282
+
297
283
  // Render the progress bar
298
284
  return (
299
285
  <div className="ProgressBar-container d-flex align-items-center">
@@ -301,25 +287,27 @@ const ProgressBar: React.FC<Props> = (props) => {
301
287
  <style>{style}</style>
302
288
  {/* Use items */}
303
289
  {!status.usePercent && status.numComplete && (
304
- <span
305
- className="ProgressBar-number-of pe-2 align-self-center"
306
- >
290
+ <span className="ProgressBar-number-of">
307
291
  {status.numComplete}
308
292
  &nbsp;of&nbsp;
309
293
  {status.total}
310
294
  </span>
311
295
  )}
312
296
  {/* Use percentage */}
313
- {status.usePercent && status.percentProgress && (
314
- <span
315
- className="ProgressBar-percent pe-2 align-self-center"
316
- >
317
- {status.percentProgress.toFixed(status?.numDecimalPlaces ?? 0)}
297
+ {status.usePercent && (
298
+ <span className="ProgressBar-percent">
299
+ {
300
+ padDecimalZeros(
301
+ roundToNumDecimals(status.percentProgress ?? 0, status?.numDecimalPlaces ?? 0),
302
+ status?.numDecimalPlaces ?? 0,
303
+ )
304
+ }
318
305
  %
319
306
  </span>
320
307
  )}
308
+ {/* Progress Bar Filled Area */}
321
309
  <div
322
- className={`ProgressBar-background bg-${bgVariant} w-100`}
310
+ className={`ProgressBar-background bg-${bgVariant} flex-grow-1`}
323
311
  style={{
324
312
  boxShadow: `0 0 0 ${outlineWidth} ${showOutline ? '#000' : '#DEE2E6'}`,
325
313
  }}
@@ -332,11 +320,13 @@ const ProgressBar: React.FC<Props> = (props) => {
332
320
  `ProgressBar-bar bg-${variant} text-start position-relative`
333
321
  }
334
322
  style={{
335
- width: `${(status.usePercent ? status.percentProgress : (status.numComplete / status.total) * 100)}%`,
323
+ width: `${progressBarWidthPercent}%`,
336
324
  overflow: 'hidden',
337
325
  }}
338
326
  >
339
- {striped && stripes}
327
+ {/* Show Strips (if they exist) */}
328
+ {stripes}
329
+ {/* Space so the bar has some content */}
340
330
  &nbsp;
341
331
  </div>
342
332
  </div>
package/src/index.ts CHANGED
@@ -103,6 +103,7 @@ import AutoscrollToBottomContainer from './components/AutoscrollToBottomContaine
103
103
  import MultiSwitch from './components/MultiSwitch';
104
104
  import Dropdown from './components/Dropdown';
105
105
  import ProgressBar from './components/ProgressBar';
106
+ import FakeProgressBar from './components/FakeProgressBar';
106
107
 
107
108
  // Import dynamic constants
108
109
  import DynamicWord from './dynamicConstants/DynamicWord';
@@ -163,6 +164,7 @@ export {
163
164
  MultiSwitch,
164
165
  Dropdown,
165
166
  ProgressBar,
167
+ FakeProgressBar,
166
168
  // Global functions
167
169
  alert,
168
170
  prompt,
@@ -3,7 +3,6 @@
3
3
  * @author Allison Zhang
4
4
  */
5
5
  enum ProgressBarSize {
6
- Small = 'Small',
7
6
  Medium = 'Medium',
8
7
  Large = 'Large',
9
8
  }