@atproto/oauth-provider 0.1.0 → 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- package/CHANGELOG.md +13 -0
- package/dist/account/account-manager.d.ts +2 -2
- package/dist/account/account-manager.d.ts.map +1 -1
- package/dist/account/account-manager.js.map +1 -1
- package/dist/account/account-store.d.ts +19 -6
- package/dist/account/account-store.d.ts.map +1 -1
- package/dist/account/account-store.js +16 -1
- package/dist/account/account-store.js.map +1 -1
- package/dist/assets/app/bundle-manifest.json +3 -3
- package/dist/assets/app/main.css +1 -1
- package/dist/assets/app/main.js +3 -3
- package/dist/assets/app/main.js.map +1 -1
- package/dist/client/client-auth.d.ts.map +1 -1
- package/dist/client/client-auth.js +2 -2
- package/dist/client/client-auth.js.map +1 -1
- package/dist/client/client-manager.d.ts.map +1 -1
- package/dist/client/client-manager.js +3 -1
- package/dist/client/client-manager.js.map +1 -1
- package/dist/client/client.d.ts.map +1 -1
- package/dist/client/client.js +3 -3
- package/dist/client/client.js.map +1 -1
- package/dist/dpop/dpop-manager.d.ts.map +1 -1
- package/dist/dpop/dpop-manager.js +3 -3
- package/dist/dpop/dpop-manager.js.map +1 -1
- package/dist/errors/invalid-token-error.d.ts.map +1 -1
- package/dist/errors/invalid-token-error.js +3 -2
- package/dist/errors/invalid-token-error.js.map +1 -1
- package/dist/errors/second-authentication-factor-required-error.d.ts +13 -0
- package/dist/errors/second-authentication-factor-required-error.d.ts.map +1 -0
- package/dist/errors/second-authentication-factor-required-error.js +23 -0
- package/dist/errors/second-authentication-factor-required-error.js.map +1 -0
- package/dist/lib/util/authorization-header.js +1 -1
- package/dist/lib/util/authorization-header.js.map +1 -1
- package/dist/metadata/build-metadata.d.ts.map +1 -1
- package/dist/metadata/build-metadata.js +2 -0
- package/dist/metadata/build-metadata.js.map +1 -1
- package/dist/oauth-errors.d.ts +1 -0
- package/dist/oauth-errors.d.ts.map +1 -1
- package/dist/oauth-errors.js +3 -1
- package/dist/oauth-errors.js.map +1 -1
- package/dist/oauth-provider.d.ts +101 -4
- package/dist/oauth-provider.d.ts.map +1 -1
- package/dist/oauth-provider.js +98 -110
- package/dist/oauth-provider.js.map +1 -1
- package/dist/output/{send-authorize-page.d.ts → build-authorize-data.d.ts} +2 -5
- package/dist/output/build-authorize-data.d.ts.map +1 -0
- package/dist/output/build-authorize-data.js +22 -0
- package/dist/output/build-authorize-data.js.map +1 -0
- package/dist/output/build-error-payload.d.ts.map +1 -1
- package/dist/output/build-error-payload.js +4 -3
- package/dist/output/build-error-payload.js.map +1 -1
- package/dist/output/customization.d.ts +2 -12
- package/dist/output/customization.d.ts.map +1 -1
- package/dist/output/customization.js +59 -32
- package/dist/output/customization.js.map +1 -1
- package/dist/output/output-manager.d.ts +16 -0
- package/dist/output/output-manager.d.ts.map +1 -0
- package/dist/output/output-manager.js +69 -0
- package/dist/output/output-manager.js.map +1 -0
- package/dist/output/send-web-page.d.ts +1 -1
- package/dist/output/send-web-page.d.ts.map +1 -1
- package/dist/output/send-web-page.js +3 -2
- package/dist/output/send-web-page.js.map +1 -1
- package/package.json +6 -6
- package/src/account/account-manager.ts +2 -2
- package/src/account/account-store.ts +12 -6
- package/src/assets/app/components/accept-form.tsx +86 -83
- package/src/assets/app/components/account-picker.tsx +98 -79
- package/src/assets/app/components/button.tsx +34 -0
- package/src/assets/app/components/client-identifier.tsx +12 -13
- package/src/assets/app/components/fieldset.tsx +26 -0
- package/src/assets/app/components/form-card.tsx +47 -0
- package/src/assets/app/components/help-card.tsx +1 -1
- package/src/assets/app/components/icons/alert-icon.tsx +5 -0
- package/src/assets/app/components/icons/at-symbol-icon.tsx +5 -0
- package/src/assets/app/components/icons/caret-right-icon.tsx +5 -0
- package/src/assets/app/components/icons/lock-icon.tsx +5 -0
- package/src/assets/app/components/icons/token-icon.tsx +5 -0
- package/src/assets/app/components/icons/util.tsx +17 -0
- package/src/assets/app/components/info-card.tsx +45 -0
- package/src/assets/app/components/input-checkbox.tsx +47 -0
- package/src/assets/app/components/input-container.tsx +37 -0
- package/src/assets/app/components/input-layout.tsx +47 -0
- package/src/assets/app/components/input-text.tsx +69 -0
- package/src/assets/app/components/layout-title-page.tsx +33 -16
- package/src/assets/app/components/layout-welcome.tsx +30 -14
- package/src/assets/app/components/sign-in-form.tsx +214 -196
- package/src/assets/app/components/sign-up-account-form.tsx +101 -117
- package/src/assets/app/components/sign-up-disclaimer.tsx +1 -1
- package/src/assets/app/hooks/use-api.ts +2 -0
- package/src/assets/app/lib/api.ts +49 -14
- package/src/assets/app/lib/clsx.ts +6 -1
- package/src/assets/app/lib/util.ts +3 -0
- package/src/assets/app/main.css +2 -1
- package/src/assets/app/views/accept-view.tsx +4 -3
- package/src/assets/app/views/authorize-view.tsx +8 -4
- package/src/assets/app/views/error-view.tsx +24 -15
- package/src/assets/app/views/sign-in-view.tsx +5 -15
- package/src/assets/app/views/sign-up-view.tsx +3 -10
- package/src/assets/app/views/welcome-view.tsx +11 -18
- package/src/client/client-auth.ts +3 -2
- package/src/client/client-manager.ts +2 -1
- package/src/client/client.ts +3 -1
- package/src/dpop/dpop-manager.ts +3 -2
- package/src/errors/invalid-token-error.ts +3 -1
- package/src/errors/second-authentication-factor-required-error.ts +25 -0
- package/src/lib/util/authorization-header.ts +1 -1
- package/src/metadata/build-metadata.ts +3 -0
- package/src/oauth-errors.ts +1 -0
- package/src/oauth-provider.ts +110 -99
- package/src/output/{send-authorize-page.ts → build-authorize-data.ts} +3 -43
- package/src/output/build-error-payload.ts +3 -1
- package/src/output/customization.ts +67 -45
- package/src/output/output-manager.ts +87 -0
- package/src/output/send-web-page.ts +4 -3
- package/tailwind.config.js +14 -1
- package/dist/output/send-authorize-page.d.ts.map +0 -1
- package/dist/output/send-authorize-page.js +0 -49
- package/dist/output/send-authorize-page.js.map +0 -1
- package/dist/output/send-error-page.d.ts +0 -5
- package/dist/output/send-error-page.d.ts.map +0 -1
- package/dist/output/send-error-page.js +0 -31
- package/dist/output/send-error-page.js.map +0 -1
- package/src/output/send-error-page.ts +0 -41
@@ -1,21 +1,30 @@
|
|
1
|
-
import type {
|
1
|
+
import type { ReactNode } from 'react'
|
2
2
|
import { Account } from '../backend-data'
|
3
|
-
import {
|
3
|
+
import { Override } from '../lib/util'
|
4
|
+
import { Button } from './button'
|
5
|
+
import { FormCard, FormCardProps } from './form-card'
|
6
|
+
import { AtSymbolIcon } from './icons/at-symbol-icon'
|
7
|
+
import { CaretRightIcon } from './icons/caret-right-icon'
|
8
|
+
import { InputContainer } from './input-container'
|
9
|
+
import { Fieldset } from './fieldset'
|
4
10
|
|
5
|
-
export type AccountPickerProps =
|
6
|
-
|
11
|
+
export type AccountPickerProps = Override<
|
12
|
+
FormCardProps,
|
13
|
+
{
|
14
|
+
accounts: readonly Account[]
|
7
15
|
|
8
|
-
|
9
|
-
|
16
|
+
onAccount: (account: Account) => void
|
17
|
+
accountAria?: (account: Account) => string
|
10
18
|
|
11
|
-
|
12
|
-
|
13
|
-
|
19
|
+
onOther?: () => void
|
20
|
+
otherLabel?: ReactNode
|
21
|
+
otherAria?: string
|
14
22
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
}
|
23
|
+
onBack?: () => void
|
24
|
+
backLabel?: ReactNode
|
25
|
+
backAria?: string
|
26
|
+
}
|
27
|
+
>
|
19
28
|
|
20
29
|
export function AccountPicker({
|
21
30
|
accounts,
|
@@ -24,85 +33,95 @@ export function AccountPicker({
|
|
24
33
|
accountAria = (a) => `Sign in as ${a.name}`,
|
25
34
|
|
26
35
|
onOther = undefined,
|
27
|
-
otherLabel = '
|
36
|
+
otherLabel = 'Another account',
|
28
37
|
otherAria = 'Login to account that is not listed',
|
29
38
|
|
30
39
|
onBack,
|
31
40
|
backAria,
|
32
41
|
backLabel = backAria,
|
33
42
|
|
34
|
-
|
35
|
-
|
36
|
-
}: AccountPickerProps & HTMLAttributes<HTMLDivElement>) {
|
43
|
+
...props
|
44
|
+
}: AccountPickerProps) {
|
37
45
|
return (
|
38
|
-
<
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
46
|
+
<FormCard
|
47
|
+
{...props}
|
48
|
+
cancel={
|
49
|
+
onBack && (
|
50
|
+
<Button onClick={onBack} aria-label={backAria}>
|
51
|
+
{backLabel}
|
52
|
+
</Button>
|
53
|
+
)
|
54
|
+
}
|
55
|
+
>
|
56
|
+
<Fieldset title="Sign in as...">
|
57
|
+
{accounts.map((account) => {
|
58
|
+
const [name, identifier] = [
|
59
|
+
account.name,
|
60
|
+
account.preferred_username,
|
61
|
+
account.email,
|
62
|
+
account.sub,
|
63
|
+
].filter(Boolean) as [string, string?]
|
48
64
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
65
|
+
return (
|
66
|
+
<InputContainer
|
67
|
+
key={account.sub}
|
68
|
+
onClick={() => onAccount(account)}
|
69
|
+
role="button"
|
70
|
+
aria-label={accountAria(account)}
|
71
|
+
icon={
|
72
|
+
account.picture ? (
|
73
|
+
<img
|
74
|
+
crossOrigin="anonymous"
|
75
|
+
src={account.picture}
|
76
|
+
alt={name}
|
77
|
+
className="-ml-1 w-6 h-6 rounded-full"
|
78
|
+
/>
|
79
|
+
) : (
|
80
|
+
<svg
|
81
|
+
className="-ml-1 w-6 h-6"
|
82
|
+
viewBox="0 0 24 24"
|
83
|
+
fill="none"
|
84
|
+
stroke="none"
|
85
|
+
>
|
86
|
+
<circle cx="12" cy="12" r="12" fill="#0070ff"></circle>
|
87
|
+
<circle cx="12" cy="9.5" r="3.5" fill="#fff"></circle>
|
88
|
+
<path
|
89
|
+
strokeLinecap="round"
|
90
|
+
strokeLinejoin="round"
|
91
|
+
fill="#fff"
|
92
|
+
d="M 12.058 22.784 C 9.422 22.784 7.007 21.836 5.137 20.262 C 5.667 17.988 8.534 16.25 11.99 16.25 C 15.494 16.25 18.391 18.036 18.864 20.357 C 17.01 21.874 14.64 22.784 12.058 22.784 Z"
|
93
|
+
></path>
|
94
|
+
</svg>
|
95
|
+
)
|
96
|
+
}
|
97
|
+
append={<CaretRightIcon className="h-4" />}
|
98
|
+
>
|
99
|
+
<span className="flex flex-wrap items-center">
|
100
|
+
<span className="font-medium truncate mr-2">{name}</span>
|
68
101
|
{identifier && (
|
69
|
-
<span className="
|
102
|
+
<span className="text-sm text-neutral-500 dark:text-neutral-400 truncate">
|
70
103
|
{identifier}
|
71
104
|
</span>
|
72
105
|
)}
|
73
|
-
</
|
74
|
-
</
|
75
|
-
|
76
|
-
|
77
|
-
)
|
78
|
-
})}
|
79
|
-
{onOther && (
|
80
|
-
<button
|
81
|
-
className="cursor-pointer text-start flex items-center justify-between py-2 px-6 border-t border-b hover:bg-slate-100 border-slate-200 dark:border-slate-700 dark:hover:bg-slate-900"
|
82
|
-
onClick={onOther}
|
83
|
-
aria-label={otherAria}
|
84
|
-
role="button"
|
85
|
-
>
|
86
|
-
<div className="min-w-0 my-2 flex-auto truncate">{otherLabel}</div>
|
87
|
-
|
88
|
-
<span className="scale-x-50 font-semibold text-xl">></span>
|
89
|
-
</button>
|
90
|
-
)}
|
91
|
-
|
92
|
-
<div className="flex-auto" />
|
106
|
+
</span>
|
107
|
+
</InputContainer>
|
108
|
+
)
|
109
|
+
})}
|
93
110
|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
className="
|
100
|
-
|
111
|
+
{onOther && (
|
112
|
+
<InputContainer
|
113
|
+
onClick={onOther}
|
114
|
+
aria-label={otherAria}
|
115
|
+
role="button"
|
116
|
+
append={<CaretRightIcon className="h-4" />}
|
117
|
+
icon={<AtSymbolIcon className="h-4" />}
|
101
118
|
>
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
119
|
+
<span className="truncate text-gray-700 dark:text-gray-400">
|
120
|
+
{otherLabel}
|
121
|
+
</span>
|
122
|
+
</InputContainer>
|
123
|
+
)}
|
124
|
+
</Fieldset>
|
125
|
+
</FormCard>
|
107
126
|
)
|
108
127
|
}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
import { ButtonHTMLAttributes } from 'react'
|
2
|
+
import { clsx } from '../lib/clsx'
|
3
|
+
|
4
|
+
export function Button({
|
5
|
+
children,
|
6
|
+
className,
|
7
|
+
type = 'button',
|
8
|
+
role = 'Button',
|
9
|
+
color = 'grey',
|
10
|
+
disabled = false,
|
11
|
+
loading = undefined,
|
12
|
+
...props
|
13
|
+
}: {
|
14
|
+
color?: 'brand' | 'grey'
|
15
|
+
loading?: boolean
|
16
|
+
} & ButtonHTMLAttributes<HTMLButtonElement>) {
|
17
|
+
return (
|
18
|
+
<button
|
19
|
+
role={role}
|
20
|
+
type={type}
|
21
|
+
disabled={disabled || loading === true}
|
22
|
+
{...props}
|
23
|
+
className={clsx(
|
24
|
+
'py-2 px-6 rounded-lg truncate cursor-pointer touch-manipulation tracking-wide overflow-hidden',
|
25
|
+
color === 'brand'
|
26
|
+
? 'bg-brand text-white'
|
27
|
+
: 'bg-slate-100 hover:bg-slate-200 text-slate-600 dark:bg-slate-800 dark:hover:bg-slate-700 dark:text-slate-300',
|
28
|
+
className,
|
29
|
+
)}
|
30
|
+
>
|
31
|
+
{children}
|
32
|
+
</button>
|
33
|
+
)
|
34
|
+
}
|
@@ -1,4 +1,8 @@
|
|
1
|
-
import {
|
1
|
+
import {
|
2
|
+
isOAuthClientIdDiscoverable,
|
3
|
+
isOAuthClientIdLoopback,
|
4
|
+
OAuthClientMetadata,
|
5
|
+
} from '@atproto/oauth-types'
|
2
6
|
import { HTMLAttributes } from 'react'
|
3
7
|
|
4
8
|
import { UrlViewer } from './url-viewer'
|
@@ -15,18 +19,13 @@ export function ClientIdentifier({
|
|
15
19
|
as: As = 'span',
|
16
20
|
...attrs
|
17
21
|
}: ClientIdentifierProps & HTMLAttributes<Element>) {
|
18
|
-
if (
|
19
|
-
return
|
20
|
-
<UrlViewer
|
21
|
-
as={As}
|
22
|
-
{...attrs}
|
23
|
-
url={clientMetadata.client_uri}
|
24
|
-
proto
|
25
|
-
path
|
26
|
-
/>
|
27
|
-
)
|
22
|
+
if (isOAuthClientIdLoopback(clientId)) {
|
23
|
+
return <As {...attrs}>An application on your device</As>
|
28
24
|
}
|
29
25
|
|
30
|
-
|
31
|
-
|
26
|
+
if (isOAuthClientIdDiscoverable(clientId)) {
|
27
|
+
return <UrlViewer as={As} {...attrs} url={clientId} proto path />
|
28
|
+
}
|
29
|
+
|
30
|
+
return <As {...attrs}>{clientMetadata.client_name || clientId}</As>
|
32
31
|
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
import { FieldsetHTMLAttributes, forwardRef, ReactNode } from 'react'
|
2
|
+
import { Override } from '../lib/util'
|
3
|
+
|
4
|
+
export type FieldsetCardProps = Override<
|
5
|
+
FieldsetHTMLAttributes<HTMLFieldSetElement>,
|
6
|
+
{
|
7
|
+
title?: ReactNode
|
8
|
+
}
|
9
|
+
>
|
10
|
+
|
11
|
+
export const Fieldset = forwardRef<HTMLFieldSetElement, FieldsetCardProps>(
|
12
|
+
({ title, children, ...props }, ref) => (
|
13
|
+
<fieldset ref={ref} {...props}>
|
14
|
+
{title && (
|
15
|
+
<p
|
16
|
+
key="title"
|
17
|
+
className="mb-1 text-slate-600 dark:text-slate-400 text-sm font-medium"
|
18
|
+
>
|
19
|
+
{title}
|
20
|
+
</p>
|
21
|
+
)}
|
22
|
+
|
23
|
+
<div className="flex flex-col space-y-4">{children}</div>
|
24
|
+
</fieldset>
|
25
|
+
),
|
26
|
+
)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { FormHTMLAttributes, forwardRef, ReactNode } from 'react'
|
2
|
+
import { InfoCard } from './info-card'
|
3
|
+
import { clsx } from '../lib/clsx'
|
4
|
+
import { Override } from '../lib/util'
|
5
|
+
|
6
|
+
export type FormCardProps = Override<
|
7
|
+
FormHTMLAttributes<HTMLFormElement>,
|
8
|
+
{
|
9
|
+
append?: ReactNode
|
10
|
+
error?: ReactNode
|
11
|
+
cancel?: ReactNode
|
12
|
+
actions?: ReactNode
|
13
|
+
}
|
14
|
+
>
|
15
|
+
|
16
|
+
export const FormCard = forwardRef<HTMLFormElement, FormCardProps>(
|
17
|
+
({ actions, cancel, append, className, children, error, ...props }, ref) => {
|
18
|
+
return (
|
19
|
+
<form
|
20
|
+
ref={ref}
|
21
|
+
className={clsx('flex flex-col py-4 space-y-4', className)}
|
22
|
+
{...props}
|
23
|
+
>
|
24
|
+
<div className="space-y-4">{children}</div>
|
25
|
+
|
26
|
+
{append && <div key="append">{append}</div>}
|
27
|
+
|
28
|
+
{error && (
|
29
|
+
<InfoCard key="error" role="alert">
|
30
|
+
{error}
|
31
|
+
</InfoCard>
|
32
|
+
)}
|
33
|
+
|
34
|
+
{(actions || cancel) && (
|
35
|
+
<div
|
36
|
+
key="buttons"
|
37
|
+
className="flex flex-wrap flex-row-reverse items-center justify-end space-x-reverse space-x-2"
|
38
|
+
>
|
39
|
+
{actions}
|
40
|
+
<div className="flex-auto" />
|
41
|
+
{cancel}
|
42
|
+
</div>
|
43
|
+
)}
|
44
|
+
</form>
|
45
|
+
)
|
46
|
+
},
|
47
|
+
)
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { makeSvgComponent } from './util'
|
2
|
+
|
3
|
+
export const AlertIcon = makeSvgComponent(
|
4
|
+
'M11.14 4.494a.995.995 0 0 1 1.72 0l7.001 12.008a.996.996 0 0 1-.86 1.498H4.999a.996.996 0 0 1-.86-1.498L11.14 4.494Zm3.447-1.007c-1.155-1.983-4.019-1.983-5.174 0L2.41 15.494C1.247 17.491 2.686 20 4.998 20h14.004c2.312 0 3.751-2.509 2.587-4.506L14.587 3.487ZM13 9.019a1 1 0 1 0-2 0v2.994a1 1 0 1 0 2 0V9.02Zm-1 4.731a1.25 1.25 0 1 0 0 2.5 1.25 1.25 0 0 0 0-2.5Z',
|
5
|
+
)
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { makeSvgComponent } from './util'
|
2
|
+
|
3
|
+
export const AtSymbolIcon = makeSvgComponent(
|
4
|
+
'M12 4a8 8 0 1 0 4.21 14.804 1 1 0 0 1 1.054 1.7A9.958 9.958 0 0 1 12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10c0 1.104-.27 2.31-.949 3.243-.716.984-1.849 1.6-3.331 1.465a4.207 4.207 0 0 1-2.93-1.585c-.94 1.21-2.388 1.94-3.985 1.715-2.53-.356-4.04-2.91-3.682-5.458.358-2.547 2.514-4.586 5.044-4.23.905.127 1.68.536 2.286 1.126a1 1 0 0 1 1.964.368l-.515 3.545v.002a2.222 2.222 0 0 0 1.999 2.526c.75.068 1.212-.21 1.533-.65.358-.493.566-1.245.566-2.067a8 8 0 0 0-8-8Zm-.112 5.13c-1.195-.168-2.544.819-2.784 2.529-.24 1.71.784 3.03 1.98 3.198 1.195.168 2.543-.819 2.784-2.529.24-1.71-.784-3.03-1.98-3.198Z',
|
5
|
+
)
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { makeSvgComponent } from './util'
|
2
|
+
|
3
|
+
export const LockIcon = makeSvgComponent(
|
4
|
+
'M7 7a5 5 0 0 1 10 0v2h1a2 2 0 0 1 2 2v9a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2v-9a2 2 0 0 1 2-2h1V7Zm-1 4v9h12v-9H6Zm9-2H9V7a3 3 0 1 1 6 0v2Zm-3 4a1 1 0 0 1 1 1v3a1 1 0 1 1-2 0v-3a1 1 0 0 1 1-1Z',
|
5
|
+
)
|
@@ -0,0 +1,5 @@
|
|
1
|
+
import { makeSvgComponent } from './util'
|
2
|
+
|
3
|
+
export const TokenIcon = makeSvgComponent(
|
4
|
+
'M4 5.5a.5.5 0 0 0-.5.5v2.535a.5.5 0 0 0 .25.433A3.498 3.498 0 0 1 5.5 12a3.498 3.498 0 0 1-1.75 3.032.5.5 0 0 0-.25.433V18a.5.5 0 0 0 .5.5h16a.5.5 0 0 0 .5-.5v-2.535a.5.5 0 0 0-.25-.433A3.498 3.498 0 0 1 18.5 12a3.5 3.5 0 0 1 1.75-3.032.5.5 0 0 0 .25-.433V6a.5.5 0 0 0-.5-.5H4ZM2.5 6A1.5 1.5 0 0 1 4 4.5h16A1.5 1.5 0 0 1 21.5 6v3.17a.5.5 0 0 1-.333.472 2.501 2.501 0 0 0 0 4.716.5.5 0 0 1 .333.471V18a1.5 1.5 0 0 1-1.5 1.5H4A1.5 1.5 0 0 1 2.5 18v-3.17a.5.5 0 0 1 .333-.472 2.501 2.501 0 0 0 0-4.716.5.5 0 0 1-.333-.471V6Zm12 2a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Zm0 4a.5.5 0 1 1 1 0 .5.5 0 0 1-1 0Z',
|
5
|
+
)
|
@@ -0,0 +1,17 @@
|
|
1
|
+
import type { SVGProps } from 'react'
|
2
|
+
|
3
|
+
export const makeSvgComponent = (path: string) =>
|
4
|
+
function (
|
5
|
+
props: Omit<SVGProps<SVGSVGElement>, 'viewBox' | 'children' | 'xmlns'>,
|
6
|
+
) {
|
7
|
+
return (
|
8
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" {...props}>
|
9
|
+
<path
|
10
|
+
fill="currentColor"
|
11
|
+
fillRule="evenodd"
|
12
|
+
clipRule="evenodd"
|
13
|
+
d={path}
|
14
|
+
></path>
|
15
|
+
</svg>
|
16
|
+
)
|
17
|
+
}
|
@@ -0,0 +1,45 @@
|
|
1
|
+
import { clsx } from '../lib/clsx'
|
2
|
+
import { Override } from '../lib/util'
|
3
|
+
import { AlertIcon } from './icons/alert-icon'
|
4
|
+
import { InputLayout, InputLayoutProps } from './input-layout'
|
5
|
+
|
6
|
+
export type InfoCardProps = Override<
|
7
|
+
InputLayoutProps,
|
8
|
+
{
|
9
|
+
role: 'alert' | 'status'
|
10
|
+
}
|
11
|
+
>
|
12
|
+
|
13
|
+
export function InfoCard({
|
14
|
+
children,
|
15
|
+
className,
|
16
|
+
role = 'alert',
|
17
|
+
...props
|
18
|
+
}: InfoCardProps) {
|
19
|
+
return (
|
20
|
+
<InputLayout
|
21
|
+
className={clsx(
|
22
|
+
role === 'alert' ? 'bg-error' : 'bg-gray-100 dark:bg-slate-800',
|
23
|
+
className,
|
24
|
+
)}
|
25
|
+
icon={
|
26
|
+
<AlertIcon
|
27
|
+
className={clsx(
|
28
|
+
'fill-current h-4 w-4',
|
29
|
+
role === 'alert' ? 'text-white' : 'text-brand',
|
30
|
+
)}
|
31
|
+
/>
|
32
|
+
}
|
33
|
+
{...props}
|
34
|
+
>
|
35
|
+
<div
|
36
|
+
className={clsx(
|
37
|
+
'py-2 overflow-hidden',
|
38
|
+
role === 'alert' ? 'text-white' : undefined,
|
39
|
+
)}
|
40
|
+
>
|
41
|
+
{children}
|
42
|
+
</div>
|
43
|
+
</InputLayout>
|
44
|
+
)
|
45
|
+
}
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { InputHTMLAttributes, useRef, useState } from 'react'
|
2
|
+
import { InputContainer } from './input-container'
|
3
|
+
|
4
|
+
const generateUniqueId = () => Math.random().toString(36).slice(2)
|
5
|
+
|
6
|
+
export type InputCheckboxProps = Omit<
|
7
|
+
InputHTMLAttributes<HTMLInputElement>,
|
8
|
+
'type'
|
9
|
+
>
|
10
|
+
|
11
|
+
export function InputCheckbox({
|
12
|
+
id,
|
13
|
+
children,
|
14
|
+
className,
|
15
|
+
...props
|
16
|
+
}: InputCheckboxProps) {
|
17
|
+
const [htmlFor] = useState(generateUniqueId)
|
18
|
+
const ref = useRef<HTMLDivElement>(null)
|
19
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
20
|
+
|
21
|
+
return (
|
22
|
+
<InputContainer
|
23
|
+
id={id}
|
24
|
+
ref={ref}
|
25
|
+
icon={
|
26
|
+
<input
|
27
|
+
{...props}
|
28
|
+
ref={inputRef}
|
29
|
+
id={htmlFor}
|
30
|
+
className="text-brand outline-none"
|
31
|
+
type="checkbox"
|
32
|
+
/>
|
33
|
+
}
|
34
|
+
className={className}
|
35
|
+
onClick={(event) => {
|
36
|
+
if (event.target === ref.current && !event.defaultPrevented) {
|
37
|
+
inputRef.current?.click()
|
38
|
+
inputRef.current?.focus()
|
39
|
+
}
|
40
|
+
}}
|
41
|
+
>
|
42
|
+
<label htmlFor={htmlFor} className="block w-full leading-[1.6]">
|
43
|
+
{children}
|
44
|
+
</label>
|
45
|
+
</InputContainer>
|
46
|
+
)
|
47
|
+
}
|
@@ -0,0 +1,37 @@
|
|
1
|
+
import { forwardRef, useState } from 'react'
|
2
|
+
import { clsx } from '../lib/clsx'
|
3
|
+
import { InputLayout, InputLayoutProps } from './input-layout'
|
4
|
+
|
5
|
+
export type InputContainerProps = InputLayoutProps
|
6
|
+
|
7
|
+
export const InputContainer = forwardRef<HTMLDivElement, InputContainerProps>(
|
8
|
+
({ className, onFocus, icon, onBlur, ...props }, ref) => {
|
9
|
+
const [focused, setFocused] = useState(false)
|
10
|
+
|
11
|
+
return (
|
12
|
+
<InputLayout
|
13
|
+
ref={ref}
|
14
|
+
className={clsx(
|
15
|
+
// Background
|
16
|
+
'bg-gray-100 has-[:focus]:bg-slate-200',
|
17
|
+
'dark:bg-slate-800 dark:has-[:focus]:bg-slate-700',
|
18
|
+
// Border
|
19
|
+
'outline-none',
|
20
|
+
'border-solid border-2 border-transparent hover:border-gray-400 has-[:focus]:border-brand hover:has-[:focus]:border-brand',
|
21
|
+
'dark:hover:border-gray-500',
|
22
|
+
className,
|
23
|
+
)}
|
24
|
+
onFocus={(event) => {
|
25
|
+
onFocus?.(event)
|
26
|
+
if (!event.defaultPrevented) setFocused(true)
|
27
|
+
}}
|
28
|
+
onBlur={(event) => {
|
29
|
+
onBlur?.(event)
|
30
|
+
if (!event.defaultPrevented) setFocused(false)
|
31
|
+
}}
|
32
|
+
icon={<div className={focused ? 'text-brand' : undefined}>{icon}</div>}
|
33
|
+
{...props}
|
34
|
+
/>
|
35
|
+
)
|
36
|
+
},
|
37
|
+
)
|
@@ -0,0 +1,47 @@
|
|
1
|
+
import { forwardRef, HTMLAttributes, ReactNode } from 'react'
|
2
|
+
import { clsx } from '../lib/clsx'
|
3
|
+
import { Override } from '../lib/util'
|
4
|
+
|
5
|
+
export type InputLayoutProps = Override<
|
6
|
+
HTMLAttributes<HTMLDivElement>,
|
7
|
+
{
|
8
|
+
icon?: ReactNode
|
9
|
+
append?: ReactNode
|
10
|
+
}
|
11
|
+
>
|
12
|
+
|
13
|
+
export const InputLayout = forwardRef<HTMLDivElement, InputLayoutProps>(
|
14
|
+
({ className, icon, append, children, ...props }, ref) => {
|
15
|
+
return (
|
16
|
+
<div
|
17
|
+
ref={ref}
|
18
|
+
className={clsx(
|
19
|
+
// Layout
|
20
|
+
'pl-1 pr-2', // Less padding on the left because icon will provide some
|
21
|
+
'min-h-12',
|
22
|
+
'flex items-center justify-stretch',
|
23
|
+
// Border
|
24
|
+
'rounded-lg',
|
25
|
+
// Font
|
26
|
+
'text-gray-700',
|
27
|
+
'dark:text-gray-100',
|
28
|
+
className,
|
29
|
+
)}
|
30
|
+
{...props}
|
31
|
+
>
|
32
|
+
<div
|
33
|
+
className={clsx(
|
34
|
+
'self-start shrink-0 grow-0',
|
35
|
+
'w-8 h-12',
|
36
|
+
'flex items-center justify-center',
|
37
|
+
'text-gray-500',
|
38
|
+
)}
|
39
|
+
>
|
40
|
+
{icon}
|
41
|
+
</div>
|
42
|
+
<div className="flex-auto relative">{children}</div>
|
43
|
+
{append && <div className="grow-0 shrink-0">{append}</div>}
|
44
|
+
</div>
|
45
|
+
)
|
46
|
+
},
|
47
|
+
)
|
@@ -0,0 +1,69 @@
|
|
1
|
+
import {
|
2
|
+
forwardRef,
|
3
|
+
InputHTMLAttributes,
|
4
|
+
MouseEventHandler,
|
5
|
+
ReactNode,
|
6
|
+
useCallback,
|
7
|
+
useImperativeHandle,
|
8
|
+
useRef,
|
9
|
+
useState,
|
10
|
+
} from 'react'
|
11
|
+
import { InputContainer } from './input-container'
|
12
|
+
|
13
|
+
export const InputText = forwardRef<
|
14
|
+
HTMLInputElement,
|
15
|
+
{
|
16
|
+
icon?: ReactNode
|
17
|
+
} & InputHTMLAttributes<HTMLInputElement>
|
18
|
+
>(({ className, icon, children, onFocus, onBlur, ...props }, ref) => {
|
19
|
+
const [focused, setFocused] = useState(false)
|
20
|
+
const inputRef = useRef<HTMLInputElement>(null)
|
21
|
+
|
22
|
+
useImperativeHandle(ref, () => inputRef.current!, [])
|
23
|
+
|
24
|
+
const handleClick = useCallback<MouseEventHandler<HTMLDivElement>>(
|
25
|
+
(event) => {
|
26
|
+
if (inputRef.current !== event.target) {
|
27
|
+
event.preventDefault()
|
28
|
+
event.stopPropagation()
|
29
|
+
inputRef.current?.focus()
|
30
|
+
}
|
31
|
+
},
|
32
|
+
[],
|
33
|
+
)
|
34
|
+
|
35
|
+
const handleMouseDown = useCallback<MouseEventHandler<HTMLDivElement>>(
|
36
|
+
(event) => {
|
37
|
+
if (focused && event.target !== inputRef.current) {
|
38
|
+
// Prevent "blur" event from firing when clicking outside the input
|
39
|
+
event.preventDefault()
|
40
|
+
event.stopPropagation()
|
41
|
+
}
|
42
|
+
},
|
43
|
+
[focused],
|
44
|
+
)
|
45
|
+
|
46
|
+
return (
|
47
|
+
<InputContainer
|
48
|
+
icon={icon}
|
49
|
+
className={className}
|
50
|
+
onClick={handleClick}
|
51
|
+
onMouseDown={handleMouseDown}
|
52
|
+
>
|
53
|
+
<input
|
54
|
+
ref={inputRef}
|
55
|
+
className="w-full bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder-gray-500"
|
56
|
+
onFocus={(event) => {
|
57
|
+
setFocused(true)
|
58
|
+
onFocus?.(event)
|
59
|
+
}}
|
60
|
+
onBlur={(event) => {
|
61
|
+
setFocused(false)
|
62
|
+
onBlur?.(event)
|
63
|
+
}}
|
64
|
+
{...props}
|
65
|
+
/>
|
66
|
+
{children}
|
67
|
+
</InputContainer>
|
68
|
+
)
|
69
|
+
})
|