@repobit/dex-system-design 0.24.19 → 0.25.0
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 +14 -0
- package/package.json +4 -4
- package/src/assets/images/badges/app-store-badge.png +0 -0
- package/src/assets/images/badges/google-play-badge.png +0 -0
- package/src/components/cards/card.css.js +118 -0
- package/src/components/cards/card.js +123 -18
- package/src/components/cards/card.stories.js +153 -3
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,20 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
## [0.25.0](https://github.com/bitdefender/dex-core/compare/@repobit/dex-system-design@0.24.20...@repobit/dex-system-design@0.25.0) (2026-09-25)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
* **DEX-28731:** add align-rows subgrid option to card section
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
## [0.24.20](https://github.com/bitdefender/dex-core/compare/@repobit/dex-system-design@0.24.19...@repobit/dex-system-design@0.24.20) (2026-09-24)
|
|
14
|
+
|
|
15
|
+
### Bug Fixes
|
|
16
|
+
|
|
17
|
+
* **DEX-1014:** new adjustments for Card component
|
|
18
|
+
|
|
19
|
+
|
|
6
20
|
## [0.24.19](https://github.com/bitdefender/dex-core/compare/@repobit/dex-system-design@0.24.18...@repobit/dex-system-design@0.24.19) (2026-09-23)
|
|
7
21
|
|
|
8
22
|
### Bug Fixes
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@repobit/dex-system-design",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Design system based on Web Components.",
|
|
5
5
|
"author": "Iordache Matei Cezar <miordache@bitdefender.com>",
|
|
6
6
|
"homepage": "https://github.com/bitdefender/dex-core#readme",
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
"url": "https://github.com/bitdefender/dex-core/issues"
|
|
77
77
|
},
|
|
78
78
|
"dependencies": {
|
|
79
|
-
"@repobit/dex-store": "1.6.
|
|
80
|
-
"@repobit/dex-store-elements": "2.1.
|
|
79
|
+
"@repobit/dex-store": "1.6.37",
|
|
80
|
+
"@repobit/dex-store-elements": "2.1.7",
|
|
81
81
|
"lit": "^3.3.2"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
@@ -94,5 +94,5 @@
|
|
|
94
94
|
"volta": {
|
|
95
95
|
"node": "24.14.0"
|
|
96
96
|
},
|
|
97
|
-
"gitHead": "
|
|
97
|
+
"gitHead": "1e626106ffdf4df5443c8da6fa45fa4dae48a2c7"
|
|
98
98
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -50,6 +50,12 @@ export default css`
|
|
|
50
50
|
gap: var(--spacing-8);
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/* Outline-variant sections get extra top padding above the header,
|
|
54
|
+
per the reference design. */
|
|
55
|
+
:host([variant="outline"]) .bd-header-light-carousel-s {
|
|
56
|
+
padding-top: 80px;
|
|
57
|
+
}
|
|
58
|
+
|
|
53
59
|
.bd-light-carousel-s {
|
|
54
60
|
display: flex;
|
|
55
61
|
align-items: center;
|
|
@@ -95,6 +101,56 @@ export default css`
|
|
|
95
101
|
background: #E4F2FF !important;
|
|
96
102
|
}
|
|
97
103
|
|
|
104
|
+
/* === OUTLINE VARIANT — bordered, tinted, fully centered card. Set
|
|
105
|
+
variant="outline" on bd-card-section (propagates to items) or on an
|
|
106
|
+
individual bd-card-item to opt it in/out per-card. === */
|
|
107
|
+
.bd-light-box-outline {
|
|
108
|
+
background-color: var(--color-neutral-25);
|
|
109
|
+
border: 2px solid var(--color-blue-500);
|
|
110
|
+
border-radius: var(--radius-2xl, 1rem);
|
|
111
|
+
padding: 40px;
|
|
112
|
+
align-items: center !important;
|
|
113
|
+
justify-content: center !important;
|
|
114
|
+
text-align: center !important;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.bd-light-box-outline .bd-light-box-header-s {
|
|
118
|
+
align-items: center;
|
|
119
|
+
justify-content: center;
|
|
120
|
+
width: 100%;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.bd-light-box-outline .bd-light-title-badge-wrapper-s {
|
|
124
|
+
justify-content: center;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
.bd-light-box-outline .bd-card-title-group {
|
|
128
|
+
align-items: center;
|
|
129
|
+
text-align: center;
|
|
130
|
+
margin-top: 60px;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.bd-light-box-outline .bd-card-title-group bd-h {
|
|
134
|
+
color: var(--color-blue-500, #006dff);
|
|
135
|
+
text-align: center;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.bd-light-box-outline .bd-card-item-subtitle,
|
|
139
|
+
.bd-light-box-outline .bd-card-content-wrapper p,
|
|
140
|
+
.bd-light-box-outline .bd-card-content-wrapper ::slotted(*) {
|
|
141
|
+
color: var(--color-neutral-700);
|
|
142
|
+
text-align: center;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.bd-light-box-outline .bd-card-content-wrapper {
|
|
146
|
+
align-items: center;
|
|
147
|
+
text-align: center;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.bd-light-box-outline .bd-card-cta-wrapper {
|
|
151
|
+
justify-content: center;
|
|
152
|
+
}
|
|
153
|
+
|
|
98
154
|
.bd-light-box-center {
|
|
99
155
|
align-items: center !important;
|
|
100
156
|
justify-content: center !important;
|
|
@@ -204,6 +260,68 @@ export default css`
|
|
|
204
260
|
display: none;
|
|
205
261
|
}
|
|
206
262
|
|
|
263
|
+
/* === ALIGN ROWS — opt-in with align-rows on bd-card-section. The track is
|
|
264
|
+
a grid and every card is a subgrid spanning the same rows (header, title,
|
|
265
|
+
one per content node, CTA), so rows line up across cards even when their
|
|
266
|
+
content wraps differently. Row counts come from _syncAlignRows(). Row gaps
|
|
267
|
+
between tracks stay 0 so spacing inside cards is unchanged; spacing
|
|
268
|
+
between wrapped card rows comes from the item margin instead. === */
|
|
269
|
+
@media (min-width: 769px) {
|
|
270
|
+
:host([align-rows]) .bd-light-carousel-track-s {
|
|
271
|
+
display: grid;
|
|
272
|
+
grid-template-columns: repeat(var(--bd-card-cols), minmax(0, 1fr));
|
|
273
|
+
column-gap: 1.5rem;
|
|
274
|
+
row-gap: 0;
|
|
275
|
+
margin-bottom: -1.5rem;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
:host([align-rows]) ::slotted(bd-card-item) {
|
|
279
|
+
display: grid;
|
|
280
|
+
grid-template-rows: subgrid;
|
|
281
|
+
grid-row: span var(--bd-card-rows, 4);
|
|
282
|
+
width: auto;
|
|
283
|
+
min-width: 0;
|
|
284
|
+
margin: 0 0 1.5rem !important;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
:host([align-rows]) .bd-light-box-s {
|
|
288
|
+
display: grid;
|
|
289
|
+
grid-template-rows: subgrid;
|
|
290
|
+
grid-row: 1 / -1;
|
|
291
|
+
row-gap: 0;
|
|
292
|
+
/* center/outline boxes force flex centering with !important; in grid
|
|
293
|
+
those would center items vertically and shrink the column */
|
|
294
|
+
align-items: start !important;
|
|
295
|
+
justify-content: normal !important;
|
|
296
|
+
align-content: start;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
:host([align-rows]) .bd-light-box-header-s {
|
|
300
|
+
grid-row: 1;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
:host([align-rows]) .bd-light-title-badge-wrapper-s {
|
|
304
|
+
grid-row: 2;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
:host([align-rows]) .bd-card-content-wrapper {
|
|
308
|
+
display: grid;
|
|
309
|
+
grid-template-rows: subgrid;
|
|
310
|
+
grid-row: 3 / span var(--bd-card-content-rows, 1);
|
|
311
|
+
row-gap: 0;
|
|
312
|
+
align-items: start;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
:host([align-rows]) .bd-light-box-center .bd-card-content-wrapper,
|
|
316
|
+
:host([align-rows]) .bd-light-box-outline .bd-card-content-wrapper {
|
|
317
|
+
justify-items: center;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
:host([align-rows]) .bd-card-cta-wrapper {
|
|
321
|
+
grid-row: -2;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
207
325
|
|
|
208
326
|
@media (max-width: 768px) {
|
|
209
327
|
.bd-light-carousel-track-s {
|
|
@@ -9,6 +9,17 @@ class Card extends LitElement {
|
|
|
9
9
|
title : { type: String },
|
|
10
10
|
subtitle : { type: String },
|
|
11
11
|
bgBlue : { type: Boolean, reflect: true, attribute: "bg-blue" },
|
|
12
|
+
// "" (default — existing grey-box carousel look) | "outline" — bordered,
|
|
13
|
+
// centered cards with a light-blue tint, blue title and centered icon,
|
|
14
|
+
// matching the reference design. Propagated automatically to every
|
|
15
|
+
// slotted bd-card-item (see _onSlotChange) so callers only set it once,
|
|
16
|
+
// on the section, unless a specific item needs to opt out by setting
|
|
17
|
+
// its own variant attribute explicitly.
|
|
18
|
+
variant : { type: String, reflect: true },
|
|
19
|
+
// Opt-in: lays the cards out on a CSS grid and makes every card a subgrid,
|
|
20
|
+
// so icon, title, each content row and CTA line up across cards (desktop
|
|
21
|
+
// only). Replaces the JS height equalization for this section.
|
|
22
|
+
alignRows : { type: Boolean, reflect: true, attribute: "align-rows" },
|
|
12
23
|
_itemCount: { state: true }
|
|
13
24
|
};
|
|
14
25
|
|
|
@@ -17,8 +28,11 @@ class Card extends LitElement {
|
|
|
17
28
|
this.title = "";
|
|
18
29
|
this.subtitle = "";
|
|
19
30
|
this.bgBlue = false;
|
|
31
|
+
this.variant = "";
|
|
32
|
+
this.alignRows = false;
|
|
20
33
|
this._itemCount = 0;
|
|
21
34
|
this._resizeObserver = null;
|
|
35
|
+
this._mutationObserver = null;
|
|
22
36
|
}
|
|
23
37
|
|
|
24
38
|
_isVisible() {
|
|
@@ -47,15 +61,68 @@ class Card extends LitElement {
|
|
|
47
61
|
this.style.setProperty("--bd-card-width", `${cardW}px`);
|
|
48
62
|
}
|
|
49
63
|
|
|
64
|
+
// Propagates this.variant onto every slotted bd-card-item that didn't
|
|
65
|
+
// itself declare a variant attribute in its ORIGINAL markup. We can't use
|
|
66
|
+
// item.hasAttribute("variant") here — CardItem's own `variant` property
|
|
67
|
+
// has reflect:true, so Lit reflects its default "" onto the attribute on
|
|
68
|
+
// first render, meaning hasAttribute("variant") becomes true for EVERY
|
|
69
|
+
// item almost immediately, even ones the author never touched. Instead
|
|
70
|
+
// we rely on item._authorSetVariant, captured in CardItem's constructor
|
|
71
|
+
// (before Lit's own reflection has a chance to run) — see below.
|
|
72
|
+
_propagateVariant() {
|
|
73
|
+
if (!this.variant) return;
|
|
74
|
+
this.querySelectorAll("bd-card-item").forEach((item) => {
|
|
75
|
+
if (!item._authorSetVariant) {
|
|
76
|
+
item.variant = this.variant;
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Mirrors align-rows onto every item (their shadow styles key off it) and
|
|
82
|
+
// sizes the shared row tracks: header, title, the longest content list, CTA.
|
|
83
|
+
_syncAlignRows() {
|
|
84
|
+
const items = [...this.querySelectorAll("bd-card-item")];
|
|
85
|
+
items.forEach((item) => item.toggleAttribute("align-rows", this.alignRows));
|
|
86
|
+
if (!this.alignRows) return;
|
|
87
|
+
|
|
88
|
+
const contentRows = Math.max(0, ...items.map((item) => item._contentNodes?.().length ?? 0));
|
|
89
|
+
this.style.setProperty("--bd-card-content-rows", contentRows);
|
|
90
|
+
this.style.setProperty("--bd-card-rows", contentRows + 3);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
updated(changed) {
|
|
94
|
+
if (!changed.has("alignRows")) return;
|
|
95
|
+
this._syncAlignRows();
|
|
96
|
+
if (this.alignRows) this._clearEqualizedHeights();
|
|
97
|
+
else this._equalizeHeights();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Removes the inline heights written by _equalizeHeights (e.g. when
|
|
101
|
+
// align-rows is switched on after the section already rendered).
|
|
102
|
+
_clearEqualizedHeights() {
|
|
103
|
+
this.querySelectorAll("bd-card-item").forEach((item) => {
|
|
104
|
+
const root = item.renderRoot;
|
|
105
|
+
if (!root) return;
|
|
106
|
+
[".bd-light-box-s", ".bd-light-box-header-s", ".bd-light-title-badge-wrapper-s", ".bd-card-content-wrapper"]
|
|
107
|
+
.forEach((selector) => {
|
|
108
|
+
const el = root.querySelector(selector);
|
|
109
|
+
if (el) { el.style.height = ""; el.style.minHeight = ""; }
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
50
114
|
_onSlotChange = () => {
|
|
51
115
|
this._itemCount = this.querySelectorAll("bd-card-item").length;
|
|
116
|
+
this._propagateVariant();
|
|
117
|
+
this._syncAlignRows();
|
|
52
118
|
this._updateCardWidth();
|
|
53
119
|
this.requestUpdate();
|
|
54
120
|
this._equalizeHeights();
|
|
55
121
|
}
|
|
56
122
|
|
|
57
123
|
_equalizeHeights() {
|
|
58
|
-
|
|
124
|
+
// align-rows sizes cards with subgrid; inline heights would fight it
|
|
125
|
+
if (this.alignRows || !this._isVisible()) return;
|
|
59
126
|
|
|
60
127
|
const items = Array.from(this.querySelectorAll("bd-card-item"));
|
|
61
128
|
if (!items.length) return;
|
|
@@ -160,6 +227,8 @@ class Card extends LitElement {
|
|
|
160
227
|
}
|
|
161
228
|
|
|
162
229
|
await new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
|
|
230
|
+
this._propagateVariant();
|
|
231
|
+
this._syncAlignRows();
|
|
163
232
|
this._updateCardWidth();
|
|
164
233
|
this._equalizeHeights();
|
|
165
234
|
|
|
@@ -171,11 +240,17 @@ class Card extends LitElement {
|
|
|
171
240
|
});
|
|
172
241
|
this._resizeObserver.observe(track);
|
|
173
242
|
}
|
|
243
|
+
|
|
244
|
+
// Content added to or removed from an item changes its row count, which
|
|
245
|
+
// doesn't trigger slotchange on the section.
|
|
246
|
+
this._mutationObserver = new MutationObserver(() => this._syncAlignRows());
|
|
247
|
+
this._mutationObserver.observe(this, { childList: true, subtree: true });
|
|
174
248
|
}
|
|
175
249
|
|
|
176
250
|
disconnectedCallback() {
|
|
177
251
|
super.disconnectedCallback();
|
|
178
252
|
this._resizeObserver?.disconnect();
|
|
253
|
+
this._mutationObserver?.disconnect();
|
|
179
254
|
}
|
|
180
255
|
|
|
181
256
|
render() {
|
|
@@ -208,38 +283,61 @@ class CardItem extends LitElement {
|
|
|
208
283
|
icon : { type: String },
|
|
209
284
|
align : { type: String },
|
|
210
285
|
bgBlue : { type: Boolean, reflect: true, attribute: "bg-blue" },
|
|
211
|
-
ctaAlign: { type: String, attribute: "cta-align" }
|
|
286
|
+
ctaAlign: { type: String, attribute: "cta-align" },
|
|
287
|
+
// "" (default) | "outline" — see Card.variant above. Reflected so
|
|
288
|
+
// card.css.js can target it with :host([variant="outline"]).
|
|
289
|
+
variant : { type: String, reflect: true }
|
|
212
290
|
};
|
|
213
291
|
|
|
214
292
|
constructor() {
|
|
215
293
|
super();
|
|
294
|
+
// Captured BEFORE Lit's own property-to-attribute reflection runs (that
|
|
295
|
+
// happens on first update/render, not here). At this point the DOM
|
|
296
|
+
// parser has already set any attribute the author wrote in markup, so
|
|
297
|
+
// this reliably answers "did the author explicitly set variant on this
|
|
298
|
+
// element", independent of whatever default value we assign below.
|
|
299
|
+
this._authorSetVariant = this.hasAttribute("variant");
|
|
300
|
+
|
|
216
301
|
this.title = "";
|
|
217
302
|
this.subtitle = "";
|
|
218
303
|
this.icon = "";
|
|
219
304
|
this.align = "start";
|
|
220
305
|
this.bgBlue = false;
|
|
221
|
-
this.ctaAlign = "
|
|
306
|
+
this.ctaAlign = "";
|
|
307
|
+
this.variant = "";
|
|
222
308
|
}
|
|
223
309
|
|
|
224
310
|
_renderIcon() {
|
|
225
311
|
const iconEl = this.querySelector("[slot='icon']");
|
|
312
|
+
const isOutline = this.variant === "outline";
|
|
313
|
+
const iconW = isOutline ? 65 : 48;
|
|
314
|
+
const iconH = isOutline ? 75 : 48;
|
|
226
315
|
|
|
227
316
|
if (iconEl) {
|
|
228
317
|
const clone = iconEl.cloneNode(true);
|
|
229
318
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
clone.style.width
|
|
233
|
-
|
|
319
|
+
// The icon components (bd-globe-icon etc.) size their inner <svg>
|
|
320
|
+
// from their own width/height attributes, not from CSS on the host —
|
|
321
|
+
// setting only clone.style.width/height left the SVG's actual
|
|
322
|
+
// width="48" height="48" attributes untouched, so nothing visibly
|
|
323
|
+
// changed. Overwrite the attributes themselves for anything that
|
|
324
|
+
// looks like an icon component (custom element) or a raw <svg>.
|
|
325
|
+
clone.setAttribute("width", iconW);
|
|
326
|
+
clone.setAttribute("height", iconH);
|
|
327
|
+
|
|
328
|
+
clone.style.maxWidth = `${iconW}px`;
|
|
329
|
+
clone.style.maxHeight = `${iconH}px`;
|
|
330
|
+
clone.style.width = `${iconW}px`;
|
|
331
|
+
clone.style.height = `${iconH}px`;
|
|
234
332
|
clone.style.objectFit = "contain";
|
|
235
333
|
clone.style.display = "block";
|
|
236
334
|
|
|
237
335
|
const innerImg = clone.querySelector("img");
|
|
238
336
|
if (innerImg) {
|
|
239
|
-
innerImg.style.maxWidth =
|
|
240
|
-
innerImg.style.maxHeight =
|
|
241
|
-
innerImg.style.width =
|
|
242
|
-
innerImg.style.height =
|
|
337
|
+
innerImg.style.maxWidth = `${iconW}px`;
|
|
338
|
+
innerImg.style.maxHeight = `${iconH}px`;
|
|
339
|
+
innerImg.style.width = `${iconW}px`;
|
|
340
|
+
innerImg.style.height = `${iconH}px`;
|
|
243
341
|
innerImg.style.objectFit = "contain";
|
|
244
342
|
innerImg.style.display = "block";
|
|
245
343
|
innerImg.removeAttribute("width");
|
|
@@ -254,30 +352,36 @@ class CardItem extends LitElement {
|
|
|
254
352
|
src="${this.icon}"
|
|
255
353
|
alt="icon"
|
|
256
354
|
class="bd-light-icon-s"
|
|
355
|
+
style="width: ${iconW}px; height: ${iconH}px;"
|
|
257
356
|
/>`;
|
|
258
357
|
}
|
|
259
358
|
|
|
260
359
|
return "";
|
|
261
360
|
}
|
|
262
|
-
|
|
263
|
-
|
|
361
|
+
// Nodes rendered in the default slot — each one is a content row
|
|
362
|
+
_contentNodes() {
|
|
363
|
+
return [...this.childNodes].filter((node) => {
|
|
264
364
|
if (node.nodeType === Node.ELEMENT_NODE) {
|
|
265
365
|
const slot = node.getAttribute("slot");
|
|
266
366
|
return slot !== "icon" && slot !== "cta";
|
|
267
367
|
}
|
|
268
368
|
return node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== "";
|
|
269
369
|
});
|
|
370
|
+
}
|
|
270
371
|
|
|
271
|
-
|
|
372
|
+
_hasContent() {
|
|
373
|
+
return this._contentNodes().length > 0;
|
|
272
374
|
}
|
|
273
375
|
|
|
274
376
|
render() {
|
|
275
|
-
const
|
|
377
|
+
const isOutline = this.variant === "outline";
|
|
378
|
+
const isCentered = this.align === "center" || isOutline;
|
|
276
379
|
const hasContent = this._hasContent();
|
|
277
380
|
const hasCtaSlot = !!this.querySelector("[slot='cta']");
|
|
278
|
-
const
|
|
381
|
+
const ctaAlign = this.ctaAlign || (isCentered ? "center" : "start");
|
|
382
|
+
const ctaJustify = ctaAlign === "center"
|
|
279
383
|
? "justify-content: center;"
|
|
280
|
-
:
|
|
384
|
+
: ctaAlign === "end"
|
|
281
385
|
? "justify-content: flex-end;"
|
|
282
386
|
: "justify-content: flex-start;";
|
|
283
387
|
|
|
@@ -285,7 +389,8 @@ class CardItem extends LitElement {
|
|
|
285
389
|
<div
|
|
286
390
|
class="bd-light-box-s
|
|
287
391
|
${isCentered ? "bd-light-box-center" : ""}
|
|
288
|
-
${this.bgBlue ? "bd-light-box-blue" : ""}
|
|
392
|
+
${this.bgBlue ? "bd-light-box-blue" : ""}
|
|
393
|
+
${isOutline ? "bd-light-box-outline" : ""}"
|
|
289
394
|
>
|
|
290
395
|
<div class="bd-light-box-header-s ${isCentered ? "bd-light-box-header-center" : ""}">
|
|
291
396
|
${this._renderIcon()}
|
|
@@ -27,6 +27,20 @@ Grid columns are calculated automatically:
|
|
|
27
27
|
| 7–8 | 4 | 4+3, 4+4 |
|
|
28
28
|
| 9 | 3 | 3+3+3 |
|
|
29
29
|
| 10+ | 4 | 4+4+... |
|
|
30
|
+
|
|
31
|
+
### Variants
|
|
32
|
+
\`variant="outline"\` on \`bd-card-section\` switches every slotted \`bd-card-item\`
|
|
33
|
+
to a bordered, light-blue-tinted, centered card style (blue title, centered
|
|
34
|
+
icon/text) — set it once on the section, it propagates to all items
|
|
35
|
+
automatically. Leaving \`variant\` unset keeps the existing grey-box look.
|
|
36
|
+
An individual \`bd-card-item\` can opt out with \`variant=""\` explicitly.
|
|
37
|
+
|
|
38
|
+
### Align rows
|
|
39
|
+
\`align-rows\` on \`bd-card-section\` lays the cards out with CSS subgrid
|
|
40
|
+
(above 768px): icon, title, each content element and the CTA line up across
|
|
41
|
+
cards, even when one card's text wraps to more lines. It replaces the JS
|
|
42
|
+
height equalization for that section. Note: an incomplete last row is
|
|
43
|
+
left-aligned instead of centered.
|
|
30
44
|
`
|
|
31
45
|
}
|
|
32
46
|
}
|
|
@@ -243,6 +257,81 @@ export const WithCTA = {
|
|
|
243
257
|
parameters: { docs: { description: { story: 'Cards cu buton CTA via `slot="cta"`.' } } }
|
|
244
258
|
};
|
|
245
259
|
|
|
260
|
+
// ─── Align Rows ───────────────────────────────────────────────────────────────
|
|
261
|
+
|
|
262
|
+
const alignRowsIcons = [
|
|
263
|
+
html`<bd-individual-icon slot="icon" width="32" height="32" color="#006DFF"></bd-individual-icon>`,
|
|
264
|
+
html`<bd-globe-icon slot="icon" width="32" height="32" color="#006DFF"></bd-globe-icon>`,
|
|
265
|
+
html`<bd-machine-learning-icon slot="icon" width="32" height="32" color="#006DFF"></bd-machine-learning-icon>`,
|
|
266
|
+
html`<bd-analysis-icon slot="icon" width="32" height="32" color="#006DFF"></bd-analysis-icon>`
|
|
267
|
+
];
|
|
268
|
+
|
|
269
|
+
// Store badges rendered as linked pictures (a content row), like on product pages
|
|
270
|
+
const storeBadges = [
|
|
271
|
+
{ src: '/images/badges/google-play-badge.png', alt: 'Get it on Google Play' },
|
|
272
|
+
{ src: '/images/badges/app-store-badge.png', alt: 'Download on the App Store' }
|
|
273
|
+
];
|
|
274
|
+
|
|
275
|
+
const storeBadge = (n) => {
|
|
276
|
+
const badge = storeBadges[(n - 1) % storeBadges.length];
|
|
277
|
+
return html`
|
|
278
|
+
<a href="#" style="display: block;">
|
|
279
|
+
<img src="${badge.src}" alt="${badge.alt}" width="188" height="56" style="display: block; width: auto; height: 56px;" />
|
|
280
|
+
</a>
|
|
281
|
+
`;
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export const AlignRows = {
|
|
285
|
+
name: 'Align Rows (subgrid)',
|
|
286
|
+
args: {
|
|
287
|
+
alignRows : true,
|
|
288
|
+
align : 'center',
|
|
289
|
+
variant : '',
|
|
290
|
+
showCta : true,
|
|
291
|
+
showBadges: true,
|
|
292
|
+
card1Title: 'Antivirus Free for Android',
|
|
293
|
+
card1Text : 'Blocks malware on your phone.',
|
|
294
|
+
card1Extra: 'No cost, no catch.',
|
|
295
|
+
card2Title: 'Virus Scanner for Mac',
|
|
296
|
+
card2Text : 'Catches Mac and Windows malware alike, including threats hiding in email attachments and downloads.',
|
|
297
|
+
card2Extra: 'Free. Forever.',
|
|
298
|
+
card3Title: 'Security for iOS, iPhone and iPad devices',
|
|
299
|
+
card3Text : 'Web protection.',
|
|
300
|
+
card3Extra: 'Free for iPhone and iPad.',
|
|
301
|
+
card4Title: '',
|
|
302
|
+
card4Text : '',
|
|
303
|
+
card4Extra: ''
|
|
304
|
+
},
|
|
305
|
+
argTypes: {
|
|
306
|
+
alignRows : { control: 'boolean', description: '`align-rows` on bd-card-section — toggle to compare with the JS height equalization' },
|
|
307
|
+
align : { control: { type: 'select' }, options: ['start', 'center'], description: 'bd-card-item align' },
|
|
308
|
+
variant : { control: { type: 'select' }, options: ['', 'outline'], description: 'bd-card-section variant' },
|
|
309
|
+
showCta : { control: 'boolean', description: 'Render a CTA (slot="cta") in every card' },
|
|
310
|
+
showBadges: { control: 'boolean', description: 'Render a store badge (linked picture) as the last content row — Google Play / App Store alternating' },
|
|
311
|
+
...Object.fromEntries([1, 2, 3, 4].flatMap((n) => [
|
|
312
|
+
[`card${n}Title`, { control: 'text', table: { category: `Card ${n}` }, description: n === 4 ? 'Leave empty to hide card 4' : 'Card title' }],
|
|
313
|
+
[`card${n}Text`, { control: 'text', table: { category: `Card ${n}` }, description: 'First content row (bd-p)' }],
|
|
314
|
+
[`card${n}Extra`, { control: 'text', table: { category: `Card ${n}` }, description: 'Second content row (bd-p) — leave empty to omit' }]
|
|
315
|
+
]))
|
|
316
|
+
},
|
|
317
|
+
render: (args) => html`
|
|
318
|
+
<bd-card-section title="Also free" variant="${args.variant}" ?align-rows=${args.alignRows}>
|
|
319
|
+
${[1, 2, 3, 4]
|
|
320
|
+
.filter((n) => args[`card${n}Title`])
|
|
321
|
+
.map((n) => html`
|
|
322
|
+
<bd-card-item align="${args.align}" title="${args[`card${n}Title`]}">
|
|
323
|
+
${alignRowsIcons[n - 1]}
|
|
324
|
+
${args[`card${n}Text`] ? html`<bd-p>${args[`card${n}Text`]}</bd-p>` : ''}
|
|
325
|
+
${args[`card${n}Extra`] ? html`<bd-p>${args[`card${n}Extra`]}</bd-p>` : ''}
|
|
326
|
+
${args.showBadges ? storeBadge(n) : ''}
|
|
327
|
+
${args.showCta ? html`<bd-button-link slot="cta" href="#" kind="primary" size="sm">Get it free</bd-button-link>` : ''}
|
|
328
|
+
</bd-card-item>
|
|
329
|
+
`)}
|
|
330
|
+
</bd-card-section>
|
|
331
|
+
`,
|
|
332
|
+
parameters: { docs: { description: { story: 'With `align-rows`, each row (icon, title, every content element, CTA) has the same height across cards, via CSS subgrid. Use the controls to toggle it and change card content. Rows only align above 768px — widen the canvas.' } } }
|
|
333
|
+
};
|
|
334
|
+
|
|
246
335
|
// ─── No Slotted Content ───────────────────────────────────────────────────────
|
|
247
336
|
|
|
248
337
|
export const NoSlottedContent = {
|
|
@@ -324,6 +413,54 @@ export const MobileView = {
|
|
|
324
413
|
</bd-card-section>
|
|
325
414
|
` };
|
|
326
415
|
|
|
416
|
+
// ─── Outline Variant ────────────────────────────────────────────────────────
|
|
417
|
+
|
|
418
|
+
export const OutlineVariant = {
|
|
419
|
+
name : 'Outline Variant — Trusted For The AI Era',
|
|
420
|
+
render: () => html`
|
|
421
|
+
<bd-card-section
|
|
422
|
+
title="Trusted For The AI Era"
|
|
423
|
+
subtitle="Pioneering AI in Cybersecurity since 2008."
|
|
424
|
+
variant="outline"
|
|
425
|
+
>
|
|
426
|
+
<bd-card-item title="Transparency">
|
|
427
|
+
<bd-individual-icon slot="icon" width="48" height="48" color="#006DFF"></bd-individual-icon>
|
|
428
|
+
<bd-p>We provide clear information about how our AI makes security decisions and what data it uses, balancing technical transparency with usability.</bd-p>
|
|
429
|
+
</bd-card-item>
|
|
430
|
+
<bd-card-item title="Privacy by Design">
|
|
431
|
+
<bd-globe-icon slot="icon" width="48" height="48" color="#006DFF"></bd-globe-icon>
|
|
432
|
+
<bd-p>We implement privacy-preserving techniques like federated learning and differential privacy to minimize data exposure while maintaining AI effectiveness.</bd-p>
|
|
433
|
+
</bd-card-item>
|
|
434
|
+
<bd-card-item title="Human Guided AI Protection">
|
|
435
|
+
<bd-family-icon slot="icon" width="48" height="48" color="#006DFF"></bd-family-icon>
|
|
436
|
+
<bd-p>While our AI is powerful, we augment it with the deep expertise of Bitdefender Labs.</bd-p>
|
|
437
|
+
</bd-card-item>
|
|
438
|
+
</bd-card-section>
|
|
439
|
+
`,
|
|
440
|
+
parameters: { docs: { description: { story: '`variant="outline"` pe `bd-card-section` — se propagă automat la toate cardurile: contur albastru subțire, fundal albastru foarte deschis, titlu albastru, conținut centrat.' } } }
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
export const OutlineVariantMixed = {
|
|
444
|
+
name : 'Outline Variant — Mixed With Default',
|
|
445
|
+
render: () => html`
|
|
446
|
+
<bd-card-section title="Mixed Variants" variant="outline">
|
|
447
|
+
<bd-card-item title="Outline Card One">
|
|
448
|
+
<bd-individual-icon slot="icon" width="48" height="48" color="#006DFF"></bd-individual-icon>
|
|
449
|
+
<bd-p>This card inherits the outline variant from the section.</bd-p>
|
|
450
|
+
</bd-card-item>
|
|
451
|
+
<bd-card-item title="Opted-out Card" variant="">
|
|
452
|
+
<bd-globe-icon slot="icon" width="32" height="32" color="#006DFF"></bd-globe-icon>
|
|
453
|
+
<bd-p>This card explicitly sets variant="" to opt out and keep the default grey-box look.</bd-p>
|
|
454
|
+
</bd-card-item>
|
|
455
|
+
<bd-card-item title="Outline Card Two">
|
|
456
|
+
<bd-machine-learning-icon slot="icon" width="48" height="48" color="#006DFF"></bd-machine-learning-icon>
|
|
457
|
+
<bd-p>This card also inherits the outline variant.</bd-p>
|
|
458
|
+
</bd-card-item>
|
|
459
|
+
</bd-card-section>
|
|
460
|
+
`,
|
|
461
|
+
parameters: { docs: { description: { story: 'Un card poate seta `variant=""` explicit ca să rămână pe stilul default, chiar dacă secțiunea are `variant="outline"`.' } } }
|
|
462
|
+
};
|
|
463
|
+
|
|
327
464
|
// ─── Playground ───────────────────────────────────────────────────────────────
|
|
328
465
|
|
|
329
466
|
export const Playground = {
|
|
@@ -331,13 +468,26 @@ export const Playground = {
|
|
|
331
468
|
args: {
|
|
332
469
|
sectionTitle : 'Why Bitdefender',
|
|
333
470
|
sectionSubtitle: '',
|
|
471
|
+
sectionVariant : '',
|
|
334
472
|
itemTitle : 'AI Protection',
|
|
335
473
|
align : 'start',
|
|
336
474
|
bgBlue : false,
|
|
337
|
-
ctaAlign : '
|
|
475
|
+
ctaAlign : ''
|
|
476
|
+
},
|
|
477
|
+
argTypes: {
|
|
478
|
+
sectionVariant: {
|
|
479
|
+
control : { type: 'select' },
|
|
480
|
+
options : ['', 'outline'],
|
|
481
|
+
description: 'bd-card-section variant — propagates to all items'
|
|
482
|
+
},
|
|
483
|
+
ctaAlign: {
|
|
484
|
+
control : { type: 'select' },
|
|
485
|
+
options : ['', 'start', 'center', 'end'],
|
|
486
|
+
description: 'bd-card-item cta-align — empty follows align'
|
|
487
|
+
}
|
|
338
488
|
},
|
|
339
489
|
render: (args) => html`
|
|
340
|
-
<bd-card-section title="${args.sectionTitle}" subtitle="${args.sectionSubtitle}">
|
|
490
|
+
<bd-card-section title="${args.sectionTitle}" subtitle="${args.sectionSubtitle}" variant="${args.sectionVariant}">
|
|
341
491
|
<bd-card-item
|
|
342
492
|
title="${args.itemTitle}"
|
|
343
493
|
align="${args.align}"
|
|
@@ -360,5 +510,5 @@ export const Playground = {
|
|
|
360
510
|
</bd-card-item>
|
|
361
511
|
</bd-card-section>
|
|
362
512
|
`,
|
|
363
|
-
parameters: { docs: { description: { story: 'Playground interactiv
|
|
513
|
+
parameters: { docs: { description: { story: 'Playground interactiv — folosește `sectionVariant` din Controls ca să testezi `variant="outline"`.' } } }
|
|
364
514
|
};
|