@seamapi/react 4.6.0 → 4.7.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 (80) hide show
  1. package/README.md +2 -2
  2. package/dist/elements.js +11530 -9343
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +255 -3
  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/Trash.d.ts +2 -0
  9. package/lib/icons/Trash.js +5 -0
  10. package/lib/icons/Trash.js.map +1 -0
  11. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +17 -1
  12. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
  13. package/lib/seam/thermostats/thermostat-device.d.ts +2 -1
  14. package/lib/seam/thermostats/thermostat-device.js.map +1 -1
  15. package/lib/seam/thermostats/unit-conversion.d.ts +5 -2
  16. package/lib/seam/thermostats/unit-conversion.js +5 -2
  17. package/lib/seam/thermostats/unit-conversion.js.map +1 -1
  18. package/lib/seam/thermostats/use-create-thermostat-climate-preset.d.ts +6 -0
  19. package/lib/seam/thermostats/use-create-thermostat-climate-preset.js +55 -0
  20. package/lib/seam/thermostats/use-create-thermostat-climate-preset.js.map +1 -0
  21. package/lib/seam/thermostats/use-delete-thermostat-climate-preset.d.ts +6 -0
  22. package/lib/seam/thermostats/use-delete-thermostat-climate-preset.js +44 -0
  23. package/lib/seam/thermostats/use-delete-thermostat-climate-preset.js.map +1 -0
  24. package/lib/seam/thermostats/use-update-thermostat-climate-preset.d.ts +6 -0
  25. package/lib/seam/thermostats/use-update-thermostat-climate-preset.js +55 -0
  26. package/lib/seam/thermostats/use-update-thermostat-climate-preset.js.map +1 -0
  27. package/lib/ui/Button.d.ts +3 -2
  28. package/lib/ui/Button.js +12 -4
  29. package/lib/ui/Button.js.map +1 -1
  30. package/lib/ui/IconButton.d.ts +5 -2
  31. package/lib/ui/IconButton.js +2 -2
  32. package/lib/ui/IconButton.js.map +1 -1
  33. package/lib/ui/Popover/Popover.d.ts +17 -0
  34. package/lib/ui/Popover/Popover.js +85 -0
  35. package/lib/ui/Popover/Popover.js.map +1 -0
  36. package/lib/ui/Popover/PopoverContentPrompt.d.ts +11 -0
  37. package/lib/ui/Popover/PopoverContentPrompt.js +12 -0
  38. package/lib/ui/Popover/PopoverContentPrompt.js.map +1 -0
  39. package/lib/ui/thermostat/ClimateModeMenu.d.ts +7 -2
  40. package/lib/ui/thermostat/ClimateModeMenu.js +7 -2
  41. package/lib/ui/thermostat/ClimateModeMenu.js.map +1 -1
  42. package/lib/ui/thermostat/ClimatePreset.d.ts +8 -0
  43. package/lib/ui/thermostat/ClimatePreset.js +141 -0
  44. package/lib/ui/thermostat/ClimatePreset.js.map +1 -0
  45. package/lib/ui/thermostat/ClimatePresets.d.ts +9 -0
  46. package/lib/ui/thermostat/ClimatePresets.js +72 -0
  47. package/lib/ui/thermostat/ClimatePresets.js.map +1 -0
  48. package/lib/ui/thermostat/FanModeMenu.d.ts +3 -1
  49. package/lib/ui/thermostat/FanModeMenu.js +5 -2
  50. package/lib/ui/thermostat/FanModeMenu.js.map +1 -1
  51. package/lib/ui/thermostat/ThermostatCard.d.ts +1 -0
  52. package/lib/ui/thermostat/ThermostatCard.js +4 -2
  53. package/lib/ui/thermostat/ThermostatCard.js.map +1 -1
  54. package/lib/ui/types.d.ts +3 -3
  55. package/lib/version.d.ts +1 -1
  56. package/lib/version.js +1 -1
  57. package/package.json +3 -2
  58. package/src/lib/icons/Trash.tsx +28 -0
  59. package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +52 -1
  60. package/src/lib/seam/thermostats/thermostat-device.ts +4 -0
  61. package/src/lib/seam/thermostats/unit-conversion.ts +12 -2
  62. package/src/lib/seam/thermostats/use-create-thermostat-climate-preset.ts +101 -0
  63. package/src/lib/seam/thermostats/use-delete-thermostat-climate-preset.ts +84 -0
  64. package/src/lib/seam/thermostats/use-update-thermostat-climate-preset.ts +103 -0
  65. package/src/lib/ui/Button.tsx +20 -3
  66. package/src/lib/ui/IconButton.tsx +19 -2
  67. package/src/lib/ui/Popover/Popover.tsx +168 -0
  68. package/src/lib/ui/Popover/PopoverContentPrompt.tsx +58 -0
  69. package/src/lib/ui/thermostat/ClimateModeMenu.tsx +33 -1
  70. package/src/lib/ui/thermostat/ClimatePreset.tsx +373 -0
  71. package/src/lib/ui/thermostat/ClimatePresets.tsx +235 -0
  72. package/src/lib/ui/thermostat/FanModeMenu.tsx +20 -2
  73. package/src/lib/ui/thermostat/ThermostatCard.tsx +10 -4
  74. package/src/lib/ui/types.ts +3 -3
  75. package/src/lib/version.ts +1 -1
  76. package/src/styles/_buttons.scss +56 -2
  77. package/src/styles/_main.scss +2 -0
  78. package/src/styles/_popover.scss +46 -0
  79. package/src/styles/_spinner.scss +1 -1
  80. package/src/styles/_thermostat.scss +154 -2
