@seamapi/react 1.48.1 → 1.50.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 (90) hide show
  1. package/README.md +1 -1
  2. package/dist/elements.js +3285 -3280
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +336 -2
  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/ChevronWide.d.ts +2 -0
  9. package/lib/icons/ChevronWide.js +5 -0
  10. package/lib/icons/ChevronWide.js.map +1 -0
  11. package/lib/icons/Copy.js +2 -4
  12. package/lib/icons/Copy.js.map +1 -1
  13. package/lib/icons/Fan.d.ts +2 -0
  14. package/lib/icons/Fan.js +7 -0
  15. package/lib/icons/Fan.js.map +1 -0
  16. package/lib/icons/Off.d.ts +2 -0
  17. package/lib/icons/Off.js +7 -0
  18. package/lib/icons/Off.js.map +1 -0
  19. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js +9 -2
  20. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js.map +1 -1
  21. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.d.ts +2 -1
  22. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js +4 -4
  23. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js.map +1 -1
  24. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.d.ts +7 -0
  25. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js +40 -0
  26. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js.map +1 -0
  27. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.d.ts +2 -1
  28. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.js +7 -5
  29. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.js.map +1 -1
  30. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.d.ts +3 -1
  31. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js +20 -3
  32. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js.map +1 -1
  33. package/lib/seam/connected-accounts/use-connected-account.d.ts +4 -0
  34. package/lib/seam/connected-accounts/use-connected-account.js +17 -0
  35. package/lib/seam/connected-accounts/use-connected-account.js.map +1 -0
  36. package/lib/seam/index.d.ts +2 -0
  37. package/lib/seam/index.js +2 -0
  38. package/lib/seam/index.js.map +1 -1
  39. package/lib/ui/layout/AccordionRow.d.ts +7 -0
  40. package/lib/ui/layout/AccordionRow.js +8 -0
  41. package/lib/ui/layout/AccordionRow.js.map +1 -0
  42. package/lib/ui/layout/DetailRow.d.ts +7 -0
  43. package/lib/ui/layout/DetailRow.js +5 -0
  44. package/lib/ui/layout/DetailRow.js.map +1 -0
  45. package/lib/ui/layout/DetailSection.d.ts +7 -0
  46. package/lib/ui/layout/DetailSection.js +6 -0
  47. package/lib/ui/layout/DetailSection.js.map +1 -0
  48. package/lib/ui/layout/DetailSectionGroup.d.ts +2 -0
  49. package/lib/ui/layout/DetailSectionGroup.js +5 -0
  50. package/lib/ui/layout/DetailSectionGroup.js.map +1 -0
  51. package/lib/ui/thermostat/ClimateSettingStatus.d.ts +1 -1
  52. package/lib/ui/thermostat/ClimateSettingStatus.js +20 -24
  53. package/lib/ui/thermostat/ClimateSettingStatus.js.map +1 -1
  54. package/lib/ui/thermostat/Temperature.d.ts +8 -0
  55. package/lib/ui/thermostat/Temperature.js +11 -0
  56. package/lib/ui/thermostat/Temperature.js.map +1 -0
  57. package/lib/ui/thermostat/ThermostatCard.d.ts +7 -0
  58. package/lib/ui/thermostat/ThermostatCard.js +46 -0
  59. package/lib/ui/thermostat/ThermostatCard.js.map +1 -0
  60. package/lib/version.d.ts +1 -1
  61. package/lib/version.js +1 -1
  62. package/package.json +3 -3
  63. package/src/lib/icons/ChevronWide.tsx +21 -0
  64. package/src/lib/icons/Copy.tsx +6 -21
  65. package/src/lib/icons/Fan.tsx +36 -0
  66. package/src/lib/icons/Off.tsx +36 -0
  67. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx +23 -2
  68. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.tsx +9 -2
  69. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.element.ts +13 -0
  70. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.tsx +121 -0
  71. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.tsx +18 -6
  72. package/src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.element.ts +2 -0
  73. package/src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.tsx +37 -2
  74. package/src/lib/seam/connected-accounts/use-connected-account.ts +34 -0
  75. package/src/lib/seam/index.ts +2 -0
  76. package/src/lib/ui/layout/AccordionRow.tsx +36 -0
  77. package/src/lib/ui/layout/DetailRow.tsx +22 -0
  78. package/src/lib/ui/layout/DetailSection.tsx +25 -0
  79. package/src/lib/ui/layout/DetailSectionGroup.tsx +7 -0
  80. package/src/lib/ui/thermostat/ClimateSettingStatus.tsx +34 -30
  81. package/src/lib/ui/thermostat/Temperature.tsx +26 -0
  82. package/src/lib/ui/thermostat/ThermostatCard.tsx +160 -0
  83. package/src/lib/version.ts +1 -1
  84. package/src/styles/_access-code-details.scss +42 -1
  85. package/src/styles/_climate-setting-schedule-details.scss +44 -0
  86. package/src/styles/_layout.scss +161 -0
  87. package/src/styles/_main.scss +2 -0
  88. package/src/styles/_seam-table.scss +8 -0
  89. package/src/styles/_thermostat.scss +164 -1
  90. package/src/styles/_tooltip.scss +1 -0
