@homefile/components-v2 2.59.2 → 2.60.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 (169) hide show
  1. package/dist/assets/locales/en/index.json +5 -2
  2. package/dist/components/folderPanel/displayFiles/GridFile.js +9 -1
  3. package/dist/components/folderPanel/displayFiles/ListFile.js +9 -1
  4. package/dist/components/forms/dynamicForm/DynamicForm.js +1 -1
  5. package/dist/components/forms/dynamicForm/HiddenField.js +2 -2
  6. package/dist/components/forms/dynamicForm/fields/HomeInitialSetup.js +3 -1
  7. package/dist/components/forms/readOnly/ReadOnlyDynamicForm.js +10 -2
  8. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyAppliances.js +21 -22
  9. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyGuidelines.js +6 -3
  10. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyHistory.d.ts +2 -0
  11. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyHistory.js +35 -0
  12. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyItemRelated.js +8 -3
  13. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyOverview.d.ts +2 -0
  14. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyOverview.js +16 -0
  15. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyPurchaseDetails.d.ts +2 -0
  16. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyPurchaseDetails.js +30 -0
  17. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyWarrantyBanner.d.ts +2 -0
  18. package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyWarrantyBanner.js +42 -0
  19. package/dist/components/forms/readOnly/fields/homeItem/index.d.ts +4 -0
  20. package/dist/components/forms/readOnly/fields/homeItem/index.js +4 -0
  21. package/dist/components/headers/TabsHeader.js +1 -1
  22. package/dist/components/history/AddDocuments.d.ts +7 -0
  23. package/dist/components/history/AddDocuments.js +8 -0
  24. package/dist/components/history/DatePennant.d.ts +7 -0
  25. package/dist/components/history/DatePennant.js +6 -0
  26. package/dist/components/history/HistoryCard.d.ts +19 -0
  27. package/dist/components/history/HistoryCard.js +13 -0
  28. package/dist/components/history/index.d.ts +3 -0
  29. package/dist/components/history/index.js +3 -0
  30. package/dist/components/homeBoard/AddToHomefile.js +1 -1
  31. package/dist/components/homeBoard/HomeCard.js +1 -1
  32. package/dist/components/homeItems/ViewingHomeItemBody.d.ts +2 -0
  33. package/dist/components/homeItems/ViewingHomeItemBody.js +41 -0
  34. package/dist/components/homeItems/ViewingHomeItemPanel.js +17 -19
  35. package/dist/components/homeItems/index.d.ts +1 -0
  36. package/dist/components/homeItems/index.js +1 -0
  37. package/dist/components/homi/HomiPresence.d.ts +2 -0
  38. package/dist/components/homi/HomiPresence.js +31 -0
  39. package/dist/components/homi/HomiVoice.d.ts +2 -0
  40. package/dist/components/homi/HomiVoice.js +6 -0
  41. package/dist/components/homi/ProgressBar.d.ts +2 -0
  42. package/dist/components/homi/ProgressBar.js +9 -0
  43. package/dist/components/homi/RevealValueCard.d.ts +2 -0
  44. package/dist/components/homi/RevealValueCard.js +23 -0
  45. package/dist/components/homi/StaggerReveal.d.ts +2 -0
  46. package/dist/components/homi/StaggerReveal.js +29 -0
  47. package/dist/components/homi/index.d.ts +5 -0
  48. package/dist/components/homi/index.js +5 -0
  49. package/dist/components/inboxTile/InboxForwardBanner.js +1 -1
  50. package/dist/components/index.d.ts +2 -0
  51. package/dist/components/index.js +2 -0
  52. package/dist/components/inputs/InlineEditField.d.ts +2 -0
  53. package/dist/components/inputs/InlineEditField.js +15 -0
  54. package/dist/components/inputs/RadioCard.d.ts +3 -1
  55. package/dist/components/inputs/RadioCard.js +3 -3
  56. package/dist/components/inputs/TextInput.d.ts +1 -1
  57. package/dist/components/inputs/TextInput.js +11 -4
  58. package/dist/components/inputs/index.d.ts +1 -0
  59. package/dist/components/inputs/index.js +1 -0
  60. package/dist/components/myHomes/HomeDetailsContent.js +1 -1
  61. package/dist/components/myHomes/IncludePublicRecords.d.ts +13 -1
  62. package/dist/components/myHomes/IncludePublicRecords.js +16 -6
  63. package/dist/components/myHomes/edithome/EditHomeDetails.js +12 -3
  64. package/dist/components/myHomes/steps/NewHomeDetails.d.ts +1 -1
  65. package/dist/components/myHomes/steps/NewHomeDetails.js +3 -3
  66. package/dist/components/myProfile/MyProfileBody.d.ts +1 -1
  67. package/dist/components/myProfile/MyProfileBody.js +2 -2
  68. package/dist/components/myProfile/details/HomiSms.d.ts +1 -1
  69. package/dist/components/myProfile/details/HomiSms.js +53 -23
  70. package/dist/components/myProfile/details/TwoFactorSetting.js +2 -1
  71. package/dist/components/rooms/RoomHeader.d.ts +1 -1
  72. package/dist/components/rooms/RoomHeader.js +3 -3
  73. package/dist/components/wizard/ControlledWizard.d.ts +1 -1
  74. package/dist/components/wizard/ControlledWizard.js +42 -3
  75. package/dist/components/wizard/WizardInitialOptions.js +3 -3
  76. package/dist/components/wizard/WizardSelectCard.d.ts +14 -0
  77. package/dist/components/wizard/WizardSelectCard.js +32 -0
  78. package/dist/components/wizard/WizardStepPrompt.d.ts +11 -0
  79. package/dist/components/wizard/WizardStepPrompt.js +20 -0
  80. package/dist/components/wizard/WizardSuccessHeader.d.ts +1 -1
  81. package/dist/components/wizard/WizardSuccessHeader.js +2 -2
  82. package/dist/components/wizard/index.d.ts +2 -0
  83. package/dist/components/wizard/index.js +2 -0
  84. package/dist/helpers/forms/dynamicForm.helper.js +4 -0
  85. package/dist/hooks/folderPanel/useDisplayFiles.js +11 -2
  86. package/dist/hooks/forms/useAddressStored.js +8 -2
  87. package/dist/hooks/index.d.ts +2 -0
  88. package/dist/hooks/index.js +2 -0
  89. package/dist/hooks/motion/index.d.ts +2 -0
  90. package/dist/hooks/motion/index.js +2 -0
  91. package/dist/hooks/motion/useCountUp.d.ts +10 -0
  92. package/dist/hooks/motion/useCountUp.js +39 -0
  93. package/dist/hooks/motion/usePrefersReducedMotion.d.ts +14 -0
  94. package/dist/hooks/motion/usePrefersReducedMotion.js +15 -0
  95. package/dist/hooks/rooms/useRoomHeader.d.ts +4 -1
  96. package/dist/hooks/rooms/useRoomHeader.js +9 -1
  97. package/dist/hooks/useDirtySave.d.ts +14 -0
  98. package/dist/hooks/useDirtySave.js +33 -0
  99. package/dist/index.d.ts +3 -3
  100. package/dist/index.js +3 -3
  101. package/dist/interfaces/forms/dynamicForm/DynamicForm.interface.d.ts +7 -3
  102. package/dist/interfaces/forms/readOnly/homeItem/ReadOnlyOverview.interface.d.ts +2 -0
  103. package/dist/interfaces/forms/readOnly/homeItem/ReadOnlyOverview.interface.js +1 -0
  104. package/dist/interfaces/forms/readOnly/homeItem/ReadOnlyPurchaseDetails.interface.d.ts +5 -0
  105. package/dist/interfaces/forms/readOnly/homeItem/ReadOnlyPurchaseDetails.interface.js +1 -0
  106. package/dist/interfaces/forms/readOnly/homeItem/ReadOnlyWarrantyBanner.interface.d.ts +27 -0
  107. package/dist/interfaces/forms/readOnly/homeItem/ReadOnlyWarrantyBanner.interface.js +1 -0
  108. package/dist/interfaces/forms/readOnly/homeItem/index.d.ts +3 -0
  109. package/dist/interfaces/forms/readOnly/homeItem/index.js +3 -0
  110. package/dist/interfaces/homeItems/ViewingHomeItemPanel.interface.d.ts +1 -0
  111. package/dist/interfaces/homi/HomiPresence.interface.d.ts +9 -0
  112. package/dist/interfaces/homi/HomiPresence.interface.js +1 -0
  113. package/dist/interfaces/homi/ProgressBar.interface.d.ts +8 -0
  114. package/dist/interfaces/homi/ProgressBar.interface.js +1 -0
  115. package/dist/interfaces/homi/RevealValueCard.interface.d.ts +16 -0
  116. package/dist/interfaces/homi/RevealValueCard.interface.js +1 -0
  117. package/dist/interfaces/homi/StaggerReveal.interface.d.ts +9 -0
  118. package/dist/interfaces/homi/StaggerReveal.interface.js +1 -0
  119. package/dist/interfaces/homi/index.d.ts +4 -0
  120. package/dist/interfaces/homi/index.js +4 -0
  121. package/dist/interfaces/index.d.ts +1 -0
  122. package/dist/interfaces/index.js +1 -0
  123. package/dist/interfaces/inputs/InlineEditField.interface.d.ts +7 -0
  124. package/dist/interfaces/inputs/InlineEditField.interface.js +1 -0
  125. package/dist/interfaces/inputs/Input.interface.d.ts +6 -1
  126. package/dist/interfaces/inputs/index.d.ts +1 -0
  127. package/dist/interfaces/inputs/index.js +1 -0
  128. package/dist/interfaces/myHomes/AddHomeContent.interface.d.ts +3 -0
  129. package/dist/interfaces/myProfile/MyProfileBody.interface.d.ts +1 -0
  130. package/dist/interfaces/myProfile/details/HomiSms.interface.d.ts +11 -2
  131. package/dist/interfaces/rooms/RoomHeader.interface.d.ts +5 -0
  132. package/dist/interfaces/wizard/ControlledWizard.interface.d.ts +6 -0
  133. package/dist/interfaces/wizard/WizardStepPrompt.interface.d.ts +9 -0
  134. package/dist/interfaces/wizard/WizardStepPrompt.interface.js +1 -0
  135. package/dist/interfaces/wizard/index.d.ts +1 -0
  136. package/dist/interfaces/wizard/index.js +1 -0
  137. package/dist/stories/forms/readOnly/ReadOnlyAppliances.stories.d.ts +4 -0
  138. package/dist/stories/forms/readOnly/ReadOnlyAppliances.stories.js +33 -0
  139. package/dist/stories/forms/readOnly/ReadOnlyWarrantyBanner.stories.d.ts +4 -0
  140. package/dist/stories/forms/readOnly/ReadOnlyWarrantyBanner.stories.js +10 -0
  141. package/dist/stories/homeItems/ReadOnlyHistory.stories.d.ts +4 -0
  142. package/dist/stories/homeItems/ReadOnlyHistory.stories.js +46 -0
  143. package/dist/stories/homeItems/ReadOnlyOverview.stories.d.ts +5 -0
  144. package/dist/stories/homeItems/ReadOnlyOverview.stories.js +69 -0
  145. package/dist/stories/homeItems/ViewingHomeItemPanel.stories.d.ts +4 -0
  146. package/dist/stories/homeItems/ViewingHomeItemPanel.stories.js +204 -0
  147. package/dist/stories/homi/HomiPresence.stories.d.ts +18 -0
  148. package/dist/stories/homi/HomiPresence.stories.js +11 -0
  149. package/dist/stories/homi/HomiVoice.stories.d.ts +4 -0
  150. package/dist/stories/homi/HomiVoice.stories.js +8 -0
  151. package/dist/stories/homi/ProgressBar.stories.d.ts +10 -0
  152. package/dist/stories/homi/ProgressBar.stories.js +9 -0
  153. package/dist/stories/homi/RevealValueCard.stories.d.ts +19 -0
  154. package/dist/stories/homi/RevealValueCard.stories.js +22 -0
  155. package/dist/stories/homi/StaggerReveal.stories.d.ts +5 -0
  156. package/dist/stories/homi/StaggerReveal.stories.js +8 -0
  157. package/dist/stories/inputs/InlineEditField.stories.d.ts +10 -0
  158. package/dist/stories/inputs/InlineEditField.stories.js +11 -0
  159. package/dist/stories/myProfile/MyProfilePanel.stories.js +1 -1
  160. package/dist/stories/myProfile/details/HomiSms.stories.d.ts +3 -2
  161. package/dist/stories/myProfile/details/HomiSms.stories.js +3 -2
  162. package/dist/stories/wizard/ControlledWizard.stories.d.ts +9 -0
  163. package/dist/stories/wizard/ControlledWizard.stories.js +41 -60
  164. package/dist/theme/index.d.ts +3 -0
  165. package/dist/theme/index.js +7 -4
  166. package/dist/utils/Formaters.utils.d.ts +2 -0
  167. package/dist/utils/Formaters.utils.js +14 -0
  168. package/dist/utils/Weather.utils.js +11 -4
  169. package/package.json +1 -1
