@homefile/components-v2 2.63.0 → 2.63.1

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 (40) hide show
  1. package/dist/components/onboarding/CardFooter.js +2 -1
  2. package/dist/components/onboarding/pages/ActivateAccount.js +3 -3
  3. package/dist/components/onboarding/pages/AuthHeader.js +2 -2
  4. package/dist/components/onboarding/pages/BasePageWrapper.d.ts +32 -1
  5. package/dist/components/onboarding/pages/BasePageWrapper.js +23 -3
  6. package/dist/components/onboarding/pages/EmailValidation.js +2 -2
  7. package/dist/components/onboarding/pages/MessagePage.js +3 -3
  8. package/dist/components/onboarding/pages/NewPassword.js +3 -3
  9. package/dist/components/onboarding/pages/ResendResetPassword.js +2 -2
  10. package/dist/components/onboarding/pages/ResetPassword.js +3 -3
  11. package/dist/components/onboarding/pages/SignIn.js +3 -3
  12. package/dist/components/onboarding/pages/SignUp.js +3 -3
  13. package/dist/components/onboarding/pages/TwoFactor.d.ts +1 -1
  14. package/dist/components/onboarding/pages/TwoFactor.js +13 -4
  15. package/dist/components/onboarding/pages/Welcome.js +2 -2
  16. package/dist/interfaces/pages/TwoFactor.interface.d.ts +2 -1
  17. package/dist/stories/onboarding/CardFooter.stories.d.ts +2 -1
  18. package/dist/stories/onboarding/CardFooter.stories.js +24 -0
  19. package/dist/stories/onboarding/pages/ActivateAccount.stories.d.ts +3 -1
  20. package/dist/stories/onboarding/pages/ActivateAccount.stories.js +41 -0
  21. package/dist/stories/onboarding/pages/EmailValidation.stories.d.ts +3 -1
  22. package/dist/stories/onboarding/pages/EmailValidation.stories.js +43 -0
  23. package/dist/stories/onboarding/pages/MessagePage.stories.js +10 -2
  24. package/dist/stories/onboarding/pages/NewPassword.stories.d.ts +3 -1
  25. package/dist/stories/onboarding/pages/NewPassword.stories.js +44 -0
  26. package/dist/stories/onboarding/pages/ResendResetPassword.stories.d.ts +3 -1
  27. package/dist/stories/onboarding/pages/ResendResetPassword.stories.js +47 -0
  28. package/dist/stories/onboarding/pages/ResetPassword.stories.d.ts +3 -1
  29. package/dist/stories/onboarding/pages/ResetPassword.stories.js +45 -0
  30. package/dist/stories/onboarding/pages/SignIn.stories.d.ts +2 -0
  31. package/dist/stories/onboarding/pages/SignIn.stories.js +52 -0
  32. package/dist/stories/onboarding/pages/SignUp.stories.d.ts +2 -1
  33. package/dist/stories/onboarding/pages/SignUp.stories.js +34 -0
  34. package/dist/stories/onboarding/pages/TwoFactor.stories.d.ts +3 -1
  35. package/dist/stories/onboarding/pages/TwoFactor.stories.js +68 -11
  36. package/dist/stories/onboarding/pages/Welcome.stories.d.ts +2 -1
  37. package/dist/stories/onboarding/pages/Welcome.stories.js +29 -0
  38. package/dist/stories/onboarding/phoneLayout.d.ts +20 -0
  39. package/dist/stories/onboarding/phoneLayout.js +89 -0
  40. package/package.json +1 -1
@@ -1,5 +1,16 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx } from "react/jsx-runtime";
11
+ import { fn, within } from '@storybook/test';
2
12
  import { NewPassword, CloudsAnimation } from '../../../components';
