@wandelbots/wandelbots-js-react-components 2.36.0 → 2.37.0-pr.feature-states-for-cycle-timer.379.4ca80c1

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 (44) hide show
  1. package/dist/components/CycleTimer/CycleTimer.d.ts +3 -0
  2. package/dist/components/CycleTimer/CycleTimer.d.ts.map +1 -0
  3. package/dist/components/CycleTimer/DefaultVariant.d.ts +10 -0
  4. package/dist/components/CycleTimer/DefaultVariant.d.ts.map +1 -0
  5. package/dist/components/CycleTimer/SmallVariant.d.ts +11 -0
  6. package/dist/components/CycleTimer/SmallVariant.d.ts.map +1 -0
  7. package/dist/components/CycleTimer/index.d.ts +28 -0
  8. package/dist/components/CycleTimer/index.d.ts.map +1 -0
  9. package/dist/components/CycleTimer/types.d.ts +49 -0
  10. package/dist/components/CycleTimer/types.d.ts.map +1 -0
  11. package/dist/components/CycleTimer/useAnimations.d.ts +13 -0
  12. package/dist/components/CycleTimer/useAnimations.d.ts.map +1 -0
  13. package/dist/components/CycleTimer/useTimerLogic.d.ts +26 -0
  14. package/dist/components/CycleTimer/useTimerLogic.d.ts.map +1 -0
  15. package/dist/components/CycleTimer/utils.d.ts +13 -0
  16. package/dist/components/CycleTimer/utils.d.ts.map +1 -0
  17. package/dist/components/CycleTimer.d.ts +2 -96
  18. package/dist/components/CycleTimer.d.ts.map +1 -1
  19. package/dist/components/TabBar.d.ts.map +1 -1
  20. package/dist/components/jogging/PoseCartesianValues.d.ts +8 -4
  21. package/dist/components/jogging/PoseCartesianValues.d.ts.map +1 -1
  22. package/dist/components/jogging/PoseJointValues.d.ts +8 -4
  23. package/dist/components/jogging/PoseJointValues.d.ts.map +1 -1
  24. package/dist/index.cjs +50 -50
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +9215 -8857
  27. package/dist/index.js.map +1 -1
  28. package/package.json +1 -1
  29. package/src/components/AppHeader.tsx +1 -1
  30. package/src/components/CycleTimer/CycleTimer.ts +6 -0
  31. package/src/components/CycleTimer/DefaultVariant.tsx +272 -0
  32. package/src/components/CycleTimer/SmallVariant.tsx +190 -0
  33. package/src/components/CycleTimer/index.tsx +143 -0
  34. package/src/components/CycleTimer/types.ts +58 -0
  35. package/src/components/CycleTimer/useAnimations.ts +154 -0
  36. package/src/components/CycleTimer/useTimerLogic.ts +377 -0
  37. package/src/components/CycleTimer/utils.ts +53 -0
  38. package/src/components/CycleTimer.tsx +6 -715
  39. package/src/components/ProgramControl.tsx +4 -4
  40. package/src/components/TabBar.tsx +8 -10
  41. package/src/components/jogging/PoseCartesianValues.tsx +67 -7
  42. package/src/components/jogging/PoseJointValues.tsx +68 -8
  43. package/src/i18n/locales/de/translations.json +4 -0
  44. package/src/i18n/locales/en/translations.json +4 -0
