From 18dac8839127120a280b9bd9df082458f2a12b9f Mon Sep 17 00:00:00 2001 From: nikazzio Date: Sat, 30 May 2026 01:16:33 +0200 Subject: [PATCH 1/2] =?UTF-8?q?refactor(#199):=20consolida=20primitive=20U?= =?UTF-8?q?I=20=E2=80=94=20IconButton=20CVA,=20Tooltip,=20StatusDot,=20Sec?= =?UTF-8?q?tionLabel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rebuild IconButton con class-variance-authority (tone/size type-safe) - Aggiunta primitive StatusDot e SectionLabel - Tooltip: aggiunto side='bottom' - Sweep completo: DocumentView, PipelineSidebar, Header, SettingsModal, PipelineConfig, ImportPreviewDialog migrati a IconButton - Eliminati ChunkIconButton, SidebarIconButton, CompactStatusIndicator, COMPACT_STATUS_TONE, NAV_STAGE_TONE, tabBtnCls - Fix PromptTemplatesTab: rimosso colore purple non-editorial --- package-lock.json | 22 ++ package.json | 1 + src/components/document/DocumentView.tsx | 219 +++++------------- .../document/ImportPreviewDialog.tsx | 49 ++-- src/components/layout/Header.tsx | 2 +- src/components/layout/PipelineSidebar.tsx | 80 ++----- src/components/library/PromptTemplatesTab.tsx | 2 +- src/components/pipeline/PipelineConfig.tsx | 100 +++----- src/components/settings/SettingsModal.tsx | 31 +-- src/components/ui/IconButton.tsx | 55 +++-- src/components/ui/SectionLabel.tsx | 17 ++ src/components/ui/StatusDot.tsx | 37 +++ src/components/ui/Tooltip.tsx | 8 +- src/components/ui/index.ts | 4 +- 14 files changed, 259 insertions(+), 368 deletions(-) create mode 100644 src/components/ui/SectionLabel.tsx create mode 100644 src/components/ui/StatusDot.tsx diff --git a/package-lock.json b/package-lock.json index e1dd4d6..22ee3ed 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", + "class-variance-authority": "^0.7.1", "diff": "^9.0.0", "i18next": "^25.1.0", "lucide-react": "^0.546.0", @@ -2404,6 +2405,27 @@ "node": ">=18" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", diff --git a/package.json b/package.json index 7b29953..610f4c5 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", + "class-variance-authority": "^0.7.1", "diff": "^9.0.0", "i18next": "^25.1.0", "lucide-react": "^0.546.0", diff --git a/src/components/document/DocumentView.tsx b/src/components/document/DocumentView.tsx index 72a6535..b990f7f 100644 --- a/src/components/document/DocumentView.tsx +++ b/src/components/document/DocumentView.tsx @@ -22,7 +22,7 @@ import { useUiStore } from '../../stores/uiStore'; import type { TranslationChunk } from '../../types'; import { indexPad } from '../../utils'; import { CopyButton, HighlightedText, MarkdownEditor, ProcessingLine } from '../common'; -import { Tooltip } from '../ui'; +import { IconButton, Tooltip, type IconButtonTone } from '../ui'; import { useFocusTrap } from '../../hooks/useFocusTrap'; import { escapeHtml, useGlossaryHighlight } from '../../hooks/useGlossaryHighlight'; import { usePanelScrollSync } from '../../hooks/usePanelScrollSync'; @@ -35,38 +35,13 @@ interface DocumentViewProps { onRetranslateChunk: (chunkId: string) => 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 ( - - - - ); -} +const STAGE_TONE_MAP: Record = { + completed: 'success', + processing: 'running', + retrying: 'running', + error: 'accent', + idle: 'muted', +}; export function DocumentView({ onRetranslateChunk }: DocumentViewProps) { @@ -290,34 +265,38 @@ export function DocumentView({ onRetranslateChunk }: DocumentViewProps) { stage.role === 'refine' ? Pencil : stage.role === 'format' ? FileText : Languages; + const stageTone = STAGE_TONE_MAP[currentChunk.stageResults[stage.id]?.status ?? 'idle'] ?? 'muted'; return ( - setTraceStageId(traceStageId === stage.id ? null : stage.id)} - /> + > + + ); })} - setTraceStageId(traceStageId === '_judge' ? null : '_judge')} - /> + > + +
- prevChunk && setSelectedChunkId(prevChunk.id)} title={t('document.previousChunk')} disabled={!prevChunk} > - +
{t('document.chunkLabel')} @@ -326,13 +305,14 @@ export function DocumentView({ onRetranslateChunk }: DocumentViewProps) { {indexPad(currentIndex + 1)}/{indexPad(chunks.length)}
- nextChunk && setSelectedChunkId(nextChunk.id)} title={t('document.nextChunk')} disabled={!nextChunk} > - +
@@ -390,23 +370,25 @@ export function DocumentView({ onRetranslateChunk }: DocumentViewProps) { ) : null} actions={
- toggleChunkSourceEditing(currentChunk.id)} title={currentChunk.sourceEditable ? t('document.disableSourceEditing') : t('document.enableSourceEditing')} disabled={sourceEditDisabled} - active={currentChunk.sourceEditable === true} ariaPressed={currentChunk.sourceEditable === true} > - + {currentChunk.sourceDisplayText !== currentChunk.originalText && ( - restoreChunkSourceText(currentChunk.id)} title={t('document.restoreSourceText')} disabled={sourceEditDisabled} > - + )}
} @@ -429,22 +411,16 @@ export function DocumentView({ onRetranslateChunk }: DocumentViewProps) { {paneFocus !== 'source' && (() => { const stageReadOnly = !isLastSelected || currentChunk.translationLocked === true; const lockToggle = ( - - - + toggleChunkTranslationLock(currentChunk.id)} + disabled={!currentChunk.currentDraft?.trim()} + ariaPressed={currentChunk.translationLocked === true} + > + + ); const stageButtons = enabledStages.map((s) => { const Icon = s.role === 'refine' ? Wand2 : s.role === 'format' ? FileText : Languages; @@ -453,16 +429,17 @@ export function DocumentView({ onRetranslateChunk }: DocumentViewProps) { ? 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) => { @@ -470,34 +447,36 @@ export function DocumentView({ onRetranslateChunk }: DocumentViewProps) { 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 ? (
{stageButtons}
) : null; @@ -748,89 +727,3 @@ function InlineStatusBadge({ ); } -function ChunkIconButton({ - onClick, - children, - title, - disabled = false, - active = false, - activeClassName, - ariaPressed, -}: { - onClick: () => void; - children: React.ReactNode; - title: string; - disabled?: boolean; - active?: boolean; - activeClassName?: string; - ariaPressed?: boolean; -}) { - return ( - - - - ); -} - -const COMPACT_STATUS_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', - error: 'border-editorial-accent/40 bg-editorial-accent/10 text-editorial-accent', - 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; - -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 ( - - ); -} - -function truncateChunk(text: string) { - const normalized = text.replace(/\s+/g, ' ').trim(); - if (normalized.length <= 58) return normalized; - return `${normalized.slice(0, 55)}...`; -} diff --git a/src/components/document/ImportPreviewDialog.tsx b/src/components/document/ImportPreviewDialog.tsx index be2ecaa..cd75a94 100644 --- a/src/components/document/ImportPreviewDialog.tsx +++ b/src/components/document/ImportPreviewDialog.tsx @@ -31,6 +31,7 @@ import { LANGUAGES } from '../../constants'; import { getSelectableModelIds, MODEL_PROVIDER_ORDER } from '../../models/catalog'; import { useUiStore } from '../../stores/uiStore'; import type { ModelProvider } from '../../types'; +import { IconButton } from '../ui'; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -800,49 +801,37 @@ export function ImportPreviewDialog({
{/* Auto-segment toggle — icon button */} - + {/* Heading-aware toggle — icon button (markdown only) */} {markdownAware && ( - + )} - + {/* Separator — solo in stato normale */} {useChunking && !hasManualEdits && ( @@ -851,19 +840,15 @@ export function ImportPreviewDialog({ {/* Preset inline — solo in stato normale */} {useChunking && !hasManualEdits && CHUNK_PRESETS.map(({ words, titleKey, Icon }) => ( - + ))} {/* Con modifiche manuali: preset + ricalcola raggruppati a destra in warning */} diff --git a/src/components/layout/Header.tsx b/src/components/layout/Header.tsx index ee56946..146ae70 100644 --- a/src/components/layout/Header.tsx +++ b/src/components/layout/Header.tsx @@ -341,7 +341,7 @@ export function Header({ onRunPipeline, onCancelPipeline }: HeaderProps = {}) { title={sandboxLabel} ariaLabel={sandboxLabel} ariaPressed={viewMode === 'sandbox'} - active={viewMode === 'sandbox'} + tone={viewMode === 'sandbox' ? 'accent' : 'default'} > diff --git a/src/components/layout/PipelineSidebar.tsx b/src/components/layout/PipelineSidebar.tsx index 17110a3..40a7356 100644 --- a/src/components/layout/PipelineSidebar.tsx +++ b/src/components/layout/PipelineSidebar.tsx @@ -27,7 +27,7 @@ import { useProjectStore } from '../../stores/projectStore'; import { useUiStore } from '../../stores/uiStore'; import { estimatePipelineCost } from '../../utils/costEstimate'; import { CostBreakdownPanel } from '../pipeline/CostBadge'; -import { Tooltip } from '../ui'; +import { IconButton, Tooltip } from '../ui'; interface PipelineSidebarProps { onRunPipeline: () => void; @@ -385,54 +385,54 @@ export function PipelineSidebar({ {t('document.panelsTitle')}
- setDocumentPaneFocus('both')} title={t('document.focusBoth')} - active={documentPaneFocus === 'both'} ariaPressed={documentPaneFocus === 'both'} > - - + setDocumentPaneFocus('source')} title={t('document.focusSource')} - active={documentPaneFocus === 'source'} ariaPressed={documentPaneFocus === 'source'} > - - + setDocumentPaneFocus('translation')} title={t('document.focusTranslation')} - active={documentPaneFocus === 'translation'} ariaPressed={documentPaneFocus === 'translation'} > - +
- setSyncScrollEnabled(!syncScrollEnabled)} title={syncScrollEnabled ? t('document.scrollSyncDisable') : t('document.scrollSyncEnable')} - active={syncScrollEnabled && !syncScrollDisabled} disabled={syncScrollDisabled} ariaPressed={syncScrollEnabled && !syncScrollDisabled} > {syncScrollEnabled && !syncScrollDisabled ? : } - - + setHighlightsEnabled(!highlightsEnabled)} title={t('document.highlightsToggle')} - active={highlightsEnabled} ariaPressed={highlightsEnabled} > - +
@@ -440,44 +440,6 @@ export function PipelineSidebar({ ); } -function SidebarIconButton({ - children, - active = false, - disabled = false, - compact = false, - title, - onClick, - ariaPressed, -}: { - children: ReactNode; - active?: boolean; - disabled?: boolean; - compact?: boolean; - title: string; - onClick: () => void; - ariaPressed?: boolean; -}) { - return ( - - - - ); -} function SectionLabel({ children, diff --git a/src/components/library/PromptTemplatesTab.tsx b/src/components/library/PromptTemplatesTab.tsx index feb56a5..9a6510d 100644 --- a/src/components/library/PromptTemplatesTab.tsx +++ b/src/components/library/PromptTemplatesTab.tsx @@ -62,7 +62,7 @@ export function PromptTemplatesTab() { const contextBadgeClass = (context: 'stage' | 'audit' | 'persona') => { if (context === 'audit') return 'bg-editorial-warning/20 text-editorial-warning'; - if (context === 'persona') return 'bg-purple-500/15 text-purple-400'; + if (context === 'persona') return 'bg-editorial-textbox/60 text-editorial-muted'; return 'bg-editorial-accent/20 text-editorial-accent'; }; diff --git a/src/components/pipeline/PipelineConfig.tsx b/src/components/pipeline/PipelineConfig.tsx index b76c2be..5b0bf37 100644 --- a/src/components/pipeline/PipelineConfig.tsx +++ b/src/components/pipeline/PipelineConfig.tsx @@ -20,6 +20,7 @@ import { useOperationLogStore } from '../../stores/operationLogStore'; import { ReasoningPicker } from '../models/ReasoningPicker'; import { PromptPreviewTab } from './PromptPreviewTab'; import { canRefineWithProvider, formatProviderModelLabel, useProviderKeyStatus } from '../../hooks/useProviderKeyStatus'; +import { IconButton } from '../ui'; export type ConfigSection = 'settings' | 'translation' | 'audit' | 'glossary' | 'preview'; @@ -814,12 +815,6 @@ export function PipelineConfig({ preview: t('pipeline.tabPreview'), }; - const tabBtnCls = (active: boolean) => - `rounded-full border p-2.5 transition-colors focus:outline-none focus-visible:ring-2 focus-visible:ring-editorial-accent ${ - active - ? 'border-editorial-accent bg-editorial-accent text-white' - : 'border-editorial-border text-editorial-muted hover:border-editorial-accent/40 hover:text-editorial-accent' - }`; return (
@@ -846,8 +841,8 @@ export function PipelineConfig({ ))} - + setConfig((prev) => ({ ...prev, @@ -902,7 +903,10 @@ export function PipelineConfig({ tone={activeTab === 'settings' ? 'accent' : 'default'} onClick={() => setActiveTab('settings')} title={t('pipeline.tabSettings')} - ariaPressed={activeTab === 'settings'} + id="pconfig-tab-settings" + role="tab" + aria-selected={activeTab === 'settings'} + aria-controls="pconfig-panel-settings" > @@ -913,7 +917,10 @@ export function PipelineConfig({ tone={activeTab === 'translation' ? 'accent' : 'default'} onClick={() => setActiveTab('translation')} title={t('pipeline.tabTranslation')} - ariaPressed={activeTab === 'translation'} + id="pconfig-tab-translation" + role="tab" + aria-selected={activeTab === 'translation'} + aria-controls="pconfig-panel-translation" > @@ -923,7 +930,10 @@ export function PipelineConfig({ tone={activeTab === 'audit' ? 'accent' : 'default'} onClick={() => setActiveTab('audit')} title={t('pipeline.tabAudit')} - ariaPressed={activeTab === 'audit'} + id="pconfig-tab-audit" + role="tab" + aria-selected={activeTab === 'audit'} + aria-controls="pconfig-panel-audit" > @@ -933,7 +943,10 @@ export function PipelineConfig({ tone={activeTab === 'glossary' ? 'accent' : 'default'} onClick={() => setActiveTab('glossary')} title={t('pipeline.tabGlossary')} - ariaPressed={activeTab === 'glossary'} + id="pconfig-tab-glossary" + role="tab" + aria-selected={activeTab === 'glossary'} + aria-controls="pconfig-panel-glossary" > @@ -942,7 +955,10 @@ export function PipelineConfig({ tone={activeTab === 'preview' ? 'accent' : 'default'} onClick={() => setActiveTab('preview')} title={t('pipeline.tabPreview')} - ariaPressed={activeTab === 'preview'} + id="pconfig-tab-preview" + role="tab" + aria-selected={activeTab === 'preview'} + aria-controls="pconfig-panel-preview" > @@ -1440,7 +1456,9 @@ export function PipelineConfig({ {/* ── PREVIEW ── */} {activeTab === 'preview' && ( - +
+ +
)} diff --git a/src/components/ui/IconButton.tsx b/src/components/ui/IconButton.tsx index a213093..28b0325 100644 --- a/src/components/ui/IconButton.tsx +++ b/src/components/ui/IconButton.tsx @@ -1,5 +1,5 @@ import { cva, type VariantProps } from 'class-variance-authority'; -import type { ReactNode } from 'react'; +import type { ButtonHTMLAttributes, ReactNode } from 'react'; import { Tooltip } from './Tooltip'; const iconButton = cva( @@ -27,15 +27,14 @@ const iconButton = cva( export type IconButtonTone = NonNullable['tone']>; export type IconButtonSize = NonNullable['size']>; -interface IconButtonProps extends VariantProps { - onClick?: () => void; - children: ReactNode; - title: string; - ariaLabel?: string; - disabled?: boolean; - ariaPressed?: boolean; - className?: string; -} +type IconButtonProps = VariantProps & + Omit, 'onClick' | 'aria-pressed' | 'aria-label'> & { + onClick?: () => void; + children: ReactNode; + title: string; + ariaLabel?: string; + ariaPressed?: boolean; + }; export function IconButton({ onClick, @@ -47,6 +46,7 @@ export function IconButton({ size, tone, className, + ...rest }: IconButtonProps) { return ( @@ -57,6 +57,7 @@ export function IconButton({ aria-label={ariaLabel ?? title} aria-pressed={ariaPressed} className={iconButton({ size, tone, className })} + {...rest} > {children}