Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/assets/javascripts/attachments/AttachmentsWidget.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,7 @@ export const AttachmentsWidget = ({
<button
ref={attachMoreButtonRef}
type="button"
className="button button-secondary"
className="button button-secondary [anchor-name:--new-feature-attachments]"
data-testid="attachments-open-modal"
aria-labelledby={`${buttonLabelId} ${statusId}`}
onClick={() => setAttachModalOpen(true)}
Expand Down
38 changes: 38 additions & 0 deletions app/assets/javascripts/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,44 @@ $(() => GOVUK.modules.start());
return null;
};
}).call(this);

document.querySelectorAll("[data-new-feature]").forEach(function (banner) {
var featureId = banner.dataset.newFeature;
var lifespan = banner.dataset.lifespan;
var expirationDate = lifespan && new Date(lifespan + "T00:00:00Z");

if (
expirationDate &&
!isNaN(expirationDate) &&
expirationDate <= new Date()
) {
banner.remove();
return;
}

if (featureId && GOVUK.cookie("new-feature-" + featureId)) {
banner.remove();
} else {
banner.classList.remove("hidden");
}
});

document.addEventListener("click", function (event) {
var hideButton = event.target.closest("[data-dismiss-notice]");
if (!hideButton) {
return;
}

var banner = hideButton.closest("[data-new-feature]");
var featureId = banner && banner.dataset.newFeature;
if (!featureId) {
return;
}

GOVUK.cookie("new-feature-" + featureId, "hidden", { days: 365 });
banner.remove();
});

(function () {
"use strict";
var root = this || window;
Expand Down
6 changes: 6 additions & 0 deletions app/assets/stylesheets/tailwind/components/buttons.css
Original file line number Diff line number Diff line change
Expand Up @@ -106,5 +106,11 @@
@apply bg-yellow text-blue outline-none border-white no-underline;
}

.bg-brand-100 .button,
.bg-brand-100 a.button {
box-shadow: 0 2px 0 theme("colors.brand.300");
@apply bg-white text-black;
}

/*! purgecss end ignore */
}
12 changes: 12 additions & 0 deletions app/assets/stylesheets/tailwind/components/layout.css
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,17 @@
@apply mx-0 pb-0;
}

.new-feature-banner {
anchor-name: --new-feature-container;
}
.new-feature-banner::after {
content: "";
position: fixed;
position-anchor: var(--anchored-to);
left: anchor(var(--anchored-to) center);
top: anchor(--new-feature-container top);
@apply w-10 h-10 -translate-x-1/2 -translate-y-1/2 rotate-45 bg-brand-100;
}

/*! purgecss end ignore */
}
142 changes: 142 additions & 0 deletions app/main/views/storybook.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import re
from pathlib import Path
from uuid import UUID

from flask import Response, jsonify, render_template, request
from flask_wtf import FlaskForm as Form
Expand Down Expand Up @@ -115,6 +116,147 @@ def __init__(self, *args, **kwargs):
self.autocomplete.choices = ["Halifax", "Montreal", "Ottawa", "Toronto"]


class TestTemplateUser:
is_authenticated = True
platform_admin = False
mobile_number = None
verified_phonenumber = False

def has_permissions(self, *permissions, **kwargs):
return True

def has_template_folder_permission(self, folder, service=None):
return True


class TestTemplateService:
id = "test-service"
name = "GC Notify prototype service"
email_from = "notify"
prefix_sms = ""
trial_mode = False
pending_live = False

def get_template_path(self, template):
return [
{"id": None, "name": "Templates", "template_type": None},
template,
]

def has_permission(self, permission):
return permission == "upload_document"


class TestTemplate:
id = UUID("8f4db2e2-7b89-4c2d-9f70-2f3d6e4b9a11")
template_type = "email"
placeholders = True

def __init__(self):
self._template = {
"id": str(self.id),
"name": "Service update: your application",
"folder": None,
"template_type": self.template_type,
"archived": False,
"updated_at": "2026-08-20T14:30:00.000000Z",
"redact_personalisation": False,
}

