@repobit/dex-system-design 0.23.5 → 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 +15 -0
- package/package.json +2 -2
- package/src/components/compare/compare.css.js +121 -75
- package/src/components/compare/compare.js +353 -68
- package/src/components/compare/compare.stories.js +358 -108
|
@@ -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,46 +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;
|
|
88
|
+
}
|
|
89
|
+
|
|
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);
|
|
35
181
|
}
|
|
36
182
|
|
|
37
183
|
get _pct() {
|
|
38
|
-
const s = parseFloat(this.score)
|
|
184
|
+
const s = parseFloat(this.score) || 0;
|
|
185
|
+
if (this.variant === "primary") return 100;
|
|
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
|
+
|
|
39
212
|
const m = parseFloat(this.maxScore) || 6;
|
|
40
213
|
if (m === 0) return 0;
|
|
41
|
-
|
|
214
|
+
const rawPct = Math.min(100, Math.max(0, (s / m) * 100));
|
|
215
|
+
return rawPct * (this.scale || 1);
|
|
42
216
|
}
|
|
43
217
|
|
|
44
218
|
get _displayScore() {
|
|
45
219
|
return this.scoreLabel || this.score;
|
|
46
220
|
}
|
|
47
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
|
+
|
|
48
244
|
render() {
|
|
49
245
|
return html`
|
|
50
246
|
<div class="bar-track">
|
|
51
|
-
<div class="bar-fill" style="width: ${this._pct}%"
|
|
52
|
-
|
|
53
|
-
<span class="bar-
|
|
54
|
-
<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>
|
|
55
250
|
</div>
|
|
56
251
|
</div>
|
|
57
252
|
`;
|
|
@@ -60,42 +255,128 @@ class CompareBar extends LitElement {
|
|
|
60
255
|
|
|
61
256
|
customElements.define("compare-bar", CompareBar);
|
|
62
257
|
|
|
63
|
-
|
|
64
258
|
// ═══════════════════════════════════════════════════════════════
|
|
65
259
|
// compare-card
|
|
66
|
-
// Attributes / Properties:
|
|
67
|
-
// title {String} – Card heading
|
|
68
|
-
// description {String} – Explanatory subtitle
|
|
69
|
-
// footnote {String} – Small source text at the bottom
|
|
70
|
-
// footnote-href {String} – URL for the linked part of the footnote
|
|
71
|
-
// icon-src {String} – Path to icon asset for bd-img
|
|
72
|
-
//
|
|
73
|
-
// Slots:
|
|
74
|
-
// (default) – <compare-bar> elements
|
|
75
260
|
// ═══════════════════════════════════════════════════════════════
|
|
76
261
|
|
|
77
262
|
class CompareCard extends LitElement {
|
|
263
|
+
static _idSeq = 0;
|
|
264
|
+
|
|
78
265
|
static properties = {
|
|
79
|
-
title
|
|
80
|
-
description
|
|
81
|
-
footnote
|
|
82
|
-
footnoteHref: { type: String, attribute: "footnote-href" },
|
|
83
|
-
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" }
|
|
84
282
|
};
|
|
85
283
|
|
|
86
284
|
static styles = [tokens, compareCSS];
|
|
87
285
|
|
|
88
286
|
constructor() {
|
|
89
287
|
super();
|
|
90
|
-
|
|
91
|
-
this.
|
|
92
|
-
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 = "";
|
|
93
294
|
this.footnoteHref = "";
|
|
94
|
-
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
|
+
});
|
|
95
378
|
}
|
|
96
379
|
|
|
97
|
-
// Splits footnote text on "Source " and wraps everything after
|
|
98
|
-
// it in a bd-link. Falls back to plain text if no href given.
|
|
99
380
|
_renderFootnote() {
|
|
100
381
|
if (!this.footnote) return "";
|
|
101
382
|
|
|
@@ -107,7 +388,6 @@ class CompareCard extends LitElement {
|
|
|
107
388
|
const idx = this.footnote.indexOf(splitOn);
|
|
108
389
|
|
|
109
390
|
if (idx === -1) {
|
|
110
|
-
// No "Source " found — link the whole footnote text
|
|
111
391
|
return html`
|
|
112
392
|
<bd-p kind="xsmall" class="card-footnote">
|
|
113
393
|
<bd-link href="${this.footnoteHref}" target="_blank" font-size="12px" color="var(--color-neutral-900)" underline>
|
|
@@ -117,7 +397,7 @@ class CompareCard extends LitElement {
|
|
|
117
397
|
`;
|
|
118
398
|
}
|
|
119
399
|
|
|
120
|
-
const before
|
|
400
|
+
const before = this.footnote.slice(0, idx + splitOn.length);
|
|
121
401
|
const linkedText = this.footnote.slice(idx + splitOn.length);
|
|
122
402
|
|
|
123
403
|
return html`
|
|
@@ -131,17 +411,24 @@ class CompareCard extends LitElement {
|
|
|
131
411
|
const isMobile = window.matchMedia("(max-width: 767px)").matches;
|
|
132
412
|
|
|
133
413
|
return html`
|
|
134
|
-
<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>
|
|
135
423
|
<div class="card-header">
|
|
136
|
-
|
|
137
424
|
${this.iconSrc
|
|
138
425
|
? html`
|
|
139
426
|
<div class="card-icon-wrap" aria-hidden="true">
|
|
140
427
|
<bd-img
|
|
141
428
|
src="${this.iconSrc}"
|
|
142
429
|
alt=""
|
|
143
|
-
width="
|
|
144
|
-
height="
|
|
430
|
+
width="48"
|
|
431
|
+
height="48"
|
|
145
432
|
fit="contain"
|
|
146
433
|
radius="none"
|
|
147
434
|
shadow="none"
|
|
@@ -153,22 +440,20 @@ class CompareCard extends LitElement {
|
|
|
153
440
|
|
|
154
441
|
<div class="card-text-wrap">
|
|
155
442
|
${isMobile
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
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>`}
|
|
159
445
|
${this.description
|
|
160
446
|
? html`
|
|
161
|
-
<bd-p kind="${isMobile ? "small" : "
|
|
447
|
+
<bd-p kind="${isMobile ? "small" : "regular"}" class="card-description">
|
|
162
448
|
${this.description}
|
|
163
449
|
</bd-p>
|
|
164
450
|
`
|
|
165
451
|
: ""}
|
|
166
452
|
</div>
|
|
167
|
-
|
|
168
453
|
</div>
|
|
169
454
|
|
|
170
|
-
<div class="card-bars">
|
|
171
|
-
<slot></slot>
|
|
455
|
+
<div class="card-bars" role="group" aria-label="Scores by brand">
|
|
456
|
+
<slot @slotchange=${this._syncBarStretch}></slot>
|
|
172
457
|
</div>
|
|
173
458
|
|
|
174
459
|
${this._renderFootnote()}
|
|
@@ -179,20 +464,13 @@ class CompareCard extends LitElement {
|
|
|
179
464
|
|
|
180
465
|
customElements.define("compare-card", CompareCard);
|
|
181
466
|
|
|
182
|
-
|
|
183
467
|
// ═══════════════════════════════════════════════════════════════
|
|
184
468
|
// compare-section
|
|
185
|
-
// Attributes / Properties:
|
|
186
|
-
// title {String} – Section heading
|
|
187
|
-
// subtitle {String} – Optional subheading
|
|
188
|
-
// columns {Number} – Column count override (default: 2)
|
|
189
|
-
// gap {String} – Gap between cards e.g. "16px" or "var(--spacing-4)"
|
|
190
|
-
//
|
|
191
|
-
// Slots:
|
|
192
|
-
// (default) – <compare-card> elements
|
|
193
469
|
// ═══════════════════════════════════════════════════════════════
|
|
194
470
|
|
|
195
471
|
class CompareSection extends LitElement {
|
|
472
|
+
static _idSeq = 0;
|
|
473
|
+
|
|
196
474
|
static properties = {
|
|
197
475
|
title : { type: String },
|
|
198
476
|
subtitle: { type: String },
|
|
@@ -204,10 +482,15 @@ class CompareSection extends LitElement {
|
|
|
204
482
|
|
|
205
483
|
constructor() {
|
|
206
484
|
super();
|
|
207
|
-
this.
|
|
485
|
+
this._sectionTitleId = `bd-compare-section-title-${++CompareSection._idSeq}`;
|
|
486
|
+
this.title = "";
|
|
208
487
|
this.subtitle = "";
|
|
209
|
-
this.columns
|
|
210
|
-
this.gap
|
|
488
|
+
this.columns = 2;
|
|
489
|
+
this.gap = "";
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
firstUpdated() {
|
|
493
|
+
scheduleCompareIntroDone();
|
|
211
494
|
}
|
|
212
495
|
|
|
213
496
|
render() {
|
|
@@ -217,16 +500,18 @@ class CompareSection extends LitElement {
|
|
|
217
500
|
: `--_cols: ${this.columns}`;
|
|
218
501
|
|
|
219
502
|
return html`
|
|
220
|
-
<section
|
|
503
|
+
<section
|
|
504
|
+
class="cs-section"
|
|
505
|
+
aria-labelledby="${this.title ? this._sectionTitleId : nothing}"
|
|
506
|
+
>
|
|
221
507
|
${this.title || this.subtitle
|
|
222
508
|
? html`
|
|
223
509
|
<div class="cs-heading">
|
|
224
510
|
${this.title
|
|
225
511
|
? html`
|
|
226
512
|
${isMobile
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
}
|
|
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>`}
|
|
230
515
|
`
|
|
231
516
|
: ""}
|
|
232
517
|
${this.subtitle
|