@@ -0,0 +1,373 @@
1
+ import classNames from 'classnames'
2
+ import {
3
+ type HTMLAttributes,
4
+ type Ref,
5
+ useCallback,
6
+ useImperativeHandle,
7
+ useMemo,
8
+ } from 'react'
9
+ import { Controller, useForm, type UseFormReturn } from 'react-hook-form'
10
+
11
+ import type {
12
+ FanModeSetting,
13
+ HvacModeSetting,
14
+ ThermostatClimatePreset,
15
+ ThermostatDevice,
16
+ } from 'lib/seam/thermostats/thermostat-device.js'
17
+ import { fahrenheitToCelsius } from 'lib/seam/thermostats/unit-conversion.js'
18
+ import { useCreateThermostatClimatePreset } from 'lib/seam/thermostats/use-create-thermostat-climate-preset.js'
19
+ import { useUpdateThermostatClimatePreset } from 'lib/seam/thermostats/use-update-thermostat-climate-preset.js'
20
+ import { Button } from 'lib/ui/Button.js'
21
+ import { FormField } from 'lib/ui/FormField.js'
22
+ import { InputLabel } from 'lib/ui/InputLabel.js'
23
+ import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
24
+ import { TextField } from 'lib/ui/TextField/TextField.js'
25
+ import { ClimateModeMenu } from 'lib/ui/thermostat/ClimateModeMenu.js'
26
+ import { FanModeMenu } from 'lib/ui/thermostat/FanModeMenu.js'
27
+ import { TemperatureControlGroup } from 'lib/ui/thermostat/TemperatureControlGroup.js'
28
+
29
+ export type ClimatePresetProps = {
30
+ preset?: ThermostatClimatePreset
31
+ onBack: () => void
32
+ device: ThermostatDevice
33
+ } & Omit<HTMLAttributes<HTMLDivElement>, 'children'>
34
+
35
+ export function ClimatePreset(props: ClimatePresetProps): JSX.Element {
36
+ const { preset, onBack, device, ...attrs } = props
37
+
38
+ return (
39
+ <div
40
+ {...attrs}
41
+ className={classNames('seam-thermostat-climate-preset', attrs.className)}
42
+ >
43
+ <ContentHeader
44
+ title={preset == null ? t.crateNewPreset : preset.display_name}
45
+ onBack={onBack}
46
+ />
47
+ {preset == null ? (
48
+ <CreateForm device={device} onComplete={onBack} />
49
+ ) : (
50
+ <UpdateForm device={device} onComplete={onBack} preset={preset} />
51
+ )}
52
+ </div>
53
+ )
54
+ }
55
+
56
+ interface PresetFormProps {
57
+ defaultValues: {
58
+ key: string
59
+ name: string
60
+ hvacMode: HvacModeSetting | undefined
61
+ heatPoint: number | undefined
62
+ coolPoint: number | undefined
63
+ fanMode: FanModeSetting | undefined
64
+ }
65
+ onSubmit: (values: PresetFormProps['defaultValues']) => void
66
+ device: ThermostatDevice
67
+ loading: boolean
68
+ instanceRef?: Ref<UseFormReturn<PresetFormProps['defaultValues']> | undefined>
69
+ withKeyField?: boolean
70
+ }
71
+
72
+ function PresetForm(props: PresetFormProps): JSX.Element {
73
+ const {
74
+ defaultValues,
75
+ device,
76
+ instanceRef,
77
+ loading,
78
+ onSubmit,
79
+ withKeyField,
80
+ } = props
81
+ const form = useForm({ defaultValues })
82
+
83
+ useImperativeHandle(instanceRef, () => form)
84
+
85
+ const {
86
+ register,
87
+ handleSubmit,
88
+ formState: { errors },
89
+ watch,
90
+ setValue,
91
+ control,
92
+ } = form
93
+
94
+ const state = watch()
95
+
96
+ const onHvacModeChange = (mode: HvacModeSetting): void => {
97
+ if (mode === 'heat_cool') {
98
+ setValue('heatPoint', defaultValues.heatPoint)
99
+ setValue('coolPoint', defaultValues.coolPoint)
100
+ } else if (mode === 'heat') {
101
+ setValue('heatPoint', defaultValues.heatPoint)
102
+ setValue('coolPoint', undefined)
103
+ } else if (mode === 'cool') {
104
+ setValue('heatPoint', undefined)
105
+ setValue('coolPoint', defaultValues.coolPoint)
106
+ } else {
107
+ setValue('heatPoint', undefined)
108
+ setValue('coolPoint', undefined)
109
+ }
110
+ }
111
+
112
+ const otherClimatePresets = useMemo(() => {
113
+ if (withKeyField !== true) return []
114
+
115
+ return (device.properties.available_climate_presets ?? []).filter(
116
+ (other) => other.climate_preset_key !== defaultValues.key
117
+ )
118
+ }, [defaultValues, device, withKeyField])
119
+
120
+ const onValid = useCallback(() => {
121
+ onSubmit(state)
122
+ }, [onSubmit, state])
123
+
124
+ return (
125
+ <div className='seam-main'>
126
+ <form
127
+ onSubmit={(e) => {
128
+ void handleSubmit(onValid)(e)
129
+ }}
130
+ >
131
+ {withKeyField === true && (
132
+ <FormField>
133
+ <InputLabel>Key</InputLabel>
134
+ <TextField
135
+ size='large'
136
+ clearable
137
+ hasError={errors.key != null}
138
+ helperText={errors.key?.message}
139
+ inputProps={{
140
+ ...register('key', {
141
+ required: 'required',
142
+ setValueAs: (value) => value.trim(),
143
+ validate(value) {
144
+ if (value.includes(' ')) {
145
+ return t.keyCannotContainSpaces
146
+ }
147
+
148
+ const exists = otherClimatePresets.some(
149
+ (other) => other.climate_preset_key === value
150
+ )
151
+
152
+ if (exists) {
153
+ return t.keyAlreadyExists
154
+ }
155
+
156
+ return true
157
+ },
158
+ }),
159
+ }}
160
+ />
161
+ </FormField>
162
+ )}
163
+
164
+ <FormField>
165
+ <InputLabel>{t.nameField}</InputLabel>
166
+ <TextField
167
+ size='large'
168
+ clearable
169
+ hasError={errors.name != null}
170
+ helperText={errors.name?.message}
171
+ inputProps={register('name', {
172
+ required: false,
173
+ setValueAs: (value) => value.trim(),
174
+ })}
175
+ />
176
+ </FormField>
177
+
178
+ {state.fanMode != null && (
179
+ <FormField>
180
+ <InputLabel>{t.fanModeField}</InputLabel>
181
+ <Controller
182
+ control={control}
183
+ name='fanMode'
184
+ render={({ field: { onChange, value } }) =>
185
+ value != null ? (
186
+ <FanModeMenu
187
+ block
188
+ size='large'
189
+ mode={value}
190
+ onChange={onChange}
191
+ />
192
+ ) : (
193
+ <></>
194
+ )
195
+ }
196
+ />
197
+ </FormField>
198
+ )}
199
+
200
+ {state.hvacMode != null && (
201
+ <FormField>
202
+ <InputLabel>{t.hvacModeField}</InputLabel>
203
+ <Controller
204
+ control={control}
205
+ name='hvacMode'
206
+ render={({ field: { onChange, value } }) =>
207
+ value == null ? (
208
+ <></>
209
+ ) : (
210
+ <ClimateModeMenu
211
+ block
212
+ size='large'
213
+ buttonTextVisible
214
+ mode={value}
215
+ onChange={(value) => {
216
+ onHvacModeChange(value)
217
+ onChange(value)
218
+ }}
219
+ />
220
+ )
221
+ }
222
+ />
223
+ </FormField>
224
+ )}
225
+
226
+ {state.hvacMode !== 'off' && state.hvacMode != null && (
227
+ <FormField>
228
+ <InputLabel>{t.heatCoolField}</InputLabel>
229
+ <TemperatureControlGroup
230
+ mode={state.hvacMode}
231
+ onHeatValueChange={(value) => {
232
+ setValue('heatPoint', value)
233
+ }}
234
+ onCoolValueChange={(value) => {
235
+ setValue('coolPoint', value)
236
+ }}
237
+ heatValue={state.heatPoint ?? 0}
238
+ coolValue={state.coolPoint ?? 0}
239
+ minHeat={device.properties.min_heating_cooling_delta_fahrenheit}
240
+ maxHeat={device.properties.max_heating_set_point_fahrenheit}
241
+ minCool={device.properties.min_cooling_set_point_fahrenheit}
242
+ maxCool={device.properties.max_cooling_set_point_fahrenheit}
243
+ delta={device.properties.min_heating_cooling_delta_fahrenheit}
244
+ />
245
+ </FormField>
246
+ )}
247
+
248
+ <div className='seam-climate-preset-buttons'>
249
+ <Button
250
+ type='submit'
251
+ variant='solid'
252
+ disabled={loading}
253
+ loading={loading}
254
+ >
255
+ {t.save}
256
+ </Button>
257
+ </div>
258
+ </form>
259
+ </div>
260
+ )
261
+ }
262
+
263
+ interface CreateFormProps {
264
+ device: ThermostatDevice
265
+ onComplete: () => void
266
+ }
267
+
268
+ function CreateForm({ device, onComplete }: CreateFormProps): JSX.Element {
269
+ const mutation = useCreateThermostatClimatePreset()
270
+
271
+ const onSubmit = useCallback(
272
+ (values: PresetFormProps['defaultValues']) => {
273
+ mutation.mutate(
274
+ {
275
+ climate_preset_key: values.key,
276
+ device_id: device.device_id,
277
+ name: values.name === '' ? undefined : values.name,
278
+ cooling_set_point_fahrenheit: values.coolPoint,
279
+ heating_set_point_fahrenheit: values.heatPoint,
280
+ fan_mode_setting: values.fanMode,
281
+ cooling_set_point_celsius: fahrenheitToCelsius(values.coolPoint),
282
+ heating_set_point_celsius: fahrenheitToCelsius(values.heatPoint),
283
+ hvac_mode_setting: values.hvacMode,
284
+ },
285
+ { onSuccess: onComplete }
286
+ )
287
+ },
288
+ [device, mutation, onComplete]
289
+ )
290
+
291
+ return (
292
+ <PresetForm
293
+ defaultValues={{
294
+ key: '',
295
+ coolPoint: 60,
296
+ heatPoint: 80,
297
+ name: '',
298
+ hvacMode: 'off',
299
+ fanMode: 'auto',
300
+ }}
301
+ device={device}
302
+ loading={mutation.isPending}
303
+ onSubmit={onSubmit}
304
+ withKeyField
305
+ />
306
+ )
307
+ }
308
+
309
+ interface UpdateFormProps {
310
+ device: ThermostatDevice
311
+ onComplete: () => void
312
+ preset: ThermostatClimatePreset
313
+ }
314
+
315
+ function UpdateForm({
316
+ device,
317
+ onComplete,
318
+ preset,
319
+ }: UpdateFormProps): JSX.Element {
320
+ const mutation = useUpdateThermostatClimatePreset()
321
+ const defaultValues = useMemo<PresetFormProps['defaultValues']>(
322
+ () => ({
323
+ coolPoint: preset.cooling_set_point_fahrenheit ?? 60,
324
+ heatPoint: preset.heating_set_point_fahrenheit ?? 80,
325
+ name: preset.display_name,
326
+ hvacMode: preset.hvac_mode_setting,
327
+ fanMode: preset.fan_mode_setting,
328
+ key: preset.climate_preset_key,
329
+ }),
330
+ [preset]
331
+ )
332
+
333
+ const onSubmit = useCallback(
334
+ (values: PresetFormProps['defaultValues']) => {
335
+ mutation.mutate(
336
+ {
337
+ climate_preset_key: values.key,
338
+ device_id: device.device_id,
339
+ name: values.name === '' ? undefined : values.name,
340
+ cooling_set_point_fahrenheit: values.coolPoint,
341
+ heating_set_point_fahrenheit: values.heatPoint,
342
+ fan_mode_setting: values.fanMode,
343
+ cooling_set_point_celsius: fahrenheitToCelsius(values.coolPoint),
344
+ heating_set_point_celsius: fahrenheitToCelsius(values.heatPoint),
345
+ hvac_mode_setting: values.hvacMode,
346
+ },
347
+ { onSuccess: onComplete }
348
+ )
349
+ },
350
+ [device, mutation, onComplete]
351
+ )
352
+
353
+ return (
354
+ <PresetForm
355
+ defaultValues={defaultValues}
356
+ device={device}
357
+ loading={mutation.isPending}
358
+ onSubmit={onSubmit}
359
+ />
360
+ )
361
+ }
362
+
363
+ const t = {
364
+ keyAlreadyExists: 'Climate Preset with this key already exists.',
365
+ keyCannotContainSpaces: 'Climate Preset key cannot contain spaces.',
366
+ nameField: 'Display Name (Optional)',
367
+ fanModeField: 'Fan Mode',
368
+ hvacModeField: 'HVAC Mode',
369
+ heatCoolField: 'Heat / Cool',
370
+ delete: 'Delete',
371
+ save: 'Save',
372
+ crateNewPreset: 'Create New Climate Preset',
373
+ }
@@ -0,0 +1,235 @@
1
+ import classNames from 'classnames'
2
+ import { type HTMLAttributes, type ReactNode, useState } from 'react'
3
+
4
+ import { AddIcon } from 'lib/icons/Add.js'
5
+ import { EditIcon } from 'lib/icons/Edit.js'
6
+ import { FanIcon } from 'lib/icons/Fan.js'
7
+ import { ThermostatCoolIcon } from 'lib/icons/ThermostatCool.js'
8
+ import { ThermostatHeatIcon } from 'lib/icons/ThermostatHeat.js'
9
+ import { TrashIcon } from 'lib/icons/Trash.js'
10
+ import type {
11
+ ThermostatClimatePreset,
12
+ ThermostatDevice,
13
+ } from 'lib/seam/thermostats/thermostat-device.js'
14
+ import { getTemperatureUnitSymbol } from 'lib/seam/thermostats/unit-conversion.js'
15
+ import { useDeleteThermostatClimatePreset } from 'lib/seam/thermostats/use-delete-thermostat-climate-preset.js'
16
+ import { Button } from 'lib/ui/Button.js'
17
+ import { IconButton } from 'lib/ui/IconButton.js'
18
+ import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
19
+ import { Popover } from 'lib/ui/Popover/Popover.js'
20
+ import { PopoverContentPrompt } from 'lib/ui/Popover/PopoverContentPrompt.js'
21
+ import { Spinner } from 'lib/ui/Spinner/Spinner.js'
22
+ import { ClimatePreset } from 'lib/ui/thermostat/ClimatePreset.js'
23
+
24
+ interface ClimatePresetsManagement {
25
+ device: ThermostatDevice
26
+ onBack: () => void
27
+ temperatureUnit: 'fahrenheit' | 'celsius'
28
+ }
29
+
30
+ const CreateNewPresetSymbol = Symbol('CreateNewPreset')
31
+
32
+ export function ClimatePresets(props: ClimatePresetsManagement): JSX.Element {
33
+ const { device, onBack } = props
34
+
35
+ const [selectedClimatePreset, setSelectedClimatePreset] = useState<
36
+ ThermostatClimatePreset | typeof CreateNewPresetSymbol | null
37
+ >(null)
38
+
39
+ const [
40
+ climatePresetKeySelectedForDeletion,
41
+ setClimatePresetKeySelectedForDeletion,
42
+ ] = useState<ThermostatClimatePreset['climate_preset_key'] | null>(null)
43
+ const deleteMutation = useDeleteThermostatClimatePreset()
44
+
45
+ if (
46
+ selectedClimatePreset != null ||
47
+ selectedClimatePreset === CreateNewPresetSymbol
48
+ ) {
49
+ return (
50
+ <ClimatePreset
51
+ onBack={() => {
52
+ setSelectedClimatePreset(null)
53
+ }}
54
+ device={device}
55
+ preset={
56
+ selectedClimatePreset === CreateNewPresetSymbol
57
+ ? undefined
58
+ : selectedClimatePreset
59
+ }
60
+ />
61
+ )
62
+ }
63
+
64
+ return (
65
+ <div className='seam-thermostat-climate-presets'>
66
+ <ContentHeader title={t.title} onBack={onBack} />
67
+ <div className='seam-thermostat-climate-presets-body'>
68
+ <Button
69
+ onClick={() => {
70
+ setSelectedClimatePreset(CreateNewPresetSymbol)
71
+ }}
72
+ className='seam-climate-presets-add-button'
73
+ >
74
+ <AddIcon />
75
+ {t.createNew}
76
+ </Button>
77
+
78
+ <div className='seam-thermostat-climate-presets-cards'>
79
+ {device.properties.available_climate_presets.map((preset) => (
80
+ <PresetCard
81
+ onClickEdit={() => {
82
+ setSelectedClimatePreset(preset)
83
+ }}
84
+ onClickDelete={() => {
85
+ setClimatePresetKeySelectedForDeletion(
86
+ preset.climate_preset_key
87
+ )
88
+ deleteMutation.mutate({
89
+ climate_preset_key: preset.climate_preset_key,
90
+ device_id: device.device_id,
91
+ })
92
+ }}
93
+ temperatureUnit={props.temperatureUnit}
94
+ preset={preset}
95
+ key={preset.climate_preset_key}
96
+ deletionLoading={
97
+ deleteMutation.isPending &&
98
+ climatePresetKeySelectedForDeletion ===
99
+ preset.climate_preset_key
100
+ }
101
+ disabled={
102
+ deleteMutation.isPending &&
103
+ climatePresetKeySelectedForDeletion !==
104
+ preset.climate_preset_key
105
+ }
106
+ />
107
+ ))}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ )
112
+ }
113
+
114
+ function PresetCard(
115
+ props: HTMLAttributes<HTMLDivElement> & {
116
+ preset: ThermostatClimatePreset
117
+ temperatureUnit: 'fahrenheit' | 'celsius'
118
+ onClickEdit: () => void
119
+ onClickDelete: () => void
120
+ deletionLoading?: boolean
121
+ disabled?: boolean
122
+ }
123
+ ): JSX.Element {
124
+ const {
125
+ preset,
126
+ temperatureUnit,
127
+ onClickEdit,
128
+ onClickDelete,
129
+ deletionLoading = false,
130
+ disabled = false,
131
+ ...attrs
132
+ } = props
133
+
134
+ const heatPoint =
135
+ temperatureUnit === 'fahrenheit'
136
+ ? preset.heating_set_point_fahrenheit
137
+ : (preset.heating_set_point_celsius ?? undefined)
138
+
139
+ const coolPoint =
140
+ temperatureUnit === 'fahrenheit'
141
+ ? preset.cooling_set_point_fahrenheit
142
+ : (preset.cooling_set_point_celsius ?? undefined)
143
+
144
+ const unitSymbol = getTemperatureUnitSymbol(temperatureUnit)
145
+
146
+ return (
147
+ <div
148
+ {...attrs}
149
+ className={classNames(
150
+ 'seam-thermostat-climate-presets-card',
151
+ attrs.className
152
+ )}
153
+ >
154
+ <div className='seam-thermostat-climate-presets-card-top'>
155
+ <div className='seam-thermostat-climate-presets-card-name'>
156
+ {preset.display_name}
157
+
158
+ {preset.name != null && (
159
+ <div className='seam-thermostat-climate-presets-card-name-key'>
160
+ {preset.climate_preset_key}
161
+ </div>
162
+ )}
163
+ </div>
164
+
165
+ <div className='seam-thermostat-climate-presets-card-buttons'>
166
+ <IconButton
167
+ disabled={disabled || deletionLoading || !preset.can_edit}
168
+ onClick={onClickEdit}
169
+ title={t.edit}
170
+ >
171
+ <EditIcon />
172
+ </IconButton>
173
+
174
+ <Popover
175
+ content={({ hide }) => (
176
+ <PopoverContentPrompt
177
+ onCancel={hide}
178
+ onConfirm={() => {
179
+ onClickDelete()
180
+ hide()
181
+ }}
182
+ />
183
+ )}
184
+ >
185
+ {({ setRef }) => (
186
+ <IconButton
187
+ elRef={setRef}
188
+ disabled={disabled || !preset.can_delete}
189
+ title={t.delete}
190
+ >
191
+ {deletionLoading ? <Spinner size='small' /> : <TrashIcon />}
192
+ </IconButton>
193
+ )}
194
+ </Popover>
195
+ </div>
196
+ </div>
197
+
198
+ <div className='seam-thermostat-climate-presets-card-body'>
199
+ {heatPoint != null && (
200
+ <Chip
201
+ icon={<ThermostatHeatIcon />}
202
+ text={`${heatPoint} ${unitSymbol}`}
203
+ />
204
+ )}
205
+
206
+ {coolPoint != null && (
207
+ <Chip
208
+ icon={<ThermostatCoolIcon />}
209
+ text={`${coolPoint} ${unitSymbol}`}
210
+ />
211
+ )}
212
+
213
+ {preset.fan_mode_setting != null && (
214
+ <Chip icon={<FanIcon />} text={preset.fan_mode_setting} />
215
+ )}
216
+ </div>
217
+ </div>
218
+ )
219
+ }
220
+
221
+ function Chip({ icon, text }: { icon: ReactNode; text: string }): JSX.Element {
222
+ return (
223
+ <div className='seam-thermostat-climate-preset-chip'>
224
+ <span className='seam-thermostat-climate-preset-chip-icon'>{icon}</span>
225
+ <span className='seam-thermostat-climate-preset-chip-value'>{text}</span>
226
+ </div>
227
+ )
228
+ }
229
+
230
+ const t = {
231
+ title: 'Climate Presets',
232
+ createNew: 'Create New',
233
+ delete: 'Delete',
234
+ edit: 'Edit',
235
+ }
@@ -1,3 +1,5 @@
1
+ import classNames from 'classnames'
2
+
1
3
  import { ChevronDownIcon } from 'lib/icons/ChevronDown.js'
