@seamapi/react 1.61.0 → 1.61.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.
Files changed (127) hide show
  1. package/README.md +1 -1
  2. package/dist/elements.js +3679 -3727
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +134 -25
  5. package/dist/index.css.map +1 -1
  6. package/dist/index.min.css +1 -1
  7. package/dist/index.min.css.map +1 -1
  8. package/lib/dates.d.ts +6 -67
  9. package/lib/dates.js +13 -111
  10. package/lib/dates.js.map +1 -1
  11. package/lib/icons/CheckGreen.d.ts +2 -0
  12. package/lib/icons/CheckGreen.js +7 -0
  13. package/lib/icons/CheckGreen.js.map +1 -0
  14. package/lib/icons/CloseWhite.d.ts +2 -0
  15. package/lib/icons/CloseWhite.js +7 -0
  16. package/lib/icons/CloseWhite.js.map +1 -0
  17. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js +14 -20
  18. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js.map +1 -1
  19. package/lib/seam/components/AccessCodeTable/CodeDetails.js +4 -6
  20. package/lib/seam/components/AccessCodeTable/CodeDetails.js.map +1 -1
  21. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js +8 -8
  22. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js.map +1 -1
  23. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js +2 -2
  24. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js.map +1 -1
  25. package/lib/seam/components/ClimateSettingScheduleDetails/dates.d.ts +1 -0
  26. package/lib/seam/components/ClimateSettingScheduleDetails/dates.js +9 -0
  27. package/lib/seam/components/ClimateSettingScheduleDetails/dates.js.map +1 -0
  28. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleRowDetails.js +4 -6
  29. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleRowDetails.js.map +1 -1
  30. package/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.js +3 -4
  31. package/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.js.map +1 -1
  32. package/lib/seam/components/CreateClimateSettingScheduleForm/CreateClimateSettingScheduleForm.js +3 -4
  33. package/lib/seam/components/CreateClimateSettingScheduleForm/CreateClimateSettingScheduleForm.js.map +1 -1
  34. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +1 -2
  35. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
  36. package/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.js +3 -4
  37. package/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.js.map +1 -1
  38. package/lib/seam/components/SupportedDeviceTable/FilterCategoryMenu.js +12 -9
  39. package/lib/seam/components/SupportedDeviceTable/FilterCategoryMenu.js.map +1 -1
  40. package/lib/ui/AccessCodeForm/AccessCodeForm.d.ts +1 -1
  41. package/lib/ui/AccessCodeForm/AccessCodeForm.js +45 -46
  42. package/lib/ui/AccessCodeForm/AccessCodeForm.js.map +1 -1
  43. package/lib/ui/AccessCodeForm/AccessCodeFormDatePicker.d.ts +8 -7
  44. package/lib/ui/AccessCodeForm/AccessCodeFormDatePicker.js +8 -4
  45. package/lib/ui/AccessCodeForm/AccessCodeFormDatePicker.js.map +1 -1
  46. package/lib/ui/AccessCodeForm/AccessCodeFormTimeZonePicker.d.ts +8 -0
  47. package/lib/ui/AccessCodeForm/AccessCodeFormTimeZonePicker.js +17 -0
  48. package/lib/ui/AccessCodeForm/{AccessCodeFormTimezonePicker.js.map → AccessCodeFormTimeZonePicker.js.map} +1 -1
  49. package/lib/ui/AccessCodeForm/AccessCodeFormTimes.d.ts +3 -2
  50. package/lib/ui/AccessCodeForm/AccessCodeFormTimes.js +3 -2
  51. package/lib/ui/AccessCodeForm/AccessCodeFormTimes.js.map +1 -1
  52. package/lib/ui/ClimateSettingForm/ClimateSettingScheduleForm.d.ts +2 -2
  53. package/lib/ui/ClimateSettingForm/ClimateSettingScheduleForm.js +9 -9
  54. package/lib/ui/ClimateSettingForm/ClimateSettingScheduleForm.js.map +1 -1
  55. package/lib/ui/ClimateSettingForm/ClimateSettingScheduleFormNameAndSchedule.d.ts +3 -3
  56. package/lib/ui/ClimateSettingForm/ClimateSettingScheduleFormNameAndSchedule.js +4 -4
  57. package/lib/ui/ClimateSettingForm/ClimateSettingScheduleFormNameAndSchedule.js.map +1 -1
  58. package/lib/ui/ClimateSettingForm/{ClimateSettingScheduleFormTimezonePicker.d.ts → ClimateSettingScheduleFormTimeZonePicker.d.ts} +2 -2
  59. package/lib/ui/ClimateSettingForm/{ClimateSettingScheduleFormTimezonePicker.js → ClimateSettingScheduleFormTimeZonePicker.js} +5 -5
  60. package/lib/ui/ClimateSettingForm/{ClimateSettingScheduleFormTimezonePicker.js.map → ClimateSettingScheduleFormTimeZonePicker.js.map} +1 -1
  61. package/lib/ui/LoadingToast/LoadingToast.js +12 -14
  62. package/lib/ui/LoadingToast/LoadingToast.js.map +1 -1
  63. package/lib/ui/Menu/Menu.js +32 -25
  64. package/lib/ui/Menu/Menu.js.map +1 -1
  65. package/lib/ui/Snackbar/Snackbar.d.ts +16 -0
  66. package/lib/ui/Snackbar/Snackbar.js +38 -0
  67. package/lib/ui/Snackbar/Snackbar.js.map +1 -0
  68. package/lib/ui/TimeZonePicker/TimeZonePicker.d.ts +8 -0
  69. package/lib/ui/TimeZonePicker/TimeZonePicker.js +28 -0
  70. package/lib/ui/TimeZonePicker/TimeZonePicker.js.map +1 -0
  71. package/lib/ui/device/BatteryStatus.js +10 -6
  72. package/lib/ui/device/BatteryStatus.js.map +1 -1
  73. package/lib/ui/thermostat/ClimateModeMenu.js +4 -4
  74. package/lib/ui/thermostat/ClimateModeMenu.js.map +1 -1
  75. package/lib/ui/use-now.d.ts +2 -0
  76. package/lib/ui/{use-current-time.js → use-now.js} +2 -2
  77. package/lib/ui/use-now.js.map +1 -0
  78. package/lib/version.d.ts +1 -1
  79. package/lib/version.js +1 -1
  80. package/package.json +2 -2
  81. package/src/lib/dates.ts +19 -135
  82. package/src/lib/icons/CheckGreen.tsx +36 -0
  83. package/src/lib/icons/CloseWhite.tsx +36 -0
  84. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx +6 -9
  85. package/src/lib/seam/components/AccessCodeTable/CodeDetails.tsx +2 -3
  86. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.tsx +9 -9
  87. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.tsx +5 -8
  88. package/src/lib/seam/components/ClimateSettingScheduleDetails/dates.ts +10 -0
  89. package/src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleRowDetails.tsx +2 -3
  90. package/src/lib/seam/components/CreateAccessCodeForm/CreateAccessCodeForm.tsx +3 -4
  91. package/src/lib/seam/components/CreateClimateSettingScheduleForm/CreateClimateSettingScheduleForm.tsx +3 -5
  92. package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +1 -6
  93. package/src/lib/seam/components/EditAccessCodeForm/EditAccessCodeForm.tsx +3 -4
  94. package/src/lib/seam/components/SupportedDeviceTable/FilterCategoryMenu.tsx +20 -15
  95. package/src/lib/telemetry/client.ts +1 -0
  96. package/src/lib/ui/AccessCodeForm/AccessCodeForm.tsx +61 -67
  97. package/src/lib/ui/AccessCodeForm/AccessCodeFormDatePicker.tsx +28 -18
  98. package/src/lib/ui/AccessCodeForm/{AccessCodeFormTimezonePicker.tsx → AccessCodeFormTimeZonePicker.tsx} +10 -10
  99. package/src/lib/ui/AccessCodeForm/AccessCodeFormTimes.tsx +9 -5
  100. package/src/lib/ui/ClimateSettingForm/ClimateSettingScheduleForm.tsx +12 -12
  101. package/src/lib/ui/ClimateSettingForm/ClimateSettingScheduleFormNameAndSchedule.tsx +10 -10
  102. package/src/lib/ui/ClimateSettingForm/{ClimateSettingScheduleFormTimezonePicker.tsx → ClimateSettingScheduleFormTimeZonePicker.tsx} +8 -8
  103. package/src/lib/ui/LoadingToast/LoadingToast.tsx +13 -16
  104. package/src/lib/ui/Menu/Menu.tsx +50 -40
  105. package/src/lib/ui/Snackbar/Snackbar.tsx +97 -0
  106. package/src/lib/ui/TimeZonePicker/TimeZonePicker.tsx +69 -0
  107. package/src/lib/ui/device/BatteryStatus.tsx +23 -14
  108. package/src/lib/ui/thermostat/ClimateModeMenu.tsx +4 -4
  109. package/src/lib/ui/{use-current-time.ts → use-now.ts} +1 -1
  110. package/src/lib/version.ts +1 -1
  111. package/src/styles/_access-code-form.scss +4 -4
  112. package/src/styles/_climate-setting-schedule-form.scss +1 -1
  113. package/src/styles/_colors.scss +2 -0
  114. package/src/styles/_loading_toast.scss +5 -16
  115. package/src/styles/_main.scss +4 -2
  116. package/src/styles/_motion.scss +34 -0
  117. package/src/styles/_snackbar.scss +107 -0
  118. package/src/styles/_supported-device-table.scss +9 -0
  119. package/src/styles/{_timezone-picker.scss → _time-zone-picker.scss} +3 -3
  120. package/lib/ui/AccessCodeForm/AccessCodeFormTimezonePicker.d.ts +0 -8
  121. package/lib/ui/AccessCodeForm/AccessCodeFormTimezonePicker.js +0 -17
  122. package/lib/ui/TimezonePicker/TimezonePicker.d.ts +0 -8
  123. package/lib/ui/TimezonePicker/TimezonePicker.js +0 -28
  124. package/lib/ui/TimezonePicker/TimezonePicker.js.map +0 -1
  125. package/lib/ui/use-current-time.d.ts +0 -2
  126. package/lib/ui/use-current-time.js.map +0 -1
  127. package/src/lib/ui/TimezonePicker/TimezonePicker.tsx +0 -70