13
+ import { PHONE, SMALL_TABLET, expectCardAtTop, expectCardBelowTopGap, expectCardFullWidth, expectPageFillsScreen, expectSides, getCard, } from '../phoneLayout';
3
14
  export default {
4
15
  title: 'Pages/Onboarding',
5
16
  component: NewPassword,
@@ -28,3 +39,36 @@ NewPasswordNoAnswer.args = {
28
39
  errorMessage: "We couldn't reach Homefile. Check your connection, then press Reset Password.",
29
40
  supportEmail: 'support@homefile.com',
30
41
  };
42
+ // New password on a phone: the card starts at the top, spans the screen and
43
+ // runs down to the copyright at the bottom of the screen, like sign in; the
44
+ // fields and the button keep 13px from its sides.
45
+ export const NewPasswordPhone = {
46
+ parameters: PHONE,
47
+ args: {
48
+ isLoading: false,
49
+ userEmail: 'jordan@example.com',
50
+ handleReset: fn(),
51
+ },
52
+ render: (args) => (_jsx(CloudsAnimation, { children: _jsx(NewPassword, Object.assign({}, args)) })),
53
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
54
+ const canvas = within(canvasElement);
55
+ const card = getCard(canvasElement);
56
+ yield expectCardAtTop(card);
57
+ yield expectCardFullWidth(card);
58
+ yield expectSides(card, {
59
+ 'password field': canvasElement.querySelector('input#password'),
60
+ 'confirm field': canvasElement.querySelector('input#confirmPassword'),
61
+ 'Reset Password button': canvas.getByRole('button', { name: 'Reset Password' }),
62
+ });
63
+ yield expectPageFillsScreen(card);
64
+ }),
65
+ };
66
+ // New password on a small tablet (744px): the card keeps the top gap.
67
+ export const NewPasswordSmallTablet = {
68
+ parameters: SMALL_TABLET,
69
+ args: NewPasswordPhone.args,
70
+ render: NewPasswordPhone.render,
71
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
72
+ yield expectCardBelowTopGap(getCard(canvasElement));
73
+ }),
74
+ };
@@ -1,4 +1,4 @@
1
- import { Meta } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react';
2
2
  import { EmailValidationI } from '../../../interfaces';
3
3
  declare const _default: Meta<EmailValidationI>;
4
4
  export default _default;
@@ -9,3 +9,5 @@ export declare const ResendResetPasswordPage: {
9
9
  isLoading: boolean;
10
10
  };
11
11
  };
12
+ export declare const ResendResetPasswordPhone: StoryObj<EmailValidationI>;
13
+ export declare const ResendResetPasswordSmallTablet: StoryObj<EmailValidationI>;
@@ -1,5 +1,16 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx } from "react/jsx-runtime";
11
+ import { fn, within } from '@storybook/test';
2
12
  import { ResendResetPassword, CloudsAnimation } from '../../../components';
13
+ import { PHONE, SMALL_TABLET, expectCardAtTop, expectCardBelowTopGap, expectCardFullWidth, expectContentSides, expectPageFillsScreen, expectSides, getCard, } from '../phoneLayout';
3
14
  export default {
4
15
  title: 'Pages/Onboarding',
5
16
  component: ResendResetPassword,
@@ -11,3 +22,39 @@ ResendResetPasswordPage.args = {
11
22
  email: 'user@user.com.br',
12
23
  isLoading: true,
13
24
  };
25
+ // The reset-email-sent page on a phone: like sign in, the card starts at the
26
+ // top and runs down to the bottom of the screen; its title and buttons keep
27
+ // 13px from the sides.
28
+ export const ResendResetPasswordPhone = {
29
+ parameters: PHONE,
30
+ args: {
31
+ email: 'jordan@example.com',
32
+ isLoading: false,
33
+ handleResendBt: fn(),
34
+ handleBack: fn(),
35
+ },
36
+ render: (args) => (_jsx(CloudsAnimation, { children: _jsx(ResendResetPassword, Object.assign({}, args)) })),
37
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
38
+ const canvas = within(canvasElement);
39
+ const card = getCard(canvasElement);
40
+ yield expectCardAtTop(card);
41
+ yield expectCardFullWidth(card);
42
+ yield expectContentSides(card, {
43
+ title: canvas.getByText('jordan@example.com').parentElement,
44
+ });
45
+ yield expectSides(card, {
46
+ 'Resend button': canvas.getByRole('button', { name: 'Resend Reset Password Link' }),
47
+ 'Back to Log In button': canvas.getByRole('button', { name: 'Back to Log In' }),
48
+ });
49
+ yield expectPageFillsScreen(card);
50
+ }),
51
+ };
52
+ // The reset-email-sent page on a small tablet (744px): the card keeps the top gap.
53
+ export const ResendResetPasswordSmallTablet = {
54
+ parameters: SMALL_TABLET,
55
+ args: ResendResetPasswordPhone.args,
56
+ render: ResendResetPasswordPhone.render,
57
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
58
+ yield expectCardBelowTopGap(getCard(canvasElement));
59
+ }),
60
+ };
@@ -1,4 +1,4 @@
1
- import { Meta } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react';
2
2
  import { ResetPasswordI } from '../../../interfaces';
3
3
  declare const _default: Meta<ResetPasswordI>;
