diff --git a/ak/homepage.py b/ak/homepage.py index f7be04b2d..063cbb634 100644 --- a/ak/homepage.py +++ b/ak/homepage.py @@ -5,19 +5,11 @@ from core.constants import SLACK_MEMBER_COUNT from core.models import HomepageSettings from core.templatetags.custom_static import large_static -from libraries.models import LibraryVersion, Tier -from libraries.utils import build_library_intro_context +from libraries.models import FEATURED_LIBRARY_TIERS, Library, LibraryVersion +from libraries.utils import build_library_intro_context, get_documentation_url from news.models import Entry from versions.models import Version -# Fallback code snippet shown on the Get Started card when no featured -# library snippet is available. TODO: replace with a proper empty state. -HELLO_WORLD_SNIPPET = """#include -int main() -{ - std::cout << "Hello, Boost."; -}""" - # Hero illustration for the V3 homepage. HERO_LEGACY_IMAGE_URL_LIGHT = large_static("img/v3/home-page/heros.png") HERO_LEGACY_IMAGE_URL_DARK = large_static("img/v3/home-page/heros_light.png") @@ -45,7 +37,7 @@ def get_v3_featured_library(): return ( LibraryVersion.objects.filter( version=latest_version, - library__tier__in=[Tier.FLAGSHIP, Tier.CORE], + library__tier__in=FEATURED_LIBRARY_TIERS, ) .order_by("?") .first() @@ -152,17 +144,97 @@ def build_join_developers_links(): ] -def build_get_started_code(): - """The Get Started code card. Hardcoded for now; a redesign of this - component is tracked in a separate PR. +def build_library_highlight_carousel(limit=3): + """Per-slide content for the homepage 'Explore battle tested libraries' + carousel. + + Shows the libraries an editor curated in HomepageSettings; when fewer than + `limit` are curated, the remaining slots are filled with random flagship/core + libraries in the latest release (admins may curate more than `limit`). When + admins curate libraries, their pinned order is preserved (fallback top-ups + follow); with no pins, slides are ordered by description length for UI + appearance. Returns a list of dicts (name, category_tags, description, + added_in_version, docs_url); empty when no candidate library exists. """ - return { - "heading": "Get started with our libraries", - "code": HELLO_WORLD_SNIPPET, - "language": "cpp", - "library_slug": "", + latest = Version.objects.most_recent() + highlighted_ids = list( + HomepageSettings.load().highlighted_libraries.values_list("id", flat=True) + ) + library_ids = list(highlighted_ids) + + # Admins may curate more than `limit`; only top up when they curate fewer. + remaining_slots = limit - len(highlighted_ids) + if remaining_slots > 0: + library_ids += list( + Library.objects.filter( + tier__in=FEATURED_LIBRARY_TIERS, + library_version__version=latest, + ) + .exclude(id__in=highlighted_ids) + .distinct() + .order_by("?") + .values_list("id", flat=True)[:remaining_slots] + ) + + libraries = Library.objects.filter(id__in=library_ids).prefetch_related( + "categories" + ) + if not libraries: + return [] + + library_versions = { + lv.library_id: lv + for lv in LibraryVersion.objects.filter(library__in=libraries, version=latest) } + category_list_url = reverse( + "libraries-list", + kwargs={"version_slug": "latest", "library_view_str": "list"}, + ) + + slides_by_id = {} + for library in libraries: + lv = library_versions.get(library.id) + first_version = library.first_boost_version + # Fall back to the library detail page when the latest release has no + # documentation URL, so the CTA always points somewhere useful. + docs_url = get_documentation_url(lv, latest=True) if lv else "" + if not docs_url: + docs_url = reverse( + "library-detail", + kwargs={"version_slug": "latest", "library_slug": library.slug}, + ) + slides_by_id[library.id] = { + "name": library.name, + "category_tags": [ + ( + { + **tag, + "url": f"{category_list_url}?category={tag['slug']}", + "aria_label": f"Browse {tag['label']} libraries", + } + if tag["slug"] + else tag + ) + for tag in library.category_tags + ], + "description": ( + (lv.description if lv else None) or library.description or "" + ), + "added_in_version": (first_version.display_name if first_version else ""), + "docs_url": docs_url, + } + + if highlighted_ids: + # Respect the admin's pinned order; fallback top-ups follow. + return [slides_by_id[lib_id] for lib_id in library_ids] + # No pins: order by description length, then category count, for looks. + return sorted( + slides_by_id.values(), + key=lambda slide: (len(slide["description"]), len(slide["category_tags"])), + reverse=True, + ) + def build_library_intro(): """Library Intro card content for the V3 homepage, or None if no diff --git a/ak/views.py b/ak/views.py index e4425e3a4..45f76f830 100644 --- a/ak/views.py +++ b/ak/views.py @@ -26,8 +26,8 @@ from ak.homepage import ( WHY_BOOST_CARDS, build_community_posts, - build_get_started_code, build_join_developers_links, + build_library_highlight_carousel, build_library_intro, hero_image_context, ) @@ -116,8 +116,8 @@ def get_v3_context_data(self, **kwargs): # Testimonial Card ctx["testimonial_cards"] = get_testimonial_cards(limit=5) - # Get Started Card - ctx["get_started_code"] = build_get_started_code() + # Library Highlight Carousel ("Explore battle tested libraries") + ctx["library_highlight_carousel"] = build_library_highlight_carousel() # Library Intro Card ctx["library_intro"] = build_library_intro() diff --git a/core/migrations/0009_homepagesettings_highlighted_libraries.py b/core/migrations/0009_homepagesettings_highlighted_libraries.py new file mode 100644 index 000000000..dcca74e31 --- /dev/null +++ b/core/migrations/0009_homepagesettings_highlighted_libraries.py @@ -0,0 +1,24 @@ +# Generated by Django 6.0.2 on 2026-06-30 16:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("core", "0008_popularsearchterm_popularsearchtermexclusion"), + ("libraries", "0041_category_short_description"), + ] + + operations = [ + migrations.AddField( + model_name="homepagesettings", + name="highlighted_libraries", + field=models.ManyToManyField( + blank=True, + help_text="Libraries shown in the homepage 'Explore battle tested libraries' carousel. Only flagship and core libraries present in the latest stable release are listed. If empty, a random selection of flagship or core libraries is used.", + related_name="+", + to="libraries.library", + ), + ), + ] diff --git a/core/models.py b/core/models.py index 8bd76c53a..86df0eebe 100644 --- a/core/models.py +++ b/core/models.py @@ -144,18 +144,20 @@ def wordcloud_ignore_set(self): class HomepageSettingsForm(WagtailAdminModelForm): - """Scopes the featured-library chooser to flagship and core libraries - present in the latest stable release, A→Z.""" + """Scopes the library choosers to flagship and core libraries present in + the latest stable release, A→Z.""" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) - from libraries.models import Library, Tier + from libraries.models import FEATURED_LIBRARY_TIERS, Library - qs = Library.objects.filter(tier__in=[Tier.FLAGSHIP, Tier.CORE]) + qs = Library.objects.filter(tier__in=FEATURED_LIBRARY_TIERS) latest = Version.objects.most_recent() if latest: qs = qs.filter(library_version__version=latest) - self.fields["featured_library"].queryset = qs.order_by(Lower("name")) + qs = qs.order_by(Lower("name")) + self.fields["featured_library"].queryset = qs + self.fields["highlighted_libraries"].queryset = qs @register_setting @@ -178,7 +180,22 @@ class HomepageSettings(BaseGenericSetting): ), ) - panels = [FieldPanel("featured_library")] + highlighted_libraries = models.ManyToManyField( + "libraries.Library", + blank=True, + related_name="+", + help_text=( + "Libraries shown in the homepage 'Explore battle tested " + "libraries' carousel. Only flagship and core libraries present in " + "the latest stable release are listed. If empty, a random " + "selection of flagship or core libraries is used." + ), + ) + + panels = [ + FieldPanel("featured_library"), + FieldPanel("highlighted_libraries"), + ] class Meta: verbose_name = "Homepage Settings" diff --git a/core/views.py b/core/views.py index 0729fd92b..04d4cda10 100644 --- a/core/views.py +++ b/core/views.py @@ -1852,6 +1852,47 @@ def get_context_data(self, **kwargs): }, ] + context["demo_library_highlight_slides"] = [ + { + "name": "Beast", + "category_tags": [ + {"label": "Concurrent", "slug": "concurrent"}, + {"label": "IO", "slug": "io"}, + ], + "description": ( + "Portable HTTP, WebSocket, and network operations using only " + "C++11 and Boost.Asio. Designed for performance at scale." + ), + "added_in_version": "1.66.0", + "docs_url": "#", + }, + { + "name": "Asio", + "category_tags": [ + {"label": "Concurrent", "slug": "concurrent"}, + {"label": "IO", "slug": "io"}, + ], + "description": ( + "Cross-platform C++ library for network and low-level I/O " + "programming with a consistent asynchronous model." + ), + "added_in_version": "1.35.0", + "docs_url": "#", + }, + { + "name": "Hana", + "category_tags": [ + {"label": "Metaprogramming", "slug": "metaprogramming"}, + ], + "description": ( + "A modern C++ metaprogramming library for computations on both " + "types and values." + ), + "added_in_version": "1.61.0", + "docs_url": "#", + }, + ] + context["learn_card_data"] = { "title": "I want to learn:", "text": "How to install Boost, use its libraries, build projects, and get help when you need it.", diff --git a/libraries/models.py b/libraries/models.py index f6da98e97..dc51da9bd 100644 --- a/libraries/models.py +++ b/libraries/models.py @@ -223,6 +223,10 @@ class Tier(models.IntegerChoices): LEGACY = 40, "Legacy" +# Tiers eligible to be featured on the V3 homepage (spotlight + highlight carousel). +FEATURED_LIBRARY_TIERS = [Tier.FLAGSHIP, Tier.CORE] + + class Library(models.Model): """ Model to represent component Libraries of Boost @@ -442,13 +446,12 @@ def github_properties(self): @cached_property def first_boost_version(self): """Returns the first Boost version that included this library""" - if not self.library_version.exists(): - return - return ( - self.library_version.order_by("version__release_date", "version__name") + first = ( + self.library_version.select_related("version") + .order_by("version__release_date", "version__name") .first() - .version ) + return first.version if first else None @cached_property def github_owner(self): diff --git a/static/css/v3/components.css b/static/css/v3/components.css index 290733966..52e77bbb6 100644 --- a/static/css/v3/components.css +++ b/static/css/v3/components.css @@ -5,6 +5,7 @@ @import "./avatar.css"; @import "./carousel-buttons.css"; @import "./cards-carousel.css"; +@import "./library-highlight-carousel.css"; @import "./badge.css"; @import "./count-badge.css"; @import "./v3-examples-section.css"; diff --git a/static/css/v3/library-highlight-carousel.css b/static/css/v3/library-highlight-carousel.css new file mode 100644 index 000000000..9680d3f59 --- /dev/null +++ b/static/css/v3/library-highlight-carousel.css @@ -0,0 +1,213 @@ +/* + Library Highlight carousel — "Explore battle tested libraries". + CSS-only, radio-driven (no JavaScript): hidden radios translate the track + and swap in the active slide's nav block; prev/next wrap cyclically. + Dependencies: carousel-buttons.css, category-tags.css, buttons.css +*/ + +.library-highlight-carousel { + display: flex; + flex-direction: column; + gap: var(--space-medium); + padding: var(--space-large) 0; + width: 100%; + max-width: 458px; + min-width: 0; + box-sizing: border-box; + background: var(--color-surface-weak); + border: 1px solid var(--color-stroke-weak); + border-radius: var(--border-radius-xl); + overflow: hidden; +} + +.library-highlight-carousel__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 var(--space-large); + gap: var(--space-medium); + width: 100%; +} + +.library-highlight-carousel__heading { + margin: 0; + font-family: var(--font-display); + font-size: var(--font-size-large); + font-weight: var(--font-weight-medium); + line-height: 1.1; + letter-spacing: -0.24px; + color: var(--color-text-primary); + min-width: 0; +} + +/* Hidden radios are the single source of carousel state. */ +.library-highlight-carousel__radio { + position: absolute; + width: 1px; + height: 1px; + padding: 0; + margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* One nav block per slide; only the active slide's block is shown via the + per-state rules emitted in the template. */ +.library-highlight-carousel__nav { + display: none; +} + +/* ── Translate track: one slide per view ───────── */ + +.library-highlight-carousel__track { + flex: 1 1 auto; + min-height: 0; + overflow: hidden; +} + +.library-highlight-carousel__list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + gap: 0; + align-items: stretch; + height: 100%; + min-width: 0; + transition: transform 0.3s ease; + will-change: transform; +} + +.library-highlight-carousel__item { + list-style: none; + flex: 0 0 100%; + min-width: 100%; + display: flex; + flex-direction: column; +} + +/* ── Yellow library card ───────────────────────── */ + +.library-highlight-carousel__card { + display: flex; + flex-direction: column; + gap: var(--space-medium); + padding: var(--space-large); + margin: 0 var(--space-large); + background: var(--color-surface-weak-accent-yellow); + border: 1px solid var(--color-accent-strong-yellow); + border-radius: var(--border-radius-l); +} + +.library-highlight-carousel__card-head { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + justify-content: space-between; + gap: var(--space-medium); +} + +.library-highlight-carousel__name { + margin: 0; + font-family: var(--font-display); + font-size: var(--font-size-large); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-tight); + letter-spacing: var(--letter-spacing-tight); + color: var(--color-text-primary); + min-width: 0; +} + +.library-highlight-carousel__tags { + display: flex; + flex-wrap: wrap; + justify-content: flex-start; + gap: var(--space-xs); + min-width: 0; +} + +.library-highlight-carousel__description { + margin: 0; + padding: 0; + color: var(--color-text-secondary); + font-size: var(--font-size-small); + line-height: var(--line-height-relaxed); + letter-spacing: var(--letter-spacing-tight); + display: -webkit-box; + line-clamp: 3; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; + overflow: hidden; +} + +/* ── Static meta row: "Added in" + dots ────────── */ +/* Stays put while the card translates; per-slide content swaps via the + inline :checked rules emitted in the template. */ + +.library-highlight-carousel__meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: var(--space-medium); + padding: 0 var(--space-large); +} + +.library-highlight-carousel__added { + display: none; + color: var(--color-text-secondary); + font-size: var(--font-size-small); + letter-spacing: var(--letter-spacing-tight); + line-height: var(--line-height-relaxed); +} + +.library-highlight-carousel__dots { + list-style: none; + margin: 0 0 0 auto; + padding: 0; + display: flex; + align-items: center; + gap: var(--space-xs); +} + +.library-highlight-carousel__dot { + width: 12px; + height: 12px; + border-radius: var(--border-radius-s); + background: var(--color-surface-strong); + cursor: pointer; +} + +.library-highlight-carousel__divider { + margin: 0; + border: none; + border-top: 1px solid var(--color-stroke-weak); + width: 100%; + height: 0; +} + +/* One CTA per slide stacked in place; only the active one is shown. */ +.library-highlight-carousel__button { + display: none; + margin: 0 var(--space-large); +} + +.library-highlight-carousel__empty { + margin: 0; + color: var(--color-text-secondary); +} + +/* Dark mode overrides */ +html.dark { + .library-highlight-carousel__card { + border: none; + } + + .library-highlight-carousel__tags .category-tag { + background: var(--color-primary-grey-100); + color: var(--color-primary-grey-800); + } +} diff --git a/static/css/v3/v3-homepage.css b/static/css/v3/v3-homepage.css index 55bdd38c1..425129385 100644 --- a/static/css/v3/v3-homepage.css +++ b/static/css/v3/v3-homepage.css @@ -132,8 +132,12 @@ body:has(.homepage-v3) .header { max-width: 100%; } + .card-masonry-top .item-d .library-highlight-carousel { + max-width: 100%; + } + .card-masonry-top { - grid-template-columns: repeat(2, 1fr); + grid-template-columns: repeat(2, minmax(0, 1fr)); grid-template-areas: "a f" "a c" @@ -145,6 +149,10 @@ body:has(.homepage-v3) .header { ; } + .card-masonry-top > div { + min-width: 0; + } + .card-masonry-top .item-f { display: contents; grid-area: f; diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html index 06d00bb9a..5cb627b1e 100644 --- a/templates/v3/examples/_v3_example_section.html +++ b/templates/v3/examples/_v3_example_section.html @@ -280,6 +280,15 @@

