From 12af6287e7977697ac4731ab626a163de44f6dea Mon Sep 17 00:00:00 2001 From: ashmod Date: Wed, 31 Dec 2025 22:05:23 +0200 Subject: [PATCH 1/8] unspiced sections --- gcp/website/frontend3/package-lock.json | 9 +- gcp/website/frontend3/package.json | 3 +- gcp/website/frontend3/src/index.js | 2 +- .../frontend3/src/osv-tabs-accordion.js | 158 +++++++++ gcp/website/frontend3/src/styles.scss | 302 +++++++++++++----- gcp/website/frontend3/src/templates/list.html | 10 +- .../src/templates/vulnerability.html | 126 +++----- 7 files changed, 433 insertions(+), 177 deletions(-) create mode 100644 gcp/website/frontend3/src/osv-tabs-accordion.js diff --git a/gcp/website/frontend3/package-lock.json b/gcp/website/frontend3/package-lock.json index 7df00d64963..e2a4e865a13 100644 --- a/gcp/website/frontend3/package-lock.json +++ b/gcp/website/frontend3/package-lock.json @@ -14,8 +14,7 @@ "@material/layout-grid": "13.0.0", "@material/theme": "13.0.0", "@material/web": "^1.5.0", - "lit": "2.8.0", - "spicy-sections": "git+https://github.com/tabvengers/spicy-sections.git#c3aae99dbf1e627cdf03a35c913d7f6e970de22b" + "lit": "2.8.0" }, "devDependencies": { "copy-webpack-plugin": "10.2.4", @@ -5198,12 +5197,6 @@ "dev": true, "license": "MIT" }, - "node_modules/spicy-sections": { - "version": "0.9.0", - "resolved": "git+ssh://git@github.com/tabvengers/spicy-sections.git#c3aae99dbf1e627cdf03a35c913d7f6e970de22b", - "integrity": "sha512-YyjgiaF9vhgpUL1NlXXrRljXJLeWs473YxVGoKWI/yAmimdqxFNrZ0eYqLHfsDjK1h0bzuyAVUkQZLuIg0UoUA==", - "license": "W3C" - }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/gcp/website/frontend3/package.json b/gcp/website/frontend3/package.json index 7e1bbd19fcc..e6df04b701f 100644 --- a/gcp/website/frontend3/package.json +++ b/gcp/website/frontend3/package.json @@ -15,8 +15,7 @@ "@material/layout-grid": "13.0.0", "@material/theme": "13.0.0", "@material/web": "^1.5.0", - "lit": "2.8.0", - "spicy-sections": "git+https://github.com/tabvengers/spicy-sections.git#c3aae99dbf1e627cdf03a35c913d7f6e970de22b" + "lit": "2.8.0" }, "devDependencies": { "copy-webpack-plugin": "10.2.4", diff --git a/gcp/website/frontend3/src/index.js b/gcp/website/frontend3/src/index.js index 47a442816fc..29f1588b5f7 100644 --- a/gcp/website/frontend3/src/index.js +++ b/gcp/website/frontend3/src/index.js @@ -4,7 +4,7 @@ import '@material/web/icon/icon.js'; import '@material/web/iconbutton/icon-button.js'; import '@material/web/progress/circular-progress.js'; import '@hotwired/turbo'; -import 'spicy-sections/src/SpicySections'; +import './osv-tabs-accordion.js'; import { MdFilledTextField } from '@material/web/textfield/filled-text-field.js'; import { LitElement, html } from 'lit'; import { ExpandableSearch, SearchSuggestionsManager } from './search.js'; diff --git a/gcp/website/frontend3/src/osv-tabs-accordion.js b/gcp/website/frontend3/src/osv-tabs-accordion.js new file mode 100644 index 00000000000..7e285ecdc28 --- /dev/null +++ b/gcp/website/frontend3/src/osv-tabs-accordion.js @@ -0,0 +1,158 @@ +class OsvTabsAccordion extends HTMLElement { + constructor() { + super(); + this.breakpoint = 500; + this.mediaQuery = null; + this.headers = []; + this.panels = []; + this.activeIndex = 0; + } + + connectedCallback() { + this.breakpoint = parseInt(this.getAttribute("breakpoint")) || 500; + this.collectHeadersAndPanels(); + this.setupMediaQuery(); + this.updateAffordance(); + this.setupEventListeners(); + } + + disconnectedCallback() { + if (this.mediaQuery) { + this.mediaQuery.removeEventListener("change", this.boundUpdateAffordance); + } + } + + collectHeadersAndPanels() { + this.headers = []; + this.panels = []; + + const children = Array.from(this.children); + for (let i = 0; i < children.length; i++) { + const child = children[i]; + if (child.matches("h2, h3")) { + const nextSibling = children[i + 1]; + if (nextSibling && nextSibling.matches("div")) { + this.headers.push(child); + this.panels.push(nextSibling); + } + } + } + } + + setupMediaQuery() { + this.mediaQuery = window.matchMedia( + `(min-width: ${this.breakpoint + 1}px)` + ); + this.boundUpdateAffordance = () => this.updateAffordance(); + this.mediaQuery.addEventListener("change", this.boundUpdateAffordance); + } + + setupEventListeners() { + this.headers.forEach((header, index) => { + header.addEventListener("click", (e) => this.handleHeaderClick(index, e)); + header.addEventListener("keydown", (e) => this.handleKeydown(index, e)); + }); + } + + updateAffordance() { + const isDesktop = this.mediaQuery.matches; + const affordance = isDesktop ? "tab-bar" : "collapse"; + this.setAttribute("affordance", affordance); + + if (isDesktop) { + this.renderTabs(); + } else { + this.renderAccordion(); + } + } + + renderTabs() { + this.headers.forEach((header, index) => { + const isActive = index === this.activeIndex; + header.setAttribute("tabindex", isActive ? "0" : "-1"); + header.setAttribute("role", "tab"); + header.setAttribute("aria-selected", isActive ? "true" : "false"); + header.removeAttribute("aria-expanded"); + header.removeAttribute("expanded"); + }); + + this.panels.forEach((panel, index) => { + const isActive = index === this.activeIndex; + panel.setAttribute("role", "tabpanel"); + panel.style.display = isActive ? "" : "none"; + panel.removeAttribute("aria-hidden"); + }); + } + + renderAccordion() { + // By default, expand all panels in accordion mode + this.headers.forEach((header, index) => { + const panel = this.panels[index]; + panel.style.display = ""; + + header.setAttribute("role", "button"); + header.setAttribute("tabindex", "0"); + header.setAttribute("aria-expanded", "true"); + header.setAttribute("expanded", ""); + header.removeAttribute("aria-selected"); + }); + + this.panels.forEach((panel) => { + panel.setAttribute("role", "region"); + panel.removeAttribute("aria-hidden"); + }); + } + + handleHeaderClick(index, event) { + if (!this.headers[index].contains(event.target)) { + return; + } + + const affordance = this.getAttribute("affordance"); + + if (affordance === "tab-bar") { + this.activeIndex = index; + this.renderTabs(); + } else { + const panel = this.panels[index]; + const header = this.headers[index]; + const isExpanded = panel.style.display !== "none"; + + panel.style.display = isExpanded ? "none" : ""; + header.setAttribute("aria-expanded", isExpanded ? "false" : "true"); + if (isExpanded) { + header.removeAttribute("expanded"); + } else { + header.setAttribute("expanded", ""); + } + } + } + + handleKeydown(index, event) { + const affordance = this.getAttribute("affordance"); + + if (event.key === "Enter" || event.key === " ") { + event.preventDefault(); + this.handleHeaderClick(index, { target: this.headers[index] }); + } + + if (affordance === "tab-bar") { + if (event.key === "ArrowRight" || event.key === "ArrowDown") { + event.preventDefault(); + const nextIndex = (index + 1) % this.headers.length; + this.activeIndex = nextIndex; + this.renderTabs(); + this.headers[nextIndex].focus(); + } else if (event.key === "ArrowLeft" || event.key === "ArrowUp") { + event.preventDefault(); + const prevIndex = + (index - 1 + this.headers.length) % this.headers.length; + this.activeIndex = prevIndex; + this.renderTabs(); + this.headers[prevIndex].focus(); + } + } + } +} + +customElements.define("osv-tabs-accordion", OsvTabsAccordion); diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index da501ef6109..2446c04f5ff 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -385,70 +385,119 @@ pre { .ecosystem-buttons { margin-top: 20px; - display: flex; - flex-wrap: wrap; - gap: 10px; - align-items: center; - - // We use to collapse the ecosystem list only on mobile. - // On desktop, display the full list. - --const-mq-affordances: [screen and (max-width: #{$osv-mobile-breakpoint})] collapse; - &::part(tab-bar) { - // The tab bar affordance isn't used, so hide it. - display: none; + > summary { + list-style: none; + &::-webkit-details-marker { + display: none; + } + &::marker { + display: none; + content: ''; + } } - &::part(content-panels) { - // Because the shadow root of actually wraps our content - // we need to use display: contents; in order for flex to apply as if the - // child elements are direct descendants of the parent flex container. - display: contents; - } + @media (min-width: #{$osv-mobile-breakpoint + 1}) { + display: flex; + flex-direction: row; + flex-wrap: wrap; + gap: 10px; + align-items: center; + + > .ecosystem-collapse-header { + display: inline-flex; + align-items: center; + } - .spicy-content { - // Expanded contents also should act as if they were direct descendants - // of our flex container. - display: contents; + > .ecosystem-content { + display: inline-flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; + } + + .ecosystem-label-all { + position: relative; + margin-right: 20px; + + &::after { + content: ''; + position: absolute; + right: -16px; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 30px; + background-color: $osv-grey-600; + } + } + + .ecosystems-divider { + display: none; + } } - // Customizing the collapsing header for . We can select it - // by looking for the element with affordance="collapse". - [affordance=collapse] { - display: flex; - align-items: center; + @media (max-width: #{$osv-mobile-breakpoint}) { + display: block; - // The ::before on the collapsible is used to display the chevron. - &::before { - display: block; - padding: 0 8px; - // HACK: Invert the black chevron to white. The chevron is set using - // background-image, so we can't use the CSS fill property on it. - filter: invert(100%); + &:not([open]) > .ecosystem-content { + display: none; + } + + &[open] > .ecosystem-content { + display: flex; + flex-wrap: wrap; + gap: 10px; + width: 100%; + margin-top: 10px; + } + + > .ecosystem-collapse-header { + display: flex; + align-items: center; + cursor: pointer; + width: 100%; + + &::before { + content: ''; + display: block; + width: 12px; + height: 12px; + margin-right: 8px; + background: url(/static/img/filled-triangle.svg); + background-position: center; + background-repeat: no-repeat; + filter: invert(100%); + transform: rotate(0deg); + transition: transform 0.2s ease-in-out; + } + } + + &[open] > .ecosystem-collapse-header::before { + transform: rotate(90deg); + } + + .ecosystems-divider { + display: none; } } .ecosystem-label { - display: flex; + display: inline-flex; gap: 16px; font-family: $osv-heading-font-family; padding: 10px 20px; background: #696969; border-radius: 999px; height: 38px; + cursor: pointer; + align-items: center; .ecosystem-count { font-weight: bold; } } - .ecosystems-divider { - display: block; - margin: 0 10px; - height: 30px; - border-right: 1px solid $osv-grey-600; - } - input[type=radio]:checked+.ecosystem-label { background: $osv-text-color; color: $osv-accent-color; @@ -878,12 +927,54 @@ dl.vulnerability-details, margin-bottom: 16px; font-weight: bold; } +} + +osv-tabs-accordion.vulnerability-packages[affordance="tab-bar"] { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(150px, auto)); + grid-template-rows: auto 1fr; + + > h2.package-header { + grid-row: 1; + } + + > div { + grid-row: 2; + grid-column: 1 / -1; + } +} + +osv-tabs-accordion.vulnerability-packages[affordance="collapse"] { + display: block; + + > h2.package-header { + display: block; + width: 100%; + cursor: pointer; - // Tab bar styling. - --const-mq-affordances: [screen and (max-width: #{$osv-mobile-breakpoint})] collapse | [screen and (min-width: #{$osv-mobile-breakpoint + 1})] tab-bar; + &::before { + content: ''; + display: inline-block; + width: 12px; + height: 12px; + margin-right: 8px; + -webkit-mask-image: url(/static/img/filled-triangle.svg); + mask-image: url(/static/img/filled-triangle.svg); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + background-color: $osv-grey-800; + transform: rotate(0deg); + transition: transform 0.2s ease-in-out, background-color 0.2s ease-in-out; + } - .force-collapse { - --const-mq-affordances: [screen] collapse; + &[expanded]::before { + transform: rotate(90deg); + background-color: $osv-accent-color; + } } } @@ -892,6 +983,7 @@ dl.vulnerability-details, background: #aaa; color: #000; display: inline-block; + font-family: $osv-heading-font-family; font-size: 14px; padding: 16px; } @@ -911,28 +1003,43 @@ dl.vulnerability-details, } .versions-section { - --const-mq-affordances: [screen] collapse; + > summary { + list-style: none; + &::-webkit-details-marker { + display: none; + } + &::marker { + display: none; + content: ''; + } + } - h2.version-header::before { + summary.version-header::before { content: ''; + display: inline-block; + width: 12px; + height: 12px; margin-right: 16px; background: url(/static/img/filled-triangle.svg); background-position: center; + background-repeat: no-repeat; transform: rotate(0deg); + transition: transform 0.2s ease-in-out; // Make the filled triangle white. filter: invert(100%); } - h2.version-header[expanded]::before { + &[open] > summary.version-header::before { transform: rotate(90deg); } - h2.version-header { + summary.version-header { background: none; font-family: $osv-heading-font-family; color: #fff; padding-left: 0px; font-size: 16px; + cursor: pointer; } } @@ -968,9 +1075,18 @@ dl.vulnerability-details, } .database-specific-section { - --const-mq-affordances: [screen] collapse; + > summary { + list-style: none; + &::-webkit-details-marker { + display: none; + } + &::marker { + display: none; + content: ''; + } + } - h2.database-specific-header { + summary.database-specific-header { background: none; font-family: $osv-heading-font-family; color: #fff; @@ -978,6 +1094,7 @@ dl.vulnerability-details, font-size: 16px; display: flex; align-items: center; + cursor: pointer; &::before { content: ''; @@ -988,12 +1105,13 @@ dl.vulnerability-details, background-position: center; background-repeat: no-repeat; transform: rotate(0deg); + transition: transform 0.2s ease-in-out; filter: invert(100%); } + } - &[expanded]::before { - transform: rotate(90deg); - } + &[open] > summary.database-specific-header::before { + transform: rotate(90deg); } } @@ -1001,25 +1119,35 @@ dl.vulnerability-details, padding: 8px 0 0 24px; } - .spicy-sections-workaround { - // https://github.com/tabvengers/spicy-sections/issues/64. - pointer-events: none; - } &.force-collapse { - h2.package-header { + .ecosystem-accordion > summary { + list-style: none; + &::-webkit-details-marker { + display: none; + } + &::marker { + display: none; + content: ''; + } + } + + summary.package-header { width: 100%; background: #393939; color: #fff; padding: 16px; margin-bottom: 2px; border-radius: 0; + font-family: $osv-heading-font-family; font-weight: normal; transition: background-color 0.2s ease-in-out; cursor: pointer; + display: block; &::before { content: ''; + display: inline-block; width: 12px; height: 12px; margin-right: 8px; @@ -1031,26 +1159,23 @@ dl.vulnerability-details, filter: invert(100%); } - &[expanded]::before { - transform: rotate(90deg); - } - &:hover { background: #4F4F4F; } + } - &[expanded] { - background: #fff; - color: $osv-accent-color; - font-weight: bold; - - &::before { - filter: invert(24%) sepia(89%) saturate(2293%) hue-rotate(345deg) brightness(81%) contrast(107%); - } - - &:hover { - background: #f0f0f0; - } + .ecosystem-accordion[open] > summary.package-header { + background: #fff; + color: $osv-accent-color; + font-weight: bold; + + &::before { + transform: rotate(90deg); + filter: invert(24%) sepia(89%) saturate(2293%) hue-rotate(345deg) brightness(81%) contrast(107%); + } + + &:hover { + background: #f0f0f0; } } @@ -1096,6 +1221,17 @@ dl.vulnerability-details, position: relative; margin-bottom: 8px; + > summary { + list-style: none; + &::-webkit-details-marker { + display: none; + } + &::marker { + display: none; + content: ''; + } + } + // The horizontal "connector" line &::before { content: ''; @@ -1113,7 +1249,7 @@ dl.vulnerability-details, padding-bottom: $last-ecosystem-border-gap; } - .package-accordion h3.package-name-title { + .package-accordion summary.package-name-title { font-family: $osv-heading-font-family; font-size: 1.1rem; color: #f1f1f1; @@ -1122,6 +1258,7 @@ dl.vulnerability-details, background: #333333; border: 1px solid #444; border-radius: 0; + display: block; &::before { content: ''; @@ -1137,13 +1274,13 @@ dl.vulnerability-details, vertical-align: middle; transform: rotate(0deg); } + } - &[expanded] { - border-bottom: 1px dashed #fff; - - &::before { - transform: rotate(90deg); - } + .package-accordion[open] > summary.package-name-title { + border-bottom: 1px dashed #fff; + + &::before { + transform: rotate(90deg); } } @@ -1213,7 +1350,6 @@ dl.vulnerability-details, &[tabindex="0"] { background: #fff; color: $osv-accent-color; - // Override spicy-sections default blue bottom border. border-bottom: 1px solid $osv-accent-color; } diff --git a/gcp/website/frontend3/src/templates/list.html b/gcp/website/frontend3/src/templates/list.html index 7218905be6d..ffb4137df60 100644 --- a/gcp/website/frontend3/src/templates/list.html +++ b/gcp/website/frontend3/src/templates/list.html @@ -43,16 +43,16 @@

Vulnerabilities

{% if ecosystem_counts %} - - +
+ - -
+
+
{% for ecosystem in ecosystem_counts %} Vulnerabilities {% endfor %}
- +
{% endif %}
diff --git a/gcp/website/frontend3/src/templates/vulnerability.html b/gcp/website/frontend3/src/templates/vulnerability.html index 3b7a1d58b48..f1c7ea29a92 100644 --- a/gcp/website/frontend3/src/templates/vulnerability.html +++ b/gcp/website/frontend3/src/templates/vulnerability.html @@ -177,16 +177,17 @@

Affected packages

{% if vulnerability.affected|should_collapse %} {% set ecosystems = vulnerability.affected | group_by_ecosystem %} - +
{% for ecosystem_name, packages in ecosystems.items() -%} {% set is_last_ecosystem = loop.last %} -

- {{ ecosystem_name }} -

-
- {% for affected in packages -%} - -

+
+ + {{ ecosystem_name }} + +
+ {% for affected in packages -%} +
+ {% if 'package' in affected %} {{ affected.package.name }} {% else %} @@ -195,7 +196,7 @@

{{ affected_repo | strip_scheme }} {% endif %} {% endif %} -

+
{%- if 'package' in affected -%} @@ -285,14 +286,14 @@

Affected ranges Affected versions

{% for group, versions in (affected.versions|group_versions(ecosystem_name)).items() -%} - -

{{ group }}

+
+ {{ group }}
{% for version in versions -%}
{{ version }}
{% endfor -%}
- +
{% endfor -%}
@@ -311,12 +312,12 @@

Database specific {% for key, value in db_specific.items() %} - -

{{ key }}

+
+ {{ key }}
{{ value | display_json }}
- +
{% endfor %}

{% else %} @@ -327,15 +328,16 @@

{{ key }}

{% endif -%}

- + {% endfor -%} -
+ + {% endfor -%} -
+ {% else %} - + {% for affected in vulnerability.affected -%} {% if 'package' in affected %} {% set ecosystem = affected.package.ecosystem %} @@ -348,9 +350,9 @@

{{ key }}

{% endif %} {% endif %}

- {{ ecosystem }} - / - {{ package }} + {{ ecosystem }} + / + {{ package }}

{%- if 'package' in affected -%} @@ -469,14 +471,14 @@

{% for group, versions in (affected.versions|group_versions(ecosystem)).items() -%} - -

{{ group }}

+
+ {{ group }}
{% for version in versions -%}
{{ version }}
{% endfor -%}
- +
{% endfor -%}
@@ -505,12 +507,12 @@

{% if db_specific is mapping %}
{% for key, value in db_specific.items() %} - -

{{ key }}

+
+ {{ key }}
{{ value | display_json }}
- +
{% endfor %}
{% else %} @@ -521,7 +523,7 @@

{{ key }}

{% endif -%} {% endfor -%} -
+ {% endif %} @@ -544,75 +546,43 @@

{{ key }}

* - Within each ecosystem, all package headers are expanded if their count is below `PACKAGE_EXPAND_THRESHOLD`. */ function setupVerticalLayout() { - const ecosystemHeaders = document.querySelectorAll('.vulnerability-packages.force-collapse .package-header'); - if (!ecosystemHeaders.length) return; + const ecosystemAccordions = document.querySelectorAll('.vulnerability-packages.force-collapse .ecosystem-accordion'); + if (!ecosystemAccordions.length) return; - const shouldExpandEcosystems = ecosystemHeaders.length < ECOSYSTEM_EXPAND_THRESHOLD; + const shouldExpandEcosystems = ecosystemAccordions.length < ECOSYSTEM_EXPAND_THRESHOLD; - ecosystemHeaders.forEach(header => { - const panel = header.nextElementSibling; - const packageHeaders = panel ? panel.querySelectorAll('.package-name-title') : []; + ecosystemAccordions.forEach(accordion => { + const panel = accordion.querySelector('.ecosystem-content-panel'); + const packageAccordions = panel ? panel.querySelectorAll('.package-accordion') : []; const hasManyPackages = - ecosystemHeaders.length > 1 && packageHeaders.length > ECOSYSTEM_PACKAGE_COLLAPSE_THRESHOLD; + ecosystemAccordions.length > 1 && packageAccordions.length > ECOSYSTEM_PACKAGE_COLLAPSE_THRESHOLD; - const shouldExpandHeader = + const shouldExpandAccordion = shouldExpandEcosystems && - header.getAttribute('aria-expanded') === 'false' && + !accordion.open && panel !== null && !hasManyPackages; - if (shouldExpandHeader) { - header.click(); + if (shouldExpandAccordion) { + accordion.open = true; } }); const ecosystemPanels = document.querySelectorAll('.ecosystem-content-panel'); ecosystemPanels.forEach(panel => { - const packageHeaders = panel.querySelectorAll('.package-name-title'); - if (packageHeaders.length <= PACKAGE_EXPAND_THRESHOLD) { - packageHeaders.forEach(header => { - if (header.getAttribute('aria-expanded') === 'false') { - header.click(); + const packageAccordions = panel.querySelectorAll('.package-accordion'); + if (packageAccordions.length <= PACKAGE_EXPAND_THRESHOLD) { + packageAccordions.forEach(accordion => { + if (!accordion.open) { + accordion.open = true; } }); } }); } - /** - * Sets up the default layout,(tabs on desktop, accordion on mobile). - * This function ensures that on the mobile accordion view, - * all package headers are expanded by default if their count is - * below a defined threshold. This has no effect on the desktop tab view. - */ - function setupDefaultLayout() { - const packageHeaders = document.querySelectorAll('.vulnerability-packages:not(.force-collapse) .package-header'); - if (!packageHeaders.length) return; - - /** - * Expands collapsed package headers. - * We use `spicy-section` to make the packages content collapsible for mobile view, - * but it collapses the content by default. We want it expanded after the page - * is loaded, so we programmatically click on the header of collapsed packages - * to make the content visible. - */ - function expandPackageHeaders() { - packageHeaders.forEach((header) => { - if (header.getAttribute('aria-expanded') === 'false') { - header.click(); - } - }); - } - - if (packageHeaders.length < ECOSYSTEM_EXPAND_THRESHOLD) { - expandPackageHeaders(); - } - } - if (document.querySelector('.vulnerability-packages.force-collapse')) { setupVerticalLayout(); - } else { - setupDefaultLayout(); } /** From e7a7851eec4bd6606fe9437186ac56dcb1338f0b Mon Sep 17 00:00:00 2001 From: ashmod Date: Wed, 31 Dec 2025 22:14:42 +0200 Subject: [PATCH 2/8] fix collapse/expand logic --- .../src/templates/vulnerability.html | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/gcp/website/frontend3/src/templates/vulnerability.html b/gcp/website/frontend3/src/templates/vulnerability.html index f1c7ea29a92..b86899a2250 100644 --- a/gcp/website/frontend3/src/templates/vulnerability.html +++ b/gcp/website/frontend3/src/templates/vulnerability.html @@ -180,13 +180,13 @@

Affected packages

{% for ecosystem_name, packages in ecosystems.items() -%} {% set is_last_ecosystem = loop.last %} -
+
{{ ecosystem_name }}
{% for affected in packages -%} -
+
{% if 'package' in affected %} {{ affected.package.name }} @@ -559,25 +559,15 @@

const shouldExpandAccordion = shouldExpandEcosystems && - !accordion.open && panel !== null && !hasManyPackages; - if (shouldExpandAccordion) { - accordion.open = true; - } - }); + accordion.open = shouldExpandAccordion; - const ecosystemPanels = document.querySelectorAll('.ecosystem-content-panel'); - ecosystemPanels.forEach(panel => { - const packageAccordions = panel.querySelectorAll('.package-accordion'); - if (packageAccordions.length <= PACKAGE_EXPAND_THRESHOLD) { - packageAccordions.forEach(accordion => { - if (!accordion.open) { - accordion.open = true; - } - }); - } + const shouldExpandPackages = packageAccordions.length <= PACKAGE_EXPAND_THRESHOLD; + packageAccordions.forEach(packageAccordion => { + packageAccordion.open = shouldExpandPackages; + }); }); } From c1952fc1a51d50615ba31cac9cdb9b40b4c7e087 Mon Sep 17 00:00:00 2001 From: ashmod Date: Wed, 31 Dec 2025 23:04:44 +0200 Subject: [PATCH 3/8] fix ecosystem filters --- gcp/website/frontend3/src/styles.scss | 108 +++++------------- gcp/website/frontend3/src/templates/list.html | 37 +++--- 2 files changed, 42 insertions(+), 103 deletions(-) diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index 2446c04f5ff..9f08fbfbb9f 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -385,101 +385,45 @@ pre { .ecosystem-buttons { margin-top: 20px; + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; - > summary { - list-style: none; - &::-webkit-details-marker { - display: none; - } - &::marker { - display: none; + .ecosystem-label-all { + position: relative; + margin-right: 20px; + + &::after { content: ''; + position: absolute; + right: -16px; + top: 50%; + transform: translateY(-50%); + width: 1px; + height: 30px; + background-color: $osv-grey-600; } } - @media (min-width: #{$osv-mobile-breakpoint + 1}) { - display: flex; - flex-direction: row; - flex-wrap: wrap; - gap: 10px; - align-items: center; - - > .ecosystem-collapse-header { - display: inline-flex; - align-items: center; - } + @media (max-width: #{$osv-mobile-breakpoint}) { + gap: 8px; - > .ecosystem-content { - display: inline-flex; - flex-wrap: wrap; - gap: 10px; - align-items: center; + .ecosystem-label { + padding: 8px 14px; + height: 32px; + gap: 12px; + font-size: 14px; } .ecosystem-label-all { - position: relative; - margin-right: 20px; + margin-right: 14px; &::after { - content: ''; - position: absolute; - right: -16px; - top: 50%; - transform: translateY(-50%); - width: 1px; - height: 30px; - background-color: $osv-grey-600; + height: 20px; + right: -12px; } } - - .ecosystems-divider { - display: none; - } - } - - @media (max-width: #{$osv-mobile-breakpoint}) { - display: block; - - &:not([open]) > .ecosystem-content { - display: none; - } - - &[open] > .ecosystem-content { - display: flex; - flex-wrap: wrap; - gap: 10px; - width: 100%; - margin-top: 10px; - } - - > .ecosystem-collapse-header { - display: flex; - align-items: center; - cursor: pointer; - width: 100%; - - &::before { - content: ''; - display: block; - width: 12px; - height: 12px; - margin-right: 8px; - background: url(/static/img/filled-triangle.svg); - background-position: center; - background-repeat: no-repeat; - filter: invert(100%); - transform: rotate(0deg); - transition: transform 0.2s ease-in-out; - } - } - - &[open] > .ecosystem-collapse-header::before { - transform: rotate(90deg); - } - - .ecosystems-divider { - display: none; - } } .ecosystem-label { diff --git a/gcp/website/frontend3/src/templates/list.html b/gcp/website/frontend3/src/templates/list.html index ffb4137df60..77c2e20d99f 100644 --- a/gcp/website/frontend3/src/templates/list.html +++ b/gcp/website/frontend3/src/templates/list.html @@ -43,27 +43,22 @@

Vulnerabilities

{% if ecosystem_counts %} -
- - - - -
- - {% for ecosystem in ecosystem_counts %} - - - {% endfor %} -
-
+
+ + + {% for ecosystem in ecosystem_counts %} + + + {% endfor %} +
{% endif %}
From 865ef3e1af354598d43a4e43ae6b5a471a7f0a9d Mon Sep 17 00:00:00 2001 From: ashmod Date: Wed, 31 Dec 2025 23:23:19 +0200 Subject: [PATCH 4/8] styling --- gcp/website/frontend3/src/styles.scss | 7 ++++--- gcp/website/frontend3/src/templates/vulnerability.html | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index 9f08fbfbb9f..02976300e67 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -1042,10 +1042,11 @@ osv-tabs-accordion.vulnerability-packages[affordance="collapse"] { &::before { content: ''; - width: 12px; - height: 12px; + width: 11px; + height: 11px; margin-right: 8px; background: url(/static/img/filled-triangle.svg); + background-size: contain; background-position: center; background-repeat: no-repeat; transform: rotate(0deg); @@ -1163,7 +1164,6 @@ osv-tabs-accordion.vulnerability-packages[affordance="collapse"] { .package-accordion { position: relative; - margin-bottom: 8px; > summary { list-style: none; @@ -1196,6 +1196,7 @@ osv-tabs-accordion.vulnerability-packages[affordance="collapse"] { .package-accordion summary.package-name-title { font-family: $osv-heading-font-family; font-size: 1.1rem; + font-weight: bold; color: #f1f1f1; padding: 12px 16px; cursor: pointer; diff --git a/gcp/website/frontend3/src/templates/vulnerability.html b/gcp/website/frontend3/src/templates/vulnerability.html index b86899a2250..063b71190ec 100644 --- a/gcp/website/frontend3/src/templates/vulnerability.html +++ b/gcp/website/frontend3/src/templates/vulnerability.html @@ -312,7 +312,7 @@

Database specific {% for key, value in db_specific.items() %} -
+
{{ key }}
{{ value | display_json }}
@@ -507,7 +507,7 @@

{% if db_specific is mapping %}
{% for key, value in db_specific.items() %} -
+
{{ key }}
{{ value | display_json }}
From 7b2be0a457cde2d29c0e9a5e979385f65b43d9cc Mon Sep 17 00:00:00 2001 From: ashmod Date: Thu, 1 Jan 2026 01:14:10 +0200 Subject: [PATCH 5/8] refactor tab layout --- .../frontend3/src/osv-tabs-accordion.js | 83 +++++++++++++++++-- gcp/website/frontend3/src/styles.scss | 20 ++--- .../src/templates/vulnerability.html | 4 +- 3 files changed, 84 insertions(+), 23 deletions(-) diff --git a/gcp/website/frontend3/src/osv-tabs-accordion.js b/gcp/website/frontend3/src/osv-tabs-accordion.js index 7e285ecdc28..48b73a38358 100644 --- a/gcp/website/frontend3/src/osv-tabs-accordion.js +++ b/gcp/website/frontend3/src/osv-tabs-accordion.js @@ -6,6 +6,47 @@ class OsvTabsAccordion extends HTMLElement { this.headers = []; this.panels = []; this.activeIndex = 0; + this.headerListeners = null; + + // shadow DOM for tab-bar layout + this.attachShadow({ mode: "open" }); + this.shadowRoot.innerHTML = ` + +
+ +
+
+ +
+ + `; } connectedCallback() { @@ -20,6 +61,7 @@ class OsvTabsAccordion extends HTMLElement { if (this.mediaQuery) { this.mediaQuery.removeEventListener("change", this.boundUpdateAffordance); } + this.removeEventListeners(); } collectHeadersAndPanels() { @@ -48,12 +90,27 @@ class OsvTabsAccordion extends HTMLElement { } setupEventListeners() { - this.headers.forEach((header, index) => { - header.addEventListener("click", (e) => this.handleHeaderClick(index, e)); - header.addEventListener("keydown", (e) => this.handleKeydown(index, e)); + this.headerListeners = this.headers.map((header, index) => { + const clickListener = (e) => this.handleHeaderClick(index, e); + const keydownListener = (e) => this.handleKeydown(index, e); + + header.addEventListener("click", clickListener); + header.addEventListener("keydown", keydownListener); + + return { header, clickListener, keydownListener }; }); } + removeEventListeners() { + if (this.headerListeners) { + this.headerListeners.forEach(({ header, clickListener, keydownListener }) => { + header.removeEventListener("click", clickListener); + header.removeEventListener("keydown", keydownListener); + }); + this.headerListeners = null; + } + } + updateAffordance() { const isDesktop = this.mediaQuery.matches; const affordance = isDesktop ? "tab-bar" : "collapse"; @@ -69,6 +126,7 @@ class OsvTabsAccordion extends HTMLElement { renderTabs() { this.headers.forEach((header, index) => { const isActive = index === this.activeIndex; + header.setAttribute("slot", "tab"); header.setAttribute("tabindex", isActive ? "0" : "-1"); header.setAttribute("role", "tab"); header.setAttribute("aria-selected", isActive ? "true" : "false"); @@ -78,28 +136,35 @@ class OsvTabsAccordion extends HTMLElement { this.panels.forEach((panel, index) => { const isActive = index === this.activeIndex; + panel.setAttribute("slot", "panel"); panel.setAttribute("role", "tabpanel"); + if (isActive) { + panel.setAttribute("data-panel-active", ""); + } else { + panel.removeAttribute("data-panel-active"); + } panel.style.display = isActive ? "" : "none"; - panel.removeAttribute("aria-hidden"); }); } renderAccordion() { - // By default, expand all panels in accordion mode this.headers.forEach((header, index) => { const panel = this.panels[index]; - panel.style.display = ""; - header.setAttribute("role", "button"); + header.removeAttribute("slot"); + header.removeAttribute("role"); + header.removeAttribute("aria-selected"); header.setAttribute("tabindex", "0"); header.setAttribute("aria-expanded", "true"); header.setAttribute("expanded", ""); - header.removeAttribute("aria-selected"); + + panel.removeAttribute("slot"); + panel.removeAttribute("data-panel-active"); + panel.style.display = ""; }); this.panels.forEach((panel) => { panel.setAttribute("role", "region"); - panel.removeAttribute("aria-hidden"); }); } diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index 02976300e67..6f6b2919af7 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -874,18 +874,7 @@ dl.vulnerability-details, } osv-tabs-accordion.vulnerability-packages[affordance="tab-bar"] { - display: grid; - grid-template-columns: repeat(auto-fill, minmax(150px, auto)); - grid-template-rows: auto 1fr; - - > h2.package-header { - grid-row: 1; - } - - > div { - grid-row: 2; - grid-column: 1 / -1; - } + display: block; } osv-tabs-accordion.vulnerability-packages[affordance="collapse"] { @@ -947,6 +936,12 @@ osv-tabs-accordion.vulnerability-packages[affordance="collapse"] { } .versions-section { + margin-bottom: 28px; + + &:last-child { + margin-bottom: 0; + } + > summary { list-style: none; &::-webkit-details-marker { @@ -992,6 +987,7 @@ osv-tabs-accordion.vulnerability-packages[affordance="collapse"] { overflow: auto; flex-wrap: wrap; gap: 8px; + margin-top: 8px; padding-bottom: 12px; } diff --git a/gcp/website/frontend3/src/templates/vulnerability.html b/gcp/website/frontend3/src/templates/vulnerability.html index 063b71190ec..d5d91c2ac64 100644 --- a/gcp/website/frontend3/src/templates/vulnerability.html +++ b/gcp/website/frontend3/src/templates/vulnerability.html @@ -286,7 +286,7 @@

Affected ranges Affected versions

{% for group, versions in (affected.versions|group_versions(ecosystem_name)).items() -%} -
+
{{ group }}
{% for version in versions -%} @@ -471,7 +471,7 @@

{% for group, versions in (affected.versions|group_versions(ecosystem)).items() -%} -
+
{{ group }}
{% for version in versions -%} From 14e8319d0bdf5ba39aacce42df41fb3c66667ead Mon Sep 17 00:00:00 2001 From: ashmod Date: Tue, 6 Jan 2026 20:25:13 +0200 Subject: [PATCH 6/8] rename --- gcp/website/frontend3/src/index.js | 2 +- .../frontend3/src/{osv-tabs-accordion.js => osv-tabs.js} | 4 ++-- gcp/website/frontend3/src/styles.scss | 4 ++-- gcp/website/frontend3/src/templates/vulnerability.html | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) rename gcp/website/frontend3/src/{osv-tabs-accordion.js => osv-tabs.js} (98%) diff --git a/gcp/website/frontend3/src/index.js b/gcp/website/frontend3/src/index.js index 29f1588b5f7..e0f6ada973b 100644 --- a/gcp/website/frontend3/src/index.js +++ b/gcp/website/frontend3/src/index.js @@ -4,7 +4,7 @@ import '@material/web/icon/icon.js'; import '@material/web/iconbutton/icon-button.js'; import '@material/web/progress/circular-progress.js'; import '@hotwired/turbo'; -import './osv-tabs-accordion.js'; +import './osv-tabs.js'; import { MdFilledTextField } from '@material/web/textfield/filled-text-field.js'; import { LitElement, html } from 'lit'; import { ExpandableSearch, SearchSuggestionsManager } from './search.js'; diff --git a/gcp/website/frontend3/src/osv-tabs-accordion.js b/gcp/website/frontend3/src/osv-tabs.js similarity index 98% rename from gcp/website/frontend3/src/osv-tabs-accordion.js rename to gcp/website/frontend3/src/osv-tabs.js index 48b73a38358..7e7f0f707b4 100644 --- a/gcp/website/frontend3/src/osv-tabs-accordion.js +++ b/gcp/website/frontend3/src/osv-tabs.js @@ -1,4 +1,4 @@ -class OsvTabsAccordion extends HTMLElement { +class OsvTabs extends HTMLElement { constructor() { super(); this.breakpoint = 500; @@ -220,4 +220,4 @@ class OsvTabsAccordion extends HTMLElement { } } -customElements.define("osv-tabs-accordion", OsvTabsAccordion); +customElements.define("osv-tabs", OsvTabs); diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index 6f6b2919af7..6ddec5844cc 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -873,11 +873,11 @@ dl.vulnerability-details, } } -osv-tabs-accordion.vulnerability-packages[affordance="tab-bar"] { +osv-tabs.vulnerability-packages[affordance="tab-bar"] { display: block; } -osv-tabs-accordion.vulnerability-packages[affordance="collapse"] { +osv-tabs.vulnerability-packages[affordance="collapse"] { display: block; > h2.package-header { diff --git a/gcp/website/frontend3/src/templates/vulnerability.html b/gcp/website/frontend3/src/templates/vulnerability.html index d5d91c2ac64..3a37f434249 100644 --- a/gcp/website/frontend3/src/templates/vulnerability.html +++ b/gcp/website/frontend3/src/templates/vulnerability.html @@ -336,7 +336,7 @@

Database specific {% for affected in vulnerability.affected -%} {% if 'package' in affected %} @@ -523,7 +523,7 @@

{% endif -%}

{% endfor -%} - + {% endif %}
From ce28d5ffea9785b594b0148c72bcbf9fd42dbb5f Mon Sep 17 00:00:00 2001 From: ashmod Date: Tue, 6 Jan 2026 21:23:07 +0200 Subject: [PATCH 7/8] use mixins and update paddings --- gcp/website/frontend3/src/styles.scss | 145 ++++++++------------------ 1 file changed, 43 insertions(+), 102 deletions(-) diff --git a/gcp/website/frontend3/src/styles.scss b/gcp/website/frontend3/src/styles.scss index 6ddec5844cc..90f45daa600 100644 --- a/gcp/website/frontend3/src/styles.scss +++ b/gcp/website/frontend3/src/styles.scss @@ -38,6 +38,37 @@ $osv-border-radius-small: 4px; margin-right: -50vw; } +@mixin hide-details-marker { + list-style: none; + &::-webkit-details-marker { + display: none; + } + &::marker { + display: none; + content: ''; + } +} + +// Indicator for expand/collapse UI +@mixin chevron-indicator($size: 12px, $margin-right: 8px, $color: $osv-grey-800) { + content: ''; + display: inline-block; + width: $size; + height: $size; + margin-right: $margin-right; + -webkit-mask-image: url(/static/img/filled-triangle.svg); + mask-image: url(/static/img/filled-triangle.svg); + -webkit-mask-size: contain; + mask-size: contain; + -webkit-mask-repeat: no-repeat; + mask-repeat: no-repeat; + -webkit-mask-position: center; + mask-position: center; + background-color: $color; + transform: rotate(0deg); + transition: transform 0.2s ease-in-out, background-color 0.2s ease-in-out; +} + /** Reset */ *, @@ -886,22 +917,7 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { cursor: pointer; &::before { - content: ''; - display: inline-block; - width: 12px; - height: 12px; - margin-right: 8px; - -webkit-mask-image: url(/static/img/filled-triangle.svg); - mask-image: url(/static/img/filled-triangle.svg); - -webkit-mask-size: contain; - mask-size: contain; - -webkit-mask-repeat: no-repeat; - mask-repeat: no-repeat; - -webkit-mask-position: center; - mask-position: center; - background-color: $osv-grey-800; - transform: rotate(0deg); - transition: transform 0.2s ease-in-out, background-color 0.2s ease-in-out; + @include chevron-indicator; } &[expanded]::before { @@ -936,36 +952,12 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { } .versions-section { - margin-bottom: 28px; - - &:last-child { - margin-bottom: 0; - } - > summary { - list-style: none; - &::-webkit-details-marker { - display: none; - } - &::marker { - display: none; - content: ''; - } + @include hide-details-marker; } summary.version-header::before { - content: ''; - display: inline-block; - width: 12px; - height: 12px; - margin-right: 16px; - background: url(/static/img/filled-triangle.svg); - background-position: center; - background-repeat: no-repeat; - transform: rotate(0deg); - transition: transform 0.2s ease-in-out; - // Make the filled triangle white. - filter: invert(100%); + @include chevron-indicator($margin-right: 16px, $color: #fff); } &[open] > summary.version-header::before { @@ -976,7 +968,7 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { background: none; font-family: $osv-heading-font-family; color: #fff; - padding-left: 0px; + padding: 16px 0; font-size: 16px; cursor: pointer; } @@ -1016,38 +1008,21 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { .database-specific-section { > summary { - list-style: none; - &::-webkit-details-marker { - display: none; - } - &::marker { - display: none; - content: ''; - } + @include hide-details-marker; } summary.database-specific-header { background: none; font-family: $osv-heading-font-family; color: #fff; - padding-left: 0; + padding: 16px 0; font-size: 16px; display: flex; align-items: center; cursor: pointer; &::before { - content: ''; - width: 11px; - height: 11px; - margin-right: 8px; - background: url(/static/img/filled-triangle.svg); - background-size: contain; - background-position: center; - background-repeat: no-repeat; - transform: rotate(0deg); - transition: transform 0.2s ease-in-out; - filter: invert(100%); + @include chevron-indicator($color: #fff); } } @@ -1063,14 +1038,7 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { &.force-collapse { .ecosystem-accordion > summary { - list-style: none; - &::-webkit-details-marker { - display: none; - } - &::marker { - display: none; - content: ''; - } + @include hide-details-marker; } summary.package-header { @@ -1087,17 +1055,7 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { display: block; &::before { - content: ''; - display: inline-block; - width: 12px; - height: 12px; - margin-right: 8px; - background-image: url(/static/img/filled-triangle.svg); - background-position: center; - background-repeat: no-repeat; - transition: transform 0.2s ease-in-out; - transform: rotate(0deg); - filter: invert(100%); + @include chevron-indicator($color: #fff); } &:hover { @@ -1112,7 +1070,7 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { &::before { transform: rotate(90deg); - filter: invert(24%) sepia(89%) saturate(2293%) hue-rotate(345deg) brightness(81%) contrast(107%); + background-color: $osv-accent-color; } &:hover { @@ -1162,14 +1120,7 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { position: relative; > summary { - list-style: none; - &::-webkit-details-marker { - display: none; - } - &::marker { - display: none; - content: ''; - } + @include hide-details-marker; } // The horizontal "connector" line @@ -1202,18 +1153,8 @@ osv-tabs.vulnerability-packages[affordance="collapse"] { display: block; &::before { - content: ''; - width: 12px; - height: 12px; - margin-right: 8px; - background-image: url(/static/img/filled-triangle.svg); - background-position: center; - background-repeat: no-repeat; - filter: invert(100%); - transition: transform 0.2s ease-in-out; - display: inline-block; + @include chevron-indicator($color: #fff); vertical-align: middle; - transform: rotate(0deg); } } From 613d27d33efc6c7e359e32229c5a91dd41ae486e Mon Sep 17 00:00:00 2001 From: ashmod Date: Fri, 9 Jan 2026 21:30:12 +0200 Subject: [PATCH 8/8] remove redundant check --- gcp/website/frontend3/src/osv-tabs.js | 1 - 1 file changed, 1 deletion(-) diff --git a/gcp/website/frontend3/src/osv-tabs.js b/gcp/website/frontend3/src/osv-tabs.js index 7e7f0f707b4..0f13df4faaa 100644 --- a/gcp/website/frontend3/src/osv-tabs.js +++ b/gcp/website/frontend3/src/osv-tabs.js @@ -143,7 +143,6 @@ class OsvTabs extends HTMLElement { } else { panel.removeAttribute("data-panel-active"); } - panel.style.display = isActive ? "" : "none"; }); }