4
4
  export default _default;
@@ -9,3 +9,5 @@ export declare const ResetPasswordPage: {
9
9
  resetError: boolean;
10
10
  };
11
11
  };
12
+ export declare const ResetPasswordPhone: StoryObj<ResetPasswordI>;
13
+ export declare const ResetPasswordSmallTablet: StoryObj<ResetPasswordI>;
@@ -1,5 +1,16 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx } from "react/jsx-runtime";
11
+ import { fn, within } from '@storybook/test';
2
12
  import { ResetPassword, CloudsAnimation } from '../../../components';
13
+ import { PHONE, SMALL_TABLET, expectCardAtTop, expectCardBelowTopGap, expectCardFullWidth, expectPageFillsScreen, expectSides, getCard, } from '../phoneLayout';
3
14
  export default {
4
15
  title: 'Pages/Onboarding',
5
16
  component: ResetPassword,
@@ -11,3 +22,37 @@ ResetPasswordPage.args = {
11
22
  isLoading: false,
12
23
  resetError: false,
13
24
  };
25
+ // Reset password on a phone: the card starts at the top, spans the screen and
26
+ // runs down to the copyright at the bottom of the screen, like sign in; the
27
+ // field, the button and the Log-in box keep 13px from its sides.
28
+ export const ResetPasswordPhone = {
29
+ parameters: PHONE,
30
+ args: {
31
+ isLoading: false,
32
+ resetError: false,
33
+ handleReset: fn(),
34
+ handleSignIn: fn(),
35
+ },
36
+ render: (args) => (_jsx(CloudsAnimation, { children: _jsx(ResetPassword, Object.assign({}, args)) })),
37
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
38
+ const canvas = within(canvasElement);
39
+ const card = getCard(canvasElement);
40
+ yield expectCardAtTop(card);
41
+ yield expectCardFullWidth(card);
42
+ yield expectSides(card, {
43
+ 'email field': canvasElement.querySelector('input#email'),
44
+ 'Reset Password button': canvas.getByRole('button', { name: 'Reset Password' }),
45
+ 'Log-in box': canvas.getByRole('button', { name: 'Log in' }).parentElement,
46
+ });
47
+ yield expectPageFillsScreen(card);
48
+ }),
49
+ };
50
+ // Reset password on a small tablet (744px): the card keeps the top gap.
51
+ export const ResetPasswordSmallTablet = {
52
+ parameters: SMALL_TABLET,
53
+ args: ResetPasswordPhone.args,
54
+ render: ResetPasswordPhone.render,
55
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
56
+ yield expectCardBelowTopGap(getCard(canvasElement));
57
+ }),
58
+ };
@@ -19,3 +19,5 @@ export declare const SignInPage: {
19
19
  }): Promise<void>;
20
20
  };
21
21
  export declare const SignInConnect: StoryObj<SignInI>;
22
+ export declare const SignInPartnerPhone: StoryObj<SignInI>;
23
+ export declare const SignInSmallTablet: StoryObj<SignInI>;
@@ -10,6 +10,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
10
10
  import { jsx as _jsx } from "react/jsx-runtime";
11
11
  import { expect, fn, within } from '@storybook/test';
12
12
  import { SignIn, CloudsAnimation } from '../../../components';