@@ -0,0 +1,154 @@
1
+ import { useCallback, useRef, useState } from "react"
2
+ import type { AnimationState } from "./types"
3
+
4
+ export const useAnimations = () => {
5
+ const [animationState, setAnimationState] = useState<AnimationState>({
6
+ showPauseAnimation: false,
7
+ showErrorAnimation: false,
8
+ showPulsatingText: false,
9
+ pulsatingFinished: false,
10
+ showLabels: true,
11
+ showMainText: true,
12
+ })
13
+
14
+ // Refs for managing timeouts and intervals
15
+ const pauseAnimationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
16
+ const errorAnimationTimeoutRef = useRef<NodeJS.Timeout | null>(null)
17
+ const pulsatingIntervalRef = useRef<NodeJS.Timeout | null>(null)
18
+ const fadeTimeoutRef = useRef<NodeJS.Timeout | null>(null)
19
+ const pulseCountRef = useRef<number>(0)
20
+
21
+ const triggerPauseAnimation = useCallback(() => {
22
+ setAnimationState((prev) => ({ ...prev, showPauseAnimation: true }))
23
+
24
+ if (pauseAnimationTimeoutRef.current) {
25
+ clearTimeout(pauseAnimationTimeoutRef.current)
26
+ }
27
+
28
+ pauseAnimationTimeoutRef.current = setTimeout(() => {
29
+ setAnimationState((prev) => ({ ...prev, showPauseAnimation: false }))
30
+ }, 800)
31
+ }, [])
32
+
33
+ const triggerErrorAnimation = useCallback(() => {
34
+ setAnimationState((prev) => ({ ...prev, showErrorAnimation: true }))
35
+
36
+ if (errorAnimationTimeoutRef.current) {
37
+ clearTimeout(errorAnimationTimeoutRef.current)
38
+ }
39
+
40
+ errorAnimationTimeoutRef.current = setTimeout(() => {
41
+ setAnimationState((prev) => ({ ...prev, showErrorAnimation: false }))
42
+ }, 600)
43
+ }, [])
44
+
45
+ const clearErrorAnimation = useCallback(() => {
46
+ setAnimationState((prev) => ({ ...prev, showErrorAnimation: false }))
47
+ if (errorAnimationTimeoutRef.current) {
48
+ clearTimeout(errorAnimationTimeoutRef.current)
49
+ }
50
+ }, [])
51
+
52
+ const startPulsatingAnimation = useCallback((onComplete?: () => void) => {
53
+ pulseCountRef.current = 0
54
+ setAnimationState((prev) => ({
55
+ ...prev,
56
+ showPulsatingText: true,
57
+ pulsatingFinished: false,
58
+ }))
59
+
60
+ pulsatingIntervalRef.current = setInterval(() => {
61
+ pulseCountRef.current += 1
62
+
63
+ if (pulseCountRef.current >= 8) {
64
+ // 4 complete cycles (on->off->on->off = 8 state changes)
65
+ if (pulsatingIntervalRef.current) {
66
+ clearInterval(pulsatingIntervalRef.current)
67
+ pulsatingIntervalRef.current = null
68
+ }
69
+ setAnimationState((prev) => ({
70
+ ...prev,
71
+ showPulsatingText: false,
72
+ pulsatingFinished: true,
73
+ }))
74
+ if (onComplete) {
75
+ onComplete()
76
+ }
77
+ } else {
78
+ setAnimationState((prev) => ({
79
+ ...prev,
80
+ showPulsatingText: !prev.showPulsatingText,
81
+ }))
82
+ }
83
+ }, 800)
84
+ }, [])
85
+
86
+ const stopPulsatingAnimation = useCallback(() => {
87
+ if (pulsatingIntervalRef.current) {
88
+ clearInterval(pulsatingIntervalRef.current)
89
+ pulsatingIntervalRef.current = null
90
+ }
91
+ setAnimationState((prev) => ({
92
+ ...prev,
93
+ showPulsatingText: false,
94
+ pulsatingFinished: false,
95
+ }))
96
+ pulseCountRef.current = 0
97
+ }, [])
98
+
99
+ const triggerFadeTransition = useCallback(() => {
100
+ setAnimationState((prev) => ({
101
+ ...prev,
102
+ showLabels: false,
103
+ showMainText: false,
104
+ }))
105
+
106
+ if (fadeTimeoutRef.current) {
107
+ clearTimeout(fadeTimeoutRef.current)
108
+ }
109
+
110
+ fadeTimeoutRef.current = setTimeout(() => {
111
+ setAnimationState((prev) => ({
112
+ ...prev,
113
+ showLabels: true,
114
+ showMainText: true,
115
+ }))
116
+ }, 200)
117
+ }, [])
118
+
119
+ const setInitialAnimationState = useCallback(() => {
120
+ setAnimationState((prev) => ({
121
+ ...prev,
122
+ showLabels: true,
123
+ showMainText: true,
124
+ }))
125
+ }, [])
126
+
127
+ // Cleanup function
128
+ const cleanup = useCallback(() => {
129
+ if (pauseAnimationTimeoutRef.current) {
130
+ clearTimeout(pauseAnimationTimeoutRef.current)
131
+ }
132
+ if (errorAnimationTimeoutRef.current) {
133
+ clearTimeout(errorAnimationTimeoutRef.current)
134
+ }
135
+ if (fadeTimeoutRef.current) {
136
+ clearTimeout(fadeTimeoutRef.current)
137
+ }
138
+ if (pulsatingIntervalRef.current) {
139
+ clearInterval(pulsatingIntervalRef.current)
140
+ }
141
+ }, [])
142
+
143
+ return {
144
+ animationState,
145
+ triggerPauseAnimation,
146
+ triggerErrorAnimation,
147
+ clearErrorAnimation,
148
+ startPulsatingAnimation,
149
+ stopPulsatingAnimation,
150
+ triggerFadeTransition,
151
+ setInitialAnimationState,
152
+ cleanup,
153
+ }
154
+ }
@@ -0,0 +1,377 @@
1
+ import { useCallback, useEffect, useRef, useState } from "react"
2
+ import { useInterpolation } from "../utils/interpolation"
3
+ import type { TimerState } from "./types"
4
+ import { calculateExactProgress } from "./utils"
5
+
6
+ interface UseTimerLogicProps {
7
+ autoStart: boolean
8
+ onCycleEnd?: () => void
9
+ onMeasuringComplete?: () => void
10
+ hasError: boolean
11
+ onPauseAnimation: () => void
12
+ onErrorAnimation: () => void
13
+ onClearErrorAnimation: () => void
14
+ onStartPulsating: (onComplete?: () => void) => void
15
+ }
16
+
17
+ export const useTimerLogic = ({
18
+ autoStart,
19
+ onCycleEnd,
20
+ onMeasuringComplete,
21
+ hasError,
22
+ onPauseAnimation,
23
+ onErrorAnimation,
24
+ onClearErrorAnimation,
25
+ onStartPulsating,
26
+ }: UseTimerLogicProps) => {
27
+ const [timerState, setTimerState] = useState<TimerState>({
28
+ currentState: "idle",
29
+ remainingTime: 0,
30
+ maxTime: null,
31
+ isRunning: false,
32
+ isPausedState: false,
33
+ currentProgress: 0,
34
+ wasRunningBeforeError: false,
35
+ })
36
+
37
+ // Timer-related refs
38
+ const animationRef = useRef<number | null>(null)
39
+ const startTimeRef = useRef<number | null>(null)
40
+ const pausedTimeRef = useRef<number>(0)
41
+
42
+ // Spring-based interpolator for smooth gauge progress animations
43
+ const [progressInterpolator] = useInterpolation([0], {
44
+ tension: 80,
45
+ friction: 18,
46
+ onChange: ([progress]) => {
47
+ setTimerState((prev) => ({ ...prev, currentProgress: progress }))
48
+ },
49
+ })
50
+
51
+ const setIdle = useCallback(() => {
52
+ setTimerState((prev) => ({
53
+ ...prev,
54
+ currentState: "idle",
55
+ maxTime: null,
56
+ remainingTime: 0,
57
+ isRunning: false,
58
+ isPausedState: false,
59
+ }))
60
+ pausedTimeRef.current = 0
61
+ startTimeRef.current = null
62
+ progressInterpolator.setTarget([0])
63
+ }, [progressInterpolator])
64
+
65
+ const startMeasuring = useCallback(
66
+ (elapsedSeconds: number = 0) => {
67
+ setTimerState((prev) => ({
68
+ ...prev,
69
+ currentState: "measuring",
70
+ maxTime: null,
71
+ remainingTime: elapsedSeconds,
72
+ isPausedState: false,
73
+ }))
74
+ pausedTimeRef.current = 0
75
+
76
+ const initialProgress = ((elapsedSeconds / 60) % 1) * 100
77
+ progressInterpolator.setTarget([initialProgress])
78
+
79
+ if (autoStart) {
80
+ startTimeRef.current = Date.now() - elapsedSeconds * 1000
81
+ setTimerState((prev) => ({ ...prev, isRunning: true }))
82
+ } else {
83
+ startTimeRef.current = null
84
+ }
85
+ },
86
+ [autoStart, progressInterpolator],
87
+ )
88
+
89
+ const startCountUp = useCallback(
90
+ (elapsedSeconds: number = 0) => {
91
+ setTimerState((prev) => ({
92
+ ...prev,
93
+ currentState: "countup",
94
+ maxTime: null,
95
+ remainingTime: elapsedSeconds,
96
+ isPausedState: false,
97
+ }))
98
+ pausedTimeRef.current = 0
99
+
100
+ const initialProgress = ((elapsedSeconds / 60) % 1) * 100
101
+ progressInterpolator.setTarget([initialProgress])
102
+
103
+ if (autoStart) {
104
+ startTimeRef.current = Date.now() - elapsedSeconds * 1000
105
+ setTimerState((prev) => ({ ...prev, isRunning: true }))
106
+ } else {
107
+ startTimeRef.current = null
108
+ }
109
+ },
110
+ [autoStart, progressInterpolator],
111
+ )
112
+
113
+ const startNewCycle = useCallback(
114
+ (maxTimeSeconds?: number, elapsedSeconds: number = 0) => {
115
+ // Stop any running timer first to prevent conflicts
116
+ setTimerState((prev) => ({ ...prev, isRunning: false }))
117
+ startTimeRef.current = null
118
+
119
+ const newState = maxTimeSeconds !== undefined ? "countdown" : "countup"
120
+ setTimerState((prev) => ({
121
+ ...prev,
122
+ currentState: newState,
123
+ maxTime: maxTimeSeconds ?? null,
124
+ isPausedState: false,
125
+ }))
126
+ pausedTimeRef.current = 0
127
+
128
+ if (maxTimeSeconds !== undefined) {
129
+ // Count-down mode
130
+ const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
131
+ setTimerState((prev) => ({ ...prev, remainingTime: remainingSeconds }))
132
+
133
+ const initialProgress =
134
+ elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
135
+
136
+ progressInterpolator.setTarget([initialProgress])
137
+ progressInterpolator.update(1)
138
+
139
+ if (remainingSeconds === 0) {
140
+ setTimerState((prev) => ({ ...prev, isRunning: false }))
141
+ startTimeRef.current = null
142
+ if (onCycleEnd) {
143
+ setTimeout(() => onCycleEnd(), 0)
144
+ }
145
+ } else if (autoStart) {
146
+ setTimeout(() => {
147
+ startTimeRef.current = Date.now() - elapsedSeconds * 1000
148
+ setTimerState((prev) => ({ ...prev, isRunning: true }))
149
+ }, 0)
150
+ } else {
151
+ startTimeRef.current = null
152
+ }
153
+ } else {
154
+ // Count-up mode
155
+ setTimerState((prev) => ({ ...prev, remainingTime: elapsedSeconds }))
156
+
157
+ const initialProgress = ((elapsedSeconds / 60) % 1) * 100
158
+ progressInterpolator.setTarget([initialProgress])
159
+ progressInterpolator.update(1)
160
+
161
+ if (autoStart) {
162
+ setTimeout(() => {
163
+ startTimeRef.current = Date.now() - elapsedSeconds * 1000
164
+ setTimerState((prev) => ({ ...prev, isRunning: true }))
165
+ }, 0)
166
+ } else {
167
+ startTimeRef.current = null
168
+ }
169
+ }
170
+ },
171
+ [autoStart, onCycleEnd, progressInterpolator],
172
+ )
173
+
174
+ const completeMeasuring = useCallback(() => {
175
+ if (timerState.currentState === "measuring") {
176
+ setTimerState((prev) => ({
177
+ ...prev,
178
+ isRunning: false,
179
+ currentState: "measured",
180
+ }))
181
+ startTimeRef.current = null
182
+
183
+ onStartPulsating(() => {
184
+ if (onMeasuringComplete) {
185
+ onMeasuringComplete()
186
+ }
187
+ })
188
+ }
189
+ }, [timerState.currentState, onStartPulsating, onMeasuringComplete])
190
+
191
+ const pause = useCallback(() => {
192
+ if (startTimeRef.current && timerState.isRunning) {
193
+ const now = Date.now()
194
+ const additionalElapsed = now - startTimeRef.current
195
+ pausedTimeRef.current += additionalElapsed
196
+
197
+ const totalElapsed = pausedTimeRef.current / 1000
198
+ const exactProgress = calculateExactProgress(
199
+ timerState.currentState,
200
+ totalElapsed,
201
+ timerState.maxTime,
202
+ )
203
+ progressInterpolator.setTarget([exactProgress])
204
+ }
205
+
206
+ setTimerState((prev) => ({
207
+ ...prev,
208
+ isRunning: false,
209
+ isPausedState: true,
210
+ }))
211
+ onPauseAnimation()
212
+ }, [
213
+ timerState.isRunning,
214
+ timerState.currentState,
215
+ timerState.maxTime,
216
+ progressInterpolator,
217
+ onPauseAnimation,
218
+ ])
219
+
220
+ const resume = useCallback(() => {
221
+ if (
222
+ timerState.isPausedState &&
223
+ (timerState.remainingTime > 0 || timerState.currentState !== "countdown")
224
+ ) {
225
+ startTimeRef.current = Date.now()
226
+ setTimerState((prev) => ({
227
+ ...prev,
228
+ isRunning: true,
229
+ isPausedState: false,
230
+ }))
231
+ }
232
+ }, [
233
+ timerState.isPausedState,
234
+ timerState.remainingTime,
235
+ timerState.currentState,
236
+ ])
237
+
238
+ const isPaused = useCallback(() => {
239
+ return timerState.isPausedState
240
+ }, [timerState.isPausedState])
241
+
242
+ // Handle error state changes
243
+ useEffect(() => {
244
+ if (hasError) {
245
+ if (timerState.isRunning && !timerState.isPausedState) {
246
+ setTimerState((prev) => ({ ...prev, wasRunningBeforeError: true }))
247
+ pause()
248
+ }
249
+ onErrorAnimation()
250
+ } else {
251
+ if (timerState.wasRunningBeforeError && timerState.isPausedState) {
252
+ setTimerState((prev) => ({ ...prev, wasRunningBeforeError: false }))
253
+ resume()
254
+ }
255
+ onClearErrorAnimation()
256
+ }
257
+ }, [
258
+ hasError,
259
+ timerState.isRunning,
260
+ timerState.isPausedState,
261
+ timerState.wasRunningBeforeError,
262
+ pause,
263
+ resume,
264
+ onErrorAnimation,
265
+ onClearErrorAnimation,
266
+ ])
267
+
268
+ // Main timer loop
269
+ useEffect(() => {
270
+ if (timerState.isRunning) {
271
+ const updateTimer = () => {
272
+ if (startTimeRef.current) {
273
+ const now = Date.now()
274
+ const elapsed =
275
+ (now - startTimeRef.current + pausedTimeRef.current) / 1000
276
+
277
+ if (
278
+ timerState.currentState === "countdown" &&
279
+ timerState.maxTime !== null
280
+ ) {
281
+ const remaining = Math.max(0, timerState.maxTime - elapsed)
282
+ setTimerState((prev) => ({
283
+ ...prev,
284
+ remainingTime: Math.ceil(remaining),
285
+ }))
286
+
287
+ const progress = Math.min(100, (elapsed / timerState.maxTime) * 100)
288
+ progressInterpolator.setTarget([progress])
289
+
290
+ if (remaining <= 0) {
291
+ setTimerState((prev) => ({
292
+ ...prev,
293
+ isRunning: false,
294
+ remainingTime: 0,
295
+ }))
296
+ startTimeRef.current = null
297
+ progressInterpolator.setTarget([100])
298
+ if (onCycleEnd) {
299
+ setTimeout(() => onCycleEnd(), 0)
300
+ }
301
+ return
302
+ }
303
+ } else if (timerState.currentState === "measuring") {
304
+ setTimerState((prev) => ({
305
+ ...prev,
306
+ remainingTime: Math.floor(elapsed),
307
+ }))
308
+ const progress = ((elapsed / 60) % 1) * 100
309
+ progressInterpolator.setTarget([progress])
310
+ } else if (timerState.currentState === "countup") {
311
+ setTimerState((prev) => ({
312
+ ...prev,
313
+ remainingTime: Math.floor(elapsed),
314
+ }))
315
+ const progress = ((elapsed / 60) % 1) * 100
316
+ progressInterpolator.setTarget([progress])
317
+ }
318
+
319
+ if (timerState.isRunning) {
320
+ animationRef.current = requestAnimationFrame(updateTimer)
321
+ }
322
+ }
323
+ }
324
+
325
+ animationRef.current = requestAnimationFrame(updateTimer)
326
+ } else {
327
+ if (animationRef.current) {
328
+ cancelAnimationFrame(animationRef.current)
329
+ animationRef.current = null
330
+ }
331
+ }
332
+
333
+ return () => {
334
+ if (animationRef.current) {
335
+ cancelAnimationFrame(animationRef.current)
336
+ }
337
+ }
338
+ }, [
339
+ timerState.isRunning,
340
+ onCycleEnd,
341
+ timerState.currentState,
342
+ timerState.maxTime,
343
+ progressInterpolator,
344
+ ])
345
+
346
+ // Interpolation animation loop
347
+ useEffect(() => {
348
+ let interpolationAnimationId: number | null = null
349
+
350
+ const animateInterpolation = () => {
351
+ progressInterpolator.update(1 / 60)
352
+ interpolationAnimationId = requestAnimationFrame(animateInterpolation)
353
+ }
354
+
355
+ interpolationAnimationId = requestAnimationFrame(animateInterpolation)
356
+
357
+ return () => {
358
+ if (interpolationAnimationId) {
359
+ cancelAnimationFrame(interpolationAnimationId)
360
+ }
361
+ }
362
+ }, [progressInterpolator])
363
+
364
+ return {
365
+ timerState,
366
+ controls: {
367
+ startNewCycle,
368
+ startMeasuring,
369
+ startCountUp,
370
+ setIdle,
371
+ completeMeasuring,
372
+ pause,
373
+ resume,
374
+ isPaused,
375
+ },
376
+ }
377
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Formats time in seconds to MM:SS format
3
+ */
4
+ export const formatTime = (seconds: number): string => {
5
+ const minutes = Math.floor(seconds / 60)
6
+ const remainingSeconds = seconds % 60
7
+ return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
8
+ }
9
+
10
+ /**
11
+ * Calculates progress percentage for different timer states
12
+ */
13
+ export const calculateProgress = (
14
+ currentState: string,
15
+ remainingTime: number,
16
+ maxTime: number | null,
17
+ ): number => {
18
+ if (currentState === "idle") {
19
+ return 0
20
+ }
21
+
22
+ if (currentState === "countdown" && maxTime !== null) {
23
+ // Count-down mode: progress based on elapsed time
24
+ const elapsed = maxTime - remainingTime
25
+ return Math.min(100, (elapsed / maxTime) * 100)
26
+ }
27
+
28
+ if (currentState === "measuring" || currentState === "countup") {
29
+ // Count-up modes: progress based on minute steps (0-100% per minute)
30
+ return ((remainingTime / 60) % 1) * 100
31
+ }
32
+
33
+ return 0
34
+ }
35
+
36
+ /**
37
+ * Calculates exact progress position based on elapsed time
38
+ */
39
+ export const calculateExactProgress = (
40
+ currentState: string,
41
+ totalElapsed: number,
42
+ maxTime: number | null,
43
+ ): number => {
44
+ if (currentState === "countdown" && maxTime !== null) {
45
+ return Math.min(100, (totalElapsed / maxTime) * 100)
46
+ }
47
+
48
+ if (currentState === "measuring" || currentState === "countup") {
49
+ return ((totalElapsed / 60) % 1) * 100
50
+ }
51
+
52
+ return 0
53
+ }