@atproto/oauth-provider 0.1.0 → 0.1.2-rc.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 (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
+ })