13
+ import { PHONE, SMALL_TABLET, expectCardAtTop, expectCardFullWidth, expectNoCardShadow, expectSides, getCard, } from '../phoneLayout';
13
14
  export default {
14
15
  title: 'Pages/Onboarding',
15
16
  component: SignIn,
@@ -38,6 +39,7 @@ const CONNECT_HEADING = 'Connect (512) 555-0147';
38
39
  const CONNECT_LINE = 'Log in to your Homefile account to connect this phone. Then Homi will know it’s you.';
39
40
  // The page a texter reaches from Homi's link to connect their phone.
40
41
  export const SignInConnect = {
42
+ parameters: PHONE,
41
43
  args: {
42
44
  isLoading: false,
43
45
  heading: CONNECT_HEADING,
@@ -56,5 +58,55 @@ export const SignInConnect = {
56
58
  yield expect(canvas.queryByText('Don’t have a Homefile account?')).toBeNull();
57
59
  yield expect(canvas.queryByRole('button', { name: 'Sign up' })).toBeNull();
58
60
  yield expect(canvas.queryByRole('button', { name: 'Continue with Apple' })).toBeNull();
61
+ // On a phone: full screen, no bar on top, 13px sides.
62
+ const card = getCard(canvasElement);
63
+ yield expectCardAtTop(card);
64
+ yield expectCardFullWidth(card);
65
+ yield expectSides(card, {
66
+ 'Google button': canvas.getByRole('button', { name: 'Continue with Google' }),
67
+ 'email field': canvasElement.querySelector('input#email'),
68
+ 'password field': canvasElement.querySelector('input#password'),
69
+ 'Log in button': canvas.getByRole('button', { name: 'Log in' }),
70
+ });
71
+ }),
72
+ };
73
+ // The partner sign-in on a phone: the partner header, the form and the Sign-up
74
+ // box all keep 13px from the sides of the screen.
75
+ export const SignInPartnerPhone = {
76
+ parameters: PHONE,
77
+ args: {
78
+ isPartner: true,
79
+ isLoading: false,
80
+ handleForgotPassword: fn(),
81
+ handleSignIn: fn(),
82
+ handleSignUp: fn(),
83
+ },
84
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
85
+ const canvas = within(canvasElement);
86
+ const card = getCard(canvasElement);
87
+ yield expectCardAtTop(card);
88
+ yield expectCardFullWidth(card);
89
+ yield expectSides(card, {
90
+ 'partner header': [
91
+ canvas.getByAltText('Homefile'),
92
+ canvas.getByText('Partner Admin'),
93
+ ],
94
+ 'email field': canvasElement.querySelector('input#email'),
95
+ 'password field': canvasElement.querySelector('input#password'),
96
+ 'Log in button': canvas.getByRole('button', { name: 'Log in' }),
97
+ 'Sign-up box': canvas.getByText('Don’t have a Homefile Partner account?'),
98
+ });
99
+ }),
100
+ };
101
+ // Sign in on a small tablet (744px): still the full-screen page below 768px,
102
+ // as in 2.63.0 (Matt kept sign in as it is at every width, 2026-10-08).
103
+ export const SignInSmallTablet = {
104
+ parameters: SMALL_TABLET,
105
+ args: SignInConnect.args,
106
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
107
+ const card = getCard(canvasElement);
108
+ yield expectCardAtTop(card);
109
+ yield expectCardFullWidth(card);
110
+ yield expectNoCardShadow(card);
59
111
  }),
60
112
  };
@@ -1,4 +1,4 @@
1
- import { Meta } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react';
2
2
  import { SignUpI } from '../../../interfaces';
3
3
  declare const _default: Meta<SignUpI>;
4
4
  export default _default;
@@ -14,3 +14,4 @@ export declare const SignUpPage: {
14
14
  };
15
15
  };
16
16
  };
17
+ export declare const SignUpPhone: StoryObj<SignUpI>;
@@ -1,5 +1,16 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx } from "react/jsx-runtime";
11
+ import { fn, within } from '@storybook/test';
2
12
  import { CloudsAnimation, SignUp } from '../../../components';
13
+ import { PHONE, expectCardAtTop, expectCardFullWidth, expectSides, getCard, } from '../phoneLayout';
3
14
  export default {
4
15
  title: 'Pages/Onboarding',
5
16
  component: SignUp,
@@ -20,3 +31,26 @@ SignUpPage.args = {
20
31
  showPromoCode: true,
21
32
  values: valuesFilled,
22
33
  };
34
+ // Sign up on a phone: full screen, no bar on top, and the form and the Log-in
35
+ // box keep 13px from the sides of the screen.
36
+ export const SignUpPhone = {
37
+ parameters: PHONE,
38
+ args: {
39
+ isLoading: false,
40
+ handleCreateAccount: fn(),
41
+ handleSignIn: fn(),
42
+ },
43
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
44
+ const canvas = within(canvasElement);
45
+ const card = getCard(canvasElement);
46
+ yield expectCardAtTop(card);
47
+ yield expectCardFullWidth(card);
48
+ yield expectSides(card, {
49
+ 'name field': canvasElement.querySelector('input#name'),
50
+ 'email field': canvasElement.querySelector('input#email'),
51
+ 'password field': canvasElement.querySelector('input#password'),
52
+ 'Create account button': canvas.getByRole('button', { name: 'Create account' }),
53
+ 'Log-in box': canvas.getByText('Already have an account?'),
54
+ });
55
+ }),
56
+ };
@@ -1,12 +1,14 @@
1
- import { Meta, StoryObj } from '@storybook/react';
1
+ import { Decorator, Meta, StoryObj } from '@storybook/react';
2
2
  import { TwoFactorI } from '../../../interfaces';
