@seamapi/react 1.28.1 → 1.30.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 (72) hide show
  1. package/README.md +4 -3
  2. package/dist/elements.js +5767 -5579
  3. package/dist/elements.js.map +1 -1
  4. package/dist/index.css +105 -17
  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/Add.d.ts +2 -0
  9. package/lib/icons/Add.js +7 -0
  10. package/lib/icons/Add.js.map +1 -0
  11. package/lib/icons/DotsEllipsisMore.js +2 -2
  12. package/lib/icons/DotsEllipsisMore.js.map +1 -1
  13. package/lib/seam/access-codes/use-create-access-code.d.ts +6 -0
  14. package/lib/seam/access-codes/use-create-access-code.js +18 -0
  15. package/lib/seam/access-codes/use-create-access-code.js.map +1 -0
  16. package/lib/seam/components/AccessCodeForm/AccessCodeForm.d.ts +7 -0
  17. package/lib/seam/components/AccessCodeForm/AccessCodeForm.js +52 -0
  18. package/lib/seam/components/AccessCodeForm/AccessCodeForm.js.map +1 -0
  19. package/lib/seam/components/AccessCodeTable/AccessCodeTable.d.ts +2 -1
  20. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js +10 -2
  21. package/lib/seam/components/AccessCodeTable/AccessCodeTable.js.map +1 -1
  22. package/lib/seam/index.d.ts +1 -0
  23. package/lib/seam/index.js +1 -0
  24. package/lib/seam/index.js.map +1 -1
  25. package/lib/ui/Button.d.ts +2 -1
  26. package/lib/ui/Button.js +2 -2
  27. package/lib/ui/Button.js.map +1 -1
  28. package/lib/ui/FormField.d.ts +7 -0
  29. package/lib/ui/FormField.js +37 -0
  30. package/lib/ui/FormField.js.map +1 -0
  31. package/lib/ui/InputLabel.d.ts +7 -0
  32. package/lib/ui/InputLabel.js +7 -0
  33. package/lib/ui/InputLabel.js.map +1 -0
  34. package/lib/ui/TextField/SearchTextField.js +1 -48
  35. package/lib/ui/TextField/SearchTextField.js.map +1 -1
  36. package/lib/ui/TextField/TextField.d.ts +8 -2
  37. package/lib/ui/TextField/TextField.js +56 -5
  38. package/lib/ui/TextField/TextField.js.map +1 -1
  39. package/lib/ui/layout/ContentHeader.d.ts +5 -3
  40. package/lib/ui/layout/ContentHeader.js +2 -2
  41. package/lib/ui/layout/ContentHeader.js.map +1 -1
  42. package/lib/version.d.ts +1 -1
  43. package/lib/version.js +1 -1
  44. package/package.json +5 -5
  45. package/src/lib/icons/Add.tsx +33 -0
  46. package/src/lib/icons/DotsEllipsisMore.tsx +7 -7
  47. package/src/lib/seam/access-codes/use-create-access-code.ts +43 -0
  48. package/src/lib/seam/components/AccessCodeForm/AccessCodeForm.element.ts +12 -0
  49. package/src/lib/seam/components/AccessCodeForm/AccessCodeForm.tsx +113 -0
  50. package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts +1 -0
  51. package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx +30 -3
  52. package/src/lib/seam/index.ts +1 -0
  53. package/src/lib/ui/Button.tsx +3 -0
  54. package/src/lib/ui/FormField.tsx +56 -0
  55. package/src/lib/ui/InputLabel.tsx +21 -0
  56. package/src/lib/ui/TextField/SearchTextField.tsx +1 -70
  57. package/src/lib/ui/TextField/TextField.tsx +132 -19
  58. package/src/lib/ui/layout/ContentHeader.tsx +13 -5
  59. package/src/lib/version.ts +1 -1
  60. package/src/styles/_access-code-form.scss +55 -0
  61. package/src/styles/_access-code-table.scss +11 -0
  62. package/src/styles/_buttons.scss +11 -1
  63. package/src/styles/_colors.scss +1 -0
  64. package/src/styles/_form-field.scss +15 -0
  65. package/src/styles/_input-label.scss +11 -0
  66. package/src/styles/_inputs.scss +47 -22
  67. package/src/styles/_layout.scss +9 -1
  68. package/src/styles/_main.scss +6 -0
  69. package/src/styles/_menus.scss +3 -1
  70. package/src/styles/_supported-device-table.scss +2 -1
  71. package/src/styles/_tables.scss +1 -1
  72. package/src/styles/_typography.scss +12 -2
