@seamapi/react 1.49.0 → 1.51.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.
Files changed (69) hide show
  1. package/README.md +1 -1
  2. package/dist/elements.js +9778 -8689
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +170 -0
  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/icons/Bee.d.ts +2 -0
  9. package/lib/icons/Bee.js +5 -0
  10. package/lib/icons/Bee.js.map +1 -0
  11. package/lib/icons/Fan.d.ts +2 -0
  12. package/lib/icons/Fan.js +7 -0
  13. package/lib/icons/Fan.js.map +1 -0
  14. package/lib/icons/Off.d.ts +2 -0
  15. package/lib/icons/Off.js +7 -0
  16. package/lib/icons/Off.js.map +1 -0
  17. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.d.ts +2 -1
  18. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js +4 -4
  19. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js.map +1 -1
  20. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js +9 -1
  21. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js.map +1 -1
  22. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.d.ts +2 -1
  23. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.js +6 -4
  24. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.js.map +1 -1
  25. package/lib/seam/components/DeviceDetails/DeviceDetails.js +5 -1
  26. package/lib/seam/components/DeviceDetails/DeviceDetails.js.map +1 -1
  27. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.d.ts +7 -0
  28. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +66 -0
  29. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -0
  30. package/lib/seam/connected-accounts/use-connected-account.d.ts +5 -0
  31. package/lib/seam/connected-accounts/use-connected-account.js +17 -0
  32. package/lib/seam/connected-accounts/use-connected-account.js.map +1 -0
  33. package/lib/seam/index.d.ts +2 -0
  34. package/lib/seam/index.js +2 -0
  35. package/lib/seam/index.js.map +1 -1
  36. package/lib/ui/layout/DetailRow.d.ts +3 -1
  37. package/lib/ui/layout/DetailRow.js +5 -2
  38. package/lib/ui/layout/DetailRow.js.map +1 -1
  39. package/lib/ui/thermostat/ClimateSettingStatus.d.ts +1 -1
  40. package/lib/ui/thermostat/ClimateSettingStatus.js +20 -24
  41. package/lib/ui/thermostat/ClimateSettingStatus.js.map +1 -1
  42. package/lib/ui/thermostat/Temperature.d.ts +8 -0
  43. package/lib/ui/thermostat/Temperature.js +11 -0
  44. package/lib/ui/thermostat/Temperature.js.map +1 -0
  45. package/lib/ui/thermostat/ThermostatCard.d.ts +7 -0
  46. package/lib/ui/thermostat/ThermostatCard.js +46 -0
  47. package/lib/ui/thermostat/ThermostatCard.js.map +1 -0
  48. package/lib/version.d.ts +1 -1
  49. package/lib/version.js +1 -1
  50. package/package.json +3 -3
  51. package/src/lib/icons/Bee.tsx +21 -0
  52. package/src/lib/icons/Fan.tsx +36 -0
  53. package/src/lib/icons/Off.tsx +36 -0
  54. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.tsx +9 -2
  55. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.tsx +18 -0
  56. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.tsx +14 -4
  57. package/src/lib/seam/components/DeviceDetails/DeviceDetails.tsx +6 -1
  58. package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +175 -0
  59. package/src/lib/seam/connected-accounts/use-connected-account.ts +35 -0
  60. package/src/lib/seam/index.ts +2 -0
  61. package/src/lib/ui/layout/DetailRow.tsx +15 -2
  62. package/src/lib/ui/thermostat/ClimateSettingStatus.tsx +34 -30
  63. package/src/lib/ui/thermostat/Temperature.tsx +26 -0
  64. package/src/lib/ui/thermostat/ThermostatCard.tsx +160 -0
  65. package/src/lib/version.ts +1 -1
  66. package/src/styles/_device-details.scss +4 -0
  67. package/src/styles/_layout.scss +36 -0
  68. package/src/styles/_thermostat.scss +160 -0
  69. package/src/styles/_tooltip.scss +1 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seamapi/react",
3
- "version": "1.49.0",
3
+ "version": "1.51.0",
4
4
  "description": "Seam Components.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -122,7 +122,7 @@
122
122
  "@tanstack/react-query": "^4.28.0",
123
123
  "classnames": "^2.3.2",
124
124
  "luxon": "^3.3.0",
125
- "seamapi": "^8.9.0",
125
+ "seamapi": "^8.9.2",
126
126
  "uuid": "^9.0.0"
127
127
  },
