@bytebrand/fe-ui-core-autobahn 1.0.125 → 1.0.126

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.
@@ -13,6 +13,30 @@
13
13
 
14
14
  .wrapInput
15
15
  display flex;
16
+ // now the actual <label> (see Checkbox.tsx) — was previously just a plain div nested
17
+ // inside a <label> that also wrapped the text content, so clicking ANYWHERE in the row
18
+ // (including the paragraph of text next to a checkbox like the checkout modal's terms
19
+ // agreement) toggled it. Shrinking the real <label> down to just this element means only
20
+ // the checkbox (+ its price badge, where used) is a native click/tap target — the text
21
+ // moved out to a plain sibling <span> in the TSX, connected back via aria-labelledby so
22
+ // screen readers still get the full text as the checkbox's accessible name.
23
+ cursor: pointer
24
+ position: relative
25
+
26
+ // Larger mobile tap target than the visible 20x20 .checkbox box (WCAG 2.5.5 wants 44x44):
27
+ // an invisible ::before extends the LABEL's own hit-region via negative inset, so a tap
28
+ // landing up to 12px outside the visible square still lands inside the <label> and toggles
29
+ // it — no extra JS, this is purely how native label click-forwarding already works, just
30
+ // given a bigger box to forward FROM. Mobile-only per the ticket; desktop clicks are
31
+ // precise enough with a mouse that enlarging the target isn't needed there.
32
+ +media-phone-only()
33
+ &::before
34
+ content: ''
35
+ position: absolute
36
+ top: -12px
37
+ right: -12px
38
+ bottom: -12px
39
+ left: -12px
16
40
 
17
41
  .wrapInput.reversed
18
42
  order: 2;
@@ -20,10 +44,19 @@
20
44
  margin-left: 5px;
21
45
  margin-right: 0;
22
46
 
47
+ // Keyboard-only focus ring (WCAG 2.4.3) — :focus-visible instead of plain :focus so it
48
+ // shows when Tabbing to the checkbox but NOT as a side effect of a mouse/touch click, which
49
+ // is what a click already relies on `:focus` for. Browsers without :focus-visible support
50
+ // simply never match this override (an invalid pseudo-class inside :not() invalidates the
51
+ // whole selector, so the rule below is skipped entirely there) and keep the old always-on-
52
+ // focus outline as a safe fallback.
23
53
  .container:focus
24
54
  outline: 2px auto lighten($primary, 80%)
25
55
  outline-offset: 2px
26
56
 
57
+ .container:focus:not(:focus-visible)
58
+ outline: none
59
+
27
60
  .container.checked
28
61
  .checkbox
29
62
  border-color: $primary
@@ -66,13 +99,15 @@
66
99
  .checkbox.withLabel
67
100
  margin-right: 10px
68
101
 
102
+ // Plain flex-row wrapper now (was a real <label> — see Checkbox.tsx/.wrapInput above for why
103
+ // it shrank down to just the checkbox). No cursor:pointer here: only .wrapInput (the actual
104
+ // clickable label now) should look clickable — the text next to it is deliberately not.
69
105
  .label
70
106
  display: flex
71
107
  flex-direction: row
72
108
  align-items: center
73
109
  position: relative
74
110
  font-size: 14px
75
- cursor: pointer
76
111
 
77
112
  .labelContent
78
113
  flex: 1 1 auto
@@ -29,6 +29,12 @@ export interface ICheckboxProps {
29
29
  label?: JSX.Element | string | null | ReactNode;
30
30
  }
31
31
 
