diff --git a/docs/UI_DESIGN_SYSTEM.md b/docs/UI_DESIGN_SYSTEM.md index 4b8dd7b..9ba1848 100644 --- a/docs/UI_DESIGN_SYSTEM.md +++ b/docs/UI_DESIGN_SYSTEM.md @@ -15,10 +15,11 @@ when-to-read: prima di creare o modificare qualsiasi componente visivo | `editorial-accent` | #C8705E | Accenti, bottoni attivi, selezioni | | `editorial-muted` | #666666 | Testo disabilitato / secondario | | `editorial-success` | #3A7A65 | Stato positivo | +| `editorial-running` | #C49B2A | Step pipeline in esecuzione (dot + label gialli con `animate-pulse`) | | `editorial-border` | #C2BCB4 | Bordi e separatori | | `editorial-textbox` | #EAE5DE | Background input | -> Warning rimosso — usa `editorial-muted` (#666666) al suo posto. +> `editorial-warning` (#666666) = grigio, per avvisi generici. Per lo stato *in esecuzione* usa `editorial-running` (giallo). **Font:** - `font-display` (Elstob variable) — heading corsivi, label attive nelle barre filtro diff --git a/package-lock.json b/package-lock.json index ed8f40a..e1dd4d6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,6 +17,7 @@ "@tauri-apps/plugin-log": "^2.8.0", "@tauri-apps/plugin-sql": "^2.4.0", "@vitejs/plugin-react": "^5.0.4", + "diff": "^9.0.0", "i18next": "^25.1.0", "lucide-react": "^0.546.0", "motion": "^12.23.24", @@ -33,6 +34,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/diff": "^7.0.2", "@types/node": "^22.14.0", "@types/papaparse": "^5.5.2", "@types/react": "^19.0.0", @@ -2049,6 +2051,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/diff": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@types/diff/-/diff-7.0.2.tgz", + "integrity": "sha512-JSWRMozjFKsGlEjiiKajUjIJVKuKdE3oVy2DNtK+fUo8q82nhFZ2CPQwicAIkXrofahDXrWJ7mjelvZphMS98Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -2486,6 +2495,15 @@ "node": ">=8" } }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", diff --git a/package.json b/package.json index 64b9143..7b29953 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "@tauri-apps/plugin-log": "^2.8.0", "@tauri-apps/plugin-sql": "^2.4.0", "@vitejs/plugin-react": "^5.0.4", + "diff": "^9.0.0", "i18next": "^25.1.0", "lucide-react": "^0.546.0", "motion": "^12.23.24", @@ -43,6 +44,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/diff": "^7.0.2", "@types/node": "^22.14.0", "@types/papaparse": "^5.5.2", "@types/react": "^19.0.0", diff --git a/src/App.tsx b/src/App.tsx index 126650a..9a9ff43 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,6 @@ import { lazy, Suspense, useCallback, useEffect, useRef } from 'react'; import { initLogger } from './utils/logger'; -import { Header, PipelineStrip } from './components/layout'; +import { Header, PipelineSidebar } from './components/layout'; import { ErrorBoundary, ConfirmDialog, PreflightDialog, RunResumeBanner } from './components/common'; import { usePipeline } from './hooks/usePipeline'; import { useProjectAutosave } from './hooks/useProjectAutosave'; @@ -100,7 +100,12 @@ export default function App() { {viewMode === 'document' ? (
- +
diff --git a/src/components/common/ConfirmDialog.tsx b/src/components/common/ConfirmDialog.tsx index ec6794f..307c679 100644 --- a/src/components/common/ConfirmDialog.tsx +++ b/src/components/common/ConfirmDialog.tsx @@ -12,27 +12,24 @@ export function ConfirmDialog() { return ( {open && request && ( -
resolve(false)} > - resolve(false)} - /> - e.stopPropagation()} > {/* Header */}
@@ -92,7 +89,7 @@ export function ConfirmDialog() {
-
+ )}
); diff --git a/src/components/common/CopyButton.tsx b/src/components/common/CopyButton.tsx index be41688..652811a 100644 --- a/src/components/common/CopyButton.tsx +++ b/src/components/common/CopyButton.tsx @@ -2,6 +2,7 @@ import { useState } from 'react'; import { Copy, Check } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { toast } from 'sonner'; +import { Tooltip } from '../ui'; interface CopyButtonProps { text: string; @@ -28,15 +29,16 @@ export function CopyButton({ text }: CopyButtonProps) { const label = copied ? t('pipeline.copied') : t('pipeline.copy'); return ( - + + + ); } diff --git a/src/components/common/StatusIndicator.tsx b/src/components/common/StatusIndicator.tsx index b3de47f..0853c41 100644 --- a/src/components/common/StatusIndicator.tsx +++ b/src/components/common/StatusIndicator.tsx @@ -12,16 +12,16 @@ const STATUS_TONE = { label: 'text-editorial-success', }, processing: { - dot: 'bg-editorial-warning ring-editorial-warning/30 animate-pulse', - label: 'text-editorial-warning', + dot: 'bg-editorial-running ring-editorial-running/30 animate-pulse', + label: 'text-editorial-running', }, error: { dot: 'bg-editorial-accent ring-editorial-accent/30', label: 'text-editorial-accent', }, retrying: { - dot: 'bg-editorial-warning ring-editorial-warning/30 animate-pulse', - label: 'text-editorial-warning', + dot: 'bg-editorial-running ring-editorial-running/30 animate-pulse', + label: 'text-editorial-running', }, idle: { dot: 'bg-editorial-border ring-editorial-border/0', diff --git a/src/components/document/DocumentView.tsx b/src/components/document/DocumentView.tsx index f809aca..72a6535 100644 --- a/src/components/document/DocumentView.tsx +++ b/src/components/document/DocumentView.tsx @@ -1,25 +1,15 @@ import { AlertTriangle, - CheckCheck, ChevronLeft, ChevronRight, - Columns2, FileText, FlaskConical, - Highlighter, - Info, + GitCompare, Languages, - Loader2, Lock, - Minus, Pencil, - PanelLeft, - PanelRight, - Play, - Plus, RotateCcw, ScanLine, - Square, Wand2, Zap, } from 'lucide-react'; @@ -29,40 +19,61 @@ import { useTranslation } from 'react-i18next'; import { usePipelineStore } from '../../stores/pipelineStore'; import { useChunksStore } from '../../stores/chunksStore'; import { useUiStore } from '../../stores/uiStore'; -import { useProjectStore } from '../../stores/projectStore'; -import { usePricingStore } from '../../stores/pricingStore'; import type { TranslationChunk } from '../../types'; import { indexPad } from '../../utils'; -import { estimatePipelineCost } from '../../utils/costEstimate'; -import { CopyButton, MarkdownEditor, ProcessingLine } from '../common'; -import { CostBreakdownPanel } from '../pipeline/CostBadge'; +import { CopyButton, HighlightedText, MarkdownEditor, ProcessingLine } from '../common'; +import { Tooltip } from '../ui'; import { useFocusTrap } from '../../hooks/useFocusTrap'; import { escapeHtml, useGlossaryHighlight } from '../../hooks/useGlossaryHighlight'; +import { usePanelScrollSync } from '../../hooks/usePanelScrollSync'; +import { useStageDiff } from '../../hooks/useStageDiff'; import { highlightSuperscriptMarkersHtml } from '../../utils/footnoteExtractor'; +const NOOP_CHANGE = () => {}; + interface DocumentViewProps { onRetranslateChunk: (chunkId: string) => void; - onRunPipeline?: () => void; - onCancelPipeline?: () => void; - onDryRun?: () => void; +} + +const NAV_STAGE_TONE = { + completed: 'border-editorial-success/40 bg-editorial-success/12 text-editorial-success', + processing: 'border-editorial-running/45 bg-editorial-running/12 text-editorial-running animate-pulse', + retrying: 'border-editorial-running/45 bg-editorial-running/12 text-editorial-running animate-pulse', + error: 'border-editorial-accent/40 bg-editorial-accent/10 text-editorial-accent', + idle: 'border-editorial-border bg-editorial-bg text-editorial-muted/60', +} as const; + +function NavStageIndicator({ status, icon: Icon, onClick, disabled, title }: { + status: string; + icon: LucideIcon; + onClick: () => void; + disabled: boolean; + title: string; +}) { + const tone = (status === 'completed' || status === 'processing' || status === 'error' || status === 'retrying') + ? status as keyof typeof NAV_STAGE_TONE + : 'idle'; + return ( + + + + ); } -export function DocumentView({ - onRetranslateChunk, - onRunPipeline, - onCancelPipeline, - onDryRun, -}: DocumentViewProps) { +export function DocumentView({ onRetranslateChunk }: DocumentViewProps) { const { t } = useTranslation(); const { config } = usePipelineStore(); - const { pipelines, activePipelineId } = useProjectStore(); - const activePipeline = pipelines.find((p) => p.id === activePipelineId); - const pricingOverrides = usePricingStore((s) => s.overrides); const { chunks, - isProcessing, - cancelRequested, updateChunkDraft, updateChunkOriginalText, restoreChunkSourceText, @@ -70,44 +81,32 @@ export function DocumentView({ toggleChunkSourceEditing, } = useChunksStore(); - const completedCount = chunks.filter((c) => c.status === 'completed' || c.translationLocked).length; const { selectedChunkId, setSelectedChunkId, documentLayout, + documentPaneFocus: paneFocus, + syncScrollEnabled, highlightsEnabled, - setHighlightsEnabled, searchQuery, - pipelineMode, - setPipelineMode, pipelineTestChunkCount, setPipelineTestChunkCount, focusedChunkId, focusedIssueQuery, focusedIssueRequestId, + traceStageId, + setTraceStageId, } = useUiStore(); - const effectivePipelineMode = pipelineMode; - const runChunkCount = effectivePipelineMode === 'test' - ? Math.max(1, Math.min(pipelineTestChunkCount, chunks.length || 1)) - : chunks.length; - const canAdjustTestCount = effectivePipelineMode === 'test' && !isProcessing; - const runPanelClass = - effectivePipelineMode === 'test' - ? 'border-editorial-warning/30 bg-editorial-textbox/60 shadow-[0_16px_50px_rgba(26,26,26,0.06)]' - : 'border-editorial-border bg-editorial-bg/90 shadow-[0_16px_50px_rgba(26,26,26,0.05)]'; - const [viewportWidth, setViewportWidth] = useState( typeof window === 'undefined' ? 0 : window.innerWidth, ); - const [paneFocus, setPaneFocus] = useState<'both' | 'source' | 'translation'>('both'); - const [traceStageId, setTraceStageId] = useState(null); - const [showCostPanel, setShowCostPanel] = useState(false); const [selectedStageId, setSelectedStageId] = useState(''); + const [showDiffMode, setShowDiffMode] = useState(false); + const [diffPairKey, setDiffPairKey] = useState(''); - const costEstimate = useMemo( - () => estimatePipelineCost(chunks, config, pricingOverrides), - [chunks, config, pricingOverrides], + const { sourceRef: scrollSourceRef, translationRef: scrollTranslationRef } = usePanelScrollSync( + paneFocus === 'both' && syncScrollEnabled, ); useEffect(() => { @@ -134,7 +133,7 @@ export function DocumentView({ chunks.findIndex((chunk) => chunk.id === selectedChunkId), ); const currentChunk = chunks[currentIndex] ?? null; - const enabledStages = config.stages.filter((s) => s.enabled); + const enabledStages = useMemo(() => config.stages.filter((s) => s.enabled), [config.stages]); const lastStageId = enabledStages[enabledStages.length - 1]?.id ?? ''; const isEditorialMode = enabledStages.length > 1; const deferredSourceText = useDeferredValue(currentChunk?.sourceDisplayText ?? ''); @@ -157,6 +156,36 @@ export function DocumentView({ setSelectedStageId(lastStageId); }, [currentChunk?.id, lastStageId]); + // Diff mode is only available in translation-only pane. + useEffect(() => { + if (paneFocus !== 'translation') setShowDiffMode(false); + }, [paneFocus]); + + const diffPairs = useMemo(() => { + return enabledStages.slice(0, -1).map((stage, index) => { + const nextStage = enabledStages[index + 1]; + return { + key: `${stage.id}::${nextStage.id}`, + fromId: stage.id, + toId: nextStage.id, + fromName: stage.name, + toName: nextStage.id === lastStageId ? t('document.finalDraft') : nextStage.name, + }; + }); + }, [enabledStages, lastStageId, t]); + + useEffect(() => { + if (diffPairs.length === 0) { + setDiffPairKey(''); + return; + } + setDiffPairKey((prev) => ( + prev && diffPairs.some((pair) => pair.key === prev) + ? prev + : diffPairs[0]!.key + )); + }, [diffPairs]); + // Hooks devono essere chiamati prima di qualsiasi return condizionale const hasGlossary = config.glossary.length > 0; const showHighlight = highlightsEnabled && hasGlossary; @@ -174,6 +203,17 @@ export function DocumentView({ focusedIssueQuery ?? '', ); + const activeDiffPair = diffPairs.find((pair) => pair.key === diffPairKey) ?? diffPairs[0] ?? null; + const effectiveDiffStageIdA = activeDiffPair?.fromId ?? ''; + const effectiveDiffStageIdB = activeDiffPair?.toId ?? ''; + const diffTextA = effectiveDiffStageIdA === lastStageId + ? (currentChunk?.currentDraft ?? '') + : (currentChunk?.stageResults[effectiveDiffStageIdA]?.content ?? ''); + const diffTextB = effectiveDiffStageIdB === lastStageId + ? (currentChunk?.currentDraft ?? '') + : (currentChunk?.stageResults[effectiveDiffStageIdB]?.content ?? ''); + const stageDiff = useStageDiff(showDiffMode ? diffTextA : '', showDiffMode ? diffTextB : ''); + const sourceHighlightHtml = useMemo(() => { const hasFootnoteMarkers = /\[[⁰¹²³⁴⁵⁶⁷⁸⁹]/.test(deferredSourceText); const showGlossary = showHighlight && paneFocus !== 'translation'; @@ -185,7 +225,7 @@ export function DocumentView({ if (!currentChunk) { return ( -
+
{/* Brand mark */}
@@ -238,280 +278,104 @@ export function DocumentView({ const sourceEditDisabled = currentChunk.status === 'processing'; return ( -
+
-
- {/* Pannello run: striscia orizzontale compatta */} - {onRunPipeline && onCancelPipeline && ( -
- {activePipeline && ( -
- {activePipeline.name} -
- )} -
-
- - -
- -
- -
- {runChunkCount} -
- - -
-
- -
-
- {isProcessing ? ( - cancelRequested ? ( - - ) : ( - - ) - ) : ( - - )} - - {costEstimate.stages.length > 0 && ( -
setShowCostPanel(true)} - onMouseLeave={() => setShowCostPanel(false)} - > - -
- )} - {showCostPanel && costEstimate.stages.length > 0 && ( -
setShowCostPanel(true)} - onMouseLeave={() => setShowCostPanel(false)} - > - -
- )} -
-
-
- )} - +
{/* Navigation bar */} -
-
- - {/* Sinistra: navigazione chunk */} -
- prevChunk && setSelectedChunkId(prevChunk.id)} - title={t('document.previousChunk')} - disabled={!prevChunk} - > - - - - {indexPad(currentIndex + 1)}/{indexPad(chunks.length)} - - nextChunk && setSelectedChunkId(nextChunk.id)} - title={t('document.nextChunk')} - disabled={!nextChunk} - > - - -
- - {/* Centro: pannelli visualizzazione + toggle highlights */} -
- setPaneFocus('both')} - title={t('document.focusBoth')} - active={paneFocus === 'both'} - ariaPressed={paneFocus === 'both'} - > - - - setPaneFocus('source')} - title={t('document.focusSource')} - active={paneFocus === 'source'} - ariaPressed={paneFocus === 'source'} - > - - - setPaneFocus('translation')} - title={t('document.focusTranslation')} - active={paneFocus === 'translation'} - ariaPressed={paneFocus === 'translation'} - > - - -
- - {/* Destra: stati pipeline + modifica sorgente + blocca */} -
- {config.stages - .filter((stage) => stage.enabled) - .map((stage) => { - const stageIcon: LucideIcon = +
+
+
+ {enabledStages.map((stage) => { + const Icon: LucideIcon = stage.role === 'refine' ? Pencil : stage.role === 'format' ? FileText : Languages; return ( - + disabled={false} + status={currentChunk.stageResults[stage.id]?.status ?? 'idle'} + onClick={() => setTraceStageId(traceStageId === stage.id ? null : stage.id)} + /> ); })} - - +
+ +
+ prevChunk && setSelectedChunkId(prevChunk.id)} + title={t('document.previousChunk')} + disabled={!prevChunk} + > + + +
+
+ {t('document.chunkLabel')} +
+
+ {indexPad(currentIndex + 1)}/{indexPad(chunks.length)} +
+
+ nextChunk && setSelectedChunkId(nextChunk.id)} + title={t('document.nextChunk')} + disabled={!nextChunk} + > + + +
+
+
-
+ {chunks.length > 1 && ( +
+ {chunks.map((chunk, idx) => { + const segmentTone = + chunk.status === 'completed' + ? 'bg-editorial-success/18 shadow-[inset_0_0_0_1px_rgba(58,122,101,0.16)]' + : chunk.status === 'preview' + ? 'bg-editorial-charcoal/22 shadow-[inset_0_0_0_1px_rgba(58,122,114,0.12)]' + : chunk.status === 'error' + ? 'bg-editorial-accent/22 shadow-[inset_0_0_0_1px_rgba(200,112,94,0.18)]' + : chunk.status === 'processing' + ? 'bg-editorial-running/24 animate-pulse shadow-[inset_0_0_0_1px_rgba(196,155,42,0.22)]' + : 'bg-editorial-border/40'; + const isCurrent = idx === currentIndex; + const sizeClass = chunk.translationLocked + ? (isCurrent ? 'h-4.5 w-4.5' : 'h-4 w-4') + : (isCurrent ? 'h-4 w-4' : 'h-3 w-3'); + return ( + + + + ); + })} +
+ )}
@@ -546,6 +410,7 @@ export function DocumentView({ )}
} + scrollRef={scrollSourceRef} > { const stageReadOnly = !isLastSelected || currentChunk.translationLocked === true; + const lockToggle = ( + + + + ); + const stageButtons = enabledStages.map((s) => { + const Icon = s.role === 'refine' ? Wand2 : s.role === 'format' ? FileText : Languages; + const isActive = effectiveSelectedStageId === s.id; + const hasContent = s.id === lastStageId + ? true + : !!(currentChunk.stageResults[s.id]?.content); + return ( + setSelectedStageId(s.id)} + title={t('document.viewStageResult', { stage: t(`pipeline.stageRole.${s.role ?? 'translation'}`) })} + disabled={!hasContent || showDiffMode} + active={isActive && !showDiffMode} + ariaPressed={isActive && !showDiffMode} + > + + + ); + }); + const diffButtons = diffPairs.map((pair) => { + const isActive = pair.key === diffPairKey && showDiffMode; + const fromStage = enabledStages.find((s) => s.id === pair.fromId); + const DiffIcon = fromStage?.role === 'refine' ? Wand2 : fromStage?.role === 'format' ? FileText : Languages; + return ( + setDiffPairKey(pair.key)} + title={`${pair.fromName} → ${pair.toName}`} + disabled={!showDiffMode} + active={isActive} + ariaPressed={isActive} + > + + + ); + }); const stageActions = isEditorialMode ? (
- {enabledStages.map((s) => { - const Icon = s.role === 'refine' ? Wand2 : s.role === 'format' ? FileText : Languages; - const isActive = effectiveSelectedStageId === s.id; - const hasContent = s.id === lastStageId - ? true - : !!(currentChunk.stageResults[s.id]?.content); - return ( - - ); - })} + {stageButtons} +
) : null; @@ -597,31 +506,47 @@ export function DocumentView({ s.id === effectiveSelectedStageId)?.role ?? 'translation'}`) : undefined} - subtitleAction={rawStageContent ? : undefined} + eyebrowMeta={currentChunk.status === 'preview' ? ( + + + + + + ) : null} + subtitle={ + showDiffMode && activeDiffPair + ? `${activeDiffPair.fromName} → ${activeDiffPair.toName}` + : undefined + } + titleMeta={rawStageContent ? : null} actions={stageActions} statusBadge={currentChunk.translationStale ? ( } label={t('document.translationStaleBadge')} /> - ) : currentChunk.status === 'preview' ? ( - } ariaLabel={t('document.chunkPreviewBadge')} /> - ) : currentChunk.translationLocked ? ( - } label={t('document.translationLockedBadge')} /> - ) : null} + ) : lockToggle} + scrollRef={scrollTranslationRef} > - updateChunkDraft(currentChunk.id, nextValue) : () => {}} - markdownEnabled={config.markdownAware === true} - readOnly={stageReadOnly} - fillHeight - textClassName="text-[15px] leading-8 text-editorial-ink" - previewClassName="min-h-[280px] text-[15px] leading-8 text-editorial-ink" - placeholder={isLastSelected ? t('pipeline.candidatePlaceholder') : ''} - highlightHtml={(showHighlight || (highlightsEnabled && !!searchQuery.trim()) || !!focusedIssueQuery) ? translationHighlight.html : null} - focusQuery={isLastSelected && focusedChunkId === currentChunk.id ? focusedIssueQuery : null} - focusRequestId={isLastSelected && focusedChunkId === currentChunk.id ? focusedIssueRequestId : 0} - - /> + {showDiffMode ? ( +
+ +
+ ) : ( + updateChunkDraft(currentChunk.id, nextValue) : NOOP_CHANGE} + markdownEnabled={config.markdownAware === true} + readOnly={stageReadOnly} + fillHeight + textClassName="text-[15px] leading-8 text-editorial-ink" + previewClassName="min-h-[280px] text-[15px] leading-8 text-editorial-ink" + placeholder={isLastSelected ? t('pipeline.candidatePlaceholder') : ''} + highlightHtml={(showHighlight || (highlightsEnabled && !!searchQuery.trim()) || !!focusedIssueQuery) ? translationHighlight.html : null} + focusQuery={isLastSelected && focusedChunkId === currentChunk.id ? focusedIssueQuery : null} + focusRequestId={isLastSelected && focusedChunkId === currentChunk.id ? focusedIssueRequestId : 0} + /> + )}
); })()} @@ -632,6 +557,7 @@ export function DocumentView({ entry.id === traceStageId) ?? null} + isJudge={traceStageId === '_judge'} onClose={() => setTraceStageId(null)} /> ) : null} @@ -642,29 +568,34 @@ export function DocumentView({ interface DocumentPageProps { label: string; eyebrow: string; + eyebrowMeta?: React.ReactNode; subtitle?: string; subtitleAction?: React.ReactNode; readOnly?: boolean; highlighted?: boolean; titleMeta?: React.ReactNode; statusBadge?: React.ReactNode; - actions?: React.ReactNode; + actions?: React.ReactNode | null; footer?: React.ReactNode; + scrollRef?: React.RefObject; children: React.ReactNode; } function StageTraceDialog({ chunk, stage, + isJudge = false, onClose, }: { chunk: TranslationChunk; stage: ReturnType['config']['stages'][number] | null; + isJudge?: boolean; onClose: () => void; }) { const { t } = useTranslation(); const trapRef = useFocusTrap(true, onClose); - const result = stage ? chunk.stageResults[stage.id] : null; + const result = isJudge ? chunk.judgeResult : stage ? chunk.stageResults[stage.id] : null; + const dialogTitle = isJudge ? t('pipeline.audit') : (stage?.name ?? t('errors.unknownError')); return (
- {stage?.name ?? t('errors.unknownError')} + {dialogTitle}

{result?.status ?? 'idle'} @@ -725,6 +656,7 @@ function StageTraceDialog({ function DocumentPage({ label, eyebrow, + eyebrowMeta, subtitle, subtitleAction, readOnly = false, @@ -733,16 +665,21 @@ function DocumentPage({ statusBadge, actions, footer, + scrollRef, children, }: DocumentPageProps) { return (

-
+ {/* Header con altezza minima fissa per allineare il corpo testo tra i due pannelli */} +
-
- {eyebrow} +
+
+ {eyebrow} +
+ {eyebrowMeta}

@@ -759,12 +696,15 @@ function DocumentPage({

)}
-
+
{titleMeta} + {titleMeta && actions && ( +
-
+
{children}
{footer && ( @@ -794,16 +734,17 @@ function InlineStatusBadge({ ? 'border-editorial-success/50 bg-editorial-success/8 text-editorial-success' : 'border-editorial-border bg-editorial-textbox/60 text-editorial-muted'; return ( - - {icon} - {label && ( - {label} - )} - + + + {icon} + {label && ( + {label} + )} + + ); } @@ -813,6 +754,7 @@ function ChunkIconButton({ title, disabled = false, active = false, + activeClassName, ariaPressed, }: { onClick: () => void; @@ -820,24 +762,26 @@ function ChunkIconButton({ title: string; disabled?: boolean; active?: boolean; + activeClassName?: string; ariaPressed?: boolean; }) { return ( - + + + ); } @@ -845,9 +789,9 @@ const COMPACT_STATUS_TONE = { completed: 'border-editorial-success/40 bg-editorial-success/12 text-editorial-success', processing: - 'border-editorial-warning/45 bg-editorial-warning/12 text-editorial-warning animate-pulse', + 'border-editorial-running/45 bg-editorial-running/12 text-editorial-running animate-pulse', error: 'border-editorial-accent/40 bg-editorial-accent/10 text-editorial-accent', - retrying: 'border-editorial-warning/45 bg-editorial-warning/12 text-editorial-warning animate-pulse', + retrying: 'border-editorial-running/45 bg-editorial-running/12 text-editorial-running animate-pulse', idle: 'border-editorial-border bg-editorial-bg text-editorial-muted', } as const; @@ -855,23 +799,27 @@ function CompactStatusIndicator({ status, label, icon: Icon, + size = 'md', }: { status: string; label?: string; icon?: LucideIcon; + size?: 'sm' | 'md'; }) { const tone = status === 'completed' || status === 'processing' || status === 'error' || status === 'retrying' ? status : 'idle'; + const sizeClass = size === 'sm' ? 'h-7 w-7' : 'h-9 w-9'; + const iconSize = size === 'sm' ? 13 : 16; return (