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

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.154",
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
@@ -1491,6 +1491,10 @@ line-height: 31px;
1491
1491
  line-height: 15px;
1492
1492
  color: #44464F;
1493
1493
  margin-bottom: 2px;
1494
+ /* long submodels (e.g. "C4 1.2 Plus/Rückfahrkamera/Sitzheizung/Tempomat") use "/" instead of
1495
+ spaces between items, so the browser's default word-wrap sees one unbreakable "word" and
1496
+ overflows past the 296px aside instead of wrapping (ADV-238). */
1497
+ overflow-wrap: break-word;
1494
1498
  }
1495
1499
  .md3 .co-aside-finance {
1496
1500
  font-family: 'Arial';
@@ -2290,6 +2294,14 @@ line-height: 31px;
2290
2294
  font-size: 13px;
2291
2295
  line-height: 15px;
2292
2296
  color: #44464F;
2297
+ /* a long .co-error message (e.g. "Bitte wählen Sie eine der Optionen für Garantiepaket und
2298
+ Fahrzeugzulassung.") sharing this row (≥601px: same row via margin-right:auto/flex-end,
2299
+ see .co-next's own media queries) squeezed this plain text span for space same as any
2300
+ other flex child — with nothing stopping it from wrapping, "Schritt 4 von 5" split onto 2
2301
+ lines instead of the long error text (the one thing actually meant to wrap) absorbing the
2302
+ squeeze. nowrap + flex-shrink:0 keeps this at its natural one-line width unconditionally. */
2303
+ white-space: nowrap;
2304
+ flex-shrink: 0;
2293
2305
  }
2294
2306
  /* "Weiter" — its own fixed-size look, not the shared .mbtn--lg sizing (icon already dropped
2295
2307
  in the JSX, not just hidden here). Text styling lands on the button itself: Button.tsx
@@ -2356,6 +2368,22 @@ line-height: 31px;
2356
2368
  background: #F4F5FA;
2357
2369
  border-top: 0;
2358
2370
  }
2371
+ /* DOM order is .co-error (only rendered when a step's validation fails), .co-back-btn,
2372
+ .co-step-count, Weiter. `order` visually moves Zurück before the error text (its DOM
2373
+ position doesn't change, so the back button's click handler/focus order are unaffected).
2374
+ Zurück's base margin-right:auto (line ~2265) is what pushes Schritt/Weiter to the row's
2375
+ right edge in the common case (no error, .co-back-btn is the only thing before the gap) —
2376
+ kept as-is here. Only when .co-error IS present does that job need to hand off to it
2377
+ instead (so the error text sits right after Zurück rather than the auto-margin gap opening
2378
+ up BEFORE the error, between it and Zurück): the `.co-error + .co-back-btn` sibling
2379
+ selector matches only that case (error is Zurück's immediately preceding DOM sibling) and
2380
+ zeroes Zurück's margin, leaving .co-error's own margin-right:auto below as the sole push. */
2381
+ .md3 .co-next .co-back-btn {
2382
+ order: -1;
2383
+ }
2384
+ .md3 .co-next .co-error + .co-back-btn {
2385
+ margin-right: 0;
2386
+ }
2359
2387
  .md3 .co-next .co-error {
2360
2388
  margin-right: auto;
2361
2389
  }
@@ -2394,11 +2422,26 @@ line-height: 31px;
2394
2422
  .md3 .co-form.co-form--payment-details {
2395
2423
  border-bottom: 0;
2396
2424
  }
2425
+ /* order-overview step only — same reasoning, see OrderOverviewNext.tsx's "co-form oo-page". */
2426
+ .md3 .co-form.oo-page {
2427
+ border-bottom: 0;
2428
+ }
2397
2429
  .md3 .co-step-title {
2398
2430
  font: 700 22px var(--md-font-display, var(--md-font));
2399
2431
  margin: 0;
2400
2432
  color: var(--md-on-surface);
2401
2433
  }
2434
+ /* order-overview only ("Bestellübersicht") — desktop already shows this same title in
2435
+ CheckoutTopBarNext's own .co-topbar-heading (CheckoutChromeNext.tsx, same steps[activeStep].
2436
+ title source), so this becomes a duplicate there; kept for mobile, where that top bar is a
2437
+ compact single row with no room for it (CheckoutMobilePageTitle covers OTHER steps' mobile
2438
+ copy, but order-overview renders its own here instead). Same 600px mobile/desktop cutoff as
2439
+ .co-back-btn etc. elsewhere on this page. */
2440
+ @media (min-width: 601px) {
2441
+ .md3 .oo-page > .co-step-title {
2442
+ display: none;
2443
+ }
2444
+ }
2402
2445
  .md3 .co-step-sub {
2403
2446
  color: var(--md-on-surface-variant);
2404
2447
  font-size: 14px;
@@ -2884,6 +2927,13 @@ line-height: 31px;
2884
2927
  height: 48px;
2885
2928
  padding: 0 40px 0 10px;
2886
2929
  }
2930
+ /* ADV-238: mandatory-but-empty Geburtsdatum must show the same red border as the other
2931
+ required fields. Same specificity as the rule above (two classes) so it only wins because
2932
+ it comes later in source order — .co-personal-grid .mfield--error can't reach in here, its
2933
+ border gets overridden by .co-birthdate .mfield right above for the same reason. */
2934
+ .md3 .co-birthdate .mfield--error {
2935
+ border: 1px solid #BA1A1A;
2936
+ }
2887
2937
  /* on-the-border notch, matching .mfield-label--notch exactly (same mechanic as Vorwahl/
2888
2938
  Telefonnummer's own label) instead of the earlier stacked-inside-the-box layout. */
