@atproto/oauth-provider 0.1.0 → 0.1.2-rc.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (118) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/account/account-manager.d.ts +2 -2
  3. package/dist/account/account-manager.d.ts.map +1 -1
  4. package/dist/account/account-manager.js.map +1 -1
  5. package/dist/account/account-store.d.ts +19 -6
  6. package/dist/account/account-store.d.ts.map +1 -1
  7. package/dist/account/account-store.js +16 -1
  8. package/dist/account/account-store.js.map +1 -1
  9. package/dist/assets/app/bundle-manifest.json +3 -3
  10. package/dist/assets/app/main.css +1 -1
  11. package/dist/assets/app/main.js +3 -3
  12. package/dist/assets/app/main.js.map +1 -1
  13. package/dist/client/client-auth.d.ts.map +1 -1
  14. package/dist/client/client-auth.js +2 -2
  15. package/dist/client/client-auth.js.map +1 -1
  16. package/dist/client/client-manager.d.ts.map +1 -1
  17. package/dist/client/client-manager.js +3 -1
  18. package/dist/client/client-manager.js.map +1 -1
  19. package/dist/client/client.d.ts.map +1 -1
  20. package/dist/client/client.js +3 -3
  21. package/dist/client/client.js.map +1 -1
  22. package/dist/dpop/dpop-manager.d.ts.map +1 -1
  23. package/dist/dpop/dpop-manager.js +3 -3
  24. package/dist/dpop/dpop-manager.js.map +1 -1
  25. package/dist/errors/invalid-token-error.d.ts.map +1 -1
  26. package/dist/errors/invalid-token-error.js +3 -2
  27. package/dist/errors/invalid-token-error.js.map +1 -1
  28. package/dist/errors/second-authentication-factor-required-error.d.ts +13 -0
  29. package/dist/errors/second-authentication-factor-required-error.d.ts.map +1 -0
  30. package/dist/errors/second-authentication-factor-required-error.js +23 -0
  31. package/dist/errors/second-authentication-factor-required-error.js.map +1 -0
  32. package/dist/lib/util/authorization-header.js +1 -1
  33. package/dist/lib/util/authorization-header.js.map +1 -1
  34. package/dist/metadata/build-metadata.d.ts.map +1 -1
  35. package/dist/metadata/build-metadata.js +2 -0
  36. package/dist/metadata/build-metadata.js.map +1 -1
  37. package/dist/oauth-errors.d.ts +1 -0
  38. package/dist/oauth-errors.d.ts.map +1 -1
  39. package/dist/oauth-errors.js +3 -1
  40. package/dist/oauth-errors.js.map +1 -1
  41. package/dist/oauth-provider.d.ts +101 -4
  42. package/dist/oauth-provider.d.ts.map +1 -1
  43. package/dist/oauth-provider.js +98 -110
  44. package/dist/oauth-provider.js.map +1 -1
  45. package/dist/output/build-authorize-data.d.ts +40 -0
  46. package/dist/output/build-authorize-data.d.ts.map +1 -0
  47. package/dist/output/build-authorize-data.js +22 -0
  48. package/dist/output/build-authorize-data.js.map +1 -0
  49. package/dist/output/build-error-payload.d.ts.map +1 -1
  50. package/dist/output/build-error-payload.js +4 -3
  51. package/dist/output/build-error-payload.js.map +1 -1
  52. package/dist/output/customization.d.ts +2 -12
  53. package/dist/output/customization.d.ts.map +1 -1
  54. package/dist/output/customization.js +59 -32
  55. package/dist/output/customization.js.map +1 -1
  56. package/dist/output/output-manager.d.ts +16 -0
  57. package/dist/output/output-manager.d.ts.map +1 -0
  58. package/dist/output/output-manager.js +69 -0
  59. package/dist/output/output-manager.js.map +1 -0
  60. package/dist/output/send-web-page.d.ts +1 -1
  61. package/dist/output/send-web-page.d.ts.map +1 -1
  62. package/dist/output/send-web-page.js +3 -2
  63. package/dist/output/send-web-page.js.map +1 -1
  64. package/package.json +7 -7
  65. package/src/account/account-manager.ts +2 -2
  66. package/src/account/account-store.ts +12 -6
  67. package/src/assets/app/components/accept-form.tsx +86 -83
  68. package/src/assets/app/components/account-picker.tsx +98 -79
  69. package/src/assets/app/components/button.tsx +34 -0
  70. package/src/assets/app/components/client-identifier.tsx +12 -13
  71. package/src/assets/app/components/fieldset.tsx +26 -0
  72. package/src/assets/app/components/form-card.tsx +47 -0
  73. package/src/assets/app/components/help-card.tsx +1 -1
  74. package/src/assets/app/components/icons/alert-icon.tsx +5 -0
  75. package/src/assets/app/components/icons/at-symbol-icon.tsx +5 -0
  76. package/src/assets/app/components/icons/caret-right-icon.tsx +5 -0
  77. package/src/assets/app/components/icons/lock-icon.tsx +5 -0
  78. package/src/assets/app/components/icons/token-icon.tsx +5 -0
  79. package/src/assets/app/components/icons/util.tsx +17 -0
  80. package/src/assets/app/components/info-card.tsx +45 -0
  81. package/src/assets/app/components/input-checkbox.tsx +47 -0
  82. package/src/assets/app/components/input-container.tsx +37 -0
  83. package/src/assets/app/components/input-layout.tsx +47 -0
  84. package/src/assets/app/components/input-text.tsx +69 -0
  85. package/src/assets/app/components/layout-title-page.tsx +33 -16
  86. package/src/assets/app/components/layout-welcome.tsx +30 -14
  87. package/src/assets/app/components/sign-in-form.tsx +214 -196
  88. package/src/assets/app/components/sign-up-account-form.tsx +101 -117
  89. package/src/assets/app/components/sign-up-disclaimer.tsx +1 -1
  90. package/src/assets/app/hooks/use-api.ts +2 -0
  91. package/src/assets/app/lib/api.ts +49 -14
  92. package/src/assets/app/lib/clsx.ts +6 -1
  93. package/src/assets/app/lib/util.ts +3 -0
  94. package/src/assets/app/main.css +2 -1
  95. package/src/assets/app/views/accept-view.tsx +4 -3
  96. package/src/assets/app/views/authorize-view.tsx +8 -4
  97. package/src/assets/app/views/error-view.tsx +24 -15
  98. package/src/assets/app/views/sign-in-view.tsx +5 -15
  99. package/src/assets/app/views/sign-up-view.tsx +3 -10
  100. package/src/assets/app/views/welcome-view.tsx +11 -18
  101. package/src/client/client-auth.ts +3 -2
  102. package/src/client/client-manager.ts +2 -1
  103. package/src/client/client.ts +3 -1
  104. package/src/dpop/dpop-manager.ts +3 -2
  105. package/src/errors/invalid-token-error.ts +3 -1
  106. package/src/errors/second-authentication-factor-required-error.ts +25 -0
  107. package/src/lib/util/authorization-header.ts +1 -1
  108. package/src/metadata/build-metadata.ts +3 -0
  109. package/src/oauth-errors.ts +1 -0
  110. package/src/oauth-provider.ts +110 -99
  111. package/src/output/{send-authorize-page.ts → build-authorize-data.ts} +3 -43
  112. package/src/output/build-error-payload.ts +3 -1
  113. package/src/output/customization.ts +67 -45
  114. package/src/output/output-manager.ts +87 -0
  115. package/src/output/send-web-page.ts +4 -3
  116. package/tailwind.config.js +14 -1
  117. package/src/assets/app/components/error-card.tsx +0 -41
  118. package/src/output/send-error-page.ts +0 -41
