@homefile/components-v2 2.60.0 → 2.60.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/forms/readOnly/ReadOnlyDynamicForm.js +11 -1
- package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyAppliances.js +52 -8
- package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyWarrantyBanner.d.ts +1 -1
- package/dist/components/forms/readOnly/fields/homeItem/ReadOnlyWarrantyBanner.js +39 -6
- package/dist/interfaces/forms/readOnly/homeItem/ReadOnlyWarrantyBanner.interface.d.ts +31 -4
- package/package.json +1 -1
|
@@ -26,7 +26,17 @@ function toSafeValue(value) {
|
|
|
26
26
|
return value;
|
|
27
27
|
}
|
|
28
28
|
export const ReadOnlyDynamicForm = ({ form: fields, onClick, onEdit, }) => {
|
|
29
|
-
return (
|
|
29
|
+
return (
|
|
30
|
+
// `overflow="auto"`, not `"scroll"`. `scroll` reserves a scrollbar gutter on
|
|
31
|
+
// BOTH axes whether or not there is anything to scroll — measured at 15px
|
|
32
|
+
// each in Chrome with classic (non-overlay) scrollbars. That gutter is
|
|
33
|
+
// inside the box, so every field rendered here stops 15px short of the
|
|
34
|
+
// panel's right edge and a dead 15px band runs along the bottom, which is
|
|
35
|
+
// what showed up as the item card and warranty banner "not filling the
|
|
36
|
+
// panel". Nothing in this form scrolls sideways, so the horizontal gutter
|
|
37
|
+
// was pure loss. `auto` shows each bar only when that axis actually
|
|
38
|
+
// overflows and reserves nothing otherwise.
|
|
39
|
+
_jsx(Box, { bg: "lightBlue.1", overflow: "auto", children: fields === null || fields === void 0 ? void 0 : fields.map((field) => {
|
|
30
40
|
const { children = [], icon, id, name, type } = field;
|
|
31
41
|
const value = toSafeValue(field.value);
|
|
32
42
|
const baseProps = {
|
|
@@ -1,23 +1,67 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { useState } from 'react';
|
|
3
|
-
import { Text, Flex, Stack, IconButton, Box, Image, SimpleGrid } from '@chakra-ui/react';
|
|
3
|
+
import { Text, Flex, Stack, IconButton, Box, Image, SimpleGrid, Grid } from '@chakra-ui/react';
|
|
4
4
|
import { Edit } from '../../../../../components';
|
|
5
5
|
import { colors } from '../../../../../theme/colors';
|
|
6
6
|
import { formatCurrency } from '../../../../../utils';
|
|
7
7
|
// Dashed rule between sections (XD: light gray dashed).
|
|
8
8
|
const DashedRule = () => (_jsx(Box, { borderBottom: "1px dashed", borderColor: "lightGray.2" }));
|
|
9
9
|
// Brand / Model# / Serial# row — label #6C7679, value #464646 (XD).
|
|
10
|
-
|
|
10
|
+
//
|
|
11
|
+
// The label column sizes to its content with a floor, rather than taking a
|
|
12
|
+
// fixed 35%: a real label is "Brand/Manufacturer:" (a schema key in ~50 places
|
|
13
|
+
// across the platform, so it cannot simply be shortened), and 35% of a narrow
|
|
14
|
+
// drawer is not wide enough for it. The percentage broke it mid-word into
|
|
15
|
+
// "Brand/Manufact" + "urer:". `keep-all` stops the mid-word break outright,
|
|
16
|
+
// so a label that still outgrows the column wraps at a natural boundary.
|
|
17
|
+
// Values arrive from different extractors with inconsistent casing — a brand
|
|
18
|
+
// comes back "Maytag", a product type "front-load washing machine" — so the
|
|
19
|
+
// column reads as a ransom note. Capitalize the FIRST character only and leave
|
|
20
|
+
// the rest of the string untouched: that makes the column consistent without
|
|
21
|
+
// rewriting the data, and keeps model numbers like "NC501" or "DCC-3400"
|
|
22
|
+
// exactly as extracted (title-casing them would produce "Nc501").
|
|
23
|
+
const leadCap = (s) => s.length > 0 ? s[0].toUpperCase() + s.slice(1) : s;
|
|
24
|
+
// Renders as TWO grid cells rather than a self-contained row, so every label
|
|
25
|
+
// shares one column and every value starts on the same left edge.
|
|
26
|
+
//
|
|
27
|
+
// Each row previously sized its own label independently, so the value column
|
|
28
|
+
// was ragged — the longest label ("Avg Retail Price Range:") pushed its own
|
|
29
|
+
// value right while the others sat further left. A fixed percentage was the
|
|
30
|
+
// original spec and broke the opposite way, wrapping "Brand/Manufacturer:"
|
|
31
|
+
// mid-word. A shared `max-content` column solves both: it is exactly as wide
|
|
32
|
+
// as the longest label, so nothing wraps and everything aligns.
|
|
33
|
+
const DetailRow = ({ label, value }) => (_jsxs(_Fragment, { children: [_jsx(Text, { wordBreak: "keep-all", fontFamily: "secondary", fontSize: "14px", lineHeight: "18px", color: "#6C7679", children: label }), _jsx(Text, { fontFamily: "secondary", fontSize: "14px", lineHeight: "18px", color: "gray.3", children: value != null ? leadCap(String(value)) : '' })] }));
|
|
34
|
+
// A value-row amount is currency-formatted only when it IS a single number.
|
|
35
|
+
//
|
|
36
|
+
// Catalog-resolved prices arrive as RANGE strings ("$229-$249") because that is
|
|
37
|
+
// what can honestly be established for a model. formatCurrency runs Number() on
|
|
38
|
+
// its input, and Number("$229-$249") is NaN, so formatting one prints "$NaN" on
|
|
39
|
+
// the owner's card. A non-numeric amount is already human-readable — render it
|
|
40
|
+
// as-is rather than mangling it.
|
|
41
|
+
const formatValueAmount = (value) => {
|
|
42
|
+
if (value == null || value === '')
|
|
43
|
+
return '';
|
|
44
|
+
const numeric = typeof value === 'number' ? value : Number(String(value).replace(/[$,\s]/g, ''));
|
|
45
|
+
return Number.isFinite(numeric)
|
|
46
|
+
? formatCurrency({ value: numeric })
|
|
47
|
+
: String(value);
|
|
48
|
+
};
|
|
11
49
|
// Retail Price / Est Replacement Cost — label left #6C7679, value right #464646.
|
|
12
|
-
const ValueRow = ({ label, value, bold, }) => (_jsxs(Flex, { justify: "space-between", children: [_jsx(Text, { fontFamily: "secondary", fontSize: "16px", lineHeight: "20px", color: "#6C7679", children: label }), _jsx(Text, { fontFamily: "secondary", fontSize: "16px", lineHeight: "20px", color: "gray.3", fontWeight: bold ? 'bold' : 'normal', children: value
|
|
13
|
-
? formatCurrency({ value: value })
|
|
14
|
-
: '' })] }));
|
|
50
|
+
const ValueRow = ({ label, value, bold, }) => (_jsxs(Flex, { justify: "space-between", children: [_jsx(Text, { fontFamily: "secondary", fontSize: "16px", lineHeight: "20px", color: "#6C7679", children: label }), _jsx(Text, { fontFamily: "secondary", fontSize: "16px", lineHeight: "20px", color: "gray.3", fontWeight: bold ? 'bold' : 'normal', children: formatValueAmount(value) })] }));
|
|
15
51
|
export const ReadOnlyAppliances = ({ description, id, name, children = [], onEdit, value, }) => {
|
|
16
52
|
var _a;
|
|
17
53
|
const imageField = children.find(({ type }) => type === 'images');
|
|
18
54
|
const images = (_a = imageField === null || imageField === void 0 ? void 0 : imageField.value) !== null && _a !== void 0 ? _a : [];
|
|
19
|
-
|
|
55
|
+
// The chosen thumbnail is stored; the displayed image is DERIVED from it.
|
|
56
|
+
//
|
|
57
|
+
// `useState(images[0])` captured the first image only on the first render —
|
|
58
|
+
// and images arrive after that render, so the card kept showing an empty
|
|
59
|
+
// <Image> (a broken-picture box) until something else forced a re-render.
|
|
60
|
+
// Deriving means there is no second copy of "which image" to go stale, and a
|
|
61
|
+
// selection that is no longer in the list falls back instead of blanking.
|
|
62
|
+
const [chosenImage, setChosenImage] = useState(undefined);
|
|
63
|
+
const mainImage = chosenImage && images.includes(chosenImage) ? chosenImage : images[0];
|
|
20
64
|
const detailFields = children.filter(({ type }) => type === 'text');
|
|
21
65
|
const valueFields = children.filter(({ type }) => type === 'currency');
|
|
22
|
-
return (_jsxs(Stack, { spacing: "base", p: "base", bg: "neutral.white", mt: "2px", children: [(name || onEdit) && (_jsxs(Flex, { justify: "space-between", align: "center", w: "full", children: [_jsx(Text, { fontFamily: "primary", fontSize: "12px", letterSpacing: "0", color: "#525458", textTransform: "uppercase", children: name }), onEdit && (_jsx(IconButton, { "aria-label": "Edit appliance", variant: "ghost", w: "fit-content", minW: "auto", h: "auto", p: "0", flexShrink: 0, icon: _jsx(Edit, { size: 18, stroke: colors.blue[3] }), onClick: () => onEdit(id) }))] })), _jsxs(Flex, { gap: "base", align: "stretch", children: [_jsxs(Flex, { direction: "column", gap: "base", flexShrink: 0, w: "174px", children: [_jsx(Image, { src: mainImage, alt: "home item", boxSize: "174px", objectFit: "cover" }), images.length > 0 && (_jsx(SimpleGrid, { columns: 4, spacing: "2", children: images.map((image) => (_jsx(Box, { as: "button", backgroundImage: image, backgroundSize: "cover", backgroundPosition: "center", boxSize: "34px", onClick: () =>
|
|
66
|
+
return (_jsxs(Stack, { spacing: "base", p: "base", bg: "neutral.white", mt: "2px", children: [(name || onEdit) && (_jsxs(Flex, { justify: "space-between", align: "center", w: "full", children: [_jsx(Text, { fontFamily: "primary", fontSize: "12px", letterSpacing: "0", color: "#525458", textTransform: "uppercase", children: name }), onEdit && (_jsx(IconButton, { "aria-label": "Edit appliance", variant: "ghost", w: "fit-content", minW: "auto", h: "auto", p: "0", flexShrink: 0, icon: _jsx(Edit, { size: 18, stroke: colors.blue[3] }), onClick: () => onEdit(id) }))] })), _jsxs(Flex, { gap: "base", align: "stretch", children: [_jsxs(Flex, { direction: "column", gap: "base", flexShrink: 0, w: "174px", children: [_jsx(Image, { src: mainImage, alt: "home item", boxSize: "174px", objectFit: "cover" }), images.length > 0 && (_jsx(SimpleGrid, { columns: 4, spacing: "2", children: images.map((image) => (_jsx(Box, { as: "button", backgroundImage: image, backgroundSize: "cover", backgroundPosition: "center", boxSize: "34px", onClick: () => setChosenImage(image) }, image))) }))] }), _jsxs(Stack, { flex: "1", spacing: "base", children: [_jsxs(Stack, { spacing: "13px", children: [_jsx(Text, { fontFamily: "primary", fontWeight: "600", fontSize: "22px", lineHeight: "26px", letterSpacing: "0", color: "gray.3", children: value }), _jsx(Text, { fontFamily: "secondary", fontSize: "14px", lineHeight: "15px", color: "gray.3", children: description })] }), _jsx(DashedRule, {}), detailFields.length > 0 && (_jsx(Grid, { templateColumns: "max-content 1fr", columnGap: "base", rowGap: "0", alignItems: "start", children: detailFields.map((field) => (_jsx(DetailRow, { label: field.name ? `${field.name}:` : undefined, value: field.value }, `${field.id}-${field.name}`))) })), valueFields.length > 0 && (_jsxs(_Fragment, { children: [_jsx(DashedRule, {}), _jsx(Stack, { spacing: "0", children: valueFields.map((field, i) => (_jsx(ValueRow, { label: field.name, value: field.value, bold: i === valueFields.length - 1 }, `${field.id}-${field.name}`))) })] }))] })] })] }));
|
|
23
67
|
};
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { ReadOnlyWarrantyBannerI } from '../../../../../interfaces';
|
|
2
|
-
export declare const ReadOnlyWarrantyBanner: ({ icon, value, state, link, }: ReadOnlyWarrantyBannerI) => import("react/jsx-runtime").JSX.Element | null;
|
|
2
|
+
export declare const ReadOnlyWarrantyBanner: ({ icon, value, state, link, onAction, actionLabel, }: ReadOnlyWarrantyBannerI) => import("react/jsx-runtime").JSX.Element | null;
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { t } from 'i18next';
|
|
3
|
+
import { Box, Circle, Flex, Image, Link, Text } from '@chakra-ui/react';
|
|
4
|
+
import { ArrowLeft } from '../../../../../components';
|
|
3
5
|
import { fieldIcons } from '../../../../../helpers';
|
|
6
|
+
import { colors } from '../../../../../theme/colors';
|
|
4
7
|
/**
|
|
5
8
|
* Warranty-status banner. Built to the EXACT CSS from the "Read Only Item" XD
|
|
6
9
|
* (dev/spec mode), under-warranty artboard:
|
|
@@ -25,18 +28,48 @@ import { fieldIcons } from '../../../../../helpers';
|
|
|
25
28
|
const STATE = {
|
|
26
29
|
underWarranty: { bg: '#F2F9F2', badge: '#E0EBE0', icon: 'warranty' },
|
|
27
30
|
expired: { bg: 'lightGray.1', badge: 'lightGray.2', icon: 'warranty' }, // TODO: swap to the expired icon
|
|
31
|
+
// EXACT CSS from the unknown-warranty XD artboard (Matt, 09-21) — no longer
|
|
32
|
+
// provisional. Light blue reads INFORMATIONAL: deliberately neither the
|
|
33
|
+
// green of "covered" nor the gray of "lapsed", because the point of the
|
|
34
|
+
// state is that we do not know which it is.
|
|
35
|
+
// container #DEEDF7 · badge #BFDCEF · "?" #5F7482 Poppins SemiBold 29px
|
|
36
|
+
unverified: { bg: '#DEEDF7', badge: '#BFDCEF', icon: 'warranty' },
|
|
28
37
|
recall: { bg: 'error.1', badge: 'error.2', icon: 'warranty' }, // TODO: swap to the recall icon
|
|
29
38
|
};
|
|
30
|
-
export const ReadOnlyWarrantyBanner = ({ icon, value,
|
|
39
|
+
export const ReadOnlyWarrantyBanner = ({ icon, value,
|
|
40
|
+
// Defaults to the NEUTRAL state, not to "covered". The only production
|
|
41
|
+
// caller spreads field data (ReadOnlyDynamicForm), so a warranty field that
|
|
42
|
+
// arrives without an explicit state used to assert coverage by omission —
|
|
43
|
+
// the same fail-unsafe default as the STATE lookup below. Callers that mean
|
|
44
|
+
// "under warranty" must say so.
|
|
45
|
+
state = 'unverified', link, onAction, actionLabel, }) => {
|
|
31
46
|
var _a;
|
|
32
47
|
// No data → render nothing. Never fabricate a status (critical for recall).
|
|
33
48
|
if (!value)
|
|
34
49
|
return null;
|
|
35
|
-
|
|
50
|
+
// Fail SAFE on an unrecognised state: degrade to "we don't know", never to
|
|
51
|
+
// "you are covered". This is not hypothetical — a caller on a NEWER build
|
|
52
|
+
// than the installed package can send a state this copy has no key for (e.g.
|
|
53
|
+
// 'unverified' against 2.60.0), and a green fallback would assert coverage
|
|
54
|
+
// the data cannot support. Neutral is the only defensible default.
|
|
55
|
+
const s = (_a = STATE[state]) !== null && _a !== void 0 ? _a : STATE.unverified;
|
|
36
56
|
// Field-provided icon overrides; otherwise the state's own (swappable) icon.
|
|
37
57
|
const iconKey = icon !== null && icon !== void 0 ? icon : s.icon;
|
|
38
58
|
const bannerIcon = iconKey ? fieldIcons[iconKey] : '';
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
59
|
+
// States drawn as a typographic glyph rather than an image asset. Both are
|
|
60
|
+
// punctuation the brand face already carries, so neither needs an icon file.
|
|
61
|
+
const glyph = state === 'recall' ? '!' : state === 'unverified' ? '?' : '';
|
|
62
|
+
// Band height is 64px, down from the XD's 76px (Matt, 2026-09-21) — it read as
|
|
63
|
+
// too deep above the item card in the live panel. Nothing else drove that
|
|
64
|
+
// height: the badge is 45px and two lines of 16px/20px copy are 40px, so 76px
|
|
65
|
+
// was a floor with ~15px of dead space above and below. 64px leaves ~9px
|
|
66
|
+
// around the badge and still clears two lines without clipping. Below ~53px
|
|
67
|
+
// the badge would start touching the edges.
|
|
68
|
+
return (_jsxs(Flex, { align: "center", gap: "10px", minH: "64px", pl: "10px", pr: "base", bg: s.bg, mt: "2px", children: [_jsx(Flex, { align: "center", justify: "center", flexShrink: 0, boxSize: "45px", borderRadius: "full", bg: s.badge, children: glyph ? (
|
|
69
|
+
// Typographic glyph sized to the internal icon (29px): "!" for
|
|
70
|
+
// recall, "?" for unverified. Recall keeps extra-bold white on its
|
|
71
|
+
// saturated badge; unverified is exact XD — Poppins SemiBold (600)
|
|
72
|
+
// in #5F7482, a desaturated slate that reads on the pale badge
|
|
73
|
+
// without competing with the message.
|
|
74
|
+
_jsx(Text, { fontFamily: "primary", fontWeight: state === 'recall' ? 'extrabold' : '600', fontSize: "29px", lineHeight: "1", color: state === 'recall' ? 'neutral.white' : '#5F7482', children: glyph })) : (bannerIcon && (_jsx(Image, { src: bannerIcon, alt: "warranty status", w: "27px", h: "29px", objectFit: "contain" }))) }), link ? (_jsx(Link, { href: link, isExternal: true, fontFamily: "primary", fontSize: "16px", lineHeight: "20px", letterSpacing: "0", color: "gray.3", sx: { textWrap: 'balance' }, children: String(value) })) : (_jsx(Text, { fontFamily: "primary", fontSize: "16px", lineHeight: "20px", letterSpacing: "0", color: "gray.3", sx: { textWrap: 'balance' }, children: String(value) })), onAction && (_jsx(Circle, { as: "button", type: "button", onClick: onAction, "aria-label": actionLabel !== null && actionLabel !== void 0 ? actionLabel : t('buttons.forward'), size: "40px", bg: "neutral.white", flexShrink: 0, ml: "auto", transition: "all 0.2s ease-in-out", _hover: { boxShadow: 'lg' }, children: _jsx(Box, { transform: "rotate(180deg)", lineHeight: "0", children: _jsx(ArrowLeft, { stroke: colors.blue[2], size: 24 }) }) }))] }));
|
|
42
75
|
};
|
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
import { IconTypes, ValueTypes } from '../../../../interfaces';
|
|
2
2
|
/**
|
|
3
|
-
* The
|
|
4
|
-
*
|
|
3
|
+
* The warranty states the banner can render. Derived upstream from the item's
|
|
4
|
+
* purchase date + parsed warranty term (see tickets #356/#357):
|
|
5
5
|
* - `underWarranty` — still covered
|
|
6
6
|
* - `expired` — warranty term has lapsed
|
|
7
|
+
* - `unverified` — the TERM is known but the purchase DATE is not, so no
|
|
8
|
+
* expiry can be computed and neither "covered" nor "lapsed" is knowable
|
|
7
9
|
* - `recall` — an active manufacturer recall
|
|
8
10
|
*
|
|
9
11
|
* HONESTY: `recall` must ONLY be passed with REAL recall data. There is no
|
|
10
12
|
* recall data source in the platform today (#357), so callers must not pass
|
|
11
13
|
* state="recall" until one exists — the banner never fabricates an alert.
|
|
14
|
+
*
|
|
15
|
+
* HONESTY: `unverified` is the DEFAULT-CORRECT state for catalog warranty
|
|
16
|
+
* data, and callers should reach for it rather than dressing the gap up as a
|
|
17
|
+
* status. We hold the term on most products (warranty_parts is populated on
|
|
18
|
+
* 68/91) but an item carries only `createdAt` — when WE first saw it, not when
|
|
19
|
+
* the owner bought it. Passing `underWarranty` there would assert coverage the
|
|
20
|
+
* user may not have, which is the most damaging thing this banner can do;
|
|
21
|
+
* passing `expired` borrows a lapsed-looking palette for something we simply
|
|
22
|
+
* do not know. `unverified` claims only what is true: this is the term, and
|
|
23
|
+
* the start date is missing.
|
|
12
24
|
*/
|
|
13
|
-
export type WarrantyBannerState = 'underWarranty' | 'expired' | 'recall';
|
|
25
|
+
export type WarrantyBannerState = 'underWarranty' | 'expired' | 'unverified' | 'recall';
|
|
14
26
|
export interface ReadOnlyWarrantyBannerI {
|
|
15
27
|
/** Status message, e.g. "This item is under the manufacturer's warranty till 2030." */
|
|
16
28
|
value?: ValueTypes;
|
|
17
29
|
/** `fieldIcons` key for the badge glyph (e.g. 'boc-warranty'). */
|
|
18
30
|
icon?: IconTypes;
|
|
19
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Which state to render. Defaults to `unverified`, and an unrecognised
|
|
33
|
+
* value falls back to it too — the banner degrades to "we don't know",
|
|
34
|
+
* never to "you are covered". Pass `underWarranty` explicitly when coverage
|
|
35
|
+
* is actually known.
|
|
36
|
+
*/
|
|
20
37
|
state?: WarrantyBannerState;
|
|
21
38
|
/**
|
|
22
39
|
* Optional destination for the banner. Down the road: a recall notice on
|
|
@@ -24,4 +41,14 @@ export interface ReadOnlyWarrantyBannerI {
|
|
|
24
41
|
* message becomes a link.
|
|
25
42
|
*/
|
|
26
43
|
link?: string;
|
|
44
|
+
/**
|
|
45
|
+
* In-app action for the banner, rendered as the circular arrow button at the
|
|
46
|
+
* right edge (unknown-warranty XD). Distinct from `link`, which is an
|
|
47
|
+
* external href — "add your receipt" opens a panel, it does not navigate
|
|
48
|
+
* away. The button renders ONLY when this is supplied, so states and callers
|
|
49
|
+
* without an action are unchanged.
|
|
50
|
+
*/
|
|
51
|
+
onAction?: () => void;
|
|
52
|
+
/** Accessible label for that button. Defaults to a generic action label. */
|
|
53
|
+
actionLabel?: string;
|
|
27
54
|
}
|