@@ -3,33 +3,63 @@ import { t } from 'i18next';
3
3
  import { Controller, useForm, useWatch } from 'react-hook-form';
4
4
  import { useEffect, useState } from 'react';
5
5
  import * as yup from 'yup';
6
- import { Stack, Flex, Img, Text, Box, FormControl, FormLabel, Switch, Checkbox, Button, } from '@chakra-ui/react';
6
+ import { Stack, Flex, Img, Text, Box, FormControl, FormLabel, Switch, Link, } from '@chakra-ui/react';
7
7
  import { SMS } from '../../../assets/images';
8
8
  import { ControlledInput, Loading } from '../../../components';
9
- export const HomiSms = ({ mode = 'full', sms = '', loading, onChange, onSave, }) => {
10
- var _a;
9
+ import { formatPhone, toPhoneDigits } from '../../../utils';
10
+ // "Enable Homi SMS Messaging" account setting. The ON/OFF toggle IS the enable
11
+ // — no separate SMS checkbox (redundant). The toggle is controllable
12
+ // (`enabled`) so it reflects real state, e.g. ON with the user's number for
13
+ // accounts Homi created via SMS. The phone is disabled until enabled and is
14
+ // pre-filled by the app from the 2FA-SMS number when present. A consent link
15
+ // (`consentUrl`) points to the SMS messaging consent language (legal's
16
+ // content). Saving is IMPLICIT — flipping the toggle saves immediately, and the
17
+ // number saves on blur once valid — so there is no Save button.
18
+ // `mode='phone-only'` renders just the phone field for flows that only need the
19
+ // number.
20
+ export const HomiSms = ({ mode = 'full', enabled = false, phone = '', consentUrl, loading, onChange, onSave, }) => {
11
21
  const isPhoneOnly = mode === 'phone-only';
12
- const { control, handleSubmit } = useForm({
13
- defaultValues: { sms },
14
- });
22
+ const { control, trigger } = useForm({ defaultValues: { phone } });
15
23
  const values = useWatch({ control });
16
- const smsValue = (_a = values === null || values === void 0 ? void 0 : values.sms) !== null && _a !== void 0 ? _a : '';
17
- const [isEnabled, setIsEnabled] = useState(false);
18
- const [isSmsEnabled, setIsSmsEnabled] = useState(true);
19
- const showButtons = isPhoneOnly
20
- ? smsValue !== sms
21
- : smsValue !== sms || isEnabled || !isSmsEnabled;
22
- const handlePhoneValidation = (value) => {
23
- if (isPhoneOnly)
24
- return yup.string().required().min(10).max(10).isValidSync(value);
25
- if (isEnabled && isSmsEnabled)
26
- return yup.string().required().min(10).max(10).isValidSync(value);
27
- return true;
28
- };
24
+ const [isEnabled, setIsEnabled] = useState(enabled);
25
+ const isPhoneValid = (value) => yup.string().required().min(10).max(10).isValidSync(value);
26
+ // Optional live-change listener.
27
+ useEffect(() => {
28
+ onChange === null || onChange === void 0 ? void 0 : onChange({ enabled: isEnabled, phone: values.phone });
29
+ }, [onChange, values, isEnabled]);
30
+ // Enabling requires a phone number: turning it on surfaces the required
31
+ // error until a valid number is entered.
29
32
  useEffect(() => {
30
- if (onChange && values) {
31
- onChange({ sms: values.sms });
33
+ if (isEnabled)
34
+ trigger('phone');
35
+ }, [isEnabled, trigger]);
36
+ // Implicit save: the toggle saves the instant it flips; the number saves on
37
+ // blur once it is valid. No Save button. Turning ON only persists `enabled`
38
+ // once there is a valid number (otherwise the error shows and the save waits
39
+ // for the number's blur); turning OFF always saves immediately.
40
+ const handleToggle = (checked) => {
41
+ setIsEnabled(checked);
42
+ if (!checked)
43
+ onSave === null || onSave === void 0 ? void 0 : onSave({ enabled: false, phone: values.phone });
44
+ else if (isPhoneValid(values.phone))
45
+ onSave === null || onSave === void 0 ? void 0 : onSave({ enabled: true, phone: values.phone });
46
+ };
47
+ const handlePhoneBlur = (value, fieldBlur) => {
48
+ fieldBlur === null || fieldBlur === void 0 ? void 0 : fieldBlur();
49
+ // Cleared number → DELETE it, and turn messaging off (it can't be on
50
+ // without a number). This is distinct from toggling off, which pauses
51
+ // messaging but keeps the number. The 2FA number is unaffected either way.
52
+ if (toPhoneDigits(value).length === 0) {
53
+ setIsEnabled(false);
54
+ onSave === null || onSave === void 0 ? void 0 : onSave({ enabled: false, phone: '' });
55
+ return;
32
56
  }
33
- }, [onChange, values]);
34
- return (_jsxs(Box, { position: "relative", children: [loading && _jsx(Loading, {}), _jsxs(Stack, { bg: "lightBlue.2", px: "base", py: "4", spacing: "4", children: [_jsxs(Stack, { spacing: "3", children: [_jsxs(Flex, { align: "center", justify: "space-between", children: [_jsxs(Flex, { gap: "base", align: "center", children: [_jsx(Img, { src: SMS, alt: t('myProfile.details.homiSmsAlt') }), _jsx(Text, { fontSize: "sm", textTransform: "uppercase", children: t('myProfile.details.homiSmsTitle') })] }), !isPhoneOnly && (_jsxs(FormControl, { display: "flex", alignItems: "center", w: "fit-content", children: [_jsx(FormLabel, { htmlFor: "homi-sms", mb: "0", fontSize: "sm", children: isEnabled ? 'ON' : 'OFF' }), _jsx(Switch, { id: "homi-sms", size: "lg", isChecked: isEnabled, onChange: (e) => setIsEnabled(e.target.checked) })] }))] }), _jsx(Text, { fontFamily: "secondary", lineHeight: "1.2", children: t('myProfile.details.homiSmsSubtitle') })] }), _jsxs(Flex, { gap: "base", align: "center", justify: "space-between", children: [!isPhoneOnly && (_jsx(Checkbox, { size: "md", isChecked: isSmsEnabled, onChange: (e) => setIsSmsEnabled(e.target.checked), isDisabled: !isEnabled, children: _jsx(Text, { variant: "info", fontWeight: "normal", children: "SMS" }) })), _jsx(Box, { w: isPhoneOnly ? '100%' : '75%', children: _jsx(Controller, { name: "sms", control: control, rules: { validate: handlePhoneValidation }, render: ({ field: { value, onBlur, onChange }, fieldState: { invalid }, }) => (_jsx(ControlledInput, { placeholder: t('myProfile.placeholders.sms'), onChange: onChange, onBlur: onBlur, value: value !== null && value !== void 0 ? value : '', type: "tel", hasError: invalid, errorMessage: `${t('forms.invalid')} ${t('forms.phone')}`, isDisabled: !isPhoneOnly && (!isEnabled || !isSmsEnabled) })) }) })] }), showButtons && onSave && (_jsx(Box, { w: "75%", alignSelf: "end", children: _jsx(Button, { variant: "secondaryFooter", type: "submit", bg: "neutral.white", fontWeight: "medium", onClick: handleSubmit(onSave), children: t('buttons.save').toUpperCase() }) }))] })] }));
57
+ if (!isPhoneValid(value))
58
+ return;
59
+ if (isPhoneOnly)
60
+ onSave === null || onSave === void 0 ? void 0 : onSave({ phone: value });
61
+ else if (isEnabled)
62
+ onSave === null || onSave === void 0 ? void 0 : onSave({ enabled: isEnabled, phone: value });
63
+ };
64
+ return (_jsxs(Box, { position: "relative", children: [loading && _jsx(Loading, {}), _jsxs(Stack, { bg: "lightBlue.2", px: "base", py: "4", spacing: "4", children: [_jsxs(Stack, { spacing: "3", children: [_jsxs(Flex, { align: "center", justify: "space-between", children: [_jsxs(Flex, { gap: "base", align: "center", children: [_jsx(Img, { src: SMS, alt: t('myProfile.details.homiSmsAlt') }), _jsx(Text, { fontSize: "sm", textTransform: "uppercase", children: t('myProfile.details.homiSmsTitle') })] }), !isPhoneOnly && (_jsxs(FormControl, { display: "flex", alignItems: "center", w: "fit-content", children: [_jsx(FormLabel, { htmlFor: "homi-sms", mb: "0", fontSize: "sm", children: isEnabled ? 'ON' : 'OFF' }), _jsx(Switch, { id: "homi-sms", size: "lg", isChecked: isEnabled, onChange: (e) => handleToggle(e.target.checked) })] }))] }), !isPhoneOnly && (_jsxs(Stack, { spacing: "1", children: [_jsx(Text, { fontFamily: "secondary", lineHeight: "1.2", children: t('myProfile.details.homiSmsSubtitle') }), consentUrl && (_jsx(Link, { href: consentUrl, isExternal: true, w: "fit-content", fontFamily: "secondary", fontSize: "sm", color: "blue.2", textDecoration: "underline", children: t('myProfile.details.homiSmsConsent') }))] }))] }), _jsx(Controller, { name: "phone", control: control, rules: { validate: (value) => isPhoneOnly || !isEnabled || isPhoneValid(value) }, render: ({ field: { value, onBlur, onChange }, fieldState: { invalid }, }) => (_jsx(ControlledInput, { placeholder: t('myProfile.placeholders.sms'), onChange: (e) => onChange(toPhoneDigits(e.target.value)), onBlur: () => handlePhoneBlur(value, onBlur), value: formatPhone(value), type: "tel", hasError: invalid, errorMessage: `${t('forms.invalid')} ${t('forms.phone')}`, isDisabled: !isPhoneOnly && !isEnabled })) })] })] }));
35
65
  };
