@seamapi/react 4.5.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 (83) hide show
  1. package/README.md +2 -2
  2. package/dist/elements.js +11560 -9368
  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/AccessCodeDetails/AccessCodeDetails.js +8 -3
  12. package/lib/seam/components/AccessCodeDetails/AccessCodeDetails.js.map +1 -1
  13. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js +17 -1
  14. package/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.js.map +1 -1
  15. package/lib/seam/thermostats/thermostat-device.d.ts +2 -1
  16. package/lib/seam/thermostats/thermostat-device.js.map +1 -1
  17. package/lib/seam/thermostats/unit-conversion.d.ts +5 -2
  18. package/lib/seam/thermostats/unit-conversion.js +5 -2
  19. package/lib/seam/thermostats/unit-conversion.js.map +1 -1
  20. package/lib/seam/thermostats/use-create-thermostat-climate-preset.d.ts +6 -0
  21. package/lib/seam/thermostats/use-create-thermostat-climate-preset.js +55 -0
  22. package/lib/seam/thermostats/use-create-thermostat-climate-preset.js.map +1 -0
  23. package/lib/seam/thermostats/use-delete-thermostat-climate-preset.d.ts +6 -0
  24. package/lib/seam/thermostats/use-delete-thermostat-climate-preset.js +44 -0
  25. package/lib/seam/thermostats/use-delete-thermostat-climate-preset.js.map +1 -0
  26. package/lib/seam/thermostats/use-update-thermostat-climate-preset.d.ts +6 -0
  27. package/lib/seam/thermostats/use-update-thermostat-climate-preset.js +55 -0
  28. package/lib/seam/thermostats/use-update-thermostat-climate-preset.js.map +1 -0
  29. package/lib/ui/Button.d.ts +3 -2
  30. package/lib/ui/Button.js +12 -4
  31. package/lib/ui/Button.js.map +1 -1
  32. package/lib/ui/IconButton.d.ts +5 -2
  33. package/lib/ui/IconButton.js +2 -2
  34. package/lib/ui/IconButton.js.map +1 -1
  35. package/lib/ui/Popover/Popover.d.ts +17 -0
  36. package/lib/ui/Popover/Popover.js +85 -0
  37. package/lib/ui/Popover/Popover.js.map +1 -0
  38. package/lib/ui/Popover/PopoverContentPrompt.d.ts +11 -0
  39. package/lib/ui/Popover/PopoverContentPrompt.js +12 -0
  40. package/lib/ui/Popover/PopoverContentPrompt.js.map +1 -0
  41. package/lib/ui/thermostat/ClimateModeMenu.d.ts +7 -2
  42. package/lib/ui/thermostat/ClimateModeMenu.js +7 -2
  43. package/lib/ui/thermostat/ClimateModeMenu.js.map +1 -1
  44. package/lib/ui/thermostat/ClimatePreset.d.ts +8 -0
  45. package/lib/ui/thermostat/ClimatePreset.js +141 -0
  46. package/lib/ui/thermostat/ClimatePreset.js.map +1 -0
  47. package/lib/ui/thermostat/ClimatePresets.d.ts +9 -0
  48. package/lib/ui/thermostat/ClimatePresets.js +72 -0
  49. package/lib/ui/thermostat/ClimatePresets.js.map +1 -0
  50. package/lib/ui/thermostat/FanModeMenu.d.ts +3 -1
  51. package/lib/ui/thermostat/FanModeMenu.js +5 -2
  52. package/lib/ui/thermostat/FanModeMenu.js.map +1 -1
  53. package/lib/ui/thermostat/ThermostatCard.d.ts +1 -0
  54. package/lib/ui/thermostat/ThermostatCard.js +4 -2
  55. package/lib/ui/thermostat/ThermostatCard.js.map +1 -1
  56. package/lib/ui/types.d.ts +3 -3
  57. package/lib/version.d.ts +1 -1
  58. package/lib/version.js +1 -1
  59. package/package.json +3 -2
  60. package/src/lib/icons/Trash.tsx +28 -0
  61. package/src/lib/seam/components/AccessCodeDetails/AccessCodeDetails.tsx +50 -34
  62. package/src/lib/seam/components/DeviceDetails/ThermostatDeviceDetails.tsx +52 -1
  63. package/src/lib/seam/thermostats/thermostat-device.ts +4 -0
  64. package/src/lib/seam/thermostats/unit-conversion.ts +12 -2
  65. package/src/lib/seam/thermostats/use-create-thermostat-climate-preset.ts +101 -0
  66. package/src/lib/seam/thermostats/use-delete-thermostat-climate-preset.ts +84 -0
  67. package/src/lib/seam/thermostats/use-update-thermostat-climate-preset.ts +103 -0
  68. package/src/lib/ui/Button.tsx +20 -3
  69. package/src/lib/ui/IconButton.tsx +19 -2
  70. package/src/lib/ui/Popover/Popover.tsx +168 -0
  71. package/src/lib/ui/Popover/PopoverContentPrompt.tsx +58 -0
  72. package/src/lib/ui/thermostat/ClimateModeMenu.tsx +33 -1
  73. package/src/lib/ui/thermostat/ClimatePreset.tsx +373 -0
  74. package/src/lib/ui/thermostat/ClimatePresets.tsx +235 -0
  75. package/src/lib/ui/thermostat/FanModeMenu.tsx +20 -2
  76. package/src/lib/ui/thermostat/ThermostatCard.tsx +10 -4
  77. package/src/lib/ui/types.ts +3 -3
  78. package/src/lib/version.ts +1 -1
  79. package/src/styles/_buttons.scss +56 -2
  80. package/src/styles/_main.scss +2 -0
  81. package/src/styles/_popover.scss +46 -0
  82. package/src/styles/_spinner.scss +1 -1
  83. package/src/styles/_thermostat.scss +154 -2
@@ -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 onClick={onOpen} className='seam-climate-mode-menu-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
  )}
@@ -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
+ }