@@ -0,0 +1,113 @@
1
+ import classNames from 'classnames'
2
+ import { useState } from 'react'
3
+
4
+ import { useCreateAccessCode } from 'lib/seam/access-codes/use-create-access-code.js'
5
+ import { useDevice, type UseDeviceData } from 'lib/seam/devices/use-device.js'
6
+ import { Button } from 'lib/ui/Button.js'
7
+ import { FormField } from 'lib/ui/FormField.js'
8
+ import { InputLabel } from 'lib/ui/InputLabel.js'
9
+ import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
10
+ import { TextField } from 'lib/ui/TextField/TextField.js'
11
+
12
+ export interface AccessCodeFormProps {
13
+ deviceId: string
14
+ onBack?: () => void
15
+ className?: string
16
+ }
17
+
18
+ export function AccessCodeForm({
19
+ deviceId,
20
+ onBack,
21
+ className,
22
+ }: AccessCodeFormProps): JSX.Element | null {
23
+ const { device } = useDevice({
24
+ device_id: deviceId,
25
+ })
26
+
27
+ if (device == null) {
28
+ return null
29
+ }
30
+
31
+ return (
32
+ <div className={classNames('seam-access-code-form', className)}>
33
+ <Content className={className} onBack={onBack} device={device} />
34
+ </div>
35
+ )
36
+ }
37
+
38
+ function Content({
39
+ onBack,
40
+ device,
41
+ }: Omit<AccessCodeFormProps, 'deviceId'> & {
42
+ device: NonNullable<UseDeviceData>
43
+ }): JSX.Element {
44
+ const [name, setName] = useState('')
45
+
46
+ const createAccessCode = useCreateAccessCode()
47
+
48
+ const save = (): void => {
49
+ if (name === '') {
50
+ return
51
+ }
52
+
53
+ if (createAccessCode.isLoading) {
54
+ return
55
+ }
56
+
57
+ createAccessCode.mutate(
58
+ {
59
+ name,
60
+ device_id: device.device_id,
61
+ },
62
+ {
63
+ onSuccess: () => {
64
+ onBack?.()
65
+ },
66
+ }
67
+ )
68
+ }
69
+
70
+ const nameError = name.length > 60 ? t.overCharacterLimitError : undefined
71
+
72
+ const isFormValid =
73
+ name.trim().length > 0 &&
74
+ nameError === undefined &&
75
+ !createAccessCode.isLoading
76
+
77
+ return (
78
+ <>
79
+ <ContentHeader
80
+ title={t.addNewAccessCode}
81
+ subheading={device.properties.name}
82
+ onBack={onBack}
83
+ />
84
+ <div className='seam-main'>
85
+ <FormField>
86
+ <InputLabel>{t.nameInputLabel}</InputLabel>
87
+ <TextField
88
+ size='large'
89
+ clearable
90
+ value={name}
91
+ onChange={setName}
92
+ hasError={nameError != null}
93
+ helperText={nameError}
94
+ />
95
+ </FormField>
96
+ <div className='seam-actions'>
97
+ <Button onClick={onBack}>{t.cancel}</Button>
98
+ <Button variant='solid' disabled={!isFormValid} onMouseDown={save}>
99
+ {t.save}
100
+ </Button>
101
+ </div>
102
+ </div>
103
+ </>
104
+ )
105
+ }
106
+
107
+ const t = {
108
+ addNewAccessCode: 'Add new access code',
109
+ overCharacterLimitError: '60 characters max',
110
+ nameInputLabel: 'Name the new code',
111
+ cancel: 'Cancel',
112
+ save: 'Save',
113
+ }
@@ -7,6 +7,7 @@ export const name = 'seam-access-code-table'
7
7
  export const props: ElementProps<AccessCodeTableProps> = {
8
8
  deviceId: 'string',
9
9
  disableLockUnlock: 'boolean',
10
+ disableCreateAccessCode: 'boolean',
10
11
  accessCodeFilter: 'function',
11
12
  accessCodeComparator: 'function',
12
13
  onAccessCodeClick: 'function',
@@ -3,6 +3,7 @@ import { useCallback, useMemo, useState } from 'react'
3
3
 
4
4
  import { compareByCreatedAtDesc } from 'lib/dates.js'
5
5
  import { AccessCodeKeyIcon } from 'lib/icons/AccessCodeKey.js'
6
+ import { AddIcon } from 'lib/icons/Add.js'
6
7
  import { CopyIcon } from 'lib/icons/Copy.js'
7
8
  import { ExclamationCircleOutlineIcon } from 'lib/icons/ExclamationCircleOutline.js'
8
9
  import { TriangleWarningOutlineIcon } from 'lib/icons/TriangleWarningOutline.js'
@@ -11,12 +12,14 @@ import {
11
12
  type UseAccessCodesData,
12
13
  } from 'lib/seam/access-codes/use-access-codes.js'
13
14
  import { AccessCodeDetails } from 'lib/seam/components/AccessCodeDetails/AccessCodeDetails.js'
15
+ import { AccessCodeForm } from 'lib/seam/components/AccessCodeForm/AccessCodeForm.js'
14
16
  import {
15
17
  type AccessCodeFilter,
16
18
  AccessCodeHealthBar,
17
19
  } from 'lib/seam/components/AccessCodeTable/AccessCodeHealthBar.js'
18
20
  import { CodeDetails } from 'lib/seam/components/AccessCodeTable/CodeDetails.js'
19
21
  import { copyToClipboard } from 'lib/ui/clipboard.js'
22
+ import { IconButton } from 'lib/ui/IconButton.js'
20
23
  import { ContentHeader } from 'lib/ui/layout/ContentHeader.js'
21
24
  import { MenuItem } from 'lib/ui/Menu/MenuItem.js'
22
25
  import { MoreActionsMenu } from 'lib/ui/Menu/MoreActionsMenu.js'
@@ -29,10 +32,12 @@ import { TableTitle } from 'lib/ui/Table/TableTitle.js'
29
32
  import { SearchTextField } from 'lib/ui/TextField/SearchTextField.js'
30
33
  import { Caption } from 'lib/ui/typography/Caption.js'
31
34
  import { Title } from 'lib/ui/typography/Title.js'
35
+ import { useToggle } from 'lib/ui/use-toggle.js'
32
36
 
33
37
  export interface AccessCodeTableProps {
34
38
  deviceId: string
35
39
  disableLockUnlock?: boolean
40
+ disableCreateAccessCode?: boolean
36
41
  accessCodeFilter?: (
37
42
  accessCode: AccessCode,
38
43
  searchInputValue: string
@@ -65,6 +70,7 @@ const defaultAccessCodeFilter = (
65
70
  export function AccessCodeTable({
66
71
  deviceId,
67
72
  disableLockUnlock = false,
73
+ disableCreateAccessCode = false,
68
74
  onAccessCodeClick = () => {},
69
75
  preventDefaultOnAccessCodeClick = false,
70
76
  onBack,
@@ -81,6 +87,7 @@ export function AccessCodeTable({
81
87
  >(null)
82
88
 
83
89
  const [searchInputValue, setSearchInputValue] = useState('')
90
+ const [accessCodeFormVisible, toggleAddAccessCodeForm] = useToggle()
84
91
 
85
92
  const filteredAccessCodes = useMemo(
86
93
  () =>
@@ -116,13 +123,33 @@ export function AccessCodeTable({
116
123
  )
117
124
  }
118
125
 
126
+ if (accessCodeFormVisible) {
127
+ return (
128
+ <AccessCodeForm
129
+ className={className}
130
+ onBack={toggleAddAccessCodeForm}
131
+ deviceId={deviceId}
132
+ />
133
+ )
134
+ }
135
+
119
136
  return (
120
137
  <div className={classNames('seam-access-code-table', className)}>
121
138
  <ContentHeader onBack={onBack} />
122
139
  <TableHeader>
123
- <TableTitle>
124
- {t.accessCodes} <Caption>({filteredAccessCodes.length})</Caption>
125
- </TableTitle>
140
+ <div className='seam-left'>
141
+ <TableTitle>
142
+ {t.accessCodes} <Caption>({filteredAccessCodes.length})</Caption>{' '}
143
+ </TableTitle>
144
+ {!disableCreateAccessCode && (
145
+ <IconButton
146
+ onClick={toggleAddAccessCodeForm}
147
+ className='seam-add-access-code-button'
148
+ >
149
+ <AddIcon />
150
+ </IconButton>
151
+ )}
152
+ </div>
126
153
  <SearchTextField
127
154
  value={searchInputValue}
128
155
  onChange={setSearchInputValue}
@@ -1,5 +1,6 @@
1
1
  export * from './access-codes/use-access-code.js'
2
2
  export * from './access-codes/use-access-codes.js'
3
+ export * from './access-codes/use-create-access-code.js'
3
4
  export * from './connect-webviews/use-create-connect-webview.js'
4
5
  export * from './devices/use-device.js'
5
6
  export * from './devices/use-device-providers.js'
@@ -7,6 +7,7 @@ interface ButtonProps extends PropsWithChildren {
7
7
  disabled?: boolean
8
8
  onClick?: MouseEventHandler<HTMLButtonElement>
9
9
  className?: string
10
+ onMouseDown?: MouseEventHandler<HTMLButtonElement>
10
11
  }
11
12
 
12
13
  export function Button({
@@ -16,6 +17,7 @@ export function Button({
16
17
  disabled = false,
17
18
  onClick,
18
19
  className,
20
+ onMouseDown,
19
21
  }: ButtonProps): JSX.Element {
20
22
  return (
21
23
  <button
@@ -28,6 +30,7 @@ export function Button({
28
30
  )}
29
31
  disabled={disabled}
30
32
  onClick={onClick}
33
+ onMouseDown={onMouseDown}
31
34
  >
32
35
  {children}
33
36
  </button>
@@ -0,0 +1,56 @@
1
+ import classNames from 'classnames'
2
+ import { cloneElement, useRef } from 'react'
3
+
4
+ import { InputLabel } from 'lib/ui/InputLabel.js'
5
+ import { TextField } from 'lib/ui/TextField/TextField.js'
6
+
7
+ interface FormFieldProps {
8
+ children: JSX.Element | JSX.Element[]
9
+ className?: string
10
+ }
11
+
12
+ export function FormField({
13
+ children,
14
+ className,
15
+ }: FormFieldProps): JSX.Element {
16
+ const inputRef = useRef<HTMLInputElement>()
17
+
18
+ const focusInput = (): void => {
19
+ if (inputRef.current != null) {
20
+ inputRef.current.focus()
21
+ }
22
+ }
23
+
24
+ const components = Array.isArray(children) ? children : [children]
25
+
26
+ // Map children to automatically focus on input when clicking
27
+ // on an InputLabel.
28
+ const clonedChildren = components.map((component, index) => {
29
+ const baseProps = {
30
+ ...component.props,
31
+ key: index,
32
+ }
33
+
34
+ if (component.type === InputLabel) {
35
+ return cloneElement(component, {
36
+ ...baseProps,
37
+ onClick: focusInput,
38
+ })
39
+ }
40
+
41
+ if (component.type === TextField) {
42
+ return cloneElement(component, {
43
+ ...baseProps,
44
+ ref: inputRef,
45
+ })
46
+ }
47
+
48
+ return cloneElement(component, baseProps)
49
+ })
50
+
51
+ return (
52
+ <div className={classNames('seam-form-field', className)}>
53
+ {clonedChildren}
54
+ </div>
55
+ )
56
+ }
@@ -0,0 +1,21 @@
1
+ import classNames from 'classnames'
2
+ import { forwardRef, type PropsWithChildren } from 'react'
3
+
4
+ interface InputLabelProps extends PropsWithChildren {
5
+ className?: string
6
+ onClick?: () => void
7
+ }
8
+
9
+ export const InputLabel = forwardRef<HTMLDivElement, InputLabelProps>(
10
+ function InputLabel({ children, className, onClick }, ref) {
11
+ return (
12
+ <div
13
+ className={classNames('seam-input-label', className)}
14
+ ref={ref}
15
+ onClick={onClick}
16
+ >
17
+ {children}
18
+ </div>
19
+ )
20
+ }
21
+ )
@@ -1,7 +1,5 @@
1
1
  import classNames from 'classnames'
2
- import { useEffect, useState } from 'react'
3
2
 
4
- import { CloseIcon } from 'lib/icons/Close.js'
5
3
  import { SearchIcon } from 'lib/icons/Search.js'
6
4
  import { TextField, type TextFieldProps } from 'lib/ui/TextField/TextField.js'
7
5
 
@@ -11,20 +9,6 @@ export function SearchTextField({
11
9
  onChange,
12
10
  ...props
13
11
  }: TextFieldProps): JSX.Element {
14
- const [inputEl, setInputEl] = useState<HTMLInputElement | null>(null)
15
-
16
- const valueIsEmpty = useValueIsEmpty(value, inputEl)
17
- const clearInput = (): void => {
18
- if (onChange != null) {
19
- onChange('')
20
- return
21
- }
22
-
23
- if (inputEl != null) {
24
- inputEl.value = ''
25
- }
26
- }
27
-
28
12
  return (
29
13
  <TextField
30
14
  value={value}
@@ -33,62 +17,9 @@ export function SearchTextField({
33
17
  className={classNames(className, 'seam-search-text-field')}
34
18
  startAdornment={<SearchIcon />}
35
19
  inputProps={{
36
- ref: setInputEl,
37
20
  placeholder: 'Search',
38
21
  }}
39
- endAdornment={
40
- <button
41
- className={classNames({
42
- 'seam-hidden': valueIsEmpty,
43
- })}
44
- onClick={clearInput}
45
- >
46
- <CloseIcon />
47
- </button>
48
- }
22
+ clearable
49
23
  />
50
24
  )
51
25
  }
52
-
53
- function useValueIsEmpty(
54
- value: string | undefined,
55
- inputEl: HTMLInputElement | null
56
- ): boolean {
57
- const [valueIsEmpty, setValueIsEmpty] = useState(true)
58
-
59
- // If this is a controlled element, we'll just look at `value`
60
- useEffect(() => {
61
- setValueIsEmpty(value == null || value === '')
62
- }, [value])
63
-
64
- // If this is not a controlled element, we'll need to listen to `input`
65
- // events.
66
- useEffect(() => {
67
- if (inputEl == null) {
68
- return
69
- }
70
-
71
- const handler = (event: Event): void => {
72
- if (value !== undefined) {
73
- return
74
- }
75
-
76
- if (event.target == null || !('value' in event.target)) {
77
- return
78
- }
79
-
80
- const inputValue = event.target.value
81
- if (value === undefined) {
82
- setValueIsEmpty(inputValue === '')
83
- }
84
- }
85
-
86
- inputEl.addEventListener('input', handler)
87
-
88
- return () => {
89
- inputEl.removeEventListener('input', handler)
90
- }
91
- }, [inputEl, value])
92
-
93
- return valueIsEmpty
94
- }
@@ -1,5 +1,16 @@
1
1
  import classNames from 'classnames'
2
- import type { ChangeEvent, InputHTMLAttributes, MutableRefObject } from 'react'
2
+ import {
3
+ type ChangeEvent,
4
+ type FocusEventHandler,
5
+ forwardRef,
6
+ type InputHTMLAttributes,
7
+ type MutableRefObject,
8
+ useEffect,
9
+ useImperativeHandle,
10
+ useState,
11
+ } from 'react'
12
+
13
+ import { CloseIcon } from 'lib/icons/Close.js'
3
14
 
4
15
  export interface TextFieldProps {
5
16
  value?: string
@@ -8,15 +19,24 @@ export interface TextFieldProps {
8
19
  endAdornment?: JSX.Element
9
20
  disabled?: boolean
10
21
  className?: string
22
+ size?: 'small' | 'large'
11
23
  inputProps?: {
12
24
  ref?:
13
25
  | MutableRefObject<HTMLInputElement | null>
14
26
  | ((inputEl: HTMLInputElement) => void)
15
27
  } & Omit<InputHTMLAttributes<HTMLInputElement>, 'value' | 'onChange'>
28
+ clearable?: boolean
29
+ onFocus?: FocusEventHandler<HTMLInputElement>
30
+ onBlur?: FocusEventHandler<HTMLInputElement>
31
+ helperText?: string
32
+ hasError?: boolean
16
33
  }
17
34
 
18
- export function TextField(props: TextFieldProps): JSX.Element {
19
- const {
35
+ export const TextField = forwardRef<
36
+ HTMLInputElement | undefined,
37
+ TextFieldProps
38
+ >(function TextField(
39
+ {
20
40
  value,
21
41
  onChange,
22
42
  className,
@@ -24,34 +44,127 @@ export function TextField(props: TextFieldProps): JSX.Element {
24
44
  endAdornment,
25
45
  inputProps,
26
46
  disabled = false,
27
- } = props
47
+ size = 'small',
48
+ clearable = false,
49
+ onFocus,
50
+ onBlur,
51
+ hasError = false,
52
+ helperText,
53
+ },
54
+ ref
55
+ ): JSX.Element {
56
+ const [inputEl, setInputEl] = useState<HTMLInputElement | null>(null)
57
+
58
+ const valueIsEmpty = useValueIsEmpty(value, inputEl)
59
+
60
+ const clearInput = (): void => {
61
+ if (onChange != null) {
62
+ onChange('')
63
+ return
64
+ }
65
+
66
+ if (inputEl != null) {
67
+ inputEl.value = ''
68
+ }
69
+ }
70
+
71
+ // Maintain a local ref, and still forward it along
72
+ useImperativeHandle(ref, () => (inputEl != null ? inputEl : undefined), [
73
+ inputEl,
74
+ ])
75
+
76
+ const endAdornmentVisible = endAdornment != null || clearable
28
77
 
29
78
  return (
30
79
  <div
31
- className={classNames('seam-text-field', className, {
80
+ className={classNames('seam-text-field', className, `seam-${size}`, {
32
81
  'seam-disabled': disabled,
82
+ 'seam-error': hasError,
33
83
  })}
34
84
  >
35
- {startAdornment != null && (
36
- <div className='seam-adornment seam-start'>{startAdornment}</div>
37
- )}
38
- <input
39
- className='seam-text-field-input'
40
- value={value}
41
- onChange={onChange == null ? undefined : handleString(onChange)}
42
- type='text'
43
- disabled={disabled}
44
- {...inputProps}
45
- />
46
- {endAdornment != null && (
47
- <div className='seam-adornment seam-end'>{endAdornment}</div>
85
+ <div className='seam-main'>
86
+ {startAdornment != null && (
87
+ <div className='seam-adornment seam-start'>{startAdornment}</div>
88
+ )}
89
+ <input
90
+ className='seam-text-field-input'
91
+ value={value}
92
+ onChange={onChange == null ? undefined : handleString(onChange)}
93
+ type='text'
94
+ disabled={disabled}
95
+ ref={setInputEl}
96
+ onFocus={onFocus}
97
+ onBlur={onBlur}
98
+ {...inputProps}
99
+ />
100
+ {endAdornmentVisible && (
101
+ <div className='seam-adornment seam-end'>
102
+ {clearable && (
103
+ <button
104
+ className={classNames({
105
+ 'seam-hidden': valueIsEmpty,
106
+ })}
107
+ onClick={clearInput}
108
+ >
109
+ <CloseIcon />
110
+ </button>
111
+ )}
112
+ {endAdornment}
113
+ </div>
114
+ )}
115
+ </div>
116
+ {helperText != null && (
117
+ <div className='seam-helper-text'>{helperText}</div>
48
118
  )}
49
119
  </div>
50
120
  )
51
- }
121
+ })
52
122
 
53
123
  export const handleString =
54
124
  (setter: (v: string) => void) =>
55
125
  (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>): void => {
56
126
  setter(event.currentTarget.value)
57
127
  }
128
+
129
+ function useValueIsEmpty(
130
+ value: string | undefined,
131
+ inputEl: HTMLInputElement | null
132
+ ): boolean {
133
+ const [valueIsEmpty, setValueIsEmpty] = useState(true)
134
+
135
+ // If this is a controlled element, we'll just look at `value`
136
+ useEffect(() => {
137
+ setValueIsEmpty(value == null || value === '')
138
+ }, [value])
139
+
140
+ // If this is not a controlled element, we'll need to listen to `input`
141
+ // events.
142
+ useEffect(() => {
143
+ if (inputEl == null) {
144
+ return
145
+ }
146
+
147
+ const handler = (event: Event): void => {
148
+ if (value !== undefined) {
149
+ return
150
+ }
151
+
152
+ if (event.target == null || !('value' in event.target)) {
153
+ return
154
+ }
155
+
156
+ const inputValue = event.target.value
157
+ if (value === undefined) {
158
+ setValueIsEmpty(inputValue === '')
159
+ }
160
+ }
161
+
162
+ inputEl.addEventListener('input', handler)
163
+
164
+ return () => {
165
+ inputEl.removeEventListener('input', handler)
166
+ }
167
+ }, [inputEl, value])
168
+
169
+ return valueIsEmpty
170
+ }
@@ -1,10 +1,13 @@
1
1
  import { ArrowBackIcon } from 'lib/icons/ArrowBack.js'
2
2
 
3
- export function ContentHeader(props: {
4
- title?: string
3
+ export interface ContentHeaderProps {
5
4
  onBack?: () => void
6
- }): JSX.Element | null {
7
- const { title, onBack } = props
5
+ title?: string
6
+ subheading?: string
7
+ }
8
+
9
+ export function ContentHeader(props: ContentHeaderProps): JSX.Element | null {
10
+ const { title, onBack, subheading } = props
8
11
  if (title == null && onBack == null) {
9
12
  return null
10
13
  }
@@ -12,7 +15,12 @@ export function ContentHeader(props: {
12
15
  return (
13
16
  <div className='seam-content-header'>
14
17
  <BackIcon onClick={onBack} />
15
- <span className='seam-title'>{title}</span>
18
+ <div>
19
+ <span className='seam-title'>{title}</span>
20
+ {subheading != null && (
21
+ <span className='seam-subheading'>{subheading}</span>
22
+ )}
23
+ </div>
16
24
  </div>
17
25
  )
18
26
  }
@@ -1,3 +1,3 @@
1
- const seamapiReactVersion = '1.28.1'
1
+ const seamapiReactVersion = '1.30.0'
2
2
 
3
3
  export default seamapiReactVersion
@@ -0,0 +1,55 @@
1
+ @use './colors';
2
+
3
+ @mixin all {
4
+ .seam-access-code-form {
5
+ @include main;
6
+ }
7
+ }
8
+
9
+ @mixin main {
10
+ > .seam-main {
11
+ padding: 24px 24px 0;
12
+
13
+ .seam-code-text-field {
14
+ margin-bottom: 4px;
15
+ }
16
+
17
+ .seam-code-field {
18
+ > .seam-text-field {
19
+ margin-bottom: 4px;
20
+ }
21
+
22
+ > .seam-bottom {
23
+ align-items: center;
24
+ justify-content: space-between;
25
+ display: none;
26
+
27
+ .seam-requirements {
28
+ color: colors.$text-gray-2;
29
+ font-size: 14px;
30
+ line-height: 134%;
31
+ list-style: disc;
32
+ padding-left: 20px;
33
+
34
+ &.seam-error {
35
+ color: colors.$status-red;
36
+ }
37
+ }
38
+
39
+ &.seam-visible {
40
+ display: flex;
41
+ }
42
+ }
43
+ }
44
+
45
+ .seam-actions {
46
+ display: flex;
47
+ gap: 8px;
48
+ justify-content: center;
49
+
50
+ .seam-btn {
51
+ width: 96px;
52
+ }
53
+ }
54
+ }
55
+ }