@oxy.so/services 6.1.0 → 6.2.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 (59) hide show
  1. package/lib/commonjs/ui/components/OxyAuthChooser.js +7 -6
  2. package/lib/commonjs/ui/components/OxyAuthChooser.js.map +1 -1
  3. package/lib/commonjs/ui/components/SurfaceScreen.js +21 -12
  4. package/lib/commonjs/ui/components/SurfaceScreen.js.map +1 -1
  5. package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js +11 -5
  6. package/lib/commonjs/ui/components/signIn/OxyAuthScreen.js.map +1 -1
  7. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js +45 -32
  8. package/lib/commonjs/ui/components/signIn/OxySignInPanel.js.map +1 -1
  9. package/lib/commonjs/ui/components/signIn/signInMethods.js +19 -18
  10. package/lib/commonjs/ui/components/signIn/signInMethods.js.map +1 -1
  11. package/lib/commonjs/ui/context/OxyContext.js +4 -4
  12. package/lib/commonjs/ui/hooks/useSurfaceFrameWidth.js +28 -0
  13. package/lib/commonjs/ui/hooks/useSurfaceFrameWidth.js.map +1 -0
  14. package/lib/module/ui/components/OxyAuthChooser.js +7 -6
  15. package/lib/module/ui/components/OxyAuthChooser.js.map +1 -1
  16. package/lib/module/ui/components/SurfaceScreen.js +21 -12
  17. package/lib/module/ui/components/SurfaceScreen.js.map +1 -1
  18. package/lib/module/ui/components/signIn/OxyAuthScreen.js +11 -5
  19. package/lib/module/ui/components/signIn/OxyAuthScreen.js.map +1 -1
  20. package/lib/module/ui/components/signIn/OxySignInPanel.js +47 -32
  21. package/lib/module/ui/components/signIn/OxySignInPanel.js.map +1 -1
  22. package/lib/module/ui/components/signIn/signInMethods.js +19 -18
  23. package/lib/module/ui/components/signIn/signInMethods.js.map +1 -1
  24. package/lib/module/ui/context/OxyContext.js +4 -4
  25. package/lib/module/ui/hooks/useSurfaceFrameWidth.js +24 -0
  26. package/lib/module/ui/hooks/useSurfaceFrameWidth.js.map +1 -0
  27. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts +3 -2
  28. package/lib/typescript/commonjs/ui/components/OxyAuthChooser.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/ui/components/SurfaceScreen.d.ts.map +1 -1
  30. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts +7 -1
  31. package/lib/typescript/commonjs/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts +12 -12
  33. package/lib/typescript/commonjs/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  34. package/lib/typescript/commonjs/ui/components/signIn/signInMethods.d.ts +18 -15
  35. package/lib/typescript/commonjs/ui/components/signIn/signInMethods.d.ts.map +1 -1
  36. package/lib/typescript/commonjs/ui/context/oxyContextTypes.d.ts +4 -4
  37. package/lib/typescript/commonjs/ui/hooks/useSurfaceFrameWidth.d.ts +12 -0
  38. package/lib/typescript/commonjs/ui/hooks/useSurfaceFrameWidth.d.ts.map +1 -0
  39. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts +3 -2
  40. package/lib/typescript/module/ui/components/OxyAuthChooser.d.ts.map +1 -1
  41. package/lib/typescript/module/ui/components/SurfaceScreen.d.ts.map +1 -1
  42. package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts +7 -1
  43. package/lib/typescript/module/ui/components/signIn/OxyAuthScreen.d.ts.map +1 -1
  44. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts +12 -12
  45. package/lib/typescript/module/ui/components/signIn/OxySignInPanel.d.ts.map +1 -1
  46. package/lib/typescript/module/ui/components/signIn/signInMethods.d.ts +18 -15
  47. package/lib/typescript/module/ui/components/signIn/signInMethods.d.ts.map +1 -1
  48. package/lib/typescript/module/ui/context/oxyContextTypes.d.ts +4 -4
  49. package/lib/typescript/module/ui/hooks/useSurfaceFrameWidth.d.ts +12 -0
  50. package/lib/typescript/module/ui/hooks/useSurfaceFrameWidth.d.ts.map +1 -0
  51. package/package.json +1 -1
  52. package/src/ui/components/OxyAuthChooser.tsx +7 -6
  53. package/src/ui/components/SurfaceScreen.tsx +19 -11
  54. package/src/ui/components/signIn/OxyAuthScreen.tsx +14 -5
  55. package/src/ui/components/signIn/OxySignInPanel.tsx +56 -37
  56. package/src/ui/components/signIn/signInMethods.ts +21 -17
  57. package/src/ui/context/OxyContext.tsx +4 -4
  58. package/src/ui/context/oxyContextTypes.ts +4 -4
  59. package/src/ui/hooks/useSurfaceFrameWidth.ts +21 -0
