@wandelbots/wandelbots-js-react-components 2.51.0 → 2.52.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wandelbots/wandelbots-js-react-components",
3
- "version": "2.51.0",
3
+ "version": "2.52.0",
4
4
  "description": "React UI toolkit for building applications on top of the Wandelbots platform",
5
5
  "type": "module",
6
6
  "sideEffects": false,
@@ -202,19 +202,19 @@ export const SmallVariant = ({
202
202
  </>
203
203
  ) : currentState === "measuring" ? (
204
204
  compact ? (
205
- `${formatTimeLocalized(remainingTime, i18n.language)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
205
+ formatTimeLocalized(remainingTime, i18n.language)
206
206
  ) : (
207
207
  `${formatTimeLocalized(remainingTime, i18n.language)} / ${t("CycleTimer.Measuring.lb", "measuring...")}`
208
208
  )
209
209
  ) : currentState === "measured" ? (
210
210
  compact ? (
211
- `${formatTimeLocalized(remainingTime, i18n.language)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
211
+ formatTimeLocalized(remainingTime, i18n.language)
212
212
  ) : (
213
213
  `${formatTimeLocalized(remainingTime, i18n.language)} / ${t("CycleTimer.Determined.lb", "determined")}`
214
214
  )
215
215
  ) : currentState === "countdown" && maxTime !== null ? (
216
216
  compact ? (
217
- `${formatTimeLocalized(remainingTime, i18n.language)} ${t("CycleTimer.Time.lb", { time: "" }).replace(/\s*$/, "")}`
217
+ formatTimeLocalized(remainingTime, i18n.language)
218
218
  ) : (
219
219
  `${formatTimeLocalized(remainingTime, i18n.language)} / ${t("CycleTimer.Time.lb", { time: formatTimeLocalized(maxTime, i18n.language) })}`
220
220
  )
@@ -1,8 +1,8 @@
1
1
  import { Box, Typography } from "@mui/material"
2
2
  import { useTheme } from "@mui/material/styles"
3
3
  import { useTranslation } from "react-i18next"
4
- import type { TimerState, TimerAnimationState } from "./types"
5
- import { formatTime } from "./utils"
4
+ import type { TimerAnimationState, TimerState } from "./types"
5
+ import { formatTimeLocalized } from "./utils"
6
6
 
7
7
  interface TimerSmallVariantProps {
8
8
  timerState: TimerState
@@ -19,7 +19,7 @@ export const TimerSmallVariant = ({
19
19
  compact,
20
20
  className,
21
21
  }: TimerSmallVariantProps) => {
22
- const { t } = useTranslation()
22
+ const { t, i18n } = useTranslation()
23
23
  const theme = useTheme()
24
24
  const { elapsedTime, currentProgress } = timerState
25
25
  const { showErrorAnimation, showPauseAnimation } = animationState
@@ -45,7 +45,9 @@ export const TimerSmallVariant = ({
45
45
  transition: "color 0.5s ease-out",
46
46
  }}
47
47
  >
48
- {hasError ? t("timer.error") : formatTime(elapsedTime)}
48
+ {hasError
49
+ ? t("timer.error")
50
+ : formatTimeLocalized(elapsedTime, i18n.language)}
49
51
  </Typography>
50
52
  </Box>
51
53
  )
@@ -87,9 +89,7 @@ export const TimerSmallVariant = ({
87
89
  r="8"
88
90
  fill="none"
89
91
  stroke={
90
- hasError
91
- ? theme.palette.error.light
92
- : theme.palette.success.main
92
+ hasError ? theme.palette.error.light : theme.palette.success.main
93
93
  }
94
94
  strokeWidth="2"
95
95
  opacity={0.3}
@@ -103,9 +103,7 @@ export const TimerSmallVariant = ({
103
103
  r="8"
104
104
  fill="none"
105
105
  stroke={
106
- hasError
107
- ? theme.palette.error.light
108
- : theme.palette.success.main
106
+ hasError ? theme.palette.error.light : theme.palette.success.main
109
107
  }
110
108
  strokeWidth="2"
111
109
  strokeLinecap="round"
@@ -133,7 +131,9 @@ export const TimerSmallVariant = ({
133
131
  "color 0.8s ease-in-out, font-size 0.3s ease-out, opacity 2s ease-in-out",
134
132
  }}
135
133
  >
136
- {hasError ? t("timer.error") : formatTime(elapsedTime)}
134
+ {hasError
135
+ ? t("timer.error")
136
+ : formatTimeLocalized(elapsedTime, i18n.language)}
137
137
  </Typography>
138
138
  </Box>
139
139
  )
@@ -1,10 +1,82 @@
1
1
  /**
2
- * Formats time in seconds to MM:SS format
2
+ * Formats time in seconds to D:HH:MM:SS, H:MM:SS or MM:SS format
3
+ * Used for the default (large) timer variant
4
+ * Automatically includes days and hours as needed for clarity
3
5
  */
4
6
  export const formatTime = (seconds: number): string => {
5
- const minutes = Math.floor(seconds / 60)
7
+ const days = Math.floor(seconds / 86400)
8
+ const hours = Math.floor((seconds % 86400) / 3600)
9
+ const minutes = Math.floor((seconds % 3600) / 60)
6
10
  const remainingSeconds = seconds % 60
7
- return `${minutes}:${remainingSeconds.toString().padStart(2, "0")}`
11
+
12
+ // Build time parts array
13
+ const parts: string[] = []
14
+
15
+ if (days > 0) {
16
+ parts.push(days.toString())
17
+ parts.push(hours.toString().padStart(2, "0"))
18
+ parts.push(minutes.toString().padStart(2, "0"))
19
+ parts.push(remainingSeconds.toString().padStart(2, "0"))
20
+ } else if (hours > 0) {
21
+ parts.push(hours.toString())
22
+ parts.push(minutes.toString().padStart(2, "0"))
23
+ parts.push(remainingSeconds.toString().padStart(2, "0"))
24
+ } else {
25
+ parts.push(minutes.toString())
26
+ parts.push(remainingSeconds.toString().padStart(2, "0"))
27
+ }
28
+
29
+ return parts.join(":")
30
+ }
31
+
32
+ /**
33
+ * Formats time in seconds to a localized human-readable format
34
+ * Used for the small timer variant
35
+ * Examples: "2h 30m 15s", "45m 30s", "30s"
36
+ * Falls back to English units if Intl.DurationFormat is not available
37
+ */
38
+ export const formatTimeLocalized = (
39
+ seconds: number,
40
+ locale?: string,
41
+ ): string => {
42
+ const days = Math.floor(seconds / 86400)
43
+ const hours = Math.floor((seconds % 86400) / 3600)
44
+ const minutes = Math.floor((seconds % 3600) / 60)
45
+ const remainingSeconds = seconds % 60
46
+
47
+ // Try using Intl.DurationFormat if available (newer browsers)
48
+ if (typeof Intl !== "undefined" && "DurationFormat" in Intl) {
49
+ try {
50
+ const duration: Record<string, number> = {}
51
+ if (days > 0) duration.days = days
52
+ if (hours > 0) duration.hours = hours
53
+ if (minutes > 0) duration.minutes = minutes
54
+ if (remainingSeconds > 0 || Object.keys(duration).length === 0) {
55
+ duration.seconds = remainingSeconds
56
+ }
57
+
58
+ // @ts-expect-error
59
+ // TODO: Remove suppression once Intl.DurationFormat is supported in TypeScript types.
60
+ // See: https://github.com/microsoft/TypeScript/issues/53971
61
+ // DurationFormat is a proposed API and not yet available in TypeScript's standard library types.
62
+ const formatter = new Intl.DurationFormat(locale, { style: "narrow" })
63
+ return formatter.format(duration)
64
+ } catch {
65
+ // Fall through to manual formatting
66
+ }
67
+ }
68
+
69
+ // Manual formatting with compact units
70
+ const parts: string[] = []
71
+
72
+ if (days > 0) parts.push(`${days}d`)
73
+ if (hours > 0) parts.push(`${hours}h`)
74
+ if (minutes > 0) parts.push(`${minutes}m`)
75
+ if (remainingSeconds > 0 || parts.length === 0) {
76
+ parts.push(`${remainingSeconds}s`)
77
+ }
78
+
79
+ return parts.join(" ")
8
80
  }
9
81
 
10
82
  /**