def __str__(self):
return """<div class="email-message mb-12">
<table class="email-message-meta mb-12 leading-tight font-normal border border-solid border-gray-grey2">
<tbody>
<tr>
<th class="email-message-table pl-doubleGutter text-gray-grey1" scope="row">From</th>
<td class="email-message-table">GC Notify prototype service</td>
</tr>
<tr>
<th class="email-message-table pl-doubleGutter text-gray-grey1" scope="row">To</th>
<td class="email-message-table"><mark class="placeholder">((email address))</mark></td>
</tr>
<tr class="email-message-meta m-0 text-smaller leading-tight font-normal">
<th class="email-message-table pl-doubleGutter text-gray-grey1" scope="row">Subject</th>
<td class="email-message-table">Service update: your application</td>
</tr>
</tbody>
</table>

<div class="email-message-body w-full relative break-words box-border px-doubleGutter border-gray-grey2 border">
<div style="max-width: 580px; margin: 0 auto;">
<div style="margin: 20px auto 30px auto;">
<img src="https://assets.notification.canada.ca/gc-logo-en.png" alt="Government of Canada" height="55" width="281">
</div>

<div style="padding: 0 10px" dir="ltr">
<p style="Margin: 0 0 20px 0; font-size: 19px; line-height: 25px; color: #0B0C0C;">Your application has been received and is now being reviewed.</p>
</div>

<div style="margin: 10px 0 20px 0; float: right">
<img src="https://assets.notification.canada.ca/canada-logo.png" alt="Symbol of the Government of Canada / Symbole du gouvernement du Canada" height="55" width="123">
</div>
<div style="clear:both;"></div>
</div>
</div>
</div>"""


@main.route("/_template-a")
def test_template_a():
template = TestTemplate()

def test_template_url_for(*args, **kwargs):
return "#"

return render_template(
"views/templates/template-a.html",
current_service=TestTemplateService(),
current_user=TestTemplateUser(),
template=template,
url_for=test_template_url_for,
fragment_count=1,
file_attachments_enabled_for_service=True,
template_attachments=[],
user_has_template_permission=True,
heading="Ready to send?",
notification_type="email",
dailyLimit=1000,
dailyUsed=120,
dailyRemaining=880,
yearlyLimit=100000,
yearlyUsed=12000,
yearlyRemaining=88000,
)


@main.route("/_template-b")
def test_template_b():
template = TestTemplate()

def test_template_url_for(*args, **kwargs):
return "#"

return render_template(
"views/templates/template-b.html",
current_service=TestTemplateService(),
current_user=TestTemplateUser(),
template=template,
url_for=test_template_url_for,
fragment_count=1,
file_attachments_enabled_for_service=True,
template_attachments=[],
user_has_template_permission=True,
heading="Ready to send?",
notification_type="email",
dailyLimit=1000,
dailyUsed=120,
dailyRemaining=880,
yearlyLimit=100000,
yearlyUsed=12000,
yearlyRemaining=88000,
)


@main.route("/_storybook", methods=["GET", "POST"])
def storybook():
component = None
Expand Down
1 change: 1 addition & 0 deletions app/models/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,7 @@ def accept_invite(self):

class AnonymousUser(AnonymousUserMixin):
# set the anonymous user so that if a new browser hits us we don't error http://stackoverflow.com/a/19275188
platform_admin = False

def logged_in_elsewhere(self):
return False
Expand Down
9 changes: 9 additions & 0 deletions app/templates/components/new-feature.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{% macro new_feature(title, cta="", cta_link="", anchored_to="", lifespan="") %}
<div style="--anchored-to:--{{ anchored_to }};" class="new-feature-banner hidden bg-brand-100 flex flex-col gap-4 p-gutterHalf" data-new-feature="{{ anchored_to }}" data-lifespan="{{ lifespan }}">
<p class="max-w-[80ch] mb-4"><span class="font-[600]">{{ _("New:") }}</span> {{ title }}</p>
<div class="flex gap-4">
<a class="button" href="{{ cta_link }}">{{ cta }}</a>
<button type="button" class="button-link" data-dismiss-notice>{{ _('Dismiss') }}</button>
</div>
</div>
{% endmacro %}
41 changes: 36 additions & 5 deletions app/templates/views/storybook/indicator.html
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,19 @@
{% from "components/badge.html" import badge %}
{% from "components/task-shortcut.html" import task_shortcut %}
{% from 'components/remaining-messages.html' import remaining_messages with context %}
{% from 'components/new-feature.html' import new_feature %}