@@ -42,21 +42,20 @@ export function Menu({
42
42
  backgroundProps,
43
43
  }: MenuProps): JSX.Element | null {
44
44
  const { Provider } = menuContext
45
+ const [documentEl, setDocumentEl] = useState<null | HTMLElement>(null)
46
+ const [bodyEl, setBodyEl] = useState<null | HTMLElement>(null)
45
47
  const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null)
46
- const [documentEl, setDocumentEl] = useState<null | Element>(null)
47
48
  const [contentEl, setContentEl] = useState<HTMLDivElement | null>(null)
48
49
  const [top, setTop] = useState(0)
49
50
  const [left, setLeft] = useState(0)
50
51
 
51
52
  useEffect(() => {
52
- const containers = globalThis.document?.querySelectorAll(
53
- seamComponentsClassName
54
- )
55
- if (containers == null) return
56
- const el = containers[containers.length - 1]
57
- if (el != null) {
58
- setDocumentEl(el)
59
- }
53
+ const documentEl = globalThis.document.documentElement
54
+ setDocumentEl(documentEl)
55
+
56
+ const bodyElements = documentEl?.getElementsByTagName('body')
57
+ if (bodyElements[0] == null) return
58
+ setBodyEl(bodyElements[0])
60
59
  }, [setDocumentEl])