@@ -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
+ }
@@ -8,12 +8,15 @@ import type {
8
8
  DeviceError,
9
9
  } from 'seamapi'
10
10
 
11
+ import { CopyIcon } from 'lib/icons/Copy.js'
11
12
  import { useAccessCode } from 'lib/seam/access-codes/use-access-code.js'
12
13
  import { useDeleteAccessCode } from 'lib/seam/access-codes/use-delete-access-code.js'
13
14
  import { AccessCodeDevice } from 'lib/seam/components/AccessCodeDetails/AccessCodeDevice.js'
14
15
  import { DeviceDetails } from 'lib/seam/components/DeviceDetails/DeviceDetails.js'
15
16
  import { Alerts } from 'lib/ui/Alert/Alerts.js'
16
17
  import { Button } from 'lib/ui/Button.js'
18
+ import { copyToClipboard } from 'lib/ui/clipboard.js'
19
+ import { IconButton } from 'lib/ui/IconButton.js'
17
20
  import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
18
21
  import { useIsDateInPast } from 'lib/ui/use-is-date-in-past.js'
19
22
 
@@ -82,7 +85,16 @@ export function AccessCodeDetails({
82
85
  >
83
86
  <span className='seam-label'>{t.accessCode}</span>
84
87
  <h5 className='seam-access-code-name'>{name}</h5>
85
- <div className='seam-code'>{accessCode.code}</div>
88
+ <div className='seam-code'>
89
+ <span>{accessCode.code}</span>
90
+ <IconButton
91
+ onClick={() => {
92
+ void copyToClipboard(accessCode.code ?? '')
93
+ }}
94
+ >
95
+ <CopyIcon />
96
+ </IconButton>
97
+ </div>
86
98
  <div className='seam-duration'>
87
99
  <Duration accessCode={accessCode} />
88
100
  </div>
@@ -117,7 +129,16 @@ export function AccessCodeDetails({
117
129
  <div className='seam-details'>
118
130
  <div className='seam-row'>
119
131
  <div className='seam-heading'>{t.id}:</div>
120
- <div className='seam-content'>{accessCode.access_code_id}</div>
132
+ <div className='seam-content seam-code-id'>
133
+ <span>{accessCode.access_code_id}</span>
134
+ <IconButton
135
+ onClick={() => {
136
+ void copyToClipboard(accessCode.access_code_id)
137
+ }}
138
+ >
139
+ <CopyIcon />
140
+ </IconButton>
141
+ </div>
121
142
  </div>
122
143
  <div className='seam-row'>
123
144
  <div className='seam-heading'>{t.created}:</div>
@@ -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>
@@ -0,0 +1,13 @@
1
+ import type { ElementProps } from 'lib/element.js'
2
+
3
+ import type { ClimateSettingScheduleDetailsProps } from './ClimateSettingScheduleDetails.js'
4
+
5
+ export const name = 'seam-climate-setting-schedule-details'
6
+
7
+ export const props: ElementProps<ClimateSettingScheduleDetailsProps> = {
8
+ climateSettingScheduleId: 'string',
9
+ onBack: 'object',
10
+ className: 'string',
11
+ }
12
+
13
+ export { ClimateSettingScheduleDetails as Component } from './ClimateSettingScheduleDetails.js'
@@ -0,0 +1,121 @@
1
+ import classNames from 'classnames'
2
+ import { useState } from 'react'
3
+
4
+ import { DeviceDetails } from '../index.js'
5
+
6
+ import { formatDateAndTime } from 'lib/dates.js'
7
+ import { ArrowRightIcon } from 'lib/icons/ArrowRight.js'
8
+ import { useClimateSettingSchedule } from 'lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedule.js'
9
+ import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
10
+ import { DetailRow } from 'lib/ui/layout/DetailRow.js'
11
+ import { DetailSection } from 'lib/ui/layout/DetailSection.js'
12
+ import { DetailSectionGroup } from 'lib/ui/layout/DetailSectionGroup.js'
13
+ import { ClimateSettingStatus } from 'lib/ui/thermostat/ClimateSettingStatus.js'
14
+
15
+ import { ClimateSettingScheduleCard } from './ClimateSettingScheduleCard.js'
16
+
17
+ export interface ClimateSettingScheduleDetailsProps {
18
+ climateSettingScheduleId: string
19
+ onBack?: () => void
20
+ className?: string
21
+ }
22
+
23
+ export function ClimateSettingScheduleDetails({
24
+ climateSettingScheduleId,
25
+ onBack,
26
+ className,
27
+ }: ClimateSettingScheduleDetailsProps): JSX.Element | null {
28
+ const { climateSettingSchedule } = useClimateSettingSchedule(
29
+ climateSettingScheduleId
30
+ )
31
+
32
+ const [selectedDeviceId, selectDevice] = useState<string | null>(null)
33
+
34
+ if (climateSettingSchedule == null) {
35
+ return null
36
+ }
37
+
38
+ const isManualOverrideAllowed =
39
+ climateSettingSchedule.manual_override_allowed ?? false
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
+
53
+ return (
54
+ <div
55
+ className={classNames('seam-climate-setting-schedule-details', className)}
56
+ >
57
+ <ContentHeader title={t.climateSettingSchedule} onBack={onBack} />
58
+ <div className='seam-climate-setting-schedule-details-content'>
59
+ <ClimateSettingScheduleCard
60
+ climateSettingScheduleId={climateSettingScheduleId}
61
+ onSelectDevice={selectDevice}
62
+ />
63
+ <div className='seam-default-setting-message-container'>
64
+ <span className='seam-default-setting-message'>
65
+ {t.defaultSettingMessagePart1}{' '}
66
+ <span className='seam-default-setting-text'>
67
+ {t.defaultSetting}
68
+ </span>{' '}
69
+ {t.defaultSettingMessagePart2}
70
+ </span>
71
+ </div>
72
+ <DetailSectionGroup>
73
+ <DetailSection>
74
+ <DetailRow label={t.startEndTime}>
75
+ <span className='seam-climate-setting-details-value seam-climate-setting-details-schedule-range'>
76
+ {`${formatDateAndTime(
77
+ climateSettingSchedule.schedule_starts_at
78
+ )}`}
79
+ <ArrowRightIcon />
80
+ {`${formatDateAndTime(
81
+ climateSettingSchedule.schedule_ends_at
82
+ )}`}
83
+ </span>
84
+ </DetailRow>
85
+ <DetailRow label={t.climateSetting}>
86
+ <ClimateSettingStatus
87
+ climateSetting={climateSettingSchedule}
88
+ iconPlacement='right'
89
+ />
90
+ </DetailRow>
91
+ <DetailRow label={t.allowManualOverride}>
92
+ <span className='seam-climate-setting-details-value'>
93
+ {isManualOverrideAllowed ? t.on : t.off}
94
+ </span>
95
+ </DetailRow>
96
+ </DetailSection>
97
+ <DetailSection>
98
+ <DetailRow label={t.creationDate}>
99
+ <div className='seam-creation-date'>
100
+ {formatDateAndTime(climateSettingSchedule.created_at)}
101
+ </div>
102
+ </DetailRow>
103
+ </DetailSection>
104
+ </DetailSectionGroup>
105
+ </div>
106
+ </div>
107
+ )
108
+ }
109
+
110
+ const t = {
111
+ climateSettingSchedule: 'Climate setting schedule',
112
+ startEndTime: 'Start/End Time',
113
+ climateSetting: 'Climate setting',
114
+ allowManualOverride: 'Allow manual override',
115
+ creationDate: 'Creation date',
116
+ defaultSettingMessagePart1: 'Thermostat will return to its',
117
+ defaultSetting: 'default setting',
118
+ defaultSettingMessagePart2: 'at end time.',
119
+ on: 'On',
120
+ off: 'Off',
121
+ }
@@ -1,14 +1,16 @@
1
1
  import { isThermostatDevice, type ThermostatDevice } from 'seamapi'
2
2
 
3
- import { ChevronRightIcon } from 'lib/icons/ChevronRight.js'
3
+ import { ChevronWideIcon } from 'lib/icons/ChevronWide.js'
4
4
  import { useDevice } from 'lib/seam/devices/use-device.js'
5
5
  import { DeviceImage } from 'lib/ui/device/DeviceImage.js'
6
6
  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} />
@@ -48,7 +58,9 @@ function Content(props: { device: ThermostatDevice }): JSX.Element {
48
58
  </div>
49
59
  </div>
50
60
  </div>
51
- <ChevronRightIcon />
61
+ <div className='seam-climate-setting-schedule-device-chevron'>
62
+ <ChevronWideIcon />
63
+ </div>
52
64
  </div>
53
65
  )
54
66
  }
@@ -7,6 +7,8 @@ export const name = 'seam-climate-setting-schedule-table'
7
7
  export const props: ElementProps<ClimateSettingScheduleTableProps> = {
8
8
  deviceId: 'string',
9
9
  disableSearch: 'boolean',
10
+ onClimateSettingScheduleClick: 'object',
11
+ preventDefaultOnClimateSettingScheduleClick: 'boolean',
10
12
  climateSettingScheduleFilter: 'object',
11
13
  climateSettingScheduleComparator: 'object',
12
14
  onBack: 'object',
@@ -1,8 +1,9 @@
1
1
  import classNames from 'classnames'
2
- import { useMemo, useState } from 'react'
2
+ import { useCallback, useMemo, useState } from 'react'
3
3
  import type { ClimateSettingSchedule } from 'seamapi'
4
4
 
5
5
  import { compareByCreatedAtDesc } from 'lib/dates.js'
6
+ import { ClimateSettingScheduleDetails } from 'lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDetails.js'
6
7
  import {
7
8
  useClimateSettingSchedules,
8
9
  type UseClimateSettingSchedulesData,
@@ -20,6 +21,8 @@ import { ClimateSettingScheduleRow } from './ClimateSettingScheduleRow.js'
20
21
  export interface ClimateSettingScheduleTableProps {
21
22
  deviceId: string
22
23
  disableSearch?: boolean
24
+ onClimateSettingScheduleClick?: (climateSettingScheduleId: string) => void
25
+ preventDefaultOnClimateSettingScheduleClick?: boolean
23
26
  climateSettingScheduleFilter?: (
24
27
  climateSettingSchedule: ClimateSettingSchedule,
25
28
  searchInputValue: string
@@ -46,8 +49,11 @@ const defaultClimateSettingScheduleFilter = (
46
49
  export function ClimateSettingScheduleTable({
47
50
  deviceId,
48
51
  disableSearch = false,
52
+ onClimateSettingScheduleClick = () => {},
53
+ preventDefaultOnClimateSettingScheduleClick = false,
49
54
  climateSettingScheduleFilter = defaultClimateSettingScheduleFilter,
50
55
  climateSettingScheduleComparator = compareByCreatedAtDesc,
56
+
51
57
  onBack,
52
58
  heading = t.climateSettingSchedules,
53
59
  className,
@@ -58,6 +64,10 @@ export function ClimateSettingScheduleTable({
58
64
  })
59
65
 
60
66
  const [searchInputValue, setSearchInputValue] = useState('')
67
+ const [
68
+ selectedViewClimateSettingScheduleId,
69
+ setSelectedViewClimateSettingScheduleId,
70
+ ] = useState<string | null>(null)
61
71
 
62
72
  const filteredClimateSettingSchedules = useMemo(
63
73
  () =>
@@ -74,6 +84,31 @@ export function ClimateSettingScheduleTable({
74
84
  ]
75
85
  )
76
86
 
87
+ const handleClimateSettingScheduleClick = useCallback(
88
+ (climateSettingScheduleId: string): void => {
89
+ onClimateSettingScheduleClick(climateSettingScheduleId)
90
+ if (preventDefaultOnClimateSettingScheduleClick) return
91
+ setSelectedViewClimateSettingScheduleId(climateSettingScheduleId)
92
+ },
93
+ [
94
+ onClimateSettingScheduleClick,
95
+ preventDefaultOnClimateSettingScheduleClick,
96
+ setSelectedViewClimateSettingScheduleId,
97
+ ]
98
+ )
99
+
100
+ if (selectedViewClimateSettingScheduleId != null) {
101
+ return (
102
+ <ClimateSettingScheduleDetails
103
+ className={className}
104
+ climateSettingScheduleId={selectedViewClimateSettingScheduleId}
105
+ onBack={() => {
106
+ setSelectedViewClimateSettingScheduleId(null)
107
+ }}
108
+ />
109
+ )
110
+ }
111
+
77
112
  if (isLoading) {
78
113
  return <p className={className}>...</p>
79
114
  }
@@ -105,7 +140,7 @@ export function ClimateSettingScheduleTable({
105
140
  <TableBody>
106
141
  <Content
107
142
  climateSettingSchedules={filteredClimateSettingSchedules}
108
- onClimateSettingScheduleClick={() => {}}
143
+ onClimateSettingScheduleClick={handleClimateSettingScheduleClick}
109
144
  />
110
145
  </TableBody>
111
146
  </div>
@@ -0,0 +1,34 @@
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 UseConnectedAccountData = ConnectedAccount | null
13
+
14
+ export function useConnectedAccount(
15
+ params: ConnectedAccountsGetRequest
16
+ ): UseSeamQueryResult<'connectedAccount', UseConnectedAccountData> {
17
+ const normalizedParams =
18
+ typeof params === 'string' ? { connected_account_id: params } : params
19
+
20
+ const { client } = useSeamClient()
21
+ const { data, ...rest } = useQuery<
22
+ ConnectedAccountsGetResponse['connected_account'] | null,
23
+ SeamError
24
+ >({
25
+ enabled: client != null,
26
+ queryKey: ['connected_accounts', 'get', normalizedParams],
27
+ queryFn: async () => {
28
+ if (client == null) return null
29
+ return await client.connectedAccounts.get(normalizedParams)
30
+ },
31
+ })
32
+
33
+ return { ...rest, connectedAccount: data }
34
+ }
@@ -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'
@@ -0,0 +1,36 @@
1
+ import type { PropsWithChildren } from 'react'
2
+
3
+ import { ChevronWideIcon } from 'lib/icons/ChevronWide.js'
4
+ import { useToggle } from 'lib/ui/use-toggle.js'
5
+
6
+ interface AccordionRowProps extends PropsWithChildren {
7
+ label: string
8
+ rightCollapsedContent?: JSX.Element
9
+ }
10
+
11
+ export function AccordionRow({
12
+ label,
13
+ rightCollapsedContent,
14
+ children,
15
+ }: AccordionRowProps): JSX.Element {
16
+ const [isExpanded, toggle] = useToggle()
17
+
18
+ return (
19
+ <div className='seam-accordion-row' aria-expanded={isExpanded}>
20
+ <button className='seam-accordion-row-trigger' onClick={toggle}>
21
+ <p className='seam-row-label'>{label}</p>
22
+ <div className='seam-row-inner-wrap'>
23
+ <div className='seam-row-trigger-right-content'>
24
+ {rightCollapsedContent}
25
+ </div>
26
+ <div className='seam-accordion-icon-wrap'>
27
+ <ChevronWideIcon />
28
+ </div>
29
+ </div>
30
+ </button>
31
+ <div className='seam-accordion-row-content'>
32
+ <div className='seam-accordion-row-inner-content'>{children}</div>
33
+ </div>
34
+ </div>
35
+ )
36
+ }
@@ -0,0 +1,22 @@
1
+ import type { PropsWithChildren } from 'react'
2
+
3
+ interface DetailRowProps {
4
+ label: string
5
+ sublabel?: string
6
+ }
7
+
8
+ export function DetailRow({
9
+ label,
10
+ sublabel,
11
+ children,
12
+ }: PropsWithChildren<DetailRowProps>): JSX.Element {
13
+ return (
14
+ <div className='seam-detail-row'>
15
+ <div className='seam-detail-row-label-wrap'>
16
+ <p className='seam-row-label'>{label}</p>
17
+ {sublabel != null && <p className='seam-row-sublabel'>{sublabel}</p>}
18
+ </div>
19
+ <div className='seam-detail-row-right-area-content'>{children}</div>
20
+ </div>
21
+ )
22
+ }
@@ -0,0 +1,25 @@
1
+ import type { PropsWithChildren } from 'react'
2
+
3
+ import { Tooltip } from 'lib/ui/Tooltip/Tooltip.js'
4
+
5
+ interface DetailSectionProps {
6
+ label?: string
7
+ tooltipContent?: string
8
+ }
9
+
10
+ export function DetailSection({
11
+ label,
12
+ tooltipContent,
13
+ children,
14
+ }: PropsWithChildren<DetailSectionProps>): JSX.Element {
15
+ return (
16
+ <div className='seam-detail-section'>
17
+ <div className='seam-detail-label-wrap'>
18
+ {label != null && <p className='seam-detail-label'>{label}</p>}
19
+ {tooltipContent != null && <Tooltip>{tooltipContent}</Tooltip>}
20
+ </div>
21
+
22
+ <div className='seam-detail-group'>{children}</div>
23
+ </div>
24
+ )
25
+ }
@@ -0,0 +1,7 @@
1
+ import type { PropsWithChildren } from 'react'
2
+
3
+ export function DetailSectionGroup({
4
+ children,
5
+ }: PropsWithChildren): JSX.Element {
6
+ return <div className='seam-detail-sections'>{children}</div>
7
+ }
@@ -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
  }