32
+ // Unique-per-instance id for the text content span, so the (visually hidden) native
33
+ // <input type="checkbox"> can point aria-labelledby at it — see render() below for why the
34
+ // text moved out of the <label> and needs this to still work as the checkbox's accessible
35
+ // name for screen readers.
36
+ let contentIdCounter = 0;
37
+
32
38
  class Checkbox extends React.Component<ICheckboxProps, {}> {
33
39
  static defaultProps = {
34
40
  name: '',
@@ -54,6 +60,13 @@ class Checkbox extends React.Component<ICheckboxProps, {}> {
54
60
  };
55
61
 
56
62
  input: HTMLInputElement = null;
63
+ contentId: string;
64
+
65
+ constructor(props: ICheckboxProps) {
66
+ super(props);
67
+ contentIdCounter += 1;
68
+ this.contentId = `checkbox-content-${contentIdCounter}`;
69
+ }
57
70
 
58
71
  setInputRef = (ref: HTMLInputElement) => {
59
72
  this.input = ref;
@@ -133,8 +146,14 @@ class Checkbox extends React.Component<ICheckboxProps, {}> {
133
146
  // putting it where a sighted user would visually expect it (after the fields above,
134
147
  // before the CTA below) instead of unpredictably early.
135
148
  <div className={containerClasses} tabIndex={0} onFocus={this.props.onFocus} onBlur={this.props.onBlur} onKeyPress={this.onKeyPress}>
136
- <label className={labelClasses}>
137
- <div className={wrapInputClasses}>
149
+ <div className={labelClasses}>
150
+ {/* Real <label>, scoped to JUST the checkbox (+ its optional price badge) — was
151
+ wrapping the text content too, so clicking anywhere in a long paragraph like the
152
+ checkout modal's terms-agreement text toggled the checkbox as a side effect.
153
+ Native label-click-forwarding only fires for clicks landing inside THIS element
154
+ now, and it still works with zero extra JS since the hidden <input> is nested
155
+ inside it. */}
156
+ <label className={wrapInputClasses}>
138
157
  {/* Visual representation of the checkbox */}
139
158
  {/* HidePlus class for OrderOverview component */}
140
159
  {checkboxPriceValue && <span className={styles.checkboxPrice}><span className={styles.hidePlus}>+</span><FormattedNumber numbersAfterDot={0} value={checkboxPriceValue} /> €</span>}
@@ -148,18 +167,22 @@ class Checkbox extends React.Component<ICheckboxProps, {}> {
148
167
  tabIndex={-1}
149
168
  onChange={this.onChange}
150
169
  ref={this.setInputRef}
170
+ // The text is no longer a DOM ancestor/descendant of this input (moved out to a
171
+ // plain sibling <span> below so it stops being a click target) — aria-labelledby
172
+ // keeps it as the input's accessible name for assistive tech regardless.
173
+ aria-labelledby={label ? this.contentId : undefined}
151
174
  />
152
175
 
153
176
  {isAboAccordionExpanded0 && <span className={checkboxClasses}>{checked ? <IconSVG name='common_CheckBlue' className={styles.checkBlueIcon} /> : false}</span>}
154
177
 
155
- </div>
178
+ </label>
156
179
  {!!label ? (
157
- <span className={labelContentClasses}>
180
+ <span id={this.contentId} className={labelContentClasses}>
158
181
  {label}
159
182
  {tooltipDescription && <Tooltip description={tooltipDescription} positionY={'top'} positionX={tooltipPosX} tooltipClassName={styles.tooltipCheckbox} iconName='InfoTransparent' />}
160
183
  </span>
161
184
  ) : false}
162
- </label>
185
+ </div>
163
186
  </div>
164
187
  );
165
188
  }
@@ -1,88 +1,88 @@
1
- import * as React from 'react';
2
- import classnames from 'classnames';
3
- import Checkbox from './Checkbox';
4
- import styles from './FormCheckbox.styl';
5
-
6
- interface IFormCheckboxProps {
7
- className?: string;
8
- errorClassName?: string;
9
- label?: JSX.Element | string | null;
10
- field: any;
11
- checkboxClassName?: string;
12
- labelClassName?: string;
13
- labelContentClassName?: string;
14
- customError?: boolean;
15
- helperText?: string;
16
- }
17
-
18
- interface IFormCheckboxState {
19
- field: {
20
- name?: string;
21
- value?: boolean;
22
- checked?: boolean;
23
- error?: boolean;
24
- disabled?: boolean
25
- };
26
- }
27
-
28
- class FormCheckbox extends React.Component<IFormCheckboxProps, IFormCheckboxState> {
29
- constructor(props: IFormCheckboxProps) {
30
- super(props);
31
-
32
- this.state = {
33
- field: {
34
- value: this.props.field.value,
35
- error: this.props.field.error,
36
- checked: this.props.field.checked,
37
- disabled: this.props.field.disabled,
38
- name: this.props.field.name
39
- }
40
- };
41
- }
42
- static defaultProps = {
43
- className: '',
44
- label: false
45
- };
46
-
47
- handleChange = (event: React.ChangeEvent<HTMLInputElement>, checked?: boolean, value?: any) => {
48
- const { name, disabled, onChange, rules } = this.props.field;
49
- const customError = (rules.includes('accepted') && !checked);
50
- this.setState(() => ({
51
- field: {
52
- value,
53
- name,
54
- disabled,
55
- checked,
56
- error: customError
57
- }
58
- }));
59
- onChange(event, checked, value);
60
- }
61
-
62
- render() {
63
- const { label, className, checkboxClassName, labelClassName, labelContentClassName } = this.props;
64
- const { name, value, checked, disabled, error } = this.state.field;
65
- const customError = error || (this.props.field.error && !checked);
66
-
67
- const _props = {
68
- value,
69
- checked,
70
- name,
71
- disabled,
72
- label,
73
- checkboxClassName,
74
- labelClassName,
75
- labelContentClassName,
76
- onChange: this.handleChange,
77
- error: customError
78
- };
79
-
80
- return (
81
- <div className={classnames(styles.field, className)}>
82
- <Checkbox {..._props} />
83
- </div>
84
- );
85
- }
86
- }
87
-
88
- export default FormCheckbox;
1
+ import * as React from 'react';
2
+ import classnames from 'classnames';
3
+ import Checkbox from './Checkbox';
4
+ import styles from './FormCheckbox.styl';
5
+
6
+ interface IFormCheckboxProps {
7
+ className?: string;
8
+ errorClassName?: string;
9
+ label?: JSX.Element | string | null;
10
+ field: any;
11
+ checkboxClassName?: string;
12
+ labelClassName?: string;
13
+ labelContentClassName?: string;
14
+ customError?: boolean;
15
+ helperText?: string;
16
+ }
17
+
18
+ interface IFormCheckboxState {
19
+ field: {
20
+ name?: string;
21
+ value?: boolean;
22
+ checked?: boolean;
23
+ error?: boolean;
24
+ disabled?: boolean
25
+ };
26
+ }
27
+
28
+ class FormCheckbox extends React.Component<IFormCheckboxProps, IFormCheckboxState> {
29
+ constructor(props: IFormCheckboxProps) {
30
+ super(props);
31
+
32
+ this.state = {
33
+ field: {
34
+ value: this.props.field.value,
35
+ error: this.props.field.error,
36
+ checked: this.props.field.checked,
37
+ disabled: this.props.field.disabled,
38
+ name: this.props.field.name
39
+ }
40
+ };
41
+ }
42
+ static defaultProps = {
43
+ className: '',
44
+ label: false
45
+ };
46
+
47
+ handleChange = (event: React.ChangeEvent<HTMLInputElement>, checked?: boolean, value?: any) => {
48
+ const { name, disabled, onChange, rules } = this.props.field;
49
+ const customError = (rules.includes('accepted') && !checked);
50
+ this.setState(() => ({
51
+ field: {
52
+ value,
53
+ name,
54
+ disabled,
55
+ checked,
56
+ error: customError
57
+ }
58
+ }));
59
+ onChange(event, checked, value);
60
+ }
61
+
62
+ render() {
63
+ const { label, className, checkboxClassName, labelClassName, labelContentClassName } = this.props;
64
+ const { name, value, checked, disabled, error } = this.state.field;
65
+ const customError = error || (this.props.field.error && !checked);
66
+
67
+ const _props = {
68
+ value,
69
+ checked,
70
+ name,
71
+ disabled,
72
+ label,
73
+ checkboxClassName,
74
+ labelClassName,
75
+ labelContentClassName,
76
+ onChange: this.handleChange,
77
+ error: customError
78
+ };
79
+
80
+ return (
81
+ <div className={classnames(styles.field, className)}>
82
+ <Checkbox {..._props} />
83
+ </div>
84
+ );
85
+ }
86
+ }
87
+
88
+ export default FormCheckbox;
@@ -58,12 +58,16 @@
58
58
  +media-tablet-down()
59
59
  column-gap: 0
60
60
 
61
+ // Cookie Layer ticket: all 3 buttons → height 50px; "Anpassen"/"zustimmen" → width 160px;
62
+ // rounded corners/color/font size/font type brought in line with the CDP's shared .mbtn
63
+ // (md3.css) — var(--md-r-full) (pill), var(--md-primary)/var(--md-on-primary), var(--md-font),
64
+ // 14px on every breakpoint (was 11px !important on mobile, one-off vs. the CDP's constant size).
65
+ // PreviewCookieModal is mounted inside App.tsx's root `.md3` div (see CookieModal in App.tsx),
66
+ // so these --md-* custom properties resolve here exactly as they do on the CDP.
61
67
  .modalBtn
62
- height: 38px
68
+ height: 50px
63
69
  text-transform: capitalize !important
64
70
  min-width: calc(50% - 18px) !important
65
- +media-tablet-down()
66
- font-size: 11px !important
67
71
 
68
72
  .necessaryCookiesBtn
69
73
  text-transform: none !important
@@ -73,54 +77,60 @@
73
77
  justify-content: center !important
74
78
  width: 184px !important
75
79
  min-width: 184px !important
76
- height: 38px !important
77
- background: #005CFF !important
80
+ height: 50px !important
81
+ background: var(--md-primary) !important
78
82
  box-shadow: 0px 4px 8px -4px rgba(76, 78, 100, 0.42) !important
79
- border-radius: 16px !important
80
- font-family: 'Arial', 'Arimo', sans-serif !important
83
+ border-radius: var(--md-r-full) !important
84
+ font-family: var(--md-font) !important
81
85
  font-weight: 500 !important
82
86
  font-size: 14px !important
83
87
  line-height: 24px !important
84
88
  letter-spacing: 0.4px !important
85
- color: #FFFFFF !important
89
+ color: var(--md-on-primary) !important
86
90
 
87
91
  .anpassenBtn
88
92
  box-sizing: border-box !important
89
- padding: 9.95px 39.39px 11.05px 40.11px !important
90
- border: 1px solid #005CFF !important
93
+ // was a fixed asymmetric Figma-export padding (9.95/39.39/11.05/40.11px) tuned to the old
94
+ // 142.5x38 box — with flex centering already doing the alignment, that padding was vestigial
95
+ // and would fight the new 160x50 box (e.g. top+bottom padding alone exceeding line-height
96
+ // could clip/misalign the label). Flex align-items/justify-content center it correctly at
97
+ // any size, so a plain symmetric padding is enough.
98
+ padding: 0 16px !important
99
+ border: 1px solid var(--md-primary) !important
91
100
  box-shadow: none !important
92
- border-radius: 16px !important
93
- width: 142.5px !important
94
- min-width: 142.5px !important
95
- height: 38px !important
101
+ border-radius: var(--md-r-full) !important
102
+ width: 160px !important
103
+ min-width: 160px !important
104
+ height: 50px !important
96
105
  display: flex !important
97
106
  align-items: center !important
98
107
  justify-content: center !important
99
108
  text-align: center !important
100
- font-family: 'Arial', 'Arimo', sans-serif !important
109
+ font-family: var(--md-font) !important
101
110
  font-style: normal !important
102
111
  font-weight: 400 !important
103
112
  font-size: 14px !important
104
113
  line-height: 16px !important
105
- color: #005CFF !important
114
+ color: var(--md-primary) !important
106
115
 
107
116
  .zustimmenBtn
108
- padding: 9.95px 36.87px 11.05px 35.63px !important
109
- width: 142.5px !important
110
- min-width: 142.5px !important
111
- height: 38px !important
112
- background: #005CFF !important
113
- border-radius: 16px !important
117
+ box-sizing: border-box !important
118
+ padding: 0 16px !important
119
+ width: 160px !important
120
+ min-width: 160px !important
121
+ height: 50px !important
122
+ background: var(--md-primary) !important
123
+ border-radius: var(--md-r-full) !important
114
124
  display: flex !important
115
125
  align-items: center !important
116
126
  justify-content: center !important
117
127
  text-align: center !important
118
- font-family: 'Arial', 'Arimo', sans-serif !important
128
+ font-family: var(--md-font) !important
119
129
  font-style: normal !important
120
130
  font-weight: 400 !important
121
131
  font-size: 14px !important
122
132
  line-height: 16px !important
123
- color: #FFFFFF !important
133
+ color: var(--md-on-primary) !important
124
134
 
125
135
  .outlinedBtnLeft
126
136
  display: flex;
@@ -164,7 +164,7 @@
164
164
  "lastNamePlaceholder": "Last name*",
165
165
  "lastNameAria": "Last name",
166
166
  "countryLabel": "Country",
167
- "areaCodeLabel": "Area code",
167
+ "areaCodeLabel": "Prefix",
168
168
  "phoneNumberLabel": "Phone number",
169
169
  "termsPrefix": "I agree to the auto.de",
170
170
  "termsLink": "terms of use",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bytebrand/fe-ui-core-autobahn",
3
- "version": "1.0.125",
3
+ "version": "1.0.126",
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
@@ -665,7 +665,7 @@ line-height: 31px;
665
665
  align-items: center;
666
666
  gap: 10px;
667
667
  height: 48px;
668
- padding: 0 14px;
668
+ padding: 0 12px;
669
669
  border-radius: var(--md-r-xs);
670
670
  background: transparent;
671
671
  box-shadow: inset 0 0 0 1px var(--md-outline-variant);
@@ -5686,8 +5686,15 @@ line-height: 31px;
5686
5686
  /* Kaufen tab (FinanceCalculator.tsx's own .fin-calc-card) — was an inline style={isCash ? {...} :
5687
5687
  undefined}; switched to a class modifier because React leaves a stale (but harmless-looking)
5688
5688
  style="" attribute on the node when that ternary flips to undefined on tab switch. */
5689
+ /* row-gap was 0px here (this rule used to be the only one — no breakpoint scoping), which on
5690
+ desktop zeroed the gap between the price row and the (empty, isCash) .fin-cond row while
5691
+ Financing/Leasing keep the base .fin-price-grid's 12px — the exact same cross-tab jump the
5692
+ ≤-breakpoint rule further down this file (search "Bug: switching the Financing/Leasing/Buy
5693
+ tabs") already diagnosed and fixed for mobile, just never mirrored up here for desktop. Same
5694
+ fix, same reasoning: keep the row-gap constant across all 3 tabs so nothing below (rating
5695
+ pill, CTA buttons) shifts when .fin-cond collapses to empty on Buy. */
5689
5696
  .md3 .fin-calc-card .fin-price-grid.fin-price-grid--cash {
5690
- gap: 0px 15px;
5697
+ gap: 12px 15px;
5691
5698
  }
5692
5699
  .md3 .fin-rate-main {
5693
5700
  grid-area: rate;
@@ -5963,12 +5970,23 @@ line-height: 31px;
5963
5970
  .md3 .fin-cond {
5964
5971
  min-width: 0;
5965
5972
  font-size: 12.5px;
5973
+ line-height: 15px;
5974
+ /* renders empty on the Buy tab (isCash hides its "0 € Anzahlung · 60 Monate · ..." text —
5975
+ see FinanceCalculator.tsx) — without a floor it collapsed to 0 height there instead of the
5976
+ one text line Financing/Leasing occupy, part of the same cross-tab jump fix as the
5977
+ .fin-price-grid--cash row-gap above (mirrors the already-working ≤-breakpoint fix). */
5978
+ min-height: 15px;
5966
5979
  color: var(--md-on-surface-variant);
5967
5980
  }
5968
5981
  /* Rate-edit (Rate anpassen) sits between the rate and the CTA now */
5969
5982
  .md3 .fin-adjust {
5970
5983
  margin-top: 14px;
5971
5984
  }
5985
+ /* Kaufen ("Details anzeigen") only — no top margin, per explicit request. Financing/Leasing
5986
+ ("Rate anpassen") keep the shared 14px above unchanged. */
5987
+ .md3 .fin-calc-card .fin-adjust.fin-adjust--cash {
5988
+ margin-top: 0;
5989
+ }
5972
5990
  /* auto.de-ID in front of the Finanzierungs-/Leasingdetails link — one row, divider above */
5973
5991
  .md3 .fin-info {
5974
5992
  margin-top: 16px;
@@ -5998,6 +6016,18 @@ line-height: 31px;
5998
6016
  display: none;
5999
6017
  margin-top: 8px;
6000
6018
  }
6019
+ /* Request ("Angebot anfragen") vs Call ("Anrufen", inside .fin-call) — was mbtn--lg's 48px on
6020
+ Request against plain .mbtn's 40px on Call (no size modifier there), a fixed mismatch present
6021
+ on every tab alike (not something that changes on tab switch). Bumped both to the same 50px —
6022
+ matches the mobile CTA row's own Request/Call sizing (already 50/50 there) and satisfies
6023
+ "must be higher" for both. Scoped to just these 2 buttons — no other spacing/position in the
6024
+ card changes. */
6025
+ .md3 .fin-calc-card .hide-mobile > .mbtn--lg.mbtn--block {
6026
+ height: 50px;
6027
+ }
6028
+ .md3 .fin-calc-card .fin-call .mbtn {
6029
+ height: 50px;
6030
+ }
6001
6031
  /* Laufzeit as selectable chips — 4-column grid, filled selected state */
6002
6032
  .md3 .term-chips {
6003
6033
  display: grid;
@@ -7135,6 +7165,12 @@ line-height: 31px;
7135
7165
  gap: 15px 44px;
7136
7166
  margin-top: 14px;
7137
7167
  }
7168
+ /* owner count — mobile only, per request. Desktop keeps it (base .vdp-meta rule, untouched).
7169
+ !important: still showed after the plain version — something later/more specific in this
7170
+ file was winning the cascade for this span's display; forcing it rather than keep hunting. */
7171
+ .md3 .vdp-meta-owner {
7172
+ display: none !important;
7173
+ }
7138
7174
  .md3 .vdp-meta > span + span::before {
7139
7175
  content: none;
7140
7176
  }
@@ -7200,10 +7236,9 @@ line-height: 31px;
7200
7236
  }
7201
7237
  /* compact dot-line = km/EZ/Getriebe/Kraftstoff · Karosserie (round 9: body type must be
7202
7238
  visible here — was previously invisible under nth-child(n+5), which also swallowed it).
7203
- Only "1. Hand" (item 5) stays hidden; item 6 (body type) now renders. */
7204
- .md3 .vdp-header-summary .vdp-meta > span:nth-child(5) {
7205
- display: none;
7206
- }
7239
+ "1. Hand" stays hidden via .vdp-meta-owner (below, generic rule) — was nth-child(5) here,
7240
+ which broke the moment Horsepower got inserted as a new 3rd item and shifted owner from
7241
+ position 5 to 6 (silently hid Gearbox instead). A class survives future reordering. */
7207
7242
  /* highlights: the ≤980 Figma 2-col grid applies here too (no swipe-line since round 9) */
7208
7243
  /* condition row: 2×2 on phone */
7209
7244
  .md3 .zustand-grid {
@@ -9167,6 +9202,15 @@ color: #005CFF;
9167
9202
  grid-template-areas: "rate evalcol" "cond cond";
9168
9203
  align-items: start;
9169
9204
  }
9205
+ /* Request/Call row jump (mobile) — per request: one fixed-height block for .fin-price-grid,
9206
+ with .fin-cta-row (next sibling once .hide-mobile's desktop block collapses to 0 height
9207
+ here) landing at the same fixed offset below it on every tab, instead of chasing each row's
9208
+ own height individually (.fin-cond min-height / .fin-price-grid--cash gap below still help
9209
+ fill this box, they're just no longer the thing .fin-cta-row's position depends on).
9210
+ 97px — measured value (devtools), confirmed. */
9211
+ .md3 .fin-calc-card .fin-price-grid {
9212
+ min-height: 97px;
9213
+ }
9170
9214
  /* Bug: switching the Financing/Leasing/Buy tabs made the price row, rating pill and the two
9171
9215
  CTA buttons below jump up/down, because the row above them had a different rendered height
9172
9216
  per tab: (a) .fin-cond (the "0 € Anzahlung · 60 Monate · ..." terms line) renders NOTHING on
@@ -9175,10 +9219,10 @@ color: #005CFF;
9175
9219
  below cancel each of those, so Financing/Leasing/Buy all reserve the same total height.
9176
9220
  (A first pass also force-reserved height on .fin-rate-main itself, to additionally cover the
9177
9221
  rare case of an extra struck list-price line on Buy when the car has a discount — isCash &&
9178
- showStrike in FinanceCalculator.tsx. Dropped: since Financing/Leasing/undiscounted-Buy don't
9179
- need that extra room, it left a visible empty gap under the price on every normal tab just to
9180
- pre-cover one edge case. That specific combination can still shift by the strike line's own
9181
- height; every other tab switch is now jump-free without the dead space.) */
9222
+ showStrike in FinanceCalculator.tsx. Dropped at the time: reserving it on every tab left a
9223
+ visible empty gap under the price whenever that edge case wasn't in play. Revisited below,
9224
+ right after the .fin-cond rule, with the actual fix instead: take the strike line out of
9225
+ flow so it no longer needs a reservation on the OTHER tabs at all.) */
9182
9226
  .md3 .fin-calc-card .fin-price-grid.fin-price-grid--cash {
9183
9227
  /* same 12px row-gap as Financing/Leasing (the base .fin-price-grid rule) — the 0px gap on
9184
9228
  .fin-price-grid--cash further up this file otherwise pulls the CTA row 12px closer on Buy. */
@@ -839,31 +839,35 @@ const FinanceCalculator: React.FunctionComponent<FinanceCalculatorProps> = ({
839
839
  <div className="fin-eval-bar-cell">{ratingLabel && <EvalBar tier={evalTier} />}</div>
840
840
  </div>
841
841
  {/* col 1 row 2: condition line — financing "0 € Anzahlung · 60 Monate · Finanzierungsdetails",
842
- leasing "25.000 € Sonderzahlung · 60 Mon. · 10.000 km/Jahr · Details" */}
843
- <div className="fin-cond">
844
- {!isCash &&
845
- condParts.map((part, i) => (
842
+ leasing "25.000 € Sonderzahlung · 60 Mon. · 10.000 km/Jahr · Details". Buy has nothing
843
+ to show here (no down payment/term/details link of its own) — not rendered at all
844
+ rather than left as an empty reserved-height div. */}
845
+ {!isCash && (
846
+ <div className="fin-cond">
847
+ {condParts.map((part, i) => (
846
848
  <React.Fragment key={i}>
847
849
  {i > 0 && <span className="cc-terms-dot"> · </span>}
848
850
  {part}
849
851
  </React.Fragment>
850
852
  ))}
851
- {!isCash && onShowDetails && (
852
- <>
853
- <span className="cc-terms-dot"> · </span>
854
- {/* plain underlined text link, same onShowDetails handler as .fin-info used to open. */}
855
- <button type="button" className="cc-terms-link" onClick={onShowDetails}>
856
- {mode === 'leasing' ? t('autobahn:card.price.detailsLink') : t('autobahn:finance.financingDetails')}
857
- </button>
858
- </>
859
- )}
860
- </div>
853
+ {onShowDetails && (
854
+ <>
855
+ <span className="cc-terms-dot"> · </span>
856
+ {/* plain underlined text link, same onShowDetails handler as .fin-info used to open. */}
857
+ <button type="button" className="cc-terms-link" onClick={onShowDetails}>
858
+ {mode === 'leasing' ? t('autobahn:card.price.detailsLink') : t('autobahn:finance.financingDetails')}
859
+ </button>
860
+ </>
861
+ )}
862
+ </div>
863
+ )}
861
864
  </div>
862
865
 
863
866
  {/* ============================== DESKTOP (≥601px) — unchanged ============================== */}
864
867
  <div className="hide-mobile">
865
- {/* RATE EDIT (Rate anpassen) — between the rate and the request CTA */}
866
- <div className="fin-adjust">
868
+ {/* RATE EDIT (Rate anpassen) — between the rate and the request CTA. fin-adjust--cash:
869
+ Kaufen-only margin-top override (0, was the shared 14px) — see md3.css. */}
870
+ <div className={'fin-adjust' + (isCash ? ' fin-adjust--cash' : '')}>
867
871
  <button onClick={() => setAdjust((a) => !a)} style={expanderBtnStyle}>
868
872
  <span style={{ display: 'inline-flex', alignItems: 'center', gap: 7 }}>
869
873
  <Icon name={isCash ? 'list' : 'sliders-horizontal'} size={16} color="var(--md-primary)" />
@@ -59,6 +59,17 @@ function stripParen(s: string): string {
59
59
  return s.replace(/\s*\(.*\)/, '');
60
60
  }
61
61
 
62
+ /**
63
+ * Opposite of stripParen, for vehicle.power specifically: pull the PS value OUT of the trailing
64
+ * parenthetical instead of discarding it, e.g. '145 kW (197 PS)' → '197 PS' — the fact line
65
+ * wants PS (Horsepower), not kW. Same helper as cards/cardParts.tsx's own extractPS (that file
66
+ * doesn't export it, so it's duplicated here rather than reaching across packages for one line).
67
+ */
68
+ function extractPS(s: string): string {
69
+ const m = s.match(/\((\d+\s*PS)\)\s*$/i);
70
+ return m ? m[1].replace(/\s+/, ' ') : stripParen(s);
71
+ }
72
+
62
73
  // ---------------------------------------------------------------------------
63
74
  // Header card — title, fact line, highlight chips, condition tiles
64
75
  // ---------------------------------------------------------------------------
@@ -73,11 +84,14 @@ export interface VdpHeaderCardProps {
73
84
  * identity block. Split out so the mobile-only layout can show this BEFORE the finance
74
85
  * card (see VdpTrustRow) while the highlights/condition tiles move after it. */
75
86
  export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({ vehicle, t }) => {
87
+ // Order: Mileage, First Registration, Horsepower, Fuel Type, Gearbox, then owners last
88
+ // (unchanged position) — per request, desktop + mobile both use this order.
76
89
  const factLine: ReadonlyArray<readonly [string, string]> = [
77
90
  ['gauge', fmtKm(vehicle.km)],
78
91
  ['calendar', vehicle.reg],
79
- ['cog', vehicle.gear],
92
+ ['zap', extractPS(vehicle.power)],
80
93
  ['fuel', stripParen(vehicle.fuel)],
94
+ ['cog', vehicle.gear],
81
95
  ['user', t('autobahn:header.owners', { owners: vehicle.owners, count: vehicle.owners })],
82
96
  ];
83
97
  return (
@@ -108,7 +122,8 @@ export const VdpHeaderSummary: React.FunctionComponent<VdpHeaderCardProps> = ({
108
122
  {/* fact line — plain text with dot separators, not pills */}
109
123
  <div className="vdp-meta">
110
124
  {factLine.map(([ic, v]) => (
111
- <span key={ic}>
125
+ // vdp-meta-owner: hidden on mobile only (md3.css, ≤600px) — desktop keeps it.
126
+ <span key={ic} className={ic === 'user' ? 'vdp-meta-owner' : undefined}>
112
127
  <Icon name={ic} size={16} color="var(--md-on-surface-variant)" stroke={1.8} />
113
128
  {v}
114
129
  </span>
@@ -3790,7 +3805,10 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
3790
3805
  const [areaCode, setAreaCode] = useState('');
3791
3806
  const [phoneNum, setPhoneNum] = useState('');
3792
3807
  const [consent, setConsent] = useState(false);
3793
- const [pref, setPref] = useState<'email' | 'phone'>('email');
3808
+ // "Telefon" selected by default (per request) — both channels stay fully selectable via the
3809
+ // chips below; `valid`/asterisk logic already branches on `pref` reactively, so this default
3810
+ // is the only change needed to make Telefon the pre-selected (not exclusive) channel.
3811
+ const [pref, setPref] = useState<'email' | 'phone'>('phone');
3794
3812
  const [message, setMessage] = useState(
3795
3813
  autoDeId
3796
3814
  ? t('autobahn:enquiry.prefilledMessageWithId', { carTitle, autoDeId })
@@ -3889,14 +3907,8 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
3889
3907
  {t('autobahn:enquiry.contactMethod.heading')}
3890
3908
  </div>
3891
3909
  <div className="enquiry-contact-buttons" style={{ display: 'flex', gap: 8 }}>
3892
- <button
3893
- type="button"
3894
- className={'chip' + (pref === 'email' ? ' chip--filter chip--selected' : '')}
3895
- onClick={() => setPref('email')}
3896
- >
3897
- <Icon name="mail" size={15} />
3898
- {t('autobahn:enquiry.contactMethod.email')}
3899
- </button>
3910
+ {/* Telefon left, E-Mail right (per request) — both stay fully selectable, only
3911
+ the order + the default `pref` above changed. */}
3900
3912
  <button
3901
3913
  type="button"
3902
3914
  className={'chip' + (pref === 'phone' ? ' chip--filter chip--selected' : '')}
@@ -3905,6 +3917,14 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
3905
3917
  <Icon name="phone" size={15} />
3906
3918
  {t('autobahn:enquiry.contactMethod.phone')}
3907
3919
  </button>
3920
+ <button
3921
+ type="button"
3922
+ className={'chip' + (pref === 'email' ? ' chip--filter chip--selected' : '')}
3923
+ onClick={() => setPref('email')}
3924
+ >
3925
+ <Icon name="mail" size={15} />
3926
+ {t('autobahn:enquiry.contactMethod.email')}
3927
+ </button>
3908
3928
  </div>
3909
3929
  </div>
3910
3930
  {/* Vorname* / Nachname* — two required fields SIDE BY SIDE (Figma round 9; fixed
@@ -3951,7 +3971,11 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
3951
3971
  <FloatField label={t('autobahn:enquiry.form.areaCodeLabel')}>
3952
3972
  <input
3953
3973
  type="tel"
3954
- placeholder="1638"
3974
+ // was a hardcoded fake number ("1638"), then the areaCodeLabel text — per
3975
+ // request, empty: the FloatField label above already reads "Prefix"/"Vorwahl",
3976
+ // no placeholder needed on top of it. Real prefill only ever comes from
3977
+ // `areaCode` state itself (authenticated profile / prior form input).
3978
+ placeholder=""
3955
3979
  maxLength={5}
3956
3980
  value={areaCode}
3957
3981
  onChange={(e) => setAreaCode(e.target.value)}
@@ -3961,7 +3985,8 @@ export const VdpEnquiry: React.FunctionComponent<VdpEnquiryProps> = ({
3961
3985
  <FloatField label={t('autobahn:enquiry.form.phoneNumberLabel')}>
3962
3986
  <input
3963
3987
  type="tel"
3964
- placeholder="5857458"
3988
+ // was a hardcoded fake number ("5857458") — see areaCode above, empty now too.
3989
+ placeholder=""
3965
3990
  maxLength={13}
3966
3991
  value={phoneNum}
3967
3992
  onChange={(e) => setPhoneNum(e.target.value)}