3
3
  declare const _default: Meta<TwoFactorI>;
4
4
  export default _default;
5
5
  export declare const TwoFactorPage: {
6
6
  (args: TwoFactorI): import("react/jsx-runtime").JSX.Element;
7
+ decorators: Decorator[];
7
8
  play({ canvasElement }: {
8
9
  canvasElement: HTMLElement;
9
10
  }): Promise<void>;
10
11
  };
11
12
  export declare const TwoFactorConnect: StoryObj<TwoFactorI>;
12
13
  export declare const TwoFactorConnectWrongCode: StoryObj<TwoFactorI>;
14
+ export declare const TwoFactorConnectResendLimit: StoryObj<TwoFactorI>;
@@ -11,6 +11,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
11
11
  import { Box } from '@chakra-ui/react';
12
12
  import { expect, fn, within } from '@storybook/test';
13
13
  import { TwoFactor } from '../../../components';
14
+ import { PHONE, expectCardAtTop, expectCardFullWidth, expectSides, getCard, } from '../phoneLayout';
14
15
  export default {
15
16
  title: 'Pages/Onboarding',
16
17
  component: TwoFactor,
@@ -22,13 +23,13 @@ export default {
22
23
  onReset: (code) => alert(`Code submitted: ${code}`),
23
24
  onResend: () => alert('Resend code clicked'),
24
25
  },
25
- decorators: [
26
- (Story) => (_jsx(Box, { p: "base", children: _jsx(Story, {}) })),
27
- ],
28
26
  };
29
27
  export const TwoFactorPage = (args) => {
30
28
  return _jsx(TwoFactor, Object.assign({}, args));
31
29
  };
30
+ // The default page keeps its framed look; the connect stories render full-screen.
31
+ const framed = (Story) => (_jsx(Box, { p: "base", children: _jsx(Story, {}) }));
32
+ TwoFactorPage.decorators = [framed];
32
33
  TwoFactorPage.play = (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
33
34
  const canvas = within(canvasElement);
34
35
  // The default page is unchanged: its own title, subtitle and button.
@@ -37,43 +38,99 @@ TwoFactorPage.play = (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvas
37
38
  yield expect(canvas.getByRole('button', { name: 'Submit' })).toBeVisible();
38
39
  });
39
40
  const CONNECT_HEADING = 'Check your email';
40
- const CONNECT_LINE = 'We sent a 6-digit code to j•••••@example.com. Enter it to connect (512) 555-0147.';
41
- const CONNECT_WARNING = 'If someone sent you this link and asked you to log in, don’t enter the code.';
41
+ const CONNECT_LINE = 'We sent a 6-digit code to j•••••@example.com. Enter it to connect (512) 555-0147. Keep this page open while you get your code.';
42
+ // Matt, 2026-10-08: one calm line in normal type, in place of the bold warning.
43
+ const CONNECT_NOTE = 'Only enter the code if (512) 555-0147 is your phone.';
42
44
  const WRONG_CODE = 'That code didn’t work. Check the email and try again.';
45
+ // Matt, 2026-10-08T03:00:39Z: a press of "Resend new code" past the two resends
46
+ // a link allows shows this line under the button; the button stays.
47
+ const RESEND_LIMIT = "You've asked for 2 new codes. Text Homi for a new link if this one doesn't arrive.";
48
+ // On a phone: full screen, no bar on top, 13px sides.
49
+ const expectPhoneLayout = (canvasElement) => __awaiter(void 0, void 0, void 0, function* () {
50
+ const canvas = within(canvasElement);
51
+ const card = getCard(canvasElement);
52
+ yield expectCardAtTop(card);
53
+ yield expectCardFullWidth(card);
54
+ yield expectSides(card, {
55
+ 'code field': canvas.getByPlaceholderText('Enter your code here'),
56
+ 'Connect phone button': canvas.getByRole('button', { name: 'Connect phone' }),
57
+ });
58
+ });
59
+ // The calm line is on every state of the screen: in normal type, above the code
60
+ // field, and on the screen without scrolling on a phone (Ken, v4.10 (d)).
61
+ const expectCalmNote = (canvasElement) => __awaiter(void 0, void 0, void 0, function* () {
62
+ const canvas = within(canvasElement);
63
+ const note = canvas.getByText(CONNECT_NOTE);
64
+ yield expect(note).toBeVisible();
65
+ yield expect(Number(getComputedStyle(note).fontWeight), 'note weight').toBe(400);
66
+ const noteBottom = note.getBoundingClientRect().bottom;
67
+ const codeField = canvas.getByPlaceholderText('Enter your code here');
68
+ yield expect(noteBottom, 'note above the code field').toBeLessThanOrEqual(codeField.getBoundingClientRect().top);
69
+ yield expect(noteBottom, 'note on screen').toBeLessThanOrEqual(window.innerHeight);
70
+ });
43
71
  const connectArgs = {
44
72
  twoFactorMethod: 'email',
45
73
  heading: CONNECT_HEADING,
46
74
  description: CONNECT_LINE,
47
- warning: CONNECT_WARNING,
75
+ note: CONNECT_NOTE,
48
76
  submitLabel: 'Connect phone',
49
77
  onReset: fn(),
50
78
  onResend: fn(),
51
79
  };
