@bytebrand/fe-ui-core-autobahn 1.0.152 → 1.0.153

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/chrome/Footer.tsx CHANGED
@@ -86,6 +86,14 @@ export interface FooterProps {
86
86
  * (VehicleDetailedNextPage), never site-wide, since every other page shows only footnotes 1-4.
87
87
  */
88
88
  showCostFootnotes?: boolean;
89
+ /**
90
+ * Hides the brand blurb + "stay in contact"/social + 3 link columns block (the
91
+ * `.footer-grid`), leaving just the legal disclaimer and copyright/"Nach Oben" bar. Opt-in,
92
+ * off by default — every other caller keeps the full footer. Only CheckoutThankYouPage.tsx
93
+ * (checkout step 6 "Danke") uses this today: that page's own Figma reference (64637:37033)
94
+ * has just the compact legal/copyright strip, not the full site footer.
95
+ */
96
+ hideBrandColumns?: boolean;
89
97
  }
90
98
 
91
99
  /**
@@ -93,7 +101,7 @@ export interface FooterProps {
93
101
  * disclaimer block, and a copyright + "Nach Oben" bar. Pure/presentational MD3 chrome — renders
94
102
  * correctly only inside a `<div className="md3">` subtree (scoped theme at source/theme/autobahn).
95
103
  */
96
- const Footer: React.FunctionComponent<FooterProps> = ({ t, showCostFootnotes }) => {
104
+ const Footer: React.FunctionComponent<FooterProps> = ({ t, showCostFootnotes, hideBrandColumns }) => {
97
105
  const scrollTop = (): void => window.scroll({ top: 0, left: 0, behavior: 'smooth' });
98
106
  const now = new Date();
99
107
  const year = now.getFullYear();
@@ -104,6 +112,7 @@ const Footer: React.FunctionComponent<FooterProps> = ({ t, showCostFootnotes })
104
112
 
105
113
  return (
106
114
  <footer style={{ background: 'var(--md-sc-low)', borderTop: '1px solid var(--md-outline-variant)' }}>
115
+ {!hideBrandColumns ? (
107
116
  <div className="container footer-grid" style={{ padding: '40px 24px 24px' }}>
108
117
  <div>
109
118
  <img src={logoUrl} height="30" alt="auto.de" />
@@ -169,10 +178,14 @@ const Footer: React.FunctionComponent<FooterProps> = ({ t, showCostFootnotes })
169
178
  </div>
170
179
  ))}
171
180
  </div>
181
+ ) : null}
172
182
 
