Skip to content

Commit 760dba9

Browse files
petesfrenchjmuzina
andauthored
feat(Pricing block): Implement pricing block component & pattern (#5489)
* feat: Create placeholder for 'grid-row-equal-height' that uses ch based container queries for responsiveness * feat: Create 'p-pricing-block' pattern * feat: Create 'vf_pricing_block' jinja macro * feat: Handle optional parameters * feat: Migrate to media queries * feat: Address comments from code review * feat: Remove top border * feat: Appease linter * feat: Add documentation for pricing block jinja template * jinja code embedding accepts optional matching ending block name * feat: Update with comments from review * chore: Bump version to 4.23.0 * style: Format prettier * fix: Run prettier & fix typo * feat: Make pricing tiers span 2 columns on tablet --------- Co-authored-by: Julie Muzina <jmuzina@jmuzina.io>
1 parent 115611a commit 760dba9

14 files changed

Lines changed: 965 additions & 2 deletions

File tree

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "vanilla-framework",
3-
"version": "4.22.0",
3+
"version": "4.23.0",
44
"author": {
55
"email": "webteam@canonical.com",
66
"name": "Canonical Webteam"

releases.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
- version: 4.23.0
2+
features:
3+
- component: Pricing block
4+
url: /docs/patterns/pricing-block
5+
status: New
6+
notes: We've introduced the new pricing block jinja pattern.
17
- version: 4.22.0
28
features:
39
- component: Icons / Heading, Quote

scss/_base_grid-definitions.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,19 @@
112112
}
113113
}
114114

