vanilla-framework 4.29.0 → 4.31.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.29.0",
3
+ "version": "4.31.0",
4
4
  "author": {
5
5
  "email": "webteam@canonical.com",
6
6
  "name": "Canonical Webteam"
@@ -58,7 +58,7 @@
58
58
  ],
59
59
  "devDependencies": {
60
60
  "@canonical/cookie-policy": "3.6.5",
61
- "@canonical/latest-news": "1.5.0",
61
+ "@canonical/latest-news": "1.6.0",
62
62
  "@percy/cli": "1.30.7",
63
63
  "@testing-library/cypress": "10.0.3",
64
64
  "autoprefixer": "10.4.20",
@@ -0,0 +1,39 @@
1
+ /*
2
+ @classreference
3
+ newsletter-signup:
4
+ Newsletter signup:
5
+ .p-newsletter-signup:
6
+ Main element of the newsletter signup component.
7
+ .p-newsletter-signup--25-75:
8
+ Modifier class for a 25-75 split layout.
9
+ .p-newsletter-signup--50-50:
10
+ Modifier class for a 50-50 split layout.
11
+ .p-newsletter-signup--2-col:
12
+ Modifier class for a 2 column layout.
13
+ Takes at most 2 columns on large and medium dimensions.
14
+ Takes full width on small dimensions.
15
+ .p-newsletter-signup--4-col:
16
+ Modifier class for a 4 column layout.
17
+ Takes at most 4 columns on large and medium dimensions.
18
+ Takes full width on small dimensions.
19
+ */
20
+
21
+ @import 'settings';
22
+
23
+ @mixin vf-p-newsletter-signup {
24
+ .p-newsletter-signup--2-col {
25
+ padding-bottom: 0;
26
+
27
+ @media (width < $breakpoint-small) {
28
+ padding-bottom: $spv--strip-regular * 0.5;
29
+ }
30
+ }
31
+
32
+ .p-newsletter-signup--4-col {
33
+ padding-bottom: 0;
34
+
35
+ @media (width < $breakpoint-large) {
36
+ padding-bottom: $spv--strip-regular * 0.5;
37
+ }
38
+ }
39
+ }
@@ -39,6 +39,7 @@
39
39
  @import 'patterns_notifications';
40
40
  @import 'patterns_pagination';
41
41
  @import 'patterns_pricing-block';
42
+ @import 'patterns_newsletter-signup';
42
43
  @import 'patterns_pull-quotes';
43
44
  @import 'patterns_rule';
44
45
  @import 'patterns_search-and-filter';
@@ -133,6 +134,7 @@
133
134
  @include vf-p-muted-heading;
134
135
  @include vf-p-navigation;
135
136
  @include vf-p-navigation-reduced;
137
+ @include vf-p-newsletter-signup;
136
138
  @include vf-p-notification;
137
139
  @include vf-p-pagination;
138
140
  @include vf-p-pricing-block;
@@ -0,0 +1,36 @@
1
+ {#-
2
+ Shared Dedent macro for Jinja templates.
3
+ Determines the minimum indentation of a block of text and removes it from each line.
4
+ This allows multi-line strings to be passed as parameters while maintaining readability in the template code and retaining the original formatting.
5
+ Usage: {{ vf_dedent(text_block='
6
+ This is a multi-line string
7
+ that will be dedented
8
+ to remove the minimum indentation.
9
+ ') }} ->
10
+ This is a multi-line string
11
+ that will be dedented
12
+ to remove the minimum indentation.
13
+ -#}
14
+ {% macro vf_dedent(text_block) -%}
15
+ {%- set lines = text_block.split('\n') -%}
16
+ {%- set ns = namespace(min_indent=none) -%}
17
+
18
+ {#- Find the minimum indentation of all non-empty lines -#}
19
+ {%- for line in lines -%}
20
+ {%- if line | trim | length > 0 -%}
21
+ {%- set indent = (line | length) - (line | trim | length) -%}
22
+ {%- if ns.min_indent is none or indent < ns.min_indent -%}
23
+ {%- set ns.min_indent = indent -%}
24
+ {%- endif -%}
25
+ {%- endif -%}
26
+ {%- endfor -%}
27
+
28
+ {#- Rebuild the string with the minimum indentation removed -#}
29
+ {%- if ns.min_indent and ns.min_indent > 0 -%}
30
+ {%- for line in lines -%}
31
+ {{- line[ns.min_indent:] -}}{%- if not loop.last and not loop.first %}<br/>{% endif -%}
32
+ {%- endfor -%}
33
+ {%- else -%}
34
+ {{- text_block -}}
35
+ {%- endif -%}
36
+ {%- endmacro %}
@@ -0,0 +1,368 @@
1
+ {% from "_macros/shared/vf_dedent.jinja" import vf_dedent %}
2
+ {% from "_macros/vf_linked-logo-section.jinja" import vf_linked_logo_section %}
3
+
4
+ # description_config
5
+ # - type: "text" | "html" (default is "text")
6
+ # - content: The content of the description. Could be plain text (if type is "text") or HTML (if type is "html").
7
+ {% macro _basic_section_description(description_config={}) %}
8
+ {%- set type = (description_config.get("type", "text")) | trim -%}
9
+ {%- if type not in ["text", "html"] -%}
10
+ {%- set type = "text" -%}
11
+ {%- endif -%}
12
+
13
+ {%- if type == "text" -%}
14
+ <p>{{- description_config.get("content", "") -}}</p>
15
+ {%- elif type == "html" -%}
16
+ {{- description_config.get("content", "") | safe -}}
17
+ {%- endif -%}
18
+ {% endmacro %}
19
+
20
+ # image_config
21
+ # - aspect_ratio: "16-9" | "3-2" | "" (default is "")
22
+ # - caption_html: The HTML content for the caption of the image (optional). Will be wrapped in <figcaption>, and the image and caption will be wrapped in a <figure>.
23
+ # - attrs: A dictionary of attributes to apply to the image
24
+ {% macro _basic_section_image(image_config={}) %}
25
+ {%- set aspect_ratio = image_config.get("aspect_ratio", "") | trim -%}
26
+ {%- if aspect_ratio not in ["16-9", "3-2"] -%}
27
+ {%- set aspect_ratio = "" -%}
28
+ {%- endif -%}
29
+
30
+ {%- set caption_html = image_config.get("caption_html", "") | trim -%}
31
+ {%- set has_caption = caption_html | length > 0 -%}
32
+
33
+ {%- if has_caption -%}
34
+ <figure>
35
+ {%- endif -%}
36
+ <div class="p-image-container{%- if aspect_ratio | length > 0 -%}--{{- aspect_ratio -}}{%- endif %} is-highlighted">
37
+ <img class="p-image-container__image {{ image_config.get("attrs", {}).get("class", "") -}}"
38
+ {%- for attr, value in image_config.get("attrs", {}).items() -%}
39
+ {% if attr != "class" %}
40
+ {{ attr }}="{{ value }}"
41
+ {%- endif -%}
42
+ {%- endfor -%}
43
+ />
44
+ </div>
45
+ {%- if has_caption -%}
46
+ <figcaption>
47
+ {{- caption_html | safe -}}
48
+ </figcaption>
49
+ </figure>
50
+ {%- endif -%}
51
+ {% endmacro %}
52
+
53
+ # video_config
54
+ # - attrs: A dictionary of attributes to apply to the video iframe
55
+ {% macro _basic_section_video(video_config={}) %}
56
+ <div class="u-embedded-media">
57
+ <iframe class="u-embedded-media__element {{ video_config.get("attrs", {}).get("class", "") -}}"
58
+ {%- for attr, value in video_config.get("attrs", {}).items() -%}
59
+ {% if attr != "class" %}
60
+ {{ attr }}="{{ value }}"
61
+ {%- endif -%}
62
+ {%- endfor -%}
63
+ ></iframe>
64
+ </div>
65
+ {% endmacro %}
66
+
67
+ # list_config
68
+ # - list_item_type: "bullet" | "tick" | "cross" | "number" | "" (default is "")
69
+ # - list_items: A list of items to be displayed in the section.
70
+ # - - content: The HTML content of the list item.
71
+ # - - sublist: A nested list configuration
72
+ {% macro _basic_section_list(list_config={}) %}
73
+ {%- if list_config.get("list_items", []) | length > 0 -%}
74
+ {#- Note: namespace() requires jinja2 2.10 or later -#}
75
+ {%- set ns = namespace(list_tag="ul") -%}
76
+
77
+ {#- If any of the list items is numbered, the entire list is numbered -#}
78
+ {%- for list_item in list_config.get("list_items", []) -%}
79
+ {%- if list_item.get("list_item_type", "")|trim|lower == "number" -%}
80
+ {%- set ns.list_tag = "ol" -%}
81
+ {%- endif -%}
82
+ {%- endfor -%}
83
+
84
+ <{{ ns.list_tag }} class="p-list--divided">
85
+ {%- for list_item in list_config.get("list_items", []) -%}
86
+ {%- set list_item_type=list_item.get("list_item_type", "") | trim | lower -%}
87
+ {#- If the list is ordered, ignore list item type. Prevents from drawing both a tick and a number, for example. -#}
88
+ {%- if ns.list_tag == "ol" -%}
89
+ {%- set list_item_type = 'number' -%}
90
+ {%- elif list_item_type|length > 0 and list_item_type not in ['bullet', 'tick', 'number', 'cross'] -%}
91
+ {%- set list_item_type = '' -%}
92
+ {%- endif -%}
93
+
94
+ {%- set list_item_style_class = "" -%}
95
+ {%- if list_item_type == "bullet" -%}
96
+ {%- set list_item_style_class = "has-bullet" -%}
97
+ {%- elif list_item_type == "tick" -%}
98
+ {%- set list_item_style_class = "is-ticked" -%}
99
+ {%- elif list_item_type == "cross" -%}
100
+ {%- set list_item_style_class = "is-crossed u-text--muted" -%}
101
+ {%- endif -%}
102
+
103
+ <li class="p-list__item {{ list_item_style_class }}">
104
+ {{- list_item.get("content", "") | safe -}}
105
+ {%- if "sublist" in list_item -%}
106
+ {{- _basic_section_list(list_config=list_item["sublist"]) -}}
107
+ {%- endif -%}
108
+ </li>
109
+ {%- endfor -%}
110
+ </{{ ns.list_tag }}>
111
+ {%- endif -%}
112
+ {% endmacro %}
113
+
114
+ # code_block_config
115
+ # - content: The HTML content to be displayed in the code block. Will be wrapped in a <pre> tag.
116
+ # - is_code_snippet: Boolean to indicate if the content is a code snippet (default is false). If true, the content will be wrapped in a <code> tag, within the <pre> tag.
117
+ {% macro _basic_section_code_block(code_block_config={}) %}
118
+ {%- set is_code_snippet = code_block_config.get("is_code_snippet", False) -%}
119
+ <pre>
120
+ {%- if is_code_snippet -%}
121
+ <code>
122
+ {%- endif -%}
123
+ {{- vf_dedent(code_block_config.get("content", "")) | safe -}}
124
+ {%- if is_code_snippet -%}
125
+ </code>
126
+ {%- endif -%}
127
+ </pre>
128
+ {% endmacro %}
129
+
130
+ # logo_block_config
131
+ # - logos: A list of logos to be displayed in the section.
132
+ # - - attrs: A dictionary of attributes to apply to the logo
133
+ # - - is_fixed_width: Boolean to indicate if the logo should be wrapped in a fixed-width container (default is true).
134
+ # - - has_line_break_after: Boolean to indicate if a line break should be included after the logo (default is false). This will be hidden on small screens.
135
+ # See https://vanillaframework.io/docs/patterns/logo-section#line-breaks
136
+ {% macro _basic_section_logo_block(logo_block_config={}) %}
137
+ {%- set is_fixed_width = logo_block_config.get("is_fixed_width", true) != false -%}
138
+ {%- if is_fixed_width -%}<div class="u-fixed-width">{%- endif -%}
139
+ <div class="p-logo-section">
140
+ {%- for logo in logo_block_config.get("logos", []) -%}
141
+ <div class="p-logo-section__item">
142
+ <img class="p-logo-section__logo {{ logo.get("attrs", {}).get("class", "") -}}"
143
+ {%- for attr, value in logo.get("attrs", {}).items() -%}
144
+ {% if attr != "class" %}
145
+ {{ attr }}="{{ value }}"
146
+ {%- endif -%}
147
+ {%- endfor -%}
148
+ />
149
+ {%- if logo.get("has_line_break_after", false) -%}
150
+ <br class="u-hide--small">
151
+ {%- endif -%}
152
+ </div>
153
+ {%- endfor -%}
154
+ </div>
155
+ {%- if is_fixed_width -%}</div>{%- endif -%}
156
+ {% endmacro %}
157
+
158
+ # linked_logo_block_config
159
+ # - links (array) (required) : Array of links, each including 'href', 'label', 'text', and 'image_attrs'
160
+ {% macro _basic_section_linked_logo_block(linked_logo_block_config={}) %}
161
+ {{- vf_linked_logo_section(
162
+ links=linked_logo_block_config.get("links", []),
163
+ top_rule_variant="none",
164
+ padding="none"
165
+ ) -}}
166
+ {% endmacro %}
167
+
168
+ # content_html: The inner HTML for the call-to-action button.
169
+ # button_type: "primary" | "secondary" (default is "primary")
170
+ # attrs: A dictionary of attributes to apply to the button or link.
171
+ {% macro _cta_button(content_html, button_type="", attrs={}) %}
172
+ {%- set cta_element_tag = "button" -%}
173
+ {%- if "href" in attrs and attrs.get("href", "") | length > 0 -%}
174
+ {%- set cta_element_tag = "a" -%}
175
+ {%- endif -%}
176
+
177
+ {%- if button_type not in ["primary", "secondary"] -%}
178
+ {%- set button_type = "" -%}
179
+ {%- endif -%}
180
+
181
+ {%- set cta_class = "" -%}
182
+ {%- if button_type == "primary" -%}
183
+ {%- set cta_class = "p-button--positive" -%}
184
+ {%- elif button_type == "secondary" -%}
185
+ {%- set cta_class = "p-button" -%}
186
+ {%- endif -%}
187
+
188
+ <{{ cta_element_tag }} class="{{ cta_class }} {{ attrs.get("class", "") }}"
189
+ {%- for attr, value in attrs.items() -%}
190
+ {% if attr != "class" %}
191
+ {{ attr }}="{{ value }}"
192
+ {%- endif -%}
193
+ {%- endfor -%}
194
+ >
195
+ {{- content_html | safe -}}
196
+ </{{ cta_element_tag }}>
197
+ {% endmacro %}
198
+
199
+ # cta_block_config
200
+ # - primary
201
+ # - content_html: The inner HTML for the primary call-to-action button.
202
+ # - attrs: A dictionary of attributes to apply to the primary button.
203
+ # - secondaries (generally, we recommend using 2):
204
+ # - content_html: The inner HTML for the secondary call-to-action button.
205
+ # - attrs: A dictionary of attributes to apply to the secondary button.
206
+ # - link
207
+ # - content_html: The inner HTML for the link.
208
+ # - attrs: A dictionary of attributes to apply to the link.
209
+ {% macro _basic_section_cta_block(cta_block_config={}) %}
210
+ <div class="p-cta-block u-no-padding--bottom">
211
+ {%- if "primary" in cta_block_config %}
212
+ {{ _cta_button(
213
+ content_html=cta_block_config["primary"].get("content_html", ""),
214
+ attrs=cta_block_config["primary"].get("attrs", {}),
215
+ button_type="primary"
216
+ ) }}
217
+ {%- endif -%}
218
+ {%- if "secondaries" in cta_block_config %}
219
+ {%- for secondary in cta_block_config["secondaries"] -%}
220
+ {{ _cta_button(
221
+ content_html=secondary.get("content_html", ""),
222
+ attrs=secondary.get("attrs", {}),
223
+ button_type="secondary"
224
+ ) }}
225
+ {%- endfor -%}
226
+ {%- endif -%}
227
+ {%- if "link" in cta_block_config %}
228
+ {{ _cta_button(
229
+ content_html=cta_block_config["link"].get("content_html", ""),
230
+ attrs=cta_block_config["link"].get("attrs", {})
231
+ ) }}
232
+ {%- endif -%}
233
+ </div>
234
+ {% endmacro %}
235
+
236
+ # item_config
237
+ # - type: "description" | "image" | "video" | "list" | "code-block" | "logo-block" | "liked-logo-block" | "cta-block"
238
+ # - item: The configuration for the item, which varies based on the type.
239
+ {% macro _basic_section_item(item_config={}) %}
240
+ {%- set type = (item_config.get("type", "")) | trim -%}
241
+
242
+ {%- if type == "description" -%}
243
+ {{- _basic_section_description(item_config.get("item", {})) -}}
244
+ {%- elif type == "image" -%}
245
+ {{- _basic_section_image(item_config.get("item", {})) -}}
246
+ {%- elif type == "video" -%}
247
+ {{- _basic_section_video(item_config.get("item", {})) -}}
248
+ {%- elif type == "list" -%}
249
+ {{- _basic_section_list(item_config.get("item", {})) -}}
250
+ {%- elif type == "code-block" -%}
251
+ {{- _basic_section_code_block(item_config.get("item", {})) -}}
252
+ {%- elif type =="logo-block" -%}
253
+ {{- _basic_section_logo_block(item_config.get("item", {})) -}}
254
+ {%- elif type == "linked-logo-block" -%}
255
+ {{- _basic_section_linked_logo_block(item_config.get("item", {})) -}}
256
+ {%- elif type == "cta-block" -%}
257
+ {{- _basic_section_cta_block(item_config.get("item", {})) -}}
258
+ {%- endif -%}
259
+ {% endmacro %}
260
+
261
+ # Params
262
+ # title: A dictionary with "text" and optional "link_attrs" (a dictionary of link attributes for the title).
263
+ # subtitle: A dictionary with "text" (required) and optional "heading_level" (default is 4).
264
+ # label_text: Muted heading above the title.
265
+ # items: A list of items to be displayed in the section.
266
+ # padding: Type of padding to apply to the pattern - "deep", "shallow", "default" (default is "default").
267
+ # is_split_on_medium: Boolean to indicate if the section should be split on medium screens (default is false).
268
+ # top_rule_variant: Type of HR to render at the top of the pattern. "default" | "muted" (default is "default").
269
+ {% macro vf_basic_section(
270
+ title={},
271
+ label_text="",
272
+ subtitle={},
273
+ items=[],
274
+ padding="default",
275
+ is_split_on_medium=false,
276
+ top_rule_variant="default"
277
+ ) -%}
278
+
279
+ {%- set padding = padding | trim -%}
280
+ {%- if padding not in ["deep", "shallow", "default"] -%}
281
+ {%- set padding = "default" -%}
282
+ {%- endif -%}
283
+
284
+ {%- set top_rule_variant = top_rule_variant | trim -%}
285
+ {%- if top_rule_variant not in ["default", "muted", "highlighted", "none"] -%}
286
+ {%- set top_rule_variant = "default" -%}
287
+ {%- endif -%}
288
+
289
+ {%- if top_rule_variant != "none" -%}
290
+ {%- set top_rule_classes = "p-rule" -%}
291
+ {%- if top_rule_variant != "default" -%}
292
+ {#-
293
+ p-rule--highlighted doesn't exist (use p-rule--highlight instead), but p-rule--muted does.
294
+ We keep the external API here consistent ("-ed" suffix) for simplicity but need to handle this internally.
295
+ -#}
296
+ {%- if top_rule_variant == "highlighted" -%}
297
+ {%- set top_rule_classes = "p-rule--highlight" -%}
298
+ {%- else -%}
299
+ {#- Other cases: just append the top_rule_variant to the p-rule class. -#}
300
+ {%- set top_rule_classes = top_rule_classes + "--" + top_rule_variant -%}
301
+ {%- endif -%}
302
+ {%- endif -%}
303
+ {%- endif -%}
304
+
305
+ {%- set padding_classes = "p-section--" + padding -%}
306
+ {%- if padding == "default" -%}
307
+ {%- set padding_classes = "p-section" -%}
308
+ {%- endif -%}
309
+
310
+ {%- set label_text=label_text|trim -%}
311
+ {%- set subtitle_text = subtitle.get("text", "") | trim -%}
312
+ {%- set subtitle_heading_level = subtitle.get("heading_level", 4) -%}
313
+ {%- if subtitle_heading_level not in [4, 5] -%}
314
+ {%- set subtitle_heading_level = 4 -%}
315
+ {%- endif -%}
316
+
317
+ {%- set has_label = label_text|length > 0 -%}
318
+ {%- set has_subtitle = subtitle_text|length > 0 -%}
319
+
320
+ <section class="{{ padding_classes }}">
321
+ <div class="grid-row--50-50{%- if not is_split_on_medium -%}-on-large{%- endif -%}">
322
+ {%- if top_rule_variant != "none" %}
323
+ <hr class="{{- top_rule_classes -}}"/>
324
+ {%- endif -%}
325
+ <div class="grid-col">
326
+ {%- if has_label -%}
327
+ <h3 class="p-muted-heading u-no-padding--top u-no-margin--bottom">{{- label_text -}}</h3>
328
+ {%- endif -%}
329
+ <h2>
330
+ {%- if "link_attrs" in title and "href" in title.get("link_attrs", {}) -%}
331
+ <a
332
+ {% for attr, value in title.get("link_attrs", {}).items() -%}
333
+ {{ attr }}="{{ value }}"
334
+ {%- endfor -%}
335
+ >{{- title.get("text", "") -}}</a>
336
+ {%- else -%}
337
+ {{- title.get("text", "") -}}
338
+ {%- endif -%}
339
+ </h2>
340
+ </div>
341
+ <div class="grid-col">
342
+ {%- if has_subtitle -%}
343
+ <p class="p-heading--{{ subtitle_heading_level }}">{{- subtitle_text -}}</p>
344
+ {%- endif -%}
345
+ {%- for item in items -%}
346
+ {%- set item_padding = (item.get("padding", "")) | trim -%}
347
+ {%- if item_padding not in ["shallow"] -%}
348
+ {%- set item_padding="" -%}
349
+ {%- endif -%}
350
+
351
+ {%- set item_classes = "" -%}
352
+ {%- if item_padding | length > 0 -%}
353
+ {%- set item_classes = "p-section--" + item_padding -%}
354
+ {%- endif -%}
355
+
356
+ {#- Last item should not have any additional padding - the pattern itself already has bottom padding -#}
357
+ {%- if loop.last -%}
358
+ {%- set item_classes = "" -%}
359
+ {%- endif -%}
360
+ <div{%- if item_classes | length > 0 %} class="{{ item_classes }}"{%- endif -%}>
361
+ {{- _basic_section_item(item) -}}
362
+ </div>
363
+ {%- endfor -%}
364
+ </div>
365
+ </div>
366
+ </section>
367
+
368
+ {%- endmacro %}
@@ -1,13 +1,18 @@
1
1
  {#
2
2
  Params
3
- - title_text (string) (required): The text to be displayed as the heading
3
+ - title_text (string): The text to be displayed as the heading
4
4
  - layout: layout of linked logo section. Options are '50/50', '25/75', 'full-width'
5
5
  - links (array) (required) : Array of links, each including 'href', 'label', 'text', and 'image_html'
6
+ - if present, `image_attrs` will be used over `image_html` as it provides a more flexible way to pass image attributes.
7
+ - top_rule_variant: Type of HR to render at the top of the pattern. "default" | "muted" | "highlighted" | "none" (default is "default").
8
+ - padding: Type of padding to apply to the pattern - "deep", "shallow", "default", "none" (default is "default").
6
9
  #}
7
10
 
8
- {% macro vf_linked_logo_section(title_text, links, layout="full-width") -%}
11
+ {% macro vf_linked_logo_section(title_text="", links=[], layout="full-width", top_rule_variant="default", padding="default") -%}
9
12
  {% set layout = layout | trim | replace('/', '-') %}
10
13
 
14
+ {% set has_title = title_text | trim | length > 0 %}
15
+
11
16
  {% if layout not in ['50-50', '25-75', 'full-width'] %}
12
17
  {% set layout = 'full-width' %}
13
18
  {% endif %}
@@ -21,6 +26,39 @@
21
26
  {% set max_logos = max_logos_map[layout] or 8 %}
22
27
  {% set links = links[:max_logos] %}
23
28
 
29
+ {% set top_rule_variant = top_rule_variant | trim -%}
30
+ {%- if top_rule_variant not in ["default", "muted", "highlighted", "none"] -%}
31
+ {%- set top_rule_variant = "default" -%}
32
+ {%- endif -%}
33
+
34
+ {%- if top_rule_variant != "none" -%}
35
+ {%- set top_rule_classes = "p-rule" -%}
36
+ {%- if top_rule_variant != "default" -%}
37
+ {#-
38
+ p-rule--highlighted doesn't exist (use p-rule--highlight instead), but p-rule--muted does.
39
+ We keep the external API here consistent ("-ed" suffix) for simplicity but need to handle this internally.
40
+ -#}
41
+ {%- if top_rule_variant == "highlighted" -%}
42
+ {%- set top_rule_classes = "p-rule--highlight" -%}
43
+ {%- else -%}
44
+ {#- Other cases: just append the top_rule_variant to the p-rule class. -#}
45
+ {%- set top_rule_classes = top_rule_classes + "--" + top_rule_variant -%}
46
+ {%- endif -%}
47
+ {%- endif -%}
48
+ {%- endif -%}
49
+
50
+ {%- set padding = padding | trim -%}
51
+ {%- if padding not in ["deep", "shallow", "default", "none"] -%}
52
+ {%- set padding = "default" -%}
53
+ {%- endif -%}
54
+
55
+ {%- set padding_classes = "p-section--" + padding -%}
56
+ {%- if padding == "default" -%}
57
+ {%- set padding_classes = "p-section" -%}
58
+ {%- elif padding == "none" -%}
59
+ {%- set padding_classes = "" -%}
60
+ {%- endif -%}
61
+
24
62
  {% macro _list(links, cols_per_item=2) %}
25
63
  {%- if links|length > 1 -%}
26
64
  <div class="grid-row">
@@ -28,7 +66,18 @@
28
66
  <div class="grid-col-{{ cols_per_item }} grid-col-medium-{{ cols_per_item }}">
29
67
  <a href="{{ link.href }}" aria-label="{{ link.label }}">
30
68
  <div class="p-image-container--16-9 is-highlighted">
69
+ {#- If image_attrs is present, use it to generate the image HTML -#}
70
+ {%- if "image_attrs" in link -%}
71
+ <img class="p-image-container__image {{ link["image_attrs"]["class"] }}"
72
+ {% for attr, value in link["image_attrs"].items() %}
73
+ {% if attr != "class" %}
74
+ {{ attr }}="{{ value }}"
75
+ {% endif %}
76
+ {% endfor %}
77
+ >
78
+ {% else %}
31
79
  {{ link.image_html | safe }}
80
+ {% endif %}
32
81
  </div>
33
82
  <hr class="p-rule--muted">
34
83
  <p>{{ link.text }}</p>
@@ -39,15 +88,23 @@
39
88
  {%- endif -%}
40
89
  {% endmacro %}
41
90
 
42
- <section class="p-section">
43
- <hr class="p-rule is-fixed-width"/>
91
+ {%- if padding == "none" -%}
92
+ <div>
93
+ {%- else -%}
94
+ <section{%- if padding_classes | length > 0 %} class="{{ padding_classes }}"{%- endif -%}>
95
+ {%- endif -%}
96
+ {%- if top_rule_variant != "none" -%}
97
+ <hr class="is-fixed-width {{ top_rule_classes -}}"/>
98
+ {%- endif -%}
44
99
  {%- if layout == "50-50" -%}
45
100
  <div class="p-section--shallow">
46
101
  <div class="grid-row">
102
+ {% if has_title %}
47
103
  <div class="grid-col-4">
48
104
  <h2>{{ title_text }}</h2>
49
105
  </div>
50
- <div class="grid-col-4 grid-col-medium-4">
106
+ {% endif %}
107
+ <div class="grid-col-4 grid-col-start-large-5 grid-col-medium-4 grid-col-start-medium-5">
51
108
  {{ _list(links) }}
52
109
  </div>
53
110
  </div>
@@ -55,21 +112,29 @@
55
112
  {%- elif layout == "25-75" -%}
56
113
  <div class="p-section--shallow">
57
114
  <div class="grid-row">
115
+ {% if has_title %}
58
116
  <div class="grid-col-2">
59
117
  <h2>{{ title_text }}</h2>
60
118
  </div>
61
- <div class="grid-col-6">
119
+ {% endif %}
120
+ <div class="grid-col-6 grid-col-start-large-3">
62
121
  {{ _list(links) }}
63
122
  </div>
64
123
  </div>
65
124
  </div>
66
125
  {%- else -%}
67
- <div class="p-section--shallow">
68
- <div class="grid-row">
69
- <h2>{{- title_text -}}</h2>
126
+ {% if has_title %}
127
+ <div class="p-section--shallow">
128
+ <div class="grid-row">
129
+ <h2>{{- title_text -}}</h2>
130
+ </div>
70
131
  </div>
71
- </div>
132
+ {% endif %}
72
133
  {{ _list(links) }}
73
134
  {%- endif -%}
135
+ {%- if padding == "none" -%}
136
+ </div>
137
+ {%- else -%}
74
138
  </section>
139
+ {%- endif -%}
75
140
  {%- endmacro %}
@@ -0,0 +1,160 @@
1
+ {#
2
+ All Params:
3
+ @param title_text: H2 title text
4
+ @param input_label: Label for the input field
5
+ @param checkbox_id: Name for the checkbox field, used for agreeing to something before submitting the form. This may vary based on the backend used to collect form responses.
6
+ @param checkbox_label: Label for the checkbox
7
+ @param layout: Layout variant for the section. Options are "50-50", "25-75", "2-col", and "4-col". Default is "25-75".
8
+ @param form_id: Form ID
9
+ @param form_action: Action URL for the form submission, typically the form endpoint.
10
+ @param return_url: URL to return to after form submission
11
+ @param top_rule_variant (string) (optional): Variant of the top rule, default is "default". Options are "muted", "highlighted", "default", "none".
12
+
13
+ All Slots:
14
+ @slot description: Description text, one or more paragraphs
15
+ @slot addendum: Additional content to include in the form, such as a disclaimer or additional information
16
+ @slot hidden_fields: Additional hidden fields to include in the form
17
+ #}
18
+ {%- macro vf_newsletter_signup(form_id, return_url, title_text, form_action="https://ubuntu.com/marketo/submit", input_label="Work email", checkbox_id="canonicalUpdatesOptIn", checkbox_label="I agree to receive information about Canonical's products and services.", layout="25-75", top_rule_variant="default", caller=None) -%}
19
+ {% set layout = layout | trim %}
20
+ {% if layout not in ['50-50', '25-75', '2-col', '4-col'] %}
21
+ {% set layout = "25-75" %}
22
+ {% endif %}
23
+ {% set top_rule_variant = top_rule_variant | trim %}
24
+ {% if top_rule_variant not in ['muted', 'highlighted', 'default', 'none'] %}
25
+ {% set top_rule_variant = "default" %}
26
+ {% endif %}
27
+ {% set description = caller('description') %}
28
+ {% set addendum = caller('addendum') %}
29
+ {% set hidden_fields = caller('hidden_fields') %}
30
+ {% set is_section_level = layout in ['50-50', '25-75'] %}
31
+ {% set is_grid_level = layout in ['2-col', '4-col'] %}
32
+ {% set heading_level = 'h2' if is_section_level else 'h3' %}
33
+ {%- macro _top_rule() -%}
34
+ {% if top_rule_variant != 'none' %}
35
+ {% set rule_class = 'muted' if top_rule_variant == 'muted' else 'highlight' if top_rule_variant == 'highlighted' %}
36
+ <hr class="p-rule{% if top_rule_variant != 'default' %}--{{ rule_class }}{% endif %} is-fixed-width" />
37
+ {% endif %}
38
+ {%- endmacro -%}
39
+ {%- if is_section_level -%}
40
+ <section class="p-section">
41
+ {%- endif -%}
42
+ {{ _top_rule() | safe }}
43
+ <div class="grid-row--{% if is_section_level %}{{ layout }}-on-large{% else %}25-25-25-25{% endif %}">
44
+ {% if is_grid_level %}
45
+ {%- set upper_limit = 4 if layout == '2-col' else 3 -%}
46
+ {%- for number in range(1, upper_limit) -%}
47
+ {%- set col_content = caller('col_' + number|string) -%}
48
+ <div class="grid-col">{{ col_content | safe }}</div>
49
+ {%- endfor -%}
50
+ {% endif %}
51
+ <div class="grid-col{% if layout == '4-col' %}-4{% endif %} {% if is_grid_level %}p-newsletter-signup--{{ layout }}{% endif %}">
52
+ {%- if is_grid_level -%}
53
+ <hr class="p-rule--muted u-hide--large {% if layout == '2-col' %}u-hide--medium{% endif %}" />
54
+ {%- endif -%}
55
+ <{{ heading_level }}>{{ title_text }}</{{ heading_level }}>
56
+ {% if is_section_level %}</div>{% endif %}
57
+ {% if is_section_level %}<div class="grid-col">{% endif %}
58
+ {{ description | safe }}
59
+ <form action="{{ form_action }}" method="post" id="{{ form_id }}">
60
+ <label class="is-required" for="email">{{ input_label }}:</label>
61
+ <input required
62
+ id="email"
63
+ name="email"
64
+ maxlength="255"
65
+ type="email"
66
+ pattern="^[^ ]+@[^ ]+\.[a-z]{2,26}$"
67
+ required="required" />
68
+ <label class="p-checkbox is-required">
69
+ <input required
70
+ class="p-checkbox__input"
71
+ value="yes"
72
+ aria-labelledby="{{ checkbox_id }}"
73
+ name="{{ checkbox_id }}"
74
+ type="checkbox" />
75
+ <span class="p-checkbox__label" id="{{ checkbox_id }}">{{ checkbox_label }}</span>
76
+ </label>
77
+ {% if addendum %}
78
+ {{ addendum | safe }}
79
+ {% else %}
80
+ <p>
81
+ By submitting this form, I confirm that I have read and agree to <a href="https://ubuntu.com/legal/terms-and-policies/privacy-policy">Canonical's Privacy Policy</a>.
82
+ </p>
83
+ {% endif %}
84
+ <button type="submit" class="u-no-margin--bottom">Sign up</button>
85
+ {% if hidden_fields %}
86
+ {{ hidden_fields | safe }}
87
+ {% else %}
88
+ <input type="hidden"
89
+ aria-hidden="true"
90
+ aria-label="hidden field"
91
+ name="returnURL"
92
+ value="{{ return_url }}" />
93
+ <input type="hidden"
94
+ aria-hidden="true"
95
+ aria-label="hidden field"
96
+ name="formid"
97
+ value="{{ form_id }}" />
98
+ <input type="hidden"
99
+ aria-hidden="true"
100
+ aria-label="hidden field"
101
+ name="productContext"
102
+ id="product-context"
103
+ value="" />
104
+ <input type="hidden"
105
+ aria-hidden="true"
106
+ aria-label="hidden field"
107
+ name="utm_campaign"
108
+ id="utm_campaign"
109
+ value="" />
110
+ <input type="hidden"
111
+ aria-hidden="true"
112
+ aria-label="hidden field"
113
+ name="utm_medium"
114
+ id="utm_medium"
115
+ value="" />
116
+ <input type="hidden"
117
+ aria-hidden="true"
118
+ aria-label="hidden field"
119
+ name="utm_source"
120
+ id="utm_source"
121
+ value="" />
122
+ <input type="hidden"
123
+ aria-hidden="true"
124
+ aria-label="hidden field"
125
+ name="utm_content"
126
+ id="utm_content"
127
+ value="" />
128
+ <input type="hidden"
129
+ aria-hidden="true"
130
+ aria-label="hidden field"
131
+ name="utm_term"
132
+ id="utm_term"
133
+ value="" />
134
+ <input type="hidden"
135
+ aria-hidden="true"
136
+ aria-label="hidden field"
137
+ name="GCLID__c"
138
+ id="GCLID__c"
139
+ value="" />
140
+ <input type="hidden"
141
+ aria-hidden="true"
142
+ aria-label="hidden field"
143
+ name="Facebook_Click_ID__c"
144
+ id="Facebook_Click_ID__c"
145
+ value="" />
146
+ <input type="hidden"
147
+ aria-hidden="true"
148
+ aria-label="hidden field"
149
+ id="preferredLanguage"
150
+ name="preferredLanguage"
151
+ maxlength="255"
152
+ value="" />
153
+ {% endif %}
154
+ </form>
155
+ </div>
156
+ </div>
157
+ {%- if is_section_level -%}
158
+ </section>
159
+ {%- endif -%}
160
+ {%- endmacro -%}
@@ -1,7 +1,7 @@
1
1
  {#
2
2
  Params
3
3
  - title_text (string) (required): The text to be displayed as the heading
4
- - top_rule_variant (string) (optional): Variant of the top rule, default is "highlighted". Options are "muted", "highlighted", "none".
4
+ - top_rule_variant (string) (optional): Variant of the top rule, default is "default". Options are "muted", "highlighted", "default", "none".
5
5
  - tiers (Array<{
6
6
  tier_name_text: String (optional),
7
7
  tier_price_text: String,
@@ -18,12 +18,12 @@
18
18
  - description (optional): paragraph-style content below the title
19
19
  #}
20
20
  {%- macro vf_pricing_block(
21
- top_rule_variant="highlighted",
21
+ top_rule_variant="default",
22
22
  title_text="",
23
23
  tiers=[]
24
24
  ) -%}
25
25
  {% set top_rule_variant = top_rule_variant | trim %}
26
- {% if top_rule_variant not in ['muted', 'highlighted', 'none'] %}
26
+ {% if top_rule_variant not in ['muted', 'highlighted', 'default', 'none'] %}
27
27
  {% set top_rule_variant = "highlighted" %}
28
28
  {% endif %}
29
29
  {% set section_description = caller('section_description') %}
@@ -80,12 +80,9 @@
80
80
  {%- endfor -%}
81
81
  {%- endmacro -%}
82
82
  {%- macro _top_rule() -%}
83
- {% if top_rule_variant == 'muted' %}
84
- <hr class="p-rule--muted is-fixed-width" />
85
- {% elif top_rule_variant == 'highlighted' %}
86
- <hr class="p-rule--highlight is-fixed-width" />
87
- {% elif top_rule_variant == 'none' %}
88
- {# No rule #}
83
+ {% if top_rule_variant != 'none' %}
84
+ {% set rule_class = 'muted' if top_rule_variant == 'muted' else 'highlight' if top_rule_variant == 'highlighted' %}
85
+ <hr class="p-rule{% if top_rule_variant != 'default' %}--{{ rule_class }}{% endif %} is-fixed-width" />
89
86
  {% endif %}
90
87
  {%- endmacro -%}
91
88
  <div class="p-section">
@@ -3,9 +3,14 @@
3
3
  @param title_text: H2 title text
4
4
  @param list_items (Array<string>)
5
5
  An array of HTML strings to be rendered
6
+ @param item_heading_level: Level of the heading to use, defaults to 2. Can be 2, or 4.
6
7
  #}
7
- {%- macro vf_text_spotlight(title_text, list_items=[], caller=None) -%}
8
+ {%- macro vf_text_spotlight(title_text, item_heading_level=2, list_items=[], caller=None) -%}
8
9
  {% set has_list = list_items | length >= 2 and list_items | length <= 7 %}
10
+ {% set item_heading_level = item_heading_level | trim %}
11
+ {%- if item_heading_level not in [2, 4] -%}
12
+ {%- set item_heading_level = 2 -%}
13
+ {%- endif -%}
9
14
  <section class="p-section">
10
15
  <hr class="p-rule is-fixed-width" />
11
16
  <div class="grid-row--25-75-on-large">
@@ -15,7 +20,7 @@
15
20
  <div class="grid-col">
16
21
  {%- if has_list -%}
17
22
  {%- for list_item in list_items -%}
18
- <p class="p-heading--2">{{- list_item -}}</p>
23
+ <p class="p-heading--{{ item_heading_level }}">{{- list_item -}}</p>
19
24
  {%- if not loop.last %}<hr class="p-rule--muted" />{%- endif %}
20
25
  {%- endfor -%}
21
26
  {%- else -%}