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