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