Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
7e1bd7c
feat: implement libraries page filtering
julhoang May 7, 2026
5f3d33c
feat: implement token search
julhoang May 7, 2026
3393479
feat: add filter clearing feature for library page
julhoang May 7, 2026
78344d8
style: improve styling of the library filter dropdown
julhoang May 8, 2026
e846dd6
chore: remove unsupported sort by options
julhoang May 8, 2026
b6fdf86
feat: add sort by popularity (using grading order)
julhoang May 11, 2026
997b392
feat: update Clear All strategy to not reset the view type selection
julhoang May 11, 2026
5bf6b03
feat: add empty state designs
julhoang May 11, 2026
4f529d2
feat: hide unsupported filter and search components when JS is disabled
julhoang May 11, 2026
821ce61
feat: add x-cloak to avoid content flashes
julhoang May 11, 2026
9224fa8
feat: add max_cpp_version and update min-max range filtering strategy
julhoang May 12, 2026
f276d60
feat: only show Clear All button when there is an active filter
julhoang May 12, 2026
4cd880e
feat: add fuse.js for fuzzy search
julhoang May 12, 2026
9548e6a
style: minor styling improvements to dropdown and version column
julhoang May 12, 2026
ae485b0
chore: update broken documentation link and add C++23 to the acceptab…
julhoang May 12, 2026
6a5802b
fix: add missing cxxstd_max field to parse_libraries_json, build cpp …
julhoang May 12, 2026
1e89a7d
feat: add min-max filtering relational logic
julhoang May 12, 2026
a108569
fix: fix no-JS styling
julhoang May 12, 2026
d4cfe53
feat: update filtering logic to overlap range
julhoang May 12, 2026
fa4fdf6
chore: code clean up, add some documentation
julhoang May 12, 2026
0aac828
fix: add missing field in mock test
julhoang May 12, 2026
7b223bd
fix: prevent users from being able to select disabled dropdown items
julhoang May 13, 2026
636db41
fix: update the URL path of the category tags
julhoang May 19, 2026
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
1 change: 1 addition & 0 deletions core/githubhelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -623,6 +623,7 @@ def parse_libraries_json(self, libraries_json: dict) -> dict:
"category": libraries_json.get("category", []),
"maintainers": libraries_json.get("maintainers", []),
"cxxstd": libraries_json.get("cxxstd"),
"cxxstd_max": libraries_json.get("cxxstd_max"),
"cpp20_module_support": libraries_json.get("modules", False),
}

Expand Down
2 changes: 2 additions & 0 deletions frontend/fuse-entry.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
import Fuse from 'fuse.js';
window.Fuse = Fuse;
18 changes: 18 additions & 0 deletions libraries/migrations/0040_libraryversion_cpp_standard_maximum.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Generated by Django 6.0.2 on 2026-05-11 23:58

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
("libraries", "0039_flag_known_bot_commit_authors"),
]

operations = [
migrations.AddField(
model_name="libraryversion",
name="cpp_standard_maximum",
field=models.CharField(blank=True, max_length=50, null=True),
),
]
35 changes: 22 additions & 13 deletions libraries/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,6 +477,17 @@ def category_tags(self):


class LibraryVersion(models.Model):
# Source: https://docs.cppalliance.org/contributor-guide/requirements/library-metadata.html
CPP_STANDARD_DISPLAY_NAMES = {
"98": "C++98",
"03": "C++03",
"11": "C++11",
"14": "C++14",
"17": "C++17",
"20": "C++20",
"23": "C++23",
}

