@burdenoff/microfe-housingvista 2026.713.1 → 2026.720.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.
- package/dist/housingvista/components/PropertyCard.d.ts +7 -0
- package/dist/housingvista/components/PropertyCard.d.ts.map +1 -1
- package/dist/housingvista/components/PropertyCard.js +83 -58
- package/dist/housingvista/components/PropertyCard.js.map +1 -1
- package/dist/housingvista/components/Sparkline.d.ts +15 -0
- package/dist/housingvista/components/Sparkline.d.ts.map +1 -0
- package/dist/housingvista/components/Sparkline.js +61 -0
- package/dist/housingvista/components/Sparkline.js.map +1 -0
- package/dist/housingvista/components/StatCard.d.ts +3 -0
- package/dist/housingvista/components/StatCard.d.ts.map +1 -1
- package/dist/housingvista/components/StatCard.js +33 -29
- package/dist/housingvista/components/StatCard.js.map +1 -1
- package/dist/housingvista/components/index.d.ts +2 -0
- package/dist/housingvista/components/index.d.ts.map +1 -1
- package/dist/housingvista/components/index.js +1 -0
- package/dist/housingvista/hooks/index.d.ts +32 -2
- package/dist/housingvista/hooks/index.d.ts.map +1 -1
- package/dist/housingvista/hooks/index.js +170 -90
- package/dist/housingvista/hooks/index.js.map +1 -1
- package/dist/housingvista/pages/HomePage.d.ts.map +1 -1
- package/dist/housingvista/pages/HomePage.js +7 -3
- package/dist/housingvista/pages/HomePage.js.map +1 -1
- package/dist/housingvista/pages/analytics/AnalyticsPage.d.ts.map +1 -1
- package/dist/housingvista/pages/analytics/AnalyticsPage.js +146 -145
- package/dist/housingvista/pages/analytics/AnalyticsPage.js.map +1 -1
- package/dist/housingvista/pages/market/AreaInsightsPage.d.ts.map +1 -1
- package/dist/housingvista/pages/market/AreaInsightsPage.js +60 -7
- package/dist/housingvista/pages/market/AreaInsightsPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertiesPage.d.ts.map +1 -1
- package/dist/housingvista/pages/properties/PropertiesPage.js +192 -162
- package/dist/housingvista/pages/properties/PropertiesPage.js.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.d.ts.map +1 -1
- package/dist/housingvista/pages/properties/PropertyDetailPage.js +792 -599
- package/dist/housingvista/pages/properties/PropertyDetailPage.js.map +1 -1
- package/dist/housingvista/pages/shortlist/ShortlistPage.d.ts.map +1 -1
- package/dist/housingvista/pages/shortlist/ShortlistPage.js +172 -75
- package/dist/housingvista/pages/shortlist/ShortlistPage.js.map +1 -1
- package/dist/housingvista/types/index.d.ts +44 -1
- package/dist/housingvista/types/index.d.ts.map +1 -1
- package/dist/housingvista/utils/pageContext.d.ts.map +1 -1
- package/dist/housingvista/utils/pageContext.js +20 -17
- package/dist/housingvista/utils/pageContext.js.map +1 -1
- package/dist/stories/EmptyState.stories.d.ts +10 -0
- package/dist/stories/EmptyState.stories.d.ts.map +1 -0
- package/dist/stories/StatCard.stories.d.ts +10 -0
- package/dist/stories/StatCard.stories.d.ts.map +1 -0
- package/dist/stories/StatusBadge.stories.d.ts +11 -0
- package/dist/stories/StatusBadge.stories.d.ts.map +1 -0
- package/package.json +13 -2
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FC } from 'react';
|
|
2
|
+
import { ReraVerificationStatus } from '../types';
|
|
2
3
|
|
|
3
4
|
interface PropertyCardProps {
|
|
4
5
|
id: string;
|
|
@@ -20,6 +21,12 @@ interface PropertyCardProps {
|
|
|
20
21
|
/** When provided the heart becomes a controlled shortlist toggle. */
|
|
21
22
|
isShortlisted?: boolean;
|
|
22
23
|
onToggleShortlist?: () => void;
|
|
24
|
+
/** RERA (India) verification workflow status — badge shown for
|
|
25
|
+
* `verified`/`pending`, omitted for `not_applicable`/`rejected` (BOFF-4132). */
|
|
26
|
+
reraStatus?: ReraVerificationStatus;
|
|
27
|
+
/** When provided, renders a compare-selection toggle (BOFF-3931). */
|
|
28
|
+
isCompareSelected?: boolean;
|
|
29
|
+
onToggleCompare?: () => void;
|
|
23
30
|
}
|
|
24
31
|
export declare const PropertyCard: FC<PropertyCardProps>;
|
|
25
32
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PropertyCard.d.ts","sourceRoot":"","sources":["../../../src/housingvista/components/PropertyCard.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAY,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"PropertyCard.d.ts","sourceRoot":"","sources":["../../../src/housingvista/components/PropertyCard.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,EAAY,MAAM,OAAO,CAAC;AAI1C,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAEvD,UAAU,iBAAiB;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yDAAyD;IACzD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iBAAiB,CAAC,EAAE,MAAM,IAAI,CAAC;IAC/B;oFACgF;IAChF,UAAU,CAAC,EAAE,sBAAsB,CAAC;IACpC,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,eAAe,CAAC,EAAE,MAAM,IAAI,CAAC;CAC9B;AAsCD,eAAO,MAAM,YAAY,EAAE,EAAE,CAAC,iBAAiB,CAmL9C,CAAC"}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { cn as e, formatCompactCurrency as t, formatNumber as n } from "../utils/helpers.js";
|
|
2
2
|
import { StatusBadge as r } from "./StatusBadge.js";
|
|
3
3
|
import { useState as i } from "react";
|
|
4
|
-
import { Bath as a, Bed as o,
|
|
5
|
-
import { jsx as
|
|
4
|
+
import { Bath as a, Bed as o, Check as s, GitCompare as c, Heart as l, Home as u, MapPin as d, ShieldCheck as f, Square as p } from "lucide-react";
|
|
5
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
6
6
|
//#region src/housingvista/components/PropertyCard.tsx
|
|
7
|
-
var
|
|
7
|
+
var g = {
|
|
8
8
|
available: "success",
|
|
9
9
|
active: "success",
|
|
10
10
|
listed: "success",
|
|
@@ -20,112 +20,137 @@ var p = {
|
|
|
20
20
|
off_market: "muted",
|
|
21
21
|
"off market": "muted",
|
|
22
22
|
draft: "muted"
|
|
23
|
-
},
|
|
23
|
+
}, _ = () => /* @__PURE__ */ h("div", {
|
|
24
24
|
className: "relative flex h-full w-full items-center justify-center overflow-hidden bg-gradient-to-br from-[var(--color-action-primary-bg)]/15 via-bg-sunken to-[var(--color-accent-blue)]/15",
|
|
25
|
-
children: [/* @__PURE__ */
|
|
25
|
+
children: [/* @__PURE__ */ m("div", {
|
|
26
26
|
className: "absolute inset-0 opacity-[0.35]",
|
|
27
27
|
style: {
|
|
28
28
|
backgroundImage: "radial-gradient(circle at 1px 1px, var(--color-border-seam, rgba(120,120,120,.25)) 1px, transparent 0)",
|
|
29
29
|
backgroundSize: "16px 16px"
|
|
30
30
|
}
|
|
31
|
-
}), /* @__PURE__ */
|
|
31
|
+
}), /* @__PURE__ */ h("div", {
|
|
32
32
|
className: "relative flex flex-col items-center gap-2 text-text-secondary/70",
|
|
33
|
-
children: [/* @__PURE__ */
|
|
33
|
+
children: [/* @__PURE__ */ m(u, { className: "h-9 w-9" }), /* @__PURE__ */ m("span", {
|
|
34
34
|
className: "text-[11px] font-medium tracking-wide",
|
|
35
35
|
children: "HousingVista"
|
|
36
36
|
})]
|
|
37
37
|
})]
|
|
38
|
-
}),
|
|
39
|
-
let [
|
|
40
|
-
return /* @__PURE__ */
|
|
41
|
-
className: e("group flex flex-col overflow-hidden rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-elevation-2 focus-within:border-border-strong",
|
|
42
|
-
onClick:
|
|
38
|
+
}), v = ({ title: u, price: v, priceType: y, pricePeriod: b, currency: x = "USD", location: S, bedrooms: C, bathrooms: w, area: T, imageUrl: E, status: D, type: O, onClick: k, className: A, isShortlisted: j, onToggleShortlist: M, reraStatus: N, isCompareSelected: P, onToggleCompare: F }) => {
|
|
39
|
+
let [I, L] = i(!1), R = !!E && !I, z = t(v, x), B = y === "rent" ? `${z}${b ? `/${b.replace("ly", "")}` : "/mo"}` : z, V = g[D.toLowerCase()] ?? "default";
|
|
40
|
+
return /* @__PURE__ */ h("div", {
|
|
41
|
+
className: e("group flex flex-col overflow-hidden rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-elevation-2 focus-within:border-border-strong", k && "cursor-pointer", A),
|
|
42
|
+
onClick: k,
|
|
43
43
|
onKeyDown: (e) => {
|
|
44
|
-
|
|
44
|
+
k && (e.key === "Enter" || e.key === " ") && (e.preventDefault(), k());
|
|
45
45
|
},
|
|
46
|
-
role:
|
|
47
|
-
tabIndex:
|
|
48
|
-
"aria-label":
|
|
49
|
-
children: [/* @__PURE__ */
|
|
46
|
+
role: k ? "button" : void 0,
|
|
47
|
+
tabIndex: k ? 0 : void 0,
|
|
48
|
+
"aria-label": k ? `View ${u}` : void 0,
|
|
49
|
+
children: [/* @__PURE__ */ h("div", {
|
|
50
50
|
className: "relative aspect-[16/10] shrink-0 overflow-hidden",
|
|
51
51
|
children: [
|
|
52
|
-
|
|
53
|
-
src:
|
|
54
|
-
alt:
|
|
52
|
+
R ? /* @__PURE__ */ m("img", {
|
|
53
|
+
src: E,
|
|
54
|
+
alt: u,
|
|
55
55
|
loading: "lazy",
|
|
56
|
-
onError: () =>
|
|
56
|
+
onError: () => L(!0),
|
|
57
57
|
className: "h-full w-full object-cover transition-[filter] duration-300 group-hover:brightness-[1.02]"
|
|
58
|
-
}) : /* @__PURE__ */
|
|
59
|
-
/* @__PURE__ */
|
|
60
|
-
/* @__PURE__ */
|
|
58
|
+
}) : /* @__PURE__ */ m(_, {}),
|
|
59
|
+
/* @__PURE__ */ m("div", { className: "pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-bg-overlay/75 to-transparent" }),
|
|
60
|
+
/* @__PURE__ */ m("div", {
|
|
61
61
|
className: "absolute left-3 top-3",
|
|
62
|
-
children: /* @__PURE__ */
|
|
63
|
-
label:
|
|
64
|
-
variant:
|
|
62
|
+
children: /* @__PURE__ */ m(r, {
|
|
63
|
+
label: D,
|
|
64
|
+
variant: V,
|
|
65
65
|
dot: !0
|
|
66
66
|
})
|
|
67
67
|
}),
|
|
68
|
-
|
|
68
|
+
M && /* @__PURE__ */ m("button", {
|
|
69
69
|
type: "button",
|
|
70
70
|
onClick: (e) => {
|
|
71
|
-
e.stopPropagation(),
|
|
71
|
+
e.stopPropagation(), M();
|
|
72
72
|
},
|
|
73
|
-
className: e("absolute right-3 top-3 rounded-full bg-bg-surface/85 p-2 shadow-elevation-1 backdrop-blur-sm transition-colors hover:bg-bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]",
|
|
74
|
-
"aria-pressed":
|
|
75
|
-
"aria-label":
|
|
76
|
-
children: /* @__PURE__ */
|
|
73
|
+
className: e("absolute right-3 top-3 rounded-full bg-bg-surface/85 p-2 shadow-elevation-1 backdrop-blur-sm transition-colors hover:bg-bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", j ? "text-status-error-text" : "text-text-secondary"),
|
|
74
|
+
"aria-pressed": j,
|
|
75
|
+
"aria-label": j ? "Remove from shortlist" : "Save to shortlist",
|
|
76
|
+
children: /* @__PURE__ */ m(l, { className: e("h-4 w-4", j && "fill-current") })
|
|
77
77
|
}),
|
|
78
|
-
/* @__PURE__ */
|
|
78
|
+
F && /* @__PURE__ */ m("button", {
|
|
79
|
+
type: "button",
|
|
80
|
+
onClick: (e) => {
|
|
81
|
+
e.stopPropagation(), F();
|
|
82
|
+
},
|
|
83
|
+
className: e("absolute top-14 right-3 rounded-full p-2 shadow-elevation-1 backdrop-blur-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]", P ? "bg-action-primary-bg text-action-primary-text" : "bg-bg-surface/85 text-text-secondary hover:bg-bg-surface"),
|
|
84
|
+
"aria-pressed": P,
|
|
85
|
+
"aria-label": P ? "Remove from comparison" : "Add to comparison",
|
|
86
|
+
children: m(P ? s : c, { className: "h-4 w-4" })
|
|
87
|
+
}),
|
|
88
|
+
/* @__PURE__ */ m("div", {
|
|
79
89
|
className: "absolute bottom-3 left-3",
|
|
80
|
-
children: /* @__PURE__ */
|
|
90
|
+
children: /* @__PURE__ */ m("span", {
|
|
81
91
|
className: "text-lg font-bold text-text-inverse drop-shadow-sm",
|
|
82
|
-
children:
|
|
92
|
+
children: B
|
|
83
93
|
})
|
|
84
94
|
}),
|
|
85
|
-
/* @__PURE__ */
|
|
95
|
+
/* @__PURE__ */ m("div", {
|
|
86
96
|
className: "absolute bottom-3 right-3",
|
|
87
|
-
children: /* @__PURE__ */
|
|
97
|
+
children: /* @__PURE__ */ m("span", {
|
|
88
98
|
className: "rounded-md bg-bg-surface/85 px-2 py-1 text-xs font-medium capitalize text-text-primary backdrop-blur-sm",
|
|
89
|
-
children:
|
|
99
|
+
children: O
|
|
90
100
|
})
|
|
91
101
|
})
|
|
92
102
|
]
|
|
93
|
-
}), /* @__PURE__ */
|
|
103
|
+
}), /* @__PURE__ */ h("div", {
|
|
94
104
|
className: "flex flex-1 flex-col p-4",
|
|
95
105
|
children: [
|
|
96
|
-
/* @__PURE__ */
|
|
97
|
-
className: "
|
|
98
|
-
children:
|
|
106
|
+
/* @__PURE__ */ h("div", {
|
|
107
|
+
className: "flex items-center gap-1.5",
|
|
108
|
+
children: [
|
|
109
|
+
/* @__PURE__ */ m("h3", {
|
|
110
|
+
className: "truncate text-sm font-semibold text-text-primary",
|
|
111
|
+
children: u
|
|
112
|
+
}),
|
|
113
|
+
N === "verified" && /* @__PURE__ */ h("span", {
|
|
114
|
+
className: "inline-flex shrink-0 items-center gap-1 rounded-full bg-status-success-bg-subtle px-1.5 py-0.5 text-[10px] font-semibold text-status-success-text",
|
|
115
|
+
title: "RERA verified",
|
|
116
|
+
children: [/* @__PURE__ */ m(f, { className: "h-3 w-3" }), "RERA"]
|
|
117
|
+
}),
|
|
118
|
+
N === "pending" && /* @__PURE__ */ m("span", {
|
|
119
|
+
className: "inline-flex shrink-0 items-center gap-1 rounded-full bg-status-warning-bg-subtle px-1.5 py-0.5 text-[10px] font-semibold text-status-warning-text",
|
|
120
|
+
title: "RERA verification pending",
|
|
121
|
+
children: "RERA pending"
|
|
122
|
+
})
|
|
123
|
+
]
|
|
99
124
|
}),
|
|
100
|
-
/* @__PURE__ */
|
|
125
|
+
/* @__PURE__ */ h("div", {
|
|
101
126
|
className: "mt-1 flex items-center gap-1 text-text-secondary",
|
|
102
|
-
children: [/* @__PURE__ */ d
|
|
127
|
+
children: [/* @__PURE__ */ m(d, { className: "h-3.5 w-3.5 shrink-0" }), /* @__PURE__ */ m("span", {
|
|
103
128
|
className: "truncate text-xs",
|
|
104
|
-
children:
|
|
129
|
+
children: S
|
|
105
130
|
})]
|
|
106
131
|
}),
|
|
107
|
-
/* @__PURE__ */
|
|
132
|
+
/* @__PURE__ */ h("div", {
|
|
108
133
|
className: "mt-3 flex items-center gap-4 border-t border-border-subtle pt-3",
|
|
109
134
|
children: [
|
|
110
|
-
|
|
135
|
+
C != null && /* @__PURE__ */ h("div", {
|
|
111
136
|
className: "flex items-center gap-1.5 text-xs text-text-secondary",
|
|
112
|
-
children: [/* @__PURE__ */
|
|
113
|
-
|
|
137
|
+
children: [/* @__PURE__ */ m(o, { className: "h-4 w-4" }), /* @__PURE__ */ h("span", { children: [
|
|
138
|
+
C,
|
|
114
139
|
" ",
|
|
115
|
-
|
|
140
|
+
C === 1 ? "Bed" : "Beds"
|
|
116
141
|
] })]
|
|
117
142
|
}),
|
|
118
|
-
|
|
143
|
+
w != null && /* @__PURE__ */ h("div", {
|
|
119
144
|
className: "flex items-center gap-1.5 text-xs text-text-secondary",
|
|
120
|
-
children: [/* @__PURE__ */
|
|
121
|
-
|
|
145
|
+
children: [/* @__PURE__ */ m(a, { className: "h-4 w-4" }), /* @__PURE__ */ h("span", { children: [
|
|
146
|
+
w,
|
|
122
147
|
" ",
|
|
123
|
-
|
|
148
|
+
w === 1 ? "Bath" : "Baths"
|
|
124
149
|
] })]
|
|
125
150
|
}),
|
|
126
|
-
/* @__PURE__ */
|
|
151
|
+
/* @__PURE__ */ h("div", {
|
|
127
152
|
className: "flex items-center gap-1.5 text-xs text-text-secondary",
|
|
128
|
-
children: [/* @__PURE__ */
|
|
153
|
+
children: [/* @__PURE__ */ m(p, { className: "h-4 w-4" }), /* @__PURE__ */ h("span", { children: [n(T), " sq ft"] })]
|
|
129
154
|
})
|
|
130
155
|
]
|
|
131
156
|
})
|
|
@@ -134,6 +159,6 @@ var p = {
|
|
|
134
159
|
});
|
|
135
160
|
};
|
|
136
161
|
//#endregion
|
|
137
|
-
export {
|
|
162
|
+
export { v as PropertyCard };
|
|
138
163
|
|
|
139
164
|
//# sourceMappingURL=PropertyCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PropertyCard.js","names":[],"sources":["../../../src/housingvista/components/PropertyCard.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { MapPin, Bed, Bath, Square, Heart, Home } from 'lucide-react';\nimport { cn, formatNumber, formatCompactCurrency } from '../utils/helpers';\nimport { StatusBadge } from './StatusBadge';\n\ninterface PropertyCardProps {\n id: string;\n title: string;\n price: number;\n priceType: 'sale' | 'rent';\n pricePeriod?: string;\n /** ISO currency code for the price (defaults to USD). */\n currency?: string;\n location: string;\n bedrooms?: number;\n bathrooms?: number;\n area: number;\n imageUrl?: string;\n status: string;\n type: string;\n onClick?: () => void;\n className?: string;\n /** When provided the heart becomes a controlled shortlist toggle. */\n isShortlisted?: boolean;\n onToggleShortlist?: () => void;\n}\n\nconst statusVariantMap: Record<string, 'success' | 'warning' | 'error' | 'info' | 'default' | 'muted'> = {\n available: 'success',\n active: 'success',\n listed: 'success',\n for_sale: 'success',\n 'for sale': 'success',\n for_rent: 'info',\n 'for rent': 'info',\n pending: 'warning',\n 'under contract': 'warning',\n 'under offer': 'warning',\n sold: 'error',\n rented: 'error',\n off_market: 'muted',\n 'off market': 'muted',\n draft: 'muted',\n};\n\n/** Tasteful branded fallback shown when a listing has no files-svc image. */\nconst PropertyPlaceholder: FC = () => (\n <div className=\"relative flex h-full w-full items-center justify-center overflow-hidden bg-gradient-to-br from-[var(--color-action-primary-bg)]/15 via-bg-sunken to-[var(--color-accent-blue)]/15\">\n <div\n className=\"absolute inset-0 opacity-[0.35]\"\n style={{\n backgroundImage:\n 'radial-gradient(circle at 1px 1px, var(--color-border-seam, rgba(120,120,120,.25)) 1px, transparent 0)',\n backgroundSize: '16px 16px',\n }}\n />\n <div className=\"relative flex flex-col items-center gap-2 text-text-secondary/70\">\n <Home className=\"h-9 w-9\" />\n <span className=\"text-[11px] font-medium tracking-wide\">HousingVista</span>\n </div>\n </div>\n);\n\nexport const PropertyCard: FC<PropertyCardProps> = ({\n title,\n price,\n priceType,\n pricePeriod,\n currency = 'USD',\n location,\n bedrooms,\n bathrooms,\n area,\n imageUrl,\n status,\n type,\n onClick,\n className,\n isShortlisted,\n onToggleShortlist,\n}) => {\n const [imgFailed, setImgFailed] = useState(false);\n const showImage = Boolean(imageUrl) && !imgFailed;\n\n const compactPrice = formatCompactCurrency(price, currency);\n const priceDisplay =\n priceType === 'rent' ? `${compactPrice}${pricePeriod ? `/${pricePeriod.replace('ly', '')}` : '/mo'}` : compactPrice;\n\n const badgeVariant = statusVariantMap[status.toLowerCase()] ?? 'default';\n\n return (\n <div\n className={cn(\n 'group flex flex-col overflow-hidden rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-elevation-2 focus-within:border-border-strong',\n onClick && 'cursor-pointer',\n className\n )}\n onClick={onClick}\n onKeyDown={(e) => {\n if (onClick && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n onClick();\n }\n }}\n role={onClick ? 'button' : undefined}\n tabIndex={onClick ? 0 : undefined}\n aria-label={onClick ? `View ${title}` : undefined}\n >\n {/* Image Section */}\n <div className=\"relative aspect-[16/10] shrink-0 overflow-hidden\">\n {showImage ? (\n <img\n src={imageUrl}\n alt={title}\n loading=\"lazy\"\n onError={() => setImgFailed(true)}\n className=\"h-full w-full object-cover transition-[filter] duration-300 group-hover:brightness-[1.02]\"\n />\n ) : (\n <PropertyPlaceholder />\n )}\n\n {/* Gradient scrim so overlaid text stays legible over any photo */}\n <div className=\"pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-bg-overlay/75 to-transparent\" />\n\n {/* Status pill (top-left) */}\n <div className=\"absolute left-3 top-3\">\n <StatusBadge label={status} variant={badgeVariant} dot />\n </div>\n\n {/* Shortlist heart (top-right) */}\n {onToggleShortlist && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggleShortlist();\n }}\n className={cn(\n 'absolute right-3 top-3 rounded-full bg-bg-surface/85 p-2 shadow-elevation-1 backdrop-blur-sm transition-colors hover:bg-bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n isShortlisted ? 'text-status-error-text' : 'text-text-secondary'\n )}\n aria-pressed={isShortlisted}\n aria-label={isShortlisted ? 'Remove from shortlist' : 'Save to shortlist'}\n >\n <Heart className={cn('h-4 w-4', isShortlisted && 'fill-current')} />\n </button>\n )}\n\n {/* Price overlay (bottom-left) */}\n <div className=\"absolute bottom-3 left-3\">\n <span className=\"text-lg font-bold text-text-inverse drop-shadow-sm\">{priceDisplay}</span>\n </div>\n\n {/* Category chip (bottom-right) */}\n <div className=\"absolute bottom-3 right-3\">\n <span className=\"rounded-md bg-bg-surface/85 px-2 py-1 text-xs font-medium capitalize text-text-primary backdrop-blur-sm\">\n {type}\n </span>\n </div>\n </div>\n\n {/* Content Section */}\n <div className=\"flex flex-1 flex-col p-4\">\n {/* Title */}\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{title}</h3>\n\n {/* Location */}\n <div className=\"mt-1 flex items-center gap-1 text-text-secondary\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" />\n <span className=\"truncate text-xs\">{location}</span>\n </div>\n\n {/* Specs Row */}\n <div className=\"mt-3 flex items-center gap-4 border-t border-border-subtle pt-3\">\n {bedrooms != null && (\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Bed className=\"h-4 w-4\" />\n <span>\n {bedrooms} {bedrooms === 1 ? 'Bed' : 'Beds'}\n </span>\n </div>\n )}\n {bathrooms != null && (\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Bath className=\"h-4 w-4\" />\n <span>\n {bathrooms} {bathrooms === 1 ? 'Bath' : 'Baths'}\n </span>\n </div>\n )}\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Square className=\"h-4 w-4\" />\n <span>{formatNumber(area)} sq ft</span>\n </div>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"PropertyCard.js","names":[],"sources":["../../../src/housingvista/components/PropertyCard.tsx"],"sourcesContent":["import { type FC, useState } from 'react';\nimport { MapPin, Bed, Bath, Square, Heart, Home, ShieldCheck, GitCompare, Check } from 'lucide-react';\nimport { cn, formatNumber, formatCompactCurrency } from '../utils/helpers';\nimport { StatusBadge } from './StatusBadge';\nimport type { ReraVerificationStatus } from '../types';\n\ninterface PropertyCardProps {\n id: string;\n title: string;\n price: number;\n priceType: 'sale' | 'rent';\n pricePeriod?: string;\n /** ISO currency code for the price (defaults to USD). */\n currency?: string;\n location: string;\n bedrooms?: number;\n bathrooms?: number;\n area: number;\n imageUrl?: string;\n status: string;\n type: string;\n onClick?: () => void;\n className?: string;\n /** When provided the heart becomes a controlled shortlist toggle. */\n isShortlisted?: boolean;\n onToggleShortlist?: () => void;\n /** RERA (India) verification workflow status — badge shown for\n * `verified`/`pending`, omitted for `not_applicable`/`rejected` (BOFF-4132). */\n reraStatus?: ReraVerificationStatus;\n /** When provided, renders a compare-selection toggle (BOFF-3931). */\n isCompareSelected?: boolean;\n onToggleCompare?: () => void;\n}\n\nconst statusVariantMap: Record<string, 'success' | 'warning' | 'error' | 'info' | 'default' | 'muted'> = {\n available: 'success',\n active: 'success',\n listed: 'success',\n for_sale: 'success',\n 'for sale': 'success',\n for_rent: 'info',\n 'for rent': 'info',\n pending: 'warning',\n 'under contract': 'warning',\n 'under offer': 'warning',\n sold: 'error',\n rented: 'error',\n off_market: 'muted',\n 'off market': 'muted',\n draft: 'muted',\n};\n\n/** Tasteful branded fallback shown when a listing has no files-svc image. */\nconst PropertyPlaceholder: FC = () => (\n <div className=\"relative flex h-full w-full items-center justify-center overflow-hidden bg-gradient-to-br from-[var(--color-action-primary-bg)]/15 via-bg-sunken to-[var(--color-accent-blue)]/15\">\n <div\n className=\"absolute inset-0 opacity-[0.35]\"\n style={{\n backgroundImage:\n 'radial-gradient(circle at 1px 1px, var(--color-border-seam, rgba(120,120,120,.25)) 1px, transparent 0)',\n backgroundSize: '16px 16px',\n }}\n />\n <div className=\"relative flex flex-col items-center gap-2 text-text-secondary/70\">\n <Home className=\"h-9 w-9\" />\n <span className=\"text-[11px] font-medium tracking-wide\">HousingVista</span>\n </div>\n </div>\n);\n\nexport const PropertyCard: FC<PropertyCardProps> = ({\n title,\n price,\n priceType,\n pricePeriod,\n currency = 'USD',\n location,\n bedrooms,\n bathrooms,\n area,\n imageUrl,\n status,\n type,\n onClick,\n className,\n isShortlisted,\n onToggleShortlist,\n reraStatus,\n isCompareSelected,\n onToggleCompare,\n}) => {\n const [imgFailed, setImgFailed] = useState(false);\n const showImage = Boolean(imageUrl) && !imgFailed;\n\n const compactPrice = formatCompactCurrency(price, currency);\n const priceDisplay =\n priceType === 'rent' ? `${compactPrice}${pricePeriod ? `/${pricePeriod.replace('ly', '')}` : '/mo'}` : compactPrice;\n\n const badgeVariant = statusVariantMap[status.toLowerCase()] ?? 'default';\n\n return (\n <div\n className={cn(\n 'group flex flex-col overflow-hidden rounded-card border border-border-seam bg-bg-surface shadow-elevation-1 transition-[box-shadow,border-color] hover:border-border-strong hover:shadow-elevation-2 focus-within:border-border-strong',\n onClick && 'cursor-pointer',\n className\n )}\n onClick={onClick}\n onKeyDown={(e) => {\n if (onClick && (e.key === 'Enter' || e.key === ' ')) {\n e.preventDefault();\n onClick();\n }\n }}\n role={onClick ? 'button' : undefined}\n tabIndex={onClick ? 0 : undefined}\n aria-label={onClick ? `View ${title}` : undefined}\n >\n {/* Image Section */}\n <div className=\"relative aspect-[16/10] shrink-0 overflow-hidden\">\n {showImage ? (\n <img\n src={imageUrl}\n alt={title}\n loading=\"lazy\"\n onError={() => setImgFailed(true)}\n className=\"h-full w-full object-cover transition-[filter] duration-300 group-hover:brightness-[1.02]\"\n />\n ) : (\n <PropertyPlaceholder />\n )}\n\n {/* Gradient scrim so overlaid text stays legible over any photo */}\n <div className=\"pointer-events-none absolute inset-x-0 bottom-0 h-24 bg-gradient-to-t from-bg-overlay/75 to-transparent\" />\n\n {/* Status pill (top-left) */}\n <div className=\"absolute left-3 top-3\">\n <StatusBadge label={status} variant={badgeVariant} dot />\n </div>\n\n {/* Shortlist heart (top-right) */}\n {onToggleShortlist && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggleShortlist();\n }}\n className={cn(\n 'absolute right-3 top-3 rounded-full bg-bg-surface/85 p-2 shadow-elevation-1 backdrop-blur-sm transition-colors hover:bg-bg-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n isShortlisted ? 'text-status-error-text' : 'text-text-secondary'\n )}\n aria-pressed={isShortlisted}\n aria-label={isShortlisted ? 'Remove from shortlist' : 'Save to shortlist'}\n >\n <Heart className={cn('h-4 w-4', isShortlisted && 'fill-current')} />\n </button>\n )}\n\n {/* Compare-selection toggle (bottom of the top-right stack, BOFF-3931) */}\n {onToggleCompare && (\n <button\n type=\"button\"\n onClick={(e) => {\n e.stopPropagation();\n onToggleCompare();\n }}\n className={cn(\n 'absolute top-14 right-3 rounded-full p-2 shadow-elevation-1 backdrop-blur-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--color-focus-ring)]',\n isCompareSelected\n ? 'bg-action-primary-bg text-action-primary-text'\n : 'bg-bg-surface/85 text-text-secondary hover:bg-bg-surface'\n )}\n aria-pressed={isCompareSelected}\n aria-label={isCompareSelected ? 'Remove from comparison' : 'Add to comparison'}\n >\n {isCompareSelected ? <Check className=\"h-4 w-4\" /> : <GitCompare className=\"h-4 w-4\" />}\n </button>\n )}\n\n {/* Price overlay (bottom-left) */}\n <div className=\"absolute bottom-3 left-3\">\n <span className=\"text-lg font-bold text-text-inverse drop-shadow-sm\">{priceDisplay}</span>\n </div>\n\n {/* Category chip (bottom-right) */}\n <div className=\"absolute bottom-3 right-3\">\n <span className=\"rounded-md bg-bg-surface/85 px-2 py-1 text-xs font-medium capitalize text-text-primary backdrop-blur-sm\">\n {type}\n </span>\n </div>\n </div>\n\n {/* Content Section */}\n <div className=\"flex flex-1 flex-col p-4\">\n {/* Title */}\n <div className=\"flex items-center gap-1.5\">\n <h3 className=\"truncate text-sm font-semibold text-text-primary\">{title}</h3>\n {reraStatus === 'verified' && (\n <span\n className=\"inline-flex shrink-0 items-center gap-1 rounded-full bg-status-success-bg-subtle px-1.5 py-0.5 text-[10px] font-semibold text-status-success-text\"\n title=\"RERA verified\"\n >\n <ShieldCheck className=\"h-3 w-3\" />\n RERA\n </span>\n )}\n {reraStatus === 'pending' && (\n <span\n className=\"inline-flex shrink-0 items-center gap-1 rounded-full bg-status-warning-bg-subtle px-1.5 py-0.5 text-[10px] font-semibold text-status-warning-text\"\n title=\"RERA verification pending\"\n >\n RERA pending\n </span>\n )}\n </div>\n\n {/* Location */}\n <div className=\"mt-1 flex items-center gap-1 text-text-secondary\">\n <MapPin className=\"h-3.5 w-3.5 shrink-0\" />\n <span className=\"truncate text-xs\">{location}</span>\n </div>\n\n {/* Specs Row */}\n <div className=\"mt-3 flex items-center gap-4 border-t border-border-subtle pt-3\">\n {bedrooms != null && (\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Bed className=\"h-4 w-4\" />\n <span>\n {bedrooms} {bedrooms === 1 ? 'Bed' : 'Beds'}\n </span>\n </div>\n )}\n {bathrooms != null && (\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Bath className=\"h-4 w-4\" />\n <span>\n {bathrooms} {bathrooms === 1 ? 'Bath' : 'Baths'}\n </span>\n </div>\n )}\n <div className=\"flex items-center gap-1.5 text-xs text-text-secondary\">\n <Square className=\"h-4 w-4\" />\n <span>{formatNumber(area)} sq ft</span>\n </div>\n </div>\n </div>\n </div>\n );\n};\n"],"mappings":";;;;;;AAkCA,IAAM,IAAmG;CACvG,WAAW;CACX,QAAQ;CACR,QAAQ;CACR,UAAU;CACV,YAAY;CACZ,UAAU;CACV,YAAY;CACZ,SAAS;CACT,kBAAkB;CAClB,eAAe;CACf,MAAM;CACN,QAAQ;CACR,YAAY;CACZ,cAAc;CACd,OAAO;CACR,EAGK,UACJ,kBAAC,OAAD;CAAK,WAAU;WAAf,CACE,kBAAC,OAAD;EACE,WAAU;EACV,OAAO;GACL,iBACE;GACF,gBAAgB;GACjB;EACD,CAAA,EACF,kBAAC,OAAD;EAAK,WAAU;YAAf,CACE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC5B,kBAAC,QAAD;GAAM,WAAU;aAAwC;GAAmB,CAAA,CACvE;IACF;IAGK,KAAuC,EAClD,UACA,UACA,cACA,gBACA,cAAW,OACX,aACA,aACA,cACA,SACA,aACA,WACA,SACA,YACA,cACA,kBACA,sBACA,eACA,sBACA,yBACI;CACJ,IAAM,CAAC,GAAW,KAAgB,EAAS,GAAM,EAC3C,IAAY,EAAQ,KAAa,CAAC,GAElC,IAAe,EAAsB,GAAO,EAAS,EACrD,IACJ,MAAc,SAAS,GAAG,IAAe,IAAc,IAAI,EAAY,QAAQ,MAAM,GAAG,KAAK,UAAU,GAEnG,IAAe,EAAiB,EAAO,aAAa,KAAK;AAE/D,QACE,kBAAC,OAAD;EACE,WAAW,EACT,0OACA,KAAW,kBACX,EACD;EACQ;EACT,YAAY,MAAM;AAChB,GAAI,MAAY,EAAE,QAAQ,WAAW,EAAE,QAAQ,SAC7C,EAAE,gBAAgB,EAClB,GAAS;;EAGb,MAAM,IAAU,WAAW,KAAA;EAC3B,UAAU,IAAU,IAAI,KAAA;EACxB,cAAY,IAAU,QAAQ,MAAU,KAAA;YAf1C,CAkBE,kBAAC,OAAD;GAAK,WAAU;aAAf;IACG,IACC,kBAAC,OAAD;KACE,KAAK;KACL,KAAK;KACL,SAAQ;KACR,eAAe,EAAa,GAAK;KACjC,WAAU;KACV,CAAA,GAEF,kBAAC,GAAD,EAAuB,CAAA;IAIzB,kBAAC,OAAD,EAAK,WAAU,2GAA4G,CAAA;IAG3H,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,GAAD;MAAa,OAAO;MAAQ,SAAS;MAAc,KAAA;MAAM,CAAA;KACrD,CAAA;IAGL,KACC,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,MAAM;AAEd,MADA,EAAE,iBAAiB,EACnB,GAAmB;;KAErB,WAAW,EACT,mOACA,IAAgB,2BAA2B,sBAC5C;KACD,gBAAc;KACd,cAAY,IAAgB,0BAA0B;eAEtD,kBAAC,GAAD,EAAO,WAAW,EAAG,WAAW,KAAiB,eAAe,EAAI,CAAA;KAC7D,CAAA;IAIV,KACC,kBAAC,UAAD;KACE,MAAK;KACL,UAAU,MAAM;AAEd,MADA,EAAE,iBAAiB,EACnB,GAAiB;;KAEnB,WAAW,EACT,+LACA,IACI,kDACA,2DACL;KACD,gBAAc;KACd,cAAY,IAAoB,2BAA2B;eAEtC,EAApB,IAAqB,IAAgC,GAAjC,EAAO,WAAU,WAAY,CAAqC;KAChF,CAAA;IAIX,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,QAAD;MAAM,WAAU;gBAAsD;MAAoB,CAAA;KACtF,CAAA;IAGN,kBAAC,OAAD;KAAK,WAAU;eACb,kBAAC,QAAD;MAAM,WAAU;gBACb;MACI,CAAA;KACH,CAAA;IACF;MAGN,kBAAC,OAAD;GAAK,WAAU;aAAf;IAEE,kBAAC,OAAD;KAAK,WAAU;eAAf;MACE,kBAAC,MAAD;OAAI,WAAU;iBAAoD;OAAW,CAAA;MAC5E,MAAe,cACd,kBAAC,QAAD;OACE,WAAU;OACV,OAAM;iBAFR,CAIE,kBAAC,GAAD,EAAa,WAAU,WAAY,CAAA,EAAA,OAE9B;;MAER,MAAe,aACd,kBAAC,QAAD;OACE,WAAU;OACV,OAAM;iBACP;OAEM,CAAA;MAEL;;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,wBAAyB,CAAA,EAC3C,kBAAC,QAAD;MAAM,WAAU;gBAAoB;MAAgB,CAAA,CAChD;;IAGN,kBAAC,OAAD;KAAK,WAAU;eAAf;MACG,KAAY,QACX,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAK,WAAU,WAAY,CAAA,EAC3B,kBAAC,QAAD,EAAA,UAAA;QACG;QAAS;QAAE,MAAa,IAAI,QAAQ;QAChC,EAAA,CAAA,CACH;;MAEP,KAAa,QACZ,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAM,WAAU,WAAY,CAAA,EAC5B,kBAAC,QAAD,EAAA,UAAA;QACG;QAAU;QAAE,MAAc,IAAI,SAAS;QACnC,EAAA,CAAA,CACH;;MAER,kBAAC,OAAD;OAAK,WAAU;iBAAf,CACE,kBAAC,GAAD,EAAQ,WAAU,WAAY,CAAA,EAC9B,kBAAC,QAAD,EAAA,UAAA,CAAO,EAAa,EAAK,EAAC,SAAa,EAAA,CAAA,CACnC;;MACF;;IACF;KACF"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface SparklinePoint {
|
|
4
|
+
label: string;
|
|
5
|
+
value: number;
|
|
6
|
+
}
|
|
7
|
+
interface SparklineProps {
|
|
8
|
+
data: SparklinePoint[];
|
|
9
|
+
height?: number;
|
|
10
|
+
width?: number;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
export declare const Sparkline: FC<SparklineProps>;
|
|
14
|
+
export {};
|
|
15
|
+
//# sourceMappingURL=Sparkline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Sparkline.d.ts","sourceRoot":"","sources":["../../../src/housingvista/components/Sparkline.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAS,KAAK,EAAE,EAAE,MAAM,OAAO,CAAC;AAEvC,MAAM,WAAW,cAAc;IAC7B,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf;AAED,UAAU,cAAc;IACtB,IAAI,EAAE,cAAc,EAAE,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAID,eAAO,MAAM,SAAS,EAAE,EAAE,CAAC,cAAc,CAmDxC,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { useId as e } from "react";
|
|
2
|
+
import { jsx as t, jsxs as n } from "react/jsx-runtime";
|
|
3
|
+
//#region src/housingvista/components/Sparkline.tsx
|
|
4
|
+
var r = 3, i = ({ data: i, height: a = 28, width: o = 96, className: s }) => {
|
|
5
|
+
let c = e();
|
|
6
|
+
if (i.length < 2) return null;
|
|
7
|
+
let l = i.map((e) => e.value), u = Math.min(...l), d = Math.max(...l) - u || 1, f = l.map((e, t) => ({
|
|
8
|
+
x: t / (l.length - 1) * o,
|
|
9
|
+
y: a - r - (e - u) / d * (a - r * 2)
|
|
10
|
+
})), p = f.map((e, t) => `${t === 0 ? "M" : "L"}${e.x.toFixed(2)},${e.y.toFixed(2)}`).join(" "), m = `${p} L${f[f.length - 1].x.toFixed(2)},${a} L0,${a} Z`, h = f[f.length - 1], g = `Trend: ${i.map((e) => `${e.label} ${e.value}`).join(", ")}`;
|
|
11
|
+
return /* @__PURE__ */ n("svg", {
|
|
12
|
+
viewBox: `0 0 ${o} ${a}`,
|
|
13
|
+
width: o,
|
|
14
|
+
height: a,
|
|
15
|
+
className: s,
|
|
16
|
+
role: "img",
|
|
17
|
+
"aria-label": g,
|
|
18
|
+
children: [
|
|
19
|
+
/* @__PURE__ */ t("title", { children: g }),
|
|
20
|
+
/* @__PURE__ */ t("defs", { children: /* @__PURE__ */ n("linearGradient", {
|
|
21
|
+
id: c,
|
|
22
|
+
x1: "0",
|
|
23
|
+
y1: "0",
|
|
24
|
+
x2: "0",
|
|
25
|
+
y2: "1",
|
|
26
|
+
children: [/* @__PURE__ */ t("stop", {
|
|
27
|
+
offset: "0%",
|
|
28
|
+
stopColor: "currentColor",
|
|
29
|
+
stopOpacity: .25
|
|
30
|
+
}), /* @__PURE__ */ t("stop", {
|
|
31
|
+
offset: "100%",
|
|
32
|
+
stopColor: "currentColor",
|
|
33
|
+
stopOpacity: 0
|
|
34
|
+
})]
|
|
35
|
+
}) }),
|
|
36
|
+
/* @__PURE__ */ t("path", {
|
|
37
|
+
d: m,
|
|
38
|
+
fill: `url(#${c})`,
|
|
39
|
+
stroke: "none"
|
|
40
|
+
}),
|
|
41
|
+
/* @__PURE__ */ t("path", {
|
|
42
|
+
d: p,
|
|
43
|
+
fill: "none",
|
|
44
|
+
stroke: "currentColor",
|
|
45
|
+
strokeWidth: 1.75,
|
|
46
|
+
strokeLinecap: "round",
|
|
47
|
+
strokeLinejoin: "round"
|
|
48
|
+
}),
|
|
49
|
+
/* @__PURE__ */ t("circle", {
|
|
50
|
+
cx: h.x,
|
|
51
|
+
cy: h.y,
|
|
52
|
+
r: 2.2,
|
|
53
|
+
fill: "currentColor"
|
|
54
|
+
})
|
|
55
|
+
]
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
//#endregion
|
|
59
|
+
export { i as Sparkline };
|
|
60
|
+
|
|
61
|
+
//# sourceMappingURL=Sparkline.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Sparkline.js","names":[],"sources":["../../../src/housingvista/components/Sparkline.tsx"],"sourcesContent":["/**\n * Sparkline — tiny inline trend line for a StatCard (BOFF-3927).\n *\n * Self-contained (no cross-MFE imports, per the platform MFE rule): a\n * minimal SVG polyline that inherits `currentColor` so it always matches\n * the surrounding text color / semantic token, never a raw hex value.\n */\nimport { useId, type FC } from 'react';\n\nexport interface SparklinePoint {\n label: string;\n value: number;\n}\n\ninterface SparklineProps {\n data: SparklinePoint[];\n height?: number;\n width?: number;\n className?: string;\n}\n\nconst PAD_Y = 3;\n\nexport const Sparkline: FC<SparklineProps> = ({ data, height = 28, width = 96, className }) => {\n const gradientId = useId();\n\n if (data.length < 2) {\n return null;\n }\n\n const values = data.map((d) => d.value);\n const min = Math.min(...values);\n const max = Math.max(...values);\n const range = max - min || 1;\n\n const points = values.map((value, index) => {\n const x = (index / (values.length - 1)) * width;\n const y = height - PAD_Y - ((value - min) / range) * (height - PAD_Y * 2);\n return { x, y };\n });\n\n const linePath = points.map((p, i) => `${i === 0 ? 'M' : 'L'}${p.x.toFixed(2)},${p.y.toFixed(2)}`).join(' ');\n const areaPath = `${linePath} L${points[points.length - 1]!.x.toFixed(2)},${height} L0,${height} Z`;\n const last = points[points.length - 1]!;\n const title = `Trend: ${data.map((d) => `${d.label} ${d.value}`).join(', ')}`;\n\n return (\n <svg\n viewBox={`0 0 ${width} ${height}`}\n width={width}\n height={height}\n className={className}\n role=\"img\"\n aria-label={title}\n >\n <title>{title}</title>\n <defs>\n <linearGradient id={gradientId} x1=\"0\" y1=\"0\" x2=\"0\" y2=\"1\">\n <stop offset=\"0%\" stopColor=\"currentColor\" stopOpacity={0.25} />\n <stop offset=\"100%\" stopColor=\"currentColor\" stopOpacity={0} />\n </linearGradient>\n </defs>\n <path d={areaPath} fill={`url(#${gradientId})`} stroke=\"none\" />\n <path\n d={linePath}\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth={1.75}\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n />\n <circle cx={last.x} cy={last.y} r={2.2} fill=\"currentColor\" />\n </svg>\n );\n};\n"],"mappings":";;;AAqBA,IAAM,IAAQ,GAED,KAAiC,EAAE,SAAM,YAAS,IAAI,WAAQ,IAAI,mBAAgB;CAC7F,IAAM,IAAa,GAAO;AAE1B,KAAI,EAAK,SAAS,EAChB,QAAO;CAGT,IAAM,IAAS,EAAK,KAAK,MAAM,EAAE,MAAM,EACjC,IAAM,KAAK,IAAI,GAAG,EAAO,EAEzB,IADM,KAAK,IAAI,GAAG,EAAO,GACX,KAAO,GAErB,IAAS,EAAO,KAAK,GAAO,OAGzB;EAAE,GAFE,KAAS,EAAO,SAAS,KAAM;EAE9B,GADF,IAAS,KAAU,IAAQ,KAAO,KAAU,IAAS,IAAQ;EACxD,EACf,EAEI,IAAW,EAAO,KAAK,GAAG,MAAM,GAAG,MAAM,IAAI,MAAM,MAAM,EAAE,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,EAAE,QAAQ,EAAE,GAAG,CAAC,KAAK,IAAI,EACtG,IAAW,GAAG,EAAS,IAAI,EAAO,EAAO,SAAS,GAAI,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAO,MAAM,EAAO,KAC1F,IAAO,EAAO,EAAO,SAAS,IAC9B,IAAQ,UAAU,EAAK,KAAK,MAAM,GAAG,EAAE,MAAM,GAAG,EAAE,QAAQ,CAAC,KAAK,KAAK;AAE3E,QACE,kBAAC,OAAD;EACE,SAAS,OAAO,EAAM,GAAG;EAClB;EACC;EACG;EACX,MAAK;EACL,cAAY;YANd;GAQE,kBAAC,SAAD,EAAA,UAAQ,GAAc,CAAA;GACtB,kBAAC,QAAD,EAAA,UACE,kBAAC,kBAAD;IAAgB,IAAI;IAAY,IAAG;IAAI,IAAG;IAAI,IAAG;IAAI,IAAG;cAAxD,CACE,kBAAC,QAAD;KAAM,QAAO;KAAK,WAAU;KAAe,aAAa;KAAQ,CAAA,EAChE,kBAAC,QAAD;KAAM,QAAO;KAAO,WAAU;KAAe,aAAa;KAAK,CAAA,CAChD;OACZ,CAAA;GACP,kBAAC,QAAD;IAAM,GAAG;IAAU,MAAM,QAAQ,EAAW;IAAI,QAAO;IAAS,CAAA;GAChE,kBAAC,QAAD;IACE,GAAG;IACH,MAAK;IACL,QAAO;IACP,aAAa;IACb,eAAc;IACd,gBAAe;IACf,CAAA;GACF,kBAAC,UAAD;IAAQ,IAAI,EAAK;IAAG,IAAI,EAAK;IAAG,GAAG;IAAK,MAAK;IAAiB,CAAA;GAC1D"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { FC, ReactNode } from 'react';
|
|
2
|
+
import { SparklinePoint } from './Sparkline';
|
|
2
3
|
|
|
3
4
|
interface StatCardProps {
|
|
4
5
|
label: string;
|
|
@@ -12,6 +13,8 @@ interface StatCardProps {
|
|
|
12
13
|
className?: string;
|
|
13
14
|
/** Render the featured hero metric with a glass treatment + accent glow. */
|
|
14
15
|
featured?: boolean;
|
|
16
|
+
/** Trailing series (oldest -> newest) rendered as a sparkline (BOFF-3927). */
|
|
17
|
+
trendData?: SparklinePoint[];
|
|
15
18
|
}
|
|
16
19
|
export declare const StatCard: FC<StatCardProps>;
|
|
17
20
|
export {};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatCard.d.ts","sourceRoot":"","sources":["../../../src/housingvista/components/StatCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"StatCard.d.ts","sourceRoot":"","sources":["../../../src/housingvista/components/StatCard.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAK3C,OAAO,EAAa,KAAK,cAAc,EAAE,MAAM,aAAa,CAAC;AAE7D,UAAU,aAAa;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,KAAK,CAAC,EAAE,IAAI,GAAG,MAAM,GAAG,QAAQ,CAAC;IACjC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,cAAc,EAAE,CAAC;CAC9B;AAED,eAAO,MAAM,QAAQ,EAAE,EAAE,CAAC,aAAa,CAuEtC,CAAC"}
|
|
@@ -1,47 +1,51 @@
|
|
|
1
1
|
import { cn as e, formatCurrency as t, formatNumber as n } from "../utils/helpers.js";
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
2
|
+
import { Sparkline as r } from "./Sparkline.js";
|
|
3
|
+
import { Minus as i, TrendingDown as a, TrendingUp as o } from "lucide-react";
|
|
4
|
+
import { jsx as s, jsxs as c } from "react/jsx-runtime";
|
|
5
|
+
import { GlassCard as l } from "@burdenoff/fe-libs/ui";
|
|
5
6
|
//#region src/housingvista/components/StatCard.tsx
|
|
6
|
-
var
|
|
7
|
-
let
|
|
8
|
-
return /* @__PURE__ */
|
|
9
|
-
treatment:
|
|
10
|
-
glow:
|
|
11
|
-
className: e("border-border-seam p-4",
|
|
7
|
+
var u = ({ label: u, value: d, previousValue: f, unit: p, prefix: m, icon: h, trend: g, change: _, className: v, featured: y = !1, trendData: b }) => {
|
|
8
|
+
let x = _ ?? (f ? Math.round(((typeof d == "number" ? d : 0) - f) / f * 100) : 0), S = g ?? (x > 0 ? "up" : x < 0 ? "down" : "stable"), C = typeof d == "string" ? d : m === "$" ? t(d) : `${n(d)}${p ? ` ${p}` : ""}`;
|
|
9
|
+
return /* @__PURE__ */ c(l, {
|
|
10
|
+
treatment: y ? "glass" : "metric",
|
|
11
|
+
glow: y,
|
|
12
|
+
className: e("border-border-seam p-4", v),
|
|
12
13
|
children: [
|
|
13
|
-
/* @__PURE__ */
|
|
14
|
+
/* @__PURE__ */ c("div", {
|
|
14
15
|
className: "flex items-center justify-between",
|
|
15
|
-
children: [/* @__PURE__ */
|
|
16
|
+
children: [/* @__PURE__ */ s("span", {
|
|
16
17
|
className: "text-sm font-medium text-text-secondary",
|
|
17
|
-
children:
|
|
18
|
-
}),
|
|
18
|
+
children: u
|
|
19
|
+
}), h && /* @__PURE__ */ s("span", {
|
|
19
20
|
className: "text-text-secondary",
|
|
20
|
-
children:
|
|
21
|
+
children: h
|
|
21
22
|
})]
|
|
22
23
|
}),
|
|
23
|
-
/* @__PURE__ */
|
|
24
|
-
className: "mt-2 flex items-baseline gap-2",
|
|
25
|
-
children: /* @__PURE__ */
|
|
24
|
+
/* @__PURE__ */ c("div", {
|
|
25
|
+
className: "mt-2 flex items-baseline justify-between gap-2",
|
|
26
|
+
children: [/* @__PURE__ */ s("span", {
|
|
26
27
|
className: "text-2xl font-bold tabular-nums text-text-primary",
|
|
27
|
-
children:
|
|
28
|
-
})
|
|
28
|
+
children: C
|
|
29
|
+
}), b && b.length >= 2 && /* @__PURE__ */ s("span", {
|
|
30
|
+
className: e("shrink-0", S === "up" && "text-status-success-text", S === "down" && "text-status-error-text", S === "stable" && "text-text-secondary"),
|
|
31
|
+
children: /* @__PURE__ */ s(r, { data: b })
|
|
32
|
+
})]
|
|
29
33
|
}),
|
|
30
|
-
|
|
34
|
+
x !== 0 && /* @__PURE__ */ c("div", {
|
|
31
35
|
className: "mt-2 flex items-center gap-1 text-xs",
|
|
32
36
|
children: [
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
/* @__PURE__ */
|
|
37
|
-
className: e("font-medium tabular-nums",
|
|
37
|
+
S === "up" && /* @__PURE__ */ s(o, { className: "h-3 w-3 text-status-success-text" }),
|
|
38
|
+
S === "down" && /* @__PURE__ */ s(a, { className: "h-3 w-3 text-status-error-text" }),
|
|
39
|
+
S === "stable" && /* @__PURE__ */ s(i, { className: "h-3 w-3 text-text-secondary" }),
|
|
40
|
+
/* @__PURE__ */ c("span", {
|
|
41
|
+
className: e("font-medium tabular-nums", S === "up" && "text-status-success-text", S === "down" && "text-status-error-text", S === "stable" && "text-text-secondary"),
|
|
38
42
|
children: [
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
x > 0 ? "+" : "",
|
|
44
|
+
x,
|
|
41
45
|
"%"
|
|
42
46
|
]
|
|
43
47
|
}),
|
|
44
|
-
/* @__PURE__ */
|
|
48
|
+
/* @__PURE__ */ s("span", {
|
|
45
49
|
className: "text-text-secondary",
|
|
46
50
|
children: "vs last period"
|
|
47
51
|
})
|
|
@@ -51,6 +55,6 @@ var l = ({ label: l, value: u, previousValue: d, unit: f, prefix: p, icon: m, tr
|
|
|
51
55
|
});
|
|
52
56
|
};
|
|
53
57
|
//#endregion
|
|
54
|
-
export {
|
|
58
|
+
export { u as StatCard };
|
|
55
59
|
|
|
56
60
|
//# sourceMappingURL=StatCard.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"StatCard.js","names":[],"sources":["../../../src/housingvista/components/StatCard.tsx"],"sourcesContent":["import type { FC, ReactNode } from 'react';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/helpers';\nimport { formatNumber, formatCurrency } from '../utils/helpers';\n\ninterface StatCardProps {\n label: string;\n value: number | string;\n previousValue?: number;\n unit?: string;\n prefix?: string;\n icon?: ReactNode;\n trend?: 'up' | 'down' | 'stable';\n change?: number;\n className?: string;\n /** Render the featured hero metric with a glass treatment + accent glow. */\n featured?: boolean;\n}\n\nexport const StatCard: FC<StatCardProps> = ({\n label,\n value,\n previousValue,\n unit,\n prefix,\n icon,\n trend,\n change,\n className,\n featured = false,\n}) => {\n const numericValue = typeof value === 'number' ? value : 0;\n const computedChange =\n change ?? (previousValue ? Math.round(((numericValue - previousValue) / previousValue) * 100) : 0);\n const computedTrend = trend ?? (computedChange > 0 ? 'up' : computedChange < 0 ? 'down' : 'stable');\n\n const displayValue =\n typeof value === 'string'\n ? value\n : prefix === '$'\n ? formatCurrency(value)\n : `${formatNumber(value)}${unit ? ` ${unit}` : ''}`;\n\n return (\n <GlassCard\n treatment={featured ? 'glass' : 'metric'}\n glow={featured}\n className={cn('border-border-seam p-4', className)}\n >\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-secondary\">{label}</span>\n {icon && <span className=\"text-text-secondary\">{icon}</span>}\n </div>\n <div className=\"mt-2 flex items-baseline gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">{displayValue}</span>\n </div>\n {computedChange !== 0 && (\n <div className=\"mt-2 flex items-center gap-1 text-xs\">\n {computedTrend === 'up' && <TrendingUp className=\"h-3 w-3 text-status-success-text\" />}\n {computedTrend === 'down' && <TrendingDown className=\"h-3 w-3 text-status-error-text\" />}\n {computedTrend === 'stable' && <Minus className=\"h-3 w-3 text-text-secondary\" />}\n <span\n className={cn(\n 'font-medium tabular-nums',\n computedTrend === 'up' && 'text-status-success-text',\n computedTrend === 'down' && 'text-status-error-text',\n computedTrend === 'stable' && 'text-text-secondary'\n )}\n >\n {computedChange > 0 ? '+' : ''}\n {computedChange}%\n </span>\n <span className=\"text-text-secondary\">vs last period</span>\n </div>\n )}\n </GlassCard>\n );\n};\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"StatCard.js","names":[],"sources":["../../../src/housingvista/components/StatCard.tsx"],"sourcesContent":["import type { FC, ReactNode } from 'react';\nimport { TrendingUp, TrendingDown, Minus } from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { cn } from '../utils/helpers';\nimport { formatNumber, formatCurrency } from '../utils/helpers';\nimport { Sparkline, type SparklinePoint } from './Sparkline';\n\ninterface StatCardProps {\n label: string;\n value: number | string;\n previousValue?: number;\n unit?: string;\n prefix?: string;\n icon?: ReactNode;\n trend?: 'up' | 'down' | 'stable';\n change?: number;\n className?: string;\n /** Render the featured hero metric with a glass treatment + accent glow. */\n featured?: boolean;\n /** Trailing series (oldest -> newest) rendered as a sparkline (BOFF-3927). */\n trendData?: SparklinePoint[];\n}\n\nexport const StatCard: FC<StatCardProps> = ({\n label,\n value,\n previousValue,\n unit,\n prefix,\n icon,\n trend,\n change,\n className,\n featured = false,\n trendData,\n}) => {\n const numericValue = typeof value === 'number' ? value : 0;\n const computedChange =\n change ?? (previousValue ? Math.round(((numericValue - previousValue) / previousValue) * 100) : 0);\n const computedTrend = trend ?? (computedChange > 0 ? 'up' : computedChange < 0 ? 'down' : 'stable');\n\n const displayValue =\n typeof value === 'string'\n ? value\n : prefix === '$'\n ? formatCurrency(value)\n : `${formatNumber(value)}${unit ? ` ${unit}` : ''}`;\n\n return (\n <GlassCard\n treatment={featured ? 'glass' : 'metric'}\n glow={featured}\n className={cn('border-border-seam p-4', className)}\n >\n <div className=\"flex items-center justify-between\">\n <span className=\"text-sm font-medium text-text-secondary\">{label}</span>\n {icon && <span className=\"text-text-secondary\">{icon}</span>}\n </div>\n <div className=\"mt-2 flex items-baseline justify-between gap-2\">\n <span className=\"text-2xl font-bold tabular-nums text-text-primary\">{displayValue}</span>\n {trendData && trendData.length >= 2 && (\n <span\n className={cn(\n 'shrink-0',\n computedTrend === 'up' && 'text-status-success-text',\n computedTrend === 'down' && 'text-status-error-text',\n computedTrend === 'stable' && 'text-text-secondary'\n )}\n >\n <Sparkline data={trendData} />\n </span>\n )}\n </div>\n {computedChange !== 0 && (\n <div className=\"mt-2 flex items-center gap-1 text-xs\">\n {computedTrend === 'up' && <TrendingUp className=\"h-3 w-3 text-status-success-text\" />}\n {computedTrend === 'down' && <TrendingDown className=\"h-3 w-3 text-status-error-text\" />}\n {computedTrend === 'stable' && <Minus className=\"h-3 w-3 text-text-secondary\" />}\n <span\n className={cn(\n 'font-medium tabular-nums',\n computedTrend === 'up' && 'text-status-success-text',\n computedTrend === 'down' && 'text-status-error-text',\n computedTrend === 'stable' && 'text-text-secondary'\n )}\n >\n {computedChange > 0 ? '+' : ''}\n {computedChange}%\n </span>\n <span className=\"text-text-secondary\">vs last period</span>\n </div>\n )}\n </GlassCard>\n );\n};\n"],"mappings":";;;;;;AAuBA,IAAa,KAA+B,EAC1C,UACA,UACA,kBACA,SACA,WACA,SACA,UACA,WACA,cACA,cAAW,IACX,mBACI;CAEJ,IAAM,IACJ,MAAW,IAAgB,KAAK,QAFb,OAAO,KAAU,WAAW,IAAQ,KAEA,KAAiB,IAAiB,IAAI,GAAG,IAC5F,IAAgB,MAAU,IAAiB,IAAI,OAAO,IAAiB,IAAI,SAAS,WAEpF,IACJ,OAAO,KAAU,WACb,IACA,MAAW,MACT,EAAe,EAAM,GACrB,GAAG,EAAa,EAAM,GAAG,IAAO,IAAI,MAAS;AAErD,QACE,kBAAC,GAAD;EACE,WAAW,IAAW,UAAU;EAChC,MAAM;EACN,WAAW,EAAG,0BAA0B,EAAU;YAHpD;GAKE,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAA2C;KAAa,CAAA,EACvE,KAAQ,kBAAC,QAAD;KAAM,WAAU;eAAuB;KAAY,CAAA,CACxD;;GACN,kBAAC,OAAD;IAAK,WAAU;cAAf,CACE,kBAAC,QAAD;KAAM,WAAU;eAAqD;KAAoB,CAAA,EACxF,KAAa,EAAU,UAAU,KAChC,kBAAC,QAAD;KACE,WAAW,EACT,YACA,MAAkB,QAAQ,4BAC1B,MAAkB,UAAU,0BAC5B,MAAkB,YAAY,sBAC/B;eAED,kBAAC,GAAD,EAAW,MAAM,GAAa,CAAA;KACzB,CAAA,CAEL;;GACL,MAAmB,KAClB,kBAAC,OAAD;IAAK,WAAU;cAAf;KACG,MAAkB,QAAQ,kBAAC,GAAD,EAAY,WAAU,oCAAqC,CAAA;KACrF,MAAkB,UAAU,kBAAC,GAAD,EAAc,WAAU,kCAAmC,CAAA;KACvF,MAAkB,YAAY,kBAAC,GAAD,EAAO,WAAU,+BAAgC,CAAA;KAChF,kBAAC,QAAD;MACE,WAAW,EACT,4BACA,MAAkB,QAAQ,4BAC1B,MAAkB,UAAU,0BAC5B,MAAkB,YAAY,sBAC/B;gBANH;OAQG,IAAiB,IAAI,MAAM;OAC3B;OAAe;OACX;;KACP,kBAAC,QAAD;MAAM,WAAU;gBAAsB;MAAqB,CAAA;KACvD;;GAEE"}
|
|
@@ -14,4 +14,6 @@ export { PropertyCard } from './PropertyCard';
|
|
|
14
14
|
export { ContactOwnerDialog } from './ContactOwnerDialog';
|
|
15
15
|
export { ReviewsSection } from './ReviewsSection';
|
|
16
16
|
export { FileUploader } from './FileUploader';
|
|
17
|
+
export { Sparkline } from './Sparkline';
|
|
18
|
+
export type { SparklinePoint } from './Sparkline';
|
|
17
19
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/housingvista/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/housingvista/components/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,OAAO,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,YAAY,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,kBAAkB,EAAE,MAAM,sBAAsB,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAClD,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACxC,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC"}
|