@@ -4,6 +4,7 @@ import { Controller } from 'react-hook-form';
4
4
  import { Stack, Flex, Img, Text, Box, FormControl, FormLabel, Switch, Button, } from '@chakra-ui/react';
5
5
  import { Security } from '../../../assets/images';
6
6
  import { ControlledInput, Loading, RadioButton, TextInput, } from '../../../components';
7
+ import { formatPhone, toPhoneDigits } from '../../../utils';
7
8
  import { useTwoFactorSetting } from '../../../hooks';
8
9
  export const TwoFactorSetting = ({ defaultValue = 'email', email = '', loading, onChange, onSave, sms = '', twoFactor, }) => {
9
10
  const { altText, control, emailRadio, group, handleEmailValidation, handlePhoneValidation, handleSubmit, is2faEnabled, radioValue, showButtons, smsRadio, } = useTwoFactorSetting({
@@ -17,5 +18,5 @@ export const TwoFactorSetting = ({ defaultValue = 'email', email = '', loading,
17
18
  return (_jsxs(Box, { position: "relative", children: [loading && _jsx(Loading, {}), _jsxs(Stack, { bg: "lightBlue.1", px: "base", py: "4", spacing: "8", children: [_jsxs(Stack, { spacing: "4", children: [_jsxs(Flex, { align: "center", justify: "space-between", children: [_jsxs(Flex, { gap: "base", align: "center", children: [_jsx(Img, { src: Security, alt: altText }), _jsx(Text, { fontSize: "sm", textTransform: "uppercase", children: t('myProfile.details.2FactorTitle') })] }), _jsx(Controller, { name: "2fa", control: control, render: ({ field: { value, onChange, onBlur } }) => {
18
19
  const label = value ? 'ON' : 'OFF';
19
20
  return (_jsxs(FormControl, { display: "flex", alignItems: "center", w: "fit-content", children: [_jsx(FormLabel, { htmlFor: "2fa", mb: "0", fontSize: "sm", children: label }), _jsx(Switch, { id: "2fa", size: "lg", isChecked: value, onChange: onChange, onBlur: onBlur })] }));
20
- } })] }), _jsx(Text, { fontFamily: "secondary", lineHeight: "1.2", children: t('myProfile.details.2FactorSubtitle') })] }), _jsxs(Stack, Object.assign({}, group, { spacing: "4", children: [_jsxs(Flex, { gap: "base", align: "center", justify: "space-between", children: [_jsx(RadioButton, Object.assign({}, emailRadio, { isDisabled: !is2faEnabled, children: "Email" })), _jsx(Box, { w: "75%", children: _jsx(Controller, { name: "email", control: control, rules: { validate: handleEmailValidation }, render: ({ field: { value, onBlur, onChange }, fieldState: { invalid }, }) => (_jsx(TextInput, { placeholder: t('myProfile.placeholders.email'), handleChange: onChange, onBlur: onBlur, value: value, hasError: invalid, errorMessage: `${t('forms.invalid')} ${t('forms.email')}`, isDisabled: true })) }) })] }), _jsxs(Flex, { gap: "base", align: "center", justify: "space-between", children: [_jsx(RadioButton, Object.assign({}, smsRadio, { isDisabled: !is2faEnabled, children: "SMS" })), _jsx(Box, { w: "75%", children: _jsx(Controller, { name: "sms", control: control, rules: { validate: handlePhoneValidation }, render: ({ field: { value, onBlur, onChange }, fieldState: { invalid }, }) => (_jsx(ControlledInput, { placeholder: t('myProfile.placeholders.sms'), onChange: onChange, onBlur: onBlur, value: value, type: "number", hasError: invalid, errorMessage: `${t('forms.invalid')} ${t('forms.phone')}`, isDisabled: radioValue === 'email' || !is2faEnabled })) }) })] }), _jsx(Box, { w: "75%", alignSelf: "end", children: showButtons && (_jsx(Button, { variant: "secondaryFooter", type: "submit", bg: "neutral.white", fontWeight: "medium", onClick: handleSubmit(onSave), children: t('buttons.save').toUpperCase() })) })] }))] })] }));
21
+ } })] }), _jsx(Text, { fontFamily: "secondary", lineHeight: "1.2", children: t('myProfile.details.2FactorSubtitle') })] }), _jsxs(Stack, Object.assign({}, group, { spacing: "4", children: [_jsxs(Flex, { gap: "base", align: "center", justify: "space-between", children: [_jsx(RadioButton, Object.assign({}, emailRadio, { isDisabled: !is2faEnabled, children: "Email" })), _jsx(Box, { w: "75%", children: _jsx(Controller, { name: "email", control: control, rules: { validate: handleEmailValidation }, render: ({ field: { value, onBlur, onChange }, fieldState: { invalid }, }) => (_jsx(TextInput, { placeholder: t('myProfile.placeholders.email'), handleChange: onChange, onBlur: onBlur, value: value, hasError: invalid, errorMessage: `${t('forms.invalid')} ${t('forms.email')}`, isDisabled: true })) }) })] }), _jsxs(Flex, { gap: "base", align: "center", justify: "space-between", children: [_jsx(RadioButton, Object.assign({}, smsRadio, { isDisabled: !is2faEnabled, children: "SMS" })), _jsx(Box, { w: "75%", children: _jsx(Controller, { name: "sms", control: control, rules: { validate: handlePhoneValidation }, render: ({ field: { value, onBlur, onChange }, fieldState: { invalid }, }) => (_jsx(ControlledInput, { placeholder: t('myProfile.placeholders.sms'), onChange: (e) => onChange(toPhoneDigits(e.target.value)), onBlur: onBlur, value: formatPhone(value), type: "tel", hasError: invalid, errorMessage: `${t('forms.invalid')} ${t('forms.phone')}`, isDisabled: radioValue === 'email' || !is2faEnabled })) }) })] }), _jsx(Box, { w: "75%", alignSelf: "end", children: showButtons && (_jsx(Button, { variant: "secondaryFooter", type: "submit", bg: "neutral.white", fontWeight: "medium", onClick: handleSubmit(onSave), children: t('buttons.save').toUpperCase() })) })] }))] })] }));
21
22
  };
