vanilla-framework 4.49.0 → 4.51.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vanilla-framework",
3
- "version": "4.49.0",
3
+ "version": "4.51.0",
4
4
  "author": {
5
5
  "email": "webteam@canonical.com",
6
6
  "name": "Canonical Webteam"
@@ -4,15 +4,15 @@
4
4
  Content card:
5
5
  .p-content-card:
6
6
  Main element of the content card component.
7
- "&.p-content-card--2-cols":
7
+ "&.p-content-card--cols-2":
8
8
  Modifier for a 2-column width vertical layout. Applied ONLY to the parent .p-content-card.
9
- "&.p-content-card--4-cols":
9
+ "&.p-content-card--cols-4":
10
10
  Modifier for a 4-column width horizontal layout. Applied ONLY to the parent .p-content-card.
11
- "&.p-content-card--6-cols":
11
+ "&.p-content-card--cols-6":
12
12
  Modifier for a 6-column width horizontal layout. Applied ONLY to the parent .p-content-card.
13
- "&.p-content-card__has-image":
13
+ "&.has-image":
14
14
  State class applied to the parent when the card includes an image.
15
- "&.p-content-card__has-desc":
15
+ "&.has-desc":
16
16
  State class applied to the parent when the card includes a description that appears on hover.
17
17
  Links:
18
18
  .p-content-card__overlay-link:
@@ -54,72 +54,170 @@
54
54
 
55
55
  @import 'settings';
56
56
 
57
+ // =========================================
58
+ // CONTENT CARD VARIABLES
59
+ // =========================================
60
+ $card-height-vertical: 23.75rem; // 380px
61
+ $card-height-default: 12rem; // 192px
62
+
63
+ $card-image-width: 17.75rem; // 284px
64
+ // Horizontal image heights are explicit to preserve the tuned text/image balance at each breakpoint
65
+ $card-image-height-horizontal: 9.703125rem; // 155.25px
66
+ $card-image-height-horizontal-large: 9.984375rem; // 159.75px
67
+
68
+ // Horizontal card heights are image height plus vertical spacing budget.
69
+ $card-height-horizontal: $card-image-height-horizontal + $sp-x-large;
70
+ $card-height-horizontal-large: $card-image-height-horizontal-large + $sp-x-large;
71
+
72
+ $card-footer-outer-height: $sp-x-large + ($sp-small * 0.5);
73
+ $card-footer-inner-height: $sp-x-large - ($sp-small * 0.5);
74
+
75
+ $card-nudge-negative: map-get($settings-text-h4, nudge);
76
+ $card-chip-margin-right: 1.5625rem; // 25px
77
+ $card-author-padding-bottom: $sp-small;
78
+
79
+ $card-fallback-spacing-large: $sp-x-large;
80
+ $card-fallback-image-gap: $sp-small + $sp-xx-small;
81
+
82
+ @mixin mq-min($breakpoint) {
83
+ @media screen and (width >= $breakpoint) {
84
+ @content;
85
+ }
86
+ }
87
+
88
+ @mixin mq-max($breakpoint) {
89
+ @media screen and (width < $breakpoint) {
90
+ @content;
91
+ }
92
+ }
93
+
94
+ @mixin mq-between($min, $max) {
95
+ @media screen and ($min <= width < $max) {
96
+ @content;
97
+ }
98
+ }
99
+
57
100
  @mixin vf-p-content-card {
101
+ /* =========================================
102
+ GRID WRAPPER
103
+ (Handles spanning and invisible bottom spacing)
104
+ ========================================= */
105
+ .p-content-card-wrapper {
106
+ display: flex;
107
+ flex-direction: column;
108
+ height: 100%;
109
+ padding-bottom: var(--spacing-vertical-large, $card-fallback-spacing-large);
110
+ width: 100%;
111
+
112
+ /* 2-Column Spanning */
113
+ &--2 {
114
+ grid-column: 1 / -1; /* Mobile */
115
+ @include mq-between($breakpoint-small, $breakpoint-large) {
116
+ grid-column: span 2; /* Medium */
117
+ }
118
+ @include mq-min($breakpoint-large) {
119
+ grid-column: span 2; /* Desktop */
120
+ }
121
+ }
122
+
123
+ /* 4-Column Spanning */
124
+ &--4 {
125
+ grid-column: 1 / -1;
126
+ @include mq-min($breakpoint-large) {
127
+ grid-column: span 4;
128
+ }
129
+ }
130
+
131
+ /* 6-Column Spanning */
132
+ &--6 {
133
+ grid-column: 1 / -1;
134
+ @include mq-min($breakpoint-large) {
135
+ grid-column: span 6;
136
+ }
137
+ }
138
+ }
139
+
140
+ /* =========================================
141
+ CARD COMPONENT
142
+ ========================================= */
58
143
  .p-content-card {
59
144
  align-items: flex-start;
60
145
  border: 1px solid var(--color-border-low-contrast, rgba(0, 0, 0, 0.1));
61
146
  display: flex;
147
+ flex: 1;
62
148
  flex-direction: column;
63
- height: 23.75rem; /* 380px */
149
+ height: 100%;
64
150
  justify-content: space-between;
151
+ max-width: 100%;
152
+
153
+ min-height: $card-height-vertical;
65
154
  overflow: hidden;
66
155
  padding-top: var(--spacing-vertical-large, $sp-medium);
67
156
  position: relative;
68
157
  text-decoration: none !important;
69
158
  transition: all 0.4s ease-in-out;
70
- width: var(--size-width-grid-col-2, 17.75rem); /* 284px */
159
+ width: 100%;
71
160
 
72
- @media screen and (width >= $breakpoint-small) {
73
- &.p-content-card--4-cols,
74
- &.p-content-card--6-cols {
75
- align-items: flex-start;
76
- align-self: stretch;
77
- flex-direction: row;
78
- gap: $sp-medium; /* 16px */
79
- height: 11.703125rem; /* 187.25px */
80
- padding: $sp-medium $sp-medium 0;
81
- width: var(--size-width-grid-col-4, 37.5rem); /* 600px */
82
- }
161
+ &:not(.has-image) {
162
+ height: $card-height-default;
163
+ min-height: $card-height-default;
164
+ }
83
165
 
84
- &.p-content-card--4-cols:not(.has-image),
85
- &.p-content-card--6-cols:not(.has-image) {
86
- height: 12rem; /* 192px - Fixed height */
166
+ /* Shrink-to-fit for cards without footers on mobile */
167
+ @include mq-max($breakpoint-small) {
168
+ &:not(:has(.p-content-card__footer-outer)) {
169
+ height: auto;
170
+ min-height: 0;
171
+ padding-bottom: var(--spacing-vertical-large, $sp-medium); /* Keeps spacing balanced */
87
172
  }
173
+ }
88
174
 
89
- &.p-content-card--4-cols:not(.has-image) .p-rule--muted,
90
- &.p-content-card--6-cols:not(.has-image) .p-rule--muted {
91
- margin-left: -$sp-medium;
92
- margin-right: -$sp-medium;
93
- width: calc(100% + $sp-large); /* 32px */
175
+ /* 2-Column Modifier Internal Resets */
176
+ &.p-content-card--cols-2 {
177
+ @include mq-between($breakpoint-small, $breakpoint-large) {
178
+ .p-content-card__author-and-date {
179
+ display: none !important;
180
+ }
94
181
  }
95
182
  }
96
183
 
97
- @media screen and ($breakpoint-small <= width < $breakpoint-large) {
98
- &.p-content-card--2-cols {
184
+ /* 4 and 6 Column Horizontal Cards (Medium/Large Screens) */
185
+ @include mq-min($breakpoint-small) {
186
+ &.p-content-card--cols-4,
187
+ &.p-content-card--cols-6 {
99
188
  align-items: flex-start;
100
189
  align-self: stretch;
190
+
191
+ flex: 0 0 auto;
101
192
  flex-direction: row;
102
- gap: $sp-medium; /* 16px */
103
- height: 11.703125rem; /* 187.25px */
193
+ gap: $sp-medium;
194
+ height: $card-height-horizontal;
195
+
196
+ max-width: 100%;
197
+ min-height: $card-height-horizontal;
104
198
  padding: $sp-medium $sp-medium 0;
105
- width: var(--size-width-grid-col-4, 37.5rem); /* 600px */
106
199
  }
107
200
 
108
- &.p-content-card--2-cols:not(.has-image) {
109
- height: 10rem; /* 160px - Fixed height */
201
+ &.p-content-card--cols-4:not(.has-image),
202
+ &.p-content-card--cols-6:not(.has-image) {
203
+ height: $card-height-default;
204
+ min-height: $card-height-default;
110
205
  }
111
206
 
112
- &.p-content-card--2-cols:not(.has-image) .p-rule--muted {
207
+ &.p-content-card--cols-4:not(.has-image) .p-rule--muted,
208
+ &.p-content-card--cols-6:not(.has-image) .p-rule--muted {
113
209
  margin-left: -$sp-medium;
114
210
  margin-right: -$sp-medium;
115
- width: calc(100% + $sp-large); /* 32px */
211
+ max-width: none;
212
+ width: calc(100% + (#{$sp-medium} * 2));
116
213
  }
117
214
  }
118
215
 
119
- @media screen and (width >= $breakpoint-large) {
120
- &.p-content-card--6-cols {
121
- height: 11.984375rem; /* 191.75px */
122
- width: var(--size-width-grid-col-6, 57.25rem); /* 916px */
216
+ @include mq-min($breakpoint-large) {
217
+ &.p-content-card--cols-6 {
218
+ height: $card-height-horizontal-large;
219
+ max-width: 100%;
220
+ min-height: $card-height-horizontal-large;
123
221
  }
124
222
  }
125
223
 
@@ -139,6 +237,11 @@
139
237
  overflow: hidden;
140
238
  text-decoration: none !important;
141
239
 
240
+ .p-content-card--cols-6 & {
241
+ -webkit-line-clamp: 2;
242
+ line-clamp: 2;
243
+ }
244
+
142
245
  &:hover,
143
246
  &:focus,
144
247
  &:active,
@@ -174,9 +277,15 @@
174
277
  text-wrap: wrap;
175
278
  width: 100%;
176
279
 
177
- @media screen and (width >= $breakpoint-large) {
178
- .p-content-card--6-cols & {
179
- margin-top: -0.45rem !important;
280
+ .p-content-card--cols-6 & {
281
+ -webkit-line-clamp: 2;
282
+ line-clamp: 2;
283
+ max-height: 3em;
284
+ }
285
+
286
+ @include mq-min($breakpoint-large) {
287
+ .p-content-card--cols-6 & {
288
+ margin-top: $card-nudge-negative !important;
180
289
  }
181
290
  }
182
291
  }
@@ -198,16 +307,11 @@
198
307
  grid-template-columns: 1fr;
199
308
  grid-template-rows: 1fr;
200
309
  overflow: hidden;
201
- padding: 0 $sp-medium; /* 16px */
310
+ padding: 0 $sp-medium;
202
311
 
203
- @media screen and (width >= $breakpoint-small) {
204
- .p-content-card--4-cols &,
205
- .p-content-card--6-cols & {
206
- padding: 0;
207
- }
208
- }
209
- @media screen and ($breakpoint-small <= width < $breakpoint-large) {
210
- .p-content-card--2-cols & {
312
+ @include mq-min($breakpoint-small) {
313
+ .p-content-card--cols-4 &,
314
+ .p-content-card--cols-6 & {
211
315
  padding: 0;
212
316
  }
213
317
  }
@@ -241,30 +345,37 @@
241
345
  margin: 0;
242
346
  overflow: hidden;
243
347
 
244
- @media screen and (width >= $breakpoint-large) {
245
- .p-content-card--6-cols & {
246
- margin-top: -0.45rem !important;
348
+ .p-content-card--cols-6 & {
349
+ -webkit-line-clamp: 2;
350
+ line-clamp: 2;
351
+ }
352
+
353
+ @include mq-min($breakpoint-large) {
354
+ .p-content-card--cols-6 & {
355
+ margin-top: $card-nudge-negative !important;
247
356
  }
248
357
  }
249
358
  }
250
359
 
360
+ &__footer-container {
361
+ /* Structural grouping element.
362
+ Because the parent (.p-content-card__content) uses flexbox space-between,
363
+ this wrapper prevents the <hr> from floating awkwardly in the middle of the card.
364
+ */
365
+ }
366
+
251
367
  &__footer-outer {
252
368
  align-items: center;
253
369
  align-self: stretch;
254
370
  display: flex;
255
- height: 2.25rem; /* Fixed height: 36px */
371
+ height: $card-footer-outer-height;
256
372
  overflow: hidden;
257
373
  padding: 0 $sp-medium var(--spacing-vertical-small, $sp-small) $sp-medium;
258
374
  position: relative;
259
375
 
260
- @media screen and (width >= $breakpoint-small) {
261
- .p-content-card--4-cols &,
262
- .p-content-card--6-cols & {
263
- padding: 0 0 var(--spacing-vertical-small, $sp-small) 0;
264
- }
265
- }
266
- @media screen and ($breakpoint-small <= width < $breakpoint-large) {
267
- .p-content-card--2-cols & {
376
+ @include mq-min($breakpoint-small) {
377
+ .p-content-card--cols-4 &,
378
+ .p-content-card--cols-6 & {
268
379
  padding: 0 0 var(--spacing-vertical-small, $sp-small) 0;
269
380
  }
270
381
  }
@@ -273,8 +384,8 @@
273
384
  &__footer-inner {
274
385
  align-items: center;
275
386
  display: flex;
276
- gap: $sp-small; /* 8px */
277
- height: 1.75rem; /* 28px */
387
+ gap: $sp-small;
388
+ height: $card-footer-inner-height;
278
389
  mask-image: linear-gradient(to right, black 85%, transparent 100%);
279
390
  -ms-overflow-style: none;
280
391
  overflow-x: auto;
@@ -294,14 +405,14 @@
294
405
  }
295
406
 
296
407
  .p-chip--information {
297
- margin: 0 1.5625rem 0 0; /* 25px -> 1.5625rem */
408
+ margin: 0 $card-chip-margin-right 0 0;
298
409
  vertical-align: baseline;
299
410
  }
300
411
 
301
412
  .u-has-icon {
302
413
  align-items: center;
303
414
  display: flex;
304
- gap: $sp-x-small; /* 4px */
415
+ gap: $sp-x-small;
305
416
 
306
417
  .p-content-card__icon[class*='p-icon--'],
307
418
  .p-content-card__small {
@@ -318,24 +429,20 @@
318
429
  aspect-ratio: 16/9;
319
430
  display: block;
320
431
  flex-shrink: 0;
321
- height: 9.984375rem; /* 159.75px */
432
+ height: auto;
322
433
  object-fit: cover;
434
+ width: 100%;
323
435
 
324
- @media screen and (width >= $breakpoint-small) {
325
- .p-content-card--4-cols &,
326
- .p-content-card--6-cols & {
327
- height: 9.703125rem; /* 155.25px */
328
- }
329
- }
330
- @media screen and ($breakpoint-small <= width < $breakpoint-large) {
331
- .p-content-card--2-cols & {
332
- height: 9.703125rem; /* 155.25px */
436
+ @include mq-min($breakpoint-small) {
437
+ .p-content-card--cols-4 &,
438
+ .p-content-card--cols-6 & {
439
+ height: $card-image-height-horizontal;
333
440
  }
334
441
  }
335
442
 
336
- @media screen and (width >= $breakpoint-large) {
337
- .p-content-card--6-cols & {
338
- height: 9.984375rem; /* 159.75px */
443
+ @include mq-min($breakpoint-large) {
444
+ .p-content-card--cols-6 & {
445
+ height: $card-image-height-horizontal-large;
339
446
  }
340
447
  }
341
448
  }
@@ -344,21 +451,18 @@
344
451
  align-items: flex-start;
345
452
  display: flex;
346
453
  flex-direction: column;
347
- gap: var(--spacing-vertical-image-container, 0.625rem); /* 10px -> 0.625rem */
454
+ gap: var(--spacing-vertical-image-container, $card-fallback-image-gap);
455
+ max-width: 100%;
348
456
  padding-bottom: var(--spacing-vertical-large, $sp-medium);
349
- width: 17.75rem; /* 284px */
350
457
 
351
- @media screen and (width >= $breakpoint-small) {
352
- .p-content-card--4-cols &,
353
- .p-content-card--6-cols & {
354
- align-self: stretch;
355
- flex: 0 0 auto;
356
- }
357
- }
358
- @media screen and ($breakpoint-small <= width < $breakpoint-large) {
359
- .p-content-card--2-cols & {
458
+ width: 100%;
459
+
460
+ @include mq-min($breakpoint-small) {
461
+ .p-content-card--cols-4 &,
462
+ .p-content-card--cols-6 & {
360
463
  align-self: stretch;
361
464
  flex: 0 0 auto;
465
+ width: $card-image-width;
362
466
  }
363
467
  }
364
468
  }
@@ -367,61 +471,59 @@
367
471
  align-items: flex-start;
368
472
  align-self: stretch;
369
473
  display: flex;
474
+ padding-bottom: $card-author-padding-bottom;
370
475
 
371
476
  > :first-child {
372
477
  margin-bottom: 0 !important;
373
478
  }
374
479
 
375
- @media screen and ($breakpoint-small <= width < $breakpoint-large) {
376
- .p-content-card--2-cols &,
377
- .p-content-card--6-cols & {
480
+ @include mq-between($breakpoint-small, $breakpoint-large) {
481
+ .p-content-card--cols-6 & {
378
482
  display: none;
379
483
  }
380
484
  }
381
485
 
382
- @media screen and (width >= $breakpoint-large) {
383
- .p-content-card--6-cols & {
486
+ @include mq-min($breakpoint-large) {
487
+ .p-content-card--cols-6 & {
384
488
  display: flex;
385
- margin-top: -0.45rem;
386
489
  }
387
490
  }
388
491
  }
389
492
 
390
- &:not(.has-image) {
391
- height: 12.25rem;
392
- }
393
-
394
493
  &.has-image {
395
494
  padding-top: 0;
396
495
 
397
- @media screen and (width >= $breakpoint-small) {
398
- &.p-content-card--4-cols,
399
- &.p-content-card--6-cols {
400
- padding-top: $sp-medium; /* 16px */
496
+ @include mq-min($breakpoint-small) {
497
+ &.p-content-card--cols-4,
498
+ &.p-content-card--cols-6 {
499
+ padding-top: $sp-medium;
401
500
  }
402
501
  }
403
502
  }
404
503
 
405
- &:has(.p-content-card__main-link:focus-visible) {
406
- outline: 3px solid var(--color-focus, #0066cc);
407
- outline-offset: 2px;
408
- }
409
-
410
504
  &:hover,
411
505
  &:focus-within {
412
506
  border-color: var(--color-border-high-contrast, #707070);
413
507
  text-decoration: none;
508
+ }
414
509
 
415
- &.has-desc {
416
- .p-content-card__primary-content {
417
- opacity: 0;
418
- transform: translateY(-150px);
419
- }
420
- .p-content-card__hover-content {
421
- opacity: 1;
422
- transform: translateY(0);
423
- }
510
+ @include mq-min($breakpoint-large) {
511
+ &.has-desc:hover .p-content-card__primary-content,
512
+ &.has-desc:focus-within .p-content-card__primary-content {
513
+ opacity: 0;
514
+ transform: translateY(-150px);
515
+ }
516
+
517
+ &.has-desc:hover .p-content-card__hover-content,
518
+ &.has-desc:focus-within .p-content-card__hover-content {
519
+ opacity: 1;
520
+ transform: translateY(0);
424
521
  }
425
522
  }
523
+
524
+ &:has(.p-content-card__main-link:focus-visible) {
525
+ outline: 3px solid var(--color-focus, #0066cc);
526
+ outline-offset: 2px;
527
+ }
426
528
  }
427
529
  }
@@ -18,6 +18,10 @@
18
18
  Wraps contents in a container with an aspect ratio of 2.4:1.
19
19
  .p-image-container--square:
20
20
  Wraps contents in a container with an aspect ratio of 1:1.
21
+ .p-image-container--auto-height:
22
+ Stretches the container to the height of an adjacent grid column, clamped between a 16:9 minimum and a 2:3 maximum of the column width using CSS container queries.
23
+ .p-image-container--auto-height-on-(small|medium|large):
24
+ Auto-height variant applied only at the specified breakpoint.
21
25
  .p-image-container--(16-9|3-2|2-3|cinematic|square)-on-(small|medium|large):
22
26
  Wraps contents in a container with the specified aspect ratio on the specified breakpoint.
23
27
  Image:
@@ -37,6 +41,11 @@ $aspect-ratios: (
37
41
  'square': 1,
38
42
  );
39
43
 
44
+ // Auto-height clamp bounds, expressed as a percentage of the container's width.
45
+ // Upper bound matches the 2:3 (portrait) ratio; lower bound matches 16:9.
46
+ $image-container-auto-height-max: math.div(1, map-get($aspect-ratios, '2-3')) * 100cqw;
47
+ $image-container-auto-height-min: math.div(1, map-get($aspect-ratios, '16-9')) * 100cqw;
48
+
40
49
  @mixin apply-aspect-ratio-styles($padding-value) {
41
50
  &::before {
42
51
  content: '';
@@ -57,6 +66,19 @@ $aspect-ratios: (
57
66
  }
58
67
  }
59
68
 
69
+ @mixin apply-auto-height-styles {
70
+ container-type: inline-size;
71
+ height: 100%;
72
+ max-height: $image-container-auto-height-max;
73
+ min-height: $image-container-auto-height-min;
74
+
75
+ .p-image-container__image {
76
+ height: 100%;
77
+ object-fit: contain;
78
+ width: 100%;
79
+ }
80
+ }
81
+
60
82
  @mixin aspect-ratio-classes {
61
83
  @each $aspect-ratio, $aspect-ratio-value in $aspect-ratios {
62
84
  $padding-percentage: math.div(1, $aspect-ratio-value) * 100%;
@@ -65,6 +87,10 @@ $aspect-ratios: (
65
87
  }
66
88
  }
67
89
 
90
+ .p-image-container--auto-height {
91
+ @include apply-auto-height-styles;
92
+ }
93
+
68
94
  // Responsive aspect ratios
69
95
  @each $breakpoint-name, $breakpoint-bounds-pair in $breakpoint-bounds {
70
96
  $min-width: map-get($breakpoint-bounds-pair, min);
@@ -78,6 +104,9 @@ $aspect-ratios: (
78
104
  @include apply-aspect-ratio-styles($padding-percentage);
79
105
  }
80
106
  }
107
+ .p-image-container--auto-height-on-#{$breakpoint-name} {
108
+ @include apply-auto-height-styles;
109
+ }
81
110
  }
82
111
  } @else if $min-width {
83
112
  @media (width >= $min-width) {
@@ -87,6 +116,9 @@ $aspect-ratios: (
87
116
  @include apply-aspect-ratio-styles($padding-percentage);
88
117
  }
89
118
  }
119
+ .p-image-container--auto-height-on-#{$breakpoint-name} {
120
+ @include apply-auto-height-styles;
121
+ }
90
122
  }
91
123
  } @else if $max-width {
92
124
  @media (width < $max-width) {
@@ -96,6 +128,9 @@ $aspect-ratios: (
96
128
  @include apply-aspect-ratio-styles($padding-percentage);
97
129
  }
98
130
  }
131
+ .p-image-container--auto-height-on-#{$breakpoint-name} {
132
+ @include apply-auto-height-styles;
133
+ }
99
134
  }
100
135
  }
101
136
  }
@@ -58,7 +58,7 @@ $hover-background-opacity-percentage: $hover-background-opacity-amount * 100%;
58
58
  // NON-SEMANTIC COLOURS
59
59
  $color-label-validated: #006b75;
60
60
  $color-code-background: rgba($color-x-dark, 0.03);
61
- $color-code-background-dark: rgba($color-x-light, 0.3);
61
+ $color-code-background-dark: rgba($color-x-light, 0.15);
62
62
  $color-code-heading-background: rgba($color-x-dark, 0.08);
63
63
 
64
64
  // Background colours for form elements
@@ -22,80 +22,77 @@
22
22
 
23
23
  {%- macro vf_card(columns="2", link=None, heading=None, image=None, author=None, date=None, footer=None, description=None) -%}
24
24
 
25
- {%- set layout = 'p-content-card--' ~ columns ~ '-cols' -%}
25
+ {%- set col_str = columns | string -%}
26
+ {%- set layout = 'p-content-card--cols-' ~ col_str -%}
26
27
  {%- set img_class = 'has-image' if image else '' -%}
27
28
  {%- set has_desc = 'has-desc' if description else '' -%}
28
29
 
29
- <div class="p-content-card {{ layout }} {{ img_class }} {{ has_desc }}">
30
-
31
- {%- if link -%}
32
- <a href="{{ link }}" class="p-content-card__overlay-link" tabindex="-1" aria-hidden="true"></a>
33
- {%- endif -%}
30
+ <div class="p-content-card-wrapper p-content-card-wrapper--{{ col_str }}">
31
+
32
+ <div class="p-content-card {{ layout }} {{ img_class }} {{ has_desc }}">
33
+
34
+ {%- if link -%}
35
+ <a href="{{ link }}" class="p-content-card__overlay-link" tabindex="-1" aria-hidden="true"></a>
36
+ {%- endif -%}
34
37
 
35
- {%- if image -%}
36
- <div class="p-content-card__image-wrapper">
37
- <img class="p-content-card__image" src="{{ image.src }}" alt="{{ image.alt }}" />
38
- </div>
39
- {%- endif -%}
38
+ {%- if image -%}
39
+ <div class="p-content-card__image-wrapper">
40
+ <img class="p-content-card__image" src="{{ image.src }}" alt="{{ image.alt }}" />
41
+ </div>
42
+ {%- endif -%}
40
43
 
41
- <div class="p-content-card__content">
42
- <div class="p-content-card__body">
43
-
44
- <div class="p-content-card__primary-content">
45
- <h4 class="p-content-card__heading">
46
- {%- if link -%}
47
- <a href="{{ link }}" class="p-content-card__main-link">{{ heading }}</a>
48
- {%- else -%}
49
- {{ heading }}
50
- {%- endif -%}
51
- </h4>
44
+ <div class="p-content-card__content">
45
+ <div class="p-content-card__body">
52
46
 
53
- {%- if image and (author or date) -%}
54
- <div class="p-content-card__author-and-date u-sv-3">
55
- <small>
56
- {%- if author -%}
57
- <span class="u-text--muted">{{ author }}</span>
58
- {%- endif -%}
59
-
60
- {%- if author and date -%}
61
- &middot;
62
- {%- endif -%}
63
-
64
- {%- if date -%}
65
- <span class="u-text--muted">{{ date }}</span>
47
+ <div class="p-content-card__primary-content">
48
+ <h4 class="p-content-card__heading">
49
+ {%- if link -%}
50
+ <a href="{{ link }}" class="p-content-card__main-link">{{ heading }}</a>
51
+ {%- else -%}
52
+ {{ heading }}
66
53
  {%- endif -%}
67
- </small>
54
+ </h4>
55
+
56
+ {%- if image and (author or date) -%}
57
+ <div class="p-content-card__author-and-date u-sv-3">
58
+ <small>
59
+ {%- set items = [] -%}
60
+ {%- if author -%}{%- set _ = items.append('<span class="u-text--muted">' ~ author ~ '</span>') -%}{%- endif -%}
61
+ {%- if date -%}{%- set _ = items.append('<span class="u-text--muted">' ~ date ~ '</span>') -%}{%- endif -%}
62
+ {{ items | join('&nbsp;&middot;&nbsp;') | safe }}
63
+ </small>
64
+ </div>
65
+ {%- endif -%}
66
+ </div>
67
+
68
+ {%- if description -%}
69
+ <div class="p-content-card__hover-content">
70
+ <p class="p-content-card__description">{{ description }}</p>
68
71
  </div>
69
72
  {%- endif -%}
70
- </div>
71
73
 
72
- {%- if description -%}
73
- <div class="p-content-card__hover-content">
74
- <p class="p-content-card__description">{{ description }}</p>
75
74
  </div>
76
- {%- endif -%}
77
-
78
- </div>
79
-
80
- {%- if footer -%}
81
- <div>
82
- <hr class="p-rule--muted">
83
- <div class="p-content-card__footer-outer">
84
- <div class="p-content-card__footer-inner" tabindex="-1">
85
- {%- if footer.resource_type -%}
86
- <span class="u-has-icon">
87
- <i class="p-content-card__icon p-icon--{{ footer.resource_type.icon }}"></i>
88
- <small class="p-content-card__small">{{ footer.resource_type.text }}</small>
89
- </span>
90
- {%- endif -%}
75
+
76
+ {%- if footer -%}
77
+ <div class="p-content-card__footer-container">
78
+ <hr class="p-rule--muted">
79
+ <div class="p-content-card__footer-outer">
80
+ <div class="p-content-card__footer-inner" tabindex="-1">
81
+ {%- if footer.resource_type -%}
82
+ <span class="u-has-icon">
83
+ <i class="p-content-card__icon p-icon--{{ footer.resource_type.icon }}"></i>
84
+ <small class="p-content-card__small">{{ footer.resource_type.text }}</small>
85
+ </span>
86
+ {%- endif -%}
91
87
 
92
- {%- if footer.content_type %}
93
- <span class="p-chip--information is-readonly">{{ footer.content_type}}</span>
94
- {%- endif -%}
88
+ {%- if footer.content_type %}
89
+ <span class="p-chip--information is-readonly">{{ footer.content_type }}</span>
90
+ {%- endif -%}
91
+ </div>
95
92
  </div>
96
93
  </div>
94
+ {%- endif -%}
97
95
  </div>
98
- {%- endif -%}
99
96
  </div>
100
97
  </div>
101
98
  {%- endmacro -%}
@@ -1,127 +1,127 @@
1
+ {% from "_macros/vf_basic-section.jinja" import basic_section_title, vf_basic_section_blocks %}
2
+ {% from "_macros/shared/vf_section_top_rule.jinja" import vf_section_top_rule %}
3
+
1
4
  # Params
2
- # title_text (string) (required): Title of the rich vertical list
3
- # list_item_tick_style (string) (optional): Type of list item tick styling. Options are "bullet", "tick", "number".
4
- # is_flipped (boolean) (optional): Whether the list items are flipped so image is on the left and the text is on the right. Defaults to false.
5
- # Slots
6
- # description: Paragraph-style description content
7
- # logo_section Logo section block
8
- # list_item_[1-7]: List item content, assumed to be li.p-list__item
9
- # image (required)
5
+ # title (dict) (required): {text, link_attrs?} rendered via basic_section_title (always renders <h2>).
6
+ # items (array) (optional): Array of {type, item} dicts rendered via vf_basic_section_blocks.
7
+ # Supported types: description, list, code-block, logo-block, cta-block.
8
+ # Entries with any other type are silently dropped.
9
+ # media (dict) (required): Media column config. Keys:
10
+ # - type (string): "image" | "video". Defaults to "image".
11
+ # - ratio.large (string): "16-9" | "3-2" | "1-1" | "2-3" | "auto-height". Defaults to "3-2".
12
+ # - ratio.medium_small (string): "16-9" | "3-2" | "1-1". Defaults to "3-2".
13
+ # - fit (string): "cover" | "contain". Defaults to "cover".
14
+ # - attrs (dict): Passthrough HTML attributes for the <img> or <iframe>.
15
+ # is_flipped (bool) (optional): Swap content and media columns. Defaults to false.
16
+ # padding (string) (optional): "deep" | "shallow" | "default". Defaults to "default".
17
+ # top_rule_variant (string) (optional): "default" | "muted". Defaults to "default".
18
+ # attrs (dict) (optional): HTML attrs for the <section>.
10
19
  {% macro vf_rich_vertical_list(
11
- title_text,
12
- list_item_tick_style="",
13
- is_flipped=false
20
+ title={},
21
+ items=[],
22
+ media={},
23
+ is_flipped=false,
24
+ padding="default",
25
+ top_rule_variant="default",
26
+ attrs={}
14
27
  ) -%}
15
- {% set description_content = caller('description') %}
16
- {% set has_description = description_content|trim|length > 0 %}
17
- {% set logo_section_content = caller('logo_section') %}
18
- {% set has_logo_section = logo_section_content|trim|length > 0 %}
19
- {% set cta_content = caller('cta') %}
20
- {% set has_cta = cta_content|trim|length > 0 %}
21
- {% set has_list = caller('list_item_1')|trim|length > 0 %}
22
- {% set image_content = caller('image') %}
23
- {% set max_list_items = 7 %}
24
-
25
- {% set list_item_tick_style=list_item_tick_style|trim|lower %}
26
- {% if list_item_tick_style|length > 0 and list_item_tick_style not in ['bullet', 'tick', 'number'] %}
27
- {% set list_item_tick_style = '' %}
28
- {% endif %}
28
+ {#- Normalise & validate padding -#}
29
+ {%- set padding = padding | string | trim | lower -%}
30
+ {%- if padding not in ['deep', 'shallow', 'default'] -%}{%- set padding = 'default' -%}{%- endif -%}
31
+ {%- set padding_classes = 'p-section--' ~ padding -%}
32
+ {%- if padding == 'default' -%}{%- set padding_classes = 'p-section' -%}{%- endif -%}
29
33
 
30
- {% if list_item_tick_style == "bullet" %}
31
- {% set list_item_tick_class = "has-bullet" %}
32
- {% elif list_item_tick_style == "tick" %}
33
- {% set list_item_tick_class = "is-ticked" %}
34
- {% endif %}
34
+ {#- Normalise & validate top_rule_variant -#}
35
+ {%- set top_rule_variant = top_rule_variant | string | trim | lower -%}
36
+ {%- if top_rule_variant not in ['default', 'muted'] -%}{%- set top_rule_variant = 'default' -%}{%- endif -%}
35
37
 
36
- {% set list_element_type = "ul" %}
37
- {% if list_item_tick_style == "number" %}
38
- {% set list_element_type = "ol" %}
39
- {% endif %}
38
+ {#- Normalise & validate media config -#}
39
+ {%- set media_type = (media.get('type', 'image') | string | trim | lower) -%}
40
+ {%- if media_type not in ['image', 'video'] -%}{%- set media_type = 'image' -%}{%- endif -%}
40
41
 
41
- {#-
42
- Construct list of list items using caller in the top-level macro
43
- The _text_column_contents macro will not have access to the caller block, so we need to extract the list items here.
44
- -#}
45
- {% set list_items = [] %}
46
- {% if has_list %}
47
- {% for list_item_index in range(1, max_list_items + 1) %}
48
- {% set list_item_content = caller('list_item_' + list_item_index|string) %}
49
- {% set has_list_item_content = list_item_content|trim|length > 0 %}
50
- {% if has_list_item_content %}
51
- {{ list_items.append(list_item_content) or ""}}
52
- {% endif %}
53
- {% endfor %}
54
- {% endif %}
42
+ {%- set media_ratio = media.get('ratio', {}) -%}
43
+ {%- set media_ratio_large = (media_ratio.get('large', '3-2') | string | trim | lower) -%}
44
+ {%- set valid_large_ratios = ['16-9', '3-2', '1-1', '2-3', 'auto-height'] -%}
45
+ {%- if media_ratio_large not in valid_large_ratios -%}{%- set media_ratio_large = '3-2' -%}{%- endif -%}
55
46
 
56
- {%- macro _text_column_contents(list_items) %}
57
- {#- Mandatory title -#}
58
- <div class="p-section--shallow">
59
- <h2>{{ title_text }}</h2>
60
- </div>
61
-
62
- {%- if has_logo_section %}
63
- {#- Optional logo section -#}
64
- <div class="p-section--shallow">
65
- <div class="u-fixed-width">
66
- {{- logo_section_content -}}
67
- </div>
68
- </div>
69
- {%- endif -%}
47
+ {%- set media_ratio_medium_small = (media_ratio.get('medium_small', '3-2') | string | trim | lower) -%}
48
+ {#- 'auto-height' is intentionally excluded — it requires side-by-side columns, but medium/small layouts stack -#}
49
+ {%- set valid_medium_small_ratios = ['16-9', '3-2', '1-1'] -%}
50
+ {%- if media_ratio_medium_small not in valid_medium_small_ratios -%}{%- set media_ratio_medium_small = '3-2' -%}{%- endif -%}
70
51
 
71
- {%- if has_description %}
72
- {#- Optional description -#}
73
- <div class="p-section--shallow">
74
- {{- description_content -}}
75
- </div>
76
- {%- endif -%}
52
+ {%- set media_fit = (media.get('fit', 'cover') | string | trim | lower) -%}
53
+ {%- if media_fit not in ['cover', 'contain'] -%}{%- set media_fit = 'cover' -%}{%- endif -%}
77
54
 
78
- {%- if list_items|length > 0 %}
79
- {#- Optional list -#}
80
- <{{ list_element_type }} class="p-list--divided">
81
- {% for list_item in list_items %}
82
- <li class="p-list__item {{ list_item_tick_class }}">
83
- {{- list_item -}}
84
- </li>
85
- {% endfor %}
86
- </{{ list_element_type }}>
87
- {%- endif -%}
55
+ {%- set media_attrs = media.get('attrs', {}) -%}
56
+ {%- set is_auto_height = (media_ratio_large == 'auto-height') -%}
88
57
 
89
- {%- if has_cta %}
90
- {#- Optional CTA block -#}
91
- <div class="p-cta-block">
92
- {{- cta_content -}}
93
- </div>
94
- {%- endif -%}
58
+ {#- Constrain items to the curated allow-list. Disallowed types are silently dropped. -#}
59
+ {%- set allowed_item_types = ['description', 'list', 'code-block', 'logo-block', 'cta-block'] -%}
60
+ {%- set filtered_items = items | selectattr('type', 'in', allowed_item_types) | list -%}
95
61
 
62
+ {%- macro _rich_list_image(ratio_large, ratio_medium_small, fit, attrs) %}
63
+ {%- set is_cover = (fit == 'cover') -%}
64
+ {#- The image-container CSS uses 'square' for the 1:1 ratio class, not '1-1'. -#}
65
+ {%- set ratio_large_class = 'square' if ratio_large == '1-1' else ratio_large -%}
66
+ {%- set ratio_medium_small_class = 'square' if ratio_medium_small == '1-1' else ratio_medium_small -%}
67
+ {%- set classes = 'p-image-container--' ~ ratio_large_class ~ '-on-large' -%}
68
+ {%- set classes = classes ~ ' p-image-container--' ~ ratio_medium_small_class ~ '-on-medium' -%}
69
+ {%- set classes = classes ~ ' p-image-container--' ~ ratio_medium_small_class ~ '-on-small' -%}
70
+ {%- if is_cover -%}{%- set classes = classes ~ ' is-cover' -%}{%- endif -%}
71
+ <div class="{{ classes }}">
72
+ <img class="p-image-container__image{%- if 'class' in attrs %} {{ attrs['class'] }}{%- endif %}"
73
+ {%- for attr, value in attrs.items() -%}
74
+ {%- if attr != 'class' %} {{ attr }}="{{ value }}"{%- endif -%}
75
+ {%- endfor -%}
76
+ />
77
+ </div>
96
78
  {%- endmacro -%}
97
79
 
98
- {%- macro _image_column_contents() %}
99
- {#- Mandatory image -#}
100
- <div class="p-section--shallow">
101
- {{- image_content -}}
80
+ {%- macro _rich_list_video(attrs) %}
81
+ <div class="u-embedded-media">
82
+ <iframe class="u-embedded-media__element{%- if 'class' in attrs %} {{ attrs['class'] }}{%- endif %}"
83
+ {%- for attr, value in attrs.items() -%}
84
+ {%- if attr != 'class' %} {{ attr }}="{{ value }}"{%- endif -%}
85
+ {%- endfor -%}
86
+ ></iframe>
102
87
  </div>
103
88
  {%- endmacro -%}
104
89
 
105
- <div class="p-section">
106
- <div class="grid-row--50-50-on-large">
107
- <hr>
108
- {% if not is_flipped -%}
109
- <div class="grid-col">
110
- {{- _text_column_contents(list_items) -}}
111
- </div>
112
- <div class="grid-col">
113
- {{- _image_column_contents() -}}
114
- </div>
90
+ {%- macro _media_column_contents(
91
+ media_type, media_ratio_large, media_ratio_medium_small, media_fit, media_attrs, is_auto_height
92
+ ) -%}
93
+ {%- if is_auto_height -%}
94
+ <div>
95
+ {%- else -%}
96
+ <div class="p-section--shallow">
97
+ {%- endif -%}
98
+ {%- if media_type == 'video' -%}
99
+ {{ _rich_list_video(media_attrs) }}
115
100
  {%- else -%}
116
- {#- For flipped layout, place the image contents in the first column and the text in the second column -#}
117
- <div class="grid-col">
118
- {{- _image_column_contents() -}}
119
- </div>
120
- <div class="grid-col">
121
- {{- _text_column_contents(list_items) -}}
122
- </div>
101
+ {{ _rich_list_image(media_ratio_large, media_ratio_medium_small, media_fit, media_attrs) }}
123
102
  {%- endif -%}
124
103
  </div>
125
- </div>
104
+ {%- endmacro -%}
126
105
 
106
+ {%- macro _content_column_contents(title, items) -%}
107
+ {{ basic_section_title(title) }}
108
+ {{ vf_basic_section_blocks(items=items) }}
109
+ {%- endmacro -%}
110
+
111
+ <section class="{{ padding_classes }}{%- if 'class' in attrs %} {{ attrs['class'] }}{%- endif -%}"
112
+ {%- for attr, value in attrs.items() -%}
113
+ {%- if attr != 'class' %} {{ attr }}="{{ value }}"{%- endif -%}
114
+ {%- endfor -%}
115
+ >
116
+ <div class="grid-row--50-50-on-large">
117
+ {{ vf_section_top_rule(top_rule_variant) }}
118
+ {%- if not is_flipped -%}
119
+ <div class="grid-col">{{ _content_column_contents(title, filtered_items) }}</div>
120
+ <div class="grid-col">{{ _media_column_contents(media_type, media_ratio_large, media_ratio_medium_small, media_fit, media_attrs, is_auto_height) }}</div>
121
+ {%- else -%}
122
+ <div class="grid-col">{{ _media_column_contents(media_type, media_ratio_large, media_ratio_medium_small, media_fit, media_attrs, is_auto_height) }}</div>
123
+ <div class="grid-col">{{ _content_column_contents(title, filtered_items) }}</div>
124
+ {%- endif -%}
125
+ </div>
126
+ </section>
127
127
  {%- endmacro %}