@homefile/components-v2 2.62.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 (63) hide show
  1. package/dist/components/onboarding/CardFooter.js +2 -1
  2. package/dist/components/onboarding/PageTitle.d.ts +1 -1
  3. package/dist/components/onboarding/PageTitle.js +5 -3
  4. package/dist/components/onboarding/pages/ActivateAccount.js +3 -3
  5. package/dist/components/onboarding/pages/AuthHeader.js +2 -2
  6. package/dist/components/onboarding/pages/BasePageWrapper.d.ts +32 -1
  7. package/dist/components/onboarding/pages/BasePageWrapper.js +23 -3
  8. package/dist/components/onboarding/pages/EmailValidation.js +2 -2
  9. package/dist/components/onboarding/pages/MessagePage.d.ts +2 -0
  10. package/dist/components/onboarding/pages/MessagePage.js +11 -0
  11. package/dist/components/onboarding/pages/NewPassword.js +3 -3
  12. package/dist/components/onboarding/pages/ResendResetPassword.js +2 -2
  13. package/dist/components/onboarding/pages/ResetPassword.js +3 -3
  14. package/dist/components/onboarding/pages/SignIn.d.ts +1 -1
  15. package/dist/components/onboarding/pages/SignIn.js +4 -4
  16. package/dist/components/onboarding/pages/SignUp.js +3 -3
  17. package/dist/components/onboarding/pages/TwoFactor.d.ts +1 -1
  18. package/dist/components/onboarding/pages/TwoFactor.js +15 -4
  19. package/dist/components/onboarding/pages/Welcome.js +2 -2
  20. package/dist/components/onboarding/pages/index.d.ts +1 -0
  21. package/dist/components/onboarding/pages/index.js +1 -0
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/interfaces/onboarding/PageTitle.interface.d.ts +1 -0
  25. package/dist/interfaces/pages/MessagePage.interface.d.ts +6 -0
  26. package/dist/interfaces/pages/MessagePage.interface.js +1 -0
  27. package/dist/interfaces/pages/SignIn.interface.d.ts +3 -1
  28. package/dist/interfaces/pages/TwoFactor.interface.d.ts +6 -0
  29. package/dist/interfaces/pages/index.d.ts +1 -0
  30. package/dist/interfaces/pages/index.js +1 -0
  31. package/dist/stories/onboarding/CardFooter.stories.d.ts +2 -1
  32. package/dist/stories/onboarding/CardFooter.stories.js +24 -0
  33. package/dist/stories/onboarding/PageTitle.stories.d.ts +2 -1
  34. package/dist/stories/onboarding/PageTitle.stories.js +31 -0
  35. package/dist/stories/onboarding/pages/ActivateAccount.stories.d.ts +3 -1
  36. package/dist/stories/onboarding/pages/ActivateAccount.stories.js +41 -0
  37. package/dist/stories/onboarding/pages/EmailValidation.stories.d.ts +3 -1
  38. package/dist/stories/onboarding/pages/EmailValidation.stories.js +43 -0
  39. package/dist/stories/onboarding/pages/MessagePage.stories.d.ts +9 -0
  40. package/dist/stories/onboarding/pages/MessagePage.stories.js +73 -0
  41. package/dist/stories/onboarding/pages/NewPassword.stories.d.ts +3 -1
  42. package/dist/stories/onboarding/pages/NewPassword.stories.js +44 -0
  43. package/dist/stories/onboarding/pages/ResendResetPassword.stories.d.ts +3 -1
  44. package/dist/stories/onboarding/pages/ResendResetPassword.stories.js +47 -0
  45. package/dist/stories/onboarding/pages/ResetPassword.stories.d.ts +3 -1
  46. package/dist/stories/onboarding/pages/ResetPassword.stories.js +45 -0
  47. package/dist/stories/onboarding/pages/SignIn.stories.d.ts +10 -1
  48. package/dist/stories/onboarding/pages/SignIn.stories.js +96 -0
  49. package/dist/stories/onboarding/pages/SignUp.stories.d.ts +2 -1
  50. package/dist/stories/onboarding/pages/SignUp.stories.js +34 -0
  51. package/dist/stories/onboarding/pages/TwoFactor.stories.d.ts +11 -2
  52. package/dist/stories/onboarding/pages/TwoFactor.stories.js +118 -3
  53. package/dist/stories/onboarding/pages/Welcome.stories.d.ts +2 -1
  54. package/dist/stories/onboarding/pages/Welcome.stories.js +29 -0
  55. package/dist/stories/onboarding/phoneLayout.d.ts +20 -0
  56. package/dist/stories/onboarding/phoneLayout.js +89 -0
  57. package/dist/stories/partner/adminTiles/InboxCard.stories.d.ts +1 -1
  58. package/dist/stories/partner/adminTiles/InboxCard.stories.js +8 -5
  59. package/dist/stories/typography/NoWidow.stories.d.ts +8 -0
  60. package/dist/stories/typography/NoWidow.stories.js +61 -0
  61. package/dist/theme/components/TextStyles.d.ts +5 -0
  62. package/dist/theme/components/TextStyles.js +8 -0
  63. package/package.json +6 -1
