From 4f0d8e81941c6000174c2d26ace486b5e81e185c Mon Sep 17 00:00:00 2001 From: julioest Date: Tue, 31 Mar 2026 02:01:05 -0400 Subject: [PATCH 01/14] feat: Add Library Item component (list + card variants) #2204 --- core/views.py | 48 +++++ static/css/v3/components.css | 1 + static/css/v3/library-item.css | 193 ++++++++++++++++++ .../v3/examples/_v3_example_section.html | 24 +++ templates/v3/includes/_library_item.html | 61 ++++++ 5 files changed, 327 insertions(+) create mode 100644 static/css/v3/library-item.css create mode 100644 templates/v3/includes/_library_item.html diff --git a/core/views.py b/core/views.py index 488b42869..74eb553d0 100644 --- a/core/views.py +++ b/core/views.py @@ -1989,4 +1989,52 @@ def get_context_data(self, **kwargs): else: context["example_library_not_found"] = library_slug context["example_library_slug"] = library_slug + + demo_library_items = [] + demo_libs_qs = Library.objects.filter( + slug__in=["geometry", "asio", "filesystem"] + ).prefetch_related("categories", "authors") + for lib in demo_libs_qs: + lv = ( + LibraryVersion.objects.filter(version=latest, library=lib).first() + if latest + else None + ) + cats = [ + {"label": cat.name, "url": "#", "variant": "neutral"} + for cat in lib.categories.all()[:3] + ] + author = lib.authors.first() + demo_library_items.append( + { + "library_name": lib.display_name_short, + "library_url": reverse( + "library-detail", + kwargs={ + "version_slug": "latest", + "library_slug": lib.slug, + }, + ), + "description": lib.description or "", + "categories": cats, + "cpp_version": ( + lv.get_cpp_standard_minimum_display() + if lv and lv.cpp_standard_minimum + else "C++03" + ), + "contributor_name": author.display_name if author else "Unknown", + "contributor_role": "Contributor", + "contributor_avatar_url": f"{settings.STATIC_URL}img/v3/demo_page/Avatar.png", + "contributor_badge_url": f"{badge_img}/badge-first-place.png", + "doc_url": reverse( + "library-detail", + kwargs={ + "version_slug": "latest", + "library_slug": lib.slug, + }, + ), + } + ) + context["demo_library_items"] = demo_library_items + return context diff --git a/static/css/v3/components.css b/static/css/v3/components.css index 244ab17b5..45e5661dd 100644 --- a/static/css/v3/components.css +++ b/static/css/v3/components.css @@ -46,3 +46,4 @@ @import "./post-card.css"; @import "./event-card.css"; @import "./badge-button.css"; +@import "./library-item.css"; diff --git a/static/css/v3/library-item.css b/static/css/v3/library-item.css new file mode 100644 index 000000000..78aa01acd --- /dev/null +++ b/static/css/v3/library-item.css @@ -0,0 +1,193 @@ +/* ========================================================================== + Library Item — list row and card variants + ========================================================================== */ + +.library-item-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + width: 100%; +} + +.library-item { + box-sizing: border-box; +} + +.library-item p { + margin: 0; + padding: 0; +} + +/* ========================================================================== + List variant (horizontal row) + ========================================================================== */ + +.library-item--list { + display: grid; + grid-template-columns: 205px 1fr 220px 220px auto; + max-width: 1408px; + align-items: flex-start; + row-gap: var(--space-large); + column-gap: var(--space-xl); + padding: var(--space-large); + padding-top: var(--space-default); + border-bottom: 1px solid var(--color-stroke-weak); +} + +.library-item--list .library-item__header { + display: contents; +} + +.library-item--list .library-item__description { + overflow: hidden; +} + +.library-item--list .library-item__tags { + display: flex; + align-items: center; + gap: var(--space-s); +} + +.library-item--list .library-item__contributor { + display: flex; + align-items: center; + gap: var(--space-default); +} + +.library-item--list .library-item__actions { + display: flex; + align-items: center; + gap: var(--space-xl); +} + +.library-item .version-tag { + border-radius: var(--border-radius-xxl); +} + +.library-item .version-tag--default:hover { + background: var(--color-tag-fill-hover); +} + +.library-item .btn-icon-library { + padding: 0; + border: none; + background: none; +} + +/* ========================================================================== + Card variant (vertical stack) + ========================================================================== */ + +.library-item--card { + display: flex; + flex-direction: column; + max-width: 320px; + width: 100%; + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-stroke-weak); + background: var(--color-surface-weak); +} + +.library-item--card .library-item__header { + display: flex; + flex-direction: column; + gap: var(--space-default); + padding: var(--space-large); +} + +.library-item--card .library-item__tags { + display: flex; + flex-wrap: wrap; + gap: var(--space-s); + padding: 0 var(--space-large) var(--space-large); +} + +.library-item--card .library-item__actions { + display: flex; + align-items: center; + gap: var(--space-default); + padding: 0 var(--space-large) var(--space-large); +} + +.library-item--card .library-item__contributor { + display: flex; + align-items: center; + gap: var(--space-default); + padding: var(--space-large); + border-top: 1px solid var(--color-stroke-weak); + margin-top: auto; +} + +/* ========================================================================== + Shared element styles + ========================================================================== */ + +.library-item__name { + font-family: var(--font-display); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-tight); + letter-spacing: var(--letter-spacing-display-regular); + color: var(--color-text-primary); + text-decoration: none; +} + +a.library-item__name:hover { + text-decoration: underline; +} + +.library-item__description { + font-size: var(--font-size-small); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + letter-spacing: -0.14px; + color: var(--color-text-secondary); +} + +.library-item__contributor-info { + display: flex; + flex-direction: column; + gap: var(--space-xs); +} + +.library-item__contributor-name { + font-size: var(--font-size-xs); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-default); + color: var(--color-text-primary); +} + +.library-item__contributor-role { + display: inline-flex; + align-items: center; + gap: var(--space-s); + font-size: var(--font-size-xs); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + color: var(--color-text-secondary); +} + +.library-item__contributor-badge { + flex-shrink: 0; +} + +.library-item__contributor-badge img { + width: var(--space-large); + height: var(--space-large); +} + +/* ========================================================================== + Mobile + ========================================================================== */ + +@media (max-width: 767px) { + .library-item--list { + grid-template-columns: 1fr; + } + + .library-item--list .library-item__tags { + flex-wrap: wrap; + } +} diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html index f41cc455f..f5a0b4e40 100644 --- a/templates/v3/examples/_v3_example_section.html +++ b/templates/v3/examples/_v3_example_section.html @@ -223,6 +223,30 @@

{{ section_title }}

{% endwith %} {% endif %} + {% with section_title="Library Item (List)" %} +
+

{{ section_title }}

+
+
    + {% for item in demo_library_items %} + {% include "v3/includes/_library_item.html" with variant="list" library_name=item.library_name library_url=item.library_url description=item.description categories=item.categories cpp_version=item.cpp_version contributor_name=item.contributor_name contributor_role=item.contributor_role contributor_avatar_url=item.contributor_avatar_url contributor_badge_url=item.contributor_badge_url doc_url=item.doc_url %} + {% endfor %} +
+
+
+ {% endwith %} + + {% with section_title="Library Item (Card)" %} +
+