2889
2939
  .md3 .co-birthdate .mfield-label {
@@ -3051,6 +3101,25 @@ line-height: 31px;
3051
3101
  .md3 .co-borrower-relationship .mselect-btn {
3052
3102
  position: relative;
3053
3103
  }
3104
+ /* ADV-240 QA (comment 35891): mandatory-but-empty red border — Dropdown has no built-in visual
3105
+ error state, so this keys off the `is-error` wrapper class PaymentDetailsNext.tsx adds. Three
3106
+ classes deep so it outranks both the base .mselect-btn idle box-shadow and this file's own
3107
+ .co-borrower-relationship .mselect-btn above without needing !important. */
3108
+ .md3 .co-borrower-relationship.is-error .mselect-btn {
3109
+ box-shadow: inset 0 0 0 1px #BA1A1A;
3110
+ }
3111
+ /* same chevron glyph as the ::after above, just stroke='%23BA1A1A' (red) instead of '%23005CFF'. */
3112
+ .md3 .co-borrower-relationship.is-error .mselect-btn::after {
3113
+ background-image: url("data:image/svg+xml,%3Csvg%20width='16'%20height='16'%20viewBox='0%200%2016%2016'%20fill='none'%20xmlns='http://www.w3.org/2000/svg'%3E%3Cpath%20d='M4%206L8%2010L12%206'%20stroke='%23BA1A1A'%20stroke-width='2'%20stroke-linecap='round'%20stroke-linejoin='round'/%3E%3C/svg%3E");
3114
+ }
3115
+ /* .co-borrower-relationship .mselect-btn:hover below is the SAME specificity (3 classes) as the
3116
+ .is-error border rule above and comes later in the file, so hovering an errored field was
3117
+ winning the box-shadow back to idle gray. One more class (:hover added to .is-error's own
3118
+ selector) outranks it regardless of source order — same fix shape as the co-birthdate
3119
+ :focus-within override above. */
3120
+ .md3 .co-borrower-relationship.is-error .mselect-btn:hover {
3121
+ box-shadow: inset 0 0 0 1px #BA1A1A;
3122
+ }
3054
3123
  .md3 .co-borrower-relationship .mselect-btn:hover {
3055
3124
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
3056
3125
  }
@@ -3249,8 +3318,12 @@ line-height: 31px;
3249
3318
  .md3 .co-pay-bene b {
3250
3319
  display: block;
3251
3320
  margin-bottom: 2px;
3252
- font-size: 13.5px;
3253
- color: var(--md-on-surface);
3321
+ font-family: 'Arimo';
3322
+ font-style: normal;
3323
+ font-weight: 700;
3324
+ font-size: 15px;
3325
+ line-height: 140%;
3326
+ color: #1A1B22;
3254
3327
  }
3255
3328
  .md3 .co-pay-bene-desc {
3256
3329
  font-family: 'Arimo';
@@ -3515,6 +3588,35 @@ line-height: 31px;
3515
3588
  color: #1A1B22;
3516
3589
  background: #FFFFFF;
3517
3590
  }
3591
+ /* ADV-249 QA: Lieferadresse's "Abweichende Lieferadresse" form had no validation at all — this
3592
+ is its red error state, same red as .co-error (--md-error). Each field is now wrapped in
3593
+ .co-addr-form-item (the actual grid cell) so a per-field hint can sit directly under its own
3594
+ input instead of one generic message for the whole form. */
3595
+ .md3 .co-addr-form-item {
3596
+ display: flex;
3597
+ flex-direction: column;
3598
+ gap: 4px;
3599
+ min-width: 0;
3600
+ }
3601
+ .md3 .co-addr-form-field.is-error {
3602
+ border-color: var(--md-error);
3603
+ }
3604
+ /* native focus outline stacked on top of the red border looked like two rings at once
3605
+ (browser default blue + our red) — same inset-ring swap as .mtextarea:focus, red instead of
3606
+ blue while the field is still invalid. */
3607
+ .md3 .co-addr-form-field:focus {
3608
+ outline: 0;
3609
+ box-shadow: inset 0 0 0 2px #005CFF;
3610
+ }
3611
+ .md3 .co-addr-form-field.is-error:focus {
3612
+ box-shadow: inset 0 0 0 2px var(--md-error);
3613
+ }
3614
+ .md3 .co-addr-field-hint {
3615
+ color: var(--md-error);
3616
+ font-family: 'Arimo';
3617
+ font-size: 12px;
3618
+ line-height: 14px;
3619
+ }
3518
3620
  .md3 .co-addr-form-actions {
3519
3621
  grid-column: 1 / -1;
3520
3622
  display: flex;
@@ -4474,10 +4576,17 @@ line-height: 31px;
4474
4576
  }
4475
4577
  /* "4. Lieferung & Services" (Figma node 63570:28320) — up to 3 cells in a row, divided by a 1px
4476
4578
  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. */
4579
+ node, so it stays correct no matter which 1-3 of the 3 cells are conditionally rendered.
4580
+ align-items: stretch (not flex-start) — a border-left only ever reaches as far down as its own
4581
+ box, so with flex-start each divider stopped at its OWN cell's content height instead of the
4582
+ row's tallest cell (the 3-line-wrapping "9. Gewährleistung & Herstellergarantie" title makes
4583
+ that cell the tallest whenever it's shown), leaving the shorter cells' dividers visibly cut off
4584
+ partway down. Cells stretching to the row's full height doesn't shift their own icon+text
4585
+ (each cell keeps its own align-items: flex-start below), it only extends the empty space (and
4586
+ therefore the border) to match. */
4478
4587
  .md3 .oo-ls-row {
4479
4588
  display: flex;
4480
- align-items: flex-start;
4589
+ align-items: stretch;
4481
4590
  }
4482
4591
  .md3 .oo-ls-cell {
4483
4592
  display: flex;
@@ -4801,11 +4910,6 @@ line-height: 31px;
4801
4910
  color: var(--md-on-surface-variant);
4802
4911
  cursor: pointer;
4803
4912
  }
4804
- .md3 .oo-underline {
4805
- color: var(--md-primary);
4806
- text-decoration: underline;
4807
- cursor: pointer;
4808
- }
4809
4913
  /* "1. Fahrzeug & Verkäufer" card (Figma node 63570:28262) — own full-width .co-card, sitting
4810
4914
  above .oo-grid (not a grid column), so no bottom divider/margin like the legacy .oo-car row
4811
4915
  (that one used to sit above other rows sharing the same card). */
@@ -4948,16 +5052,94 @@ line-height: 31px;
4948
5052
  font-size: 13px;
4949
5053
  cursor: pointer;
4950
5054
  }
