From 600c035ac5642037005d6397afd9583e2e76a3b9 Mon Sep 17 00:00:00 2001 From: Cyber Preacher <72062250+Cyber-preacher@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:19:43 +0400 Subject: [PATCH] Show complete proposal card summaries --- src/components/GlassyRecordCard.css | 52 +++++++++++++++++--- src/components/GlassyRecordCard.tsx | 12 +++-- src/lib/textPreview.ts | 6 ++- tests/unit/glassy-record-card.test.tsx | 68 ++++++++++++++++++++++++++ 4 files changed, 125 insertions(+), 13 deletions(-) create mode 100644 tests/unit/glassy-record-card.test.tsx diff --git a/src/components/GlassyRecordCard.css b/src/components/GlassyRecordCard.css index 28ad14f..887bbad 100644 --- a/src/components/GlassyRecordCard.css +++ b/src/components/GlassyRecordCard.css @@ -88,7 +88,12 @@ display: grid; width: 100%; grid-template-columns: minmax(10rem, 1fr) minmax(36rem, 42rem); + grid-template-areas: + "title aside" + "summary summary" + "association association"; gap: 1rem; + row-gap: 0.4rem; border: 0; background: transparent; padding: 0.82rem 1rem 0.82rem 0; @@ -107,15 +112,14 @@ .glassy-record-card__copy, .glassy-record-card__aside { min-width: 0; - display: flex; } .glassy-record-card__copy { - flex-direction: column; - gap: 0.28rem; + display: contents; } .glassy-record-card__aside { + grid-area: aside; display: grid; grid-template-columns: minmax(15rem, 1fr) minmax(8.75rem, 9.5rem) @@ -126,7 +130,14 @@ } .glassy-record-card__titleRow { + grid-area: title; min-width: 0; + height: 5.4rem; + overflow-x: hidden; + overflow-y: auto; + padding-right: 0.35rem; + scrollbar-color: color-mix(in srgb, var(--muted), transparent 58%) transparent; + scrollbar-width: thin; } .glassy-record-card__metaPill { @@ -153,14 +164,26 @@ } .glassy-record-card__summary { - max-width: 52rem; - min-height: 1.45em; - max-height: 1.45em; - overflow: hidden; + grid-area: summary; + width: 100%; + max-width: none; + height: 4.35em; + overflow-x: hidden; + overflow-y: auto; + overscroll-behavior: contain; + padding-right: 0.35rem; color: var(--muted); font-size: 0.9rem; line-height: 1.45; overflow-wrap: anywhere; + scrollbar-color: color-mix(in srgb, var(--muted), transparent 58%) transparent; + scrollbar-width: thin; +} + +.glassy-record-card__associationSlot { + grid-area: association; + min-width: 0; + min-height: 1.75rem; } .glassy-record-card__association { @@ -210,9 +233,18 @@ @media (max-width: 1180px) { .glassy-record-card__button { grid-template-columns: 1fr; + grid-template-areas: + "title" + "aside" + "summary" + "association"; gap: 0.75rem; } + .glassy-record-card__summary { + height: 8.7em; + } + .glassy-record-card__aside { display: flex; flex-wrap: wrap; @@ -229,6 +261,12 @@ } } +@media (max-width: 560px) { + .glassy-record-card__summary { + height: 14.5em; + } +} + @media (prefers-reduced-motion: reduce) { .glassy-record-card__title, .glassy-record-card__chevron { diff --git a/src/components/GlassyRecordCard.tsx b/src/components/GlassyRecordCard.tsx index ce3aa25..e2f68c3 100644 --- a/src/components/GlassyRecordCard.tsx +++ b/src/components/GlassyRecordCard.tsx @@ -5,7 +5,7 @@ import { ChevronDown } from "lucide-react"; import { Chip } from "@/components/Chip"; import { GlassyCard } from "@/components/GlassyCard"; import { StageChip } from "@/components/StageChip"; -import { proposalSummaryPreview } from "@/lib/textPreview"; +import { normalizePreviewText } from "@/lib/textPreview"; import { cn } from "@/lib/utils"; import type { Stage } from "@/types/stages"; import "./GlassyRecordCard.css"; @@ -40,7 +40,7 @@ export function GlassyRecordCard({ title, }: GlassyRecordCardProps) { const renderedSummary = - typeof summary === "string" ? proposalSummaryPreview(summary) : summary; + typeof summary === "string" ? normalizePreviewText(summary) : summary; return ( {renderedSummary} {association ? ( - - {association} - + + + {association} + + ) : null} diff --git a/src/lib/textPreview.ts b/src/lib/textPreview.ts index 433afe2..1185c1e 100644 --- a/src/lib/textPreview.ts +++ b/src/lib/textPreview.ts @@ -1,10 +1,14 @@ export const PROPOSAL_SUMMARY_PREVIEW_MAX = 180; +export function normalizePreviewText(value: string): string { + return value.replace(/\s+/g, " ").trim(); +} + export function proposalSummaryPreview( value: string, maxLength = PROPOSAL_SUMMARY_PREVIEW_MAX, ): string { - const text = value.replace(/\s+/g, " ").trim(); + const text = normalizePreviewText(value); if (text.length <= maxLength) return text; const hardCut = text.slice(0, maxLength + 1); diff --git a/tests/unit/glassy-record-card.test.tsx b/tests/unit/glassy-record-card.test.tsx new file mode 100644 index 0000000..064978f --- /dev/null +++ b/tests/unit/glassy-record-card.test.tsx @@ -0,0 +1,68 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { test } from "@rstest/core"; +import { renderToStaticMarkup } from "react-dom/server"; +import { MemoryRouter } from "react-router"; + +import { GlassyRecordCard } from "../../src/components/GlassyRecordCard"; + +test("glassy record cards retain the complete normalized summary", () => { + const summary = [ + "Build and launch Ember, an independent Humanode-native chain explorer", + "indexing both the Humanode chain and its governance history so every", + "reader can inspect the complete public record without a shortened card.", + ].join(" \n "); + + const html = renderToStaticMarkup( + + undefined} + stage="pool" + summary={summary} + title="Ember - a native Humanode Explorer" + > +

Proposal details

+
+
, + ); + + assert.match( + html, + /indexing both the Humanode chain and its governance history/, + ); + assert.match(html, /without a shortened card\./); + assert.doesNotMatch(html, /\s{2,}/); +}); + +test("glassy record cards reserve a stable scrollable summary region", () => { + const css = readFileSync( + join(process.cwd(), "src/components/GlassyRecordCard.css"), + "utf8", + ); + const summaryRule = css.match( + /\.glassy-record-card__summary\s*\{([^}]*)\}/, + )?.[1]; + + assert.ok(summaryRule); + assert.match(summaryRule, /height:\s*4\.35em/); + assert.match(summaryRule, /overflow-y:\s*auto/); + assert.match(summaryRule, /grid-area:\s*summary/); + assert.doesNotMatch(summaryRule, /max-height:\s*1\.45em/); + assert.doesNotMatch(summaryRule, /overflow:\s*hidden/); + + const buttonRule = css.match( + /\.glassy-record-card__button\s*\{([^}]*)\}/, + )?.[1]; + assert.ok(buttonRule); + assert.match(buttonRule, /"summary summary"/); + assert.match(buttonRule, /"association association"/); + + const titleRule = css.match( + /\.glassy-record-card__titleRow\s*\{([^}]*)\}/, + )?.[1]; + assert.ok(titleRule); + assert.match(titleRule, /height:\s*5\.4rem/); + assert.match(titleRule, /overflow-y:\s*auto/); +});