Skip to content

Commit fb27a82

Browse files
committed
Surface FlowFuse Certified program with hero card, URL-driven filter, and tile badge
1 parent 86ab0ab commit fb27a82

2 files changed

Lines changed: 260 additions & 19 deletions

File tree

src/css/style.catalog.css

Lines changed: 167 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -51,22 +51,180 @@
5151
color: theme(colors.blue.500);
5252
}
5353

54-
/* .ff-certified-tag {
55-
background-color: theme(colors.blue.50);
56-
border: 1px solid theme(colors.blue.600);
57-
padding: 3px 6px;
58-
border-radius: 6px;
59-
align-items: center;
60-
gap: 3px;
61-
} */
62-
6354
.certified-icon {
6455
width: 24px;
6556
height: 24px;
6657
fill: theme(colors.indigo.600);
6758
stroke: theme(colors.white);
6859
}
6960

61+
/* Compensates for catalog.njk's empty `{% block actions %}` div, which always
62+
reserves ~32px of vertical space whether or not a page defines the block. */
63+
.certified-hero {
64+
margin-top: -32px;
65+
}
66+
67+
.certified-eyebrow {
68+
display: inline-flex;
69+
align-items: center;
70+
gap: 8px;
71+
color: theme(colors.indigo.700);
72+
font-size: 12px;
73+
font-weight: 700;
74+
letter-spacing: 0.1em;
75+
text-transform: uppercase;
76+
margin-bottom: 8px;
77+
line-height: 1;
78+
}
79+
80+
.certified-eyebrow .certified-icon {
81+
width: 18px;
82+
height: 18px;
83+
}
84+
85+
.certified-hero--title {
86+
font-size: 24px;
87+
line-height: 1.25;
88+
font-weight: 700;
89+
color: theme(colors.gray.900);
90+
margin: 0 0 8px 0;
91+
}
92+
93+
@media (min-width: 768px) {
94+
.certified-hero--title {
95+
font-size: 28px;
96+
}
97+
}
98+
99+
.certified-hero--lede {
100+
font-size: 16px;
101+
line-height: 1.5;
102+
color: theme(colors.gray.700);
103+
margin: 0 0 20px 0;
104+
}
105+
106+
.certified-hero--actions {
107+
display: flex;
108+
flex-wrap: wrap;
109+
align-items: center;
110+
gap: 20px;
111+
}
112+
113+
.certified-hero--link {
114+
color: theme(colors.indigo.700);
115+
font-weight: 600;
116+
font-size: 14px;
117+
}
118+
119+
.certified-hero--link:hover {
120+
color: theme(colors.indigo.800);
121+
}
122+
123+
.certified-pillars {
124+
list-style: none;
125+
padding: 0;
126+
margin: 0;
127+
display: grid;
128+
gap: 20px;
129+
}
130+
131+
.certified-pillar {
132+
display: grid;
133+
grid-template-columns: 36px 1fr;
134+
gap: 12px;
135+
align-items: start;
136+
}
137+
138+
.certified-pillar--icon {
139+
display: inline-flex;
140+
align-items: center;
141+
justify-content: center;
142+
width: 36px;
143+
height: 36px;
144+
border-radius: 9999px;
145+
background-color: theme(colors.white);
146+
border: 1px solid theme(colors.indigo.200);
147+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
148+
color: theme(colors.indigo.600);
149+
}
150+
151+
.certified-pillar--icon svg {
152+
width: 20px;
153+
height: 20px;
154+
}
155+
156+
.certified-pillar--title {
157+
font-size: 16px;
158+
font-weight: 700;
159+
color: theme(colors.gray.900);
160+
margin: 0 0 4px 0;
161+
}
162+
163+
.certified-pillar--body {
164+
font-size: 14px;
165+
line-height: 1.5;
166+
color: theme(colors.gray.700);
167+
margin: 0;
168+
}
169+
170+
.certified-pill {
171+
display: inline-flex;
172+
align-items: center;
173+
gap: 4px;
174+
padding: 2px 8px 2px 4px;
175+
background-color: theme(colors.indigo.50);
176+
color: theme(colors.indigo.700);
177+
border: 1px solid theme(colors.indigo.200);
178+
border-radius: 9999px;
179+
font-size: 12px;
180+
font-weight: 600;
181+
line-height: 1.4;
182+
white-space: nowrap;
183+
}
184+
185+
.certified-pill .certified-icon {
186+
width: 16px;
187+
height: 16px;
188+
}
189+
190+
.certified-toggle {
191+
display: inline-flex;
192+
align-items: center;
193+
gap: 8px;
194+
cursor: pointer;
195+
transition: background-color 150ms ease, color 150ms ease;
196+
}
197+
198+
.certified-toggle:focus-visible {
199+
outline: 2px solid theme(colors.indigo.600);
200+
outline-offset: 2px;
201+
}
202+
203+
.certified-toggle .certified-icon {
204+
width: 18px;
205+
height: 18px;
206+
fill: theme(colors.white);
207+
stroke: theme(colors.indigo.600);
208+
}
209+
210+
.certified-toggle[aria-pressed="true"],
211+
.certified-toggle.certified-pill--active {
212+
background-color: theme(colors.indigo.50);
213+
color: theme(colors.indigo.700);
214+
box-shadow: inset 0 0 0 1px theme(colors.indigo.300);
215+
}
216+
217+
.certified-toggle[aria-pressed="true"] .certified-icon,
218+
.certified-toggle.certified-pill--active .certified-icon {
219+
fill: theme(colors.indigo.600);
220+
stroke: theme(colors.white);
221+
}
222+
223+
.certified-toggle--count {
224+
opacity: 0.85;
225+
font-weight: 400;
226+
}
227+
70228
.integration-card a {
71229
color: theme(colors.gray.600);
72230
}

