@atproto/oauth-provider-ui 0.0.2

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 (208) hide show
  1. package/.linguirc +57 -0
  2. package/CHANGELOG.md +7 -0
  3. package/CONTRIBUTING.md +6 -0
  4. package/LICENSE.txt +7 -0
  5. package/dist/assets/COdVzed-.css +3 -0
  6. package/dist/assets/COdVzed-.js +100 -0
  7. package/dist/assets/COdVzed-.js.map +1 -0
  8. package/dist/assets/Cqnfnbvc.js +6 -0
  9. package/dist/assets/Cqnfnbvc.js.map +1 -0
  10. package/dist/assets/bundle-manifest.json +630 -0
  11. package/dist/assets/error-view-Bu4y7Nd8.js +208 -0
  12. package/dist/assets/error-view-Bu4y7Nd8.js.map +1 -0
  13. package/dist/assets/index-DXlCRM6V.js +36 -0
  14. package/dist/assets/index-DXlCRM6V.js.map +1 -0
  15. package/dist/assets/messages-2GoTm2qL.js +4 -0
  16. package/dist/assets/messages-2GoTm2qL.js.map +1 -0
  17. package/dist/assets/messages-6Cn2Jbhw.js +4 -0
  18. package/dist/assets/messages-6Cn2Jbhw.js.map +1 -0
  19. package/dist/assets/messages-75hFgOK2.js +4 -0
  20. package/dist/assets/messages-75hFgOK2.js.map +1 -0
  21. package/dist/assets/messages-B3OK4k0O.js +4 -0
  22. package/dist/assets/messages-B3OK4k0O.js.map +1 -0
  23. package/dist/assets/messages-BNXlPzKV.js +4 -0
  24. package/dist/assets/messages-BNXlPzKV.js.map +1 -0
  25. package/dist/assets/messages-BUygB8mD.js +4 -0
  26. package/dist/assets/messages-BUygB8mD.js.map +1 -0
  27. package/dist/assets/messages-BVPPcwNr.js +4 -0
  28. package/dist/assets/messages-BVPPcwNr.js.map +1 -0
  29. package/dist/assets/messages-BbbWUQS8.js +4 -0
  30. package/dist/assets/messages-BbbWUQS8.js.map +1 -0
  31. package/dist/assets/messages-BibKCYyW.js +4 -0
  32. package/dist/assets/messages-BibKCYyW.js.map +1 -0
  33. package/dist/assets/messages-BlPrr9_7.js +4 -0
  34. package/dist/assets/messages-BlPrr9_7.js.map +1 -0
  35. package/dist/assets/messages-ByVCw40U.js +4 -0
  36. package/dist/assets/messages-ByVCw40U.js.map +1 -0
  37. package/dist/assets/messages-C5DU1neP.js +4 -0
  38. package/dist/assets/messages-C5DU1neP.js.map +1 -0
  39. package/dist/assets/messages-C6IgUtbX.js +4 -0
  40. package/dist/assets/messages-C6IgUtbX.js.map +1 -0
  41. package/dist/assets/messages-C92Zzt2o.js +4 -0
  42. package/dist/assets/messages-C92Zzt2o.js.map +1 -0
  43. package/dist/assets/messages-CGZqYT14.js +4 -0
  44. package/dist/assets/messages-CGZqYT14.js.map +1 -0
  45. package/dist/assets/messages-CGlsy4wt.js +4 -0
  46. package/dist/assets/messages-CGlsy4wt.js.map +1 -0
  47. package/dist/assets/messages-CPT1nd0u.js +4 -0
  48. package/dist/assets/messages-CPT1nd0u.js.map +1 -0
  49. package/dist/assets/messages-CTTdXyw_.js +4 -0
  50. package/dist/assets/messages-CTTdXyw_.js.map +1 -0
  51. package/dist/assets/messages-ChK_C_Pj.js +4 -0
  52. package/dist/assets/messages-ChK_C_Pj.js.map +1 -0
  53. package/dist/assets/messages-CjJbk7Uf.js +4 -0
  54. package/dist/assets/messages-CjJbk7Uf.js.map +1 -0
  55. package/dist/assets/messages-CoiLjLYO.js +4 -0
  56. package/dist/assets/messages-CoiLjLYO.js.map +1 -0
  57. package/dist/assets/messages-Cwx6B4Ti.js +4 -0
  58. package/dist/assets/messages-Cwx6B4Ti.js.map +1 -0
  59. package/dist/assets/messages-D0uXAp_H.js +4 -0
  60. package/dist/assets/messages-D0uXAp_H.js.map +1 -0
  61. package/dist/assets/messages-DG0_arU0.js +4 -0
  62. package/dist/assets/messages-DG0_arU0.js.map +1 -0
  63. package/dist/assets/messages-DOXFJh9K.js +4 -0
  64. package/dist/assets/messages-DOXFJh9K.js.map +1 -0
  65. package/dist/assets/messages-DPK7nOoC.js +4 -0
  66. package/dist/assets/messages-DPK7nOoC.js.map +1 -0
  67. package/dist/assets/messages-Duccgtu0.js +4 -0
  68. package/dist/assets/messages-Duccgtu0.js.map +1 -0
  69. package/dist/assets/messages-DxTqgsHq.js +4 -0
  70. package/dist/assets/messages-DxTqgsHq.js.map +1 -0
  71. package/dist/assets/messages-E5_lTg7A.js +4 -0
  72. package/dist/assets/messages-E5_lTg7A.js.map +1 -0
  73. package/dist/assets/messages-UhunAjh1.js +4 -0
  74. package/dist/assets/messages-UhunAjh1.js.map +1 -0
  75. package/dist/assets/messages-Xg_3YLGw.js +4 -0
  76. package/dist/assets/messages-Xg_3YLGw.js.map +1 -0
  77. package/dist/assets/messages-iliBQHY2.js +4 -0
  78. package/dist/assets/messages-iliBQHY2.js.map +1 -0
  79. package/dist/assets/messages-lRprpIl-.js +4 -0
  80. package/dist/assets/messages-lRprpIl-.js.map +1 -0
  81. package/dist/assets/messages-pbPHQbz1.js +4 -0
  82. package/dist/assets/messages-pbPHQbz1.js.map +1 -0
  83. package/dist/assets/messages-q-O7ZQGs.js +4 -0
  84. package/dist/assets/messages-q-O7ZQGs.js.map +1 -0
  85. package/dist/lib/index.d.ts +19 -0
  86. package/dist/lib/index.d.ts.map +1 -0
  87. package/dist/lib/index.js +47 -0
  88. package/dist/lib/index.js.map +1 -0
  89. package/dist/tsconfig.backend.tsbuildinfo +1 -0
  90. package/lib/index.ts +72 -0
  91. package/package.json +73 -0
  92. package/rollup.config.js +102 -0
  93. package/src/authorization-page.html +183 -0
  94. package/src/authorization-page.tsx +55 -0
  95. package/src/backend-data.ts +35 -0
  96. package/src/components/forms/button-toggle-visibility.tsx +43 -0
  97. package/src/components/forms/button.tsx +60 -0
  98. package/src/components/forms/fieldset.tsx +55 -0
  99. package/src/components/forms/form-card-async.tsx +103 -0
  100. package/src/components/forms/form-card.tsx +49 -0
  101. package/src/components/forms/input-checkbox.tsx +78 -0
  102. package/src/components/forms/input-container.tsx +107 -0
  103. package/src/components/forms/input-email-address.tsx +65 -0
  104. package/src/components/forms/input-new-password.tsx +62 -0
  105. package/src/components/forms/input-password.tsx +87 -0
  106. package/src/components/forms/input-text.tsx +82 -0
  107. package/src/components/forms/input-token.tsx +94 -0
  108. package/src/components/forms/wizard-card.tsx +116 -0
  109. package/src/components/layouts/layout-title-page.tsx +77 -0
  110. package/src/components/layouts/layout-welcome.tsx +73 -0
  111. package/src/components/utils/account-identifier.tsx +23 -0
  112. package/src/components/utils/account-image.tsx +33 -0
  113. package/src/components/utils/admonition.tsx +52 -0
  114. package/src/components/utils/client-name.tsx +45 -0
  115. package/src/components/utils/error-card.tsx +93 -0
  116. package/src/components/utils/error-message.tsx +88 -0
  117. package/src/components/utils/help-card.tsx +46 -0
  118. package/src/components/utils/icons.tsx +88 -0
  119. package/src/components/utils/link-anchor.tsx +28 -0
  120. package/src/components/utils/link-title.tsx +26 -0
  121. package/src/components/utils/multi-lang-string.tsx +56 -0
  122. package/src/components/utils/password-strength-label.tsx +37 -0
  123. package/src/components/utils/password-strength-meter.tsx +58 -0
  124. package/src/components/utils/url-viewer.tsx +73 -0
  125. package/src/cookies.ts +11 -0
  126. package/src/error-page.html +125 -0
  127. package/src/error-page.tsx +29 -0
  128. package/src/hooks/use-api.ts +182 -0
  129. package/src/hooks/use-async-action.ts +120 -0
  130. package/src/hooks/use-bound-dispatch.ts +5 -0
  131. package/src/hooks/use-browser-color-scheme.ts +31 -0
  132. package/src/hooks/use-csrf-token.ts +5 -0
  133. package/src/hooks/use-random-string.ts +37 -0
  134. package/src/hooks/use-stepper.ts +87 -0
  135. package/src/index.html +13 -0
  136. package/src/lib/api.ts +234 -0
  137. package/src/lib/backend-data.ts +6 -0
  138. package/src/lib/clsx.ts +6 -0
  139. package/src/lib/json-client.ts +97 -0
  140. package/src/lib/password.ts +98 -0
  141. package/src/lib/ref.ts +17 -0
  142. package/src/lib/util.ts +13 -0
  143. package/src/locales/an/messages.po +487 -0
  144. package/src/locales/ast/messages.po +487 -0
  145. package/src/locales/ca/messages.po +487 -0
  146. package/src/locales/da/messages.po +487 -0
  147. package/src/locales/de/messages.po +487 -0
  148. package/src/locales/el/messages.po +487 -0
  149. package/src/locales/en/messages.po +487 -0
  150. package/src/locales/en-GB/messages.po +487 -0
  151. package/src/locales/es/messages.po +487 -0
  152. package/src/locales/eu/messages.po +487 -0
  153. package/src/locales/fi/messages.po +487 -0
  154. package/src/locales/fr/messages.po +487 -0
  155. package/src/locales/ga/messages.po +487 -0
  156. package/src/locales/gl/messages.po +487 -0
  157. package/src/locales/hi/messages.po +487 -0
  158. package/src/locales/hu/messages.po +487 -0
  159. package/src/locales/ia/messages.po +487 -0
  160. package/src/locales/id/messages.po +487 -0
  161. package/src/locales/it/messages.po +487 -0
  162. package/src/locales/ja/messages.po +487 -0
  163. package/src/locales/km/messages.po +487 -0
  164. package/src/locales/ko/messages.po +487 -0
  165. package/src/locales/load.ts +8 -0
  166. package/src/locales/locale-context.ts +19 -0
  167. package/src/locales/locale-provider.tsx +112 -0
  168. package/src/locales/locale-selector.tsx +58 -0
  169. package/src/locales/locales.ts +168 -0
  170. package/src/locales/ne/messages.po +487 -0
  171. package/src/locales/nl/messages.po +487 -0
  172. package/src/locales/pl/messages.po +487 -0
  173. package/src/locales/pt-BR/messages.po +487 -0
  174. package/src/locales/ro/messages.po +487 -0
  175. package/src/locales/ru/messages.po +487 -0
  176. package/src/locales/sv/messages.po +487 -0
  177. package/src/locales/th/messages.po +487 -0
  178. package/src/locales/tr/messages.po +487 -0
  179. package/src/locales/uk/messages.po +487 -0
  180. package/src/locales/vi/messages.po +487 -0
  181. package/src/locales/zh-CN/messages.po +487 -0
  182. package/src/locales/zh-HK/messages.po +487 -0
  183. package/src/locales/zh-TW/messages.po +487 -0
  184. package/src/styles.css +33 -0
  185. package/src/views/authorize/accept/accept-form.tsx +150 -0
  186. package/src/views/authorize/accept/accept-view.tsx +70 -0
  187. package/src/views/authorize/authorize-view.tsx +183 -0
  188. package/src/views/authorize/reset-password/reset-password-confirm-form.tsx +88 -0
  189. package/src/views/authorize/reset-password/reset-password-request-form.tsx +80 -0
  190. package/src/views/authorize/reset-password/reset-password-view.tsx +127 -0
  191. package/src/views/authorize/sign-in/sign-in-form.tsx +242 -0
  192. package/src/views/authorize/sign-in/sign-in-picker.tsx +116 -0
  193. package/src/views/authorize/sign-in/sign-in-view.tsx +145 -0
  194. package/src/views/authorize/sign-up/sign-up-account-form.tsx +142 -0
  195. package/src/views/authorize/sign-up/sign-up-disclaimer.tsx +51 -0
  196. package/src/views/authorize/sign-up/sign-up-handle-form.tsx +287 -0
  197. package/src/views/authorize/sign-up/sign-up-hcaptcha-form.tsx +108 -0
  198. package/src/views/authorize/sign-up/sign-up-view.tsx +158 -0
  199. package/src/views/authorize/welcome/welcome-view.tsx +56 -0
  200. package/src/views/error/error-view.tsx +31 -0
  201. package/tailwind.config.js +31 -0
  202. package/tsconfig.backend.json +8 -0
  203. package/tsconfig.frontend.json +10 -0
  204. package/tsconfig.frontend.tsbuildinfo +1 -0
  205. package/tsconfig.json +8 -0
  206. package/tsconfig.tools.json +8 -0
  207. package/tsconfig.tools.tsbuildinfo +1 -0
  208. package/vite.config.mjs +16 -0
