From e8ff6a8a619f3533a1d1ef0b9017e418411d28a2 Mon Sep 17 00:00:00 2001 From: Tarek Lokal Date: Sat, 11 Jul 2026 12:44:30 +0200 Subject: [PATCH] feat: filter appendix licenses by rule attributes (#637) Interactive filter panel matches licenses that satisfy every selected permission, condition, or limitation. Table rows carry data-rules tags. Co-authored-by: Cursor --- _includes/appendix-filter.html | 25 ++++++++++++++++ _includes/css/responsive.css | 45 +++++++++++++++++++++++++++++ appendix.md | 15 ++++++++-- assets/js/appendix-filter.js | 52 ++++++++++++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 2 deletions(-) create mode 100644 _includes/appendix-filter.html create mode 100644 assets/js/appendix-filter.js diff --git a/_includes/appendix-filter.html b/_includes/appendix-filter.html new file mode 100644 index 000000000..5f9755a4e --- /dev/null +++ b/_includes/appendix-filter.html @@ -0,0 +1,25 @@ +
+

Filter by rules — show only licenses that match every selected attribute:

+
+ {% assign types = "permissions|conditions|limitations" | split: "|" %} + {% assign seen_tags = '' %} + {% for type in types %} +
+ {{ type | capitalize }} + {% assign rules = site.data.rules[type] | sort: "label" %} + {% for rule_obj in rules %} + {% if seen_tags contains rule_obj.tag or rule_obj.tag contains '--' %} + {% continue %} + {% endif %} + {% capture seen_tags %}{{ seen_tags | append:rule_obj.tag }}{% endcapture %} + + {% endfor %} +
+ {% endfor %} +
+ +

+
diff --git a/_includes/css/responsive.css b/_includes/css/responsive.css index 64524f36b..a79da24b7 100644 --- a/_includes/css/responsive.css +++ b/_includes/css/responsive.css @@ -164,6 +164,51 @@ } } +.appendix-table-wrapper { + overflow-x: auto; + max-width: 100%; + margin-bottom: 2rem; +} + +.appendix-filter { + margin: 1.5rem 0; + padding: 1rem; + border: 1px solid #ddd; + border-radius: 6px; + background: color-mix(in srgb, var(--backgroundColor) 92%, #149ad4 8%); +} + +.appendix-filter-groups { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.appendix-filter-group { + border: none; + margin: 0; + padding: 0; + min-width: 12rem; +} + +.appendix-filter-chip { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin: 0.15rem 0.5rem 0.15rem 0; + font-size: 0.85rem; + font-weight: normal; +} + +.appendix-filter-clear { + margin-top: 0.75rem; +} + +.appendix-filter-status { + margin: 0.5rem 0 0; + font-size: 0.85rem; +} + /* Smartphones (portrait) ----------- */ @media only screen and (max-width : 321px) { .container { diff --git a/appendix.md b/appendix.md index 62c119d98..cf421dd33 100644 --- a/appendix.md +++ b/appendix.md @@ -9,8 +9,12 @@ For reference, here is a table of every license described in the [choosealicense If you're here to choose a license, **[start from the home page](/)** to see a few licenses that will work for most cases. - +{% include appendix-filter.html %} + +
+
{% assign types = "permissions|conditions|limitations" | split: "|" %} + {% assign seen_tags = '' %} @@ -25,9 +29,12 @@ If you're here to choose a license, **[start from the home page](/)** to see a f {% endfor %} {% endfor %} + + {% assign licenses = site.licenses | sort: "path" %} {% for license in licenses %} - + {% capture rule_tags %}{% for t in types %}{% for r in license[t] %}{{ r }},{% endfor %}{% endfor %}{% endcapture %} + {% assign seen_tags = '' %} {% for type in types %} {% assign rules = site.data.rules[type] | sort: "label" %} @@ -62,7 +69,9 @@ If you're here to choose a license, **[start from the home page](/)** to see a f {% endfor %} {% endfor %} +
License
{{ license.title }}
{{ license.title }}
+ ## Legend @@ -93,3 +102,5 @@ If you're here to choose a license, **[start from the home page](/)** to see a f {% endfor %} {% endfor %} + + diff --git a/assets/js/appendix-filter.js b/assets/js/appendix-filter.js new file mode 100644 index 000000000..9de502520 --- /dev/null +++ b/assets/js/appendix-filter.js @@ -0,0 +1,52 @@ +class AppendixFilter { + constructor(root) { + this.root = root; + this.table = document.querySelector('.appendix-table'); + this.rows = Array.from(this.table?.querySelectorAll('tbody tr') || []); + this.checkboxes = Array.from(root.querySelectorAll('input[type="checkbox"]')); + this.statusEl = document.querySelector('#appendix-filter-status'); + this.clearBtn = document.querySelector('#appendix-filter-clear'); + + this.checkboxes.forEach((cb) => cb.addEventListener('change', () => this.apply())); + this.clearBtn?.addEventListener('click', () => { + this.checkboxes.forEach((cb) => { cb.checked = false; }); + this.apply(); + }); + + this.apply(); + } + + selectedRules() { + return this.checkboxes.filter((cb) => cb.checked).map((cb) => cb.value); + } + + rowMatches(row, rules) { + if (!rules.length) return true; + const rowTags = (row.dataset.rules || '').split(',').filter(Boolean); + return rules.every((rule) => rowTags.includes(rule)); + } + + apply() { + const rules = this.selectedRules(); + let visible = 0; + + this.rows.forEach((row) => { + const match = this.rowMatches(row, rules); + row.hidden = !match; + if (match) visible += 1; + }); + + if (this.statusEl) { + if (!rules.length) { + this.statusEl.textContent = `Showing all ${this.rows.length} licenses.`; + } else { + this.statusEl.textContent = `Showing ${visible} of ${this.rows.length} licenses matching ${rules.length} selected rule(s).`; + } + } + } +} + +document.addEventListener('DOMContentLoaded', () => { + const root = document.querySelector('#appendix-filter'); + if (root) new AppendixFilter(root); +});