@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,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
+ }
@@ -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={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 function IconButton({ className, ...props }: ButtonProps): JSX.Element {
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 {...props} className={classNames('seam-icon-btn', className)} />
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 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
  )}