feat: add display density slider to synthesis detail view

4 levels: Compact (title+date only) → 2 lines → half summary → full.
Slider in a gray bar above sections. Card spacing tightens at level 1.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
master
oabrivard 3 months ago
parent 6b84c335d0
commit 2650356c01

@ -98,6 +98,8 @@ const fr = {
'synthesis.deleteError': 'Erreur lors de la suppression.', 'synthesis.deleteError': 'Erreur lors de la suppression.',
'synthesis.backToHome': 'Retour a l\'accueil', 'synthesis.backToHome': 'Retour a l\'accueil',
'synthesis.noSections': 'Aucune section trouvee dans cette synthese.', 'synthesis.noSections': 'Aucune section trouvee dans cette synthese.',
'synthesis.displayCompact': 'Compact',
'synthesis.displayFull': 'Complet',
// Synthesis - Email // Synthesis - Email
'synthesis.email.title': 'Envoyer par email', 'synthesis.email.title': 'Envoyer par email',

@ -17,8 +17,23 @@ import type { Synthesis, NewsItem as NewsItemType, ArticleHistoryEntry } from '~
import { extractWeekNumber, formatDateLong } from '~/utils/dates'; import { extractWeekNumber, formatDateLong } from '~/utils/dates';
import LoadingSpinner from '~/components/ui/LoadingSpinner'; import LoadingSpinner from '~/components/ui/LoadingSpinner';
/** Truncate text to approximately N lines (assuming ~80 chars per line). */
function truncateSummary(text: string, level: number): string {
if (level >= 4 || !text) return text;
if (level === 1) return '';
if (level === 2) {
// ~2 lines ≈ 160 chars
if (text.length <= 160) return text;
return text.slice(0, 157) + '...';
}
// level 3: half
const half = Math.ceil(text.length / 2);
if (text.length <= half + 3) return text;
return text.slice(0, half) + '...';
}
/** Renders a single news article with its title (linked) and summary. */ /** Renders a single news article with its title (linked) and summary. */
const NewsItemCard: Component<{ item: NewsItemType }> = (props) => { const NewsItemCard: Component<{ item: NewsItemType; displayLevel: number }> = (props) => {
return ( return (
<div class="bg-white rounded-lg shadow-sm border border-gray-100 p-6 hover:shadow-md transition-shadow"> <div class="bg-white rounded-lg shadow-sm border border-gray-100 p-6 hover:shadow-md transition-shadow">
<h3 class="text-lg font-semibold text-indigo-700 mb-2"> <h3 class="text-lg font-semibold text-indigo-700 mb-2">
@ -35,24 +50,26 @@ const NewsItemCard: Component<{ item: NewsItemType }> = (props) => {
<Show when={props.item.date}> <Show when={props.item.date}>
<p class="text-xs text-gray-400 mb-1">{props.item.date}</p> <p class="text-xs text-gray-400 mb-1">{props.item.date}</p>
</Show> </Show>
<p class="text-gray-700 leading-relaxed text-sm"> <Show when={props.displayLevel > 1 && props.item.summary}>
{props.item.summary} <p class={`text-gray-700 leading-relaxed text-sm ${props.displayLevel < 4 ? 'text-gray-500' : ''}`}>
{truncateSummary(props.item.summary, props.displayLevel)}
</p> </p>
</Show>
</div> </div>
); );
}; };
/** Renders a titled category section containing a list of {@link NewsItemCard}s. */ /** Renders a titled category section containing a list of {@link NewsItemCard}s. */
const Section: Component<{ title: string; items: NewsItemType[] }> = (props) => { const Section: Component<{ title: string; items: NewsItemType[]; displayLevel: number }> = (props) => {
return ( return (
<Show when={props.items && props.items.length > 0}> <Show when={props.items && props.items.length > 0}>
<div class="mb-10"> <div class="mb-10">
<h2 class="text-2xl font-bold text-gray-900 mb-6 border-b pb-2"> <h2 class="text-2xl font-bold text-gray-900 mb-6 border-b pb-2">
{props.title} {props.title}
</h2> </h2>
<div class="space-y-6"> <div class={props.displayLevel === 1 ? 'space-y-2' : 'space-y-6'}>
<For each={props.items}> <For each={props.items}>
{(item) => <NewsItemCard item={item} />} {(item) => <NewsItemCard item={item} displayLevel={props.displayLevel} />}
</For> </For>
</div> </div>
</div> </div>
@ -94,6 +111,9 @@ const SynthesisDetail: Component = () => {
if (t) clearTimeout(t); if (t) clearTimeout(t);
}); });
// Display level for article cards (1=title only, 2=+2 lines, 3=+half, 4=full)
const [displayLevel, setDisplayLevel] = createSignal(4);
// Export state // Export state
const [exportingMarkdown, setExportingMarkdown] = createSignal(false); const [exportingMarkdown, setExportingMarkdown] = createSignal(false);
const [exportingPdf, setExportingPdf] = createSignal(false); const [exportingPdf, setExportingPdf] = createSignal(false);
@ -402,6 +422,21 @@ const SynthesisDetail: Component = () => {
</div> </div>
{/* Sections */} {/* Sections */}
{/* Display level slider */}
<div class="mb-8 flex items-center gap-4 bg-gray-50 rounded-lg p-4 border border-gray-200">
<span class="text-xs text-gray-500 whitespace-nowrap">{t('synthesis.displayCompact')}</span>
<input
type="range"
min="1"
max="4"
step="1"
class="flex-1 h-2 bg-gray-200 rounded-lg appearance-none cursor-pointer accent-indigo-600"
value={displayLevel()}
onInput={(e) => setDisplayLevel(parseInt(e.currentTarget.value) || 4)}
/>
<span class="text-xs text-gray-500 whitespace-nowrap">{t('synthesis.displayFull')}</span>
</div>
<div class="space-y-12"> <div class="space-y-12">
<Show <Show
when={synth().sections && synth().sections.length > 0} when={synth().sections && synth().sections.length > 0}
@ -412,7 +447,7 @@ const SynthesisDetail: Component = () => {
} }
> >
<For each={synth().sections}> <For each={synth().sections}>
{(section) => <Section title={section.title} items={section.items} />} {(section) => <Section title={section.title} items={section.items} displayLevel={displayLevel()} />}
</For> </For>
</Show> </Show>
</div> </div>

Loading…
Cancel
Save