{{ section_title }}

+
+ {% for item in demo_library_items %} + {% include "v3/includes/_library_item.html" with variant="card" library_name=item.library_name library_url=item.library_url description=item.description categories=item.categories cpp_version=item.cpp_version contributor_name=item.contributor_name contributor_role=item.contributor_role contributor_avatar_url=item.contributor_avatar_url contributor_badge_url=item.contributor_badge_url doc_url=item.doc_url %} + {% endfor %} +
+
+ {% endwith %} + {% if example_library_choices %} {% with section_title="Commits per release" %}
diff --git a/templates/v3/includes/_library_item.html b/templates/v3/includes/_library_item.html new file mode 100644 index 000000000..21d9fa163 --- /dev/null +++ b/templates/v3/includes/_library_item.html @@ -0,0 +1,61 @@ +{% comment %} + Library Item — displays a Boost library in list or card layout. + + Variables: + variant (string, required) - "list" | "card" + library_name (string, required) - Library display name, e.g. "Boost.Geometry" + library_url (string, optional) - Link for the library name + description (string, required) - Short library description + categories (list, required) - List of {label, url, variant} dicts for category tags + cpp_version (string, required) - e.g. "C++ 14" — rendered as neutral tag + contributor_name (string, required) - Contributor display name + contributor_role (string, optional) - Contributor role, e.g. "Contributor". Omit to hide. + contributor_avatar_url (string, optional) - Avatar image URL + contributor_badge_url (string, optional) - Badge icon image URL + doc_url (string, required) - Documentation link URL + extra_attrs (string, optional) - Extra HTML attributes for analytics + + Usage (list): + {% include "v3/includes/_library_item.html" with variant="list" library_name="Boost.Geometry" library_url="#" description="A library for solving geometry problems." categories=cats cpp_version="C++ 14" contributor_name="Barend Gehrels" contributor_avatar_url="/img/avatar.png" doc_url="/libs/geometry/doc/" %} + + Usage (card): + {% include "v3/includes/_library_item.html" with variant="card" library_name="Boost.Asio" description="Portable networking and low-level I/O." categories=cats cpp_version="C++ 11" contributor_name="Christopher Kohlhoff" doc_url="/libs/asio/doc/" %} +{% endcomment %} +{% if variant == "list" %}
  • {% else %}
    {% endif %} + +
    + {% if library_url %} + {{ library_name }} + {% else %} + {{ library_name }} + {% endif %} +

    {{ description }}

    +
    + +
    + {% for cat in categories %} + {% include "v3/includes/_category_tag.html" with tag_label=cat.label url=cat.url variant=cat.variant size="tight" %} + {% endfor %} +
    + +
    + {% include "v3/includes/_avatar_v3.html" with src=contributor_avatar_url name=contributor_name size="xl" %} +
    + {{ contributor_name }} + {% if contributor_role %} + + {{ contributor_role }} + {% if contributor_badge_url %} + + {% endif %} + + {% endif %} +
    +
    + +
    + {{ cpp_version }} + {% include "v3/includes/_button.html" with url=doc_url label="" icon_name="documentation" icon_size=16 style="icon-library" aria_label="View documentation for "|add:library_name %} +
    + +{% if variant == "list" %}
  • {% else %}
    {% endif %} From bbb920bb26643d7402d5480d2cec48d18f91aa5e Mon Sep 17 00:00:00 2001 From: julioest Date: Thu, 2 Apr 2026 13:08:38 -0400 Subject: [PATCH 02/14] fix: Use correct border token for dark mode neutral category tag --- static/css/v3/category-tags.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/static/css/v3/category-tags.css b/static/css/v3/category-tags.css index 24eac43d4..3cb2904c8 100644 --- a/static/css/v3/category-tags.css +++ b/static/css/v3/category-tags.css @@ -99,7 +99,7 @@ /* Dark theme: use semantic variables from themes.css only (no raw primitives) */ html.dark .category-tag--neutral { background: var(--color-tag-neutral-bg); - border-color: var(--color-tag-neutral-border); + border-color: var(--color-tag-stroke); color: var(--color-text-secondary); } From 3159401ca09f7ffbbcb13307785fbce1ffc0be70 Mon Sep 17 00:00:00 2001 From: julioest Date: Thu, 2 Apr 2026 13:08:44 -0400 Subject: [PATCH 03/14] fix: Improve Library Item responsive layout and clean up dead CSS --- static/css/v3/library-item.css | 79 +++++++++++++++++++--------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/static/css/v3/library-item.css b/static/css/v3/library-item.css index 78aa01acd..cc5b49048 100644 --- a/static/css/v3/library-item.css +++ b/static/css/v3/library-item.css @@ -26,13 +26,14 @@ .library-item--list { display: grid; - grid-template-columns: 205px 1fr 220px 220px auto; + grid-template-columns: 2fr 3fr 2fr 2fr auto; max-width: 1408px; align-items: flex-start; row-gap: var(--space-large); column-gap: var(--space-xl); padding: var(--space-large); padding-top: var(--space-default); + background: var(--color-surface-weak); border-bottom: 1px solid var(--color-stroke-weak); } @@ -40,12 +41,9 @@ display: contents; } -.library-item--list .library-item__description { - overflow: hidden; -} - .library-item--list .library-item__tags { display: flex; + flex-wrap: wrap; align-items: center; gap: var(--space-s); } @@ -64,6 +62,7 @@ .library-item .version-tag { border-radius: var(--border-radius-xxl); + cursor: pointer; } .library-item .version-tag--default:hover { @@ -146,36 +145,16 @@ a.library-item__name:hover { color: var(--color-text-secondary); } -.library-item__contributor-info { - display: flex; - flex-direction: column; - gap: var(--space-xs); -} - -.library-item__contributor-name { - font-size: var(--font-size-xs); - font-weight: var(--font-weight-medium); - line-height: var(--line-height-default); - color: var(--color-text-primary); -} - -.library-item__contributor-role { - display: inline-flex; - align-items: center; - gap: var(--space-s); - font-size: var(--font-size-xs); - font-weight: var(--font-weight-regular); - line-height: var(--line-height-default); - color: var(--color-text-secondary); -} -.library-item__contributor-badge { - flex-shrink: 0; -} +/* ========================================================================== + Tablet + ========================================================================== */ -.library-item__contributor-badge img { - width: var(--space-large); - height: var(--space-large); +@media (max-width: 1024px) { + .library-item--list { + grid-template-columns: 2fr 3fr 2fr 2fr auto; + column-gap: var(--space-large); + } } /* ========================================================================== @@ -183,11 +162,43 @@ a.library-item__name:hover { ========================================================================== */ @media (max-width: 767px) { + .library-item-list { + gap: var(--space-large); + } + .library-item--list { - grid-template-columns: 1fr; + display: flex; + flex-direction: column; + border-bottom: none; + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-stroke-weak); + background: var(--color-surface-weak); + } + + .library-item--list .library-item__header { + display: flex; + flex-direction: column; + gap: var(--space-default); + padding: var(--space-large); } .library-item--list .library-item__tags { + display: flex; flex-wrap: wrap; + gap: var(--space-s); + padding: 0 var(--space-large) var(--space-large); + } + + .library-item--list .library-item__actions { + display: flex; + align-items: center; + gap: var(--space-default); + padding: 0 var(--space-large) var(--space-large); + } + + .library-item--list .library-item__contributor { + border-top: 1px solid var(--color-stroke-weak); + margin-top: auto; + padding: var(--space-large); } } From 9d63a1a1809f117b4b422e4300bc3da2aa725ef8 Mon Sep 17 00:00:00 2001 From: julioest Date: Thu, 2 Apr 2026 13:08:56 -0400 Subject: [PATCH 04/14] refactor: Replace Library Item contributor markup with User Profile component --- core/views.py | 16 +++++++++----- .../v3/examples/_v3_example_section.html | 4 ++-- templates/v3/includes/_library_item.html | 22 ++++--------------- 3 files changed, 17 insertions(+), 25 deletions(-) diff --git a/core/views.py b/core/views.py index 74eb553d0..7a64bed38 100644 --- a/core/views.py +++ b/core/views.py @@ -1992,7 +1992,7 @@ def get_context_data(self, **kwargs): demo_library_items = [] demo_libs_qs = Library.objects.filter( - slug__in=["geometry", "asio", "filesystem"] + slug__in=["accumulators", "filesystem", "asio", "geometry", "beast"] ).prefetch_related("categories", "authors") for lib in demo_libs_qs: lv = ( @@ -2022,10 +2022,16 @@ def get_context_data(self, **kwargs): if lv and lv.cpp_standard_minimum else "C++03" ), - "contributor_name": author.display_name if author else "Unknown", - "contributor_role": "Contributor", - "contributor_avatar_url": f"{settings.STATIC_URL}img/v3/demo_page/Avatar.png", - "contributor_badge_url": f"{badge_img}/badge-first-place.png", + "author": { + "name": author.display_name if author else "Unknown", + "role": "Contributor", + "avatar_url": ( + f"https://github.com/{author.github_username}.png" + if author and author.github_username + else f"{settings.STATIC_URL}img/v3/demo_page/Avatar.png" + ), + "badge_url": f"{badge_img}/badge-first-place.png", + }, "doc_url": reverse( "library-detail", kwargs={ diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html index f5a0b4e40..ef0064ac4 100644 --- a/templates/v3/examples/_v3_example_section.html +++ b/templates/v3/examples/_v3_example_section.html @@ -229,7 +229,7 @@

    {{ section_title }}

      {% for item in demo_library_items %} - {% include "v3/includes/_library_item.html" with variant="list" library_name=item.library_name library_url=item.library_url description=item.description categories=item.categories cpp_version=item.cpp_version contributor_name=item.contributor_name contributor_role=item.contributor_role contributor_avatar_url=item.contributor_avatar_url contributor_badge_url=item.contributor_badge_url doc_url=item.doc_url %} + {% include "v3/includes/_library_item.html" with variant="list" library_name=item.library_name library_url=item.library_url description=item.description categories=item.categories cpp_version=item.cpp_version author=item.author doc_url=item.doc_url %} {% endfor %}
    @@ -241,7 +241,7 @@

    {{ section_title }}

    {{ section_title }}

    {% for item in demo_library_items %} - {% include "v3/includes/_library_item.html" with variant="card" library_name=item.library_name library_url=item.library_url description=item.description categories=item.categories cpp_version=item.cpp_version contributor_name=item.contributor_name contributor_role=item.contributor_role contributor_avatar_url=item.contributor_avatar_url contributor_badge_url=item.contributor_badge_url doc_url=item.doc_url %} + {% include "v3/includes/_library_item.html" with variant="card" library_name=item.library_name library_url=item.library_url description=item.description categories=item.categories cpp_version=item.cpp_version author=item.author doc_url=item.doc_url %} {% endfor %}
    diff --git a/templates/v3/includes/_library_item.html b/templates/v3/includes/_library_item.html index 21d9fa163..13aa28407 100644 --- a/templates/v3/includes/_library_item.html +++ b/templates/v3/includes/_library_item.html @@ -8,18 +8,15 @@ description (string, required) - Short library description categories (list, required) - List of {label, url, variant} dicts for category tags cpp_version (string, required) - e.g. "C++ 14" — rendered as neutral tag - contributor_name (string, required) - Contributor display name - contributor_role (string, optional) - Contributor role, e.g. "Contributor". Omit to hide. - contributor_avatar_url (string, optional) - Avatar image URL - contributor_badge_url (string, optional) - Badge icon image URL + author (dict, required) - Author object for _user_profile.html (name, role, avatar_url, badge_url) doc_url (string, required) - Documentation link URL extra_attrs (string, optional) - Extra HTML attributes for analytics Usage (list): - {% include "v3/includes/_library_item.html" with variant="list" library_name="Boost.Geometry" library_url="#" description="A library for solving geometry problems." categories=cats cpp_version="C++ 14" contributor_name="Barend Gehrels" contributor_avatar_url="/img/avatar.png" doc_url="/libs/geometry/doc/" %} + {% include "v3/includes/_library_item.html" with variant="list" library_name="Boost.Geometry" library_url="#" description="A library for solving geometry problems." categories=cats cpp_version="C++ 14" author=author doc_url="/libs/geometry/doc/" %} Usage (card): - {% include "v3/includes/_library_item.html" with variant="card" library_name="Boost.Asio" description="Portable networking and low-level I/O." categories=cats cpp_version="C++ 11" contributor_name="Christopher Kohlhoff" doc_url="/libs/asio/doc/" %} + {% include "v3/includes/_library_item.html" with variant="card" library_name="Boost.Asio" description="Portable networking and low-level I/O." categories=cats cpp_version="C++ 11" author=author doc_url="/libs/asio/doc/" %} {% endcomment %} {% if variant == "list" %}
  • {% else %}
    {% endif %} @@ -39,18 +36,7 @@
    - {% include "v3/includes/_avatar_v3.html" with src=contributor_avatar_url name=contributor_name size="xl" %} -
    - {{ contributor_name }} - {% if contributor_role %} - - {{ contributor_role }} - {% if contributor_badge_url %} - - {% endif %} - - {% endif %} -
    + {% include "v3/includes/_user_profile.html" with author=author %}
    From 81807b90050c3d7650ee4c0ce90b99c73b6f7437 Mon Sep 17 00:00:00 2001 From: julioest Date: Fri, 3 Apr 2026 01:57:54 -0400 Subject: [PATCH 05/14] fix: Filter out deleted stub authors Prefer authors with GitHub usernames for avatar display, falling back to any non-deleted author. --- core/views.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/views.py b/core/views.py index 7a64bed38..907ed8d15 100644 --- a/core/views.py +++ b/core/views.py @@ -1992,7 +1992,7 @@ def get_context_data(self, **kwargs): demo_library_items = [] demo_libs_qs = Library.objects.filter( - slug__in=["accumulators", "filesystem", "asio", "geometry", "beast"] + slug__in=["accumulators", "mysql", "asio", "geometry", "beast"] ).prefetch_related("categories", "authors") for lib in demo_libs_qs: lv = ( @@ -2004,7 +2004,11 @@ def get_context_data(self, **kwargs): {"label": cat.name, "url": "#", "variant": "neutral"} for cat in lib.categories.all()[:3] ] - author = lib.authors.first() + author = ( + lib.authors.exclude(email__startswith="deleted-") + .exclude(github_username="") + .first() + ) or lib.authors.exclude(email__startswith="deleted-").first() demo_library_items.append( { "library_name": lib.display_name_short, From 9c52cead4cedfd9ae9604ef7fa46d6410c13abe2 Mon Sep 17 00:00:00 2001 From: julioest Date: Fri, 3 Apr 2026 02:00:25 -0400 Subject: [PATCH 06/14] fix: Library Item responsive layout #2204 Subgrid for consistent column alignment, card layout for mobile list variant, dual version tag for desktop/mobile, and design token for icon size. --- static/css/v3/library-item.css | 138 +++++++++++++++++------ templates/v3/includes/_library_item.html | 5 +- 2 files changed, 104 insertions(+), 39 deletions(-) diff --git a/static/css/v3/library-item.css b/static/css/v3/library-item.css index cc5b49048..604fa32f9 100644 --- a/static/css/v3/library-item.css +++ b/static/css/v3/library-item.css @@ -6,8 +6,10 @@ list-style: none; margin: 0; padding: 0; - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: 160px 3fr minmax(180px, 2fr) minmax(180px, 2fr) auto; + column-gap: var(--space-xl); + max-width: 1408px; width: 100%; } @@ -26,11 +28,10 @@ .library-item--list { display: grid; - grid-template-columns: 2fr 3fr 2fr 2fr auto; - max-width: 1408px; + grid-column: 1 / -1; + grid-template-columns: subgrid; align-items: flex-start; row-gap: var(--space-large); - column-gap: var(--space-xl); padding: var(--space-large); padding-top: var(--space-default); background: var(--color-surface-weak); @@ -57,16 +58,18 @@ .library-item--list .library-item__actions { display: flex; align-items: center; + justify-content: space-between; gap: var(--space-xl); + min-width: 120px; } -.library-item .version-tag { - border-radius: var(--border-radius-xxl); - cursor: pointer; +/* Show desktop version tag in actions, hide mobile one in tags */ +.library-item__cpp-version { + display: none; } -.library-item .version-tag--default:hover { - background: var(--color-tag-fill-hover); +.library-item__cpp-version--desktop { + display: inline-flex; } .library-item .btn-icon-library { @@ -75,13 +78,18 @@ background: none; } +.library-item .btn-icon-library .btn-icon svg { + width: var(--space-xl); + height: var(--space-xl); +} + /* ========================================================================== - Card variant (vertical stack) + Card layout — used by card variant and mobile list variant ========================================================================== */ .library-item--card { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: 1fr auto; max-width: 320px; width: 100%; border-radius: var(--border-radius-xl); @@ -90,35 +98,54 @@ } .library-item--card .library-item__header { + grid-column: 1; + grid-row: 1; display: flex; flex-direction: column; gap: var(--space-default); padding: var(--space-large); + padding-bottom: 0; } -.library-item--card .library-item__tags { +.library-item--card .library-item__actions { + grid-column: 2; + grid-row: 1; display: flex; - flex-wrap: wrap; - gap: var(--space-s); - padding: 0 var(--space-large) var(--space-large); + align-items: flex-start; + justify-content: center; + padding: var(--space-large); + padding-bottom: 0; } -.library-item--card .library-item__actions { +.library-item--card .library-item__cpp-version--desktop { + display: none; +} + +.library-item--card .library-item__cpp-version { + display: inline-flex; +} + +.library-item--card .library-item__tags { + grid-column: 1 / -1; + grid-row: 2; display: flex; + flex-wrap: wrap; align-items: center; - gap: var(--space-default); - padding: 0 var(--space-large) var(--space-large); + gap: var(--space-s); + padding: var(--space-large); } .library-item--card .library-item__contributor { + grid-column: 1 / -1; + grid-row: 3; display: flex; align-items: center; gap: var(--space-default); padding: var(--space-large); - border-top: 1px solid var(--color-stroke-weak); margin-top: auto; } + /* ========================================================================== Shared element styles ========================================================================== */ @@ -151,9 +178,9 @@ a.library-item__name:hover { ========================================================================== */ @media (max-width: 1024px) { - .library-item--list { - grid-template-columns: 2fr 3fr 2fr 2fr auto; - column-gap: var(--space-large); + .library-item-list { + grid-template-columns: 120px 2fr minmax(100px, 1fr) minmax(140px, 1fr) auto; + column-gap: var(--space-default); } } @@ -163,42 +190,79 @@ a.library-item__name:hover { @media (max-width: 767px) { .library-item-list { - gap: var(--space-large); + display: flex; + flex-direction: column; } .library-item--list { - display: flex; - flex-direction: column; + display: grid; + grid-template-columns: 1fr auto; + border-bottom: 1px solid var(--color-stroke-weak); + border-radius: 0; + background: var(--color-surface-weak); + padding: 0; + } + + .library-item--list:first-child { + border-radius: var(--border-radius-xl) var(--border-radius-xl) 0 0; + } + + .library-item--list:last-child { + border-radius: 0 0 var(--border-radius-xl) var(--border-radius-xl); border-bottom: none; + } + + .library-item--list:only-child { border-radius: var(--border-radius-xl); - border: 1px solid var(--color-stroke-weak); - background: var(--color-surface-weak); } .library-item--list .library-item__header { + grid-column: 1; + grid-row: 1; display: flex; flex-direction: column; gap: var(--space-default); padding: var(--space-large); + padding-bottom: 0; + } + + .library-item--list .library-item__actions { + grid-column: 2; + grid-row: 1; + display: flex; + align-items: flex-start; + justify-content: flex-end; + min-width: unset; + padding: var(--space-large); + padding-bottom: 0; + } + + .library-item--list .library-item__cpp-version--desktop { + display: none; + } + + .library-item--list .library-item__cpp-version { + display: inline-flex; } .library-item--list .library-item__tags { + grid-column: 1 / -1; + grid-row: 2; display: flex; flex-wrap: wrap; + align-items: center; gap: var(--space-s); - padding: 0 var(--space-large) var(--space-large); + padding: var(--space-large); } - .library-item--list .library-item__actions { + .library-item--list .library-item__contributor { + grid-column: 1 / -1; + grid-row: 3; display: flex; align-items: center; gap: var(--space-default); - padding: 0 var(--space-large) var(--space-large); - } - - .library-item--list .library-item__contributor { - border-top: 1px solid var(--color-stroke-weak); - margin-top: auto; + border-top: none; padding: var(--space-large); + margin-top: auto; } } diff --git a/templates/v3/includes/_library_item.html b/templates/v3/includes/_library_item.html index 13aa28407..7769f4bb5 100644 --- a/templates/v3/includes/_library_item.html +++ b/templates/v3/includes/_library_item.html @@ -31,8 +31,9 @@
    {% for cat in categories %} - {% include "v3/includes/_category_tag.html" with tag_label=cat.label url=cat.url variant=cat.variant size="tight" %} + {% include "v3/includes/_category_tag.html" with tag_label=cat.label url=cat.url variant=cat.variant %} {% endfor %} + {{ cpp_version }}
    @@ -40,7 +41,7 @@
    - {{ cpp_version }} + {{ cpp_version }} {% include "v3/includes/_button.html" with url=doc_url label="" icon_name="documentation" icon_size=16 style="icon-library" aria_label="View documentation for "|add:library_name %}
    From cd2ecd95be0165ed14d09bb159840332ca9654e2 Mon Sep 17 00:00:00 2001 From: julioest Date: Fri, 3 Apr 2026 13:30:24 -0400 Subject: [PATCH 07/14] fix: Update demo data and template #2204 Expand demo libraries to match Figma, move tags inside header, fix doc icon viewBox to 16x16. --- core/views.py | 22 ++++++++++++++++--- .../v3/examples/_v3_example_section.html | 2 +- templates/v3/includes/_library_item.html | 15 ++++++------- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/core/views.py b/core/views.py index 907ed8d15..4ca055a20 100644 --- a/core/views.py +++ b/core/views.py @@ -1991,9 +1991,25 @@ def get_context_data(self, **kwargs): context["example_library_slug"] = library_slug demo_library_items = [] - demo_libs_qs = Library.objects.filter( - slug__in=["accumulators", "mysql", "asio", "geometry", "beast"] - ).prefetch_related("categories", "authors") + demo_libs_qs = ( + Library.objects.filter( + slug__in=[ + "accumulators", + "algorithm", + "align", + "any", + "array", + "asio", + "assign", + "atomic", + "beast", + "bimap", + "bind", + ] + ) + .prefetch_related("categories", "authors") + .order_by("name") + ) for lib in demo_libs_qs: lv = ( LibraryVersion.objects.filter(version=latest, library=lib).first() diff --git a/templates/v3/examples/_v3_example_section.html b/templates/v3/examples/_v3_example_section.html index ef0064ac4..b5acf9daf 100644 --- a/templates/v3/examples/_v3_example_section.html +++ b/templates/v3/examples/_v3_example_section.html @@ -239,7 +239,7 @@

    {{ section_title }}

    {% with section_title="Library Item (Card)" %}

    {{ section_title }}

    -
    +
    {% for item in demo_library_items %} {% include "v3/includes/_library_item.html" with variant="card" library_name=item.library_name library_url=item.library_url description=item.description categories=item.categories cpp_version=item.cpp_version author=item.author doc_url=item.doc_url %} {% endfor %} diff --git a/templates/v3/includes/_library_item.html b/templates/v3/includes/_library_item.html index 7769f4bb5..6b2438e05 100644 --- a/templates/v3/includes/_library_item.html +++ b/templates/v3/includes/_library_item.html @@ -27,13 +27,12 @@ {{ library_name }} {% endif %}

    {{ description }}

    -
    - -
    - {% for cat in categories %} - {% include "v3/includes/_category_tag.html" with tag_label=cat.label url=cat.url variant=cat.variant %} - {% endfor %} - {{ cpp_version }} +
    + {% for cat in categories %} + {% include "v3/includes/_category_tag.html" with tag_label=cat.label url=cat.url variant=cat.variant %} + {% endfor %} + {{ cpp_version }} +
    @@ -42,7 +41,7 @@
    {{ cpp_version }} - {% include "v3/includes/_button.html" with url=doc_url label="" icon_name="documentation" icon_size=16 style="icon-library" aria_label="View documentation for "|add:library_name %} + {% include "v3/includes/_button.html" with url=doc_url label="" icon_name="documentation" icon_size=32 icon_viewbox="0 0 16 16" style="icon-library" aria_label="View documentation for "|add:library_name %}
    {% if variant == "list" %}
  • {% else %}{% endif %} From 024790a924ce0172d088ac9fb5cc7c8cd4210dd2 Mon Sep 17 00:00:00 2001 From: julioest Date: Fri, 3 Apr 2026 13:30:33 -0400 Subject: [PATCH 08/14] fix: Card variant and grid layout #2204 Card grid layout, split actions into columns, mobile card overrides, icon sizing, and comments. --- static/css/v3/library-item.css | 192 ++++++++++++++++++++------------- 1 file changed, 116 insertions(+), 76 deletions(-) diff --git a/static/css/v3/library-item.css b/static/css/v3/library-item.css index 604fa32f9..3c28ef643 100644 --- a/static/css/v3/library-item.css +++ b/static/css/v3/library-item.css @@ -2,12 +2,21 @@ Library Item — list row and card variants ========================================================================== */ +/* Demo only: provides a contrasting background for the list variant in + Lookbook/examples so the item surface colours are visible. Remove when + the component is used on a real page with its own background. */ +.v3-examples-section__example-box:has(.library-item-list) { + background: #F7FDFE; +} + +/* ---------- Base / reset ---------- */ + .library-item-list { list-style: none; margin: 0; padding: 0; display: grid; - grid-template-columns: 160px 3fr minmax(180px, 2fr) minmax(180px, 2fr) auto; + grid-template-columns: 160px 3fr minmax(180px, 2fr) minmax(180px, 2fr) auto auto; column-gap: var(--space-xl); max-width: 1408px; width: 100%; @@ -22,16 +31,63 @@ padding: 0; } +/* ---------- Shared element styles ---------- */ + +.library-item__name { + font-family: var(--font-display); + font-size: var(--font-size-base); + font-weight: var(--font-weight-medium); + line-height: var(--line-height-tight); + letter-spacing: var(--letter-spacing-display-regular); + color: var(--color-text-primary); + text-decoration: none; +} + +a.library-item__name:hover { + text-decoration: underline; +} + +.library-item__description { + font-size: var(--font-size-small); + font-weight: var(--font-weight-regular); + line-height: var(--line-height-default); + letter-spacing: -0.14px; /* no matching design token available */ + color: var(--color-text-secondary); +} + +.library-item .btn-icon-library { + padding: 0; + border: none; + background: none; + width: var(--space-large); +} + +.library-item .btn-icon-library .btn-icon svg { + width: 100%; + height: 100%; +} + +/* Version tag visibility — desktop shows the tag inside .library-item__actions, + mobile/card shows the tag inline with category tags. */ +.library-item__cpp-version { + display: none; +} + +.library-item__cpp-version--desktop { + display: inline-flex; +} + /* ========================================================================== - List variant (horizontal row) + List variant — horizontal row (desktop) ========================================================================== */ .library-item--list { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; - align-items: flex-start; + align-items: start; row-gap: var(--space-large); + column-gap: var(--space-large); padding: var(--space-large); padding-top: var(--space-default); background: var(--color-surface-weak); @@ -56,45 +112,36 @@ } .library-item--list .library-item__actions { - display: flex; - align-items: center; - justify-content: space-between; - gap: var(--space-xl); - min-width: 120px; -} - -/* Show desktop version tag in actions, hide mobile one in tags */ -.library-item__cpp-version { - display: none; + display: contents; } -.library-item__cpp-version--desktop { - display: inline-flex; +/* Right-align the doc button with extra spacing from version tag */ +.library-item--list .library-item__actions > .btn-icon-library { + justify-self: end; + margin-left: var(--space-xl); } -.library-item .btn-icon-library { - padding: 0; - border: none; - background: none; -} +/* ---------- Card grid — equal-width columns, equal-height rows ---------- */ -.library-item .btn-icon-library .btn-icon svg { - width: var(--space-xl); - height: var(--space-xl); +.library-item-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: var(--space-large); } /* ========================================================================== - Card layout — used by card variant and mobile list variant + Card variant — vertical stack ========================================================================== */ .library-item--card { display: grid; grid-template-columns: 1fr auto; - max-width: 320px; + column-gap: var(--space-large); width: 100%; border-radius: var(--border-radius-xl); border: 1px solid var(--color-stroke-weak); background: var(--color-surface-weak); + padding: var(--space-large); } .library-item--card .library-item__header { @@ -102,19 +149,15 @@ grid-row: 1; display: flex; flex-direction: column; - gap: var(--space-default); - padding: var(--space-large); - padding-bottom: 0; + gap: var(--space-large); } .library-item--card .library-item__actions { grid-column: 2; grid-row: 1; display: flex; - align-items: flex-start; - justify-content: center; - padding: var(--space-large); - padding-bottom: 0; + align-items: start; + justify-content: end; } .library-item--card .library-item__cpp-version--desktop { @@ -126,66 +169,42 @@ } .library-item--card .library-item__tags { - grid-column: 1 / -1; - grid-row: 2; display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-s); - padding: var(--space-large); } .library-item--card .library-item__contributor { grid-column: 1 / -1; - grid-row: 3; + grid-row: 2; display: flex; align-items: center; gap: var(--space-default); - padding: var(--space-large); margin-top: auto; + padding-top: var(--space-large); } - /* ========================================================================== - Shared element styles - ========================================================================== */ - -.library-item__name { - font-family: var(--font-display); - font-size: var(--font-size-base); - font-weight: var(--font-weight-medium); - line-height: var(--line-height-tight); - letter-spacing: var(--letter-spacing-display-regular); - color: var(--color-text-primary); - text-decoration: none; -} - -a.library-item__name:hover { - text-decoration: underline; -} - -.library-item__description { - font-size: var(--font-size-small); - font-weight: var(--font-weight-regular); - line-height: var(--line-height-default); - letter-spacing: -0.14px; - color: var(--color-text-secondary); -} - - -/* ========================================================================== - Tablet + Tablet (≤ 1024px) ========================================================================== */ @media (max-width: 1024px) { .library-item-list { - grid-template-columns: 120px 2fr minmax(100px, 1fr) minmax(140px, 1fr) auto; + grid-template-columns: 120px 2fr minmax(100px, 1fr) minmax(140px, 1fr) min-content auto; + /* Figma specifies --space-large here, but long descriptions cause overflow + at tablet widths. Using --space-default until content constraints are + defined. */ column-gap: var(--space-default); } + + .library-item--list .library-item__actions > .btn-icon-library { + margin-left: var(--space-default); + } } /* ========================================================================== - Mobile + Mobile (≤ 767px) — list items switch to card-like layout ========================================================================== */ @media (max-width: 767px) { @@ -194,13 +213,14 @@ a.library-item__name:hover { flex-direction: column; } + /* Container — stacked cards with shared borders */ .library-item--list { display: grid; grid-template-columns: 1fr auto; border-bottom: 1px solid var(--color-stroke-weak); border-radius: 0; background: var(--color-surface-weak); - padding: 0; + padding: var(--space-large); } .library-item--list:first-child { @@ -216,27 +236,29 @@ a.library-item__name:hover { border-radius: var(--border-radius-xl); } + /* Header — name + description, left column */ .library-item--list .library-item__header { grid-column: 1; grid-row: 1; display: flex; flex-direction: column; gap: var(--space-default); - padding: var(--space-large); + padding-bottom: 0; } + /* Actions — doc icon only, top-right */ .library-item--list .library-item__actions { grid-column: 2; grid-row: 1; display: flex; - align-items: flex-start; - justify-content: flex-end; + align-items: start; + justify-content: end; min-width: unset; - padding: var(--space-large); - padding-bottom: 0; + padding: 0; } + /* Version tag swap — hide desktop, show mobile (inline with tags) */ .library-item--list .library-item__cpp-version--desktop { display: none; } @@ -245,6 +267,7 @@ a.library-item__name:hover { display: inline-flex; } + /* Tags — category tags + C++ version, full width */ .library-item--list .library-item__tags { grid-column: 1 / -1; grid-row: 2; @@ -252,9 +275,10 @@ a.library-item__name:hover { flex-wrap: wrap; align-items: center; gap: var(--space-s); - padding: var(--space-large); + } + /* Contributor — full width, bottom */ .library-item--list .library-item__contributor { grid-column: 1 / -1; grid-row: 3; @@ -262,7 +286,23 @@ a.library-item__name:hover { align-items: center; gap: var(--space-default); border-top: none; - padding: var(--space-large); + margin-top: auto; } + + /* ---------- Card variant — mobile overrides ---------- */ + + .library-item--card { + max-width: none; + padding: var(--space-large); + gap: var(--space-large); + } + + .library-item--card .library-item__header, + .library-item--card .library-item__actions, + .library-item--card .library-item__tags, + .library-item--card .library-item__contributor { + padding: 0; + } + } From 3048d54b52674edaca2cf77d6ea16b6123782611 Mon Sep 17 00:00:00 2001 From: julioest Date: Mon, 6 Apr 2026 19:09:51 -0400 Subject: [PATCH 09/14] fix: Use real GitHub avatar data #2204 Replace fabricated github.com/{username}.png URLs with actual avatar data from CommitAuthor/User thumbnail. Falls back to initials circle instead of static placeholder image. --- core/views.py | 42 ++++++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/core/views.py b/core/views.py index 4ca055a20..6d1f932b1 100644 --- a/core/views.py +++ b/core/views.py @@ -1245,6 +1245,23 @@ class V3ComponentDemoView(TemplateView): template_name = "base.html" + @staticmethod + def _get_author_avatar(author): + """Return the best available avatar URL for a library author (User). + + Returns empty string when no image is available so the avatar + template falls back to a colored initials circle. + """ + if not author: + return "" + url = author.get_thumbnail_url() + if url: + return url + ca = getattr(author, "commitauthor", None) + if ca and getattr(ca, "avatar_url", None): + return ca.avatar_url + return "" + def get_context_data(self, **kwargs): from django.urls import reverse from libraries.models import Library, LibraryVersion @@ -1252,6 +1269,7 @@ def get_context_data(self, **kwargs): build_library_intro_context, get_commit_data_by_release_for_library, commit_data_to_stats_bars, + patch_commit_authors, ) CODE_DEMO_BEAST = """int main() @@ -2010,6 +2028,18 @@ def get_context_data(self, **kwargs): .prefetch_related("categories", "authors") .order_by("name") ) + # Collect one author per demo library and patch CommitAuthor data + demo_authors = {} + for lib in demo_libs_qs: + author = ( + lib.authors.exclude(email__startswith="deleted-") + .exclude(github_username="") + .first() + ) or lib.authors.exclude(email__startswith="deleted-").first() + if author: + demo_authors[lib.pk] = author + patch_commit_authors(list(demo_authors.values())) + for lib in demo_libs_qs: lv = ( LibraryVersion.objects.filter(version=latest, library=lib).first() @@ -2020,11 +2050,7 @@ def get_context_data(self, **kwargs): {"label": cat.name, "url": "#", "variant": "neutral"} for cat in lib.categories.all()[:3] ] - author = ( - lib.authors.exclude(email__startswith="deleted-") - .exclude(github_username="") - .first() - ) or lib.authors.exclude(email__startswith="deleted-").first() + author = demo_authors.get(lib.pk) demo_library_items.append( { "library_name": lib.display_name_short, @@ -2045,11 +2071,7 @@ def get_context_data(self, **kwargs): "author": { "name": author.display_name if author else "Unknown", "role": "Contributor", - "avatar_url": ( - f"https://github.com/{author.github_username}.png" - if author and author.github_username - else f"{settings.STATIC_URL}img/v3/demo_page/Avatar.png" - ), + "avatar_url": self._get_author_avatar(author), "badge_url": f"{badge_img}/badge-first-place.png", }, "doc_url": reverse( From ade3425a00f9ef3a9ecc2b5652fcdcffc82f50dd Mon Sep 17 00:00:00 2001 From: julioest Date: Mon, 6 Apr 2026 19:09:55 -0400 Subject: [PATCH 10/14] fix: Avatar variant context leak #2204 Add 'only' to user_profile include to prevent parent variant="list" leaking into the avatar template, which rendered avatar--list (no bg) instead of avatar--yellow. --- templates/v3/includes/_library_item.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/templates/v3/includes/_library_item.html b/templates/v3/includes/_library_item.html index 6b2438e05..25118327b 100644 --- a/templates/v3/includes/_library_item.html +++ b/templates/v3/includes/_library_item.html @@ -36,7 +36,7 @@
    - {% include "v3/includes/_user_profile.html" with author=author %} + {% include "v3/includes/_user_profile.html" with author=author only %}
    From ad37ff77451888820262a6b2a332f32fa1fc0632 Mon Sep 17 00:00:00 2001 From: julioest Date: Tue, 7 Apr 2026 01:33:08 -0400 Subject: [PATCH 11/14] fix: Clean up library-item CSS #2204 Reorganize by variant with co-located breakpoints, remove redundant declarations, fix card border visibility in dark mode. --- static/css/v3/category-tags.css | 2 +- static/css/v3/library-item.css | 210 +++++++++++++++----------------- 2 files changed, 102 insertions(+), 110 deletions(-) diff --git a/static/css/v3/category-tags.css b/static/css/v3/category-tags.css index 3cb2904c8..24eac43d4 100644 --- a/static/css/v3/category-tags.css +++ b/static/css/v3/category-tags.css @@ -99,7 +99,7 @@ /* Dark theme: use semantic variables from themes.css only (no raw primitives) */ html.dark .category-tag--neutral { background: var(--color-tag-neutral-bg); - border-color: var(--color-tag-stroke); + border-color: var(--color-tag-neutral-border); color: var(--color-text-secondary); } diff --git a/static/css/v3/library-item.css b/static/css/v3/library-item.css index 3c28ef643..f87d8d715 100644 --- a/static/css/v3/library-item.css +++ b/static/css/v3/library-item.css @@ -6,29 +6,32 @@ Lookbook/examples so the item surface colours are visible. Remove when the component is used on a real page with its own background. */ .v3-examples-section__example-box:has(.library-item-list) { - background: #F7FDFE; + background: var(--color-surface-mid); } -/* ---------- Base / reset ---------- */ +/* ---------- Containers ---------- */ .library-item-list { list-style: none; margin: 0; padding: 0; display: grid; - grid-template-columns: 160px 3fr minmax(180px, 2fr) minmax(180px, 2fr) auto auto; + grid-template-columns: 160px 2fr 1fr 1fr 1fr auto; column-gap: var(--space-xl); max-width: 1408px; width: 100%; } -.library-item { - box-sizing: border-box; +.library-item-card-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: var(--space-large); } -.library-item p { - margin: 0; - padding: 0; +@media (max-width: 1024px) { + .library-item-card-grid { + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + } } /* ---------- Shared element styles ---------- */ @@ -47,11 +50,16 @@ a.library-item__name:hover { text-decoration: underline; } +.library-item p { + margin: 0; + padding: 0; +} + .library-item__description { font-size: var(--font-size-small); font-weight: var(--font-weight-regular); line-height: var(--line-height-default); - letter-spacing: -0.14px; /* no matching design token available */ + letter-spacing: var(--letter-spacing-tight); color: var(--color-text-secondary); } @@ -75,12 +83,16 @@ a.library-item__name:hover { .library-item__cpp-version--desktop { display: inline-flex; + width: fit-content; + justify-self: center; } /* ========================================================================== - List variant — horizontal row (desktop) + List variant ========================================================================== */ +/* ---------- Desktop ---------- */ + .library-item--list { display: grid; grid-column: 1 / -1; @@ -102,12 +114,13 @@ a.library-item__name:hover { display: flex; flex-wrap: wrap; align-items: center; + justify-self: start; gap: var(--space-s); } .library-item--list .library-item__contributor { display: flex; - align-items: center; + align-items: start; gap: var(--space-default); } @@ -121,91 +134,27 @@ a.library-item__name:hover { margin-left: var(--space-xl); } -/* ---------- Card grid — equal-width columns, equal-height rows ---------- */ - -.library-item-card-grid { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); - gap: var(--space-large); -} - -/* ========================================================================== - Card variant — vertical stack - ========================================================================== */ - -.library-item--card { - display: grid; - grid-template-columns: 1fr auto; - column-gap: var(--space-large); - width: 100%; - border-radius: var(--border-radius-xl); - border: 1px solid var(--color-stroke-weak); - background: var(--color-surface-weak); - padding: var(--space-large); -} - -.library-item--card .library-item__header { - grid-column: 1; - grid-row: 1; - display: flex; - flex-direction: column; - gap: var(--space-large); -} - -.library-item--card .library-item__actions { - grid-column: 2; - grid-row: 1; - display: flex; - align-items: start; - justify-content: end; -} - -.library-item--card .library-item__cpp-version--desktop { - display: none; -} - -.library-item--card .library-item__cpp-version { - display: inline-flex; -} - -.library-item--card .library-item__tags { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-s); -} - -.library-item--card .library-item__contributor { - grid-column: 1 / -1; - grid-row: 2; - display: flex; - align-items: center; - gap: var(--space-default); - margin-top: auto; - padding-top: var(--space-large); -} - -/* ========================================================================== - Tablet (≤ 1024px) - ========================================================================== */ +/* ---------- Tablet (≤ 1024px) ---------- */ @media (max-width: 1024px) { .library-item-list { - grid-template-columns: 120px 2fr minmax(100px, 1fr) minmax(140px, 1fr) min-content auto; + grid-template-columns: auto 2fr 1fr 1fr min-content auto; /* Figma specifies --space-large here, but long descriptions cause overflow at tablet widths. Using --space-default until content constraints are defined. */ column-gap: var(--space-default); } + .library-item--list .user-profile { + align-items: normal; + } + .library-item--list .library-item__actions > .btn-icon-library { margin-left: var(--space-default); } } -/* ========================================================================== - Mobile (≤ 767px) — list items switch to card-like layout - ========================================================================== */ +/* ---------- Mobile (≤ 767px) — switches to card-like layout ---------- */ @media (max-width: 767px) { .library-item-list { @@ -217,6 +166,7 @@ a.library-item__name:hover { .library-item--list { display: grid; grid-template-columns: 1fr auto; + row-gap: var(--space-medium); border-bottom: 1px solid var(--color-stroke-weak); border-radius: 0; background: var(--color-surface-weak); @@ -242,20 +192,14 @@ a.library-item__name:hover { grid-row: 1; display: flex; flex-direction: column; - gap: var(--space-default); - - padding-bottom: 0; + gap: var(--space-card); } /* Actions — doc icon only, top-right */ .library-item--list .library-item__actions { grid-column: 2; grid-row: 1; - display: flex; - align-items: start; - justify-content: end; min-width: unset; - padding: 0; } /* Version tag swap — hide desktop, show mobile (inline with tags) */ @@ -271,38 +215,86 @@ a.library-item__name:hover { .library-item--list .library-item__tags { grid-column: 1 / -1; grid-row: 2; - display: flex; - flex-wrap: wrap; - align-items: center; - gap: var(--space-s); - } /* Contributor — full width, bottom */ .library-item--list .library-item__contributor { grid-column: 1 / -1; grid-row: 3; - display: flex; - align-items: center; - gap: var(--space-default); - border-top: none; - margin-top: auto; } +} - /* ---------- Card variant — mobile overrides ---------- */ +/* ========================================================================== + Card variant + ========================================================================== */ - .library-item--card { - max-width: none; - padding: var(--space-large); - gap: var(--space-large); +/* ---------- Desktop ---------- */ + +.library-item--card { + display: grid; + grid-template-columns: 1fr auto; + column-gap: var(--space-large); + width: 100%; + border-radius: var(--border-radius-xl); + border: 1px solid var(--color-stroke-mid); + background: var(--color-surface-weak); + padding: var(--space-large); +} + +.library-item--card .library-item__header { + grid-column: 1; + grid-row: 1; + display: flex; + flex-direction: column; + gap: var(--space-large); +} + +.library-item--card .library-item__actions { + grid-column: 2; + grid-row: 1; + display: flex; + align-items: start; + justify-content: end; +} + +.library-item--card .library-item__cpp-version--desktop { + display: none; +} + +.library-item--card .library-item__cpp-version { + display: inline-flex; +} + +.library-item--card .library-item__tags { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--space-s); +} + +.library-item--card .library-item__contributor { + grid-column: 1 / -1; + grid-row: 2; + display: flex; + align-items: center; + gap: var(--space-default); + margin-top: auto; + padding-top: var(--space-large); +} + +/* ---------- Mobile (≤ 767px) ---------- */ + +@media (max-width: 767px) { + .library-item-card-grid { + gap: var(--space-medium); } - .library-item--card .library-item__header, - .library-item--card .library-item__actions, - .library-item--card .library-item__tags, - .library-item--card .library-item__contributor { - padding: 0; + .library-item--card .library-item__header { + gap: var(--space-card); } + .library-item--card .library-item__contributor { + padding-top: var(--space-card); + } } From a4137780cddd7995a25029c23039a120195c2e4f Mon Sep 17 00:00:00 2001 From: julioest Date: Wed, 8 Apr 2026 00:16:25 -0400 Subject: [PATCH 12/14] refactor: Move avatar logic to User model Move _get_author_avatar from demo view to User.get_avatar_url() with walrus operator. --- core/views.py | 19 +------------------ users/models.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/core/views.py b/core/views.py index 6d1f932b1..03c841091 100644 --- a/core/views.py +++ b/core/views.py @@ -1245,23 +1245,6 @@ class V3ComponentDemoView(TemplateView): template_name = "base.html" - @staticmethod - def _get_author_avatar(author): - """Return the best available avatar URL for a library author (User). - - Returns empty string when no image is available so the avatar - template falls back to a colored initials circle. - """ - if not author: - return "" - url = author.get_thumbnail_url() - if url: - return url - ca = getattr(author, "commitauthor", None) - if ca and getattr(ca, "avatar_url", None): - return ca.avatar_url - return "" - def get_context_data(self, **kwargs): from django.urls import reverse from libraries.models import Library, LibraryVersion @@ -2071,7 +2054,7 @@ def get_context_data(self, **kwargs): "author": { "name": author.display_name if author else "Unknown", "role": "Contributor", - "avatar_url": self._get_author_avatar(author), + "avatar_url": author.get_avatar_url() if author else "", "badge_url": f"{badge_img}/badge-first-place.png", }, "doc_url": reverse( diff --git a/users/models.py b/users/models.py index c7491044d..44cc66eaf 100644 --- a/users/models.py +++ b/users/models.py @@ -289,6 +289,22 @@ def get_thumbnail_url(self): with suppress(AttributeError, MissingSource): return getattr(self.image_thumbnail, "url", None) + def get_avatar_url(self): + """Return the best available avatar URL. + + Tries the profile image thumbnail first, then falls back to + the linked CommitAuthor avatar. Returns empty string when no + image is available so the avatar template falls back to a + colored initials circle. + """ + if url := self.get_thumbnail_url(): + return url + if (ca := getattr(self, "commitauthor", None)) and getattr( + ca, "avatar_url", None + ): + return ca.avatar_url + return "" + def get_hq_image_url(self): # convenience method for templates if self.hq_image and self.hq_image_render: From 7b530e6d3f3e951b1c1954cacc8533ecfbdf90f8 Mon Sep 17 00:00:00 2001 From: julioest Date: Tue, 14 Apr 2026 02:47:31 -0400 Subject: [PATCH 13/14] fix: Add space between C++ and version number --- templates/v3/includes/_library_item.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/v3/includes/_library_item.html b/templates/v3/includes/_library_item.html index 25118327b..a5a9371cc 100644 --- a/templates/v3/includes/_library_item.html +++ b/templates/v3/includes/_library_item.html @@ -31,7 +31,7 @@ {% for cat in categories %} {% include "v3/includes/_category_tag.html" with tag_label=cat.label url=cat.url variant=cat.variant %} {% endfor %} - {{ cpp_version }} + C++ {{ cpp_version|cut:"C++" }}
    @@ -40,7 +40,7 @@
    - {{ cpp_version }} + C++ {{ cpp_version|cut:"C++" }} {% include "v3/includes/_button.html" with url=doc_url label="" icon_name="documentation" icon_size=32 icon_viewbox="0 0 16 16" style="icon-library" aria_label="View documentation for "|add:library_name %}
    From 3969842d3b0ac930d0fdac68dd745935f57a63a8 Mon Sep 17 00:00:00 2001 From: julioest Date: Tue, 14 Apr 2026 02:57:00 -0400 Subject: [PATCH 14/14] fix: Update Library Item card grid columns --- static/css/v3/library-item.css | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/static/css/v3/library-item.css b/static/css/v3/library-item.css index f87d8d715..08b3dcdcd 100644 --- a/static/css/v3/library-item.css +++ b/static/css/v3/library-item.css @@ -24,13 +24,13 @@ .library-item-card-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + grid-template-columns: repeat(4, 1fr); gap: var(--space-large); } @media (max-width: 1024px) { .library-item-card-grid { - grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + grid-template-columns: repeat(3, 1fr); } } @@ -287,6 +287,7 @@ a.library-item__name:hover { @media (max-width: 767px) { .library-item-card-grid { + grid-template-columns: 1fr; gap: var(--space-medium); }