@@ -0,0 +1,103 @@
1
+ import { Trans } from '@lingui/react/macro'
2
+ import { FormEvent, ReactNode, useCallback } from 'react'
3
+ import {
4
+ UseAsyncActionOptions,
5
+ useAsyncAction,
6
+ } from '../../hooks/use-async-action.ts'
7
+ import { Override } from '../../lib/util.ts'
8
+ import { ErrorCard } from '../utils/error-card.tsx'
9
+ import { Button } from './button.tsx'
10
+ import { FormCard, FormCardProps } from './form-card.tsx'
11
+
12
+ export type { AsyncActionController } from '../../hooks/use-async-action.ts'
13
+
14
+ export type ErrorRender = (data: { error: Error }) => ReactNode
15
+ export const errorRenderDefault: ErrorRender = ({ error }) => (
16
+ <ErrorCard error={error} />
17
+ )
18
+
19
+ export type FormCardAsyncProps = Override<
20
+ Override<
21
+ Omit<FormCardProps, 'cancel' | 'actions' | 'prepend'>,
22
+ Pick<UseAsyncActionOptions, 'ref' | 'onLoading' | 'onError'>
23
+ >,
24
+ {
25
+ invalid?: boolean
26
+ disabled?: boolean
27
+
28
+ onSubmit: (signal: AbortSignal) => void | PromiseLike<void>
29
+ submitLabel?: ReactNode
30
+
31
+ onCancel?: () => void
32
+ cancelLabel?: ReactNode
33
+
34
+ errorRender?: ErrorRender
35
+ }
36
+ >
37
+
38
+ export function FormCardAsync({
39
+ invalid,
40
+ disabled,
41
+
42
+ onSubmit,
43
+ submitLabel,
44
+
45
+ onCancel = undefined,
46
+ cancelLabel,
47
+
48
+ errorRender = errorRenderDefault,
49
+
50
+ // UseAsyncActionOptions
51
+ ref,
52
+ onLoading,
53
+ onError,
54
+
55
+ // FormCardProps
56
+ children,
57
+ ...props
58
+ }: FormCardAsyncProps) {
59
+ const { run, loading, error } = useAsyncAction(onSubmit, {
60
+ ref,
61
+ onError,
62
+ onLoading,
63
+ })
64
+
65
+ const doSubmit = useCallback(
66
+ (event: FormEvent<HTMLFormElement>) => {
67
+ event.preventDefault()
68
+
69
+ if (!event.currentTarget.reportValidity()) return
70
+
71
+ if (!disabled && !invalid) void run()
72
+ },
73
+ [disabled, invalid, run],
74
+ )
75
+
76
+ return (
77
+ <FormCard
78
+ {...props}
79
+ onSubmit={doSubmit}
80
+ disabled={disabled || loading}
81
+ prepend={error != null ? errorRender({ error }) : undefined}
82
+ cancel={
83
+ onCancel && (
84
+ <Button onClick={onCancel}>
85
+ {cancelLabel || <Trans>Cancel</Trans>}
86
+ </Button>
87
+ )
88
+ }
89
+ actions={
90
+ <Button
91
+ color="brand"
92
+ type="submit"
93
+ loading={loading}
94
+ disabled={disabled}
95
+ >
96
+ {submitLabel || <Trans>Submit</Trans>}
97
+ </Button>
98
+ }
99
+ >
100
+ {children}
101
+ </FormCard>
102
+ )
103
+ }
@@ -0,0 +1,49 @@
1
+ import { JSX, ReactNode } from 'react'
2
+ import { Override } from '../../lib/util.ts'
3
+
4
+ export type FormCardProps = Override<
5
+ JSX.IntrinsicElements['form'],
6
+ {
7
+ disabled?: boolean
8
+ append?: ReactNode
9
+ prepend?: ReactNode
10
+ cancel?: ReactNode
11
+ actions?: ReactNode
12
+ }
13
+ >
14
+
15
+ export function FormCard({
16
+ actions,
17
+ cancel,
18
+ append,
19
+ children,
20
+ prepend,
21
+ disabled,
22
+
23
+ // form
24
+ inert = disabled,
25
+ ...props
26
+ }: FormCardProps) {
27
+ return (
28
+ <form {...props} inert={inert} className="flex flex-col space-y-4">
29
+ {prepend && <div key="prepend">{prepend}</div>}
30
+
31
+ <div key="children" className="space-y-4">
32
+ {children}
33
+ </div>
34
+
35
+ {append && <div key="append">{append}</div>}
36
+
37
+ {(actions || cancel) && (
38
+ <div
39
+ key="buttons"
40
+ className="flex flex-wrap flex-row-reverse items-center justify-end space-x-reverse space-x-2"
41
+ >
42
+ {actions}
43
+ <div className="flex-auto" />
44
+ {cancel}
45
+ </div>
46
+ )}
47
+ </form>
48
+ )
49
+ }
@@ -0,0 +1,78 @@
1
+ import { JSX, ReactNode, useContext, useRef } from 'react'
2
+ import { useRandomString } from '../../hooks/use-random-string.ts'
3
+ import { clsx } from '../../lib/clsx.ts'
4
+ import { mergeRefs } from '../../lib/ref.ts'
5
+ import { Override } from '../../lib/util.ts'
6
+ import { FieldsetContext } from './fieldset.tsx'
7
+ import { InputContainer } from './input-container.tsx'
8
+
9
+ export type InputCheckboxProps = Override<
10
+ Omit<JSX.IntrinsicElements['input'], 'className' | 'type' | 'children'>,
11
+ {
12
+ className?: string
13
+ children?: ReactNode
14
+ }
15
+ >
16
+
17
+ export function InputCheckbox({
18
+ className,
19
+ children,
20
+
21
+ // input
22
+ id,
23
+ ref,
24
+ disabled,
25
+ title,
26
+ 'aria-label': ariaLabel = title,
27
+ 'aria-labelledby': ariaLabelledBy,
28
+ ...props
29
+ }: InputCheckboxProps) {
30
+ const htmlFor = useRandomString('input-checkbox-')
31
+ const labelRef = useRef<HTMLLabelElement>(null)
32
+ const inputRef = useRef<HTMLInputElement>(null)
33
+ const ctx = useContext(FieldsetContext)
34
+
35
+ const inputId = id ?? htmlFor
36
+
37
+ return (
38
+ <InputContainer
39
+ className={clsx('cursor-pointer', className)}
40
+ icon={
41
+ <input
42
+ {...props}
43
+ disabled={disabled ?? ctx.disabled}
44
+ title={title}
45
+ aria-label={ariaLabel}
46
+ aria-labelledby={
47
+ children
48
+ ? // Prefer the local "<label>" element (through "htmlFor") over the wrapping "<fieldset>" to describe the checkbox.
49
+ undefined
50
+ : ariaLabelledBy ?? ctx.labelId
51
+ }
52
+ ref={mergeRefs([ref, inputRef])}
53
+ id={inputId}
54
+ className="accent-brand outline-none"
55
+ type="checkbox"
56
+ />
57
+ }
58
+ tabIndex={-1}
59
+ onClick={({ target }) => {
60
+ // Native behavior of clicking the label should toggle the checkbox.
61
+ if (target === labelRef.current) return
62
+ if (target === inputRef.current) return
63
+
64
+ inputRef.current?.click()
65
+ }}
66
+ >
67
+ {children && (
68
+ <label
69
+ ref={labelRef}
70
+ htmlFor={inputId}
71
+ className="block w-full leading-[1.6] select-none cursor-pointer"
72
+ >
73
+ {children}
74
+ </label>
75
+ )}
76
+ </InputContainer>
77
+ )
78
+ }
@@ -0,0 +1,107 @@
1
+ import { JSX, ReactNode, useState } from 'react'
2
+ import { clsx } from '../../lib/clsx.ts'
3
+ import { Override } from '../../lib/util.ts'
4
+
5
+ export type InputContainerProps = Override<
6
+ JSX.IntrinsicElements['div'],
7
+ {
8
+ icon: ReactNode
9
+ append?: ReactNode
10
+ bellow?: ReactNode
11
+ }
12
+ >
13
+
14
+ export function InputContainer({
15
+ icon,
16
+ append,
17
+ bellow,
18
+
19
+ // div
20
+ className,
21
+ children,
22
+ onFocus,
23
+ onBlur,
24
+ ...props
25
+ }: InputContainerProps) {
26
+ const [hasFocus, setHasFocus] = useState(false)
27
+
28
+ return (
29
+ <div
30
+ {...props}
31
+ onFocus={(event) => {
32
+ onFocus?.(event)
33
+ if (!event.defaultPrevented) setHasFocus(true)
34
+ }}
35
+ onBlur={(event) => {
36
+ onBlur?.(event)
37
+ if (!event.defaultPrevented) setHasFocus(false)
38
+ }}
39
+ className={clsx(
40
+ // Layout
41
+ 'min-h-12',
42
+ 'max-w-full',
43
+ 'overflow-hidden',
44
+ // Border
45
+ 'rounded-lg',
46
+ className,
47
+ )}
48
+ >
49
+ <div
50
+ className={clsx(
51
+ // Layout
52
+ 'px-1',
53
+ 'w-full min-h-12',
54
+ 'flex items-center justify-stretch',
55
+ // Border
56
+ 'rounded-lg',
57
+ bellow ? 'rounded-br-none rounded-bl-none' : undefined,
58
+ 'outline-none',
59
+ 'border-solid border-2 border-transparent',
60
+ 'focus:border-brand has-[:focus]:border-brand',
61
+ 'hover:border-gray-400 hover:focus:border-gray-400',
62
+ 'dark:hover:border-gray-500 dark:hover:focus:border-gray-500',
63
+ // Background
64
+ 'bg-gray-100 focus:bg-slate-200 has-[:focus]:bg-slate-200',
65
+ 'dark:bg-slate-800 dark:focus:bg-slate-700 dark:has-[:focus]:bg-slate-700',
66
+ // Font
67
+ 'text-slate-600 dark:text-slate-300',
68
+ 'accent-brand',
69
+ )}
70
+ >
71
+ {icon && (
72
+ <div
73
+ className={clsx(
74
+ 'shrink-0 grow-0',
75
+ 'mx-1',
76
+ hasFocus ? 'text-brand' : 'text-slate-500',
77
+ )}
78
+ >
79
+ {icon}
80
+ </div>
81
+ )}
82
+
83
+ {children}
84
+
85
+ <div className="ml-1 grow-0 shrink-0 flex items-center">{append}</div>
86
+ </div>
87
+ {bellow && (
88
+ <div
89
+ className={clsx(
90
+ // Layout
91
+ 'px-3 py-2 space-x-2',
92
+ 'flex flex-row items-center gap-1',
93
+ // Border
94
+ 'rounded-br-2 rounded-bl-2',
95
+ // Background
96
+ 'bg-gray-200 dark:bg-slate-700',
97
+ // Font
98
+ 'text-gray-700 dark:text-gray-300',
99
+ 'text-sm italic',
100
+ )}
101
+ >
102
+ {bellow}
103
+ </div>
104
+ )}
105
+ </div>
106
+ )
107
+ }
@@ -0,0 +1,65 @@
1
+ import { useLingui } from '@lingui/react/macro'
2
+ import { ChangeEvent, useCallback, useState } from 'react'
3
+ import { Override } from '../../lib/util.ts'
4
+ import { AtSymbolIcon } from '../utils/icons.tsx'
5
+ import { InputText, InputTextProps } from './input-text.tsx'
6
+
7
+ export type InputEmailAddressProps = Override<
8
+ Omit<InputTextProps, 'type'>,
9
+ {
10
+ onEmail?: (email: string | undefined) => void
11
+ }
12
+ >
13
+
14
+ export function InputEmailAddress({
15
+ onEmail,
16
+
17
+ // InputTextProps
18
+ autoCapitalize = 'none',
19
+ autoComplete = 'email',
20
+ autoCorrect = 'off',
21
+ dir = 'auto',
22
+ icon = <AtSymbolIcon className="w-5" />,
23
+ onBlur,
24
+ onChange,
25
+ pattern = '^[^@]+@[^@]+\\.[^@]+$',
26
+ spellCheck = 'false',
27
+ value,
28
+ defaultValue = value,
29
+ title,
30
+ ...props
31
+ }: InputEmailAddressProps) {
32
+ const { t } = useLingui()
33
+ const [email, setEmail] = useState<string>(
34
+ typeof defaultValue === 'string' ? defaultValue : '',
35
+ )
36
+
37
+ const doChange = useCallback(
38
+ (event: ChangeEvent<HTMLInputElement>) => {
39
+ const email = event.target.value.toLowerCase()
40
+
41
+ setEmail(email)
42
+ onChange?.(event)
43
+ onEmail?.(event.target.validity.valid ? email : undefined)
44
+ },
45
+ [onChange, onEmail],
46
+ )
47
+
48
+ return (
49
+ <InputText
50
+ {...props}
51
+ title={title ?? t`Email address`}
52
+ type="email"
53
+ autoCapitalize={autoCapitalize}
54
+ autoCorrect={autoCorrect}
55
+ dir={dir}
56
+ spellCheck={spellCheck}
57
+ icon={icon}
58
+ pattern={pattern}
59
+ autoComplete={autoComplete}
60
+ value={email}
61
+ onChange={doChange}
62
+ onBlur={onBlur}
63
+ />
64
+ )
65
+ }
@@ -0,0 +1,62 @@
1
+ import { useLingui } from '@lingui/react/macro'
2
+ import { ChangeEvent, useCallback, useState } from 'react'
3
+ import { MIN_PASSWORD_LENGTH } from '../../lib/password.ts'
4
+ import { Override } from '../../lib/util.ts'
5
+ import { PasswordStrengthLabel } from '../utils/password-strength-label.tsx'
6
+ import { PasswordStrengthMeter } from '../utils/password-strength-meter.tsx'
7
+ import { InputPassword, InputPasswordProps } from './input-password.tsx'
8
+
9
+ export type InputNewPasswordProps = Override<
10
+ Omit<InputPasswordProps, 'value' | 'defaultValue'>,
11
+ {
12
+ password?: string
13
+ onPassword?: (password: undefined | string) => void
14
+ }
15
+ >
16
+
17
+ export function InputNewPassword({
18
+ password: passwordInit = '',
19
+ onPassword,
20
+
21
+ // InputPasswordProps
22
+ onChange,
23
+ autoComplete = 'new-password',
24
+ minLength = MIN_PASSWORD_LENGTH,
25
+ ...props
26
+ }: InputNewPasswordProps) {
27
+ const { t } = useLingui()
28
+ const [password, setPassword] = useState<string>(passwordInit)
29
+
30
+ const doChange = useCallback(
31
+ (event: ChangeEvent<HTMLInputElement>) => {
32
+ const { value } = event.target
33
+ onChange?.(event)
34
+ if (event.defaultPrevented) return
35
+ setPassword(value)
36
+ onPassword?.(event.target.validity.valid ? value : undefined)
37
+ },
38
+ [onChange, onPassword],
39
+ )
40
+
41
+ return (
42
+ <InputPassword
43
+ {...props}
44
+ placeholder={t`Enter a password`}
45
+ aria-label={t`Enter your new password`}
46
+ title={t`Password with at least ${MIN_PASSWORD_LENGTH} characters`}
47
+ minLength={minLength}
48
+ onChange={doChange}
49
+ value={password}
50
+ autoComplete={autoComplete}
51
+ bellow={
52
+ <>
53
+ <PasswordStrengthMeter password={password} />
54
+ <PasswordStrengthLabel
55
+ className="grow-1 min-w-max text-xs text-gray-500 dark:text-gray-400"
56
+ password={password}
57
+ />
58
+ </>
59
+ }
60
+ />
61
+ )
62
+ }
@@ -0,0 +1,87 @@
1
+ import { useLingui } from '@lingui/react/macro'
2
+ import { ChangeEvent, useCallback, useRef, useState } from 'react'
3
+ import { mergeRefs } from '../../lib/ref.ts'
4
+ import { Override } from '../../lib/util.ts'
5
+ import { LockIcon } from '../utils/icons.tsx'
6
+ import { ButtonToggleVisibility } from './button-toggle-visibility.tsx'
7
+ import { InputText, InputTextProps } from './input-text.tsx'
8
+
9
+ export type InputPasswordProps = Override<
10
+ Omit<InputTextProps, 'type' | 'children'>,
11
+ {
12
+ autoHide?: boolean
13
+ }
14
+ >
15
+
16
+ export function InputPassword({
17
+ autoHide = true,
18
+
19
+ // InputTextProps
20
+ onBlur,
21
+ onChange,
22
+ append,
23
+ autoComplete = 'current-password',
24
+ icon = <LockIcon className="w-5" />,
25
+ value,
26
+ defaultValue = value,
27
+ ref,
28
+ title,
29
+ dir = 'auto',
30
+ autoCapitalize = 'none',
31
+ autoCorrect = 'off',
32
+ spellCheck = 'false',
33
+ ...props
34
+ }: InputPasswordProps) {
35
+ const { t } = useLingui()
36
+ const inputRef = useRef<HTMLInputElement>(null)
37
+ const [visible, setVisible] = useState<boolean>(false)
38
+ const [password, setPassword] = useState<string>(
39
+ typeof defaultValue === 'string' ? defaultValue : '',
40
+ )
41
+
42
+ const doChange = useCallback(
43
+ (event: ChangeEvent<HTMLInputElement>) => {
44
+ onChange?.(event)
45
+ setPassword(event.target.value)
46
+ },
47
+ [onChange],
48
+ )
49
+
50
+ return (
51
+ <InputText
52
+ {...props}
53
+ title={title ?? t`Password`}
54
+ ref={mergeRefs([ref, inputRef])}
55
+ dir={dir}
56
+ autoCapitalize={autoCapitalize}
57
+ autoCorrect={autoCorrect}
58
+ spellCheck={spellCheck}
59
+ icon={icon}
60
+ onBlur={
61
+ autoHide
62
+ ? (event) => {
63
+ onBlur?.(event)
64
+ if (!event.defaultPrevented) setVisible(false)
65
+ }
66
+ : onBlur
67
+ }
68
+ value={password}
69
+ onChange={doChange}
70
+ type={visible ? 'text' : 'password'}
71
+ autoComplete={autoComplete}
72
+ append={
73
+ <>
74
+ <ButtonToggleVisibility
75
+ className="m-1"
76
+ visible={visible}
77
+ toggleVisible={() => {
78
+ setVisible((prev) => !prev)
79
+ inputRef.current?.focus()
80
+ }}
81
+ />
82
+ {append}
83
+ </>
84
+ }
85
+ />
86
+ )
87
+ }
@@ -0,0 +1,82 @@
1
+ import { JSX, ReactNode, useContext, useRef } from 'react'
2
+ import { clsx } from '../../lib/clsx.ts'
3
+ import { mergeRefs } from '../../lib/ref.ts'
4
+ import { Override } from '../../lib/util.ts'
5
+ import { FieldsetContext } from './fieldset.tsx'
6
+ import { InputContainer } from './input-container.tsx'
7
+
8
+ export type InputTextProps = Override<
9
+ Omit<JSX.IntrinsicElements['input'], 'children'>,
10
+ {
11
+ icon?: ReactNode
12
+ append?: ReactNode
13
+ bellow?: ReactNode
14
+ className?: string
15
+ }
16
+ >
17
+
18
+ export function InputText({
19
+ icon,
20
+ append,
21
+ bellow,
22
+ className,
23
+
24
+ // input
25
+ onFocus,
26
+ onBlur,
27
+ ref,
28
+ disabled,
29
+ title,
30
+ 'aria-label': ariaLabel = title,
31
+ 'aria-labelledby': ariaLabelledBy,
32
+ placeholder = ariaLabel,
33
+ ...props
34
+ }: InputTextProps) {
35
+ const ctx = useContext(FieldsetContext)
36
+
37
+ const inputRef = useRef<HTMLInputElement>(null)
38
+ const focusedRef = useRef(false) // ref instead of state to avoid re-renders
39
+
40
+ return (
41
+ <InputContainer
42
+ icon={icon}
43
+ append={append}
44
+ bellow={bellow}
45
+ className={clsx('cursor-text', className)}
46
+ tabIndex={-1}
47
+ onClick={(event) => {
48
+ if (inputRef.current !== event.target) {
49
+ event.preventDefault()
50
+ event.stopPropagation()
51
+ inputRef.current?.focus()
52
+ }
53
+ }}
54
+ onMouseDown={(event) => {
55
+ if (focusedRef.current && event.target !== inputRef.current) {
56
+ // Prevent "blur" event from firing when clicking outside the input
57
+ event.preventDefault()
58
+ event.stopPropagation()
59
+ }
60
+ }}
61
+ >
62
+ <input
63
+ {...props}
64
+ disabled={disabled ?? ctx.disabled}
65
+ title={title}
66
+ placeholder={placeholder}
67
+ aria-label={ariaLabel}
68
+ aria-labelledby={ariaLabelledBy ?? ctx.labelId}
69
+ ref={mergeRefs([ref, inputRef])}
70
+ className="w-full bg-transparent bg-clip-padding text-base text-inherit outline-none dark:placeholder-gray-500 text-ellipsis"
71
+ onFocus={(event) => {
72
+ onFocus?.(event)
73
+ if (!event.defaultPrevented) focusedRef.current = true
74
+ }}
75
+ onBlur={(event) => {
76
+ onBlur?.(event)
77
+ if (!event.defaultPrevented) focusedRef.current = false
78
+ }}
79
+ />
80
+ </InputContainer>
81
+ )
82
+ }