@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 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.24.19",
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.35",
80
- "@repobit/dex-store-elements": "2.1.5",
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": "09e86b2eb429cec7fadc15b6aa945152ab500a19"
97
+ "gitHead": "1e626106ffdf4df5443c8da6fa45fa4dae48a2c7"
98
98
  }
@@ -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
- if (!this._isVisible()) return;
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 = "start";
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
- clone.style.maxWidth = "48px";
231
- clone.style.maxHeight = "48px";
232
- clone.style.width = "48px";
233
- clone.style.height = "48px";
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 = "48px";
240
- innerImg.style.maxHeight = "48px";
241
- innerImg.style.width = "48px";
242
- innerImg.style.height = "48px";
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
- _hasContent() {
263
- const slottedNodes = [...this.childNodes].filter((node) => {
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
- return slottedNodes.length > 0;
372
+ _hasContent() {
373
+ return this._contentNodes().length > 0;
272
374
  }
273
375
 
274
376
  render() {
275
- const isCentered = this.align === "center";
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 ctaJustify = this.ctaAlign === "center"
381
+ const ctaAlign = this.ctaAlign || (isCentered ? "center" : "start");
382
+ const ctaJustify = ctaAlign === "center"
279
383
  ? "justify-content: center;"
280
- : this.ctaAlign === "end"
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 : 'start'
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
  };