61
60
 
62
61
  const handleClose = (): void => {
@@ -68,18 +67,24 @@ export function Menu({
68
67
  }
69
68
 
70
69
  const setPositions = useCallback(() => {
71
- if (anchorEl == null || contentEl == null || documentEl == null) return
72
-
73
- const { right: containerRight, bottom: containerBottom } =
74
- documentEl.getBoundingClientRect()
70
+ if (
71
+ anchorEl == null ||
72
+ contentEl == null ||
73
+ bodyEl == null ||
74
+ documentEl == null
75
+ )
76
+ return
75
77
 
76
- const { height: anchorHeight } = anchorEl.getBoundingClientRect()
78
+ const containerRight = documentEl.offsetLeft + documentEl.clientWidth
79
+ const containerBottom = documentEl.offsetTop + documentEl.clientHeight
77
80
 
78
- const anchorTop = anchorEl.offsetTop
79
- const anchorLeft = anchorEl.offsetLeft
81
+ const anchorBox = anchorEl.getBoundingClientRect()
82
+ const anchorTop = anchorBox.top + bodyEl.clientTop
83
+ const anchorLeft = anchorBox.left + bodyEl.clientLeft
84
+ const anchorHeight = anchorEl.offsetHeight
80
85
 
81
- const { width: contentWidth, height: contentHeight } =
82
- contentEl.getBoundingClientRect()
86
+ const contentWidth = contentEl.offsetWidth
87
+ const contentHeight = contentEl.offsetHeight
83
88
 
84
89
  const anchorBottom = anchorTop + anchorHeight
85
90
 
@@ -97,17 +102,20 @@ export function Menu({
97
102
 
98
103
  // If the content would overflow bottom, position it above the anchor.
99
104
  const isOverFlowingBottom = bottom > containerBottom
100
- const visibleTop = isOverFlowingBottom
101
- ? anchorTop - contentHeight - verticalOffset
102
- : top
105
+ const topWhenAboveAnchor = anchorTop - contentHeight - verticalOffset
106
+
107
+ // Only open the menu above the anchor if it won't get clipped, i.e., not < 0.
108
+ const visibleTop =
109
+ isOverFlowingBottom && topWhenAboveAnchor > 0 ? topWhenAboveAnchor : top
103
110
  setTop(visibleTop)
104
111
  }, [
105
112
  anchorEl,
106
113
  horizontalOffset,
107
114
  verticalOffset,
108
115
  contentEl,
109
- documentEl,
110
116
  edgeOffset,
117
+ bodyEl,
118
+ documentEl,
111
119
  ])
112
120
 
113
121
  useLayoutEffect(() => {
@@ -124,7 +132,7 @@ export function Menu({
124
132
  const hasSetPosition = top !== 0 && left !== 0
125
133
  const visible = isOpen && hasSetPosition
126
134
 
127
- if (documentEl == null) {
135
+ if (bodyEl == null) {
128
136
  return null
129
137
  }
130
138
 
@@ -136,28 +144,30 @@ export function Menu({
136
144
  >
137
145
  {renderButton({ onOpen: handleOpen })}
138
146
  {createPortal(
139
- <div
140
- className={classNames(
141
- 'seam-menu-bg',
142
- backgroundProps?.className,
143
- visible ? 'seam-menu-visible' : 'seam-menu-hidden'
144
- )}
145
- onClick={(event) => {
146
- event.stopPropagation()
147
- handleClose()
148
- }}
149
- >
147
+ <div className={seamComponentsClassName}>
150
148
  <div
151
- className='seam-menu-content'
152
- style={{
153
- top,
154
- left,
149
+ className={classNames(
150
+ 'seam-menu-bg',
151
+ backgroundProps?.className,
152
+ visible ? 'seam-menu-visible' : 'seam-menu-hidden'
153
+ )}
154
+ onClick={(event) => {
155
+ event.stopPropagation()
156
+ handleClose()
155
157
  }}
156
158
  >
157
- {children}
159
+ <div
160
+ className='seam-menu-content'
161
+ style={{
162
+ top,
163
+ left,
164
+ }}
165
+ >
166
+ {children}
167
+ </div>
158
168
  </div>
159
169
  </div>,
160
- documentEl
170
+ bodyEl
161
171
  )}
162
172
 
163
173
  {/*
@@ -0,0 +1,97 @@
1
+ import classNames from 'classnames'
2
+ import { useEffect, useState } from 'react'
3
+
4
+ import { CheckGreenIcon } from 'lib/icons/CheckGreen.js'
5
+ import { CloseWhiteIcon } from 'lib/icons/CloseWhite.js'
6
+ import { ExclamationCircleIcon } from 'lib/icons/ExclamationCircle.js'
7
+
8
+ type SnackbarVariant = 'success' | 'error'
9
+
10
+ interface SnackbarProps {
11
+ message: string
12
+ variant: SnackbarVariant
13
+ visible: boolean
14
+ action?: {
15
+ label: string
16
+ onClick: () => void
17
+ }
18
+ autoDismiss?: boolean
19
+ dismissAfterMs?: number
20
+ disableCloseButton?: boolean
21
+ }
22
+
23
+ export function Snackbar({
24
+ message,
25
+ variant,
26
+ visible,
27
+ action,
28
+ autoDismiss = false,
29
+ dismissAfterMs = 5000,
30
+ disableCloseButton = false,
31
+ }: SnackbarProps): JSX.Element {
32
+ const [hidden, setHidden] = useState(visible)
33
+
34
+ const { label: actionLabel, onClick: handleActionClick } = action ?? {}
35
+
36
+ useEffect(() => {
37
+ setHidden(!visible)
38
+ }, [visible])
39
+
40
+ useEffect(() => {
41
+ if (!autoDismiss) {
42
+ return () => {}
43
+ }
44
+
45
+ const timeout = globalThis.setTimeout(() => {
46
+ setHidden(false)
47
+ }, dismissAfterMs)
48
+
49
+ return () => {
50
+ globalThis.clearTimeout(timeout)
51
+ }
52
+ }, [autoDismiss, dismissAfterMs])
53
+
54
+ return (
55
+ <div className='seam-snackbar-wrap'>
56
+ <div
57
+ className={classNames('seam-snackbar', {
58
+ 'seam-snackbar-hide': hidden,
59
+ })}
60
+ >
61
+ <SnackbarIcon variant={variant} />
62
+ <div className='seam-snackbar-message-wrap'>
63
+ <p className='seam-snackbar-message'>{message}</p>
64
+ </div>
65
+ <div className='seam-snackbar-actions-wrap'>
66
+ {action != null && (
67
+ <button
68
+ className='seam-snackbar-action'
69
+ onClick={handleActionClick}
70
+ >
71
+ <span className='seam-snackbar-action-label'>{actionLabel}</span>
72
+ </button>
73
+ )}
74
+ {!disableCloseButton && (
75
+ <button
76
+ className='seam-snackbar-close-button'
77
+ onClick={() => {
78
+ setHidden(true)
79
+ }}
80
+ >
81
+ <CloseWhiteIcon />
82
+ </button>
83
+ )}
84
+ </div>
85
+ </div>
86
+ </div>
87
+ )
88
+ }
89
+
90
+ function SnackbarIcon(props: { variant: SnackbarVariant }): JSX.Element {
91
+ switch (props.variant) {
92
+ case 'success':
93
+ return <CheckGreenIcon />
94
+ case 'error':
95
+ return <ExclamationCircleIcon />
96
+ }
97
+ }
@@ -0,0 +1,69 @@
1
+ import { useEffect, useState } from 'react'
2
+
3
+ import {
4
+ formatTimeZone,
5
+ getSupportedTimeZones,
6
+ getSystemTimeZone,
7
+ } from 'lib/dates.js'
8
+ import { Checkbox } from 'lib/ui/Checkbox.js'
9
+ import { handleString } from 'lib/ui/TextField/TextField.js'
10
+
11
+ interface TimeZonePickerProps {
12
+ value: string
13
+ onChange: (timeZone: string) => void
14
+ onManualTimeZoneSelected?: (manualTimeZoneSelected: boolean) => void
15
+ }
16
+
17
+ export function TimeZonePicker({
18
+ onChange,
19
+ value,
20
+ onManualTimeZoneSelected,
21
+ }: TimeZonePickerProps): JSX.Element {
22
+ const [manualTimeZoneEnabled, setManualTimeZoneEnabled] = useState(false)
23
+
24
+ const isBrowserTimeZoneSelected = value === getSystemTimeZone()
25
+ const isManualTimeZoneSelected =
26
+ !isBrowserTimeZoneSelected || manualTimeZoneEnabled
27
+
28
+ useEffect(() => {
29
+ if (onManualTimeZoneSelected != null)
30
+ onManualTimeZoneSelected(isManualTimeZoneSelected)
31
+ }, [isManualTimeZoneSelected, onManualTimeZoneSelected])
32
+
33
+ const handleChangeManualTimeZone = (enabled: boolean): void => {
34
+ setManualTimeZoneEnabled(enabled)
35
+ if (!enabled) {
36
+ onChange(getSystemTimeZone())
37
+ }
38
+ }
39
+
40
+ return (
41
+ <div className='seam-time-zone-picker'>
42
+ <Checkbox
43
+ label={t.setTimeZoneManuallyLabel}
44
+ checked={!isManualTimeZoneSelected}
45
+ onChange={(manual) => {
46
+ handleChangeManualTimeZone(!manual)
47
+ }}
48
+ className='seam-manual-time-zone-checkbox'
49
+ />
50
+
51
+ <select
52
+ value={value}
53
+ onChange={handleString(onChange)}
54
+ className='seam-time-zone-select'
55
+ >
56
+ {getSupportedTimeZones().map((timeZone) => (
57
+ <option value={timeZone} key={timeZone}>
58
+ {formatTimeZone(timeZone)}
59
+ </option>
60
+ ))}
61
+ </select>
62
+ </div>
63
+ )
64
+ }
65
+
66
+ const t = {
67
+ utc: 'UTC',
68
+ setTimeZoneManuallyLabel: 'Use local time zone',
69
+ }
@@ -26,15 +26,14 @@ function Content(props: {
26
26
  }): JSX.Element | null {
27
27
  const { status, level } = props
28
28
 
29
- const percentage = level != null ? ` (${Math.floor(level * 100)}%)` : null
30
-
31
29
  if (status === 'full') {
32
30
  return (
33
31
  <>
34
32
  <BatteryLevelFullIcon />
35
- <span className='seam-status-text'>{`${t.full}${
36
- percentage ?? ''
37
- }`}</span>
33
+ <span className='seam-status-text'>
34
+ {t.full}
35
+ <Percentage level={level} />
36
+ </span>
38
37
  </>
39
38
  )
40
39
  }
@@ -43,9 +42,10 @@ function Content(props: {
43
42
  return (
44
43
  <>
45
44
  <BatteryLevelHighIcon />
46
- <span className='seam-status-text'>{`${t.high}${
47
- percentage ?? ''
48
- }`}</span>
45
+ <span className='seam-status-text'>
46
+ {t.high}
47
+ <Percentage level={level} />
48
+ </span>
49
49
  </>
50
50
  )
51
51
  }
@@ -54,9 +54,10 @@ function Content(props: {
54
54
  return (
55
55
  <>
56
56
  <BatteryLevelLowIcon />
57
- <span className='seam-status-text'>{`${t.low}${
58
- percentage ?? ''
59
- }`}</span>
57
+ <span className='seam-status-text'>
58
+ {t.low}
59
+ <Percentage level={level} />
60
+ </span>
60
61
  </>
61
62
  )
62
63
  }
@@ -65,9 +66,10 @@ function Content(props: {
65
66
  return (
66
67
  <>
67
68
  <BatteryLevelCriticalIcon />
68
- <span className='seam-text-danger'>{`${t.critical}${
69
- percentage ?? ''
70
- }`}</span>
69
+ <span className='seam-text-danger'>
70
+ {t.critical}
71
+ <Percentage level={level} />
72
+ </span>
71
73
  </>
72
74
  )
73
75
  }
@@ -75,6 +77,13 @@ function Content(props: {
75
77
  return null
76
78
  }
77
79
 
80
+ function Percentage(props: {
81
+ level: number | null | undefined
82
+ }): JSX.Element | null {
83
+ if (props.level == null) return null
84
+ return <> ({Math.floor(props.level * 100)}%)</>
85
+ }
86
+
78
87
  const t = {
79
88
  full: 'Good',
80
89
  high: 'Good',
@@ -24,7 +24,7 @@ export function ClimateModeMenu({
24
24
  renderButton={({ onOpen }) => (
25
25
  <button onClick={onOpen} className='seam-climate-mode-menu-button'>
26
26
  <div className='seam-climate-mode-menu-button-icon'>
27
- {ModeIcon(mode)}
27
+ <ModeIcon mode={mode} />
28
28
  </div>
29
29
  <ChevronDownIcon />
30
30
  </button>
@@ -39,7 +39,7 @@ export function ClimateModeMenu({
39
39
  <ThermoModeMenuOption
40
40
  key={m}
41
41
  label={t[m]}
42
- icon={ModeIcon(m)}
42
+ icon={<ModeIcon mode={m} />}
43
43
  isSelected={mode === m}
44
44
  onClick={() => {
45
45
  onChange(m)
@@ -50,8 +50,8 @@ export function ClimateModeMenu({
50
50
  )
51
51
  }
52
52
 
53
- function ModeIcon(mode: HvacModeSetting): JSX.Element {
54
- switch (mode) {
53
+ function ModeIcon(props: { mode: HvacModeSetting }): JSX.Element {
54
+ switch (props.mode) {
55
55
  case 'heat':
56
56
  return <ThermostatHeatIcon />
57
57
  case 'cool':
@@ -3,7 +3,7 @@ import { useCallback, useState } from 'react'
3
3
 
4
4
  import { useInterval } from 'lib/ui/use-interval.js'
5
5
 
6
- export function useCurrentTime(): DateTime {
6
+ export function useNow(): DateTime {
7
7
  const [date, setDate] = useState<DateTime>(DateTime.now())
8
8
 
9
9
  const update = useCallback(() => {
@@ -1,3 +1,3 @@
1
- const seamapiReactVersion = '1.61.0'
1
+ const seamapiReactVersion = '1.61.2'
2
2
 
3
3
  export default seamapiReactVersion
@@ -4,7 +4,7 @@
4
4
  .seam-access-code-form {
5
5
  @include main;
6
6
  @include schedule-picker;
7
- @include timezone-picker;
7
+ @include time-zone-picker;
8
8
  }
9
9
  }
10
10
 
@@ -96,7 +96,7 @@
96
96
  .seam-content {
97
97
  padding: 0 24px;
98
98
 
99
- .seam-timezone {
99
+ .seam-time-zone {
100
100
  display: flex;
101
101
  align-items: center;
102
102
  font-size: 14px;
@@ -127,8 +127,8 @@
127
127
  }
128
128
  }
129
129
 
130
- @mixin timezone-picker {
131
- .seam-access-code-timezone-picker {
130
+ @mixin time-zone-picker {
131
+ .seam-access-code-time-zone-picker {
132
132
  .seam-content {
133
133
  padding: 0 24px;
134
134
  }
@@ -25,7 +25,7 @@
25
25
 
26
26
  @mixin name-and-schedule {
27
27
  .seam-climate-setting-schedule-form-name-and-schedule {
28
- .seam-timezone {
28
+ .seam-time-zone {
29
29
  display: flex;
30
30
  align-items: center;
31
31
  font-size: 14px;
@@ -20,6 +20,7 @@ $bg-b: #e9edef;
20
20
  $bg-a: #f1f3f4;
21
21
  $bg-aa: #fafafa;
22
22
  $bg-gray: #ececec;
23
+ $feedback-bg: #30373a;
23
24
  $status-red: #e36857;
24
25
  $white: #fff;
25
26
  $black: #000;
@@ -38,3 +39,4 @@ $thermo-orange: #fc8e28;
38
39
  $thermo-orange-faded: #fff2e0;
39
40
  $thermo-blue: #6b95ff;
40
41
  $thermo-blue-faded: #e7f2ff;
42
+ $text-hyperlink: #6ac1ff;
@@ -1,21 +1,10 @@
1
1
  @use './colors';
2
-
3
- @keyframes scale-in {
4
- 0% {
5
- transform: scale(0.5);
6
- opacity: 0;
7
- visibility: hidden;
8
- }
9
-
10
- 100% {
11
- transform: scale(1);
12
- opacity: 1;
13
- visibility: visible;
14
- }
15
- }
2
+ @use './motion';
16
3
 
17
4
  @mixin all {
18
5
  .seam-loading-toast {
6
+ @include motion.scale-in;
7
+
19
8
  height: 32px;
20
9
  padding: 0 10px;
21
10
  display: flex;
@@ -29,8 +18,8 @@
29
18
  0 0 1px 0 rgb(24 29 37 / 25%);
30
19
  gap: 8px;
31
20
  will-change: transform;
32
- animation: scale-in 0.2s cubic-bezier(0.22, 1, 0.36, 1);
33
- transition: all 0.2s cubic-bezier(0.22, 1, 0.36, 1);
21
+ animation: scale-in 0.2s motion.$ease-out-quint;
22
+ transition: all 0.2s motion.$ease-out-quint;
34
23
  pointer-events: none;
35
24
  position: absolute;
36
25
  z-index: 9;
@@ -21,11 +21,12 @@
21
21
  @use './thermostat';
22
22
  @use './tooltip';
23
23
  @use './seam-table';
24
+ @use './snackbar';
24
25
  @use './spinner';
25
26
  @use './switch';
26
27
  @use './climate-setting-schedule-form';
27
28
  @use './climate-setting-schedule-details';
28
- @use './timezone-picker';
29
+ @use './time-zone-picker';
29
30
 
30
31
  .seam-components {
31
32
  // Reset
@@ -45,9 +46,10 @@
45
46
  @include checkbox.all;
46
47
  @include radio-field.all;
47
48
  @include tooltip.all;
49
+ @include snackbar.all;
48
50
  @include spinner.all;
49
51
  @include switch.all;
50
- @include timezone-picker.all;
52
+ @include time-zone-picker.all;
51
53
 
52
54
  // Components
53
55
  @include device-details.all;
@@ -0,0 +1,34 @@
1
+ $ease-out-quint: cubic-bezier(0.22, 1, 0.36, 1);
2
+ $ease-in-out-quint: cubic-bezier(0.83, 0, 0.17, 1);
3
+
4
+ @mixin scale-in {
5
+ @keyframes scale-in {
6
+ 0% {
7
+ transform: scale(0.5);
8
+ opacity: 0;
9
+ visibility: hidden;
10
+ }
11
+
12
+ 100% {
13
+ transform: scale(1);
14
+ opacity: 1;
15
+ visibility: visible;
16
+ }
17
+ }
18
+ }
19
+
20
+ @mixin fade-in-up {
21
+ @keyframes fade-in-up {
22
+ 0% {
23
+ transform: translateY(24px);
24
+ opacity: 0;
25
+ visibility: hidden;
26
+ }
27
+
28
+ 100% {
29
+ transform: translateY(0);
30
+ opacity: 1;
31
+ visibility: visible;
32
+ }
33
+ }
34
+ }
@@ -0,0 +1,107 @@
1
+ @use './colors';
2
+ @use './motion';
3
+
4
+ @mixin all {
5
+ .seam-snackbar-wrap {
6
+ width: 100%;
7
+ position: fixed;
8
+ bottom: 0;
9
+ display: flex;
10
+ justify-content: center;
11
+ align-items: center;
12
+ pointer-events: none;
13
+ height: 0;
14
+ z-index: 9;
15
+ }
16
+
17
+ .seam-snackbar {
18
+ @include motion.fade-in-up;
19
+
20
+ pointer-events: auto;
21
+ display: flex;
22
+ padding: 16px;
23
+ align-items: flex-start;
24
+ border-radius: 12px;
25
+ background: colors.$feedback-bg;
26
+ box-shadow: 0 2px 12px 0 rgb(0 0 0 / 25%);
27
+ gap: 10px;
28
+ position: absolute;
29
+ bottom: 24px;
30
+ will-change: transform;
31
+ animation: fade-in-up 0.2s motion.$ease-in-out-quint;
32
+ transition: all 0.2s motion.$ease-in-out-quint;
33
+
34
+ &.seam-snackbar-hide {
35
+ transform: translateY(24px);
36
+ opacity: 0;
37
+ visibility: hidden;
38
+ }
39
+
40
+ .seam-snackbar-message-wrap {
41
+ display: flex;
42
+ max-width: 300px;
43
+ }
44
+
45
+ .seam-snackbar-message {
46
+ color: colors.$white;
47
+ font-size: 16px;
48
+ font-weight: 400;
49
+ line-height: 134%;
50
+ margin: 0;
51
+ padding: 0;
52
+ }
53
+
54
+ .seam-snackbar-actions-wrap {
55
+ height: 24px;
56
+ display: flex;
57
+ align-items: center;
58
+ flex-direction: row;
59
+ gap: 16px;
60
+ }
61
+
62
+ .seam-snackbar-action {
63
+ appearance: none;
64
+ background-color: transparent;
65
+ margin: 0;
66
+ padding: 0;
67
+ padding-top: 0;
68
+ display: flex;
69
+ justify-content: center;
70
+ align-items: center;
71
+ box-shadow: none;
72
+ border: none;
73
+ cursor: pointer;
74
+ transition: opacity 0.2s ease-in-out;
75
+
76
+ &:hover {
77
+ opacity: 0.75;
78
+ }
79
+
80
+ .seam-snackbar-action-label {
81
+ color: colors.$text-hyperlink;
82
+ font-size: 16px;
83
+ font-weight: 600;
84
+ line-height: 0.8;
85
+ white-space: nowrap;
86
+ }
87
+ }
88
+
89
+ .seam-snackbar-close-button {
90
+ appearance: none;
91
+ background-color: transparent;
92
+ display: flex;
93
+ justify-content: center;
94
+ align-items: center;
95
+ border: 0;
96
+ box-shadow: none;
97
+ margin: 0;
98
+ padding: 0;
99
+ cursor: pointer;
100
+ transition: opacity 0.2s ease-in-out;
101
+
102
+ &:hover {
103
+ opacity: 0.75;
104
+ }
105
+ }
106
+ }
107
+ }