52
80
  // The code step of the page a texter reaches from Homi's link.
53
81
  export const TwoFactorConnect = {
82
+ parameters: PHONE,
54
83
  args: connectArgs,
55
84
  play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
56
85
  const canvas = within(canvasElement);
57
86
  yield expect(canvas.getByText(CONNECT_HEADING)).toBeVisible();
58
87
  yield expect(canvas.getByText(CONNECT_LINE)).toBeVisible();
59
- const warning = canvas.getByText(CONNECT_WARNING);
60
- yield expect(warning).toBeVisible();
61
- yield expect(Number(getComputedStyle(warning).fontWeight)).toBeGreaterThanOrEqual(600);
88
+ yield expectCalmNote(canvasElement);
62
89
  yield expect(canvas.getByRole('button', { name: 'Connect phone' })).toBeVisible();
63
90
  yield expect(canvas.getByRole('button', { name: 'Resend new code' })).toBeVisible();
64
91
  yield expect(canvas.queryByText('Please validate your identity to log in.')).toBeNull();
65
92
  yield expect(canvas.queryByRole('button', { name: 'Submit' })).toBeNull();
93
+ // No line under "Resend new code" unless the page passes one.
94
+ yield expect(canvas.queryByText(RESEND_LIMIT)).toBeNull();
95
+ yield expectPhoneLayout(canvasElement);
66
96
  }),
67
97
  };
68
- // After a wrong code: the error sits above the code field, and the warning stays.
98
+ // After a wrong code: the error sits above the code field, and the calm line stays.
69
99
  export const TwoFactorConnectWrongCode = {
100
+ parameters: PHONE,
70
101
  args: Object.assign(Object.assign({}, connectArgs), { errorMessage: WRONG_CODE }),
71
102
  play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
72
103
  const canvas = within(canvasElement);
73
104
  const error = canvas.getByText(WRONG_CODE);
74
105
  yield expect(error).toBeVisible();
75
- yield expect(canvas.getByText(CONNECT_WARNING)).toBeVisible();
106
+ yield expectCalmNote(canvasElement);
76
107
  const codeField = canvas.getByPlaceholderText('Enter your code here');
77
108
  yield expect(error.getBoundingClientRect().bottom).toBeLessThanOrEqual(codeField.getBoundingClientRect().top);
109
+ yield expect(canvas.queryByText(RESEND_LIMIT)).toBeNull();
110
+ yield expectPhoneLayout(canvasElement);
111
+ }),
112
+ };
113
+ // A press past the resend limit: a calm line under "Resend new code", in normal
114
+ // type and without error styling; the button and the calm note stay.
115
+ const resendLimitArgs = Object.assign(Object.assign({}, connectArgs), { resendNote: RESEND_LIMIT });
116
+ export const TwoFactorConnectResendLimit = {
117
+ parameters: PHONE,
118
+ args: resendLimitArgs,
119
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
120
+ const canvas = within(canvasElement);
121
+ const line = canvas.getByText(RESEND_LIMIT);
122
+ yield expect(line).toBeVisible();
123
+ const resend = canvas.getByRole('button', { name: 'Resend new code' });
124
+ yield expect(resend).toBeVisible();
125
+ yield expect(line.getBoundingClientRect().top, 'line under the resend button').toBeGreaterThanOrEqual(resend.getBoundingClientRect().bottom);
126
+ yield expect(line.getBoundingClientRect().bottom, 'line on screen').toBeLessThanOrEqual(window.innerHeight);
127
+ const style = getComputedStyle(line);
128
+ const note = getComputedStyle(canvas.getByText(CONNECT_NOTE));
129
+ yield expect(Number(style.fontWeight), 'line weight').toBe(400);
130
+ yield expect(style.color, 'line colour (no error styling)').toBe(note.color);
131
+ yield expect(line.closest('[role="alert"], .chakra-form__error-message')).toBeNull();
132
+ yield expect(line.children).toHaveLength(0);
133
+ yield expectCalmNote(canvasElement);
134
+ yield expectPhoneLayout(canvasElement);
78
135
  }),