@@ -1,2 +1,2 @@
1
1
  import { RoomHeaderI } from '../../interfaces';
2
- export declare const RoomHeader: ({ isDeleteDisabled, onClickDelete, onSubmit, room, roomAmount, disabled, }: RoomHeaderI) => import("react/jsx-runtime").JSX.Element;
2
+ export declare const RoomHeader: ({ isDeleteDisabled, onClickDelete, onSubmit, room, roomAmount, roomEstimatedAmount, disabled, }: RoomHeaderI) => import("react/jsx-runtime").JSX.Element;
@@ -4,9 +4,9 @@ import { Box, Flex, Center, Image, IconButton, Text, Show, Button, } from '@chak
4
4
  import { TextInput, TrashIcon, Edit } from '../../components';
5
5
  import { roomIcons } from '../../helpers';
6
6
  import { useRoomHeader } from '../../hooks';
7
- export const RoomHeader = ({ isDeleteDisabled, onClickDelete, onSubmit, room, roomAmount, disabled, }) => {
8
- const { formattedAmount, handleChange, handleClick, inputValue, isEditing } = useRoomHeader({ onSubmit, room, roomAmount });
7
+ export const RoomHeader = ({ isDeleteDisabled, onClickDelete, onSubmit, room, roomAmount, roomEstimatedAmount, disabled, }) => {
8
+ const { formattedAmount, formattedEstimatedAmount, hasEstimate, handleChange, handleClick, inputValue, isEditing, } = useRoomHeader({ onSubmit, room, roomAmount, roomEstimatedAmount });
9
9
  return (_jsx(Box, { w: "full", bg: "lightBlue.4", py: "2", px: "base", "data-testid": "room-header", children: _jsxs(Flex, { justify: "space-between", align: "center", children: [_jsxs(Flex, { w: "fit-content", children: [_jsx(Center, { h: "2.3rem", minW: "2.6rem", mr: "2", children: _jsx(Image, { alt: room.name, maxH: "2rem", src: roomIcons[room.type], w: "auto" }) }), _jsx(TextInput, { fontWeight: isEditing ? 'regular' : 'medium', fontSize: "1.25rem", fontFamily: "primary", handleChange: handleChange, height: "full", id: room.name, isDisabled: !isEditing || disabled, placeholder: "", value: isEditing && !disabled
10
10
  ? inputValue || ''
11
- : (inputValue === null || inputValue === void 0 ? void 0 : inputValue.toUpperCase()) || '' }), isEditing && !disabled ? (_jsx(Button, { variant: "tertiary", onClick: handleClick, ml: "2", children: t('roomHeader.saveBtn') })) : (_jsx(IconButton, { "aria-label": t('ariaLabels.edit'), icon: _jsx(Edit, {}), maxW: "fit-content", onClick: handleClick, px: "2", variant: "menuIconWithShadow", id: "customizeRoomName", disabled: disabled }))] }), _jsxs(Flex, { align: "center", gap: "6", children: [_jsxs(Flex, { align: "center", gap: "6", id: "trackRoomValue", bg: "lightBlue.4", children: [_jsx(Show, { breakpoint: "(min-width: 600px)", children: _jsx(Text, { fontWeight: "medium", variant: "caption", children: t('roomHeader.roomValue').toUpperCase() }) }), _jsx(Text, { variant: "titleBolder", children: formattedAmount }), _jsx(Show, { breakpoint: "(min-width: 600px)", children: _jsx(Box, { h: "5", w: "1px", bg: "lightBlue.6" }) })] }), _jsx(IconButton, { "aria-label": t('ariaLabels.delete'), icon: _jsx(TrashIcon, {}), maxW: "fit-content", onClick: () => onClickDelete(room._id), disabled: isDeleteDisabled, px: "1", variant: "menuIconWithShadow" })] })] }) }));
11
+ : (inputValue === null || inputValue === void 0 ? void 0 : inputValue.toUpperCase()) || '' }), isEditing && !disabled ? (_jsx(Button, { variant: "tertiary", onClick: handleClick, ml: "2", children: t('roomHeader.saveBtn') })) : (_jsx(IconButton, { "aria-label": t('ariaLabels.edit'), icon: _jsx(Edit, {}), maxW: "fit-content", onClick: handleClick, px: "2", variant: "menuIconWithShadow", id: "customizeRoomName", disabled: disabled }))] }), _jsxs(Flex, { align: "center", gap: "6", children: [_jsxs(Flex, { align: "center", gap: "6", id: "trackRoomValue", bg: "lightBlue.4", children: [_jsx(Show, { breakpoint: "(min-width: 600px)", children: _jsx(Text, { fontWeight: "medium", variant: "caption", children: t('roomHeader.roomValue').toUpperCase() }) }), _jsx(Text, { variant: "titleBolder", children: formattedAmount }), _jsx(Show, { breakpoint: "(min-width: 600px)", children: _jsx(Box, { h: "5", w: "1px", bg: "lightBlue.6" }) })] }), hasEstimate && (_jsxs(Flex, { align: "center", gap: "6", id: "trackRoomEstimatedValue", bg: "lightBlue.4", children: [_jsx(Show, { breakpoint: "(min-width: 600px)", children: _jsx(Text, { fontWeight: "medium", variant: "caption", children: t('roomHeader.estValue').toUpperCase() }) }), _jsx(Text, { variant: "titleBolder", children: formattedEstimatedAmount }), _jsx(Show, { breakpoint: "(min-width: 600px)", children: _jsx(Box, { h: "5", w: "1px", bg: "lightBlue.6" }) })] })), _jsx(IconButton, { "aria-label": t('ariaLabels.delete'), icon: _jsx(TrashIcon, {}), maxW: "fit-content", onClick: () => onClickDelete(room._id), disabled: isDeleteDisabled, px: "1", variant: "menuIconWithShadow" })] })] }) }));
12
12
  };