{% block maincolumn_content %}
<div class="heading-large">Indicator component examples</div>
<div class="heading-large">Indicator and new feature banner</div>
<hr />
<p>The indicator component wraps any element and displays a badge in the corner.</p>

<h2 class="heading-medium mt-8">Button with "New" Badge</h2>
<div class="bg-gray-100 px-8 py-16 rounded-md">
<div class="bg-gray-100 px-8 py-16">
{{ indicator('<button type="button" class="button">Inbox</button>', badge('New', variant='blue')) }}
<h3 class="heading-small mb-2">Code</h3>
<div class="bg-gray-200 px-8 pb-8 rounded-md border-1 border-gray-300">
<div class="bg-gray-200 px-8 pb-8 border-1 border-gray-300">
<pre><code>{% raw %}
{{ indicator(
'&lt;button type="button" class="button"&gt;Inbox&lt;/button&gt;',
Expand All @@ -24,7 +25,7 @@ <h3 class="heading-small mb-2">Code</h3>
</div>

<h2 class="heading-medium mt-8">Task Shortcut with Badge</h2>
<div class="bg-gray-100 px-8 py-16 rounded-md">
<div class="bg-gray-100 px-8 py-16">
{{ indicator(
task_shortcut(
description=_("Start here to write a new message."),
Expand All @@ -35,7 +36,7 @@ <h2 class="heading-medium mt-8">Task Shortcut with Badge</h2>
badge('New', variant='blue')
) }}
<h3 class="heading-small mb-2">Code</h3>
<div class="bg-gray-200 px-8 pb-8 rounded-md border-1 border-gray-300">
<div class="bg-gray-200 px-8 pb-8 border-1 border-gray-300">
<pre><code>{% raw %}
{{ indicator(
task_shortcut(
Expand All @@ -49,4 +50,34 @@ <h3 class="heading-small mb-2">Code</h3>
{% endraw %}</code></pre>
</div>
</div>

<h2 class="heading-medium mt-8">New Feature Banner</h2>
<p>The feature banner sits within the page flow. This keeps content visible and accessible.</p>
<p>You can optionally anchor the banner to an element using CSS. A caret points to the anchor, creating a visual connection..</p>
<p>Set the lifespan to stop showing the banner after a specific date. run `make check-expired-banners` to find expired banners. Omit the lifespan option to show the banner indefinitely.</p>
<div class="bg-gray-100 px-8 py-16">
<div class="flex flex-col gap-gutter items-start">
<button type="button" class="button [anchor-name:--new-feature-example]" id="new-feature-example">Anchoring element</button>
{{ new_feature(
title="You can do something with this new thing",
cta="About this new thing",
cta_link="/new-features",
anchored_to="new-feature-example",
lifespan="2100-12-31"
) }}
</div>
<h3 class="heading-small mb-2">Code</h3>
<div class="bg-gray-200 px-8 pb-8 border-1 border-gray-300 overflow-scroll">
<pre><code>{% raw %}
&lt;button type="button" class="button [anchor-name:--new-feature-example]" id="new-feature-example"&gt;Anchoring element&lt;/button&gt;
{{ new_feature(
title=_("You can do something with this new thing"),
cta=_("About this new thing"),
cta_link="/new-features",
anchored_to="new-feature-example",
lifespan="2100-12-31"
) }}
{% endraw %}</code></pre>
</div>
</div>
{% endblock %}
2 changes: 1 addition & 1 deletion app/templates/views/storybook/storybook-menu.html
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
'path': 'focus_management/full-form'
},
{
'name': 'Indicator',
'name': 'Indicator and new feature banner',
'path': 'indicator'
},
{
Expand Down
Loading
Loading