79
136
  };
@@ -1,4 +1,4 @@
1
- import { Meta } from '@storybook/react';
1
+ import { Meta, StoryObj } from '@storybook/react';
2
2
  import { WelcomePageI } from '../../../interfaces';
3
3
  declare const _default: Meta<WelcomePageI>;
4
4
  export default _default;
@@ -9,3 +9,4 @@ export declare const WelcomePage: {
9
9
  videoUrl: string;
10
10
  };
11
11
  };
12
+ export declare const WelcomePhone: StoryObj<WelcomePageI>;
@@ -1,6 +1,17 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
1
10
  import { jsx as _jsx } from "react/jsx-runtime";
2
11
  import { Box } from '@chakra-ui/react';
12
+ import { fn, within } from '@storybook/test';
3
13
  import { Welcome } from '../../../components';
14
+ import { PHONE, expectCardFullWidth, expectSides, getCard } from '../phoneLayout';
4
15
  export default {
5
16
  title: 'Pages/Onboarding',
6
17
  component: Welcome,
@@ -12,3 +23,21 @@ WelcomePage.args = {
12
23
  name: 'Sam',
13
24
  videoUrl: 'https://www.youtube.com/embed/rkt_YtZYRDI',
14
25
  };
26
+ // The welcome page on a phone: the card spans the screen, and its text keeps
27
+ // 13px from the sides.
28
+ export const WelcomePhone = {
29
+ parameters: PHONE,
30
+ args: {
31
+ name: 'Jordan',
32
+ handleClick: fn(),
33
+ },
34
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
35
+ const canvas = within(canvasElement);
36
+ const card = getCard(canvasElement);
37
+ yield expectCardFullWidth(card);
38
+ yield expectSides(card, {
39
+ message: canvas.getByText('We hope you enjoy your time with Homefile.'),
40
+ signature: canvas.getByText('Your Homefile Crew'),
41
+ });
42
+ }),
43
+ };
@@ -0,0 +1,20 @@
1
+ export declare const PHONE: {
2
+ layout: string;
3
+ viewport: {
4
+ defaultViewport: string;
5
+ };
6
+ };
7
+ export declare const SMALL_TABLET: {
8
+ layout: string;
9
+ viewport: {
10
+ defaultViewport: string;
11
+ };
12
+ };
13
+ export declare const getCard: (canvasElement: HTMLElement) => HTMLElement;
14
+ export declare const expectCardFullWidth: (card: HTMLElement) => Promise<void>;
15
+ export declare const expectCardAtTop: (card: HTMLElement) => Promise<void>;
16
+ export declare const expectCardBelowTopGap: (card: HTMLElement) => Promise<void>;
17
+ export declare const expectNoCardShadow: (card: HTMLElement) => Promise<void>;
18
+ export declare const expectPageFillsScreen: (card: HTMLElement) => Promise<void>;
19
+ export declare const expectSides: (card: HTMLElement, elements: Record<string, Element | [Element, Element]>) => Promise<void>;
20
+ export declare const expectContentSides: (card: HTMLElement, elements: Record<string, HTMLElement>) => Promise<void>;
@@ -0,0 +1,89 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import { expect } from '@storybook/test';
11
+ // Onboarding pages on a phone (Matt, 2026-10-07): the card fills the screen
12
+ // from side to side, and its fields and buttons keep 13px from each side.
13
+ // The full-screen pages also start at the very top, with no bar above the card.
14
+ export const PHONE = {
15
+ layout: 'fullscreen',
16
+ viewport: { defaultViewport: 'iphone12' }, // 390x844
17
+ };
18
+ // A small tablet, between the 480px and 768px breakpoints (iPad mini, 744x1133).
19
+ export const SMALL_TABLET = {
20
+ layout: 'fullscreen',
21
+ viewport: { defaultViewport: 'ipadmini' },
22
+ };
23
+ const SIDE = 13;
24
+ // The onboarding top gap ('onboarding.top', 3.125rem).
25
+ const TOP_GAP = 50;
26
+ // The page's card: the first Chakra Container in the story.
27
+ export const getCard = (canvasElement) => canvasElement.querySelector('.chakra-container');
28
+ // The card spans the screen from side to side.
29
+ export const expectCardFullWidth = (card) => __awaiter(void 0, void 0, void 0, function* () {
30
+ const box = card.getBoundingClientRect();
31
+ yield expect(Math.round(box.left), 'card left').toBe(0);
32
+ yield expect(Math.round(box.right), 'card right').toBe(document.documentElement.clientWidth);
33
+ });
34
+ // The card starts at the very top of the screen: no bar above it.
35
+ export const expectCardAtTop = (card) => __awaiter(void 0, void 0, void 0, function* () {
36
+ yield expect(Math.round(card.getBoundingClientRect().top), 'card top').toBe(0);
37
+ });
38
+ // The card sits below the onboarding top gap, and is still a card: it has a
39
+ // shadow and is narrower than the screen (tablets and desktops).
40
+ export const expectCardBelowTopGap = (card) => __awaiter(void 0, void 0, void 0, function* () {
41
+ yield expect(Math.round(card.getBoundingClientRect().top), 'card top').toBe(TOP_GAP);
42
+ yield expect(getComputedStyle(card).boxShadow, 'card shadow').not.toBe('none');
43
+ yield expect(Math.round(card.getBoundingClientRect().width), 'card width').toBeLessThan(document.documentElement.clientWidth);
44
+ });
45
+ // The card has no shadow: on a phone it is the page, not a card on it.
46
+ export const expectNoCardShadow = (card) => __awaiter(void 0, void 0, void 0, function* () {
47
+ yield expect(getComputedStyle(card).boxShadow, 'card shadow').toBe('none');
48
+ });
49
+ // The page ends at the bottom of the screen, as sign in does on a phone: the
50
+ // card grows down to the copyright, and the copyright sits at the bottom of the
51
+ // screen. A page without the copyright: the card reaches the bottom.
52
+ export const expectPageFillsScreen = (card) => __awaiter(void 0, void 0, void 0, function* () {
53
+ const cardBottom = card.getBoundingClientRect().bottom;
54
+ const footer = card.nextElementSibling;
55
+ if (!footer) {
56
+ yield expect(Math.round(cardBottom), 'card bottom').toBe(window.innerHeight);
57
+ return;
58
+ }
59
+ const box = footer.getBoundingClientRect();
60
+ const style = getComputedStyle(footer);
61
+ yield expect(Math.round(box.bottom + parseFloat(style.marginBottom)), 'page bottom').toBe(window.innerHeight);
62
+ yield expect(Math.round(cardBottom + parseFloat(style.marginTop)), 'card bottom').toBe(Math.round(box.top));
63
+ });
64
+ // Each element sits 13px in from both sides of the card. A row given as
65
+ // [first, last] is measured from its first element's left side to its last
66
+ // element's right side.
67
+ export const expectSides = (card, elements) => __awaiter(void 0, void 0, void 0, function* () {
68
+ const box = card.getBoundingClientRect();
69
+ for (const [name, element] of Object.entries(elements)) {
70
+ const [first, last] = Array.isArray(element) ? element : [element, element];
71
+ const left = first.getBoundingClientRect().left;
72
+ const right = last.getBoundingClientRect().right;
73
+ yield expect(Math.round(left - box.left), `${name} left`).toBe(SIDE);
74
+ yield expect(Math.round(box.right - right), `${name} right`).toBe(SIDE);
75
+ }
76
+ });
77
+ // For a full-width block that sets the margin with its own padding (a centred
78
+ // title): its content starts 13px in from both sides of the card.
79
+ export const expectContentSides = (card, elements) => __awaiter(void 0, void 0, void 0, function* () {
80
+ const box = card.getBoundingClientRect();
81
+ for (const [name, element] of Object.entries(elements)) {
82
+ const rect = element.getBoundingClientRect();
83
+ const style = getComputedStyle(element);
84
+ const left = rect.left + parseFloat(style.borderLeftWidth) + parseFloat(style.paddingLeft);
85
+ const right = rect.right - parseFloat(style.borderRightWidth) - parseFloat(style.paddingRight);
86
+ yield expect(Math.round(left - box.left), `${name} left`).toBe(SIDE);
87
+ yield expect(Math.round(box.right - right), `${name} right`).toBe(SIDE);
88
+ }
89
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homefile/components-v2",
3
- "version": "2.63.0",
3
+ "version": "2.63.1",
4
4
  "author": "Homefile",
5
5
  "license": "UNLICENSED",
6
6
  "typings": "dist/index.d.ts",