@@ -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 { expect, fn, within } from '@storybook/test';
3
13
  import { TwoFactor } from '../../../components';
14
+ import { PHONE, expectCardAtTop, expectCardFullWidth, expectSides, getCard, } from '../phoneLayout';
4
15
  export default {
5
16
  title: 'Pages/Onboarding',
6
17
  component: TwoFactor,
@@ -12,10 +23,114 @@ export default {
12
23
  onReset: (code) => alert(`Code submitted: ${code}`),
13
24
  onResend: () => alert('Resend code clicked'),
14
25
  },
15
- decorators: [
16
- (Story) => (_jsx(Box, { p: "base", children: _jsx(Story, {}) })),
17
- ],
18
26
  };
19
27
  export const TwoFactorPage = (args) => {
20
28
  return _jsx(TwoFactor, Object.assign({}, args));
21
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];
33
+ TwoFactorPage.play = (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
34
+ const canvas = within(canvasElement);
35
+ // The default page is unchanged: its own title, subtitle and button.
36
+ yield expect(canvas.getByText('Please validate your identity to log in.')).toBeVisible();
37
+ yield expect(canvas.getByText('We’ve sent a 6-digit security code to your phone. Please enter that code below.')).toBeVisible();
38
+ yield expect(canvas.getByRole('button', { name: 'Submit' })).toBeVisible();
39
+ });
40
+ const CONNECT_HEADING = 'Check your email';
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.';
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
+ });
71
+ const connectArgs = {
72
+ twoFactorMethod: 'email',
73
+ heading: CONNECT_HEADING,
74
+ description: CONNECT_LINE,
75
+ note: CONNECT_NOTE,
76
+ submitLabel: 'Connect phone',
77
+ onReset: fn(),
78
+ onResend: fn(),
79
+ };
80
+ // The code step of the page a texter reaches from Homi's link.
81
+ export const TwoFactorConnect = {
82
+ parameters: PHONE,
83
+ args: connectArgs,
84
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
85
+ const canvas = within(canvasElement);
86
+ yield expect(canvas.getByText(CONNECT_HEADING)).toBeVisible();
87
+ yield expect(canvas.getByText(CONNECT_LINE)).toBeVisible();
88
+ yield expectCalmNote(canvasElement);
89
+ yield expect(canvas.getByRole('button', { name: 'Connect phone' })).toBeVisible();
90
+ yield expect(canvas.getByRole('button', { name: 'Resend new code' })).toBeVisible();
91
+ yield expect(canvas.queryByText('Please validate your identity to log in.')).toBeNull();
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);
96
+ }),
97
+ };
98
+ // After a wrong code: the error sits above the code field, and the calm line stays.
99
+ export const TwoFactorConnectWrongCode = {
100
+ parameters: PHONE,
101
+ args: Object.assign(Object.assign({}, connectArgs), { errorMessage: WRONG_CODE }),
102
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
103
+ const canvas = within(canvasElement);
104
+ const error = canvas.getByText(WRONG_CODE);
105
+ yield expect(error).toBeVisible();
106
+ yield expectCalmNote(canvasElement);
107
+ const codeField = canvas.getByPlaceholderText('Enter your code here');
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);
135
+ }),
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
+ });
@@ -1,5 +1,5 @@
1
1
  import { Meta } from '@storybook/react';
