Display user story description as multiline in specs pages

- GherkinHighlighter: Render user story lines (En tant que, Je peux,
  Afin de) in a violet card at the top of feature details
- FeatureView: Remove duplicate description display (now in GherkinHighlighter)
- SpecsPage: Display feature cards in single column layout with
  multiline user story formatting

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-01-18 19:43:34 +01:00
parent 9843936212
commit 4ae96b0e94
3 changed files with 46 additions and 15 deletions
+1 -6
View File
@@ -82,14 +82,9 @@ export function FeatureView({ feature, onBack, onSelectScreen, onSelectStory }:
)}
</div>
<h1 className="text-2xl font-semibold mb-2">
<h1 className="text-2xl font-semibold">
{feature.name.replace(/^US-\d+\s*/, '')}
</h1>
{feature.description && (
<p className="text-muted-foreground max-w-3xl">
{feature.description}
</p>
)}
</div>
<div className="px-4 sm:px-8 py-6">
+27 -4
View File
@@ -121,8 +121,8 @@ export function GherkinHighlighter({ content, scenarioResults = [] }: GherkinHig
</Button>
</div>
{/* Scenario/Background Blocks */}
{blocks.filter(b => b.type !== 'header').map((block, blockIndex) => (
{/* All Blocks including header */}
{blocks.map((block, blockIndex) => (
<BlockRenderer
key={blockIndex}
block={block}
@@ -206,8 +206,31 @@ interface BlockRendererProps {
function BlockRenderer({ block, isCollapsed, onToggle, showDefinitions }: BlockRendererProps) {
if (block.type === 'header') {
// Header is now handled separately in the main component
return null;
// Extract user story lines (En tant que, Je peux/Je veux, Afin de)
const userStoryLines = block.lines.filter(line => {
const trimmed = line.trim();
return trimmed.startsWith('En tant qu') ||
trimmed.startsWith('Je peux') ||
trimmed.startsWith('Je veux') ||
trimmed.startsWith('Et ') ||
trimmed.startsWith('Afin ');
});
if (userStoryLines.length === 0) return null;
return (
<Card className="border-l-4 border-l-violet-500 bg-violet-50/50 dark:bg-violet-950/20">
<CardContent className="p-3">
<div className="space-y-0.5">
{userStoryLines.map((line, index) => (
<div key={index} className="text-sm text-foreground">
{line.trim()}
</div>
))}
</div>
</CardContent>
</Card>
);
}
const restLines = block.lines.slice(1);
+18 -5
View File
@@ -4,7 +4,7 @@ import { categoryLabels, categoryColors, priorityLabels, priorityColors, getStor
import { getTestStatus, getTestSummary } from '../../data/testResults';
import { FeatureView } from './FeatureView';
import { FeatureFilter } from './FeatureFilter';
import { Card, CardHeader, CardTitle, CardContent, CardDescription } from '../ui/card';
import { Card, CardHeader, CardTitle, CardContent } from '../ui/card';
import { Button } from '../ui/button';
import { ArrowLeft, FileText, Monitor, CheckCircle2, XCircle, AlertCircle, ExternalLink } from 'lucide-react';
import type { ParsedFeature } from '../../types/gherkin';
@@ -146,7 +146,7 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS
</span>
</div>
<div className="grid gap-3 sm:gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
<div className="flex flex-col gap-3 sm:gap-4">
{features.map(feature => (
<FeatureCard
key={feature.id}
@@ -168,6 +168,15 @@ export function SpecsPage({ selectedFeatureId, onBack, onSelectScreen, onSelectS
);
}
// Split user story description into separate lines
function formatUserStory(description: string): string[] {
// Split on user story keywords while keeping the keywords
return description
.split(/(?=En tant qu|Je peux|Je veux|Et |Afin d)/)
.map(s => s.trim())
.filter(Boolean);
}
interface FeatureCardProps {
feature: ParsedFeature;
onClick: () => void;
@@ -230,9 +239,13 @@ function FeatureCard({ feature, onClick }: FeatureCardProps) {
</CardHeader>
<CardContent>
{feature.description && (
<CardDescription className="line-clamp-2 text-sm mb-3">
{feature.description}
</CardDescription>
<div className="text-sm text-muted-foreground mb-3 space-y-1">
{formatUserStory(feature.description).map((line, i) => (
<div key={i} className="leading-snug">
{line}
</div>
))}
</div>
)}
<div className="flex items-center gap-4 text-xs text-muted-foreground">
<span className="flex items-center gap-1">