From 31af563b33e363bef5588c9bc374b7a1f93e8b2e Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 31 Oct 2026 18:45:29 +0100 Subject: [PATCH 1/2] fix: fade code blocks that continue beyond the commit preview --- KNOWLEDGE_BASE.md | 2 +- resources/js/components/code-block.tsx | 9 +++- resources/js/components/commit-body.tsx | 13 ++++- tests/Unit/FoldedCommitBody.test.tsx | 72 +++++++++++++++++++++++++ 4 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 tests/Unit/FoldedCommitBody.test.tsx diff --git a/KNOWLEDGE_BASE.md b/KNOWLEDGE_BASE.md index 4f575f267..c0b15c83f 100644 --- a/KNOWLEDGE_BASE.md +++ b/KNOWLEDGE_BASE.md @@ -196,7 +196,7 @@ Livestream history: the team agreed to build semantic search on [day two at 2:57 ### Writing commits -A **commit** is a post on /dev/null, not a Git commit in a repository. Open the composer or press **c**, write your update, and choose **Commit** or press **⌘/Ctrl+Enter**. Text-only commits accept 1–4,000 characters; as you type, the ring around your avatar in the composer fills toward that limit. On the web and in the app, an image attachment makes the text optional (terminal commits still need text), but the 4,000-character maximum still applies. Runs of blank lines outside code collapse to one before the length is checked. Lists fold posts longer than 280 characters or 8 lines behind **Show more**, without cutting through code. Commits can be created and deleted, but not edited after publication. There are no plans to add commit editing, and it will never be added: to change a commit, delete it and post a new one. +A **commit** is a post on /dev/null, not a Git commit in a repository. Open the composer or press **c**, write your update, and choose **Commit** or press **⌘/Ctrl+Enter**. Text-only commits accept 1–4,000 characters; as you type, the ring around your avatar in the composer fills toward that limit. On the web and in the app, an image attachment makes the text optional (terminal commits still need text), but the 4,000-character maximum still applies. Runs of blank lines outside code collapse to one before the length is checked. Lists fold posts longer than 280 characters or 8 lines behind **Show more**. When a commit starts with a code block too long to fit, its first lines fade out to show that the code continues. **Show more** reveals the full snippet, and the copy control copies the whole block even while folded. Commits can be created and deleted, but not edited after publication. There are no plans to add commit editing, and it will never be added: to change a commit, delete it and post a new one. Commits, replies, quotes, scheduling, terminal submissions, and app (signed API) commits share a limit of 10 attempts per minute per user. Posts go through [moderation](#moderation), so a newly submitted commit may be pending rather than public immediately. diff --git a/resources/js/components/code-block.tsx b/resources/js/components/code-block.tsx index e355c78f2..265a58315 100644 --- a/resources/js/components/code-block.tsx +++ b/resources/js/components/code-block.tsx @@ -62,7 +62,14 @@ export function CodeBlock({ }; return ( -
+
{(language || isCopyable) && (
segment.type === 'block'); let index = 0; - return previewSegments.map((segment) => + const preview = previewSegments.map((segment) => segment.type === 'block' ? { ...segment, fullHtml: blocks[index++]?.html } : segment, ); + const last = preview.at(-1); + const previous = preview.at(-2); + + // A faded block already shows that it continues; keep the ellipsis for completed blocks and prose. + return last?.type === 'text' && + last.text.trim() === '…' && + previous?.type === 'block' && + previous.fullHtml !== undefined && + previous.fullHtml !== previous.html + ? preview.slice(0, -1) + : preview; } export function CommitBodyExcerpt({ diff --git a/tests/Unit/FoldedCommitBody.test.tsx b/tests/Unit/FoldedCommitBody.test.tsx new file mode 100644 index 000000000..e49a51d48 --- /dev/null +++ b/tests/Unit/FoldedCommitBody.test.tsx @@ -0,0 +1,72 @@ +import assert from 'node:assert/strict'; +import { App } from '@inertiajs/react'; +import type { ComponentProps } from 'react'; +import { renderToStaticMarkup } from 'react-dom/server'; +import { test } from 'vite-plus/test'; +import { FoldedCommitBody } from '../../resources/js/components/commit-body'; +import type { BodySegment } from '../../resources/js/types'; + +function renderBody( + segments: BodySegment[], + previewSegments: BodySegment[] | null, +) { + return renderToStaticMarkup( + ( + + )} + initialPage={ + { + component: 'Preview', + url: '/', + version: null, + props: { + errors: {}, + auth: { user: null, pasteis: null, isAdmin: false }, + }, + clearHistory: false, + encryptHistory: false, + } as unknown as ComponentProps['initialPage'] + } + />, + ); +} + +const block = (html: string): BodySegment => ({ + type: 'block', + language: 'bash', + filename: null, + html, +}); + +test('a shortened code preview uses its continuation cue without a loose ellipsis', () => { + const markup = renderBody( + [block('echo before\necho after')], + [block('echo before'), { type: 'text', text: '\n…' }], + ); + assert.ok(markup.includes('echo before')); + assert.ok(!markup.includes('echo after')); + assert.ok(!markup.includes('…')); + assert.ok(markup.includes('Show more')); + assert.ok(markup.includes('aria-label="Copy code"')); +}); + +test('a completed code block keeps the ellipsis when later prose is hidden', () => { + const markup = renderBody( + [block('echo ready'), { type: 'text', text: '\nMore explanation' }], + [block('echo ready'), { type: 'text', text: '\n…' }], + ); + assert.ok(markup.includes('echo ready')); + assert.ok(markup.includes('…')); + assert.ok(markup.includes('Show more')); +}); + +test('a complete body shows all its code without an expansion control', () => { + const markup = renderBody([block('echo before\necho after')], null); + assert.ok(markup.includes('echo after')); + assert.ok(!markup.includes('Show more')); +}); From 81e50624c61024979aa92b461774544aefa926aa Mon Sep 17 00:00:00 2001 From: Mark Date: Sat, 31 Oct 2026 19:04:06 +0100 Subject: [PATCH 2/2] fix: keep code preview scrollbars outside the fade --- resources/js/components/code-block.tsx | 39 +++++++++++++++----------- tests/Unit/FoldedCommitBody.test.tsx | 3 ++ 2 files changed, 26 insertions(+), 16 deletions(-) diff --git a/resources/js/components/code-block.tsx b/resources/js/components/code-block.tsx index 265a58315..210345235 100644 --- a/resources/js/components/code-block.tsx +++ b/resources/js/components/code-block.tsx @@ -64,10 +64,10 @@ export function CodeBlock({ return (
{(language || isCopyable) && ( @@ -127,20 +127,27 @@ export function CodeBlock({ )}
)} - {/* The server escapes the code while highlighting it. */} -
+            
+ {/* The server escapes the code while highlighting it. */} +
+            
); } diff --git a/tests/Unit/FoldedCommitBody.test.tsx b/tests/Unit/FoldedCommitBody.test.tsx index e49a51d48..bd001cd9d 100644 --- a/tests/Unit/FoldedCommitBody.test.tsx +++ b/tests/Unit/FoldedCommitBody.test.tsx @@ -48,6 +48,7 @@ test('a shortened code preview uses its continuation cue without a loose ellipsi [block('echo before\necho after')], [block('echo before'), { type: 'text', text: '\n…' }], ); + assert.ok(markup.includes('mask-b-from-')); assert.ok(markup.includes('echo before')); assert.ok(!markup.includes('echo after')); assert.ok(!markup.includes('…')); @@ -60,6 +61,7 @@ test('a completed code block keeps the ellipsis when later prose is hidden', () [block('echo ready'), { type: 'text', text: '\nMore explanation' }], [block('echo ready'), { type: 'text', text: '\n…' }], ); + assert.ok(!markup.includes('mask-b-from-')); assert.ok(markup.includes('echo ready')); assert.ok(markup.includes('…')); assert.ok(markup.includes('Show more')); @@ -67,6 +69,7 @@ test('a completed code block keeps the ellipsis when later prose is hidden', () test('a complete body shows all its code without an expansion control', () => { const markup = renderBody([block('echo before\necho after')], null); + assert.ok(!markup.includes('mask-b-from-')); assert.ok(markup.includes('echo after')); assert.ok(!markup.includes('Show more')); });