@bytebrand/fe-ui-core-autobahn 1.0.37 → 1.0.39
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/locales/de/autobahn.json +771 -728
- package/locales/en/autobahn.json +771 -728
- package/package.json +1 -1
- package/theme/md3.css +4 -3
- package/vdp/VdpModules.tsx +199 -6
- package/vdp/index.ts +4 -0
- package/vdp/types.ts +22 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bytebrand/fe-ui-core-autobahn",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.39",
|
|
4
4
|
"description": "auto.de md3 (Autobahn) design system — UI primitives, cards, chrome, VDP modules, theme CSS and locales. Ships raw TypeScript source (like fe-ui-core before it); each consumer's bundler compiles it.",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"license": "UNLICENSED",
|
package/theme/md3.css
CHANGED
|
@@ -1466,9 +1466,10 @@
|
|
|
1466
1466
|
.md3 .vdp-header-summary .vdp-meta > span + span::before { content: ''; margin: 0 8px; }
|
|
1467
1467
|
.md3 .vdp-header-summary .vdp-meta > span { overflow: visible; text-overflow: clip; }
|
|
1468
1468
|
.md3 .vdp-header-summary .vdp-meta .lucide { display: none; }
|
|
1469
|
-
/* compact dot-line = km/EZ/Getriebe/Kraftstoff
|
|
1470
|
-
|
|
1471
|
-
|
|
1469
|
+
/* compact dot-line = km/EZ/Getriebe/Kraftstoff · Karosserie (round 9: body type must be
|
|
1470
|
+
visible here — was previously invisible under nth-child(n+5), which also swallowed it).
|
|
1471
|
+
Only "1. Hand" (item 5) stays hidden; item 6 (body type) now renders. */
|
|
1472
|
+
.md3 .vdp-header-summary .vdp-meta > span:nth-child(5) { display: none; }
|
|
1472
1473
|
/* highlights: the ≤980 Figma 2-col grid applies here too (no swipe-line since round 9) */
|
|
1473
1474
|
/* condition row: 2×2 on phone */
|
|
1474
1475
|
.md3 .zustand-grid { grid-template-columns: 1fr 1fr; }
|
package/vdp/VdpModules.tsx
CHANGED
|
@@ -26,7 +26,7 @@ import { TranslationFunction } from '../types';
|
|
|
26
26
|
|
|
27
27
|
import { Button, Icon, Spec, StatChip, Stars, Switch } from '../ui';
|
|
28
28
|
import { CO2_CLASSES, CO2_COLORS, TIER_LABELS, TIER_KEYS, TIER_COLORS } from '../constants';
|
|
29
|
-
import { defaultVdpSpecs, EquipmentGroups, VehicleDetail, VdpSpec, VdpReview } from './types';
|
|
29
|
+
import { defaultVdpSpecs, EquipmentGroups, PriceRatingBands, VehicleDetail, VdpSpec, VdpReview } from './types';
|
|
30
30
|
import { CalcRow, expanderBtnStyle, FinanceMode, Slider, TermChips } from './FinanceCalculator';
|
|
31
31
|
|
|
32
32
|
const eur = (n: number): string => n.toLocaleString('de-DE');
|
|
@@ -674,18 +674,157 @@ export const PriceHistory: React.FunctionComponent<PriceHistoryProps> = ({ vehic
|
|
|
674
674
|
);
|
|
675
675
|
}
|
|
676
676
|
|
|
677
|
+
// mobile-only (2026-07-17 round 10 decision): the real market-value gauge is scoped to
|
|
678
|
+
// phone widths for now — desktop keeps its exact pre-existing needs-data behaviour
|
|
679
|
+
// (hidden unless a real price-history sparkline exists) by rendering the ORIGINAL
|
|
680
|
+
// hide-mobile grid unconditionally, so it stays byte-identical whenever hasHistory is
|
|
681
|
+
// false (today, always). Once priceHistory gets a real source, both paths coexist fine.
|
|
682
|
+
const mobileOnlyMarket = hasMarket && !hasHistory;
|
|
683
|
+
|
|
677
684
|
return (
|
|
678
|
-
<div className=
|
|
685
|
+
<div className={'mcard' + (mobileOnlyMarket ? ' show-mobile' : '')} id="vdp-preis" style={{ padding: 22, ...(mobileOnlyMarket ? { flexDirection: 'column' } : {}) }}>
|
|
679
686
|
<h2 className="md-headline-sm" style={{ marginBottom: 4 }}>
|
|
680
687
|
{t('autobahn:priceMarket.heading')}
|
|
681
688
|
</h2>
|
|
682
689
|
<p className="md-body-sm" style={{ marginBottom: 18 }}>
|
|
683
690
|
{t('autobahn:priceMarket.description')}
|
|
684
691
|
</p>
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
692
|
+
{mobileOnlyMarket ? (
|
|
693
|
+
<>
|
|
694
|
+
{marketBlock}
|
|
695
|
+
<PriceRatingBreakdown vehicle={vehicle} t={t} />
|
|
696
|
+
</>
|
|
697
|
+
) : (
|
|
698
|
+
<div className="pmv-grid hide-mobile">
|
|
699
|
+
{historyBlock}
|
|
700
|
+
{marketBlock}
|
|
701
|
+
</div>
|
|
702
|
+
)}
|
|
703
|
+
</div>
|
|
704
|
+
);
|
|
705
|
+
};
|
|
706
|
+
|
|
707
|
+
// ---------------------------------------------------------------------------
|
|
708
|
+
// "Details zur Preisbewertung" expandable — criteria grid + 5-tier band list
|
|
709
|
+
// (mobile-only, round 10; real data — see toVehicleDetail's getPriceRatingConfig call)
|
|
710
|
+
// ---------------------------------------------------------------------------
|
|
711
|
+
|
|
712
|
+
/** [tier key, tone color var]. Order matches the Figma "Bewertungskategorien" list. */
|
|
713
|
+
const RATING_TIERS: ReadonlyArray<readonly [keyof PriceRatingBands, string]> = [
|
|
714
|
+
['topPrice', 'var(--md-good)'],
|
|
715
|
+
['goodPrice', '#4C9A2A'],
|
|
716
|
+
['fairPrice', '#C7A61A'],
|
|
717
|
+
['increasedPrice', '#D97B1F'],
|
|
718
|
+
['highPrice', 'var(--md-error)'],
|
|
719
|
+
];
|
|
720
|
+
|
|
721
|
+
const PriceRatingBreakdown: React.FunctionComponent<{ vehicle: VehicleDetail; t: TranslationFunction }> = ({ vehicle, t }) => {
|
|
722
|
+
const [open, setOpen] = useState(false);
|
|
723
|
+
const bands = vehicle.priceRatingBands;
|
|
724
|
+
if (!bands) return null;
|
|
725
|
+
|
|
726
|
+
const totalEquipment = Object.values(vehicle.equipment || {}).reduce((s, a) => s + a.length, 0);
|
|
727
|
+
const allCriteria: ReadonlyArray<readonly [icon: string, label: string, value: string]> = [
|
|
728
|
+
['car-front', t('autobahn:priceMarket.criteria.makeModel'), `${vehicle.make} ${vehicle.model}`],
|
|
729
|
+
['cog', t('autobahn:priceMarket.criteria.gear'), vehicle.gear],
|
|
730
|
+
['calendar', t('autobahn:priceMarket.criteria.reg'), vehicle.reg],
|
|
731
|
+
['droplet', t('autobahn:priceMarket.criteria.cons'), vehicle.cons],
|
|
732
|
+
['fuel', t('autobahn:priceMarket.criteria.fuel'), vehicle.fuel],
|
|
733
|
+
['leaf', t('autobahn:priceMarket.criteria.co2'), vehicle.co2],
|
|
734
|
+
['zap', t('autobahn:priceMarket.criteria.power'), vehicle.power],
|
|
735
|
+
['palette', t('autobahn:priceMarket.criteria.color'), vehicle.color],
|
|
736
|
+
['gauge', t('autobahn:priceMarket.criteria.km'), fmtKm(vehicle.km)],
|
|
737
|
+
['star', t('autobahn:priceMarket.criteria.equipment'), t('autobahn:priceMarket.criteria.equipmentCount', { count: totalEquipment })],
|
|
738
|
+
];
|
|
739
|
+
const criteria = allCriteria.filter(([, , v]) => !!v);
|
|
740
|
+
|
|
741
|
+
return (
|
|
742
|
+
<div style={{ marginTop: 18 }}>
|
|
743
|
+
<button
|
|
744
|
+
type="button"
|
|
745
|
+
onClick={() => setOpen((o) => !o)}
|
|
746
|
+
style={{ ...expanderBtnStyle, justifyContent: 'flex-start', gap: 6, padding: '2px 0' }}
|
|
747
|
+
>
|
|
748
|
+
{t('autobahn:priceMarket.detailsLink')}
|
|
749
|
+
<Icon name={open ? 'chevron-up' : 'chevron-down'} size={16} color="var(--md-primary)" />
|
|
750
|
+
</button>
|
|
751
|
+
{open && (
|
|
752
|
+
<div style={{ marginTop: 14, display: 'flex', flexDirection: 'column', gap: 18 }}>
|
|
753
|
+
<div className="mcard mcard--filled" style={{ padding: 16 }}>
|
|
754
|
+
<div className="md-title-sm" style={{ marginBottom: 12 }}>
|
|
755
|
+
{t('autobahn:priceMarket.criteriaTitle')}
|
|
756
|
+
</div>
|
|
757
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 14 }}>
|
|
758
|
+
{criteria.map(([ic, label, value]) => (
|
|
759
|
+
<div key={label} style={{ display: 'flex', alignItems: 'flex-start', gap: 9, minWidth: '44%', flex: '1 1 44%' }}>
|
|
760
|
+
<Icon name={ic} size={17} color="var(--md-primary)" stroke={1.8} style={{ marginTop: 1, flex: 'none' }} />
|
|
761
|
+
<span style={{ minWidth: 0 }}>
|
|
762
|
+
<span className="md-label-sm" style={{ display: 'block' }}>
|
|
763
|
+
{label}
|
|
764
|
+
</span>
|
|
765
|
+
<span className="md-title-sm" style={{ display: 'block', fontSize: 13.5 }}>
|
|
766
|
+
{value}
|
|
767
|
+
</span>
|
|
768
|
+
</span>
|
|
769
|
+
</div>
|
|
770
|
+
))}
|
|
771
|
+
</div>
|
|
772
|
+
</div>
|
|
773
|
+
|
|
774
|
+
<div className="mcard mcard--filled" style={{ padding: 16 }}>
|
|
775
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 12 }}>
|
|
776
|
+
<Icon name="bar-chart-3" size={18} color="var(--md-primary)" />
|
|
777
|
+
<div className="md-title-sm">{t('autobahn:priceMarket.categoriesTitle')}</div>
|
|
778
|
+
</div>
|
|
779
|
+
<div style={{ display: 'flex', flexDirection: 'column' }}>
|
|
780
|
+
{RATING_TIERS.map(([key, color], i) => (
|
|
781
|
+
<div
|
|
782
|
+
key={key}
|
|
783
|
+
style={{ padding: '12px 0', borderTop: i > 0 ? '1px solid var(--md-outline-variant)' : 0 }}
|
|
784
|
+
>
|
|
785
|
+
<div style={{ display: 'flex', alignItems: 'baseline', gap: 10, flexWrap: 'wrap' }}>
|
|
786
|
+
<span
|
|
787
|
+
className="md-label-sm"
|
|
788
|
+
style={{
|
|
789
|
+
fontWeight: 700,
|
|
790
|
+
padding: '4px 11px',
|
|
791
|
+
borderRadius: 'var(--md-r-full)',
|
|
792
|
+
color,
|
|
793
|
+
background: `color-mix(in srgb, ${color} 16%, transparent)`,
|
|
794
|
+
}}
|
|
795
|
+
>
|
|
796
|
+
{t(`autobahn:priceMarket.categories.${key}.label`)}
|
|
797
|
+
</span>
|
|
798
|
+
<span className="num md-label-sm" style={{ fontWeight: 700 }}>
|
|
799
|
+
{eur(bands[key].min)} – {eur(bands[key].max)} €
|
|
800
|
+
</span>
|
|
801
|
+
</div>
|
|
802
|
+
<p className="md-body-sm" style={{ margin: '6px 0 0' }}>
|
|
803
|
+
{t(`autobahn:priceMarket.categories.${key}.description`)}
|
|
804
|
+
</p>
|
|
805
|
+
</div>
|
|
806
|
+
))}
|
|
807
|
+
<div style={{ padding: '12px 0 0', borderTop: '1px solid var(--md-outline-variant)' }}>
|
|
808
|
+
<span
|
|
809
|
+
className="md-label-sm"
|
|
810
|
+
style={{
|
|
811
|
+
fontWeight: 700,
|
|
812
|
+
padding: '4px 11px',
|
|
813
|
+
borderRadius: 'var(--md-r-full)',
|
|
814
|
+
color: 'var(--md-on-surface-variant)',
|
|
815
|
+
background: 'var(--md-sc)',
|
|
816
|
+
}}
|
|
817
|
+
>
|
|
818
|
+
{t('autobahn:priceMarket.categories.noRating.label')}
|
|
819
|
+
</span>
|
|
820
|
+
<p className="md-body-sm" style={{ margin: '6px 0 0' }}>
|
|
821
|
+
{t('autobahn:priceMarket.categories.noRating.description')}
|
|
822
|
+
</p>
|
|
823
|
+
</div>
|
|
824
|
+
</div>
|
|
825
|
+
</div>
|
|
826
|
+
</div>
|
|
827
|
+
)}
|
|
689
828
|
</div>
|
|
690
829
|
);
|
|
691
830
|
};
|
|
@@ -1647,6 +1786,60 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
|
|
|
1647
1786
|
// Customer ratings (auto.de Google reviews) — REAL data, loaded by the page
|
|
1648
1787
|
// ---------------------------------------------------------------------------
|
|
1649
1788
|
|
|
1789
|
+
export interface ReviewsTeaserProps {
|
|
1790
|
+
/** review count (REAL — reviews.length from the same WP json-quotes source). */
|
|
1791
|
+
count: number;
|
|
1792
|
+
/** overall Google rating average — same real aggregate CustomerRatings displays. */
|
|
1793
|
+
average: number;
|
|
1794
|
+
/** i18n function (drilled from the page). */
|
|
1795
|
+
t: TranslationFunction;
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
/**
|
|
1799
|
+
* Compact "jump to reviews" teaser — mobile-only (round 10), sits between the offer card
|
|
1800
|
+
* and the highlights card (Figma "Google Review Teaser / Card"). Scrolls the CustomerRatings
|
|
1801
|
+
* section into view on tap; renders nothing when there are no real reviews to jump to.
|
|
1802
|
+
*/
|
|
1803
|
+
export const ReviewsTeaser: React.FunctionComponent<ReviewsTeaserProps> = ({ count, average, t }) => {
|
|
1804
|
+
if (!count) return null;
|
|
1805
|
+
const go = (): void => {
|
|
1806
|
+
if (typeof document === 'undefined') return;
|
|
1807
|
+
const el = document.getElementById('vdp-bewertungen');
|
|
1808
|
+
if (!el) return;
|
|
1809
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
1810
|
+
};
|
|
1811
|
+
return (
|
|
1812
|
+
<button
|
|
1813
|
+
type="button"
|
|
1814
|
+
onClick={go}
|
|
1815
|
+
className="mcard show-mobile"
|
|
1816
|
+
style={{
|
|
1817
|
+
width: '100%',
|
|
1818
|
+
textAlign: 'left',
|
|
1819
|
+
cursor: 'pointer',
|
|
1820
|
+
padding: '16px 18px',
|
|
1821
|
+
alignItems: 'center',
|
|
1822
|
+
justifyContent: 'space-between',
|
|
1823
|
+
gap: 12,
|
|
1824
|
+
// sits between the finance card (order:3) and the highlights card (order:4) in the
|
|
1825
|
+
// mobile .vdp-grid flex flow — tie-broken to AFTER the aside by JSX/DOM source order.
|
|
1826
|
+
order: 3,
|
|
1827
|
+
}}
|
|
1828
|
+
>
|
|
1829
|
+
<span style={{ minWidth: 0 }}>
|
|
1830
|
+
<span style={{ display: 'inline-flex', alignItems: 'center', gap: 8 }}>
|
|
1831
|
+
<Stars value={average} size={16} />
|
|
1832
|
+
<span className="num md-title-sm">{average.toLocaleString('de-DE', { minimumFractionDigits: 1, maximumFractionDigits: 1 })}/5</span>
|
|
1833
|
+
</span>
|
|
1834
|
+
<span className="md-label-sm" style={{ display: 'block', marginTop: 2 }}>
|
|
1835
|
+
{t('autobahn:reviews.teaserCount', { count })}
|
|
1836
|
+
</span>
|
|
1837
|
+
</span>
|
|
1838
|
+
<Icon name="chevron-right" size={19} color="var(--md-on-surface-variant)" style={{ flex: 'none' }} />
|
|
1839
|
+
</button>
|
|
1840
|
+
);
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1650
1843
|
export interface CustomerRatingsProps {
|
|
1651
1844
|
/** the loaded reviews (REAL — from the WP json-quotes source). */
|
|
1652
1845
|
reviews: VdpReview[];
|
package/vdp/index.ts
CHANGED
|
@@ -39,6 +39,7 @@ export {
|
|
|
39
39
|
VdpBottomBar,
|
|
40
40
|
InspectionBlock,
|
|
41
41
|
HowItWorks,
|
|
42
|
+
ReviewsTeaser,
|
|
42
43
|
CustomerRatings,
|
|
43
44
|
VdpEnquiry,
|
|
44
45
|
FinanceDetailsModal,
|
|
@@ -62,6 +63,7 @@ export type {
|
|
|
62
63
|
InspectionBlockProps,
|
|
63
64
|
HowItWorksProps,
|
|
64
65
|
HowItWorksStep,
|
|
66
|
+
ReviewsTeaserProps,
|
|
65
67
|
CustomerRatingsProps,
|
|
66
68
|
VdpEnquiryProps,
|
|
67
69
|
FinanceDetailsModalProps,
|
|
@@ -75,6 +77,8 @@ export type {
|
|
|
75
77
|
VdpSpec,
|
|
76
78
|
EquipmentGroups,
|
|
77
79
|
PriceHistoryPoint,
|
|
80
|
+
PriceRatingBand,
|
|
81
|
+
PriceRatingBands,
|
|
78
82
|
VdpReview,
|
|
79
83
|
AutobahnPhev,
|
|
80
84
|
} from './types';
|
package/vdp/types.ts
CHANGED
|
@@ -66,6 +66,25 @@ export interface PriceHistoryPoint {
|
|
|
66
66
|
price: number;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** One [min,max] EUR band of the 5-tier Preisbewertung (mirrors legacy IRatingConfig). */
|
|
70
|
+
export interface PriceRatingBand {
|
|
71
|
+
min: number;
|
|
72
|
+
max: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The full 5-tier price-rating band breakdown (EUR), same source/math as the header's
|
|
77
|
+
* rating pill (getPriceRatingConfig(currentSalesPrice, predictedPrice)) — REAL data, not
|
|
78
|
+
* needs-data. Powers the mobile "Die auto.de Bewertungskategorien" expandable list.
|
|
79
|
+
*/
|
|
80
|
+
export interface PriceRatingBands {
|
|
81
|
+
topPrice: PriceRatingBand;
|
|
82
|
+
goodPrice: PriceRatingBand;
|
|
83
|
+
fairPrice: PriceRatingBand;
|
|
84
|
+
increasedPrice: PriceRatingBand;
|
|
85
|
+
highPrice: PriceRatingBand;
|
|
86
|
+
}
|
|
87
|
+
|
|
69
88
|
/**
|
|
70
89
|
* View-model for a single vehicle as rendered by the Autobahn VDP modules.
|
|
71
90
|
*
|
|
@@ -172,6 +191,9 @@ export interface VehicleDetail {
|
|
|
172
191
|
marketHigh?: number;
|
|
173
192
|
/** number of buyers currently watching this car. */
|
|
174
193
|
watchers?: number;
|
|
194
|
+
/** full 5-tier Preisbewertung bands — REAL (same source as marketLow/marketHigh); powers the
|
|
195
|
+
* mobile "Die auto.de Bewertungskategorien" breakdown. Undefined when rating is unavailable. */
|
|
196
|
+
priceRatingBands?: PriceRatingBands;
|
|
175
197
|
}
|
|
176
198
|
|
|
177
199
|
/**
|