From 3be4b073b43c91f55181fe77a3f2e322495721be Mon Sep 17 00:00:00 2001 From: nikazzio Date: Fri, 29 May 2026 08:51:50 +0200 Subject: [PATCH 1/6] feat(#197, #8): panel UX, scroll sync, pipeline delete, diff view MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DocumentView: redesign navbar — chunk progress dots, context-aware scroll-sync / diff toggle (Link2 in both mode, GitCompare in single mode) - Panel header min-height alignment so text baselines match across panels - usePanelScrollSync: new hook, capture-phase ratio-based sync, anti-loop ref - useStageDiff: new hook using diff npm lib for word-level HTML diff - hl-diff-add / hl-diff-rm CSS classes added to index.css - PipelineStrip: delete pipeline per-tab (×, group hover), confirm dialog - uiStore: maxPipelines (default 5, persisted, migration v2→v3) - Diff mode: visible only in single-panel view; replaces MarkdownEditor with HighlightedText; stage A/B selectors replace stage tabs in navbar - i18n: new keys for scrollSync, diffMode, finalDraft, deletePipeline --- package-lock.json | 18 ++ package.json | 2 + src/components/document/DocumentView.tsx | 320 +++++++++++++++++------ src/components/layout/PipelineStrip.tsx | 71 +++-- src/hooks/usePanelScrollSync.ts | 53 ++++ src/hooks/useStageDiff.ts | 26 ++ src/i18n/en.json | 11 +- src/i18n/it.json | 11 +- src/index.css | 16 ++ src/stores/uiStore.ts | 12 +- 10 files changed, 434 insertions(+), 106 deletions(-) create mode 100644 src/hooks/usePanelScrollSync.ts create mode 100644 src/hooks/useStageDiff.ts 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/components/document/DocumentView.tsx b/src/components/document/DocumentView.tsx index f809aca..a42cac7 100644 --- a/src/components/document/DocumentView.tsx +++ b/src/components/document/DocumentView.tsx @@ -6,9 +6,12 @@ import { Columns2, FileText, FlaskConical, + GitCompare, Highlighter, Info, Languages, + Link2, + Link2Off, Loader2, Lock, Minus, @@ -34,10 +37,12 @@ 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 { CopyButton, HighlightedText, MarkdownEditor, ProcessingLine } from '../common'; import { CostBreakdownPanel } from '../pipeline/CostBadge'; 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'; interface DocumentViewProps { @@ -104,6 +109,14 @@ export function DocumentView({ const [traceStageId, setTraceStageId] = useState(null); const [showCostPanel, setShowCostPanel] = useState(false); const [selectedStageId, setSelectedStageId] = useState(''); + const [syncScrollEnabled, setSyncScrollEnabled] = useState(false); + const [showDiffMode, setShowDiffMode] = useState(false); + const [diffStageIdA, setDiffStageIdA] = useState(''); + const [diffStageIdB, setDiffStageIdB] = useState(''); + + const { sourceRef: scrollSourceRef, translationRef: scrollTranslationRef } = usePanelScrollSync( + paneFocus === 'both' && syncScrollEnabled, + ); const costEstimate = useMemo( () => estimatePipelineCost(chunks, config, pricingOverrides), @@ -157,6 +170,19 @@ export function DocumentView({ setSelectedStageId(lastStageId); }, [currentChunk?.id, lastStageId]); + // Reset diff mode when switching to both-panel view + useEffect(() => { + if (paneFocus === 'both') setShowDiffMode(false); + }, [paneFocus]); + + // Initialise diff stage IDs when stages are available + useEffect(() => { + if (enabledStages.length >= 2) { + setDiffStageIdA((prev) => prev || enabledStages[0].id); + setDiffStageIdB((prev) => prev || lastStageId); + } + }, [enabledStages, lastStageId]); + // Hooks devono essere chiamati prima di qualsiasi return condizionale const hasGlossary = config.glossary.length > 0; const showHighlight = highlightsEnabled && hasGlossary; @@ -174,6 +200,16 @@ export function DocumentView({ focusedIssueQuery ?? '', ); + const effectiveDiffStageIdA = diffStageIdA || enabledStages[0]?.id || ''; + const effectiveDiffStageIdB = diffStageIdB || lastStageId; + 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'; @@ -397,28 +433,55 @@ export function DocumentView({
- {/* 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} - > - - + {/* Sinistra: navigazione chunk + strip di stato */} +
+
+ 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} + > + + +
+ {chunks.length > 1 && ( + + )}
- {/* Centro: pannelli visualizzazione + toggle highlights */} + {/* Centro: pannelli + toggle contestuale (scroll sync / diff) + highlights */}
setPaneFocus('both')} @@ -445,52 +508,95 @@ export function DocumentView({
- {/* Destra: stati pipeline + modifica sorgente + blocca */} + {/* Destra: stage controls + qualità */}
- {config.stages - .filter((stage) => stage.enabled) - .map((stage) => { - const stageIcon: LucideIcon = - stage.role === 'refine' ? Pencil - : stage.role === 'format' ? FileText - : Languages; - return ( - - ); - })} - + {showDiffMode ? ( +
+ + vs + +
+ ) : ( + <> + {config.stages + .filter((stage) => stage.enabled) + .map((stage) => { + const stageIcon: LucideIcon = + stage.role === 'refine' ? Pencil + : stage.role === 'format' ? FileText + : Languages; + return ( + + ); + })} + + + )}
} + scrollRef={scrollSourceRef} > ) : null; + const diffStageName = (stageId: string) => + stageId === lastStageId + ? t('document.finalDraft') + : (enabledStages.find((s) => s.id === stageId)?.name ?? stageId); + return ( s.id === effectiveSelectedStageId)?.role ?? 'translation'}`) : undefined} - subtitleAction={rawStageContent ? : undefined} - actions={stageActions} + subtitle={ + showDiffMode + ? `${diffStageName(effectiveDiffStageIdA)} → ${diffStageName(effectiveDiffStageIdB)}` + : isEditorialMode + ? t(`pipeline.stageRole.${enabledStages.find(s => s.id === effectiveSelectedStageId)?.role ?? 'translation'}`) + : undefined + } + subtitleAction={!showDiffMode && rawStageContent ? : undefined} + actions={!showDiffMode ? stageActions : null} statusBadge={currentChunk.translationStale ? ( } label={t('document.translationStaleBadge')} /> ) : currentChunk.status === 'preview' ? ( @@ -607,21 +725,30 @@ export function DocumentView({ ) : currentChunk.translationLocked ? ( } label={t('document.translationLockedBadge')} /> ) : null} + 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) : () => {}} + 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} + /> + )}
); })()} @@ -648,8 +775,9 @@ interface DocumentPageProps { highlighted?: boolean; titleMeta?: React.ReactNode; statusBadge?: React.ReactNode; - actions?: React.ReactNode; + actions?: React.ReactNode | null; footer?: React.ReactNode; + scrollRef?: React.RefObject; children: React.ReactNode; } @@ -733,13 +861,15 @@ function DocumentPage({ statusBadge, actions, footer, + scrollRef, children, }: DocumentPageProps) { return (
-
+ {/* Header con altezza minima fissa per allineare il corpo testo tra i due pannelli */} +
{eyebrow} @@ -759,12 +889,12 @@ function DocumentPage({
)}
-
+
{titleMeta} {actions}
-
+
{children}
{footer && ( @@ -881,6 +1011,38 @@ function CompactStatusIndicator({ ); } +function StageSelect({ + value, + onChange, + stages, + lastStageId, + label, +}: { + value: string; + onChange: (id: string) => void; + stages: ReturnType['config']['stages']; + lastStageId: string; + label: string; +}) { + const { t } = useTranslation(); + return ( +
+ {label} + +
+ ); +} + function truncateChunk(text: string) { const normalized = text.replace(/\s+/g, ' ').trim(); if (normalized.length <= 58) return normalized; diff --git a/src/components/layout/PipelineStrip.tsx b/src/components/layout/PipelineStrip.tsx index c584fea..680a8ee 100644 --- a/src/components/layout/PipelineStrip.tsx +++ b/src/components/layout/PipelineStrip.tsx @@ -1,22 +1,34 @@ -import { Settings2 } from 'lucide-react'; +import { Settings2, X } from 'lucide-react'; import { useTranslation } from 'react-i18next'; import { useProjectStore } from '../../stores/projectStore'; import { usePipelineStore } from '../../stores/pipelineStore'; import { useUiStore } from '../../stores/uiStore'; +import { confirm } from '../../stores/confirmStore'; export function PipelineStrip() { const { t } = useTranslation(); - const { pipelines, activePipelineId, currentProjectId, switchPipeline, createNewPipeline } = useProjectStore(); + const { pipelines, activePipelineId, currentProjectId, switchPipeline, createNewPipeline, deletePipeline } = useProjectStore(); const runStatus = usePipelineStore((s) => s.runStatus); - const { showConfigDrawer, setShowConfigDrawer } = useUiStore(); + const { showConfigDrawer, setShowConfigDrawer, maxPipelines } = useUiStore(); const hasProject = !!currentProjectId; + const isRunning = runStatus === 'running'; + + const handleDelete = async (pipelineId: string, pipelineName: string) => { + const ok = await confirm({ + title: t('pipeline.confirmDeleteTitle'), + message: t('pipeline.confirmDeleteMessage', { name: pipelineName }), + confirmLabel: t('pipeline.deletePipeline'), + danger: true, + }); + if (!ok) return; + await deletePipeline(pipelineId); + }; return (
{pipelines.length === 0 ? ( - // Placeholder prima del salvataggio — rappresenta la config corrente
{ const isActive = pipeline.id === activePipelineId; - const isRunning = isActive && runStatus === 'running'; + const isPipelineRunning = isActive && isRunning; return ( - + {pipelines.length > 1 && !isPipelineRunning && ( + )} - +
); }) )} - {hasProject && pipelines.length > 0 && pipelines.length < 10 && ( + {hasProject && pipelines.length > 0 && pipelines.length < maxPipelines && ( ); })} +
) : null; @@ -712,11 +718,10 @@ export function DocumentView({ subtitle={ showDiffMode ? `${diffStageName(effectiveDiffStageIdA)} → ${diffStageName(effectiveDiffStageIdB)}` - : isEditorialMode - ? t(`pipeline.stageRole.${enabledStages.find(s => s.id === effectiveSelectedStageId)?.role ?? 'translation'}`) - : undefined + : undefined } - subtitleAction={!showDiffMode && rawStageContent ? : undefined} + subtitleAction={showDiffMode && rawStageContent ? : undefined} + titleMeta={!showDiffMode && rawStageContent ? : undefined} actions={!showDiffMode ? stageActions : null} statusBadge={currentChunk.translationStale ? ( } label={t('document.translationStaleBadge')} /> @@ -759,6 +764,7 @@ export function DocumentView({ entry.id === traceStageId) ?? null} + isJudge={traceStageId === '_judge'} onClose={() => setTraceStageId(null)} /> ) : null} @@ -784,15 +790,18 @@ interface DocumentPageProps { 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'} @@ -869,7 +878,7 @@ function DocumentPage({ highlighted ? 'border border-editorial-accent ring-2 ring-editorial-accent/30' : 'border border-[#d8cfbf]' }`}> {/* Header con altezza minima fissa per allineare il corpo testo tra i due pannelli */} -