173
183
  {/* Legally required disclaimers (PAngV §17a financing example w/ live APR/nominal rate +
174
- term, DAT CO₂-Leitfaden, MwSt, Streichpreis, image notice). Same text as the legacy footer. */}
175
- <div style={{ borderTop: '1px solid var(--md-outline-variant)' }}>
184
+ term, DAT CO₂-Leitfaden, MwSt, Streichpreis, image notice). Same text as the legacy footer.
185
+ The top border separates this from .footer-grid above — with hideBrandColumns that
186
+ block is gone, so the border would just double up against whatever's already directly
187
+ above the footer on the page (CheckoutThankYouPage.tsx's own header border). */}
188
+ <div style={hideBrandColumns ? undefined : { borderTop: '1px solid var(--md-outline-variant)' }}>
176
189
  <div className="container" style={{ padding: '18px 24px' }}>
177
190
  <p
178
191
  className="md-label-sm"
@@ -37,11 +37,17 @@ export interface MdFieldProps {
37
37
  * Same contract/markup as FormField's own showValid (.mfield--valid/.mfield-ok, md3.css). */
38
38
  showValid?: boolean;
39
39
  onChange: (value: string, name?: string, event?: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
40
+ /** fires after the field's own internal blur handling (floating-label state). Callers use
41
+ * this to trigger `field.validate()` (mobx-react-form) on blur — this component only owns
42
+ * the `error` prop's display, not when validation actually runs, so without a caller wiring
43
+ * this in, `field.error` never populates from blur and a required-but-empty field never
44
+ * shows its error text/red border until some other validate() call happens to run. */
45
+ onBlur?: () => void;
40
46
  }
41
47
 
42
48
  const MdField: React.FunctionComponent<MdFieldProps> = ({
43
49
  className, error, sx, required, disabled, multiline, autoComplete,
44
- value, type, name, rows, label, placeholder, floatingLabel, maxLength, showValid, onChange
50
+ value, type, name, rows, label, placeholder, floatingLabel, maxLength, showValid, onChange, onBlur
45
51
  }) => {
46
52
  const [reveal, setReveal] = React.useState(false);
47
53
  const [focused, setFocused] = React.useState(false);
@@ -113,7 +119,7 @@ const MdField: React.FunctionComponent<MdFieldProps> = ({
113
119
  value={value == null ? '' : value}
114
120
  onChange={handleChange}
115
121
  onFocus={() => setFocused(true)}
116
- onBlur={() => setFocused(false)}
122
+ onBlur={() => { setFocused(false); onBlur?.(); }}
117
123
  disabled={disabled}
118
124
  rows={rows || 3}
119
125
  maxLength={maxLength}
@@ -132,7 +138,7 @@ const MdField: React.FunctionComponent<MdFieldProps> = ({
132
138
  value={value == null ? '' : value}
133
139
  onChange={handleChange}
134
140
  onFocus={() => setFocused(true)}
135
- onBlur={() => setFocused(false)}
141
+ onBlur={() => { setFocused(false); onBlur?.(); }}
136
142
  disabled={disabled}
137
143
  maxLength={maxLength}
138
144
  placeholder={effectivePlaceholder}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.152",
3
+ "version": "1.0.153",
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
@@ -2290,6 +2290,14 @@ line-height: 31px;
2290
2290
  font-size: 13px;
2291
2291
  line-height: 15px;
2292
2292
  color: #44464F;
2293
+ /* a long .co-error message (e.g. "Bitte wählen Sie eine der Optionen für Garantiepaket und
2294
+ Fahrzeugzulassung.") sharing this row (≥601px: same row via margin-right:auto/flex-end,
2295
+ see .co-next's own media queries) squeezed this plain text span for space same as any
2296
+ other flex child — with nothing stopping it from wrapping, "Schritt 4 von 5" split onto 2
2297
+ lines instead of the long error text (the one thing actually meant to wrap) absorbing the
2298
+ squeeze. nowrap + flex-shrink:0 keeps this at its natural one-line width unconditionally. */
2299
+ white-space: nowrap;
2300
+ flex-shrink: 0;
2293
2301
  }
2294
2302
  /* "Weiter" — its own fixed-size look, not the shared .mbtn--lg sizing (icon already dropped
2295
2303
  in the JSX, not just hidden here). Text styling lands on the button itself: Button.tsx
@@ -2356,6 +2364,22 @@ line-height: 31px;
2356
2364
  background: #F4F5FA;
2357
2365
  border-top: 0;
2358
2366
  }
2367
+ /* DOM order is .co-error (only rendered when a step's validation fails), .co-back-btn,
2368
+ .co-step-count, Weiter. `order` visually moves Zurück before the error text (its DOM
2369
+ position doesn't change, so the back button's click handler/focus order are unaffected).
2370
+ Zurück's base margin-right:auto (line ~2265) is what pushes Schritt/Weiter to the row's
2371
+ right edge in the common case (no error, .co-back-btn is the only thing before the gap) —
2372
+ kept as-is here. Only when .co-error IS present does that job need to hand off to it
2373
+ instead (so the error text sits right after Zurück rather than the auto-margin gap opening
2374
+ up BEFORE the error, between it and Zurück): the `.co-error + .co-back-btn` sibling
2375
+ selector matches only that case (error is Zurück's immediately preceding DOM sibling) and
2376
+ zeroes Zurück's margin, leaving .co-error's own margin-right:auto below as the sole push. */
2377
+ .md3 .co-next .co-back-btn {
2378
+ order: -1;
2379
+ }
2380
+ .md3 .co-next .co-error + .co-back-btn {
2381
+ margin-right: 0;
2382
+ }
2359
2383
  .md3 .co-next .co-error {
2360
2384
  margin-right: auto;
2361
2385
  }
@@ -2394,11 +2418,26 @@ line-height: 31px;
2394
2418
  .md3 .co-form.co-form--payment-details {
2395
2419
  border-bottom: 0;
2396
2420
  }
2421
+ /* order-overview step only — same reasoning, see OrderOverviewNext.tsx's "co-form oo-page". */
2422
+ .md3 .co-form.oo-page {
2423
+ border-bottom: 0;
2424
+ }
2397
2425
  .md3 .co-step-title {
2398
2426
  font: 700 22px var(--md-font-display, var(--md-font));
2399
2427
  margin: 0;
2400
2428
  color: var(--md-on-surface);
2401
2429
  }
2430
+ /* order-overview only ("Bestellübersicht") — desktop already shows this same title in
2431
+ CheckoutTopBarNext's own .co-topbar-heading (CheckoutChromeNext.tsx, same steps[activeStep].
2432
+ title source), so this becomes a duplicate there; kept for mobile, where that top bar is a
2433
+ compact single row with no room for it (CheckoutMobilePageTitle covers OTHER steps' mobile
2434
+ copy, but order-overview renders its own here instead). Same 600px mobile/desktop cutoff as
2435
+ .co-back-btn etc. elsewhere on this page. */
2436
+ @media (min-width: 601px) {
2437
+ .md3 .oo-page > .co-step-title {
2438
+ display: none;
2439
+ }
2440
+ }
2402
2441
  .md3 .co-step-sub {
2403
2442
  color: var(--md-on-surface-variant);
2404
2443
  font-size: 14px;
@@ -3249,8 +3288,12 @@ line-height: 31px;
3249
3288
  .md3 .co-pay-bene b {
3250
3289
  display: block;
3251
3290
  margin-bottom: 2px;
3252
- font-size: 13.5px;
3253
- color: var(--md-on-surface);
3291
+ font-family: 'Arimo';
3292
+ font-style: normal;
3293
+ font-weight: 700;
3294
+ font-size: 15px;
3295
+ line-height: 140%;
3296
+ color: #1A1B22;
3254
3297
  }
3255
3298
  .md3 .co-pay-bene-desc {
3256
3299
  font-family: 'Arimo';
@@ -4474,10 +4517,17 @@ line-height: 31px;
4474
4517
  }
4475
4518
  /* "4. Lieferung & Services" (Figma node 63570:28320) — up to 3 cells in a row, divided by a 1px
4476
4519
  rule; the rule is a border on the cell itself (:not(:first-child)) rather than a separate DOM
4477
- node, so it stays correct no matter which 1-3 of the 3 cells are conditionally rendered. */
4520
+ node, so it stays correct no matter which 1-3 of the 3 cells are conditionally rendered.
4521
+ align-items: stretch (not flex-start) — a border-left only ever reaches as far down as its own
4522
+ box, so with flex-start each divider stopped at its OWN cell's content height instead of the
4523
+ row's tallest cell (the 3-line-wrapping "9. Gewährleistung & Herstellergarantie" title makes
4524
+ that cell the tallest whenever it's shown), leaving the shorter cells' dividers visibly cut off
4525
+ partway down. Cells stretching to the row's full height doesn't shift their own icon+text
4526
+ (each cell keeps its own align-items: flex-start below), it only extends the empty space (and
4527
+ therefore the border) to match. */
4478
4528
  .md3 .oo-ls-row {
4479
4529
  display: flex;
4480
- align-items: flex-start;
4530
+ align-items: stretch;
4481
4531
  }
4482
4532
  .md3 .oo-ls-cell {
4483
4533
  display: flex;
@@ -4801,11 +4851,6 @@ line-height: 31px;
4801
4851
  color: var(--md-on-surface-variant);
4802
4852
  cursor: pointer;
4803
4853
  }
4804
- .md3 .oo-underline {
4805
- color: var(--md-primary);
4806
- text-decoration: underline;
4807
- cursor: pointer;
4808
- }
4809
4854
  /* "1. Fahrzeug & Verkäufer" card (Figma node 63570:28262) — own full-width .co-card, sitting
4810
4855
  above .oo-grid (not a grid column), so no bottom divider/margin like the legacy .oo-car row
4811
4856
  (that one used to sit above other rows sharing the same card). */
@@ -4948,16 +4993,94 @@ line-height: 31px;
4948
4993
  font-size: 13px;
4949
4994
  cursor: pointer;
4950
4995
  }
4951
- .md3 .oo-terms {
4952
- margin-top: 20px;
4996
+ /* Figma 63570:28475 ("CK / Content — Rechtliche Hinweise") — text/underline color is the SAME
4997
+ muted gray as the body copy (var(--md-on-surface-variant) === Figma's var(--text/muted,
4998
+ #44464f) fallback exactly), not the design system's usual blue link color (.oo-underline,
4999
+ used elsewhere), so links here are styled with underline only. No border per explicit
5000
+ request. Sits below .oo-next (order-overview's own bottom bar) — a spot this exact node
5001
+ isn't shown at in Figma (its own component description has it above the buy button instead),
5002
+ so this margin-top is this page's own established gap rhythm, not a value read off the design. */
5003
+ .md3 .oo-legal-notice {
5004
+ margin-top: 16px;
5005
+ font-family: Arial;
4953
5006
  font-size: 13px;
5007
+ line-height: 1.5;
4954
5008
  color: var(--md-on-surface-variant);
4955
- line-height: 1.6;
4956
5009
  }
4957
- .md3 .oo-buy {
5010
+ .md3 .oo-legal-link {
5011
+ text-decoration: underline;
5012
+ color: inherit;
5013
+ cursor: pointer;
5014
+ }
5015
+ /* Order overview's own "Zurück / Schritt X von Y / Jetzt kaufen" action bar (Figma 63502:5801) —
5016
+ distinct classes from .co-next/.co-back-btn/.co-step-count (CheckoutChromeNext.tsx) rather than
5017
+ reusing them: that bar's CTA is "advance to next step" and returns null on the last step, this
5018
+ one's CTA is "buy now" (OfferRequestButtonWrapper) with its own, larger Figma-specified sizing. */
5019
+ .md3 .oo-next {
4958
5020
  display: flex;
4959
- justify-content: flex-end;
4960
- margin-top: 16px;
5021
+ align-items: center;
5022
+ gap: 16px;
5023
+ margin-top: 24px;
5024
+ padding-top: 18px;
5025
+ border-top: 1px solid #C5C6D2;
5026
+ }
5027
+ .md3 .oo-next-back {
5028
+ display: flex;
5029
+ align-items: center;
5030
+ justify-content: center;
5031
+ height: 48px;
5032
+ padding: 0 16px;
5033
+ margin-right: auto;
5034
+ border: 0;
5035
+ background: none;
5036
+ border-radius: 16px;
5037
+ font-family: 'Arial';
5038
+ font-style: normal;
5039
+ font-weight: 700;
5040
+ font-size: 15px;
5041
+ line-height: normal;
5042
+ color: #005CFF;
5043
+ cursor: pointer;
5044
+ }
5045
+ .md3 .oo-next-count {
5046
+ font-family: 'Arial';
5047
+ font-style: normal;
5048
+ font-weight: 400;
5049
+ font-size: 13px;
5050
+ line-height: normal;
5051
+ color: #44464F;
5052
+ white-space: nowrap;
5053
+ }
5054
+ .md3 .oo-buy-btn {
5055
+ display: flex;
5056
+ flex-direction: row;
5057
+ justify-content: center;
5058
+ align-items: center;
5059
+ padding: 0 16px;
5060
+ gap: 8px;
5061
+ width: 340px;
5062
+ height: 60px;
5063
+ background: #005CFF;
5064
+ border-radius: 16px;
5065
+ font-family: 'Arial';
5066
+ font-style: normal;
5067
+ font-weight: 700;
5068
+ font-size: 17px;
5069
+ line-height: normal;
5070
+ color: #FFFFFF;
5071
+ }
5072
+ @media (max-width: 700px) {
5073
+ .md3 .oo-next-count {
5074
+ display: none;
5075
+ }
5076
+ }
5077
+ @media (max-width: 600px) {
5078
+ .md3 .oo-next-back {
5079
+ display: none;
5080
+ }
5081
+ .md3 .oo-buy-btn {
5082
+ width: 100%;
5083
+ }
4961
5084
  }
4962
5085
  @media (max-width: 800px) {
4963
5086
  .md3 .oo-grid {
@@ -5124,12 +5247,33 @@ line-height: 31px;
5124
5247
  line instead of sitting visibly lower (more obvious once the label wraps to 2 lines). */
5125
5248
  margin-top: 7px;
5126
5249
  }
5127
- /* Figma's design has no inline error message under the checkbox, only the red box border
5128
- (.mcheck-group--error above already gives that) — the shared FormCheckbox's own error text
5129
- is hidden here rather than touching that component (other FormCheckbox usages sitewide still
5130
- show their error text as before). */
5131
- .md3 .oo-terms-block .mcheck-error-text {
5132
- display: none;
5250
+ /* Error state (Figma 64670:106293, "Jetzt kaufen" clicked without confirming both agreements) —
5251
+ unconfirmed checkbox gets a thicker red box, plus an icon+message row underneath, indented
5252
+ 32px (20px box + 12px gap) to sit under the label rather than the box. errorIcon (FormCheckbox
5253
+ prop, opt-in) renders that icon; every other FormCheckbox caller sitewide is untouched. */
5254
+ .md3 .oo-terms-block .mcheck-group--error .mcheck-input:not(:checked) + .mcheck-box {
5255
+ border-width: 2px;
5256
+ }
5257
+ .md3 .oo-terms-block .mcheck-error-text--icon {
5258
+ display: flex;
5259
+ align-items: flex-start;
5260
+ gap: 6px;
5261
+ padding-left: 32px;
5262
+ margin-top: -2px;
5263
+ }
5264
+ .md3 .oo-terms-block .mcheck-error-icon {
5265
+ flex: none;
5266
+ margin-top: 1px;
5267
+ }
5268
+ .md3 .oo-terms-block .mcheck-error-msg {
5269
+ flex: 1 0 0;
5270
+ font-size: 13px;
5271
+ line-height: 1.35;
5272
+ }
5273
+ /* Card itself gets a red border once either checkbox is showing its error (OrderOverviewNext's
5274
+ termsHasError — same hasError+error double-check as FormCheckbox uses internally). */
5275
+ .md3 .oo-terms-card--error {
5276
+ border: 2px solid var(--md-error);
5133
5277
  }
5134
5278
  /* "7. Ergänzende Regelungen" dialogs (Verkürzung der Verjährungsfrist / Ausschluss der
5135
5279
  Aktualisierung, Figma 64596:36289 desktop / 64596:36264 mobile) — rendered via the shared
@@ -5186,6 +5330,60 @@ line-height: 31px;
5186
5330
  line-height: 1.5;
5187
5331
  color: #0E2238;
5188
5332
  }
5333
+ /* Legal-notice row's 3 dialogs (AGB von auto.de / AGB des Verkäufers / Widerrufsbelehrung —
5334
+ Figma 64604:48257 / 63583:6830 / 63583:6850) reuse the SAME oo-terms-modal chrome above, but
5335
+ these bodies are long reference documents rather than a single clause, so Figma specs them
5336
+ smaller/muted (text/muted #44464F) with bold text/strong (#1A1B22) section headings instead of
5337
+ .oo-terms-modal-text's bigger, darker single-paragraph style. oo-terms-modal-legal-text also
5338
+ carries the .oo-terms-modal-legal-html "AGB von auto.de"/"AGB des Verkäufers" bodies' shared
5339
+ base (dangerouslySetInnerHTML'd CMS content / seller.termCond plain text — both re-used
5340
+ verbatim from elsewhere in the app, not authored here, see OrderOverviewNext.tsx). */
5341
+ .md3 .oo-terms-modal-legal-text,
5342
+ .md3 .oo-terms-modal-legal-html {
5343
+ font-family: Arial;
5344
+ font-weight: 400;
5345
+ font-size: 13px;
5346
+ line-height: 1.48;
5347
+ color: #44464F;
5348
+ }
5349
+ .md3 .oo-terms-modal-legal-text {
5350
+ margin: 0;
5351
+ /* seller.termCond is plain text (Imprint.tsx renders it the same way) — preserves its literal
5352
+ line breaks instead of collapsing them. No-op for the revocation dialog's own JSON strings,
5353
+ which don't contain any. */
5354
+ white-space: pre-line;
5355
+ }
5356
+ .md3 .oo-terms-modal-legal-heading {
5357
+ margin: 0;
5358
+ font-family: Arial;
5359
+ font-weight: 700;
5360
+ font-size: 15px;
5361
+ line-height: 1.48;
5362
+ color: #1A1B22;
5363
+ }
5364
+ .md3 .oo-terms-modal-legal-body {
5365
+ display: flex;
5366
+ flex-direction: column;
5367
+ gap: 12px;
5368
+ }
5369
+ /* ourTermsAndConditions is a WordPress `content.rendered` HTML blob (CheckoutStore.ts) — its own
5370
+ <p> tags need their own spacing/heading treatment since it isn't discrete React children like
5371
+ oo-terms-modal-legal-body's blocks above. */
5372
+ .md3 .oo-terms-modal-legal-html p {
5373
+ margin: 0 0 12px;
5374
+ }
5375
+ .md3 .oo-terms-modal-legal-html p:last-child {
5376
+ margin-bottom: 0;
5377
+ }
5378
+ .md3 .oo-terms-modal-legal-html strong,
5379
+ .md3 .oo-terms-modal-legal-html b,
5380
+ .md3 .oo-terms-modal-legal-html h1,
5381
+ .md3 .oo-terms-modal-legal-html h2,
5382
+ .md3 .oo-terms-modal-legal-html h3 {
5383
+ font-weight: 700;
5384
+ font-size: 15px;
5385
+ color: #1A1B22;
5386
+ }
5189
5387
  /* mobile bottom sheet (.mmodal's own @media max-width:560px already makes it full-width/
5190
5388
  bottom-pinned/top-rounded) — Figma's mobile header additionally wants a 24px title (vs 22px
5191
5389
  on desktop) and a decorative grip bar (Figma 64596:36264's handleRow), which this Modal
@@ -5256,15 +5454,16 @@ line-height: 31px;
5256
5454
  .md3 .oo-promo-body .mfield:focus-within {
5257
5455
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
5258
5456
  }
5259
- /* "9. Gewährleistung & Herstellergarantie" (Figma 64402:60309 collapsed / 64559:104000
5260
- expanded) — 2 levels of collapse: the outer card, then its own nested "Herstellergarantie"
5261
- row. Both graphic blocks (the EU statutory-warranty notice, the GARAN certificate) are
5262
- static images, not hand-coded — see the long comment above WARRANTY_NOTICE_IMG in
5263
- OrderOverviewNext.tsx for why (Figma ships them fully vector-flattened / no real per-car
5264
- data exists yet). */
5457
+ /* Cards 9 "Gewährleistung" / 10 "Herstellergarantie" (ADV-295; Figma 64669:106647 /
5458
+ 64669:108591) — share one .oo-warranty-card/.oo-warranty-body treatment (same card chrome,
5459
+ flat single-level collapse, same link/image styling); which fields each renders is decided in
5460
+ the JSX, not here. Both graphic blocks (the EU statutory-warranty notice, the GARAN
5461
+ certificate) are static images, not hand-coded — see the long comment above WARRANTY_NOTICE_IMG
5462
+ in OrderOverviewNext.tsx for why (Figma ships them fully vector-flattened). */
5265
5463
  /* same fix as .oo-promo-card above — .co-card-title's shared margin-bottom:16px leaves a
5266
- stray gap under the title when the card is collapsed (default here); .oo-warranty-body's
5267
- own margin-top restores that 16px only once actually expanded. */
5464
+ stray gap under the title when the card is collapsed; .oo-warranty-body's own margin-top
5465
+ restores that 16px only once actually expanded (both cards default expanded, but the user can
5466
+ still collapse them). */
5268
5467
  .md3 .oo-warranty-card .co-card-title {
5269
5468
  margin-bottom: 0;
5270
5469
  }
@@ -5272,6 +5471,7 @@ line-height: 31px;
5272
5471
  margin-top: 16px;
5273
5472
  display: flex;
5274
5473
  flex-direction: column;
5474
+ gap: 12px;
5275
5475
  }
5276
5476
  .md3 .oo-warranty-notice-img {
5277
5477
  display: block;
@@ -5279,36 +5479,6 @@ line-height: 31px;
5279
5479
  max-width: 400px;
5280
5480
  height: auto;
5281
5481
  }
5282
- .md3 .oo-warranty-divider {
5283
- margin-top: 16px;
5284
- height: 1px;
5285
- background: var(--md-outline-variant);
5286
- }
5287
- .md3 .oo-warranty-subrow {
5288
- margin-top: 14px;
5289
- padding-bottom: 6px;
5290
- display: flex;
5291
- align-items: center;
5292
- justify-content: space-between;
5293
- gap: 10px;
5294
- width: 100%;
5295
- background: none;
5296
- border: 0;
5297
- cursor: pointer;
5298
- text-align: left;
5299
- }
5300
- .md3 .oo-warranty-subrow-label {
5301
- font-family: Arial, Helvetica, Arimo, sans-serif;
5302
- font-weight: 700;
5303
- font-size: 15px;
5304
- color: #0E2238;
5305
- }
5306
- .md3 .oo-warranty-manufacturer-body {
5307
- margin-top: 10px;
5308
- display: flex;
5309
- flex-direction: column;
5310
- gap: 12px;
5311
- }
5312
5482
  .md3 .oo-warranty-garan-img {
5313
5483
  display: block;
5314
5484
  width: 100%;
@@ -5783,11 +5953,20 @@ a.oo-warranty-link--icon:hover {
5783
5953
  @media (min-width: 601px) and (max-width: 993px) {
5784
5954
  .md3 .co-next {
5785
5955
  flex-direction: row;
5786
- flex-wrap: nowrap;
5956
+ flex-wrap: wrap;
5787
5957
  align-items: center;
5788
5958
  justify-content: flex-end;
5789
5959
  gap: 16px;
5790
5960
  }
5961
+ /* same reasoning as the ≤700px block above: .co-error (a step-validation message, can run
5962
+ long — e.g. "Bitte wählen Sie eine der Optionen für Garantiepaket und Fahrzeugzulassung.")
5963
+ must not crowd onto the Zurück/Schritt/Weiter row. Without this, flex-wrap: nowrap forced
5964
+ all 4 children onto one line with no room, so the error visually overlapped Zurück and
5965
+ squeezed "Schritt X von Y" into wrapping onto 2 lines. flex-basis:100% forces the error onto
5966
+ its own first line; flex-wrap: wrap (above) then lays the 3 buttons/label out on the next. */
5967
+ .md3 .co-next .co-error {
5968
+ flex: 1 0 100%;
5969
+ }
5791
5970
  }
5792
5971
  /* below the 992px desktop breakpoint the content no longer reserves the 424px left
5793
5972
  gap, so the fixed rail would overlap the form — hide it (mobile uses the header stepper). */
@@ -9299,6 +9478,64 @@ a.oo-warranty-link--icon:hover {
9299
9478
  padding: 18px 20px 22px;
9300
9479
  overflow-y: auto;
9301
9480
  }
9481
+ /* Checkout's Finanzierungsdetails/Leasingdetails modal (CheckoutFinancePanelNext's cf-details
9482
+ button) reuses this SAME FinanceDetailsModal as the VDP page, but the rate hero + price-rating
9483
+ pill/bar (.fin-price-grid) would just repeat the SAME figures the cf-panel already shows above
9484
+ this button — unlike the Anzahlung/Laufzeit sliders and Mit-Schlussrate/Rundum-Sorglos-Paket
9485
+ toggles (disabled by simply not passing their change handlers), nothing gates this block by
9486
+ prop, so it's hidden here instead. Scoped to the checkout portal wrapper only — VDP/SearchPage
9487
+ keep it. */
9488
+ .md3.cf-fin-details-modal .fin-price-grid {
9489
+ display: none;
9490
+ }
9491
+ /* Leasing's breakdown table runs longer than financing's (Leasinggeber+Vertragsart split, the
9492
+ mandatory Rundum-Sorglos-Paket notice box, an extra disclaimer line) — 88vh left it a little
9493
+ short of fitting whole, needing a few pixels of real scroll even after the hero/sliders/
9494
+ toggles/CTA above are stripped. A taller cap gives it room to fit outright on realistic
9495
+ viewports; overflow-y:auto above still catches genuinely short windows as a fallback. */
9496
+ .md3.cf-fin-details-modal .fin-modal {
9497
+ max-height: 95vh;
9498
+ }
9499
+ /* overflow-y:visible here (tried first for the short financing case) turned out to break the
9500
+ LONGER leasing case: without a real scroll container, flexbox's automatic min-height:auto
9501
+ let .fin-modal-body grow to its full content height (839px+ — leasing adds the Leasinggeber/
9502
+ Vertragsart split + the mandatory Rundum-Sorglos-Paket notice box) past .fin-modal's own
9503
+ max-height:88vh, and since .fin-modal-scrim centers it, the overflow pushed the header off the
9504
+ TOP of the viewport instead of clipping the bottom. Keep the real overflow-y:auto (correct
9505
+ height-constraining behaviour for any content length) and just hide the bar. */
9506
+ .md3.cf-fin-details-modal .fin-modal-body {
9507
+ scrollbar-width: none;
9508
+ -ms-overflow-style: none;
9509
+ }
9510
+ .md3.cf-fin-details-modal .fin-modal-body::-webkit-scrollbar {
9511
+ display: none;
9512
+ }
9513
+ /* "Rate anfragen"/call CTA row — checkout has no separate request flow to trigger from inside an
9514
+ already-running checkout (reviewing the breakdown IS the action), so this instance drops the
9515
+ row entirely rather than wiring it to a stand-in action. Unlike the sliders/toggles, it isn't
9516
+ gated by a handler prop, so it's hidden here. Shared class, one rule covers both breakpoints.
9517
+ `!important` — the element carries an inline style="display:flex" (VdpModules.tsx), which
9518
+ otherwise wins over any external rule regardless of selector specificity. */
9519
+ .md3.cf-fin-details-modal .fin-sheet-cta-row {
9520
+ display: none !important;
9521
+ }
9522
+ /* the (kept) legal breakdown's wrapper carries an inline marginTop:18 meant to separate it from
9523
+ the now-hidden hero/sliders/toggles above — stacked on .fin-modal-body's own 18px top padding
9524
+ that reads as unexplained empty space once those sections are gone. `!important` because the
9525
+ marginTop is inline (VdpModules.tsx), and only the DESKTOP wrapper needs it: it's the immediate
9526
+ next sibling of the (also hidden) .fin-price-grid there; the mobile breakdown wrapper is
9527
+ separated from .fin-price-grid by the still-visible data-sheet-fold button, so this selector
9528
+ doesn't reach it and mobile's spacing (already just marginBottom, no marginTop) is untouched. */
9529
+ .md3.cf-fin-details-modal .fin-price-grid + div {
9530
+ margin-top: 0 !important;
9531
+ }
9532
+ /* mobile breakpoint below sets align-items:flex-start on .fin-modal-head, top-aligning the close
9533
+ button against the 2-line "Kredit-Rate anpassen" + vehicle-name title — checkout's stripped
9534
+ instance has nothing that needs that extra top room, so it stays vertically centered instead
9535
+ (3-class selector already outweighs the 2-class mobile rule, no !important needed). */
9536
+ .md3.cf-fin-details-modal .fin-modal-head {
9537
+ align-items: center;
9538
+ }
9302
9539
  @media (max-width: 600px) {
9303
9540
  .md3 .fin-modal-scrim {
9304
9541
  align-items: flex-end;
@@ -14177,3 +14414,247 @@ color: #005CFF;
14177
14414
  border-bottom: 1px solid var(--md-outline-variant);
14178
14415
  }
14179
14416
  }
14417
+
14418
+ /* Post-purchase confirmation modal (CONGRATULATIONS_CHECKOUT_MODAL, Congratulations.tsx) —
14419
+ Figma 64637:37064 (desktop) / 63536:6276 (mobile). BaseModal's own `.modalCheckout` chrome
14420
+ (BaseModal.styl: fixed 375px width, square corners, its own white bg + shadow) is neutralized
14421
+ here via the `cgz-modal` className OfferRequestButtonWrapper passes — Congratulations.tsx's
14422
+ own `.cgz-card` supplies the actual Figma card (border/radius/bg/padding) instead, so there's
14423
+ no double box. `.md3` ancestor: App.tsx's root `<div className="${styles.app} md3">` wraps
14424
+ the whole SPA unconditionally (confirmed — this is NOT the same as the per-page opt-in `.md3`
14425
+ noted elsewhere for legacy MainContent pages), and BaseModal renders inline (no portal), so a
14426
+ single `.md3`-prefixed selector is enough, same as every other CheckoutPage-tree rule already
14427
+ in this file. */
14428
+ .md3 .cgz-modal {
14429
+ width: 800px;
14430
+ max-width: calc(100vw - 48px);
14431
+ background: transparent;
14432
+ border: 0;
14433
+ border-radius: 0;
14434
+ box-shadow: none;
14435
+ padding: 0;
14436
+ }
14437
+ .md3 .cgz-card {
14438
+ display: flex;
14439
+ flex-direction: column;
14440
+ align-items: center;
14441
+ gap: 32px;
14442
+ /* Figma: fixed 800px, not fluid — was width:100%, which (now that this also renders as a
14443
+ real page, not just inside the fixed-800px .cgz-modal) stretched edge-to-edge on any
14444
+ viewport wider than 800px+padding instead of staying a centered 800px card. max-width is
14445
+ just the narrow-viewport safety clamp (matches .cgz-modal's own calc(100vw - 48px)) —
14446
+ the max-width:600px block below still overrides both to 100% for the mobile layout. */
14447
+ width: 800px;
14448
+ max-width: calc(100vw - 48px);
14449
+ background: #FFFFFF;
14450
+ border: 1px solid var(--md-outline-variant);
14451
+ border-radius: 16px;
14452
+ padding: 48px 56px;
14453
+ box-sizing: border-box;
14454
+ }
14455
+ .md3 .cgz-intro {
14456
+ display: flex;
14457
+ flex-direction: column;
14458
+ align-items: center;
14459
+ gap: 12px;
14460
+ width: 100%;
14461
+ }
14462
+ .md3 .cgz-logo {
14463
+ width: 138.6px;
14464
+ height: 44px;
14465
+ }
14466
+ .md3 .cgz-badge {
14467
+ display: flex;
14468
+ align-items: center;
14469
+ gap: 8px;
14470
+ padding: 6px 13px;
14471
+ background: #DFF3E5;
14472
+ border-radius: 999px;
14473
+ font-family: Arimo, Arial, sans-serif;
14474
+ font-weight: 700;
14475
+ font-size: 13.5px;
14476
+ line-height: 1.35;
14477
+ color: #0F6B36;
14478
+ white-space: nowrap;
14479
+ }
14480
+ /* Figma specifies "42dot Sans" for this headline — not a typeface loaded anywhere in this
14481
+ project (confirmed: no other component/font-face references it). Mapped to the design
14482
+ system's own existing "big display heading" token instead (same one .co-step-title/hero
14483
+ numbers use) rather than adding a whole new webfont for one screen. 500 is the lightest
14484
+ weight actually loaded for it (Schibsted+Grotesk:wght@500;600;700;800, see :root) — closest
14485
+ available match to Figma's "Regular". */
14486
+ .md3 .cgz-title {
14487
+ margin: 0;
14488
+ font-family: var(--md-font-display, var(--md-font));
14489
+ font-weight: 500;
14490
+ font-size: 32px;
14491
+ line-height: 1.35;
14492
+ color: var(--md-on-surface);
14493
+ text-align: center;
14494
+ }
14495
+ .md3 .cgz-desc {
14496
+ margin: 0;
14497
+ max-width: 560px;
14498
+ font-family: Arimo, Arial, sans-serif;
14499
+ font-weight: 400;
14500
+ font-size: 16px;
14501
+ line-height: 1.35;
14502
+ color: var(--md-on-surface-variant);
14503
+ text-align: center;
14504
+ }
14505
+ .md3 .cgz-car {
14506
+ width: 420px;
14507
+ height: 275px;
14508
+ object-fit: contain;
14509
+ }
14510
+ .md3 .cgz-steps {
14511
+ display: flex;
14512
+ flex-direction: row;
14513
+ gap: 24px;
14514
+ width: 100%;
14515
+ padding: 20px 0;
14516
+ border-top: 1px solid var(--md-outline-variant);
14517
+ border-bottom: 1px solid var(--md-outline-variant);
14518
+ box-sizing: border-box;
14519
+ }
14520
+ .md3 .cgz-step {
14521
+ display: flex;
14522
+ flex: 1 0 0;
14523
+ min-width: 0;
14524
+ align-items: flex-start;
14525
+ gap: 10px;
14526
+ }
14527
+ .md3 .cgz-step-text {
14528
+ display: flex;
14529
+ flex-direction: column;
14530
+ gap: 1px;
14531
+ min-width: 0;
14532
+ }
14533
+ .md3 .cgz-step-title {
14534
+ margin: 0;
14535
+ font-family: Arimo, Arial, sans-serif;
14536
+ font-weight: 700;
14537
+ font-size: 14px;
14538
+ line-height: 1.35;
14539
+ color: var(--md-on-surface);
14540
+ }
14541
+ .md3 .cgz-step-sub {
14542
+ margin: 0;
14543
+ font-family: Arimo, Arial, sans-serif;
14544
+ font-weight: 400;
14545
+ font-size: 13px;
14546
+ line-height: 1.35;
14547
+ color: var(--md-on-surface-variant);
14548
+ }
14549
+ .md3 .cgz-btn {
14550
+ width: 320px;
14551
+ height: 48px;
14552
+ padding: 0 16px;
14553
+ gap: 8px;
14554
+ background: #005cff;
14555
+ border-radius: 16px;
14556
+ font-family: Arial;
14557
+ font-weight: 700;
14558
+ font-size: 15px;
14559
+ }
14560
+ .md3 .cgz-btn:hover {
14561
+ box-shadow: var(--md-e2);
14562
+ background: var(--autode-blau-strong);
14563
+ }
14564
+ @media (hover: none) {
14565
+ .md3 .cgz-btn:hover {
14566
+ box-shadow: none;
14567
+ background: #005cff;
14568
+ }
14569
+ }
14570
+ @media (max-width: 600px) {
14571
+ .md3 .cgz-modal {
14572
+ width: 100%;
14573
+ max-width: 100%;
14574
+ }
14575
+ .md3 .cgz-card {
14576
+ width: 100%;
14577
+ max-width: 100%;
14578
+ gap: 16px;
14579
+ padding: 24px 20px 26px;
14580
+ }
14581
+ .md3 .cgz-title {
14582
+ font-size: 26px;
14583
+ }
14584
+ .md3 .cgz-desc {
14585
+ max-width: none;
14586
+ font-family: Arial;
14587
+ font-size: 15px;
14588
+ }
14589
+ .md3 .cgz-car {
14590
+ width: 260px;
14591
+ height: 170px;
14592
+ }
14593
+ .md3 .cgz-steps {
14594
+ flex-direction: column;
14595
+ gap: 12px;
14596
+ padding: 16px 0;
14597
+ }
14598
+ .md3 .cgz-step-title,
14599
+ .md3 .cgz-step-sub {
14600
+ font-family: Arial;
14601
+ }
14602
+ .md3 .cgz-btn {
14603
+ width: 100%;
14604
+ }
14605
+ }
14606
+
14607
+ /* Escapes MainContent.styl's `container()` mixin (max-width: 1100px, centered) — App.tsx wraps
14608
+ every non-SPA-layout route (checkout included) in <MainContent>, which defaults to that
14609
+ 1100px cap regardless of what the routed page itself renders. CheckoutPage.tsx already
14610
+ escapes it the CSS-module way (its own mainContentWrap, max-width:100%); this is the same
14611
+ fix as a plain literal class for CheckoutThankYouPage.tsx's <Page mainContentClass="..."> —
14612
+ consistent with cgz-modal above (a plain class, not a CSS-module one, so it's reliably
14613
+ targetable here in md3.css, a completely different bundle/file). */
14614
+ .md3 .cgz-main-content {
14615
+ max-width: 100%;
14616
+ min-height: 100%;
14617
+ height: auto;
14618
+ }
14619
+
14620
+ /* Checkout step 6 "Danke" page chrome (CheckoutThankYouPage.tsx) — Figma 64637:37033's own
14621
+ "Checkout Header/Footer Desktop", deliberately simpler than CheckoutTopBarNext/
14622
+ CheckoutHeaderNext (no car price/back/stepper — the order's already placed by the time this
14623
+ renders). `.cgz-card` etc. above are shared with the CONGRATULATIONS_CHECKOUT_MODAL path and
14624
+ need no changes for this context. */
14625
+ .md3 .cgz-page {
14626
+ display: flex;
14627
+ flex-direction: column;
14628
+ min-height: 100vh;
14629
+ }
14630
+ .md3 .cgz-page-header {
14631
+ display: flex;
14632
+ align-items: center;
14633
+ flex: none;
14634
+ height: 64px;
14635
+ padding: 0 120px;
14636
+ background: #FFFFFF;
14637
+ border-bottom: 1px solid var(--md-outline-variant);
14638
+ }
14639
+ .md3 .cgz-page-logo {
14640
+ display: block;
14641
+ width: 81.9px;
14642
+ height: 26px;
14643
+ }
14644
+ .md3 .cgz-page-main {
14645
+ display: flex;
14646
+ flex-direction: column;
14647
+ align-items: center;
14648
+ flex: 1 0 auto;
14649
+ padding: 56px 24px 80px;
14650
+ background: #F4F5FA;
14651
+ }
14652
+ @media (max-width: 600px) {
14653
+ .md3 .cgz-page-header {
14654
+ height: 56px;
14655
+ padding: 0 23px;
14656
+ }
14657
+ .md3 .cgz-page-main {
14658
+ padding: 24px 23px 40px;
14659
+ }
14660
+ }
@@ -36,10 +36,14 @@ export interface FormCheckboxProps {
36
36
  /** override the field's own disabled flag. */
37
37
  disabled?: boolean;
38
38
  id?: string;
39
+ /** show a small alert-circle glyph before the error text (Figma 64670:106293's "Fehlermeldung"
40
+ * row) — opt-in, defaults off so every other FormCheckbox caller keeps its plain text-only
41
+ * error message unchanged. */
42
+ errorIcon?: boolean;
39
43
  }
40
44
 
41
45
  const FormCheckbox: React.FunctionComponent<FormCheckboxProps> = observer(
42
- ({ field, t, label, children, disabled, id }) => {
46
+ ({ field, t, label, children, disabled, id, errorIcon }) => {
43
47
  const isChecked = !!field.checked;
44
48
  const isDisabled = disabled != null ? disabled : !!field.disabled;
45
49
  // field.hasError alone is a LIVE check (mobx-react-form's Field#hasError getter re-evaluates
@@ -77,7 +81,16 @@ const FormCheckbox: React.FunctionComponent<FormCheckboxProps> = observer(
77
81
  </span>
78
82
  {content != null ? <span className="mcheck-label">{content}</span> : null}
79
83
  </label>
80
- {hasError ? <span className="mcheck-error-text">{field.error}</span> : null}
84
+ {hasError ? (
85
+ errorIcon ? (
86
+ <span className="mcheck-error-text mcheck-error-text--icon">
87
+ <Icon name="alert-circle" size={16} stroke={2} cls="mcheck-error-icon" />
88
+ <span className="mcheck-error-msg">{field.error}</span>
89
+ </span>
90
+ ) : (
91
+ <span className="mcheck-error-text">{field.error}</span>
92
+ )
93
+ ) : null}
81
94
  </div>
82
95
  );
83
96
  },
package/ui/Icon.tsx CHANGED
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import {
3
3
  Activity,
4
+ AlertCircle,
4
5
  Apple,
5
6
  Armchair,
6
7
  ArrowLeft,
@@ -57,6 +58,7 @@ import {
57
58
  Facebook,
58
59
  File,
59
60
  FileCheck,
61
+ FileSignature,
60
62
  FileText,
61
63
  Flame,
62
64
  Fuel,
@@ -199,6 +201,7 @@ function toPascalCase(name: string): string {
199
201
  */
200
202
  const REGISTRY: Record<string, React.ComponentType<LucideProps>> = {
201
203
  Activity,
204
+ AlertCircle,
202
205
  Apple,
203
206
  Armchair,
204
207
  ArrowLeft,
@@ -256,6 +259,7 @@ const REGISTRY: Record<string, React.ComponentType<LucideProps>> = {
256
259
  Facebook,
257
260
  File,
258
261
  FileCheck,
262
+ FileSignature,
259
263
  FileText,
260
264
  Flame,
261
265
  Fuel,
@@ -2126,6 +2126,12 @@ export interface FinanceDetailsModalProps {
2126
2126
  /** opened directly via the page's Finanzierungs-/Leasingdetails link — the sheet opens
2127
2127
  * scrolled to the breakdown instead of the "Rate anpassen" controls at its top. */
2128
2128
  initialShowBreakdown?: boolean;
2129
+ /** skips the smooth scrollIntoView/scrollSheetToBreakdown animation initialShowBreakdown
2130
+ * otherwise triggers on open (still sets showBreakdown itself) — for a consumer whose sheet
2131
+ * has nothing ABOVE the breakdown to scroll past (e.g. checkout's stripped-down instance,
2132
+ * with the rate hero/sliders/toggles above it hidden), that animation has nowhere real to go
2133
+ * and only reads as a stray jump. */
2134
+ skipInitialScroll?: boolean;
2129
2135
  /** mobile only — price-rating label, drives the eval pill/bar (same as the collapsed card). */
2130
2136
  ratingLabel?: string;
2131
2137
  /** Kaufen tab only — real carStore.isAvailable (same source as VdpTrustRow's "Sofort
@@ -2154,6 +2160,10 @@ export interface FinanceDetailsModalProps {
2154
2160
  leasingAvailable?: boolean;
2155
2161
  /** i18n function (drilled from the page). */
2156
2162
  t: TranslationFunction;
2163
+ /** overrides the desktop dialog heading (default: the mode's own, shorter "Details" text) —
2164
+ * a consumer whose own trigger already carries a specific label (e.g. checkout's cf-details
2165
+ * button, "Finanzierungsdetails"/"Leasingdetails") can pass that same text through here. */
2166
+ titleOverride?: string;
2157
2167
  }
2158
2168
 
2159
2169
  /**
@@ -2197,6 +2207,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2197
2207
  onAnnualMileageChange,
2198
2208
  onClose,
2199
2209
  initialShowBreakdown,
2210
+ skipInitialScroll,
2200
2211
  ratingLabel,
2201
2212
  available,
2202
2213
  onModeChange,
@@ -2211,6 +2222,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2211
2222
  onToggleAddServices,
2212
2223
  leasingAvailable = true,
2213
2224
  t,
2225
+ titleOverride,
2214
2226
  }) => {
2215
2227
  // same "UVP-Rate"/list-price footnote badge + popover as FinanceCalculator's own hero — this
2216
2228
  // modal's mobile "Rate anpassen" sheet renders its own separate copy of that hero row, so it
@@ -2314,14 +2326,14 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2314
2326
  React.useEffect(() => {
2315
2327
  if (!open) return;
2316
2328
  setShowBreakdown(!!initialShowBreakdown);
2317
- if (!initialShowBreakdown) return;
2329
+ if (!initialShowBreakdown || skipInitialScroll) return;
2318
2330
  const timer = window.setTimeout(() => {
2319
2331
  if (mobileBreakdownRef.current?.offsetParent) scrollSheetToBreakdown();
2320
2332
  else desktopBreakdownRef.current?.scrollIntoView({ behavior: 'smooth', block: 'start' });
2321
2333
  }, 50);
2322
2334
  return () => window.clearTimeout(timer);
2323
2335
  // eslint-disable-next-line react-hooks/exhaustive-deps
2324
- }, [open, initialShowBreakdown]);
2336
+ }, [open, initialShowBreakdown, skipInitialScroll]);
2325
2337
  // in-sheet details link: expanding scrolls down to the breakdown, collapsing back to the top.
2326
2338
  const revealBreakdownRef = React.useRef(false);
2327
2339
  React.useEffect(() => {
@@ -2389,7 +2401,7 @@ export const FinanceDetailsModal: React.FunctionComponent<FinanceDetailsModalPro
2389
2401
  if (!open) return null;
2390
2402
  const isLease = mode === 'leasing';
2391
2403
  const isCash = mode === 'cash';
2392
- const title = isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails');
2404
+ const title = titleOverride || (isLease ? t('autobahn:modal.leasingDetails') : t('autobahn:modal.financingDetails'));
2393
2405
  const mobileTitle = isCash
2394
2406
  ? t('autobahn:modal.sheetTitleCash')
2395
2407
  : isLease