@wandelbots/wandelbots-js-react-components 2.40.0 → 2.41.0-pr.feature-seperate-timer.383.38eb55e

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 (55) hide show
  1. package/dist/components/CycleTimer/DefaultVariant.d.ts.map +1 -1
  2. package/dist/components/CycleTimer/SmallVariant.d.ts.map +1 -1
  3. package/dist/components/CycleTimer/index.d.ts +4 -5
  4. package/dist/components/CycleTimer/index.d.ts.map +1 -1
  5. package/dist/components/CycleTimer/types.d.ts +2 -3
  6. package/dist/components/CycleTimer/types.d.ts.map +1 -1
  7. package/dist/components/CycleTimer/useTimerLogic.d.ts +1 -2
  8. package/dist/components/CycleTimer/useTimerLogic.d.ts.map +1 -1
  9. package/dist/components/TabBar.d.ts +2 -0
  10. package/dist/components/TabBar.d.ts.map +1 -1
  11. package/dist/components/Timer/Timer.d.ts +3 -0
  12. package/dist/components/Timer/Timer.d.ts.map +1 -0
  13. package/dist/components/Timer/TimerDefaultVariant.d.ts +10 -0
  14. package/dist/components/Timer/TimerDefaultVariant.d.ts.map +1 -0
  15. package/dist/components/Timer/TimerSmallVariant.d.ts +11 -0
  16. package/dist/components/Timer/TimerSmallVariant.d.ts.map +1 -0
  17. package/dist/components/Timer/index.d.ts +19 -0
  18. package/dist/components/Timer/index.d.ts.map +1 -0
  19. package/dist/components/Timer/types.d.ts +36 -0
  20. package/dist/components/Timer/types.d.ts.map +1 -0
  21. package/dist/components/Timer/useTimerAnimations.d.ts +11 -0
  22. package/dist/components/Timer/useTimerAnimations.d.ts.map +1 -0
  23. package/dist/components/Timer/useTimerLogic.d.ts +20 -0
  24. package/dist/components/Timer/useTimerLogic.d.ts.map +1 -0
  25. package/dist/components/Timer/utils.d.ts +9 -0
  26. package/dist/components/Timer/utils.d.ts.map +1 -0
  27. package/dist/components/jogging/PoseCartesianValues.d.ts.map +1 -1
  28. package/dist/components/jogging/PoseJointValues.d.ts.map +1 -1
  29. package/dist/index.cjs +48 -48
  30. package/dist/index.cjs.map +1 -1
  31. package/dist/index.d.ts +1 -0
  32. package/dist/index.d.ts.map +1 -1
  33. package/dist/index.js +5769 -5328
  34. package/dist/index.js.map +1 -1
  35. package/package.json +1 -1
  36. package/src/components/CycleTimer/DefaultVariant.tsx +0 -2
  37. package/src/components/CycleTimer/SmallVariant.tsx +2 -5
  38. package/src/components/CycleTimer/index.tsx +4 -5
  39. package/src/components/CycleTimer/types.ts +1 -3
  40. package/src/components/CycleTimer/useTimerLogic.ts +40 -96
  41. package/src/components/CycleTimer/utils.ts +3 -3
  42. package/src/components/TabBar.tsx +35 -8
  43. package/src/components/Timer/Timer.ts +2 -0
  44. package/src/components/Timer/TimerDefaultVariant.tsx +140 -0
  45. package/src/components/Timer/TimerSmallVariant.tsx +140 -0
  46. package/src/components/Timer/index.tsx +101 -0
  47. package/src/components/Timer/types.ts +38 -0
  48. package/src/components/Timer/useTimerAnimations.ts +94 -0
  49. package/src/components/Timer/useTimerLogic.ts +214 -0
  50. package/src/components/Timer/utils.ts +15 -0
  51. package/src/components/jogging/PoseCartesianValues.tsx +78 -67
  52. package/src/components/jogging/PoseJointValues.tsx +77 -67
  53. package/src/i18n/locales/de/translations.json +1 -0
  54. package/src/i18n/locales/en/translations.json +1 -0
  55. package/src/index.ts +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.40.0",