128
128
  "devDependencies": {
@@ -133,7 +133,7 @@
133
133
  "@mui/icons-material": "^5.11.16",
134
134
  "@mui/material": "^5.12.2",
135
135
  "@rxfork/r2wc-react-to-web-component": "^2.3.0",
136
- "@seamapi/fake-seam-connect": "^1.9.0",
136
+ "@seamapi/fake-seam-connect": "^1.9.1",
137
137
  "@storybook/addon-designs": "^7.0.1",
138
138
  "@storybook/addon-essentials": "^7.0.2",
139
139
  "@storybook/addon-links": "^7.0.2",
@@ -0,0 +1,21 @@
1
+ /*
2
+ * Automaically generated by SVGR from assets/icons/*.svg.
3
+ * Do not edit this file or add other components to this directory.
4
+ */
5
+ import type { SVGProps } from 'react'
6
+ export function BeeIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
7
+ return (
8
+ <svg
9
+ xmlns='http://www.w3.org/2000/svg'
10
+ width={33}
11
+ height={33}
12
+ fill='none'
13
+ {...props}
14
+ >
15
+ <path
16
+ fill='#5DBE77'
17
+ d='M23.902 25.469c-.18.263-3.2-.2-4.15-5.212-.95-5.01 1.214-5.411 1.214-5.411s2.745-.79 4.086 4.938c1.225 5.21-1.15 5.685-1.15 5.685Zm-8.437-6.917h-.01c-1.468-.98-1.996-1.853-1.996-1.853 1.257-.18 2.724-.958 2.724-.958l.813.568v.01l.834.548s-.18 1.643.106 2.874c0 0-1.014-.168-2.46-1.168m-9.63-5.327s-.422-2.4 4.931-3.18c5.881-.852 6.156 2 6.156 2s.412 2.18-4.646 3.138c-5.068.958-6.62-1.695-6.44-1.958Zm14.149-6.117c.612.421.77 1.242.348 1.853a1.33 1.33 0 0 1-1.858.347 1.322 1.322 0 0 1-.348-1.852 1.33 1.33 0 0 1 1.858-.348Zm4.445 3.022c.612.41.77 1.242.36 1.853-.413.61-1.247.758-1.86.347a1.322 1.322 0 0 1-.348-1.853 1.33 1.33 0 0 1 1.859-.347m2.523 10.58c-.37-3.053-2.006-5.496-2.64-6.19 0 0-.063-.021-.01-.148.053-.126.158-.495.158-.495s0-.042.074-.063c.074-.02.834-.179 1.426-1.042.59-.863.971-2.653-1.046-4.022 0 0-.01 0-.031-.094-.01-.085-.338-1.548-1.225-2.159-.887-.6-2.376-.368-2.46-.347-.085.021-.095 0-.095 0-2.017-1.369-3.548-.358-4.14.505-.59.864-.453 1.632-.453 1.706 0 .074-.032.084-.032.084l-.401.327c-.095.084-.138.042-.138.042-.886-.337-3.769-.969-6.747-.19-2.977.78-4.16 1.874-4.814 2.832-.655.958-1.024 2.874 1.024 4.264 2.048 1.39 4.952 1.263 4.952 1.263.084 0 .095.063.095.063.707 2.506 3.051 3.97 3.21 4.075.158.116 2.386 1.747 4.973 1.484 0 0 .063-.01.095.074 0 0 .95 2.737 2.988 4.127 2.048 1.39 3.696.347 4.35-.61.655-.959 1.246-2.454.887-5.496m-9.914 3.074s-1.964.084-4.213-1.443c-2.25-1.526-2.893-3.38-2.893-3.38-.053-.146-.106-.052-.106-.052-.602.885-.781 1.39-.443 2.8.327 1.359 1.974 2.517 2.09 2.601.116.074 1.795 1.18 3.189.99 1.436-.2 1.848-.558 2.45-1.443 0 0 .063-.084-.085-.073m-4.424 2.39s-.95 0-1.943-.664l-.507.737.507-.747c-.992-.674-1.34-1.558-1.34-1.558-.16-.243-.201 0-.201 0-.55 2.116.39 3.032.475 3.105.095.042 1.298.58 3.072-.705 0 0 .211-.126-.074-.179'
18
+ />
19
+ </svg>
20
+ )
21
+ }
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Automaically generated by SVGR from assets/icons/*.svg.
3
+ * Do not edit this file or add other components to this directory.
4
+ */
5
+ import type { SVGProps } from 'react'
6
+ export function FanIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
7
+ return (
8
+ <svg
9
+ xmlns='http://www.w3.org/2000/svg'
10
+ width={18}
11
+ height={18}
12
+ fill='none'
13
+ {...props}
14
+ >
15
+ <mask
16
+ id='fan_svg__a'
17
+ width={18}
18
+ height={18}
19
+ x={0}
20
+ y={0}
21
+ maskUnits='userSpaceOnUse'
22
+ style={{
23
+ maskType: 'alpha',
24
+ }}
25
+ >
26
+ <path fill='#D9D9D9' d='M0 0h18v18H0z' />
27
+ </mask>
28
+ <g mask='url(#fan_svg__a)'>
29
+ <path
30
+ fill='#9DA1A9'
31
+ d='M9.921 16.456c-.61.164-1.124.106-1.539-.175a1.916 1.916 0 0 1-.81-1.12 2.188 2.188 0 0 1-.037-.963c.059-.33.207-.63.443-.899.218-.238.373-.51.464-.818.091-.306.127-.618.106-.933l-.244-.05a1.154 1.154 0 0 1-.231-.073l-1.495 1.036c-.184.126-.365.24-.544.338-.18.1-.37.177-.574.231-.755.202-1.51.052-2.268-.451-.757-.503-1.306-1.39-1.646-2.66-.164-.61-.106-1.123.174-1.539.28-.415.648-.684 1.104-.806.311-.083.635-.096.971-.038.337.057.64.204.908.44.238.219.51.373.817.465.307.09.619.126.934.106l.05-.245c.017-.081.041-.158.073-.231L5.54 6.577a6.808 6.808 0 0 1-.339-.545 2.446 2.446 0 0 1-.225-.556c-.206-.766-.057-1.528.446-2.286.503-.757 1.39-1.306 2.66-1.646.61-.164 1.123-.105 1.539.175.415.28.683.648.805 1.103.084.311.097.635.04.972a1.818 1.818 0 0 1-.441.907c-.219.238-.374.511-.465.818a2.655 2.655 0 0 0-.106.933l.245.05c.081.017.158.041.231.073l1.49-1.054c.184-.126.363-.235.538-.327.174-.091.363-.165.566-.22.97-.26 1.803-.052 2.497.622s1.173 1.502 1.436 2.485c.164.61.096 1.126-.202 1.546a2.021 2.021 0 0 1-2.075.845 1.865 1.865 0 0 1-.88-.448 2.064 2.064 0 0 0-.818-.464 2.655 2.655 0 0 0-.933-.106l-.05.244a1.154 1.154 0 0 1-.073.231l1.036 1.494c.123.173.232.34.326.5.095.161.167.337.219.529.22.775.082 1.55-.417 2.326-.498.775-1.388 1.335-2.67 1.678Zm-.63-6.378c.3-.08.526-.253.679-.519.153-.266.19-.548.11-.848-.08-.3-.253-.526-.52-.68a1.076 1.076 0 0 0-.847-.109c-.3.08-.526.253-.68.52-.153.265-.19.547-.11.847.081.3.254.526.52.68.266.153.548.19.848.11Z'
32
+ />
33
+ </g>
34
+ </svg>
35
+ )
36
+ }
@@ -0,0 +1,36 @@
1
+ /*
2
+ * Automaically generated by SVGR from assets/icons/*.svg.
3
+ * Do not edit this file or add other components to this directory.
4
+ */
5
+ import type { SVGProps } from 'react'
6
+ export function OffIcon(props: SVGProps<SVGSVGElement>): JSX.Element {
7
+ return (
8
+ <svg
9
+ xmlns='http://www.w3.org/2000/svg'
10
+ width={16}
11
+ height={16}
12
+ fill='none'
13
+ {...props}
14
+ >
15
+ <mask
16
+ id='off_svg__a'
17
+ width={16}
18
+ height={16}
19
+ x={0}
20
+ y={0}
21
+ maskUnits='userSpaceOnUse'
22
+ style={{
23
+ maskType: 'alpha',
24
+ }}
25
+ >
26
+ <path fill='#D9D9D9' d='M.421.5h15v15h-15z' />
27
+ </mask>
28
+ <g mask='url(#off_svg__a)'>
29
+ <path
30
+ fill='#9DA1A9'
31
+ d='M12.78 14.64 11.36 13.22a6.404 6.404 0 0 1-1.633.765 6.127 6.127 0 0 1-1.805.266 6.086 6.086 0 0 1-2.437-.492 6.311 6.311 0 0 1-1.985-1.336 6.312 6.312 0 0 1-1.336-1.985A6.086 6.086 0 0 1 1.671 8c0-.625.089-1.227.266-1.805a6.4 6.4 0 0 1 .766-1.633L1.28 3.141l.89-.891 11.5 11.5-.89.89ZM7.922 13a4.955 4.955 0 0 0 2.532-.688L3.609 5.47A4.956 4.956 0 0 0 2.92 8c0 1.385.487 2.565 1.461 3.54.974.973 2.154 1.46 3.54 1.46Zm5.219-1.563-.906-.906A4.955 4.955 0 0 0 12.922 8c0-1.385-.488-2.565-1.462-3.54C10.486 3.488 9.307 3 7.921 3a4.955 4.955 0 0 0-2.531.688l-.906-.907a6.4 6.4 0 0 1 1.633-.765A6.127 6.127 0 0 1 7.92 1.75c.865 0 1.677.164 2.438.492.76.328 1.422.774 1.984 1.336a6.311 6.311 0 0 1 1.336 1.985c.328.76.492 1.572.492 2.437 0 .625-.088 1.227-.265 1.805a6.399 6.399 0 0 1-.766 1.633Z'
32
+ />
33
+ </g>
34
+ </svg>
35
+ )
36
+ }
@@ -12,22 +12,28 @@ import { ClimateSettingScheduleDeviceBar } from './ClimateSettingScheduleDeviceB
12
12
 