src/integrations/index.njk

Lines changed: 93 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@ Explore the list of integrations and modules available for your Node-RED project
3333
currentPage: 0,
3434
maxPages: 0
3535
}
36-
let filterCertified = false;
36+
const params = new URLSearchParams(window.location.search);
37+
let filterCertified = params.get('certified') === '1';
3738
const filters = {
3839
ai: {
3940
checked: false,
@@ -84,7 +85,7 @@ Explore the list of integrations and modules available for your Node-RED project
8485
label: 'Utility'
8586
}
8687
}
87-
var catalogue = []
88+
let catalogue = []
8889
8990
function showElementById (id) {
9091
document.getElementById(id).style.display = 'block';
@@ -116,6 +117,10 @@ Explore the list of integrations and modules available for your Node-RED project
116117
catalogue = data.catalogue
117118
pagination.maxPage = Math.ceil(catalogue.length / pagination.perPage);
118119
renderFilters();
120+
// reflect any URL-driven certified state in the sidebar checkbox
121+
const sidebar = document.getElementById('catalogue-filter-certified');
122+
if (sidebar) sidebar.checked = filterCertified;
123+
syncCertifiedUI();
119124
filterCatalogue(catalogue);
120125
});
121126
}
@@ -171,9 +176,42 @@ Explore the list of integrations and modules available for your Node-RED project
171176
172177
function toggleCertified () {
173178
filterCertified = document.getElementById('catalogue-filter-certified').checked;
179+
syncCertifiedUI();
180+
syncCertifiedUrl();
174181
filterCatalogue();
175182
}
176183
184+
function setCertified (value) {
185+
filterCertified = !!value;
186+
const sidebar = document.getElementById('catalogue-filter-certified');
187+
if (sidebar) sidebar.checked = filterCertified;
188+
syncCertifiedUI();
189+
syncCertifiedUrl();
190+
filterCatalogue();
191+
}
192+
193+
function syncCertifiedUrl () {
194+
const url = new URL(window.location.href);
195+
if (filterCertified) {
196+
url.searchParams.set('certified', '1');
197+
} else {
198+
url.searchParams.delete('certified');
199+
}
200+
history.replaceState(null, '', url.toString());
201+
}
202+
203+
function syncCertifiedUI () {
204+
const pill = document.getElementById('certified-pill-toggle');
205+
if (pill) {
206+
pill.setAttribute('aria-pressed', filterCertified ? 'true' : 'false');
207+
pill.classList.toggle('certified-pill--active', filterCertified);
208+
}
209+
const count = document.getElementById('certified-count');
210+
if (count && Array.isArray(catalogue)) {
211+
count.textContent = catalogue.filter(n => n.ffCertified).length;
212+
}
213+
}
214+
177215
178216
function filterCatalogue () {
179217
const search = document.getElementById('search-catalogue').value;
@@ -319,12 +357,10 @@ Explore the list of integrations and modules available for your Node-RED project
319357
<li class="integration-card group border border-gray-300 rounded-xl bg-white drop-shadow-md">
320358
<a href="${nodeUrl}"${linkAttrs} class="h-48 flex flex-col">
321359
<div class="integration-card--details p-3 grow min-h-0">
322-
<div class="flex justify-between text-sm items-center">
323-
<span>@${integration.npmScope || integration.npmOwners[0]}${externalIcon}</span>
324-
<span class="ff-certified-tag" style="display: ${certified ? 'flex' : 'none'}">
325-
<certified-icon />
326-
</span>
327-
</div>
360+
<div class="flex justify-between text-sm items-center gap-2">
361+
<span class="truncate">@${integration.npmScope || integration.npmOwners[0]}${externalIcon}</span>
362+
<span class="certified-pill" style="display: ${certified ? 'inline-flex' : 'none'}" title="FlowFuse Certified"><certified-icon></certified-icon><span>Certified</span></span>
363+
</div>
328364
<label class="group-hover:text-indigo-600 cursor-pointer">${integration.name}</label>
329365
<p class="text-sm my-2 leading-5">${description}</p>
330366
</div>
@@ -343,13 +379,60 @@ Explore the list of integrations and modules available for your Node-RED project
343379
}
344380
customElements.define('integration-tile', IntegrationTile);
345381
</script>
382+
<section class="certified-hero">
383+
<div class="container m-auto md:max-w-6xl px-6">
384+
<div class="certified-hero--card bg-indigo-50 rounded-lg border-[3px] border-indigo-200 drop-shadow-xl p-6 md:p-10 grid md:grid-cols-12 gap-8 md:gap-10 items-start">
385+
<div class="md:col-span-5">
386+
<span class="certified-eyebrow"><certified-icon></certified-icon><span>FlowFuse Certified</span></span>
387+
<h2 class="certified-hero--title text-balance">Certified nodes, backed by their authors and supported long-term.</h2>
388+
<p class="certified-hero--lede">
389+
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.
390+
</p>
391+
<div class="certified-hero--actions">
392+
<button
393+
id="certified-pill-toggle"
394+
type="button"
395+
class="ff-btn ff-btn--primary uppercase certified-toggle"
396+
aria-pressed="false"
397+
onclick="setCertified(!filterCertified)"><certified-icon></certified-icon><span>Show only Certified</span><span class="certified-toggle--count">(<span id="certified-count">…</span>)</span></button>
398+
<a class="certified-hero--link inline-flex items-center gap-1" href="/blog/2025/07/certified-nodes-v2/">
399+
Learn more {% include "components/icons/arrow-long-right.svg" %}
400+
</a>
401+
</div>
402+
</div>
403+
<ul class="certified-pillars md:col-span-7">
404+
<li class="certified-pillar">
405+
<span class="certified-pillar--icon" aria-hidden="true">{% include "components/icons/users.svg" %}</span>
406+
<div>
407+
<h3 class="certified-pillar--title">Vetted authors</h3>
408+
<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>
409+
</div>
410+
</li>
411+
<li class="certified-pillar">
412+
<span class="certified-pillar--icon" aria-hidden="true">{% include "components/icons/shield-check.svg" %}</span>
413+
<div>
414+
<h3 class="certified-pillar--title">Tested for production</h3>
415+
<p class="certified-pillar--body">We check each node for reliability, security posture, and current documentation before it ships — and patch CVEs on our own timeline.</p>
416+
</div>
417+
</li>
418+
<li class="certified-pillar">
419+
<span class="certified-pillar--icon" aria-hidden="true">{% include "components/icons/code-bracket.svg" %}</span>
420+
<div>
421+
<h3 class="certified-pillar--title">Open source and proprietary, both welcome</h3>
422+
<p class="certified-pillar--body">Some Certified Nodes are free and open; others target specific enterprise needs. The certification standard is the same.</p>
423+
</div>
424+
</li>
425+
</ul>
426+
</div>
427+
</div>
428+
</section>
346429
<div class="container m-auto text-left md:max-w-6xl pt-8 pb-12 w-full ff-full-bg gap-4 flex">
347430
<div class="catalogue-filters w-52 shrink-0 hidden md:block">
348431
<label>Filters</label>
349432
<ul>
350433
<li>
351434
<input type="checkbox" id="catalogue-filter-certified" onchange="toggleCertified()"/>
352-
<label class="inline-flex gap-1 items-center" for="catalogue-filter-certified">FlowFuse Certified <certified-icon /></label>
435+
<label class="inline-flex gap-1 items-center" for="catalogue-filter-certified">FlowFuse Certified <certified-icon></certified-icon></label>
353436
</li>
354437
</ul>
355438
<label>Categories</label>
@@ -360,7 +443,7 @@ Explore the list of integrations and modules available for your Node-RED project
360443
</div>
361444
<div class="grow max-md:max-w-lg mx-auto">
362445
<input id="search-catalogue" class="catalogue-search" type="text" placeholder="Search Integrations" onkeyup="filterCatalogue()" onchange="filterCatalogue()"/>
363-
<div class="catalogue-meta">
446+
<div class="catalogue-meta" aria-live="polite" aria-atomic="true">
364447
<div id="count-container" style="display: none;"><span id="integrations-count">X</span> Integrations</div>
365448
<div id="count-placeholder"><span id="integrations-count">Loading...</div>
366449
</div>

0 commit comments

Comments
 (0)