@@ -66,20 +66,29 @@ export const OxyAuthScreen: React.FC<{ children: React.ReactNode; className?: st
66
66
  <View className={`w-full max-w-[448px] self-center gap-6 ${className ?? ''}`}>{children}</View>
67
67
  );
68
68
 
69
- /** The split card: `children` on the left and, from `md`, `aside` on the right. */
70
- export const OxyAuthSplit: React.FC<{ children: React.ReactNode; aside: React.ReactNode }> = ({ children, aside }) => {
69
+ /**
70
+ * The split card: `children` on the left and, from `md`, `aside` on the right.
71
+ * `bare` inside a surface that already is the card (the account dialog): no
72
+ * border of its own, and the right column is a rounded tile within the
73
+ * dialog's margins.
74
+ */
75
+ export const OxyAuthSplit: React.FC<{ children: React.ReactNode; aside: React.ReactNode; bare?: boolean }> = ({
76
+ children,
77
+ aside,
78
+ bare = false,
79
+ }) => {
71
80
  const theme = useTheme();
72
81
  return (
73
82
  <View
74
- className="w-full self-center md:max-w-[880px] md:flex-row md:rounded-3xl md:border md:border-border md:bg-card md:overflow-hidden"
83
+ className={`w-full self-center md:max-w-[880px] md:flex-row ${bare ? 'md:gap-8' : 'md:rounded-3xl md:border md:border-border md:bg-card md:overflow-hidden'}`}
75
84
  >
76
85
  {/* Padding on an inner box: a padded flex item cannot shrink its base
77
86
  size below its padding, which breaks the two equal tracks. */}
78
87
  <View className="md:flex-1 md:basis-0 md:min-w-0">
79
- <View className="md:p-8">{children}</View>
88
+ <View className={bare ? '' : 'md:p-8'}>{children}</View>
80
89
  </View>
81
90
  <View
82
- className="hidden md:flex md:flex-1 md:basis-0 md:min-w-0 items-center justify-center overflow-hidden p-8"
91
+ className={`hidden md:flex md:flex-1 md:basis-0 md:min-w-0 items-center justify-center overflow-hidden p-8 ${bare ? 'rounded-[20px]' : ''}`}
83
92
  style={{ backgroundColor: theme.colors.backgroundSecondary }}
84
93
  >
85
94
  {aside}
@@ -4,18 +4,18 @@
4
4
  * differs (`host`).
5
5
  *
6
6
  * picker a returning device: "Choose an account", then "Use another account"
7
- * entry the header, the Commons way in, and the way in for someone with no
8
- * account. On auth.oxy.so (`page`) also the username with its
9
- * Continue — a username-first passkey sign-in, which takes a
10
- * platform passkey AND a hardware security key with no resident
11
- * credential (a U2F key such as a Titan) — and "or continue with" the
12
- * discoverable passkey. From `md` that page is Bloom `AuthCard`'s
13
- * split card, with the embedded QR over its photo carousel in the
14
- * right column; below `md` it is "Continue with Oxy" at the top.
15
- * In an app's dialog on the web, "Continue with Oxy" opens that page
16
- * in a window over the app, like "Sign in with Google": the browser's
17
- * session lives on auth.oxy.so, so every Oxy app shares it. On
18
- * native it is "Continue with Oxy" ("Get Commons" without Commons).
7
+ * entry the header, the Commons way in, the passkey, and the way in for
8
+ * someone with no account. On the web, from `md`, the screen is
9
+ * Bloom `AuthCard`'s split card and the Commons way in is the
10
+ * embedded QR over its photo carousel, in the right column (the
11
+ * account dialog grows to that card); below `md` it is "Continue
12
+ * with Oxy" at the top. The passkey belongs to `oxy.so`: on
13
+ * auth.oxy.so (`page`) it runs here — the username with its Continue,
14
+ * a username-first ceremony that takes a hardware security key with
15
+ * no resident credential too, and the discoverable passkey — and in
16
+ * an app's dialog it opens auth.oxy.so's window for that one step, as
17
+ * does "Create account". On native it is "Continue with Oxy" ("Get
18
+ * Commons" without Commons).
19
19
  *
20
20
  * `signInMethods.ts` owns which blocks a platform gets.
21
21
  */
@@ -39,6 +39,7 @@ import { useOxy } from '../../context/OxyContext';
39
39
  import { useAccountDialogSnapshot } from '../../hooks/accountDialogSnapshot';
40
40
  import { useDeviceSwitcher } from '../../hooks/useDeviceSwitcher';
41
41
  import { useI18n } from '../../hooks/useI18n';
42
+ import { useSurfaceFrameWidth } from '../../hooks/useSurfaceFrameWidth';
42
43
  import { getCommonsAcquisitionUrl } from '../../utils/commonsStoreLinks';
43
44
  import { isWebBrowser } from '../../utils/isWebBrowser';
44
45
  import { SubtleLink } from '../authChooser/primitives';
@@ -49,6 +50,9 @@ import { OxyAuthScreen, OxyAuthScreenHeader, OxyAuthSplit, OxyAuthTerms } from '
49
50
  import { RATE_LIMIT_SECONDS, describePasskeyError, isRateLimited } from './passkeyError';
50
51
  import { resolveSignInMethods } from './signInMethods';
51
52
 
53
+ /** Bloom `AuthCard`'s split card width, which the account dialog grows to for it. */
54
+ const SPLIT_WIDTH = 880;
55
+
52
56
  export interface OxySignInPanelProps {
53
57
  /** This screen signed the origin in. */
54
58
  onSignedIn: () => void;
@@ -124,7 +128,8 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
124
128
  void controller?.signInWithOxy();
125
129
  showRequest();
126
130
  };
127
- const signInOnOxy = () => {
131
+ // The passkey is `oxy.so`'s: in an app it runs in auth.oxy.so's window.
132
+ const passkeyOnOxy = () => {
128
133
  void continueOnAuth('signin').then((result) => {
129
134
  if (result.status === 'signed-in') onSignedIn();
130
135
  else if (result.status === 'failed') toast.error(t('signin.errors.failed'));
@@ -189,7 +194,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
189
194
  return;
190
195
  case 'failed':
191
196
  // The pair could not be activated as it stands: sign in as it explicitly.
192
- if (methods.passkey && context.handle) reauthenticate(context);
197
+ if (methods.passkey === 'here' && context.handle) reauthenticate(context);
193
198
  else toast.error(t('accountSwitcher.toasts.activateFailed'));
194
199
  return;
195
200
  default:
@@ -198,8 +203,11 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
198
203
  };
199
204
 
200
205
  const showsPicker = pickerAllowed && !showForm && principals.length > 0;
201
- // auth.oxy.so's page is the split card from `md`.
206
+ // The web entry is the split card from `md`. In the account dialog it grows
207
+ // the dialog to that card; below `md` the dialog is a bottom sheet, which a
208
+ // width does not touch.
202
209
  const splits = methods.commons === 'qr';
210
+ useSurfaceFrameWidth(host === 'dialog' && !showsPicker && splits ? SPLIT_WIDTH : null);
203
211
 
204
212
  if (showsPicker) {
205
213
  return (
@@ -241,7 +249,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
241
249
  </Text>
242
250
  );
243
251
  // One solid action per screen: the username's Continue when there is one.
244
- const commonsAppearance = methods.passkey ? 'outline' : 'solid';
252
+ const commonsAppearance = methods.passkey === 'here' ? 'outline' : 'solid';
245
253
 
246
254
  const continueWithOxyButton = (
247
255
  <Button
@@ -249,13 +257,28 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
249
257
  tone={commonsAppearance === 'solid' ? 'action' : 'neutral'}
250
258
  size="lg"
251
259
  fullWidth
252
- onPress={methods.commons === 'window' ? signInOnOxy : continueWithOxy}
260
+ onPress={continueWithOxy}
253
261
  testID="continue-with-oxy"
254
262
  >
255
263
  {t('accountSwitcher.continueWithOxy')}
256
264
  </Button>
257
265
  );
258
266
 
267
+ const passkeyButton = (
268
+ <Button
269
+ appearance="outline"
270
+ tone="neutral"
271
+ size="lg"
272
+ fullWidth
273
+ leadingIcon={RiKey2Line}
274
+ disabled={methods.passkey === 'here' && blocked}
275
+ onPress={methods.passkey === 'here' ? () => void runPasskey(undefined, t('signin.errors.failed')) : passkeyOnOxy}
276
+ testID="passkey-sign-in"
277
+ >
278
+ {t('signin.methods.passkey')}
279
+ </Button>
280
+ );
281
+
259
282
  const form = (
260
283
  <OxyAuthScreen className={splits ? 'md:max-w-none' : undefined}>
261
284
  <OxyAuthScreenHeader title={title} description={description} />
@@ -263,7 +286,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
263
286
  {/* Below `md` this screen is the phone a QR would be scanned with. */}
264
287
  {splits ? <CssView className="md:hidden">{continueWithOxyButton}</CssView> : null}
265
288
 
266
- {methods.commons === 'continue' || methods.commons === 'window' ? continueWithOxyButton : null}
289
+ {methods.commons === 'continue' ? continueWithOxyButton : null}
267
290
  {methods.commons === 'get-commons' ? (
268
291
  <View style={styles.stack}>
269
292
  <Text style={[styles.note, { color: theme.colors.textSecondary }]}>{t('accountSwitcher.commonsNotInstalled')}</Text>
@@ -273,7 +296,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
273
296
  </View>
274
297
  ) : null}
275
298
 
276
- {methods.passkey ? (
299
+ {methods.passkey === 'here' ? (
277
300
  <View style={styles.stack}>
278
301
  <View style={styles.field}>
279
302
  <TextFieldLabel nativeID="username-label">{t('signin.username.label')}</TextFieldLabel>
@@ -314,24 +337,17 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
314
337
  </View>
315
338
  ) : null}
316
339
 
317
- {/* The alternatives to the screen's primary way in, always under it. The
318
- window an app opens on the web is where they all are. */}
319
- {methods.commons === 'window' ? null : <Divider>{t('signin.orContinueWith')}</Divider>}
340
+ {/* The alternatives to the screen's primary way in, always under it. In an
341
+ app's dialog, from `md`, the QR beside it IS the primary way in. */}
342
+ {methods.passkey === 'window' ? (
343
+ <CssView className="md:hidden">
344
+ <Divider>{t('signin.orContinueWith')}</Divider>
345
+ </CssView>
346
+ ) : (
347
+ <Divider>{t('signin.orContinueWith')}</Divider>
348
+ )}
320
349
 
321
- {methods.passkey ? (
322
- <Button
323
- appearance="outline"
324
- tone="neutral"
325
- size="lg"
326
- fullWidth
327
- leadingIcon={RiKey2Line}
328
- disabled={blocked}
329
- onPress={() => void runPasskey(undefined, t('signin.errors.failed'))}
330
- testID="passkey-sign-in"
331
- >
332
- {t('signin.methods.passkey')}
333
- </Button>
334
- ) : methods.commons === 'window' ? null : (
350
+ {methods.passkey === 'none' ? (
335
351
  <Button
336
352
  appearance="outline"
337
353
  tone="neutral"
@@ -343,9 +359,11 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
343
359
  >
344
360
  {t('accountSwitcher.scanQr')}
345
361
  </Button>
362
+ ) : (
363
+ passkeyButton
346
364
  )}
347
365
 
348
- {methods.passkey ? null : noAccount}
366
+ {methods.passkey === 'here' ? null : noAccount}
349
367
  {onRecover ? (
350
368
  <SubtleLink label={t('signin.recoverLink')} theme={theme} onPress={onRecover} testID="recover-link" />
351
369
  ) : null}
@@ -356,6 +374,7 @@ export const OxySignInPanel: React.FC<OxySignInPanelProps> = ({
356
374
  if (!splits) return form;
357
375
  return (
358
376
  <OxyAuthSplit
377
+ bare={host === 'dialog'}
359
378
  aside={
360
379
  <>
361
380
  <AuthMediaCarousel slides={SIGN_IN_SLIDES} style={StyleSheet.absoluteFill} />
@@ -1,34 +1,38 @@
1
1
  /**
2
2
  * Which sign-in methods a surface offers, from where it runs.
3
3
  *
4
- * One screen everywhere; only the transport behind each block changes:
4
+ * One screen everywhere, and sign-in happens IN it (ADR 0029 D1):
5
5
  *
6
- * - auth.oxy.so (`page`) is where the web signs in: the embedded Commons QR
7
- * (the split card's right column from `md`, "Continue with Oxy" below it),
8
- * the username with its Continue, and the passkey — asserted right here,
9
- * on the one origin every Oxy passkey belongs to;
10
- * - an app's account dialog on the web (`dialog`) opens that screen in a
11
- * window over the app ("Continue with Oxy"), like "Sign in with Google":
12
- * the browser's session lives on auth.oxy.so, so every Oxy app shares it;
13
- * - native signs in with Commons: "Continue with Oxy", or "Get Commons" on a
14
- * device that has no Commons to continue with.
6
+ * - the COMMONS way in — on the web the embedded QR (the split card's right
7
+ * column from `md`, with "Continue with Oxy" below `md`, where the screen
8
+ * is the phone that would scan it); on native "Continue with Oxy", and
9
+ * "Get Commons" on a device that has no Commons to continue with;
10
+ * - the PASSKEY — it belongs to `oxy.so`, so it runs right here on auth.oxy.so
11
+ * (`page`: the username with its Continue, and the discoverable passkey), and
12
+ * in an app's dialog on the web it opens auth.oxy.so's window for that one
13
+ * step (with account creation); native has none, Commons holds the identity.
15
14
  */
16
15
 
17
16
  import type { CommonsAvailability } from '@oxy.so/core';
18
17
 
19
18
  /**
20
- * `qr` — auth.oxy.so: the embedded QR from `md`, "Continue with Oxy" below.
21
- * `window` — an app on the web: "Continue with Oxy" opens auth.oxy.so's window.
19
+ * `qr` — the web: the embedded QR from `md`, "Continue with Oxy" below.
22
20
  * `continue` — native "Continue with Oxy": Oxy picks the route (shared
23
21
  * keychain, Commons on this device, a push, or the QR view).
24
22
  * `get-commons` — native, and Commons is not installed here.
25
23
  */
26
- export type CommonsEntry = 'qr' | 'window' | 'continue' | 'get-commons';
24
+ export type CommonsEntry = 'qr' | 'continue' | 'get-commons';
25
+
26
+ /**
27
+ * `here` — auth.oxy.so: the username and the passkey run on this page.
28
+ * `window` — an app's dialog on the web: the passkey opens auth.oxy.so's window.
29
+ * `none` — native.
30
+ */
31
+ export type PasskeyEntry = 'here' | 'window' | 'none';
27
32
 
28
33
  export interface SignInMethods {
29
34
  commons: CommonsEntry;
30
- /** The username and passkey block: only on auth.oxy.so. */
31
- passkey: boolean;
35
+ passkey: PasskeyEntry;
32
36
  }
33
37
 
34
38
  export interface SignInSurfaceFacts {
@@ -42,10 +46,10 @@ export interface SignInSurfaceFacts {
42
46
 
43
47
  export function resolveSignInMethods(facts: SignInSurfaceFacts): SignInMethods {
44
48
  if (facts.web) {
45
- return facts.host === 'page' ? { commons: 'qr', passkey: true } : { commons: 'window', passkey: false };
49
+ return { commons: 'qr', passkey: facts.host === 'page' ? 'here' : 'window' };
46
50
  }
47
51
  return {
48
52
  commons: facts.commonsAvailability === 'unavailable' ? 'get-commons' : 'continue',
49
- passkey: false,
53
+ passkey: 'none',
50
54
  };
51
55
  }
@@ -785,10 +785,10 @@ export const OxyRuntimeProvider: React.FC<OxyRuntimeProviderProps> = ({
785
785
  [webAuthMode, oxyServices, clientId, authorizeBaseUrl, isIdentityBound, user?.id],
786
786
  );
787
787
 
788
- // On the web an Oxy app signs in and creates accounts in auth.oxy.so's
789
- // window over the app, like "Sign in with Google": the passkey belongs to
790
- // that origin, and the browser's session lives there, shared by every Oxy
791
- // app. A blocked window falls back to the same page in this tab.
788
+ // What only auth.oxy.so can do for an Oxy app on the web — assert the
789
+ // `oxy.so` passkey, create or recover a passkey account — runs in its window
790
+ // over the app, for that one step. A blocked window falls back to the same
791
+ // page in this tab.
792
792
  const continueOnAuth = useCallback(
793
793
  (screen: OxyAuthScreen): Promise<WebOAuthSignInResult> =>
794
794
  startWebOAuthSignInForContext({
@@ -131,10 +131,10 @@ export interface OxyContextState {
131
131
  startWebOAuthSignIn: (options: StartWebOAuthSignInOptions) => Promise<WebOAuthSignInResult>;
132
132
 
133
133
  /**
134
- * Sign in (`signin`), create an account (`signup`) or recover one
135
- * (`recover`) in auth.oxy.so's window over the app, where the browser's
136
- * shared Oxy session lives. Web only; call it from the press, so the window
137
- * opens with the gesture.
134
+ * The passkey sign-in (`signin`), account creation (`signup`) or recovery
135
+ * (`recover`) in auth.oxy.so's window over the app — the one step the
136
+ * account dialog cannot run itself, since the passkey belongs to `oxy.so`.
137
+ * Web only; call it from the press, so the window opens with the gesture.
138
138
  */
139
139
  continueOnAuth: (screen: OxyAuthScreen) => Promise<WebOAuthSignInResult>;
140
140
 
@@ -0,0 +1,21 @@
1
+ import { createContext, useContext, useLayoutEffect } from 'react';
2
+
3
+ /**
4
+ * A screen's say in how wide its surface is.
5
+ *
6
+ * `SurfaceScreen` is the ONE host that declares the surface's dialog frame, so
7
+ * a screen whose views need different widths (the account dialog: the 420 menu,
8
+ * the sign-in's 880 split card) hands its width to that host rather than
9
+ * declaring a second frame. A change morphs the centered card to it.
10
+ */
11
+ export const SurfaceFrameWidthContext = createContext<((maxWidth: number | null) => void) | null>(null);
12
+
13
+ /** Ask the surface for `maxWidth` (px) while this is mounted; `null` keeps the surface's own. */
14
+ export function useSurfaceFrameWidth(maxWidth: number | null): void {
15
+ const setWidth = useContext(SurfaceFrameWidthContext);
16
+ useLayoutEffect(() => {
17
+ if (!setWidth) return;
18
+ setWidth(maxWidth);
19
+ return () => setWidth(null);
20
+ }, [setWidth, maxWidth]);
21
+ }