@seamapi/react 4.6.0 → 4.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/elements.js +11791 -9562
- package/dist/elements.js.map +1 -1
- package/dist/index.css +255 -3
- package/dist/index.css.map +1 -1
- package/dist/index.min.css +1 -1
- package/dist/index.min.css.map +1 -1
- package/lib/errors.d.ts +1 -0
- package/lib/errors.js +11 -0
- package/lib/errors.js.map +1 -0
- package/lib/icons/Trash.d.ts +2 -0
- package/lib/icons/Trash.js +5 -0
- package/lib/icons/Trash.js.map +1 -0
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +17 -1
- package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
- package/lib/seam/thermostats/thermostat-device.d.ts +2 -1
- package/lib/seam/thermostats/thermostat-device.js +1 -1
- package/lib/seam/thermostats/thermostat-device.js.map +1 -1
- package/lib/seam/thermostats/unit-conversion.d.ts +5 -2
- package/lib/seam/thermostats/unit-conversion.js +5 -2
- package/lib/seam/thermostats/unit-conversion.js.map +1 -1
- package/lib/seam/thermostats/use-create-thermostat-climate-preset.d.ts +6 -0
- package/lib/seam/thermostats/use-create-thermostat-climate-preset.js +55 -0
- package/lib/seam/thermostats/use-create-thermostat-climate-preset.js.map +1 -0
- package/lib/seam/thermostats/use-delete-thermostat-climate-preset.d.ts +6 -0
- package/lib/seam/thermostats/use-delete-thermostat-climate-preset.js +44 -0
- package/lib/seam/thermostats/use-delete-thermostat-climate-preset.js.map +1 -0
- package/lib/seam/thermostats/use-update-thermostat-climate-preset.d.ts +6 -0
- package/lib/seam/thermostats/use-update-thermostat-climate-preset.js +55 -0
- package/lib/seam/thermostats/use-update-thermostat-climate-preset.js.map +1 -0
- package/lib/ui/Button.d.ts +3 -2
- package/lib/ui/Button.js +12 -4
- package/lib/ui/Button.js.map +1 -1
- package/lib/ui/IconButton.d.ts +5 -2
- package/lib/ui/IconButton.js +2 -2
- package/lib/ui/IconButton.js.map +1 -1
- package/lib/ui/Popover/Popover.d.ts +17 -0
- package/lib/ui/Popover/Popover.js +85 -0
- package/lib/ui/Popover/Popover.js.map +1 -0
- package/lib/ui/Popover/PopoverContentPrompt.d.ts +11 -0
- package/lib/ui/Popover/PopoverContentPrompt.js +12 -0
- package/lib/ui/Popover/PopoverContentPrompt.js.map +1 -0
- package/lib/ui/thermostat/ClimateModeMenu.d.ts +7 -2
- package/lib/ui/thermostat/ClimateModeMenu.js +7 -2
- package/lib/ui/thermostat/ClimateModeMenu.js.map +1 -1
- package/lib/ui/thermostat/ClimatePreset.d.ts +8 -0
- package/lib/ui/thermostat/ClimatePreset.js +146 -0
- package/lib/ui/thermostat/ClimatePreset.js.map +1 -0
- package/lib/ui/thermostat/ClimatePresets.d.ts +9 -0
- package/lib/ui/thermostat/ClimatePresets.js +76 -0
- package/lib/ui/thermostat/ClimatePresets.js.map +1 -0
- package/lib/ui/thermostat/FanModeMenu.d.ts +3 -1
- package/lib/ui/thermostat/FanModeMenu.js +5 -2
- package/lib/ui/thermostat/FanModeMenu.js.map +1 -1
- package/lib/ui/thermostat/ThermostatCard.d.ts +1 -0
- package/lib/ui/thermostat/ThermostatCard.js +4 -2
- package/lib/ui/thermostat/ThermostatCard.js.map +1 -1
- package/lib/ui/types.d.ts +3 -3
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +3 -2
- package/src/lib/errors.ts +13 -0
- package/src/lib/icons/Trash.tsx +28 -0
- package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +52 -1
- package/src/lib/seam/thermostats/thermostat-device.ts +6 -1
- package/src/lib/seam/thermostats/unit-conversion.ts +12 -2
- package/src/lib/seam/thermostats/use-create-thermostat-climate-preset.ts +101 -0
- package/src/lib/seam/thermostats/use-delete-thermostat-climate-preset.ts +84 -0
- package/src/lib/seam/thermostats/use-update-thermostat-climate-preset.ts +103 -0
- package/src/lib/ui/Button.tsx +20 -3
- package/src/lib/ui/IconButton.tsx +19 -2
- package/src/lib/ui/Popover/Popover.tsx +168 -0
- package/src/lib/ui/Popover/PopoverContentPrompt.tsx +58 -0
- package/src/lib/ui/thermostat/ClimateModeMenu.tsx +33 -1
- package/src/lib/ui/thermostat/ClimatePreset.tsx +401 -0
- package/src/lib/ui/thermostat/ClimatePresets.tsx +251 -0
- package/src/lib/ui/thermostat/FanModeMenu.tsx +20 -2
- package/src/lib/ui/thermostat/ThermostatCard.tsx +10 -4
- package/src/lib/ui/types.ts +3 -3
- package/src/lib/version.ts +1 -1
- package/src/styles/_buttons.scss +56 -2
- package/src/styles/_main.scss +2 -0
- package/src/styles/_popover.scss +46 -0
- package/src/styles/_spinner.scss +1 -1
- package/src/styles/_thermostat.scss +154 -2
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SeamHttpApiError,
|
|
3
|
+
ThermostatsCreateClimatePresetBody,
|
|
4
|
+
} from '@seamapi/http/connect'
|
|
5
|
+
import {
|
|
6
|
+
useMutation,
|
|
7
|
+
type UseMutationResult,
|
|
8
|
+
useQueryClient,
|
|
9
|
+
} from '@tanstack/react-query'
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ThermostatClimatePreset,
|
|
13
|
+
ThermostatDevice,
|
|
14
|
+
} from 'lib/seam/thermostats/thermostat-device.js'
|
|
15
|
+
import { fahrenheitToCelsius } from 'lib/seam/thermostats/unit-conversion.js'
|
|
16
|
+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
17
|
+
|
|
18
|
+
export type UseCreateThermostatClimatePresetParams = never
|
|
19
|
+
export type UseCreateThermostatClimatePresetData = undefined
|
|
20
|
+
|
|
21
|
+
export type UseCreateThermostatClimatePresetVariables =
|
|
22
|
+
ThermostatsCreateClimatePresetBody
|
|
23
|
+
|
|
24
|
+
export function useCreateThermostatClimatePreset(): UseMutationResult<
|
|
25
|
+
UseCreateThermostatClimatePresetData,
|
|
26
|
+
SeamHttpApiError,
|
|
27
|
+
UseCreateThermostatClimatePresetVariables
|
|
28
|
+
> {
|
|
29
|
+
const { client } = useSeamClient()
|
|
30
|
+
const queryClient = useQueryClient()
|
|
31
|
+
|
|
32
|
+
return useMutation<
|
|
33
|
+
UseCreateThermostatClimatePresetData,
|
|
34
|
+
SeamHttpApiError,
|
|
35
|
+
UseCreateThermostatClimatePresetVariables
|
|
36
|
+
>({
|
|
37
|
+
mutationFn: async (variables) => {
|
|
38
|
+
if (client === null) throw new NullSeamClientError()
|
|
39
|
+
await client.thermostats.createClimatePreset(variables)
|
|
40
|
+
},
|
|
41
|
+
onSuccess: (_data, variables) => {
|
|
42
|
+
queryClient.setQueryData<ThermostatDevice | null>(
|
|
43
|
+
['devices', 'get', { device_id: variables.device_id }],
|
|
44
|
+
(device) => {
|
|
45
|
+
if (device == null) {
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return getUpdatedDevice(device, variables)
|
|
50
|
+
}
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
queryClient.setQueryData<ThermostatDevice[]>(
|
|
54
|
+
['devices', 'list', { device_id: variables.device_id }],
|
|
55
|
+
(devices): ThermostatDevice[] => {
|
|
56
|
+
if (devices == null) {
|
|
57
|
+
return []
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return devices.map((device) => {
|
|
61
|
+
if (device.device_id === variables.device_id) {
|
|
62
|
+
return getUpdatedDevice(device, variables)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return device
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
)
|
|
69
|
+
},
|
|
70
|
+
})
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const getUpdatedDevice = (
|
|
74
|
+
device: ThermostatDevice,
|
|
75
|
+
variables: UseCreateThermostatClimatePresetVariables
|
|
76
|
+
): ThermostatDevice => {
|
|
77
|
+
const preset: ThermostatClimatePreset = {
|
|
78
|
+
...variables,
|
|
79
|
+
cooling_set_point_celsius: fahrenheitToCelsius(
|
|
80
|
+
variables.cooling_set_point_fahrenheit
|
|
81
|
+
),
|
|
82
|
+
heating_set_point_celsius: fahrenheitToCelsius(
|
|
83
|
+
variables.heating_set_point_fahrenheit
|
|
84
|
+
),
|
|
85
|
+
display_name: variables.name ?? variables.climate_preset_key,
|
|
86
|
+
can_delete: true,
|
|
87
|
+
can_edit: true,
|
|
88
|
+
manual_override_allowed: false,
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
...device,
|
|
93
|
+
properties: {
|
|
94
|
+
...device.properties,
|
|
95
|
+
available_climate_presets: [
|
|
96
|
+
preset,
|
|
97
|
+
...(device.properties.available_climate_presets ?? []),
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SeamHttpApiError,
|
|
3
|
+
ThermostatsDeleteClimatePresetBody,
|
|
4
|
+
} from '@seamapi/http/connect'
|
|
5
|
+
import {
|
|
6
|
+
useMutation,
|
|
7
|
+
type UseMutationResult,
|
|
8
|
+
useQueryClient,
|
|
9
|
+
} from '@tanstack/react-query'
|
|
10
|
+
|
|
11
|
+
import type { ThermostatDevice } from 'lib/seam/thermostats/thermostat-device.js'
|
|
12
|
+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
13
|
+
|
|
14
|
+
export type UseDeleteThermostatClimatePresetParams = never
|
|
15
|
+
|
|
16
|
+
export type UseDeleteThermostatClimatePresetData = undefined
|
|
17
|
+
|
|
18
|
+
export type UseDeleteThermostatClimatePresetVariables =
|
|
19
|
+
ThermostatsDeleteClimatePresetBody
|
|
20
|
+
|
|
21
|
+
export function useDeleteThermostatClimatePreset(): UseMutationResult<
|
|
22
|
+
UseDeleteThermostatClimatePresetData,
|
|
23
|
+
SeamHttpApiError,
|
|
24
|
+
UseDeleteThermostatClimatePresetVariables
|
|
25
|
+
> {
|
|
26
|
+
const { client } = useSeamClient()
|
|
27
|
+
const queryClient = useQueryClient()
|
|
28
|
+
|
|
29
|
+
return useMutation<
|
|
30
|
+
UseDeleteThermostatClimatePresetData,
|
|
31
|
+
SeamHttpApiError,
|
|
32
|
+
UseDeleteThermostatClimatePresetVariables
|
|
33
|
+
>({
|
|
34
|
+
mutationFn: async (variables) => {
|
|
35
|
+
if (client === null) throw new NullSeamClientError()
|
|
36
|
+
await client.thermostats.deleteClimatePreset(variables)
|
|
37
|
+
},
|
|
38
|
+
onSuccess: (_data, variables) => {
|
|
39
|
+
queryClient.setQueryData<ThermostatDevice | null>(
|
|
40
|
+
['devices', 'get', { device_id: variables.device_id }],
|
|
41
|
+
(device) => {
|
|
42
|
+
if (device == null) {
|
|
43
|
+
return
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return getUpdatedDevice(device, variables)
|
|
47
|
+
}
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
queryClient.setQueryData<ThermostatDevice[]>(
|
|
51
|
+
['devices', 'list', { device_id: variables.device_id }],
|
|
52
|
+
(devices): ThermostatDevice[] => {
|
|
53
|
+
if (devices == null) {
|
|
54
|
+
return []
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return devices.map((device) => {
|
|
58
|
+
if (device.device_id === variables.device_id) {
|
|
59
|
+
return getUpdatedDevice(device, variables)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return device
|
|
63
|
+
})
|
|
64
|
+
}
|
|
65
|
+
)
|
|
66
|
+
},
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function getUpdatedDevice(
|
|
71
|
+
device: ThermostatDevice,
|
|
72
|
+
variables: UseDeleteThermostatClimatePresetVariables
|
|
73
|
+
): ThermostatDevice {
|
|
74
|
+
return {
|
|
75
|
+
...device,
|
|
76
|
+
properties: {
|
|
77
|
+
...device.properties,
|
|
78
|
+
available_climate_presets:
|
|
79
|
+
device.properties.available_climate_presets.filter((preset) => {
|
|
80
|
+
return preset.climate_preset_key !== variables.climate_preset_key
|
|
81
|
+
}),
|
|
82
|
+
},
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SeamHttpApiError,
|
|
3
|
+
ThermostatsUpdateClimatePresetBody,
|
|
4
|
+
} from '@seamapi/http/connect'
|
|
5
|
+
import {
|
|
6
|
+
useMutation,
|
|
7
|
+
type UseMutationResult,
|
|
8
|
+
useQueryClient,
|
|
9
|
+
} from '@tanstack/react-query'
|
|
10
|
+
|
|
11
|
+
import type {
|
|
12
|
+
ThermostatClimatePreset,
|
|
13
|
+
ThermostatDevice,
|
|
14
|
+
} from 'lib/seam/thermostats/thermostat-device.js'
|
|
15
|
+
import { fahrenheitToCelsius } from 'lib/seam/thermostats/unit-conversion.js'
|
|
16
|
+
import { NullSeamClientError, useSeamClient } from 'lib/seam/use-seam-client.js'
|
|
17
|
+
|
|
18
|
+
export type UseUpdateThermostatClimatePresetParams = never
|
|
19
|
+
export type UseUpdateThermostatClimatePresetData = undefined
|
|
20
|
+
|
|
21
|
+
export type UseUpdateThermostatClimatePresetVariables = Omit<
|
|
22
|
+
ThermostatsUpdateClimatePresetBody,
|
|
23
|
+
'manual_override_allowed'
|
|
24
|
+
>
|
|
25
|
+
|
|
26
|
+
export function useUpdateThermostatClimatePreset(): UseMutationResult<
|
|
27
|
+
UseUpdateThermostatClimatePresetData,
|
|
28
|
+
SeamHttpApiError,
|
|
29
|
+
UseUpdateThermostatClimatePresetVariables
|
|
30
|
+
> {
|
|
31
|
+
const { client } = useSeamClient()
|
|
32
|
+
const queryClient = useQueryClient()
|
|
33
|
+
|
|
34
|
+
return useMutation<
|
|
35
|
+
UseUpdateThermostatClimatePresetData,
|
|
36
|
+
SeamHttpApiError,
|
|
37
|
+
UseUpdateThermostatClimatePresetVariables
|
|
38
|
+
>({
|
|
39
|
+
mutationFn: async (variables) => {
|
|
40
|
+
if (client === null) throw new NullSeamClientError()
|
|
41
|
+
await client.thermostats.createClimatePreset(variables)
|
|
42
|
+
},
|
|
43
|
+
onSuccess: (_data, variables) => {
|
|
44
|
+
queryClient.setQueryData<ThermostatDevice | null>(
|
|
45
|
+
['devices', 'get', { device_id: variables.device_id }],
|
|
46
|
+
(device) => {
|
|
47
|
+
if (device == null) {
|
|
48
|
+
return
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return getUpdatedDevice(device, variables)
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
queryClient.setQueryData<ThermostatDevice[]>(
|
|
56
|
+
['devices', 'list', { device_id: variables.device_id }],
|
|
57
|
+
(devices): ThermostatDevice[] => {
|
|
58
|
+
if (devices == null) {
|
|
59
|
+
return []
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return devices.map((device) => {
|
|
63
|
+
if (device.device_id === variables.device_id) {
|
|
64
|
+
return getUpdatedDevice(device, variables)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return device
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getUpdatedDevice(
|
|
76
|
+
device: ThermostatDevice,
|
|
77
|
+
variables: UseUpdateThermostatClimatePresetVariables
|
|
78
|
+
): ThermostatDevice {
|
|
79
|
+
const preset: ThermostatClimatePreset = {
|
|
80
|
+
...variables,
|
|
81
|
+
cooling_set_point_celsius: fahrenheitToCelsius(
|
|
82
|
+
variables.cooling_set_point_fahrenheit
|
|
83
|
+
),
|
|
84
|
+
heating_set_point_celsius: fahrenheitToCelsius(
|
|
85
|
+
variables.heating_set_point_fahrenheit
|
|
86
|
+
),
|
|
87
|
+
display_name: variables.name ?? variables.climate_preset_key,
|
|
88
|
+
can_delete: true,
|
|
89
|
+
can_edit: true,
|
|
90
|
+
manual_override_allowed: true,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return {
|
|
94
|
+
...device,
|
|
95
|
+
properties: {
|
|
96
|
+
...device.properties,
|
|
97
|
+
available_climate_presets: [
|
|
98
|
+
preset,
|
|
99
|
+
...(device.properties.available_climate_presets ?? []),
|
|
100
|
+
],
|
|
101
|
+
},
|
|
102
|
+
}
|
|
103
|
+
}
|
package/src/lib/ui/Button.tsx
CHANGED
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
2
|
import type { MouseEventHandler, PropsWithChildren } from 'react'
|
|
3
3
|
|
|
4
|
+
import { Spinner } from 'lib/ui/Spinner/Spinner.js'
|
|
5
|
+
|
|
4
6
|
interface ButtonProps extends PropsWithChildren {
|
|
5
|
-
variant?: 'solid' | 'outline' | 'neutral'
|
|
7
|
+
variant?: 'solid' | 'outline' | 'neutral' | 'danger'
|
|
6
8
|
size?: 'small' | 'medium' | 'large'
|
|
7
9
|
type?: 'button' | 'submit'
|
|
8
10
|
disabled?: boolean
|
|
9
11
|
onClick?: MouseEventHandler<HTMLButtonElement>
|
|
10
12
|
className?: string
|
|
11
13
|
onMouseDown?: MouseEventHandler<HTMLButtonElement>
|
|
14
|
+
loading?: boolean
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
export function Button({
|
|
@@ -20,6 +23,7 @@ export function Button({
|
|
|
20
23
|
className,
|
|
21
24
|
onMouseDown,
|
|
22
25
|
type = 'button',
|
|
26
|
+
loading = false,
|
|
23
27
|
}: ButtonProps): JSX.Element {
|
|
24
28
|
return (
|
|
25
29
|
<button
|
|
@@ -27,15 +31,28 @@ export function Button({
|
|
|
27
31
|
`seam-btn seam-btn-${variant} seam-btn-${size}`,
|
|
28
32
|
{
|
|
29
33
|
'seam-btn-disabled': disabled,
|
|
34
|
+
'seam-btn-loading': loading,
|
|
30
35
|
},
|
|
31
36
|
className
|
|
32
37
|
)}
|
|
33
38
|
disabled={disabled}
|
|
34
|
-
onClick={
|
|
39
|
+
onClick={(e) => {
|
|
40
|
+
if (loading || disabled) {
|
|
41
|
+
e.preventDefault()
|
|
42
|
+
return
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
onClick?.(e)
|
|
46
|
+
}}
|
|
35
47
|
onMouseDown={onMouseDown}
|
|
36
48
|
type={type}
|
|
37
49
|
>
|
|
38
|
-
{children}
|
|
50
|
+
<span className='seam-btn-content'>{children}</span>
|
|
51
|
+
{loading && (
|
|
52
|
+
<div className='seam-btn-loading'>
|
|
53
|
+
<Spinner size='small' />
|
|
54
|
+
</div>
|
|
55
|
+
)}
|
|
39
56
|
</button>
|
|
40
57
|
)
|
|
41
58
|
}
|
|
@@ -1,9 +1,26 @@
|
|
|
1
1
|
import classNames from 'classnames'
|
|
2
|
+
import type { Ref } from 'react'
|
|
2
3
|
|
|
3
4
|
import type { ButtonProps } from 'lib/ui/types.js'
|
|
4
5
|
|
|
5
|
-
export
|
|
6
|
+
export type IconProps = ButtonProps & {
|
|
7
|
+
elRef?: Ref<HTMLButtonElement>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function IconButton({
|
|
11
|
+
className,
|
|
12
|
+
elRef,
|
|
13
|
+
...props
|
|
14
|
+
}: IconProps): JSX.Element {
|
|
6
15
|
return (
|
|
7
|
-
<button
|
|
16
|
+
<button
|
|
17
|
+
{...props}
|
|
18
|
+
ref={elRef}
|
|
19
|
+
className={classNames(
|
|
20
|
+
'seam-icon-btn',
|
|
21
|
+
props.disabled === true && 'seam-icon-btn-disabled',
|
|
22
|
+
className
|
|
23
|
+
)}
|
|
24
|
+
/>
|
|
8
25
|
)
|
|
9
26
|
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import {
|
|
2
|
+
autoUpdate,
|
|
3
|
+
flip,
|
|
4
|
+
limitShift,
|
|
5
|
+
offset,
|
|
6
|
+
type ReferenceElement,
|
|
7
|
+
shift,
|
|
8
|
+
useFloating,
|
|
9
|
+
} from '@floating-ui/react'
|
|
10
|
+
import {
|
|
11
|
+
type ReactNode,
|
|
12
|
+
type Ref,
|
|
13
|
+
useCallback,
|
|
14
|
+
useEffect,
|
|
15
|
+
useImperativeHandle,
|
|
16
|
+
useMemo,
|
|
17
|
+
useRef,
|
|
18
|
+
useState,
|
|
19
|
+
} from 'react'
|
|
20
|
+
import { createPortal } from 'react-dom'
|
|
21
|
+
|
|
22
|
+
import { seamComponentsClassName } from 'lib/seam/SeamProvider.js'
|
|
23
|
+
|
|
24
|
+
export interface PopoverInstance {
|
|
25
|
+
show: () => void
|
|
26
|
+
hide: () => void
|
|
27
|
+
toggle: () => void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type PopoverChildren = (
|
|
31
|
+
params: {
|
|
32
|
+
setRef: (ref: HTMLElement | undefined | null) => void
|
|
33
|
+
} & PopoverInstance
|
|
34
|
+
) => ReactNode
|
|
35
|
+
|
|
36
|
+
export interface PopoverProps {
|
|
37
|
+
children: PopoverChildren
|
|
38
|
+
content: ReactNode | ((instance: PopoverInstance) => ReactNode)
|
|
39
|
+
instanceRef?: Ref<PopoverInstance>
|
|
40
|
+
preventCloseOnClickOutside?: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function Popover(props: PopoverProps): JSX.Element {
|
|
44
|
+
const { children, content, instanceRef, preventCloseOnClickOutside } = props
|
|
45
|
+
|
|
46
|
+
const [open, setOpen] = useState(false)
|
|
47
|
+
|
|
48
|
+
const { refs, floatingStyles } = useFloating({
|
|
49
|
+
whileElementsMounted: autoUpdate,
|
|
50
|
+
transform: false,
|
|
51
|
+
open,
|
|
52
|
+
onOpenChange: setOpen,
|
|
53
|
+
placement: 'bottom',
|
|
54
|
+
middleware: [
|
|
55
|
+
shift({
|
|
56
|
+
crossAxis: true,
|
|
57
|
+
limiter: limitShift(),
|
|
58
|
+
}),
|
|
59
|
+
flip(),
|
|
60
|
+
offset(5),
|
|
61
|
+
],
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const referenceEl = useRef<HTMLElement | null>()
|
|
65
|
+
const floatingEl = useRef<HTMLElement | null>()
|
|
66
|
+
|
|
67
|
+
const setFLoating = useCallback(
|
|
68
|
+
(ref: HTMLElement | null): void => {
|
|
69
|
+
refs.setFloating(ref)
|
|
70
|
+
floatingEl.current = ref
|
|
71
|
+
},
|
|
72
|
+
[refs, floatingEl]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
const toggle = useCallback(() => {
|
|
76
|
+
setOpen((value) => !value)
|
|
77
|
+
}, [])
|
|
78
|
+
|
|
79
|
+
const instance = useMemo(
|
|
80
|
+
() => ({
|
|
81
|
+
show: () => {
|
|
82
|
+
setOpen(true)
|
|
83
|
+
},
|
|
84
|
+
hide: () => {
|
|
85
|
+
setOpen(false)
|
|
86
|
+
},
|
|
87
|
+
toggle,
|
|
88
|
+
}),
|
|
89
|
+
[toggle]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
const setReference = useCallback(
|
|
93
|
+
(ref: ReferenceElement | undefined | null): void => {
|
|
94
|
+
if (!(ref instanceof HTMLElement) || referenceEl.current === ref) return
|
|
95
|
+
|
|
96
|
+
if (referenceEl.current != null) {
|
|
97
|
+
referenceEl.current.removeEventListener('click', toggle)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
refs.setReference(ref)
|
|
101
|
+
ref.addEventListener('click', toggle)
|
|
102
|
+
referenceEl.current = ref
|
|
103
|
+
},
|
|
104
|
+
[toggle, refs]
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
useImperativeHandle(instanceRef, () => instance)
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Closes the popover when the user clicks outside of it.
|
|
111
|
+
*/
|
|
112
|
+
const windowClickHandler = useCallback((e: MouseEvent): void => {
|
|
113
|
+
const target = e.target as HTMLElement
|
|
114
|
+
|
|
115
|
+
// If the target is the reference element, do nothing.
|
|
116
|
+
if (
|
|
117
|
+
referenceEl.current === target ||
|
|
118
|
+
referenceEl.current?.contains(target) === true
|
|
119
|
+
) {
|
|
120
|
+
return
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const closest = target.closest('[data-seam-popover]')
|
|
124
|
+
|
|
125
|
+
// Prevents closing if target is floating element, also adds support for nested popovers somehow :)
|
|
126
|
+
if (
|
|
127
|
+
closest != null &&
|
|
128
|
+
referenceEl.current != null &&
|
|
129
|
+
!closest.contains(referenceEl.current)
|
|
130
|
+
) {
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
setOpen(false)
|
|
135
|
+
}, [])
|
|
136
|
+
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
setTimeout(() => {
|
|
139
|
+
if (preventCloseOnClickOutside === false) return
|
|
140
|
+
|
|
141
|
+
globalThis.addEventListener('click', windowClickHandler)
|
|
142
|
+
}, 0)
|
|
143
|
+
|
|
144
|
+
return () => {
|
|
145
|
+
globalThis.removeEventListener('click', windowClickHandler)
|
|
146
|
+
}
|
|
147
|
+
}, [windowClickHandler, preventCloseOnClickOutside])
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<>
|
|
151
|
+
{children({ setRef: setReference, ...instance })}
|
|
152
|
+
{open &&
|
|
153
|
+
createPortal(
|
|
154
|
+
<div
|
|
155
|
+
className={seamComponentsClassName}
|
|
156
|
+
data-seam-popover=''
|
|
157
|
+
ref={setFLoating}
|
|
158
|
+
style={floatingStyles}
|
|
159
|
+
>
|
|
160
|
+
<div className='seam-popover'>
|
|
161
|
+
{typeof content === 'function' ? content(instance) : content}
|
|
162
|
+
</div>
|
|
163
|
+
</div>,
|
|
164
|
+
globalThis.document.body
|
|
165
|
+
)}
|
|
166
|
+
</>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { Button } from 'lib/ui/Button.js'
|
|
2
|
+
|
|
3
|
+
export interface PopoverContentPromptProps {
|
|
4
|
+
onConfirm?: () => void
|
|
5
|
+
onCancel?: () => void
|
|
6
|
+
prompt?: string
|
|
7
|
+
description?: string
|
|
8
|
+
confirmText?: string
|
|
9
|
+
cancelText?: string
|
|
10
|
+
confirmLoading?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function PopoverContentPrompt(
|
|
14
|
+
props: PopoverContentPromptProps
|
|
15
|
+
): JSX.Element {
|
|
16
|
+
const {
|
|
17
|
+
confirmText = t.confirm,
|
|
18
|
+
cancelText = t.cancel,
|
|
19
|
+
confirmLoading = false,
|
|
20
|
+
prompt = t.areYouSure,
|
|
21
|
+
description,
|
|
22
|
+
onConfirm,
|
|
23
|
+
onCancel,
|
|
24
|
+
} = props
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className='seam-popover-content-prompt'>
|
|
28
|
+
<div>
|
|
29
|
+
<div className='seam-popover-content-prompt-text'>{prompt}</div>
|
|
30
|
+
{description != null && (
|
|
31
|
+
<div className='seam-popover-content-prompt-description'>
|
|
32
|
+
{description}
|
|
33
|
+
</div>
|
|
34
|
+
)}
|
|
35
|
+
</div>
|
|
36
|
+
<div className='seam-popover-content-prompt-buttons'>
|
|
37
|
+
<Button
|
|
38
|
+
variant='solid'
|
|
39
|
+
onClick={onConfirm}
|
|
40
|
+
loading={confirmLoading}
|
|
41
|
+
size='small'
|
|
42
|
+
>
|
|
43
|
+
{confirmText}
|
|
44
|
+
</Button>
|
|
45
|
+
|
|
46
|
+
<Button variant='danger' size='small' onClick={onCancel}>
|
|
47
|
+
{cancelText}
|
|
48
|
+
</Button>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const t = {
|
|
55
|
+
confirm: 'Confirm',
|
|
56
|
+
cancel: 'Cancel',
|
|
57
|
+
areYouSure: 'Are you sure?',
|
|
58
|
+
}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import classNames from 'classnames'
|
|
2
|
+
import type { CSSProperties } from 'react'
|
|
3
|
+
|
|
1
4
|
import { ChevronDownIcon } from 'lib/icons/ChevronDown.js'
|
|
2
5
|
import { OffIcon } from 'lib/icons/Off.js'
|
|
3
6
|
import { ThermostatCoolIcon } from 'lib/icons/ThermostatCool.js'
|
|
@@ -11,20 +14,49 @@ interface ClimateModeMenuProps {
|
|
|
11
14
|
mode: HvacModeSetting
|
|
12
15
|
onChange: (mode: HvacModeSetting) => void
|
|
13
16
|
supportedModes?: HvacModeSetting[]
|
|
17
|
+
buttonTextVisible?: boolean
|
|
18
|
+
className?: string
|
|
19
|
+
style?: CSSProperties
|
|
20
|
+
block?: boolean
|
|
21
|
+
size?: 'regular' | 'large'
|
|
14
22
|
}
|
|
15
23
|
|
|
16
24
|
export function ClimateModeMenu({
|
|
17
25
|
mode,
|
|
18
26
|
onChange,
|
|
19
27
|
supportedModes = ['heat', 'cool', 'heat_cool', 'off'],
|
|
28
|
+
buttonTextVisible = false,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
block,
|
|
32
|
+
size = 'regular',
|
|
20
33
|
}: ClimateModeMenuProps): JSX.Element {
|
|
21
34
|
return (
|
|
22
35
|
<Menu
|
|
23
36
|
renderButton={({ onOpen }) => (
|
|
24
|
-
<button
|
|
37
|
+
<button
|
|
38
|
+
style={style}
|
|
39
|
+
onClick={onOpen}
|
|
40
|
+
className={classNames(
|
|
41
|
+
'seam-climate-mode-menu-button',
|
|
42
|
+
{
|
|
43
|
+
'seam-climate-mode-menu-button-block': block,
|
|
44
|
+
'seam-climate-mode-menu-button-regular': size === 'regular',
|
|
45
|
+
'seam-climate-mode-menu-button-large': size === 'large',
|
|
46
|
+
},
|
|
47
|
+
className
|
|
48
|
+
)}
|
|
49
|
+
>
|
|
25
50
|
<div className='seam-climate-mode-menu-button-icon'>
|
|
26
51
|
<ModeIcon mode={mode} />
|
|
27
52
|
</div>
|
|
53
|
+
|
|
54
|
+
{buttonTextVisible && (
|
|
55
|
+
<span className='seam-climate-mode-menu-button-text'>
|
|
56
|
+
{t[mode]}
|
|
57
|
+
</span>
|
|
58
|
+
)}
|
|
59
|
+
|
|
28
60
|
<ChevronDownIcon className='seam-climate-mode-menu-button-chevron' />
|
|
29
61
|
</button>
|
|
30
62
|
)}
|