2
4
  import { FanIcon } from 'lib/icons/Fan.js'
3
5
  import { FanOutlineIcon } from 'lib/icons/FanOutline.js'
@@ -10,13 +12,29 @@ const modes: FanModeSetting[] = ['auto', 'on']
10
12
  interface FanModeMenuProps {
11
13
  mode: FanModeSetting
12
14
  onChange: (mode: FanModeSetting) => void
15
+ block?: boolean
16
+ size?: 'regular' | 'large'
13
17
  }
14
18
 
15
- export function FanModeMenu({ mode, onChange }: FanModeMenuProps): JSX.Element {
19
+ export function FanModeMenu({
20
+ mode,
21
+ onChange,
22
+ block,
23
+ size = 'regular',
24
+ }: FanModeMenuProps): JSX.Element {
16
25
  return (
17
26
  <Menu
18
27
  renderButton={({ onOpen }) => (
19
- <button onClick={onOpen} className='seam-fan-mode-menu-button'>
28
+ <button
29
+ onClick={onOpen}
30
+ className={classNames(
31
+ 'seam-fan-mode-menu-button',
32
+ `seam-fan-mode-menu-button-size-${size}`,
33
+ {
34
+ 'seam-fan-mode-menu-button-block-sized': block,
35
+ }
36
+ )}
37
+ >
20
38
  <div className='seam-fan-mode-menu-button-block'>
21
39
  <FanOutlineIcon />
22
40
  <span className='seam-fan-mode-menu-button-text'>
@@ -12,12 +12,17 @@ import { Temperature } from 'lib/ui/thermostat/Temperature.js'
12
12
  interface ThermostatCardProps {
13
13
  device: ThermostatDevice
14
14
  onEditName?: (newName: string) => void
15
+ onTemperatureUnitChange?: (temperatureUnit: 'fahrenheit' | 'celsius') => void
15
16
  }
16
17
 
17
18
  export function ThermostatCard(props: ThermostatCardProps): JSX.Element {
18
19
  return (
19
20
  <div className='seam-thermostat-card'>
20
- <Content device={props.device} onEditName={props.onEditName} />
21
+ <Content
22
+ device={props.device}
23
+ onEditName={props.onEditName}
24
+ onTemperatureUnitChange={props.onTemperatureUnitChange}
25
+ />
21
26
  </div>
22
27
  )
23
28
  }
@@ -30,9 +35,10 @@ function Content(props: ThermostatCardProps): JSX.Element | null {
30
35
  >('fahrenheit')
31
36
 
32
37
  const toggleTemperatureUnit = (): void => {
33
- setTemperatureUnit(
34
- temperatureUnit === 'fahrenheit' ? 'celsius' : 'fahrenheit'
35
- )
38
+ const newUnit = temperatureUnit === 'fahrenheit' ? 'celsius' : 'fahrenheit'
39
+
40
+ setTemperatureUnit(newUnit)
41
+ props.onTemperatureUnitChange?.(newUnit)
36
42
  }
37
43
 
38
44
  const {
@@ -1,5 +1,5 @@
1
- import type { HTMLAttributes, HtmlHTMLAttributes } from 'react'
1
+ import type { ButtonHTMLAttributes, HTMLAttributes } from 'react'
2
2
 
3
3
  export type DivProps = HTMLAttributes<HTMLDivElement>
4
- export type ButtonProps = HtmlHTMLAttributes<HTMLButtonElement>
5
- export type SpanProps = HtmlHTMLAttributes<HTMLSpanElement>
4
+ export type ButtonProps = ButtonHTMLAttributes<HTMLButtonElement>
5
+ export type SpanProps = HTMLAttributes<HTMLSpanElement>
@@ -1,3 +1,3 @@
1
- const seamapiReactVersion = '4.6.0'
1
+ const seamapiReactVersion = '4.7.0'
2
2
 
3
3
  export default seamapiReactVersion