+
{eyebrow} From 835a81fa773752f6f541f81c38cd40c266ac70b8 Mon Sep 17 00:00:00 2001 From: nikazzio Date: Fri, 29 May 2026 14:53:18 +0200 Subject: [PATCH 3/6] =?UTF-8?q?fix(#197):=20refine=20panel=20UX=20?= =?UTF-8?q?=E2=80=94=20stage=20indicators,=20nav=20bar,=20chunk=20dots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Aggiunge token editorial-running (#C49B2A) per stato in esecuzione - StatusIndicator + CompactStatusIndicator usano editorial-running (giallo) per processing/retrying con animate-pulse - Stage indicators spostati nella riga del nome pipeline nel run panel (destra, allineati) - CompactStatusIndicator: aggiunta prop size (sm|md) per varianti dimensionali - Stage disabilitati mostrati con opacity-25 nel run panel - Stage indicators rimossi dal nav bar (destra) — solo diff select + lock rimangono - Chunk dots: h-2.5 w-2.5 (da 6→10px), colore idle editorial-charcoal/25, ring attivo charcoal/50 - DocumentPage header: separatore | tra CopyButton e stage switcher, ml-6 - Stage switcher nome: larghezza fissa w-[8rem] per evitare layout shift al cambio stage --- docs/UI_DESIGN_SYSTEM.md | 3 +- src/components/common/StatusIndicator.tsx | 8 +- src/components/document/DocumentView.tsx | 120 ++++++++++++---------- src/index.css | 1 + 4 files changed, 72 insertions(+), 60 deletions(-) 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/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 c4b3f0a..f9dfd54 100644 --- a/src/components/document/DocumentView.tsx +++ b/src/components/document/DocumentView.tsx @@ -279,12 +279,53 @@ export function DocumentView({
{/* Pannello run: striscia orizzontale compatta */} {onRunPipeline && onCancelPipeline && ( -
- {activePipeline && ( -
- {activePipeline.name} +
+
+ {activePipeline ? ( + + {activePipeline.name} + + ) : } +
+ {config.stages.map((stage) => { + const stageIcon: LucideIcon = + stage.role === 'refine' ? Pencil + : stage.role === 'format' ? FileText + : Languages; + return ( + + ); + })} +
- )} +
- {/* Destra: stage controls + qualità */} + {/* Destra: diff select + lock */}
- {showDiffMode ? ( + {showDiffMode && (
- ) : ( - <> - {config.stages - .filter((stage) => stage.enabled) - .map((stage) => { - const stageIcon: LucideIcon = - stage.role === 'refine' ? Pencil - : stage.role === 'format' ? FileText - : Languages; - return ( - - ); - })} - - )}
@@ -898,8 +901,11 @@ function DocumentPage({
)}
-
+
{titleMeta} + {titleMeta && actions && ( +
@@ -984,9 +990,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; @@ -994,23 +1000,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 (