@@ -1,4 +1,4 @@
1
1
  import { ControlledWizardI } from '../../interfaces';
2
2
  import { PropsWithChildren } from 'react';
3
- export declare function ControlledWizard<Key extends string = string>({ bodyWidth, steps, height, headerBlink, footerBlink, ...props }: ControlledWizardI<Key>): import("react/jsx-runtime").JSX.Element | null;
3
+ export declare function ControlledWizard<Key extends string = string>({ bodyWidth, steps, height, headerBlink, footerBlink, transition, ...props }: ControlledWizardI<Key>): import("react/jsx-runtime").JSX.Element | null;
4
4
  export declare const WizardBodyPadding: ({ children }: PropsWithChildren) => import("react/jsx-runtime").JSX.Element;
@@ -12,10 +12,12 @@ var __rest = (this && this.__rest) || function (s, e) {
12
12
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
13
  import { Box, Flex, Stack } from '@chakra-ui/react';
14
14
  import { AnimatePresence, motion } from 'framer-motion';
15
+ import { usePrefersReducedMotion } from '../../hooks';
15
16
  const MotionBox = motion.create(typeof Box);
16
17
  export function ControlledWizard(_a) {
17
18
  var _b, _c;
18
- var { bodyWidth = '80%', steps, height = '700px', headerBlink, footerBlink } = _a, props = __rest(_a, ["bodyWidth", "steps", "height", "headerBlink", "footerBlink"]);
19
+ var { bodyWidth = '80%', steps, height = '700px', headerBlink, footerBlink, transition = 'crossfade' } = _a, props = __rest(_a, ["bodyWidth", "steps", "height", "headerBlink", "footerBlink", "transition"]);
20
+ const prefersReducedMotion = usePrefersReducedMotion();
19
21
  const isKeyed = 'stepKey' in props;
20
22
  const current = isKeyed
21
23
  ? (_b = steps.find((s) => s.key === props.stepKey)) !== null && _b !== void 0 ? _b : steps[0]
@@ -75,10 +77,47 @@ export function ControlledWizard(_a) {
75
77
  transition: { duration: 0.18 },
76
78
  },
77
79
  };
80
+ // Directional (Typeform-style) body transition: horizontal slide + a subtle
81
+ // enter-flip (POC). Reduced motion collapses every transition to a plain fade.
82
+ const directionalBodyVariants = {
83
+ initial: { opacity: 0, x: 74, rotateY: 38 },
84
+ animate: {
85
+ opacity: 1,
86
+ x: 0,
87
+ rotateY: 0,
88
+ transition: { duration: 0.55, ease: [0.5, 0, 0.15, 1] },
89
+ },
90
+ exit: {
91
+ opacity: 0,
92
+ x: -74,
93
+ transition: { duration: 0.3, ease: [0.5, 0, 0.15, 1] },
94
+ },
95
+ };
96
+ const reducedVariants = {
97
+ initial: { opacity: 0 },
98
+ animate: { opacity: 1, transition: { duration: 0.2 } },
99
+ exit: { opacity: 0, transition: { duration: 0.15 } },
100
+ };
101
+ const effectiveHeaderVariants = prefersReducedMotion
102
+ ? reducedVariants
103
+ : headerVariants;
104
+ const effectiveBodyVariants = prefersReducedMotion
105
+ ? reducedVariants
106
+ : transition === 'directional'
107
+ ? directionalBodyVariants
108
+ : bodyVariants;
109
+ const effectiveFooterVariants = prefersReducedMotion
110
+ ? reducedVariants
111
+ : footerVariants;
78
112
  if (!current)
79
113
  return null;
80
- return (_jsxs(Stack, { shadow: "md", h: height, bg: "white", overflow: "hidden", spacing: "0", children: [_jsx(AnimatePresence, { mode: "wait", children: _jsx(Box, { as: MotionBox, variants: headerVariants, initial: "initial", animate: "animate", exit: "exit", w: "full", flexShrink: 0, children: current.header }, `h-${motionKey}`) }), _jsx(AnimatePresence, { mode: "wait", children: _jsx(Box, { as: MotionBox, variants: bodyVariants, initial: "initial", animate: "animate", exit: "exit", flex: "1", display: "flex", minH: "0", children: _jsx(Flex, { direction: "column", flex: "auto", overflowY: "auto", minH: "0", children: _jsx(Flex, { justify: "center", h: "full", w: "full", children: _jsx(Box, { w: bodyWidth, children: current.body({ setStep, setStepKey }) }) }) }) }, `b-${motionKey}`) }), current.footer && (_jsx(AnimatePresence, { mode: "wait", children: _jsx(Box, { as: MotionBox, variants: footerVariants, initial: "initial", animate: "animate", exit: "exit", w: "full", flexShrink: 0, bg: "white", borderTopWidth: "1px", borderColor: "lightBlue.3", children: _jsx(Flex, { justify: "center", py: { base: '4', md: '5' }, children: _jsx(Box, { w: bodyWidth, children: current.footer }) }) }, `f-${motionKey}`) }))] }));
114
+ return (_jsxs(Stack, { shadow: "md", h: height, bg: "white", borderRadius: "10px", overflow: "hidden", spacing: "0", sx: { perspective: '1100px' }, children: [_jsx(AnimatePresence, { mode: "wait", children: _jsx(Box, { as: MotionBox, variants: effectiveHeaderVariants, initial: "initial", animate: "animate", exit: "exit", w: "full", flexShrink: 0, children: current.header }, `h-${motionKey}`) }), _jsx(AnimatePresence, { mode: "wait", children: _jsx(Box, { as: MotionBox, variants: effectiveBodyVariants, initial: "initial", animate: "animate", exit: "exit", flex: "1", display: "flex", minH: "0", children: _jsx(Flex, { direction: "column", flex: "auto", overflowY: "auto",
115
+ // overflowY:auto alone coerces overflow-x to auto too (CSS: one axis
116
+ // can't stay visible when the other scrolls), so a full-bleed step
117
+ // (e.g. the success header's negative mx) produced a horizontal
118
+ // scrollbar. Clip X explicitly — no step should scroll sideways.
119
+ overflowX: "hidden", minH: "0", px: "1", pb: "2", children: _jsx(Flex, { justify: "center", h: "full", w: "full", children: _jsx(Box, { w: bodyWidth, children: current.body({ setStep, setStepKey }) }) }) }) }, `b-${motionKey}`) }), current.footer && (_jsx(AnimatePresence, { mode: "wait", children: _jsx(Box, { as: MotionBox, variants: effectiveFooterVariants, initial: "initial", animate: "animate", exit: "exit", w: "full", flexShrink: 0, bg: "white", borderTopWidth: "1px", borderColor: "lightBlue.3", children: _jsx(Flex, { justify: "center", py: { base: '4', md: '5' }, children: _jsx(Box, { w: bodyWidth, children: current.footer }) }) }, `f-${motionKey}`) }))] }));
81
120
  }
82
121
  export const WizardBodyPadding = ({ children }) => {
83
- return _jsx(Box, { py: { base: '4', md: '8' }, children: children });
122
+ return (_jsx(Box, { pt: { base: '4', md: '8' }, pb: { base: '64px', md: '104px' }, children: children }));
84
123
  };
