@repobit/dex-system-design 0.23.11 → 0.23.13
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 +5 -2
- package/schemas/README.md +33 -0
- package/schemas/component-manifest.json +695 -0
- package/schemas/page-schema.json +128 -0
- package/src/components/compare/compare.css.js +30 -88
- package/src/components/compare/compare.js +165 -149
|
@@ -1,18 +1,6 @@
|
|
|
1
1
|
import { LitElement, html, nothing } from "lit";
|
|
2
2
|
import { tokens } from "../../tokens/tokens.js";
|
|
3
3
|
import compareCSS from "./compare.css.js";
|
|
4
|
-
// ═══════════════════════════════════════════════════════════════
|
|
5
|
-
// compare-bar
|
|
6
|
-
// Attributes / Properties:
|
|
7
|
-
// label {String} – Brand or product name
|
|
8
|
-
// score {Number} – Numeric score
|
|
9
|
-
// max-score {Number} – Max possible score (default: 6)
|
|
10
|
-
// variant {String} – "primary" | "secondary"
|
|
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
|
-
// ═══════════════════════════════════════════════════════════════
|
|
16
4
|
|
|
17
5
|
function readStretchVars(el) {
|
|
18
6
|
const styles = getComputedStyle(el);
|
|
@@ -24,32 +12,25 @@ function readStretchVars(el) {
|
|
|
24
12
|
};
|
|
25
13
|
}
|
|
26
14
|
|
|
27
|
-
const SCORE_ANIM_MS
|
|
15
|
+
const SCORE_ANIM_MS = 800;
|
|
28
16
|
const SCORE_ANIM_EASING = (t) => 1 - (1 - t) ** 3;
|
|
29
17
|
|
|
30
|
-
|
|
31
|
-
let compareIntroDone = false;
|
|
32
|
-
/** @type {ReturnType<typeof setTimeout> | 0} */
|
|
18
|
+
let compareIntroDone = false;
|
|
33
19
|
let compareIntroTimer = 0;
|
|
34
20
|
|
|
35
|
-
function shouldPlayCompareIntro() {
|
|
36
|
-
return !compareIntroDone;
|
|
37
|
-
}
|
|
21
|
+
function shouldPlayCompareIntro() { return !compareIntroDone; }
|
|
38
22
|
|
|
39
23
|
function scheduleCompareIntroDone() {
|
|
40
24
|
if (compareIntroDone || compareIntroTimer) return;
|
|
41
25
|
compareIntroTimer = setTimeout(() => {
|
|
42
26
|
compareIntroTimer = 0;
|
|
43
|
-
compareIntroDone
|
|
27
|
+
compareIntroDone = true;
|
|
44
28
|
}, SCORE_ANIM_MS + 120);
|
|
45
29
|
}
|
|
46
30
|
|
|
47
31
|
function prefersReducedMotion() {
|
|
48
|
-
try {
|
|
49
|
-
|
|
50
|
-
} catch {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
32
|
+
try { return window.matchMedia("(prefers-reduced-motion: reduce)").matches; }
|
|
33
|
+
catch { return false; }
|
|
53
34
|
}
|
|
54
35
|
|
|
55
36
|
if (typeof globalThis !== "undefined" && prefersReducedMotion()) {
|
|
@@ -72,16 +53,15 @@ class CompareBar extends LitElement {
|
|
|
72
53
|
|
|
73
54
|
constructor() {
|
|
74
55
|
super();
|
|
75
|
-
this.label
|
|
76
|
-
this.score
|
|
77
|
-
this.maxScore
|
|
78
|
-
this.variant
|
|
79
|
-
this.scoreLabel
|
|
56
|
+
this.label = "";
|
|
57
|
+
this.score = 0;
|
|
58
|
+
this.maxScore = 6;
|
|
59
|
+
this.variant = "secondary";
|
|
60
|
+
this.scoreLabel = "";
|
|
80
61
|
this.referenceScore = 0;
|
|
81
|
-
this.scale
|
|
62
|
+
this.scale = 1;
|
|
82
63
|
this._animatedScore = NaN;
|
|
83
|
-
|
|
84
|
-
this._scoreAnimRaf = 0;
|
|
64
|
+
this._scoreAnimRaf = 0;
|
|
85
65
|
}
|
|
86
66
|
|
|
87
67
|
connectedCallback() {
|
|
@@ -105,15 +85,7 @@ class CompareBar extends LitElement {
|
|
|
105
85
|
updated(changed) {
|
|
106
86
|
super.updated(changed);
|
|
107
87
|
this.setAttribute("aria-label", this._barAriaLabel());
|
|
108
|
-
const deps = [
|
|
109
|
-
"score",
|
|
110
|
-
"referenceScore",
|
|
111
|
-
"scoreLabel",
|
|
112
|
-
"variant",
|
|
113
|
-
"maxScore",
|
|
114
|
-
"label"
|
|
115
|
-
|
|
116
|
-
];
|
|
88
|
+
const deps = ["score", "referenceScore", "scoreLabel", "variant", "maxScore", "label"];
|
|
117
89
|
if (!deps.some((k) => changed.has(k))) return;
|
|
118
90
|
|
|
119
91
|
this._cancelScoreAnim();
|
|
@@ -132,15 +104,14 @@ class CompareBar extends LitElement {
|
|
|
132
104
|
|
|
133
105
|
_shouldAnimateCompetitorScore() {
|
|
134
106
|
if (this.variant === "primary" || this.scoreLabel) return false;
|
|
135
|
-
|
|
136
|
-
return Number.isFinite(t);
|
|
107
|
+
return Number.isFinite(parseFloat(this.score));
|
|
137
108
|
}
|
|
138
109
|
|
|
139
110
|
_scoreAnimStart() {
|
|
140
111
|
const target = parseFloat(this.score);
|
|
141
|
-
const ref
|
|
142
|
-
const maxM
|
|
143
|
-
let start
|
|
112
|
+
const ref = parseFloat(this.referenceScore);
|
|
113
|
+
const maxM = parseFloat(this.maxScore) || 6;
|
|
114
|
+
let start = ref > 0 ? ref : maxM;
|
|
144
115
|
if (start <= target) start = Math.min(maxM, target + 0.2);
|
|
145
116
|
return start;
|
|
146
117
|
}
|
|
@@ -154,26 +125,22 @@ class CompareBar extends LitElement {
|
|
|
154
125
|
|
|
155
126
|
_runScoreAnim() {
|
|
156
127
|
const target = parseFloat(this.score);
|
|
157
|
-
const start
|
|
128
|
+
const start = this._animatedScore;
|
|
158
129
|
if (!Number.isFinite(target) || !Number.isFinite(start)) return;
|
|
159
130
|
|
|
160
131
|
const t0 = performance.now();
|
|
161
|
-
|
|
162
132
|
const tick = (now) => {
|
|
163
|
-
const
|
|
164
|
-
const u = Math.min(1, elapsed / SCORE_ANIM_MS);
|
|
133
|
+
const u = Math.min(1, (now - t0) / SCORE_ANIM_MS);
|
|
165
134
|
const e = SCORE_ANIM_EASING(u);
|
|
166
|
-
|
|
167
|
-
this._animatedScore = v;
|
|
135
|
+
this._animatedScore = start + (target - start) * e;
|
|
168
136
|
if (u < 1) {
|
|
169
137
|
this._scoreAnimRaf = requestAnimationFrame(tick);
|
|
170
138
|
} else {
|
|
171
|
-
this._scoreAnimRaf
|
|
139
|
+
this._scoreAnimRaf = 0;
|
|
172
140
|
this._animatedScore = NaN;
|
|
173
141
|
this.requestUpdate();
|
|
174
142
|
}
|
|
175
143
|
};
|
|
176
|
-
|
|
177
144
|
this._scoreAnimRaf = requestAnimationFrame(tick);
|
|
178
145
|
}
|
|
179
146
|
|
|
@@ -183,7 +150,7 @@ class CompareBar extends LitElement {
|
|
|
183
150
|
|
|
184
151
|
const ref = parseFloat(this.referenceScore);
|
|
185
152
|
if (ref > 0) {
|
|
186
|
-
const r
|
|
153
|
+
const r = Math.min(1, Math.max(0, s / ref));
|
|
187
154
|
const linearPct = r * 100;
|
|
188
155
|
const { stretch, rMinStr, fillMin, gamma } = readStretchVars(this);
|
|
189
156
|
|
|
@@ -196,12 +163,12 @@ class CompareBar extends LitElement {
|
|
|
196
163
|
return Math.min(100, Math.max(0, linearPct));
|
|
197
164
|
}
|
|
198
165
|
|
|
199
|
-
const denom
|
|
200
|
-
const wMin
|
|
201
|
-
const g
|
|
202
|
-
const u
|
|
203
|
-
const t
|
|
204
|
-
const shaped
|
|
166
|
+
const denom = 1 - rMin;
|
|
167
|
+
const wMin = Number.isFinite(fillMin) ? fillMin : 50;
|
|
168
|
+
const g = Number.isFinite(gamma) && gamma > 0 ? gamma : 1;
|
|
169
|
+
const u = (r - rMin) / denom;
|
|
170
|
+
const t = Math.min(1, Math.max(0, u));
|
|
171
|
+
const shaped = Math.pow(t, g);
|
|
205
172
|
const stretched = wMin + shaped * (100 - wMin);
|
|
206
173
|
return Math.min(100, Math.max(0, stretched));
|
|
207
174
|
}
|
|
@@ -212,26 +179,20 @@ class CompareBar extends LitElement {
|
|
|
212
179
|
return rawPct * (this.scale || 1);
|
|
213
180
|
}
|
|
214
181
|
|
|
215
|
-
get _displayScore() {
|
|
216
|
-
return this.scoreLabel || this.score;
|
|
217
|
-
}
|
|
182
|
+
get _displayScore() { return this.scoreLabel || this.score; }
|
|
218
183
|
|
|
219
184
|
_renderScoreText() {
|
|
220
|
-
if (this.variant === "primary" || this.scoreLabel)
|
|
221
|
-
return this._displayScore;
|
|
222
|
-
}
|
|
185
|
+
if (this.variant === "primary" || this.scoreLabel) return this._displayScore;
|
|
223
186
|
if (Number.isFinite(this._animatedScore)) {
|
|
224
187
|
return CompareBar._formatScoreTick(this._animatedScore);
|
|
225
188
|
}
|
|
226
189
|
return this._displayScore;
|
|
227
190
|
}
|
|
228
191
|
|
|
229
|
-
/** @param {number} v */
|
|
230
192
|
static _formatScoreTick(v) {
|
|
231
193
|
return (Math.round(v * 100) / 100).toFixed(2);
|
|
232
194
|
}
|
|
233
195
|
|
|
234
|
-
/** Accessible name uses final numeric score (not the intro animation value). */
|
|
235
196
|
_barAriaLabel() {
|
|
236
197
|
const max = this.maxScore ?? 6;
|
|
237
198
|
const val = this.scoreLabel || this.score;
|
|
@@ -252,9 +213,6 @@ class CompareBar extends LitElement {
|
|
|
252
213
|
|
|
253
214
|
customElements.define("compare-bar", CompareBar);
|
|
254
215
|
|
|
255
|
-
// ═══════════════════════════════════════════════════════════════
|
|
256
|
-
// compare-card
|
|
257
|
-
// ═══════════════════════════════════════════════════════════════
|
|
258
216
|
|
|
259
217
|
class CompareCard extends LitElement {
|
|
260
218
|
static _idSeq = 0;
|
|
@@ -265,35 +223,28 @@ class CompareCard extends LitElement {
|
|
|
265
223
|
footnote : { type: String },
|
|
266
224
|
footnoteHref : { type: String, attribute: "footnote-href" },
|
|
267
225
|
iconSrc : { type: String, attribute: "icon-src" },
|
|
268
|
-
/** When true, competitor bar widths use stretched mapping so small score gaps read clearly. */
|
|
269
226
|
barStretch : { type: Boolean, attribute: "bar-stretch" },
|
|
270
|
-
|
|
271
|
-
|
|
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" }
|
|
227
|
+
barFillMin : { type: Number, attribute: "bar-fill-min" },
|
|
228
|
+
barStretchGamma: { type: Number, attribute: "bar-stretch-gamma" },
|
|
229
|
+
chartScale : { type: Number, attribute: "chart-scale" }
|
|
279
230
|
};
|
|
280
231
|
|
|
281
232
|
static styles = [tokens, compareCSS];
|
|
282
233
|
|
|
283
234
|
constructor() {
|
|
284
235
|
super();
|
|
285
|
-
const n
|
|
286
|
-
this._headingId
|
|
287
|
-
this._chartHelpId
|
|
288
|
-
this.title
|
|
289
|
-
this.description
|
|
290
|
-
this.footnote
|
|
291
|
-
this.footnoteHref
|
|
292
|
-
this.iconSrc
|
|
293
|
-
this.barStretch
|
|
294
|
-
this.barFillMin
|
|
236
|
+
const n = ++CompareCard._idSeq;
|
|
237
|
+
this._headingId = `compare-card-title-${n}`;
|
|
238
|
+
this._chartHelpId = `compare-card-chart-${n}`;
|
|
239
|
+
this.title = "";
|
|
240
|
+
this.description = "";
|
|
241
|
+
this.footnote = "";
|
|
242
|
+
this.footnoteHref = "";
|
|
243
|
+
this.iconSrc = "";
|
|
244
|
+
this.barStretch = true;
|
|
245
|
+
this.barFillMin = 50;
|
|
295
246
|
this.barStretchGamma = 1;
|
|
296
|
-
this.chartScale
|
|
247
|
+
this.chartScale = 6;
|
|
297
248
|
}
|
|
298
249
|
|
|
299
250
|
firstUpdated() {
|
|
@@ -316,10 +267,9 @@ class CompareCard extends LitElement {
|
|
|
316
267
|
if (!slot) return;
|
|
317
268
|
|
|
318
269
|
const apply = () => {
|
|
319
|
-
const
|
|
320
|
-
|
|
321
|
-
(el) => el.localName === "compare-bar"
|
|
322
|
-
);
|
|
270
|
+
const bars = slot
|
|
271
|
+
.assignedElements({ flatten: true })
|
|
272
|
+
.filter((el) => el.localName === "compare-bar");
|
|
323
273
|
|
|
324
274
|
if (!this.barStretch) {
|
|
325
275
|
this.style.setProperty("--cs-bar-stretch", "0");
|
|
@@ -328,7 +278,7 @@ class CompareCard extends LitElement {
|
|
|
328
278
|
}
|
|
329
279
|
|
|
330
280
|
const primary = bars.find((b) => b.variant === "primary");
|
|
331
|
-
const ref
|
|
281
|
+
const ref = primary ? parseFloat(primary.score) : 0;
|
|
332
282
|
|
|
333
283
|
if (!ref || ref <= 0) {
|
|
334
284
|
this.style.setProperty("--cs-bar-stretch", "0");
|
|
@@ -356,10 +306,7 @@ class CompareCard extends LitElement {
|
|
|
356
306
|
this.style.setProperty("--cs-bar-stretch", "1");
|
|
357
307
|
this.style.setProperty("--cs-stretch-r-min", String(rMin));
|
|
358
308
|
this.style.setProperty("--cs-bar-fill-min", String(this.barFillMin));
|
|
359
|
-
this.style.setProperty(
|
|
360
|
-
"--cs-bar-stretch-gamma",
|
|
361
|
-
String(this.barStretchGamma)
|
|
362
|
-
);
|
|
309
|
+
this.style.setProperty("--cs-bar-stretch-gamma", String(this.barStretchGamma));
|
|
363
310
|
}
|
|
364
311
|
|
|
365
312
|
this._refreshBars(bars);
|
|
@@ -374,6 +321,20 @@ class CompareCard extends LitElement {
|
|
|
374
321
|
});
|
|
375
322
|
}
|
|
376
323
|
|
|
324
|
+
/**
|
|
325
|
+
* Called by CompareSection to set a fixed min-height on .card-header
|
|
326
|
+
* so all headers in the same row are the same height.
|
|
327
|
+
*/
|
|
328
|
+
setHeaderMinHeight(px) {
|
|
329
|
+
const header = this.shadowRoot?.querySelector(".card-header");
|
|
330
|
+
if (header) header.style.minHeight = px != null ? `${px}px` : "";
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
getHeaderHeight() {
|
|
334
|
+
const header = this.shadowRoot?.querySelector(".card-header");
|
|
335
|
+
return header ? header.getBoundingClientRect().height : 0;
|
|
336
|
+
}
|
|
337
|
+
|
|
377
338
|
_renderFootnote() {
|
|
378
339
|
if (!this.footnote) return "";
|
|
379
340
|
|
|
@@ -381,8 +342,8 @@ class CompareCard extends LitElement {
|
|
|
381
342
|
return html`<bd-p kind="xsmall" class="card-footnote">${this.footnote}</bd-p>`;
|
|
382
343
|
}
|
|
383
344
|
|
|
384
|
-
const splitOn
|
|
385
|
-
const idx
|
|
345
|
+
const splitOn = "Source ";
|
|
346
|
+
const idx = this.footnote.indexOf(splitOn);
|
|
386
347
|
|
|
387
348
|
if (idx === -1) {
|
|
388
349
|
return html`
|
|
@@ -394,7 +355,7 @@ class CompareCard extends LitElement {
|
|
|
394
355
|
`;
|
|
395
356
|
}
|
|
396
357
|
|
|
397
|
-
const before
|
|
358
|
+
const before = this.footnote.slice(0, idx + splitOn.length);
|
|
398
359
|
const linkedText = this.footnote.slice(idx + splitOn.length);
|
|
399
360
|
|
|
400
361
|
return html`
|
|
@@ -417,34 +378,34 @@ class CompareCard extends LitElement {
|
|
|
417
378
|
<p class="cs-sr-only" id="${this._chartHelpId}">
|
|
418
379
|
Bar lengths are scaled for visibility between brands. Use the announced scores for exact values; scale is out of ${this.chartScale}.
|
|
419
380
|
</p>
|
|
381
|
+
|
|
420
382
|
<div class="card-header">
|
|
421
383
|
${this.iconSrc
|
|
422
384
|
? html`
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
385
|
+
<div class="card-icon-wrap" aria-hidden="true">
|
|
386
|
+
<bd-img
|
|
387
|
+
src="${this.iconSrc}"
|
|
388
|
+
alt=""
|
|
389
|
+
width="48"
|
|
390
|
+
height="48"
|
|
391
|
+
fit="contain"
|
|
392
|
+
radius="none"
|
|
393
|
+
shadow="none"
|
|
394
|
+
loading="eager"
|
|
395
|
+
></bd-img>
|
|
396
|
+
</div>
|
|
397
|
+
`
|
|
436
398
|
: ""}
|
|
437
|
-
|
|
438
399
|
<div class="card-text-wrap">
|
|
439
400
|
${isMobile
|
|
440
401
|
? html`<bd-h as="h5" class="card-title" heading-id="${this._headingId}">${this.title}</bd-h>`
|
|
441
402
|
: html`<bd-h as="h4" class="card-title" heading-id="${this._headingId}">${this.title}</bd-h>`}
|
|
442
403
|
${this.description
|
|
443
404
|
? html`
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
405
|
+
<bd-p kind="${isMobile ? "small" : "regular"}" class="card-description">
|
|
406
|
+
${this.description}
|
|
407
|
+
</bd-p>
|
|
408
|
+
`
|
|
448
409
|
: ""}
|
|
449
410
|
</div>
|
|
450
411
|
</div>
|
|
@@ -461,9 +422,6 @@ class CompareCard extends LitElement {
|
|
|
461
422
|
|
|
462
423
|
customElements.define("compare-card", CompareCard);
|
|
463
424
|
|
|
464
|
-
// ═══════════════════════════════════════════════════════════════
|
|
465
|
-
// compare-section
|
|
466
|
-
// ═══════════════════════════════════════════════════════════════
|
|
467
425
|
|
|
468
426
|
class CompareSection extends LitElement {
|
|
469
427
|
static _idSeq = 0;
|
|
@@ -480,14 +438,74 @@ class CompareSection extends LitElement {
|
|
|
480
438
|
constructor() {
|
|
481
439
|
super();
|
|
482
440
|
this._sectionTitleId = `bd-compare-section-title-${++CompareSection._idSeq}`;
|
|
483
|
-
this.title
|
|
484
|
-
this.subtitle
|
|
485
|
-
this.columns
|
|
486
|
-
this.gap
|
|
441
|
+
this.title = "";
|
|
442
|
+
this.subtitle = "";
|
|
443
|
+
this.columns = 2;
|
|
444
|
+
this.gap = "";
|
|
445
|
+
this._syncRaf = 0;
|
|
446
|
+
this._ro = null;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
connectedCallback() {
|
|
450
|
+
super.connectedCallback();
|
|
451
|
+
// ResizeObserver — re-sync whenever any card changes size
|
|
452
|
+
this._ro = new ResizeObserver(() => this._syncHeaderHeights());
|
|
453
|
+
this._ro.observe(this);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
disconnectedCallback() {
|
|
457
|
+
super.disconnectedCallback();
|
|
458
|
+
this._ro?.disconnect();
|
|
459
|
+
this._ro = null;
|
|
460
|
+
if (this._syncRaf) cancelAnimationFrame(this._syncRaf);
|
|
487
461
|
}
|
|
488
462
|
|
|
489
463
|
firstUpdated() {
|
|
490
464
|
scheduleCompareIntroDone();
|
|
465
|
+
// Initial sync after first paint
|
|
466
|
+
requestAnimationFrame(() => this._syncHeaderHeights());
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Reads all compare-card children, finds the tallest .card-header
|
|
471
|
+
* across all of them, then sets that height as minHeight on every card.
|
|
472
|
+
* Runs in a rAF to batch DOM reads before writes.
|
|
473
|
+
*/
|
|
474
|
+
_syncHeaderHeights() {
|
|
475
|
+
if (this._syncRaf) cancelAnimationFrame(this._syncRaf);
|
|
476
|
+
this._syncRaf = requestAnimationFrame(() => {
|
|
477
|
+
this._syncRaf = 0;
|
|
478
|
+
|
|
479
|
+
const isMobile = window.matchMedia("(max-width: 767px)").matches;
|
|
480
|
+
|
|
481
|
+
// Get all slotted compare-card elements
|
|
482
|
+
const slot = this.shadowRoot?.querySelector("slot");
|
|
483
|
+
if (!slot) return;
|
|
484
|
+
|
|
485
|
+
const cards = slot
|
|
486
|
+
.assignedElements({ flatten: true })
|
|
487
|
+
.filter((el) => el.localName === "compare-card");
|
|
488
|
+
|
|
489
|
+
if (!cards.length) return;
|
|
490
|
+
|
|
491
|
+
// On mobile reset all — no alignment needed
|
|
492
|
+
if (isMobile) {
|
|
493
|
+
cards.forEach((c) => c.setHeaderMinHeight?.(null));
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Reset first so we get natural heights
|
|
498
|
+
cards.forEach((c) => c.setHeaderMinHeight?.(null));
|
|
499
|
+
|
|
500
|
+
// Read heights after reset
|
|
501
|
+
requestAnimationFrame(() => {
|
|
502
|
+
const heights = cards.map((c) => c.getHeaderHeight?.() ?? 0);
|
|
503
|
+
const maxH = Math.max(...heights);
|
|
504
|
+
|
|
505
|
+
// Write — set all to tallest
|
|
506
|
+
cards.forEach((c) => c.setHeaderMinHeight?.(maxH));
|
|
507
|
+
});
|
|
508
|
+
});
|
|
491
509
|
}
|
|
492
510
|
|
|
493
511
|
render() {
|
|
@@ -503,27 +521,25 @@ class CompareSection extends LitElement {
|
|
|
503
521
|
>
|
|
504
522
|
${this.title || this.subtitle
|
|
505
523
|
? html`
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
:
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
</div>
|
|
522
|
-
`
|
|
524
|
+
<div class="cs-heading">
|
|
525
|
+
${this.title
|
|
526
|
+
? isMobile
|
|
527
|
+
? html`<bd-h as="h4" class="cs-title" heading-id="${this._sectionTitleId}">${this.title}</bd-h>`
|
|
528
|
+
: html`<bd-h as="h3" class="cs-title" heading-id="${this._sectionTitleId}">${this.title}</bd-h>`
|
|
529
|
+
: ""}
|
|
530
|
+
${this.subtitle
|
|
531
|
+
? html`
|
|
532
|
+
<bd-p kind="${isMobile ? "small" : "regular"}" class="cs-subtitle">
|
|
533
|
+
${this.subtitle}
|
|
534
|
+
</bd-p>
|
|
535
|
+
`
|
|
536
|
+
: ""}
|
|
537
|
+
</div>
|
|
538
|
+
`
|
|
523
539
|
: ""}
|
|
524
540
|
|
|
525
541
|
<div class="cs-grid" style="${gapStyle}">
|
|
526
|
-
<slot></slot>
|
|
542
|
+
<slot @slotchange=${this._syncHeaderHeights}></slot>
|
|
527
543
|
</div>
|
|
528
544
|
</section>
|
|
529
545
|
`;
|