13
13
  interface ClimateSettingScheduleCardProps {
14
14
  climateSettingScheduleId: string
15
+ onSelectDevice: (deviceId: string) => void
15
16
  }
16
17
 
17
18
  export function ClimateSettingScheduleCard({
18
19
  climateSettingScheduleId,
20
+ onSelectDevice,
19
21
  }: ClimateSettingScheduleCardProps): JSX.Element {
20
22
  return (
21
23
  <div className='seam-climate-setting-schedule-card'>
22
- <Content climateSettingScheduleId={climateSettingScheduleId} />
24
+ <Content
25
+ climateSettingScheduleId={climateSettingScheduleId}
26
+ onSelectDevice={onSelectDevice}
27
+ />
23
28
  </div>
24
29
  )
25
30
  }
26
31
 
27
32
  function Content(props: {
28
33
  climateSettingScheduleId: string
34
+ onSelectDevice: (deviceId: string) => void
29
35
  }): JSX.Element | null {
30
- const { climateSettingScheduleId } = props
36
+ const { climateSettingScheduleId, onSelectDevice } = props
31
37
 
32
38
  const { climateSettingSchedule } = useClimateSettingSchedule(
33
39
  climateSettingScheduleId
@@ -57,6 +63,7 @@ function Content(props: {
57
63
  </div>
58
64
  </div>
59
65
  <ClimateSettingScheduleDeviceBar
66
+ onSelectDevice={onSelectDevice}
60
67
  deviceId={climateSettingSchedule.device_id}
61
68
  />
62
69
  </div>
@@ -1,4 +1,7 @@
1
1
  import classNames from 'classnames'
2
+ import { useState } from 'react'
3
+
4
+ import { DeviceDetails } from '../index.js'
2
5
 
3
6
  import { formatDateAndTime } from 'lib/dates.js'
4
7
  import { ArrowRightIcon } from 'lib/icons/ArrowRight.js'
@@ -26,6 +29,8 @@ export function ClimateSettingScheduleDetails({
26
29
  climateSettingScheduleId
27
30
  )
28
31
 
32
+ const [selectedDeviceId, selectDevice] = useState<string | null>(null)
33
+
29
34
  if (climateSettingSchedule == null) {
30
35
  return null
31
36
  }
@@ -33,6 +38,18 @@ export function ClimateSettingScheduleDetails({
33
38
  const isManualOverrideAllowed =
34
39
  climateSettingSchedule.manual_override_allowed ?? false
35
40
 
41
+ if (selectedDeviceId != null) {
42
+ return (
43
+ <DeviceDetails
44
+ className={className}
45
+ deviceId={selectedDeviceId}
46
+ onBack={() => {
47
+ selectDevice(null)
48
+ }}
49
+ />
50
+ )
51
+ }
52
+
36
53
  return (
37
54
  <div
38
55
  className={classNames('seam-climate-setting-schedule-details', className)}
@@ -41,6 +58,7 @@ export function ClimateSettingScheduleDetails({
41
58
  <div className='seam-climate-setting-schedule-details-content'>
42
59
  <ClimateSettingScheduleCard
43
60
  climateSettingScheduleId={climateSettingScheduleId}
61
+ onSelectDevice={selectDevice}
44
62
  />
45
63
  <div className='seam-default-setting-message-container'>
46
64
  <span className='seam-default-setting-message'>
@@ -7,8 +7,10 @@ import { ClimateSettingStatus } from 'lib/ui/thermostat/ClimateSettingStatus.js'
7
7
 
8
8
  export function ClimateSettingScheduleDeviceBar({
9
9
  deviceId,
10
+ onSelectDevice,
10
11
  }: {
11
12
  deviceId: string
13
+ onSelectDevice: (deviceId: string) => void
12
14
  }): JSX.Element | null {
13
15
  const { isLoading, device } = useDevice({
14
16
  device_id: deviceId,
@@ -26,14 +28,22 @@ export function ClimateSettingScheduleDeviceBar({
26
28
  return null
27
29
  }
28
30
 
29
- return <Content device={device} />
31
+ return <Content device={device} onSelectDevice={onSelectDevice} />
30
32
  }
31
33
 
32
- function Content(props: { device: ThermostatDevice }): JSX.Element {
33
- const { device } = props
34
+ function Content(props: {
35
+ device: ThermostatDevice
36
+ onSelectDevice: (deviceId: string) => void
37
+ }): JSX.Element {
38
+ const { device, onSelectDevice } = props
34
39
 
35
40
  return (
36
- <div className='seam-climate-setting-schedule-device-bar'>
41
+ <div
42
+ className='seam-climate-setting-schedule-device-bar'
43
+ onClick={() => {
44
+ onSelectDevice(device.device_id)
45
+ }}
46
+ >
37
47
  <div className='seam-climate-setting-schedule-device-content'>
38
48
  <div className='seam-device-image'>
39
49
  <DeviceImage device={device} />
@@ -1,6 +1,7 @@
1
- import { isLockDevice } from 'seamapi'
1
+ import { isLockDevice, isThermostatDevice } from 'seamapi'
2
2
 
3
3
  import { LockDeviceDetails } from 'lib/seam/components/DeviceDetails/LockDeviceDetails.js'
4
+ import { ThermostatDeviceDetails } from 'lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js'
4
5
  import { useDevice } from 'lib/seam/devices/use-device.js'
5
6
 
6
7
  export interface DeviceDetailsProps {
@@ -35,5 +36,9 @@ export function DeviceDetails({
35
36
  )
36
37
  }
37
38
 
39
+ if (isThermostatDevice(device)) {
40
+ return <ThermostatDeviceDetails device={device} />
41
+ }
42
+
38
43
  return null
39
44
  }
@@ -0,0 +1,175 @@
1
+ import classNames from 'classnames'
2
+ import { useState } from 'react'
3
+ import type { ThermostatDevice } from 'seamapi'
4
+
5
+ import { BeeIcon } from 'lib/icons/Bee.js'
6
+ import { ChevronWideIcon } from 'lib/icons/ChevronWide.js'
7
+ import { ClimateSettingScheduleTable } from 'lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js'
8
+ import { useConnectedAccount } from 'lib/seam/connected-accounts/use-connected-account.js'
9
+ import { useClimateSettingSchedules } from 'lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedules.js'
10
+ import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
11
+ import { DetailRow } from 'lib/ui/layout/DetailRow.js'
12
+ import { DetailSection } from 'lib/ui/layout/DetailSection.js'
13
+ import { DetailSectionGroup } from 'lib/ui/layout/DetailSectionGroup.js'
14
+ import { ClimateSettingStatus } from 'lib/ui/thermostat/ClimateSettingStatus.js'
15
+ import { ThermostatCard } from 'lib/ui/thermostat/ThermostatCard.js'
16
+
17
+ export function ThermostatDeviceDetails(props: {
18
+ device: ThermostatDevice
19
+ onBack?: () => void
20
+ className?: string
21
+ }): JSX.Element | null {
22
+ const { device, onBack, className } = props
23
+
24
+ const [climateSettingsOpen, setClimateSettingsOpen] = useState(false)
25
+
26
+ const { connectedAccount } = useConnectedAccount(device.connected_account_id)
27
+
28
+ const {
29
+ climateSettingSchedules,
30
+ isLoading: isLoadingClimateSchedules,
31
+ isError: isErrorClimateSchedules,
32
+ } = useClimateSettingSchedules({
33
+ device_id: device.device_id,
34
+ })
35
+
36
+ const isClimateSchedulesPlural = climateSettingSchedules?.length !== 1
37
+
38
+ if (climateSettingsOpen) {
39
+ return (
40
+ <ClimateSettingScheduleTable
41
+ deviceId={device.device_id}
42
+ onBack={() => {
43
+ setClimateSettingsOpen(false)
44
+ }}
45
+ />
46
+ )
47
+ }
48
+
49
+ if (device == null) {
50
+ return null
51
+ }
52
+
53
+ return (
54
+ <div className={classNames('seam-device-details', className)}>
55
+ <ContentHeader title={t.thermostat} onBack={onBack} />
56
+
57
+ <div className='seam-body'>
58
+ <ThermostatCard device={device} />
59
+
60
+ <div className='seam-thermostat-device-details'>
61
+ <DetailSectionGroup>
62
+ <DetailSection
63
+ label={t.scheduledClimates}
64
+ tooltipContent={t.scheduledClimatesTooltip}
65
+ >
66
+ <DetailRow
67
+ label={
68
+ isLoadingClimateSchedules || isErrorClimateSchedules
69
+ ? t.viewingClimateSchedules
70
+ : `${climateSettingSchedules?.length} ${
71
+ isClimateSchedulesPlural
72
+ ? t.climateSchedules
73
+ : t.climateSchedule
74
+ }`
75
+ }
76
+ onClick={() => {
77
+ setClimateSettingsOpen(true)
78
+ }}
79
+ >
80
+ <div className='seam-detail-row-rotated-icon'>
81
+ <ChevronWideIcon />
82
+ </div>
83
+ </DetailRow>
84
+ </DetailSection>
85
+
86
+ <DetailSection
87
+ label={t.currentSettings}
88
+ tooltipContent={t.currentSettingsTooltip}
89
+ >
90
+ <DetailRow label={t.climate}>
91
+ <ClimateSettingStatus
92
+ climateSetting={device.properties.current_climate_setting}
93
+ temperatureUnit='fahrenheit'
94
+ />
95
+ </DetailRow>
96
+ </DetailSection>
97
+
98
+ <DetailSection
99
+ label={t.defaultSettings}
100
+ tooltipContent={t.defaultSettingsTooltip}
101
+ >
102
+ <DetailRow label={t.defaultClimate}>
103
+ {/* @ts-expect-error not currently included in sdk */}
104
+ {device.properties.default_climate_setting != null ? (
105
+ <ClimateSettingStatus
106
+ climateSetting={device.properties.current_climate_setting}
107
+ temperatureUnit='fahrenheit'
108
+ />
109
+ ) : (
110
+ <p>{t.none}</p>
111
+ )}
112
+ </DetailRow>
113
+ <DetailRow label={t.allowManualOverride}>
114
+ <p>
115
+ {device.properties.current_climate_setting
116
+ .manual_override_allowed
117
+ ? t.yes
118
+ : t.no}
119
+ </p>
120
+ </DetailRow>
121
+ </DetailSection>
122
+
123
+ <DetailSection
124
+ label={t.deviceDetails}
125
+ tooltipContent={t.deviceDetailsTooltip}
126
+ >
127
+ <DetailRow label={t.brand}>
128
+ <div className='seam-detail-row-hstack'>
129
+ {device.properties.model.manufacturer_display_name}
130
+ {device.properties.manufacturer === 'ecobee' && <BeeIcon />}
131
+ </div>
132
+ </DetailRow>
133
+ <DetailRow
134
+ label={t.linkedAccount}
135
+ sublabel={
136
+ connectedAccount?.user_identifier?.email ??
137
+ device.connected_account_id
138
+ }
139
+ />
140
+ <DetailRow label={t.deviceId} sublabel={device.device_id} />
141
+ </DetailSection>
142
+ </DetailSectionGroup>
143
+ </div>
144
+ </div>
145
+ </div>
146
+ )
147
+ }
148
+
149
+ const t = {
150
+ thermostat: 'Thermostat',
151
+ climateSchedule: 'scheduled climate',
152
+ climateSchedules: 'scheduled climates',
153
+ viewingClimateSchedules: 'View scheduled climates',
154
+ scheduledClimates: 'Scheduled climates',
155
+ scheduledClimatesTooltip:
156
+ "Scheduled climates let you automatically change the thermostat's climate at a set time.",
157
+ currentSettings: 'Current settings',
158
+ currentSettingsTooltip:
159
+ 'These are the settings currently on the device. If you change them here, they change on the device.',
160
+ climate: 'Climate',
161
+ defaultSettings: 'Default settings',
162
+ defaultSettingsTooltip:
163
+ 'When a scheduled climate reaches its end time, the default settings will kick in.',
164
+ defaultClimate: 'Default climate',
165
+ allowManualOverride: 'Allow manual override',
166
+ deviceDetails: 'Device details',
167
+ deviceDetailsTooltip:
168
+ 'When a scheduled climate reaches its end time, the default settings will kick in.',
169
+ brand: 'Brand',
170
+ linkedAccount: 'Linked account',
171
+ deviceId: 'Device ID',
172
+ none: 'None',
173
+ yes: 'Yes',
174
+ no: 'No',
175
+ }
@@ -0,0 +1,35 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import type {
3
+ ConnectedAccount,
4
+ ConnectedAccountsGetRequest,
5
+ ConnectedAccountsGetResponse,
6
+ SeamError,
7
+ } from 'seamapi'
8
+
9
+ import { useSeamClient } from 'lib/seam/use-seam-client.js'
10
+ import type { UseSeamQueryResult } from 'lib/seam/use-seam-query-result.js'
11
+
12
+ export type UseConnectedAccountParams = ConnectedAccountsGetRequest | string
13
+ export type UseConnectedAccountData = ConnectedAccount | null
14
+
15
+ export function useConnectedAccount(
16
+ params: UseConnectedAccountParams
17
+ ): UseSeamQueryResult<'connectedAccount', UseConnectedAccountData> {
18
+ const normalizedParams =
19
+ typeof params === 'string' ? { connected_account_id: params } : params
20
+
21
+ const { client } = useSeamClient()
22
+ const { data, ...rest } = useQuery<
23
+ ConnectedAccountsGetResponse['connected_account'] | null,
24
+ SeamError
25
+ >({
26
+ enabled: client != null,
27
+ queryKey: ['connected_accounts', 'get', normalizedParams],
28
+ queryFn: async () => {
29
+ if (client == null) return null
30
+ return await client.connectedAccounts.get(normalizedParams)
31
+ },
32
+ })
33
+
34
+ return { ...rest, connectedAccount: data }
35
+ }
@@ -1,7 +1,9 @@
1
1
  export * from './access-codes/use-access-code.js'
2
2
  export * from './access-codes/use-access-codes.js'
3
3
  export * from './access-codes/use-create-access-code.js'
4
+ export * from './access-codes/use-generate-access-code-code.js'
4
5
  export * from './connect-webviews/use-create-connect-webview.js'
6
+ export * from './connected-accounts/use-connected-account.js'
5
7
  export * from './devices/use-device.js'
6
8
  export * from './devices/use-device-providers.js'
7
9
  export * from './devices/use-devices.js'
@@ -1,16 +1,29 @@
1
+ import classNames from 'classnames'
1
2
  import type { PropsWithChildren } from 'react'
2
3
 
3
4
  interface DetailRowProps {
4
5
  label: string
6
+ sublabel?: string
7
+ onClick?: () => void
5
8
  }
6
9
 
7
10
  export function DetailRow({
8
11
  label,
12
+ sublabel,
13
+ onClick,
9
14
  children,
10
15
  }: PropsWithChildren<DetailRowProps>): JSX.Element {
11
16
  return (
12
- <div className='seam-detail-row'>
13
- <p className='seam-row-label'>{label}</p>
17
+ <div
18
+ className={classNames('seam-detail-row', {
19
+ 'seam-detail-row-clickable': onClick != null,
20
+ })}
21
+ onClick={onClick}
22
+ >
23
+ <div className='seam-detail-row-label-wrap'>
24
+ <p className='seam-row-label'>{label}</p>
25
+ {sublabel != null && <p className='seam-row-sublabel'>{sublabel}</p>}
26
+ </div>
14
27
  <div className='seam-detail-row-right-area-content'>{children}</div>
15
28
  </div>
16
29
  )
@@ -4,9 +4,10 @@ import { ThermostatCoolIcon } from 'lib/icons/ThermostatCool.js'
4
4
  import { ThermostatHeatIcon } from 'lib/icons/ThermostatHeat.js'
5
5
  import { ThermostatHeatCoolIcon } from 'lib/icons/ThermostatHeatCool.js'
6
6
  import { ThermostatOffIcon } from 'lib/icons/ThermostatOff.js'
7
+ import { Temperature } from 'lib/ui/thermostat/Temperature.js'
7
8
 
8
9
  interface ClimateSettingStatusProps {
9
- climateSetting: Partial<ClimateSetting>
10
+ climateSetting: ClimateSetting
10
11
  temperatureUnit?: 'fahrenheit' | 'celsius'
11
12
  iconPlacement?: 'left' | 'right'
12
13
  }
@@ -23,16 +24,14 @@ export function ClimateSettingStatus({
23
24
  )}
24
25
  <Content
25
26
  mode={climateSetting.hvac_mode_setting}
26
- coolingSetPoint={formatTemperatureValue(
27
- temperatureUnit === 'fahrenheit'
28
- ? climateSetting.cooling_set_point_fahrenheit
29
- : climateSetting.cooling_set_point_celsius
30
- )}
31
- heatingSetPoint={formatTemperatureValue(
32
- temperatureUnit === 'fahrenheit'
33
- ? climateSetting.heating_set_point_fahrenheit
34
- : climateSetting.heating_set_point_celsius
35
- )}
27
+ coolingSetPoint={{
28
+ fahrenheit: climateSetting.cooling_set_point_fahrenheit,
29
+ celsius: climateSetting.cooling_set_point_celsius,
30
+ }}
31
+ heatingSetPoint={{
32
+ fahrenheit: climateSetting.heating_set_point_fahrenheit,
33
+ celsius: climateSetting.heating_set_point_celsius,
34
+ }}
36
35
  temperatureUnit={temperatureUnit}
37
36
  />
38
37
  {iconPlacement === 'right' && (
@@ -51,33 +50,42 @@ function ClimateSettingIcon(props: {
51
50
  <div className='seam-climate-setting-status-icon'>
52
51
  {mode === 'cool' && <ThermostatCoolIcon />}
53
52
  {mode === 'heat' && <ThermostatHeatIcon />}
54
- {mode === 'heatcool' && <ThermostatHeatCoolIcon />}
53
+ {mode === 'heat_cool' && <ThermostatHeatCoolIcon />}
55
54
  {mode === 'off' && <ThermostatOffIcon />}
56
55
  </div>
57
56
  )
58
57
  }
59
58
 
59
+ interface SetPoint {
60
+ fahrenheit: number
61
+ celsius: number
62
+ }
63
+
60
64
  function Content(props: {
61
65
  mode: ClimateSetting['hvac_mode_setting']
62
- coolingSetPoint: string
63
- heatingSetPoint: string
66
+ coolingSetPoint: Partial<SetPoint>
67
+ heatingSetPoint: Partial<SetPoint>
64
68
  temperatureUnit: 'fahrenheit' | 'celsius'
65
69
  }): JSX.Element | null {
66
70
  const { mode, coolingSetPoint, heatingSetPoint, temperatureUnit } = props
67
- const hasCoolingSetPoint = coolingSetPoint !== undefined
68
- const hasHeatingSetPoint = heatingSetPoint !== undefined
69
- const unit =
70
- temperatureUnit === 'fahrenheit' ? t.degreeFahrenheit : t.degreeCelsius
71
71
 
72
- if (mode === 'cool' && hasCoolingSetPoint)
73
- return <span>{`${coolingSetPoint}${unit}`}</span>
72
+ if (mode === 'cool' && isSetPoint(coolingSetPoint))
73
+ return <Temperature {...coolingSetPoint} unit={temperatureUnit} />
74
74
 
75
- if (mode === 'heat' && hasHeatingSetPoint)
76
- return <span>{`${heatingSetPoint}${unit}`}</span>
75
+ if (mode === 'heat' && isSetPoint(heatingSetPoint))
76
+ return <Temperature {...heatingSetPoint} unit={temperatureUnit} />
77
77
 
78
- if (mode === 'heatcool' && hasHeatingSetPoint && hasCoolingSetPoint)
78
+ if (
79
+ mode === 'heat_cool' &&
80
+ isSetPoint(heatingSetPoint) &&
81
+ isSetPoint(coolingSetPoint)
82
+ )
79
83
  return (
80
- <span>{`${heatingSetPoint}${unit} - ${coolingSetPoint}${unit}`}</span>
84
+ <span>
85
+ <Temperature {...heatingSetPoint} unit={temperatureUnit} />
86
+ {' - '}
87
+ <Temperature {...coolingSetPoint} unit={temperatureUnit} />
88
+ </span>
81
89
  )
82
90
 
83
91
  if (mode === 'off') return <span>{t.off}</span>
@@ -85,14 +93,10 @@ function Content(props: {
85
93
  return null
86
94
  }
87
95
 
88
- const formatTemperatureValue = (value: number | undefined): string => {
89
- if (value == null) return '0'
90
- if (Number.isInteger(value)) return value.toFixed()
91
- return value.toFixed(1)
96
+ function isSetPoint(setPoint: Partial<SetPoint>): setPoint is SetPoint {
97
+ return setPoint.fahrenheit != null && setPoint.celsius != null
92
98
  }
93
99
 
94
100
  const t = {
95
- degreeFahrenheit: '°F',
96
- degreeCelsius: '°C',
97
101
  off: 'Off',
98
102
  }
@@ -0,0 +1,26 @@
1
+ interface TemperatureProps {
2
+ fahrenheit: number
3
+ celsius: number
4
+ unit?: 'fahrenheit' | 'celsius'
5
+ }
6
+
7
+ export function Temperature({
8
+ fahrenheit,
9
+ celsius,
10
+ unit = 'fahrenheit',
11
+ }: TemperatureProps): JSX.Element {
12
+ const temperature = unit === 'fahrenheit' ? fahrenheit : celsius
13
+ const degree = unit === 'fahrenheit' ? t.degreeFahrenheit : t.degreeCelsius
14
+
15
+ return (
16
+ <span>
17
+ {Math.trunc(temperature)}
18
+ {degree}
19
+ </span>
20
+ )
21
+ }
22
+
23
+ const t = {
24
+ degreeFahrenheit: '°F',
25
+ degreeCelsius: '°C',
26
+ }