@seamapi/react 1.47.0 → 1.48.1

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 (126) hide show
  1. package/README.md +1 -1
  2. package/dist/elements.js +3312 -3292
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +465 -1
  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 +9 -0
  9. package/lib/dates.js +15 -0
  10. package/lib/dates.js.map +1 -1
  11. package/lib/icons/ArrowRight.d.ts +2 -0
  12. package/lib/icons/ArrowRight.js +7 -0
  13. package/lib/icons/ArrowRight.js.map +1 -0
  14. package/lib/icons/Info.d.ts +2 -0
  15. package/lib/icons/Info.js +7 -0
  16. package/lib/icons/Info.js.map +1 -0
  17. package/lib/icons/InfoDark.d.ts +2 -0
  18. package/lib/icons/InfoDark.js +7 -0
  19. package/lib/icons/InfoDark.js.map +1 -0
  20. package/lib/icons/TemperatureAdd.d.ts +2 -0
  21. package/lib/icons/TemperatureAdd.js +7 -0
  22. package/lib/icons/TemperatureAdd.js.map +1 -0
  23. package/lib/icons/TemperatureSubtract.d.ts +2 -0
  24. package/lib/icons/TemperatureSubtract.js +7 -0
  25. package/lib/icons/TemperatureSubtract.js.map +1 -0
  26. package/lib/seam/SeamProvider.d.ts +2 -2
  27. package/lib/seam/SeamProvider.js.map +1 -1
  28. package/lib/seam/access-codes/use-generate-access-code-code.d.ts +6 -0
  29. package/lib/seam/access-codes/use-generate-access-code-code.js +13 -0
  30. package/lib/seam/access-codes/use-generate-access-code-code.js.map +1 -0
  31. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js.map +1 -1
  32. package/lib/seam/components/AccessCodeDetails/AccessCodeDevice.js +1 -1
  33. package/lib/seam/components/AccessCodeDetails/AccessCodeDevice.js.map +1 -1
  34. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.d.ts +6 -0
  35. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js +41 -0
  36. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.js.map +1 -0
  37. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.d.ts +4 -0
  38. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.js +29 -0
  39. package/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.js.map +1 -0
  40. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleRow.js +2 -2
  41. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleRow.js.map +1 -1
  42. package/lib/seam/components/ClimateSettingScheduleTable/{ClimateSettingScheduleDetails.d.ts → ClimateSettingScheduleRowDetails.d.ts} +1 -1
  43. package/lib/seam/components/ClimateSettingScheduleTable/{ClimateSettingScheduleDetails.js → ClimateSettingScheduleRowDetails.js} +2 -2
  44. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleRowDetails.js.map +1 -0
  45. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.d.ts +6 -1
  46. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js +21 -4
  47. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.js.map +1 -1
  48. package/lib/seam/components/DeviceTable/DeviceTable.js +2 -2
  49. package/lib/seam/components/DeviceTable/DeviceTable.js.map +1 -1
  50. package/lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedule.d.ts +5 -0
  51. package/lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedule.js +24 -0
  52. package/lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedule.js.map +1 -0
  53. package/lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedules.js +1 -0
  54. package/lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedules.js.map +1 -1
  55. package/lib/seam/thermostats/climate-setting-schedules/use-create-climate-setting-schedule.d.ts +5 -0
  56. package/lib/seam/thermostats/climate-setting-schedules/use-create-climate-setting-schedule.js +21 -0
  57. package/lib/seam/thermostats/climate-setting-schedules/use-create-climate-setting-schedule.js.map +1 -0
  58. package/lib/seam/thermostats/climate-setting-schedules/use-delete-climate-setting-schedule.d.ts +5 -0
  59. package/lib/seam/thermostats/climate-setting-schedules/use-delete-climate-setting-schedule.js +27 -0
  60. package/lib/seam/thermostats/climate-setting-schedules/use-delete-climate-setting-schedule.js.map +1 -0
  61. package/lib/seam/use-seam-client.js +2 -2
  62. package/lib/seam/use-seam-client.js.map +1 -1
  63. package/lib/ui/AccessCodeForm/AccessCodeFormDatePicker.js +1 -1
  64. package/lib/ui/AccessCodeForm/AccessCodeFormDatePicker.js.map +1 -1
  65. package/lib/ui/Menu/Menu.js +5 -5
  66. package/lib/ui/Menu/Menu.js.map +1 -1
  67. package/lib/ui/Switch/Switch.d.ts +8 -0
  68. package/lib/ui/Switch/Switch.js +15 -0
  69. package/lib/ui/Switch/Switch.js.map +1 -0
  70. package/lib/ui/TextField/TextField.js +1 -3
  71. package/lib/ui/TextField/TextField.js.map +1 -1
  72. package/lib/ui/Tooltip/Tooltip.d.ts +2 -0
  73. package/lib/ui/Tooltip/Tooltip.js +33 -0
  74. package/lib/ui/Tooltip/Tooltip.js.map +1 -0
  75. package/lib/ui/clipboard.js +1 -1
  76. package/lib/ui/clipboard.js.map +1 -1
  77. package/lib/ui/thermostat/ClimateSettingStatus.js +10 -3
  78. package/lib/ui/thermostat/ClimateSettingStatus.js.map +1 -1
  79. package/lib/ui/thermostat/TemperatureControl.d.ts +11 -0
  80. package/lib/ui/thermostat/TemperatureControl.js +42 -0
  81. package/lib/ui/thermostat/TemperatureControl.js.map +1 -0
  82. package/lib/ui/use-current-time.d.ts +2 -0
  83. package/lib/ui/use-current-time.js +12 -0
  84. package/lib/ui/use-current-time.js.map +1 -0
  85. package/lib/version.d.ts +1 -1
  86. package/lib/version.js +1 -1
  87. package/package.json +7 -7
  88. package/src/lib/dates.ts +17 -0
  89. package/src/lib/element.tsx +6 -4
  90. package/src/lib/icons/ArrowRight.tsx +36 -0
  91. package/src/lib/icons/Info.tsx +36 -0
  92. package/src/lib/icons/InfoDark.tsx +37 -0
  93. package/src/lib/icons/TemperatureAdd.tsx +38 -0
  94. package/src/lib/icons/TemperatureSubtract.tsx +35 -0
  95. package/src/lib/seam/SeamProvider.tsx +2 -3
  96. package/src/lib/seam/access-codes/use-generate-access-code-code.ts +38 -0
  97. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx +0 -2
  98. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDevice.tsx +1 -4
  99. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleCard.tsx +101 -0
  100. package/src/lib/seam/components/ClimateSettingScheduleDetails/ClimateSettingScheduleDeviceBar.tsx +58 -0
  101. package/src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleRow.tsx +2 -2
  102. package/src/lib/seam/components/ClimateSettingScheduleTable/{ClimateSettingScheduleDetails.tsx → ClimateSettingScheduleRowDetails.tsx} +1 -1
  103. package/src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.element.ts +4 -0
  104. package/src/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleTable.tsx +59 -6
  105. package/src/lib/seam/components/DeviceTable/DeviceTable.tsx +2 -2
  106. package/src/lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedule.ts +47 -0
  107. package/src/lib/seam/thermostats/climate-setting-schedules/use-climate-setting-schedules.ts +1 -0
  108. package/src/lib/seam/thermostats/climate-setting-schedules/use-create-climate-setting-schedule.ts +48 -0
  109. package/src/lib/seam/thermostats/climate-setting-schedules/use-delete-climate-setting-schedule.ts +53 -0
  110. package/src/lib/seam/use-seam-client.ts +2 -2
  111. package/src/lib/ui/AccessCodeForm/AccessCodeFormDatePicker.tsx +2 -2
  112. package/src/lib/ui/Menu/Menu.tsx +5 -6
  113. package/src/lib/ui/Switch/Switch.tsx +38 -0
  114. package/src/lib/ui/TextField/TextField.tsx +1 -3
  115. package/src/lib/ui/Tooltip/Tooltip.tsx +64 -0
  116. package/src/lib/ui/clipboard.ts +1 -1
  117. package/src/lib/ui/thermostat/ClimateSettingStatus.tsx +12 -10
  118. package/src/lib/ui/thermostat/TemperatureControl.tsx +121 -0
  119. package/src/lib/ui/use-current-time.ts +16 -0
  120. package/src/lib/version.ts +1 -1
  121. package/src/styles/_colors.scss +4 -0
  122. package/src/styles/_main.scss +4 -0
  123. package/src/styles/_switch.scss +53 -0
  124. package/src/styles/_thermostat.scss +384 -2
  125. package/src/styles/_tooltip.scss +86 -0
  126. package/lib/seam/components/ClimateSettingScheduleTable/ClimateSettingScheduleDetails.js.map +0 -1
