From 0c2cea8c259dbe5926973dc399136c09a1b9c91e Mon Sep 17 00:00:00 2001
From: Oleks
Date: Tue, 2 Jun 2026 04:16:58 +0300
Subject: [PATCH] fix: architecture + UI/UX review fixes
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Architecture:
- Cap homepage plugin list at PLUGIN_FETCH_CAP like other pages
- Declare @types/node directly instead of relying on transitive dep
- Single-source status label text (statuses.ts vs seed.json drift)
UI/UX:
- Stop auto-submitting filter selects so keyboard navigation works
- Fix heading hierarchy (add h2) on flat list pages
- Improve homepage title beyond bare "Plugins"
- Make status taxonomy descriptions self-contained
- Render only relevant statuses in the legend, not all 7
- Fix PluginCard "WordPress -> —" for missing target
- Clarify "{n} from / {n} targeting" microcopy
- Use proper count meta markup on CMS list
- Allow header nav row to wrap
- Fix bare CMS URL horizontal overflow
- Add standard line-clamp fallback to cards
- Even out footer stacked paragraph spacing
- Center plugin detail status line (drop margin-left hack)
- Raise toolbar tap targets to 44px
- Surface status badge meaning beyond title attribute
- Include source-CMS breadcrumb step on lookup miss
- Add link into filtered catalog from CMS detail
---
app/package-lock.json | 21 ++++++++++-----------
app/package.json | 3 ++-
app/seed/seed.json | 14 +++++++-------
app/src/components/PluginCard.astro | 2 +-
app/src/components/StatusBadge.astro | 2 +-
app/src/components/StatusLegend.astro | 6 +++++-
app/src/lib/statuses.ts | 16 +++++++++-------
app/src/pages/cms/[slug].astro | 3 ++-
app/src/pages/cms/index.astro | 5 +++--
app/src/pages/index.astro | 17 ++++++++++-------
app/src/pages/plugins/[slug].astro | 9 +++++----
app/src/styles/global.css | 15 ++++++++++-----
12 files changed, 65 insertions(+), 48 deletions(-)
diff --git a/app/package-lock.json b/app/package-lock.json
index 1c776b8..c19cfb4 100644
--- a/app/package-lock.json
+++ b/app/package-lock.json
@@ -14,7 +14,8 @@
"emdash": "^0.10.0"
},
"devDependencies": {
- "@astrojs/check": "^0.9.7"
+ "@astrojs/check": "^0.9.7",
+ "@types/node": "^22"
}
},
"node_modules/@astrojs/check": {
@@ -3607,13 +3608,12 @@
}
},
"node_modules/@types/node": {
- "version": "25.9.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.1.tgz",
- "integrity": "sha512-xfrlY7UD5rMJk3ZVJP8BNzS28J36YJg+xp+LPXV1TdWxr8uMH5A860QNxYDGQe/ylDSgjxE52Q9VnO7p75tJxg==",
+ "version": "22.19.19",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz",
+ "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==",
"license": "MIT",
- "optional": true,
"dependencies": {
- "undici-types": ">=7.24.0 <7.24.7"
+ "undici-types": "~6.21.0"
}
},
"node_modules/@types/react": {
@@ -9505,11 +9505,10 @@
"license": "MIT"
},
"node_modules/undici-types": {
- "version": "7.24.6",
- "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz",
- "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==",
- "license": "MIT",
- "optional": true
+ "version": "6.21.0",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+ "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
+ "license": "MIT"
},
"node_modules/unified": {
"version": "11.0.5",
diff --git a/app/package.json b/app/package.json
index 528be6d..ad81a12 100644
--- a/app/package.json
+++ b/app/package.json
@@ -22,6 +22,7 @@
"emdash": "^0.10.0"
},
"devDependencies": {
- "@astrojs/check": "^0.9.7"
+ "@astrojs/check": "^0.9.7",
+ "@types/node": "^22"
}
}
diff --git a/app/seed/seed.json b/app/seed/seed.json
index ab16a1e..7dfbb0f 100644
--- a/app/seed/seed.json
+++ b/app/seed/seed.json
@@ -47,13 +47,13 @@
"defaultValue": "proposed",
"searchable": true,
"options": [
- { "value": "port", "label": "Port — reimplement on target" },
- { "value": "built-in", "label": "Built-in — covered by target core" },
- { "value": "saas", "label": "SaaS — replaced by external service" },
- { "value": "drop", "label": "Drop — not needed" },
- { "value": "gated", "label": "Gated — pending decision" },
- { "value": "done", "label": "Done — ported and shipped" },
- { "value": "proposed", "label": "Proposed — newly submitted" }
+ { "value": "port", "label": "Port" },
+ { "value": "built-in", "label": "Built-in" },
+ { "value": "saas", "label": "SaaS" },
+ { "value": "drop", "label": "Drop" },
+ { "value": "gated", "label": "Gated" },
+ { "value": "done", "label": "Done" },
+ { "value": "proposed", "label": "Proposed" }
]
},
{ "slug": "source_repo_url", "label": "Source repo URL", "type": "string" },
diff --git a/app/src/components/PluginCard.astro b/app/src/components/PluginCard.astro
index 3ff81be..3193a54 100644
--- a/app/src/components/PluginCard.astro
+++ b/app/src/components/PluginCard.astro
@@ -20,7 +20,7 @@ const d = entry.data;
- {d.source_cms && {d.source_cms} → {d.target_cms ?? "—"}}
+ {d.source_cms && {d.source_cms}{d.target_cms ? ` → ${d.target_cms}` : ""}}
{d.purpose && {d.purpose}
}
diff --git a/app/src/components/StatusBadge.astro b/app/src/components/StatusBadge.astro
index e26c833..5affb4c 100644
--- a/app/src/components/StatusBadge.astro
+++ b/app/src/components/StatusBadge.astro
@@ -8,4 +8,4 @@ const value = (status ?? "proposed").toLowerCase();
const label = STATUS_LABELS[value] ?? value;
const desc = STATUSES.find((s) => s.value === value)?.desc;
---
-Status: {label}
+Status: {label}{desc ? ` — ${desc}` : ""}. {label}
diff --git a/app/src/components/StatusLegend.astro b/app/src/components/StatusLegend.astro
index d6860da..46371ec 100644
--- a/app/src/components/StatusLegend.astro
+++ b/app/src/components/StatusLegend.astro
@@ -1,9 +1,13 @@
---
import StatusBadge from "./StatusBadge.astro";
import { STATUSES } from "../lib/statuses";
+interface Props {
+ items?: typeof STATUSES;
+}
+const { items = STATUSES } = Astro.props;
---
- {STATUSES.map((i) => (
+ {items.map((i) => (
{i.desc}
))}
diff --git a/app/src/lib/statuses.ts b/app/src/lib/statuses.ts
index 7f70f16..b5c0008 100644
--- a/app/src/lib/statuses.ts
+++ b/app/src/lib/statuses.ts
@@ -1,3 +1,5 @@
+export const PLUGIN_FETCH_CAP = 10000;
+
export interface StatusDef {
value: string;
label: string;
@@ -5,13 +7,13 @@ export interface StatusDef {
}
export const STATUSES: StatusDef[] = [
- { value: "port", label: "Port", desc: "must be reimplemented on the target CMS" },
- { value: "built-in", label: "Built-in", desc: "covered by target CMS core / framework" },
- { value: "saas", label: "SaaS", desc: "replaced by an external service" },
- { value: "drop", label: "Drop", desc: "not needed on the target CMS" },
- { value: "gated", label: "Gated", desc: "fate depends on an unresolved decision" },
- { value: "done", label: "Done", desc: "ported and shipped" },
- { value: "proposed", label: "Proposed", desc: "submitted for cataloging" },
+ { value: "port", label: "Port", desc: "Reimplemented as a new Emdash plugin" },
+ { value: "built-in", label: "Built-in", desc: "Already covered by Emdash core" },
+ { value: "saas", label: "SaaS", desc: "Replaced by an external hosted service" },
+ { value: "drop", label: "Drop", desc: "Not needed after migration" },
+ { value: "gated", label: "Gated", desc: "Blocked on an open decision" },
+ { value: "done", label: "Done", desc: "Ported and shipped" },
+ { value: "proposed", label: "Proposed", desc: "Newly added, not yet classified" },
];
export const STATUS_LABELS: Record = Object.fromEntries(
diff --git a/app/src/pages/cms/[slug].astro b/app/src/pages/cms/[slug].astro
index 516530f..f736eee 100644
--- a/app/src/pages/cms/[slug].astro
+++ b/app/src/pages/cms/[slug].astro
@@ -2,6 +2,7 @@
import { getEmDashCollection, getEmDashEntry, decodeSlug } from "emdash";
import Base from "../../layouts/Base.astro";
import PluginCard from "../../components/PluginCard.astro";
+import { PLUGIN_FETCH_CAP } from "../../lib/statuses";
export const prerender = false;
@@ -14,7 +15,6 @@ if (!slug || !cms) {
}
if (cacheHint) Astro.cache.set(cacheHint);
-const PLUGIN_FETCH_CAP = 10000;
const { entries: all } = cms
? await getEmDashCollection("plugins", { orderBy: { title: "asc" }, limit: PLUGIN_FETCH_CAP })
: { entries: [] };
@@ -36,6 +36,7 @@ const targetingHere = all.filter((p) => p.data.target_cms === cmsName);
{cms.data.description && {cms.data.description}
}
Plugins from this CMS ({fromHere.length})
+ Filter these in the catalog
{fromHere.length === 0 ? (
No plugins cataloged for this CMS yet.
) : (
diff --git a/app/src/pages/cms/index.astro b/app/src/pages/cms/index.astro
index 3654ebd..ec01823 100644
--- a/app/src/pages/cms/index.astro
+++ b/app/src/pages/cms/index.astro
@@ -1,6 +1,7 @@
---
import { getEmDashCollection } from "emdash";
import Base from "../../layouts/Base.astro";
+import { PLUGIN_FETCH_CAP } from "../../lib/statuses";
export const prerender = false;
@@ -9,7 +10,6 @@ const { entries: cmses, cacheHint } = await getEmDashCollection("cmses", {
});
Astro.cache.set(cacheHint);
-const PLUGIN_FETCH_CAP = 10000;
const { entries: plugins } = await getEmDashCollection("plugins", { limit: PLUGIN_FETCH_CAP });
if (plugins.length >= PLUGIN_FETCH_CAP) console.warn("[cms] plugin fetch hit cap", PLUGIN_FETCH_CAP, "- counts/lists may be truncated");
const countBySource = new Map();
@@ -23,11 +23,12 @@ for (const p of plugins) {
---
By CMS
+ CMSes
{cmses.map((c) => (
-
-
{countBySource.get(c.data.title) ?? 0} from · {countByTarget.get(c.data.title) ?? 0} targeting
+ {countBySource.get(c.data.title) ?? 0} plugins from · {countByTarget.get(c.data.title) ?? 0} targeting
{c.data.description && {c.data.description}
}
))}
diff --git a/app/src/pages/index.astro b/app/src/pages/index.astro
index 78153db..df2decd 100644
--- a/app/src/pages/index.astro
+++ b/app/src/pages/index.astro
@@ -3,13 +3,15 @@ import { getEmDashCollection } from "emdash";
import Base from "../layouts/Base.astro";
import PluginCard from "../components/PluginCard.astro";
import StatusLegend from "../components/StatusLegend.astro";
-import { STATUSES } from "../lib/statuses";
+import { STATUSES, PLUGIN_FETCH_CAP } from "../lib/statuses";
export const prerender = false;
const { entries: plugins, cacheHint } = await getEmDashCollection("plugins", {
orderBy: { title: "asc" },
+ limit: PLUGIN_FETCH_CAP,
});
+if (plugins.length >= PLUGIN_FETCH_CAP) console.warn("[index] plugin fetch hit cap", PLUGIN_FETCH_CAP, "- list may be truncated");
Astro.cache.set(cacheHint);
const url = new URL(Astro.request.url);
@@ -29,33 +31,34 @@ const filtered = plugins.filter((p) => {
return true;
});
---
-
+
Plugins
- {(q || statusFilter || sourceFilter) ? `${filtered.length} of ${plugins.length} match.` : `${plugins.length} entries cataloged. Filter by status, source CMS, or search by name.`}
+ {(q || statusFilter || sourceFilter) ? `${filtered.length} of ${plugins.length} match.` : `${plugins.length} WordPress plugins cataloged with how each maps onto Emdash. Filter by status, source CMS, or search by name.`}
-
+
{filtered.length === 0 ? (
No plugins match {q ? `“${q}”` : "the current filters"}. Clear filters
) : (
+ Plugin results
{filtered.map((entry) => )}
diff --git a/app/src/pages/plugins/[slug].astro b/app/src/pages/plugins/[slug].astro
index f1591f1..20d06d7 100644
--- a/app/src/pages/plugins/[slug].astro
+++ b/app/src/pages/plugins/[slug].astro
@@ -18,8 +18,9 @@ if (cacheHint) Astro.cache.set(cacheHint);
const d = entry?.data;
const { entries: cmses } = entry ? await getEmDashCollection("cmses", {}) : { entries: [] };
-const cmsSlugByTitle = new Map(cmses.map((c) => [c.data.title as string, c.id]));
-const sourceCmsSlug = d?.source_cms ? cmsSlugByTitle.get(d.source_cms) : undefined;
+const norm = (s: string) => s.trim().toLowerCase();
+const cmsSlugByTitle = new Map(cmses.map((c) => [norm(c.data.title as string), c.id]));
+const sourceCmsSlug = d?.source_cms ? cmsSlugByTitle.get(norm(d.source_cms)) : undefined;
---
{!entry || !d ? (
@@ -31,7 +32,7 @@ const sourceCmsSlug = d?.source_cms ? cmsSlugByTitle.get(d.source_cms) : undefin
title={d.title}
description={d.purpose ?? undefined}
content={{ collection: "plugins", id: entry.data.id, slug: entry.id }}
- breadcrumbs={[{ name: "Plugins", url: "/" }, ...(sourceCmsSlug ? [{ name: d.source_cms, url: `/cms/${sourceCmsSlug}` }] : []), { name: d.title, url: Astro.url.pathname }]}
+ breadcrumbs={[{ name: "Plugins", url: "/" }, ...(d.source_cms ? [{ name: d.source_cms, url: sourceCmsSlug ? `/cms/${sourceCmsSlug}` : "/cms" }] : []), { name: d.title, url: Astro.url.pathname }]}
>
@@ -40,7 +41,7 @@ const sourceCmsSlug = d?.source_cms ? cmsSlugByTitle.get(d.source_cms) : undefin
{d.source_cms && (<> · {sourceCmsSlug ? {d.source_cms} : {d.source_cms}}>)}
{d.title}
-
+
{d.source_cms && {d.source_cms}{d.target_cms ? ` → ${d.target_cms}` : ""}}
diff --git a/app/src/styles/global.css b/app/src/styles/global.css
index 0e33698..4662315 100644
--- a/app/src/styles/global.css
+++ b/app/src/styles/global.css
@@ -70,7 +70,7 @@ header.site {
background: var(--c-bg);
}
header.site .row {
- display: flex; align-items: center; justify-content: space-between; gap: 1.5rem;
+ display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.5rem 1.5rem;
}
header.site .brand { font-weight: 700; color: var(--c-heading); }
header.site .brand:hover { text-decoration: none; }
@@ -85,6 +85,8 @@ footer.site {
color: var(--c-muted);
font-size: var(--fs-sm);
}
+footer.site .container { display: flex; flex-direction: column; gap: 0.35rem; }
+footer.site p { margin: 0; }
/* status badge */
.badge {
@@ -115,14 +117,15 @@ footer.site {
.toolbar input[type="search"], .toolbar select {
font: inherit;
padding: 0.4rem 0.6rem;
+ min-height: 44px;
border: 1px solid var(--c-border);
border-radius: var(--radius);
background: white;
}
.toolbar input[type="search"] { flex: 1 1 240px; min-width: 0; }
-.toolbar button { font: inherit; padding: 0.4rem 0.9rem; border: 1px solid var(--c-border); border-radius: var(--radius); background: var(--c-link); color: #fff; cursor: pointer; }
+.toolbar button { font: inherit; padding: 0.4rem 0.9rem; min-height: 44px; border: 1px solid var(--c-border); border-radius: var(--radius); background: var(--c-link); color: #fff; cursor: pointer; }
.toolbar button:hover { filter: brightness(1.08); }
-.toolbar a { font: inherit; padding: 0.4rem 0.6rem; border-radius: var(--radius); }
+.toolbar a { font: inherit; padding: 0.4rem 0.6rem; min-height: 44px; display: inline-flex; align-items: center; border-radius: var(--radius); }
.plugin-grid {
list-style: none; padding: 0; margin: 0;
@@ -142,7 +145,7 @@ footer.site {
.plugin-card h3 { margin: 0; font-size: var(--fs-card-title); }
.plugin-card h3 a { color: var(--c-heading); }
.plugin-card .meta { font-size: var(--fs-sm); color: var(--c-muted); display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
-.plugin-card p { margin: 0; font-size: 0.92rem; color: var(--c-body); display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
+.plugin-card p { margin: 0; font-size: 0.92rem; color: var(--c-body); display: -webkit-box; -webkit-line-clamp: 3; line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }
/* detail page */
.detail header { border-bottom: 1px solid var(--c-border); padding-bottom: 1rem; margin-bottom: 1.5rem; }
@@ -151,7 +154,9 @@ footer.site {
.detail dt { color: var(--c-muted); font-size: var(--fs-sm); }
.detail dd { margin: 0; min-width: 0; overflow-wrap: anywhere; }
.detail .lead { font-size: 1.05rem; }
-.detail .source-target { margin-left: .75rem; color: var(--c-muted); }
+.detail .meta { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; }
+.detail .source-target { color: var(--c-muted); }
+main p a { overflow-wrap: anywhere; }
.section-gap { margin-top: 2rem; }
.detail .notes { line-height: 1.65; }
.detail .notes pre { background: var(--c-bg-alt); padding: 0.6rem 0.8rem; border-radius: var(--radius); overflow-x: auto; }