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.title}

- {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) => (
  • {c.data.title}

    -

    {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.`}

    - +
    - {sources.map((s) => ( ))} - {statuses.map((s) => ( ))} - {(q || statusFilter || sourceFilter) && Reset} + {(q || statusFilter || sourceFilter) && Clear all}
    {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; }