@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.
- package/README.md +4 -3
- package/dist/elements.js +5767 -5579
- package/dist/elements.js.map +1 -1
- package/dist/index.css +105 -17
- package/dist/index.css.map +1 -1
- package/dist/index.min.css +1 -1
- package/dist/index.min.css.map +1 -1
- package/lib/icons/Add.d.ts +2 -0
- package/lib/icons/Add.js +7 -0
- package/lib/icons/Add.js.map +1 -0
- package/lib/icons/DotsEllipsisMore.js +2 -2
- package/lib/icons/DotsEllipsisMore.js.map +1 -1
- package/lib/seam/access-codes/use-create-access-code.d.ts +6 -0
- package/lib/seam/access-codes/use-create-access-code.js +18 -0
- package/lib/seam/access-codes/use-create-access-code.js.map +1 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeForm.d.ts +7 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeForm.js +52 -0
- package/lib/seam/components/AccessCodeForm/AccessCodeForm.js.map +1 -0
- package/lib/seam/components/AccessCodeTable/AccessCodeTable.d.ts +2 -1
- package/lib/seam/components/AccessCodeTable/AccessCodeTable.js +10 -2
- package/lib/seam/components/AccessCodeTable/AccessCodeTable.js.map +1 -1
- package/lib/seam/index.d.ts +1 -0
- package/lib/seam/index.js +1 -0
- package/lib/seam/index.js.map +1 -1
- package/lib/ui/Button.d.ts +2 -1
- package/lib/ui/Button.js +2 -2
- package/lib/ui/Button.js.map +1 -1
- package/lib/ui/FormField.d.ts +7 -0
- package/lib/ui/FormField.js +37 -0
- package/lib/ui/FormField.js.map +1 -0
- package/lib/ui/InputLabel.d.ts +7 -0
- package/lib/ui/InputLabel.js +7 -0
- package/lib/ui/InputLabel.js.map +1 -0
- package/lib/ui/TextField/SearchTextField.js +1 -48
- package/lib/ui/TextField/SearchTextField.js.map +1 -1
- package/lib/ui/TextField/TextField.d.ts +8 -2
- package/lib/ui/TextField/TextField.js +56 -5
- package/lib/ui/TextField/TextField.js.map +1 -1
- package/lib/ui/layout/ContentHeader.d.ts +5 -3
- package/lib/ui/layout/ContentHeader.js +2 -2
- package/lib/ui/layout/ContentHeader.js.map +1 -1
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +5 -5
- package/src/lib/icons/Add.tsx +33 -0
- package/src/lib/icons/DotsEllipsisMore.tsx +7 -7
- package/src/lib/seam/access-codes/use-create-access-code.ts +43 -0
- package/src/lib/seam/components/AccessCodeForm/AccessCodeForm.element.ts +12 -0
- package/src/lib/seam/components/AccessCodeForm/AccessCodeForm.tsx +113 -0
- package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.element.ts +1 -0
- package/src/lib/seam/components/AccessCodeTable/AccessCodeTable.tsx +30 -3
- package/src/lib/seam/index.ts +1 -0
- package/src/lib/ui/Button.tsx +3 -0
- package/src/lib/ui/FormField.tsx +56 -0
- package/src/lib/ui/InputLabel.tsx +21 -0
- package/src/lib/ui/TextField/SearchTextField.tsx +1 -70
- package/src/lib/ui/TextField/TextField.tsx +132 -19
- package/src/lib/ui/layout/ContentHeader.tsx +13 -5
- package/src/lib/version.ts +1 -1
- package/src/styles/_access-code-form.scss +55 -0
- package/src/styles/_access-code-table.scss +11 -0
- package/src/styles/_buttons.scss +11 -1
- package/src/styles/_colors.scss +1 -0
- package/src/styles/_form-field.scss +15 -0
- package/src/styles/_input-label.scss +11 -0
- package/src/styles/_inputs.scss +47 -22
- package/src/styles/_layout.scss +9 -1
- package/src/styles/_main.scss +6 -0
- package/src/styles/_menus.scss +3 -1
- package/src/styles/_supported-device-table.scss +2 -1
- package/src/styles/_tables.scss +1 -1
- 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
|
-
<
|
|
124
|
-
|
|
125
|
-
|
|
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}
|
package/src/lib/seam/index.ts
CHANGED
|
@@ -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'
|
package/src/lib/ui/Button.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
|
4
|
-
title?: string
|
|
3
|
+
export interface ContentHeaderProps {
|
|
5
4
|
onBack?: () => void
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
<
|
|
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
|
}
|
package/src/lib/version.ts
CHANGED
|
@@ -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
|
+
}
|