Skip to content
Merged
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
30 changes: 30 additions & 0 deletions core/templatetags/number_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""
Template filters for number formatting.
"""

from django import template

register = template.Library()


@register.filter
def compact_number(value):
"""
Format integers in compact form: 2300 → 2.3k, 33000 → 33k, 1500000 → 1.5M.
Values under 1000 are shown as-is. Non-numeric values are returned unchanged.
"""
if value is None:
return ""
try:
n = int(value)
except (TypeError, ValueError):
return value
if n < 1000:
return str(n)
if n < 1_000_000:
k = n / 1000
formatted = f"{k:.1f}".rstrip("0").rstrip(".")
return f"{formatted}k"
m = n / 1_000_000
formatted = f"{m:.1f}".rstrip("0").rstrip(".")
return f"{formatted}M"
20 changes: 20 additions & 0 deletions static/css/v3/badge.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* V3 Badge – small non-clickable badge (e.g. count on carousel cards).
* Uses Icon/Brand Accent background and Text/On Accent for text.
*/

.badge-v3 {
display: flex;
width: 16px;
height: 16px;
justify-content: center;
align-items: center;
aspect-ratio: 1 / 1;
box-sizing: border-box;
border-radius: var(--s, 4px);
background: var(--Icon-Brand-Accent, #FFA000);
font-size: 10px;
line-height: 1;
color: var(--color-text-on-accent);
pointer-events: none;
}
9 changes: 7 additions & 2 deletions static/css/v3/carousel-buttons.css
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,15 @@
height: 12px;
}

.carousel-buttons .btn-carousel:hover {
.carousel-buttons .btn-carousel:hover:not(:disabled) {
color: var(--color-secondary-dark-blue, #0077B8);
}

.carousel-buttons .btn-carousel[data-hover] {
.carousel-buttons .btn-carousel[data-hover]:not(:disabled) {
color: var(--color-secondary-dark-blue, #0077B8);
}

.carousel-buttons .btn-carousel:disabled {
opacity: 0.5;
cursor: not-allowed;
}
1 change: 1 addition & 0 deletions static/css/v3/components.css
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
@import "./buttons.css";
@import "./avatar.css";
@import "./carousel-buttons.css";
@import "./badge.css";
@import "./v3-examples-section.css";
@import "./header.css";
@import "./footer.css";
Expand Down
15 changes: 14 additions & 1 deletion static/css/v3/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,21 @@ a:hover .content-detail-icon:not(.content-detail-icon--contained) {
display: block;
}

.content-detail-icon__badge-slot {
position: absolute;
top: var(--space-card);
right: var(--space-card);
}

.content-detail-icon__title {
margin: 0;
font-size: var(--font-size-base);
font-weight: var(--font-weight-medium);
line-height: var(--line-height-default);
}

.content-detail-icon--has-icon .content-detail-icon__title {
.content-detail-icon--has-icon .content-detail-icon__title,
.content-detail-icon--has-badge .content-detail-icon__title {
padding-right: var(--space-icon-offset);
}

Expand Down Expand Up @@ -87,6 +94,12 @@ a:hover .content-detail-icon:not(.content-detail-icon--contained) {
.content-detail-icon__cta:hover {
text-decoration: underline;
}
.content-detail-icon__cta,
.content-detail-icon__cta:focus,
.content-detail-icon__cta:focus-visible {
text-decoration: underline;
text-decoration-skip-ink: none;
}

.content-detail-icon--contained {
background: transparent;
Expand Down
15 changes: 15 additions & 0 deletions static/css/v3/post-cards.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
text-decoration: none;
}

.post-cards .content-detail-icon__cta,
.post-cards .content-card__cta {
text-decoration: underline;
text-decoration-skip-ink: none;
}

.post-cards .content-detail-icon__cta:hover,
.post-cards .content-detail-icon__cta:focus,
.post-cards .content-detail-icon__cta:focus-visible,
.post-cards .content-card__cta:hover,
.post-cards .content-card__cta:focus,
.post-cards .content-card__cta:focus-visible {
text-decoration: underline;
}

.post-cards {
font-family: var(--font-sans);
color: var(--color-text-primary);
Expand Down
35 changes: 33 additions & 2 deletions static/js/carousel.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,24 @@

const CAROUSEL_STEP_PX_FALLBACK = 320;
const SCROLL_RESET_EPSILON = 2;
const SCROLL_END_EPSILON = 1;
const DEFAULT_AUTOPLAY_MS = 4000;
const CAROUSEL_ITEM_SELECTOR = '[data-carousel-item]';

function updateArrowState(track, prevBtn, nextBtn) {
if (!track || !prevBtn || !nextBtn) return;
const maxScroll = track.scrollWidth - track.clientWidth;
if (maxScroll <= 0) {
prevBtn.disabled = true;
nextBtn.disabled = true;
return;
}
const atStart = track.scrollLeft <= 0;
const atEnd = track.scrollLeft >= maxScroll - SCROLL_END_EPSILON;
prevBtn.disabled = atStart;
nextBtn.disabled = atEnd;
}

function getStepPx(track) {
const first = track.querySelector(CAROUSEL_ITEM_SELECTOR);
if (first) {
Expand Down Expand Up @@ -138,10 +153,26 @@
const prevBtn = controls.querySelector('[data-carousel-prev]');
const nextBtn = controls.querySelector('[data-carousel-next]');
if (prevBtn) {
prevBtn.addEventListener('click', function () { scrollCarousel(track, 'prev', true); });
prevBtn.addEventListener('click', function () {
if (prevBtn.disabled) return;
scrollCarousel(track, 'prev', true);
});
}
if (nextBtn) {
nextBtn.addEventListener('click', function () { scrollCarousel(track, 'next', true); });
nextBtn.addEventListener('click', function () {
if (nextBtn.disabled) return;
scrollCarousel(track, 'next', true);
});
}

const syncArrows = function () {
updateArrowState(track, prevBtn, nextBtn);
};
requestAnimationFrame(syncArrows);
track.addEventListener('scroll', syncArrows, { passive: true });
if (typeof ResizeObserver !== 'undefined') {
const ro = new ResizeObserver(syncArrows);
ro.observe(track);
}

const autoplayDelay = root.getAttribute('data-carousel-autoplay');
Expand Down
10 changes: 10 additions & 0 deletions templates/v3/includes/_badge_v3.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{% comment %}
V3 Badge – small non-clickable count/label badge.
Variables:
value (required) — number or text to show. Numbers >= 1000 are shown compact (e.g. 2.3k, 33k).
Usage:
{% include "v3/includes/_badge_v3.html" with value=1 %}
{% include "v3/includes/_badge_v3.html" with value=count %}
{% endcomment %}
{% load number_filters %}
<span class="badge-v3" aria-hidden="true">{{ value|compact_number }}</span>
2 changes: 1 addition & 1 deletion templates/v3/includes/_cards_carousel_v3.html
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ <h2 id="{{ carousel_id }}-heading" class="cards-carousel__heading">{{ heading }}
{% if cards %}
{% for card in cards %}
<li class="post-cards__item" data-carousel-item>
{% include "v3/includes/_content_detail_card_item.html" with title=card.title description=card.description icon_name=card.icon_name cta_label=card.cta_label cta_href=card.cta_href %}
{% include "v3/includes/_content_detail_card_item.html" with title=card.title description=card.description badge_count=forloop.counter cta_label="Start here" cta_href=card.cta_href %}
</li>
{% endfor %}
{% endif %}
Expand Down
13 changes: 9 additions & 4 deletions templates/v3/includes/_content_detail_card_item.html
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,21 @@
title (required) — card heading
description (required) — card body text
icon_name (optional) — icon name for includes/icon.html (e.g. "bullseye-arrow").
If omitted, card is rendered without icon and modifier --has-icon is not applied.
If omitted (and badge_count not set), card is rendered without icon and modifier --has-icon is not applied.
badge_count (optional) — when set (e.g. in carousel), shows a non-clickable count badge instead of icon.
title_url (optional) — if set, title is wrapped in a link
cta_label (optional) — if set with cta_href, renders a CTA link below description
cta_href (optional) — used with cta_label for the CTA link
Usage:
{% include "v3/includes/_content_detail_card_item.html" with title="Get help" description="Tap into quick answers..." icon_name="bullseye-arrow" %}
With CTA: {% include "v3/includes/_content_detail_card_item.html" with title="Get help" description="..." icon_name="info-box" cta_label="Start here" cta_href="#" %}
With badge (e.g. carousel): {% include "..." with title="..." description="..." badge_count=1 %}
{% endcomment %}
<article class="content-detail-icon{% if icon_name %} content-detail-icon--has-icon{% endif %}">
{% if icon_name %}
<article class="content-detail-icon{% if icon_name %} content-detail-icon--has-icon{% endif %}{% if badge_count %} content-detail-icon--has-badge{% endif %}">
{% if badge_count %}
<div class="content-detail-icon__badge-slot">
{% include "v3/includes/_badge_v3.html" with value=badge_count %}
</div>
{% elif icon_name %}
<div class="content-detail-icon__icon" aria-hidden="true">
{% include "includes/icon.html" with icon_name=icon_name icon_class="content-detail-icon__icon-svg" icon_size=24 %}
</div>
Expand Down
Loading