version = models.ForeignKey(
"versions.Version",
related_name="library_version",
Expand Down Expand Up @@ -512,6 +523,7 @@ class LibraryVersion(models.Model):
deletions = models.IntegerField(default=0)
files_changed = models.IntegerField(default=0)
cpp_standard_minimum = models.CharField(max_length=50, blank=True, null=True)
cpp_standard_maximum = models.CharField(max_length=50, blank=True, null=True)
cpp20_module_support = models.BooleanField(default=False)
dependencies = models.ManyToManyField(
"libraries.Library",
Expand Down Expand Up @@ -554,19 +566,16 @@ def author_details(self):
}

def get_cpp_standard_minimum_display(self):
"""Returns the display name for the C++ standard, or the value if not found.

Source of values is
https://docs.cppalliance.org/user-guide/prev/library_metadata.html"""
display_names = {
"98": "C++98",
"03": "C++03",
"11": "C++11",
"14": "C++14",
"17": "C++17",
"20": "C++20",
}
return display_names.get(self.cpp_standard_minimum, self.cpp_standard_minimum)
"""Returns the display name for the minimum C++ standard, or the value if not found."""
return self.CPP_STANDARD_DISPLAY_NAMES.get(
self.cpp_standard_minimum, self.cpp_standard_minimum
)

def get_cpp_standard_maximum_display(self):
"""Returns the display name for the maximum C++ standard, or the value if not found."""
return self.CPP_STANDARD_DISPLAY_NAMES.get(
self.cpp_standard_maximum, self.cpp_standard_maximum
)


class Issue(models.Model):
Expand Down
1 change: 1 addition & 0 deletions libraries/tests/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ def test_get_library_list(library_updater):
"github_url": "example.com",
"description": "Test description",
"cxxstd": "11",
"cxxstd_max": None,
"category": ["Test"],
"authors": ["John Doe"],
"maintainers": ["Jane Doe"],
Expand Down
10 changes: 10 additions & 0 deletions libraries/tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ def test_get_cpp_standard_minimum_display(library_version):
assert library_version.get_cpp_standard_minimum_display() == "42"


def test_get_cpp_standard_maximum_display(library_version):
library_version.cpp_standard_maximum = "17"
library_version.save()
assert library_version.get_cpp_standard_maximum_display() == "C++17"

library_version.cpp_standard_maximum = "42"
library_version.save()
assert library_version.get_cpp_standard_maximum_display() == "42"


def test_github_properties(library):
properties = library.github_properties()
assert properties["owner"] == "boostorg"
Expand Down
133 changes: 93 additions & 40 deletions libraries/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,29 @@ class LibraryListBase(BoostVersionMixin, V3Mixin, VersionAlertMixin, ListView):
template_name = "libraries/grid_list.html"
v3_template_name = "v3/library_page.html"

def get_v3_context_data(self, **kwargs):
def get_v3_context_data(self, queryset=None, **kwargs):
context = {}
cpp_options = [
("all", "All"),
("cpp03", "C++03"),
("cpp11", "C++11"),
("cpp14", "C++14"),
("cpp17", "C++17"),
("cpp20", "C++20"),
("cpp23", "C++23"),
view_str = self.kwargs.get("library_view_str")

cpp_options = [("all", "All")] + list(
LibraryVersion.CPP_STANDARD_DISPLAY_NAMES.items()
)

tiers_present = sorted(
{lv.library.tier for lv in (queryset or []) if lv.library.tier is not None}
)

grading_options = [("all", "All")] + [
(Tier(t).label.lower(), Tier(t).label) for t in tiers_present
]

category_options = [
(c.slug, c.name) for c in self.get_categories(self._selected_version)
]

request_get = self.request.GET
selected_categories = request_get.getlist("category")

context["library_filter_fields"] = [
{
"type": "dropdown",
Expand All @@ -146,83 +158,124 @@ def get_v3_context_data(self, **kwargs):
("categorized", "Category"),
("grading", "Grading"),
],
"selected": self.kwargs.get("library_view_str"),
"selected": view_str,
"default": "list",
"width": "category",
"deselectable": False,
"exclude_from_clear": True,
},
{
"type": "dropdown",
"name": "grading",
"label": "Grading",
"options": [
("all", "All"),
("flagship", "Flagship"),
("core", "Core"),
("deprecated", "Deprecated"),
("legacy", "Legacy"),
],
"selected": "all",
"options": grading_options,
"selected": request_get.get("grading", "all"),
"default": "all",
"width": "wide",
"deselectable": True,
},
{
"type": "dropdown",
"name": "min_cpp",
"label": "Min. C++ Version",
"options": cpp_options,
"selected": "all",
"selected": request_get.get("min_cpp", "all"),
"default": "all",
"width": "narrow",
"deselectable": True,
},
{
"type": "dropdown",
"name": "max_cpp",
"label": "Max. C++ Version",
"options": cpp_options,
"selected": "all",
"selected": request_get.get("max_cpp", "all"),
"default": "all",
"width": "narrow",
"deselectable": True,
},
{
"type": "combo_multi",
"name": "category",
"label": "Category",
"options": [
("algorithms", "Algorithms"),
("asynchronous", "Asynchronous"),
("awaitables", "Awaitables"),
("containers", "Containers"),
("coroutines", "Coroutines"),
("correctness", "Correctness"),
# More dummy data to show scrollbar
("data_processing", "Data processing"),
("debugging", "Debugging"),
("file_systems", "File systems"),
("formatting", "Formatting"),
("graphics", "Graphics"),
],
"options": category_options,
"selected_values": selected_categories,
"width": "wide",
"placeholder": "Search",
"deselectable": True,
},
# Sort is applied client-side via libraryFilter.sortItems(); no
# queryset.order_by() here. The default alphabetical order comes
# from the view's `ordering = "library__name"`.
{
"type": "dropdown",
"name": "sort",
"label": "Sort by",
"options": [
("alphabetical", "Alphabetical"),
("popular", "Most Popular"),
("updated", "Recently Updated"),
("release", "Release Date"),
("popularity", "Most Popular"),
],
"selected": "alphabetical",
"selected": request_get.get("sort", "alphabetical"),
"default": "alphabetical",
"deselectable": True,
},
]
context["library_view_str"] = self.kwargs.get("library_view_str")
context["library_view_str"] = view_str
context["library_filter_defaults"] = {
f["name"]: f["default"]
for f in context["library_filter_fields"]
if "default" in f
}
context["library_filter_clear_url"] = reverse(
"libraries-list",
kwargs={
"version_slug": self.kwargs.get("version_slug"),
"library_view_str": "list",
},
)

# Compact JSON payload for client-side filtering on list/grid views.
context["library_dataset"] = [
{
"slug": lv.library.slug,
"name": lv.library.name,
"description": lv.description or lv.library.description or "",
"category_slugs": [c.slug for c in lv.library.categories.all()],
"category_names": [c.name for c in lv.library.categories.all()],
"author_names": [
a.display_name for a in lv.authors.all() if a.display_name
],
"cpp_min": lv.get_cpp_standard_minimum_display() or "",
"cpp_max": lv.get_cpp_standard_maximum_display() or "",
"tier": (
Tier(lv.library.tier).label.lower()
if lv.library.tier is not None
else ""
),
}
for lv in (queryset or [])
]
context["library_search_query"] = self.request.GET.get("q", "")
return context

def render_v3_response(self):
"""Render the v3 template through Django's standard TemplateView pipeline."""
queryset = self.get_queryset()
# Resolve selected_version once so get_v3_context_data can reuse it.
self._selected_version = self._resolve_selected_version()
context = self.get_context_data(
**self.get_v3_context_data(), object_list=self.get_queryset()
**self.get_v3_context_data(queryset=queryset), object_list=queryset
)
return self.render_to_response(context)

def _resolve_selected_version(self):
version_slug = determine_selected_boost_version(
self.kwargs.get("version_slug"), self.request
)
if version_slug == LATEST_RELEASE_URL_PATH_STR:
return Version.objects.most_recent()
return Version.objects.filter(slug=version_slug).first()

def get_queryset(self):
queryset = super().get_queryset()
version_slug = determine_selected_boost_version(
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"build": "NODE_ENV=production tailwindcss -i frontend/styles.css -o static/css/styles.css --minify",
"builddocs": "NODE_ENV=production tailwindcss -c ./docstailwind.config.js -i frontend/docsstyles.css -o static/css/docsstyles.css --minify",
"builduserguide": "NODE_ENV=production tailwindcss -c ./userguidetailwind.config.js -i frontend/userguidestyles.css -o static/css/userguidestyles.css --minify",
"build:wysiwyg": "esbuild frontend/wysiwyg-editor.js --bundle --minify --format=esm --outfile=static/js/v3/wysiwyg-editor.js --external:mermaid"
"build:wysiwyg": "esbuild frontend/wysiwyg-editor.js --bundle --minify --format=esm --outfile=static/js/v3/wysiwyg-editor.js --external:mermaid",
"build:fuse": "esbuild frontend/fuse-entry.js --bundle --minify --format=iife --outfile=static/js/v3/fuse.min.js"
},
"dependencies": {
"@svgdotjs/svg.js": "^3.2.4",
Expand All @@ -37,6 +38,7 @@
"htmx": "^0.0.2",
"lowlight": "^3.0.0",
"dompurify": "^3.2.2",
"fuse.js": "7.3.0",
"marked": "^17.0.0",
"tailwindcss": "3.2.1",
"turndown": "^7.2.0",
Expand Down
43 changes: 41 additions & 2 deletions static/css/v3/forms.css
Original file line number Diff line number Diff line change
Expand Up @@ -311,10 +311,15 @@
.dropdown--combo.dropdown--open .dropdown__trigger,
.dropdown__trigger--active {
background-color: var(--color-surface-mid, #f7f7f8);
border-color: var(--color-stroke-strong, #05081640);
border-color: var(--color-stroke-weak);
border-bottom-left-radius: 0;
border-bottom-right-radius: 0;
border-bottom-color: var(--color-stroke-weak, #0508161A);
border-bottom-color: var(--color-stroke-weak);
}

.dropdown--has-value .dropdown__trigger,
.dropdown--has-value .dropdown__trigger:hover {
border-color: var(--color-stroke-link-accent);
}

.dropdown__panel {
Expand Down Expand Up @@ -388,6 +393,17 @@
text-decoration: underline;
}

.dropdown__item--disabled {
opacity: 0.4;
cursor: not-allowed;
}

.dropdown__item--disabled:hover,
.dropdown__item--disabled:focus {
background-color: transparent;
color: var(--color-text-secondary);
}

.field--error .dropdown__trigger {
background-color: var(--color-surface-error-weak, #fdf2f2);
border-color: var(--color-stroke-error, #d53f3f33);
Expand Down Expand Up @@ -430,6 +446,29 @@
outline: none;
}

.dropdown__clear-btn {
display: flex;
align-items: center;
justify-content: center;
background: none;
border: none;
padding: 0;
margin: 0;
cursor: pointer;
flex-shrink: 0;
}

.dropdown__clear-btn:focus-visible {
outline: none;
}

.dropdown__clear-icon {
width: 16px;
height: 16px;
flex-shrink: 0;
color: var(--color-icon-link-accent);
}

.combo__search-icon {
width: 16px;
height: 16px;
Expand Down
Loading
Loading