@@ -1,21 +1,30 @@
1
- import type { HTMLAttributes, ReactNode } from 'react'
1
+ import type { ReactNode } from 'react'
2
2
  import { Account } from '../backend-data'
3
- import { clsx } from '../lib/clsx'
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
- accounts: readonly Account[]
11
+ export type AccountPickerProps = Override<
12
+ FormCardProps,
13
+ {
14
+ accounts: readonly Account[]
7
15
 
8
- onAccount: (account: Account) => void
9
- accountAria?: (account: Account) => string
16
+ onAccount: (account: Account) => void
17
+ accountAria?: (account: Account) => string
10
18
 
11
- onOther?: () => void
12
- otherLabel?: ReactNode
13
- otherAria?: string
19
+ onOther?: () => void
20
+ otherLabel?: ReactNode
21
+ otherAria?: string
14
22
 
15
- onBack?: () => void
16
- backLabel?: ReactNode
17
- backAria?: string
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 = 'Other account',
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
- className,
35
- ...attrs
36
- }: AccountPickerProps & HTMLAttributes<HTMLDivElement>) {
43
+ ...props
44
+ }: AccountPickerProps) {
37
45
  return (
38
- <div {...attrs} className={clsx('flex flex-col', className)}>
39
- <p className="font-medium p-4">Sign in as...</p>
40
-
41
- {accounts.map((account) => {
42
- const [name, identifier] = [
43
- account.name,
44
- account.preferred_username,
45
- account.email,
46
- account.sub,
47
- ].filter(Boolean) as [string, string?]
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
- return (
50
- <button
51
- key={account.sub}
52
- className="cursor-pointer text-start flex items-center justify-between py-2 px-6 border-t border-b -mb-px hover:bg-slate-100 border-slate-200 dark:border-slate-700 dark:hover:bg-slate-900"
53
- onClick={() => onAccount(account)}
54
- role="button"
55
- aria-label={accountAria(account)}
56
- >
57
- <div className="pr-2 flex items-center justify-start max-w-full overflow-hidden">
58
- {account.picture && (
59
- <img
60
- crossOrigin="anonymous"
61
- src={account.picture}
62
- alt={name}
63
- className="w-8 h-8 mr-2 rounded-full"
64
- />
65
- )}
66
- <div className="min-w-0 my-2 flex-auto truncate">
67
- <span className="font-semibold">{name}</span>
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="ml-2 text-sm text-neutral-500 dark:text-neutral-400">
102
+ <span className="text-sm text-neutral-500 dark:text-neutral-400 truncate">
70
103
  {identifier}
71
104
  </span>
72
105
  )}
73
- </div>
74
- </div>
75
- <span className="scale-x-50 font-semibold text-xl">&gt;</span>
76
- </button>
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">&gt;</span>
89
- </button>
90
- )}
91
-
92
- <div className="flex-auto" />
106
+ </span>
107
+ </InputContainer>
108
+ )
109
+ })}
93
110
 
94
- {onBack && (
95
- <div className="p-4 flex flex-wrap items-center justify-between">
96
- <button
97
- type="button"
98
- onClick={() => onBack()}
99
- className="py-2 bg-transparent text-primary rounded-md font-light"
100
- aria-label={backAria}
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
- {backLabel}
103
- </button>
104
- </div>
105
- )}
106
- </div>
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 { OAuthClientMetadata } from '@atproto/oauth-types'
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 (clientMetadata.client_uri) {
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
- // Fallback to the client ID
31
- return <As {...attrs}>{clientId}</As>
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
+ )
@@ -33,7 +33,7 @@ export function HelpCard({
33
33
  href={helpLink.href}
34
34
  rel={helpLink.rel}
35
35
  target="_blank"
36
- className="text-primary"
36
+ className="text-brand"
37
37
  >
38
38
  Contact {helpLink.title}
39
39
  </a>
@@ -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 CaretRightIcon = makeSvgComponent(
4
+ 'M8.293 3.293a1 1 0 0 1 1.414 0l8 8a1 1 0 0 1 0 1.414l-8 8a1 1 0 0 1-1.414-1.414L15.586 12 8.293 4.707a1 1 0 0 1 0-1.414Z',
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
+ })