Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
172 changes: 163 additions & 9 deletions src/css/style.catalog.css
Original file line number Diff line number Diff line change
Expand Up @@ -51,22 +51,176 @@
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;
fill: theme(colors.indigo.600);
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);
}
Expand Down
108 changes: 97 additions & 11 deletions src/integrations/index.njk
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
}
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -319,12 +359,10 @@ Explore the list of integrations and modules available for your Node-RED project
<li class="integration-card group border border-gray-300 rounded-xl bg-white drop-shadow-md">
<a href="${nodeUrl}"${linkAttrs} class="h-48 flex flex-col">
<div class="integration-card--details p-3 grow min-h-0">
<div class="flex justify-between text-sm items-center">
<span>@${integration.npmScope || integration.npmOwners[0]}${externalIcon}</span>
<span class="ff-certified-tag" style="display: ${certified ? 'flex' : 'none'}">
<certified-icon />
</span>
</div>
<div class="flex justify-between text-sm items-center gap-2">
<span class="truncate">@${integration.npmScope || integration.npmOwners[0]}${externalIcon}</span>
<span class="certified-pill" style="display: ${certified ? 'inline-flex' : 'none'}" title="FlowFuse Certified"><certified-icon></certified-icon><span>Certified</span></span>
</div>
<label class="group-hover:text-indigo-600 cursor-pointer">${integration.name}</label>
<p class="text-sm my-2 leading-5">${description}</p>
</div>
Expand All @@ -343,13 +381,61 @@ Explore the list of integrations and modules available for your Node-RED project
}
customElements.define('integration-tile', IntegrationTile);
</script>
<section class="certified-hero mb-8">
<div class="container m-auto md:max-w-6xl">
<div class="certified-hero--card rounded-lg border-[3px] border-indigo-200 p-6 md:p-10 grid md:grid-cols-12 gap-8 md:gap-10 items-start">
<div class="md:col-span-5">
<span class="certified-eyebrow"><certified-icon></certified-icon><span>FlowFuse Certified</span></span>
<h2 class="certified-hero--title text-balance mb-8">Certified Nodes, backed by their authors and supported long-term</h2>
<p class="certified-hero--lede">
Choosing a Node-RED node for production raises questions you can't always answer from a README. Is it actively maintained? Is it secure? Will the maintainer still be around in two years? Certified Nodes answer those questions.
</p>
<div class="certified-hero--actions mt-8">
<button
id="certified-pill-toggle"
type="button"
class="ff-btn ff-btn--primary uppercase certified-toggle"
aria-pressed="false"
onclick="setCertified(!filterCertified)"><certified-icon></certified-icon><span>Show only Certified</span><span id="certified-count-wrapper" class="certified-toggle--count" hidden> (<span id="certified-count"></span>)</span></button>
{# TODO: repoint to a proper FlowFuse-owned Certified Nodes explainer page when one exists. A year-old blog post is not the long-term destination. #}
<a class="certified-hero--link inline-flex items-center gap-1 uppercase" href="/blog/2025/07/certified-nodes-v2/">
Learn more {% include "components/icons/arrow-long-right.svg" %}
</a>
</div>
</div>
<ul class="certified-pillars md:col-span-7">
<li class="certified-pillar">
<span class="certified-pillar--icon" aria-hidden="true">{% include "components/icons/users.svg" %}</span>
<div>
<h3 class="certified-pillar--title">Vetted authors</h3>
<p class="certified-pillar--body">Every Certified Node comes from a developer with a track record in their domain — not an anonymous npm publisher.</p>
</div>
</li>
<li class="certified-pillar">
<span class="certified-pillar--icon" aria-hidden="true">{% include "components/icons/shield-check.svg" %}</span>
<div>
<h3 class="certified-pillar--title">Supported through production</h3>
<p class="certified-pillar--body">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.</p>
</div>
</li>
<li class="certified-pillar">
<span class="certified-pillar--icon" aria-hidden="true">{% include "components/icons/code-bracket.svg" %}</span>
<div>
<h3 class="certified-pillar--title">Free or commercial, same bar</h3>
<p class="certified-pillar--body">Some Certified Nodes are free and open; others target specific enterprise needs. The certification standard is the same.</p>
</div>
</li>
</ul>
</div>
</div>
</section>
<div class="container m-auto text-left md:max-w-6xl pt-8 pb-12 w-full ff-full-bg gap-4 flex">
<div class="catalogue-filters w-52 shrink-0 hidden md:block">
<label>Filters</label>
<ul>
<li>
<input type="checkbox" id="catalogue-filter-certified" onchange="toggleCertified()"/>
<label class="inline-flex gap-1 items-center" for="catalogue-filter-certified">FlowFuse Certified <certified-icon /></label>
<label class="inline-flex gap-1 items-center" for="catalogue-filter-certified">FlowFuse Certified <certified-icon></certified-icon></label>
</li>
</ul>
<label>Categories</label>
Expand All @@ -360,9 +446,9 @@ Explore the list of integrations and modules available for your Node-RED project
</div>
<div class="grow max-md:max-w-lg mx-auto">
<input id="search-catalogue" class="catalogue-search" type="text" placeholder="Search Integrations" onkeyup="filterCatalogue()" onchange="filterCatalogue()"/>
<div class="catalogue-meta">
<div class="catalogue-meta" aria-live="polite" aria-atomic="true">
<div id="count-container" style="display: none;"><span id="integrations-count">X</span> Integrations</div>
<div id="count-placeholder"><span id="integrations-count">Loading...</div>
<div id="count-placeholder"><span>Loading...</span></div>
</div>
<ul id="integration-tiles-placeholder" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
<integration-tile-placeholder></integration-tile-placeholder>
Expand Down