@bytebrand/fe-ui-core-autobahn 1.0.38 → 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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.38",
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",
@@ -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="mcard" id="vdp-preis" style={{ padding: 22 }}>
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
- <div className="pmv-grid">
686
- {historyBlock}
687
- {marketBlock}
688
- </div>
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
  /**