@@ -4,7 +4,7 @@ import { TableCell } from 'lib/ui/Table/TableCell.js'
4
4
  import { TableRow } from 'lib/ui/Table/TableRow.js'
5
5
  import { Title } from 'lib/ui/typography/Title.js'
6
6
 
7
- import { ClimateSettingScheduleDetails } from './ClimateSettingScheduleDetails.js'
7
+ import { ClimateSettingScheduleRowDetails } from './ClimateSettingScheduleRowDetails.js'
8
8
 
9
9
  interface ClimateSettingScheduleRowProps {
10
10
  climateSettingSchedule: UseClimateSettingSchedulesData[number]
@@ -26,7 +26,7 @@ export function ClimateSettingScheduleRow({
26
26
  <Title className='seam-truncated-text'>
27
27
  {climateSettingSchedule.name}
28
28
  </Title>
29
- <ClimateSettingScheduleDetails
29
+ <ClimateSettingScheduleRowDetails
30
30
  climateSettingSchedule={climateSettingSchedule}
31
31
  />
32
32
  </TableCell>
@@ -6,7 +6,7 @@ import { DotDivider } from 'lib/ui/layout/DotDivider.js'
6
6
  import { ClimateSettingStatus } from 'lib/ui/thermostat/ClimateSettingStatus.js'
7
7
  import { useIsDateInPast } from 'lib/ui/use-is-date-in-past.js'
8
8
 
9
- export function ClimateSettingScheduleDetails(props: {
9
+ export function ClimateSettingScheduleRowDetails(props: {
10
10
  climateSettingSchedule: ClimateSettingSchedule
11
11
  }): JSX.Element {
12
12
  const { climateSettingSchedule } = props
@@ -6,7 +6,11 @@ export const name = 'seam-climate-setting-schedule-table'
6
6
 
7
7
  export const props: ElementProps<ClimateSettingScheduleTableProps> = {
8
8
  deviceId: 'string',
9
+ disableSearch: 'boolean',
10
+ climateSettingScheduleFilter: 'object',
11
+ climateSettingScheduleComparator: 'object',
9
12
  onBack: 'object',
13
+ heading: 'string',
10
14
  className: 'string',
11
15
  }
12
16
 
@@ -1,5 +1,8 @@
1
1
  import classNames from 'classnames'
2
+ import { useMemo, useState } from 'react'
3
+ import type { ClimateSettingSchedule } from 'seamapi'
2
4
 
5
+ import { compareByCreatedAtDesc } from 'lib/dates.js'
3
6
  import {
4
7
  useClimateSettingSchedules,
5
8
  type UseClimateSettingSchedulesData,
@@ -9,19 +12,44 @@ import { EmptyPlaceholder } from 'lib/ui/Table/EmptyPlaceholder.js'
9
12
  import { TableBody } from 'lib/ui/Table/TableBody.js'
10
13
  import { TableHeader } from 'lib/ui/Table/TableHeader.js'
11
14
  import { TableTitle } from 'lib/ui/Table/TableTitle.js'
15
+ import { SearchTextField } from 'lib/ui/TextField/SearchTextField.js'
12
16
  import { Caption } from 'lib/ui/typography/Caption.js'
13
17
 
14
18
  import { ClimateSettingScheduleRow } from './ClimateSettingScheduleRow.js'
15
19
 
16
20
  export interface ClimateSettingScheduleTableProps {
17
21
  deviceId: string
22
+ disableSearch?: boolean
23
+ climateSettingScheduleFilter?: (
24
+ climateSettingSchedule: ClimateSettingSchedule,
25
+ searchInputValue: string
26
+ ) => boolean
27
+ climateSettingScheduleComparator?: (
28
+ climateSettingScheduleA: ClimateSettingSchedule,
29
+ climateSettingScheduleB: ClimateSettingSchedule
30
+ ) => number
18
31
  onBack?: () => void
32
+ heading?: string | null
19
33
  className?: string
20
34
  }
21
35
 
36
+ const defaultClimateSettingScheduleFilter = (
37
+ climateSettingSchedule: ClimateSettingSchedule,
38
+ searchInputValue: string
39
+ ): boolean => {
40
+ const value = searchInputValue.trim().toLowerCase()
41
+ if (value === '') return true
42
+ const name = climateSettingSchedule.name ?? ''
43
+ return name.trim().toLowerCase().includes(value)
44
+ }
45
+
22
46
  export function ClimateSettingScheduleTable({
23
47
  deviceId,
48
+ disableSearch = false,
49
+ climateSettingScheduleFilter = defaultClimateSettingScheduleFilter,
50
+ climateSettingScheduleComparator = compareByCreatedAtDesc,
24
51
  onBack,
52
+ heading = t.climateSettingSchedules,
25
53
  className,
26
54
  }: ClimateSettingScheduleTableProps): JSX.Element {
27
55
  const { climateSettingSchedules, isLoading, isError, error } =
@@ -29,8 +57,22 @@ export function ClimateSettingScheduleTable({
29
57
  device_id: deviceId,
30
58
  })
31
59
 
32
- // TODO filtering
33
- const filteredClimateSettingSchedules = climateSettingSchedules ?? []
60
+ const [searchInputValue, setSearchInputValue] = useState('')
61
+
62
+ const filteredClimateSettingSchedules = useMemo(
63
+ () =>
64
+ climateSettingSchedules
65
+ ?.filter((schedule) =>
66
+ climateSettingScheduleFilter(schedule, searchInputValue)
67
+ )
68
+ ?.sort(climateSettingScheduleComparator) ?? [],
69
+ [
70
+ climateSettingSchedules,
71
+ searchInputValue,
72
+ climateSettingScheduleFilter,
73
+ climateSettingScheduleComparator,
74
+ ]
75
+ )
34
76
 
35
77
  if (isLoading) {
36
78
  return <p className={className}>...</p>
@@ -44,10 +86,21 @@ export function ClimateSettingScheduleTable({
44
86
  <div className={classNames('seam-table', className)}>
45
87
  <ContentHeader onBack={onBack} />
46
88
  <TableHeader>
47
- <TableTitle>
48
- {t.climateSettingSchedules}{' '}
49
- <Caption>({filteredClimateSettingSchedules.length})</Caption>
50
- </TableTitle>
89
+ {heading != null ? (
90
+ <TableTitle>
91
+ {heading ?? t.climateSettingSchedules}{' '}
92
+ <Caption>({filteredClimateSettingSchedules.length})</Caption>
93
+ </TableTitle>
94
+ ) : (
95
+ <div className='seam-fragment' />
96
+ )}
97
+ {!disableSearch && (
98
+ <SearchTextField
99
+ value={searchInputValue}
100
+ onChange={setSearchInputValue}
101
+ disabled={(climateSettingSchedules?.length ?? 0) === 0}
102
+ />
103
+ )}
51
104
  </TableHeader>
52
105
  <TableBody>
53
106
  <Content
@@ -1,6 +1,6 @@
1
1
  import classNames from 'classnames'
2
2
  import { useCallback, useMemo, useState } from 'react'
3
- import { isLockDevice } from 'seamapi'
3
+ import { isLockDevice, isThermostatDevice } from 'seamapi'
4
4
 
5
5
  import { compareByCreatedAtDesc } from 'lib/dates.js'
6
6
  import { DeviceDetails } from 'lib/seam/components/DeviceDetails/DeviceDetails.js'
@@ -76,7 +76,7 @@ export function DeviceTable({
76
76
  const filteredDevices = useMemo(
77
77
  () =>
78
78
  devices
79
- ?.filter(isLockDevice)
79
+ ?.filter((device) => isLockDevice(device) || isThermostatDevice(device))
80
80
  ?.filter((device) => deviceFilter(device, searchInputValue))
81
81
  ?.sort(deviceComparator) ?? [],
82
82
  [devices, searchInputValue, deviceFilter, deviceComparator]
@@ -0,0 +1,47 @@
1
+ import { useQuery } from '@tanstack/react-query'
2
+ import type {
3
+ ClimateSettingSchedule,
4
+ ClimateSettingScheduleGetRequest,
5
+ ClimateSettingScheduleGetResponse,
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 UseClimateSettingScheduleParams =
13
+ | ClimateSettingScheduleGetRequest
14
+ | string
15
+
16
+ export type UseClimateSettingScheduleData = ClimateSettingSchedule | null
17
+
18
+ export function useClimateSettingSchedule(
19
+ params: UseClimateSettingScheduleParams
20
+ ): UseSeamQueryResult<'climateSettingSchedule', UseClimateSettingScheduleData> {
21
+ const normalizedParams =
22
+ typeof params === 'string'
23
+ ? { climate_setting_schedule_id: params }
24
+ : params
25
+ const { client } = useSeamClient()
26
+
27
+ const { data, ...rest } = useQuery<
28
+ ClimateSettingScheduleGetResponse['climate_setting_schedule'] | null,
29
+ SeamError
30
+ >({
31
+ enabled: client != null,
32
+ queryKey: [
33
+ 'thermostats',
34
+ 'climate_setting_schedules',
35
+ 'get',
36
+ normalizedParams,
37
+ ],
38
+ queryFn: async () => {
39
+ if (client == null) return null
40
+ return await client.thermostats.climateSettingSchedules.get(
41
+ normalizedParams
42
+ )
43
+ },
44
+ })
45
+
46
+ return { ...rest, climateSettingSchedule: data }
47
+ }
@@ -37,6 +37,7 @@ export function useClimateSettingSchedules(
37
37
  for (const schedule of schedules) {
38
38
  queryClient.setQueryData(
39
39
  [
40
+ 'thermostats',
40
41
  'climate_setting_schedules',
41
42
  'get',
42
43
  {
@@ -0,0 +1,48 @@
1
+ import {
2
+ useMutation,
3
+ type UseMutationResult,
4
+ useQueryClient,
5
+ } from '@tanstack/react-query'
6
+ import type {
7
+ ClimateSettingSchedule,
8
+ ClimateSettingScheduleCreateRequest,
9
+ ClimateSettingScheduleCreateResponse,
10
+ SeamError,
11
+ } from 'seamapi'
12
+
13
+ import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
14
+
15
+ export type UseCreateClimateSettingScheduleData = ClimateSettingSchedule
16
+ export type UseCreateClimateSettingScheduleMutationParams =
17
+ ClimateSettingScheduleCreateRequest
18
+
19
+ export function useCreateClimateSettingSchedule(): UseMutationResult<
20
+ UseCreateClimateSettingScheduleData,
21
+ SeamError,
22
+ UseCreateClimateSettingScheduleMutationParams
23
+ > {
24
+ const { client } = useSeamClient()
25
+ const queryClient = useQueryClient()
26
+
27
+ return useMutation<
28
+ ClimateSettingScheduleCreateResponse['climate_setting_schedule'],
29
+ SeamError,
30
+ ClimateSettingScheduleCreateRequest
31
+ >({
32
+ mutationFn: async (
33
+ mutationParams: UseCreateClimateSettingScheduleMutationParams
34
+ ) => {
35
+ if (client === null) throw new NullSeamClientError()
36
+ return await client.thermostats.climateSettingSchedules.create(
37
+ mutationParams
38
+ )
39
+ },
40
+ onSuccess: () => {
41
+ void queryClient.invalidateQueries([
42
+ 'thermostats',
43
+ 'climate_setting_schedules',
44
+ 'list',
45
+ ])
46
+ },
47
+ })
48
+ }
@@ -0,0 +1,53 @@
1
+ import {
2
+ useMutation,
3
+ type UseMutationResult,
4
+ useQueryClient,
5
+ } from '@tanstack/react-query'
6
+ import type {
7
+ ClimateSettingScheduleDeleteRequest,
8
+ ClimateSettingScheduleDeleteResponse,
9
+ SeamError,
10
+ } from 'seamapi'
11
+
12
+ import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
13
+
14
+ export type UseDeleteClimateSettingScheduleMutationParams =
15
+ ClimateSettingScheduleDeleteRequest
16
+
17
+ export type UseDeleteClimateSettingScheduleData =
18
+ ClimateSettingScheduleDeleteResponse
19
+
20
+ export function useDeleteClimateSettingSchedule(): UseMutationResult<
21
+ UseDeleteClimateSettingScheduleData,
22
+ SeamError,
23
+ UseDeleteClimateSettingScheduleMutationParams
24
+ > {
25
+ const { client } = useSeamClient()
26
+ const queryClient = useQueryClient()
27
+
28
+ return useMutation<
29
+ UseDeleteClimateSettingScheduleData,
30
+ SeamError,
31
+ ClimateSettingScheduleDeleteRequest
32
+ >({
33
+ mutationFn: async (
34
+ mutationParams: UseDeleteClimateSettingScheduleMutationParams
35
+ ) => {
36
+ if (client === null) throw new NullSeamClientError()
37
+ await client.thermostats.climateSettingSchedules.delete(mutationParams)
38
+ },
39
+ onSuccess: (_data, variables) => {
40
+ void queryClient.invalidateQueries([
41
+ 'thermostats',
42
+ 'climate_setting_schedules',
43
+ 'get',
44
+ { climate_setting_schedule_id: variables.climate_setting_schedule_id },
45
+ ])
46
+ void queryClient.invalidateQueries([
47
+ 'thermostats',
48
+ 'climate_setting_schedules',
49
+ 'list',
50
+ ])
51
+ },
52
+ })
53
+ }
@@ -102,10 +102,10 @@ This is not recommended because the client session is now bound to this machine
102
102
  }
103
103
 
104
104
  const fingerprint =
105
- globalThis?.localStorage?.getItem('seam_user_fingerprint') ??
105
+ globalThis.localStorage?.getItem('seam_user_fingerprint') ??
106
106
  `fingerprint_${uuidv4()}`
107
107
 
108
- globalThis?.localStorage?.setItem('seam_user_fingerprint', fingerprint)
108
+ globalThis.localStorage?.setItem('seam_user_fingerprint', fingerprint)
109
109
 
110
110
  return fingerprint
111
111
  }
@@ -42,7 +42,7 @@ export function AccessCodeFormDatePicker({
42
42
  <FormField>
43
43
  <InputLabel>{t.startTimeLabel}</InputLabel>
44
44
  <DateTimePicker
45
- value={startDate == null ? '' : startDate}
45
+ value={startDate ?? ''}
46
46
  onChange={setStartDate}
47
47
  size='large'
48
48
  />
@@ -50,7 +50,7 @@ export function AccessCodeFormDatePicker({
50
50
  <FormField>
51
51
  <InputLabel>{t.endTimeLabel}</InputLabel>
52
52
  <DateTimePicker
53
- value={endDate == null ? '' : endDate}
53
+ value={endDate ?? ''}
54
54
  onChange={setEndDate}
55
55
  size='large'
56
56
  />
@@ -47,8 +47,7 @@ export function Menu({
47
47
  const [left, setLeft] = useState(0)
48
48
 
49
49
  useEffect(() => {
50
- const containers =
51
- globalThis?.document?.querySelectorAll('.seam-components')
50
+ const containers = globalThis.document?.querySelectorAll('.seam-components')
52
51
  if (containers == null) return
53
52
  const el = containers[containers.length - 1]
54
53
  if (el != null) {
@@ -110,11 +109,11 @@ export function Menu({
110
109
 
111
110
  useLayoutEffect(() => {
112
111
  setPositions()
113
- globalThis?.addEventListener('scroll', setPositions)
114
- globalThis?.addEventListener('resize', setPositions)
112
+ globalThis.addEventListener('scroll', setPositions)
113
+ globalThis.addEventListener('resize', setPositions)
115
114
  return () => {
116
- globalThis?.removeEventListener('scroll', setPositions)
117
- globalThis?.removeEventListener('resize', setPositions)
115
+ globalThis.removeEventListener('scroll', setPositions)
116
+ globalThis.removeEventListener('resize', setPositions)
118
117
  }
119
118
  }, [setPositions])
120
119
 
@@ -0,0 +1,38 @@
1
+ import classNames from 'classnames'
2
+
3
+ export interface SwitchProps {
4
+ checked: boolean
5
+ onChange: (checked: boolean) => void
6
+ enableLabel?: boolean
7
+ }
8
+
9
+ function Switch({
10
+ checked,
11
+ onChange,
12
+ enableLabel = false,
13
+ }: SwitchProps): JSX.Element {
14
+ return (
15
+ <div className='seam-switch-container'>
16
+ {enableLabel && (
17
+ <label className='seam-switch-label'>{checked ? t.on : t.off}</label>
18
+ )}
19
+ <div
20
+ className={classNames('seam-switch', {
21
+ 'seam-switch-checked': checked,
22
+ })}
23
+ onClick={() => {
24
+ onChange(!checked)
25
+ }}
26
+ >
27
+ <div className='seam-switch-slider' />
28
+ </div>
29
+ </div>
30
+ )
31
+ }
32
+
33
+ const t = {
34
+ on: 'On',
35
+ off: 'Off',
36
+ }
37
+
38
+ export default Switch
@@ -74,9 +74,7 @@ export const TextField = forwardRef<
74
74
  }
75
75
 
76
76
  // Maintain a local ref, and still forward it along
77
- useImperativeHandle(ref, () => (inputEl != null ? inputEl : undefined), [
78
- inputEl,
79
- ])
77
+ useImperativeHandle(ref, () => inputEl ?? undefined, [inputEl])
80
78
 
81
79
  const endAdornmentVisible = endAdornment != null || clearable
82
80
 
@@ -0,0 +1,64 @@
1
+ import {
2
+ type MouseEventHandler,
3
+ type PropsWithChildren,
4
+ useEffect,
5
+ useState,
6
+ } from 'react'
7
+
8
+ import { InfoIcon } from 'lib/icons/Info.js'
9
+ import { InfoDarkIcon } from 'lib/icons/InfoDark.js'
10
+
11
+ export function Tooltip({ children }: PropsWithChildren): JSX.Element {
12
+ const [isOpen, setIsOpen] = useState(false)
13
+
14
+ useEffect(() => {
15
+ const handleClose = (): void => {
16
+ setIsOpen(false)
17
+ }
18
+
19
+ const handleEscape = (event: KeyboardEvent): void => {
20
+ if (event.key === 'Escape') {
21
+ handleClose()
22
+ }
23
+ }
24
+ if (isOpen) {
25
+ globalThis.document?.addEventListener('click', handleClose)
26
+ globalThis.addEventListener?.('keydown', handleEscape)
27
+ }
28
+
29
+ return () => {
30
+ globalThis.document?.removeEventListener('click', handleClose)
31
+ globalThis.removeEventListener?.('keydown', handleEscape)
32
+ }
33
+ }, [isOpen, setIsOpen])
34
+
35
+ const handleToggle: MouseEventHandler<HTMLButtonElement> = (event): void => {
36
+ event.stopPropagation()
37
+ setIsOpen((prev) => !prev)
38
+ }
39
+
40
+ return (
41
+ <div className='seam-tooltip'>
42
+ <div className='seam-tooltip-trigger-wrap'>
43
+ <button onClick={handleToggle} className='seam-tooltip-button'>
44
+ <div className='seam-tooltip-button-icon seam-tooltip-button-icon-default'>
45
+ <InfoIcon />
46
+ </div>
47
+ <div className='seam-tooltip-button-icon seam-tooltip-button-icon-hover'>
48
+ <InfoDarkIcon />
49
+ </div>
50
+ </button>
51
+ </div>
52
+
53
+ <div
54
+ className='seam-tooltip-popover'
55
+ aria-expanded={isOpen}
56
+ onClick={(event) => {
57
+ event.stopPropagation()
58
+ }}
59
+ >
60
+ <p className='seam-tooltip-text'>{children}</p>
61
+ </div>
62
+ </div>
63
+ )
64
+ }
@@ -1,3 +1,3 @@
1
1
  export async function copyToClipboard(value: string): Promise<void> {
2
- await globalThis?.navigator?.clipboard?.writeText(value)
2
+ await globalThis.navigator?.clipboard?.writeText(value)
3
3
  }
@@ -23,16 +23,16 @@ export function ClimateSettingStatus({
23
23
  )}
24
24
  <Content
25
25
  mode={climateSetting.hvac_mode_setting}
26
- coolingSetPoint={
26
+ coolingSetPoint={formatTemperatureValue(
27
27
  temperatureUnit === 'fahrenheit'
28
28
  ? climateSetting.cooling_set_point_fahrenheit
29
29
  : climateSetting.cooling_set_point_celsius
30
- }
31
- heatingSetPoint={
30
+ )}
31
+ heatingSetPoint={formatTemperatureValue(
32
32
  temperatureUnit === 'fahrenheit'
33
33
  ? climateSetting.heating_set_point_fahrenheit
34
34
  : climateSetting.heating_set_point_celsius
35
- }
35
+ )}
36
36
  temperatureUnit={temperatureUnit}
37
37
  />
38
38
  {iconPlacement === 'right' && (
@@ -59,12 +59,8 @@ function ClimateSettingIcon(props: {
59
59
 
60
60
  function Content(props: {
61
61
  mode: ClimateSetting['hvac_mode_setting']
62
- coolingSetPoint:
63
- | ClimateSetting['cooling_set_point_fahrenheit']
64
- | ClimateSetting['cooling_set_point_celsius']
65
- heatingSetPoint:
66
- | ClimateSetting['heating_set_point_fahrenheit']
67
- | ClimateSetting['heating_set_point_celsius']
62
+ coolingSetPoint: string
63
+ heatingSetPoint: string
68
64
  temperatureUnit: 'fahrenheit' | 'celsius'
69
65
  }): JSX.Element | null {
70
66
  const { mode, coolingSetPoint, heatingSetPoint, temperatureUnit } = props
@@ -89,6 +85,12 @@ function Content(props: {
89
85
  return null
90
86
  }
91
87
 
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)
92
+ }
93
+
92
94
  const t = {
93
95
  degreeFahrenheit: '°F',
94
96
  degreeCelsius: '°C',
@@ -0,0 +1,121 @@
1
+ import { type ChangeEventHandler, useEffect, useRef } from 'react'
2
+
3
+ import { TemperatureAddIcon } from 'lib/icons/TemperatureAdd.js'
4
+ import { TemperatureSubtractIcon } from 'lib/icons/TemperatureSubtract.js'
5
+
6
+ interface TemperatureControlProps {
7
+ variant: 'heat' | 'cool'
8
+ value: number
9
+ onChange: (temperature: number) => void
10
+ min?: number
11
+ max?: number
12
+ unit?: 'fahrenheit' | 'celsius'
13
+ }
14
+
15
+ export function TemperatureControl({
16
+ variant,
17
+ value,
18
+ onChange,
19
+ min = 50,
20
+ max = 90,
21
+ unit = 'fahrenheit',
22
+ }: TemperatureControlProps): JSX.Element {
23
+ const increment = (): void => {
24
+ onChange(Math.min(value + 1, max))
25
+ }
26
+
27
+ const decrement = (): void => {
28
+ onChange(Math.max(value - 1, min))
29
+ }
30
+
31
+ return (
32
+ <div className='seam-temperature-control'>
33
+ <button className='seam-temperature-stepper-button' onClick={decrement}>
34
+ <TemperatureSubtractIcon />
35
+ </button>
36
+ <RangeSlider
37
+ variant={variant}
38
+ min={min}
39
+ max={max}
40
+ value={value}
41
+ onChange={(temperature) => {
42
+ onChange(temperature)
43
+ }}
44
+ unit={unit}
45
+ />
46
+ <button className='seam-temperature-stepper-button' onClick={increment}>
47
+ <TemperatureAddIcon />
48
+ </button>
49
+ </div>
50
+ )
51
+ }
52
+
53
+ interface RangeSliderProps {
54
+ variant: 'heat' | 'cool'
55
+ value: number
56
+ onChange: (temperature: number) => void
57
+ min: number
58
+ max: number
59
+ unit: 'fahrenheit' | 'celsius'
60
+ }
61
+
62
+ function RangeSlider({
63
+ variant,
64
+ value,
65
+ onChange,
66
+ min,
67
+ max,
68
+ unit,
69
+ }: RangeSliderProps): JSX.Element {
70
+ const wrapRef = useRef<HTMLDivElement>(null)
71
+ const inputRef = useRef<HTMLInputElement>(null)
72
+
73
+ useEffect(() => {
74
+ if (wrapRef.current == null || inputRef.current == null) return
75
+
76
+ const wrap = wrapRef.current
77
+ const input = inputRef.current
78
+
79
+ wrap.style.setProperty('--temperature-current', input.value)
80
+ wrap.style.setProperty('--temperature-min', input.min)
81
+ wrap.style.setProperty('--temperature-max', input.max)
82
+ }, [value])
83
+
84
+ const handleRangeChange: ChangeEventHandler<HTMLInputElement> = (
85
+ event
86
+ ): void => {
87
+ const wrap = wrapRef.current
88
+
89
+ if (wrap == null) return
90
+ const temperature = event.target.value
91
+ wrap.style.setProperty('--temperature-current', temperature)
92
+ onChange(Number(temperature))
93
+ }
94
+
95
+ return (
96
+ <div ref={wrapRef} className='seam-temperature-range-wrap'>
97
+ <input
98
+ ref={inputRef}
99
+ type='range'
100
+ min={min}
101
+ max={max}
102
+ value={value}
103
+ onChange={handleRangeChange}
104
+ className='seam-temperature-range'
105
+ data-variant={variant}
106
+ />
107
+
108
+ <div className='seam-floating-temperature'>
109
+ <span className='seam-floating-temperature-value'>{value}</span>
110
+ <span className='seam-floating-temperature-unit'>
111
+ {unit === 'fahrenheit' ? t.degreeFahrenheit : t.degreeCelsius}
112
+ </span>
113
+ </div>
114
+ </div>
115
+ )
116
+ }
117
+
118
+ const t = {
119
+ degreeFahrenheit: 'ºF',
120
+ degreeCelsius: 'ºC',
121
+ }