@@ -6,16 +6,16 @@ import { fieldIcons, setOptionButtonStyles } from '../../helpers';
6
6
  export const WizardInitialOptions = ({ form, title, callback, style = 'wizard', }) => {
7
7
  const { control } = useFormContext();
8
8
  const isWizard = style === 'wizard';
9
- return (_jsxs(Stack, { spacing: "base", bg: "lightBlue.1", p: "base", children: [_jsx(Text, { fontFamily: "secondary", fontSize: "sm", children: title }), _jsx(SimpleGrid, { columns: { base: 1, md: isWizard ? 2 : 1 }, spacing: { base: 1, md: isWizard ? 'base' : 1 }, children: form.map((field) => {
9
+ return (_jsxs(Stack, { spacing: "base", children: [title && (_jsx(Text, { fontFamily: "secondary", fontSize: "sm", children: title })), _jsx(SimpleGrid, { columns: { base: 1, md: isWizard ? 2 : 1 }, spacing: { base: 1, md: isWizard ? 'base' : 1 }, children: form.map((field) => {
10
10
  const { id, value, label, icon, description } = field;
11
11
  return (_jsx(Controller, { control: control, name: id, defaultValue: value, render: ({ field: { value, onChange } }) => {
12
12
  const iconSrc = icon
13
13
  ? fieldIcons[icon]
14
14
  : undefined;
15
- return (_jsxs(Container, { position: "relative", children: [value && (_jsx(Image, { src: CheckInCircle, alt: "check", w: "22px", h: "auto", position: "absolute", top: "-4px", left: "-2px", boxShadow: "lg", borderRadius: "full" })), _jsxs(Stack, Object.assign({ as: "button", p: "base", w: "full", onClick: () => {
15
+ return (_jsxs(Container, { position: "relative", boxShadow: "none", children: [value && (_jsx(Image, { src: CheckInCircle, alt: "check", w: "22px", h: "auto", position: "absolute", top: "-6px", left: "-6px", boxShadow: "lg", borderRadius: "full", zIndex: 1 })), _jsxs(Stack, Object.assign({ as: "button", p: "base", w: "full", border: "1px solid", borderColor: value ? 'green.1' : 'transparent', borderRadius: "8px", boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)", onClick: () => {
16
16
  onChange(!value);
17
17
  callback === null || callback === void 0 ? void 0 : callback(Object.assign(Object.assign({}, field), { value: !value }));
18
- } }, setOptionButtonStyles(value), { children: [_jsxs(Flex, { gap: "2", w: "full", align: "center", children: [_jsx(Image, { src: iconSrc, alt: "icon", h: "16px", w: "auto" }), label && (_jsx(Text, { fontWeight: "bold", fontSize: "sm", textTransform: "capitalize", fontFamily: "secondary", children: label }))] }), _jsx(Text, { fontFamily: "secondary", textAlign: "left", children: description })] }))] }));
18
+ } }, setOptionButtonStyles(value), { children: [_jsxs(Flex, { gap: "2", w: "full", align: "center", children: [_jsx(Image, { src: iconSrc, alt: "icon", h: "16px", w: "auto" }), label && (_jsx(Text, { fontWeight: "600", fontSize: "sm", textTransform: "capitalize", fontFamily: "primary", children: label }))] }), _jsx(Text, { fontFamily: "secondary", textAlign: "left", children: description })] }))] }));
19
19
  } }, id));
20
20
  }) })] }));
21
21
  };
@@ -0,0 +1,14 @@
1
+ import { DynamicFormI } from '../../interfaces';
2
+ /**
3
+ * WizardSelectCard — the learn-the-basics wizard's selectable icon tile.
4
+ *
5
+ * Deliberately its OWN component, separate from the legacy `WizardStepOptions`
6
+ * (which the deprecated HomeAssistant setup wizard still renders). Design:
7
+ * - fixed 80x80 square tiles that auto-fit across the row
8
+ * - a green outline + a corner check (top-left, poking out) when selected
9
+ * - a soft shadow, no gray container
10
+ *
11
+ * Same field-shape/RHF contract as the other option fields so the shared
12
+ * DynamicForm engine can route to it, but the legacy card is left as-is.
13
+ */
14
+ export declare const WizardSelectCard: ({ form, title, callback }: DynamicFormI) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,32 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Box, Center, Flex, Image, Stack, Text } from '@chakra-ui/react';
3
+ import { Controller, useFormContext } from 'react-hook-form';
4
+ import { CheckInCircle } from '../../assets/images';
5
+ import { fieldIcons, setOptionButtonStyles } from '../../helpers';
6
+ /**
7
+ * WizardSelectCard — the learn-the-basics wizard's selectable icon tile.
8
+ *
9
+ * Deliberately its OWN component, separate from the legacy `WizardStepOptions`
10
+ * (which the deprecated HomeAssistant setup wizard still renders). Design:
11
+ * - fixed 80x80 square tiles that auto-fit across the row
12
+ * - a green outline + a corner check (top-left, poking out) when selected
13
+ * - a soft shadow, no gray container
14
+ *
15
+ * Same field-shape/RHF contract as the other option fields so the shared
16
+ * DynamicForm engine can route to it, but the legacy card is left as-is.
17
+ */
18
+ export const WizardSelectCard = ({ form, title, callback }) => {
19
+ const { control } = useFormContext();
20
+ return (_jsxs(Stack, { spacing: "base", children: [title && (_jsx(Text, { fontFamily: "secondary", fontSize: "sm", children: title })), _jsx(Flex, { flexWrap: "wrap", gap: "base", children: form.map((field) => {
21
+ const { id, value, label, icon } = field;
22
+ return (_jsx(Controller, { control: control, name: id, defaultValue: value, render: ({ field: { value, onChange } }) => {
23
+ const iconSrc = icon
24
+ ? fieldIcons[icon]
25
+ : undefined;
26
+ return (_jsxs(Box, { position: "relative", boxSize: "80px", children: [value && (_jsx(Image, { src: CheckInCircle, alt: "check", w: "22px", h: "auto", position: "absolute", top: "-6px", left: "-6px", boxShadow: "lg", borderRadius: "full", zIndex: 1 })), _jsxs(Stack, Object.assign({ as: "button", py: "1", spacing: "1", w: "full", h: "full", justify: "center", border: "1px solid", borderColor: value ? 'green.1' : 'transparent', borderRadius: "8px", boxShadow: "0 2px 8px rgba(0, 0, 0, 0.15)", onClick: () => {
27
+ onChange(!value);
28
+ callback === null || callback === void 0 ? void 0 : callback(Object.assign(Object.assign({}, field), { value: !value }));
29
+ } }, setOptionButtonStyles(value), { children: [_jsx(Center, { h: "38px", flexShrink: 0, w: "100%", alignItems: "flex-end", children: _jsx(Image, { src: iconSrc, alt: "icon", boxSize: "36px", objectFit: "contain" }) }), _jsx(Flex, { h: "26px", flexShrink: 0, px: "1", pt: "2px", w: "100%", justify: "center", align: "center", children: _jsx(Text, { fontFamily: "primary", fontSize: "11px", fontWeight: "500", color: "gray.3", textTransform: "uppercase", textAlign: "center", lineHeight: "1", noOfLines: 2, children: label }) })] }))] }));
30
+ } }, id));
31
+ }) })] }));
32
+ };
@@ -0,0 +1,11 @@
1
+ import { WizardStepPromptI } from '../../interfaces';
2
+ /**
3
+ * WizardStepPrompt — the conversational (Typeform) step header for the setup
4
+ * wizard: a step marker, Homi's bold prompt, and an optional supporting line.
5
+ * Left-aligned, white. Structure is fixed; the copy is Homi-authored (passed
6
+ * in as props), never hardcoded.
7
+ *
8
+ * Baked-in type treatment (from the approved POC): 30px semibold Poppins
9
+ * headline, 16px Assistant subtitle in body-copy grey with tight line-height.
10
+ */
11
+ export declare const WizardStepPrompt: ({ marker, title, subtitle, }: WizardStepPromptI) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,20 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Stack, Text } from '@chakra-ui/react';
3
+ /**
4
+ * WizardStepPrompt — the conversational (Typeform) step header for the setup
5
+ * wizard: a step marker, Homi's bold prompt, and an optional supporting line.
6
+ * Left-aligned, white. Structure is fixed; the copy is Homi-authored (passed
7
+ * in as props), never hardcoded.
8
+ *
9
+ * Baked-in type treatment (from the approved POC): 30px semibold Poppins
10
+ * headline, 16px Assistant subtitle in body-copy grey with tight line-height.
11
+ */
12
+ export const WizardStepPrompt = ({ marker, title, subtitle, }) => (_jsxs(Stack, { spacing: "3", pt: "8", children: [marker != null && (_jsx(Text, { fontFamily: "primary", fontWeight: "semibold", fontSize: "sm", color: "blue.2", children: marker })), _jsx(Text, { fontFamily: "primary", fontWeight: "semibold", fontSize: "28px", lineHeight: "1.15", color: "gray.4",
13
+ // text-wrap: balance evens out line lengths on the (mind-authored,
14
+ // variable-length) headline — no hand-tuning needed. Graceful no-op
15
+ // on older browsers.
16
+ sx: { textWrap: 'balance' }, children: title }), subtitle && (_jsx(Text, { fontFamily: "secondary", fontSize: "16px", lineHeight: "1.3", color: "gray.3",
17
+ // text-wrap: pretty prevents widows/orphans on the supporting line.
18
+ // Required for dynamic copy: the mind authors this, so CSS (not
19
+ // sentence-tuning) has to keep every generated variant clean.
20
+ sx: { textWrap: 'pretty' }, children: subtitle }))] }));
@@ -1,2 +1,2 @@
1
1
  import { WizardSuccessHeaderI } from '../../interfaces';
2
- export declare const WizardSuccessHeader: ({ title, subtitle, address, name, image, }: WizardSuccessHeaderI) => import("react/jsx-runtime").JSX.Element;
2
+ export declare const WizardSuccessHeader: ({ bg, title, subtitle, address, name, image, }: WizardSuccessHeaderI) => import("react/jsx-runtime").JSX.Element;
@@ -4,8 +4,8 @@ import { Center, Show, Stack, Text, Box, Flex, Container, Image, } from '@chakra
4
4
  import { ContainerHeader, Logo } from '../../components';
5
5
  import { randomImages } from '../../utils';
6
6
  import { useMyHomeCard } from '../../hooks';
7
- export const WizardSuccessHeader = ({ title, subtitle, address, name, image, }) => {
8
- return (_jsx(Center, { w: "full", bg: "lightGreen.1", minH: "212px", children: _jsxs(Stack, { w: "full", spacing: "0", align: "center", children: [_jsx(Show, { below: "md", children: _jsx(Logo, {}) }), _jsxs(Flex, { gap: "8", w: "80%", align: "center", pt: { base: '0', md: '8' }, direction: { base: 'column', md: 'row' }, children: [_jsx(Box, { maxW: "205px", children: _jsx(WizardHomeCard, { address: address, name: name, image: image }) }), _jsxs(Stack, { spacing: "2", children: [_jsxs(Text, { fontSize: "xl", fontWeight: "semibold", color: "green.1", children: [t('ariaLabels.success'), "!"] }), _jsx(Text, { fontSize: { base: '2xl', md: '3xl' }, lineHeight: "shorter", children: title }), subtitle && _jsx(Text, { fontFamily: "secondary", children: subtitle }), _jsx(Show, { below: "md", children: _jsx(Box, { h: "6" }) })] })] })] }) }));
7
+ export const WizardSuccessHeader = ({ bg = 'lightGreen.1', title, subtitle, address, name, image, }) => {
8
+ return (_jsx(Center, { w: "full", bg: bg, minH: "212px", children: _jsxs(Stack, { w: "full", spacing: "0", align: "center", children: [_jsx(Show, { below: "md", children: _jsx(Logo, {}) }), _jsxs(Flex, { gap: "8", w: "80%", align: "center", pt: { base: '0', md: '8' }, direction: { base: 'column', md: 'row' }, children: [_jsx(Box, { maxW: "205px", children: _jsx(WizardHomeCard, { address: address, name: name, image: image }) }), _jsxs(Stack, { spacing: "2", children: [_jsxs(Text, { fontSize: "xl", fontWeight: "semibold", color: "green.1", children: [t('ariaLabels.success'), "!"] }), _jsx(Text, { fontSize: { base: '2xl', md: '3xl' }, lineHeight: "shorter", children: title }), subtitle && _jsx(Text, { fontFamily: "secondary", children: subtitle }), _jsx(Show, { below: "md", children: _jsx(Box, { h: "6" }) })] })] })] }) }));
9
9
  };
10
10
  const WizardHomeCard = ({ address, name, image, }) => {
11
11
  const { imageUrl } = useMyHomeCard({
@@ -1,7 +1,9 @@
1
1
  export * from './ControlledWizard';
2
2
  export * from './WizardInitialOptions';
3
3
  export * from './WizardStepOptions';
4
+ export * from './WizardSelectCard';
4
5
  export * from './WizardStepGoal';
6
+ export * from './WizardStepPrompt';
5
7
  export * from './WizardSteps';
6
8
  export * from './WizardSuccessHeader';
7
9
  export * from './WizardTextHeader';
@@ -1,7 +1,9 @@
1
1
  export * from './ControlledWizard';
2
2
  export * from './WizardInitialOptions';
3
3
  export * from './WizardStepOptions';
4
+ export * from './WizardSelectCard';
4
5
  export * from './WizardStepGoal';
6
+ export * from './WizardStepPrompt';
5
7
  export * from './WizardSteps';
6
8
  export * from './WizardSuccessHeader';
7
9
  export * from './WizardTextHeader';
@@ -1,4 +1,7 @@
1
1
  import { Paint, Receipt, Barcode, GoldenBars, Heart, Rating, Notes, Book, ImgIcon, Registry, ShareWith, Calendar, Contacts, Tools2, Billing, Calc, CheckPen, GenericField, Price, Battery2, Co2, Detector, Electricity, Sprinkler, SixtyEight, DropCalendar, MobileDrop, SolarPanel, Calendar2, Drop, Wind, Foundation, Heater, Light, Plate, Pool, Closet, Structure, GlassWater, Roof2, Target, CircleFace, Pressure, WindowCleaning, RoofCleaning, HolidayLights, GutterCleaning, HomeCleaning, HouseWashing, PressureWasher, BookOpened, Company2, AADisease, AADressing, AALawn, AAMowing, AAWeed, AApest, FilterSize, Wallet, Fire, Plant, LifePreserver, BOCFilter, BOCGreen, BOCRenovation, BOCRepair, BOCService, BOCWarranty, MRBoxes, MRClock, MRCouch, MRHand, MRMakeReady, MRRental, Cleaning, GearTime, YellowFolder, Warranty, Flower, Jacuzzi, MagnifyingGlassPerson, TPlumberDrain, TPlumberEmergency, TPlumberExcavation, TPlumberGeneral, TPlumberWater, PNMSlab, PNMTreatment, PNMWaterHeater, HMCeilingFan, HMDrill, HMFaucet, HMHanging, HMSecurity, HMTV, IlHome, IlHvac, IlPower, IlSolar, IlTools } from '../../assets/images';
2
+ // Relative (not the @/ alias): tsc-alias does not rewrite direct static-asset
3
+ // imports, so an @/ alias here survives into dist unresolved and breaks Vite.
4
+ import PdfIcon from '../../assets/images/pdf-icon.svg';
2
5
  import { CookTop, Dishwasher, Dryer, Freezer, Fridge, Hood, Microwave, Oven, Range, Washer, tankWater, tanklessWater, } from '../../assets/images/appliances';
3
6
  import { BatterySmokeDetector, Co2Detector, FireExtinguisher, Furnace, HeatPump, InWall, LandscapeLights, Other, SprinklerSystem, SprinklerSystem2, WiredSmokeDetector, } from '../../assets/images/home-assistant';
4
7
  export const fieldIcons = {
@@ -67,6 +70,7 @@ export const fieldIcons = {
67
70
  'tank-water': tankWater,
68
71
  'tankless-water': tanklessWater,
69
72
  'wired-smoke-detector': WiredSmokeDetector,
73
+ pdf: PdfIcon,
70
74
  barcode: Barcode,
71
75
  battery: Battery2,
72
76
  billing: Billing,
@@ -1,14 +1,23 @@
1
1
  import { useState, useEffect } from 'react';
2
+ // Extensions we can render directly as an <img> thumbnail. A file is treated as
3
+ // an image by its extension rather than by bucket name: image uploads land in
4
+ // `homefile-images` in production but in `homefile-test` in non-prod, so gating
5
+ // on a single bucket name silently hid thumbnails outside production. Every
6
+ // storage bucket is public-read and the object key is `<fileName>.<extension>`,
7
+ // so the public URL is derivable from the file's own stored bucketName.
8
+ const IMAGE_EXTENSIONS = ['jpg', 'jpeg', 'png', 'webp', 'gif'];
2
9
  export const useDisplayFiles = ({ files, title }) => {
3
10
  const [fileExtension, updateFileExtension] = useState("");
4
11
  const [imageUrl, updateImageUrl] = useState("");
5
12
  useEffect(() => {
13
+ var _a;
6
14
  if (files && files.length > 0) {
7
15
  const firstFile = files[0];
8
16
  updateFileExtension(firstFile.extension);
9
- if (firstFile.bucketName === "homefile-images") {
17
+ const ext = `${(_a = firstFile.extension) !== null && _a !== void 0 ? _a : ""}`.toLowerCase();
18
+ if (firstFile.bucketName && firstFile.fileName && IMAGE_EXTENSIONS.includes(ext)) {
10
19
  const storageUrl = import.meta.env.VITE_STORAGE_URL;
11
- updateImageUrl(`${storageUrl}/${firstFile === null || firstFile === void 0 ? void 0 : firstFile.bucketName}/${firstFile === null || firstFile === void 0 ? void 0 : firstFile.fileName}.${firstFile === null || firstFile === void 0 ? void 0 : firstFile.extension}`);
20
+ updateImageUrl(`${storageUrl}/${firstFile.bucketName}/${firstFile.fileName}.${firstFile.extension}`);
12
21
  }
13
22
  else {
14
23
  updateImageUrl("");
@@ -76,8 +76,14 @@ export const useAddressStored = ({ proxy = addressProxy, setIsDirty, }) => {
76
76
  };
77
77
  const handleCityAndState = () => __awaiter(void 0, void 0, void 0, function* () {
78
78
  const { city, state } = yield getAddress(fields.zip);
79
- fields.city = city;
80
- fields.state = state;
79
+ // Only fill from the zip lookup when it actually returns values. A failed or
80
+ // empty lookup must NOT wipe city/state we already have — e.g. values seeded
81
+ // from a saved home when opening the Edit Home panel (the lookup for an
82
+ // existing zip can come back empty and was blanking the fields).
83
+ if (city)
84
+ fields.city = city;
85
+ if (state)
86
+ fields.state = state;
81
87
  });
82
88
  const handleCleanProxy = () => proxy.setFields({
83
89
  apartmentNumber: '',
@@ -32,5 +32,7 @@ export * from './useComponentStyles';
32
32
  export * from './useCustomToast';
33
33
  export * from './useFileUploader';
34
34
  export * from './useImage';
35
+ export * from './useDirtySave';
35
36
  export * from './useWindowDimensions';
36
37
  export * from './valueMonitor';
38
+ export * from './motion';
@@ -32,5 +32,7 @@ export * from './useComponentStyles';
32
32
  export * from './useCustomToast';
33
33
  export * from './useFileUploader';
34
34
  export * from './useImage';
35
+ export * from './useDirtySave';
35
36
  export * from './useWindowDimensions';
36
37
  export * from './valueMonitor';
38
+ export * from './motion';
@@ -0,0 +1,2 @@
1
+ export * from './usePrefersReducedMotion';
2
+ export * from './useCountUp';
@@ -0,0 +1,2 @@
1
+ export * from './usePrefersReducedMotion';
2
+ export * from './useCountUp';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Counts a number up to `target` over `duration` ms — a direct port of the
3
+ * POC's implementation: `duration = 1100ms`, ease-out-cubic, a
4
+ * `requestAnimationFrame` loop, and `toLocaleString()` formatting. Respects
5
+ * reduced motion (jumps straight to the target, no animation). Returns the
6
+ * formatted display string.
7
+ *
8
+ * Narrow utility — NOT a general animation engine (Eli's over-engineering flag).
9
+ */
10
+ export declare const useCountUp: (target: number, duration?: number, easing?: (p: number) => number) => string;
@@ -0,0 +1,39 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { usePrefersReducedMotion } from './usePrefersReducedMotion';
3
+ // ease-out-cubic (POC): 1 - (1 - p)^3
4
+ const easeOutCubic = (p) => 1 - Math.pow(1 - p, 3);
5
+ /**
6
+ * Counts a number up to `target` over `duration` ms — a direct port of the
7
+ * POC's implementation: `duration = 1100ms`, ease-out-cubic, a
8
+ * `requestAnimationFrame` loop, and `toLocaleString()` formatting. Respects
9
+ * reduced motion (jumps straight to the target, no animation). Returns the
10
+ * formatted display string.
11
+ *
12
+ * Narrow utility — NOT a general animation engine (Eli's over-engineering flag).
13
+ */
14
+ export const useCountUp = (target, duration = 1100, easing = easeOutCubic) => {
15
+ const prefersReducedMotion = usePrefersReducedMotion();
16
+ const [value, setValue] = useState(prefersReducedMotion ? target : 0);
17
+ const rafRef = useRef(undefined);
18
+ useEffect(() => {
19
+ if (prefersReducedMotion) {
20
+ setValue(target);
21
+ return;
22
+ }
23
+ let start = null;
24
+ const step = (timestamp) => {
25
+ if (start === null)
26
+ start = timestamp;
27
+ const progress = Math.min((timestamp - start) / duration, 1);
28
+ setValue(Math.round(target * easing(progress)));
29
+ if (progress < 1)
30
+ rafRef.current = requestAnimationFrame(step);
31
+ };
32
+ rafRef.current = requestAnimationFrame(step);
33
+ return () => {
34
+ if (rafRef.current)
35
+ cancelAnimationFrame(rafRef.current);
36
+ };
37
+ }, [target, duration, easing, prefersReducedMotion]);
38
+ return value.toLocaleString();
39
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The single house hook for JS / framer-motion reduced-motion gating.
3
+ *
4
+ * A thin wrapper over framer-motion's built-in `useReducedMotion()` (already a
5
+ * dependency, SSR-safe, purpose-built). framer-motion returns `boolean | null`
6
+ * (`null` before it can read the media query); we coerce to a definite boolean,
7
+ * defaulting to motion-allowed (`false`).
8
+ *
9
+ * NOTE: the 3 existing CSS `@media (prefers-reduced-motion: reduce)` blocks
10
+ * (Stars / Birds / HomeMonitorButton) are a DIFFERENT mechanism (they gate CSS
11
+ * animations declaratively) and stay as-is — this hook is only for JS-driven
12
+ * (framer-motion) motion. One hook; no fourth reduced-motion implementation.
13
+ */
14
+ export declare const usePrefersReducedMotion: () => boolean;
@@ -0,0 +1,15 @@
1
+ import { useReducedMotion } from 'framer-motion';
2
+ /**
3
+ * The single house hook for JS / framer-motion reduced-motion gating.
4
+ *
5
+ * A thin wrapper over framer-motion's built-in `useReducedMotion()` (already a
6
+ * dependency, SSR-safe, purpose-built). framer-motion returns `boolean | null`
7
+ * (`null` before it can read the media query); we coerce to a definite boolean,
8
+ * defaulting to motion-allowed (`false`).
9
+ *
10
+ * NOTE: the 3 existing CSS `@media (prefers-reduced-motion: reduce)` blocks
11
+ * (Stars / Birds / HomeMonitorButton) are a DIFFERENT mechanism (they gate CSS
12
+ * animations declaratively) and stay as-is — this hook is only for JS-driven
13
+ * (framer-motion) motion. One hook; no fourth reduced-motion implementation.
14
+ */
15
+ export const usePrefersReducedMotion = () => { var _a; return (_a = useReducedMotion()) !== null && _a !== void 0 ? _a : false; };
@@ -4,9 +4,12 @@ interface UseRoomHeaderI {
4
4
  onSubmit: (room: RoomItemI) => void;
5
5
  room: RoomItemI;
6
6
  roomAmount: string;
7
+ roomEstimatedAmount?: string;
7
8
  }
8
- export declare const useRoomHeader: ({ onSubmit, room: { _id, name, type, active, home }, roomAmount, }: UseRoomHeaderI) => {
9
+ export declare const useRoomHeader: ({ onSubmit, room: { _id, name, type, active, home }, roomAmount, roomEstimatedAmount, }: UseRoomHeaderI) => {
9
10
  formattedAmount: string;
11
+ formattedEstimatedAmount: string;
12
+ hasEstimate: boolean;
10
13
  handleChange: (e: ChangeEvent<HTMLInputElement>) => void;
11
14
  handleClick: () => void;
12
15
  inputValue: string | null;
@@ -1,9 +1,15 @@
1
1
  import { useState, useEffect } from 'react';
2
2
  import { formatCurrency } from '../../utils';
3
- export const useRoomHeader = ({ onSubmit, room: { _id, name, type, active, home }, roomAmount, }) => {
3
+ export const useRoomHeader = ({ onSubmit, room: { _id, name, type, active, home }, roomAmount, roomEstimatedAmount, }) => {
4
4
  const [isEditing, setIsEditing] = useState(false);
5
5
  const [inputValue, setInputValue] = useState(null);
6
6
  const formattedAmount = formatCurrency({ value: roomAmount });
7
+ // Estimated value renders only when there is a real (non-zero) estimate —
8
+ // honest-empty until the enrichment side emits the est-value-field tag.
9
+ const hasEstimate = Number(roomEstimatedAmount) > 0;
10
+ const formattedEstimatedAmount = formatCurrency({
11
+ value: roomEstimatedAmount !== null && roomEstimatedAmount !== void 0 ? roomEstimatedAmount : 0,
12
+ });
7
13
  useEffect(() => {
8
14
  setInputValue(name);
9
15
  }, [name]);
@@ -19,6 +25,8 @@ export const useRoomHeader = ({ onSubmit, room: { _id, name, type, active, home
19
25
  };
20
26
  return {
21
27
  formattedAmount,
28
+ formattedEstimatedAmount,
29
+ hasEstimate,
22
30
  handleChange,
23
31
  handleClick,
24
32
  inputValue,