vanilla-framework 4.50.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.50.0",
3
+ "version": "4.51.0",
4
4
  "author": {
5
5
  "email": "webteam@canonical.com",
6
6
  "name": "Canonical Webteam"
@@ -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
@@ -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 %}