3
+ "version": "2.41.0-pr.feature-seperate-timer.383.38eb55e",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -114,7 +114,6 @@ export const DefaultVariant = ({
114
114
  showLabels &&
115
115
  !hasError &&
116
116
  currentState !== "idle" &&
117
- currentState !== "countup" &&
118
117
  currentState !== "success"
119
118
  }
120
119
  timeout={300}
@@ -277,7 +276,6 @@ export const DefaultVariant = ({
277
276
  showLabels &&
278
277
  !hasError &&
279
278
  currentState !== "idle" &&
280
- currentState !== "countup" &&
281
279
  currentState !== "success"
282
280
  }
283
281
  timeout={300}
@@ -31,7 +31,7 @@ export const SmallVariant = ({
31
31
  } = animationState
32
32
 
33
33
  // Simple text-only mode for compact variant in certain states
34
- if (compact && (currentState === "countup" || currentState === "idle")) {
34
+ if (compact && currentState === "idle") {
35
35
  return (
36
36
  <Box
37
37
  className={className}
@@ -72,10 +72,7 @@ export const SmallVariant = ({
72
72
  }}
73
73
  >
74
74
  {/* Animated progress ring icon */}
75
- {!(
76
- currentState === "countup" ||
77
- (currentState === "idle" && compact)
78
- ) && (
75
+ {!(currentState === "idle" && compact) && (
79
76
  <Box
80
77
  sx={{
81
78
  width: 20,
@@ -8,19 +8,18 @@ import { useAnimations } from "./useAnimations"
8
8
  import { useTimerLogic } from "./useTimerLogic"
9
9
 
10
10
  /**
11
- * A circular gauge timer component that shows the remaining time of a cycle or counts up
11
+ * A circular gauge timer component for cycle-specific timing operations
12
12
  *
13
13
  * Features:
14
14
  * - Custom SVG circular gauge with 264px diameter and 40px thickness
15
- * - Multiple states: idle, measuring, measured, countdown, countup, success
15
+ * - Multiple states: idle, measuring, measured, countdown, success
16
16
  * - Idle state: shows "Waiting for program cycle" with transparent inner circle
17
17
  * - Measuring state: counts up with "Cycle Time" / "measuring..." labels
18
18
  * - Measured state: shows final time with "Cycle Time" / "determined" labels in pulsating green
19
19
  * - Countdown mode: shows remaining time prominently, counts down to zero
20
- * - Count-up mode: shows elapsed time without special labels
21
20
  * - Success state: brief green flash after cycle completion
22
21
  * - Displays appropriate labels based on state
23
- * - Automatically counts down/up and triggers callback when reaching zero (countdown only)
22
+ * - Automatically counts down and triggers callback when reaching zero
24
23
  * - Full timer control: start, pause, resume functionality
25
24
  * - Support for starting with elapsed time (resume mid-cycle)
26
25
  * - Error state support: pauses timer and shows error styling (red color)
@@ -28,7 +27,7 @@ import { useTimerLogic } from "./useTimerLogic"
28
27
  * - Pulsating text animation for completed measuring state
29
28
  * - Fully localized with i18next
30
29
  * - Material-UI theming integration
31
- * - Small variant with animated progress icon (gauge border only) next to text or simple text-only mode
30
+ * - Small variant with animated progress icon (gauge border only) next to text
32
31
  */
33
32
  export const CycleTimer = externalizeComponent(
34
33
  observer(
@@ -3,13 +3,11 @@ export type CycleTimerState =
3
3
  | "measuring" // Counting up without max time, showing "Cycle Time" / "measuring..."
4
4
  | "measured" // Completed measuring state showing "Cycle Time" / "determined" with pulsating green text
5
5
  | "countdown" // Counting down with max time
6
- | "countup" // Simple count up without special text
7
6
  | "success" // Brief success state after cycle completion
8
7
 
9
8
  export interface CycleTimerControls {
10
- startNewCycle: (maxTimeSeconds?: number, elapsedSeconds?: number) => void
9
+ startNewCycle: (maxTimeSeconds: number, elapsedSeconds?: number) => void
11
10
  startMeasuring: (elapsedSeconds?: number) => void
12
- startCountUp: (elapsedSeconds?: number) => void
13
11
  setIdle: () => void
14
12
  completeMeasuring: () => void
15
13
  pause: () => void
@@ -88,114 +88,66 @@ export const useTimerLogic = ({
88
88
  [autoStart, progressInterpolator],
89
89
  )
90
90
 
91
- const startCountUp = useCallback(
92
- (elapsedSeconds: number = 0) => {
93
- const initialProgress = ((elapsedSeconds / 60) % 1) * 100
94
- setTimerState((prev) => ({
95
- ...prev,
96
- currentState: "countup",
97
- maxTime: null,
98
- remainingTime: elapsedSeconds,
99
- isPausedState: false,
100
- currentProgress: initialProgress, // Immediately set progress
101
- }))
102
- pausedTimeRef.current = 0
103
-
104
- progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
105
-
106
- if (autoStart) {
107
- startTimeRef.current = Date.now() - elapsedSeconds * 1000
108
- setTimerState((prev) => ({ ...prev, isRunning: true }))
109
- } else {
110
- startTimeRef.current = null
111
- }
112
- },
113
- [autoStart, progressInterpolator],
114
- )
115
-
116
91
  const startNewCycle = useCallback(
117
- (maxTimeSeconds?: number, elapsedSeconds: number = 0) => {
92
+ (maxTimeSeconds: number, elapsedSeconds: number = 0) => {
118
93
  // Stop any running timer first to prevent conflicts
119
94
  setTimerState((prev) => ({ ...prev, isRunning: false }))
120
95
  startTimeRef.current = null
121
96
 
122
- const newState = maxTimeSeconds !== undefined ? "countdown" : "countup"
123
97
  setTimerState((prev) => ({
124
98
  ...prev,
125
- currentState: newState,
126
- maxTime: maxTimeSeconds ?? null,
99
+ currentState: "countdown",
100
+ maxTime: maxTimeSeconds,
127
101
  isPausedState: false,
128
102
  }))
129
103
  pausedTimeRef.current = 0
130
104
 
131
- if (maxTimeSeconds !== undefined) {
132
- // Count-down mode
133
- const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
134
- const initialProgress =
135
- elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
136
-
137
- setTimerState((prev) => ({
138
- ...prev,
139
- remainingTime: remainingSeconds,
140
- currentProgress: initialProgress, // Immediately set progress
141
- }))
142
-
143
- progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
144
-
145
- if (remainingSeconds === 0) {
146
- setTimerState((prev) => ({ ...prev, isRunning: false }))
147
- startTimeRef.current = null
148
- if (onCycleEnd) {
149
- queueMicrotask(() => onCycleEnd())
150
- }
151
- } else if (autoStart) {
152
- setTimeout(() => {
153
- startTimeRef.current = Date.now() - elapsedSeconds * 1000
154
- setTimerState((prev) => ({ ...prev, isRunning: true }))
155
- }, 0)
156
- } else {
157
- startTimeRef.current = null
105
+ // Count-down mode
106
+ const remainingSeconds = Math.max(0, maxTimeSeconds - elapsedSeconds)
107
+ const initialProgress =
108
+ elapsedSeconds > 0 ? (elapsedSeconds / maxTimeSeconds) * 100 : 0
109
+
110
+ setTimerState((prev) => ({
111
+ ...prev,
112
+ remainingTime: remainingSeconds,
113
+ currentProgress: initialProgress, // Immediately set progress
114
+ }))
115
+
116
+ progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
117
+
118
+ if (remainingSeconds === 0) {
119
+ setTimerState((prev) => ({ ...prev, isRunning: false }))
120
+ startTimeRef.current = null
121
+ if (onCycleEnd) {
122
+ queueMicrotask(() => onCycleEnd())
158
123
  }
124
+ } else if (autoStart) {
125
+ setTimeout(() => {
126
+ startTimeRef.current = Date.now() - elapsedSeconds * 1000
127
+ setTimerState((prev) => ({ ...prev, isRunning: true }))
128
+ }, 0)
159
129
  } else {
160
- // Count-up mode
161
- const initialProgress = ((elapsedSeconds / 60) % 1) * 100
162
- setTimerState((prev) => ({
163
- ...prev,
164
- remainingTime: elapsedSeconds,
165
- currentProgress: initialProgress, // Immediately set progress
166
- }))
167
-
168
- progressInterpolator.setImmediate([initialProgress]) // Use setImmediate for instant reset
169
-
170
- if (autoStart) {
171
- setTimeout(() => {
172
- startTimeRef.current = Date.now() - elapsedSeconds * 1000
173
- setTimerState((prev) => ({ ...prev, isRunning: true }))
174
- }, 0)
175
- } else {
176
- startTimeRef.current = null
177
- }
130
+ startTimeRef.current = null
178
131
  }
179
132
  },
180
133
  [autoStart, onCycleEnd, progressInterpolator],
181
134
  )
182
135
 
183
136
  const completeMeasuring = useCallback(() => {
184
- if (timerState.currentState === "measuring") {
185
- setTimerState((prev) => ({
186
- ...prev,
187
- isRunning: false,
188
- currentState: "measured",
189
- }))
190
- startTimeRef.current = null
137
+ // Always trigger completion regardless of current state
138
+ setTimerState((prev) => ({
139
+ ...prev,
140
+ isRunning: false,
141
+ currentState: "measured",
142
+ }))
143
+ startTimeRef.current = null
191
144
 
192
- onStartPulsating(() => {
193
- if (onMeasuringComplete) {
194
- onMeasuringComplete()
195
- }
196
- })
197
- }
198
- }, [timerState.currentState, onStartPulsating, onMeasuringComplete])
145
+ onStartPulsating(() => {
146
+ if (onMeasuringComplete) {
147
+ onMeasuringComplete()
148
+ }
149
+ })
150
+ }, [onStartPulsating, onMeasuringComplete])
199
151
 
200
152
  const pause = useCallback(() => {
201
153
  if (startTimeRef.current && timerState.isRunning) {
@@ -316,13 +268,6 @@ export const useTimerLogic = ({
316
268
  }))
317
269
  const progress = ((elapsed / 60) % 1) * 100
318
270
  progressInterpolator.setTarget([progress])
319
- } else if (timerState.currentState === "countup") {
320
- setTimerState((prev) => ({
321
- ...prev,
322
- remainingTime: Math.floor(elapsed),
323
- }))
324
- const progress = ((elapsed / 60) % 1) * 100
325
- progressInterpolator.setTarget([progress])
326
271
  }
327
272
 
328
273
  if (timerState.isRunning) {
@@ -375,7 +320,6 @@ export const useTimerLogic = ({
375
320
  controls: {
376
321
  startNewCycle,
377
322
  startMeasuring,
378
- startCountUp,
379
323
  setIdle,
380
324
  completeMeasuring,
381
325
  pause,
@@ -25,8 +25,8 @@ export const calculateProgress = (
25
25
  return Math.min(100, (elapsed / maxTime) * 100)
26
26
  }
27
27
 
28
- if (currentState === "measuring" || currentState === "countup") {
29
- // Count-up modes: progress based on minute steps (0-100% per minute)
28
+ if (currentState === "measuring") {
29
+ // Measuring mode: progress based on minute steps (0-100% per minute)
30
30
  return ((remainingTime / 60) % 1) * 100
31
31
  }
32
32
 
@@ -45,7 +45,7 @@ export const calculateExactProgress = (
45
45
  return Math.min(100, (totalElapsed / maxTime) * 100)
46
46
  }
47
47
 
48
- if (currentState === "measuring" || currentState === "countup") {
48
+ if (currentState === "measuring") {
49
49
  return ((totalElapsed / 60) % 1) * 100
50
50
  }
51
51
 
@@ -1,7 +1,7 @@
1
1
  import type { SxProps } from "@mui/material"
2
2
  import { Box, Tab, Tabs } from "@mui/material"
3
3
  import { observer } from "mobx-react-lite"
4
- import { useState } from "react"
4
+ import { useEffect, useState } from "react"
5
5
  import { externalizeComponent } from "../externalizeComponent"
6
6
 
7
7
  export interface TabItem {
@@ -18,6 +18,8 @@ export interface TabItem {
18
18
  export interface TabBarProps {
19
19
  /** Array of tab items to display */
20
20
  items: TabItem[]
21
+ /** Controlled active tab index */
22
+ activeTab?: number
21
23
  /** Default active tab index */
22
24
  defaultActiveTab?: number
23
25
  /** Callback when tab changes */
@@ -57,14 +59,39 @@ function TabPanel(props: TabPanelProps) {
57
59
  */
58
60
  export const TabBar = externalizeComponent(
59
61
  observer((props: TabBarProps) => {
60
- const { items, defaultActiveTab = 0, onTabChange, sx, ref } = props
61
- const [activeTab, setActiveTab] = useState(defaultActiveTab)
62
+ const {
63
+ items,
64
+ activeTab,
65
+ defaultActiveTab = 0,
66
+ onTabChange,
67
+ sx,
68
+ ref,
69
+ } = props
70
+ const isControlled = activeTab !== undefined
71
+ const [uncontrolledActiveTab, setUncontrolledActiveTab] =
72
+ useState(defaultActiveTab)
73
+
74
+ // Keep uncontrolled state in range when items change
75
+ useEffect(() => {
76
+ if (isControlled) return
77
+ if (items.length === 0) return
78
+ if (
79
+ uncontrolledActiveTab < 0 ||
80
+ uncontrolledActiveTab > items.length - 1
81
+ ) {
82
+ setUncontrolledActiveTab(0)
83
+ }
84
+ }, [items.length, isControlled, uncontrolledActiveTab])
85
+
86
+ const currentValue = isControlled ? activeTab! : uncontrolledActiveTab
62
87
 
63
88
  const handleTabChange = (
64
89
  _event: React.SyntheticEvent,
65
90
  newValue: number,
66
91
  ) => {
67
- setActiveTab(newValue)
92
+ if (!isControlled) {
93
+ setUncontrolledActiveTab(newValue)
94
+ }
68
95
  onTabChange?.(newValue)
69
96
  }
70
97
 
@@ -76,7 +103,7 @@ export const TabBar = externalizeComponent(
76
103
  {/* Tabs */}
77
104
  <Box sx={{ px: 0, py: 0 }}>
78
105
  <Tabs
79
- value={activeTab}
106
+ value={currentValue}
80
107
  onChange={handleTabChange}
81
108
  sx={{
82
109
  minHeight: "32px",
@@ -104,11 +131,11 @@ export const TabBar = externalizeComponent(
104
131
  backgroundColor: (theme) =>
105
132
  theme.palette.backgroundPaperElevation?.[11] || "#32344B",
106
133
  color: "text.primary",
107
- opacity: activeTab === index ? 1 : 0.38,
134
+ opacity: currentValue === index ? 1 : 0.38,
108
135
  fontSize: "13px",
109
136
  transition: "all 0.2s ease-in-out",
110
137
  "&:hover": {
111
- opacity: activeTab === index ? 1 : 0.6,
138
+ opacity: currentValue === index ? 1 : 0.6,
112
139
  },
113
140
  "&.Mui-selected": {
114
141
  opacity: 1,
@@ -134,7 +161,7 @@ export const TabBar = externalizeComponent(
134
161
  {/* Tab Content */}
135
162
  <Box sx={{ flex: 1, overflow: "auto" }}>
136
163
  {items.map((item, index) => (
137
- <TabPanel key={item.id} value={activeTab} index={index}>
164
+ <TabPanel key={item.id} value={currentValue} index={index}>
138
165
  {item.content}
139
166
  </TabPanel>
140
167
  ))}
@@ -0,0 +1,2 @@
1
+ export { Timer } from "./index"
2
+ export type { TimerControls, TimerProps } from "./types"
@@ -0,0 +1,140 @@
1
+ import { Box, Fade, Typography } from "@mui/material"
2
+ import { Gauge } from "@mui/x-charts"
3
+ import { useTheme } from "@mui/material/styles"
4
+ import { useTranslation } from "react-i18next"
5
+ import type { TimerState, TimerAnimationState } from "./types"
6
+ import { formatTime } from "./utils"
7
+
8
+ interface TimerDefaultVariantProps {
9
+ timerState: TimerState
10
+ animationState: TimerAnimationState
11
+ hasError: boolean
12
+ className?: string
13
+ }
14
+
15
+ export const TimerDefaultVariant = ({
16
+ timerState,
17
+ animationState,
18
+ hasError,
19
+ className,
20
+ }: TimerDefaultVariantProps) => {
21
+ const { t } = useTranslation()
22
+ const theme = useTheme()
23
+ const { elapsedTime, currentProgress } = timerState
24
+ const { showErrorAnimation, showPauseAnimation, showMainText } = animationState
25
+
26
+ return (
27
+ <Box
28
+ className={className}
29
+ sx={{
30
+ position: "relative",
31
+ width: 264,
32
+ height: 264,
33
+ display: "flex",
34
+ alignItems: "center",
35
+ justifyContent: "center",
36
+ }}
37
+ >
38
+ <Gauge
39
+ width={264}
40
+ height={264}
41
+ value={currentProgress}
42
+ valueMin={0}
43
+ valueMax={100}
44
+ innerRadius="85%"
45
+ outerRadius="100%"
46
+ margin={0}
47
+ skipAnimation={true}
48
+ text={() => ""}
49
+ sx={{
50
+ opacity: showPauseAnimation || showErrorAnimation ? 0.6 : 1,
51
+ transition: "opacity 0.5s ease-out",
52
+ [`& .MuiGauge-valueArc`]: {
53
+ fill: hasError
54
+ ? theme.palette.error.light
55
+ : theme.palette.success.main,
56
+ transition: "fill 0.5s ease-out",
57
+ },
58
+ [`& .MuiGauge-referenceArc`]: {
59
+ fill: "#171927",
60
+ stroke: "transparent",
61
+ strokeWidth: 0,
62
+ transition:
63
+ "fill 0.5s ease-out, stroke 0.5s ease-out, stroke-width 0.5s ease-out",
64
+ },
65
+ [`& .MuiGauge-valueText`]: {
66
+ display: "none",
67
+ },
68
+ [`& .MuiGauge-text`]: {
69
+ display: "none",
70
+ },
71
+ }}
72
+ />
73
+
74
+ {/* Center content overlay */}
75
+ <Box
76
+ sx={{
77
+ position: "absolute",
78
+ top: "50%",
79
+ left: "50%",
80
+ transform: "translate(-50%, -50%)",
81
+ width: 225,
82
+ height: 225,
83
+ borderRadius: "50%",
84
+ backgroundColor: "#292B3F",
85
+ display: "flex",
86
+ flexDirection: "column",
87
+ alignItems: "center",
88
+ justifyContent: "center",
89
+ textAlign: "center",
90
+ gap: 1,
91
+ transition: "background-color 0.5s ease-out",
92
+ }}
93
+ >
94
+ {/* Main display */}
95
+ <Box
96
+ sx={{
97
+ position: "relative",
98
+ height: "48px",
99
+ display: "flex",
100
+ alignItems: "center",
101
+ justifyContent: "center",
102
+ marginBottom: 0.5,
103
+ }}
104
+ >
105
+ {/* Error text */}
106
+ <Fade in={showMainText && hasError} timeout={200}>
107
+ <Typography
108
+ variant="h6"
109
+ sx={{
110
+ position: "absolute",
111
+ fontSize: "16px",
112
+ fontWeight: 500,
113
+ color: theme.palette.error.light,
114
+ }}
115
+ >
116
+ {t("timer.error")}
117
+ </Typography>
118
+ </Fade>
119
+
120
+ {/* Timer display */}
121
+ <Fade in={showMainText && !hasError} timeout={300}>
122
+ <Typography
123
+ variant="h1"
124
+ sx={{
125
+ position: "absolute",
126
+ fontSize: "48px",
127
+ fontWeight: 500,
128
+ color: theme.palette.text.primary,
129
+ lineHeight: 1,
130
+ letterSpacing: "-0.5px",
131
+ }}
132
+ >
133
+ {formatTime(elapsedTime)}
134
+ </Typography>
135
+ </Fade>
136
+ </Box>
137
+ </Box>
138
+ </Box>
139
+ )
140
+ }
@@ -0,0 +1,140 @@
1
+ import { Box, Typography } from "@mui/material"
2
+ import { useTheme } from "@mui/material/styles"
3
+ import { useTranslation } from "react-i18next"
4
+ import type { TimerState, TimerAnimationState } from "./types"
5
+ import { formatTime } from "./utils"
6
+
7
+ interface TimerSmallVariantProps {
8
+ timerState: TimerState
9
+ animationState: TimerAnimationState
10
+ hasError: boolean
11
+ compact: boolean
12
+ className?: string
13
+ }
14
+
15
+ export const TimerSmallVariant = ({
16
+ timerState,
17
+ animationState,
18
+ hasError,
19
+ compact,
20
+ className,
21
+ }: TimerSmallVariantProps) => {
22
+ const { t } = useTranslation()
23
+ const theme = useTheme()
24
+ const { elapsedTime, currentProgress } = timerState
25
+ const { showErrorAnimation, showPauseAnimation } = animationState
26
+
27
+ // Simple text-only mode for compact variant
28
+ if (compact) {
29
+ return (
30
+ <Box
31
+ className={className}
32
+ sx={{
33
+ display: "flex",
34
+ alignItems: "center",
35
+ m: 0,
36
+ }}
37
+ >
38
+ <Typography
39
+ variant="body2"
40
+ sx={{
41
+ color: hasError
42
+ ? theme.palette.error.light
43
+ : theme.palette.text.primary,
44
+ fontSize: "14px",
45
+ transition: "color 0.5s ease-out",
46
+ }}
47
+ >
48
+ {hasError ? t("timer.error") : formatTime(elapsedTime)}
49
+ </Typography>
50
+ </Box>
51
+ )
52
+ }
53
+
54
+ return (
55
+ <Box
56
+ className={className}
57
+ sx={{
58
+ display: "flex",
59
+ alignItems: "center",
60
+ m: 0,
61
+ gap: 1,
62
+ }}
63
+ >
64
+ {/* Animated progress ring icon */}
65
+ <Box
66
+ sx={{
67
+ width: 20,
68
+ height: 20,
69
+ display: "flex",
70
+ alignItems: "center",
71
+ justifyContent: "center",
72
+ opacity: showPauseAnimation || showErrorAnimation ? 0.6 : 1,
73
+ transition: "opacity 0.5s ease-out",
74
+ }}
75
+ >
76
+ <svg
77
+ width="20"
78
+ height="20"
79
+ viewBox="0 0 20 20"
80
+ style={{ transform: "rotate(-90deg)" }}
81
+ role="img"
82
+ aria-label="Timer progress"
83
+ >
84
+ <circle
85
+ cx="10"
86
+ cy="10"
87
+ r="8"
88
+ fill="none"
89
+ stroke={
90
+ hasError
91
+ ? theme.palette.error.light
92
+ : theme.palette.success.main
93
+ }
94
+ strokeWidth="2"
95
+ opacity={0.3}
96
+ style={{
97
+ transition: "stroke 0.8s ease-in-out, opacity 2s ease-in-out",
98
+ }}
99
+ />
100
+ <circle
101
+ cx="10"
102
+ cy="10"
103
+ r="8"
104
+ fill="none"
105
+ stroke={
106
+ hasError
107
+ ? theme.palette.error.light
108
+ : theme.palette.success.main
109
+ }
110
+ strokeWidth="2"
111
+ strokeLinecap="round"
112
+ strokeDasharray={`${2 * Math.PI * 8}`}
113
+ strokeDashoffset={`${2 * Math.PI * 8 * (1 - currentProgress / 100)}`}
114
+ style={{
115
+ transition:
116
+ "stroke-dashoffset 0.1s ease-out, stroke 0.8s ease-in-out, opacity 2s ease-in-out",
117
+ }}
118
+ />
119
+ </svg>
120
+ </Box>
121
+
122
+ {/* Timer text display */}
123
+ <Typography
124
+ variant="body2"
125
+ sx={{
126
+ color: hasError
127
+ ? theme.palette.error.light
128
+ : theme.palette.text.primary,
129
+ fontSize: "14px",
130
+ lineHeight: "normal",
131
+ letterSpacing: "normal",
132
+ transition:
133
+ "color 0.8s ease-in-out, font-size 0.3s ease-out, opacity 2s ease-in-out",
134
+ }}
135
+ >
136
+ {hasError ? t("timer.error") : formatTime(elapsedTime)}
137
+ </Typography>
138
+ </Box>
139
+ )
140
+ }