@repobit/dex-system-design 0.23.6 → 0.23.7
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/CHANGELOG.md +8 -0
- package/package.json +2 -2
- package/src/components/compare/compare.css.js +121 -72
- package/src/components/compare/compare.js +351 -70
- package/src/components/compare/compare.stories.js +356 -106
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { LitElement, html } from "lit";
|
|
2
|
-
import "../../components/image/image.js";
|
|
1
|
+
import { LitElement, html, nothing } from "lit";
|
|
3
2
|
import { tokens } from "../../tokens/tokens.js";
|
|
3
|
+
import "../image/image.js";
|
|
4
4
|
import "../link/link.js";
|
|
5
|
-
import compareCSS from "./compare.css";
|
|
5
|
+
import compareCSS from "./compare.css.js";
|
|
6
6
|
|
|
7
7
|
// ═══════════════════════════════════════════════════════════════
|
|
8
8
|
// compare-bar
|
|
@@ -12,50 +12,241 @@ import compareCSS from "./compare.css";
|
|
|
12
12
|
// max-score {Number} – Max possible score (default: 6)
|
|
13
13
|
// variant {String} – "primary" | "secondary"
|
|
14
14
|
// score-label {String} – Optional override text for the score
|
|
15
|
+
// reference-score {Number} – Baseline score (e.g. Bitdefender). With stretch
|
|
16
|
+
// (set by compare-card), width maps [r_min,1] → [fill_min,100%].
|
|
17
|
+
// scale {Number} – Only used when reference-score is unset (× max-score %)
|
|
15
18
|
// ═══════════════════════════════════════════════════════════════
|
|
16
19
|
|
|
20
|
+
function readStretchVars(el) {
|
|
21
|
+
const styles = getComputedStyle(el);
|
|
22
|
+
return {
|
|
23
|
+
stretch: styles.getPropertyValue("--cs-bar-stretch").trim() !== "0",
|
|
24
|
+
rMinStr: styles.getPropertyValue("--cs-stretch-r-min").trim(),
|
|
25
|
+
fillMin: parseFloat(styles.getPropertyValue("--cs-bar-fill-min").trim()),
|
|
26
|
+
gamma : parseFloat(styles.getPropertyValue("--cs-bar-stretch-gamma").trim())
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const SCORE_ANIM_MS = 800;
|
|
31
|
+
const SCORE_ANIM_EASING = (t) => 1 - (1 - t) ** 3;
|
|
32
|
+
|
|
33
|
+
/** After first paint of this document, intro is “consumed” (scroll-back won’t replay). */
|
|
34
|
+
let compareIntroDone = false;
|
|
35
|
+
/** @type {ReturnType<typeof setTimeout> | 0} */
|
|
36
|
+
let compareIntroTimer = 0;
|
|
37
|
+
|
|
38
|
+
function shouldPlayCompareIntro() {
|
|
39
|
+
return !compareIntroDone;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function scheduleCompareIntroDone() {
|
|
43
|
+
if (compareIntroDone || compareIntroTimer) return;
|
|
44
|
+
compareIntroTimer = setTimeout(() => {
|
|
45
|
+
compareIntroTimer = 0;
|
|
46
|
+
compareIntroDone = true;
|
|
47
|
+
}, SCORE_ANIM_MS + 120);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function prefersReducedMotion() {
|
|
51
|
+
try {
|
|
52
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
53
|
+
} catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (typeof globalThis !== "undefined" && prefersReducedMotion()) {
|
|
59
|
+
compareIntroDone = true;
|
|
60
|
+
}
|
|
61
|
+
|
|
17
62
|
class CompareBar extends LitElement {
|
|
18
63
|
static properties = {
|
|
19
|
-
label
|
|
20
|
-
score
|
|
21
|
-
maxScore
|
|
22
|
-
variant
|
|
23
|
-
scoreLabel: { type: String, attribute: "score-label" }
|
|
64
|
+
label : { type: String },
|
|
65
|
+
score : { type: Number },
|
|
66
|
+
maxScore : { type: Number, attribute: "max-score" },
|
|
67
|
+
variant : { type: String },
|
|
68
|
+
scoreLabel : { type: String, attribute: "score-label" },
|
|
69
|
+
referenceScore: { type: Number, attribute: "reference-score" },
|
|
70
|
+
scale : { type: Number },
|
|
71
|
+
_animatedScore: { state: true }
|
|
24
72
|
};
|
|
25
73
|
|
|
26
74
|
static styles = [tokens, compareCSS];
|
|
27
75
|
|
|
28
76
|
constructor() {
|
|
29
77
|
super();
|
|
30
|
-
this.label
|
|
31
|
-
this.score
|
|
32
|
-
this.maxScore
|
|
33
|
-
this.variant
|
|
78
|
+
this.label = "";
|
|
79
|
+
this.score = 0;
|
|
80
|
+
this.maxScore = 6;
|
|
81
|
+
this.variant = "secondary";
|
|
34
82
|
this.scoreLabel = "";
|
|
83
|
+
this.referenceScore = 0;
|
|
84
|
+
this.scale = 1;
|
|
85
|
+
this._animatedScore = NaN;
|
|
86
|
+
/** @type {number} */
|
|
87
|
+
this._scoreAnimRaf = 0;
|
|
35
88
|
}
|
|
36
89
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
if (
|
|
90
|
+
connectedCallback() {
|
|
91
|
+
super.connectedCallback();
|
|
92
|
+
this.setAttribute("aria-label", this._barAriaLabel());
|
|
93
|
+
if (!shouldPlayCompareIntro()) {
|
|
94
|
+
this.classList.add("compare-no-motion");
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
scheduleCompareIntroDone();
|
|
98
|
+
if (this._shouldAnimateCompetitorScore()) {
|
|
99
|
+
this._animatedScore = this._scoreAnimStart();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
disconnectedCallback() {
|
|
104
|
+
super.disconnectedCallback();
|
|
105
|
+
this._cancelScoreAnim();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
updated(changed) {
|
|
109
|
+
super.updated(changed);
|
|
110
|
+
this.setAttribute("aria-label", this._barAriaLabel());
|
|
111
|
+
const deps = [
|
|
112
|
+
"score",
|
|
113
|
+
"referenceScore",
|
|
114
|
+
"scoreLabel",
|
|
115
|
+
"variant",
|
|
116
|
+
"maxScore",
|
|
117
|
+
"label"
|
|
118
|
+
|
|
119
|
+
];
|
|
120
|
+
if (!deps.some((k) => changed.has(k))) return;
|
|
121
|
+
|
|
122
|
+
this._cancelScoreAnim();
|
|
123
|
+
if (!shouldPlayCompareIntro()) {
|
|
124
|
+
this.classList.add("compare-no-motion");
|
|
125
|
+
this._animatedScore = NaN;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
if (this._shouldAnimateCompetitorScore()) {
|
|
129
|
+
this._animatedScore = this._scoreAnimStart();
|
|
130
|
+
this._runScoreAnim();
|
|
131
|
+
} else {
|
|
132
|
+
this._animatedScore = NaN;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_shouldAnimateCompetitorScore() {
|
|
137
|
+
if (this.variant === "primary" || this.scoreLabel) return false;
|
|
138
|
+
const t = parseFloat(this.score);
|
|
139
|
+
return Number.isFinite(t);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
_scoreAnimStart() {
|
|
143
|
+
const target = parseFloat(this.score);
|
|
144
|
+
const ref = parseFloat(this.referenceScore);
|
|
145
|
+
const maxM = parseFloat(this.maxScore) || 6;
|
|
146
|
+
let start = ref > 0 ? ref : maxM;
|
|
147
|
+
if (start <= target) start = Math.min(maxM, target + 0.2);
|
|
148
|
+
return start;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
_cancelScoreAnim() {
|
|
152
|
+
if (this._scoreAnimRaf) {
|
|
153
|
+
cancelAnimationFrame(this._scoreAnimRaf);
|
|
154
|
+
this._scoreAnimRaf = 0;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
_runScoreAnim() {
|
|
159
|
+
const target = parseFloat(this.score);
|
|
160
|
+
const start = this._animatedScore;
|
|
161
|
+
if (!Number.isFinite(target) || !Number.isFinite(start)) return;
|
|
162
|
+
|
|
163
|
+
const t0 = performance.now();
|
|
164
|
+
|
|
165
|
+
const tick = (now) => {
|
|
166
|
+
const elapsed = now - t0;
|
|
167
|
+
const u = Math.min(1, elapsed / SCORE_ANIM_MS);
|
|
168
|
+
const e = SCORE_ANIM_EASING(u);
|
|
169
|
+
const v = start + (target - start) * e;
|
|
170
|
+
this._animatedScore = v;
|
|
171
|
+
if (u < 1) {
|
|
172
|
+
this._scoreAnimRaf = requestAnimationFrame(tick);
|
|
173
|
+
} else {
|
|
174
|
+
this._scoreAnimRaf = 0;
|
|
175
|
+
this._animatedScore = NaN;
|
|
176
|
+
this.requestUpdate();
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
this._scoreAnimRaf = requestAnimationFrame(tick);
|
|
181
|
+
}
|
|
41
182
|
|
|
183
|
+
get _pct() {
|
|
184
|
+
const s = parseFloat(this.score) || 0;
|
|
42
185
|
if (this.variant === "primary") return 100;
|
|
43
186
|
|
|
187
|
+
const ref = parseFloat(this.referenceScore);
|
|
188
|
+
if (ref > 0) {
|
|
189
|
+
const r = Math.min(1, Math.max(0, s / ref));
|
|
190
|
+
const linearPct = r * 100;
|
|
191
|
+
const { stretch, rMinStr, fillMin, gamma } = readStretchVars(this);
|
|
192
|
+
|
|
193
|
+
if (!stretch || !rMinStr || rMinStr === "none") {
|
|
194
|
+
return Math.min(100, Math.max(0, linearPct));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const rMin = parseFloat(rMinStr);
|
|
198
|
+
if (Number.isNaN(rMin) || rMin >= 1 - 1e-9) {
|
|
199
|
+
return Math.min(100, Math.max(0, linearPct));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const denom = 1 - rMin;
|
|
203
|
+
const wMin = Number.isFinite(fillMin) ? fillMin : 50;
|
|
204
|
+
const g = Number.isFinite(gamma) && gamma > 0 ? gamma : 1;
|
|
205
|
+
const u = (r - rMin) / denom;
|
|
206
|
+
const t = Math.min(1, Math.max(0, u));
|
|
207
|
+
const shaped = Math.pow(t, g);
|
|
208
|
+
const stretched = wMin + shaped * (100 - wMin);
|
|
209
|
+
return Math.min(100, Math.max(0, stretched));
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const m = parseFloat(this.maxScore) || 6;
|
|
213
|
+
if (m === 0) return 0;
|
|
44
214
|
const rawPct = Math.min(100, Math.max(0, (s / m) * 100));
|
|
45
|
-
return rawPct *
|
|
215
|
+
return rawPct * (this.scale || 1);
|
|
46
216
|
}
|
|
47
217
|
|
|
48
218
|
get _displayScore() {
|
|
49
219
|
return this.scoreLabel || this.score;
|
|
50
220
|
}
|
|
51
221
|
|
|
222
|
+
_renderScoreText() {
|
|
223
|
+
if (this.variant === "primary" || this.scoreLabel) {
|
|
224
|
+
return this._displayScore;
|
|
225
|
+
}
|
|
226
|
+
if (Number.isFinite(this._animatedScore)) {
|
|
227
|
+
return CompareBar._formatScoreTick(this._animatedScore);
|
|
228
|
+
}
|
|
229
|
+
return this._displayScore;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** @param {number} v */
|
|
233
|
+
static _formatScoreTick(v) {
|
|
234
|
+
return (Math.round(v * 100) / 100).toFixed(2);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Accessible name uses final numeric score (not the intro animation value). */
|
|
238
|
+
_barAriaLabel() {
|
|
239
|
+
const max = this.maxScore ?? 6;
|
|
240
|
+
const val = this.scoreLabel || this.score;
|
|
241
|
+
return `${this.label}, score ${val} out of ${max}`;
|
|
242
|
+
}
|
|
243
|
+
|
|
52
244
|
render() {
|
|
53
245
|
return html`
|
|
54
246
|
<div class="bar-track">
|
|
55
|
-
<div class="bar-fill" style="width: ${this._pct}%"
|
|
56
|
-
|
|
57
|
-
<span class="bar-
|
|
58
|
-
<span class="bar-score">${this._displayScore}</span>
|
|
247
|
+
<div class="bar-fill" style="width: ${this._pct}%">
|
|
248
|
+
<span class="bar-label" aria-hidden="true">${this.label}</span>
|
|
249
|
+
<span class="bar-score" aria-hidden="true">${this._renderScoreText()}</span>
|
|
59
250
|
</div>
|
|
60
251
|
</div>
|
|
61
252
|
`;
|
|
@@ -64,42 +255,128 @@ class CompareBar extends LitElement {
|
|
|
64
255
|
|
|
65
256
|
customElements.define("compare-bar", CompareBar);
|
|
66
257
|
|
|
67
|
-
|
|
68
258
|
// ═══════════════════════════════════════════════════════════════
|
|
69
259
|
// compare-card
|
|
70
|
-
// Attributes / Properties:
|
|
71
|
-
// title {String} – Card heading
|
|
72
|
-
// description {String} – Explanatory subtitle
|
|
73
|
-
// footnote {String} – Small source text at the bottom
|
|
74
|
-
// footnote-href {String} – URL for the linked part of the footnote
|
|
75
|
-
// icon-src {String} – Path to icon asset for bd-img
|
|
76
|
-
//
|
|
77
|
-
// Slots:
|
|
78
|
-
// (default) – <compare-bar> elements
|
|
79
260
|
// ═══════════════════════════════════════════════════════════════
|
|
80
261
|
|
|
81
262
|
class CompareCard extends LitElement {
|
|
263
|
+
static _idSeq = 0;
|
|
264
|
+
|
|
82
265
|
static properties = {
|
|
83
|
-
title
|
|
84
|
-
description
|
|
85
|
-
footnote
|
|
86
|
-
footnoteHref: { type: String, attribute: "footnote-href" },
|
|
87
|
-
iconSrc
|
|
266
|
+
title : { type: String },
|
|
267
|
+
description : { type: String },
|
|
268
|
+
footnote : { type: String },
|
|
269
|
+
footnoteHref : { type: String, attribute: "footnote-href" },
|
|
270
|
+
iconSrc : { type: String, attribute: "icon-src" },
|
|
271
|
+
/** When true, competitor bar widths use stretched mapping so small score gaps read clearly. */
|
|
272
|
+
barStretch : { type: Boolean, attribute: "bar-stretch" },
|
|
273
|
+
/** Visual floor % for the weakest competitor (rest of span goes to 100% / Bitdefender). */
|
|
274
|
+
barFillMin : { type: Number, attribute: "bar-fill-min" },
|
|
275
|
+
/**
|
|
276
|
+
* Curve on normalized rank within [r_min, 1]. >1 pulls stronger competitors down vs Bitdefender
|
|
277
|
+
* (bigger BD gap); =1 is pure linear spread.
|
|
278
|
+
*/
|
|
279
|
+
barStretchGamma: { type: Number, attribute: "bar-stretch-gamma" },
|
|
280
|
+
/** Shown only in the screen-reader chart hint (scores are “out of” this scale). */
|
|
281
|
+
chartScale : { type: Number, attribute: "chart-scale" }
|
|
88
282
|
};
|
|
89
283
|
|
|
90
284
|
static styles = [tokens, compareCSS];
|
|
91
285
|
|
|
92
286
|
constructor() {
|
|
93
287
|
super();
|
|
94
|
-
|
|
95
|
-
this.
|
|
96
|
-
this.
|
|
288
|
+
const n = ++CompareCard._idSeq;
|
|
289
|
+
this._headingId = `compare-card-title-${n}`;
|
|
290
|
+
this._chartHelpId = `compare-card-chart-${n}`;
|
|
291
|
+
this.title = "";
|
|
292
|
+
this.description = "";
|
|
293
|
+
this.footnote = "";
|
|
97
294
|
this.footnoteHref = "";
|
|
98
|
-
this.iconSrc
|
|
295
|
+
this.iconSrc = "";
|
|
296
|
+
this.barStretch = true;
|
|
297
|
+
this.barFillMin = 50;
|
|
298
|
+
this.barStretchGamma = 1;
|
|
299
|
+
this.chartScale = 6;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
firstUpdated() {
|
|
303
|
+
this._syncBarStretch();
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
updated(changed) {
|
|
307
|
+
super.updated(changed);
|
|
308
|
+
if (
|
|
309
|
+
changed.has("barFillMin") ||
|
|
310
|
+
changed.has("barStretchGamma") ||
|
|
311
|
+
changed.has("barStretch")
|
|
312
|
+
) {
|
|
313
|
+
this._syncBarStretch();
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
_syncBarStretch() {
|
|
318
|
+
const slot = this.shadowRoot?.querySelector("slot");
|
|
319
|
+
if (!slot) return;
|
|
320
|
+
|
|
321
|
+
const apply = () => {
|
|
322
|
+
const nodes = slot.assignedElements({ flatten: true });
|
|
323
|
+
const bars = nodes.filter(
|
|
324
|
+
(el) => el.localName === "compare-bar"
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
if (!this.barStretch) {
|
|
328
|
+
this.style.setProperty("--cs-bar-stretch", "0");
|
|
329
|
+
this._refreshBars(bars);
|
|
330
|
+
return;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const primary = bars.find((b) => b.variant === "primary");
|
|
334
|
+
const ref = primary ? parseFloat(primary.score) : 0;
|
|
335
|
+
|
|
336
|
+
if (!ref || ref <= 0) {
|
|
337
|
+
this.style.setProperty("--cs-bar-stretch", "0");
|
|
338
|
+
this._refreshBars(bars);
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const secondaries = bars.filter((b) => b.variant !== "primary");
|
|
343
|
+
if (secondaries.length < 2) {
|
|
344
|
+
this.style.setProperty("--cs-bar-stretch", "0");
|
|
345
|
+
this._refreshBars(bars);
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
let rMin = 1;
|
|
350
|
+
for (const b of bars) {
|
|
351
|
+
if (b.variant === "primary") continue;
|
|
352
|
+
const sc = parseFloat(b.score);
|
|
353
|
+
if (!Number.isNaN(sc)) rMin = Math.min(rMin, sc / ref);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
if (rMin >= 1 - 1e-6) {
|
|
357
|
+
this.style.setProperty("--cs-bar-stretch", "0");
|
|
358
|
+
} else {
|
|
359
|
+
this.style.setProperty("--cs-bar-stretch", "1");
|
|
360
|
+
this.style.setProperty("--cs-stretch-r-min", String(rMin));
|
|
361
|
+
this.style.setProperty("--cs-bar-fill-min", String(this.barFillMin));
|
|
362
|
+
this.style.setProperty(
|
|
363
|
+
"--cs-bar-stretch-gamma",
|
|
364
|
+
String(this.barStretchGamma)
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
this._refreshBars(bars);
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
apply();
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
_refreshBars(bars) {
|
|
375
|
+
queueMicrotask(() => {
|
|
376
|
+
bars.forEach((b) => b.requestUpdate?.());
|
|
377
|
+
});
|
|
99
378
|
}
|
|
100
379
|
|
|
101
|
-
// Splits footnote text on "Source " and wraps everything after
|
|
102
|
-
// it in a bd-link. Falls back to plain text if no href given.
|
|
103
380
|
_renderFootnote() {
|
|
104
381
|
if (!this.footnote) return "";
|
|
105
382
|
|
|
@@ -111,7 +388,6 @@ class CompareCard extends LitElement {
|
|
|
111
388
|
const idx = this.footnote.indexOf(splitOn);
|
|
112
389
|
|
|
113
390
|
if (idx === -1) {
|
|
114
|
-
// No "Source " found — link the whole footnote text
|
|
115
391
|
return html`
|
|
116
392
|
<bd-p kind="xsmall" class="card-footnote">
|
|
117
393
|
<bd-link href="${this.footnoteHref}" target="_blank" font-size="12px" color="var(--color-neutral-900)" underline>
|
|
@@ -121,7 +397,7 @@ class CompareCard extends LitElement {
|
|
|
121
397
|
`;
|
|
122
398
|
}
|
|
123
399
|
|
|
124
|
-
const before
|
|
400
|
+
const before = this.footnote.slice(0, idx + splitOn.length);
|
|
125
401
|
const linkedText = this.footnote.slice(idx + splitOn.length);
|
|
126
402
|
|
|
127
403
|
return html`
|
|
@@ -135,17 +411,24 @@ class CompareCard extends LitElement {
|
|
|
135
411
|
const isMobile = window.matchMedia("(max-width: 767px)").matches;
|
|
136
412
|
|
|
137
413
|
return html`
|
|
138
|
-
<div
|
|
414
|
+
<div
|
|
415
|
+
class="card"
|
|
416
|
+
role="article"
|
|
417
|
+
aria-labelledby="${this._headingId}"
|
|
418
|
+
aria-describedby="${this._chartHelpId}"
|
|
419
|
+
>
|
|
420
|
+
<p class="cs-sr-only" id="${this._chartHelpId}">
|
|
421
|
+
Bar lengths are scaled for visibility between brands. Use the announced scores for exact values; scale is out of ${this.chartScale}.
|
|
422
|
+
</p>
|
|
139
423
|
<div class="card-header">
|
|
140
|
-
|
|
141
424
|
${this.iconSrc
|
|
142
425
|
? html`
|
|
143
426
|
<div class="card-icon-wrap" aria-hidden="true">
|
|
144
427
|
<bd-img
|
|
145
428
|
src="${this.iconSrc}"
|
|
146
429
|
alt=""
|
|
147
|
-
width="
|
|
148
|
-
height="
|
|
430
|
+
width="48"
|
|
431
|
+
height="48"
|
|
149
432
|
fit="contain"
|
|
150
433
|
radius="none"
|
|
151
434
|
shadow="none"
|
|
@@ -157,9 +440,8 @@ class CompareCard extends LitElement {
|
|
|
157
440
|
|
|
158
441
|
<div class="card-text-wrap">
|
|
159
442
|
${isMobile
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
}
|
|
443
|
+
? html`<bd-h as="h5" class="card-title" heading-id="${this._headingId}">${this.title}</bd-h>`
|
|
444
|
+
: html`<bd-h as="h4" class="card-title" heading-id="${this._headingId}">${this.title}</bd-h>`}
|
|
163
445
|
${this.description
|
|
164
446
|
? html`
|
|
165
447
|
<bd-p kind="${isMobile ? "small" : "regular"}" class="card-description">
|
|
@@ -168,11 +450,10 @@ class CompareCard extends LitElement {
|
|
|
168
450
|
`
|
|
169
451
|
: ""}
|
|
170
452
|
</div>
|
|
171
|
-
|
|
172
453
|
</div>
|
|
173
454
|
|
|
174
|
-
<div class="card-bars">
|
|
175
|
-
<slot></slot>
|
|
455
|
+
<div class="card-bars" role="group" aria-label="Scores by brand">
|
|
456
|
+
<slot @slotchange=${this._syncBarStretch}></slot>
|
|
176
457
|
</div>
|
|
177
458
|
|
|
178
459
|
${this._renderFootnote()}
|
|
@@ -183,20 +464,13 @@ class CompareCard extends LitElement {
|
|
|
183
464
|
|
|
184
465
|
customElements.define("compare-card", CompareCard);
|
|
185
466
|
|
|
186
|
-
|
|
187
467
|
// ═══════════════════════════════════════════════════════════════
|
|
188
468
|
// compare-section
|
|
189
|
-
// Attributes / Properties:
|
|
190
|
-
// title {String} – Section heading
|
|
191
|
-
// subtitle {String} – Optional subheading
|
|
192
|
-
// columns {Number} – Column count override (default: 2)
|
|
193
|
-
// gap {String} – Gap between cards e.g. "16px" or "var(--spacing-4)"
|
|
194
|
-
//
|
|
195
|
-
// Slots:
|
|
196
|
-
// (default) – <compare-card> elements
|
|
197
469
|
// ═══════════════════════════════════════════════════════════════
|
|
198
470
|
|
|
199
471
|
class CompareSection extends LitElement {
|
|
472
|
+
static _idSeq = 0;
|
|
473
|
+
|
|
200
474
|
static properties = {
|
|
201
475
|
title : { type: String },
|
|
202
476
|
subtitle: { type: String },
|
|
@@ -208,10 +482,15 @@ class CompareSection extends LitElement {
|
|
|
208
482
|
|
|
209
483
|
constructor() {
|
|
210
484
|
super();
|
|
211
|
-
this.
|
|
485
|
+
this._sectionTitleId = `bd-compare-section-title-${++CompareSection._idSeq}`;
|
|
486
|
+
this.title = "";
|
|
212
487
|
this.subtitle = "";
|
|
213
|
-
this.columns
|
|
214
|
-
this.gap
|
|
488
|
+
this.columns = 2;
|
|
489
|
+
this.gap = "";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
firstUpdated() {
|
|
493
|
+
scheduleCompareIntroDone();
|
|
215
494
|
}
|
|
216
495
|
|
|
217
496
|
render() {
|
|
@@ -221,16 +500,18 @@ class CompareSection extends LitElement {
|
|
|
221
500
|
: `--_cols: ${this.columns}`;
|
|
222
501
|
|
|
223
502
|
return html`
|
|
224
|
-
<section
|
|
503
|
+
<section
|
|
504
|
+
class="cs-section"
|
|
505
|
+
aria-labelledby="${this.title ? this._sectionTitleId : nothing}"
|
|
506
|
+
>
|
|
225
507
|
${this.title || this.subtitle
|
|
226
508
|
? html`
|
|
227
509
|
<div class="cs-heading">
|
|
228
510
|
${this.title
|
|
229
511
|
? html`
|
|
230
512
|
${isMobile
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
}
|
|
513
|
+
? html`<bd-h as="h4" class="cs-title" heading-id="${this._sectionTitleId}">${this.title}</bd-h>`
|
|
514
|
+
: html`<bd-h as="h3" class="cs-title" heading-id="${this._sectionTitleId}">${this.title}</bd-h>`}
|
|
234
515
|
`
|
|
235
516
|
: ""}
|
|
236
517
|
${this.subtitle
|