diff --git a/src/css/style.catalog.css b/src/css/style.catalog.css index 61fe2ae508..e2e47f1495 100644 --- a/src/css/style.catalog.css +++ b/src/css/style.catalog.css @@ -51,15 +51,6 @@ color: theme(colors.blue.500); } -/* .ff-certified-tag { - background-color: theme(colors.blue.50); - border: 1px solid theme(colors.blue.600); - padding: 3px 6px; - border-radius: 6px; - align-items: center; - gap: 3px; -} */ - .certified-icon { width: 24px; height: 24px; @@ -67,6 +58,169 @@ stroke: theme(colors.white); } +/* Compensates for catalog.njk's empty `{% block actions %}` div, which always + reserves ~32px of vertical space whether or not a page defines the block. */ +.certified-hero { + margin-top: -32px; +} + +.certified-eyebrow { + display: inline-flex; + align-items: center; + gap: 8px; + color: theme(colors.indigo.700); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.1em; + text-transform: uppercase; + margin-bottom: 8px; + line-height: 1; +} + +.certified-eyebrow .certified-icon { + width: 18px; + height: 18px; +} + +.certified-hero--title { + font-size: 24px; + line-height: 1.25; + color: theme(colors.gray.900); + margin: 0 0 8px 0; +} + +@media (min-width: 768px) { + .certified-hero--title { + font-size: 28px; + } +} + +.certified-hero--lede { + font-size: 16px; + line-height: 1.5; + color: theme(colors.gray.700); + margin: 0 0 20px 0; +} + +.certified-hero--actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 20px; +} + +.certified-hero--link { + color: theme(colors.indigo.700); + font-weight: 600; + font-size: 14px; +} + +.certified-hero--link:hover { + color: theme(colors.indigo.800); +} + +.certified-pillars { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 20px; +} + +.certified-pillar { + display: grid; + grid-template-columns: 36px 1fr; + gap: 12px; + align-items: start; +} + +.certified-pillar--icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 9999px; + background-color: theme(colors.indigo.50); + color: theme(colors.indigo.600); +} + +.certified-pillar--icon svg { + width: 20px; + height: 20px; +} + +.certified-pillar--title { + font-size: 16px; + color: theme(colors.gray.900); + margin: 0 0 4px 0; +} + +.certified-pillar--body { + font-size: 14px; + line-height: 1.5; + color: theme(colors.gray.700); + margin: 0; +} + +.certified-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px 2px 4px; + background-color: theme(colors.indigo.50); + color: theme(colors.indigo.700); + border: 1px solid theme(colors.indigo.200); + border-radius: 9999px; + font-size: 12px; + font-weight: 600; + line-height: 1.4; + white-space: nowrap; +} + +.certified-pill .certified-icon { + width: 16px; + height: 16px; +} + +.certified-toggle { + display: inline-flex; + align-items: center; + gap: 8px; + cursor: pointer; + transition: background-color 150ms ease, color 150ms ease; +} + +.certified-toggle:focus-visible { + outline: 2px solid theme(colors.indigo.600); + outline-offset: 2px; +} + +.certified-toggle .certified-icon { + width: 18px; + height: 18px; + fill: theme(colors.white); + stroke: theme(colors.indigo.600); +} + +.certified-toggle[aria-pressed="true"], +.certified-toggle.certified-pill--active { + background-color: theme(colors.indigo.50); + color: theme(colors.indigo.700); + box-shadow: inset 0 0 0 1px theme(colors.indigo.300); +} + +.certified-toggle[aria-pressed="true"] .certified-icon, +.certified-toggle.certified-pill--active .certified-icon { + fill: theme(colors.indigo.600); + stroke: theme(colors.white); +} + +.certified-toggle--count { + opacity: 0.85; + font-weight: 400; +} + .integration-card a { color: theme(colors.gray.600); } diff --git a/src/integrations/index.njk b/src/integrations/index.njk index d36f1c2930..4c68f7b313 100644 --- a/src/integrations/index.njk +++ b/src/integrations/index.njk @@ -33,7 +33,8 @@ Explore the list of integrations and modules available for your Node-RED project currentPage: 0, maxPages: 0 } - let filterCertified = false; + const params = new URLSearchParams(window.location.search); + let filterCertified = params.get('certified') === '1'; const filters = { ai: { checked: false, @@ -84,7 +85,7 @@ Explore the list of integrations and modules available for your Node-RED project label: 'Utility' } } - var catalogue = [] + let catalogue = [] function showElementById (id) { document.getElementById(id).style.display = 'block'; @@ -116,6 +117,10 @@ Explore the list of integrations and modules available for your Node-RED project catalogue = data.catalogue pagination.maxPage = Math.ceil(catalogue.length / pagination.perPage); renderFilters(); + // reflect any URL-driven certified state in the sidebar checkbox + const sidebar = document.getElementById('catalogue-filter-certified'); + if (sidebar) sidebar.checked = filterCertified; + syncCertifiedUI(); filterCatalogue(catalogue); }); } @@ -171,9 +176,44 @@ Explore the list of integrations and modules available for your Node-RED project function toggleCertified () { filterCertified = document.getElementById('catalogue-filter-certified').checked; + syncCertifiedUI(); + syncCertifiedUrl(); filterCatalogue(); } + function setCertified (value) { + filterCertified = !!value; + const sidebar = document.getElementById('catalogue-filter-certified'); + if (sidebar) sidebar.checked = filterCertified; + syncCertifiedUI(); + syncCertifiedUrl(); + filterCatalogue(); + } + + function syncCertifiedUrl () { + const url = new URL(window.location.href); + if (filterCertified) { + url.searchParams.set('certified', '1'); + } else { + url.searchParams.delete('certified'); + } + history.replaceState(null, '', url.toString()); + } + + function syncCertifiedUI () { + const pill = document.getElementById('certified-pill-toggle'); + if (pill) { + pill.setAttribute('aria-pressed', filterCertified ? 'true' : 'false'); + pill.classList.toggle('certified-pill--active', filterCertified); + } + const count = document.getElementById('certified-count'); + const wrapper = document.getElementById('certified-count-wrapper'); + if (count && wrapper && Array.isArray(catalogue) && catalogue.length > 0) { + count.textContent = catalogue.filter(n => n.ffCertified).length; + wrapper.hidden = false; + } + } + function filterCatalogue () { const search = document.getElementById('search-catalogue').value; @@ -319,12 +359,10 @@ Explore the list of integrations and modules available for your Node-RED project
  • -
    - @${integration.npmScope || integration.npmOwners[0]}${externalIcon} - - - -
    +
    + @${integration.npmScope || integration.npmOwners[0]}${externalIcon} + Certified +

    ${description}

    @@ -343,13 +381,61 @@ Explore the list of integrations and modules available for your Node-RED project } customElements.define('integration-tile', IntegrationTile); +
    +
    +
    + +
      +
    • + +
      +

      Vetted authors

      +

      Every Certified Node comes from a developer with a track record in their domain — not an anonymous npm publisher.

      +
      +
    • +
    • + +
      +

      Supported through production

      +

      FlowFuse stands behind every Certified Node after launch — patching CVEs on our own timeline. Each node is vetted for reliability, security posture, and current documentation before shipping.

      +
      +
    • +
    • + +
      +

      Free or commercial, same bar

      +

      Some Certified Nodes are free and open; others target specific enterprise needs. The certification standard is the same.

      +
      +
    • +
    +
    +
    +
    -
    +
    -
    Loading...
    +
    Loading...