4951
- .md3 .oo-terms {
4952
- margin-top: 20px;
5055
+ /* Figma 63570:28475 ("CK / Content — Rechtliche Hinweise") — text/underline color is the SAME
5056
+ muted gray as the body copy (var(--md-on-surface-variant) === Figma's var(--text/muted,
5057
+ #44464f) fallback exactly), not the design system's usual blue link color (.oo-underline,
5058
+ used elsewhere), so links here are styled with underline only. No border per explicit
5059
+ request. Sits below .oo-next (order-overview's own bottom bar) — a spot this exact node
5060
+ isn't shown at in Figma (its own component description has it above the buy button instead),
5061
+ so this margin-top is this page's own established gap rhythm, not a value read off the design. */
5062
+ .md3 .oo-legal-notice {
5063
+ margin-top: 16px;
5064
+ font-family: Arial;
4953
5065
  font-size: 13px;
5066
+ line-height: 1.5;
4954
5067
  color: var(--md-on-surface-variant);
4955
- line-height: 1.6;
4956
5068
  }
4957
- .md3 .oo-buy {
5069
+ .md3 .oo-legal-link {
5070
+ text-decoration: underline;
5071
+ color: inherit;
5072
+ cursor: pointer;
5073
+ }
5074
+ /* Order overview's own "Zurück / Schritt X von Y / Jetzt kaufen" action bar (Figma 63502:5801) —
5075
+ distinct classes from .co-next/.co-back-btn/.co-step-count (CheckoutChromeNext.tsx) rather than
5076
+ reusing them: that bar's CTA is "advance to next step" and returns null on the last step, this
5077
+ one's CTA is "buy now" (OfferRequestButtonWrapper) with its own, larger Figma-specified sizing. */
5078
+ .md3 .oo-next {
4958
5079
  display: flex;
4959
- justify-content: flex-end;
4960
- margin-top: 16px;
5080
+ align-items: center;
5081
+ gap: 16px;
5082
+ margin-top: 24px;
5083
+ padding-top: 18px;
5084
+ border-top: 1px solid #C5C6D2;
5085
+ }
5086
+ .md3 .oo-next-back {
5087
+ display: flex;
5088
+ align-items: center;
5089
+ justify-content: center;
5090
+ height: 48px;
5091
+ padding: 0 16px;
5092
+ margin-right: auto;
5093
+ border: 0;
5094
+ background: none;
5095
+ border-radius: 16px;
5096
+ font-family: 'Arial';
5097
+ font-style: normal;
5098
+ font-weight: 700;
5099
+ font-size: 15px;
5100
+ line-height: normal;
5101
+ color: #005CFF;
5102
+ cursor: pointer;
5103
+ }
5104
+ .md3 .oo-next-count {
5105
+ font-family: 'Arial';
5106
+ font-style: normal;
5107
+ font-weight: 400;
5108
+ font-size: 13px;
5109
+ line-height: normal;
5110
+ color: #44464F;
5111
+ white-space: nowrap;
5112
+ }
5113
+ .md3 .oo-buy-btn {
5114
+ display: flex;
5115
+ flex-direction: row;
5116
+ justify-content: center;
5117
+ align-items: center;
5118
+ padding: 0 16px;
5119
+ gap: 8px;
5120
+ width: 340px;
5121
+ height: 60px;
5122
+ background: #005CFF;
5123
+ border-radius: 16px;
5124
+ font-family: 'Arial';
5125
+ font-style: normal;
5126
+ font-weight: 700;
5127
+ font-size: 17px;
5128
+ line-height: normal;
5129
+ color: #FFFFFF;
5130
+ }
5131
+ @media (max-width: 700px) {
5132
+ .md3 .oo-next-count {
5133
+ display: none;
5134
+ }
5135
+ }
5136
+ @media (max-width: 600px) {
5137
+ .md3 .oo-next-back {
5138
+ display: none;
5139
+ }
5140
+ .md3 .oo-buy-btn {
5141
+ width: 100%;
5142
+ }
4961
5143
  }
4962
5144
  @media (max-width: 800px) {
4963
5145
  .md3 .oo-grid {
@@ -5124,12 +5306,33 @@ line-height: 31px;
5124
5306
  line instead of sitting visibly lower (more obvious once the label wraps to 2 lines). */
5125
5307
  margin-top: 7px;
5126
5308
  }
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;
5309
+ /* Error state (Figma 64670:106293, "Jetzt kaufen" clicked without confirming both agreements) —
5310
+ unconfirmed checkbox gets a thicker red box, plus an icon+message row underneath, indented
5311
+ 32px (20px box + 12px gap) to sit under the label rather than the box. errorIcon (FormCheckbox
5312
+ prop, opt-in) renders that icon; every other FormCheckbox caller sitewide is untouched. */
5313
+ .md3 .oo-terms-block .mcheck-group--error .mcheck-input:not(:checked) + .mcheck-box {
5314
+ border-width: 2px;
5315
+ }
5316
+ .md3 .oo-terms-block .mcheck-error-text--icon {
5317
+ display: flex;
5318
+ align-items: flex-start;
5319
+ gap: 6px;
5320
+ padding-left: 32px;
5321
+ margin-top: -2px;
5322
+ }
5323
+ .md3 .oo-terms-block .mcheck-error-icon {
5324
+ flex: none;
5325
+ margin-top: 1px;
5326
+ }
5327
+ .md3 .oo-terms-block .mcheck-error-msg {
5328
+ flex: 1 0 0;
5329
+ font-size: 13px;
5330
+ line-height: 1.35;
5331
+ }
5332
+ /* Card itself gets a red border once either checkbox is showing its error (OrderOverviewNext's
5333
+ termsHasError — same hasError+error double-check as FormCheckbox uses internally). */
5334
+ .md3 .oo-terms-card--error {
5335
+ border: 2px solid var(--md-error);
5133
5336
  }
5134
5337
  /* "7. Ergänzende Regelungen" dialogs (Verkürzung der Verjährungsfrist / Ausschluss der
5135
5338
  Aktualisierung, Figma 64596:36289 desktop / 64596:36264 mobile) — rendered via the shared
@@ -5186,6 +5389,60 @@ line-height: 31px;
5186
5389
  line-height: 1.5;
5187
5390
  color: #0E2238;
5188
5391
  }
5392
+ /* Legal-notice row's 3 dialogs (AGB von auto.de / AGB des Verkäufers / Widerrufsbelehrung —
5393
+ Figma 64604:48257 / 63583:6830 / 63583:6850) reuse the SAME oo-terms-modal chrome above, but
5394
+ these bodies are long reference documents rather than a single clause, so Figma specs them
5395
+ smaller/muted (text/muted #44464F) with bold text/strong (#1A1B22) section headings instead of
5396
+ .oo-terms-modal-text's bigger, darker single-paragraph style. oo-terms-modal-legal-text also
5397
+ carries the .oo-terms-modal-legal-html "AGB von auto.de"/"AGB des Verkäufers" bodies' shared
5398
+ base (dangerouslySetInnerHTML'd CMS content / seller.termCond plain text — both re-used
5399
+ verbatim from elsewhere in the app, not authored here, see OrderOverviewNext.tsx). */
5400
+ .md3 .oo-terms-modal-legal-text,
5401
+ .md3 .oo-terms-modal-legal-html {
5402
+ font-family: Arial;
5403
+ font-weight: 400;
5404
+ font-size: 13px;
5405
+ line-height: 1.48;
5406
+ color: #44464F;
5407
+ }
5408
+ .md3 .oo-terms-modal-legal-text {
5409
+ margin: 0;
5410
+ /* seller.termCond is plain text (Imprint.tsx renders it the same way) — preserves its literal
5411
+ line breaks instead of collapsing them. No-op for the revocation dialog's own JSON strings,
5412
+ which don't contain any. */
5413
+ white-space: pre-line;
5414
+ }
5415
+ .md3 .oo-terms-modal-legal-heading {
5416
+ margin: 0;
5417
+ font-family: Arial;
5418
+ font-weight: 700;
5419
+ font-size: 15px;
5420
+ line-height: 1.48;
5421
+ color: #1A1B22;
5422
+ }
5423
+ .md3 .oo-terms-modal-legal-body {
5424
+ display: flex;
5425
+ flex-direction: column;
5426
+ gap: 12px;
5427
+ }
5428
+ /* ourTermsAndConditions is a WordPress `content.rendered` HTML blob (CheckoutStore.ts) — its own
5429
+ <p> tags need their own spacing/heading treatment since it isn't discrete React children like
5430
+ oo-terms-modal-legal-body's blocks above. */
5431
+ .md3 .oo-terms-modal-legal-html p {
5432
+ margin: 0 0 12px;
5433
+ }
5434
+ .md3 .oo-terms-modal-legal-html p:last-child {
5435
+ margin-bottom: 0;
5436
+ }
5437
+ .md3 .oo-terms-modal-legal-html strong,
5438
+ .md3 .oo-terms-modal-legal-html b,
5439
+ .md3 .oo-terms-modal-legal-html h1,
5440
+ .md3 .oo-terms-modal-legal-html h2,
5441
+ .md3 .oo-terms-modal-legal-html h3 {
5442
+ font-weight: 700;
5443
+ font-size: 15px;
5444
+ color: #1A1B22;
5445
+ }
5189
5446
  /* mobile bottom sheet (.mmodal's own @media max-width:560px already makes it full-width/
5190
5447
  bottom-pinned/top-rounded) — Figma's mobile header additionally wants a 24px title (vs 22px
5191
5448
  on desktop) and a decorative grip bar (Figma 64596:36264's handleRow), which this Modal
@@ -5256,15 +5513,16 @@ line-height: 31px;
5256
5513
  .md3 .oo-promo-body .mfield:focus-within {
5257
5514
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
5258
5515
  }
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). */
5516
+ /* Cards 9 "Gewährleistung" / 10 "Herstellergarantie" (ADV-295; Figma 64669:106647 /
5517
+ 64669:108591) — share one .oo-warranty-card/.oo-warranty-body treatment (same card chrome,
5518
+ flat single-level collapse, same link/image styling); which fields each renders is decided in
5519
+ the JSX, not here. Both graphic blocks (the EU statutory-warranty notice, the GARAN
5520
+ certificate) are static images, not hand-coded — see the long comment above WARRANTY_NOTICE_IMG
5521
+ in OrderOverviewNext.tsx for why (Figma ships them fully vector-flattened). */
5265
5522
  /* 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. */
5523
+ stray gap under the title when the card is collapsed; .oo-warranty-body's own margin-top
5524
+ restores that 16px only once actually expanded (both cards default expanded, but the user can
5525
+ still collapse them). */
5268
5526
  .md3 .oo-warranty-card .co-card-title {
5269
5527
  margin-bottom: 0;
5270
5528
  }
@@ -5272,6 +5530,7 @@ line-height: 31px;
5272
5530
  margin-top: 16px;
5273
5531
  display: flex;
5274
5532
  flex-direction: column;
5533
+ gap: 12px;
5275
5534
  }
5276
5535
  .md3 .oo-warranty-notice-img {
5277
5536
  display: block;
@@ -5279,36 +5538,6 @@ line-height: 31px;
5279
5538
  max-width: 400px;
5280
5539
  height: auto;
5281
5540
  }
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
5541
  .md3 .oo-warranty-garan-img {
5313
5542
  display: block;
5314
5543
  width: 100%;
@@ -5783,11 +6012,20 @@ a.oo-warranty-link--icon:hover {
5783
6012
  @media (min-width: 601px) and (max-width: 993px) {
5784
6013
  .md3 .co-next {
5785
6014
  flex-direction: row;
5786
- flex-wrap: nowrap;
6015
+ flex-wrap: wrap;
5787
6016
  align-items: center;
5788
6017
  justify-content: flex-end;
5789
6018
  gap: 16px;
5790
6019
  }
6020
+ /* same reasoning as the ≤700px block above: .co-error (a step-validation message, can run
6021
+ long — e.g. "Bitte wählen Sie eine der Optionen für Garantiepaket und Fahrzeugzulassung.")
6022
+ must not crowd onto the Zurück/Schritt/Weiter row. Without this, flex-wrap: nowrap forced
6023
+ all 4 children onto one line with no room, so the error visually overlapped Zurück and
6024
+ squeezed "Schritt X von Y" into wrapping onto 2 lines. flex-basis:100% forces the error onto
6025
+ its own first line; flex-wrap: wrap (above) then lays the 3 buttons/label out on the next. */
6026
+ .md3 .co-next .co-error {
6027
+ flex: 1 0 100%;
6028
+ }
5791
6029
  }
5792
6030
  /* below the 992px desktop breakpoint the content no longer reserves the 424px left
5793
6031
  gap, so the fixed rail would overlap the form — hide it (mobile uses the header stepper). */
@@ -6240,7 +6478,8 @@ a.oo-warranty-link--icon:hover {
6240
6478
  .md3 .enq-float input:focus-visible,
6241
6479
  .md3 .enq-float textarea:focus-visible,
6242
6480
  .md3 .enquiry-name-row input:focus-visible,
6243
- .md3 .enquiry-email-wrap input:focus-visible {
6481
+ .md3 .enquiry-email-wrap input:focus-visible,
6482
+ .md3 .co-addr-form-field:focus-visible {
6244
6483
  box-shadow: none !important;
6245
6484
  }
6246
6485
  .md3 .link-accent {
@@ -9299,6 +9538,64 @@ a.oo-warranty-link--icon:hover {
9299
9538
  padding: 18px 20px 22px;
9300
9539
  overflow-y: auto;
9301
9540
  }
9541
+ /* Checkout's Finanzierungsdetails/Leasingdetails modal (CheckoutFinancePanelNext's cf-details
9542
+ button) reuses this SAME FinanceDetailsModal as the VDP page, but the rate hero + price-rating
9543
+ pill/bar (.fin-price-grid) would just repeat the SAME figures the cf-panel already shows above
9544
+ this button — unlike the Anzahlung/Laufzeit sliders and Mit-Schlussrate/Rundum-Sorglos-Paket
9545
+ toggles (disabled by simply not passing their change handlers), nothing gates this block by
9546
+ prop, so it's hidden here instead. Scoped to the checkout portal wrapper only — VDP/SearchPage
9547
+ keep it. */
9548
+ .md3.cf-fin-details-modal .fin-price-grid {
9549
+ display: none;
9550
+ }
9551
+ /* Leasing's breakdown table runs longer than financing's (Leasinggeber+Vertragsart split, the
9552
+ mandatory Rundum-Sorglos-Paket notice box, an extra disclaimer line) — 88vh left it a little
9553
+ short of fitting whole, needing a few pixels of real scroll even after the hero/sliders/
9554
+ toggles/CTA above are stripped. A taller cap gives it room to fit outright on realistic
9555
+ viewports; overflow-y:auto above still catches genuinely short windows as a fallback. */
9556
+ .md3.cf-fin-details-modal .fin-modal {
9557
+ max-height: 95vh;
9558
+ }
9559
+ /* overflow-y:visible here (tried first for the short financing case) turned out to break the
9560
+ LONGER leasing case: without a real scroll container, flexbox's automatic min-height:auto
9561
+ let .fin-modal-body grow to its full content height (839px+ — leasing adds the Leasinggeber/
9562
+ Vertragsart split + the mandatory Rundum-Sorglos-Paket notice box) past .fin-modal's own
9563
+ max-height:88vh, and since .fin-modal-scrim centers it, the overflow pushed the header off the
9564
+ TOP of the viewport instead of clipping the bottom. Keep the real overflow-y:auto (correct
9565
+ height-constraining behaviour for any content length) and just hide the bar. */
9566
+ .md3.cf-fin-details-modal .fin-modal-body {
9567
+ scrollbar-width: none;
9568
+ -ms-overflow-style: none;
9569
+ }
9570
+ .md3.cf-fin-details-modal .fin-modal-body::-webkit-scrollbar {
9571
+ display: none;
9572
+ }
9573
+ /* "Rate anfragen"/call CTA row — checkout has no separate request flow to trigger from inside an
9574
+ already-running checkout (reviewing the breakdown IS the action), so this instance drops the
9575
+ row entirely rather than wiring it to a stand-in action. Unlike the sliders/toggles, it isn't
9576
+ gated by a handler prop, so it's hidden here. Shared class, one rule covers both breakpoints.
9577
+ `!important` — the element carries an inline style="display:flex" (VdpModules.tsx), which
9578
+ otherwise wins over any external rule regardless of selector specificity. */
9579
+ .md3.cf-fin-details-modal .fin-sheet-cta-row {
9580
+ display: none !important;
9581
+ }
9582
+ /* the (kept) legal breakdown's wrapper carries an inline marginTop:18 meant to separate it from
9583
+ the now-hidden hero/sliders/toggles above — stacked on .fin-modal-body's own 18px top padding
9584
+ that reads as unexplained empty space once those sections are gone. `!important` because the
9585
+ marginTop is inline (VdpModules.tsx), and only the DESKTOP wrapper needs it: it's the immediate
9586
+ next sibling of the (also hidden) .fin-price-grid there; the mobile breakdown wrapper is
9587
+ separated from .fin-price-grid by the still-visible data-sheet-fold button, so this selector
9588
+ doesn't reach it and mobile's spacing (already just marginBottom, no marginTop) is untouched. */
9589
+ .md3.cf-fin-details-modal .fin-price-grid + div {
9590
+ margin-top: 0 !important;
9591
+ }
9592
+ /* mobile breakpoint below sets align-items:flex-start on .fin-modal-head, top-aligning the close
9593
+ button against the 2-line "Kredit-Rate anpassen" + vehicle-name title — checkout's stripped
9594
+ instance has nothing that needs that extra top room, so it stays vertically centered instead
9595
+ (3-class selector already outweighs the 2-class mobile rule, no !important needed). */
9596
+ .md3.cf-fin-details-modal .fin-modal-head {
9597
+ align-items: center;
9598
+ }
9302
9599
  @media (max-width: 600px) {
9303
9600
  .md3 .fin-modal-scrim {
9304
9601
  align-items: flex-end;
@@ -14177,3 +14474,247 @@ color: #005CFF;
14177
14474
  border-bottom: 1px solid var(--md-outline-variant);
14178
14475
  }
14179
14476
  }
14477
+
14478
+ /* Post-purchase confirmation modal (CONGRATULATIONS_CHECKOUT_MODAL, Congratulations.tsx) —
14479
+ Figma 64637:37064 (desktop) / 63536:6276 (mobile). BaseModal's own `.modalCheckout` chrome
14480
+ (BaseModal.styl: fixed 375px width, square corners, its own white bg + shadow) is neutralized
14481
+ here via the `cgz-modal` className OfferRequestButtonWrapper passes — Congratulations.tsx's
14482
+ own `.cgz-card` supplies the actual Figma card (border/radius/bg/padding) instead, so there's
14483
+ no double box. `.md3` ancestor: App.tsx's root `<div className="${styles.app} md3">` wraps
14484
+ the whole SPA unconditionally (confirmed — this is NOT the same as the per-page opt-in `.md3`
14485
+ noted elsewhere for legacy MainContent pages), and BaseModal renders inline (no portal), so a
14486
+ single `.md3`-prefixed selector is enough, same as every other CheckoutPage-tree rule already
14487
+ in this file. */
14488
+ .md3 .cgz-modal {
14489
+ width: 800px;
14490
+ max-width: calc(100vw - 48px);
14491
+ background: transparent;
14492
+ border: 0;
14493
+ border-radius: 0;
14494
+ box-shadow: none;
14495
+ padding: 0;
14496
+ }
14497
+ .md3 .cgz-card {
14498
+ display: flex;
14499
+ flex-direction: column;
14500
+ align-items: center;
14501
+ gap: 32px;
14502
+ /* Figma: fixed 800px, not fluid — was width:100%, which (now that this also renders as a
14503
+ real page, not just inside the fixed-800px .cgz-modal) stretched edge-to-edge on any
14504
+ viewport wider than 800px+padding instead of staying a centered 800px card. max-width is
14505
+ just the narrow-viewport safety clamp (matches .cgz-modal's own calc(100vw - 48px)) —
14506
+ the max-width:600px block below still overrides both to 100% for the mobile layout. */
14507
+ width: 800px;
14508
+ max-width: calc(100vw - 48px);
14509
+ background: #FFFFFF;
14510
+ border: 1px solid var(--md-outline-variant);
14511
+ border-radius: 16px;
14512
+ padding: 48px 56px;
14513
+ box-sizing: border-box;
14514
+ }
14515
+ .md3 .cgz-intro {
14516
+ display: flex;
14517
+ flex-direction: column;
14518
+ align-items: center;
14519
+ gap: 12px;
14520
+ width: 100%;
14521
+ }
14522
+ .md3 .cgz-logo {
14523
+ width: 138.6px;
14524
+ height: 44px;
14525
+ }
14526
+ .md3 .cgz-badge {
14527
+ display: flex;
14528
+ align-items: center;
14529
+ gap: 8px;
14530
+ padding: 6px 13px;
14531
+ background: #DFF3E5;
14532
+ border-radius: 999px;
14533
+ font-family: Arimo, Arial, sans-serif;
14534
+ font-weight: 700;
14535
+ font-size: 13.5px;
14536
+ line-height: 1.35;
14537
+ color: #0F6B36;
14538
+ white-space: nowrap;
14539
+ }
14540
+ /* Figma specifies "42dot Sans" for this headline — not a typeface loaded anywhere in this
14541
+ project (confirmed: no other component/font-face references it). Mapped to the design
14542
+ system's own existing "big display heading" token instead (same one .co-step-title/hero
14543
+ numbers use) rather than adding a whole new webfont for one screen. 500 is the lightest
14544
+ weight actually loaded for it (Schibsted+Grotesk:wght@500;600;700;800, see :root) — closest
14545
+ available match to Figma's "Regular". */
14546
+ .md3 .cgz-title {
14547
+ margin: 0;
14548
+ font-family: var(--md-font-display, var(--md-font));
14549
+ font-weight: 500;
14550
+ font-size: 32px;
14551
+ line-height: 1.35;
14552
+ color: var(--md-on-surface);
14553
+ text-align: center;
14554
+ }
14555
+ .md3 .cgz-desc {
14556
+ margin: 0;
14557
+ max-width: 560px;
14558
+ font-family: Arimo, Arial, sans-serif;
14559
+ font-weight: 400;
14560
+ font-size: 16px;
14561
+ line-height: 1.35;
14562
+ color: var(--md-on-surface-variant);
14563
+ text-align: center;
14564
+ }
14565
+ .md3 .cgz-car {
14566
+ width: 420px;
14567
+ height: 275px;
14568
+ object-fit: contain;
14569
+ }
14570
+ .md3 .cgz-steps {
14571
+ display: flex;
14572
+ flex-direction: row;
14573
+ gap: 24px;
14574
+ width: 100%;
14575
+ padding: 20px 0;
14576
+ border-top: 1px solid var(--md-outline-variant);
14577
+ border-bottom: 1px solid var(--md-outline-variant);
14578
+ box-sizing: border-box;
14579
+ }
14580
+ .md3 .cgz-step {
14581
+ display: flex;
14582
+ flex: 1 0 0;
14583
+ min-width: 0;
14584
+ align-items: flex-start;
14585
+ gap: 10px;
14586
+ }
14587
+ .md3 .cgz-step-text {
14588
+ display: flex;
14589
+ flex-direction: column;
14590
+ gap: 1px;
14591
+ min-width: 0;
14592
+ }
14593
+ .md3 .cgz-step-title {
14594
+ margin: 0;
14595
+ font-family: Arimo, Arial, sans-serif;
14596
+ font-weight: 700;
14597
+ font-size: 14px;
14598
+ line-height: 1.35;
14599
+ color: var(--md-on-surface);
14600
+ }
14601
+ .md3 .cgz-step-sub {
14602
+ margin: 0;
14603
+ font-family: Arimo, Arial, sans-serif;
14604
+ font-weight: 400;
14605
+ font-size: 13px;
14606
+ line-height: 1.35;
14607
+ color: var(--md-on-surface-variant);
14608
+ }
14609
+ .md3 .cgz-btn {
14610
+ width: 320px;
14611
+ height: 48px;
14612
+ padding: 0 16px;
14613
+ gap: 8px;
14614
+ background: #005cff;
14615
+ border-radius: 16px;
14616
+ font-family: Arial;
14617
+ font-weight: 700;
14618
+ font-size: 15px;
14619
+ }
14620
+ .md3 .cgz-btn:hover {
14621
+ box-shadow: var(--md-e2);
14622
+ background: var(--autode-blau-strong);
14623
+ }
14624
+ @media (hover: none) {
14625
+ .md3 .cgz-btn:hover {
14626
+ box-shadow: none;
14627
+ background: #005cff;
14628
+ }
14629
+ }
14630
+ @media (max-width: 600px) {
14631
+ .md3 .cgz-modal {
14632
+ width: 100%;
14633
+ max-width: 100%;
14634
+ }
14635
+ .md3 .cgz-card {
14636
+ width: 100%;
14637
+ max-width: 100%;
14638
+ gap: 16px;
14639
+ padding: 24px 20px 26px;
14640
+ }
14641
+ .md3 .cgz-title {
14642
+ font-size: 26px;
14643
+ }
14644
+ .md3 .cgz-desc {
14645
+ max-width: none;
14646
+ font-family: Arial;
14647
+ font-size: 15px;
14648
+ }
14649
+ .md3 .cgz-car {
14650
+ width: 260px;
14651
+ height: 170px;
14652
+ }
14653
+ .md3 .cgz-steps {
14654
+ flex-direction: column;
14655
+ gap: 12px;
14656
+ padding: 16px 0;
14657
+ }
14658
+ .md3 .cgz-step-title,
14659
+ .md3 .cgz-step-sub {
14660
+ font-family: Arial;
14661
+ }
14662
+ .md3 .cgz-btn {
14663
+ width: 100%;
14664
+ }
14665
+ }
14666
+
14667
+ /* Escapes MainContent.styl's `container()` mixin (max-width: 1100px, centered) — App.tsx wraps
14668
+ every non-SPA-layout route (checkout included) in <MainContent>, which defaults to that
14669
+ 1100px cap regardless of what the routed page itself renders. CheckoutPage.tsx already
14670
+ escapes it the CSS-module way (its own mainContentWrap, max-width:100%); this is the same
14671
+ fix as a plain literal class for CheckoutThankYouPage.tsx's <Page mainContentClass="..."> —
14672
+ consistent with cgz-modal above (a plain class, not a CSS-module one, so it's reliably
14673
+ targetable here in md3.css, a completely different bundle/file). */
14674
+ .md3 .cgz-main-content {
14675
+ max-width: 100%;
14676
+ min-height: 100%;
14677
+ height: auto;
14678
+ }
14679
+
14680
+ /* Checkout step 6 "Danke" page chrome (CheckoutThankYouPage.tsx) — Figma 64637:37033's own
14681
+ "Checkout Header/Footer Desktop", deliberately simpler than CheckoutTopBarNext/
14682
+ CheckoutHeaderNext (no car price/back/stepper — the order's already placed by the time this
14683
+ renders). `.cgz-card` etc. above are shared with the CONGRATULATIONS_CHECKOUT_MODAL path and
14684
+ need no changes for this context. */
14685
+ .md3 .cgz-page {
14686
+ display: flex;
14687
+ flex-direction: column;
14688
+ min-height: 100vh;
14689
+ }
14690
+ .md3 .cgz-page-header {
14691
+ display: flex;
14692
+ align-items: center;
14693
+ flex: none;
14694
+ height: 64px;
14695
+ padding: 0 120px;
14696
+ background: #FFFFFF;
14697
+ border-bottom: 1px solid var(--md-outline-variant);
14698
+ }
14699
+ .md3 .cgz-page-logo {
14700
+ display: block;
14701
+ width: 81.9px;
14702
+ height: 26px;
14703
+ }
14704
+ .md3 .cgz-page-main {
14705
+ display: flex;
14706
+ flex-direction: column;
14707
+ align-items: center;
14708
+ flex: 1 0 auto;
14709
+ padding: 56px 24px 80px;
14710
+ background: #F4F5FA;
14711
+ }
14712
+ @media (max-width: 600px) {
14713
+ .md3 .cgz-page-header {
14714
+ height: 56px;
14715
+ padding: 0 23px;
14716
+ }
14717
+ .md3 .cgz-page-main {
14718
+ padding: 24px 23px 40px;
14719
+ }
14720
+ }
@@ -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,
@@ -1,13 +1,13 @@
1
- import * as React from 'react';
2
-
3
- // Figma-exact valid-field checkmark (.mfield-ok, FormField/MdField's showValid) — a distinct
4
- // path/stroke from the generic lucide Check that Icon name="check" renders elsewhere, so it's
5
- // its own tiny component rather than a REGISTRY entry in Icon.tsx (which would change every
6
- // other "check" usage in the app too).
7
- const MfieldCheckIcon: React.FunctionComponent = () => (
8
- <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
9
- <path d="M13.3334 4L6.00002 11.3333L2.66669 8" stroke="#168A45" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
10
- </svg>
11
- );
12
-
13
- export default MfieldCheckIcon;
1
+ import * as React from 'react';
2
+
3
+ // Figma-exact valid-field checkmark (.mfield-ok, FormField/MdField's showValid) — a distinct
4
+ // path/stroke from the generic lucide Check that Icon name="check" renders elsewhere, so it's
5
+ // its own tiny component rather than a REGISTRY entry in Icon.tsx (which would change every
6
+ // other "check" usage in the app too).
7
+ const MfieldCheckIcon: React.FunctionComponent = () => (
8
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
9
+ <path d="M13.3334 4L6.00002 11.3333L2.66669 8" stroke="#168A45" strokeWidth={2} strokeLinecap="round" strokeLinejoin="round" />
10
+ </svg>
11
+ );
12
+
13
+ export default MfieldCheckIcon;
@@ -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