vanilla-framework 4.34.2 → 4.36.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.34.2",
3
+ "version": "4.36.0",
4
4
  "author": {
5
5
  "email": "webteam@canonical.com",
6
6
  "name": "Canonical Webteam"
@@ -27,6 +27,9 @@
27
27
  ".": {
28
28
  "sass": "./_index.scss"
29
29
  },
30
+ "./scss/*": {
31
+ "sass": "./scss/*"
32
+ },
30
33
  "./js": "./templates/static/js/index.js",
31
34
  "./js/*": "./templates/static/js/*.js"
32
35
  },
@@ -67,7 +70,7 @@
67
70
  ],
68
71
  "devDependencies": {
69
72
  "@canonical/cookie-policy": "3.6.5",
70
- "@canonical/latest-news": "1.6.0",
73
+ "@canonical/latest-news": "2.1.0",
71
74
  "@percy/cli": "1.30.7",
72
75
  "@testing-library/cypress": "10.0.3",
73
76
  "autoprefixer": "10.4.20",
@@ -0,0 +1,120 @@
1
+ /*
2
+ @classreference
3
+ Article block:
4
+ .p-article-block:
5
+ Container for blog article content
6
+ .p-article-block__item:
7
+ Individual content block within article block
8
+ .p-article-block__image:
9
+ An image within an article block
10
+ .p-article-block__title:
11
+ The title of the article block
12
+ .p-article-block__metadata:
13
+ Metadata container
14
+ .p-article-block__metadata-item:
15
+ Individual metadata item with middot separator between items
16
+ */
17
+
18
+ @use 'sass:math';
19
+
20
+ @mixin vf-p-article-block {
21
+ // same as paragraph bottom margin.
22
+ // To simplify the logic of adding bottom margin to items only if they have content, we will use row-gap instead of margin-bottom.
23
+ $article-block-row-gap: map-get($settings-text-p, sp-after) - map-get($settings-text-p, nudge);
24
+
25
+ .p-article-block {
26
+ display: grid;
27
+ // image, title, desc/excerpt, metadata
28
+ grid-row: span 4;
29
+ grid-template-rows: subgrid;
30
+ margin-bottom: #{$spv--small + $article-block-row-gap};
31
+ row-gap: $article-block-row-gap;
32
+
33
+ // the section that wraps the items already has bottom padding defined, so the last row of items must have collapsed bottom margin to avoid duplicating spacing.
34
+ // small screens - the last item gets collapsed bottom margin
35
+ @media screen and (width < $threshold-4-small-4-med-col) {
36
+ &:last-child {
37
+ margin-bottom: 0;
38
+ }
39
+ }
40
+
41
+ // medium screens - the last two items get collapsed bottom margin
42
+ @media screen and ($threshold-4-small-4-med-col <= width < $threshold-4-8-col) {
43
+ &:nth-last-child(-n + 2) {
44
+ margin-bottom: 0;
45
+ }
46
+ }
47
+
48
+ // large screen - all items get collapsed bottom margin
49
+ @media screen and (width >= $threshold-4-8-col) {
50
+ margin-bottom: 0;
51
+ }
52
+ }
53
+
54
+ /*
55
+ Article block items and their descendants by default don't get any margin or padding.
56
+ Article block items are always rendered (we need to keep subgrid alignment, and we don't know at request time whether the content is going to be there or not due to dynamic content).
57
+ So, we by default remove all margin and padding. Then, further style rules can query for presence of content and add appropriate margins and padding.
58
+ */
59
+ .p-article-block__item {
60
+ display: contents;
61
+
62
+ &:not(:last-child) {
63
+ // empty items pull their following contents up by canceling the row-gap.
64
+ margin-bottom: -#{$article-block-row-gap};
65
+ }
66
+
67
+ // Only apply article block spacing styles to contentful items
68
+ &:has(.p-article-block__title:not(:empty)),
69
+ &:has(.p-article-block__description:not(:empty)),
70
+ &:has(.article-author:not(:empty)),
71
+ &:has(.article-time:not(:empty)) {
72
+ display: grid;
73
+ font-size: #{map-get($settings-text-p, font-size)}rem;
74
+ line-height: map-get($settings-text-p, line-height);
75
+ // cancel out the row-gap cancellation if the item has contents.
76
+ margin-bottom: 0;
77
+ padding-top: map-get($settings-text-p, nudge);
78
+ }
79
+
80
+ // Apply all spacing at the item level.
81
+ & * {
82
+ margin-bottom: 0;
83
+ margin-top: 0;
84
+ padding-bottom: 0;
85
+ padding-top: 0;
86
+ }
87
+ }
88
+
89
+ .p-article-block__title {
90
+ font-size: #{map-get($settings-text-p, font-size)}rem;
91
+ line-height: map-get($settings-text-p, line-height);
92
+ }
93
+
94
+ /**
95
+ Article block images are an exception to the above - they get small bottom margin, not paragraph styling.
96
+ */
97
+ .p-article-block__item:has(.p-article-block__image) {
98
+ display: grid;
99
+ // we want $spv--small below the images - but we are also using `row-gap`. We can create this effective spacing by subtracting the desired bottom margin from the row gap.
100
+ margin-bottom: -#{$article-block-row-gap - $spv--small};
101
+ }
102
+
103
+ /*
104
+ Add a middot between non-empty metadata items
105
+ The metadata-items are wrappers around child elements that are **always** rendered, so that dynamic content has slots to populate items into.
106
+ We don't know at build time if there will be any content.
107
+ So, we use :has() to target `p-article-block__metadata-item` elements that have at least one non-empty child element, without having to target any specific child selectors here.
108
+ Note that this requires careful management of whitespace within the metadata items. Extra whitespace can cause elements with no children to be considered non-empty and unnecessarily render a middot.
109
+ */
110
+ .p-article-block__metadata-item:has(> *:not(:empty)) {
111
+ @include vf-inline-list-item;
112
+
113
+ margin-right: $sph--x-small;
114
+
115
+ & ~ &::before {
116
+ content: '\2022';
117
+ position: relative;
118
+ }
119
+ }
120
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ @classreference
3
+ blog:
4
+ Blog:
5
+ .p-blog:
6
+ Main element of the blog pattern.
7
+ .p-blog__articles:
8
+ Container for blog articles.
9
+ */
10
+
11
+ @mixin vf-p-blog {
12
+ .p-blog__articles {
13
+ display: contents;
14
+ }
15
+ }
@@ -0,0 +1,21 @@
1
+ @import 'settings';
2
+
3
+ $col-25: calc($grid-8-columns / 4);
4
+ $col-50: calc($grid-8-columns / 2);
5
+
6
+ @mixin vf-p-resources-block {
7
+ .p-resources-block {
8
+ > .p-resources-block__col {
9
+ @media (min-width: $threshold-4-8-col) {
10
+ &:nth-of-type(1) {
11
+ @include vf-grid-8-column($col-25);
12
+ }
13
+
14
+ &:nth-of-type(2) {
15
+ grid-column-start: calc($col-25 + 1);
16
+ @include vf-grid-8-column($col-50);
17
+ }
18
+ }
19
+ }
20
+ }
21
+ }
@@ -3,8 +3,10 @@
3
3
  // Patterns
4
4
 
5
5
  @import 'patterns_accordion';
6
+ @import 'patterns_article-block';
6
7
  @import 'patterns_article-pagination';
7
8
  @import 'patterns_badge';
9
+ @import 'patterns_blog';
8
10
  @import 'patterns_breadcrumbs';
9
11
  @import 'patterns_buttons';
10
12
  @import 'patterns_card';
@@ -43,6 +45,7 @@
43
45
  @import 'patterns_pricing-block';
44
46
  @import 'patterns_newsletter-signup';
45
47
  @import 'patterns_pull-quotes';
48
+ @import 'patterns_resources-block';
46
49
  @import 'patterns_rule';
47
50
  @import 'patterns_search-and-filter';
48
51
  @import 'patterns_search-box';
@@ -102,8 +105,10 @@
102
105
  @include vf-base;
103
106
  // Patterns
104
107
  @include vf-p-accordion;
108
+ @include vf-p-article-block;
105
109
  @include vf-p-article-pagination;
106
110
  @include vf-p-badge;
111
+ @include vf-p-blog;
107
112
  @include vf-p-breadcrumbs;
108
113
  @include vf-p-buttons;
109
114
  @include vf-p-card;
@@ -143,6 +148,7 @@
143
148
  @include vf-p-pagination;
144
149
  @include vf-p-pricing-block;
145
150
  @include vf-p-pull-quotes;
151
+ @include vf-p-resources-block;
146
152
  @include vf-p-rule;
147
153
  @include vf-p-search-and-filter;
148
154
  @include vf-p-search-box;
@@ -0,0 +1,187 @@
1
+ {#
2
+ Helper macro to render the article title with optional link and attributes.
3
+
4
+ Parameters:
5
+ title (object): Title configuration
6
+ text (string): The title text
7
+ link_attrs (object) (optional): Link attributes. If passed, wraps the title in a link.
8
+ attrs (object) (optional): Additional HTML attributes
9
+ heading_level (integer) (optional): Heading level to use, defaults to 3. Can be 3 or 4.
10
+ template_mode (boolean) (optional): Whether this is being rendered in template mode
11
+ #}
12
+ {%- macro _article_title(title={}, template_mode=False) -%}
13
+ {%- set link_attrs = title.get("link_attrs", {}) -%}
14
+ {%- set heading_level = title.get("heading_level", 3) -%}
15
+ {%- if heading_level not in [3, 4] -%}
16
+ {%- set heading_level = 3 -%}
17
+ {%- endif -%}
18
+ {#- Assume blog module always sets a link in template mode. -#}
19
+ {%- set is_link = link_attrs.items() | length > 0 or template_mode -%}
20
+ {%- if is_link -%}
21
+ <a
22
+ class="article-link{%- if link_attrs.get("class") %} {{ link_attrs.get("class", "") }}{%- endif -%}"
23
+ {% for attr, value in link_attrs.items() %}
24
+ {% if attr != "class" %}
25
+ {{ attr }}="{{ value }}"
26
+ {% endif %}
27
+ {% endfor %}
28
+ >
29
+ {%- endif -%}
30
+ <h{{ heading_level }}
31
+ class="p-article-block__title article-title{%- if title.get("class") %} {{ title.get("class", "") }}{%- endif -%}"
32
+ {% for attr, value in title.items() %}
33
+ {% if attr not in ["text", "link_attrs", "class", "heading_level"] %}
34
+ {{ attr }}="{{ value }}"
35
+ {% endif %}
36
+ {% endfor %}>
37
+ {{- title.get("text", "") | trim -}}
38
+ </h{{ heading_level }}>
39
+ {%- if is_link -%}
40
+ </a>
41
+ {%- endif -%}
42
+ {%- endmacro -%}
43
+
44
+ {#
45
+ Helper macro to render the article description.
46
+
47
+ Parameters:
48
+ description (object): Description configuration
49
+ text (string): The description text
50
+ attrs (object) (optional): Additional HTML attributes
51
+ #}
52
+ {%- macro _article_description(description={}) -%}
53
+ {%- set description_attrs = description.get("attrs", {}) -%}
54
+ {%- set description_class = description_attrs.get("class", "") -%}
55
+ <p class="p-article-block__description article-excerpt{%- if description_class %} {{ description_class }}{%- endif -%}"
56
+ {% for attr, value in description_attrs.items() %}
57
+ {% if attr != "class" %}
58
+ {{ attr }}="{{ value }}"
59
+ {% endif %}
60
+ {% endfor %}
61
+ >
62
+ {{- description.get("text", "") | trim -}}
63
+ </p>
64
+ {%- endmacro -%}
65
+
66
+ {#
67
+ Helper macro to render the article metadata with authors and date.
68
+
69
+ Parameters:
70
+ authors (array): List of author objects
71
+ text (string): Author name
72
+ link_attrs (object) (optional): Link attributes for the author. If passed, wraps the author in a link.
73
+
74
+ date (object): Date configuration
75
+ text (string): The date text
76
+ attrs (object) (optional): Additional HTML attributes for the time element
77
+ #}
78
+ {%- macro _article_metadata(authors=[], date={}) -%}
79
+ {% set has_authors = authors | length > 0 %}
80
+ {% set has_date = date.get("text", "") | length > 0 %}
81
+ {%- set date_attrs = date.get("attrs", {}) -%}
82
+ {%- set date_text = date.get("text", "") | trim -%}
83
+
84
+ <div class="p-article-block__metadata">
85
+ <small class="p-article-block__metadata-item">
86
+ <span class="article-author">
87
+ {%- for author in authors -%}
88
+ {%- set author_text = author.get("text", "") | trim -%}
89
+ {%- set author_link_attrs = author.get("link_attrs", {}) -%}
90
+ {%- if author_link_attrs.items() | length > 0 -%}
91
+ <a
92
+ {% for attr, value in author_link_attrs.items() %}
93
+ {{ attr }}="{{ value }}"
94
+ {% endfor %}
95
+ >
96
+ {%- endif -%}
97
+ {{- author_text -}}
98
+ {%- if author_link_attrs -%}</a>{%- endif -%}
99
+ {%- if not loop.last -%}, {% endif -%}
100
+ {%- endfor -%}
101
+ </span>
102
+ </small>
103
+ <small class="p-article-block__metadata-item">
104
+ <time
105
+ datetime="{{ date_text }}"
106
+ class="article-time{%- if date_attrs.get("class") %} {{ date_attrs.get("class", "") }}{%- endif -%}"
107
+ {% for attr, value in date_attrs.items() %}
108
+ {% if attr not in ["datetime", "class"] %}
109
+ {{ attr }}="{{ value }}"
110
+ {% endif %}
111
+ {% endfor %}
112
+ >
113
+ {{- date_text -}}
114
+ </time>
115
+ </small>
116
+ </div>
117
+ {%- endmacro -%}
118
+
119
+ {#
120
+ Blog article block pattern - displays a single blog article with title, description and metadata.
121
+
122
+ Parameters:
123
+ article_config (object) (required): Article configuration
124
+ title (object) (required): Title configuration
125
+ text (string) (required): The title text
126
+ link_attrs (object) (optional): Link attributes for the title. If passed, wraps the title in a link.
127
+ attrs (object) (optional): Additional attributes for the title
128
+ description (object) (optional): Description configuration
129
+ text (string) (required): Description text
130
+ attrs (object) (optional): Additional attributes for description paragraph
131
+ metadata (object) (optional): Metadata configuration
132
+ authors (array) (optional): List of author objects
133
+ text (string) (required): Author name
134
+ link_attrs (object) (optional): Author link attributes. If passed, wraps the author in a link.
135
+ date (object) (optional): Date configuration
136
+ text (string) (required): Date text
137
+ attrs (object) (optional): Date element attributes
138
+ image_html (string) (optional): Image to render before the title.
139
+ When used with vf_blog macro, this is automatically populated with a 16:9 cover image
140
+ from the article's image configuration.
141
+
142
+ attrs (object) (optional): Additional attributes for the resource block
143
+ template_mode (boolean) (optional): Whether this is being rendered in template mode
144
+ #}
145
+ {%- macro vf_article_block(article_config={}, attrs={}, template_mode=False) -%}
146
+ {%- set title = article_config.get("title", {}) -%}
147
+ {%- set description = article_config.get("description", {}) -%}
148
+ {%- set _ = description.setdefault('attrs', {}) -%}
149
+ {%- set metadata = article_config.get("metadata", {}) -%}
150
+ {%- set image_html = article_config.get("image_html", "") | trim -%}
151
+ {%- set authors = metadata.get("authors", []) -%}
152
+ {%- set date = metadata.get("date", {}) -%}
153
+
154
+ <div class="p-article-block{%- if attrs.get("class") %} {{ attrs.get("class", "") }}{%- endif -%}"
155
+ {% for attr, value in attrs.items() %}
156
+ {% if attr != "class" %}
157
+ {{ attr }}="{{ value }}"
158
+ {% endif %}
159
+ {% endfor %}
160
+ >
161
+ {#-
162
+ Note: p-article-block relies on subgrid to align items vertically across the grid.
163
+ This means that we must always create 4 p-article-block__items, even if some are empty.
164
+ This allows the subgrid to maintain alignment, even when some blocks are missing some items.
165
+ Note that this means `p-article-block__item` elements should never define padding or margin, as this would cause empty items to take up space.
166
+ -#}
167
+ <div class="p-article-block__item">
168
+ {%- if image_html | length > 0 -%}
169
+ <div class="p-article-block__image">
170
+ {{ image_html | safe }}
171
+ </div>
172
+ {%- endif -%}
173
+ </div>
174
+
175
+ <div class="p-article-block__item">
176
+ {{ _article_title(title=title, template_mode=template_mode) }}
177
+ </div>
178
+
179
+ <div class="p-article-block__item">
180
+ {{ _article_description(description=description) }}
181
+ </div>
182
+
183
+ <div class="p-article-block__item">
184
+ {{ _article_metadata(authors=authors, date=date) }}
185
+ </div>
186
+ </div>
187
+ {%- endmacro -%}
@@ -0,0 +1,29 @@
1
+ # Helper macro to render a top rule for a section.
2
+ # Parameters:
3
+ # - top_rule_variant: string, one of "default", "muted", "highlighted", "none"
4
+ {%- macro vf_section_top_rule(top_rule_variant="default") -%}
5
+ {%- set top_rule_variant = top_rule_variant | trim | lower -%}
6
+ {%- if top_rule_variant not in ["default", "muted", "highlighted", "none"] -%}
7
+ {%- set top_rule_variant = "default" -%}
8
+ {%- endif -%}
9
+
10
+ {%- if top_rule_variant != "none" -%}
11
+ {%- set top_rule_classes = "p-rule" -%}
12
+ {%- if top_rule_variant != "default" -%}
13
+ {#-
14
+ p-rule--highlighted doesn't exist (use p-rule--highlight instead), but p-rule--muted does.
15
+ We keep the external API here consistent ("-ed" suffix) for simplicity but need to handle this internally.
16
+ -#}
17
+ {%- if top_rule_variant == "highlighted" -%}
18
+ {%- set top_rule_classes = "p-rule--highlight" -%}
19
+ {%- else -%}
20
+ {#- Other cases: just append the top_rule_variant to the p-rule class. -#}
21
+ {%- set top_rule_classes = top_rule_classes + "--" + top_rule_variant -%}
22
+ {%- endif -%}
23
+ {%- endif -%}
24
+ {%- endif -%}
25
+
26
+ {%- if top_rule_variant != "none" -%}
27
+ <hr class="{{- top_rule_classes -}}"/>
28
+ {%- endif -%}
29
+ {%- endmacro -%}
@@ -1,4 +1,5 @@
1
1
  {% from "_macros/shared/vf_dedent.jinja" import vf_dedent %}
2
+ {% from "_macros/shared/vf_section_top_rule.jinja" import vf_section_top_rule %}
2
3
  {% from "_macros/vf_linked-logo-section.jinja" import vf_linked_logo_section %}
3
4
 
4
5
  # description_config
@@ -291,33 +292,6 @@
291
292
  </h2>
292
293
  {%- endmacro -%}
293
294
 
294
- {%- macro basic_section_top_rule(top_rule_variant="default") -%}
295
- {%- set top_rule_variant = top_rule_variant | trim | lower -%}
296
- {%- if top_rule_variant not in ["default", "muted", "highlighted", "none"] -%}
297
- {%- set top_rule_variant = "default" -%}
298
- {%- endif -%}
299
-
300
- {%- if top_rule_variant != "none" -%}
301
- {%- set top_rule_classes = "p-rule" -%}
302
- {%- if top_rule_variant != "default" -%}
303
- {#-
304
- p-rule--highlighted doesn't exist (use p-rule--highlight instead), but p-rule--muted does.
305
- We keep the external API here consistent ("-ed" suffix) for simplicity but need to handle this internally.
306
- -#}
307
- {%- if top_rule_variant == "highlighted" -%}
308
- {%- set top_rule_classes = "p-rule--highlight" -%}
309
- {%- else -%}
310
- {#- Other cases: just append the top_rule_variant to the p-rule class. -#}
311
- {%- set top_rule_classes = top_rule_classes + "--" + top_rule_variant -%}
312
- {%- endif -%}
313
- {%- endif -%}
314
- {%- endif -%}
315
-
316
- {%- if top_rule_variant != "none" -%}
317
- <hr class="{{- top_rule_classes -}}"/>
318
- {%- endif -%}
319
- {%- endmacro -%}
320
-
321
295
  # Params
322
296
  # title: A dictionary with "text" and optional "link_attrs" (a dictionary of link attributes for the title).
323
297
  # subtitle: A dictionary with "text" (required) and optional "heading_level" (default is 4).
@@ -358,7 +332,7 @@
358
332
 
359
333
  <section class="{{ padding_classes }}">
360
334
  <div class="grid-row--50-50{%- if not is_split_on_medium -%}-on-large{%- endif -%}">
361
- {{ basic_section_top_rule(top_rule_variant) }}
335
+ {{ vf_section_top_rule(top_rule_variant) }}
362
336
  <div class="grid-col">
363
337
  {%- if has_label -%}
364
338
  <h3 class="p-muted-heading u-no-padding--top u-no-margin--bottom">{{- label_text -}}</h3>
@@ -0,0 +1,133 @@
1
+ {% from "_macros/shared/vf_section_top_rule.jinja" import vf_section_top_rule %}
2
+ {% from "_macros/shared/vf_article_block.jinja" import vf_article_block %}
3
+
4
+ # Params
5
+ # - article_config: A dictionary with the article configuration.
6
+ # - "title": A dictionary with "text" and optional "link" (a dictionary of link attributes for the title).
7
+ # - "image": A dictionary with "attrs" dict containing "src", "alt", and other image attributes.
8
+ # The image is automatically wrapped in a 16:9 cover image container and passed to
9
+ # vf_article_block as before_title_html. A fallback image is used if no image
10
+ # is provided (can be overridden via fallback_image_url).
11
+ # - "description": A dictionary with "text" containing the article description.
12
+ # - "metadata": A dictionary with:
13
+ # - "authors": A list of author objects, each with "text" and optional "link" (a dictionary of link attributes)
14
+ # - "date": A dictionary with "text" and optional "attrs" (a dictionary of time element attributes)
15
+ # - template_mode: Boolean indicating if the macro is being used in template mode (for dynamic loading scenarios).
16
+ # - fallback_image_url: URL to use as a fallback image, if no image is provided for an article.
17
+ {%- macro _blog_article(article_config={}, template_mode=False, fallback_image_url="https://assets.ubuntu.com/v1/94c82a15-blog_fallback_image.png") -%}
18
+ {%- set base_image_container_classes = "p-image-container--16-9 is-cover article-image" -%}
19
+ {%- if not template_mode -%}
20
+ {%- set default_image_attrs = {
21
+ 'src': fallback_image_url,
22
+ 'alt': 'Blog fallback image'
23
+ }
24
+ -%}
25
+ {%- set input_image_attrs = article_config.get('image', {}).get('attrs', {}) -%}
26
+ {#- Merge user attributes over the defaults -#}
27
+ {%- set img_attrs = default_image_attrs.copy() -%}
28
+ {%- set _ = img_attrs.update(input_image_attrs) -%}
29
+ {#- class merging -#}
30
+ {%- set base_image_class = 'p-image-container__image' -%}
31
+ {%- set input_image_class = input_image_attrs.get('class', '') -%}
32
+ {%- if input_image_class -%}
33
+ {%- set final_image_classes = base_image_class + ' ' + input_image_class -%}
34
+ {%- else -%}
35
+ {%- set final_image_classes = base_image_class -%}
36
+ {%- endif -%}
37
+ {%- set _ = img_attrs.update({'class': final_image_classes}) -%}
38
+ {#- Build the HTML tag -#}
39
+ {%- set ns = namespace(image_html="<img") -%}
40
+ {%- for attr, value in img_attrs.items() -%}
41
+ {%- set ns.image_html = ns.image_html + " " + attr + "=\"" + value + "\"" -%}
42
+ {%- endfor -%}
43
+ {%- set image_html = ns.image_html + ">" -%}
44
+ {%- set _ = article_config.update({'image_html': '<div class="' + base_image_container_classes + '">\n ' + image_html + '\n </div>'}) -%}
45
+ {%- else -%}
46
+ {#- template mode - the template JS will fill in the image slot. -#}
47
+ {%- set _ = article_config.update({'image_html': '<div class="' + base_image_container_classes + '"></div>'}) -%}
48
+ {%- endif -%}
49
+ {{ vf_article_block(article_config=article_config, attrs={"class": "grid-col-2 grid-col-medium-2"}, template_mode=template_mode) }}
50
+
51
+ {%- endmacro -%}
52
+
53
+ # Params
54
+ # title: A dictionary with "text" and optional "link_attrs" (a dictionary of link attributes for the title).
55
+ # articles: A list of article dictionaries, each with "title", "image", "description", and "metadata".
56
+ # template_config:
57
+ # - enabled: A boolean to enable or disable the template mode.
58
+ # - template_container_id: A string for the id of the container to use for dynamic loading scenarios.
59
+ # - template_id: A string for the id of the template to use for dynamic loading scenarios.
60
+ # - layout: Layout to apply to the template. Options are "3-blocks" and "4-blocks".
61
+ # padding: Type of padding to apply to the pattern - "deep", "shallow", "default" (default is "default").
62
+ # top_rule_variant: Type of HR to render at the top of the pattern. "default" | "muted" (default is "default").
63
+ # fallback_image_url: URL to use as a fallback image, if no image is provided for an article.
64
+ {% macro vf_blog(
65
+ title={},
66
+ articles=[],
67
+ template_config={},
68
+ padding="default",
69
+ top_rule_variant="default",
70
+ fallback_image_url="https://assets.ubuntu.com/v1/94c82a15-blog_fallback_image.png"
71
+ ) -%}
72
+ {%- set padding = padding | trim -%}
73
+ {%- if padding not in ["deep", "shallow", "default"] -%}
74
+ {%- set padding = "default" -%}
75
+ {%- endif -%}
76
+
77
+ {%- set padding_classes = "p-section--" + padding -%}
78
+ {%- if padding == "default" -%}
79
+ {%- set padding_classes = "p-section" -%}
80
+ {%- endif -%}
81
+
82
+ {#- Infer layout from article count -#}
83
+ {%- if articles | length == 3 -%}
84
+ {%- set layout = "3-blocks" -%}
85
+ {%- elif articles | length == 4 -%}
86
+ {%- set layout = "4-blocks" -%}
87
+ {%- endif -%}
88
+
89
+ {%- set template_mode = template_config.get("enabled", False) -%}
90
+
91
+ {#- In template mode, read layout from the template config, instead of inferring it from the article count. -#}
92
+ {%- if template_mode -%}
93
+ {%- set layout = template_config.get("layout", "4-blocks") -%}
94
+ {%- endif -%}
95
+
96
+ {#- Default to "4-blocks" layout if an unrecognized layout is used. -#}
97
+ {%- if layout not in ["3-blocks", "4-blocks"] -%}
98
+ {%- set layout = "4-blocks" -%}
99
+ {%- endif -%}
100
+
101
+ <section class="{{ padding_classes }}">
102
+ <div class="p-blog grid-row">
103
+ {{- vf_section_top_rule(top_rule_variant) -}}
104
+ <div class="grid-col-{%- if layout == "3-blocks" -%}2{%- else -%}8{%- endif -%}">
105
+ <h2 class="p-muted-heading">
106
+ {%- if title.get("link_attrs") -%}
107
+ <a
108
+ {% for attr, value in title.get("link_attrs", {}).items() %}
109
+ {{ attr }}="{{ value }}"
110
+ {% endfor %}
111
+ >
112
+ {%- endif -%}
113
+ {{- title.get("text", "Blog Title") -}}
114
+ {%- if title.get("link_attrs") -%}
115
+ </a>
116
+ {%- endif -%}
117
+ </h2>
118
+ </div>
119
+ {%- if template_mode -%}
120
+ <div id="{{ template_config.get("template_container_id", "articles") }}" class="p-blog__articles"></div>
121
+ <template style="display: none;" id="{{ template_config.get("template_id", "template") }}">
122
+ {{ _blog_article(template_mode=True) }}
123
+ </template>
124
+ {%- else -%}
125
+ <div class="p-blog__articles grid-row">
126
+ {%- for article in articles -%}
127
+ {{ _blog_article(article_config=article, fallback_image_url=fallback_image_url) }}
128
+ {%- endfor -%}
129
+ </div>
130
+ {%- endif -%}
131
+ </div>
132
+ </section>
133
+ {%- endmacro -%}
@@ -3,7 +3,9 @@
3
3
  # - padding: Type of padding to apply to the pattern - "deep", "shallow", "default" (default is "default").
4
4
  # - is_split_on_medium: Boolean to indicate if the section should be split on medium screens (default is false).
5
5
  # - top_rule_variant: Type of HR to render at the top of the pattern. "default" | "muted" (default is "default").
6
- {% from "_macros/vf_basic-section.jinja" import basic_section_item, basic_section_title, basic_section_top_rule %}
6
+ {% from "_macros/vf_basic-section.jinja" import basic_section_item, basic_section_title %}
7
+ {% from "_macros/shared/vf_section_top_rule.jinja" import vf_section_top_rule %}
8
+
7
9
  {% macro _get_item_padding_classes(item_config={}, isLastLoopItr=False, isLastBlockItr=False) %}
8
10
  {%- set item_padding = (item_config.get("padding", "")) | trim -%}
9
11
  {%- if item_padding not in ["shallow"] -%}
@@ -32,7 +34,7 @@
32
34
  <div class="grid-row--50-50{%- if not is_split_on_medium -%}-on-large{%- endif -%}">
33
35
  {%- set description_block = blocks | selectattr("type", "equalto", "description-block") | first -%}
34
36
  {%- set divided_blocks = blocks | selectattr("type", "equalto", "divided-block") | list -%}
35
- {{ basic_section_top_rule(top_rule_variant) }}
37
+ {{ vf_section_top_rule(top_rule_variant) }}
36
38
  <div class="grid-col">{{- basic_section_title(title) -}}</div>
37
39
  <div class="grid-col">
38
40
  {%- if description_block -%}
@@ -4,9 +4,9 @@
4
4
  - subtitle_text (string) (optional): The text to be displayed as the subtitle
5
5
  - subtitle_heading_level (int) (optional): The heading level for the subtitle. Can be 4 or 5. Defaults to 5.
6
6
  - highlight_images (boolean) (optional): If the images need to be highlighted, which means adding a subtle grey background. Not added by default.
7
- - image_aspect_ratio_small (string) (optional): The aspect ratio for item images on small screens. Defaults to "square". Can be "square", "2-3", "3-2", "16-9", "cinematic". Defaults to "square".
8
- - image_aspect_ratio_medium (string) (optional): The aspect ratio for item images on medium screens. Defaults to "square". Can be "square", "2-3", "3-2", "16-9", "cinematic". Defaults to "square".
9
- - image_aspect_ratio_large (string) (optional): The aspect ratio for item images on large screens. Defaults to "2-3". Can be "square", "2-3", "3-2", "16-9", "cinematic". Defaults to "2-3".
7
+ - image_aspect_ratio_small (string) (optional): The aspect ratio for item images on small screens. Defaults to "square". Can be "square", "2-3", "3-2", "16-9", "cinematic" or "auto". Defaults to "square".
8
+ - image_aspect_ratio_medium (string) (optional): The aspect ratio for item images on medium screens. Defaults to "square". Can be "square", "2-3", "3-2", "16-9", "cinematic" or "auto". Defaults to "square".
9
+ - image_aspect_ratio_large (string) (optional): The aspect ratio for item images on large screens. Defaults to "2-3". Can be "square", "2-3", "3-2", "16-9", "cinematic" or "auto". Defaults to "2-3".
10
10
  - items (array) (required): An array of items, each including 'image_html', 'title_text', 'description_html', and 'cta_html'.
11
11
  -#}
12
12
  {%- macro vf_equal_heights(
@@ -69,7 +69,7 @@
69
69
  {#- Image item (required) -#}
70
70
  <div class="p-equal-height-row__item">
71
71
  <div
72
- class="p-image-container--{{ image_aspect_ratio_small }}-on-small p-image-container--{{ image_aspect_ratio_medium }}-on-medium p-image-container--{{ image_aspect_ratio_large }}-on-large is-cover{% if highlight_images %} is-highlighted{% endif %}" >
72
+ class="p-image-container {% if image_aspect_ratio_small != 'auto' %} p-image-container--{{ image_aspect_ratio_small }}-on-small{% endif %}{% if image_aspect_ratio_medium != 'auto' %} p-image-container--{{ image_aspect_ratio_medium }}-on-medium{% endif %}{% if image_aspect_ratio_large != 'auto' %} p-image-container--{{ image_aspect_ratio_large }}-on-large{% endif %} is-cover{% if highlight_images %} is-highlighted{% endif %}" >
73
73
  {#- The consumer must pass in an img.p-image-container__image for the image to be properly formatted -#}
74
74
  {{- image | safe -}}
75
75
  </div>
@@ -10,11 +10,13 @@
10
10
  # cta: call-to-action block below the description
11
11
  # image: slot for image content
12
12
  # signpost_image: slot for signpost (left column) image content in 25/75 layout. Required for 25/75 layout.
13
+ # display_blank_signpost_image_space: whether to indent the content for 25/75 layout on large screens.
13
14
  {% macro vf_hero(
14
15
  title_text,
15
16
  subtitle_text='',
16
17
  layout="fallback",
17
- is_split_on_medium=false
18
+ is_split_on_medium=false,
19
+ display_blank_signpost_image_space=false
18
20
  ) -%}
19
21
  {% set has_subtitle = subtitle_text|trim|length > 0 %}
20
22
  {% set description_content = caller('description') %}
@@ -24,7 +26,7 @@
24
26
  {% set image_content = caller('image') %}
25
27
  {% set has_image = image_content|trim|length > 0 %}
26
28
  {% set signpost_image_content = caller('signpost_image') %}
27
- {% set has_signpost_image = signpost_image_content|trim|length > 0 %}
29
+ {% set has_signpost_image = signpost_image_content|trim|length > 0 or display_blank_signpost_image_space %}
28
30
 
29
31
  {#- User can pass layout as "X-Y" or "X/Y" -#}
30
32
  {% set layout = layout | trim | replace('/', '-') %}
@@ -0,0 +1,364 @@
1
+ {#
2
+ VF Resources Pattern Macros
3
+
4
+ This file contains macros for rendering resources sections with various layouts
5
+ including text-only, image-based, and categorized displays.
6
+ #}
7
+ {% from "_macros/vf_basic-section.jinja" import basic_section_item, basic_section_title %}
8
+ {% from "_macros/shared/vf_article_block.jinja" import vf_article_block %}
9
+ {% from "_macros/shared/vf_section_top_rule.jinja" import vf_section_top_rule %}
10
+ {#
11
+ Renders the pattern's title block
12
+
13
+ @param {object} title - Title configuration object
14
+ {
15
+ "text": "Title text",
16
+ "link_attrs": {
17
+ "href": "#"
18
+ } // Optional: link attributes to make title clickable
19
+ }
20
+ #}
21
+ {% macro _title_block(title={}) %}
22
+ {%- if title -%}
23
+ {{- basic_section_title(title) -}}
24
+ {%- endif -%}
25
+ {% endmacro %}
26
+ {#
27
+ Renders the pattern's description block
28
+
29
+ @param {object} description - Description configuration object
30
+ {
31
+ "type": "description",
32
+ "item": {
33
+ "type": "text|html",
34
+ "content": "Description content",
35
+ }
36
+ }
37
+ #}
38
+ {% macro _description_block(description={}) %}
39
+ {%- if description -%}
40
+ {{- basic_section_item(description) -}}
41
+ {%- endif -%}
42
+ {% endmacro %}
43
+ {#
44
+ Renders the pattern's CTA block
45
+
46
+ @param {object} cta - CTA configuration object
47
+ {
48
+ "type": "cta-block",
49
+ "item": {
50
+ "primary": {
51
+ "content_html": "Primary Action",
52
+ "attrs": {
53
+ "href": "#"
54
+ }
55
+ },
56
+ "secondaries": [
57
+ {
58
+ "content_html": "Secondary Action",
59
+ "attrs": {
60
+ "href": "#"
61
+ }
62
+ }
63
+ ],
64
+ "link": {
65
+ "content_html": "Lorem ipsum dolor sit amet ›",
66
+ "attrs": {
67
+ "href": "#"
68
+ }
69
+ }
70
+ }
71
+ }
72
+ #}
73
+ {% macro _cta_block(cta={}) %}
74
+ {%- if cta -%}
75
+ {{- basic_section_item(cta) -}}
76
+ {%- endif -%}
77
+ {% endmacro %}
78
+ {#
79
+ Renders an image based on its type configuration
80
+
81
+ @param {object} image_config - Image configuration object
82
+ {
83
+ "type": "default" | "logo", // default applies 16-9 ratio container
84
+ "attrs": {
85
+ "src": "image url",
86
+ "alt": "Image description",
87
+ "width": "100",
88
+ "height": "100"
89
+ }
90
+ }
91
+ #}
92
+ {% macro _render_image(image_config={}) %}
93
+ {%- if image_config -%}
94
+ {%- set image_type = image_config.get("type", "default") -%}
95
+ {%- if image_type not in ["default", "logo"] -%}
96
+ {%- set image_type = "default" -%}
97
+ {%- endif -%}
98
+ {%- set attrs = image_config.get("attrs", {}) -%}
99
+ {%- if image_type == "default" -%}
100
+ <div class="p-image-container--16-9 is-cover">
101
+ <img class="p-image-container__image {{ attrs.get('class', '') }}" alt="{{ attrs.get('alt', '') }}" {%- if attrs.get('src') %} src="{{ attrs.get('src') }}"{% endif -%} {%- if attrs.get('width') %} width="{{ attrs.get('width') }}"{% endif -%} {%- if attrs.get('height') %} height="{{ attrs.get('height') }}"{% endif -%} {%- for attr, value in attrs.items() -%} {%- if attr not in ["class", "alt", "src", "width", "height"] %} {{ attr }}="{{ value }}"{% endif -%} {%- endfor -%} />
102
+ </div>
103
+ {%- elif image_type == "logo" -%}
104
+ <img class="{{ attrs.get('class', '') }}" alt="{{ attrs.get('alt', '') }}" {%- if attrs.get('src') %} src="{{ attrs.get('src') }}"{% endif -%} {%- if attrs.get('width') %} width="{{ attrs.get('width') }}"{% endif -%} {%- if attrs.get('height') %} height="{{ attrs.get('height') }}"{% endif -%} {%- for attr, value in attrs.items() -%} {%- if attr not in ["class", "alt", "src", "width", "height"] %} {{ attr }}="{{ value }}"{% endif -%} {%- endfor -%} />
105
+ {%- endif -%}
106
+ {%- endif -%}
107
+ {% endmacro %}
108
+ {#
109
+ Renders a category title with proper spacing and styling
110
+
111
+ @param {object} category - Category configuration object
112
+ {
113
+ "text": "Title text",
114
+ "link_attrs": {
115
+ "href": "#"
116
+ } // Optional: link attributes to make title clickable
117
+ }
118
+ @param {boolean} is_first_category - Whether this is the first category in the list
119
+ @param {boolean} is_first_item - Whether this is the first item in the category
120
+ #}
121
+ {% macro _render_category_title(category={}, is_first_category=True, is_first_item=True) %}
122
+ {%- if category.title -%}
123
+ <div class="grid-col">
124
+ {% if is_first_item %}<h3 class="p-text--small-caps">{{ category.title }}</h3>{% endif %}
125
+ </div>
126
+ {%- endif -%}
127
+ {% endmacro %}
128
+ {#
129
+ Renders a resource item for text-only layout
130
+
131
+ @param {object} item - Resource item configuration
132
+ {
133
+ "title": {
134
+ "text": "Resource Title",
135
+ "link_attrs": {
136
+ "href": "/resource-path"
137
+ }
138
+ },
139
+ "description": {
140
+ "text": "Resource description",
141
+ },
142
+ "metadata": {
143
+ "authors": [
144
+ {
145
+ "text": "Resource author",
146
+ "link": {
147
+ "href": "#"
148
+ }
149
+ }
150
+ ],
151
+ "date": {
152
+ "text": "20th April 2024",
153
+ "link": {
154
+ "href": "#"
155
+ }
156
+ }
157
+ },
158
+ "image": {
159
+ "type": "default" | "logo", // default applies 16-9 ratio container
160
+ "attrs": {
161
+ "src": "Image url",
162
+ "alt": "Image description",
163
+ "width": "Image width",
164
+ "height": "Image height"
165
+ }
166
+ }
167
+ }
168
+ @param {boolean} is_last_item - Whether this is the last item
169
+ @param {boolean} is_last_category - Whether this is the last category
170
+ #}
171
+ {% macro _render_text_only_item(item={}, is_last_item=False, is_last_category=False) %}
172
+ {%- set item_title = item.get("title", {}) -%}
173
+ {%- set item_description = item.get("description", {}) -%}
174
+ {%- set item_metadata = item.get("metadata", {}) -%}
175
+ <div class="{% if not (is_last_category and is_last_item) and item_description %}p-section--shallow{% endif %}">
176
+ {{- vf_article_block({"title": item_title, "description": item_description, "metadata": item_metadata}) -}}
177
+ </div>
178
+ {% endmacro %}
179
+ {#
180
+ Renders a resource item for full layout (with images and/or categories)
181
+
182
+ @param {object} item - Resource item configuration (same structure as _render_text_only_item)
183
+ @param {object} category - Category configuration
184
+ @param {boolean} render_images - Whether to render images
185
+ @param {boolean} render_categories - Whether to render category titles
186
+ @param {boolean} is_first_category - Whether this is the first category
187
+ @param {boolean} is_first_item - Whether this is the first item in category
188
+ @param {boolean} is_last_item - Whether this is the last item in category
189
+ #}
190
+ {% macro _render_full_layout_item(item={}, category={}, render_images=False, render_categories=True, is_first_category=True, is_first_item=True, is_last_item=False) %}
191
+ {%- set item_title = item.get("title", {}) -%}
192
+ {%- set item_description = item.get("description", {}) -%}
193
+ {%- set item_metadata = item.get("metadata", {}) -%}
194
+ {%- set image_config = item.get("image", {}) -%}
195
+ <div class="grid-row--25-75-on-large{% if not is_last_item %} p-section--shallow{% endif %}">
196
+ {%- if render_categories and render_images -%}
197
+ {% if not is_first_category and is_first_item %}<hr class="p-rule--muted" />{% endif %}
198
+ {{- _render_category_title(category, is_first_category, is_first_item) -}}
199
+ {%- endif -%}
200
+ <div class="grid-col">
201
+ {%- if render_categories and not render_images -%}
202
+ {% if not is_first_category and is_first_item %}<hr class="p-rule--muted" />{% endif %}
203
+ {%- endif %}
204
+ <div class="p-resources-block grid-row{% if render_images or render_categories %} grid-row--50-50-on-medium{% endif %}">
205
+ <div class="grid-col p-resources-block__col">
206
+ {%- if render_images -%}
207
+ {{- _render_image(image_config) -}}
208
+ {%- elif render_categories -%}
209
+ {{- _render_category_title(category, is_first_category, is_first_item) -}}
210
+ {%- endif -%}
211
+ </div>
212
+ <div class="grid-col p-resources-block__col">
213
+ {%- if render_categories -%}
214
+ {%- set x=item_title.__setitem__("heading_level", 4) -%}
215
+ {%- set x=item_title.__setitem__("class", "p-heading--3") -%}
216
+ {%- endif -%}
217
+ {{- vf_article_block({"title": item_title, "description": item_description, "metadata": item_metadata}) -}}
218
+ </div>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ {% endmacro %}
223
+ {#
224
+ Main VF Resources macro for rendering resource sections
225
+
226
+ @param {object} title - Section title configuration
227
+ {
228
+ "text": "Section Title",
229
+ "link_attrs": {
230
+ "href": "#"
231
+ }
232
+ }
233
+ @param {array} blocks - Array of content blocks
234
+ [
235
+ {
236
+ "type": "description",
237
+ "item": {
238
+ "type": "text|html",
239
+ "content": "Description content",
240
+ }
241
+ },
242
+ {
243
+ "type": "cta-block",
244
+ "item": {
245
+ "primary": {
246
+ "content_html": "Primary Action",
247
+ "attrs": {
248
+ "href": "#"
249
+ }
250
+ },
251
+ "secondaries": [
252
+ {
253
+ "content_html": "Secondary Action",
254
+ "attrs": {
255
+ "href": "#"
256
+ }
257
+ }
258
+ ],
259
+ "link": {
260
+ "content_html": "Lorem ipsum dolor sit amet ›",
261
+ "attrs": {
262
+ "href": "#"
263
+ }
264
+ }
265
+ }
266
+ },
267
+ {
268
+ "type": "resources",
269
+ "render_images": true|false,
270
+ "render_categories": true|false,
271
+ "categories": [
272
+ {
273
+ "title": {
274
+ "text": "Category Name",
275
+ "link_attrs": {
276
+ "href": "#"
277
+ }
278
+ },
279
+ "items": [
280
+ {
281
+ "title": {
282
+ "text": "Resource Title",
283
+ "link_attrs": {
284
+ "href": "/resource-path"
285
+ }
286
+ },
287
+ "description": {
288
+ "text": "Resource description",
289
+ },
290
+ "metadata": {
291
+ "authors": [
292
+ {
293
+ "text": "Resource author",
294
+ "link": {
295
+ "href": "#"
296
+ }
297
+ }
298
+ ],
299
+ "date": {
300
+ "text": "20th April 2024",
301
+ "link": {
302
+ "href": "#"
303
+ }
304
+ }
305
+ },
306
+ "image": {
307
+ "type": "default" | "logo", // default applies 16-9 ratio container
308
+ "attrs": {
309
+ "src": "Image url",
310
+ "alt": "Image description",
311
+ "width": "Image width",
312
+ "height": "Image height"
313
+ }
314
+ }
315
+ }
316
+ ]
317
+ }
318
+ ]
319
+ }
320
+ ]
321
+ #}
322
+ {% macro vf_resources(title={}, blocks=[], caller=None) %}
323
+ {%- set description = blocks | selectattr("type", "equalto", "description") | first -%}
324
+ {%- set cta = blocks | selectattr("type", "equalto", "cta-block") | first -%}
325
+ {%- set resources = blocks | selectattr("type", "equalto", "resources") | first -%}
326
+ {%- set render_images = resources.get("render_images", true) -%}
327
+ {%- set render_categories = resources.get("render_categories", true) -%}
328
+ {%- set is_text_only = not (render_images or render_categories) -%}
329
+ <section class="p-section">
330
+ <div class="grid-row--50-50{% if is_text_only %}-on-large{% endif %}{% if not is_text_only %} p-section--shallow{% endif %}">
331
+ {{- vf_section_top_rule("default") -}}
332
+ <div class="grid-col">{{- _title_block(title) -}}</div>
333
+ <div class="grid-col">
334
+ {{- _description_block(description) -}}
335
+ {{- _cta_block(cta) -}}
336
+ {%- if is_text_only -%}
337
+ {%- for category in resources.get("categories", []) -%}
338
+ {%- set category_last = loop.last -%}
339
+ {%- for item in category.get("items", []) -%}
340
+ {{- _render_text_only_item(item, loop.last, category_last) -}}
341
+ {%- endfor -%}
342
+ {%- endfor -%}
343
+ {%- endif -%}
344
+ </div>
345
+ </div>
346
+ {%- if not is_text_only -%}
347
+ {%- for category in resources.get("categories", []) -%}
348
+ {%- set first_category = loop.first -%}
349
+ {%- set last_category = loop.last -%}
350
+ <div {% if not last_category %}class="p-section--shallow"{% endif %}>
351
+ {%- for item in category.get("items", []) -%}
352
+ {{- _render_full_layout_item(item,
353
+ category,
354
+ render_images,
355
+ render_categories,
356
+ first_category,
357
+ loop.first,
358
+ loop.last) -}}
359
+ {%- endfor -%}
360
+ </div>
361
+ {%- endfor -%}
362
+ {%- endif -%}
363
+ </section>
364
+ {% endmacro %}