{{ section_title }}

{% endwith %} + {% with section_title="Library Highlight Carousel" %} +
+

{{ section_title }}

+
+ {% include "v3/includes/_library_highlight_carousel.html" with carousel_id="library-highlight-demo" slides=demo_library_highlight_slides %} +
+
+ {% endwith %} + {% with section_title="Detail card carousel with autoplay" %}

{{ section_title }}

diff --git a/templates/v3/homepage.html b/templates/v3/homepage.html index 145733f0b..c3ca6705f 100644 --- a/templates/v3/homepage.html +++ b/templates/v3/homepage.html @@ -31,8 +31,7 @@ {% include "v3/includes/_post_card.html" with heading="Posts from the Boost Community" items=community_posts variant="card" theme="teal" primary_cta_label="View all posts" primary_cta_url=posts_url %}
- {% url 'library-detail' version_slug='latest' library_slug=get_started_code.library_slug as get_started_url %} - {% include "v3/includes/_code_block_card.html" with heading=get_started_code.heading code=get_started_code.code language=get_started_code.language block_variant="grey-bg" button_text="View library" button_url=get_started_url button_aria_label="Explore examples" %} + {% include "v3/includes/_library_highlight_carousel.html" with carousel_id="library-highlight" slides=library_highlight_carousel %}
{% include "v3/includes/_join_card.html" with heading="Join developers building the future of C++" items=join_developers_links variant="card" theme="yellow" primary_cta_label="Explore the community" primary_cta_url=community_url %} diff --git a/templates/v3/includes/_library_highlight_carousel.html b/templates/v3/includes/_library_highlight_carousel.html new file mode 100644 index 000000000..bb35698ee --- /dev/null +++ b/templates/v3/includes/_library_highlight_carousel.html @@ -0,0 +1,148 @@ +{% comment %} + Library Highlight carousel — "Explore battle tested libraries". + CSS-only, radio-driven carousel (no JavaScript), mirroring the testimonial + card: hidden radios are the state. Only the yellow card translates; the meta + row (added-in + dots), divider and CTA stay static and swap their per-slide + content via the inline + + {# Hidden radios drive all carousel state via the per-state rules above. #} + {% for slide in slides %} + + {% endfor %} + {% else %} + + {% endif %} + + + + {# Translating track — only the yellow cards move. #} + + + {# Static meta row — content swaps per active slide, position never moves. #} + + + + + {# Static CTA — one per slide stacked in place, only the active one shown. #} + {% for slide in slides %} + {% with cnt=forloop.counter|stringformat:"s" %} + {% include "v3/includes/_button.html" with label="View documentation" url=slide.docs_url extra_classes="btn-flex library-highlight-carousel__button library-highlight-carousel__button--"|add:cnt %} + {% endwith %} + {% endfor %} + {% else %} + + {# Empty state: no libraries configured and no fallback available. #} + + {% endif %} + +{% endwith %}