@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.
- package/compat/Checkbox/Checkbox.styl +36 -1
- package/compat/Checkbox/Checkbox.tsx +28 -5
- package/compat/Checkbox/FormCheckbox.tsx +88 -88
- package/compat/CookieModal/modals/PreviewCookieModal/PreviewCookieModal.styl +34 -24
- package/locales/en/autobahn.json +1 -1
- package/package.json +1 -1
- package/theme/md3.css +54 -10
- package/vdp/FinanceCalculator.tsx +20 -16
- package/vdp/VdpModules.tsx +38 -13
|
@@ -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
|
-
<
|
|
137
|
-
<
|
|
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
|
-
</
|
|
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
|
-
</
|
|
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:
|
|
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:
|
|
77
|
-
background:
|
|
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:
|
|
80
|
-
font-family:
|
|
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:
|
|
89
|
+
color: var(--md-on-primary) !important
|
|
86
90
|
|
|
87
91
|
.anpassenBtn
|
|
88
92
|
box-sizing: border-box !important
|
|
89
|
-
padding
|
|
90
|
-
|
|
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:
|
|
93
|
-
width:
|
|
94
|
-
min-width:
|
|
95
|
-
height:
|
|
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:
|
|
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:
|
|
114
|
+
color: var(--md-primary) !important
|
|
106
115
|
|
|
107
116
|
.zustimmenBtn
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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:
|
|
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:
|
|
133
|
+
color: var(--md-on-primary) !important
|
|
124
134
|
|
|
125
135
|
.outlinedBtnLeft
|
|
126
136
|
display: flex;
|
package/locales/en/autobahn.json
CHANGED
|
@@ -164,7 +164,7 @@
|
|
|
164
164
|
"lastNamePlaceholder": "Last name*",
|
|
165
165
|
"lastNameAria": "Last name",
|
|
166
166
|
"countryLabel": "Country",
|
|
167
|
-
"areaCodeLabel": "
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
7204
|
-
|
|
7205
|
-
|
|
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:
|
|
9179
|
-
|
|
9180
|
-
|
|
9181
|
-
|
|
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
|
-
|
|
844
|
-
|
|
845
|
-
|
|
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
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
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
|
-
|
|
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)" />
|
package/vdp/VdpModules.tsx
CHANGED
|
@@ -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
|
-
['
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3893
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)}
|