115+
%vf-grid-row-subgrid {
116+
@extend %vf-grid-row;
117+
118+
@supports (display: grid) {
119+
& > * {
120+
display: grid;
121+
grid-column: span calc($grid-8-columns / 4);
122+
grid-row: span 100;
123+
grid-template-rows: subgrid;
124+
}
125+
}
126+
}
127+
115128
%vf-grid-container-padding {
116129
// set static outside padding per breakpoint
117130
padding-left: map-get($grid-margin-widths, small);

scss/_patterns_pricing-block.scss

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*
2+
@classreference
3+
pricing-block:
4+
Pricing block:
5+
.p-pricing-block:
6+
Main element of the pricing block component, to represent 4 cards.
7+
.p-pricing-block--25-75:
8+
Modifier class for 3 cards with a 25-75 split. The first 25% row is empty.
9+
.p-pricing-block--50-50:
10+
Modifier class for 2 cards, where each card takes up 50% of the row.
11+
Column:
12+
.p-pricing-block__tier:
13+
Column element within a pricing block. Each column represents a card.
14+
*/
15+
16+
@import 'settings';
17+
18+
@mixin vf-p-pricing-block {
19+
.p-pricing-block,
20+
.p-pricing-block--25-75,
21+
.p-pricing-block--50-50 {
22+
@extend %vf-grid-row-subgrid;
23+
24+
.p-pricing-block__tier {
25+
grid-column: span $grid-8-columns-small;
26+
padding-bottom: $spv--x-large;
27+
28+
@media (min-width: $threshold-4-6-col) {
29+
grid-column: span calc($grid-8-columns-medium / 2);
30+
}
31+
}
32+
}
33+
34+
.p-pricing-block--25-75 .p-pricing-block__tier {
35+
@media (min-width: $threshold-6-12-col) {
36+
&:first-child {
37+
grid-column: calc($grid-8-columns / 4 + 1) / span calc($grid-8-columns / 4); // Shift the first card to the right
38+
}
39+
}
40+
}
41+
42+
.p-pricing-block--50-50 .p-pricing-block__tier {
43+
@media (min-width: $threshold-6-12-col) {
44+
grid-column: span $grid-8-columns-small;
45+
}
46+
}
47+
}

scss/_vanilla.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
@import 'patterns_navigation-reduced';
3939
@import 'patterns_notifications';
4040
@import 'patterns_pagination';
41+
@import 'patterns_pricing-block';
4142
@import 'patterns_pull-quotes';
4243
@import 'patterns_rule';
4344
@import 'patterns_search-and-filter';
@@ -134,6 +135,7 @@
134135
@include vf-p-navigation-reduced;
135136
@include vf-p-notification;
136137
@include vf-p-pagination;
138+
@include vf-p-pricing-block;
137139
@include vf-p-pull-quotes;
138140
@include vf-p-rule;
139141
@include vf-p-search-and-filter;
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
@import '../vanilla';
2+
@include vf-base;
3+
4+
// vf-p-grid is needed for this pattern to work
5+
@include vf-p-grid;
6+
@include vf-p-pricing-block;

side-navigation.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@
123123
url: /docs/patterns/tiered-list
124124
- title: Quote Wrapper
125125
url: /docs/patterns/quote-wrapper
126+
- title: Pricing block
127+
url: /docs/patterns/pricing-block
126128
- heading: Utilities
127129
ordering: alphabetical
128130
subheadings:
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
{#
2+
Params
3+
- title_text (string) (required): The text to be displayed as the heading
4+
- tiers (Array<{
5+
tier_name_text: String,
6+
tier_price_text: String,
7+
tier_price_explanation: String,
8+
tier_description_html?: String,
9+
tier_label_text: String,
10+
tier_offerings: Array<{
11+
list_item_style?: ‘ticked’ | ‘crossed’,
12+
list_item_content_html: string
13+
}>‘,
14+
cta_html?: String
15+
}>) (required)
16+
Slots
17+
- description (optional): paragraph-style content below the title
18+
#}
19+
{%- macro vf_pricing_block(
20+
title_text="",
21+
tiers=[]
22+
) -%}
23+
{% set section_description = caller('section_description') %}
24+
{% set has_tier_descriptions = (tiers | selectattr("tier_description_html") | list | length) > 0 %}
25+
{% if tiers|length == 2 %}
26+
{% set base_class_name = "p-pricing-block--50-50" %}
27+
{% elif tiers|length == 3 %}
28+
{% set base_class_name = "p-pricing-block--25-75" %}
29+
{% else %}
30+
{% set base_class_name = "p-pricing-block" %}
31+
{% endif %}
32+
33+
{%- macro _tier_offerings_list(tier) %}
34+
<p class="u-text--muted">What's included</p>
35+
<hr class="p-rule--muted u-no-margin--bottom" />
36+
<ul class="p-list--divided">
37+
{%- for offering in tier.get("tier_offerings") %}
38+
{%- set item_style = offering.get('list_item_style', 'undefined') -%}
39+
{% if item_style == 'ticked' -%}
40+
{% set item_class = "is-ticked" -%}
41+
{% elif item_style == 'crossed' -%}
42+
{% set item_class = "is-crossed u-text--muted" -%}
43+
{% else -%}
44+
{% set item_class = "has-bullet" -%}
45+
{%- endif -%}
46+
<li class="p-list__item {{ item_class }}">{{ offering.list_item_content_html }}</li>
47+
{% endfor -%}
48+
</ul>
49+
{%- endmacro -%}
50+
51+
<div class="p-section">
52+
<div class="p-section--shallow">
53+
<hr class="p-rule--highlight is-fixed-width" />
54+
<div class="grid-row">
55+
<div class="grid-col-4 grid-col-medium-4 grid-col-small-4">
56+
<h2>{{ title_text }}</h2>
57+
</div>
58+
{%- if section_description -%}
59+
<div class="grid-col-4 grid-col-medium-4 grid-col-small-4">{{ section_description }}</div>
60+
{%- endif -%}
61+
</div>
62+
</div>
63+
64+
<div class="{{ base_class_name }}">
65+
{%- for tier in tiers %}
66+
<div class="p-pricing-block__tier">
67+
<div class="p-pricing-block__section">
68+
<div class="p-section--shallow">
69+
<h3 class="p-heading--4">
70+
{{ tier.get("tier_name_text") }}
71+
<br />
72+
<strong>{{ tier.get("tier_price_text") }}</strong>
73+
<span class="u-text--muted">{{ tier.get("tier_price_explanation") }}</span>
74+
</h3>
75+
</div>
76+
</div>
77+
{%- if has_tier_descriptions -%}
78+
{% set description = tier.get("tier_description_html") if tier.get("tier_description_html") else '' %}
79+
<div class="p-pricing-block__section"
80+
{% if not description %}role="presentation"{% endif %}>{{ description }}</div>
81+
{%- endif %}
82+
<div class="p-pricing-block__section">{{ _tier_offerings_list(tier) }}</div>
83+
{% if tier.cta_html %}<div class="p-pricing-block__section">{{ tier.cta_html }}</div>{% endif -%}
84+
</div>
85+
{%- endfor -%}
86+
</div>
87+
</div>
88+
{% endmacro -%}
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
{% extends '_layouts/examples.html' %}
2+
{% from '_macros/vf_pricing-block.jinja' import vf_pricing_block %}
3+
{% block title %}Pricing block / 2-blocks{% endblock %}
4+
{% block standalone_css %}patterns_all{% endblock %}
5+
{% block content %}
6+
{% call(slot) vf_pricing_block(
7+
title_text='Kubernetes pricing',
8+
tiers=[
9+
{
10+
'tier_name_text': 'Kubernetes Explorer',
11+
'tier_price_text': '$3099',
12+
'tier_price_explanation': 'per year, per VM',
13+
'tier_description_html': '<p>Three-day Kubernetes training to ramp up your team\'s skills.</p>',
14+
'tier_label_text': 'What\'s included',
15+
'tier_offerings': [
16+
{
17+
'list_item_style': 'ticked',
18+
'list_item_content_html': 'High availability'
19+
},
20+
{
21+
'list_item_style': 'ticked',
22+
'list_item_content_html': '<a href="#">Public clouds</a>, VMware, <a href="#">OpenStack</a>'
23+
},
24+
{
25+
'list_item_style': 'ticked',
26+
'list_item_content_html': 'Storage and SDN basic options'
27+
},
28+
{
29+
'list_item_style': 'ticked',
30+
'list_item_content_html': 'Logging, monitoring, alerting'
31+
},
32+
{
33+
'list_item_style': 'ticked',
34+
'list_item_content_html': 'Kubernetes lifecycle management'
35+
},
36+
{
37+
'list_item_style': 'ticked',
38+
'list_item_content_html': 'Security essentials'
39+
},
40+
{
41+
'list_item_style': 'crossed',
42+
'list_item_content_html': 'Support or Fully Managed'
43+
},
44+
{
45+
'list_item_style': 'crossed',
46+
'list_item_content_html': 'Third-party integrations'
47+
},
48+
{
49+
'list_item_style': 'crossed',
50+
'list_item_content_html': 'GPU acceleration'
51+
},
52+
{
53+
'list_item_style': 'crossed',
54+
'list_item_content_html': 'GPU acceleration'
55+
}
56+
],
57+
'cta_html': '<a href="#">Three-day Kubernetes training to ramp up your team’s skills.</a>'
58+
},
59+
{
60+
'tier_name_text': 'Kubernetes Explorer',
61+
'tier_price_text': '$3099',
62+
'tier_price_explanation': 'per year, per VM',
63+
'tier_description_html': '<p>Three-day Kubernetes training to ramp up your team\'s skills. Three-day Kubernetes training to ramp up your team\'s skills.</p>',
64+
'tier_label_text': 'What\'s included',
65+
'tier_offerings': [
66+
{
67+
'list_item_style': 'ticked',
68+
'list_item_content_html': 'High availability'
69+
},
70+
{
71+
'list_item_style': 'ticked',
72+
'list_item_content_html': '<a href="#">Public clouds</a>, VMware, <a href="#">OpenStack</a>'
73+
},
74+
{
75+
'list_item_style': 'ticked',
76+
'list_item_content_html': 'Storage and SDN basic options'
77+
},
78+
{
79+
'list_item_style': 'ticked',
80+
'list_item_content_html': 'Logging, monitoring, alerting'
81+
},
82+
{
83+
'list_item_style': 'ticked',
84+
'list_item_content_html': 'Kubernetes lifecycle management'
85+
},
86+
{
87+
'list_item_style': 'ticked',
88+
'list_item_content_html': 'Security essentials'
89+
},
90+
{
91+
'list_item_style': 'crossed',
92+
'list_item_content_html': 'Support or Fully Managed'
93+
},
94+
{
95+
'list_item_style': 'crossed',
96+
'list_item_content_html': 'Third-party integrations'
97+
},
98+
{
99+
'list_item_style': 'crossed',
100+
'list_item_content_html': 'GPU acceleration'
101+
}
102+
],
103+
'cta_html': '<a href="#">Three-day Kubernetes training to ramp up your team’s skills.</a>'
104+
}
105+
]
106+
) -%}
107+
{%- if slot == 'section_description' -%}
108+
<p>
109+
A low total cost of ownership. Up to five times lower than public clouds. A low total cost of ownership. Up to five times lower than public clouds. A low total cost of ownership. Up to five times lower than public clouds.
110+
</p>
111+
{%- endif -%}
112+
{% endcall %}
113+
{% endblock content %}

0 commit comments

Comments
 (0)