2
2
  import { InboxCardI } from '../../../interfaces';
3
- declare const _default: Meta;
3
+ declare const _default: Meta<InboxCardI>;
4
4
  export default _default;
5
5
  export declare const InboxCardComponent: (args: InboxCardI) => import("react/jsx-runtime").JSX.Element;
@@ -1,16 +1,19 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box } from '@chakra-ui/react';
3
+ import { fn } from '@storybook/test';
3
4
  import { InboxCard } from '../../../components';
4
5
  import { Document } from '../../../assets/images';
5
6
  export default {
6
7
  title: 'Components/Partner/Admin Tiles/Inbox',
7
8
  component: InboxCard,
8
9
  args: {
9
- image: Document,
10
- name: 'Inbox',
11
- newMessages: 10,
12
- showBorder: true,
13
- totalMessages: 32,
10
+ box: {
11
+ image: Document,
12
+ name: 'Inbox',
13
+ newMessages: 10,
14
+ totalMessages: 32,
15
+ },
16
+ onClick: fn(),
14
17
  },
15
18
  };
16
19
  export const InboxCardComponent = (args) => {
@@ -0,0 +1,8 @@
1
+ import { Meta, StoryObj } from '@storybook/react';
2
+ interface NoWidowArgs {
3
+ width: number;
4
+ }
5
+ declare const _default: Meta<NoWidowArgs>;
6
+ export default _default;
7
+ export declare const TitleAndBody: StoryObj<NoWidowArgs>;
8
+ export declare const NeverWrappingTextStaysOnOneLine: StoryObj<NoWidowArgs>;
@@ -0,0 +1,61 @@
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 { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ import { Box, Button, Stack, Text } from '@chakra-ui/react';
12
+ import { expect, within } from '@storybook/test';
13
+ export default {
14
+ title: 'Components/Typography/No Widow',
15
+ // A 390px phone screen: without the fix, both the title and the body end on one word.
16
+ args: { width: 390 },
17
+ };
18
+ const TITLE = 'Connect this phone to your Homefile account';
19
+ const BODY = 'We sent a 6-digit code to the email on your account. Enter it here to finish connecting this phone.';
20
+ // How many words sit on the last rendered line of an element's text.
21
+ const wordsOnLastLine = (element) => {
22
+ var _a, _b, _c, _d, _e;
23
+ const rows = new Map();
24
+ const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT);
25
+ for (let node = walker.nextNode(); node; node = walker.nextNode()) {
26
+ const text = (_a = node.textContent) !== null && _a !== void 0 ? _a : '';
27
+ for (const match of text.matchAll(/\S+/g)) {
28
+ const range = document.createRange();
29
+ range.setStart(node, (_b = match.index) !== null && _b !== void 0 ? _b : 0);
30
+ range.setEnd(node, ((_c = match.index) !== null && _c !== void 0 ? _c : 0) + match[0].length);
31
+ const top = Math.round(range.getBoundingClientRect().top);
32
+ rows.set(top, ((_d = rows.get(top)) !== null && _d !== void 0 ? _d : 0) + 1);
33
+ }
34
+ }
35
+ return (_e = rows.get(Math.max(...rows.keys()))) !== null && _e !== void 0 ? _e : 0;
36
+ };
37
+ export const TitleAndBody = {
38
+ render: ({ width }) => (_jsx(Box, { w: `${width}px`, p: "base", bg: "neutral.white", children: _jsxs(Stack, { spacing: "base", children: [_jsx(Text, { variant: "title", children: TITLE }), _jsx(Text, { children: BODY })] }) })),
39
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
40
+ const canvas = within(canvasElement);
41
+ const title = canvas.getByText(TITLE);
42
+ const body = canvas.getByText(BODY);
43
+ yield expect(getComputedStyle(title).getPropertyValue('text-wrap-style')).toBe('balance');
44
+ yield expect(getComputedStyle(body).getPropertyValue('text-wrap-style')).toBe('pretty');
45
+ // Neither block ends on a single word.
46
+ yield expect(wordsOnLastLine(title)).toBeGreaterThanOrEqual(2);
47
+ yield expect(wordsOnLastLine(body)).toBeGreaterThanOrEqual(2);
48
+ }),
49
+ };
50
+ const BUTTON_LABEL = 'Set Up Homi on this phone';
51
+ // The no-widow rule only changes HOW text breaks: text that never wraps (inside a
52
+ // button, a badge) must still stay on one line.
53
+ export const NeverWrappingTextStaysOnOneLine = {
54
+ render: () => (_jsx(Box, { w: "120px", p: "base", bg: "neutral.white", children: _jsx(Button, { w: "full", children: _jsx(Text, { children: BUTTON_LABEL }) }) })),
55
+ play: (_a) => __awaiter(void 0, [_a], void 0, function* ({ canvasElement }) {
56
+ const label = within(canvasElement).getByText(BUTTON_LABEL);
57
+ yield expect(getComputedStyle(label).getPropertyValue('text-wrap-mode')).toBe('nowrap');
58
+ const lineHeight = parseFloat(getComputedStyle(label).lineHeight);
59
+ yield expect(label.getBoundingClientRect().height).toBeLessThan(lineHeight * 1.5);
60
+ }),
61
+ };
@@ -2,24 +2,29 @@ export declare const Text: {
2
2
  baseStyle: {
3
3
  fontWeight: string;
4
4
  lineHeight: string;
5
+ textWrapStyle: string;
5
6
  };
6
7
  variants: {
7
8
  heading: {
9
+ textWrapStyle: string;
8
10
  fontSize: string;
9
11
  color: string;
10
12
  lineHeight: string;
11
13
  };
12
14
  heading6: {
15
+ textWrapStyle: string;
13
16
  fontSize: string;
14
17
  color: string;
15
18
  lineHeight: string;
16
19
  };
17
20
  title: {
21
+ textWrapStyle: string;
18
22
  fontSize: string;
19
23
  color: string;
20
24
  lineHeight: string;
21
25
  };
22
26
  titleBolder: {
27
+ textWrapStyle: string;
23
28
  fontSize: string;
24
29
  color: string;
25
30
  fontWeight: string;
@@ -2,24 +2,32 @@ export const Text = {
2
2
  baseStyle: {
3
3
  fontWeight: 'regular',
4
4
  lineHeight: '20px',
5
+ // No widows: running text never ends on a single word. The longhand only
6
+ // changes HOW lines break; the text-wrap shorthand would also undo the
7
+ // nowrap a Text inherits inside a Button or Badge.
8
+ textWrapStyle: 'pretty',
5
9
  },
6
10
  variants: {
7
11
  heading: {
12
+ textWrapStyle: 'balance',
8
13
  fontSize: '3xl',
9
14
  color: 'gray.3',
10
15
  lineHeight: '30px',
11
16
  },
12
17
  heading6: {
18
+ textWrapStyle: 'balance',
13
19
  fontSize: '2xl',
14
20
  color: 'gray.3',
15
21
  lineHeight: '30px',
16
22
  },
17
23
  title: {
24
+ textWrapStyle: 'balance',
18
25
  fontSize: 'lg',
19
26
  color: 'gray.3',
20
27
  lineHeight: '22px',
21
28
  },
22
29
  titleBolder: {
30
+ textWrapStyle: 'balance',
23
31
  fontSize: 'lg',
24
32
  color: 'gray.3',
25
33
  fontWeight: 'semibold',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@homefile/components-v2",
3
- "version": "2.62.0",
3
+ "version": "2.63.1",
4
4
  "author": "Homefile",
5
5
  "license": "UNLICENSED",
6
6
  "typings": "dist/index.d.ts",
@@ -71,6 +71,7 @@
71
71
  "@storybook/react": "^8.5.2",
72
72
  "@storybook/react-vite": "^8.5.2",
73
73
  "@storybook/test": "^8.5.2",
74
+ "@storybook/test-runner": "0.21.3",
74
75
  "@types/node": "^22.10.7",
75
76
  "@types/react": "^19.0.7",
76
77
  "@types/react-datepicker": "^7.0.0",
@@ -120,5 +121,9 @@
120
121
  "tsc-alias": "^1.8.16",
121
122
  "valtio": "^2.1.3",
122
123
  "yup": "^1.6.1"
124
+ },
125
+ "resolutions": {
126
+ "playwright": "1.58.2",
127
+ "playwright-core": "1.58.2"
123
128
  }
124
129
  }