gs4f2wcln: Fit long text-only posts to the slide and add Read more and more/less for long post text on mobile

This commit is contained in:
2026-10-20 10:12:01 -06:00
parent d653e8e75d
commit 6af225afa5
6 changed files with 477 additions and 28 deletions
+55 -4
View File
@@ -7,6 +7,7 @@ import { pickCurrent, isDoubleTap, isTapTarget, featuredSlideIndex, railPercent
import { FIXTURE_DISCOVER } from "@/dev/fixtures";
import { CompactLayoutContext, isCompactWidth } from "@/components/layout-context";
import { toCardPosts } from "@/lib/feed-model";
import { pickQuoteFit, QUOTE_SIZES } from "@/components/SlideText";
const NOW = Date.parse("2026-10-19T12:00:00Z");
const item = (id: string, authorId: string, mediaItems: unknown[] = []) => ({
@@ -50,10 +51,10 @@ describe("mobile feed", () => {
it("renders one slide per post, text as a quote and media under the fades", () => {
const html = render({});
expect(html.match(/data-testid="feed-slide"/g)).toHaveLength(3);
expect(html).toContain('class="slide-quote">text t1</p>');
expect(html).toMatch(/class="slide-quote" data-fit-size="31" style="font-size:31px">text t1<\/p>/);
expect(html).toMatch(/slide slide-on-media[^"]*"[^>]*aria-label="Post by Author i1"/);
expect(html).toContain("slide-scrim-top");
expect(html).toContain('class="slide-text">text i1</p>');
expect(html).toMatch(/class="slide-text"[^>]*>text i1<\/p>/);
expect(html).toContain('aria-label="Play video"');
// The quote slide shows its text once, as the quote.
expect(html.match(/text t1/g)).toHaveLength(1);
@@ -61,7 +62,7 @@ describe("mobile feed", () => {
it("marks the first slide as the one on screen", () => {
const html = render({});
expect(html).toMatch(/class="slide slide-on-text is-cur"/);
expect(html).toMatch(/class="slide slide-on-text is-cur slide-own"/);
expect(html).toContain('data-current="0"');
});
@@ -101,7 +102,7 @@ describe("mobile stylesheet", () => {
});
it("uses a 31px serif quote with a crimson mark, and eases media from 104%", () => {
expect(css).toMatch(/\.slide-quote \{[^}]*font-size: 31px/);
expect(css).toMatch(/\.slide-quote::before \{[^}]*color: var\(--accent\)/);
expect(css).toMatch(/\.slide-quote-mark \{[^}]*color: var\(--accent\)/);
expect(css).toMatch(/transform: scale\(1\.04\)/);
expect(css).toMatch(/\.is-cur \.slide-art \{ transform: scale\(1\); \}/);
});
@@ -206,3 +207,53 @@ describe("videoFit", () => {
expect(videoFit(0, 0)).toBe("cover");
});
});
describe("long post text", () => {
it("picks the largest quote size that fits, measured, not counted", () => {
const fitsBelow = (max: number) => (size: number) => size <= max;
expect(pickQuoteFit(fitsBelow(40), () => 0)).toEqual({ size: 31, lines: 0 });
expect(pickQuoteFit(fitsBelow(26), () => 0)).toEqual({ size: 26, lines: 0 });
expect(pickQuoteFit(fitsBelow(22), () => 0)).toEqual({ size: 22, lines: 0 });
expect(pickQuoteFit(fitsBelow(19), () => 0)).toEqual({ size: 19, lines: 0 });
expect([...QUOTE_SIZES]).toEqual([31, 26, 22, 19]);
});
it("clamps at 19px to the whole lines that fit when nothing fits", () => {
const tried: number[] = [];
const fit = pickQuoteFit((s) => (tried.push(s), false), () => 17.8);
expect(tried).toEqual([31, 26, 22, 19]);
expect(fit).toEqual({ size: 19, lines: 17 });
expect(pickQuoteFit(() => false, () => 0.2)).toEqual({ size: 19, lines: 1 });
});
it("shows no button on short text until a measurement says it is long", () => {
const html = render({});
expect(html).not.toContain("Read more");
expect(html).not.toMatch(/>more<\/button>/);
expect(html).not.toContain("slide-reader");
});
it("keeps taps on open text from counting toward a double-tap like", () => {
const el = { closest: (sel: string) => (sel.includes("[data-expanded-text]") ? {} : null) } as unknown as Element;
expect(isTapTarget(el)).toBe(false);
});
});
describe("long post text stylesheet", () => {
const css = readFileSync("src/styles/mobile.css", "utf8");
it("fits the quote between the top bar and the author block", () => {
expect(css).toMatch(/\.slide-quote-w \{[^}]*top: calc\(72px[^}]*bottom: 181px[^}]*overflow: hidden/);
});
it("makes the buttons at least 44px tall", () => {
expect(css).toMatch(/\.slide-more \{[^}]*min-height: 44px/);
});
it("holds the reel still while text is open, and the open text scrolls", () => {
expect(css).toMatch(/\.reel\[data-locked="true"\] \{ overflow-y: hidden; \}/);
expect(css).toMatch(/\.slide-reader-scroll \{[^}]*overflow-y: auto/);
expect(css).toMatch(/\.slide-text\.is-open \{[^}]*overflow-y: auto/);
expect(css).toMatch(/\.slide-text-open \.slide-scrim \{[^}]*var\(--media-scrim\)/);
});
it("drops the expand animation under reduced motion", () => {
expect(css).toMatch(/prefers-reduced-motion: reduce\) \{[^}]*\.slide-reader,\s*\.slide-text\.is-open,/);
});
});